Class: Insika::SettingsStore

Inherits:
Object
  • Object
show all
Defined in:
lib/insika/settings_store.rb

Overview

GENERAL deploy settings: timeouts, streaming and compaction. A single record in the ConfigStore (scope "settings", key "general"). A read always returns the DEFAULTS overlaid by whatever was authored — so a brand-new deploy (empty store) already responds with coherent config, and the Studio only persists the delta. Shallow merge at the top, deep in compaction (sub-hash).

Not to be confused with the transport CONFIG (bind/port/token, ENV+freeze at boot): this is editable RUNTIME config, durable in the same backend.

Constant Summary collapse

SCOPE =
"settings"
KEY =
"general"
SCHEMA_VERSION =

STRICT config, settings layer (— "no silent config compat: every schema migration explicit"). The settings record carries a schema_version; every shape change is a numbered migration here, applied ONLY by the explicit migrate! (Studio settings saves never silently reinterpret old-shaped data). v1 is the baseline: a pre-versioning record (no schema_version) reads as 0 and insika doctor --fix stamps it to 1. Add a real change as MIGRATIONS = proc and bump SCHEMA_VERSION.

1
MIGRATIONS =

target_version(Integer) => ->(record_hash){ migrated_hash }

{}.freeze
DEFAULTS =
{
  "schema_version" => SCHEMA_VERSION,
  "streaming" => true,
  "request_timeout" => 120,
  "max_retries" => 2,
  "turn_timeout" => 120,
  "tool_timeout" => 30,
  "compaction" => { "enabled" => false, "keep_last" => 20 },
  # LLM config v2. Platform-wide model layer, resolved by the
  # ModelResolver under an agent that pins no model of its own:
  #   default_model/default_provider -> the platform default (Chat > Agent > HERE)
  #   fallback_models -> ordered chain ["provider/model" | "model", ...] tried
  #                      when the primary is NOT a user pin (source semantics)
  #   utility_model   -> slot for cheap internal tasks (titles, distillation,
  #                      compaction); reserved for later wiring.
  "default_model" => nil,
  "default_provider" => nil,
  "fallback_models" => [],
  "utility_model" => nil,
  # Reasoning control (4-layer: Chat > Agent > Model > Global). `thinking`
  # is the GLOBAL default (off/on/low/medium/high; nil = provider default);
  # `model_params` is the PER-MODEL layer, a map "<provider/model>"|"<model>" ->
  # { "thinking" => ... }. Both resolved by the ModelResolver.
  "thinking" => nil,
  "model_params" => {},
  # Evals (panel by). The GRADERS are platform config, so
  # the operator picks them in the Studio instead of remembering a CLI flag:
  #   judges        -> [{ "model" =>, "provider" => }, …]. [] = deterministic
  #                    asserts only (rubric'd cases read as judge_pending).
  #   aggregate     -> median | mean | min — how the panel's scores become the one
  #                    the report and the baseline read.
  #   min_agreement -> fraction of judges that must pass for the case to pass.
  #   quorum        -> samples per judge (variance), on top of the panel.
  #   tolerance     -> max judge-score drop before it counts as a regression.
  # Additive key: reads overlay DEFAULTS, so no numbered migration is due (only a
  # later SHAPE change to this key would earn one).
  "evals" => {
    "judges" => [],
    "aggregate" => "median",
    "min_agreement" => 0.5,
    "quorum" => 1,
    "tolerance" => 0.05
  },
  # Edge limits — the platform layer of the EdgeLimiter.
  # nil/0 = off (opt-in). chat_rate_limit = turn attempts per chat per
  # chat_rate_window (s); agent_token_ceiling = total tokens per agent per
  # agent_token_window (s). limit_response overrides the safe reply.
  # Per-agent overrides live in profile.limits (same keys, sans windows).
  "edge" => {
    "chat_rate_limit" => nil,
    "chat_rate_window" => 60,
    "agent_token_ceiling" => nil,
    "agent_token_window" => 86_400,
    "limit_response" => nil
  }
}.freeze

Instance Method Summary collapse

Constructor Details

#initialize(config_store:) ⇒ SettingsStore

Returns a new instance of SettingsStore.



83
84
85
# File 'lib/insika/settings_store.rb', line 83

def initialize(config_store:)
  @cs = config_store
end

Instance Method Details

#getObject

-> Hash (defaults overlaid by the authored values). String keys (Store contract).



88
89
90
# File 'lib/insika/settings_store.rb', line 88

def get
  deep_merge(DEFAULTS, stored)
end

#migrate!Object

Applies the pending migrations EXPLICITLY (in order) and stamps the version. No-op when fresh or already current. -> resulting schema_version (Integer).



120
121
122
123
124
125
126
127
128
129
130
131
# File 'lib/insika/settings_store.rb', line 120

def migrate!
  data = stored
  return SCHEMA_VERSION if data.empty?

  pending_migrations.each do |version|
    migration = MIGRATIONS[version]
    data = stringify(migration.call(data)) if migration
  end
  data["schema_version"] = SCHEMA_VERSION
  @cs.put(SCOPE, KEY, data)
  SCHEMA_VERSION
end

#pending_migrationsObject

Which numbered migrations still need to run. [] when fresh or already current.



111
112
113
114
115
116
# File 'lib/insika/settings_store.rb', line 111

def pending_migrations
  from = stored_schema_version
  return [] if from.nil? || from >= SCHEMA_VERSION

  ((from + 1)..SCHEMA_VERSION).to_a
end

#stored_schema_versionObject

RAW persisted schema version (bypasses the DEFAULTS overlay, which would always report the current one). nil = no settings persisted yet (fresh deploy — nothing to migrate); an Integer otherwise, 0 for a pre-versioning record.



103
104
105
106
107
108
# File 'lib/insika/settings_store.rb', line 103

def stored_schema_version
  raw = stored
  return nil if raw.empty?

  Integer(raw["schema_version"] || 0)
end

#update(patch) ⇒ Object

Merges the patch over the current one and persists. -> Hash (resulting settings). Unknown keys are preserved (the Studio decides the screen's schema).



94
95
96
97
98
# File 'lib/insika/settings_store.rb', line 94

def update(patch)
  merged = deep_merge(get, stringify(patch || {}))
  @cs.put(SCOPE, KEY, merged)
  merged
end