samagotchi 0.53.0 → 0.54.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 (63) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +32 -1
  3. data/README.md +1 -1
  4. data/docs/architecture.md +94 -54
  5. data/docs/configuration.md +1 -2
  6. data/docs/context.md +104 -9
  7. data/docs/internals/background-tasks.md +1 -1
  8. data/docs/internals/bundles.md +73 -0
  9. data/docs/internals/context-telemetry.md +8 -5
  10. data/docs/internals/engine.md +16 -7
  11. data/docs/internals/tool-guardrails.md +8 -6
  12. data/docs/plugins.md +2 -1
  13. data/docs/sessions.md +1 -1
  14. data/lib/samagotchi/bridge/turn_accumulator.rb +2 -0
  15. data/lib/samagotchi/bundle_command.rb +31 -36
  16. data/lib/samagotchi/bundle_needs.rb +4 -5
  17. data/lib/samagotchi/bundles/skills/manifest.yml +2 -2
  18. data/lib/samagotchi/bundles/skills/plugin.rb +2 -1
  19. data/lib/samagotchi/bundles/system/config_modification_protocol.md +1 -1
  20. data/lib/samagotchi/bundles/system/manifest.yml +2 -2
  21. data/lib/samagotchi/child_move.rb +6 -4
  22. data/lib/samagotchi/config.rb +0 -1
  23. data/lib/samagotchi/context_command.rb +1 -1
  24. data/lib/samagotchi/context_providers.rb +11 -14
  25. data/lib/samagotchi/context_status.rb +10 -18
  26. data/lib/samagotchi/engine.rb +117 -134
  27. data/lib/samagotchi/extension_load.rb +102 -0
  28. data/lib/samagotchi/guardrail_wiring.rb +16 -16
  29. data/lib/samagotchi/kernel_loop.rb +5 -3
  30. data/lib/samagotchi/llm/chat_loop.rb +5 -4
  31. data/lib/samagotchi/memory_bundle/asset_installer.rb +302 -0
  32. data/lib/samagotchi/memory_bundle/builder.rb +170 -178
  33. data/lib/samagotchi/memory_bundle/installed_bundle.rb +90 -0
  34. data/lib/samagotchi/memory_bundle/installer.rb +252 -488
  35. data/lib/samagotchi/memory_bundle/listing.rb +6 -6
  36. data/lib/samagotchi/memory_bundle/manifest.rb +4 -2
  37. data/lib/samagotchi/memory_bundle/plugin_ref.rb +27 -0
  38. data/lib/samagotchi/memory_bundle/profile.rb +21 -21
  39. data/lib/samagotchi/memory_bundle/provenance.rb +38 -40
  40. data/lib/samagotchi/memory_bundle/shipped_update.rb +24 -26
  41. data/lib/samagotchi/memory_bundle/status.rb +22 -24
  42. data/lib/samagotchi/memory_bundle/system_bundle.rb +4 -5
  43. data/lib/samagotchi/memory_bundle/uninstaller.rb +8 -10
  44. data/lib/samagotchi/plugin/loader.rb +8 -8
  45. data/lib/samagotchi/self_report.rb +2 -2
  46. data/lib/samagotchi/send_command.rb +1 -1
  47. data/lib/samagotchi/session_continue.rb +275 -0
  48. data/lib/samagotchi/session_manager.rb +9 -253
  49. data/lib/samagotchi/session_metrics.rb +3 -1
  50. data/lib/samagotchi/terminal_ui/attached_loop.rb +2 -1
  51. data/lib/samagotchi/terminal_ui.rb +12 -2
  52. data/lib/samagotchi/tool_activity.rb +84 -14
  53. data/lib/samagotchi/version.rb +1 -1
  54. data/lib/samagotchi/web/app.rb +1 -1
  55. data/lib/samagotchi/web/public/activity.js +2 -0
  56. data/lib/samagotchi/web/public/app.js +6 -6
  57. data/lib/samagotchi/web/public/ctx.js +18 -4
  58. data/lib/samagotchi/web/public/index.html +4 -2
  59. data/lib/samagotchi/web/public/question_card.js +9 -1
  60. data/lib/samagotchi/web/public/question_cards.js +5 -3
  61. data/lib/samagotchi/web/public/timing.js +8 -2
  62. data/lib/samagotchi/web/public/turn_html.js +10 -6
  63. metadata +7 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f1d170176f088bc87560a05b7596d57925d955011cc5818b93f5ae33ae2b1f55
4
- data.tar.gz: 941c3bd879e09b78144ede81d4c6cf4724f9ca3a7ce8fa62f0174524a26f25bf
3
+ metadata.gz: 143c9195fbbe50c8f8193c76815004f30354a01d2e1ec7d5019e70bc79c376df
4
+ data.tar.gz: c1047a49f05415f8ea9a2789cf32ecc2f6edbcbf5bbfa432a8b919bd193ae360
5
5
  SHA512:
6
- metadata.gz: ddc3c2d02b18137887e5e90f636b23dd3ea44461f33a723e4033d58f4cfcd9c7be3267db9cc3349aba1d3af6b73a398415696e3af2c8fdad6662704944dad189
7
- data.tar.gz: bd8bded906f5e3855c8f63ab081582b601d4b5332bb10e9ed5a7dff36e185851583c95d28e8b3528ece4d8b34463cc6e65fd16bb95bf2c493de502d9efa79775
6
+ metadata.gz: 12b4e73421655936e2d3183e7839f4a8580b1d2b93633cff098170540e2c98ba61cf02c847a4af8b5e19a59424f15691a81d0b38195640263773bdccf2e8b3d3
7
+ data.tar.gz: bf03f5dab6f9d7e30d37531127dfb88e48e779eb65f271de703f12566d728bbe869a0d3a935d33932afff4556c09b21bfe4d3d95f4b72c131bfc0521a41bf4b9
data/CHANGELOG.md CHANGED
@@ -8,6 +8,36 @@ and commands may change between minor versions. How releases are made:
8
8
 
9
9
  ## [Unreleased]
10
10
 
11
+ ## [0.54.0] - 2026-10-10
12
+
13
+ ### Changed
14
+
15
+ - Web: the ctx chip is always shown and updates on every request (it used to appear only once the context crossed
16
+ 20/40/60/80%): `ctx ?` before a session's first request, with a tooltip saying why, on session cards too. The
17
+ attached TUI's status line gets the same fresh number each request; the model's own `[CONTEXT: …]` line still
18
+ comes only on a rise into a guidance bucket.
19
+ - Web: a denied approval's summary line reads `<tool>: <command> → Denied`; the typed reason shows in full in the
20
+ card body (expand the card) instead of being cut off in the summary.
21
+ - A grep-family search that found nothing (`grep`/`rg`/`ag`/`pgrep` exiting 1 with no stderr) is no longer a
22
+ failed tool call: it doesn't count in the turn's "(N failed)", the session's tool errors or the skills bundle's
23
+ "fix the skill" nudge, and the web row shows a done mark with a muted "no match". The model still sees
24
+ `exit: 1 (no output)`. Turns saved before keep their "error". (skills bundle 0.1.7; run `chi update`.)
25
+
26
+ ### Removed
27
+
28
+ - The `context.status_cadence` setting (config key, `--status-cadence` flag, `SAMAGOTCHI_CONTEXT_STATUS_CADENCE`):
29
+ the context status now streams on every request, so nothing reads it. A config.yml that still lists it gets a
30
+ startup warning (`config: unknown key 'context.status_cadence'`) and otherwise loads.
31
+
32
+ ### Fixed
33
+
34
+ - `chi -p "/cut TEXT" --non-interactive` (and `/queue TEXT`) runs `TEXT` as the turn's prompt, the same as the idle
35
+ REPL does, instead of feeding the model the literal `/cut` / `/queue` words; a bare `/cut` / `/queue` is still
36
+ refused, as before.
37
+ - `chi bundle install` / `upgrade` no longer warns "Checksum mismatch for hook <file>: expected ..., got …" for a
38
+ hook whose manifest entry declares no `sha256` (or a `hooks/*.rb` the manifest doesn't list); a declared sha256 that
39
+ differs still warns. (Since 0.53.0.)
40
+
11
41
  ## [0.53.0] - 2026-10-10
12
42
 
13
43
  ### Changed
@@ -2412,7 +2442,8 @@ and long-lived sessions.
2412
2442
  - A macOS desktop helper (`chi desktop install`): a "Send to chi" Service and a
2413
2443
  hotkey panel that send selected text or the clipboard to your sessions.
2414
2444
 
2415
- [Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.53.0...HEAD
2445
+ [Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.54.0...HEAD
2446
+ [0.54.0]: https://github.com/dm1try/samagotchi/compare/v0.53.0...v0.54.0
2416
2447
  [0.53.0]: https://github.com/dm1try/samagotchi/compare/v0.52.0...v0.53.0
2417
2448
  [0.52.0]: https://github.com/dm1try/samagotchi/compare/v0.51.0...v0.52.0
2418
2449
  [0.51.0]: https://github.com/dm1try/samagotchi/compare/v0.50.0...v0.51.0
data/README.md CHANGED
@@ -202,7 +202,7 @@ once, for the session, for the repo, or for the whole rule in the repo.
202
202
  - [Configuration](docs/configuration.md): `config.yml`, hosts, model server transports, timeouts, retries, logs
203
203
  - [Memory](docs/memory.md): scopes, model-specific overlays and model notes
204
204
  - [Sessions](docs/sessions.md): storage, retention, `chi sessions`
205
- - [Attached context](docs/context.md): `chi context`, sources, scopes, waking, the github-pr bundle, safety
205
+ - [Attached context](docs/context.md): `chi context`, sources, scopes, recipes (a file, a note, a stream), waking, the github-pr bundle, safety
206
206
  - [Broadcast](docs/broadcast.md): `chi broadcast`, who a note reaches, tags, triage, what a session gets, the triage log
207
207
  - [chi as a sub-agent](docs/sub-agent.md): `chi send --wait --format json`, `chi answer`, instructions for a parent agent
208
208
  - [Desktop helper](docs/desktop.md): `chi desktop`, the macOS "Send to chi" Service and hotkey
data/docs/architecture.md CHANGED
@@ -6,48 +6,51 @@ A compact visual overview of the current architecture, then the core API in pros
6
6
  ## System map
7
7
 
8
8
  ```
9
- ┌─────────────────────────────────────────┐
10
- │ bin/chi │
11
- │ (CLI entry point) │
12
- └───────────────────┬─────────────────────┘
13
- │
14
- ┌──────────────────────────┴──────────────┐
15
- │ TerminalUI │ ← REPL (Reline),
16
- │ render · status line · REPL commands │ rendering, commands
17
- └──────────────────────────┬──────────────┘
18
- │ delegates
19
- ┌───────────────────────────┴───────────────────────┐
20
- │ Engine (core logic) │
21
- │ system prompt · memory injection │
22
- │ tool declarations · session lifecycle │
23
- │ model↔tool loop — `run_turn` │
24
- └───────────────────────────┬───────────────────────┘
25
- │ delegates
26
- ┌────────────────────────────┴──────────────┐
27
- │ Web::App (Rack) │ ← browser UI
28
- │ session hub · /api/events · /api/* │ via SessionManager file IPC
29
- └───────────────────────────┬───────────────┘
30
- │
31
- ┌───────────────────────────────┬──────────┴───────────────┬──────────────────┐
32
- │ │ │ │
33
- ┌─────┴─────┐ ┌───────┴───────────┐ ┌────────┴───────┐ ┌───────┴────────┐
34
- │ KernelLoop │◀── drives────────▶│ Model API │ │ Session │ │ SessionManager │
35
- │ (the loop)│ │ (LLM HTTP call) │ │ (state, │ │ (bg workers) │
36
- └─────┬──────┘ └─────────────────────┘ │ history) │ └────────────────┘
37
- │ └──────────────────┘ └────────────────┘
38
- │ run_turn events (turn_started, turn_completed,
39
- │ turn_canceled, raw KernelLoop events)
40
- ▼
9
+ bin/chi (CLI entry point) bin/chi web
10
+ │ │
11
+ ┌────────────────────┴────────────────────┐ ┌───────────────┴────────────────┐
12
+ │ TerminalUI │ │ Web::App (Rack) │ ← browser UI
13
+ │ attached (default): AttachLauncher + │ │ session hub · /api/events │
14
+ │ AttachedLoop, no Engine │ │ /api/* · reads session files │
15
+ │ REPL (--no-shared …): its own Engine │ └───────────────┬────────────────┘
16
+ └────────────────────┬────────────────────┘ │ App#relay, bridge_get,
17
+ │ BridgeClient (SSE + HTTP) │ /stream proxy
18
+ └────────────────────┬────────────────────┘
19
+ ▼
20
+ ┌─────────────────────┴─────────────────────┐
21
+ │ Worker (one process per session) + Bridge │ ← forked/spawned by
22
+ │ input queue · idle exit · owner.lock │ SessionManager
23
+ └─────────────────────┬─────────────────────┘
24
+ │ builds, runs turns
25
+ ┌─────────────────────┴─────────────────────┐
26
+ │ Engine (core logic) │
27
+ │ system prompt · memory injection │
28
+ │ tool declarations · session lifecycle │
29
+ │ model↔tool loop — `run_turn` │
30
+ └─────────────────────┬─────────────────────┘
31
+ │
32
+ ┌───────────────────────────────────┴──────────────────────────┐
33
+ │ │ │
34
+ ┌─────┴──────┐ ┌─────────┴──────────┐ ┌────────┴─────────┐
35
+ │ KernelLoop │◀──── drives ────▶│ Model API │ │ Session │
36
+ │ / ChatLoop │ │ (LLM HTTP call) │ │ (state, history) │
37
+ └─────┬──────┘ └────────────────────┘ └──────────────────┘
38
+ │
39
+ │ run_turn events (turn_started, turn_completed,
40
+ │ turn_canceled, raw KernelLoop events)
41
+ ▼
41
42
  ┌──────────────────────────────────────────────────────────────────────────────────┐
42
- │ Tools │
43
- │ execute · read · edit · write · memory · web_fetch · task_create/get/list/ │
44
- │ task_stop/wait │
45
- │ declared via tool_declarations.rb │
43
+ │ Tools │
44
+ │ execute · read · edit · write · memory_read/write · web_fetch · │
45
+ │ task_create/get/list/stop/wait · delegate · delegate_result · ask_user_question │
46
+ │ register_reminder/cancel_reminder/list_reminders · send_note · list_sessions · │
47
+ │ context_read · forget_outputs (+ bundle plugin tools) │
48
+ │ declared via tool_declarations.rb │
46
49
  └──────────────────────────────────────────────────────────────────────────────────┘
47
50
  │
48
51
  ▼
49
52
  ┌──────────────────────────────────────────────────────────────────────────────────┐
50
- │ Persistence │
53
+ │ Persistence │
51
54
  │ Project: ~/.config/samagotchi/memories/projects/<repo>_<hash>/ (per git repo) │
52
55
  │ System: ~/.config/samagotchi/memories/ │
53
56
  └──────────────────────────────────────────────────────────────────────────────────┘
@@ -81,6 +84,9 @@ bin/chi ─▶ TerminalUI ─▶ Engine#run_turn ─▶ KernelLoop ──┬─
81
84
  (turn started / completed / canceled)
82
85
  ```
83
86
 
87
+ That is the REPL; attached (the default) the session's `Worker` calls `Engine#run_turn` and the
88
+ terminal renders the events it gets over the Bridge.
89
+
84
90
  ## Layers at a glance
85
91
 
86
92
  | Layer | Class(es) | Responsibility |
@@ -89,8 +95,9 @@ bin/chi ─▶ TerminalUI ─▶ Engine#run_turn ─▶ KernelLoop ──┬─
89
95
  | UI | `Samagotchi::TerminalUI` | REPL (Reline), rendering, REPL commands. Delegates all core work to an `Engine`. |
90
96
  | Model loops | `KernelLoop` (via `LLM::NativeBackend`), `LLM::ChatLoop` | The model↔tool loop: raw prompt or OpenAI chat API, chosen per host (see below). |
91
97
  | Adapters | `Samagotchi::Client`, `LLM::OpenAIChat`, `LLM::HTTP` | Raw-prompt servers, the OpenAI chat API, and the HTTP both share. |
92
- | Tools | `lib/samagotchi/tools/*` | Execute, read, edit, write, memory, task_*, web_fetch, plus runtime/output-guardrails. |
93
- | Background | `Samagotchi::SessionManager` | Builds `Engine` directly (no terminal rendering) for workers. |
98
+ | Tools | `lib/samagotchi/tools/*` | Execute, read, edit, write, memory, task_*, web_fetch, delegate/delegate_result, ask_user_question, reminders, send_note, list_sessions, context_read, forget_outputs, plus runtime/output-guardrails. |
99
+ | Bundles | `lib/samagotchi/memory_bundle/*`, `BundleCommand` | `chi bundle`: install/upgrade (3-way merge), build, uninstall; the install record under `.bundles/<name>/` that the hook, guardrail and plugin loaders read ([internals/bundles.md](internals/bundles.md)). |
100
+ | Background | `Samagotchi::Worker`, `Samagotchi::SessionManager` | `SessionManager` forks/spawns one worker process per session; its `Worker` builds the `Engine` and `Bridge` (no terminal rendering) and runs queued turns. |
94
101
  | Web | `Samagotchi::Web::App`, `Samagotchi::Web::Server`, `Samagotchi::Web::SessionHub` | Rack+WEBrick single-port `127.0.0.1:4567` (index.html + `/api/*` + SSE). The hub is chi web's projection of the session list, pushed to every tab over `GET /api/events`. |
95
102
  | Sessions | `Samagotchi::Session`, `SessionManager` | File `sessions/<uuid>.json` + sidecar `input/`/`output/`; retention 14d/500, `updated_at desc`, lazy sweep. |
96
103
 
@@ -110,15 +117,16 @@ bin/chi ─▶ TerminalUI ─▶ Engine#run_turn ─▶ KernelLoop ──┬─
110
117
  `dismiss_question`). The worker runs in the session's `working_directory`, so `!cmd` and
111
118
  the tools don't depend on where the terminal attached from.
112
119
  - `bin/chi web` → builds `Web::Server` (Rack+WEBrick on `127.0.0.1:4567`, `--port`/`SAMAGOTCHI_WEB_PORT`, `--open`).
113
- - `bin/chi sessions {list,stop,delete,prune,clean}` (`SessionsCommand`; not the REPL's `SessionCommands`) → retention & ordering (`SessionRetention`, `updated_at desc`, dry-run, test-only); `stop` is `SessionManager.stop_session(wait:)`, which waits for the worker to release `owner.lock`; `delete` (`SessionDeleteCommand`) is `SessionManager.delete_session(stop:)`, which the TUI's `/exit --delete` and the web's `DELETE /api/sessions/:id` use too.
114
- - `--prompt`, `--non-interactive`, and `SessionManager` workers build `Engine` directly.
120
+ - `bin/chi sessions {list,stop,restart,archive,unarchive,delete,prune,clean,stats}` (`SessionsCommand`; not the REPL's `SessionCommands`) → retention & ordering (`SessionRetention`, `updated_at desc`, dry-run, test-only); `stop` is `SessionManager.stop_session(wait:)`, which waits for the worker to release `owner.lock`; `delete` (`SessionDeleteCommand`) is `SessionManager.delete_session(stop:)`, which the TUI's `/exit --delete` and the web's `DELETE /api/sessions/:id` use too.
121
+ - In REPL mode `TerminalUI` builds the `Engine` in process (`-p` and `--non-interactive` too);
122
+ otherwise the session's `Worker` (`lib/samagotchi/worker.rb`, spawned by `SessionManager`) builds it.
115
123
 
116
124
  ## Session retention & ordering
117
125
 
118
126
  - **Files:** `~/.local/state/samagotchi/sessions/<uuid>.json` + `<uuid>/input|output|owner.lock|bridge.json` (XDG-aware), and the `<uuid>/stopped` and `<uuid>/archived` marker files.
119
127
  - **Single owner:** the process running a session's Engine (worker or in-process TUI) holds a flock on `owner.lock` (`OwnerLock`); a second owner backs off, and the web answers 409 for a TUI-owned session.
120
128
  - **Status:** `status` is turn state (`idle`/`running`); liveness is the lock. A stop is the `<uuid>/stopped` marker (`Session.mark_stopped`; a resume removes it), which `Session.load` and `.summary_from_file` read as status `stopped`, so no other process rewrites a worker's session file.
121
- - **Retention:** 14 days / 500 cap (env `SAMAGOTCHI_SESSION_RETENTION_DAYS`/`MAX_COUNT`, optional `KEEP_STATUS`), live-owner guard, only when `*.json` present; lazy sweep ≤1/24h from the session hub's full-probe tick (and on `GET /api/sessions`, which the page no longer calls) & `Dashboard#render_list`, manual via `bin/chi sessions prune --dry-run`.
129
+ - **Retention:** 14 days / 500 cap (env `SAMAGOTCHI_SESSION_RETENTION_DAYS`/`MAX_COUNT`, optional `KEEP_STATUS`), live-owner guard, only when `*.json` present; lazy sweep ≤1/24h from the session hub's full-probe tick (and on `GET /api/sessions`, which the page no longer calls), manual via `bin/chi sessions prune --dry-run`.
122
130
  - **Ordering:** `Session.list(sort:,order:,limit:,offset:)` and `GET /api/sessions?sort=&order=&limit=&offset=` default `updated_at desc`; Web UI sort/filter/pagination.
123
131
  - **Test hygiene:** `test_run` flag when `SAMAGOTCHI_ENV=test`/`RACK_ENV=test`/`CI`, targetable via `prune --test-only` / `clean`.
124
132
 
@@ -133,7 +141,7 @@ agent logic and can be used without any terminal rendering; the UI is a thin lay
133
141
  | UI | `Samagotchi::TerminalUI` | Interactive REPL (Reline), rendering (ANSI, spinner, status line), REPL commands. Delegates all core work to an `Engine`. |
134
142
  | Model loops and adapters | `KernelLoop`, `LLM::ChatLoop`, `Samagotchi::Client`, `LLM::OpenAIChat`, `LLM::HTTP` | The model↔tool loops and the HTTP adapters they talk through (see "Model loops and adapters"). |
135
143
  | Bridge (SSE/HTTP) | `Samagotchi::Bridge`, `SessionManager` | The **single live client transport**: an SSE read stream + HTTP POST turn/cancel/answer surface that attaches to a worker's existing `Engine` via `Engine#subscribe`. Every session worker starts it (bound `127.0.0.1`, no auth, localhost-only). |
136
- | Web (Rack) | `Samagotchi::Web::App`, `SessionManager` | Single-port `127.0.0.1:4567` control plane via `rack`+`webrick` (serve `index.html` + `/api/*`; `/stream` proxies each session's Bridge). `bin/chi web` entrypoint. |
144
+ | Web (Rack) | `Samagotchi::Web::App`, `SessionManager` | Single-port `127.0.0.1:4567` control plane via `rack`+`webrick` (serve `index.html` + `/api/*`). It talks to each worker's Bridge (`App#relay`, `bridge_get`; `/stream` proxies it) and reads the session files; no `Engine` of its own. `bin/chi web` entrypoint. |
137
145
  | Sessions | `Samagotchi::Session`, `SessionManager` | File-based `~/.local/state/samagotchi/sessions/<uuid>.json` + sidecar `input/`/`output/`; retention (14d/500) + ordering (`updated_at desc`). |
138
146
 
139
147
  - `bin/chi` in REPL mode (see `LaunchMode` above) builds `TerminalUI`. `TerminalUI#run` is the single
@@ -141,7 +149,7 @@ agent logic and can be used without any terminal rendering; the UI is a thin lay
141
149
  builds the working session once, runs a single prompt turn when `-p` is given,
142
150
  then either exits (`--non-interactive`) or drops into the REPL carrying the
143
151
  post-turn conversation.
144
- - `SessionManager` background workers build `Engine` directly (no terminal rendering).
152
+ - A session's `Worker` (spawned by `SessionManager`) builds its `Engine` (no terminal rendering).
145
153
  - `bin/chi` in attached mode (the default, `--attach`, `--shared`) builds no `Engine`: `TerminalUI::AttachLauncher` finds
146
154
  or starts the worker, and `TerminalUI::AttachedLoop` is a client of its Bridge
147
155
  (`BridgeClient#follow` for events, `post_turn`/`cancel`/`answer` for input). It
@@ -168,18 +176,23 @@ engine.run_turn(session, "hello", on_event: nil) # => LLM::ModelResult (`.outp
168
176
  forwards the raw `KernelLoop` events unchanged (the low-level contract) and adds a few
169
177
  higher-level events so UIs get clean turn boundaries without inferring them:
170
178
 
171
- - `:turn_started` — `{ session_id:, prompt: }`
172
- - `:turn_completed` — `{ result: }` (the final `LLM::ModelResult`)
173
- - `:turn_canceled` — `{ cancellation_reason:, cancelled_by: }` (`cancelled_by`: the bundle or hook label that
179
+ - `:turn_started` — `{ session_id:, prompt:, turn_id: }` (plus `continue: true` for a continue
180
+ turn, `images:` with the prompt's image refs)
181
+ - `:turn_completed` — `{ result:, turn_summary:, display_pending: }` (`result`: the final
182
+ `LLM::ModelResult`; `display_pending`: `after_turn` hooks may still present the answer)
183
+ - `:turn_canceled` — `{ cancellation_reason:, cancelled_by:, duration_ms: }` (`cancelled_by`: the bundle or hook label that
174
184
  stopped it, for `cancellation_reason: :hook`; else nil)
175
185
 
186
+ A turn started with `origin:` carries it on each of these (and `:turn_failed`).
187
+
176
188
  Every event is a `Hash` with a `:type` symbol key; the sink must not raise (the Engine
177
189
  rescues sink errors). A new UI (web, API) supplies its own `on_event` and
178
190
  renders whatever it needs from the stream + final `Result`. The public Engine API:
179
191
 
180
192
  ```ruby
181
- engine.run_turn(session, prompt, on_event: nil, max_iterations: nil, cancel_controller: nil) # nil: turn.max_iterations
182
- engine.run(session: nil, prompt: "...", on_event: nil) # create/resume session + run
193
+ engine.run_turn(session, prompt, on_event: nil, max_iterations: nil, cancel_controller: nil, # nil: turn.max_iterations
194
+ max_tool_output_chars: nil, pending_input: nil, continue: false, origin: nil,
195
+ images: [], id: nil)
183
196
  engine.system_prompt # fully built system prompt string
184
197
  engine.session # current session (Engine owns create/resume)
185
198
  ```
@@ -240,6 +253,9 @@ process that already owns the `Engine`); every worker starts it, and it exposes:
240
253
  `501 not_supported` with the restart hint; a Bridge `404 unknown_session`, no bridge or a
241
254
  refused connection is `503 not_live`.
242
255
  - `POST /session/:id/cancel` — cancel the running turn; `202`, or `409` when none runs.
256
+ - `POST /session/:id/recap` — `/recap` in an attached TUI: `200 {enabled, saved, request,
257
+ min_user_turns}` (`{enabled: false}` without recaps); a new recap is asked for at once and
258
+ arrives as `:recap_ready`. Answers mid-turn too.
243
259
  - `POST /session/:id/answer` — answer the pending question; `200`, `409` when another client
244
260
  answered first or it is gone, `400` for an invalid selection. With `client_id: "cli:answer"`
245
261
  (`chi answer`, a parent agent) an allow on an approval beyond the worker's
@@ -318,8 +334,8 @@ Every route goes through `Bridge#dispatch`: an id other than the bridge's own se
318
334
  The per-session port is OS-assigned (bound to `0`) and published to a `bridge.json` sidecar
319
335
  for client discovery. `chi web`'s `GET /api/sessions/:id/stream` proxies this bridge
320
336
  (503 `not_live` when the worker is not running; full history of any session is served by
321
- `GET /api/sessions/:id/output`). Resume/ring-buffer state is **in-memory** (v1) — durable
322
- cross-process resume is a staged next step, not part of v1.
337
+ `GET /api/sessions/:id/output`). Resume/ring-buffer state is **in-memory**, per worker: a cursor from
338
+ an earlier worker gets the `reset` snapshot, not a replay.
323
339
 
324
340
  **Session hub.** The cross-session layer (which sessions exist, who owns them, what changed)
325
341
  never pulls from the page: `chi web` runs one `Samagotchi::Web::SessionHub` (a thread inside
@@ -394,9 +410,10 @@ its own). An empty answer with a `length` stop and the context 90 % full or
394
410
  more is not asked again (a full window, not a thinking loop); with no finish
395
411
  reason it is. Both also keep one `ContextStatus` per turn: before each request
396
412
  it estimates how full the window is (the server's last count plus what the turn
397
- appended since), emits `:context_status` on a bucket change, and on a rise past
398
- the second threshold puts the model's own `[CONTEXT: …]` line on the tail as a
399
- system message (docs/internals/context-telemetry.md).
413
+ appended since), emits `:context_status` on every request (so the web and the
414
+ attached TUI get a fresh number each time), and on a rise past the second
415
+ threshold puts the model's own `[CONTEXT: …]` line on the tail as a system
416
+ message (docs/internals/context-telemetry.md).
400
417
 
401
418
  **Adapters.** `Client` (raw-prompt servers) and `LLM::OpenAIChat` (one per host,
402
419
  `HostRegistry#adapter_for`) share `LLM::HTTP`: timeouts, TLS for https, a line
@@ -421,3 +438,26 @@ server (`/props`), then the host's model list, then config.
421
438
  **Keys.** A host's API key comes only from the environment variable its
422
439
  `api_key_env:` names; it never reaches config.yml, `HOSTS_JSON`, `chi self`,
423
440
  logs or events.
441
+
442
+ ## Other components
443
+
444
+ A map of the parts the overview above doesn't name (paths under `lib/samagotchi/`):
445
+
446
+ | Component | Main file(s) | What it is |
447
+ |---|---|---|
448
+ | Worker | `worker.rb` (`worker_idle_exit.rb`, `worker_inbound.rb`) | A session's background process: builds the `Engine` and `Bridge`, runs queued turns, exits when idle. |
449
+ | Config | `config.rb` | Every setting as an `Entry`: env, config.yml and CLI layers ([configuration.md](configuration.md)). |
450
+ | 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. |
451
+ | Plugin API | `plugin/api.rb` (`Plugin::Api`), `plugin/loader.rb` | What a bundle's plugin registers: tools, commands, hooks, services ([plugins.md](plugins.md)). |
452
+ | Shipped bundles | `bundles/` (`mcp`, `github-pr`, `loop-guard`, `skills`, `source-links`, …) | Bundles that ship with chi. |
453
+ | Hooks | `hooks/registry.rb` (`Hooks::Registry`), `hooks/loader.rb` | Hook registration and dispatch ([hooks.md](hooks.md)). |
454
+ | Guardrails | `guardrails.rb`, `guardrails/shell_lex.rb`, `guardrails/rules.rb`, `guardrail_wiring.rb` | The gate before every tool call: shell lexing, YAML rules, approvals ([guardrails.md](guardrails.md)). |
455
+ | Delegation | `tools/delegate.rb`, `tools/delegate_result.rb`, `tools/delegate_wait.rb`, `child_reports.rb`, `relay_desk.rb` | Child sessions a session starts and waits on, and their approval relay ([sessions.md](sessions.md#delegating)). |
456
+ | send / broadcast | `send_command.rb` (`SendCommand`), `broadcast_command.rb` (`BroadcastCommand`), `broadcast/` | `chi send` and `chi broadcast` ([broadcast.md](broadcast.md), [sub-agent.md](sub-agent.md)). |
457
+ | Idle work | `idle_scheduler.rb` (`IdleScheduler`), `idle_recap.rb` (`IdleRecap`), `idle_reminders.rb` | One poller for an idle session's recap and reminders. |
458
+ | Attached context | `context_sources.rb` (`ContextSources`), `context_poller.rb`, `context_absorber.rb` | External sources a session keeps fresh ([context.md](context.md)). |
459
+ | LLM context strategies | `llm_context_strategy.rb`, `llm_context_view.rb`, `llm_context_stale.rb`, `llm_context_forget.rb` | What a request sends of the history (none, stale, forget) ([internals/llm-context-forget.md](internals/llm-context-forget.md)). |
460
+ | Session chains | `session_continue.rb` (`SessionContinue`), `session_chain.rb` (`SessionChain`) | A session that continues an earlier one. |
461
+ | Analytics | `analytics_file.rb` (`AnalyticsFile`) | A session's `analytics.json` (`SessionMetrics#persist`). |
462
+ | Images / vision | `image_store.rb` (`ImageStore`), `vision_support.rb`, `vision_context.rb` | Stored prompt images and whether a model takes them. |
463
+ | Desktop | `desktop.rb`, `desktop/macos.rb`, `desktop_command.rb` | `chi desktop`, the macOS "Send to chi" helper ([desktop.md](desktop.md)). |
@@ -1167,8 +1167,7 @@ described in their own sections.
1167
1167
  | `llm_context.policy` | the subtask-boundaries line | | The sentence `forget_outputs`' description carries (the `forget` layer): "Tidy at subtask boundaries: once a subtask is done, forget its tool outputs and note what it established; keep anything you'll still edit against." Blank: none. Read when a session starts. |
1168
1168
  | `context.chars_per_token` | `4.0` | yes | Estimate ratio when the server reports no usage. |
1169
1169
  | `memory.index_warn_tokens` | `2500` | yes | The tokens one scope's memory index (project or system; sent with every prompt) may hold before a `memory_write`, `write` or `edit` that takes it over gets a note asking the model to tighten long index descriptions; `0`: no note. Estimated as characters / `context.chars_per_token`. `memory:` is its own section, not the `memories:` preload list. See [Memory](memory.md). |
1170
- | `context.status_thresholds` | `20,40,60,80` | yes | Percentages that trigger a status. |
1171
- | `context.status_cadence` | `0` | yes | Also every N rounds; `0` = thresholds only. |
1170
+ | `context.status_thresholds` | `20,40,60,80` | yes | Percentages that put the estimate into a bucket; the rise into a guidance bucket (from the second up) leaves the model its own `[CONTEXT: …]` line, and the bucket decides the guidance. |
1172
1171
  | `context.every_seconds` | `300` | | How often an attached context source's command runs when it has no `--every` (seconds, at least 30). See [Attached context](context.md). |
1173
1172
  | `context.wake` | `true` | yes | A source whose update says `wake: true` may start a turn in a live, idle session (one per source per 10 minutes, within `session.max_wakes`); `false`: updates wait as notes for the next turn. See [Waking](context.md#waking). |
1174
1173
  | `thinking.turn_preamble` | `true` | yes | Ask a `qwen36` model to open its thinking with a short `TURN:` line (the step label). |
data/docs/context.md CHANGED
@@ -25,7 +25,8 @@ update.
25
25
  - **A command** (`--cmd CMD`): chi runs it every `--every` seconds (at least
26
26
  30; default `context.every_seconds`, 300) while the session's worker is up.
27
27
  - **Pushed** (`--push`): nothing runs; `chi context push NAME` (stdin, or
28
- `-m TEXT`) sets its text. Only a pushed source takes a push: a command
28
+ `-m TEXT`) sets its text, as plain text or the same JSON a command prints
29
+ ([below](#the-command-contract)). Only a pushed source takes a push: a command
29
30
  source's text is its command's.
30
31
  - **A URL** (`chi context add URL`): an installed bundle's provider turns it
31
32
  into a command source (name, command, hint, interval). The `github-pr`
@@ -38,19 +39,24 @@ for up to 60 s; stdout is the text (up to 1 MiB), stderr goes to the debug
38
39
  log. It gets two variables:
39
40
 
40
41
  - `SAMAGOTCHI_CONTEXT_NAME`: the source's name;
41
- - `SAMAGOTCHI_CONTEXT_PREVIOUS`: the path of the last snapshot (JSON with
42
- `text`), absent the first time, so the script can say what changed.
42
+ - `SAMAGOTCHI_CONTEXT_PREVIOUS`: the path of the last snapshot, absent the
43
+ first time, so the script can say what changed. It is JSON: `text` (missing
44
+ while every run so far failed), `summary`, `fetched_at`, `error` and a few
45
+ more. Only the text is kept from your output: an extra key of yours (a
46
+ cursor) isn't there next time.
43
47
 
44
48
  Its output is either:
45
49
 
46
50
  - **plain text**: all of it is the text; chi's summary is
47
- `content changed (+12/−3 lines)`. It never wakes the session.
48
- - **JSON**, an object with a string `text`:
51
+ `content changed (+12/−3 lines)` (`12 lines of text` the first time). It never wakes the session.
52
+ - **JSON**: output that starts with `{` and parses as an object with a string
53
+ `text` (anything else is plain text):
49
54
  ```json
50
55
  {"text": "...the full text...", "summary": "2 new comments; checks failing", "wake": true, "hint": "optional new hint"}
51
56
  ```
52
57
  `summary` (one line, up to 200 characters) goes into the update note;
53
- `wake: true` asks for a turn ([Waking](#waking)). Unknown keys are ignored.
58
+ `wake: true` asks for a turn ([Waking](#waking)); `hint` replaces the
59
+ source's hint until a later output gives another. Unknown keys are ignored.
54
60
 
55
61
  A non-zero exit, a timeout, more than 1 MiB or empty output is a failed
56
62
  fetch: the last good text stays, and the agent hears once, at the first
@@ -84,7 +90,7 @@ web) without touching the other sessions.
84
90
 
85
91
  ```
86
92
  chi context add NAME (--cmd CMD | --push) [--every SECONDS] [--why TEXT] [--hint TEXT] TARGET
87
- chi context add URL [--why TEXT] TARGET a URL an installed bundle's provider knows
93
+ chi context add URL [--why TEXT] [--hint TEXT] TARGET a URL an installed bundle's provider knows
88
94
  chi context push NAME [-m TEXT] [TARGET] new text for a --push source (stdin without -m)
89
95
  chi context ls [TARGET] [--format json]
90
96
  chi context show NAME [--json] [TARGET]
@@ -95,11 +101,100 @@ chi context mute|unmute NAME ID... a project source, for one sessi
95
101
 
96
102
  `TARGET` is session ids (or unique prefixes) or `--project`. Inside a chi
97
103
  session (its `execute`) the default is that session. `NAME` is `a-z`, `0-9`
98
- and `-`, up to 40 characters.
104
+ and `-`, up to 40 characters, starting with a letter or digit
105
+ (`subscriptions` is taken).
99
106
 
100
107
  In a session, `/context` lists what is attached (as `context_read` does
101
108
  with no name).
102
109
 
110
+ ## Recipes
111
+
112
+ Anything that prints text is a source.
113
+
114
+ ```sh
115
+ # a file you keep editing
116
+ chi context add todo --cmd 'cat ~/notes/todo.md' --every 60 --why "my running todo" --project
117
+
118
+ # an Apple Note (macOS asks once to let chi control Notes)
119
+ chi context add plan --cmd "osascript -e 'tell application \"Notes\" to get plaintext of note \"Plan\"'" --project
120
+
121
+ # pushed by something else: a Shortcut, a cron job, the clipboard
122
+ chi context add clip --push --project
123
+ pbpaste | chi context push clip --project
124
+ ```
125
+
126
+ ### A stream: chat, a log, a feed
127
+
128
+ A source's text is a snapshot, not a log: each run replaces it. For
129
+ something that only grows (a chat channel, CI events, a log), let the script
130
+ keep a window of the latest items and fetch only what's new. chi keeps no
131
+ state for the script apart from the last text, so the cursor lives in the
132
+ text: each item is a line starting with its id, and the script reads the
133
+ newest id from `SAMAGOTCHI_CONTEXT_PREVIOUS`.
134
+
135
+ ```ruby
136
+ #!/usr/bin/env ruby
137
+ # chat-feed: the last KEEP items of a stream, fetching only the new ones.
138
+ require "json"
139
+
140
+ KEEP = 50
141
+ LINE = /\A#(?<id>\S+) / # each item is one line: "#<id> 14:02 @ann: text"
142
+
143
+ # Swap this for the real thing (Slack's conversations.history with oldest:,
144
+ # a log file, an RSS feed…). Here: items from a JSONL file, one per line.
145
+ def fetch_since(since_id)
146
+ File.readlines(ARGV.fetch(0), chomp: true).map { JSON.parse(_1) }
147
+ .select { since_id.nil? || _1["id"].to_s > since_id }
148
+ end
149
+
150
+ prev_path = ENV["SAMAGOTCHI_CONTEXT_PREVIOUS"]
151
+ prev = prev_path && JSON.parse(File.read(prev_path))["text"] # absent or no text: first run
152
+ old_lines = prev.to_s.lines(chomp: true).grep(LINE)
153
+ since_id = old_lines.last&.match(LINE)&.[](:id)
154
+
155
+ new_items = fetch_since(since_id)
156
+ new_lines = new_items.map { "##{_1["id"]} #{_1["ts"]} @#{_1["author"]}: #{_1["text"].tr("\n", " ")}" }
157
+ lines = (old_lines + new_lines).last(KEEP)
158
+
159
+ summary =
160
+ if new_items.empty? then "no new messages"
161
+ else "#{new_items.size} new from #{new_items.map { "@#{_1["author"]}" }.uniq.join(", ")}"
162
+ end
163
+ wake_word = ENV["FEED_WAKE_ON"]
164
+ wake = !wake_word.nil? && new_items.any? { _1["text"].include?(wake_word) }
165
+
166
+ puts JSON.generate(text: lines.empty? ? "(no messages yet)" : lines.join("\n"), summary:, wake:)
167
+ ```
168
+
169
+ ```sh
170
+ chi context add chat --cmd "FEED_WAKE_ON=@me ~/bin/chat-feed ~/chat.jsonl" --why "team chat" --project
171
+ ```
172
+
173
+ What makes it work:
174
+
175
+ - **Nothing new, same text.** The script prints the old window again, the
176
+ hash doesn't change, and the agent hears nothing.
177
+ - **The window keeps it small.** The text has to stay under 1 MiB, and a
178
+ small model's context window is smaller still.
179
+ - **The summary gives counts and authors, never the words** ("3 new from
180
+ @ann, @bob"), like the github-pr bundle's: other people's words reach the
181
+ agent only when it reads the text, framed as third-party.
182
+ - **Wake rarely.** A busy channel that wakes on every message keeps a
183
+ session (and its bill) running; wake on what is meant for you.
184
+ - **A failed run keeps the last text**, so the next run picks up from the
185
+ last good cursor.
186
+ - **Edits and deletions aren't seen.** An id already in the window isn't
187
+ fetched again.
188
+
189
+ The cursor lives in the text because chi keeps nothing else from the output.
190
+ A script that wants it out of the text keeps its own file (say under
191
+ `$XDG_STATE_HOME/<your tool>/`, by `$SAMAGOTCHI_CONTEXT_NAME`). Two sessions
192
+ with a session source of the same name would share that file.
193
+
194
+ An attached source feeds the sessions you attach it to. To drop an item and
195
+ let chi find the sessions it concerns, pipe it to
196
+ [`chi broadcast`](broadcast.md) instead.
197
+
103
198
  ## What the agent sees
104
199
 
105
200
  Notes join the conversation **between turns** (a change during a turn waits
@@ -226,7 +321,7 @@ project root, working directory, branch, PR URL, main checkout or worktree,
226
321
  model), `attached` and `declined` (with the seconds since the offer), and
227
322
  `first_prompt` (the first 160 characters of every session's first message,
228
323
  offered or not; a fork's first after the conversation it started from, once;
229
- not scratch sessions or delegate children). Over 1 MB it is
324
+ not scratch sessions or delegate children). Over 1 MiB it is
230
325
  renamed to `offers.ndjson.1` (one old file is kept). Only you can read it
231
326
  (0600), and nothing leaves your machine.
232
327
 
@@ -24,6 +24,6 @@ Behavior:
24
24
  - `task_get` returns metadata and `output_path`; use `read` for output contents.
25
25
  - `task_wait` accepts `timeout`, `tail_lines` (maximum 100), and `done_pattern` (a regular expression string).
26
26
  - `task_create` accepts `env` as a JSON object string for deterministic overrides such as `PATH`; use an absolute interpreter path when that is simpler. Ruby/Bundler isolation variables remain protected.
27
- - `execute` and `task_create` export chi's own markers into the command, set over the model's `env` (which can't set them: `Error: env key is reserved`): `SAMAGOTCHI_PARENT_SESSION` (the session's id, `chi` without one; a chi started there answers as a parent agent) and `SAMAGOTCHI_SESSION_MODEL` (the resolved ref the session runs on, read at each call so it follows `/model`; unset when unknown). `chi self` reads the latter for its `model` row. A session's worker never inherits them from whoever started it (a `chi send` run by another session's `execute`): its other commands (a user's `!cmd`, an MCP server, a context command) have none.
27
+ - `execute` and `task_create` export chi's own markers into the command, set over the model's `env` (which can't set them: `Error: env key is reserved`): `SAMAGOTCHI_PARENT_SESSION` (the session's id, `chi` without one; a chi started there answers as a parent agent), `SAMAGOTCHI_SESSION_MODEL` (the resolved ref the session runs on, read at each call so it follows `/model`; unset when unknown) and `SAMAGOTCHI_SESSION_THINKING` (the session's own thinking level; unset when it has none). `chi self` reads the last two for its model and thinking level. The three are `TaskRuntime::MARKER_ENV_KEYS` (and `SessionManager::SESSION_MARKER_ENV`, which a spawned worker unsets). A session's worker never inherits them from whoever started it (a `chi send` run by another session's `execute`): its other commands (a user's `!cmd`, an MCP server, a context command) have none.
28
28
 
29
29
  Work that needs a model, not a shell command, goes to a child chi session instead: `delegate` / `delegate_result` have the same shape (start, then wait) and return only the child's final reply; see [Sessions: Delegating](../sessions.md#delegating).
@@ -0,0 +1,73 @@
1
+ # Memory bundles: install, upgrade and build
2
+
3
+ How `lib/samagotchi/memory_bundle/` turns a bundle (a dir, git URL, zip or tar holding `manifest.yml`, `*.md`
4
+ memories and optional `hooks/`, `guardrails/`, a plugin file and `scripts/`) into installed state, and back. The
5
+ user-facing side (what a bundle owns, profiles, needs) is in [memory.md](../memory.md#what-a-bundle-owns) and
6
+ [plugins.md](../plugins.md).
7
+
8
+ ## Where things live
9
+
10
+ ```
11
+ <scope memories dir>/ MemoryPaths.scope_dir: system ~/.config/samagotchi/memories,
12
+ project <system>/projects/<project key>
13
+ *.md, index.md the bundle's memories, one managed index line each ("· from <bundle>")
14
+ <system memories>/.bundles/ MemoryPaths.bundles_dir
15
+ <name>/manifest.json the install record (Provenance#write, read as InstalledBundle)
16
+ <name>/bases/ each file as installed: the base of the next upgrade's 3-way merge
17
+ <name>/hooks/ guardrails/ plugin/ scripts/
18
+ .trash/<name>-<stamp>/ files an uninstall or upgrade took away (Trash), never deleted
19
+ ```
20
+
21
+ ## Components
22
+
23
+ | Piece | Role |
24
+ |---|---|
25
+ | `BundleCommand` (`lib/samagotchi/bundle_command.rb`) | `chi bundle install/upgrade/uninstall/status/diff/list/build/trash`; runs the Installer, prints its summary, runs the conflict agent step |
26
+ | `SourceNormalizer` (source.rb) | source → local dir; a git/zip/tar extract is *owned* and removed after the install |
27
+ | `Manifest` | parses/validates `manifest.yml` (files, hooks as `BundleHook`, plugin, scripts, needs, requires_chi, includes, context_providers) and writes it (`Manifest.write`, for Builder) |
28
+ | `Installer` | one install/upgrade/dry run of one bundle (below) |
29
+ | `AssetInstaller` | the parts that live in `.bundles/<name>/`: hooks, guardrail rules, plugin, scripts; returns `AssetInstaller::Installed` (the paths the record keeps) |
30
+ | `Merger` | 3-way classification per file: `install`, `fast_forward`, `keep`, `noop`, `conflict` |
31
+ | `Provenance` | the `.bundles/<name>/` dir: `#write` the record + bases, `#record` → `InstalledBundle`, `.each_installed`, `.claimants` (which bundles own a file) |
32
+ | `InstalledBundle`, `BundleHook`, `PluginRef` | value objects for the record; tolerant of older records (missing keys read as empty) |
33
+ | `Builder` | `chi bundle build`: memories (+ the installed bundle's hooks, rules, plugin, scripts) → zip/tar/dir |
34
+ | `Profile` | meta bundles (`includes:`): install the members, record which |
35
+ | `SystemBundle`, `ShippedUpdate` | the built-in system bundle (`ensure!` at Engine start, `sync` in `chi update`) and `chi update`'s upgrade of shipped bundles; both drive the Installer with `upgrade: true` |
36
+ | `Uninstaller`, `Trash`, `Status`, `Listing` | remove, keep removed files, `chi bundle status`, `chi bundle list` |
37
+ | `IndexUpdater`, `IndexSync` | the managed `index.md` lines (Installer; and the write/edit tools for memories) |
38
+
39
+ Readers of the installed state: `Hooks::BundleLoader` (Engine), `GuardrailWiring#bundle_rules`, `Plugin::Loader`,
40
+ `ContextProviders` (scripts), `BundleNeeds` (index markers). Each goes through `Provenance.each_installed` / `#record`
41
+ and checks the recorded sha256 (and `requires_chi`) before loading.
42
+
43
+ ## Installer#run, in order
44
+
45
+ The order matters: warnings print in it, and a plugin/script the manifest names but the bundle lacks raises
46
+ `InstallError` after the memories are already copied.
47
+
48
+ 1. `load_source`: normalize, read `manifest.yml` (strict: a missing/invalid one raises; non-strict: a warning).
49
+ 2. `resolve_target`: scope = CLI > manifest > `system`.
50
+ 3. `install_memories`, per `*.md`: an upgrade 3-way merges the files the bundle owns (`merge_memory`), a dry run only
51
+ classifies (`preview_memory`), a plain install writes new files and skips existing ones (`skip_existing`). A
52
+ same-name file the bundle didn't write is the user's: skipped and never recorded (`@owned`). Model overlays
53
+ (`<name>.<key>.md`) get no index line (`note_overlay`).
54
+ 4. `AssetInstaller#run`: hooks (manifest-listed, else every `hooks/*.rb`), prune dropped hooks, hook checksums,
55
+ guardrails, plugin, scripts; each set replaces the previous version's. requires_chi warnings for hooks/rules.
56
+ 5. An upgrade's `prune_dropped_files`: unchanged → trash; edited → kept (`kept_pruned`); owned by another bundle → kept.
57
+ 6. `verify_memory_checksums` (strict), `warn_missing_needs`.
58
+ 7. `write_provenance` (manifest, not a dry run): bases come from the incoming file, except where the user's edit was
59
+ kept (kept/noop/conflict/kept_pruned, or a re-install's skipped edit): those keep the old base, so the next
60
+ upgrade still sees the edit.
61
+ 8. `detect_placeholders`.
62
+
63
+ Results are `@results` (file → `{status:, reason:}`), summarised by `#summary`. An upgrade with conflicts keeps an
64
+ owned source dir alive (`#source_dir`) until `#cleanup_source!`, because the conflict agent reads the incoming
65
+ files; `Provenance#resolve_conflicts` then moves the resolved files' bases forward.
66
+
67
+ ## Builder#run, in order
68
+
69
+ `select_memories` (all but `index.md`/hidden; with `--files` the named ones + their overlays, else leaving out files
70
+ another installed bundle owns) → `checksum_memories` → `installed_parts` (`Builder::InstalledParts`: the same-name
71
+ installed bundle's hooks, rules, plugin, scripts, context providers, else the scope dir's `hooks/`/`guardrails/`) →
72
+ `stage_files` + `write_manifest` in a temp dir → `write_out` (zip, tar, tgz, or a new/empty dir). An installed
73
+ profile's name is refused (`refuse_profile!`): a build can't remake its includes.
@@ -5,7 +5,7 @@ web/SSE clients) as a `:context_status` stream event. The telemetry itself is
5
5
  not injected into the model's conversation; what the model gets is one short
6
6
  line, as a tail system message (`kind: context`), when usage rises into a bucket
7
7
  whose guidance asks it to change how it works (from the second threshold, 40%
8
- by default, up; never on a fall or a cadence tick, and not again for a bucket a
8
+ by default, up; never on a fall, and not again for a bucket a
9
9
  resumed session's line already names):
10
10
 
11
11
  `[CONTEXT: about 55% of the context window is in use (estimated; bucket=40plus). context moderate — prefer targeted and range reads over full-file dumps]`
@@ -19,8 +19,12 @@ carries a `status` string using this prefix:
19
19
 
20
20
  Emission behavior:
21
21
 
22
- - A status is emitted when estimated usage crosses configured threshold buckets.
23
- - Optional cadence-based updates can also be enabled every N rounds.
22
+ - The estimate is emitted on every request (both loops, before each request),
23
+ so the web and the attached TUI get a fresh number each time.
24
+ - The model's own `[CONTEXT: …]` line is still gated: it is left only on a rise
25
+ into a guidance bucket (from the second threshold up), never on a fall or a
26
+ cadence tick, and not again for a bucket a resumed session's line already
27
+ names.
24
28
  - This is warn-only behavior (no automatic history truncation).
25
29
  - When the model server reports real `usage` fields in the stream payload, the
26
30
  telemetry uses those actual token counts (prefixed `src=server`) instead of the
@@ -55,5 +59,4 @@ Configuration:
55
59
  - `SAMAGOTCHI_CONTEXT_STATUS` (`true` by default): set to `false` or `0` to disable telemetry.
56
60
  - `SAMAGOTCHI_CONTEXT_WINDOW_TOKENS` / `context.window_tokens`: context window size for when the server doesn't report one. chi asks llama.cpp for its real window first (`/props`, the per-slot `n_ctx`); this setting only fills in when it can't (mlx, oMLX, server down), and 256000 is the last resort.
57
61
  - `SAMAGOTCHI_CONTEXT_CHARS_PER_TOKEN` (default `4.0`): heuristic ratio for char-to-token estimation.
58
- - `SAMAGOTCHI_CONTEXT_STATUS_THRESHOLDS` (default `20,40,60,80`): comma-separated threshold percentages.
59
- - `SAMAGOTCHI_CONTEXT_STATUS_CADENCE` (default `0`): emit every N rounds in addition to threshold crossings.
62
+ - `SAMAGOTCHI_CONTEXT_STATUS_THRESHOLDS` (default `20,40,60,80`): comma-separated threshold percentages. The buckets' guidance still gates the model's own `[CONTEXT: …]` line; nothing else reads `context.status_cadence` (it was removed once the event streamed on every request).