samagotchi 0.46.1 → 0.47.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 +34 -1
- data/docs/cli.md +1 -1
- data/docs/plugins.md +6 -0
- data/docs/sessions.md +23 -5
- data/lib/samagotchi/bundles/coordinator/manifest.yml +4 -4
- data/lib/samagotchi/bundles/coordinator/plugin.rb +15 -2
- data/lib/samagotchi/bundles/coordinator/skill_coordinator.md +20 -0
- data/lib/samagotchi/bundles/system/manifest.yml +1 -1
- data/lib/samagotchi/send_command.rb +59 -9
- data/lib/samagotchi/session.rb +13 -4
- data/lib/samagotchi/session_chain.rb +93 -0
- data/lib/samagotchi/session_manager.rb +225 -14
- data/lib/samagotchi/sessions_command.rb +5 -1
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +48 -0
- data/lib/samagotchi/web/public/app.js +200 -18
- data/lib/samagotchi/web/public/chains.js +234 -0
- data/lib/samagotchi/web/public/data.js +12 -0
- data/lib/samagotchi/web/public/families_view.js +3 -2
- data/lib/samagotchi/web/public/index.html +29 -6
- data/lib/samagotchi/web/public/session_state.js +2 -0
- data/lib/samagotchi/web/public/sessions_list.js +4 -2
- data/lib/samagotchi/web/public/turn_events.js +7 -3
- data/lib/samagotchi/web/session_summary.rb +3 -0
- 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: 31cacb8f3d63a915a92684dcac667920837f96675f9c4256e3962cbfd95df5ef
|
|
4
|
+
data.tar.gz: 29668be85fcc4178f470f15d8f2a1d1745cccb2a4e0391649ef85f8b5a5ad9bd
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e4c46bb7eee9e046679b9d4ca15540b17f6b1e15f92756c3c55d7733fb8d313b3e5b12b5077027950dc7f1671d46a11d6aaac1aa72ce3d4668475e87aa0ebea3
|
|
7
|
+
data.tar.gz: 86e41f7cd8fe707cc8ad6914a72ce183b824f86d6bbc4c953a356ff9d5709776e49c0b254734f2e675061de8d26c2c66bfcd538fe9d8f28f5f4d7d727b7decbc
|
data/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,38 @@ and commands may change between minor versions. How releases are made:
|
|
|
8
8
|
|
|
9
9
|
## [Unreleased]
|
|
10
10
|
|
|
11
|
+
## [0.47.0] - 2026-10-09
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- Session chains: a session can be continued as the next link of its chain (a day's work after yesterday's, say).
|
|
16
|
+
The next link starts in the same folder, on the same model and LLM context, with a visible note that carries the
|
|
17
|
+
previous link's recap (asked for fresh from a live worker), and the previous link is archived with its delegates.
|
|
18
|
+
A continue is refused while one of those delegates is still open, for a link continued already (no forks), and for
|
|
19
|
+
a folder that's gone. See [docs/sessions.md](docs/sessions.md#session-chains).
|
|
20
|
+
- Web: `continue →` in the info bar starts the next link and opens it with `Continue where we left off.` in the
|
|
21
|
+
composer, not sent. A link continued already links to its neighbours (`← date` / `date →`), and a continue
|
|
22
|
+
refused for open delegates names them, each a link. A chain is one card in the lists, the latest link's, whose
|
|
23
|
+
`↩ day N` chip lists the earlier links (day, date, recap) in a popover; a link titled with the opener shows its
|
|
24
|
+
chain's first title.
|
|
25
|
+
- CLI: `chi send --new --continues (ID|PREFIX|last:ID) [-m TEXT]` (idle without a message); `last:ID` follows the
|
|
26
|
+
chain to its latest link, for a script or a scheduled job.
|
|
27
|
+
- API: `POST /api/sessions` takes `continues: <id>` (with `idle: true` or a `prompt`); a link continued already
|
|
28
|
+
answers 409 `continued` with `next_id`, an open delegate 409 `open_children`. Sessions carry `continues`
|
|
29
|
+
(`chi sessions list` ends such a row with `↪ <previous short id>`; `--format json` and the web summaries too).
|
|
30
|
+
- coordinator 0.3.0: `/coordinate end` ends the day. The model brings its `handoff_*` memory up to date (state per
|
|
31
|
+
branch, decisions, verdicts, follow-ups, each open child's id, branch and worktree), keeps it OPEN unless everything
|
|
32
|
+
is done, and gives a short end-of-day report that names the children still running and says
|
|
33
|
+
`/coordinate resume <slug>` for next time. It starts, merges and stops nothing.
|
|
34
|
+
|
|
35
|
+
### Fixed
|
|
36
|
+
|
|
37
|
+
- A web message that ended (or began) with whitespace stayed in the composer after it was sent: one sent right
|
|
38
|
+
after Annotate (its quote ends in a blank line), or with a trailing newline or space. The sent text is trimmed,
|
|
39
|
+
and the composer was cleared only when it held exactly that text; it is now compared trimmed too.
|
|
40
|
+
|
|
41
|
+
Update with `chi update`: it updates coordinator (0.3.0: `/coordinate end`).
|
|
42
|
+
|
|
11
43
|
## [0.46.1] - 2026-10-09
|
|
12
44
|
|
|
13
45
|
### Changed
|
|
@@ -2183,7 +2215,8 @@ and long-lived sessions.
|
|
|
2183
2215
|
- A macOS desktop helper (`chi desktop install`): a "Send to chi" Service and a
|
|
2184
2216
|
hotkey panel that send selected text or the clipboard to your sessions.
|
|
2185
2217
|
|
|
2186
|
-
[Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.
|
|
2218
|
+
[Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.47.0...HEAD
|
|
2219
|
+
[0.47.0]: https://github.com/dm1try/samagotchi/compare/v0.46.1...v0.47.0
|
|
2187
2220
|
[0.46.1]: https://github.com/dm1try/samagotchi/compare/v0.46.0...v0.46.1
|
|
2188
2221
|
[0.46.0]: https://github.com/dm1try/samagotchi/compare/v0.45.0...v0.46.0
|
|
2189
2222
|
[0.45.0]: https://github.com/dm1try/samagotchi/compare/v0.44.0...v0.45.0
|
data/docs/cli.md
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
- `chi sessions list|stop|restart|archive|unarchive|delete|prune|clean` — manage persisted sessions; `list` shows this git project's, `list --scope=all` every one, a delegated session with `↳ <parent>`, `list --archived` the archived ones too (see [Sessions](sessions.md)). A usage error (an unknown subcommand or flag, a flag missing its value, `stop`, `restart` or `delete` with no ids, a bad `list --format` or `--scope`) exits 2; an unknown or refused session exits 1
|
|
19
19
|
- `chi note [--source NAME] [-m TEXT] (ID|PREFIX)... | --all` — add a context note (TEXT or stdin) to sessions: background the model sees on its next turn; it starts no turn (see [Sessions: Context notes](sessions.md#context-notes))
|
|
20
20
|
- `chi context <add|push|ls|show|refresh|rm|mute|unmute> …` — attach live external text to sessions (a command chi runs every so often, or text pushed in); the model gets a short note when it changes and reads it with `context_read`; `chi context add <URL>` attaches a GitHub PR with the github-pr bundle (see [Attached context](context.md))
|
|
21
|
-
- `chi send [-m TEXT] [--image PATH]... (ID|PREFIX)...` — send a message to sessions as if typed there: a turn starts (or a running one picks it up); piped stdin goes above `-m` as quoted context, and `--image` attaches images (see [Sessions: Sending a message](sessions.md#sending-a-message)); `--new` starts a session with it instead, and `--wait` prints the answer (`--wait ID` with no message waits for the next reply without sending; `--format json` prints one JSON object instead; exit 3 a question waits, its options on stderr; exit 4 `--timeout` passed with the turn still running; see [Starting a session](sessions.md#starting-a-session))
|
|
21
|
+
- `chi send [-m TEXT] [--image PATH]... (ID|PREFIX)...` — send a message to sessions as if typed there: a turn starts (or a running one picks it up); piped stdin goes above `-m` as quoted context, and `--image` attaches images (see [Sessions: Sending a message](sessions.md#sending-a-message)); `--new` starts a session with it instead (`--new --continues ID|last:ID` the next link of that session's chain, see [Session chains](sessions.md#session-chains)), and `--wait` prints the answer (`--wait ID` with no message waits for the next reply without sending; `--format json` prints one JSON object instead; exit 3 a question waits, its options on stderr; exit 4 `--timeout` passed with the turn still running; see [Starting a session](sessions.md#starting-a-session))
|
|
22
22
|
- `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))
|
|
23
23
|
- `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))
|
|
24
24
|
- `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))
|
data/docs/plugins.md
CHANGED
|
@@ -1291,6 +1291,12 @@ by themselves there.
|
|
|
1291
1291
|
the model to the skill. **`/coordinate resume`** asks it to resume this
|
|
1292
1292
|
project's open handoff; with several open it shows a card with a Resume
|
|
1293
1293
|
action each (`/coordinate resume <name>` picks one), with none it says so.
|
|
1294
|
+
**`/coordinate end`** ends the day: the model checks each branch, brings
|
|
1295
|
+
its handoff up to date (state per branch, decisions, verdicts,
|
|
1296
|
+
follow-ups, each open child's id, branch and worktree), keeps it OPEN unless everything is done, and gives a short
|
|
1297
|
+
report ending with the `/coordinate resume <slug>` to pick it up next
|
|
1298
|
+
time. It starts, merges and stops nothing: running children stay running
|
|
1299
|
+
and are named in the report.
|
|
1294
1300
|
- **`/children [all]`** shows the session's children as a card, newest first,
|
|
1295
1301
|
one line each: `` `ab12cd34` · running · fix/flaky · "fix the flaky spec" ``,
|
|
1296
1302
|
`` `9a8b7c6d` · done · feat/x · reported · "All 12 specs pass" `` (`not
|
data/docs/sessions.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Sessions
|
|
2
2
|
|
|
3
|
-
Sessions are plain files — no DB. Each session is `~/.local/state/samagotchi/sessions/<uuid>.json` (XDG-aware via `XDG_STATE_HOME`) plus a sidecar dir `<uuid>/` with `input/`/`notes/`/`output/`/`owner.lock`/`bridge.json` (`Session.session_dir`). Besides the messages and turn state, the JSON keeps `used_memory_names` (what the session read), `preloaded_memory_names` (`--memory`) and `muted_memory_names` (`--mute`), the last two written before the worker starts so a respawn builds the same prompt, `prompt_notes`, the model notes the system prompt carried at its last build (`[{name, scope, chars, digest}]`, see [Memory](memory.md#model-notes)),
|
|
3
|
+
Sessions are plain files — no DB. Each session is `~/.local/state/samagotchi/sessions/<uuid>.json` (XDG-aware via `XDG_STATE_HOME`) plus a sidecar dir `<uuid>/` with `input/`/`notes/`/`output/`/`owner.lock`/`bridge.json` (`Session.session_dir`). Besides the messages and turn state, the JSON keeps `used_memory_names` (what the session read), `preloaded_memory_names` (`--memory`) and `muted_memory_names` (`--mute`), the last two written before the worker starts so a respawn builds the same prompt, `prompt_notes`, the model notes the system prompt carried at its last build (`[{name, scope, chars, digest}]`, see [Memory](memory.md#model-notes)), `parent_id`, the session that delegated this one (see *Delegating*; `null` otherwise), and `continues`, the session this one continues (the previous link of a chain, see *Session chains*; `null` otherwise); files from before those keys read as empty lists, no parent and no previous link. `model_name` is the resolved model ref (an alias applied, `host:id` when it names a host), so a resumed session keeps its model when an alias is retargeted; `model_typed` is the name it was given as when that differs (an alias), for the `models:` lookup after a resume (`null` otherwise).
|
|
4
4
|
|
|
5
5
|
**Retention (file-based, opt-out via env):**
|
|
6
6
|
|
|
@@ -25,7 +25,7 @@ A session is deleted if **expired by age OR overflow by count** (unless `keep_st
|
|
|
25
25
|
|
|
26
26
|
**Deleting a session:** `chi sessions delete ID...`, `/exit --delete` in a terminal and the Web UI's delete all go through `SessionManager.delete_session`: it resolves a unique prefix, removes `<id>.json` and the whole `<id>/` dir, and returns what it removed. A session a plain REPL has open (`owner.lock` kind `tui`) is always refused. One a worker runs is refused unless the caller asks to stop it: then it stops the worker as `chi sessions stop` does and deletes once `owner.lock` is free (`--force` on the CLI, 10 s; the web always, 2 s; `/exit --delete` after the worker agreed to exit, 10 s). A worker that outlives the wait leaves the session in place. The web's route is `DELETE /api/sessions/:id` (200 `{status: "deleted", session_id, stopped}`; 409 `owned_by_tui` or `still_stopping`; 404).
|
|
27
27
|
|
|
28
|
-
<a id="archiving-a-session"></a>**Archiving a session:** `chi sessions archive ID...`, `/archive` in a terminal and the Web UI's `archive` go through `SessionManager.archive_session`: it writes `<id>/archived` (`{"archived_at": …}`; not a session.json field, which every save rewrites) for the session and its delegated children. `Session.list` leaves archived sessions out unless `include_archived:`, and everything built on it follows: `chi sessions list` (`--archived` shows them, `[archived]`), the summaries (`--live`, json/tsv, the desktop panel, the agent's `list_sessions`) and the retention prune, which neither deletes nor counts them against `max_count`. `children_of` (max_children, `delegate_result`) still sees them, and `--resume`/`--attach ID`, `chi send`/`chi note ID` reach them as before. Refused: a turn running in the session or in one of its children (naming the child; 409 `busy` on the web), a prompt queued in `input/` (it would run on the next resume) or a live worker's step-limit question still open, in it or a child (409 `busy` too), a plain REPL owning it (`owned_by_tui`), a `chi scratch` session (`scratch`). A live idle worker is stopped first, and the marker is written even if it is still shutting down. Input a human typed brings a session back (`{"unarchived_at": …}`): a prompt from the web, an attached or plain terminal or `chi send` (origin `web:*`, `tui:*`, `cli:send`, none), steering merged into a running turn, a `/continue` answer or a question answered (the step-limit question too); a delegate's follow-up, a plugin's turn, a reminder, the idle recap, a note and just opening it don't. A child's input brings only the child back. The retention sweep ages an unarchived session from when it was unarchived, so unarchiving an old session doesn't let the next sweep take it. `/archive` leaves the session first, as `/exit` does, then archives it: in an attached terminal the worker is asked to exit (one that stays up for another UI is stopped by the archive; a running turn, a queued prompt or an open step-limit question refuses it), and an empty session is discarded instead; `chi scratch` refuses it. Typed while a turn runs, `/archive` waits for the turn's end (an attached terminal for the commands queued behind it too) and then archives. `chi sessions unarchive ID...` and the web's `unarchive` bring back the whole family. Web routes: `POST /api/sessions/:id/archive` (200 `{status, session_id, archived, stopped, discarded}`, ids) and `POST /api/sessions/:id/unarchive` (200 `{status, session_id, unarchived}`). The session hub and `GET /api/sessions` keep archived sessions, `archived: true` in their summaries: the page leaves them out of the strip, its count and the list at render, and "include archived" by the all-sessions search shows them, dimmed with an `archived` badge. The info bar marks a session whose live worker runs an older chi than the newest installed (`worker_version` in its summary): `chi 0.18.1 · Restart`, which asks to confirm and calls `POST /api/sessions/:id/restart` (a refusal's reason shows in a toast); a worker from before restarts shows `chi sessions stop ID` instead. The info bar reads `archive · stop · delete` (`unarchive` on an archived one); archiving the open session stays on it, with an Undo toast.
|
|
28
|
+
<a id="archiving-a-session"></a>**Archiving a session:** `chi sessions archive ID...`, `/archive` in a terminal and the Web UI's `archive` go through `SessionManager.archive_session`: it writes `<id>/archived` (`{"archived_at": …}`; not a session.json field, which every save rewrites) for the session and its delegated children. `Session.list` leaves archived sessions out unless `include_archived:`, and everything built on it follows: `chi sessions list` (`--archived` shows them, `[archived]`), the summaries (`--live`, json/tsv, the desktop panel, the agent's `list_sessions`) and the retention prune, which neither deletes nor counts them against `max_count`. `children_of` (max_children, `delegate_result`) still sees them, and `--resume`/`--attach ID`, `chi send`/`chi note ID` reach them as before. Refused: a turn running in the session or in one of its children (naming the child; 409 `busy` on the web), a prompt queued in `input/` (it would run on the next resume) or a live worker's step-limit question still open, in it or a child (409 `busy` too), a plain REPL owning it (`owned_by_tui`), a `chi scratch` session (`scratch`). A live idle worker is stopped first, and the marker is written even if it is still shutting down. Input a human typed brings a session back (`{"unarchived_at": …}`): a prompt from the web, an attached or plain terminal or `chi send` (origin `web:*`, `tui:*`, `cli:send`, none), steering merged into a running turn, a `/continue` answer or a question answered (the step-limit question too); a delegate's follow-up, a plugin's turn, a reminder, the idle recap, a note and just opening it don't. A child's input brings only the child back. The retention sweep ages an unarchived session from when it was unarchived, so unarchiving an old session doesn't let the next sweep take it. `/archive` leaves the session first, as `/exit` does, then archives it: in an attached terminal the worker is asked to exit (one that stays up for another UI is stopped by the archive; a running turn, a queued prompt or an open step-limit question refuses it), and an empty session is discarded instead; `chi scratch` refuses it. Typed while a turn runs, `/archive` waits for the turn's end (an attached terminal for the commands queued behind it too) and then archives. `chi sessions unarchive ID...` and the web's `unarchive` bring back the whole family. Web routes: `POST /api/sessions/:id/archive` (200 `{status, session_id, archived, stopped, discarded}`, ids) and `POST /api/sessions/:id/unarchive` (200 `{status, session_id, unarchived}`). The session hub and `GET /api/sessions` keep archived sessions, `archived: true` in their summaries: the page leaves them out of the strip, its count and the list at render, and "include archived" by the all-sessions search shows them, dimmed with an `archived` badge. The info bar marks a session whose live worker runs an older chi than the newest installed (`worker_version` in its summary): `chi 0.18.1 · Restart`, which asks to confirm and calls `POST /api/sessions/:id/restart` (a refusal's reason shows in a toast); a worker from before restarts shows `chi sessions stop ID` instead. The info bar reads `continue → · archive · stop · delete` (`unarchive` on an archived one; no `continue →` on a link continued already, see *Session chains*); archiving the open session stays on it, with an Undo toast.
|
|
29
29
|
|
|
30
30
|
**Lazy sweep:** automatic prune runs only while `chi web` runs, at most once per `session.sweep_interval_hours` (default 24): the web's session hub gives it a chance on its periodic full scan, and `GET /api/sessions` does too. No cron, and a terminal-only chi never sweeps. Manual prune is always available.
|
|
31
31
|
|
|
@@ -56,18 +56,18 @@ chi sessions clean --dry-run --days 7 # test sessions older than 7 da
|
|
|
56
56
|
|
|
57
57
|
`--dry-run` is the safe preview. `--keep N` keeps the N newest sessions that stay: one that goes anyway (a leftover scratch session, one left empty, one older than `--days`) takes no place in the count. Web has no prune endpoint; use the CLI.
|
|
58
58
|
|
|
59
|
-
`chi sessions list` shows each session's saved recap, its first sentence without the "The user was…" opening (as on the web cards), in place of its last prompt, cut to 60 characters; a session with no recap shows its last prompt. A session whose last turn a hook stopped carries `[looped]` when the hook was loop-guard (the thinking watch's cut, or its stop) and `[stopped by <hook>]` for any other hook, after `[archived]` (`--format json` has it as `stopped_by`; from `Session#last_turn`, so it stays until a later turn ends differently). The web's session cards wear the same marker as a badge: `looped` or `stopped by <hook>` after the status dot (a session that also waits on you shows only its waiting badge: the stop is behind the need), and the all-view's search matches its words too. A delegated session's row ends with `↳ <parent's short id>` (plain and `--live`; `--format json` has `parent_id`); rows keep their `updated_at` order, so a parent may be on another page than its children. After the status, `ctx 12%` says how full the context was after the session's last counted turn (from its `analytics.json`; blank when no turn had a server count or the window is unknown; `ctx_pct` in `--format json`). The fill is counted against the session's LLM context budget when one is set and smaller than the window (its own `--llm-context-budget`, else the model's/host's/config's; see [configuration.md](configuration.md#llm-context-a-sessions-own-strategy)), as the live meter counts it; the list, the web card and the session page otherwise agree. The web's session cards show it quietly too (`12%`), and a session's ctx meter shows it on load, before its next turn streams.
|
|
59
|
+
`chi sessions list` shows each session's saved recap, its first sentence without the "The user was…" opening (as on the web cards), in place of its last prompt, cut to 60 characters; a session with no recap shows its last prompt. A session whose last turn a hook stopped carries `[looped]` when the hook was loop-guard (the thinking watch's cut, or its stop) and `[stopped by <hook>]` for any other hook, after `[archived]` (`--format json` has it as `stopped_by`; from `Session#last_turn`, so it stays until a later turn ends differently). The web's session cards wear the same marker as a badge: `looped` or `stopped by <hook>` after the status dot (a session that also waits on you shows only its waiting badge: the stop is behind the need), and the all-view's search matches its words too. A delegated session's row ends with `↳ <parent's short id>` (plain and `--live`; `--format json` has `parent_id`), and a chain's link with `↪ <the previous link's short id>` (`--format json`: `continues`); rows keep their `updated_at` order, so a parent may be on another page than its children. After the status, `ctx 12%` says how full the context was after the session's last counted turn (from its `analytics.json`; blank when no turn had a server count or the window is unknown; `ctx_pct` in `--format json`). The fill is counted against the session's LLM context budget when one is set and smaller than the window (its own `--llm-context-budget`, else the model's/host's/config's; see [configuration.md](configuration.md#llm-context-a-sessions-own-strategy)), as the live meter counts it; the list, the web card and the session page otherwise agree. The web's session cards show it quietly too (`12%`), and a session's ctx meter shows it on load, before its next turn streams.
|
|
60
60
|
|
|
61
61
|
**Projects:** a session belongs to the git project it was started in: the repository, whichever worktree or subfolder of it (the same project root memories use). `chi sessions list` (with `--live` and `--format` too) and `chi web` show the current folder's project; `--scope=all` shows every session, and so does a folder in no repo (`~`). `--cwd PATH` is a folder filter instead of the project. The project is stored with the session (`project_root` in its JSON, `project` in `--format json`), so a session keeps it after its worktree is deleted; sessions older than that are placed by their folder, and one whose folder is gone shows in `--scope=all` only. Retention, `--resume`/`--attach ID`, `chi send`/`chi note ID` and `chi note --all` (every live session) are not scoped. The agent's `list_sessions` lists its own project's sessions; `cwd: "/"` lists every one.
|
|
62
62
|
|
|
63
|
-
`--live`, `--cwd` and `--format` make `list` a picker for scripts (`SessionManager.session_summaries`): `--live` keeps the sessions a worker runs now (the owner lock, not the saved status; a session open in a plain REPL is left out), `--cwd PATH` those in PATH or below, and test runs are left out unless the list is itself run as one (`SAMAGOTCHI_ENV=test`, `RACK_ENV=test` or `CI` set: then they show, marked `[test]`; `chi note --all` likewise). `--live` shows 10 unless `--limit` says otherwise; filters apply before the limit. `--format json` prints `[{id, short_id, desc, cwd, project, updated_at, live, busy, owner, recap, parent_id, delegate, archived, scratch, ctx_pct}]` (`owner`: `"worker"`, `"tui"` for a plain REPL, which takes no notes or messages, or null; `recap`: the first sentence of the session's recap, or null; `delegate`: true for a child the `delegate` tool started, false for a fork, which has a `parent_id` too), `--format tsv` one `id<TAB>desc` line per session, where `desc` is `<folder> · <last prompt>` cut to 60 characters (the text form of `--live`/`--cwd` shows `<folder> · <recap>` when there is one; tsv and json keep `desc`).
|
|
63
|
+
`--live`, `--cwd` and `--format` make `list` a picker for scripts (`SessionManager.session_summaries`): `--live` keeps the sessions a worker runs now (the owner lock, not the saved status; a session open in a plain REPL is left out), `--cwd PATH` those in PATH or below, and test runs are left out unless the list is itself run as one (`SAMAGOTCHI_ENV=test`, `RACK_ENV=test` or `CI` set: then they show, marked `[test]`; `chi note --all` likewise). `--live` shows 10 unless `--limit` says otherwise; filters apply before the limit. `--format json` prints `[{id, short_id, desc, cwd, project, updated_at, live, busy, owner, recap, parent_id, continues, delegate, archived, scratch, ctx_pct}]` (`owner`: `"worker"`, `"tui"` for a plain REPL, which takes no notes or messages, or null; `recap`: the first sentence of the session's recap, or null; `delegate`: true for a child the `delegate` tool started, false for a fork, which has a `parent_id` too), `--format tsv` one `id<TAB>desc` line per session, where `desc` is `<folder> · <last prompt>` cut to 60 characters (the text form of `--live`/`--cwd` shows `<folder> · <recap>` when there is one; tsv and json keep `desc`).
|
|
64
64
|
|
|
65
65
|
**Ordering:**
|
|
66
66
|
|
|
67
67
|
- `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.
|
|
68
68
|
- 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; in a window under 900 px wide, where the strip scrolls sideways, `▴ hide` sits in the top bar where the pill comes back; 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). A parent and its delegates are one card there: its `▸ 2 delegates · 1 live · 1 waiting` chip (the waiting part in the attention colour, the card's border too) opens their rows in a popover under it (a row per delegate at any depth, indented; a click opens that one, Esc or a click outside closes it, and the strip's order holds while it is open). A fork is a card of its own, and so is a delegate whose parent isn't listed (archived, another project), with its `↳ <parent>` chip; the pill counts cards, and every live or waiting session. "All sessions" (or `/`) opens every session at `#/sessions`, with a search over preview, id and status (Esc or Back returns). There a parent's card holds its delegates the same way, the chip opening their rows inside the card (each with an archive button on hover), open by itself while one of them waits on you; the count says `14 (+7 delegates)`, a search that matches a delegate shows its parent's card with the list open and the matching rows marked, and select mode picks cards (archiving a parent takes its delegates). 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. A session whose prompt carried model notes has a `notes: deepseek` chip there (the names without `model_notes_`; the tooltip lists each with its scope and size; `session.prompt_notes` in `GET /api/sessions/:id`, the live worker's, else the session file's; see [Memory](memory.md#model-notes)). Its `llm ctx` chip names the LLM context strategy the next turn runs under (`llm ctx stale,forget · session`, in the accent colour when the session set its own; the tooltip gives each value's source), and its popover sets the session's own strategy, apply rule and budget, or follows the model again, by running `/llm-context` in the worker (the reply shows as a command bubble); `GET /api/sessions/:id`'s `session.llm_context` carries the values (the live worker's, else resolved from the session file), and a `command_ran` that changed them carries the new ones. It shows the context used (`ctx 42%`) and the last generation's decode speed (`87 tok/s`, `~64 tok/s` when estimated; see `/stats` in [cli.md](cli.md#session-commands)); hovering `ctx` there or on a session card lists the session's tokens in (summed over every request, with the cached share), out (with reasoning) and the cost when the provider reports one, for this session only (one estimated from a `hosts.<name>.models` price shows with `~`: `cost: ~$0.54 ($0.42 reported, ~$0.12 estimated)`), and the memory indexes its prompt holds (`memory index: ~3.4k tokens in this session's prompt (system 2.0k, project 1.4k)`; see [Memory](memory.md#the-indexs-size)). 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 (an id the host declares under `hosts.<name>.models` first, noted `config`, its tooltip `served by config (hosts.<name>.models)`; a host that failed to list its models keeps those ids, greyed and noted `host down`, still pickable; a remembered or default model stays chosen even then, greyed on the button); 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, configured?, unavailable?, sampling?, llm_context?}], warning?}`; `configured: true` marks an id declared under `hosts.<name>.models`, listed first under its host, `unavailable: true` one whose host failed to list its models; `sampling` is the model's configured request parameters as `/model` words them, the row's tooltip in the list; 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). Beside it, an `llm ctx` chip shows the LLM context the picked model starts under (each `GET /api/models` row's `llm_context`, as the info bar's); its form sets one for the new chat only, sent as `POST /api/sessions`'s `llm_context` and never remembered (see [configuration.md](configuration.md#llm-context-a-sessions-own-strategy)). 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 model picked; 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 one-word `/word` no command answers (`/modle`) is not sent: the page shows `Unknown command /modle. Did you mean /model? /help lists the commands.` (the "Did you mean" part only when a name is close). 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).
|
|
69
69
|
- 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).
|
|
70
|
-
- 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; `families()` groups delegates under their parent), `families_view.js` (a parent's family chip, its rows and the strip's popover), `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`).
|
|
70
|
+
- 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; `families()` groups delegates under their parent), `families_view.js` (a parent's family chip, its rows and the strip's popover), `chains.js` (session chains: the links from `continues`, the cards folded into the latest link's, its `day N` chip and rows, the info bar's ← → links, a refused continue), `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`).
|
|
71
71
|
- 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).
|
|
72
72
|
|
|
73
73
|
**Test-session hygiene:**
|
|
@@ -173,6 +173,7 @@ git diff | chi send --new -m "review this" # stdin is quoted context
|
|
|
173
173
|
chi send --new --wait -m "review the diff on feat/x" # blocks; stdout is the answer
|
|
174
174
|
chi send --wait -m "and the tests?" 3fa2 # a follow-up in the same session, waits too
|
|
175
175
|
chi send --wait 3fa2 # sends nothing: waits for its next reply
|
|
176
|
+
chi send --new --continues last:3fa2 -m "new day" # the next link of 3fa2's chain; the previous one is archived
|
|
176
177
|
```
|
|
177
178
|
|
|
178
179
|
- `--new` takes no ids (one new session per call). `--dir DIR` is its folder, and so its project (default the current one); `--model M` its model (default the configured one). A host config.yml doesn't have is refused before anything is spawned (exit 1). A `host:id` whose id that host's last saved list doesn't have (`~/.local/state/samagotchi/model_lists.json`, what `chi models` / `/models` / the web last listed for that host, at most a week old) gets one warning line on stderr, with up to three close ids as a hint (`did you mean: gemma-small?`), and the session starts anyway: some hosts serve ids they don't list (a one-model llama.cpp server takes any name). A saved list older than 10 minutes that misses the id is re-listed once (that one host, capped at 5 s) before the warning: the list can be stale by content, e.g. a one-model server reloaded with another model; a list re-listed that way names the typed id, nothing is said. A list younger than 10 minutes is taken at its word (a host that serves ids it never lists would otherwise pay the re-list on every launch). A re-list that fails, times out or lists nothing falls back to the saved list. An id its host declares under `hosts.<name>.models` (see [configuration](configuration.md#models-a-host-serves-but-doesnt-list)) is never warned about or re-listed. A bare id (no `host:`) isn't checked that way — routing sends it to whichever host lists or declares it — and with no saved list for the host nothing is said; a wrong id then fails the worker's first turn.
|
|
@@ -181,9 +182,26 @@ chi send --wait 3fa2 # sends nothing: waits fo
|
|
|
181
182
|
- Exit codes with `--wait`: 0 answered; 3 the turn waits for an answer from you (a question, a hook's question, a guardrail approval, or the step-limit question: see [chi as a sub-agent](sub-agent.md#the-step-limit)), and the session keeps waiting: stderr gets the whole question (`waiting for an answer (approval): …`, its text, the numbered options, whether free text is allowed) and how to answer it (`chi answer ID --question QID --option N`, or `chi --attach ID`, the web); 4 `--timeout S` passed with the turn still running (it goes on; wait again); 1 the turn ended without a reply (canceled, failed or empty), the worker failed or vanished, or the session was stopped; 130 on Ctrl-C, which leaves the turn running (`still running: chi --attach ID`). There is no default timeout.
|
|
182
183
|
- `--format json` (with `--wait`) prints one JSON object on stdout however the wait ends, for a script or a parent agent (see [chi as a sub-agent](sub-agent.md)): `{"status":"answered","session_id":…,"text":…}`; `{"status":"question",…,"question":{"id","kind","header","text","options","multi_select","allow_freeform","approval"},"answer_with":"chi answer ID --question QID --option N"}`, where `kind` is `question` (the model's), `hook` or `approval` (with the tool, command, cwd, rule, reason and offered scopes); `{"status":"running",…,"detail":…}` (exit 4, and on Ctrl-C); and `failed`, `canceled`, `no_answer`, `error`, `worker_gone` or `stopped` with a `detail` line (exit 1); `failed` adds `error_kind` and `retryable`, `canceled` adds `cancel_reason` and `stopped_by`, when known. A failure before the wait (no such session, a busy one) is `error` too. A usage error (exit 2) prints no JSON.
|
|
183
184
|
- `--wait ID` with no message (no `-m`, nothing piped) sends nothing: it waits for the session's next reply and prints and exits as above. A running session is fine (that is the point), so after exit 3, 4 or 130 an agent waits again this way while the user answers in the web or `chi --attach`; the question pending when it starts doesn't count again. An idle session with no worker waits until something wakes one. With `--new`, or two ids, it is a usage error (exit 2).
|
|
185
|
+
- `--continues ID` (with `--new`) starts the next link of that session's chain instead (see *Session chains*): in its folder, on its model and LLM context, so `--dir`, `--model` and `--llm-context*` are refused beside it; `ID` may be a prefix or `last:ID`, the chain's latest link. Without a message the new link starts idle; `--wait` then has nothing to wait for (a usage error). It prints `<full id> started (continues 3fa2c1d0)`; a refusal (continued already, naming the next link; a delegate still open; a turn running) is `chi send: refused: …`, exit 1.
|
|
184
186
|
- The 16 KiB cap applies: `git diff | chi send --new …` on a big diff is refused; name the branch in the message instead and let the session read it.
|
|
185
187
|
- A session nobody attaches to stalls at its first question or guardrail ask until someone answers it (`chi answer`, the web, `chi --attach`), and its worker stays up meanwhile: it doesn't idle-exit while a question waits (`chi sessions list` shows it as `waiting`; `chi sessions stop ID` frees it). Otherwise it idle-exits after `session.idle_exit_minutes` like any worker. These are ordinary sessions: delete them like any other.
|
|
186
188
|
|
|
189
|
+
## Session chains
|
|
190
|
+
|
|
191
|
+
A session can **continue** an earlier one: "the next day of the same routine" (a daily coordinator, say). The new session's `continues` field names the previous link; the chain is derived from it, never stored: follow `continues` back for the earlier links, forward for the later ones. It is not a parent: archiving one link leaves the next alone, and lists fold delegates, not links. A link is continued at most once, so a chain has no forks and its latest link is the one nobody continues.
|
|
192
|
+
|
|
193
|
+
`SessionManager.continue_session` makes the next link:
|
|
194
|
+
|
|
195
|
+
- **What it copies:** the previous link's folder, its model (the name it was typed as, so an alias holds) and its own LLM context values. Its preview is the previous link's (until its first message is saved), unless it starts with a message.
|
|
196
|
+
- **The carry note:** the new link starts with a context note from `session chain` (shown in its transcript as `note from session chain`), queued before its worker spawns, so its first turn sees it: `This session continues 3fa2c1d0 (2026-10-08), the previous link of its chain. Its recap, as of 2026-10-08 18:02:` and the previous link's recap (`recap.json`; cut at 8000 characters). No recap (fewer user turns than a recap needs, recaps off, none written in time): `… It left no recap.` Never more than the recap: no transcript. An idle new link holding only the note is kept, not discarded as empty.
|
|
197
|
+
- **The recap first:** a previous link with a live worker is asked for a recap (the Bridge's `POST /recap`) and given up to 30 s to write it; one with none wrote its recap when its worker left.
|
|
198
|
+
- **The previous link is archived** before the new one starts, by the archive's own rules (see *Archiving a session*): a turn running, a prompt queued or the step-limit question refuse it, a live idle worker is stopped, a plain REPL's session is refused. Its delegates are archived with it, as any archive does; so a continue is **refused while a delegate is still open** (at any depth, checked again after the recap wait): running, waiting for an answer, with a live worker, or with a reply its parent wasn't given (`ContinueRefused`, naming them). Wait for them, stop or archive them, then continue.
|
|
199
|
+
- **Refused too:** a link that is continued already (naming the link that continues it; continue that one, or `last:<id>`), a previous link whose folder is gone, and one whose model names a host config.yml no longer has. A refusal starts nothing and archives nothing; a new link that fails to start puts back what was archived. Every refusal that can be known up front (the archive's ones included) comes before the recap wait. One continue of a session runs at a time (a lock in its folder; other chains don't wait), so a double click finds the first one's link and is refused, while a `last:<id>` continue that waited follows the chain to its new end.
|
|
200
|
+
- **Web:** `POST /api/sessions` takes `continues: "<id, prefix or last:id>"`, with `idle: true` (a new link waiting for its first message, previewed by the previous link's, or `preview`) or a `prompt`, and not `dir`, `model` or `llm_context` (400 `invalid_continues`: the previous link's are taken). 201 is the new session's summary, `continues` set; 404 `not_found`; 400 `ambiguous_id` for a prefix of several sessions, `invalid_model` for a model whose host is gone; 409 `continued` with `next_id` (the link that continues it already, which the page opens instead), `open_children` with `ids`, `folder_gone`, `busy`, `scratch` or `owned_by_tui`. Every session summary carries `continues`.
|
|
201
|
+
- **The page:** the info bar's `continue →` continues the open session (`continues: <id>, idle: true`; it reads `continuing…` through the recap wait) and opens the new link with `Continue where we left off.` in the composer, not sent (the same line for every chain; a draft already there stays instead); the previous link is archived, a toast says so. A link continued already has no `continue →`: its `→` link goes to the next link (a stale page's continue answered 409 `continued` opens `next_id`). A continue refused for open delegates shows a toast that names each one (its preview and why the server says it is open: `running`, `waiting`, `live`, `unreported reply`), each a link to its page; any other refusal shows the server's words. The info bar of a link carries `← 2026-10-08` (the previous link's day) and, on an earlier link, `2026-10-10 →`, each a link with its preview and id in the tooltip. A link whose title is the opener and nothing else (its first message once sent) shows its chain's first link's title instead, on its card, in the popover and in its info bar's title line, display only (its messages and data keep the opener; its own title when the first link is gone). The lists fold a chain into the card of its latest listed link, whatever the earlier links' archived state (typing in an archived link brings it back, folded there still), their delegates with them: the card's `↩ day N` chip (`day N of M` when later links aren't listed) opens a popover of the other links, newest first (`day N`, its date, its recap's first sentence or its preview, `archived`), a click opens one; the chip shows a live dot or `waiting` for a link that has a worker or waits on the user, and a waiting link puts its chain's card first. All sessions folds nothing while a search is typed or "include archived" is on: every link is a card of its own then, its chip `day N of M`, with its own delegates folded in, so a search finds an earlier link and its delegates.
|
|
202
|
+
- **CLI:** `chi send --new --continues (ID|PREFIX|last:ID) [-m TEXT]` (see *Starting a session*).
|
|
203
|
+
- **Which link:** an id, a unique prefix, or `last:<id or prefix>`, the latest link of the chain that session is in. It walks the links that still exist: a deleted link in the middle (`chi sessions delete`, the retention sweep of an unarchived one) ends the walk before it, so `last:` of an earlier link then names that earlier link, and continuing it starts a second line beside the links after the gap; name a link after the gap instead. A trigger names the chain this way: `chi send` to an archived link would bring it back (a human's input), not continue the chain.
|
|
204
|
+
|
|
187
205
|
## Delegating
|
|
188
206
|
|
|
189
207
|
The `delegate` tool hands a task to a **child session**: an ordinary chi session in a worker of its own, started in the parent's `working_directory` with the task, verbatim, as its first user message, on the parent's model unless the call names one (`model:` takes a name or an alias, resolved as `--model` is; an unknown host comes back as the tool's `Error:` line; an id the host's saved model list doesn't have starts the child with a `Warning: host 'h' doesn't list model 'x' (did you mean: …?)` line in the result). Children run in parallel and only their **final reply** comes back to the parent: the child's newest `output/<timestamp>.txt` file, which the worker writes at the end of each turn that produced visible text, cut head-and-tail like an `execute` result. Nothing of the child's trace enters the parent's context. Nothing is hidden: a child shows in `chi sessions list` (`↳ <parent>`), the web (folded into its parent's card on the strip and in all sessions, whose `▸ N delegates` chip lists the delegates at any depth, see Web UI above; a card of its own with a `↳` chip when its parent isn't listed; `delegated by` in its info bar; the parent's info bar carries a `⑂ N` chip counting its delegates, with how many run, wait, are done, failed, stopped or idle in the tooltip and the waiting ones named in the attention colour; a click opens all sessions), the terminal's status row (a parent's `⑂ 2 running · 1 waiting`, see [Status Line](cli.md#status-line)) and the `list_sessions` tool (`child`; the parent shows as `parent` in the child's own list); the user can `chi --attach <child id>` and steer it mid-run, and `chi send` and `send_note` reach it like any session.
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: coordinator
|
|
3
|
-
version: 0.
|
|
3
|
+
version: 0.3.0
|
|
4
4
|
scope: system
|
|
5
|
-
description: "Coordinate parallel work: the coordinator skill (split the work, one worktree and child session per task, check each report, ask before each merge, a handoff memory to resume from), /coordinate <goal>, /coordinate resume and /children"
|
|
5
|
+
description: "Coordinate parallel work: the coordinator skill (split the work, one worktree and child session per task, check each report, ask before each merge, a handoff memory to resume from), /coordinate <goal>, /coordinate resume, /coordinate end and /children"
|
|
6
6
|
trust_level: reviewed
|
|
7
7
|
files:
|
|
8
8
|
skill_coordinator.md:
|
|
9
|
-
sha256: sha256:
|
|
9
|
+
sha256: sha256:2ef3faefa89cbb23ce89c76bd5d913e29dde9817edf08cd29a063aeafa698643
|
|
10
10
|
description: "Coordinate parallel work: split it, delegate each task wait:false in its own worktree (cwd:), check each report, ask the user before each merge; resume from a handoff_* memory"
|
|
11
11
|
plugin:
|
|
12
12
|
file: plugin.rb
|
|
13
|
-
sha256: sha256:
|
|
13
|
+
sha256: sha256:cc2986da0e734d2a5d6da2def682cc558c63c16e47b3605d55c597414027497d
|
|
14
14
|
# the handoff memory needs memory_write's description-only form and remove: (0.36.0)
|
|
15
15
|
requires_chi: ">= 0.36.0"
|
|
@@ -25,6 +25,10 @@
|
|
|
25
25
|
# (/coordinate resume <name>); none: says so. It reads the project's
|
|
26
26
|
# memories folder, nothing else.
|
|
27
27
|
#
|
|
28
|
+
# /coordinate end ends the day: the model is asked to follow the skill's End
|
|
29
|
+
# of day (bring its handoff up to date, keep it OPEN unless all is done, give
|
|
30
|
+
# a short report). It reads nothing itself; "end" with more words is a goal.
|
|
31
|
+
#
|
|
28
32
|
# A child is asked before it changes anything outside its worktree by chi
|
|
29
33
|
# itself (the core child-boundary guardrail), in every mode: nothing to set up.
|
|
30
34
|
class Plugin
|
|
@@ -35,7 +39,7 @@ class Plugin
|
|
|
35
39
|
TEXT_CHARS = 80
|
|
36
40
|
USAGE = "usage: /children [all] | /children stop <id>"
|
|
37
41
|
COORDINATE_USAGE = "usage: /coordinate <goal> — chi splits it into tasks, one child session and worktree each; " \
|
|
38
|
-
"/coordinate resume [name] — pick up an open handoff"
|
|
42
|
+
"/coordinate resume [name] — pick up an open handoff; /coordinate end — save the handoff, end-of-day report"
|
|
39
43
|
RESUME_CARD_ID = "coordinate-resume"
|
|
40
44
|
# A card takes 6 actions.
|
|
41
45
|
MAX_RESUMES = 6
|
|
@@ -49,7 +53,7 @@ class Plugin
|
|
|
49
53
|
children_command(args.to_s.strip, ctx)
|
|
50
54
|
end
|
|
51
55
|
chi.command "/coordinate", "run work in parallel: chi splits <goal> into tasks, each in a child session and worktree; " \
|
|
52
|
-
"resume: pick up an open handoff",
|
|
56
|
+
"resume: pick up an open handoff; end: save the handoff, end-of-day report",
|
|
53
57
|
anytime: true do |args, ctx|
|
|
54
58
|
coordinate(args.to_s.strip, ctx)
|
|
55
59
|
end
|
|
@@ -113,6 +117,8 @@ class Plugin
|
|
|
113
117
|
verb, rest = goal.split(/\s+/, 2)
|
|
114
118
|
# "resume" and at most a name; "resume the old work" is a goal.
|
|
115
119
|
return resume(rest.to_s.strip, ctx) if verb == "resume" && !rest.to_s.strip.match?(/\s/)
|
|
120
|
+
# A bare "end" only; "end the flaky specs" is a goal.
|
|
121
|
+
return end_of_day(ctx) if verb == "end" && rest.to_s.strip.empty?
|
|
116
122
|
|
|
117
123
|
ask(ctx, "Read the skill_coordinator memory and follow it for this goal:\n\n#{goal}",
|
|
118
124
|
"asked chi to coordinate it; a running turn gets it at its next step")
|
|
@@ -137,6 +143,13 @@ class Plugin
|
|
|
137
143
|
"asked chi to resume #{name}; a running turn gets it at its next step")
|
|
138
144
|
end
|
|
139
145
|
|
|
146
|
+
# /coordinate end
|
|
147
|
+
def end_of_day(ctx)
|
|
148
|
+
ask(ctx, "Read the skill_coordinator memory and follow its End of day now: bring your handoff up to date and " \
|
|
149
|
+
"give me the end-of-day report.",
|
|
150
|
+
"asked chi to end the day; a running turn gets it at its next step")
|
|
151
|
+
end
|
|
152
|
+
|
|
140
153
|
def resume_card(open, ctx)
|
|
141
154
|
body = open.map { |handoff| "- #{handoff[:name]}#{" — #{handoff[:description]}" unless handoff[:description].empty?}" }
|
|
142
155
|
actions = open.first(MAX_RESUMES).map do |handoff|
|
|
@@ -88,6 +88,25 @@ If the request is a new goal, say which handoffs are open (one line each) and go
|
|
|
88
88
|
description to "DONE: …" and ask the user whether to remove it (memory_write name, scope and
|
|
89
89
|
remove: true).
|
|
90
90
|
|
|
91
|
+
## End of day
|
|
92
|
+
When the user ends the day (`/coordinate end`, "that's all for today"), before you answer:
|
|
93
|
+
1. Check where things stand: for each branch in your handoff `git log --oneline <default>..<branch>` and
|
|
94
|
+
`git merge-base --is-ancestor <branch> <default>` (merged only if that exits 0; a commit listed by the
|
|
95
|
+
log means not merged), and the list_sessions tool for its child.
|
|
96
|
+
2. Bring your handoff (the one whose description names your session) up to date: per branch its state
|
|
97
|
+
now (merged, ready to merge, waiting for a report, waiting for the user), the decisions and verdicts,
|
|
98
|
+
the follow-ups found but not fixed, and each child still open (not stopped, its branch not merged or
|
|
99
|
+
dropped): its session id, branch and worktree path, so the next session can pick them up. If you
|
|
100
|
+
own none and work is still open (a child running, a branch you made not merged), create one (Handoff
|
|
101
|
+
memory above); if nothing is open, skip to the report. Never write another epic's handoff. Write it
|
|
102
|
+
now (edit, then memory_write description only), before the report: a report is no update.
|
|
103
|
+
3. Set its description to the status: still "OPEN …" unless every task is done (then follow step 8
|
|
104
|
+
instead: the DONE status is set there).
|
|
105
|
+
4. Give the user a short end-of-day report: per branch its state and who it waits on, the children still
|
|
106
|
+
running, the follow-ups, and last "next time: `/coordinate resume <epic-slug>`" (when a handoff is
|
|
107
|
+
open). Report only what the commands showed.
|
|
108
|
+
Don't start, merge, stop or clean up anything here: a child still running stays running; say so.
|
|
109
|
+
|
|
91
110
|
## Gotchas
|
|
92
111
|
- Children are asked before changing anything outside their worktree (your checkout, a sibling's):
|
|
93
112
|
the user approves it on the child's card. Still check where commits landed (step 4).
|
|
@@ -106,3 +125,4 @@ If the request is a new goal, say which handoffs are open (one line each) and go
|
|
|
106
125
|
- 2026-10-06 clean up only merged or dropped branches; report only what commands showed (0.1.2)
|
|
107
126
|
- 2026-10-07 handoff memory with its status in the description, resume (step 0, /coordinate resume),
|
|
108
127
|
save decisions first, fast-forward and worktree-remove checks, remove the handoff when done (0.2.0)
|
|
128
|
+
- 2026-10-09 End of day (/coordinate end): handoff up to date, still OPEN, a short report (0.3.0)
|
|
@@ -4,6 +4,7 @@ require_relative "client_id"
|
|
|
4
4
|
require_relative "session"
|
|
5
5
|
require_relative "session_inbox"
|
|
6
6
|
require_relative "session_manager"
|
|
7
|
+
require_relative "session_chain"
|
|
7
8
|
require_relative "context_quote"
|
|
8
9
|
require_relative "reply_wait"
|
|
9
10
|
require_relative "parent_report"
|
|
@@ -34,6 +35,7 @@ module Samagotchi
|
|
|
34
35
|
USAGE = <<~TEXT
|
|
35
36
|
Usage: chi send [-m TEXT] [--image PATH]... (ID|PREFIX)...
|
|
36
37
|
chi send --new [--dir DIR] [--model M] [--llm-context LAYERS] [-m TEXT] [--image PATH]...
|
|
38
|
+
chi send --new --continues (ID|PREFIX|last:ID) [-m TEXT] [--image PATH]...
|
|
37
39
|
chi send --wait [--timeout S] [--format json] [-m TEXT] [--image PATH]... (--new | ID)
|
|
38
40
|
chi send --wait [--timeout S] [--format json] ID
|
|
39
41
|
Sends a message to each session, as if typed in it: a turn starts,
|
|
@@ -52,6 +54,14 @@ module Samagotchi
|
|
|
52
54
|
web does, and print its id
|
|
53
55
|
--dir DIR (--new) its folder, the project it belongs to; default
|
|
54
56
|
the current one
|
|
57
|
+
--continues ID
|
|
58
|
+
(--new) start the next link of that session's chain:
|
|
59
|
+
in its folder, on its model and LLM context, with a
|
|
60
|
+
note carrying its recap; it is archived. last:ID
|
|
61
|
+
names the chain's latest link. Without -m the new
|
|
62
|
+
session waits idle. Refused for a session continued
|
|
63
|
+
already (naming the next link) or with delegates
|
|
64
|
+
still open
|
|
55
65
|
--model M (--new) its model; default the configured one
|
|
56
66
|
--llm-context LAYERS, --llm-context-apply RULE, --llm-context-budget N
|
|
57
67
|
(--new) its own LLM context strategy (none, stale,forget),
|
|
@@ -83,6 +93,7 @@ module Samagotchi
|
|
|
83
93
|
f.value "-m", "--message"
|
|
84
94
|
f.switch "--new"
|
|
85
95
|
f.value "--dir"
|
|
96
|
+
f.value "--continues"
|
|
86
97
|
f.value "--model"
|
|
87
98
|
f.value "--llm-context", key: :llm_strategy
|
|
88
99
|
f.value "--llm-context-apply", key: :llm_apply
|
|
@@ -152,10 +163,14 @@ module Samagotchi
|
|
|
152
163
|
# Before the wait-only case, which would drop the images.
|
|
153
164
|
return usage_error("--image needs a message: pass -m TEXT or pipe it in") if prompt.nil? && !options[:images].empty?
|
|
154
165
|
return run_wait_only(options) if prompt.nil? && options[:wait] && !options[:new]
|
|
155
|
-
|
|
166
|
+
|
|
167
|
+
# The next link of a chain may start idle, waiting for its first message.
|
|
168
|
+
idle_link = prompt.nil? && options[:continues]
|
|
169
|
+
return usage_error("--wait needs a message to wait for: pass -m TEXT") if idle_link && options[:wait]
|
|
170
|
+
return usage_error("no message: pass -m TEXT or pipe it in") unless prompt || idle_link
|
|
156
171
|
|
|
157
172
|
begin
|
|
158
|
-
prompt
|
|
173
|
+
prompt &&= SessionInbox.checked_text(prompt, noun: "message")
|
|
159
174
|
rescue SessionInbox::NoteRejected => e
|
|
160
175
|
@stderr.puts("chi send: #{e.message}")
|
|
161
176
|
return 1
|
|
@@ -205,7 +220,7 @@ module Samagotchi
|
|
|
205
220
|
return usage_error("at most #{MAX_IMAGES} images") if options[:images].size > MAX_IMAGES
|
|
206
221
|
return new_options(options) if options[:new]
|
|
207
222
|
|
|
208
|
-
%i[dir model].each { |key| return usage_error("--#{key} needs --new") if options[key] }
|
|
223
|
+
%i[dir model continues].each { |key| return usage_error("--#{key} needs --new") if options[key] }
|
|
209
224
|
LLM_CONTEXT_FLAGS.each { |key, flag| return usage_error("#{flag} needs --new") if options[key] }
|
|
210
225
|
return usage_error("give session ids") if options[:ids].empty?
|
|
211
226
|
|
|
@@ -220,6 +235,15 @@ module Samagotchi
|
|
|
220
235
|
def new_options(options)
|
|
221
236
|
return usage_error("--new takes no session ids: it starts one session") unless options[:ids].empty?
|
|
222
237
|
|
|
238
|
+
if options[:continues]
|
|
239
|
+
taken = [("--dir" if options[:dir]), ("--model" if options[:model])] +
|
|
240
|
+
LLM_CONTEXT_FLAGS.filter_map { |key, flag| flag if options[key] }
|
|
241
|
+
unless taken.compact.empty?
|
|
242
|
+
return usage_error("--continues takes the previous session's folder, model and LLM context; " \
|
|
243
|
+
"leave out #{taken.compact.join(", ")}")
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
|
|
223
247
|
words = LLM_CONTEXT_FIELDS.filter_map { |key, field| [field, options[key]] if options[key] }.to_h
|
|
224
248
|
begin
|
|
225
249
|
options[:llm_context] = LLMContextOverride.update(nil, words) unless words.empty?
|
|
@@ -253,7 +277,10 @@ module Samagotchi
|
|
|
253
277
|
# @return [Integer] the exit status
|
|
254
278
|
def run_new(prompt, options)
|
|
255
279
|
images = !@images.empty?
|
|
256
|
-
|
|
280
|
+
model, typed = continued_model(options)
|
|
281
|
+
return 1 if model == false
|
|
282
|
+
|
|
283
|
+
if images && (refusal = vision_refusal(model, typed: typed))
|
|
257
284
|
error_line("chi send: refused: #{refusal}")
|
|
258
285
|
return 1
|
|
259
286
|
end
|
|
@@ -264,9 +291,16 @@ module Samagotchi
|
|
|
264
291
|
through_worker = images || command_like?(prompt)
|
|
265
292
|
begin
|
|
266
293
|
start = through_worker ? { prompt: nil, title: prompt } : { prompt: prompt }
|
|
267
|
-
session =
|
|
268
|
-
|
|
269
|
-
|
|
294
|
+
session = if options[:continues]
|
|
295
|
+
SessionManager.continue_session(options[:continues], **start, state_dir: @state_dir)
|
|
296
|
+
else
|
|
297
|
+
SessionManager.spawn_session(**start, working_directory: options[:dir],
|
|
298
|
+
model_name: options[:model], llm_context: options[:llm_context],
|
|
299
|
+
state_dir: @state_dir)
|
|
300
|
+
end
|
|
301
|
+
rescue SessionManager::ContinueRefused, SessionManager::ArchiveRefused, SessionManager::OwnedByTUI => e
|
|
302
|
+
error_line("chi send: refused: #{e.message}")
|
|
303
|
+
return 1
|
|
270
304
|
rescue StandardError => e
|
|
271
305
|
error_line("chi send: could not start a session: #{e.message}")
|
|
272
306
|
return 1
|
|
@@ -275,7 +309,7 @@ module Samagotchi
|
|
|
275
309
|
if through_worker
|
|
276
310
|
return 1 unless deliver_new(session, prompt)
|
|
277
311
|
else
|
|
278
|
-
@info.puts("#{session.id} started")
|
|
312
|
+
@info.puts("#{session.id} started#{continued_words(session)}")
|
|
279
313
|
end
|
|
280
314
|
@session_id = session.id
|
|
281
315
|
return 0 unless options[:wait]
|
|
@@ -285,6 +319,22 @@ module Samagotchi
|
|
|
285
319
|
timeout: options[:timeout])
|
|
286
320
|
end
|
|
287
321
|
|
|
322
|
+
# --continues: the model the new link will run (the previous link's),
|
|
323
|
+
# for the images check; [nil, nil] without --continues.
|
|
324
|
+
# @return [Array, false] [model_name, model_typed], or false after the
|
|
325
|
+
# error line (no such session)
|
|
326
|
+
def continued_model(options)
|
|
327
|
+
return [options[:model], nil] unless options[:continues]
|
|
328
|
+
|
|
329
|
+
previous = Session.load(SessionChain.resolve(options[:continues], state_dir: @state_dir), state_dir: @state_dir)
|
|
330
|
+
[previous.model_name, previous.model_typed]
|
|
331
|
+
rescue ArgumentError => e
|
|
332
|
+
error_line("chi send: #{e.message}")
|
|
333
|
+
false
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
def continued_words(session) = session.continues ? " (continues #{session.continues[0, 8]})" : ""
|
|
337
|
+
|
|
288
338
|
# One existing session, then its next reply. The cursor and baseline
|
|
289
339
|
# are taken before the message goes in, so neither an older reply nor a
|
|
290
340
|
# turn that ends before the first look is mistaken for the answer. A
|
|
@@ -371,7 +421,7 @@ module Samagotchi
|
|
|
371
421
|
end
|
|
372
422
|
|
|
373
423
|
@sent_command = command_ack?(result[:ack])
|
|
374
|
-
@info.puts("#{session.id} started#{with_images}#{"; #{SENT_COMMAND}" if @sent_command}")
|
|
424
|
+
@info.puts("#{session.id} started#{continued_words(session)}#{with_images}#{"; #{SENT_COMMAND}" if @sent_command}")
|
|
375
425
|
true
|
|
376
426
|
end
|
|
377
427
|
|
data/lib/samagotchi/session.rb
CHANGED
|
@@ -72,6 +72,11 @@ module Samagotchi
|
|
|
72
72
|
# Started by the `delegate` tool (a fork has a parent_id too, but isn't
|
|
73
73
|
# one): see #delegate?.
|
|
74
74
|
attr_writer :delegate
|
|
75
|
+
# The session this one continues (SessionManager.continue_session): the
|
|
76
|
+
# previous link of a chain, "the next day of the same routine", else
|
|
77
|
+
# nil. Not a parent: archiving one link leaves the next alone. The chain
|
|
78
|
+
# itself is derived (SessionChain), never stored.
|
|
79
|
+
attr_accessor :continues
|
|
75
80
|
# A `chi scratch` session: deleted when its REPL ends, and by the next
|
|
76
81
|
# sweep (or `chi sessions clean`) when the process died first.
|
|
77
82
|
attr_accessor :scratch
|
|
@@ -130,7 +135,8 @@ module Samagotchi
|
|
|
130
135
|
first_preview: "", test_run: false, pending_question: nil,
|
|
131
136
|
used_memory_names: [], project_root: nil,
|
|
132
137
|
preloaded_memory_names: [], muted_memory_names: [], parent_id: nil, scratch: false,
|
|
133
|
-
last_turn: nil, model_typed: nil, delegate: false, llm_context: nil, prompt_notes: []
|
|
138
|
+
last_turn: nil, model_typed: nil, delegate: false, llm_context: nil, prompt_notes: [],
|
|
139
|
+
continues: nil)
|
|
134
140
|
@id = id
|
|
135
141
|
@metadata_version = metadata_version
|
|
136
142
|
@mode = mode
|
|
@@ -154,6 +160,7 @@ module Samagotchi
|
|
|
154
160
|
@scratch = !!scratch
|
|
155
161
|
@last_turn = last_turn
|
|
156
162
|
@delegate = !!delegate
|
|
163
|
+
@continues = continues&.to_s
|
|
157
164
|
self.llm_context = llm_context
|
|
158
165
|
@archived = false
|
|
159
166
|
end
|
|
@@ -205,7 +212,7 @@ module Samagotchi
|
|
|
205
212
|
# seed); [] by default
|
|
206
213
|
def self.new_session(mode:, model_name:, working_directory:, test_run: nil,
|
|
207
214
|
preloaded_memory_names: [], muted_memory_names: [], parent_id: nil, messages: [],
|
|
208
|
-
scratch: false, model_typed: nil, delegate: false, llm_context: nil)
|
|
215
|
+
scratch: false, model_typed: nil, delegate: false, llm_context: nil, continues: nil)
|
|
209
216
|
now = Time.now.iso8601(3)
|
|
210
217
|
resolved_test = if test_run.nil?
|
|
211
218
|
test_session_env?
|
|
@@ -230,7 +237,8 @@ module Samagotchi
|
|
|
230
237
|
parent_id: parent_id,
|
|
231
238
|
scratch: scratch,
|
|
232
239
|
delegate: delegate,
|
|
233
|
-
llm_context: llm_context
|
|
240
|
+
llm_context: llm_context,
|
|
241
|
+
continues: continues
|
|
234
242
|
)
|
|
235
243
|
end
|
|
236
244
|
|
|
@@ -287,7 +295,8 @@ module Samagotchi
|
|
|
287
295
|
"scratch" => false,
|
|
288
296
|
"last_turn" => nil,
|
|
289
297
|
"delegate" => false,
|
|
290
|
-
"llm_context" => nil
|
|
298
|
+
"llm_context" => nil,
|
|
299
|
+
"continues" => nil
|
|
291
300
|
}.freeze
|
|
292
301
|
|
|
293
302
|
# A session from a parsed session file. +messages+ false leaves the
|