samagotchi 0.18.1 → 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.
Files changed (61) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +85 -1
  3. data/README.md +4 -3
  4. data/docs/architecture.md +15 -2
  5. data/docs/cli.md +34 -8
  6. data/docs/configuration.md +8 -4
  7. data/docs/memory.md +4 -0
  8. data/docs/sessions.md +6 -4
  9. data/lib/samagotchi/bridge.rb +25 -9
  10. data/lib/samagotchi/bridge_client/event_stream.rb +37 -1
  11. data/lib/samagotchi/bridge_client.rb +9 -5
  12. data/lib/samagotchi/bundle_command.rb +2 -1
  13. data/lib/samagotchi/bundles/source-links/hooks/source_links.rb +7 -4
  14. data/lib/samagotchi/bundles/source-links/manifest.yml +2 -2
  15. data/lib/samagotchi/bundles/system/manifest.yml +2 -2
  16. data/lib/samagotchi/bundles/system/memory_guide.md +1 -1
  17. data/lib/samagotchi/engine.rb +9 -3
  18. data/lib/samagotchi/guardrail_wiring.rb +8 -2
  19. data/lib/samagotchi/guardrails/approval.rb +10 -4
  20. data/lib/samagotchi/guardrails/approvals.rb +60 -12
  21. data/lib/samagotchi/host_registry.rb +22 -4
  22. data/lib/samagotchi/idle_scheduler.rb +1 -1
  23. data/lib/samagotchi/installed_versions.rb +91 -0
  24. data/lib/samagotchi/live_versions.rb +5 -3
  25. data/lib/samagotchi/memory_bundle/builder.rb +24 -1
  26. data/lib/samagotchi/memory_bundle/index_sync.rb +1 -9
  27. data/lib/samagotchi/memory_bundle/installer.rb +33 -0
  28. data/lib/samagotchi/memory_bundle/status.rb +5 -1
  29. data/lib/samagotchi/memory_bundle/uninstaller.rb +6 -2
  30. data/lib/samagotchi/model_overlay.rb +39 -0
  31. data/lib/samagotchi/model_profile.rb +4 -0
  32. data/lib/samagotchi/relay_desk.rb +9 -0
  33. data/lib/samagotchi/session_manager.rb +166 -11
  34. data/lib/samagotchi/session_metrics.rb +36 -13
  35. data/lib/samagotchi/sessions_command.rb +43 -4
  36. data/lib/samagotchi/system_prompt.rb +10 -1
  37. data/lib/samagotchi/terminal_ui/attached_loop.rb +48 -3
  38. data/lib/samagotchi/terminal_ui/image_input.rb +11 -4
  39. data/lib/samagotchi/terminal_ui/version_lines.rb +48 -0
  40. data/lib/samagotchi/tools/builtins.rb +21 -5
  41. data/lib/samagotchi/turn_note.rb +3 -2
  42. data/lib/samagotchi/update_command.rb +33 -12
  43. data/lib/samagotchi/version.rb +1 -1
  44. data/lib/samagotchi/web/app.rb +64 -11
  45. data/lib/samagotchi/web/public/app.js +102 -29
  46. data/lib/samagotchi/web/public/data.js +6 -0
  47. data/lib/samagotchi/web/public/format.js +4 -2
  48. data/lib/samagotchi/web/public/index.html +11 -0
  49. data/lib/samagotchi/web/public/question_card.js +12 -0
  50. data/lib/samagotchi/web/public/question_cards.js +17 -11
  51. data/lib/samagotchi/web/public/scope.js +15 -6
  52. data/lib/samagotchi/web/public/session_state.js +4 -0
  53. data/lib/samagotchi/web/public/turn_events.js +107 -3
  54. data/lib/samagotchi/web/public/update_notice.js +87 -14
  55. data/lib/samagotchi/web/server.rb +28 -1
  56. data/lib/samagotchi/web/session_hub.rb +30 -3
  57. data/lib/samagotchi/web/session_summary.rb +5 -0
  58. data/lib/samagotchi/worker.rb +25 -8
  59. data/lib/samagotchi/worker_idle_exit.rb +31 -2
  60. data/lib/samagotchi/worker_sidecar.rb +7 -5
  61. metadata +3 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 34f4c3e1960aa23902e27160bd1f325ba035731c283a93b2cd819676481c90a2
4
- data.tar.gz: 708e58e5bfc26d10fe9f20bd1b696f6355e2a25749a36ca6503e82ca9077fa64
3
+ metadata.gz: 5b6bb51634467b04d3519fc2c42a778be2db25558565e8e872bc2fbdc17f5c76
4
+ data.tar.gz: 9f1e4b1bd71272126a3dea346b2b0f5125a608770a97832b17c42ec4a237156b
5
5
  SHA512:
6
- metadata.gz: 56c634e50c34f3fd027ca9ed7208d7edaab21a639050fe321c1d9fa5e8a0e818a31ae6c6351f8c21c0417984f3235a1a44b14aaa3d415b392a85d3bff2916aa1
7
- data.tar.gz: 41862c98747027f791e4aa59b4c6d2eda092d818e3492212fbc79a24970c74734c3196c480511876aa97b95a8e1245c8bfd6fceccb73d47ade67d3f96ced89fa
6
+ metadata.gz: b1b2eb7e4142061cce070830722bc88625cfc8b8301b97e836751ac9690d0e78984741afd747a6cc9ac42be6a0b59122bc49075273522629eda56f22327999d9
7
+ data.tar.gz: 6277d5a520c55f5b620f79936321c2536739409ef98a5a7b530e41505a2ffb17bd0bf04987fc0bf027c09a24dbedbea29f61affca2bf7f3a74218bbcf8651df8
data/CHANGELOG.md CHANGED
@@ -8,6 +8,89 @@ 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
+
11
94
  ## [0.18.1] - 2026-10-03
12
95
 
13
96
  ### Fixed
@@ -1035,7 +1118,8 @@ and long-lived sessions.
1035
1118
  - A macOS desktop helper (`chi desktop install`): a "Send to chi" Service and a
1036
1119
  hotkey panel that send selected text or the clipboard to your sessions.
1037
1120
 
1038
- [Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.18.1...HEAD
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
1039
1123
  [0.18.1]: https://github.com/dm1try/samagotchi/compare/v0.18.0...v0.18.1
1040
1124
  [0.18.0]: https://github.com/dm1try/samagotchi/compare/v0.17.0...v0.18.0
1041
1125
  [0.17.0]: https://github.com/dm1try/samagotchi/compare/v0.16.0...v0.17.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 move to the new
58
- chi when their worker idles out (30 min) or on `chi sessions stop ID`; a
59
- running `chi web` needs a restart. From a checkout, `git pull` instead. See
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/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 move to 0.3.0 at idle exit (30 min) or chi sessions stop 2ea8c1f0 91b0d2aa
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). Then it
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 use
117
- them (so don't `gem cleanup` while they run). Offline, the row says
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: live workers on another
141
- version, and a `chi web` on `web.port` running an older chi (sessions it
142
- starts run that version too: restart it).
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
@@ -251,8 +259,12 @@ and `--shared` are refused. Its first line says it is a scratch session.
251
259
  answers "scratch session: nothing is saved", and `write`/`edit` into the
252
260
  memories folder are denied (a guardrail, rule `scratch-session`). `execute`
253
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.
254
265
  - No child sessions: the `delegate` tools are not offered, and a plugin's
255
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.
256
268
 
257
269
  ### Sharing a session
258
270
 
@@ -320,6 +332,20 @@ a `chi --resume ID` after it starts a fresh one. A worker still running an
320
332
  older chi (from before an upgrade) takes turns but not commands; the attached
321
333
  terminal and the Web UI say so, with that restart line.
322
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
+
323
349
  `chi sessions archive ID...` hides sessions from every list (the terminal's,
324
350
  the web's, `list_sessions`) and keeps them for good: the retention sweep never
325
351
  deletes an archived session, nor counts it. Its delegates go with it. A live
@@ -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). A worker gets the CLI flags of the chi
98
- that started it through its environment.
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. An `http` host given by a name
239
- (`localhost`, `box`, `mac.local`) is local: chi doesn't look names up. A remote
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/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 answer
122
- above ends where it was cut off.]`, or `…; no answer had been shown.]` when only thinking had streamed;
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
 
@@ -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). The
687
- # worker decides with the event log held, so no POST /turn lands between
688
- # its check and its answer; it leaves from its loop after this reply.
689
- # 200 {status: "exiting", discard?: the session is empty and goes}, or 409 {status: "held", reason:} naming what
690
- # keeps it up. Returns [headers, status, body].
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
- reason = @engine.synchronize_events { @on_exit_request.call(client_id, delete: delete) }
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 reports older ones).
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, &on_event)
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
@@ -156,14 +156,16 @@ module Samagotchi
156
156
  post("command", payload, read_body: true)
157
157
  end
158
158
 
159
- # POST /session/:id/exit: ask the worker to exit now. 200 = it will
160
- # (status "exiting"), 409 = something keeps it up (reason), 404 with
159
+ # POST /session/:id/exit: ask the worker to exit now (+restart+: hand
160
+ # the session to a new worker, a worker with the "restart" feature). 200
161
+ # = it will (status "exiting"/"restarting"), 409 = something keeps it up (reason), 404 with
161
162
  # error "not_found" = a worker older than the route.
162
163
  # @param client_id [String] the asking UI, whose own streams don't hold
163
164
  # @return [Response]
164
- def request_exit(client_id:, delete: false)
165
+ def request_exit(client_id:, delete: false, restart: false)
165
166
  body = { client_id: client_id }
166
167
  body[:delete] = true if delete
168
+ body[:restart] = true if restart
167
169
  post("exit", body, read_body: true)
168
170
  end
169
171
 
@@ -290,11 +292,13 @@ module Samagotchi
290
292
  # Follow the session's events on a reader thread (see EventStream).
291
293
  # @param snapshot [Boolean] join with a snapshot frame
292
294
  # @param client_id [String, nil] names the stream (see EventStream)
295
+ # @param rediscover [#call, nil] follows a new worker (see EventStream)
293
296
  # @yieldparam event [Hash] string-keyed event
294
297
  # @return [EventStream] started
295
- def follow(snapshot: true, client_id: nil, reconnect_delays: EventStream::DEFAULT_RECONNECT_DELAYS, &on_event)
298
+ def follow(snapshot: true, client_id: nil, reconnect_delays: EventStream::DEFAULT_RECONNECT_DELAYS, rediscover: nil,
299
+ &on_event)
296
300
  EventStream.new(self, snapshot: snapshot, client_id: client_id, reconnect_delays: reconnect_delays,
297
- &on_event).start
301
+ rediscover: rediscover, &on_event).start
298
302
  end
299
303
 
300
304
  # Open GET /session/:id/stream and read past the response headers.
@@ -360,8 +360,9 @@ module Samagotchi
360
360
  mods << "conflict (kept your edits over v#{st[:provenance][:version]}: chi bundle diff #{name} #{k})" if info[:conflict]
361
361
  mods << "modified" if info[:modified]
362
362
  mods << "missing" if info[:missing]
363
- mods << "no-index" unless info[:index_present]
363
+ mods << "no-index" unless info[:index_present] || info[:overlay]
364
364
  label = mods.empty? ? "ok" : mods.join(",")
365
+ label += " (model overlay)" if info[:overlay]
365
366
  @stdout.puts " #{k}: #{label}"
366
367
  end
367
368
  # Hooks
@@ -321,7 +321,8 @@ class SourceLinks
321
321
  links.any? do |link|
322
322
  in_target = start >= link[:target][0] && finish <= link[:target][1]
323
323
  in_label = start >= link[:label][0] && finish <= link[:label][1]
324
- in_target || (in_label && target_names_ref?(link[:target_text], text[start...finish]))
324
+ in_target || (in_label && target_names_ref?(link[:target_text], text[start...finish],
325
+ case_insensitive: match.regexp.casefold?))
325
326
  end
326
327
  end
327
328
 
@@ -330,10 +331,12 @@ class SourceLinks
330
331
  # the ref. So `[JIRA-1](…/JIRA-12)` is NOT skipped (the target names
331
332
  # JIRA-12), while `[JIRA-123](…/JIRA-123)` is. Note this is stricter than
332
333
  # the bare-text scan's `\b`, which treats `-` as a boundary: `…/JIRA-1-foo`
333
- # would match there but not here.
334
- def target_names_ref?(target, ref)
334
+ # would match there but not here. A case_insensitive source compares
335
+ # without case too: `[jira-1](…/JIRA-1)` is skipped.
336
+ def target_names_ref?(target, ref, case_insensitive: false)
335
337
  escaped = Regexp.escape(ref)
336
- target.match?(/(?<![A-Za-z0-9\-])#{escaped}(?![A-Za-z0-9\-])/)
338
+ flags = case_insensitive ? Regexp::IGNORECASE : 0
339
+ target.match?(Regexp.new("(?<![A-Za-z0-9\\-])#{escaped}(?![A-Za-z0-9\\-])", flags))
337
340
  end
338
341
 
339
342
  # A ref glued to URL punctuation is part of a link too, even when the span
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: source-links
3
- version: 0.3.1
3
+ version: 0.3.2
4
4
  scope: system
5
5
  description: Links source refs (JIRA tickets, GitHub issues, …) in the model's answer — inline links in the web, and a one-line note after the turn (note false turns it off); sources are configured patterns
6
6
  trust_level: reviewed
@@ -8,7 +8,7 @@ files:
8
8
  source_links.md: sha256:aebd37f1e25491780994ecc4afc20ed6e8db32b724dc038302bb050177efada8
9
9
  hooks:
10
10
  source_links.rb:
11
- sha256: sha256:574ea0c2b36f80dde410c4251753d70cbeea081d129f3f3547f41370e3990993
11
+ sha256: sha256:5a996b718b1db729305b893ed583b9e600e813570861d47e0b65a24fec36a35d
12
12
  event: after_turn
13
13
  on_error: log
14
14
  priority: 90
@@ -1,11 +1,11 @@
1
1
  ---
2
2
  name: samagotchi-system
3
- version: 0.18.1
3
+ version: 0.19.0
4
4
  scope: system
5
5
  description: Default system memories — identity, self map, config modification protocol, memory guide and the delegated-session rules
6
6
  files:
7
7
  identity.md: sha256:29c2f7df9f6c5acbc5ec17000a4490741e55e426af15ca0d81c8706edd9bd027
8
8
  self_map.md: sha256:69bc0ec091fdaeedf6e3aa7365275461fe3eba32b2dedc9cf45044176c356af5
9
9
  config_modification_protocol.md: sha256:8d8921854e616d840b393cdda6fd911b7d5e8172501bb8293c01f734121ccc21
10
- memory_guide.md: sha256:e535e31fcf29b4b32c270042851ecdd5f412ef2e3c56b0076963e3e1f1a08ef8
10
+ memory_guide.md: sha256:8819678a243590203fa439ad736bf1959af4b4677bc6d2d507bad92c46fb1ccc
11
11
  delegated.md: sha256:0a71d5dfa7127df2d51cb5f0e7214240ed88777e36559430a8724c796d7ae561