insika 0.8.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (276) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +312 -2
  3. data/README.md +10 -6
  4. data/bin/insika +4 -1
  5. data/bin/insika-router +0 -1
  6. data/docs/AGENTS.md +142 -18
  7. data/docs/API.md +73 -0
  8. data/docs/ARCHITECTURE.md +56 -50
  9. data/docs/ARTIFACTS.md +33 -3
  10. data/docs/BENCHMARK.md +50 -7
  11. data/docs/CHANNELS.md +19 -2
  12. data/docs/CONTEXT.md +96 -27
  13. data/docs/CROSS-HARNESS-BENCH.md +157 -0
  14. data/docs/DEPLOY.md +70 -13
  15. data/docs/EMBEDDING.md +10 -0
  16. data/docs/EVALS.md +129 -10
  17. data/docs/FACTS.md +12 -6
  18. data/docs/HARVEST.md +8 -3
  19. data/docs/KNOWLEDGE.md +14 -0
  20. data/docs/MEDIA.md +6 -2
  21. data/docs/OBSERVABILITY.md +164 -28
  22. data/docs/PERFORMANCE.md +110 -0
  23. data/docs/PLUGINS.md +4 -4
  24. data/docs/POLICY.md +26 -14
  25. data/docs/RELEASING.md +9 -0
  26. data/docs/ROUTER.md +7 -0
  27. data/docs/RUBYLLM_2_MIGRATION.md +307 -0
  28. data/docs/SECURITY.md +34 -1
  29. data/docs/SHARED-CONVERSATIONS.md +40 -0
  30. data/docs/SKILLS.md +5 -3
  31. data/docs/STUDIO.md +122 -0
  32. data/docs/TOOLS.md +238 -42
  33. data/docs/operate.md +3 -0
  34. data/docs/prompts/ADD-TOOL.md +12 -2
  35. data/docs/prompts/DIAGNOSE-TURN.md +3 -0
  36. data/docs/prompts/GO-LIVE.md +3 -1
  37. data/lib/insika/agent_defaults.rb +140 -0
  38. data/lib/insika/agent_file_store.rb +12 -0
  39. data/lib/insika/agent_profile.rb +99 -17
  40. data/lib/insika/artifact_signing.rb +11 -0
  41. data/lib/insika/channels/web/widget.js +33 -0
  42. data/lib/insika/channels/web.rb +5 -2
  43. data/lib/insika/chat_builder.rb +124 -28
  44. data/lib/insika/checkpoint.rb +6 -1
  45. data/lib/insika/checkpoint_store.rb +59 -7
  46. data/lib/insika/commands/agent_payload.rb +3 -3
  47. data/lib/insika/commands/delete_kit.rb +26 -0
  48. data/lib/insika/commands/gate_harvest.rb +3 -0
  49. data/lib/insika/commands/review_guardrail_flag.rb +38 -0
  50. data/lib/insika/commands/run_distillation.rb +5 -8
  51. data/lib/insika/commands/run_harvest.rb +17 -1
  52. data/lib/insika/commands/seed_session.rb +128 -0
  53. data/lib/insika/commands/send_message.rb +9 -3
  54. data/lib/insika/commands/update_settings.rb +23 -1
  55. data/lib/insika/commands/write_kit.rb +27 -0
  56. data/lib/insika/compaction.rb +33 -7
  57. data/lib/insika/config_store.rb +87 -7
  58. data/lib/insika/context/builder.rb +29 -9
  59. data/lib/insika/context/priority.rb +5 -0
  60. data/lib/insika/context/provider.rb +7 -2
  61. data/lib/insika/context/providers/briefing.rb +11 -8
  62. data/lib/insika/context/providers/fence_notice.rb +27 -0
  63. data/lib/insika/context/providers/knowledge.rb +32 -6
  64. data/lib/insika/context/providers/memory.rb +61 -8
  65. data/lib/insika/context/providers/pending_confirmation.rb +52 -0
  66. data/lib/insika/context/providers/prompt.rb +17 -4
  67. data/lib/insika/context/providers/session.rb +46 -6
  68. data/lib/insika/context/reranker.rb +75 -0
  69. data/lib/insika/distill.rb +22 -13
  70. data/lib/insika/doctor.rb +142 -4
  71. data/lib/insika/dsl/runtime.rb +37 -4
  72. data/lib/insika/dsl/server_boot.rb +4 -12
  73. data/lib/insika/dsl.rb +19 -5
  74. data/lib/insika/edge_limiter.rb +27 -16
  75. data/lib/insika/egress_guard.rb +11 -3
  76. data/lib/insika/env_schema.rb +33 -73
  77. data/lib/insika/errors.rb +1 -0
  78. data/lib/insika/evals/assertions.rb +259 -18
  79. data/lib/insika/evals/golden.rb +230 -4
  80. data/lib/insika/evals/judge.rb +19 -10
  81. data/lib/insika/evals/moderator_compare.rb +47 -0
  82. data/lib/insika/evals/report.rb +61 -9
  83. data/lib/insika/evals/runner.rb +64 -4
  84. data/lib/insika/evals/simulator.rb +21 -3
  85. data/lib/insika/evals/transport.rb +317 -15
  86. data/lib/insika/event_stream.rb +63 -7
  87. data/lib/insika/evidence.rb +105 -17
  88. data/lib/insika/executor.rb +698 -164
  89. data/lib/insika/fence.rb +96 -0
  90. data/lib/insika/golden_store.rb +3 -0
  91. data/lib/insika/harvest.rb +26 -14
  92. data/lib/insika/harvest_engine.rb +1 -0
  93. data/lib/insika/harvest_store.rb +12 -0
  94. data/lib/insika/http_client.rb +87 -4
  95. data/lib/insika/kits.rb +54 -0
  96. data/lib/insika/knowledge.rb +34 -20
  97. data/lib/insika/llm_configurator.rb +46 -4
  98. data/lib/insika/llm_provider_store.rb +1 -3
  99. data/lib/insika/llm_trace_store.rb +54 -0
  100. data/lib/insika/mcp_client.rb +27 -63
  101. data/lib/insika/mcp_live_tool.rb +55 -9
  102. data/lib/insika/mcp_store.rb +37 -3
  103. data/lib/insika/mcp_tool_registry.rb +76 -26
  104. data/lib/insika/media.rb +38 -25
  105. data/lib/insika/memory_store.rb +16 -3
  106. data/lib/insika/memory_trim.rb +60 -0
  107. data/lib/insika/middleware.rb +12 -3
  108. data/lib/insika/model_metrics_store.rb +285 -0
  109. data/lib/insika/model_registry.rb +110 -0
  110. data/lib/insika/model_resolver.rb +1 -1
  111. data/lib/insika/model_selection.rb +14 -21
  112. data/lib/insika/model_visible.rb +17 -6
  113. data/lib/insika/onboarding.rb +1 -0
  114. data/lib/insika/outcome_store.rb +1 -1
  115. data/lib/insika/overlay_tool_registry.rb +30 -2
  116. data/lib/insika/pack_importer.rb +54 -9
  117. data/lib/insika/packaging.rb +14 -5
  118. data/lib/insika/pending_action_store.rb +45 -18
  119. data/lib/insika/plugin/loader.rb +4 -2
  120. data/lib/insika/prefix_fingerprint.rb +32 -27
  121. data/lib/insika/profile_source.rb +5 -0
  122. data/lib/insika/provider_error_classifier.rb +18 -3
  123. data/lib/insika/queue_policy.rb +7 -2
  124. data/lib/insika/refinement/proposer.rb +42 -17
  125. data/lib/insika/reliability.rb +12 -3
  126. data/lib/insika/router/app.rb +14 -5
  127. data/lib/insika/router/session_key.rb +35 -5
  128. data/lib/insika/safety/config.rb +20 -3
  129. data/lib/insika/safety/corpus.rb +10 -3
  130. data/lib/insika/safety/factory.rb +39 -1
  131. data/lib/insika/safety/input_guardrail.rb +21 -1
  132. data/lib/insika/safety/judge_moderator.rb +70 -0
  133. data/lib/insika/safety/output_validator.rb +29 -5
  134. data/lib/insika/safety/prompt_echo.rb +89 -0
  135. data/lib/insika/schema_guard.rb +53 -76
  136. data/lib/insika/server/app.rb +58 -7
  137. data/lib/insika/server/rack_app.rb +2 -0
  138. data/lib/insika/server/responses.rb +50 -5
  139. data/lib/insika/session_actor.rb +58 -28
  140. data/lib/insika/session_store.rb +163 -11
  141. data/lib/insika/settings_store.rb +57 -2
  142. data/lib/insika/shared_conversations.rb +305 -0
  143. data/lib/insika/skill_catalog.rb +28 -2
  144. data/lib/insika/skill_store.rb +6 -0
  145. data/lib/insika/soak/runner.rb +1 -1
  146. data/lib/insika/spoken_transcript.rb +31 -0
  147. data/lib/insika/steer_injector.rb +7 -48
  148. data/lib/insika/store.rb +29 -1
  149. data/lib/insika/stores/json_value.rb +37 -0
  150. data/lib/insika/stores/memory.rb +22 -35
  151. data/lib/insika/stores/postgres.rb +295 -0
  152. data/lib/insika/stores/sqlite.rb +257 -43
  153. data/lib/insika/stores/turn_counter.rb +25 -0
  154. data/lib/insika/studio/app.rb +1459 -163
  155. data/lib/insika/studio/assets/dist/application.css +1 -1
  156. data/lib/insika/studio/assets/dist/application.js +25 -25
  157. data/lib/insika/studio/assets/dist/fonts/ibm-plex-mono-latin-400-normal.woff2 +0 -0
  158. data/lib/insika/studio/assets/dist/fonts/ibm-plex-mono-latin-500-normal.woff2 +0 -0
  159. data/lib/insika/studio/assets/dist/fonts/ibm-plex-mono-latin-600-normal.woff2 +0 -0
  160. data/lib/insika/studio/assets/dist/fonts/ibm-plex-sans-latin-wght-normal.woff2 +0 -0
  161. data/lib/insika/studio/forms.rb +189 -12
  162. data/lib/insika/studio/nav_icons.rb +3 -0
  163. data/lib/insika/studio/views/_agent_config_retrieval.erb +73 -0
  164. data/lib/insika/studio/views/_agent_picker.erb +15 -0
  165. data/lib/insika/studio/views/_agent_tab_config.erb +83 -243
  166. data/lib/insika/studio/views/_agent_tab_history.erb +56 -1
  167. data/lib/insika/studio/views/_agent_tab_performance.erb +34 -0
  168. data/lib/insika/studio/views/_agent_tab_prompts.erb +7 -9
  169. data/lib/insika/studio/views/_agent_tab_skills.erb +16 -7
  170. data/lib/insika/studio/views/_agent_tab_tools.erb +90 -0
  171. data/lib/insika/studio/views/_agents_master.erb +8 -12
  172. data/lib/insika/studio/views/_cfg_alerts.erb +4 -0
  173. data/lib/insika/studio/views/_cfg_budget.erb +20 -0
  174. data/lib/insika/studio/views/_cfg_distill.erb +26 -0
  175. data/lib/insika/studio/views/_cfg_followup.erb +30 -0
  176. data/lib/insika/studio/views/_cfg_grounding.erb +21 -0
  177. data/lib/insika/studio/views/_cfg_guardrails.erb +47 -0
  178. data/lib/insika/studio/views/_cfg_harvest.erb +35 -0
  179. data/lib/insika/studio/views/_cfg_reliability.erb +32 -0
  180. data/lib/insika/studio/views/_chats_master.erb +66 -0
  181. data/lib/insika/studio/views/_customers_master.erb +37 -0
  182. data/lib/insika/studio/views/_empty.erb +15 -0
  183. data/lib/insika/studio/views/_facts_master.erb +42 -0
  184. data/lib/insika/studio/views/_harvest_candidate_detail.erb +14 -0
  185. data/lib/insika/studio/views/_identity.erb +16 -0
  186. data/lib/insika/studio/views/_kpi.erb +17 -0
  187. data/lib/insika/studio/views/_message.erb +40 -2
  188. data/lib/insika/studio/views/_model_chart.erb +82 -0
  189. data/lib/insika/studio/views/_refinement_master.erb +38 -0
  190. data/lib/insika/studio/views/{tool_edit.erb → _tool_edit.erb} +38 -11
  191. data/lib/insika/studio/views/_tool_users.erb +17 -0
  192. data/lib/insika/studio/views/_why.erb +53 -0
  193. data/lib/insika/studio/views/agent_detail.erb +17 -10
  194. data/lib/insika/studio/views/agents.erb +28 -16
  195. data/lib/insika/studio/views/approvals.erb +16 -11
  196. data/lib/insika/studio/views/artifact.erb +20 -7
  197. data/lib/insika/studio/views/artifacts.erb +19 -14
  198. data/lib/insika/studio/views/chats.erb +22 -29
  199. data/lib/insika/studio/views/customer.erb +88 -43
  200. data/lib/insika/studio/views/customers.erb +16 -26
  201. data/lib/insika/studio/views/evals.erb +49 -31
  202. data/lib/insika/studio/views/fact_detail.erb +114 -0
  203. data/lib/insika/studio/views/facts.erb +15 -116
  204. data/lib/insika/studio/views/followups.erb +63 -52
  205. data/lib/insika/studio/views/funnel.erb +104 -43
  206. data/lib/insika/studio/views/harvest.erb +85 -59
  207. data/lib/insika/studio/views/home.erb +138 -23
  208. data/lib/insika/studio/views/inbox.erb +36 -0
  209. data/lib/insika/studio/views/kits.erb +176 -0
  210. data/lib/insika/studio/views/knowledge.erb +92 -80
  211. data/lib/insika/studio/views/layout.erb +42 -11
  212. data/lib/insika/studio/views/live.erb +45 -0
  213. data/lib/insika/studio/views/mcp.erb +43 -13
  214. data/lib/insika/studio/views/models.erb +187 -0
  215. data/lib/insika/studio/views/parity.erb +38 -32
  216. data/lib/insika/studio/views/playground.erb +13 -9
  217. data/lib/insika/studio/views/refinement.erb +15 -224
  218. data/lib/insika/studio/views/refinement_detail.erb +243 -0
  219. data/lib/insika/studio/views/session.erb +153 -118
  220. data/lib/insika/studio/views/settings.erb +86 -22
  221. data/lib/insika/studio/views/skills.erb +53 -19
  222. data/lib/insika/studio/views/system_files.erb +7 -7
  223. data/lib/insika/studio/views/task.erb +45 -15
  224. data/lib/insika/studio/views/tasks.erb +35 -18
  225. data/lib/insika/studio/views/tools.erb +62 -73
  226. data/lib/insika/task_actor.rb +4 -1
  227. data/lib/insika/task_store.rb +67 -1
  228. data/lib/insika/telemetry/pricing.rb +16 -14
  229. data/lib/insika/telemetry/recorder.rb +115 -9
  230. data/lib/insika/telemetry/ruby_llm_instrumenter.rb +110 -0
  231. data/lib/insika/telemetry.rb +12 -2
  232. data/lib/insika/templates/daily-digest/README.md +13 -0
  233. data/lib/insika/templates/daily-digest/agent.rb +22 -11
  234. data/lib/insika/testing/store_contract.rb +65 -0
  235. data/lib/insika/text_tool_call.rb +66 -0
  236. data/lib/insika/tick.rb +7 -1
  237. data/lib/insika/token_store.rb +5 -0
  238. data/lib/insika/tool_assembly.rb +21 -13
  239. data/lib/insika/tool_batch.rb +1 -1
  240. data/lib/insika/tool_catalog.rb +12 -2
  241. data/lib/insika/tool_definition.rb +82 -13
  242. data/lib/insika/tool_envelope.rb +266 -29
  243. data/lib/insika/tool_manifest.rb +1 -0
  244. data/lib/insika/tool_store.rb +12 -4
  245. data/lib/insika/tool_trace_store.rb +15 -8
  246. data/lib/insika/tool_usage_report.rb +14 -4
  247. data/lib/insika/tools/a2a_remote.rb +1 -1
  248. data/lib/insika/tools/agent_enum.rb +1 -1
  249. data/lib/insika/tools/confirm_pending.rb +112 -0
  250. data/lib/insika/tools/data_defined_tool.rb +24 -15
  251. data/lib/insika/tools/generate_image.rb +2 -2
  252. data/lib/insika/tools/load_knowledge.rb +7 -3
  253. data/lib/insika/tools/load_skill.rb +3 -3
  254. data/lib/insika/tools/present.rb +115 -0
  255. data/lib/insika/tools/remember.rb +7 -2
  256. data/lib/insika/tools/run_persona_eval.rb +9 -6
  257. data/lib/insika/tools/save_artifact.rb +16 -6
  258. data/lib/insika/tools/schedule_followup.rb +3 -3
  259. data/lib/insika/tools/stuck_signal.rb +3 -3
  260. data/lib/insika/tools/subagent.rb +5 -5
  261. data/lib/insika/tools/subagents.rb +3 -3
  262. data/lib/insika/tools/tool_search.rb +11 -7
  263. data/lib/insika/tools/tts.rb +2 -2
  264. data/lib/insika/tools/update_briefing.rb +4 -4
  265. data/lib/insika/turn_output.rb +2 -2
  266. data/lib/insika/turn_state.rb +31 -1
  267. data/lib/insika/turn_timing.rb +51 -14
  268. data/lib/insika/version.rb +1 -1
  269. data/lib/insika/wiring/graph.rb +88 -16
  270. data/lib/insika/wiring/graph_chat.rb +4 -0
  271. data/lib/insika/workflow.rb +25 -64
  272. data/lib/insika.rb +23 -3
  273. metadata +74 -13
  274. data/lib/insika/commands/import_mcp_tools.rb +0 -48
  275. data/lib/insika/mcp_http_client.rb +0 -67
  276. data/lib/insika/mcp_tool_ingestor.rb +0 -165
data/docs/CHANNELS.md CHANGED
@@ -149,16 +149,24 @@ Content-Type: application/json
149
149
  { "agent": "support", "session_id": "web:8f3c…", "message": "cadê meu pedido?" }
150
150
  ```
151
151
 
152
- The reply is SSE on that same connection — four frame types, and an unknown one is
152
+ The reply is SSE on that same connection — five frame types, and an unknown one is
153
153
  safe to ignore:
154
154
 
155
155
  ```
156
- event: delta data: {"delta":"Seu pedido "} the answer, token by token
156
+ event: delta data: {"delta":"Seu pedido chegou."} the published answer
157
157
  event: working data: {"name":"order_status"} a tool is running
158
+ event: ui data: {"component":"product_cards","title":"Chocolate","items":[{"id":"SKU-1","url":"https://shop.example/1","caption":"Dark chocolate"}]}
159
+ a presentation tool picked cards to show
158
160
  event: done data: {} the turn ended
159
161
  event: error data: {"message":"…"} it ended badly
160
162
  ```
161
163
 
164
+ The shipped widget renders `ui` as a plain list of caption + link under the
165
+ conversation (only an `http(s)` url becomes a link; any other scheme renders as
166
+ text); a host page that wants real cards restyles or replaces that — the
167
+ protocol is the frame. An empty selection sends `items: []`. Unlike `/v1/responses`,
168
+ the web frame omits `count` and `dropped`. See [presentation tools](TOOLS.md#presentation-tools-the-model-picks-ids-the-engine-shows-the-cards).
169
+
162
170
  **The engine issues the session id and the client never proposes one.** `POST
163
171
  /messages` with an id nobody minted is a `404`, never a new conversation: on an
164
172
  anonymous endpoint, create-on-write means anyone who guesses an id can read someone
@@ -315,6 +323,15 @@ stays internal unless the agent opts in. That contract is
315
323
  [the edge contract](ARCHITECTURE.md#what-crosses-the-edge), and it is why you can
316
324
  forward `content` straight to the customer.
317
325
 
326
+ The last balloon may also carry `attachments`: objects with `type`, `url`, `caption`
327
+ and an optional `id`,
328
+ the cards an evidence tool returned (see [Evidence](TOOLS.md#evidence-the-lean-envelope-and-grounding)).
329
+ When the turn called a **presentation tool**, the list is exactly the cards the model
330
+ selected, in call order, each also carrying the `component` and `title` of that call;
331
+ otherwise it is every card the turn's evidence tools returned. An empty presentation
332
+ selection suppresses that automatic fallback. Ignore the key if the consumer only
333
+ supports text.
334
+
318
335
  ### Delivery policy
319
336
 
320
337
  A relay declares **how the outbox flushes** — a property of the surface
data/docs/CONTEXT.md CHANGED
@@ -23,6 +23,7 @@ into a deterministic prompt:
23
23
  | Provider | Block | Priority | Layer | Notes |
24
24
  |----------|-------|:--------:|-------|-------|
25
25
  | **Identity** | system | **100 — pinned** | identity | The agent's prompt files (global system files first). Never cut. |
26
+ | **Fence notice** | system | **99 — pinned** | identity | Fixed instruction to treat contextual material as data; only with `fencing` on. |
26
27
  | **Skills** | `<available_skills>` | 80 | identity | Level-1 skill list, minus whatever is already eager — see [Skills](SKILLS.md). |
27
28
  | **Tool search** | `<available_tools>` | 70 | identity | Level-1 list of deferred tools — see [Tools](TOOLS.md). |
28
29
  | **Skill trigger** | `<active_skill>` | 85 | volatile | Level-2 bodies: the agent's `skills_eager` set, plus the ones whose `triggers:` match the message — see [Skills](SKILLS.md). |
@@ -86,6 +87,28 @@ Compaction does not replace the budget: eviction stays as the hard backstop
86
87
  for a single oversized turn. Tune `context_budget` and keep the identity lean
87
88
  regardless.
88
89
 
90
+ #### Native Responses compaction
91
+
92
+ Set `compaction.mode` to `"native"` alongside `enabled: true` to use RubyLLM's
93
+ manual compaction endpoint on supported OpenAI, Azure and xAI Responses chats.
94
+ The default is `"summary"`, described above. Native mode uses the turn's resolved
95
+ model and credential context; `compaction.model`, `prompt` and `utility_model`
96
+ do not apply. Unsupported providers remain uncompacted; there is no second
97
+ summary request or automatic switch to another provider.
98
+
99
+ After the turn commits, a fresh chat compacts the stored, filtered prefix.
100
+ The original messages remain stored. Opaque state is separate from the spoken
101
+ transcript and is replayed only with the same provider, model and protocol.
102
+ Fallbacks and protocol changes restore the original prefix. Checkpoints retain
103
+ both alternatives. A native marker is also skipped if another context provider
104
+ has already supplied history, because Responses would replace that history.
105
+
106
+ The budget conservatively counts the original prefix as well as the native state,
107
+ and can evict this entire unit. This version reduces provider input when that unit
108
+ fits; it does not increase local context-budget capacity. Each compaction rebuilds
109
+ the whole stored prefix rather than chaining opaque state. Native usage is reported
110
+ on `context_compacted`; the opaque payload is not included in that event.
111
+
89
112
  One cheap half **is** wired, opt-in per agent: `tool_output_compression` (DSL
90
113
  `tool_output_compression`, or `"tool_output_compression": true` in the pack).
91
114
  When on, byte-identical repeated **tool results** in the replayed history
@@ -101,19 +124,60 @@ budget first.
101
124
  > and the budget verdict (`used / cap`, evicted sources). Counts only, never
102
125
  > fragment content.
103
126
 
127
+ ### Repairing stored assistant context
128
+
129
+ An operator may attach a string `context_content` to a stored plain assistant
130
+ message. Session replay uses that text while preserving the original `content`
131
+ for display and audit. User messages, tool results, assistant tool calls, explicit
132
+ history and checkpoint replay ignore this field. Existing compaction summaries
133
+ are not rewritten. This is a targeted data repair, not an automatic tool-markup
134
+ parser; text is never converted into tool execution.
135
+
104
136
  ## Memory
105
137
 
106
138
  With `memory` enabled, an agent gains a built-in `remember` tool for durable
107
139
  facts, and those facts (plus recent notes) are injected back into the prompt on
108
140
  later turns — **including turns in a different session**. Memory is scoped per
109
- agent, per `(tenant, customer)` when the message carries a `customer`, and per
110
- session otherwise — a session's own memory lives in a marked `memory:chat:<session id>`
111
- cell, never a bare one, so the Customers drill cannot read a conversation as a
112
- customer. This is distinct from *session history*, which is the transcript of one
141
+ `(tenant, customer)` when the message carries a `customer`. Without a customer,
142
+ it uses the tenant's cell; without either, it uses the session's marked
143
+ `memory:chat:<session id>` cell. A fresh session therefore does not isolate
144
+ memory when it still carries the same tenant. This is distinct from *session history*, which is the transcript of one
113
145
  conversation; memory is the small set of facts that should outlive any single
114
146
  conversation. Facts and notes are editable from the Studio agent page. See
115
147
  [`examples/memory/`](https://github.com/guizaols/insika/tree/main/examples/memory/) for a runnable cross-session example.
116
148
 
149
+ In Studio, open **Agents → select an agent → Configuration → Memory & knowledge**.
150
+ Memory, knowledge learning and knowledge retrieval have separate switches. Each
151
+ source has its own optional rerank provider, model, candidate limit and timeout;
152
+ reranking is off by default. Disabling rerank keeps memory and knowledge working
153
+ with their usual retrieval and clears only that source's rerank configuration.
154
+ The memory selection count applies only while memory reranking is on. The knowledge
155
+ selection count also applies without reranking. Provider credentials are configured
156
+ separately, never in this form. Changes apply to this agent's future turns.
157
+
158
+ To select a smaller memory block for each message, keep `"memory": true` and add
159
+ `"memory_retrieval": {"top_k": 3, "rerank": {"provider": "cohere", "model": "rerank-v3.5", "candidate_limit": 20, "timeout_seconds": 2}}`
160
+ to the agent profile or pack. The engine first reads only the current memory cell,
161
+ ranks active facts and considers the newest `candidate_limit` notes by word overlap and recency,
162
+ then sends at most `candidate_limit` redacted candidates to the configured reranker.
163
+ An empty message or reranker failure keeps the usual full facts and ten recent notes.
164
+ Older notes outside that read window remain available in the store and Studio, but
165
+ cannot be selected for the turn.
166
+
167
+ Knowledge can use the same opt-in reranker: set `knowledge.retrieve: true`,
168
+ `knowledge.top_k: 3`, and `knowledge.rerank` to the `provider`, `model`,
169
+ `candidate_limit`, and `timeout_seconds` object above. The Scan index first
170
+ finds matching concepts in the agent and tenant scope. The reranker can reorder
171
+ only those candidates; a paraphrase with no matching terms still finds nothing.
172
+ Knowledge sends the candidate name, description, and body; memory sends the
173
+ fact key/value or note text. The built-in PII and secret detector redacts
174
+ recognized patterns before transmission, but it cannot recognize every secret.
175
+ Use a provider and credentials appropriate for this external text transfer.
176
+ Reranker failure keeps lexical knowledge matches or the usual memory block.
177
+ The reranker adds a provider call and may add cost; missing provider usage is
178
+ reported as unknown, not zero. Keep reranking off until an agent's own live
179
+ evaluation shows better answers within its latency and cost budget.
180
+
117
181
  Facts carry **provenance metadata**: every fact record stores `origin`
118
182
  (who wrote it — `"engine"`, `"operator"`, `"legacy"` or `"distilled"`),
119
183
  `created_at` / `updated_at` timestamps, and an optional `expires_at` (ISO8601) —
@@ -174,7 +238,7 @@ Attention is strongest at the end of the context: a goal stated only at the top
174
238
  is the first thing a 30-call turn forgets. So the recitation was **moved** there,
175
239
  not copied — the head never repeats it, and the turn pays for it once. It rides
176
240
  as a `user` message, like every other engine append inside a turn, so the system
177
- prefix stays byte-stable and the cache breakpoint at its end keeps hitting.
241
+ identity prefix stays byte-stable.
178
242
 
179
243
  The `still missing` list is the point: the *model* sees which declared fields are
180
244
  still unanswered, so it stops re-asking for something already given. Stored keys
@@ -194,17 +258,21 @@ turns and resumes — a resumed conversation re-opens with the briefing intact.
194
258
 
195
259
  ## The provider prefix cache
196
260
 
197
- Two distinct caching mechanisms — don't conflate them:
261
+ A stable prefix makes provider caching possible; eligibility, expiry and reported
262
+ savings still depend on the provider.
198
263
 
199
- - **Automatic server-side prefix cache.** Some providers prefix-cache a stable
200
- system prefix automatically, at no cost to configure. This works **only because**
201
- the engine renders the system in two layers (below) and the volatile half sits
202
- **under** the identity boundary, keeping the cacheable prefix byte-stable.
203
- Anything that injects volatile content high in the system block breaks the cache.
204
- - **Manual cache breakpoints (opt-in).** With `prompt_caching` on **and** a
205
- provider that supports explicit cache control, the builder sets one cache
206
- breakpoint at the end of the system block. Only enable this for a byte-stable
207
- system — a volatile system turns every turn into a paid cache *write*.
264
+ - **Automatic prefix caching** needs no Insika flag. The identity-first render
265
+ order keeps changing memory and request data below the stable prefix.
266
+ - **Explicit cache breakpoints** are opt-in via `"prompt_caching": true` on
267
+ the agent profile and applied only when the resolved provider is `anthropic`. With both system layers present,
268
+ Insika sends two text blocks: identity with `cache_control`, then volatile text
269
+ without it. With no volatile text there is one block; with no identity there is
270
+ no breakpoint. Other providers receive a plain system string.
271
+
272
+ Custom context builders and hooks must keep `system_identity`, `system_volatile`
273
+ and their joined `system` consistent. Without a split, or after a hook replaces
274
+ only `system`, the chat builder falls back to treating the whole system as identity.
275
+ That fallback cannot protect a volatile suffix from cache invalidation.
208
276
 
209
277
  Cache accounting surfaces as `cached_tokens` (reads) and `cache_creation_tokens`
210
278
  (writes), visible in telemetry and the Studio tokens chip.
@@ -215,13 +283,14 @@ The system block is partitioned into two cache layers:
215
283
 
216
284
  - **Identity** — bytes that change only on deploy/config edit: the persona
217
285
  prompt (`Prompt`), the level-1 skill list (`Skill`) and the deferred-tool
218
- catalog (`ToolSearch`). This is the cacheable prefix.
286
+ catalog (`ToolSearch`), the fixed tool-discipline instructions and, when enabled,
287
+ the fencing notice. This is the cacheable prefix.
219
288
  - **Volatile** — bytes that may change per turn: memory, session history,
220
289
  triggered skill bodies, the `<request_context>`. Everything else.
221
290
 
222
291
  The layer is a **provider-class contract**, not profile data: `ContextProvider`
223
292
  declares `def layer = :volatile` (conservative — nothing gets pinned by
224
- accident) and the three identity builtins override to `:identity`. A pack does
293
+ accident) and the identity providers override to `:identity`. A pack does
225
294
  not set it — a pack reorganizes *which content goes into the Prompt provider vs
226
295
  the volatile providers*. The Builder stamps the layer on every fragment at
227
296
  production, and the render order is **identity first, volatile after** — a
@@ -236,13 +305,14 @@ output must be byte-stable across turns).
236
305
 
237
306
  ### The observable cache: fingerprints and the invalidation reason
238
307
 
239
- Each turn, the Executor hashes the rendered prefix into a PII-free fingerprint
240
- chain — one SHA-256 per system category in render order, one for the tool
241
- schemas, one cumulative `prefix` — and compares it against the previous turn's
242
- entry. The **invalidation reason** is the first category whose bytes changed (or
243
- vanished); a turn whose prefix held reports nothing. History is deliberately
244
- excluded: a new user message is a divergence every turn, which would be noise,
245
- not a reason.
308
+ Each turn, the Executor hashes the rendered identity and tool schemas into
309
+ SHA-256 fingerprints and a cumulative `prefix`. The invalidation reason names
310
+ the first changed part of that stable prefix. Volatile system categories have
311
+ separate diagnostic digests: changes to memory, knowledge or request context do
312
+ not report an identity-prefix invalidation. History is excluded.
313
+
314
+ These fingerprints explain local prompt changes; they do not prove a provider
315
+ cache hit. Use the provider's token accounting for that.
246
316
 
247
317
  The Studio surfaces it in two places: the **session Context card** shows the
248
318
  turn's cache-hit percentage and the `broke: <category>` line (plus the
@@ -251,9 +321,8 @@ cache tab with the per-agent hit series over time. The per-agent series lives
251
321
  in its own capped store, because a session does not stamp its author — the
252
322
  per-session trace cannot answer "cache-hit over time for *this* agent".
253
323
 
254
- With the prefix stable by construction, the existing `prompt_caching` breakpoint
255
- sits on bytes that stay put — the first (write) turn of a deployment pays the
256
- cache write once, every subsequent turn reads.
324
+ A stable identity can be reused while the provider cache remains eligible and valid.
325
+ A changed tool schema still invalidates the local prefix fingerprint.
257
326
 
258
327
  ## The volume
259
328
 
@@ -0,0 +1,157 @@
1
+ ---
2
+ title: Cross-harness bench
3
+ parent: Operate
4
+ nav_order: 7
5
+ permalink: /cross-harness-bench/
6
+ ---
7
+
8
+ # Cross-harness bench — the same store, six harnesses
9
+
10
+ Six agent harnesses answer the same twelve customer conversations against the same
11
+ MCP store, on the same model at the same reasoning setting, graded on the store's own
12
+ state. It answers a question the engine benchmark deliberately does not touch: not
13
+ "what does the engine cost per turn" but "does a shop staffed by this agent behave".
14
+
15
+ This is a **different suite with a different contract** from
16
+ [Benchmark](BENCHMARK.md), which measures engine overhead with no provider, no key and
17
+ no competitor. This one needs a provider key by design — a commerce conversation
18
+ cannot be stubbed — and it names other harnesses. Read the publication rule at the
19
+ bottom before quoting anything from it.
20
+
21
+ ## Running it
22
+
23
+ ```bash
24
+ cd evals/bench
25
+ cp .env.example .env # OPENROUTER_API_KEY, BENCH_MODEL
26
+ docker compose build # six images, every version pinned
27
+ bash cut3.sh # ~4-5h, ~$6, resumable
28
+ ```
29
+
30
+ `RUNBOOK.md` in that directory is the operator's copy: the traps already paid for, the
31
+ resumability contract, and what to do after the run. Every entrant is a pinned Docker
32
+ image driven through the same one-JSON-in, one-JSON-out adapter; the store is reset
33
+ between every cell; three rounds is the floor, because a single run is not a
34
+ measurement.
35
+
36
+ ## Two scorecards, and why
37
+
38
+ **A is parity** — same tools, same model, same prompt, none of anyone's own product
39
+ layer, ours included. **B is out of the box** — each harness as it ships. The distance
40
+ between a harness's two rows is what its product adds over the bare harness.
41
+
42
+ That distance is the number this bench was built to produce. In cut 3 it was zero for
43
+ us: scorecard B did not show our commerce layer paying for itself, and it was published
44
+ as a null result. In cut 4, with the one irreversible write held for the customer's
45
+ word, it is one cell — and that cell is described below, because one cell is a
46
+ mechanism to read, not a number to quote.
47
+
48
+ ## What cut 3 found, for us
49
+
50
+ Twelve tasks, three rounds, `deepseek/deepseek-v4-flash` at reasoning `medium`, run
51
+ 2026-09-09.
52
+
53
+ | | Scorecard A | Scorecard B |
54
+ | --- | --- | --- |
55
+ | Passed | 34/36 (94%) | 34/36 (94%) |
56
+ | Tokens per success | 2170 | 2263 |
57
+ | Wall clock p50 | 10.0s | 11.8s |
58
+
59
+ Two findings hold regardless of where the other rows land:
60
+
61
+ **Cost is where the spread is.** Accuracy across the field sits inside eleven points;
62
+ tokens per success spans an order of magnitude. Ours is the cheapest row on both
63
+ cards, at comparable accuracy. When five harnesses answer equally well, what separates
64
+ them is what they spend answering.
65
+
66
+ **Take the rules out of the prompt and the field separates.** The shared `AGENTS.md`
67
+ carries the rules the three hardest tasks test, which means a guarded cut partly
68
+ measures the prompt. Re-run with those rules removed, our rows hold at 9/9 on both
69
+ scorecards while the field spreads from 44% to 100%. That is the engine holding a rule
70
+ the prompt no longer states — the closest thing to a claim in this bench, and it is
71
+ about the engine, not about scorecard B.
72
+
73
+ ## What cut 4 found, for us
74
+
75
+ Same model, same setting, run 2026-09-11, with our scorecard-B entry declaring
76
+ `customer_confirm "create_order"` — the close is held until the customer's next
77
+ message confirms it — and task 07 given the third turn that makes that answerable.
78
+
79
+ | | Scorecard A | Scorecard B |
80
+ | --- | --- | --- |
81
+ | Passed | 35/36 (97%) | 36/36 (100%) |
82
+ | Tokens per success | 2349 | 2890 |
83
+ | Wall clock p50 | 11.6s | 8.7s |
84
+
85
+ The one A cell we lose is task 09: *"adiciona logo por favor"* over two units already
86
+ in the cart, and the parity entry closed the order. The B entry, same engine, wrote no
87
+ order in any round of that task and closed task 07 on the confirming turn every time.
88
+ That pair of rows is the distance between A and B made of exactly one thing, and it is
89
+ the thing [Agents](AGENTS.md#do-not-offer-the-irreversible-action) describes: an
90
+ action the reply may offer freely because the engine will not run it in the same turn.
91
+ Three rounds is the floor, not a proof; the mechanism is in `docs/TOOLS.md`, the cells
92
+ in `evals/bench/cuts/2026-09-11/`.
93
+
94
+ Claude Code did not run this cut — the provider's Anthropic-shaped endpoint returned
95
+ nothing visible to it, with the same image that scored 94% two days earlier. Its cells
96
+ are kept outside the scorecard; a row that cannot run is not a row that ran badly.
97
+
98
+ ## Reasoning is a deployment variable, not an axis
99
+
100
+ Every entrant is pinned to `medium` because that is what production runs. Crossing
101
+ reasoning against the table is eight combinations and roughly 1500 cells, so it gets
102
+ its own arm instead: ours against itself, both scorecards, at `medium` and at `off`.
103
+
104
+ Better on one card, worse on the other, with the same three tasks failing at both
105
+ settings. A focused probe — the three tasks we lose, ten rounds each — came back 24/30
106
+ at medium and 27/30 at off, the three tasks contradicting each other. **Reasoning does
107
+ not decide these cases.** Choose it on cost and latency.
108
+
109
+ ## Reading a cut
110
+
111
+ `scorecard.rb` says who scored what. `viewer.rb` says what was actually said, which is
112
+ the question every finding here has turned on:
113
+
114
+ ```bash
115
+ evals/bench/viewer.rb --runs cuts/2026-09-09
116
+ open evals/bench/bench.html
117
+ ```
118
+
119
+ One self-contained HTML file: a harness-by-task matrix, and behind each square the
120
+ verdict, the checks that failed with their detail, and the whole conversation — the
121
+ customer's line, the reply verbatim, the tool calls, the timing. Every cell is also a
122
+ JSON file under `evals/bench/cuts/<date>/`, so any table here can be recomputed or
123
+ contested.
124
+
125
+ ## Publication rule
126
+
127
+ **Everything here is published with its evidence. The comparison table is not a
128
+ ranking we will quote until someone outside this repo has reviewed the configs.**
129
+
130
+ Every entrant's configuration in this repo was written by us, and cut 3 found two
131
+ failures in our own wiring before anyone else did:
132
+
133
+ - OpenClaw's config carried a thinking level under `reasoningDefault`, which is a
134
+ visibility setting taking only `on|off|stream`. OpenClaw rejected the whole config
135
+ and ran no turn; the adapter read an empty answer off the error object and scored it
136
+ as a harness that answered nothing. Seventy-two cells published a broken row as 33%.
137
+ Fixed, the same harness scores 97% on both cards.
138
+ - Fifteen Hermes cells resumed from an earlier session had run against a store image
139
+ that was never rebuilt after its code changed.
140
+
141
+ Both were caught by someone reading transcripts, not by the bench. So the tables ship
142
+ with every cell behind them — anyone can reproduce a row or show it is wrong — but
143
+ until each entrant's configuration has been reviewed by someone who does not maintain
144
+ this repo, **we do not cite another harness's row as a ranking** in comparative or
145
+ marketing material. Our own rows carry no such problem: we can read our own config.
146
+
147
+ Publishing the numbers and declining to market them is the point, not a hedge. A
148
+ ranking whose losing rows we wired ourselves is worth less than a bench anyone can
149
+ clone, rewire and re-run — and the two failures above are the argument for that, not
150
+ against it. Fix an entrant's config, re-run one harness, send back the cut.
151
+
152
+ ## See also
153
+
154
+ - [Benchmark](BENCHMARK.md) — engine overhead: neutral, keyless, reproducible.
155
+ - [Evals](EVALS.md) — the grading corpus and the runner this bench drives.
156
+ - [Agents](AGENTS.md#do-not-offer-the-irreversible-action) — what the bench taught us
157
+ about writing a store agent's prompt.
data/docs/DEPLOY.md CHANGED
@@ -117,10 +117,11 @@ this section is the single source of truth for what changing it means.
117
117
  | `INSIKA_TICK_STALE_AFTER` | `900` | seconds a `:queued`/`:running` task must sit untouched before the tick sweeps it. Must exceed the largest `turn_timeout` of the deployment |
118
118
  | `INSIKA_GATEWAY_TOKEN` | falls back to `ADMIN_TOKEN` | Bearer for `/v1/responses` and `/v1/agents` (the API contract) |
119
119
  | `ADMIN_TOKEN` | `local-demo` | login token for `/studio` (**change in production**) |
120
+ | `INSIKA_ENV` | `RACK_ENV`, else `local` | environment name on the Studio banner; `production`/`prod` hides the banner |
120
121
  | `DEEPSEEK_API_KEY` | — | provider key. **Without it the engine still boots** (`/up` green), but turns fail until it is configured (env or Studio → LLM providers) — cloud resilience |
121
122
  | `DEEPSEEK_MODEL` | `deepseek-v4-flash` | model |
122
123
  | `CONSUMER_INTERNAL_URL` | — | base URL for data-tools calling back a consumer's internal API (see below) |
123
- | `INSIKA_EGRESS_HOSTS` | — | outbound host allowlist (SSRF guard) |
124
+ | `INSIKA_EGRESS_HOSTS` | — | outbound host allowlist for data tools (SSRF guard). An MCP server is pinned by it only when its host resolves to a private address |
124
125
  | `INSIKA_EGRESS_ALLOW_HTTP` / `_ALLOW_PRIVATE` | off | for `http`/loopback callbacks only (**never in cloud**). Also opens inbound **media** fetches (audio/image URLs) to `http`/private targets — a local run serving media over `http://` needs it |
125
126
  | `INSIKA_RELAY_TOKEN` | — | **mounts the relay channel** at `POST /channels/relay/events`, and is the Bearer it requires. Empty = the route does not exist (`404`). See [Channels](CHANNELS.md) |
126
127
  | `INSIKA_RELAY_DELIVER_URL` | — | your callback; the engine POSTs each reply there. Goes through the egress guard |
@@ -128,16 +129,17 @@ this section is the single source of truth for what changing it means.
128
129
  | `INSIKA_RELAY_DELIVERY` | `at_end` | how the relay flushes the outbox: `at_end` (one POST) or `progressive` (one POST per balloon — [delivery policy](CHANNELS.md#delivery-policy)) |
129
130
  | `INSIKA_WIDGET_ORIGINS` | — | exact-match origins allowed to embed the [web widget](CHANNELS.md#the-web-widget), comma-separated. No wildcards. **Half the switch**: with `INSIKA_WIDGET_AGENTS` unset, nothing is mounted (`404`) |
130
131
  | `INSIKA_WIDGET_AGENTS` | — | agent ids a widget visitor may address, comma-separated. The other half of the switch. **A chat rate limit is also required** or the widget answers `503` |
132
+ | `INSIKA_DATABASE_URL` | — | **Postgres store** (`postgres://…`, Postgres 13+). Set, it replaces `INSIKA_DB` and Litestream is skipped. Needs the `pg` gem. See [Postgres store](#postgres-store-optional) |
133
+ | `INSIKA_DATABASE_POOL` | `10` | Postgres connections per process |
131
134
  | `LITESTREAM_REPLICA_URL` | — | **enables Litestream** (backup/DR). Empty = disabled (default). See below |
132
135
  | `LITESTREAM_ENDPOINT` | — | S3-compatible endpoint (R2/MinIO). Empty = AWS S3 |
133
136
  | `LITESTREAM_REGION` | — | bucket region (AWS: `us-east-1`; R2: `auto`) |
134
137
  | `LITESTREAM_ACCESS_KEY_ID` / `LITESTREAM_SECRET_ACCESS_KEY` | — | bucket credentials (read natively by Litestream) |
135
138
 
136
- > **Renamed from `HARNESS_*` → `INSIKA_*`.** Every engine variable now uses the
137
- > `INSIKA_` prefix. The old `HARNESS_*` names are still honored as deprecated aliases
138
- > — set either one and the engine reads it, logging a one-line deprecation notice at
139
- > boot (`insika doctor` reports it too). Migrate at your convenience; the legacy names
140
- > will be dropped in a future release.
139
+ > **Renamed from `HARNESS_*` → `INSIKA_*`.** Every engine variable uses the `INSIKA_`
140
+ > prefix. The old `HARNESS_*` names were removed in 1.0 and are no longer read: a
141
+ > leftover one is reported as an error naming its replacement (at boot and in
142
+ > `insika doctor`). Rename it before upgrading.
141
143
 
142
144
  > **The database filename changed too** — the image now defaults to
143
145
  > `INSIKA_DB=/data/insika.db` (it was `/data/harness.db`). **Existing volumes are
@@ -188,7 +190,7 @@ insika doctor --json # machine-readable (CI / monitoring)
188
190
  insika doctor --fix # applies the safe autofixes and re-diagnoses
189
191
  insika env # lists known keys + current values (secrets masked)
190
192
  insika tools:report # tool audit over the stored traces: never-called
191
- # allowlisted tools, error rate > 30%, stale tools —
193
+ # allowlisted tools, error rate > 30%, stale tools, gate refusals —
192
194
  # read-only, the operator removes ([--agent ID] [--days N] [--json])
193
195
  ```
194
196
 
@@ -203,7 +205,18 @@ text rather than a serialized object** (a file whose content is a stringified Ha
203
205
  serves a mangled prompt on every turn while looking perfectly healthy — present,
204
206
  non-empty, and the agent still answers), **a prompt file that outgrew a prompt**
205
207
  (WARN past ~6 000 estimated tokens or 600 lines — the LLM-generated pack shape
206
- that costs 20%+ extra tokens per turn for no better instruction-following), and
208
+ that costs 20%+ extra tokens per turn for no better instruction-following),
209
+ **in-session compaction enabled with no model slot** (WARN — neither
210
+ `compaction.model` nor the platform `utility_model` is set, so it can never run;
211
+ see [Context](CONTEXT.md)), **eval seeding left on** (WARN — `evals.seeding`
212
+ opens `POST /v1/conversations/:id/seed`, which writes a fabricated conversation
213
+ state under the tenant token; fine while running snapshot evals, off in
214
+ production — see [Evals](EVALS.md#state--a-case-starts-from-a-snapshot)),
215
+ **provenance and presentation tools without an allowed evidence data tool**
216
+ (WARN — the check only sees stored data declarations; verify any code-tool source),
217
+ **fencing off for agents exposed by relay/widget environment configuration**,
218
+ **cache-layer declarations** (known volatile providers marked identity are errors;
219
+ custom identity providers need a byte-stability review), and
207
220
  **skill drift** — a shared skill whose
208
221
  body names one store, a prompt file routing to a skill the agent cannot load, a broken
209
222
  companion pair, a stale `eager:` key (see
@@ -336,6 +349,21 @@ LITESTREAM_SECRET_ACCESS_KEY=...
336
349
  Credentials are read natively by Litestream (they are not in `deploy/litestream.yml`,
337
350
  which only references URL/endpoint/region via `${VAR}`).
338
351
 
352
+ ### One writer per replica path
353
+
354
+ - **Never point two deployments at the same `LITESTREAM_REPLICA_URL`** (two services,
355
+ or staging and production). Both replicate into the same lineage and a later restore
356
+ can bring back the wrong database.
357
+ - **A database already on the volume skips the restore** and is replicated as it is.
358
+ Give a new deployment an empty volume, and do not set the replica URL on a box whose
359
+ volume holds the wrong file — it will be copied over the backup.
360
+ - **Moving a database** to another deployment or path: stop replication on the old box
361
+ first (unset the variable and redeploy), and give the new one a fresh path. If the old
362
+ box's local state is stale (`LTX file is missing`), run `litestream reset <db>`.
363
+ - **Point-in-time recovery** into a separate file, to inspect before swapping it in:
364
+ `litestream ltx -level all <url>` lists the history, then
365
+ `litestream restore -txid <id> -o /data/recovered.db <url>`.
366
+
339
367
  ### Restore drill (the real "done" — an untested backup does not count)
340
368
 
341
369
  The pilot→production gap only closes once a restore has been **exercised**. Two ways:
@@ -353,8 +381,8 @@ scripts/litestream-restore-drill.sh # needs docker, sqlite3, curl
353
381
 
354
382
  ```bash
355
383
  # a. with the service live and replicating, generate some config/conversation and
356
- # confirm the replica has generations:
357
- litestream snapshots -config deploy/litestream.yml "$INSIKA_DB"
384
+ # confirm the replica has files (Litestream 0.5 lists them as LTX files):
385
+ litestream ltx -config deploy/litestream.yml "$INSIKA_DB"
358
386
 
359
387
  # b. boot a NEW box (empty volume) with the same LITESTREAM_* vars → the entrypoint
360
388
  # restores on boot. Verify manually in /studio that conversations and config came
@@ -362,12 +390,41 @@ litestream snapshots -config deploy/litestream.yml "$INSIKA_DB"
362
390
  litestream restore -config deploy/litestream.yml -o /tmp/restored.db "$INSIKA_DB"
363
391
  ```
364
392
 
393
+ ## Postgres store (optional)
394
+
395
+ SQLite is the default and the right choice for one box, including several worker
396
+ processes (`INSIKA_WORKERS` > 1): their writes share commits (group commit), so
397
+ they only queue behind the file's single write lock under sustained bursts of
398
+ writes. Postgres removes that lock — writers only meet on the keys they share — but
399
+ every store call becomes a network round trip, and a turn makes many store calls,
400
+ so on one box it is usually slower per turn, not faster.
401
+
402
+ Choose Postgres when the store must be shared: several nodes or replicas running
403
+ the engine, or other systems that query the data directly. Measure your own load
404
+ before switching for speed alone.
405
+
406
+ 1. Add `gem "pg"` to your Gemfile (the reference image already has it and libpq).
407
+ 2. Set `INSIKA_DATABASE_URL=postgres://user:pass@host:5432/db` (Postgres 13+).
408
+ It replaces `INSIKA_DB`; Litestream is skipped. `INSIKA_DATABASE_POOL` sets the
409
+ connections per process (default 10). A transaction keeps its connection while
410
+ its turn waits for the reactor, so under many concurrent turns a small pool is
411
+ where calls queue; raise it, keeping workers × pool under the server's
412
+ `max_connections` (100 by default).
413
+ 3. The table (`insika_kv`) is created on first boot.
414
+ 4. Moving an existing SQLite store into an **empty** database (the copy adds and
415
+ overwrites keys, it does not remove extra ones): stop the app, then
416
+ `INSIKA_DB=/data/insika.db INSIKA_DATABASE_URL=… bundle exec ruby scripts/store_copy.rb`,
417
+ then boot with `INSIKA_DATABASE_URL` set. Unsetting it goes back to the SQLite
418
+ file as it was at the copy.
419
+
420
+ Backups are Postgres's job (your provider's snapshots or point-in-time recovery).
421
+
365
422
  ## Kubernetes (evolution)
366
423
 
367
424
  SQLite does not share one file across nodes. Paths forward: a StatefulSet + a PVC
368
- per pod + **sticky-by-agent** routing (shard by tenant), or **LiteFS**, or an
369
- optional **Postgres** adapter. **Litestream** (above) for backup/DR from day one —
370
- orthogonal to topology.
425
+ per pod + **sticky-by-agent** routing (shard by tenant), or **LiteFS**, or the
426
+ [Postgres store](#postgres-store-optional), which every pod can share.
427
+ **Litestream** (above) for backup/DR of SQLite — orthogonal to topology.
371
428
 
372
429
  For the *session-routing* half specifically — as opposed to the storage
373
430
  topology above — see [`insika-router`](ROUTER.md): N pods behind a
data/docs/EMBEDDING.md CHANGED
@@ -98,6 +98,16 @@ It also holds for runtime reconfiguration. Editing a provider key in the Studio
98
98
  (or dispatching `:upsert_llm_provider`) applies to **that graph only** — the
99
99
  change is real, takes effect without a restart, and stops at the graph boundary.
100
100
 
101
+ Each turn's chat gets a copied RubyLLM configuration with Insika's diagnostic
102
+ instrumenter. It composes with a host instrumenter already on that configuration:
103
+ host notifications still run, and the graph and global configurations keep the
104
+ host instrumenter. Insika attributes only those chat operations to the task;
105
+ auxiliary model work on the graph's original context has no task correlation.
106
+ Each graph stores its task diagnostics in its own backend, so separate backends
107
+ keep those records isolated. Insika's diagnostic fields are allowlisted, but a
108
+ host instrumenter receives RubyLLM's original payload and must apply its own
109
+ data policy. See [Observability](OBSERVABILITY.md#native-model-diagnostics).
110
+
101
111
  ### 3. The process still owns signals and the reactor
102
112
 
103
113
  Draining in-flight turns on SIGTERM ([the process model](DEPLOY.md)) is a process