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
data/docs/sessions.md
CHANGED
|
@@ -6,9 +6,9 @@ Sessions are plain files — no DB. Each session is `~/.local/state/samagotchi/s
|
|
|
6
6
|
|
|
7
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
8
|
|
|
9
|
-
**Revise stacks:** a web Revise turn (POST /turn `revises: {turn_id}`, the answer it rewrites) stamps its opening message (its prompt) with `revises: {turn_id}` at the turn's end, only when the turn completed with an answer (`AnswerTail`): a failed, canceled or empty turn revised nothing (`TurnStages::VerifyAnchors#stamp_opening`). A draft no prompt carries (a stale page, a rolled-back turn) is no target: no stamp. A draft a stamped turn already revised is re-targeted to its stack's top, the newest version, so a stack is a chain `v1 ← v2 ← v3`. Like `verify`, it is metadata only, never added to the prompt (no change to a request's bytes); chi web's history passes it on the opening message (and on a wake turn's note), and `:turn_completed` carries the stamped `revises` (the participant view's too). The history also marks a Verify turn's opening `verify: true`, and so does its live end event (`:turn_completed`'s `verify: true`, with its `turn_id`): the page offers no Revise on its answer. The web page draws the stacks from these stamps (docs/cli.md, Revise). Like a Verify turn's, a Revise message waits for the running turn's end (`SessionInbox.turn_end_data?`; the Bridge takes either as `delivery: queue`, so it never cuts and its ack and `:turn_enqueued` say it queues), is sent only to a worker whose `bridge.json` names the `revise` feature (else 409 `revise_unavailable`), and a failed turn gives it back (`:prompt_restored`'s `revises`).
|
|
9
|
+
**Revise stacks:** a web Revise turn (POST /turn `revises: {turn_id}`, the answer it rewrites) stamps its opening message (its prompt) with `revises: {turn_id}` at the turn's end, only when the turn completed with an answer (`AnswerTail`): a failed, canceled or empty turn revised nothing (`TurnStages::VerifyAnchors#stamp_opening`). A draft no prompt carries (a stale page, a rolled-back turn) is no target: no stamp. A draft a stamped turn already revised is re-targeted to its stack's top, the newest version, so a stack is a chain `v1 ← v2 ← v3`. Like `verify`, it is metadata only, never added to the prompt (no change to a request's bytes); chi web's history passes it on the opening message (and on a wake turn's note), and `:turn_completed` carries the stamped `revises` (the participant view's too). The history also marks a Verify turn's opening `verify: true`, and so does its live end event (`:turn_completed`'s `verify: true`, with its `turn_id`): the page offers no Revise on its answer. The web page draws the stacks from these stamps (docs/cli.md, Revise). Like a Verify turn's, a Revise message waits for the running turn's end (`SessionInbox.turn_end_data?`; the Bridge takes either as `delivery: queue`, so it never cuts and its ack and `:turn_enqueued` say it queues), is sent only to a worker whose `bridge.json` names the `revise` feature (else 409 `revise_unavailable`), and a failed turn gives it back (`:prompt_restored`'s `revises`). A participant's Revise is theirs only (`ParticipantRevise`, from the authors the server stamped): the answer to their own message (not a Verify turn's), with only their own turn openings after it (an owner's `!` line, which they never see, counts for nothing). chi web checks it at the send against the live worker's snapshot (403 `revise_not_own`, the same for a missing turn; 409 `revise_overtaken`; 409 `revise_unavailable` for a live worker without the `participant_revise` feature), and the worker checks it again at the stamp, up to the Revise turn itself. A Revise someone else's turn overtook after the send silently becomes a plain turn: it runs and answers, but nothing folds, no stack is drawn and neither they nor the owner get a notice; only a `revise_anchor_rejected` log line (reason `overtaken` or `not_own`) records it. It isn't refused at the start because a participant gets no `prompt_restored`, so their text would be lost. A delivery to a live worker without `participant_revise` is refused too (`SessionManager::Delivery`, 409 `revise_unavailable` in their words). The owner's Revise is unchanged, of anyone's answer. Both anchors travel as one `TurnAnchors` inside chi; on the wire and in an input file each keeps its own key (`verify`, `revises`).
|
|
10
10
|
|
|
11
|
-
**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
|
|
11
|
+
**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, one with images or one whose author isn't the running turn's (a participant's goes only into their own turn, and a participant's turn takes nobody else's: `SessionInbox.turn_end_data?`): 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`). For each file it merges, before deleting it, the drain records which turn took it: `input/merged/<enqueued_id>` = `{"into": the turn's enqueued_id, "at"}` (`SessionInbox.mark_merged`; none for a turn without an id), so a wait for that message waits for that turn (`ReplyWait::Poll#follow_merge`). A turn nobody typed (a reminder, a wake turn: a delegate report's, an attached context change's, a finished task's) gets an `enqueued_id` its worker mints as it runs, `wake-` and 16 hex digits (`Worker#wake_origin`), for its mark, `last_turn`, reply file and settle; the reports it merges keep id-less origins. Such a file, with a prompt, from the user, `chi send` or a delegating parent, also ends a running `task_wait` or `delegate_result` and hands a running `execute` over as a task after 3 s (`SessionInbox.steer_waiting?`, a peek the worker's turn polls through `Engine#input_waiting?`; in a participant's turn only their own line): the file stays for the 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`.
|
|
12
12
|
|
|
13
13
|
**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/`), and a message in a stopped worker's inbox (the next worker runs it).
|
|
14
14
|
|
|
@@ -104,7 +104,7 @@ chi sessions clean --dry-run --days 7 # test sessions older than 7 da
|
|
|
104
104
|
- `Session.list` / `SessionManager.list_sessions` / `GET /api/sessions?sort=&order=&limit=&offset=` default to `updated_at desc` (newest activity first). Also supports `created_at`, `asc`. `X-Total-Count` header when paginated.
|
|
105
105
|
- Web UI (`chi web`): the page's scope is in its URL. Started in a project (a git repository or a `chi project init` folder), `chi web` opens `/?dir=<that folder>`: that project's sessions, and new sessions start in that folder; the header chip says `<project> · all`, and `all` opens the same place as `?scope=all` (every session; a new session there starts in chi web's start folder, shown on the start page, and cards name their folder; `← <project>` goes back to the project view it came from, or to the start folder's project). A bare `/` (the LAN link, the Home Screen app) is the start folder's project too: the folder chi web runs in, or `web.start_dir` / `chi web --start-dir DIR`; outside a project (no repository or marked folder) it shows every session. A pushed notification opens its session in that session's own project (`/?dir=<its project>#/s/<id>`; `?scope=all` outside a project). One server serves every project: a second `chi web` (from another repo) finds it through `GET /api/info` and prints (with `--open`, opens) its page for its own folder instead of starting another. The 3 latest sessions sit above the chat, and the grip on the chat's top edge — the same thin handle as the composer's, its twin — puts them away and brings them back: a drag up (or a click, or Enter/Space on it) hides them, a drag down (or a click) shows them again, and the choice is remembered. A hidden strip leaves its status in the top bar (`N sessions · ● 1 live · ● 1 waiting`, from the cards), and a click on that pill opens All sessions, without showing the strip first; there the pill shows whatever the strip's state, pressed, and a click on it goes back to the chat (as Esc does). In a window under 900 px wide, where the strip scrolls sideways, the grip stays under it, outside the scroller; a window under 700 px tall hides the strip by itself, so a question or approval card in the stage keeps room, and the grip shows it until the window is tall again. A parent and its delegates are one card there: its `▸ 2 delegates · 1 live · 1 waiting` chip (the waiting part in the attention colour, the card's border too; just `▸ 2 delegates` while the rows are open: they carry the marks then) opens their rows in a popover under it (a row per delegate at any depth, indented; a click opens that one, Esc or a click outside closes it, and the strip's order holds while it is open). A fork is a card of its own, and so is a delegate whose parent isn't listed (archived, another project), with its `↳ <parent>` chip; the pill counts cards, and every live or waiting session. "All sessions" (or `/`) opens every session at `#/sessions`; its header is the top bar's right end: a search over preview, id and status, the count, an `archived` toggle and Select (on a phone a row of its own under the bar). Esc or the pressed pill returns; where the sessions are stored is a faint line under the list. A card is its title on one line (an untitled session reads `Untitled` over its first message), a line under it (its recap, else its first message) and its meta row, with one mark per state and no status dot: a waiting card has its warn outline and its `question` (or `approval`…) chip, an errored one an `error` badge, a session a worker runs the `live` badge (not on a waiting card: waiting means a worker), and idle, completed and stopped ones are unmarked; a family's and a chain's rows the same. There a parent's card holds its delegates the same way, the chip opening their rows inside the card (each with an archive button on hover), open by itself while one of them waits on you; the count says `14 · 7 delegates` (`3 / 14` while a search narrows it), a search that matches a delegate shows its parent's card with the list open and the matching rows marked, and select mode picks cards (archiving a parent takes its delegates). The open session is in the URL (`#/s/<id>`), so a reload or a copied link opens it again; the chi logo top left goes back to the empty start for a new session. The message box grows with its text; drag its top edge to keep it taller (double-click resets). A file in a tool row (a read, edit or write's title) and its diff lines are refs: hover one and click to open the current file in your editor at that line, on this machine only (`web.editor`; the browser's "always allow" for the page's port and VS Code's "allow opening local files" are asked once; see [Opening files from chi web](configuration.md#opening-files-from-chi-web)). So are an edit or write approval card's paths (at the first line the dry run changes) and its diff lines, before you approve, and the numbered lines of a read's output: `lines 40–60` under it opens the text as read, numbered, when the output is the file as-is (not the head/tail preview of a big file, an error or an image; no numbers after a cut). A `path:line` (or `path:line-line`) in an answer or a step's text is one too once the turn has ended, and after a reload, when it names a file a row or card on the page touched: the path under the session's folder, else the one touched file it ends with (a bare `app.js:12` when only one touched file is named so; two such files make it none); a path without a line counts only when it is a touched file exactly. Code blocks, links, bare URLs and inline code with more than the ref in it are left alone, and a file the session didn't touch is never linked; a link whose text is such a ref alone (github-pr's link to the PR diff) becomes a ref too, and its plain click still goes to the PR. A plugin's refs on an answer (an `after_turn` hook's, see [Refs on the answer](hooks.md#refs-on-the-answer-display-only)) are paired with its links and text whatever the rows touched: they can link a file the session didn't touch, or an issue or a commit, and add their own actions to the ref's menu. A plugin tool's row carries the refs its `refs:` gives (see [Refs on a tool's row](plugins.md#refs-on-a-tools-row)): its title opens the first, and its `▾` menu adds the others as `<ref> · <action>` (the URL on hover); their URL actions work on a phone too. A ref with more than one target shows a small `▾` on hover: it opens a menu of them, in order **PR diff** (a link's), **Open in editor**, **Show in turn** and **Copy path**, and a plain click runs the first (a link's own, else the editor, else show in turn). **Show in turn** scrolls to the tool row that touched the file (looked for in the ref's own step, then its own turn, then the rows before it, then the nearest after it; there, the last one showing the ref's line, else the last one), opens the steps, the stage's steps and the row's diff (or the read showing the line) and flashes the row and the line; an answer's or a step's ref and an approval card's path have it, not a row's own title or lines. It needs no editor, so a viewer without one (a phone on the LAN) gets an answer's refs with **Show in turn** as their click; a row's title and diff lines stay plain text there. On a touch screen a tap opens the menu. A ref with a target is a tab stop: Enter runs its first target, ↓ opens the menu (↑/↓ move, Enter picks, Esc closes back to the ref). Diff and read lines have their click only. The info bar's id copies the full session id on click ("Session id copied"), the id every CLI command takes (`chi --attach ID`, `chi send ID`). A session whose prompt carried model notes has a `notes: deepseek` chip there (the names without `model_notes_`; the tooltip lists each with its scope and size; `session.prompt_notes` in `GET /api/sessions/:id`, the live worker's, else the session file's; see [Memory](memory.md#model-notes)). Its ⚙ chip, after the model, holds the session's thinking level, reasoning history and LLM context: `⚙` alone while the session follows the model for all three, else `⚙` and what the session set, in the accent colour (`⚙ think low · ctx stale,forget`; a phone shows ⚙ with a dot; the tooltip gives each value and its source). Its panel is the start page's ⚙ panel for this session: a Thinking row (`/thinking`), on a chat host a Reasoning row (the reasoning history, `/reasoning-history`), and a Context row with its Apply and budget fields (`/llm-context`); a tap runs that row's command at once in the worker (one reply in the conversation, as a typed one) and changes that field alone, the Context row's `default` resets the strategy, apply rule and budget (`/llm-context reset`), and a tap while a turn runs is queued for after it (the panel's note says so). `GET /api/sessions/:id`'s `session.thinking`, `session.reasoning_history` (null on a native host) and `session.llm_context` carry the values (the live worker's, else resolved from the session file), and a `command_ran` that changed them carries the new ones. After ⚙, the 🧰 chip names the session's toolkit: `🧰` alone on the configured default (`toolkit.default`, `session.toolkit_default`), `🧰 lean` on another (see [Toolkits](configuration.md#toolkits); `/toolkit` changes it). A `/thinking` or `/reasoning-history` change typed in the composer that restarts the prompt cache asks first: the reply bubble has an **Apply (restarts the cache)** button that sends its `--yes` line (the ⚙ panel sends that line itself; see [Changes that restart the prompt cache](configuration.md#changes-that-restart-the-prompt-cache)). It shows the context used (`ctx 42%`) and the last generation's decode speed (`87 tok/s`, `~64 tok/s` when estimated; see `/stats` in [cli.md](cli.md#session-commands)); hovering `ctx` there or on a session card lists the session's tokens in (summed over every request, with the cached share), out (with reasoning) and the cost when the provider reports one, for this session only (one estimated from a `hosts.<name>.models` price shows with `~`: `cost: ~$0.54 ($0.42 reported, ~$0.12 estimated)`); after a `/model` switch the info bar's tooltip adds a `per model:` row per run of turns on one model above those totals (`splash:qwen3-35b · 6 turns · 96.0k in / 9.0k out · ~$0.02`, a switch's prefill under its row: `└ switch prefill 58.0k uncached prompt ≈$0.01`; the timing's `model_segments`, replaced by each `generation_completed`'s; the session cards stay as they were), and right after the switch its ctx % is the new model's window's; and the memory indexes its prompt holds (`memory index: ~3.4k tokens in this session's prompt (system 2.0k, project 1.4k)`; see [Memory](memory.md#the-indexs-size)). The start page's model picker (bottom left of the composer, the chosen name `▾`) chooses the model a new session starts on, and a fresh chain link's (see [Session chains](#session-chains); there a pick shows as a `/model` command bubble in the link); it opens a search list over the conversation (↓ or a click; downward when the start page leaves no room above): an empty search shows the last 5 picks (Recent) and then each host's model ids A–Z under the host, the default host first (an id the host declares under `hosts.<name>.models` first, noted `config`, its tooltip `served by config (hosts.<name>.models)`; a host that failed to list its models keeps those ids, greyed and noted `host down`, still pickable; a remembered or default model stays chosen even then, greyed on the button); typed words must each match the host or the id (a substring, else for 3+ characters the letters in order, so `deepseek4.1 fla` finds `openrouter · deepseek/deepseek-v4.1-flash`), ⏎ picks, Esc/Tab close, a pick puts the focus in the message box: `GET /api/models` lists the hosts' models as chi spells them (`{default, models: [{name, host, id, configured?, unavailable?, sampling?, llm_context?}], warning?}`; `configured: true` marks an id declared under `hosts.<name>.models`, listed first under its host, `unavailable: true` one whose host failed to list its models; `sampling` is the model's configured request parameters as `/model` words them, the row's tooltip in the list; bare for the default host, `host:model` for the others, from the same cached lists as `/models`, a bounded wait, a host that is down noted in `warning`; `toolkits`: `{default, suggest, kits: [{name, description?}]}`, the start page's 🧰 list: the configured `toolkit.default`, `suggest` `on` only when a new session's first message may get a suggested kit (`toolkit.suggest.mode: on`, a System 1 host, two described kits; else `audit` or `off`), and the built-in and config kits a new session can start on, without `review`, `child`, `undecided`, a kit `groups:` refuses or one that doesn't resolve (a base that is a bundle kit), the default always listed; left out when it can't be read), and `POST /api/sessions` takes `model` (blank means the default). Beside it, ⚙ opens the new session's rarely used settings (thinking and LLM context, following each `GET /api/models` row's `thinking` and `llm_context`, and the review files; see [configuration.md](configuration.md#llm-context-a-sessions-own-strategy)), and 🧰 its toolkit: **Auto** (with `toolkit.suggest.mode: on`: the first message may get a suggested kit, else the default) or a kit from the payload's `toolkits`, sent as `POST /api/sessions`'s `toolkit` (see [configuration.md](configuration.md#toolkits)); a refused kit (`400 toolkit_refused`) is said in the list, opened again. What is set shows on the row as an accent chip (`think high`, `tools: lean`; a dot on ⚙ or 🧰 on a phone); each is for the next new session only, never remembered. The start page's first message goes as later ones do: `POST /api/sessions` with `idle: true` and `preview` (the message, which names the session until its turn is saved), then `POST /api/sessions/:id/turn`, so a failed first turn puts its prompt back in the composer too; a first line that is one of the new session's commands (`/model …`) goes to `POST /api/sessions/:id/command` instead, as later ones do. The browser remembers the last model picked; a running session's model is in the info bar and changes only with `/model`. In the composer a line is a command when it is `!…` or starts with one of the session's command names (its `commands` list; `/stats`, `/exit` and the other terminal words get the page's own reply); an unknown `/word` (`/usr/bin/env is missing`) goes to the model as a prompt, as in a terminal; a one-word `/word` no command answers (`/modle`) is not sent: the page shows `Unknown command /modle. Did you mean /model? /help lists the commands.` (the "Did you mean" part only when a name is close). A `POST /turn` (or a session's first `prompt`) that is one of the worker's commands runs as that command too (`202 {status, command_id, session_id}`), so `chi send`, `chi -p` and a page whose command list lags all get the command. ↑/↓ in the composer walk the prompt history the TUI's ↑ walks ([Persistent Prompt History](cli.md#persistent-prompt-history)): ↑ with the caret on the first line, ↓ from the last line while browsing, ↓ past the newest brings the draft back; `GET /api/history` returns `{entries: [...]}`, oldest first (the page fetches it on focus and after each send). The server records a sent line in the routes once the worker took it (`POST /turn` accepted, a `!command` through `POST /command` or as a session's first line); `"history": false` in the body keeps it out (an image-only send).
|
|
106
106
|
- Web notifications: when a session needs you while the tab is hidden or behind another window (a question or a guardrail approval, a plugin card with buttons during a turn such as check-in's, a failed turn, a turn done after 10 s or more; a delegated session only for its questions, a canceled turn never), the page title counts it, `(N) Chi`, until you come back to the tab. The bell at the right of the top bar also turns on OS notifications ("needs an answer", "needs approval", "needs you", "turn failed", "done in 42 s", under the session's first message; a click opens the session): its first click asks the browser for the permission, and the choice is kept per browser. Any session of the page's scope counts, not only the open one. What was already so when the page loaded never counts, and several chi tabs show one notification per event. A chi tab in front tells the others (a `BroadcastChannel`), so while you look at one, the ones behind neither notify nor count what it shows; coming back to a tab clears those events in the other tabs' titles too. The hub's `session` frames carry what this needs: `pending_question` (`{id, kind}`, kind `question` or `approval`), `pending_card` (`{id, bundle}`: the running turn's card with actions, from `pending_card.json` that the worker keeps in the session's folder while one is open; a card without actions, such as loop-guard's, never counts) and `last_turn` (`{outcome, ended_at, seconds, origin}`, saved in the session's JSON as the engine ends each turn).
|
|
107
|
-
- The web frontend is a zero-build ES-module stack in `lib/samagotchi/web/public/`: `data.js` (retrieval, typed SSE `openStream` for a session and `openEvents` for the session list), `sessions_list.js` (the list as a pure reducer over `GET /api/events`: a snapshot replaces it, an upsert keeps a known card in place, a removal drops it; `families()` groups delegates under their parent), `families_view.js` (a parent's family chip, its rows and the strip's popover; the chip and rows as lit-html templates), `keep_focus.js` (lit reuses a list's nodes by position: a redraw keeps the focus on the same item by its `data-id` / `data-name`), `chains.js` (session chains: the links from `continues`, the cards folded into the latest link's, its `#N` chip and rows as lit-html templates, the info bar's ← → links, a refused continue, and the fresh-link rule), `notify.js` (which of those events needs the user, for the notifications), `app.js` (presentation/state; a session with no live stream gets one when its `session` event says `bridge_up`, after one re-read: `event_seq` starts over in each worker, so a dropped stream is never resumed with its old cursor), `bubbles.js` (the history's bubbles between the turns' rows: output chunks, end lines, every client's prompt bubbles by `enqueued_id` with their queued/steered badges, the idle recap, guardrail warnings, hooks' notices, plugins' steers, command and note bubbles; lit-html templates, put in the history with the sticky follow by `createBubbles`), `cards_place.js` (where plugin cards and a snapshot's notices go in the history: a card added or updated in place by its id, a running turn's into its step, a snapshot's each above the first turn after it, a finished turn's notice in its step's rows and its question resolved where it was asked; a card's action buttons), `format.js` (pure formatters such as `previewOf`), `plain_line.js` (markdown out of the plain one-line places: the stage headline, a closed step's title, an answered question card's kept-reply line), `prompt_history.js` (the composer's ↑/↓ over the shared prompt history), `commands.js` (session commands: which composer lines are commands, the page's own replies, a `command_ran`'s bubble, the page's command bubbles kept across a re-read), `composer.js` (where a composer line goes, `routeComposerLine`: /cut and /queue, the start page's own replies, a session command or `!cmd`, a typo hint, else a turn; and a session command sent from the composer or a chip), `delivery_line.js` (the composer's row above the text field in a session, always there so the composer keeps its height (on a phone no row: the line lies over the session bar's left while a turn runs and you type or have a choice armed): the key hint while idle, and the delivery line while a turn runs: where a message typed now goes in, from the step's phase, clock and size, whether "cut in now" can land, and the queue link for a worker with the `delivery` feature, as a pure `deliveryLine`, its lit-html template and `createDeliveryControl`, which ticks it and keeps a choice armed from a link; see docs/cli.md, "Typing during a turn"), `restore.js` (a failed turn's prompt handed back into this tab's composer, before or after its `/turn` ack), `lists_view.js` (the session lists: the strip and its pill, all sessions with its search, count and select mode, their cards, the family and chain popovers; the cards, tile and pill as lit-html templates, drawn as new nodes each time, and the lists' orders as pure functions), `stream_handlers.js` (the open session's stream event handlers, by event type, over the page's views and hooks; a snapshot replays through them too), `stage_view.js` (the stage view: the running turn pinned above the composer, with Stop turn in its status row while it runs, then handed off into the history; its pure rules and texts in `stage_model.js`, its live slots (headline, now row, trail, ticks) in `stage_slots.js`, its cap and held height in `stage_frame.js`, the in-use watch that times the hand-off in `stage_watch.js`), `info_bar.js` (the session bar's info line as a lit-html template over a plain model, three groups with hairlines between them, on one line above a phone's width (the folder gives way first): the setup (id, status, model, the ⚙ and 🧰 chips, notes, the memory or working-set chip), the relations (chain links, the parent as `↑ <id>`, delegates, participants; only when there is one) and the telemetry (folder, ctx, speed, the session's time, worker badge)), `settings_chip.js` (the session bar's ⚙ chip and its panel over `settings_panel.js`: a tap runs that row's command, one field), `toolkit_chip.js` (the session bar's 🧰 chip: the kit against `toolkit.default`), `info_bar_model.js` (that model and the bar's buttons' states, built from the page's session state, its list and its in-flight continues and restarts (the restarting set is `version_notices.js`'s) as pure functions; `app.js`'s `updateInfoBar` applies them every second while a turn runs), `session_state.js` (what the page knows about the open session: its empty state, and `sessionStateFrom`, the fields a `GET /sessions/:id` payload sets), `refs.js` (refs: a kind and a target on the element, a plugin's ref data (label, http(s) URL actions) bound in a WeakMap by `bindRef`, the targets per kind, show in turn, one delegated set of click, key and hover listeners), `ref_menu.js` (a ref's `▾` and its menu), `ref_scan.js` (the `path:line` refs in an answer's or a step's text that name a file the session's rows touched), `answer_refs.js` (the refs a plugin attached to an answer, its `display_refs`: bound to the answer's bubble, paired with its links and text, bound with `bindRef`), `markdown_place.js` (where rendered markdown and its refs go: a text as markdown HTML, its `path:line` refs, a final answer's rendering), `images.js` (image chips in the composer: paste, drop and the image button, the upload before a turn, the thumbnails and the lightbox), `composer_grip.js` (the composer's grip and fold: the drag, tap and keys, the fold per visit), `composer_mount.js` (the composer mounted on a page's markup: the message box and its height, the image chips from a paste, a drop or the image button, the grip and fold, ↑/↓ over the prompt history and the keys; the page's own parts come in as hooks: what Enter and Send do, keys taken first, Esc, where the history comes from, the info bar's drop while folded, typing anywhere to open it; its styles, the card it sits in (`.composer-card`, its footer and its line above the text field) included, are the shared `composer.css`, linked after `tokens.css` by `index.html`
|
|
107
|
+
- The web frontend is a zero-build ES-module stack in `lib/samagotchi/web/public/`: `data.js` (retrieval, typed SSE `openStream` for a session and `openEvents` for the session list), `sessions_list.js` (the list as a pure reducer over `GET /api/events`: a snapshot replaces it, an upsert keeps a known card in place, a removal drops it; `families()` groups delegates under their parent), `families_view.js` (a parent's family chip, its rows and the strip's popover; the chip and rows as lit-html templates), `keep_focus.js` (lit reuses a list's nodes by position: a redraw keeps the focus on the same item by its `data-id` / `data-name`), `chains.js` (session chains: the links from `continues`, the cards folded into the latest link's, its `#N` chip and rows as lit-html templates, the info bar's ← → links, a refused continue, and the fresh-link rule), `notify.js` (which of those events needs the user, for the notifications), `app.js` (presentation/state; a session with no live stream gets one when its `session` event says `bridge_up`, after one re-read: `event_seq` starts over in each worker, so a dropped stream is never resumed with its old cursor), `bubbles.js` (the history's bubbles between the turns' rows: output chunks, end lines, every client's prompt bubbles by `enqueued_id` with their queued/steered badges, the idle recap, guardrail warnings, hooks' notices, plugins' steers, command and note bubbles; lit-html templates, put in the history with the sticky follow by `createBubbles`), `cards_place.js` (where plugin cards and a snapshot's notices go in the history: a card added or updated in place by its id, a running turn's into its step, a snapshot's each above the first turn after it, a finished turn's notice in its step's rows and its question resolved where it was asked; a card's action buttons), `format.js` (pure formatters such as `previewOf`), `plain_line.js` (markdown out of the plain one-line places: the stage headline, a closed step's title, an answered question card's kept-reply line), `prompt_history.js` (the composer's ↑/↓ over the shared prompt history), `commands.js` (session commands: which composer lines are commands, the page's own replies, a `command_ran`'s bubble, the page's command bubbles kept across a re-read), `composer.js` (where a composer line goes, `routeComposerLine`: /cut and /queue, the start page's own replies, a session command or `!cmd`, a typo hint, else a turn; and a session command sent from the composer or a chip), `delivery_line.js` (the composer's row above the text field in a session, always there so the composer keeps its height (on a phone no row: the line lies over the session bar's left while a turn runs and you type or have a choice armed): the key hint while idle, and the delivery line while a turn runs: where a message typed now goes in, from the step's phase, clock and size, whether "cut in now" can land, and the queue link for a worker with the `delivery` feature, as a pure `deliveryLine`, its lit-html template and `createDeliveryControl`, which ticks it and keeps a choice armed from a link; see docs/cli.md, "Typing during a turn"), `restore.js` (a failed turn's prompt handed back into this tab's composer, before or after its `/turn` ack), `lists_view.js` (the session lists: the strip and its pill, all sessions with its search, count and select mode, their cards, the family and chain popovers; the cards, tile and pill as lit-html templates, drawn as new nodes each time, and the lists' orders as pure functions), `stream_handlers.js` (the open session's stream event handlers, by event type, over the page's views and hooks; a snapshot replays through them too), `stage_view.js` (the stage view: the running turn pinned above the composer, with Stop turn in its status row while it runs, then handed off into the history; its pure rules and texts in `stage_model.js`, its live slots (headline, now row, trail, ticks) in `stage_slots.js`, its cap and held height in `stage_frame.js`, the in-use watch that times the hand-off in `stage_watch.js`), `info_bar.js` (the session bar's info line as a lit-html template over a plain model, three groups with hairlines between them, on one line above a phone's width (the folder gives way first): the setup (id, status, model, the ⚙ and 🧰 chips, notes, the memory or working-set chip), the relations (chain links, the parent as `↑ <id>`, delegates, participants; only when there is one) and the telemetry (folder, ctx, speed, the session's time, worker badge)), `settings_chip.js` (the session bar's ⚙ chip and its panel over `settings_panel.js`: a tap runs that row's command, one field), `toolkit_chip.js` (the session bar's 🧰 chip: the kit against `toolkit.default`), `info_bar_model.js` (that model and the bar's buttons' states, built from the page's session state, its list and its in-flight continues and restarts (the restarting set is `version_notices.js`'s) as pure functions; `app.js`'s `updateInfoBar` applies them every second while a turn runs), `session_state.js` (what the page knows about the open session: its empty state, and `sessionStateFrom`, the fields a `GET /sessions/:id` payload sets), `refs.js` (refs: a kind and a target on the element, a plugin's ref data (label, http(s) URL actions) bound in a WeakMap by `bindRef`, the targets per kind, show in turn, one delegated set of click, key and hover listeners), `ref_menu.js` (a ref's `▾` and its menu), `ref_scan.js` (the `path:line` refs in an answer's or a step's text that name a file the session's rows touched), `answer_refs.js` (the refs a plugin attached to an answer, its `display_refs`: bound to the answer's bubble, paired with its links and text, bound with `bindRef`), `markdown_place.js` (where rendered markdown and its refs go: a text as markdown HTML, its `path:line` refs, a final answer's rendering), `images.js` (image chips in the composer: paste, drop and the image button, the upload before a turn, the thumbnails and the lightbox), `composer_grip.js` (the composer's grip and fold: the drag, tap and keys, the fold per visit), `composer_mount.js` (the composer mounted on a page's markup: the message box and its height, the image chips from a paste, a drop or the image button, the grip and fold, ↑/↓ over the prompt history and the keys; the page's own parts come in as hooks: what Enter and Send do, keys taken first, Esc, where the history comes from, the info bar's drop while folded, typing anywhere to open it; its styles, the card it sits in (`.composer-card`, its footer and its line above the text field) included, are the shared `composer.css`, linked after `tokens.css` by `index.html`), `strip_grip.js` (the latest-sessions strip's grip, the composer grip's twin: hides and shows the strip), `strip_hidden.js` (the strip hidden or shown: the saved choice, the short window's own hide, the grip and the body's class), `storage.js` (the page's per-browser choices in localStorage, guarded: a choice that can't be read or written is not remembered), `unqueue.js` (the × on a message queued for after the turn: takes it back, its text into an empty composer or a toast), `toasts.js` (the page's toast stack: a timer each, sticky ones until acted on, the same text reusing its item), `toast_place.js` (where the toast sits: above the composer card, the stage and an open model panel), `dock_reserve.js` (what floats over the history: the history's bottom reserve (`--dock-h`) at the composer card's and the showing stage's height, a reader at the end kept there, and the toast's place measured for `toast_place.js`), `notice_links.js` (a hook notice line's http(s) URLs as links), `start_page.js` (the start page's model picker, its ⚙ panel's and 🧰 list's choices for the next new session only and their override chips; a fresh chain link's picker and its `/model`), `start_settings.js` (the start page's ⚙ panel: thinking and LLM context as rows of segments, applied at once, the Apply and budget disclosure, the review files' view; the override chips' words and ⚙'s dot), `settings_panel.js` (the panel's rows as lit-html templates, shared: the segments, the disclosure, a hidden row's sent-anyway line, the review line; the caller passes its rows and applies the picks), `start_toolkit.js` (the start page's 🧰 list: Auto with suggest on, the kits, the default marked; a pick closes it; its override chip and dot), `session_actions.js` (Continue, Archive and its Undo, Delete and Stop, and the start page when the open session is gone), `send_turn.js` (the composer's send: a new session first on the start page, the route, the echo, the image upload, the `/turn` ack, a fast failure's restore and each error's place). Unit-tested via `npm test` (`spec/web/public/*.test.js`).
|
|
108
108
|
- Selecting a session in the web UI is read-only: `GET /api/sessions/:id` never spawns a worker (it reads a live worker's snapshot when one runs, else the session file). A prompt (`POST /turn`) or a command (`POST /command`) wakes the worker, and `/stream` briefly waits for a freshly-spawned bridge before answering. A caught-up SSE reconnect holds the stream open; `reset` markers are only sent for reconnects behind the ring window, or with a cursor from another worker (event ids are `<event_seq>-<epoch>`, one epoch per worker).
|
|
109
109
|
- The web session page's message box folds away for room to read. Drag the grip on its top edge (the pill that shows on hover; always on a phone) down, past the box by 32 px: the pill turns accent while releasing would hide it, and on release the box folds and the info bar stays as the page's bottom edge. The grip stays as a handle on the info bar's top edge, drawn always: pull it up (8 px opens it, and the box follows the pointer in the same gesture), click or tap it (opens it and focuses the box), or just type on the page (the key lands in the box) to bring it back; Enter or Space on the focused grip folds or opens too. A draft (text or images, which folding never clears) colours the handle with the accent, a session waiting on you with the attention colour; its tooltip says so. Stop turn isn't the message box's: it sits in the running turn's stage, at the right edge of its status row beside the fold caret (collapsed stage or not), while the turn runs, and goes when the turn ends A turn the page knows runs but has nothing of to draw (a reload whose session payload carries no current turn) shows that status row alone, `running` and Stop turn. The fold is remembered per browser (`chi_composer_collapsed`), an archived session opens folded for the visit, and opening it there, like the page opening it itself (a restored line, a quote, continue →'s opener), is for that visit only. The fold keeps the height the grip set (`chi_composer_floor`): the box opens again at the height it had, and only a double-click on the grip forgets it. That height never takes more than half of the window the box is drawn in: a floor saved in a bigger window is held to that half in a smaller one (a rotated phone, a resized window, another device on the same origin), and the saved floor itself stays, so the height comes back when the room does. A click on the expanded grip hides the box as well: it folds 250 ms after the press, so a second click inside that window is read as a double-click and resets the height instead (a drag, Enter or Space act at once, and a drag after a click takes the wait back). The start page's grip only resizes.
|
|
110
110
|
|
|
@@ -448,6 +448,9 @@ sessions](cli.md#review-sessions)); both start idle and print the `chi share` li
|
|
|
448
448
|
and **Start a review of it**: a review fork of it, opened, with a toast (`Review 4b7e9a01 started from 3f9a1c2e's
|
|
449
449
|
recap; 3f9a1c2e keeps running.`) whose **Invite** opens the new session's panel. Every summary carries `review`
|
|
450
450
|
(null for other sessions).
|
|
451
|
+
- **A participant's side:** their link opens the main page on the review session as a plain conversation: their
|
|
452
|
+
turns stay read-only review turns ("Alex · review"), its subjects are not shown (no file view: they ask chi to
|
|
453
|
+
quote what they need), and **Apply** is on the owner's page only.
|
|
451
454
|
|
|
452
455
|
## Toolkits
|
|
453
456
|
|
|
@@ -83,10 +83,14 @@ module Samagotchi
|
|
|
83
83
|
end
|
|
84
84
|
end
|
|
85
85
|
|
|
86
|
-
# Whether the turn in progress is a participant's
|
|
87
|
-
#
|
|
88
|
-
def participant_turn?
|
|
89
|
-
|
|
86
|
+
# Whether the turn in progress is a participant's turn (its author):
|
|
87
|
+
# anyone else's input queues behind it then (Bridge's POST /turn).
|
|
88
|
+
def participant_turn? = !participant_author.nil?
|
|
89
|
+
|
|
90
|
+
# The turn in progress's participant author ("p:<id>"), nil for the
|
|
91
|
+
# owner's or none.
|
|
92
|
+
def participant_author
|
|
93
|
+
@mutex.synchronize { @turn && Steer.sender_author(@turn[:author] || TurnOrigin.from(@turn[:origin])&.author) }
|
|
90
94
|
end
|
|
91
95
|
|
|
92
96
|
# The turn in progress as conversation messages, for a plugin's
|
data/lib/samagotchi/bridge.rb
CHANGED
|
@@ -779,12 +779,23 @@ module Samagotchi
|
|
|
779
779
|
# pair_turns: a participant's turn in a working session runs as a pair
|
|
780
780
|
# turn (Guardrails::PairBoundary); a worker without it runs it as a
|
|
781
781
|
# review turn, and chi web says so (Delivery::PAIR_FEATURE).
|
|
782
|
+
# participant_steer: a participant's message sent while their own turn
|
|
783
|
+
# runs goes in at its next step (a worker without it queues it); only
|
|
784
|
+
# what the page's delivery line and chi web's ack say depends on it
|
|
785
|
+
# (Delivery::OWN_TURN_STEER_FEATURE).
|
|
786
|
+
# cancel_turn_id: POST cancel with a turn_id cancels that turn only
|
|
787
|
+
# (Delivery::CANCEL_TURN_ID_FEATURE), what chi web needs to relay a
|
|
788
|
+
# participant's cancel.
|
|
789
|
+
# participant_revise: a participant's Revise draft is kept, and the
|
|
790
|
+
# Engine checks it at the stamp (ParticipantRevise): what chi web needs
|
|
791
|
+
# to pass one on (ReviseAnchor::PARTICIPANT_FEATURE).
|
|
782
792
|
# activity: GET /activity answers with Worker#activity_snapshot, so
|
|
783
793
|
# `chi schedule tick` can ask a live worker what it is busy with.
|
|
784
794
|
FEATURES = ["restart", "task_stop", "reparent", Delivery::QUEUE_FEATURE, ReplyWait::TURN_IDS_FEATURE,
|
|
785
795
|
VerifyAnchor::FEATURE, ReviseAnchor::FEATURE,
|
|
786
796
|
SessionInbox::UNQUEUE_FEATURE, Delivery::REVIEW_FEATURE, Delivery::PAIR_FEATURE, TurnDrawings::FEATURE,
|
|
787
|
-
WorkerLink::ACTIVITY_FEATURE, ObjectivesTrack::FEATURE
|
|
797
|
+
WorkerLink::ACTIVITY_FEATURE, ObjectivesTrack::FEATURE, Delivery::OWN_TURN_STEER_FEATURE,
|
|
798
|
+
Delivery::CANCEL_TURN_ID_FEATURE, ReviseAnchor::PARTICIPANT_FEATURE].freeze
|
|
788
799
|
|
|
789
800
|
# The routes, by method and what follows /session/:id/: a handler takes
|
|
790
801
|
# the session id and the request body and returns [headers, status, body].
|
|
@@ -1249,10 +1260,10 @@ module Samagotchi
|
|
|
1249
1260
|
# request's principal; the Bridge is loopback and trusts its caller).
|
|
1250
1261
|
author = fetched(parsed, "author")
|
|
1251
1262
|
author = nil unless author.is_a?(String) && !author.empty?
|
|
1252
|
-
# A Verify turn's claim, a Revise turn's draft
|
|
1253
|
-
#
|
|
1263
|
+
# A Verify turn's claim, a Revise turn's draft. A participant's draft
|
|
1264
|
+
# is checked by chi web at the send and by the Engine at the stamp
|
|
1265
|
+
# (TurnStages::VerifyAnchors#revision_target, ParticipantRevise).
|
|
1254
1266
|
anchors = TurnAnchors.from(parsed)
|
|
1255
|
-
anchors = anchors.without_revises if Steer.participant_author?(author)
|
|
1256
1267
|
if sid.to_s.strip.empty? || prompt.to_s.strip.empty?
|
|
1257
1268
|
return [{ "Allow" => "POST" }, 400,
|
|
1258
1269
|
{ error: "missing_fields", detail: "session_id and prompt are required" }]
|
|
@@ -1275,12 +1286,14 @@ module Samagotchi
|
|
|
1275
1286
|
# (chi web's, from their grant: its queue limit plus the running one).
|
|
1276
1287
|
pending_limit = nil
|
|
1277
1288
|
if Steer.participant_author?(author)
|
|
1278
|
-
# A participant's message
|
|
1279
|
-
#
|
|
1280
|
-
#
|
|
1289
|
+
# A participant's message never cuts, nor is a command or a shell
|
|
1290
|
+
# line: chi web refuses those first, and this is the worker's own
|
|
1291
|
+
# layer. It goes into a running turn only when that turn is theirs
|
|
1292
|
+
# (#enqueue_post_turn, under the event log), else it is a turn of
|
|
1293
|
+
# its own (D6).
|
|
1281
1294
|
return participant_refused(sid, author, "command") if command_registry.command?(prompt.to_s)
|
|
1282
1295
|
|
|
1283
|
-
delivery = Delivery::
|
|
1296
|
+
delivery = Delivery::NEXT_STEP if Delivery.cut?(delivery)
|
|
1284
1297
|
pending_limit = fetched(parsed, "pending_limit")
|
|
1285
1298
|
pending_limit = nil unless pending_limit.is_a?(Integer) && pending_limit.positive?
|
|
1286
1299
|
end
|
|
@@ -1336,10 +1349,11 @@ module Samagotchi
|
|
|
1336
1349
|
next :too_many if pending_limit && pending_turns(author) >= pending_limit
|
|
1337
1350
|
next :duplicate unless @engine.reserve_enqueued_id(enqueued_id)
|
|
1338
1351
|
|
|
1339
|
-
#
|
|
1340
|
-
#
|
|
1341
|
-
#
|
|
1342
|
-
|
|
1352
|
+
# Input goes into a running turn only of its own author: the
|
|
1353
|
+
# owner's never into a participant's turn (a steer, a cut), a
|
|
1354
|
+
# participant's only into their own; else it queues behind it as
|
|
1355
|
+
# a turn of its own, and the ack and :turn_enqueued say so.
|
|
1356
|
+
delivery = Delivery::QUEUE unless same_author_steer?(author)
|
|
1343
1357
|
queued_behind_turn = Delivery.queue?(delivery) && !@accumulator.current_turn.nil?
|
|
1344
1358
|
unless enqueue_turn(prompt: prompt, client_id: client_id, enqueued_id: enqueued_id,
|
|
1345
1359
|
no_interrupt: no_interrupt, images: images, delivery: delivery, author: author, anchors: anchors)
|
|
@@ -1430,6 +1444,19 @@ module Samagotchi
|
|
|
1430
1444
|
dequeue_reply(taken, id, input)
|
|
1431
1445
|
end
|
|
1432
1446
|
|
|
1447
|
+
# Whether input from +author+ may go into the running turn: the owner's
|
|
1448
|
+
# into the owner's (or none running), a participant's into their own
|
|
1449
|
+
# only (none running: theirs runs as the next turn, a queue). Read with
|
|
1450
|
+
# the event log held.
|
|
1451
|
+
def same_author_steer?(author)
|
|
1452
|
+
sender = Steer.sender_author(author)
|
|
1453
|
+
running = @accumulator.participant_author
|
|
1454
|
+
return running.nil? if sender.nil?
|
|
1455
|
+
return false if @accumulator.current_turn.nil?
|
|
1456
|
+
|
|
1457
|
+
running == sender
|
|
1458
|
+
end
|
|
1459
|
+
|
|
1433
1460
|
# +author+'s turns running or waiting: the running turn when it is
|
|
1434
1461
|
# theirs, and their queued ones. Read with the event log held.
|
|
1435
1462
|
def pending_turns(author)
|
data/lib/samagotchi/delivery.rb
CHANGED
|
@@ -64,6 +64,19 @@ module Samagotchi
|
|
|
64
64
|
# share treat the session as :review (ParticipantMode.live) until it
|
|
65
65
|
# restarts.
|
|
66
66
|
PAIR_FEATURE = "pair_turns"
|
|
67
|
+
# The worker sidecar feature (Bridge::FEATURES) of a worker whose
|
|
68
|
+
# participant's message sent while their own turn runs goes in at its
|
|
69
|
+
# next step (Bridge#same_author_steer?, SessionInbox.turn_end_data?'s
|
|
70
|
+
# running author). One without it queues every participant message,
|
|
71
|
+
# which is safe: the feature only says what will really happen (chi
|
|
72
|
+
# web's ack, the page's delivery line).
|
|
73
|
+
OWN_TURN_STEER_FEATURE = "participant_steer"
|
|
74
|
+
# The worker sidecar feature (Bridge::FEATURES) of a worker whose cancel
|
|
75
|
+
# compares the turn_id it names under its lock (TurnState#cancel!, 409
|
|
76
|
+
# turn_mismatch). One without it may ignore the id and cancel whatever
|
|
77
|
+
# runs, so chi web relays a participant's cancel only to a worker with it
|
|
78
|
+
# (ParticipantAccess#participant_cancel).
|
|
79
|
+
CANCEL_TURN_ID_FEATURE = "cancel_turn_id"
|
|
67
80
|
|
|
68
81
|
# Whether it is the one every sender defaulting to a step boundary uses.
|
|
69
82
|
def self.next_step?(value) = parse(value) == NEXT_STEP
|
data/lib/samagotchi/engine.rb
CHANGED
|
@@ -528,7 +528,11 @@ module Samagotchi
|
|
|
528
528
|
served_expected_by: ->(served) { served_expected_by(served) },
|
|
529
529
|
working_set_snapshot: ->(session, conversation) { working_set_snapshot(session, conversation) },
|
|
530
530
|
turn_running: -> { turn_running? },
|
|
531
|
-
announce_or_hold: ->(event) { announce_or_hold(event) }
|
|
531
|
+
announce_or_hold: ->(event) { announce_or_hold(event) },
|
|
532
|
+
# The newest request's estimate, for a client that joins mid-turn
|
|
533
|
+
# (see EventFanout#record_context_status). The turn thread writes it;
|
|
534
|
+
# the snapshot reads it once and dups it.
|
|
535
|
+
keep_context_status: ->(status) { @last_context_status = status }
|
|
532
536
|
)
|
|
533
537
|
end
|
|
534
538
|
private :build_event_fanout
|
|
@@ -35,9 +35,12 @@ module Samagotchi
|
|
|
35
35
|
# @param working_set_snapshot [#call] (session, conversation) → WorkingSet::Snapshot
|
|
36
36
|
# @param turn_running [#call] → Boolean
|
|
37
37
|
# @param announce_or_hold [#call] (event) → announces the event, or holds it
|
|
38
|
+
# @param keep_context_status [#call] (status) → keeps the newest request's
|
|
39
|
+
# context estimate (the Engine's @last_context_status) for a client that
|
|
40
|
+
# joins mid-turn
|
|
38
41
|
def initialize(observer:, used_memories:, muted:, steer_cut:, thinking_notices:, metrics:, hooks:, turn_context:,
|
|
39
42
|
last_turn_context:, model_settings:, session:, live_conversation:, used_memory_names:, served_expected_by:,
|
|
40
|
-
working_set_snapshot:, turn_running:, announce_or_hold:)
|
|
43
|
+
working_set_snapshot:, turn_running:, announce_or_hold:, keep_context_status:)
|
|
41
44
|
@observer = observer
|
|
42
45
|
@used_memories = used_memories
|
|
43
46
|
@muted = muted
|
|
@@ -55,6 +58,7 @@ module Samagotchi
|
|
|
55
58
|
@working_set_snapshot = working_set_snapshot
|
|
56
59
|
@turn_running = turn_running
|
|
57
60
|
@announce_or_hold = announce_or_hold
|
|
61
|
+
@keep_context_status = keep_context_status
|
|
58
62
|
@working_set_emitted = nil
|
|
59
63
|
end
|
|
60
64
|
|
|
@@ -97,6 +101,7 @@ module Samagotchi
|
|
|
97
101
|
watch&.started(event[:iteration], restarted: true)
|
|
98
102
|
end
|
|
99
103
|
when :generation_chunk then thinking_notices.note_thought(progress[:thinking])
|
|
104
|
+
when :context_status then record_context_status(event)
|
|
100
105
|
end
|
|
101
106
|
# The chat loop asked again without the thinking fields or the
|
|
102
107
|
# reasoning sent back: a notice, not an event of its own.
|
|
@@ -179,6 +184,22 @@ module Samagotchi
|
|
|
179
184
|
event
|
|
180
185
|
end
|
|
181
186
|
|
|
187
|
+
# The newest request's estimate, kept for a client that joins mid-turn:
|
|
188
|
+
# EngineSnapshots#session_state_snapshot's `context_status` reads it, so a
|
|
189
|
+
# page opened while a turn runs shows the percent at once instead of
|
|
190
|
+
# "ctx ?". Stored before the event is emitted: a snapshot read between the
|
|
191
|
+
# emit and this store would have a cursor past the event and the older
|
|
192
|
+
# value. Only a turn's stream handler comes through here, so a side
|
|
193
|
+
# request (/btw, a recap) can't overwrite it; the turn-end write stays.
|
|
194
|
+
def record_context_status(event)
|
|
195
|
+
pct = event.dig(:usage, :estimated_pct)
|
|
196
|
+
return unless pct.is_a?(Numeric)
|
|
197
|
+
|
|
198
|
+
@keep_context_status.call({ est_pct: pct, bucket: event[:bucket] })
|
|
199
|
+
rescue StandardError => e
|
|
200
|
+
Log.warn(:turn, "context_status_keep_failed", error: e.class.name, msg: e.message.to_s[0, 200])
|
|
201
|
+
end
|
|
202
|
+
|
|
182
203
|
# The turn's StreamWatch, or nil when no hook listens.
|
|
183
204
|
def stream_watch(cancel_controller)
|
|
184
205
|
return nil unless cancel_controller && hooks.any?(Hooks::StreamWatch::EVENT)
|
data/lib/samagotchi/grants.rb
CHANGED
|
@@ -72,6 +72,11 @@ module Samagotchi
|
|
|
72
72
|
# (config web.participants.queue); at most MAX_QUEUE.
|
|
73
73
|
DEFAULT_QUEUE = 3
|
|
74
74
|
MAX_QUEUE = 20
|
|
75
|
+
# How many distinct image files a participant may upload into the
|
|
76
|
+
# grant's session (ParticipantUploads, chi web's participant upload),
|
|
77
|
+
# and how many uploads they may try in all, refused ones counted.
|
|
78
|
+
MAX_IMAGES = 40
|
|
79
|
+
MAX_IMAGE_ATTEMPTS = 2 * MAX_IMAGES
|
|
75
80
|
# chi share --expires and config web.participants.expires: Nh or Nd.
|
|
76
81
|
EXPIRES_RE = /\A(\d+)([hd])\z/
|
|
77
82
|
|
|
@@ -15,8 +15,8 @@ module Samagotchi
|
|
|
15
15
|
# project to ~/.ssh is ~/.ssh; a `..` after a symlinked dir climbs out
|
|
16
16
|
# of its target, as the kernel opens it: PathResolve.reals).
|
|
17
17
|
# ReviewBoundary asks it for every read of a participant's turn; a
|
|
18
|
-
# review session's subjects (chi review
|
|
19
|
-
#
|
|
18
|
+
# review session's subjects (chi review) go through the same
|
|
19
|
+
# check.
|
|
20
20
|
class ReviewPaths
|
|
21
21
|
# Home folders that hold keys and tokens, never read in a review turn
|
|
22
22
|
# (nor read or written in a pair turn).
|
|
@@ -127,20 +127,54 @@ module Samagotchi
|
|
|
127
127
|
!@tool.nil?
|
|
128
128
|
end
|
|
129
129
|
|
|
130
|
+
# How long one conversion may run before its process is killed.
|
|
131
|
+
TIMEOUT = 60
|
|
132
|
+
# ImageMagick's coder for each sniffed format (ImageHeader.format_of):
|
|
133
|
+
# the input is read as that format, never as whatever its bytes claim.
|
|
134
|
+
CODERS = { png: "PNG", jpeg: "JPEG", gif: "GIF", webp: "WEBP", bmp: "BMP", tiff: "TIFF", heic: "HEIC" }.freeze
|
|
135
|
+
# ImageMagick's resource limits for one conversion.
|
|
136
|
+
MAGICK_LIMITS = ["-limit", "memory", "256MiB", "-limit", "disk", "1GiB", "-limit", "area", "64MP"].freeze
|
|
137
|
+
|
|
130
138
|
# Writes +input+ to +output+ as +format+ (:png or :jpeg), no bigger than
|
|
131
139
|
# +max_side+ on its long side (never upscaled; nil keeps the size).
|
|
132
|
-
|
|
140
|
+
# +from+: the input's sniffed format (magick reads it with that coder).
|
|
141
|
+
# A tool still running after +timeout+ seconds is killed (Error).
|
|
142
|
+
def convert(input, output, format:, max_side: nil, quality: 85, from: nil, timeout: TIMEOUT)
|
|
133
143
|
raise Error, "no image tool (install ImageMagick)" unless available?
|
|
134
144
|
|
|
135
|
-
command = @tool == :sips
|
|
136
|
-
|
|
137
|
-
|
|
145
|
+
command = if @tool == :sips
|
|
146
|
+
sips_command(input, output, format, max_side, quality)
|
|
147
|
+
else
|
|
148
|
+
magick_command(input, output, format, max_side, quality, from)
|
|
149
|
+
end
|
|
150
|
+
out, ok = run_command(command, timeout)
|
|
151
|
+
raise Error, "#{@tool} failed: #{out.strip[0, 200]}" unless ok && File.size?(output)
|
|
138
152
|
|
|
139
153
|
output
|
|
140
154
|
end
|
|
141
155
|
|
|
142
156
|
private
|
|
143
157
|
|
|
158
|
+
# Runs +command+, its output and whether it succeeded; kills it (its own
|
|
159
|
+
# pid only) past +timeout+.
|
|
160
|
+
def run_command(command, timeout)
|
|
161
|
+
Open3.popen2e(*command) do |stdin, out, wait|
|
|
162
|
+
stdin.close
|
|
163
|
+
reader = Thread.new { out.read.to_s }
|
|
164
|
+
unless wait.join(timeout)
|
|
165
|
+
begin
|
|
166
|
+
Process.kill("KILL", wait.pid)
|
|
167
|
+
rescue Errno::ESRCH
|
|
168
|
+
nil
|
|
169
|
+
end
|
|
170
|
+
wait.join
|
|
171
|
+
reader.kill unless reader.join(1)
|
|
172
|
+
raise Error, "#{@tool} took over #{timeout} s"
|
|
173
|
+
end
|
|
174
|
+
[reader.value, wait.value.success?]
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
|
|
144
178
|
def sips_command(input, output, format, max_side, quality)
|
|
145
179
|
command = ["sips", "-s", "format", format.to_s]
|
|
146
180
|
command += ["-s", "formatOptions", quality.to_s] if format == :jpeg
|
|
@@ -148,8 +182,9 @@ module Samagotchi
|
|
|
148
182
|
command + [input, "--out", output]
|
|
149
183
|
end
|
|
150
184
|
|
|
151
|
-
def magick_command(input, output, format, max_side, quality)
|
|
152
|
-
|
|
185
|
+
def magick_command(input, output, format, max_side, quality, from = nil)
|
|
186
|
+
coder = CODERS[from]
|
|
187
|
+
command = ["magick", *MAGICK_LIMITS, "#{"#{coder}:" if coder}#{input}[0]"]
|
|
153
188
|
command += ["-resize", "#{max_side}x#{max_side}>"] if max_side
|
|
154
189
|
command += ["-quality", quality.to_s] if format == :jpeg
|
|
155
190
|
command + ["#{format}:#{output}"]
|
|
@@ -255,6 +290,10 @@ module Samagotchi
|
|
|
255
290
|
store(session_dir, bytes, info, name, source)
|
|
256
291
|
end
|
|
257
292
|
|
|
293
|
+
# Where +bytes+ of +format+ (a SENDABLE one) are stored as they are:
|
|
294
|
+
# images/<sha256's first 16 hex>.<ext>.
|
|
295
|
+
def self.file_name_for(bytes, format) = "#{DIR}/#{Digest::SHA256.hexdigest(bytes)[0, 16]}.#{EXT.fetch(format)}"
|
|
296
|
+
|
|
258
297
|
# A ref is only ever read when its file name matches REF_RE and the file
|
|
259
298
|
# is inside this session's images folder.
|
|
260
299
|
def self.valid_ref?(session_dir, ref)
|
|
@@ -283,7 +322,7 @@ module Samagotchi
|
|
|
283
322
|
return "images are refs to uploaded files, not paths" if ref.key?(:path)
|
|
284
323
|
return Error.unknown_image(ref[:file]).message unless valid_ref?(session_dir, ref)
|
|
285
324
|
|
|
286
|
-
{ file: ref[:file].to_s, name: File.basename(ref[:name].to_s)[0, 120] }
|
|
325
|
+
{ file: ref[:file].to_s, name: File.basename(ref[:name].to_s).gsub(/[[:cntrl:]]/, "")[0, 120] }
|
|
287
326
|
end
|
|
288
327
|
end
|
|
289
328
|
|
|
@@ -398,8 +437,8 @@ module Samagotchi
|
|
|
398
437
|
File.binwrite(input, bytes)
|
|
399
438
|
# sips upscales to --resampleHeightWidthMax, so pass it only to shrink.
|
|
400
439
|
side = too_wide ? limits.max_side : nil
|
|
401
|
-
out = run(resizer, input, File.join(dir, "out.png"), :png, side)
|
|
402
|
-
out = run(resizer, input, File.join(dir, "out.jpg"), :jpeg, side) if File.size(out) > limits.max_bytes
|
|
440
|
+
out = run(resizer, input, File.join(dir, "out.png"), :png, side, info.format)
|
|
441
|
+
out = run(resizer, input, File.join(dir, "out.jpg"), :jpeg, side, info.format) if File.size(out) > limits.max_bytes
|
|
403
442
|
result = File.binread(out)
|
|
404
443
|
raise Error, "#{name} is still over #{limits.max_bytes} bytes after downscaling" if result.bytesize > limits.max_bytes
|
|
405
444
|
|
|
@@ -407,8 +446,8 @@ module Samagotchi
|
|
|
407
446
|
end
|
|
408
447
|
end
|
|
409
448
|
|
|
410
|
-
def self.run(resizer, input, output, format, max_side)
|
|
411
|
-
resizer.convert(input, output, format: format, max_side: max_side)
|
|
449
|
+
def self.run(resizer, input, output, format, max_side, from)
|
|
450
|
+
resizer.convert(input, output, format: format, max_side: max_side, from: from)
|
|
412
451
|
rescue ImageResizer::Error => e
|
|
413
452
|
raise Error, e.message
|
|
414
453
|
end
|
|
@@ -417,7 +456,7 @@ module Samagotchi
|
|
|
417
456
|
raise Error, "#{name}: can't read its size" unless info&.width && info.height
|
|
418
457
|
raise Error, "#{name} is not an image chi can send" unless MIME.key?(info.format)
|
|
419
458
|
|
|
420
|
-
file =
|
|
459
|
+
file = file_name_for(bytes, info.format)
|
|
421
460
|
path = File.join(session_dir.to_s, file)
|
|
422
461
|
unless File.file?(path)
|
|
423
462
|
FileUtils.mkdir_p(File.dirname(path))
|
|
@@ -102,7 +102,9 @@ module Samagotchi
|
|
|
102
102
|
errors = refused.map { |name| "Error: memory '#{name}' is muted for this session" }
|
|
103
103
|
return errors.join("\n") if allowed.empty?
|
|
104
104
|
|
|
105
|
-
|
|
105
|
+
found = read.call(allowed.join(","))
|
|
106
|
+
text = [found, *errors].join(Tools::MemoryRead::SEPARATOR)
|
|
107
|
+
found.respond_to?(:reads) ? Tools::MemoryRead::Result.new(text, found.reads) : text
|
|
106
108
|
end
|
|
107
109
|
|
|
108
110
|
# ask_user_question: validate the call, then hand the payload to the
|
|
@@ -1192,6 +1194,10 @@ module Samagotchi
|
|
|
1192
1194
|
# The tools a find_tools hit loads (Tools::ToolIndex::FoundText),
|
|
1193
1195
|
# saved with a chat result (ToolResponse.single).
|
|
1194
1196
|
dispatched[:loaded] = result.loaded if result.respond_to?(:loaded) && !Array(result.loaded).empty?
|
|
1197
|
+
# Which files answered a memory_read (Tools::MemoryRead::Result), so the
|
|
1198
|
+
# run can be stamped with them (ToolRunner#memory_stamp): nothing else
|
|
1199
|
+
# says which scope's file, or which overlay, read each name.
|
|
1200
|
+
dispatched[:memory_reads] = result.reads if result.respond_to?(:reads)
|
|
1195
1201
|
if result.respond_to?(:images) && !Array(result.images).empty?
|
|
1196
1202
|
dispatched[:images] = Array(result.images)
|
|
1197
1203
|
dispatched[:image_only] = true if result.respond_to?(:image_only?) && result.image_only?
|
|
@@ -11,7 +11,8 @@ module Samagotchi
|
|
|
11
11
|
# the reply once more, and a second one fails the turn.
|
|
12
12
|
#
|
|
13
13
|
# The rule: the whole reply is one closed block that opens with the call
|
|
14
|
-
# and ends with
|
|
14
|
+
# and ends with a close of that block, its own tag or the wrapper's
|
|
15
|
+
# `</tool_call>`. It needs no model profile (ChatLoop
|
|
15
16
|
# has no ToolCallParser). A reply opening with prose, a fence or a quote is
|
|
16
17
|
# an answer, and so is one that mentions, quotes or fences the syntax, or
|
|
17
18
|
# says anything after the close. The block carries no inner tags: an empty
|
|
@@ -27,11 +28,13 @@ module Samagotchi
|
|
|
27
28
|
OPENS = ["<tool_call>", "<function=", "<parameter="].freeze
|
|
28
29
|
# ... and end with the close of that same block: nothing else in the
|
|
29
30
|
# reply. A `<parameter=` call often closes with `</function>` (the
|
|
30
|
-
# flavour that mixes the two tags does it mid-block too)
|
|
31
|
+
# flavour that mixes the two tags does it mid-block too), and a block
|
|
32
|
+
# opened by `<function=` or `<parameter=` can also end with the
|
|
33
|
+
# wrapper's own `</tool_call>` instead of its tag.
|
|
31
34
|
CLOSES = {
|
|
32
35
|
"<tool_call>" => ["</tool_call>"],
|
|
33
|
-
"<function=" => ["</function>"],
|
|
34
|
-
"<parameter=" => ["</parameter>", "</function>"]
|
|
36
|
+
"<function=" => ["</function>", "</tool_call>"],
|
|
37
|
+
"<parameter=" => ["</parameter>", "</function>", "</tool_call>"]
|
|
35
38
|
}.freeze
|
|
36
39
|
# How much of the reply the warning keeps (KernelLoop's own cap).
|
|
37
40
|
TAIL_CHARS = 400
|
|
@@ -209,6 +209,9 @@ module Samagotchi
|
|
|
209
209
|
refs.zip(calls, texts.map(&:text))
|
|
210
210
|
end
|
|
211
211
|
|
|
212
|
+
# One run as a FileRun (nil for a call that isn't a read, edit or
|
|
213
|
+
# write, or names no path). Public: WorkingSet.timeline_of reads a
|
|
214
|
+
# read's range and partial mark off it, never off the arguments again.
|
|
212
215
|
def file_run(index, ref, call, text, root, step)
|
|
213
216
|
name = call[:name].to_s
|
|
214
217
|
return nil unless name == READ || CHANGES.include?(name)
|
|
@@ -272,6 +275,6 @@ module Samagotchi
|
|
|
272
275
|
end
|
|
273
276
|
end
|
|
274
277
|
|
|
275
|
-
private_class_method :file_runs, :results, :
|
|
278
|
+
private_class_method :file_runs, :results, :lines, :line, :field, :arguments, :native_parser
|
|
276
279
|
end
|
|
277
280
|
end
|
|
@@ -28,5 +28,44 @@ module Samagotchi
|
|
|
28
28
|
# Their turn failed (a wait's error, a failed turn's no_reply): never
|
|
29
29
|
# the exception's words or the failure summary, which may name a path.
|
|
30
30
|
TURN_FAILED = "this turn failed: the session's owner can see why"
|
|
31
|
+
# A participant's Stop on a worker that can't stop only the turn named
|
|
32
|
+
# (Delivery::CANCEL_TURN_ID_FEATURE): it might stop the owner's next turn.
|
|
33
|
+
CANCEL_UNAVAILABLE = "this session's worker is too old to stop only your turn: ask the session's owner to restart it"
|
|
34
|
+
# 403 revise_not_own: a Revise of a turn that isn't theirs, unanswered
|
|
35
|
+
# or a Verify turn, or no turn at all (ParticipantRevise: one answer,
|
|
36
|
+
# no existence oracle).
|
|
37
|
+
REVISE_NOT_OWN = "you can revise only an answer to your own message"
|
|
38
|
+
# 409 revise_overtaken: someone else's turn came after the answer they revise.
|
|
39
|
+
REVISE_OVERTAKEN = "someone else's turn came after this answer: send a new message instead"
|
|
40
|
+
# 409 revise_unavailable: a live worker without
|
|
41
|
+
# ReviseAnchor::PARTICIPANT_FEATURE (it doesn't check their Revise), or
|
|
42
|
+
# without Revise at all; never ReviseAnchor::UNAVAILABLE's owner words.
|
|
43
|
+
# The page's chip has the one copy (participant_revise.js).
|
|
44
|
+
REVISE_UNAVAILABLE = "this session's worker is too old for you to revise an answer: ask the session's owner to " \
|
|
45
|
+
"restart it"
|
|
46
|
+
# 429 too_many_images: their grant uploaded Grants::MAX_IMAGES distinct images.
|
|
47
|
+
TOO_MANY_IMAGES = "this participant's image allowance is used up: describe what the image shows"
|
|
48
|
+
# 429 too_many_image_attempts: Grants::MAX_IMAGE_ATTEMPTS uploads tried, refused ones counted.
|
|
49
|
+
TOO_MANY_IMAGE_ATTEMPTS = "this participant has tried too many image uploads: describe what the image shows"
|
|
50
|
+
# 422 unsupported_image: a bmp, tiff or heic, the formats chi converts
|
|
51
|
+
# with an external tool (sips or magick). Their png, jpeg, gif or webp
|
|
52
|
+
# still goes through the tool when it must be downscaled.
|
|
53
|
+
IMAGE_TYPE = "this image type can't be sent from a participant's page: send a png, jpeg, gif or webp"
|
|
54
|
+
# 422 image_too_big: no size in its header, or over the participant
|
|
55
|
+
# limits (Web::ParticipantImages::MAX_PARTICIPANT_PIXELS / _SIDE).
|
|
56
|
+
IMAGE_TOO_BIG = "this image is too large to send from a participant's page: send a smaller one (at most 40 megapixels)"
|
|
57
|
+
# 422 bad_image: their upload isn't an image chi can store. Never
|
|
58
|
+
# ImageStore's words (an image tool's output may name a host path).
|
|
59
|
+
BAD_IMAGE = "this file can't be sent as an image: send a png, jpeg, gif or webp"
|
|
60
|
+
# 400 bad_images: their turn's images aren't a list of at most
|
|
61
|
+
# ImageStore::MAX_TURN_REFS {file, name} refs (no path).
|
|
62
|
+
IMAGES_SHAPE = "images are a list of at most 20 {file, name} refs to your uploads"
|
|
63
|
+
# 400 bad_images: an image they uploaded is no longer in the session
|
|
64
|
+
# (ImageStore's words, which name the file, only in the audit line).
|
|
65
|
+
IMAGE_GONE = "an image you uploaded is no longer in this session: attach it again"
|
|
66
|
+
# 422 no_vision: their turn has images and the session's model can't
|
|
67
|
+
# take them. Never the model's name or the /model hint: those are the
|
|
68
|
+
# owner's (the audit line has the full refusal).
|
|
69
|
+
NO_VISION = "this session's model can't take images: send text only"
|
|
31
70
|
end
|
|
32
71
|
end
|