samagotchi 0.52.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +58 -1
- data/README.md +1 -1
- data/docs/architecture.md +94 -54
- data/docs/configuration.md +1 -2
- data/docs/context.md +104 -9
- data/docs/internals/background-tasks.md +1 -1
- data/docs/internals/bundles.md +73 -0
- data/docs/internals/context-telemetry.md +8 -5
- data/docs/internals/engine.md +16 -7
- data/docs/internals/tool-guardrails.md +8 -6
- data/docs/plugins.md +2 -1
- data/docs/releasing.md +6 -3
- data/docs/sessions.md +1 -1
- data/docs/testing.md +5 -3
- data/lib/samagotchi/analytics_file.rb +35 -0
- data/lib/samagotchi/bridge/turn_accumulator.rb +2 -0
- data/lib/samagotchi/bundle_command.rb +35 -44
- data/lib/samagotchi/bundle_needs.rb +4 -5
- data/lib/samagotchi/bundles/skills/manifest.yml +2 -2
- data/lib/samagotchi/bundles/skills/plugin.rb +2 -1
- data/lib/samagotchi/bundles/system/config_modification_protocol.md +1 -1
- data/lib/samagotchi/bundles/system/manifest.yml +2 -2
- data/lib/samagotchi/child_move.rb +6 -4
- data/lib/samagotchi/config.rb +0 -1
- data/lib/samagotchi/context_command.rb +1 -1
- data/lib/samagotchi/context_providers.rb +11 -14
- data/lib/samagotchi/context_status.rb +10 -18
- data/lib/samagotchi/engine.rb +132 -135
- data/lib/samagotchi/extension_load.rb +102 -0
- data/lib/samagotchi/guardrail_wiring.rb +16 -16
- data/lib/samagotchi/hooks/bundle_loader.rb +9 -8
- data/lib/samagotchi/host_registry.rb +13 -0
- data/lib/samagotchi/kernel_loop.rb +5 -3
- data/lib/samagotchi/llm/chat_loop.rb +5 -4
- data/lib/samagotchi/memory_bundle/asset_installer.rb +302 -0
- data/lib/samagotchi/memory_bundle/builder.rb +184 -190
- data/lib/samagotchi/memory_bundle/bundle_hook.rb +51 -0
- data/lib/samagotchi/memory_bundle/installed_bundle.rb +90 -0
- data/lib/samagotchi/memory_bundle/installer.rb +252 -488
- data/lib/samagotchi/memory_bundle/listing.rb +6 -6
- data/lib/samagotchi/memory_bundle/manifest.rb +9 -29
- data/lib/samagotchi/memory_bundle/plugin_ref.rb +27 -0
- data/lib/samagotchi/memory_bundle/profile.rb +21 -21
- data/lib/samagotchi/memory_bundle/provenance.rb +46 -57
- data/lib/samagotchi/memory_bundle/shipped_update.rb +24 -26
- data/lib/samagotchi/memory_bundle/status.rb +22 -24
- data/lib/samagotchi/memory_bundle/system_bundle.rb +4 -5
- data/lib/samagotchi/memory_bundle/uninstaller.rb +8 -10
- data/lib/samagotchi/plugin/loader.rb +8 -8
- data/lib/samagotchi/self_report.rb +2 -2
- data/lib/samagotchi/send_command.rb +4 -28
- data/lib/samagotchi/session_commands.rb +32 -4
- data/lib/samagotchi/session_continue.rb +275 -0
- data/lib/samagotchi/session_manager.rb +45 -257
- data/lib/samagotchi/session_metrics.rb +18 -21
- data/lib/samagotchi/sessions_command.rb +3 -2
- data/lib/samagotchi/terminal_ui/attached_loop.rb +2 -1
- data/lib/samagotchi/terminal_ui/formatting.rb +5 -1
- data/lib/samagotchi/terminal_ui.rb +25 -3
- data/lib/samagotchi/tool_activity.rb +84 -14
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +3 -8
- data/lib/samagotchi/web/public/activity.js +2 -0
- data/lib/samagotchi/web/public/app.js +6 -8
- data/lib/samagotchi/web/public/command_complete.js +7 -2
- data/lib/samagotchi/web/public/ctx.js +18 -4
- data/lib/samagotchi/web/public/index.html +4 -2
- data/lib/samagotchi/web/public/question_card.js +9 -1
- data/lib/samagotchi/web/public/question_cards.js +5 -3
- data/lib/samagotchi/web/public/thinking_chip.js +31 -21
- data/lib/samagotchi/web/public/timing.js +8 -2
- data/lib/samagotchi/web/public/turn_html.js +10 -6
- metadata +9 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 143c9195fbbe50c8f8193c76815004f30354a01d2e1ec7d5019e70bc79c376df
|
|
4
|
+
data.tar.gz: c1047a49f05415f8ea9a2789cf32ecc2f6edbcbf5bbfa432a8b919bd193ae360
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 12b4e73421655936e2d3183e7839f4a8580b1d2b93633cff098170540e2c98ba61cf02c847a4af8b5e19a59424f15691a81d0b38195640263773bdccf2e8b3d3
|
|
7
|
+
data.tar.gz: bf03f5dab6f9d7e30d37531127dfb88e48e779eb65f271de703f12566d728bbe869a0d3a935d33932afff4556c09b21bfe4d3d95f4b72c131bfc0521a41bf4b9
|
data/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,61 @@ 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
|
+
|
|
41
|
+
## [0.53.0] - 2026-10-10
|
|
42
|
+
|
|
43
|
+
### Changed
|
|
44
|
+
|
|
45
|
+
- Web: the `think` chip's level popover is a lit-html template (it was built as an HTML string); it looks and works
|
|
46
|
+
as before.
|
|
47
|
+
|
|
48
|
+
### Fixed
|
|
49
|
+
|
|
50
|
+
- `/model X` (and `/model X --default`, `/model clear`, and a successful `/model X --alias NAME`) now carry the
|
|
51
|
+
model's thinking level in the same place the plain `/model` reply does; a switch that fails to persist its alias
|
|
52
|
+
is unchanged.
|
|
53
|
+
- `chi bundle build --name <profile>` (an installed profile such as `core`) is refused with a line saying why: it used
|
|
54
|
+
to build a plain bundle of the scope's other memories under the profile's name, without its `includes:` and with
|
|
55
|
+
its trust level, which reinstalled over the profile would lose its members.
|
|
56
|
+
- Web: Tab completion in the composer offers `/cut` (`/cu` + Tab); the terminals' own commands (`/stats`,
|
|
57
|
+
`/exit` …) stay out of it.
|
|
58
|
+
- `chi -p /stats --non-interactive` (and `/exit`, `/recap` … — the terminals' own commands) is refused with a line
|
|
59
|
+
on stderr, as `chi send` refuses them, instead of going to the model as a prompt; it exits 1 and makes no session.
|
|
60
|
+
- A turn that fails on its host under a model alias says what the alias became: `--model splash` (an alias with no
|
|
61
|
+
host) failing on host `main` adds `(splash is an alias: gemma-x on host main)` to the error line.
|
|
62
|
+
- `chi sessions prune`, the lazy sweep and the TUI's discard-on-exit no longer crash on a session folder whose
|
|
63
|
+
`analytics.json` or `cards.json` is broken (not JSON or the wrong shape): every caller now keeps that folder like any
|
|
64
|
+
other not-empty one, so one corrupt file no longer stops the whole retention sweep.
|
|
65
|
+
|
|
11
66
|
## [0.52.0] - 2026-10-10
|
|
12
67
|
|
|
13
68
|
### Added
|
|
@@ -2387,7 +2442,9 @@ and long-lived sessions.
|
|
|
2387
2442
|
- A macOS desktop helper (`chi desktop install`): a "Send to chi" Service and a
|
|
2388
2443
|
hotkey panel that send selected text or the clipboard to your sessions.
|
|
2389
2444
|
|
|
2390
|
-
[Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.
|
|
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
|
|
2447
|
+
[0.53.0]: https://github.com/dm1try/samagotchi/compare/v0.52.0...v0.53.0
|
|
2391
2448
|
[0.52.0]: https://github.com/dm1try/samagotchi/compare/v0.51.0...v0.52.0
|
|
2392
2449
|
[0.51.0]: https://github.com/dm1try/samagotchi/compare/v0.50.0...v0.51.0
|
|
2393
2450
|
[0.50.0]: https://github.com/dm1try/samagotchi/compare/v0.49.0...v0.50.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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
│
|
|
22
|
-
│
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
│
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
│
|
|
43
|
-
│ execute · read · edit · write ·
|
|
44
|
-
│
|
|
45
|
-
│
|
|
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
|
-
|
|
|
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
|
-
-
|
|
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)
|
|
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
|
|
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`
|
|
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
|
-
|
|
173
|
-
- `:
|
|
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
|
|
182
|
-
|
|
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
|
|
322
|
-
|
|
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
|
|
398
|
-
|
|
399
|
-
|
|
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)). |
|
data/docs/configuration.md
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
42
|
-
|
|
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)
|
|
48
|
-
- **JSON
|
|
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))
|
|
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
|
|
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
|
|
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)
|
|
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.
|