insika 0.7.0 → 0.8.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 (49) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +116 -0
  3. data/README.md +4 -3
  4. data/bin/insika +43 -2
  5. data/docs/AGENTS.md +22 -3
  6. data/docs/ARTIFACTS.md +42 -0
  7. data/docs/CONTEXT.md +54 -12
  8. data/docs/DEPLOY.md +16 -7
  9. data/docs/LOADTEST.md +15 -27
  10. data/docs/MEDIA.md +1 -1
  11. data/docs/OBSERVABILITY.md +31 -2
  12. data/docs/POLICY.md +10 -4
  13. data/docs/RUNNING-LOCAL.md +2 -2
  14. data/docs/SECURITY.md +1 -1
  15. data/docs/SOAK.md +1 -1
  16. data/docs/TOOLS.md +24 -0
  17. data/docs/prompts/GO-LIVE.md +3 -3
  18. data/lib/insika/agent_profile.rb +26 -1
  19. data/lib/insika/chat_builder.rb +28 -17
  20. data/lib/insika/compaction.rb +196 -0
  21. data/lib/insika/context/builder.rb +6 -2
  22. data/lib/insika/context/fragment.rb +4 -1
  23. data/lib/insika/context/priority.rb +6 -0
  24. data/lib/insika/context/providers/briefing.rb +53 -24
  25. data/lib/insika/context/providers/session.rb +46 -10
  26. data/lib/insika/context_trace_store.rb +11 -1
  27. data/lib/insika/doctor.rb +105 -10
  28. data/lib/insika/dsl/runtime.rb +4 -0
  29. data/lib/insika/env_schema.rb +5 -6
  30. data/lib/insika/evals/transport.rb +1 -1
  31. data/lib/insika/executor.rb +64 -0
  32. data/lib/insika/loop_detector.rb +5 -34
  33. data/lib/insika/profile_source.rb +7 -0
  34. data/lib/insika/server/responses.rb +4 -4
  35. data/lib/insika/session_store.rb +34 -4
  36. data/lib/insika/settings_store.rb +8 -1
  37. data/lib/insika/soak/runner.rb +4 -4
  38. data/lib/insika/studio/app.rb +28 -6
  39. data/lib/insika/studio/forms.rb +11 -0
  40. data/lib/insika/studio/views/settings.erb +11 -0
  41. data/lib/insika/telemetry/recorder.rb +49 -1
  42. data/lib/insika/templates/daily-digest/README.md +9 -0
  43. data/lib/insika/templates/research-analyst/agent.rb +10 -0
  44. data/lib/insika/tool_batch.rb +67 -0
  45. data/lib/insika/tool_usage_report.rb +162 -0
  46. data/lib/insika/turn_budget.rb +91 -0
  47. data/lib/insika/version.rb +1 -1
  48. data/lib/insika.rb +7 -0
  49. metadata +5 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5c2b2fbe064af45aa132f93de00954b24dc504e052db2e059b4f6daa151e248b
4
- data.tar.gz: 50cabcc10d82eabdfd08de562bd61feddf1a9de68ee6427343c80c5cf6ad9d8c
3
+ metadata.gz: 345e57c18a277e8ecd49b2481300c1474c60b2e0c3c384a086740e0de5ec3a2e
4
+ data.tar.gz: 800794431e279fec34f135e4e203a0488b04066b667bc691806ea5f575d81530
5
5
  SHA512:
6
- metadata.gz: 3004e30a234349bcb91db185fcc31f9b52cbdeb7660c613b1fc229adf6bbbe717d9abc8ebcdc5bf9ecc125986694c0d2f3a82e1d48eb83105ff74db0467e4d91
7
- data.tar.gz: ca4bec6a8b8cd03ee1a5c2df627e7da778d3dd157f30f848c70f8077ac467570284bf934e3a4c8040fb223503855d9cdcad111b9139ed093c702b6a8850ea1ff
6
+ metadata.gz: 199cc16b9ea651ec2da1d2e6f7ddea5020194882da00cf96981e8d5d44086233862873d14640d11cd1d445f9d202992ed46691210f9fb1699b1a3d56a99b3145
7
+ data.tar.gz: 44d24bc53036da3803ee6eba6203d1addc8ccf0bd27c1aa15d1e9acfe763bca807ede55c2b7136ee2c245e5cd725541c719baa018c99e09638e964803bf688a9
data/CHANGELOG.md CHANGED
@@ -8,6 +8,122 @@ it is released. Entries land with the pull request that makes the change.
8
8
 
9
9
  ## [Unreleased]
10
10
 
11
+ ## [0.8.0] - 2026-08-31
12
+
13
+ The post-consolidation hardening wave: the last two OpenClaw-era names are gone
14
+ from the wire (**breaking** — `insika:<agent>` / `X-Insika-Agent` and
15
+ `INSIKA_GATEWAY_TOKEN`, no aliases), and the engine learned to manage its own
16
+ long conversations — in-session compaction (RFC-0044), a tool budget that warns
17
+ before it kills, goal recitation at the tail of the context, and a tool-usage
18
+ audit report. Update `/v1` consumers and the gateway-token variable together
19
+ with this upgrade.
20
+
21
+ ### Added
22
+
23
+ - **In-session compaction (RFC-0044).** When a session's uncompacted
24
+ transcript grows past `compact_after` messages (default 40), everything but
25
+ the last `keep_last` (default 20) is summarized by a cheap model
26
+ (`compaction.model` → platform `utility_model`) into one
27
+ `<conversation_summary>` history fragment; the tail stays verbatim and the
28
+ boundary is stable between compactions, so the prompt cache holds after it.
29
+ Runs post-turn, off the critical path, best-effort; persisted on the session
30
+ record with a monotonic boundary. Opt-in via the long-reserved Settings
31
+ `compaction` hash (the Studio general form is back), `insika doctor` warns
32
+ when it is enabled with no model, and every compaction is observable:
33
+ `:context_compacted` event, `insika.context.compacted` counter, `{upto,
34
+ runs}` + its own category in the context trace. See
35
+ [Context](docs/CONTEXT.md).
36
+
37
+ - **`insika tools:report` — the tool audit.** The per-session trace never
38
+ aggregated, so "which tools does this agent carry and never use?" had no
39
+ answer. The new CLI report reads the stored traces per agent (tasks →
40
+ sessions → `tool_traces`) and flags allowlisted-but-never-called tools,
41
+ tools over 30% errors in the window, and tools not called in N days
42
+ (default 14). Read-only — the operator removes. `--agent`, `--days`,
43
+ `--json`. See [Tools](docs/TOOLS.md).
44
+
45
+ - **Two series promoted to OTEL metrics.** `insika.cache.hit_rate` (histogram,
46
+ `%` — cache reads over the billed prompt, per turn, same arithmetic as the
47
+ Studio's per-agent series) and `insika.tool.loop_intervened` (counter — the
48
+ loop detector's one-shot warning, labelled by tool). Both computed in the
49
+ telemetry bridge from events that already existed: zero cost when OTEL is
50
+ off, no engine change. See [Observability](docs/OBSERVABILITY.md).
51
+
52
+ - **The doctor warns on a prompt file that outgrew a prompt.** The
53
+ `prompt-files` sweep now WARNs (never errors) past ~6 000 estimated tokens
54
+ or 600 lines, with the actual counts — the LLM-generated merchant-pack
55
+ shape that costs 20%+ extra tokens per turn. Corrupted-wrapper detection
56
+ is unchanged.
57
+
58
+ - **The tool budget is announced before it kills.** `max_tool_calls` was
59
+ enforced but silent: the model met the ceiling only when the turn died with
60
+ `stage: :tool_limit`, having delivered nothing. At **10 / 5 / 2** calls
61
+ remaining the engine now appends a short, escalating notice at the next
62
+ tool-batch boundary and emits `:tool_budget_warned`. It is a `user` message,
63
+ never a system one, so the cacheable system prefix stays byte-stable.
64
+ Verified live (DeepSeek): a turn asking for 30 lookups on a budget of 20 used
65
+ to die at `tool_limit`; it now stops at 18 and answers with what it has. The
66
+ turn's counter, the notices and the abort are one object (`TurnBudget`), and
67
+ the batch-boundary arithmetic it shares with the loop detector moved into
68
+ `ToolBatch`.
69
+
70
+ ### Changed
71
+
72
+ - **The briefing recites the goal at the TAIL of the context.** `<briefing>`
73
+ used to carry known facts, the still-missing list and the next step in one
74
+ `:system` block at the top. The goal half was **moved** (not copied) into a
75
+ new `<recitation>` block rendered after the whole history, as the last thing
76
+ the model reads before the current message — attention is strongest at the
77
+ end, and a goal stated only at the top is the first thing a 30-call turn
78
+ forgets. New `:tail` fragment placement; the head keeps the durable facts.
79
+
80
+ - **The `/v1/responses` wire names are insika-native**: the agent arrives as
81
+ `model: "insika:<agent>"` and the fallback header is `X-Insika-Agent`. Hard
82
+ rename, no alias — a consumer still sending `openclaw:<agent>` gets
83
+ "model/agent missing". Update every `/v1` consumer together with this upgrade.
84
+ - **`INSIKA_GATEWAY_TOKEN` replaces `OPENCLAW_GATEWAY_TOKEN`** as the Bearer for
85
+ `/v1` + `/a2a`. Hard rename, no alias: the old name was a leftover of the
86
+ OpenClaw migration and is now a foreign var the engine ignores. Update the
87
+ variable on every deployment before upgrading. The dead `OPENCLAW_AGENTS_DIR`
88
+ env spec (nothing read it) is gone.
89
+
90
+ ### Fixed
91
+
92
+ - **The Studio home charts went blank for three hours every day.** The activity
93
+ buckets key on calendar parts (date, hour) of `updated_at`, which the engine
94
+ writes in UTC, but `render_home` read `Time.now` in the host's local zone. On a
95
+ UTC-3 host, from 21:00 local onward "today" was already tomorrow in UTC: the
96
+ 14-day chart matched no session and the 24h sparkline's floor was set three
97
+ hours in the future, dropping the newest buckets. Stamps are now read through
98
+ one `utc_time` helper and `now` is UTC. Instant comparisons (`active_now`)
99
+ never had the bug — only the calendar arithmetic did.
100
+ - **The Conversations trend showed "−1" for the day's first conversation.** The
101
+ 14-day series is oldest-first and ends at today, so its last pair reads
102
+ `[yesterday, today]`; it was destructured the other way round, inverting the
103
+ sign on every conversations delta. The Messages card was always correct.
104
+ - **Declaring a tool allow/deny list now opts the profile into the policy.** The
105
+ lists are applied by exactly one policy — the builtin `tool_allowlist` — and
106
+ the policy engine runs only the policies a profile *names*. So an agent with
107
+ `tools_allow: ["a", "b"]` and no `policies` sent **every registered tool** to
108
+ the model, silently: 508 tools / 52 405 tokens of schemas per request against
109
+ an allowlist of 2, observed on a real run. `tools_deny` was ignored the same
110
+ way. `AgentProfile.build` now appends `tool_allowlist` whenever `tools_allow`,
111
+ `tools_deny` or `tools_allow_groups` is declared — presence, not emptiness, so
112
+ `tools_allow: []` still means "no tools". A stored record left in the old
113
+ state is repaired on read but stays wrong on disk, so `doctor` gained a
114
+ `tool-allowlist` **error** naming each agent to re-save.
115
+ - **A DSL agent could write its briefing and never read it back.** The
116
+ `Insika.agent`/`Insika.system` runtime wired every context provider except
117
+ `Briefing`, so `update_briefing` / `set_next_step` persisted state that never
118
+ reached a prompt. Now wired, before `Session`, like `config/wiring.rb`.
119
+ - **Docker image shipped without `docs/`, so `GET /start.md` was a 500 with
120
+ `INSIKA_ONBOARDING=1`.** `.dockerignore` excluded the whole tree; the onboarding
121
+ surface reads `docs/onboarding/start.md`, `docs/prompts/` and the public
122
+ `docs/*.md` from disk at request time. Only the Jekyll build output and the
123
+ internal notes are excluded now, and a spec asserts `.dockerignore` never drops
124
+ a file `Insika::Onboarding` serves. Found on the first Railway deploy with
125
+ onboarding on; the gem was never affected (its payload keeps `docs/`).
126
+
11
127
  ## [0.7.0] - 2026-08-29
12
128
 
13
129
  The pre-1.0 consolidation: MCP servers as first-class tools, a template gallery,
data/README.md CHANGED
@@ -143,7 +143,7 @@ navigates by.
143
143
  - [Evals](docs/EVALS.md) — the cases that grade an agent: rubrics, the judge panel, and the pre-merge gate.
144
144
  - [Refinement](docs/REFINEMENT.md) — read an agent's own traffic back as a ranked report of what broke.
145
145
  - [Outcomes and follow-ups](docs/OUTCOMES.md) — what the traffic was worth in business terms, and the tool that comes back on a promise.
146
- - [Knowledge](docs/KNOWLEDGE.md) — extract durable concepts from finished conversations into a per-agent knowledge base; provenance-stamped, PII-redacted, best-effort with a re-scan recovery path (retrieval and consolidation are not shipped yet).
146
+ - [Knowledge](docs/KNOWLEDGE.md) — extract durable concepts from finished conversations into a per-agent knowledge base; provenance-stamped, PII-redacted, best-effort with a re-scan recovery path.
147
147
  - [Facts](docs/FACTS.md) — distill finished customer conversations into proposed facts; a human approves them into memory (nothing is ever applied automatically).
148
148
  - [Harvest](docs/HARVEST.md) — mine real traffic for SKILL proposals; the negative list, the evidence-ledger grounding filter and the double gate hold every proposal to a human approval and the append-only log (nothing is ever applied automatically).
149
149
 
@@ -176,5 +176,6 @@ and [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md). Vulnerabilities go through
176
176
 
177
177
  ## Status
178
178
 
179
- Pre-releaseAPIs may still change, nothing is tagged yet
180
- ([CHANGELOG.md](CHANGELOG.md)). Licensed **MIT** ([LICENSE](LICENSE)).
179
+ Pre-1.0published on [RubyGems](https://rubygems.org/gems/insika); APIs may
180
+ still change between minor versions ([CHANGELOG.md](CHANGELOG.md)). Licensed
181
+ **MIT** ([LICENSE](LICENSE)).
data/bin/insika CHANGED
@@ -32,6 +32,7 @@ module Insika
32
32
  when "harvest" then harvest(argv)
33
33
  when "harvest:negative" then harvest_negative_import(argv)
34
34
  when "harvest:criterion" then harvest_criterion_check(argv)
35
+ when "tools:report" then tools_report(argv)
35
36
  when "knowledge:backfill" then knowledge_backfill(argv)
36
37
  when "knowledge:export" then knowledge_export(argv)
37
38
  when "soak" then exit(Insika::Soak::Runner.main(argv, stdout: $stdout, stderr: $stderr))
@@ -65,6 +66,11 @@ module Insika
65
66
  seed the profile's negative list from a rules file
66
67
  harvest:criterion check --file F
67
68
  strict-load the frozen conversion criterion
69
+ tools:report [--agent ID] [--days N] [--json]
70
+ tool usage per agent over the stored traces:
71
+ allowlisted-but-never-called, error rate > 30%
72
+ in the window, and stale (not called in N days).
73
+ Read-only — the operator removes
68
74
  knowledge:backfill --agent ID [--since ISO8601] [--json]
69
75
  the recovery path for post-turn extraction: replays
70
76
  the agent's stored sessions through the same
@@ -337,7 +343,7 @@ def evals_simulate(argv)
337
343
  require "fileutils"
338
344
  require "time"
339
345
  opts = { base_url: ENV["INSIKA_URL"] || "http://localhost:9292",
340
- token: ENV["OPENCLAW_GATEWAY_TOKEN"] || ENV["ADMIN_TOKEN"] || "local-demo",
346
+ token: ENV["INSIKA_GATEWAY_TOKEN"] || ENV["ADMIN_TOKEN"] || "local-demo",
341
347
  timeout: 120, persona_model: ENV["EVAL_PERSONA_MODEL"],
342
348
  judge_model: ENV["EVAL_JUDGE_MODEL"], judge_provider: ENV["EVAL_JUDGE_PROVIDER"] }
343
349
  OptionParser.new do |o|
@@ -345,7 +351,7 @@ def evals_simulate(argv)
345
351
  o.on("--persona FILE", "the persona YAML — a case with a `persona:` key (required)") { |v| opts[:persona] = v }
346
352
  o.on("--target T", "agent id (over --base-url) or an A2A http(s) URL (required)") { |v| opts[:target] = v }
347
353
  o.on("--base-url URL", "deployment base URL (default #{opts[:base_url]})") { |v| opts[:base_url] = v }
348
- o.on("--token T", "gateway token (default OPENCLAW_GATEWAY_TOKEN/local-demo)") { |v| opts[:token] = v }
354
+ o.on("--token T", "gateway token (default INSIKA_GATEWAY_TOKEN/local-demo)") { |v| opts[:token] = v }
349
355
  o.on("--timeout N", Integer, "per-turn read timeout, and the A2A poll budget in seconds (default 120)") { |v| opts[:timeout] = v }
350
356
  o.on("--staging", "the target is a staging deployment (side effects acceptable)") { opts[:staging] = true }
351
357
  o.on("--eval-profile", "the target runs an eval profile (side-effect tools swapped for fakes)") { opts[:eval_profile] = true }
@@ -1162,6 +1168,41 @@ end
1162
1168
  "last #{n} session(s)"
1163
1169
  end
1164
1170
 
1171
+ # insika tools:report [--agent ID] [--days N] [--json]
1172
+ #
1173
+ # The tool audit: reads the SAME durable backend as `doctor` (tasks →
1174
+ # sessions → tool_traces), no app boot. Read-only — it names never-called
1175
+ # allowlisted tools, high-error tools and stale tools; the operator removes.
1176
+ def tools_report(argv)
1177
+ opts = { json: false, days: Insika::ToolUsageReport::WINDOW_DAYS }
1178
+ OptionParser.new do |o|
1179
+ o.banner = "Usage: insika tools:report [--agent ID] [--days N] [--json]"
1180
+ o.on("--agent ID", "narrow the report to one stored agent") { |v| opts[:agent] = v }
1181
+ o.on("--days N", Integer, "window for error-rate and staleness (default #{opts[:days]})") { |v| opts[:days] = v }
1182
+ o.on("--json", "emit the report as JSON") { opts[:json] = true }
1183
+ end.parse!(argv)
1184
+
1185
+ backend = Insika::Wiring::Graph.backend_from_env
1186
+ config_store = Insika::ConfigStore.new(store: backend)
1187
+ report = Insika::ToolUsageReport.new(
1188
+ task_store: Insika::TaskStore.new(store: backend),
1189
+ tool_trace_store: Insika::ToolTraceStore.new(store: backend),
1190
+ profile_source: Insika::StoredProfileSource.new(config_store: config_store)
1191
+ ).generate(days: opts[:days], agent: opts[:agent])
1192
+
1193
+ if opts[:agent] && report.agents.empty?
1194
+ warn "insika tools:report: no stored agent '#{opts[:agent]}'"
1195
+ exit 2
1196
+ end
1197
+
1198
+ if opts[:json]
1199
+ require "json"
1200
+ puts JSON.pretty_generate(report.to_h)
1201
+ else
1202
+ puts report.to_s
1203
+ end
1204
+ end
1205
+
1165
1206
  # Config stores over the real durable backend — no app boot / no DEEPSEEK.
1166
1207
  def build_doctor
1167
1208
  backend = Insika::Wiring::Graph.backend_from_env
data/docs/AGENTS.md CHANGED
@@ -32,9 +32,10 @@ All three land on the **same** config-over-code import path — they differ only
32
32
  ergonomics, not in what they produce.
33
33
 
34
34
  - **DSL** — `Insika.agent("id") { … }` builds an agent definition and imports it
35
- into the durable store. Best for code-defined agents and examples. The DSL
36
- auto-enables the tool- and skill-allowlist policies. `model` is optional a nil
37
- model resolves the platform `default_model` at turn start.
35
+ into the durable store. Best for code-defined agents and examples. The DSL also
36
+ names the **skill**-allowlist policy for you (the tool one is implied by any
37
+ declared tool list, on every path). `model` is optional — a nil model resolves
38
+ the platform `default_model` at turn start.
38
39
  - **API** — `POST /v1/agents` with a definition (a "pack": an agent config plus its
39
40
  prompt files, skills, and data-tools). The import is **idempotent** and
40
41
  **authoritative** — what leaves the definition leaves the agent, so a
@@ -108,6 +109,14 @@ DEFAULT_LIMITS = {
108
109
 
109
110
  `build` merges your overrides over these — you set only the deltas.
110
111
 
112
+ `max_tool_calls` is announced before it kills. At **10 / 5 / 2** calls remaining
113
+ the engine appends a short user message at the next tool-batch boundary
114
+ ("2 tool calls left — consolidate what you already have and answer now"), so a
115
+ long turn converges on an answer instead of dying with `stage: :tool_limit` and
116
+ delivering nothing. It is a `user` message, never a system one: the system prefix
117
+ stays byte-stable and `prompt_caching` keeps hitting. Each threshold fires at
118
+ most once per turn and emits `:tool_budget_warned` on the event stream.
119
+
111
120
  `max_tool_repeat` is the loop guard: the same tool called with **identical
112
121
  arguments** that many times in a row gets ONE in-turn warning (a user message at
113
122
  the next tool-batch boundary: "you already ran this, answer with what you
@@ -382,6 +391,16 @@ The same three-state rule governs tools, skills, context providers, and workflow
382
391
  For tools, a paired deny list (`tools_deny`) **always wins**, and
383
392
  `tools_allow_groups` unions a per-group allowlist on top of `tools_allow`.
384
393
 
394
+ **Declaring a list is opting into it.** The lists are applied by one policy, the
395
+ builtin `tool_allowlist`, and the policy engine runs only the policies a profile
396
+ names — so declaring `tools_allow` without naming that policy used to mean the
397
+ model got *every* registered tool. The engine now adds `tool_allowlist` for you
398
+ the moment any of `tools_allow`, `tools_deny` or `tools_allow_groups` is
399
+ declared, on every creation path. Presence is the trigger, not content:
400
+ `tools_allow: []` means "no tools" and enforces just as hard. If a stored agent
401
+ predates this, `insika doctor` names it — the engine repairs it on read, but the
402
+ stored record stays wrong until you re-save it.
403
+
385
404
  Three capabilities invert the default — `nil`/absent means **OFF**, not "all":
386
405
  `subagents`, `memory`, and `guardrails` (each defaults to off or a conservative
387
406
  setting, never "everything on"). `tool_output_compression` is a fourth: opt-in
data/docs/ARTIFACTS.md CHANGED
@@ -67,6 +67,48 @@ No script, no external fetch, no forms. The model writes HTML with **inline
67
67
  SVG** for charts — that is a skill instruction (palette, tables, pure-SVG bars),
68
68
  not engine code. A "real" charting need is a plugin.
69
69
 
70
+ ## Reasoning effort on a report turn
71
+
72
+ A report turn is not one shape of work — it plans, then it mines, then it
73
+ writes. `thinking` is one value per agent for the whole turn, so an agent set to
74
+ `high` pays deliberation on every one of the 30–50 tool calls a real report
75
+ makes, and that is where the 300 s turn timeout gets spent.
76
+
77
+ Split the phases across agents instead, which the engine already supports today:
78
+
79
+ ```ruby
80
+ Insika.system do
81
+ # The miners: one narrow question each, no judgement to make.
82
+ agent("sales_miner") do
83
+ model "deepseek-v4-flash"
84
+ params thinking: "low"
85
+ tools %w[query_sales]
86
+ instructions "Answer ONE question about sales from the store data. Numbers, no prose."
87
+ end
88
+
89
+ # The orchestrator: it plans the report and writes it. This is the turn
90
+ # that deserves the deliberation.
91
+ agent("reporter") do
92
+ model "deepseek-v4-flash"
93
+ params thinking: "high"
94
+ tools %w[save_artifact]
95
+ subagents "sales_miner"
96
+ instructions "Plan the report, call spawn_subagents ONCE for every number you need, then write the page and save_artifact it."
97
+ end
98
+ end
99
+ ```
100
+
101
+ Two things make this work: a child inherits the *environment* (model, thinking)
102
+ only as a **default**, so its own `params thinking:` wins; and `spawn_subagents`
103
+ runs the children in parallel, so wall-clock is the slowest miner rather than
104
+ the sum. Each child also mines in its own isolated context, which is what keeps
105
+ the orchestrator's context from filling with raw rows.
106
+
107
+ Measure it before reaching for anything cleverer: the numbers that matter are
108
+ the turn's wall-clock, the timeout rate, and the judge score on the same report.
109
+ Per-phase effort *inside* a single turn is a real idea, but it is only worth
110
+ building once this recipe is shown not to be enough.
111
+
70
112
  ## Limits and retention
71
113
 
72
114
  - **Size cap** — `INSIKA_ARTIFACT_MAX_BYTES` (default 1 MB): an artifact is a
data/docs/CONTEXT.md CHANGED
@@ -28,9 +28,10 @@ into a deterministic prompt:
28
28
  | **Skill trigger** | `<active_skill>` | 85 | volatile | Level-2 bodies: the agent's `skills_eager` set, plus the ones whose `triggers:` match the message — see [Skills](SKILLS.md). |
29
29
  | **Knowledge** | `<knowledge>` | 77 | volatile | Level-1 top-K learned concepts for the turn's message (+ one-hop `[[links]]`), only if `knowledge.retrieve` is on. Cuttable — see [Knowledge](KNOWLEDGE.md). |
30
30
  | **Memory** | `<memory>` | 75 | volatile | Durable facts + recent notes, only if `memory` is on. Cuttable. |
31
- | **Briefing** | `<briefing>` | 65 | volatile | The session's working state (known fields, still-missing list, next step) — only if the pack declared `briefing_fields`. Cuttable. |
31
+ | **Briefing** | `<briefing>` | 65 | volatile | The session's working state — the *known* fields only. Only if the pack declared `briefing_fields`. Cuttable. |
32
32
  | **Session** | history | 60–79 | volatile | The running transcript; priority scales with recency. |
33
33
  | **Request** | `<request_context>` | 40 | volatile | Turn variables + tenant. Most cuttable; sits last. |
34
+ | **Briefing (tail)** | `<recitation>` | 95 | volatile | The still-missing list + next step, rendered **after the whole history** as a `user` message — the last thing the model reads before the current message. |
34
35
 
35
36
  The ordering is deliberate: the render order is **identity layer first, volatile
36
37
  layer after** — nothing volatile can sit above the cache boundary, whatever its
@@ -53,14 +54,37 @@ cacheable prefix byte-stable (see the prefix cache below).
53
54
  > returns empty turns, raise `context_budget` (e.g. to `60000`) before looking
54
55
  > anywhere else. See [Agents](AGENTS.md#default-limits).
55
56
 
56
- ### Compaction is not wired — except the mechanical dedupe
57
-
58
- There is a settings stub for LLM-summarization compaction (`enabled: false`,
59
- `keep_last`, a reserved utility-model slot), but **nothing consumes it today**
60
- — and the Studio no longer shows a form for it, so the setting cannot be
61
- switched on by accident. Size is managed purely by hard budget eviction.
62
- Do not rely on compaction to shrink a bloated agent: tune `context_budget` and
63
- keep the identity lean.
57
+ ### In-session compaction (opt-in, platform Settings)
58
+
59
+ When enabled (Studio Settings General, or the `compaction` settings hash),
60
+ the engine summarizes old turns *inside* the session instead of losing them to
61
+ eviction:
62
+
63
+ - **Trigger:** after a turn commits, if the session's *uncompacted* message
64
+ count exceeds `compact_after` (default 40), everything but the last
65
+ `keep_last` messages (default 20) is summarized by a cheap model
66
+ (`compaction.model`, falling back to the platform `utility_model`; neither
67
+ set = the feature is inert and `insika doctor` warns). Runs off the critical
68
+ path — the customer already has the answer.
69
+ - **Read path:** the Session provider replaces the compacted prefix with ONE
70
+ history fragment — a `user` message wrapped in `<conversation_summary>` tags,
71
+ priority 59 (one step below the oldest verbatim message), source
72
+ `compaction` (its own category in the context trace). The tail stays
73
+ verbatim and the boundary is **stable** between compactions, so the prompt
74
+ cache holds after it.
75
+ - **What survives:** the default prompt orders the summary to preserve customer
76
+ facts (sizes, CEP, order numbers), the assistant's commitments, the still-open
77
+ questions and the decisions already made; on re-compaction the previous
78
+ summary is folded in, so a fact from turn 3 survives every later batch.
79
+ A platform `compaction.prompt` replaces the default wholesale.
80
+ - **Scope:** store-sourced history only — a checkpoint resume replays its own
81
+ tape and an explicit `history` is the caller's contract; neither is
82
+ rewritten. Observability: the `:context_compacted` event, the
83
+ `insika.context.compacted` counter and `{upto, runs}` in the context trace.
84
+
85
+ Compaction does not replace the budget: eviction stays as the hard backstop
86
+ for a single oversized turn. Tune `context_budget` and keep the identity lean
87
+ regardless.
64
88
 
65
89
  One cheap half **is** wired, opt-in per agent: `tool_output_compression` (DSL
66
90
  `tool_output_compression`, or `"tool_output_compression": true` in the pack).
@@ -121,19 +145,37 @@ pack:
121
145
  briefing_fields "size", "budget", "delivery_day"
122
146
  ```
123
147
 
124
- With fields declared, the turn's `:system` context gains a `<briefing>` block
125
- (priority 65 — below identity/skill/memory so it never breaks the cacheable
148
+ With fields declared, the briefing renders in **two places**, and the split is
149
+ deliberate.
150
+
151
+ The durable half — what is already known — sits in the `:system` context
152
+ (priority 65: below identity/skill/memory so it never breaks the cacheable
126
153
  prefix, above the turn's own `<request_context>`):
127
154
 
128
155
  ```
129
156
  <briefing>
130
157
  known:
131
158
  size: M
159
+ </briefing>
160
+ ```
161
+
162
+ The half that is a *goal* — what is still missing and the agreed next step — is
163
+ **recited at the tail**, after the whole history, as the last thing the model
164
+ reads before the current user message:
165
+
166
+ ```
167
+ <recitation>
132
168
  still missing: budget, delivery_day
133
169
  next step: send the payment link tomorrow at 10
134
- </briefing>
170
+ </recitation>
135
171
  ```
136
172
 
173
+ Attention is strongest at the end of the context: a goal stated only at the top
174
+ is the first thing a 30-call turn forgets. So the recitation was **moved** there,
175
+ not copied — the head never repeats it, and the turn pays for it once. It rides
176
+ as a `user` message, like every other engine append inside a turn, so the system
177
+ prefix stays byte-stable and the cache breakpoint at its end keeps hitting.
178
+
137
179
  The `still missing` list is the point: the *model* sees which declared fields are
138
180
  still unanswered, so it stops re-asking for something already given. Stored keys
139
181
  that the pack no longer declares are never rendered. The Studio session screen
data/docs/DEPLOY.md CHANGED
@@ -25,7 +25,7 @@ is durable SQLite (WAL) at `INSIKA_DB`; mount a volume and point it inside.
25
25
  docker build -t insika .
26
26
  docker run -p 9292:9292 -v insika-data:/data \
27
27
  -e DEEPSEEK_API_KEY=sk-... \
28
- -e OPENCLAW_GATEWAY_TOKEN=change-me \
28
+ -e INSIKA_GATEWAY_TOKEN=change-me \
29
29
  insika
30
30
  curl localhost:9292/up # {"status":"ok"}
31
31
  ```
@@ -115,7 +115,7 @@ this section is the single source of truth for what changing it means.
115
115
  | `INSIKA_DRAIN_TIMEOUT` | `20` | seconds a stopping worker waits for in-flight turns before abandoning them to the next boot's recovery (process model, item 4). The entrypoint sizes Falcon's `--graceful-stop` from it; on Railway also set `RAILWAY_DEPLOYMENT_DRAINING_SECONDS` ≥ drain + 10 |
116
116
  | `INSIKA_TICK_INTERVAL` | `60` | seconds between tick passes — outbox drain + stale recovery sweep (process model, item 5). `0` disables |
117
117
  | `INSIKA_TICK_STALE_AFTER` | `900` | seconds a `:queued`/`:running` task must sit untouched before the tick sweeps it. Must exceed the largest `turn_timeout` of the deployment |
118
- | `OPENCLAW_GATEWAY_TOKEN` | falls back to `ADMIN_TOKEN` | Bearer for `/v1/responses` and `/v1/agents` (the API contract) |
118
+ | `INSIKA_GATEWAY_TOKEN` | falls back to `ADMIN_TOKEN` | Bearer for `/v1/responses` and `/v1/agents` (the API contract) |
119
119
  | `ADMIN_TOKEN` | `local-demo` | login token for `/studio` (**change in production**) |
120
120
  | `DEEPSEEK_API_KEY` | — | provider key. **Without it the engine still boots** (`/up` green), but turns fail until it is configured (env or Studio → LLM providers) — cloud resilience |
121
121
  | `DEEPSEEK_MODEL` | `deepseek-v4-flash` | model |
@@ -157,7 +157,7 @@ values (the API token falling back to `ADMIN_TOKEN` is a dev convenience only):
157
157
  - **`ADMIN_TOKEN`** — the `/studio` login (cookie auth). This is the **operator**
158
158
  surface (just you). Rotating it is **safe and independent**: change it, redeploy,
159
159
  log in with the new value. It does not affect any API consumer.
160
- - **`OPENCLAW_GATEWAY_TOKEN`** — the Bearer for `/v1/responses` and `/v1/agents`.
160
+ - **`INSIKA_GATEWAY_TOKEN`** — the Bearer for `/v1/responses` and `/v1/agents`.
161
161
  This is the **contract with your API consumers**. Rotating it means **changing
162
162
  both sides together** (or the integration breaks): update the runtime var **and**
163
163
  each consumer's token in the same step.
@@ -187,15 +187,24 @@ insika doctor # colored report; exits != 0 on any error
187
187
  insika doctor --json # machine-readable (CI / monitoring)
188
188
  insika doctor --fix # applies the safe autofixes and re-diagnoses
189
189
  insika env # lists known keys + current values (secrets masked)
190
+ insika tools:report # tool audit over the stored traces: never-called
191
+ # allowlisted tools, error rate > 30%, stale tools —
192
+ # read-only, the operator removes ([--agent ID] [--days N] [--json])
190
193
  ```
191
194
 
192
195
  Checks: env (the schema above), settings schema version (a pending migration →
193
196
  `--fix` applies it), a missing platform `default_model` (`--fix` seeds it from
194
197
  `DEEPSEEK_MODEL`), durable vs ephemeral backend, LLM provider configured,
195
- `ADMIN_TOKEN` set, data-tool definitions still valid, **prompt files that hold
198
+ `ADMIN_TOKEN` set, data-tool definitions still valid, **a stored agent whose
199
+ declared tool allow/deny list does not name the `tool_allowlist` policy** (the
200
+ engine repairs it on read, but as stored the list is inert — see
201
+ [Agents](AGENTS.md#the-allowlist-convention)), **prompt files that hold
196
202
  text rather than a serialized object** (a file whose content is a stringified Hash
197
203
  serves a mangled prompt on every turn while looking perfectly healthy — present,
198
- non-empty, and the agent still answers), and **skill drift** a shared skill whose
204
+ non-empty, and the agent still answers), **a prompt file that outgrew a prompt**
205
+ (WARN past ~6 000 estimated tokens or 600 lines — the LLM-generated pack shape
206
+ that costs 20%+ extra tokens per turn for no better instruction-following), and
207
+ **skill drift** — a shared skill whose
199
208
  body names one store, a prompt file routing to a skill the agent cannot load, a broken
200
209
  companion pair, a stale `eager:` key (see
201
210
  [Skills](SKILLS.md#drift-guards)). Settings-schema migrations are **explicit**
@@ -227,7 +236,7 @@ healthcheck, and a restart policy.
227
236
  2. **Volume**: mount it at `/data` (the default `INSIKA_DB` points there) —
228
237
  without a volume, SQLite is ephemeral and recovery resumes nothing after a
229
238
  redeploy.
230
- 3. **Vars**: `DEEPSEEK_API_KEY`, `OPENCLAW_GATEWAY_TOKEN`, `CONSUMER_INTERNAL_URL`,
239
+ 3. **Vars**: `DEEPSEEK_API_KEY`, `INSIKA_GATEWAY_TOKEN`, `CONSUMER_INTERNAL_URL`,
231
240
  `INSIKA_EGRESS_HOSTS`. **Leave `WEB_CONCURRENCY` at its default of 1** unless
232
241
  you run [`insika-router`](ROUTER.md) in front. Railway's own docs say it
233
242
  "does not support sticky sessions" and randomly distributes traffic across
@@ -402,7 +411,7 @@ cache hits, P50/P95, error rate. Runs against local or a remote deployment. See
402
411
  [LOADTEST.md](LOADTEST.md).
403
412
 
404
413
  ```bash
405
- INSIKA_URL=http://localhost:9292 OPENCLAW_GATEWAY_TOKEN=xxx \
414
+ INSIKA_URL=http://localhost:9292 INSIKA_GATEWAY_TOKEN=xxx \
406
415
  bundle exec ruby scripts/loadtest.rb --agents assistant --concurrency 16 --iterations 3
407
416
  ```
408
417
 
data/docs/LOADTEST.md CHANGED
@@ -67,7 +67,7 @@ frame that carries it.
67
67
 
68
68
  ```bash
69
69
  INSIKA_URL=http://localhost:9292 \
70
- OPENCLAW_GATEWAY_TOKEN=xxx \
70
+ INSIKA_GATEWAY_TOKEN=xxx \
71
71
  bundle exec ruby scripts/loadtest.rb \
72
72
  --agents demo,my-store --concurrency 16 --iterations 3 \
73
73
  --message "hi, how are you?"
@@ -80,7 +80,7 @@ Runs against a local server **or** a remote one (e.g. Railway) — just point
80
80
 
81
81
  | Flag | Default | Meaning |
82
82
  |------|---------|---------|
83
- | `--agents a,b,c` | `demo` | comma-separated agent ids (mapped to `model: openclaw:<agent>`) |
83
+ | `--agents a,b,c` | `demo` | comma-separated agent ids (mapped to `model: insika:<agent>`) |
84
84
  | `--concurrency N` | `8` | concurrent turns per wave |
85
85
  | `--iterations N` | `1` | number of waves per agent |
86
86
  | `--message TEXT` | greeting | user message sent every turn |
@@ -95,14 +95,14 @@ Runs against a local server **or** a remote one (e.g. Railway) — just point
95
95
  | Env | Default | Meaning |
96
96
  |-----|---------|---------|
97
97
  | `INSIKA_URL` | `http://localhost:9292` | base URL of the engine |
98
- | `OPENCLAW_GATEWAY_TOKEN` | falls back to `ADMIN_TOKEN`, then `local-demo` | Bearer for `/v1/responses` |
98
+ | `INSIKA_GATEWAY_TOKEN` | falls back to `ADMIN_TOKEN`, then `local-demo` | Bearer for `/v1/responses` |
99
99
  | `DEEPSEEK_API_KEY` | — | must be configured **on the server** for real turns (not read by the client) |
100
100
 
101
101
  Use `--dry-run` to sanity-check your flags/URL/token before firing real traffic
102
102
  (and to confirm the request body without needing a running server):
103
103
 
104
104
  ```bash
105
- INSIKA_URL=http://localhost:9292 OPENCLAW_GATEWAY_TOKEN=xxx \
105
+ INSIKA_URL=http://localhost:9292 INSIKA_GATEWAY_TOKEN=xxx \
106
106
  bundle exec ruby scripts/loadtest.rb --agents demo --concurrency 16 --dry-run
107
107
  ```
108
108
 
@@ -133,7 +133,7 @@ DEEPSEEK_API_KEY=sk-... ./scripts/loadtest-local.sh [WORKERS] [CONCURRENCY]
133
133
  | Env | Default | Meaning |
134
134
  |-----|---------|---------|
135
135
  | `DEEPSEEK_API_KEY` | — (required) | real turns hit the provider; also auto-sourced from `.env.local` |
136
- | `OPENCLAW_GATEWAY_TOKEN` | falls back to `ADMIN_TOKEN`, then `local-demo` | Bearer for the sweep |
136
+ | `INSIKA_GATEWAY_TOKEN` | falls back to `ADMIN_TOKEN`, then `local-demo` | Bearer for the sweep |
137
137
  | `PORT` | `9299` | bind port for the local Falcon |
138
138
  | `AGENT` | `demo` | agent id to load |
139
139
 
@@ -158,30 +158,18 @@ the engine, once at the gateway (its `/v1/responses` speaks the same protocol).
158
158
  Keep `--agents`, `--concurrency`, `--iterations` and `--message` identical, and use
159
159
  matching agents on both sides. Compare the printed TTFB/total/cache/error lines.
160
160
 
161
- ### 4b. Reuse OpenClaw's `loadtest-gateway.mjs` unmodified
161
+ ### 4b. Comparing against an OpenClaw gateway
162
162
 
163
- `loadtest.rb` is the Ruby port of OpenClaw's `loadtest-gateway.mjs`. You do **not**
164
- need to change that script to point it at the engine — because the engine is a
165
- drop-in for the gateway, you only change **where it points**:
163
+ The engine speaks its own wire names (`model: insika:<agent>`, header
164
+ `X-Insika-Agent`), so OpenClaw's `loadtest-gateway.mjs` cannot be pointed at it
165
+ unmodified. For a shadow comparison, run `loadtest.rb` (section 4a) against each
166
+ side with identical `--agents`, `--concurrency`, `--iterations` and `--message`,
167
+ and diff the reports. What you still need in hand:
166
168
 
167
- ```bash
168
- # In the OpenClaw checkout, run its gateway loadtest against the HARNESS:
169
- OPENCLAW_GATEWAY_URL=http://localhost:9292 \
170
- OPENCLAW_GATEWAY_TOKEN=<same bearer the engine accepts> \
171
- node scripts/loadtest-gateway.mjs --agents demo --concurrency 16 --iterations 3
172
- ```
173
-
174
- Then run the exact same command with `OPENCLAW_GATEWAY_URL` pointing at the real
175
- gateway, and diff the two reports. This is the shadow comparison the pilot needs.
176
-
177
- **What the operator must have in hand** (this repo does not vendor OpenClaw):
178
-
179
- - The OpenClaw checkout containing `scripts/loadtest-gateway.mjs` and Node installed.
180
- - A **bearer token accepted by both** sides. For the engine that is
181
- `OPENCLAW_GATEWAY_TOKEN` (see DEPLOY.md); point the gateway run at its own token.
182
- - **The same agent id provisioned on both** sides (e.g. `demo`) so `model:
183
- openclaw:<agent>` resolves on each. On the engine, provision via
184
- `scripts/import_pack.rb`.
169
+ - A bearer token accepted by each side — `INSIKA_GATEWAY_TOKEN` for the engine
170
+ (see DEPLOY.md), the gateway's own token for the gateway run.
171
+ - **The same agent id provisioned on both** sides (e.g. `demo`). On the engine,
172
+ provision via `scripts/import_pack.rb`.
185
173
  - The **same provider** (or an equivalent-latency one) behind each, otherwise you
186
174
  are comparing providers, not engines.
187
175
  - Both endpoints reachable from where you run the client, warmed up (hit `/up` on
data/docs/MEDIA.md CHANGED
@@ -87,7 +87,7 @@ receive it (`channel.capabilities` on the request):
87
87
 
88
88
  ```bash
89
89
  curl -X POST /v1/responses -H "Authorization: Bearer $TOKEN" -d '{
90
- "model": "openclaw:store-support", "user": "chat-7",
90
+ "model": "insika:store-support", "user": "chat-7",
91
91
  "input": "manda a foto do sofá da promoção",
92
92
  "channel": { "capabilities": ["image_output", "audio_output"] }
93
93
  }'