samagotchi 0.63.0 → 0.65.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 (90) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +86 -1
  3. data/docs/architecture.md +32 -5
  4. data/docs/cli.md +41 -9
  5. data/docs/configuration.md +143 -11
  6. data/docs/internals/engine.md +25 -6
  7. data/docs/plugins.md +7 -0
  8. data/docs/sessions.md +14 -10
  9. data/docs/sub-agent.md +21 -4
  10. data/docs/testing.md +6 -0
  11. data/lib/samagotchi/answer_command.rb +5 -1
  12. data/lib/samagotchi/bridge/turn_accumulator.rb +1 -0
  13. data/lib/samagotchi/bridge.rb +29 -5
  14. data/lib/samagotchi/bundles/slow-commands/manifest.yml +2 -2
  15. data/lib/samagotchi/bundles/slow-commands/plugin.rb +5 -3
  16. data/lib/samagotchi/bundles/system/config_modification_protocol.md +2 -2
  17. data/lib/samagotchi/bundles/system/manifest.yml +2 -2
  18. data/lib/samagotchi/child_reports.rb +7 -2
  19. data/lib/samagotchi/cli/parent_wait.rb +9 -2
  20. data/lib/samagotchi/client.rb +4 -2
  21. data/lib/samagotchi/composer_route.rb +90 -0
  22. data/lib/samagotchi/config.rb +11 -7
  23. data/lib/samagotchi/continue_offer.rb +17 -3
  24. data/lib/samagotchi/delivery.rb +13 -8
  25. data/lib/samagotchi/engine.rb +41 -7
  26. data/lib/samagotchi/generation_phase.rb +17 -0
  27. data/lib/samagotchi/host_config.rb +33 -12
  28. data/lib/samagotchi/host_model.rb +2 -2
  29. data/lib/samagotchi/host_registry.rb +34 -11
  30. data/lib/samagotchi/idle_client.rb +62 -25
  31. data/lib/samagotchi/idle_recap.rb +5 -2
  32. data/lib/samagotchi/idle_target.rb +30 -7
  33. data/lib/samagotchi/interrupted_turn.rb +7 -3
  34. data/lib/samagotchi/last_turn.rb +5 -2
  35. data/lib/samagotchi/llm/adapters.rb +41 -0
  36. data/lib/samagotchi/llm/anthropic_assembly.rb +210 -0
  37. data/lib/samagotchi/llm/anthropic_messages.rb +416 -0
  38. data/lib/samagotchi/llm/anthropic_wire.rb +238 -0
  39. data/lib/samagotchi/llm/api_key.rb +26 -9
  40. data/lib/samagotchi/llm/chat_loop.rb +113 -14
  41. data/lib/samagotchi/llm/dialect.rb +57 -0
  42. data/lib/samagotchi/llm/errors.rb +81 -12
  43. data/lib/samagotchi/llm/model_listing.rb +111 -0
  44. data/lib/samagotchi/llm/model_result.rb +9 -2
  45. data/lib/samagotchi/llm/openai_chat.rb +46 -93
  46. data/lib/samagotchi/model_profile.rb +1 -1
  47. data/lib/samagotchi/pending_input_queue.rb +10 -3
  48. data/lib/samagotchi/plugin/sessions.rb +27 -5
  49. data/lib/samagotchi/reasoning_history.rb +14 -4
  50. data/lib/samagotchi/recap_setup.rb +2 -1
  51. data/lib/samagotchi/reply_wait.rb +171 -6
  52. data/lib/samagotchi/self_report.rb +18 -28
  53. data/lib/samagotchi/send_command.rb +63 -19
  54. data/lib/samagotchi/session.rb +3 -2
  55. data/lib/samagotchi/session_commands.rb +1 -1
  56. data/lib/samagotchi/session_inbox.rb +63 -7
  57. data/lib/samagotchi/session_manager.rb +49 -20
  58. data/lib/samagotchi/session_metrics.rb +6 -11
  59. data/lib/samagotchi/steer_cut.rb +4 -0
  60. data/lib/samagotchi/terminal_ui/attached_loop.rb +55 -28
  61. data/lib/samagotchi/terminal_ui/event_renderer.rb +2 -1
  62. data/lib/samagotchi/terminal_ui/formatting.rb +7 -22
  63. data/lib/samagotchi/terminal_ui/status_row.rb +24 -3
  64. data/lib/samagotchi/terminal_ui.rb +72 -57
  65. data/lib/samagotchi/tool_activity.rb +5 -5
  66. data/lib/samagotchi/tools/delegate.rb +28 -1
  67. data/lib/samagotchi/tools/delegate_cursor.rb +9 -4
  68. data/lib/samagotchi/tools/delegate_wait.rb +17 -5
  69. data/lib/samagotchi/tools/execute.rb +11 -4
  70. data/lib/samagotchi/turn_checkpoint.rb +9 -3
  71. data/lib/samagotchi/turn_note.rb +22 -5
  72. data/lib/samagotchi/version.rb +1 -1
  73. data/lib/samagotchi/web/app.rb +2 -1
  74. data/lib/samagotchi/web/public/activity.js +3 -0
  75. data/lib/samagotchi/web/public/app.js +96 -13
  76. data/lib/samagotchi/web/public/commands.js +8 -8
  77. data/lib/samagotchi/web/public/composer.js +3 -3
  78. data/lib/samagotchi/web/public/delivery_line.js +228 -0
  79. data/lib/samagotchi/web/public/index.html +29 -0
  80. data/lib/samagotchi/web/public/live_turn.js +8 -0
  81. data/lib/samagotchi/web/public/stage_model.js +27 -7
  82. data/lib/samagotchi/web/public/stage_view.js +32 -10
  83. data/lib/samagotchi/web/public/stream_handlers.js +8 -2
  84. data/lib/samagotchi/web/public/timing.js +2 -1
  85. data/lib/samagotchi/web/public/turn_events.js +22 -8
  86. data/lib/samagotchi/web/public/turn_html.js +20 -4
  87. data/lib/samagotchi/web/public/turn_model.js +29 -12
  88. data/lib/samagotchi/web/public/turn_view.js +6 -4
  89. data/lib/samagotchi/worker.rb +7 -5
  90. metadata +9 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 20b05cd0da6dfdd621eba3b2add37f00b2abe96c2e3950d8ddd066bdf3078424
4
- data.tar.gz: ecf5784f9f82c8a8c9e813185570d326a3f4d9ad3a404033f53abf1b62a20902
3
+ metadata.gz: 43045b7872d6b50ba24cc2ef1678ac6708346aac9236a5764593660e4e81fc6f
4
+ data.tar.gz: dad96a9922329ea3294d7bf04159b3c829796bdfda03d42eef724bbee4a3e4e7
5
5
  SHA512:
6
- metadata.gz: 302c8ccf54cc947c9165fe8e30f4d32b4054ee02a1e2d62cbb7d7942a30574af58af84a7eafc5b2daf24a8f167e98024e2798fcacccf597b919435019d498613
7
- data.tar.gz: b6ac96ff05fb4e52a5705b55de3fa505054860b154cd6622c4badc232c6284bc5e80d57ffc94b13a7eae1384557acd5b697b630a0ed21be1a8e85d99610b3d04
6
+ metadata.gz: 4a35a44e9905f8c3411a656383c8a7f2493318732b959d526cdcbdc5dba90e71e874fe678057d3b060878dc5e4560398683790dade299258bd43cf541e0b62cf
7
+ data.tar.gz: 9dc56d9cd85b5c5f323315f99bc7c2f4e7580927966f41723a833db561dd5d6c0b3a8720c92ea32e1fafbb3b75e9eb0e93e57e9c4784e1a9983f95e7ac6f17c6
data/CHANGELOG.md CHANGED
@@ -8,6 +8,89 @@ and commands may change between minor versions. How releases are made:
8
8
 
9
9
  ## [Unreleased]
10
10
 
11
+ ## [0.65.0] - 2026-10-13
12
+
13
+ ### Added
14
+
15
+ - **Anthropic Messages hosts** (`api: anthropic`): streamed text and thinking, native tool use, images, usage with
16
+ cache reads and writes, thinking blocks replayed verbatim (also after `--resume`; `reasoning_history` defaults to
17
+ `all` there). A new host key `auth: bearer|x-api-key` (any host) picks the header the key goes in. Thinking levels
18
+ map to Messages' thinking (`off` disabled, `low`/`medium`/`high` adaptive at that effort, `default` nothing), with
19
+ fallbacks when a host refuses a field. Recaps, `ctx.ask_model` and broadcast triage work there too. See
20
+ docs/configuration.md, "Anthropic Messages hosts". **Known limit until the next release:** Claude 5.x models
21
+ (Opus, Sonnet and Haiku 5.5, Fable 5.1) may stop a turn with a 400 "… bound to a different conversation" after the
22
+ history changed under their thinking blocks; the next release adds the recovery.
23
+ - `dialect: fireworks` on an `api: anthropic` host, for Fireworks directly or behind a gateway: Bearer auth, no
24
+ thinking `display`, no `cache_control`, and `x-session-affinity` for cache routing. `chi self` names it.
25
+ - A provider's refusal (Messages `stop_reason: refusal`, or an empty `content_filter` stop) ends the turn with
26
+ `no answer: refused by the provider (category)` in every UI and isn't retried. A `content_filter` stop after some
27
+ text keeps the text, marked `[cut off by the provider's content filter]`.
28
+ - Web composer: a row above the text field shows the keys while idle and, while a turn runs, where a message sent now
29
+ goes in (after this thinking step, tool call or answer, with its clock), with `cut in now` and `queue for after the
30
+ turn` links. Alt+Enter (Option+Enter) queues. See docs/cli.md, "Typing during a turn".
31
+ - Terminal: while a turn runs, the status row ends in `· typing goes in at the next step (/cut, /queue)`.
32
+
33
+ ### Changed
34
+
35
+ - Web: a message queued for after the running turn sits below the running turn's card, not among its rows, also
36
+ after a reload.
37
+ - A chat host that refuses a sampling field (`temperature`, `top_p`, `top_k`) gets the step again without it instead
38
+ of the turn failing; on `api: openai` hosts such a 400 no longer turns reasoning off.
39
+ - A Splash or Fireworks model's vision comes from its model list instead of reading "unknown".
40
+
41
+ ### Fixed
42
+
43
+ - A server error without a numeric code is classified by its type: an invalid request fails at once instead of being
44
+ retried, an unknown model (404, `NOT_FOUND`) is no longer retried, a `timeout_error` is a 504.
45
+ - Anthropic's context-overflow 400s ("prompt is too long", "input length and `max_tokens` exceed context limit")
46
+ roll the turn back and report it as too long for the context window, also through a gateway on the OpenAI path.
47
+
48
+ Update with `chi update` (the system bundle moves with the gem).
49
+
50
+ ## [0.64.0] - 2026-10-13
51
+
52
+ ### Added
53
+
54
+ - `/queue TEXT` during a running turn now works in an attached terminal and the web composer too, as it did in the
55
+ plain REPL: TEXT runs as a turn of its own after the running one, instead of merging into it (`(queued: runs after
56
+ this turn)`). A session whose worker is older than this says to restart it rather than steering the turn. See
57
+ docs/cli.md, "Typing during a turn".
58
+ - `chi send --queue`: the message runs as a turn of its own after the running turn instead of merging into it, and
59
+ `chi send --queue --wait` waits for that turn's own answer (a running turn no longer refuses it; the running turn's
60
+ reply, end and questions are not the answer). Each turn's reply file now carries its message's id. See
61
+ docs/sessions.md, "Sending a message", and docs/sub-agent.md.
62
+ - Plugins: `ctx.sessions.send(id, text, delivery: :queue)` runs the message as a turn of its own after the session's
63
+ running turn; `delivery: :cut` raises (plugins never cut). See docs/plugins.md.
64
+
65
+ ### Changed
66
+
67
+ - Plain REPL (`--no-shared`): the lines a turn left over (a steer it didn't merge, `/queue` lines, image lines, commands
68
+ that wait) run in the order you sent them; before, an unmerged steer ran ahead of a `/queue` line typed earlier.
69
+ - A message with images sent during a running turn no longer holds back the text lines sent after it: they go in at
70
+ the turn's next step as usual, and the image message still runs as a turn of its own after it.
71
+
72
+ ### Fixed
73
+
74
+ - Stopping a session mid-turn (`chi sessions stop`, the web's stop, a parent stopping its delegate) cancels the turn
75
+ with its own reason, `session_stopped`, instead of the user's Stop: the turn's line reads "canceled (session
76
+ stopped)", a command it was running "stopped as the session was stopped" (slow-commands 0.2.2 counts it as
77
+ stopped, not slow), and `chi send --wait --format json` reports `cancel_reason: session_stopped`.
78
+ - A delegate (and any worker a session's own commands start, such as `chi send --new`) reads `hosts:` from config.yml
79
+ as it starts, so it sees a host added or changed after its parent's worker started; before, it got the parent's
80
+ start-time hosts through `SAMAGOTCHI_HOSTS_JSON`, which workers no longer inherit (one set in your shell
81
+ applies to that chi process only; put hosts in config.yml).
82
+ - Web: a step that only called plugin tools with a verb label is titled by the label ("echoing"), not "working
83
+ with echoing"; among other tools such a call is named by its tool ("working with read, echo").
84
+ - Web: a send from a stale tab into a session deleted meanwhile goes back to the start page with a "was not
85
+ found" notice, the line kept in the composer, instead of an alert.
86
+ - `chi send`, the web composer and a plugin's `ctx.sessions.send` now report a message the session's worker refuses
87
+ (a 4xx answer such as a delivery it doesn't run) instead of writing it as an input file the worker would merge
88
+ anyway; a body over the worker's size limit still goes in as a file, as before.
89
+ - `chi answer`, `chi send --wait ID` (no message) and `delegate_result` wait for the reply of the turn they are about
90
+ (the answered turn, the running turn, the follow-up's turn), not a newer one queued behind it; a Continue of the
91
+ step-limit question gets the continued turn's reply, and a Stop ends the wait as `not_continued`. Workers older
92
+ than this keep the old behaviour.
93
+
11
94
  ## [0.63.0] - 2026-10-13
12
95
 
13
96
  ### Added
@@ -2750,7 +2833,9 @@ and long-lived sessions.
2750
2833
  - A macOS desktop helper (`chi desktop install`): a "Send to chi" Service and a
2751
2834
  hotkey panel that send selected text or the clipboard to your sessions.
2752
2835
 
2753
- [Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.63.0...HEAD
2836
+ [Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.65.0...HEAD
2837
+ [0.65.0]: https://github.com/dm1try/samagotchi/compare/v0.64.0...v0.65.0
2838
+ [0.64.0]: https://github.com/dm1try/samagotchi/compare/v0.63.0...v0.64.0
2754
2839
  [0.63.0]: https://github.com/dm1try/samagotchi/compare/v0.62.0...v0.63.0
2755
2840
  [0.62.0]: https://github.com/dm1try/samagotchi/compare/v0.61.0...v0.62.0
2756
2841
  [0.61.0]: https://github.com/dm1try/samagotchi/compare/v0.60.0...v0.61.0
data/docs/architecture.md CHANGED
@@ -73,6 +73,12 @@ TerminalUI ── delegates ──▶ Engine
73
73
  delegates all core work to an `Engine`.
74
74
  - The `on_event:` seam on `run_turn` exposes raw `KernelLoop` events plus the
75
75
  higher-level turn events, so any new UI can render without terminal coupling.
76
+ - **`ComposerRoute`** (`lib/samagotchi/composer_route.rb`) decides where a line typed at
77
+ either terminal's prompt goes (the REPL's `steer_line` / `run_input_line`, the attached
78
+ `AttachedLoop#submit`): `/cut TEXT` / `/queue TEXT`, a terminal's own word, a session
79
+ command, a typo, a prompt. Each UI keeps what it does with the route. The web's
80
+ `composer.js#routeComposerLine` decides the same; `spec/fixtures/composer_routes.json`
81
+ holds the cases both answer (see [testing.md](testing.md)).
76
82
 
77
83
  ## Request / turn flow
78
84
 
@@ -94,7 +100,7 @@ terminal renders the events it gets over the Bridge.
94
100
  | Core | `Samagotchi::Engine` | System prompt, memory injection, tool declarations, session lifecycle, model↔tool loop (`run_turn`). No terminal coupling. |
95
101
  | UI | `Samagotchi::TerminalUI` | REPL (Reline), rendering, REPL commands. Delegates all core work to an `Engine`. |
96
102
  | Model loops | `KernelLoop` (via `LLM::NativeBackend`), `LLM::ChatLoop` | The model↔tool loop: raw prompt or OpenAI chat API, chosen per host (see below). |
97
- | Adapters | `Samagotchi::Client`, `LLM::OpenAIChat`, `LLM::HTTP` | Raw-prompt servers, the OpenAI chat API, and the HTTP both share. |
103
+ | Adapters | `Samagotchi::Client`, `LLM::OpenAIChat`, `LLM::AnthropicMessages`, `LLM::HTTP` | Raw-prompt servers, the OpenAI chat API, Anthropic's Messages API, and the HTTP they share. |
98
104
  | Tools | `lib/samagotchi/tools/*` | Execute, read, edit, write, memory, task_*, web_fetch, delegate/delegate_result, ask_user_question, reminders, send_note, list_sessions, context_read, forget_outputs, plus runtime/output-guardrails. |
99
105
  | Bundles | `lib/samagotchi/memory_bundle/*`, `BundleCommand` | `chi bundle`: install/upgrade (3-way merge), build, uninstall; the install record under `.bundles/<name>/` that the hook, guardrail and plugin loaders read ([internals/bundles.md](internals/bundles.md)). |
100
106
  | Background | `Samagotchi::Worker`, `Samagotchi::SessionManager` | `SessionManager` forks/spawns one worker process per session; its `Worker` builds the `Engine` and `Bridge` (no terminal rendering) and runs queued turns. The spawned process (own process group) forks the worker, which `setsid`s and is reparented to init once its starter exits (it waits until the worker holds `owner.lock`, so the spawner's `Process.detach` waiter tells a starting worker): nothing that walks the spawner's process tree reaches it (`SAMAGOTCHI_WORKER_DETACH=1`, `detach_from_spawner`). The lock's pid is the worker's; `spawn`'s `child_pid` is its starter's. |
@@ -369,7 +375,11 @@ session every 10 s, so a `kill -9` shows within a second. The summary (`Web::Ses
369
375
  shared with `/api/sessions` and the session view) carries `owner`, `project_root` and
370
376
  `bridge_up` (the sidecar is there *and* the lock is held: the page attaches its stream on it), and
371
377
  from that sidecar `worker_version` (the chi the worker runs) and `worker_features`
372
- (`Bridge::FEATURES`, what a client may ask of it beyond the routes). A worker's Bridge snapshot
378
+ (`Bridge::FEATURES`, what a client may ask of it beyond the routes: `restart`, `task_stop`, `reparent`,
379
+ `delivery`, a worker that queues a message for after the turn; `SessionManager.deliver_turn` and
380
+ the attached TUI refuse a queue to a worker without it, which would steer; `turn_ids`, a worker whose running
381
+ turn's mark, continue turn and Stop carry the turn's `enqueued_id`, which `chi answer`, `chi send --wait` with no
382
+ message and `delegate_result` rely on through `ReplyWait.own_turn`). A worker's Bridge snapshot
373
383
  names its `chi_version` too.
374
384
  `POST/DELETE /api/sessions` and `/stop` rescan the session before answering (`SessionHub#touch`).
375
385
  `GET /api/sessions` is the hub's projection too (its sort, paging and total). `Server` always builds
@@ -388,6 +398,7 @@ Engine picks the loop from the effective model's host (`HostRegistry#resolve`):
388
398
  |---|---|---|---|
389
399
  | Raw prompt | `KernelLoop`, wrapped by `LLM::NativeBackend` | no `api:`, or `llama_cpp`/`mlx`/`omlx` | `Client` (`/completion` or `/v1/completions`), chi's own Gemma/Qwen prompt and tool-call parsing |
390
400
  | Chat | `LLM::ChatLoop` | `api: openai` | `LLM::OpenAIChat` (`/v1/chat/completions`, streamed, native tool calls) |
401
+ | Chat | `LLM::ChatLoop` | `api: anthropic` | `LLM::AnthropicMessages` (`/v1/messages`, streamed SSE, native `tool_use`) |
391
402
 
392
403
  Both return an `LLM::ModelResult` and emit the same stream events; tool calls in
393
404
  both go through `ToolRunner` (events, hooks, veto, output cap) and
@@ -428,12 +439,28 @@ threshold puts the model's own `[CONTEXT: …]` line on the tail as a system
428
439
  message (docs/internals/context-telemetry.md).
429
440
 
430
441
  **Adapters.** `Client` (raw-prompt servers) and `LLM::OpenAIChat` (one per host,
431
- `HostRegistry#adapter_for`) share `LLM::HTTP`: timeouts, TLS for https, a line
432
- reader for streamed bodies, the retry loop (`retry.*`; network errors, 429 and
442
+ `HostRegistry#adapter_for`, built by `LLM::Adapters` from the host's api) share
443
+ `LLM::HTTP`: timeouts, TLS for https, a line reader for streamed bodies, the retry loop (`retry.*`; network errors, 429 and
433
444
  500/502/503/504/529, honouring `Retry-After`; never after a stream has produced
434
445
  output) and cancel. Cancel closes the in-flight socket from the
435
446
  `CancellationController` listener, so it works on any thread.
436
447
 
448
+ `LLM::AnthropicMessages` talks Anthropic's Messages API (`POST {root}/v1/messages`) for the same chat loop:
449
+ ChatLoop builds the same OpenAI-shaped wire messages for every chat host, and the adapter translates them
450
+ (`LLM::AnthropicWire`: the head system message becomes the top-level `system` blocks, tool calls `tool_use` blocks,
451
+ tool messages `tool_result` blocks in one user message, image parts `image` blocks, later system messages user
452
+ text; empty text blocks are never sent). Its answer (`LLM::AnthropicAssembly`) is a `ChatResponse` whose
453
+ `provider_content` holds the content blocks as received, which ChatLoop saves on the model entry as
454
+ `anthropic_content` (string keys, so a resumed session sends the same bytes) and sends back verbatim when the
455
+ adapter `replays_content?` (only for entries whose calls are all paired, never for an interrupted one; earlier
456
+ entries' thinking blocks left out under `reasoning_history: turn`) (thinking blocks
457
+ need their signature and their order among text and `tool_use`). Usage is normalized to the OpenAI shape the rest
458
+ of chi reads (the prompt is input + cache reads + cache writes), stop reasons to the loop's finish reasons
459
+ (`max_tokens` → `length`, `tool_use` → `tool_calls`, `end_turn` → `stop`). A stream is read by content-block index
460
+ from the `data:` lines alone (`event:` lines are not needed); `message_start` / `message_delta` events reach
461
+ `on_delta` with that normalized `usage`, pings not at all, and a block's start or delta counts as shown for the
462
+ first-token limit. Both adapters share their model-list cache (`LLM::ModelListing`).
463
+
437
464
  **Errors.** A failed request raises an `LLM::ProviderError` of one kind:
438
465
  `ConnectionError` (`RetryExhausted`), `RateLimited` (`CreditsHeld`: a 402 about
439
466
  credit held by in-flight requests, retried after 20 s), `OutOfCredits` (any other
@@ -448,7 +475,7 @@ stream events (server counts, else a chars/4 estimate). Model lists are `LLM::Mo
448
475
  server (`/props`), then the host's model list, then config.
449
476
 
450
477
  **Keys.** A host's API key comes only from the environment variable its
451
- `api_key_env:` names; it never reaches config.yml, `HOSTS_JSON`, `chi self`,
478
+ `api_key_env:` names; it never reaches config.yml, `SAMAGOTCHI_HOSTS_JSON`, `chi self`,
452
479
  logs or events.
453
480
 
454
481
  ## Other components
data/docs/cli.md CHANGED
@@ -18,7 +18,7 @@
18
18
  - `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 (a live worker's `GET stats`, else its saved `analytics.json`; `--format json` one object) 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`) exits 2; an unknown or refused session exits 1
19
19
  - `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))
20
20
  - `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))
21
- - `chi send [-m TEXT] [--image PATH]... [--cut] (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); 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; `--format json` prints one JSON object instead; 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))
21
+ - `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; `--format json` prints one JSON object instead; 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))
22
22
  - `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`). An approval can be denied, not allowed, unless `guardrails.parent_approvals: once` (a convention, not a security boundary; see [Guardrails](guardrails.md#approvals-from-a-parent-agent))
23
23
  - `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))
24
24
  - `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))
@@ -298,8 +298,8 @@ and cuts nothing. `/cut TEXT` in a terminal or the web composer (or `chi send --
298
298
  once the model has streamed only thinking for `steer.cut_after` seconds (per host/model: `steer_cut_after`; default
299
299
  20), now or while your message waits, that generation is cut and the step starts again with your message, marked
300
300
  `↪ cut in for your message` (see
301
- [configuration.md](configuration.md#messages-during-a-running-turn-steer-and-cut)). The web
302
- composer has no `/queue` yet (it says so and sends nothing). The
301
+ [configuration.md](configuration.md#messages-during-a-running-turn-steer-and-cut)). `/queue TEXT`
302
+ (terminal or web composer, or `chi send --queue`) keeps TEXT for after the turn: it runs as a turn of its own once the running one ends. The
303
303
  first answer to an `ask_user_question` wins; the other UIs close their widget.
304
304
  An empty answer dismisses the question in every UI. The model is then told
305
305
  not to go ahead with what it asked about, or change anything else, and to wait.
@@ -541,12 +541,40 @@ REPL alike:
541
541
  20 s)` (it waits for the thinking; the step may end first) or `(cutting is
542
542
  off; goes in at the next step)` (`steer.cut_after` is 0, per host/model:
543
543
  `steer_cut_after`, or no generation to cut).
544
- - `/queue TEXT` (plain REPL only) runs TEXT as a turn of its own after this
545
- one: `(queued: runs after this turn)`. An attached terminal says
546
- `(/queue isn't available in an attached session yet)` and sends nothing (the web
547
- composer: `(/queue isn't available in the web yet; …)`).
544
+ - `/queue TEXT` runs TEXT as a turn of its own after this one, in an attached
545
+ terminal, the plain REPL and the web composer alike: `(queued: runs after this
546
+ turn)` (the web draws it as a queued bubble until its turn starts). It never
547
+ merges into the running turn, and a plain line sent after it still does.
548
+ Several queued lines run one turn each, in the order sent; they still run
549
+ after a Ctrl-C or a failed turn, and a queued line drops a continue offer as
550
+ any prompt does. Other terminals show someone else's queued line with the same
551
+ note. A session whose worker is older than queueing would steer it instead, so
552
+ it is not sent: `(/queue needs a newer worker: restart it with chi sessions
553
+ restart, then send again)` (the web shows the refusal under the line). A line
554
+ with images works the same way: it waits for the turn's end, and the lines
555
+ sent after it still go in at the next step.
548
556
  At the open prompt (no turn running) `/cut TEXT` and `/queue TEXT` run TEXT
549
557
  as a normal prompt.
558
+ - **The web composer** has a row above the text field in a session (always
559
+ there, so the composer never changes height): the keys while idle, and while
560
+ the turn runs the delivery line, where a message sent now goes in (`↳ goes in after this
561
+ thinking step · 0:42 · 3k chars`, `… after this tool call (execute · 0:12)`,
562
+ `… after this answer; the turn may go on`, `… once the card above is
563
+ answered`), then two links. **Enter** (or Send) is the next step;
564
+ **Alt+Enter** (Option+Enter) or `queue for after the turn` queues it, like
565
+ `/queue TEXT`, and the bubble is dashed (`after this turn`), below the
566
+ running turn's card with the commands queued for its end, until its turn
567
+ starts; `cut in now` cuts, like `/cut TEXT` (by click only: it throws the
568
+ thinking away). Cut is enabled only while the step has streamed nothing but
569
+ thinking and cutting is on for the model (the line says `cuts at about 0:25
570
+ if you choose cut in now` until the thinking passes `steer_cut_after`, and
571
+ `cutting is off for <model>` with no cut link when it is 0); the queue link
572
+ is gone for a worker too old to queue (Alt+Enter then sends nothing, keeps the
573
+ text and says to restart the worker). A link clicked with an empty composer
574
+ arms that choice (`cut in now ✓ · Enter sends`); a second click or Esc lets
575
+ go of it. The echo says what it waits for: `waits for the next step` or
576
+ `cutting in…`, then `steered`. A phone shows the short form (`↳ after this
577
+ thinking · 0:42`) with `cut in` and `queue` chips.
550
578
  - Commands answer, wait or are refused, by what they do:
551
579
  - At once, beside the turn: `/stats`, `/recap`, `/help`, `/context`, a
552
580
  plugin's anytime command (`/btw`), and the commands that only show
@@ -700,7 +728,9 @@ approval, a hook's question and the step limit don't): two muted lines
700
728
  under its `question → answer` line, the whole text when you open the card,
701
729
  in both views and after a reload. The chip at the stage's bottom edge, `N steps · M tool
702
730
  calls` with one tick per call, opens the turn view's block right above it (newest step first
703
- while it runs; a tick opens its step). `▾` folds the stage to its status
731
+ while it runs; a tick opens its step). Below the chip, past a dashed rule, wait the
732
+ messages queued for after the turn and the commands queued for its end; they go into
733
+ the history after the turn's answer. `▾` folds the stage to its status
704
734
  row, remembered in this browser. The stage keeps its height while a turn
705
735
  runs: it grows, with a short ease, when a card or the answer needs room and gives the room back
706
736
  only when the turn moves into the history. When the turn ends the answer shows in
@@ -836,7 +866,7 @@ Notes:
836
866
  - Without `--default` the command is session-scoped and does not rewrite config files.
837
867
  - With `--default` the new default is written to `~/.config/samagotchi/config.yml` (honoring `XDG_CONFIG_HOME`) and takes effect for all new sessions; the current session's effective model is also updated immediately. Bare aliases and `host:model` are both valid.
838
868
  - `--default` and `--alias` change only their one key's line in `config.yml` (`default.model`, or the alias under `model_aliases:`): comments and the rest of the file stay as written, a symlinked file is written through and keeps its mode. When the edited text wouldn't read back as the expected settings (a flow-style section, anchors), chi writes the whole file out from the parsed data instead, which drops its comments.
839
- - Worker sessions inherit `hosts:` via `SAMAGOTCHI_HOSTS_JSON`.
869
+ - A worker reads `hosts:` from config.yml when it starts (not from the chi that started it).
840
870
  - The idle recap uses the session's current model (a switch counts from the next recap), unless `recap: {host_ref, model}` pins one.
841
871
 
842
872
  ## Session recap
@@ -945,6 +975,8 @@ status> model=Qwen3.6-35B | ⑂ 2 running · 1 waiting | ctx=8.1% (under20) | me
945
975
  report none, `api: openai`, at the turn's end).
946
976
  - `mem:`: the memories the session read, with its `--memory` list; `muted:` its `--mute` list.
947
977
  Up to 8 names each, then `+N`.
978
+ - While a turn runs it ends in `· typing goes in at the next step (/cut, /queue)`; the hint is the first thing dropped
979
+ on a narrow terminal.
948
980
  - The row is cut to the terminal's width. Without a live region (output or input not a terminal,
949
981
  `TERM=dumb`) it prints as a line when it changes.
950
982
  - `SAMAGOTCHI_STATUS_LINE` / `status.line` (default `on`): `off`, `false` or `0` hides it.
@@ -106,7 +106,10 @@ Behavior:
106
106
  at its start until it is stopped; the web's model picker lists a `hosts:`
107
107
  edit at once). A worker gets the CLI flags of the chi that started it
108
108
  through its environment.
109
- - Workers inherit hosts via `SAMAGOTCHI_HOSTS_JSON` propagated through `SessionManager.spawn_options`.
109
+ - A worker reads `hosts:` from the file as it starts, as every other setting, so a delegate (or any session
110
+ started later) sees a host added after its parent started. `SAMAGOTCHI_HOSTS_JSON` (a JSON object in the shape
111
+ of `hosts:`) replaces the file's `hosts:` for the process it is set on, the way specs use it; no worker inherits
112
+ it (`SessionManager.spawn_options` unsets it).
110
113
 
111
114
  This lets you run `chi` without repeating common defaults such as model
112
115
  and llama host/port on every invocation.
@@ -228,7 +231,8 @@ hosts:
228
231
 
229
232
  `chi self` shows the variable and whether it is set (`api key FIREWORKS_API_KEY (set)`).
230
233
 
231
- Every host with `api_key_env:` sends `Authorization: Bearer <key>` on each request,
234
+ Every host with `api_key_env:` sends `Authorization: Bearer <key>` on each request (`api: anthropic`:
235
+ `x-api-key: <key>`, see [Anthropic Messages hosts](#anthropic-messages-hosts)),
232
236
  chat or raw-prompt alike, so a llama.cpp started with `--api-key` works as a native
233
237
  host too (`chi bootstrap --key-env VAR` writes such an entry). An unset variable
234
238
  fails the turn before any request (`set VAR`); a 401/403 names the variable to
@@ -289,6 +293,90 @@ workers, and resumed sessions all use the loop of the model's host. Hosts withou
289
293
  `api: openai` keep using the native `/completion`, `/v1/completions`, or oMLX
290
294
  transport path.
291
295
 
296
+ ### Anthropic Messages hosts
297
+
298
+ `api: anthropic` makes a host talk Anthropic's Messages API (`POST {url}/v1/messages`) through the same chat loop:
299
+ Anthropic's own API, a gateway that serves `/v1/messages`, or a local server that does (Splash). The url is the API
300
+ root **without** `/v1`, as `ANTHROPIC_BASE_URL` takes it (a url that ends in `/v1` is accepted: no second `/v1` is
301
+ added); an entry with neither `url:` nor `host:` talks to `https://api.anthropic.com`. The key goes in the
302
+ `x-api-key` header; `auth: bearer` sends it as `Authorization: Bearer` instead (a gateway or provider that wants
303
+ that), `auth: x-api-key` is the default spelled out. Exactly one of the two headers is sent.
304
+
305
+ ```yaml
306
+ hosts:
307
+ claude:
308
+ api: anthropic
309
+ api_key_env: ANTHROPIC_API_KEY
310
+ gateway:
311
+ url: https://gateway.example.com
312
+ api: anthropic
313
+ api_key_env: GATEWAY_KEY
314
+ auth: bearer
315
+ splash-msg:
316
+ url: http://192.0.2.10:8000
317
+ api: anthropic
318
+ ```
319
+
320
+ `auth:` works on any host (`bearer` is the default everywhere but `api: anthropic`).
321
+
322
+ **Known limit (until the next release):** Claude 5.x models (Opus, Sonnet and Haiku 5.5, Fable 5.1) bind each
323
+ thinking block to the request it came from. When the history before a block changes (the system prompt rebuilt after a
324
+ worker restart or a `/model` switch, a changed tool set, the forget layer's edits), such a model may answer HTTP 400
325
+ "… bound to a different conversation" and the turn fails with that error; `/reasoning-history turn` makes it rarer.
326
+ The next release sends Anthropic's binding controls and recovers from that 400. Fireworks, Splash and other
327
+ non-Claude models are not affected.
328
+
329
+ The loop, tools, events, hooks and UIs are the chat loop's, as for `api: openai`; only the wire differs: the
330
+ system prompt goes as the request's `system` (later system notes as user text), tool calls as `tool_use` blocks,
331
+ results as `tool_result` blocks, images as `image` blocks; thinking streams into the thinking lane; usage counts
332
+ cache reads and writes into the prompt (so `/stats` shows them). Without `default.max_tokens`, `max_tokens` is the
333
+ model list's `max_tokens` capped at 64000, else 64000 (Messages requires one). chi's OpenAI-only request fields are
334
+ never sent there; `sampling:` passes as written.
335
+
336
+ Its model list is `GET {url}/v1/models` (every page): Anthropic's shape (the window from `max_input_tokens`, images
337
+ from `capabilities.image_input`) or the OpenAI shape a gateway, Splash or Fireworks serves there (images from
338
+ `input_modalities`, `vision` or `supports_image_input`). A gateway whose list lacks an
339
+ id (a high-availability alias) takes it from the host's declared `models:`, as on any host.
340
+
341
+ **Dialects.** A provider whose `/v1/messages` departs from Anthropic's gets `dialect:` on its host (only on
342
+ `api: anthropic`; an unknown value, or a dialect on another api, warns once and is ignored). `chi self` names it on
343
+ its `loop` line. `dialect: standard` is the default written out. There is one other dialect today, `fireworks` (Fireworks' Anthropic-compatible API, directly or behind a gateway):
344
+
345
+ - the key goes as `Authorization: Bearer` (an explicit `auth:` still wins);
346
+ - thinking at `low`/`medium`/`high` is `{type: "adaptive"}` with `output_config.effort`, without `display` (Fireworks'
347
+ schema has none and refuses unknown fields; it returns thinking by default); a model switched to the budget form gets
348
+ none either;
349
+ - no `cache_control` breakpoints (Fireworks caches prompt prefixes by itself);
350
+ - `x-session-affinity: <session id>` beside `Session-Id`, so a session stays on one replica, where its cache is.
351
+
352
+ A host without `dialect:` is standard Anthropic (Claude, Splash). Give each upstream kind its own host entry: when a
353
+ gateway serves both Fireworks models and Claude ids on one url, the Claude ids go on a second entry with the same url
354
+ and no `dialect:` (Claude wants `display: summarized` and the breakpoints).
355
+
356
+ ```yaml
357
+ hosts:
358
+ work-msg: # the work gateway's Messages route, Fireworks behind it
359
+ url: https://gateway.example.com # the API root, no /v1 (chi adds /v1/messages)
360
+ api: anthropic
361
+ dialect: fireworks # Bearer auth, no thinking display, no cache_control, x-session-affinity
362
+ api_key_env: WORK_API_KEY
363
+ # auth: x-api-key # only if the gateway itself wants that header
364
+ thinking: medium # low|medium|high → output_config.effort; off → thinking disabled
365
+ models:
366
+ accounts/fireworks/models/kimi-k2p5:
367
+ price: {input: 0.6, cache_read: 0.1, output: 2.5} # example rates: put the gateway's (no cache writes on Fireworks)
368
+ work-claude: # the same gateway's Claude ids: standard Messages
369
+ url: https://gateway.example.com
370
+ api: anthropic
371
+ api_key_env: WORK_API_KEY
372
+ auth: bearer # if the gateway wants Bearer for Claude ids too
373
+ fireworks: # Fireworks directly
374
+ url: https://api.fireworks.ai/inference
375
+ api: anthropic
376
+ dialect: fireworks
377
+ api_key_env: FIREWORKS_API_KEY
378
+ ```
379
+
292
380
  To manually verify a live chat-loop tool round trip, run the gated integration
293
381
  spec. It requires the model to call `execute` and return the current UTC date:
294
382
 
@@ -372,7 +460,7 @@ system prompt stays the same; a server that swaps models between turns goes unno
372
460
  `server (chat_template)`, `name`, `default`); `chi self` shows what config says without asking the server. A chat
373
461
  host (`api: openai`) formats nothing itself: its profile comes from the name and only strips thought tags.
374
462
 
375
- Workers get `hosts:` (with `profile:`) through `SAMAGOTCHI_HOSTS_JSON` and read `models:` from the same config file.
463
+ Workers read `hosts:` (with `profile:`) and `models:` from the config file.
376
464
  `--profile` reaches the worker a chi starts, but a worker that another process wakes later (`chi web`, `--attach`
377
465
  after an idle exit) gets that process's environment, so put a lasting choice in config.
378
466
 
@@ -416,8 +504,8 @@ models:
416
504
 
417
505
  `/model` shows what applies (`sampling: temperature=0.6 presence_penalty=1.5 (hosts.work)`), and each request's
418
506
  `stream` line in the debug log carries a `sampling=` field with what was sent. The fields are read every turn, so a
419
- `/model` switch takes the new model's. A worker gets `hosts:` (with `sampling:`) when it starts, through
420
- `SAMAGOTCHI_HOSTS_JSON`, and reads `models:` from the config file each turn: after changing a host's `sampling:`,
507
+ `/model` switch takes the new model's. A worker reads `hosts:` (with `sampling:`) from the config file when it
508
+ starts, and `models:` each turn: after changing a host's `sampling:`,
421
509
  stop the session's worker (`chi sessions stop`) for it to take effect.
422
510
 
423
511
  ## Prices
@@ -509,7 +597,13 @@ models:
509
597
  - `/model` shows the level and where it came from (`thinking: off (models: qwen3.6-35b-a3b)`, `thinking: low
510
598
  (session)`), `/stats` on its `thinking:` line, `chi self` too (a session's own level first when run from its
511
599
  commands).
512
- - The idle recap and plugins' side questions always ask with thinking off, whatever the level.
600
+ - The idle recap, plugins' side questions and a broadcast's triage always ask with thinking off, whatever the
601
+ level. A model that refuses that gets no thinking fields on an `api: openai` host (its default); on an
602
+ `api: anthropic` host, where such a model always thinks, it gets adaptive thinking at `low` effort and a limit of
603
+ at least 4096 tokens, so the thinking leaves room for the answer; a model that refuses that too (one that takes no
604
+ thinking fields at all) gets none, at the normal limit. What a host refused is kept for the next recap, the next
605
+ `ctx.ask_model` on the same model, and the rest of a broadcast. A recap and `ctx.ask_model` send the session's id
606
+ as the turns do (`Session-Id`, and `x-session-affinity` under `dialect: fireworks`); a triage sends none.
513
607
 
514
608
  What each backend gets:
515
609
 
@@ -518,6 +612,20 @@ What each backend gets:
518
612
  | native (`/completion`), `qwen36` | an empty thought after the assistant cue, and no turn preamble | no knob: thinking stays as the model has it, one notice |
519
613
  | native, `gemma4` | no `<\|think\|>` token at the start of the system prompt | no knob, one notice |
520
614
  | chat host (`api: openai`) | `chat_template_kwargs: {enable_thinking: false}` and `reasoning_effort: "none"` | `reasoning_effort: <level>` |
615
+ | Messages host (`api: anthropic`) | `thinking: {type: "disabled"}` | `thinking: {type: "adaptive", display: "summarized"}` and `output_config: {effort: <level>}` |
616
+ | Messages host, `dialect: fireworks` | `thinking: {type: "disabled"}` | `thinking: {type: "adaptive"}` and `output_config: {effort: <level>}` (no `display`) |
617
+
618
+ On a Messages host `default` sends nothing: the provider's own default, which differs by model (some Claude models
619
+ think adaptively but show no thinking text, their blocks still going back; others, and Splash, don't think at all). A model that refuses `disabled` (current
620
+ Claude models) gets the request again without it, as on any chat host. A model that refuses adaptive thinking
621
+ (older Claude: Haiku 4.5) is switched, once and for the worker's life, to the budget form: `thinking: {type:
622
+ "enabled", budget_tokens: B}` with B 2048 / 8192 / 24576 for low / medium / high and no effort; `max_tokens` always
623
+ stays above the budget (the budget is lowered to leave 1024 for the answer, or `max_tokens` raised to 2048). A
624
+ host that refuses the thinking `display` field gets thinking without it from then on (the level still applies); one
625
+ that refuses `cache_control` gets no cache breakpoints from then on, and a `max_tokens` over the model's output
626
+ limit is sent again with the limit the host names. On any chat host, a model that refuses a sampling field
627
+ (`temperature`, `top_p`, `top_k`; Claude refuses the empty-answer retry's temperature while thinking) gets the
628
+ request again without them, and none of them after.
521
629
 
522
630
  On chat hosts: llama.cpp honours both off switches but ignores the effort (when its `/props` says
523
631
  `chat_template_caps.supports_reasoning_effort: false`, chi says so once per session and host, from the `/props`
@@ -590,10 +698,17 @@ models:
590
698
  turns included, and answers HTTP 400 without it: under `turn` every turn after one that thought fails. Set
591
699
  `reasoning_history: all` there (the example above). DeepSeek through OpenRouter takes `turn`.
592
700
  - `drop` sends none (what chi did before this setting).
701
+ - **On an `api: anthropic` host** the reasoning goes back as the answer's own content blocks, verbatim (thinking
702
+ blocks with their signatures, in their order among text and `tool_use`), not as `reasoning_content`. The default
703
+ there is `all`: Claude keeps earlier turns' thinking in context, and leaving it out would rewrite the cached prefix
704
+ each turn (a Qwen template on Splash drops earlier turns' reasoning itself, so `all` costs nothing there). `turn`
705
+ leaves earlier turns' thinking blocks out (their text and calls stay), and `drop` counts as `turn`: the running
706
+ turn's thinking blocks always go back (a tool loop with thinking needs the last reply's). An interrupted reply, or
707
+ one whose tool calls never got their results, goes back as plain text without its blocks.
593
708
  - `all` sends every reply's reasoning, earlier turns too. It sends only the field: a local template that leaves out
594
709
  earlier turns' reasoning (Qwen's) needs `preserve_thinking` as well (above).
595
710
  - Order, first set wins: the session's own level, `SAMAGOTCHI_REASONING_HISTORY` (the whole process; for A/B runs),
596
- the `models:` entry (found the way `profile:` is), the `hosts:` entry, then `turn`. Anything else than a level warns
711
+ the `models:` entry (found the way `profile:` is), the `hosts:` entry, then `turn` (`all` on `api: anthropic`). Anything else than a level warns
597
712
  once and counts as unset. A host's level reaches a worker when it starts, as its `sampling:` does.
598
713
  - **A session's own level** (`drop`, `turn` or `all`; an explicit `turn` counts as its own) is saved in the session
599
714
  (its file's `"reasoning_history"`), so a `--resume`, a respawned worker, a continue and `/model` keep it; a plugin's
@@ -888,6 +1003,12 @@ many times per turn; `0` ends the turn at the empty answer as before. An answer
888
1003
  (90 % or more) is not retried. When the retries run out the turn ends with no answer: every UI shows one muted line,
889
1004
  `no answer: the model returned nothing (after 1 retry)`, and the model is told on its next turn with a hidden note;
890
1005
  no made-up answer is saved.
1006
+ A provider that refuses the answer (a Messages host's `stop_reason: refusal`, or an OpenAI-compatible host's
1007
+ `finish_reason: content_filter` with no text) is not asked again: the turn ends with no answer (text streamed before
1008
+ a Messages refusal is not kept), every UI shows `no answer: refused by the provider (cyber)` (the category from
1009
+ `stop_details`, when there is one), and the hidden note says the answer was refused. A `content_filter` stop after
1010
+ some text keeps that text as the answer, followed by `[cut off by the provider's content filter]`, and is not asked
1011
+ again either. Tool calls that come with either are not run.
891
1012
  A generation a plugin cuts while it streams (loop-guard's thinking watch, or any `stop_generation`,
892
1013
  [hooks.md](hooks.md#watching-the-stream)) uses the same budget: `↻ cut by loop-guard, asking again (1/1)`, and with
893
1014
  none left the turn ends as cancelled (hook).
@@ -907,7 +1028,8 @@ that model's cut wait (`steer.cut_after` seconds, default `20`, `0` = never; cou
907
1028
  so the wait for a busy server doesn't count) and still does, and the model starts the step again with the message
908
1029
  (`↪ cut in for your message`). One that comes earlier cuts once the thinking passes it, unless the step ends first
909
1030
  and the message goes in there. A plain message (from the web composer, `chi send` without `--cut`, a parent agent)
910
- never asks for a cut. Such a cut spends no `retry.empty_answer` attempt and sends no hidden note; only the cut
1031
+ never asks for a cut, nor does a queued one (`/queue TEXT`, `chi send --queue`, `ctx.sessions.send(…, delivery:
1032
+ :queue)`), which runs as a turn of its own after the running one. Such a cut spends no `retry.empty_answer` attempt and sends no hidden note; only the cut
911
1033
  thinking is lost. A generation that has streamed visible text or a tool call is never cut, and a plugin's steer
912
1034
  (`ctx.steer`, `ctx.sessions.send`) never cuts.
913
1035
 
@@ -931,9 +1053,12 @@ models:
931
1053
 
932
1054
  `/model` shows the resolved wait (`; steer cut: after 60 s (models: qwen)`, `; steer cut: never (hosts.splash)`).
933
1055
 
934
- `/queue TEXT` in the plain REPL runs TEXT as a turn of its own after the running one; the attached terminal and the web
935
- don't queue yet, and a request that asks the Bridge or the web for `delivery: "queue"` is refused (400
936
- `delivery_unavailable`).
1056
+ `/queue TEXT` (in a terminal or the web composer) runs TEXT as a turn of its own after the running one: the request
1057
+ carries `delivery: "queue"`, the input file keeps it, and the worker's drain leaves the file for after the turn. The
1058
+ Bridge acks `delivery: "queue"` while a turn runs (to an idle session the message is simply the next turn:
1059
+ `next_step`). A worker whose sidecar doesn't name the `delivery` feature (older than queueing) would steer it, so the
1060
+ sender refuses it (`409 delivery_unavailable`, "restart it"), and so does a worker that answers the request with a
1061
+ 400 `delivery_unavailable`.
937
1062
 
938
1063
  ## Server errors
939
1064
 
@@ -952,6 +1077,13 @@ message (before, a failed llama.cpp `/completion` ended the turn as
952
1077
  | bad request | other 4xx; a prompt larger than the context window, whatever the status | no |
953
1078
  | protocol | a body the API doesn't promise | no |
954
1079
 
1080
+ An error event (or an error body inside a 200) has no HTTP status of its own: its numeric `error.code` stands for
1081
+ one, or a Google-style code name (Fireworks: `INVALID_ARGUMENT` 400, `UNAUTHENTICATED` 401, `PERMISSION_DENIED` 403,
1082
+ `NOT_FOUND` 404, `RESOURCE_EXHAUSTED` 429), else its `error.type` (Anthropic's names; some OpenAI-compatible servers,
1083
+ OpenAI and Ollama, use some of them: `invalid_request_error` 400, `authentication_error` 401, `billing_error` 402,
1084
+ `permission_error` 403, `not_found_error` 404, `request_too_large` 413, `rate_limit_error` 429, `api_error` 500,
1085
+ `timeout_error` 504, `overloaded_error` 529); with neither it counts as a 500.
1086
+
955
1087
  The turn's prompt and its completed tool calls stay in the session.
956
1088
 
957
1089
  ## Debug Log File
@@ -54,8 +54,10 @@ Engine#run_turn(session, prompt, on_event:, max_iterations:, cancel_controller:,
54
54
  │
55
55
  ├── complete_turn(turn, result)
56
56
  │ # end_turn(turn, "completed" | "canceled"): under the event lock the session gets the kept
57
- │ # messages, goes IDLE, records last_turn, and :turn_completed {result, turn_summary,
58
- │ # display_pending} or :turn_canceled {cancellation_reason, cancelled_by, duration_ms} is emitted
57
+ │ # messages, goes IDLE, records last_turn (enqueued_id: the turn's input, from its origin;
58
+ │ # a continue turn's origin carries its offer turn's, ContinueOffer#run_continue_turn),
59
+ │ # and :turn_completed {result, turn_summary, display_pending} or :turn_canceled
60
+ │ # {cancellation_reason, cancelled_by, duration_ms} is emitted
59
61
  │ # then the :after_turn hook (status, messages, present: the AnswerDisplay presenter,
60
62
  │ # refs: the AnswerRefs adder)
61
63
  │ # store_answer_display (one replace of the answer: `display` and/or `display_refs`;
@@ -127,8 +129,8 @@ The Engine does not talk to models directly. `Engine#backend` picks the loop the
127
129
 
128
130
  ```
129
131
  Engine#backend → backend_for(@host_registry.resolve(@effective_model_name))
130
- → host entry chat? (api: openai): LLM::ChatLoop (one per Engine, built on first use)
131
- → else: LLM::NativeBackend wrapping the Engine's KernelLoop
132
+ → host entry chat? (an api in LLM::Adapters::BY_API: openai, anthropic): LLM::ChatLoop (one per Engine, built on first use)
133
+ → else: LLM::NativeBackend wrapping the Engine's KernelLoop
132
134
  ```
133
135
 
134
136
  Both implement the `LLM::ModelBackend` contract (`#complete(messages:, max_iterations:, on_stream_event:,
@@ -208,7 +210,10 @@ The differences from the native loop:
208
210
  - A 400 about reasoning (`BadRequest#reasoning_refused?`) for a request that sent reasoning back asks the step again
209
211
  without it; when that goes through, `#reasoning_history_refused!` marks the model (worker lifetime) and
210
212
  `:reasoning_history_refused` goes out. When it is refused too, the thinking-fields fallback (`#thinking_refused!`,
211
- `:thinking_refused`) runs on it, still without reasoning. Up to three requests per step.
213
+ `:thinking_refused`) runs on it, still without reasoning. Up to three requests per step. A 400 about a thinking
214
+ block's signature (`BadRequest#thinking_signature?`) or a refused binding control
215
+ (`#thinking_binding_unsupported?`) is not one: `ProviderErrors` leaves `reasoning_refused?` false for them, so one
216
+ bad block never turns reasoning off for the model.
212
217
 
213
218
  ### Shared: ToolResponse (per batch) and ToolRunner (per call)
214
219
 
@@ -341,6 +346,10 @@ What the running generation streams, fed by the Engine's stream handler: whether
341
346
  Bridge acks it as `cut`.
342
347
  A generation is cuttable while it has streamed only thinking, is still thinking, and has thought for at least
343
348
  the model's cut wait (`SteerCutAfter.resolve`: `steer_cut_after` per models:/hosts: entry, else `steer.cut_after`) seconds (`cuttable?(min_age)`). It has `started!`, `chunk!`, `retrying!` and `finished!`.
349
+ `snapshot` is the running generation as `GenerationPhase::Snapshot` `{thinking_only, age}` (nil with none running;
350
+ `age`: its thinking's seconds, nil before any): `SteerCut#generation_snapshot` → `Engine#generation_snapshot`, which
351
+ `Bridge#snapshot` merges into `current_turn.generation`, so a web page joining mid-generation can clock its delivery
352
+ line and tell whether "cut in now" can land.
344
353
 
345
354
  ### WaitingSteer
346
355
 
@@ -518,10 +527,20 @@ Both loops:
518
527
  The retry request runs at temperature 0.6 when the configured sampling sets no temperature; a configured one
519
528
  is kept. A length stop with the context at least 90% full is not retried. When the retries run out, the
520
529
  Engine keeps a `TurnNote.empty` note as the turn's end.
530
+ - **A refusal** (chat loop only): a finish reason `refusal` (Messages), or `content_filter` (OpenAI) with no text, is
531
+ an empty answer never retried; text streamed before a refusal is not kept, and tool calls with it are not run. A
532
+ `content_filter` with text keeps it, marked `ChatLoop::FILTERED_MARK`, also never retried. `ModelResult#refusal` is `{category:}` (from the response's
533
+ `stop_details`) or `{}`, and goes on the `TurnNote.empty` marker and `turn_summary.empty_answer` as `refusal`, so
534
+ every UI words the notice `no answer: refused by the provider (cyber)`.
521
535
  - **A cut generation** (`after_cut`): a generation a plugin cut (`stop_generation`) spends the same retry budget
522
536
  with its own nudge; when it is used up, the turn ends cancelled (`:hook`). A steer's cut asks again with the
523
537
  steer and spends nothing.
524
- - **Steering**: queued input goes in at iteration boundaries (`Steer.inject!`, `:pending_input_merged`).
538
+ - **Steering**: queued input goes in at iteration boundaries (`Steer.inject!`, `:pending_input_merged`). A
539
+ worker's drain (`Worker#pending_input_drain`) takes every input file in name order except the ones
540
+ `SessionInbox.waits_for_turn_end?` leaves for a turn of their own (`delivery: queue`, or images): those are
541
+ skipped, not a barrier, so a line sent after them still merges. Only the files it claims reach
542
+ `:input_merged`, the failure hand-back (`@merged_this_turn`) and the checkpoint; the skipped ones run after the
543
+ turn, one turn each, in name order (`Worker#run_input_files`).
525
544
  - **Context status**: an estimate of the window's fill before each request,
526
545
  emitted on every request (`:context_status`) so the UI always has a current
527
546
  number, with a guidance line on the tail only when it rises far enough.