Context
Leg 1 of #16620 (ambient gesture arming/preview failures under suite load), split per the pull-request workflow's one-PR-one-leaf rule: the parent stays open for the battery/classification legs (its AC-2/3/4). The parent's Fix step 1 is this leaf: convert the unguarded post-dwell read into a gate-named executor failure — both the robustness repair and the attribution instrument the battery needs.
Live latest-open sweep: checked latest 20 open issues at 2026-08-08 ~13:44Z; no equivalent (parent #16620 only). A2A herd-window sweep: mailbox current through 13:27Z; no competing claim.
The Problem
apps/workstation/view/Workspace.mjs executeCrossZoneShowcaseStep verifies the active candidate (waitUntil on indicators.activeCandidate?.preview?.previewId), waits dwellDelay, then reads indicators.activeCandidate.preview unguarded. A candidate lost during the dwell — the human-pause-dwell claim-TTL-expiry family pinned on #16497 (600-900ms dwells outlive the gesture claim's arbitration TTL) — turns the read into an unattributed Cannot read properties of null (reading 'preview') that kills the gesture chain mid-flight. Under suite load this face cost ~10 attribution runs on the #16499 table alone (the parent's origin evidence).
The Architectural Reality
- The executor's own
catch converts any throw into the returned {applied: false, errors: [message]} receipt — so a thrown, gate-named error IS the receipts channel; no new error path is added.
- The re-verify discriminates the two loss modes in the message:
active=null (expiry) vs active='<other-id>' (preemption), plus dwell position — the classification data leg 2 needs.
- The file's other
activeCandidate reads are all optional-chained (verified by grep at dev 5792ec4d46); this is the only unguarded read of the class in the file.
The Fix
Optional-chain the read and throw a gate-named executor error on loss: gate=dwell-reverify, expected vs live candidate id, dwell index/position. Witness: prototype-call spec (the file's established idiom) whose fake host retires the active candidate inside the dwell — red against the unpatched read (the unattributed TypeError message), green with the guard.
Acceptance Criteria
Out of Scope
- The parent's suite battery (≥5 runs), face classification, and any product-side repair of residual expiry paths — legs 2-4 live on
#16620.
- The silent-degradation note from the
#16688 review (diverged-shell fallback) — already owned by the #16500 oracle leg.
Avoided Traps
- Repairing the product behavior in this leg (e.g. re-arming the claim mid-dwell): rejected — classification first, per the parent; the guard is the instrument, not the verdict.
- A new error channel: rejected — the executor's existing catch-to-receipt contract is the channel.
Decision Record impact
none — consumes the executor's existing receipt contract; no ADR surface.
Related
Parent: #16620 (legs 2-4 live there) · Refs #16497 (claim-TTL family mechanism) · Refs #16500 (oracle instrument) · Refs #16499 (origin suite table)
Origin Session ID: f62a4ece-0cb8-43eb-9311-e684aaf9cbc5
Retrieval Hint: query_raw_memories("cross-zone dwell activeCandidate claim TTL expiry gate-named receipt")
Structure-map gate: N/A — app-view + spec surface, no ai//Agent OS/MCP/skills touch.
Context
Leg 1 of
#16620(ambient gesture arming/preview failures under suite load), split per the pull-request workflow's one-PR-one-leaf rule: the parent stays open for the battery/classification legs (its AC-2/3/4). The parent's Fix step 1 is this leaf: convert the unguarded post-dwell read into a gate-named executor failure — both the robustness repair and the attribution instrument the battery needs.Live latest-open sweep: checked latest 20 open issues at 2026-08-08 ~13:44Z; no equivalent (parent
#16620only). A2A herd-window sweep: mailbox current through 13:27Z; no competing claim.The Problem
apps/workstation/view/Workspace.mjsexecuteCrossZoneShowcaseStepverifies the active candidate (waitUntilonindicators.activeCandidate?.preview?.previewId), waitsdwellDelay, then readsindicators.activeCandidate.previewunguarded. A candidate lost during the dwell — the human-pause-dwell claim-TTL-expiry family pinned on#16497(600-900ms dwells outlive the gesture claim's arbitration TTL) — turns the read into an unattributedCannot read properties of null (reading 'preview')that kills the gesture chain mid-flight. Under suite load this face cost ~10 attribution runs on the#16499table alone (the parent's origin evidence).The Architectural Reality
catchconverts any throw into the returned{applied: false, errors: [message]}receipt — so a thrown, gate-named error IS the receipts channel; no new error path is added.active=null(expiry) vsactive='<other-id>'(preemption), plus dwell position — the classification data leg 2 needs.activeCandidatereads are all optional-chained (verified by grep at dev5792ec4d46); this is the only unguarded read of the class in the file.The Fix
Optional-chain the read and throw a gate-named executor error on loss:
gate=dwell-reverify, expected vs live candidate id, dwell index/position. Witness: prototype-call spec (the file's established idiom) whose fake host retires the active candidate inside the dwell — red against the unpatched read (the unattributed TypeError message), green with the guard.Acceptance Criteria
gate=dwell-reverifystep receipt (no unattributed null-read remains reachable from this seam).apps/workstation/Workspacespec file green.Out of Scope
#16620.#16688review (diverged-shell fallback) — already owned by the#16500oracle leg.Avoided Traps
Decision Record impact
none — consumes the executor's existing receipt contract; no ADR surface.
Related
Parent: #16620 (legs 2-4 live there) · Refs #16497 (claim-TTL family mechanism) · Refs #16500 (oracle instrument) · Refs #16499 (origin suite table)
Origin Session ID: f62a4ece-0cb8-43eb-9311-e684aaf9cbc5
Retrieval Hint:
query_raw_memories("cross-zone dwell activeCandidate claim TTL expiry gate-named receipt")Structure-map gate: N/A — app-view + spec surface, no
ai//Agent OS/MCP/skills touch.