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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +28 -1
- data/bin/chi +13 -10
- data/docs/architecture.md +8 -6
- data/docs/cli.md +47 -7
- data/docs/configuration.md +25 -4
- data/docs/internals/bundles.md +1 -1
- data/docs/internals/engine.md +1 -1
- data/docs/internals/llm-context-forget.md +14 -0
- data/docs/internals/prompt-caching.md +8 -1
- data/docs/internals/working-set.md +86 -11
- data/docs/plugins.md +6 -3
- data/docs/sessions.md +28 -11
- data/docs/testing.md +7 -0
- data/lib/samagotchi/bridge/turn_accumulator.rb +16 -0
- data/lib/samagotchi/bridge.rb +9 -3
- data/lib/samagotchi/bundle_command/check.rb +5 -1
- data/lib/samagotchi/bundle_command/try/command.rb +6 -6
- data/lib/samagotchi/bundle_command/try/sandbox.rb +3 -3
- data/lib/samagotchi/bundle_command.rb +4 -4
- data/lib/samagotchi/bundles/system/manifest.yml +1 -1
- data/lib/samagotchi/commands/registry.rb +12 -1
- data/lib/samagotchi/config.rb +89 -53
- data/lib/samagotchi/engine.rb +131 -12
- data/lib/samagotchi/idle_scheduler.rb +16 -1
- data/lib/samagotchi/llm_context_edit.rb +32 -10
- data/lib/samagotchi/llm_context_forget.rb +12 -3
- data/lib/samagotchi/llm_context_view.rb +19 -7
- data/lib/samagotchi/memory_bundle/installer.rb +2 -0
- data/lib/samagotchi/memory_bundle/manifest.rb +13 -2
- data/lib/samagotchi/message_parts.rb +8 -3
- data/lib/samagotchi/plugin/context.rb +2 -2
- data/lib/samagotchi/prompt.rb +1 -1
- data/lib/samagotchi/recap_command.rb +68 -0
- data/lib/samagotchi/recap_setup.rb +27 -14
- data/lib/samagotchi/send_command.rb +14 -11
- data/lib/samagotchi/session.rb +19 -12
- data/lib/samagotchi/session_commands.rb +44 -2
- data/lib/samagotchi/session_config.rb +411 -0
- data/lib/samagotchi/session_setup.rb +9 -9
- data/lib/samagotchi/sessions_command.rb +6 -2
- data/lib/samagotchi/terminal_ui/attach_launcher.rb +5 -5
- data/lib/samagotchi/terminal_ui/attached_loop.rb +2 -1
- data/lib/samagotchi/terminal_ui/formatting.rb +10 -3
- data/lib/samagotchi/terminal_ui.rb +8 -6
- data/lib/samagotchi/testing/bundle.rb +3 -2
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +30 -13
- data/lib/samagotchi/web/public/app.js +2 -0
- data/lib/samagotchi/web/public/commands.js +8 -2
- data/lib/samagotchi/web/public/data.js +3 -2
- data/lib/samagotchi/web/public/index.html +20 -2
- data/lib/samagotchi/web/public/stream_handlers.js +4 -0
- data/lib/samagotchi/web/public/turn_html.js +5 -0
- data/lib/samagotchi/web/public/working_set.js +113 -21
- data/lib/samagotchi/web/public/working_set_view.js +136 -21
- data/lib/samagotchi/worker.rb +9 -6
- data/lib/samagotchi/working_set.rb +49 -11
- data/lib/samagotchi/working_set_command.rb +119 -0
- data/lib/samagotchi/working_set_steer.rb +250 -0
- metadata +5 -2
- data/lib/samagotchi/bundle_session_settings.rb +0 -251
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: '084d47c40205bd43fa1126dd3169a0792cc6faee949899bf6b62d84a559d1501'
|
|
4
|
+
data.tar.gz: 23dde4e086c819fc43349cb20afed56121fb51bfb3b54b9be6e90fd8a332003b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
|
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
|
|
137
|
-
|
|
138
|
-
|
|
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
|
|
439
|
-
# against
|
|
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/
|
|
444
|
+
require "samagotchi/session_config"
|
|
442
445
|
begin
|
|
443
|
-
options[:
|
|
444
|
-
rescue Samagotchi::
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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)
|
|
450
|
-
worker's `/snapshot` messages, else the session file, under the session's LLM context strategy
|
|
451
|
-
|
|
452
|
-
|
|
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
|
-
|
|
|
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
|
|
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
|
|
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}
|
|
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
|
|
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 `
|
|
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.
|
data/docs/configuration.md
CHANGED
|
@@ -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
|
|
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). |
|
data/docs/internals/bundles.md
CHANGED
|
@@ -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 `
|
|
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` |
|
data/docs/internals/engine.md
CHANGED
|
@@ -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
|
|
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
|
|
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.
|
|
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
|
|
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`.
|
|
98
|
-
|
|
99
|
-
|
|
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
|
|
115
|
-
|
|
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
|
-
|
|
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` `
|
|
791
|
-
keeps JSON numbers and booleans.
|
|
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
|
|