samagotchi 0.104.0 → 0.106.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 (103) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +70 -1
  3. data/docs/architecture.md +6 -3
  4. data/docs/cli.md +62 -19
  5. data/docs/configuration.md +10 -6
  6. data/docs/guardrails.md +69 -0
  7. data/docs/hooks.md +9 -0
  8. data/docs/internals/anthropic.md +2 -1
  9. data/docs/internals/engine.md +37 -1
  10. data/docs/internals/participants.md +229 -21
  11. data/docs/internals/prompt-caching.md +9 -0
  12. data/docs/internals/working-set.md +12 -5
  13. data/docs/plugins.md +2 -0
  14. data/docs/testing.md +23 -0
  15. data/lib/samagotchi/bridge/turn_accumulator.rb +15 -4
  16. data/lib/samagotchi/bridge.rb +45 -8
  17. data/lib/samagotchi/bridge_client.rb +4 -1
  18. data/lib/samagotchi/bundles/system/manifest.yml +1 -1
  19. data/lib/samagotchi/client_id.rb +21 -9
  20. data/lib/samagotchi/config.rb +17 -2
  21. data/lib/samagotchi/continue_offer.rb +13 -3
  22. data/lib/samagotchi/delivery.rb +10 -0
  23. data/lib/samagotchi/engine.rb +98 -8
  24. data/lib/samagotchi/guardrail_wiring.rb +13 -3
  25. data/lib/samagotchi/guardrails/review_boundary.rb +148 -0
  26. data/lib/samagotchi/guardrails.rb +1 -0
  27. data/lib/samagotchi/hook_routes.rb +5 -2
  28. data/lib/samagotchi/hooks/registry.rb +12 -3
  29. data/lib/samagotchi/idle_recap.rb +9 -0
  30. data/lib/samagotchi/last_turn.rb +5 -2
  31. data/lib/samagotchi/llm/chat_loop.rb +1 -0
  32. data/lib/samagotchi/parent_report.rb +17 -5
  33. data/lib/samagotchi/participant_messages.rb +16 -0
  34. data/lib/samagotchi/participant_name.rb +21 -0
  35. data/lib/samagotchi/participants.rb +29 -6
  36. data/lib/samagotchi/participants_watch.rb +90 -0
  37. data/lib/samagotchi/plugin/context.rb +7 -1
  38. data/lib/samagotchi/plugin/side_question.rb +9 -1
  39. data/lib/samagotchi/queued_command.rb +9 -5
  40. data/lib/samagotchi/reply_wait/poll.rb +18 -3
  41. data/lib/samagotchi/run_options.rb +1 -1
  42. data/lib/samagotchi/send_command.rb +41 -3
  43. data/lib/samagotchi/session_access/contract.rb +2 -0
  44. data/lib/samagotchi/session_access/local.rb +31 -9
  45. data/lib/samagotchi/session_access/remote.rb +19 -2
  46. data/lib/samagotchi/session_access/web_bridge_client.rb +4 -2
  47. data/lib/samagotchi/session_access.rb +6 -3
  48. data/lib/samagotchi/session_commands.rb +6 -0
  49. data/lib/samagotchi/session_inbox.rb +31 -5
  50. data/lib/samagotchi/session_manager.rb +43 -3
  51. data/lib/samagotchi/session_metrics/side_totals.rb +21 -7
  52. data/lib/samagotchi/session_metrics.rb +94 -1
  53. data/lib/samagotchi/session_participants.rb +59 -0
  54. data/lib/samagotchi/sessions_command.rb +10 -7
  55. data/lib/samagotchi/share_command.rb +61 -22
  56. data/lib/samagotchi/side_usage.rb +4 -2
  57. data/lib/samagotchi/steer.rb +120 -11
  58. data/lib/samagotchi/system_prompt.rb +21 -2
  59. data/lib/samagotchi/terminal_ui/attach_link.rb +5 -0
  60. data/lib/samagotchi/terminal_ui/attached_loop.rb +94 -22
  61. data/lib/samagotchi/terminal_ui/formatting.rb +66 -5
  62. data/lib/samagotchi/terminal_ui/local_link.rb +7 -0
  63. data/lib/samagotchi/terminal_ui/participant_commands.rb +77 -0
  64. data/lib/samagotchi/terminal_ui/reline_seam.rb +21 -1
  65. data/lib/samagotchi/terminal_ui/remote_link.rb +6 -2
  66. data/lib/samagotchi/terminal_ui/status_row.rb +11 -2
  67. data/lib/samagotchi/terminal_ui.rb +5 -0
  68. data/lib/samagotchi/testing/host.rb +4 -2
  69. data/lib/samagotchi/testing/recorder.rb +3 -1
  70. data/lib/samagotchi/toolkit.rb +21 -3
  71. data/lib/samagotchi/turn_checkpoint.rb +5 -2
  72. data/lib/samagotchi/version.rb +1 -1
  73. data/lib/samagotchi/web/app.rb +45 -20
  74. data/lib/samagotchi/web/grants.rb +89 -8
  75. data/lib/samagotchi/web/participant_access.rb +206 -35
  76. data/lib/samagotchi/web/participant_review.rb +165 -0
  77. data/lib/samagotchi/web/participant_stream_filter.rb +157 -0
  78. data/lib/samagotchi/web/participant_view.rb +415 -0
  79. data/lib/samagotchi/web/public/app.js +15 -1
  80. data/lib/samagotchi/web/public/bubbles.js +3 -3
  81. data/lib/samagotchi/web/public/data.js +20 -0
  82. data/lib/samagotchi/web/public/index.html +75 -30
  83. data/lib/samagotchi/web/public/info_bar.js +12 -5
  84. data/lib/samagotchi/web/public/info_bar_model.js +8 -5
  85. data/lib/samagotchi/web/public/participant.html +119 -0
  86. data/lib/samagotchi/web/public/participant.js +300 -0
  87. data/lib/samagotchi/web/public/participant_model.js +160 -0
  88. data/lib/samagotchi/web/public/participant_names.js +21 -0
  89. data/lib/samagotchi/web/public/participants_panel.js +311 -0
  90. data/lib/samagotchi/web/public/stream_handlers.js +8 -1
  91. data/lib/samagotchi/web/public/tokens.css +12 -0
  92. data/lib/samagotchi/web/public/turn_events.js +16 -5
  93. data/lib/samagotchi/web/public/turn_html.js +8 -4
  94. data/lib/samagotchi/web/public/turn_view.js +2 -1
  95. data/lib/samagotchi/web/public/working_set.js +6 -4
  96. data/lib/samagotchi/web/public/working_set_view.js +4 -4
  97. data/lib/samagotchi/web/qr.rb +14 -0
  98. data/lib/samagotchi/web/running.rb +52 -0
  99. data/lib/samagotchi/web/server.rb +13 -0
  100. data/lib/samagotchi/web/session_access_routes.rb +3 -0
  101. data/lib/samagotchi/worker.rb +125 -11
  102. data/lib/samagotchi/worker_inbound.rb +12 -1
  103. metadata +17 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f4a794e9e201d233ca72705b62746997d6ef702804e18ad8a5bb1f85851ed454
4
- data.tar.gz: 341e3b0cfcbc4367faed3782d600b2d934395eef7623499619f018b1984a06db
3
+ metadata.gz: 13a61f40d26663d6ecd66b89b94d1406f8495e161a5bbdd33a319a41e46dbfb3
4
+ data.tar.gz: 2b0ab74468e6020dad2b60b12abba06809933621234080f0ef75f8fbc6243ccc
5
5
  SHA512:
6
- metadata.gz: 20bdcd9c915786fe5698401079a36e676ace823d4e42625c5b75fd2472d9990b342f1cc9c23e2a609bffee3b57df88c4fb9b6c927cdf0e182c1f7966fc5fde37
7
- data.tar.gz: 64d05e8f627bb8c628e3510681b7acf1de4c37995175333614094d00333f66b1de28d91a6b2819f005e4d1e5150c8ffd189f6c9e2379044b89268188086231e6
6
+ metadata.gz: d53a1f7c2f4347d9688e3d5f4000400297b343d2089536dd9efececcaf30cacceec91834639fed0750848e098c4845e4622c44f6b1df49180a84422dfc153430
7
+ data.tar.gz: 83dce80d3f6ebaa03b1bc84c6d84f025bb6055a9125c0e6ade2a7ad427922c8c2dbeb631991500e9b3be9454161917229d2f0b5f27b55f0a36e7afd0f3ab76c2
data/CHANGELOG.md CHANGED
@@ -8,6 +8,73 @@ and commands may change between minor versions. How releases are made:
8
8
 
9
9
  ## [Unreleased]
10
10
 
11
+ ## [0.106.0] - 2026-10-23
12
+
13
+ ### Added
14
+
15
+ - Participants (experimental) can now ask questions and comment: a participant's message through chi web is a
16
+ review turn of its own, queued behind the running one, that the model answers read-only. It may read the session's
17
+ project (never its secret files, `~/.ssh` or chi's own state and config) and project memories, and proposes
18
+ changes as text for the owner to apply; writes, commands, fetches, notes, reminders, plugin and MCP tools are
19
+ denied, never asked about, and every approval or question stays the owner's. A line starting `!` or `/` and images
20
+ are refused. Each participant gets one running and 3 waiting turns (`web.participants.queue`) and a spending cap,
21
+ `chi share --cap DOLLARS` (default $2, `web.participants.cap`); at the cap their next comment is refused and the
22
+ owner gets a notice, and `chi share cap ID P DOLLARS` raises it. `/stats` shows each participant's spend, their
23
+ turns and the side requests they caused. Hooks see `author:` on a participant's turn and plugins
24
+ `ctx.turn_author`; a hook still runs with the owner's power. A participant now sees the conversation and comments
25
+ from their page (desktop and phone): prompts and replies with who wrote each, one-line tool rows (the tool and a
26
+ project file's path, never its arguments or output), the reply streaming in, their own queued comments (Take back),
27
+ "waiting for the owner" while you answer a question, and every refusal in words. They never see tool output,
28
+ thinking, approvals, questions, memories, notes, the working set, cost or host paths: one server-side filter covers
29
+ their stream and reads, and redacts secret-shaped text as well. On your page, a participant's prompts carry their
30
+ name, and the info bar's participants chip opens a panel to invite (a link to copy, share or scan), see each one's
31
+ spend, turns and last visit, change a cap, revoke, and open the participant view: their page for this session, as
32
+ they see it (an agent participant can wait on its turns' replies through chi web's API). The `review`
33
+ toolkit is new and reserved: config's `toolkit.kits.review` is refused. A session whose worker predates this
34
+ release takes no participant's turn until it is restarted. In the attached terminal a participant's message reads
35
+ `Alex ▸ review> …`, a line says when someone is invited, revoked or expires, the status row counts them, and
36
+ `/share` and `/participants` (list, revoke, cap) run `chi share` on the session there. See [docs/cli.md](docs/cli.md#participants) and
37
+ [docs/guardrails.md](docs/guardrails.md#a-participants-review-boundary).
38
+
39
+ ### Fixed
40
+
41
+ - Web, dark theme: the working set's rail (its rule and count), the list's header rows (`files · 2`) and its ops
42
+ text and buttons (drop, pin, add) are easier to read.
43
+ - Web: `!rollback` (and a canceled continue) updates the working set at once; one that emptied it no longer leaves
44
+ the dropped items in the rail and the list until the next turn.
45
+ - `chi share`'s link names the port the running chi web listens on (recorded in `$XDG_STATE_HOME/samagotchi/web.json`),
46
+ not only config's `web.port`.
47
+ - A participant's `chi --remote NAME send -m` prints the review turn's id, and `send --wait -m` waits on it instead of
48
+ being refused as busy; a participant's turn held for the owner reads `waiting_for_owner` (exit 3).
49
+ - `chi send --wait --format json`: a turn still running names the wait on its turn id.
50
+ - The attached terminal no longer announces your own `/share` or `/participants revoke` a second time.
51
+ - Web: the session bar's first line keeps one width through a turn: the status word takes the same room for idle and
52
+ running, the working set's chip shows from the session's start (`set 0`), and every chip is one height. A short
53
+ folder path no longer hops between the bar's rows mid-turn and moves the stage.
54
+
55
+ Update with `chi update`.
56
+
57
+ ## [0.105.0] - 2026-10-23
58
+
59
+ ### Changed
60
+
61
+ - Web: the stage view's **working set** is on by default (no longer experimental). Beside the running turn a rail
62
+ shows the files, memories and URLs it brought into the model's context; each finished turn gets a footprint line;
63
+ the session bar's `● set N` chip (in place of the mem chip) opens the session's list, where rows say whether the
64
+ model still has them (stale, forgotten, dropped) and offer drop, pin, add and refresh. Turn it off with
65
+ `web.working_set: false`, `chi web --no-web-working-set` or `?working_set=0` (the mem chip comes back). See
66
+ [docs/cli.md](docs/cli.md#the-working-set).
67
+
68
+ ### Fixed
69
+
70
+ - `chi --remote NAME sessions list` shows test sessions, marked `[test]`, as the local list does.
71
+ - The attached terminal echoes a command typed after `/unqueue`'s text (`fix it/unqueue`) as the command alone, not the
72
+ whole line.
73
+ - Attaching to a session whose last turn was stopped or canceled shows the turn's canceled line, not the text it
74
+ streamed before the stop as if it were the answer.
75
+
76
+ Update with `chi update`.
77
+
11
78
  ## [0.104.0] - 2026-10-23
12
79
 
13
80
  ### Added
@@ -3944,7 +4011,9 @@ and long-lived sessions.
3944
4011
  - A macOS desktop helper (`chi desktop install`): a "Send to chi" Service and a
3945
4012
  hotkey panel that send selected text or the clipboard to your sessions.
3946
4013
 
3947
- [Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.104.0...HEAD
4014
+ [Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.106.0...HEAD
4015
+ [0.106.0]: https://github.com/dm1try/samagotchi/compare/v0.105.0...v0.106.0
4016
+ [0.105.0]: https://github.com/dm1try/samagotchi/compare/v0.104.0...v0.105.0
3948
4017
  [0.104.0]: https://github.com/dm1try/samagotchi/compare/v0.103.0...v0.104.0
3949
4018
  [0.103.0]: https://github.com/dm1try/samagotchi/compare/v0.102.0...v0.103.0
3950
4019
  [0.102.0]: https://github.com/dm1try/samagotchi/compare/v0.101.0...v0.102.0
data/docs/architecture.md CHANGED
@@ -470,7 +470,10 @@ The per-session port is OS-assigned (bound to `0`) and published to a `bridge.js
470
470
  for client discovery. `chi web`'s `GET /api/sessions/:id/stream` proxies this bridge
471
471
  (503 `not_live` when the worker is not running; full history of any session is served by
472
472
  `GET /api/sessions/:id/output`), as a byte pipe for the owner: `App#proxy_stream_body` gives `ProxyStreamBody` the
473
- request principal's frame filter (`App#stream_filter`, nil for the owner, the only principal today). Resume/ring-buffer state is **in-memory**, per worker: a cursor from
473
+ request's frame filter (`App#stream_filter`: nil for the owner; for a participant, and the owner's participant view
474
+ `?as=participant`, a `Web::ParticipantStreamFilter`, which cuts the Bridge's chunks into frames and writes each again
475
+ from `Web::ParticipantView.event`'s allowlist, dropping every other event; only `from_seq` and `snapshot` reach the
476
+ Bridge; [internals/participants.md](internals/participants.md#what-a-participant-sees)). Resume/ring-buffer state is **in-memory**, per worker: a cursor from
474
477
  an earlier worker gets the `reset` snapshot, not a replay.
475
478
 
476
479
  **Session hub.** The cross-session layer (which sessions exist, who owns them, what changed)
@@ -513,8 +516,8 @@ calling the `SessionAccess::Local` operation of the same name, so a remote backe
513
516
  token-gated like the rest (LAN mode: `Authorization: Bearer`), and one that takes an id is a `:session` route (an id
514
517
  that isn't one, or no such session: 404 `not_found`). Every `ROUTES` row also names who may use it, checked against
515
518
  the request's `Web::Principal` (`App#principal_for`): `owner`, or `participant` for a route a participant may use too.
516
- A participant (a `Web::Grants` token for one session) may use three: `GET /`, `GET /api/info` and `GET /api/sessions/:id`,
517
- each answered as theirs; every other route is `owner` (Permission below, and the participants routes in
519
+ A participant (a `Web::Grants` token for one session) may use the page (`GET /`), `GET /api/info`, their session's
520
+ show, review and filtered stream, and their turns and waits, each answered as theirs; every other route is `owner` (Permission below, and the participants routes in
518
521
  [internals/participants.md](internals/participants.md)); `spec/web/principal_spec.rb` fails on a row without one. How a request flows end to end, which checks run where and the trust
519
522
  model: [internals/remote-sessions.md](internals/remote-sessions.md).
520
523
 
data/docs/cli.md CHANGED
@@ -15,14 +15,14 @@
15
15
  - `chi web --web-host lan` — the Web UI on your home network too, for your phone: a link with an access token and its QR code (see [chi web on your phone](#chi-web-on-your-phone)); `chi web --new-token` replaces the token
16
16
  - `chi web --web-markdown` — opt in to sanitized Markdown rendering for completed assistant messages, the stage's asking text and a resolved question card's reply
17
17
  - `chi web --web-view turn` — draw turns with the turn view (each turn as one block of steps, the running one at the bottom of the history) instead of the default stage view (the running turn pinned above the composer); `?view=stage|turn` on the page URL overrides it (see [Web views](#web-views))
18
- - `chi web --web-working-set` — the stage view's working set (experimental, off by default; `--no-web-working-set` turns a config's `web.working_set: true` off): what each turn brought into the model's context, in the stage and under each finished turn; `?working_set=1|0` on the page URL overrides it (see [The working set](#the-working-set))
18
+ - `chi web --no-web-working-set` — turns the stage view's working set off (on by default; `--web-working-set` turns a config's `web.working_set: false` back on): what each turn brought into the model's context, in the stage and under each finished turn; `?working_set=1|0` on the page URL overrides it (see [The working set](#the-working-set))
19
19
  - `chi sessions list|stop|restart|archive|unarchive|delete|prune|clean|stats` — manage persisted sessions; `list` shows this git project's, `list --scope=all` every one, a delegated session with `↳ <parent>`, `list --archived` the archived ones too (see [Sessions](sessions.md)). `stats ID` prints a session's cost, tokens and progress, and its own settings (`--set`; a `settings:` line), from a live worker's `GET stats`, else its saved `analytics.json` (`--format json` one object, with `settings`), without running a model turn. A usage error (an unknown subcommand or flag, a flag missing its value, `stop`, `restart` or `delete` with no ids, a bad `list --format` or `--scope`, a `--days`, `--keep` or `--limit` that isn't a whole number) exits 2; an unknown or refused session exits 1
20
20
  - `chi note [--source NAME] [-m TEXT] (ID|PREFIX)... | --all` — add a context note (TEXT or stdin) to sessions: background the model sees on its next turn; it starts no turn (see [Sessions: Context notes](sessions.md#context-notes))
21
21
  - `chi context <add|push|ls|show|refresh|rm|mute|unmute> …` — attach live external text to sessions (a command chi runs every so often, or text pushed in); the model gets a short note when it changes and reads it with `context_read`; `chi context add <URL>` attaches a GitHub PR with the github-pr bundle (see [Attached context](context.md))
22
22
  - `chi send [-m TEXT] [--image PATH]... [--cut | --queue] (ID|PREFIX)...` — send a message to sessions as if typed there: a turn starts (or a running one takes it at its next step; `--cut` asks it to cut long thinking for it; `--queue` runs it as a turn of its own after the running one, and `--queue --wait` waits for that turn's own answer); piped stdin goes above `-m` as quoted context, and `--image` attaches images (see [Sessions: Sending a message](sessions.md#sending-a-message)); `--new` starts a session with it instead (`--new --continues ID|last:ID` the next link of that session's chain, see [Session chains](sessions.md#session-chains)), and `--wait` prints the answer (`--wait ID` with no message waits for the next reply without sending — to an idle session whose last turn already ended, that ended turn's reply or end comes back; `--wait --turn T ID` with no message waits for exactly turn T, whether running, queued or last ended; `--format json` includes its `turn_id` when known, for waiting again after exit 3 or 4; exit 3 a question waits, its options on stderr; exit 4 `--timeout` passed with the turn still running; see [Starting a session](sessions.md#starting-a-session))
23
23
  - `chi answer ID --question QID (--option N|LABEL)... [--text T] [--timeout S] [--format json]` (or `--dismiss`) — answer the question a session waits on (the one `chi send --wait` exited 3 with), then wait and print what comes next as `chi send --wait` does: the reply (0), the next question (3), still running after `--timeout` (4). `--option` is 1-based or the label, repeated on a multi-select question; `--text` is free text, or a Deny's reason; `--dismiss` leaves it unanswered and the model finishes its reply. A question no longer open (the web answered first) isn't answered again: it waits for that turn's reply. An option the question doesn't offer exits 2; a worker that is gone exits 1 (`send the task again: chi send --wait -m "…" ID`). What a parent may pick follows `guardrails.parent_answers`, or the session's own `parent_answers` when stricter (`soft`, the default: a deny, a plain answer, Continue and "Allow once"; `standard`: no allow; `hard`: no Continue either), never a wider scope or an allow on chi's own config; a refused pick exits 1 with what to answer instead (a convention, not a security boundary; see [Guardrails](guardrails.md#answers-from-a-parent-agent))
24
24
  - `chi remote add NAME URL [--token-file F | --token-stdin]`, `chi remote ls`, `chi remote rm NAME` — the other machines whose chi web the session commands reach with `--remote NAME` (see [Remote sessions](#remote-sessions))
25
- - `chi share ID --name NAME [--agent] [--expires 3d]`, `chi share ls [ID]`, `chi share revoke ID [P] | --all` — invite a participant to review a session through chi web on the LAN (a link of their own, 7 days), list them, revoke them (see [Participants](#participants))
25
+ - `chi share ID --name NAME [--agent] [--expires 3d] [--cap 2]`, `chi share ls [ID]`, `chi share cap ID P DOLLARS`, `chi share revoke ID [P] | --all` — invite a participant to review a session through chi web on the LAN (a link of their own, 7 days, a $2 spending cap), list them, change one's cap, revoke them (see [Participants](#participants))
26
26
  - `chi --remote NAME send|answer|note|context|sessions|share …` (or `CHI_REMOTE=NAME`) — those commands on that machine's sessions, through its chi web (see [Remote sessions](#remote-sessions))
27
27
  - `chi --remote NAME --attach ID` (`--resume ID`, `--dir DIR [-p TEXT]`) — the attached TUI on a session of that machine (see [Remote sessions](#remote-sessions))
28
28
  - `chi memory ls [--scope project|system] [--format text|json]` — the memories of both scopes (this folder's project first), one row each: scope, name, size, date, `from <bundle>`, the index description and its model overlays; `chi memory rm NAME [--scope project|system]` removes one as `memory_write` with `remove: true` does: its file and model overlays go to the bundle trash (the line names the dir; `chi bundle trash` lists and empties it) and its index line is dropped. NAME in both scopes without `--scope` exits 2 naming both; a missing NAME or a bundle's memory (`chi bundle uninstall` removes it) exits 1 (see [Memory: Listing and removing from the shell](memory.md#listing-and-removing-from-the-shell))
@@ -760,7 +760,7 @@ REPL alike:
760
760
  it is in the input history, ↑; not with input from a pipe; its images don't
761
761
  come back), then the one before it on the next `/unqueue`. A command typed
762
762
  after that text without clearing it (`fix it/unqueue`, `fix it/queue more`)
763
- runs as the command, and the text stays in the input. `(nothing queued: …)` when none waits,
763
+ runs as the command (the scrollback shows the command alone), and the text stays in the input. `(nothing queued: …)` when none waits,
764
764
  `(not removed: it already started)` when its turn won the race, `(not removed:
765
765
  it is already gone)` when another client removed it. Other terminals that
766
766
  showed it say `(a queued message was removed)`; the web drops its bubble. A
@@ -1010,12 +1010,12 @@ made (up to 120 lines or 8 KB), live and after a reload (see
1010
1010
 
1011
1011
  ### The working set
1012
1012
 
1013
- `web.working_set: true` (or `chi web --web-working-set`,
1014
- `SAMAGOTCHI_WEB_WORKING_SET=true`; `?working_set=1` or `?working_set=0` on the
1015
- page URL for one page load) shows, in the stage view, what a turn brought into
1016
- the model's context: the files it read, edited or wrote, the memories it read
1017
- or wrote and the URLs it fetched. Off by default while it is tried out; the
1018
- turn view ignores it.
1013
+ The stage view shows what a turn brought into the model's context: the files
1014
+ it read, edited or wrote, the memories it read or wrote and the URLs it
1015
+ fetched. It is on by default; `web.working_set: false` (or `chi web
1016
+ --no-web-working-set`, `SAMAGOTCHI_WEB_WORKING_SET=false`; `?working_set=0` on
1017
+ the page URL for one page load, `?working_set=1` back on) turns it off, and the
1018
+ session bar shows the `mem N` chip again. The turn view ignores it.
1019
1019
 
1020
1020
  - **The rail**, on the right of the stage's live slots: the running turn's set
1021
1021
  in four lines, folded as it grows (files under one folder become
@@ -1027,8 +1027,10 @@ turn view ignores it.
1027
1027
  on a phone it isn't shown.
1028
1028
  - **The footprint**, one line under each finished turn's block, live and after
1029
1029
  a reload: `touched …/web/public/ · 4 files · ◆ 2 memories s2–10 · +1 url ·
1030
- edited 1`. A step tag (`s2`) goes to that call's row; the rest opens the
1031
- turn's list. A turn that touched nothing has none.
1030
+ edited 1`, then `n stale`, `n forgotten` or `n dropped` while some are (a file
1031
+ changed after the turn read it, even at its very end, says so here). A step
1032
+ tag (`s2`) goes to that call's row; the rest opens the turn's list. A turn
1033
+ that touched nothing has none.
1032
1034
  - **The `set N` chip** in the session bar, in place of the `mem N` chip (whose
1033
1035
  tooltip goes too): the session's whole set, preloaded (`--memory`) and muted
1034
1036
  (`--mute`) memories included; its dot lights when the set changed since you
@@ -1235,7 +1237,7 @@ chi --remote studio --dir /Users/me/app -p "hi" # a new session there (--
1235
1237
  `/detach`, `/exit`, `/exit --delete`, `/archive`, `/stats` and `/recap` work
1236
1238
  as here, on that machine's session (`/exit --delete` deletes it there).
1237
1239
  The status row's children segment counts its delegates there, and a join
1238
- shows the steps' durations and a failed turn's line from its saved records
1240
+ shows the steps' durations and a failed or canceled turn's line from its saved records
1239
1241
  there. A chi web too old for one of them says so in one line ("remote
1240
1242
  studio runs chi X, too old for this command through --remote: update chi
1241
1243
  there").
@@ -1272,10 +1274,11 @@ A session's owner can invite **participants**, people or agents, to review it th
1272
1274
 
1273
1275
  ```sh
1274
1276
  chi share 3f9a1c2e --name Alex
1275
- # p_7c01d2aa Alex (human, reviewer) · session 3f9a1c2e · expires 2026-10-30 14:02
1277
+ # p_7c01d2aa Alex (human, reviewer) · session 3f9a1c2e · expires 2026-10-30 14:02 · cap $2.00
1276
1278
  # http://192.168.1.29:4567/?token=chp_…#s=3f9a1c2e-…
1277
1279
  # (shown once: chi keeps only its digest; chi share revoke 3f9a1c2e p_7c01d2aa ends it)
1278
1280
  chi share ls # every session's participants (--format json; never a token)
1281
+ chi share cap 3f9a1c2e p_7c01d2aa 5 # Alex's turns may cost you $5, from their next one
1279
1282
  chi share revoke 3f9a1c2e # every link to that session; add a participant id for one
1280
1283
  chi share revoke --all # every link to every session
1281
1284
  ```
@@ -1283,13 +1286,52 @@ chi share revoke --all # every link to every session
1283
1286
  - The link is printed once (with a QR code at a terminal): chi keeps only a digest of its token. Send it to the
1284
1287
  participant; opening it sets their browser's cookie, as the owner's LAN link does. `--agent` marks an agent
1285
1288
  participant, which uses the token as `Authorization: Bearer` (e.g. `chi --remote` with that token).
1286
- - It works for 7 days (`--expires Nh` or `Nd`, 1 hour to 30 days), until it is revoked, or until the session is
1287
- archived or deleted. A revoked or expired link shows "This review link has expired or was revoked."
1288
- - For now a participant sees a page with the session's title and status; comments and the review itself come in
1289
- a later chi. They can't reach anything else: every other route answers them 403, another session 404.
1289
+ - It works for 7 days (`--expires Nh` or `Nd`, 1 hour to 30 days; `web.participants.expires`), until it is revoked,
1290
+ or until the session is archived or deleted. A revoked or expired link shows "This review link has expired or was
1291
+ revoked."
1292
+ - **Review turns (experimental).** A participant can ask questions and comment through chi web's `POST …/turn`: each
1293
+ message is a turn of its own, queued behind the running one, that the model answers **read-only**. It may read the
1294
+ project (never its secret files or chi's own), and proposes changes as text or a diff for you to apply; it can't
1295
+ write, run, fetch, send notes or set reminders, and any approval or question is yours. A line starting `!` or `/`
1296
+ and images are refused. One running and 3 waiting turns per participant (`web.participants.queue`). You pay: each
1297
+ participant has a spending cap (`--cap DOLLARS`, default $2, `web.participants.cap`), counted from their turns'
1298
+ cost and the side requests they cause; at the cap their next comment is refused and you get a notice, and `chi
1299
+ share cap` raises it. `/stats` shows each participant's spend. Their turns go only to a worker with the review
1300
+ boundary: restart an older one. While your turn waits at the step limit (Continue or Stop), their turns wait too
1301
+ (still queued, still counted): they run, in order, once you continue, stop or send a turn of your own.
1302
+ - **The participant page.** Their link opens a page of its own (never yours): the session's title and status, who
1303
+ else takes part, and the conversation as a participant sees it: every prompt and reply with who wrote it, each
1304
+ step's tools as one line (`read docs/plan.md`: the tool and a project file's path, never arguments or output), and
1305
+ the reply streaming in while a turn runs. A "Comment or ask" box sends their comment; their queued comments show
1306
+ with **Take back**; "waiting for the owner" shows while you answer a question or an approval of the turn; a refused
1307
+ comment (a `!` or `/` line, too many waiting, their cap reached) says why. On a phone it is the thread first with
1308
+ the box pinned, tool lines folded. They never see tool output or arguments, thinking, approvals or questions,
1309
+ memories, context notes, plugin and delegate input, cards, command output, the working set, cost, timing or host
1310
+ paths; secret-shaped text (API keys, tokens, private keys) is redacted too. An agent participant can wait on its
1311
+ turns' replies through the API, or with chi: its link's token in a remote, `chi --remote NAME send --wait -m "…" ID`,
1312
+ or `send -m`, which prints the turn id `--wait --turn T` takes. They can't reach anything else: every other route answers them 403, another
1313
+ session 404.
1314
+ - **Your side of it.** In the session bar, the participants chip (`invite` while nobody has a link, then `2
1315
+ participants`, with a green dot while one is on their page) opens the panel: each participant's state, last visit,
1316
+ spend against their cap, turns and expiry, with **Cap** and **Revoke** (revoking ends their page and stream and
1317
+ drops their queued comments). **Invite** takes a name, person or agent, the expiry and a cap, and shows the link
1318
+ once, with Copy, Share (a phone's share sheet) and its QR code. **Participant view ↗** opens their page for this
1319
+ session in a new tab, read-only, through the same server filter: exactly what they see. A participant's prompt
1320
+ shows their name in their colour, live and after a reload.
1290
1321
  - `chi --remote studio share …` invites on studio: the link names studio's LAN address. Locally, the link names
1291
- this machine's chi web; without one on the LAN, chi says the link opens on this machine only.
1322
+ this machine's running chi web, on the port it listens on (it records it in `$XDG_STATE_HOME/samagotchi/web.json`;
1323
+ `web.port` when none answers there); without one on the LAN, chi says the link opens on this machine only.
1292
1324
  - The owner's own token and the `chi web --new-token` rotation are separate: rotating it ends no participant's link.
1325
+ - **In the attached terminal.** A participant's message reads `Alex ▸ review> is step 3 right?`, the name in that
1326
+ participant's own colour (queued, running, and the last exchange when you join); yours stay as they were. A line
1327
+ says when someone is invited, revoked or their link expires (`participants> Alex joined (p_7c01d2aa)`,
1328
+ `participants> Alex left: their link was revoked`), within a few seconds and between turns, and the status row
1329
+ counts who is in the review (`2 participants`). The same commands work from there, on the attached session:
1330
+ `/share NAME [--agent] [--expires 3d] [--cap 2]` (the link and its QR code, once), `/participants` (the list),
1331
+ `/participants revoke [P]` (one participant, or everyone) and `/participants cap P DOLLARS`. They are `chi share`'s
1332
+ own, so under `chi --remote NAME` they reach that machine's chi web, as `chi --remote NAME share` does; an invite or
1333
+ a revoke made with them isn't announced again by the `participants>` line. The REPL
1334
+ (`--no-shared`) answers them with a note: a participant's turns run on the session's worker.
1293
1335
 
1294
1336
  How it works inside (grants, the gate, the routes, the audit trail):
1295
1337
  [internals/participants.md](internals/participants.md).
@@ -1415,7 +1457,7 @@ changes:
1415
1457
 
1416
1458
  ```
1417
1459
  status> Coordinator | model=Qwen3.6-35B | ↳ 3f2a1c9e | ctx=12.3% (under20) | mem: notes, cli_usage | muted: gh-helper
1418
- status> model=Qwen3.6-35B | ⑂ 2 running · 1 waiting | ctx=8.1% (under20) | mem: skill_coordinator
1460
+ status> model=Qwen3.6-35B | ⑂ 2 running · 1 waiting | 2 participants | ctx=8.1% (under20) | mem: skill_coordinator
1419
1461
  ```
1420
1462
 
1421
1463
  - The session's title first, when it has one (`/title`; not the first message a session without one is named by),
@@ -1432,6 +1474,7 @@ status> model=Qwen3.6-35B | ⑂ 2 running · 1 waiting | ctx=8.1% (under20) | me
1432
1474
  ends and when a delegate's report merges into a turn, the REPL whenever it draws the row. A
1433
1475
  child that starts waiting on an approval (which doesn't wake the parent) shows at the next of
1434
1476
  those.
1477
+ - `N participants`: who is in the session's review now ([Participants](#participants)), attached only.
1435
1478
  - `ctx=`: the kernel's context estimate and its bucket, updated during a turn (on hosts that
1436
1479
  report none, `api: openai`, at the turn's end).
1437
1480
  - `mem:`: the memories the session read, with its `--memory` list; `muted:` its `--mute` list.
@@ -157,7 +157,7 @@ or `chi web` restart.
157
157
  | `toolkit.default`, `toolkit.child` | Session creation | The chosen toolkit is saved; changing the defaults does not move existing sessions. |
158
158
  | `toolkit.kits`, `execute.description`, `llm_context.policy` | Worker start | These are held by the engine's toolkit or tool registry. |
159
159
  | `skip_agents_md` | Prompt rebuild | A running engine caches its built prompt; a new worker or `/model` rebuild uses the changed value. |
160
- | `context.*` except `context.every_seconds`, `llm_context.*` except `llm_context.policy`, `steer.cut_after`, `memory.index_warn_tokens`, `status.line` | Next matching turn, display, poll or write | Readers resolve these settings when they perform that operation. Session-specific LLM context and thinking values remain saved in the session. |
160
+ | `context.*` except `context.every_seconds`, `llm_context.*` except `llm_context.policy`, `steer.cut_after`, `memory.index_warn_tokens`, `status.line`, `web.participants.*` | Next matching turn, display, poll or write | Readers resolve these settings when they perform that operation. Session-specific LLM context and thinking values remain saved in the session. A participant's grant keeps the expiry and cap it was issued with; `web.participants.*` is what the next invite gets, and the cap and queue of a grant from before caps. |
161
161
  | `context.every_seconds` | Next context poll | The poller checks it for each source that has no explicit interval. |
162
162
  | `guardrails.*` | Next tool call or approval | Guardrail rules and disable lists are reloaded when the config file changes; rule settings take effect on that evaluation. `guardrails.parent_answers` is checked when a parent answers a question. |
163
163
  | `hooks`, `bundles` | Worker start | Hooks, plugins and their bundle settings load with the engine. |
@@ -165,7 +165,7 @@ or `chi web` restart.
165
165
  | `session.shared` | Next `chi` invocation | It selects attached-worker or in-process mode for that invocation. |
166
166
  | `session.idle_exit_minutes`, `session.keep_empty`, `session.max_children`, `session.delegate_reports`, `session.task_wakes`, `session.max_wakes` | Worker start or next matching lifecycle decision | Existing workers keep their idle-exit timer; the other values are read for the next corresponding decision. |
167
167
  | `session.retention_*`, `session.max_count`, `session.keep_status`, `session.sweep_interval_hours` | Next retention sweep | `chi web` sweeps on its configured schedule and on `GET /api/sessions`; `chi sessions prune` uses the values for that invocation. The sweep interval is checked on the next hub tick. |
168
- | `web.*` | `chi web` start | Restart `chi web` to change its bind address, port or page settings. |
168
+ | `web.*` except `web.participants.*` | `chi web` start | Restart `chi web` to change its bind address, port or page settings. |
169
169
  | `log.*` | Process start | Each process holds its configured log sink and level after first use; restart that process. |
170
170
  | `history.file` | Next history access | The TUI and web composer resolve its path when they read or append history. |
171
171
  | `broadcast.*` | Next `chi broadcast` | Each broadcast invocation reads its targeting and triage settings. |
@@ -1391,7 +1391,7 @@ toolkit:
1391
1391
  default: lean # the sessions you start (unset: full)
1392
1392
  child: child # delegate children (unset: full)
1393
1393
  kits: # your own, on a base
1394
- review: { base: lean, add: [web_fetch, delegate], remove: [memory_write] }
1394
+ audit: { base: lean, add: [web_fetch, delegate], remove: [memory_write] }
1395
1395
  terse: { base: lean, prompt: "- Be concise in your responses" }
1396
1396
  ```
1397
1397
 
@@ -1402,13 +1402,14 @@ toolkit:
1402
1402
  | `child` | `lean` plus `send_note`, without `memory_write`, `delegate`, `delegate_result` |
1403
1403
  | `coordinator` | `lean` plus `delegate`, `delegate_result`, `list_sessions` (declared by the coordinator bundle) |
1404
1404
  | `coding` | `child`, with the prompt text `- Be concise in your responses` / `- Show file paths clearly when working with files` (declared by the coordinator bundle, for build and fix children) |
1405
+ | `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 |
1405
1406
 
1406
1407
  - **Plugin tools** are declared in every toolkit unless its `remove:` names them, and so is `forget_outputs` under
1407
1408
  the `forget` layer. A `kits:` entry takes `base:` (default `full`), `add:`, `remove:` and `prompt:` (any other
1408
- key, a typo or a `tools:` list, is ignored with a warning); it can't reuse a built-in's name. A bundle declares its own with `chi.toolkit` ([plugins](plugins.md#chitoolkitname-base-add--remove--prompt-nil)).
1409
+ 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)).
1409
1410
  - **A kit's prompt text.** `prompt:` (text, several lines allowed) goes into the system prompt of the sessions on
1410
1411
  that kit, verbatim, with no header: after AGENTS.md and the appended files, before the memory indexes. A kit
1411
- without one inherits its base's (`review: {base: terse, add: [web_fetch]}` stays concise); `prompt: ""` clears
1412
+ without one inherits its base's (`audit: {base: terse, add: [web_fetch]}` stays concise); `prompt: ""` clears
1412
1413
  it; a session's own `--tool` / `/toolkit add` never touch it. Sessions on the same kit share it in the prompt
1413
1414
  cache; `full` and every kit without a prompt send the bytes they sent before. Keep it to a line or two that
1414
1415
  fits the kit's work (the coordinator bundle's `coding`: be concise): one that isn't text or is over 2000
@@ -1716,9 +1717,12 @@ column says so, a `--kebab-name` flag. The maps (`hosts:`, `models:`,
1716
1717
  | `web.host` | `127.0.0.1` | yes | `127.0.0.1`, `::1` or `localhost`; `lan` (this machine's private IPv4 address) or one of its IPv4 addresses also opens `chi web` to the network, with an access token (`chi web --new-token` replaces it). Anything else binds `127.0.0.1` with a warning. See [CLI: chi web on your phone](cli.md#chi-web-on-your-phone). |
1717
1718
  | `web.markdown` | `false` | yes | Render answers as Markdown in `chi web`. |
1718
1719
  | `web.view` | `stage` | yes | How `chi web` draws a turn: `stage` (the running turn pinned above the composer) or `turn` (one block per turn in the history). See [CLI](cli.md#web-views). |
1719
- | `web.working_set` | `false` | yes | The stage view's working set (experimental): the files, memories and URLs a turn brought into the model's context, in a rail in the stage, a footprint line under each finished turn and a `set N` chip in place of the mem chip. `?working_set=1` or `?working_set=0` on the page URL overrides it for that page load; the turn view ignores it. See [CLI: the working set](cli.md#the-working-set). |
1720
+ | `web.working_set` | `true` | yes | The stage view's working set: the files, memories and URLs a turn brought into the model's context, in a rail in the stage, a footprint line under each finished turn and a `set N` chip in place of the mem chip; `false` brings the mem chip back. `?working_set=1` or `?working_set=0` on the page URL overrides it for that page load; the turn view ignores it. See [CLI: the working set](cli.md#the-working-set). |
1720
1721
  | `web.annotate_presets` | `Agreed\|Could you please elaborate?` | yes | Quick replies next to Annotate in `chi web`, `\|`-separated (a YAML list works too); `""` in the file or on the CLI leaves only Annotate (an empty env value means the default). See [CLI](cli.md#web-annotate-presets). |
1721
1722
  | `web.editor` | `vscode` | yes | What a file ref in `chi web` (a file tool row's title, a diff line) opens, for a viewer on this machine: a preset (`vscode`, `vscode-insiders`, `cursor`, `zed`) or a URL template with `{path}` and an optional `{line}`; `none` turns refs off. See [Opening files from chi web](#opening-files-from-chi-web). |
1723
+ | `web.participants.expires` | `7d` | | How long a participant's link lasts unless `chi share --expires` says otherwise: `Nh` or `Nd`, 1h to 30d. See [participants](internals/participants.md). |
1724
+ | `web.participants.cap` | `2.0` | | What a participant's turns, and the side calls they cause, may cost you in dollars unless `chi share --cap` says otherwise ($0 to $1000); at the cap their next comment is refused (429 `cap_reached`) and you get a notice. `chi share cap ID P DOLLARS` changes one grant's. |
1725
+ | `web.participants.queue` | `3` | | How many of a participant's turns may wait behind their running one (0 to 20); one more is refused (429 `too_many_turns`). |
1722
1726
  | `history.file` | state dir | | Prompt history path, shared by the TUI and the web composer. |
1723
1727
  | `no_interrupt` | `false` | `--no-interrupt` | Raise the step limit of a turn to 1000 (or `turn.max_iterations`, when larger); a top-level key. |
1724
1728
  | `no_default_input` | `false` | `--no-default-input` | Don't pre-fill `default.input`; a top-level key. |
data/docs/guardrails.md CHANGED
@@ -298,6 +298,50 @@ card still names `child-boundary`. The parent (never a delegate), forks and
298
298
  ordinary sessions don't get it. Like the rules it reads command text: a
299
299
  `sh -c` body, a script or `$(…)` isn't looked into.
300
300
 
301
+ ## A participant's review boundary
302
+
303
+ Built in, for a [participant's](internals/participants.md#turns) turns only (a
304
+ turn whose author is `p:<id>`), in every mode (also with
305
+ `guardrails.enabled: false`) and without the guardrails bundle:
306
+ `Guardrails::ReviewBoundary`, rule `review-boundary`, source `core`. It comes
307
+ first among the core checks, before the hooks' asks and the rules, and it
308
+ **denies, never asks**: nobody on the participant's side may approve, and the
309
+ owner's approval would give the participant's text the owner's tools. A
310
+ participant's turn may call only the `review` kit's tools:
311
+
312
+ - `read`, under the session's project root (the work tree top of its folder)
313
+ and never a secret: chi's state and config dirs (sessions, the grants, the
314
+ web token, memories and their `.bundles`, `config.yml`, hooks), `~/.ssh`,
315
+ `~/.gnupg`, `~/.aws`, and the project's `.env*`, `.netrc`, `*.pem`, `*.key`,
316
+ `*.p12`, anything named `credentials` (`.git-credentials` too), `.npmrc`,
317
+ `.pypirc`, ssh keys (`id_rsa`, `id_ed25519`, …), `*.tfstate`,
318
+ `secrets.yml`/`.json`, `.docker/config.json`, `.kube/config` and any config
319
+ under `.git` (a submodule's too). The path is checked as given and as it
320
+ resolves, so a symlink in the project to `~/.ssh` is `~/.ssh`. A session
321
+ whose folder is the home folder or above it reads nothing.
322
+ - `memory_read` with `scope: project` (a read without a scope would fall back
323
+ to the system memories). The prompt's preloaded memories and the memory
324
+ index are in the system prompt anyway: a review session should start
325
+ without personal ones.
326
+
327
+ Everything else is denied with "a participant's review turn may only read …",
328
+ and the model is told to propose the change as text or a diff for the owner
329
+ to apply: `write`, `edit`, `memory_write`, `execute`, the task tools,
330
+ `delegate`, `send_note`, `list_sessions`, `register_reminder` (it would fire
331
+ an owner turn later), `web_fetch`, `context_read`, `ask_user_question`,
332
+ `forget_outputs` (it would replace the owner's earlier tool outputs with the
333
+ participant's note, which later owner turns would read as their own), and
334
+ every plugin or MCP tool, including through `call_tool` (a built-in reached
335
+ through `call_tool` is checked as that built-in). The session's declared tools
336
+ stay its own (no prompt rebuild per author), so the model may still try a
337
+ write; the turn's header ("This turn may only read") keeps that rare.
338
+
339
+ The kit is `Toolkit::REVIEW`, a frozen built-in: config's `toolkit.kits.review`
340
+ is refused with one message ([configuration](configuration.md#toolkits)), so
341
+ config can't widen the boundary. Hooks still run on a participant's turn, with
342
+ the owner's power ([hooks](hooks.md#participants-turns)); a hook that rewrites a
343
+ call is checked on the call it leaves.
344
+
301
345
  ## Rules in config.yml
302
346
 
303
347
  ```yaml
@@ -691,6 +735,31 @@ else goes to chi's own rules. It needs nah installed; see its README for the set
691
735
  - Every load failure is shown once, at the start of the first turn
692
736
  (`guardrails> …` in the terminal, a red line on the web).
693
737
 
738
+ ## Participants' text in later turns
739
+
740
+ A participant's message (a review contribution, see
741
+ [internals/participants.md](internals/participants.md#attribution)) stays in
742
+ the history, so later **owner** turns read it. chi marks it: the wire header
743
+ above it says it is a participant's and not the owner's instruction, and the
744
+ system prompt says the same while the session has participants. Owner turns
745
+ keep every guardrail. But an owner turn that says "do what Alex said" hands
746
+ Alex's text the owner's tools: the model will act on it as the owner asked.
747
+ That is the residual risk, documented rather than engineered away: on a
748
+ session with sensitive tools, quote the proposal you want applied in your own
749
+ turn instead.
750
+
751
+ Within its own turn a participant's text can't act: the review boundary
752
+ leaves it reads of the project only, and an injection that convinces the
753
+ model still meets the boundary (containment over detection). What it can do
754
+ is make the model *say* what the history holds: a review turn's model sees the
755
+ whole conversation, earlier tool outputs included, so start a review in a
756
+ fresh session (or a continue), not on a working session full of outputs. A
757
+ line of a participant's text that looks like one of chi's own headers goes
758
+ out quoted (`(quoted) [Steer from the user, …]`), under the real one, in any
759
+ case and after any spaces (chi's context-note and task frames too). The owner's
760
+ due reminders wait for the owner's next turn: they never go into a
761
+ participant's.
762
+
694
763
  ## Limits
695
764
 
696
765
  Text matching on shell commands stops accidents, not a model set on getting
data/docs/hooks.md CHANGED
@@ -74,6 +74,15 @@ of the hook about to run) and the callables `notify:`, `ask_user:`,
74
74
  undefined behaviour. `:before_tool_call` carries no messages (the gate stays
75
75
  cheap).
76
76
 
77
+ ## Participants' turns
78
+
79
+ A [participant's](internals/participants.md#turns) review turn fires the same hooks, and every event of it carries
80
+ `author: "p:<id>"` (the owner's carry no `author:` key, as before); a plugin reads the same as `ctx.turn_author`
81
+ (nil for the owner's turns and between turns). **A hook runs with the owner's power whatever the author**: it may
82
+ read, write and run what the owner's chi may, and `notify`, `ask_user` and `steer` reach the owner. A bundle that
83
+ acts on a turn's text (a prompt rewrite, a fetch of what it names) should check `author:` first: the text may be a
84
+ participant's. The review boundary checks a tool call after the hooks, on the call they leave.
85
+
77
86
  ## What a hook can do: the runtime
78
87
 
79
88
  Besides reading (and, on `:before_tool_call`, voting on) its event, a hook
@@ -133,7 +133,8 @@ append-only within a process, so what changes the prefix is:
133
133
  1. **A system-prompt rebuild** (`SystemPrompt#build` is cached per process and key; `PromptBuilder#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
- overlays and notes); `/thinking` (another key); a toolkit or profile change; a plugin's `tools_changed!`.
136
+ overlays and notes); `/thinking` (another key); a toolkit or profile change; a plugin's `tools_changed!`; the
137
+ session's first participant joining or last leaving (the tail's participants line, participants.md).
137
138
  2. **A tool set change**: the same triggers, plus MCP lines that differ on a respawn.
138
139
  3. **History edits by design**: the `llm_context` `stale`/`forget` stubs, and the image cut moving a batch.
139
140
 
@@ -481,7 +481,9 @@ TUI. Errors in the sink or an observer are caught; they never break the turn.
481
481
 
482
482
  `Engine#announce` puts an event that belongs to no turn (`ANNOUNCEABLE_EVENTS`: `turn_enqueued`, `turn_dequeued`
483
483
  (a queued message taken back before it ran, `{enqueued_id, client_id}`: the remover's), `card`,
484
- `hook_notice`, `guardrail_warning`, `plugin_init_started`, and more) to the observers only. `:side_usage`
484
+ `hook_notice`, `guardrail_warning`, `plugin_init_started`, `participants_changed` (`{participants, joined, left}`:
485
+ who is in the review now and who was invited, revoked or expired since the worker last looked; `ParticipantsWatch`,
486
+ [participants](participants.md#the-owners-terminal)), and more) to the observers only. `:side_usage`
485
487
  (`{kind, side}`: a side request's usage, the recap's, `/btw`'s or `ask_model`'s, and the session's side sums) goes
486
488
  to the observers the same way from `Engine#note_side_usage` (any thread), without being announceable.
487
489
 
@@ -523,6 +525,40 @@ owner wrote it (its input file's `author`, stamped by chi web from the request's
523
525
  has no `author` anywhere: not in its input file, its origin or the session. `GuardrailWiring#begin_turn` gets the
524
526
  origin, and `#turn_author` reads its author for the gate's checks.
525
527
 
528
+ Where a participant's author (`"p:<id>"`; `Steer.participant_author?`: any author but none or `"owner"`) goes:
529
+
530
+ - **The opening message**: `{role: "user", content:, turn_id:, author:, author_name:}` (`Engine#author_fields`;
531
+ the name from `participants.json` at the turn's start, `SessionParticipants`, left out when the sidecar doesn't
532
+ name the participant). Merged input and steers carry the same two keys (`Steer::Line#author`, grouped by sender
533
+ and author so a participant's lines never share the owner's message). Assistant and tool messages carry none:
534
+ `Steer.author_at(messages, index)` resolves the turn's author by position. `ChatLoop#plain` keeps the keys
535
+ (`Steer::AUTHOR_KEYS`).
536
+ - **The wire**: such a message goes out under the review-contribution header (`Steer.header` /
537
+ `Steer.review_header`, both loops through `Steer.wire_text`; see prompt-caching.md). Saved raw.
538
+ - **`Session#last_turn.author`** (`LastTurn#origin` stays the category) and the turn's analytics record's `author`
539
+ (`SessionMetrics`).
540
+ - **Queued turns**: `turn_enqueued`; the accumulator's queued rows. A participant's `/word` or `!line` is never a
541
+ command: `Bridge#handle_post_turn` answers it 403 and `WorkerInbound#queue_as_command` drops it (so
542
+ `QueuedCommand#author` stays nil on every path now).
543
+ - **Delivery**: a participant's input file always waits for a turn of its own (`SessionInbox.waits_for_turn_end?`),
544
+ so `Worker#pending_input_drain` never merges it; the Bridge queues their `POST /turn` whatever its delivery and
545
+ counts their running and queued turns against chi web's `pending_limit` (429 `too_many_turns`).
546
+ `Worker#run_prompt` checks their link and cap (`participant_refusal`: `participants.json`'s entry and cap against
547
+ `SessionMetrics#spend`) before their turn starts and after it ends (the owner's notice, once per cap). A Continue
548
+ keeps the offer turn's author (`ContinueOffer#run_continue_turn`, from `last_turn.author`).
549
+ - **The gate**: `GuardrailWiring#review_boundary` (`Guardrails::ReviewBoundary`) is the first core check; it reads
550
+ `#turn_author` and denies a participant's turn all but `Toolkit::REVIEW_TOOLS` (not `forget_outputs`)
551
+ (docs/guardrails.md). The declared tools stay the session's (D3: no rebuild per author).
552
+ - **Hooks and plugins**: `Engine#turn_author` (the running turn's participant author, nil otherwise) is the hook
553
+ runtime's `turn_author` (`Hooks::Registry#with_runtime` adds `author:` to every event of such a turn) and the
554
+ plugin host's (`ctx.turn_author`).
555
+ - **Spend**: the analytics turn record's `author` feeds `SessionMetrics#spend` / `spend_by_author` (the snapshot's
556
+ `spend_by_author`, `/stats`); `Engine#note_side_usage` stamps a side request's author (`#side_author`: the running
557
+ turn's, the recap's after it, else the owner's) into `SideTotals#by_author`.
558
+ - **Side models**: `IdleRecap::TranscriptFilter` and `Plugin::SideQuestion` name a participant's message as theirs.
559
+ - **The system prompt**: `Engine#refresh_participants!` at a turn's start (after `apply_toolkit!`) rebuilds the
560
+ prompt when the session gains its first active participant or loses its last (the participants line).
561
+
526
562
  ### Hook points (not events)
527
563
 
528
564
  `:session_start`, `:before_turn`, `:after_turn` and `:session_end` are hook points: the Engine fires them on the