samagotchi 0.168.0 → 0.169.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 +38 -1
- data/docs/architecture.md +1 -1
- data/docs/configuration.md +1 -1
- data/docs/guardrails.md +2 -2
- data/docs/hooks.md +2 -2
- data/docs/internals/block-renderers.md +1 -1
- data/docs/internals/bridge.md +16 -4
- data/docs/internals/inline-widgets.md +1 -1
- data/docs/internals/objectives.md +2 -1
- data/docs/internals/remote-sessions.md +1 -1
- data/docs/internals/web-frontend.md +62 -22
- data/docs/plugins.md +1 -1
- data/docs/releasing.md +10 -6
- data/docs/testing.md +12 -0
- data/lib/samagotchi/bridge.rb +12 -2
- data/lib/samagotchi/bundles/system/manifest.yml +1 -1
- data/lib/samagotchi/client_id.rb +3 -0
- data/lib/samagotchi/command_intake.rb +7 -7
- data/lib/samagotchi/compat.rb +14 -5
- data/lib/samagotchi/engine/objectives.rb +10 -1
- data/lib/samagotchi/guardrails/approvals.rb +8 -3
- data/lib/samagotchi/session_continue.rb +8 -11
- data/lib/samagotchi/session_manager/removal.rb +3 -3
- data/lib/samagotchi/session_stores.rb +79 -0
- data/lib/samagotchi/turn_stages/objectives.rb +6 -8
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/public/app.js +3 -1
- data/lib/samagotchi/web/public/block_watch.js +1 -1
- data/lib/samagotchi/web/public/cards_place.js +5 -1
- data/lib/samagotchi/web/public/index.html +58 -39
- data/lib/samagotchi/web/public/markdown_place.js +1 -1
- data/lib/samagotchi/web/public/question_cards.js +20 -6
- data/lib/samagotchi/web/public/stage_asking.js +283 -0
- data/lib/samagotchi/web/public/stage_frame.js +7 -11
- data/lib/samagotchi/web/public/stage_model.js +37 -11
- data/lib/samagotchi/web/public/stage_opened.js +28 -18
- data/lib/samagotchi/web/public/stage_slots.js +13 -89
- data/lib/samagotchi/web/public/stage_view.js +202 -160
- data/lib/samagotchi/web/public/stage_waiting.js +180 -0
- data/lib/samagotchi/web/public/stream_handlers.js +1 -1
- data/lib/samagotchi/web/public/turn_model.js +1 -1
- data/lib/samagotchi/web/session_show.rb +14 -4
- data/lib/samagotchi/worker.rb +3 -4
- metadata +4 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 329ec9300af5c8d50782f89e0b422192dde8893d14c1cebe1a096e5512095e4c
|
|
4
|
+
data.tar.gz: 138fde389b9f7776b24db545b27c5550cc47a0565cf93141320fc7ba13ddb997
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 743ca2eb1a501a506de420953b0d955ad9894b2fedc89880c767f53881afd8feae04468e44309b7883c2623354de87716d3d133602ff90a574da5d0cb3a3b0c7
|
|
7
|
+
data.tar.gz: becf43bf35201c4c78e946fec1356153a43199c863a99ab341f2406541d02e20e18236f8475ea3e30db5f13bc47342c6b30e6187e413d6fec7c6add57a48a073
|
data/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,42 @@ and commands may change between minor versions. How releases are made:
|
|
|
8
8
|
|
|
9
9
|
## [Unreleased]
|
|
10
10
|
|
|
11
|
+
## [0.169.0] - 2026-11-12
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
|
|
15
|
+
- chi web: every card that waits for you docks alone above the composer, and the turn so far (your message, the steps
|
|
16
|
+
line) goes into the conversation: chi's question with or without a message before it, an approval, a delegate's
|
|
17
|
+
relayed card, a hook's question and check-in's card. chi's line before its own question or approval becomes a bubble
|
|
18
|
+
in the conversation (`chi · step N · asking you`, `↑ its reason` on the card); a relayed card, a hook's question or a
|
|
19
|
+
card with no line before it shows alone. The box has no steps chip once the turn is in the conversation. Folded, its
|
|
20
|
+
row says what the card asks (an approval: its summary and the command's start; check-in: its title).
|
|
21
|
+
- chi web: while check-in's card waits, the box's top row says the call chi runs now and the dot keeps blinking; when
|
|
22
|
+
chi writes with the card still open, the text streams in the box under the card and the row says "writing". The
|
|
23
|
+
composer no longer says a message waits for the card: check-in doesn't stop the turn.
|
|
24
|
+
- chi web: with two cards waiting at once (check-in still open, then a question or an approval) the box shows one at a
|
|
25
|
+
time, the one that stops chi in front, with `‹ 1 of 2 ›` in its top row to page between them.
|
|
26
|
+
- chi web: at the step limit the turn goes into the conversation at once with its card right under it, as a reload
|
|
27
|
+
draws it; no box waits with it.
|
|
28
|
+
- chi web: no amber on question and approval cards or on the box a card waits in (edge, tint, ▸, the option's radio);
|
|
29
|
+
amber stays on a waiting message's edge and "asking you", and on the box's "waiting for you".
|
|
30
|
+
- chi web: the question waiting above the composer has one fold, the stage's ▾. Folded, its row says "waiting for you"
|
|
31
|
+
and the question (on a phone, on a second line), so the conversation gets the room; a tap on the row brings the card
|
|
32
|
+
back. The fold is for that question only and isn't saved: a question always arrives open, also when you keep the
|
|
33
|
+
stage folded while chi works, and after the answer the stage is as you had it.
|
|
34
|
+
|
|
35
|
+
### Fixed
|
|
36
|
+
|
|
37
|
+
- Bridge: a broken objectives store no longer breaks snapshots, SSE resets or attached clients; the web falls back to
|
|
38
|
+
its file view.
|
|
39
|
+
- Guardrails: approvals saved by chi 0.166 and before as a list are read and retained after the next write.
|
|
40
|
+
- chi web: after a reload, the objectives strip matches the running session's view; it no longer flips on update.
|
|
41
|
+
- Guardrails: approvals in an unversioned `approvals.json` object are read instead of set aside.
|
|
42
|
+
- chi web: a card waiting in the stage (a question, an approval) no longer closes to its title line in an empty box
|
|
43
|
+
when its title is clicked; its own ▾ is gone while it waits.
|
|
44
|
+
|
|
45
|
+
Update with `chi update`, then restart chi web and running sessions (`chi sessions restart`).
|
|
46
|
+
|
|
11
47
|
## [0.168.0] - 2026-11-11
|
|
12
48
|
|
|
13
49
|
### Changed
|
|
@@ -6239,7 +6275,8 @@ and long-lived sessions.
|
|
|
6239
6275
|
- A macOS desktop helper (`chi desktop install`): a "Send to chi" Service and a
|
|
6240
6276
|
hotkey panel that send selected text or the clipboard to your sessions.
|
|
6241
6277
|
|
|
6242
|
-
[Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.
|
|
6278
|
+
[Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.169.0...HEAD
|
|
6279
|
+
[0.169.0]: https://github.com/dm1try/samagotchi/compare/v0.168.0...v0.169.0
|
|
6243
6280
|
[0.168.0]: https://github.com/dm1try/samagotchi/compare/v0.167.0...v0.168.0
|
|
6244
6281
|
[0.167.0]: https://github.com/dm1try/samagotchi/compare/v0.166.0...v0.167.0
|
|
6245
6282
|
[0.166.0]: https://github.com/dm1try/samagotchi/compare/v0.165.0...v0.166.0
|
data/docs/architecture.md
CHANGED
|
@@ -198,7 +198,7 @@ log's last 4 MB). Rows: [configuration.md](configuration.md#checking-it-chi-home
|
|
|
198
198
|
- **Single owner:** the process running a session's Engine (its worker, or a `--non-interactive` one-shot) holds a flock on `owner.lock` (`OwnerLock`); a second owner backs off, and the web answers 409 for a session a one-shot owns. That owner is also the one writer of the session's `turn_drawings.jsonl` (a worker's Bridge, a one-shot's own accumulator); a fork's copy of its parent's lines is written before the child's worker spawns.
|
|
199
199
|
- **Status:** `status` is turn state (`idle`/`running`); liveness is the lock. A stop is the `<uuid>/stopped` marker (`Session.mark_stopped`; a resume removes it), which `Session.load` and `.summary_from_file` read as status `stopped`, so no other process rewrites a worker's session file. A resume writes nothing else: the worker, holding the lock, turns a status from before it idle (`Worker#settle_stale_status`: `stopped` with no marker, `error`, a `running` no turn is behind; not a new session's `running` whose first prompt waits), and saves at start whatever the status, which stamps `updated_at` as the resume's save did (a session woken with no turn stays young for the sweep). N-1: a newer spawner starting an older worker (a checkout's chi spawning the installed one) loses that reset; a restart covers it.
|
|
200
200
|
- **Starting:** every spawn marks the session starting (`<uuid>/starting`, `{pid, at}`, `StartingMarker`) until its worker holds the lock: `spawn_worker_for_session` writes it with its own pid before `Process.spawn` and the starter's after (the starter lives until the worker holds the lock, at most `WORKER_START_GRACE`; a spawn that raises or whose starter is gone leaves none), `spawn_session` before the new file is saved, and `Worker#start` removes it. It holds for 60 s at most, and only while the pid it names runs. A resume claims it (`StartingMarker.claim`, a link(2): of resumes racing from any processes one wins), then reads the owner again. A stop marker cancels the hold-off (`start_pending?` = starting and not stop-marked) for a start marked before the stop only: of two sends after a stop, the claim lets one spawn. The stop marker carries its own `at` (`{"at"}`, on the starting marker's clock; `Session.stopped_at`), an older chi's empty one its mtime. The sweep, archive (`:starting`) and delete (refused as an owned session) honour it. N-1: an older spawner writes none for a resume (the owner checks stay); an older worker never leaves one it was given (every worker clears it at start); a leftover one is stale by age or pid (a claim's temp files a kill left, `starting.*`, count as skeleton); a 0.160 spawner or chi web still holds off on a fresh marker under a stop (it reads `starting?` before the stop), until the start ends or 60 s. `SessionManager` keeps only its starters' pids, for chi web's restart to adopt (`live_spawn_pids`), and decides nothing from them.
|
|
201
|
-
- **Deleting:** `delete_session`, the retention sweep and a failed continue's cleanup use `remove_files` for the session JSON, sidecar dir, plugin state, attached context, participant grants, System 1 runs and context-candidate records. The JSON and dir go through `remove_session_files`, holding the owner lock (kind `deleting`, which `stop_session` never signals; not waited for: a worker that took it since the check keeps the session) and reading the starting marker again under it (a resume that claimed the start just before keeps the session; one after it sees the lock held). N-1: an older chi's stop reading the owner then would TERM the deleter (chi web, the CLI) in that moment; an older reader shows the kind as unknown. `Session#save` of a session read from its file (or saved once) skips a file that is gone (log `save_skipped_deleted`) rather than recreate it; only a new session's first save creates one.
|
|
201
|
+
- **Deleting:** `delete_session`, the retention sweep and a failed continue's cleanup use `remove_files` for the session JSON, sidecar dir, plugin state, attached context, participant grants, System 1 runs and context-candidate records. `SessionStores` registers the sidecar filenames and the removal categories; `remove_session_files` deletes the whole sidecar directory, so those files need no individual removal path. The JSON and dir go through `remove_session_files`, holding the owner lock (kind `deleting`, which `stop_session` never signals; not waited for: a worker that took it since the check keeps the session) and reading the starting marker again under it (a resume that claimed the start just before keeps the session; one after it sees the lock held). N-1: an older chi's stop reading the owner then would TERM the deleter (chi web, the CLI) in that moment; an older reader shows the kind as unknown. `Session#save` of a session read from its file (or saved once) skips a file that is gone (log `save_skipped_deleted`) rather than recreate it; only a new session's first save creates one.
|
|
202
202
|
- **Worker presence:** whether a worker is up, coming or held off is read once per decision as a `WorkerPresence` (`worker_presence.rb`, a `Data.define`), the deciders' one reader of those facts (display and list rows still read `session_owner` / `WorkerLink` themselves): the owner lock's holder, the stop marker (`stop_marked?`: the only "stopped" a decider reads, never the session file's status), the `starting` marker (`StartingMarker`, `starting?`, `start_pending?`: every spawn writes it, see Starting), the archive marker, the session file's existence, and the `WorkerLink` outcome when asked (never the file itself: a scratch session's flag is read by `WakePolicy`, last). The deciders read it: `resume_session` (tui, owned, `resume_held_off?`), the session inbox's `WakePolicy` (a ring, a push's wake, a note, a fresh verdict, `wake_for_report`, `deliver_turn`'s two liveness checks, `WorkerBoot`'s leave: below), `Local#wake`/`#worker`, `AttachLauncher`, `Schedules#busy_reason`, archive, delete and the retention sweep. Each keeps its own subset of the facts and their order.
|
|
203
203
|
- **Session inbox:** a delivery into a session (`SessionInbox.deliver(kind, id) { |at| write }`, `session_inbox_delivery.rb`) runs one path: resolve the link (`SessionInbox::Head.resolve`: push and a fresh verdict are pinned to the id named; a send and a note go to the chain's newest link, and for a send (a kind that starts an archived link) a continue of the reached link in flight (`ContinueIntent.in_flight`: `continue.json` written, `continue.lock` held, the successor not saved yet) gives `:continuing` with the successor's id, which chi send asks again for a moment and then refuses; a note needs no such check (`NoteMove` moves or adopts it); a ring's file stays with the parent `parent.json` names and its wake goes one hop, to the link whose `ChildReports` reads that file; only an archived id is walked, with one `Session.list`: `SessionChain::Links#first_live`), refuse before any write (a gone session; a one-turn run's or a scratch session for send and a note), write with the kind's own writer (formats unchanged), read `WorkerPresence` once, and apply the kind's `WakePolicy` row. `decide` reads in one order for every kind: gone, one-turn run (`:owned`), owned (`:live`), the stop marker (`:stopped`: the only "stopped", the file's status never decides; send un-stops), archived (send alone starts one), starting (`:starting`, never a second spawn), the kind never starts (`:none`: a note, a fresh verdict) or wasn't asked to (`:off`), scratch (the session file is read only here), else `:start` through `SessionManager.start_for_inbox` (`resume_session`, its claim on the starting marker; it answers `:woken` only when it spawned the worker, `:starting` when another start won the claim, `:live` when a worker took the lock since). N-1 gap: a session file that says `stopped` with no stop marker (stopped before the marker existed, or a resume whose spawn failed after it cleared the marker) is woken, and an older worker, which has no `Worker#settle_stale_status`, exits on its first pass; the wake still reads `:woken`, and each later ring or push spawns another such worker (bounded by the rings and pushes). Restarting chi on the new version (`chi web`, the workers) closes it. `SessionInbox.wake` is the wake half alone (`wake_for_report`, a failed continue's rings). On it: `ChildRing.ring` (its start on a thread), `Local#context_push`'s wake (its outcome is `ContextPush#wake`, the push line's ending), `NoteDelivery.deliver` (every note writer a person or another session drives: `chi note`, `send_note`, `chi broadcast`, chi web's notes route; `NoteDelivery::Result#id` is the link that has it, `#asked` the one named; `/objectives` passes `pin: true`: its list is its own session's) and `VerifyFresh.forward` (`:fresh_verdict`: the child's parent, pinned, no worker). Send keeps its Bridge/file flow in `SessionManager.deliver_turn`, which resolves nothing (it acts on the id it is given: an archived link gets the message and comes back); chi send resolves the head first (`SessionAccess#head`, remotely `GET /api/sessions/resolve?head=1`), while the web composer, a participant's turn, `delegate` follow-ups, `Plugin::Sessions` and `Schedules` stay on the id they name; its two liveness checks (before the post, after a file's write) read the sender's row (`WakePolicy.for_sender(client_id)`, `ClientId.wake_kind`: a person's — chi send, a web tab, a TUI — is `:send`; a schedule, a plugin, a delegate follow-up and a participant have rows of their own, any other id `:automatic`, which start an idle session but neither un-stop nor start an archived link: refused before the write, `DeliverResult::STOPPED` / `ARCHIVED`) and leave the other refusals to `resume_session`'s wording. One row is not a delivery: `:leave`, `WorkerBoot.run_session_loop`'s successor as a worker leaves (input left after a signal, an idle exit's or `/exit`'s input or rings, a restart), read on a fresh presence once the lock is free: a stop that landed as it left starts none for any way out (`resume_session` would clear the stop; logged `respawn_dropped`, `restart_dropped` for a restart, whose reminders go with it), the input and rings stay for the next start. The notes a spawn writes into the session it starts stay direct `SessionInbox.write_note` calls, pinned by construction: spawn_session's chain note, `ContextMove`'s notes, `Objectives.carry`, `ChildMove`'s note to a moved child. A continue moves the previous link's waiting notes to the new link last in its `before_spawn` (`NoteMove.move`), a failed one gives them back before the link goes (`NoteMove.give_back`), and the new link's worker adopts what the archived links before it get after their continue (`NoteMove.adoption`, from `WorkerInbound#absorb_notes` and `Worker#start`: back along the chain through up to 8 archived links, from each only notes from the creation of the link that continued it, never an objectives list). `write_note` refuses a session with no file and no folder (`Session::NotFound`), so a late writer can't bring back a deleted link's folder.
|
|
204
204
|
- **Retention:** 14 days / 500 cap (env `SAMAGOTCHI_SESSION_RETENTION_DAYS`/`MAX_COUNT`, optional `KEEP_STATUS`), live-owner and starting guard (looked at again right before a session's files go), only when `*.json` present; lazy sweep at most once per `session.sweep_interval_hours` (default 24) from the session hub's full-probe tick (and on `GET /api/sessions`, which the page no longer calls), manual via `bin/chi sessions prune --dry-run`.
|
data/docs/configuration.md
CHANGED
|
@@ -2385,7 +2385,7 @@ column says so, a `--kebab-name` flag. The maps (`hosts:`, `models:`,
|
|
|
2385
2385
|
| `web.host` | `127.0.0.1` | yes | `127.0.0.1`, `::1` or `localhost`; `lan` (this machine's private IPv4 address) or one of its IPv4 addresses also opens `chi web` to the network, with an access token, asked of this machine's 127.0.0.1 too (its `Local:` link carries a one-time code; `chi web --new-token` replaces the token; it is chi web's one access token, also used by `--tunnel` and `web.public_url`/a proxy). Anything else binds `127.0.0.1` with a warning. See [CLI: chi web on your phone](cli.md#chi-web-on-your-phone). |
|
|
2386
2386
|
| `web.markdown` | `false` | yes | Render answers as Markdown in `chi web`. |
|
|
2387
2387
|
| `web.working_set` | `true` | yes | The stage view's working set: the files, memories and URLs a turn brought into the model's context, in a rail in the stage, a footprint as each finished turn's block summary and a `set N` chip in place of the mem chip; `false` brings the mem chip back. `?working_set=1` or `?working_set=0` on the page URL overrides it for that page load. See [CLI: the working set](cli.md#the-working-set). |
|
|
2388
|
-
| `web.reload_check` | `false` | yes | Opt-in: once each finished turn has moved into the history, the page reads the session again (one full read per turn end), draws that turn as a reload would and compares the two, strictly: a known divergence counts as a mismatch too. `chi web` logs one `reload_checked` line per check and a `reload_mismatch` line naming the kinds that differed, `excused` (the known divergences, `history_shape.js` `ACCEPTED`, that would explain them) and `explained` (all of them had one), with `turn_id`, `drawn` (the turn came back from its drawing, so a mismatch with `drawn: true` is a drawing bug), `undrawn` (how many of the reload's rows came without their turn's drawing, the page's plain fallback: 0 unless chi web missed a turn) and the worker's version: counts only, never content. The count is `grep -c reload_mismatch` over `grep -c reload_checked` in chi web's log, split by `excused`. Cards are left out: a reload places them after the render, which the check doesn't run (the e2e reload checks compare them). Every open owner tab runs the check; `chi web` logs a turn's first report only, and starts over when it restarts. |
|
|
2388
|
+
| `web.reload_check` | `false` | yes | Opt-in: once each finished turn has moved into the history, the page reads the session again (one full read per turn end), draws that turn as a reload would and compares the two, strictly: a known divergence counts as a mismatch too. `chi web` logs one `reload_checked` line per check and a `reload_mismatch` line naming the kinds that differed, `excused` (the known divergences, `history_shape.js` `ACCEPTED`, that would explain them) and `explained` (all of them had one), with `turn_id`, `drawn` (the turn came back from its drawing, so a mismatch with `drawn: true` is a drawing bug), `undrawn` (how many of the reload's rows came without their turn's drawing, the page's plain fallback: 0 unless chi web missed a turn) and the worker's version: counts only, never content. The count is `grep -c reload_mismatch` over `grep -c reload_checked` in chi web's log, split by `excused`. Cards and answered question cards are left out: a reload places them after the render, which the check doesn't run (the e2e reload checks compare them). Every open owner tab runs the check; `chi web` logs a turn's first report only, and starts over when it restarts. |
|
|
2389
2389
|
| `web.annotate_presets` | `Agreed\|Could you please elaborate?` | yes | Quick replies next to Annotate in `chi web`, `\|`-separated (a YAML list works too); `""` in the file or on the CLI leaves only Annotate (an empty env value means the default). See [CLI](cli.md#web-annotate-presets). |
|
|
2390
2390
|
| `web.editor` | `vscode` | yes | What a file ref in `chi web` (a file tool row's title, a diff line) opens, for a viewer on this machine: a preset (`vscode`, `vscode-insiders`, `cursor`, `zed`) or a URL template with `{path}` and an optional `{line}`; `none` turns refs off. See [Opening files from chi web](#opening-files-from-chi-web). |
|
|
2391
2391
|
| `web.start_dir` | the folder `chi web` runs in | | The folder `chi web` starts in (`chi web --start-dir DIR` for one run): its page (the printed link, a bare `/`, the LAN link and QR code, the Home Screen app) opens on that folder's project, outside a project (no repository or marked folder) on every session, and a new session from the all view starts there; the header's project picker switches projects. `~` and a relative path (resolved from your home folder) work; one that isn't a folder stops `chi web` from starting. `chi web` never changes its own folder. See [CLI](cli.md). |
|
data/docs/guardrails.md
CHANGED
|
@@ -115,8 +115,8 @@ once, session and repo, while `git merge y && git push` offers once, as `git-mer
|
|
|
115
115
|
Two rules with one id (a config.yml copy of a bundle's rule) are one rule; a core ask followed by a rule's (the
|
|
116
116
|
[child boundary's](#a-delegate-childs-boundary)) keeps its own scopes.
|
|
117
117
|
The file is a versioned store (`{"version": 1, "approvals": [...]}`, `Approvals::FORMAT`); a write keeps its version,
|
|
118
|
-
the keys this chi doesn't know and an entry with no `scope`. A bare list of entries, as chi 0.166 and before wrote it,
|
|
119
|
-
|
|
118
|
+
the keys this chi doesn't know and an entry with no `scope`. A bare list of entries, as chi 0.166 and before wrote it,
|
|
119
|
+
is read and converted to a versioned store on the next write. Chi 0.165 and before set a versioned store aside, so restart their sessions
|
|
120
120
|
(`chi sessions restart`); a downgrade to 0.165 or before moves the store to a recoverable `approvals.json.corrupt-*`.
|
|
121
121
|
A file that doesn't parse, or a versioned store whose `approvals` isn't a list, is moved aside to
|
|
122
122
|
`approvals.json.corrupt-<UTC time>` with one warning, and chi starts with no stored approvals (more asks, nothing lost).
|
data/docs/hooks.md
CHANGED
|
@@ -61,7 +61,7 @@ The plugin class must respond to `#call(event)` — duck-typed, no base class re
|
|
|
61
61
|
| `:after_generation` | After LLM returns (both loops); not after a generation a hook cut (`stop_generation`) | `{ type: :after_generation, iteration: N, response: "...", messages: [...] (the conversation as sent) }` |
|
|
62
62
|
| `:generation_progress` | While the response streams, in batches (see [Watching the stream](#watching-the-stream)) | `{ type: :generation_progress, iteration: N, thinking: "..." (new since the last fire), text: "..." (new visible text), thinking_chars: N, text_chars: N (this generation so far), elapsed_ms: N, restarted: true (only on the first fire after a dropped stream was asked again) }` |
|
|
63
63
|
| `:before_tool_call` | Before tool dispatch (and before `tool_call_started`) | `{ type: :before_tool_call, iteration: N, call: {...}, params: "...", guardrail: Verdict, context: {...}, targets: {...}, blocked: false, block_reason: nil }` |
|
|
64
|
-
| `:question_asked` | A question of the model's own (`ask_user_question`) or an approval of its tool call is about to open, on the turn's thread (not a hook's or plugin's `ask_user`, a relayed delegate approval, the step-limit question, a `--non-interactive` run or a stored approval: none of those fire it) | `{ type: :question_asked, kind: "question" \| "approval", text: "..." (the reply that asks, as the
|
|
64
|
+
| `:question_asked` | A question of the model's own (`ask_user_question`) or an approval of its tool call is about to open, on the turn's thread (not a hook's or plugin's `ask_user`, a relayed delegate approval, the step-limit question, a `--non-interactive` run or a stored approval: none of those fire it) | `{ type: :question_asked, kind: "question" \| "approval", text: "..." (the reply that asks, as the lead-in bubble shows it: no thoughts, no tool-call markup), question: {...} (the question's fields, frozen: question/options/header…, an approval's approval: facts too; no id yet), refs: (see [Refs on a question](#refs-on-a-question-display-only)) }` |
|
|
65
65
|
| `:after_tool_call` | After tool execution | `{ type: :after_tool_call, iteration: N, tool: "read", output: "..." (the text the model gets: capped at max_tool_output_chars (a memory_read's cap is memory.read_max_chars, 0: none); a cut one has a [cut: …] line, at its end, or for an execute result between its head and its tail, the whole output saved in the session's tool_output/ folder), status: "ok" \| "error" \| "blocked" \| "stopped", call: {...} (the call that ran, as the gate left it), elapsed_ms: N (the tool's own run, without an approval wait; nil for a denied call), result: "..." (the tool's own text: no "[read]" lead, no "ran as:" line, never cut) }`; `stopped` is a `task_wait` the turn's Stop ended (the task runs on) or whose task was stopped. Match on `result:`, not `output:`: it is what the tool said (`Error: command timed out after 120s` for an execute killed at its limit in kill mode, `Samagotchi::Tools::Execute::TIMEOUT_PREFIX`; `Still running at the 120s limit: …` for one handed over as a task, `Samagotchi::Tools::Execute::HANDED_OVER`) |
|
|
66
66
|
| `:session_end` | After each turn `:after_turn` fires for, after it (turn-level lifecycle) | `{ type: :session_end, session_id: "..." }` |
|
|
67
67
|
|
|
@@ -415,7 +415,7 @@ end
|
|
|
415
415
|
The model never sees them.
|
|
416
416
|
- The web checks them again wherever a saved one reaches it (the pending
|
|
417
417
|
question, a saved card, the history), as an answer's, and pairs them as an
|
|
418
|
-
answer's (the three passes above) in: the
|
|
418
|
+
answer's (the three passes above) in: the lead-in bubble while the
|
|
419
419
|
question waits (its raw text, then again once its markdown is in), the
|
|
420
420
|
card's question text, the answered card's kept reply (raw, then its
|
|
421
421
|
markdown when opened; never its one-line summary copy), and that step's
|
|
@@ -26,7 +26,7 @@ model. The TUI is untouched: it shows a fence as source.
|
|
|
26
26
|
helpers live there too: `failBlock` and its `.block-note` (a click copies it), `holdingView`, `rejoin`, the
|
|
27
27
|
toggle's listener; frames, the cache, hang marks, size checks, the zoom and the theme redraw stay in
|
|
28
28
|
`block_host.js`. One `MutationObserver` per turn root,
|
|
29
|
-
filtered to `TEXTS` (`text_roots.js`), so cards,
|
|
29
|
+
filtered to `TEXTS` (`text_roots.js`), so cards, a lead-in bubble, question replies and delegate reports never draw, and
|
|
30
30
|
every path that adds a block to a root is seen the same way. One `IntersectionObserver` per scroller, with an explicit `root`
|
|
31
31
|
(with `root: null`, `rootMargin` doesn't pass an `overflow: auto` in between). Closed `<details>` fire on open. **A
|
|
32
32
|
move isn't over when it is seen:** a block waiting to come near is watched by its scroller's observer, so one that moves
|
data/docs/internals/bridge.md
CHANGED
|
@@ -132,7 +132,10 @@ no line for (an older worker's, one from before drawings, a dropped one) gets on
|
|
|
132
132
|
`#saved_tool_records` through chi web for `--remote`) and the snapshot's cards, through the same renderer. Nothing
|
|
133
133
|
rebuilds rows from the messages any more. `chi --remote` attaches through chi web's stream proxy,
|
|
134
134
|
which relays an owner's frames byte for byte, the field included. A participant never gets it: its stream's snapshot
|
|
135
|
-
is bare (`ParticipantView`).
|
|
135
|
+
is bare (`ParticipantView`). The snapshot also carries `objectives`, this worker's `Engine#objectives_view` computed
|
|
136
|
+
with the same messages as its `:objectives_updated` event; the key is present (`nil` when there is no list), but is
|
|
137
|
+
omitted if reading the view fails so chi web can use its file view. An older worker sends no key. An older TUI
|
|
138
|
+
ignores the drawing field; an older worker sends none.
|
|
136
139
|
- **The show.** chi web's full read (`GET /api/sessions/:id`, whatever its query: a tab from before 0.157 still sends
|
|
137
140
|
`?parts=1`) draws every finished turn from a line: `SessionShow#drawn_lines` is `TurnDrawings.cover` over the file,
|
|
138
141
|
the show's timing records and its cards (CardStore's list, live or saved), the running turn (`current_turn`'s id)
|
|
@@ -401,6 +404,10 @@ The Bridge exposes:
|
|
|
401
404
|
the question still open. `chi answer` makes the same check first, from the session file.
|
|
402
405
|
- `POST /session/:id/question/dismiss` — leave the question unanswered (an approval: denied);
|
|
403
406
|
`200`, or `409` when it is no longer pending.
|
|
407
|
+
- `POST /session/:id/guardrails/wrong` — `{id}`: label this session's System 1 auto-allow as
|
|
408
|
+
one it should have asked about; `200`, `404` for an unknown judgment, `409` if already labelled.
|
|
409
|
+
- `POST /session/:id/holds/stop` — stop those holds without the worker leaving; `200 {status: "stopped",
|
|
410
|
+
session_id, stopped}` or `501 holds_unavailable`.
|
|
404
411
|
- `POST /session/:id/tasks/stop` — `{task_id}`: stop a background task as the user
|
|
405
412
|
(`TaskRuntime.stop_task by: "user"`, the web's stop-task button on a running `task_wait`;
|
|
406
413
|
web route `POST /api/sessions/:id/tasks/:task_id/stop`). Only a task this conversation's
|
|
@@ -452,9 +459,14 @@ The Bridge exposes:
|
|
|
452
459
|
- `GET /session/:id/state` — `session_state_snapshot` (JSON), read under the event lock so its `event_seq`/`event_id` match the state.
|
|
453
460
|
- `GET /session/:id/stats` — `Engine#stats_snapshot` for attached `/stats`: the metrics, with the context window and prompt profile asked from the server before the first turn.
|
|
454
461
|
- `GET /session/:id/activity` — `Worker#activity_snapshot`: `{session_id, turn_running, input_pending, awaiting_continue, open_streams, reminders, tasks_holding}` — what a `chi schedule tick` asks a live worker before it fires (the tick runs out of process and cannot read a worker's memory). Read-only and small (under 300 bytes). `501 activity_unavailable` when this Bridge was built without an `on_activity:` step; the worker advertises it in `Bridge::FEATURES` (`activity`). See [scheduling.md](../scheduling.md).
|
|
462
|
+
- `GET /session/:id/holds` — the reminders, watched tasks and delegate children the user can stop;
|
|
463
|
+
`200 {session_id, holds}` or `501 holds_unavailable`.
|
|
455
464
|
- `GET /session/:id/snapshot` — the snapshot frame's content as one request (the web server renders
|
|
456
465
|
the messages itself, then streams from its `event_seq`). Read by the web's full show
|
|
457
|
-
(`GET /api/sessions/:id`) and
|
|
466
|
+
(`GET /api/sessions/:id`) and `ctx.sessions.read` (`Plugin::Sessions#read`), which unwraps
|
|
467
|
+
`snapshot`; the Bridge reply is `{snapshot:, session_state_snapshot:}`. `snapshot.objectives` is the worker's
|
|
468
|
+
objectives view, including the running conversation; it is present as `nil` when the worker has no list and omitted
|
|
469
|
+
if reading the view fails so chi web can use its file view.
|
|
458
470
|
- `GET /session/:id/tail` — the page's light re-read (`Bridge#tail_frame`): `session_state_snapshot`,
|
|
459
471
|
`working_directory` (the session's folder, which the web checks the answer's file refs against),
|
|
460
472
|
`answer` (the last message a UI shows as an answer, `AnswerTail`: the one message, copied alone),
|
|
@@ -539,8 +551,8 @@ The page's frequent reads of `GET /api/sessions/:id` stay light too:
|
|
|
539
551
|
read's re-render) and pairs them with the bubble's links and text before the `path:line` scan. A question's
|
|
540
552
|
refs (`pending_question.refs`, `:question_asked`) pair the same way (`markdown_place.js` `linkRefsIn`/`pairRefsIn`):
|
|
541
553
|
`question_requested` (or a snapshot's pending question) keeps them on the asking step's gen
|
|
542
|
-
(`turn_model.js` `questionRefs`) before the card draws, so the
|
|
543
|
-
`
|
|
554
|
+
(`turn_model.js` `questionRefs`) before the card draws, so the lead-in bubble (`stage_opened.js`, its refs from
|
|
555
|
+
`turn_model.js askingRefs`) pairs them on its raw draw and again after its markdown swap; the card pairs its
|
|
544
556
|
question text and its kept reply (`question_cards.js`); the step's closed `.gen-text` takes them
|
|
545
557
|
(`turn_block.js`; on a reload its `data-answer-at`, the message's `display_refs`) and `linkTextRefs` pairs it at
|
|
546
558
|
the turn's end and after a reload. A closed step's text is markdown too with `web.markdown` on: a reload's
|
|
@@ -138,7 +138,7 @@ strings reach the page through `textContent` and attributes only, never `innerHT
|
|
|
138
138
|
|
|
139
139
|
**What doesn't draw.** The participant page (`participant.js` wires no block host), `web.markdown` off (no
|
|
140
140
|
`data-lang`), a live answer while it streams (its YAML shows until the turn's end re-renders it as markdown), and
|
|
141
|
-
anything outside `TEXTS`: cards, question replies,
|
|
141
|
+
anything outside `TEXTS`: cards, question replies, a lead-in bubble, delegate reports. The terminal shows the YAML.
|
|
142
142
|
|
|
143
143
|
## The fixtures
|
|
144
144
|
|
|
@@ -279,7 +279,8 @@ never an approval, no coordinator path reads it, and nothing enters the conversa
|
|
|
279
279
|
**Why not a `VerifyFresh` child.** The plan said "VerifyFresh-like, fit unchecked". It fits poorly:
|
|
280
280
|
|
|
281
281
|
- `VerifyFresh.start` starts from a saved unclear verdict and its `VerifyAnchor`, a quote of at most 200 characters that
|
|
282
|
-
`
|
|
282
|
+
`TurnStages::VerifyAnchors#verified` checks against the source session's answer (`#source_session` loads the parent
|
|
283
|
+
for a fresh child). A move has neither, and a faked one would land in `session.verdicts`.
|
|
283
284
|
- A child is an agent on the session's model with the owner's tools: an unknown number of requests, approvals waiting
|
|
284
285
|
on the child, a place in `session.max_children` and in the session lists, and its spend outside the parent's line.
|
|
285
286
|
Keeping it read-only and out of the delegate paths (`ChildRing`, `ChildrenStatus`) needs a new session field.
|
|
@@ -333,7 +333,7 @@ sends twice on a 408; 5 s, `JOIN_READ_TIMEOUT`, for the join's reads: the childr
|
|
|
333
333
|
|
|
334
334
|
| chi web's answer | What the loop gets |
|
|
335
335
|
|---|---|
|
|
336
|
-
| 503 `not_live`, 404 `not_found` | raises `Unreachable` (< `IOError`): "no live worker for session ID on NAME" (`
|
|
336
|
+
| 503 `not_live`, 404 `not_found` | raises `Unreachable` (< `IOError`): "no live worker for session ID on NAME" (`WorkerLink.not_live` plus " on NAME", in `WebBridgeClient::Mapping.response`) |
|
|
337
337
|
| 502 `bridge_secret` (stream) | `Refused(:bridge_secret)`; `EventStream` ends with `stream_closed bridge_secret` |
|
|
338
338
|
| 504 (any) | `Response(408, {error: deadline_passed})`: the loop's late line |
|
|
339
339
|
| 501 with `command` (`/command`) | `Response(400, {error: unknown_command, command})`: the loop names the command |
|
|
@@ -28,7 +28,7 @@ page. A factory dep a new version stops passing is the same hazard the other way
|
|
|
28
28
|
out of `send_turn.js`, `session_actions.js` and `lists_view.js`); a page loaded before keeps all its old modules
|
|
29
29
|
until a reload, so the risk is a mixed cache, not an open tab. `answer_fold.js`, `details_motion.js`,
|
|
30
30
|
`turn_range.js`, `history_reveal.js`, `latest.js`, `session_payload.js`, `method_api.js`, `session_store.js`,
|
|
31
|
-
`session_fields.js`, `feature_gates.js`, `revise_stack.js`, `revise_switch.js`, `shell_command.js`, `stale_params.js`, `popover_modal.js`, `more_sheet.js`, `hash_intent.js` (what the hash asks for: session, all, start or keep), `worker_features.js`, `version_notices.js`, `owner_claim.js`, `owner_verify.js`, `composer_mode.js`, `strip_hidden.js`, `block_open.js`, `text_roots.js`, `block_source.js`, `block_frame.js`, `block_host.js`, `block_watch.js`, `card_stamps.js`, `recap_stamp.js` (a saved recap's place by its `opening_id` stamp, else its count), `inline_widgets.js`, `widget_walk.js`, `widget_picks.js`, `history_shape.js` (the history's shape, live against a reload: the e2e reload checks and the opt-in reload check share its normalizer and its `ACCEPTED` allowlist), `turn_pieces.js` (a finished turn's one order and its answer rule: the live hand-off's `handOffOrder` and `turnEnded`, and a reload's `turnHistoryNodes`), `reload_check.js` (the opt-in `web.reload_check`: after the stage's hand-off, the turn against a reload of it, the counts posted), `turn_drawing.js` (a finished turn's drawing, `turn_drawings` in the show: its fold into steps and its end kind; `turn_block.js` draws it), `stage_stall.js` (the stage's quiet signal), `history_end.js` (the stage's one move of pieces to the history's end), `stage_opened.js` (a running turn opened in the history while the
|
|
31
|
+
`session_fields.js`, `feature_gates.js`, `revise_stack.js`, `revise_switch.js`, `shell_command.js`, `stale_params.js`, `popover_modal.js`, `more_sheet.js`, `hash_intent.js` (what the hash asks for: session, all, start or keep), `worker_features.js`, `version_notices.js`, `owner_claim.js`, `owner_verify.js`, `composer_mode.js`, `strip_hidden.js`, `block_open.js`, `text_roots.js`, `block_source.js`, `block_frame.js`, `block_host.js`, `block_watch.js`, `card_stamps.js`, `recap_stamp.js` (a saved recap's place by its `opening_id` stamp, else its count), `inline_widgets.js`, `widget_walk.js`, `widget_picks.js`, `history_shape.js` (the history's shape, live against a reload: the e2e reload checks and the opt-in reload check share its normalizer and its `ACCEPTED` allowlist), `turn_pieces.js` (a finished turn's one order and its answer rule: the live hand-off's `handOffOrder` and `turnEnded`, and a reload's `turnHistoryNodes`), `reload_check.js` (the opt-in `web.reload_check`: after the stage's hand-off, the turn against a reload of it, the counts posted), `turn_drawing.js` (a finished turn's drawing, `turn_drawings` in the show: its fold into steps and its end kind; `turn_block.js` draws it), `stage_stall.js` (the stage's quiet signal), `history_end.js` (the stage's one move of pieces to the history's end), `stage_opened.js` (a running turn opened in the history while a card waits: the lead-in bubble, the moves, the end fold), `stage_asking.js` (the stage's docked card and its fold: the docking and lead-in rules, the opening, a waiting card's own toggle, the folded row's text per kind, the fold's choice, reveal and the docked card's fold; `stage_view.js` wires it), `stage_waiting.js` (the stage's waiting cards as data, keyed by `data-qid` / `data-card-id`: kind, docked, approval, the stage's order, the shown card and its pager, the docked card's fold; `front`, `count`, `shown`, `blocking`), `stage_cards.js` (the cards that wait in the stage: `PENDING`, `ASKS`, `waitsInStage`, read by `stage_model.js placeKind`), `resync_reach.js` (a re-read that can't reach chi web keeps the drawn history; the status chip says retrying / offline, outside a turn), `session_sync.js` (the open session's sync: `select`, `resync` and its retries, the stream's life; the stream handle and the re-read bookkeeping are its own), `session_view.js` (the open session's view: `renderSessionView` draws a read, `forgetSession` resets the page for another session, `readTurnEnd`, `endTurnView`; app.js makes both right after the cards, each reaching the other and the later views through getters), `no_contact_fill.js` (a text field that isn't a contact form: autocomplete off and a name with no contact word, against iOS Safari's AutoFill Contact bar; a template's fields carry the same attributes, and its spec checks the page's markup for them) and the `objectives_*.js` modules are new modules for that reason. The last four `block_*` draw fenced blocks (`block_watch.js` finds and watches them, `block_host.js` is its frame drawer for a bundle's renderers): [block-renderers.md](block-renderers.md). `inline_widgets.js` is its core drawer for `chi-ui` blocks, drawing `widget_walk.js`'s and `widget_picks.js`'s widgets in a shadow root styled by `widgets.css`: [inline-widgets.md](inline-widgets.md).
|
|
32
32
|
|
|
33
33
|
## Two pages, two models
|
|
34
34
|
|
|
@@ -274,6 +274,7 @@ toast's fallback place (in a session `--toast-bottom` already sits it above the
|
|
|
274
274
|
signed-out banner (`auth_lost.js`, `env()` itself: the participant page has no `--safe-*`). A new fixed or bottom-anchored
|
|
275
275
|
piece reads `--safe-*`, never `env()`: the e2e set the names by hand (`web.spec.js`, a portrait phone and a landscape one),
|
|
276
276
|
as Chromium has no insets. The theme-color metas are tokens.css's `--bg-top` per theme up to 600px wide (a phone's page starts from that colour at its top edge, so the status bar meets the top bar with no seam), else its `--bg` (`theme.test.js` pins them). An iOS 26 Home Screen app ignores them for the status bar strip: it paints the strip in the body's `background-color`, so a phone's body gradient keeps a `--bg-top` colour layer under it (black strip without one).
|
|
277
|
+
In a session, the dock (composer card) shares the history's centered column, at most 960px wide; its right edge follows a classic scrollbar (`--history-sb`, 0 with overlay scrollbars; index.html and `dock_reserve.js`).
|
|
277
278
|
|
|
278
279
|
## The stage's height
|
|
279
280
|
|
|
@@ -281,8 +282,8 @@ as Chromium has no insets. The theme-color metas are tokens.css's `--bg-top` per
|
|
|
281
282
|
cap, and never above the session panel (less the card, its offset from the panel's bottom as computed: index.html
|
|
282
283
|
`--dock-bottom`, and 16px). The frame is grow-only while a turn runs, so a change inside the stage never moves its top
|
|
283
284
|
edge, with **two allowed shrinks, once per question and once per answer** (`forgetContent`):
|
|
284
|
-
- **the asking form**: each
|
|
285
|
-
|
|
285
|
+
- **the asking form**: each card that docks (it opens, or finds opened, the turn in the history, below): the stage shows
|
|
286
|
+
that card alone (`stage_view.js place`, in the card's task);
|
|
286
287
|
- **the next step after a blocking card's answer** (`stage_view.js releaseHeldWait`, from `generationStarted`). The
|
|
287
288
|
`held-wait` class says it is due: set with `waiting`, cleared there, so a later step, which calls it again, does
|
|
288
289
|
nothing. What goes together: the live slots' waiting height, a raised cap, the tallest content seen. At the tap nothing
|
|
@@ -290,32 +291,69 @@ edge, with **two allowed shrinks, once per question and once per answer** (`forg
|
|
|
290
291
|
the view (a reader who scrolled down a card taller than the stage), when it comes back to the view's top
|
|
291
292
|
(`settleAnsweredCard`).
|
|
292
293
|
|
|
293
|
-
|
|
294
|
-
(`
|
|
295
|
-
|
|
294
|
+
**The stage's fold** (`stage_model.js stageCollapsed`, `stage_asking.js`): collapsed to its status row the stage holds no
|
|
295
|
+
frame (`frameTarget` returns null) and an unfold refits to what shows. Three things decide it, kept apart: the user's
|
|
296
|
+
**choice** (the ▾ by hand, in memory, written to `chi_stage_collapsed`; never read back from storage to decide, so
|
|
297
|
+
another tab's write doesn't apply live), **revealed** (a ref's "show in turn" opened it for this turn; the choice stays)
|
|
298
|
+
and **the docked card's fold** (the waiting card the user folded, by its key). In the asking form only that fold counts:
|
|
299
|
+
the ▾ and a tap on the folded row set or clear it and write nothing (another blocking card that comes to wait meanwhile
|
|
300
|
+
drops it, so the stage opens on the front card), so a card always arrives open (in the card's task, before
|
|
301
|
+
`question_cards.js focusWhenIdle` focuses it) and a new one never matches an old fold; the same card drawn again with its
|
|
302
|
+
id keeps it. The fold reads the registry checked against the slots first (`reconcileWaiting`), so a card answered in
|
|
303
|
+
the same task no longer holds a click. When the form ends (an answer, a dismiss, a cancel; `emptySlots` for a reset, a
|
|
304
|
+
session switch, the next turn) the stage is as the choice and revealed say. `refresh` applies it (`drawCollapsed`: only
|
|
305
|
+
when it changed, no refresh of its own). Folded, the status row's `.ts-mini` says what the shown card asks
|
|
306
|
+
(`stage_asking.js foldedText`: a question's `.question-text`, an approval's summary and its command's first line, a
|
|
307
|
+
plugin card's `.card-title`; `foldedRowText`, `.ts-mini-question`: first line, the whole as its title, the UI font)
|
|
308
|
+
instead of the running call; on a phone the step and the timing leave the row and the text wraps to a second line.
|
|
309
|
+
|
|
310
|
+
There is no asking slot: the reply that asked is the lead-in bubble in the history, or nothing. One card raises the cap:
|
|
311
|
+
a blocking approval too tall for the stage's view, a delegate's relayed one too (`raiseForApproval`, the registry's
|
|
312
|
+
`approval` flag, `stage_frame.js fitsCap`). It opens at its question and command (`revealTop`).
|
|
296
313
|
|
|
297
314
|
## A turn opened in the history
|
|
298
315
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
(
|
|
303
|
-
|
|
316
|
+
Every card that waits for the user docks (`stage_asking.js docks`: a `PENDING` card but the step limit, or an `ASKS`
|
|
317
|
+
card, placed before the turn's end): the stage opens the turn in the history (`stage_asking.js openTurn`,
|
|
318
|
+
`stage_opened.js`) and shows the card alone. **The lead-in:** chi's own question or approval
|
|
319
|
+
(`.bubble.question[data-kind="question"]:not([data-relay]), .bubble.question[data-kind="approval"]:not([data-relay])`)
|
|
320
|
+
whose step wrote text gets that text as a bubble in the history (`stage_asking.js leadInText`, the reply from
|
|
321
|
+
`askingGen`: live, the step whose call runs; in a replay, below); a relayed card, a hook's question and a card without
|
|
322
|
+
a lead-in message get none, and then a reader who was at the history's end follows the turn's move (`atEnd`, read
|
|
323
|
+
before it). **The step limit** never docks: its card arrives after the turn's end and hands the turn off at once
|
|
324
|
+
(`place` calls `handOff`, not the in-use watch; the end read doesn't hold it either), so the card sits right under its
|
|
325
|
+
turn in the history, as a reload draws it; a piece removed while the turn slides (the old continue card the step-limit
|
|
326
|
+
question replaces) stays gone.
|
|
304
327
|
|
|
305
328
|
| Piece | Where, from the opening to the turn's end |
|
|
306
329
|
|---|---|
|
|
307
|
-
| The prompt, the steps block (closed; its steps in order from now on, `turn_block.js inOrder`) and the rows the extras held | the history's end, in `handOffOrder`'s order (`history_end.js moveToHistoryEnd`) |
|
|
330
|
+
| The prompt, the steps block (closed; its steps in order from now on, `turn_block.js inOrder`) and the rows the extras held | the history's end, in `handOffOrder`'s order (`history_end.js moveToHistoryEnd`). A lone step's block keeps its chrome there (`data-opened-turn`, until the hand-off), so its summary leads to the step's text when no lead-in shows it (a hook's question asked before its call's row) |
|
|
308
331
|
| The reply that asked: `.bubble.said` (`data-question-id`, `data-said-turn`, never `data-turn-id`: that marks an opening) | the history's end, in the card's task; raw, then its markdown; `.pending` ("asking you", the amber edge) while its card waits |
|
|
309
332
|
| Rows that arrive (`placeFor` `"opened"`: notes, steered prompts, commands, a card that stopped waiting) | the history's end, in arrival order |
|
|
310
|
-
| A card that waits (`stage_cards.js waitsInStage`: `PENDING`, `ASKS`, a warning) | the stage
|
|
311
|
-
| A
|
|
312
|
-
|
|
|
333
|
+
| A card that waits (`stage_cards.js waitsInStage`: `PENDING`, `ASKS`, a warning) | the stage; a docked one shows the asking form (`.asking-form`: the status row and the card; the prompt, the live slots with the working set and the chip hidden), one with a lead-in a "↑ its reason" link in its summary. Two docked at once (check-in still open, then a question or approval): one at a time (`stage_waiting.js shown`; the others `.ts-behind`), the front one first, `‹ 1 of 2 ›` in the status row (`.ts-pager`). A warning stays beside the card. A `PENDING` card in the stage has no toggle of its own (a capture listener refuses the summary's click, tested at the click, so `question_cards.js` sets no `data-toggled`; the summary is out of the tab order, without its ▾): the stage's fold is the one (The stage's height) |
|
|
334
|
+
| A docked card answered or cancelled | right under its lead-in (`settle`), also after the turn's end; one without a lead-in at the history's end while the turn runs (after the turn's end it stays in the stage for the hand-off). Its own clamp is hidden while the lead-in is there (CSS). A card that stopped waiting goes there before a row placed after it in the same task (a replay) |
|
|
335
|
+
| check-in docked, nothing blocking (`.asks-live`) | the status row says the running call (`liveSlots().tool`, `foldedRowText` with `running`) after "waiting for you", then the turn's time; the dot keeps blinking; the live step's text streams under the card (`.ts-asks-text`), the row's word "writing" meanwhile (`stage_model.js phaseLabel`; `data-phase` stays "waiting for you"); at the turn's end `placeAnswer` empties it in the task that fills `.ts-answer`. The composer's delivery line reads blocking cards only (`stage_view.js phase`) |
|
|
336
|
+
| Live slots, the streaming answer, the end line, the queued slot | the stage, as before (no steps chip once opened: the steps line in the history says the count; a tick or a ref still reveals a step there); the hand-off appends what it still holds |
|
|
313
337
|
|
|
314
338
|
A turn released without its end (a lost `turn_completed`, the next turn's start) drops its reasons (`stage_opened.js
|
|
315
339
|
drop`) and leaves the rest; a late recap goes above an opened turn (`place`). The reload check's turn starts at its
|
|
316
340
|
opening: its prompt, else a wake's note (`data-opening-id`) right before it, notes that reached the history mid-turn
|
|
317
341
|
included (`wholeTurn`).
|
|
318
342
|
|
|
343
|
+
**Where the waiting-card state lives:** `stage_waiting.js`, one registry per stage keyed by a card's id (`q:<qid>`,
|
|
344
|
+
`c:<card id>`, `continue` for the old continue card), never by its node (`card.js drawCard` may swap a plugin card's
|
|
345
|
+
DIV for a DETAILS; a resync draws a question card anew). `place` adds (`arrive`: the kind, docked, approval);
|
|
346
|
+
`question_cards.js` settles through its `onSettled` hook (answered, cancelled, replaced, removed, the
|
|
347
|
+
continue card resolved or dropped), `cards_place.js upsertCard` settles a plugin card redrawn without its actions,
|
|
348
|
+
`turnEnded` the warn cards that leave, and `emptySlots` resets it. A second feed only settles: at each refresh
|
|
349
|
+
`stage_asking.js reconcileWaiting` drops an entry whose card is gone from the slots or waits no more (a card answered
|
|
350
|
+
outside `question_cards.js`, as the stage's unit tests do), and again before the fold, `pendingCard` and an opened row.
|
|
351
|
+
The registry's order is the stage's: blocking cards before the asking ones, arrival order within each, and `place`
|
|
352
|
+
puts a blocking card before check-in's in the slot, so a live page and a reload agree whatever the replay's order. The
|
|
353
|
+
asking form's card is the shown docked entry, the fold is the folded key (`stageCollapsed` compares keys); `opener`'s
|
|
354
|
+
lead-in Map is keyed the same way (a card drawn again with its key keeps its bubble: `relink`). `pendingCard`,
|
|
355
|
+
`asksCard`, the approval's cap, the summaries' tab order and the frame's waiting card read the registry.
|
|
356
|
+
|
|
319
357
|
The stage says where the block is (`blockPlace`: cloud, answer, history; `data-block="history"` on the stage for
|
|
320
358
|
`refs.js turnBlockOf`); the chip and a tick reveal the block or a step in the history. **Follow:** every row that reaches
|
|
321
359
|
the history follows a reader who was at its end (`atHistoryEnd`, read before the change); a reason is read from its first
|
|
@@ -325,10 +363,12 @@ the pieces, in their task): the reasons go and each answered card's two-line cla
|
|
|
325
363
|
row in view after a reason keeps its place when the history has room to scroll. The hand-off also waits while the pointer
|
|
326
364
|
or a selection is in a reason (`stage_watch.js alsoWatched`). The reload check gets the turn from its opening (`wholeTurn`).
|
|
327
365
|
**A reload or a resync** draws the same picture: `renderSessionView` replays the running turn (`data.replay`), in which
|
|
328
|
-
the asking call
|
|
329
|
-
|
|
330
|
-
(
|
|
331
|
-
|
|
366
|
+
the asking call may have completed when its `question_requested` comes (`TurnAccumulator.join_events`: a question's
|
|
367
|
+
always, an approval's once it is answered), so the asking step is the last one whose last call is the one that asks
|
|
368
|
+
(any of its calls: `ask_user_question`; for an approval the tool it approves, the card's `data-tool`) and whose reply no
|
|
369
|
+
question card of this page took yet (`stage_asking.js askingGen`, `asked`; live, the step whose call runs). Live or
|
|
370
|
+
replayed, a step's reply is one card's lead-in: a second approval, or a question after an approval, of the same step has
|
|
371
|
+
none. The replayed card opens the turn as live; a question answered earlier in the turn settles under its reason with the
|
|
332
372
|
replayed `question_answered`, and `releaseHeldWait` reads a pending card, not the `waiting` class the replay's one task
|
|
333
373
|
hasn't refreshed yet.
|
|
334
374
|
|
|
@@ -347,8 +387,8 @@ counts. So:
|
|
|
347
387
|
`init-spin .8s steps(16)`, 20 updates/s.
|
|
348
388
|
- A blocking card (`.turn-stage.waiting`: a question, an approval, a delegate's relayed one, whose parent's turn blocks on
|
|
349
389
|
it) stops them all, so movement means chi is working; `stage.spec.js`'s question scenario checks it, also after a
|
|
350
|
-
reload. An asking card (check-in) has no `.waiting`, as its turn goes on: its dot keeps blinking
|
|
351
|
-
outside the stage (plugin setup only).
|
|
390
|
+
reload. An asking card (check-in) has no `.waiting`, as its turn goes on: docked, its dot keeps blinking and the status
|
|
391
|
+
row says the running call (A turn opened in the history). `.init-spin` is outside the stage (plugin setup only).
|
|
352
392
|
- A one-shot fade on something that arrives many times a second is a recurring cost too. A live answer's new line
|
|
353
393
|
appears at once: its fade alone was about a quarter of the streaming answer's total on the Studio (24-25% of a core to
|
|
354
394
|
17-19%). The headline's `ts-fade` stays: turning it off saved nothing measurable.
|
|
@@ -431,7 +471,7 @@ reach another device or the server.
|
|
|
431
471
|
| `chi_composer_collapsed` | `composer_fold.js` | the composer folded (a card's text field folds it for the time it has the focus on a narrow or touch screen, `composer_grip.js watchCardField`, on top of this and of the visit's state: neither is written) |
|
|
432
472
|
| `chi_composer_floor` | `composer_grip.js`, `composer_mount.js` | the composer's dragged height |
|
|
433
473
|
| `chi_strip_hidden` (`chi_zen`: the old name, read once) | `strip_hidden.js` | the session strip hidden |
|
|
434
|
-
| `chi_stage_collapsed` | `stage_view.js` | the stage collapsed |
|
|
474
|
+
| `chi_stage_collapsed` | `stage_view.js` | the stage collapsed by hand (not written from the asking form, whose fold is that question's only) |
|
|
435
475
|
| `chi_working_set_collapsed` | `working_set_rail.js` | the working set rail collapsed |
|
|
436
476
|
| `chi_markdown_tip_dismissed` | `markdown_tip.js` | the markdown tip dismissed |
|
|
437
477
|
| `chi_update_available_shown` | `version_notices.js` | update notices already shown |
|
data/docs/plugins.md
CHANGED
|
@@ -1432,7 +1432,7 @@ block_renderers:
|
|
|
1432
1432
|
fence has no language in the page and nothing draws, though a hint in the bundle's prompt section still reaches
|
|
1433
1433
|
the model: say so in the bundle's README.
|
|
1434
1434
|
- **Where it draws:** the owner's page only, in answers and closed steps. Participants' pages, cards, question
|
|
1435
|
-
replies,
|
|
1435
|
+
replies, a lead-in bubble, delegate reports and the doc viewer show the source, and so does the terminal.
|
|
1436
1436
|
- **The script's side:** the last file calls `chi.defineRenderer({ init, render })`. `init()` (optional, may be async)
|
|
1437
1437
|
runs once; `render(source, theme)` returns the SVG as a string, or a promise of one. Throw to keep the block as
|
|
1438
1438
|
source. `theme` is `{name, bg, panel, text, muted, border, accent, font}`, from the page's tokens. The script runs in
|
data/docs/releasing.md
CHANGED
|
@@ -101,7 +101,9 @@ The agent does each step and stops where the user has to say yes.
|
|
|
101
101
|
the release's session, bridge, cards, schedules and bundle state, and a
|
|
102
102
|
session's input file, note, continue intent and stop marker and a bundle's
|
|
103
103
|
install journal, under
|
|
104
|
-
`spec/compat/X.Y.Z
|
|
104
|
+
`spec/compat/X.Y.Z/`. `SessionStores` supplies the cross-version
|
|
105
|
+
session-sidecar files; the stale `starting` marker is its documented
|
|
106
|
+
exception. The corpus also records in `wire/` what a worker answers its clients
|
|
105
107
|
(`GET state`, `snapshot` idle and mid-turn, `tail`, and the session file:
|
|
106
108
|
`script/release/compat_wire.rb` runs a real Engine and Bridge with a
|
|
107
109
|
scripted kernel, in process); commit those samples with the release so the
|
|
@@ -109,11 +111,13 @@ The agent does each step and stops where the user has to say yes.
|
|
|
109
111
|
recorded twice and must be equal: a bump that stops with "two recordings
|
|
110
112
|
of wire/<file> differ at <JSON path>" found a field that changes per run
|
|
111
113
|
(a new time, a random id, a measured number). Pin it in
|
|
112
|
-
`CompatWire.normalise` (
|
|
113
|
-
chooses is better pinned at its source), check `spec/release/compat_wire_spec.rb`,
|
|
114
|
-
and run the bump again (nothing is written when it stops). The new
|
|
115
|
-
compat shim's `remove_after` (`Compat::SHIMS`):
|
|
116
|
-
|
|
114
|
+
`CompatWire.normalise` (`*_ms`, `*_ms_sum` and `age` are pinned already, as is `last_turn.seconds`;
|
|
115
|
+
an id the script chooses is better pinned at its source), check `spec/release/compat_wire_spec.rb`,
|
|
116
|
+
and run the bump again (nothing is written when it stops). The new
|
|
117
|
+
VERSION may pass a compat shim's `remove_after` (`Compat::SHIMS`): once the
|
|
118
|
+
shim is also at least 14 days old (`added_on`, `Compat::MIN_AGE_DAYS`),
|
|
119
|
+
`spec/compat_spec.rb` names it, and the shim goes (or its date moves)
|
|
120
|
+
before the release.
|
|
117
121
|
5. **Check.** Add all release files, including the new compatibility corpus, then run it:
|
|
118
122
|
`git add -A && git commit -m "Release X.Y.Z"` and `bundle exec rake release:check`.
|
|
119
123
|
Fix anything it finds in new commits and run it again.
|
data/docs/testing.md
CHANGED
|
@@ -160,6 +160,10 @@ continue's `context/sessions/<id>/left.json`, stored approvals
|
|
|
160
160
|
home (`home.json`). A corpus from
|
|
161
161
|
0.164.0 or before has none of those and skips them; a later one must hold them. 0.165.0's and 0.166.0's approvals are a list, which chi no longer reads: the main read skips them and a spec there
|
|
162
162
|
checks that the file is set aside. The corpus is always read from a copy, since a reader may rewrite or move what it reads.
|
|
163
|
+
A corpus after 0.168.0 also records every registered cross-version session store
|
|
164
|
+
(`SessionStores`): objective checks, tracks and proposal cards, delegates,
|
|
165
|
+
pending cards, reminders and parent overrides. The starting marker is the
|
|
166
|
+
documented stale-file exception.
|
|
163
167
|
A corpus recorded after 0.165.0 also holds `wire/`: a worker's replies to `GET state`, `snapshot` (after a turn, and
|
|
164
168
|
mid-turn with a prompt queued behind it) and `tail`, each `{route, status, body}`, and the session file it saved,
|
|
165
169
|
recorded from a real Engine and Bridge (`script/release/compat_wire.rb`, `CompatWire`: a scripted kernel, read over the
|
|
@@ -212,6 +216,14 @@ Each level owns its own share; a detail is asserted once, at the lowest level th
|
|
|
212
216
|
`UPDATE_TURN_DRAWINGS=1 bundle exec rspec spec/bridge/turn_drawings_spec.rb spec/web/session_show_drawings_spec.rb`
|
|
213
217
|
rewrites their lines and show after an intended change) feed `turn_drawing_parity.test.js`, which draws each one
|
|
214
218
|
live (the real stream handlers and stage) and from its drawing and compares the shapes with no allow list.
|
|
219
|
+
A turn cut mid-way while a card waits is `stage_cuts_parity.test.js`'s: the scenarios in `spec/shared/stage_cuts/`
|
|
220
|
+
(written from `spec/support/stage_cut_scenarios.rb` by `spec/web/session_show_cuts_spec.rb`, which folds the events
|
|
221
|
+
to each cut into a worker's snapshot and sends it through `Web::SessionShow`; `UPDATE_STAGE_CUTS=1` rewrites them)
|
|
222
|
+
are drawn live to the cut (question cards included) and reloaded from the cut's show, and the stage (`stageShape`:
|
|
223
|
+
its cards, the docked ones, the lead-in bubbles, the card in front, the one that shows and the pager, the fold) and
|
|
224
|
+
the history compared; a known difference is listed in its `EXPECTED`, by scenario, cut and field (one gone fails
|
|
225
|
+
too; none today), and on both pages `stage_waiting.js`'s cards agree with the slots, one docked card showing. `STAGE_CUTS_DEBUG=1` prints each cut's differences. Both
|
|
226
|
+
harnesses build their page with `spec/web/public/live_page.js`.
|
|
215
227
|
`spec/shared/turn_ranges.json` (`UPDATE_TURN_RANGES=1 bundle exec rspec spec/bridge/turn_ranges_spec.rb`) pins
|
|
216
228
|
`Bridge::TurnRanges`' turns, their record pairing, the items as the show stamps them and each turn's drawing end;
|
|
217
229
|
timing.test.js checks `turnGroups` (stamps only) against it. Each scenario's
|
data/lib/samagotchi/bridge.rb
CHANGED
|
@@ -82,6 +82,7 @@ module Samagotchi
|
|
|
82
82
|
input_epoch: "waiting-steer monitor",
|
|
83
83
|
last_answer_message: "message snapshot under event lock",
|
|
84
84
|
messages_checkpoint: "replace-only message copy",
|
|
85
|
+
objectives_view: "objectives snapshot under event lock",
|
|
85
86
|
pending_question: "question-desk monitor",
|
|
86
87
|
plugin_warning: "warning-state reader",
|
|
87
88
|
recap: "recap-job reference",
|
|
@@ -372,14 +373,21 @@ module Samagotchi
|
|
|
372
373
|
# against these messages; a reminder's, a Continue's or a note after it
|
|
373
374
|
# hides it no more. Absent otherwise (no prompt with a turn id, a
|
|
374
375
|
# rollback, an older worker's file).
|
|
376
|
+
# objectives: the view this worker announces, computed with the running
|
|
377
|
+
# conversation too; nil when there is no list, absent if reading it fails.
|
|
375
378
|
# @return [Hash] {messages:, current_turn:, working_set:, queued:, queued_commands:, recap:, saved_recap:, continue_offer:, pending_question:,
|
|
376
|
-
# guardrail_warning:, plugin_warning:, init_tasks:, cards:, commands:, chi_version:, event_seq:, event_id:, last_turn_drawing
|
|
379
|
+
# guardrail_warning:, plugin_warning:, init_tasks:, cards:, commands:, chi_version:, event_seq:, event_id:, last_turn_drawing:,
|
|
380
|
+
# objectives:}
|
|
377
381
|
def snapshot
|
|
378
382
|
@engine.synchronize_events do
|
|
379
383
|
seq = @engine.event_count
|
|
380
384
|
messages = @engine.messages_checkpoint
|
|
381
385
|
drawing = last_turn_drawing(messages)
|
|
382
|
-
|
|
386
|
+
objectives_unavailable = Object.new
|
|
387
|
+
objectives = Degrade.call(:bridge, "objectives_snapshot_failed", fallback: objectives_unavailable) do
|
|
388
|
+
@engine.objectives_view(@session_id, messages, state_dir: @state_dir)
|
|
389
|
+
end
|
|
390
|
+
snapshot = {
|
|
383
391
|
messages: messages,
|
|
384
392
|
current_turn: current_turn_with_generation,
|
|
385
393
|
working_set: @accumulator.working_set,
|
|
@@ -398,6 +406,8 @@ module Samagotchi
|
|
|
398
406
|
event_seq: seq,
|
|
399
407
|
event_id: event_id(seq)
|
|
400
408
|
}.merge(drawing ? { last_turn_drawing: drawing } : {})
|
|
409
|
+
snapshot[:objectives] = objectives unless objectives.equal?(objectives_unavailable)
|
|
410
|
+
snapshot
|
|
401
411
|
end
|
|
402
412
|
end
|
|
403
413
|
|
data/lib/samagotchi/client_id.rb
CHANGED
|
@@ -109,6 +109,9 @@ module Samagotchi
|
|
|
109
109
|
HUMAN_IDS.include?(id) || HUMAN_PREFIXES.any? { |prefix| id.start_with?(prefix) }
|
|
110
110
|
end
|
|
111
111
|
|
|
112
|
+
# Whether +client_id+ is the owner's input, not a participant's.
|
|
113
|
+
def owner?(client_id) = human?(client_id) && !review?(client_id)
|
|
114
|
+
|
|
112
115
|
# The WakePolicy row a turn delivery from +client_id+ reads (deliver_turn's
|
|
113
116
|
# two liveness checks): :send for a person (a web tab, a chi TUI, chi
|
|
114
117
|
# send), :schedule, :plugin, :delegate or :participant for those
|
|
@@ -35,14 +35,14 @@ module Samagotchi
|
|
|
35
35
|
|
|
36
36
|
module_function
|
|
37
37
|
|
|
38
|
-
# Whether a message from +client_id+ may run as a session command:
|
|
39
|
-
#
|
|
40
|
-
# client)
|
|
41
|
-
#
|
|
42
|
-
#
|
|
43
|
-
#
|
|
38
|
+
# Whether a message from +client_id+ may run as a session command: the
|
|
39
|
+
# owner's input (ClientId.owner?: a web tab, a chi TUI, chi send, no
|
|
40
|
+
# client) and chi answer's. A participant (review:) is refused on its own.
|
|
41
|
+
# A delegate, a child's report, a context or task wake, a schedule, a
|
|
42
|
+
# reminder, a relay and a plugin may not (the bundles that send,
|
|
43
|
+
# coordinator and skills, send prose).
|
|
44
44
|
def allowed?(client_id)
|
|
45
|
-
ALLOWED_IDS.include?(client_id.to_s) ||
|
|
45
|
+
ALLOWED_IDS.include?(client_id.to_s) || ClientId.owner?(client_id)
|
|
46
46
|
end
|
|
47
47
|
|
|
48
48
|
# Whether +text+ would be a shell line (!<cmd>, !rollback) if a command:
|