samagotchi 0.152.0 → 0.153.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 (44) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +18 -1
  3. data/docs/bundle-authoring.md +1 -1
  4. data/docs/bundles.md +84 -0
  5. data/docs/cli.md +3 -3
  6. data/docs/configuration.md +19 -14
  7. data/docs/guardrails.md +7 -5
  8. data/docs/hooks.md +4 -3
  9. data/docs/internals/anthropic.md +3 -3
  10. data/docs/internals/attached-context.md +3 -0
  11. data/docs/internals/engine.md +9 -5
  12. data/docs/internals/participants.md +1 -1
  13. data/docs/internals/prompt-caching.md +7 -3
  14. data/docs/internals/web-frontend.md +20 -17
  15. data/docs/plugins.md +29 -15
  16. data/docs/releasing.md +1 -1
  17. data/docs/sessions.md +1 -1
  18. data/docs/testing.md +2 -2
  19. data/lib/samagotchi/bundles/system/manifest.yml +1 -1
  20. data/lib/samagotchi/bundles/web-search/manifest.yml +13 -0
  21. data/lib/samagotchi/bundles/web-search/plugin.rb +174 -0
  22. data/lib/samagotchi/engine.rb +10 -5
  23. data/lib/samagotchi/guardrails/review_boundary.rb +4 -3
  24. data/lib/samagotchi/llm_context_stale.rb +2 -2
  25. data/lib/samagotchi/message_parts.rb +3 -2
  26. data/lib/samagotchi/toolkit.rb +8 -5
  27. data/lib/samagotchi/toolkit_binding.rb +30 -5
  28. data/lib/samagotchi/toolkit_report_command.rb +9 -5
  29. data/lib/samagotchi/toolkit_suggest.rb +12 -6
  30. data/lib/samagotchi/tools/builtin_calls.rb +14 -10
  31. data/lib/samagotchi/tools/deferred_tools.rb +2 -2
  32. data/lib/samagotchi/tools/registry.rb +44 -20
  33. data/lib/samagotchi/tools/tool_index.rb +27 -21
  34. data/lib/samagotchi/version.rb +1 -1
  35. data/lib/samagotchi/web/public/app.js +61 -357
  36. data/lib/samagotchi/web/public/composer_mode.js +110 -0
  37. data/lib/samagotchi/web/public/owner_claim.js +81 -0
  38. data/lib/samagotchi/web/public/owner_verify.js +112 -0
  39. data/lib/samagotchi/web/public/session_fields.js +3 -3
  40. data/lib/samagotchi/web/public/strip_grip.js +3 -3
  41. data/lib/samagotchi/web/public/strip_hidden.js +65 -0
  42. data/lib/samagotchi/web/public/version_notices.js +132 -0
  43. data/lib/samagotchi/worker.rb +6 -0
  44. metadata +8 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6d208a5aecb9745a21107ab97399c7ec4901c24fd781e3ba4ec1658a69764ded
4
- data.tar.gz: abc21f8bfe39f0bdc614b70b755dd3cd195d016dd305b6d3ba28b018d0b75d54
3
+ metadata.gz: e9dd8da2f6f95806fccc0df5dad4844525947650b92589461adcb70b8bcdf2d7
4
+ data.tar.gz: a1457568c9d119b661d025be9e39989a33593666d668b328c3cd8528e451aa55
5
5
  SHA512:
6
- metadata.gz: 1105443bb7097fd93f0ab4bb1f0f9926fbbc9c89f9d57584d12ae3ac560b726498c607c1e897416e8a17f0e150076fc7de2b2b19b0919b49ad5b40645dd3883a
7
- data.tar.gz: ba009b4669d67eb24ccd15cf669f84431a1738153b721c13c18633d6b05f32cdd4bfd01d25530b18ef5ab39cbf84e0b19688691f6751884c92e0a8d412935771
6
+ metadata.gz: 6ed8fbbd7439b3ff95cb6eb251edf5635812cff0579928469a575bbb4bdc190e14ff6959a2f49eb627f127f711c1782c62c99379a2130433e057d0a55dd2e359
7
+ data.tar.gz: 82b22635aad92e8edb672e61ac0eed1c461c1c81330bfde64ac09cd8c969183e37f9420495cbb6b0fbe0624f9a552a17af9cb70a2707b0f483084d25fb10d936
data/CHANGELOG.md CHANGED
@@ -8,6 +8,22 @@ and commands may change between minor versions. How releases are made:
8
8
 
9
9
  ## [Unreleased]
10
10
 
11
+ ## [0.153.0] - 2026-11-06
12
+
13
+ ### Added
14
+
15
+ - A `web-search` bundle (`chi bundle install web-search`, in no profile): a `web_search` tool that searches the web through your own SearXNG and returns titles, URLs and snippets for `web_fetch` to read. It is declared on `full` and kits on `base: full`; on kits built on `lean` or `child` it is reachable (`find_tools` finds `chi/web_search`, `call_tool` runs it); `add: [web_search]` in a kit declares it. [docs/bundles.md](docs/bundles.md#the-web-search-bundle)
16
+
17
+ ### Changed
18
+
19
+ - A bundle's tools (`chi.tool`) follow a toolkit's tool list as built-ins do: `full`, the kits on `base: full` (the `kits:` default) and a kit's `add:` declare them, any other kit leaves them reachable through `find_tools` / `call_tool` (deferred on `api: anthropic`), and `remove:` removes them. [docs/configuration.md](docs/configuration.md#toolkits)
20
+
21
+ ### Fixed
22
+
23
+ - An attached context change that arrives while the worker's leaving pass is already in flight is saved as a note before the worker stops, instead of being dropped with its poller.
24
+
25
+ Update with `chi update` (no installed bundle's version moved; `chi bundle install web-search` adds the new one).
26
+
11
27
  ## [0.152.0] - 2026-11-06
12
28
 
13
29
  ### Added
@@ -5717,7 +5733,8 @@ and long-lived sessions.
5717
5733
  - A macOS desktop helper (`chi desktop install`): a "Send to chi" Service and a
5718
5734
  hotkey panel that send selected text or the clipboard to your sessions.
5719
5735
 
5720
- [Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.152.0...HEAD
5736
+ [Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.153.0...HEAD
5737
+ [0.153.0]: https://github.com/dm1try/samagotchi/compare/v0.152.0...v0.153.0
5721
5738
  [0.152.0]: https://github.com/dm1try/samagotchi/compare/v0.151.0...v0.152.0
5722
5739
  [0.151.0]: https://github.com/dm1try/samagotchi/compare/v0.150.0...v0.151.0
5723
5740
  [0.150.0]: https://github.com/dm1try/samagotchi/compare/v0.149.0...v0.150.0
@@ -119,7 +119,7 @@ end
119
119
  `questions` record effects; `bundle.answer` / `model_answers` provide `ask_user` / `ask_model` answers.
120
120
  `bundle.judgments` answers `ctx.judge` and records its asks and runs (each judgment is on in the kit, off in chi until the user sets it);
121
121
  `bundle.http.stub` stubs `ctx.http`; `bundle.question_asked(text, question:)` fires `:question_asked` and returns
122
- the refs; `sandbox.config`, `sandbox.fake_executable` and `bundle.install` cover setup.
122
+ the refs; `sandbox.config`, `sandbox.fake_executable` and `bundle.install` cover setup. The kit's `ctx.declared_tools` is every tool, as on `full`; in chi a `chi.tool` is declared only on `full` and by a kit's `add:`, and on `lean` or `child` the model reaches it with `find_tools` (`chi/<name>`) and `call_tool` ([Toolkits](configuration.md#toolkits)), so `bundle.set(declared_tools: %w[read])` tests that case and the tool's description should be easy to search.
123
123
  The full list is in [plugins.md, Testing a bundle](plugins.md#testing-a-bundle-experimental). The kit is
124
124
  experimental (it may change before chi 1.0). A failing matcher that meets `ENV` prints `#<the process ENV, contents not shown>`, not your keys. If a test needs a fake of chi's own (a stub of a `Samagotchi::`
125
125
  class, a hand-built `ctx`), that is a gap in the kit: say so to the user rather than reaching into chi.
data/docs/bundles.md CHANGED
@@ -225,6 +225,90 @@ bundles:
225
225
  tools, or with different args, stay distinct, since the tool is in the
226
226
  args).
227
227
 
228
+ ## The web-search bundle
229
+
230
+ `chi bundle install web-search` installs the bundle shipped with chi (`lib/samagotchi/bundles/web-search/plugin.rb`).
231
+ No profile includes it, since it needs a running [SearXNG](https://docs.searxng.org), so `chi update` lists it as
232
+ not installed until you install it. It gives the model one tool, `web_search`, which asks your SearXNG through its
233
+ JSON API. `web_fetch` fetches a URL but can't find one; `web_search` finds them, and the model then reads the ones it
234
+ wants with `web_fetch`. It is an ordinary plugin tool, so it works on every host and loop.
235
+
236
+ - **The tool:** `web_search(query, time_range?, page?)`. `time_range` is `day`, `week`, `month` or `year`; `page`
237
+ starts at 1. It returns plain text: a header, the first answer SearXNG gives (if any), a numbered list of results
238
+ (title, URL, and a snippet of at most 300 characters), and SearXNG's suggestions:
239
+
240
+ ```
241
+ SearXNG results for "ruby prism parser" (page 1):
242
+ 1. Prism — Ruby parser
243
+ https://github.com/ruby/prism
244
+ A portable, error-tolerant Ruby parser…
245
+ Suggestions: ruby prism gem, prism vs parser
246
+ ```
247
+
248
+ With nothing found it says `No results for "<query>".` and names the engines that failed. A SearXNG that can't be
249
+ reached, a 403 (JSON not enabled), a 429 (its limiter) or an answer that isn't JSON each give an `Error:` line that
250
+ says what to fix.
251
+
252
+ ```yaml
253
+ # config.yml
254
+ bundles:
255
+ web-search:
256
+ url: http://127.0.0.1:8888 # the SearXNG root
257
+ max_results: 8 # results listed, 1..20
258
+ timeout: 10 # seconds per read
259
+ # optional, sent only when set:
260
+ language: en
261
+ safesearch: 0 # 0, 1 or 2
262
+ categories: general # comma-separated
263
+ engines: duckduckgo,brave # comma-separated
264
+ ```
265
+
266
+ | key | default | |
267
+ |---|---|---|
268
+ | `url` | `http://127.0.0.1:8888` | the SearXNG root, `http` or `https` with a host; a trailing `/` is fine |
269
+ | `max_results` | `8` | clamped to 1..20 |
270
+ | `timeout` | `10` | seconds; a value that isn't a positive number uses the default |
271
+ | `language` | unset | unset (the default): SearXNG picks the language from the query; set it (e.g. `en`) to force one; dates and some results may follow your region |
272
+ | `safesearch` | unset | `0`, `1` or `2` |
273
+ | `categories`, `engines` | unset | comma-separated. SearXNG silently drops names it doesn't know, so a typo gives its defaults |
274
+
275
+ - **Privacy:** the query goes to your SearXNG and on to the engines it is configured with. No guardrail rule asks
276
+ about it by default; add one (`tool: web_search`, `verdict: ask`) if you want to approve each search. A review
277
+ turn can't call it.
278
+ - **Toolkits:** the tool follows a kit's list like a built-in ([Toolkits](configuration.md#toolkits)). `full` (and
279
+ any kit on `base: full`) declares it (about 150 tokens per request); on `lean`, `child` and the kits built on them
280
+ it is reachable, not declared: the model finds it with `find_tools` (`chi/web_search`) and calls it with
281
+ `call_tool` (on `api: anthropic` it goes deferred and is called by name). To declare it on such a kit, `add:` it
282
+ (or `/toolkit add web_search` in a session); to keep it away entirely, `remove:` it (or `/toolkit remove
283
+ web_search`).
284
+
285
+ ```yaml
286
+ toolkit:
287
+ default: search
288
+ kits:
289
+ search: { base: lean, add: [web_search] }
290
+ quiet: { base: full, remove: [web_search] }
291
+ ```
292
+
293
+ ### Running SearXNG locally
294
+
295
+ ```sh
296
+ mkdir -p searxng
297
+ cat > searxng/settings.yml <<'YAML'
298
+ use_default_settings: true
299
+ server:
300
+ secret_key: "change-me-to-a-random-string" # openssl rand -hex 32
301
+ limiter: false # a local instance: no rate limiting (else 429s)
302
+ search:
303
+ formats: [html, json] # json is off by default (else 403)
304
+ YAML
305
+ docker run -d --name searxng -p 127.0.0.1:8888:8080 -v ./searxng:/etc/searxng searxng/searxng
306
+ ```
307
+
308
+ The port is bound to loopback, not the LAN. Under colima, run it from a folder in your home directory: colima
309
+ shares only that with its VM by default, and elsewhere SearXNG starts on its own default settings (JSON off).
310
+ `curl 'http://127.0.0.1:8888/search?q=ruby&format=json'` should answer JSON.
311
+
228
312
  ## The loop-guard bundle
229
313
 
230
314
  `chi bundle install loop-guard` installs the bundle shipped with chi (`chi
data/docs/cli.md CHANGED
@@ -41,7 +41,7 @@
41
41
  - `chi guardrails replay [--dry-run] [--threshold T] [--model M]` — seed System 1's record from the approvals this machine's sessions already answered: each ask is judged by the configured model and written to `guardrails/judgments.jsonl` with the answer the user gave, then a per-rule table prints (asks, allowed, denied, would-be auto-allows, false allows, asks with no judgment). A parent agent's answer is not the user's: only its deny is replayed, and only where the parent could have allowed. `--dry-run` judges without writing, `--threshold` overrides `guardrails.system_one.threshold` for the table, `--model` the configured model for this run. Exit 2 when `guardrails.system_one.mode` is off or no `host` is set; a usage error exits 2 (see [System 1](guardrails.md#system-1-experimental))
42
42
  - `chi guardrails rule <id>` — print exactly `asks` when that rule is loaded, not disabled and votes `ask` in this `guardrails.mode` for every model; any other line says why not (no such rule, disabled, another mode, it denies, for some models only, rules that failed to load, `guardrails.enabled: false`). Exit 0 either way, 2 for a usage error. It reads config.yml and the installed bundles as a worker does (`GuardrailWiring#rules`) and takes the rule's id, never a command; the [coordinator skill](bundles.md#the-coordinator-bundle) reads it before it merges without asking
43
43
  - `chi judgments ls` / `chi judgments report NAME [--days N]` — what [System 1's saved runs](system-one.md) hold: each judgment's digests, runs, labels, failures, models and size; one judgment's counts, p histogram and labels by value. Reads only; local only; `--days` must be a positive whole number.
44
- - `chi toolkit report [--days N]` — grade toolkit suggestions: the kit on mode would have run (the suggested kit when it would have applied it, else the default; an on-mode line's `did` says what it ran) against the tools the session called from that turn on (only that turn when the toolkit was chosen since), `call_tool` targets included, for both the chat and the raw-prompt loop. Each is *covers*, *short of a tool* (naming the tools), *wider than needed* (a narrower kit offered in the same ask would also have covered) or *ungradable* (calls it can't read back); then a per-kit tally with what on mode would have done. Includes archived sessions and caps the report at 14 days and 500 sessions. Reads `toolkit_suggestions.jsonl` (each ask logs every offered kit's rules, expanded against the called tools here); it never runs the model or changes a session. Local only; `--days` must be a positive whole number.
44
+ - `chi toolkit report [--days N]` — grade toolkit suggestions: the kit on mode would have run (the suggested kit when it would have applied it, else the default; an on-mode line's `did` says what it ran) against the tools the session called from that turn on (only that turn when the toolkit was chosen since), `call_tool` targets included, for both the chat and the raw-prompt loop. Each is *covers*, *short of a tool* (naming the tools; a built-in or plugin tool the kit left reachable but didn't declare counts), *wider than needed* (a narrower kit offered in the same ask would also have covered) or *ungradable* (calls it can't read back); then a per-kit tally with what on mode would have done. Includes archived sessions and caps the report at 14 days and 500 sessions. Reads `toolkit_suggestions.jsonl` (each ask logs every offered kit's rules, expanded against the called tools here); it never runs the model or changes a session. Local only; `--days` must be a positive whole number.
45
45
 
46
46
  - `chi bundle try PATH -p TEXT [options]` — install a bundle in a temporary, filtered sandbox, run one real model turn,
47
47
  and print its trace (see [Trying a bundle](#trying-a-bundle)).
@@ -451,7 +451,7 @@ invalid option (exit 1).
451
451
  | `--dir DIR` | With `--remote`: a new session's folder on that machine, its absolute path (required for a new session there). Without `--remote` it exits 2: cd there instead. |
452
452
  | `--model NAME` | Use this model for the run (overrides the configured default and a resumed session's model). |
453
453
  | `--thinking LEVEL` | This session's own thinking level: `off`, `low`, `medium` or `high`, saved in the session and first in the order (`default` unsets it). On `--resume` or `--attach` it is set at once, through a `/thinking` on an attached worker. With `chi web` it is instead the default for the sessions its workers run, below a session's own (as env `SAMAGOTCHI_THINKING_LEVEL` is). See "Thinking" in configuration.md. |
454
- | `--toolkit NAME`, `--tool NAME`, `--no-tool NAME` | A new session's toolkit, the tools its prompts declare: `full` (every built-in), `lean`, `child`, one of config's `toolkit.kits` or a bundle's (without `--toolkit`: config's `toolkit.default`), with tools declared on top of it (`--tool`) or left out (`--no-tool`); both repeatable, a comma list too. Saved in the session as given; a respawned worker, a fork and a continue keep it. Ignored with `--attach`/`--resume` (one line, as `--memory`); `/toolkit` changes it. Not on `chi web` (config's `toolkit.default` covers its sessions). See [Toolkits](configuration.md#toolkits). |
454
+ | `--toolkit NAME`, `--tool NAME`, `--no-tool NAME` | A new session's toolkit, the tools its prompts declare: `full` (every built-in and plugin tool), `lean`, `child`, one of config's `toolkit.kits` or a bundle's (without `--toolkit`: config's `toolkit.default`), with tools declared on top of it (`--tool`) or left out (`--no-tool`); both repeatable, a comma list too. Saved in the session as given; a respawned worker, a fork and a continue keep it. Ignored with `--attach`/`--resume` (one line, as `--memory`); `/toolkit` changes it. Not on `chi web` (config's `toolkit.default` covers its sessions). See [Toolkits](configuration.md#toolkits). |
455
455
  | `--parent-answers LEVEL` | A new session's own `guardrails.parent_answers` (`hard`, `standard` or `soft`): what a parent agent may answer on its questions. It may only tighten config.yml's: the stricter of the two applies, and a looser one is saved as given with a warning (`--parent-answers soft is looser than config.yml's guardrails.parent_answers: standard; standard applies`). Saved in the session; a respawned worker, a fork and a continue keep it, a `delegate` child takes its parent's. Ignored with `--attach`/`--resume` (one line, as `--toolkit`); refused with `chi web`. Only you loosen it: `/parent-answers` in the terminal or the web. See [Guardrails](guardrails.md#answers-from-a-parent-agent). |
456
456
  | `--set KEY=VALUE` | A setting for a new session (repeatable; a later one of the same key wins). A core key a session may set (`recap.enabled`, `recap.sentences`) is typed by the key and goes first, over the CLI, env and config.yml: `--set recap.enabled=false` turns the idle recap off for this session; a value it doesn't take (`recap.enabled=maybe`) or a key no session sets (`guardrails.mode`) is an error, exit 2 (see [Per-session settings](configuration.md#per-session-settings)). Any other name is a bundle's `BUNDLE.KEY`, laid over config.yml's `bundles: <bundle>:` in what the bundle's plugin reads (`ctx.settings`): `--set cost-cap.cap=3`. The value is typed simply: an integer (`3`, `-1`) or a decimal (`2.5`, `.5`, `1e1`) is a number, `true`, `yes` and `on` are true, `false`, `no` and `off` are false (any case, as in config.yml), anything else stays the text as given (`010`, `0x10`, `3:30`, `y`, `null`), and so does every value of a key the manifest types `string`. For cost-cap, `0`, a negative number or `off` is no cap. Only a key the installed bundle's manifest lists in `session_settings:` is taken, with a value of the type it gives the key: a bundle that isn't installed (or whose plugin doesn't load), a key it doesn't list (a record installed before the bundle listed it says to run `chi update` or `chi bundle upgrade NAME`), a value of another type (`--set cost-cap.cap=five`), or another shape (`--set foo`, `--set a.b.c=1`) is an error naming what it takes, exit 2, nothing started. Saved in the session; a respawned worker, a fork and a continue keep it, a delegate child doesn't. Ignored with `--attach`/`--resume` (one line, as `--toolkit`); refused with `chi web`. See [Per-session settings](plugins.md#per-session-settings). |
457
457
  | `--profile NAME` | Prompt profile (`qwen36` or `gemma4`) for every model in this run, over config and the server's template (same as `--model-profile`, env `SAMAGOTCHI_MODEL_PROFILE`). See "Prompt profile" in configuration.md. |
@@ -888,7 +888,7 @@ line, the web in the session bar's ⚙ panel (`⚙ reasoning all` once the sessi
888
888
 
889
889
  `/toolkit` lists the tools the next turn declares (each with its estimated
890
890
  tokens), the session's toolkit and the tools it leaves out: `reachable
891
- (find_tools): …` (built-ins the model reaches with find_tools and call_tool)
891
+ (find_tools): …` (built-ins and plugin tools the model reaches with find_tools and call_tool)
892
892
  and `removed: …` (only `/toolkit add` brings one back), or `not loaded: …` when
893
893
  the toolkit removes call_tool, and its kit's prompt text when it has one (`prompt: <first line> (+N lines, ~T
894
894
  tokens)`). `/toolkit remove TOOL` is hard: a tool the
@@ -1501,17 +1501,18 @@ toolkit:
1501
1501
 
1502
1502
  | toolkit | declares |
1503
1503
  |---|---|
1504
- | `full` | every built-in; the default |
1504
+ | `full` | every built-in and every plugin tool (a bundle's `chi.tool`, such as `web_search`); the default |
1505
1505
  | `lean` | `execute`, `read`, `write`, `edit`, `memory_read`, `memory_write`, `task_create`, `task_wait`, `task_get`, `task_list`, `task_stop`, `ask_user_question`, `web_fetch`, `context_read` |
1506
1506
  | `child` | `lean` plus `send_note`, without `memory_write`, `delegate`, `delegate_result` |
1507
1507
  | `coordinator` | `lean` plus `delegate`, `delegate_result`, `list_sessions`, `objectives` (declared by the coordinator bundle) |
1508
1508
  | `coding` | `child`, with the prompt text `- Be concise in your responses` / `- Show file paths clearly when working with files` (declared by the coordinator bundle, for build and fix children) |
1509
1509
  | `review` | `read`, `memory_read` (not `forget_outputs`, even under the `forget` layer): what a [participant's](internals/participants.md#turns) turn may call, whatever the session declares (the review boundary, [guardrails](guardrails.md#a-participants-review-boundary)). Reserved: `toolkit.kits.review` is refused ("review is reserved for participants' turns"), and a bundle can't declare it. A session may run on it too, as any kit |
1510
1510
 
1511
- - **Plugin tools** are declared in every toolkit unless its `remove:` names them, and so is `forget_outputs` under
1512
- the `forget` layer. A `kits:` entry takes `base:` (default `full`), `add:`, `remove:`, `prompt:`, `groups:` and
1513
- `description:`
1514
- (any other key, a typo or a `tools:` list, is ignored with a warning); it can't reuse a built-in's name
1511
+ - **Plugin tools** (a bundle's `chi.tool`, such as web-search's `web_search`) follow a kit's list as built-ins do:
1512
+ `full` (and every kit on `base: full`, the `kits:` default) declares them, another kit leaves them **reachable**
1513
+ (below) unless its `remove:` names them, and `add:` declares one (`search: { base: lean, add: [web_search] }`).
1514
+ `forget_outputs` is declared in every toolkit under the `forget` layer, unless removed. A `kits:` entry takes
1515
+ `base:` (default `full`), `add:`, `remove:`, `prompt:`, `groups:` and `description:` (any other key, a typo or a `tools:` list, is ignored with a warning); it can't reuse a built-in's name
1515
1516
  (`review`'s least of all: it is the participants' boundary). A bundle declares its own with `chi.toolkit`
1516
1517
  ([plugins](plugins.md#chitoolkitname-base-add--remove--prompt-nil-groups-nil-description-nil)).
1517
1518
  `description:` is optional classifier input, up to 300 characters; it is not included in the session prompt.
@@ -1565,7 +1566,7 @@ toolkit:
1565
1566
  reaches none. A kit whose base doesn't resolve (an unknown name, a cycle) falls back to `full`, which reaches
1566
1567
  every group.
1567
1568
  A selector that matches no group in that load-time snapshot warns once. `find_tools` and `call_tool` are
1568
- declared only when the toolkit reaches a source group or has a reachable built-in. Calls naming an unreached
1569
+ declared only when the toolkit reaches a source group or has a reachable tool (a built-in or a plugin tool). Calls naming an unreached
1569
1570
  group are refused before guardrails and do not start its server. The first-run init wait is unchanged: it can
1570
1571
  still block the first turn even when its server's group is unreached; `start: eager` and stale-cache refreshes
1571
1572
  also still start. `/mcp` marks servers outside the current toolkit.
@@ -1599,17 +1600,21 @@ toolkit:
1599
1600
  them with `/toolkit`. The configured default is always listed. A pick closes the list and shows a `tools: lean`
1600
1601
  chip on the row (a dot on 🧰 on a phone); it is for that create only, never remembered. A review session is
1601
1602
  always `chosen`: with review files set the list says it starts on the default unless you pick a kit.
1602
- - **Left out: reachable or removed.** A built-in missing from a kit's base list (`lean` has no `delegate`; a
1603
- `kits:` entry on `base: lean` or `child` inherits that) is left out softly: it is **reachable**. `find_tools` searches it as group `chi` (`chi/delegate`, with its inputSchema) beside
1604
- the MCP servers' tools, and `call_tool(tool: "chi/delegate", args: {…})` runs it as `delegate` itself, through the
1605
- usual guardrails, approvals and hooks ([plugins](plugins.md#chitool_sourcecall-acts_as-label-wait-prefix)). A
1603
+ - **Left out: reachable or removed.** A built-in or plugin tool missing from a kit's base list (`lean` has no
1604
+ `delegate` and no `web_search`; a `kits:` entry on `base: lean` or `child` inherits that) is left out softly: it
1605
+ is **reachable**. `find_tools` searches it as group `chi` (`chi/delegate`, `chi/web_search`, with its
1606
+ inputSchema) beside the MCP servers' tools, and `call_tool(tool: "chi/delegate", args: {…})` runs it as `delegate`
1607
+ itself, through the usual guardrails, approvals and hooks ([plugins](plugins.md#chitool_sourcecall-acts_as-label-wait-prefix)). A
1606
1608
  tool in a toolkit's `remove:`, a `--no-tool` or a `/toolkit remove` is **removed**: hard, also after an add, and
1607
1609
  call_tool can't reach it (`child` removes `memory_write`, `delegate` and `delegate_result`, so a child on it
1608
- starts no children). To keep a built-in away from the model, name it under `remove:`. `find_tools` and `call_tool`
1609
- are declared when a toolkit has a reachable built-in or reaches a source group; `full` with no reachable source
1610
- declares neither, and removing them there (`--no-tool call_tool`) leaves nothing out: no prompt line, no chip.
1610
+ starts no children). To keep a tool away from the model, name it under `remove:`. `find_tools` and `call_tool`
1611
+ are declared when a toolkit has a reachable tool or reaches a source group, as the tools stood when the
1612
+ session's worker started; `full` with no reachable source declares neither, and removing them there
1613
+ (`--no-tool call_tool`) leaves nothing out: no prompt line, no chip. A plugin tool that arrives later
1614
+ (`chi.replace_tools`) on a kit that declared neither stays out of reach for that worker, with one warn notice
1615
+ (`/toolkit add NAME` declares it).
1611
1616
  - **On `api: anthropic`, reachable tools are callable by name.** Every tool `call_tool` reaches (the reachable
1612
- built-ins, tools from reached MCP groups as `mcp_<server>_<tool>`) also goes in the request's tools deferred
1617
+ built-ins and plugin tools, tools from reached MCP groups as `mcp_<server>_<tool>`) also goes in the request's tools deferred
1613
1618
  (`defer_loading`: Messages keeps them out of the model's view until something loads them, at a flat ~100 tokens).
1614
1619
  A direct call of one (`list_sessions`, `mcp_fs_read`) runs, under the same guardrails, approvals and rows as
1615
1620
  through `call_tool` (an MCP tool's call is `call_tool`'s). A `find_tools` hit **loads** its matches (up to 5):
data/docs/guardrails.md CHANGED
@@ -841,11 +841,13 @@ trade-off, when such a clash exists:
841
841
  - deny rules cover both, which is the point.
842
842
 
843
843
  A `call_tool` of one of chi's own tools (`chi/delegate`: a built-in the
844
- session's toolkit leaves out, [toolkits](configuration.md#toolkits)) is
845
- gated **as that tool**: it is turned into the `delegate` call before the
846
- guardrails run, so every rule on `delegate` (or `execute`'s `command:`, the
847
- `rm` check, a delegate child's boundary) gives the same verdict as for a
848
- direct call, and an approval is keyed `delegate:…` either way. `tool:
844
+ session's toolkit leaves out, [toolkits](configuration.md#toolkits)), or of
845
+ a bundle's `chi.tool` it leaves out (`chi/web_search`), is gated **as that
846
+ tool**: it is turned into the `delegate` call before the guardrails run, so
847
+ every rule on `delegate` (or `execute`'s `command:`, the `rm` check, a
848
+ delegate child's boundary, a plugin tool's `targets:`) gives the same
849
+ verdict as for a direct call, and an approval is keyed `delegate:…` either
850
+ way. `tool:
849
851
  call_tool` rules no longer see such calls, only MCP calls and misses. A
850
852
  `chi/` name the toolkit removes is refused before the gate, with no card.
851
853
 
data/docs/hooks.md CHANGED
@@ -644,8 +644,9 @@ A `write`'s `content:` is the file text; an `edit` carries `old_text:` and
644
644
  arguments whole on `args:` (string keys) instead, and its `content:` is those
645
645
  arguments as JSON. An MCP tool's call is chi's `call_tool` (its tool index's, which the mcp
646
646
  bundle runs): `tool: "call_tool"`, with `args: {"tool" => "<server>/<tool>", "args" => {…}}`.
647
- A built-in reached through `call_tool` (`chi/delegate`, a tool the session's toolkit doesn't
648
- declare) fires as that built-in, its call built as a direct call's, with `call[:via] == "call_tool"`.
647
+ A built-in or plugin tool reached through `call_tool` (`chi/delegate`, `chi/web_search`: a tool the
648
+ session's toolkit doesn't declare) fires as that tool, its call built as a direct call's, with
649
+ `call[:via] == "call_tool"`.
649
650
  A hook that drops `via:` or rewrites the call to a tool the toolkit removes gets the call refused
650
651
  by its name.
651
652
  On a Messages host (`api: anthropic`), the model may call those tools by their own names (they
@@ -710,7 +711,7 @@ Notes:
710
711
  - Ordering: bundle hooks fire by `(priority, bundle_name, hook_name)` (lower priority first), then plain `config.yml` hooks in registration order.
711
712
  - Settings: a hook class with `initialize(settings = {})` gets the bundle's section of `config.yml` `bundles:` (see [Settings](#settings)).
712
713
  - A bundle can also ship a `plugin.rb` whose `chi.on(event)` blocks are bundle hooks too, next to commands and tools; see [Plugins](plugins.md). There `ctx.notify`, `ctx.steer` and the other `ctx` helpers act as this page's `event[:notify]`… do for the event the block runs for.
713
- - Shipped bundles (each in a profile: `core` or `dev`, see [Bundle profiles](memory.md#bundle-profiles-core-and-dev)): `chi bundle install guardrails` (rules, see [Guardrails](guardrails.md#the-guardrails-bundle)) and `chi bundle install known-names` (a hook, see [Guardrails](guardrails.md#the-known-names-bundle)), `chi bundle install source-links` (a hook: announces source refs, see [The source-links bundle](#the-source-links-bundle)), `chi bundle install btw` (a plugin: `/btw`, see [Bundles](bundles.md#the-btw-bundle)), `chi bundle install mcp` (a plugin: tools from MCP servers, see [Bundles](bundles.md#the-mcp-bundle)) `chi bundle install loop-guard` (a plugin: breaks tool-call loops, see [Bundles](bundles.md#the-loop-guard-bundle)), `chi bundle install check-in` (a plugin: checks on a long turn, see [Bundles](bundles.md#the-check-in-bundle)), `chi bundle install slow-commands` (a plugin: commands that ran past the execute limit are run as tasks next time, see [Bundles](bundles.md#the-slow-commands-bundle)), `chi bundle install agents-md` (a plugin: a nested AGENTS.md is steered into the conversation on the first touch under its directory, see [Bundles](bundles.md#the-agents-md-bundle)), `chi bundle install cost-cap` (a plugin: a per-session $ cap, `/cap`, see [Bundles](bundles.md#the-cost-cap-bundle)), `chi bundle install work-cap` (a plugin: a per-session cap on tool calls and active minutes, `/workcap`, see [Bundles](bundles.md#the-work-cap-bundle)), `chi bundle install ruby` (a plugin: an RSpec run with no examples at all, a hint, see [Bundles](bundles.md#the-ruby-bundle)) `chi bundle install skills` (a plugin: `/skill`, versions of skills, see [Bundles](bundles.md#the-skills-bundle)), `chi bundle install github-pr` (attached context: the branch's open PR, see [Bundles](bundles.md#the-github-pr-bundle)), `chi bundle install coordinator` (a plugin: parallel work in worktrees and child sessions, see [Bundles](bundles.md#the-coordinator-bundle)), `chi bundle install claude-peer` / `codex-peer` (a plugin: talk to a Claude Code or Codex session, see [Bundles](bundles.md#the-claude-peer-bundle)) and `chi bundle install model-notes` (memories: reviewed model notes, see [Bundles](bundles.md#the-model-notes-bundle)).
714
+ - Shipped bundles (each but web-search in a profile: `core` or `dev`, see [Bundle profiles](memory.md#bundle-profiles-core-and-dev)): `chi bundle install guardrails` (rules, see [Guardrails](guardrails.md#the-guardrails-bundle)) and `chi bundle install known-names` (a hook, see [Guardrails](guardrails.md#the-known-names-bundle)), `chi bundle install source-links` (a hook: announces source refs, see [The source-links bundle](#the-source-links-bundle)), `chi bundle install btw` (a plugin: `/btw`, see [Bundles](bundles.md#the-btw-bundle)), `chi bundle install mcp` (a plugin: tools from MCP servers, see [Bundles](bundles.md#the-mcp-bundle)) `chi bundle install loop-guard` (a plugin: breaks tool-call loops, see [Bundles](bundles.md#the-loop-guard-bundle)), `chi bundle install check-in` (a plugin: checks on a long turn, see [Bundles](bundles.md#the-check-in-bundle)), `chi bundle install slow-commands` (a plugin: commands that ran past the execute limit are run as tasks next time, see [Bundles](bundles.md#the-slow-commands-bundle)), `chi bundle install agents-md` (a plugin: a nested AGENTS.md is steered into the conversation on the first touch under its directory, see [Bundles](bundles.md#the-agents-md-bundle)), `chi bundle install cost-cap` (a plugin: a per-session $ cap, `/cap`, see [Bundles](bundles.md#the-cost-cap-bundle)), `chi bundle install work-cap` (a plugin: a per-session cap on tool calls and active minutes, `/workcap`, see [Bundles](bundles.md#the-work-cap-bundle)), `chi bundle install ruby` (a plugin: an RSpec run with no examples at all, a hint, see [Bundles](bundles.md#the-ruby-bundle)) `chi bundle install skills` (a plugin: `/skill`, versions of skills, see [Bundles](bundles.md#the-skills-bundle)), `chi bundle install github-pr` (attached context: the branch's open PR, see [Bundles](bundles.md#the-github-pr-bundle)), `chi bundle install coordinator` (a plugin: parallel work in worktrees and child sessions, see [Bundles](bundles.md#the-coordinator-bundle)), `chi bundle install claude-peer` / `codex-peer` (a plugin: talk to a Claude Code or Codex session, see [Bundles](bundles.md#the-claude-peer-bundle)), `chi bundle install model-notes` (memories: reviewed model notes, see [Bundles](bundles.md#the-model-notes-bundle)) and `chi bundle install web-search` (a plugin: a `web_search` tool through your SearXNG, in no profile, see [Bundles](bundles.md#the-web-search-bundle)).
714
715
  - Installing a bundle executes its hook code at `Engine` startup. Only install bundles you trust, as you would a gem. Hooks are **not** executed at install time (copy-only); they are `module_eval`'d at `Engine.new` inside per-bundle `Samagotchi::Bundles::<name>` namespaces (no top-level `require` collisions). Keep hook files side-effect-free at load time; do work in `#call` — top-level side effects (require, IO, `at_exit`, global assignment) run once per `Engine.new` (class redefinition is idempotent).
715
716
 
716
717
  Lifecycle:
@@ -225,7 +225,7 @@ host refused it for that model.
225
225
  `ChatLoop#tool_definitions(conversation, model:)` appends them (`Tools::DeferredTools.list`) only on such an
226
226
  adapter, and only with at least one tool declared (Messages wants one). The list, per request:
227
227
 
228
- 1. the built-ins the toolkit reaches (`Registry#reachable_names`), in registry order;
228
+ 1. the built-ins and plugin tools the toolkit reaches (`Registry#reachable_names`), in registry order;
229
229
  2. the tool index's source tools while `call_tool` is declared (`ToolIndex#deferrable`, via
230
230
  `Registry::Index#deferrable`): callable, the only tool with its acts_as, a name Messages takes, no registered
231
231
  tool's name; by acts_as (`mcp_<server>_<tool>`), sorted by name;
@@ -270,11 +270,11 @@ sets `:tool_changes` for the model: the retry and every later request drop the s
270
270
 
271
271
  A direct call of a deferred tool runs whether something loaded it or not (the API doesn't check): the Engine sets
272
272
  `TurnSettings#loads_tools` from the turn's final model (after the before_turn hooks), and then `ToolRunner#run` and
273
- `KernelLoop#dispatch` take a reachable built-in by its own name (`Registry#indexed`) through the normal gate, and wrap
273
+ `KernelLoop#dispatch` take a reachable built-in or plugin tool by its own name (`Registry#indexed`) through the normal gate, and wrap
274
274
  an MCP tool's acts_as into the `call_tool` call it stands for (`ToolIndex#wrap`): `call_tool`'s row, rules and
275
275
  approvals. A reload rebuilds that wrapped call from the saved one (`BuiltinCalls.wrap_saved`). Each such call is
276
276
  logged (`tool_direct_deferred`, with `loaded:` whether the conversation had loaded it). On `api: openai` and the
277
- native loop nothing is deferred, and a direct call of a reachable built-in is refused with the `call_tool` route as
277
+ native loop nothing is deferred, and a direct call of a reachable built-in or plugin tool is refused with the `call_tool` route as
278
278
  before.
279
279
 
280
280
  ## 400 fallbacks
@@ -35,6 +35,9 @@ Three rules hold the shape:
35
35
  conversation already holds. Ids are deterministic: `ctx-<name>-<serial>-<revision[0,12]>` (the serial makes A, B, A
36
36
  three notes), `ctx-<name>-error-<error_since digits>`, `ctx-<name>-detached-<seen[0,12]>`.
37
37
  - **Polling is not activity.** The poller and the absorb never hold a worker up (`WorkerIdleExit`).
38
+ - **The leave absorbs too.** A pass looks at the context and then checks whether it should leave; a change that
39
+ lands in that gap has no later pass (the poller stops with the worker), so `Worker#left` absorbs whatever is
40
+ pending as plain notes before the recap, as it does for a background task that finished in the same gap.
38
41
 
39
42
  ## ContextSources
40
43
 
@@ -57,7 +57,8 @@ Engine#run_turn(session, prompt, on_event:, max_iterations:, cancel_controller:,
57
57
  │ # local System 1 host they wait for the turn's end: started before warm_up_next_turn, whose warm-up
58
58
  │ # waits for them, or from run_turn's ensure; docs/context.md, The judge);
59
59
  │ # then the session's toolkit (apply_toolkit!: Registry#toolkit, ToolIndex#refresh! for find_tools'
60
- │ # chi line, a prompt rebuild when it changed). A Stop during the probes or the waits returns false
60
+ │ # chi line, a prompt rebuild when it changed; a warn notice per plugin tool that arrived mid-session
61
+ │ # with nothing to reach it, Registry#stranded_names). A Stop during the probes or the waits returns false
61
62
  │ # (the ask's line says canceled; an ensure marks one the turn never settled failed).
62
63
  │ # fires the :session_start hook on the Engine's first turn, then the :before_turn hook
63
64
  │ # (a frozen copy of the history, the prompt, and resumed)
@@ -298,12 +299,13 @@ One iteration with tool calls:
298
299
  on_stream_event:, max_tool_output_chars:)
299
300
  → under TurnSettings#loads_tools, a direct call of a source tool's acts_as (mcp_fs_read) is wrapped
300
301
  into the call_tool call it stands for (Registry::Index#wrap, ToolIndex#wrap)
301
- → first, the call's entry unwraps it (Registry::Entry#unwrap: a call_tool of chi/<built-in> becomes
302
- that built-in's call with via: "call_tool", ToolIndex#unwrap); the rest sees the real call
302
+ → first, the call's entry unwraps it (Registry::Entry#unwrap: a call_tool of chi/<name>, a built-in or
303
+ a plugin tool, becomes that tool's call with via: "call_tool", ToolIndex#unwrap); the rest sees the
304
+ real call
303
305
  → a tool the session's toolkit leaves out (Registry#left_out; with via:, one Registry#indexed doesn't
304
306
  reach) skips the gate: no hooks, no card; KernelLoop#dispatch answers it (the call_tool route for a
305
307
  reachable tool, the not-loaded error for a removed one). Under TurnSettings#loads_tools a reachable
306
- built-in called by its own name is no such call: it runs through the gate, and KernelLoop#dispatch
308
+ built-in or plugin tool called by its own name is no such call: it runs through the gate, and KernelLoop#dispatch
307
309
  takes it (#loaded_entry)
308
310
  → Guardrails::Gate#evaluate (the before_tool_call hooks, then the core checks)
309
311
  → :tool_call_started
@@ -644,7 +646,9 @@ Engine initialization (in this order, among other things):
644
646
  the bundles' plugins load (load_plugins → ExtensionLoad#load_plugins; each plugin's ctx is a
645
647
  Plugin::Context over plugin_host, whose ctx.sessions it builds itself: the test kit passes its own, sessions:),
646
648
  then (plugins or not) @tool_index.declare(@tools): find_tools + call_tool (source "index", not core),
647
- declared per toolkit by Registry#declared? (a source registered at load, or a reachable built-in) and
649
+ declared per toolkit by Registry#declared? (a source registered at load, or a tool registered then that
650
+ the toolkit leaves reachable: Registry::Index#loaded; Toolkit::Resolved#declares? treats built-ins and
651
+ plugin tools alike: full declares both, another kit its list and adds) and
648
652
  sets call_tool's unwrap (Tools::ToolIndex: the chi group from Registry#reachable_names, the sources'
649
653
  groups, search, call checks, the unwrap; a source runs the call with its own plugin Context)
650
654
  @reminder_store = ReminderStore.new (the kernel gets it), @reminder_queue, @reminders
@@ -471,7 +471,7 @@ fields: a field added to an event later reaches nobody until it is named there.
471
471
  this matches the rendered text the page selects without treating different casing as the same claim. The quote may
472
472
  also be in the answer as the pages show it (`Engine#verify_quote_in?`, `ParticipantView::Redact.text`: the session's
473
473
  folder relative, home `~`, secret shapes `[redacted]`): a participant's page has only that text, and the owner's
474
- page quotes it the same way (`claimOpts()`, `normalizeClaim`; `spec/fixtures/redact_cases.json` keeps the two
474
+ page quotes it the same way (`owner_claim.js` `opts()`, `normalizeClaim`; `spec/fixtures/redact_cases.json` keeps the two
475
475
  sides alike), so a claim naming a path is taken on both. A fresh child
476
476
  checks the same anchor against its parent's history. A completed Verify reply whose final non-empty line has
477
477
  a valid `Verdict: confirmed|refuted|unclear — <reason>` appends a verdict to the session. Both pages place its card
@@ -108,9 +108,13 @@ right-hand column happens.
108
108
  Claude 5.x cost the preserved thinking each time).
109
109
  - find_tools' description is a function of the toolkit and the source groups **at load**: `ToolIndex#declare`
110
110
  snapshots source group names and description lines (name, summary, callable count). `ToolIndex#refresh!` (from
111
- `ToolkitBinding#apply!`) filters that snapshot by the new toolkit and rebuilds the `chi` line from the built-ins
112
- it reaches; it never consults live group names. The same group filter drives searches, deferred MCP tools and
113
- whether `find_tools`/`call_tool` are declared (`Registry#index_needed?`). An MCP server that finishes booting
111
+ `ToolkitBinding#apply!` at a toolkit change, and from `Engine#apply_staged_tools!` when a plugin's tool set
112
+ changed) filters that snapshot by the toolkit and rebuilds the `chi` line from the built-ins and plugin tools it
113
+ reaches; it never consults live group names. The same group filter drives searches and deferred MCP tools.
114
+ Whether `find_tools`/`call_tool` are declared (`Registry#index_needed?`) reads the groups and the entries
115
+ registered at load (`Registry::Index#loaded`), never later ones: a plugin tool `chi.replace_tools` adds doesn't
116
+ bring them in, and a loaded plugin tool it later removes still counts, so `find_tools` stays declared, perhaps
117
+ with an empty `chi` group. That is deliberate: the declared tools stay the same for the worker's life. An MCP server that finishes booting
114
118
  between load and the first turn changes search answers only, so two sessions on the same toolkit with the same
115
119
  MCP cache send the same bytes. Kits that omit `groups:` keep their previous request bytes; switching only
116
120
  groups changes the prompt key and asks `/toolkit` to confirm the cache cost once the session has history.
@@ -10,8 +10,9 @@ only library. Pure modules (`*_model.js`, `session_state.js`, `sessions_list.js`
10
10
  and have `node --test` specs under `spec/web/public/` (happy-dom for the DOM ones); the pages as a whole are covered
11
11
  by `npm run e2e` (`spec/e2e/`). `app.js` has no unit spec.
12
12
 
13
- **`app.js` has a size cap**: `npm run lint` fails past `max-lines` in `eslint.config.js` (counted as `wc -l` counts,
14
- a little over its size today). A new feature goes into a module of its own, wired from `app.js` in a few lines; a
13
+ **`app.js` has a size cap**: `npm run lint` fails past `max-lines` in `eslint.config.js` (counted as `wc -l` counts:
14
+ 2000, ratcheted down from 2250 when the versions, the owner's claim links and Verify, the composer's mode and the
15
+ strip's hide went into modules of their own, which left room for a few features' wiring). A new feature goes into a module of its own, wired from `app.js` in a few lines; a
15
16
  refactor that shrinks `app.js` ratchets the cap down with it. Never raise it: extract instead.
16
17
 
17
18
  **New exports go in new modules.** The static JS is served at unversioned URLs. Today every load revalidates
@@ -23,7 +24,7 @@ page. A factory dep a new version stops passing is the same hazard the other way
23
24
  out of `send_turn.js`, `session_actions.js` and `lists_view.js`); a page loaded before keeps all its old modules
24
25
  until a reload, so the risk is a mixed cache, not an open tab. `answer_fold.js`, `details_motion.js`,
25
26
  `turn_range.js`, `history_reveal.js`, `latest.js`, `session_payload.js`, `method_api.js`, `session_store.js`,
26
- `session_fields.js`, `feature_gates.js`, `revise_stack.js`, `revise_switch.js`, `shell_command.js`, `stale_params.js`, `popover_modal.js`, `more_sheet.js`, `hash_intent.js` (what the hash asks for: session, all, start or keep), `worker_features.js`, `block_open.js`, `text_roots.js`, `block_source.js`, `block_frame.js`, `block_host.js` and the `objectives_*.js` modules are new modules for that reason. The last three `block_*` draw a bundle's fenced blocks: [block-renderers.md](block-renderers.md).
27
+ `session_fields.js`, `feature_gates.js`, `revise_stack.js`, `revise_switch.js`, `shell_command.js`, `stale_params.js`, `popover_modal.js`, `more_sheet.js`, `hash_intent.js` (what the hash asks for: session, all, start or keep), `worker_features.js`, `version_notices.js`, `owner_claim.js`, `owner_verify.js`, `composer_mode.js`, `strip_hidden.js`, `block_open.js`, `text_roots.js`, `block_source.js`, `block_frame.js`, `block_host.js` and the `objectives_*.js` modules are new modules for that reason. The last three `block_*` draw a bundle's fenced blocks: [block-renderers.md](block-renderers.md).
27
28
 
28
29
  ## Two pages, two models
29
30
 
@@ -37,7 +38,7 @@ claim links, `composer_mount.js`, the verdict card and Verify fresh, `verify_tar
37
38
  `auth_lost.js`, `token_fragment.js`, `format.js`) but none of the data layer, the stream handling or the message
38
39
  rendering. It has its own `api` (`{status, ok, body}`, never throws, its own ended / auth-lost handling) and its own
39
40
  `EventSource`. Its parallel copies of the owner's Verify and claim helpers (`verifyFreshState`, `verifyAnchorFor`,
40
- `showPendingClaim`, `CLAIM_ROOTS`) differ on purpose in places (the source kind `reply` vs `answer`, the server's
41
+ `showPendingClaim`, `CLAIM_ROOTS`; the owner's are `owner_verify.js` and `owner_claim.js`) differ on purpose in places (the source kind `reply` vs `answer`, the server's
41
42
  `fresh_running` vs `verify_fresh`, no home/cwd in its claim options). Don't converge them as a side effect of another
42
43
  change.
43
44
 
@@ -46,8 +47,8 @@ change.
46
47
  | Holder | What | Who writes | How a change reaches the screen |
47
48
  |---|---|---|---|
48
49
  | **The session store** (`session_store.js`; app.js `store`) | `store.selected` (the open session's id), `store.current` and `store.sessions` (below) | through the store only (see The session store); other modules get getters (`selected: () => store.selected`, `state: () => store.current`) or the store itself (`stream_handlers.js`, `send_turn.js`, `session_actions.js`) | the store runs the redraws that read what changed (below); nobody calls them by hand |
49
- | **app.js module scope**: about 20 top-level `let`s (`streamControl`, `eventsControl`, `pageCapabilities`/`pageEditorUrl`, `pendingVerifyAnchor`, `pendingClaim`, `resyncsInFlight`/`resyncAgain`, `resyncAttempts`, `scopeError`…) | the page's mode and its streams | app.js only; other modules get getters and hooks | the writer touches the redraw that reads it (`store.touch("infoBar")`: the stream, versions, `restartingIds`), or calls a redraw only it feeds |
50
- | **`store.current`** (`session_state.js`: `emptySessionState` / `sessionStateFrom`; per field: `session_fields.js FIELDS`) | the open session's fields, about 40 (info bar, chips, verdicts, timing, worker, stream cursor) | `store.update` / `setStatus` from app.js, `stream_handlers.js`, `send_turn.js` (`firstPreview`, `owner`), `session_actions.js` (`status`); `forgetSession`'s `replaceCurrent`. Two writers in place, on purpose (D8 in `FIELDS`): live_timing.js's `activeTurn`, verdict_fresh.js's running list | the store runs the field's redraws (`FIELDS[key].redraws`) |
50
+ | **app.js module scope**: about 20 top-level `let`s (`streamControl`, `eventsControl`, `pageCapabilities`/`pageEditorUrl`, `resyncsInFlight`/`resyncAgain`, `resyncAttempts`, `scopeError`…) | the page's mode and its streams | app.js only; other modules get getters and hooks | the writer touches the redraw that reads it (`store.touch("infoBar")`: the stream; `version_notices.js` does it for the versions and its `restarting` set), or calls a redraw only it feeds |
51
+ | **`store.current`** (`session_state.js`: `emptySessionState` / `sessionStateFrom`; per field: `session_fields.js FIELDS`) | the open session's fields, about 40 (info bar, chips, verdicts, timing, worker, stream cursor) | `store.update` / `setStatus` from app.js, `stream_handlers.js`, `send_turn.js` (`firstPreview`, `owner`), `session_actions.js` (`status`), `owner_verify.js` (`verdicts`, `verifyFresh`); `forgetSession`'s `replaceCurrent`. Two writers in place, on purpose (D8 in `FIELDS`): live_timing.js's `activeTurn`, verdict_fresh.js's running list | the store runs the field's redraws (`FIELDS[key].redraws`) |
51
52
  | **`store.sessions`** (the hub's projection) | every card | `store.update({sessions})` from app.js: `openSessionEvents` (`applySessionEvent`), `refresh` (the list fallback), `addCreatedSession`, `applyView` (re-sorted), `replaceSelectedCard` (`setLiveStatus` through `setStatus`, `readTurnEnd`, `setUsedMemories`), the stream's close (`withoutWorker`); `session_actions.js` (`setSessions`) | the store: `lists` and `infoBar`. **Always a new array**; an entry is replaced, never changed in place. `followSelectedSession` copies the open one's owner and worker (`fieldsFromCard`) and, when not streaming, its status (`setStatus(…, "hub")`) into `store.current` |
52
53
  | **Factory closures**: the `create*` factories (popovers, panels, views) | each component's own state: open-for, busy, error, its own fetched list | the factory | an `onChange` hook back into app.js, or a getter the factory reads at draw time |
53
54
  | **The history DOM** | the conversation as drawn: which turn a bubble belongs to (`data-turn-id`, `data-answer-turn`), what opens a turn (`TURN_PROMPT`, `bubbles.js`: a prompt or a wake's note, the same turns `Steer.turn_opening?` counts for a card's or the recap's `turns_since`), kept command bubbles, which texts were shown, unnamed badges, folded answers and long prompts (`.folded`), Revise stamps (`data-revises` on a prompt; a Verify turn's `data-verify`) and the stacks' folded versions (`.stack-hidden`, `.stack-stub.open`); the switcher's version on view is `revise_stack.js`'s own (a copy `.stack-view` before the top answer, the top `.stack-swapped`; never stored) | `bubbles.js`, `cards_place.js`, `question_cards.js`, `turn_block.js`/`stage_view.js`, `answer_fold.js`, `revise_stack.js`, `stream_handlers.js` (a live `turn_completed`'s `revises` and `verify` onto its prompt) | read back as state: `turn_range.js turnElements` (one walker for a turn's elements), `keptCommandBubbles`, `seedSeenFromDom`, `nameUnnamedBadges`/`nameVerdictCards`, `answer_fold.js` and `revise_stack.js` (their observers decorate every path that fills `#history`; the stacks are `stacks()` of the stamps, read again on each change, never a store field), `history_reveal.js revealInHistory` (the one way a jump unfolds what hides its target) |
@@ -73,8 +74,9 @@ through the store**, never `current.X = …`:
73
74
  old one writes there alone.
74
75
  - `store.batch(fn)`, `store.touch(name)`, `store.on(name, fn)`: the redraws (below).
75
76
 
76
- The ESLint block `samagotchi/session-writes` (eslint.config.js) enforces it in the six modules that hold the state
77
- (app.js, `stream_handlers.js`, `send_turn.js`, `session_actions.js`, `start_page.js`, `working_set_view.js`): an
77
+ The ESLint block `samagotchi/session-writes` (eslint.config.js) enforces it in the seven modules that hold the state
78
+ (app.js, `stream_handlers.js`, `send_turn.js`, `session_actions.js`, `start_page.js`, `working_set_view.js`,
79
+ `owner_verify.js`): an
78
80
  assignment, `++`, `delete` or `Object.assign` into `current`, `store.current` or `state()` is an error
79
81
  (`spec/web/public/lint_session_writes.test.js` runs it on a sample). An alias (`const st = state(); st.x = …`) isn't
80
82
  caught: don't write through one.
@@ -105,16 +107,16 @@ stale).
105
107
  **The redraws are registered, not called.** app.js registers each name once (`store.on`, at the bottom, once every
106
108
  piece a redraw calls exists): `lists` → `lists.render()`, `paintActive` → `lists.paintActive()`, `settingsPanel` →
107
109
  `settingsControl.refresh()`, `workingSetStrategy` → `workingSet.strategyChanged()`, `featureGates` (below), `composer` →
108
- `updateComposerMode()`, `infoBar` → `updateInfoBar()`. A writer writes and stops there: no `updateInfoBar()` after
110
+ `composerMode.update()` (`composer_mode.js`), `infoBar` → `updateInfoBar()`. A writer writes and stops there: no `updateInfoBar()` after
109
111
  a store write. A value written unchanged redraws nothing (a chunk with the same ctx %).
110
112
 
111
113
  An input the store doesn't hold calls `store.touch(name)` where it changes: `infoBar` for the clock (liveTurn's
112
- `onTick`), the versions (`noticeVersions`), `restartingIds`, the stream (`startStream`, `workerGone`), the working
114
+ `onTick`), the versions and the sessions restarting (`version_notices.js` `notice`, `restart`), the stream (`startStream`, `workerGone`), the working
113
115
  set's and the participants' `onChange`, the continue and review in-flight sets
114
116
  (`session_actions.js`); `composer` and `infoBar` for `liveTurn.running` (`setTurnRunning`, with `settingsPanel`:
115
117
  the open ⚙ panel's running-turn note) and at the end of `renderSessionView` (a whole view drawn). Left as they are,
116
- on purpose: the `narrowScreen` listener calls `updateComposerMode()` (the bar doesn't read it),
117
- `applyStripHidden`/`applyView` call `updateSessionTitle()`, and `closeStream`/`stream.restart` set
118
+ on purpose: `composer_mode.js`'s phone-width listener calls its `update()` (the bar doesn't read it),
119
+ `strip_hidden.js`'s apply and `applyView` call `updateSessionTitle()`, and `closeStream`/`stream.restart` set
118
120
  `streamControl = null` without a redraw (a resync or a workerGone follows).
119
121
 
120
122
  A function that writes more than once batches its synchronous part, so each redraw runs once: `select` (before its
@@ -178,7 +180,7 @@ The redraws, by their store names:
178
180
  - `infoBar` (`updateInfoBar()`): the session bar's buttons and info line (`info_bar_model.js sessionBar` →
179
181
  `info_bar.js`), the annotate bar, the fold controls, `updateSessionTitle()` and the fresh link's picker. Cheap: it
180
182
  runs on most writes and every clock tick.
181
- - `composer` (`updateComposerMode()`): the composer's placeholder, buttons and delivery line (selected, running,
183
+ - `composer` (`composer_mode.js` `update()`): the composer's placeholder, buttons and delivery line (selected, running,
182
184
  owner).
183
185
  - `settingsPanel` (`settingsControl.refresh()`): the ⚙ chip's **open panel** (`settings_chip.js`; the chip itself is
184
186
  drawn by the bar), on `thinking`, `reasoningHistory` and `llmContext`; `workingSetStrategy`: the working set's open
@@ -190,7 +192,8 @@ The redraws, by their store names:
190
192
  `forgetSession()` is the one reset: `select` and `clearSelection` both call it, so anything per-session that isn't
191
193
  in `store.current` (capabilities, the markdown warning and tip, the init row, the Verify claim, the armed Revise,
192
194
  the running turn's DOM) is cleared there, and the feature gates are primed for the empty session. A new per-session holder
193
- (an armed composer anchor like the Verify claim or the Revise) goes into it. The armed Revise
195
+ (an armed composer anchor like the Verify claim or the Revise) goes into it. The armed Verify claim is
196
+ `owner_verify.js`'s holder (`anchor()`, `set(a)`, `clear(a?)`: `clear(a)` lets go only of `a`). The armed Revise
194
197
  (`pendingReviseAnchor`) is set only through `setReviseAnchor`, which redraws its chip and the Revise buttons (a
195
198
  redraw only it feeds, not a store field).
196
199
 
@@ -222,7 +225,7 @@ each keeps one set of handlers, confirmations and in-flight rules. A chip the sh
222
225
  matters at exactly 600 px, where the CSS is a phone's but the shell (`SHEET_MAX`, `<`) still draws a popover, not a sheet.
223
226
  A new chip on the bar needs its place in that CSS's hidden list and a row in `moreRows`, and its attention state in the row, or
224
227
  it is lost on a phone. `app.js`'s `updateInfoBar` feeds the sheet (`update`) on every redraw; its rows and actions draw into
225
- fixed slots, so a row that joins keeps the focus where it was. The `narrowScreen` listener closes the sheet when the window grows.
228
+ fixed slots, so a row that joins keeps the focus where it was. `composer_mode.js`'s phone-width listener closes the sheet when the window grows.
226
229
 
227
230
 
228
231
  ## The stage's height
@@ -321,11 +324,11 @@ reach another device or the server.
321
324
  | `chi_model`, `chi_model_recent` | `model_pick.js`, `model_picker.js`, `start_page.js` | the start page's model and its recent list |
322
325
  | `chi_composer_collapsed` | `composer_fold.js` | the composer folded (a card's text field folds it for the time it has the focus on a narrow or touch screen, `composer_grip.js watchCardField`, on top of this and of the visit's state: neither is written) |
323
326
  | `chi_composer_floor` | `composer_grip.js`, `composer_mount.js` | the composer's dragged height |
324
- | `chi_strip_hidden` (`chi_zen`: the old name, read once) | app.js | the session strip hidden |
327
+ | `chi_strip_hidden` (`chi_zen`: the old name, read once) | `strip_hidden.js` | the session strip hidden |
325
328
  | `chi_stage_collapsed` | `stage_view.js` | the stage collapsed |
326
329
  | `chi_working_set_collapsed` | `working_set_rail.js` | the working set rail collapsed |
327
330
  | `chi_markdown_tip_dismissed` | `markdown_tip.js` | the markdown tip dismissed |
328
- | `chi_update_available_shown` | app.js | update notices already shown |
331
+ | `chi_update_available_shown` | `version_notices.js` | update notices already shown |
329
332
  | `chi_notify`, `chi_push_device` | `notifications.js` (an injected storage) | the bell's state, this device's push id |
330
333
  | `chi_answer_fold:<session>` | `answer_fold.js`, `revise_stack.js` | the answers and long prompts (`<turn id>:prompt`) folded by hand, and the Revise stubs unfolded by hand (`stub:<turn id>`) (swept with the session list) |
331
334
  | `chi_review_history:<grant>`, `chi_review_history_session:<session>` | `review_history.js` (participant page) | a participant's composer history |