samagotchi 0.130.0 → 0.131.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 (38) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +62 -1
  3. data/docs/architecture.md +1 -1
  4. data/docs/cli.md +15 -4
  5. data/docs/configuration.md +4 -1
  6. data/docs/desktop.md +28 -9
  7. data/docs/internals/background-tasks.md +1 -0
  8. data/docs/internals/participants.md +36 -9
  9. data/docs/internals/prompt-caching.md +5 -2
  10. data/docs/sessions.md +4 -2
  11. data/lib/samagotchi/bundles/system/manifest.yml +1 -1
  12. data/lib/samagotchi/cache_confirm.rb +29 -9
  13. data/lib/samagotchi/desktop/macos.rb +99 -19
  14. data/lib/samagotchi/desktop_command.rb +7 -2
  15. data/lib/samagotchi/engine.rb +31 -2
  16. data/lib/samagotchi/guardrails/rules.rb +2 -2
  17. data/lib/samagotchi/guardrails/targets.rb +13 -0
  18. data/lib/samagotchi/level_setting_command.rb +3 -2
  19. data/lib/samagotchi/session.rb +49 -4
  20. data/lib/samagotchi/session_commands.rb +4 -4
  21. data/lib/samagotchi/toolkit_command.rb +2 -2
  22. data/lib/samagotchi/tools/execute.rb +5 -2
  23. data/lib/samagotchi/tools/task_runtime.rb +4 -2
  24. data/lib/samagotchi/update_command.rb +9 -1
  25. data/lib/samagotchi/version.rb +1 -1
  26. data/lib/samagotchi/web/app.rb +47 -8
  27. data/lib/samagotchi/web/participant_review.rb +9 -2
  28. data/lib/samagotchi/web/participant_view.rb +22 -6
  29. data/lib/samagotchi/web/proxy_gate.rb +6 -0
  30. data/lib/samagotchi/web/public/annotate_bar.js +6 -1
  31. data/lib/samagotchi/web/public/app.js +14 -5
  32. data/lib/samagotchi/web/public/participant.js +24 -6
  33. data/lib/samagotchi/web/public/verdict_card.js +48 -4
  34. data/lib/samagotchi/web/public/verdict_fresh.js +8 -4
  35. data/lib/samagotchi/web/public/verify_target.js +73 -0
  36. data/lib/samagotchi/web/token.rb +20 -1
  37. data/lib/samagotchi/worker.rb +7 -0
  38. metadata +2 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5830c4028607020136e2f0493d0d365f5d10889fa8ff2a493b979de7b7e164b9
4
- data.tar.gz: 28f0fda8ba4b824af19381b12274262c0858f879565b58ab4a6a12e18b82f64e
3
+ metadata.gz: 37f2a54062786637083ade10186ebb751a1cfd7a7996540defa03da018b29533
4
+ data.tar.gz: 6836e60f2a87358698adaf000592ac68af5ce8fe8f3c26799825fa26641d3f1a
5
5
  SHA512:
6
- metadata.gz: 38f04438ffc54f5569abbbfb5fdfdd7470ca805926f3593969924a5a248b29ff266f2eec6972aff25d29395982b255b6518989f174a1579b227a711ae1aeec68
7
- data.tar.gz: 3dc45f2617ca67e11f8cf563c83ea1c979142d798d900b000b5f1ae0c12ffb559b692cdec8f8f614da09eeb8cd176a1e7e5af59d47958ddffc838b3faa8f8d9d
6
+ metadata.gz: b908ad2c276d4dabb1c8bd64ad8435a2f9ab5408663243f8cd0f24e25ca2e4647cb356228a7a37027c1c5f2895936c4df8424af5203d8c121b5403577e2a2456
7
+ data.tar.gz: a3c96161fc7cb55e200367417e1dac93442845e0a868ad02d58ecca15a504af6824c419e8188252c9100daf405892be203e75fd6d7baaca1406c20186a4ff6f4
data/CHANGELOG.md CHANGED
@@ -7,6 +7,65 @@ and commands may change between minor versions. How releases are made:
7
7
  [docs/releasing.md](docs/releasing.md).
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.131.0] - 2026-10-29
11
+
12
+ ### Changed
13
+
14
+ - Cache: confirmations for prompt-cache changes estimate the reread size when known and warn that Anthropic drops
15
+ preserved thinking after a full-prefix change.
16
+
17
+ ### Fixed
18
+
19
+ - Web: a Verify verdict card sits right after the answer it quotes, on your page and a participant's, instead of
20
+ under the turn's prompt, screens above the claim in a long turn. A quote the page can't find yet puts the card at
21
+ the turn's end.
22
+ - Web: Verify on a claim in a model's reply on a participant's page records its verdict and shows the card again;
23
+ the claim was sent without its turn, so the check ran as a plain comment.
24
+ - Web: after a fresh verdict lands on an unclear card, its button reads **Verify fresh again**: each press starts
25
+ another paid check (a participant's counts against their cap).
26
+ - Web: Verify is offered only on a finished turn's final answer, on your page and a participant's: the one place a
27
+ verdict card can come from. Selecting an earlier reply, a step, a message, a line added mid-turn or a turn still
28
+ running no longer offers a Verify that silently ran as a plain comment; the bar keeps its other actions there.
29
+ - Web: on a participant's page, a turn a delegate's report or a context note woke opens as "An automatic turn"
30
+ (still without the report or note). Its answer read as one more reply of the turn before: Verify was offered on
31
+ it and refused, and hidden on that turn's real final answer.
32
+ - Web: Verify on a claim that names a file in the session's folder or your home folder (or holds a key) records its
33
+ verdict: the pages quote such a path relative, or with `~`, and the check compared it against the answer's full
34
+ path, so it ran as a plain comment with no card.
35
+
36
+ Update with `chi update`, then restart `chi web` and the session workers (`chi sessions restart`).
37
+
38
+ ## [0.130.1] - 2026-10-29
39
+
40
+ ### Fixed
41
+
42
+ - Sessions started from the desktop helper find the tools your terminal finds (mise, Homebrew, `codex`):
43
+ `chi desktop install` writes your shell's PATH into the helper's launch file, `chi update` adds it to a
44
+ launch file that has none (it never changes a baked one; `chi desktop install --force` does), and
45
+ `chi desktop status` shows it. Run `chi update` once from your terminal, then restart the sessions the
46
+ panel started.
47
+ - `chi update` no longer repoints the desktop helper at the Ruby or chi it runs from, nor refreshes the launch file
48
+ from an older chi: only `chi desktop install --force` and `chi desktop upgrade` pick the current shell's Ruby,
49
+ chi and PATH. When the helper's Ruby or chi is gone, `chi update` says so and names that command.
50
+ - An older chi that saves a session a newer chi wrote (waking it from a project whose Ruby has an older chi, say)
51
+ keeps the session's fields it doesn't know instead of dropping them, and the session file names the chi that saved
52
+ it last (`written_by`; a worker woken on a file a newer chi saved logs `newer_writer`). This works from this release
53
+ on: chi 0.130.0 and older still drop them, so update the chi in every Ruby you run it from.
54
+ - `execute` (and a user's `!cmd`) no longer hands a project's commands chi's own Bundler setup: run under
55
+ `bundle exec`, chi passed on `RUBYOPT=-rbundler/setup` and its `BUNDLE_GEMFILE`, so `ruby -e` or `bundle exec`
56
+ in another project loaded chi's Gemfile. It now scrubs them the way `task_create` always has.
57
+
58
+ ### Security
59
+
60
+ - On the LAN (`web.host: lan`), the cookie chi web sets for this machine's 127.0.0.1 no longer holds the access
61
+ token: a server on another local port gets every 127.0.0.1 cookie, so a dev server that logs headers caught the
62
+ token. It now holds a value that works only as that cookie on this machine and ends with `chi web --new-token`.
63
+ An open 127.0.0.1 tab lands on the 401 page once: open the `Local:` link. If you ran a participant's pair turns
64
+ with dev servers on 0.130.0, run `chi web --new-token`. The LAN address's cookie (your phone's) still holds the
65
+ token.
66
+
67
+ Update with `chi update`, then restart `chi web` and the session workers (`chi sessions restart`).
68
+
10
69
  ## [0.130.0] - 2026-10-29
11
70
 
12
71
  ### Changed
@@ -4933,7 +4992,9 @@ and long-lived sessions.
4933
4992
  - A macOS desktop helper (`chi desktop install`): a "Send to chi" Service and a
4934
4993
  hotkey panel that send selected text or the clipboard to your sessions.
4935
4994
 
4936
- [Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.130.0...HEAD
4995
+ [Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.131.0...HEAD
4996
+ [0.131.0]: https://github.com/dm1try/samagotchi/compare/v0.130.1...v0.131.0
4997
+ [0.130.1]: https://github.com/dm1try/samagotchi/compare/v0.130.0...v0.130.1
4937
4998
  [0.130.0]: https://github.com/dm1try/samagotchi/compare/v0.129.0...v0.130.0
4938
4999
  [0.129.0]: https://github.com/dm1try/samagotchi/compare/v0.128.0...v0.129.0
4939
5000
  [0.128.0]: https://github.com/dm1try/samagotchi/compare/v0.127.0...v0.128.0
data/docs/architecture.md CHANGED
@@ -153,7 +153,7 @@ agent logic and can be used without any terminal rendering; the UI is a thin lay
153
153
  | UI | `Samagotchi::TerminalUI` | Interactive REPL (Reline), rendering (ANSI, spinner, status line), REPL commands. Delegates all core work to an `Engine`. |
154
154
  | Model loops and adapters | `KernelLoop`, `LLM::ChatLoop`, `Samagotchi::Client`, `LLM::OpenAIChat`, `LLM::AnthropicMessages`, `LLM::HTTP` | The model↔tool loops and the HTTP adapters they talk through (see "Model loops and adapters"). |
155
155
  | Bridge (SSE/HTTP) | `Samagotchi::Bridge`, `SessionManager` | The **single live client transport**: an SSE read stream + HTTP POST turn/cancel/answer surface that attaches to a worker's existing `Engine` via `Engine#subscribe`. Every session worker starts it (bound `127.0.0.1`, localhost-only), and it answers only a request that names its secret (`X-Chi-Bridge`, from the worker's 0600 `bridge.json`). |
156
- | Web (Rack) | `Samagotchi::Web::App`, `SessionManager` | Single-port `127.0.0.1:4567` control plane via `rack`+`webrick` (serve `index.html` + `/api/*`). With `web.host: lan` (or one of this machine's IPv4 addresses) the server also listens on that address, where every request needs the access token, this machine's on loopback too (`App#token_gate`: the `chi_token` cookie or `Authorization: Bearer`; chi web prints a `Local:` link with a one-time code; [CLI](cli.md#chi-web-on-your-phone)). Experimental: behind an auth proxy (`web.public_url`, `Web::ProxyGate`) every request needs the token, this machine's too, and one that came through the proxy also the configured identity header for the owner ([configuration.md](configuration.md#behind-an-auth-proxy)). A request goes through, in order: the Host check (`local_host_header?`: loopback names, the LAN address, the public host), the cross-site check (`cross_site?`: Origin this server's or the public origin, no `Sec-Fetch-Site` cross-site/same-site on a POST or `/api`), the frame check (`framed?`: a frame/embed/object load from another origin, 403 `framed`; every HTML answer carries `X-Frame-Options: DENY` and `frame-ancestors 'none'`), the token gate (`token_gate` → `principal_for`, `proxied_principal` for a proxied request; the `?token=` / `POST /api/token` trade sets the cookie, `__Host-chi_token` through the proxy; with `web.public_url` a direct request takes Bearer only, and with it or LAN mode chi's token-less `/api/info` probe gets `probe_info`), then the route's permission (`Principal#permits?`). It talks to each worker's Bridge (`App#relay`, `bridge_get`; `/stream` proxies it) and reads the session files; no `Engine` of its own. Markdown (`web.markdown`) is rendered here only (`Web::MarkdownRenderer`): the answers it serves, card bodies, and `POST /api/render` for a text the page already shows. `App` keeps the gate, `ROUTES` and the helpers the handlers share; its handlers live in mixins it includes, each a `Web::` module with private methods: `SessionAccessRoutes` (`web/session_access_routes.rb`), `ParticipantAccess` (`web/participant_access.rb`), `ParticipantsAdmin` (`web/participants_admin.rb`), `ParticipantReview` (`web/participant_review.rb`), `MessageDisplay` (`web/message_display.rb`), `SessionShow` (`web/session_show.rb`), `SessionCreate` (`web/session_create.rb`), `SessionControl` (`web/session_control.rb`), `PushRoutes` (`web/push_routes.rb`) and `ProxyGate` (`web/proxy_gate.rb`). Exact paths in `GATE_FREE_PATHS` are served before the token gate (the fragment trade's script, and the Home Screen app's manifest, service worker and icons). `bin/chi web` entrypoint. |
156
+ | Web (Rack) | `Samagotchi::Web::App`, `SessionManager` | Single-port `127.0.0.1:4567` control plane via `rack`+`webrick` (serve `index.html` + `/api/*`). With `web.host: lan` (or one of this machine's IPv4 addresses) the server also listens on that address, where every request needs the access token, this machine's on loopback too (`App#token_gate`: the `chi_token` cookie, on loopback a value derived from the token (`Token.loopback_cookie`), or `Authorization: Bearer`; chi web prints a `Local:` link with a one-time code; [CLI](cli.md#chi-web-on-your-phone)). Experimental: behind an auth proxy (`web.public_url`, `Web::ProxyGate`) every request needs the token, this machine's too, and one that came through the proxy also the configured identity header for the owner ([configuration.md](configuration.md#behind-an-auth-proxy)). A request goes through, in order: the Host check (`local_host_header?`: loopback names, the LAN address, the public host), the cross-site check (`cross_site?`: Origin this server's or the public origin, no `Sec-Fetch-Site` cross-site/same-site on a POST or `/api`), the frame check (`framed?`: a frame/embed/object load from another origin, 403 `framed`; every HTML answer carries `X-Frame-Options: DENY` and `frame-ancestors 'none'`), the token gate (`token_gate` → `principal_for`, `proxied_principal` for a proxied request; the `?token=` / `POST /api/token` trade sets the cookie, `__Host-chi_token` through the proxy; with `web.public_url` a direct request takes Bearer only, and with it or LAN mode chi's token-less `/api/info` probe gets `probe_info`), then the route's permission (`Principal#permits?`). It talks to each worker's Bridge (`App#relay`, `bridge_get`; `/stream` proxies it) and reads the session files; no `Engine` of its own. Markdown (`web.markdown`) is rendered here only (`Web::MarkdownRenderer`): the answers it serves, card bodies, and `POST /api/render` for a text the page already shows. `App` keeps the gate, `ROUTES` and the helpers the handlers share; its handlers live in mixins it includes, each a `Web::` module with private methods: `SessionAccessRoutes` (`web/session_access_routes.rb`), `ParticipantAccess` (`web/participant_access.rb`), `ParticipantsAdmin` (`web/participants_admin.rb`), `ParticipantReview` (`web/participant_review.rb`), `MessageDisplay` (`web/message_display.rb`), `SessionShow` (`web/session_show.rb`), `SessionCreate` (`web/session_create.rb`), `SessionControl` (`web/session_control.rb`), `PushRoutes` (`web/push_routes.rb`) and `ProxyGate` (`web/proxy_gate.rb`). Exact paths in `GATE_FREE_PATHS` are served before the token gate (the fragment trade's script, and the Home Screen app's manifest, service worker and icons). `bin/chi web` entrypoint. |
157
157
  | Sessions | `Samagotchi::Session`, `SessionManager` | File-based `~/.local/state/samagotchi/sessions/<uuid>.json` + sidecar `input/`/`output/`; retention (14d/500) + ordering (`updated_at desc`). |
158
158
 
159
159
  - `bin/chi` in REPL mode (see `LaunchMode` above) builds `TerminalUI`. `TerminalUI#run` is the single
data/docs/cli.md CHANGED
@@ -290,8 +290,12 @@ done
290
290
  one that failed before; one you uninstalled stays out. See [Bundle
291
291
  profiles](memory.md#bundle-profiles-core-and-dev).
292
292
  - **The desktop helper** (macOS) is rebuilt and restarted only when its Swift
293
- sources changed (or the Ruby it runs moved); otherwise only its launch file
294
- is refreshed. See [Desktop helper](desktop.md).
293
+ sources changed; otherwise only its launch file is refreshed (a newer chi or
294
+ other terminal settings). It keeps the Ruby, chi and PATH the helper runs (a
295
+ PATH is filled in only when the file has none): `chi desktop install --force`
296
+ and `chi desktop upgrade` bake this shell's. A helper whose Ruby or chi is gone
297
+ is reported as failed, naming `chi desktop install --force`. See
298
+ [Desktop helper](desktop.md).
295
299
  - **Running processes** are reported, never stopped. A live worker keeps its
296
300
  chi until it is restarted: `chi sessions restart ID` (one from before
297
301
  restarts: `chi sessions stop ID`, and the next prompt starts it on the new
@@ -1210,7 +1214,9 @@ pills there (a review turn refuses `/` lines).
1210
1214
 
1211
1215
  After the presets, both pages have a built-in **Verify**: the quote, then
1212
1216
  an ask to check the claim against the code and end on a line `Verdict:
1213
- confirmed|refuted|unclear — <reason>`. A session whose worker predates Verify (started by an older chi) refuses it (409
1217
+ confirmed|refuted|unclear — <reason>`. Verify is offered only on text in a finished turn's final answer, the
1218
+ claim a verdict card can come from; elsewhere (an earlier reply, a step, a message, a line added mid-turn, a turn
1219
+ still running) the bar keeps its other actions. A session whose worker predates Verify (started by an older chi) refuses it (409
1214
1220
  `verify_unavailable`); a participant's page disables Verify and Verify fresh with a hint to restart the session. On your page a selection in a turn
1215
1221
  also offers **Copy link**: a link to that turn and quote for a participant's
1216
1222
  browser (the one their review link was opened in), which opens their page
@@ -1271,7 +1277,12 @@ access token", paste the token there (the part of the link after
1271
1277
  paste the token) and the API answers 401. On this Mac open the `Local:`
1272
1278
  link (`chi web --open` opens it): its one-time code (10 minutes, once) sets
1273
1279
  the cookie for 127.0.0.1 without the token in the address; the LAN link
1274
- works here too. chi's own probe (`GET /api/info` with no token or cookie)
1280
+ works here too. The cookie for 127.0.0.1 holds a value derived from the
1281
+ token, not the token: the browser sends it to a server on any other local
1282
+ port too (a dev server that logs headers), and that server can't use it
1283
+ from the network (unless something forwards it to this machine's
1284
+ loopback), as Bearer, or as the token; `chi web --new-token` ends it. The LAN address's cookie (your phone's, or this Mac's when it opens
1285
+ the LAN link) is the token itself. chi's own probe (`GET /api/info` with no token or cookie)
1275
1286
  gets a few fields (version, pid, features, the LAN address, the start
1276
1287
  folder), nothing of the sessions.
1277
1288
  - The token lives in `$XDG_STATE_HOME/samagotchi/web-token` (0600).
@@ -838,7 +838,10 @@ Gemma's thinking switch, a remote chat host's thinking fields ("Thinking"), a ch
838
838
  reply has thinking ("Reasoning history"), and any change to the declared tools, which start every request
839
839
  ("Toolkits"). They ask only when what the next turn runs under really changes and the session has a model reply.
840
840
 
841
- - The reply says what it costs and changes nothing yet: `Nothing changed yet: send /thinking low --yes to apply.`
841
+ - The reply says what it costs and changes nothing yet. When known, it estimates the reread size: the whole
842
+ conversation for full-prefix changes, or from the first changed reply for reasoning history. On `api: anthropic`,
843
+ a full-prefix change also says preserved thinking in replies is dropped from then on.
844
+ - `Nothing changed yet: send /thinking low --yes to apply.` names the line that applies it.
842
845
  - `--yes` (or `-y`, anywhere after the command) applies it at once.
843
846
  - The terminal (the REPL and an attached TUI) puts the `--yes` line in the next input: Enter applies it.
844
847
  - The web's reply bubble has an **Apply (restarts the cache)** button. The session bar's chips are already an explicit
data/docs/desktop.md CHANGED
@@ -194,7 +194,8 @@ grant:
194
194
 
195
195
  `tmux.agents` picks the programs listed (kitty.agents' default; `"*"` lists every pane), `tmux.binary` the tmux
196
196
  to run (default: the `tmux` on your shell's PATH when the launch file is written, e.g. `/opt/homebrew/bin/tmux`;
197
- the helper itself has no PATH). See [Configuration](configuration.md#desktop-helper-agents-in-kitty-and-tmux).
197
+ the helper itself keeps macOS's default PATH, and tmux is resolved when the file is written). See
198
+ [Configuration](configuration.md#desktop-helper-agents-in-kitty-and-tmux).
198
199
  2. Run `chi update` (or `chi desktop upgrade`), and again after every edit of `tmux:`. With no tmux found they say
199
200
  "tmux.enabled but no tmux found on PATH: set tmux.binary", and the panel "tmux not found…".
200
201
 
@@ -256,9 +257,16 @@ don't break it. From a checkout it runs **that** checkout's `bin/chi`; installin
256
257
  warning, because the helper stops working once that worktree is removed. After switching between a checkout and a
257
258
  gem install, run `chi desktop upgrade` from the one you now use.
258
259
 
260
+ The helper runs the Ruby and chi of **the shell you installed from**, and only `chi desktop install --force` and
261
+ `chi desktop upgrade` change that: they pick the Ruby and chi of the shell you run them in. `chi update` never
262
+ repoints it, so running it from a project with another Ruby (or an older chi) leaves the helper where it was. If
263
+ the Ruby or chi the helper runs is gone (a Ruby upgrade, a removed worktree), `chi update` fails the helper's row
264
+ with "chi not found at …: run chi desktop install --force from the shell whose chi it should run".
265
+
259
266
  `chi update` keeps it current: it rebuilds and restarts the helper only when its Swift sources changed since the
260
- build (`launch.json` records their digest) or the Ruby it runs moved. A new chi that left the sources alone only
261
- rewrites `launch.json`, which the app reads at each send, so the app keeps its older version number and that's fine.
267
+ build (`launch.json` records their digest), keeping the Ruby, chi and PATH it runs. A newer chi that left the
268
+ sources alone only rewrites `launch.json` (its version and settings), which the app reads at each send, so the app
269
+ keeps its older version number and that's fine. An older chi than the one that wrote the file changes nothing.
262
270
 
263
271
  ## Commands
264
272
 
@@ -267,7 +275,7 @@ rewrites `launch.json`, which the app reads at each send, so the app keeps its o
267
275
  | `chi desktop install [--force] [--login]` | builds, installs and starts it; `--force` replaces an existing copy |
268
276
  | `chi desktop upgrade` | rebuilds it for this chi and restarts it, keeping the login setting (always; `chi update` does it only when needed) |
269
277
  | `chi desktop uninstall` | quits it and removes the app, its login item, launch file and settings |
270
- | `chi desktop status` | version against chi's, how it runs chi, state dirs, Service, hotkey, process, login item, host keys |
278
+ | `chi desktop status` | version against chi's, how it runs chi, state dirs, PATH, Service, hotkey, process, login item, host keys |
271
279
 
272
280
  `chi self` has a `desktop` line: `0.1.x (matches)`, `0.1.w (up to date for chi 0.1.x)` (an older build whose sources
273
281
  haven't changed), `0.1.w (chi is 0.1.x: chi update)` (a rebuild is due) or `not installed`. `chi desktop status`
@@ -278,17 +286,24 @@ says the same.
278
286
  Apps started by macOS get a bare environment: no shell rc files, so no rbenv/chruby/mise/asdf and none of your
279
287
  exports. So `install` writes `~/Library/Application Support/Chi Helper/launch.json` with:
280
288
 
281
- - the absolute path of the Ruby running chi and of chi itself (the gem's wrapper, or a checkout's `bin/chi`);
289
+ - the absolute path of the Ruby running chi and of chi itself (the gem's wrapper, or a checkout's `bin/chi`), as of
290
+ the last `chi desktop install` or `upgrade` (`chi update` keeps them);
282
291
  - `LANG=en_US.UTF-8`;
283
292
  - only these variables, and only when they are set: `XDG_CONFIG_HOME`, `XDG_STATE_HOME`, `GEM_HOME`, `GEM_PATH`,
284
293
  `RUBYLIB` (a relative `XDG_*` dir made absolute from the install's current folder: the helper starts in `/`).
285
294
  No tokens and no `SAMAGOTCHI_*` settings go in;
295
+ - `PATH`: the shell's PATH when the file is written, with only absolute folders, in order, each once. The helper's
296
+ chi, the sessions it starts and their `execute` commands can find the tools your terminal finds (mise, Homebrew,
297
+ `codex`, `node`);
286
298
  - the settings the panel needs from config.yml, as chi read them then: the `kitty:` section (when
287
299
  `kitty.listen_on` is set), the `tmux:` section (when `tmux.enabled`: the tmux binary and the socket dir resolved
288
300
  then, and the agents) and `broadcast_timeout` (`broadcast.triage_deadline` + 10 s).
289
301
 
290
- The file freezes the install shell's values. If you install with a temporary `XDG_STATE_HOME`, the helper keeps
291
- using it. `status` prints the baked dirs.
302
+ The file freezes the install shell's values, including PATH. If you install with a temporary `XDG_STATE_HOME`, the
303
+ helper keeps using it. `chi update` fills in a PATH only when the file has none (as in a launch file written before
304
+ this change); it never changes an existing baked PATH. To change it (for example, after a mise upgrade changes the
305
+ versioned folders on PATH), run `chi desktop install --force` or `chi desktop upgrade` from the shell whose PATH
306
+ you want. `status` prints the baked state dirs and PATH.
292
307
 
293
308
  **Host API keys** don't go in the launch file either: with the variable a host's `api_key_env:` names unset, chi
294
309
  reads the key from the login Keychain (service `chi`, account the variable's name; see
@@ -331,8 +346,12 @@ broadcast` says the note may be partly delivered.
331
346
 
332
347
  ## Troubleshooting
333
348
 
334
- - **"chi not found at …, run `chi desktop upgrade`"**: the Ruby or checkout in `launch.json` moved (a Ruby upgrade,
335
- a removed worktree). Run `chi desktop upgrade` (or `chi update`) from the chi you use now.
349
+ - **"chi not found at …"**: the Ruby or checkout in `launch.json` moved (a Ruby upgrade, a removed worktree).
350
+ `chi update` won't repoint it: run `chi desktop install --force` (or `chi desktop upgrade`) from the shell whose
351
+ chi the helper should run.
352
+ - **A session started from the panel can't find a tool (`codex`, `node`):** `chi desktop status` shows the PATH it
353
+ gets. With none, run `chi update`; to change it, run `chi desktop install --force` from your terminal. Then
354
+ restart that session (`chi sessions restart ID`).
336
355
  - **No "Send to chi" in the Services menu:** check `chi desktop status` (service). Try
337
356
  `/System/Library/CoreServices/pbs -update`, start the app again, or log out and back in. It must be ticked in
338
357
  System Settings → Keyboard → Keyboard Shortcuts… → Services → Text.
@@ -42,6 +42,7 @@ Behavior:
42
42
  again or stop it (`TaskWait::MESSAGE_WAITING_NOTE`), and `waited: Ns`; the task keeps running and stays watched
43
43
  (below). A task that finished meanwhile gives its finished result, as after a Stop. A `chi -p` turn has no probe.
44
44
  - `task_create` accepts `env` as a JSON object string for deterministic overrides such as `PATH`; use an absolute interpreter path when that is simpler. Ruby/Bundler isolation variables remain protected.
45
+ - `execute` and `task_create` both spawn with chi's own Ruby/Bundler setup scrubbed (`TaskRuntime.sanitized_spawn_env`): `RUBYOPT` loses its `-r…bundler/setup` flags (others stay) and `RUBYLIB`, `BUNDLE_GEMFILE`, `BUNDLE_BIN_PATH`, `BUNDLER_VERSION` are unset, so a project's `ruby -e` or `bundle exec` uses its own Gemfile when chi runs under `bundle exec`. `task_create` refuses those keys in the model's `env`; `execute`'s `env` comes from chi's callers (`Builtins`, `!cmd`), not the model, and is set over the scrub. An `execute` handed over at its limit keeps the process it spawned, so the task runs with the same env.
45
46
  - `execute` and `task_create` export chi's own markers into the command, set over the model's `env` (which can't set them: `Error: env key is reserved`): `SAMAGOTCHI_PARENT_SESSION` (the session's id, `chi` without one; a chi started there answers as a parent agent), `SAMAGOTCHI_SESSION_MODEL` (the resolved ref the session runs on, read at each call so it follows `/model`; unset when unknown) `SAMAGOTCHI_SESSION_THINKING` (the session's own thinking level; unset when it has none) and `SAMAGOTCHI_SESSION_REASONING_HISTORY` (its own reasoning_history level, the same way). `chi self` reads the last three for its model, thinking level and reasoning history. The four are `ChildEnv::SESSION_MARKERS` (`TaskRuntime::MARKER_ENV_KEYS`), which a spawned worker unsets (`ChildEnv.unset`, merged into its spawn env). A session's worker never inherits them from whoever started it (a `chi send` run by another session's `execute`): its other commands (a user's `!cmd`, an MCP server, a context command) have none.
46
47
  - They also pass on a set `XDG_STATE_HOME` / `XDG_CONFIG_HOME` made absolute (`Builtins.xdg_absolute`, via `Paths`), so a chi the command runs in another `cwd` (`chi send`) reads this chi's state and config. These aren't reserved: an XDG dir the model sets in `task_create`'s `env` stays its own.
47
48
 
@@ -122,7 +122,13 @@ participant** and **one role**, until it expires or is revoked.
122
122
  the detail (only the requester's own identity). Unproxied, every request needs a token too (no "loopback is the
123
123
  owner"): rule 3 doesn't apply.
124
124
  1. The owner's token (Bearer, then the cookie; `?token=` on the page's trade): the **owner**. Checked first, so the
125
- owner's requests never touch the grants store (measured: no change to an owner route's time).
125
+ owner's requests never touch the grants store (measured: no change to an owner route's time). On a LAN chi web
126
+ (no `web.public_url`) a loopback peer's owner cookie is `Token.loopback_cookie` instead (`chl_`, an HMAC of a
127
+ label keyed by the token, `App#loopback_cookie_peer?` / `#loopback_cookie`): it stands for the token only as that
128
+ cookie from a loopback peer, never as Bearer, a trade or from the LAN address, and `chi web --new-token` ends it.
129
+ The token itself as a loopback cookie (set by 0.130.0) is refused, and its 401 clears it (`#refused_token`). The
130
+ token as Bearer on loopback is the owner as before (SSH tunnels, `chi --remote` over `ssh -L`, curl, the e2e
131
+ harness).
126
132
  2. A `chp_` token (`Grants.token?`) whose grant is active: a **participant** (`Principal.new(kind: :participant,
127
133
  grant:)`), from any peer: loopback and a chi web not on the LAN too. An ended or unknown one is refused
128
134
  (below), never the owner.
@@ -134,7 +140,10 @@ participant** and **one role**, until it expires or is revoked.
134
140
  (`Server.local_owner_link`), the fragment trade sets the cookie there. `PairCommand` still denies a pair command
135
141
  naming chi web on a loopback name (defense in depth), and URL openers and listeners: a page opened in the owner's
136
142
  browser at a port the shell listens on would catch the owner's cookie (one on 127.0.0.1 or the LAN address goes
137
- to every port).
143
+ to every port: on 127.0.0.1 it holds the loopback value of rule 1, usable only as that cookie from a loopback peer with no proxy trace (anything that can reach loopback, such as an `ssh -L` user or code running as the owner, can still replay it); on
144
+ the LAN address it is the token, a known gap, as a cookie there is replayable from any LAN device anyway). A
145
+ server on another local port can also answer `Set-Cookie: chi_token=…` and overwrite the owner's cookie (cookie
146
+ tossing: it logs the owner out, or swaps in a grant's token; visible, nothing leaks; not fixed).
138
147
  4. Anything else: nil. `#token_gate` answers a bound grant's token without its identity **401 `token_bound`**, an
139
148
  ended grant's token **401 `token_revoked`** or **401
140
149
  `token_expired`** (the page says the same words as a sentence: "This review link was revoked by the session's
@@ -143,7 +152,8 @@ participant** and **one role**, until it expires or is revoked.
143
152
 
144
153
  The `?token=` trade (and `POST /api/token`, with a link's one-time `{code}` or the 401 page's pasted `{token}`; with
145
154
  `web.public_url` only through the proxy, as a direct request takes Bearer only) sets the `chi_token` cookie (`__Host-chi_token`, Secure, on a proxied request) to the token that was
146
- presented, the owner's or the participant's
155
+ presented, the owner's or the participant's (the owner's loopback value instead, on a LAN chi web's loopback peer,
156
+ `App#owner_cookie_value`)
147
157
  (one at a time per browser: a participant link opened in the owner's browser replaces the owner's cookie there, and
148
158
  makes that browser the participant, on loopback too). A grant's token is traded on any peer; the owner's only where
149
159
  the gate asks for it (on the LAN, this machine's loopback too). On a chi web the gate doesn't ask (loopback only), the
@@ -365,7 +375,10 @@ fields: a field added to an event later reaches nobody until it is named there.
365
375
  so the owner's `!` line and its output, a delegate's report (a wake turn's too), and the notes chi adds (a declined
366
376
  Continue) never show. A turn no person sent (its record's `automatic`, `SessionMetrics`: a task's or a plugin's,
367
377
  read from analytics.json and the live worker's records; the running turn's by its start; a delegate child's first
368
- turn, its parent's task) shows as an automatic turn without its prompt, as the stream does. A raw loop's saved
378
+ turn, its parent's task) shows as an automatic turn without its prompt, as the stream does; so does a wake turn's
379
+ start (`.wake_start`: the delegate report or the context note a wake turn opens with is dropped, its `turn_id` and
380
+ `at` kept), so its answer is never read as one more reply of the turn before (Verify, verdict cards and claim
381
+ links group by `[data-turn-id]`). A raw loop's saved
369
382
  reply is cut before the display reads it (`.visible_raw`): what came before a bare `</think>` and everything from
370
383
  an unclosed thought or tool call on, which the display would only untag. Each
371
384
  prompt carries its author and an image count; replies their text and their step's tool rows. Notes, steers and
@@ -418,6 +431,13 @@ fields: a field added to an event later reaches nobody until it is named there.
418
431
  After the presets comes the built-in **Verify** (`verify_preset.js`, on the owner's page too): the quote, then
419
432
  "Verify this claim against the code. If this session's context doesn't settle it, say so. End with a line
420
433
  `Verdict: confirmed|refuted|unclear — <one-line reason>`." It fills the box like any preset and never sends.
434
+ It is offered only where the Engine will take the claim (`verify_target.js`, the bar's `offeredPreset`): a
435
+ selection in the final answer of an ended turn, the last answer bubble (the owner's `.bubble.output` that isn't a
436
+ delegate report's reply; a participant's last `.p-msg.reply > .p-bubble`) between the turn's opening
437
+ (`[data-turn-id]`) and the next one (or, on the owner's page, a context note: a wake turn opens with one; a
438
+ participant's page gets a wake turn's opening as an automatic turn).
439
+ Anywhere else (an earlier reply, a step, a prompt, a line merged mid-turn, the Plan, the running turn) the button
440
+ is hidden and the bar keeps its other actions.
421
441
  - **Deep links to a claim** (`claim_link.js`). The owner selects text in a turn's answer, step or own message and
422
442
  Annotate's **Copy link** copies `<base>/#s=<id>&t=<turn8>&q=<quote>&p=<prefix>`: `base` the participants list's
423
443
  `link_base` (`ParticipantsAdmin#participant_base`: `web.public_url`'s origin, else the LAN address, else the Host),
@@ -444,13 +464,18 @@ fields: a field added to an event later reaches nobody until it is named there.
444
464
  with the submitted turn, without changing its prompt text. A Verify turn that fails fast and is rolled back hands
445
465
  the claim back with its prompt (`prompt_restored`'s `verify`), so the owner's resend from the composer is a Verify
446
466
  turn again. Before the Engine stamps the anchor or records a verdict, it checks that the source turn exists and
447
- its assistant reply contains the quote. Matching is case-sensitive, collapses whitespace, and strips common
467
+ its final assistant reply (`AnswerTail`: the turn's last assistant message with text) contains the quote. Matching is case-sensitive, collapses whitespace, and strips common
448
468
  Markdown presentation (links, headings, list/blockquote prefixes, emphasis and code delimiters) from both sides;
449
- this matches the rendered text the page selects without treating different casing as the same claim. A fresh child
469
+ this matches the rendered text the page selects without treating different casing as the same claim. The quote may
470
+ also be in the answer as the pages show it (`Engine#verify_quote_in?`, `ParticipantView::Redact.text`: the session's
471
+ folder relative, home `~`, secret shapes `[redacted]`): a participant's page has only that text, and the owner's
472
+ page quotes it the same way (`claimOpts()`, `normalizeClaim`; `spec/fixtures/redact_cases.json` keeps the two
473
+ sides alike), so a claim naming a path is taken on both. A fresh child
450
474
  checks the same anchor against its parent's history. A completed Verify reply whose final non-empty line has
451
475
  a valid `Verdict: confirmed|refuted|unclear — <reason>` appends a verdict to the session. Both pages place its card
452
- after the source turn, name its author, show the reason and quote, and link to the checking turn; a live completion
453
- adds the card without a reload. The participant view receives only the card fields through its allowlist, with the
476
+ in the source turn, right after the bubble that holds the quote (the answer's, matched as a claim link reads the
477
+ page; a quote not found yet goes at the turn's end), name its author, show the reason and quote, and link to the
478
+ checking turn; a live completion adds the card without a reload. The participant view receives only the card fields through its allowlist, with the
454
479
  quote and reason redacted by the view's normal rules, never the full reply.
455
480
  - **Verify fresh** (`VerifyFresh`, `verdict_fresh.js`). An `unclear` card has a **Verify fresh** button (the owner's
456
481
  page always; a participant's while `can_comment`, i.e. while their grant could run a turn now; never in the owner's
@@ -510,7 +535,9 @@ fields: a field added to an event later reaches nobody until it is named there.
510
535
  `:verdict_recorded`; chi web shows the ones still waiting (`VerifyFresh.verdicts_of`: the summary, the review
511
536
  data). The card shows "Verifying fresh…" while the child runs (the summary's `verify_fresh` `{of, child_id}`,
512
537
  the review data's `fresh_running`, ids only) and the fresh verdict as a `fresh: ✅ confirmed — …` line on the same
513
- card. The participant view passes `fresh` and `of`, never the child's id (a participant can't open it). No push
538
+ card. The original stays unclear, so the button stays, as **Verify fresh again**: each press is another paid
539
+ child (a participant's against their cap). The participant view passes `fresh` and `of`, never the child's id (a
540
+ participant can't open it). No push
514
541
  for a fresh verdict yet (`Attention` pushes a verdict at a turn's end).
515
542
  - **The comment box** is the owner's composer (`composer_mount.js`, styled by the shared `composer.css`), mounted by
516
543
  `participant.js` with a subset on. It has: the height that grows with the text, the grip (drag to resize,
@@ -232,8 +232,11 @@ is prefilled once. The native and the chat loop use the same plan.
232
232
  classifier: `Thinking.cache_cost` (`:full` for Gemma, `:provider` for a remote chat host, `:tail` otherwise) and
233
233
  `ReasoningHistory.cache_cost` (`:history` for a change to or from `all` once an earlier reply has thinking).
234
234
  `CacheConfirm.needed?` asks only for `:full`, `:provider` and `:history`, when the effective level changes and
235
- the session has a model reply. A new setting with a cost (a `/model` across templates, `/llm-context` turning
236
- forget on) adds its classifier and goes through the same step.
235
+ the session has a model reply. The confirmation estimates the reread size from the latest known context count:
236
+ the whole conversation for `:full` / `:provider`, or up to the first changed reply for `:history`; it omits the
237
+ estimate when no count is known. On `api: anthropic`, a `:full` / `:provider` change also warns that preserved
238
+ thinking in replies is dropped from then on. A new setting with a cost (a `/model` across templates,
239
+ `/llm-context` turning forget on) adds its classifier and goes through the same step.
237
240
  7. **No wall-clock time rendered at wire time.** A history entry's `at` (`MessageTime::KEY`, when it happened) is
238
241
  display-only: no wire format reads it, and a guard spec checks that `ChatLoop#wire_messages` and
239
242
  `Prompt.format_with_images` give the same bytes with and without it (`spec/message_time_history_spec.rb`). A
data/docs/sessions.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Sessions
2
2
 
3
- Sessions are plain files — no DB. Each session is `~/.local/state/samagotchi/sessions/<uuid>.json` (XDG-aware via `XDG_STATE_HOME`) plus a sidecar dir `<uuid>/` with `input/`/`notes/`/`output/`/`owner.lock`/`bridge.json` (`Session.session_dir`), and `tool_output/` once a long `execute` result was cut: its whole output, which the cut result names (`ToolOutputFiles`, the newest 50 kept; see [Tool output guardrails](internals/tool-guardrails.md#the-runners-cut-max_tool_output_chars)). Besides the messages and turn state, the JSON keeps `used_memory_names` (what the session read), `preloaded_memory_names` (`--memory`) and `muted_memory_names` (`--mute`), the last two written before the worker starts so a respawn builds the same prompt, `prompt_notes`, the model notes the system prompt carried at its last build (`[{name, scope, chars, digest}]`, see [Memory](memory.md#model-notes)), `parent_id`, the session that delegated this one (see *Delegating*; `null` otherwise), and `continues`, the session this one continues (the previous link of a chain, see *Session chains*; `null` otherwise); files from before those keys read as empty lists, no parent and no previous link. `model_name` is the resolved model ref (an alias applied, `host:id` when it names a host), so a resumed session keeps its model when an alias is retargeted; `model_typed` is the name it was given as when that differs (an alias), for the `models:` lookup after a resume (`null` otherwise).
3
+ Sessions are plain files — no DB. Each session is `~/.local/state/samagotchi/sessions/<uuid>.json` (XDG-aware via `XDG_STATE_HOME`) plus a sidecar dir `<uuid>/` with `input/`/`notes/`/`output/`/`owner.lock`/`bridge.json` (`Session.session_dir`), and `tool_output/` once a long `execute` result was cut: its whole output, which the cut result names (`ToolOutputFiles`, the newest 50 kept; see [Tool output guardrails](internals/tool-guardrails.md#the-runners-cut-max_tool_output_chars)). Besides the messages and turn state, the JSON keeps `written_by` (the chi version that saved it last; see *Which chi a worker runs*), `used_memory_names` (what the session read), `preloaded_memory_names` (`--memory`) and `muted_memory_names` (`--mute`), the last two written before the worker starts so a respawn builds the same prompt, `prompt_notes`, the model notes the system prompt carried at its last build (`[{name, scope, chars, digest}]`, see [Memory](memory.md#model-notes)), `parent_id`, the session that delegated this one (see *Delegating*; `null` otherwise), and `continues`, the session this one continues (the previous link of a chain, see *Session chains*; `null` otherwise); files from before those keys read as empty lists, no parent and no previous link. `model_name` is the resolved model ref (an alias applied, `host:id` when it names a host), so a resumed session keeps its model when an alias is retargeted; `model_typed` is the name it was given as when that differs (an alias), for the `models:` lookup after a resume (`null` otherwise).
4
4
 
5
5
  **Message times:** an entry of `messages` may carry `at`, when it happened: a local-offset ISO time with milliseconds (`2026-10-24T14:32:05.120+02:00`). User prompts, lines merged into a turn and steers, reminders, context and task notes, and the model's replies have one (a note's is its `created_at`); tool results, `!cmd` messages and chi's own turn notes don't, nor does anything saved before chi stamped messages (no backfill). It is for the UIs only (the gap dividers, a bubble's time): it never goes to the model, so it changes nothing in the requests or their prompt cache. The one time the model reads is the reminders': due reminders go into a turn as a system message headed `[SYSTEM: REMINDERS DUE 2026-10-24 14:32 local]` (the turn's local time; `ReminderQueue#inject!`), written into the saved content then, so a later request sends the same text.
6
6
 
@@ -29,7 +29,9 @@ A session is deleted if **expired by age OR overflow by count** (unless `keep_st
29
29
 
30
30
  **Attached by default:** plain `chi` and `chi --resume ID` run their session in a background worker and attach to it (`session.shared`, default `true`; env `SAMAGOTCHI_SESSION_SHARED`). The worker runs in the session's `working_directory`, so `!commands` and tools see the directory the session was started in. `session.shared: false` keeps the in-process REPL, `--no-shared` does for one run; a session the REPL has open can't be attached ("close it there first"). See [CLI: Sharing a session](cli.md#sharing-a-session).
31
31
 
32
- **Which chi a worker runs:** a new or woken worker runs the newest chi installed for this Ruby (`gem 'samagotchi', '>= 0.a'`, prereleases count), whoever starts it: a `chi web` or terminal still on an older chi starts it on the newest one, so a stale process doesn't spread old code. A worker already running keeps its version until it is restarted. Should the newest install fail to load, the worker runs the starter's own version and logs `boot_fallback` (its stdio is `/dev/null`). Under `bundle exec` the worker drops Bundler's env first, so it activates the newest gem too. Two consequences: a wrapper pin (`chi _0.17.0_ web`) pins only that process, not the workers it starts; and a source checkout (`bin/chi` in a worktree, `bundle exec`) starts its workers on its own `lib/`, but a session it made that is later woken by an installed chi (`chi web`, `chi send`) runs the installed code. The names an older starter hands a newer worker (the boot line, its env keys, `input/` files, the session JSON fields and the Bridge routes sessions use on each other) are pinned in `spec/worker_contract_spec.rb`.
32
+ **Which chi a worker runs:** a new or woken worker runs the newest chi installed for this Ruby (`gem 'samagotchi', '>= 0.a'`, prereleases count), whoever starts it: a `chi web` or terminal still on an older chi starts it on the newest one, so a stale process doesn't spread old code. A worker already running keeps its version until it is restarted. Should the newest install fail to load, the worker runs the starter's own version and logs `boot_fallback` (its stdio is `/dev/null`). Under `bundle exec` the worker drops Bundler's env first, so it activates the newest gem too. Two consequences: a wrapper pin (`chi _0.17.0_ web`) pins only that process, not the workers it starts; and a source checkout (`bin/chi` in a worktree, `bundle exec`) starts its workers on its own `lib/`, but a session it made that is later woken by an installed chi (`chi web`, `chi send`) runs the installed code. The names an older starter hands a newer worker (the boot line, its env keys, `input/` files, the session JSON fields and the Bridge routes sessions use on each other) are pinned in `spec/worker_contract_spec.rb`. A chi newer than 0.130.0 keeps the top-level keys it doesn't know (a newer chi's fields) when it saves a session, after its own, and doesn't act on them; `written_by` names the chi that saved the file last, and a worker woken on a file a newer chi saved logs `newer_writer`. chi 0.130.0 and older drop those keys: a session a newer chi made loses its newer fields when an older chi wakes or saves it (for example `chi --attach` from a project whose Ruby has an older chi). Keep the chi in every Ruby you run it from newer than 0.130.0.
33
+
34
+ A new session field (the rule in `Session::FIELDS`' comment): only top-level keys are kept (a new sub-key of `review`, `toolkit` or another value object is still dropped by an older chi), and an older chi keeps a key without honouring it, so a new field must be safe to come back unchanged after an older chi ran turns on the session.
33
35
 
34
36
  **A worker outlives whoever started it:** a spawned worker leaves its spawner's process tree (it is forked again, in a session of its own, and reparented to init), so closing the terminal of a `chi web`, or a tool tearing down a background `chi send --wait` and its children, no longer takes the worker and its turn with it.
35
37
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: samagotchi-system
3
- version: 0.130.0
3
+ version: 0.131.0
4
4
  scope: system
5
5
  description: Default system memories — identity, self map, config modification protocol, memory guide and the delegated-session rules
6
6
  files:
@@ -1,9 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Samagotchi
4
- # The confirm step before a session setting change that restarts the
5
- # prompt cache (/thinking, /reasoning-history): such a change, sent
6
- # without --yes from a person at a web tab or a chi TUI, changes nothing
4
+ # The confirm step before a command that restarts the prompt cache: sent
5
+ # without --yes by a person at a web tab or a chi TUI, it changes nothing
7
6
  # and replies with what it costs and the line that applies it. Two steps,
8
7
  # no stored state: the reply carries a Confirm (SessionCommands::Result,
9
8
  # the worker's command_ran), the TUI prefills its line, the web draws an
@@ -43,12 +42,33 @@ module Samagotchi
43
42
  changed && history && COSTLY.include?(cost)
44
43
  end
45
44
 
46
- # The Confirm for +name+ set to +word+: the line with --yes, and the
47
- # question naming it.
48
- # @return [Confirm]
49
- def confirm(name, word)
50
- line = "#{name} #{word} --yes"
51
- Confirm.new(line: line, text: "Nothing changed yet: send `#{line}` to apply.")
45
+ # Whether a cache-changing command should ask, and its reply line.
46
+ # Kept independent of any UI so commands and plugins can share it.
47
+ # @param line [String] the full command line without --yes
48
+ # @param cost [Symbol, nil] the change's cache cost
49
+ # @param changed [Boolean] whether the effective setting changes
50
+ # @param engine [Engine] supplies history, used tokens and model host
51
+ # @return [Confirm, nil]
52
+ def ask(line:, cost:, changed:, engine:)
53
+ return nil unless needed?(cost: cost, history: engine.history?, changed: changed)
54
+
55
+ apply_line = "#{line.to_s.strip} --yes"
56
+ notes = []
57
+ tokens = engine.cache_restart_tokens
58
+ if tokens
59
+ size = tokens >= 1000 ? "#{format("%.1f", tokens / 1000.0).delete_suffix(".0")}k" : tokens.to_s
60
+ notes << case cost
61
+ when :full, :provider
62
+ "The next request reads the whole conversation again (about #{size} tokens)."
63
+ when :history
64
+ "The next request reads again from the first changed reply (up to about #{size} tokens)."
65
+ end
66
+ end
67
+ if %i[full provider].include?(cost) && engine.cache_restart_anthropic?
68
+ notes << "On an `api: anthropic` host, preserved thinking in replies is dropped from then on."
69
+ end
70
+ notes << "Nothing changed yet: send `#{apply_line}` to apply."
71
+ Confirm.new(line: apply_line, text: notes.compact.join("\n"))
52
72
  end
53
73
  end
54
74
  end