samagotchi 0.101.0 → 0.102.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 (62) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +28 -1
  3. data/bin/chi +13 -10
  4. data/docs/architecture.md +8 -6
  5. data/docs/cli.md +47 -7
  6. data/docs/configuration.md +25 -4
  7. data/docs/internals/bundles.md +1 -1
  8. data/docs/internals/engine.md +1 -1
  9. data/docs/internals/llm-context-forget.md +14 -0
  10. data/docs/internals/prompt-caching.md +8 -1
  11. data/docs/internals/working-set.md +86 -11
  12. data/docs/plugins.md +6 -3
  13. data/docs/sessions.md +28 -11
  14. data/docs/testing.md +7 -0
  15. data/lib/samagotchi/bridge/turn_accumulator.rb +16 -0
  16. data/lib/samagotchi/bridge.rb +9 -3
  17. data/lib/samagotchi/bundle_command/check.rb +5 -1
  18. data/lib/samagotchi/bundle_command/try/command.rb +6 -6
  19. data/lib/samagotchi/bundle_command/try/sandbox.rb +3 -3
  20. data/lib/samagotchi/bundle_command.rb +4 -4
  21. data/lib/samagotchi/bundles/system/manifest.yml +1 -1
  22. data/lib/samagotchi/commands/registry.rb +12 -1
  23. data/lib/samagotchi/config.rb +89 -53
  24. data/lib/samagotchi/engine.rb +131 -12
  25. data/lib/samagotchi/idle_scheduler.rb +16 -1
  26. data/lib/samagotchi/llm_context_edit.rb +32 -10
  27. data/lib/samagotchi/llm_context_forget.rb +12 -3
  28. data/lib/samagotchi/llm_context_view.rb +19 -7
  29. data/lib/samagotchi/memory_bundle/installer.rb +2 -0
  30. data/lib/samagotchi/memory_bundle/manifest.rb +13 -2
  31. data/lib/samagotchi/message_parts.rb +8 -3
  32. data/lib/samagotchi/plugin/context.rb +2 -2
  33. data/lib/samagotchi/prompt.rb +1 -1
  34. data/lib/samagotchi/recap_command.rb +68 -0
  35. data/lib/samagotchi/recap_setup.rb +27 -14
  36. data/lib/samagotchi/send_command.rb +14 -11
  37. data/lib/samagotchi/session.rb +19 -12
  38. data/lib/samagotchi/session_commands.rb +44 -2
  39. data/lib/samagotchi/session_config.rb +411 -0
  40. data/lib/samagotchi/session_setup.rb +9 -9
  41. data/lib/samagotchi/sessions_command.rb +6 -2
  42. data/lib/samagotchi/terminal_ui/attach_launcher.rb +5 -5
  43. data/lib/samagotchi/terminal_ui/attached_loop.rb +2 -1
  44. data/lib/samagotchi/terminal_ui/formatting.rb +10 -3
  45. data/lib/samagotchi/terminal_ui.rb +8 -6
  46. data/lib/samagotchi/testing/bundle.rb +3 -2
  47. data/lib/samagotchi/version.rb +1 -1
  48. data/lib/samagotchi/web/app.rb +30 -13
  49. data/lib/samagotchi/web/public/app.js +2 -0
  50. data/lib/samagotchi/web/public/commands.js +8 -2
  51. data/lib/samagotchi/web/public/data.js +3 -2
  52. data/lib/samagotchi/web/public/index.html +20 -2
  53. data/lib/samagotchi/web/public/stream_handlers.js +4 -0
  54. data/lib/samagotchi/web/public/turn_html.js +5 -0
  55. data/lib/samagotchi/web/public/working_set.js +113 -21
  56. data/lib/samagotchi/web/public/working_set_view.js +136 -21
  57. data/lib/samagotchi/worker.rb +9 -6
  58. data/lib/samagotchi/working_set.rb +49 -11
  59. data/lib/samagotchi/working_set_command.rb +119 -0
  60. data/lib/samagotchi/working_set_steer.rb +250 -0
  61. metadata +5 -2
  62. data/lib/samagotchi/bundle_session_settings.rb +0 -251
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6c75329bad389a0434a3f99db7d05d512a45864aa3ce08ecb8a6a7399bddbe33
4
- data.tar.gz: c057af30d62ee4f72bf0b681c10edb0bd0bf5225e14a3e1470f4b0d726bbaf67
3
+ metadata.gz: '084d47c40205bd43fa1126dd3169a0792cc6faee949899bf6b62d84a559d1501'
4
+ data.tar.gz: 23dde4e086c819fc43349cb20afed56121fb51bfb3b54b9be6e90fd8a332003b
5
5
  SHA512:
6
- metadata.gz: d2324f636a992fe36e2c0250ed55cf979b574359fd7e0f67271ceaf8da0803ffd60fc70408d62c73af984a2f9310b6acbfef92bbaac61a6c272e20540e38c840
7
- data.tar.gz: cffd6a4d948302b563f1e30cf5542be9db7e95a68cc07a97cdb2b81e008454f7fe605f8bb96946c6b43c0fccd1957d05ea764867fde4f976f4699f242d8897fe
6
+ metadata.gz: 763b8db4ab722ec994871d8d5e4e1238beda7ae846c9cf8bf5f9dc0f2c8d545eb767b7d82afcf35a8cb096f2f30461a8a793068c5359d5c926a7209e626c91b9
7
+ data.tar.gz: abb0665762926a27e66133b5326cf8b18658e5404047d9f1ce414c6455ac3ddffa15667c00ab6a6ad929a9d15d451f667ad11b702d08bccb9fa9053161415a5b
data/CHANGELOG.md CHANGED
@@ -8,6 +8,32 @@ and commands may change between minor versions. How releases are made:
8
8
 
9
9
  ## [Unreleased]
10
10
 
11
+ ## [0.102.0] - 2026-10-23
12
+
13
+ ### Added
14
+
15
+ - `/working-set` lists the session's working set as the model has it: each file, memory and URL its turns read,
16
+ edited or fetched, with its state (`stale`, `forgotten`, `dropped`) and its size as sent. `/working-set drop KEY…`
17
+ takes an item out of what the model is sent (its reads go as a stub saying you dropped it, under any LLM context
18
+ strategy); `/working-set undrop KEY…` sends it whole again. A change applies from the next turn. In the web's
19
+ working set list (still experimental, `web.working_set: true`), rows offer `drop` / `undrop`; mid-turn they wait
20
+ for the turn's end, with an `undo`. See [docs/cli.md](docs/cli.md).
21
+ - `--set` takes chi's own session settings too: `--set recap.enabled=false` or `--set recap.sentences=3` for this
22
+ session, over config.yml. A bad value or a key no session sets is an error (exit 2). See
23
+ [docs/configuration.md](docs/configuration.md#per-session-settings).
24
+ - `/recap on|off|default` turns the idle recap on or off for this session, at once and in any UI (mid-turn too);
25
+ saved with the session.
26
+
27
+ ### Changed
28
+
29
+ - `POST /api/sessions`: the field `bundle_settings` is now `settings` (it takes core keys too). No alias.
30
+ - An attached TUI started before this release treats `/recap off` as the plain `/recap`; restart it.
31
+
32
+ ### Fixed
33
+
34
+ - The web's working set no longer keeps the last item when the set is emptied, and a reply to an older request can
35
+ no longer overwrite a newer one.
36
+
11
37
  ## [0.101.0] - 2026-10-22
12
38
 
13
39
  ### Added
@@ -3828,7 +3854,8 @@ and long-lived sessions.
3828
3854
  - A macOS desktop helper (`chi desktop install`): a "Send to chi" Service and a
3829
3855
  hotkey panel that send selected text or the clipboard to your sessions.
3830
3856
 
3831
- [Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.101.0...HEAD
3857
+ [Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.102.0...HEAD
3858
+ [0.102.0]: https://github.com/dm1try/samagotchi/compare/v0.101.0...v0.102.0
3832
3859
  [0.101.0]: https://github.com/dm1try/samagotchi/compare/v0.100.1...v0.101.0
3833
3860
  [0.100.1]: https://github.com/dm1try/samagotchi/compare/v0.100.0...v0.100.1
3834
3861
  [0.100.0]: https://github.com/dm1try/samagotchi/compare/v0.99.0...v0.100.0
data/bin/chi CHANGED
@@ -133,9 +133,11 @@ parser = OptionParser.new do |opts|
133
133
  # --no-tool is taken out of ARGV before parse! (OptionParser would read it
134
134
  # as --tool's negation); this is its help line.
135
135
  opts.separator " --no-tool NAME Leave this tool out of the toolkit (repeatable; a comma list too)"
136
- opts.on("--set BUNDLE.KEY=VALUE", "A bundle setting for this session, over config.yml's bundles: (repeatable; a key the " \
137
- "bundle's manifest lists in session_settings:; numbers and booleans (true, off, no) are typed, the rest is " \
138
- "text; a value of the wrong type is refused; saved in the session: --set cost-cap.cap=3)") do |v|
136
+ opts.on("--set KEY=VALUE", "A setting for this session, saved in it (repeatable): a core key a session may set " \
137
+ "(recap.enabled, recap.sentences; typed by the key, over CLI, env and config.yml), or BUNDLE.KEY over " \
138
+ "config.yml's bundles: (a key the bundle's manifest lists in session_settings:; numbers and booleans " \
139
+ "(true, off, no) are typed, the rest is text). A wrong key or value is refused: " \
140
+ "--set recap.enabled=false, --set cost-cap.cap=3") do |v|
139
141
  (options[:sets] ||= []) << v
140
142
  end
141
143
  opts.on("--parent-answers LEVEL", "What a parent agent may answer on this session's questions: " \
@@ -435,13 +437,14 @@ if options[:toolkit] || options[:tools] || options[:no_tools]
435
437
  remove: options[:no_tools])
436
438
  end
437
439
 
438
- # --set BUNDLE.KEY=VALUE: a new session's bundle settings, checked here
439
- # against the installed bundles (a typo isn't saved).
440
+ # --set KEY=VALUE / BUNDLE.KEY=VALUE: a new session's own settings, checked
441
+ # here (a core key against its Config entry, a bundle's against the installed
442
+ # bundles), so a typo isn't saved.
440
443
  if options[:sets]
441
- require "samagotchi/bundle_session_settings"
444
+ require "samagotchi/session_config"
442
445
  begin
443
- options[:bundle_settings] = Samagotchi::BundleSessionSettings.from_flags(options[:sets])
444
- rescue Samagotchi::BundleSessionSettings::Error => e
446
+ options[:settings] = Samagotchi::SessionConfig.from_flags(options[:sets])
447
+ rescue Samagotchi::SessionConfig::Error => e
445
448
  warn "Error: #{e.message}"
446
449
  exit 2
447
450
  end
@@ -500,7 +503,7 @@ if launch == :attached
500
503
  appended_prompt_files: options[:appended_prompt_files] || [],
501
504
  toolkit: options[:toolkit_value],
502
505
  toolkit_flags: options[:toolkit_flags] || [],
503
- bundle_settings: options[:bundle_settings] || {},
506
+ settings: options[:settings],
504
507
  parent_answers: options[:parent_answers])
505
508
  rescue Samagotchi::TerminalUI::AttachLauncher::Error => e
506
509
  warn "Error: #{e.message}"
@@ -523,7 +526,7 @@ begin
523
526
  appended_prompt_files: options[:appended_prompt_files] || [],
524
527
  toolkit: options[:toolkit_value],
525
528
  toolkit_flags: options[:toolkit_flags] || [],
526
- bundle_settings: options[:bundle_settings] || {},
529
+ settings: options[:settings],
527
530
  parent_answers: options[:parent_answers],
528
531
  non_interactive: options[:non_interactive],
529
532
  model_name: options[:model],
data/docs/architecture.md CHANGED
@@ -311,7 +311,8 @@ process that already owns the `Engine`); every worker starts it, and it exposes:
311
311
  refused connection is `503 not_live`.
312
312
  - `POST /session/:id/cancel` — cancel the running turn; `202`, or `409` when none runs.
313
313
  - `POST /session/:id/recap` — `/recap` in an attached TUI: `200 {enabled, saved, request,
314
- min_user_turns}` (`{enabled: false}` without recaps); a new recap is asked for at once and
314
+ min_user_turns}` (`{enabled: false, session_off}` without recaps; `session_off` when the session's own
315
+ `/recap off` turned it off); a new recap is asked for at once and
315
316
  arrives as `:recap_ready`. Answers mid-turn too.
316
317
  - `POST /session/:id/answer` — answer the pending question; `200`, `409` when another client
317
318
  answered first or it is gone, `400` for an invalid selection. With `client_id: "cli:answer"`
@@ -446,10 +447,11 @@ The page's frequent reads of `GET /api/sessions/:id` stay light too:
446
447
  - `?cards=1`: the cards, from `/tail`; the session file is only stat'ed for the 404.
447
448
 
448
449
  The stage view's working set (`web.working_set`) has its own read, `GET /api/sessions/:id/working_set`: the
449
- `WorkingSet` projection (`{items, at}`: each file, memory and URL live, stale or forgotten, with its tokens) of the
450
- worker's `/snapshot` messages, else the session file, under the session's LLM context strategy. Its live form is the
451
- `:working_set_updated` event. The page overlays it on the set it builds from its tool rows
452
- ([internals/working-set.md](internals/working-set.md)).
450
+ `WorkingSet` projection (`{items, at, running}`: each file, memory and URL live, stale or forgotten, with its tokens)
451
+ of the worker's `/snapshot` messages, else the session file, under the session's LLM context strategy; while a turn
452
+ runs, the snapshot's `working_set` (the running turn's last event, which the messages lag) when it has one. Its live form
453
+ is the `:working_set_updated` event, which always wins over a GET's answer it overtook. The page overlays it on the
454
+ set it builds from its tool rows ([internals/working-set.md](internals/working-set.md)).
453
455
 
454
456
  A worker without `/tail` (an older chi; `404 not_found`) is read through `/snapshot` in the same
455
457
  shape; any other failure (no worker, a 500, a timeout) reads the disk, trimmed to the newest
@@ -609,7 +611,7 @@ A map of the parts the overview above doesn't name (paths under `lib/samagotchi/
609
611
  | Paths | `paths.rb` (`Paths`) | The XDG base dirs chi's files live under, read from the env on every call: `Paths.state_home` (`$XDG_STATE_HOME`, else `~/.local/state`) and `Paths.config_home` (`$XDG_CONFIG_HOME`, else `~/.config`), both defaults under `Dir.home`; a blank variable counts as unset, a relative one is made absolute from the current directory. `Paths.tilde` shows a path with `~` for the home folder. `SessionManager` hands a spawned worker the spawner's `XDG_STATE_HOME` / `XDG_CONFIG_HOME`, when set, made absolute, so a relative one isn't read from the session's folder. |
610
612
  | MemoryBundle | `memory_bundle.rb`, `memory_bundle/{installer,builder,installed_bundle,plugin_ref,bundle_hook}.rb` (`Installer`, `Builder`, `InstalledBundle`, `PluginRef`, `BundleHook`) | Installing, building and reading bundles of memories, hooks and plugins. |
611
613
  | Bundle authoring | `bundle_command.rb` (`BundleCommand`), `bundle_command/{scaffold,check,test_run}.rb`, `bundle_command/try/{run,sandbox,trace}.rb`, `testing.rb` + `testing/` (the test kit) | `chi bundle new`, `check`, `test` and `try`, and the kit a bundle's tests load it with ([bundle-authoring.md](bundle-authoring.md), [internals/bundles.md](internals/bundles.md)). |
612
- | Bundle session settings | `bundle_session_settings.rb` (`BundleSessionSettings`) | A session's own `--set BUNDLE.KEY=VALUE` values, checked against the manifest's `session_settings:`, laid over `bundles: <name>:` in `ctx.settings` ([plugins.md](plugins.md)). |
614
+ | Session config (`--set`) | `session_config.rb` (`SessionConfig`, `SessionConfig::Settings`) | A session's own settings as one value (`Session#settings`, `SessionSetup#settings`): `core`, the `--set KEY=VALUE` values of the core keys marked `session: true` (`Config::Entry`), typed by `Config.coerce!` and read first through `Config.get(key, session:)` (`RecapSetup`; `Engine#session=` rebuilds the recap job, `IdleScheduler#replace_job`), file key `config_settings`; `bundles`, the `--set BUNDLE.KEY=VALUE` values, checked against the manifest's `session_settings:`, laid over `bundles: <name>:` in `ctx.settings` ([plugins.md](plugins.md)), file key `bundle_settings`. `SessionConfig.clash` refuses a bundle setting named like a core one (install, `chi bundle check`). See [configuration.md](configuration.md#per-session-settings). |
613
615
  | Plugin API | `plugin/api.rb` (`Plugin::Api`), `plugin/tool_spec.rb` (`Plugin::ToolSpec`: a checked tool declaration, how it is registered), `plugin/loader.rb`, `plugin/context.rb` (`ctx`), `plugin/http.rb` + `plugin/http_transport.rb` (`ctx.http`), `plugin/card.rb` (`Plugin::Card`) | What a bundle's plugin registers: tools, commands, annotation actions, hooks, services, toolkits, peer sources; what its handlers get as `ctx` ([plugins.md](plugins.md)). |
614
616
  | Peer bundles' base | `plugin/peer.rb` (`Plugin::Peer`) | What claude-peer and codex-peer share (the send tool, the command, the cards, payloads); each bundle's subclass sets its strings and transport ([plugins.md](plugins.md#the-claude-peer-bundle)). |
615
617
  | Shipped bundles | `bundles/` (`mcp`, `github-pr`, `loop-guard`, `skills`, `source-links`, …) | Bundles that ship with chi. |
data/docs/cli.md CHANGED
@@ -16,7 +16,7 @@
16
16
  - `chi web --web-markdown` — opt in to sanitized Markdown rendering for completed assistant messages, the stage's asking text and a resolved question card's reply
17
17
  - `chi web --web-view turn` — draw turns with the turn view (each turn as one block of steps, the running one at the bottom of the history) instead of the default stage view (the running turn pinned above the composer); `?view=stage|turn` on the page URL overrides it (see [Web views](#web-views))
18
18
  - `chi web --web-working-set` — the stage view's working set (experimental, off by default; `--no-web-working-set` turns a config's `web.working_set: true` off): what each turn brought into the model's context, in the stage and under each finished turn; `?working_set=1|0` on the page URL overrides it (see [The working set](#the-working-set))
19
- - `chi sessions list|stop|restart|archive|unarchive|delete|prune|clean|stats` — manage persisted sessions; `list` shows this git project's, `list --scope=all` every one, a delegated session with `↳ <parent>`, `list --archived` the archived ones too (see [Sessions](sessions.md)). `stats ID` prints a session's cost, tokens and progress (a live worker's `GET stats`, else its saved `analytics.json`; `--format json` one object) without running a model turn. A usage error (an unknown subcommand or flag, a flag missing its value, `stop`, `restart` or `delete` with no ids, a bad `list --format` or `--scope`, a `--days`, `--keep` or `--limit` that isn't a whole number) exits 2; an unknown or refused session exits 1
19
+ - `chi sessions list|stop|restart|archive|unarchive|delete|prune|clean|stats` — manage persisted sessions; `list` shows this git project's, `list --scope=all` every one, a delegated session with `↳ <parent>`, `list --archived` the archived ones too (see [Sessions](sessions.md)). `stats ID` prints a session's cost, tokens and progress, and its own settings (`--set`; a `settings:` line), from a live worker's `GET stats`, else its saved `analytics.json` (`--format json` one object, with `settings`), without running a model turn. A usage error (an unknown subcommand or flag, a flag missing its value, `stop`, `restart` or `delete` with no ids, a bad `list --format` or `--scope`, a `--days`, `--keep` or `--limit` that isn't a whole number) exits 2; an unknown or refused session exits 1
20
20
  - `chi note [--source NAME] [-m TEXT] (ID|PREFIX)... | --all` — add a context note (TEXT or stdin) to sessions: background the model sees on its next turn; it starts no turn (see [Sessions: Context notes](sessions.md#context-notes))
21
21
  - `chi context <add|push|ls|show|refresh|rm|mute|unmute> …` — attach live external text to sessions (a command chi runs every so often, or text pushed in); the model gets a short note when it changes and reads it with `context_read`; `chi context add <URL>` attaches a GitHub PR with the github-pr bundle (see [Attached context](context.md))
22
22
  - `chi send [-m TEXT] [--image PATH]... [--cut | --queue] (ID|PREFIX)...` — send a message to sessions as if typed there: a turn starts (or a running one takes it at its next step; `--cut` asks it to cut long thinking for it; `--queue` runs it as a turn of its own after the running one, and `--queue --wait` waits for that turn's own answer); piped stdin goes above `-m` as quoted context, and `--image` attaches images (see [Sessions: Sending a message](sessions.md#sending-a-message)); `--new` starts a session with it instead (`--new --continues ID|last:ID` the next link of that session's chain, see [Session chains](sessions.md#session-chains)), and `--wait` prints the answer (`--wait ID` with no message waits for the next reply without sending — to an idle session whose last turn already ended, that ended turn's reply or end comes back; `--wait --turn T ID` with no message waits for exactly turn T, whether running, queued or last ended; `--format json` includes its `turn_id` when known, for waiting again after exit 3 or 4; exit 3 a question waits, its options on stderr; exit 4 `--timeout` passed with the turn still running; see [Starting a session](sessions.md#starting-a-session))
@@ -282,7 +282,7 @@ controls exit behavior (`--non-interactive`); `--resume` composes with both.
282
282
  | `--thinking LEVEL` | This session's own thinking level: `off`, `low`, `medium` or `high`, saved in the session and first in the order (`default` unsets it). On `--resume` or `--attach` it is set at once, through a `/thinking` on an attached worker. With `chi web` it is instead the default for the sessions its workers run, below a session's own (as env `SAMAGOTCHI_THINKING_LEVEL` is). See "Thinking" in configuration.md. |
283
283
  | `--toolkit NAME`, `--tool NAME`, `--no-tool NAME` | A new session's toolkit, the tools its prompts declare: `full` (every built-in), `lean`, `child`, one of config's `toolkit.kits` or a bundle's (without `--toolkit`: config's `toolkit.default`), with tools declared on top of it (`--tool`) or left out (`--no-tool`); both repeatable, a comma list too. Saved in the session as given; a respawned worker, a fork and a continue keep it. Ignored with `--attach`/`--resume` (one line, as `--memory`); `/toolkit` changes it. Not on `chi web` (config's `toolkit.default` covers its sessions). See [Toolkits](configuration.md#toolkits). |
284
284
  | `--parent-answers LEVEL` | A new session's own `guardrails.parent_answers` (`hard`, `standard` or `soft`): what a parent agent may answer on its questions. It may only tighten config.yml's: the stricter of the two applies, and a looser one is saved as given with a warning (`--parent-answers soft is looser than config.yml's guardrails.parent_answers: standard; standard applies`). Saved in the session; a respawned worker, a fork and a continue keep it, a `delegate` child takes its parent's. Ignored with `--attach`/`--resume` (one line, as `--toolkit`); refused with `chi web`. Only you loosen it: `/parent-answers` in the terminal or the web. See [Guardrails](guardrails.md#answers-from-a-parent-agent). |
285
- | `--set BUNDLE.KEY=VALUE` | A bundle setting for a new session (repeatable; a later one of the same key wins), laid over config.yml's `bundles: <bundle>:` in what the bundle's plugin reads (`ctx.settings`): `--set cost-cap.cap=3`. The value is typed simply: an integer (`3`, `-1`) or a decimal (`2.5`, `.5`, `1e1`) is a number, `true`, `yes` and `on` are true, `false`, `no` and `off` are false (any case, as in config.yml), anything else stays the text as given (`010`, `0x10`, `3:30`, `y`, `null`), and so does every value of a key the manifest types `string`. For cost-cap, `0`, a negative number or `off` is no cap. Only a key the installed bundle's manifest lists in `session_settings:` is taken, with a value of the type it gives the key: a bundle that isn't installed (or whose plugin doesn't load), a key it doesn't list (a record installed before the bundle listed it says to run `chi update` or `chi bundle upgrade NAME`), a value of another type (`--set cost-cap.cap=five`), or another shape (`--set foo`, `--set a.b.c=1`) is an error naming what it takes, exit 2, nothing started. Saved in the session; a respawned worker, a fork and a continue keep it, a delegate child doesn't. Ignored with `--attach`/`--resume` (one line, as `--toolkit`); refused with `chi web`. See [Per-session settings](plugins.md#per-session-settings). |
285
+ | `--set KEY=VALUE` | A setting for a new session (repeatable; a later one of the same key wins). A core key a session may set (`recap.enabled`, `recap.sentences`) is typed by the key and goes first, over the CLI, env and config.yml: `--set recap.enabled=false` turns the idle recap off for this session; a value it doesn't take (`recap.enabled=maybe`) or a key no session sets (`guardrails.mode`) is an error, exit 2 (see [Per-session settings](configuration.md#per-session-settings)). Any other name is a bundle's `BUNDLE.KEY`, laid over config.yml's `bundles: <bundle>:` in what the bundle's plugin reads (`ctx.settings`): `--set cost-cap.cap=3`. The value is typed simply: an integer (`3`, `-1`) or a decimal (`2.5`, `.5`, `1e1`) is a number, `true`, `yes` and `on` are true, `false`, `no` and `off` are false (any case, as in config.yml), anything else stays the text as given (`010`, `0x10`, `3:30`, `y`, `null`), and so does every value of a key the manifest types `string`. For cost-cap, `0`, a negative number or `off` is no cap. Only a key the installed bundle's manifest lists in `session_settings:` is taken, with a value of the type it gives the key: a bundle that isn't installed (or whose plugin doesn't load), a key it doesn't list (a record installed before the bundle listed it says to run `chi update` or `chi bundle upgrade NAME`), a value of another type (`--set cost-cap.cap=five`), or another shape (`--set foo`, `--set a.b.c=1`) is an error naming what it takes, exit 2, nothing started. Saved in the session; a respawned worker, a fork and a continue keep it, a delegate child doesn't. Ignored with `--attach`/`--resume` (one line, as `--toolkit`); refused with `chi web`. See [Per-session settings](plugins.md#per-session-settings). |
286
286
  | `--profile NAME` | Prompt profile (`qwen36` or `gemma4`) for every model in this run, over config and the server's template (same as `--model-profile`, env `SAMAGOTCHI_MODEL_PROFILE`). See "Prompt profile" in configuration.md. |
287
287
  | `--memory NAME` | Preload a memory entry into the system prompt (repeatable; a comma list too). Merged under the config.yml `memories:` baseline. Works attached: the list is stored on the session, so its worker builds the same prompt on every respawn. |
288
288
  | `--append-system-prompt FILE` | Put `FILE`'s text into the system prompt, after AGENTS.md and before the memory indexes (repeatable; relative to the shell's directory). Added after the config.yml `append_system_prompt:` list. A missing or unreadable file is an error, exit 1. Saved in the session, so a respawned worker, a fork and a continue keep it; ignored with `--attach`/`--resume` (one line, as `--memory`). See [Appended prompt files](configuration.md#appended-prompt-files). |
@@ -500,7 +500,8 @@ a terminal, headed by `<id8> <status> <model>`: a live worker answers its
500
500
  Bridge's `GET stats`, else the snapshot is rebuilt from the session's saved
501
501
  `analytics.json` (never starting a worker; a worker that does not answer in
502
502
  time falls back to the disk and the line says so). `--format json` prints one
503
- object `{session_id, status, live, metrics}`; a session with no
503
+ object `{session_id, status, live, metrics, settings}` (`settings`: the session's own `--set` values, flat as
504
+ `--set` names them, `{}` for none); a session with no
504
505
  `analytics.json` yet reports `metrics: null` (`(no metrics yet)` in text) and
505
506
  exits 0. An unknown or ambiguous id exits 1 with `chi sessions stop`'s
506
507
  wording. See [Sessions](sessions.md#cli).
@@ -602,6 +603,25 @@ the same values on its `llm context:` line. The web's info bar shows them as the
602
603
  through `/llm-context`. See "LLM context: a session's own strategy" in
603
604
  [configuration.md](configuration.md).
604
605
 
606
+ `/working-set` lists the session's working set as the model has it (see [The
607
+ working set](#the-working-set)): each file, memory and URL its turns read,
608
+ edited or fetched, with its state (`stale`, `forgotten`, `dropped`) and what it
609
+ costs as sent, then the changes the next turn starts with. `/working-set drop
610
+ KEY…` takes an item out of what the model is sent: every read of it still sent
611
+ goes as a stub, `(dropped by the user) — read it again if the task needs it`,
612
+ whatever the LLM context strategy (none too), and stays dropped across a
613
+ strategy switch. KEY is a path (absolute or the session folder's), `memory:NAME`
614
+ or a URL. `/working-set undrop KEY…` sends it whole again. A change applies
615
+ from the next turn's first request, which also carries one line telling the
616
+ model what you dropped or brought back (so it doesn't read it again by reflex);
617
+ the reply says what it frees and the tail the server reads again (a drop changes
618
+ history, so the prompt cache breaks there once). An undrop before that request
619
+ undoes the drop for free. Refused, with why: an item only edited or written
620
+ (nothing of it to drop), one not in the set, an older session's output chi
621
+ can't name. The model can't undo a drop (`forget_outputs` refuses it).
622
+ `/working-set` alone answers mid-turn too; `drop`/`undrop` sent while a turn
623
+ runs wait for the turn's end.
624
+
605
625
  `/thinking` shows the thinking level the next turn runs at and where it came
606
626
  from (`session`, `--thinking` / `SAMAGOTCHI_THINKING_LEVEL`, `models: <key>`,
607
627
  `hosts.<name>`, `thinking.level`). `/thinking off|low|medium|high` sets the
@@ -741,7 +761,7 @@ REPL alike:
741
761
  `cutting in…`, then `steered`. A phone shows the short form (`↳ after this
742
762
  thinking · 0:42`) with `cut in` and `queue` chips.
743
763
  - Commands answer, wait or are refused, by what they do:
744
- - At once, beside the turn: `/stats`, `/recap`, `/help`, `/context`, a
764
+ - At once, beside the turn: `/stats`, `/recap` (and `/recap on|off|default`), `/help`, `/context`, a
745
765
  plugin's anytime command (`/btw`), and the commands that only show
746
766
  something: `/models` (with a filter too), and `/model`, `/llm-context`,
747
767
  `/thinking`, `/reasoning-history` and `/guardrails` alone.
@@ -987,7 +1007,15 @@ turn view ignores it.
987
1007
  (used, preloaded, muted) and URLs, each with what was done to it (`read ×2`,
988
1008
  `read, edited`, `preloaded`) and a step tag (`s3`, `t2·s3` in the session's
989
1009
  scope, `prompt` for a preloaded memory) that goes to the call. A file opens
990
- as its row's title does, a URL in a new tab.
1010
+ as its row's title does, a URL in a new tab. A row (on hover or keyboard
1011
+ focus) and a folded group offer `drop`, which runs `/working-set drop` (see
1012
+ `/working-set` above); a dropped row offers `undrop`. Between turns it
1013
+ applies at once; while a turn runs it waits for the turn's end: the row is
1014
+ dashed with `drops next turn` and an `undo`, and the list's foot counts the
1015
+ pending changes (`undo all`), in every tab and after a reload. On a phone a
1016
+ tap on a row opens its actions under it. `drop` is greyed, with why on
1017
+ hover, for an item only edited, a memory only preloaded, or one already
1018
+ forgotten.
991
1019
 
992
1020
  Only `read`, `edit`, `write`, `memory_read`, `memory_write` and `web_fetch`
993
1021
  calls that succeeded count; files an `execute` touched don't.
@@ -1000,6 +1028,9 @@ the same live, after a reload and in every tab:
1000
1028
  write doesn't make a file stale. Chi checks at each request and when you
1001
1029
  open the list, so a file changed while the session sat idle shows stale on
1002
1030
  opening it. A turn saved before chi kept that record shows its files live.
1031
+ - **dropped** (struck): you dropped it (`/working-set drop`, the list's
1032
+ `drop`); its reads are sent as stubs under every strategy until you undrop
1033
+ it.
1003
1034
  - **forgotten** (struck): nothing of the item's content is still sent to the
1004
1035
  model under the session's current LLM context strategy (`forget_outputs`
1005
1036
  forgot every read of it; see
@@ -1007,7 +1038,7 @@ the same live, after a reload and in every tab:
1007
1038
  forget kept count as still sent. Under `/llm-context strategy none` nothing
1008
1039
  is cut, so a forgotten item is live again from the next request.
1009
1040
 
1010
- The list says `stale` or `forgotten` in words with each row, and how many
1041
+ The list says `stale`, `forgotten` or `dropped` in words with each row, and how many
1011
1042
  tokens its content costs as sent (`~1.2k`); a folded group shows its forgotten
1012
1043
  members as `✂n`, and a footprint adds `n forgotten`.
1013
1044
 
@@ -1120,7 +1151,16 @@ working on, what came of it and what is still open, not a turn-by-turn log.
1120
1151
  after it (`recap (before the last 2 turns)>`). One written while you sit at
1121
1152
  the prompt prints there.
1122
1153
  - `/recap` shows the saved one and asks for a new one when the chat moved on
1123
- (`writing a recap…`, then the recap when it comes).
1154
+ (`writing a recap…`, then the recap when it comes). The web answers the bare
1155
+ `/recap` with a note (the recap shows there by itself).
1156
+ - `/recap off` and `/recap on` turn the recap off or on for this session, at
1157
+ once and in any UI (the web composer too, and mid-turn); `/recap default`
1158
+ goes back to config.yml's. The reply says both: `recap: off (this session;
1159
+ config.yml: on)`. It is the session's own `recap.enabled`, as `--set
1160
+ recap.enabled=false` gives it (see [Per-session
1161
+ settings](configuration.md#per-session-settings)): saved with the session,
1162
+ so a respawned worker keeps it, and it beats config.yml (`/recap on` under
1163
+ `recap: false` turns it on here). With it off, `/recap` says so.
1124
1164
  - One written while a continue offer waits (the last turn ran out of steps)
1125
1165
  says that turn stopped before the task was finished. Answering `no` counts
1126
1166
  as activity, so the next recap no longer says so.
@@ -119,7 +119,8 @@ own subsystems: `hosts:` (below), `models:` (see "Prompt profile" and
119
119
  `guardrails:` (see [Guardrails](guardrails.md)), `bundles:` (a bundle's
120
120
  settings for its hooks, see [Hooks: Settings](hooks.md#settings); a session's own
121
121
  `--set BUNDLE.KEY=VALUE`, for a key the bundle's manifest lists in `session_settings:`, lays over them, see
122
- [Per-session settings](plugins.md#per-session-settings)),
122
+ [Per-session settings](plugins.md#per-session-settings); `--set KEY=VALUE` does the same for a few core keys,
123
+ see "Per-session settings" below),
123
124
  `memories:` and `append_system_prompt:` (see "Appended prompt files"). Their entry names (host names, model ids, aliases) are yours to
124
125
  choose. The `memories:` list is the persistent baseline for preloaded memory entries —
125
126
  the same name shape as `--memory` (bare name or `scope/name`), merged under
@@ -160,7 +161,7 @@ or `chi web` restart.
160
161
  | `context.every_seconds` | Next context poll | The poller checks it for each source that has no explicit interval. |
161
162
  | `guardrails.*` | Next tool call or approval | Guardrail rules and disable lists are reloaded when the config file changes; rule settings take effect on that evaluation. `guardrails.parent_answers` is checked when a parent answers a question. |
162
163
  | `hooks`, `bundles` | Worker start | Hooks, plugins and their bundle settings load with the engine. |
163
- | `recap.*` | Worker start | The engine builds its recap job once. |
164
+ | `recap.*` | Worker start | The engine builds its recap job at start, and again when it binds a session with its own `recap.enabled` or `recap.sentences` (`--set`, see "Per-session settings") and when `/recap on\|off\|default` changes the session's own value. |
164
165
  | `session.shared` | Next `chi` invocation | It selects attached-worker or in-process mode for that invocation. |
165
166
  | `session.idle_exit_minutes`, `session.keep_empty`, `session.max_children`, `session.delegate_reports`, `session.task_wakes`, `session.max_wakes` | Worker start or next matching lifecycle decision | Existing workers keep their idle-exit timer; the other values are read for the next corresponding decision. |
166
167
  | `session.retention_*`, `session.max_count`, `session.keep_status`, `session.sweep_interval_hours` | Next retention sweep | `chi web` sweeps on its configured schedule and on `GET /api/sessions`; `chi sessions prune` uses the values for that invocation. The sweep interval is checked on the next hub tick. |
@@ -172,6 +173,26 @@ or `chi web` restart.
172
173
  | `kitty.*`, `tmux.*` | `chi desktop upgrade` or desktop update | These settings are copied into the desktop helper's launch file. |
173
174
  | `model.profile`, `reasoning_history` | Not from config.yml | These are CLI/environment settings; `models:` and `hosts:` provide their model- and host-specific forms. |
174
175
 
176
+ ### Per-session settings
177
+
178
+ A few core keys can be set for one session: `chi --set KEY=VALUE` (or `chi send --new --set KEY=VALUE`, or
179
+ `POST /api/sessions` `settings`) at its creation, and `recap.enabled` later too with `/recap on`, `/recap off` or `/recap default`
180
+ (at once; `default` removes the session's own value, see [Session recap](cli.md#session-recap)). The value is saved in the session file (`config_settings`), so a
181
+ worker respawn, a continue and a fork keep it; a delegate child starts without it, on config.yml's. It goes first:
182
+
183
+ session (--set) → CLI → environment → config.yml → default
184
+
185
+ - `recap.enabled`: `true` or `false` (also `yes`/`on`/`1`, `no`/`off`/`0`); `true` turns the recap on for the
186
+ session even under `recap: false`.
187
+ - `recap.sentences`: `N` or `N-M` (1–10).
188
+
189
+ A value is typed by its key and checked when given: `--set recap.enabled=maybe` is refused (`recap.enabled takes
190
+ true or false, not "maybe"`), and so is a key no session sets (`--set guardrails.mode=strict`: not a session
191
+ setting). The same `--set` takes a bundle's `BUNDLE.KEY` (see [Per-session settings](plugins.md#per-session-settings));
192
+ a name that is a core key is always the core one, and a bundle can't offer a setting whose `NAME.KEY` is one.
193
+ `/stats` lists a session's own values (`settings: recap.enabled=false · cost-cap.cap=3`), and `chi sessions stats
194
+ --format json` has them as `settings`.
195
+
175
196
  ### Environment variables
176
197
 
177
198
  Every setting has an environment variable (except the ones the table marks
@@ -1589,14 +1610,14 @@ column says so, a `--kebab-name` flag. The maps (`hosts:`, `models:`,
1589
1610
  | `server.open_timeout` | `10` | yes | Connection timeout, seconds. |
1590
1611
  | `server.read_timeout` | `600` | yes | Read timeout, seconds. |
1591
1612
  | `server.first_token_timeout` | 120 remote, off local | | Seconds to the first token; `0` = off. `hosts.<name>.first_token_timeout` wins. |
1592
- | `recap.enabled` | on | | `false` (or `recap: false`) turns the idle recap off. |
1613
+ | `recap.enabled` | on | | `false` (or `recap: false`) turns the idle recap off. Per session too: `--set recap.enabled=false`, or `/recap off` in the session (see "Per-session settings"). |
1593
1614
  | `recap.model` | session's | yes | Model that writes the recap: an id, alias or `host:model` (the host then picks where to ask; a bare id goes where a bare `--model` goes). |
1594
1615
  | `recap.host_ref` | session's | yes | A `hosts:` name to ask (`host:` is accepted too, also a host name, not a model ref). |
1595
1616
  | `recap.base_url` | none | yes | An OpenAI API base to ask instead (`http://h:8081/v1`). |
1596
1617
  | `recap.inactivity` | `180` | yes | Idle seconds before a recap. |
1597
1618
  | `recap.timeout` | `30` | yes | Seconds a recap request may take. |
1598
1619
  | `recap.min_user_turns` | `2` | yes | Prompts a session needs before it gets a recap. |
1599
- | `recap.sentences` | `2-4` | yes | Recap length, `N` or `N-M` (1–10). |
1620
+ | `recap.sentences` | `2-4` | yes | Recap length, `N` or `N-M` (1–10). Per session too: `--set recap.sentences=3`. |
1600
1621
  | `broadcast.active_hours` | `8` | | `chi broadcast` reaches a session a worker or a chi REPL runs, or whose last turn ended within this many hours. See [Broadcast](broadcast.md). |
1601
1622
  | `broadcast.ticket_pattern` | `\b[A-Z][A-Z0-9]+-\d+\b` | | A Ruby regex for ticket ids; one in the note and in a session's branch (any case) or prompts delivers the broadcast there. See [Broadcast](broadcast.md). |
1602
1623
  | `broadcast.triage_model` | recap's | | The model that judges a recipient no tag matched (a model ref, as `recap.model`); unset: the recap's model, else `default.model`. A ~9B dense instruct model or larger; 4B-class ones say yes to nearly everything. See [Broadcast](broadcast.md#triage). |
@@ -32,7 +32,7 @@ user-facing side (what a bundle owns, profiles, needs) is in [memory.md](../memo
32
32
  | `SourceIdentity` (source.rb) | what a source is, for comparing two installs of one name: `shipped:<dir>` (any gem or checkout), `git:<url>` (no `#ref`, trailing `/` or `.git`), `path:<realpath>`; nil when it can't place one |
33
33
  | `PreparedSource` (source.rb) | one source normalized once (dir, owned, git commit): `BundleCommand` reads the bundle's name from its `manifest.yml` (the name for every kind of source, a git URL too) and hands it to the Installer (`prepared:`), so a source is cloned or extracted once per command |
34
34
  | `MemoryFiles` | which top-level `*.md` files of a bundle are its memories (`.select`: the `files:` list, else all but README/LICENSE and the like), `.unshipped` (listed, not shipped) and `.symlinks` (skipped: a symlink is never a memory) |
35
- | `Manifest` | parses/validates `manifest.yml` (`name:` is the bundle's name and a directory under `.bundles/`, so a path such as `../x`, `a/b`, `.` or `..` is refused: `name: … is not a bundle name`; YAML that doesn't parse or isn't a mapping is a `ValidationError` too; files, hooks as `BundleHook`, plugin, scripts, needs, requires_chi, includes, context_providers, session_settings: the keys a session's `chi --set` may give, a list or a mapping of them to types (`SESSION_SETTING_TYPES`), recorded at install in `manifest.json` (`session_settings`, `session_setting_types`) for `BundleSessionSettings` to check) and writes it (`Manifest.write`, for Builder). It ignores `requires_chi_next`, a shipped bundle's release-time marker (`script/release/release_tools.rb`, docs/releasing.md) |
35
+ | `Manifest` | parses/validates `manifest.yml` (`name:` is the bundle's name and a directory under `.bundles/`, so a path such as `../x`, `a/b`, `.` or `..` is refused: `name: … is not a bundle name`; YAML that doesn't parse or isn't a mapping is a `ValidationError` too; files, hooks as `BundleHook`, plugin, scripts, needs, requires_chi, includes, context_providers, session_settings: the keys a session's `chi --set` may give, a list or a mapping of them to types (`SESSION_SETTING_TYPES`), recorded at install in `manifest.json` (`session_settings`, `session_setting_types`) for `SessionConfig` to check) and writes it (`Manifest.write`, for Builder). It ignores `requires_chi_next`, a shipped bundle's release-time marker (`script/release/release_tools.rb`, docs/releasing.md) |
36
36
  | `Installer` | one install/upgrade/dry run of one bundle (below) |
37
37
  | `AssetInstaller` | the parts that live in `.bundles/<name>/`: hooks, guardrail rules, plugin, scripts; returns `AssetInstaller::Installed` (the paths the record keeps) |
38
38
  | `Merger` | 3-way classification per file: `install`, `fast_forward`, `keep`, `noop`, `conflict` |
@@ -509,7 +509,7 @@ Turn events (the main ones; `Events` in `events.rb` keeps the shared sets):
509
509
  | `:question_requested` / `:question_answered` / `:question_cancelled` | `QuestionDesk`: a question is asked (an approval, `ask_user_question`, a hook's or plugin's `ask_user`, the step-limit Continue), answered, or withdrawn | `{pending_question}` / `{id, answer}` / `{id, reason}` |
510
510
  | `:plugin_init_wait` | a turn waits for plugin init tasks that provide tools (`PluginTasks`) | `{tasks: [{bundle, id, label}]}` |
511
511
  | `:used_memories_updated` | a memory was read, and after the loop | `{used_memory_names, read_names?}` |
512
- | `:working_set_updated` | after `:generation_started` (the running turn's conversation, the turn's strategy) and once in `end_turn` before the end event (the kept messages), only when the set differs from the last one emitted for the session and is not empty: a tool-free turn emits none (`Engine#emit_working_set`) | `{items: [{kind, key, state, tokens, ops, count, first_id}], at}` (`WorkingSet`, internals/working-set.md) |
512
+ | `:working_set_updated` | after `:generation_started` (the running turn's conversation, the turn's strategy) and once in `end_turn` before the end event (the kept messages), only when the set differs from the last one emitted for the session and is not empty, except one empty event when the last one had items (a rollback emptied the set): a tool-free turn emits none (`Engine#emit_working_set`); between turns `Engine#announce_working_set` announces it under the next turn's strategy (after a `/working-set` op, held behind the command's `command_ran`) | `{items: [{kind, key, state (live\|stale\|forgotten\|dropped), tokens, ops, count, first_id}], at}` (`WorkingSet`, internals/working-set.md) |
513
513
  | `:hook_notice` | a hook's or plugin's notify during a turn | `{hook, text, level}` (announced with `between_turns: true` outside a turn) |
514
514
  | `:guardrail_warning` | a guardrails line during a turn | `{message}` |
515
515
  | `:turn_completed` | normal end | Engine event `{result, turn_summary, display_pending}`; the Bridge's SSE frame omits `result` (answer remains in `turn_summary.output`) |
@@ -35,6 +35,20 @@ lets the model forget its own tool outputs. The pieces:
35
35
  - **Restore** takes the forget off the entry (`LLMContextEdit.remove`, a new edits Hash); the next request sends the
36
36
  output again, outside the apply rule (the result says the tail the server reads again, and `ContextStatus#edited!`
37
37
  restarts the estimate). A read whose stub was sent isn't restored; a staged one is.
38
+ - **Which edits are sent** is one predicate, `LLMContextView#sends?(edit)`: applied, and its kind one of the view's
39
+ layers or `:drop`. The view stubs by it and `WorkingSet` decides "still sent" by it. The view's layers are
40
+ `LLMContextStrategy::LAYERS` (`stale`, `forget`), the strategy's words, not `LLMContextEdit::KINDS`: whatever a
41
+ caller passes as the strategy, an edit kind no strategy names never becomes a layer that `apply_llm_context!` would
42
+ hand to `LLMContextApply.run!`. Under none the view has no layers and, unless an entry holds a drop
43
+ (`LLMContextEdit.drop_on?`, one scan), `#messages` returns the Array it was given.
44
+ - **The user's layer, always on**: an edit of kind `:drop` (`by: "user"`, the working set's drop:
45
+ [working-set.md](working-set.md#steering-drop-and-undrop)) is sent under every strategy, none included, as
46
+ `(dropped by the user) — read it again if the task needs it` after the output's lead. It is saved applied by the
47
+ op itself, between turns, so the apply rule never holds it and no `[#id]` is added under none. One edit per id: a
48
+ drop on an output that holds another edit keeps that edit's saved Hash in its `was:`, and an undrop puts it back.
49
+ `LLMContextApply.measure`/`.weigh` and `#restored_line` build their views from the same class, so a drop counts in
50
+ them under none too. The model can't take a drop off: forget_outputs refuses to forget or restore a dropped
51
+ output, in `WorkingSetSteer::Refused`'s words ("dropped by the user").
38
52
  - **Stubs** (`LLMContextView`): `(forgotten) <note>` on the first output of a call's forgets in a row, `(forgotten
39
53
  with tN: see its note)` on the rest (a run of the same note, author and stamp, no other output between them),
40
54
  ` [restore: tN]` on a non-read with a stored id, then each kept range under `lines A-B kept:`. The entry keeps its
@@ -56,7 +56,7 @@ right-hand column happens.
56
56
  │ ◄── remote Claude / Messages: breakpoint #2 on the last │ the image cut moving
57
57
  │ non-system message (Messages: its last user block) │ a batch, history
58
58
  ├──────────────────────────────────────────────────────────────────────────┤ sanitizing (thinking
59
- │ tail notes reminders, turn notes, steers (system/user messages) │ stripped per spec)
59
+ │ tail notes reminders, turn notes, steers, the working-set note │ stripped per spec)
60
60
  │ new prompt this turn's user message │
61
61
  └──────────────────────────────────────────────────────────────────────────┘
62
62
  ```
@@ -108,6 +108,13 @@ right-hand column happens.
108
108
  loaded name stays deferred, a stub once its source is gone), so the request after a load reads the whole prefix;
109
109
  the loaded definitions and the new result are the fresh part. A plugin's
110
110
  `ctx.load_tools` only appends (a system `tool_addition` after the user message, never a change to `tools`).
111
+ - A user's working-set drop (`/working-set drop`, [working-set.md](working-set.md#steering-drop-and-undrop)) changes
112
+ an earlier entry: it is saved between turns, so the next turn's first request is the first to send the stub and
113
+ the cache breaks there once, from the dropped output's entry on (the command's reply says that tail; an undrop
114
+ after the drop was sent breaks it once more). It is sent under every strategy, so it is the one edit that rewrites
115
+ history under `none`; without a drop, `none` sends the stored Array as it is. Its `working_set` note (what the user
116
+ dropped or brought back) is a tail system message after the history, one per turn with ops, escaped as context
117
+ notes are (`Prompt::ESCAPED_KINDS`). The turn-end warm-up never warms a drop sent after the turn: it misses once.
111
118
  - Side requests (idle recap, `/btw`) go through `IdleClient` with their own short system prompt. On a current
112
119
  llama.cpp they don't evict the session (see the server settings below); chi never pins them to a slot.
113
120
 
@@ -33,7 +33,8 @@ same-size edit in the same second goes unseen (APFS keeps nanoseconds).
33
33
 
34
34
  `Samagotchi::WorkingSet.build(conversation, cwd:, strategy:, stat: File.method(:stat))` (`lib/samagotchi/working_set.rb`)
35
35
  is pure and returns a `WorkingSet::Snapshot` (`items`, `at`: the conversation's size in entries, the web's ordering
36
- key). Each `WorkingSet::Item` is `kind` (`file`, `memory`, `url`), `key`, `state`, `tokens`, `ops`, `count` and
36
+ key). Each `WorkingSet::Item` is `kind` (`file`, `memory`, `url`), `key`, `state` (`live`, `stale`, `forgotten`,
37
+ `dropped`), `tokens`, `ops`, `count` and
37
38
  `first_id` (the first run's ToolIds id).
38
39
 
39
40
  **Runs.** It pairs the runs twice with `LLMContextStale.paired_runs`: once on the stored conversation, once on what
@@ -71,9 +72,21 @@ strategy. A content output is still sent when:
71
72
  A stale stub of an old read that a newer read superseded never strikes the file: the newer read is still sent. A file
72
73
  only edited or written is never forgotten.
73
74
 
75
+ **Dropped** is a forgotten item whose content outputs all hold the user's applied `:drop` (the user's layer, sent
76
+ under every strategy: [llm-context-forget.md](llm-context-forget.md)), so it is dropped under any strategy and after
77
+ a switch. An item with one output dropped and another forgotten by the model is forgotten. Under none with no drop
78
+ saved the build doesn't pair the sent conversation at all (the view returns the stored Array).
79
+
74
80
  **Stale** is a file item whose latest run left a stamp, and whose file now differs from that stamp in mtime or size,
75
81
  or is gone. No stamp, live. A forgotten item stays forgotten whatever its file did.
76
82
 
83
+ **An item's outputs.** `WorkingSet.outputs_of(conversation, kind:, key:, strategy:, cwd:)` answers an item's
84
+ content outputs (`WorkingSet::Output`: `id`, the tool_response entry's `index`, `derived` for a legacy entry's id,
85
+ and `sent`, the rule above) in conversation order. It walks the runs exactly as `build` does (one shared
86
+ `all_touches`), so what acts on an item's outputs and the projection never disagree: an item is forgotten exactly
87
+ when it has outputs and none is `sent`. An edit-only item, a failed run's file or an unknown key has none; a
88
+ `memory_read` of several names is an output of each name.
89
+
77
90
  **Cost.** Two `paired_runs` walks, one view build and one stat per file item. A 500-message session builds in about
78
91
  10 ms on an M5 Ultra and 75 ms on a CI runner; `spec/working_set_spec.rb` only guards against a blow-up (< 400 ms).
79
92
 
@@ -85,18 +98,62 @@ or is gone. No stamp, live. A forgotten item stays forgotten whatever its file d
85
98
  (`@kernel.turn_settings.llm_context`). Both loops start a generation after `apply_llm_context!`, so the set is
86
99
  what that request sends;
87
100
  - once in `Engine#end_turn`, after `replace_session_messages` and before the end event, from the kept messages (the
88
- turn-end apply has run by then).
101
+ turn-end apply has run by then);
102
+ - between turns, from `Engine#announce_working_set` (the session's messages, under the event lock; after a
103
+ `/working-set` op): announced
104
+ (`working_set_updated` is in `ANNOUNCEABLE_EVENTS`; held after a command's `command_ran` inside
105
+ `holding_announcements`), under the *next* turn's strategy (`SessionModelSettings#turn_llm_context`, as the
106
+ turn-end warm-up reads it; `TurnSettings` keeps the last turn's, stale after `/llm-context` or `/model`). While a
107
+ turn runs, the strategy is always the turn's.
89
108
 
90
109
  It is emitted only when the set differs from the last one emitted for the session (kept per session id), and never
91
- while the set is empty, so a tool-free turn on a fresh session emits nothing. It goes out whatever the web's flag;
110
+ while the set is empty, so a tool-free turn on a fresh session emits nothing. One exception: when the last one had
111
+ items and the set is empty now (a rollback removed the tool calls), one empty event goes out, so the page drops the
112
+ stale items; it is remembered, so the next empty request sends none. It goes out whatever the web's flag;
92
113
  the Bridge (`ClientEvent.project`) passes it on unchanged, and the attached TUI ignores it.
93
114
 
94
- **The route.** `GET /api/sessions/:id/working_set` (`Web::App#handle_working_set`) answers the snapshot's `to_h`.
95
- With a live worker it builds from the worker's snapshot messages (as `handle_show` reads them, keys symbolized) and
96
- the worker's llm ctx chip. Without one it builds from the session file and its saved chip
97
- (`SessionSetting::LLM_CONTEXT`). The folder is the session's `working_directory`. A worker's snapshot holds the
98
- session's messages, not the running turn's, so a GET during a turn answers an older `at` than that turn's events,
99
- and the page keeps the events' set.
115
+ **The route.** `GET /api/sessions/:id/working_set` (`Web::App#handle_working_set`) answers the snapshot's `to_h`
116
+ and `running`. With a live worker it builds from the worker's snapshot messages (as `handle_show` reads them, keys
117
+ symbolized) and the worker's llm ctx chip. Without one it builds from the session file and its saved chip
118
+ (`SessionSetting::LLM_CONTEXT`). The folder is the session's `working_directory`. `running` is true when the
119
+ snapshot has a turn in progress (`current_turn`).
120
+
121
+ **The GET while a turn runs.** A worker's snapshot messages are the session's, not the running turn's, so they lag
122
+ its events. The snapshot also carries `working_set`: the running turn's last `:working_set_updated` (`{items, at}`),
123
+ which the Bridge's `TurnAccumulator#working_set` folds from the event log. It is cleared at `:turn_started` and at
124
+ the turn's end, so it is nil between turns and before the turn emitted one. While a turn runs and it is set, the GET
125
+ answers it as is (no build, no stat): a page opened mid-turn gets the running turn's set at once, and its `at` is the
126
+ event's, so the page takes it (below). Before the turn's first event the turn's set is still the session's (the
127
+ Engine emits only a set that differs from the last one), and the GET builds from the messages as between turns; so
128
+ does one from an older worker without the field, its answer then behind the turn's events, which the page keeps.
129
+
130
+ ## Steering: drop and undrop
131
+
132
+ `/working-set drop KEY…` and `undrop KEY…` (`WorkingSetCommand`, `SessionCommands#working_set`; the web's list posts
133
+ the same lines on `POST /api/sessions/:id/command`) act on the session's messages between turns: the line is
134
+ `mid_turn: :queue`, so one sent while a turn runs waits for its end (bare `/working-set`, the listing, is
135
+ `:anytime`). `WorkingSetSteer` does the work on a copy (`Engine#clone_messages`, then `LLMContextEdit.store`'s new
136
+ edits Hashes), which `Engine#replace_messages` puts in place; the session is saved, the rollback window closes
137
+ (`TurnFlow#note_conversation_changed`, as `!cmd`), the result says `changed: [:messages]` (the web reads the
138
+ conversation again) and `Engine#announce_working_set` re-emits the set, held behind the command's `command_ran`.
139
+
140
+ - **The outputs** are `WorkingSet.outputs_of` the item under the strategy the set is built under
141
+ (`Engine#working_set_strategy`): the ones still `sent` get an applied `:drop` (`by: "user"`), the edit each held
142
+ before kept in its `was:`. A drop's view is the user's layer ([llm-context-forget.md](llm-context-forget.md)).
143
+ The reply weighs the batch (`LLMContextApply.weigh`: what it frees, the tail the server reads again).
144
+ - **undrop** takes the item's drops off and puts each `was:` back. Free when the drop never reached a request (its
145
+ op is still in the pending note), else the reply says the tail.
146
+ - **Refusals** live on one exception, `WorkingSetSteer::Refused` (its `reason` and words): not in the working set;
147
+ nothing to drop (only edited or written; only a legacy entry's derived ids; none of it sent now); already
148
+ dropped; not dropped. `forget_outputs` uses the same words for a dropped output ("dropped by the user"): the
149
+ model can neither forget nor restore it. A `memory_read` of several names is one output of each: dropping one
150
+ name drops that read for the others too.
151
+ - **The note**: the ops since the last turn are one tail system message, `kind: working_set` (in
152
+ `Prompt::ESCAPED_KINDS`), its ops in `working_set_ops`. It is pending while only system messages follow it
153
+ (`WorkingSetSteer.pending_note_index`); a later op joins it, and an inverse op (an undrop of a pending drop) takes
154
+ both off, the note too when nothing is left. A turn that sends it makes it history; the next op starts a new one.
155
+
156
+ The projection shows a dropped item as `dropped` (below), the GET and the event alike.
100
157
 
101
158
  ## The page's overlay
102
159
 
@@ -111,8 +168,19 @@ An item the server doesn't know yet (a running call) stays live. Known mismatch:
111
168
  worker's `Dir.pwd` (a real path), the server against `working_directory`, so a session under `/tmp` can miss
112
169
  (`/private/tmp`).
113
170
 
114
- `working_set_view.js` keeps one server snapshot per session and takes a new one only when its `at` is at least the
115
- kept one's (`newerSnapshot`): a GET that answers after a newer event never wins. The page asks for it when it seeds a
171
+ `working_set_view.js` keeps one server snapshot per session; `working_set.js newerSnapshot` picks which. `at` can't
172
+ order them: a rollback (a canceled turn's `end_turn`, a failed turn's `TurnFlow` restore) shortens the conversation,
173
+ so the right set can have the smaller `at`. The page orders by what it saw instead:
174
+ - an event is always taken. The stream delivers them in the order the Engine built them, so the newest event is the
175
+ server's latest set, a rollback's smaller (or empty) one too;
176
+ - a GET's answer is dropped when an event came while it was on its way (the view counts the events it took and
177
+ compares the count at the ask with the count at the answer): the event may be newer;
178
+ - else it is taken, unless it is `running` and its `at` is smaller than the kept one's (it is behind the running
179
+ turn's events: a GET built from the messages). A `running` GET answered from the turn's last event has that
180
+ event's `at`, so it is taken. An idle GET after a rollback the page missed (a resync) or after a file changed wins at any `at`.
181
+
182
+ No server sequence: the GET is built in `chi web` from the worker's snapshot or the file, the events in the worker,
183
+ and a worker's event count starts over when it respawns; the page's own order needs neither. The page asks for it when it seeds a
116
184
  session's set (load, resync) and when the list opens (a file changed while the session sat idle). The stream handler
117
185
  passes `working_set_updated` to it, and does nothing when the flag is off. A new snapshot redraws:
118
186
  - the session bar's chip and the open list;
@@ -123,6 +191,13 @@ passes `working_set_updated` to it, and does nothing when the flag is off. A new
123
191
  A state is the item's state now, not when its turn ran. A state change never refolds and never moves a line: the
124
192
  rail's line key carries the label's state and its forgotten count, which are patched in place.
125
193
 
194
+ **Steering from the page.** The list's `drop`/`undrop` (and `undo`, `undo all`) post `/working-set` lines on the
195
+ command route (`createWorkingSet`'s `sendLine`). Pending ops are the queued lines (`command_queued` with `waits:
196
+ "turn_end"`, any client's, and the snapshot's `queued_commands` replay after a reload), parsed and netted by
197
+ `working_set.js pendingSteers` until each one's `command_ran`; a seed clears them (the replay brings them back).
198
+ A steer line names a file by its absolute path, a memory as `memory:NAME`, a URL by the server's full key. `dropped`
199
+ is in `STATES`: struck like forgotten, and a group is `dropped` when all its members are.
200
+
126
201
  **Looks** (design spec §3, §5): a stale line is faded (`opacity: .55`); a forgotten one is struck in `--text-faint`
127
202
  at `opacity: .5`. A group shows `✂n` for its forgotten members. It is struck only when all its members are
128
203
  forgotten, and faded when all are stale or forgotten (`groupState`). The list's ops text says `stale` or `forgotten`
data/docs/plugins.md CHANGED
@@ -780,15 +780,18 @@ The Engine itself is never handed to a plugin.
780
780
 
781
781
  A bundle's settings are config.yml's, for every session. A setting a session may have of its own (a child's cap,
782
782
  say) is one the manifest lists in `session_settings:`; `chi --set BUNDLE.KEY=VALUE` and `chi send --new --set …`
783
- then give it to a new session, saved with it (a respawn, a fork and a continued link keep it; a delegate child
783
+ (which also take a few core keys, `--set recap.enabled=false`: see
784
+ [Per-session settings](configuration.md#per-session-settings)) then give it to a new session, saved with it (a respawn, a fork and a continued link keep it; a delegate child
784
785
  doesn't). chi refuses a bundle that isn't installed (or whose plugin doesn't load) and a key the manifest doesn't
785
786
  list, so a typo isn't saved. A manifest that types a key (`session_settings: {cap: number}`: `number`, which
786
787
  takes off too, `boolean`, `string`, or `any`) gets only a value of that type: `--set my-bundle.cap=five` is refused,
787
788
  exit 2, as a key it doesn't list is (a mapping needs a chi newer than 0.91.0). Values are typed simply: an integer (`3`, `-1`) or a decimal (`2.5`, `.5`, `1e1`)
788
789
  is a number, `true`, `yes` and `on` are true, `false`, `no` and `off` are false (any case, as config.yml's YAML reads
789
790
  them), and anything else stays the text as given (`010`, `0x10`, `3:30`, `y`, `null`; YAML would read the first
790
- three as numbers and `null` as nothing); a key typed `string` keeps its text as given (`yes`, `5`, `1.10`); `POST /api/sessions` `bundle_settings` types its strings the same way and
791
- keeps JSON numbers and booleans. `ctx.settings` is the config's section with the session's values laid over it (one level: a session's
791
+ three as numbers and `null` as nothing); a key typed `string` keeps its text as given (`yes`, `5`, `1.10`); `POST /api/sessions` `settings` (`{"my-bundle.cap": 3}`) types its strings the same way and
792
+ keeps JSON numbers and booleans. A name that is a core session setting is always chi's: a manifest whose
793
+ `session_settings:` key `K` makes `NAME.K` one (a bundle `recap` offering `enabled`) is refused at install, and
794
+ `chi bundle check` reports it as an `error clash`, in the same words. `ctx.settings` is the config's section with the session's values laid over it (one level: a session's
792
795
  `cap` replaces the config's `cap`). Read such a key from `ctx.settings` where it is used, not in
793
796
  `initialize(settings)`: that runs once at load, before any session, and gets the config only.
794
797