Context
PR #15481 (the seat-config generator, #15392 — APPROVED, at the human gate) emits a wake-envelope boot hook, making it the SECOND writer of wake-envelope.json: the merged #15394 plugin (ai/services/fleet/opencodeWakeEnvelopePlugin.mjs) already writes the identical envelope shape to the same path. @neo-opus-grace's review (PR #15481 review) named the resulting unreconciled state as a fast-follow, with the author accepting: the plugin disposition, the stale daemon pointer, the unledgered contract — plus two verified generator hardening nits, folded in here.
The Problem
Four reconciliations, all verified:
- Two writers, one file, no stated disposition. The plugin is event-driven (
session.created, with a load-bearing guard ignoring child/subagent sessions so a subagent cannot retarget the seat's wake route); the boot hook is one-shot with supervisor-supplied args. If both are ever planted they race last-writer-wins on one file. Undecided: does the hook RETIRE the plugin, or coexist — and if coexist, what precedence, and is the subagent-retarget guard preserved on the boot path?
- The daemon's pointer is stale.
ai/daemons/wake/daemon.mjs:1028 names opencodeWakeEnvelopePlugin.mjs as "the writer." The hook exists precisely because the plugin route is unreliable on desktop (background dependency install can fail — @opencode-ai/plugin@local unresolvable — leaving planted plugins unloaded; creds rotate per boot; restored sessions never fire session.created).
- The envelope shape is a two-writer contract living only in prose (two JSDoc blocks + the daemon route). No single canonical contract node — drift risk for
ask_knowledge_base ingestion (Grace's [KB_GAP]).
- Two verified generator defects (Grace's nits, author-verified): (a) the island guard mis-rejects a trailing-slash
canonicalRoot — path.posix.normalize('/x/') keeps the slash, so root + '/' becomes /x// and valid scripts fail the startsWith; (b) the permission allow-list derives the seat home as dirname(memoryDir) — a latent coupling, wrong for any caller whose memory dir is not directly under the seat home.
The Architectural Reality
- The consumer:
deliverViaOpencodeServer (ai/daemons/wake/daemon.mjs:1042+) re-reads the envelope on EVERY delivery — the wire contract is {hostname, port, sessionId, username, password, projectId, updatedAt} at a mode-0600 JSON path; loopback-only, typed validation, 204-expected prompt_async POST.
- The producers: the plugin (desktop/CLI event-driven) and the emitted hook (boot-boundary, creds from
OPENCODE_SERVER_USERNAME/PASSWORD env — provisioned by deriveHarnessLaunchSpec's serverPassword seam, never argv).
- The generator:
ai/services/fleet/generateOpenCodeSeatConfig.mjs — the island guard + allowedPaths derivation are the two defect sites.
- The natural owner: #15391's launch-path lane — the hook goes live when the launch path wires it.
The Fix
- Disposition the plugin, recorded: recommendation — the boot hook is the reliable writer on desktop; the plugin stays the TUI/CLI path where it loads. Coexistence is safe ONLY because both write the same contract for the same session (last-writer-wins is a no-op); the boot path's anti-retarget guarantee is structural (the supervisor, never a child session, runs the hook) and must be stated as such. Decision is the implementer's to falsify, not to inherit.
- Daemon pointer fix:
daemon.mjs:1028 names both writers with precedence (hook first, plugin fallback).
- Ledger the envelope shape: one canonical contract node (a Contract Ledger row on the owning ticket or a dedicated daemon-JSDoc section) naming fields, path, mode, producers, consumer — so the two producers cannot drift silently.
- Trailing-slash fix:
root.replace(/\/$/, '') in the island guard + a counter-example witness (canonicalRoot: '/canonical/' must NOT throw).
- Seat-home loudness: explicit
seatHome param (preferred) or a documented memoryDir precondition, with spec coverage of the coupling.
Contract Ledger Matrix
| Target Surface |
Source of Authority |
Proposed Behavior |
Fallback |
Docs |
Evidence |
wake-envelope.json wire shape |
ai/daemons/wake/daemon.mjs deliverViaOpencodeServer validation (:1053-1087) |
fields {hostname(loopback), port(1..65535), sessionId, username, password, projectId, updatedAt}; mode 0600; producers: boot hook (primary) + plugin (fallback) |
a malformed/hostile envelope throws before any fetch (live on dev) |
daemon route JSDoc + the ledger row this ticket adds |
two live deliveries verified 2026-07-18 (manual heal + post-restart re-heal, both 204) |
generateOpenCodeSeatConfig island guard |
ai/services/fleet/generateOpenCodeSeatConfig.mjs |
trailing-slash roots accepted; escape attempts still throw |
fail-closed on any escape |
module JSDoc |
counter-example spec (this ticket) |
generateOpenCodeSeatConfig seat home |
same |
explicit seatHome param or documented precondition |
n/a |
module JSDoc |
spec coverage (this ticket) |
Acceptance Criteria
Out of Scope
- The launch-path wiring of the hook (#15391's lane — this ticket only reconciles the contract it will consume).
- OpenCode's plugin-lifecycle upstream behavior (external; the desktop dependency-install failure is recorded as context, not fixed here).
Avoided Traps
- "Just delete the plugin" — rejected: it remains the working path where it loads (TUI/CLI sessions), its child-session retarget guard is a designed security behavior, and reconciliation beats replacement while the desktop load failure is upstream's.
- "Let last-writer-wins stay implicit" — the race is only safe because the contract is singular; left implicit, a third writer (or a plugin shape change) drifts silently.
Related
- PR #15481 + @neo-opus-grace's review (the disposition challenge + both nits, author-accepted) · #15392 (the generator's parent) · #15394 / PR #15438 (the plugin + daemon route) · #15391 (the launch path that wires the hook) · #15415 (the seat's routing-discipline companion).
Live latest-open sweep: checked latest 20 open issues at 2026-07-18T16:55Z; no equivalent (nearest: #15428/#15431 — my own mailbox/activation lanes, disjoint). A2A in-flight sweep (last 30, all read-states): no competing claim on this scope.
Origin Session ID: 9b748a56-8b84-43bf-a542-ee8dcf437ebf
Retrieval Hint: query_raw_memories("wake envelope two writers plugin boot hook disposition daemon pointer trailing-slash seatHome")
Context
PR #15481 (the seat-config generator, #15392 — APPROVED, at the human gate) emits a wake-envelope boot hook, making it the SECOND writer of
wake-envelope.json: the merged #15394 plugin (ai/services/fleet/opencodeWakeEnvelopePlugin.mjs) already writes the identical envelope shape to the same path. @neo-opus-grace's review (PR #15481 review) named the resulting unreconciled state as a fast-follow, with the author accepting: the plugin disposition, the stale daemon pointer, the unledgered contract — plus two verified generator hardening nits, folded in here.The Problem
Four reconciliations, all verified:
session.created, with a load-bearing guard ignoring child/subagent sessions so a subagent cannot retarget the seat's wake route); the boot hook is one-shot with supervisor-supplied args. If both are ever planted they race last-writer-wins on one file. Undecided: does the hook RETIRE the plugin, or coexist — and if coexist, what precedence, and is the subagent-retarget guard preserved on the boot path?ai/daemons/wake/daemon.mjs:1028namesopencodeWakeEnvelopePlugin.mjsas "the writer." The hook exists precisely because the plugin route is unreliable on desktop (background dependency install can fail —@opencode-ai/plugin@localunresolvable — leaving planted plugins unloaded; creds rotate per boot; restored sessions never firesession.created).ask_knowledge_baseingestion (Grace's[KB_GAP]).canonicalRoot—path.posix.normalize('/x/')keeps the slash, soroot + '/'becomes/x//and valid scripts fail thestartsWith; (b) the permission allow-list derives the seat home asdirname(memoryDir)— a latent coupling, wrong for any caller whose memory dir is not directly under the seat home.The Architectural Reality
deliverViaOpencodeServer(ai/daemons/wake/daemon.mjs:1042+) re-reads the envelope on EVERY delivery — the wire contract is{hostname, port, sessionId, username, password, projectId, updatedAt}at a mode-0600 JSON path; loopback-only, typed validation, 204-expectedprompt_asyncPOST.OPENCODE_SERVER_USERNAME/PASSWORDenv — provisioned byderiveHarnessLaunchSpec'sserverPasswordseam, never argv).ai/services/fleet/generateOpenCodeSeatConfig.mjs— the island guard +allowedPathsderivation are the two defect sites.The Fix
daemon.mjs:1028names both writers with precedence (hook first, plugin fallback).root.replace(/\/$/, '')in the island guard + a counter-example witness (canonicalRoot: '/canonical/'must NOT throw).seatHomeparam (preferred) or a documentedmemoryDirprecondition, with spec coverage of the coupling.Contract Ledger Matrix
wake-envelope.jsonwire shapeai/daemons/wake/daemon.mjsdeliverViaOpencodeServervalidation (:1053-1087){hostname(loopback), port(1..65535), sessionId, username, password, projectId, updatedAt}; mode 0600; producers: boot hook (primary) + plugin (fallback)generateOpenCodeSeatConfigisland guardai/services/fleet/generateOpenCodeSeatConfig.mjsgenerateOpenCodeSeatConfigseat homeseatHomeparam or documented preconditionAcceptance Criteria
daemon.mjs:1028pointer names both writers with precedence.canonicalRootaccepted + counter-example witness green.generateOpenCodeSeatConfig.spec.mjs+ wake-dir suites untouched-green).Out of Scope
Avoided Traps
Related
Live latest-open sweep: checked latest 20 open issues at 2026-07-18T16:55Z; no equivalent (nearest: #15428/#15431 — my own mailbox/activation lanes, disjoint). A2A in-flight sweep (last 30, all read-states): no competing claim on this scope.
Origin Session ID: 9b748a56-8b84-43bf-a542-ee8dcf437ebf
Retrieval Hint:
query_raw_memories("wake envelope two writers plugin boot hook disposition daemon pointer trailing-slash seatHome")