Skip to content

feat: narrow core boundaries, durable deliverables, and worker-state correctness#34

Merged
roodriigoooo merged 4 commits into
mainfrom
core-issues
Jul 25, 2026
Merged

feat: narrow core boundaries, durable deliverables, and worker-state correctness#34
roodriigoooo merged 4 commits into
mainfrom
core-issues

Conversation

@roodriigoooo

Copy link
Copy Markdown
Owner

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.

  • Worker sources are accepted only when the referenced current Worker Deliverable holds an exact terminal approval. Approval is never widened from one generation to another.
  • Parent authoring is interactive and gets synthetic human-authorship approval.
  • Writes serialize through a per-deliverable lock and an atomic no-replace version claim: a corrupt or already-claimed version is never overwritten, and repeated saves of the same worker generation are idempotent.
  • /docket load mounts a record under a d slot at zero model-context cost. Use → Parent queues the exact body for the next human submission; Use → Worker starts a fresh confirmed worker with byte-exact input.
  • Directory scans treat their contents as untrusted; malformed or unrelated files are skipped, not crashed on.

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:

  • Verdict cards could resolve in the wrong family. A needs_input card offers answers to a live question; every other card offers judgment on finished work — and the two reuse verb ids for opposite actions. A plan-gated worker that answers its own docket_wait and publishes crosses that line on its own. Answering it then delivered the reply to a finished worker, demoting ready → active → idle, and accept would have recorded an approval of a Deliverable the human never reviewed. Cards now detect the family change, send nothing, and reopen the current state.

Implements the core boundaries described in issues #28 and #29.
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.
@roodriigoooo
roodriigoooo merged commit 4b0a0b0 into main Jul 25, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Keep tmux deep and invisible; move operator features out

1 participant