samagotchi 0.123.0 → 0.125.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 +48 -1
- data/bin/chi +13 -2
- data/docs/architecture.md +23 -3
- data/docs/cli.md +40 -13
- data/docs/desktop.md +51 -3
- data/docs/internals/participants.md +26 -1
- data/docs/internals/remote-sessions.md +2 -0
- data/docs/sessions.md +2 -2
- data/docs/testing.md +2 -1
- data/lib/samagotchi/bundles/system/manifest.yml +1 -1
- data/lib/samagotchi/desktop/macos/App.swift +9 -1
- data/lib/samagotchi/desktop/macos/ChiRunner.swift +129 -17
- data/lib/samagotchi/desktop/macos/Images.swift +4 -3
- data/lib/samagotchi/desktop/macos/Panel.swift +315 -80
- data/lib/samagotchi/desktop/macos/Sessions.swift +272 -0
- data/lib/samagotchi/process_group.rb +10 -1
- data/lib/samagotchi/reminder_carry.rb +92 -0
- data/lib/samagotchi/reminder_store.rb +49 -0
- data/lib/samagotchi/run_options.rb +43 -2
- data/lib/samagotchi/session_manager.rb +39 -4
- data/lib/samagotchi/sessions_command.rb +1 -1
- data/lib/samagotchi/terminal_ui/attached_loop/composer.rb +225 -0
- data/lib/samagotchi/terminal_ui/attached_loop/exit_commands.rb +138 -0
- data/lib/samagotchi/terminal_ui/attached_loop/questions.rb +225 -0
- data/lib/samagotchi/terminal_ui/attached_loop/queueing.rb +154 -0
- data/lib/samagotchi/terminal_ui/attached_loop/sending.rb +302 -0
- data/lib/samagotchi/terminal_ui/attached_loop.rb +16 -1253
- data/lib/samagotchi/terminal_ui/exit_lines.rb +141 -0
- data/lib/samagotchi/terminal_ui/join_replay.rb +235 -0
- data/lib/samagotchi/update_command.rb +40 -13
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +205 -5
- data/lib/samagotchi/web/participants_admin.rb +15 -4
- data/lib/samagotchi/web/public/annotate_bar.css +8 -4
- data/lib/samagotchi/web/public/annotate_bar.js +34 -6
- data/lib/samagotchi/web/public/app.js +106 -6
- data/lib/samagotchi/web/public/claim_link.js +347 -0
- data/lib/samagotchi/web/public/data.js +17 -0
- data/lib/samagotchi/web/public/index.html +21 -2
- data/lib/samagotchi/web/public/participant.html +1 -0
- data/lib/samagotchi/web/public/participant.js +46 -4
- data/lib/samagotchi/web/public/participants_panel.js +15 -0
- data/lib/samagotchi/web/public/route.js +3 -2
- data/lib/samagotchi/web/public/toasts.js +13 -5
- data/lib/samagotchi/web/public/tokens.css +2 -1
- data/lib/samagotchi/web/public/update_notice.js +6 -6
- data/lib/samagotchi/web/public/update_sheet.js +158 -0
- data/lib/samagotchi/web/public/verify_preset.js +18 -0
- data/lib/samagotchi/web/server.rb +116 -7
- data/lib/samagotchi/web/session_hub.rb +28 -0
- data/lib/samagotchi/web/tunnel/tailscale.rb +8 -2
- data/lib/samagotchi/web/update_check.rb +14 -7
- data/lib/samagotchi/worker.rb +57 -3
- data/lib/samagotchi/worker_idle_exit.rb +7 -6
- metadata +13 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8d36716e229ef5ccac918c840a3b55d878257811aeffdf594d2588c3bf067533
|
|
4
|
+
data.tar.gz: fb632ff225b0a9f5dfbbc869ce8df2842df811b6858caa3d714362879a3e4287
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d16837760eae98a5f26bef1349f70d6fecf80b2400858d2fab12d0a39b11cb5ed125901b21e99bede021585cd9ad60c44adb6a3dad0416a7c4302ebaed459950
|
|
7
|
+
data.tar.gz: 8210416816fda4da83f18801d6e2b991335c41054cfd5459361c9b478d5c633a02ac58855d9f77520d05269c2f64e004cd600c7e56b36ac12d71efe612965430
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,51 @@ and commands may change between minor versions. How releases are made:
|
|
|
7
7
|
[docs/releasing.md](docs/releasing.md).
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.125.0] - 2026-10-27
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- chi web links to a claim: selecting text in a turn shows **Copy link**, a link for a participant's browser that
|
|
15
|
+
opens their page on that turn, scrolled to the quote and highlighted (no token: it opens where their review link
|
|
16
|
+
was opened, one link per browser); with no one invited, a link on your own page, which takes the same anchor.
|
|
17
|
+
- A built-in **Verify** annotate preset on both pages, after `web.annotate_presets`: it quotes the selection and asks
|
|
18
|
+
to verify the claim against the code, ending on `Verdict: confirmed|refuted|unclear — <reason>`.
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
|
|
22
|
+
- `chi sessions restart` (and chi web's restart, its update's worker restarts) keeps a session's reminders: the
|
|
23
|
+
leaving worker hands them to the new one, with their ids and schedules (one that came due meanwhile fires once),
|
|
24
|
+
instead of refusing the restart. A stop, an idle exit, a crash and the TERM fallback for a worker that can't be
|
|
25
|
+
asked still drop them; a worker from an older chi still refuses.
|
|
26
|
+
|
|
27
|
+
### Fixed
|
|
28
|
+
|
|
29
|
+
- `chi sessions stop` (and the web's stop, delete and archive, which stop the same way) stops a stopped (SIGSTOPped,
|
|
30
|
+
Ctrl-Z) worker at once, instead of waiting out the stop's 10 s and leaving it running; a command or background task
|
|
31
|
+
chi stops while it is stopped ends with its TERM, not the KILL after the grace.
|
|
32
|
+
|
|
33
|
+
Update from chi web's Update button (or `chi update`, then restart `chi web` and the session workers with
|
|
34
|
+
`chi sessions restart`). Workers on 0.124.0 still refuse a restart while they hold reminders: stop and resume those
|
|
35
|
+
once; from 0.125.0 on a restart keeps them.
|
|
36
|
+
|
|
37
|
+
## [0.124.0] - 2026-10-27
|
|
38
|
+
|
|
39
|
+
### Added
|
|
40
|
+
|
|
41
|
+
- The desktop helper's panel lists and sends to the sessions of the remotes `chi remote add` keeps: a section per
|
|
42
|
+
remote ("on studio") after this Mac's rows, each row tagged with its machine, "→ studio" in the hint while one is
|
|
43
|
+
ticked, one call per machine at once; a remote that doesn't answer within 4 s or refuses shows one line, and this
|
|
44
|
+
Mac's rows don't wait for it. A remote with a session also gets a "New session on studio in <folder>" row, in its
|
|
45
|
+
newest session's folder, on that remote's default model. The Everyone row, the model chooser and agents stay on
|
|
46
|
+
this Mac.
|
|
47
|
+
- chi web updates chi from its page: the notice's **Update** installs the release it names in a progress sheet
|
|
48
|
+
that lists kept edits, then offers **Restart workers** (each worker on an older chi; a busy one says what holds
|
|
49
|
+
it) and **Restart chi web** (in place: same port, token, flags and Tailscale Serve). `chi update --to VERSION`
|
|
50
|
+
installs an exact release; `--json` prints its rows as JSON.
|
|
51
|
+
|
|
52
|
+
Update with `chi update` (it rebuilds the desktop helper), then restart `chi web` and the session workers
|
|
53
|
+
(`chi sessions restart`). From the next release on, chi web's Update button does this from its page.
|
|
54
|
+
|
|
10
55
|
## [0.123.0] - 2026-10-27
|
|
11
56
|
|
|
12
57
|
### Added
|
|
@@ -4695,7 +4740,9 @@ and long-lived sessions.
|
|
|
4695
4740
|
- A macOS desktop helper (`chi desktop install`): a "Send to chi" Service and a
|
|
4696
4741
|
hotkey panel that send selected text or the clipboard to your sessions.
|
|
4697
4742
|
|
|
4698
|
-
[Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.
|
|
4743
|
+
[Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.125.0...HEAD
|
|
4744
|
+
[0.125.0]: https://github.com/dm1try/samagotchi/compare/v0.124.0...v0.125.0
|
|
4745
|
+
[0.124.0]: https://github.com/dm1try/samagotchi/compare/v0.123.0...v0.124.0
|
|
4699
4746
|
[0.123.0]: https://github.com/dm1try/samagotchi/compare/v0.122.0...v0.123.0
|
|
4700
4747
|
[0.122.0]: https://github.com/dm1try/samagotchi/compare/v0.121.1...v0.122.0
|
|
4701
4748
|
[0.121.1]: https://github.com/dm1try/samagotchi/compare/v0.121.0...v0.121.1
|
data/bin/chi
CHANGED
|
@@ -18,6 +18,15 @@ rescue Samagotchi::ConfigFile::RemovedApi => e
|
|
|
18
18
|
end
|
|
19
19
|
require "samagotchi"
|
|
20
20
|
|
|
21
|
+
# A web self-restart re-enters through the installed wrapper: its marker and
|
|
22
|
+
# the worker starters to adopt leave ENV before anything spawns with it
|
|
23
|
+
# (Server.take_restart_env!); a restarted chi web skips the first-run hint.
|
|
24
|
+
web_restarted = false
|
|
25
|
+
if ENV.key?("SAMAGOTCHI_WEB_RESTARTED") || ENV.key?("SAMAGOTCHI_WEB_ADOPT_PIDS")
|
|
26
|
+
require "samagotchi/web/server"
|
|
27
|
+
web_restarted = Samagotchi::Web::Server.take_restart_env!
|
|
28
|
+
end
|
|
29
|
+
|
|
21
30
|
# `chi self`: print version, source dir, config/memory/session paths, model and
|
|
22
31
|
# bundles (read-only, offline) — lets the agent find its own code and state.
|
|
23
32
|
if ARGV[0] == "self"
|
|
@@ -221,7 +230,7 @@ chi_update_hint = lambda do
|
|
|
221
230
|
end
|
|
222
231
|
|
|
223
232
|
if options[:web]
|
|
224
|
-
chi_update_hint.call
|
|
233
|
+
chi_update_hint.call unless web_restarted
|
|
225
234
|
require "samagotchi/web/server"
|
|
226
235
|
port = options[:web_port] || Samagotchi::Config.get("web.port")
|
|
227
236
|
markdown = options[:web_markdown]
|
|
@@ -233,7 +242,9 @@ if options[:web]
|
|
|
233
242
|
annotate_presets: Samagotchi::Config.get("web.annotate_presets"),
|
|
234
243
|
editor: Samagotchi::Config.get("web.editor"),
|
|
235
244
|
start_dir: options[:start_dir] || Samagotchi::Config.get("web.start_dir"),
|
|
236
|
-
start_dir_from: options[:start_dir] ? :flag : :config
|
|
245
|
+
start_dir_from: options[:start_dir] ? :flag : :config,
|
|
246
|
+
restarted: web_restarted,
|
|
247
|
+
restart_argv: Samagotchi::RunOptions.web_restart_argv(options))
|
|
237
248
|
end
|
|
238
249
|
|
|
239
250
|
# --resume and --attach take a unique id prefix too, like git.
|
data/docs/architecture.md
CHANGED
|
@@ -75,7 +75,7 @@ TerminalUI ── delegates ──▶ Engine
|
|
|
75
75
|
higher-level turn events, so any new UI can render without terminal coupling.
|
|
76
76
|
- **`ComposerRoute`** (`lib/samagotchi/composer_route.rb`) decides where a line typed at
|
|
77
77
|
either terminal's prompt goes (the REPL's `steer_line` / `run_input_line`, the attached
|
|
78
|
-
`AttachedLoop#submit`): `/cut TEXT` / `/queue TEXT`, a terminal's own word, a session
|
|
78
|
+
`AttachedLoop::Composer#submit`): `/cut TEXT` / `/queue TEXT`, a terminal's own word, a session
|
|
79
79
|
command, a typo, a prompt. Each UI keeps what it does with the route. The web's
|
|
80
80
|
`composer.js#routeComposerLine` decides the same; `spec/fixtures/composer_routes.json`
|
|
81
81
|
holds the cases both answer (see [testing.md](testing.md)).
|
|
@@ -171,7 +171,9 @@ agent logic and can be used without any terminal rendering; the UI is a thin lay
|
|
|
171
171
|
activity slot gets a second, dim row: the turn's tool tally (`TurnTally`, seeded from the
|
|
172
172
|
snapshot's tool parts on a mid-turn join). Reline still reads the input,
|
|
173
173
|
but `RelineSeam` (prepended to `Reline::LineEditor`) sends its drawing to the
|
|
174
|
-
`Screen`.
|
|
174
|
+
`Screen`. The saved history and cards in a join or resync are rendered by
|
|
175
|
+
`TerminalUI::JoinReplay` with the loop's screen, view, link, gap clock and renderer.
|
|
176
|
+
Without a capable terminal it falls back to `PlainSurface` (append-only).
|
|
175
177
|
|
|
176
178
|
#### Using the core
|
|
177
179
|
|
|
@@ -551,6 +553,24 @@ version than the server itself still means restart chi web (its workers already
|
|
|
551
553
|
web runs in) it names `start_dir`, the folder it starts in (`web.start_dir`, `--start-dir`;
|
|
552
554
|
`Web::StartDir`), which a second `chi web` given another one mentions.
|
|
553
555
|
|
|
556
|
+
The owner's update routes publish progress and results as `chi_update` events with an
|
|
557
|
+
`operation` (`install` or `workers`), `state`, and `rows`; install events also name the target
|
|
558
|
+
`version` and may include a progress line. The hub retains the latest event for a reconnecting
|
|
559
|
+
tab's snapshot. `POST /api/update` decides itself whether this install can take a gem
|
|
560
|
+
(`UpdateCheck.blocked_reason`: a checkout, Bundler, an unwritable gem dir → 409 `update_blocked`), so
|
|
561
|
+
it holds with `update.notify: off`; a child that prints no rows fails with its own last line, and a
|
|
562
|
+
done install refreshes the hub's installed version (`SessionHub#refresh_installed`) before its `done`.
|
|
563
|
+
Worker restarts target every worker older than the version installed now (refreshed the same way)
|
|
564
|
+
and publish per-session results. Only one of an install, a worker restart pass and a web restart runs
|
|
565
|
+
at a time (409 `update_running`). `POST /api/update/restart_web` (409 `restart_unavailable` without a
|
|
566
|
+
chi wrapper to exec) schedules WEBrick shutdown only after its response is sent; after the hub and
|
|
567
|
+
listener stop, `Server` execs the RubyGems chi wrapper with every flag chi web was started with, as
|
|
568
|
+
`RunOptions`' parser recorded them (`RunOptions.web_restart_argv`), but the one-shot `--open` and
|
|
569
|
+
`--new-token`. The re-entry (`SAMAGOTCHI_WEB_RESTARTED`, taken out of ENV first by
|
|
570
|
+
`Server.take_restart_env!`) skips the first-run update hint, adopts live worker-starter PIDs (workers
|
|
571
|
+
themselves are detached), and adopts an exact Tailscale Serve handler. A failed exec, or a restarted
|
|
572
|
+
chi web that doesn't start for any reason, turns off only that exact adopted handler.
|
|
573
|
+
|
|
554
574
|
The page's scope is in its URL, and the server picks the default: `?dir=<folder>` is that folder's
|
|
555
575
|
project (the list, the events stream and a new chat follow it), `?scope=all` every session, and a bare
|
|
556
576
|
`/` the start folder's project when that folder is in a repo (`App#index_data_attributes` writes
|
|
@@ -786,4 +806,4 @@ A map of the parts the overview above doesn't name (paths under `lib/samagotchi/
|
|
|
786
806
|
| Stores | `reminder_store.rb` (`ReminderStore`), `recap_store.rb` (`RecapStore`) | A session's reminders, and its idle recap (`recap.json`). |
|
|
787
807
|
| Setup and update | `bootstrap_command.rb`, `bootstrap/`, `update_command.rb`, `gem_update.rb` | `chi bootstrap` (first config.yml, system bundle, core profile) and `chi update` (the gem, shipped bundles, the macOS helper). |
|
|
788
808
|
| Process groups | `process_group.rb` (`ProcessGroup`) | A command in a process group of its own, stopped as a group (TERM, grace, KILL): execute, tasks, context fetches, the mcp bundle's servers. |
|
|
789
|
-
| Desktop | `desktop.rb`, `desktop/macos.rb`, `desktop_command.rb`, `desktop/macos/*.swift` | `chi desktop`, the macOS "Send to chi" helper ([desktop.md](desktop.md)). The helper's Swift side: the panel (`Panel.swift`), the chi CLI runner (`ChiRunner.swift`), agent terminals behind `AgentTerminal` (`AgentTerminal.swift`, `Kitty.swift`, `Tmux.swift`; `ChiHelper --agents` is their CLI seam). |
|
|
809
|
+
| Desktop | `desktop.rb`, `desktop/macos.rb`, `desktop_command.rb`, `desktop/macos/*.swift` | `chi desktop`, the macOS "Send to chi" helper ([desktop.md](desktop.md)). The helper's Swift side: the panel (`Panel.swift`), the chi CLI runner (`ChiRunner.swift`), the session rows and the chi calls a send makes (`Sessions.swift`: `SendPlan`; `ChiHelper --sessions` and `--send-plan` are their CLI seams), agent terminals behind `AgentTerminal` (`AgentTerminal.swift`, `Kitty.swift`, `Tmux.swift`; `ChiHelper --agents` is their CLI seam). |
|
data/docs/cli.md
CHANGED
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
- `chi desktop install|upgrade|uninstall|status` — the macOS "Send to chi" helper: a Service and a ⌃⌥⌘N hotkey that send text or images to a session (live, stopped or new) as a message (⏎, a turn runs) or a context note (⌘⏎) (see [Desktop helper](desktop.md))
|
|
35
35
|
- `chi models [--format text|json] [--timeout S] [TEXT]` — list the models every configured host offers, as the names `--model` takes (see [Listing the models](#listing-the-models))
|
|
36
36
|
- `chi self` — print version, source dir (checkout or installed gem), config/memory/session paths, model/host and bundles. Run by a session's `execute`, the `model` row (and host, loop, profile, thinking, served model) is **that session's** model, labelled `(this session <id8>; default …)`; elsewhere it is the default, `(default)`. `model key` is the memory overlay key (`<name>.<key>.md`), and `model notes` the model notes a prompt of that model loads (`model_notes_deepseek (system, 612 chars)`, `none`; inside a session its `--mute` list is left out). `chi self --model` prints only the model a new session starts on, an alias resolved to its target (the desktop helper's hint)
|
|
37
|
-
- `chi update [--dry-run] [--no-gem] [--no-bundles] [--no-desktop]` — update an installed chi: the gem, the system bundle, the shipped bundles you installed and the desktop helper, in one table (see [Updating](#updating))
|
|
37
|
+
- `chi update [--dry-run] [--to VERSION] [--json] [--no-gem] [--no-bundles] [--no-desktop]` — update an installed chi: the gem, the system bundle, the shipped bundles you installed and the desktop helper, in one table (see [Updating](#updating))
|
|
38
38
|
- `chi bundle install|upgrade|uninstall|status|diff|list|build|trash|new|test|check` — manage memory bundles (see [Bundle hooks](hooks.md#bundle-hooks-unified-workflow-bundle)); `list` shows the installed ones and the ones shipped with chi, which `install <name>` installs; `core` and `dev` are profiles that install a set of them (see [Bundle profiles](memory.md#bundle-profiles-core-and-dev), [Guardrails](guardrails.md), [Plugins](plugins.md#the-btw-bundle), [the mcp bundle](plugins.md#the-mcp-bundle) [the loop-guard bundle](plugins.md#the-loop-guard-bundle), [the check-in bundle](plugins.md#the-check-in-bundle) and [the skills bundle](plugins.md#the-skills-bundle)). `trash` lists and empties the bundle trash (moved files from uninstalls/upgrades). A usage error (an unknown subcommand or flag, a missing argument, a bad `build --scope`) exits 2, as for the other subcommands. Plain `chi` and `chi web` exit 1 on an unknown flag, a flag missing its value or a stray argument. `new`, `test` and `check` are for writing one: see [Writing a bundle](#writing-a-bundle-new-test-check)
|
|
39
39
|
|
|
40
40
|
- `chi guardrails replay [--dry-run] [--threshold T] [--model M]` — seed System 1's record from the approvals this machine's sessions already answered: each ask is judged by the configured model and written to `guardrails/judgments.jsonl` with the answer the user gave, then a per-rule table prints (asks, allowed, denied, would-be auto-allows, false allows, asks with no judgment). A parent agent's answer is not the user's: only its deny is replayed, and only where the parent could have allowed. `--dry-run` judges without writing, `--threshold` overrides `guardrails.system_one.threshold` for the table, `--model` the configured model for this run. Exit 2 when `guardrails.system_one.mode` is off or no `host` is set; a usage error exits 2 (see [System 1](guardrails.md#system-1-experimental))
|
|
@@ -233,13 +233,26 @@ Also shipped, not installed: dev (mcp, skills, source-links) (chi bundle install
|
|
|
233
233
|
done
|
|
234
234
|
```
|
|
235
235
|
|
|
236
|
+
- `--to VERSION` installs exactly that gem version instead of checking for the
|
|
237
|
+
newest release. It must be newer than the running chi; an equal or older
|
|
238
|
+
version is refused.
|
|
239
|
+
- `--json` writes the component rows as one JSON array on stdout. Each row has
|
|
240
|
+
`component`, `from`, `to`, `status`, `failed` and `kept` fields; `kept` is an
|
|
241
|
+
array of edited filenames. Install progress is written to stderr.
|
|
242
|
+
|
|
236
243
|
- **chi web update notices.** By default, chi web checks RubyGems about 30
|
|
237
244
|
seconds after it starts and every 24 hours after that. Once a stable release
|
|
238
245
|
is at least 24 hours old and newer than both the running chi and the newest
|
|
239
|
-
installed one, the page shows its version
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
246
|
+
installed one, the owner's page shows its version and a link to that
|
|
247
|
+
RubyGems release. When the install is unblocked, **Update** installs that
|
|
248
|
+
exact release and opens a progress sheet with component results and any
|
|
249
|
+
edited files kept. A blocked install instead shows the reason and a command
|
|
250
|
+
to copy. After installation, the notice and the sheet's footer offer
|
|
251
|
+
**Restart workers** and **Restart chi web**; the sheet reports worker results
|
|
252
|
+
(a busy worker's refusal says what holds it) and waits for chi web to
|
|
253
|
+
reconnect, and keeps the other restart in reach. The notice is once per version per browser: a reload or a chi
|
|
254
|
+
web restart doesn't repeat it. `update.notify: off` disables checking;
|
|
255
|
+
`update.check_hours` and
|
|
243
256
|
`update.min_age_hours` tune the interval and release age (see
|
|
244
257
|
[configuration](configuration.md#all-settings)). If the gem can't be updated
|
|
245
258
|
in place, the notice gives the blocker and its command instead.
|
|
@@ -528,14 +541,15 @@ terminal and the Web UI say so, with that restart line.
|
|
|
528
541
|
`chi sessions restart ID...` hands each session to a new worker on the newest
|
|
529
542
|
chi installed, without stopping it: an attached terminal and the web's tabs
|
|
530
543
|
move to the new worker. It is refused, with the reason, while something would
|
|
531
|
-
be lost: a turn running or queued, a question or approval waiting,
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
544
|
+
be lost: a turn running or queued, a question or approval waiting, a `/btw`
|
|
545
|
+
still running, an approval relayed from a delegate, or background tasks the
|
|
546
|
+
session started. Its reminders move to the new worker with their schedules
|
|
547
|
+
(a worker from an older chi still refuses for them). If the worker cannot be
|
|
548
|
+
asked (a refused Bridge, protocol mismatch or stuck start), chi sends it TERM
|
|
549
|
+
and resumes only when disk shows no running turn, pending question or
|
|
550
|
+
approval, queued prompt, or running background task; otherwise it says to run
|
|
551
|
+
`chi sessions stop ID`, then send any prompt. That fallback drops reminders,
|
|
552
|
+
and may lose a running `/btw` and other in-memory state. A session with no running worker needs no restart (its next
|
|
539
553
|
prompt starts one on the newest chi), and an older worker without restart
|
|
540
554
|
support needs `chi sessions stop ID`. The Web UI's
|
|
541
555
|
`POST /api/sessions/:id/restart` does the same (409 with `reason` and `detail`
|
|
@@ -1194,6 +1208,17 @@ their comment box, labelled `From docs/plan.md:` or `From Sam's comment:`,
|
|
|
1194
1208
|
with the same presets. Not in the owner's participant view, and no plugin
|
|
1195
1209
|
pills there (a review turn refuses `/` lines).
|
|
1196
1210
|
|
|
1211
|
+
After the presets, both pages have a built-in **Verify**: the quote, then
|
|
1212
|
+
an ask to check the claim against the code and end on a line `Verdict:
|
|
1213
|
+
confirmed|refuted|unclear — <reason>`. On your page a selection in a turn
|
|
1214
|
+
also offers **Copy link**: a link to that turn and quote for a participant's
|
|
1215
|
+
browser (the one their review link was opened in), which opens their page
|
|
1216
|
+
scrolled to the quote and highlights it; with no one invited, a link to it
|
|
1217
|
+
on your own page. A secret in the selection goes into the link as
|
|
1218
|
+
`[redacted]`, as the participant's page shows it. One browser holds one participant link at a time, and
|
|
1219
|
+
opening a participant's invite link yourself swaps your own sign-in for it
|
|
1220
|
+
(see [participants.md](internals/participants.md#what-a-participant-sees)).
|
|
1221
|
+
|
|
1197
1222
|
The list is `web.annotate_presets`, `|`-separated (at most five; a preset
|
|
1198
1223
|
can't contain `|`; a YAML list works too):
|
|
1199
1224
|
|
|
@@ -1325,6 +1350,8 @@ CHI_REMOTE=studio chi context push claude-outbox ab12 < answer.json
|
|
|
1325
1350
|
studio (http://192.168.1.29:4567): connection refused`, exit 1; with
|
|
1326
1351
|
`--format json` an `error` object).
|
|
1327
1352
|
- Plain http, the same as the phone's: use it on your home network.
|
|
1353
|
+
- The macOS desktop helper's panel lists and sends to a remote's sessions too: see
|
|
1354
|
+
[Desktop helper: Remote sessions](desktop.md#remote-sessions).
|
|
1328
1355
|
|
|
1329
1356
|
A session there in this terminal (the attached TUI, through that machine's
|
|
1330
1357
|
chi web; its worker stays there):
|
data/docs/desktop.md
CHANGED
|
@@ -41,7 +41,7 @@ alias as the model it names).
|
|
|
41
41
|
|
|
42
42
|
### Everyone it concerns
|
|
43
43
|
|
|
44
|
-
The second row, **Everyone it concerns** (⌘B), shares a note with every session it may concern instead of the ones
|
|
44
|
+
The second row, **Everyone it concerns (this Mac)** (⌘B), shares a note with every session it may concern instead of the ones
|
|
45
45
|
you pick: ⌘⏎ (the **Broadcast** button) runs [`chi broadcast`](broadcast.md) with the line, then the text, on stdin.
|
|
46
46
|
chi picks the sessions (a shared ticket, PR or link, else a small triage model), so the preselected session doesn't
|
|
47
47
|
count: the row is ticked alone, like the new row. Write it the way you'd tell a colleague: a first line such as
|
|
@@ -51,6 +51,8 @@ count: the row is ticked alone, like the new row. Write it the way you'd tell a
|
|
|
51
51
|
skipped 2 · 1 unchecked: triage deadline`), and **stays open**: Esc closes. Which sessions got it and why:
|
|
52
52
|
`chi broadcast log` in a terminal.
|
|
53
53
|
- A broadcast is a text note: ⏎ and images beep. The source field and Send are hidden (its source is `broadcast`).
|
|
54
|
+
- It reaches this Mac's sessions only (`chi broadcast` runs on this machine only), never a
|
|
55
|
+
[remote's](#remote-sessions).
|
|
54
56
|
- It may take `broadcast.triage_deadline` (20 s) and 10 s more before the helper stops it ("the note may be partly
|
|
55
57
|
delivered"). The helper reads that from its launch file: after changing the deadline, run `chi update` (or
|
|
56
58
|
`chi desktop upgrade`).
|
|
@@ -106,6 +108,46 @@ thumbnails sit under the message line, 48 px high, the file name as a tooltip; h
|
|
|
106
108
|
the panel closes; files from Finder are sent as they are, never touched. A send with images may take up to 30 s
|
|
107
109
|
(converting, a worker starting) before the panel gives up.
|
|
108
110
|
|
|
111
|
+
## Remote sessions
|
|
112
|
+
|
|
113
|
+
The panel also lists the sessions of the other machines `chi remote add` keeps (see
|
|
114
|
+
[Remote sessions](cli.md#remote-sessions)), say the Studio's from a laptop: select text or take a screenshot here,
|
|
115
|
+
and it lands in a session running there.
|
|
116
|
+
|
|
117
|
+
- **Which:** every remote in `chi remote ls` that has a token, asked at each open (a remote you don't want in the
|
|
118
|
+
panel: `chi remote rm` it). No `chi update` or `chi desktop upgrade` after adding one.
|
|
119
|
+
- **Where:** after this Mac's rows, one section per remote under an **on studio** divider: its live sessions, then up
|
|
120
|
+
to 3 recent ones, as for this Mac. Each row carries a grey **studio** tag and its whole folder path (that machine's
|
|
121
|
+
home may differ from this one's). ⌘1…⌘9 number this Mac's rows first, then the remotes' in order; past nine, a row
|
|
122
|
+
is clicked.
|
|
123
|
+
- **Not twice:** a session shows once, where it is listed first (this Mac, then the remotes in `chi remote ls` order),
|
|
124
|
+
so two remotes for the same Studio (its LAN address and its ts.net name), or a remote that is this Mac, don't
|
|
125
|
+
list or send to it twice.
|
|
126
|
+
- **Wrong machine:** both machines likely have the same paths. While a remote's row is ticked the message line's hint
|
|
127
|
+
ends in **→ studio**, and **→ studio** shows beside Send (it stays there while you type). The last choice is
|
|
128
|
+
preselected while it is still live, a remote's row too; the "only one live session" rule picks this Mac's only,
|
|
129
|
+
never a remote's. A remote that answers after this Mac's list never changes what is ticked: it is preselected only
|
|
130
|
+
when nothing is (so a remembered remote row is ticked when the remote answers first, or when this Mac has live
|
|
131
|
+
sessions but none was remembered).
|
|
132
|
+
- **Keys:** ⏎ and ⌘⏎ work as for this Mac's sessions (`chi --remote studio send …`, `chi --remote studio note …`;
|
|
133
|
+
a note to a stopped session there waits for its next start). Images go along (`--image`: chi uploads them). With
|
|
134
|
+
rows of several machines ticked, the helper runs one call per machine at once and shows one line: `sent · studio:
|
|
135
|
+
sent`. ⌥⏎ (paste) beeps on a remote's row as on this Mac's.
|
|
136
|
+
- **Down or refused:** this Mac's rows show as soon as they are in; each remote's fill in as it answers. A remote's
|
|
137
|
+
two lists run at once and get 4 s together: one that doesn't answer shows **studio: no answer within 4 s** under
|
|
138
|
+
its divider, one that chi gives up on by itself (connection refused, the token refused) the first line of chi's
|
|
139
|
+
error. The previous open's rows for it are never reused: a stale row could send to a stopped worker.
|
|
140
|
+
- **New session there:** a remote with a session has its own new row under its divider, **New session on studio in
|
|
141
|
+
proj**: the folder is that remote's most recently updated session's (live first, then recent), whole and not
|
|
142
|
+
checked on this Mac. A remote with no session has no new row (never a guess at its home). It is ticked alone like
|
|
143
|
+
this Mac's new row (ticking it clears the sessions, and the reverse), is never preselected and has no ⌘ number (⌘0
|
|
144
|
+
stays this Mac's). ⏎ runs `chi --remote studio send --new --dir <that folder>` with the message and images, and
|
|
145
|
+
shows `studio: started 1a2b3c4d…` for 3 s. It starts on the remote's default model, never `--model`: its hint
|
|
146
|
+
says **remote default**, and ⌘M or a click on it beeps with "Model choice is for this Mac's sessions" (`chi models`
|
|
147
|
+
answers for this Mac's hosts).
|
|
148
|
+
- **What stays on this Mac:** the model chooser, the **Everyone it concerns** row (`chi broadcast` is local only)
|
|
149
|
+
and the agents in kitty and tmux.
|
|
150
|
+
|
|
109
151
|
## Agents in kitty and tmux
|
|
110
152
|
|
|
111
153
|
The panel can also send to agent CLIs (claude, codex, gemini, aider, …) running in
|
|
@@ -276,8 +318,14 @@ The helper uses only these commands, so it could ship on its own later:
|
|
|
276
318
|
(`delivered N · skipped M…`); exit 0 when it ran, 1 when refused or a delivery failed.
|
|
277
319
|
- `chi self --model` → the model a new session starts on (the panel's hint).
|
|
278
320
|
- `chi models --format json` → the models the hosts offer (the new-session model picker).
|
|
279
|
-
|
|
280
|
-
|
|
321
|
+
- `chi remote ls --format json` → `[{name, url, token}]` (never the token itself): the remotes whose sessions the
|
|
322
|
+
panel lists. A name goes on only when it has a token and passes chi's own rule (letters, digits, `_`, `-`, not
|
|
323
|
+
starting with `-`). An older chi without `chi remote` lists none.
|
|
324
|
+
- `chi --remote NAME sessions list …`, `chi --remote NAME send …` and `chi --remote NAME note …`: the same calls on a
|
|
325
|
+
remote's sessions. `CHI_REMOTE` is taken out of the helper's environment, so this Mac's calls never go remote.
|
|
326
|
+
|
|
327
|
+
Each call is stopped after 10 s, a remote's two lists after 4 s, a send of text to a remote after 15 s (waking a
|
|
328
|
+
stopped worker there), a send with images after 30 s, a broadcast after the launch file's
|
|
281
329
|
`broadcast_timeout` (`broadcast.triage_deadline` + 10 s; 30 s when it has none). A stopped `chi note` or `chi
|
|
282
330
|
broadcast` says the note may be partly delivered.
|
|
283
331
|
|
|
@@ -389,6 +389,31 @@ fields: a field added to an event later reaches nobody until it is named there.
|
|
|
389
389
|
header, the Plan's truncation note. No plugin pills (a review turn refuses `/` lines), and none of it in the owner's
|
|
390
390
|
participant view (no presets in its shell) or while `can_comment` is false. On a touch screen the bar docks above the
|
|
391
391
|
composer. A re-read keeps unchanged thread nodes and the Plan's (redrawn only on change), so a selection survives it.
|
|
392
|
+
After the presets comes the built-in **Verify** (`verify_preset.js`, on the owner's page too): the quote, then
|
|
393
|
+
"Verify this claim against the code. If this session's context doesn't settle it, say so. End with a line
|
|
394
|
+
`Verdict: confirmed|refuted|unclear — <one-line reason>`." It fills the box like any preset and never sends.
|
|
395
|
+
- **Deep links to a claim** (`claim_link.js`). The owner selects text in a turn's answer, step or own message and
|
|
396
|
+
Annotate's **Copy link** copies `<base>/#s=<id>&t=<turn8>&q=<quote>&p=<prefix>`: `base` the participants list's
|
|
397
|
+
`link_base` (`ParticipantsAdmin#participant_base`: `web.public_url`'s origin, else the LAN address, else the Host),
|
|
398
|
+
`t` the turn's id (its first 8 characters, the opening message's `data-turn-id`), `q` the selection's rendered
|
|
399
|
+
text (whitespace collapsed, at most 200 characters) and `p` up to 32 characters before it in the same bubble, both
|
|
400
|
+
URL-encoded by `URLSearchParams`. The text is read as the participant page shows it, by `Redact`'s own rules
|
|
401
|
+
mirrored in JS (`spec/fixtures/redact_cases.json` holds cases both sides must read the same): secret shapes
|
|
402
|
+
`[redacted]`, whole even when the selection cuts into one, so a link never carries a secret; on the owner's page
|
|
403
|
+
also the session's folder relative and the home folder `~` (the page's `data-home`). Not the owner's token (the
|
|
404
|
+
page doesn't hold it). With no person (`kind: human`) active in the session, Copy link gives the owner's own form,
|
|
405
|
+
`#/s/<id>?t=…&q=…&p=…`; the owner's page also opens the participant form. Opening one: the turn's bubbles
|
|
406
|
+
(`.p-msg > .p-bubble`; the owner's answers, own messages and steps) are searched for the prefix and quote, then
|
|
407
|
+
the quote alone; the match's bubble is ringed (`.claim-hit`), a closed `<details>` around it opened, and the quote
|
|
408
|
+
highlighted (the CSS Highlight API, `::highlight(chi-claim)`, where the browser has it). A quote not found (text
|
|
409
|
+
that reads differently) rings the turn's opening. A turn no opening carries (a non-person turn the page doesn't
|
|
410
|
+
stamp) is searched for in the whole page; neither found (one from before a continue) leaves the page at the top and
|
|
411
|
+
says so. The hash then drops the claim (`#s=<id>`, `#/s/<id>`), so the same link opened again shows it again. The
|
|
412
|
+
link holds no token, so it opens only in a browser that already redeemed a grant for that session; a link for
|
|
413
|
+
another session says this browser holds the link for this one. Its limits: **one grant per browser** (the grant
|
|
414
|
+
cookie is one name: a second participant link redeemed there replaces the first), and **the owner opening a
|
|
415
|
+
participant's invite link** in their own browser swaps their owner cookie for that grant (the page asks first). A
|
|
416
|
+
continue archives the session and revokes its grants: re-share, and links to the old session stay on it.
|
|
392
417
|
- **The comment box** is the owner's composer (`composer_mount.js`, styled by the shared `composer.css`), mounted by
|
|
393
418
|
`participant.js` with a subset on. It has: the height that grows with the text, the grip (drag to resize,
|
|
394
419
|
double-click resets, click or drag down folds it; folded, the grip is the handle on the dock's edge and typing on the
|
|
@@ -468,7 +493,7 @@ gate and routes) answers these.
|
|
|
468
493
|
|
|
469
494
|
| Route | Body / params | Answer | Refusals |
|
|
470
495
|
|---|---|---|---|
|
|
471
|
-
| `GET /api/sessions/:id/participants` | | 200 `{participants: [grant], warning, identity_header}`, every state; `warning`: the share warning for a session that isn't a review session (`ReviewStart.share_warning`, null for one), shown before the panel's Invite with Start a review of it; `identity_header`: the header this chi web's proxy names the user in (null without one), the Invite's email field | |
|
|
496
|
+
| `GET /api/sessions/:id/participants` | | 200 `{participants: [grant], warning, identity_header, link_base}`, every state; `warning`: the share warning for a session that isn't a review session (`ReviewStart.share_warning`, null for one), shown before the panel's Invite with Start a review of it; `identity_header`: the header this chi web's proxy names the user in (null without one), the Invite's email field; `link_base`: where a participant's link opens, no token (a claim link's base) | |
|
|
472
497
|
| `GET /api/participants` | | 200 `{participants: [grant]}`, every session's | |
|
|
473
498
|
| `POST /api/sessions/:id/participants` | `name`, `kind` (`human`/`agent`), `ttl` (seconds), `cap`, `identity` (an email: a bound grant) | 201 `{participant: grant, token, link, lan, qr_svg, warning}`: the token and the link once; `warning` for a session that isn't a review session (`ReviewStart.share_warning`, null for one); `link` names this chi web's LAN address (`http://IP:PORT/?token=…#s=<id>`), `lan` false when it has none | 400 `invalid_participant` (name, kind, ttl, an identity that isn't one address or with no proxy to check it, an archived session); 404 |
|
|
474
499
|
| `GET /api/sessions/:id/apply[?turn=T]` | `turn`: a turn id or a prefix (none: the latest review turn with a reply) | 200 `{text, turn_id, author, author_name}`: the Apply turn for the composer, never sent | 404 `not_found` (no such turn, no review turn), 400 `not_review_turn` (an owner's turn), 409 `no_reply` |
|
|
@@ -268,6 +268,8 @@ with a "Machine-local" comment; the rest branch on `@access.remote?` (`SendComma
|
|
|
268
268
|
`spec/terminal_ui/remote_connector_spec.rb`, `spec/remote_attach_command_spec.rb`,
|
|
269
269
|
`spec/session_access/remote_attach_spec.rb` (wake, worker, feature check through `RackTransport`), and bin/chi's
|
|
270
270
|
branch in `spec/session_access_dispatch_spec.rb`.
|
|
271
|
+
The saved-history rendering is the same `TerminalUI::JoinReplay` as local attaches; its saved-record reads use
|
|
272
|
+
the injected link.
|
|
271
273
|
- By hand: the smoke-run skill (`.claude/skills/smoke-run`, kept in the checkout, not in git) covers chi web; its
|
|
272
274
|
cli-tui recipe has remote attach on one machine (two isolated envs, `chi remote add self http://<LAN IP>:PORT`, the
|
|
273
275
|
terminal's commands through chi web). The real lane is laptop → Studio over the LAN: `chi remote add`, `send --new --wait`,
|
data/docs/sessions.md
CHANGED
|
@@ -19,7 +19,7 @@ Sessions are plain files — no DB. Each session is `~/.local/state/samagotchi/s
|
|
|
19
19
|
|
|
20
20
|
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.
|
|
21
21
|
|
|
22
|
-
**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, no background task it watches for a [finish wake](internals/background-tasks.md#finish-wake) still running or waiting for its wake turn (with `session.task_wakes: wake`; not one whose turn you stopped) 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`). As its Bridge stops, the worker marks the lock leaving (`OwnerLock#mark_leaving`: `leaving_at`, and `leave_wait`, its `recap.timeout`), since it keeps the lock while it writes the recap on leave: an `--attach` or `--resume` meanwhile (here, or a remote one through chi web's wake) prints `(waiting for the session to finish its recap…)` (`LaunchMode::RECAP_WAIT`) and waits for the release, up to that budget plus `OwnerLock::LEAVE_MARGIN` (5 s), before starting the new worker (`OwnerLock.await_release`). A lock held by a worker that isn't leaving (a live one, a stuck one) is left as before. 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 (for background tasks, which ones and how to stop them). When every hold is one the user can stop — reminders, a watched task still running, a delegate with a live worker — the 409 also carries `holds: [{kind:, items: [{id:, label:, detail:}]}]` (a reminder's detail is its schedule, `every 20m`/`every 1h`, `, due now` when it is already due), and an attached terminal asks what to do with them; its "stop them and exit" posts `{"stop": "all"}`, which clears the reminders, stops those tasks (`TaskRuntime.stop_task`, as `task_stop`) and stops those children (`SessionManager.stop_session`) before leaving, re-checking first that nothing the user can't stop appeared meanwhile (`HoldStop`). A delegate counts only for that ask: it never holds the idle exit, and an exit that deletes or archives the session does not ask about it. 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,
|
|
22
|
+
**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, no background task it watches for a [finish wake](internals/background-tasks.md#finish-wake) still running or waiting for its wake turn (with `session.task_wakes: wake`; not one whose turn you stopped) 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`). As its Bridge stops, the worker marks the lock leaving (`OwnerLock#mark_leaving`: `leaving_at`, and `leave_wait`, its `recap.timeout`), since it keeps the lock while it writes the recap on leave: an `--attach` or `--resume` meanwhile (here, or a remote one through chi web's wake) prints `(waiting for the session to finish its recap…)` (`LaunchMode::RECAP_WAIT`) and waits for the release, up to that budget plus `OwnerLock::LEAVE_MARGIN` (5 s), before starting the new worker (`OwnerLock.await_release`). A lock held by a worker that isn't leaving (a live one, a stuck one) is left as before. 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 (for background tasks, which ones and how to stop them). When every hold is one the user can stop — reminders, a watched task still running, a delegate with a live worker — the 409 also carries `holds: [{kind:, items: [{id:, label:, detail:}]}]` (a reminder's detail is its schedule, `every 20m`/`every 1h`, `, due now` when it is already due), and an attached terminal asks what to do with them; its "stop them and exit" posts `{"stop": "all"}`, which clears the reminders, stops those tasks (`TaskRuntime.stop_task`, as `task_stop`) and stops those children (`SessionManager.stop_session`) before leaving, re-checking first that nothing the user can't stop appeared meanwhile (`HoldStop`). A delegate counts only for that ask: it never holds the idle exit, and an exit that deletes or archives the session does not ask about it. 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, 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`). Reminders go along: as it leaves, the worker writes them to `<id>/reminders.json` (0600: `session_id`, `written_at`, and each reminder's id, name, description, interval and next fire as wall-clock time; `ReminderCarry`), and its successor reads and deletes the file as it starts, before its idle scheduler runs. A restored reminder keeps its id and schedule; one that came due meanwhile fires once at once, and one further ahead than its interval (a clock that jumped) is due one interval from now. The file counts only for the same session within 2 minutes of `written_at`, and not once the session is stopped: otherwise it is deleted unread (log `reminders_carry_dropped`, at warn unless the session was stopped). A stop that lands while the worker leaves for a restart wins: the worker writes no file, and no successor starts (`SessionManager.run_session_loop`, log `restart_dropped`); an archive deletes the file of each session it archives. Once an exit or restart is taken, a later exit request (a stop-all from another terminal) is answered as that same exit: the first one decides how the worker leaves. A write that fails keeps the worker up with its reminders (log `reminders_carry_failed`), and the caller reports no new worker. A reminder already due when the restart is asked runs its turn first, so the restart's wait (10 s) may report no new worker yet though the hand-off follows. A worker from before this still refuses a restart for its reminders (409 `reminders`). Only an asked restart carries them: the fallback's TERM (a worker that can't be asked, below), a stop, an idle exit (which reminders hold anyway) and a crash drop them as before. 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.
|
|
23
23
|
|
|
24
24
|
**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 is empty: it will be discarded if nothing arrives before its worker stops.` (a note or prompt landing first keeps it). 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.
|
|
25
25
|
|
|
@@ -33,7 +33,7 @@ A session is deleted if **expired by age OR overflow by count** (unless `keep_st
|
|
|
33
33
|
|
|
34
34
|
**A worker killed mid-turn:** while a turn runs, its worker keeps it on disk (`TurnCheckpoint`): the session file has the turn so far (the prompt from the turn's start, then the finished tool steps and lines merged into it) and `interrupted_turn: {id, started_at, at, iteration, enqueued_id}` (`enqueued_id`: the input the turn runs, so any process can tell which turn is running; a continue turn carries its offer turn's), written as the turn starts, at the loop's first iteration boundary and then at most every 10 s (`updated_at` moves with it). The turn's end saves the real messages without the mark. A worker that died without ending its turn (SIGKILL, a crash of the process) leaves the mark, and loses what came after its last written boundary: at most the last 10 s of steps, plus the step in flight. Each checkpoint write also saves the turn's metrics so far to `analytics.json` (tokens, speeds, timings, its finished tool calls), as the record a killed turn leaves: canceled, `cancellation_reason: worker_stopped`; the turn's own end writes over it. The next worker to start for the session (a `chi send`, `--resume`, `--attach`, the web), or a `--no-shared` REPL resuming it, settles the turn before anything else (`TurnCheckpoint.settle`, log `interrupted_turn_settled`): a note `the previous turn was cancelled (worker-stopped) after Ns` goes at the tail, the status goes idle and the mark goes; `last_turn` stays the previous turn's (a new one saved before the turn a `chi send --wait` woke the worker for would end that wait at once); the dead turn's end is recorded under its `enqueued_id` instead, in `<id>/input/settled/<enqueued_id>` (a `last_turn` record: canceled, `cancel_reason: worker_stopped`), which only a wait for that very turn reads (`chi send --queue --wait`, a `chi send --wait -m` to an idle session or `--new`, `chi send --wait ID --turn`, a continue's wait): it ends `canceled` with `cancel_reason: worker_stopped`, detail "its worker exited mid-turn", instead of timing out. A wait with no turn id that saw the turn running ends the same way when the settle comes (`chi send --wait ID` with no message on a turn with no id, such as a session's first prompt or a reminder turn; a message merged into the turn as a steer), unless an input still waits or was claimed since: an unmerged message runs as its own turn next, and that turn's end is the answer. A reply the turn wrote before its worker died is still its answer. The session shows the turn's prompt and steps with that note; the dead turn is not run again, and the model reads on its next turn that it was cut.
|
|
35
35
|
|
|
36
|
-
**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`. A running turn is canceled over the Bridge first, with reason `session_stopped` (the turn's line ends "(session stopped)", its note says `cancelled (session-stopped)`, and a command it was running says "stopped as the session was stopped"; a bare signal to the worker makes it "stopped by a signal to chi"; an older worker takes the reason as `manual`); the TERM stops whatever still runs as a Stop with reason `signal` and saves it (a spawned worker traps TERM, HUP and INT: `WorkerSignals`; a turn not ended within 3 s, 0.5 s after a stop or a second signal, is cut and the session saved as it was), and a stopped worker still saves its kept turn, since the marker, not the saved status, says stopped. Any TERM, HUP or INT does the same to a worker outside a stop: it leaves (log `worker stop reason=signal`) and the next send wakes a new one, 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`). A command this chi has that the older worker doesn't (`/llm-context`, the web's `llm ctx` chip, against a 0.39.0 worker) says the same, not `not a session command` (`SessionCommands.builtin_name`); a typo keeps its `400 unknown_command`. The web's `POST /api/sessions/:id/stop` waits the same way, for 2 s (the page's Stop shows the stop at once), or for the body's `wait` up to the same 10 s (`SessionManager::STOP_WAIT`): `chi --remote NAME sessions stop` sends it, so it waits as long as here. Before that stop, the page's `stop session` button asks what the worker still has that the user can stop: `GET /api/sessions/:id/holds` (owner-only) answers the worker's `holds` — the same list the exit ask's 409 carries — always an array, empty when there is no live worker, one from before the route, or one that did not answer. With something, a sheet (a bottom sheet on a phone, a popover at the button otherwise — `stop_sheet.js`) lists them, at most three of a kind named and `+N more`, and offers **Stop all** (`POST /api/sessions/:id/holds/stop` → `HoldStop#stop_all`, then the stop), **Stop session only** (the stop exactly as it was) or **Cancel**. Nothing to ask about is the plain confirm, one tap as before. Both buttons stay because a stop only TERMs the worker: its reminders die with it, while a background task and a delegate are their own processes and keep running. A command sent during a turn that waits for its end (`/model X`, `!cmd`; see *Typing during a turn* in [cli.md](cli.md)) and hasn't run when the worker leaves (a stop, a crash) is answered `command_ran {status: "dropped"}`, so no UI keeps showing it waiting.
|
|
36
|
+
**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, then CONT (a stopped worker, SIGSTOP or Ctrl-Z, runs its TERM handler only once continued, so it leaves at once instead of outliving the wait), and waits (up to 10 s) until the worker has let go of `owner.lock`. A running turn is canceled over the Bridge first, with reason `session_stopped` (the turn's line ends "(session stopped)", its note says `cancelled (session-stopped)`, and a command it was running says "stopped as the session was stopped"; a bare signal to the worker makes it "stopped by a signal to chi"; an older worker takes the reason as `manual`); the TERM stops whatever still runs as a Stop with reason `signal` and saves it (a spawned worker traps TERM, HUP and INT: `WorkerSignals`; a turn not ended within 3 s, 0.5 s after a stop or a second signal, is cut and the session saved as it was), and a stopped worker still saves its kept turn, since the marker, not the saved status, says stopped. Any TERM, HUP or INT does the same to a worker outside a stop: it leaves (log `worker stop reason=signal`) and the next send wakes a new one, 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`). A command this chi has that the older worker doesn't (`/llm-context`, the web's `llm ctx` chip, against a 0.39.0 worker) says the same, not `not a session command` (`SessionCommands.builtin_name`); a typo keeps its `400 unknown_command`. The web's `POST /api/sessions/:id/stop` waits the same way, for 2 s (the page's Stop shows the stop at once), or for the body's `wait` up to the same 10 s (`SessionManager::STOP_WAIT`): `chi --remote NAME sessions stop` sends it, so it waits as long as here. Before that stop, the page's `stop session` button asks what the worker still has that the user can stop: `GET /api/sessions/:id/holds` (owner-only) answers the worker's `holds` — the same list the exit ask's 409 carries — always an array, empty when there is no live worker, one from before the route, or one that did not answer. With something, a sheet (a bottom sheet on a phone, a popover at the button otherwise — `stop_sheet.js`) lists them, at most three of a kind named and `+N more`, and offers **Stop all** (`POST /api/sessions/:id/holds/stop` → `HoldStop#stop_all`, then the stop), **Stop session only** (the stop exactly as it was) or **Cancel**. Nothing to ask about is the plain confirm, one tap as before. Both buttons stay because a stop only TERMs the worker: its reminders die with it (a stop also deletes a restart's `reminders.json` no successor took yet, so a later resume doesn't bring them back), while a background task and a delegate are their own processes and keep running. A command sent during a turn that waits for its end (`/model X`, `!cmd`; see *Typing during a turn* in [cli.md](cli.md)) and hasn't run when the worker leaves (a stop, a crash) is answered `command_ran {status: "dropped"}`, so no UI keeps showing it waiting.
|
|
37
37
|
|
|
38
38
|
**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 page 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?force=true` (the page sends `force`; without it a live worker's session is 409 `worker_running`, as the CLI without `--force`; 200 `{status: "deleted", session_id, stopped}`; 409 `owned_by_tui` or `still_stopping`; 404).
|
|
39
39
|
|
data/docs/testing.md
CHANGED
|
@@ -301,7 +301,8 @@ to install). It refuses when the folder or the branch already exists.
|
|
|
301
301
|
|
|
302
302
|
`SAMAGOTCHI_MACOS_BUILD=1 bundle exec rspec spec/desktop` also compiles the macOS helper (`:macos_build`, needs the
|
|
303
303
|
Command Line Tools) and runs `ChiHelper` against a fake kitty and, when a `tmux` is on PATH, a real one (private
|
|
304
|
-
`-L` servers under a temp `TMUX_TMPDIR`, killed after)
|
|
304
|
+
`-L` servers under a temp `TMUX_TMPDIR`, killed after), and its `--sessions`, `--send-plan` and `--broadcast` seams
|
|
305
|
+
against a fake chi (a `/bin/sh` script as the launch file's argv). Without the variable they are filtered out (CI's runs), and
|
|
305
306
|
`script/affected-specs` doesn't pick them for a Swift change: run them by hand after editing
|
|
306
307
|
`lib/samagotchi/desktop/macos/`.
|
|
307
308
|
|
|
@@ -5,7 +5,9 @@
|
|
|
5
5
|
// directly by chi desktop) manages the login item and exits;
|
|
6
6
|
// `ChiHelper --agents list|send` is the agent terminals' seam (AgentTerminal.swift),
|
|
7
7
|
// `ChiHelper --model-pick` the model chooser's (Models.swift),
|
|
8
|
-
// `ChiHelper --broadcast` the Everyone row's (Broadcast.swift)
|
|
8
|
+
// `ChiHelper --broadcast` the Everyone row's (Broadcast.swift),
|
|
9
|
+
// `ChiHelper --sessions` and `--send-plan` the session rows' and their
|
|
10
|
+
// sends' (Sessions.swift).
|
|
9
11
|
import AppKit
|
|
10
12
|
import ServiceManagement
|
|
11
13
|
|
|
@@ -111,6 +113,12 @@ struct ChiHelperMain {
|
|
|
111
113
|
if args.count >= 2, args[1] == "--broadcast" {
|
|
112
114
|
exit(broadcastCommand(Array(args.dropFirst(2))))
|
|
113
115
|
}
|
|
116
|
+
if args.count >= 2, args[1] == "--sessions" {
|
|
117
|
+
exit(sessionsCommand(Array(args.dropFirst(2))))
|
|
118
|
+
}
|
|
119
|
+
if args.count >= 2, args[1] == "--send-plan" {
|
|
120
|
+
exit(sendPlanCommand(Array(args.dropFirst(2))))
|
|
121
|
+
}
|
|
114
122
|
let app = NSApplication.shared
|
|
115
123
|
let delegate = AppDelegate()
|
|
116
124
|
app.delegate = delegate
|