samagotchi 0.103.0 → 0.105.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +76 -1
  3. data/bin/chi +39 -2
  4. data/docs/architecture.md +36 -24
  5. data/docs/cli.md +97 -21
  6. data/docs/configuration.md +1 -1
  7. data/docs/guardrails.md +15 -1
  8. data/docs/internals/anthropic.md +2 -1
  9. data/docs/internals/context-telemetry.md +6 -2
  10. data/docs/internals/engine.md +22 -1
  11. data/docs/internals/participants.md +167 -0
  12. data/docs/internals/prompt-caching.md +8 -0
  13. data/docs/internals/remote-sessions.md +153 -27
  14. data/docs/internals/working-set.md +4 -3
  15. data/docs/plugins.md +7 -1
  16. data/docs/sessions.md +4 -2
  17. data/docs/sub-agent.md +8 -0
  18. data/docs/testing.md +89 -4
  19. data/lib/samagotchi/bridge/turn_accumulator.rb +2 -1
  20. data/lib/samagotchi/bridge.rb +17 -10
  21. data/lib/samagotchi/bridge_client/event_stream.rb +9 -4
  22. data/lib/samagotchi/bridge_client.rb +4 -1
  23. data/lib/samagotchi/bundles/system/manifest.yml +1 -1
  24. data/lib/samagotchi/chi_invocation.rb +13 -16
  25. data/lib/samagotchi/config.rb +1 -1
  26. data/lib/samagotchi/context_status.rb +15 -2
  27. data/lib/samagotchi/engine.rb +52 -8
  28. data/lib/samagotchi/guardrail_wiring.rb +7 -1
  29. data/lib/samagotchi/idle_recap.rb +1 -1
  30. data/lib/samagotchi/idle_scheduler.rb +34 -9
  31. data/lib/samagotchi/kernel_loop.rb +8 -1
  32. data/lib/samagotchi/last_turn.rb +5 -2
  33. data/lib/samagotchi/launch_mode.rb +4 -0
  34. data/lib/samagotchi/llm/chat_loop.rb +9 -1
  35. data/lib/samagotchi/owner_lock.rb +65 -5
  36. data/lib/samagotchi/participant_name.rb +21 -0
  37. data/lib/samagotchi/participants.rb +122 -0
  38. data/lib/samagotchi/queued_command.rb +9 -5
  39. data/lib/samagotchi/remote_attach_command.rb +119 -0
  40. data/lib/samagotchi/run_options.rb +14 -2
  41. data/lib/samagotchi/send_command.rb +1 -1
  42. data/lib/samagotchi/session.rb +8 -4
  43. data/lib/samagotchi/session_access/contract.rb +42 -4
  44. data/lib/samagotchi/session_access/dispatch.rb +29 -10
  45. data/lib/samagotchi/session_access/local.rb +52 -1
  46. data/lib/samagotchi/session_access/remote.rb +112 -16
  47. data/lib/samagotchi/session_access/web_bridge_client.rb +60 -3
  48. data/lib/samagotchi/session_access.rb +85 -3
  49. data/lib/samagotchi/session_continue.rb +1 -1
  50. data/lib/samagotchi/session_inbox.rb +18 -9
  51. data/lib/samagotchi/session_manager.rb +21 -5
  52. data/lib/samagotchi/session_metrics.rb +7 -1
  53. data/lib/samagotchi/session_participants.rb +59 -0
  54. data/lib/samagotchi/sessions_command.rb +14 -11
  55. data/lib/samagotchi/share_command.rb +174 -0
  56. data/lib/samagotchi/steer.rb +83 -10
  57. data/lib/samagotchi/system_prompt.rb +21 -2
  58. data/lib/samagotchi/terminal_ui/attach_launcher.rb +17 -4
  59. data/lib/samagotchi/terminal_ui/attach_link.rb +10 -0
  60. data/lib/samagotchi/terminal_ui/attached_loop.rb +117 -29
  61. data/lib/samagotchi/terminal_ui/formatting.rb +13 -1
  62. data/lib/samagotchi/terminal_ui/local_link.rb +5 -0
  63. data/lib/samagotchi/terminal_ui/reline_seam.rb +21 -1
  64. data/lib/samagotchi/terminal_ui/remote_connector.rb +66 -0
  65. data/lib/samagotchi/terminal_ui/remote_link.rb +245 -0
  66. data/lib/samagotchi/terminal_ui/version_lines.rb +23 -0
  67. data/lib/samagotchi/terminal_ui.rb +4 -1
  68. data/lib/samagotchi/testing/waiting.rb +92 -9
  69. data/lib/samagotchi/tools/task_runtime.rb +17 -3
  70. data/lib/samagotchi/turn_checkpoint.rb +5 -2
  71. data/lib/samagotchi/version.rb +1 -1
  72. data/lib/samagotchi/web/app.rb +187 -74
  73. data/lib/samagotchi/web/grants.rb +303 -0
  74. data/lib/samagotchi/web/participant_access.rb +286 -0
  75. data/lib/samagotchi/web/principal.rb +29 -0
  76. data/lib/samagotchi/web/public/index.html +6 -5
  77. data/lib/samagotchi/web/public/working_set.js +6 -4
  78. data/lib/samagotchi/web/public/working_set_view.js +1 -1
  79. data/lib/samagotchi/web/server.rb +3 -1
  80. data/lib/samagotchi/web/session_access_routes.rb +120 -4
  81. data/lib/samagotchi/worker.rb +44 -11
  82. data/lib/samagotchi/worker_inbound.rb +5 -2
  83. metadata +12 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7c4a19357a9f5c94b3c76052abc018a034968ad8baf3520d50ace0ab581943b6
4
- data.tar.gz: ec637848fd8dc33b292d836618753699bc8159962c6051d7b9b669beb31eb50f
3
+ metadata.gz: e0920f477e95206617da7d0d43e39dc981b071b34f456c60a7b669e4836875be
4
+ data.tar.gz: 1f9219bb12b92c2178ab15d57fddfaaee3654972d9b90e2ce11b8265ddab90cf
5
5
  SHA512:
6
- metadata.gz: 5ca1a63b9c68e9b18b21fb2f2bf6503eb3cec3368e10410c4efce78dc5a853e842fc8ddd74bb5387d21f6b0d38c69f4abdb58a5320bf5c9e8272232ebf56234c
7
- data.tar.gz: 7c86172b9193fc68c5c639989573c17f99af9621d118624684ace1483628a01cde8e609c2438368c297e534043fb86a165e94511e34dfd880171d77f2a75eb57
6
+ metadata.gz: 5c7625cff1523fe72e997ec3265465f9385b7a7fe59e8a3f6b097f8015e84f1b32441582218c75b4145f2f8ffb2b58ea7909c30f65efd4dbac114650c4b65f55
7
+ data.tar.gz: 3f2afdc87fa1a48efebbc14138016f2da28502e74b3a69d59ddd2c1ae5c37041af2192a0a071cf78325a14c2f630afe28e42cfed03b625f103fbf88f2037533c
data/CHANGELOG.md CHANGED
@@ -8,6 +8,79 @@ and commands may change between minor versions. How releases are made:
8
8
 
9
9
  ## [Unreleased]
10
10
 
11
+ ## [0.105.0] - 2026-10-23
12
+
13
+ ### Changed
14
+
15
+ - Web: the stage view's **working set** is on by default (no longer experimental). Beside the running turn a rail
16
+ shows the files, memories and URLs it brought into the model's context; each finished turn gets a footprint line;
17
+ the session bar's `● set N` chip (in place of the mem chip) opens the session's list, where rows say whether the
18
+ model still has them (stale, forgotten, dropped) and offer drop, pin, add and refresh. Turn it off with
19
+ `web.working_set: false`, `chi web --no-web-working-set` or `?working_set=0` (the mem chip comes back). See
20
+ [docs/cli.md](docs/cli.md#the-working-set).
21
+
22
+ ### Fixed
23
+
24
+ - `chi --remote NAME sessions list` shows test sessions, marked `[test]`, as the local list does.
25
+ - The attached terminal echoes a command typed after `/unqueue`'s text (`fix it/unqueue`) as the command alone, not the
26
+ whole line.
27
+ - Attaching to a session whose last turn was stopped or canceled shows the turn's canceled line, not the text it
28
+ streamed before the stop as if it were the answer.
29
+
30
+ Update with `chi update`.
31
+
32
+ ## [0.104.0] - 2026-10-23
33
+
34
+ ### Added
35
+
36
+ - Remote attach: `chi --remote NAME --attach ID` (and `--resume ID`, `-p TEXT`, or `--dir DIR` for a new session
37
+ there, that machine's absolute path) runs the attached TUI on another machine's session, through its chi web:
38
+ the same lines and exit codes, every printed command spelled with `--remote NAME`, `@path` images from this
39
+ machine, a "(reconnecting…)" / "(reconnected)" line for about a minute of reconnects, and the version line naming
40
+ both machines. `--non-interactive`, `--verbose`, `--no-shared`, `--memory`, `--mute` and
41
+ `--append-system-prompt` stay this machine's (exit 2). `--dir` without `--remote` exits 2. `/exit`,
42
+ `/exit --delete`, `/archive`, `/stats` and `/recap` work through a remote as here, the status row counts the
43
+ session's delegates there, and a join shows the steps' durations and a failed turn's line from that machine's
44
+ saved records. See [docs/cli.md](docs/cli.md#remote-sessions).
45
+ - Participants (experimental: identity only, the review itself comes in a later release): `chi share ID --name NAME
46
+ [--agent] [--expires 3d]` invites someone, or an agent, to review a session through chi web on the LAN with a link
47
+ of their own, printed once with a QR code (chi keeps only a digest of its token). `chi share ls [ID]` lists
48
+ participants and `chi share revoke ID [P] | --all` ends links; all three work through `--remote`. A link works for 7
49
+ days (1 hour to 30 days), until it is revoked, or until the session is archived or deleted. For now it opens a page
50
+ with the session's title and status; every other route answers it 403, and another session 404. chi web refuses to
51
+ start when another user can read its grants store. Through `--remote`, a revoked, expired or participant's token
52
+ gets its own message instead of "the token was refused". See [docs/cli.md](docs/cli.md#participants).
53
+ - The bundle test kit (experimental) waits with `wait_for(what) { … }`, which raises
54
+ `Samagotchi::Testing::Waiting::TimedOut` naming what it waited for (and, with `seen:`, what it last saw), plus
55
+ `join!` and `pop!` with the same message. Every helper deadline is multiplied by `SPEC_TIME_SCALE` (default 1).
56
+ See [docs/plugins.md](docs/plugins.md#testing-a-bundle-experimental).
57
+
58
+ ### Changed
59
+
60
+ - The bundle test kit's `wait_until!` is renamed `wait_for`, with no alias: a bundle spec that calls
61
+ `wait_until!` changes the name (same arguments).
62
+
63
+ ### Fixed
64
+
65
+ - `chi --attach ID` (and `--resume ID`, and `chi --remote NAME --attach ID`) right after `/exit`, while the leaving
66
+ worker still writes its recap, waits for it with `(waiting for the session to finish its recap…)` (up to
67
+ `recap.timeout` plus 5 s) and then attaches, instead of failing "did not start its Bridge in time". A Ctrl-C while
68
+ an attach connects exits 130 without a backtrace.
69
+ - `/queue TEXT` (or `/cut TEXT`) with no turn running says `(nothing is running: sent now)` in the REPL and the
70
+ attached terminal, instead of sending without a word.
71
+ - The ctx % no longer dips at the start of each turn on a chat host (`api: openai` / `anthropic`): the turn's first
72
+ estimate, made before the host counts anything, left out the tool schemas; it now uses the last count's chars per
73
+ token.
74
+ - Attached terminal: a command typed right after the text `/unqueue` put back in the input (`/unqueue` again,
75
+ `/queue TEXT`) runs as the command, with the text kept in the input, instead of going out joined to the text as
76
+ one prompt.
77
+ - `chi --remote NAME sessions stop` waits up to 10 s there for the worker to let go, as `chi sessions stop` does
78
+ here (chi web waited 2 s, so a slow worker read "still shutting down" more often); the page's Stop still waits 2 s.
79
+ - The terminal's status line, cut to the terminal's width, ends at a word with `…` instead of mid-word (in a pipe
80
+ too).
81
+
82
+ Update with `chi update`.
83
+
11
84
  ## [0.103.0] - 2026-10-23
12
85
 
13
86
  ### Added
@@ -3892,7 +3965,9 @@ and long-lived sessions.
3892
3965
  - A macOS desktop helper (`chi desktop install`): a "Send to chi" Service and a
3893
3966
  hotkey panel that send selected text or the clipboard to your sessions.
3894
3967
 
3895
- [Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.103.0...HEAD
3968
+ [Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.105.0...HEAD
3969
+ [0.105.0]: https://github.com/dm1try/samagotchi/compare/v0.104.0...v0.105.0
3970
+ [0.104.0]: https://github.com/dm1try/samagotchi/compare/v0.103.0...v0.104.0
3896
3971
  [0.103.0]: https://github.com/dm1try/samagotchi/compare/v0.102.0...v0.103.0
3897
3972
  [0.102.0]: https://github.com/dm1try/samagotchi/compare/v0.101.0...v0.102.0
3898
3973
  [0.101.0]: https://github.com/dm1try/samagotchi/compare/v0.100.1...v0.101.0
data/bin/chi CHANGED
@@ -52,6 +52,8 @@ SUBCOMMANDS = {
52
52
  "send" => ["samagotchi/send_command", "SendCommand"],
53
53
  # answer the question a session waits on, then wait (--question, --option)
54
54
  "answer" => ["samagotchi/answer_command", "AnswerCommand"],
55
+ # invite participants to review a session, list them, revoke them
56
+ "share" => ["samagotchi/share_command", "ShareCommand"],
55
57
  # find a model server and write config.yml (--model, --name)
56
58
  "bootstrap" => ["samagotchi/bootstrap_command", "BootstrapCommand"],
57
59
  # bring an installed chi up to date
@@ -69,7 +71,10 @@ SUBCOMMANDS = {
69
71
  }.freeze
70
72
  # chi --remote NAME <command> (or CHI_REMOTE=NAME): the session commands
71
73
  # against another machine's chi web; what runs here only is refused (exit 2).
72
- # chi remote, --version and --help run here as always.
74
+ # chi remote, --version and --help run here as always. Run flags under a
75
+ # remote (--attach, --resume, -p, --dir: a session there in this terminal)
76
+ # are parsed below and go to RemoteAttachCommand before this machine's
77
+ # checks; the local-only ones are refused here.
73
78
  require "samagotchi/session_access/dispatch"
74
79
  begin
75
80
  remote_run = Samagotchi::SessionAccess::Dispatch.from(ARGV)
@@ -78,7 +83,12 @@ rescue Samagotchi::SessionAccess::Dispatch::Usage => e
78
83
  exit 2
79
84
  end
80
85
  ARGV.replace(remote_run.argv)
81
- exit remote_run.run(SUBCOMMANDS) if remote_run.remote? && !remote_run.here?
86
+ remote_attach = remote_run.remote? && !remote_run.here? && remote_run.run_flags?
87
+ if remote_attach && (refusal = remote_run.refusal)
88
+ warn "Error: #{refusal}"
89
+ exit 2
90
+ end
91
+ exit remote_run.run(SUBCOMMANDS) if remote_run.remote? && !remote_run.here? && !remote_attach
82
92
 
83
93
  if (path, klass = SUBCOMMANDS[ARGV[0]])
84
94
  require path
@@ -152,6 +162,29 @@ Samagotchi::Config.reload!(cli_overrides: cli_overrides) unless cli_overrides.em
152
162
  require "samagotchi/log"
153
163
  Samagotchi::Log.configure(level: options[:verbose] ? :debug : nil, mirror: options[:verbose])
154
164
 
165
+ # chi --remote NAME --attach ID (--resume ID, --dir DIR, -p TEXT): the
166
+ # attached TUI on that machine's session. Nothing below runs: it checks
167
+ # this machine's sessions, memories and files.
168
+ if remote_attach
169
+ unless ARGV.empty?
170
+ warn "Error: unknown command #{ARGV.first} (see chi --help)"
171
+ exit 1
172
+ end
173
+ require "samagotchi/remote_attach_command"
174
+ begin
175
+ entry = Samagotchi::Remotes.find(Samagotchi::Remotes.check_name!(remote_run.remote))
176
+ rescue Samagotchi::Remotes::Invalid => e
177
+ warn "Error: #{e.message}"
178
+ exit 2
179
+ end
180
+ exit Samagotchi::RemoteAttachCommand.new(options, access: Samagotchi::SessionAccess::Remote.new(entry)).run
181
+ end
182
+ # --dir names a folder on a remote; here the session starts in this one.
183
+ if options[:dir]
184
+ warn "Error: --dir goes with --remote; cd there instead"
185
+ exit 2
186
+ end
187
+
155
188
  # `chi web`: its flags (--port, --open, --web-view…) went through the parser above.
156
189
  options[:web] = true if web
157
190
 
@@ -340,6 +373,10 @@ if launch == :attached
340
373
  rescue Samagotchi::TerminalUI::AttachLauncher::Error => e
341
374
  warn "Error: #{e.message}"
342
375
  exit 1
376
+ rescue Interrupt
377
+ # Ctrl-C while it connects (a wait for a leaving worker, a late Bridge):
378
+ # no backtrace.
379
+ exit Samagotchi::CLI::Exit::INTERRUPTED
343
380
  end
344
381
  exit Samagotchi::TerminalUI::AttachLauncher.exit_status(ended)
345
382
  end
data/docs/architecture.md CHANGED
@@ -469,7 +469,8 @@ Every route goes through `Bridge#dispatch`: an id other than the bridge's own se
469
469
  The per-session port is OS-assigned (bound to `0`) and published to a `bridge.json` sidecar
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
- `GET /api/sessions/:id/output`). Resume/ring-buffer state is **in-memory**, per worker: a cursor from
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
474
  an earlier worker gets the `reset` snapshot, not a replay.
474
475
 
475
476
  **Session hub.** The cross-session layer (which sessions exist, who owns them, what changed)
@@ -510,35 +511,46 @@ chi web (the remote-sessions plan; `SessionAccess::Contract` says which commands
510
511
  cover most of it; `Web::SessionAccessRoutes` (`web/session_access_routes.rb`, in `App`'s `ROUTES`) adds the rest, each
511
512
  calling the `SessionAccess::Local` operation of the same name, so a remote backend maps 1:1 onto them. Every one is
512
513
  token-gated like the rest (LAN mode: `Authorization: Bearer`), and one that takes an id is a `:session` route (an id
513
- that isn't one, or no such session: 404 `not_found`). How a request flows end to end, which checks run where and the trust
514
+ that isn't one, or no such session: 404 `not_found`). Every `ROUTES` row also names who may use it, checked against
515
+ 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
518
+ [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
514
519
  model: [internals/remote-sessions.md](internals/remote-sessions.md).
515
520
 
516
- | Route | Body / params | Answer | Refusals |
517
- |---|---|---|---|
518
- | `GET /api/sessions/resolve?id=` | an id or a unique prefix | 200 `{id}` | 404 `not_found`; 400 `ambiguous_id` with `ids` |
519
- | `GET /api/sessions/summaries` | `dir` (a folder: its project; none: every project), `cwd`, `live=1`, `archived=1`, `tests=0`, `sort`, `order`, `limit` | 200: the rows `chi sessions list` prints (`SessionManager.session_summaries`: `status`, `live`, `busy`, `owner`, `recap`, `ctx_pct`, `waiting`, `waiting_id`, `title`, `preview`, …) | 400 `invalid_dir`, `invalid_cwd` |
520
- | `POST /api/sessions` (more fields) | `toolkit`, `tools`, `no_tools`, `parent_answers`; `client_id` (`cli:send`/`cli:answer`: the first line is checked as chi send checks it); `check_vision: true` (images follow) | 201 adds `model_warning` and `parent_answers_warning` (a level looser than config.yml's, capped) | 400 `invalid_toolkit`, `invalid_parent_answers`, `invalid_continues` (beside `continues`); 403 `parent_answers_refused`, `parent_continue_refused`; 422 `no_vision` |
521
- | `POST /api/sessions/:id/turn` (more fields) | `client_id` as above; `check_vision: true` with `images` | as before | 403 `parent_answers_refused`, `parent_continue_refused`; 422 `no_vision` |
522
- | `POST /api/sessions/:id/wait/prepare` | `question_id` (absent: the pending one; `null`: none), `turn`, `created`, `busy` (+ `queue`) | 200 `{session_id, cursor, baseline: {messages, question_id, last_turn}, own_turn, known_turn, running, live, settled, last_turn_id}` (`WaitStart`) | 409 `busy`, `waiting` (`SendCommand::Busy`'s words, and its fields: `question`, `parent_strategy`) |
523
- | `POST /api/sessions/:id/wait` | `cursor`, `baseline`, `turn`, `queued`, `ended_before`, `owner_grace`, `timeout` (capped at 30 s) | 200: `ReplyWait::Result`'s fields (`status`: `done`, `waiting_for_answer`, `no_reply`, `error`, `stopped`, `worker_gone`, or `timeout`: ask again; `text`, `file`, `question` as `PendingQuestion#to_file`, `outcome`, `limit`, `error_kind`, `retryable`, `cancel_reason`, `stopped_by`, `kept_steps`, `turn_id`, `origin`; never the session) and `parent_strategy` `{name, allowed, origin}` (`ParentReport.strategy_for`, this server's config.yml) | 400 `invalid_wait`; 503 `server_stopping` |
524
- | `POST /api/sessions/:id/notes` | `text`, `source` (default `cli`, never `broadcast`) | 200 `{status: queued \| waits, session_id, queued}` (`NoteDelivery`) | 409 `owned_by_tui` (`status: refused`); 400 `note_rejected`, `missing_fields`, `invalid_source` |
525
- | `POST /api/sessions/:id/context` (more bodies) | `{name, push: true, why, hint}` or `{name, cmd, every, why, hint}` beside the page's `{url, why, hint}`; `added_by` `cli`/`agent` (else `web`) | 201 `{status: attached, name}` | 400 `invalid_source`; 409 `exists` |
526
- | `POST /api/sessions/:id/context/:name/push` | `text`, `summary`, `wake`, `hint` (a source's JSON output, checked the same way) | 200 `{status: pushed, name, before, revision, changed}` | 404 `not_found`; 409 `not_push`; 400 `invalid_text` |
527
- | `DELETE /api/sessions/:id/context/:name?own=1` | | 200 `{status: detached}`: the session's own only | 409 `project_source` (chi context rm's words); 404 |
528
- | `POST /api/sessions/:id/stop` | | 200 `{status: stopped, session_id, released}`: `released`, whether the worker let go within `STOP_WAIT_SECONDS` (2 s) | 409 `owned_by_tui` |
529
- | `DELETE /api/sessions/:id?force=` | `force=true` stops a live worker first (the page sends it) | as before | 409 `worker_running` without it |
530
- | `GET /api/sessions/:id?brief=1` | | 200 `{brief, session: {id, status (the file's), title, display_title, project_root, working_directory, model_name, model_typed, parent_answers, owner}, pending_question}` (the file's; no messages): `Remote#show` | 404 |
531
- | `POST /api/sessions/:id/turn` (its 202) | | the ack with `via`: `bridge` or `file` (the input file: a remote `chi send --wait` follows that turn) | |
532
- | `GET /api/sessions/:id/context?full=1`, `…/context/:name?full=1` | | each row with `source` (`Source#to_h`, its command too; the page's rows never carry one), `snapshot` (`Snapshot#to_h`, no text) and `shadowed`, shadowed project sources included | |
533
- | `POST /api/sessions/:id/wake` | `wait` (seconds, default and most 10: `WAKE_MAX_WAIT`) | 200 `{session_id, woke, worker: {epoch, started_at, version, features}, model_name, default_model, parent_strategy, installed}`: the live worker as is (`woke: false`), else resumed and its Bridge waited for (`Local#wake`); `default_model` and `installed` are this machine's | 409 `owned_by_tui`, `scratch` (`LaunchMode::SCRATCH_LEFTOVER`); 504 `worker_timeout` (`LaunchMode::BRIDGE_LATE`); 400 `invalid_wait`; 404 |
534
- | `GET /api/sessions/:id/worker` | | 200 `{session_id, live, stopped, owner: worker \| tui \| null, worker: {epoch, started_at, version, features} \| null, parent_strategy}` (`Local#worker`; never starts one) | 404 |
535
- | `POST /api/sessions/:id/turn` (`no_interrupt`) | `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 | |
536
- | `POST /api/sessions/:id/command` (its 501) | | | 501 `not_supported` for a builtin the worker lacks adds `command` (`"/title"`) and `message` |
521
+ | Route | Permission | Body / params | Answer | Refusals |
522
+ |---|---|---|---|---|
523
+ | `GET /api/sessions/resolve?id=` | owner | an id or a unique prefix | 200 `{id}` | 404 `not_found`; 400 `ambiguous_id` with `ids` |
524
+ | `GET /api/sessions/summaries` | owner | `dir` (a folder: its project; none: every project), `cwd`, `live=1`, `archived=1`, `tests=0`, `sort`, `order`, `limit` | 200: the rows `chi sessions list` prints (`SessionManager.session_summaries`: `status`, `live`, `busy`, `owner`, `recap`, `ctx_pct`, `waiting`, `waiting_id`, `title`, `preview`, …) | 400 `invalid_dir`, `invalid_cwd` |
525
+ | `POST /api/sessions` (more fields) | owner | `toolkit`, `tools`, `no_tools`, `parent_answers`; `client_id` (`cli:send`/`cli:answer`: the first line is checked as chi send checks it); `check_vision: true` (images follow) | 201 adds `model_warning` and `parent_answers_warning` (a level looser than config.yml's, capped) | 400 `invalid_toolkit`, `invalid_parent_answers`, `invalid_continues` (beside `continues`); 403 `parent_answers_refused`, `parent_continue_refused`; 422 `no_vision` |
526
+ | `POST /api/sessions/:id/turn` (more fields) | owner | `client_id` as above; `check_vision: true` with `images` | as before | 403 `parent_answers_refused`, `parent_continue_refused`; 422 `no_vision` |
527
+ | `POST /api/sessions/:id/wait/prepare` | owner | `question_id` (absent: the pending one; `null`: none), `turn`, `created`, `busy` (+ `queue`) | 200 `{session_id, cursor, baseline: {messages, question_id, last_turn}, own_turn, known_turn, running, live, settled, last_turn_id}` (`WaitStart`) | 409 `busy`, `waiting` (`SendCommand::Busy`'s words, and its fields: `question`, `parent_strategy`) |
528
+ | `POST /api/sessions/:id/wait` | owner | `cursor`, `baseline`, `turn`, `queued`, `ended_before`, `owner_grace`, `timeout` (capped at 30 s) | 200: `ReplyWait::Result`'s fields (`status`: `done`, `waiting_for_answer`, `no_reply`, `error`, `stopped`, `worker_gone`, or `timeout`: ask again; `text`, `file`, `question` as `PendingQuestion#to_file`, `outcome`, `limit`, `error_kind`, `retryable`, `cancel_reason`, `stopped_by`, `kept_steps`, `turn_id`, `origin`; never the session) and `parent_strategy` `{name, allowed, origin}` (`ParentReport.strategy_for`, this server's config.yml) | 400 `invalid_wait`; 503 `server_stopping` |
529
+ | `POST /api/sessions/:id/notes` | owner | `text`, `source` (default `cli`, never `broadcast`) | 200 `{status: queued \| waits, session_id, queued}` (`NoteDelivery`) | 409 `owned_by_tui` (`status: refused`); 400 `note_rejected`, `missing_fields`, `invalid_source` |
530
+ | `POST /api/sessions/:id/context` (more bodies) | owner | `{name, push: true, why, hint}` or `{name, cmd, every, why, hint}` beside the page's `{url, why, hint}`; `added_by` `cli`/`agent` (else `web`) | 201 `{status: attached, name}` | 400 `invalid_source`; 409 `exists` |
531
+ | `POST /api/sessions/:id/context/:name/push` | owner | `text`, `summary`, `wake`, `hint` (a source's JSON output, checked the same way) | 200 `{status: pushed, name, before, revision, changed}` | 404 `not_found`; 409 `not_push`; 400 `invalid_text` |
532
+ | `DELETE /api/sessions/:id/context/:name?own=1` | owner | | 200 `{status: detached}`: the session's own only | 409 `project_source` (chi context rm's words); 404 |
533
+ | `POST /api/sessions/:id/stop` | owner | `wait?` (seconds, capped at `SessionManager::STOP_WAIT`, 10: `chi --remote NAME sessions stop` sends it) | 200 `{status: stopped, session_id, released}`: `released`, whether the worker let go within the wait (without one `STOP_WAIT_SECONDS`, 2 s: the page's Stop) | 409 `owned_by_tui` |
534
+ | `DELETE /api/sessions/:id?force=` | owner | `force=true` stops a live worker first (the page sends it) | as before | 409 `worker_running` without it |
535
+ | `GET /api/sessions/:id?brief=1` | participant (theirs: title, status, participants) | | 200 `{brief, session: {id, status (the file's), title, display_title, project_root, working_directory, model_name, model_typed, parent_answers, owner}, pending_question}` (the file's; no messages): `Remote#show` | 404 |
536
+ | `POST /api/sessions/:id/turn` (its 202) | owner | | the ack with `via`: `bridge` or `file` (the input file: a remote `chi send --wait` follows that turn) | |
537
+ | `GET /api/sessions/:id/context?full=1`, `…/context/:name?full=1` | owner | | each row with `source` (`Source#to_h`, its command too; the page's rows never carry one), `snapshot` (`Snapshot#to_h`, no text) and `shadowed`, shadowed project sources included | |
538
+ | `POST /api/sessions/:id/wake` | owner | `wait` (seconds, default and most 10: `WAKE_MAX_WAIT`) | 200 `{session_id, woke, worker: {epoch, started_at, version, features}, model_name, default_model, parent_strategy, installed}`: the live worker as is (`woke: false`), else resumed and its Bridge waited for (`Local#wake`); `default_model` and `installed` are this machine's | 409 `owned_by_tui`, `scratch` (`LaunchMode::SCRATCH_LEFTOVER`); 504 `worker_timeout` (`LaunchMode::BRIDGE_LATE`); 400 `invalid_wait`; 404 |
539
+ | `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 |
540
+ | `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` |
541
+ | `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 |
542
+ | `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` |
543
+ | `GET /api/sessions/:id/children/counts` | owner | | 200 `{running, waiting, unreported}` (`ChildrenStatus.counts`, the delegates' files here) | 404 |
544
+ | `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 |
545
+ | `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 | |
546
+ | `POST /api/sessions/:id/command` (its 501) | owner | | | 501 `not_supported` for a builtin the worker lacks adds `command` (`"/title"`) and `message` |
547
+ | `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 |
537
548
 
538
549
  `GET /api/info` names the feature `session_commands` (`SessionAccessRoutes::FEATURE`): `SessionAccess::Remote` asks it
539
550
  once before its first request, and a chi web without it (or `serve_static`'s 404 for a path no route takes) is "remote NAME runs chi X, too
540
551
  old for this command through --remote: update chi there (chi update)". The feature `remote_attach`
541
- (`SessionAccessRoutes::ATTACH_FEATURE`) names the wake and worker routes a remote attached TUI needs.
552
+ (`SessionAccessRoutes::ATTACH_FEATURE`) names the routes a remote attached TUI needs: wake, worker, exit, recap, stats,
553
+ children counts and a turn's records. Their refusals carry `{error, detail, message}`.
542
554
 
543
555
  `POST …/answer` passes on `client_id` only when it is chi answer's (`cli:answer`), so the worker holds the answer to
544
556
  its `guardrails.parent_answers`; any other is dropped, so a forwarded id can only tighten. The worker's 403
data/docs/cli.md CHANGED
@@ -15,14 +15,16 @@
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 --remote NAME send|answer|note|context|sessions …` (or `CHI_REMOTE=NAME`) — those commands on that machine's sessions, through its chi web (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))
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
+ - `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))
26
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))
27
29
  - `chi desktop install|upgrade|uninstall|status` — the macOS "Send to chi" helper: a Service and a ⌃⌥⌘N hotkey that send text or images to a session (live, stopped or new) as a message (⏎, a turn runs) or a context note (⌘⏎) (see [Desktop helper](desktop.md))
28
30
  - `chi models [--format text|json] [--timeout S] [TEXT]` — list the models every configured host offers, as the names `--model` takes (see [Listing the models](#listing-the-models))
@@ -280,7 +282,8 @@ controls exit behavior (`--non-interactive`); `--resume` composes with both.
280
282
  | `--shared` | Run the session (new, or `--resume`'s) in a background worker and attach to it: the default, and the way to get it when `session.shared` is off. See [Sharing a session](#sharing-a-session). |
281
283
  | `--no-shared` | Run the plain in-process REPL for this run. |
282
284
  | `--attach SESSION_ID` | Attach to a session's worker, waking one if it has exited. |
283
- | `--remote NAME` | Before a session command (`chi --remote studio send …`): run it against that remote's sessions, through its chi web; beats `CHI_REMOTE`. Commands and flags that run on this machine only exit 2. See [Remote sessions](#remote-sessions). |
285
+ | `--remote NAME` | Before a session command (`chi --remote studio send …`) or the run flags (`chi --remote studio --attach ID`, `--resume ID`, `--dir DIR -p TEXT`): run it against that remote's sessions, through its chi web; beats `CHI_REMOTE`. Commands and flags that run on this machine only exit 2. See [Remote sessions](#remote-sessions). |
286
+ | `--dir DIR` | With `--remote`: a new session's folder on that machine, its absolute path (required for a new session there). Without `--remote` it exits 2: cd there instead. |
284
287
  | `--model NAME` | Use this model for the run (overrides the configured default and a resumed session's model). |
285
288
  | `--thinking LEVEL` | This session's own thinking level: `off`, `low`, `medium` or `high`, saved in the session and first in the order (`default` unsets it). On `--resume` or `--attach` it is set at once, through a `/thinking` on an attached worker. With `chi web` it is instead the default for the sessions its workers run, below a session's own (as env `SAMAGOTCHI_THINKING_LEVEL` is). See "Thinking" in configuration.md. |
286
289
  | `--toolkit NAME`, `--tool NAME`, `--no-tool NAME` | A new session's toolkit, the tools its prompts declare: `full` (every built-in), `lean`, `child`, one of config's `toolkit.kits` or a bundle's (without `--toolkit`: config's `toolkit.default`), with tools declared on top of it (`--tool`) or left out (`--no-tool`); both repeatable, a comma list too. Saved in the session as given; a respawned worker, a fork and a continue keep it. Ignored with `--attach`/`--resume` (one line, as `--memory`); `/toolkit` changes it. Not on `chi web` (config's `toolkit.default` covers its sessions). See [Toolkits](configuration.md#toolkits). |
@@ -750,12 +753,14 @@ REPL alike:
750
753
  with images works the same way: it waits for the turn's end, and the lines
751
754
  sent after it still go in at the next step.
752
755
  At the open prompt (no turn running) `/cut TEXT` and `/queue TEXT` run TEXT
753
- as a normal prompt.
756
+ as a normal prompt, and the terminals say so: `(nothing is running: sent now)`.
754
757
  - `/unqueue` (attached terminal) takes back the newest line this terminal
755
758
  queued that hasn't started: `removed: <its first 60 characters>`, and its text
756
759
  goes back into the input to change and send again (not over typed text: then
757
760
  it is in the input history, ↑; not with input from a pipe; its images don't
758
- come back), then the one before it on the next `/unqueue`; `(nothing queued: …)` when none waits,
761
+ come back), then the one before it on the next `/unqueue`. A command typed
762
+ after that text without clearing it (`fix it/unqueue`, `fix it/queue more`)
763
+ runs as the command (the scrollback shows the command alone), and the text stays in the input. `(nothing queued: …)` when none waits,
759
764
  `(not removed: it already started)` when its turn won the race, `(not removed:
760
765
  it is already gone)` when another client removed it. Other terminals that
761
766
  showed it say `(a queued message was removed)`; the web drops its bubble. A
@@ -1005,12 +1010,12 @@ made (up to 120 lines or 8 KB), live and after a reload (see
1005
1010
 
1006
1011
  ### The working set
1007
1012
 
1008
- `web.working_set: true` (or `chi web --web-working-set`,
1009
- `SAMAGOTCHI_WEB_WORKING_SET=true`; `?working_set=1` or `?working_set=0` on the
1010
- page URL for one page load) shows, in the stage view, what a turn brought into
1011
- the model's context: the files it read, edited or wrote, the memories it read
1012
- or wrote and the URLs it fetched. Off by default while it is tried out; the
1013
- 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.
1014
1019
 
1015
1020
  - **The rail**, on the right of the stage's live slots: the running turn's set
1016
1021
  in four lines, folded as it grows (files under one folder become
@@ -1022,8 +1027,10 @@ turn view ignores it.
1022
1027
  on a phone it isn't shown.
1023
1028
  - **The footprint**, one line under each finished turn's block, live and after
1024
1029
  a reload: `touched …/web/public/ · 4 files · ◆ 2 memories s2–10 · +1 url ·
1025
- edited 1`. A step tag (`s2`) goes to that call's row; the rest opens the
1026
- 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.
1027
1034
  - **The `set N` chip** in the session bar, in place of the `mem N` chip (whose
1028
1035
  tooltip goes too): the session's whole set, preloaded (`--memory`) and muted
1029
1036
  (`--mute`) memories included; its dot lights when the set changed since you
@@ -1189,10 +1196,9 @@ CHI_REMOTE=studio chi context push claude-outbox ab12 < answer.json
1189
1196
  list|stop|restart|archive|unarchive|delete`. The rest runs on this machine
1190
1197
  only and exits 2 with `chi <command> runs on this machine only: run it on
1191
1198
  <remote>`: `context refresh|mute|unmute` and `--project`, `sessions
1192
- stats|prune|clean`, `broadcast`, the machine's own commands (`bundle`,
1193
- `memory`, `update`, …) and a session in this terminal (`--attach`, `-p`,
1194
- `--resume`: remote attach comes later). `chi remote`, `--version` and
1195
- `--help` always run here.
1199
+ stats|prune|clean`, `broadcast` and the machine's own commands (`bundle`,
1200
+ `memory`, `update`, …). `chi remote`, `--version` and `--help` always run
1201
+ here.
1196
1202
  - `send --new` needs `--dir`, that machine's absolute path (no folder of this
1197
1203
  one is guessed); `--toolkit`, `--set` and `--parent-answers` are checked
1198
1204
  against that machine's config and bundles. `sessions list` lists every
@@ -1208,18 +1214,88 @@ CHI_REMOTE=studio chi context push claude-outbox ab12 < answer.json
1208
1214
  `--timeout`; exit codes are the same. The commands chi prints for what to
1209
1215
  do next work as pasted: they carry `--remote NAME` (`chi --remote studio
1210
1216
  answer ab12 --question q1 --option N`, `chi --remote studio send --wait …`),
1211
- under `CHI_REMOTE` too. What runs on this machine only isn't offered:
1212
- where chi says `chi --attach ID` here it names that machine's chi web
1213
- (`chi web on studio shows it`), and `sessions stop` names no `chi
1214
- --resume`.
1217
+ under `CHI_REMOTE` too, an attach as well (`chi --remote studio --attach
1218
+ ab12 shows it`, `sessions stop`'s `chi --remote studio --resume ab12`).
1215
1219
  - A remote that can't be reached names itself and why (`chi send: remote
1216
1220
  studio (http://192.168.1.29:4567): connection refused`, exit 1; with
1217
1221
  `--format json` an `error` object).
1218
1222
  - Plain http, the same as the phone's: use it on your home network.
1219
1223
 
1224
+ A session there in this terminal (the attached TUI, through that machine's
1225
+ chi web; its worker stays there):
1226
+
1227
+ ```sh
1228
+ chi --remote studio --attach ab12 # attach (wakes its worker if it exited)
1229
+ chi --remote studio --resume ab12 -p "go on" # resume there, run one turn, stay attached
1230
+ chi --remote studio --dir /Users/me/app -p "hi" # a new session there (--dir required), one turn, stay
1231
+ ```
1232
+
1233
+ - The same TUI and exit codes (0 detached, 3 a question left waiting with
1234
+ input from a pipe, 1 otherwise), every printed command with `--remote
1235
+ studio`. Prompts, session commands, `/model`, `!cmd` (a shell command **on
1236
+ that machine**, under its guardrails), `/queue`, `/cut`, `/unqueue`,
1237
+ `/detach`, `/exit`, `/exit --delete`, `/archive`, `/stats` and `/recap` work
1238
+ as here, on that machine's session (`/exit --delete` deletes it there).
1239
+ The status row's children segment counts its delegates there, and a join
1240
+ shows the steps' durations and a failed or canceled turn's line from its saved records
1241
+ there. A chi web too old for one of them says so in one line ("remote
1242
+ studio runs chi X, too old for this command through --remote: update chi
1243
+ there").
1244
+ - A new session needs `--dir`, that machine's absolute path; `--model`,
1245
+ `--thinking`, `--llm-context*`, `--toolkit`, `--tool`, `--no-tool`, `--set`
1246
+ and `--parent-answers` are checked there (exit 2 for a bad one). With
1247
+ `--attach`/`--resume` they work as here (`--model`, `--thinking`,
1248
+ `--llm-context*` change the session; the others are named as ignored).
1249
+ `--non-interactive` (use `chi --remote studio send --new --dir D -m TEXT
1250
+ --wait`), `--verbose`, `--no-shared`, `--memory`, `--mute` and
1251
+ `--append-system-prompt` run on this machine only (exit 2).
1252
+ - `@path` images are this machine's files (read and downscaled here, then
1253
+ uploaded); Tab completes `/` commands and `@path`, not memory names. The
1254
+ prompt history (↑) and `no_interrupt` / `no_default_input` are this
1255
+ terminal's.
1256
+ - When chi web can't be reached the TUI says "(lost the connection to
1257
+ studio; reconnecting…)" and tries for about a minute, then "(reconnected)";
1258
+ what it missed is replayed or resynced. A prompt typed meanwhile is usually
1259
+ accepted there and runs when a worker reads it. A worker that went away
1260
+ (not stopped) is woken again. The end is worded by why: "The session was
1261
+ stopped on studio. Resume it with: chi --remote studio --resume ab12", the
1262
+ token refused, chi web unreachable, the worker gone, the session deleted.
1263
+ - The version line names both machines: the worker older than what studio
1264
+ has installed (`chi --remote studio sessions restart ab12`), this terminal
1265
+ older than studio (`chi update` here).
1266
+
1220
1267
  How it works inside (`SessionAccess`, chi web's routes, where each check runs):
1221
1268
  [internals/remote-sessions.md](internals/remote-sessions.md).
1222
1269
 
1270
+ ### Participants
1271
+
1272
+ A session's owner can invite **participants**, people or agents, to review it through chi web on the LAN
1273
+ (`web.host: lan`), each with a link of their own that reaches that one session:
1274
+
1275
+ ```sh
1276
+ chi share 3f9a1c2e --name Alex
1277
+ # p_7c01d2aa Alex (human, reviewer) · session 3f9a1c2e · expires 2026-10-30 14:02
1278
+ # http://192.168.1.29:4567/?token=chp_…#s=3f9a1c2e-…
1279
+ # (shown once: chi keeps only its digest; chi share revoke 3f9a1c2e p_7c01d2aa ends it)
1280
+ chi share ls # every session's participants (--format json; never a token)
1281
+ chi share revoke 3f9a1c2e # every link to that session; add a participant id for one
1282
+ chi share revoke --all # every link to every session
1283
+ ```
1284
+
1285
+ - The link is printed once (with a QR code at a terminal): chi keeps only a digest of its token. Send it to the
1286
+ participant; opening it sets their browser's cookie, as the owner's LAN link does. `--agent` marks an agent
1287
+ participant, which uses the token as `Authorization: Bearer` (e.g. `chi --remote` with that token).
1288
+ - It works for 7 days (`--expires Nh` or `Nd`, 1 hour to 30 days), until it is revoked, or until the session is
1289
+ archived or deleted. A revoked or expired link shows "This review link has expired or was revoked."
1290
+ - For now a participant sees a page with the session's title and status; comments and the review itself come in
1291
+ a later chi. They can't reach anything else: every other route answers them 403, another session 404.
1292
+ - `chi --remote studio share …` invites on studio: the link names studio's LAN address. Locally, the link names
1293
+ this machine's chi web; without one on the LAN, chi says the link opens on this machine only.
1294
+ - The owner's own token and the `chi web --new-token` rotation are separate: rotating it ends no participant's link.
1295
+
1296
+ How it works inside (grants, the gate, the routes, the audit trail):
1297
+ [internals/participants.md](internals/participants.md).
1298
+
1223
1299
  ## Runtime Model Switch (Assist Mode)
1224
1300
 
1225
1301
  In interactive assist mode, you can switch the request model without restarting:
@@ -1716,7 +1716,7 @@ column says so, a `--kebab-name` flag. The maps (`hosts:`, `models:`,
1716
1716
  | `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
1717
  | `web.markdown` | `false` | yes | Render answers as Markdown in `chi web`. |
1718
1718
  | `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). |
1719
+ | `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
1720
  | `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
1721
  | `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). |
1722
1722
  | `history.file` | state dir | | Prompt history path, shared by the TUI and the web composer. |
data/docs/guardrails.md CHANGED
@@ -595,7 +595,8 @@ only by the user: these change a model's system prompt for later sessions (see
595
595
  [Model notes](memory.md#model-notes); a shell command reaches them only under
596
596
  `shell-touches-chi`'s text heuristic); and answering chi's
597
597
  questions around `chi answer`: `chi --attach` or `chi -p` with stdin from a
598
- pipe, a here-string or a file, not `/dev/null` (`chi-answer-piped`), and `curl`/`wget` to a
598
+ pipe, a here-string or a file, not `/dev/null` (`chi-answer-piped`, which also sees
599
+ `chi --remote NAME --attach|--resume|-p` and `CHI_REMOTE=NAME chi …`), and `curl`/`wget` to a
599
600
  session's `/answer` route (`chi-answer-http`), both once or for the session, and only by the
600
601
  user (a parent agent allowing one would answer as the user).
601
602
  `chi answer` itself isn't asked about. It denies writes into `.git/hooks`.
@@ -690,6 +691,19 @@ else goes to chi's own rules. It needs nah installed; see its README for the set
690
691
  - Every load failure is shown once, at the start of the first turn
691
692
  (`guardrails> …` in the terminal, a red line on the web).
692
693
 
694
+ ## Participants' text in later turns
695
+
696
+ A participant's message (a review contribution, see
697
+ [internals/participants.md](internals/participants.md#attribution)) stays in
698
+ the history, so later **owner** turns read it. chi marks it: the wire header
699
+ above it says it is a participant's and not the owner's instruction, and the
700
+ system prompt says the same while the session has participants. Owner turns
701
+ keep every guardrail. But an owner turn that says "do what Alex said" hands
702
+ Alex's text the owner's tools: the model will act on it as the owner asked.
703
+ That is the residual risk, documented rather than engineered away: on a
704
+ session with sensitive tools, quote the proposal you want applied in your own
705
+ turn instead.
706
+
693
707
  ## Limits
694
708
 
695
709
  Text matching on shell commands stops accidents, not a model set on getting
@@ -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
 
@@ -12,8 +12,12 @@ resumed session's line already names):
12
12
 
13
13
  Both loops do this, before each request (`ContextStatus`, one per turn). The
14
14
  chat loop's estimate counts the conversation's text and tool calls, not the
15
- tool schemas it sends, until the host's first count (its `usage`). The event
16
- carries a `status` string using this prefix:
15
+ tool schemas it sends, until the host's first count (its `usage`). So that a
16
+ turn's first request doesn't dip below the last turn's count (the schemas are
17
+ thousands of tokens), the loop keeps the last counted request's chars per token
18
+ (`ContextStatus#counted_chars_per_token`, on `KernelLoop` / `ChatLoop`) and the
19
+ next turn's estimate before its first count uses it instead of
20
+ `context.chars_per_token`. The event carries a `status` string using this prefix:
17
21
 
18
22
  `CONTEXT_STATUS ...`
19
23
 
@@ -518,7 +518,28 @@ Turn events (the main ones; `Events` in `events.rb` keeps the shared sets):
518
518
  | `:answer_display` | after the after_turn hooks (observers only, not the sink) | `{display, refs?}` (display a string, or nil; refs the count of the answer's refs, only when a hook attached some) |
519
519
 
520
520
  The boundary events (`:turn_started`, `:turn_completed`, `:turn_canceled`, `:turn_failed`) also carry `origin`
521
- when the turn was queued with one.
521
+ when the turn was queued with one: `{client_id:, enqueued_id:}`, plus `author:` when someone other than the session's
522
+ owner wrote it (its input file's `author`, stamped by chi web from the request's `Web::Principal`). An owner's turn
523
+ has no `author` anywhere: not in its input file, its origin or the session. `GuardrailWiring#begin_turn` gets the
524
+ origin, and `#turn_author` reads its author for the gate's checks.
525
+
526
+ Where a participant's author (`"p:<id>"`; `Steer.participant_author?`: any author but none or `"owner"`) goes:
527
+
528
+ - **The opening message**: `{role: "user", content:, turn_id:, author:, author_name:}` (`Engine#author_fields`;
529
+ the name from `participants.json` at the turn's start, `SessionParticipants`, left out when the sidecar doesn't
530
+ name the participant). Merged input and steers carry the same two keys (`Steer::Line#author`, grouped by sender
531
+ and author so a participant's lines never share the owner's message). Assistant and tool messages carry none:
532
+ `Steer.author_at(messages, index)` resolves the turn's author by position. `ChatLoop#plain` keeps the keys
533
+ (`Steer::AUTHOR_KEYS`).
534
+ - **The wire**: such a message goes out under the review-contribution header (`Steer.header` /
535
+ `Steer.review_header`, both loops through `Steer.wire_text`; see prompt-caching.md). Saved raw.
536
+ - **`Session#last_turn.author`** (`LastTurn#origin` stays the category) and the turn's analytics record's `author`
537
+ (`SessionMetrics`).
538
+ - **Commands and queued turns**: `QueuedCommand#author` (a `/word` input file, `WorkerInbound#take_input`, or a
539
+ `/word` POST /turn, `Bridge#queue_command_request`), in `command_queued`; `turn_enqueued`; the accumulator's
540
+ queued rows.
541
+ - **The system prompt**: `Engine#refresh_participants!` at a turn's start (after `apply_toolkit!`) rebuilds the
542
+ prompt when the session gains its first active participant or loses its last (the participants line).
522
543
 
523
544
  ### Hook points (not events)
524
545