insika 0.2.0 → 0.7.0

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 (300) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +325 -17
  3. data/README.md +52 -11
  4. data/bin/insika +895 -8
  5. data/bin/insika-router +87 -0
  6. data/docs/AGENTS.md +229 -163
  7. data/docs/API.md +56 -0
  8. data/docs/ARCHITECTURE.md +3 -2
  9. data/docs/ARTIFACTS.md +95 -0
  10. data/docs/BENCHMARK.md +2 -2
  11. data/docs/CHANNELS.md +111 -21
  12. data/docs/CONTEXT.md +132 -19
  13. data/docs/DEMO.md +80 -0
  14. data/docs/DEPLOY.md +75 -4
  15. data/docs/EMBEDDING.md +1 -1
  16. data/docs/EVALS.md +146 -2
  17. data/docs/FACTS.md +135 -0
  18. data/docs/HARVEST.md +116 -0
  19. data/docs/KNOWLEDGE.md +290 -0
  20. data/docs/LOADTEST.md +16 -10
  21. data/docs/MEDIA.md +128 -0
  22. data/docs/OBSERVABILITY.md +65 -6
  23. data/docs/OUTCOMES.md +137 -0
  24. data/docs/PLUGINS.md +51 -6
  25. data/docs/POLICY.md +216 -0
  26. data/docs/REFINEMENT.md +17 -12
  27. data/docs/RELEASING.md +27 -0
  28. data/docs/ROUTER.md +213 -0
  29. data/docs/RUNNING-LOCAL.md +6 -6
  30. data/docs/SCHEDULING.md +121 -0
  31. data/docs/SECURITY.md +79 -2
  32. data/docs/SKILLS.md +13 -4
  33. data/docs/SOAK.md +127 -0
  34. data/docs/TEMPLATES.md +134 -0
  35. data/docs/TOOLS.md +220 -27
  36. data/docs/WHY.md +1 -1
  37. data/docs/WORKFLOWS.md +2 -2
  38. data/docs/_includes/head_custom.html +5 -0
  39. data/docs/_includes/title.html +13 -0
  40. data/docs/_sass/color_schemes/insika.scss +32 -0
  41. data/docs/_sass/custom/custom.scss +199 -0
  42. data/docs/_sass/custom/setup.scss +26 -0
  43. data/docs/assets/img/favicon.svg +7 -0
  44. data/docs/assets/img/insika-mark.svg +7 -0
  45. data/docs/core-concepts.md +21 -0
  46. data/docs/domain.md +115 -0
  47. data/docs/improve.md +20 -0
  48. data/docs/index.md +9 -6
  49. data/docs/integrate.md +20 -0
  50. data/docs/operate.md +13 -6
  51. data/docs/prompts/ADD-TOOL.md +118 -0
  52. data/docs/prompts/DIAGNOSE-TURN.md +65 -0
  53. data/docs/prompts/GO-LIVE.md +138 -0
  54. data/docs/prompts/RUN-EXAMPLES.md +70 -0
  55. data/docs/reference.md +19 -0
  56. data/docs/ship.md +10 -2
  57. data/docs/start-here.md +18 -0
  58. data/lib/insika/agent_profile.rb +202 -10
  59. data/lib/insika/artifact_signing.rb +82 -0
  60. data/lib/insika/artifact_store.rb +160 -0
  61. data/lib/insika/balloon_splitter.rb +102 -0
  62. data/lib/insika/budget_ledger.rb +34 -3
  63. data/lib/insika/cache_series_store.rb +49 -0
  64. data/lib/insika/channel_delivery.rb +119 -14
  65. data/lib/insika/channels/relay.rb +77 -3
  66. data/lib/insika/channels/web.rb +2 -2
  67. data/lib/insika/chat_builder.rb +105 -2
  68. data/lib/insika/checkpoint_store.rb +16 -0
  69. data/lib/insika/commands/agent_payload.rb +3 -3
  70. data/lib/insika/commands/backfill_knowledge.rb +145 -0
  71. data/lib/insika/commands/cancel_followup.rb +49 -0
  72. data/lib/insika/commands/delete_artifact.rb +35 -0
  73. data/lib/insika/commands/delete_concept.rb +34 -0
  74. data/lib/insika/commands/delete_mcp.rb +6 -2
  75. data/lib/insika/commands/delete_tenant_data.rb +107 -0
  76. data/lib/insika/commands/export_customer_memory.rb +48 -0
  77. data/lib/insika/commands/forget_customer.rb +117 -0
  78. data/lib/insika/commands/freeze_funnel_baseline.rb +113 -0
  79. data/lib/insika/commands/gate_harvest.rb +138 -0
  80. data/lib/insika/commands/gate_refinement.rb +1 -1
  81. data/lib/insika/commands/judge_shadow_pairs.rb +124 -0
  82. data/lib/insika/commands/memory_forget_fact.rb +20 -4
  83. data/lib/insika/commands/memory_put_fact.rb +23 -4
  84. data/lib/insika/commands/promote_harvest.rb +130 -0
  85. data/lib/insika/commands/record_outcome.rb +46 -0
  86. data/lib/insika/commands/record_shadow_reply.rb +68 -0
  87. data/lib/insika/commands/refresh_mcp_tools.rb +47 -0
  88. data/lib/insika/commands/reject_harvest.rb +38 -0
  89. data/lib/insika/commands/resolve_proposal.rb +108 -0
  90. data/lib/insika/commands/restore_concept.rb +34 -0
  91. data/lib/insika/commands/revoke_contact.rb +49 -0
  92. data/lib/insika/commands/rollback_harvest.rb +86 -0
  93. data/lib/insika/commands/run_distillation.rb +186 -0
  94. data/lib/insika/commands/run_harvest.rb +393 -0
  95. data/lib/insika/commands/seed_demo_data.rb +31 -0
  96. data/lib/insika/commands/send_message.rb +103 -6
  97. data/lib/insika/commands/session_purge.rb +67 -0
  98. data/lib/insika/commands/upsert_mcp.rb +6 -3
  99. data/lib/insika/commands/write_concept.rb +57 -0
  100. data/lib/insika/contact_store.rb +183 -0
  101. data/lib/insika/context/builder.rb +21 -3
  102. data/lib/insika/context/fragment.rb +7 -3
  103. data/lib/insika/context/priority.rb +5 -0
  104. data/lib/insika/context/provider.rb +17 -3
  105. data/lib/insika/context/providers/briefing.rb +96 -0
  106. data/lib/insika/context/providers/knowledge.rb +108 -0
  107. data/lib/insika/context/providers/memory.rb +16 -7
  108. data/lib/insika/context/providers/prompt.rb +57 -23
  109. data/lib/insika/context/providers/skill.rb +2 -0
  110. data/lib/insika/context/providers/tool_search.rb +2 -0
  111. data/lib/insika/context_trace_store.rb +38 -2
  112. data/lib/insika/cron.rb +189 -0
  113. data/lib/insika/demo/agent_attrs.rb +43 -0
  114. data/lib/insika/demo/golden_cases.rb +81 -0
  115. data/lib/insika/demo/seeder.rb +336 -0
  116. data/lib/insika/distill.rb +224 -0
  117. data/lib/insika/distill_engine.rb +169 -0
  118. data/lib/insika/doctor.rb +889 -11
  119. data/lib/insika/dsl/definition.rb +3 -2
  120. data/lib/insika/dsl/runtime.rb +69 -82
  121. data/lib/insika/dsl/server_boot.rb +92 -1
  122. data/lib/insika/dsl/system.rb +10 -2
  123. data/lib/insika/dsl.rb +186 -5
  124. data/lib/insika/edge_limiter.rb +28 -7
  125. data/lib/insika/env_schema.rb +25 -3
  126. data/lib/insika/errors.rb +11 -0
  127. data/lib/insika/evals/assertions.rb +3 -2
  128. data/lib/insika/evals/golden.rb +41 -4
  129. data/lib/insika/evals/judge.rb +47 -2
  130. data/lib/insika/evals/pairwise.rb +29 -6
  131. data/lib/insika/evals/persona.rb +98 -0
  132. data/lib/insika/evals/runner.rb +9 -0
  133. data/lib/insika/evals/simulator.rb +225 -0
  134. data/lib/insika/evals/transport.rb +83 -1
  135. data/lib/insika/event_stream.rb +10 -0
  136. data/lib/insika/evidence.rb +183 -0
  137. data/lib/insika/executor.rb +869 -76
  138. data/lib/insika/followup_engine.rb +207 -0
  139. data/lib/insika/followup_policy.rb +198 -0
  140. data/lib/insika/followup_store.rb +306 -0
  141. data/lib/insika/funnel_declaration.rb +106 -0
  142. data/lib/insika/funnel_fold.rb +179 -0
  143. data/lib/insika/funnel_store.rb +163 -0
  144. data/lib/insika/golden_store.rb +17 -2
  145. data/lib/insika/grounding/matcher.rb +69 -0
  146. data/lib/insika/grounding.rb +44 -0
  147. data/lib/insika/harvest/conversion_gate.rb +159 -0
  148. data/lib/insika/harvest/criterion.rb +98 -0
  149. data/lib/insika/harvest/gate.rb +194 -0
  150. data/lib/insika/harvest/negative_list.rb +199 -0
  151. data/lib/insika/harvest.rb +241 -0
  152. data/lib/insika/harvest_engine.rb +193 -0
  153. data/lib/insika/harvest_store.rb +548 -0
  154. data/lib/insika/knowledge.rb +680 -0
  155. data/lib/insika/knowledge_store.rb +140 -0
  156. data/lib/insika/mcp_client.rb +94 -0
  157. data/lib/insika/mcp_json.rb +74 -0
  158. data/lib/insika/mcp_live_tool.rb +43 -0
  159. data/lib/insika/mcp_store.rb +98 -26
  160. data/lib/insika/mcp_tool_ingestor.rb +30 -8
  161. data/lib/insika/mcp_tool_registry.rb +100 -0
  162. data/lib/insika/media.rb +382 -0
  163. data/lib/insika/memory_audit_store.rb +85 -0
  164. data/lib/insika/memory_store.rb +264 -23
  165. data/lib/insika/message_origin.rb +7 -2
  166. data/lib/insika/middleware.rb +9 -0
  167. data/lib/insika/model_visible.rb +87 -0
  168. data/lib/insika/model_visible_trace_store.rb +66 -0
  169. data/lib/insika/onboarding.rb +22 -1
  170. data/lib/insika/outbox_store.rb +42 -4
  171. data/lib/insika/outcome_store.rb +147 -0
  172. data/lib/insika/overlay_tool_registry.rb +37 -17
  173. data/lib/insika/packaging.rb +163 -0
  174. data/lib/insika/parity/criterion.rb +79 -0
  175. data/lib/insika/parity/verdict.rb +318 -0
  176. data/lib/insika/prefix_fingerprint.rb +58 -0
  177. data/lib/insika/profile_source.rb +29 -1
  178. data/lib/insika/prompt_catalog.rb +10 -0
  179. data/lib/insika/proposal_store.rb +271 -0
  180. data/lib/insika/queue_policy.rb +4 -1
  181. data/lib/insika/refinement/proposer.rb +1 -1
  182. data/lib/insika/reliability.rb +32 -6
  183. data/lib/insika/retention.rb +316 -0
  184. data/lib/insika/router/app.rb +157 -0
  185. data/lib/insika/router/backend_pool.rb +98 -0
  186. data/lib/insika/router/hash_ring.rb +55 -0
  187. data/lib/insika/router/proxy_body.rb +34 -0
  188. data/lib/insika/router/session_key.rb +54 -0
  189. data/lib/insika/router.rb +18 -0
  190. data/lib/insika/routing.rb +101 -0
  191. data/lib/insika/safety/config.rb +44 -4
  192. data/lib/insika/safety/corpus.rb +255 -0
  193. data/lib/insika/safety/detectors.rb +32 -113
  194. data/lib/insika/safety/factory.rb +15 -2
  195. data/lib/insika/safety/grounding_enforcer.rb +59 -0
  196. data/lib/insika/safety/grounding_validator.rb +49 -0
  197. data/lib/insika/safety/input_guardrail.rb +1 -1
  198. data/lib/insika/safety/moderator.rb +1 -1
  199. data/lib/insika/safety/output_filter.rb +9 -5
  200. data/lib/insika/safety/output_validator.rb +11 -5
  201. data/lib/insika/schedule.rb +177 -0
  202. data/lib/insika/schedule_engine.rb +314 -0
  203. data/lib/insika/schedule_store.rb +208 -0
  204. data/lib/insika/schema_guard.rb +35 -0
  205. data/lib/insika/server/app.rb +247 -17
  206. data/lib/insika/server/rack_app.rb +21 -1
  207. data/lib/insika/server/responses.rb +40 -1
  208. data/lib/insika/session_actor.rb +8 -4
  209. data/lib/insika/session_store.rb +65 -2
  210. data/lib/insika/settings_store.rb +10 -0
  211. data/lib/insika/shadow_pair_store.rb +258 -0
  212. data/lib/insika/skill_catalog.rb +16 -0
  213. data/lib/insika/soak/envelope.rb +140 -0
  214. data/lib/insika/soak/report.rb +392 -0
  215. data/lib/insika/soak/runner.rb +554 -0
  216. data/lib/insika/steer_injector.rb +21 -10
  217. data/lib/insika/store.rb +10 -1
  218. data/lib/insika/stores/memory.rb +6 -0
  219. data/lib/insika/stores/sqlite.rb +8 -0
  220. data/lib/insika/studio/app.rb +1437 -54
  221. data/lib/insika/studio/assets/dist/application.css +1 -1
  222. data/lib/insika/studio/assets/dist/application.js +23 -23
  223. data/lib/insika/studio/assets/dist/favicon.svg +6 -0
  224. data/lib/insika/studio/forms.rb +308 -10
  225. data/lib/insika/studio/nav_icons.rb +19 -1
  226. data/lib/insika/studio/views/_agent_tab_cache.erb +25 -0
  227. data/lib/insika/studio/views/_agent_tab_config.erb +514 -0
  228. data/lib/insika/studio/views/_agent_tab_history.erb +24 -0
  229. data/lib/insika/studio/views/_agent_tab_loops.erb +54 -0
  230. data/lib/insika/studio/views/_agent_tab_memory.erb +51 -0
  231. data/lib/insika/studio/views/_agent_tab_outcomes.erb +31 -0
  232. data/lib/insika/studio/views/_agent_tab_prompts.erb +108 -0
  233. data/lib/insika/studio/views/_agent_tab_skills.erb +38 -0
  234. data/lib/insika/studio/views/_agents_master.erb +44 -0
  235. data/lib/insika/studio/views/_message.erb +49 -32
  236. data/lib/insika/studio/views/agent_detail.erb +62 -278
  237. data/lib/insika/studio/views/agents.erb +71 -54
  238. data/lib/insika/studio/views/approvals.erb +4 -1
  239. data/lib/insika/studio/views/artifact.erb +23 -0
  240. data/lib/insika/studio/views/artifacts.erb +59 -0
  241. data/lib/insika/studio/views/chats.erb +4 -1
  242. data/lib/insika/studio/views/customer.erb +94 -0
  243. data/lib/insika/studio/views/customers.erb +32 -0
  244. data/lib/insika/studio/views/evals.erb +6 -3
  245. data/lib/insika/studio/views/facts.erb +133 -0
  246. data/lib/insika/studio/views/followups.erb +125 -0
  247. data/lib/insika/studio/views/funnel.erb +106 -0
  248. data/lib/insika/studio/views/harvest.erb +234 -0
  249. data/lib/insika/studio/views/home.erb +106 -66
  250. data/lib/insika/studio/views/knowledge.erb +123 -0
  251. data/lib/insika/studio/views/layout.erb +15 -11
  252. data/lib/insika/studio/views/mcp.erb +174 -80
  253. data/lib/insika/studio/views/parity.erb +147 -0
  254. data/lib/insika/studio/views/playground.erb +7 -1
  255. data/lib/insika/studio/views/session.erb +233 -124
  256. data/lib/insika/studio/views/settings.erb +41 -1
  257. data/lib/insika/studio/views/skills.erb +1 -2
  258. data/lib/insika/studio/views/system_files.erb +1 -1
  259. data/lib/insika/studio/views/task.erb +13 -0
  260. data/lib/insika/studio/views/tasks.erb +4 -1
  261. data/lib/insika/studio/views/tools.erb +24 -10
  262. data/lib/insika/task_store.rb +21 -1
  263. data/lib/insika/templates/browser-agent/README.md +36 -0
  264. data/lib/insika/templates/browser-agent/agent.rb +49 -0
  265. data/lib/insika/templates/daily-digest/README.md +38 -0
  266. data/lib/insika/templates/daily-digest/agent.rb +77 -0
  267. data/lib/insika/templates/repo-explorer/README.md +36 -0
  268. data/lib/insika/templates/repo-explorer/agent.rb +45 -0
  269. data/lib/insika/templates/research-analyst/README.md +26 -0
  270. data/lib/insika/templates/research-analyst/agent.rb +58 -0
  271. data/lib/insika/templates/review-panel/README.md +20 -0
  272. data/lib/insika/templates/review-panel/agent.rb +50 -0
  273. data/lib/insika/templates/travel-planner/README.md +35 -0
  274. data/lib/insika/templates/travel-planner/agent.rb +87 -0
  275. data/lib/insika/templates.rb +112 -0
  276. data/lib/insika/testing/store_contract.rb +27 -6
  277. data/lib/insika/tick.rb +47 -11
  278. data/lib/insika/timezone.rb +45 -0
  279. data/lib/insika/tool_definition.rb +17 -7
  280. data/lib/insika/tool_envelope.rb +69 -0
  281. data/lib/insika/tool_manifest.rb +5 -1
  282. data/lib/insika/tools/data_defined_tool.rb +10 -0
  283. data/lib/insika/tools/generate_image.rb +89 -0
  284. data/lib/insika/tools/load_knowledge.rb +74 -0
  285. data/lib/insika/tools/run_persona_eval.rb +328 -0
  286. data/lib/insika/tools/save_artifact.rb +95 -0
  287. data/lib/insika/tools/schedule_followup.rb +164 -0
  288. data/lib/insika/tools/tts.rb +47 -0
  289. data/lib/insika/tools/update_briefing.rb +126 -0
  290. data/lib/insika/turn_output.rb +1 -1
  291. data/lib/insika/turn_state.rb +49 -1
  292. data/lib/insika/turn_timing.rb +22 -2
  293. data/lib/insika/version.rb +1 -1
  294. data/lib/insika/vitals.rb +84 -0
  295. data/lib/insika/wiring/graph.rb +472 -13
  296. data/lib/insika/wiring/graph_chat.rb +102 -0
  297. data/lib/insika.rb +149 -3
  298. metadata +181 -5
  299. data/docs/build.md +0 -14
  300. data/docs/understand.md +0 -10
data/docs/KNOWLEDGE.md ADDED
@@ -0,0 +1,290 @@
1
+ ---
2
+ title: Knowledge
3
+ parent: Improve
4
+ nav_order: 4
5
+ permalink: /knowledge/
6
+ ---
7
+
8
+ # Knowledge — learning from conversations
9
+
10
+ An agent is amnesiac in exactly one dimension. It has history (the session),
11
+ memory (facts about one customer, written by the `remember` tool), skills
12
+ (procedures a human curated), and tool traces — but nothing turns a finished
13
+ conversation into knowledge the agent can use on someone else's turn. Every
14
+ conversation teaches a store things — which product customers actually ask
15
+ about, which objection keeps coming up, which CEP maps to which distribution
16
+ center — and today all of it is write-once trace data nobody reads back.
17
+
18
+ Knowledge is that loop. After a turn completes, the engine can extract durable
19
+ **concepts** from it — facts, procedures, policies, objections — and persist
20
+ them for the agent (never a customer, never a session) to build on.
21
+
22
+ **What ships today: extraction, consolidation, retrieval, export, and a
23
+ Studio page.** The engine writes what it learns, decides whether a repeat
24
+ sighting confirms, merges with, or contradicts what it already knew,
25
+ retrieves the concepts relevant to a turn's message back into the prompt,
26
+ and an operator can see, edit and resolve all of it in the Studio. Only the
27
+ optional FTS5 index remains, deferred with a measured trigger — see
28
+ [What's not here yet](#whats-not-here-yet).
29
+
30
+ ## The concept format
31
+
32
+ One concept is one record — a markdown document with a YAML frontmatter
33
+ block, the same shape a `SKILL.md` uses:
34
+
35
+ ```markdown
36
+ ---
37
+ name: cep-sudeste-cd-campinas
38
+ description: CEPs 13xxx-13999 ship from the Campinas DC, 1-2 business days.
39
+ type: fact # fact | entity | procedure | policy | objection
40
+ provenance: observed # policy (curated) | observed (learned from conversations)
41
+ confidence: 0.6 # 0..1, evidence-weighted
42
+ sources: ["sess_8f3c"] # session ids, never message content
43
+ occurrences: 1
44
+ created_at: 2026-08-24T18:02:11Z
45
+ updated_at: 2026-08-24T18:02:11Z
46
+ ---
47
+
48
+ Orders to CEP range 13000-13999 are fulfilled by the Campinas DC. Quoted
49
+ delivery is 1-2 business days. Related: [[frete-gratis-acima-199]].
50
+ ```
51
+
52
+ The model only ever writes `name`, `description`, `type` and `body`. Every
53
+ other field — `provenance`, `confidence`, `sources`, `occurrences`, the
54
+ timestamps — is stamped by the engine; an extraction answer that tries to
55
+ supply one of those is rejected outright (the same discipline Facts applies
56
+ to a model-authored scope). `[[links]]` inside the body are plain text stored
57
+ as-is; retrieval resolves them lazily by name at read time (below) — that one
58
+ hop is the entire "graph," never a stored structure of its own.
59
+
60
+ **`provenance` is not decoration.** Everything the extractor writes is
61
+ `provenance: observed` — a claim learned from what people said in
62
+ conversations, not official policy. A promise an agent made ("we'll get back
63
+ to you in 48h") is observed practice, not a guarantee, and the field exists so
64
+ nothing downstream states it to a customer as a commitment. `provenance:
65
+ policy` is reserved for a concept a human authored or promoted by hand — the
66
+ engine never sets it.
67
+
68
+ ## Enabling it — the `knowledge:` block
69
+
70
+ Knowledge is pack data on the agent, exactly like `distill:` or `harvest:` —
71
+ absent = the feature is off for that agent, byte-identical engine:
72
+
73
+ ```ruby
74
+ agent = Insika.agent("store-support") do
75
+ instructions "…"
76
+ knowledge extract: true, retrieve: true,
77
+ types: %w[fact policy objection], # what the extractor may emit
78
+ top_k: 5 # concepts injected per turn (default 5)
79
+ # prompt: "<what counts as a concept for THIS store>" — the pack-authored
80
+ # half; absent = the engine's generic prompt. `model:` (absent = the
81
+ # platform utility_model) names the extractor AND the consolidator.
82
+ # index: "scan" (default, the only one built) | "fts5" (accepted, falls
83
+ # back to "scan" — the optional SQLite index isn't built yet).
84
+ end
85
+ ```
86
+
87
+ `extract` and `retrieve` are independent switches: an agent can learn without
88
+ recalling (write-only, inspected by hand), or recall without learning
89
+ (curate every concept by hand in the Studio, `provenance: policy`).
90
+
91
+ ## The write path
92
+
93
+ After a turn completes, off the turn's critical path (the customer already has
94
+ the answer), the engine sends the turn's transcript slice to the platform
95
+ `utility_model` and asks for a JSON array of candidate concepts. The answer is
96
+ schema-validated, filtered against the agent's configured `types`, and any key
97
+ the model should not be writing is dropped and counted, never trusted. The
98
+ survivors are redacted for PII, stamped with provenance and a first-sighting
99
+ confidence, and written to the store — one `:knowledge_learned` event per
100
+ concept (name, type, agent — never content).
101
+
102
+ **Durability, honestly.** Extraction is best-effort: a crash between a turn's
103
+ terminal and the write loses that turn's concepts, not the conversation (which
104
+ is durable) and not a previously learned concept. The recovery path is a
105
+ re-scan, not a queue:
106
+
107
+ ```
108
+ insika knowledge:backfill --agent store-support --since 2026-08-01T00:00:00Z
109
+ ```
110
+
111
+ replays the agent's stored sessions through the same extractor a live turn
112
+ uses.
113
+
114
+ ## Consolidation — what happens on a repeat sighting
115
+
116
+ Writing a concept name that already exists is never a blind overwrite. The
117
+ engine compares the new sighting against what is on record and picks one of
118
+ three outcomes:
119
+
120
+ - **Same claim, reworded or reconfirmed** — a cheap, deterministic check
121
+ (no model call): occurrences go up, the new session id joins `sources`,
122
+ confidence climbs (`min(0.95, 0.5 + 0.1 × distinct sources)` — more
123
+ independent sightings, more confidence, never certainty). The body itself
124
+ is untouched, so an operator's edit is never silently discarded by a
125
+ repeat sighting.
126
+ - **Related claim** — the two bodies say compatible things that combine into
127
+ one coherent statement. A second model call (the only place this feature
128
+ spends a second call, and only when a name already exists) merges them;
129
+ the result bumps occurrences/sources/confidence the same way a same-claim
130
+ sighting does.
131
+ - **Contradicting claim** — the two bodies say genuinely different things.
132
+ **Never merged, never silently overwritten.** The new claim is appended
133
+ under a `## Contradiction` heading, confidence drops to a flat `0.4`, and
134
+ a `:knowledge_conflict` event fires. A human resolves it in the Studio by
135
+ editing the concept directly — there is no separate "resolve" action,
136
+ because resolving IS editing the markdown to say what's actually true.
137
+
138
+ When no model is configured for consolidation (or its answer is unusable),
139
+ the engine defaults to the conservative outcome — contradicting. A concept
140
+ this feature is unsure about becomes a human's problem, never a guess that
141
+ looks confident and might be wrong.
142
+
143
+ ## The Studio page
144
+
145
+ `/studio/knowledge` — single-agent-scoped like Harvest (`?agent=`), not
146
+ shared like Skills, because a concept only ever belongs to one store. A
147
+ drill-down list (name, type, confidence, occurrences, updated_at) with a
148
+ conflict filter (`?status=conflict`) and the same CodeMirror markdown editor
149
+ Skills uses, version history and restore, and delete. Editing the raw
150
+ markdown is also how an operator promotes `provenance: observed` to `policy`
151
+ — there is no separate "promote" button, the field is just another line in
152
+ the file.
153
+
154
+ ## Retrieval — what reaches a turn's prompt
155
+
156
+ With `knowledge.retrieve` on, every turn the engine searches the agent's
157
+ concepts for the ones relevant to the customer's message (pure term overlap —
158
+ no embeddings, no network call) and injects the top few as a level-1
159
+ `<knowledge>` block, the same progressive-disclosure shape Skills uses:
160
+
161
+ ```
162
+ <knowledge>
163
+ <concept name="cep-13-campinas" confidence="0.60" provenance="observed">CEPs 13xxx-13999 ship from the Campinas DC, 1-2 business days.</concept>
164
+ </knowledge>
165
+
166
+ If the customer's question needs more than the summary above, call
167
+ `load_knowledge("name")` FIRST — before any other lookup for that topic.
168
+ This is learned from past conversations, not official policy: never state
169
+ a `provenance="observed"` concept to the customer as a guarantee.
170
+ ```
171
+
172
+ Only `name`/`description`/`confidence`/`provenance` are shown — the same
173
+ "summary now, body on demand" shape a skill's level-1 list uses. A `load_knowledge`
174
+ tool (outside `tools_allow`, wired only when `retrieve` is on — same as
175
+ `load_skill`) fetches one concept's complete body. Calling it fires
176
+ `:knowledge_retrieved` — this, not the injection itself, is what the
177
+ adoption metric tracks (see [the honest limits](#the-honest-limits)): a
178
+ concept sitting unread in the prompt taught the agent nothing.
179
+
180
+ **One hop through `[[links]]`.** A matched concept's body may reference
181
+ `[[other-concept-name]]`; retrieval resolves those names against the store
182
+ and injects them too (capped at `top_k` again), so a concept that names its
183
+ neighbor arrives with it. No transitive walk — one level, deliberately, the
184
+ same reasoning a skill's declared `companions:` uses.
185
+
186
+ **Where it sits, and what gets cut first.** `<knowledge>` sits at priority 77
187
+ — below curated skills (80), above a single conversation's memory (75): a
188
+ human's playbook always outranks what the engine inferred, and what the
189
+ engine inferred outranks one customer's chat facts. Never pinned; under
190
+ budget pressure it goes before skills but survives longer than memory,
191
+ briefing, history and request context — see [Context](CONTEXT.md).
192
+
193
+ ## External knowledge over MCP
194
+
195
+ A native knowledge base is not the only shape this can take. Mounting a
196
+ third-party knowledge or memory server as MCP tools on an agent is a
197
+ supported pattern, complementary to the native loop above — a deployment can
198
+ run both: this loop learns concepts from ITS OWN traffic automatically, while
199
+ an MCP-mounted server can serve as a synthesis/consultant tool an agent calls
200
+ explicitly.
201
+
202
+ The one adoption lesson worth carrying over regardless of which shape is used:
203
+ a model does not reliably call a recall/lookup tool just because it exists,
204
+ even when it clearly should. A polite "use this when relevant" instruction
205
+ measured close to zero calls under pressure; what worked was an explicit,
206
+ ordered rule naming the competing tools directly — "call the recall tool
207
+ FIRST, before any data lookup, on topics X" — not a sentiment about when it's
208
+ a good idea. Tool *adoption* is a prompt-ordering problem, not a tool-quality
209
+ one, and it is worth measuring (calls per conversation), not assuming.
210
+
211
+ ## The honest limits
212
+
213
+ - **Best-effort extraction, re-scan recovery.** Same discipline as Facts: no
214
+ queue, no exactly-once claim. A concept is re-derivable from the session
215
+ transcript, so a missed extraction is recoverable, never lost.
216
+ - **Consolidation trusts the same model that extracts.** The "related vs.
217
+ contradicting" call is a model judgment, not a proof — the conservative
218
+ default (contradicting, when unsure) bounds the failure mode to "a human
219
+ looks at it," never "two different claims silently became one wrong one."
220
+ - **Precision is a forge audit.** The engine guarantees the gates (schema,
221
+ key-stripping, PII redaction, type allowlist); it cannot guarantee the
222
+ model's judgment about what is worth remembering. That is tuned per store,
223
+ the same way a Harvest or Facts prompt is.
224
+ - **Retrieval quality is not adoption.** Injecting the right concept proves
225
+ nothing if the model never reads it — measure `:knowledge_retrieved`
226
+ (retrieval calls per conversation), not just whether the block appeared.
227
+ The explicit, ordered instruction in the block exists because a softer
228
+ "when to use" wording measured close to zero calls in practice.
229
+ - **Term overlap, not understanding.** `Index::Scan` matches words, not
230
+ meaning — a concept phrased very differently from the customer's words
231
+ will not surface even if it answers the question. No embeddings by
232
+ design: revisit only with evidence that retrieval, not
233
+ extraction, is the bottleneck.
234
+
235
+ ## Index::Scan's performance, measured not assumed
236
+
237
+ `Index::Scan` keeps a read cache per instance (the context provider holds
238
+ one for the whole process, never rebuilt per turn): a concept's YAML
239
+ frontmatter is parsed once and reused until that concept's own record
240
+ `updated_at` changes, so a write invalidates itself for free. Reproduce
241
+ these numbers with:
242
+
243
+ ```bash
244
+ bundle exec ruby scripts/bench_knowledge_index.rb
245
+ ```
246
+
247
+ | concepts/agent | p50 | p95 |
248
+ |---|---|---|
249
+ | 50 | 0.28 ms | 0.47 ms |
250
+ | 200 | 1.17 ms | 1.56 ms |
251
+ | 1000 | 6.4 ms | 8.0 ms |
252
+ | 5000 | 35.6 ms | 41.3 ms |
253
+
254
+ At the scale this feature targets for the first year — hundreds of concepts
255
+ per agent — a warm-cache search costs a bit over a millisecond, close to
256
+ the engine's own documented per-turn overhead (see [Benchmark](BENCHMARK.md)).
257
+ Past roughly a thousand concepts it becomes a real, measurable cost again —
258
+ that specific, numeric point is the trigger for building `Index::FTS5`, not
259
+ a guess made in advance.
260
+
261
+ ## Export
262
+
263
+ ```
264
+ insika knowledge:export --agent store-support --out ./export [--tenant loja-a]
265
+ ```
266
+
267
+ Writes one `<name>.md` per concept — the storage format IS the export
268
+ format, so this is a dump, not a converter (same discipline as Facts/
269
+ Harvest's own append-only records): each file is the concept's markdown,
270
+ byte for byte, directly consumable by okf-gem (`OKF::Bundle`) or graphify.
271
+ Re-running it is safe — nothing here is lossy, so there is no `--force` to
272
+ reason about.
273
+
274
+ `--format graphml` writes one combined `knowledge.graphml` instead: a node
275
+ per concept (`name`/`type`/`description`/`confidence`/`provenance` as node
276
+ data) and an edge per `[[link]]` that resolves to another concept in the
277
+ same export — a link to a concept outside the scope is dropped, never a
278
+ dangling edge. Hand-built, hand-escaped XML (no new dependency), directly
279
+ openable in Gephi, yEd, or graphify.
280
+
281
+ ## What's not here yet
282
+
283
+ - **The optional FTS5 index** — `knowledge.index: "fts5"` is accepted but
284
+ falls back to `Index::Scan`. Deliberately not built yet: `Scan` was
285
+ measured (above), not assumed, and it comfortably meets this feature's
286
+ target scale. A deployment whose concept count is heading past ~1000 per
287
+ agent is the evidence that would justify building the SQLite
288
+ `MATCH`/`bm25()` adapter — not before.
289
+ - **Decay** — recency is a ranking tiebreak today; a real confidence decay
290
+ curve is a later, evidence-driven addition, not a default.
data/docs/LOADTEST.md CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  title: Load test
3
- parent: Operate & prove it
4
- nav_order: 3
3
+ parent: Operate
4
+ nav_order: 4
5
5
  permalink: /loadtest/
6
6
  ---
7
7
 
@@ -16,7 +16,7 @@ The whole point: the engine exposes `POST /v1/responses` as an **SSE drop-in** o
16
16
  the OpenClaw gateway. Same contract → the same load tools work against either side,
17
17
  so you can measure the engine you are about to ship against the gateway it replaces.
18
18
 
19
- There are three scripts, each answering a different question:
19
+ There are four scripts, each answering a different question:
20
20
 
21
21
  | Script | Question it answers | Needs a provider? |
22
22
  |--------|---------------------|-------------------|
@@ -24,8 +24,13 @@ There are three scripts, each answering a different question:
24
24
  | `scripts/loadtest.rb` | End-to-end: TTFB/total/tokens/cache/error against `/v1/responses` | Yes |
25
25
  | `scripts/loadtest-local.sh` | Single-proc baseline vs N-worker multi-proc on one box | Yes |
26
26
  | `scripts/loadtest_session.rb` | A full multi-message session (CEP, searches, FAQ) under C concurrent sessions — direct to the engine (`--surface engine`, stream vs steer) or through the consumer's real ingress (`--surface web`, the consumer's widget API) | Yes |
27
+ | `insika soak` | Does the deploy degrade over 72 h of steady load? | Yes |
27
28
 
28
- All three take `--help` / `-h`.
29
+ The first four take `--help` / `-h`; the soak is a shipped command (`insika soak --help`) rather
30
+ than a repo script. Bursts and uptime are different questions: a wave driver
31
+ measures a burst, and the soak's arrival process measures *degradation over
32
+ uptime* — the load-test table above deliberately stops where
33
+ [Soak](SOAK.md) begins.
29
34
 
30
35
  ---
31
36
 
@@ -64,7 +69,7 @@ frame that carries it.
64
69
  INSIKA_URL=http://localhost:9292 \
65
70
  OPENCLAW_GATEWAY_TOKEN=xxx \
66
71
  bundle exec ruby scripts/loadtest.rb \
67
- --agents bia,my-store --concurrency 16 --iterations 3 \
72
+ --agents demo,my-store --concurrency 16 --iterations 3 \
68
73
  --message "hi, how are you?"
69
74
  ```
70
75
 
@@ -75,7 +80,7 @@ Runs against a local server **or** a remote one (e.g. Railway) — just point
75
80
 
76
81
  | Flag | Default | Meaning |
77
82
  |------|---------|---------|
78
- | `--agents a,b,c` | `bia` | comma-separated agent ids (mapped to `model: openclaw:<agent>`) |
83
+ | `--agents a,b,c` | `demo` | comma-separated agent ids (mapped to `model: openclaw:<agent>`) |
79
84
  | `--concurrency N` | `8` | concurrent turns per wave |
80
85
  | `--iterations N` | `1` | number of waves per agent |
81
86
  | `--message TEXT` | greeting | user message sent every turn |
@@ -98,7 +103,7 @@ Use `--dry-run` to sanity-check your flags/URL/token before firing real traffic
98
103
 
99
104
  ```bash
100
105
  INSIKA_URL=http://localhost:9292 OPENCLAW_GATEWAY_TOKEN=xxx \
101
- bundle exec ruby scripts/loadtest.rb --agents bia --concurrency 16 --dry-run
106
+ bundle exec ruby scripts/loadtest.rb --agents demo --concurrency 16 --dry-run
102
107
  ```
103
108
 
104
109
  ### `--same-user` and the cache
@@ -130,7 +135,7 @@ DEEPSEEK_API_KEY=sk-... ./scripts/loadtest-local.sh [WORKERS] [CONCURRENCY]
130
135
  | `DEEPSEEK_API_KEY` | — (required) | real turns hit the provider; also auto-sourced from `.env.local` |
131
136
  | `OPENCLAW_GATEWAY_TOKEN` | falls back to `ADMIN_TOKEN`, then `local-demo` | Bearer for the sweep |
132
137
  | `PORT` | `9299` | bind port for the local Falcon |
133
- | `AGENT` | `bia` | agent id to load |
138
+ | `AGENT` | `demo` | agent id to load |
134
139
 
135
140
  The final block prints the lock counts; the expected reading is `0` for both:
136
141
 
@@ -163,7 +168,7 @@ drop-in for the gateway, you only change **where it points**:
163
168
  # In the OpenClaw checkout, run its gateway loadtest against the HARNESS:
164
169
  OPENCLAW_GATEWAY_URL=http://localhost:9292 \
165
170
  OPENCLAW_GATEWAY_TOKEN=<same bearer the engine accepts> \
166
- node scripts/loadtest-gateway.mjs --agents bia --concurrency 16 --iterations 3
171
+ node scripts/loadtest-gateway.mjs --agents demo --concurrency 16 --iterations 3
167
172
  ```
168
173
 
169
174
  Then run the exact same command with `OPENCLAW_GATEWAY_URL` pointing at the real
@@ -174,7 +179,7 @@ gateway, and diff the two reports. This is the shadow comparison the pilot needs
174
179
  - The OpenClaw checkout containing `scripts/loadtest-gateway.mjs` and Node installed.
175
180
  - A **bearer token accepted by both** sides. For the engine that is
176
181
  `OPENCLAW_GATEWAY_TOKEN` (see DEPLOY.md); point the gateway run at its own token.
177
- - **The same agent id provisioned on both** sides (e.g. `bia`) so `model:
182
+ - **The same agent id provisioned on both** sides (e.g. `demo`) so `model:
178
183
  openclaw:<agent>` resolves on each. On the engine, provision via
179
184
  `scripts/import_pack.rb`.
180
185
  - The **same provider** (or an equivalent-latency one) behind each, otherwise you
@@ -224,6 +229,7 @@ Work top-down and **measure before assuming** — avoid premature topology optim
224
229
  | 4 | Insika vs gateway, identical knobs | §4 (either method) | Is the engine at parity with the engine it replaces before cut-over? |
225
230
  | 5 | Cold vs hot conversation (cache) | `loadtest.rb` with/without `--same-user 1` | Expected steady-state cost/latency once conversations warm up. |
226
231
  | 6 | Remote (Railway) vs local | `loadtest.rb` with `INSIKA_URL` remote | Network/deploy overhead of the real environment. |
232
+ | 7 | Degradation over uptime (72 h) | `insika soak` (see [Soak](SOAK.md)) | The cut argument: does latency or memory grow with uptime? Run it last — it needs the topology settled first. |
227
233
 
228
234
  **Reaching for horizontal scale is only justified after 1–3 show the single box is
229
235
  the limit.** If it is, the paths are: sharding-by-tenant +
data/docs/MEDIA.md ADDED
@@ -0,0 +1,128 @@
1
+ ---
2
+ title: Media
3
+ parent: Integrate
4
+ nav_order: 3
5
+ permalink: /media/
6
+ ---
7
+
8
+ # Media
9
+
10
+ The engine transports media, it never means it. Photos, voice notes and documents
11
+ travel through the message contract as additive content parts; what a picture
12
+ *means* — a fitting room, a product mockup — stays a skill on top.
13
+
14
+ ## In — the message contract
15
+
16
+ The engine transports media, it never means it. The message accepts additive
17
+ **content parts** alongside the text — voice notes and photos travel, and any
18
+ skill (a fitting room, an image QA) stays a consumer layer on top:
19
+
20
+ ```bash
21
+ curl -X POST /v1/messages?stream=false -H "Authorization: Bearer $TOKEN" \
22
+ -d '{ "agent": "store-support", "session_id": "chat-7",
23
+ "message": "", "parts": [
24
+ { "type": "audio", "url": "https://cdn.example.com/voz.ogg" },
25
+ { "type": "image", "url": "https://cdn.example.com/sofa.jpg" },
26
+ { "type": "document", "url": "https://cdn.example.com/receita.pdf" }
27
+ ] }'
28
+ ```
29
+
30
+ - **Audio** is transcribed (RubyLLM STT; model via `INSIKA_STT_MODEL`) and the
31
+ text enters the turn marked `source: "voice"` on the terminal event — the
32
+ consumer's signal the person spoke. A consumer that transcribes itself can
33
+ send the text with `"source": "voice"` directly. A domain vocabulary hint
34
+ (product names, brand terms) rides the transcription as `prompt:` —
35
+ per-agent `stt_prompt` (the DSL setter, or the Studio config form) beats the
36
+ deployment-wide `INSIKA_STT_PROMPT` env, which beats nothing. OPERATOR
37
+ config, never customer input.
38
+ - **Images** attach to the model's ask (vision); the provider bills them and
39
+ the usage flows like any ask. The first image URL is also
40
+ `{{ctx.image_url}}` for data tools — photo analysis outside the prompt, the
41
+ tool's own egress applying when it fetches.
42
+ - **Documents** (a prescription, a recipe, an invoice — most often a PDF)
43
+ attach the same way images do; the first document URL is
44
+ `{{ctx.document_url}}`. A model without document support fails the ask at
45
+ `:ruby_llm` with the provider's own error — the transport does not preflight
46
+ capability. Capped at 10 MB (`MAX_DOCUMENT_BYTES` — "a prescription, not an
47
+ archive"), separately from the 5 MB image cap.
48
+ - Media URLs (audio, image AND document) are fetched by the engine through
49
+ the same egress guard (a private/metadata target is refused — SSRF) and a
50
+ size ceiling per kind (1 MB audio, 5 MB image, 10 MB document: the bytes
51
+ land in this process). A refused, oversized or unreadable part fails the turn
52
+ loudly at the `:media` stage, never a silent drop.
53
+ - **Media alone is a turn.** A voice note with no caption is `parts` and an
54
+ empty `message` — the transcription becomes the message at the `:media`
55
+ stage. A media message never joins another turn (`collect`/`steer` move text
56
+ only, and the parts would be left behind), and a transcription that comes
57
+ back empty fails the turn instead of asking the model about nothing.
58
+ - **Parts are contract at the edge** — a malformed part (unknown type, an
59
+ image/audio/document without `url`, a text without `text`) is a 422 before
60
+ dispatch on `/v1/messages` and `/v1/responses`. `document` is an ADDITIVE
61
+ part type (the compatibility rule in [the /v1 API](API.md) — no
62
+ `Insika-Version` bump needed).
63
+ - `/v1/responses` accepts the OpenAI multimodal shape: `input` as an array of
64
+ text/image/audio/document parts.
65
+
66
+ ### Image editing
67
+
68
+ `generate_image` (below) doesn't only generate — it can EDIT an existing
69
+ image, using `RubyLLM.paint`'s `with:`/`mask:`. The tool exposes
70
+ `source_image_urls` (an array — up to 4) and `mask_url`; when the model omits
71
+ `source_image_urls` AND the turn carries an inbound photo, that photo is
72
+ edited by default — no URL round-trip needed for "edit the photo the customer
73
+ just sent". Explicit URLs always win over the default. A text-to-image call
74
+ (no sources at all) is byte-identical to before this feature existed.
75
+
76
+ What the edit MEANS — a virtual try-on, a product mockup on the customer's
77
+ wall — is the calling skill's business; the tool only transports the bytes.
78
+ Not every image model can edit (`dall-e-3` cannot); a call against a
79
+ non-editing model fails at the provider, surfaced verbatim.
80
+
81
+ ## Out — generated media
82
+
83
+ The turn can **produce** an image or a voice clip — but only when both sides of
84
+ the gate agree, because nothing leaks by default. The agent declares it may
85
+ generate media (`outputs` on the profile), and the **channel** declares it can
86
+ receive it (`channel.capabilities` on the request):
87
+
88
+ ```bash
89
+ curl -X POST /v1/responses -H "Authorization: Bearer $TOKEN" -d '{
90
+ "model": "openclaw:store-support", "user": "chat-7",
91
+ "input": "manda a foto do sofá da promoção",
92
+ "channel": { "capabilities": ["image_output", "audio_output"] }
93
+ }'
94
+ ```
95
+
96
+ ```ruby
97
+ agent = Insika.agent("store-support") do
98
+ instructions "…"
99
+ outputs image: { model: "gpt-image-1", size: "1024x1024" }, # the AGENT's half
100
+ tts: { model: "tts-1", voice: "alloy" }
101
+ end
102
+ ```
103
+
104
+ - **Both gates** must pass for the model to even see the `generate_image` /
105
+ `tts` tools: the agent opted in (`outputs`) and the request declared the
106
+ matching capability (`image_output` / `audio_output` — an unknown value is a
107
+ 422, never a silent ignore). The "abstraction admits only what leaks" rule.
108
+ - **The media rides the envelope, never the answer text.** The terminal event
109
+ and the `/v1/responses` completed frame carry an additive `output_parts`
110
+ array — `{ type: "image", mime_type:, base64:, model: }` /
111
+ `{ type: "audio", mime_type:, base64:, model: }`. The model's prose stays
112
+ the `:content` answer; the channel consumes the bytes next to it.
113
+ - **Generation is billed and counted.** Image tokens join the turn's usage
114
+ (like any ask). The speech API reports no token counts, so a TTS call adds
115
+ an honest `usage.media` counter and the part carries the `model` for
116
+ consumer-side pricing.
117
+ - **Seams, not magic.** The generator is injectable per kind (specs stub it);
118
+ the defaults are lazy: images via RubyLLM (paint), speech via a thin POST to
119
+ the OpenAI-compatible `/audio/speech` endpoint using the same provider
120
+ config the chat uses — RubyLLM as of 1.16.0 has no speech API. A generated
121
+ part over 8 MB refuses loudly, never silently truncates.
122
+ - **Not here:** what the generated image *means* — a fitting room, a product
123
+ mockup — is a skill on top. The engine transports bytes and cost.
124
+
125
+ ## See also
126
+
127
+ - [The /v1 API](API.md) — the full message contract.
128
+ - [Channels](CHANNELS.md) — how WhatsApp and the web widget carry these parts.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  title: Observability
3
- parent: Operate & prove it
3
+ parent: Operate
4
4
  nav_order: 1
5
5
  permalink: /observability/
6
6
  ---
@@ -36,12 +36,28 @@ something that left no task of its own behind:
36
36
  | `:turn_steered` | `task_id`, `count`, `total` | a message arrived mid-run and was appended to the turn in flight |
37
37
  | `:turn_steer_released` | `task_id`, `released_as`, `count` | the run could not absorb it, so it became the turn `released_as` |
38
38
  | `:turn_interrupted` | `task_id`, `replaced_by` | the turn was abandoned mid-run, and which turn replaced it |
39
- | `:turn_stuck` | `task_id`, `agent`, `reason`, `message` | the agent declared it could not proceed (`signal_stuck`, WS5) — the deterministic signal a consumer escalates on |
39
+ | `:turn_stuck` | `task_id`, `agent`, `reason`, `message` | the agent declared it could not proceed (`signal_stuck`) — the deterministic signal a consumer escalates on |
40
40
  | `:channel_delivered` | `channel`, `outbox_id`, `status`, `attempts`, `error` | the answer reached the platform (or did not) — the turn completing says nothing about that |
41
- | `:delivery_failed` | `channel`, `outbox_id`, `status`, `attempts`, `error` | a delivery exhausted its bounded retries — the alert face of the row above (WS6) |
42
- | `:budget_warning` | `agent`, `tenant`, `window`, `spent`, `cap` | a calendar budget crossed its threshold (`alert_at` or a soft cap) — once per window (WS2) |
43
- | `:breaker_open` | `agent`, `ref`, `tenant` | the reliability circuit breaker tripped for a `(tenant, provider/model)` — further turns fail fast until the cooldown (WS3/WS6) |
44
- | `:ttft` | `task_id`, `session_id`, `ttft_ms` | the provider's time-to-first-token on the streaming envelopeonly under `INSIKA_TURN_TIMING`, once per turn (WS6) |
41
+ | `:delivery_failed` | `channel`, `outbox_id`, `status`, `attempts`, `error` | a delivery exhausted its bounded retries — the alert face of the row above |
42
+ | `:budget_warning` | `agent`, `tenant`, `window`, `spent`, `cap` | a calendar budget crossed its threshold (`alert_at` or a soft cap) — once per window |
43
+ | `:breaker_open` | `agent`, `ref`, `tenant` | the reliability circuit breaker tripped for a `(tenant, provider/model)` — further turns fail fast until the cooldown |
44
+ | `:provider_failure` | `agent`, `ref`, `error`, `kind` | one attempt against `ref` failed and spent a retry emitted with or without a circuit breaker |
45
+ | `:provider_fallback` | `agent`, `from`, `to`, `error`, `kind` | the turn ROTATED mid-flight to the next node of the fallback chain, and the error that caused it |
46
+ | `:ttft` | `task_id`, `session_id`, `ttft_ms` | the provider's time-to-first-token on the streaming envelope — only under `INSIKA_TURN_TIMING`, once per turn |
47
+ | `:distillation_completed` | `session_ref`, `agent`, `proposals`, `dropped{}`, `deduped`, `cost{}` | a session's traffic was distilled into N proposals — counts and ids only, never a fact value |
48
+ | `:proposal_approved` | `proposal_id`, `status`, `operator` | a human approved a distilled fact on the Facts page — written to memory via CAS |
49
+ | `:proposal_rejected` | `proposal_id`, `status`, `operator` | a human rejected it (the reason is on the proposal record, not in the event) |
50
+ | `:proposal_dismissed` | `proposal_id`, `status`, `operator` | a human dismissed it — the tuple is latched, never proposed again |
51
+ | `:proposal_stale` | `proposal_id`, `status`, `operator` | the CAS lost: the fact moved after distillation — re-presented with both values on the wiki, never silently overwritten |
52
+ | `:harvest_mined` | `agent`, `run_id`, `candidates`, `rejected{}`, `cost{}` | a mining pass finished — counts and rule ids only, never a skill body |
53
+ | `:harvest_gated` | `run_id`, `candidate_id`, `agent`, `eval_passed`, `conversion_passed`, `reason` | the double gate's verdict on one candidate — ids and verdicts only |
54
+ | `:skill_promoted` | `agent`, `skill`, `candidate_id`, `snapshot_ref`, `promotion_ref`, `approver` | a human approved a mined skill — it is live for the store, with the snapshot for rollback |
55
+ | `:skill_rolled_back` | `snapshot_ref`, `skill`, `agent`, `operator` | the snapshot was restored — the promotion row carries the `rolled_back_at` stamp |
56
+ | `:knowledge_learned` | `name`, `type`, `agent` | a turn's transcript taught the agent a new concept, or a related sighting merged into an existing one — name and type only, never the body |
57
+ | `:knowledge_conflict` | `name`, `agent` | a repeat sighting contradicted what's on record — never merged, appended under a heading, needs a human in the Studio |
58
+ | `:knowledge_deleted` | `name`, `agent` | an operator removed a concept from the Studio |
59
+ | `:knowledge_retrieved` | `name`, `agent` | the model called `load_knowledge` — the adoption signal (retrieval calls per conversation), not the `<knowledge>` injection itself |
60
+ | `:knowledge_backfilled` | `agent`, `sessions`, `concepts`, `conflicts`, `dropped{}` | the recovery re-scan (`insika knowledge:backfill`) finished replaying an agent's stored sessions |
45
61
 
46
62
  `delivery_failed` and `breaker_open` are the two the operator config is pointed at
47
63
  (`alerts.webhook` on the profile): each only fires when something durable did
@@ -366,6 +382,49 @@ consumer that fell far behind would have its subscription closed (telemetry stop
366
382
  the turn does not). Span and instrument operations are cheap, so there's ample
367
383
  headroom.
368
384
 
385
+ ## Process vitals — `GET /v1/vitals`
386
+
387
+ OTel carries turn/tool telemetry; it says nothing about the **process**. For the
388
+ questions a soak (or any operator) asks — *which process is this, how long has it
389
+ been up, how much memory does it hold, and what is the Ruby heap doing?* — there
390
+ is one read-only route:
391
+
392
+ ```bash
393
+ curl -H "Authorization: Bearer $TOKEN" https://<target>/v1/vitals
394
+ ```
395
+
396
+ ```jsonc
397
+ {
398
+ "boot_id": "20260820T09-…", // one per container start; a change = a restart
399
+ "pid": 42,
400
+ "started_at": "2026-08-20T09:00:00Z",
401
+ "uptime_s": 259200,
402
+ "version": "0.2.0",
403
+ "ruby": "ruby 3.4.1 …",
404
+ "yjit": true,
405
+ "rss_bytes": 536870912, // nil when unreadable — never a guess
406
+ "gc": { "heap_live_slots": …, "major_gc_count": …, "malloc_increase_bytes": … },
407
+ "threads": 8,
408
+ "in_flight": 1, // the executor's in-flight turns
409
+ "db_bytes": { "db": …, "wal": …, "shm": … },
410
+ "at": "2026-08-20T09:00:00Z"
411
+ }
412
+ ```
413
+
414
+ The two fields that make it a **restart detector**: `boot_id` (one per container
415
+ start, exported by the entrypoint and shared by every worker) and `pid`. A
416
+ `boot_id` change is a container restart; a `pid` change under the same `boot_id`
417
+ is a worker respawn — the event a platform metrics API cannot see.
418
+
419
+ - **Operator-only.** The route is not in the public allowlist (no bearer →
420
+ unauthorized) and not on the tenant surface, so only an operator reads
421
+ process internals. `/up` stays the public health probe and carries no
422
+ process data.
423
+ - **Reads no store.** Pure OS/VM readings — safe to poll at any rate, and it
424
+ cannot contend with turns.
425
+ - **The soak's sampler.** [Soak](SOAK.md) polls it hourly; a `nil` RSS reads as
426
+ missing coverage, never as zero.
427
+
369
428
  ---
370
429
 
371
430
  *Packaging note.* Today the bridge lives in the Insika repo as an opt-in core