samagotchi 0.70.0 → 0.72.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 (86) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +83 -1
  3. data/docs/architecture.md +16 -10
  4. data/docs/cli.md +7 -4
  5. data/docs/configuration.md +24 -14
  6. data/docs/hooks.md +1 -1
  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 +4 -2
  14. data/docs/testing.md +12 -3
  15. data/lib/samagotchi/bootstrap/config_writer.rb +2 -3
  16. data/lib/samagotchi/bridge/turn_accumulator.rb +4 -1
  17. data/lib/samagotchi/bridge.rb +5 -2
  18. data/lib/samagotchi/broadcast/scope_card.rb +2 -2
  19. data/lib/samagotchi/bundles/claude-peer/manifest.yml +2 -7
  20. data/lib/samagotchi/bundles/claude-peer/plugin.rb +2 -2
  21. data/lib/samagotchi/bundles/codex-peer/manifest.yml +2 -6
  22. data/lib/samagotchi/bundles/codex-peer/plugin.rb +2 -2
  23. data/lib/samagotchi/bundles/coordinator/manifest.yml +8 -6
  24. data/lib/samagotchi/bundles/coordinator/plugin.rb +10 -0
  25. data/lib/samagotchi/bundles/coordinator/skill_coordinator.md +2 -1
  26. data/lib/samagotchi/bundles/system/config_modification_protocol.md +1 -1
  27. data/lib/samagotchi/bundles/system/manifest.yml +2 -2
  28. data/lib/samagotchi/config.rb +7 -25
  29. data/lib/samagotchi/context_note.rb +2 -6
  30. data/lib/samagotchi/effective_model.rb +1 -1
  31. data/lib/samagotchi/engine.rb +91 -213
  32. data/lib/samagotchi/engine_peers.rb +2 -0
  33. data/lib/samagotchi/event_routes.rb +83 -0
  34. data/lib/samagotchi/hook_routes.rb +16 -37
  35. data/lib/samagotchi/host_config.rb +1 -1
  36. data/lib/samagotchi/host_keys.rb +0 -2
  37. data/lib/samagotchi/paths.rb +13 -0
  38. data/lib/samagotchi/plugin/api.rb +11 -5
  39. data/lib/samagotchi/plugin/peer.rb +5 -8
  40. data/lib/samagotchi/prompt_files.rb +2 -4
  41. data/lib/samagotchi/session.rb +2 -1
  42. data/lib/samagotchi/session_manager.rb +6 -16
  43. data/lib/samagotchi/session_model_settings.rb +227 -0
  44. data/lib/samagotchi/session_setup.rb +2 -1
  45. data/lib/samagotchi/sessions_command.rb +12 -3
  46. data/lib/samagotchi/system_prompt.rb +18 -13
  47. data/lib/samagotchi/terminal_ui/attached_loop.rb +36 -15
  48. data/lib/samagotchi/terminal_ui/formatting.rb +4 -0
  49. data/lib/samagotchi/tool_activity.rb +20 -1
  50. data/lib/samagotchi/tool_declarations.rb +4 -0
  51. data/lib/samagotchi/tool_output_cut.rb +11 -2
  52. data/lib/samagotchi/toolkit.rb +60 -22
  53. data/lib/samagotchi/toolkit_binding.rb +7 -5
  54. data/lib/samagotchi/toolkit_command.rb +28 -5
  55. data/lib/samagotchi/tools/builtins.rb +2 -1
  56. data/lib/samagotchi/tools/delegate.rb +32 -6
  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 +13 -2
  61. data/lib/samagotchi/tools/task_wait.rb +4 -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 +18 -17
  66. data/lib/samagotchi/web/message_parts.rb +1 -1
  67. data/lib/samagotchi/web/public/answer_refs.js +1 -0
  68. data/lib/samagotchi/web/public/app.js +20 -18
  69. data/lib/samagotchi/web/public/bubbles.js +1 -1
  70. data/lib/samagotchi/web/public/cards_place.js +6 -10
  71. data/lib/samagotchi/web/public/chains.js +1 -1
  72. data/lib/samagotchi/web/public/chip_popover.js +3 -1
  73. data/lib/samagotchi/web/public/composer.js +13 -6
  74. data/lib/samagotchi/web/public/context_chips.js +25 -12
  75. data/lib/samagotchi/web/public/data.js +5 -1
  76. data/lib/samagotchi/web/public/format.js +5 -0
  77. data/lib/samagotchi/web/public/index.html +1 -0
  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_view.js +8 -2
  81. data/lib/samagotchi/web/public/stream_handlers.js +6 -3
  82. data/lib/samagotchi/web/public/timing.js +1 -1
  83. data/lib/samagotchi/web/public/toolkit_chip.js +3 -1
  84. data/lib/samagotchi/web/public/turn_html.js +55 -3
  85. data/lib/samagotchi/web/session_hub.rb +8 -0
  86. metadata +3 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d5031a069a844109b4d11d2aa6c43e8471da137decd1231a45a6e5ac6a57176c
4
- data.tar.gz: 9f4c0d1696b567d42bef5594288a3068781c0e38b52cee478cb2d551fcfe2d6e
3
+ metadata.gz: 6421f740a2683f0dcf3d24b92fba7bba62bed9edb92ddfd532ce69c74e14e0e8
4
+ data.tar.gz: 23841684a58d0a3efedddcd0fb0caef4d1403c61a88268ad79f6b28a9d8f00ab
5
5
  SHA512:
6
- metadata.gz: 3f4718ac2eb9e287fd6a023b619fc6b3ad78f13e55b3edc4c5715ba164e9c5aea0afe08154fbda59d07f83a6120aa96cd77d9e2d36b10738a27d8246c33e3fc7
7
- data.tar.gz: f33e3a9813f3fdc914489d69261c975b2c2699a642896d757374dac2ff281e9bbce776cacd5f4b815bd77a6fa1f9bc5c4400413aaeb90cf95dd04be2f50b49b3
6
+ metadata.gz: 91ccf28b2be3f0bfff968c5d0448b0c1748ef90c505f1195ec655ae93784e48975409e54a56e18d7f78605812770d4ec9eeae336208da1a95ddbab2aa057f3da
7
+ data.tar.gz: c5f8e40dfea68d7d6d706970106cbbdc5caf8e9571b92d771dc2928d6a3358704aafcd6b3a047f96ccfbe285b28ee2bc98de81505f10a342cc8b6efc228b0b9c
data/CHANGELOG.md CHANGED
@@ -8,6 +8,86 @@ and commands may change between minor versions. How releases are made:
8
8
 
9
9
  ## [Unreleased]
10
10
 
11
+ ## [0.72.0] - 2026-10-16
12
+
13
+ ### Added
14
+
15
+ - A toolkit can carry a prompt text (`toolkit.kits.<name>.prompt:`, `chi.toolkit … prompt:`): the sessions on it
16
+ get it in their system prompt, others don't; `/toolkit`, `/stats` and the web's tools chip show it. `delegate`
17
+ can start a child on a named toolkit (`toolkit:`). The coordinator bundle (0.5.0) declares a `coding` toolkit
18
+ (`child`'s tools plus "Be concise in your responses" and "Show file paths clearly when working with files"),
19
+ and its skill starts build and fix children on it.
20
+
21
+ ### Removed
22
+
23
+ - `SAMAGOTCHI_HOSTS_JSON`: chi no longer reads it (workers stopped inheriting it in 0.64.0). Hosts come only from
24
+ config.yml's `hosts:`; one still exported in your shell is ignored.
25
+
26
+ ### Fixed
27
+
28
+ - Attached TUI: a turn of images alone sent from the web names each image once while you watch it start (or join
29
+ it running), not twice.
30
+ - Attached TUI: joining a session shows a call the guardrails denied as `blocked`, as it showed live and as the
31
+ web shows it, instead of `error`.
32
+ - Web: an answer's file ref outside the session's folder is dropped from the end-of-turn re-read of a running
33
+ session too, as a reload drops it, instead of showing until the reload.
34
+ - Plugins: a turn no longer fails with "can't add a new key into hash during iteration" when a plugin's
35
+ `replace_tools` registers a tool while a page reads the session's settings (no MCP server loaded).
36
+ - `call_tool` given the bare name of a tool the session declares (`read`) answers at once that it's declared and
37
+ should be called directly, instead of first waiting for every MCP server still starting.
38
+ - Web: a `/model`, `!command` or chip change still in flight when you open another session no longer acts on
39
+ that one: it no longer marks it as held by a terminal, puts the failure in its history or follows its worker,
40
+ and a card's action button no longer follows it either; an after-turn hook's answer re-read no longer lands in it.
41
+ - Web: a stop the server refuses (the stop button in the session bar) now says why in a toast instead of nothing.
42
+ - Web: a failure the server explains (a refused `/model`, send, archive, attach…) shows its words alone, without an
43
+ HTTP status after them (`a terminal chi holds it`, not `… (409)`); the status shows only when the server gave no words.
44
+ - Web: opening another session right as a finished turn slides into the history no longer shows that turn in the
45
+ new session's history, and its "Loading…" line stays until the session is drawn.
46
+ - Web: opening a chip's popover (llm ctx, thinking, reasoning history, attached context) closes the one already
47
+ open, instead of leaving both open.
48
+ - The empty-answer retry note asks the model to continue the task (its next tool call) or answer if it is done,
49
+ instead of "answer the user's last message now, briefly", which made a mid-task model stop before its last steps.
50
+
51
+ 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
52
+ restart `chi web`.
53
+
54
+ ## [0.71.0] - 2026-10-15
55
+
56
+ ### Changed
57
+
58
+ - Web: an `execute` row, and a `task_wait` row showing its log's last lines, now shows the end of its output (a
59
+ test run's summary, a failing exit line) instead of the start, without the success `exit: 0` line or a lone
60
+ `stdout:` label; the whole output stays on hover.
61
+ - `task_wait` on a task that finished now returns the last lines of its log too (`output_tail:`, `tail_lines` of
62
+ them, 10 by default), as a timed-out wait does: a test run's summary or an error is there without reading the
63
+ log; its web row shows them.
64
+ - Web: a page opened mid-turn (or reconnecting) gets each tool's output cut as a reload does (2000 characters),
65
+ so a joined row matches the reloaded one and the snapshot is smaller.
66
+ - Web: a tool row whose call the guardrails corrected shows its output without the `ran as: …` line inline (it
67
+ stays on hover).
68
+
69
+ ### Fixed
70
+
71
+ - A command a signal ended (a segfault, an OOM kill: its result ends `exit: ` with no number) is read as finished:
72
+ a long output's cut keeps its exit line apart and tells the model not to rerun it (the saved file is the whole
73
+ output), and its web row shows the output's end as other failed commands' do.
74
+ - `chi sessions`: a `--days`, `--keep` or `--limit` that isn't a whole number (`--days=abc`) is now a usage error
75
+ (exit 2) instead of being read as 0, which turned `prune`'s age limit off; the usage now lists `clean --all`.
76
+ - `full` with `--no-tool call_tool` (or `find_tools`) and no MCP server no longer adds a "Not loaded in this
77
+ session" line to the system prompt nor a web toolkit chip: there call_tool reaches nothing, so nothing is left
78
+ out.
79
+ - A `toolkit.kits` entry with a key other than `base:`, `add:` and `remove:` (a typo, a `tools:` list) now warns
80
+ (once, as the toolkit's other config warnings) instead of being ignored silently; the kit still loads.
81
+ - Attached terminal: a session's reloaded tool rows read each result as the live row did: a command handed over
82
+ as a task is no longer shown failed because its output so far had `exit: 1`, and a failed `execute`
83
+ (`Error: …`) is no longer shown done.
84
+ - A `task_wait` row is marked stopped only by the wait's own status, not by a `status: stopped` line the task's
85
+ log printed.
86
+ - A `task_wait` whose task failed (exited non-zero, or without an exit code) is now marked failed in the web and
87
+ terminal rows and counted in the turn's "(N failed)", as a failing `execute` is.
88
+
89
+ Update with `chi update` (the system bundle 0.71.0), then restart `chi web`.
90
+
11
91
  ## [0.70.0] - 2026-10-15
12
92
 
13
93
  ### Added
@@ -3029,7 +3109,9 @@ and long-lived sessions.
3029
3109
  - A macOS desktop helper (`chi desktop install`): a "Send to chi" Service and a
3030
3110
  hotkey panel that send selected text or the clipboard to your sessions.
3031
3111
 
3032
- [Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.70.0...HEAD
3112
+ [Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.72.0...HEAD
3113
+ [0.72.0]: https://github.com/dm1try/samagotchi/compare/v0.71.0...v0.72.0
3114
+ [0.71.0]: https://github.com/dm1try/samagotchi/compare/v0.70.0...v0.71.0
3033
3115
  [0.70.0]: https://github.com/dm1try/samagotchi/compare/v0.69.0...v0.70.0
3034
3116
  [0.69.0]: https://github.com/dm1try/samagotchi/compare/v0.68.0...v0.69.0
3035
3117
  [0.68.0]: https://github.com/dm1try/samagotchi/compare/v0.67.0...v0.68.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`. |
@@ -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
@@ -337,9 +339,11 @@ The page's frequent reads of `GET /api/sessions/:id` stay light too:
337
339
  alone (no session file parsed, no `analytics.json`); `timing` carries the worker's recent records
338
340
  and `turn_count` (finished turns). The page merges the records by id (`timing.js mergeTiming`) and
339
341
  finishes the ended turn's own line by its `turn_id` (sent as `&turn_id=`, which also picks that
340
- turn's answer). A plain `?tail=1` (a tab opened before this) gets the whole timing.
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).
@@ -349,7 +353,7 @@ The page's frequent reads of `GET /api/sessions/:id` stay light too:
349
353
 
350
354
  A worker without `/tail` (an older chi; `404 not_found`) is read through `/snapshot` in the same
351
355
  shape; any other failure (no worker, a 500, a timeout) reads the disk, trimmed to the newest
352
- turn's records with `recent=1`.
356
+ turn's records.
353
357
 
354
358
  Every route goes through `Bridge#dispatch`: an id other than the bridge's own session is
355
359
  `404 unknown_session`, and a handler that raises answers `500 bridge_error` (logged as
@@ -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
@@ -15,7 +15,7 @@
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
16
  - `chi web --web-markdown` — opt in to sanitized Markdown rendering for completed assistant messages
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
- - `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`) exits 2; an unknown or refused session exits 1
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))
20
20
  - `chi context <add|push|ls|show|refresh|rm|mute|unmute> …` — attach live external text to sessions (a command chi runs every so often, or text pushed in); the model gets a short note when it changes and reads it with `context_read`; `chi context add <URL>` attaches a GitHub PR with the github-pr bundle (see [Attached context](context.md))
21
21
  - `chi send [-m TEXT] [--image PATH]... [--cut | --queue] (ID|PREFIX)...` — send a message to sessions as if typed there: a turn starts (or a running one takes it at its next step; `--cut` asks it to cut long thinking for it; `--queue` runs it as a turn of its own after the running one, and `--queue --wait` waits for that turn's own answer); piped stdin goes above `-m` as quoted context, and `--image` attaches images (see [Sessions: Sending a message](sessions.md#sending-a-message)); `--new` starts a session with it instead (`--new --continues ID|last:ID` the next link of that session's chain, see [Session chains](sessions.md#session-chains)), and `--wait` prints the answer (`--wait ID` with no message waits for the next reply without sending; `--format json` prints one JSON object instead; exit 3 a question waits, its options on stderr; exit 4 `--timeout` passed with the turn still running; see [Starting a session](sessions.md#starting-a-session))
@@ -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
@@ -766,7 +767,9 @@ tool rows), the earlier ones stack above it collapsed to one line each
766
767
  (their narration's first line, else their first call's title such as
767
768
  `edit lib/a.rb`, and a call count), expandable for inspection. A tool row
768
769
  says what the call did: a file's path relative to the session's folder, a
769
- command without its leading `cd … &&` (the full parameters on hover). The live thinking is one line: the
770
+ command without its leading `cd … &&` (the full parameters on hover). Under it a done row shows its
771
+ output's first 300 characters; an `execute` and a `task_wait` with its log's tail show the last ones (a test
772
+ run's summary, a failing exit line), without the success `exit: 0` line; a gate-corrected call's `ran as:` line only on hover; all of it on hover. The live thinking is one line: the
770
773
  newest complete sentence, changing at most once per 1.5 s. Click it for the
771
774
  full text; a peek is per step (the next step's thinking starts closed
772
775
  again). When a step ends its thinking closes to a plain `thinking` line
@@ -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:`; it can't reuse a
1344
- 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
@@ -1355,7 +1365,7 @@ toolkit:
1355
1365
  call_tool can't reach it (`child` removes `memory_write`, `delegate` and `delegate_result`, so a child on it
1356
1366
  starts no children). To keep a built-in away from the model, name it under `remove:`. `find_tools` and `call_tool`
1357
1367
  are declared when a toolkit has a reachable built-in (or an MCP server is configured); `full` with no MCP server
1358
- declares neither.
1368
+ declares neither, and removing them there (`--no-tool call_tool`) leaves nothing out: no prompt line, no chip.
1359
1369
  - **The prompt line.** One line after the base prompt names them: `Not declared in this session (toolkit lean): ….
1360
1370
  Search them with find_tools; call one with call_tool.`, then `Removed (toolkit child): memory_write, …. Only the
1361
1371
  user can load one (/toolkit add <name>).` With `find_tools` removed, the first sentence ends `Call one with
@@ -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
@@ -526,7 +526,7 @@ stripped of surrounding whitespace, except file text, an edit's
526
526
  | `list_sessions` | — | — | `cwd` |
527
527
  | `send_note` | `text` | — | `session` |
528
528
  | `context_read` | `name` | — | `offset`, `limit` |
529
- | `delegate` | `task` | — | `model`, `session`, `cwd`, `wait`, `timeout` |
529
+ | `delegate` | `task` | — | `model`, `session`, `cwd`, `toolkit`, `wait`, `timeout` |
530
530
  | `delegate_result` | — | — | `session`, `timeout` |
531
531
  | `ask_user_question` | `question` | — | `question`, `options`, `header`, `multi_select`, `allow_freeform` |
532
532
  | `forget_outputs` | `note` | — | `ids`, `note`, `keep`, `restore` |
@@ -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
 
@@ -11,7 +11,7 @@ Samagotchi supports long-running commands in the background through five task to
11
11
  Recommended workflow:
12
12
 
13
13
  1. Create a task with `task_create`.
14
- 2. Use `task_wait` once. On timeout it returns the last 10 log lines, avoiding a separate read just to see progress.
14
+ 2. Use `task_wait` once. Every wait returns the log's last `tail_lines` lines (10 by default): a finished task's (its summary, its error), a timed-out or pattern-matched one's progress. So there is no separate read just to see them.
15
15
  3. For commands with a reliable completion marker, pass `done_pattern` to return when the recent log tail matches it.
16
16
  4. Use `task_get` or `task_list` for nonblocking status checks, and `task_stop` if needed.
17
17
 
@@ -26,7 +26,7 @@ Behavior:
26
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.
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
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.
29
- - `task_wait` and `task_get` show `stderr_path` when the record has one; where `task_wait` returns an output tail, a `stderr_tail:` of at most 10 lines follows it 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)).
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.
32
32
  - `task_wait` accepts `timeout`, `tail_lines` (maximum 100), and `done_pattern` (a regular expression string).
@@ -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