samagotchi 0.66.0 → 0.68.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 (151) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +80 -1
  3. data/docs/architecture.md +55 -30
  4. data/docs/cli.md +3 -1
  5. data/docs/configuration.md +50 -9
  6. data/docs/context.md +35 -1
  7. data/docs/desktop.md +90 -22
  8. data/docs/guardrails.md +8 -5
  9. data/docs/hooks.md +2 -2
  10. data/docs/internals/anthropic.md +156 -0
  11. data/docs/internals/background-tasks.md +1 -1
  12. data/docs/internals/bundles.md +2 -0
  13. data/docs/internals/engine.md +129 -38
  14. data/docs/internals/gemma4-contract.md +1 -1
  15. data/docs/internals/llm-context-bench.md +5 -4
  16. data/docs/internals/prompt-caching.md +26 -11
  17. data/docs/plugins.md +126 -23
  18. data/docs/sessions.md +6 -6
  19. data/docs/sub-agent.md +4 -1
  20. data/docs/testing.md +18 -4
  21. data/lib/samagotchi/bootstrap/probe.rb +4 -3
  22. data/lib/samagotchi/bootstrap_command.rb +13 -3
  23. data/lib/samagotchi/bridge/ring_buffer.rb +0 -5
  24. data/lib/samagotchi/bridge.rb +6 -3
  25. data/lib/samagotchi/bundles/claude-peer/manifest.yml +7 -6
  26. data/lib/samagotchi/bundles/claude-peer/plugin.rb +34 -313
  27. data/lib/samagotchi/bundles/claude-peer/scripts/chi-peer-SKILL.md +8 -3
  28. data/lib/samagotchi/bundles/claude-peer/skill_claude_peer.md +14 -6
  29. data/lib/samagotchi/bundles/codex-peer/manifest.yml +7 -6
  30. data/lib/samagotchi/bundles/codex-peer/plugin.rb +37 -312
  31. data/lib/samagotchi/bundles/codex-peer/scripts/chi-peer-SKILL.md +8 -3
  32. data/lib/samagotchi/bundles/codex-peer/skill_codex_peer.md +14 -6
  33. data/lib/samagotchi/bundles/mcp/manifest.yml +5 -5
  34. data/lib/samagotchi/bundles/mcp/plugin.rb +82 -294
  35. data/lib/samagotchi/bundles/system/config_modification_protocol.md +7 -3
  36. data/lib/samagotchi/bundles/system/manifest.yml +3 -3
  37. data/lib/samagotchi/bundles/system/self_map.md +1 -1
  38. data/lib/samagotchi/config.rb +15 -7
  39. data/lib/samagotchi/context_absorber.rb +66 -13
  40. data/lib/samagotchi/desktop/macos/AgentTerminal.swift +260 -0
  41. data/lib/samagotchi/desktop/macos/App.swift +3 -3
  42. data/lib/samagotchi/desktop/macos/ChiRunner.swift +5 -2
  43. data/lib/samagotchi/desktop/macos/Images.swift +2 -2
  44. data/lib/samagotchi/desktop/macos/Kitty.swift +33 -197
  45. data/lib/samagotchi/desktop/macos/Panel.swift +118 -82
  46. data/lib/samagotchi/desktop/macos/ProcessRunner.swift +1 -1
  47. data/lib/samagotchi/desktop/macos/Tmux.swift +239 -0
  48. data/lib/samagotchi/desktop/macos.rb +84 -13
  49. data/lib/samagotchi/desktop_command.rb +14 -3
  50. data/lib/samagotchi/effective_model.rb +283 -0
  51. data/lib/samagotchi/engine.rb +307 -549
  52. data/lib/samagotchi/engine_peers.rb +3 -0
  53. data/lib/samagotchi/guardrail_wiring.rb +1 -1
  54. data/lib/samagotchi/guardrails/approval.rb +1 -1
  55. data/lib/samagotchi/guardrails/approvals.rb +1 -1
  56. data/lib/samagotchi/guardrails/rules.rb +1 -1
  57. data/lib/samagotchi/guardrails/targets.rb +1 -1
  58. data/lib/samagotchi/hook_routes.rb +151 -0
  59. data/lib/samagotchi/hooks/registry.rb +2 -2
  60. data/lib/samagotchi/host_keys.rb +147 -0
  61. data/lib/samagotchi/level_setting_command.rb +99 -0
  62. data/lib/samagotchi/llm/adapter_common.rb +138 -0
  63. data/lib/samagotchi/llm/anthropic_messages.rb +28 -105
  64. data/lib/samagotchi/llm/api_key.rb +23 -7
  65. data/lib/samagotchi/llm/chat_loop.rb +10 -6
  66. data/lib/samagotchi/llm/openai_chat.rb +9 -92
  67. data/lib/samagotchi/llm/refusal_memo.rb +29 -0
  68. data/lib/samagotchi/llm_context_override.rb +1 -0
  69. data/lib/samagotchi/llm_context_strategy.rb +5 -10
  70. data/lib/samagotchi/peer_sources.rb +58 -0
  71. data/lib/samagotchi/plugin/api.rb +64 -155
  72. data/lib/samagotchi/plugin/loader.rb +4 -2
  73. data/lib/samagotchi/plugin/peer.rb +394 -0
  74. data/lib/samagotchi/plugin/tool_spec.rb +166 -0
  75. data/lib/samagotchi/plugin_tasks.rb +2 -2
  76. data/lib/samagotchi/reasoning_history_command.rb +29 -90
  77. data/lib/samagotchi/row_refs.rb +1 -1
  78. data/lib/samagotchi/self_report.rb +5 -3
  79. data/lib/samagotchi/send_command.rb +21 -11
  80. data/lib/samagotchi/session.rb +22 -8
  81. data/lib/samagotchi/session_continue.rb +58 -15
  82. data/lib/samagotchi/session_inbox.rb +0 -13
  83. data/lib/samagotchi/session_setting.rb +86 -0
  84. data/lib/samagotchi/thinking_command.rb +24 -76
  85. data/lib/samagotchi/thinking_notices.rb +169 -0
  86. data/lib/samagotchi/tool_call_parser.rb +1 -1
  87. data/lib/samagotchi/toolkit_binding.rb +138 -0
  88. data/lib/samagotchi/tools/context_read.rb +14 -0
  89. data/lib/samagotchi/tools/execute.rb +12 -12
  90. data/lib/samagotchi/tools/registry.rb +1 -1
  91. data/lib/samagotchi/tools/task_create.rb +6 -6
  92. data/lib/samagotchi/tools/task_get.rb +13 -13
  93. data/lib/samagotchi/tools/task_list.rb +6 -6
  94. data/lib/samagotchi/tools/task_record.rb +52 -0
  95. data/lib/samagotchi/tools/task_runtime.rb +55 -91
  96. data/lib/samagotchi/tools/task_stop.rb +6 -6
  97. data/lib/samagotchi/tools/task_wait.rb +12 -13
  98. data/lib/samagotchi/tools/tool_index.rb +454 -0
  99. data/lib/samagotchi/update_command.rb +9 -6
  100. data/lib/samagotchi/version.rb +1 -1
  101. data/lib/samagotchi/web/app.rb +71 -93
  102. data/lib/samagotchi/web/capabilities.rb +1 -1
  103. data/lib/samagotchi/web/public/activity.js +2 -2
  104. data/lib/samagotchi/web/public/annotate_actions.js +1 -1
  105. data/lib/samagotchi/web/public/app.js +103 -539
  106. data/lib/samagotchi/web/public/bubbles.js +291 -0
  107. data/lib/samagotchi/web/public/cards_place.js +184 -0
  108. data/lib/samagotchi/web/public/chains.js +1 -1
  109. data/lib/samagotchi/web/public/chip_popover.js +111 -0
  110. data/lib/samagotchi/web/public/command_list.js +12 -10
  111. data/lib/samagotchi/web/public/commands.js +2 -2
  112. data/lib/samagotchi/web/public/composer_size.js +2 -2
  113. data/lib/samagotchi/web/public/copy.js +1 -1
  114. data/lib/samagotchi/web/public/ctx.js +1 -1
  115. data/lib/samagotchi/web/public/delivery_line.js +4 -4
  116. data/lib/samagotchi/web/public/families_view.js +1 -1
  117. data/lib/samagotchi/web/public/format.js +11 -7
  118. data/lib/samagotchi/web/public/images.js +25 -2
  119. data/lib/samagotchi/web/public/index.html +15 -7
  120. data/lib/samagotchi/web/public/info_bar.js +3 -3
  121. data/lib/samagotchi/web/public/info_bar_model.js +110 -0
  122. data/lib/samagotchi/web/public/init_row.js +21 -10
  123. data/lib/samagotchi/web/public/level_chip.js +29 -80
  124. data/lib/samagotchi/web/public/lists_view.js +64 -9
  125. data/lib/samagotchi/web/public/llm_context_chip.js +38 -107
  126. data/lib/samagotchi/web/public/model_picker.js +26 -22
  127. data/lib/samagotchi/web/public/notify.js +6 -4
  128. data/lib/samagotchi/web/public/question_card.js +1 -1
  129. data/lib/samagotchi/web/public/ref_scan.js +2 -8
  130. data/lib/samagotchi/web/public/refs.js +13 -0
  131. data/lib/samagotchi/web/public/session_state.js +14 -0
  132. data/lib/samagotchi/web/public/session_title.js +2 -3
  133. data/lib/samagotchi/web/public/sessions_list.js +0 -8
  134. data/lib/samagotchi/web/public/stage_frame.js +179 -0
  135. data/lib/samagotchi/web/public/stage_model.js +35 -2
  136. data/lib/samagotchi/web/public/stage_slots.js +130 -0
  137. data/lib/samagotchi/web/public/stage_view.js +56 -335
  138. data/lib/samagotchi/web/public/stage_watch.js +65 -0
  139. data/lib/samagotchi/web/public/strip.js +3 -3
  140. data/lib/samagotchi/web/public/task_stop.js +3 -2
  141. data/lib/samagotchi/web/public/turn_events.js +1 -1
  142. data/lib/samagotchi/web/public/turn_html.js +2 -2
  143. data/lib/samagotchi/web/public/turn_model.js +2 -2
  144. data/lib/samagotchi/web/public/turn_view.js +1 -1
  145. data/lib/samagotchi/web/public/vendor/directive-helpers.js +8 -0
  146. data/lib/samagotchi/web/public/vendor/directives/repeat.js +9 -0
  147. data/lib/samagotchi/web/server.rb +0 -4
  148. data/lib/samagotchi/worker.rb +22 -24
  149. data/lib/samagotchi/worker_inbound.rb +36 -4
  150. data/lib/samagotchi/worker_wakes.rb +67 -7
  151. metadata +27 -1
@@ -34,7 +34,8 @@ Engine#run_turn(session, prompt, on_event:, max_iterations:, cancel_controller:,
34
34
  │ # resumed: true (only on a step-limit Continue: resumed: given),
35
35
  │ # images (only when there are any), cut_after (the model's cut wait, s)}
36
36
  │ # refresh_profile! (may ask the server's /props once)
37
- │ # turn.settings = LLM::TurnSettings (vision, sampling, thinking; model_name is set in generate)
37
+ │ # turn.settings = LLM::TurnSettings (vision, sampling, thinking; model_name is set in generate),
38
+ │ # from one EffectiveModel; ThinkingNotices#turn_started keeps the level and says one the host can't do
38
39
  │ # announces guardrail load failures, starts and awaits plugin init tasks, applies staged tools,
39
40
  │ # then the session's toolkit (apply_toolkit!: Registry#toolkit, a prompt rebuild when it changed)
40
41
  │ # fires the :session_start hook on the Engine's first turn, then the :before_turn hook
@@ -43,11 +44,12 @@ Engine#run_turn(session, prompt, on_event:, max_iterations:, cancel_controller:,
43
44
  │ # Returns false when a Stop came during the probes or the init wait, else true
44
45
  │
45
46
  ├── generate(turn, max_iterations:, max_tool_output_chars:, pending_input:)
46
- │ # sync_kernel_client!, then @kernel.turn_settings = turn.settings with model_name, window_setting,
47
- │ # llm_context (turn_llm_context: the session's, the model's/host's, else llm_context.strategy)
48
- │ # and price (turn_price: hosts.<name>.models.<id>.price), all read per turn
47
+ │ # one EffectiveModel (after the hooks, which may switch the model): sync_kernel_client!, then
48
+ │ # @kernel.turn_settings = turn.settings with model_name, window_setting, llm_context (the
49
+ │ # session's, the model's/host's, else llm_context.strategy) and price
50
+ │ # (hosts.<name>.models.<id>.price), all read per turn
49
51
  │ # turn.limit = max_iterations or IterationLimit.for
50
- │ # backend.complete(messages: turn.messages, ..., pending_input: turn_drain(pending_input))
52
+ │ # backend(model).complete(messages: turn.messages, ..., pending_input: turn_drain(pending_input))
51
53
  │ # Returns LLM::ModelResult (text, conversation, canceled?, exhausted?, tool_activity,
52
54
  │ # context_status, empty_steps, empty_retries)
53
55
  │
@@ -72,7 +74,7 @@ Engine#run_turn(session, prompt, on_event:, max_iterations:, cancel_controller:,
72
74
  │
73
75
  └── ensure: release_turn(probe_cancel_before)
74
76
  # TurnState#finish! (turn flag off, controller, sink and steers cleared)
75
- # GenerationPhase#finished!, WaitingSteer#clear!
77
+ # SteerCut#finished! (its GenerationPhase finished, its WaitingSteer cleared)
76
78
  # probe cancel swapped back
77
79
  # the context window cache dropped, so the next turn asks the server again
78
80
  # steers left logged as dropped, activity recorded, turn-scoped hooks cleared
@@ -83,7 +85,7 @@ Engine#run_turn(session, prompt, on_event:, max_iterations:, cancel_controller:,
83
85
  ```
84
86
  StandardError in run_turn:
85
87
  → if turn.ended (it came from the after_turn/session_end hooks): logged as post_turn_error, re-raised
86
- → else: end_turn(turn, "failed", save: true)
88
+ → else: end_turn(turn, "failed", save: true) (a save that fails there is logged, turn_end_save_failed)
87
89
  # Both loops call LLM::FailedTurn.attach(e, conversation) before re-raising, so
88
90
  # failed_messages keeps the loop's partial conversation (tool iterations included),
89
91
  # else turn.messages, with TurnNote.failed(summary, continued: turn.continue) at the tail.
@@ -118,6 +120,16 @@ A worker's TERM, HUP or INT (WorkerSignals, trapped by a spawned worker only):
118
120
  loop left brings only that hard exit, and run_session_loop restores the default handlers
119
121
  → run_session_loop: exit 0, no discard; input queued behind the stopped turn wakes a new worker
120
122
 
123
+ The effective model doesn't resolve (a broken host config, HostRegistry#resolve raising):
124
+ → the rescued readers give their fallback (Engine#model_setting) and log effective_model_unresolved;
125
+ a setting that fails on a resolved model gives its fallback and logs <setting>_unresolved
126
+ (sampling, thinking, reasoning_history, steer_cut_after, llm_context, window, price, served,
127
+ vision_adapter); served_probe_failed when asking the server for its served model fails;
128
+ alias_note_failed, thinking_notice_failed likewise
129
+ → WARN once per process for an (event, model, error class), DEBUG after (EffectiveModel::Unresolved)
130
+ → session_state_snapshot never raises (its model fields are nil); a turn, stats_snapshot,
131
+ chat_model? and system_prompt raise, as before
132
+
121
133
  Stopped before the model was asked (prepare_turn returned false):
122
134
  → stopped_before_model(turn) → end_turn(turn, "canceled")
123
135
  → nothing kept, :turn_canceled {cancellation_reason, cancelled_by, duration_ms}
@@ -129,13 +141,14 @@ Stopped before the model was asked (prepare_turn returned false):
129
141
  The Engine does not talk to models directly. `Engine#backend` picks the loop the effective model's host speaks:
130
142
 
131
143
  ```
132
- Engine#backend → backend_for(@host_registry.resolve(@effective_model_name))
144
+ Engine#backend → backend_for(effective_model) # EffectiveModel.of(@host_registry, @model_choice)
133
145
  → host entry chat? (an api in LLM::Adapters::BY_API: openai, anthropic): LLM::ChatLoop (one per Engine, built on first use)
134
146
  → else: LLM::NativeBackend wrapping the Engine's KernelLoop
135
147
  ```
136
148
 
137
149
  Both implement the `LLM::ModelBackend` contract (`#complete(messages:, max_iterations:, on_stream_event:,
138
- cancel_controller:, model_name:, max_tool_output_chars:, pending_input:)`). `NativeBackend#complete` only calls
150
+ cancel_controller:, model_name:, max_tool_output_chars:, pending_input:, reasoning_since:)`; `reasoning_since:` is
151
+ where the run a step-limit Continue resumes started, the Engine's `resume_from`). `NativeBackend#complete` only calls
139
152
  `KernelLoop#run` with the same arguments. `ChatLoop` runs its own loop but is built with the same KernelLoop,
140
153
  whose tools, hooks, gate and dispatch it uses.
141
154
 
@@ -172,17 +185,21 @@ KernelLoop#run(messages, max_iterations:, on_stream_event:, cancel_controller:,
172
185
  `turn.conversation` is the loop's own copy, not the Engine's `turn.messages`. A failure attaches it to the error
173
186
  (`LLM::FailedTurn.attach`), so `Engine#failed_messages` can keep it; a Ctrl-C does not (see Error paths).
174
187
 
175
- ### ChatLoop (the OpenAI chat API)
188
+ ### ChatLoop (the chat APIs)
176
189
 
177
- Runs hosts with `api: openai`: remote providers such as OpenRouter (Claude and others) and any OpenAI-compatible
178
- chat server. The model gets the tools as JSON schemas and returns structured tool calls.
190
+ Runs hosts whose api has a chat adapter (`LLM::Adapters::BY_API`): `api: openai` on `LLM::OpenAIChat` (remote
191
+ providers such as OpenRouter, Claude among others, and any OpenAI-compatible chat server) and `api: anthropic` on
192
+ `LLM::AnthropicMessages` (Anthropic's Messages API; [anthropic.md](anthropic.md)). The Engine sets the adapter per
193
+ turn for the effective model's host (`HostRegistry#adapter_for`). The model gets the tools as JSON schemas and returns
194
+ structured tool calls.
179
195
 
180
196
  ```
181
197
  ChatLoop#complete(messages:, max_iterations:, on_stream_event:, cancel_controller:,
182
- model_name:, max_tool_output_chars:, pending_input:)
198
+ model_name:, max_tool_output_chars:, pending_input:, reasoning_since:)
183
199
  │
184
200
  │ conversation = Array(messages).map(&:dup) # a shallow copy of each message
185
- │ Run.new(self, conversation, on_stream_event, cancel_controller, model_name, pending_input)
201
+ │ Run.new(self, conversation, on_stream_event, cancel_controller, model_name, pending_input,
202
+ │ start: reasoning_since)
186
203
  │ .call(max_iterations: max_iterations || 1, cap: KernelLoop.resolve_output_char_cap(max_tool_output_chars))
187
204
  │
188
205
  └──→ LLM::ModelResult(text, conversation, exhausted, canceled, tool_activity, ...)
@@ -191,10 +208,13 @@ ChatLoop#complete(messages:, max_iterations:, on_stream_event:, cancel_controlle
191
208
  The differences from the native loop:
192
209
 
193
210
  - The tools go in each request's `tools` field (`tool_definitions`), not in the system prompt.
194
- - The conversation is converted to the OpenAI wire format (`wire_messages`).
211
+ - The conversation is converted to the OpenAI wire format (`wire_messages`), for every chat host;
212
+ `AnthropicMessages` translates that again into Messages' `system` and content blocks (`LLM::AnthropicWire`).
195
213
  - The first system message carries `cache_split:` (from `SystemPrompt#stable_length`, which the Engine passes in
196
- as `stable_length:`), where the prompt's stable part ends. For remote Claude, `LLM::PromptCache` puts the
197
- first cache breakpoint there; see prompt-caching.md.
214
+ as `stable_length:`), where the prompt's stable part ends. The adapter puts the first cache breakpoint there:
215
+ `OpenAIChat` for a Claude model on a remote host (`LLM::PromptCache.mark`), `AnthropicMessages` on every chat
216
+ request unless its dialect has no breakpoints or the host refused `cache_control` (`AnthropicWire.translate`,
217
+ `Dialect#cache_breakpoints`); see prompt-caching.md and anthropic.md.
198
218
  - Each call's result is its own tool message, paired by `tool_call_id`.
199
219
  - A model turn's saved reasoning (`thinking:`) goes back as `reasoning_content` per the turn's
200
220
  `TurnSettings#reasoning_history` (`ReasoningHistory.resolve`, the session's own level first:
@@ -208,8 +228,15 @@ The differences from the native loop:
208
228
  in `#resumed_run_start`), past a user entry added after that run's last reply (a `!cmd`'s output). Without
209
229
  `since:` (`#reasoning_chars` from outside a Run) the boundary is the last query (a `role: "user"` or `turn_start`
210
230
  entry). `Run#prompt_chars` counts it (`#reasoning_chars`).
231
+ - On an adapter that sends its blocks back (`#replays_content?`: `AnthropicMessages`), a model turn's saved blocks
232
+ (`anthropic_content`, the response's `provider_content`, saved by `Run#with_thinking`) go back verbatim, thinking
233
+ blocks with their signatures (`#replay_blocks`): not for an interrupted entry or one with an unpaired call, and
234
+ without the thinking blocks before the reasoning boundary (the last model entry with tool calls keeps them).
235
+ `#reasoning_history` floors `drop` at `turn` there, since Messages needs the running run's thinking blocks.
211
236
  - A 400 about reasoning (`BadRequest#reasoning_refused?`) for a request that sent reasoning back asks the step again
212
- without it; when that goes through, `#reasoning_history_refused!` marks the model (worker lifetime) and
237
+ without it (not on a replaying adapter, where the body would be the same); when that goes through,
238
+ `#reasoning_history_refused!` marks the model (worker lifetime; the loop's `LLM::RefusalMemo`, as for the thinking
239
+ and sampling refusals) and
213
240
  `:reasoning_history_refused` goes out. When it is refused too, the thinking-fields fallback (`#thinking_refused!`,
214
241
  `:thinking_refused`) runs on it, still without reasoning. Up to three requests per step. A 400 about a thinking
215
242
  block's signature (`BadRequest#thinking_signature?`) or a refused binding control
@@ -256,7 +283,7 @@ One iteration with tool calls:
256
283
  `EVENT_FIELDS`) copy them by these lists, so a new row field is added there. `ref` is `FileRef.for` (a
257
284
  read/write/edit's file against the cwd, `{kind: "file", path, line?, end_line?}`); the web opens it in the editor
258
285
  that `Web::Editor` (web.editor) names, for a local viewer only (`App#viewer_capabilities`).
259
- `refs` is a plugin tool's row refs: its registry entry's `refs` (chi.tool's `refs:` lambda, `Api.per_call`), run
286
+ `refs` is a plugin tool's row refs: its registry entry's `refs` (chi.tool's `refs:` lambda, `ToolSpec#per_call`), run
260
287
  once per call by `RowRefs.for` (never raises: a raise or a non-list gives none, `tool_refs_failed`; each entry a
261
288
  `Ref`, a bad one dropped, `tool_refs_rejected`; a relative file resolved against the session's folder
262
289
  (`KernelLoop#session_cwd`, else the process's), one outside it (its spelling or its real path) dropped, a kept one
@@ -311,13 +338,46 @@ TurnState
311
338
 
312
339
  The lock is a leaf: nothing is called out while it is held.
313
340
 
341
+ ### EffectiveModel (a snapshot per read)
342
+
343
+ `EffectiveModel.of(registry, choice)` resolves the model once (`HostRegistry#resolve`: host, client, the id
344
+ sent) with its lookup names, and reads each setting from them (sampling, thinking, reasoning history, steer cut,
345
+ LLM context, window, price, served model, vision, profile). It is built per read and never kept: config.yml is
346
+ read live, a model listing can move a bare id to another host, and another process's saved model lists change
347
+ what a resolve gives, all without a `/model`. Frozen, so any thread may hold one.
348
+
349
+ - The Engine's readers take one (`Engine#effective_model`); those whose fallback always covered the resolve go
350
+ through `#model_setting`. A turn takes one in `turn_settings` (before the hooks) and one in `generate` (after
351
+ them); `system_prompt`, `steer_cut_after` (at `:turn_started`) and the turn-end warm-up take their own;
352
+ `stats_snapshot` and `session_state_snapshot` take one each (the latter nil-tolerant).
353
+ - Never resolved (they hold on a broken resolve): `effective_model_ref`, `bare_model_name`,
354
+ `guardrail_model_name`, `sync_model_key!`, `served_model(probe: false)` (`HostRegistry#model_ref`/`#bare_name`).
355
+ - Readers on other threads: the Bridge (`/state`, `/stats`, the SSE reset, a steer's cut wait), the worker's
356
+ command thread, the recap thread and plugin threads (`session_model_recap_target`), the REPL.
357
+ - `ModelChoice` (`@model_choice`) holds the name and the typed name as one value, so a reader on another thread
358
+ never pairs one model's name with another's typed name.
359
+ - `EffectiveModel.of_session` builds a saved session's (no worker): `Web::App`, `LLMContextStrategy.for_session`.
360
+
361
+ `SessionSetting` names the four per-session settings (llm_context, thinking, reasoning_history, toolkit): the
362
+ session's own value, the commands that move each chip, its chip from an EffectiveModel or from the Engine's
363
+ readers, a saved session's chip, and how the web takes a worker snapshot's. The session state, `/stats` (no
364
+ toolkit), a `:command_ran` and the web's session JSON list them from it.
365
+
366
+ ### ThinkingNotices
367
+
368
+ What a turn's thinking level leads to, said once per session and host (`thinking_notices.rb`): an effort the
369
+ host can't do, off the model didn't honour, a host refusing the thinking fields or the reasoning sent back. It
370
+ keeps the turn's `[level, ModelTarget]` and whether the generation thought; `turn_settings` and the stream handler
371
+ call it. Turn thread only.
372
+
314
373
  ### Turn (Engine::Turn)
315
374
 
316
375
  The turn as the Engine sees it (the KernelLoop has its own `Turn` struct):
317
376
 
318
377
  ```ruby
319
378
  Turn = Struct.new(:session, :prompt, :continue, :on_event, :controller, :origin, :started_at, :messages, :ended,
320
- :settings, :id, :limit) do
379
+ :settings, :id, :limit, :resumed, :resume_from) do
380
+ def resumed? = resumed ? true : false # a step-limit Continue
321
381
  def tag(event) = origin ? event.merge(origin: origin) : event # boundary events carry the origin
322
382
  def elapsed = ... # seconds since started_at
323
383
  end
@@ -342,10 +402,14 @@ end
342
402
  running turn's record so far as a killed turn's (canceled, `worker_stopped`), which the next worker loads; the
343
403
  turn's end writes over it.
344
404
 
345
- ### GenerationPhase
405
+ ### SteerCut, GenerationPhase and WaitingSteer
406
+
407
+ The Engine holds one `SteerCut` (`@steer_cut`), which owns a `GenerationPhase` and a `WaitingSteer`: the Bridge and
408
+ the TUI ask it (`Engine#cut_for_steer`, `#input_epoch`, `#generation_snapshot`), the Engine's stream handler feeds it
409
+ (`SteerCut#observe`), a drain that took input calls `#delivered!`, and `release_turn` calls `#finished!`.
346
410
 
347
- What the running generation streams, fed by the Engine's stream handler: whether a steer sent as a cut (delivery
348
- `cut`: `/cut TEXT`, `chi send --cut`) may cut it. `SteerCut#cut_for_steer` answers `:now`, `:waits` or `:off`; the
411
+ `GenerationPhase` is what the running generation streams, fed through `SteerCut#observe`: whether a steer sent as a
412
+ cut (delivery `cut`: `/cut TEXT`, `chi send --cut`) may cut it. `SteerCut#cut_for_steer` answers `:now`, `:waits` or `:off`; the
349
413
  Bridge acks it as `cut`.
350
414
  A generation is cuttable while it has streamed only thinking, is still thinking, and has thought for at least
351
415
  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!`.
@@ -357,8 +421,8 @@ line and tell whether "cut in now" can land.
357
421
  ### WaitingSteer
358
422
 
359
423
  A message that may cut the generation but came too early (the generation hadn't been thinking long enough). It
360
- waits until the thinking passes the model's cut wait (the Engine checks on each thinking chunk), until a drain hands
361
- the message to the model at a boundary, or until the turn ends (`clear!` in `release_turn`).
424
+ waits until the thinking passes the model's cut wait (`SteerCut#observe` rechecks it on each thinking chunk), until a
425
+ drain hands the message to the model at a boundary, or until the turn ends (`SteerCut#finished!` clears it).
362
426
 
363
427
  ## Events
364
428
 
@@ -414,22 +478,28 @@ hook registry (`@hooks.fire`), not on the sink or the observers. The other hook
414
478
 
415
479
  ```
416
480
  Engine initialization (in this order, among other things):
417
- @turn_state, @generation_phase, @waiting_steer
418
- @host_registry, @effective_model_name; @client = @host_registry.resolve(...).client
481
+ @turn_state, @steer_cut (SteerCut: its GenerationPhase and WaitingSteer)
482
+ @host_registry, @model_choice (ModelChoice: the name and the name as typed); @client = effective_model.client
419
483
  @given_profile = profile or nil (resolved on first need, so building an Engine makes no network call)
484
+ @hook_routes (HookRoutes: notices, hook questions, stop_turn/stop_generation, steer attribution)
420
485
  @guardrail_wiring, @question_desk
486
+ @toolkit_binding (ToolkitBinding: the kits, the plugins' among them, and the turn-start apply)
421
487
  @extension_load = ExtensionLoad.new; @hooks = @extension_load.hooks (config.yml's, then the bundles')
422
- @tools = Tools::Builtins.registry; the bundles' plugins load (load_plugins → ExtensionLoad#load_plugins)
488
+ @tools = Tools::Builtins.registry; the bundles' plugins load (load_plugins → ExtensionLoad#load_plugins),
489
+ then @tool_index.declare(@tools): find_tools + call_tool (source "index", not core) when a plugin
490
+ registered a chi.tool_source (Tools::ToolIndex: the index's groups, search, call checks; the source runs
491
+ the call with its own plugin Context)
423
492
  wire_kernel: @kernel = KernelLoop.new(client:, profile:, hooks:, reminder_store:, tools:)
424
493
  @kernel.guardrail_gate = @guardrail_wiring.gate; @kernel.warmup = PromptWarmup.new
425
- @hooks.runtime = hook_runtime
494
+ @hooks.runtime = @hook_routes.runtime (HookRoutes, built first: what a hook or plugin does reaches the UIs)
426
495
  @native_backend = LLM::NativeBackend.new(kernel: @kernel)
427
496
  @prompt_builder = SystemPrompt.new(...)
428
497
  @session_observer with the metrics, the log subscriber and the thinking tails subscribed (subscribe_session_observers)
429
498
 
430
499
  Model switching, Engine#switch_model!(model_name, persist_default:, typed:):
431
- → checks the host (ModelProfile.check_host!), sets @effective_model_name
432
- → drops the given profile and the profile resolution (resolved again on first need)
500
+ → checks the host (ModelProfile.check_host!), sets @model_choice (name and typed name in one assignment)
501
+ → drops the given profile and the profile resolution (resolved again on first need; one that another
502
+ thread finishes for the old model is neither kept nor applied: #store_profile)
433
503
  → sync_model_key!, @prompt_builder.reset! (the system prompt is built again)
434
504
  → sync_kernel_client! (the kernel talks to the new host), the context window cache dropped
435
505
  → the kernel is never rebuilt; the next turn's backend follows the new host's api
@@ -444,7 +514,7 @@ Model probes (per turn):
444
514
 
445
515
  Plugins and hook files register hooks on hook points (see "Hook points" above, and docs/hooks.md). Every fire
446
516
  puts the hook runtime on the event: `event[:hook]` (the hook's label) and five helpers, which call the Engine's
447
- `Hooks::Runtime`. A plugin's `ctx` has the same ones (`ctx.notify`, `ctx.steer`, ...).
517
+ `Hooks::Runtime` (built by `HookRoutes`, `hook_routes.rb`, which routes each to the UIs). A plugin's `ctx` has the same ones (`ctx.notify`, `ctx.steer`, ...).
448
518
 
449
519
  | Helper | What it does | Used by |
450
520
  |---|---|---|
@@ -551,8 +621,10 @@ Both loops:
551
621
  - **The same events**: generation events, tool dispatch and tool call events, and the before/after generation
552
622
  and tool call hooks.
553
623
 
554
- Native loop only: the Qwen open-block recovery, and the thinking prefill (Qwen with thinking off gets an empty
555
- thought after the cue, kept in the model message so the next prompt starts with what the server has cached).
624
+ Native loop only: the Qwen open-block recovery, and the thinking prefill (`Thinking.native`: with thinking off, Qwen
625
+ gets an empty thought after the cue, `QWEN_EMPTY_THOUGHT`, and Gemma 4 its empty thought channel,
626
+ `GEMMA_EMPTY_THOUGHT`, with no `<|think|>` token before the system prompt; kept in the model message so the next
627
+ prompt starts with what the server has cached).
556
628
 
557
629
  ## Key design principles
558
630
 
@@ -566,8 +638,9 @@ thought after the cue, kept in the model message so the next prompt starts with
566
638
  persistent.
567
639
  5. **A stable system prompt for the cache.** `SystemPrompt#build` caches the prompt per Engine, keyed by
568
640
  `[chat, thinking, layers]` (chat/native, the thinking level, the turn's LLM context layers), and is reset
569
- only on a model switch, a tools change or a profile change. The stable part (base prompt, identity,
570
- memories, AGENTS.md, memory indexes) comes before the volatile tail (model, working directory, session).
641
+ only on a model switch, a tools change, a profile change or a toolkit change (`Engine#apply_toolkit!`). The
642
+ stable part (base prompt, the toolkit's not-loaded line, rg guidance, identity, model notes, preloaded memories,
643
+ AGENTS.md, appended files, memory indexes) comes before the volatile tail (model, working directory, session).
571
644
  prompt-caching.md covers this in detail.
572
645
 
573
646
  ## Key files
@@ -576,12 +649,26 @@ Paths are under `lib/samagotchi/`.
576
649
 
577
650
  | File | Role |
578
651
  |---|---|
579
- | `engine.rb` | Engine: turn lifecycle, event emission, hooks runtime, plugins, model switching |
652
+ | `engine.rb` | Engine: turn lifecycle, event emission, plugins, model switching |
580
653
  | `turn_state.rb` | Cross-thread turn state (running, controller, sink, steers, activity clock) |
654
+ | `steer_cut.rb` | `SteerCut`: the Engine's seam for a message cutting the generation |
581
655
  | `generation_phase.rb`, `waiting_steer.rb` | Whether a steer may cut the running generation, and the steer that waits to |
656
+ | `effective_model.rb` | `EffectiveModel` (the effective model's settings, a snapshot per read; `.of_session`), `ModelChoice`, the fallbacks' log lines (`Unresolved`) |
657
+ | `session_setting.rb` | `SessionSetting`: the per-session settings' descriptors (own value, chips, the web's fallback) |
658
+ | `level_setting_command.rb`, `thinking_command.rb`, `reasoning_history_command.rb` | `/thinking` and `/reasoning-history` (`LevelSettingCommand` and its two Specs) |
659
+ | `thinking_notices.rb` | `ThinkingNotices`: the notices a turn's thinking level leads to |
660
+ | `steer_cut_after.rb`, `cut_policy.rb` | The model's cut wait (`SteerCutAfter.resolve`); the loops' cut path (`CutPolicy`) |
582
661
  | `steer.rb` | Queued input and steers at iteration boundaries |
583
662
  | `kernel_loop.rb` | Native loop: `#run` (format, generate, parse, dispatch, iterate) |
584
- | `llm/chat_loop.rb` | Chat loop: `#complete` (tool schemas per request, wire conversion) |
663
+ | `llm/chat_loop.rb` | Chat loop: `#complete` (tool schemas per request, wire conversion, block replay) |
664
+ | `llm/adapters.rb` | `LLM::Adapters::BY_API`: which chat adapter a host's api gets |
665
+ | `llm/openai_chat.rb` | `LLM::OpenAIChat`, the `/v1/chat/completions` adapter |
666
+ | `llm/adapter_common.rb` | `LLM::AdapterCommon`: what both chat adapters share (the HTTP layer and its timeouts, the paged model list, the SSE line loop, JSON errors) |
667
+ | `llm/anthropic_messages.rb`, `llm/anthropic_wire.rb`, `llm/anthropic_assembly.rb`, `llm/dialect.rb` | The `/v1/messages` adapter, its wire translation, its response, its dialects ([anthropic.md](anthropic.md)) |
668
+ | `llm/prompt_cache.rb` | Cache breakpoints on the OpenAI path, and the `cache_control` both adapters send |
669
+ | `llm/turn_settings.rb` | `LLM::TurnSettings`, what a turn runs under (vision, sampling, thinking, reasoning history, …) |
670
+ | `llm/native_tool_normalizer.rb`, `llm/stream_drops.rb` | A chat adapter's tool calls as chi's call hashes (`Tools::BuiltinCalls`); asking again for a step whose stream dropped (both loops) |
671
+ | `reasoning_history.rb` | `ReasoningHistory`: the levels, `.resolve`, `.run_start` |
585
672
  | `llm/backend.rb` | The `LLM::ModelBackend` contract (`#complete(...)`) |
586
673
  | `llm/native_backend.rb` | Wraps KernelLoop as a backend (`#complete` calls `#run`) |
587
674
  | `llm/model_result.rb` | `LLM::ModelResult`, what a backend returns |
@@ -591,7 +678,11 @@ Paths are under `lib/samagotchi/`.
591
678
  | `prompt_warmup.rb` | The turn-end warm-up |
592
679
  | `tools/registry.rb` | The tools a session offers: schemas, handlers, order |
593
680
  | `system_prompt.rb` | System prompt construction (stable part, volatile tail) |
681
+ | `prompt_files.rb`, `model_notes.rb` | The AGENTS.md chain and appended files; the model notes the prompt loads |
682
+ | `toolkit.rb`, `toolkit_command.rb` | A session's toolkit (`Toolkit#resolve`, what `Registry` declares); `/toolkit` |
683
+ | `toolkit_binding.rb` | `ToolkitBinding`: the Engine's toolkit seam (the kits, `#explained`, `ctx.use_toolkit`, the turn-start `#apply!` behind `Engine#apply_toolkit!`) |
594
684
  | `hooks/registry.rb` | Hook registry, fire order, `Hooks::Runtime` |
685
+ | `hook_routes.rb` | `HookRoutes`: the Engine's routes for a hook's or plugin's notice (`:hook_notice`, `fallback_for:`), question, stop and steer, and the guardrails' after-load line |
595
686
  | `extension_load.rb` | What an Engine loads at start: config.yml and bundle hooks, plugins, `bundles:` settings; events held while plugins load |
596
687
  | `guardrail_wiring.rb`, `guardrails/gate.rb` | The gate's context and approvals; the verdict per call |
597
688
  | `events.rb` | Event type sets shared by the Worker, the Bridge, the TUI and the web |
@@ -14,7 +14,7 @@ XML tool tags and declaration-echo parsing are intentionally not supported.
14
14
 
15
15
  Thought handling follows the Gemma guidance:
16
16
 
17
- - Include `<|think|>` in the system instruction to activate thinking mode.
17
+ - Include `<|think|>` in the system instruction to activate thinking mode. With thinking off (`/thinking off`) chi leaves the token out and prefills Gemma's empty thought channel after the model cue (`Thinking::GEMMA_EMPTY_THOUGHT`, `Thinking.native`, `Prompt.prefill_for`), as Gemma's chat template does with `enable_thinking` false.
18
18
  - When thinking mode is active, the model may emit internal reasoning as `<|channel>thought ... <channel|>`.
19
19
  - Standard multi-turn: prior model thoughts are stripped from conversation history before the next turn.
20
20
  - Function/tool-calling exception: during a single turn that includes tool calls, thoughts are not stripped between those tool-call rounds.
@@ -75,9 +75,9 @@ The proxies are the context-edit spike's (lexical and lenient: a basename mentio
75
75
  stale's stubs, every output led by its `[#tN]` id (`LLMContextView`), the `[CONTEXT: …]` line chi's `ContextStatus`
76
76
  adds at a turn's start under `--budget N` (default 64000; no line below its guided buckets, so a small case may get
77
77
  none), and the chat messages and tools of chi's `ChatLoop`, `forget_outputs` with its real description. The model
78
- (`--forget-model MODEL`, a chi model ref on an `api: openai` host) answers once; a `forget_outputs` call among its
79
- calls runs through chi's `LLMContextForget` (refusals, `protect_steps`, `keep`), and the row scores stale's stubs
80
- plus the forgets. Each answer is saved in `--out DIR` (`<case>.forget_offer.json`) and read back, so a rerun asks
78
+ (`--forget-model MODEL`, a chi model ref on a chat host: `api: openai` or `api: anthropic`) answers once; a
79
+ `forget_outputs` call among its calls runs through chi's `LLMContextForget` (refusals, `protect_steps`, `keep`),
80
+ and the row scores stale's stubs plus the forgets. Each answer is saved in `--out DIR` (`<case>.forget_offer.json`) and read back, so a rerun asks
81
81
  only for the rest (an error isn't saved); `--out` alone scores saved answers; `--max-cost USD` stops the asking
82
82
  once the costs the server reports reach it (exit 1, the answers so far kept). The row's notes give the call rate
83
83
  (`called`, `no_call`, `error`, `unasked`), the ids chi refused and the notes' mean length. Ids in this row cost
@@ -96,7 +96,8 @@ ruby script/llm_context_bench.rb "$SESSIONS" --picks deepseek=picks/deepseek --p
96
96
  ## Live picks (the D7 tool-name A/B)
97
97
 
98
98
  The one part that calls a model, and only with `--live MODEL`: at each case it asks the model which of its tool
99
- outputs to forget, through chi's own chat client (the model must be on an `api: openai` host), and saves each
99
+ outputs to forget, through chi's own chat client (the model must be on a chat host, `api: openai` or `api: anthropic`:
100
+ `LivePick.chat_adapter` takes any `HostEntry#chat?` host), and saves each
100
101
  answer under `--out DIR` for `--picks` to score. A saved pick is never asked for again. The request is the
101
102
  session's conversation up to the turn's end (thinking dropped, every tool result led by its id `[#tN]`), chi's
102
103
  built-in chat tools plus the forget tool, and the policy's tail line; a model that doesn't call the tool is asked
@@ -3,8 +3,9 @@
3
3
  A model server reuses work only for the **exact token prefix** it has already seen. Change one token and
4
4
  everything after it is computed again (prefill). The local models we run (hybrid Qwen 3.6 / Ornith on llama.cpp
5
5
  and Splash) can only resume from a **restore point near the end** of an earlier prompt, not from any shared
6
- prefix. Anthropic (Claude via OpenRouter) caches only up to an explicit `cache_control` breakpoint. So the order
7
- of what chi sends decides what a new turn, or a new session, has to pay for.
6
+ prefix. Anthropic (Claude via OpenRouter, or over the Messages API, `api: anthropic`) caches only up to an explicit
7
+ `cache_control` breakpoint. So the order of what chi sends decides what a new turn, or a new session, has to pay
8
+ for.
8
9
 
9
10
  Measured with prompt-cache slice 1 (2026-10-04, fresh sessions, ~7.7k-token prompt):
10
11
 
@@ -23,6 +24,8 @@ right-hand column happens.
23
24
  shared by every session ────────────────────────────────────────────────── breaks it
24
25
  ┌──────────────────────────────────────────────────────────────────────────┐
25
26
  │ tools built-ins, then plugin tools sorted by (bundle, name), │ a tool added/changed,
27
+ │ the tool index's find_tools/call_tool as bundle "index" │
28
+ │ (declared once at load; publishes change only results) │
26
29
  │ those the session's toolkit declares (per toolkit) │ /model, a profile change,
27
30
  │ (chat: the `tools` field; native: the model's template) │ a toolkit change
28
31
  ├──────────────────────────────────────────────────────────────────────────┤
@@ -36,7 +39,7 @@ right-hand column happens.
36
39
  │ AGENTS.md (root → cwd) │ (picked up by a new
37
40
  │ appended files (append_system_prompt:, --append-system-prompt) │ process or a rebuild)
38
41
  │ project + system memory indexes │
39
- │ ◄── remote Claude: cache_control breakpoint #1 │
42
+ │ ◄── remote Claude / Messages: cache_control breakpoint #1 │
40
43
  ├──────────────────────────────────────────────────────────────────────────┤
41
44
  │ SYSTEM, volatile tail (per session, least → most changing) │
42
45
  │ Model: this session runs on … │ another session
@@ -48,17 +51,19 @@ right-hand column happens.
48
51
  ┌──────────────────────────────────────────────────────────────────────────┐
49
52
  │ history user / assistant / tool messages, APPEND-ONLY │ !rollback, a failed or
50
53
  │ (images: up to the newest N, cut in batches; ImagePlan) │ cancelled turn's tail,
51
- │ ◄── remote Claude: breakpoint #2 on the last │ the image cut moving
52
- │ non-system message │ a batch, history
54
+ │ ◄── remote Claude / Messages: breakpoint #2 on the last │ the image cut moving
55
+ │ non-system message (Messages: its last user block) │ a batch, history
53
56
  ├──────────────────────────────────────────────────────────────────────────┤ sanitizing (thinking
54
57
  │ tail notes reminders, turn notes, steers (system/user messages) │ stripped per spec)
55
58
  │ new prompt this turn's user message │
56
59
  └──────────────────────────────────────────────────────────────────────────┘
57
60
  ```
58
61
 
59
- - The system prompt is built **once per loop** and rebuilt only on `@prompt_builder.reset!`: a model switch
60
- (`Engine#switch_model!`), a plugin's `tools_changed!`, or a profile change. A per-turn rebuild would
61
- break everything below it.
62
+ - The system prompt is built **once per key** and kept (`SystemPrompt#build`, keyed by `[chat, thinking, layers]`:
63
+ the loop, the thinking level, the turn's LLM context layers; a `/thinking` change builds under another key). All
64
+ of them are dropped only on `@prompt_builder.reset!`: a model switch (`Engine#switch_model!`), a plugin's
65
+ `tools_changed!`, a profile change, or a toolkit change (`Engine#apply_toolkit!`). A per-turn rebuild would break
66
+ everything below it.
62
67
  - The tools layer is shared **per toolkit** (configuration.md, "Toolkits"): sessions on the same toolkit share it,
63
68
  and a chain's links keep theirs. A session's toolkit is saved when it is created and never resolved again from
64
69
  config. A change (`/toolkit`, `ctx.use_toolkit`) is applied only at a turn's start (`Engine#apply_toolkit!`,
@@ -69,7 +74,14 @@ right-hand column happens.
69
74
  breakpoint #1, so a **new session** reads it from the cache. The last non-system message carries breakpoint
70
75
  #2, so the **next request in the session** reads everything up to it. Mid and tail system messages never take
71
76
  a breakpoint. The split point comes from `SystemPrompt#stable_length` via the message's `cache_split:`, which
72
- never goes on the wire.
77
+ never goes on the wire. `PromptCache.mark` keys on the model id (`claude`) and runs only for a remote host's chat
78
+ requests (`OpenAIChat#cache_breakpoints`).
79
+ - On a Messages host (`api: anthropic`), `AnthropicWire.translate` sets the same two breakpoints on the translated
80
+ request: the first `system` block (the stable part, when `cache_split:` splits it) and the last user block that
81
+ can carry one (never a thinking block, never one from a mid or tail system message). It doesn't look at the model
82
+ id: every chat request carries them, unless the host's dialect has none (`Dialect#cache_breakpoints`: `fireworks`,
83
+ an automatic prefix cache) or the host refused `cache_control` with a 400 (no breakpoints for that worker's life).
84
+ [anthropic.md](anthropic.md) has the details.
73
85
  - Side requests (idle recap, `/btw`) go through `IdleClient` with their own short system prompt. On a current
74
86
  llama.cpp they don't evict the session (see the server settings below); chi never pins them to a slot.
75
87
 
@@ -115,14 +127,17 @@ is prefilled once. The native and the chat loop use the same plan.
115
127
  one provider. `cache.key: session` also sends the session id as `prompt_cache_key` in the body, the field
116
128
  OpenAI's API routes and caches by (OpenRouter honours it too); the log line shows `cache_by=session`. Only
117
129
  `api.openai.com` and OpenRouter get it, and only the session's own chat requests (not recap or `/btw`): a local
118
- llama.cpp, Splash or another gateway may reject an unknown field. Off by default.
130
+ llama.cpp, Splash or another gateway may reject an unknown field. Off by default. A Messages host never gets
131
+ `prompt_cache_key` (`AnthropicMessages::DROPPED_OPTIONS`); its dialect may add a header of its own beside
132
+ `Session-Id` (`Dialect#session_header`: `x-session-affinity` under `dialect: fireworks`, which keeps a session on
133
+ the replica that has its cache).
119
134
 
120
135
  ## Reading the numbers
121
136
 
122
137
  - The debug log's `generation_completed` line: `prompt=` (the request's prompt tokens), `cached=` (read from the
123
138
  server's cache), `cache_write=` (written to it, where the provider reports writes) and `reprefill=` (of the last
124
139
  request's prompt on the same model, what was prefilled again). The `http` line of a
125
- remote Claude request shows `cache=on` (or `cache=1h`) when it carried breakpoints.
140
+ remote Claude or Messages request shows `cache=on` (or `cache=1h`) when it carried breakpoints.
126
141
  - The warm-up logs `model warmup` with `cached=`, `prefilled=` and `prefill_ms=`.
127
142
  - `/stats`: "cached N (P%)", "cache writes" and "re-prefilled" for the session.
128
143
  - A healthy session: from the second request on `cached=` is close to `prompt=`; a second fresh session in the