samagotchi 0.44.0 → 0.46.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 (68) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +120 -1
  3. data/docs/architecture.md +2 -1
  4. data/docs/cli.md +43 -12
  5. data/docs/configuration.md +102 -5
  6. data/docs/context.md +2 -1
  7. data/docs/internals/background-tasks.md +1 -0
  8. data/docs/internals/engine.md +1 -1
  9. data/docs/internals/llm-context-live.md +111 -0
  10. data/docs/plugins.md +4 -2
  11. data/docs/sessions.md +9 -5
  12. data/lib/samagotchi/bridge/turn_accumulator.rb +19 -0
  13. data/lib/samagotchi/bridge.rb +22 -10
  14. data/lib/samagotchi/bundles/github-pr/manifest.yml +3 -3
  15. data/lib/samagotchi/bundles/github-pr/plugin.rb +22 -4
  16. data/lib/samagotchi/bundles/system/config_modification_protocol.md +1 -1
  17. data/lib/samagotchi/bundles/system/manifest.yml +3 -3
  18. data/lib/samagotchi/bundles/system/self_map.md +2 -1
  19. data/lib/samagotchi/commands/registry.rb +54 -9
  20. data/lib/samagotchi/config.rb +57 -4
  21. data/lib/samagotchi/desktop/macos/Panel.swift +7 -3
  22. data/lib/samagotchi/engine.rb +47 -9
  23. data/lib/samagotchi/host_model.rb +116 -0
  24. data/lib/samagotchi/host_registry.rb +43 -12
  25. data/lib/samagotchi/kernel_loop.rb +2 -1
  26. data/lib/samagotchi/llm/chat_loop.rb +89 -7
  27. data/lib/samagotchi/llm/turn_settings.rb +5 -3
  28. data/lib/samagotchi/llm_context_strategy.rb +37 -0
  29. data/lib/samagotchi/model_catalog.rb +17 -12
  30. data/lib/samagotchi/model_notes.rb +8 -6
  31. data/lib/samagotchi/model_overlay.rb +3 -2
  32. data/lib/samagotchi/model_price.rb +54 -0
  33. data/lib/samagotchi/model_profile.rb +74 -3
  34. data/lib/samagotchi/plugin/context.rb +19 -7
  35. data/lib/samagotchi/process_group.rb +56 -9
  36. data/lib/samagotchi/recap_setup.rb +1 -1
  37. data/lib/samagotchi/self_report.rb +33 -3
  38. data/lib/samagotchi/session_commands.rb +47 -21
  39. data/lib/samagotchi/session_manager.rb +19 -1
  40. data/lib/samagotchi/session_metrics.rb +79 -11
  41. data/lib/samagotchi/sessions_command.rb +15 -5
  42. data/lib/samagotchi/terminal_ui/attached_loop.rb +85 -3
  43. data/lib/samagotchi/terminal_ui/attached_view.rb +2 -0
  44. data/lib/samagotchi/terminal_ui/formatting.rb +45 -10
  45. data/lib/samagotchi/terminal_ui/status_row.rb +12 -6
  46. data/lib/samagotchi/terminal_ui.rb +97 -12
  47. data/lib/samagotchi/tools/delegate_wait.rb +5 -4
  48. data/lib/samagotchi/tools/task_runtime.rb +11 -2
  49. data/lib/samagotchi/version.rb +1 -1
  50. data/lib/samagotchi/web/app.rb +12 -11
  51. data/lib/samagotchi/web/public/app.js +30 -18
  52. data/lib/samagotchi/web/public/command_complete.js +3 -1
  53. data/lib/samagotchi/web/public/ctx.js +62 -22
  54. data/lib/samagotchi/web/public/format.js +7 -2
  55. data/lib/samagotchi/web/public/index.html +3 -1
  56. data/lib/samagotchi/web/public/llm_context_chip.js +2 -4
  57. data/lib/samagotchi/web/public/model_pick.js +23 -3
  58. data/lib/samagotchi/web/public/model_picker.js +5 -3
  59. data/lib/samagotchi/web/public/stage_model.js +18 -4
  60. data/lib/samagotchi/web/public/stage_view.js +4 -3
  61. data/lib/samagotchi/web/public/turn_events.js +19 -6
  62. data/lib/samagotchi/web/public/turn_model.js +16 -0
  63. data/lib/samagotchi/web/public/turn_view.js +19 -0
  64. data/lib/samagotchi/web/session_hub.rb +9 -1
  65. data/lib/samagotchi/web/session_summary.rb +19 -2
  66. data/lib/samagotchi/worker.rb +172 -21
  67. data/lib/samagotchi/worker_inbound.rb +6 -5
  68. metadata +4 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 684d8e5c169b4ce5df4cd21c4c46d0ef8524230e530d3e90ec2076f5b674e905
4
- data.tar.gz: 54dbaf8bd2ad38b78e16aca155414748491912bdaebe1069f128e6182df7bfe5
3
+ metadata.gz: 0fb03f917133e78236dfb0bf16eae6c21b00ec2fb3720501d6c1982aeb9dc220
4
+ data.tar.gz: b3edff20b87e1f954c6079cd0315ab64559a5814f9a64e92e215fedc745a2778
5
5
  SHA512:
6
- metadata.gz: de459298c279f571db785d374e1752a58060d26a13c4f7a6d22f8c4e4a5c82acbcffecd5784712eda405908cc3a8fd7bf1526952878b3791017088ebb62f120e
7
- data.tar.gz: a99cf35d53b0a9fb7b3ed568a73d3383252ba334e4c8efd50efd9d97ded3cb716432fd51ee069e066ce3d21d137f1fadf49e20f2b99e9a769e430c8274278a64
6
+ metadata.gz: 96b8d90741656cae236454b5f1b56f49e77ad1a511eb81cdc31594b311031b57363993091ae0df70cf78c6f346a2b6b867896b53e0093ae2e59671146cd12eb6
7
+ data.tar.gz: 6041ee413db16da9147f763f83cfabcf91e0c62ea95380fa8ae454aab9e242b646501fafdd2ed5071008b4b218ad740c585517ee1e3cd0fb84531cb36a43d59d
data/CHANGELOG.md CHANGED
@@ -8,6 +8,123 @@ and commands may change between minor versions. How releases are made:
8
8
 
9
9
  ## [Unreleased]
10
10
 
11
+ ## [0.46.0] - 2026-10-09
12
+
13
+ ### Added
14
+
15
+ - `hosts.<name>.models:` declares the model ids a host serves whatever its `/v1/models` lists (a gateway's
16
+ round-robin ids), as a map of ids or a plain list. A declared `host:id` is never warned about or re-listed at
17
+ `chi send --new` and delegate spawns, and a bare id routes to its declaring host as if listed, before any
18
+ `/models` and in workers too. Two hosts declaring one id warn at start. See "Models a host serves but doesn't
19
+ list" in [docs/configuration.md](docs/configuration.md).
20
+ - Declared `hosts.<name>.models` ids show first under their host in `/models` (as `rr/x (config)`, outside the 20
21
+ per host, also on a host that lists nothing), `chi models` (plain names; `--format json` adds `configured: true`)
22
+ and `GET /api/models` (`configured: true`). A host that doesn't answer shows none of them.
23
+ - `hosts.<name>.models.<id>.price: {input, output, cache_read?, cache_write?}` (USD per 1M tokens) estimates the
24
+ cost of a generation whose provider reports none, or reports 0. A reported non-zero cost always wins. The estimate
25
+ is saved apart (`cost_estimate` per turn, `cost_estimate_sum` in the totals). See "Prices" in
26
+ [docs/configuration.md](docs/configuration.md).
27
+ - `hosts.<name>.models.<id>.served: [ids]` (or `any`) names the models a gateway may answer a round-robin id with.
28
+ Those are no longer marked as served by another model (the web's `⚠`, the status line's `(served; asked …)`,
29
+ `/model`'s `served:`, `/stats`' "(asked for …)"); one it doesn't name still is.
30
+ - An estimated cost shows with `~`: `/stats` (`$0.42 reported + ~$0.12 from hosts.<name>.models prices`) and the
31
+ ctx tooltip on the info bar and the session cards (`cost: ~$0.54 ($0.42 reported, ~$0.12 estimated)`).
32
+ - The web model picker lists a host's declared ids first under it, noted `config` (the default model keeps
33
+ `default`), with `served by config (hosts.<name>.models)` in the row's tooltip.
34
+
35
+ ### Fixed
36
+
37
+ - A chat generation whose stream dropped mid-answer (a read timeout or a reset after deltas had shown) failed the
38
+ whole turn: the chat loop now drops the partial reply and asks again for the same step, from the same
39
+ conversation, at most twice, reporting each ask-again as `generation_retrying`, as the transport's retries do.
40
+ A cancel, a provider error, a first-token timeout and a connection refused still fail as before, and a step whose
41
+ retries run out fails the turn with the same error. Each ask-again waits the transport's backoff (`retry.*`),
42
+ which a cancel cuts short, and `retry.max` caps the asks-again (`retry.max: 0` turns them off). Its
43
+ `generation_retrying` carries `restarted: true`: the web's live step, the TUI's activity line, a reload in the
44
+ middle of the turn and the stream hooks drop the partial reply, so the answer shows once.
45
+ - `task_stop` could signal an unrelated process group whose leader reused a finished task's pid. A task now records
46
+ its process's start time, and a pid that started at another time is not signalled (the task ends as failed).
47
+ - A delegate's report merged into a reminder or continue turn was lost when the turn failed past its loop (the
48
+ turn's own end, say): the turn kept only its start, yet the report counted as read. Its ring now stays, and the
49
+ next turn brings it.
50
+ - A delegate wait that relayed a child's approval ran to its timeout when the child ended its turn with no reply
51
+ while the answer was being posted; it now ends there.
52
+ - Chi Helper: a send that ended after the panel was reopened wrote its "sent"/"failed" line into the new panel, and
53
+ a success could close it; like a broadcast, it now leaves the new open alone.
54
+ - `chi -p … --non-interactive` hung forever when the model asked a question (`ask_user_question`) while its stdin
55
+ was a pipe that stayed open (`sleep 30 | chi -p …`, a parent holding stdin): it waited for an answer line. Nobody
56
+ can answer there, as for an approval: the question is dismissed at once (one line on stderr), the model finishes
57
+ its reply, and stdin is never read. Piped answers without `--non-interactive` (`printf '2\n' | chi --no-shared -p
58
+ …`) work as before.
59
+
60
+ ## [0.45.0] - 2026-10-08
61
+
62
+ ### Added
63
+
64
+ - Plugins: `ctx.fork?` says whether the session is a fork (`ctx.sessions.fork`: it has a parent and isn't a
65
+ delegate child). See [docs/plugins.md](docs/plugins.md).
66
+
67
+ ### Changed
68
+
69
+ - Commands sent while a turn runs no longer all answer `busy`. Those that only show something answer at once
70
+ (`/models`, and `/model`, `/llm-context` and `/guardrails` alone). Those that change something (`/model X`,
71
+ `/llm-context strategy …`, `/guardrails revoke N`) and `!cmd` run after the turn, in the order sent with the
72
+ prompts: the terminals say `(queued: runs after this turn)`, the web draws the command's bubble dashed until it
73
+ ran (after a reload too), and the web's `llm ctx` chip can Set mid-turn. A `!cmd` waiting after a canceled or
74
+ failed turn is dropped, so the turn's `!rollback` stays open, and one still waiting when the worker stops is
75
+ answered `dropped`. `!rollback` and `/continue` stay refused (`busy: Ctrl-C the turn first, then !rollback`).
76
+ `/archive` typed during a turn archives once it ends, in the REPL and an attached terminal. `chi --attach ID
77
+ --model X` on a running turn waits for its end. See "Typing during a turn" in [docs/cli.md](docs/cli.md).
78
+
79
+ ### Fixed
80
+
81
+ - `/stats`, `/model` and the web named no model notes for a fresh session (a REPL's before the first turn, a new
82
+ web or worker session), though its first turn's prompt carries them; they now name the notes that prompt will
83
+ load. A session whose prompt carried none keeps saying so.
84
+ - `chi self`'s `model notes` row resolved a note's model overlay from the process's `XDG_CONFIG_HOME` while it
85
+ read the note itself from the env it was given, so the row could count no overlay (or another config's); it now
86
+ reads both from the same env, and passes the fallback key the session's prompt does, so a model typed as an
87
+ alias whose note overlay is keyed by the alias's name shows it — the row agrees with the notes the session's prompt loads.
88
+ - In the plain REPL a session command typed at a continue offer (other than `/continue`) was read as an invalid
89
+ answer and lost; it now runs and the offer stays open, as in a worker.
90
+ - A session command sent as a message behind queued prompts (`chi send -m "/model x"` after `chi send -m "task"`)
91
+ could be answered `busy` by the next queued prompt's turn instead of running before it; it now runs once its
92
+ own message's turn comes.
93
+ - The web's ctx meter under an LLM context budget smaller than the window (`llm_context.budget_tokens`) read the
94
+ percentage of the window after a reload (`ctx 12%`) where live it read the budget's (`ctx 25%`); it now counts
95
+ against the budget both ways, as the `[CONTEXT: …]` line and the status line do, and a generation's final counts
96
+ no longer flip it back to the window mid-turn. The ctx tooltip's context line says so: `context: ~16.0k of the
97
+ 64.0k budget, 128.0k window (server)`.
98
+ - The web's strip of latest sessions, back from All sessions with the pointer still on it, could keep a waiting
99
+ family behind other cards until the pointer moved: an update that came while All sessions opened was drawn in
100
+ the order held for the pointer.
101
+ - github-pr 0.3.1: in `auto_attach: offer` mode a forked session (`/btw keep`, a plugin's fork) now gets its
102
+ `first_prompt` row in `offers.ndjson` (its first prompt after the conversation it started from), as a new
103
+ session does; it got none.
104
+ - A resumed or attached session (`chi --resume`, `chi --attach`) drew a turn that failed with its steps kept as its
105
+ prompt and tool rows followed by an earlier step's text, as if that were the answer; the join now draws the line
106
+ the live view left (`✕ turn failed: HTTP 500: boom · 2.0s`, then ` partial progress kept; !rollback restores the
107
+ pre-turn state` when the steps stayed) after the rows, with no answer text, as the web does.
108
+ - The web's stage trail: when the ✂ row of an applied LLM context edit landed in its step inside the
109
+ closed cloud it was out of sight and, unlike a hook's notice, flashed nothing. It now flashes in the
110
+ trail for a few seconds, as its row reads (`✂ forgot 2 outputs, stubbed 1 stale read · frees ~4.1k
111
+ tokens (paid off)`), as a hook notice does.
112
+ - The web's stage trail: when a message cut a generation that was still thinking, its row (`↪ cut in
113
+ for your message`, `… a message sent with chi send`, `… the parent agent's message`) landed in its
114
+ step inside the closed cloud and, out of sight, flashed nothing. It now flashes in the trail for a
115
+ few seconds, as its row reads, as the ✂ row and a hook notice do.
116
+ - `chi send --new --model` and `delegate` warned `host 'h' doesn't list model 'x'` for an id the host serves, when
117
+ the on-disk model list still named the model a one-model server served before it was reloaded: a saved list
118
+ older than 10 minutes that misses the id now re-lists that one host once (bounded at 5 s) and judges again
119
+ against what it lists now, saving the fresh ids. A list from the last 10 minutes is taken at its word, so a host
120
+ that serves ids it never lists (a gateway's round-robin aliases) is not asked on every launch; a re-list that
121
+ fails, times out or lists nothing warns from the saved list.
122
+ - The session lists' `ctx %` (the web's session cards, `chi sessions list` and its `ctx 12%` column, the
123
+ `list_sessions` tool's `ctx_pct`) and `/stats`' `context used:` still divided by the window when the session's
124
+ LLM context budget (`llm_context.budget_tokens`) was smaller, where the live meter counts the smaller of the
125
+ two; they now count the same way, and `/stats` says so: `context used: 16000 tokens (25.0% of the 64000
126
+ budget)`.
127
+
11
128
  ## [0.44.0] - 2026-10-08
12
129
 
13
130
  ### Added
@@ -2026,7 +2143,9 @@ and long-lived sessions.
2026
2143
  - A macOS desktop helper (`chi desktop install`): a "Send to chi" Service and a
2027
2144
  hotkey panel that send selected text or the clipboard to your sessions.
2028
2145
 
2029
- [Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.44.0...HEAD
2146
+ [Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.46.0...HEAD
2147
+ [0.46.0]: https://github.com/dm1try/samagotchi/compare/v0.45.0...v0.46.0
2148
+ [0.45.0]: https://github.com/dm1try/samagotchi/compare/v0.44.0...v0.45.0
2030
2149
  [0.44.0]: https://github.com/dm1try/samagotchi/compare/v0.43.0...v0.44.0
2031
2150
  [0.43.0]: https://github.com/dm1try/samagotchi/compare/v0.42.0...v0.43.0
2032
2151
  [0.42.0]: https://github.com/dm1try/samagotchi/compare/v0.41.0...v0.42.0
data/docs/architecture.md CHANGED
@@ -287,7 +287,8 @@ The metrics in `/state`, `/snapshot`, `/stats` and the SSE `snapshot`/`reset` fr
287
287
  newest turn the worker finished and any it hasn't saved yet (at most 20 turns), with their tool
288
288
  calls and the running turn's finished ones. The full history is the session's `analytics.json`,
289
289
  which `SessionMetrics#persist` rewrites after each turn; the web server merges the two by id.
290
- The totals' `tokens` block adds the cached, cache-write and reasoning sums, the cost, the decode time and tokens
290
+ The totals' `tokens` block adds the cached, cache-write and reasoning sums, the reported cost (`cost_sum`) and the
291
+ estimated one (`cost_estimate_sum`, from `hosts.<name>.models` prices; 0 for none), the decode time and tokens
291
292
  behind the average speed (`avg_decode_tps`) and the last speeds (`last_decode_tps`,
292
293
  `last_prefill_tps`, `tps_source`: `server` or `estimate`). Each `generation_completed` event
293
294
  carries the generation's `speed` (`{decode_tps, source}`, null without one), its own prompt-cache
data/docs/cli.md CHANGED
@@ -85,12 +85,15 @@ small -> box:gemma-small # an alias and the ref it resolves to
85
85
  $ chi models qwen # only the names containing "qwen" (any case)
86
86
  ```
87
87
 
88
+ - A host's ids declared under `hosts.<name>.models` (see
89
+ [configuration](configuration.md#models-a-host-serves-but-doesnt-list)) come first under that host, listed or not,
90
+ as plain names like any other.
88
91
  - A default-host id is bare, except one with a `:` (`qwen3:8b`), which is written `<host>:qwen3:8b` so it can't
89
92
  read as a host name. An id that an alias of the same name hides is left out.
90
93
  - Each run lists anew (no cache); `--timeout S` caps the wait for the hosts (default 4 s). A host that fails or doesn't
91
94
  answer in time is noted on stderr (`chi models: box: no answer in 4 s`).
92
95
  - `--format json` prints one object: `default` (and `default_typed` when it was set as an alias), `default_host`,
93
- `models` (`name`, `host`, `id`), `aliases` (`name`, `ref`, `host`) and `warnings`. The desktop helper's model
96
+ `models` (`name`, `host`, `id`, and `configured: true` on an id declared under `hosts.<name>.models`), `aliases` (`name`, `ref`, `host`) and `warnings`. The desktop helper's model
94
97
  chooser reads it.
95
98
  - Exit 0 when any host listed its models, 1 when none did (the default is still printed), 2 on a usage error.
96
99
 
@@ -245,6 +248,9 @@ Notes:
245
248
  then `chi: the session is kept with your prompt; continue it with: chi
246
249
  --resume ID` (exit 1). Ctrl-C saves the session with the prompt and a
247
250
  cancel note and exits 130.
251
+ Nobody can answer there: an approval is denied, and a question the model
252
+ asks (`ask_user_question`) is dismissed at once (one line on stderr), never
253
+ read from stdin, so the model finishes its reply.
248
254
 
249
255
  ### Scratch sessions
250
256
 
@@ -429,9 +435,11 @@ request (with the cached tokens, the cache writes, the re-prefilled tokens and t
429
435
  reports them),
430
436
  the decode speed, generation latency, cancellations, retries (the network's),
431
437
  thinking cuts (generations a plugin such as loop-guard cut) and output cap hits
432
- (generations that ended at the provider's cap), context used and window, the memory indexes the session's prompt holds ([Memory](memory.md#the-indexs-size)), the model notes it carried (`model notes: model_notes_deepseek (system, 612 chars)`, none without any; [Memory](memory.md#model-notes)), the prompt profile and the model the server says it ran, and the
433
- cost when the provider reports one (OpenRouter's `usage.cost`; no prices are
434
- guessed). The speed line is the last generation's and the session's average
438
+ (generations that ended at the provider's cap), context used and window (the used figure is counted against the smaller of the window and the session's LLM context budget, and says so: `context used: 16000 tokens (25.0% of the 64000 budget)`), the memory indexes the session's prompt holds ([Memory](memory.md#the-indexs-size)), the model notes it carried (`model notes: model_notes_deepseek (system, 612 chars)`, none without any; [Memory](memory.md#model-notes)), the prompt profile and the model the server says it ran, and the
439
+ cost when the provider reports one (OpenRouter's `usage.cost`), and the cost
440
+ estimated from a price under `hosts.<name>.models` when it reports none or 0,
441
+ marked `~` (`cost: $0.42 reported + ~$0.12 from hosts.<name>.models prices (this session only)`;
442
+ see "Prices" in [configuration](configuration.md#prices)). The speed line is the last generation's and the session's average
435
443
  (weighted by tokens): llama.cpp reports it exactly (`87 tok/s out, 1.9k tok/s
436
444
  prompt (last, server)`); for other servers it is estimated from the first
437
445
  streamed chunk to the end and marked `~` (`~64 tok/s out (last, estimate)`).
@@ -451,7 +459,8 @@ saved with the session and applies from the next turn's start; the reply says
451
459
  what it costs: `forget` on or off re-reads the whole prompt once, `stale` turned
452
460
  on stages the reads already superseded as one batch under the apply rule, and a
453
461
  layer turned off sends its stubs whole again (turning it back on brings them
454
- back). It runs between turns (a worker answers `busy` mid-turn). `/stats` shows
462
+ back). `/llm-context` alone answers mid-turn too; a change sent while a turn runs
463
+ waits for the turn's end (see *Typing during a turn*). `/stats` shows
455
464
  the same values on its `llm context:` line. The web's info bar shows them as the
456
465
  `llm ctx` chip (`llm ctx stale,forget · session`), whose popover sets them
457
466
  through `/llm-context`. See "LLM context: a session's own strategy" in
@@ -468,11 +477,28 @@ REPL alike:
468
477
  A line that comes after the turn's last step runs as the next turn, and so
469
478
  does one sent after Ctrl-C: it doesn't merge into the turn being cancelled.
470
479
  Reminder turns take merged lines too.
471
- - `/stats` and `/recap` answer at once, and in an attached terminal so do `/help`
472
- and a plugin's anytime command (`/btw`). Other commands (`!cmd`, `/model`, `/models`,
473
- `!rollback`, `/continue`, `/guardrails`) say `busy: wait for the turn to end`
474
- and go back into the prompt, so Enter runs them once the turn ends. A
475
- one-word `/word` no command answers (`/modle`) is not steering text either:
480
+ - Commands answer, wait or are refused, by what they do:
481
+ - At once, beside the turn: `/stats`, `/recap`, `/help`, `/context`, a
482
+ plugin's anytime command (`/btw`), and the commands that only show
483
+ something: `/models` (with a filter too), and `/model`, `/llm-context` and
484
+ `/guardrails` alone.
485
+ - After the turn: the ones that change something (`/model X`, `/model clear`,
486
+ `/llm-context strategy …`, `/guardrails revoke N`) and `!cmd`, whose output
487
+ joins the conversation. `(queued: runs after this turn)` says so at once (the
488
+ web draws the command's bubble dashed, with the same words, until it ran);
489
+ when the turn ends they run in the order sent, a prompt sent before one
490
+ running first and one sent after it running after. A `/model X` sent
491
+ mid-turn doesn't change the running turn, nor a line merged into it later.
492
+ After a canceled or failed turn a waiting `!cmd` is dropped
493
+ (`turn canceled: !ls not run; send it again`): running it would close the
494
+ `!rollback` the turn left; the others still run. A command still waiting
495
+ when the session's worker stops is answered `dropped` too.
496
+ - Refused: `!rollback` (`busy: Ctrl-C the turn first, then !rollback`),
497
+ `/continue` and a plugin's command that isn't anytime
498
+ (`busy: wait for the turn to end`). The line goes back into the prompt, so
499
+ Enter runs it once the turn ends.
500
+
501
+ A one-word `/word` no command answers (`/modle`) is not steering text either:
476
502
  the hint prints and the line is dropped.
477
503
  - A question (`ask_user_question`, a guardrails approval) turns the prompt into
478
504
  a yellow `? ` and lists its choices under it, fitted to the terminal; only a
@@ -486,6 +512,10 @@ REPL alike:
486
512
  (`/exit --delete` deletes the session then too). In an
487
513
  attached terminal it detaches at once and the turn goes on in the worker
488
514
  (`/exit` then says the worker stays up: a turn is running).
515
+ - `/archive` mid-turn archives once the turn ends:
516
+ `(archives after this turn; Ctrl-C cancels it)` in the plain REPL,
517
+ `(archives after this turn)` in an attached terminal, which also waits for
518
+ the commands queued behind the turn.
489
519
 
490
520
  With stdin that isn't a terminal (a pipe), the REPL reads a line only between
491
521
  turns.
@@ -712,7 +742,7 @@ In interactive assist mode, you can switch the request model without restarting:
712
742
  - `/model`: show the effective model (and default when diverged: `runtime model: <effective> (default: <default>, profile=<name>, <source>)`, e.g. `profile=qwen36, server (chat_template)`). When the session's prompt carries model notes the line ends with `; notes: model_notes_deepseek (system, 612 chars)`; a switch's `runtime model set to …` names the new model's ([Memory](memory.md#model-notes)).
713
743
  - `/model clear` (or `default`/`none`/`off`): clear the session override, reverting to the configured default.
714
744
  - `/guardrails`: the guardrail rules (by source), what failed to load, and your stored approvals, numbered; `/guardrails revoke N` removes approval N (see [Guardrails](guardrails.md)).
715
- - `/models`: list model ids aggregated across all `hosts:` (an alias shows next to its id: a bare alias on every host listing the id, a `host:model` one only under its host; grouped `host (host:port):` with per-host `unreachable` warnings, e.g. an unset `api_key_env`; lists cached 60s, 10 minutes for a remote host; lazy — no startup prefill). At most 20 ids per host, then `… and N more`; `/models <text>` lists every id containing `<text>` (any case), e.g. `/models qwen` on OpenRouter.
745
+ - `/models`: list model ids aggregated across all `hosts:` (an alias shows next to its id: a bare alias on every host listing the id, a `host:model` one only under its host; grouped `host (host:port):` with per-host `unreachable` warnings, e.g. an unset `api_key_env`; lists cached 60s, 10 minutes for a remote host; lazy — no startup prefill). Ids the host declares under `hosts.<name>.models` come first, as `rr/x (config)`, and don't count toward the cap. At most 20 ids per host, then `… and N more`; `/models <text>` lists every id containing `<text>` (any case), e.g. `/models qwen` on OpenRouter.
716
746
 
717
747
  Notes:
718
748
 
@@ -815,7 +845,8 @@ status> model=Qwen3.6-35B | ⑂ 2 running · 1 waiting | ctx=8.1% (under20) | me
815
845
  ```
816
846
 
817
847
  - `model=`: the model in use, `(default: …)` beside it when it isn't the config's default, and
818
- `model=<served> (served; asked <name>)` when the server said it served another model.
848
+ `model=<served> (served; asked <name>)` when the server said it served another model (not for one its host's
849
+ `hosts.<name>.models.<id>.served` names; see [configuration](configuration.md#models-a-host-serves-but-doesnt-list)).
819
850
  - `↳ <id>`: the session that delegated this one.
820
851
  - `⑂ …`: this session's delegate children (forks aren't counted): how many run and how many wait
821
852
  for an answer; only the counts there are, nothing when none does (`/children` has the rest:
@@ -413,6 +413,41 @@ models:
413
413
  `SAMAGOTCHI_HOSTS_JSON`, and reads `models:` from the config file each turn: after changing a host's `sampling:`,
414
414
  stop the session's worker (`chi sessions stop`) for it to take effect.
415
415
 
416
+ ## Prices
417
+
418
+ A provider that reports no cost (`usage.cost`), or reports 0, can still be priced: give the model a price under
419
+ the host that serves it, in USD per 1M tokens.
420
+
421
+ ```yaml
422
+ hosts:
423
+ work:
424
+ url: https://gateway.example/v1
425
+ api: openai
426
+ models:
427
+ rr/deepseek-v4.1:
428
+ price: {input: 0.27, cache_read: 0.07, output: 1.10}
429
+ openrouter:
430
+ url: https://openrouter.ai/api/v1
431
+ api: openai
432
+ models:
433
+ qwen/qwen3.8-27b: {price: {input: 0.10, output: 0.40}} # a listed id, here only for its price
434
+ ```
435
+
436
+ - `input` and `output` are required; `cache_read` and `cache_write` are optional. Each is a number >= 0; a price
437
+ missing one of the required two, or with a value that isn't a number >= 0, warns at start and is ignored.
438
+ - An unset rate is the input rate: that overstates cached reads, and understates Anthropic cache writes (billed
439
+ 1.25x input for the 5 min cache, 2x for 1 h). Set them when it matters.
440
+ - A price is per host, since one model can cost differently on two hosts. It is looked up by the model the session
441
+ asked for (its host and id), never by the model the host says it served.
442
+ - Each generation with the server's token counts is estimated as uncached prompt × `input` + cached reads ×
443
+ `cache_read` + cache writes × `cache_write` + completion × `output`, over 1M. A generation with only estimated
444
+ counts (characters / 4, no usage from the server) isn't priced.
445
+ - A reported cost wins: a generation with a non-zero `usage.cost` is never estimated. A reported 0 with a price is
446
+ estimated (a gateway that bills elsewhere reports 0); the 0 is kept as the reported cost.
447
+ - The estimate is saved apart from the reported cost (`cost_estimate` per turn, `cost_estimate_sum` in the
448
+ session's totals in `analytics.json`) and shows with a `~`.
449
+ - A server that sends no cached counts has its whole prompt priced at `input`: an overestimate.
450
+
416
451
  ## Thinking
417
452
 
418
453
  How much a model thinks before it answers. One level, `off`, `low`, `medium`, `high` or `default`, set per model,
@@ -555,11 +590,12 @@ When `default.model` is unset or blank, Samagotchi fails fast with a clear start
555
590
  With several `hosts:`, `host:model` (or an alias naming a host) pins the host.
556
591
  An unqualified model name goes to the default host when its `/models` list has
557
592
  that exact id, else to the first host in `hosts:` order whose list has it, else
558
- to the default host. Only exact ids count, never a substring: `/model gemma` on
593
+ to the default host. A host's declared ids (`hosts.<name>.models`, below) count
594
+ as listed. Only exact ids count, never a substring: `/model gemma` on
559
595
  a box that lists `gemma-4-26b` needs `box:gemma` or the exact id. The lists are
560
596
  known only after `/models` ran (nothing is fetched before the first turn), so
561
- until then an unqualified name goes to the default host; use `host:model` to pin
562
- one. A **remote** host (an `https` url or a public address; see "Remote or local") keeps its model
597
+ until then an unqualified name goes to the default host, unless a host declares
598
+ it; use `host:model` to pin one. A **remote** host (an `https` url or a public address; see "Remote or local") keeps its model
563
599
  list for 10 minutes (60s for local hosts).
564
600
 
565
601
  **The context window** (what the context status and `ctx=` count against) comes from,
@@ -608,6 +644,59 @@ alias, `/model`, `chi send --new --model`, the web's new-session model and a
608
644
  delegate's model. Any other unknown prefix (`nosuch:x`) is sent to the default
609
645
  host as the model id.
610
646
 
647
+ ### Models a host serves but doesn't list
648
+
649
+ A gateway can serve ids its `/v1/models` never lists (round-robin aliases such
650
+ as `rr/deepseek-v4.1`). Declare them under the host:
651
+
652
+ ```yaml
653
+ hosts:
654
+ work:
655
+ url: https://gateway.example/v1
656
+ api: openai
657
+ api_key_env: WORK_API_KEY
658
+ models:
659
+ rr/deepseek-v4.1:
660
+ rr/qwen3.8-27b:
661
+ ```
662
+
663
+ A plain list says the same: `models: [rr/deepseek-v4.1, rr/qwen3.8-27b]`. Ids are
664
+ full ids (no globs), matched case-insensitively. A declared id is known on its
665
+ host whatever the host lists:
666
+
667
+ - `work:rr/deepseek-v4.1` never gets the "host 'work' doesn't list model" warning,
668
+ and the host isn't re-listed for it (`chi send --new --model`, delegates).
669
+ - A bare `rr/deepseek-v4.1` goes to `work` as if `work` listed it, also before
670
+ any `/models` and in a session's worker. When several hosts list or declare an
671
+ id, the default host wins if it is one of them, else the first in `hosts:`
672
+ order; two hosts declaring one id get a warning at start saying which one a
673
+ bare id goes to. `host:id` still pins a host.
674
+
675
+ **A gateway that answers with another model.** A round-robin id is served by one
676
+ of its targets, and the reply names that one (`fireworks/deepseek-v4.1`), so chi
677
+ marks every turn as served by another model than asked (the web's `⚠`, the status
678
+ line's `(served; asked …)`, `/model`'s `served:`). `served:` lists the models the
679
+ host may answer with, as exact ids (any case), or `any`:
680
+
681
+ ```yaml
682
+ hosts:
683
+ work:
684
+ models:
685
+ rr/deepseek-v4.1:
686
+ served: [fireworks/deepseek-v4.1, baseten/deepseek-v4.1]
687
+ rr/qwen3.8-27b:
688
+ served: any
689
+ ```
690
+
691
+ A served model it names is no mismatch: the web and the status line keep the name
692
+ you asked for, and `/stats` names the served model without "(asked for …)". One it
693
+ doesn't name is still marked. `any` gives that check up for the id.
694
+
695
+ **Root `models:` vs `hosts.<name>.models:`.** The root `models:` holds settings
696
+ by model name (sampling, thinking, window, profile…), on whatever host the model
697
+ runs on; it says nothing about which host serves a model. `hosts.<name>.models:`
698
+ says which models that host serves. Keep settings in the root one.
699
+
611
700
  ### Model aliases
612
701
 
613
702
  `model_aliases:` maps a name to a model id or `host:model`. An alias works
@@ -641,12 +730,17 @@ Transient network failures are retried automatically with exponential backoff.
641
730
  budget, so it fails at once (lower `default.max_tokens` or raise the key's limit).
642
731
  - A refused connection (nothing listening) is not retried: the turn fails at once with `can't reach host <name> at
643
732
  <address> (connection refused) — is the server running?`.
644
- - A stream that has already produced output is never retried (the retry would repeat it); it fails the turn.
733
+ - A stream that has already produced output is never retried at the transport (the retry would repeat it). The
734
+ chat loop (an OpenAI-compatible host) drops the partial reply and asks again for the whole step, from the same
735
+ conversation, at most twice (at most `retry.max` times when that is smaller: `retry.max: 0` turns it off), after
736
+ the same backoff, reported as a retry (`↻ retrying (stream dropped) …`); once those run out the turn fails with
737
+ the error, as before. A refusal, a first-token timeout, a provider error and a cancel are never retried this way.
645
738
  - Cancellation (`Ctrl-C`) is never retried.
646
739
 
647
740
  Configuration:
648
741
 
649
- - `retry.max` (default `5`, env `SAMAGOTCHI_RETRY_MAX`): number of retries after the first failed attempt.
742
+ - `retry.max` (default `5`, env `SAMAGOTCHI_RETRY_MAX`): number of retries after the first failed attempt; it also
743
+ caps a dropped stream's asks-again (at most 2), and `0` turns those off too.
650
744
  - `retry.base_delay` (default `0.5`, env `SAMAGOTCHI_RETRY_BASE_DELAY`): backoff base delay in seconds.
651
745
  - `retry.max_delay` (default `8.0`, env `SAMAGOTCHI_RETRY_MAX_DELAY`): cap for backoff delay in seconds.
652
746
 
@@ -1036,6 +1130,9 @@ described in their own sections.
1036
1130
  | `thinking.turn_preamble` | `true` | yes | Ask a `qwen36` model to open its thinking with a short `TURN:` line (the step label). |
1037
1131
  | `thinking.level` | `default` | `--thinking` | `off`, `low`, `medium`, `high` or `default` for every model; the flag and env outrank the `models:`/`hosts:` entries, the file's value doesn't. See "Thinking". |
1038
1132
  | `models.<key>.thinking`, `hosts.<name>.thinking` | none | | A model's or host's level. See "Thinking". |
1133
+ | `hosts.<name>.models` | none | | The model ids a host serves whatever its `/v1/models` lists: a map of ids, or a plain list. See "Models a host serves but doesn't list". |
1134
+ | `hosts.<name>.models.<id>.served` | none | | The models the host may answer that id with (exact ids, any case), or `any`: no served-model warning for those. See "Models a host serves but doesn't list". |
1135
+ | `hosts.<name>.models.<id>.price` | none | | The model's price on that host, USD per 1M tokens: `input`, `output`, optional `cache_read` and `cache_write`. Estimates the cost a provider doesn't report. See "Prices". |
1039
1136
  | `hosts.<name>.remote` | by address | | `true`/`false`: treat the host as a remote provider or a local server. See "Remote or local". |
1040
1137
  | `max_tool_output_chars` | `10000` | yes | Characters of each tool output kept in the conversation (both loops); a longer one is cut and ends with `[cut: N of M chars; read it in parts]`. A top-level key (see below). |
1041
1138
  | `cache.warmup` | `auto` | | `auto`: after a turn, send the next turn's prompt (up to the next message) to a local llama.cpp host on the native loop, so the next turn prefills only its message; never a remote or `api: openai` host. `off`: never. See [prompt caching](internals/prompt-caching.md#the-turn-end-warm-up). |
data/docs/context.md CHANGED
@@ -225,7 +225,8 @@ later when an offer is wanted: one JSON line per event, `offered` (session,
225
225
  project root, working directory, branch, PR URL, main checkout or worktree,
226
226
  model), `attached` and `declined` (with the seconds since the offer), and
227
227
  `first_prompt` (the first 160 characters of every session's first message,
228
- offered or not; not scratch sessions or delegate children). Over 1 MB it is
228
+ offered or not; a fork's first after the conversation it started from, once;
229
+ not scratch sessions or delegate children). Over 1 MB it is
229
230
  renamed to `offers.ndjson.1` (one old file is kept). Only you can read it
230
231
  (0600), and nothing leaves your machine.
231
232
 
@@ -19,6 +19,7 @@ Behavior:
19
19
 
20
20
  - `execute`, `task_create` and the `!cmd` shell bang run the command in a non-login shell: `/bin/sh -c` on Linux; on macOS, when `/bin/zsh` is there, `/bin/zsh --emulate sh +o ignore_braces +o sh_glob -o bash_rematch +o bsd_echo -c` (macOS's `/bin/sh` is bash 3.2, which can't parse an apostrophe in a heredoc inside `"$( )"`, the usual commit message shape). That mode reads no rc files and keeps POSIX word splitting, unmatched globs left as is, `{1..3}`, `[[ … =~ (…) ]]` with `BASH_REMATCH`, arrays, `$'…'`, `&>` and echo expanding `\t`. Bash-only builtins and variables (`shopt`, `BASH_VERSION`, `${!prefix*}`, `read -p`) are not there.
21
21
  - Task metadata and output are persisted under `tmp/tasks/`.
22
+ - A task record keeps its process's pid and start time (`pid_started`). Before `task_stop` signals, the pid must still lead its own process group and must not have started at another time; otherwise the pid was reused by another process, nothing is signalled and the task ends as failed. A start time that can't be read at that moment (a failed `ps`) falls back to the group check, as does a record without `pid_started` (from an older chi). `task_get`, `task_list` and `task_wait`'s polls check the group only (no `ps` per poll).
22
23
  - Task listing is workspace-scoped (current project only).
23
24
  - `task_get` returns metadata and `output_path`; use `read` for output contents.
24
25
  - `task_wait` accepts `timeout`, `tail_lines` (maximum 100), and `done_pattern` (a regular expression string).
@@ -293,7 +293,7 @@ Turn events (the main ones; `Events` in `events.rb` keeps the shared sets):
293
293
  | `:context_status` | the window's fill crossed a bucket | `{iteration, ...}` |
294
294
  | `:generation_started` | a request begins | `{iteration, context_window_tokens, context_window_source}` (native adds `profile`, `profile_source`) |
295
295
  | `:generation_chunk` | each streamed chunk | `{iteration, content, text, thinking, payload, tool_call?}` |
296
- | `:generation_retrying` | the transport retries the request | `{iteration, ...}` (the retry's fields) |
296
+ | `:generation_retrying` | the transport retries the request, or the chat loop asks again for a step whose stream dropped mid-answer (`restarted: true`: what the step streamed is void, and the web's live step, the TUI's activity line, the Bridge's TurnAccumulator and the stream hooks' watch start it over) | `{iteration, attempt, max_retries, next_delay, error_class, error_message, status, restarted?}` |
297
297
  | `:generation_completed` | a generation ended, or was cut | `{iteration, content_length, thinking_chars, served_model, requested_model, finish_reason, ...}`; a cut adds `stopped_by`, `stop_reason`; the Engine adds `speed`, `tokens` |
298
298
  | `:generation_cancelled` | the turn was cancelled mid-loop | `{iteration, reason, stopped_by}` |
299
299
  | `:empty_answer_retry` | an empty, cut or malformed answer is asked again | `{iteration, attempt, of, finish_reason, thinking_chars, stopped_by?, malformed?}` |
@@ -0,0 +1,111 @@
1
+ # The llm_context live runs
2
+
3
+ `script/llm_context_live.rb` runs chi itself on real coding tasks under each LLM context strategy and scores the
4
+ sessions. The replay benchmark ([llm-context-bench.md](llm-context-bench.md)) scores stored sessions offline. These
5
+ runs close the loop: the model lives with the strategy for a whole task, so they measure what the replay can't, such
6
+ as whether a forgotten output comes back as a re-read and whether a note narrows the scope (context strategies P6).
7
+ It is a development tool under `script/` and isn't shipped with the gem. It calls a paid model, so it is opt-in: the
8
+ specs drive it with fakes on synthetic data.
9
+
10
+ ```sh
11
+ ruby script/llm_context_live.rb validate TASKS ROOT # no model
12
+ ruby script/llm_context_live.rb run TASKS ROOT --model openrouter:deepseek/deepseek-v4.1-flash \
13
+ [--tasks T1,T3] [--arms none,stale,stale_forget] [--samples 3] [--budget 64000|off] [--cap 8] [--jobs 3] [--limit 9]
14
+ ruby script/llm_context_live.rb report ROOT [--json]
15
+ ruby script/llm_context_live.rb grade|collect TASKS ROOT RUN... # again, for runs already done
16
+ ```
17
+
18
+ `TASKS` (a YAML file) and `ROOT` (where the runs and results go) live outside the repo. They hold task prompts and
19
+ session files, and no session text goes into the repo, scrubbed or not.
20
+
21
+ ## Tasks
22
+
23
+ A task is a real merged fix, replayed from its parent commit. The fix's spec changes are held back and graded
24
+ afterwards.
25
+
26
+ ```yaml
27
+ source_repo: /path/to/samagotchi
28
+ tasks:
29
+ T1:
30
+ fix: 545a3562 # the run's repo is its parent
31
+ size: S
32
+ turns: # one chi turn each; {repo} is the run's repo path
33
+ - "In {repo} … find the cause and plan the fix. Don't edit yet."
34
+ - "Implement the plan and run the specs."
35
+ hidden: [spec/broadcast/triage_spec.rb] # optional: the fix's *_spec.rb
36
+ regression: [spec/broadcast/triage_spec.rb] # optional: the hidden ones' paths
37
+ fix_files: [lib/samagotchi/broadcast/triage.rb, spec/broadcast/triage_spec.rb] # optional: what the fix changed
38
+ ```
39
+
40
+ A file at `hidden/<task id>/<path>` beside the tasks file stands in for that hidden spec. Use it when the fix's spec
41
+ pins a detail that another correct fix would get differently, such as an exact page size; the override keeps only the
42
+ contract.
43
+
44
+ `validate` checks each task's hidden specs: they must fail at the fix's parent and pass at the fix.
45
+
46
+ ## A run
47
+
48
+ Each task × arm × sample is a run in `ROOT/runs/<task>-<arm>-s<n>/`:
49
+
50
+ 1. **prepare:** `git archive <fix>^` into `repo/`, then a fresh git repo with one base commit. It writes chi's config
51
+ in `config/` (the model's host copied from `--hosts-config`, default the user's config.yml; `turn.max_iterations:
52
+ 150`; `recap: false`; `llm_context.apply: payoff`, no `stale_edits`). State goes in `state/`, and it runs `chi
53
+ bundle install loop-guard`. Every chi and rspec command gets `SAMAGOTCHI_ENV=test`, the run's `XDG_STATE_HOME`
54
+ and `XDG_CONFIG_HOME`, and no inherited `SAMAGOTCHI_HOSTS_JSON` (the smoke-run isolation).
55
+ 2. **run (Driver):** `chi send --new --wait --format json --dir repo --model M --llm-context <arm>
56
+ --llm-context-budget <N|off>` with the first turn, then each later turn into the same session. The arms are
57
+ `none`, `stale` and `stale_forget` (`stale,forget`). The tasks use two turns, because forget is offered at turn
58
+ ends. The driver answers questions as a parent agent would:
59
+ - the model's first question gets "use your judgement" (option 1 when it takes no text), and later ones are
60
+ dismissed;
61
+ - an approval is denied;
62
+ - the step-limit question gets Stop: there is no Continue, since the limit is part of the task.
63
+
64
+ A turn waits at most 45 minutes. Afterwards the session is stopped with `chi sessions stop`, then `pkill -f
65
+ <the run's state dir>`. Nothing is ever killed by a pid read from a file.
66
+ 3. **collect:** the numbers below, read before grading, and `diff.patch` (the work against the base commit).
67
+ 4. **grade (Grader):** the hidden specs are copied in as `<name>_p6hidden_spec.rb` and run in one `bundle exec rspec`
68
+ with the regression specs (the model's own copies), then removed. Pass means rspec exits 0 with at least one
69
+ example. The working tree is graded: nothing needs committing.
70
+
71
+ The result goes to `ROOT/results/<run>.json`. A rerun skips runs that have a result, and starts over any run that
72
+ has none.
73
+
74
+ ## Cost
75
+
76
+ Each run's cost (OpenRouter's reported `cost`, summed over the turn records) goes to `ROOT/ledger.jsonl`. No run
77
+ starts once the ledger reaches `--cap` (default $8). With `--jobs N` the runs in flight can pass the cap by what they
78
+ spend. A payment error (chi send's `error_kind` `credits` or `credits_held`, or a 402 / over_budget in the detail)
79
+ ends that run at once and writes `ROOT/STOP`, and no further run starts until someone removes it. The account's own
80
+ key limit stays the hard stop.
81
+
82
+ ## What it scores
83
+
84
+ Per run (`Collector`), from the session file, `analytics.json` and the repo:
85
+
86
+ - **success:** the grade, plus a manual 0/1/2 in `ROOT/grades.yml` (`T1-none-s1: {grade: 2, note: …}`);
87
+ - **steps, tool calls, edits, commits:** `ModelNotesReport::SessionReader`;
88
+ - **reads, re-reads, re-reads after a stub** (by kind): `LLMContextBench::Replay#read_counts`. A re-read reads a
89
+ path the session read before. It counts as after a stub when an earlier read of that path had an applied stub
90
+ whose cause came before it: a forget's cause is its `forget_outputs` call, a stale stub's the read that superseded
91
+ it;
92
+ - **stale stubs, forget calls, forgotten ids and their tokens:** `Replay#stubs` (chars/4);
93
+ - **tokens:** prompt tokens sent (summed over requests), cached, re-prefilled and completion tokens, and the cost,
94
+ from the turn records;
95
+ - **peak context:** the largest prompt of any turn (`prompt_tokens_max` on the turn record);
96
+ - **scope:** distinct files read, files read and files changed outside the fix's set, and diff lines;
97
+ - **wall time.**
98
+
99
+ `report` gives each task × arm its pass count, manual grades, step-limit stops, and every metric's median with its
100
+ min–max spread. It also gives the gate: `stale_forget` against `none`. Per task, a metric is better when
101
+ `stale_forget`'s median is below `none`'s whole spread, worse when above it, and the same otherwise. The gate passes
102
+ when, on both re-reads and scope (files read + files changed outside), the better tasks outnumber the worse, and
103
+ `stale_forget` passed at least as many runs as `none`.
104
+
105
+ ## Limits
106
+
107
+ - Three samples a cell is little. Read nothing into a difference inside one arm's spread.
108
+ - Tokens from `Replay` are chars/4, and those from the turn records are the server's.
109
+ - A Splash host may not report cached tokens or cost (they read 0).
110
+ - The fix's spec file is copied in whole, so its old examples run twice: once hidden and once as the model left
111
+ them.
data/docs/plugins.md CHANGED
@@ -94,8 +94,9 @@ commands it knows.
94
94
 
95
95
  #### `anytime: true`
96
96
 
97
- A normal command waits for its turn: typed while a turn runs, it is refused
98
- as busy (the REPL puts it back into the prompt). An `anytime: true` command
97
+ A plugin's normal command waits for its turn: typed while a turn runs, it is
98
+ refused as busy (the REPL puts it back into the prompt; a plugin command can't
99
+ ask to run after the turn, as the built-in `/model X` does). An `anytime: true` command
99
100
  runs **at once, on its own thread, beside the running turn**, and the turn
100
101
  goes on:
101
102
 
@@ -504,6 +505,7 @@ full label, `plugin.rb (bundle my-bundle)`.
504
505
  | `ctx.repo_root` | the git checkout holding `cwd`, or nil |
505
506
  | `ctx.scratch?` | whether the session is a `chi scratch` one (deleted when it ends) |
506
507
  | `ctx.delegate?` | whether the session is a delegate child: a task another session handed over |
508
+ | `ctx.fork?` | whether the session is a fork (`ctx.sessions.fork`): it has a parent but isn't a delegate child, and started from a conversation, so its first prompt isn't its first user message |
507
509
  | `ctx.model` | the model the session runs on now: its resolved ref (`host:id`), right after `/model` too |
508
510
  | `ctx.model_key` | that model's memory overlay key (`<name>.<key>.md`, what `memory_write current_model_only` writes) |
509
511
  | `ctx.settings` | the bundle's settings, frozen |