samagotchi 0.120.0 → 0.121.1

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 (120) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +116 -1
  3. data/bin/chi +3 -2
  4. data/docs/architecture.md +36 -11
  5. data/docs/bundle-authoring.md +2 -1
  6. data/docs/cli.md +10 -4
  7. data/docs/configuration.md +34 -11
  8. data/docs/guardrails.md +48 -19
  9. data/docs/internals/anthropic.md +1 -1
  10. data/docs/internals/background-tasks.md +1 -1
  11. data/docs/internals/participants.md +6 -2
  12. data/docs/internals/prompt-caching.md +7 -6
  13. data/docs/internals/remote-sessions.md +15 -11
  14. data/docs/internals/web-push.md +22 -11
  15. data/docs/plugins.md +28 -8
  16. data/docs/sessions.md +15 -0
  17. data/docs/testing.md +44 -2
  18. data/lib/samagotchi/answer_command.rb +1 -1
  19. data/lib/samagotchi/bridge.rb +8 -8
  20. data/lib/samagotchi/bridge_client/event_stream.rb +11 -27
  21. data/lib/samagotchi/bridge_client/stream_open.rb +79 -0
  22. data/lib/samagotchi/bridge_client/stream_request.rb +8 -2
  23. data/lib/samagotchi/bridge_client.rb +52 -65
  24. data/lib/samagotchi/bridge_secret.rb +9 -7
  25. data/lib/samagotchi/bundle_command/check.rb +27 -2
  26. data/lib/samagotchi/bundles/claude-peer/manifest.yml +2 -2
  27. data/lib/samagotchi/bundles/claude-peer/scripts/pair +21 -7
  28. data/lib/samagotchi/bundles/codex-peer/manifest.yml +2 -2
  29. data/lib/samagotchi/bundles/codex-peer/scripts/pair +14 -3
  30. data/lib/samagotchi/bundles/dev/manifest.yml +2 -2
  31. data/lib/samagotchi/bundles/github-pr/manifest.yml +2 -2
  32. data/lib/samagotchi/bundles/github-pr/plugin.rb +2 -6
  33. data/lib/samagotchi/bundles/mcp/manifest.yml +3 -3
  34. data/lib/samagotchi/bundles/mcp/plugin.rb +5 -1
  35. data/lib/samagotchi/bundles/system/manifest.yml +1 -1
  36. data/lib/samagotchi/child_move.rb +5 -7
  37. data/lib/samagotchi/children_status.rb +2 -1
  38. data/lib/samagotchi/engine.rb +1 -0
  39. data/lib/samagotchi/guardrail_wiring.rb +19 -2
  40. data/lib/samagotchi/guardrails/parent_approvals.rb +9 -0
  41. data/lib/samagotchi/guardrails_command.rb +41 -6
  42. data/lib/samagotchi/image_store.rb +6 -3
  43. data/lib/samagotchi/interrupted_turn.rb +6 -3
  44. data/lib/samagotchi/kernel_loop.rb +5 -0
  45. data/lib/samagotchi/live_versions.rb +6 -7
  46. data/lib/samagotchi/memory_bundle/profile.rb +1 -1
  47. data/lib/samagotchi/memory_bundle/uninstaller.rb +6 -2
  48. data/lib/samagotchi/plugin/api.rb +14 -8
  49. data/lib/samagotchi/plugin/sessions.rb +13 -4
  50. data/lib/samagotchi/push_command.rb +1 -1
  51. data/lib/samagotchi/relay_verifier.rb +2 -3
  52. data/lib/samagotchi/relay_watcher.rb +2 -2
  53. data/lib/samagotchi/remote_attach_command.rb +1 -1
  54. data/lib/samagotchi/reply_wait/poll.rb +8 -2
  55. data/lib/samagotchi/reply_wait.rb +4 -3
  56. data/lib/samagotchi/review_command.rb +1 -1
  57. data/lib/samagotchi/run_options.rb +1 -0
  58. data/lib/samagotchi/session.rb +4 -0
  59. data/lib/samagotchi/session_access/local.rb +18 -8
  60. data/lib/samagotchi/session_access/remote.rb +5 -0
  61. data/lib/samagotchi/session_access/web_bridge_client.rb +31 -19
  62. data/lib/samagotchi/session_access.rb +21 -9
  63. data/lib/samagotchi/session_continue.rb +2 -3
  64. data/lib/samagotchi/session_manager.rb +118 -28
  65. data/lib/samagotchi/sessions_command.rb +10 -5
  66. data/lib/samagotchi/system_one/client.rb +6 -1
  67. data/lib/samagotchi/system_one/fake_backend.rb +0 -2
  68. data/lib/samagotchi/system_one/log.rb +21 -6
  69. data/lib/samagotchi/system_one/self_protect.rb +39 -43
  70. data/lib/samagotchi/terminal_ui/attach_launcher.rb +8 -3
  71. data/lib/samagotchi/terminal_ui/attached_loop.rb +4 -2
  72. data/lib/samagotchi/terminal_ui/local_link.rb +28 -19
  73. data/lib/samagotchi/terminal_ui/question_slot.rb +19 -8
  74. data/lib/samagotchi/tool_declarations.rb +1 -1
  75. data/lib/samagotchi/tool_runner.rb +37 -1
  76. data/lib/samagotchi/toolkit.rb +117 -27
  77. data/lib/samagotchi/toolkit_binding.rb +32 -0
  78. data/lib/samagotchi/toolkit_command.rb +23 -0
  79. data/lib/samagotchi/tools/deferred_tools.rb +10 -2
  80. data/lib/samagotchi/tools/delegate.rb +1 -1
  81. data/lib/samagotchi/tools/delegate_relay.rb +9 -7
  82. data/lib/samagotchi/tools/registry.rb +20 -6
  83. data/lib/samagotchi/tools/tool_index.rb +103 -20
  84. data/lib/samagotchi/turn_checkpoint.rb +15 -9
  85. data/lib/samagotchi/version.rb +1 -1
  86. data/lib/samagotchi/web/app.rb +123 -65
  87. data/lib/samagotchi/web/participant_access.rb +26 -7
  88. data/lib/samagotchi/web/participant_review.rb +11 -6
  89. data/lib/samagotchi/web/participant_view.rb +7 -3
  90. data/lib/samagotchi/web/participants_admin.rb +13 -0
  91. data/lib/samagotchi/web/public/app.js +1 -1
  92. data/lib/samagotchi/web/public/forms.css +12 -0
  93. data/lib/samagotchi/web/public/index.html +7 -5
  94. data/lib/samagotchi/web/public/level_chip.js +2 -2
  95. data/lib/samagotchi/web/public/llm_context_chip.js +4 -4
  96. data/lib/samagotchi/web/public/notifications.js +21 -1
  97. data/lib/samagotchi/web/public/participant.html +4 -2
  98. data/lib/samagotchi/web/public/participant.js +34 -11
  99. data/lib/samagotchi/web/public/participants_panel.js +49 -14
  100. data/lib/samagotchi/web/public/push.js +4 -3
  101. data/lib/samagotchi/web/public/push_devices.js +17 -3
  102. data/lib/samagotchi/web/public/question_card.js +15 -1
  103. data/lib/samagotchi/web/public/question_cards.js +11 -2
  104. data/lib/samagotchi/web/public/review_history.js +24 -4
  105. data/lib/samagotchi/web/public/review_start.js +1 -3
  106. data/lib/samagotchi/web/public/session_actions.js +14 -2
  107. data/lib/samagotchi/web/public/storage.js +16 -0
  108. data/lib/samagotchi/web/public/toolkit_chip.js +3 -0
  109. data/lib/samagotchi/web/push/client.rb +5 -0
  110. data/lib/samagotchi/web/push/store.rb +16 -3
  111. data/lib/samagotchi/web/push_routes.rb +3 -2
  112. data/lib/samagotchi/web/session_access_routes.rb +25 -17
  113. data/lib/samagotchi/web/session_create.rb +4 -4
  114. data/lib/samagotchi/web/session_hub.rb +6 -4
  115. data/lib/samagotchi/web/session_summary.rb +8 -16
  116. data/lib/samagotchi/web/tunnel.rb +10 -1
  117. data/lib/samagotchi/worker.rb +1 -1
  118. data/lib/samagotchi/worker_link.rb +213 -0
  119. data/lib/samagotchi/worker_sidecar.rb +15 -37
  120. metadata +4 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0b242fb85587a862bad996a05e189367e5a4e1b9d025df7a7d335d60b2184fe4
4
- data.tar.gz: 85ababba9d74adb82a892cc96d8e2a9fc430a36caf55e9fa9bf549b0f502d136
3
+ metadata.gz: b07fb49361e67b325eaaf3190d15d3f889a6df2e590585b45257a0a2b1409ac9
4
+ data.tar.gz: 37e6db4dc2235fcfe9127d38261056983f609ebd0dd1a214a903ec94c8afec35
5
5
  SHA512:
6
- metadata.gz: 179d89d141810eccc0038f7eac484775c17cc6b07b80504e4139a6ad0579ebb51fe344619bbb949ad9d5a183c5e82fff90915015915a12bdfac46a5b4589cde2
7
- data.tar.gz: faeb2bde8eab54b4c286f596e9e3005678d1a6aeebc4758af00aa33da12e4ec5492627a6edc4b7f19c7343e361b0501e3ad92ef53edcf7684255241904e14006
6
+ metadata.gz: 8a8e5c43eee0002c4054c6e347f75a795fa09216a7f0ea3db30770043a5eaaf57fa34f9a1ee5a4ddf64117f1e8f3bb5ed4ccf8b34c55bb5c9a2e6398c1fec997
7
+ data.tar.gz: 23714d86e0872fdcba52fb019b30dbf84c4531a3c44a13f0cb30b72cc95bb59506c654cb53ae395b3d8ae5d83672c29fe8624958ef968dc5648b4189d9e8731f
data/CHANGELOG.md CHANGED
@@ -7,6 +7,119 @@ and commands may change between minor versions. How releases are made:
7
7
  [docs/releasing.md](docs/releasing.md).
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.121.1] - 2026-10-27
11
+
12
+ ### Fixed
13
+
14
+ - `chi sessions restart` can recover an unreachable, secret-refusing, newer-protocol or stuck-start worker without a
15
+ stop marker, but refuses when disk shows a running turn, pending question, queued input or running background task.
16
+ - chi web now opens a session stream before answering: a missing, leaving or unreachable worker gets 503, a Bridge
17
+ refusal gets 502 instead of an empty 200 or SSE frame, and the opened socket closes when the stream ends. Remote
18
+ stream retries distinguish 503 from a 502 `bridge_secret`, and `/stats` reports secret refusals consistently.
19
+ - A plugin's `ctx.sessions.fork` reports a child chi won't start as `Samagotchi::Plugin::Sessions::Error` with chi's
20
+ own reason (the session's model is gone from the config, or its toolkit is a `toolkit.kits` kit with an invalid
21
+ `groups:`) instead of handing the bundle the raw `Toolkit::Refused` or `ModelProfile::MissingModel`.
22
+ - A `toolkit.kits` kit whose `groups:` isn't a name or a list (a map such as `{chrome: true}`) no longer loads
23
+ silently with its base's groups: chi, `chi send --new`, chi web and `delegate` refuse a new session on it, naming
24
+ the kit and the reason, and a session already saved on it reaches no tool-index group until config.yml is fixed.
25
+ - A worker's liveness is one rule everywhere, the owner lock plus the sidecar's pid, with no port probe: after a
26
+ `kill -9` whose port another session's Bridge took, `chi send`, `--attach` and chi web no longer take the dead
27
+ worker for live (they wake a new one). A worker on a newer sidecar protocol is not driven by an older chi: a wake
28
+ answers 409 `worker_newer`.
29
+ - System 1 no longer auto-allows a command that changes chi's own config through a spelling its check missed
30
+ (`$XDG_CONFIG_HOME/samagotchi/config.yml`, `$HOME/.config/…`, `memories/../config.yml`, `cd ~/.config && …`): it
31
+ refuses by the same test that makes an ask the user's alone, so such a call goes to the user. Memory files still
32
+ can be auto-allowed.
33
+ - `chi guardrails replay` no longer judges an ask again that System 1 already judged live (audit or on mode), which
34
+ counted each such decision twice and let a rule pass `min_record` early: a live label now carries the card's
35
+ `question_id`, and a live line without one is matched by its session and call digest.
36
+ - Stopping a turn while System 1 judges one of its approvals no longer opens an approval card for the stopped turn
37
+ (nor logs the cancel as a failed judgment): the call is denied with "The turn was stopped".
38
+ - A participant no longer reads a one-time link code (`chc_…`, as in an invite link a reply quotes) or a Bridge's
39
+ `X-Chi-Bridge` secret: both are redacted like `chp_` tokens.
40
+ - An agent participant's `chi --remote … send --wait` goes on after chi web restarts: chi web reads whose turn it is
41
+ from the claimed input file, the last turn or the refused turn's record, not only its memory (it answered 403).
42
+ - chi web's participants routes (list, invite, revoke, cap) answer a grants store chi can't write (unreadable, or a
43
+ newer chi's) with 409 `grants_unwritable` and the store's own message, not a 500 with a backtrace on every refresh.
44
+ - A participant name or an annotate preset holding `\&`, `` \` `` or `\'` no longer breaks chi web's page: it was read
45
+ as a backreference when the page's attributes went in.
46
+ - One push subscription with a `p256dh` key off the P-256 curve no longer stops every push: chi web refuses such a
47
+ subscribe (400 `invalid_subscription`), and one already on disk fails that device alone, so the other devices still
48
+ get the push, `POST /api/push/test` answers that device as failed instead of a 500, and `chi web push test` prints
49
+ its failure instead of a backtrace.
50
+ - The notifications sheet's switches always show what chi web has: "this device" no longer stays on after iOS's
51
+ Don't Allow, nor "Long turns done" moved after its change failed.
52
+ - Turning push off on a flaky connection no longer drops this phone's subscription while chi web keeps the device:
53
+ chi web is told first, and a failure leaves the bell on with the error. The Home Screen app whose first `GET
54
+ /api/push` failed (chi web restarting, a phone just woken) tries again when it comes back to the front instead of
55
+ staying in tab mode until a reload.
56
+ - `chi --help` lists `chi guardrails replay` (seeding System 1's record), the one subcommand it left out.
57
+ - The participants panel's Cap form no longer sets a $0 cap when its field is cleared, nor sends a negative or
58
+ over-$1000 one: it refuses as the Invite's cap field does ("A cap is dollars, 0 to 1000.").
59
+ - A double tap on the web's Stop no longer asks twice (two confirms, or two stops): a second Stop of the same session
60
+ while the first one asks the worker what still runs, or stops it, does nothing.
61
+ - A participant's comment box keeps what they typed while their last comment was on its way (it was cleared when the
62
+ comment was accepted), and an error under the box (a refusal, "Images can't go in a review comment yet") goes at
63
+ their next edit or send instead of staying until a comment went through.
64
+ - An invite whose participants panel closed before chi web answered no longer loses its one-time link (the grant was
65
+ made, so the owner had to revoke and invite again): a sticky toast names it with Copy link.
66
+ - A participant's thread no longer jumps when a read fails and again when it recovers: "Can't reach the session —
67
+ retrying…" (and "This session is gone.") shows over the thread's top instead of as a new row in the header.
68
+ - A participant's wait on a turn whose worker died in it is no longer refused (403) after chi web restarts: the turn's
69
+ canceled (`worker_stopped`) record keeps whose turn it was.
70
+ - claude-peer (0.4.5): `pair` run as `bash pair` or by zsh no longer refuses every call with its usage (only `sh
71
+ pair` kept the id and the `--inbox-socket` path), and a carriage return or another control character in what it
72
+ pushes (the cwd) is escaped, so chi's JSON read no longer refuses the push.
73
+ - codex-peer (0.3.4): a carriage return or another control character in what `pair` pushes (the cwd) is escaped, as
74
+ claude-peer's is, so chi's JSON read no longer refuses the push.
75
+ - A toolkit's `groups:` match source groups in any case and with `{a,b}` braces, as the guardrails rules' `models:`
76
+ do (`groups: [Chrome]` missed `chrome`), and a config kit's `groups:` that isn't a name or a list (a map) is refused
77
+ with a line, the kit loading without it, instead of matching its stringified pairs.
78
+ - A plugin's `chi.toolkit(groups:)` that isn't a name, a list or `:all` (a map) is a load error ("toolkit NAME: groups
79
+ must be a list of group names or globs"), as a config kit's is refused, instead of matching its stringified pairs.
80
+ - `chi bundle check`'s `requires-chi-floor` sees a keyword added later: `chi.toolkit(prompt:)` (0.72.0),
81
+ `chi.toolkit(groups:)` (0.121.0), `ctx.ask_model(model:)` (0.80.0) and `ctx.sessions.send(delivery:)` (0.64.0).
82
+ - A remote `chi share` (or `share ls`/`cap`/`revoke`) whose chi web can't read its grants store prints the store's own
83
+ line, as a local one does, instead of "… answered 409 grants_unwritable: …".
84
+ - `chi web push test`'s line for a push that failed before the push service answered reads "failed (12 ms)", not
85
+ "failed (12 ms)".
86
+
87
+ Update with `chi update` (claude-peer 0.4.5, codex-peer 0.3.4, github-pr 0.5.1), then restart `chi web` and
88
+ `chi sessions restart` the running sessions.
89
+
90
+ ## [0.121.0] - 2026-10-26
91
+
92
+ ### Added
93
+
94
+ - Toolkit kits pick the MCP servers their sessions reach: `groups: [chrome]` in `toolkit.kits` (names or globs; no
95
+ key = the base's; the built-ins reach every server, `review` none). `/toolkit` switches them from the next turn
96
+ (asking first: the cached prompt changes). A call into a server the kit doesn't reach is refused without starting
97
+ it, naming a kit that reaches it; its deferred stub says why, `/mcp` marks such servers, and the web's toolkit chip
98
+ lists a restricted kit's servers. Kits without `groups:` send the same request as before.
99
+
100
+ ### Changed
101
+
102
+ - `ask_user_question`'s description asks the model, when it recommends one option, to put it first and end its
103
+ label with " (Recommended)" (eval 0005: Flash Next and DeepSeek v4.1-flash mark it 5/5 on a decision question,
104
+ 0/5 before; an answer comes back with the suffix, so `chi answer --option` takes that exact label or the number).
105
+ - The recommended option shows as such: chi web's question card marks its row in the accent colour with a small
106
+ "recommended" tag (the label shown, and the answered line, without the suffix; nothing is pre-checked), and the
107
+ terminal's question paints its row bold. Approval and step-limit cards are unchanged.
108
+ - chi web's select boxes have one shape in every dialog: the Invite panel's person/agent and Expires now match the
109
+ text fields beside them (flat box, same height and corners, one thin caret), as the Thinking and LLM context
110
+ dialogs' selects already did, in place of the browser's own pill.
111
+
112
+ ### Fixed
113
+
114
+ - claude-peer (0.4.4): `pair --inbox-socket PATH` says the remote socket is forwarded to this Claude's socket, not
115
+ from it (`ssh -R`).
116
+ - An ended participant link forgets its comment history even when its first review read is refused.
117
+ - The participant review status stays idle when a live snapshot has no current turn and its status is stale.
118
+ - The participant usage row no longer says "0 queued" when no worker is running.
119
+ - The participant usage row counts link time down and shows its end while the page stays open.
120
+
121
+ Update with `chi update` (the mcp bundle 0.7.2, claude-peer 0.4.4, the dev profile 0.7.3).
122
+
10
123
  ## [0.120.0] - 2026-10-26
11
124
 
12
125
  ### Added
@@ -4518,7 +4631,9 @@ and long-lived sessions.
4518
4631
  - A macOS desktop helper (`chi desktop install`): a "Send to chi" Service and a
4519
4632
  hotkey panel that send selected text or the clipboard to your sessions.
4520
4633
 
4521
- [Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.120.0...HEAD
4634
+ [Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.121.1...HEAD
4635
+ [0.121.1]: https://github.com/dm1try/samagotchi/compare/v0.121.0...v0.121.1
4636
+ [0.121.0]: https://github.com/dm1try/samagotchi/compare/v0.120.0...v0.121.0
4522
4637
  [0.120.0]: https://github.com/dm1try/samagotchi/compare/v0.119.0...v0.120.0
4523
4638
  [0.119.0]: https://github.com/dm1try/samagotchi/compare/v0.118.0...v0.119.0
4524
4639
  [0.118.0]: https://github.com/dm1try/samagotchi/compare/v0.117.0...v0.118.0
data/bin/chi CHANGED
@@ -391,7 +391,7 @@ if launch == :attached
391
391
  toolkit_flags: options[:toolkit_flags] || [],
392
392
  settings: options[:settings],
393
393
  parent_answers: options[:parent_answers])
394
- rescue Samagotchi::TerminalUI::AttachLauncher::Error => e
394
+ rescue Samagotchi::TerminalUI::AttachLauncher::Error, Samagotchi::Toolkit::Refused => e
395
395
  warn "Error: #{e.message}"
396
396
  exit 1
397
397
  rescue Interrupt
@@ -427,7 +427,8 @@ begin
427
427
  # limit, or Ctrl-C cancelled it.
428
428
  exit 1 if %i[turn_failed empty_answer iteration_limit].include?(ended)
429
429
  exit Samagotchi::CLI::Exit::INTERRUPTED if ended == :interrupted
430
- rescue Samagotchi::TerminalUI::SessionBusy, Samagotchi::TerminalUI::SessionNotFound, Samagotchi::ModelProfile::MissingModel => e
430
+ rescue Samagotchi::TerminalUI::SessionBusy, Samagotchi::TerminalUI::SessionNotFound, Samagotchi::ModelProfile::MissingModel,
431
+ Samagotchi::Toolkit::Refused => e
431
432
  warn "Error: #{e.message}"
432
433
  exit 1
433
434
  rescue Samagotchi::LLM::ProviderError => e
data/docs/architecture.md CHANGED
@@ -393,14 +393,18 @@ process that already owns the `Engine`); every worker starts it, and it exposes:
393
393
  worker.
394
394
  - **The secret.** Every request, on every route (the stream, the reads such as `state` and `stats`, the writes, and
395
395
  an unknown path), names the Bridge's secret in `X-Chi-Bridge`; one without it, or with another one, is
396
- `403 bridge_secret` with one message (`BridgeSecret::REFUSED`: restart chi web, re-attach terminals and restart
397
- older session workers), after the browser and proxy checks and before any route (`Bridge#secret_ok?`,
396
+ `403 bridge_secret` with one session-specific message (`BridgeSecret.refused`): "session ID's worker refused
397
+ this chi's request (it didn't name its secret): a chi web or terminal started before chi was updated can't
398
+ reach it; restart chi web, re-attach with `chi --attach ID`, or restart the worker with `chi sessions restart
399
+ ID`." `chi sessions restart` can also recover when the worker cannot be asked: TERM + resume if disk shows no
400
+ running turn, pending question or approval, queued input, or running background task. The check is after the
401
+ browser and proxy checks and before any route (`Bridge#secret_ok?`,
398
402
  `OpenSSL.secure_compare`). No route is left open: what the reads show is the session itself. Only a request that
399
403
  got past it counts as client activity, so a refused one (an older chi web's polls) never holds the idle exit. Each Bridge makes its own (`SecureRandom.hex(32)`, 64 hex
400
404
  characters) when it starts and writes it to `bridge.json`, which is 0600 from the moment it exists
401
405
  (`AtomicFile` creates the temporary file with that mode, then renames it), so only this machine's user can read
402
406
  it: a forward that adds no proxy header, or another local account, reaches the port but not a session. Clients get
403
- it only from the sidecar: `BridgeClient.discover`/`.wait_for`/`.for_sidecar` take the port and the secret from the
407
+ it only from the sidecar: `WorkerLink.client`/`.await` and `BridgeClient.for_sidecar` take the port and the secret from the
404
408
  same read (chi web makes its client from the sidecar for every request, the attached terminal again on a
405
409
  `worker_changed`), so a respawned worker's new one is used and nothing passes a port by hand. A worker from before
406
410
  the secret writes none and asks for none; a chi web or attached terminal from before it sends none, so a worker
@@ -408,7 +412,7 @@ process that already owns the `Engine`); every worker starts it, and it exposes:
408
412
  (`BridgeClient::Response#secret_refused?`) and shows the one message: chi web's relays answer 502 `bridge_secret`,
409
413
  `chi answer` and a parent's approval relay say it instead of "refused", and an event stream ends with reason
410
414
  `bridge_secret` once rediscovery finds no newer worker (the attached terminal's closing line), never retrying in
411
- silence; chi web's stream proxy passes no refusal on as frames. The secret is never logged (a refusal logs
415
+ silence; chi web's stream proxy answers 502 `bridge_secret`, never passing the refusal on as frames. The secret is never logged (a refusal logs
412
416
  `secret_refused` with the path, at most once a minute), never in an event, a reply, `/api/info` or a session
413
417
  file, and `WorkerSidecar`, `BridgeClient` and `Bridge` leave it out of `#inspect`.
414
418
 
@@ -491,10 +495,29 @@ Every route goes through `Bridge#dispatch`: an id other than the bridge's own se
491
495
  `handler_failed`) rather than dropping the connection.
492
496
 
493
497
  The per-session port is OS-assigned (bound to `0`) and published to a `bridge.json` sidecar
494
- for client discovery, with the Bridge's secret (see above). `chi web`'s `GET /api/sessions/:id/stream` proxies this bridge
495
- (503 `not_live` when the worker is not running; full history of any session is served by
496
- `GET /api/sessions/:id/output`), as a byte pipe for the owner: `App#proxy_stream_body` gives `ProxyStreamBody` the
497
- request's frame filter (`App#stream_filter`: nil for the owner; for a participant, and the owner's participant view
498
+ for client discovery, with the Bridge's secret (see above).
499
+
500
+ **Worker link.** Every client asks `WorkerLink` (`worker_link.rb`) whether a session's worker can be reached, never
501
+ reading `bridge.json` or probing a port itself: `WorkerLink.resolve(session_dir, unlink:, owner:)` answers a typed
502
+ outcome. The rule is the owner lock plus the sidecar's `pid`; there is no port probe (a port can be reused by another
503
+ session's Bridge, a free lock is the kernel's proof the worker died). In order: no lock holder, or a chi REPL's: a
504
+ sidecar is `Stale(:lock_free)` (removed with `unlink: true`, only if it is still the file that was read), else `None`;
505
+ a holder with no kind yet: `Starting`; a leaving worker: `Leaving`; a worker whose sidecar names no port or another
506
+ pid than the holder's (a worker that has not written its own yet): `Starting`, `stuck` once it has held the lock
507
+ past `START_LIMIT` (30 s) with none; a sidecar whose `protocol` is newer than `WorkerLink::PROTOCOL`: `Skew` (a
508
+ newer worker; never driven, `wake` answers 409 `worker_newer`); else `Live` (its sidecar; `#client` makes the one
509
+ `BridgeClient`). A sidecar with no `pid` is an older worker's: the lock alone says it is live. `owner:` is the
510
+ holder as the caller already read it (nil: free), so one read serves a summary and its status. `WorkerLink.client` is
511
+ the Live's client or nil; `WorkerLink.await` polls for a new worker's Live (an `until:` check tells a restart's
512
+ successor from the old worker); chi web's `App#worker_link` / `#await_worker_link` wrap them. The Bridge writes
513
+ `pid` and `protocol` with its port and removes its sidecar on stop only when the file still carries its epoch.
514
+
515
+ `chi web`'s `GET /api/sessions/:id/stream` proxies this bridge
516
+ (503 `not_live` for no worker, a leaving or stuck worker, or an unreachable Bridge; 502 `worker_newer` for skew and
517
+ `bridge_error` / `bridge_secret` for a refused Bridge; 200 only after the Bridge answers; full history of any session
518
+ is served by `GET /api/sessions/:id/output`). It opens the Bridge before answering and gives the one opened socket to
519
+ `App#proxy_stream_body` / `ProxyStreamBody`, which closes it when the body or hijacked client ends. The owner's stream
520
+ is a byte pipe; the body gives it the request's frame filter (`App#stream_filter`: nil for the owner; for a participant, and the owner's participant view
498
521
  `?as=participant`, a `Web::ParticipantStreamFilter`, which cuts the Bridge's chunks into frames and writes each again
499
522
  from `Web::ParticipantView.event`'s allowlist, dropping every other event; only `from_seq` and `snapshot` reach the
500
523
  Bridge; [internals/participants.md](internals/participants.md#what-a-participant-sees)). Resume/ring-buffer state is **in-memory**, per worker: a cursor from
@@ -531,7 +554,7 @@ moved through `Session.summary_from_file`. Liveness is probed, since a killed ow
531
554
  file trace: the owner lock every tick for the sessions the projection believes owned, and every
532
555
  session every 10 s, so a `kill -9` shows within a second. The summary (`Web::SessionSummary`,
533
556
  shared with `/api/sessions` and the session view) carries `owner`, `project_root` and
534
- `bridge_up` (the sidecar is there *and* the lock is held: the page attaches its stream on it), and
557
+ `bridge_up` (`WorkerLink` answers Live: a worker holds the lock and its sidecar names its pid; the page attaches its stream on it), and
535
558
  from that sidecar `worker_version` (the chi the worker runs) and `worker_features`
536
559
  (`Bridge::FEATURES`, what a client may ask of it beyond the routes: `restart`, `task_stop`, `reparent`, `unqueue`,
537
560
  `delivery`, a worker that queues a message for after the turn; `SessionManager.deliver_turn` and
@@ -577,12 +600,12 @@ model: [internals/remote-sessions.md](internals/remote-sessions.md).
577
600
  | `GET /api/sessions/:id/worker` | owner | | 200 `{session_id, live, stopped, owner: worker \| tui \| null, worker: {epoch, started_at, version, features} \| null, default_model, parent_strategy}` (`Local#worker`; never starts one; `default_model` is this machine's) | 404 |
578
601
  | `POST /api/sessions/:id/exit` | owner | `client_id` (passed on only when it is an attached TUI's, `tui:…`; any other is dropped), `delete` | 200 the worker's `{status: exiting, session_id, discard}` (`BridgeClient#request_exit`; never wakes one) | 409 `held` with the worker's `{status, reason, tasks, hint}`; 503 `not_live`; 501 `not_supported`; 504 `worker_timeout` |
579
602
  | `POST /api/sessions/:id/recap` | owner | | 200 the worker's `{enabled, saved, request, min_user_turns, session_off}` (`BridgeClient#request_recap`) | 503, 501, 504 as every relay |
580
- | `GET /api/sessions/:id/stats` | owner | | 200 `{session_id, metrics}`: the worker's stats, its state's metrics for a worker from before the stats route | 503 `not_live`; 504 `worker_timeout`; 502 `bridge_error` |
603
+ | `GET /api/sessions/:id/stats` | owner | | 200 `{session_id, metrics}`: the worker's stats, its state's metrics for a worker from before the stats route | 503 `not_live`; 504 `worker_timeout`; 502 `bridge_secret` or `bridge_error` |
581
604
  | `GET /api/sessions/:id/children/counts` | owner | | 200 `{running, waiting, unreported}` (`ChildrenStatus.counts`, the delegates' files here) | 404 |
582
605
  | `GET /api/sessions/:id/turns/:turn_id/records` | owner | | 200 `{tool_records: […], turn_record: {…} \| null}` (`SessionMetrics.saved_tool_records` / `saved_turn_record`, as saved) | 400 `invalid_turn_id`; 404 |
583
606
  | `POST /api/sessions/:id/turn` (`no_interrupt`) | owner | `no_interrupt: true`: the turn runs with the raised iteration limit (`SessionManager.deliver_turn(no_interrupt:)`, to the Bridge or the input file) | as before | |
584
607
  | `POST /api/sessions/:id/command` (its 501) | owner | | | 501 `not_supported` for a builtin the worker lacks adds `command` (`"/title"`) and `message` |
585
- | `GET`/`POST`/`DELETE /api/sessions/:id/participants`, `GET`/`DELETE /api/participants` | owner | `chi share`'s (`name`, `kind`, `ttl`; `?id=p_…`) | the grants, never a token but POST's own once ([internals/participants.md](internals/participants.md)) | 400 `invalid_participant`; 404 |
608
+ | `GET`/`POST`/`DELETE /api/sessions/:id/participants`, `GET`/`DELETE /api/participants` | owner | `chi share`'s (`name`, `kind`, `ttl`; `?id=p_…`) | the grants, never a token but POST's own once ([internals/participants.md](internals/participants.md)) | 400 `invalid_participant`; 404; 409 `grants_unwritable` (the store doesn't read: its words, which a remote `chi share` prints as a local one does) |
586
609
  | `GET /api/sessions/:id/apply[?turn=T]` | owner | `turn`: a turn id or a prefix (none: the latest review turn with a reply) | 200 `{text, turn_id, author, author_name}` (`ReviewApply`): the owner's Apply of a participant's review turn, for the composer, never sent ([internals/participants.md](internals/participants.md)) | 404 `not_found`; 400 `not_review_turn`; 409 `no_reply` |
587
610
  | `POST /api/sessions` (`review`) | owner | `review: {subjects: [project files], from: a session id}`; never with `prompt`, `preview`, `idle` or `continues` (a `from` takes the source's folder and setup) | 201 as a create's: an idle review session (`ReviewStart.start`, or `ReviewStart.fork` with `from`) | 400 `invalid_review` (`ReviewStart::Refused`'s words: a subject, a gone folder); with `from` also 404 `not_found`, 400 `ambiguous_id` |
588
611
 
@@ -723,8 +746,10 @@ A map of the parts the overview above doesn't name (paths under `lib/samagotchi/
723
746
  | Shipped bundles | `bundles/` (`mcp`, `github-pr`, `loop-guard`, `skills`, `source-links`, …) | Bundles that ship with chi. |
724
747
  | 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)). |
725
748
  | 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)). |
749
+ | System 1 | `system_one.rb`, `system_one/` (`SystemOne::Client`, `ApprovalQuestion`, `Log`, `Record`, `SelfProtect`, `Similar`, the backends), `guardrail_wiring.rb` (`GuardrailWiring#request_approval`), `guardrails_command.rb` (`chi guardrails replay`) | The approval judge: a model's `p` for each ask, logged to `judgments.jsonl` with the user's answer; in `on` mode an ask on a listed rule with a clean record is allowed once without the user. Labelled by `/guardrails wrong` (`SessionCommands`) and the web's **should have asked** (the Bridge's `POST guardrails/wrong`) ([guardrails.md](guardrails.md#system-1-experimental)). |
726
750
  | 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)). |
727
751
  | 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)). |
752
+ | Worker link | `worker_link.rb` (`WorkerLink`: `Live`, `Starting`, `Leaving`, `Skew`, `Stale`, `None`), `worker_sidecar.rb` (`WorkerSidecar`) | Whether a session's worker can be reached and the client for it: every Bridge client asks `WorkerLink.resolve` / `.client` / `.await` (*Worker link* above); `WorkerSidecar` is the `bridge.json` it reads. |
728
753
  | Session access | `session_access.rb` (`SessionAccess`: `NewSession`, `SetupWords`, `WaitStart`, `SessionView`, `Created`, `ContextEntry`, `ContextPush`, the errors `RemoteFailed`, `RemoteTooOld`, `Refused`, `BadRequest`), `session_access/local.rb` (`SessionAccess::Local`), `session_access/remote.rb` (`SessionAccess::Remote`), `session_access/http_transport.rb`, `session_access/web_bridge_client.rb` (`SessionAccess::WebBridgeClient`, a remote attach's worker through chi web), `session_access/dispatch.rb` (`SessionAccess::Dispatch`), `session_access/contract.rb` (`SessionAccess::Contract`), `remotes.rb` (`Remotes`), `remote_command.rb` (`chi remote`), `web/session_access_routes.rb` (`Web::SessionAccessRoutes`) | The seam between the session commands and where the sessions live: `Local` is this machine's (the `SessionManager`, `ReplyWait`, `NoteDelivery`, `ContextSources` and Bridge calls the commands made); `Remote` is another machine's, the same methods as requests to its chi web's routes of the same name (Bearer token, `HTTPTransport`), results as Local's value objects and refusals as the classes Local's callees raise, built from the fields chi web sends; waits and prefixes run on the server, a wait asked again per 30 s slice. `Dispatch` is bin/chi's front for `--remote NAME` / `CHI_REMOTE` (taken off before the subcommand lookup): it refuses what the contract table lists `local_only` (exit 2, one message) and runs the command with a `Remote` on the `Remotes` entry (`$XDG_CONFIG_HOME/samagotchi/remotes.yml`, 0600). chi web's routes answer from `Local` (*chi web's routes for the session commands* above). Checks that read this machine's config or files (vision, parent answers, the cwd's project, `SAMAGOTCHI_PARENT_SESSION`) stay in the commands and are skipped under a `Remote`, where chi web's create, turn and wait routes run the vision, parent-answers and busy checks with the server's config and the worker checks a parent's answer. See [internals/remote-sessions.md](internals/remote-sessions.md). |
729
754
  | Idle work | `idle_scheduler.rb` (`IdleScheduler`), `idle_recap.rb` (`IdleRecap`), `idle_reminders.rb` | One poller for an idle session's recap and reminders. |
730
755
  | 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)). |
@@ -130,7 +130,8 @@ class, a hand-built `ctx`), that is a gap in the kit: say so to the user rather
130
130
  after the last edit.
131
131
  - **requires_chi.** Use the chi the user runs (`chi --version`). `check` warns when the plugin calls something
132
132
  newer than the floor (`ctx.http` needs 0.80.0, `ctx.cost` 0.83.0, `ctx.usage` 0.100.0, `ctx.sessions.children` 0.34.0). A keyword added
133
- later it can't see: `ctx.ask_model(model:)` needs 0.80.0, `ctx.sessions.send(delivery:)` 0.64.0.
133
+ later counts too (`ctx.ask_model(model:)` 0.80.0, `ctx.sessions.send(delivery:)` 0.64.0, `chi.toolkit(prompt:)`
134
+ 0.72.0, `chi.toolkit(groups:)` 0.121.0), when the call is written on `ctx` / `chi` itself (not through a variable).
134
135
  - **trust_level.** Leave it out. `reviewed` / `trusted` are for someone who reviewed the bundle, not its author.
135
136
  - **Restart.** A session loads bundles when it starts: after an install or a settings change, the user starts a
136
137
  new session (or `chi sessions restart ID`).
data/docs/cli.md CHANGED
@@ -518,10 +518,16 @@ chi installed, without stopping it: an attached terminal and the web's tabs
518
518
  move to the new worker. It is refused, with the reason, while something would
519
519
  be lost: a turn running or queued, a question or approval waiting, reminders,
520
520
  a `/btw` still running, an approval relayed from a delegate, or background
521
- tasks the session started. A session with no running worker needs none (its
522
- next prompt starts one on the newest chi), and a worker from before restarts
523
- needs `chi sessions stop ID`. The Web UI's `POST /api/sessions/:id/restart`
524
- does the same (409 with `reason` and `detail` when it can't). An attached
521
+ tasks the session started. If the worker cannot be asked (a refused Bridge,
522
+ protocol mismatch or stuck start), chi sends it TERM and resumes only when
523
+ disk shows no running turn, pending question or approval, queued prompt, or
524
+ running background task; otherwise it says to run `chi sessions stop ID`, then
525
+ send any prompt. That fallback may lose reminders, a running `/btw`, and other
526
+ in-memory state. A session with no running worker needs no restart (its next
527
+ prompt starts one on the newest chi), and an older worker without restart
528
+ support needs `chi sessions stop ID`. The Web UI's
529
+ `POST /api/sessions/:id/restart` does the same (409 with `reason` and `detail`
530
+ when it can't). An attached
525
531
  terminal says at the join, in one `chi>` line, when the worker or the terminal
526
532
  runs an older chi than the newest installed (the worker moves with `chi
527
533
  sessions restart ID`, the terminal with `/detach` and `chi --attach ID`), and
@@ -1400,6 +1400,8 @@ toolkit:
1400
1400
  kits: # your own, on a base
1401
1401
  audit: { base: lean, add: [web_fetch, delegate], remove: [memory_write] }
1402
1402
  terse: { base: lean, prompt: "- Be concise in your responses" }
1403
+ web: { base: lean, groups: [chrome] }
1404
+ coding: { base: lean, groups: [] }
1403
1405
  ```
1404
1406
 
1405
1407
  | toolkit | declares |
@@ -1412,8 +1414,25 @@ toolkit:
1412
1414
  | `review` | `read`, `memory_read` (not `forget_outputs`, even under the `forget` layer): what a [participant's](internals/participants.md#turns) turn may call, whatever the session declares (the review boundary, [guardrails](guardrails.md#a-participants-review-boundary)). Reserved: `toolkit.kits.review` is refused ("review is reserved for participants' turns"), and a bundle can't declare it. A session may run on it too, as any kit |
1413
1415
 
1414
1416
  - **Plugin tools** are declared in every toolkit unless its `remove:` names them, and so is `forget_outputs` under
1415
- the `forget` layer. A `kits:` entry takes `base:` (default `full`), `add:`, `remove:` and `prompt:` (any other
1416
- key, a typo or a `tools:` list, is ignored with a warning); it can't reuse a built-in's name (`review`'s least of all: it is the participants' boundary). A bundle declares its own with `chi.toolkit` ([plugins](plugins.md#chitoolkitname-base-add--remove--prompt-nil)).
1417
+ the `forget` layer. A `kits:` entry takes `base:` (default `full`), `add:`, `remove:`, `prompt:` and `groups:`
1418
+ (any other key, a typo or a `tools:` list, is ignored with a warning); it can't reuse a built-in's name
1419
+ (`review`'s least of all: it is the participants' boundary). A bundle declares its own with `chi.toolkit`
1420
+ ([plugins](plugins.md#chitoolkitname-base-add--remove--prompt-nil-groups-nil)).
1421
+ - **Tool-index groups.** `groups:` selects source groups by name or glob (`chrome`, `db*`; any case, `{a,b}` braces,
1422
+ as the guardrails rules' `models:`); for the MCP bundle each group is a server. Anything but a name or a list (a
1423
+ map such as `{chrome: true}`) refuses every new session on that kit or on a kit built on it, on every surface (chi,
1424
+ `chi send --new`, chi web, `delegate`, a continue or a fork): `toolkit.kits.NAME: groups must be a list of group
1425
+ names or globs; fix config.yml to start a session on toolkit KIT`. A session already saved on it keeps running,
1426
+ its kit reaching no group (with a `toolkit>` warning) until config.yml is fixed. Selectors are sorted and deduplicated and match the groups declared when plugins load.
1427
+ An omitted key inherits the base's selectors; `full` and `lean` (and `child`, which is on `lean`) reach all
1428
+ groups, `review` (the participants' kit) reaches none, `groups: ["*"]` matches every group, and `groups: []`
1429
+ reaches none. A kit whose base doesn't resolve (an unknown name, a cycle) falls back to `full`, which reaches
1430
+ every group.
1431
+ A selector that matches no group in that load-time snapshot warns once. `find_tools` and `call_tool` are
1432
+ declared only when the toolkit reaches a source group or has a reachable built-in. Calls naming an unreached
1433
+ group are refused before guardrails and do not start its server. The first-run init wait is unchanged: it can
1434
+ still block the first turn even when its server's group is unreached; `start: eager` and stale-cache refreshes
1435
+ also still start. `/mcp` marks servers outside the current toolkit.
1417
1436
  - **A kit's prompt text.** `prompt:` (text, several lines allowed) goes into the system prompt of the sessions on
1418
1437
  that kit, verbatim, with no header: after AGENTS.md and the appended files, before the memory indexes. A kit
1419
1438
  without one inherits its base's (`audit: {base: terse, add: [web_fetch]}` stays concise); `prompt: ""` clears
@@ -1422,12 +1441,13 @@ toolkit:
1422
1441
  fits the kit's work (the coordinator bundle's `coding`: be concise): one that isn't text or is over 2000
1423
1442
  characters is left out with a warning, and long text belongs in `append_system_prompt:`. `/toolkit` shows it
1424
1443
  (`prompt: …`), `/stats` its kit and size (`kit prompt:`), the web's tools chip in its tooltip.
1425
- - **Saved at creation, never re-resolved from config.** A session stores its toolkit as named
1444
+ - **Saved at creation, definitions resolve at worker start.** A session stores its toolkit as named
1426
1445
  (`"toolkit": {"name": "lean", "add": [], "remove": []}`) when it is created: `toolkit.default`, `toolkit.child`
1427
1446
  for a delegate child (or the kit its `delegate` call names), or `chi --toolkit NAME` / `--tool` / `--no-tool` ([cli](cli.md)). Changing `toolkit.default`
1428
1447
  doesn't move existing sessions, not even when their worker is woken again; a session from before toolkits is on
1429
- `full`. A continue and a fork keep the toolkit. `/toolkit` shows and changes it. The prompt text is part of the
1430
- kit, not of the session file: it follows the name.
1448
+ `full`. A continue and a fork keep the toolkit name. `/toolkit` shows and changes it. The kit's tools, prompt
1449
+ text and group selectors are part of its definition, not the session file: edits reach a saved name at its next
1450
+ worker start.
1431
1451
  - **Left out: reachable or removed.** A built-in missing from a kit's base list (`lean` has no `delegate`; a
1432
1452
  `kits:` entry on `base: lean` or `child` inherits that) is left out softly: it is **reachable**. `find_tools` searches it as group `chi` (`chi/delegate`, with its inputSchema) beside
1433
1453
  the MCP servers' tools, and `call_tool(tool: "chi/delegate", args: {…})` runs it as `delegate` itself, through the
@@ -1435,10 +1455,10 @@ toolkit:
1435
1455
  tool in a toolkit's `remove:`, a `--no-tool` or a `/toolkit remove` is **removed**: hard, also after an add, and
1436
1456
  call_tool can't reach it (`child` removes `memory_write`, `delegate` and `delegate_result`, so a child on it
1437
1457
  starts no children). To keep a built-in away from the model, name it under `remove:`. `find_tools` and `call_tool`
1438
- are declared when a toolkit has a reachable built-in (or an MCP server is configured); `full` with no MCP server
1458
+ are declared when a toolkit has a reachable built-in or reaches a source group; `full` with no reachable source
1439
1459
  declares neither, and removing them there (`--no-tool call_tool`) leaves nothing out: no prompt line, no chip.
1440
1460
  - **On `api: anthropic`, reachable tools are callable by name.** Every tool `call_tool` reaches (the reachable
1441
- built-ins, the MCP servers' tools as `mcp_<server>_<tool>`) also goes in the request's tools deferred
1461
+ built-ins, tools from reached MCP groups as `mcp_<server>_<tool>`) also goes in the request's tools deferred
1442
1462
  (`defer_loading`: Messages keeps them out of the model's view until something loads them, at a flat ~100 tokens).
1443
1463
  A direct call of one (`list_sessions`, `mcp_fs_read`) runs, under the same guardrails, approvals and rows as
1444
1464
  through `call_tool` (an MCP tool's call is `call_tool`'s). A `find_tools` hit **loads** its matches (up to 5):
@@ -1455,10 +1475,13 @@ toolkit:
1455
1475
  the route: `Error: delegate isn't declared in this session (toolkit lean); call it with call_tool(tool:
1456
1476
  "chi/delegate", args: {…}) (find_tools gives its inputSchema).` A removed tool's: `Error: memory_write isn't
1457
1477
  loaded in this session (toolkit child) and can't be loaded from here; only the user can (/toolkit add
1458
- memory_write).` `/toolkit` lists both kinds, and the web's toolkit chip says them in its tooltip.
1478
+ memory_write).` An MCP tool in an unreached group gets a toolkit-specific refusal before guardrails; a deferred
1479
+ stub for it points to the toolkit instead of suggesting a search. `/toolkit` lists group selectors and the
1480
+ change; the web's toolkit chip says its source-group scope in the tooltip.
1459
1481
  - **Applied at a turn's start.** A change (`/toolkit`, a plugin's `ctx.use_toolkit`) is saved at once and the next
1460
- turn declares it, with its kit's prompt text; once the session has a model reply, `/toolkit` asks first
1461
- ("Changes that restart the prompt cache"), also for a kit with the same tools and another prompt text.
1482
+ turn declares it, with its kit's prompt text and group filter; once the session has a model reply, `/toolkit`
1483
+ asks first ("Changes that restart the prompt cache"), also when only the group selectors change and the
1484
+ declared tools stay the same.
1462
1485
  - **A name that doesn't resolve** (a typo, a bundle that was uninstalled) declares every tool for that process,
1463
1486
  with one `toolkit>` warning at the first turn; the saved name stays, so the bundle's return brings it back.
1464
1487
  - **Versions.** A toolkit's definition (its tools and its prompt text) can change with chi or a bundle, which
@@ -1928,7 +1951,7 @@ column says so, a `--kebab-name` flag. The maps (`hosts:`, `models:`,
1928
1951
  | `guardrails.mode` | `auto` | | Which rules vote: `auto` leaves out the rules tagged `modes: [strict]` (in the guardrails bundle: `git rebase`, writes and git outside the session's repo); `strict` runs them all. An unknown value warns and is `auto`. config.yml only. See [Guardrails](guardrails.md#modes). |
1929
1952
  | `guardrails.small_models` | `auto` | | Which models get the `models: small` rules: `auto` (32B or less by the size in the name, an MoE's active size), a list of globs, or `[]`. config.yml only. See [Guardrails](guardrails.md#rules-for-some-models). |
1930
1953
  | `guardrails.parent_answers` | `soft` | | Which options a parent agent (`chi answer`) may pick on a session's question, by their effect: `hard` (a deny, a Stop, a plain answer), `standard` (+ Continue and other spending options), `soft` (+ "Allow once"); never a wider scope or an allow on chi's own config. An invalid value warns and is `hard`. A session's own `parent_answers` (`chi --parent-answers`, `/parent-answers`) may only tighten it. Replaces `guardrails.parent_approvals` and `turn.parent_continue`. config.yml only: no environment variable (a parent can still pick the config dir with `XDG_CONFIG_HOME`). A convention for an honest parent, not a security boundary. See [Guardrails](guardrails.md#answers-from-a-parent-agent). |
1931
- | `guardrails.system_one.mode` | `off` | | System 1, the cheap judgment that answers an ask before the user sees it: `off` (the default), `audit` (judge every ask, log it, never decide), `on` (also allowed to allow a call once, behind the gates in [System 1](guardrails.md#system-1)). An unknown value warns and is `off`. Experimental. config.yml only. See [Guardrails](guardrails.md#system-1). |
1954
+ | `guardrails.system_one.mode` | `off` | | System 1, the cheap judgment that answers an ask before the user sees it: `off` (the default), `audit` (judge every ask, log it, never decide), `on` (also allowed to allow a call once, behind the gates in [System 1](guardrails.md#system-1-experimental)). An unknown value warns and is `off`. Experimental. config.yml only. See [Guardrails](guardrails.md#system-1-experimental). |
1932
1955
  | `guardrails.system_one.host` | none | | The `hosts:` entry serving `/v1/systemone` (a local Splash, or the hosted Jev API); unset: no judgment. config.yml only. |
1933
1956
  | `guardrails.system_one.model` | none | | The judgment model; unset uses the host's first model, and a host that names none has no judgment. config.yml only. |
1934
1957
  | `guardrails.system_one.rules` | none | | The rule ids System 1 may answer for in `on` mode: `"git-push|git-rebase"` or a YAML list, parsed like `guardrails.small_models`. Nothing is auto-allowed while it is unset; the `chi-answer-piped`, `chi-answer-http` and `chi-broadcast` rules never are, however they are listed. config.yml only. |
data/docs/guardrails.md CHANGED
@@ -243,6 +243,16 @@ with its `reason`; `chi answer` exits 1 with the line), checked against the
243
243
  question pending at that moment. Answers from the web and an attached
244
244
  terminal keep every option.
245
245
 
246
+ If `chi answer` reaches a worker that refuses its Bridge secret (`403
247
+ bridge_secret`), it prints a session-specific recovery message rather than
248
+ treating that as a refused answer: `"session ID's worker refused this chi's
249
+ request (it didn't name its secret): a chi web or terminal started before chi
250
+ was updated can't reach it; restart chi web, re-attach with chi --attach ID,
251
+ or restart the worker with chi sessions restart ID"`. `chi sessions restart
252
+ ID` can also recover when the worker cannot be asked, but only if disk shows
253
+ no running turn, pending question or approval, queued prompt, or running
254
+ background task; when one exists, stop it explicitly and then send a prompt.
255
+
246
256
  A parent may also type its answer into chi itself: a piped `chi --attach ID`
247
257
  or `printf '2\n' | chi --no-shared -p …`. So chi treats an answer as a
248
258
  parent's, held to the same setting, when its input isn't a terminal or an
@@ -662,8 +672,8 @@ With a client, `GuardrailWiring#request_approval` asks System 1 one `noul`
662
672
  question, `allow`, before the user is asked. Its instruction is a constant
663
673
  in code (`SystemOne::ApprovalQuestion::INSTRUCTIONS`), reviewed like a
664
674
  prompt; the state is the ask's own facts — the rule and its source, the
665
- reason, the tool, the command (or the paths) cut to 2000 characters, the
666
- edit's diff, the cwd, the repo and branch, and the session's title when it
675
+ reason, the tool, the command (or the paths) cut to 2000 characters
676
+ (`ApprovalQuestion::TEXT_CHARS`), the edit's diff, the cwd, the repo and branch, and the session's title when it
667
677
  has one. A memory ask also names what it is about: the entry and its scope,
668
678
  and for a removal the description that entry's index line carries
669
679
  (`Targets#memory`, read into the card by `Guardrails::Approval`): a path
@@ -680,11 +690,13 @@ Each judgment is a line in `judgments.jsonl`, beside `approvals.json` in
680
690
  `$XDG_STATE_HOME/samagotchi/guardrails/` (`SystemOne::Log`): 0600, appended
681
691
  under an exclusive `flock` (several workers write it), and rewritten —
682
692
  keeping the newest half of its lines — once it passes 5 MB. A line is never
683
- rewritten to add its label:
693
+ rewritten to add its label. Its `call` is shorter than the state's: cut to 300
694
+ characters (`ApprovalQuestion::CALL_CHARS`), with `call_digest` the SHA256 of
695
+ the whole call:
684
696
 
685
697
  ```json
686
698
  {"kind":"judgment","id":"…","at":"…","session_id":"…","rule":"git-push","source":"config","tool":"execute","call":"git push origin main","call_digest":"<sha256 of the whole call>","repo_name":"samagotchi","p":0.97,"threshold":0.8,"model":"Qwen3.8-Flash","latency_ms":180,"decision":"audit","origin":"live"}
687
- {"kind":"label","id":"…","at":"…","answer":"allow_once"}
699
+ {"kind":"label","id":"…","at":"…","answer":"allow_once","question_id":"…"}
688
700
  ```
689
701
 
690
702
  `decision` is what the judgment led to: `audit` (audit mode), `asked` (on
@@ -692,7 +704,9 @@ mode, the user was asked), `auto` (on mode, System 1 answered for the user).
692
704
  A judgment that never arrived is logged too, with `p` null, so the failures
693
705
  count as well. The label line, keyed by the judgment's id, is what the user
694
706
  did: `allow_once`, `allow_wide` (any scope wider than once), `deny`, or
695
- `none` for an ask that was cancelled. **A parent agent's answer is not the
707
+ `none` for an ask that was cancelled. A live label carries the card's
708
+ `question_id` when the user answered (the judgment is written before the card
709
+ exists), so a replay knows the ask was judged. **A parent agent's answer is not the
696
710
  user's: its allow labels nothing. Its deny is a label only where the parent
697
711
  could have allowed instead** — the ask is not `user_only` and the session's
698
712
  parent level permits a permit (`Guardrails::ParentPolicy`); a parent that was
@@ -704,6 +718,10 @@ their last label.
704
718
  Everything about the log is best-effort: a write that fails is one
705
719
  `Log.warn(:guardrails, "system_one_log_failed", …)` and the approval goes on
706
720
  as if System 1 were not there.
721
+ A turn stopped while the judge is asked is not a failed judgment: the cancel
722
+ (`LLM::RequestCancelled`) goes through `SystemOne::Client#ask`, no line is
723
+ written, and the call is denied without a card; a turn that stopped as the
724
+ judgment came back opens no card either.
707
725
 
708
726
  `ruby script/system-one-probe HOST [MODEL]` asks three fixed approval-like
709
727
  questions through the client and prints each probability and the latency of
@@ -722,13 +740,18 @@ gate below passes, in this order:
722
740
  questions — `chi-answer-piped`, `chi-answer-http`, `chi-broadcast` — which
723
741
  are never auto-allowed, even when listed
724
742
  (`SystemOne::SelfProtect::NEVER_AUTO`);
725
- 4. the call does not change chi itself (`SystemOne::SelfProtect.refuse?`): no
726
- edit or write whose paths include config.yml in chi's config dir, anything
727
- under the hooks dir, the installed bundles dir or the guardrails store (where
728
- `approvals.json` and `judgments.jsonl` live), and no `execute` or
729
- `task_create` command naming any of them — the `~/.config/samagotchi`
730
- spelling counts too. The memories dir is deliberately not one of these:
731
- reading or appending a memory file can be auto-allowed. A protected rule
743
+ 4. the call does not change chi itself (`SystemOne::SelfProtect.refuse?`), by
744
+ the same test that makes an ask one only the user may answer
745
+ (`ParentApprovals.chi_files?`): no edit or write whose paths are in chi's
746
+ config dir (config.yml), the hooks dir, the installed bundles dir, the
747
+ guardrails store (where `approvals.json` and `judgments.jsonl` live) or the
748
+ attached context dir, and no `execute` or `task_create` command naming any
749
+ of them, however it spells them (`ParentApprovals::CHI_TEXT`:
750
+ `$XDG_CONFIG_HOME/samagotchi/config.yml`, `~/.config/samagotchi/…`, a
751
+ `cd ~/.config` then `samagotchi/config.yml`). The memories dir is
752
+ deliberately not one of these: reading or appending a memory file can be
753
+ auto-allowed, unless the command has a `..` segment (`memories/../config.yml`
754
+ climbs out of it). A protected rule
732
755
  (`Verdict::PROTECTED_RULES`) may be listed by name and System 1 may answer
733
756
  it, but a call that really writes config.yml or the hooks is refused here;
734
757
  5. the judgment's `p` is at least `guardrails.system_one.threshold` (0.8);
@@ -744,11 +767,14 @@ Then the judgment's `decision` is `auto`, the verdict settles as
744
767
  written to `approvals.json` — the next call is judged again. **No question is
745
768
  opened, so no card appears, no `:question_asked` hook fires, and no
746
769
  notification is sent.** The verdict's activity carries
747
- `note: "auto-allowed by System 1 (p=0.97)"`, `judgment_id` and `p`; the line
748
- that shows it in the terminal and the web is P3b, and is not rendered yet, so
749
- today an auto-allowed call simply looks like a call that never needed
750
- approval. If the judgment line cannot be written to the log, the ask goes to
751
- the user.
770
+ `note: "auto-allowed by System 1 (p=0.97)"`, `judgment_id` and `p`, and the
771
+ call's tool row shows it: the terminal's tool line ends in
772
+ `· auto-allowed (System 1, p=0.97)`
773
+ (`TerminalUI::Formatting#format_tool_guardrail_suffix`, live and when
774
+ attaching to a saved turn), and the web's activity row carries
775
+ **auto-allowed · System 1** with `rule …, p=…, allowed once` on hover and the
776
+ owner's **should have asked** button (below). The stage trail shows no mark.
777
+ If the judgment line cannot be written to the log, the ask goes to the user.
752
778
 
753
779
  A delegate child answers from its own session's settings: its System 1 can
754
780
  auto-allow a call even when the child's `parent_answers: hard` bars a parent
@@ -778,8 +804,11 @@ whose `pending_question.approval` is there and that have an `answer` or were
778
804
  `cancelled`. A label is the scope the user took — `allow_once` for "Allow once",
779
805
  `allow_wide` for a wider one, `deny` for anything else, `none` for a cancelled
780
806
  ask. Skipped: a question no one answered, a relay card, which carries no
781
- approval of its own, and a question id a previous replay already logged — so
782
- running it twice adds nothing. A parent agent's answer is not the user's: its
807
+ approval of its own, a question id a previous replay already logged — so
808
+ running it twice adds nothing — and an ask already judged live (audit or on
809
+ mode): by the `question_id` its label carries, or, for a live judgment with
810
+ none (a cancelled ask, an older line), by its session and `call_digest`, one
811
+ card per such line. A parent agent's answer is not the user's: its
783
812
  allow is skipped, and its deny is replayed only where the parent could have
784
813
  allowed (the card is not `user_only` and the session's parent level permits a
785
814
  permit). A cards file that doesn't parse is skipped with one warning line,
@@ -130,7 +130,7 @@ gets a 400 `messages.N.content.M: Invalid signature in thinking block. The block
130
130
  conversation.`, and its last sentence says what changed ("The `system` prompt differs …"). chi's history is
131
131
  append-only within a process, so what changes the prefix is:
132
132
 
133
- 1. **A system-prompt rebuild** (`SystemPrompt#build` is cached per process and key; `PromptBuilder#reset!` drops it):
133
+ 1. **A system-prompt rebuild** (`SystemPrompt#build` is cached per process and key; `SystemPrompt#reset!` drops it):
134
134
  a new process (`--resume`, a respawn after `session.idle_exit_minutes`, a crash) after a memory index line, an
135
135
  AGENTS.md, an appended file, a model note or a bundle memory changed; `/model` (the tail's `Model:` line, its
136
136
  overlays and notes); `/thinking` (another key); a toolkit or profile change; a plugin's `tools_changed!`; the
@@ -31,7 +31,7 @@ Behavior:
31
31
  - `task_get` returns metadata and `output_path`; use `read` for output contents.
32
32
  - `task_wait` accepts `timeout`, `tail_lines` (maximum 100), and `done_pattern` (a regular expression string).
33
33
  - `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.
34
- - `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), `SAMAGOTCHI_SESSION_MODEL` (the resolved ref the session runs on, read at each call so it follows `/model`; unset when unknown) `SAMAGOTCHI_SESSION_THINKING` (the session's own thinking level; unset when it has none) and `SAMAGOTCHI_SESSION_REASONING_HISTORY` (its own reasoning_history level, the same way). `chi self` reads the last three for its model, thinking level and reasoning history. The four are `TaskRuntime::MARKER_ENV_KEYS` (and `SessionManager::SESSION_MARKER_ENV`, which a spawned worker unsets). A session's worker never inherits them from whoever started it (a `chi send` run by another session's `execute`): its other commands (a user's `!cmd`, an MCP server, a context command) have none.
34
+ - `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), `SAMAGOTCHI_SESSION_MODEL` (the resolved ref the session runs on, read at each call so it follows `/model`; unset when unknown) `SAMAGOTCHI_SESSION_THINKING` (the session's own thinking level; unset when it has none) and `SAMAGOTCHI_SESSION_REASONING_HISTORY` (its own reasoning_history level, the same way). `chi self` reads the last three for its model, thinking level and reasoning history. The four are `ChildEnv::SESSION_MARKERS` (`TaskRuntime::MARKER_ENV_KEYS`), which a spawned worker unsets (`ChildEnv.unset`, merged into its spawn env). A session's worker never inherits them from whoever started it (a `chi send` run by another session's `execute`): its other commands (a user's `!cmd`, an MCP server, a context command) have none.
35
35
  - They also pass on a set `XDG_STATE_HOME` / `XDG_CONFIG_HOME` made absolute (`Builtins.xdg_absolute`, via `Paths`), so a chi the command runs in another `cwd` (`chi send`) reads this chi's state and config. These aren't reserved: an XDG dir the model sets in `task_create`'s `env` stays its own.
36
36
 
37
37
  ## Finish wake
@@ -94,7 +94,9 @@ participant** and **one role**, until it expires or is revoked.
94
94
  block under the lock, and `App#code_identity_ok?` declines unless the trade is proxied with the grant's identity.
95
95
  Declined, the code stays and the trade answers `token_bound`, so a forwarded link opened by someone else still
96
96
  opens, once, for the account it was sent to. Owner codes and unbound grants' codes are used up whatever the gate
97
- says, as before.
97
+ says, as before. Owner codes burn on purpose (user decision 2026-10-26, don't "align" them with bound grants): an
98
+ owner code is the whole control plane, so one presented by another identity has leaked and is killed at once;
99
+ the owner prints a new link (spec: proxy_gate_spec "uses up an owner's code someone else presents").
98
100
  - **Which identity string.** The grant binds to whatever the header this run trusts carries: through Okta and
99
101
  oauth2-proxy the work email (`alex@corp.com`), through Tailscale Serve the Tailscale login (`alex@github`, or a
100
102
  private-relay address). The same person is two strings: bind to the one that proxy sends. A request from a
@@ -185,7 +187,9 @@ answers 429 `too_many_turns` past it; with no worker, their waiting input files
185
187
  written, under the session dir's `participants.lock` (`SessionManager.deliver_turn`), so two at once can't both
186
188
  pass. The ack is 202 with
187
189
  `delivery: "queue"`; chi web keeps enqueued id → grant (the last 1000), which `dequeue` and `wait` check, falling
188
- back to a waiting file's author after a restart.
190
+ back after a restart to the author the session's files keep (`ParticipantAccess#turn_author`): the waiting or claimed
191
+ input file's, then `last_turn`'s or the settled record's (a refused turn's, `TurnCheckpoint.record_refused`; one whose
192
+ worker died, `TurnCheckpoint.settle`, from its checkpoint mark's `author`).
189
193
 
190
194
  **The worker.** A participant's input file waits for its own turn whatever its delivery says
191
195
  (`SessionInbox.waits_for_turn_end?`), so a drain never merges it into another author's turn; the Bridge queues their