samagotchi 0.71.0 → 0.73.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 (87) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +70 -1
  3. data/docs/architecture.md +15 -9
  4. data/docs/cli.md +12 -5
  5. data/docs/configuration.md +23 -13
  6. data/docs/hooks.md +5 -3
  7. data/docs/internals/anthropic.md +1 -1
  8. data/docs/internals/background-tasks.md +2 -2
  9. data/docs/internals/engine.md +38 -13
  10. data/docs/internals/llm-context-live.md +1 -1
  11. data/docs/internals/prompt-caching.md +6 -0
  12. data/docs/plugins.md +18 -7
  13. data/docs/sessions.md +5 -3
  14. data/docs/testing.md +10 -1
  15. data/lib/samagotchi/bootstrap/config_writer.rb +2 -3
  16. data/lib/samagotchi/bridge.rb +5 -2
  17. data/lib/samagotchi/broadcast/scope_card.rb +2 -2
  18. data/lib/samagotchi/bundles/claude-peer/manifest.yml +2 -7
  19. data/lib/samagotchi/bundles/claude-peer/plugin.rb +2 -2
  20. data/lib/samagotchi/bundles/codex-peer/manifest.yml +2 -6
  21. data/lib/samagotchi/bundles/codex-peer/plugin.rb +2 -2
  22. data/lib/samagotchi/bundles/coordinator/manifest.yml +8 -6
  23. data/lib/samagotchi/bundles/coordinator/plugin.rb +10 -0
  24. data/lib/samagotchi/bundles/coordinator/skill_coordinator.md +2 -1
  25. data/lib/samagotchi/bundles/system/config_modification_protocol.md +1 -1
  26. data/lib/samagotchi/bundles/system/manifest.yml +2 -2
  27. data/lib/samagotchi/config.rb +7 -25
  28. data/lib/samagotchi/context_note.rb +2 -6
  29. data/lib/samagotchi/effective_model.rb +1 -1
  30. data/lib/samagotchi/engine.rb +91 -213
  31. data/lib/samagotchi/engine_peers.rb +2 -0
  32. data/lib/samagotchi/event_routes.rb +83 -0
  33. data/lib/samagotchi/hook_routes.rb +16 -37
  34. data/lib/samagotchi/host_config.rb +1 -1
  35. data/lib/samagotchi/host_keys.rb +0 -2
  36. data/lib/samagotchi/paths.rb +13 -0
  37. data/lib/samagotchi/plugin/api.rb +11 -5
  38. data/lib/samagotchi/plugin/peer.rb +5 -8
  39. data/lib/samagotchi/prompt_files.rb +2 -4
  40. data/lib/samagotchi/session.rb +7 -1
  41. data/lib/samagotchi/session_chain.rb +24 -0
  42. data/lib/samagotchi/session_continue.rb +3 -1
  43. data/lib/samagotchi/session_manager.rb +6 -16
  44. data/lib/samagotchi/session_model_settings.rb +227 -0
  45. data/lib/samagotchi/session_setup.rb +2 -1
  46. data/lib/samagotchi/system_prompt.rb +18 -13
  47. data/lib/samagotchi/terminal_ui/attached_loop.rb +30 -14
  48. data/lib/samagotchi/terminal_ui/formatting.rb +4 -0
  49. data/lib/samagotchi/tool_activity.rb +1 -0
  50. data/lib/samagotchi/tool_declarations.rb +4 -0
  51. data/lib/samagotchi/toolkit.rb +55 -24
  52. data/lib/samagotchi/toolkit_binding.rb +7 -5
  53. data/lib/samagotchi/toolkit_command.rb +28 -5
  54. data/lib/samagotchi/tools/builtins.rb +2 -1
  55. data/lib/samagotchi/tools/delegate.rb +32 -6
  56. data/lib/samagotchi/tools/execute.rb +1 -15
  57. data/lib/samagotchi/tools/list_sessions.rb +2 -2
  58. data/lib/samagotchi/tools/peers.rb +5 -2
  59. data/lib/samagotchi/tools/register_reminder.rb +1 -0
  60. data/lib/samagotchi/tools/registry.rb +1 -1
  61. data/lib/samagotchi/tools/task_runtime.rb +69 -6
  62. data/lib/samagotchi/tools/tool_index.rb +15 -4
  63. data/lib/samagotchi/turn_note.rb +4 -1
  64. data/lib/samagotchi/version.rb +1 -1
  65. data/lib/samagotchi/web/app.rb +40 -13
  66. data/lib/samagotchi/web/public/app.js +53 -28
  67. data/lib/samagotchi/web/public/bubbles.js +5 -3
  68. data/lib/samagotchi/web/public/cards_place.js +6 -10
  69. data/lib/samagotchi/web/public/chains.js +22 -2
  70. data/lib/samagotchi/web/public/chip_popover.js +3 -1
  71. data/lib/samagotchi/web/public/composer.js +13 -6
  72. data/lib/samagotchi/web/public/context_chips.js +25 -12
  73. data/lib/samagotchi/web/public/data.js +14 -1
  74. data/lib/samagotchi/web/public/format.js +5 -0
  75. data/lib/samagotchi/web/public/index.html +39 -2
  76. data/lib/samagotchi/web/public/notice_links.js +55 -0
  77. data/lib/samagotchi/web/public/question_cards.js +33 -1
  78. data/lib/samagotchi/web/public/sessions_list.js +2 -3
  79. data/lib/samagotchi/web/public/stage_frame.js +1 -1
  80. data/lib/samagotchi/web/public/stage_slots.js +35 -3
  81. data/lib/samagotchi/web/public/stage_view.js +17 -6
  82. data/lib/samagotchi/web/public/stream_handlers.js +6 -3
  83. data/lib/samagotchi/web/public/timing.js +1 -1
  84. data/lib/samagotchi/web/public/toolkit_chip.js +3 -1
  85. data/lib/samagotchi/web/public/turn_view.js +3 -2
  86. data/lib/samagotchi/web/session_hub.rb +8 -0
  87. metadata +4 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b92ddae3d906d7ba8192ddb77ac75b8d3f12e5d34ffc146f45ca18971a3df9d7
4
- data.tar.gz: 668e0a999ef5cf276e8b40953a968301c84930399fb2e2ace30e34e18332641e
3
+ metadata.gz: 71fb1fa81145a92b955f7beaa2a9a1eabc37d29b9c8336dd65e27d5d616f23fa
4
+ data.tar.gz: b68963f09b363c1b03f8a32f14ebc41cf36297df3c290c594acef6735d0bb863
5
5
  SHA512:
6
- metadata.gz: 9ebddc5482d48c2e8b8d637b967ea87c939c267a79db1839dbfd7286459ccb6a303b75e2b2307a6456bc5aafc74228f1a49b912e18f4b5e31eacb1368589cf2c
7
- data.tar.gz: ef0521a3cfa0af34d6469f8b5a77ed0fd8a34bd5f300e08a8a6ef5b1f0878dfc643db2e56b08b0ee0e9f414ed5ed5c5dc875ca1a1410176e2c0e8205dd31b256
6
+ metadata.gz: 1752a44dd7c444534218f23c00ecc1746047875e4dac427b8395cd6cfb06169b38c456ff2bcbf4ca4000d68ce467454f52c2f4564547824ddb5c83dd586f5f2f
7
+ data.tar.gz: 66d5fcfd2b0a62957011653911edc44774843ac13837203cf1211e0523ae8d834025d0145324cd955d2f5c827f8b4a1e52c8b9c7db6a3735c6c24e4442eea7c0
data/CHANGELOG.md CHANGED
@@ -8,6 +8,73 @@ and commands may change between minor versions. How releases are made:
8
8
 
9
9
  ## [Unreleased]
10
10
 
11
+ ## [0.73.0] - 2026-10-16
12
+
13
+ ### Added
14
+
15
+ - Web: the stage's asking text (a question's or approval's reply) renders as markdown with clickable links when
16
+ `web.markdown` is on, and so does a resolved question card's copy of it once the card is opened; their
17
+ `path:line` refs are links with markdown off too.
18
+
19
+ ### Changed
20
+
21
+ - Web: `continue →` fills the new link's composer with the previous link's own first message when that link was a
22
+ continuation itself (else `Continue where we left off.`), so a chain's opener comes back on every link; a chain
23
+ link's top bar and tab show its card's title whatever opener it was sent.
24
+ - Web: http(s) URLs in a hook's notice line (source-links' `sources:` line with markdown off) are links; they open
25
+ in a new tab.
26
+
27
+ ### Fixed
28
+
29
+ - A `task_create` task given a `cwd`, or running `cd`, recorded `failed` / `process_ended_without_exit_code` even
30
+ when it succeeded (its exit code was written relative to the shell's directory); a task killed by a signal now
31
+ records 128 + the signal.
32
+
33
+ Update with `chi update` (the system bundle 0.73.0), then restart `chi web`.
34
+
35
+ ## [0.72.0] - 2026-10-16
36
+
37
+ ### Added
38
+
39
+ - A toolkit can carry a prompt text (`toolkit.kits.<name>.prompt:`, `chi.toolkit … prompt:`): the sessions on it
40
+ get it in their system prompt, others don't; `/toolkit`, `/stats` and the web's tools chip show it. `delegate`
41
+ can start a child on a named toolkit (`toolkit:`). The coordinator bundle (0.5.0) declares a `coding` toolkit
42
+ (`child`'s tools plus "Be concise in your responses" and "Show file paths clearly when working with files"),
43
+ and its skill starts build and fix children on it.
44
+
45
+ ### Removed
46
+
47
+ - `SAMAGOTCHI_HOSTS_JSON`: chi no longer reads it (workers stopped inheriting it in 0.64.0). Hosts come only from
48
+ config.yml's `hosts:`; one still exported in your shell is ignored.
49
+
50
+ ### Fixed
51
+
52
+ - Attached TUI: a turn of images alone sent from the web names each image once while you watch it start (or join
53
+ it running), not twice.
54
+ - Attached TUI: joining a session shows a call the guardrails denied as `blocked`, as it showed live and as the
55
+ web shows it, instead of `error`.
56
+ - Web: an answer's file ref outside the session's folder is dropped from the end-of-turn re-read of a running
57
+ session too, as a reload drops it, instead of showing until the reload.
58
+ - Plugins: a turn no longer fails with "can't add a new key into hash during iteration" when a plugin's
59
+ `replace_tools` registers a tool while a page reads the session's settings (no MCP server loaded).
60
+ - `call_tool` given the bare name of a tool the session declares (`read`) answers at once that it's declared and
61
+ should be called directly, instead of first waiting for every MCP server still starting.
62
+ - Web: a `/model`, `!command` or chip change still in flight when you open another session no longer acts on
63
+ that one: it no longer marks it as held by a terminal, puts the failure in its history or follows its worker,
64
+ and a card's action button no longer follows it either; an after-turn hook's answer re-read no longer lands in it.
65
+ - Web: a stop the server refuses (the stop button in the session bar) now says why in a toast instead of nothing.
66
+ - Web: a failure the server explains (a refused `/model`, send, archive, attach…) shows its words alone, without an
67
+ HTTP status after them (`a terminal chi holds it`, not `… (409)`); the status shows only when the server gave no words.
68
+ - Web: opening another session right as a finished turn slides into the history no longer shows that turn in the
69
+ new session's history, and its "Loading…" line stays until the session is drawn.
70
+ - Web: opening a chip's popover (llm ctx, thinking, reasoning history, attached context) closes the one already
71
+ open, instead of leaving both open.
72
+ - The empty-answer retry note asks the model to continue the task (its next tool call) or answer if it is done,
73
+ instead of "answer the user's last message now, briefly", which made a mid-task model stop before its last steps.
74
+
75
+ Update with `chi update` (the system bundle 0.72.0, coordinator 0.5.0, claude-peer 0.4.1, codex-peer 0.3.1), then
76
+ restart `chi web`.
77
+
11
78
  ## [0.71.0] - 2026-10-15
12
79
 
13
80
  ### Changed
@@ -3066,7 +3133,9 @@ and long-lived sessions.
3066
3133
  - A macOS desktop helper (`chi desktop install`): a "Send to chi" Service and a
3067
3134
  hotkey panel that send selected text or the clipboard to your sessions.
3068
3135
 
3069
- [Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.71.0...HEAD
3136
+ [Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.73.0...HEAD
3137
+ [0.73.0]: https://github.com/dm1try/samagotchi/compare/v0.72.0...v0.73.0
3138
+ [0.72.0]: https://github.com/dm1try/samagotchi/compare/v0.71.0...v0.72.0
3070
3139
  [0.71.0]: https://github.com/dm1try/samagotchi/compare/v0.70.0...v0.71.0
3071
3140
  [0.70.0]: https://github.com/dm1try/samagotchi/compare/v0.69.0...v0.70.0
3072
3141
  [0.69.0]: https://github.com/dm1try/samagotchi/compare/v0.68.0...v0.69.0
data/docs/architecture.md CHANGED
@@ -44,7 +44,7 @@ A compact visual overview of the current architecture, then the core API in pros
44
44
  │ execute · read · edit · write · memory_read/write · web_fetch · │
45
45
  │ task_create/get/list/stop/wait · delegate · delegate_result · ask_user_question │
46
46
  │ register_reminder/cancel_reminder/list_reminders · send_note · list_sessions · │
47
- │ context_read · forget_outputs (+ bundle plugin tools) │
47
+ │ context_read · forget_outputs · find_tools · call_tool (+ bundle plugin tools) │
48
48
  │ offered by Tools::Registry (Builtins + bundles), filtered by the toolkit │
49
49
  └──────────────────────────────────────────────────────────────────────────────────┘
50
50
  │
@@ -101,7 +101,7 @@ terminal renders the events it gets over the Bridge.
101
101
  | UI | `Samagotchi::TerminalUI` | REPL (Reline), rendering, REPL commands. Delegates all core work to an `Engine`. |
102
102
  | Model loops | `KernelLoop` (via `LLM::NativeBackend`), `LLM::ChatLoop` | The model↔tool loop: raw prompt, or a chat API (OpenAI chat, Anthropic Messages), chosen per host (see below). |
103
103
  | Adapters | `Samagotchi::Client`, `LLM::OpenAIChat`, `LLM::AnthropicMessages`, `LLM::HTTP` | Raw-prompt servers, the OpenAI chat API, Anthropic's Messages API, and the HTTP they share. |
104
- | Tools | `lib/samagotchi/tools/*` | Execute, read, edit, write, memory, task_*, web_fetch, delegate/delegate_result, ask_user_question, reminders, send_note, list_sessions, context_read, forget_outputs, plus runtime/output-guardrails. |
104
+ | Tools | `lib/samagotchi/tools/*` | Execute, read, edit, write, memory, task_*, web_fetch, delegate/delegate_result, ask_user_question, reminders, send_note, list_sessions, context_read, forget_outputs, find_tools/call_tool (the tool index), plus runtime/output-guardrails. |
105
105
  | Bundles | `lib/samagotchi/memory_bundle/*`, `BundleCommand` | `chi bundle`: install/upgrade (3-way merge), build, uninstall; the install record under `.bundles/<name>/` that the hook, guardrail and plugin loaders read ([internals/bundles.md](internals/bundles.md)). |
106
106
  | Background | `Samagotchi::Worker`, `Samagotchi::SessionManager` | `SessionManager` forks/spawns one worker process per session; its `Worker` builds the `Engine` and `Bridge` (no terminal rendering) and runs queued turns. The spawned process (own process group) forks the worker, which `setsid`s and is reparented to init once its starter exits (it waits until the worker holds `owner.lock`, so the spawner's `Process.detach` waiter tells a starting worker): nothing that walks the spawner's process tree reaches it (`SAMAGOTCHI_WORKER_DETACH=1`, `detach_from_spawner`). The lock's pid is the worker's; `spawn`'s `child_pid` is its starter's. |
107
107
  | Web | `Samagotchi::Web::App`, `Samagotchi::Web::Server`, `Samagotchi::Web::SessionHub` | Rack+WEBrick on one port, `127.0.0.1:4567` (index.html + `/api/*` + SSE); with `web.host: lan` also on this machine's LAN address, behind an access token (`Web::Lan`, `Web::Token`). The hub is chi web's projection of the session list, pushed to every tab over `GET /api/events`. |
@@ -148,7 +148,7 @@ agent logic and can be used without any terminal rendering; the UI is a thin lay
148
148
  | UI | `Samagotchi::TerminalUI` | Interactive REPL (Reline), rendering (ANSI, spinner, status line), REPL commands. Delegates all core work to an `Engine`. |
149
149
  | Model loops and adapters | `KernelLoop`, `LLM::ChatLoop`, `Samagotchi::Client`, `LLM::OpenAIChat`, `LLM::AnthropicMessages`, `LLM::HTTP` | The model↔tool loops and the HTTP adapters they talk through (see "Model loops and adapters"). |
150
150
  | Bridge (SSE/HTTP) | `Samagotchi::Bridge`, `SessionManager` | The **single live client transport**: an SSE read stream + HTTP POST turn/cancel/answer surface that attaches to a worker's existing `Engine` via `Engine#subscribe`. Every session worker starts it (bound `127.0.0.1`, no auth, localhost-only). |
151
- | Web (Rack) | `Samagotchi::Web::App`, `SessionManager` | Single-port `127.0.0.1:4567` control plane via `rack`+`webrick` (serve `index.html` + `/api/*`). With `web.host: lan` (or one of this machine's IPv4 addresses) the server also listens on that address, where every request but this machine's needs the access token (`App#token_gate`: the `chi_token` cookie or `Authorization: Bearer`; [CLI](cli.md#chi-web-on-your-phone)). It talks to each worker's Bridge (`App#relay`, `bridge_get`; `/stream` proxies it) and reads the session files; no `Engine` of its own. `bin/chi web` entrypoint. |
151
+ | Web (Rack) | `Samagotchi::Web::App`, `SessionManager` | Single-port `127.0.0.1:4567` control plane via `rack`+`webrick` (serve `index.html` + `/api/*`). With `web.host: lan` (or one of this machine's IPv4 addresses) the server also listens on that address, where every request but this machine's needs the access token (`App#token_gate`: the `chi_token` cookie or `Authorization: Bearer`; [CLI](cli.md#chi-web-on-your-phone)). It talks to each worker's Bridge (`App#relay`, `bridge_get`; `/stream` proxies it) and reads the session files; no `Engine` of its own. Markdown (`web.markdown`) is rendered here only (`Web::MarkdownRenderer`): the answers it serves, card bodies, and `POST /api/render` for a text the page already shows. `bin/chi web` entrypoint. |
152
152
  | Sessions | `Samagotchi::Session`, `SessionManager` | File-based `~/.local/state/samagotchi/sessions/<uuid>.json` + sidecar `input/`/`output/`; retention (14d/500) + ordering (`updated_at desc`). |
153
153
 
154
154
  - `bin/chi` in REPL mode (see `LaunchMode` above) builds `TerminalUI`. `TerminalUI#run` is the single
@@ -208,7 +208,8 @@ engine.session # current session (Engine owns create/resume)
208
208
  The system prompt (`SystemPrompt#build`, assembled in `#system_prompt_with_index`) is the
209
209
  base prompt, then the toolkit's "Not loaded in this session" line (only when the toolkit
210
210
  leaves tools out), rg guidance, the identity, the model notes, the preloaded memories,
211
- AGENTS.md (root → cwd), the appended files (`--append-system-prompt`), the memory
211
+ AGENTS.md (root → cwd), the appended files (`--append-system-prompt`), the toolkit's
212
+ kit prompt (a kit's `prompt:` text, only when it has one), the memory
212
213
  indexes, and last **the model** (`Model: this session runs on <ref> (host …; model key …)` and a line telling
213
214
  the model to answer "which model are you" from it, not from training), the working
214
215
  directory and the session id and log (Gemma's native tool declarations still follow).
@@ -307,6 +308,7 @@ process that already owns the `Engine`); every worker starts it, and it exposes:
307
308
  the messages itself, then streams from its `event_seq`). Read by the web's full show
308
309
  (`GET /api/sessions/:id`, `?parts=1`) and the plugin's `list_sessions`; every message is copied.
309
310
  - `GET /session/:id/tail` — the page's light re-read (`Bridge#tail_frame`): `session_state_snapshot`,
311
+ `working_directory` (the session's folder, which the web checks the answer's file refs against),
310
312
  `answer` (the last message a UI shows as an answer, `AnswerTail`: the one message, copied alone),
311
313
  `cards` (`CardStore#list`; a card an earlier worker showed during a turn comes back read-only, with no
312
314
  actions: its turn no longer runs), `event_seq`, `event_id`, all taken in one event-log hold; no message
@@ -339,7 +341,9 @@ The page's frequent reads of `GET /api/sessions/:id` stay light too:
339
341
  finishes the ended turn's own line by its `turn_id` (sent as `&turn_id=`, which also picks that
340
342
  turn's answer). A tail read's timing is always the newest turn's; `recent=1` is accepted and not needed.
341
343
  The page binds an answer's `display_refs` to its bubble (`answer_refs.js`: on a reload's render and on this
342
- read's re-render) and pairs them with the bubble's links and text before the `path:line` scan.
344
+ read's re-render) and pairs them with the bubble's links and text before the `path:line` scan. A file ref outside the
345
+ session's folder is dropped, as a reload drops it: the folder `/tail` names, else (an older worker) the hub's
346
+ session list entry's.
343
347
  A plugin tool's row refs (`refs`, `RowRefs`) ride its tool events and, on a reload, its part (saved as
344
348
  `tool_refs`); `turn_view.js` binds them to the row's title (`refs.js bindRowRefs`: the first on the title,
345
349
  the others in its menu).
@@ -490,7 +494,7 @@ server (`/props`), then the host's model list, then config.
490
494
  **Keys.** A host's API key comes from the environment variable its
491
495
  `api_key_env:` names, else, on macOS, the login Keychain's item for that name (`HostKeys`, read through
492
496
  `/usr/bin/security` with a deadline; hits cached per process; `SAMAGOTCHI_KEYCHAIN=off` skips it); it never
493
- reaches config.yml, `SAMAGOTCHI_HOSTS_JSON`, `chi self`, logs or events.
497
+ reaches config.yml, `chi self`, logs or events.
494
498
 
495
499
  ## Other components
496
500
 
@@ -504,7 +508,7 @@ A map of the parts the overview above doesn't name (paths under `lib/samagotchi/
504
508
  | Plugin API | `plugin/api.rb` (`Plugin::Api`), `plugin/tool_spec.rb` (`Plugin::ToolSpec`: a checked tool declaration, how it is registered), `plugin/loader.rb` | What a bundle's plugin registers: tools, commands, annotation actions, hooks, services, toolkits, peer sources ([plugins.md](plugins.md)). |
505
509
  | Peer bundles' base | `plugin/peer.rb` (`Plugin::Peer`) | What claude-peer and codex-peer share (the send tool, the command, the cards, payloads); each bundle's subclass sets its strings and transport ([plugins.md](plugins.md#the-claude-peer-bundle)). |
506
510
  | Shipped bundles | `bundles/` (`mcp`, `github-pr`, `loop-guard`, `skills`, `source-links`, …) | Bundles that ship with chi. |
507
- | Hooks | `hooks/registry.rb` (`Hooks::Registry`), `hooks/loader.rb`, `hook_routes.rb` (`HookRoutes`) | Hook registration and dispatch; what a hook or plugin does beyond reading its event (notice, question, stop, steer) routed to the UIs ([hooks.md](hooks.md)). |
511
+ | Hooks | `hooks/registry.rb` (`Hooks::Registry`), `hooks/loader.rb`, `hook_routes.rb` (`HookRoutes`), `event_routes.rb` (`EventRoutes`) | Hook registration and dispatch; what a hook or plugin does beyond reading its event (notice, question, stop, steer) routed to the UIs (cards and notices through one set of routes) ([hooks.md](hooks.md)). |
508
512
  | Guardrails | `guardrails.rb`, `guardrails/shell_lex.rb`, `guardrails/rules.rb`, `guardrail_wiring.rb` | The gate before every tool call: shell lexing, YAML rules, approvals ([guardrails.md](guardrails.md)). |
509
513
  | Delegation | `tools/delegate.rb`, `tools/delegate_result.rb`, `tools/delegate_wait.rb`, `child_reports.rb`, `relay_desk.rb` | Child sessions a session starts and waits on, and their approval relay ([sessions.md](sessions.md#delegating)). |
510
514
  | send / broadcast | `send_command.rb` (`SendCommand`), `broadcast_command.rb` (`BroadcastCommand`), `broadcast/` | `chi send` and `chi broadcast` ([broadcast.md](broadcast.md), [sub-agent.md](sub-agent.md)). |
@@ -514,11 +518,13 @@ A map of the parts the overview above doesn't name (paths under `lib/samagotchi/
514
518
  | Session chains | `session_continue.rb` (`SessionContinue`), `session_chain.rb` (`SessionChain`) | A session that continues an earlier one. |
515
519
  | Analytics | `analytics_file.rb` (`AnalyticsFile`) | A session's `analytics.json` (`SessionMetrics#persist`). |
516
520
  | Images / vision | `image_store.rb` (`ImageStore`), `vision_support.rb`, `vision_context.rb` | Stored prompt images and whether a model takes them. |
517
- | Toolkits | `toolkit.rb` (`Toolkit`), `toolkit_binding.rb` (`ToolkitBinding`, the Engine's seam), `toolkit_command.rb` (`/toolkit`), `tools/registry.rb` (`Registry#toolkit`, `#left_out_names`), `web/public/toolkit_chip.js` | Which registered tools a session's prompts declare: saved in the session, resolved at a turn's start (`Engine#apply_toolkit!`), named in the prompt's not-loaded line; `chi.toolkit` and `ctx.use_toolkit` for plugins ([configuration.md](configuration.md#toolkits)). |
521
+ | Toolkits | `toolkit.rb` (`Toolkit`), `toolkit_binding.rb` (`ToolkitBinding`, the Engine's seam), `toolkit_command.rb` (`/toolkit`), `tools/registry.rb` (`Registry#toolkit`, `#left_out_names`), `web/public/toolkit_chip.js` | Which registered tools a session's prompts declare: saved in the session, resolved at a turn's start (`Engine#apply_toolkit!`), named in the prompt's not-loaded line, a kit's prompt text after the appended files; `chi.toolkit` and `ctx.use_toolkit` for plugins ([configuration.md](configuration.md#toolkits)). |
522
+ | Tool index | `tools/tool_index.rb` (`Tools::ToolIndex`) | `find_tools` and `call_tool`: the tools a session knows but doesn't declare one by one (an mcp bundle's servers through `chi.tool_source`, the built-ins its toolkit leaves out), searched and called by name ([plugins.md](plugins.md)). |
523
+ | Tool output cut | `tool_output_cut.rb` (`ToolOutputCut`), `tool_output_files.rb` (`ToolOutputFiles`) | A tool's output over `max_tool_output_chars`, cut by the tool's policy; an execute's whole output saved in the session's `tool_output/` folder ([internals/tool-guardrails.md](internals/tool-guardrails.md)). |
518
524
  | Steer delivery | `steer.rb` (`Steer`), `delivery.rb` (`Delivery`), `steer_cut.rb` (`SteerCut`), `generation_phase.rb`, `waiting_steer.rb`, `steer_cut_after.rb` (`SteerCutAfter`), `cut_policy.rb` (`CutPolicy`) | A message for a running turn: at the next boundary, cutting a thinking-only generation, or queued for the turn's end ([internals/engine.md](internals/engine.md)). |
519
525
  | Prompt files | `prompt_files.rb` (`PromptFiles`) | The AGENTS.md chain (root → cwd) and the appended files the system prompt carries. |
520
526
  | Model settings | `effective_model.rb` (`EffectiveModel`, `ModelChoice`), `model_profile.rb` (`ModelProfile`), `model_settings.rb`, `sampling_settings.rb`, `model_catalog.rb` (`ModelCatalog`), `model_notes.rb` (`ModelNotes`) | The effective model's settings as one snapshot per read, a model's prompt format, its `models:` entry, its request parameters, the pickable model rows (`chi models`, `/api/models`), the notes its prompt loads. |
521
- | Session settings | `session_setting.rb` (`SessionSetting`), `level_setting_command.rb` (`/thinking`, `/reasoning-history`), `llm_context_command.rb`, `toolkit_command.rb`, `thinking_notices.rb` (`ThinkingNotices`) | A session's own llm_context, thinking, reasoning_history and toolkit: their chips (session state, `/stats`, `:command_ran`, the web), the commands that set them, the thinking level's notices. |
527
+ | Session settings | `session_setting.rb` (`SessionSetting`), `session_model_settings.rb` (`SessionModelSettings`), `level_setting_command.rb` (`/thinking`, `/reasoning-history`), `llm_context_command.rb`, `toolkit_command.rb`, `thinking_notices.rb` (`ThinkingNotices`) | A session's own llm_context, thinking, reasoning_history and toolkit: their chips (session state, `/stats`, `:command_ran`, the web), the commands that set them, the thinking level's notices. |
522
528
  | Dialects | `llm/dialect.rb` (`LLM::Dialect`) | What an `api: anthropic` provider's `/v1/messages` does differently (`dialect: fireworks`) ([internals/anthropic.md](internals/anthropic.md)). |
523
529
  | Logging | `log.rb` (`Log`), `log_subscriber.rb` (`LogSubscriber`), `log_path.rb` (`LogPath`), `log_line.rb` | The debug log: tagged records, a session's event trail, where the file goes ([configuration.md](configuration.md#debug-log-file)). |
524
530
  | Stores | `reminder_store.rb` (`ReminderStore`), `recap_store.rb` (`RecapStore`) | A session's reminders, and its idle recap (`recap.json`). |
data/docs/cli.md CHANGED
@@ -13,7 +13,7 @@
13
13
  - A session id can be shortened to any unique prefix (like git): `chi --attach 2ea8`. `--resume`, `--attach`, `sessions stop`, `sessions archive` and `sessions delete` take one; an ambiguous prefix lists the sessions it matches.
14
14
  - `chi web [--port 4567] [--open] [--scope=all]` — start the Web UI (single localhost port session control plane) on this git project's sessions (`--scope=all`, or a folder in no repo: every session); if a chi web already runs on the port, print (with `--open`, open) its page for this folder and exit. Something else on the port (an older chi web too) exits 1 with "port N is in use"
15
15
  - `chi web --web-host lan` — the Web UI on your home network too, for your phone: a link with an access token and its QR code (see [chi web on your phone](#chi-web-on-your-phone)); `chi web --new-token` replaces the token
16
- - `chi web --web-markdown` — opt in to sanitized Markdown rendering for completed assistant messages
16
+ - `chi web --web-markdown` — opt in to sanitized Markdown rendering for completed assistant messages, the stage's asking text and a resolved question card's reply
17
17
  - `chi web --web-view turn` — draw turns with the turn view (each turn as one block of steps, the running one at the bottom of the history) instead of the default stage view (the running turn pinned above the composer); `?view=stage|turn` on the page URL overrides it (see [Web views](#web-views))
18
18
  - `chi sessions list|stop|restart|archive|unarchive|delete|prune|clean|stats` — manage persisted sessions; `list` shows this git project's, `list --scope=all` every one, a delegated session with `↳ <parent>`, `list --archived` the archived ones too (see [Sessions](sessions.md)). `stats ID` prints a session's cost, tokens and progress (a live worker's `GET stats`, else its saved `analytics.json`; `--format json` one object) without running a model turn. A usage error (an unknown subcommand or flag, a flag missing its value, `stop`, `restart` or `delete` with no ids, a bad `list --format` or `--scope`, a `--days`, `--keep` or `--limit` that isn't a whole number) exits 2; an unknown or refused session exits 1
19
19
  - `chi note [--source NAME] [-m TEXT] (ID|PREFIX)... | --all` — add a context note (TEXT or stdin) to sessions: background the model sees on its next turn; it starts no turn (see [Sessions: Context notes](sessions.md#context-notes))
@@ -459,7 +459,7 @@ request (with the cached tokens, the cache writes, the re-prefilled tokens and t
459
459
  reports them),
460
460
  the decode speed, generation latency, cancellations, retries (the network's),
461
461
  thinking cuts (generations a plugin such as loop-guard cut) and output cap hits
462
- (generations that ended at the provider's cap), context used and window (the used figure is counted against the smaller of the window and the session's LLM context budget, and says so: `context used: 16000 tokens (25.0% of the 64000 budget)`), the memory indexes the session's prompt holds ([Memory](memory.md#the-indexs-size)), the model notes it carried (`model notes: model_notes_deepseek (system, 612 chars)`, none without any; [Memory](memory.md#model-notes)), the prompt profile and the model the server says it ran, and the
462
+ (generations that ended at the provider's cap), context used and window (the used figure is counted against the smaller of the window and the session's LLM context budget, and says so: `context used: 16000 tokens (25.0% of the 64000 budget)`), the memory indexes the session's prompt holds ([Memory](memory.md#the-indexs-size)), the model notes it carried (`model notes: model_notes_deepseek (system, 612 chars)`, none without any; [Memory](memory.md#model-notes)), the toolkit's prompt text (`kit prompt: coding (~20 tokens)`, only when its kit has one; [Toolkits](configuration.md#toolkits)), the prompt profile and the model the server says it ran, and the
463
463
  cost when the provider reports one (OpenRouter's `usage.cost`), and the cost
464
464
  estimated from a price under `hosts.<name>.models` when it reports none or 0,
465
465
  marked `~` and naming the current model's host (`cost: $0.42 reported + ~$0.12 from hosts.gw.models prices (this session only)`;
@@ -520,7 +520,8 @@ line, the web as the `reasoning` chip. See "Reasoning history" in
520
520
  tokens), the session's toolkit and the tools it leaves out: `reachable
521
521
  (find_tools): …` (built-ins the model reaches with find_tools and call_tool)
522
522
  and `removed: …` (only `/toolkit add` brings one back), or `not loaded: …` when
523
- the toolkit removes call_tool. `/toolkit remove TOOL` is hard: a tool the
523
+ the toolkit removes call_tool, and its kit's prompt text when it has one (`prompt: <first line> (+N lines, ~T
524
+ tokens)`). `/toolkit remove TOOL` is hard: a tool the
524
525
  toolkit left out reachable becomes unreachable (saved, and asked first once the
525
526
  session has a model reply), also after an add. `/toolkit NAME` puts
526
527
  the session on another toolkit (dropping its own adds and removes); `/toolkit
@@ -697,8 +698,14 @@ web:
697
698
  ```
698
699
 
699
700
  Only finalized assistant messages are rendered; user messages and live streaming
700
- chunks remain escaped text. Generated HTML is sanitized, raw HTML in model output
701
- is not trusted, and unsafe links are removed. If Markdown is enabled without
701
+ chunks remain escaped text. The stage's asking text (the whole reply that asked
702
+ a waiting question or approval) shows raw at once, then as markdown, its links
703
+ opening in a new tab; a resolved question card's copy of that reply renders when
704
+ the card is first opened (its summary line stays plain). A text the page already shows and renders later goes
705
+ through `POST /api/render` (`{text}` → `{html}`, up to 64 KB; `html` is null
706
+ with markdown off), the same renderer and sanitizer. Generated HTML is
707
+ sanitized, raw HTML in model output is not trusted, and unsafe links are
708
+ removed. If Markdown is enabled without
702
709
  commonmarker installed, Chi Web keeps the normal escaped-text display and shows a
703
710
  warning explaining how to install the optional gem.
704
711
 
@@ -107,9 +107,8 @@ Behavior:
107
107
  edit at once). A worker gets the CLI flags of the chi that started it
108
108
  through its environment.
109
109
  - A worker reads `hosts:` from the file as it starts, as every other setting, so a delegate (or any session
110
- started later) sees a host added after its parent started. `SAMAGOTCHI_HOSTS_JSON` (a JSON object in the shape
111
- of `hosts:`) replaces the file's `hosts:` for the process it is set on, the way specs use it; no worker inherits
112
- it (`SessionManager.spawn_options` unsets it).
110
+ started later) sees a host added after its parent started. Hosts come only from config.yml: no environment
111
+ variable sets them.
113
112
 
114
113
  This lets you run `chi` without repeating common defaults such as model
115
114
  and llama host/port on every invocation.
@@ -1330,6 +1329,7 @@ toolkit:
1330
1329
  child: child # delegate children (unset: full)
1331
1330
  kits: # your own, on a base
1332
1331
  review: { base: lean, add: [web_fetch, delegate], remove: [memory_write] }
1332
+ terse: { base: lean, prompt: "- Be concise in your responses" }
1333
1333
  ```
1334
1334
 
1335
1335
  | toolkit | declares |
@@ -1338,15 +1338,25 @@ toolkit:
1338
1338
  | `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` |
1339
1339
  | `child` | `lean` plus `send_note`, without `memory_write`, `delegate`, `delegate_result` |
1340
1340
  | `coordinator` | `lean` plus `delegate`, `delegate_result`, `list_sessions` (declared by the coordinator bundle) |
1341
+ | `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) |
1341
1342
 
1342
1343
  - **Plugin tools** are declared in every toolkit unless its `remove:` names them, and so is `forget_outputs` under
1343
- the `forget` layer. A `kits:` entry takes `base:` (default `full`), `add:` and `remove:` (any other key, a typo
1344
- or a `tools:` list, is ignored with a warning); it can't reuse a built-in's name. A bundle declares its own with `chi.toolkit` ([plugins](plugins.md#chitoolkitname-base-add--remove-)).
1344
+ the `forget` layer. A `kits:` entry takes `base:` (default `full`), `add:`, `remove:` and `prompt:` (any other
1345
+ key, a typo or a `tools:` list, is ignored with a warning); it can't reuse a built-in's name. A bundle declares its own with `chi.toolkit` ([plugins](plugins.md#chitoolkitname-base-add--remove--prompt-nil)).
1346
+ - **A kit's prompt text.** `prompt:` (text, several lines allowed) goes into the system prompt of the sessions on
1347
+ that kit, verbatim, with no header: after AGENTS.md and the appended files, before the memory indexes. A kit
1348
+ without one inherits its base's (`review: {base: terse, add: [web_fetch]}` stays concise); `prompt: ""` clears
1349
+ it; a session's own `--tool` / `/toolkit add` never touch it. Sessions on the same kit share it in the prompt
1350
+ cache; `full` and every kit without a prompt send the bytes they sent before. Keep it to a line or two that
1351
+ fits the kit's work (the coordinator bundle's `coding`: be concise): one that isn't text or is over 2000
1352
+ characters is left out with a warning, and long text belongs in `append_system_prompt:`. `/toolkit` shows it
1353
+ (`prompt: …`), `/stats` its kit and size (`kit prompt:`), the web's tools chip in its tooltip.
1345
1354
  - **Saved at creation, never re-resolved from config.** A session stores its toolkit as named
1346
1355
  (`"toolkit": {"name": "lean", "add": [], "remove": []}`) when it is created: `toolkit.default`, `toolkit.child`
1347
- for a delegate child, or `chi --toolkit NAME` / `--tool` / `--no-tool` ([cli](cli.md)). Changing `toolkit.default`
1356
+ for a delegate child (or the kit its `delegate` call names), or `chi --toolkit NAME` / `--tool` / `--no-tool` ([cli](cli.md)). Changing `toolkit.default`
1348
1357
  doesn't move existing sessions, not even when their worker is woken again; a session from before toolkits is on
1349
- `full`. A continue and a fork keep the toolkit. `/toolkit` shows and changes it.
1358
+ `full`. A continue and a fork keep the toolkit. `/toolkit` shows and changes it. The prompt text is part of the
1359
+ kit, not of the session file: it follows the name.
1350
1360
  - **Left out: reachable or removed.** A built-in missing from a kit's base list (`lean` has no `delegate`; a
1351
1361
  `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
1352
1362
  the MCP servers' tools, and `call_tool(tool: "chi/delegate", args: {…})` runs it as `delegate` itself, through the
@@ -1368,13 +1378,13 @@ toolkit:
1368
1378
  loaded in this session (toolkit child) and can't be loaded from here; only the user can (/toolkit add
1369
1379
  memory_write).` `/toolkit` lists both kinds, and the web's toolkit chip says them in its tooltip.
1370
1380
  - **Applied at a turn's start.** A change (`/toolkit`, a plugin's `ctx.use_toolkit`) is saved at once and the next
1371
- turn declares it; once the session has a model reply, `/toolkit` asks first ("Changes that restart the prompt
1372
- cache").
1381
+ turn declares it, with its kit's prompt text; once the session has a model reply, `/toolkit` asks first
1382
+ ("Changes that restart the prompt cache"), also for a kit with the same tools and another prompt text.
1373
1383
  - **A name that doesn't resolve** (a typo, a bundle that was uninstalled) declares every tool for that process,
1374
1384
  with one `toolkit>` warning at the first turn; the saved name stays, so the bundle's return brings it back.
1375
- - **Versions.** A toolkit's definition can change with chi or a bundle, which changes what saved sessions on it
1376
- declare after an upgrade: the next request reads the prompt again once, as a tool's schema change does. So can
1377
- an edit to `toolkit.kits`: a session's worker reads the kits once when it starts, so a running session keeps its
1385
+ - **Versions.** A toolkit's definition (its tools and its prompt text) can change with chi or a bundle, which
1386
+ changes what saved sessions on it send after an upgrade: the next request reads the prompt again once, as a
1387
+ tool's schema change does. So can an edit to `toolkit.kits`: a session's worker reads the kits once when it starts, so a running session keeps its
1378
1388
  tools and the edit reaches it at its next worker start (after an idle exit, `chi sessions restart`).
1379
1389
 
1380
1390
  ## LLM context: the forget layer
@@ -1586,7 +1596,7 @@ column says so, a `--kebab-name` flag. The maps (`hosts:`, `models:`,
1586
1596
  | `context.wake` | `true` | yes | A source whose update says `wake: true` may start a turn in a live, idle session (one per source per 10 minutes, a peer source up to `context.peer_turns` in a row, within `session.max_wakes`); so may a session's pushed source's first push. `false`: updates wait as notes for the next turn. See [Waking](context.md#waking). |
1587
1597
  | `context.peer_turns` | `3` | yes | Wake turns in a row a peer agent's answers may start (a peer bundle's outbox source, such as `codex-outbox`), with no message typed in the web or a TUI in between (`chi send` doesn't count); within `session.max_wakes`. Then answers wait as notes, and the UIs hear it once. An answer whose summary ends its verdicts with `VERDICT OK` wakes once to close the round, counted toward `session.max_wakes` only. `0`: a peer source wakes like any other. See [Waking](context.md#waking). |
1588
1598
  | `toolkit.default` | `full` | | The toolkit a new session is stored on: the tools its prompts declare (`full`, `lean`, `child`, one of `toolkit.kits` or a bundle's). `chi --toolkit` picks one per session. See "Toolkits". |
1589
- | `toolkit.child` | `full` | | A delegate child's toolkit. See "Toolkits". |
1599
+ | `toolkit.child` | `full` | | A delegate child's toolkit, unless its `delegate` call names one (`toolkit:`). See "Toolkits". |
1590
1600
  | `thinking.turn_preamble` | `true` | yes | Ask a `qwen36` model to open its thinking with a short `TURN:` line (the step label). |
1591
1601
  | `reasoning_history` | `turn` | | Which of its reasoning a chat host's model gets back: `drop`, `turn`, `all`; environment only (`SAMAGOTCHI_REASONING_HISTORY`), below a session's own level (`/reasoning-history`), above the `models:`/`hosts:` entries. See "Reasoning history". |
1592
1602
  | `thinking.level` | `default` | `--thinking` | `off`, `low`, `medium`, `high` or `default` for every model; the flag and env outrank the `models:`/`hosts:` entries, the file's value doesn't; a session's own level outranks them all, and `--thinking` sets it on a session run (on `chi web` it sets this). See "Thinking". |
data/docs/hooks.md CHANGED
@@ -124,7 +124,8 @@ links), for a UI that doesn't render that display. The hook never names a UI;
124
124
  each decides by what it renders. The web with markdown on (`web.markdown` and
125
125
  the commonmarker gem) leaves such a line out, live and after a reload; with
126
126
  markdown off it shows, as it does in the REPL, the attached TUI and
127
- `chi -p`, which print the answer as text. The log keeps the line with
127
+ `chi -p`, which print the answer as text (in the web its http(s) URLs are
128
+ links, as in any hook's notice line). The log keeps the line with
128
129
  `fallback_for=display`. `:editor` marks a line that names a file the viewer
129
130
  could open from a ref in its editor: the web leaves it out for a viewer on
130
131
  this machine with `web.editor` set, and every other UI and viewer (a phone,
@@ -526,7 +527,7 @@ stripped of surrounding whitespace, except file text, an edit's
526
527
  | `list_sessions` | — | — | `cwd` |
527
528
  | `send_note` | `text` | — | `session` |
528
529
  | `context_read` | `name` | — | `offset`, `limit` |
529
- | `delegate` | `task` | — | `model`, `session`, `cwd`, `wait`, `timeout` |
530
+ | `delegate` | `task` | — | `model`, `session`, `cwd`, `toolkit`, `wait`, `timeout` |
530
531
  | `delegate_result` | — | — | `session`, `timeout` |
531
532
  | `ask_user_question` | `question` | — | `question`, `options`, `header`, `multi_select`, `allow_freeform` |
532
533
  | `forget_outputs` | `note` | — | `ids`, `note`, `keep`, `restore` |
@@ -660,7 +661,8 @@ keeps the web links. With markdown on, the web leaves the line out when the
660
661
  answer links every URL it names, since it would only repeat them as plain
661
662
  text ([`fallback_for: :display`](#what-a-hook-can-do-the-runtime)); it shows
662
663
  when it names one the answer doesn't link (a ref only in code, two sources on
663
- one ref) and whenever markdown is off. Bundle 0.4.0 needs chi 0.35.0 for that.
664
+ one ref) and whenever markdown is off (its URLs are links in the web). Bundle
665
+ 0.4.0 needs chi 0.35.0 for that.
664
666
 
665
667
  Each ref the answer links is also a [ref on the
666
668
  answer](#refs-on-the-answer-display-only): kind `issue` (or the source's
@@ -82,7 +82,7 @@ index: `message_start` (served model, usage), `content_block_start` (text, think
82
82
  `content_block_delta` (`text_delta`, `thinking_delta`, `signature_delta`, `input_json_delta`), `message_delta`
83
83
  (stop reason, `stop_details`, usage). `AnthropicMessages#chat_streamed` passes every event but `ping` to `on_delta`,
84
84
  `message_start` and `message_delta` with the usage so far attached (`#with_usage`); a block's start or delta counts as shown for the first-token limit
85
- (`.shows?`). A 200 whose body is a JSON error instead of a stream raises that error (`#raise_plain_error`).
85
+ (`.shows?`). A 200 whose body is a JSON error instead of a stream raises that error (`LLM::AdapterCommon#raise_plain_error`, shared with the OpenAI adapter).
86
86
 
87
87
  `#response` is a `ChatResponse`:
88
88
 
@@ -23,9 +23,9 @@ Behavior:
23
23
  - A stop (`task_stop`, the user's Stop of an `execute`, the execute limit in kill mode) TERMs the task's process group, then KILLs it after a grace. The groups its processes started are stopped too: a GNU `timeout N cmd` (without `--foreground`) moves itself and its command to a group of its own. `ProcessGroup.stop` finds them with one `ps -A -o pid=,ppid=,pgid=` (~40 ms on macOS) before the TERM and again before the KILL, walking down from the group's members. A process whose parent already exited (reparented to init, e.g. a setsid'd `cmd &`, or a backgrounded `timeout N cmd &` after its shell exited) is not found and runs on.
24
24
  - Task listing is workspace-scoped (current project only).
25
25
  - `execute` runs its command with stdout and stderr in two logs under `$XDG_STATE_HOME/samagotchi/execute/<id>/` (outside the workspace: `git status` and `rm -rf tmp` never see them), stdin `/dev/null` and the same `exit_code` wrapper as a task. Its own copies of the logs hold a shared `flock` across the spawn: while a fresh exclusive `flock` fails, some process the command started still has them open. When the shell exits, a background child still holding them (`server &`) gets 1.5 s, then its group is stopped; one holding none (`nohup server >log 2>&1 &`) is left alone. A holder that left the group (setsid) is out of the stop's reach: it runs on, the result ends with the "left running" hint, and its dir stays for it to write to (the sweep removes it once no one holds the logs). Otherwise a finished command's dir is removed. Known gap on Linux: a command that writes through `/dev/stdout` or `/dev/stderr` (`echo b >/dev/stdout`) reopens the log file itself with `O_TRUNC`, so the output before it is lost (macOS dups the descriptor; the fix would be a relay pipe). The check needs the state dir on a local file system: over NFS, `flock` is emulated with `fcntl` locks, which children don't inherit.
26
- - The exit code: the command runs after an EXIT trap that writes the shell's status to the `exit_code` file (`TaskRuntime.wrapped_shell_command`, shared with `task_create`), so `exit N`, a `set -e` failure and a trailing `\` or `|` in the command all record what the shell exited with. A command that sets its own EXIT trap replaces it: `execute` still reports the shell's status, and a handed-over one gets its code from chi's waiter while that chi runs; a `task_create` task ends failed with no exit code.
26
+ - The exit code: the command runs after an EXIT trap that writes the shell's status to the `exit_code` file (`TaskRuntime.wrapped_shell_command`, shared with `task_create`), so `exit N`, a `set -e` failure and a trailing `\` or `|` in the command all record what the shell exited with. The trap runs wherever the shell is when it exits (a `task_create` `cwd`, or a `cd` in the command), so `task_create`'s `exit_code_path` is absolute; its `output_path` stays relative (`tmp/tasks/<id>/output.log`, opened by chi in the worker's cwd before the spawn). A command that sets its own EXIT trap replaces it, and a shell killed by a signal skips it (zsh's sh mode, on TERM, INT and KILL): `execute` still reports the shell's status, and a `task_create` task or a handed-over execute gets its code from chi's waiter while the chi that started it runs (`TaskRuntime.record_exit_status`): the exit status, or `128 + the signal` (TERM → 143), with no new `stop_reason`. A record a refresh already finished without a code in between (`failed`, `process_ended_without_exit_code` or the not-chi's-process reason) is corrected, its `finished_at` kept; a stopped task (its stop flag on disk) is never touched. `process_ended_without_exit_code` is left only when the shell wrote nothing and the chi that started it was gone too.
27
27
  - A stop of an `execute` (the user's Stop, the limit in kill mode) is done once the shell is reaped and nothing holds the logs: a child that ignores TERM after its shell died gets the KILL.
28
- - A command still running at `execute.timeout_sec` is handed over as a task (`execute.on_timeout: task`, the default; `kill` stops it instead, and the `!cmd` bang always kills): the result starts `Still running at the 120s limit: moved to a background task, not killed.`, then `task_id: …` and the tails of both logs so far. A timeout during the 1.5 s background grace is a kill (the shell is gone, nothing to hand over). If the record can't be written, the command is killed. `TaskRuntime.adopt` writes a record for the process execute already started, with `handed_over_by: "execute"`. Its logs stay where execute wrote them, so the record's paths are absolute: `output_path` (stdout), `stderr_path` (stderr, a log of its own) and `exit_code_path`. `started_at` (and `created_at`) is the spawn time, not the hand-over's; `pid_started` is read at the hand-over, while execute's detach waiter is still running. From there it is an ordinary task: `task_wait`, `task_get`, `task_list` and `task_stop` treat it as any other. Its dir gets a `task` back-pointer file holding the record's absolute path. Once per process, the first `execute` sweeps `…/execute/`: a dir whose logs no process holds is removed when it has no back-pointer and is a day old (chi died mid-command), when its record is gone, or when it is 14 days old. The records live in each project's `tmp/tasks`, which the state dir can't list: hence the back-pointer.
28
+ - A command still running at `execute.timeout_sec` is handed over as a task (`execute.on_timeout: task`, the default; `kill` stops it instead, and the `!cmd` bang always kills): the result starts `Still running at the 120s limit: moved to a background task, not killed.`, then `task_id: …` and the tails of both logs so far. A timeout during the 1.5 s background grace is a kill (the shell is gone, nothing to hand over). If the record can't be written, the command is killed. `TaskRuntime.adopt` writes a record for the process execute already started, with `handed_over_by: "execute"`. Its logs stay where execute wrote them, so the record's paths are absolute: `output_path` (stdout), `stderr_path` (stderr, a log of its own) and `exit_code_path` (a `task_create` record's `exit_code_path` is absolute too; its `output_path` is relative). `started_at` (and `created_at`) is the spawn time, not the hand-over's; `pid_started` is read at the hand-over, while execute's detach waiter is still running. From there it is an ordinary task: `task_wait`, `task_get`, `task_list` and `task_stop` treat it as any other. Its dir gets a `task` back-pointer file holding the record's absolute path. Once per process, the first `execute` sweeps `…/execute/`: a dir whose logs no process holds is removed when it has no back-pointer and is a day old (chi died mid-command), when its record is gone, or when it is 14 days old. The records live in each project's `tmp/tasks`, which the state dir can't list: hence the back-pointer.
29
29
  - `task_wait` and `task_get` show `stderr_path` when the record has one; `task_wait`'s output tail is followed by a `stderr_tail:` of at most 10 lines when that log isn't empty. A `task_create` task has one log (stdout and stderr together) and no `stderr_path`. A `task_wait` result longer than `max_tool_output_chars` keeps its header and the tail's end, with a gap line naming `output_path`, where the whole log already is (and `stderr_path`, when the record has one) (`ToolOutputCut`'s `:head_tail_ref`; nothing is copied; see [Tool output guardrails](tool-guardrails.md#the-runners-cut-max_tool_output_chars)).
30
30
  - Which tasks are a session's own (a cancelled turn's note, the turn-end note, a worker's idle exit, the web's stop-task button): the ids its `task_create` results and its handed-over executes name (`TaskRuntime.created_ids_in`, `created_id`). An execute's id is read only after its first line (`Execute::HANDED_OVER`, through `TaskRuntime.handed_over_id`; the web has a JS twin in `task_stop.js`), not from a `task_id:` line inside a run, which the command can print. (A command that prints the `---` separator chi joins a turn's results with, then a hand-over line, still fakes one: a known gap.) Either result may start with the `ran as: …` line of a gate-corrected call.
31
31
  - `task_get` returns metadata and `output_path`; use `read` for output contents.
@@ -122,7 +122,7 @@ A worker's TERM, HUP or INT (WorkerSignals, trapped by a spawned worker only):
122
122
  → run_session_loop: exit 0, no discard; input queued behind the stopped turn wakes a new worker
123
123
 
124
124
  The effective model doesn't resolve (a broken host config, HostRegistry#resolve raising):
125
- → the rescued readers give their fallback (Engine#model_setting) and log effective_model_unresolved;
125
+ → the rescued readers give their fallback (SessionModelSettings#model_setting) and log effective_model_unresolved;
126
126
  a setting that fails on a resolved model gives its fallback and logs <setting>_unresolved
127
127
  (sampling, thinking, reasoning_history, steer_cut_after, llm_context, window, price, served,
128
128
  vision_adapter); served_probe_failed when asking the server for its served model fails;
@@ -226,7 +226,7 @@ The differences from the native loop:
226
226
  merges (a person's line, a plugin's steer, a Continue's text) don't move it, as natively (nothing re-sanitizes
227
227
  within a run): the request after a steer extends the one before. A step-limit Continue's Run starts where the run
228
228
  it resumes did: `#complete`'s `reasoning_since:` (the Engine's `resume_from`, checked against the messages' length
229
- in `#resumed_run_start`), past a user entry added after that run's last reply (a `!cmd`'s output). Without
229
+ in `Engine#resumed_run_start`), past a user entry added after that run's last reply (a `!cmd`'s output). Without
230
230
  `since:` (`#reasoning_chars` from outside a Run) the boundary is the last query (a `role: "user"` or `turn_start`
231
231
  entry). `Run#prompt_chars` counts it (`#reasoning_chars`).
232
232
  - On an adapter that sends its blocks back (`#replays_content?`: `AnthropicMessages`), a model turn's saved blocks
@@ -312,10 +312,17 @@ One iteration with tool calls:
312
312
  [Tool output guardrails](tool-guardrails.md#the-runners-cut-max_tool_output_chars). The model (both loops), the
313
313
  event and the after_tool_call hook get that text.
314
314
  - Emits `:tool_call_completed` {iteration, call_count, call_index, tool, output, output_truncated, output_file,
315
- activity, images, diff, waited_ms, view, ref}, then fires `:after_tool_call` {iteration, tool, output, status}.
315
+ activity, images, diff, waited_ms, view, ref, refs}, then fires `:after_tool_call` {iteration, tool, output,
316
+ status, call, elapsed_ms, result}.
316
317
  `output_file` (the saved whole output) is also on the run hash; the session doesn't keep it (the cut text names it).
317
- - The activity's status (`ToolActivity#tool_activity_status`): `error` for an `Error:` result and for an
318
- execute that exited non-zero, except a grep-family search that found nothing (`ToolActivity.no_match?`:
318
+ - The activity's status (`ToolActivity#tool_activity_status`; a call the gate denied is `blocked`, set by
319
+ `ToolRunner#denied`): `stopped` for a call the turn's Stop (or a signal to chi, or the session's stop) left
320
+ unrun (`Tools::Execute::NOT_RUN_ON_STOP`, `NOT_RUN`) or an execute it killed (`STOPPED_BY_USER`, `STOPPED_BY`),
321
+ and for a `task_wait` whose header (before `output_tail:`) says `wait_result: canceled` or `status: stopped`;
322
+ `ok` for an execute handed over as a task (`#handed_over?`: its output tail may hold any `exit: N`); `error` for
323
+ any other `Error:` result, for a `task_wait` whose header says `status: failed`, and for an execute that exited
324
+ non-zero (a numberless `exit:` line, killed by a signal, counts), except a grep-family search that found nothing
325
+ (`ToolActivity.no_match?`:
319
326
  exit 1, no stderr block, the last step is grep/egrep/fgrep/rg/ag/pgrep, no `&&` after a step that can fail
320
327
  silently or sends output to /dev/null, no background `&`, no earlier exit/return or `set -e`; when unsure,
321
328
  not a no-match). That one stays `ok` with `no_match: true` on the activity, which SessionMetrics keeps on the tool
@@ -358,7 +365,7 @@ read live, a model listing can move a bare id to another host, and another proce
358
365
  what a resolve gives, all without a `/model`. Frozen, so any thread may hold one.
359
366
 
360
367
  - The Engine's readers take one (`Engine#effective_model`); those whose fallback always covered the resolve go
361
- through `#model_setting`. A turn takes one in `turn_settings` (before the hooks) and one in `generate` (after
368
+ through `SessionModelSettings#model_setting` (`session_model_settings.rb`: the Engine forwards its readers of the session's settings there). A turn takes one in `turn_settings` (before the hooks) and one in `generate` (after
362
369
  them); `system_prompt`, `steer_cut_after` (at `:turn_started`) and the turn-end warm-up take their own;
363
370
  `stats_snapshot` and `session_state_snapshot` take one each (the latter nil-tolerant).
364
371
  - Never resolved (they hold on a broken resolve): `effective_model_ref`, `bare_model_name`,
@@ -490,17 +497,22 @@ hook registry (`@hooks.fire`), not on the sink or the observers. The other hook
490
497
  ```
491
498
  Engine initialization (in this order, among other things):
492
499
  @turn_state, @steer_cut (SteerCut: its GenerationPhase and WaitingSteer)
493
- @host_registry, @model_choice (ModelChoice: the name and the name as typed); @client = effective_model.client
500
+ @host_registry, @model_choice (ModelChoice: the name and the name as typed); @model_settings (SessionModelSettings: the choice and the session read at use); @client = effective_model.client
494
501
  @given_profile = profile or nil (resolved on first need, so building an Engine makes no network call)
495
- @hook_routes (HookRoutes: notices, hook questions, stop_turn/stop_generation, steer attribution)
496
- @guardrail_wiring, @question_desk
497
- @toolkit_binding (ToolkitBinding: the kits, the plugins' among them, and the turn-start apply)
502
+ MemoryBundle::SystemBundle.ensure! (the shipped system bundle installed; a failure is logged, not raised)
503
+ @event_routes (EventRoutes: where a card or a notice goes: held while the plugins load, an anytime command's, an init task's, a turn event, else announced or held)
504
+ @hook_routes (HookRoutes: notices (through @event_routes), hook questions, stop_turn/stop_generation, steer attribution)
505
+ @guardrail_wiring, @question_desk, @relay_desk (RelayDesk: a delegate's approval relay)
498
506
  @extension_load = ExtensionLoad.new; @hooks = @extension_load.hooks (config.yml's, then the bundles')
499
- @tools = Tools::Builtins.registry; the bundles' plugins load (load_plugins → ExtensionLoad#load_plugins),
507
+ @tools = Tools::Builtins.registry; @command_registry (SessionCommands' built-ins); @plugin_tasks
508
+ @toolkit_binding (ToolkitBinding: the kits, the plugins' among them, and the turn-start apply)
509
+ @thinking_notices (ThinkingNotices); @tool_index = Tools::ToolIndex.new; @peer_sources = PeerSources.new
510
+ the bundles' plugins load (load_plugins → ExtensionLoad#load_plugins),
500
511
  then (plugins or not) @tool_index.declare(@tools): find_tools + call_tool (source "index", not core),
501
512
  declared per toolkit by Registry#declared? (a source registered at load, or a reachable built-in) and
502
513
  sets call_tool's unwrap (Tools::ToolIndex: the chi group from Registry#reachable_names, the sources'
503
514
  groups, search, call checks, the unwrap; a source runs the call with its own plugin Context)
515
+ @reminder_store = ReminderStore.new (the kernel gets it), @reminder_queue, @reminders
504
516
  wire_kernel: @kernel = KernelLoop.new(client:, profile:, hooks:, reminder_store:, tools:)
505
517
  @kernel.guardrail_gate = @guardrail_wiring.gate; @kernel.warmup = PromptWarmup.new
506
518
  @hooks.runtime = @hook_routes.runtime (HookRoutes, built first: what a hook or plugin does reaches the UIs)
@@ -555,7 +567,9 @@ them across turns. `Web::App#messages_for_display` sends `display_refs` re-valid
555
567
  - Order of a fire: bundle hooks (by priority, bundle, hook name), then config hooks, then turn-scoped hooks.
556
568
  - A hook that raises is logged; it never breaks the turn.
557
569
  - A notice from a hook goes to the running turn's sink and the observers. Outside a turn it is announced with
558
- `between_turns: true`; while the plugins load it is held and announced after.
570
+ `between_turns: true`; while the plugins load it is held and announced after. `EventRoutes#route` decides it, the
571
+ same routes as a card's (`Engine#show_card`), with their own marks (`NOTICE_MARKS`, `CARD_MARKS`); a guardrails
572
+ line after load takes only the turn / between-turns routes (`#route_by_turn`).
559
573
  - Plugin init tasks (`chi.init`, slow setup such as an MCP server's first start): a turn waits for the ones that
560
574
  provide tools before it builds its messages, and a Stop or Ctrl-C ends the wait.
561
575
 
@@ -652,7 +666,8 @@ prompt starts with what the server has cached).
652
666
  `[chat, thinking, layers]` (chat/native, the thinking level, the turn's LLM context layers), and is reset
653
667
  only on a model switch, a tools change, a profile change or a toolkit change (`Engine#apply_toolkit!`). The
654
668
  stable part (base prompt, the toolkit's not-loaded line, rg guidance, identity, model notes, preloaded memories,
655
- AGENTS.md, appended files, memory indexes) comes before the volatile tail (model, working directory, session).
669
+ AGENTS.md, appended files, the toolkit's kit prompt, memory indexes) comes before the volatile tail (model,
670
+ working directory, session).
656
671
  prompt-caching.md covers this in detail.
657
672
 
658
673
  ## Key files
@@ -667,6 +682,7 @@ Paths are under `lib/samagotchi/`.
667
682
  | `generation_phase.rb`, `waiting_steer.rb` | Whether a steer may cut the running generation, and the steer that waits to |
668
683
  | `effective_model.rb` | `EffectiveModel` (the effective model's settings, a snapshot per read; `.of_session`), `ModelChoice`, the fallbacks' log lines (`Unresolved`) |
669
684
  | `session_setting.rb` | `SessionSetting`: the per-session settings' descriptors (own value, chips, the web's fallback) |
685
+ | `session_model_settings.rb` | `SessionModelSettings`: the session's settings on its effective model (its own thinking, reasoning_history and llm_context over the model's, the `/model` and `/stats` summaries, what a turn resolves, `#model_setting`'s fallbacks); the Engine forwards its readers there |
670
686
  | `level_setting_command.rb`, `thinking_command.rb`, `reasoning_history_command.rb` | `/thinking` and `/reasoning-history` (`LevelSettingCommand` and its two Specs) |
671
687
  | `thinking_notices.rb` | `ThinkingNotices`: the notices a turn's thinking level leads to |
672
688
  | `steer_cut_after.rb`, `cut_policy.rb` | The model's cut wait (`SteerCutAfter.resolve`); the loops' cut path (`CutPolicy`) |
@@ -676,6 +692,7 @@ Paths are under `lib/samagotchi/`.
676
692
  | `llm/adapters.rb` | `LLM::Adapters::BY_API`: which chat adapter a host's api gets |
677
693
  | `llm/openai_chat.rb` | `LLM::OpenAIChat`, the `/v1/chat/completions` adapter |
678
694
  | `llm/adapter_common.rb` | `LLM::AdapterCommon`: what both chat adapters share (the HTTP layer and its timeouts, the paged model list, the SSE line loop, JSON errors) |
695
+ | `llm/refusal_memo.rb` | `LLM::RefusalMemo`: what a host refused (for one model, or the whole host), remembered for its holder's life (an adapter, or the worker's ChatLoop) |
679
696
  | `llm/anthropic_messages.rb`, `llm/anthropic_wire.rb`, `llm/anthropic_assembly.rb`, `llm/dialect.rb` | The `/v1/messages` adapter, its wire translation, its response, its dialects ([anthropic.md](anthropic.md)) |
680
697
  | `llm/prompt_cache.rb` | Cache breakpoints on the OpenAI path, and the `cache_control` both adapters send |
681
698
  | `llm/turn_settings.rb` | `LLM::TurnSettings`, what a turn runs under (vision, sampling, thinking, reasoning history, …) |
@@ -685,19 +702,27 @@ Paths are under `lib/samagotchi/`.
685
702
  | `llm/native_backend.rb` | Wraps KernelLoop as a backend (`#complete` calls `#run`) |
686
703
  | `llm/model_result.rb` | `LLM::ModelResult`, what a backend returns |
687
704
  | `tool_runner.rb` | Per-call path (gate, start/complete events, output cap, images, diffs) |
705
+ | `tool_output_cut.rb`, `tool_output_files.rb` | `ToolOutputCut`: how a tool's output longer than `max_tool_output_chars` is cut, by the tool's policy (`POLICIES`; `:head` for a tool with none); `ToolOutputFiles`: the whole output of an execute cut that way, saved in the session's `tool_output/` folder ([tool-guardrails.md](tool-guardrails.md)) |
706
+ | `tool_row_fields.rb`, `row_refs.rb` | `ToolRowFields`: a tool row's fields beyond its params (`ACTIVITY_KEYS`, `EVENT_KEYS`), the same live and on a reload; `RowRefs`: a plugin tool's row refs (`refs:`), checked, capped and saved with the result |
688
707
  | `tool_response.rb` | Per-batch dispatch and tool_response entries (joined for native, single for chat) |
689
708
  | `empty_answer_retry.rb` | The empty-answer and cut retry budget |
690
709
  | `prompt_warmup.rb` | The turn-end warm-up |
691
710
  | `tools/registry.rb` | The tools a session offers: schemas, handlers, order |
711
+ | `tools/tool_index.rb` | `Tools::ToolIndex`: `find_tools` and `call_tool`, the tools a session knows but doesn't declare one by one (the sources' groups and the built-ins its toolkit leaves out), `#declare`, `#refresh!`, `#unwrap` |
712
+ | `plugin/tool_spec.rb` | `Plugin::ToolSpec`: a checked tool declaration (`chi.tool`, a tool set, `ToolSpec.build`), what `Registry#register` gets; `#per_call` |
692
713
  | `system_prompt.rb` | System prompt construction (stable part, volatile tail) |
693
714
  | `prompt_files.rb`, `model_notes.rb` | The AGENTS.md chain and appended files; the model notes the prompt loads |
694
715
  | `toolkit.rb`, `toolkit_command.rb` | A session's toolkit (`Toolkit#resolve`, what `Registry` declares); `/toolkit` |
695
716
  | `toolkit_binding.rb` | `ToolkitBinding`: the Engine's toolkit seam (the kits, `#explained`, `ctx.use_toolkit`, the turn-start `#apply!` behind `Engine#apply_toolkit!`) |
696
717
  | `hooks/registry.rb` | Hook registry, fire order, `Hooks::Runtime` |
697
718
  | `hook_routes.rb` | `HookRoutes`: the Engine's routes for a hook's or plugin's notice (`:hook_notice`, `fallback_for:`), question, stop and steer, and the guardrails' after-load line |
719
+ | `event_routes.rb` | `EventRoutes`: where a card (`Engine#show_card`), a notice (`HookRoutes#notify`) and a guardrails line (`#guardrail_notify`, turn or between turns only) go: held while the plugins load, an anytime command's, an init task's, a running turn's event, else announced or held for `holding_announcements` |
720
+ | `engine_peers.rb`, `peer_sources.rb` | `Engine::PeerView` (what the kernel's peer tools see of the Engine) and `Engine::RelayPeer` (a delegate's approval relay); `PeerSources`: the attached context sources a peer bundle declares as its peer's answers (`chi.peer_source`) |
698
721
  | `extension_load.rb` | What an Engine loads at start: config.yml and bundle hooks, plugins, `bundles:` settings; events held while plugins load |
699
722
  | `guardrail_wiring.rb`, `guardrails/gate.rb` | The gate's context and approvals; the verdict per call |
700
723
  | `events.rb` | Event type sets shared by the Worker, the Bridge, the TUI and the web |
701
724
  | `cancellation_controller.rb` | A turn's cancel signal, and a generation's child controller |
702
725
  | `session.rb` | Session model (status, messages, persistence) |
726
+ | `turn_checkpoint.rb` | `TurnCheckpoint`: a worker's running turn saved at the loop's iteration boundaries (`Session#turn_checkpoint`), so a killed worker loses at most `INTERVAL` of it |
727
+ | `context_status.rb` | `ContextStatus`: one turn's context tracking (the status line's value, the `:context_status` event, the model's guidance line) ([context-telemetry.md](context-telemetry.md)) |
703
728
  | `session_manager.rb` | Background session processes (workers) and their IPC |
@@ -51,7 +51,7 @@ Each task × arm × sample is a run in `ROOT/runs/<task>-<arm>-s<n>/`:
51
51
  in `config/` (the model's host copied from `--hosts-config`, default the user's config.yml; `turn.max_iterations:
52
52
  150`; `recap: false`; `llm_context.apply: payoff`, no `stale_edits`). State goes in `state/`, and it runs `chi
53
53
  bundle install loop-guard`. Every chi and rspec command gets `SAMAGOTCHI_ENV=test`, the run's `XDG_STATE_HOME`
54
- and `XDG_CONFIG_HOME`, and no inherited `SAMAGOTCHI_HOSTS_JSON` (the smoke-run isolation).
54
+ and `XDG_CONFIG_HOME` (the smoke-run isolation).
55
55
  2. **run (Driver):** `chi send --new --wait --format json --dir repo --model M --llm-context <arm>
56
56
  --llm-context-budget <N|off>` with the first turn, then each later turn into the same session. The arms are
57
57
  `none`, `stale` and `stale_forget` (`stale,forget`). The tasks use two turns, because forget is offered at turn
@@ -39,6 +39,7 @@ right-hand column happens.
39
39
  │ explicit (preloaded) memories │
40
40
  │ AGENTS.md (root → cwd) │ (picked up by a new
41
41
  │ appended files (append_system_prompt:, --append-system-prompt) │ process or a rebuild)
42
+ │ kit prompt (per toolkit: its kit's prompt: text, only when it has one) │
42
43
  │ project + system memory indexes │
43
44
  │ ◄── remote Claude / Messages: cache_control breakpoint #1 │
44
45
  ├──────────────────────────────────────────────────────────────────────────┤
@@ -71,6 +72,11 @@ right-hand column happens.
71
72
  beside `apply_staged_tools!`, then `reset!`): the chat loop builds `tools:` per request, so a mid-turn change
72
73
  would differ from the turn's prompt. Under `full` nothing is filtered and no line is added: the bytes are what
73
74
  they were before toolkits.
75
+ - A kit's prompt text (`toolkit.kits.<name>.prompt:`, `chi.toolkit … prompt:`) sits after the appended files and
76
+ before the memory indexes (`SystemPrompt#kit_prompt_section`): one prefix per kit, never per-session text, so
77
+ sessions on the kit share it; a memory write (the indexes after it) doesn't move it. It is part of the
78
+ `Toolkit::Resolved` the Registry holds, so a switch that changes only the text is a toolkit change
79
+ (`apply_toolkit!` → `reset!`). `full` and kits without one send the bytes they sent before.
74
80
  - find_tools' description is a function of the toolkit and the source groups **at load**: `ToolIndex#declare`
75
81
  snapshots the MCP groups' lines (name, summary, callable count) and `ToolIndex#refresh!` (from
76
82
  `ToolkitBinding#apply!`) rebuilds only the `chi` line, the built-ins the toolkit reaches. An MCP server that