From 67f241f448c5237e1920f0949b03e8abb67ee310 Mon Sep 17 00:00:00 2001 From: Aly Raffauf Date: Fri, 2 Oct 2026 01:01:22 -0400 Subject: [PATCH] docs(operations): explain receipts and recovery Document the queue, storage, and retry contracts. Spell out what an unresolved action means. --- robot-docs/commands.md | 4 +++- robot-docs/migrations.md | 29 +++++++++++++++++++++++- robot-docs/operations.md | 49 ++++++++++++++++++++++++++++++++++++++++ robot-docs/sessions.md | 4 ++++ robot-docs/skills.md | 2 +- 5 files changed, 85 insertions(+), 3 deletions(-) create mode 100644 robot-docs/operations.md diff --git a/robot-docs/commands.md b/robot-docs/commands.md index c924538..f8af5f9 100644 --- a/robot-docs/commands.md +++ b/robot-docs/commands.md @@ -4,7 +4,7 @@ every session command is one definition with three faces: the CLI menu, user inv ## the catalog -`GET /sessions/:id/commands` lists the materialized catalog: name (the slash spelling), description, the minted python `method`, its `usage` line, declared `arguments`, and the `modelCallable` / `userTurn` flags. the CLI menu renders it (merged with its presentation-only entries like `/login` and `/sessions`), `GET`'s list mints the kernel bindings at boot, and the prompt's `` block summarizes it. all three read the same list. +`GET /sessions/:id/commands` lists the materialized catalog: name (the slash spelling), description, the minted python `method`, its `usage` line, declared `arguments`, and the `modelCallable`, `userTurn`, and `skill` flags. The `skill` flag marks a selected skill with a preparation function. The CLI submits these through the durable events route. Other command handlers keep their existing uncertainty policy. The CLI menu renders the catalog, merged with its presentation-only entries like `/login` and `/sessions`. The list mints the kernel bindings at boot, and the prompt's `` block summarizes it. `POST /sessions/:id/commands` runs one command. the body is `{name, arguments}` with raw invocation text (`{"name": "/model", "arguments": "gpt-5 anthropic"}`) or `{name, args}` with declared names (`{"name": "/model", "args": {"model": "gpt-5"}}`); `clientId` labels a submitted turn. the answer is `{"result": ...}` with the command's JSON value or `{"submitted": true}` when a user invocation submitted its turn. @@ -21,6 +21,8 @@ Command( [Argument("window", "reporting window", False)], True, False, + False, + None, fn(context, _caller, args) { ... Ok(command.Data(value)) }, ) ``` diff --git a/robot-docs/migrations.md b/robot-docs/migrations.md index 282fb08..892bb0e 100644 --- a/robot-docs/migrations.md +++ b/robot-docs/migrations.md @@ -29,7 +29,10 @@ sessions; session selection and live reload never rerun migrations. 1. `conversation.initialise` creates the core tables, then calls `conversation_columns.apply`: the existing session columns first, transcript - columns second. it then runs its existing domain-local session recovery + columns second. The same core initialization creates `operations`, + `pending_inputs`, `submission_events`, and `continuation_markers` through + `operations.schema`. + These are new tables and require no backfill. it then runs its existing domain-local session recovery (missing activity/title from transcript), then creates `sessions_activity`. 2. quota, mail, and family initialise their tables, unchanged. 3. `migrations.run(ledger, backup)` runs the core `image_store.run`, which first @@ -132,3 +135,27 @@ to their implementation modules. provider assignment likewise remains in the existing server startup path. - no zstd, compression, schema-version, offline storage upgrade, new dependency, or unrelated transaction fix is part of this refactor. + +## durable operation admissions + +`operations` owns receipt retention and pending delivery state on the shared +store connection. Session creation commits its session row and accepted receipt +in one immediate transaction. Submission admission commits its receipt and +pending input together. Transcript consumption moves pending metadata into +`submission_events`, records its committed position, and removes pending input +in the same transaction as the transcript rows. Failed statements roll back the +whole transaction, including a failed commit. + +Receipts retain the original admission result. They do not reference the session +row, so deletion cancels pending inputs and leaves receipts available. Terminal +receipts expire seven days after acceptance for creation or rejection, or after +commit or cancellation for submissions. Pending receipts do not expire. Cleanup +removes at most 128 expired receipts per call. Durable display metadata in +`submission_events` remains until session deletion, including operation IDs, +display text, and echo identity. Continuations commit a `continuation_markers` +row in the same transaction without adding a user-text transcript row. These +markers also remain until session deletion and survive receipt cleanup. + +Acceptance guarantees stored input or its committed or cancelled outcome. +Commit means the input reached the transcript. Neither guarantee promises that +assistant generation, tools, or external effects occur exactly once. diff --git a/robot-docs/operations.md b/robot-docs/operations.md new file mode 100644 index 0000000..8320470 --- /dev/null +++ b/robot-docs/operations.md @@ -0,0 +1,49 @@ +# Retry-safe operation admission + +Session creation and user turn submissions use a client-generated UUIDv7 `operationId`. Covered submissions are text, images, continuation, and skill activation. Each intended action gets one ID, even when two actions have identical content. The client retains the ID and encoded request until it resolves the outcome. Failure to obtain random bytes prevents submission. + +An accepted response means the daemon stored the action safely. It does not mean the assistant finished. Assistant generation, tool calls, and arbitrary command effects do not have an exactly-once guarantee. + +## Ownership and atomicity + +`daemon/operations.gleam` owns receipts and pending inputs in the core SQLite store. `daemon/conversation.gleam` commits session creation with its receipt and consumes pending input with its transcript changes. The store actor runs each transaction on its own connection. + +The `operations` table survives session deletion. `pending_inputs` records acceptance order and the full resolved submission, including display text, model input, image, source, client echo metadata, and operation ID. Consumption writes transcript inputs and images, compact display metadata, receipt delivery state, and pending deletion in one transaction. A failure rolls back all of those changes. + +Committed display metadata retains display text, source, client echo ID, operation ID, and optional original image MIME type, dimensions, and byte count. It contains no image content, image references, or resolved model text. The transcript and image store own those payloads. History displays the original upload dimensions even when the model receives a fitted image. An operations-owned commit descriptor carries this metadata and the transcript offset from submission preparation into consumption. + +`continuation_markers` retains consumed continuations at their transcript boundary without adding a user-text row. These markers survive receipt expiry and go with the session on deletion. A creation receipt's target is the original session ID, including after deletion. + +A failed admission transaction leaves neither an accepted receipt nor its mutation. A failed consumption transaction leaves the input pending and appends no transcript input. The session retains blocked work and retries preparation at most once every 15 seconds while it remains usable. Startup opens sessions with pending work, including sessions whose previous turn was idle. + +## Requests and duplicate detection + +`POST /sessions` requires `operationId` alongside the creation fields. `POST /sessions/:id/events` requires `operationId` alongside the submission fields. Text and images use `type: "user"`, which is also the omitted default. Continuation uses `type: "continue"`. Skill activation uses `type: "skill"`, `name`, and raw `arguments`, and resolves through the selected skill's preparation function. It does not execute an arbitrary command handler. The commands route refuses user skill activation; model skill calls still return instructions as data. + +The fingerprint includes a version, operation kind, target session, and normalized request fields. JSON key ordering and omitted defaults do not distinguish actions. Image content and skill arguments do. Authentication and echo-only `clientId` do not. An explicit `submissionId` participates because it identifies ownership for targeted cancellation; otherwise ownership defaults to the operation ID. Provider defaults and skill content resolve on first execution. + +An identical retry returns the original HTTP status and admission body. A changed request under the same ID returns `409 operation_conflict` and leaves the original decision intact. Receipt lookup precedes mutable workspace, provider, skill, and queue validation. Rejected decisions also replay their original status and error. + +Sessions accept at most 32 waiting inputs, including while idle, blocked, interrupted, or opening a kernel. An identical retry uses no additional slot. A distinct request at capacity receives a durable rejection that still replays after the queue drains. Recovery restores every accepted pending input without truncation and refuses new admissions while the restored queue is at or above capacity. + +## Query and retention + +Authenticated `GET /operations/:operationId` returns the receipt, including its ID, fingerprint, kind, target, timestamp, admission status, original result or error, and HTTP status. A submission also reports `deliveryStatus`: + +- `pending`: safely stored and waiting for transcript consumption. `blockingReason` can explain why delivery is waiting. +- `committed`: the input is recorded in the transcript. A continuation records its durable marker without adding a user-text row. +- `cancelled`: interruption or session deletion cancelled the waiting input. + +Creation receipts have no delivery status. Interrupt cancels waiting accepted inputs and stops the active turn. Deletion cancels pending inputs before removing the session. A retry never resurrects cancelled input or recreates a deleted session. + +The daemon keeps receipts for seven days after their terminal outcome. Pending inputs and their receipts never expire. Cleanup removes expired terminal receipts in batches of at most 128. Existing receipts are checked first. An unknown ID more than seven days old returns `410 operation_expired` and cannot execute. An ID more than five minutes ahead returns `400 operation_future`. + +A missing young ID returns `404 operation_unknown`. This result does not prove that an earlier in-flight request cannot still commit. Recovery retains the same ID. + +## Client recovery boundaries + +The Go client retains an explicit operation handle with the original encoded request. After transport loss, an invalid acknowledgement, or a server error, it queries the receipt and permits one automatic replay of the same request within the existing deadline. Cancellation stops retries and preserves the handle. A later query resolves that action without another submission POST. The prompt CLI also requests cancellation of its own submission on timeout or cancellation: queued input is removed atomically with its cancelled receipt, while a shared running turn continues for its other participants. + +Pending UI rows match durable events by operation ID. Identical text from different actions cannot settle each other's pending rows. The TUI retains pending submission handles when you switch sessions and queries them when you return. Uncertain creation also keeps its handle across navigation; recovery adds the original session to the picker without changing the selected session. General commands, settings, interruption, child creation, and external effects retain their separate uncertainty handling and do not use this automatic replay policy. + +`410 operation_expired` ends automatic recovery. The client returns an uncertain outcome with the original handle and sends no further automatic receipt queries or submission requests for that action. Expired user rows keep their IDs and attachments and remain visibly unresolved, but do not count toward the pending-send limit. Expired continuations retain an unresolved note with their operation ID. These states survive navigation. A matching durable user event can still reconcile an unresolved row. Expired creation retains its handle and notice without changing the selected session. diff --git a/robot-docs/sessions.md b/robot-docs/sessions.md index 389f122..1ee222a 100644 --- a/robot-docs/sessions.md +++ b/robot-docs/sessions.md @@ -1,5 +1,9 @@ # session attachment +Session creation and user turn admission use durable [operation receipts](operations.md). +An accepted submission survives a daemon restart while it waits for transcript +consumption. A retry with the same operation ID returns the original admission. + opening a session attaches the cli to its event stream and independently reads `GET /sessions/:id/status`. replayed history does not establish the session's live phase; status and live events do. diff --git a/robot-docs/skills.md b/robot-docs/skills.md index 07f913e..220f2ee 100644 --- a/robot-docs/skills.md +++ b/robot-docs/skills.md @@ -55,7 +55,7 @@ Activation returns the installed `SKILL.md` path as `source`. Relative paths in The official client guide recommends user-explicit activation through a slash command or mention, while leaving the exact syntax to the client. Albedo assigns `/` and includes it in command autocomplete. Built-in commands always win. A colliding skill such as `model` gets the deterministic command `/skill:model` instead of replacing `/model`. -The CLI resolves an invocation against the daemon's [command catalog](commands.md) and asks the daemon to run it. The daemon resolves the same immutable session catalog used by Python, runs the same skill command, and a user invocation submits one normal user turn. The visible intent remains the original slash command while the model input carries a JSON-delimited activation with the exact arguments, source, and instructions. Normal idle, enabled-extension, and workspace checks still apply. +The CLI resolves an invocation against the daemon's [command catalog](commands.md), whose `skill` flag identifies skill entries. It submits a dedicated skill activation through the events route with a stable operation ID. The daemon uses the selected entry's `skill_activation` preparation function, shared with the existing command implementation. It stores the resolved instructions with the admission, so a retry or restart does not reread changed skill content. The visible intent remains the original slash command while the model input carries a JSON-delimited activation with the exact arguments, source, and instructions. See [operation receipts](operations.md) for recovery and retention. ## Trust and execution -- knotmirror 0.0.1