samagotchi 0.91.0 → 0.93.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 (110) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +88 -1
  3. data/README.md +5 -2
  4. data/bin/chi +8 -8
  5. data/docs/architecture.md +20 -16
  6. data/docs/bundle-authoring.md +2 -1
  7. data/docs/cli.md +5 -5
  8. data/docs/configuration.md +3 -1
  9. data/docs/context.md +5 -3
  10. data/docs/guardrails.md +4 -5
  11. data/docs/hooks.md +3 -3
  12. data/docs/internals/background-tasks.md +2 -2
  13. data/docs/internals/bundles.md +4 -4
  14. data/docs/internals/engine.md +27 -8
  15. data/docs/internals/tool-guardrails.md +10 -2
  16. data/docs/memory.md +6 -0
  17. data/docs/plugins.md +30 -13
  18. data/docs/releasing.md +3 -0
  19. data/docs/sessions.md +4 -4
  20. data/docs/testing.md +2 -1
  21. data/lib/samagotchi/bootstrap/probe.rb +2 -2
  22. data/lib/samagotchi/bridge.rb +8 -9
  23. data/lib/samagotchi/bundle_command/check.rb +43 -12
  24. data/lib/samagotchi/bundle_command/scaffold.rb +12 -0
  25. data/lib/samagotchi/bundle_command/test_run.rb +12 -0
  26. data/lib/samagotchi/bundle_command/try/command.rb +164 -0
  27. data/lib/samagotchi/bundle_command/try/render.rb +80 -0
  28. data/lib/samagotchi/bundle_command/try/run.rb +35 -17
  29. data/lib/samagotchi/bundle_command/try/sandbox.rb +113 -34
  30. data/lib/samagotchi/bundle_command/try/trace.rb +20 -26
  31. data/lib/samagotchi/bundle_command.rb +11 -213
  32. data/lib/samagotchi/bundle_session_settings.rb +82 -45
  33. data/lib/samagotchi/bundles/core/manifest.yml +2 -2
  34. data/lib/samagotchi/bundles/cost-cap/manifest.yml +6 -5
  35. data/lib/samagotchi/bundles/cost-cap/plugin.rb +57 -12
  36. data/lib/samagotchi/bundles/system/manifest.yml +1 -1
  37. data/lib/samagotchi/child_env.rb +50 -0
  38. data/lib/samagotchi/child_reports.rb +12 -14
  39. data/lib/samagotchi/client_id.rb +18 -0
  40. data/lib/samagotchi/config/restart_watch.rb +112 -0
  41. data/lib/samagotchi/config.rb +18 -59
  42. data/lib/samagotchi/engine.rb +26 -26
  43. data/lib/samagotchi/guardrails/data_readers.rb +34 -30
  44. data/lib/samagotchi/guardrails/parent_levels.rb +31 -0
  45. data/lib/samagotchi/guardrails/parent_policy.rb +9 -20
  46. data/lib/samagotchi/guardrails/shell_lex.rb +28 -7
  47. data/lib/samagotchi/hook_question.rb +21 -0
  48. data/lib/samagotchi/hook_routes.rb +6 -6
  49. data/lib/samagotchi/hooks/registry.rb +7 -5
  50. data/lib/samagotchi/host_registry.rb +1 -1
  51. data/lib/samagotchi/kernel_loop.rb +14 -3
  52. data/lib/samagotchi/llm/anthropic_messages.rb +5 -0
  53. data/lib/samagotchi/llm/chat_loop.rb +3 -1
  54. data/lib/samagotchi/llm/errors.rb +4 -4
  55. data/lib/samagotchi/memory_bundle/builder.rb +3 -2
  56. data/lib/samagotchi/memory_bundle/installed_bundle.rb +7 -2
  57. data/lib/samagotchi/memory_bundle/installer.rb +1 -0
  58. data/lib/samagotchi/memory_bundle/manifest.rb +42 -8
  59. data/lib/samagotchi/memory_bundle/provenance.rb +2 -1
  60. data/lib/samagotchi/parent_answers_command.rb +6 -6
  61. data/lib/samagotchi/parent_report.rb +12 -0
  62. data/lib/samagotchi/plugin/context.rb +6 -5
  63. data/lib/samagotchi/plugin/http_transport.rb +14 -3
  64. data/lib/samagotchi/plugin/sessions.rb +2 -3
  65. data/lib/samagotchi/question_desk.rb +12 -9
  66. data/lib/samagotchi/reply_wait/poll.rb +267 -0
  67. data/lib/samagotchi/reply_wait.rb +51 -190
  68. data/lib/samagotchi/sandbox_layout.rb +66 -0
  69. data/lib/samagotchi/send_command/parent_gate.rb +83 -0
  70. data/lib/samagotchi/send_command/wait_target.rb +55 -0
  71. data/lib/samagotchi/send_command.rb +34 -103
  72. data/lib/samagotchi/session.rb +10 -9
  73. data/lib/samagotchi/session_inbox.rb +31 -8
  74. data/lib/samagotchi/session_manager.rb +60 -35
  75. data/lib/samagotchi/session_metrics.rb +22 -3
  76. data/lib/samagotchi/sessions_command.rb +4 -4
  77. data/lib/samagotchi/task_watch.rb +64 -21
  78. data/lib/samagotchi/terminal_ui/attached_loop.rb +3 -3
  79. data/lib/samagotchi/terminal_ui/formatting.rb +8 -5
  80. data/lib/samagotchi/testing/bundle.rb +33 -20
  81. data/lib/samagotchi/testing/host.rb +2 -8
  82. data/lib/samagotchi/testing/install.rb +26 -17
  83. data/lib/samagotchi/testing/recorder.rb +6 -9
  84. data/lib/samagotchi/testing/sandbox.rb +45 -37
  85. data/lib/samagotchi/testing/waiting.rb +5 -7
  86. data/lib/samagotchi/tool_response.rb +28 -6
  87. data/lib/samagotchi/tools/delegate.rb +10 -10
  88. data/lib/samagotchi/tools/delegate_cursor.rb +32 -8
  89. data/lib/samagotchi/tools/delegate_relay.rb +31 -16
  90. data/lib/samagotchi/tools/delegate_wait.rb +4 -4
  91. data/lib/samagotchi/tools/task_runtime.rb +2 -2
  92. data/lib/samagotchi/turn_checkpoint.rb +23 -10
  93. data/lib/samagotchi/version.rb +1 -1
  94. data/lib/samagotchi/web/app.rb +46 -63
  95. data/lib/samagotchi/web/public/app.js +130 -554
  96. data/lib/samagotchi/web/public/cards_place.js +4 -1
  97. data/lib/samagotchi/web/public/images.js +6 -0
  98. data/lib/samagotchi/web/public/index.html +1 -1
  99. data/lib/samagotchi/web/public/model_pick.js +10 -0
  100. data/lib/samagotchi/web/public/send_turn.js +173 -0
  101. data/lib/samagotchi/web/public/session_actions.js +203 -0
  102. data/lib/samagotchi/web/public/start_page.js +236 -0
  103. data/lib/samagotchi/web/public/toasts.js +17 -9
  104. data/lib/samagotchi/web/public/unqueue.js +20 -4
  105. data/lib/samagotchi/worker.rb +77 -185
  106. data/lib/samagotchi/worker_idle_exit.rb +20 -5
  107. data/lib/samagotchi/worker_inbound.rb +5 -5
  108. data/lib/samagotchi/worker_task_wakes.rb +160 -0
  109. data/lib/samagotchi/worker_wakes.rb +0 -3
  110. metadata +15 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e4a20c3555c1f3e592919853f9dc59a5067e7e42251d11dad2f3be844f118f90
4
- data.tar.gz: 817bb797b870985aeb21789377eae7770225265d03c527bb9218a1ecd3c2376e
3
+ metadata.gz: 7fb0f158cad212e0caef1636e67b16290dd85d750a906a04e409adf9c29511ea
4
+ data.tar.gz: e1ff65cd0228751f4adcd8601492a952070cc59d415d324e7908fdd3f998e61e
5
5
  SHA512:
6
- metadata.gz: 74b69c624e9e43882940e48bdf00d0bedb1172fd53f4094c30ef4250e15a4e7ae9fedba4fc9704b923d3b622bcad47950432d450c1c3c219f42a54186dacad58
7
- data.tar.gz: ac112286f3eaa6364bd68cee410e4b580f8246215222a17ecd3781f2980d828eb12d47cc3b64c800e201f9a81662a0c2ee61627a1c485e4249f409c771b65744
6
+ metadata.gz: b58b31c103e24a12b880dbeeae67be626b85b8478c87a19ba9e36440d10e07b34b3f4d615d5f3fceb5518a76172643736efc66263646cc0f75c919186b597bda
7
+ data.tar.gz: 05e2fc82913cafd6bc7a80fb0bc625b8f235b9008a78955b146a857e8f2b6c9e9b7c6c5a5717f31c2df32f14b17e4bc98781e68f8d160b95a657c1071dd85484
data/CHANGELOG.md CHANGED
@@ -8,6 +8,91 @@ and commands may change between minor versions. How releases are made:
8
8
 
9
9
  ## [Unreleased]
10
10
 
11
+ ## [0.93.0] - 2026-10-20
12
+
13
+ ### Changed
14
+
15
+ - A session open in a chi REPL is refused with one message everywhere: `session ID is open in a chi REPL; close it
16
+ there first` (chi web's stop, restart, turn and command, `chi sessions stop|restart`, chi send). Before, some
17
+ places said `owned by an interactive TUI`.
18
+ - Plugins: `ctx.http` turns only network, TLS and provider errors into `HttpError`; any other error (a bug in chi)
19
+ is logged with its backtrace and raised as it is, instead of looking like a failed request. In the bundle test
20
+ kit, `bundle.questions` entries are `Samagotchi::HookQuestion` values (same fields as before).
21
+
22
+ ### Fixed
23
+
24
+ - Guardrail rules' `command:` keeps a `cat`/`tee` heredoc body after an `exec` that sends the shell's output to a
25
+ file (`exec >run.sh; cat <<EOF …`, `exec 3>run.sh; cat <<EOF >&3 …`, also inside `{ }`, `if`, a function, or
26
+ with `exec {fd}>file` / `3<>file`): the body may be run later, as with `cat <<EOF >run.sh`; before, it was left
27
+ out as data.
28
+ - A config.yml that doesn't parse (saved mid-edit) no longer tells a worker or chi web that every start-only setting
29
+ changed and needs a restart: it warns once that the file doesn't parse, and the settings are compared again once
30
+ it does.
31
+ - A background delegate's relayed approval is reported to the parent's model as `approval relayed to your user:
32
+ execute: git push → …`, as a waiting parent already got it; before, it read `hook question relayed…` with an
33
+ empty label. A relayed question from a delegate's own delegate is named by its header (`Cost cap`), not the
34
+ outer card's `delegate ab12 asks: Cost cap`.
35
+ - `chi send --wait` on a queued image message that is removed (unqueued) as the wait looks no longer returns the
36
+ running turn's reply: the removal writes its tombstone before it takes the file, so the wait reads it as removed.
37
+
38
+ Update with `chi update` (the system bundle 0.93.0), then restart `chi web`.
39
+
40
+ ## [0.92.0] - 2026-10-19
41
+
42
+ ### Added
43
+
44
+ - A bundle's manifest can type its session settings (`session_settings: {cap: number}`: `number`, `boolean`,
45
+ `string` or `any`); `--set` and `POST /api/sessions` refuse a value of another type (exit 2 / 400), and a
46
+ `string` key keeps its text as given.
47
+
48
+ ### Changed
49
+
50
+ - `--set BUNDLE.KEY=VALUE` (and `bundle_settings` strings in `POST /api/sessions`) type values as config.yml does:
51
+ for a bundle that lists its keys plainly (or types them `any`), `yes`/`on` are now `true`, `no` is now `false`
52
+ (any case: `No`, `ON`), and `.5`, `5.`, `1e3` are now numbers; before they reached the plugin as text. A bundle
53
+ that wants the text types the key `string`.
54
+ - Test kit: `Samagotchi::Testing::Install.run(...).registered` is an `Install::Registered` value
55
+ (`registered.commands`, `.tools`, `.hooks`, `.inits`, `.toolkits`, `.tool_sources`, `.peer_sources`), its commands
56
+ and hooks `Install::Command` / `Install::Hook` values, not a Hash of arrays: a bundle spec reading
57
+ `registered[:commands]` needs `registered.commands`.
58
+ - Test kit: `Install::Result#ok?` is gone; `result.failures.empty?` says the same.
59
+
60
+ ### Fixed
61
+
62
+ - A Ctrl-C during a turn keeps what the turn did, as a Stop does: its finished tool calls and their results stay in
63
+ the session with the cancel note (a call it cut short reads as interrupted), so the next turn knows what already
64
+ ran and the note lists the background tasks the turn started. `chi bundle try --timeout` now shows that turn's
65
+ tool calls with their arguments.
66
+ - `chi --help` lists `chi sessions restart`, `chi bundle trash` and the bundle authoring subcommands (`new`, `test`,
67
+ `check`, `try`).
68
+ - Web: removing a queued message (its ×) and opening another session before the worker answered no longer puts the
69
+ removed text into that session's composer; a toast names the session it came from and stays until
70
+ dismissed, with Copy for the full text.
71
+ - Web: images added to the composer while a message with images was still uploading or sending stay after it is sent,
72
+ instead of vanishing with the sent ones.
73
+ - Web: when the model list can't be read, the start page says so in a toast (a new session starts on the default
74
+ model) instead of just leaving out the model picker.
75
+ - `--set cost-cap.cap=.5` and `=1e1` set a cap (they were read as text and turned the cap off); `--set
76
+ cost-cap.cap=five` is refused (exit 2) instead of silently running without a cap. A `cap:`, `child:` or `default:`
77
+ in config.yml that isn't a number (or is infinite, `1e400`) now warns once, and a bad `cap:`/`child:` falls back to
78
+ the next one. Run `chi update` (cost-cap 0.2.1).
79
+ - An attached-context wake turn stopped before the model call keeps its note as a plain one and pauses wakes until
80
+ you write, as delegate and task wake turns do; it left the note marked as a turn's start.
81
+ - A background task that finished just as the idle worker was about to exit still wakes the session (the worker
82
+ stays up for its wake); one that finished before the worker left for another reason is a note for the next turn.
83
+ A task whose turn you stopped no longer keeps the worker up or holds `/exit`.
84
+ - `chi send --wait` on a session command delivered as a file reads the command's real outcome (failed, or canceled
85
+ when it was refused mid-turn or dropped), not always completed, and says so as the command's ("the command
86
+ failed"), not a turn's.
87
+ - `chi bundle try` no longer signals its child's process group after the child exited (the group id may name
88
+ another process by then), and no longer waits 0.1 s after every run. A `--timeout` stops the background tasks the
89
+ turn started along with the child, and the delegate workers and tasks a run leaves behind are stopped before the
90
+ sandbox is deleted.
91
+ - `chi bundle check` warns (`clash`) about an installed bundle of the same name whose record can't be read, instead
92
+ of skipping the clash check without a word.
93
+
94
+ Update with `chi update` (the system bundle 0.92.0, cost-cap 0.2.1, core 0.3.4), then restart `chi web`.
95
+
11
96
  ## [0.91.0] - 2026-10-19
12
97
 
13
98
  ### Added
@@ -3549,7 +3634,9 @@ and long-lived sessions.
3549
3634
  - A macOS desktop helper (`chi desktop install`): a "Send to chi" Service and a
3550
3635
  hotkey panel that send selected text or the clipboard to your sessions.
3551
3636
 
3552
- [Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.91.0...HEAD
3637
+ [Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.93.0...HEAD
3638
+ [0.93.0]: https://github.com/dm1try/samagotchi/compare/v0.92.0...v0.93.0
3639
+ [0.92.0]: https://github.com/dm1try/samagotchi/compare/v0.91.0...v0.92.0
3553
3640
  [0.91.0]: https://github.com/dm1try/samagotchi/compare/v0.90.0...v0.91.0
3554
3641
  [0.90.0]: https://github.com/dm1try/samagotchi/compare/v0.89.0...v0.90.0
3555
3642
  [0.89.0]: https://github.com/dm1try/samagotchi/compare/v0.88.0...v0.89.0
data/README.md CHANGED
@@ -80,7 +80,8 @@ the config. With a config already there, it adds a `hosts:` entry and keeps
80
80
  the rest of the file. Then it installs the `core` bundles (loop-guard,
81
81
  check-in, guardrails, slow-commands, agents-md, cost-cap: the safety set for a
82
82
  small local model, the cost cap inert until you set one) and, on a terminal, offers `dev` (known-names, mcp, btw, skills,
83
- source-links). Edit the
83
+ source-links, github-pr, coordinator, claude-peer, codex-peer, ruby; see
84
+ [Bundle profiles](docs/memory.md#bundle-profiles-core-and-dev)). Edit the
84
85
  file later as in [Configure](#configure); `chi bootstrap --help` has the
85
86
  options.
86
87
 
@@ -216,8 +217,10 @@ once, for the session, for the repo, or for the whole rule in the repo.
216
217
  - [Guardrails](docs/guardrails.md): allow / ask / deny for tool calls, rules, approvals
217
218
  - [Hooks](docs/hooks.md): plugin hooks and bundle hooks
218
219
  - [Plugins](docs/plugins.md): bundle plugins (commands, tools, hooks)
220
+ - [Writing a bundle](docs/bundle-authoring.md): `chi bundle new`, `test`, `check`, `try`, `install --dev`
221
+ - [Testing](docs/testing.md): specs, affected specs, lint, worktrees
219
222
  - [Architecture](docs/architecture.md): Engine, TerminalUI, bridge, web
220
- - Internals: [Engine](docs/internals/engine.md), [Gemma 4 contract](docs/internals/gemma4-contract.md), [prompt caching](docs/internals/prompt-caching.md), [context telemetry](docs/internals/context-telemetry.md), [LLM context: forget layer](docs/internals/llm-context-forget.md), [replay benchmark](docs/internals/llm-context-bench.md), [live runs](docs/internals/llm-context-live.md), [tool output limits](docs/internals/tool-guardrails.md), [background tasks](docs/internals/background-tasks.md)
223
+ - Internals: [Engine](docs/internals/engine.md), [Gemma 4 contract](docs/internals/gemma4-contract.md), [prompt caching](docs/internals/prompt-caching.md), [context telemetry](docs/internals/context-telemetry.md), [LLM context: forget layer](docs/internals/llm-context-forget.md), [replay benchmark](docs/internals/llm-context-bench.md), [live runs](docs/internals/llm-context-live.md), [tool output limits](docs/internals/tool-guardrails.md), [background tasks](docs/internals/background-tasks.md), [bundles](docs/internals/bundles.md), [Anthropic Messages API](docs/internals/anthropic.md)
221
224
 
222
225
  ## Development
223
226
 
data/bin/chi CHANGED
@@ -95,13 +95,13 @@ parser = OptionParser.new do |opts|
95
95
  chi bootstrap [HOST[:PORT]|URL] first setup: find the model server, write config.yml
96
96
  chi scratch [options] a one-time session in this terminal: nothing is kept
97
97
  chi web [--port PORT] [--open] [--scope=all] the web UI
98
- chi sessions <list|stop|archive|unarchive|delete|prune|clean|stats>
98
+ chi sessions <list|stop|restart|archive|unarchive|delete|prune|clean|stats>
99
99
  chi note [--source NAME] [-m TEXT] ID...|--all background context for sessions
100
100
  chi broadcast [-m TEXT] [--all] [--dry-run] | log | deliver a note for every session it may concern
101
101
  chi context <add|push|ls|show|refresh|rm|mute|unmute> live external text attached to sessions
102
102
  chi send [-m TEXT] ID...|--new a message into sessions, as if typed there
103
103
  chi answer ID --question QID --option N answer a question a session waits on
104
- chi bundle <install|upgrade|uninstall|status|diff|list|build>
104
+ chi bundle <install|upgrade|uninstall|status|diff|list|build|trash|new|test|check|try>
105
105
  chi desktop <install|upgrade|uninstall|status> macOS "Send to chi" helper
106
106
  chi models [--format json] [TEXT] the models --model takes, from every host
107
107
  chi self version, paths, model and bundles
@@ -126,8 +126,8 @@ parser = OptionParser.new do |opts|
126
126
  # as --tool's negation); this is its help line.
127
127
  opts.separator " --no-tool NAME Leave this tool out of the toolkit (repeatable; a comma list too)"
128
128
  opts.on("--set BUNDLE.KEY=VALUE", "A bundle setting for this session, over config.yml's bundles: (repeatable; a key the " \
129
- "bundle's manifest lists in session_settings:; numbers, true, false and off are typed, the rest is text; " \
130
- "saved in the session: --set cost-cap.cap=3)") do |v|
129
+ "bundle's manifest lists in session_settings:; numbers and booleans (true, off, no) are typed, the rest is " \
130
+ "text; a value of the wrong type is refused; saved in the session: --set cost-cap.cap=3)") do |v|
131
131
  (options[:sets] ||= []) << v
132
132
  end
133
133
  opts.on("--parent-answers LEVEL", "What a parent agent may answer on this session's questions: " \
@@ -267,14 +267,14 @@ end
267
267
  # --parent-answers: a level in any case (no abbreviations), else a usage
268
268
  # error, as chi send's.
269
269
  if options[:parent_answers]
270
- require "samagotchi/guardrails/parent_policy"
270
+ require "samagotchi/guardrails/parent_levels"
271
271
  levels = Samagotchi::Guardrails::ParentPolicy::ORDER
272
- word = options[:parent_answers].strip.downcase
273
- unless levels.include?(word)
272
+ level = Samagotchi::Guardrails::ParentPolicy.parse_level(options[:parent_answers])
273
+ unless levels.include?(level)
274
274
  warn "Error: --parent-answers takes #{levels.join(", ")}, not #{options[:parent_answers]}"
275
275
  exit 2
276
276
  end
277
- options[:parent_answers] = word
277
+ options[:parent_answers] = level
278
278
  end
279
279
  if options[:parent_answers] && web
280
280
  warn "Error: --parent-answers is a new session's (chi --parent-answers, chi send --new --parent-answers), not chi web's"
data/docs/architecture.md CHANGED
@@ -301,10 +301,10 @@ process that already owns the `Engine`); every worker starts it, and it exposes:
301
301
  An optional `deadline` (epoch seconds; `BridgeClient` sends 5/6 of its read timeout ahead) makes
302
302
  a request read after it (a worker frozen by sleep or SIGSTOP) answer `408 deadline_passed` and
303
303
  not run: a client that timed out has said the message was not sent. `/answer`,
304
- `/question/dismiss` and `/command` take the same `deadline` (a command is checked with the event
305
- log held, as a turn is), and the Bridge logs `turn_expired`, `answer_expired`, `dismiss_expired`
306
- or `command_expired`. The web app answers either kind of timeout with `504 worker_timeout`
307
- ("… so the command was not run"). `/cancel`, `/recap` and `/exit` take none.
304
+ `/question/dismiss`, `/command`, `/turn/dequeue` and `/exit` take the same `deadline` (a command is
305
+ checked with the event log held, as a turn is), and the Bridge logs `turn_expired`, `answer_expired`,
306
+ `dismiss_expired`, `command_expired`, `dequeue_expired` or `exit_expired`. The web app answers either
307
+ kind of timeout with `504 worker_timeout` ("… so the command was not run"). `/cancel` and `/recap` take none.
308
308
  The web relays answer, dismiss, command and cancel the same way (`App#relay`): a 408 or a read
309
309
  timeout is `504 worker_timeout`; a Bridge 404 for a route an older worker lacks is
310
310
  `501 not_supported` with the restart hint; a Bridge `404 unknown_session`, no bridge or a
@@ -330,13 +330,13 @@ process that already owns the `Engine`); every worker starts it, and it exposes:
330
330
  saved messages, or the running turn's accumulator); answers once the
331
331
  task is gone: `200 {status, stop_reason, task_id}`, `404 task_not_found`, `409 not_running`.
332
332
  The worker names it in its sidecar features (`task_stop`).
333
- - `POST /session/:id/turn/dequeue` — `{enqueued_id, client_id?}`: take back a queued message
333
+ - `POST /session/:id/turn/dequeue` — `{enqueued_id, client_id?, deadline?}`: take back a queued message
334
334
  (one that waits for the running turn's end: `--queue`, Alt+Enter, `/queue TEXT`, or a message
335
335
  with images) before a turn takes it (web route `POST /api/sessions/:id/turn/:enqueued_id/dequeue`,
336
336
  the × on a queued bubble; `/unqueue` in the attached TUI). Under the event log, like its enqueue:
337
- `SessionInbox.take_queued_input` claims the file as the drain does (one rename wins), deletes it,
338
- leaves a tombstone (`input/removed/<enqueued_id>`, for `ReplyWait`'s `turn:`), and the Bridge
339
- announces `:turn_dequeued`. `200 {status: "removed", enqueued_id, prompt}`, `409 already_started`
337
+ `SessionInbox.take_queued_input` writes a tombstone (`input/removed/<enqueued_id>`, for `ReplyWait`'s
338
+ `turn:`), claims the file as the drain does (one rename wins; a lost claim takes the tombstone back),
339
+ deletes it, and the Bridge announces `:turn_dequeued`. `200 {status: "removed", enqueued_id, prompt}`, `409 already_started`
340
340
  (a turn took it), `409 not_queued` (a next-step message: a drain may merge it any moment), `404
341
341
  unknown_enqueued_id`. It never wakes the worker. The worker names it in its sidecar features (`unqueue`).
342
342
  - `POST /session/:id/relay` — the approval relay, on a delegated child's worker
@@ -356,11 +356,13 @@ process that already owns the `Engine`); every worker starts it, and it exposes:
356
356
  (the summaries the next turn runs under) when the command changed them or the model, and
357
357
  `confirm: {line, text}` when a `/thinking` or `/reasoning-history` change from a `web:` or `tui:`
358
358
  client asks first (`CacheConfirm`; nothing changed, `line` applies it).
359
- - `POST /session/:id/exit` — ask the worker to exit now (`{client_id:}`). The worker checks with
359
+ - `POST /session/:id/exit` — ask the worker to exit now (`{client_id:, delete?, restart?, deadline?}`; `delete`:
360
+ `/exit --delete`, no recap written; `restart: true`: hand the session to a new worker on the newest chi
361
+ installed, `200 {status: "restarting"}`; an empty session's `exiting` carries `discard: true`). The worker checks with
360
362
  the event log held (`WorkerIdleExit#hold_for_request`): `200 {status: "exiting"}` and it leaves
361
363
  like an idle exit, or `409 {status: "held", reason:}` with `turn_running`, `input_queued`,
362
364
  `continue_offered`, `client_connected` (a stream not named by the asker), `reminders`,
363
- `background_tasks` (with `tasks: [{id, command}]`: tasks a finish wake watches) or `starting`.
365
+ `background_tasks` (with `tasks: [{id, command}]`: the running tasks a finish wake watches) or `starting`.
364
366
  - `GET /session/:id/state` — `session_state_snapshot` (JSON).
365
367
  - `GET /session/:id/stats` — `Engine#stats_snapshot` for attached `/stats`: the metrics, with the context window and prompt profile asked from the server before the first turn.
366
368
  - `GET /session/:id/snapshot` — the snapshot frame's content as one request (the web server renders
@@ -581,22 +583,24 @@ A map of the parts the overview above doesn't name (paths under `lib/samagotchi/
581
583
 
582
584
  | Component | Main file(s) | What it is |
583
585
  |---|---|---|
584
- | Worker | `worker.rb` (`worker_idle_exit.rb`, `worker_inbound.rb`, `worker_signals.rb`, `worker_wakes.rb`, `task_watch.rb`) | A session's background process: builds the `Engine` and `Bridge`, runs queued turns and the wake turns nobody typed (delegate reports, a background task that finished unseen: [Finish wake](internals/background-tasks.md#finish-wake), attached context), exits when idle. A spawned one traps TERM, HUP and INT (`WorkerSignals`, installed by `run_session_loop` when `SAMAGOTCHI_WORKER_PROCESS=1`): the running turn stops as a Stop with reason `signal`, is saved, and the worker leaves (`:signal`, exit 0; a new worker only for input already queued). A running turn is checkpointed at its iteration boundaries (`TurnCheckpoint`, the `interrupted_turn` mark), and the next worker settles a turn its predecessor died in (`TurnCheckpoint.settle`). The session is saved from the turn's thread, Bridge threads and a signal's thread; `Session#save` takes a per-session lock around its snapshot and write, so an older snapshot never lands over a newer one. |
585
- | Config | `config.rb` | Every setting as an `Entry`: env, config.yml and CLI layers ([configuration.md](configuration.md)). |
586
+ | Worker | `worker.rb` (`worker_idle_exit.rb`, `worker_inbound.rb`, `worker_signals.rb`, `worker_wakes.rb`, `worker_task_wakes.rb`, `task_watch.rb`) | A session's background process: builds the `Engine` and `Bridge`, runs queued turns and the wake turns nobody typed (delegate reports, a background task that finished unseen: [Finish wake](internals/background-tasks.md#finish-wake), attached context), exits when idle. A spawned one traps TERM, HUP and INT (`WorkerSignals`, installed by `run_session_loop` when `SAMAGOTCHI_WORKER_PROCESS=1`): the running turn stops as a Stop with reason `signal`, is saved, and the worker leaves (`:signal`, exit 0; a new worker only for input already queued). A running turn is checkpointed at its iteration boundaries (`TurnCheckpoint`, the `interrupted_turn` mark), and the next worker settles a turn its predecessor died in (`TurnCheckpoint.settle`). The session is saved from the turn's thread, Bridge threads and a signal's thread; `Session#save` takes a per-session lock around its snapshot and write, so an older snapshot never lands over a newer one. |
587
+ | Config | `config.rb`, `config/restart_watch.rb` | Every setting as an `Entry`: env, config.yml and CLI layers ([configuration.md](configuration.md)); the notice a worker or chi web gets when config.yml changes a setting it reads only at its start. |
586
588
  | Paths | `paths.rb` (`Paths`) | The XDG base dirs chi's files live under, read from the env on every call: `Paths.state_home` (`$XDG_STATE_HOME`, else `~/.local/state`) and `Paths.config_home` (`$XDG_CONFIG_HOME`, else `~/.config`), both defaults under `Dir.home`; a blank variable counts as unset, a relative one is made absolute from the current directory. `Paths.tilde` shows a path with `~` for the home folder. `SessionManager` hands a spawned worker the spawner's `XDG_STATE_HOME` / `XDG_CONFIG_HOME`, when set, made absolute, so a relative one isn't read from the session's folder. |
587
589
  | MemoryBundle | `memory_bundle.rb`, `memory_bundle/{installer,builder,installed_bundle,plugin_ref,bundle_hook}.rb` (`Installer`, `Builder`, `InstalledBundle`, `PluginRef`, `BundleHook`) | Installing, building and reading bundles of memories, hooks and plugins. |
588
- | 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)). |
590
+ | Bundle authoring | `bundle_command.rb` (`BundleCommand`), `bundle_command/{scaffold,check,test_run}.rb`, `bundle_command/try/{run,sandbox,trace}.rb`, `testing.rb` + `testing/` (the test kit) | `chi bundle new`, `check`, `test` and `try`, and the kit a bundle's tests load it with ([bundle-authoring.md](bundle-authoring.md), [internals/bundles.md](internals/bundles.md)). |
591
+ | Bundle session settings | `bundle_session_settings.rb` (`BundleSessionSettings`) | A session's own `--set BUNDLE.KEY=VALUE` values, checked against the manifest's `session_settings:`, laid over `bundles: <name>:` in `ctx.settings` ([plugins.md](plugins.md)). |
592
+ | Plugin API | `plugin/api.rb` (`Plugin::Api`), `plugin/tool_spec.rb` (`Plugin::ToolSpec`: a checked tool declaration, how it is registered), `plugin/loader.rb`, `plugin/context.rb` (`ctx`), `plugin/http.rb` + `plugin/http_transport.rb` (`ctx.http`), `plugin/card.rb` (`Plugin::Card`) | What a bundle's plugin registers: tools, commands, annotation actions, hooks, services, toolkits, peer sources; what its handlers get as `ctx` ([plugins.md](plugins.md)). |
589
593
  | 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)). |
590
594
  | Shipped bundles | `bundles/` (`mcp`, `github-pr`, `loop-guard`, `skills`, `source-links`, …) | Bundles that ship with chi. |
591
595
  | 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)). |
592
- | 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)). |
593
- | 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)). |
596
+ | Guardrails | `guardrails.rb`, `guardrails/shell_lex.rb`, `guardrails/data_readers.rb`, `guardrails/rules.rb`, `guardrails/protected_paths.rb`, `guardrails/parent_policy.rb` + `guardrails/parent_approvals.rb`, `guardrail_wiring.rb` | The gate before every tool call: shell lexing (and the commands that only read a heredoc as data), YAML rules, the core checks on chi's own files, approvals, what a parent agent may answer (`guardrails.parent_answers`) ([guardrails.md](guardrails.md)). |
597
+ | Delegation | `tools/delegate.rb`, `tools/delegate_result.rb`, `tools/delegate_wait.rb`, `child_reports.rb`, `tools/delegate_relay.rb`, `relay_desk.rb`, `relay_verifier.rb`, `relay_watcher.rb` | Child sessions a session starts and waits on, and their approval relay ([sessions.md](sessions.md#delegating)). |
594
598
  | 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)). |
595
599
  | Idle work | `idle_scheduler.rb` (`IdleScheduler`), `idle_recap.rb` (`IdleRecap`), `idle_reminders.rb` | One poller for an idle session's recap and reminders. |
596
600
  | Attached context | `context_sources.rb` (`ContextSources`), `context_poller.rb`, `context_absorber.rb`, `peer_sources.rb` (`PeerSources`: the sources peer bundles declare) | External sources a session keeps fresh, and the peer loop's wakes ([context.md](context.md)). |
597
601
  | LLM context strategies | `llm_context_strategy.rb`, `llm_context_view.rb`, `llm_context_stale.rb`, `llm_context_forget.rb` | What a request sends of the history (none, stale, forget) ([internals/llm-context-forget.md](internals/llm-context-forget.md)). |
598
602
  | Session chains | `session_continue.rb` (`SessionContinue`), `session_chain.rb` (`SessionChain`) | A session that continues an earlier one. `SessionChain.opener_for` gives the text web `continue →` prefills in the new link: the previous link's own first typed message when that link continues one itself, else nil (the 201's `opener`; not saved) ([sessions.md](sessions.md)). |
599
- | Analytics | `analytics_file.rb` (`AnalyticsFile`), `session_metrics/totals.rb` (`SessionMetrics::Totals`), `session_metrics/model_segment.rb` (`SessionMetrics::ModelSegment`) | A session's `analytics.json` (`SessionMetrics#persist`); the sums over its turn records (`Totals`: tokens, cost, speeds, kept as running counters) and their split per model after a `/model` switch (`ModelSegment`: a run of turns on one model, the `model_segments` rows). `session_metrics/side_totals.rb` (`SessionMetrics::SideTotals`) and `side_usage.rb` (`SideUsage`): the side requests' sums, apart from the turns'. |
603
+ | Analytics | `analytics_file.rb` (`AnalyticsFile`), `session_metrics/totals.rb` (`SessionMetrics::Totals`), `session_metrics/model_segment.rb` (`SessionMetrics::ModelSegment`) | A session's `analytics.json` (`SessionMetrics#persist`); the sums over its turn records (`Totals`: tokens, cost, speeds, kept as running counters) and their split per model after a `/model` switch (`ModelSegment`: a run of turns on one model, the `model_segments` rows); `session_metrics/cost.rb` (`SessionMetrics::Cost`): the spend `ctx.cost` reads. `session_metrics/side_totals.rb` (`SessionMetrics::SideTotals`) and `side_usage.rb` (`SideUsage`): the side requests' sums, apart from the turns'. |
600
604
  | Images / vision | `image_store.rb` (`ImageStore`), `vision_support.rb`, `vision_context.rb` | Stored prompt images and whether a model takes them. |
601
605
  | 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)). |
602
606
  | 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)). |
@@ -57,7 +57,8 @@ plugin:
57
57
 
58
58
  `plugin.rb` defines `class Plugin` with `register(chi)`; `initialize(settings)` (optional) gets config.yml
59
59
  `bundles: my-bundle:` as a Hash with string keys, once, when the plugin loads. A value a session may set
60
- (`chi --set my-bundle.KEY=V`, listed in the manifest's `session_settings:`) is read from `ctx.settings` at call
60
+ (`chi --set my-bundle.KEY=V`, listed in the manifest's `session_settings:`, typed with `{KEY: number}` so a
61
+ value of another type is refused) is read from `ctx.settings` at call
61
62
  time. `chi bundle check --set KEY=VALUE` gives the sandboxed load a setting.
62
63
 
63
64
  ## What a hook sees
data/docs/cli.md CHANGED
@@ -83,9 +83,9 @@ chi bootstrap # try localhost 8080, 11434, 1234, 8000
83
83
  or with anchors gets the lines printed to paste instead. `--dry-run` shows
84
84
  what it would write.
85
85
  - **The bundles.** Unless writing the config failed, it installs the system
86
- bundle and the `core` profile (loop-guard, check-in, guardrails, slow-commands), one line
86
+ bundle and the `core` profile (loop-guard, check-in, guardrails, slow-commands, agents-md, cost-cap), one line
87
87
  each; on a terminal it then asks `Also install dev (known-names, mcp, btw,
88
- skills, source-links)? [y/N]`. Run again, it installs nothing new: a bundle
88
+ skills, source-links, github-pr, coordinator, claude-peer, codex-peer, ruby)? [y/N]`. Run again, it installs nothing new: a bundle
89
89
  you uninstalled stays out ([Bundle profiles](memory.md#bundle-profiles-core-and-dev)).
90
90
  A bundle that fails to install makes the exit 1. `--dry-run` says what it
91
91
  would install.
@@ -151,8 +151,8 @@ one experimental JSON object to stdout and human trace lines to stderr; its keys
151
151
  `interactive_blocked`, `used_memories`, `tokens` (`prompt`, `completion`), `cost`, `cost_is_estimate`, `model`,
152
152
  `sandbox`, `denied` (1-based tool indexes), `stderr_tail` (last 4,000 characters) and `timed_out`. A run past
153
153
  `--timeout` is interrupted like a Ctrl-C and still prints the trace it got to, with `timed_out: true` (text output:
154
- the trace lines, then `timed out after N seconds` on stderr). That turn's messages aren't saved, so its tool calls
155
- come from their timing records: name, status and duration, with empty `args`. Exit status: 2 for usage,
154
+ the trace lines, then `timed out after N seconds` on stderr). That turn keeps its tool calls, as a Ctrl-C does, so
155
+ they show with their `args`; a call the interrupt cut short has status `interrupted`. Exit status: 2 for usage,
156
156
  1 for install/turn failure or timeout, 130 for Ctrl-C, otherwise the child's status.
157
157
 
158
158
  `try` runs the bundle's code and the model's tools with your real HOME, account and keys. Its temporary working
@@ -280,7 +280,7 @@ controls exit behavior (`--non-interactive`); `--resume` composes with both.
280
280
  | `--thinking LEVEL` | This session's own thinking level: `off`, `low`, `medium` or `high`, saved in the session and first in the order (`default` unsets it). On `--resume` or `--attach` it is set at once, through a `/thinking` on an attached worker. With `chi web` it is instead the default for the sessions its workers run, below a session's own (as env `SAMAGOTCHI_THINKING_LEVEL` is). See "Thinking" in configuration.md. |
281
281
  | `--toolkit NAME`, `--tool NAME`, `--no-tool NAME` | A new session's toolkit, the tools its prompts declare: `full` (every built-in), `lean`, `child`, one of config's `toolkit.kits` or a bundle's (without `--toolkit`: config's `toolkit.default`), with tools declared on top of it (`--tool`) or left out (`--no-tool`); both repeatable, a comma list too. Saved in the session as given; a respawned worker, a fork and a continue keep it. Ignored with `--attach`/`--resume` (one line, as `--memory`); `/toolkit` changes it. Not on `chi web` (config's `toolkit.default` covers its sessions). See [Toolkits](configuration.md#toolkits). |
282
282
  | `--parent-answers LEVEL` | A new session's own `guardrails.parent_answers` (`hard`, `standard` or `soft`): what a parent agent may answer on its questions. It may only tighten config.yml's: the stricter of the two applies, and a looser one is saved as given with a warning (`--parent-answers soft is looser than config.yml's guardrails.parent_answers: standard; standard applies`). Saved in the session; a respawned worker, a fork and a continue keep it, a `delegate` child takes its parent's. Ignored with `--attach`/`--resume` (one line, as `--toolkit`); refused with `chi web`. Only you loosen it: `/parent-answers` in the terminal or the web. See [Guardrails](guardrails.md#answers-from-a-parent-agent). |
283
- | `--set BUNDLE.KEY=VALUE` | A bundle setting for a new session (repeatable; a later one of the same key wins), laid over config.yml's `bundles: <bundle>:` in what the bundle's plugin reads (`ctx.settings`): `--set cost-cap.cap=3`. The value is typed simply: an integer (`3`, `-1`) or a decimal (`2.5`) is a number, `true` is true, `false` and `off` are false, anything else stays the text as given (`010`, `0x10`, `3:30`, `No`, `null`). For cost-cap, `0`, a negative number or `off` is no cap. Only a key the installed bundle's manifest lists in `session_settings:` is taken: a bundle that isn't installed (or whose plugin doesn't load), a key it doesn't list (a record installed before the bundle listed it says to run `chi update` or `chi bundle upgrade NAME`), or another shape (`--set foo`, `--set a.b.c=1`) is an error naming what it takes, exit 2, nothing started. Saved in the session; a respawned worker, a fork and a continue keep it, a delegate child doesn't. Ignored with `--attach`/`--resume` (one line, as `--toolkit`); refused with `chi web`. See [Per-session settings](plugins.md#per-session-settings). |
283
+ | `--set BUNDLE.KEY=VALUE` | A bundle setting for a new session (repeatable; a later one of the same key wins), laid over config.yml's `bundles: <bundle>:` in what the bundle's plugin reads (`ctx.settings`): `--set cost-cap.cap=3`. The value is typed simply: an integer (`3`, `-1`) or a decimal (`2.5`, `.5`, `1e1`) is a number, `true`, `yes` and `on` are true, `false`, `no` and `off` are false (any case, as in config.yml), anything else stays the text as given (`010`, `0x10`, `3:30`, `y`, `null`), and so does every value of a key the manifest types `string`. For cost-cap, `0`, a negative number or `off` is no cap. Only a key the installed bundle's manifest lists in `session_settings:` is taken, with a value of the type it gives the key: a bundle that isn't installed (or whose plugin doesn't load), a key it doesn't list (a record installed before the bundle listed it says to run `chi update` or `chi bundle upgrade NAME`), a value of another type (`--set cost-cap.cap=five`), or another shape (`--set foo`, `--set a.b.c=1`) is an error naming what it takes, exit 2, nothing started. Saved in the session; a respawned worker, a fork and a continue keep it, a delegate child doesn't. Ignored with `--attach`/`--resume` (one line, as `--toolkit`); refused with `chi web`. See [Per-session settings](plugins.md#per-session-settings). |
284
284
  | `--profile NAME` | Prompt profile (`qwen36` or `gemma4`) for every model in this run, over config and the server's template (same as `--model-profile`, env `SAMAGOTCHI_MODEL_PROFILE`). See "Prompt profile" in configuration.md. |
285
285
  | `--memory NAME` | Preload a memory entry into the system prompt (repeatable; a comma list too). Merged under the config.yml `memories:` baseline. Works attached: the list is stored on the session, so its worker builds the same prompt on every respawn. |
286
286
  | `--append-system-prompt FILE` | Put `FILE`'s text into the system prompt, after AGENTS.md and before the memory indexes (repeatable; relative to the shell's directory). Added after the config.yml `append_system_prompt:` list. A missing or unreadable file is an error, exit 1. Saved in the session, so a respawned worker, a fork and a continue keep it; ignored with `--attach`/`--resume` (one line, as `--memory`). See [Appended prompt files](configuration.md#appended-prompt-files). |
@@ -118,7 +118,9 @@ Besides the settings sections, the file holds maps that are read by their
118
118
  own subsystems: `hosts:` (below), `models:` (see "Prompt profile" and
119
119
  "Images"), `model_aliases:`, `hooks:` (see [Hooks](hooks.md)),
120
120
  `guardrails:` (see [Guardrails](guardrails.md)), `bundles:` (a bundle's
121
- settings for its hooks, see [Hooks: Settings](hooks.md#settings)),
121
+ settings for its hooks, see [Hooks: Settings](hooks.md#settings); a session's own
122
+ `--set BUNDLE.KEY=VALUE`, for a key the bundle's manifest lists in `session_settings:`, lays over them, see
123
+ [Per-session settings](plugins.md#per-session-settings)),
122
124
  `memories:` and `append_system_prompt:` (see "Appended prompt files"). Their entry names (host names, model ids, aliases) are yours to
123
125
  choose. The `memories:` list is the persistent baseline for preloaded memory entries —
124
126
  the same name shape as `--memory` (bare name or `scope/name`), merged under
data/docs/context.md CHANGED
@@ -249,8 +249,8 @@ you're away from the terminal. All of these must hold:
249
249
  - the session's worker is up and idle: no turn running, nothing queued, no
250
250
  question or continue offer waiting, not just started;
251
251
  - under `session.max_wakes` turns in a row with no message from you (the
252
- budget delegate reports use too; a turn that failed pauses wakes until you
253
- write);
252
+ budget delegate reports and finished background tasks use too; a turn that
253
+ failed, or was stopped before the model call, pauses wakes until you write);
254
254
  - the source hasn't woken the session in the last 10 minutes (a peer
255
255
  source has its own limit instead: below).
256
256
 
@@ -266,7 +266,9 @@ notifications count it as a finished turn however short.
266
266
  A wake turn that fails goes back to before it and keeps its note as a
267
267
  plain one; the model reads `[SYSTEM: the previous turn failed before any
268
268
  answer: … The wake turn for the change in attached context <name> was not
269
- answered; chi starts no other wake turn until the user writes.]`.
269
+ answered; chi starts no other wake turn until the user writes.]`. One
270
+ stopped before the model call (a Stop, a `before_turn` hook's cancel) goes
271
+ back the same way, its note plain, and pauses wakes too.
270
272
 
271
273
  ### Peer sources: the peer loop
272
274
 
data/docs/guardrails.md CHANGED
@@ -336,11 +336,10 @@ the `<<TAG` word and the terminator line stay in the text, so a rule on
336
336
  anything else (`bash`, an interpreter, `ssh`, a wrapper), an unquoted one
337
337
  whose body holds `$(…)` or backticks, a piped one, a file-writing
338
338
  `cat`/`tee` (a `>&2` or `>&-` writes no file; `>& file` does), a
339
- here-string and an unterminated heredoc all stay in it. An fd opened
340
- earlier and written to by number (`exec 3>r.sh; cat <<E >&3`) isn't
341
- followed, so that body counts as data: out of scope here, which is
342
- chi-nah's job for a model that goes looking. Approvals, the approval
343
- question and hooks see the whole command.
339
+ here-string and an unterminated heredoc all stay in it, and so does every
340
+ `cat`/`tee` body after an `exec` that sent the shell's output to a file
341
+ (`exec >r.sh; cat <<E`, `exec 3>r.sh; cat <<E >&3`). Approvals, the
342
+ approval question and hooks see the whole command.
344
343
 
345
344
  `memory: remove` matches a `memory_write` with `remove: true` (it moves a
346
345
  memory into the bundle trash). Nothing else asks about it: a memory's file is
data/docs/hooks.md CHANGED
@@ -590,7 +590,7 @@ hooks:
590
590
  event: after_tool_call
591
591
  on_error: log
592
592
  priority: 100
593
- trust_level: reviewed # reviewed | experimental (default)
593
+ trust_level: reviewed # reviewed | trusted | experimental (default)
594
594
  requires_chi: ">= 0.12.0" # optional: a gem-style requirement, as for plugins
595
595
  needs: [gh] # optional: outside commands the memories use (docs/memory.md#bundles-that-need-outside-commands)
596
596
  ```
@@ -608,12 +608,12 @@ Notes:
608
608
  - Ordering: bundle hooks fire by `(priority, bundle_name, hook_name)` (lower priority first), then plain `config.yml` hooks in registration order.
609
609
  - Settings: a hook class with `initialize(settings = {})` gets the bundle's section of `config.yml` `bundles:` (see [Settings](#settings)).
610
610
  - A bundle can also ship a `plugin.rb` whose `chi.on(event)` blocks are bundle hooks too, next to commands and tools; see [Plugins](plugins.md). There `ctx.notify`, `ctx.steer` and the other `ctx` helpers act as this page's `event[:notify]`… do for the event the block runs for.
611
- - Shipped bundles (each in a profile: `core` or `dev`, see [Bundle profiles](memory.md#bundle-profiles-core-and-dev)): `chi bundle install guardrails` (rules, see [Guardrails](guardrails.md#the-guardrails-bundle)) and `chi bundle install known-names` (a hook, see [Guardrails](guardrails.md#the-known-names-bundle)), `chi bundle install source-links` (a hook: announces source refs, see [The source-links bundle](#the-source-links-bundle)), `chi bundle install btw` (a plugin: `/btw`, see [Plugins](plugins.md#the-btw-bundle)), `chi bundle install mcp` (a plugin: tools from MCP servers, see [Plugins](plugins.md#the-mcp-bundle)) `chi bundle install loop-guard` (a plugin: breaks tool-call loops, see [Plugins](plugins.md#the-loop-guard-bundle)), `chi bundle install check-in` (a plugin: checks on a long turn, see [Plugins](plugins.md#the-check-in-bundle)), `chi bundle install slow-commands` (a plugin: commands that ran past the execute limit are run as tasks next time, see [Plugins](plugins.md#the-slow-commands-bundle)), `chi bundle install agents-md` (a plugin: a nested AGENTS.md is steered into the conversation on the first touch under its directory, see [Plugins](plugins.md#the-agents-md-bundle)), `chi bundle install cost-cap` (a plugin: a per-session $ cap, `/cap`, see [Plugins](plugins.md#the-cost-cap-bundle)), `chi bundle install ruby` (a plugin: an RSpec run with no examples at all, a hint, see [Plugins](plugins.md#the-ruby-bundle)) and `chi bundle install skills` (a plugin: `/skill`, versions of skills, see [Plugins](plugins.md#the-skills-bundle)).
611
+ - Shipped bundles (each in a profile: `core` or `dev`, see [Bundle profiles](memory.md#bundle-profiles-core-and-dev)): `chi bundle install guardrails` (rules, see [Guardrails](guardrails.md#the-guardrails-bundle)) and `chi bundle install known-names` (a hook, see [Guardrails](guardrails.md#the-known-names-bundle)), `chi bundle install source-links` (a hook: announces source refs, see [The source-links bundle](#the-source-links-bundle)), `chi bundle install btw` (a plugin: `/btw`, see [Plugins](plugins.md#the-btw-bundle)), `chi bundle install mcp` (a plugin: tools from MCP servers, see [Plugins](plugins.md#the-mcp-bundle)) `chi bundle install loop-guard` (a plugin: breaks tool-call loops, see [Plugins](plugins.md#the-loop-guard-bundle)), `chi bundle install check-in` (a plugin: checks on a long turn, see [Plugins](plugins.md#the-check-in-bundle)), `chi bundle install slow-commands` (a plugin: commands that ran past the execute limit are run as tasks next time, see [Plugins](plugins.md#the-slow-commands-bundle)), `chi bundle install agents-md` (a plugin: a nested AGENTS.md is steered into the conversation on the first touch under its directory, see [Plugins](plugins.md#the-agents-md-bundle)), `chi bundle install cost-cap` (a plugin: a per-session $ cap, `/cap`, see [Plugins](plugins.md#the-cost-cap-bundle)), `chi bundle install ruby` (a plugin: an RSpec run with no examples at all, a hint, see [Plugins](plugins.md#the-ruby-bundle)) `chi bundle install skills` (a plugin: `/skill`, versions of skills, see [Plugins](plugins.md#the-skills-bundle)), `chi bundle install github-pr` (attached context: the branch's open PR, see [Plugins](plugins.md#the-github-pr-bundle)), `chi bundle install coordinator` (a plugin: parallel work in worktrees and child sessions, see [Plugins](plugins.md#the-coordinator-bundle)) and `chi bundle install claude-peer` / `codex-peer` (a plugin: talk to a Claude Code or Codex session, see [Plugins](plugins.md#the-claude-peer-bundle)).
612
612
  - Installing a bundle executes its hook code at `Engine` startup. Only install bundles you trust, as you would a gem. Hooks are **not** executed at install time (copy-only); they are `module_eval`'d at `Engine.new` inside per-bundle `Samagotchi::Bundles::<name>` namespaces (no top-level `require` collisions). Keep hook files side-effect-free at load time; do work in `#call` — top-level side effects (require, IO, `at_exit`, global assignment) run once per `Engine.new` (class redefinition is idempotent).
613
613
 
614
614
  Lifecycle:
615
615
 
616
- - `chi bundle install <source>` copies the hook files its manifest's `hooks:` map lists (only those; with no `hooks:` map, every `hooks/*.rb`) to `$XDG_CONFIG_HOME/samagotchi/memories/.bundles/<name>/hooks/` (`$XDG_CONFIG_HOME` defaults to `~/.config`) and persists metadata + `trust_level` + `source_commit` (git HEAD) to provenance.
616
+ - `chi bundle install <source>` copies the hook files its manifest's `hooks:` map lists (only those; with no `hooks:` map, every `hooks/*.rb`) to `$XDG_CONFIG_HOME/samagotchi/memories/.bundles/<name>/hooks/` (`$XDG_CONFIG_HOME` defaults to `~/.config`) and persists metadata + `trust_level` + `source_commit` (git HEAD) to provenance. `install --dev <dir>` links them to `<dir>`'s files instead, with no sha256 check ([cli.md](cli.md#writing-a-bundle-new-test-check)).
617
617
  - `Engine.new` loads `config.yml` hooks first, then bundle hooks via `MemoryBundle::Provenance.each_installed(holding: :hooks)` → `Hooks::BundleLoader.load`. Bundle hooks are process-scoped (they survive the per-turn `clear_hooks`; only plain hooks are cleared). Experimental bundles emit a one-line startup warning.
618
618
  - `chi bundle status`, `diff`, `uninstall`, `build` are hook-aware (counts, metadata, removal).
619
619
 
@@ -38,7 +38,7 @@ Behavior:
38
38
 
39
39
  A task the model leaves running (it ends its turn: "I'll check when it's finished") is reported when it ends, so the result doesn't wait for the user's next message. `session.task_wakes` (env `SAMAGOTCHI_SESSION_TASK_WAKES`): `wake` (default), `queue` or `off` (no watch, as before).
40
40
 
41
- - **The watch** (`TaskWatch`, `task_watch.rb`) is the Worker's, in memory: the in-process REPL (`--no-shared`) has none, and a crash loses it (those tasks behave as before). The Worker's Engine observer feeds it the raw `:tool_call_completed` events, the input `Bridge#own_task_ids` reads too. A `task_create` result and an `execute` hand-over add the task (`TaskRuntime.created_id`: a `task_id:` the command printed doesn't count), the hand-over's limit kept for the note. What shows the model the end removes it: a `task_wait` with a final status, or with `wait_result: pattern_matched` (the model chose its own "done" marker); a `task_get` with a final status; any `task_stop`. `task_list` and a `read` of `output.log` don't: a model that read the log still gets the wake once. Tasks a tool starts inside an anytime command (`/btw` and the like) reach the same observer, so they are watched too.
41
+ - **The watch** (`TaskWatch`, `task_watch.rb`) is the Worker's (`WorkerTaskWakes`, `worker_task_wakes.rb`, which writes the notes; the Worker runs the wake turn), in memory: the in-process REPL (`--no-shared`) has none, and a crash loses it (those tasks behave as before). The Worker's Engine observer feeds it the raw `:tool_call_completed` events, the input `Bridge#own_task_ids` reads too. A `task_create` result and an `execute` hand-over add the task (`TaskRuntime.created_id`: a `task_id:` the command printed doesn't count), the hand-over's limit kept for the note. What shows the model the end removes it: a `task_wait` with a final status, or with `wait_result: pattern_matched` (the model chose its own "done" marker); a `task_get` with a final status; any `task_stop`. `task_list` and a `read` of `output.log` don't: a model that read the log still gets the wake once. Tasks a tool starts inside an anytime command (`/btw` and the like) reach the same observer, so they are watched too.
42
42
  - **The idle tick**: with something watched, each idle pass (after delegate reports, before due reminders) refreshes the watched records (`TaskRuntime.get_record`, one `ps` per running task) and takes the finished ones, keeping only the ids the conversation still holds (`TaskRuntime.created_ids_in`: a `!rollback` or a restored turn may have dropped the result that started one; an LLM context strategy can't, since its stale and forget stubs are drawn in the request, never written over the saved result). A note that can't be written puts its task back for the next pass. An empty watch costs nothing. A task that finishes during a turn and is still unseen when the turn ends wakes on the next idle pass. One the user stopped with the web's stop-task button is dropped without a note.
43
43
  - **The note** is a context note under its own header (`ContextNote::TASK_HEADER`; `TaskFinishNote`), `note_id: task-<id>`, `source: task <id8>`:
44
44
 
@@ -56,7 +56,7 @@ A task the model leaves running (it ends its turn: "I'll check when it's finishe
56
56
  - **The wake turn**: the first finished task runs a continue turn from client `task:<id>` (`last_turn.origin: "task"`, labelled `task <id8> finished` in the TUI and the web; the web's bell rings at its end however short), whose first message is the note (`turn_start`, `turn_id`, written before the turn's checkpoint so a restore keeps it). It counts toward `session.max_wakes` with the other wakes (`WorkerWakes#task_open?`: no continue offer, not paused, past the 2 s start grace). Several tasks finishing together: one wake turn, the others plain notes before it.
57
57
  - **A plain note instead** (the same text without the last two lines, no turn start), written at once, with `queue`, the budget spent (the UIs hear `N finished background tasks noted; the model reads them with your next message` once per human input, the flag shared with the other wakes), wakes paused, a continue offer waiting, input queued, an exit asked for, or a **note-only** task: one started or waited on in a turn the user stopped (reasons `user`, `manual`, `ctrl_c`), since a wake telling the model to carry on would undo that Stop. A prompt's turn takes the finished ones as plain notes first.
58
58
  - **Failure**: a wake turn that fails and kept nothing, or is stopped and restored, turns its note back into the plain one (`WorkerInbound#unmark_wake`, as for a context wake) and pauses wakes until the user writes.
59
- - **Idle exit**: with `wake`, a watched task still running holds the worker's idle exit and `/exit` (`WorkerIdleExit`'s `:background_tasks`; the 409 lists the tasks, `{id, command}`, with a `hint` on stopping them, and the attached terminal names them and says to use the web's stop-task button or ask for `task_stop`), as reminders do. With `queue` they don't: nothing would wake for the end, so keeping the worker up isn't worth it; a task that ends after the worker left gets no note (the model sees its end at its next look, as before). A forgotten dev server keeps the worker up until it is stopped; a crash of that server wakes the session.
59
+ - **Idle exit**: with `wake`, a watched task holds the worker's idle exit (`WorkerIdleExit`'s `:background_tasks`) until its wake is taken: while it runs, and once it finished until the next idle pass takes it (`TaskWatch#holding`, in memory, no `ps`), so one that ends between the pass's look and the exit's check still wakes the session. A still-running one holds `/exit` too (`TaskWatch#running`; the 409 lists the tasks, `{id, command}`, with a `hint` on stopping them, and the attached terminal names them and says to use the web's stop-task button or ask for `task_stop`), as reminders do. A note-only task holds neither (no wake would come for it), nor does one whose note, or the look for it (the conversation's task ids unreadable), failed 3 times (`TaskWatch::MAX_FAILURES`: it is still tried at each look, but can't keep the worker up for good). With `queue` nothing holds: nothing would wake for the end, so keeping the worker up isn't worth it. As the worker leaves (any exit but a stop, a signal or an `/exit --delete`), the tasks that finished since the last look are written as plain notes; a task that ends after the worker left gets no note (the model sees its end at its next look, as before). A forgotten dev server keeps the worker up until it is stopped; a crash of that server wakes the session.
60
60
  - **The tool descriptions**: a worker's `execute` and `task_create` say "if you end your turn before it finishes, chi tells you when it does" (left out under `off` and in the REPL), fixed when the worker builds its Engine.
61
61
 
62
62
  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).
@@ -22,9 +22,9 @@ user-facing side (what a bundle owns, profiles, needs) is in [memory.md](../memo
22
22
 
23
23
  | Piece | Role |
24
24
  |---|---|
25
- | `BundleCommand` (`lib/samagotchi/bundle_command.rb`) | `chi bundle install/upgrade/uninstall/status/diff/list/build/trash/new/test/check`; runs the Installer, prints its summary, runs the conflict agent step; refuses a source already installed under another name (`installed_under_another_name?`: a git URL bundle recorded under its repo's name before the manifest named it) |
25
+ | `BundleCommand` (`lib/samagotchi/bundle_command.rb`) | `chi bundle install/upgrade/uninstall/status/diff/list/build/trash/new/test/check/try` (`install --dev`: the Installer's `dev:`, links and `Provenance::DEV_SHA`); runs the Installer, prints its summary, runs the conflict agent step; refuses a source already installed under another name (`installed_under_another_name?`: a git URL bundle recorded under its repo's name before the manifest named it) |
26
26
  | `BundleCommand::Check` (bundle_command/check.rb) | `chi bundle check [PATH] [--fix] [--set K=V]`: a bundle being written, checked as an install takes it (docs/cli.md lists the codes); parses the manifest from `Shas.parseable` (a placeholder sha is a `sha` finding, not a YAML error), reads the user's installed bundles only for `clash`, and installs and loads through `Testing::Install` in a `Testing::Sandbox` with `force: true`, the log's echoes swallowed |
27
- | `BundleCommand::Try` (`bundle_command/try/`) | `chi bundle try PATH -p TEXT`: filtered-config `Sandbox` installs the tried bundle and shipped `guardrails` / `cost-cap` in a separate child, then `Run` starts one non-interactive chi process from its realpath work dir; `TraceReader` reads that session's analytics, cards and session JSON by id (never `Session.list`), joining timing and tool args for text or experimental JSON output (tool records the messages lack, from a turn a `--timeout` interrupt cut, become rows without args; `Run::Result#timed_out` becomes `timed_out`). `Sandbox#child_env` is a Hash, never an ENV mutation; it keeps ordinary user model settings and Keychain access while dropping the spawner's session/guardrail/worker markers. |
27
+ | `BundleCommand::Try` (`bundle_command/try/`) | `chi bundle try PATH -p TEXT`: `Command` (its flags, help and checks) drives the rest and `Render` prints the trace; a filtered-config `Sandbox` (its folders a `SandboxLayout`, the layout `Testing::Sandbox` uses too) installs the tried bundle and shipped `guardrails` / `cost-cap` in a separate child, then `Run` starts one non-interactive chi process from its realpath work dir; `TraceReader` reads that session's analytics, cards (`Bridge::CardStore.saved`, as the web does) and session JSON by id (never `Session.list`), joining timing and tool args for text or experimental JSON output (a turn a `--timeout` interrupt cut keeps its calls like a Ctrl-C, the one cut short answered as interrupted, so its rows come from the saved calls too; `Run::Result#timed_out` becomes `timed_out`). `Sandbox#child_env` is a Hash, never an ENV mutation; it keeps ordinary user model settings and Keychain access while dropping the spawner's session/guardrail/worker markers (`ChildEnv`, the scrub a session's worker spawn uses too). The child runs in its own process group: a `--timeout` sends it INT, then `ProcessGroup.stop` (the groups its processes started too); after any exit the group is signalled only while a straggler is still in it, never a reaped group. `Sandbox#stop_leftovers` (before the trace, and in `cleanup`) stops what left the child's tree, found by the sandbox's own records: a delegate's worker by its session's `owner.lock`, a task by its running `task.json` whose pid still leads its group. |
28
28
  | `BundleCommand::Scaffold` (bundle_command/scaffold.rb) | `chi bundle new NAME`: the starting files (block-form `plugin:` entry, `requires_chi` at this chi, a kit test that passes as written); the manifest's shas are filled through `Shas.refresh` |
29
29
  | `BundleCommand::TestRun` (bundle_command/test_run.rb) | `chi bundle test`: `RbConfig.ruby -I <chi's lib>` on `test/**/*_test.rb` (Minitest) or `spec/` (RSpec, as `rake bundles:specs`), under `Bundler.with_unbundled_env` when Bundler's runtime is loaded |
30
30
  | `Shas` (shas.rb) | a manifest's `sha256:` lines: `.refresh(dir[, text])` rewrites them line by line (the layout kept; a `files:` entry it can't place raises `Shas::Error`), `.stale` → `[Stale(file, old, new)]` (`old` `""` for a placeholder, nil for a plugin / hook / script entry with no sha line), `.parseable` (a placeholder `sha256: sha256:` isn't YAML). Plain Ruby: `rake bundles:sha` / `bundles:check` (`script/release/release_tools.rb`) and `chi bundle check --fix` share it |
@@ -32,7 +32,7 @@ user-facing side (what a bundle owns, profiles, needs) is in [memory.md](../memo
32
32
  | `SourceIdentity` (source.rb) | what a source is, for comparing two installs of one name: `shipped:<dir>` (any gem or checkout), `git:<url>` (no `#ref`, trailing `/` or `.git`), `path:<realpath>`; nil when it can't place one |
33
33
  | `PreparedSource` (source.rb) | one source normalized once (dir, owned, git commit): `BundleCommand` reads the bundle's name from its `manifest.yml` (the name for every kind of source, a git URL too) and hands it to the Installer (`prepared:`), so a source is cloned or extracted once per command |
34
34
  | `MemoryFiles` | which top-level `*.md` files of a bundle are its memories (`.select`: the `files:` list, else all but README/LICENSE and the like), `.unshipped` (listed, not shipped) and `.symlinks` (skipped: a symlink is never a memory) |
35
- | `Manifest` | parses/validates `manifest.yml` (`name:` is the bundle's name and a directory under `.bundles/`, so a path such as `../x`, `a/b`, `.` or `..` is refused: `name: … is not a bundle name`; YAML that doesn't parse or isn't a mapping is a `ValidationError` too; files, hooks as `BundleHook`, plugin, scripts, needs, requires_chi, includes, context_providers, session_settings: the keys a session's `chi --set` may give, recorded at install in `manifest.json` for `BundleSessionSettings` to check) and writes it (`Manifest.write`, for Builder). It ignores `requires_chi_next`, a shipped bundle's release-time marker (`script/release/release_tools.rb`, docs/releasing.md) |
35
+ | `Manifest` | parses/validates `manifest.yml` (`name:` is the bundle's name and a directory under `.bundles/`, so a path such as `../x`, `a/b`, `.` or `..` is refused: `name: … is not a bundle name`; YAML that doesn't parse or isn't a mapping is a `ValidationError` too; files, hooks as `BundleHook`, plugin, scripts, needs, requires_chi, includes, context_providers, session_settings: the keys a session's `chi --set` may give, a list or a mapping of them to types (`SESSION_SETTING_TYPES`), recorded at install in `manifest.json` (`session_settings`, `session_setting_types`) for `BundleSessionSettings` to check) and writes it (`Manifest.write`, for Builder). It ignores `requires_chi_next`, a shipped bundle's release-time marker (`script/release/release_tools.rb`, docs/releasing.md) |
36
36
  | `Installer` | one install/upgrade/dry run of one bundle (below) |
37
37
  | `AssetInstaller` | the parts that live in `.bundles/<name>/`: hooks, guardrail rules, plugin, scripts; returns `AssetInstaller::Installed` (the paths the record keeps) |
38
38
  | `Merger` | 3-way classification per file: `install`, `fast_forward`, `keep`, `noop`, `conflict` |
@@ -63,7 +63,7 @@ The checks live in `Hooks::BundleLoader.load` and `Plugin::Loader.load_bundle`;
63
63
  plugin file: build, `register`, `commit!`), which raise instead of logging: the bundle test kit
64
64
  (`Samagotchi::Testing::Bundle`) loads a bundle's source through them, unchecked. `Samagotchi::Testing::Install` is the
65
65
  checked path in a sandbox: the Installer, then `ExtensionLoad` (bundle hooks, then `load_plugins`), returning the
66
- Installer's warnings, this bundle's load failures and what it registered (`Install.registered`, also the parity
66
+ Installer's warnings, this bundle's load failures and what it registered (`Install.registered`, an `Install::Registered` value, also the parity
67
67
  spec's shape). A load failure carries `LoadFailures::Failure#location` (the first backtrace frame inside the
68
68
  bundle, or a SyntaxError's own line), which `Install` maps from the installed copy (`.bundles/<name>/plugin/`,
69
69
  `hooks/`) back to the source dir.
@@ -97,11 +97,14 @@ StandardError in run_turn:
97
97
  Interrupt (Ctrl-C; the REPL only: a worker traps INT, see the signal row below):
98
98
  → if turn.ended: re-raised only
99
99
  → turn.controller.cancel!(:ctrl_c)
100
- → end_turn(turn, "canceled"): ctrl_c_messages keeps turn.messages with TurnNote.cancelled(:ctrl_c)
101
- # Both loops work on their own copy of the conversation, and Interrupt is not a
102
- # StandardError, so nothing attaches the loop's conversation: only what the Engine
103
- # built before the loop (system head, history, reminders, the prompt) survives.
104
- # Tasks the turn started are missed in the cancel note for the same reason.
100
+ → end_turn(turn, "canceled"): ctrl_c_messages keeps the loop's conversation, else turn.messages
101
+ # (it came before the loop), with TurnNote.cancelled(:ctrl_c) at the tail.
102
+ # Both loops attach their conversation to the Interrupt as to a failure
103
+ # (LLM::FailedTurn.attach), so the turn's finished iterations stay, as after a Stop,
104
+ # and the cancel note names the tasks the turn started. One mid-tool is answered:
105
+ # ToolResponse.run_batch gives every call left an interrupted result (the native
106
+ # loop's batch entry joins them after the finished ones), so no call is kept without
107
+ # its result. A half-streamed reply is not kept (a Stop keeps it, [interrupted]).
105
108
  → :turn_canceled {cancellation_reason: :ctrl_c, duration_ms}
106
109
  → re-raised (the caller decides whether to exit)
107
110
 
@@ -191,7 +194,8 @@ KernelLoop#run(messages, max_iterations:, on_stream_event:, cancel_controller:,
191
194
  ```
192
195
 
193
196
  `turn.conversation` is the loop's own copy, not the Engine's `turn.messages`. A failure attaches it to the error
194
- (`LLM::FailedTurn.attach`), so `Engine#failed_messages` can keep it; a Ctrl-C does not (see Error paths).
197
+ (`LLM::FailedTurn.attach`), so `Engine#failed_messages` can keep it; an Interrupt (Ctrl-C) too, for
198
+ `Engine#ctrl_c_messages` (see Error paths).
195
199
 
196
200
  ### ChatLoop (the chat APIs)
197
201
 
@@ -468,7 +472,9 @@ TUI. Errors in the sink or an observer are caught; they never break the turn.
468
472
 
469
473
  `Engine#announce` puts an event that belongs to no turn (`ANNOUNCEABLE_EVENTS`: `turn_enqueued`, `turn_dequeued`
470
474
  (a queued message taken back before it ran, `{enqueued_id, client_id}`: the remover's), `card`,
471
- `hook_notice`, `guardrail_warning`, `plugin_init_started`, and more) to the observers only.
475
+ `hook_notice`, `guardrail_warning`, `plugin_init_started`, and more) to the observers only. `:side_usage`
476
+ (`{kind, side}`: a side request's usage, the recap's, `/btw`'s or `ask_model`'s, and the session's side sums) goes
477
+ to the observers the same way from `Engine#note_side_usage` (any thread), without being announceable.
472
478
 
473
479
  Turn events (the main ones; `Events` in `events.rb` keeps the shared sets):
474
480
 
@@ -491,6 +497,8 @@ Turn events (the main ones; `Events` in `events.rb` keeps the shared sets):
491
497
  | `:tool_dispatch_started` / `:tool_dispatch_completed` | around a batch | `{iteration, call_count}` |
492
498
  | `:tool_call_started` | before a call runs | Engine event includes `call` (see ToolRunner); the Bridge's SSE frame omits it |
493
499
  | `:tool_call_completed` | after a call ran | see ToolRunner |
500
+ | `:question_requested` / `:question_answered` / `:question_cancelled` | `QuestionDesk`: a question is asked (an approval, `ask_user_question`, a hook's or plugin's `ask_user`, the step-limit Continue), answered, or withdrawn | `{pending_question}` / `{id, answer}` / `{id, reason}` |
501
+ | `:plugin_init_wait` | a turn waits for plugin init tasks that provide tools (`PluginTasks`) | `{tasks: [{bundle, id, label}]}` |
494
502
  | `:used_memories_updated` | a memory was read, and after the loop | `{used_memory_names, read_names?}` |
495
503
  | `:hook_notice` | a hook's or plugin's notify during a turn | `{hook, text, level}` (announced with `between_turns: true` outside a turn) |
496
504
  | `:guardrail_warning` | a guardrails line during a turn | `{message}` |
@@ -561,6 +569,11 @@ Model probes (per turn):
561
569
  Plugins and hook files register hooks on hook points (see "Hook points" above, and docs/hooks.md). Every fire
562
570
  puts the hook runtime on the event: `event[:hook]` (the hook's label) and five helpers, which call the Engine's
563
571
  `Hooks::Runtime` (built by `HookRoutes`, `hook_routes.rb`, which routes each to the UIs). A plugin's `ctx` has the same ones (`ctx.notify`, `ctx.steer`, ...).
572
+ A question takes keywords only at that plugin-facing edge (`event[:ask_user]`, `ctx.ask_user`); from there it is one
573
+ `HookQuestion` (`hook_question.rb`: the question, its options, header, freeform flag, the hook's label and its
574
+ label => effect map) through `Hooks::Runtime#ask_user` / `Plugin::Host#ask_user`, `Engine#hook_ask_user` and
575
+ `HookRoutes#ask_user` to `QuestionDesk#ask_for_hook`, which turns the effects into the pending question's per-option
576
+ tags. The bundle test kit records the same value (`Testing::Recorder#questions`).
564
577
 
565
578
  | Helper | What it does | Used by |
566
579
  |---|---|---|
@@ -681,7 +694,7 @@ prompt starts with what the server has cached).
681
694
  and config.yml hooks survive.
682
695
  3. **Each loop works on its own copy of the conversation.** The Engine's `turn.messages` is what it sent. After
683
696
  a normal end or a Stop it keeps `result.conversation`; after a failure, the copy attached with
684
- `FailedTurn.attach`; after a Ctrl-C, only `turn.messages`.
697
+ `FailedTurn.attach` (a Ctrl-C's too); `turn.messages` only when it ended before the loop.
685
698
  4. **Events are the contract.** UIs read events, not internal state. `on_event` is per turn; observers are
686
699
  persistent.
687
700
  5. **A stable system prompt for the cache.** `SystemPrompt#build` caches the prompt per Engine, keyed by
@@ -739,6 +752,7 @@ Paths are under `lib/samagotchi/`.
739
752
  | `hooks/registry.rb` | Hook registry, fire order, `Hooks::Runtime` |
740
753
  | `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 |
741
754
  | `question_desk.rb`, `pending_question.rb`, `question_answer.rb` | `QuestionDesk`: the one open question (ask, post, answer, cancel, relay mark); `PendingQuestion` and `QuestionAnswer`, its values (Hashes only in events, Bridge JSON, the session file and the hook/plugin `ask_user` result) |
755
+ | `hook_question.rb` | `HookQuestion`: a hook's or plugin's `ask_user` question as one value, from the runtime/host edge to `QuestionDesk#ask_for_hook` |
742
756
  | `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` |
743
757
  | `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`) |
744
758
  | `extension_load.rb` | What an Engine loads at start: config.yml and bundle hooks, plugins, `bundles:` settings; events held while plugins load |
@@ -749,3 +763,8 @@ Paths are under `lib/samagotchi/`.
749
763
  | `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 |
750
764
  | `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)) |
751
765
  | `session_manager.rb` | Background session processes (workers) and their IPC |
766
+ | `worker_wakes.rb` | `WorkerWakes`: a worker's wake budget for the turns nobody typed (`session.max_wakes`) |
767
+ | `task_watch.rb`, `task_finish_note.rb` | `TaskWatch`: the background tasks a worker's session started that the model hasn't seen finish; `TaskFinishNote`: the note one leaves when it does ([background-tasks.md](background-tasks.md#finish-wake)) |
768
+ | `side_usage.rb` | `SideUsage`: one side request's usage (a recap, `ctx.ask_model`), folded into the session's side sums (`:side_usage`) |
769
+ | `bridge/client_event.rb` | `Bridge::ClientEvent.project`: what an SSE client gets for an engine event ([architecture.md](../architecture.md)) |
770
+ | `guardrails/parent_policy.rb` | `Guardrails::ParentPolicy`: which options of a question a parent agent may pick (`guardrails.parent_answers`) |