CHAT-01C private readback and recovery contracts
R2 draft candidate for independent review, not an implemented API.
R1 request 26137 received REQUEST CHANGES from Filbert and Dewey via agent-send.
Both complete verdicts were collected before revision; R1 remains archived at
/tmp/chat-01c-r1-frozen-yvsfowd1.
Jason approved the contract-only proposal in #1507 comment 26131 in the control-board conversation and required performing-agent attribution. Darkwing authors these four files. Filbert independently reviews; Dewey reviews consumer semantics. No earlier CHAT-01 approval applies to this candidate.
Baseline: reviewed CHAT-01 commit 28d4e98ad8b406ca84b30170558bee22402f1e55.
Those files remain unchanged. This companion adds private requests/projections;
it does not widen the base command parser, grants or native capabilities.
Boundary and attribution
Only this directory's README, schema, fixtures and checker are authored, plus scoped shared tracking and append-only logs. No runtime endpoints, engine adapters, native sessions, live access, policy changes or deployments. CHAT-02 and CHAT-03I/03D retain their separate charters and unresolved gates.
Darkwing's seat credentials were verified to authenticate as Gitea darkwing,
account 104. Use that identity for issue actions. Do not use the legacy API
helper's default Jason credentials. Future commits explicitly use the performing
agent as author and committer, without changing shared Git defaults. Existing
published history is not rewritten. No credential value belongs in these files.
Closed messages and authority
The draft 2020-12 schema defines six closed top-level message variants and five
closed private item projections. Every property is required. Requests alone use
privateClientRequest; the other variants are server responses, not client input.
Validate that request definition, not the union of requests and responses, at
untrusted ingress. Version 1 identifies this new proposal, not a live migration.
IDs are opaque bounded references, never paths or authority. The server resolves connection, actor, current grant, approved scope and storage epoch. A client cannot post an actor, grant, snapshot contents or private storage reference. Native controller ownership is not required for reading one's own data or explicitly recovering one's own refused input. Separate read-private/recover-private grants are proposed names only; no role file or actual grant is changed here.
Every operation requires connected authenticated ingress, active/unexpired current actor grant, approved conversation/scope and the relevant capability. The model's scope digest binds the full inherited host/seat/project/workspace/conversation scope. These are supplied synthetic facts, not an authentication implementation. Browser origin/CSRF, private channel identity, storage provenance and actual grant producers remain prerequisite runtime work. Current authority is checked on every page, range, preview and recovery, including an idempotent retry.
| Command | Behavior |
|---|---|
| read-private-state | Bounded actor-private snapshot page; optional exact opaque cursor |
| read-upload-range | Exact owned immutable upload revision and verified byte range |
| preview-upload-text | Bounded complete UTF-8 preview as literal text, never active content |
| recover-refused-draft | Explicit CAS recovery of own never-dispatched refused input into a private unsent draft |
Refusals use the closed privateRefusal reason set. The reference functions return
symbolic refused:reason strings internally; response() demonstrates the typed
wire projection. Refusal is not permission to change source or retry native input.
There are no hidden writes, engine commands or automatic resume operations.
Safe private-state restoration
The page includes authenticated actor, conversation, opaque snapshot ID, source revision, items and next cursor. Items contain only explicit allowlisted fields:
- Draft: original branch, text, upload IDs, revision, state and recovery lineage.
- Upload: display filename/media type, total and committed received byte counts, full digest, revision and state.
- Queue: revision, actual state, causal admission request, original draft, exact target and frozen payload digest. Bounded frozen input includes original branch, draft revision, text and immutable upload IDs/revisions/filenames/sizes/digests.
- Receipt: causal request, monotonic source revision, actual/uncertain state/reason.
- Confirmation: exact target, operation, stop context, expiry and display state.
No privateBlobRef, source path, credential, grant or arbitrary internal property is serialized. Filenames and media labels remain untrusted display data. This is actor-private restoration, not a shared transcript or permission to adopt another actor's drafts. Reading any earlier branch never changes the active branch.
Queue and receipt request fields reference the same causal admission. Queue draft and target fields preserve the original lineage and branch; never join pending input to a newer mutable draft's text. Frozen input resolves by immutable digest, not the current mutable queue/draft contents. Missing/corrupt or mismatched retained content yields frozenInput null and integrity-unavailable, without inventing text or changing actual queue status. No private blob reference is projected. Snapshot retention must preserve referenced immutable payloads for valid cursors.
For every snapshot page, resolve each confirmation's CURRENT authoritative record by scoped actor/conversation/ID. Overlay current consumed/cancelled/expired outcomes. Do not promote an old snapshot to more permissive authority. Missing or changed current revision, operation, connection/incarnation, target, expiry or stop context makes the pending/confirmed snapshot reconfirm-required. A changed current stop invalidates it even when connection and execution target did not change. An old connection/incarnation or changed target makes a pending/confirmed confirmation display reconfirm-required. Expiry is explicit. Consumed/cancelled/ expired terminal outcomes are preserved rather than relabelled by reconnect. Reading a confirmation never renews it or creates a replacement. The base control protocol still validates exact current target/stop context and single use. The miniature clock and projected confirmation expiry use UTC epoch milliseconds. Production normalization must convert authoritative timestamps without guessing.
Pagination and snapshot rules
Capture the authorized private records at one atomic cut. Sort by kind and opaque ID; the cursor retains the exact immutable snapshot and next offset. A page has at most 100 items and 8 MiB serialized UTF-8, whichever is reached first. Reduce the number of items to fit; do not truncate text or silently omit records. An individual over-budget item refuses projection-size. Field limits keep valid individual draft projections below the page ceiling.
A cursor binds actor, full scope digest, conversation, private-state purpose, source epoch/revision, expiry, snapshot ID and offset. Only its opaque ID crosses the client boundary. Expiry equality is expired. Unknown/foreign/stale cursors refuse; never follow an alternate file or substitute a newer snapshot. Validate snapshot record ownership again before projection. Expiry affects the cursor, not the retained draft or upload. Cursor retention needs bounded storage/quota and cleanup in the eventual implementation; none is provided by this model.
Here sourceRevision means replacement of the private store/source identity, not every ordinary item edit. Per-item revisions track mutations. An existing snapshot retains its old content while mutable source items advance. Current authorization and confirmation actionability are rechecked at read time and can become stricter than the snapshot; they never inherit an old grant's authority.
After browser/server restart, reconstruct drafts, queue/receipts and confirmations from the durable private index. Preserve delivery-unknown; absent native evidence never becomes unsent. The JSON round-trip fixture tests reference-state readback, not disk durability, transactions or authenticated reconnection. Full persistence, source-epoch management, grant revocation and race injection remain runtime gates.
Attachment access and preview
Resolve only an owned upload in the approved recipient scope. Require exact revision and a completed staged/delivered/sent record. Refuse receiving, rejected, discarded, foreign or missing uploads. Check stored bytes against the complete length and SHA-256 before returning any range. A posted filename, size, media type or blob reference is not evidence. Missing/corrupt bytes refuse integrity.
Ranges are explicit offset/length, at most 256 KiB, wholly within a file of at most 20 MiB. This contract refuses overrun instead of silently clamping. Responses give actual offset/length/total, full digest, range digest and canonical base64 bytes. They specify attachment disposition and application/octet-stream. Transport must use authenticated JSON/binary responses with nosniff; never reflect an uploaded media label into active HTML or an executable navigation response. Complete byte assembly must match the advertised file digest before being called a download.
Text preview accepts only a complete file of at most 256 KiB that decodes as strict UTF-8. It escapes non-display controls, escape characters and directional controls into visible Unicode escape notation. HTML/Markdown remains literal text. The response explicitly requires textContent-only rendering. It does not authorize innerHTML, Markdown execution, terminal escapes, SVG documents or embedded scripts. The original bytes/digest remain unchanged; preview is a separate display value. Invalid UTF-8 or over-budget content refuses preview without truncating it. Other formats remain downloadable; image/document rendering needs the separately reviewed CHAT-05 decoder/renderer behavior. The reference checks do not prove browser safety.
Receiving upload restoration
Upload projections retain authoritative receivedBytes and revision even while receiving; ranges/preview still refuse incomplete uploads. After a lost append acknowledgment or restart, obtain a FRESH private-state snapshot, not an old cursor's progress, before constructing another append. Reselect the original local file explicitly and match full byte count and digest, never filename alone. Do not persist or guess a browser-local file path. The reference resumeOffset check gives the restored revision/offset only after that match. No file, wrong bytes or invalid progress means no append; preserve the staging record and let the actor explicitly select matching bytes or discard/start a new upload through the base lifecycle.
Construct the existing CHAT-01 append command using that revision and committed offset. Its CAS must reject progress that advanced again after readback. If all bytes are received but state is still receiving, use explicit completion rather than append duplicates; full server digest verification still gates staged state. The lost-ack fixture starts with a GIVEN authoritative committed append result, then tests readback and local-file matching. It does not prove the append producer, disk durability, native delivery or a live browser's file picker.
No extraction, execution, native delivery, workspace staging or remote forwarding occurs from readback or preview. These operations expose only already authorized private content to its actor, not to the agent engine before Send.
Recovering refused input without replay
This closes the private recovery path tracked as R3-2 in #1507 comment 26128. Eligibility is deliberately narrow: owned item, state dispatch-refused, exact item revision and payload digest, and null nativeRequest. Dispatched, acknowledged, working, delivery-unknown, finished and recovery-failed items are refused. The trusted queue journal must establish that dispatch-refused means no native attempt; this model assumes that upstream fact and does not reconstruct it from a timeout.
Verify the retained CHAT-01 frozen payload with its existing canonical digest, including complete upload revisions/digests and original branch. Verify current owned upload revisions, recipient scopes and actual retained bytes too. A missing, changed or corrupt payload/upload leaves the queue and original data intact with an integrity refusal. Never fabricate a draft from partial metadata or returned native text. The fixture copies a synthetic frozen example from the published baseline and computes the same sorted-key, UTF-8 digest excluding its digest field.
One idealized transaction creates a NEW private unsent draft, preserves actor, branch, exact text and upload references, records recoveredFrom, advances the queue revision and marks recovered-as-draft. Original immutable payloads remain intact. No native outbox entry is emitted. The draft still needs explicit Send and normal current controller/branch/grant checks under CHAT-01.
Recovery dedup is scoped to authenticated actor, conversation and client request ID, encoded as a canonical JSON tuple, with canonical command fingerprint. Delimiter concatenation is forbidden: opaque IDs may themselves contain colons. The shared ledger fixture tests colliding colon spellings in two independently authorized conversations, legitimate distinct recoveries and conflicting reuse. Reconnect/restart retry returns the original result without creating another draft. Conflicting reuse refuses. A fresh request ID cannot recover an already recovered item. The model persists the idealized receipt in its synthetic state; actual multi-record crash atomicity is not proved.
R3-1, reconciliation of dispatched-but-unconsumed native input after fencing, belongs to CHAT-02 adapter evidence. It is NOT made recoverable by this companion.
Evidence and completion gates
Run without installs or network:
node --check docs/plans/chat-01c/check.mjs
node docs/plans/chat-01c/check.mjs
node docs/plans/chat-01/check.mjs
node docs/plans/chat-00/check.mjs
The checker uses Node and installed Python jsonschema. Missing validation fails. R2 validates eleven closed request/response examples and 187 omission/extra-field refusals, plus the reference scenarios. New regressions cover terminal/stop changes between confirmation pages and after restart, colon-ID cross-conversation dedup, two immutable queue inputs with distinct branches and receipt/draft joins despite mutable draft edits, and receiving-offset restoration after a lost append reply. It checks closed request/response examples, required-field/extra-field refusals, private filtering, cursor actor/scope/purpose/source/expiry, immutable pagination, synthetic restart, confirmation restoration, byte ranges/digests, literal preview, corrupt upload/payload refusal, immutable recovery, repeated request identity and no native replay. It does not implement HTTP, real authentication, storage quotas, durable transactions, every record mutation, native adapters or UI rendering.
Independent exact-candidate approval by Filbert and Dewey is required before Q21 scoped publication. Existing B1-B6, CHAT-02/03I/03D, Q22, live-cutover and all-seat acceptance gates remain. #1507 stays open. No new queue item starts because this draft exists.