samagotchi 0.170.0 → 0.171.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 +29 -1
- data/docs/architecture.md +2 -2
- data/docs/cli.md +37 -23
- data/docs/configuration.md +1 -1
- data/docs/internals/block-renderers.md +2 -2
- data/docs/internals/bridge.md +5 -4
- data/docs/internals/context-telemetry.md +13 -1
- data/docs/internals/engine.md +4 -4
- data/docs/internals/inline-widgets.md +1 -1
- data/docs/internals/objectives.md +1 -1
- data/docs/internals/participants.md +235 -119
- data/docs/internals/web-frontend.md +164 -33
- data/docs/internals/working-set.md +69 -5
- data/docs/releasing.md +2 -1
- data/docs/sessions.md +6 -3
- data/lib/samagotchi/bridge/turn_accumulator.rb +8 -4
- data/lib/samagotchi/bridge.rb +39 -12
- data/lib/samagotchi/bundles/system/manifest.yml +1 -1
- data/lib/samagotchi/delivery.rb +13 -0
- data/lib/samagotchi/engine.rb +5 -1
- data/lib/samagotchi/event_fanout.rb +22 -1
- data/lib/samagotchi/grants.rb +5 -0
- data/lib/samagotchi/guardrails/review_paths.rb +2 -2
- data/lib/samagotchi/image_store.rb +51 -12
- data/lib/samagotchi/kernel_loop.rb +7 -1
- data/lib/samagotchi/llm/stray_tool_call.rb +7 -4
- data/lib/samagotchi/llm_context_stale.rb +4 -1
- data/lib/samagotchi/participant_messages.rb +39 -0
- data/lib/samagotchi/participant_revise.rb +61 -0
- data/lib/samagotchi/participant_uploads.rb +98 -0
- data/lib/samagotchi/reply_wait/poll.rb +5 -1
- data/lib/samagotchi/revise_anchor.rb +9 -0
- data/lib/samagotchi/send_command.rb +5 -0
- data/lib/samagotchi/session_inbox.rb +20 -12
- data/lib/samagotchi/session_manager/delivery.rb +7 -0
- data/lib/samagotchi/session_stores.rb +2 -0
- data/lib/samagotchi/tool_runner.rb +39 -5
- data/lib/samagotchi/tools/memory.rb +26 -7
- data/lib/samagotchi/turn_anchors.rb +0 -3
- data/lib/samagotchi/turn_stages/verify_anchors.rb +13 -1
- data/lib/samagotchi/verify_anchor.rb +6 -0
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +42 -77
- data/lib/samagotchi/web/participant_access.rb +129 -26
- data/lib/samagotchi/web/participant_images.rb +185 -0
- data/lib/samagotchi/web/participant_review.rb +19 -138
- data/lib/samagotchi/web/participant_show.rb +240 -0
- data/lib/samagotchi/web/participant_stream_filter.rb +25 -2
- data/lib/samagotchi/web/participant_view.rb +331 -24
- data/lib/samagotchi/web/public/annotate_bar.css +1 -2
- data/lib/samagotchi/web/public/annotate_bar.js +1 -2
- data/lib/samagotchi/web/public/app.js +124 -88
- data/lib/samagotchi/web/public/block_host.js +7 -4
- data/lib/samagotchi/web/public/bubbles.js +9 -1
- data/lib/samagotchi/web/public/cards_offscreen.js +69 -0
- data/lib/samagotchi/web/public/chip_popover.js +5 -4
- data/lib/samagotchi/web/public/claim_link.js +4 -4
- data/lib/samagotchi/web/public/composer.css +4 -6
- data/lib/samagotchi/web/public/composer_mode.js +11 -6
- data/lib/samagotchi/web/public/composer_mount.js +4 -4
- data/lib/samagotchi/web/public/ctx.js +12 -3
- data/lib/samagotchi/web/public/delivery_line.js +34 -5
- data/lib/samagotchi/web/public/history_shape.js +5 -1
- data/lib/samagotchi/web/public/images.js +7 -1
- data/lib/samagotchi/web/public/index.html +40 -4
- data/lib/samagotchi/web/public/info_bar_model.js +2 -1
- data/lib/samagotchi/web/public/markdown_place.js +5 -4
- data/lib/samagotchi/web/public/owner_claim.js +3 -3
- data/lib/samagotchi/web/public/owner_verify.js +0 -2
- data/lib/samagotchi/web/public/participant_bar.js +108 -0
- data/lib/samagotchi/web/public/participant_main.js +289 -0
- data/lib/samagotchi/web/public/participant_model.js +5 -292
- data/lib/samagotchi/web/public/participant_names.js +6 -1
- data/lib/samagotchi/web/public/participant_revise.js +66 -0
- data/lib/samagotchi/web/public/participants_panel.js +3 -12
- data/lib/samagotchi/web/public/question_cards.js +14 -3
- data/lib/samagotchi/web/public/refs.js +53 -36
- data/lib/samagotchi/web/public/reload_check.js +68 -11
- data/lib/samagotchi/web/public/revise_stack.js +21 -10
- data/lib/samagotchi/web/public/send_turn.js +24 -6
- data/lib/samagotchi/web/public/session_fields.js +1 -1
- data/lib/samagotchi/web/public/session_state.js +12 -4
- data/lib/samagotchi/web/public/session_sync.js +9 -2
- data/lib/samagotchi/web/public/session_view.js +17 -7
- data/lib/samagotchi/web/public/stage_asking.js +6 -4
- data/lib/samagotchi/web/public/stage_owner_wait.js +35 -0
- data/lib/samagotchi/web/public/stage_view.js +18 -4
- data/lib/samagotchi/web/public/stage_waiting.js +74 -6
- data/lib/samagotchi/web/public/storage.js +22 -0
- data/lib/samagotchi/web/public/stream_handlers.js +2 -1
- data/lib/samagotchi/web/public/tokens.css +1 -1
- data/lib/samagotchi/web/public/turn_block.js +3 -2
- data/lib/samagotchi/web/public/turn_events.js +15 -6
- data/lib/samagotchi/web/public/turn_html.js +9 -2
- data/lib/samagotchi/web/public/turn_range.js +1 -2
- data/lib/samagotchi/web/public/verify_target.js +2 -4
- data/lib/samagotchi/web/public/viewer.js +59 -0
- data/lib/samagotchi/web/public/viewer_self.js +53 -0
- data/lib/samagotchi/web/public/working_set.js +11 -5
- data/lib/samagotchi/web/public/working_set_view.js +53 -8
- data/lib/samagotchi/web/public/ws_cards.js +89 -0
- data/lib/samagotchi/web/public/ws_peek.js +106 -0
- data/lib/samagotchi/web/public/ws_preview.css +126 -0
- data/lib/samagotchi/web/public/ws_reader.js +319 -0
- data/lib/samagotchi/web/public/ws_timeline.js +200 -0
- data/lib/samagotchi/web/public/ws_way_in.js +25 -0
- data/lib/samagotchi/web/session_control.rb +39 -14
- data/lib/samagotchi/web/session_create.rb +1 -1
- data/lib/samagotchi/web/session_show.rb +75 -28
- data/lib/samagotchi/web/viewer.rb +70 -0
- data/lib/samagotchi/web/working_set_routes.rb +132 -0
- data/lib/samagotchi/worker.rb +19 -14
- data/lib/samagotchi/working_set.rb +260 -8
- metadata +20 -6
- data/lib/samagotchi/web/public/participant.html +0 -218
- data/lib/samagotchi/web/public/participant.js +0 -719
- data/lib/samagotchi/web/public/participant_annotate.js +0 -76
- data/lib/samagotchi/web/public/review_history.js +0 -72
- data/lib/samagotchi/web/public/tools_overview.js +0 -42
|
@@ -175,17 +175,20 @@ row answers another session's id 404, so a new route is covered without a new ex
|
|
|
175
175
|
|
|
176
176
|
| Route | Permission | A participant gets |
|
|
177
177
|
|---|---|---|
|
|
178
|
-
| `POST /api/sessions/:id/turn` | participant | a
|
|
178
|
+
| `POST /api/sessions/:id/turn` | participant | a turn of their own, or a line into their own running turn at its next step ([Turns](#turns)) |
|
|
179
|
+
| `POST /api/sessions/:id/cancel` | participant | their own running turn stopped, named by its `turn_id` ([Turns](#turns), Stopping their turn); no `turn_id`, the owner's turn or another participant's: 403 `not_permitted`, audited |
|
|
179
180
|
| `POST /api/sessions/:id/turn/:eid/dequeue` | participant | their own queued turn back; another's (the owner's included) is 403 `not_permitted` |
|
|
180
181
|
| `GET /api/sessions/resolve?id=` | participant | their session for its id or a prefix of it; anything else 404 as no session (never an ambiguity listing others) |
|
|
181
182
|
| `POST /api/sessions/:id/wait/prepare`, `…/wait` | participant | only for a turn of theirs (`turn`: the enqueued id `POST …/turn` answered; else 403); `wait/prepare` answers only what that wait needs, `{session_id, baseline: {last_turn}, known_turn, live, last_turn_id}` (`last_turn_id` only when it is their turn), never the owner's cursor, message count, question or running turn; the answer trimmed to `{status, text, turn_id, outcome, cancel_reason}`, the reply's `text` through `ParticipantView.reply` (no thinking or tool call, redacted) and any other text (an error's exception, a failed turn's summary) `ParticipantMessages::TURN_FAILED`; a question or approval their turn raised, or their turn held by the owner's step-limit Continue offer ([Turns](#turns)), is `{status: "waiting_for_owner", detail: "waiting for the session's owner"}` with nothing of it; a revoke or the expiry ends a `wait` already running (401 `token_revoked` / `token_expired`), as it ends their stream |
|
|
182
|
-
| `GET /` | participant | the
|
|
183
|
-
| `GET /api/sessions/:id/review` | participant | the page's data, filtered ([What a participant sees](#what-a-participant-sees)): `{viewer, session: {id, title, status}, subjects, participants, messages, current_turn, waiting_for_owner, queued: {mine, others}, last_event_id}`; a participant's `viewer` has their `usage` |
|
|
183
|
+
| `GET /` | participant | the main page (`index.html`) stamped for them (`App#participant_index_data`: their viewer, the grant's session and its title; [web-frontend.md](web-frontend.md#the-participants-page)); the page reads the rest through the show and the stream below |
|
|
184
184
|
| `GET /api/sessions/:id/stream` | participant | the session's stream through `ParticipantStreamFilter` (only `from_seq` and `snapshot` reach the Bridge); it ends within a poll of a revoke or the expiry |
|
|
185
|
+
| `GET /api/sessions/:id/working_set` | participant | the working set's project files only (`ParticipantView.working_set`), with `running` |
|
|
186
|
+
| `POST /api/sessions/:id/images` | participant | their upload ([Participant images](#participant-images)): refused at their cap, past their attempts or quota, a bmp/tiff/heic; the reply `{file, name, width, height}` |
|
|
187
|
+
| `GET /api/sessions/:id/images/<name>` | participant | only an image they uploaded, `Cache-Control: no-store`; any other the 404 a missing file gets |
|
|
185
188
|
| `GET /api/info` | participant | `{app, version, features}` (what `SessionAccess::Remote`'s check needs); not `pid`, `cwd`, `lan`, `installed` |
|
|
186
|
-
| `GET /api/sessions/:id` | participant |
|
|
189
|
+
| `GET /api/sessions/:id` | participant | the main page's show in the owner's shape, every part filtered (`Web::ParticipantShow`, [The main page's show](#the-main-pages-show)); `?tail=1` and `?timing=1` filtered, `?cards=1` `{cards: []}`; `?brief=1` (what `SessionAccess::Remote#show` reads) `{session: {id, title, status, participants: [{id, name, kind, role}]}}` (the active ones). `title` is the session's own title, or "an untitled session": never `display_title`'s fallback, the first prompt's opening words |
|
|
187
190
|
|
|
188
|
-
Everything else is `:owner`: `/answer`, `/command`,
|
|
191
|
+
Everything else is `:owner`: `/answer`, `/command`,
|
|
189
192
|
`/output` (the raw replies), the hub's `/api/events`, the cap route. Static assets (`/app.js`, …) are served to any principal past the gate: they are the public frontend.
|
|
190
193
|
|
|
191
194
|
## Turns
|
|
@@ -219,34 +222,65 @@ The worker decides it once at the turn's start (`GuardrailWiring#begin_turn` →
|
|
|
219
222
|
(`participant_mode`), the boundary in `GuardrailWiring#checks`, `ToolRunner#unreached_group_refusal` (through
|
|
220
223
|
the call's `TurnContext`: a review turn's boundary words the refusal; a pair turn gets the owner's), the
|
|
221
224
|
mid-turn stop and the cap notice's words. The system prompt's line is the pair one once an active participant is
|
|
222
|
-
stamped pair (`SessionParticipants#prompt_mode`);
|
|
225
|
+
stamped pair (`SessionParticipants#prompt_mode`); their show's `viewer.mode` is their own grant's
|
|
223
226
|
(`ParticipantMode.live_for`).
|
|
224
227
|
|
|
225
228
|
A participant's `POST /api/sessions/:id/turn` (`ParticipantAccess#handle_participant_turn`) takes only the body's
|
|
226
|
-
`prompt
|
|
229
|
+
`prompt`, `delivery` and `images` (refs to their own uploads). Three layers keep it out of anyone else's turn, inside its mode's boundary: it goes into a
|
|
230
|
+
running turn only when that turn is theirs (at its next step, as the owner's message does), else it runs as a turn
|
|
231
|
+
of its own after it.
|
|
227
232
|
|
|
228
233
|
**chi web.** The author is the grant's (`p:<id>`) and the client id `review:<id>` (`ClientId.review`: a human's
|
|
229
|
-
input, origin `review`, source `participant`, labelled `review`); the body's `client_id`, `author
|
|
230
|
-
`no_interrupt`
|
|
234
|
+
input, origin `review`, source `participant`, labelled `review`); the body's `client_id`, `author` and
|
|
235
|
+
`no_interrupt` never count, its `images` only as refs to their own uploads, and its `delivery` only as `queue` or
|
|
236
|
+
next step (none: next step).
|
|
237
|
+
Refused before delivery, each audited:
|
|
231
238
|
|
|
232
239
|
- a line starting `!` or `/` (after leading spaces): 403 `not_permitted`, so no shell line, session command or
|
|
233
240
|
plugin command (v1);
|
|
234
|
-
-
|
|
235
|
-
- the grant's cap: 429 `cap_reached`, from the session's saved analytics (`SessionMetrics.saved_spend`)
|
|
236
|
-
|
|
237
|
-
|
|
241
|
+
- a cut (`delivery: cut`): 403 `not_permitted`: a participant's message never cuts a generation;
|
|
242
|
+
- the grant's cap: 429 `cap_reached`, from the session's saved analytics (`SessionMetrics.saved_spend`);
|
|
243
|
+
- a Revise draft (`revises: {turn_id}`, `ParticipantAccess#participant_revise_refusal`): a live worker without
|
|
244
|
+
`participant_revise` (`ReviseAnchor::PARTICIPANT_FEATURE`): 409 `revise_unavailable` in
|
|
245
|
+
`ParticipantMessages::REVISE_UNAVAILABLE` (never `ReviseAnchor::UNAVAILABLE`'s owner words, which
|
|
246
|
+
`#participant_turn_result` also maps for a worker without Revise at all); then `ParticipantRevise.refusal` over the
|
|
247
|
+
messages a live worker's snapshot has (`show_reads(...).raw_messages`: the file lags it mid-turn and just after a
|
|
248
|
+
turn's end), the file's only with no live worker. The turn must open with their prompt (the author chi web
|
|
249
|
+
stamped), have an answer and not be a Verify turn, else 403 `revise_not_own` (a missing turn id answers the same:
|
|
250
|
+
no existence oracle); every turn opening after it (`ParticipantRevise.opening?`: not the owner's `!` lines, which
|
|
251
|
+
they never see) must be theirs too, else 409 `revise_overtaken`. The audit line names the turn id (`revises`),
|
|
252
|
+
accepted or refused. The worker checks the same rule again at the stamp (`TurnStages::VerifyAnchors#revision_target`,
|
|
253
|
+
up to the Revise turn itself): one overtaken after the send silently becomes a plain turn (it runs and answers, no
|
|
254
|
+
stack, no notice to them or the owner), logged only as `revise_anchor_rejected`. `SessionManager::Delivery` also
|
|
255
|
+
refuses a participant's Revise to a live worker without `participant_revise` (`ReviseAnchor::PARTICIPANT_UNAVAILABLE`);
|
|
256
|
+
- images (`ParticipantImages#participant_images_refusal`, D10 lifted), in this order so nothing tells them which
|
|
257
|
+
files exist: not a list of at most 20 `{file, name}` refs (no `path`): 400 `bad_images` (`IMAGES_SHAPE`); any
|
|
258
|
+
ref they didn't upload (`ParticipantUploads`, read once), whether its file is there or not: 403 `not_permitted`;
|
|
259
|
+
only then the files (`ImageStore.check_refs`; one of theirs gone: 400 `bad_images`, `IMAGE_GONE`, the file named
|
|
260
|
+
only in the audit line); then the model: 422 `no_vision` in `ParticipantMessages::NO_VISION` when chi send's
|
|
261
|
+
check (`SendCommand.vision_answer`) says it takes no images (its full words, which name the model and `/model`,
|
|
262
|
+
only in the audit line's `detail`). The check reads the session's saved model, which can lag a live `/model`;
|
|
263
|
+
the Engine's own no-vision failure stays the backstop (their turn then fails, `TURN_FAILED`). Their refs go to
|
|
264
|
+
`deliver_turn` as `images:`; the turn's audit line counts them (`images`), accepted or refused by the worker.
|
|
265
|
+
|
|
266
|
+
It goes out with that delivery and `pending_limit` = the grant's queue limit + 1 (the running one), never
|
|
238
267
|
into the owner's prompt history. The Bridge counts the author's running and queued turns under its event lock and
|
|
239
268
|
answers 429 `too_many_turns` past it; with no worker, their waiting input files are counted, and the new one
|
|
240
269
|
written, under the session dir's `participants.lock` (`SessionManager.deliver_turn`), so two at once can't both
|
|
241
|
-
pass. The ack is 202 with
|
|
242
|
-
`delivery
|
|
270
|
+
pass. The ack is 202 with the worker's
|
|
271
|
+
`delivery` (`queue`: a turn of its own; `next_step`: into their running turn, or the next turn of an idle worker; a
|
|
272
|
+
file, with no worker yet, reads `queue`); chi web keeps enqueued id → grant (the last 1000), which `dequeue` and `wait` check, falling
|
|
243
273
|
back after a restart to the author the session's files keep (`ParticipantAccess#turn_author`): the waiting or claimed
|
|
244
274
|
input file's, then `last_turn`'s or the settled record's (a refused turn's, `TurnCheckpoint.record_refused`; one whose
|
|
245
275
|
worker died, `TurnCheckpoint.settle`, from its checkpoint mark's `author`).
|
|
246
276
|
|
|
247
|
-
**The worker.** A participant's input file
|
|
248
|
-
(`SessionInbox.waits_for_turn_end?`
|
|
249
|
-
|
|
277
|
+
**The worker.** A participant's input file goes into a running turn only of its own author
|
|
278
|
+
(`SessionInbox.waits_for_turn_end?` / `turn_end_data?` with the running turn's participant author, which the worker
|
|
279
|
+
keeps from the turn's origin, `Worker#@turn_author`), so a drain never merges it into another author's turn, and a
|
|
280
|
+
participant's turn takes nobody else's line; the Bridge queues their `POST /turn` unless their own turn runs
|
|
281
|
+
(`Bridge#same_author_steer?`, under the event log) and never cuts for it; a line merged into their own turn first
|
|
282
|
+
gets the checks their queued turn gets at its start (below: the link, the cap), and one refused is dropped as that turn
|
|
283
|
+
would be, never merged; a participant's `/word` or `!line` is refused by the Bridge (403)
|
|
250
284
|
and dropped from the worker's file path. A participant's turn goes only to a worker that names the `review_turns`
|
|
251
285
|
feature (409 `review_unavailable` otherwise: it has no boundary; restart it), and never as a file while a worker
|
|
252
286
|
that may be older holds the session (504 `worker_timeout`: send again). Before a queued participant turn starts, the
|
|
@@ -260,14 +294,29 @@ Verify turn's anchor (`VerifyAnchor`, `{turn_id, quote}`: a UUID and a quote of
|
|
|
260
294
|
anchor, or a Verify fresh check, sent to a connected worker without it is refused 409 `verify_unavailable`
|
|
261
295
|
(`VerifyAnchor::UNAVAILABLE`: "restart this session to use Verify"; `SessionManager` checks `worker_has?` before it
|
|
262
296
|
delivers, on the features of the sidecar its client was made from), since an older worker would run it as a plain prompt. A session with no connected worker is not gated: the
|
|
263
|
-
worker that starts for it is current.
|
|
264
|
-
(`VerifyAnchor.worker_unavailable?`, from the worker's features; `
|
|
297
|
+
worker that starts for it is current. A participant's show carries `session.verify_unavailable`
|
|
298
|
+
(`VerifyAnchor.worker_unavailable?`, from the worker's features; `participant_show.rb`), and the page disables
|
|
265
299
|
Verify and Verify fresh with a "restart this session" hint.
|
|
266
|
-
**
|
|
300
|
+
**Their message during their own turn goes in at its next step.** A worker names `participant_steer`
|
|
301
|
+
(`Delivery::OWN_TURN_STEER_FEATURE`) when it does; one without it queues every participant message, which is safe by
|
|
302
|
+
construction, so the feature only says what will really happen (chi web's ack, the page's delivery line). A
|
|
303
|
+
message sent while someone else's turn runs (the owner's, another participant's) queues behind it as their own turn.
|
|
304
|
+
|
|
305
|
+
**Stopping their turn.** `POST …/cancel` from a participant (`ParticipantAccess#participant_cancel`) names the
|
|
306
|
+
turn by its `turn_id`: chi web reads the live snapshot's running turn, requires it to be that turn and theirs (its
|
|
307
|
+
author the grant's), and asks the worker to cancel that turn id (`BridgeClient#cancel(turn_id:)`), which
|
|
308
|
+
`TurnState#cancel!` compares under its lock (409 `turn_mismatch`): a turn that ended and gave way to another
|
|
309
|
+
person's between the read and the cancel is never stopped. Without a `turn_id` (the Bridge would cancel whatever
|
|
310
|
+
runs), or for a turn not theirs, it is 403 `not_permitted`, audited. It is relayed only to a worker that names
|
|
311
|
+
`cancel_turn_id` (`Delivery::CANCEL_TURN_ID_FEATURE`): an older one ignores the id and would stop whatever runs, the
|
|
312
|
+
owner's next turn too, so it is 409 `cancel_unavailable` (`ParticipantMessages::CANCEL_UNAVAILABLE`), audited, until
|
|
313
|
+
the owner restarts it. The reason is the server's (`user`), never the body's.
|
|
314
|
+
|
|
315
|
+
**The owner's input during a participant's turn queues.** While a participant's turn runs, the owner's input never merges
|
|
267
316
|
into it, from any client (the web, an attached TUI, `chi send` without `--queue`): the Bridge's `POST /turn` makes it
|
|
268
|
-
`delivery: queue` whatever it asked, so it never steers or cuts (`
|
|
269
|
-
lock), and its ack and `:turn_enqueued` say `queue`; the worker's drain takes
|
|
270
|
-
turn (`Worker#pending_input_drain`, for a file the Bridge never saw), so each runs as the owner's own turn after it.
|
|
317
|
+
`delivery: queue` whatever it asked, so it never steers or cuts (`Bridge#same_author_steer?`, under the event
|
|
318
|
+
lock), and its ack and `:turn_enqueued` say `queue`; the worker's drain takes only the participant's own lines
|
|
319
|
+
during their turn (`Worker#pending_input_drain`, for a file the Bridge never saw), so each runs as the owner's own turn after it.
|
|
271
320
|
The web's delivery line says so (`participantTurn`: Enter queues, no cut), the TUI prints its queued note, `chi send`
|
|
272
321
|
says `(queued: runs after the current turn)` and its `--wait` waits for that turn (`ReplyWait::Target.message`). The owner's
|
|
273
322
|
own turns steer as before. A step-limit Continue of a participant's turn (the owner answers it) continues as theirs: its origin (a `TurnOrigin`,
|
|
@@ -283,8 +332,9 @@ turns run in the order they came, each through the link and cap check above as i
|
|
|
283
332
|
offer too and so releases them. A held file isn't queued input for the idle-exit policy (`Worker#input_pending?`): `/exit`
|
|
284
333
|
and `chi sessions restart` name what waits, the offer (`continue_offered`), not a queued prompt. The offer lives in
|
|
285
334
|
the worker: a worker that restarts has none, and runs them.
|
|
286
|
-
Their `wait`
|
|
287
|
-
|
|
335
|
+
Their `wait` waits on their turn's id, never for the next reply past a cursor (which could be anyone's); a line
|
|
336
|
+
merged into their own running turn is followed into it by its merge record (`ReplyWait::Poll#follow_merge`: chi web
|
|
337
|
+
waits with `queued: false`, and the poll asks `turn_end_data?` as for its author's own turn); on a held turn it answers `waiting_for_owner` (`ReplyWait::Poll#held`), the offer asked before the wait
|
|
288
338
|
began too. A chi client gets it through `--remote` with a participant's token in `remotes.yml`: `chi --remote NAME
|
|
289
339
|
send --wait --turn T ID` reaches only participant routes (resolve, `wait/prepare` and `wait` on their turn), and
|
|
290
340
|
`ParentReport` reads `waiting_for_owner` as its own status, exit 3 like a question, its detail
|
|
@@ -337,14 +387,54 @@ and `ctx.turn_author` names it ([hooks](../hooks.md#participants-turns)).
|
|
|
337
387
|
participant once the grant is revoked or expired, so their stream closes within a poll of a revoke, including one
|
|
338
388
|
made by `chi share revoke` in another process; the page then reads a 401 and shows the ended-link page.
|
|
339
389
|
|
|
390
|
+
## Participant images
|
|
391
|
+
|
|
392
|
+
A participant attaches images to their own turns, as the owner does (`Viewer::PARTICIPANT_CAPABILITIES`' `images`:
|
|
393
|
+
the composer's paste, drop and image button). Review and pair grants alike: an image is input, not tool power; their
|
|
394
|
+
cap and the quota bound the cost (their turn's image tokens count to their cap by author, `SessionMetrics.saved_spend`;
|
|
395
|
+
later turns that re-send it in history are the owner's). `ImageStore` is content-addressed and keeps no metadata, so
|
|
396
|
+
who uploaded what is chi web's own record, `ParticipantUploads` (`participant_images.json`, [Files in the session
|
|
397
|
+
dir](#files-in-the-session-dir)), keyed by (file, grant): an owner's or another grant's identical image never
|
|
398
|
+
changes hands, and nothing else (a ref's `source` or `name`, the body's `author`, a message) grants a file.
|
|
399
|
+
|
|
400
|
+
**Upload** (`POST …/images`, `ParticipantImages#participant_image_upload`). Every upload counts as an attempt first,
|
|
401
|
+
before the cap check (a cap-refused one too). Refused, in order: their cap (429
|
|
402
|
+
`cap_reached`); past `Grants::MAX_IMAGE_ATTEMPTS` (80) uploads tried, refused ones counted (429
|
|
403
|
+
`too_many_image_attempts`); the owner's size cap (413 `too_large`); not an image chi stores (422 `bad_image`, in
|
|
404
|
+
`ParticipantMessages::BAD_IMAGE`, never `ImageStore`'s words: an image tool's output may name a host path); a
|
|
405
|
+
bmp, tiff or heic (422 `unsupported_image`, `IMAGE_TYPE`: sniffed with `ImageHeader.read` before `ingest`, so sips
|
|
406
|
+
or magick never converts their bytes from those formats); a header with no size, or over 40 megapixels or 12000 px
|
|
407
|
+
on a side (422 `image_too_big`, `IMAGE_TOO_BIG`; a phone's 4032×3024 passes); at `Grants::MAX_IMAGES` (40)
|
|
408
|
+
distinct files (429 `too_many_images`), unless the bytes are a file they own already (as stored unchanged,
|
|
409
|
+
`ImageStore.file_name_for`: a downscaled or re-encoded one is refused). A png/jpeg/gif/webp over the store's limits
|
|
410
|
+
is still downscaled by `ImageStore.fit`, so the tool does read their bytes: their conversions run one at a time
|
|
411
|
+
(`@participant_conversion_mutex`), and every conversion (anyone's) is killed past `ImageResizer::TIMEOUT` (60 s),
|
|
412
|
+
magick reading the sniffed format's coder under `-limit memory 256MiB -limit disk 1GiB -limit area 64MP`. An
|
|
413
|
+
upload that raises is audited (`internal_error`) before chi web's 500. The quota is checked under `@participant_images_mutex`, the
|
|
414
|
+
image ingested, then checked again and recorded under it (a race past it is refused there; the stored file may stay
|
|
415
|
+
in `images/`, unreachable to them). Each answer is an `image` audit line. The reply is the ref without `source`,
|
|
416
|
+
`mime` or `bytes`.
|
|
417
|
+
|
|
418
|
+
**Read back** (`GET …/images/<name>`, `SessionControl#handle_image`): a participant gets the file only when
|
|
419
|
+
`ParticipantUploads.owned?` says they uploaded it (the record proves they had the bytes), with `Cache-Control:
|
|
420
|
+
no-store`; anything else (the owner's, another grant's, a guess) is the same 404 a missing file gets, body and
|
|
421
|
+
headers alike. A revoked or expired grant is refused by the gate (401) as on every route.
|
|
422
|
+
|
|
423
|
+
**Their turn** takes only refs they uploaded ([Turns](#turns)); **what they see** is their own images as thumbnails
|
|
424
|
+
and anyone else's as "N images" ([What a participant sees](#what-a-participant-sees)). A corrupt or unreadable
|
|
425
|
+
record fails closed (every grant at quota, owning nothing, `Log.warn`); the owner can delete the file. Not covered:
|
|
426
|
+
a participant's `chi --remote … --image`; a failed turn of theirs gives the text back without its chips; Apply
|
|
427
|
+
doesn't mention a proposal's images.
|
|
428
|
+
|
|
340
429
|
## What a participant sees
|
|
341
430
|
|
|
342
|
-
D5 (a): prompts and replies with their authors, one-line tool rows, turn boundaries, the queue and waiting states
|
|
431
|
+
D5 (a): prompts and replies with their authors, one-line tool rows, turn boundaries, the queue and waiting states
|
|
432
|
+
(and for what kind of thing: an approval, a question, a continue offer); for the main page (2026-11-13) also the
|
|
433
|
+
context's fill, the speed, each turn's time and the working set's file names.
|
|
343
434
|
Not tool output or arguments, thinking, approvals' and questions' content, memories (text or names), context notes,
|
|
344
|
-
plugin steers, automatic and delegate input, cards, command output, the working set
|
|
345
|
-
(but their own usage, as their grant's exposure shows it),
|
|
346
|
-
|
|
347
|
-
and hover times, D6 of the session-time-gaps plan), host paths or the owner's prompt history. Every read a participant reaches goes through one module,
|
|
435
|
+
plugin steers, automatic and delegate input, cards, command output, the working set's memories and URLs, cost
|
|
436
|
+
(but their own usage, as their grant's exposure shows it), tokens and cost per model, guardrail judgments,
|
|
437
|
+
host paths or the owner's prompt history. Every read a participant reaches goes through one module,
|
|
348
438
|
`Web::ParticipantView` (`lib/samagotchi/web/participant_view.rb`), whose every answer is a new hash built from named
|
|
349
439
|
fields: a field added to an event later reaches nobody until it is named there.
|
|
350
440
|
|
|
@@ -354,12 +444,18 @@ fields: a field added to an event later reaches nobody until it is named there.
|
|
|
354
444
|
type and turn id; `turn_completed` may also carry an allowlisted verdict card), `generation_chunk` (its visible
|
|
355
445
|
`text` only; a thinking-only chunk is dropped),
|
|
356
446
|
`generation_retrying` (`restarted`), `tool_call_started` (the tool's name, and for a file tool the path relative to
|
|
357
|
-
the session's folder when it is inside it
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
447
|
+
the session's folder when it is inside it, as the owner's `ref`
|
|
448
|
+
`{kind: "file", path, line, end_line}`; never when that folder is home or `/`),
|
|
449
|
+
`tool_call_completed` (its status word); the viewer's **own** queue events in the owner's shape (`turn_enqueued`:
|
|
450
|
+
`enqueued_id`, `client_id`, `prompt`, `delivery`, `author`, their image refs; `turn_dequeued`: its `enqueued_id`, never
|
|
451
|
+
the client that took it back, the filter knowing their ids from the `turn_enqueued` it sent them and from
|
|
452
|
+
`ParticipantAccess#own_turn?`; `input_merged`: the count and origins of their own lines only), anyone else's bare;
|
|
453
|
+
`question_requested` and `continue_offered` with one derived `kind` (`.waiting_kind`: `approval` for a guardrail's
|
|
454
|
+
approval or a relayed one, `continue` for the step-limit question and every offer, `question` else); the main
|
|
455
|
+
page's numbers (`context_status`: usage's `estimated_pct` and token counts, `bucket`, a switch's
|
|
456
|
+
`context_window_tokens`, never `status`, the model-facing line; `generation_completed`: its `speed` only;
|
|
457
|
+
`working_set_updated`: `.working_set`, below); and bare markers (the type alone) for the answers to a question or
|
|
458
|
+
offer, and `snapshot` / `reset` (the page re-reads the session at a `reset`, as the owner's). Every other type,
|
|
363
459
|
known (`DROPPED`) or new, is dropped. `spec/web/participant_view_spec.rb` scans every `type: :name` chi emits and
|
|
364
460
|
every handler of the web's stream, and fails until a new one is named in `EVENTS` or `DROPPED`.
|
|
365
461
|
- **The frames** (`Web::ParticipantStreamFilter`). The Bridge's chunks are cut into frames (`\r?\n\r?\n`, a partial
|
|
@@ -372,8 +468,8 @@ fields: a field added to an event later reaches nobody until it is named there.
|
|
|
372
468
|
and never after a key word whose value is still to come, `Redact::OPEN_VALUE`; past 1600 with no such space,
|
|
373
469
|
the line's last unbroken run and the rest of that line go out as one `[redacted]`). The owner's stream never
|
|
374
470
|
goes through it (a byte pipe).
|
|
375
|
-
- **The conversation** (`.messages`, over `App#messages_for_display(parts: true)`, `Web::MessageDisplay`;
|
|
376
|
-
|
|
471
|
+
- **The conversation** (`.messages`, over `App#messages_for_display(parts: true)`, `Web::MessageDisplay`; each finished
|
|
472
|
+
turn's drawing comes with the show, [The main page's show](#the-main-pages-show)): a user message only when it is a
|
|
377
473
|
turn's prompt (it has the turn's id) or a person's merged line, and never one from a source that isn't a person's;
|
|
378
474
|
so the owner's `!` line and its output, a delegate's report (a wake turn's too), and the notes chi adds (a declined
|
|
379
475
|
Continue) never show. A turn no person sent (its record's `automatic`, `SessionMetrics`: a task's or a plugin's,
|
|
@@ -384,11 +480,55 @@ fields: a field added to an event later reaches nobody until it is named there.
|
|
|
384
480
|
links group by `[data-turn-id]`). A raw loop's saved
|
|
385
481
|
reply is cut before the display reads it (`.visible_raw`): what came before a bare `</think>` and everything from
|
|
386
482
|
an unclosed thought or tool call on, which the display would only untag. Each
|
|
387
|
-
prompt carries its author and
|
|
483
|
+
prompt carries its author and its images: the viewer's own as refs, anyone else's as a count
|
|
484
|
+
(`.image_refs`, below); replies their text and their step's tool rows. Notes, steers and
|
|
388
485
|
empty-answer markers are dropped. The reply's
|
|
389
486
|
`html` is rendered from the filtered text. The turn in progress (`.current_turn`, over the Bridge snapshot's) gives
|
|
390
487
|
its start's fields, the text so far, its tool rows (with `running`) and `waiting_for_owner`.
|
|
488
|
+
- **Images** (`.image_refs`, on a message, a `turn_started` (so the running turn and the drawings) and a queued
|
|
489
|
+
entry): the viewer's own as `[{file, name, width, height}]`, only refs to a stored image (`ImageStore::REF_RE`;
|
|
490
|
+
any other dropped), never `source`, `bytes` or `mime`; anyone else's (the owner's, another participant's) as a
|
|
491
|
+
count, which the page draws as a muted "N images" line (`images.js thumbsTemplate`).
|
|
391
492
|
- **The queue.** Their own queued turns (enqueued id and text, for Take back) and how many others wait.
|
|
493
|
+
- **The working set** (`.working_set`: the event and `GET …/working_set`): the file items inside the project, the
|
|
494
|
+
key made relative, with `state`, `tokens`, `ops`, `count`, `pinned`, and the set's `at`. Never a memory or URL
|
|
495
|
+
item, a path outside the project, `first_id`, `has_drop` or the pending `adds`.
|
|
496
|
+
|
|
497
|
+
### The main page's show
|
|
498
|
+
|
|
499
|
+
`GET /api/sessions/:id` for a participant (`Web::ParticipantShow#participant_main_show`) is the main page's read
|
|
500
|
+
in the owner's shape (`SessionShow#owner_show`'s keys, from the same `show_reads`), every part through
|
|
501
|
+
`ParticipantView`: the one read their page makes (the participant page's `GET …/review` is gone).
|
|
502
|
+
`spec/web/participant_show_spec.rb` holds its parity: no key the owner's lacks but the
|
|
503
|
+
verdicts' (`fresh_running`, `session.verify_unavailable`).
|
|
504
|
+
|
|
505
|
+
- `messages` as the review read's (a turn's opening with its Revise stamp, `revises: {turn_id}`, and a Verify
|
|
506
|
+
turn's `verify: true`, a boolean, never the quote; `turn_completed` carries both too), stamped `drawn` (the turn whose drawing draws them); `turn_drawings` each
|
|
507
|
+
finished turn's drawing replayed event by event through `.event` (no cards), its start's prompt the filtered
|
|
508
|
+
messages' prompt of that turn, else none (an automatic turn, a wake, a delegate child's task: a made drawing's
|
|
509
|
+
start has no origin to say so), its end `.drawing_end` (type, turn id, duration, kept steps; never a failure's
|
|
510
|
+
words or a cancel's reason).
|
|
511
|
+
- `current_turn` (`.current_turn`: its start, text, tool rows, `waiting_for_owner` and `kind`, and the generation's
|
|
512
|
+
phase, `generation: {thinking_only, age}`, the delivery line's clock), `replay` (the join's events through
|
|
513
|
+
`.event`) and `queued` (the viewer's own entries, `.queued_entry`).
|
|
514
|
+
- `session`: id, participant title, status (idle when a worker says running with no turn), `bridge_up`,
|
|
515
|
+
`worker_features`, `verdicts` (filtered, as the review's) and `verify_unavailable`.
|
|
516
|
+
- `viewer`: `Web::Viewer`'s kind, id, name, `can` (`Viewer::PARTICIPANT_CAPABILITIES`: `send`, `stop_own_turn`,
|
|
517
|
+
`steer_own_turn`, `revise_own_turn`, and of the owner's `working_set` and `images`), `client_id` (`review:<grant id>`, which chi web gives their
|
|
518
|
+
turns), their `participant_kind`, `mode`, `cap_reached` and `usage`, `session_stopped`, and
|
|
519
|
+
`waiting_for_owner` (the kind the session waits on, the review read's rule: the running turn's question, a
|
|
520
|
+
Continue offer, a question between turns; nil when nothing).
|
|
521
|
+
- `timing` (`.timing`): the session's clock, each turn record's id, times, `duration_ms`, `decode_tps` and
|
|
522
|
+
`tps_source`, the running turn's start, the context's `used_tokens` and `window_tokens`. Never `tool_records`,
|
|
523
|
+
`active_tools`, `tokens`, `model_segments`, `memory_index`, `side`, nor a record's cost, tokens, model, author,
|
|
524
|
+
cancel reason or failure.
|
|
525
|
+
- Empty or nil: `history`, `commands`, `cards`, `init_tasks`, `queued_commands`, `recap`, `saved_recap`,
|
|
526
|
+
`continue_offer`, `pending_question`, `failed_turn`, the warnings, the markdown tips; `capabilities` never names
|
|
527
|
+
the editor.
|
|
528
|
+
|
|
529
|
+
The main page's `<body>` for a participant (`App#participant_index_data`): `version`, `session`,
|
|
530
|
+
`session-title`, the Annotate presets, the viewer and `working-set` (the owner's `web.working_set`); nothing of the
|
|
531
|
+
owner's machine (`home`, the folders, `project-*`, `back-*`).
|
|
392
532
|
- **Their usage** (`viewer.usage`, `ParticipantReview#review_usage`, built from named fields by the exposure policy
|
|
393
533
|
beside `ParticipantView`'s): the fields their grant's exposure shows
|
|
394
534
|
([Grants](#grants)); at `own`, `{spent, cap, queued, queue_limit, expires_at, model}`. `spent` is dollars: the saved
|
|
@@ -397,62 +537,33 @@ fields: a field added to an event later reaches nobody until it is named there.
|
|
|
397
537
|
running turn shows before it is saved; the two are never added), read from the Bridge snapshot the review already
|
|
398
538
|
reads. `queued` counts their own queued turns on the worker; `model` is the session's model as its host is sent it
|
|
399
539
|
(the alias applied, no host name), live else saved, redacted. Every source reads the grant's own author: another
|
|
400
|
-
participant's spend, the owner's and the session's totals never reach them (the red-team spec checks the body).
|
|
401
|
-
|
|
402
|
-
(
|
|
403
|
-
$2.00 cap · 1/3 queued · link: 6 d left · model", each with a title in words; warn colours from 80% of the cap, a
|
|
404
|
-
full queue and a link's last day, "over" at the cap. The key hint is the line above the text field, wrapping to at most two (hidden on a
|
|
405
|
-
phone). The usage is cut at the end (the model first); folded, it stays as the card's footer under the grip. It is redrawn at each re-read (a
|
|
406
|
-
turn's end, a queued or taken-back comment), not while a turn streams.
|
|
540
|
+
participant's spend, the owner's and the session's totals never reach them (the red-team spec checks the body). Their
|
|
541
|
+
page draws the spend of the cap in its bottom row ("$0.42 of $2.00 used") and the link's end in its top row; it reads
|
|
542
|
+
the show again at each turn's end for it (the light re-reads carry no viewer).
|
|
407
543
|
- **A fork's note.** A review fork (`chi review --from`, `/coordinate review`) starts with a note holding the source's
|
|
408
544
|
recap and, from the coordinator, its handoff's body: a participant's review turn reads it like the rest of the
|
|
409
545
|
history and may quote it (host paths, worktree names). That is by design (it is what the review is about); the
|
|
410
546
|
owner chose to share it. Nothing else of the source comes along.
|
|
411
|
-
- **
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
access to the project tree, the owner's, so it isn't a participant's way out; the next read checks again.
|
|
420
|
-
`[]` for a session that isn't a review session. The page's title for a
|
|
421
|
-
review session with no title of its own is `review of docs/plan.md` (`+N` for more), never the first prompt. The
|
|
422
|
-
page shows them as the Plan: beside the thread on a wide screen (≥ 1100 px), a Plan | Thread toggle on a narrower
|
|
423
|
-
one (the thread first, the composer pinned in both views); several subjects get a file picker. It re-reads them
|
|
424
|
-
with the rest at every turn boundary and redraws the Plan only when they changed.
|
|
425
|
-
- **Annotate** (`participant_annotate.js` over the owner's `annotate_bar.js`, styled by the shared
|
|
426
|
-
`annotate_bar.css`). A participant who can comment selects text in the Plan or the thread and gets **Annotate** and
|
|
427
|
-
the owner's `web.annotate_presets` (the participant shell's `data-annotate-presets`, as `index.html`'s), which append
|
|
428
|
-
the quote to the comment box; nothing is sent. Labels: the Plan `From docs/plan.md:` (the subject shown), a comment
|
|
429
|
-
`From Sam's comment:` / `From my earlier comment:` / `From the owner's message:` (the message's `data-by`,
|
|
430
|
-
`data-name`), a reply none. Not annotatable: the reply still streaming, tool rows, waiting lines, gap lines, the
|
|
431
|
-
header, the Plan's truncation note. No plugin pills (a review turn refuses `/` lines), and none of it in the owner's
|
|
432
|
-
participant view (no presets in its shell) or while `can_comment` is false. On a touch screen the bar docks above the
|
|
433
|
-
composer. A re-read keeps unchanged thread nodes and the Plan's (redrawn only on change), so a selection survives it.
|
|
434
|
-
After the presets comes the built-in **Verify** (`verify_preset.js`, on the owner's page too): the quote, then
|
|
435
|
-
"Verify this claim against the code. If this session's context doesn't settle it, say so. End with a line
|
|
436
|
-
`Verdict: confirmed|refuted|unclear — <one-line reason>`." It fills the box like any preset and never sends.
|
|
437
|
-
It is offered only where the Engine will take the claim (`verify_target.js`, the bar's `offeredPreset`): a
|
|
438
|
-
selection in the final answer of an ended turn, the last answer bubble (the owner's `.bubble.output` that isn't a
|
|
439
|
-
delegate report's reply; a participant's last `.p-msg.reply > .p-bubble`) between the turn's opening
|
|
440
|
-
(`[data-turn-id]`) and the next one (or, on the owner's page, a context note: a wake turn opens with one; a
|
|
441
|
-
participant's page gets a wake turn's opening as an automatic turn).
|
|
442
|
-
Anywhere else (an earlier reply, a step, a prompt, a line merged mid-turn, the Plan, the running turn) the button
|
|
443
|
-
is hidden and the bar keeps its other actions.
|
|
547
|
+
- **No file view.** A review session's subjects are not sent to a participant (their page has no file panel; they ask
|
|
548
|
+
chi to quote the part they need: [docs/sessions.md](../sessions.md#review-sessions)). Its title for a review session
|
|
549
|
+
with no title of its own is `review of docs/plan.md` (`+N` for more), never the first prompt.
|
|
550
|
+
- **Annotate** is the main page's (`annotate_bar.js`): a selection in the conversation offers **Annotate** and the
|
|
551
|
+
owner's `web.annotate_presets` (their `<body>`'s `data-annotate-presets`), which append the quote to the composer,
|
|
552
|
+
then the built-in **Verify** (`verify_preset.js`), offered only where the Engine takes the claim (`verify_target.js`
|
|
553
|
+
`OWNER_FINAL`: the final answer of an ended turn). No plugin pills (their show's `commands` is empty) and no Copy
|
|
554
|
+
link (no participants panel).
|
|
444
555
|
- **Deep links to a claim** (`claim_link.js`). The owner selects text in a turn's answer, step or own message and
|
|
445
556
|
Annotate's **Copy link** copies `<base>/#s=<id>&t=<turn8>&q=<quote>&p=<prefix>`: `base` the participants list's
|
|
446
557
|
`link_base` (`ParticipantsAdmin#participant_base`: `web.public_url`'s origin, else the LAN address, else the Host),
|
|
447
558
|
`t` the turn's id (its first 8 characters, the opening message's `data-turn-id`), `q` the selection's rendered
|
|
448
559
|
text (whitespace collapsed, at most 200 characters) and `p` up to 32 characters before it in the same bubble, both
|
|
449
|
-
URL-encoded by `URLSearchParams`. The text is read as
|
|
560
|
+
URL-encoded by `URLSearchParams`. The text is read as a participant's page shows it, by `Redact`'s own rules
|
|
450
561
|
mirrored in JS (`spec/fixtures/redact_cases.json` holds cases both sides must read the same): secret shapes
|
|
451
562
|
`[redacted]`, whole even when the selection cuts into one, so a link never carries a secret; on the owner's page
|
|
452
563
|
also the session's folder relative and the home folder `~` (the page's `data-home`). Not the owner's token (the
|
|
453
564
|
page doesn't hold it). With no person (`kind: human`) active in the session, Copy link gives the owner's own form,
|
|
454
|
-
`#/s/<id>?t=…&q=…&p=…`; the
|
|
455
|
-
|
|
565
|
+
`#/s/<id>?t=…&q=…&p=…`; the main page opens either form (`owner_claim.js`, a participant's page too: its one
|
|
566
|
+
session is the claim's). Opening one: the turn's bubbles (answers, own messages and steps) are searched for the prefix and quote, then
|
|
456
567
|
the quote alone; the match's bubble is ringed (`.claim-hit`), a closed `<details>` around it opened, and the quote
|
|
457
568
|
highlighted (the CSS Highlight API, `::highlight(chi-claim)`, where the browser has it). A quote not found (text
|
|
458
569
|
that reads differently) rings the turn's opening. A turn no opening carries (a non-person turn the page doesn't
|
|
@@ -478,11 +589,10 @@ fields: a field added to an event later reaches nobody until it is named there.
|
|
|
478
589
|
a valid `Verdict: confirmed|refuted|unclear — <reason>` appends a verdict to the session. Both pages place its card
|
|
479
590
|
in the source turn, right after the bubble that holds the quote (the answer's, matched as a claim link reads the
|
|
480
591
|
page; a quote not found yet goes at the turn's end), name its author, show the reason and quote, and link to the
|
|
481
|
-
checking turn; a live completion adds the card without a reload.
|
|
592
|
+
checking turn; a live completion adds the card without a reload. A participant receives only the card fields through its allowlist, with the
|
|
482
593
|
quote and reason redacted by the view's normal rules, never the full reply.
|
|
483
594
|
- **Verify fresh** (`VerifyFresh`, `verdict_fresh.js`). An `unclear` card has a **Verify fresh** button (the owner's
|
|
484
|
-
page always; a participant's while
|
|
485
|
-
participant view). It is `POST /api/sessions/:id/verdicts/:check_turn_id/fresh` (`VerifyFreshRoutes`, `:owner`
|
|
595
|
+
page always; a participant's while their grant could run a turn now). It is `POST /api/sessions/:id/verdicts/:check_turn_id/fresh` (`VerifyFreshRoutes`, `:owner`
|
|
486
596
|
and `:participant`), which starts a **fresh child**: a delegate child (`spawn_session`, `delegate: true`,
|
|
487
597
|
`parent_id`) in the session's own folder (no worktree; a folder outside git too), on its model, whose first and only
|
|
488
598
|
prompt is the claim, quoted, and "Verify this claim against the files here. You have none of the original session's
|
|
@@ -539,38 +649,34 @@ fields: a field added to an event later reaches nobody until it is named there.
|
|
|
539
649
|
data). The card shows "Verifying fresh…" while the child runs (the summary's `verify_fresh` `{of, child_id}`,
|
|
540
650
|
the review data's `fresh_running`, ids only) and the fresh verdict as a `fresh: ✅ confirmed — …` line on the same
|
|
541
651
|
card. The original stays unclear, so the button stays, as **Verify fresh again**: each press is another paid
|
|
542
|
-
child (a participant's against their cap).
|
|
652
|
+
child (a participant's against their cap). `ParticipantView` passes `fresh` and `of`, never the child's id (a
|
|
543
653
|
participant can't open it). No push
|
|
544
654
|
for a fresh verdict yet (`Attention` pushes a verdict at a turn's end).
|
|
545
|
-
- **The
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
turn; Alt+Enter is Enter), the model picker, Stop turn (the owner page's sits in its stage's status row; this
|
|
555
|
-
page has no stage), and images: the chips are mounted but `imagesOff` refuses a
|
|
556
|
-
paste, drop or pick with one line (the image button is hidden) until participant images land. The grip's floor and
|
|
557
|
-
the fold are the owner page's keys (`chi_composer_floor`, `chi_composer_collapsed`), per browser. The owner's
|
|
558
|
-
participant view mounts none: its dock stays hidden.
|
|
655
|
+
- **The composer** is the owner's, with what is theirs (`participant_main.js`; [web-frontend.md](web-frontend.md#the-participants-page)):
|
|
656
|
+
no `/` list or plugin pills, no ↑/↓ prompt history (the owner's `GET /api/history`), no model picker. Images go
|
|
657
|
+
in as the owner's do (paste, drop, the image button), within their quota ([Participant images](#participant-images));
|
|
658
|
+
a refused upload or send keeps the chips and toasts the server's words. While their own turn runs, a message goes in at its next step on a
|
|
659
|
+
worker with `participant_steer` (Alt+Enter queues it); while anyone else's runs, it goes in as their own turn after
|
|
660
|
+
it, and the delivery line says so. Stop turn shows on their own turn only. At their cap, in a stopped session, or
|
|
661
|
+
once their link ended, the composer gives way to one line (the cap's own words; the server's for the others) and
|
|
662
|
+
the conversation stays readable. A turn of theirs that waits for the owner (an approval, a question, a Continue
|
|
663
|
+
offer) gives the stage its waiting look with one line naming the kind, never a card.
|
|
559
664
|
- **Redaction** (`Samagotchi::Redact`, `lib/samagotchi/redact.rb`, on every string that goes out): secret shapes (`sk-…`, `ghp_…`,
|
|
560
665
|
`github_pat_…`, `AKIA…`, `xox…-`, `chp_…`, `AIza…`, `rk_live_…`, JWTs, Slack webhooks, private key blocks, an
|
|
561
666
|
`Authorization:` header's value, any `…token` / `…api_key` / `…secret` / `…password` value, `=` or `:`, quoted or
|
|
562
667
|
not, and a URL's password), the owner's web token, the session's folder (shown as relative paths) and the home
|
|
563
668
|
folder (`~`). Defence in depth only: the allowlist is the control; a reply that paraphrases a secret isn't caught.
|
|
564
669
|
- **Errors.** A participant's 404 and 500 carry fixed words, never an exception's (which may name a path); so does their `wait` on a turn that failed (`TURN_FAILED`).
|
|
565
|
-
- **
|
|
566
|
-
|
|
567
|
-
|
|
670
|
+
- **No "view as a participant" for the owner.** The owner's `/?as=participant` view went with the participant page
|
|
671
|
+
(2026-11-13: every read on the main page would need the flag threaded through, and one missed read would show
|
|
672
|
+
unfiltered data in a view that claims to be the participant's).
|
|
568
673
|
|
|
569
|
-
The red-team spec (`spec/participant_red_team_spec.rb`) reads the stream,
|
|
570
|
-
|
|
571
|
-
a
|
|
572
|
-
|
|
573
|
-
|
|
674
|
+
The red-team spec (`spec/participant_red_team_spec.rb`) reads the stream, every show variant, `/output` and images
|
|
675
|
+
they didn't upload (a guessed name, the owner's by its hash: 404) as a participant, sends another's image refs
|
|
676
|
+
(the owner's, another participant's, a guessed hash, a forged `source`: 403), with an approval, a question, a tool's call and output, thinking, a memory, a steer, a delegate
|
|
677
|
+
report, an unknown event and host paths in the session, and finds none of them. It also names a secret and a file
|
|
678
|
+
outside the project as subjects in a review session's file and reads none of them in the show, and has `chi review`
|
|
679
|
+
refuse each as a subject.
|
|
574
680
|
|
|
575
681
|
## The owner's controls
|
|
576
682
|
|
|
@@ -660,9 +766,15 @@ invited to has none: its files stay byte for byte as they were.
|
|
|
660
766
|
it, or `(none)`, once a minute per grant and identity whatever the path; at most 256 refusals a minute are
|
|
661
767
|
remembered and audited; or `what: command`, a participant's `/word` or
|
|
662
768
|
`!line` the Bridge or the worker refused), `turn` (chars, `status: accepted` with its enqueued id, or `refused` with
|
|
663
|
-
the reason: `not_permitted`, `too_many_turns`, `cap_reached`, `
|
|
769
|
+
the reason: `not_permitted`, `too_many_turns`, `cap_reached`, `bad_images`, `no_vision` with its `detail`,
|
|
770
|
+
`review_unavailable`, `worker_timeout`; `images`: how many it carried), `image` (an upload: `status` accepted with
|
|
771
|
+
its `file` and `bytes`, or refused with the `reason`; never its name), `cap` (a new
|
|
664
772
|
cap), `verify_fresh` (`of`, `status`, the child and mode or the reason). A Verify fresh child's participants' lines
|
|
665
773
|
are its parent's. It goes with the session (retention, delete).
|
|
774
|
+
- **`participant_images.json`** (0600, `ParticipantUploads`, chi web only, beside `participants.json`, not under
|
|
775
|
+
`images/`: no route serves it): `{images: [{file, grant_id, at}], attempts: {<grant id>: n}}`, which image files
|
|
776
|
+
each grant uploaded and how many uploads each tried. Written whole (`AtomicFile`) under chi web's
|
|
777
|
+
`@participant_images_mutex`. It goes with the session.
|
|
666
778
|
- **`participants.lock`** (0600, empty): the lock a participant's turn file is counted and written under when no
|
|
667
779
|
worker runs ([Turns](#turns)).
|
|
668
780
|
|
|
@@ -698,13 +810,13 @@ means the owner: a turn or session with no author keys reads as the owner's and
|
|
|
698
810
|
- **Records.** `last_turn.author`, the analytics turn record's `author`, `turn_enqueued`'s `author`, and the
|
|
699
811
|
Bridge's queued rows. `turn_started` carries `author`, `author_name` and `participant_mode` on a participant's turn
|
|
700
812
|
(the owner's carries none), and so does the Bridge's turn snapshot and its replay. The owner's history
|
|
701
|
-
(`MessageDisplay`) and
|
|
702
|
-
reads "Alex · review" or "Alex · pair" (a prompt drawn while queued takes its mode at `turn_started`).
|
|
813
|
+
(`MessageDisplay`) and `ParticipantView` pass a participant message's `participant_mode` on: the author badge
|
|
814
|
+
reads "Alex · review" or "Alex · pair" (to a participant, a pair turn is the name alone, the owner's prompt "owner") (a prompt drawn while queued takes its mode at `turn_started`).
|
|
703
815
|
- **Side models' transcripts.** The idle recap's (`IdleRecap::TranscriptFilter`) leads a participant's message with
|
|
704
816
|
"(a review contribution from Alex, a participant; not the user's words)", and `ctx.ask_model`'s names its paragraph
|
|
705
817
|
`Participant`. The UI's readers name them: `TurnAccumulator.messages_of` gives a participant's prompt and merged
|
|
706
818
|
input `author` and `author_name`, `Merge#event_fields` names the merged text's author when one participant wrote all
|
|
707
|
-
of it, and the web's display messages carry both (the
|
|
819
|
+
of it, and the web's display messages carry both (the badges).
|
|
708
820
|
- **Through `--remote`.** `SessionAccess::WebBridgeClient#post_turn` takes `author:` and drops it: the remote chi web
|
|
709
821
|
stamps the author from the token.
|
|
710
822
|
|
|
@@ -763,16 +875,20 @@ participant TUI in v1.
|
|
|
763
875
|
- A participant reaches one session, through the `:participant` rows only; another session is a 404.
|
|
764
876
|
- No token is written anywhere: the grants store has digests, the sidecar and the audit trail have neither.
|
|
765
877
|
- Owner sessions are unchanged: no sidecar, no audit file, no key in the session JSON until a grant exists.
|
|
766
|
-
- No participant reaches `/command`, `/answer`, `/output` or an image. Their turn's author and client id are stamped
|
|
878
|
+
- No participant reaches `/command`, `/answer`, `/output` or an image they didn't upload. Their turn's author and client id are stamped
|
|
767
879
|
from the principal, never the body.
|
|
768
|
-
- Everything a participant reads (their stream, the
|
|
769
|
-
an event type it doesn't name is dropped.
|
|
770
|
-
- A participant's
|
|
880
|
+
- Everything a participant reads (their stream, the show) is built by `ParticipantView` from named fields;
|
|
881
|
+
an event type it doesn't name is dropped.
|
|
882
|
+
- A participant's message is a turn of its own, or goes into their own running turn at its next step (chi web, the
|
|
883
|
+
Bridge, the inbox), never into anyone else's turn and never a cut, a command or a
|
|
771
884
|
shell line (chi web, the Bridge, the worker), and runs only on a worker with the review boundary: in a review
|
|
772
885
|
session it denies everything but reads of the project; in a working session (a worker with `pair_turns`) the pair
|
|
773
886
|
boundary denies secrets and owner-voiced tools, the owner's guardrails decide the rest, and every ask is the
|
|
774
887
|
owner's, once only.
|
|
775
888
|
- A participant's message is never sent without its review header, and never merged into the owner's message.
|
|
776
|
-
- The owner's input is never merged into a participant's running turn (the Bridge queues it, the drain takes
|
|
777
|
-
it runs as the owner's own turn after it.
|
|
889
|
+
- The owner's input is never merged into a participant's running turn (the Bridge queues it, the drain takes only
|
|
890
|
+
the participant's own lines): it runs as the owner's own turn after it.
|
|
891
|
+
- A participant stops only a turn they started, named by its id, which the worker compares under its lock.
|
|
892
|
+
- A participant revises only their own answer with only their own turns after it, checked by chi web at the send and
|
|
893
|
+
by the worker at the stamp (`ParticipantRevise`); their Revise never folds anyone else's turn.
|
|
778
894
|
- A participant's turn never answers or drops the owner's Continue offer: it waits until the owner has.
|