samagotchi 0.95.0 → 0.97.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +39 -1
  3. data/bin/chi +3 -0
  4. data/docs/architecture.md +2 -0
  5. data/docs/bundle-authoring.md +2 -1
  6. data/docs/cli.md +13 -1
  7. data/docs/configuration.md +13 -0
  8. data/docs/internals/background-tasks.md +1 -1
  9. data/docs/internals/bundles.md +1 -1
  10. data/docs/internals/engine.md +3 -1
  11. data/docs/memory.md +43 -3
  12. data/docs/plugins.md +4 -3
  13. data/docs/sessions.md +8 -2
  14. data/lib/samagotchi/bundle_command/check.rb +5 -3
  15. data/lib/samagotchi/bundle_command/try/run.rb +19 -4
  16. data/lib/samagotchi/bundles/system/manifest.yml +1 -1
  17. data/lib/samagotchi/children_status.rb +1 -1
  18. data/lib/samagotchi/engine.rb +49 -38
  19. data/lib/samagotchi/llm/errors.rb +100 -2
  20. data/lib/samagotchi/memory_bundle/profile.rb +14 -4
  21. data/lib/samagotchi/memory_command.rb +237 -0
  22. data/lib/samagotchi/plugin/api.rb +7 -3
  23. data/lib/samagotchi/plugin/sessions.rb +4 -3
  24. data/lib/samagotchi/send_command.rb +12 -12
  25. data/lib/samagotchi/session.rb +13 -1
  26. data/lib/samagotchi/session_commands.rb +13 -0
  27. data/lib/samagotchi/session_continue.rb +8 -8
  28. data/lib/samagotchi/session_delete_command.rb +1 -1
  29. data/lib/samagotchi/session_inbox.rb +29 -0
  30. data/lib/samagotchi/session_manager.rb +24 -7
  31. data/lib/samagotchi/sessions_command.rb +8 -6
  32. data/lib/samagotchi/terminal_ui/attached_loop.rb +2 -0
  33. data/lib/samagotchi/terminal_ui/status_row.rb +24 -3
  34. data/lib/samagotchi/terminal_ui.rb +1 -0
  35. data/lib/samagotchi/title_command.rb +61 -0
  36. data/lib/samagotchi/tools/memory.rb +1 -1
  37. data/lib/samagotchi/turn_checkpoint.rb +6 -0
  38. data/lib/samagotchi/version.rb +1 -1
  39. data/lib/samagotchi/web/app.rb +4 -4
  40. data/lib/samagotchi/web/public/app.js +12 -0
  41. data/lib/samagotchi/web/public/chains.js +21 -8
  42. data/lib/samagotchi/web/public/format.js +4 -3
  43. data/lib/samagotchi/web/public/index.html +7 -0
  44. data/lib/samagotchi/web/public/info_bar_model.js +2 -0
  45. data/lib/samagotchi/web/public/session_rename.js +86 -0
  46. data/lib/samagotchi/web/public/session_state.js +4 -2
  47. data/lib/samagotchi/web/session_summary.rb +3 -0
  48. data/lib/samagotchi/worker.rb +24 -7
  49. data/lib/samagotchi/worker_idle_exit.rb +16 -8
  50. metadata +4 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a359a1fc0fd6f83c6676d21b651513596e6618652aa996087087fb4bdb432d03
4
- data.tar.gz: 96a3421ca2dee43940f7591e32dd76764090b6ae1791cd5e91c13e6be575339a
3
+ metadata.gz: 8bec4407abc70e688b17f913e574b7b7968a13941e7d2f0b2599e8ebdcdd5954
4
+ data.tar.gz: b1f1b5d30b31953901a94849b22da5deeb7ff97b7b57fe17bda2f3c8d40ec41b
5
5
  SHA512:
6
- metadata.gz: 57f03d06345f6e2cd7f74411a6eca333cb08352eb1a2e48a39dfafe5ce5b7977b88d8b7fa327056f5aa6fc448b10ac7030661d149d61ce0823d66a104d5212fd
7
- data.tar.gz: 2d0dae400a20ae0b023ccd613cb6dc871e34e86de7a6c3605d22eb21d5f1babbd3dc94d7f47a54ff31afcf36da4f71c9c667f80e5c6c90bb16e0a41c92892615
6
+ metadata.gz: 8d1d4b749231445533c0526e4fc87052294b70026ec9be742db89b474bb8bdb18079c95f67d77b8c7116f1d18d0c13be9b413eceea5e1f21343ee93925121756
7
+ data.tar.gz: 7d32ff1ed5d3d7fb3ccb57f304b96bdad8053671a0395db7408852b57a40130ce7fd76492a5048eba5bf7cb6ac7b94ff3b7dd0c178e536a1b9006815c10f07b9
data/CHANGELOG.md CHANGED
@@ -8,6 +8,42 @@ and commands may change between minor versions. How releases are made:
8
8
 
9
9
  ## [Unreleased]
10
10
 
11
+ ## [0.97.0] - 2026-10-21
12
+
13
+ ### Added
14
+
15
+ - The terminal's status row (the REPL's and an attached one's) shows the session's title first when it has one, and
16
+ follows `/title` from any client without a restart. See [docs/cli.md](docs/cli.md#status-line).
17
+ - A model server's 404 that lists the models it serves (oMLX's "Model 'X' not found. Available models: …") ends with
18
+ `did you mean '<id>'?` when one listed id is close to the one asked for: the same name under a prefix
19
+ (`mlx-community--…`, `vendor/…`), another case, or a typo. The listed ids are read past the message's cut.
20
+ - `chi memory ls` lists the memories of both scopes (or `--scope project|system`; `--format json`) with their size,
21
+ date, bundle, description and model overlays, and `chi memory rm NAME [--scope …]` removes one as `memory_write`
22
+ `remove: true` does: the file and its overlays go to the bundle trash, the index line is dropped, a bundle's memory
23
+ is refused. A profile's "kept your own" line now points at `chi memory rm`. See
24
+ [docs/memory.md](docs/memory.md#listing-and-removing-from-the-shell).
25
+
26
+ ### Fixed
27
+
28
+ - `chi bundle try` exits 130 for a Ctrl-C in the child chi's first moments too (while its Ruby was still booting it
29
+ exited 1).
30
+ - `chi bundle upgrade core --dry-run` (or `dev`) names the memory files of a member it would install that you already
31
+ have, which the install keeps as yours (`model-notes: kept your own …`), as the real run does; it named none.
32
+
33
+ Update with `chi update` (the system bundle 0.97.0), then restart `chi web`.
34
+
35
+ ## [0.96.0] - 2026-10-21
36
+
37
+ ### Added
38
+
39
+ - Session titles: `/title Coordinator` names a session (`/title` shows it, `/title --clear` drops it), from any UI or
40
+ `chi send`, mid-turn too. Every list, card, chain row, top bar, tab and notification shows it in place of the first
41
+ message, `chi sessions list` too (json has `title`), and a continue's next link keeps it. The web's info bar has `✎`
42
+ to rename the open session. See [docs/sessions.md](docs/sessions.md#session-titles).
43
+
44
+ Update with `chi update` (the system bundle 0.96.0), then restart `chi web`: a `chi web` or session worker from
45
+ before the update drops a session's title when it saves the session.
46
+
11
47
  ## [0.95.0] - 2026-10-21
12
48
 
13
49
  ### Removed
@@ -3691,7 +3727,9 @@ and long-lived sessions.
3691
3727
  - A macOS desktop helper (`chi desktop install`): a "Send to chi" Service and a
3692
3728
  hotkey panel that send selected text or the clipboard to your sessions.
3693
3729
 
3694
- [Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.95.0...HEAD
3730
+ [Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.97.0...HEAD
3731
+ [0.97.0]: https://github.com/dm1try/samagotchi/compare/v0.96.0...v0.97.0
3732
+ [0.96.0]: https://github.com/dm1try/samagotchi/compare/v0.95.0...v0.96.0
3695
3733
  [0.95.0]: https://github.com/dm1try/samagotchi/compare/v0.94.0...v0.95.0
3696
3734
  [0.94.0]: https://github.com/dm1try/samagotchi/compare/v0.93.0...v0.94.0
3697
3735
  [0.93.0]: https://github.com/dm1try/samagotchi/compare/v0.92.0...v0.93.0
data/bin/chi CHANGED
@@ -61,6 +61,8 @@ SUBCOMMANDS = {
61
61
  "models" => ["samagotchi/models_command", "ModelsCommand"],
62
62
  # the native "Send to chi" helper
63
63
  "desktop" => ["samagotchi/desktop_command", "DesktopCommand"],
64
+ # list and remove memories (ls, rm; --scope, --format)
65
+ "memory" => ["samagotchi/memory_command", "MemoryCommand"],
64
66
  # install, upgrade, uninstall, status, diff, list, build (--scope, --force, …)
65
67
  "bundle" => ["samagotchi/bundle_command", "BundleCommand"]
66
68
  }.freeze
@@ -106,6 +108,7 @@ parser = OptionParser.new do |opts|
106
108
  chi context <add|push|ls|show|refresh|rm|mute|unmute> live external text attached to sessions
107
109
  chi send [-m TEXT] ID...|--new a message into sessions, as if typed there
108
110
  chi answer ID --question QID --option N answer a question a session waits on
111
+ chi memory <ls|rm> [--scope project|system] the memories; rm moves one to the bundle trash
109
112
  chi bundle <install|upgrade|uninstall|status|diff|list|build|trash|new|test|check|try>
110
113
  chi desktop <install|upgrade|uninstall|status> macOS "Send to chi" helper
111
114
  chi models [--format json] [TEXT] the models --model takes, from every host
data/docs/architecture.md CHANGED
@@ -339,6 +339,8 @@ process that already owns the `Engine`); every worker starts it, and it exposes:
339
339
  deletes it, and the Bridge announces `:turn_dequeued`. `200 {status: "removed", enqueued_id, prompt}`, `409 already_started`
340
340
  (a turn took it), `409 not_queued` (a next-step message: a drain may merge it any moment), `404
341
341
  unknown_enqueued_id`. It never wakes the worker. The worker names it in its sidecar features (`unqueue`).
342
+ A worker's start prunes the `input/removed/` and `input/settled/` tombstones older than a week
343
+ (`SessionInbox::TOMBSTONE_MAX_AGE`, log `tombstones_pruned`); otherwise they go with the session dir.
342
344
  - `POST /session/:id/relay` — the approval relay, on a delegated child's worker
343
345
  (`{action, relay_id, question_id}`): `opened` / `closed` (with a `reason`) set or clear the
344
346
  pending question's `relayed_to` mark (`QuestionDesk#annotate`, `:question_relay`); `answered`
@@ -127,7 +127,8 @@ class, a hand-built `ctx`), that is a gap in the kit: say so to the user rather
127
127
  of a checksum mismatch, and a line with no sha installs unchecked. `chi bundle check --fix` rewrites them; run it
128
128
  after the last edit.
129
129
  - **requires_chi.** Use the chi the user runs (`chi --version`). `check` warns when the plugin calls something
130
- newer than the floor (`ctx.http` needs 0.80.0, `ctx.cost` 0.83.0).
130
+ newer than the floor (`ctx.http` needs 0.80.0, `ctx.cost` 0.83.0, `ctx.sessions.children` 0.34.0). A keyword added
131
+ later it can't see: `ctx.ask_model(model:)` needs 0.80.0, `ctx.sessions.send(delivery:)` 0.64.0.
131
132
  - **trust_level.** Leave it out. `reviewed` / `trusted` are for someone who reviewed the bundle, not its author.
132
133
  - **Restart.** A session loads bundles when it starts: after an install or a settings change, the user starts a
133
134
  new session (or `chi sessions restart ID`).
data/docs/cli.md CHANGED
@@ -20,6 +20,7 @@
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
21
  - `chi send [-m TEXT] [--image PATH]... [--cut | --queue] (ID|PREFIX)...` — send a message to sessions as if typed there: a turn starts (or a running one takes it at its next step; `--cut` asks it to cut long thinking for it; `--queue` runs it as a turn of its own after the running one, and `--queue --wait` waits for that turn's own answer); piped stdin goes above `-m` as quoted context, and `--image` attaches images (see [Sessions: Sending a message](sessions.md#sending-a-message)); `--new` starts a session with it instead (`--new --continues ID|last:ID` the next link of that session's chain, see [Session chains](sessions.md#session-chains)), and `--wait` prints the answer (`--wait ID` with no message waits for the next reply without sending — to an idle session whose last turn already ended, that ended turn's reply or end comes back; `--wait --turn T ID` with no message waits for exactly turn T, whether running, queued or last ended; `--format json` includes its `turn_id` when known, for waiting again after exit 3 or 4; exit 3 a question waits, its options on stderr; exit 4 `--timeout` passed with the turn still running; see [Starting a session](sessions.md#starting-a-session))
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`). What a parent may pick follows `guardrails.parent_answers`, or the session's own `parent_answers` when stricter (`soft`, the default: a deny, a plain answer, Continue and "Allow once"; `standard`: no allow; `hard`: no Continue either), never a wider scope or an allow on chi's own config; a refused pick exits 1 with what to answer instead (a convention, not a security boundary; see [Guardrails](guardrails.md#answers-from-a-parent-agent))
23
+ - `chi memory ls [--scope project|system] [--format text|json]` — the memories of both scopes (this folder's project first), one row each: scope, name, size, date, `from <bundle>`, the index description and its model overlays; `chi memory rm NAME [--scope project|system]` removes one as `memory_write` with `remove: true` does: its file and model overlays go to the bundle trash (the line names the dir; `chi bundle trash` lists and empties it) and its index line is dropped. NAME in both scopes without `--scope` exits 2 naming both; a missing NAME or a bundle's memory (`chi bundle uninstall` removes it) exits 1 (see [Memory: Listing and removing from the shell](memory.md#listing-and-removing-from-the-shell))
23
24
  - `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
25
  - `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))
25
26
  - `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`), and `model notes` the model notes a prompt of that model loads (`model_notes_deepseek (system, 612 chars)`, `none`; inside a session its `--mute` list is left out). `chi self --model` prints only the model a new session starts on, an alias resolved to its target (the desktop helper's hint)
@@ -653,6 +654,14 @@ attach and `chi -p --non-interactive` are refused. `/parent-answers` alone answe
653
654
  mid-turn too; a change waits for the turn's end. No chip: its reply is where the
654
655
  web shows it. See [Guardrails](guardrails.md#answers-from-a-parent-agent).
655
656
 
657
+ `/title` shows the session's title (`title: Coordinator`, or `title: none (shows
658
+ the first message: …)`); `/title TEXT` names the session (whitespace collapsed,
659
+ cut at 80 characters), `/title --clear` drops the name (anything after `--clear`
660
+ is refused with the usage line). It is saved at once,
661
+ mid-turn too, and the lists, cards, top bar and tab show it in place of the first
662
+ message; a continue's next link keeps it. The web's info bar has `✎` for it. See
663
+ [Session titles](sessions.md#session-titles).
664
+
656
665
  A `/thinking`, `/reasoning-history` or `/toolkit` change that restarts the prompt cache
657
666
  asks first: the reply says what it costs, changes nothing, and names the line
658
667
  that applies it (`/thinking low --yes`). `--yes` (or `-y`) applies it at once.
@@ -1124,10 +1133,13 @@ The REPL and attached mode show one status row under the prompt, drawn when what
1124
1133
  changes:
1125
1134
 
1126
1135
  ```
1127
- status> model=Qwen3.6-35B | ↳ 3f2a1c9e | ctx=12.3% (under20) | mem: notes, cli_usage | muted: gh-helper
1136
+ status> Coordinator | model=Qwen3.6-35B | ↳ 3f2a1c9e | ctx=12.3% (under20) | mem: notes, cli_usage | muted: gh-helper
1128
1137
  status> model=Qwen3.6-35B | ⑂ 2 running · 1 waiting | ctx=8.1% (under20) | mem: skill_coordinator
1129
1138
  ```
1130
1139
 
1140
+ - The session's title first, when it has one (`/title`; not the first message a session without one is named by),
1141
+ cut to 32 characters and to a third of a narrow row. It follows `/title` from any client (this terminal, another
1142
+ one, the web's `✎`, `chi send`) without a restart: the worker's `command_ran` for it carries the new title.
1131
1143
  - `model=`: the model in use, `(default: …)` beside it when it isn't the config's default, and
1132
1144
  `model=<served> (served; asked <name>)` when the server said it served another model (not for one its host's
1133
1145
  `hosts.<name>.models.<id>.served` names; see [configuration](configuration.md#models-a-host-serves-but-doesnt-list)).
@@ -187,6 +187,19 @@ An environment name used as a top-level key (`SAMAGOTCHI_DEFAULT_MODEL: my-model
187
187
  the old flat form) is not read: it warns as an unknown key with the nested one
188
188
  to use (`did you mean 'default.model'?`).
189
189
 
190
+ A few environment variables are not settings (no config.yml key, no flag):
191
+
192
+ - `SAMAGOTCHI_SKIP_SYSTEM_BUNDLE=1` (or `true`): don't install or upgrade the system bundle
193
+ (`samagotchi-system`) into the system memories; chi uses whatever is installed there. For isolated runs and
194
+ specs. `chi bundle try` drops it from its sandboxed child, so the sandbox gets the system bundle installed.
195
+ - `SAMAGOTCHI_BOOT_FALLBACK`: internal. A worker's boot line sets it when the newest installed chi failed to
196
+ load and the worker fell back to its starter's version; the worker reads and unsets it, and logs
197
+ `boot_fallback` with the error. See [Sessions](sessions.md) (Which chi a worker runs).
198
+ - `SAMAGOTCHI_DEFAULT_MODEL_FROM_CLI=1`: internal. Set beside `SAMAGOTCHI_DEFAULT_MODEL` for a worker a
199
+ `chi --model X` spawned: X is that run's model, which the chi its commands run must not inherit.
200
+ - `SAMAGOTCHI_LOG_STRICT=1`: an unknown log tag or a malformed log event raises instead of being dropped. The spec
201
+ suite sets it, and a bundle's tests keep it (`Samagotchi::Testing::Sandbox`).
202
+
190
203
  ## Model Server API
191
204
 
192
205
  Chi talks to a model server over HTTP in one of two ways, set by the host's `api:`:
@@ -56,7 +56,7 @@ A task the model leaves running (it ends its turn: "I'll check when it's finishe
56
56
  - **The wake turn**: the first finished task runs a continue turn from client `task:<id>` (`last_turn.origin: "task"`, labelled `task <id8> finished` in the TUI and the web; the web's bell rings at its end however short), whose first message is the note (`turn_start`, `turn_id`, written before the turn's checkpoint so a restore keeps it). It counts toward `session.max_wakes` with the other wakes (`WorkerWakes#task_open?`: no continue offer, not paused, past the 2 s start grace). Several tasks finishing together: one wake turn, the others plain notes before it.
57
57
  - **A plain note instead** (the same text without the last two lines, no turn start), written at once, with `queue`, the budget spent (the UIs hear `N finished background tasks noted; the model reads them with your next message` once per human input, the flag shared with the other wakes), wakes paused, a continue offer waiting, input queued, an exit asked for, or a **note-only** task: one started or waited on in a turn the user stopped (reasons `user`, `manual`, `ctrl_c`), since a wake telling the model to carry on would undo that Stop. A prompt's turn takes the finished ones as plain notes first.
58
58
  - **Failure**: a wake turn that fails and kept nothing, or is stopped and restored, turns its note back into the plain one (`WorkerInbound#unmark_wake`, as for a context wake) and pauses wakes until the user writes.
59
- - **Idle exit**: with `wake`, a watched task holds the worker's idle exit (`WorkerIdleExit`'s `:background_tasks`) until its wake is taken: while it runs, and once it finished until the next idle pass takes it (`TaskWatch#holding`, in memory, no `ps`), so one that ends between the pass's look and the exit's check still wakes the session. A still-running one holds `/exit` too (`TaskWatch#running`; the 409 lists the tasks, `{id, command}`, with a `hint` on stopping them, and the attached terminal names them and says to use the web's stop-task button or ask for `task_stop`), as reminders do. A note-only task holds neither (no wake would come for it), nor does one whose note, or the look for it (the conversation's task ids unreadable), failed 3 times (`TaskWatch::MAX_FAILURES`: it is still tried at each look, but can't keep the worker up for good). With `queue` nothing holds: nothing would wake for the end, so keeping the worker up isn't worth it. As the worker leaves (any exit but a stop, a signal or an `/exit --delete`), the tasks that finished since the last look are written as plain notes; a task that ends after the worker left gets no note (the model sees its end at its next look, as before). A forgotten dev server keeps the worker up until it is stopped; a crash of that server wakes the session.
59
+ - **Idle exit**: with `wake`, a watched task holds the worker's idle exit (`WorkerIdleExit`'s `:background_tasks`) until its wake is taken: while it runs, and once it finished until the next idle pass takes it (`TaskWatch#holding`, in memory, no `ps`), so one that ends between the pass's look and the exit's check still wakes the session. A still-running one holds `/exit` too (`TaskWatch#running`, read once per request by `WorkerIdleExit#request_hold`; the 409 lists the tasks, `{id, command}`, with a `hint` on stopping them, and the attached terminal names them and says to use the web's stop-task button or ask for `task_stop`), as reminders do. A note-only task holds neither (no wake would come for it), nor does one whose note, or the look for it (the conversation's task ids unreadable), failed 3 times (`TaskWatch::MAX_FAILURES`: it is still tried at each look, but can't keep the worker up for good). With `queue` nothing holds: nothing would wake for the end, so keeping the worker up isn't worth it. As the worker leaves (any exit but a stop, a signal or an `/exit --delete`), the tasks that finished since the last look are written as plain notes; a task that ends after the worker left gets no note (the model sees its end at its next look, as before). A forgotten dev server keeps the worker up until it is stopped; a crash of that server wakes the session.
60
60
  - **The tool descriptions**: a worker's `execute` and `task_create` say "if you end your turn before it finishes, chi tells you when it does" (left out under `off` and in the REPL), fixed when the worker builds its Engine.
61
61
 
62
62
  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).
@@ -39,7 +39,7 @@ user-facing side (what a bundle owns, profiles, needs) is in [memory.md](../memo
39
39
  | `Provenance` | the `.bundles/<name>/` dir: `#write` the record + bases, `#record` → `InstalledBundle`, `.each_installed`, `.claimants` (which bundles own a file) |
40
40
  | `InstalledBundle`, `BundleHook`, `PluginRef` | value objects for the record; tolerant of older records (missing keys read as empty) |
41
41
  | `Builder` | `chi bundle build`: memories (+ the installed bundle's hooks, rules, plugin, scripts) → zip/tar/dir |
42
- | `Profile` | meta bundles (`includes:`): install the members, record which; `InstallResult#kept_own` names a new member's memory files the user already had (`kept_own_line`, printed by `chi bundle`, `chi update` and `chi bootstrap`) |
42
+ | `Profile` | meta bundles (`includes:`): install the members, record which; `InstallResult#kept_own` names a new member's memory files the user already had, with other bytes (`kept_own_line`, printed by `chi bundle`, `chi update` and `chi bootstrap`); a dry run runs each member's Installer as a dry run too, so it names them as well |
43
43
  | `SystemBundle`, `ShippedUpdate` | the built-in system bundle (`ensure!` at Engine start, `sync` in `chi update`) and `chi update`'s upgrade of shipped bundles; both drive the Installer with `upgrade: true` |
44
44
  | `Uninstaller`, `Trash`, `Status`, `Listing` | remove, keep removed files, `chi bundle status`, `chi bundle list` |
45
45
  | `IndexUpdater`, `IndexSync` | the managed `index.md` lines (Installer; and the write/edit tools for memories) |
@@ -527,10 +527,12 @@ Engine initialization (in this order, among other things):
527
527
  @event_routes (EventRoutes: where a card or a notice goes: held while the plugins load, an anytime command's, an init task's, a turn event, else announced or held)
528
528
  @hook_routes (HookRoutes: notices (through @event_routes), hook questions, stop_turn/stop_generation, steer attribution)
529
529
  @guardrail_wiring, @question_desk, @relay_desk (RelayDesk: a delegate's approval relay)
530
+ @thinking_notices (ThinkingNotices)
531
+ build_tools_and_plugins:
530
532
  @extension_load = ExtensionLoad.new; @hooks = @extension_load.hooks (config.yml's, then the bundles')
531
533
  @tools = Tools::Builtins.registry; @command_registry (SessionCommands' built-ins); @plugin_tasks
532
534
  @toolkit_binding (ToolkitBinding: the kits, the plugins' among them, and the turn-start apply)
533
- @thinking_notices (ThinkingNotices); @tool_index = Tools::ToolIndex.new; @peer_sources = PeerSources.new
535
+ @tool_index = Tools::ToolIndex.new; @peer_sources = PeerSources.new
534
536
  the bundles' plugins load (load_plugins → ExtensionLoad#load_plugins; each plugin's ctx is a
535
537
  Plugin::Context over plugin_host, whose ctx.sessions it builds itself: the test kit passes its own, sessions:),
536
538
  then (plugins or not) @tool_index.declare(@tools): find_tools + call_tool (source "index", not core),
data/docs/memory.md CHANGED
@@ -18,8 +18,46 @@ Tool behavior:
18
18
  - `memory_write`'s `description` is the managed `index.md` line's text: one line (whitespace and newlines collapse to single spaces), at most 200 characters (longer is refused, since every session's prompt for the repo carries it). A missing or blank one keeps the line's description.
19
19
  - `memory_write` with `name`, `scope` and `description` but no `content` changes only that entry's index line (its size and date are refreshed; the date becomes today) and leaves the file as it is: a skill that keeps its status in a memory's description changes it without resending the body. The memory must exist in that scope; the form is refused for `index` and with `current_model_only`.
20
20
  - `memory_write` with `name`, `scope` and `remove: true` (no content, description or `current_model_only`) removes a memory: its file and its model overlays (`<name>.<key>.md`, not a dotted memory with an index line of its own) move into one dir of the bundle trash, `memory-<name>-<time>` under `memories/.bundles/.trash/`, which `chi bundle trash` lists and empties, and its index line is dropped. When the file is already gone (deleted by hand), the dangling line is dropped alone. A memory a bundle owns (an installed bundle's record lists it, or its line says `· from <bundle>`) is refused: `chi bundle uninstall` removes it. `index` can't be removed. The guardrails bundle's `memory-remove` rule asks before every removal, once at a time (see [Guardrails](guardrails.md#the-guardrails-bundle)).
21
+ - `chi memory ls` and `chi memory rm NAME` are the user's side of the same (see [Listing and removing from the shell](#listing-and-removing-from-the-shell)).
21
22
  - `write`/`edit` that change a `*.md` right in a scope dir refresh its managed `index.md` line (date, bytes; the description is kept), as `memory_write` does. A memory created with `write` gets a line with no description; any other `.md` written there gets a line too. Model overlays (`<name>.<key>.md` next to `<name>.md`), files in another project's folder and files changed by `execute` are left out.
22
23
 
24
+ ## Listing and removing from the shell
25
+
26
+ `chi memory ls [--scope project|system] [--format text|json]` lists the
27
+ memories of both scopes (this folder's project first, then system; `--scope`
28
+ one), by name: scope, name, size in bytes (right-aligned), date, `from <bundle>` for a
29
+ bundle's memory, and the index description (cut at 60 characters; `--format
30
+ json` has it all, with `path`, `source`, `overlays`, `indexed` and `file`).
31
+ It reads what `memory_read` does: each scope's `index.md` lines and its
32
+ `*.md` files. A model overlay isn't a row of its own: its memory's row ends
33
+ in `[overlays: <key>, …]`. A file with no index line says `[no index line]`
34
+ (its date is the file's), a line whose file was deleted by hand `[no file]`.
35
+
36
+ ```
37
+ project handoff 812 2026-10-21 Coordinator hand-off: where the work stands [overlays: qwen3-6-35b-a3b]
38
+ system identity 5120 2026-10-03 from samagotchi-system Who chi is and how it works with you
39
+ ```
40
+
41
+ `chi memory rm NAME [--scope project|system]` removes a memory exactly as
42
+ `memory_write` with `remove: true` does, and prints that line (the trash dir
43
+ it went to):
44
+
45
+ - the file and its model overlays (`NAME.<key>.md`) move into one dir of the
46
+ bundle trash, `memory-NAME-<time>`, which `chi bundle trash` lists and
47
+ empties; its index line is dropped (a dangling line alone, when the file is
48
+ already gone);
49
+ - without `--scope`, NAME must be in one scope: in both it exits 2 with
50
+ `chi memory rm: NAME is in both scopes: chi memory rm NAME --scope project, or --scope system`;
51
+ in neither, 1;
52
+ - a bundle's memory is refused (exit 1, `memory 'NAME' came from bundle B;
53
+ chi bundle uninstall B removes it`), as for `memory_write`: the bundle
54
+ owns it, and an upgrade would bring it back or merge into nothing;
55
+ - `NAME.<key>` removes that one model overlay and leaves its memory (a
56
+ bundle's overlay is refused like its memory);
57
+ - `index` can't be removed.
58
+
59
+ It isn't asked first: it is the user's command.
60
+
23
61
  ## Model-Specific Memory Overlays
24
62
 
25
63
  Each memory entry may have a companion file named `<name>.<model-key>.md` in the same scope directory. When the entry is read under a matching model, the overlay body is appended automatically, separated by the standard `---` separator with a `Model-specific guidance (<key>):` header.
@@ -118,10 +156,12 @@ Working habits for this model:
118
156
  (already exists; use --force to overwrite)`; through the `dev` profile
119
157
  (`chi bundle install dev`, `chi bundle upgrade dev`, `chi update`) it is
120
158
  `model-notes: kept your own model_notes_deepseek.md (not the bundle's; to
121
- take the bundle's, remove yours with memory_write remove: true, then chi
122
- bundle install model-notes)`.
159
+ take the bundle's, remove yours with chi memory rm model_notes_deepseek
160
+ --scope system, then chi bundle install model-notes)` (a dry run,
161
+ `chi bundle upgrade dev --dry-run`, names it too).
123
162
  - **To take the bundle's version instead,** remove your note
124
- (`memory_write` with `remove: true` moves it to the trash) and run
163
+ (`chi memory rm model_notes_deepseek --scope system`, or `memory_write`
164
+ with `remove: true`, moves it to the trash) and run
125
165
  `chi bundle install model-notes` again: the install writes the missing
126
166
  file and owns it from then on. Don't use `--force`: it overwrites every
127
167
  file of the bundle with no trash copy.
data/docs/plugins.md CHANGED
@@ -966,7 +966,7 @@ id = ctx.sessions.fork(messages: ctx.messages + [{ role: "user", content: q }, {
966
966
  title: "btw: #{q}")
967
967
  ctx.sessions.send(id, "go on from here")
968
968
  ctx.sessions.send(id, "then the docs", delivery: :queue) # after its running turn
969
- ctx.sessions.read(id) # => {id:, title:, status:, parent_id:, running:, messages:}
969
+ ctx.sessions.read(id) # => {id:, title:, status:, parent_id:, running:, messages:} (title: its /title, else its preview)
970
970
  ctx.sessions.children # => [{id:, short_id:, state:, branch:, last_reply:, reported:, ...}]
971
971
  ctx.sessions.stop(id) # one of this session's own children
972
972
  ```
@@ -977,8 +977,9 @@ ctx.sessions.stop(id) # one of this session's own children
977
977
  attach to it. It returns the child's id.
978
978
  - Without `prompt:` the child waits idle. With one, it runs it as its first
979
979
  turn, and counts against `session.max_children` (like `delegate`).
980
- - `title:` is what the lists show until its first turn (else the prompt, or
981
- the first user message).
980
+ - `title:` is its preview: what the lists show for it (else the prompt, or
981
+ the first user message). It stays after the first turn; it is not a
982
+ `/title` of its own (see [Session titles](sessions.md#session-titles)).
982
983
  - An image a message names is copied into the child. One whose file is
983
984
  gone is dropped, with `[image x.png was not copied]` in its message.
984
985
  - `send(id, text)` sends a user message to a session (an id or a unique
data/docs/sessions.md CHANGED
@@ -217,13 +217,19 @@ chi send --new --continues last:3fa2 -m "new day" # the next link of 3fa2's
217
217
  - 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.
218
218
  - 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.
219
219
 
220
+ ## Session titles
221
+
222
+ A session is named by its first message until you give it a title: `/title Coordinator` (in the REPL, an attached terminal, the web composer or `chi send -m "/title Coordinator" ID`; a parent agent may name its delegates the same way). The title is saved in the session file (`title`), whitespace collapsed and cut at 80 characters, and runs mid-turn too, at once. `/title` alone shows it (`title: none (shows the first message: hey chi …)` without one); `/title --clear` drops it, and the first message names the session again. A session whose first line was the command itself (`chi send --new -m "/title X"`) takes its preview from its first real message.
223
+
224
+ The title names the session wherever its first message did: `chi sessions list` (plain, `--live`, tsv and json's `desc`, before the recap; json has `title` too), `chi sessions delete`'s line, the `list_sessions` tool, `chi broadcast`, the desktop helper, a parent's children (the coordinator bundle's `/children` card, `ctx.sessions.children`; `ctx.sessions.read` too), and in the web the cards, the families and chain rows, the top bar and the browser tab, the notifications and the search; the terminal's status row too (the REPL's and an attached one's, first, see [Status Line](cli.md#status-line)). The web's info bar has `✎` (Rename this session): a small input prefilled with the title; Enter or a click elsewhere sends `/title TEXT` when it changed (empty: `/title --clear`), Esc closes it unsent. A session a chi REPL owns has no `✎`. A continue's new link keeps the title (see [Session chains](#session-chains)). An older `chi web` (or any chi from before the field) that saves a session drops its title: restart `chi web` after upgrading.
225
+
220
226
  ## Session chains
221
227
 
222
228
  A session can **continue** an earlier one: "the next run of the same routine" (a daily coordinator, say, continued once a day or several times). 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.
223
229
 
224
230
  `SessionManager.continue_session` makes the next link:
225
231
 
226
- - **What it copies:** the previous link's folder, its model (the name it was typed as, so an alias holds) unless the continue names one (`--model`, the web's `model`), and its own LLM context values. A model given is checked like a new session's (a host config.yml doesn't have refuses the continue, nothing archived). An inherited model whose host can't run it any more (gone from config.yml, `enabled: false`, an alias naming another host) **falls back to the default model** with a warning, `gone:org/model can't run (unknown host 'gone' in model …); started on <default>`, decided before the archive; with no default model configured the continue is refused. The carry note doesn't mention the model. The new link carries no transcript, so a different model, or another provider, starts clean there. Its preview is the previous link's (until its first message is saved), unless it starts with a message.
232
+ - **What it copies:** the previous link's folder, its model (the name it was typed as, so an alias holds) unless the continue names one (`--model`, the web's `model`), and its own LLM context values. A model given is checked like a new session's (a host config.yml doesn't have refuses the continue, nothing archived). An inherited model whose host can't run it any more (gone from config.yml, `enabled: false`, an alias naming another host) **falls back to the default model** with a warning, `gone:org/model can't run (unknown host 'gone' in model …); started on <default>`, decided before the archive; with no default model configured the continue is refused. The carry note doesn't mention the model. The new link carries no transcript, so a different model, or another provider, starts clean there. Its preview is the previous link's (until its first message is saved), unless it starts with a message. Its title (`/title`) is the previous link's, with or without a first message.
227
233
  - **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 09:12), 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.
228
234
  - **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.
229
235
  - **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 finished delegates are archived with it, as any archive does.
@@ -231,7 +237,7 @@ A session can **continue** an earlier one: "the next run of the same routine" (a
231
237
  - **If the continue fails part-way:** a new link that fails to start gives its moved delegates back before it is deleted (each gets a note saying so; its worker is stopped first, if one started, and a worker that won't stop is reported, the link left), with any report one of them rang it with meanwhile; the previous link, back in the lists, is woken for reports waiting. A continue that dies in the middle leaves a `move.json` in the new link's folder: the new link's next worker finishes that move (only the delegates it names, and without a second note to one already told) as it starts. Until the new link's own worker holds it, a ring from a moved delegate doesn't start another (a `starting` marker in its folder, which that worker removes; a stale one, over 60 s old or from a process that is gone, is ignored). An open delegate below an archived delegate refuses the continue (`under archived …`): unarchive that one, or archive this one.
232
238
  - **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's host can't run it when no default model is configured to fall back to. 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.
233
239
  - **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 `model` (or `model_name`) for the new link's model, but not `dir`, `llm_context` or `thinking` (400 `invalid_continues`: the previous link's are taken). 201 is the new session's summary, `continues` set, with `moved`: the ids of the delegates that moved to it (`[]` for none), and `model_warning`: the default-model fallback's warning, then any about the model id, joined by `; ` (`null` for none), and `opener`: the previous link's first typed message, in full, when that link continues one itself (`null` otherwise: a chain's first link's first message is its task); 404 `not_found`; 400 `ambiguous_id` for a prefix of several sessions, `invalid_model` for a given model whose host config.yml doesn't have (or a fallback with no default model); 409 `continued` with `next_id` (the link that continues it already, which the page opens instead), `open_children` with `ids` (open delegates that can't move), `folder_gone`, `busy`, `scratch` or `owned_by_tui`. Every session summary carries `continues`.
234
- - **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 an opener in the composer, not sent (a draft already there stays instead): the previous link's own first typed message when that link continues one (the 201's `opener`; a `!shell` line isn't taken), else `Continue where we left off.`. So whatever you sent on the last link comes back on the next one, and editing it once changes it for every later link; the previous link is archived, a toast says so (`Continued 3fa2c1d0 in 4b7e9a01; 3fa2c1d0 is archived; 1 open delegate moved`), and a moved delegate's card folds under the new link. When the previous link's model's host is gone the link starts on the default model and the toast carries the server's `model_warning` too, and then stays until closed. A fresh link (one with no turn yet, however it was made: `continue →`, `chi send --new --continues`, a reload) shows the model picker in its composer with the link's model, until its first turn starts (a failed or canceled first turn ends it too); a pick runs `/model <name>` in the link (a command bubble; the info bar follows) and changes neither the start page's remembered model nor its Recent list, and a Send right after it waits for the `/model` to be taken. A link a chi REPL owns, or one continued already, shows none. 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 that can't move shows a toast that names each one (its preview and what the server says to do: `older chi worker 0.46.1; restart it`, `open in a chi REPL; close it there`), 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 id, its full start (`2026-10-10 14:32`) and its preview in the tooltip; a neighbour that started on the open link's own day shows its time instead (`← 09:12`, `14:32 →`). A link made by `continue →` inherits the previous link's title when it is created, so its card keeps its chain's title whatever opener it is sent, and its top bar and browser tab show its card's title. A link whose title is the opener and nothing else (a `chi send --new --continues` link whose first message is `Continue where we left off.`) 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 `↩ #N` chip (`#N of M` when later links aren't listed; its tooltip `Session chain: link N of M`) opens a popover of the other links, newest first (`#N`, its date, with its time (`2026-10-17 14:32`) when another link of the chain started the same day, 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 `#N of M`, with its own delegates folded in, so a search finds an earlier link and its delegates.
240
+ - **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 an opener in the composer, not sent (a draft already there stays instead): the previous link's own first typed message when that link continues one (the 201's `opener`; a `!shell` line isn't taken), else `Continue where we left off.`. So whatever you sent on the last link comes back on the next one, and editing it once changes it for every later link; the previous link is archived, a toast says so (`Continued 3fa2c1d0 in 4b7e9a01; 3fa2c1d0 is archived; 1 open delegate moved`), and a moved delegate's card folds under the new link. When the previous link's model's host is gone the link starts on the default model and the toast carries the server's `model_warning` too, and then stays until closed. A fresh link (one with no turn yet, however it was made: `continue →`, `chi send --new --continues`, a reload) shows the model picker in its composer with the link's model, until its first turn starts (a failed or canceled first turn ends it too); a pick runs `/model <name>` in the link (a command bubble; the info bar follows) and changes neither the start page's remembered model nor its Recent list, and a Send right after it waits for the `/model` to be taken. A link a chi REPL owns, or one continued already, shows none. 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 that can't move shows a toast that names each one (its preview and what the server says to do: `older chi worker 0.46.1; restart it`, `open in a chi REPL; close it there`), 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 id, its full start (`2026-10-10 14:32`) and its preview in the tooltip; a neighbour that started on the open link's own day shows its time instead (`← 09:12`, `14:32 →`). A link made by `continue →` inherits the previous link's preview (and its title, if it has one) when it is created, so its card keeps its chain's title whatever opener it is sent, and its top bar and browser tab show its card's title. A link whose title is the opener and nothing else (a `chi send --new --continues` link whose first message is `Continue where we left off.`) 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 `↩ #N` chip (`#N of M` when later links aren't listed; its tooltip `Session chain: link N of M`) opens a popover of the other links, newest first (`#N`, its date, with its time (`2026-10-17 14:32`) when another link of the chain started the same day, its own title when it differs from the card's (a link inherits its chain's, so mostly it doesn't), else its recap's first sentence, else 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 `#N of M`, with its own delegates folded in, so a search finds an earlier link and its delegates.
235
241
  - **CLI:** `chi send --new --continues (ID|PREFIX|last:ID) [--model M] [-m TEXT]` (see *Starting a session*).
236
242
  - **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.
237
243
 
@@ -51,8 +51,9 @@ module Samagotchi
51
51
 
52
52
  LIB_DIR = File.expand_path("../..", __dir__)
53
53
  TRUSTED = %w[reviewed trusted].freeze
54
- # A call of the plugin API, or a name SINCE lists, in the source.
55
- API_CALL = /\b(?:chi|ctx)\.[a-z_]+\b|\bfallback_for:|\bSamagotchi::AtomicFile\b/
54
+ # A call of the plugin API (with one member after it: ctx.sessions.children),
55
+ # or a name SINCE lists, in the source.
56
+ API_CALL = /\b(?:chi|ctx)\.[a-z_]+(?:\.[a-z_]+)?\b|\bfallback_for:|\bSamagotchi::AtomicFile\b/
56
57
  # A requirement's lower bound: >= X, > X, ~> X or = X.
57
58
  LOWER_BOUND = /\A(?:>=|>|~>|=)?\s*(\d+(?:\.\d+)*)\z/
58
59
 
@@ -213,7 +214,8 @@ module Samagotchi
213
214
  floor = lower_bound(manifest.requires_chi)
214
215
  return unless floor && File.file?(path(manifest.plugin[:file]))
215
216
 
216
- calls = File.read(path(manifest.plugin[:file])).scan(API_CALL).uniq.select { |call| Plugin::Api::SINCE.key?(call) }
217
+ calls = File.read(path(manifest.plugin[:file])).scan(API_CALL).flat_map { |call| [call, call.split(".").first(2).join(".")] }
218
+ .uniq.select { |call| Plugin::Api::SINCE.key?(call) }
217
219
  call = calls.max_by { |c| Gem::Version.new(Plugin::Api::SINCE[c]) } or return
218
220
  version = Plugin::Api::SINCE[call]
219
221
  return unless Gem::Version.new(version) > floor
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "open3"
4
4
  require "rbconfig"
5
+ require_relative "../../cli/exit"
5
6
  require_relative "../../process_group"
6
7
 
7
8
  module Samagotchi
@@ -54,7 +55,7 @@ module Samagotchi
54
55
  end
55
56
  trap_state[:pid] = nil
56
57
  out, err = finish_readers(readers, streams)
57
- code = timed_out ? CLI::Exit::FAILED : process_exit_status(status)
58
+ code = timed_out ? CLI::Exit::FAILED : exit_code(status, trap_state)
58
59
  Result.new(exit_status: code, stdout: out, stderr: err, session_id: err[/^Session: (\S+)/, 1],
59
60
  timed_out: timed_out)
60
61
  end
@@ -91,13 +92,16 @@ module Samagotchi
91
92
  end
92
93
 
93
94
  def trap_forwarding
94
- state = { pid: nil, wait: nil, pending: nil }
95
+ state = { pid: nil, wait: nil, pending: nil, interrupted: false }
95
96
  old = {}
96
97
  %w[INT TERM].each do |signal|
97
98
  old[signal] = Signal.trap(signal) do
98
99
  if state[:pid]
99
100
  # Not once its waiter reaped it: that group may be gone.
100
- ProcessGroup.signal(state[:pid], signal) if state[:wait]&.alive?
101
+ if state[:wait]&.alive?
102
+ state[:interrupted] ||= signal == "INT"
103
+ ProcessGroup.signal(state[:pid], signal)
104
+ end
101
105
  else
102
106
  state[:pending] ||= signal
103
107
  end
@@ -111,7 +115,10 @@ module Samagotchi
111
115
  state[:pid] = pid
112
116
  pending = state[:pending]
113
117
  state[:pending] = nil
114
- ProcessGroup.signal(pid, pending) if pending
118
+ return unless pending
119
+
120
+ state[:interrupted] ||= pending == "INT"
121
+ ProcessGroup.signal(pid, pending)
115
122
  end
116
123
 
117
124
  def restore_traps(old)
@@ -171,6 +178,14 @@ module Samagotchi
171
178
  readers.each(&:join)
172
179
  end
173
180
 
181
+ # A Ctrl-C forwarded to the child is an interrupted run, however it
182
+ # ended: a Ruby still booting (before chi's INT handler) exits 1.
183
+ def exit_code(status, trap_state)
184
+ return CLI::Exit::INTERRUPTED if trap_state[:interrupted] && !status.success?
185
+
186
+ process_exit_status(status)
187
+ end
188
+
174
189
  def process_exit_status(status)
175
190
  status.exitstatus || (128 + status.termsig.to_i)
176
191
  end
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: samagotchi-system
3
- version: 0.95.0
3
+ version: 0.97.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:
@@ -81,7 +81,7 @@ module Samagotchi
81
81
  def child(session, row, cursors, state_dir:)
82
82
  reply = ReplyWait.newest_reply(session.id, state_dir: state_dir)
83
83
  cursor = Tools::DelegateCursors.get_from(cursors, session.id)
84
- Child.new(id: session.id, short_id: session.id[0, 8], title: row[:preview].to_s,
84
+ Child.new(id: session.id, short_id: session.id[0, 8], title: (session.title || row[:preview]).to_s,
85
85
  state: state_of(session, row, reply), waiting: row[:waiting], live: !!row[:live],
86
86
  delegate: session.delegate?, cwd: session.working_directory, branch: GitHead.branch(session.working_directory),
87
87
  last_reply: reply && first_line(session.id, reply, state_dir: state_dir),
@@ -187,49 +187,13 @@ module Samagotchi
187
187
  @question_desk = build_question_desk
188
188
  # Built here, not on first use: the turn thread and the Bridge's read it.
189
189
  @relay_desk = RelayDesk.new
190
- @extension_load = ExtensionLoad.new(hook_failures: @guardrail_failures)
191
- @hooks = @extension_load.hooks
192
- # The tools this session offers (the prompts' declarations and the
193
- # kernel's dispatch): the built-ins, per Engine. execute.description
194
- # off: execute offers no description parameter. +task_finish_notes+:
195
- # a worker's, whose finish wake reports a task left running (the
196
- # Worker's TaskWatch): execute and task_create say so.
197
- @tools = Tools::Builtins.registry(command_description: Config.get("execute.description") != false,
198
- policy: Config.get(LLMContextStrategy::POLICY_SETTING),
199
- task_finish_notes: task_finish_notes)
200
- restrict_scratch_tools! if @scratch
201
- # Likewise the slash commands its SessionCommands run.
202
- @command_registry = SessionCommands.register_builtins(Commands::Registry.new)
203
- # Installed bundles' plugins add commands, tools and hooks to these
204
- # (docs/plugins.md); one that fails is announced with the load
205
- # failures, and the rest still load.
206
- # Their services (chi.service), which #shutdown stops, and the anytime
207
- # commands running now (#spawn_anytime), which it waits for.
208
- @services = Plugin::Services.new
209
- @anytime_threads = []
210
- # chi.init tasks (#add_init_task), started by #start_init_tasks!, and
211
- # plugins' tool sets from chi.replace_tools until the turn thread
212
- # applies them (#apply_staged_tools!). Built before the plugins load.
213
- @plugin_tasks = build_plugin_tasks
214
- # The session's toolkit: the kits (the plugins' too, chi.toolkit),
215
- # what its next turn declares, the turn-start apply.
216
- @toolkit_binding = build_toolkit_binding
217
190
  # The notices a turn's thinking level leads to (once per session and host).
218
191
  @thinking_notices = ThinkingNotices.new(session: -> { @session }, profile: -> { self.profile },
219
192
  notify: ->(text, level, source) { hook_notify(text, level, source) })
220
- # The tools plugins publish without declaring (chi.tool_source), and
221
- # the search and call tools that reach them (Tools::ToolIndex).
222
- @tool_index = Tools::ToolIndex.new
223
- # The context sources the peer bundles declare as a peer agent's
224
- # answers (chi.peer_source); the worker's ContextAbsorber asks it.
225
- @peer_sources = PeerSources.new
226
193
  @lifecycle_mutex = Mutex.new
227
194
  @shut_down = false
228
- load_plugins if plugins
229
- # Registered from what the sources published while they loaded, and
230
- # with no plugins too: a toolkit's left-out built-ins are reachable
231
- # either way (declared per toolkit, Tools::Registry).
232
- @tool_index.declare(@tools, failures: @plugin_failures)
195
+ # The built-ins, the commands, and the plugins that add to them.
196
+ build_tools_and_plugins(load: plugins, task_finish_notes: task_finish_notes)
233
197
  guardrail_rules
234
198
  # The Engine owns the reminder store; its kernel (built here, or a
235
199
  # spec's) gets it, so tool calls via KernelLoop and reminder injection
@@ -277,6 +241,52 @@ module Samagotchi
277
241
 
278
242
  # ── initialize's collaborators ──────────────────────────────────────────
279
243
 
244
+ # The Engine as the plugins' host: the tools and slash commands this
245
+ # session offers, what plugins add to them (services, init tasks, the
246
+ # toolkit, the tool index, peer sources), then the plugins themselves
247
+ # unless +load+ is false (see #initialize for when).
248
+ def build_tools_and_plugins(load:, task_finish_notes:)
249
+ @extension_load = ExtensionLoad.new(hook_failures: @guardrail_failures)
250
+ @hooks = @extension_load.hooks
251
+ # The tools this session offers (the prompts' declarations and the
252
+ # kernel's dispatch): the built-ins, per Engine. execute.description
253
+ # off: execute offers no description parameter. +task_finish_notes+:
254
+ # a worker's, whose finish wake reports a task left running (the
255
+ # Worker's TaskWatch): execute and task_create say so.
256
+ @tools = Tools::Builtins.registry(command_description: Config.get("execute.description") != false,
257
+ policy: Config.get(LLMContextStrategy::POLICY_SETTING),
258
+ task_finish_notes: task_finish_notes)
259
+ restrict_scratch_tools! if @scratch
260
+ # Likewise the slash commands its SessionCommands run.
261
+ @command_registry = SessionCommands.register_builtins(Commands::Registry.new)
262
+ # Installed bundles' plugins add commands, tools and hooks to these
263
+ # (docs/plugins.md); one that fails is announced with the load
264
+ # failures, and the rest still load.
265
+ # Their services (chi.service), which #shutdown stops, and the anytime
266
+ # commands running now (#spawn_anytime), which it waits for.
267
+ @services = Plugin::Services.new
268
+ @anytime_threads = []
269
+ # chi.init tasks (#add_init_task), started by #start_init_tasks!, and
270
+ # plugins' tool sets from chi.replace_tools until the turn thread
271
+ # applies them (#apply_staged_tools!). Built before the plugins load.
272
+ @plugin_tasks = build_plugin_tasks
273
+ # The session's toolkit: the kits (the plugins' too, chi.toolkit),
274
+ # what its next turn declares, the turn-start apply.
275
+ @toolkit_binding = build_toolkit_binding
276
+ # The tools plugins publish without declaring (chi.tool_source), and
277
+ # the search and call tools that reach them (Tools::ToolIndex).
278
+ @tool_index = Tools::ToolIndex.new
279
+ # The context sources the peer bundles declare as a peer agent's
280
+ # answers (chi.peer_source); the worker's ContextAbsorber asks it.
281
+ @peer_sources = PeerSources.new
282
+ load_plugins if load
283
+ # Registered from what the sources published while they loaded, and
284
+ # with no plugins too: a toolkit's left-out built-ins are reachable
285
+ # either way (declared per toolkit, Tools::Registry).
286
+ @tool_index.declare(@tools, failures: @plugin_failures)
287
+ end
288
+ private :build_tools_and_plugins
289
+
280
290
  # The tools a scratch session doesn't offer, and its memory_write.
281
291
  def restrict_scratch_tools!
282
292
  # A scratch session's children would outlive it, and so would a
@@ -1260,6 +1270,7 @@ module Samagotchi
1260
1270
  muted_memory_names: @muted_memory_names.dup,
1261
1271
  prompt_notes: prompt_notes.map(&:to_h),
1262
1272
  parent_id: @session&.parent_id,
1273
+ title: @session&.title,
1263
1274
  model_name: effective_model_ref,
1264
1275
  served_model: served_pair[0],
1265
1276
  served_model_for: served_pair[1],