samagotchi 0.140.0 → 0.142.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 (109) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +64 -1
  3. data/README.md +1 -1
  4. data/bin/chi +2 -0
  5. data/docs/architecture.md +14 -285
  6. data/docs/bundle-authoring.md +1 -0
  7. data/docs/cli.md +15 -7
  8. data/docs/configuration.md +57 -18
  9. data/docs/context.md +68 -6
  10. data/docs/guardrails.md +20 -11
  11. data/docs/hooks.md +9 -7
  12. data/docs/internals/attached-context.md +226 -0
  13. data/docs/internals/background-tasks.md +1 -1
  14. data/docs/internals/bridge.md +380 -0
  15. data/docs/internals/bundles.md +1 -0
  16. data/docs/internals/context-telemetry.md +1 -1
  17. data/docs/internals/engine.md +24 -18
  18. data/docs/internals/participants.md +13 -12
  19. data/docs/internals/remote-sessions.md +5 -2
  20. data/docs/internals/{tool-guardrails.md → tool-output-limits.md} +21 -2
  21. data/docs/memory.md +31 -1
  22. data/docs/plugins.md +162 -2
  23. data/docs/sessions.md +53 -28
  24. data/docs/system-one.md +122 -0
  25. data/docs/testing.md +3 -1
  26. data/lib/samagotchi/bridge.rb +2 -0
  27. data/lib/samagotchi/bundles/check-in/manifest.yml +6 -3
  28. data/lib/samagotchi/bundles/check-in/plugin.rb +101 -4
  29. data/lib/samagotchi/bundles/claude-peer/manifest.yml +2 -2
  30. data/lib/samagotchi/bundles/claude-peer/scripts/chi-peer-SKILL.md +14 -0
  31. data/lib/samagotchi/bundles/codex-peer/manifest.yml +2 -2
  32. data/lib/samagotchi/bundles/codex-peer/scripts/chi-peer-SKILL.md +15 -1
  33. data/lib/samagotchi/bundles/core/manifest.yml +2 -2
  34. data/lib/samagotchi/bundles/system/config_modification_protocol.md +2 -2
  35. data/lib/samagotchi/bundles/system/manifest.yml +4 -4
  36. data/lib/samagotchi/bundles/system/memory_guide.md +1 -1
  37. data/lib/samagotchi/bundles/system/self_map.md +1 -1
  38. data/lib/samagotchi/config.rb +74 -14
  39. data/lib/samagotchi/context_candidate_choice.rb +8 -0
  40. data/lib/samagotchi/context_candidates_log.rb +25 -1
  41. data/lib/samagotchi/context_command.rb +32 -9
  42. data/lib/samagotchi/context_judge.rb +6 -2
  43. data/lib/samagotchi/context_move.rb +31 -0
  44. data/lib/samagotchi/context_sources.rb +248 -10
  45. data/lib/samagotchi/engine.rb +68 -7
  46. data/lib/samagotchi/extension_load.rb +3 -2
  47. data/lib/samagotchi/guardrail_wiring.rb +36 -31
  48. data/lib/samagotchi/guardrails/chi_dirs.rb +7 -4
  49. data/lib/samagotchi/guardrails/path_resolve.rb +1 -1
  50. data/lib/samagotchi/guardrails/protected_paths.rb +7 -4
  51. data/lib/samagotchi/guardrails_command.rb +3 -3
  52. data/lib/samagotchi/judgments_command.rb +155 -0
  53. data/lib/samagotchi/kernel_loop.rb +13 -1
  54. data/lib/samagotchi/llm/chat_loop.rb +74 -10
  55. data/lib/samagotchi/llm/errors.rb +7 -5
  56. data/lib/samagotchi/llm/stray_tool_call.rb +61 -0
  57. data/lib/samagotchi/log_line.rb +1 -1
  58. data/lib/samagotchi/memory_bundle/entry_size.rb +100 -0
  59. data/lib/samagotchi/plugin/api.rb +33 -2
  60. data/lib/samagotchi/plugin/attached_context.rb +14 -2
  61. data/lib/samagotchi/plugin/context.rb +54 -1
  62. data/lib/samagotchi/plugin/judgments.rb +72 -0
  63. data/lib/samagotchi/plugin/loader.rb +3 -2
  64. data/lib/samagotchi/plugin/peer.rb +53 -0
  65. data/lib/samagotchi/plugin/sessions.rb +9 -2
  66. data/lib/samagotchi/run_options.rb +1 -0
  67. data/lib/samagotchi/self_report.rb +36 -0
  68. data/lib/samagotchi/send_command.rb +3 -1
  69. data/lib/samagotchi/session.rb +4 -0
  70. data/lib/samagotchi/session_access/contract.rb +3 -1
  71. data/lib/samagotchi/session_access/local.rb +24 -3
  72. data/lib/samagotchi/session_access/remote.rb +19 -8
  73. data/lib/samagotchi/session_access.rb +26 -6
  74. data/lib/samagotchi/session_commands.rb +3 -0
  75. data/lib/samagotchi/session_continue.rb +9 -2
  76. data/lib/samagotchi/session_manager.rb +13 -2
  77. data/lib/samagotchi/session_retention.rb +10 -0
  78. data/lib/samagotchi/side_judgments.rb +12 -1
  79. data/lib/samagotchi/system_one/answer.rb +63 -11
  80. data/lib/samagotchi/system_one/approval_label.rb +56 -0
  81. data/lib/samagotchi/system_one/approval_question.rb +2 -0
  82. data/lib/samagotchi/system_one/client.rb +43 -17
  83. data/lib/samagotchi/system_one/fake_backend.rb +1 -1
  84. data/lib/samagotchi/system_one/http_backend.rb +12 -4
  85. data/lib/samagotchi/system_one/judgment_settings.rb +61 -0
  86. data/lib/samagotchi/system_one/judgments.rb +260 -0
  87. data/lib/samagotchi/system_one/log.rb +4 -2
  88. data/lib/samagotchi/system_one/recording.rb +117 -0
  89. data/lib/samagotchi/system_one/runs.rb +179 -0
  90. data/lib/samagotchi/system_one/runs_store.rb +230 -0
  91. data/lib/samagotchi/system_one.rb +9 -0
  92. data/lib/samagotchi/testing/boundary.rb +5 -1
  93. data/lib/samagotchi/testing/bundle.rb +9 -2
  94. data/lib/samagotchi/testing/host.rb +2 -1
  95. data/lib/samagotchi/testing/judgments.rb +185 -0
  96. data/lib/samagotchi/testing/recorder.rb +5 -0
  97. data/lib/samagotchi/testing/recording_api.rb +1 -1
  98. data/lib/samagotchi/tool_output_cut.rb +26 -6
  99. data/lib/samagotchi/tool_response.rb +1 -1
  100. data/lib/samagotchi/tool_runner.rb +63 -4
  101. data/lib/samagotchi/toolkit_suggest.rb +11 -2
  102. data/lib/samagotchi/tools/memory.rb +28 -0
  103. data/lib/samagotchi/turn_note.rb +9 -0
  104. data/lib/samagotchi/version.rb +1 -1
  105. data/lib/samagotchi/web/app.rb +14 -8
  106. data/lib/samagotchi/web/public/chains.js +10 -2
  107. data/lib/samagotchi/web/session_access_routes.rb +7 -2
  108. data/lib/samagotchi/web/session_create.rb +1 -0
  109. metadata +17 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 74f14fd45c9fe7ee5c7ff6a7169a49b983309d7ab37a4af0530de063eb64a0ef
4
- data.tar.gz: b7674ded1f9a41c094e0daf2220c2836d66e7df8f880d1cdecee4da566ff5b7b
3
+ metadata.gz: a85be5a5307b395bec2801e6ef026217134a6012989388a24dde0068bbc411cf
4
+ data.tar.gz: a9619ea7ae5b97ce7fa11929468c04f617ec948566a8aeba19d70ffcc0e48bd6
5
5
  SHA512:
6
- metadata.gz: 877da3c7a446f711d3fe42e3f3388109f9c4f2cedde21f41292a48ea829904101982e68b697a5f6bf0729119df9c528361af4965e8e2c805357ddb49e19f6fe0
7
- data.tar.gz: 7e77427d1782222f5876527e2095fc53e7e8d35c1c7b8d3a484c9bbb4640f482656d70fb9b948cc541a6b5c843c9764fd179131972786a3c7ed5f55971b67a64
6
+ metadata.gz: 29f21b2213ec66f61cb410ad7f68bedcf76e05f46249f28f56952f83797eb11bd50069484096c2d8f3d5745293446b2c8fc0977f41a709828cfe59beb349cff7
7
+ data.tar.gz: 63fb61ada67c933d764ec1c5f6a3e692bfd24faee0af91d666ccb32f6b60f17a3ee06494a3713531659229cfeb1d378d2ac38db3709801fefd7dd6e5a5e6e5a5
data/CHANGELOG.md CHANGED
@@ -7,6 +7,67 @@ and commands may change between minor versions. How releases are made:
7
7
  [docs/releasing.md](docs/releasing.md).
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.142.0] - 2026-11-01
11
+
12
+ ### Added
13
+
14
+ - System 1's asks are saved, so a later comparison or fine-tune has what was sent, answered and done: approvals, toolkit
15
+ suggestions and context candidates, with the user's answers as labels, under `<state dir>/system_one/runs/`, 0600 and
16
+ local only. They hold prompts, commands and diffs, as sent. They last as long as sessions (`session.retention_days`, 100 MB
17
+ in all), `chi sessions delete` purges a session's, and `system_one.runs: off` stops it. `chi judgments ls` and
18
+ `chi judgments report NAME [--days N]` show what's there. See docs/system-one.md.
19
+ - Bundles can ask System 1 cheap questions of their own: `chi.judgment` declares one, `ctx.judge` and `ctx.judge_later` ask it,
20
+ `ctx.judgments.label` says what came of it, and each ask saves a run. Each is off until you set its mode under
21
+ `system_one.judgments`, which may also give it its own host and model (core's judgments take neither). Bundles' judgments stay out of
22
+ the session's spend, so no cost cap covers them on a hosted host. The test kit has `bundle.judgments`. See docs/plugins.md (Judgments).
23
+ - check-in asks System 1 `check-in.progress` at every check-in ("is this turn still making progress toward what the user asked?"),
24
+ off until you set its mode. The turn's prompt, the calls since the last check-in, the count and the elapsed time go, and what you
25
+ then did with the card is saved as a label on that run. Nothing is acted on: the card, the nudge and the notice are as they were.
26
+ It is the first use to send tool output to System 1 (docs/system-one.md). `chi update` gets it (check-in 0.3.0).
27
+
28
+ ### Changed
29
+
30
+ - `memory_read` reads a memory whole: `max_tool_output_chars` no longer cuts it, so a skill or memory over 10,000 characters
31
+ arrives complete (before, the end was lost and "read it in parts" had no parts). `memory.read_max_chars` (default `0`, no
32
+ fixed cap) caps it if you want one. A result over half a known context window is still cut, since an overflow would roll
33
+ the turn back. A cut now says where the rest is: the memory's `.md` file, read with `start_line`.
34
+ - A write that takes a memory over `memory.entry_warn_tokens` (default `5000` tokens) ends with a note to tighten it, once
35
+ per crossing (`0` turns it off); `chi self` has a `big memories` row for the ones already over. See docs/memory.md.
36
+ - **Edit your config:** System 1's shared settings moved out of `guardrails.system_one`, with no alias:
37
+ `guardrails.system_one.host` → `system_one.host`, `.model` → `system_one.model`, `.timeout_ms` →
38
+ `system_one.timeout_ms`. `mode`, `rules`, `threshold` and `min_record` stay where they are. Until all of them have moved chi warns
39
+ at start, nothing is judged and nothing auto-allows: approvals ask you, and toolkit suggestions and context candidates
40
+ ask nothing. Move `model` rather than deleting it: with no model set, the host's first model would answer under approvals'
41
+ existing record. Restart workers on an older chi after the edit. See docs/configuration.md.
42
+
43
+ ### Fixed
44
+
45
+ - A reply that was only a tool call written as text, which the host couldn't read, ended the turn as if it were the answer:
46
+ exit 0, work left uncommitted. The chat loop now asks again once with a hidden note; two in a row fail the turn visibly
47
+ and keep its earlier steps. Seen with Flash Next.
48
+ - A System 1 `score` answer was dropped: the host sends `score` as the probability-weighted level index (a fraction,
49
+ not a level number), and chi only accepted a whole number. The score is kept as sent and `answer.level` names the
50
+ level with the most probability. See docs/guardrails.md (System 1).
51
+
52
+ Update with `chi update` (check-in 0.3.0, the core profile 0.4.1).
53
+
54
+ ## [0.141.0] - 2026-10-31
55
+
56
+ ### Added
57
+
58
+ - Movable context: a continue (`chi send --new --continues`, the web's Continue →) and a bundle's fork take the
59
+ session's attached context with them, with a note in the new session naming what moved and what stayed. A command
60
+ source moves, a pushed one stays; `chi context add --movable / --no-movable` (and `ctx.context.attach(movable:)`)
61
+ say it outright, and `chi context ls` marks one that stays. A push into a continued session is refused with the
62
+ next link's id. See docs/context.md (Movable context).
63
+ - A paired Claude Code or Codex session follows a continue: the new session asks it once to pair again with the new
64
+ id (a `moved` message to the inbox the old session had), and a reply pushed to the old id is refused with the new
65
+ one, so the peer pairs again then (claude-peer 0.4.6, codex-peer 0.3.5: install the chi-peer skill again with
66
+ `/claude install-skill` or `/codex install-skill`). Bundles get `ctx.continues` and `ctx.context.left_behind`. See
67
+ docs/context.md (Peers and a continue).
68
+
69
+ Update with `chi update` (claude-peer 0.4.6, codex-peer 0.3.5).
70
+
10
71
  ## [0.140.0] - 2026-10-31
11
72
 
12
73
  ### Added
@@ -5362,7 +5423,9 @@ and long-lived sessions.
5362
5423
  - A macOS desktop helper (`chi desktop install`): a "Send to chi" Service and a
5363
5424
  hotkey panel that send selected text or the clipboard to your sessions.
5364
5425
 
5365
- [Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.140.0...HEAD
5426
+ [Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.142.0...HEAD
5427
+ [0.142.0]: https://github.com/dm1try/samagotchi/compare/v0.141.0...v0.142.0
5428
+ [0.141.0]: https://github.com/dm1try/samagotchi/compare/v0.140.0...v0.141.0
5366
5429
  [0.140.0]: https://github.com/dm1try/samagotchi/compare/v0.139.1...v0.140.0
5367
5430
  [0.139.1]: https://github.com/dm1try/samagotchi/compare/v0.139.0...v0.139.1
5368
5431
  [0.139.0]: https://github.com/dm1try/samagotchi/compare/v0.138.0...v0.139.0
data/README.md CHANGED
@@ -221,7 +221,7 @@ once, for the session, for the repo, or for the whole rule in the repo.
221
221
  - [Writing a bundle](docs/bundle-authoring.md): `chi bundle new`, `test`, `check`, `try`, `install --dev`
222
222
  - [Testing](docs/testing.md): specs, affected specs, lint, worktrees
223
223
  - [Architecture](docs/architecture.md): Engine, TerminalUI, bridge, web
224
- - Internals: [Engine](docs/internals/engine.md), [Gemma 4 contract](docs/internals/gemma4-contract.md), [prompt caching](docs/internals/prompt-caching.md), [context telemetry](docs/internals/context-telemetry.md), [LLM context: forget layer](docs/internals/llm-context-forget.md), [replay benchmark](docs/internals/llm-context-bench.md), [live runs](docs/internals/llm-context-live.md), [tool output limits](docs/internals/tool-guardrails.md), [background tasks](docs/internals/background-tasks.md), [bundles](docs/internals/bundles.md), [Anthropic Messages API](docs/internals/anthropic.md)
224
+ - Internals: [Engine](docs/internals/engine.md), [Gemma 4 contract](docs/internals/gemma4-contract.md), [prompt caching](docs/internals/prompt-caching.md), [context telemetry](docs/internals/context-telemetry.md), [LLM context: forget layer](docs/internals/llm-context-forget.md), [replay benchmark](docs/internals/llm-context-bench.md), [live runs](docs/internals/llm-context-live.md), [tool output limits](docs/internals/tool-output-limits.md), [background tasks](docs/internals/background-tasks.md), [bundles](docs/internals/bundles.md), [Anthropic Messages API](docs/internals/anthropic.md)
225
225
 
226
226
  ## Development
227
227
 
data/bin/chi CHANGED
@@ -97,6 +97,8 @@ SUBCOMMANDS = {
97
97
  "guardrails" => ["samagotchi/guardrails_command", "GuardrailsCommand"],
98
98
  # audit suggestions for which toolkit fits a session's first prompt
99
99
  "toolkit" => ["samagotchi/toolkit_report_command", "ToolkitReportCommand"],
100
+ # System 1's saved asks on this machine (ls, report)
101
+ "judgments" => ["samagotchi/judgments_command", "JudgmentsCommand"],
100
102
  # other machines' chi web, for chi --remote NAME (add, ls, rm)
101
103
  "remote" => ["samagotchi/remote_command", "RemoteCommand"],
102
104
  # the chi home: the Ruby every chi launch boots (set, doctor)
data/docs/architecture.md CHANGED
@@ -209,7 +209,7 @@ agent logic and can be used without any terminal rendering; the UI is a thin lay
209
209
  | Core | `Samagotchi::Engine` | System prompt, memory injection, tool declarations, session lifecycle, the model↔tool loop (`run_turn`). No terminal coupling. |
210
210
  | UI | `Samagotchi::TerminalUI` | Interactive REPL (Reline), rendering (ANSI, spinner, status line), REPL commands. Delegates all core work to an `Engine`. |
211
211
  | Model loops and adapters | `KernelLoop`, `LLM::ChatLoop`, `Samagotchi::Client`, `LLM::OpenAIChat`, `LLM::AnthropicMessages`, `LLM::HTTP` | The model↔tool loops and the HTTP adapters they talk through (see "Model loops and adapters"). |
212
- | Bridge (SSE/HTTP) | `Samagotchi::Bridge`, `SessionManager` | The **single live client transport**: an SSE read stream + HTTP POST turn/cancel/answer surface that attaches to a worker's existing `Engine` via `Engine#subscribe`. Every session worker starts it (bound `127.0.0.1`, localhost-only), and it answers only a request that names its secret (`X-Chi-Bridge`, from the worker's 0600 `bridge.json`). |
212
+ | Bridge (SSE/HTTP) ([internals/bridge.md](internals/bridge.md)) | `Samagotchi::Bridge`, `SessionManager` | The **single live client transport**: an SSE read stream + HTTP POST turn/cancel/answer surface that attaches to a worker's existing `Engine` via `Engine#subscribe`. Every session worker starts it (bound `127.0.0.1`, localhost-only), and it answers only a request that names its secret (`X-Chi-Bridge`, from the worker's 0600 `bridge.json`). |
213
213
  | Web (Rack) | `Samagotchi::Web::App`, `SessionManager` | Single-port `127.0.0.1:4567` control plane via `rack`+`webrick` (serve `index.html` + `/api/*`). With `web.host: lan` (or one of this machine's IPv4 addresses) the server also listens on that address, where every request needs the access token, this machine's on loopback too (`App#token_gate`: the `chi_token` cookie, on loopback a value derived from the token (`Token.loopback_cookie`), or `Authorization: Bearer`; chi web prints a `Local:` link with a one-time code; [CLI](cli.md#chi-web-on-your-phone)). Experimental: behind an auth proxy (`web.public_url`, `Web::ProxyGate`) every request needs the token, this machine's too, and one that came through the proxy also the configured identity header for the owner ([configuration.md](configuration.md#behind-an-auth-proxy)). A request goes through, in order: the Host check (`local_host_header?`: loopback names, the LAN address, the public host), the cross-site check (`cross_site?`: Origin this server's or the public origin, no `Sec-Fetch-Site` cross-site/same-site on a POST or `/api`), the frame check (`framed?`: a frame/embed/object load from another origin, 403 `framed`; every HTML answer carries `X-Frame-Options: DENY` and `frame-ancestors 'none'`), the token gate (`token_gate` → `principal_for`, `proxied_principal` for a proxied request; the `?token=` / `POST /api/token` trade sets the cookie, `__Host-chi_token` through the proxy; with `web.public_url` a direct request takes Bearer only, and with it or LAN mode chi's token-less `/api/info` probe gets `probe_info`), then the route's permission (`Principal#permits?`). It talks to each worker's Bridge (`App#relay`, `bridge_get`; `/stream` proxies it) and reads the session files; no `Engine` of its own. Markdown (`web.markdown`) is rendered here only (`Web::MarkdownRenderer`): the answers it serves, card bodies, and `POST /api/render` for a text the page already shows. `App` keeps the gate, `ROUTES` and the helpers the handlers share; its handlers live in mixins it includes, each a `Web::` module with private methods: `SessionAccessRoutes` (`web/session_access_routes.rb`), `ParticipantAccess` (`web/participant_access.rb`), `ParticipantsAdmin` (`web/participants_admin.rb`), `ParticipantReview` (`web/participant_review.rb`), `MessageDisplay` (`web/message_display.rb`), `SessionShow` (`web/session_show.rb`), `SessionCreate` (`web/session_create.rb`), `SessionControl` (`web/session_control.rb`), `PushRoutes` (`web/push_routes.rb`) and `ProxyGate` (`web/proxy_gate.rb`). Exact paths in `GATE_FREE_PATHS` are served before the token gate (the fragment trade's script, and the Home Screen app's manifest, service worker and icons). `bin/chi web` entrypoint. |
214
214
  | Sessions | `Samagotchi::Session`, `SessionManager` | File-based `~/.local/state/samagotchi/sessions/<uuid>.json` + sidecar `input/`/`output/`; retention (14d/500) + ordering (`updated_at desc`). |
215
215
 
@@ -315,286 +315,15 @@ engine.session_state_snapshot # => { status:, message_count:, last_prompt:, ev
315
315
 
316
316
  `Engine#subscribe` is the seam the SSE bridge (`Samagotchi::Bridge`) rides on. The bridge
317
317
  is the **single live transport** and runs **inside the forked session worker** (the same
318
- process that already owns the `Engine`); every worker starts it, and it exposes:
319
-
320
- - `GET /session/:id/stream` — SSE stream of engine + kernel events, each with an
321
- `id: <event_seq>-<epoch>` cursor (the epoch is drawn per worker's Bridge, since `event_seq` starts
322
- over in each worker; snapshots and `/state` carry it as `event_id`); resume via `Last-Event-ID` /
323
- `?from_seq=` (a plain `event_seq` is still accepted); a `: ping` heartbeat keeps idle proxies alive;
324
- too-old reconnects, and cursors from another worker's epoch, receive a `reset` marker carrying
325
- `session_state_snapshot`. `?snapshot=1` joins with a snapshot frame instead of a replay;
326
- `?client_id=` names whose stream it is (`Bridge#open_streams_except`, used by `POST /exit`).
327
- Each event goes out as clients get it (`Bridge::ClientEvent.project`, in `SSEWriter#write_event`, live
328
- and replayed alike); `generation_chunk`, `tool_call_started` and `turn_completed` are projected to the
329
- contracts below. The ring retains raw events for replay and `?raw=1`.
330
- `?raw=1` writes events as the engine emitted them, for debugging on the worker's port: `chi web`'s
331
- `/api/sessions/:id/stream` never passes it on (`Web::App#stream_query`).
332
- An attached terminal's stream (`EventStream` with `rediscover:`) looks for the session's live
333
- worker whenever it drops: a new one (a restart) is followed from its snapshot, and the terminal
334
- sends its requests there (`worker_changed`); it gives up after ~12 s, or at once when the session
335
- was stopped.
336
-
337
- The wire chunk (the contract; the engine's own event keeps the provider's `payload` and the raw `content`):
338
-
339
- ```
340
- event: generation_chunk
341
- data: {"type":"generation_chunk","iteration":2,"text":"…","thinking":"",
342
- "tool_call":true, // only when set
343
- "counts":{"prompt_tokens":489,"completion_tokens":12,
344
- "total_tokens":501,"context_window_tokens":32768}, // only when the server reported counts
345
- "event_seq":812}
346
- ```
347
-
348
- A `tool_call_started` frame keeps its row fields but omits the model's `call`:
349
-
350
- ```
351
- event: tool_call_started
352
- data: {"type":"tool_call_started","iteration":2,"call_index":1,"tool":"write",
353
- "params":"path=notes.txt","event_seq":812}
354
- ```
355
-
356
- A `turn_completed` frame omits `result`; `turn_summary.output` carries the answer:
357
-
358
- ```
359
- event: turn_completed
360
- data: {"type":"turn_completed","turn_summary":{"output":"Done."},
361
- "display_pending":false,"event_seq":813}
362
- ```
363
-
364
- `counts` is `ContextUsage.normalize` of the provider's payload without `ctx_pct` (normalize computes it
365
- without the session's LLM context budget; a client counts `total_tokens` against the window or budget, as
366
- `ContextStatus#generation_done` does for the TUI) and with its nil keys left out: cumulative for the
367
- request, a missing key means unknown (never 0), `prompt_tokens` includes cached tokens, and
368
- `context_window_tokens` is there only when the payload reported a window itself (llama.cpp's `/completion`
369
- doesn't: the client uses `generation_started`'s). Unknown provider fields never cross; a field is added here
370
- when a client needs it, never as a catch-all bag. Details: [internals/context-telemetry.md](internals/context-telemetry.md).
371
- - `POST /session/:id/turn` — fire-and-forget turn creation; returns `202` with an `enqueued_id`
372
- (delivery is at-least-once via the worker's file-IPC input path — it never calls `run_turn`
373
- across the HTTP boundary). Only the bridge's own session: another id, in the path or the body's
374
- `session_id`, is `404 unknown_session`. Inspect results through the read surface, not the turn response.
375
- An optional `deadline` (epoch seconds; `BridgeClient` sends 5/6 of its read timeout ahead) makes
376
- a request read after it (a worker frozen by sleep or SIGSTOP) answer `408 deadline_passed` and
377
- not run: a client that timed out has said the message was not sent. `/answer`,
378
- `/question/dismiss`, `/command`, `/turn/dequeue` and `/exit` take the same `deadline` (a command is
379
- checked with the event log held, as a turn is), and the Bridge logs `turn_expired`, `answer_expired`,
380
- `dismiss_expired`, `command_expired`, `dequeue_expired` or `exit_expired`. The web app answers either
381
- kind of timeout with `504 worker_timeout` ("… so the command was not run"). `/cancel` and `/recap` take none.
382
- The web relays answer, dismiss, command and cancel the same way (`App#relay`): a 408 or a read
383
- timeout is `504 worker_timeout`; a Bridge 404 for a route an older worker lacks is
384
- `501 not_supported` with the restart hint; a Bridge `404 unknown_session`, no bridge or a
385
- refused connection is `503 not_live`.
386
- - `POST /session/:id/cancel` — cancel the running turn; `202`, or `409` when none runs.
387
- - `POST /session/:id/recap` — `/recap` in an attached TUI: `200 {enabled, saved, request,
388
- min_user_turns}` (`{enabled: false, session_off}` without recaps; `session_off` when the session's own
389
- `/recap off` turned it off); a new recap is asked for at once and
390
- arrives as `:recap_ready`. Answers mid-turn too.
391
- - `POST /session/:id/answer` — answer the pending question; `200`, `409` when another client
392
- answered first or it is gone, `400` for an invalid selection. With `client_id: "cli:answer"`
393
- (`chi answer`, a parent agent) the answer is checked by `Guardrails::ParentPolicy` in
394
- `QuestionDesk#answer`: the question carries each option's effect (`effects`, set by its asker:
395
- `Approval.payload`, `ContinueOffer`, `Relay.card`; derived for an untagged one) and `user_only`,
396
- and an option beyond the worker's `guardrails.parent_answers` (tightened by the session's own
397
- `parent_answers`: `ParentPolicy.for_session`) is `403 parent_answer_refused` (with its `reason`),
398
- the question still open. `chi answer` makes the same check first, from the session file.
399
- - `POST /session/:id/question/dismiss` — leave the question unanswered (an approval: denied);
400
- `200`, or `409` when it is no longer pending.
401
- - `POST /session/:id/tasks/stop` — `{task_id}`: stop a background task as the user
402
- (`TaskRuntime.stop_task by: "user"`, the web's stop-task button on a running `task_wait`;
403
- web route `POST /api/sessions/:id/tasks/:task_id/stop`). Only a task this conversation's
404
- `task_create` started, or an `execute` handed over as a task at its limit (`TaskRuntime.created_ids_in` on the
405
- saved messages, or the running turn's accumulator); answers once the
406
- task is gone: `200 {status, stop_reason, task_id}`, `404 task_not_found`, `409 not_running`.
407
- The worker names it in its sidecar features (`task_stop`).
408
- - `POST /session/:id/turn/dequeue` — `{enqueued_id, client_id?, deadline?}`: take back a queued message
409
- (one that waits for the running turn's end: `--queue`, Alt+Enter, `/queue TEXT`, or a message
410
- with images) before a turn takes it (web route `POST /api/sessions/:id/turn/:enqueued_id/dequeue`,
411
- the × on a queued bubble; `/unqueue` in the attached TUI). Under the event log, like its enqueue:
412
- `SessionInbox.take_queued_input` writes a tombstone (`input/removed/<enqueued_id>`, for `ReplyWait`'s
413
- `turn:`), claims the file as the drain does (one rename wins; a lost claim takes the tombstone back),
414
- deletes it, and the Bridge announces `:turn_dequeued`. `200 {status: "removed", enqueued_id, prompt}`, `409 already_started`
415
- (a turn took it), `409 not_queued` (a next-step message: a drain may merge it any moment), `404
416
- unknown_enqueued_id`. It never wakes the worker. The worker names it in its sidecar features (`unqueue`).
417
- A worker's start prunes the `input/removed/` and `input/settled/` tombstones older than a week
418
- (`SessionInbox::TOMBSTONE_MAX_AGE`, log `tombstones_pruned`); otherwise they go with the session dir.
419
- - `POST /session/:id/relay` — the approval relay, on a delegated child's worker
420
- (`{action, relay_id, question_id}`): `opened` / `closed` (with a `reason`) set or clear the
421
- pending question's `relayed_to` mark (`QuestionDesk#annotate`, `:question_relay`); `answered`
422
- makes the child ask its parent's Bridge for the answer (`RelayVerifier`) and take it only for
423
- this child and the question pending now. `200`, `409` no longer pending, `403` a parent agent's
424
- pick beyond the child's own `guardrails.parent_answers` and session `parent_answers` (`parent_answer_refused`; the
425
- relay card in the parent was held to the parent's), `422 relay_unverified` (the question
426
- stays open).
427
- - `POST /session/:id/relay/status` — on a parent's worker, `{relay_id}`: what its `RelayDesk`
428
- (memory only) holds for that relay (`child_id`, `child_question_id`, `state`, `answer`, `by`);
429
- `404` for an unknown id. A POST with a body because routes match exact paths.
430
- - `POST /session/:id/command` — a session command (`/model`, `/models`, `!rollback`, `!cmd`,
431
- `/continue`) for the worker loop; `202` with a `command_id` its `:command_ran` names, `400` when
432
- the line isn't one. A `:command_ran` carries `thinking`, `reasoning_history` and `llm_context`
433
- (the summaries the next turn runs under) when the command changed them or the model, and
434
- `confirm: {line, text}` when a `/thinking` or `/reasoning-history` change from a `web:` or `tui:`
435
- client asks first (`CacheConfirm`; nothing changed, `line` applies it).
436
- - `POST /session/:id/exit` — ask the worker to exit now (`{client_id:, delete?, restart?, deadline?}`; `delete`:
437
- `/exit --delete`, no recap written; `restart: true`: hand the session to a new worker on the newest chi
438
- installed, `200 {status: "restarting"}`; an empty session's `exiting` carries `discard: true`). The worker checks with
439
- the event log held (`WorkerIdleExit#hold_for_request`): `200 {status: "exiting"}` and it leaves
440
- like an idle exit, or `409 {status: "held", reason:}` with `turn_running`, `input_queued`,
441
- `continue_offered`, `client_connected` (a stream not named by the asker), `reminders`,
442
- `background_tasks` (with `tasks: [{id, command}]`: the running tasks a finish wake watches), `starting`, or
443
- `leaving_exit` / `leaving_delete` / `leaving_restart` when another client's exit of another kind is already taken
444
- (`Worker#claim_exit`, claimed before a stop-all; the words are `WorkerIdleExit::LEAVING_WORDS`). A second exit of
445
- the kind already taken is answered `200` as that one.
446
- - `GET /session/:id/state` — `session_state_snapshot` (JSON).
447
- - `GET /session/:id/stats` — `Engine#stats_snapshot` for attached `/stats`: the metrics, with the context window and prompt profile asked from the server before the first turn.
448
- - `GET /session/:id/snapshot` — the snapshot frame's content as one request (the web server renders
449
- the messages itself, then streams from its `event_seq`). Read by the web's full show
450
- (`GET /api/sessions/:id`, `?parts=1`) and the plugin's `list_sessions`; every message is copied.
451
- - `GET /session/:id/tail` — the page's light re-read (`Bridge#tail_frame`): `session_state_snapshot`,
452
- `working_directory` (the session's folder, which the web checks the answer's file refs against),
453
- `answer` (the last message a UI shows as an answer, `AnswerTail`: the one message, copied alone),
454
- `cards` (`CardStore#list`; a card an earlier worker showed during a turn comes back read-only, with no
455
- actions: its turn no longer runs), `event_seq`, `event_id`, all taken in one event-log hold; no message
456
- list, so it costs the same however long the session is. `?turn_id=` answers that turn's answer
457
- (an unknown or missing id: the newest); the only route that reads its query.
458
- - No browser: only chi's own clients talk to a Bridge. A request with an `Origin` header, a `Sec-Fetch-Site`
459
- other than `none`, or a `Host` that isn't loopback (a DNS-rebound page) is `403 cross_origin`
460
- (`Bridge#browser_request?`, before any route); there is no CORS and no `OPTIONS` route. The browser reaches a
461
- worker only through `chi web`. A request carrying any trace a proxy adds (`ProxyTrace`: `X-Forwarded-*`,
462
- `Forwarded`, `X-Real-IP`, `X-Auth-Request-*`, `Cf-*`…, underscore twins too) is `403 proxied`
463
- (`Bridge#proxied_request?`): an auth proxy that forwards every local port and adds such a header doesn't reach a
464
- worker.
465
- - **The secret.** Every request, on every route (the stream, the reads such as `state` and `stats`, the writes, and
466
- an unknown path), names the Bridge's secret in `X-Chi-Bridge`; one without it, or with another one, is
467
- `403 bridge_secret` with one session-specific message (`BridgeSecret.refused`): "session ID's worker refused
468
- this chi's request (it didn't name its secret): a chi web or terminal started before chi was updated can't
469
- reach it; restart chi web, re-attach with `chi --attach ID`, or restart the worker with `chi sessions restart
470
- ID`." `chi sessions restart` can also recover when the worker cannot be asked: TERM + resume if disk shows no
471
- running turn, pending question or approval, queued input, or running background task. The check is after the
472
- browser and proxy checks and before any route (`Bridge#secret_ok?`,
473
- `OpenSSL.secure_compare`). No route is left open: what the reads show is the session itself. Only a request that
474
- got past it counts as client activity, so a refused one (an older chi web's polls) never holds the idle exit. Each Bridge makes its own (`SecureRandom.hex(32)`, 64 hex
475
- characters) when it starts and writes it to `bridge.json`, which is 0600 from the moment it exists
476
- (`AtomicFile` creates the temporary file with that mode, then renames it), so only this machine's user can read
477
- it: a forward that adds no proxy header, or another local account, reaches the port but not a session. Clients get
478
- it only from the sidecar: `WorkerLink.client`/`.await` and `BridgeClient.for_sidecar` take the port and the secret from the
479
- same read (chi web makes its client from the sidecar for every request, the attached terminal again on a
480
- `worker_changed`), so a respawned worker's new one is used and nothing passes a port by hand. A worker from before
481
- the secret writes none and asks for none; a chi web or attached terminal from before it sends none, so a worker
482
- the newer chi started refuses it until it is restarted. A client tells that refusal from a route's own 403
483
- (`BridgeClient::Response#secret_refused?`) and shows the one message: chi web's relays answer 502 `bridge_secret`,
484
- `chi answer` and a parent's approval relay say it instead of "refused", and an event stream ends with reason
485
- `bridge_secret` once rediscovery finds no newer worker (the attached terminal's closing line), never retrying in
486
- silence; chi web's stream proxy answers 502 `bridge_secret`, never passing the refusal on as frames. The secret is never logged (a refusal logs
487
- `secret_refused` with the path, at most once a minute), never in an event, a reply, `/api/info` or a session
488
- file, and `WorkerSidecar`, `BridgeClient` and `Bridge` leave it out of `#inspect`.
489
-
490
- The metrics in `/state`, `/snapshot`, `/stats` and the SSE `snapshot`/`reset` frames
491
- (`SessionMetrics#snapshot`) carry the session's totals and only the recent timing records: the
492
- newest turn the worker finished and any it hasn't saved yet (at most 20 turns), with their tool
493
- calls and the running turn's finished ones. The full history is the session's `analytics.json`,
494
- which `SessionMetrics#persist` rewrites after each turn; the web server merges the two by id.
495
- The totals' `tokens` block adds the cached, cache-write and reasoning sums, the reported cost (`cost_sum`) and the
496
- estimated one (`cost_estimate_sum`, from `hosts.<name>.models` prices; 0 for none), the decode time and tokens
497
- behind the average speed (`avg_decode_tps`) and the last speeds (`last_decode_tps`,
498
- `last_prefill_tps`, `tps_source`: `server` or `estimate`). `model_segments` (always present) splits those sums
499
- by model after a `/model` switch: one row per run of consecutive turns on one model (`SessionMetrics::ModelSegment`,
500
- keyed on the turn records' `model_ref`), in order, with its turns, token and cost sums, speeds and the switch's prefill
501
- (`prefill_tokens`, `prefill_cost_estimate`: the sums of its turn records' `switch_prefill_tokens` and
502
- `switch_prefill_cost_estimate`, [sessions.md](sessions.md)); `[]` with one model, the oldest rows folded into one (`folded_segments`)
503
- past 6; a running turn counts in its model's row (`ended_at` null). The rows add up to `tokens`. They are derived from
504
- the records, never read back. Each `generation_completed` event
505
- carries the generation's `speed` (`{decode_tps, source}`, null without one), its own prompt-cache
506
- counts (`prompt_tokens`, `cached_tokens`, `cache_write_tokens`: null when the server reported no writes) and
507
- those running `tokens` and `model_segments`: the Engine closes the generation in `SessionMetrics#finish_generation` before relaying
508
- it, so the web's info bar updates per generation without parsing chunk payloads.
509
- Side requests (the idle recap, a plugin's `ctx.ask_model` such as `/btw`) are counted apart, in a `side` block
510
- (only when there were any; `SessionMetrics::SideTotals`: requests, token sums, `cost_sum` null until reported,
511
- `cost_estimate_sum` from the target host's price, `by_kind`): `IdleClient` hands each reply's usage to its
512
- `on_usage:` callback, `Engine#note_side_usage`, which folds it (`SessionMetrics#add_side`) and announces
513
- `:side_usage` (`kind`, the whole `side` block) under the event lock, so the blocks arrive in fold order, then saves
514
- `analytics.json` between turns (a running turn's end saves it). `tokens`, `model_segments` and the turn records
515
- stay the turns' only. `SessionMetrics#persist` is serialized (its own mutex, taken before the metrics one), so two
516
- threads' saves land in snapshot order.
517
- A bundle's `ctx.cost` reads the same sums (`SessionMetrics#cost`, a frozen `SessionMetrics::Cost`): the `tokens`
518
- block's `cost_sum` and `cost_estimate_sum` with the running turn's finished generations folded in, plus the `side`
519
- block's, so it counts what earlier workers spent too ([plugins.md](plugins.md)). Its `ctx.usage` reads the work
520
- (`SessionMetrics#usage`, a frozen `SessionMetrics::Usage`): the snapshot's `turns`, `tool_calls_total` /
521
- `tool_calls_by_tool` and `iterations_total`, and `active_ms`, the generations' `gen_ms` plus the finished tool
522
- records' `duration_ms` (tallied per tool as they are counted; `ask_user_question`'s, a wait on the user, left out).
523
-
524
- The page's frequent reads of `GET /api/sessions/:id` stay light too:
525
- - `?tail=1&recent=1` (each turn's end, a cancel, an answer display): `{tail, session: {id, status,
526
- used_memory_names}, messages: [the answer, with its display and display_refs (re-read on an `answer_display` with a display or refs)], markdown_warning, timing}`, from the worker's `/tail`
527
- alone (no session file parsed, no `analytics.json`); `timing` carries the worker's recent records
528
- and `turn_count` (finished turns). The page merges the records by id (`timing.js mergeTiming`) and
529
- finishes the ended turn's own line by its `turn_id` (sent as `&turn_id=`, which also picks that
530
- turn's answer). A tail read's timing is always the newest turn's; `recent=1` is accepted and not needed.
531
- The page binds an answer's `display_refs` to its bubble (`answer_refs.js`: on a reload's render and on this
532
- read's re-render) and pairs them with the bubble's links and text before the `path:line` scan. A question's
533
- refs (`pending_question.refs`, `:question_asked`) pair the same way (`markdown_place.js` `linkRefsIn`/`pairRefsIn`):
534
- `question_requested` (or a snapshot's pending question) keeps them on the asking step's gen
535
- (`turn_model.js` `questionRefs`) before the card draws, so the stage's asking slot (`stage_slots.js`
536
- `drawAsking(text, refs)`) pairs them on its raw draw and again after its markdown swap; the card pairs its
537
- question text and its kept reply (`question_cards.js`); the step's closed `.gen-text` takes them
538
- (`turn_block.js`; on a reload its `data-answer-at`, the message's `display_refs`) and `linkTextRefs` pairs it at
539
- the turn's end and after a reload. A closed step's text is markdown too with `web.markdown` on: a reload's
540
- from its message's `html` (`turn_block.js` `textTemplate`), a live turn's from `POST /api/render` on its step's
541
- first open after the turn's end (`markdown_place.js` `renderStepTexts`, which pairs its refs again after the
542
- swap); the text as written stays on it as `data-copy-source` (`stepText`, a reloaded card's kept reply). A file ref outside the
543
- session's folder is dropped, as a reload drops it: the folder `/tail` names, else (an older worker) the hub's
544
- session list entry's, else (refs to check, no hub entry) the session file's; with no folder known at all, every
545
- file ref is dropped rather than shown unchecked.
546
- A plugin tool's row refs (`refs`, `RowRefs`) ride its tool events and, on a reload, its part (saved as
547
- `tool_refs`); `turn_block.js` binds them to the row's title (`refs.js bindRowRefs`: the first on the title,
548
- the others in its menu).
549
- - `?timing=1`: the whole timing (`analytics.json` merged with the worker's `/state`) and
550
- `turn_count`, when the page's merge came up short.
551
- - `?cards=1`: the cards, from `/tail`; the session file is only stat'ed for the 404.
552
-
553
- The stage view's working set (`web.working_set`) has its own read, `GET /api/sessions/:id/working_set`: the
554
- `WorkingSet` projection (`{items, at, running}`: each file, memory and URL live, stale or forgotten, with its tokens)
555
- of the worker's `/snapshot` messages, else the session file, under the session's LLM context strategy; while a turn
556
- runs, the snapshot's `working_set` (the running turn's last event, which the messages lag) when it has one. Its live form
557
- is the `:working_set_updated` event, which always wins over a GET's answer it overtook. The page overlays it on the
558
- set it builds from its tool rows ([internals/working-set.md](internals/working-set.md)).
559
-
560
- A worker without `/tail` (an older chi; `404 not_found`) is read through `/snapshot` in the same
561
- shape; any other failure (no worker, a 500, a timeout) reads the disk, trimmed to the newest
562
- turn's records.
563
-
564
- Every route goes through `Bridge#dispatch`: an id other than the bridge's own session is
565
- `404 unknown_session`, and a handler that raises answers `500 bridge_error` (logged as
566
- `handler_failed`) rather than dropping the connection.
567
-
568
- The per-session port is OS-assigned (bound to `0`) and published to a `bridge.json` sidecar
569
- for client discovery, with the Bridge's secret (see above).
570
-
571
- **Worker link.** Every client asks `WorkerLink` (`worker_link.rb`) whether a session's worker can be reached, never
572
- reading `bridge.json` or probing a port itself: `WorkerLink.resolve(session_dir, unlink:, owner:)` answers a typed
573
- outcome. The rule is the owner lock plus the sidecar's `pid`; there is no port probe (a port can be reused by another
574
- session's Bridge, a free lock is the kernel's proof the worker died). In order: no lock holder, or a chi REPL's: a
575
- sidecar is `Stale(:lock_free)` (removed with `unlink: true`, only if it is still the file that was read and the lock,
576
- read again just before, is still free: a worker that took it meanwhile is resolved as that worker), else `None`;
577
- a holder with no kind yet: `Starting`; a leaving worker: `Leaving` (marked before its Bridge stops, on every way out:
578
- `Worker#teardown` marks it too, with `TEARDOWN_LEAVE_WAIT`); a worker whose sidecar names no port or another
579
- pid than the holder's (a worker that has not written its own yet): `Starting`, `stuck` once it has held the lock
580
- past `START_LIMIT` (30 s) with none; a sidecar whose `protocol` is newer than `WorkerLink::PROTOCOL`: `Skew` (a
581
- newer worker; never driven, `wake` answers 409 `worker_newer`); else `Live` (its sidecar; `#client` makes the one
582
- `BridgeClient`). A sidecar with no `pid` is an older worker's: the lock alone says it is live. `owner:` is the
583
- holder as the caller already read it (nil: free), so one read serves a summary and its status. `WorkerLink.client` is
584
- the Live's client or nil; `WorkerLink.await` polls for a new worker's Live (an `until:` check tells a restart's
585
- successor from the old worker); chi web's `App#worker_link` / `#await_worker_link` wrap them. The Bridge writes
586
- `pid` and `protocol` with its port and removes its sidecar on stop only when the file still carries its epoch.
587
-
588
- `chi web`'s `GET /api/sessions/:id/stream` proxies this bridge
589
- (503 `not_live` for no worker, a leaving or stuck worker, or an unreachable Bridge; 502 `worker_newer` for skew and
590
- `bridge_error` / `bridge_secret` for a refused Bridge; 200 only after the Bridge answers; full history of any session
591
- is served by `GET /api/sessions/:id/output`). It opens the Bridge before answering and gives the one opened socket to
592
- `App#proxy_stream_body` / `ProxyStreamBody`, which closes it when the body or hijacked client ends. The owner's stream
593
- is a byte pipe; the body gives it the request's frame filter (`App#stream_filter`: nil for the owner; for a participant, and the owner's participant view
594
- `?as=participant`, a `Web::ParticipantStreamFilter`, which cuts the Bridge's chunks into frames and writes each again
595
- from `Web::ParticipantView.event`'s allowlist, dropping every other event; only `from_seq` and `snapshot` reach the
596
- Bridge; [internals/participants.md](internals/participants.md#what-a-participant-sees)). Resume/ring-buffer state is **in-memory**, per worker: a cursor from
597
- an earlier worker gets the `reset` snapshot, not a replay.
318
+ process that already owns the `Engine`); every worker starts it. It keeps what a client that joins late needs
319
+ (`RingBuffer` of recent events, `TurnAccumulator` for the turn in progress, `CardStore` for cards and notices), writes
320
+ each client's stream with an `SSEWriter` (`id: <event_seq>-<epoch>` cursors, a `snapshot` or `reset` frame when a
321
+ cursor cannot be replayed), and answers HTTP routes: `GET /session/:id/stream`, `state`, `snapshot`, `tail`, `stats`
322
+ and the writes `POST …/turn`, `cancel`, `answer`, `command`, `exit` and more. Only chi's own clients reach it (never a
323
+ browser; a proxy trace is refused), and every request names the Bridge's secret from the 0600 `bridge.json`. `chi web`
324
+ proxies the stream and relays the writes. The routes, wire shapes, replay rules, refusals and the worker link
325
+ (`WorkerLink`: whether a session's worker can be reached) are in [internals/bridge.md](internals/bridge.md).
326
+
598
327
 
599
328
  **Session hub.** The cross-session layer (which sessions exist, who owns them, what changed)
600
329
  never pulls from the page: `chi web` runs one `Samagotchi::Web::SessionHub` (a thread inside
@@ -719,8 +448,8 @@ model: [internals/remote-sessions.md](internals/remote-sessions.md).
719
448
  | `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`) |
720
449
  | `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` |
721
450
  | `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` |
722
- | `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` |
723
- | `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` |
451
+ | `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`); `movable` (any body) | 201 `{status: attached, name, movable}` | 400 `invalid_source`; 409 `exists` |
452
+ | `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`; 410 `context_continued` (`next_id`); 400 `invalid_text` |
724
453
  | `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 |
725
454
  | `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` |
726
455
  | `DELETE /api/sessions/:id?force=` | owner | `force=true` stops a live worker first (the page sends it) | as before | 409 `worker_running` without it |
@@ -877,7 +606,7 @@ A map of the parts the overview above doesn't name (paths under `lib/samagotchi/
877
606
  | Shipped bundles | `bundles/` (`mcp`, `github-pr`, `loop-guard`, `skills`, `source-links`, …) | Bundles that ship with chi. |
878
607
  | Hooks | `hooks/registry.rb` (`Hooks::Registry`), `hooks/loader.rb`, `hook_routes.rb` (`HookRoutes`), `event_routes.rb` (`EventRoutes`) | Hook registration and dispatch; what a hook or plugin does beyond reading its event (notice, question, stop, steer) routed to the UIs (cards and notices through one set of routes) ([hooks.md](hooks.md)). |
879
608
  | Guardrails | `guardrails.rb`, `guardrails/shell_lex.rb`, `guardrails/data_readers.rb`, `guardrails/rules.rb`, `guardrails/protected_paths.rb`, `guardrails/parent_policy.rb` + `guardrails/parent_approvals.rb`, `guardrail_wiring.rb` | The gate before every tool call: shell lexing (and the commands that only read a heredoc as data), YAML rules, the core checks on chi's own files, approvals, what a parent agent may answer (`guardrails.parent_answers`) ([guardrails.md](guardrails.md)). |
880
- | System 1 | `system_one.rb`, `system_one/` (`SystemOne::Client`, `ApprovalQuestion`, `Log`, `Record`, `SelfProtect`, `Similar`, the backends), `guardrail_wiring.rb` (`GuardrailWiring#request_approval`), `guardrails_command.rb` (`chi guardrails replay`) | The approval judge: a model's `p` for each ask, logged to `judgments.jsonl` with the user's answer; in `on` mode an ask on a listed rule with a clean record is allowed once without the user. Labelled by `/guardrails wrong` (`SessionCommands`) and the web's **should have asked** (the Bridge's `POST guardrails/wrong`) ([guardrails.md](guardrails.md#system-1-experimental)). |
609
+ | System 1 | `system_one.rb`, `system_one/` (`SystemOne::Client`, `Recording`, `Runs`, `ApprovalQuestion`, `Log`, `Record`, `SelfProtect`, `Similar`, the backends), `guardrail_wiring.rb` (`GuardrailWiring#request_approval`), `guardrails_command.rb` (`chi guardrails replay`) | The approval judge: a model's `p` for each ask, logged to `judgments.jsonl` with the user's answer; in `on` mode an ask on a listed rule with a clean record is allowed once without the user. Labelled by `/guardrails wrong` (`SessionCommands`) and the web's **should have asked** (the Bridge's `POST guardrails/wrong`) ([guardrails.md](guardrails.md#system-1-experimental)). Every core ask goes through `SystemOne::Recording`, saved as a run under `<state dir>/system_one/runs/` (`Runs`, `RunsStore`, `ApprovalLabel`, `judgments_command.rb`: [system-one.md](system-one.md)). |
881
610
  | Delegation | `tools/delegate.rb`, `tools/delegate_result.rb`, `tools/delegate_wait.rb`, `child_reports.rb`, `tools/delegate_relay.rb`, `relay_desk.rb`, `relay_verifier.rb`, `relay_watcher.rb` | Child sessions a session starts and waits on, and their approval relay ([sessions.md](sessions.md#delegating)). |
882
611
  | send / broadcast | `send_command.rb` (`SendCommand`), `broadcast_command.rb` (`BroadcastCommand`), `broadcast/` | `chi send` and `chi broadcast` ([broadcast.md](broadcast.md), [sub-agent.md](sub-agent.md)). |
883
612
  | Worker link | `worker_link.rb` (`WorkerLink`: `Live`, `Starting`, `Leaving`, `Skew`, `Stale`, `None`), `worker_sidecar.rb` (`WorkerSidecar`) | Whether a session's worker can be reached and the client for it: every Bridge client asks `WorkerLink.resolve` / `.client` / `.await` (*Worker link* above); `WorkerSidecar` is the `bridge.json` it reads. |
@@ -896,7 +625,7 @@ A map of the parts the overview above doesn't name (paths under `lib/samagotchi/
896
625
  | Images / vision | `image_store.rb` (`ImageStore`), `vision_support.rb`, `vision_context.rb` | Stored prompt images and whether a model takes them. |
897
626
  | Toolkits | `toolkit.rb` (`Toolkit`), `toolkit_binding.rb` (`ToolkitBinding`, the Engine's seam), `toolkit_command.rb` (`/toolkit`), `tools/registry.rb` (`Registry#toolkit`, `#left_out_names`), `web/public/toolkit_chip.js` | Which registered tools a session's prompts declare: saved in the session, resolved at a turn's start (`Engine#apply_toolkit!`), named in the prompt's not-loaded line, a kit's prompt text after the appended files; `chi.toolkit` and `ctx.use_toolkit` for plugins ([configuration.md](configuration.md#toolkits)). |
898
627
  | Tool index | `tools/tool_index.rb` (`Tools::ToolIndex`), `tools/deferred_tools.rb` | `find_tools` and `call_tool`: the tools a session knows but doesn't declare one by one (an mcp bundle's servers through `chi.tool_source`, the built-ins its toolkit leaves out), searched and called by name ([plugins.md](plugins.md)). On `api: anthropic` they also go out deferred, a hit loads its matches and the model calls them by name ([internals/anthropic.md](internals/anthropic.md)). |
899
- | Tool output cut | `tool_output_cut.rb` (`ToolOutputCut`), `tool_output_files.rb` (`ToolOutputFiles`) | A tool's output over `max_tool_output_chars`, cut by the tool's policy; an execute's whole output saved in the session's `tool_output/` folder ([internals/tool-guardrails.md](internals/tool-guardrails.md)). |
628
+ | Tool output cut | `tool_output_cut.rb` (`ToolOutputCut`), `tool_output_files.rb` (`ToolOutputFiles`) | A tool's output over `max_tool_output_chars` (a `memory_read`'s: `memory.read_max_chars`, 0 = none, plus a guard at half a known window), cut by the tool's policy; an execute's whole output saved in the session's `tool_output/` folder ([internals/tool-output-limits.md](internals/tool-output-limits.md)). |
900
629
  | Steer delivery | `steer.rb` (`Steer`), `delivery.rb` (`Delivery`), `steer_cut.rb` (`SteerCut`), `generation_phase.rb`, `waiting_steer.rb`, `steer_cut_after.rb` (`SteerCutAfter`), `cut_policy.rb` (`CutPolicy`) | A message for a running turn: at the next boundary, cutting a thinking-only generation, or queued for the turn's end ([internals/engine.md](internals/engine.md)). |
901
630
  | Prompt files | `prompt_files.rb` (`PromptFiles`) | The AGENTS.md chain (root → cwd) and the appended files the system prompt carries. |
902
631
  | Model settings | `effective_model.rb` (`EffectiveModel`, `ModelChoice`), `model_profile.rb` (`ModelProfile`), `model_settings.rb`, `sampling_settings.rb`, `model_catalog.rb` (`ModelCatalog`), `model_notes.rb` (`ModelNotes`) | The effective model's settings as one snapshot per read, a model's prompt format, its `models:` entry, its request parameters, the pickable model rows (`chi models`, `/api/models`), the notes its prompt loads. |
@@ -117,6 +117,7 @@ end
117
117
 
118
118
  `bundle.tool_call` runs guarded hooks; `bundle.tool` calls a registered tool. `bundle.steers`, `cards`, `stops` and
119
119
  `questions` record effects; `bundle.answer` / `model_answers` provide `ask_user` / `ask_model` answers.
120
+ `bundle.judgments` answers `ctx.judge` and records its asks and runs (each judgment is on in the kit, off in chi until the user sets it);
120
121
  `bundle.http.stub` stubs `ctx.http`; `bundle.question_asked(text, question:)` fires `:question_asked` and returns
121
122
  the refs; `sandbox.config`, `sandbox.fake_executable` and `bundle.install` cover setup.
122
123
  The full list is in [plugins.md, Testing a bundle](plugins.md#testing-a-bundle-experimental). The kit is
data/docs/cli.md CHANGED
@@ -16,12 +16,12 @@
16
16
  - `chi web --start-dir DIR` (config `web.start_dir`) — the folder chi web starts in, in place of the one you run it in (`~` and a relative path work; chi web never changes its own folder): its page opens on that folder's project (the printed link, a bare `/`, the LAN link and its QR code, the Home Screen app), and a new session from the all view starts there; the header's project chip switches projects. One that isn't a folder exits 1; `--start-dir` without `web` exits 2. A chi web already running in another folder keeps it (the second one says so: restart it to change it)
17
17
  - `chi web --tunnel tailscale` — the Web UI over https on your tailnet, for your phone anywhere: Tailscale Serve (never Funnel) on this machine's `<machine>.<tailnet>.ts.net` name for this run, the owner your Tailscale login, turned off on exit (see [Tailscale Serve](configuration.md#tailscale-serve-your-tailnet)); `--tunnel` without `web` exits 2
18
18
  - `chi web push ls [--format json]`, `chi web push rm ID`, `chi web push test [ID]` — the phones chi web pushes to (see [Phone notifications](configuration.md#phone-notifications-web-push)): list them (short id, label, origin, added, last delivery, done turns, last error; `stale` for one made on another public URL than chi web's now: never sent to), remove one, or send a test notification to ID or to every device not stale. chi web need not run; nothing printed holds a device's push address or keys. A usage error exits 2; a store others can read, a corrupt one, an unknown device or a failed test push exit 1. This machine only: `chi --remote X web push` exits 2
19
- - `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
19
+ - `chi web --web-markdown` / `--no-web-markdown` — opt in to, or opt out of, sanitized Markdown rendering for completed assistant messages, the stage's asking text and a resolved question card's reply (`web.markdown`, off by default; `--no-web-markdown` turns a config's `web.markdown: true` off for this run)
20
20
  - `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))
21
21
  - `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
22
22
  - `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))
23
23
  - `chi broadcast [-m TEXT] [--all] [--dry-run]`, `chi broadcast log [--last N] [--format json]`, `chi broadcast deliver BROADCAST_ID (ID|PREFIX)...` — share one piece of information (TEXT or stdin) with every session it may concern, as a context note from `broadcast` (see [Broadcast](broadcast.md))
24
- - `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))
24
+ - `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; `chi context push NAME -m TEXT` (the long form is `--message TEXT`) gives a `--push` source its new text, read from stdin when no `-m` is given (see [Attached context](context.md))
25
25
  - `chi context report [--days N]` — the candidate judge's counts from `context_candidates.jsonl`: candidates proposed, mentioned, attached or declined by you; the judge's asks; what it would attach per threshold, and how that agrees with what you did (this machine only; see [The log and the report](context.md#the-log-and-the-report))
26
26
  - `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))
27
27
  - `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))
@@ -33,12 +33,13 @@
33
33
  - `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))
34
34
  - `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))
35
35
  - `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))
36
- - `chi self` — print version, source dir (checkout or installed gem), config/memory/session paths, model/host and bundles. Run by a session's `execute`, the `model` row (and host, loop, profile, thinking, served model) is **that session's** model, labelled `(this session <id8>; default …)`; elsewhere it is the default, `(default)`. `model key` is the memory overlay key (`<name>.<key>.md`), and `model notes` the model notes a prompt of that model loads (`model_notes_deepseek (system, 612 chars)`, `none`; inside a session its `--mute` list is left out). `chi self --model` prints only the model a new session starts on, an alias resolved to its target (the desktop helper's hint)
36
+ - `chi self` — print version, source dir (checkout or installed gem), config/memory/session paths, model/host and bundles. Run by a session's `execute`, the `model` row (and host, loop, profile, thinking, served model) is **that session's** model, labelled `(this session <id8>; default …)`; elsewhere it is the default, `(default)`. `model key` is the memory overlay key (`<name>.<key>.md`), and `big memories` the memories over `memory.entry_warn_tokens` ([Memory](memory.md#a-memorys-size)), and `model notes` the model notes a prompt of that model loads (`model_notes_deepseek (system, 612 chars)`, `none`; inside a session its `--mute` list is left out). `chi self --model` prints only the model a new session starts on, an alias resolved to its target (the desktop helper's hint)
37
37
  - `chi update [--dry-run] [--to VERSION] [--json] [--no-gem] [--no-bundles] [--no-desktop]` — update an installed chi: the gem, the system bundle, the shipped bundles you installed and the desktop helper, in one table (see [Updating](#updating))
38
38
  - `chi home`, `chi home set [--ruby PATH]`, `chi home doctor` — the chi home: the one Ruby (and its gem path) every chi launch boots, whoever launches it. `chi home` shows it (its Ruby, the newest chi in it, gem home and path, `mise`, when and by which chi it was set, and the chi you ran when that is another one; exit 1 with the problem when it can't boot chi), `set` records this chi's Ruby and gem path (or `--ruby PATH`'s own; it must have samagotchi), `doctor` checks it and what should follow it, one row each with `ok`, `warn` or `fail` and what to run (exit 1 on a `fail`; see [Checking it](configuration.md#checking-it-chi-home-doctor)). At a terminal, a chi on another Ruby than the home's says so in one dim line a day. This machine only (see [The chi home](configuration.md#the-chi-home))
39
39
  - `chi bundle install|upgrade|uninstall|status|diff|list|build|trash|new|test|check` — manage memory bundles (see [Bundle hooks](hooks.md#bundle-hooks-unified-workflow-bundle)); `list` shows the installed ones and the ones shipped with chi, which `install <name>` installs; `core` and `dev` are profiles that install a set of them (see [Bundle profiles](memory.md#bundle-profiles-core-and-dev), [Guardrails](guardrails.md), [Plugins](plugins.md#the-btw-bundle), [the mcp bundle](plugins.md#the-mcp-bundle) [the loop-guard bundle](plugins.md#the-loop-guard-bundle), [the check-in bundle](plugins.md#the-check-in-bundle) and [the skills bundle](plugins.md#the-skills-bundle)). `trash` lists and empties the bundle trash (moved files from uninstalls/upgrades). A usage error (an unknown subcommand or flag, a missing argument, a bad `build --scope`) exits 2, as for the other subcommands. Plain `chi` and `chi web` exit 1 on an unknown flag, a flag missing its value or a stray argument. `new`, `test` and `check` are for writing one: see [Writing a bundle](#writing-a-bundle-new-test-check)
40
40
 
41
41
  - `chi guardrails replay [--dry-run] [--threshold T] [--model M]` — seed System 1's record from the approvals this machine's sessions already answered: each ask is judged by the configured model and written to `guardrails/judgments.jsonl` with the answer the user gave, then a per-rule table prints (asks, allowed, denied, would-be auto-allows, false allows, asks with no judgment). A parent agent's answer is not the user's: only its deny is replayed, and only where the parent could have allowed. `--dry-run` judges without writing, `--threshold` overrides `guardrails.system_one.threshold` for the table, `--model` the configured model for this run. Exit 2 when `guardrails.system_one.mode` is off or no `host` is set; a usage error exits 2 (see [System 1](guardrails.md#system-1-experimental))
42
+ - `chi judgments ls` / `chi judgments report NAME [--days N]` — what [System 1's saved runs](system-one.md) hold: each judgment's digests, runs, labels, failures, models and size; one judgment's counts, p histogram and labels by value. Reads only; local only; `--days` must be a positive whole number.
42
43
  - `chi toolkit report [--days N]` — grade toolkit suggestions: the kit on mode would have run (the suggested kit when it would have applied it, else the default; an on-mode line's `did` says what it ran) against the tools the session called from that turn on (only that turn when the toolkit was chosen since), `call_tool` targets included, for both the chat and the raw-prompt loop. Each is *covers*, *short of a tool* (naming the tools), *wider than needed* (a narrower kit offered in the same ask would also have covered) or *ungradable* (calls it can't read back); then a per-kit tally with what on mode would have done. Includes archived sessions and caps the report at 14 days and 500 sessions. Reads `toolkit_suggestions.jsonl` (each ask logs every offered kit's rules, expanded against the called tools here); it never runs the model or changes a session. Local only; `--days` must be a positive whole number.
43
44
 
44
45
  - `chi bundle try PATH -p TEXT [options]` — install a bundle in a temporary, filtered sandbox, run one real model turn,
@@ -377,10 +378,12 @@ flag also works as `--kebab-case VALUE`, e.g. `--server-host`, `--server-port`,
377
378
  `--read-truncate-at-bytes`; an on/off setting is `--[no-]kebab-case` with no value
378
379
  (`--context-status`, `--no-context-status`). `chi --help` lists them all.
379
380
 
380
- **Which loop runs.** There is no backend flag: the model's host decides. A host with
381
- `api: openai` in config.yml is driven through the OpenAI chat API (streamed; a remote
382
- provider via `url:` and `api_key_env:`); every other host gets chi's own raw-prompt loop. `/model` and `--model host:model` switch hosts, and
383
- the loop with them. See [Configuration](configuration.md) (`hosts:` and `api:`).
381
+ **Which loop runs.** There is no backend flag: the model's host's `api:` decides. A host whose `api:` has
382
+ a chat adapter runs `LLM::ChatLoop` — `api: openai` through `LLM::OpenAIChat` (`/v1/chat/completions`,
383
+ streamed; a remote provider via `url:` and `api_key_env:`) or `api: anthropic` through
384
+ `LLM::AnthropicMessages` (`/v1/messages`). Every other host (no `api:`, or `api: llama_cpp`) runs chi's own
385
+ raw-prompt loop, `KernelLoop`, on llama.cpp's native `/completion`. `/model` and `--model host:model` switch
386
+ hosts, and the loop with them. See [Configuration](configuration.md) (`hosts:` and `api:`).
384
387
  `--backend`, `SAMAGOTCHI_BACKEND` and a `backend:` key were removed (`backend:` warns
385
388
  as an unknown key).
386
389
 
@@ -461,6 +464,11 @@ and `--shared` are refused. Its first line says it is a scratch session.
461
464
 
462
465
  ### Sharing a session
463
466
 
467
+ Two different things share a session: `session.shared` is about UIs — several
468
+ terminals and the Web UI attached to one worker — while `chi share` invites
469
+ other people, participants with a link of their own (see
470
+ [Participants](#participants)).
471
+
464
472
  Plain `chi` runs the session in a background worker and attaches the terminal
465
473
  to it (`session.shared`, default `true`). A worker's session can have any number
466
474
  of UIs at once: the Web UI and attached terminals (`chi`, `--resume`,