feat: narrow core boundaries, durable deliverables, and worker-state correctness#34
Merged
Conversation
A stored record whose schemaVersion did not equal the current one failed structural validation, returned undefined, and was filtered out of list(), read() and find() with no warning — while still holding its version slot, so it was both unreachable and unreplaceable. A schema bump would have orphaned every existing record. Reads now branch on schemaVersion before structural validation: older versions run through a registered upgrade chain (empty today, one entry per future version), and a record this build cannot load is reported as a non-loadable entry via DeliverableStore.listUnsupported() and shown by /docket list as `deliverable:unreadable` with reason and path. save() refuses such a slot with an explicit message rather than the generic corrupt-file one. Also from review: - save() throws `Invalid stored deliverable: <failing invariants>`; storedDeliverableIssues() collects failing predicate names instead of one bare boolean over ~40 conjoined checks. - split the single-line worker-approval invariant into validWorkerApproval, validHumanApproval, and approvalMatchesSource; source rules now switch on the discriminant instead of repeating `source?.kind !== "worker" || …`. - drop the unused workerDeliverableSafeId and deliverableRef aliases. - narrow save()'s try to the existence check so error routing no longer depends on plain Errors lacking a .code.
e7d2b8a added docs/manual-demo.md to .gitignore without `git rm --cached`, so it stayed tracked and kept showing as modified. File preserved on disk. Also ignore /.pi/ (local Pi runtime state) and the branch review notes.
Three defects surfaced by manual visual testing. Arrow keys were dead in every docket-keymap overlay. normalizeDocketKey lowercased its input before looking up the alias table, so raw escape sequences keyed with an uppercase final byte could never match. Sequences now resolve in a case-sensitive table (CSI plus SS3/application-cursor forms), and longer input falls through to pi-tui parseKey so Kitty and modifyOtherKeys encodings decode too. A worker that finished while its verdict card was open read as idle/stale to the parent: - A needs_input card offers answers; every other card offers judgment on finished work. The families reuse verb ids for opposite actions, so answering a worker that had already published sent the reply anyway, demoting ready to active, and would have recorded accept as an unreviewed Deliverable approval. Cards now refuse to resolve in the other family and reopen the current state. - Staleness measured progress, not liveness: heartbeats skip the write when nothing changed, so any worker unchanged for 90s derived stale, including one thinking through a long turn. Statuses carry heartbeatAt, and a heartbeat-only beat persists without advancing updatedAt. - The end-of-turn protocol nudge called sendUserMessage without deliverAs, throwing "Agent is already processing" when a parent message landed at the same moment. Its async failure escaped the try/catch and the recovery it would have triggered was lost. /docket kinds is grouped by source with one block per kind: name and authority as the heading, description, decision rights, and migration warnings hanging under it.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #28, #29
Durable deliverable store
/docket save now writes an immutable StoredDeliverable at ~/.pi/agent/docket/deliverables//v.json instead of assembling a mutable bundle. A record preserves exact body bytes, structured outcome/evidence/recommendations, artifact refs, an optional frozen change set, ordered generation-bound review notes, approval, and inherited handoff provenance.
The bundle path stays readable for existing list/load/preview/unload/delete. New saves never write checkpoint files, the legacy event log, or the legacy index. Checkpoint lifecycle, selector, and summarizer modules are deleted.
Schema drift is survivable
A stored record whose schemaVersion did not match failed structural validation, returned undefined, and was filtered out of list()/read()/find() silently — while still holding its version slot. It was simultaneously unreachable and unreplaceable, so a future schema bump would have orphaned every existing record.
Reads now branch on schemaVersion before structural validation. Older versions run a registered upgrade chain (empty today, one entry per future version). A record this build cannot load is reported through listUnsupported() and shown by /docket list as deliverable:unreadable with its reason and path. save() refuses that slot with an explicit message rather than the generic corrupt-file error, and validation failures name the exact failing invariants instead of one bare Invalid stored deliverable.
tmux behind a narrow core boundary
Core creates one ordinary window per worker in one shared session and persists both the stable window ID and the pane ID. Tell, multiline paste, peek, dead-pane probing, and harvesting target the recorded pane first; window/name fallback survives only for legacy statuses. Companion-created panes can no longer redirect input or evidence capture.
Removed from core: split-event layouts, status-right rendering, pipe-pane terminal capture, pane.log, and their configuration. Retained: shared windows, literal/paste input, peek, durable dead-pane tails, attach/return.
One exclusive globalThis.__docket.registerTmuxAdapter(adapter) seam lets a companion own operator layouts. Docket dispatches it after IDs are persisted and does not await it — adapter exceptions are warned and isolated, and a slow or stalled adapter cannot delay, roll back, or fail a worker launch.
Fixes from visual testing
Arrow keys were dead in every Docket overlay. normalizeDocketKey lowercased its input before the alias lookup, so a raw sequence keyed with an uppercase final byte could never match — leaving only the j/k aliases the footer advertises next to them. Sequences now resolve case-sensitively (CSI plus SS3/application-cursor forms, legacy Home/End), and longer input falls through to pi-tui parseKey, so Kitty and modifyOtherKeys encodings decode too. Esc and Enter under Kitty protocol now resolve as a side effect.
A finished worker read as idle/stale to the parent. Three independent defects stacked into one symptom: