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
@@ -0,0 +1,110 @@
1
+ ---
2
+ title: Performance
3
+ parent: Operate
4
+ nav_order: 8
5
+ permalink: /performance/
6
+ ---
7
+
8
+ # Performance
9
+
10
+ What the engine does to stay cheap per turn, and what was tried and left out. A turn
11
+ mostly waits on the model provider, so the engine's job is to add little CPU, hold
12
+ little memory and never block the other turns in the same process.
13
+
14
+ To measure your own load, see [Deploy › Measuring performance](DEPLOY.md#measuring-performance--load),
15
+ [Load test](LOADTEST.md) and the [Benchmark](BENCHMARK.md).
16
+
17
+ ## Numbers you can reproduce
18
+
19
+ Each one comes from a script in this repository, with no API key and no real data.
20
+
21
+ | What | Number | Run |
22
+ |---|---|---|
23
+ | Engine work per turn, model stubbed | p50 ≈ 0.4 ms, ~1,700–1,900 turns/s per process | `bundle exec ruby scripts/bench.rb` ([Benchmark](BENCHMARK.md)) |
24
+ | SQLite writes, 1 to 8 processes on one file | ~25–30k writes/s in total, zero "database is locked" | `bundle exec ruby scripts/bench_store.rb 1,2,4,8 3000` ([Deploy](DEPLOY.md#1-sqlite-write-ceiling-no-provider--bench_storerb)) |
25
+ | One key lookup, `execute` vs a cached prepared statement | ~2.7 µs vs ~0.9 µs (Ruby 4.0, laptop) | the snippet below |
26
+
27
+ A turn makes a few hundred store calls at most. Saving ~2 µs on each is well under a
28
+ millisecond per turn, which is why statement caching is in the "left out" table.
29
+
30
+ ```ruby
31
+ require "sqlite3"; require "benchmark"
32
+ db = SQLite3::Database.new(":memory:")
33
+ db.execute("CREATE TABLE kv (scope TEXT, key TEXT, value TEXT, PRIMARY KEY (scope, key))")
34
+ 1000.times { |i| db.execute("INSERT INTO kv VALUES (?, ?, ?)", ["s", "k#{i}", "x" * 500]) }
35
+ sql = "SELECT value FROM kv WHERE scope = ? AND key = ?"
36
+ st = db.prepare(sql)
37
+ Benchmark.bm(9) do |b|
38
+ b.report("execute") { 200_000.times { |i| db.get_first_value(sql, ["s", "k#{i % 1000}"]) } }
39
+ b.report("prepared") { 200_000.times { |i| st.reset!; st.bind_params("s", "k#{i % 1000}"); st.step } }
40
+ end
41
+ ```
42
+
43
+ ## What the engine does
44
+
45
+ **No Rails, no ORM.** The server is Falcon with Rack and Roda. The store is a key-value
46
+ table driven by the `sqlite3` gem directly. Nothing loads a framework or builds model
47
+ objects on the hot path.
48
+
49
+ **One fiber per turn.** Each turn runs as an `Async` task. While it waits on the provider,
50
+ a tool or the store, the same process runs other turns. One worker holds many concurrent
51
+ conversations without threads. See [Architecture](ARCHITECTURE.md).
52
+
53
+ **Session actors end when idle.** Turns of the same session run in order through a small
54
+ actor. Its loop ends as soon as its queue drains and it leaves the executor's table, so an
55
+ idle session holds no fiber.
56
+
57
+ **SQLite tuned for many writers.**
58
+
59
+ - WAL mode with `synchronous = NORMAL`.
60
+ - `busy_handler_timeout=` instead of `busy_timeout=`. The C-level handler sleeps while
61
+ holding the GVL, so a worker waiting on another process's write lock would freeze every
62
+ fiber it runs. The Ruby-level handler lets the scheduler keep the other turns going.
63
+ - **Group commit.** Writes that arrive while one is waiting for the lock join the same
64
+ transaction and share one `COMMIT`. Each caller still returns only after its own write is
65
+ committed, so durability and per-fiber order do not change. Fewer commits means less
66
+ fighting over the file's single write lock when several workers share it.
67
+ - Prefix and recent-items reads are one range query, not one query per key.
68
+ - `INSIKA_SLOW_STORE_MS` logs every slow store call, and every slow group-commit batch with
69
+ its time split into wait, statements and commit. A long wait with a short commit means
70
+ another process holds the lock. A long commit means the batch itself is heavy.
71
+
72
+ **Memory goes back to the OS.**
73
+
74
+ - `MALLOC_ARENA_MAX=2` caps glibc's malloc arenas. Under a multi-worker load, the peak
75
+ stays lower with the same latency.
76
+ - `MALLOC_MMAP_THRESHOLD_=131072` pins the mmap threshold. Large buffers stay mmapped and
77
+ return to the OS when freed instead of fragmenting the arenas.
78
+ - `Insika::MemoryTrim` runs on a quiet worker: no turn in flight and enough allocated since
79
+ the last pass. It runs a full GC and then `malloc_trim(0)`. Without it, a worker stays at
80
+ its peak after a burst, because a GC alone gives nothing back to the OS. It needs the
81
+ `fiddle` gem and does nothing where `malloc_trim` does not exist (macOS, musl).
82
+ - Long-lived fibers return `nil` from their blocks, so a finished task's result is not kept
83
+ alive by a stale reference.
84
+
85
+ **YJIT on.** The reference image sets `RUBY_YJIT_ENABLE=1`. It lowers CPU per turn
86
+ (serialization, streaming, context building) for a small memory cost per worker.
87
+
88
+ **Workers are a contract input, not a tuning knob.** More worker processes means more
89
+ memory and more writers on one SQLite file. Start with one. Add workers, and the
90
+ [Router](ROUTER.md) in front of them, only when one process's CPU is the limit. See
91
+ [Deploy › The process model](DEPLOY.md#the-process-model).
92
+
93
+ ## Tried and left out
94
+
95
+ Each of these was measured on a production-like load and did not pay off. Measure again
96
+ before you reopen one.
97
+
98
+ | Idea | Why it is out |
99
+ |---|---|
100
+ | jemalloc or mimalloc instead of glibc | `MALLOC_ARENA_MAX` plus `MemoryTrim` already got most of the gain. Both left a higher memory floor after repeated loads, and `malloc_trim` does nothing under them. |
101
+ | Preforking workers from a parent with the code loaded (copy-on-write) | Saves memory only right after boot. After a load, each worker's floor is its own heap, not duplicated code. |
102
+ | `GC.compact` during the trim | Fewer heap pages, same RSS. |
103
+ | Async HTTP adapter for the provider | Slower end to end than plain `Net::HTTP`, and it breaks Streamable-HTTP MCP outside an Async reactor. |
104
+ | Write-behind for traces, compressing large stored values | No measurable gain over group commit. Old compressed rows are still read. |
105
+ | Postgres store for speed on one box | Every store call becomes a network round trip, and a turn makes many store calls. Use it to share the store across nodes, not for speed. See [Deploy › Postgres store](DEPLOY.md#postgres-store-optional). |
106
+ | Caching prepared SQLite statements | Saves microseconds per call. Too small next to a turn that waits on the provider. |
107
+
108
+ Once group commit is in place, writer contention on one SQLite file under sustained heavy
109
+ load is a matter of architecture: fewer writer processes, a faster volume, or a shared
110
+ store. More application-level tuning does not move it.
data/docs/PLUGINS.md CHANGED
@@ -73,7 +73,7 @@ require "ruby_llm"
73
73
  module WeatherPlugin
74
74
  class GetWeather < RubyLLM::Tool
75
75
  description "Looks up the current weather for a city"
76
- param :city, desc: "City name"
76
+ parameter :city, description: "City name"
77
77
 
78
78
  def execute(city:) = { city: city, temp_c: 24, condition: "sunny" }
79
79
  end
@@ -169,9 +169,9 @@ require "insika/plugin"
169
169
  Insika::Plugin.announce(File.expand_path("../..", __dir__))
170
170
  ```
171
171
 
172
- Discovery details worth knowing: the manifest may be named `insika.plugin.yml`
173
- (preferred) or `plugin.yml` (deprecated — it warns); one manifest per directory,
174
- with `insika.plugin.yml` winning if both exist; and if two roots ship the same
172
+ Discovery details worth knowing: the manifest is named `insika.plugin.yml` (a leftover
173
+ `plugin.yml` from before 1.0 is not loaded and warns to rename it); one manifest per
174
+ directory; and if two roots ship the same
175
175
  `id`, the **first root wins** and the second is skipped.
176
176
 
177
177
  ### Secrets
data/docs/POLICY.md CHANGED
@@ -34,7 +34,15 @@ Builtins cover tool-, skill-, and workflow-allowlisting, plus
34
34
  does not allow or deny but *tags* a tool as needing human approval. Set
35
35
  `approvals_required: [tool names]`; the gate then fires when the model tries to
36
36
  call that tool, suspending the turn until an operator approves it in the Studio.
37
- See [Security](SECURITY.md#human-approval).
37
+ A data tool's `requires_evidence` check runs before this approval gate; an unknown
38
+ ID is blocked without asking the operator. See
39
+ [Tools](TOOLS.md#provenance-checking-ids-before-a-write) and
40
+ [Security](SECURITY.md#human-approval).
41
+
42
+ Checkpoints preserve tools enabled by `tool_search`; resuming restores only those
43
+ still allowed by the current policy. If a tool halts the turn, unanswered sibling
44
+ calls receive a non-execution result instead of waiting for approval or executing.
45
+ Their outstanding operator approvals are rejected with `resolved_by: engine:halted`.
38
46
 
39
47
  ## Layer 3: Guardrails (content safety)
40
48
 
@@ -92,8 +100,8 @@ budget daily: 100_000, monthly: 2_000_000, soft: false # or soft: true
92
100
 
93
101
  ### Reliability — retries, fallback, circuit breaker
94
102
 
95
- The provider interaction is a single attempt by default (RubyLLM's own 2
96
- transport retries aside). For a store that cannot have a dead model take the
103
+ Without a reliability policy, transport retries use the provider context's
104
+ configuration. For a store that cannot have a dead model take the
97
105
  chat down, the reliability policy is DATA on the profile:
98
106
 
99
107
  ```ruby
@@ -102,11 +110,12 @@ reliability retries: 2, backoff: "exponential",
102
110
  circuit_breaker: { after: 10, within: 60, cooldown: 300 }
103
111
  ```
104
112
 
105
- - **Retries** — transient failures (`:retryable` / `:rate_limited_*` per the
106
- error classification) retry with exponential backoff, up to `retries`.
107
- A `:fatal` (auth, billing, bad request) is NEVER retried or rotated. Each
108
- attempt runs on a fresh chat — the customer-visible answer comes only from
109
- the attempt that returns.
113
+ - **Retries** — RubyLLM owns transport retries for native chats, up to `retries`
114
+ per request, using its transient-error classification and backoff. Once stream
115
+ data has arrived, the same request is not retried. `backoff: "exponential"`
116
+ doubles the native interval; `"constant"` keeps it fixed. Interval and jitter
117
+ come from the provider context. Insika does not add another retry loop.
118
+ Fatal authentication/billing/request errors do not trigger model rotation.
110
119
  - **Fallback** — after a node's retries, the turn ROTATES to the next model in
111
120
  the chain: the profile's `fallback` refs first, then the platform
112
121
  `fallback_models`. The turn's usage is attributed to the model that actually
@@ -115,12 +124,15 @@ reliability retries: 2, backoff: "exponential",
115
124
  `within` seconds open the circuit; while open, the turn fail-fasts with the
116
125
  typed `circuit_open` + `retry_after` (remaining cooldown) and the provider is
117
126
  never touched. After `cooldown` a half-open trial closes the circuit on
118
- success or reopens it on failure.
119
- - **`timeout`** — per-attempt ceiling (default 30s), counted as a retryable
120
- failure.
121
-
122
- Absent `reliability` = the plain single attempt, byte-for-byte today's
123
- behavior.
127
+ success or reopens it on failure. A native retry sequence that ultimately
128
+ fails counts once, not once per internal HTTP attempt. `provider_failure`
129
+ reports that exhausted sequence; native usage records retain individual attempts.
130
+ - **`timeout`** — the native request timeout and the overall ceiling for one
131
+ model's execution (default 30s), including its retries. Exhaustion can rotate
132
+ to the next allowed model but never adds another same-model retry.
133
+
134
+ Absent `reliability` leaves the provider context unchanged and does not enable
135
+ Insika's model rotation or circuit breaker.
124
136
 
125
137
  ### Intent routing — classify before you answer
126
138
 
data/docs/RELEASING.md CHANGED
@@ -14,11 +14,20 @@ is invisible to it. Do not publish on rspec alone.
14
14
 
15
15
  ## Before anything
16
16
 
17
+ RubyLLM comes from RubyGems (`~> 2.1` in the gemspec); the development Gemfile must
18
+ not Git-pin it, or the published gem resolves a different RubyLLM than the suite ran.
19
+ The measured performance tradeoff was explicitly accepted for the RubyLLM 2 migration,
20
+ not for future regressions; see [migration results](RUBYLLM_2_MIGRATION.md).
21
+
17
22
  1. The suite is green: `bundle exec rspec`.
18
23
  2. `lib/insika/version.rb` carries the version being published.
19
24
  3. Every new `lib/` file is **tracked in git**. The gemspec's `files` come from
20
25
  `git ls-files`: an untracked file builds without a warning and the installed
21
26
  gem fails at `require` — this is exactly the failure this proof exists to catch.
27
+ 4. If changing the `fencing` default (currently off), first run the deployment's
28
+ golden cases with it enabled, retain the comparison report, and document the
29
+ behavior change in `CHANGELOG.md`. A default change needs evaluation evidence;
30
+ a scheduled version number alone is not the release gate.
22
31
 
23
32
  ## Cut the gem
24
33
 
data/docs/ROUTER.md CHANGED
@@ -106,6 +106,13 @@ INSIKA_ROUTER_BACKENDS=http://127.0.0.1:9292,http://127.0.0.1:9293,http://127.0.
106
106
  bundle exec insika-router
107
107
  ```
108
108
 
109
+ The image does this for you: set `INSIKA_WORKERS=N` (and keep
110
+ `WEB_CONCURRENCY=1`) and `deploy/entrypoint.sh` runs `deploy/workers.sh`, which
111
+ starts the N workers on ports from `INSIKA_WORKER_BASE_PORT` (default 9300),
112
+ waits until each accepts connections, then binds the router on `$PORT` with a
113
+ backend timeout of `INSIKA_ROUTER_BACKEND_TIMEOUT` (default 1300s there, so a
114
+ long turn is never cut). Unset or `1` boots exactly as before.
115
+
109
116
  `insika doctor` treats `WEB_CONCURRENCY>1` as `ok` (not `error`/`warn`) once
110
117
  it sees `INSIKA_ROUTER_BACKENDS` or `INSIKA_ROUTER_BACKENDS_DNS` set — it
111
118
  cannot verify a router process is actually running at those addresses, only
@@ -0,0 +1,307 @@
1
+ ---
2
+ title: RubyLLM 2 Migration
3
+ parent: Ship it
4
+ nav_order: 6
5
+ permalink: /rubyllm-2-migration/
6
+ ---
7
+
8
+ # RubyLLM 2 Migration
9
+
10
+ Status reviewed on 2026-09-24: implemented on `codex/rubyllm-2-migration`,
11
+ and **approved for merge and testing on the existing Railway staging service**.
12
+ Keep Insika's durable runtime and policy;
13
+ delegate provider protocols, tool schemas, and cache formatting to RubyLLM.
14
+ No Rails, package extraction or database migration. JSONSchemer is a direct
15
+ dependency for workflow and tool argument validation.
16
+
17
+ The follow-up ecosystem audit and execution status (kept with the internal plans)
18
+ records replacements and the concrete gates that still block others.
19
+
20
+ ## Dependency
21
+
22
+ The Gemfile and lockfile pin RubyLLM commit
23
+ `099c381b44fec6f1d72b2b63b9a9eea7635755f1`, the merge of
24
+ [native MCP PR 959](https://github.com/crmne/ruby_llm/pull/959).
25
+ The external `ruby_llm-mcp` dependency and its Origin-header patch are removed.
26
+ The pinned source still reports version 2.0.0, but published 2.0.0 does not contain
27
+ this client. Before publishing Insika, require a released RubyLLM version with
28
+ native MCP in the gemspec and remove the Git pin. A gemspec cannot carry it.
29
+
30
+ Existing MCP records and cached tool descriptors remain readable. Discovery and
31
+ execution use native Streamable HTTP or stdio; Insika retains policy, evidence,
32
+ credential masking, the stdio opt-in, and the egress allowlist. Refresh preserves
33
+ the server's strict boolean read-only hint. Legacy SSE records fail with a request
34
+ to configure a Streamable HTTP endpoint; they are not silently rewritten. Native
35
+ MCP also requires HTTPS outside loopback, even with the HTTP egress opt-in.
36
+ Sampling, roots and server logging are not supported by the native client. Insika
37
+ does not yet expose native MCP input-request resumption or its OAuth UI; unanswered
38
+ input requests surface as tool errors through the existing wrapper.
39
+
40
+ Native MCP validation on 2026-09-23: **5,614 examples, zero failures**, seed 46560.
41
+ Local protocol fixtures cover real stdio and HTTP sockets, modern discovery,
42
+ legacy handshake/session headers, event-stream responses, authorization headers,
43
+ tool errors, structured content, attachments, and read-only metadata. No external
44
+ production MCP server was invoked for these fixture checks. The current-candidate
45
+ benchmark below was repeated after this dependency switch; it does not call MCP.
46
+
47
+ The official RubyLLM coding skill (now from 2.1.0) is copied into `.agents/skills/rubyllm`.
48
+ Refresh it from the installed gem when upgrading RubyLLM, not from upstream main.
49
+
50
+ ## Preserved Behavior
51
+
52
+ - Native `step` drives the existing runtime; a terminal tool finishes its batch
53
+ and prevents another model request. Siblings, result IDs, steering, and output remain covered.
54
+ - Tools use `parameter` and `parameters_schema`; data-defined JSON schemas stay unchanged.
55
+ - Native cache markers preserve the stable/volatile Anthropic instruction boundary.
56
+ - Native tokens and model readers retain Insika's usage fields. Auxiliary billing
57
+ includes cache reads and writes without counting thinking twice.
58
+ - RubyLLM now handles operator approval gating for native chats. Insika retains
59
+ authorization, persisted decisions, customer confirmation, security checks and UI.
60
+ Decisions bind to task, turn, call ID, real tool name and arguments. Checkpoints
61
+ preserve pending calls, completed results, usage, evidence and filtered halt text.
62
+ Recorded side effects are skipped on replay; terminal results survive interruption.
63
+ Direct workflow/customer-confirmation paths retain the existing blocking gate.
64
+ Legacy tool-name decisions do not authorize newly generated native calls.
65
+ A crash between an external effect and its local marker still requires downstream
66
+ idempotency; this is not an exactly-once guarantee.
67
+ - Opt-in native
68
+ compaction now uses separate model-bound state and filtered stored history, with
69
+ original-prefix recovery on fallback. See [native compaction](CONTEXT.md#native-responses-compaction)
70
+ for its conservative budget and supported providers.
71
+
72
+ ## Verification
73
+
74
+ Latest completed checks (2026-09-24, before merge): **5,616 RSpec
75
+ examples, zero failures**, seed 19184; **53 Studio tests passed**. Assets were
76
+ rebuilt during the preceding Studio implementation and remain unchanged.
77
+ Final diff review reran 200 focused runtime/MCP/contract examples with no failures
78
+ and found no actionable introduced regression in the reviewed scope.
79
+ Browser checks covered tool grouping by origin/server, filtering, collapsible
80
+ sections and section selection without changing other sections or denied tools.
81
+ Changes still require **Save tools**; filtering does not change permissions.
82
+
83
+ Local development validation also exercised a real provider reply and MCP tool
84
+ discovery. The user confirmed WhatsApp replies and execution through the Metabase
85
+ MCP. This is smoke-test evidence, not a comparative quality eval or a new benchmark.
86
+ An HTTP data tool and an MCP instance may use different credentials: successful
87
+ MCP discovery does not validate a separately configured HTTP tool.
88
+
89
+ ### Earlier Verification
90
+
91
+ Native transport retries are enabled for reliable native chats, with isolated
92
+ per-chat configuration and no outer Insika retries. The approved behavior is
93
+ per-request retries, stopping once streaming starts. Insika still owns fallback,
94
+ model policy, tenant circuits and the overall per-model deadline. Circuits count
95
+ an exhausted native sequence once, not each internal HTTP failure. The native
96
+ request timeout is also configured from the profile. Completed tool results
97
+ survive fallback, and failed-stream redactor tails are discarded.
98
+ Native `with_fallbacks` adoption remains deferred: it restores the original model
99
+ before the next generation and requires adaptation for model-bound compaction.
100
+
101
+ After native compaction, operator approvals and native transport retries: **5,603 examples, zero failures** (seed 32098).
102
+ The execution record (kept with the internal plans) has the new contracts, remaining replacement gates and offline benchmark.
103
+
104
+ Full RSpec after the benchmark correction: **5,519 examples, zero failures**
105
+ (seed 55367). Studio: **50 tests passed** during the preceding migration validation.
106
+ The real-gem contract has 17 behavioral cases, with no missing-gem skip.
107
+ Synthetic SQLite records passed 1.16 write -> 2.0 read/write -> 1.16 read/write,
108
+ including history, pending/resolved approvals, and recorded side-effect markers.
109
+ An isolated installed gem passed reply, SQLite embed, Rack mount, actual HTTP/SSE,
110
+ Studio login, packaged assets/docs, and lazy-load guards using an offline provider.
111
+ That earlier install used the former external MCP Git dependency. On 2026-09-24,
112
+ the current native-MCP candidate was built and installed in a fresh GEM_HOME
113
+ outside the checkout and passed all those checks again. The synthetic SQLite
114
+ 1.16 -> 2.0 -> 1.16 record rehearsal also passed again. A Linux ARM64 Docker
115
+ build with the deployment lockfile passed and loaded native MCP while preserving
116
+ the core lazy-load guard. The isolated gem install explicitly pins RubyLLM's Git
117
+ revision and resolves other allowed dependencies; it is not a public gem-install
118
+ proof or a substitute for verifying Railway's x86_64 deployment.
119
+ Browser checks passed for login, agent configuration, skill/provider creation,
120
+ Playground replies, completed tasks, and session transcripts. Approval/resume
121
+ flows were covered by automated tests, not separately exercised in the browser.
122
+
123
+ ## Performance
124
+
125
+ ### Current Candidate (2026-09-24)
126
+
127
+ Five alternating runs per version in each of three modes (stub, real Chat with
128
+ a synthetic model, and real Chat with catalog model `gpt-4o`), plus five SQLite
129
+ runs per version. The catalog experiment was subsequently corrected and rerun
130
+ five times per version, as described below. Ruby 4.0.6,
131
+ arm64-darwin25, YJIT off; 300 latency turns, 30 warmups, concurrency 16 and
132
+ 20 waves per scenario. Native MCP and the latest Studio changes are present.
133
+ The same existing benchmark harnesses load each checkout's actual runtime.
134
+
135
+ All 40 benchmark processes succeeded: 58,500 engine turns including warmups,
136
+ matching provider/tool/history/chunk counts, and 300,000 SQLite writes with zero
137
+ reported lock errors. Automated suites ran after the measurement. Development
138
+ servers remained running, so workstation noise is not eliminated.
139
+
140
+ Profiling exposed a flaw in the earlier catalog experiment: its flag assertion
141
+ wrapped `RubyLLM.chat`, but `main` constructs chats through a context. Its
142
+ `assume_model_exists: false` override therefore did not reach the actual Chat.
143
+ Those earlier catalog numbers do not establish equivalent registry behavior.
144
+ The corrected local harness forces the option at `Chat#initialize` and asserts
145
+ that the resulting model is the exact registry object. The separate profiler
146
+ confirms one registry lookup per measured turn in both versions.
147
+
148
+ All ten corrected benchmark processes succeeded, with matching activity counts
149
+ across 19,500 additional turns including warmups. Median corrected measurements:
150
+
151
+ | Scenario | Main p50 / p95 ms | Migration p50 / p95 ms | Turns/s, before -> after |
152
+ | --- | --- | --- | --- |
153
+ | Greeting | 1.37 / 2.01 | 1.48 / 2.48 | 654.1 -> 603.8 |
154
+ | Two tool rounds | 1.46 / 2.10 | 1.80 / 2.42 | 603.5 -> 515.7 |
155
+ | History | 1.51 / 2.08 | 1.62 / 2.25 | 573.9 -> 558.2 |
156
+
157
+ Catalog p50 increases are **7.3-23.3%**, or **0.11-0.34 ms per turn**.
158
+ Peak RSS increased **13.9%** (87,769,088 -> 99,991,552 bytes), and tool
159
+ allocations increased 8.4%. Tool p50 ranges did not overlap (main 1.35-1.58 ms,
160
+ migration 1.65-1.85 ms); the other scenarios' ranges overlap. These are new runs,
161
+ so differences from the invalid comparison are not attributable solely to the
162
+ harness correction. Synthetic-model p50 increased 18.3-28.2% and peak
163
+ RSS 29.4%. Stub p50 changes were -5.3%, +4.5% and +9.1%, with peak RSS +2.3%.
164
+ SQLite median writes/s stayed within 2.3% of baseline in either direction.
165
+
166
+ The proposed 10% gate still fails numerically. This isolates an increase along the real-Chat
167
+ path, not its cause; it does not measure native HTTP, MCP calls, UI speed or
168
+ provider latency. No safety or accounting was disabled. Earlier measurements
169
+ below are retained as historical comparisons, not current acceptance evidence.
170
+
171
+ **Decision (2026-09-24):** The project owner accepted the measured performance
172
+ tradeoff for this migration and approved continuing with RubyLLM 2's native
173
+ runtime. The additional 0.34 ms in the two-tool benchmark does not block ongoing
174
+ development validation. This is an explicit exception, not a passing benchmark
175
+ or acceptance of future regressions. There are no real operational workflows
176
+ beyond the tests already executed. Continue validation with the existing
177
+ automated tests, controlled integration checks and offline benchmarks. Measure
178
+ live latency, errors and memory when actual usage exists; that is not a current
179
+ development prerequisite. Investigate
180
+ upstream improvements with isolated evidence; do not add custom caches or a
181
+ replacement tool loop without demonstrated waste. The performance decision alone
182
+ did not authorize deployment. The project owner subsequently authorized merge
183
+ to `main` and testing through its automatic Railway staging deployment. Gem
184
+ publication remains outside that authorization.
185
+
186
+ ### Targeted Profiling (2026-09-24)
187
+
188
+ Three alternating instrumented runs per version, each with 1,001 measured
189
+ two-tool turns after 30 warmups, completed without errors. Both versions made
190
+ one registry lookup, three provider calls and two tool calls per measured turn.
191
+ The probe wraps selected methods using the monotonic clock and `GC.stat`; it is
192
+ local diagnostic tooling, not a runtime dependency.
193
+
194
+ Median inclusive time per turn, rounded, across those instrumented runs:
195
+
196
+ | Scope | Main ms | Migration ms |
197
+ | --- | --- | --- |
198
+ | `Executor#ask_on` (generation and tool cycle) | 0.402 | 0.659 |
199
+ | `Executor#create_chat` | 0.261 | 0.279 |
200
+ | `Executor#record_model_visible` | 0.011 | 0.042 |
201
+ | `Executor#persist_turn` | 0.326 | 0.322 |
202
+ | `ContextBuilder#call` | 0.251 | 0.267 |
203
+
204
+ The largest observed increase is inside the generation/tool cycle. Native
205
+ `instrumentation_payload`, `record_completion_event`, and
206
+ `record_out_of_band_usage` account for approximately 0.030, 0.022 and 0.038 ms
207
+ per turn respectively. The latter is the offline provider's fallback accounting
208
+ path, not the accounting path used by native HTTP providers. Message creation
209
+ also grows from approximately 0.063 to 0.079 ms for the same 55 messages/chunks
210
+ per turn. These nested times overlap other scopes and must not be added to them.
211
+
212
+ Registry lookup is slower (approximately 0.073 -> 0.111 ms), but native 2.0
213
+ creates one connection per turn instead of 1.16's two; the overall Chat creation
214
+ difference is much smaller. The trace increase includes corrected behavior:
215
+ `main` treats the tool Hash as pairs and misses schemas, whereas the migration
216
+ captures actual tool schemas and system instructions. Removing those records
217
+ would reintroduce a correctness problem, not provide an equivalent optimization.
218
+
219
+ Instrumented timings include probe overhead and cannot apportion the exact
220
+ uninstrumented regression. Instrumented allocation counts are not clean
221
+ allocation measurements. RSS growth remains unattributed; MCP and HTTP were not
222
+ exercised. The next investigation should isolate RubyLLM's per-round bookkeeping
223
+ without Insika before proposing an upstream optimization. Keep accounting,
224
+ approval checks and trace correctness intact; no custom tool loop or cache was
225
+ added during profiling.
226
+
227
+ ### Earlier Catalog-Model Experiment (Invalid Flag Assertion)
228
+
229
+ These results used the ineffective `RubyLLM.chat` assertion described above.
230
+ They are historical only, not an equivalent-registry comparison. Five
231
+ alternating runs per version named `gpt-4o`. The offline provider is
232
+ registered under `openai` only inside the experiment; it does not make HTTP calls.
233
+ Its intended assertions were bypassed by context-created chats. No application
234
+ model-selection behavior was changed.
235
+ Settings match the earlier comparison: Ruby 4.0.6, YJIT off, 300 latency turns,
236
+ 30 warmups, concurrency 16, 20 waves, and identical tool/history/chunk counts.
237
+ Every scenario in all ten runs completed without errors.
238
+
239
+ | Scenario | 1.16 p50 / p95 ms | 2.0 p50 / p95 ms | Turns/s, before -> after |
240
+ | --- | --- | --- | --- |
241
+ | Greeting | 1.23 / 1.65 | 1.32 / 2.06 | 549.2 -> 629.2 |
242
+ | Two tool rounds | 1.36 / 1.75 | 1.62 / 2.09 | 620.3 -> 539.8 |
243
+ | History | 1.28 / 1.78 | 1.44 / 1.95 | 590.6 -> 561.9 |
244
+
245
+ Median p50 changes are +7.3%, +19.1%, and +12.5%. Median peak RSS is
246
+ 89,194,496 -> 105,971,712 bytes (+18.8%); tool allocations rise 4.8%.
247
+ These numbers cannot determine whether registry lookup avoidance explains the
248
+ difference. They are offline engine-plus-Chat results, not
249
+ live-provider latency or proof of a RubyLLM defect. Investigate the remaining
250
+ tool-loop and memory differences before making an upstream performance claim.
251
+
252
+ ### Earlier Unknown-Model Experiment
253
+
254
+ The following results are retained for comparison, not generalized to catalog
255
+ models. The fake model used `assume_model_exists: true`, which bypassed registry
256
+ resolution in 1.16 but not 2.0.
257
+
258
+ Five interleaved runs per version, Ruby 4.0.6, YJIT off, same host and settings.
259
+ Real Chat with a synthetic provider, no network; provider/tool/chunk/history counts match.
260
+
261
+ | Scenario | 1.16 p50 / p95 ms | 2.0 p50 / p95 ms | Turns/s, before -> after |
262
+ | --- | --- | --- | --- |
263
+ | Greeting | 1.38 / 1.83 | 1.60 / 2.23 | 476.3 -> 571.2 |
264
+ | Two tool rounds | 1.35 / 1.95 | 1.79 / 2.44 | 619.8 -> 491.5 |
265
+ | History | 1.34 / 1.87 | 1.78 / 2.37 | 597.3 -> 498.2 |
266
+
267
+ These replace the initial measurements: the offline provider omitted request
268
+ `model_info`, unlike native `Accounting::Usage::Tracker#succeed`. Supplying it
269
+ removes three spurious pricing catalog lookups per two-tool turn. The benchmark
270
+ spec now rejects those lookups. No production runtime workaround was added.
271
+
272
+ Corrected real-Chat p50 increased 15.9-32.8%; peak RSS increased 24.0%
273
+ (87,310,336 -> 108,232,704 bytes); tool allocations increased 7.4%.
274
+ RSS also increased 11.3% in stub mode, so the entire memory difference cannot
275
+ be attributed to RubyLLM catalog loading. SQLite had zero lock errors, but
276
+ throughput varied from +1.6% to -17.2%; the earlier no-regression claim is withdrawn.
277
+ These are host-specific samples, not a controlled attribution of every difference.
278
+
279
+ The standalone `scripts/rubyllm_profile.rb` reproduces the fixture defect and
280
+ the remaining catalog lookup without Insika. With `--request-context --profile`,
281
+ 1,000 two-tool turns make 1,000 `Models#find` calls in 2.0 versus none in 1.16.
282
+ RubyLLM 2 resolves catalog metadata even with `assume_model_exists`; 1.16 skipped
283
+ it for this non-local provider. In one instrumented run that lookup consumed
284
+ 112 ms total; fallback usage accounting consumed 39 ms. Inclusive timings must
285
+ not be summed. All three requests, both tools, history and aggregate tokens are
286
+ checked. The fake provider bypasses native transport and uses fallback usage
287
+ accounting, so this is not an end-to-end native-provider performance claim.
288
+
289
+ For upstream discussion, run the script outside Bundler with `1.16.0 --profile`
290
+ and `2.0.0 --request-context --profile`. Omitting `--request-context` deliberately
291
+ recreates the old fixture defect, not native provider behavior. The useful upstream
292
+ question is whether repeated registry resolution can be cheaper while preserving
293
+ model metadata and refresh semantics. No upstream patch or issue was published;
294
+ remaining latency and memory attribution is open.
295
+ The proposed 10% performance gate **failed**. No accounting or safety was disabled.
296
+ Reproduce the candidate with the commands in [Benchmark](BENCHMARK.md).
297
+
298
+ ## Release Gate
299
+
300
+ Public gem release still requires a published RubyLLM native-MCP dependency.
301
+ The measured performance tradeoff is accepted; current installed-artifact and
302
+ synthetic record-compatibility checks passed. Merge to `main` and testing of its
303
+ automatic Railway staging deployment are authorized. Confirm that deployment's
304
+ status and HTTP behavior before reporting success. No production rollout,
305
+ database schema migration or gem publication is part of this validation.
306
+ Record compatibility does not guarantee exactly-once external effects; keep
307
+ destination idempotency or reconciliation for an execution-before-checkpoint failure.
data/docs/SECURITY.md CHANGED
@@ -205,6 +205,32 @@ category: the agent's category reply → the agent's default → the builtin
205
205
  category → the builtin default. All of it is editable in the Studio Configuration
206
206
  form. See [Agents §Layer 3](POLICY.md#layer-3-guardrails-content-safety).
207
207
 
208
+ ## Third-party text is data (fencing)
209
+
210
+ Input guardrails cover the incoming message. The opt-in `fencing` flag also
211
+ sanitizes ordinary tool result strings, memory text and the injected knowledge
212
+ names/descriptions, and places a
213
+ fixed notice under the identity. It reduces known markup and Unicode tricks;
214
+ it does not make arbitrary third-party instructions safe.
215
+
216
+ The user/assistant-only extraction filter applies even when fencing is off.
217
+ Attachment captions and `load_knowledge` bodies are not fenced. The extraction
218
+ filter excludes direct tool messages,
219
+ but assistant paraphrases can still become extraction input. See
220
+ [Agents](AGENTS.md#fencing--third-party-text-is-data-never-instructions) for the
221
+ exact scope, exceptions, default and size cap.
222
+
223
+ ## Write provenance and serial execution
224
+
225
+ A data tool's `requires_evidence` gate checks IDs against the session ledger
226
+ before approval or backend execution. A known ID proves a prior lookup; it does
227
+ not authorize access or establish current stock, price or quantity limits.
228
+ Those checks remain the backend's responsibility.
229
+
230
+ Tools marked `side_effect` execute one at a time within a session, including in
231
+ parallel batches. This is not a cross-session or distributed backend lock.
232
+ See [Tools](TOOLS.md#provenance-checking-ids-before-a-write).
233
+
208
234
  ## Human approval
209
235
 
210
236
  Some tool calls should not happen unattended. Mark them with
@@ -220,6 +246,13 @@ resume path is durable: an approval that arrives after a restart still resumes t
220
246
  turn from its checkpoint (see
221
247
  [Architecture](ARCHITECTURE.md#durability-checkpoints-and-resume)).
222
248
 
249
+ There is a second approver: the customer. `customer_confirm: [tool names]` holds a
250
+ call the same way, but the turn *ends* with a question instead of suspending, and the
251
+ next message of the conversation decides it through `confirm_pending` /
252
+ `cancel_pending`. A hold nobody resolves expires when that next turn ends. Use it for
253
+ the write the person on the other end must agree to; use approval for the one a staff
254
+ member must. A tool cannot be on both lists. See [Tools](TOOLS.md#customer-confirmation-a-write-the-conversation-approves).
255
+
223
256
  ## Egress: the SSRF boundary
224
257
 
225
258
  Every outbound HTTP call from a data tool passes through the **EgressGuard**, a
@@ -242,7 +275,7 @@ defense-in-depth: without it, `ALLOW_PRIVATE` opens *any* private destination.
242
275
  > the request never leaves the process, and the conversation *looks* fine. Verify
243
276
  > tool health by the Studio session **trace** (a healthy call shows the backend's
244
277
  > `200`), never by the model's reply. Full detail in
245
- > [Tools §Egress](TOOLS.md#egress-the-ssrf-guard-and-its-silent-failure).
278
+ > [Tools §Egress](TOOLS.md#egress-the-ssrf-guard).
246
279
 
247
280
  ## Sandbox: confined execution
248
281
 
@@ -0,0 +1,40 @@
1
+ ---
2
+ layout: default
3
+ title: Shared conversations
4
+ ---
5
+
6
+ # Shared conversations
7
+
8
+ `shared_conversations: true` opts an agent into required central history. The
9
+ DSL accepts `shared_conversations true`; stored profiles preserve the flag.
10
+ Default is false. Configure `INSIKA_CONVERSATIONS_URL`, a scoped
11
+ `INSIKA_CONVERSATIONS_TOKEN`, and a durable native backend (`INSIKA_DB` or
12
+ `INSIKA_DATABASE_URL`). The client provisions the central conversation first.
13
+
14
+ The tenant-authenticated `/v1/responses` request adds `user_text` (original speech)
15
+ and `shared_conversation` with UUID `tenant_id`, `user_id`, `agent_id`,
16
+ `conversation_id`, `turn_id`, and `message_id`. Keep the
17
+ same turn/message IDs across retries. Existing `input` still contains the
18
+ presenter/context text used for this request. The authenticated tenant must match.
19
+ Omit `generation` to acquire a settled conversation automatically when admitting
20
+ the next turn. The engine reads and writes the acquired generation internally.
21
+ An explicit integer `generation` retains the legacy ownership check.
22
+
23
+ Upload incoming media to the central attachment endpoint first. Optional
24
+ `shared_conversation.content` carries canonical text/media parts with attachment
25
+ IDs; its text must match `user_text`. Generated image/audio bytes upload before
26
+ completion. Native checkpoints store attachment bytes and hydrate them at the
27
+ RubyLLM boundary. Full tool results retain secret-key masking without prompt caps.
28
+
29
+ Admission precedes model/tool work. Delivery follows central completion. A lost
30
+ completion acknowledgment reuses the native durable result; it never reruns tools.
31
+ An uncertain execution stays blocked for operator reconciliation. Only the same
32
+ native waiting approval can resume from its checkpoint under the current generation.
33
+ Unresolved customer confirmations block switching. Central deletion/outage fails
34
+ before cached history is used. Local native purge still requires an explicit cleanup.
35
+
36
+ Run the isolated HTTP proof (synthetic model/tools, temporary databases):
37
+
38
+ ```sh
39
+ SHARED_CONVERSATIONS_SOURCE=/path/to/shared-memory-repo bundle exec rspec spec/insika/shared_conversations_http_spec.rb
40
+ ```