Render reconstructable system prompts at each request-series boundary, preserve series declarations through pre-step wrappers, and keep the presentation and replay snapshots aligned across clients.
A claimed slash command consumed only the text half of the composer
submission: /goal with reference images executed, cleared the draft, and
silently stranded the images in the rail. Model-visible attachment intent
had no route through the command plane.
The submission envelope is now modeled end to end. CommandDefinition
input.images declares acceptance; the declaration rides the descriptor to
every client, onto the minted CommandClaim, and into the input machine's
claim snapshot. commands.execute carries the submission's base64 images
and enforces the declaration in the executor: non-declaring commands, a
missing attachment store, and exceeded batch limits settle as logged
error results before the handler runs. Admission reuses the attachment
package's new admitEncodedImages, extracted from api-proxy's prompt path
so both wire endpoints share one limits/validation/commit sequence.
Producers own model visibility: /goal submits one user followup (image
blocks + a fixed reference line) after a successful create/edit so goal
rounds read the images from session history; /plan folds them into its
steered message. Grammar misfits (/goal pause, bare /plan, /plan off)
return direct errors and the composer keeps the images.
On the client, enter adjudication carries a SubmitEnvelope and every
command route that cannot consume images throws a localized refusal that
renders as one composer notice with draft and images retained; the
claimed pre-gate applies the same copy. An accepting claim serializes the
draft images, forwards them to commands.execute, and clears plus releases
them only on a success outcome.
The assembled web test roster gains the ui-input-trigger and ui-commands
plugins, mirroring the shipped composition, so slash submissions exercise
the command plane; a new keyless snapshot pins the refusal banner and the
accepting /goal flow over the built client graph.
Review round: the JSONL backend now refuses a foreign header version straight
from the raw header line, before validating today's header shape or decoding
any event row, so a structurally different future format reports the upgrade
direction instead of corruption (shared message builder
sessionFormatVersionRefusal). HMR live-prefix adoption runs the unknown-type
guard like the other read paths. The appendCore comment now states why the
unknown-type guard is read-side only, the loadStoredFrom JSDoc and README pin
the seek-vs-sequential refusal-scope divergence, and the generated catalog
preamble lists the ignorable envelope field.
Old runtimes meeting a newer session format now fail loud instead of
misreading: version refusal names the direction (newer: upgrade the
harness; older: no upgrade path) and points at the raw JSONL log, and an
event type outside the generated known vocabulary refuses resume unless
its envelope carries the new ignorable: true marker (default: required,
so a forgotten marker over-refuses instead of silently resuming a gutted
session). gen-persistence-catalog now also emits
KNOWN_SESSION_EVENT_TYPES; SQLite stores the marker in a dedicated
column (SCHEMA_VERSION 15). The versioning design (monotonic integer,
n->n+1 upgrader chain, migrate-on-continue) is recorded in the
session-log-version-mechanism Agent Note.
Review round findings:
- Clear the streaming attempt before the request-error waterfall: a cancel
landing during recovery (typically the llm/retry backoff, after clients
reset the streamed rendering) must not resurrect the failed stream's
prefix. Provider failures commit nothing, now including that window.
- Record interrupted: true on the cancellation-finalized assistant/message.
The chat projection keeps the settled prefix classified as interrupted
(Stopped chip, restored web goldens), and request inspection leaves the
request uncompleted so the step boundary classifies it as before.
- Pin the recovery-window and retry-discard semantics with content-bearing
failed streams in cancel.spec; update the ACP late-end expectation to the
finalized-prefix transcript.
- Mention interruptedBlocks() in the assembler stream-lifecycle JSDoc.
A turn cancelled mid-stream dropped everything the interrupted step had
streamed: chunks stayed in the log for replay, but no assistant/message
joined the surface, so the next request carried nothing the user had
watched stream. Follow-ups after cancel could not connect and forks
inherited the gap.
Keep the streaming attempt alive across the step's request loop and, when
an abort escapes with the attempt uncommitted, finalize its user-visible
prefix as the step's ordinary assistant/message citing the logged chunk
seqs. BlockAssembler.interruptedBlocks() owns the safe subset next to the
max-tokens rule: closed and open text/reasoning blocks with content, in
stream order; tool calls, empty blocks, and unknown open blocks drop.
Retry clears the attempt first, so an abort after llm/retry finalizes
nothing from the reset stream. Tool-phase cancellation and provider
failures keep their existing shapes.