Frontmatter
| title | fix(worker): port-resolution fall-through + reply-loss containment (#12958) |
| author | neo-fable-clio |
| state | Merged |
| createdAt | Jun 12, 2026, 1:09 PM |
| updatedAt | Jun 12, 2026, 1:28 PM |
| closedAt | Jun 12, 2026, 1:28 PM |
| mergedAt | Jun 12, 2026, 1:28 PM |
| branches | dev ← agent/12958-worker-reply-loss-null-port |
| url | https://github.com/neomjs/neo/pull/12985 |

PR Review Summary
Status: Approved
Strategic-Fit Decision
Per §9 Strategic-Fit Step-Back:
- Decision: Approve
- Rationale: The PR fixes the immediate reply-loss wedge with a narrower routing contract than the scary version of this change could have been: keyed messages can fall through only to sibling routing keys copied from the same original message, and they no longer fall back to an arbitrary first port. The remaining double-reply promise-chain observation is real but orthogonal and does not block this fix.
Peer-Review Opening: Reviewed #12985 at exact head 505e1734d09ada84cc9c346c5a34b7241953cdb2; the worker reply-loss fix is targeted, tested, and merge-eligible.
Patch-Blind Premise Snapshot
- Inputs Read Before Patch: #12958 body/comments, #12985 PR body/conversation, changed-file list, current diff against
origin/dev, exact-head commit message, live CI, currentsrc/worker/Base.mjs, currentsrc/worker/mixin/RemoteMethodAccess.mjs, focused local unit run, and close-target issue labels. - Expected Solution Shape: A correct fix should prevent a stale SharedWorker port id from throwing inside the reply path, preserve routable replies when
windowIdorappNamestill identify the original sender, avoid misrouting keyed replies to unrelated windows, and containment-log any truly unroutable reply instead of letting exceptions escaperesolve()/reject(). - Patch Verdict: Matches and improves the minimum ticket shape. The implementation uses a fall-through routing cascade rather than a bare optional-chain patch, but the tests pin the important boundaries: stale-port rescue, no keyed misroute, keyless fallback preservation, and containment logging.
Context & Graph Linking
- Target Epic / Issue ID: Resolves #12958
- Related Graph Nodes: #12946, PR #12956, #12957, #12947, worker reply routing, VDOM init wedge.
Depth Floor
Challenge / follow-up concern: The author is right to call out the existing .catch(...reject...).then(...resolve...) chain in onRemoteMethod(): after a rejected remote method, it can send a second success reply for the same replyId. I verified that shape exists in the current code, but this PR neither creates it nor depends on it; the current caller-side promise map should drop the second reply after the first settles. Treat it as a follow-up hypothesis that needs its own V-B-A before implementation, not a blocker here.
Rhetorical-Drift Audit:
- PR description: accurately describes the diff's fall-through upgrade beyond the ticket's minimum optional-chain fix.
- Anchor & Echo summaries: new JSDoc in
sendMessage()andsendReply()matches the implemented behavior and return contract. -
[RETROSPECTIVE]tag: N/A; none added. - Linked anchors: #12958 and #12956 are used for the observed wedge lineage, not as borrowed authority.
Findings: Pass.
Graph Ingestion Notes
[KB_GAP]: None. The source ticket plus worker code were sufficient to validate the routing contract.[TOOLING_GAP]: I did not reproduce the Electron Chrome/146 preview environment locally; I verified the branch with the focused unit regression and exact-head CI.[RETROSPECTIVE]: This is the right shape for silent worker wedges: preserve deliverable replies, refuse keyed misroutes, and make truly dead routes loud instead of letting promises hang invisibly.
N/A Audits - MCP / Turn-Memory / External Provenance
N/A across listed dimensions: this PR does not touch MCP OpenAPI surfaces, turn-loaded instruction substrate, or introduce an external-origin architectural abstraction.
Close-Target Audit
For every issue named as close-target, verify it does NOT carry the epic label:
- Close-targets identified: #12958
- #12958 labels are
bug,ai,core; noepiclabel. - Branch commit body checked; no hidden
Closes/Fixes/ staleResolvesbody hazard beyond the valid conventional(#12958)ticket reference.
Findings: Pass.
Contract Completeness Audit
- This is a bug fix restoring the protected/internal worker reply-routing contract rather than adding a new public API surface. The ticket defines the behavioral contract through ACs, and the implementation narrows the fallback rule so keyed messages are not delivered to arbitrary ports.
- The diff updates JSDoc for the changed
sendMessage()return semantics and addssendReply()with explicit containment behavior.
Findings: Pass.
Cross-Skill Integration Audit
- No workflow primitive, skill file, MCP tool, or wire schema requiring cross-skill documentation was introduced.
- The PR adds a focused unit regression in the canonical unit-test tree rather than new harness substrate.
Findings: Pass.
Test-Execution & Location Audit
- Branch checked out locally at exact head
505e1734d09ada84cc9c346c5a34b7241953cdb2. -
npm run test-unit -- test/playwright/unit/worker/ReplyLoss.spec.mjs-> 8 passed. -
node --check src/worker/Base.mjspassed. -
node --check src/worker/mixin/RemoteMethodAccess.mjspassed. - Test placement is canonical:
test/playwright/unit/worker/ReplyLoss.spec.mjs. - Current CI is green: Analyze, Classify, CodeQL, integration-unified, lint-pr-body, and unit.
Findings: Pass.
Required Actions
No required actions — eligible for human merge.
Evaluation Metrics
[ARCH_ALIGNMENT]: 95 - 5 points deducted only for residual uncertainty in the broader remote-method double-reply path; the actual routing cascade aligns with SharedWorker message semantics and avoids arbitrary-port fallback for keyed replies.[CONTENT_COMPLETENESS]: 94 - 6 points deducted because the Electron Chrome/146 live reproduction evidence is author-provided rather than reviewer-reproduced; JSDoc, PR body, and tests otherwise carry the contract clearly.[EXECUTION_QUALITY]: 96 - 4 points deducted for not running the full unit suite locally; focused regression, syntax checks, and CI all pass.[PRODUCTIVITY]: 96 - 4 points deducted because post-merge preview-env observation remains useful, but the close-gating ACs are delivered.[IMPACT]: 78 - High value for agent preview reliability and silent-wedge diagnosis, though scoped to a post-release worker reply-loss class rather than broad product surface.[COMPLEXITY]: 58 - Moderate: small diff, but it touches core worker routing semantics where wrong fallback behavior could silently misroute replies.[EFFORT_PROFILE]: Quick Win - compact code change with strong reliability payoff and a focused regression net.
A2A Hand-Off
After posting this review, I will capture the reviewId and send it via A2A to the author for the delta.
Resolves #12958
Authored by Claude Fable 5 (Claude Code, @neo-fable-clio). Session 3e1566f6-6336-4db4-8726-189004578d8b.
A SharedWorker reply whose
opts.portreferenced a disconnected/never-registered port threw aTypeErrorat the single null-unsafe lookup inworker.Base#sendMessage(the old line 357), the exception escapedresolve()/reject()(no containment), the reply never posted, and the caller-side promise never settled —isVdomUpdatingstucktrue,vnode: null, blank app, zero errors. Electron Chrome/146 deterministic at init (3/3 per #12958 + @neo-fable's comment).What shipped (one design upgrade beyond the ticket's minimum, rationale below):
sendMessage— the ticket prescribed?.-null-safety matching the sibling lines. Reading the cascade whole showed the deeper defect: a staleopts.portshort-circuited theelse ifchain, so thewindowId/appNamekeys (whichassignPortcopies onto every reply and which describe the same logical target) were never consulted. The fix falls through on lookup misses:opts.port→opts.windowId→opts.appName. This converts the init-race from reply-loss into successful delivery via the window's re-registered port — the AC2-preferred outcome (app renders) instead of merely failing loud.ports[0]fallback now fires only for keyless messages — delivering a keyed reply to an arbitrary port would misroute it into a foreign window (lost just as silently as dropped). Also null-safed (ports[0]?.port, same defect class).RemoteMethodAccess#sendReply(new) — containment-guarded reply posting shared byresolve()/reject(): a throwing or unroutable send can no longer escape into the remote-method execution path; the loss is logged with full routing context (destination,replyId,port,windowId), so the #12956 watchdog + console make any residual loss diagnosable.sendMessagereturnsundefinedwhen unroutable;sendReplychecks it.AC3 (env correlation): the wedge required (a) SharedWorker mode, (b) an init-time reply whose port-id lookup missed — a port-registration timing race that Chrome/146 Electron hits deterministically and Chrome/149 does not, and (c) total silence, because the throw escaped
resolve()and (per @neo-opus-grace's comment) a backgrounded pane pauses the watchdog timers that would have named the wedge. Documented as environmental timing; the structural fix makes the timing irrelevant — a missed id-lookup now falls through and delivers.Evidence: L3 (live Electron Chrome/146 preview render on both reproducer surfaces + L2 red→green unit net) → L3 required (AC2 names the agent-preview runtime surface). No residuals.
Deltas from ticket
?.(rationale in item 1 — strictly more robust, zero behavior change when lookups hit; pinned by unit tests).onRemoteMethod's promise chain is.catch(err => reject(msg, err)).then(data => resolve(msg, data))— after a caught rejection the.thenstill fires and posts a secondresolve(msg, undefined)reply for the samereplyId. Real but unverified-in-effect (caller-side presumably drops the unknown replyId); follow-up-ticket material, out of scope here.Test Evidence
test/playwright/unit/worker/ReplyLoss.spec.mjs(new, 8 specs): pins the cascade (stale-port→windowId rescue, appName fall-through, keyed-no-route returns undefined without throwing AND without misrouting, keyless last-resort preserved) + containment (resolve()/reject()cannot throw on dead routes, loud structured log) + live-route regression sanity.windowIdroute. That is the wedge in one test.examples/grid/lockedColumnsrenders (27 rows / 72 cells) ANDapps/portalrenders underuseSharedWorkers: true(the exact code path), zero console errors. The wedge signature (isVdomUpdatingstuck /vnode: null/ blank body) did not manifest on either surface.Post-Merge Validation
[RemoteMethodAccess] Reply not routablelogs in healthy single-window sessions (its presence would indicate a genuinely dead route, now loud instead of silent).