samagotchi 0.18.0 → 0.19.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 +119 -1
- data/README.md +4 -3
- data/bin/chi +5 -2
- data/docs/architecture.md +15 -2
- data/docs/cli.md +41 -9
- data/docs/configuration.md +8 -4
- data/docs/guardrails.md +8 -0
- data/docs/memory.md +4 -0
- data/docs/sessions.md +6 -4
- data/lib/samagotchi/bridge.rb +25 -9
- data/lib/samagotchi/bridge_client/event_stream.rb +37 -1
- data/lib/samagotchi/bridge_client.rb +9 -5
- data/lib/samagotchi/bundle_command.rb +2 -1
- data/lib/samagotchi/bundles/source-links/hooks/source_links.rb +7 -4
- data/lib/samagotchi/bundles/source-links/manifest.yml +2 -2
- data/lib/samagotchi/bundles/system/manifest.yml +2 -2
- data/lib/samagotchi/bundles/system/memory_guide.md +1 -1
- data/lib/samagotchi/client.rb +58 -10
- data/lib/samagotchi/engine.rb +35 -11
- data/lib/samagotchi/guardrail_wiring.rb +64 -5
- data/lib/samagotchi/guardrails/approval.rb +10 -4
- data/lib/samagotchi/guardrails/approvals.rb +60 -12
- data/lib/samagotchi/host_registry.rb +22 -4
- data/lib/samagotchi/idle_scheduler.rb +1 -1
- data/lib/samagotchi/installed_versions.rb +91 -0
- data/lib/samagotchi/live_versions.rb +5 -3
- data/lib/samagotchi/llm/http.rb +9 -3
- data/lib/samagotchi/memory_bundle/builder.rb +24 -1
- data/lib/samagotchi/memory_bundle/index_sync.rb +1 -9
- data/lib/samagotchi/memory_bundle/installer.rb +33 -0
- data/lib/samagotchi/memory_bundle/status.rb +5 -1
- data/lib/samagotchi/memory_bundle/uninstaller.rb +6 -2
- data/lib/samagotchi/model_overlay.rb +39 -0
- data/lib/samagotchi/model_profile.rb +6 -2
- data/lib/samagotchi/output_formatter.rb +9 -4
- data/lib/samagotchi/relay_desk.rb +9 -0
- data/lib/samagotchi/served_model.rb +14 -4
- data/lib/samagotchi/session_manager.rb +166 -11
- data/lib/samagotchi/session_metrics.rb +36 -13
- data/lib/samagotchi/sessions_command.rb +43 -4
- data/lib/samagotchi/system_prompt.rb +10 -1
- data/lib/samagotchi/terminal_ui/attached_loop.rb +56 -6
- data/lib/samagotchi/terminal_ui/image_input.rb +11 -4
- data/lib/samagotchi/terminal_ui/one_shot_sink.rb +63 -0
- data/lib/samagotchi/terminal_ui/version_lines.rb +48 -0
- data/lib/samagotchi/terminal_ui.rb +65 -16
- data/lib/samagotchi/tool_declarations.rb +2 -2
- data/lib/samagotchi/tools/builtins.rb +21 -5
- data/lib/samagotchi/tools/execute.rb +97 -19
- data/lib/samagotchi/turn_note.rb +3 -2
- data/lib/samagotchi/update_command.rb +33 -12
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +64 -11
- data/lib/samagotchi/web/public/app.js +102 -29
- data/lib/samagotchi/web/public/data.js +6 -0
- data/lib/samagotchi/web/public/format.js +4 -2
- data/lib/samagotchi/web/public/index.html +11 -0
- data/lib/samagotchi/web/public/question_card.js +12 -0
- data/lib/samagotchi/web/public/question_cards.js +17 -11
- data/lib/samagotchi/web/public/scope.js +15 -6
- data/lib/samagotchi/web/public/session_state.js +4 -0
- data/lib/samagotchi/web/public/turn_events.js +107 -3
- data/lib/samagotchi/web/public/update_notice.js +87 -14
- data/lib/samagotchi/web/server.rb +28 -1
- data/lib/samagotchi/web/session_hub.rb +30 -3
- data/lib/samagotchi/web/session_summary.rb +5 -0
- data/lib/samagotchi/worker.rb +25 -8
- data/lib/samagotchi/worker_idle_exit.rb +31 -2
- data/lib/samagotchi/worker_sidecar.rb +7 -5
- metadata +4 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5b6bb51634467b04d3519fc2c42a778be2db25558565e8e872bc2fbdc17f5c76
|
|
4
|
+
data.tar.gz: 9f1e4b1bd71272126a3dea346b2b0f5125a608770a97832b17c42ec4a237156b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b1b2eb7e4142061cce070830722bc88625cfc8b8301b97e836751ac9690d0e78984741afd747a6cc9ac42be6a0b59122bc49075273522629eda56f22327999d9
|
|
7
|
+
data.tar.gz: 6277d5a520c55f5b620f79936321c2536739409ef98a5a7b530e41505a2ffb17bd0bf04987fc0bf027c09a24dbedbea29f61affca2bf7f3a74218bbcf8651df8
|
data/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,122 @@ and commands may change between minor versions. How releases are made:
|
|
|
8
8
|
|
|
9
9
|
## [Unreleased]
|
|
10
10
|
|
|
11
|
+
## [0.19.0] - 2026-10-03
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
|
|
15
|
+
- A new or woken session worker runs the newest chi installed, whoever starts it: a `chi web`, `chi send` or
|
|
16
|
+
terminal left on an older chi no longer starts workers on its own version. A worker already running keeps its
|
|
17
|
+
version until it is restarted.
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
|
|
21
|
+
- `chi web` notices a newer chi installed (`chi update`, `gem update`, a `git pull` in a checkout) and says to
|
|
22
|
+
restart it, in its terminal and once in each open page.
|
|
23
|
+
- The web's info bar marks a session whose worker runs an older chi than the newest installed, with a Restart button
|
|
24
|
+
(or, for a worker from before restarts, the `chi sessions stop ID` to run); the page and the draft in the
|
|
25
|
+
composer stay as they are and follow the new worker.
|
|
26
|
+
- An attached terminal says in one line when the session's worker or the terminal itself runs an older chi than
|
|
27
|
+
the newest installed, and how to move each (`chi sessions restart ID`; `/detach`, then `chi --attach ID`). When
|
|
28
|
+
the worker restarts, the terminal stays attached and says so.
|
|
29
|
+
- `chi sessions restart ID...` moves a running session to a new worker on the newest chi installed, keeping its
|
|
30
|
+
attached terminal and web tabs on it; it is refused, with the reason, while a turn, a question, reminders or
|
|
31
|
+
background tasks would be lost.
|
|
32
|
+
- A bundle can ship per-model memory overlays (`tips.<model-key>.md` beside `tips.md`): they load only for that
|
|
33
|
+
model, as a `memory_write current_model_only` overlay does, and `chi bundle build tips.md` brings them along.
|
|
34
|
+
|
|
35
|
+
### Fixed
|
|
36
|
+
|
|
37
|
+
- `chi update` no longer says running sessions move to the new chi "at idle exit (30 min)": a worker keeps its chi
|
|
38
|
+
until restarted, and idles out only with nothing attached, after `session.idle_exit_minutes` (never with 0). The
|
|
39
|
+
workers row names `chi sessions restart ID` (or `stop` for a worker from before restarts).
|
|
40
|
+
- `chi web` no longer leaves exited session workers behind as zombie processes.
|
|
41
|
+
- The web's model picker lists a host added to (or changed in) `hosts:` while `chi web` runs, instead of the hosts
|
|
42
|
+
it started with.
|
|
43
|
+
- A bundle's model overlay no longer gets an `index.md` line of its own after `chi bundle install`, which showed it
|
|
44
|
+
to every model as a memory to read; `chi bundle status` reads it `ok (model overlay)`, uninstall no longer re-adds
|
|
45
|
+
its line, and an overlay whose base is nowhere, or only among your installed memories, installs with a warning. A
|
|
46
|
+
line an earlier install wrote goes with the bundle's next upgrade.
|
|
47
|
+
- source-links (0.3.2): a `case_insensitive: true` source no longer lists a ref in its `sources:` note when the ref
|
|
48
|
+
is a markdown link's label whose target names it in another case (`[jira-1](…/JIRA-1)`). Run `chi update` to get it.
|
|
49
|
+
- An `@path` image in a prompt takes backslash escapes, as a Finder drag into the terminal types them:
|
|
50
|
+
`@my\ shot.png` and `@a\ \(1\).png` attach the file instead of stopping at the first space.
|
|
51
|
+
- An `http` host named by a public name (`gpu.example.com`) counts as a remote provider (120-second first-token
|
|
52
|
+
limit, no llama.cpp `/props`), as a public IP address already did. Names without a dot and local suffixes
|
|
53
|
+
(`.local`, `.lan`, `.home.arpa`, `.internal`, `.ts.net`, `fritz.box`, …) stay local; `hosts.<name>.remote` decides
|
|
54
|
+
either way.
|
|
55
|
+
- `chi web` no longer logs `ERROR HTTPRequest#fixup: WEBrick::HTTPStatus::LengthRequired occurred.` after a POST
|
|
56
|
+
without a body.
|
|
57
|
+
- The web's delete confirm no longer promises a running worker from a stale list: it says the worker is stopped
|
|
58
|
+
first if it is still running.
|
|
59
|
+
- The web's scope links (`all`, `← project`) keep the page's `?view=stage|turn`.
|
|
60
|
+
- An approval card's verdict (`Allowed (scope)` / `Denied`) reads above the command it decided, so it no longer
|
|
61
|
+
looks like part of the tool call when the card's details are expanded.
|
|
62
|
+
- Answering or dismissing a question no longer says "Session is not running" when the worker is live but failed the
|
|
63
|
+
request: the server answers 502 with the worker's detail and the card shows it. 503 stays for a session with no
|
|
64
|
+
live worker.
|
|
65
|
+
- A turn that failed to send no longer leaves its prompt waiting to be restored into the composer later; and text
|
|
66
|
+
typed while a fast-failed turn's prompt was coming back is kept, with the restored prompt going under it.
|
|
67
|
+
- A worker that dies between a turn's end and its after-turn display no longer leaves the turn's answer bubble
|
|
68
|
+
waiting: the page stops waiting when the worker goes away, or after 20 s.
|
|
69
|
+
- A first web message the page answers itself (`/stats`, `/exit` …) or that is a typo of a command (`/modle`) no
|
|
70
|
+
longer leaves an empty session behind.
|
|
71
|
+
- A question card reopened after a reload shows its result once, in the collapsed summary, not twice.
|
|
72
|
+
- After a mid-turn reload, this tab's own prompt is still recognized as its own instead of being labelled `web`.
|
|
73
|
+
- A `chi scratch` session no longer offers `send_note`, so it leaves no note behind in another session.
|
|
74
|
+
- A `chi scratch` session's tool approvals offer only "Allow once" and "for the session", and keep a session
|
|
75
|
+
approval in memory: no "in this repo" or rule approval from it stays in the approvals store.
|
|
76
|
+
- A message sent (web, `chi send`, a delegate) to a session whose worker is slow to start no longer starts a second
|
|
77
|
+
worker beside it.
|
|
78
|
+
- The note a cancelled turn leaves for the model says "the reply above ends where it was cut off", not "the
|
|
79
|
+
answer", so narration cut off before a tool call isn't taken for an answer.
|
|
80
|
+
- A session worker that wakes again shows the model the server last said it served (`/stats`, the status row,
|
|
81
|
+
the web's info bar) right away, instead of none until its first generation.
|
|
82
|
+
- A tool call still running when its turn is cancelled (or fails) is saved in the session's analytics as
|
|
83
|
+
`canceled`, so the session's tool totals (`/stats`) count it as the turn did instead of dropping it.
|
|
84
|
+
- A session's `analytics.json` `session_duration_ms` runs from the session's `started_at`, not from when its current
|
|
85
|
+
worker started.
|
|
86
|
+
- A session started with `chi --model X` no longer hands X to the commands it runs as the default model: a `chi
|
|
87
|
+
self` (or other chi) run there reports and uses the configured default, with X as this session's model. A
|
|
88
|
+
`SAMAGOTCHI_DEFAULT_MODEL` you export yourself still passes.
|
|
89
|
+
- A preloaded memory that can't be loaded is warned about once, not again each time the prompt is rebuilt (another
|
|
90
|
+
thinking level, a `/model` switch).
|
|
91
|
+
|
|
92
|
+
Update with `chi update`: source-links moves to 0.3.2 (case-insensitive link targets). Restart `chi web` afterwards; from now on it tells you when a newer chi is installed.
|
|
93
|
+
|
|
94
|
+
## [0.18.1] - 2026-10-03
|
|
95
|
+
|
|
96
|
+
### Fixed
|
|
97
|
+
|
|
98
|
+
- The `execute` tool no longer hangs when a command leaves a process in the background (`server &`, or
|
|
99
|
+
`cd DIR && nohup server > log 2>&1 &`) that keeps its output open: after a short grace it stops that process and
|
|
100
|
+
returns the output, with a note to start servers and long jobs with `task_create`. Stop and
|
|
101
|
+
`execute.timeout_sec` work during the wait too.
|
|
102
|
+
- A running session no longer denies every tool call after an installed bundle's rules are upgraded to a version
|
|
103
|
+
that needs a newer chi (`requires_chi`): it keeps the rules it loaded before and says once to restart the session
|
|
104
|
+
(`chi sessions stop ID`, then `chi --resume ID`).
|
|
105
|
+
- `chi -p … --non-interactive` keeps stdout for the answer alone: the `Session:` (or `Resumed session:`) line goes
|
|
106
|
+
to stderr, before the turn. A failed turn now says after its error that the session is kept with the prompt and
|
|
107
|
+
how to continue it (`chi --resume ID`).
|
|
108
|
+
- Ctrl-C in `chi -p … --non-interactive` keeps the session: the prompt and a cancel note are saved, so
|
|
109
|
+
`chi --resume ID` has them, and stderr says so instead of a Ruby backtrace. The exit status stays 130.
|
|
110
|
+
- `chi -p` shows the after_turn hooks' notices (source-links' `sources:`, any notifier), which it lost: with
|
|
111
|
+
`--non-interactive` on stderr after the answer; attached (input from a pipe) it waits for them and shows them
|
|
112
|
+
before it detaches.
|
|
113
|
+
- `chi -p … --non-interactive` prints the retry lines on stderr (`↻ empty answer, asking again (1/1)`, `retrying
|
|
114
|
+
(1/5 in 0.5s): …`), as the REPL shows them, instead of nothing.
|
|
115
|
+
- A native llama.cpp turn sends `GET /props` once, not twice.
|
|
116
|
+
- After a failed `/props` probe, a server that then answers is probed again at the next turn, instead of the
|
|
117
|
+
failure being reused for 30 s.
|
|
118
|
+
- A request to a model server is sent only as many times as chi's own retries say: Ruby's hidden extra retry is off.
|
|
119
|
+
- The served-model notice no longer treats `gpt-4` and `gpt-4o` as the same model: a served name counts as the
|
|
120
|
+
asked one only when what follows starts with `:`, `-`, `@` or `/` (`qwen3` = `qwen3:latest`).
|
|
121
|
+
- A Gemma thought channel (`<|channel>thought` … `<channel|>`) is stripped as a whole block, so the model's
|
|
122
|
+
reasoning no longer shows up in the answer.
|
|
123
|
+
- An unknown model profile name falls back to `qwen36`, the same default as everywhere else, instead of `gemma4`.
|
|
124
|
+
|
|
125
|
+
Update with `gem update samagotchi` (or `chi update`); no bundle changed.
|
|
126
|
+
|
|
11
127
|
## [0.18.0] - 2026-10-03
|
|
12
128
|
|
|
13
129
|
### Added
|
|
@@ -1002,7 +1118,9 @@ and long-lived sessions.
|
|
|
1002
1118
|
- A macOS desktop helper (`chi desktop install`): a "Send to chi" Service and a
|
|
1003
1119
|
hotkey panel that send selected text or the clipboard to your sessions.
|
|
1004
1120
|
|
|
1005
|
-
[Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.
|
|
1121
|
+
[Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.19.0...HEAD
|
|
1122
|
+
[0.19.0]: https://github.com/dm1try/samagotchi/compare/v0.18.1...v0.19.0
|
|
1123
|
+
[0.18.1]: https://github.com/dm1try/samagotchi/compare/v0.18.0...v0.18.1
|
|
1006
1124
|
[0.18.0]: https://github.com/dm1try/samagotchi/compare/v0.17.0...v0.18.0
|
|
1007
1125
|
[0.17.0]: https://github.com/dm1try/samagotchi/compare/v0.16.0...v0.17.0
|
|
1008
1126
|
[0.16.0]: https://github.com/dm1try/samagotchi/compare/v0.15.0...v0.16.0
|
data/README.md
CHANGED
|
@@ -54,9 +54,10 @@ chi update # the gem, then the bundles and the desktop helper
|
|
|
54
54
|
stay installed: running sessions still use them), then brings the rest up to
|
|
55
55
|
the new version: the system bundle, the bundles chi ships that you installed,
|
|
56
56
|
and the macOS helper (rebuilt only when its sources changed). Memory files you
|
|
57
|
-
edited are kept, and the table says which. Running sessions
|
|
58
|
-
|
|
59
|
-
running `chi web` needs a restart
|
|
57
|
+
edited are kept, and the table says which. Running sessions keep their chi
|
|
58
|
+
until restarted (`chi sessions restart ID`, or the Restart badge on the web);
|
|
59
|
+
new and woken ones start on the new chi. A running `chi web` needs a restart,
|
|
60
|
+
and its page says so. From a checkout, `git pull` instead. See
|
|
60
61
|
[CLI: Updating](docs/cli.md#updating).
|
|
61
62
|
|
|
62
63
|
## Set up
|
data/bin/chi
CHANGED
|
@@ -314,6 +314,7 @@ if launch == :attached
|
|
|
314
314
|
exit Samagotchi::TerminalUI::AttachLauncher.exit_status(ended)
|
|
315
315
|
end
|
|
316
316
|
|
|
317
|
+
require "samagotchi/cli/exit"
|
|
317
318
|
begin
|
|
318
319
|
ended = Samagotchi::TerminalUI.new(
|
|
319
320
|
prompt: options[:prompt],
|
|
@@ -326,8 +327,10 @@ begin
|
|
|
326
327
|
model_name: options[:model],
|
|
327
328
|
scratch: scratch
|
|
328
329
|
).run
|
|
329
|
-
# `chi -p X </dev/null`: the turn failed (nothing was sent again)
|
|
330
|
-
|
|
330
|
+
# `chi -p X </dev/null`: the turn failed (nothing was sent again); -p
|
|
331
|
+
# --non-interactive: the turn ended with no answer, or Ctrl-C cancelled it.
|
|
332
|
+
exit 1 if %i[turn_failed empty_answer].include?(ended)
|
|
333
|
+
exit Samagotchi::CLI::Exit::INTERRUPTED if ended == :interrupted
|
|
331
334
|
rescue Samagotchi::TerminalUI::SessionBusy, Samagotchi::TerminalUI::SessionNotFound, Samagotchi::ModelProfile::MissingModel => e
|
|
332
335
|
warn "Error: #{e.message}"
|
|
333
336
|
exit 1
|
data/docs/architecture.md
CHANGED
|
@@ -213,6 +213,10 @@ process that already owns the `Engine`); every worker starts it, and it exposes:
|
|
|
213
213
|
too-old reconnects, and cursors from another worker's epoch, receive a `reset` marker carrying
|
|
214
214
|
`session_state_snapshot`. `?snapshot=1` joins with a snapshot frame instead of a replay;
|
|
215
215
|
`?client_id=` names whose stream it is (`Bridge#open_streams_except`, used by `POST /exit`).
|
|
216
|
+
An attached terminal's stream (`EventStream` with `rediscover:`) looks for the session's live
|
|
217
|
+
worker whenever it drops: a new one (a restart) is followed from its snapshot, and the terminal
|
|
218
|
+
sends its requests there (`worker_changed`); it gives up after ~12 s, or at once when the session
|
|
219
|
+
was stopped.
|
|
216
220
|
- `POST /session/:id/turn` — fire-and-forget turn creation; returns `202` with an `enqueued_id`
|
|
217
221
|
(delivery is at-least-once via the worker's file-IPC input path — it never calls `run_turn`
|
|
218
222
|
across the HTTP boundary). Only the bridge's own session: another id, in the path or the body's
|
|
@@ -302,7 +306,13 @@ the server, no daemon) that keeps an in-memory projection of the session list an
|
|
|
302
306
|
changes to every open tab over `GET /api/events` (SSE: a `snapshot` frame on every connect,
|
|
303
307
|
then `session` for an upsert and `session_gone` for a removal, `: ping` while idle, no replay).
|
|
304
308
|
The snapshot names chi's version: a tab served by another one (its `<body data-version>`) offers a
|
|
305
|
-
reload in a toast, since an open tab keeps its old JS across a `chi web` upgrade.
|
|
309
|
+
reload in a toast, since an open tab keeps its old JS across a `chi web` upgrade. It also names
|
|
310
|
+
`installed`, the newest chi on this machine (`InstalledVersions`: the highest `samagotchi-<v>.gemspec`
|
|
311
|
+
in RubyGems' specification dirs, or a checkout's `version.rb` on disk; looked at again on each full
|
|
312
|
+
probe, every 10 s), and a change is a `chi` frame `{version, installed}`. A newer one than the
|
|
313
|
+
server runs is said in chi web's terminal and on the page: restart chi web (its workers already
|
|
314
|
+
run the newest chi, [Sessions](sessions.md)). `GET /api/info` carries `installed` too, and the
|
|
315
|
+
feature `newest_workers`.
|
|
306
316
|
Files stay the source of truth and workers don't know the hub. Its watcher is a 1 s tick that
|
|
307
317
|
stats the sessions dir (every session writer goes tmp + rename, which bumps the dir's mtime) and
|
|
308
318
|
each `<id>/` folder (recap.json, bridge.json, the stopped and archived markers), re-parsing only the files whose mtime or size
|
|
@@ -310,7 +320,10 @@ moved through `Session.summary_from_file`. Liveness is probed, since a killed ow
|
|
|
310
320
|
file trace: the owner lock every tick for the sessions the projection believes owned, and every
|
|
311
321
|
session every 10 s, so a `kill -9` shows within a second. The summary (`Web::SessionSummary`,
|
|
312
322
|
shared with `/api/sessions` and the session view) carries `owner`, `project_root` and
|
|
313
|
-
`bridge_up` (the sidecar is there *and* the lock is held: the page attaches its stream on it)
|
|
323
|
+
`bridge_up` (the sidecar is there *and* the lock is held: the page attaches its stream on it), and
|
|
324
|
+
from that sidecar `worker_version` (the chi the worker runs) and `worker_features`
|
|
325
|
+
(`Bridge::FEATURES`, what a client may ask of it beyond the routes). A worker's Bridge snapshot
|
|
326
|
+
names its `chi_version` too.
|
|
314
327
|
`POST/DELETE /api/sessions` and `/stop` rescan the session before answering (`SessionHub#touch`).
|
|
315
328
|
`GET /api/sessions` is the hub's projection too (its sort, paging and total). `Server` always builds
|
|
316
329
|
a hub; an `App.new` without one answers both routes `503 no_hub`.
|
data/docs/cli.md
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
- `chi web --web-host lan` — the Web UI on your home network too, for your phone: a link with an access token and its QR code (see [chi web on your phone](#chi-web-on-your-phone)); `chi web --new-token` replaces the token
|
|
16
16
|
- `chi web --web-markdown` — opt in to sanitized Markdown rendering for completed assistant messages
|
|
17
17
|
- `chi web --web-view turn` — draw turns with the turn view (each turn as one block of steps, the running one at the bottom of the history) instead of the default stage view (the running turn pinned above the composer); `?view=stage|turn` on the page URL overrides it (see [Web views](#web-views))
|
|
18
|
-
- `chi sessions list|stop|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` or `delete` with no ids, a bad `list --format` or `--scope`) exits 2; an unknown or refused session exits 1
|
|
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 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
21
|
- `chi answer ID --question QID (--option N|LABEL)... [--text T] [--timeout S] [--format json]` (or `--dismiss`) — answer the question a session waits on (the one `chi send --wait` exited 3 with), then wait and print what comes next as `chi send --wait` does: the reply (0), the next question (3), still running after `--timeout` (4). `--option` is 1-based or the label, repeated on a multi-select question; `--text` is free text, or a Deny's reason; `--dismiss` leaves it unanswered and the model finishes its reply. A question no longer open (the web answered first) isn't answered again: it waits for that turn's reply. An option the question doesn't offer exits 2; a worker that is gone exits 1 (`send the task again: chi send --wait -m "…" ID`). An approval can be denied, not allowed, unless `guardrails.parent_approvals: once` (a convention, not a security boundary; see [Guardrails](guardrails.md#approvals-from-a-parent-agent))
|
|
@@ -104,17 +104,19 @@ known-names 0.1.0 0.1.1 updated
|
|
|
104
104
|
core 0.1.0 0.2.0 updated (+ new-bundle)
|
|
105
105
|
infra_tools 1.0.0 skipped (not from chi)
|
|
106
106
|
Chi Helper 0.2.0 up to date (launch file refreshed)
|
|
107
|
-
workers 2 live on 0.2.0: they
|
|
107
|
+
workers 2 live on 0.2.0: they keep it until restarted (chi sessions restart 2ea8c1f0 91b0d2aa); or they idle out after 30 min with nothing attached
|
|
108
108
|
Also shipped, not installed: dev (mcp, skills, source-links) (chi bundle install NAME)
|
|
109
109
|
done
|
|
110
110
|
```
|
|
111
111
|
|
|
112
112
|
- **The gem.** It asks rubygems.org for the newest samagotchi (5 s timeout)
|
|
113
113
|
and, when that's newer, runs `gem install samagotchi` with the gem command
|
|
114
|
-
of the Ruby chi runs on (the real one, not a mise/rbenv/asdf shim
|
|
114
|
+
of the Ruby chi runs on (the real one, not a mise/rbenv/asdf shim; a chi
|
|
115
|
+
installed under another Ruby is a separate install it doesn't touch). Then it
|
|
115
116
|
hands over to the new chi, which does the rest and prints the table. Old
|
|
116
|
-
versions stay installed: running workers and an old `chi web` still
|
|
117
|
-
them
|
|
117
|
+
versions stay installed: running workers and an old `chi web` still load
|
|
118
|
+
files from them, so a `gem cleanup` while they run breaks them (a
|
|
119
|
+
`LoadError`, missing web assets); restart them first. Offline, the row says
|
|
118
120
|
"couldn't check" and the rest still runs; a failed install fails the row
|
|
119
121
|
and the rest runs on the current version. Under Bundler (`bundle exec`)
|
|
120
122
|
the row says `bundle update samagotchi` instead.
|
|
@@ -137,9 +139,15 @@ done
|
|
|
137
139
|
- **The desktop helper** (macOS) is rebuilt and restarted only when its Swift
|
|
138
140
|
sources changed (or the Ruby it runs moved); otherwise only its launch file
|
|
139
141
|
is refreshed. See [Desktop helper](desktop.md).
|
|
140
|
-
- **Running processes** are reported, never stopped
|
|
141
|
-
|
|
142
|
-
|
|
142
|
+
- **Running processes** are reported, never stopped. A live worker keeps its
|
|
143
|
+
chi until it is restarted: `chi sessions restart ID` (one from before
|
|
144
|
+
restarts: `chi sessions stop ID`, and the next prompt starts it on the new
|
|
145
|
+
chi); it exits on its own only when nothing is attached (no web tab, no
|
|
146
|
+
terminal, no reminder) and `session.idle_exit_minutes` pass (never with 0).
|
|
147
|
+
A new or woken worker always starts on the newest chi installed. A `chi web`
|
|
148
|
+
on `web.port` running an older chi needs a restart (Ctrl-C, then `chi web`):
|
|
149
|
+
its page says so too. One from before workers started on the newest chi
|
|
150
|
+
also starts its new sessions on its own version until then (the row says so).
|
|
143
151
|
|
|
144
152
|
`--dry-run` shows the table with "would update" and changes nothing. It is
|
|
145
153
|
this version's view: bundles that only a newer gem ships newer show up once
|
|
@@ -224,7 +232,13 @@ Notes:
|
|
|
224
232
|
messages as turn context (a `-p` run on a resumed session never clobbers its
|
|
225
233
|
conversation).
|
|
226
234
|
- Non-interactive runs (`-p` with `--non-interactive`, or bare `--non-interactive`)
|
|
227
|
-
print only the final result output — no spinner, status line, or REPL.
|
|
235
|
+
print only the final result output on stdout — no spinner, status line, or REPL.
|
|
236
|
+
Everything else goes to stderr: the `Session: ID` line first, before the turn;
|
|
237
|
+
retry lines and hooks' notices as they come, the after_turn hooks' ones
|
|
238
|
+
(source-links' `sources:`) after the answer; and the error of a failed turn,
|
|
239
|
+
then `chi: the session is kept with your prompt; continue it with: chi
|
|
240
|
+
--resume ID` (exit 1). Ctrl-C saves the session with the prompt and a
|
|
241
|
+
cancel note and exits 130.
|
|
228
242
|
|
|
229
243
|
### Scratch sessions
|
|
230
244
|
|
|
@@ -245,8 +259,12 @@ and `--shared` are refused. Its first line says it is a scratch session.
|
|
|
245
259
|
answers "scratch session: nothing is saved", and `write`/`edit` into the
|
|
246
260
|
memories folder are denied (a guardrail, rule `scratch-session`). `execute`
|
|
247
261
|
can still write files anywhere, memories included.
|
|
262
|
+
- An approval offers only "Allow once" and "for the session", and a session
|
|
263
|
+
approval is kept in memory, not in the approvals store; approvals stored
|
|
264
|
+
before still apply.
|
|
248
265
|
- No child sessions: the `delegate` tools are not offered, and a plugin's
|
|
249
266
|
`ctx.sessions.fork` (btw's side session) refuses, since they would outlive it.
|
|
267
|
+
Nor is `send_note`, whose note would stay in the other session.
|
|
250
268
|
|
|
251
269
|
### Sharing a session
|
|
252
270
|
|
|
@@ -314,6 +332,20 @@ a `chi --resume ID` after it starts a fresh one. A worker still running an
|
|
|
314
332
|
older chi (from before an upgrade) takes turns but not commands; the attached
|
|
315
333
|
terminal and the Web UI say so, with that restart line.
|
|
316
334
|
|
|
335
|
+
`chi sessions restart ID...` hands each session to a new worker on the newest
|
|
336
|
+
chi installed, without stopping it: an attached terminal and the web's tabs
|
|
337
|
+
move to the new worker. It is refused, with the reason, while something would
|
|
338
|
+
be lost: a turn running or queued, a question or approval waiting, reminders,
|
|
339
|
+
a `/btw` still running, an approval relayed from a delegate, or background
|
|
340
|
+
tasks the session started. A session with no running worker needs none (its
|
|
341
|
+
next prompt starts one on the newest chi), and a worker from before restarts
|
|
342
|
+
needs `chi sessions stop ID`. The Web UI's `POST /api/sessions/:id/restart`
|
|
343
|
+
does the same (409 with `reason` and `detail` when it can't). An attached
|
|
344
|
+
terminal says at the join, in one `chi>` line, when the worker or the terminal
|
|
345
|
+
runs an older chi than the newest installed (the worker moves with `chi
|
|
346
|
+
sessions restart ID`, the terminal with `/detach` and `chi --attach ID`), and
|
|
347
|
+
`chi> the session's worker restarted on chi X` when its worker was replaced.
|
|
348
|
+
|
|
317
349
|
`chi sessions archive ID...` hides sessions from every list (the terminal's,
|
|
318
350
|
the web's, `list_sessions`) and keeps them for good: the retention sweep never
|
|
319
351
|
deletes an archived session, nor counts it. Its delegates go with it. A live
|
data/docs/configuration.md
CHANGED
|
@@ -94,8 +94,9 @@ Behavior:
|
|
|
94
94
|
- An edit to the file needs no restart of `chi web`: a new session's worker
|
|
95
95
|
reads the file when it starts, and a running chi takes a changed value the
|
|
96
96
|
next time it reads that setting (a worker keeps `hosts:` and what it set up
|
|
97
|
-
at its start until it is stopped
|
|
98
|
-
that started it
|
|
97
|
+
at its start until it is stopped; the web's model picker lists a `hosts:`
|
|
98
|
+
edit at once). A worker gets the CLI flags of the chi that started it
|
|
99
|
+
through its environment.
|
|
99
100
|
- Workers inherit hosts via `SAMAGOTCHI_HOSTS_JSON` propagated through `SessionManager.spawn_options`.
|
|
100
101
|
|
|
101
102
|
This lets you run `chi` without repeating common defaults such as model
|
|
@@ -235,8 +236,11 @@ hosts:
|
|
|
235
236
|
**Remote or local.** chi treats a host as a remote provider by its address, not
|
|
236
237
|
by its key: an `https` url, or an `http` IP address outside the loopback,
|
|
237
238
|
private (`10.*`, `172.16–31.*`, `192.168.*`, IPv6 `fc00::/7`), link-local and
|
|
238
|
-
CGNAT/Tailscale (`100.64.0.0/10`) nets
|
|
239
|
-
|
|
239
|
+
CGNAT/Tailscale (`100.64.0.0/10`) nets, or an `http` name with a dot that
|
|
240
|
+
doesn't end in a local suffix (`gpu.example.com`). A name without a dot (`box`)
|
|
241
|
+
or ending in `localhost`, `.local`, `.lan`, `.home`, `.home.arpa`, `.internal`,
|
|
242
|
+
`.intranet`, `.localdomain`, `.private`, `.corp`, `.test`, `.box` (`fritz.box`)
|
|
243
|
+
or `.ts.net` (Tailscale) is local: chi doesn't look names up. A remote
|
|
240
244
|
host keeps its model list for 10 minutes, gets a 120-second first-token limit,
|
|
241
245
|
and isn't asked for llama.cpp's `/props` (its context window and served model
|
|
242
246
|
come from its model list and each turn). `remote: true` or `remote: false` on a
|
data/docs/guardrails.md
CHANGED
|
@@ -295,6 +295,14 @@ restart. A rule that doesn't parse (an unknown key, a bad regex, no
|
|
|
295
295
|
verdict, a `disable:` that isn't an id or a list of ids) makes chi **deny every tool call** and say why, rather than run
|
|
296
296
|
without it.
|
|
297
297
|
|
|
298
|
+
A bundle whose `requires_chi` the running chi doesn't meet (installed by a
|
|
299
|
+
newer chi while the session's worker kept running) isn't read again: the
|
|
300
|
+
rules the session loaded from it before stay, and a `guardrails>` line says
|
|
301
|
+
once per bundle version to restart the session (`chi sessions stop ID`, then
|
|
302
|
+
`chi --resume ID`). A chi that never loaded that bundle's rules (it starts
|
|
303
|
+
older than the bundle) denies every tool call, as for a rule that doesn't
|
|
304
|
+
parse, until chi is updated or the bundle uninstalled.
|
|
305
|
+
|
|
298
306
|
## The guardrails bundle
|
|
299
307
|
|
|
300
308
|
```sh
|
data/docs/memory.md
CHANGED
|
@@ -27,6 +27,10 @@ Each memory entry may have a companion file named `<name>.<model-key>.md` in the
|
|
|
27
27
|
- **In the system prompt**: the identity memory and the preloaded memories (`--memory`, config `memories:`) get their overlays too, as `memory_read` gives them; after `/model` the rebuilt prompt carries the new model's.
|
|
28
28
|
- **Dormancy**: Overlays are only active under the matching model key; other models see the base entry only.
|
|
29
29
|
- **Invariant**: The base entry is the contract. Overlays only add model-specific guidance and never contradict the base protocol.
|
|
30
|
+
- **In a bundle**: ship `<name>.<key>.md` next to `<name>.md` (ship the base too; an overlay with no base anywhere installs with a warning and loads only once the base exists; one whose base is only among your installed memories is taken as an overlay of it, with a warning naming it). The overlay gets no `index.md` line, so other models don't see it, and `chi bundle status` reads it `ok (model overlay)`; `chi bundle build FILES...` brings a named memory's overlays along.
|
|
31
|
+
- The key is the `model key` row of `chi self` under that model. It follows the provider's model id: OpenRouter's `deepseek/deepseek-v4.1-flash` keys as `deepseek-deepseek-v4-1-flash`, while a local server names the same model differently, so one model may need one overlay per provider.
|
|
32
|
+
- Key an alias by its **target**, not the alias: the alias's own key is read only as a fallback, when the alias was typed and the target has no overlay.
|
|
33
|
+
- The overlay is read in the scope its base is found in: a project memory with the same name shadows a system-scope bundle's base and overlay.
|
|
30
34
|
|
|
31
35
|
|
|
32
36
|
At startup, the agent reads both scope indexes with blank-name memory reads
|
data/docs/sessions.md
CHANGED
|
@@ -13,17 +13,19 @@ Sessions are plain files — no DB. Each session is `~/.local/state/samagotchi/s
|
|
|
13
13
|
|
|
14
14
|
A session is deleted if **expired by age OR overflow by count** (unless `keep_status` or the live-owner guard: a session a worker or `chi` still has open is never pruned). A session's `status` is its turn state (`idle`/`running`), not whether a worker is alive. Orphan dirs without a `*.json` are never deleted, except skeleton-only ones (see *Empty sessions*). Deletion removes both `*.json` and sidecar dir atomically. Set both `DAYS=0` and `MAX=0` to retain forever.
|
|
15
15
|
|
|
16
|
-
**Worker idle exit:** a background worker (plain `chi`, Web UI sessions) exits after `SAMAGOTCHI_SESSION_IDLE_EXIT_MINUTES` (`session.idle_exit_minutes`, default `30`, `0`=never) with no turn running or queued, no client on its stream (an open web tab or attached terminal keeps it), no reminder registered and no continue offer (the step-limit question) waiting for an answer. It removes `bridge.json` and frees `owner.lock`; the next prompt or `--attach` wakes a new worker (`WorkerIdleExit`, `SessionManager.run_session_loop`). A client can also ask the worker to exit right away (Bridge `POST /session/:id/exit`; `/exit` in an attached terminal does, `/detach` doesn't): the same rules apply without the timeout (even with `0`), and the asker's own stream doesn't count. The reply names what keeps it up. The session's status stays as it was, not `stopped`.
|
|
16
|
+
**Worker idle exit:** a background worker (plain `chi`, Web UI sessions) exits after `SAMAGOTCHI_SESSION_IDLE_EXIT_MINUTES` (`session.idle_exit_minutes`, default `30`, `0`=never) with no turn running or queued, no client on its stream (an open web tab or attached terminal keeps it), no reminder registered and no continue offer (the step-limit question) waiting for an answer. It removes `bridge.json` and frees `owner.lock`; the next prompt or `--attach` wakes a new worker (`WorkerIdleExit`, `SessionManager.run_session_loop`). A client can also ask the worker to exit right away (Bridge `POST /session/:id/exit`; `/exit` in an attached terminal does, `/detach` doesn't): the same rules apply without the timeout (even with `0`), and the asker's own stream doesn't count. The reply names what keeps it up. The session's status stays as it was, not `stopped`. With `"restart": true` (a worker whose sidecar lists the `restart` feature) the worker hands the session to a new worker on the newest chi installed: it leaves, then starts its successor once its lock is free, keeping even an empty session and writing no recap. Clients' streams don't hold a restart (a web tab and an attached terminal reconnect to the new worker); what would be lost does: a turn running or queued, a continue offer, a question or approval waiting, reminders, a `/btw` (or another anytime command) still running, a delegate's approval relay open or settled in the last 10 minutes, and background tasks this conversation started that still run (`WorkerIdleExit#hold_for_restart`; 409 `held` with `reason`). What lives only in the worker's memory goes: the event history a late client replays, the kept `/btw` answers, a plugin's in-memory state.
|
|
17
17
|
|
|
18
18
|
**Empty sessions:** a session nothing happened in is deleted when it is left, so `chi` → `/exit` leaves no trace. Empty means: no messages, no turn tried (a failed turn leaves a `last_prompt` and keeps it), no memory used, no pending question, nothing in `input/`, `notes/` or `images/`, nothing in its dir beyond the worker's skeleton, mode `assist`, and the default model. A session on another model (`/model`, `--model`) counts as prepared and stays (`SessionManager.discardable?`). The worker checks as it idle-exits or leaves on a client's request and deletes it once its lock is free, checking again first. The attached `/exit` then says `Detached; the session was empty, so it is discarded.` The REPL (`--no-shared`) does the same at `/exit` or Ctrl-D. The retention sweep catches those killed first: empty sessions over an hour old with no live owner, whatever `DAYS`/`MAX`, and skeleton-only dirs with no `*.json`. `session.keep_empty: true` (env `SAMAGOTCHI_SESSION_KEEP_EMPTY`) turns all of it off.
|
|
19
19
|
|
|
20
20
|
**Attached by default:** plain `chi` and `chi --resume ID` run their session in a background worker and attach to it (`session.shared`, default `true`; env `SAMAGOTCHI_SESSION_SHARED`). The worker runs in the session's `working_directory`, so `!commands` and tools see the directory the session was started in. `session.shared: false` keeps the in-process REPL, `--no-shared` does for one run; a session the REPL has open can't be attached ("close it there first"). See [CLI: Sharing a session](cli.md#sharing-a-session).
|
|
21
21
|
|
|
22
|
+
**Which chi a worker runs:** a new or woken worker runs the newest chi installed for this Ruby (`gem 'samagotchi', '>= 0.a'`, prereleases count), whoever starts it: a `chi web` or terminal still on an older chi starts it on the newest one, so a stale process doesn't spread old code. A worker already running keeps its version until it is restarted. Should the newest install fail to load, the worker runs the starter's own version and logs `boot_fallback` (its stdio is `/dev/null`). Under `bundle exec` the worker drops Bundler's env first, so it activates the newest gem too. Two consequences: a wrapper pin (`chi _0.17.0_ web`) pins only that process, not the workers it starts; and a source checkout (`bin/chi` in a worktree, `bundle exec`) starts its workers on its own `lib/`, but a session it made that is later woken by an installed chi (`chi web`, `chi send`) runs the installed code. The names an older starter hands a newer worker (the boot line, its env keys, `input/` files, the session JSON fields and the Bridge routes sessions use on each other) are pinned in `spec/worker_contract_spec.rb`.
|
|
23
|
+
|
|
22
24
|
**Stopping a worker:** `chi sessions stop ID` marks the session stopped (an empty `<id>/stopped` file, not a session.json field, which a worker saving at that moment would write over; a resume removes it), sends its worker TERM and waits (up to 10 s) until the worker has let go of `owner.lock`, so a `chi --resume ID` right after spawns a fresh worker. That is also how to restart a worker still running an older chi, which the attached terminal reports when a command or a question dismiss gets a 404, and the web when an answer, a dismiss, a command or a cancel does (`501 not_supported`; `BridgeClient.stale_worker_message`). The web's `POST /api/sessions/:id/stop` waits the same way, for 2 s.
|
|
23
25
|
|
|
24
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).
|
|
25
27
|
|
|
26
|
-
<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 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 refuses it), and an empty session is discarded instead; `chi scratch` refuses it. `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 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 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 refuses it), and an empty session is discarded instead; `chi scratch` refuses it. `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.
|
|
27
29
|
|
|
28
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.
|
|
29
31
|
|
|
@@ -118,8 +120,8 @@ system message marked `kind: turn_note` (the UIs don't show it; they showed the
|
|
|
118
120
|
before any answer: network error after 2 attempts (host main: Errno::ECONNREFUSED). The message went back to
|
|
119
121
|
the user, who may send it again.]` in the REPL and attached mode, where the prompt is given back;
|
|
120
122
|
`… The user's last message was not answered.]` after `-p`, where the prompt stays in the session;
|
|
121
|
-
- cancelled (Ctrl-C, the web's stop): `[SYSTEM: the previous turn was cancelled (ctrl-c) after 12s; the
|
|
122
|
-
above ends where it was cut off.]
|
|
123
|
+
- cancelled (Ctrl-C, the web's stop): `[SYSTEM: the previous turn was cancelled (ctrl-c) after 12s; the reply
|
|
124
|
+
above ends where it was cut off.]` (an answer or only narration had streamed), or `…; no answer had been shown.]` when only thinking had streamed;
|
|
123
125
|
- ended with nothing visible: `[SYSTEM: the previous turn ended with no visible answer (thinking only, or
|
|
124
126
|
nothing). The user's last message is still unanswered.]`.
|
|
125
127
|
|
data/lib/samagotchi/bridge.rb
CHANGED
|
@@ -212,8 +212,10 @@ module Samagotchi
|
|
|
212
212
|
# The pending question at the top level too: one asked between turns
|
|
213
213
|
# (the step-limit question) belongs to no turn, so the accumulator
|
|
214
214
|
# doesn't keep it.
|
|
215
|
+
# chi_version: the chi this worker runs (an attaching TUI compares it
|
|
216
|
+
# with its own).
|
|
215
217
|
# @return [Hash] {messages:, current_turn:, queued:, recap:, saved_recap:, continue_offer:, pending_question:,
|
|
216
|
-
# guardrail_warning:, plugin_warning:, init_tasks:, cards:, commands:, event_seq:, event_id:}
|
|
218
|
+
# guardrail_warning:, plugin_warning:, init_tasks:, cards:, commands:, chi_version:, event_seq:, event_id:}
|
|
217
219
|
def snapshot
|
|
218
220
|
@engine.synchronize_events do
|
|
219
221
|
seq = @engine.event_count
|
|
@@ -230,6 +232,7 @@ module Samagotchi
|
|
|
230
232
|
init_tasks: @engine.init_tasks,
|
|
231
233
|
cards: @cards.list,
|
|
232
234
|
commands: command_registry.listing,
|
|
235
|
+
chi_version: Samagotchi::VERSION,
|
|
233
236
|
event_seq: seq,
|
|
234
237
|
event_id: event_id(seq)
|
|
235
238
|
}
|
|
@@ -452,6 +455,12 @@ module Samagotchi
|
|
|
452
455
|
!host.empty? && !LOOPBACK_NAMES.include?(host.downcase.sub(/:\d*\z/, ""))
|
|
453
456
|
end
|
|
454
457
|
|
|
458
|
+
# What a client may ask of this worker beyond the routes, named in its
|
|
459
|
+
# sidecar (WorkerSidecar#features): a client checks a name here rather
|
|
460
|
+
# than a version.
|
|
461
|
+
# restart: POST /exit takes "restart": true (WorkerIdleExit#hold_for_restart).
|
|
462
|
+
FEATURES = ["restart"].freeze
|
|
463
|
+
|
|
455
464
|
# The routes, by method and what follows /session/:id/: a handler takes
|
|
456
465
|
# the session id and the request body and returns [headers, status, body].
|
|
457
466
|
# GET stream is served apart (#serve_sse owns the connection).
|
|
@@ -683,11 +692,13 @@ module Samagotchi
|
|
|
683
692
|
@engine.announce(type: :command_queued, **command, **anytime)
|
|
684
693
|
end
|
|
685
694
|
|
|
686
|
-
# A client asks the worker to exit now (`/exit` in the attached TUI)
|
|
687
|
-
#
|
|
688
|
-
#
|
|
689
|
-
#
|
|
690
|
-
#
|
|
695
|
+
# A client asks the worker to exit now (`/exit` in the attached TUI), or
|
|
696
|
+
# with "restart": true to hand the session to a new worker on the newest
|
|
697
|
+
# chi installed (the "restart" feature). The worker decides with the
|
|
698
|
+
# event log held, so no POST /turn lands between its check and its
|
|
699
|
+
# answer; it leaves from its loop after this reply.
|
|
700
|
+
# 200 {status: "exiting", discard?: the session is empty and goes} ({status: "restarting"} for a restart), or 409
|
|
701
|
+
# {status: "held", reason:} naming what keeps it up. Returns [headers, status, body].
|
|
691
702
|
def handle_exit_request(session_id, body)
|
|
692
703
|
return [{}, 501, { error: "exit_unavailable" }] unless @on_exit_request
|
|
693
704
|
|
|
@@ -696,8 +707,12 @@ module Samagotchi
|
|
|
696
707
|
|
|
697
708
|
client_id = fetched(parsed, "client_id")
|
|
698
709
|
delete = fetched(parsed, "delete") == true
|
|
699
|
-
|
|
710
|
+
restart = fetched(parsed, "restart") == true
|
|
711
|
+
options = restart ? { delete: delete, restart: true } : { delete: delete }
|
|
712
|
+
reason = @engine.synchronize_events { @on_exit_request.call(client_id, **options) }
|
|
700
713
|
if reason.nil?
|
|
714
|
+
return [{}, 200, { status: "restarting", session_id: @session_id }] if restart
|
|
715
|
+
|
|
701
716
|
body = { status: "exiting", session_id: @session_id }
|
|
702
717
|
body[:discard] = @exit_discards.call == true if @exit_discards
|
|
703
718
|
return [{}, 200, body]
|
|
@@ -1029,9 +1044,10 @@ module Samagotchi
|
|
|
1029
1044
|
end
|
|
1030
1045
|
|
|
1031
1046
|
def write_sidecar
|
|
1032
|
-
# version: the chi this worker runs (chi update
|
|
1047
|
+
# version: the chi this worker runs (chi update and the web's badge
|
|
1048
|
+
# report older ones); features: what a client may ask of it.
|
|
1033
1049
|
WorkerSidecar.new(port: @port, bind: @bind, session_id: @session_id, started_at: Time.now.iso8601(3),
|
|
1034
|
-
version: Samagotchi::VERSION, input_format: @input_format).write(session_dir)
|
|
1050
|
+
version: Samagotchi::VERSION, input_format: @input_format, features: FEATURES).write(session_dir)
|
|
1035
1051
|
rescue StandardError => e
|
|
1036
1052
|
Log.warn(:bridge, "sidecar_write_failed", echo: "Bridge: failed to write #{WorkerSidecar::FILE}: #{e.class}: #{e.message}", error: e.class.name)
|
|
1037
1053
|
end
|
|
@@ -17,8 +17,17 @@ module Samagotchi
|
|
|
17
17
|
# not serve the session (404), it yields one synthetic
|
|
18
18
|
# `{"type" => "stream_closed", "reason" => ...}` and stops. #close stops
|
|
19
19
|
# it without that event.
|
|
20
|
+
#
|
|
21
|
+
# With +rediscover+, a failed connection first looks for the session's
|
|
22
|
+
# live worker (its sidecar): a new one (a restart) is followed from its
|
|
23
|
+
# snapshot, announced by a synthetic `{"type" => "worker_changed",
|
|
24
|
+
# "port" => ...}` first, since its event ids start over. (A new worker
|
|
25
|
+
# on the old one's port is the plain reconnect: its Bridge can't replay
|
|
26
|
+
# the old ids and sends a reset.)
|
|
20
27
|
class EventStream
|
|
21
28
|
DEFAULT_RECONNECT_DELAYS = [0.1, 0.25, 0.5, 1.0, 2.0].freeze
|
|
29
|
+
# Long enough for a restarted worker to boot (its Engine, the plugins).
|
|
30
|
+
REDISCOVER_DELAYS = [0.1, 0.25, 0.5, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 2.0, 2.0, 2.0].freeze
|
|
22
31
|
HEADER_TIMEOUT = 5.0
|
|
23
32
|
|
|
24
33
|
# @return [String, nil] the id of the last frame yielded (the reconnect cursor)
|
|
@@ -30,9 +39,13 @@ module Samagotchi
|
|
|
30
39
|
# tell this client's stream from others' (Bridge#open_streams_except)
|
|
31
40
|
# @param reconnect_delays [Array<Float>] sleeps between failed attempts;
|
|
32
41
|
# one more failure than there are delays gives up
|
|
42
|
+
# @param rediscover [#call, nil] → a BridgeClient for the session's live
|
|
43
|
+
# worker now, nil when there is none yet, or :gone to give up now
|
|
33
44
|
# @yieldparam event [Hash] string-keyed event
|
|
34
|
-
def initialize(client, snapshot: true, client_id: nil, reconnect_delays: DEFAULT_RECONNECT_DELAYS,
|
|
45
|
+
def initialize(client, snapshot: true, client_id: nil, reconnect_delays: DEFAULT_RECONNECT_DELAYS, rediscover: nil,
|
|
46
|
+
&on_event)
|
|
35
47
|
@client = client
|
|
48
|
+
@rediscover = rediscover
|
|
36
49
|
params = []
|
|
37
50
|
params << ["snapshot", "1"] if snapshot
|
|
38
51
|
params << ["client_id", client_id] if client_id
|
|
@@ -86,6 +99,13 @@ module Samagotchi
|
|
|
86
99
|
next
|
|
87
100
|
end
|
|
88
101
|
|
|
102
|
+
case rediscover_worker
|
|
103
|
+
when :moved
|
|
104
|
+
failures = 0
|
|
105
|
+
next
|
|
106
|
+
when :gone then return finish("unreachable")
|
|
107
|
+
end
|
|
108
|
+
|
|
89
109
|
failures += 1
|
|
90
110
|
return finish("unreachable") if failures > @reconnect_delays.size
|
|
91
111
|
|
|
@@ -148,6 +168,22 @@ module Samagotchi
|
|
|
148
168
|
nil
|
|
149
169
|
end
|
|
150
170
|
|
|
171
|
+
# @return [Symbol, nil] :moved to a new worker's Bridge, :gone, or nil
|
|
172
|
+
def rediscover_worker
|
|
173
|
+
return nil unless @rediscover
|
|
174
|
+
|
|
175
|
+
found = @rediscover.call
|
|
176
|
+
return :gone if found == :gone
|
|
177
|
+
return nil if found.nil? || found.port == @client.port
|
|
178
|
+
|
|
179
|
+
@client = found
|
|
180
|
+
@last_event_id = nil
|
|
181
|
+
@on_event.call({ "type" => "worker_changed", "port" => found.port }) unless closed?
|
|
182
|
+
:moved
|
|
183
|
+
rescue StandardError
|
|
184
|
+
nil
|
|
185
|
+
end
|
|
186
|
+
|
|
151
187
|
def finish(reason)
|
|
152
188
|
@on_event.call({ "type" => "stream_closed", "reason" => reason }) unless closed?
|
|
153
189
|
end
|