samagotchi 0.15.0 → 0.17.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 (83) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +135 -1
  3. data/README.md +2 -0
  4. data/docs/architecture.md +23 -0
  5. data/docs/cli.md +7 -4
  6. data/docs/configuration.md +7 -5
  7. data/docs/guardrails.md +16 -2
  8. data/docs/internals/background-tasks.md +1 -0
  9. data/docs/memory.md +39 -0
  10. data/docs/plugins.md +2 -0
  11. data/docs/sessions.md +10 -4
  12. data/docs/sub-agent.md +16 -3
  13. data/docs/testing.md +47 -0
  14. data/lib/samagotchi/answer_display.rb +6 -2
  15. data/lib/samagotchi/bridge/card_store.rb +16 -1
  16. data/lib/samagotchi/bridge/turn_accumulator.rb +26 -5
  17. data/lib/samagotchi/bridge.rb +122 -7
  18. data/lib/samagotchi/bridge_client.rb +33 -6
  19. data/lib/samagotchi/bundle_command.rb +13 -2
  20. data/lib/samagotchi/bundles/system/manifest.yml +3 -3
  21. data/lib/samagotchi/bundles/system/memory_guide.md +5 -5
  22. data/lib/samagotchi/bundles/system/self_map.md +3 -1
  23. data/lib/samagotchi/empty_answer_retry.rb +2 -1
  24. data/lib/samagotchi/engine.rb +82 -18
  25. data/lib/samagotchi/kernel_loop.rb +16 -5
  26. data/lib/samagotchi/llm/chat_loop.rb +14 -8
  27. data/lib/samagotchi/llm/model_result.rb +12 -7
  28. data/lib/samagotchi/log_subscriber.rb +1 -0
  29. data/lib/samagotchi/memory_bundle/builder.rb +22 -0
  30. data/lib/samagotchi/memory_bundle/index_updater.rb +17 -6
  31. data/lib/samagotchi/memory_bundle/installer.rb +60 -21
  32. data/lib/samagotchi/memory_bundle/profile.rb +9 -4
  33. data/lib/samagotchi/memory_bundle/provenance.rb +14 -0
  34. data/lib/samagotchi/memory_bundle/shipped_update.rb +5 -3
  35. data/lib/samagotchi/memory_bundle/system_bundle.rb +5 -3
  36. data/lib/samagotchi/memory_bundle/trash.rb +57 -0
  37. data/lib/samagotchi/memory_bundle/uninstaller.rb +34 -5
  38. data/lib/samagotchi/parent_report.rb +8 -0
  39. data/lib/samagotchi/plugin/context.rb +14 -2
  40. data/lib/samagotchi/question_desk.rb +97 -18
  41. data/lib/samagotchi/relay.rb +88 -0
  42. data/lib/samagotchi/relay_desk.rb +73 -0
  43. data/lib/samagotchi/relay_verifier.rb +113 -0
  44. data/lib/samagotchi/relay_watcher.rb +46 -0
  45. data/lib/samagotchi/reply_wait.rb +10 -1
  46. data/lib/samagotchi/self_report.rb +49 -3
  47. data/lib/samagotchi/send_command.rb +45 -6
  48. data/lib/samagotchi/session.rb +15 -2
  49. data/lib/samagotchi/session_manager.rb +3 -2
  50. data/lib/samagotchi/session_metrics.rb +154 -48
  51. data/lib/samagotchi/sessions_command.rb +10 -4
  52. data/lib/samagotchi/system_prompt.rb +45 -4
  53. data/lib/samagotchi/terminal_ui/attach_launcher.rb +11 -2
  54. data/lib/samagotchi/terminal_ui/attached_loop.rb +85 -21
  55. data/lib/samagotchi/terminal_ui/event_renderer.rb +8 -2
  56. data/lib/samagotchi/terminal_ui/formatting.rb +7 -0
  57. data/lib/samagotchi/terminal_ui/question_prompt.rb +48 -2
  58. data/lib/samagotchi/terminal_ui/question_slot.rb +5 -1
  59. data/lib/samagotchi/terminal_ui.rb +24 -0
  60. data/lib/samagotchi/tool_declarations.rb +2 -2
  61. data/lib/samagotchi/tools/builtins.rb +11 -3
  62. data/lib/samagotchi/tools/delegate_relay.rb +220 -0
  63. data/lib/samagotchi/tools/delegate_wait.rb +72 -6
  64. data/lib/samagotchi/tools/peers.rb +7 -4
  65. data/lib/samagotchi/tools/task_runtime.rb +1 -1
  66. data/lib/samagotchi/turn_note.rb +30 -2
  67. data/lib/samagotchi/version.rb +1 -1
  68. data/lib/samagotchi/web/app.rb +22 -1
  69. data/lib/samagotchi/web/public/app.js +60 -33
  70. data/lib/samagotchi/web/public/index.html +7 -0
  71. data/lib/samagotchi/web/public/notify.js +22 -5
  72. data/lib/samagotchi/web/public/question_card.js +45 -3
  73. data/lib/samagotchi/web/public/question_cards.js +49 -1
  74. data/lib/samagotchi/web/public/sessions_list.js +8 -1
  75. data/lib/samagotchi/web/public/stage_model.js +8 -3
  76. data/lib/samagotchi/web/public/stage_view.js +20 -3
  77. data/lib/samagotchi/web/public/strip.js +11 -0
  78. data/lib/samagotchi/web/public/timing.js +37 -15
  79. data/lib/samagotchi/web/public/turn_events.js +22 -3
  80. data/lib/samagotchi/web/public/turn_view.js +34 -12
  81. data/lib/samagotchi/worker.rb +28 -2
  82. data/lib/samagotchi/worker_sidecar.rb +4 -1
  83. metadata +8 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7cb85a0b5509a00cd396dd2db8c269c7959ccbbdd004649d626476067b652bbd
4
- data.tar.gz: b2a46cc3632563de6f1d7c0f979b9ac54bccdfa765a643c576a5c24d7b492bf3
3
+ metadata.gz: '056529144efb1765ab53c0ce9e88a9ea979ecd8c202d3e41a12a794827fee260'
4
+ data.tar.gz: 054d2a858289e543a8085f0ce774935ea164a41e98467e8fb099452a81eb4729
5
5
  SHA512:
6
- metadata.gz: '07187883e2ffb8d0a0cbeaafd870551df02e166d7301e3696223efeba5d7b4eaab002a4ccc5180dd88c8d20c8cbcec302b2c9932ea99bd0b872b34051375d651'
7
- data.tar.gz: 759493ed3abbd40da61128bca6cbb2805937697af546739b52e1e365b01867d1c648484aefdd7a4b1512acc50515d2c588f6fc48342c36f238b2c34713827750
6
+ metadata.gz: 0a88b140031661b2dd2903df3ca74e50587971cba38029e65f8dcf8ec5e1e2a098fbfa4247c2a0954b5f249ca70d85a30600236f728e6131056f4defb6058a5b
7
+ data.tar.gz: 5a2d8a1fac55ad2d4a69ec0002a99a856e82de8b0a91a4c09e4a05c04a10a27d634febf0d77d5dfdfb94a7fd5b3c8f88397ed0affb13384e4394a33c7d21424e
data/CHANGELOG.md CHANGED
@@ -8,6 +8,138 @@ and commands may change between minor versions. How releases are made:
8
8
 
9
9
  ## [Unreleased]
10
10
 
11
+ ## [0.17.0] - 2026-10-03
12
+
13
+ ### Added
14
+
15
+ - The system prompt names the model the session runs on: `Model: this session runs on box:gemma-small (host box,
16
+ box.test:8080; model key gemma-small).`, plus a line to answer "which model are you" from it, not from training
17
+ (a fine-tune often knows only its base model's name), and that model-only guidance goes in
18
+ `memory_write current_model_only: true` overlays. A local llama.cpp host whose `/props` names another model adds
19
+ `; the server says it serves <name>`. The line changes only with `/model`.
20
+ - `execute` and `task_create` export `SAMAGOTCHI_SESSION_MODEL`, the session's model ref (it follows `/model`; the
21
+ model's `env` can't set it).
22
+ - Plugins: `ctx.model` (the session's model ref, right after `/model` too) and `ctx.model_key` (its memory overlay
23
+ key).
24
+ - `chi self` has a `model key` row: the memory overlay key of the model it reports.
25
+
26
+ ### Changed
27
+
28
+ - `chi self` run by a session's `execute` reports **that session's** model, and its host, loop, profile, thinking and
29
+ served model: `model splash:… (this session abcd1234; default main:…)`. Before, it showed the config default
30
+ (or the `--model` the worker started with), wrong after `/model`, for web-picker and `chi send --new --model`
31
+ sessions and delegate children. Elsewhere the row reads `main:… (default)`. `chi self --model` is still the default.
32
+ - The identity memory and the preloaded memories (`--memory`, config `memories:`) get their model overlays
33
+ (`<name>.<key>.md`) in the system prompt, as `memory_read` gives them.
34
+ - A session's live state (what the web and an attached terminal read from its worker) no longer carries every turn's
35
+ and tool call's timing record, only the newest turn's: reads stay the same size however long the session runs. The
36
+ full history stays in the session's `analytics.json`.
37
+ - A memory a bundle installed says so in its `index.md` line, and so in the prompt's memory index:
38
+ `- **memory_guide** · system · 2026-10-03 · 5120 · from samagotchi-system — …`. `memory_write` and `write`/`edit`
39
+ keep the tag; a same-name memory the install skipped gets none. Bundles installed earlier get it on their next
40
+ install or upgrade (the system bundle: with the next chi version).
41
+ - `chi bundle uninstall` (and an upgrade dropping a file the new version no longer ships) moves the bundle's memory
42
+ files to `<memories>/.bundles/.trash/<bundle>-<time>/` instead of deleting them, and says where:
43
+ `Moved to the trash: notes.md (…/.trash/notes-bundle-20261003-120000)`. Nothing empties the trash; delete it by hand.
44
+ - `chi bundle build` leaves out the memories an installed bundle owns (the system bundle's `identity.md`, a shipped
45
+ bundle's memory), one line each: `Left out identity.md: installed by bundle samagotchi-system (name it to include
46
+ it)`; naming a file in `FILES...` includes it. It now prints the builder's warnings.
47
+ - An `index.md` line a bundle install, upgrade or uninstall couldn't write or remove is now a warning
48
+ (`index.md: line for notes not updated (…)`) instead of passing silently; `chi bundle uninstall` of a profile
49
+ prints its bundles' warnings too.
50
+ - Web: a composer line is a command only when it names one of the session's commands (or is `!…`); an unknown
51
+ `/word` (a path like `/usr/bin/env …`, a typo) goes to the model as a prompt, as in a terminal, instead of being
52
+ refused with `not a session command`.
53
+ - Web: a window under 700 px tall hides the latest-sessions strip by itself (the `▾ N sessions` pill shows it again
54
+ until the window is tall), so a question or approval card in the turn's stage has room for its choices and
55
+ buttons. Taller windows and phones are unchanged.
56
+
57
+ ### Fixed
58
+
59
+ - `chi self`'s `served model` row for a chat host (`api: openai`), or a server without `/props`, says
60
+ `reported per turn (the server has no /props)`; it said `unknown (no answer from the server's /props)`, which a
61
+ model took to mean the server never said what it served.
62
+ - `chi bundle uninstall` no longer deletes a memory the user had before the install. A bundle now records (and
63
+ so upgrades and removes) only the files it wrote; a same-name file that was already there is reported
64
+ `Skipped` and stays the user's, on install, re-install and upgrade. Before, it was recorded with the user's
65
+ content as its base, so uninstall removed it without `--force`: a third-party bundle shipping `notes.md`, or
66
+ `chi bundle build` then installing the result on the same machine. An upgrade no longer reports such a file as
67
+ a conflict (or `chi update --dry-run` as kept). Bundles installed before this fix may still list such a file;
68
+ uninstall now moves it to the trash (below) rather than deleting it.
69
+ - A memory file two installed bundles list (one an older install adopted, like `identity.md` under both
70
+ `samagotchi-system` and a `chi bundle build` of your memories) stays when one of them is uninstalled, or drops it
71
+ in an upgrade: `Kept identity.md: bundle samagotchi-system has it too`. It goes with the last one.
72
+ - Web: after a reload, a turn with an empty retry mid-turn shows each tool row's own duration; the rows took their
73
+ records by iteration, so every call after the retry showed the wrong one, or none.
74
+ - Attached TUI: `/detach` typed at a question's `?` prompt detaches (the question stays open), as Ctrl-D does; it
75
+ was read as an answer and refused as an unknown option.
76
+ - Attached TUI: joining a turn while a tool call runs (a long `delegate`, a slow command), its row's duration counts
77
+ from the call's start, not from the join.
78
+ - Web: in a short window a question or approval card that opens in the turn's stage is scrolled into view (its
79
+ choices and buttons); it sat below the prompt and headline, out of sight.
80
+ - A session command sent as a message runs as the command, as typed in a terminal: `chi send -m "/model x" ID`,
81
+ `chi send --new -m "/model x"`, `chi -p "/model x"` (attached and `--non-interactive`) and the web start page's
82
+ first message went to the model as a prompt. `chi send` says `sent as a session command`; with `--wait` there is
83
+ no reply to wait for (exit 0, `--format json` status `command`). An unknown `/word` still goes to the model.
84
+ - macOS 26: a dead worker's leftover `bridge.json` no longer counts as a live worker. Ruby's `Socket.tcp` with a
85
+ connect timeout returns the socket of a refused connect there, so the check said live: clients tried its port
86
+ first, `chi update` listed the dead worker as running, and an approval relay kept watching a parent that was gone.
87
+ - REPL with piped input: when the input ends at a question or approval, no bare `? ` line is left above its
88
+ `(denied)` / `(cancelled)` summary.
89
+ - Web: an approval from a delegate its parent isn't waiting on directly rings once (the parent's relayed card), not
90
+ twice: a child's approval now waits 3.5 s (was 1.5 s) for the relay before it notifies on its own. Only delegated
91
+ children's approvals wait; one nobody relays rings that much later.
92
+
93
+ Update with `chi update` (no bundle changes; the system bundle upgrades itself with chi).
94
+
95
+ ## [0.16.0] - 2026-10-03
96
+
97
+ ### Added
98
+
99
+ - A delegated child's guardrail approvals go to the parent session's user (the approval relay): while the parent
100
+ waits (`delegate`, `delegate_result`), the child's approval opens as the parent's own approval, in the parent's web
101
+ page and attached terminal, with the same facts and diff and a line naming the delegate (linked) and its task. The
102
+ user answers once there; the parent's model sees only an outcome line in the tool's result (`approval relayed to
103
+ your user: execute: git push → allowed once`). The child verifies each answer with the parent's own worker before
104
+ taking it, and holds a parent agent's answer to its own `guardrails.parent_approvals`. The child's card stays
105
+ answerable (first answer wins) and says it waits in the parent too; `chi sessions list` shows it as
106
+ `waiting (in parent ab12)` (json `relayed_to`), and the web's bell rings once, for the parent's card. The other
107
+ running children's approvals are relayed during a wait too, one card at a time (`+1 more delegate waiting`), and a
108
+ grandchild's hop by hop. The time a card is open doesn't count against the wait's timeout.
109
+ - For other parents (`chi send --wait`, `chi answer`), an approval's question block also offers to leave it open for
110
+ the user: `chi send --wait --format json ID` waits until they answer it in chi.
111
+
112
+ ### Changed
113
+
114
+ - A turn that ends with no answer (the empty-answer retries used up) saves the same shape from both loops: the hidden
115
+ turn note alone. The native loop no longer saves the empty thinking-only reply and a literal `[No response]` (the
116
+ next prompt had two assistant turns in a row), and the chat loop's `(the model returned an empty answer)` is no
117
+ longer the answer text (`chi send --wait` saw it as an answer). The note carries a marker the UIs draw from, never
118
+ sent to the model. Older sessions keep their `[No response]`.
119
+ - A parent agent's answer (`chi answer`, a marked `chi --attach`) no longer brings an archived session back to the
120
+ lists; only a human's does.
121
+
122
+ ### Fixed
123
+
124
+ - A turn with no answer shows one muted line in the attached TUI and the REPL,
125
+ `no answer: the model returned nothing (after 1 retry)`, like the retry row, and so does `chi --resume` when it was
126
+ the last turn (it showed `[No response]` or an earlier step's text as the answer, or nothing).
127
+ - The web shows a turn with no answer as the same muted notice where the answer would go, before the timing line,
128
+ live and after a reload; the reload keeps the turn's steps (both thinkings, the retry row inside them). Live it
129
+ showed nothing (native hosts) or an answer bubble below the timing line (`api: openai`); a reload showed
130
+ `[No response]` as the answer with one step fewer, or lost the steps and turned the retry row into a bubble of
131
+ its own.
132
+ - Attached `chi -p` from a script exits 1 with `chi: the model gave an empty answer` on stderr when its turn ends with
133
+ no answer, as `chi -p --non-interactive` does; it exited 0 with a blank line (native hosts) or the placeholder text
134
+ (`api: openai`) as the answer. `chi send --wait` reports such a turn as `no_answer` from both kinds of host (an
135
+ `api: openai` host's placeholder was reported as the answer).
136
+ - `chi --resume` and `--attach` give the last exchange's replayed tool rows their durations, as the live rows show
137
+ them (`ok (1.3s)`), from the session's saved tool records; a session from before turn ids keeps them bare.
138
+ - The web stage's status row reads `∅ no answer` (muted, with a warn edge) for a turn that ended with no answer; it
139
+ said `✓ answered`.
140
+
141
+ Update with `chi update` (no bundle changes).
142
+
11
143
  ## [0.15.0] - 2026-10-02
12
144
 
13
145
  ### Added
@@ -808,7 +940,9 @@ and long-lived sessions.
808
940
  - A macOS desktop helper (`chi desktop install`): a "Send to chi" Service and a
809
941
  hotkey panel that send selected text or the clipboard to your sessions.
810
942
 
811
- [Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.15.0...HEAD
943
+ [Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.17.0...HEAD
944
+ [0.17.0]: https://github.com/dm1try/samagotchi/compare/v0.16.0...v0.17.0
945
+ [0.16.0]: https://github.com/dm1try/samagotchi/compare/v0.15.0...v0.16.0
812
946
  [0.15.0]: https://github.com/dm1try/samagotchi/compare/v0.14.0...v0.15.0
813
947
  [0.14.0]: https://github.com/dm1try/samagotchi/compare/v0.13.0...v0.14.0
814
948
  [0.13.0]: https://github.com/dm1try/samagotchi/compare/v0.12.0...v0.13.0
data/README.md CHANGED
@@ -201,6 +201,8 @@ npm test # web frontend specs
201
201
  npm run e2e # web UI happy paths in Chromium, fake model (once: npx playwright install chromium)
202
202
  ```
203
203
 
204
+ Integration specs against a live model server, and what the suite isolates: [docs/testing.md](docs/testing.md).
205
+
204
206
  ## License
205
207
 
206
208
  MIT, see [LICENSE](LICENSE).
data/docs/architecture.md CHANGED
@@ -183,6 +183,13 @@ engine.system_prompt # fully built system prompt string
183
183
  engine.session # current session (Engine owns create/resume)
184
184
  ```
185
185
 
186
+ The system prompt (`SystemPrompt#build`) is the base prompt, then rg guidance,
187
+ AGENT.md, the working directory, **the model** (`Model: this session runs on
188
+ <ref> (host …; model key …)` and a line telling the model to answer "which model
189
+ are you" from it, not from training), the session id and log, the memory indexes,
190
+ the identity and the preloaded memories. It is built once per loop and rebuilt only
191
+ by a model switch (or changed tools), so the model line costs no KV churn.
192
+
186
193
  #### Subscribing to the live stream (and the bridge)
187
194
 
188
195
  For an **always-on** consumer (an external SSE client, a second UI), use
@@ -228,6 +235,16 @@ process that already owns the `Engine`); every worker starts it, and it exposes:
228
235
  `guardrails.parent_approvals` is `403 parent_approval_refused`, the question still open.
229
236
  - `POST /session/:id/question/dismiss` — leave the question unanswered (an approval: denied);
230
237
  `200`, or `409` when it is no longer pending.
238
+ - `POST /session/:id/relay` — the approval relay, on a delegated child's worker
239
+ (`{action, relay_id, question_id}`): `opened` / `closed` (with a `reason`) set or clear the
240
+ pending question's `relayed_to` mark (`QuestionDesk#annotate`, `:question_relay`); `answered`
241
+ makes the child ask its parent's Bridge for the answer (`RelayVerifier`) and take it only for
242
+ this child and the question pending now. `200`, `409` no longer pending, `403` a parent agent's
243
+ allow beyond the child's `guardrails.parent_approvals`, `422 relay_unverified` (the question
244
+ stays open).
245
+ - `POST /session/:id/relay/status` — on a parent's worker, `{relay_id}`: what its `RelayDesk`
246
+ (memory only) holds for that relay (`child_id`, `child_question_id`, `state`, `answer`, `by`);
247
+ `404` for an unknown id. A POST with a body because routes match exact paths.
231
248
  - `POST /session/:id/command` — a session command (`/model`, `/models`, `!rollback`, `!cmd`,
232
249
  `/continue`) for the worker loop; `202` with a `command_id` its `:command_ran` names, `400` when
233
250
  the line isn't one.
@@ -242,6 +259,12 @@ process that already owns the `Engine`); every worker starts it, and it exposes:
242
259
  the messages itself, then streams from its `event_seq`).
243
260
  - `OPTIONS *` — CORS preflight (`Access-Control-Allow-Origin: *`).
244
261
 
262
+ The metrics in `/state`, `/snapshot`, `/stats` and the SSE `snapshot`/`reset` frames
263
+ (`SessionMetrics#snapshot`) carry the session's totals and only the recent timing records: the
264
+ newest turn the worker finished and any it hasn't saved yet (at most 20 turns), with their tool
265
+ calls and the running turn's finished ones. The full history is the session's `analytics.json`,
266
+ which `SessionMetrics#persist` rewrites after each turn; the web server merges the two by id.
267
+
245
268
  Every route goes through `Bridge#dispatch`: an id other than the bridge's own session is
246
269
  `404 unknown_session`, and a handler that raises answers `500 bridge_error` (logged as
247
270
  `handler_failed`) rather than dropping the connection.
data/docs/cli.md CHANGED
@@ -21,7 +21,7 @@
21
21
  - `chi answer ID --question QID (--option N|LABEL)... [--text T] [--timeout S] [--format json]` (or `--dismiss`) — answer the question a session waits on (the one `chi send --wait` exited 3 with), then wait and print what comes next as `chi send --wait` does: the reply (0), the next question (3), still running after `--timeout` (4). `--option` is 1-based or the label, repeated on a multi-select question; `--text` is free text, or a Deny's reason; `--dismiss` leaves it unanswered and the model finishes its reply. A question no longer open (the web answered first) isn't answered again: it waits for that turn's reply. An option the question doesn't offer exits 2; a worker that is gone exits 1 (`send the task again: chi send --wait -m "…" ID`). An approval can be denied, not allowed, unless `guardrails.parent_approvals: once` (a convention, not a security boundary; see [Guardrails](guardrails.md#approvals-from-a-parent-agent))
22
22
  - `chi desktop install|upgrade|uninstall|status` — the macOS "Send to chi" helper: a Service and a ⌃⌥⌘N hotkey that send text or images to a session (live, stopped or new) as a message (⏎, a turn runs) or a context note (⌘⏎) (see [Desktop helper](desktop.md))
23
23
  - `chi models [--format text|json] [--timeout S] [TEXT]` — list the models every configured host offers, as the names `--model` takes (see [Listing the models](#listing-the-models))
24
- - `chi self` — print version, source dir (checkout or installed gem), config/memory/session paths, model/host and bundles; `chi self --model` prints only the model a new session starts on, an alias resolved to its target (the desktop helper's hint)
24
+ - `chi self` — print version, source dir (checkout or installed gem), config/memory/session paths, model/host and bundles. Run by a session's `execute`, the `model` row (and host, loop, profile, thinking, served model) is **that session's** model, labelled `(this session <id8>; default …)`; elsewhere it is the default, `(default)`. `model key` is the memory overlay key (`<name>.<key>.md`). `chi self --model` prints only the model a new session starts on, an alias resolved to its target (the desktop helper's hint)
25
25
  - `chi update [--dry-run] [--no-gem] [--no-bundles] [--no-desktop]` — update an installed chi: the gem, the system bundle, the shipped bundles you installed and the desktop helper, in one table (see [Updating](#updating))
26
26
  - `chi bundle install|upgrade|uninstall|status|diff|list|build` — manage memory bundles (see [Bundle hooks](hooks.md#bundle-hooks-unified-workflow-bundle)); `list` shows the installed ones and the ones shipped with chi, which `install <name>` installs; `core` and `dev` are profiles that install a set of them (see [Bundle profiles](memory.md#bundle-profiles-core-and-dev), [Guardrails](guardrails.md), [Plugins](plugins.md#the-btw-bundle), [the mcp bundle](plugins.md#the-mcp-bundle) [the loop-guard bundle](plugins.md#the-loop-guard-bundle), [the check-in bundle](plugins.md#the-check-in-bundle) and [the skills bundle](plugins.md#the-skills-bundle)). A usage error (an unknown subcommand or flag, a missing argument, a bad `build --scope`) exits 2, as for the other subcommands. Plain `chi` and `chi web` exit 1 on an unknown flag, a flag missing its value or a stray argument
27
27
 
@@ -211,8 +211,9 @@ Notes:
211
211
  ended, 3 a question or an approval waits for an answer (stderr gets the
212
212
  whole question as `chi send --wait` prints it, with the
213
213
  `chi answer ID --question QID --option N` command; the worker keeps it
214
- open, and `chi answer` or the web answers it), 1 the turn failed or the
215
- worker went away. For a
214
+ open, and `chi answer` or the web answers it), 1 the turn failed, ended
215
+ with no answer (stderr: `chi: the model gave an empty answer`, as with
216
+ `--non-interactive`) or the worker went away. For a
216
217
  script, `chi send --new --wait` is the better entry point: stdout holds the
217
218
  answer alone (see [Starting a session](sessions.md#starting-a-session)).
218
219
  - `-p` always feeds **and** runs the prompt; there is no feed-and-edit variant. To
@@ -282,7 +283,9 @@ In an attached terminal:
282
283
  - The prompt stays open while a turn runs; see [Typing during a turn](#typing-during-a-turn).
283
284
  - `/model`, `/models`, `/guardrails`, `/continue`, `!rollback` and `!commands` run in the
284
285
  worker, and every UI sees their output; `/stats` and `/recap` work too. The
285
- Web UI's composer takes the same commands.
286
+ Web UI's composer takes the same commands, and so do `-p "/model x"` and
287
+ `chi send -m "/model x"`. A `/word` the session doesn't know (a path, a
288
+ typo) goes to the model as a prompt.
286
289
  - `!commands` and the model's tools run in the session's directory (where it
287
290
  was started), whichever terminal you attach from.
288
291
  - `-p` sends its prompt once attached, `--model` switches the worker's model
@@ -279,14 +279,14 @@ spec. It requires the model to call `execute` and return the current UTC date:
279
279
 
280
280
  ```shell
281
281
  SAMAGOTCHI_INTEGRATION=1 \
282
- SAMAGOTCHI_SERVER_HOST=192.0.2.10 SAMAGOTCHI_SERVER_PORT=8000 \
283
- SAMAGOTCHI_DEFAULT_MODEL=incoai/Qwen3.8-27B-Splash \
284
- bundle exec rspec spec/integration/chat_loop_spec.rb -fd < /dev/null
282
+ SAMAGOTCHI_INTEGRATION_HOST=192.0.2.10 SAMAGOTCHI_INTEGRATION_PORT=8000 \
283
+ SAMAGOTCHI_INTEGRATION_MODEL=incoai/Qwen3.8-27B-Splash \
284
+ bundle exec rspec spec/integration/chat_loop_spec.rb
285
285
  ```
286
286
 
287
287
  The same test works against a llama.cpp OpenAI-compatible server by changing
288
288
  the host, port, and model values. The test is skipped unless
289
- `SAMAGOTCHI_INTEGRATION=1` is set.
289
+ `SAMAGOTCHI_INTEGRATION=1` is set; see [Testing](testing.md).
290
290
 
291
291
  oMLX's known tool-call limitation (a stream filter that strips markup) only
292
292
  affects its `/v1/chat/completions` endpoint, not the `/v1/completions` endpoint
@@ -625,7 +625,9 @@ asks again in the same turn with a hidden note ("your last reply had no visible
625
625
  unless `sampling:` sets one; the REPL prints `↻ empty answer, asking again (1/1)` and the web shows it as a row of
626
626
  the step. `retry.empty_answer` (default `1`, at most `3`, env `SAMAGOTCHI_RETRY_EMPTY_ANSWER`, no CLI flag) is how
627
627
  many times per turn; `0` ends the turn at the empty answer as before. An answer cut because the context is full
628
- (90 % or more) is not retried. When the retries run out the turn ends with "(the model returned an empty answer)".
628
+ (90 % or more) is not retried. When the retries run out the turn ends with no answer: every UI shows one muted line,
629
+ `no answer: the model returned nothing (after 1 retry)`, and the model is told on its next turn with a hidden note;
630
+ no made-up answer is saved.
629
631
  A generation a plugin cuts while it streams (loop-guard's thinking watch, or any `stop_generation`,
630
632
  [hooks.md](hooks.md#watching-the-stream)) uses the same budget: `↻ cut by loop-guard, asking again (1/1)`, and with
631
633
  none left the turn ends as cancelled (hook).
data/docs/guardrails.md CHANGED
@@ -56,9 +56,14 @@ Who answers:
56
56
  the approval waits (in the session file) and shows on attach.
57
57
  - `-p … --non-interactive`: nobody; the call is denied ("No one to approve it
58
58
  (non-interactive run)").
59
+ - A delegated child session (the `delegate` tool), while its parent waits:
60
+ the parent's user, on the parent's own card (the approval relay; see
61
+ [Sessions: Delegating](sessions.md#delegating)). The child's card stays
62
+ answerable too, the first answer wins. The parent's model sees only the
63
+ outcome.
59
64
  - A parent agent running chi as a sub-agent (`chi send --wait` exits 3 with
60
- the approval): `chi answer` may deny it, never allow it by default. See
61
- [Approvals from a parent agent](#approvals-from-a-parent-agent).
65
+ the approval): `chi answer` may deny it, never allow it by default, or leave
66
+ it for the user. See [Approvals from a parent agent](#approvals-from-a-parent-agent).
62
67
 
63
68
  The model gets one line on a deny. A rule's or hook's deny reads
64
69
  `[execute] Error: denied by guardrail (rule git-push, bundle guardrails): git push publishes commits. The user was not asked. Do not retry it or reach the same result another way; ask the user how to proceed.`
@@ -84,6 +89,15 @@ The file tools can't write the store.
84
89
 
85
90
  ### Approvals from a parent agent
86
91
 
92
+ A chi parent's `delegate` children don't need any of this: their approvals go
93
+ to the user on the parent's own card (the approval relay). This section is the
94
+ path for other agents, and for a chi parent that can't host the relay (a
95
+ `--no-shared` REPL, `--non-interactive`): there the model gets the approval
96
+ block and the rules below. With the relay, the same rules still hold for a
97
+ parent model that answers its own relay card (a piped or marked `chi --attach`
98
+ on the parent): that answer is a parent agent's, checked by the parent and
99
+ again by the child.
100
+
87
101
  When another agent (Claude Code, Codex, a script) runs chi with
88
102
  `chi send --wait`, an approval comes back to it as exit 3 with `kind:
89
103
  approval`, and `chi answer` answers it. A rule with `verdict: ask` means "ask
@@ -22,5 +22,6 @@ Behavior:
22
22
  - `task_get` returns metadata and `output_path`; use `read` for output contents.
23
23
  - `task_wait` accepts `timeout`, `tail_lines` (maximum 100), and `done_pattern` (a regular expression string).
24
24
  - `task_create` accepts `env` as a JSON object string for deterministic overrides such as `PATH`; use an absolute interpreter path when that is simpler. Ruby/Bundler isolation variables remain protected.
25
+ - `execute` and `task_create` export chi's own markers into the command, set over the model's `env` (which can't set them: `Error: env key is reserved`): `SAMAGOTCHI_PARENT_SESSION` (the session's id, `chi` without one; a chi started there answers as a parent agent) and `SAMAGOTCHI_SESSION_MODEL` (the resolved ref the session runs on, read at each call so it follows `/model`; unset when unknown). `chi self` reads the latter for its `model` row.
25
26
 
26
27
  Work that needs a model, not a shell command, goes to a child chi session instead: `delegate` / `delegate_result` have the same shape (start, then wait) and return only the child's final reply; see [Sessions: Delegating](../sessions.md#delegating).
data/docs/memory.md CHANGED
@@ -23,6 +23,8 @@ Each memory entry may have a companion file named `<name>.<model-key>.md` in the
23
23
 
24
24
  - **Key derivation**: The harness normalizes the model name sent to the server, without its host prefix (`box:qwen3.6-35b-a3b` keys as `qwen3.6-35b-a3b`; an alias keys as its target, and the alias's own key is read when the target has no overlay) (lowercase, replace non-alphanumeric with `-`, squeeze dashes) to derive the file key. For example, `qwen3.6-35b-a3b` → `qwen3-6-35b-a3b`.
25
25
  - **Saving overlays**: Pass `current_model_only: true` to `memory_write` (the harness resolves the model key automatically). This writes the content as `<name>.<model-key>.md` and skips index maintenance.
26
+ - **Which key is mine**: the system prompt's `Model:` line names the session's model key, and `chi self` (via `execute`) has a `model key` row; both follow `/model`.
27
+ - **In the system prompt**: the identity memory and the preloaded memories (`--memory`, config `memories:`) get their overlays too, as `memory_read` gives them; after `/model` the rebuilt prompt carries the new model's.
26
28
  - **Dormancy**: Overlays are only active under the matching model key; other models see the base entry only.
27
29
  - **Invariant**: The base entry is the contract. Overlays only add model-specific guidance and never contradict the base protocol.
28
30
 
@@ -80,6 +82,43 @@ skills`, [docs/plugins.md](plugins.md#the-skills-bundle)) adds `/skill save`,
80
82
  one-line diff after each update, and nudges a model that skips a failing
81
83
  step instead of fixing the skill.
82
84
 
85
+ ## What a bundle owns
86
+
87
+ A bundle owns the memory files it wrote, nothing else. `chi bundle install`
88
+ copies each of its `*.md` files into the scope dir; a file of the same name
89
+ that is already there is left as it is and reported `Skipped` (`--force`
90
+ overwrites it, and then the bundle owns it). A skipped file stays yours: the
91
+ bundle doesn't record it, so `chi bundle upgrade` never merges into it and
92
+ `chi bundle uninstall` never removes it. That holds for a file the user wrote,
93
+ for `chi bundle build` then installing the result on the same machine, and
94
+ for a file another bundle installed.
95
+
96
+ - **The index says where a memory came from.** The `index.md` line of a file
97
+ a bundle installed ends in `· from <bundle>`, before the description:
98
+ `- **memory_guide** · system · 2026-10-03 · 5120 · from samagotchi-system —
99
+ …`. `memory_write`, `write` and `edit` keep it. A bundle installed by chi
100
+ 0.16 or earlier gets it on its next install or upgrade.
101
+ - **Uninstall moves, it doesn't delete.** The bundle's memory files go to
102
+ `$XDG_CONFIG_HOME/samagotchi/memories/.bundles/.trash/<bundle>-<YYYYmmdd-HHMMSS>/`,
103
+ and the output names them and the dir:
104
+ `Moved to the trash: notes.md (…/.trash/team-notes-20261003-120000)`. So
105
+ does an upgrade's removal of a file the new version no longer ships. A
106
+ file you edited still needs `--force`. Nothing empties the trash: delete
107
+ the dirs when you're sure. Hooks, rules and the plugin live in the
108
+ bundle's own dir and are deleted with it.
109
+ - **A file two bundles list stays** until the last of them is uninstalled
110
+ (`Kept identity.md: bundle samagotchi-system has it too`); its index line
111
+ then names the bundle that still has it.
112
+ - **Installs by chi 0.16 or earlier** recorded skipped files too. Such a bundle may
113
+ still list a file that was yours; uninstalling it moves that file to the
114
+ trash rather than deleting it, so check the `Moved to the trash` line and
115
+ move a file back if it was yours (and its line in `index.md` comes back
116
+ with the next `memory_write`, `write` or `edit` of it).
117
+ - **`chi bundle build`** packs your memories, not other bundles': a memory an
118
+ installed bundle owns is left out with a line (`Left out identity.md:
119
+ installed by bundle samagotchi-system (name it to include it)`); naming it
120
+ in `FILES...` includes it.
121
+
83
122
  ## Bundle profiles: core and dev
84
123
 
85
124
  Every bundle chi ships (the system bundle aside) belongs to one of two
data/docs/plugins.md CHANGED
@@ -478,6 +478,8 @@ full label, `plugin.rb (bundle my-bundle)`.
478
478
  | `ctx.session_id` | the session's id (nil before there is one) |
479
479
  | `ctx.cwd` | the session's working directory |
480
480
  | `ctx.repo_root` | the git checkout holding `cwd`, or nil |
481
+ | `ctx.model` | the model the session runs on now: its resolved ref (`host:id`), right after `/model` too |
482
+ | `ctx.model_key` | that model's memory overlay key (`<name>.<key>.md`, what `memory_write current_model_only` writes) |
481
483
  | `ctx.settings` | the bundle's settings, frozen |
482
484
  | `ctx.data_dir` | `$XDG_STATE_HOME/samagotchi/plugins/<bundle>/`, created on first use. A session's own state goes in `sessions/<id>.json` or `sessions/<id>/` there: chi removes it when the session is deleted, discarded or pruned |
483
485
  | `ctx.log` | `ctx.log.info(:event, key: value)`: debug-log records tagged `plugins`, with `bundle=<bundle>` |
data/docs/sessions.md CHANGED
@@ -40,7 +40,7 @@ chi sessions prune [--dry-run] [--days N] [--keep N] [--keep-status running,...]
40
40
  chi sessions clean [--dry-run] [--days N] # test sessions: all, or older than N days
41
41
  ```
42
42
 
43
- A session whose worker waits for an answer (a question, a hook's question or a guardrail approval) reads `waiting` in the status column, and json has `waiting: "question"|"approval"|"hook"` and `waiting_id`, the question's id for `chi answer --question` (both null otherwise). Only while a worker runs it: a question a dead worker left in the file shows the saved status. Answer it with `chi answer`, the web or `chi --attach ID`; `chi sessions stop ID` frees the worker instead.
43
+ A session whose worker waits for an answer (a question, a hook's question or a guardrail approval) reads `waiting` in the status column, and json has `waiting: "question"|"approval"|"hook"` and `waiting_id`, the question's id for `chi answer --question` (both null otherwise). A delegate's approval its parent relayed to its own card (see *Delegating*) reads `waiting (in parent ab12)`, json `relayed_to: "ab12…"` (the parent's short id, else null). Only while a worker runs it: a question a dead worker left in the file shows the saved status. Answer it with `chi answer`, the web or `chi --attach ID`; `chi sessions stop ID` frees the worker instead.
44
44
 
45
45
  Examples:
46
46
 
@@ -63,7 +63,7 @@ chi sessions clean --dry-run --days 7 # test sessions older than 7 da
63
63
  **Ordering:**
64
64
 
65
65
  - `Session.list` / `SessionManager.list_sessions` / `GET /api/sessions?sort=&order=&limit=&offset=` default to `updated_at desc` (newest activity first). Also supports `created_at`, `asc`. `X-Total-Count` header when paginated.
66
- - Web UI (`chi web`): the page's scope is in its URL. Started in a git repo, `chi web` opens `/?dir=<that folder>`: that project's sessions, and new chats start in that folder; the header chip says `<project> · all`, and `all` opens the same place without `?dir` (every session; a new chat there starts in the server's own folder, shown on the start page, and cards name their folder; `← <project>` goes back to the project view it came from, or to the server's own project). One server serves every project: a second `chi web` (from another repo) finds it through `GET /api/info` and prints (with `--open`, opens) its page for its own folder instead of starting another. The 3 latest sessions sit above the chat; "All sessions" (or `/`) opens every session at `#/sessions`, with a search over preview, id and status (Esc or Back returns). The open session is in the URL (`#/s/<id>`), so a reload or a copied link opens it again; the chi logo top left goes back to the empty start for a new chat. The message box grows with its text; drag its top edge to keep it taller (double-click resets). The info bar copies `chi --attach <id>` for a terminal. The start page's model picker (bottom left of the composer, the chosen name `▾`) chooses the model a new chat starts on; it opens a search list over the conversation (↓ or a click; downward when the start page leaves no room above): an empty search shows the last 5 picks (Recent) and then each host's model ids A–Z under the host, the default host first; typed words must each match the host or the id (a substring, else for 3+ characters the letters in order, so `deepseek4.1 fla` finds `openrouter · deepseek/deepseek-v4.1-flash`), ⏎ picks, Esc/Tab close, a pick puts the focus in the message box: `GET /api/models` lists the hosts' models as chi spells them (`{default, models: [{name, host, id}], warning?}`; bare for the default host, `host:model` for the others, from the same cached lists as `/models`, a bounded wait, a host that is down noted in `warning`), and `POST /api/sessions` takes `model` (blank means the default). The start page's first message goes as later ones do: `POST /api/sessions` with `idle: true` and `preview` (the message, which names the session until its turn is saved), then `POST /api/sessions/:id/turn`, so a failed first turn puts its prompt back in the composer too; a first command line (`/model …`) still starts the session as its `prompt`. The browser remembers the last choice; a running session's model is in the info bar and changes only with `/model`. ↑/↓ in the composer walk the prompt history the TUI's ↑ walks ([Persistent Prompt History](cli.md#persistent-prompt-history)): ↑ with the caret on the first line, ↓ from the last line while browsing, ↓ past the newest brings the draft back; `GET /api/history` returns `{entries: [...]}`, oldest first (the page fetches it on focus and after each send). The server records a sent line in the routes once the worker took it (`POST /turn` accepted, a `!command` through `POST /command` or as a session's first line); `"history": false` in the body keeps it out (an image-only send).
66
+ - Web UI (`chi web`): the page's scope is in its URL. Started in a git repo, `chi web` opens `/?dir=<that folder>`: that project's sessions, and new chats start in that folder; the header chip says `<project> · all`, and `all` opens the same place without `?dir` (every session; a new chat there starts in the server's own folder, shown on the start page, and cards name their folder; `← <project>` goes back to the project view it came from, or to the server's own project). One server serves every project: a second `chi web` (from another repo) finds it through `GET /api/info` and prints (with `--open`, opens) its page for its own folder instead of starting another. The 3 latest sessions sit above the chat (`hide ▴` puts them away behind a `▾ N sessions` pill, remembered; a window under 700 px tall hides them by itself, so a question or approval card in the stage keeps room, and the pill shows them until the window is tall again); "All sessions" (or `/`) opens every session at `#/sessions`, with a search over preview, id and status (Esc or Back returns). The open session is in the URL (`#/s/<id>`), so a reload or a copied link opens it again; the chi logo top left goes back to the empty start for a new chat. The message box grows with its text; drag its top edge to keep it taller (double-click resets). The info bar copies `chi --attach <id>` for a terminal. The start page's model picker (bottom left of the composer, the chosen name `▾`) chooses the model a new chat starts on; it opens a search list over the conversation (↓ or a click; downward when the start page leaves no room above): an empty search shows the last 5 picks (Recent) and then each host's model ids A–Z under the host, the default host first; typed words must each match the host or the id (a substring, else for 3+ characters the letters in order, so `deepseek4.1 fla` finds `openrouter · deepseek/deepseek-v4.1-flash`), ⏎ picks, Esc/Tab close, a pick puts the focus in the message box: `GET /api/models` lists the hosts' models as chi spells them (`{default, models: [{name, host, id}], warning?}`; bare for the default host, `host:model` for the others, from the same cached lists as `/models`, a bounded wait, a host that is down noted in `warning`), and `POST /api/sessions` takes `model` (blank means the default). The start page's first message goes as later ones do: `POST /api/sessions` with `idle: true` and `preview` (the message, which names the session until its turn is saved), then `POST /api/sessions/:id/turn`, so a failed first turn puts its prompt back in the composer too; a first line that is one of the new session's commands (`/model …`) goes to `POST /api/sessions/:id/command` instead, as later ones do. The browser remembers the last choice; a running session's model is in the info bar and changes only with `/model`. In the composer a line is a command when it is `!…` or starts with one of the session's command names (its `commands` list; `/stats`, `/exit` and the other terminal words get the page's own reply); an unknown `/word` (`/usr/bin/env is missing`) goes to the model as a prompt, as in a terminal. A `POST /turn` (or a session's first `prompt`) that is one of the worker's commands runs as that command too (`202 {status, command_id, session_id}`), so `chi send`, `chi -p` and a page whose command list lags all get the command. ↑/↓ in the composer walk the prompt history the TUI's ↑ walks ([Persistent Prompt History](cli.md#persistent-prompt-history)): ↑ with the caret on the first line, ↓ from the last line while browsing, ↓ past the newest brings the draft back; `GET /api/history` returns `{entries: [...]}`, oldest first (the page fetches it on focus and after each send). The server records a sent line in the routes once the worker took it (`POST /turn` accepted, a `!command` through `POST /command` or as a session's first line); `"history": false` in the body keeps it out (an image-only send).
67
67
  - Web notifications: when a session needs you while the tab is hidden or behind another window (a question or a guardrail approval, a plugin card with buttons during a turn such as check-in's, a failed turn, a turn done after 10 s or more; a delegated session only for its questions, a canceled turn never), the page title counts it, `(N) Chi`, until you come back to the tab. The bell at the right of the top bar also turns on OS notifications ("needs an answer", "needs approval", "needs you", "turn failed", "done in 42 s", under the session's first message; a click opens the session): its first click asks the browser for the permission, and the choice is kept per browser. Any session of the page's scope counts, not only the open one. What was already so when the page loaded never counts, and several chi tabs show one notification per event. A chi tab in front tells the others (a `BroadcastChannel`), so while you look at one, the ones behind neither notify nor count what it shows; coming back to a tab clears those events in the other tabs' titles too. The hub's `session` frames carry what this needs: `pending_question` (`{id, kind}`, kind `question` or `approval`), `pending_card` (`{id, bundle}`: the running turn's card with actions, from `pending_card.json` that the worker keeps in the session's folder while one is open; a card without actions, such as loop-guard's, never counts) and `last_turn` (`{outcome, ended_at, seconds, origin}`, saved in the session's JSON as the engine ends each turn).
68
68
  - The web frontend is a zero-build ES-module stack in `lib/samagotchi/web/public/`: `data.js` (retrieval, typed SSE `openStream` for a session and `openEvents` for the session list), `sessions_list.js` (the list as a pure reducer over `GET /api/events`: a snapshot replaces it, an upsert keeps a known card in place, a removal drops it), `notify.js` (which of those events needs the user, for the notifications), `app.js` (presentation/state; a session with no live stream gets one when its `session` event says `bridge_up`, after one re-read: `event_seq` starts over in each worker, so a dropped stream is never resumed with its old cursor), `format.js` (pure formatters such as `previewOf`), `prompt_history.js` (the composer's ↑/↓ over the shared prompt history). Unit-tested via `npm test` (`node --test spec/web/public/*.test.js`).
69
69
  - Selecting a session in the web UI is read-only: `GET /api/sessions/:id` never spawns a worker (it reads a live worker's snapshot when one runs, else the session file). A prompt (`POST /turn`) or a command (`POST /command`) wakes the worker, and `/stream` briefly waits for a freshly-spawned bridge before answering. A caught-up SSE reconnect holds the stream open; `reset` markers are only sent for reconnects behind the ring window, or with a cursor from another worker (event ids are `<event_seq>-<epoch>`, one epoch per worker).
@@ -149,7 +149,8 @@ pbpaste | chi send 3fa2 # the clipboard is the messa
149
149
  - A session with no worker gets one started (like `--attach` or the web composer). A session open in a plain REPL (`--no-shared`) refuses it. Only sessions on this machine: Bridges listen on 127.0.0.1.
150
150
  - Fire and forget: it returns once the message is queued and never prints the answer; that shows in whatever is attached. A guardrail "ask" waits for a UI to answer it, so with nothing attached the turn stalls there until one attaches (`chi --attach ID`).
151
151
  - `--image PATH` (repeatable, up to 20) sends images with the message, as the web composer's chips do. Each file is read once before anything is sent (bmp, tiff and heic converted to png, large ones downscaled, as for `@path`); a missing file or one that isn't an image stops the send (exit 1; with `--wait --format json`, an `error` line) and nothing is sent; more than 20 is a usage error (exit 2). Each session gets its own copy in `<session>/images/`. Text is still required (`-m` or stdin; context alone counts), also with `--wait`. The session's model must see images: one known not to (the host's model list says text-only, `vision: false` in `models:` or `hosts:`, a native llama.cpp without a vision model) is refused before anything is sent, with the reason (`<id> refused: gemma can't take images (…); send text only or switch the model (/model)`); the other sessions of the same send still get it, and the exit is 1. With `--new` nothing is started. When chi can't tell (a native host whose `/props` doesn't answer, a list that doesn't say), the message is sent as before and a text-only model fails the turn in the session. A running turn doesn't take images mid-turn: the message runs as the next turn, and the line says `(runs after the current turn)`. With `--new` the session starts idle with the message as its preview, then the message goes in as its first turn once its worker is up (`<id> started with 1 image`); if the worker doesn't come up in 5 s the session is kept, with its id on the `failed:` line.
152
- - One line per session: `sent`, `sent with 2 images`, `sent (the running turn picks it up)`, `sent (started its worker)`, `refused: …` or `failed: …`. Exit 0 when all were sent, 1 when any was refused, failed or not found, 2 for a usage error. There is no `--all`.
152
+ - A session command runs as the command, as typed in a terminal: `chi send -m "/model qwen" 3fa2` switches the model (`sent as a session command`), and the model never sees the line. What counts is what the session's worker knows (the built-ins, `!cmd`, its bundles' commands); an unknown `/word` (`/usr/bin/env is missing`) goes in as a message. With `--new` a line starting with `/` or `!` starts the session idle and goes to its worker once it is up, which decides. With `--wait` a command has no reply to wait for: exit 0 at once, `<id> sent as a session command: no reply to wait for …` on stderr (`{"status":"command",…,"detail":…}` with `--format json`); its output shows where the session is open.
153
+ - One line per session: `sent`, `sent as a session command`, `sent with 2 images`, `sent (the running turn picks it up)`, `sent (started its worker)`, `refused: …` or `failed: …`. Exit 0 when all were sent, 1 when any was refused, failed or not found, 2 for a usage error. There is no `--all`.
153
154
 
154
155
  The Automator action above works for messages too: swap its last line for `pbpaste | "$chi" send -m "what do you make of this?" $(print -r -- "$picked" | cut -f1)`.
155
156
 
@@ -180,7 +181,12 @@ The `delegate` tool hands a task to a **child session**: an ordinary chi session
180
181
 
181
182
  - `delegate(task, model:, session:, wait:, timeout:)`: starts a child and, by default, waits for its reply (`wait: false` returns at once, so several children can run side by side; `timeout`, default 600 s, leaves a slow child running; 0 checks once without waiting). With `session:` (a child's id or prefix) the task goes to that child as a follow-up instead, delivered like `chi send` (client id `delegate:<parent short id>`, shown as `delegate>` in the child's UIs); only this session's children take one.
182
183
  - `delegate_result(session:, timeout:)`: waits for a child's next reply: the one named, or the newest running child (`timeout` as for `delegate`: 0 checks once and returns at once). The reply comes back once; a later call waits for the next one.
183
- - What a wait returns, `session: <id>` then `status:` and the text, with the status words `chi send --wait --format json` uses: `answered` with the reply after a `---` line; `question` as soon as the child opens a question or a guardrail approval, with the question's text, options and the `chi answer` command for it (nothing is answered for it: `chi answer`, attach or use the web, then wait again); `failed`, `canceled` or `no_answer` when the child's turn ended with nothing to say (its session shows what happened); `error` (its worker crashed) or `stopped` (`chi sessions stop`) at once; `worker_gone` when the child has had no worker for 15 seconds (it crashed without saying so, or idle-exited): `delegate` with `session:` starts it again; `running` on timeout, or when the parent's own turn is canceled (the child keeps running; the next turn can wait again). A reply saved before the child's worker idle-exited is returned at once.
184
+ - What a wait returns, `session: <id>` then `status:` and the text, with the status words `chi send --wait --format json` uses: `answered` with the reply after a `---` line; `question` as soon as the child opens a question (the model's own, or a hook's), with the question's text, options and the `chi answer` command for it (nothing is answered for it: `chi answer`, attach or use the web, then wait again); `failed`, `canceled` or `no_answer` when the child's turn ended with nothing to say (its session shows what happened); `error` (its worker crashed) or `stopped` (`chi sessions stop`) at once; `worker_gone` when the child has had no worker for 15 seconds (it crashed without saying so, or idle-exited): `delegate` with `session:` starts it again; `running` on timeout, or when the parent's own turn is canceled (the child keeps running; the next turn can wait again). A reply saved before the child's worker idle-exited is returned at once.
185
+ - **A child's approvals go to the parent's user** (the approval relay). While the parent waits (`delegate`, `delegate_result`), a guardrail approval the child opens reopens as the parent session's own approval: the same card in the parent's web page and attached terminal, with the same facts and diff, a line naming the delegate and its task (`delegate ab12 · fix the flaky spec…`, linked to its session) and the header `Approve delegate ab12's tool call?`. The options are the child's, by index; "for the session" says it is the delegate's session (the session scope keys on the child; repo and rule approvals are one shared store). The user answers once there; the parent's model never sees the approval, only one line per relayed approval in the tool's result, before the reply: `approval relayed to your user: execute: git push origin main → allowed once` (or `denied ("why")`, `dismissed (denied)`, `answered on the child`, `not delivered (the child's worker is gone)`, `refused by the child …`, `still open (your turn was stopped)`). The time the card is open doesn't count against `timeout`. While the parent waits on one child, its other running children's approvals are relayed too, oldest first, one card at a time (`+1 more delegate waiting`); each outcome comes in that child's next result.
186
+ - The child's own card stays answerable (the first answer wins: answered there, the parent's card closes as "answered in ab12"). While relayed it says `Waiting for approval in parent ab12: answering here works too`; `chi sessions list` shows the child as `waiting (in parent ab12)` (`--format json`: `relayed_to`), and the web's bell rings for the parent's card, not the child's.
187
+ - How the child trusts it: the parent's POST that says "answered" (Bridge `POST /session/<child>/relay`) carries no answer. The child asks its parent session's live Bridge (`POST /session/<parent>/relay/status`), which reports only what the parent's question flow took from a UI (`RelayDesk`, in memory only), and takes it only for this child, the question pending now, answered. A parent agent's answer on the relay card (a piped or marked `chi --attach PARENT`, `chi answer`) is held to `guardrails.parent_approvals` by the parent and again by the child.
188
+ - A Stop of the parent's turn closes its card; the child's approval stays open there. A child's worker that goes closes the card (`… worker is gone`); a parent's worker that goes clears the child's "waiting in parent" line within 2 s. A later `delegate_result` relays it again.
189
+ - No relay where the parent can't host one (a `--no-shared` REPL, `--non-interactive`) or isn't waiting (`wait: false` until the next `delegate_result`): the approval waits on the child, with the web's bell, and the result's question block says to deny it or leave it for the user. The model's own questions and hooks' questions come back to the model as before. A grandchild's (a plugin's `ctx.sessions.fork` can make one) is relayed hop by hop: `delegate ab12 → cd34 asks:`.
184
190
  - Every child starts with the shipped `delegated` system memory preloaded (`preloaded_memory_names: ["system/delegated"]`; `--mute delegated` would hide it): put everything in the final reply, don't delegate further, don't write memories or change config, stay in the folder. Its system prompt also says `Delegated by session <parent id>`. The parent's own `--mute` list does not carry over.
185
191
  - Limits: a child cannot `delegate` (depth 1; the tool refuses), and one session may have at most `session.max_children` (default `4`, env `SAMAGOTCHI_SESSION_MAX_CHILDREN`) children running at a time, counted from the session files (a finished or idle-exited child does not count; one whose worker is still starting does, for 15 s). A refused call creates no session. Nothing stops children with the parent: `chi sessions stop ID` does.
186
192
  - The link is the session's `parent_id`, written before the worker starts, so a respawn keeps it; `GET /api/sessions` and `/api/sessions/:id` carry it, and `SessionManager.children_of` lists a parent's children.
data/docs/sub-agent.md CHANGED
@@ -33,6 +33,7 @@ Exit codes, both commands:
33
33
  | Exit | `status` | Meaning |
34
34
  |---|---|---|
35
35
  | 0 | `answered` | `text` is chi's reply |
36
+ | 0 | `command` | (`chi send`) the message was a session command (`/model x`): it ran as one, no reply comes |
36
37
  | 3 | `question` | chi waits for an answer: `question`, `answer_with` |
37
38
  | 4 | `running` | `--timeout` passed; the turn goes on, wait again |
38
39
  | 1 | `failed`, `canceled`, `no_answer`, `error`, `worker_gone`, `stopped` | `detail` says what happened |
@@ -53,12 +54,22 @@ command, folder, rule and reason.
53
54
 
54
55
  ## Approvals
55
56
 
57
+ A chi parent (the `delegate` tool) doesn't go through any of this: its
58
+ children's approvals go to its own user as its own approvals, and its model
59
+ sees only how each was answered. See
60
+ [Sessions: Delegating](sessions.md#delegating). What follows is for other
61
+ parents (Claude Code, Codex, a script), and for a chi parent that can't host
62
+ the relay (a `--no-shared` REPL, `--non-interactive`).
63
+
56
64
  An approval is a guardrail rule with `verdict: ask`: its user said "ask me". So
57
65
  a parent may deny it (`--option Deny --text "why"`, `--text` alone, or
58
66
  `--dismiss`) but not allow it: `chi answer` refuses an Allow with exit 1 and
59
67
  `allowing a tool call is up to the user: deny it (--option Deny --text WHY),
60
68
  and tell your user`. For an approval, the question block and `answer_with` give
61
- the deny command, not `--option N` or `chi --attach`. With
69
+ the deny command, not `--option N` or `chi --attach`. The block also offers to
70
+ leave it open: tell your user it waits in chi web (the bell and badge show
71
+ it there), and `chi send --wait --format json ID` (no message) waits until
72
+ they answer it. With
62
73
  `guardrails.parent_approvals: once` in chi's config.yml (it has no environment
63
74
  variable), "Allow once" goes through; the wider scopes never do,
64
75
  and neither does any allow on chi's own config, hooks or guardrail rules. See
@@ -113,8 +124,10 @@ Start a task: `chi send --new --wait --format json --timeout 500 -m "<task>"`
113
124
  - `question.kind: approval`: chi wants to run something its user's guardrails
114
125
  flagged, and allowing it is up to your user. Deny it (the `answer_with`
115
126
  command: `--option Deny --text "why"`), and tell your user what it wanted to
116
- run and why. Never allow it yourself, through `chi answer`, a piped
117
- `chi --attach` or chi's web API.
127
+ run and why. Or leave it open: tell your user it waits in chi web (the
128
+ session's short id), then `chi send --wait --format json --timeout 500 <session_id>`
129
+ waits until they answer it there. Never allow it yourself, through
130
+ `chi answer`, a piped `chi --attach` or chi's web API.
118
131
  - `status: running` (exit 4): still working. Wait again:
119
132
  `chi send --wait --format json --timeout 500 <session_id>`.
120
133
  - Anything else (exit 1): report `detail` to your user. `chi --attach <session_id>`
data/docs/testing.md ADDED
@@ -0,0 +1,47 @@
1
+ # Testing
2
+
3
+ ```sh
4
+ bundle exec parallel_rspec -n 8 # Ruby specs, in parallel (or plain: bundle exec rspec)
5
+ npm test # web frontend specs
6
+ npm run e2e # web UI happy paths in Chromium, fake model (once: npx playwright install chromium)
7
+ ```
8
+
9
+ No spec reads your own setup. The suite points `XDG_CONFIG_HOME` and `XDG_STATE_HOME` at temp folders,
10
+ clears your `SAMAGOTCHI_*` environment, and keeps every spec off the network (WebMock).
11
+
12
+ ## Integration specs
13
+
14
+ Examples tagged `:integration` (`spec/integration/`, the "with LLM access" part of `spec/hooks/integration_spec.rb`)
15
+ run turns against a live model server. They are skipped unless `SAMAGOTCHI_INTEGRATION=1` is set, and they need the
16
+ server and the model it serves:
17
+
18
+ | Variable | Meaning | Default |
19
+ | --- | --- | --- |
20
+ | `SAMAGOTCHI_INTEGRATION` | `1` runs them | off |
21
+ | `SAMAGOTCHI_INTEGRATION_HOST` | model server host | `localhost` |
22
+ | `SAMAGOTCHI_INTEGRATION_PORT` | model server port | `8080` |
23
+ | `SAMAGOTCHI_INTEGRATION_MODEL` | the served model id | none: the examples skip |
24
+ | `SAMAGOTCHI_INTEGRATION_API` | `openai` for the chat loop (`hosts.<name>.api`) | none: the native loop |
25
+ | `SAMAGOTCHI_INTEGRATION_TRANSPORT` | `llama_cpp`, `mlx` or `omlx` (`server.transport`) | none: `llama_cpp` |
26
+
27
+ They run under a config built from these variables alone (`default.model`, `server.host`/`port`, and one
28
+ `hosts.integration` entry; `spec/support/integration_server.rb`), never your `~/.config/samagotchi/config.yml`: no
29
+ aliases, memories, hooks, guardrails or other hosts of yours. Sessions and logs go to the suite's temp state folder.
30
+
31
+ Run them one at a time (plain `rspec`, not `parallel_rspec`): each example is a real model turn, and the server may be
32
+ shared.
33
+
34
+ ```sh
35
+ SAMAGOTCHI_INTEGRATION=1 \
36
+ SAMAGOTCHI_INTEGRATION_HOST=192.0.2.10 SAMAGOTCHI_INTEGRATION_PORT=8081 \
37
+ SAMAGOTCHI_INTEGRATION_MODEL=org/Model-GGUF:Q4_K_M \
38
+ bundle exec rspec --tag integration spec/integration spec/hooks/integration_spec.rb spec/integration_config_spec.rb
39
+ ```
40
+
41
+ A run takes one to four minutes against a 35B MoE model on llama.cpp (25 examples). `ECONNREFUSED` means the server is down. The
42
+ examples assert what the model did (the tool it called, the file it edited), not its wording, but they still depend on
43
+ a model that follows tool instructions: a small model may fail some of them.
44
+
45
+ `spec/integration/omlx_spec.rb` tests oMLX's model-id resolution and runs only with
46
+ `SAMAGOTCHI_INTEGRATION_TRANSPORT=omlx`, against an oMLX server (a short model selector in
47
+ `SAMAGOTCHI_INTEGRATION_MODEL`).
@@ -23,10 +23,14 @@ module Samagotchi
23
23
 
24
24
  # A message as a model-facing reader may see it: without `display`.
25
25
  # The same object when it has none.
26
+ # An empty-answer note's marker (TurnNote::EMPTY_ANSWER) is display
27
+ # only too: it goes the same way.
28
+ DISPLAY_ONLY = [KEY.to_s, "empty_answer"].freeze
29
+
26
30
  def self.strip(message)
27
- return message unless message.is_a?(Hash) && (message.key?(KEY) || message.key?(KEY.to_s))
31
+ return message unless message.is_a?(Hash) && DISPLAY_ONLY.any? { |key| message.key?(key.to_sym) || message.key?(key) }
28
32
 
29
- message.reject { |key, _| key.to_s == KEY.to_s }
33
+ message.reject { |key, _| DISPLAY_ONLY.include?(key.to_s) }
30
34
  end
31
35
 
32
36
  def self.strip_all(messages)