samagotchi 0.100.1 → 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 (76) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +50 -1
  3. data/bin/chi +14 -10
  4. data/docs/architecture.md +10 -2
  5. data/docs/cli.md +100 -5
  6. data/docs/configuration.md +26 -4
  7. data/docs/internals/bundles.md +1 -1
  8. data/docs/internals/engine.md +1 -0
  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 +205 -0
  12. data/docs/plugins.md +6 -3
  13. data/docs/sessions.md +28 -11
  14. data/docs/testing.md +8 -1
  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 +92 -53
  24. data/lib/samagotchi/engine.rb +153 -4
  25. data/lib/samagotchi/host_keys.rb +35 -18
  26. data/lib/samagotchi/idle_scheduler.rb +16 -1
  27. data/lib/samagotchi/llm_context_edit.rb +32 -10
  28. data/lib/samagotchi/llm_context_forget.rb +12 -3
  29. data/lib/samagotchi/llm_context_view.rb +19 -7
  30. data/lib/samagotchi/memory_bundle/installer.rb +2 -0
  31. data/lib/samagotchi/memory_bundle/manifest.rb +13 -2
  32. data/lib/samagotchi/message_parts.rb +8 -3
  33. data/lib/samagotchi/plugin/context.rb +2 -2
  34. data/lib/samagotchi/prompt.rb +1 -1
  35. data/lib/samagotchi/recap_command.rb +68 -0
  36. data/lib/samagotchi/recap_setup.rb +27 -14
  37. data/lib/samagotchi/send_command.rb +14 -11
  38. data/lib/samagotchi/session.rb +19 -12
  39. data/lib/samagotchi/session_commands.rb +44 -2
  40. data/lib/samagotchi/session_config.rb +411 -0
  41. data/lib/samagotchi/session_setup.rb +9 -9
  42. data/lib/samagotchi/sessions_command.rb +6 -2
  43. data/lib/samagotchi/terminal_ui/attach_launcher.rb +5 -5
  44. data/lib/samagotchi/terminal_ui/attached_loop.rb +2 -1
  45. data/lib/samagotchi/terminal_ui/formatting.rb +10 -3
  46. data/lib/samagotchi/terminal_ui.rb +8 -6
  47. data/lib/samagotchi/testing/bundle.rb +3 -2
  48. data/lib/samagotchi/testing/fake_executable.rb +6 -16
  49. data/lib/samagotchi/testing/fake_executable_launcher +6 -0
  50. data/lib/samagotchi/tool_response.rb +10 -6
  51. data/lib/samagotchi/tool_runner.rb +25 -6
  52. data/lib/samagotchi/version.rb +1 -1
  53. data/lib/samagotchi/web/app.rb +76 -11
  54. data/lib/samagotchi/web/public/app.js +35 -3
  55. data/lib/samagotchi/web/public/chip_popover.js +56 -17
  56. data/lib/samagotchi/web/public/commands.js +8 -2
  57. data/lib/samagotchi/web/public/data.js +7 -0
  58. data/lib/samagotchi/web/public/index.html +145 -1
  59. data/lib/samagotchi/web/public/info_bar.js +16 -4
  60. data/lib/samagotchi/web/public/info_bar_model.js +7 -3
  61. data/lib/samagotchi/web/public/refs.js +10 -1
  62. data/lib/samagotchi/web/public/stage_model.js +6 -5
  63. data/lib/samagotchi/web/public/stage_view.js +34 -2
  64. data/lib/samagotchi/web/public/stream_handlers.js +10 -1
  65. data/lib/samagotchi/web/public/turn_html.js +5 -0
  66. data/lib/samagotchi/web/public/turn_view.js +16 -4
  67. data/lib/samagotchi/web/public/working_set.js +612 -0
  68. data/lib/samagotchi/web/public/working_set_rail.js +278 -0
  69. data/lib/samagotchi/web/public/working_set_view.js +541 -0
  70. data/lib/samagotchi/web/server.rb +6 -4
  71. data/lib/samagotchi/worker.rb +9 -6
  72. data/lib/samagotchi/working_set.rb +238 -0
  73. data/lib/samagotchi/working_set_command.rb +119 -0
  74. data/lib/samagotchi/working_set_steer.rb +250 -0
  75. metadata +11 -2
  76. data/lib/samagotchi/bundle_session_settings.rb +0 -251
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 47dbac4f107af7d9c7d9d571b907a1e9e777326732614ed34986f2dce954168a
4
- data.tar.gz: 4335f033596dc75f3db80b754a347e36862d0fd07f1d72bafcc7a5c50ed539ff
3
+ metadata.gz: '084d47c40205bd43fa1126dd3169a0792cc6faee949899bf6b62d84a559d1501'
4
+ data.tar.gz: 23dde4e086c819fc43349cb20afed56121fb51bfb3b54b9be6e90fd8a332003b
5
5
  SHA512:
6
- metadata.gz: a2454b93c9271b14ba0349c2991f6b0312dfdee76fd6a90005feb38726025dad71b6b4f1701bcbb2622c3db786d86fe2e58636668aa2f7d43d0ac289cae36e5a
7
- data.tar.gz: 8fef8d270b58719f286b57c840f1a5fc3a53de10fec01fdcc08ea3de1315de17d267a834759b05ae09c05ca80b8f27009329eb38ff0aaf9a7b6ab59585b3e1d3
6
+ metadata.gz: 763b8db4ab722ec994871d8d5e4e1238beda7ae846c9cf8bf5f9dc0f2c8d545eb767b7d82afcf35a8cb096f2f30461a8a793068c5359d5c926a7209e626c91b9
7
+ data.tar.gz: abb0665762926a27e66133b5326cf8b18658e5404047d9f1ce414c6455ac3ddffa15667c00ab6a6ad929a9d15d451f667ad11b702d08bccb9fa9053161415a5b
data/CHANGELOG.md CHANGED
@@ -8,6 +8,53 @@ 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
+
37
+ ## [0.101.0] - 2026-10-22
38
+
39
+ ### Added
40
+
41
+ - Web (experimental, off by default): the stage view's **working set**, which shows what defines the model's
42
+ context: the files it read or edited, the memories, the fetched URLs and the preloaded and muted memories. It
43
+ shows when each one came in, folded into a few lines beside the stage. It replaces the mem chip with
44
+ `● set N`, adds a footprint line under each finished turn, and opens a list whose step tags jump to the call.
45
+ Items say whether the model still has them: **stale** (faded) when the file changed after the model last saw it,
46
+ through `execute`, your editor or anything else (the model's own edits don't count); **forgotten** (struck)
47
+ when the LLM context strategy no longer sends any of it. Each row shows its size in tokens. Turn it on with
48
+ `web.working_set: true`, `chi web --web-working-set` or `?working_set=1`. See
49
+ [docs/cli.md](docs/cli.md#the-working-set).
50
+
51
+ ### Fixed
52
+
53
+ - macOS: reading a host's API key from the Keychain can no longer hang chi past its 5 s deadline when starting
54
+ `security` stalls (a first run scanned by XProtect): chi warns and goes on without the key.
55
+
56
+ Update with `chi update`.
57
+
11
58
  ## [0.100.1] - 2026-10-22
12
59
 
13
60
  ### Fixed
@@ -3807,7 +3854,9 @@ and long-lived sessions.
3807
3854
  - A macOS desktop helper (`chi desktop install`): a "Send to chi" Service and a
3808
3855
  hotkey panel that send selected text or the clipboard to your sessions.
3809
3856
 
3810
- [Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.100.1...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
3859
+ [0.101.0]: https://github.com/dm1try/samagotchi/compare/v0.100.1...v0.101.0
3811
3860
  [0.100.1]: https://github.com/dm1try/samagotchi/compare/v0.100.0...v0.100.1
3812
3861
  [0.100.0]: https://github.com/dm1try/samagotchi/compare/v0.99.0...v0.100.0
3813
3862
  [0.99.0]: https://github.com/dm1try/samagotchi/compare/v0.98.0...v0.99.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: " \
@@ -333,6 +335,7 @@ if options[:web]
333
335
  view = Samagotchi::Config.get("web.view")
334
336
  exit Samagotchi::Web::Server.launch(port: port, scope: options[:scope] || "project", open_browser: options[:web_open],
335
337
  markdown: markdown, view: view, new_token: options[:new_token],
338
+ working_set: Samagotchi::Config.get("web.working_set"),
336
339
  annotate_presets: Samagotchi::Config.get("web.annotate_presets"),
337
340
  editor: Samagotchi::Config.get("web.editor"))
338
341
  end
@@ -434,13 +437,14 @@ if options[:toolkit] || options[:tools] || options[:no_tools]
434
437
  remove: options[:no_tools])
435
438
  end
436
439
 
437
- # --set BUNDLE.KEY=VALUE: a new session's bundle settings, checked here
438
- # 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.
439
443
  if options[:sets]
440
- require "samagotchi/bundle_session_settings"
444
+ require "samagotchi/session_config"
441
445
  begin
442
- options[:bundle_settings] = Samagotchi::BundleSessionSettings.from_flags(options[:sets])
443
- rescue Samagotchi::BundleSessionSettings::Error => e
446
+ options[:settings] = Samagotchi::SessionConfig.from_flags(options[:sets])
447
+ rescue Samagotchi::SessionConfig::Error => e
444
448
  warn "Error: #{e.message}"
445
449
  exit 2
446
450
  end
@@ -499,7 +503,7 @@ if launch == :attached
499
503
  appended_prompt_files: options[:appended_prompt_files] || [],
500
504
  toolkit: options[:toolkit_value],
501
505
  toolkit_flags: options[:toolkit_flags] || [],
502
- bundle_settings: options[:bundle_settings] || {},
506
+ settings: options[:settings],
503
507
  parent_answers: options[:parent_answers])
504
508
  rescue Samagotchi::TerminalUI::AttachLauncher::Error => e
505
509
  warn "Error: #{e.message}"
@@ -522,7 +526,7 @@ begin
522
526
  appended_prompt_files: options[:appended_prompt_files] || [],
523
527
  toolkit: options[:toolkit_value],
524
528
  toolkit_flags: options[:toolkit_flags] || [],
525
- bundle_settings: options[:bundle_settings] || {},
529
+ settings: options[:settings],
526
530
  parent_answers: options[:parent_answers],
527
531
  non_interactive: options[:non_interactive],
528
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"`
@@ -445,6 +446,13 @@ The page's frequent reads of `GET /api/sessions/:id` stay light too:
445
446
  `turn_count`, when the page's merge came up short.
446
447
  - `?cards=1`: the cards, from `/tail`; the session file is only stat'ed for the 404.
447
448
 
449
+ The stage view's working set (`web.working_set`) has its own read, `GET /api/sessions/:id/working_set`: the
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)).
455
+
448
456
  A worker without `/tail` (an older chi; `404 not_found`) is read through `/snapshot` in the same
449
457
  shape; any other failure (no worker, a 500, a timeout) reads the disk, trimmed to the newest
450
458
  turn's records.
@@ -603,7 +611,7 @@ A map of the parts the overview above doesn't name (paths under `lib/samagotchi/
603
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. |
604
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. |
605
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)). |
606
- | 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). |
607
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)). |
608
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)). |
609
617
  | Shipped bundles | `bundles/` (`mcp`, `github-pr`, `loop-guard`, `skills`, `source-links`, …) | Bundles that ship with chi. |
data/docs/cli.md CHANGED
@@ -15,7 +15,8 @@
15
15
  - `chi web --web-host lan` — the Web UI on your home network too, for your phone: a link with an access token and its QR code (see [chi web on your phone](#chi-web-on-your-phone)); `chi web --new-token` replaces the token
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
- - `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
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, 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
19
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))
20
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))
21
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))
@@ -281,7 +282,7 @@ controls exit behavior (`--non-interactive`); `--resume` composes with both.
281
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. |
282
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). |
283
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). |
284
- | `--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). |
285
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. |
286
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. |
287
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). |
@@ -499,7 +500,8 @@ a terminal, headed by `<id8> <status> <model>`: a live worker answers its
499
500
  Bridge's `GET stats`, else the snapshot is rebuilt from the session's saved
500
501
  `analytics.json` (never starting a worker; a worker that does not answer in
501
502
  time falls back to the disk and the line says so). `--format json` prints one
502
- 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
503
505
  `analytics.json` yet reports `metrics: null` (`(no metrics yet)` in text) and
504
506
  exits 0. An unknown or ambiguous id exits 1 with `chi sessions stop`'s
505
507
  wording. See [Sessions](sessions.md#cli).
@@ -601,6 +603,25 @@ the same values on its `llm context:` line. The web's info bar shows them as the
601
603
  through `/llm-context`. See "LLM context: a session's own strategy" in
602
604
  [configuration.md](configuration.md).
603
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
+
604
625
  `/thinking` shows the thinking level the next turn runs at and where it came
605
626
  from (`session`, `--thinking` / `SAMAGOTCHI_THINKING_LEVEL`, `models: <key>`,
606
627
  `hosts.<name>`, `thinking.level`). `/thinking off|low|medium|high` sets the
@@ -740,7 +761,7 @@ REPL alike:
740
761
  `cutting in…`, then `steered`. A phone shows the short form (`↳ after this
741
762
  thinking · 0:42`) with `cut in` and `queue` chips.
742
763
  - Commands answer, wait or are refused, by what they do:
743
- - 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
744
765
  plugin's anytime command (`/btw`), and the commands that only show
745
766
  something: `/models` (with a filter too), and `/model`, `/llm-context`,
746
767
  `/thinking`, `/reasoning-history` and `/guardrails` alone.
@@ -956,6 +977,71 @@ steps saved before that have none, so they show no thinking. An `edit` or
956
977
  made (up to 120 lines or 8 KB), live and after a reload (see
957
978
  [Guardrails](guardrails.md#ask) for the diff an approval shows first).
958
979
 
980
+ ### The working set
981
+
982
+ `web.working_set: true` (or `chi web --web-working-set`,
983
+ `SAMAGOTCHI_WEB_WORKING_SET=true`; `?working_set=1` or `?working_set=0` on the
984
+ page URL for one page load) shows, in the stage view, what a turn brought into
985
+ the model's context: the files it read, edited or wrote, the memories it read
986
+ or wrote and the URLs it fetched. Off by default while it is tried out; the
987
+ turn view ignores it.
988
+
989
+ - **The rail**, on the right of the stage's live slots: the running turn's set
990
+ in four lines, folded as it grows (files under one folder become
991
+ `…/web/public/ · 9 files ✎2`, memories `◆ 3 memories`, else `N files · K
992
+ dirs`), with the newest entries marked for a few seconds and the context
993
+ ring once its fill is known. It never changes the stage's height. Its `–`
994
+ folds it to one line (`SET 17 + ◆ name`), kept per browser; a click opens the
995
+ list. In a stage narrower than 760 px it is one line under the last calls;
996
+ on a phone it isn't shown.
997
+ - **The footprint**, one line under each finished turn's block, live and after
998
+ a reload: `touched …/web/public/ · 4 files · ◆ 2 memories s2–10 · +1 url ·
999
+ edited 1`. A step tag (`s2`) goes to that call's row; the rest opens the
1000
+ turn's list. A turn that touched nothing has none.
1001
+ - **The `set N` chip** in the session bar, in place of the `mem N` chip (whose
1002
+ tooltip goes too): the session's whole set, preloaded (`--memory`) and muted
1003
+ (`--mute`) memories included; its dot lights when the set changed since you
1004
+ last opened it.
1005
+ - **The list** the three open (a bottom sheet on a phone): this turn or the
1006
+ whole session, files grouped by folder (a group opens in place), memories
1007
+ (used, preloaded, muted) and URLs, each with what was done to it (`read ×2`,
1008
+ `read, edited`, `preloaded`) and a step tag (`s3`, `t2·s3` in the session's
1009
+ scope, `prompt` for a preloaded memory) that goes to the call. A file opens
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.
1019
+
1020
+ Only `read`, `edit`, `write`, `memory_read`, `memory_write` and `web_fetch`
1021
+ calls that succeeded count; files an `execute` touched don't.
1022
+
1023
+ What the model still has of each item comes from chi, not the page, so it is
1024
+ the same live, after a reload and in every tab:
1025
+ - **stale** (faded): the file changed since the model last read, edited or
1026
+ wrote it, whatever changed it (a later `execute` such as `sed` or `git
1027
+ checkout`, your editor, another session). The model's own successful edit or
1028
+ write doesn't make a file stale. Chi checks at each request and when you
1029
+ open the list, so a file changed while the session sat idle shows stale on
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.
1034
+ - **forgotten** (struck): nothing of the item's content is still sent to the
1035
+ model under the session's current LLM context strategy (`forget_outputs`
1036
+ forgot every read of it; see
1037
+ [the forget layer](configuration.md#llm-context-the-forget-layer)). Lines a
1038
+ forget kept count as still sent. Under `/llm-context strategy none` nothing
1039
+ is cut, so a forgotten item is live again from the next request.
1040
+
1041
+ The list says `stale`, `forgotten` or `dropped` in words with each row, and how many
1042
+ tokens its content costs as sent (`~1.2k`); a folded group shows its forgotten
1043
+ members as `✂n`, and a footprint adds `n forgotten`.
1044
+
959
1045
  ### Web annotate presets
960
1046
 
961
1047
  Selecting text in an answer, a thinking block, a tool row or one of your
@@ -1065,7 +1151,16 @@ working on, what came of it and what is still open, not a turn-by-turn log.
1065
1151
  after it (`recap (before the last 2 turns)>`). One written while you sit at
1066
1152
  the prompt prints there.
1067
1153
  - `/recap` shows the saved one and asks for a new one when the chat moved on
1068
- (`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.
1069
1164
  - One written while a continue offer waits (the last turn ran out of steps)
1070
1165
  says that turn stopped before the task was finished. Answering `no` counts
1071
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). |
@@ -1689,6 +1710,7 @@ column says so, a `--kebab-name` flag. The maps (`hosts:`, `models:`,
1689
1710
  | `web.host` | `127.0.0.1` | yes | `127.0.0.1`, `::1` or `localhost`; `lan` (this machine's private IPv4 address) or one of its IPv4 addresses also opens `chi web` to the network, with an access token (`chi web --new-token` replaces it). Anything else binds `127.0.0.1` with a warning. See [CLI: chi web on your phone](cli.md#chi-web-on-your-phone). |
1690
1711
  | `web.markdown` | `false` | yes | Render answers as Markdown in `chi web`. |
1691
1712
  | `web.view` | `stage` | yes | How `chi web` draws a turn: `stage` (the running turn pinned above the composer) or `turn` (one block per turn in the history). See [CLI](cli.md#web-views). |
1713
+ | `web.working_set` | `false` | yes | The stage view's working set (experimental): the files, memories and URLs a turn brought into the model's context, in a rail in the stage, a footprint line under each finished turn and a `set N` chip in place of the mem chip. `?working_set=1` or `?working_set=0` on the page URL overrides it for that page load; the turn view ignores it. See [CLI: the working set](cli.md#the-working-set). |
1692
1714
  | `web.annotate_presets` | `Agreed\|Could you please elaborate?` | yes | Quick replies next to Annotate in `chi web`, `\|`-separated (a YAML list works too); `""` in the file or on the CLI leaves only Annotate (an empty env value means the default). See [CLI](cli.md#web-annotate-presets). |
1693
1715
  | `web.editor` | `vscode` | yes | What a file ref in `chi web` (a file tool row's title, a diff line) opens, for a viewer on this machine: a preset (`vscode`, `vscode-insiders`, `cursor`, `zed`) or a URL template with `{path}` and an optional `{line}`; `none` turns refs off. See [Opening files from chi web](#opening-files-from-chi-web). |
1694
1716
  | `history.file` | state dir | | Prompt history path, shared by the TUI and the web composer. |
@@ -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,6 +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, 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) |
512
513
  | `:hook_notice` | a hook's or plugin's notify during a turn | `{hook, text, level}` (announced with `between_turns: true` outside a turn) |
513
514
  | `:guardrail_warning` | a guardrails line during a turn | `{message}` |
514
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