samagotchi 0.125.0 → 0.126.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 +25 -1
- data/docs/guardrails.md +22 -0
- data/docs/internals/participants.md +70 -2
- data/docs/internals/web-push.md +10 -6
- data/docs/sessions.md +3 -0
- data/lib/samagotchi/bridge.rb +4 -3
- data/lib/samagotchi/bridge_client.rb +8 -1
- data/lib/samagotchi/bundles/known-names/hooks/known_names.rb +25 -4
- data/lib/samagotchi/bundles/known-names/manifest.yml +2 -2
- data/lib/samagotchi/bundles/system/manifest.yml +1 -1
- data/lib/samagotchi/child_reports.rb +3 -0
- data/lib/samagotchi/children_status.rb +6 -1
- data/lib/samagotchi/engine.rb +43 -7
- data/lib/samagotchi/guardrail_wiring.rb +10 -1
- data/lib/samagotchi/llm/chat_loop.rb +1 -0
- data/lib/samagotchi/session.rb +40 -3
- data/lib/samagotchi/session_inbox.rb +14 -8
- data/lib/samagotchi/session_manager.rb +11 -4
- data/lib/samagotchi/session_participants.rb +10 -2
- data/lib/samagotchi/verdict.rb +20 -0
- data/lib/samagotchi/verify_anchor.rb +28 -0
- data/lib/samagotchi/verify_fresh.rb +275 -0
- data/lib/samagotchi/verify_fresh_origin.rb +71 -0
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +3 -0
- data/lib/samagotchi/web/participant_access.rb +15 -4
- data/lib/samagotchi/web/participant_review.rb +7 -1
- data/lib/samagotchi/web/participant_view.rb +46 -5
- data/lib/samagotchi/web/public/annotate_bar.js +9 -1
- data/lib/samagotchi/web/public/app.js +69 -4
- data/lib/samagotchi/web/public/data.js +2 -1
- data/lib/samagotchi/web/public/index.html +1 -0
- data/lib/samagotchi/web/public/notify.js +13 -2
- data/lib/samagotchi/web/public/participant.html +2 -1
- data/lib/samagotchi/web/public/participant.js +69 -7
- data/lib/samagotchi/web/public/send_turn.js +6 -1
- data/lib/samagotchi/web/public/session_state.js +5 -0
- data/lib/samagotchi/web/public/stream_handlers.js +3 -0
- data/lib/samagotchi/web/public/verdict_card.js +65 -0
- data/lib/samagotchi/web/public/verdict_fresh.js +120 -0
- data/lib/samagotchi/web/public/verify_card.css +17 -0
- data/lib/samagotchi/web/push/attention.rb +25 -5
- data/lib/samagotchi/web/push/notifier.rb +4 -3
- data/lib/samagotchi/web/session_control.rb +2 -0
- data/lib/samagotchi/web/session_summary.rb +25 -1
- data/lib/samagotchi/web/verify_fresh_routes.rb +110 -0
- data/lib/samagotchi/worker.rb +68 -9
- data/lib/samagotchi/worker_inbound.rb +4 -4
- metadata +9 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2704d1d3f6cb8fa560e7f903ac58ac12d468fcd006b2a8782dcea02aede223a8
|
|
4
|
+
data.tar.gz: 49f91663fa29e4f77b41dae067efe8bba85917eef2682f1911af7ed7225db8fd
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0f2ed298f6d733e3f90e926b2124348c40fc119c934106d9661aa3fdb0cfd060535263516d31789ee5be3a043abe3a4c3c844754323d6bdf9e44e527587739b4
|
|
7
|
+
data.tar.gz: e3d1196a4881d05ae6fcc9f94700cc4e180a024b9d725de02baa42de3d46ae1b78146a8493aa5b42c707919476f410d23e059e9df24e3d9c1fa955362921afe3
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,29 @@ and commands may change between minor versions. How releases are made:
|
|
|
7
7
|
[docs/releasing.md](docs/releasing.md).
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.126.0] - 2026-10-27
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- chi web places each completed Verify verdict on its source claim, links to the checking turn, and sends a phone
|
|
15
|
+
notification with the verdict and a short reason (including for short turns).
|
|
16
|
+
- **Verify fresh** on an unclear verdict's card (owner or participant): a new agent checks the claim in the session's
|
|
17
|
+
folder with none of its context; its verdict lands on the same card. A participant's runs under their own access
|
|
18
|
+
and spend cap.
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
|
|
22
|
+
- A pair participant's page header reads "<name> · pairing" instead of "<name>'s review", and its tools overview's
|
|
23
|
+
disclosure triangle matches the text's size.
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
|
|
27
|
+
- The known-names hook lets through a protected name's CamelCase form (`CoffeeAssistant` or `coffeeAssistant` for a
|
|
28
|
+
repo named `coffee_assistant`) instead of rejecting it as a misspelling; `coffee-assistant` and `coffeeassistant`
|
|
29
|
+
are still caught.
|
|
30
|
+
- The known-names hook ignores separators at a word's start or end, so `grep coffee_assistant-` or
|
|
31
|
+
`.coffee_assistant` is no longer rejected as a misspelling; `mode: correct` keeps those separators.
|
|
32
|
+
|
|
10
33
|
## [0.125.0] - 2026-10-27
|
|
11
34
|
|
|
12
35
|
### Added
|
|
@@ -4740,7 +4763,8 @@ and long-lived sessions.
|
|
|
4740
4763
|
- A macOS desktop helper (`chi desktop install`): a "Send to chi" Service and a
|
|
4741
4764
|
hotkey panel that send selected text or the clipboard to your sessions.
|
|
4742
4765
|
|
|
4743
|
-
[Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.
|
|
4766
|
+
[Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.126.0...HEAD
|
|
4767
|
+
[0.126.0]: https://github.com/dm1try/samagotchi/compare/v0.125.0...v0.126.0
|
|
4744
4768
|
[0.125.0]: https://github.com/dm1try/samagotchi/compare/v0.124.0...v0.125.0
|
|
4745
4769
|
[0.124.0]: https://github.com/dm1try/samagotchi/compare/v0.123.0...v0.124.0
|
|
4746
4770
|
[0.123.0]: https://github.com/dm1try/samagotchi/compare/v0.122.0...v0.123.0
|
data/docs/guardrails.md
CHANGED
|
@@ -452,6 +452,28 @@ can send out in a URL), plugin and MCP tools, `memory_read` and `context_read`,
|
|
|
452
452
|
does that the command checks don't read (a script file's body, an interpreter's
|
|
453
453
|
`-e` code). Share a working session only with people you would let type in it.
|
|
454
454
|
|
|
455
|
+
## A Verify fresh child's boundary
|
|
456
|
+
|
|
457
|
+
A [Verify fresh](internals/participants.md#what-a-participant-sees) child (a
|
|
458
|
+
delegate child chi web starts for an unclear verdict) runs a participant's
|
|
459
|
+
check under **the boundary that participant has in the parent**. A verdict a
|
|
460
|
+
participant recorded is always checked again as them, even when the owner
|
|
461
|
+
clicks (the claim is their text; their link must still be live), so it never
|
|
462
|
+
runs with the owner's rules or System 1's auto-allows. The mode is decided once,
|
|
463
|
+
at the start, from their grant's mode there (`ParticipantMode.live_for`) and
|
|
464
|
+
saved on the child (`verify_fresh.mode`): `review` keeps every turn of it
|
|
465
|
+
inside `ReviewBoundary` (read only), `pair` inside `PairBoundary`.
|
|
466
|
+
`GuardrailWiring#turn_mode` reads the saved mode and never looks it up again,
|
|
467
|
+
and every turn of the child carries the participant as its author, whoever
|
|
468
|
+
started it, so the boundary can't fall away for a turn the owner typed into it
|
|
469
|
+
or for an author lookup that finds nobody (the child has no
|
|
470
|
+
`participants.json`; revoke and cap read the parent's). It fails closed: a
|
|
471
|
+
`verify_fresh` field that doesn't read is a participant nobody has, under
|
|
472
|
+
review, whose turns are refused. `ChildBoundary` applies on top, as for every
|
|
473
|
+
delegate child, and its asks go to the owner (once only, as a participant's
|
|
474
|
+
do; System 1 skips them). The owner's own fresh child is an ordinary delegate
|
|
475
|
+
child: the owner's turns, `ChildBoundary`.
|
|
476
|
+
|
|
455
477
|
## Rules in config.yml
|
|
456
478
|
|
|
457
479
|
```yaml
|
|
@@ -315,7 +315,8 @@ fields: a field added to an event later reaches nobody until it is named there.
|
|
|
315
315
|
- **Events** (`.event`, the stream). `EVENTS` names the types a participant gets, each rebuilt by its own method:
|
|
316
316
|
`turn_started` (turn id, its wall time `at`, the author and name, `mine` for their own, the prompt only when a person sent the turn:
|
|
317
317
|
`ClientId.human?`, else `automatic`; `continue`), the turn's end (`turn_completed`, `turn_canceled`, `turn_failed`:
|
|
318
|
-
type and turn id
|
|
318
|
+
type and turn id; `turn_completed` may also carry an allowlisted verdict card), `generation_chunk` (its visible
|
|
319
|
+
`text` only; a thinking-only chunk is dropped),
|
|
319
320
|
`generation_retrying` (`restarted`), `tool_call_started` (the tool's name, and for a file tool the path relative to
|
|
320
321
|
the session's folder when it is inside it; never when that folder is home or `/`), `tool_call_completed` (its status word), and bare markers (the type
|
|
321
322
|
alone) for the queue (`turn_enqueued`, `turn_dequeued`, `input_merged`), the owner's question, approval or Continue
|
|
@@ -414,6 +415,72 @@ fields: a field added to an event later reaches nobody until it is named there.
|
|
|
414
415
|
cookie is one name: a second participant link redeemed there replaces the first), and **the owner opening a
|
|
415
416
|
participant's invite link** in their own browser swaps their owner cookie for that grant (the page asks first). A
|
|
416
417
|
continue archives the session and revokes its grants: re-share, and links to the old session stay on it.
|
|
418
|
+
- **Verify verdict cards** (`verdict_card.js`). The built-in Verify preset carries the selected source turn id and quote
|
|
419
|
+
with the submitted turn, without changing its prompt text. A completed Verify reply whose final non-empty line has
|
|
420
|
+
a valid `Verdict: confirmed|refuted|unclear — <reason>` appends a verdict to the session. Both pages place its card
|
|
421
|
+
after the source turn, name its author, show the reason and quote, and link to the checking turn; a live completion
|
|
422
|
+
adds the card without a reload. The participant view receives only the card fields through its allowlist, with the
|
|
423
|
+
quote and reason redacted by the view's normal rules, never the full reply.
|
|
424
|
+
- **Verify fresh** (`VerifyFresh`, `verdict_fresh.js`). An `unclear` card has a **Verify fresh** button (the owner's
|
|
425
|
+
page always; a participant's while `can_comment`, i.e. while their grant could run a turn now; never in the owner's
|
|
426
|
+
participant view). It is `POST /api/sessions/:id/verdicts/:check_turn_id/fresh` (`VerifyFreshRoutes`, `:owner`
|
|
427
|
+
and `:participant`), which starts a **fresh child**: a delegate child (`spawn_session`, `delegate: true`,
|
|
428
|
+
`parent_id`) in the session's own folder (no worktree; a folder outside git too), on its model, whose first and only
|
|
429
|
+
prompt is the claim, quoted, and "Verify this claim against the files here. You have none of the original session's
|
|
430
|
+
context. End with a line `Verdict: confirmed|refuted|unclear — <one-line reason>`." No parent history. Its first
|
|
431
|
+
turn is a Verify turn on the same anchor, so its verdict is parsed. Refused: a verdict the session doesn't have (404
|
|
432
|
+
`not_found`; a fresh verdict itself is never the target, its original is), one that isn't unclear (409
|
|
433
|
+
`not_unclear`), and, checked together under `verify_fresh.lock`: a fresh check of that verdict still running (409
|
|
434
|
+
`fresh_running`), a fresh child of the same requester still running (409 `fresh_busy`: one at a time per
|
|
435
|
+
participant, and one for the owner; each start passes the cap on saved spend, so parallel children would multiply
|
|
436
|
+
it), and the parent already running `session.max_children` children of any kind (409 `children_full`, so fresh
|
|
437
|
+
checks never crowd out the owner's own delegates). For a participant also what refuses their turn: their cap (429
|
|
438
|
+
`cap_reached`, counting their fresh children) and a live worker that predates participants' turns (409
|
|
439
|
+
`review_unavailable`). A start that involves a participant is audited in the parent's trail (`verify_fresh`:
|
|
440
|
+
`participant` whose child it is, `asked_by` the clicking participant's id or `owner`, `of`, `status`, the child and
|
|
441
|
+
mode, or the reason); the answer is `{of, running: true}`, the owner's with `child_id` too.
|
|
442
|
+
- **Whose child it is.** A verdict a participant's turn recorded (`by: p:<id>`) is checked again **as that
|
|
443
|
+
participant**, whoever clicks, the owner included: the claim is their text (the quote their Verify turn sent),
|
|
444
|
+
so it never runs with the owner's power (the owner's rules, System 1's auto-allows). Their grant must still be
|
|
445
|
+
live in this session, else 409 `author_link_ended` ("Sam's link has ended: …"), and their cap and the worker's
|
|
446
|
+
features are checked as for their turn (a participant who clicks passes their own checks first). An owner's
|
|
447
|
+
verdict (`by` none) is checked again as whoever clicks: an owner's child for the owner, the clicking
|
|
448
|
+
participant's own for a participant.
|
|
449
|
+
- **The child is its requester's.** Its session file carries `verify_fresh: {author, mode, of, anchor}`
|
|
450
|
+
(`VerifyFreshOrigin`), saved before its worker starts. `author` is the participant's (`p:<id>`), none for the
|
|
451
|
+
owner. `mode` is the participant's grant's mode in the parent at that moment (`ParticipantMode.live_for`:
|
|
452
|
+
`pair` only for a grant stamped to act in a working session whose worker runs pair turns, else `review`). Every
|
|
453
|
+
turn of a participant's child is theirs, whoever starts it (the owner typing into it, a continue, a reminder):
|
|
454
|
+
`VerifyFreshOrigin#origin_for` sets the author in `Engine#begin_turn` and in the worker, so their boundary
|
|
455
|
+
(`GuardrailWiring#turn_mode` reads the saved mode, never a lookup), their System 1 skip, their once-only asks to
|
|
456
|
+
the owner and the stamps on their messages follow. The child has no `participants.json`: its participant lookups
|
|
457
|
+
(`Session#participants_source_id`) read the parent's, so a revoke, an expiry or the parent's archive refuses the
|
|
458
|
+
child's next turn and stops a pair turn mid-turn, as in the parent; their audit lines go to the parent's
|
|
459
|
+
`participants.jsonl`. A field that doesn't read is `VerifyFreshOrigin::INVALID`: a participant author nobody
|
|
460
|
+
has, under review, so its turns are refused, never run as the owner's. Its approvals wait on the child for the
|
|
461
|
+
owner (D2); the owner's page links to it.
|
|
462
|
+
- **What follows from that.** The owner typing into a participant's child is that participant's turn: charged to
|
|
463
|
+
their cap and run under their boundary (open an ordinary session to act with the owner's own). A revoke (or an
|
|
464
|
+
expiry, or the parent's archive) stops a pair-mode child mid-turn, as a pair turn; a review-mode child finishes
|
|
465
|
+
the turn it is in and is refused at its next turn's start, as a review turn. A continue of the parent archives it
|
|
466
|
+
(and revokes its grants) and moves a running fresh child to the new link with the other open delegates: the
|
|
467
|
+
turn it is in still forwards its verdict to the archived link (the parent it started under), which shows it
|
|
468
|
+
there, and its next turn is refused (its requester's grant isn't on the new link). Nothing reaches the new link's
|
|
469
|
+
card: fails closed.
|
|
470
|
+
- **Spend.** The parent's `verify_fresh.jsonl` (one line per child: `child_id`, `of`, `author`, `at`) is the
|
|
471
|
+
rollup's index: a participant's cap check in chi web (`#participant_saved_spend`), their usage line, and the
|
|
472
|
+
worker's (`Worker#participant_spend`) add their fresh children's saved spend (`VerifyFresh.children_spend`); a
|
|
473
|
+
child's worker adds the parent's saved spend and its siblings'. A child's spend counts from its turns' ends, as
|
|
474
|
+
a turn's does.
|
|
475
|
+
- **The verdict lands on the card**, never in the parent's conversation: the child's worker writes each verdict
|
|
476
|
+
its turn recorded into the parent's `fresh_verdicts/<check turn>.json` (`fresh: true`, `of`, `child_id`) after its
|
|
477
|
+
save, and a fresh child never rings its parent (no delegate report, `ChildRing.ring`). The parent's worker takes
|
|
478
|
+
them into its `verdicts` between turns (`Worker#absorb_fresh_verdicts`), saves, deletes the files and announces
|
|
479
|
+
`:verdict_recorded`; chi web shows the ones still waiting (`VerifyFresh.verdicts_of`: the summary, the review
|
|
480
|
+
data). The card shows "Verifying fresh…" while the child runs (the summary's `verify_fresh` `{of, child_id}`,
|
|
481
|
+
the review data's `fresh_running`, ids only) and the fresh verdict as a `fresh: ✅ confirmed — …` line on the same
|
|
482
|
+
card. The participant view passes `fresh` and `of`, never the child's id (a participant can't open it). No push
|
|
483
|
+
for a fresh verdict yet (`Attention` pushes a verdict at a turn's end).
|
|
417
484
|
- **The comment box** is the owner's composer (`composer_mount.js`, styled by the shared `composer.css`), mounted by
|
|
418
485
|
`participant.js` with a subset on. It has: the height that grows with the text, the grip (drag to resize,
|
|
419
486
|
double-click resets, click or drag down folds it; folded, the grip is the handle on the dock's edge and typing on the
|
|
@@ -532,7 +599,8 @@ invited to has none: its files stay byte for byte as they were.
|
|
|
532
599
|
remembered and audited; or `what: command`, a participant's `/word` or
|
|
533
600
|
`!line` the Bridge or the worker refused), `turn` (chars, `status: accepted` with its enqueued id, or `refused` with
|
|
534
601
|
the reason: `not_permitted`, `too_many_turns`, `cap_reached`, `review_unavailable`, `worker_timeout`), `cap` (a new
|
|
535
|
-
cap)
|
|
602
|
+
cap), `verify_fresh` (`of`, `status`, the child and mode or the reason). A Verify fresh child's participants' lines
|
|
603
|
+
are its parent's. It goes with the session (retention, delete).
|
|
536
604
|
- **`participants.lock`** (0600, empty): the lock a participant's turn file is counted and written under when no
|
|
537
605
|
worker runs ([Turns](#turns)).
|
|
538
606
|
|
data/docs/internals/web-push.md
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
# Web Push (internals)
|
|
2
2
|
|
|
3
3
|
chi web pushes to the owner's phone when a session needs them, for what the tab notifies for: an approval, a
|
|
4
|
-
question, a step-limit question, a card with actions, a failed turn, and (per device,
|
|
4
|
+
question, a step-limit question, a card with actions, a failed turn, a recorded Verify verdict, and (per device,
|
|
5
|
+
off by default) a done one. The
|
|
5
6
|
phone runs chi web as a Home Screen app (iOS 16.4+), its bell subscribes the device, and chi web, the only sender,
|
|
6
7
|
pushes through the device's push service (Apple's `web.push.apple.com`). The user side is in
|
|
7
8
|
[configuration.md](../configuration.md#phone-notifications-web-push). This page is how it is built (P1, P2a and P2b of the
|
|
@@ -146,17 +147,20 @@ the top and in `pending_question` / `pending_card`, strings in `last_turn`), `no
|
|
|
146
147
|
3.5 s (notify.js `RELAY_HOLD_MS`): its parent may relay it.
|
|
147
148
|
2. a new `pending_card` id: `card`, key `"<sid>:card:<cid>"`.
|
|
148
149
|
3. a top-level turn whose `last_turn["ended_at"]` changed (a delegated child's is its parent's to report):
|
|
149
|
-
|
|
150
|
-
`
|
|
151
|
-
|
|
150
|
+
a newly recorded verdict is `verdict`, keyed by `"<sid>:<check_turn_id>"`; otherwise `failed`, or `done` when
|
|
151
|
+
`completed` and `seconds` ≥ `DONE_MIN_SECONDS` 10 or its origin is in `UNWATCHED_ORIGINS`
|
|
152
|
+
(`delegate_report`, `context`, `task`), keyed by `"<sid>:<ended_at>"`. A verdict wins over the done/no-event
|
|
153
|
+
rule, including for a short turn. A reminder turn follows the same rules (no reminder event of its own).
|
|
152
154
|
- `WAITING` (approval, question, continue, card) are the reasons that stay open until answered: badged, in `open`,
|
|
153
|
-
cleaned up, and pushed only while still open. A failed or
|
|
155
|
+
cleaned up, and pushed only while still open. A failed, done or verdict turn has nothing to answer.
|
|
154
156
|
- `Attention.closed_keys(prev, next)`: the question answered, replaced, newly relayed; the card resolved or
|
|
155
157
|
replaced; both when the session is gone.
|
|
156
158
|
- `Attention.text(summary, reason)`: notify.js `attentionText`'s words: the session's name as its card has it
|
|
157
159
|
(title, auto title, the recap's first sentence, first preview, last prompt — the same order `format.js cardName`
|
|
158
160
|
uses) clipped to 61, and "needs approval" / "needs an answer" / "hit its step limit" / "needs you" / "turn
|
|
159
|
-
failed" / "done in N s" (`last_turn["seconds"]` rounded)
|
|
161
|
+
failed" / "done in N s" (`last_turn["seconds"]` rounded) / "<who>: <icon> <verdict> — <reason>" (reason clipped
|
|
162
|
+
to 120 characters). A verdict names its participant from the session's participant sidecar, or says "You" for the
|
|
163
|
+
owner; only verdicts use that push copy, never the full reply. Never the question or the command.
|
|
160
164
|
|
|
161
165
|
`Notifier#call(event)` is a `SessionHub` sink. **The hub calls it under its Monitor**, from the tick thread and also
|
|
162
166
|
from chi web's request threads (`SessionHub#touch`, from the stop/archive/delete routes), so it only enqueues: a push
|
data/docs/sessions.md
CHANGED
|
@@ -4,6 +4,8 @@ Sessions are plain files — no DB. Each session is `~/.local/state/samagotchi/s
|
|
|
4
4
|
|
|
5
5
|
**Message times:** an entry of `messages` may carry `at`, when it happened: a local-offset ISO time with milliseconds (`2026-10-24T14:32:05.120+02:00`). User prompts, lines merged into a turn and steers, reminders, context and task notes, and the model's replies have one (a note's is its `created_at`); tool results, `!cmd` messages and chi's own turn notes don't, nor does anything saved before chi stamped messages (no backfill). It is for the UIs only (the gap dividers, a bubble's time): it never goes to the model, so it changes nothing in the requests or their prompt cache. The one time the model reads is the reminders': due reminders go into a turn as a system message headed `[SYSTEM: REMINDERS DUE 2026-10-24 14:32 local]` (the turn's local time; `ReminderQueue#inject!`), written into the saved content then, so a later request sends the same text.
|
|
6
6
|
|
|
7
|
+
**Verify verdicts:** a web Verify turn stamps its opening user message with `verify: {turn_id, quote}` (the selected source turn and quote; metadata only, never added to the prompt). When that turn completes and the final assistant reply's last non-empty line is a valid `Verdict: confirmed|refuted|unclear — <reason>`, the session file appends `{anchor: {turn_id, quote}, verdict, reason, by, check_turn_id, at}` to its optional `verdicts` array. `by` is the turn's author (nil for the owner), `check_turn_id` names the Verify turn, and `at` is its timestamp. The array is append-only; older files without it have no saved verdicts. An `unclear` verdict can be checked again by **Verify fresh** (a delegate child that gets only the claim; see *Delegating*): its verdict is appended to the parent's `verdicts` with `fresh: true`, `of` (the verdict it checked again) and `child_id`, from the parent's `fresh_verdicts/` dir, which its worker takes between turns. Verdicts are display state: none enters a prompt.
|
|
8
|
+
|
|
7
9
|
**Input files:** a message for a session's worker waits in `input/<timestamp>.json` (`SessionInbox.write_input`: `prompt`, `client_id`, `enqueued_id`, and `images`, `no_interrupt` and `delivery` when they apply; `delivery` is written only for `"queue"`). While a turn runs, the worker merges them at each step boundary in name order, except a queued one or one with images: those wait for the turn's end and run one turn each, in name order, and a file after them still merges (`Worker#pending_input_drain`). A queue is sent only to a worker whose `bridge.json` names the `delivery` feature; an older one would steer it, so `SessionManager.deliver_turn` refuses it (409 `delivery_unavailable`), and any other 4xx the Bridge answers is a refusal too, never a fallback file (except a 413, a body over its size limit: that one still goes in as a file, its `delivery` kept). A session command recognized in a file runs without a turn; its outcome is kept at `input/settled/<enqueued_id>` (`completed`, `failed` for a command error, `canceled` with `cancel_reason` `busy` or `dropped` for one refused mid-turn or dropped as the worker left; `origin: command`) so `chi send --wait` ends instead of waiting for a turn that will not run, its detail saying `the command failed` or `the command was refused (busy)` rather than a turn's words. **Reply files:** a turn that ends with visible text leaves its reply in `output/<timestamp>.txt`, or `output/<timestamp>-<enqueued_id>.txt` when the turn ran an input file with an id (`SessionInbox.write_output`), and the session's `last_turn` keeps that `enqueued_id`; so a wait for one queued message (`chi send --queue --wait`, `ReplyWait`'s `turn:`) finds its own turn's reply or end, never another turn's. The names still sort by time, and every reader takes `*.txt`.
|
|
8
10
|
|
|
9
11
|
**Removing a queued message:** a message queued for after the running turn (`chi send --queue`, the web's Alt+Enter, `/queue TEXT` in a terminal), or one with images (it waits for the turn's end too), can be taken back while it still waits: the worker's Bridge (`POST /session/:id/turn/dequeue {enqueued_id}`, the `unqueue` feature) deletes its input file and every UI drops it (`:turn_dequeued`). It is a race with the turn's end, decided by the same rename the worker uses to take the file: once a turn took it the answer is `409 already_started` (it runs), and an id the worker no longer knows is `404 unknown_enqueued_id`. A plain message sent without `--queue` is not removable (`409 not_queued`): a running turn's next step may take it at any moment. A wait for its turn (`chi send --queue --wait`, a delegate follow-up's wait) ends at once with `removed` (the tombstone `input/removed/<enqueued_id>` says so), and a `chi -p` run whose queued prompt went ends with exit 1. The `200` carries the removed `prompt`: the web and the attached terminal put it back into the composer to edit (not its images). The images a removed message named stay in the session's `images/`. Not removable: a queued session command (`/queue /model x`: it waits in the worker, not in `input/`), a message in a stopped worker's inbox (the next worker runs it), and the plain REPL's (`--no-shared`) own queue.
|
|
@@ -384,6 +386,7 @@ The `delegate` tool hands a task to a **child session**: an ordinary chi session
|
|
|
384
386
|
- Every child starts with the shipped `delegated` system memory preloaded (`preloaded_memory_names: ["system/delegated"]`; `--mute delegated` would hide it): put everything in the final reply, don't delegate further, don't write memories or change config, stay in the folder. Its system prompt also says `Delegated by session <parent id>`. The parent's own `--mute` list does not carry over.
|
|
385
387
|
- **A child asks before it changes anything outside its folder** (the work tree top of the folder it started in): a write or edit there, mutating git in another checkout, a command that isn't read-only run from outside it or naming the main checkout or a sibling worktree. It's a core guardrail, on in every mode and without the guardrails bundle, and answered once or for the child's session ([Guardrails](guardrails.md#a-delegate-childs-boundary)); the approval relays to the parent's user as any other.
|
|
386
388
|
- Limits: a child cannot `delegate` (depth 1; the tool refuses), and one session may have at most `session.max_children` (default `4`, env `SAMAGOTCHI_SESSION_MAX_CHILDREN`) children running at a time, counted from the session files (a finished or idle-exited child does not count; one whose worker is still starting does, for 15 s). A refused call creates no session. Nothing stops children with the parent: `chi sessions stop ID` does.
|
|
389
|
+
- **Verify fresh children** (a web card's Verify fresh, [Participants: Verify fresh](internals/participants.md)) are delegate children chi web starts, not the model: in the parent's own folder, with only the claim as their task and none of the parent's history. Their session file carries `verify_fresh: {author, mode, of, anchor}`; a participant's child runs every turn as that participant's (their boundary, saved at the start; their cap, counted with the parent's spend; the parent's `participants.json`). One never rings its parent: its verdict goes to the parent's verdict card (`fresh_verdicts/`), not its conversation, and the parent's `verify_fresh.jsonl` lists them for the spend rollup and the running check (one per verdict at a time). They count among the parent's `session.max_children` while they run.
|
|
387
390
|
- The link is the session's `parent_id`, written before the worker starts, so a respawn keeps it (and `delegate: true`, which a fork's `parent_id` doesn't come with; a child saved before that field is known by its `system/delegated` memory); `GET /api/sessions`, `/api/sessions/:id` and the web hub's `session` frames carry it and `delegate` (a fork is `false`; the web parent's children chip counts delegates only); `SessionManager.children_of` lists a parent's children.
|
|
388
391
|
## Restart fallback
|
|
389
392
|
|
data/lib/samagotchi/bridge.rb
CHANGED
|
@@ -1071,6 +1071,7 @@ module Samagotchi
|
|
|
1071
1071
|
# request's principal; the Bridge is loopback and trusts its caller).
|
|
1072
1072
|
author = fetched(parsed, "author")
|
|
1073
1073
|
author = nil unless author.is_a?(String) && !author.empty?
|
|
1074
|
+
verify = VerifyAnchor.from(fetched(parsed, "verify"))
|
|
1074
1075
|
if sid.to_s.strip.empty? || prompt.to_s.strip.empty?
|
|
1075
1076
|
return [{ "Allow" => "POST" }, 400,
|
|
1076
1077
|
{ error: "missing_fields", detail: "session_id and prompt are required" }]
|
|
@@ -1132,7 +1133,7 @@ module Samagotchi
|
|
|
1132
1133
|
delivery = Delivery::QUEUE if @accumulator.participant_turn?
|
|
1133
1134
|
queued_behind_turn = Delivery.queue?(delivery) && !@accumulator.current_turn.nil?
|
|
1134
1135
|
enqueue_turn(prompt: prompt, client_id: client_id, enqueued_id: enqueued_id,
|
|
1135
|
-
no_interrupt: no_interrupt, images: images, delivery: delivery, author: author).tap do |ok|
|
|
1136
|
+
no_interrupt: no_interrupt, images: images, delivery: delivery, author: author, verify: verify).tap do |ok|
|
|
1136
1137
|
next unless ok
|
|
1137
1138
|
|
|
1138
1139
|
enqueued_event = { type: :turn_enqueued, enqueued_id: enqueued_id, client_id: client_id, prompt: prompt.to_s }
|
|
@@ -1296,9 +1297,9 @@ module Samagotchi
|
|
|
1296
1297
|
|
|
1297
1298
|
# Write a turn into this session's input dir, reusing the file IPC the
|
|
1298
1299
|
# worker polls. Never calls run_turn across the boundary.
|
|
1299
|
-
def enqueue_turn(prompt:, client_id: nil, enqueued_id: nil, no_interrupt: false, images: [], delivery: nil, author: nil)
|
|
1300
|
+
def enqueue_turn(prompt:, client_id: nil, enqueued_id: nil, no_interrupt: false, images: [], delivery: nil, author: nil, verify: nil)
|
|
1300
1301
|
SessionInbox.write_input(session_dir, prompt: prompt, client_id: client_id, enqueued_id: enqueued_id,
|
|
1301
|
-
no_interrupt: no_interrupt, images: images, delivery: delivery, author: author)
|
|
1302
|
+
no_interrupt: no_interrupt, images: images, delivery: delivery, author: author, verify: verify)
|
|
1302
1303
|
end
|
|
1303
1304
|
|
|
1304
1305
|
# ── HTTP plumbing ────────────────────────────────────────────────────────
|
|
@@ -14,6 +14,10 @@ require_relative "bridge_client/event_stream"
|
|
|
14
14
|
require_relative "bridge_client/stream_request"
|
|
15
15
|
|
|
16
16
|
module Samagotchi
|
|
17
|
+
# Loaded on first use (a Verify turn): keeps the bundle test kit's load
|
|
18
|
+
# budget (spec/samagotchi/testing/sandbox_spec.rb).
|
|
19
|
+
autoload :VerifyAnchor, File.expand_path("verify_anchor", __dir__)
|
|
20
|
+
|
|
17
21
|
# Client side of a session worker's Bridge: the 127.0.0.1 HTTP + SSE server
|
|
18
22
|
# each SessionManager worker runs for its Engine (see Bridge). Discovery goes
|
|
19
23
|
# through WorkerLink (the worker's `bridge.json` sidecar, WorkerSidecar, in
|
|
@@ -182,13 +186,16 @@ module Samagotchi
|
|
|
182
186
|
# owner; sent only when given (SessionManager.deliver_turn)
|
|
183
187
|
# @param pending_limit [Integer, nil] with a participant's +author+: how
|
|
184
188
|
# many of their turns may be running or waiting (429 too_many_turns past it)
|
|
185
|
-
def post_turn(prompt:, client_id: nil, no_interrupt: false, images: nil, delivery: nil, author: nil, pending_limit: nil
|
|
189
|
+
def post_turn(prompt:, client_id: nil, no_interrupt: false, images: nil, delivery: nil, author: nil, pending_limit: nil,
|
|
190
|
+
verify: nil)
|
|
186
191
|
body = { session_id: @session_id, prompt: prompt, client_id: client_id }
|
|
187
192
|
body[:no_interrupt] = true if no_interrupt
|
|
188
193
|
body[:author] = author if author
|
|
189
194
|
body[:pending_limit] = pending_limit if author && pending_limit
|
|
190
195
|
body[:images] = images if images && !images.empty?
|
|
191
196
|
body[:delivery] = delivery unless delivery.nil? || Delivery.next_step?(delivery)
|
|
197
|
+
anchor = VerifyAnchor.from(verify)
|
|
198
|
+
body[:verify] = anchor.to_file if anchor
|
|
192
199
|
body[:deadline] = deadline
|
|
193
200
|
post("turn", body, read_body: true)
|
|
194
201
|
end
|
|
@@ -26,6 +26,8 @@ class KnownNames
|
|
|
26
26
|
# name after it is what is checked.
|
|
27
27
|
TOKEN_SPLIT = %r{[/\s"'=:,;|&()<>@~#]+}
|
|
28
28
|
PART_SPLIT = /[-_.]+/
|
|
29
|
+
# Separators at a token's start or end, dropped before it is compared.
|
|
30
|
+
EDGE_SEPARATORS = /\A[-_.]+|[-_.]+\z/
|
|
29
31
|
# A candidate with a shell glob character is not checked: `workato*`
|
|
30
32
|
# is a pattern over the name, not a misspelling of it.
|
|
31
33
|
GLOB = /[*?\[\]{}]/
|
|
@@ -94,15 +96,22 @@ class KnownNames
|
|
|
94
96
|
return nil if names.empty?
|
|
95
97
|
|
|
96
98
|
scan_targets(event).each do |where, text|
|
|
97
|
-
candidates(text).each do |
|
|
99
|
+
candidates(text).each do |token|
|
|
100
|
+
next if names.key?(token.downcase)
|
|
101
|
+
|
|
102
|
+
# "coffee_assistant-" (a prefix search) is the name, not a misspelling.
|
|
103
|
+
candidate = token.gsub(EDGE_SEPARATORS, "")
|
|
98
104
|
down = candidate.downcase
|
|
99
|
-
next if names.key?(down)
|
|
105
|
+
next if candidate.empty? || names.key?(down)
|
|
100
106
|
|
|
101
107
|
names.each do |name_down, name|
|
|
108
|
+
next if camel_case?(candidate, name)
|
|
109
|
+
|
|
102
110
|
max = @max_distance || (name.length < 10 ? 1 : 2)
|
|
103
|
-
|
|
111
|
+
bare = name_down.gsub(EDGE_SEPARATORS, "")
|
|
112
|
+
next if (candidate.length - bare.length).abs > max
|
|
104
113
|
|
|
105
|
-
d = self.class.distance(down,
|
|
114
|
+
d = self.class.distance(down, bare)
|
|
106
115
|
return { miss: candidate, name: name, distance: d, where: where } if d.between?(1, max)
|
|
107
116
|
end
|
|
108
117
|
end
|
|
@@ -110,6 +119,18 @@ class KnownNames
|
|
|
110
119
|
nil
|
|
111
120
|
end
|
|
112
121
|
|
|
122
|
+
# "CoffeeAssistant" or "coffeeAssistant" for "coffee_assistant": the
|
|
123
|
+
# name's own class or variable form, not a misspelling of it. Only a
|
|
124
|
+
# form with a capital: "coffeeassistant" and "coffee-assistant" are
|
|
125
|
+
# still near misses.
|
|
126
|
+
def camel_case?(candidate, name)
|
|
127
|
+
parts = name.split(PART_SPLIT)
|
|
128
|
+
return false if parts.size < 2
|
|
129
|
+
|
|
130
|
+
upper = parts.map(&:capitalize).join
|
|
131
|
+
[upper, upper[0].downcase + upper[1..]].include?(candidate)
|
|
132
|
+
end
|
|
133
|
+
|
|
113
134
|
# [where, text] pairs to scan: the command, the cwd a command names,
|
|
114
135
|
# each path, and a write/edit call's own path (in case normalisation
|
|
115
136
|
# dropped it). Not file contents.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: known-names
|
|
3
|
-
version: 0.1.
|
|
3
|
+
version: 0.1.7
|
|
4
4
|
scope: system
|
|
5
5
|
description: Catches a near-miss of a protected name (home folder, login, git name, repo name, names from config) in a tool call and rejects it with the right spelling; mode ask / correct optional
|
|
6
6
|
trust_level: reviewed
|
|
@@ -8,7 +8,7 @@ files:
|
|
|
8
8
|
known_names.md: sha256:d5348b95b6c655017b541910b36e384220da280818160201f6ec9a50e1e4287c
|
|
9
9
|
hooks:
|
|
10
10
|
known_names.rb:
|
|
11
|
-
sha256: sha256:
|
|
11
|
+
sha256: sha256:a851637e9292f825dc5b8877cd060184de976aa4297b114dab412a89fa3cf091
|
|
12
12
|
event: before_tool_call
|
|
13
13
|
on_error: log
|
|
14
14
|
priority: 50
|
|
@@ -64,6 +64,9 @@ module Samagotchi
|
|
|
64
64
|
def ring(child, why:, state_dir:, wake: nil)
|
|
65
65
|
return nil if mode == "off"
|
|
66
66
|
return nil unless child&.delegate?
|
|
67
|
+
# A Verify fresh child's verdict reaches its parent's card
|
|
68
|
+
# (VerifyFresh.forward), never its conversation.
|
|
69
|
+
return nil if child.verify_fresh
|
|
67
70
|
|
|
68
71
|
parent_id = Session.parent_override(child.id, state_dir: state_dir) || child.parent_id
|
|
69
72
|
return nil unless parent_id
|
|
@@ -86,11 +86,16 @@ module Samagotchi
|
|
|
86
86
|
state: state_of(session, row, reply), waiting: row[:waiting], live: !!row[:live],
|
|
87
87
|
delegate: session.delegate?, cwd: session.working_directory, branch: GitHead.branch(session.working_directory),
|
|
88
88
|
last_reply: reply && first_line(session.id, reply, state_dir: state_dir),
|
|
89
|
-
last_reply_at: reply && reply_time(reply), reported: !reply.nil? && cursor
|
|
89
|
+
last_reply_at: reply && reply_time(reply), reported: !reply.nil? && reported?(session, cursor, reply),
|
|
90
90
|
updated_at: session.updated_at, archived: !!row[:archived])
|
|
91
91
|
end
|
|
92
92
|
private_class_method :child
|
|
93
93
|
|
|
94
|
+
# A reply the parent was given; a Verify fresh child's always counts
|
|
95
|
+
# (its verdict goes to the parent's card, never a report).
|
|
96
|
+
def reported?(session, cursor, reply) = !session.verify_fresh.nil? || cursor.reply_file == reply
|
|
97
|
+
private_class_method :reported?
|
|
98
|
+
|
|
94
99
|
def state_of(session, row, reply)
|
|
95
100
|
return "waiting" if row[:waiting]
|
|
96
101
|
return "running" if Tools::Delegate.running?(row)
|
data/lib/samagotchi/engine.rb
CHANGED
|
@@ -15,6 +15,8 @@ require_relative "steer"
|
|
|
15
15
|
require_relative "turn_note"
|
|
16
16
|
require_relative "turn_endings"
|
|
17
17
|
require_relative "turn_origin"
|
|
18
|
+
require_relative "verify_anchor"
|
|
19
|
+
require_relative "verdict"
|
|
18
20
|
require_relative "model_profile"
|
|
19
21
|
require_relative "effective_model"
|
|
20
22
|
require_relative "session_setting"
|
|
@@ -901,10 +903,12 @@ module Samagotchi
|
|
|
901
903
|
|
|
902
904
|
# Event types #announce accepts: facts about the session's input queue,
|
|
903
905
|
# a failed turn's prompt handed back and the continue offer, which live
|
|
904
|
-
# UIs need in the event log, emitted outside any turn's stream
|
|
906
|
+
# UIs need in the event log, emitted outside any turn's stream; a
|
|
907
|
+
# Verify fresh child's verdict landing on its card (:verdict_recorded).
|
|
905
908
|
ANNOUNCEABLE_EVENTS = %i[turn_enqueued turn_dequeued input_merged prompt_restored continue_offered continue_resolved
|
|
906
909
|
command_queued command_ran context_added card hook_notice guardrail_warning
|
|
907
|
-
plugin_init_started plugin_init_finished working_set_updated participants_changed
|
|
910
|
+
plugin_init_started plugin_init_finished working_set_updated participants_changed
|
|
911
|
+
verdict_recorded].freeze
|
|
908
912
|
|
|
909
913
|
# Put a transport-level event into the ordered event log. Unlike turn
|
|
910
914
|
# events it reaches only persistent observers (no turn sink, no memory
|
|
@@ -1589,7 +1593,10 @@ module Samagotchi
|
|
|
1589
1593
|
seconds = turn.elapsed
|
|
1590
1594
|
synchronize_events do
|
|
1591
1595
|
kept, event = yield(seconds)
|
|
1596
|
+
stamp_verify_anchor(kept, turn)
|
|
1592
1597
|
replace_session_messages(turn.session, kept) if kept
|
|
1598
|
+
verdict = record_verdict(turn, outcome)
|
|
1599
|
+
event[:verdict] = verdict if verdict
|
|
1593
1600
|
emit_working_set(turn.on_event, turn.session, turn.session.messages)
|
|
1594
1601
|
turn.session.status = Session::STATUS_IDLE
|
|
1595
1602
|
record_last_turn(turn.session, outcome, seconds, turn.origin, event, limit: exhausted ? turn.limit : nil)
|
|
@@ -1602,6 +1609,31 @@ module Samagotchi
|
|
|
1602
1609
|
end
|
|
1603
1610
|
private :end_turn
|
|
1604
1611
|
|
|
1612
|
+
def stamp_verify_anchor(messages, turn)
|
|
1613
|
+
return unless turn.verify && messages.is_a?(Array)
|
|
1614
|
+
|
|
1615
|
+
opening = messages.reverse.find do |message|
|
|
1616
|
+
(message[:role] || message["role"]).to_s == "user" &&
|
|
1617
|
+
(message[:turn_id] || message["turn_id"]).to_s == turn.id
|
|
1618
|
+
end
|
|
1619
|
+
opening[:verify] = turn.verify.to_h if opening
|
|
1620
|
+
end
|
|
1621
|
+
private :stamp_verify_anchor
|
|
1622
|
+
|
|
1623
|
+
def record_verdict(turn, outcome)
|
|
1624
|
+
return unless outcome == "completed" && turn.verify
|
|
1625
|
+
|
|
1626
|
+
answer = AnswerTail.find(turn.session.messages, turn_id: turn.id)
|
|
1627
|
+
parsed = Verdict.parse(answer && (answer[:content] || answer["content"]))
|
|
1628
|
+
return unless parsed
|
|
1629
|
+
|
|
1630
|
+
record = { anchor: turn.verify.to_h, verdict: parsed.verdict, reason: parsed.reason,
|
|
1631
|
+
by: turn.origin.author, check_turn_id: turn.id, at: Time.now.iso8601(3) }
|
|
1632
|
+
turn.session.verdicts = turn.session.verdicts + [record]
|
|
1633
|
+
record
|
|
1634
|
+
end
|
|
1635
|
+
private :record_verdict
|
|
1636
|
+
|
|
1605
1637
|
def save_quietly(session)
|
|
1606
1638
|
session.save(state_dir: session_state_dir)
|
|
1607
1639
|
rescue StandardError => e
|
|
@@ -2011,7 +2043,7 @@ module Samagotchi
|
|
|
2011
2043
|
# +wall+: the wall time it started (MessageTime.now, once): the
|
|
2012
2044
|
# :turn_started event's at and its user message's (and reminders') stamp.
|
|
2013
2045
|
Turn = Struct.new(:session, :prompt, :continue, :on_event, :controller, :origin, :started_at, :messages, :ended,
|
|
2014
|
-
:settings, :id, :limit, :resumed, :resume_from, :wall) do
|
|
2046
|
+
:settings, :id, :limit, :resumed, :resume_from, :wall, :verify) do
|
|
2015
2047
|
def resumed? = resumed ? true : false
|
|
2016
2048
|
|
|
2017
2049
|
# The turn's boundary events carry the origin only when it names a
|
|
@@ -2083,9 +2115,9 @@ module Samagotchi
|
|
|
2083
2115
|
# re-raises; a provider error adds error_kind:, retryable:, host: and a
|
|
2084
2116
|
# one-line summary:.
|
|
2085
2117
|
def run_turn(session, prompt, on_event: nil, max_iterations: nil, cancel_controller: nil, max_tool_output_chars: nil, pending_input: nil, continue: false,
|
|
2086
|
-
origin: TurnOrigin::NONE, images: [], id: nil, resumed: false, resume_from: nil)
|
|
2118
|
+
origin: TurnOrigin::NONE, images: [], id: nil, resumed: false, resume_from: nil, verify: nil)
|
|
2087
2119
|
turn = begin_turn(session, prompt, on_event: on_event, cancel_controller: cancel_controller, origin: origin,
|
|
2088
|
-
continue: continue, id: id)
|
|
2120
|
+
continue: continue, id: id, verify: verify)
|
|
2089
2121
|
if continue
|
|
2090
2122
|
turn.resumed = resumed
|
|
2091
2123
|
turn.resume_from = resume_from
|
|
@@ -2133,8 +2165,11 @@ module Samagotchi
|
|
|
2133
2165
|
# The turn's state before anything can fail it: the session running,
|
|
2134
2166
|
# the turn flag, its cancel controller and sink. The cross-thread state
|
|
2135
2167
|
# is set here and cleared in #release_turn only.
|
|
2136
|
-
def begin_turn(session, prompt, on_event:, cancel_controller:, origin:, continue:, id: nil)
|
|
2168
|
+
def begin_turn(session, prompt, on_event:, cancel_controller:, origin:, continue:, id: nil, verify: nil)
|
|
2137
2169
|
origin = TurnOrigin.of(origin)
|
|
2170
|
+
# A participant's Verify fresh child: every turn is theirs (their
|
|
2171
|
+
# author, boundary and cap), whoever started it.
|
|
2172
|
+
origin = session.verify_fresh.origin_for(origin) if session.respond_to?(:verify_fresh) && session.verify_fresh
|
|
2138
2173
|
# Track the active session for recap and status snapshot.
|
|
2139
2174
|
@session = session
|
|
2140
2175
|
# status is turn state: running now, idle again before the turn's end
|
|
@@ -2157,7 +2192,7 @@ module Samagotchi
|
|
|
2157
2192
|
|
|
2158
2193
|
Turn.new(session: session, prompt: continue ? nil : prompt, continue: continue, on_event: on_event,
|
|
2159
2194
|
controller: effective_controller, origin: origin, started_at: Process.clock_gettime(Process::CLOCK_MONOTONIC),
|
|
2160
|
-
id: id || SecureRandom.uuid, wall: MessageTime.now)
|
|
2195
|
+
id: id || SecureRandom.uuid, wall: MessageTime.now, verify: VerifyAnchor.from(verify))
|
|
2161
2196
|
end
|
|
2162
2197
|
private :begin_turn
|
|
2163
2198
|
|
|
@@ -2360,6 +2395,7 @@ module Samagotchi
|
|
|
2360
2395
|
unless turn.continue
|
|
2361
2396
|
user_message = { role: "user", content: turn.prompt, turn_id: turn.id, **author_fields(turn.origin.author),
|
|
2362
2397
|
**participant_mode_fields(turn.origin.author), MessageTime::KEY => turn.at }
|
|
2398
|
+
user_message[:verify] = turn.verify.to_h if turn.verify
|
|
2363
2399
|
user_message[:images] = image_refs unless image_refs.empty?
|
|
2364
2400
|
turn.messages << user_message
|
|
2365
2401
|
session.last_prompt = turn.prompt
|
|
@@ -109,9 +109,18 @@ module Samagotchi
|
|
|
109
109
|
@git = Guardrails::GitInfo.new
|
|
110
110
|
author = turn_author
|
|
111
111
|
@turn_entry = author && @participant.call(author)
|
|
112
|
-
@turn_mode = author &&
|
|
112
|
+
@turn_mode = author && participant_turn_mode(@session_lookup.call, @turn_entry)
|
|
113
113
|
end
|
|
114
114
|
|
|
115
|
+
# A participant's turn's mode: in a Verify fresh child the one saved
|
|
116
|
+
# on it at spawn (VerifyFreshOrigin#turn_mode: :review unless saved
|
|
117
|
+
# :pair), never looked up again; elsewhere ParticipantMode.for_turn.
|
|
118
|
+
def participant_turn_mode(session, entry)
|
|
119
|
+
fresh = session.respond_to?(:verify_fresh) && session.verify_fresh
|
|
120
|
+
fresh ? fresh.turn_mode : ParticipantMode.for_turn(session, entry)
|
|
121
|
+
end
|
|
122
|
+
private :participant_turn_mode
|
|
123
|
+
|
|
115
124
|
# The running participant turn's mode (ParticipantMode.for_turn: :pair
|
|
116
125
|
# only in a working session for a participant invited to act), decided
|
|
117
126
|
# at its start; nil for the owner's turns. The boundary in #checks, the
|
|
@@ -447,6 +447,7 @@ module Samagotchi
|
|
|
447
447
|
message[:images] = entry[:images] if entry[:images].is_a?(Array) && !entry[:images].empty?
|
|
448
448
|
message[:turn_id] = entry[:turn_id] if entry[:turn_id]
|
|
449
449
|
message[:turn_start] = true if entry[:turn_start] == true
|
|
450
|
+
message[:verify] = entry[:verify] if entry[:verify].is_a?(Hash)
|
|
450
451
|
message[:thinking] = entry[:thinking] if entry[:thinking].is_a?(String) && !entry[:thinking].empty?
|
|
451
452
|
message[:anthropic_content] = entry[:anthropic_content] if entry[:anthropic_content].is_a?(Array)
|
|
452
453
|
ToolResponse::SAVED_KEYS.each { |key| message[key] = entry[key] if entry[key] }
|