samagotchi 0.16.0 → 0.17.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 +86 -1
- data/README.md +2 -0
- data/docs/architecture.md +13 -0
- data/docs/cli.md +4 -2
- data/docs/configuration.md +4 -4
- data/docs/internals/background-tasks.md +1 -0
- data/docs/memory.md +39 -0
- data/docs/plugins.md +2 -0
- data/docs/sessions.md +3 -2
- data/docs/sub-agent.md +1 -0
- data/docs/testing.md +47 -0
- data/lib/samagotchi/bridge/turn_accumulator.rb +20 -5
- data/lib/samagotchi/bridge.rb +38 -7
- data/lib/samagotchi/bundle_command.rb +13 -2
- data/lib/samagotchi/bundles/system/manifest.yml +3 -3
- data/lib/samagotchi/bundles/system/memory_guide.md +5 -5
- data/lib/samagotchi/bundles/system/self_map.md +1 -0
- data/lib/samagotchi/engine.rb +31 -5
- data/lib/samagotchi/memory_bundle/builder.rb +22 -0
- data/lib/samagotchi/memory_bundle/index_updater.rb +17 -6
- data/lib/samagotchi/memory_bundle/installer.rb +60 -21
- data/lib/samagotchi/memory_bundle/profile.rb +9 -4
- data/lib/samagotchi/memory_bundle/provenance.rb +14 -0
- data/lib/samagotchi/memory_bundle/shipped_update.rb +5 -3
- data/lib/samagotchi/memory_bundle/system_bundle.rb +5 -3
- data/lib/samagotchi/memory_bundle/trash.rb +57 -0
- data/lib/samagotchi/memory_bundle/uninstaller.rb +34 -5
- data/lib/samagotchi/plugin/context.rb +14 -2
- data/lib/samagotchi/self_report.rb +49 -3
- data/lib/samagotchi/send_command.rb +45 -6
- data/lib/samagotchi/session_metrics.rb +137 -48
- data/lib/samagotchi/system_prompt.rb +45 -4
- data/lib/samagotchi/terminal_ui/attached_loop.rb +16 -3
- data/lib/samagotchi/terminal_ui/event_renderer.rb +2 -1
- data/lib/samagotchi/terminal_ui.rb +24 -0
- data/lib/samagotchi/tools/builtins.rb +11 -3
- data/lib/samagotchi/tools/delegate_relay.rb +4 -0
- data/lib/samagotchi/tools/peers.rb +3 -2
- data/lib/samagotchi/tools/task_runtime.rb +1 -1
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +2 -1
- data/lib/samagotchi/web/public/app.js +37 -28
- data/lib/samagotchi/web/public/notify.js +7 -3
- data/lib/samagotchi/web/public/stage_view.js +13 -1
- data/lib/samagotchi/web/public/strip.js +11 -0
- data/lib/samagotchi/web/public/timing.js +26 -12
- data/lib/samagotchi/web/public/turn_events.js +13 -3
- data/lib/samagotchi/worker.rb +28 -2
- data/lib/samagotchi/worker_sidecar.rb +4 -1
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: '056529144efb1765ab53c0ce9e88a9ea979ecd8c202d3e41a12a794827fee260'
|
|
4
|
+
data.tar.gz: 054d2a858289e543a8085f0ce774935ea164a41e98467e8fb099452a81eb4729
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0a88b140031661b2dd2903df3ca74e50587971cba38029e65f8dcf8ec5e1e2a098fbfa4247c2a0954b5f249ca70d85a30600236f728e6131056f4defb6058a5b
|
|
7
|
+
data.tar.gz: 5a2d8a1fac55ad2d4a69ec0002a99a856e82de8b0a91a4c09e4a05c04a10a27d634febf0d77d5dfdfb94a7fd5b3c8f88397ed0affb13384e4394a33c7d21424e
|
data/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,90 @@ and commands may change between minor versions. How releases are made:
|
|
|
8
8
|
|
|
9
9
|
## [Unreleased]
|
|
10
10
|
|
|
11
|
+
## [0.17.0] - 2026-10-03
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- The system prompt names the model the session runs on: `Model: this session runs on box:gemma-small (host box,
|
|
16
|
+
box.test:8080; model key gemma-small).`, plus a line to answer "which model are you" from it, not from training
|
|
17
|
+
(a fine-tune often knows only its base model's name), and that model-only guidance goes in
|
|
18
|
+
`memory_write current_model_only: true` overlays. A local llama.cpp host whose `/props` names another model adds
|
|
19
|
+
`; the server says it serves <name>`. The line changes only with `/model`.
|
|
20
|
+
- `execute` and `task_create` export `SAMAGOTCHI_SESSION_MODEL`, the session's model ref (it follows `/model`; the
|
|
21
|
+
model's `env` can't set it).
|
|
22
|
+
- Plugins: `ctx.model` (the session's model ref, right after `/model` too) and `ctx.model_key` (its memory overlay
|
|
23
|
+
key).
|
|
24
|
+
- `chi self` has a `model key` row: the memory overlay key of the model it reports.
|
|
25
|
+
|
|
26
|
+
### Changed
|
|
27
|
+
|
|
28
|
+
- `chi self` run by a session's `execute` reports **that session's** model, and its host, loop, profile, thinking and
|
|
29
|
+
served model: `model splash:… (this session abcd1234; default main:…)`. Before, it showed the config default
|
|
30
|
+
(or the `--model` the worker started with), wrong after `/model`, for web-picker and `chi send --new --model`
|
|
31
|
+
sessions and delegate children. Elsewhere the row reads `main:… (default)`. `chi self --model` is still the default.
|
|
32
|
+
- The identity memory and the preloaded memories (`--memory`, config `memories:`) get their model overlays
|
|
33
|
+
(`<name>.<key>.md`) in the system prompt, as `memory_read` gives them.
|
|
34
|
+
- A session's live state (what the web and an attached terminal read from its worker) no longer carries every turn's
|
|
35
|
+
and tool call's timing record, only the newest turn's: reads stay the same size however long the session runs. The
|
|
36
|
+
full history stays in the session's `analytics.json`.
|
|
37
|
+
- A memory a bundle installed says so in its `index.md` line, and so in the prompt's memory index:
|
|
38
|
+
`- **memory_guide** · system · 2026-10-03 · 5120 · from samagotchi-system — …`. `memory_write` and `write`/`edit`
|
|
39
|
+
keep the tag; a same-name memory the install skipped gets none. Bundles installed earlier get it on their next
|
|
40
|
+
install or upgrade (the system bundle: with the next chi version).
|
|
41
|
+
- `chi bundle uninstall` (and an upgrade dropping a file the new version no longer ships) moves the bundle's memory
|
|
42
|
+
files to `<memories>/.bundles/.trash/<bundle>-<time>/` instead of deleting them, and says where:
|
|
43
|
+
`Moved to the trash: notes.md (…/.trash/notes-bundle-20261003-120000)`. Nothing empties the trash; delete it by hand.
|
|
44
|
+
- `chi bundle build` leaves out the memories an installed bundle owns (the system bundle's `identity.md`, a shipped
|
|
45
|
+
bundle's memory), one line each: `Left out identity.md: installed by bundle samagotchi-system (name it to include
|
|
46
|
+
it)`; naming a file in `FILES...` includes it. It now prints the builder's warnings.
|
|
47
|
+
- An `index.md` line a bundle install, upgrade or uninstall couldn't write or remove is now a warning
|
|
48
|
+
(`index.md: line for notes not updated (…)`) instead of passing silently; `chi bundle uninstall` of a profile
|
|
49
|
+
prints its bundles' warnings too.
|
|
50
|
+
- Web: a composer line is a command only when it names one of the session's commands (or is `!…`); an unknown
|
|
51
|
+
`/word` (a path like `/usr/bin/env …`, a typo) goes to the model as a prompt, as in a terminal, instead of being
|
|
52
|
+
refused with `not a session command`.
|
|
53
|
+
- Web: a window under 700 px tall hides the latest-sessions strip by itself (the `▾ N sessions` pill shows it again
|
|
54
|
+
until the window is tall), so a question or approval card in the turn's stage has room for its choices and
|
|
55
|
+
buttons. Taller windows and phones are unchanged.
|
|
56
|
+
|
|
57
|
+
### Fixed
|
|
58
|
+
|
|
59
|
+
- `chi self`'s `served model` row for a chat host (`api: openai`), or a server without `/props`, says
|
|
60
|
+
`reported per turn (the server has no /props)`; it said `unknown (no answer from the server's /props)`, which a
|
|
61
|
+
model took to mean the server never said what it served.
|
|
62
|
+
- `chi bundle uninstall` no longer deletes a memory the user had before the install. A bundle now records (and
|
|
63
|
+
so upgrades and removes) only the files it wrote; a same-name file that was already there is reported
|
|
64
|
+
`Skipped` and stays the user's, on install, re-install and upgrade. Before, it was recorded with the user's
|
|
65
|
+
content as its base, so uninstall removed it without `--force`: a third-party bundle shipping `notes.md`, or
|
|
66
|
+
`chi bundle build` then installing the result on the same machine. An upgrade no longer reports such a file as
|
|
67
|
+
a conflict (or `chi update --dry-run` as kept). Bundles installed before this fix may still list such a file;
|
|
68
|
+
uninstall now moves it to the trash (below) rather than deleting it.
|
|
69
|
+
- A memory file two installed bundles list (one an older install adopted, like `identity.md` under both
|
|
70
|
+
`samagotchi-system` and a `chi bundle build` of your memories) stays when one of them is uninstalled, or drops it
|
|
71
|
+
in an upgrade: `Kept identity.md: bundle samagotchi-system has it too`. It goes with the last one.
|
|
72
|
+
- Web: after a reload, a turn with an empty retry mid-turn shows each tool row's own duration; the rows took their
|
|
73
|
+
records by iteration, so every call after the retry showed the wrong one, or none.
|
|
74
|
+
- Attached TUI: `/detach` typed at a question's `?` prompt detaches (the question stays open), as Ctrl-D does; it
|
|
75
|
+
was read as an answer and refused as an unknown option.
|
|
76
|
+
- Attached TUI: joining a turn while a tool call runs (a long `delegate`, a slow command), its row's duration counts
|
|
77
|
+
from the call's start, not from the join.
|
|
78
|
+
- Web: in a short window a question or approval card that opens in the turn's stage is scrolled into view (its
|
|
79
|
+
choices and buttons); it sat below the prompt and headline, out of sight.
|
|
80
|
+
- A session command sent as a message runs as the command, as typed in a terminal: `chi send -m "/model x" ID`,
|
|
81
|
+
`chi send --new -m "/model x"`, `chi -p "/model x"` (attached and `--non-interactive`) and the web start page's
|
|
82
|
+
first message went to the model as a prompt. `chi send` says `sent as a session command`; with `--wait` there is
|
|
83
|
+
no reply to wait for (exit 0, `--format json` status `command`). An unknown `/word` still goes to the model.
|
|
84
|
+
- macOS 26: a dead worker's leftover `bridge.json` no longer counts as a live worker. Ruby's `Socket.tcp` with a
|
|
85
|
+
connect timeout returns the socket of a refused connect there, so the check said live: clients tried its port
|
|
86
|
+
first, `chi update` listed the dead worker as running, and an approval relay kept watching a parent that was gone.
|
|
87
|
+
- REPL with piped input: when the input ends at a question or approval, no bare `? ` line is left above its
|
|
88
|
+
`(denied)` / `(cancelled)` summary.
|
|
89
|
+
- Web: an approval from a delegate its parent isn't waiting on directly rings once (the parent's relayed card), not
|
|
90
|
+
twice: a child's approval now waits 3.5 s (was 1.5 s) for the relay before it notifies on its own. Only delegated
|
|
91
|
+
children's approvals wait; one nobody relays rings that much later.
|
|
92
|
+
|
|
93
|
+
Update with `chi update` (no bundle changes; the system bundle upgrades itself with chi).
|
|
94
|
+
|
|
11
95
|
## [0.16.0] - 2026-10-03
|
|
12
96
|
|
|
13
97
|
### Added
|
|
@@ -856,7 +940,8 @@ and long-lived sessions.
|
|
|
856
940
|
- A macOS desktop helper (`chi desktop install`): a "Send to chi" Service and a
|
|
857
941
|
hotkey panel that send selected text or the clipboard to your sessions.
|
|
858
942
|
|
|
859
|
-
[Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.
|
|
943
|
+
[Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.17.0...HEAD
|
|
944
|
+
[0.17.0]: https://github.com/dm1try/samagotchi/compare/v0.16.0...v0.17.0
|
|
860
945
|
[0.16.0]: https://github.com/dm1try/samagotchi/compare/v0.15.0...v0.16.0
|
|
861
946
|
[0.15.0]: https://github.com/dm1try/samagotchi/compare/v0.14.0...v0.15.0
|
|
862
947
|
[0.14.0]: https://github.com/dm1try/samagotchi/compare/v0.13.0...v0.14.0
|
data/README.md
CHANGED
|
@@ -201,6 +201,8 @@ npm test # web frontend specs
|
|
|
201
201
|
npm run e2e # web UI happy paths in Chromium, fake model (once: npx playwright install chromium)
|
|
202
202
|
```
|
|
203
203
|
|
|
204
|
+
Integration specs against a live model server, and what the suite isolates: [docs/testing.md](docs/testing.md).
|
|
205
|
+
|
|
204
206
|
## License
|
|
205
207
|
|
|
206
208
|
MIT, see [LICENSE](LICENSE).
|
data/docs/architecture.md
CHANGED
|
@@ -183,6 +183,13 @@ engine.system_prompt # fully built system prompt string
|
|
|
183
183
|
engine.session # current session (Engine owns create/resume)
|
|
184
184
|
```
|
|
185
185
|
|
|
186
|
+
The system prompt (`SystemPrompt#build`) is the base prompt, then rg guidance,
|
|
187
|
+
AGENT.md, the working directory, **the model** (`Model: this session runs on
|
|
188
|
+
<ref> (host …; model key …)` and a line telling the model to answer "which model
|
|
189
|
+
are you" from it, not from training), the session id and log, the memory indexes,
|
|
190
|
+
the identity and the preloaded memories. It is built once per loop and rebuilt only
|
|
191
|
+
by a model switch (or changed tools), so the model line costs no KV churn.
|
|
192
|
+
|
|
186
193
|
#### Subscribing to the live stream (and the bridge)
|
|
187
194
|
|
|
188
195
|
For an **always-on** consumer (an external SSE client, a second UI), use
|
|
@@ -252,6 +259,12 @@ process that already owns the `Engine`); every worker starts it, and it exposes:
|
|
|
252
259
|
the messages itself, then streams from its `event_seq`).
|
|
253
260
|
- `OPTIONS *` — CORS preflight (`Access-Control-Allow-Origin: *`).
|
|
254
261
|
|
|
262
|
+
The metrics in `/state`, `/snapshot`, `/stats` and the SSE `snapshot`/`reset` frames
|
|
263
|
+
(`SessionMetrics#snapshot`) carry the session's totals and only the recent timing records: the
|
|
264
|
+
newest turn the worker finished and any it hasn't saved yet (at most 20 turns), with their tool
|
|
265
|
+
calls and the running turn's finished ones. The full history is the session's `analytics.json`,
|
|
266
|
+
which `SessionMetrics#persist` rewrites after each turn; the web server merges the two by id.
|
|
267
|
+
|
|
255
268
|
Every route goes through `Bridge#dispatch`: an id other than the bridge's own session is
|
|
256
269
|
`404 unknown_session`, and a handler that raises answers `500 bridge_error` (logged as
|
|
257
270
|
`handler_failed`) rather than dropping the connection.
|
data/docs/cli.md
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
- `chi answer ID --question QID (--option N|LABEL)... [--text T] [--timeout S] [--format json]` (or `--dismiss`) — answer the question a session waits on (the one `chi send --wait` exited 3 with), then wait and print what comes next as `chi send --wait` does: the reply (0), the next question (3), still running after `--timeout` (4). `--option` is 1-based or the label, repeated on a multi-select question; `--text` is free text, or a Deny's reason; `--dismiss` leaves it unanswered and the model finishes its reply. A question no longer open (the web answered first) isn't answered again: it waits for that turn's reply. An option the question doesn't offer exits 2; a worker that is gone exits 1 (`send the task again: chi send --wait -m "…" ID`). An approval can be denied, not allowed, unless `guardrails.parent_approvals: once` (a convention, not a security boundary; see [Guardrails](guardrails.md#approvals-from-a-parent-agent))
|
|
22
22
|
- `chi desktop install|upgrade|uninstall|status` — the macOS "Send to chi" helper: a Service and a ⌃⌥⌘N hotkey that send text or images to a session (live, stopped or new) as a message (⏎, a turn runs) or a context note (⌘⏎) (see [Desktop helper](desktop.md))
|
|
23
23
|
- `chi models [--format text|json] [--timeout S] [TEXT]` — list the models every configured host offers, as the names `--model` takes (see [Listing the models](#listing-the-models))
|
|
24
|
-
- `chi self` — print version, source dir (checkout or installed gem), config/memory/session paths, model/host and bundles
|
|
24
|
+
- `chi self` — print version, source dir (checkout or installed gem), config/memory/session paths, model/host and bundles. Run by a session's `execute`, the `model` row (and host, loop, profile, thinking, served model) is **that session's** model, labelled `(this session <id8>; default …)`; elsewhere it is the default, `(default)`. `model key` is the memory overlay key (`<name>.<key>.md`). `chi self --model` prints only the model a new session starts on, an alias resolved to its target (the desktop helper's hint)
|
|
25
25
|
- `chi update [--dry-run] [--no-gem] [--no-bundles] [--no-desktop]` — update an installed chi: the gem, the system bundle, the shipped bundles you installed and the desktop helper, in one table (see [Updating](#updating))
|
|
26
26
|
- `chi bundle install|upgrade|uninstall|status|diff|list|build` — manage memory bundles (see [Bundle hooks](hooks.md#bundle-hooks-unified-workflow-bundle)); `list` shows the installed ones and the ones shipped with chi, which `install <name>` installs; `core` and `dev` are profiles that install a set of them (see [Bundle profiles](memory.md#bundle-profiles-core-and-dev), [Guardrails](guardrails.md), [Plugins](plugins.md#the-btw-bundle), [the mcp bundle](plugins.md#the-mcp-bundle) [the loop-guard bundle](plugins.md#the-loop-guard-bundle), [the check-in bundle](plugins.md#the-check-in-bundle) and [the skills bundle](plugins.md#the-skills-bundle)). A usage error (an unknown subcommand or flag, a missing argument, a bad `build --scope`) exits 2, as for the other subcommands. Plain `chi` and `chi web` exit 1 on an unknown flag, a flag missing its value or a stray argument
|
|
27
27
|
|
|
@@ -283,7 +283,9 @@ In an attached terminal:
|
|
|
283
283
|
- The prompt stays open while a turn runs; see [Typing during a turn](#typing-during-a-turn).
|
|
284
284
|
- `/model`, `/models`, `/guardrails`, `/continue`, `!rollback` and `!commands` run in the
|
|
285
285
|
worker, and every UI sees their output; `/stats` and `/recap` work too. The
|
|
286
|
-
Web UI's composer takes the same commands
|
|
286
|
+
Web UI's composer takes the same commands, and so do `-p "/model x"` and
|
|
287
|
+
`chi send -m "/model x"`. A `/word` the session doesn't know (a path, a
|
|
288
|
+
typo) goes to the model as a prompt.
|
|
287
289
|
- `!commands` and the model's tools run in the session's directory (where it
|
|
288
290
|
was started), whichever terminal you attach from.
|
|
289
291
|
- `-p` sends its prompt once attached, `--model` switches the worker's model
|
data/docs/configuration.md
CHANGED
|
@@ -279,14 +279,14 @@ spec. It requires the model to call `execute` and return the current UTC date:
|
|
|
279
279
|
|
|
280
280
|
```shell
|
|
281
281
|
SAMAGOTCHI_INTEGRATION=1 \
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
bundle exec rspec spec/integration/chat_loop_spec.rb
|
|
282
|
+
SAMAGOTCHI_INTEGRATION_HOST=192.0.2.10 SAMAGOTCHI_INTEGRATION_PORT=8000 \
|
|
283
|
+
SAMAGOTCHI_INTEGRATION_MODEL=incoai/Qwen3.8-27B-Splash \
|
|
284
|
+
bundle exec rspec spec/integration/chat_loop_spec.rb
|
|
285
285
|
```
|
|
286
286
|
|
|
287
287
|
The same test works against a llama.cpp OpenAI-compatible server by changing
|
|
288
288
|
the host, port, and model values. The test is skipped unless
|
|
289
|
-
`SAMAGOTCHI_INTEGRATION=1` is set.
|
|
289
|
+
`SAMAGOTCHI_INTEGRATION=1` is set; see [Testing](testing.md).
|
|
290
290
|
|
|
291
291
|
oMLX's known tool-call limitation (a stream filter that strips markup) only
|
|
292
292
|
affects its `/v1/chat/completions` endpoint, not the `/v1/completions` endpoint
|
|
@@ -22,5 +22,6 @@ Behavior:
|
|
|
22
22
|
- `task_get` returns metadata and `output_path`; use `read` for output contents.
|
|
23
23
|
- `task_wait` accepts `timeout`, `tail_lines` (maximum 100), and `done_pattern` (a regular expression string).
|
|
24
24
|
- `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.
|
|
25
|
+
- `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.
|
|
25
26
|
|
|
26
27
|
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).
|
data/docs/memory.md
CHANGED
|
@@ -23,6 +23,8 @@ Each memory entry may have a companion file named `<name>.<model-key>.md` in the
|
|
|
23
23
|
|
|
24
24
|
- **Key derivation**: The harness normalizes the model name sent to the server, without its host prefix (`box:qwen3.6-35b-a3b` keys as `qwen3.6-35b-a3b`; an alias keys as its target, and the alias's own key is read when the target has no overlay) (lowercase, replace non-alphanumeric with `-`, squeeze dashes) to derive the file key. For example, `qwen3.6-35b-a3b` → `qwen3-6-35b-a3b`.
|
|
25
25
|
- **Saving overlays**: Pass `current_model_only: true` to `memory_write` (the harness resolves the model key automatically). This writes the content as `<name>.<model-key>.md` and skips index maintenance.
|
|
26
|
+
- **Which key is mine**: the system prompt's `Model:` line names the session's model key, and `chi self` (via `execute`) has a `model key` row; both follow `/model`.
|
|
27
|
+
- **In the system prompt**: the identity memory and the preloaded memories (`--memory`, config `memories:`) get their overlays too, as `memory_read` gives them; after `/model` the rebuilt prompt carries the new model's.
|
|
26
28
|
- **Dormancy**: Overlays are only active under the matching model key; other models see the base entry only.
|
|
27
29
|
- **Invariant**: The base entry is the contract. Overlays only add model-specific guidance and never contradict the base protocol.
|
|
28
30
|
|
|
@@ -80,6 +82,43 @@ skills`, [docs/plugins.md](plugins.md#the-skills-bundle)) adds `/skill save`,
|
|
|
80
82
|
one-line diff after each update, and nudges a model that skips a failing
|
|
81
83
|
step instead of fixing the skill.
|
|
82
84
|
|
|
85
|
+
## What a bundle owns
|
|
86
|
+
|
|
87
|
+
A bundle owns the memory files it wrote, nothing else. `chi bundle install`
|
|
88
|
+
copies each of its `*.md` files into the scope dir; a file of the same name
|
|
89
|
+
that is already there is left as it is and reported `Skipped` (`--force`
|
|
90
|
+
overwrites it, and then the bundle owns it). A skipped file stays yours: the
|
|
91
|
+
bundle doesn't record it, so `chi bundle upgrade` never merges into it and
|
|
92
|
+
`chi bundle uninstall` never removes it. That holds for a file the user wrote,
|
|
93
|
+
for `chi bundle build` then installing the result on the same machine, and
|
|
94
|
+
for a file another bundle installed.
|
|
95
|
+
|
|
96
|
+
- **The index says where a memory came from.** The `index.md` line of a file
|
|
97
|
+
a bundle installed ends in `· from <bundle>`, before the description:
|
|
98
|
+
`- **memory_guide** · system · 2026-10-03 · 5120 · from samagotchi-system —
|
|
99
|
+
…`. `memory_write`, `write` and `edit` keep it. A bundle installed by chi
|
|
100
|
+
0.16 or earlier gets it on its next install or upgrade.
|
|
101
|
+
- **Uninstall moves, it doesn't delete.** The bundle's memory files go to
|
|
102
|
+
`$XDG_CONFIG_HOME/samagotchi/memories/.bundles/.trash/<bundle>-<YYYYmmdd-HHMMSS>/`,
|
|
103
|
+
and the output names them and the dir:
|
|
104
|
+
`Moved to the trash: notes.md (…/.trash/team-notes-20261003-120000)`. So
|
|
105
|
+
does an upgrade's removal of a file the new version no longer ships. A
|
|
106
|
+
file you edited still needs `--force`. Nothing empties the trash: delete
|
|
107
|
+
the dirs when you're sure. Hooks, rules and the plugin live in the
|
|
108
|
+
bundle's own dir and are deleted with it.
|
|
109
|
+
- **A file two bundles list stays** until the last of them is uninstalled
|
|
110
|
+
(`Kept identity.md: bundle samagotchi-system has it too`); its index line
|
|
111
|
+
then names the bundle that still has it.
|
|
112
|
+
- **Installs by chi 0.16 or earlier** recorded skipped files too. Such a bundle may
|
|
113
|
+
still list a file that was yours; uninstalling it moves that file to the
|
|
114
|
+
trash rather than deleting it, so check the `Moved to the trash` line and
|
|
115
|
+
move a file back if it was yours (and its line in `index.md` comes back
|
|
116
|
+
with the next `memory_write`, `write` or `edit` of it).
|
|
117
|
+
- **`chi bundle build`** packs your memories, not other bundles': a memory an
|
|
118
|
+
installed bundle owns is left out with a line (`Left out identity.md:
|
|
119
|
+
installed by bundle samagotchi-system (name it to include it)`); naming it
|
|
120
|
+
in `FILES...` includes it.
|
|
121
|
+
|
|
83
122
|
## Bundle profiles: core and dev
|
|
84
123
|
|
|
85
124
|
Every bundle chi ships (the system bundle aside) belongs to one of two
|
data/docs/plugins.md
CHANGED
|
@@ -478,6 +478,8 @@ full label, `plugin.rb (bundle my-bundle)`.
|
|
|
478
478
|
| `ctx.session_id` | the session's id (nil before there is one) |
|
|
479
479
|
| `ctx.cwd` | the session's working directory |
|
|
480
480
|
| `ctx.repo_root` | the git checkout holding `cwd`, or nil |
|
|
481
|
+
| `ctx.model` | the model the session runs on now: its resolved ref (`host:id`), right after `/model` too |
|
|
482
|
+
| `ctx.model_key` | that model's memory overlay key (`<name>.<key>.md`, what `memory_write current_model_only` writes) |
|
|
481
483
|
| `ctx.settings` | the bundle's settings, frozen |
|
|
482
484
|
| `ctx.data_dir` | `$XDG_STATE_HOME/samagotchi/plugins/<bundle>/`, created on first use. A session's own state goes in `sessions/<id>.json` or `sessions/<id>/` there: chi removes it when the session is deleted, discarded or pruned |
|
|
483
485
|
| `ctx.log` | `ctx.log.info(:event, key: value)`: debug-log records tagged `plugins`, with `bundle=<bundle>` |
|
data/docs/sessions.md
CHANGED
|
@@ -63,7 +63,7 @@ chi sessions clean --dry-run --days 7 # test sessions older than 7 da
|
|
|
63
63
|
**Ordering:**
|
|
64
64
|
|
|
65
65
|
- `Session.list` / `SessionManager.list_sessions` / `GET /api/sessions?sort=&order=&limit=&offset=` default to `updated_at desc` (newest activity first). Also supports `created_at`, `asc`. `X-Total-Count` header when paginated.
|
|
66
|
-
- Web UI (`chi web`): the page's scope is in its URL. Started in a git repo, `chi web` opens `/?dir=<that folder>`: that project's sessions, and new chats start in that folder; the header chip says `<project> · all`, and `all` opens the same place without `?dir` (every session; a new chat there starts in the server's own folder, shown on the start page, and cards name their folder; `← <project>` goes back to the project view it came from, or to the server's own project). One server serves every project: a second `chi web` (from another repo) finds it through `GET /api/info` and prints (with `--open`, opens) its page for its own folder instead of starting another. The 3 latest sessions sit above the chat; "All sessions" (or `/`) opens every session at `#/sessions`, with a search over preview, id and status (Esc or Back returns). The open session is in the URL (`#/s/<id>`), so a reload or a copied link opens it again; the chi logo top left goes back to the empty start for a new chat. The message box grows with its text; drag its top edge to keep it taller (double-click resets). The info bar copies `chi --attach <id>` for a terminal. The start page's model picker (bottom left of the composer, the chosen name `▾`) chooses the model a new chat starts on; it opens a search list over the conversation (↓ or a click; downward when the start page leaves no room above): an empty search shows the last 5 picks (Recent) and then each host's model ids A–Z under the host, the default host first; typed words must each match the host or the id (a substring, else for 3+ characters the letters in order, so `deepseek4.1 fla` finds `openrouter · deepseek/deepseek-v4.1-flash`), ⏎ picks, Esc/Tab close, a pick puts the focus in the message box: `GET /api/models` lists the hosts' models as chi spells them (`{default, models: [{name, host, id}], warning?}`; bare for the default host, `host:model` for the others, from the same cached lists as `/models`, a bounded wait, a host that is down noted in `warning`), and `POST /api/sessions` takes `model` (blank means the default). The start page's first message goes as later ones do: `POST /api/sessions` with `idle: true` and `preview` (the message, which names the session until its turn is saved), then `POST /api/sessions/:id/turn`, so a failed first turn puts its prompt back in the composer too; a first
|
|
66
|
+
- Web UI (`chi web`): the page's scope is in its URL. Started in a git repo, `chi web` opens `/?dir=<that folder>`: that project's sessions, and new chats start in that folder; the header chip says `<project> · all`, and `all` opens the same place without `?dir` (every session; a new chat there starts in the server's own folder, shown on the start page, and cards name their folder; `← <project>` goes back to the project view it came from, or to the server's own project). One server serves every project: a second `chi web` (from another repo) finds it through `GET /api/info` and prints (with `--open`, opens) its page for its own folder instead of starting another. The 3 latest sessions sit above the chat (`hide ▴` puts them away behind a `▾ N sessions` pill, remembered; a window under 700 px tall hides them by itself, so a question or approval card in the stage keeps room, and the pill shows them until the window is tall again); "All sessions" (or `/`) opens every session at `#/sessions`, with a search over preview, id and status (Esc or Back returns). The open session is in the URL (`#/s/<id>`), so a reload or a copied link opens it again; the chi logo top left goes back to the empty start for a new chat. The message box grows with its text; drag its top edge to keep it taller (double-click resets). The info bar copies `chi --attach <id>` for a terminal. The start page's model picker (bottom left of the composer, the chosen name `▾`) chooses the model a new chat starts on; it opens a search list over the conversation (↓ or a click; downward when the start page leaves no room above): an empty search shows the last 5 picks (Recent) and then each host's model ids A–Z under the host, the default host first; typed words must each match the host or the id (a substring, else for 3+ characters the letters in order, so `deepseek4.1 fla` finds `openrouter · deepseek/deepseek-v4.1-flash`), ⏎ picks, Esc/Tab close, a pick puts the focus in the message box: `GET /api/models` lists the hosts' models as chi spells them (`{default, models: [{name, host, id}], warning?}`; bare for the default host, `host:model` for the others, from the same cached lists as `/models`, a bounded wait, a host that is down noted in `warning`), and `POST /api/sessions` takes `model` (blank means the default). The start page's first message goes as later ones do: `POST /api/sessions` with `idle: true` and `preview` (the message, which names the session until its turn is saved), then `POST /api/sessions/:id/turn`, so a failed first turn puts its prompt back in the composer too; a first line that is one of the new session's commands (`/model …`) goes to `POST /api/sessions/:id/command` instead, as later ones do. The browser remembers the last choice; a running session's model is in the info bar and changes only with `/model`. In the composer a line is a command when it is `!…` or starts with one of the session's command names (its `commands` list; `/stats`, `/exit` and the other terminal words get the page's own reply); an unknown `/word` (`/usr/bin/env is missing`) goes to the model as a prompt, as in a terminal. A `POST /turn` (or a session's first `prompt`) that is one of the worker's commands runs as that command too (`202 {status, command_id, session_id}`), so `chi send`, `chi -p` and a page whose command list lags all get the command. ↑/↓ in the composer walk the prompt history the TUI's ↑ walks ([Persistent Prompt History](cli.md#persistent-prompt-history)): ↑ with the caret on the first line, ↓ from the last line while browsing, ↓ past the newest brings the draft back; `GET /api/history` returns `{entries: [...]}`, oldest first (the page fetches it on focus and after each send). The server records a sent line in the routes once the worker took it (`POST /turn` accepted, a `!command` through `POST /command` or as a session's first line); `"history": false` in the body keeps it out (an image-only send).
|
|
67
67
|
- Web notifications: when a session needs you while the tab is hidden or behind another window (a question or a guardrail approval, a plugin card with buttons during a turn such as check-in's, a failed turn, a turn done after 10 s or more; a delegated session only for its questions, a canceled turn never), the page title counts it, `(N) Chi`, until you come back to the tab. The bell at the right of the top bar also turns on OS notifications ("needs an answer", "needs approval", "needs you", "turn failed", "done in 42 s", under the session's first message; a click opens the session): its first click asks the browser for the permission, and the choice is kept per browser. Any session of the page's scope counts, not only the open one. What was already so when the page loaded never counts, and several chi tabs show one notification per event. A chi tab in front tells the others (a `BroadcastChannel`), so while you look at one, the ones behind neither notify nor count what it shows; coming back to a tab clears those events in the other tabs' titles too. The hub's `session` frames carry what this needs: `pending_question` (`{id, kind}`, kind `question` or `approval`), `pending_card` (`{id, bundle}`: the running turn's card with actions, from `pending_card.json` that the worker keeps in the session's folder while one is open; a card without actions, such as loop-guard's, never counts) and `last_turn` (`{outcome, ended_at, seconds, origin}`, saved in the session's JSON as the engine ends each turn).
|
|
68
68
|
- The web frontend is a zero-build ES-module stack in `lib/samagotchi/web/public/`: `data.js` (retrieval, typed SSE `openStream` for a session and `openEvents` for the session list), `sessions_list.js` (the list as a pure reducer over `GET /api/events`: a snapshot replaces it, an upsert keeps a known card in place, a removal drops it), `notify.js` (which of those events needs the user, for the notifications), `app.js` (presentation/state; a session with no live stream gets one when its `session` event says `bridge_up`, after one re-read: `event_seq` starts over in each worker, so a dropped stream is never resumed with its old cursor), `format.js` (pure formatters such as `previewOf`), `prompt_history.js` (the composer's ↑/↓ over the shared prompt history). Unit-tested via `npm test` (`node --test spec/web/public/*.test.js`).
|
|
69
69
|
- Selecting a session in the web UI is read-only: `GET /api/sessions/:id` never spawns a worker (it reads a live worker's snapshot when one runs, else the session file). A prompt (`POST /turn`) or a command (`POST /command`) wakes the worker, and `/stream` briefly waits for a freshly-spawned bridge before answering. A caught-up SSE reconnect holds the stream open; `reset` markers are only sent for reconnects behind the ring window, or with a cursor from another worker (event ids are `<event_seq>-<epoch>`, one epoch per worker).
|
|
@@ -149,7 +149,8 @@ pbpaste | chi send 3fa2 # the clipboard is the messa
|
|
|
149
149
|
- A session with no worker gets one started (like `--attach` or the web composer). A session open in a plain REPL (`--no-shared`) refuses it. Only sessions on this machine: Bridges listen on 127.0.0.1.
|
|
150
150
|
- Fire and forget: it returns once the message is queued and never prints the answer; that shows in whatever is attached. A guardrail "ask" waits for a UI to answer it, so with nothing attached the turn stalls there until one attaches (`chi --attach ID`).
|
|
151
151
|
- `--image PATH` (repeatable, up to 20) sends images with the message, as the web composer's chips do. Each file is read once before anything is sent (bmp, tiff and heic converted to png, large ones downscaled, as for `@path`); a missing file or one that isn't an image stops the send (exit 1; with `--wait --format json`, an `error` line) and nothing is sent; more than 20 is a usage error (exit 2). Each session gets its own copy in `<session>/images/`. Text is still required (`-m` or stdin; context alone counts), also with `--wait`. The session's model must see images: one known not to (the host's model list says text-only, `vision: false` in `models:` or `hosts:`, a native llama.cpp without a vision model) is refused before anything is sent, with the reason (`<id> refused: gemma can't take images (…); send text only or switch the model (/model)`); the other sessions of the same send still get it, and the exit is 1. With `--new` nothing is started. When chi can't tell (a native host whose `/props` doesn't answer, a list that doesn't say), the message is sent as before and a text-only model fails the turn in the session. A running turn doesn't take images mid-turn: the message runs as the next turn, and the line says `(runs after the current turn)`. With `--new` the session starts idle with the message as its preview, then the message goes in as its first turn once its worker is up (`<id> started with 1 image`); if the worker doesn't come up in 5 s the session is kept, with its id on the `failed:` line.
|
|
152
|
-
-
|
|
152
|
+
- A session command runs as the command, as typed in a terminal: `chi send -m "/model qwen" 3fa2` switches the model (`sent as a session command`), and the model never sees the line. What counts is what the session's worker knows (the built-ins, `!cmd`, its bundles' commands); an unknown `/word` (`/usr/bin/env is missing`) goes in as a message. With `--new` a line starting with `/` or `!` starts the session idle and goes to its worker once it is up, which decides. With `--wait` a command has no reply to wait for: exit 0 at once, `<id> sent as a session command: no reply to wait for …` on stderr (`{"status":"command",…,"detail":…}` with `--format json`); its output shows where the session is open.
|
|
153
|
+
- One line per session: `sent`, `sent as a session command`, `sent with 2 images`, `sent (the running turn picks it up)`, `sent (started its worker)`, `refused: …` or `failed: …`. Exit 0 when all were sent, 1 when any was refused, failed or not found, 2 for a usage error. There is no `--all`.
|
|
153
154
|
|
|
154
155
|
The Automator action above works for messages too: swap its last line for `pbpaste | "$chi" send -m "what do you make of this?" $(print -r -- "$picked" | cut -f1)`.
|
|
155
156
|
|
data/docs/sub-agent.md
CHANGED
|
@@ -33,6 +33,7 @@ Exit codes, both commands:
|
|
|
33
33
|
| Exit | `status` | Meaning |
|
|
34
34
|
|---|---|---|
|
|
35
35
|
| 0 | `answered` | `text` is chi's reply |
|
|
36
|
+
| 0 | `command` | (`chi send`) the message was a session command (`/model x`): it ran as one, no reply comes |
|
|
36
37
|
| 3 | `question` | chi waits for an answer: `question`, `answer_with` |
|
|
37
38
|
| 4 | `running` | `--timeout` passed; the turn goes on, wait again |
|
|
38
39
|
| 1 | `failed`, `canceled`, `no_answer`, `error`, `worker_gone`, `stopped` | `detail` says what happened |
|
data/docs/testing.md
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Testing
|
|
2
|
+
|
|
3
|
+
```sh
|
|
4
|
+
bundle exec parallel_rspec -n 8 # Ruby specs, in parallel (or plain: bundle exec rspec)
|
|
5
|
+
npm test # web frontend specs
|
|
6
|
+
npm run e2e # web UI happy paths in Chromium, fake model (once: npx playwright install chromium)
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
No spec reads your own setup. The suite points `XDG_CONFIG_HOME` and `XDG_STATE_HOME` at temp folders,
|
|
10
|
+
clears your `SAMAGOTCHI_*` environment, and keeps every spec off the network (WebMock).
|
|
11
|
+
|
|
12
|
+
## Integration specs
|
|
13
|
+
|
|
14
|
+
Examples tagged `:integration` (`spec/integration/`, the "with LLM access" part of `spec/hooks/integration_spec.rb`)
|
|
15
|
+
run turns against a live model server. They are skipped unless `SAMAGOTCHI_INTEGRATION=1` is set, and they need the
|
|
16
|
+
server and the model it serves:
|
|
17
|
+
|
|
18
|
+
| Variable | Meaning | Default |
|
|
19
|
+
| --- | --- | --- |
|
|
20
|
+
| `SAMAGOTCHI_INTEGRATION` | `1` runs them | off |
|
|
21
|
+
| `SAMAGOTCHI_INTEGRATION_HOST` | model server host | `localhost` |
|
|
22
|
+
| `SAMAGOTCHI_INTEGRATION_PORT` | model server port | `8080` |
|
|
23
|
+
| `SAMAGOTCHI_INTEGRATION_MODEL` | the served model id | none: the examples skip |
|
|
24
|
+
| `SAMAGOTCHI_INTEGRATION_API` | `openai` for the chat loop (`hosts.<name>.api`) | none: the native loop |
|
|
25
|
+
| `SAMAGOTCHI_INTEGRATION_TRANSPORT` | `llama_cpp`, `mlx` or `omlx` (`server.transport`) | none: `llama_cpp` |
|
|
26
|
+
|
|
27
|
+
They run under a config built from these variables alone (`default.model`, `server.host`/`port`, and one
|
|
28
|
+
`hosts.integration` entry; `spec/support/integration_server.rb`), never your `~/.config/samagotchi/config.yml`: no
|
|
29
|
+
aliases, memories, hooks, guardrails or other hosts of yours. Sessions and logs go to the suite's temp state folder.
|
|
30
|
+
|
|
31
|
+
Run them one at a time (plain `rspec`, not `parallel_rspec`): each example is a real model turn, and the server may be
|
|
32
|
+
shared.
|
|
33
|
+
|
|
34
|
+
```sh
|
|
35
|
+
SAMAGOTCHI_INTEGRATION=1 \
|
|
36
|
+
SAMAGOTCHI_INTEGRATION_HOST=192.0.2.10 SAMAGOTCHI_INTEGRATION_PORT=8081 \
|
|
37
|
+
SAMAGOTCHI_INTEGRATION_MODEL=org/Model-GGUF:Q4_K_M \
|
|
38
|
+
bundle exec rspec --tag integration spec/integration spec/hooks/integration_spec.rb spec/integration_config_spec.rb
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
A run takes one to four minutes against a 35B MoE model on llama.cpp (25 examples). `ECONNREFUSED` means the server is down. The
|
|
42
|
+
examples assert what the model did (the tool it called, the file it edited), not its wording, but they still depend on
|
|
43
|
+
a model that follows tool instructions: a small model may fail some of them.
|
|
44
|
+
|
|
45
|
+
`spec/integration/omlx_spec.rb` tests oMLX's model-id resolution and runs only with
|
|
46
|
+
`SAMAGOTCHI_INTEGRATION_TRANSPORT=omlx`, against an oMLX server (a short model selector in
|
|
47
|
+
`SAMAGOTCHI_INTEGRATION_MODEL`).
|
|
@@ -41,11 +41,23 @@ module Samagotchi
|
|
|
41
41
|
# continue, ordered parts (thinking / text / tool / input / steer /
|
|
42
42
|
# reminder / notice), pending_question and the last event_seq folded
|
|
43
43
|
# in. A finished tool part keeps the live row's action and duration_ms
|
|
44
|
-
# too
|
|
45
|
-
#
|
|
46
|
-
#
|
|
44
|
+
# too, a running one the time it has run so far (elapsed_ms), so a
|
|
45
|
+
# join times its row from its start. A notice part holds the event
|
|
46
|
+
# of one of the turn's rows (TurnNotice: a hook's notice, an
|
|
47
|
+
# empty-answer retry, a question and its answer), which a UI replays
|
|
48
|
+
# through its live handler.
|
|
47
49
|
def current_turn
|
|
48
|
-
@mutex.synchronize
|
|
50
|
+
@mutex.synchronize do
|
|
51
|
+
next nil unless @turn
|
|
52
|
+
|
|
53
|
+
turn = Marshal.load(Marshal.dump(@turn))
|
|
54
|
+
now = @clock.call
|
|
55
|
+
Array(turn[:parts]).each do |part|
|
|
56
|
+
started_at = part[:kind] == "tool" && part[:status] == "running" && @tool_started_at[[part[:iteration], part[:call_index]]]
|
|
57
|
+
part[:elapsed_ms] = ((now - started_at) * 1000).round if started_at
|
|
58
|
+
end
|
|
59
|
+
turn
|
|
60
|
+
end
|
|
49
61
|
end
|
|
50
62
|
|
|
51
63
|
# The turn in progress as conversation messages, for a plugin's
|
|
@@ -142,7 +154,10 @@ module Samagotchi
|
|
|
142
154
|
call[:label] = part[:label] if part[:label]
|
|
143
155
|
title = part[:title] ? { title: part[:title] } : {}
|
|
144
156
|
events = [{ type: :tool_call_started, **call, params: part[:params], **title }]
|
|
145
|
-
|
|
157
|
+
if part[:status] == "running"
|
|
158
|
+
events.first[:elapsed_ms] = part[:elapsed_ms] unless part[:elapsed_ms].nil?
|
|
159
|
+
return events
|
|
160
|
+
end
|
|
146
161
|
|
|
147
162
|
activity = { tool: part[:tool], status: part[:status], params: part[:params], **title }
|
|
148
163
|
activity[:action] = part[:action] if part[:action]
|
data/lib/samagotchi/bridge.rb
CHANGED
|
@@ -244,6 +244,16 @@ module Samagotchi
|
|
|
244
244
|
end
|
|
245
245
|
end
|
|
246
246
|
|
|
247
|
+
# The worker's own way in for a session command that came as a message
|
|
248
|
+
# (its first prompt, an input file): queued and announced as a POST
|
|
249
|
+
# /command's is (#handle_command).
|
|
250
|
+
# @return [String] its command_id
|
|
251
|
+
def queue_command(line, client_id: nil)
|
|
252
|
+
command = { command_id: SecureRandom.uuid, client_id: client_id, line: line.to_s.strip }
|
|
253
|
+
@engine.synchronize_events { queue_command_locked(command) }
|
|
254
|
+
command[:command_id]
|
|
255
|
+
end
|
|
256
|
+
|
|
247
257
|
private
|
|
248
258
|
|
|
249
259
|
# The Engine's commands: the built-ins and its plugins'.
|
|
@@ -612,17 +622,21 @@ module Samagotchi
|
|
|
612
622
|
deadline = fetched(parsed, "deadline")
|
|
613
623
|
return BAD_DEADLINE unless deadline_valid?(deadline)
|
|
614
624
|
|
|
615
|
-
|
|
625
|
+
queue_command_request(session_id, line, client_id: fetched(parsed, "client_id"), deadline: deadline,
|
|
626
|
+
card: fetched(parsed, "card") == true)
|
|
627
|
+
end
|
|
628
|
+
|
|
629
|
+
# Queues +line+ (a session command) for the worker and announces its
|
|
630
|
+
# :command_queued, unless +deadline+ has passed. Returns [headers,
|
|
631
|
+
# status, body]: 202 with its command_id, or 408.
|
|
632
|
+
def queue_command_request(session_id, line, client_id:, deadline: nil, card: false)
|
|
633
|
+
command = { command_id: SecureRandom.uuid, client_id: client_id, line: line }
|
|
616
634
|
# A card's action: the UIs leave its line out (the card is the echo).
|
|
617
|
-
command[:card] = true if
|
|
635
|
+
command[:card] = true if card
|
|
618
636
|
queued = @engine.synchronize_events do
|
|
619
637
|
next false if expired?("command_expired", deadline, sid: session_id, client_id: command[:client_id])
|
|
620
638
|
|
|
621
|
-
|
|
622
|
-
# An anytime command runs now, beside a turn (D8): the UIs show its
|
|
623
|
-
# line here, so the cards it shows come after it.
|
|
624
|
-
anytime = command_registry.lookup(line)&.anytime ? { anytime: true } : {}
|
|
625
|
-
@engine.announce(type: :command_queued, **command, **anytime)
|
|
639
|
+
queue_command_locked(command)
|
|
626
640
|
true
|
|
627
641
|
end
|
|
628
642
|
return deadline_passed("command") unless queued
|
|
@@ -630,6 +644,15 @@ module Samagotchi
|
|
|
630
644
|
[{}, 202, { status: "accepted", command_id: command[:command_id], session_id: @session_id }]
|
|
631
645
|
end
|
|
632
646
|
|
|
647
|
+
# With the event log held.
|
|
648
|
+
def queue_command_locked(command)
|
|
649
|
+
@on_command.call(command)
|
|
650
|
+
# An anytime command runs now, beside a turn (D8): the UIs show its
|
|
651
|
+
# line here, so the cards it shows come after it.
|
|
652
|
+
anytime = command_registry.lookup(command[:line])&.anytime ? { anytime: true } : {}
|
|
653
|
+
@engine.announce(type: :command_queued, **command, **anytime)
|
|
654
|
+
end
|
|
655
|
+
|
|
633
656
|
# A client asks the worker to exit now (`/exit` in the attached TUI). The
|
|
634
657
|
# worker decides with the event log held, so no POST /turn lands between
|
|
635
658
|
# its check and its answer; it leaves from its loop after this reply.
|
|
@@ -688,6 +711,14 @@ module Samagotchi
|
|
|
688
711
|
deadline = fetched(parsed, "deadline")
|
|
689
712
|
return BAD_DEADLINE unless deadline_valid?(deadline)
|
|
690
713
|
|
|
714
|
+
# A session command sent as a message (chi send -m "/model x", chi -p,
|
|
715
|
+
# a UI whose command list lags) runs as the command, as typed in a
|
|
716
|
+
# TUI; an unknown /word stays a prompt. Not with images: those are
|
|
717
|
+
# for the model.
|
|
718
|
+
if images.empty? && @on_command && command_registry.command?(prompt.to_s)
|
|
719
|
+
return queue_command_request(sid, prompt.to_s.strip, client_id: client_id, deadline: deadline)
|
|
720
|
+
end
|
|
721
|
+
|
|
691
722
|
enqueued_id = SecureRandom.uuid
|
|
692
723
|
# Write and announce with the event log held: the worker can't emit
|
|
693
724
|
# this turn's :turn_started (or merge it mid-turn) before
|
|
@@ -36,7 +36,7 @@ module Samagotchi
|
|
|
36
36
|
build [--scope system|project] [--name NAME] [--version VER] [--description DESC] [--out PATH] [FILES...]
|
|
37
37
|
Build local memories and installed hooks (and plugin) into a shareable bundle (dir or zip).
|
|
38
38
|
--scope selects source dir (default: system). --out inferred from extension; default <name>.zip.
|
|
39
|
-
FILES... optional allowlist of *.md basenames to include (default: all).
|
|
39
|
+
FILES... optional allowlist of *.md basenames to include (default: all but installed bundles' ones).
|
|
40
40
|
TEXT
|
|
41
41
|
|
|
42
42
|
# Each subcommand's --help (and only --help: -h and help are taken as
|
|
@@ -57,7 +57,7 @@ module Samagotchi
|
|
|
57
57
|
build [--scope system|project] [--name NAME] [--version VER] [--description DESC] [--out PATH] [FILES...]
|
|
58
58
|
Build local memories and installed hooks into a shareable bundle (dir or zip).
|
|
59
59
|
--scope selects source dir (default: system). --out inferred from extension; default <name>.zip.
|
|
60
|
-
FILES... optional allowlist of *.md basenames to include (default: all).
|
|
60
|
+
FILES... optional allowlist of *.md basenames to include (default: all but installed bundles' ones).
|
|
61
61
|
|
|
62
62
|
Examples:
|
|
63
63
|
chi bundle build --scope system
|
|
@@ -272,6 +272,7 @@ module Samagotchi
|
|
|
272
272
|
begin
|
|
273
273
|
uninstaller.run
|
|
274
274
|
@stdout.puts "Uninstalled bundle '#{bundle_name}'"
|
|
275
|
+
puts_trashed(uninstaller.trashed_files, uninstaller.trash_dir)
|
|
275
276
|
@stdout.puts "Removed: #{uninstaller.removed_files.join(', ')}" unless uninstaller.removed_files.empty?
|
|
276
277
|
hook_removed = uninstaller.removed_files.count { |f| f.start_with?("hooks/") }
|
|
277
278
|
@stdout.puts "Hooks removed: #{hook_removed}" if hook_removed > 0
|
|
@@ -311,9 +312,13 @@ module Samagotchi
|
|
|
311
312
|
if result.gone
|
|
312
313
|
@stdout.puts "Uninstalled bundle '#{name}'"
|
|
313
314
|
@stdout.puts "Removed: #{result.removed.join(", ")}" unless result.removed.empty?
|
|
315
|
+
result.trash.each_value { |files, dir| puts_trashed(files, dir) }
|
|
316
|
+
result.warnings.each { |w| @stdout.puts w }
|
|
314
317
|
return 0
|
|
315
318
|
end
|
|
316
319
|
@stdout.puts "Removed from #{name}: #{result.removed.join(", ")}" unless result.removed.empty?
|
|
320
|
+
result.trash.each_value { |files, dir| puts_trashed(files, dir) }
|
|
321
|
+
result.warnings.each { |w| @stdout.puts w }
|
|
317
322
|
result.blocked.each { |member, why| @stdout.puts "Kept #{member}: #{why}" }
|
|
318
323
|
@stderr.puts "Uninstall failed: #{name} stays installed until #{result.blocked.keys.join(", ")} goes " \
|
|
319
324
|
"(chi bundle uninstall #{name} --force)"
|
|
@@ -323,6 +328,11 @@ module Samagotchi
|
|
|
323
328
|
1
|
|
324
329
|
end
|
|
325
330
|
|
|
331
|
+
# Memory files an uninstall moved to the trash (MemoryBundle::Trash).
|
|
332
|
+
def puts_trashed(files, dir)
|
|
333
|
+
@stdout.puts "Moved to the trash: #{files.join(", ")} (#{dir})" if dir && !files.empty?
|
|
334
|
+
end
|
|
335
|
+
|
|
326
336
|
def status(rest)
|
|
327
337
|
name = rest.find { |a| !a.start_with?("--") }
|
|
328
338
|
require "json"
|
|
@@ -557,6 +567,7 @@ module Samagotchi
|
|
|
557
567
|
@stdout.puts "Built #{result[:files].size} file(s) to #{result[:out_path]}"
|
|
558
568
|
@stdout.puts "Bundle: #{result[:name]} v#{result[:version]} scope=#{result[:scope]}"
|
|
559
569
|
@stdout.puts "Files: #{result[:files].join(', ')}" unless result[:files].empty?
|
|
570
|
+
builder.warnings.each { |w| @stdout.puts w }
|
|
560
571
|
unless result[:placeholder_warnings].empty?
|
|
561
572
|
@stdout.puts "Placeholders:"
|
|
562
573
|
result[:placeholder_warnings].each { |w| @stdout.puts " #{w}" }
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: samagotchi-system
|
|
3
|
-
version: 0.
|
|
3
|
+
version: 0.17.0
|
|
4
4
|
scope: system
|
|
5
5
|
description: Default system memories — identity, self map, config modification protocol, memory guide and the delegated-session rules
|
|
6
6
|
files:
|
|
7
7
|
identity.md: sha256:29c2f7df9f6c5acbc5ec17000a4490741e55e426af15ca0d81c8706edd9bd027
|
|
8
|
-
self_map.md: sha256:
|
|
8
|
+
self_map.md: sha256:69bc0ec091fdaeedf6e3aa7365275461fe3eba32b2dedc9cf45044176c356af5
|
|
9
9
|
config_modification_protocol.md: sha256:d0e9d4a84652bf54ec2fd664f61c536637af9decdd22fcd982f52195bf9f5fb4
|
|
10
|
-
memory_guide.md: sha256:
|
|
10
|
+
memory_guide.md: sha256:e535e31fcf29b4b32c270042851ecdd5f412ef2e3c56b0076963e3e1f1a08ef8
|
|
11
11
|
delegated.md: sha256:0a71d5dfa7127df2d51cb5f0e7214240ed88777e36559430a8724c796d7ae561
|
|
@@ -16,7 +16,7 @@ This memory teaches you (the agent) how to use Samagotchi memories — persisten
|
|
|
16
16
|
| `project` | `<system dir>/projects/<basename>_<hash>/` (`MemoryPaths.project_dir`: `basename(root)` + 8-char `MD5(root)`, root = `MemoryPaths.project_root`) | Repo-specific conventions, workflow, stack decisions |
|
|
17
17
|
|
|
18
18
|
- The project root is the git repository: its common git dir, which every linked worktree shares. All worktrees and subdirectories of one repository share one project folder; the system prompt shows the resolved root and folder. Outside a repository the root is the working directory. A separate clone is a different project.
|
|
19
|
-
- `index.md` lives in each scope dir and contains auto-managed lines like `- **name** · scope · date · bytes — description
|
|
19
|
+
- `index.md` lives in each scope dir and contains auto-managed lines like `- **name** · scope · date · bytes — description`; a memory a bundle installed adds `· from <bundle>` before the description. Your free-form sections in `index.md` are preserved but managed lines are owned by `memory_write` (and refreshed when `write`/`edit` change a memory's file).
|
|
20
20
|
|
|
21
21
|
## Tools you have
|
|
22
22
|
|
|
@@ -85,13 +85,13 @@ Bundles are versioned directories/zips/tar.gz/git URLs with a `manifest.yml` and
|
|
|
85
85
|
|
|
86
86
|
| Command | Purpose |
|
|
87
87
|
|---------|---------|
|
|
88
|
-
| `install <source> [--scope system\|project] [--force]` | Install from dir/zip/tar.gz/git URL. `--force` overwrites existing entries, otherwise skips. Writes provenance to `.bundles/<name>/` (base snapshots + `manifest.json`) and updates `index.md`. Warns on double-brace placeholders and checksum mismatches (strict mode). |
|
|
88
|
+
| `install <source> [--scope system\|project] [--force]` | Install from dir/zip/tar.gz/git URL. `--force` overwrites existing entries, otherwise skips them; a skipped file stays the user's (not recorded, never upgraded or removed by the bundle). Writes provenance to `.bundles/<name>/` (base snapshots + `manifest.json`) and updates `index.md`. Warns on double-brace placeholders and checksum mismatches (strict mode). |
|
|
89
89
|
| `upgrade <source> [--force] [--dry-run] [--agent]` | 3-way merge upgrade (base vs current vs incoming) per `Merger.classify`: `install` (new file), `noop` (current==incoming), `fast_forward` (current==base, not edited → auto-update), `keep` (incoming==base → preserve local edits), `conflict` (both edited → warn, needs `--force` or interactive `memory_write` resolution). Pruned files (removed from new bundle) are kept if locally edited, otherwise warned. |
|
|
90
|
-
| `uninstall <bundle> [--force]` |
|
|
90
|
+
| `uninstall <bundle> [--force]` | Moves the bundle's memory files to `.bundles/.trash/<bundle>-<time>/` (refuses locally edited ones unless `--force`; keeps a file another installed bundle also lists), removes their `index.md` lines, deletes the provenance dir. |
|
|
91
91
|
| `status [<bundle>]` | Provenance + per-file `ok|modified|missing|no-index` vs stored checksum and base snapshot. |
|
|
92
92
|
| `diff <bundle> [file]` | Prints `base` (provenance snapshot) vs `current` (on-disk) for each file. |
|
|
93
93
|
| `list` | Lists installed bundles (`name v<version> scope files installed_at`, plus the shipped version when newer) and the bundles shipped with chi that are not installed (`install <name>` installs one), grouped under their profile; an installed profile shows `includes=` and `left out=`. |
|
|
94
|
-
| `build [--scope system\|project] [--name NAME] [--version VER] [--description DESC] [--out PATH] [FILES...]` | **Inverse of install** — builds a shareable bundle from local memories and installed hooks. Infers `zip` vs `dir`/`tar.gz` from `--out` extension; default `chi_system_memories.zip` (system) or `chi_<repo>_memories.zip` (project, named after the project root) v`1.0.0` in `Dir.pwd`. `FILES...` is an optional allowlist of memory basenames (`identity` or `identity.md`); if omitted, all `*.md` except `index.md`/hidden/non-md are included. Installed hooks are copied to `hooks/` with their manifest metadata. Computes `sha256:` checksums and writes `manifest.yml` via `Manifest.write`. No provenance write. |
|
|
94
|
+
| `build [--scope system\|project] [--name NAME] [--version VER] [--description DESC] [--out PATH] [FILES...]` | **Inverse of install** — builds a shareable bundle from local memories and installed hooks. Infers `zip` vs `dir`/`tar.gz` from `--out` extension; default `chi_system_memories.zip` (system) or `chi_<repo>_memories.zip` (project, named after the project root) v`1.0.0` in `Dir.pwd`. `FILES...` is an optional allowlist of memory basenames (`identity` or `identity.md`); if omitted, all `*.md` except `index.md`/hidden/non-md and the memories other installed bundles own (each named in a `Left out` line) are included. Installed hooks are copied to `hooks/` with their manifest metadata. Computes `sha256:` checksums and writes `manifest.yml` via `Manifest.write`. No provenance write. |
|
|
95
95
|
|
|
96
96
|
**Scope resolution for install/build:**
|
|
97
97
|
- CLI `--scope` wins over `manifest.yml` `scope`. Default is `system` if none given (`Installer#run`). For `project`, target is the project folder `<system dir>/projects/<basename>_<hash>` of the project root (`Installer#resolve_target_dir`).
|
|
@@ -119,7 +119,7 @@ chi bundle build --scope system --out updated.zip
|
|
|
119
119
|
### Provenance internals (for debugging)
|
|
120
120
|
|
|
121
121
|
- Each installed bundle is recorded at `<system dir>/.bundles/<name>/manifest.json` + `bases/<file>.md` snapshots (`Provenance`). Used only for upgrade `Merger` and `status`/`diff`. Build does **not** write provenance.
|
|
122
|
-
- `index.md` is
|
|
122
|
+
- A bundle records only the files it wrote. `index.md` updates aren't fatal: a failed one is a warning in the install/uninstall output.
|
|
123
123
|
|
|
124
124
|
## Best practices for the agent
|
|
125
125
|
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
## Start here
|
|
4
4
|
- `chi self` (via `execute`) prints my version, **source dir**, config path, hooks dir,
|
|
5
5
|
memory dirs, sessions dir, model/host and bundles. Use its source dir; don't hunt via `which`/`gem list`/`find /`.
|
|
6
|
+
Its `model` row is this session's model (live after `/model`), `model key` my overlay key.
|
|
6
7
|
- My current session id is in the system prompt; resume with `chi --resume <id>`.
|
|
7
8
|
- My debug log is the `log` line of `chi self` (default `$XDG_STATE_HOME/samagotchi/samagotchi.log`,
|
|
8
9
|
next to the sessions dir; not `~/.local/state` when `XDG_STATE_HOME` is set). One record per line,
|