Context
During #19671's run, @neo-gpt's fresh page called neuralLink.connectToApp('Neo.examples.grid.bigData') and bound his older same-name session c763996a, which had columnMenu: true. The test's setProperties wrote to that older app. His trace: the identity step "returned a handle immediately and jsonValue null during bootstrap, then appName selected oldest." Captured as a defect-note on 2026-10-13; promoted here by triage: every Brain-gated diagnosis reads through this fixture, the tier now blocks every Engine PR (#19705), and its retry bridge ends on 2026-10-25.
The Problem
connectToApp asks the page for its own App Worker id with page.waitForFunction(async () => …). Playwright treats the async predicate's Promise as truthy, so the wait resolves on its first evaluation and never polls. A probe on the pinned Playwright 1.63.0, with a page that sets the value after 1500 ms:
| Predicate |
Result |
async () => window.id ?? null |
null after 9 ms |
() => window.id ?? null |
"ready" after 1492 ms |
During bootstrap the remote is not registered yet, so the first read is null. The fixture then binds by appName (all 139 connectToApp calls pass one). ConnectionService.waitForSession returns the first sessionData entry with that appName, which is the oldest.
Observed: locally, with another same-name app open, the test drives the other app. Inference, not yet observed in CI: a same-name session still registered from an earlier test would bind the same way. FiveBeat scene 2 (morph) needed the retry in both #19705's run and the post-merge dev run 38403855169. Its first-attempt error was "no worker error reached the page console, and the page threw nothing", which is what a test driving another page's worker would see.
Two specs already work around it locally: WorkstationTopologyGroupsNL.spec.mjs:99 (readWorkerId, expect.poll + page.evaluate), and #19711 adds another.
The Architectural Reality
test/playwright/fixtures.mjs:287-330 connectToApp: the async waitForFunction at :301-309, and the appName fallback at :319-326.
- Brain
ai/services/neural-link/ConnectionService.mjs:1035 waitForSession: id === target || appName match, first in Map insertion order.
- Design authority: the fixture's own comments. "The identity path is what makes same-name topologies safe", and "Poll with a bounded wait instead; only genuinely old builds (no getWorkerId remote) fall through to appName." The code contradicts both. It was introduced by #12897 (c115c846c4).
- The argument fallback has two faces. On an open page without Neo it never ran: its "2 s" Neo wait has passed
{timeout: 2000} as the predicate's arg since #8851, so it waits until the test timeout. On a closed page the read throws at once and the fallback runs. WorkstationTopologyGroupsNL :290 and :333 rely on that: they close the fixture's context and pass an exact worker id read from a page in a fresh context.
The Fix
Engine-only, in connectToApp:
- Read the id with a Node-side poll of
page.evaluate through expect.poll, the shape readWorkerId already proves, bounded and returning as soon as the remote answers.
- An exact App Worker id passed as the argument binds as given. A name goes through the page's own id and is never bound by name; when no id arrives within the bound,
connectToApp throws an error naming it. The unbounded Neo wait is deleted.
Decision Record impact: none.
Contract Ledger
Every row is in test/playwright/fixtures.mjs at #19715's head b0a0edc2fc. There is no Brain change.
| Target surface |
Source of authority |
Proposed behavior |
Fallback |
Docs |
Evidence |
neuralLink.connectToApp(target), target a name (139 of 139 call sites pass one) |
this ticket; the method's own JSDoc, "the identity path is what makes same-name topologies safe" |
binds the page's own App Worker id, read by readWorkerId, through NeuralLink_ConnectionService.waitForSession(id, 30000)'s exact-id match; never binds by name |
no id within identityTimeout: throws neuralLink.connectToApp('<target>'): the page's App Worker reported no id within <n> ms… |
JSDoc |
Fixture.spec.mjs: the competitor test (the older session never binds) and the refusal test |
connectToApp(target), target an App Worker id (UUID, matched by WORKER_ID) |
this ticket; it keeps the exact-id behavior that WorkstationTopologyGroupsNL :183 and :583 rely on |
binds that id as given, without reading the page, so it works after the fixture's page has closed |
an id that never connects: waitForSession rejects after 30 s (unchanged) |
JSDoc |
Fixture.spec.mjs closed-page arm; TopologyGroups :290 and :333 |
connectToApp(), target omitted |
this ticket |
same as a name: the page's own id (WORKER_ID.test(undefined) is false) |
no id: throws, naming undefined. The old appPath inference is gone; no call site omits the target |
JSDoc |
the name-path tests |
options.identityTimeout (new) |
this ticket |
ms bound for the id poll; default 30000, the bound waitForSession already uses |
— |
JSDoc |
the refusal test passes 1000 |
readWorkerId(page, timeout), exported (new) |
this ticket; it lifts WorkstationTopologyGroupsNL.spec.mjs:99's proven helper |
polls Neo.worker.App.getWorkerId() through expect.poll over page.evaluate until a non-empty id, unwrapping a SharedWorker {data} envelope |
resolves null on timeout or a closed page; never throws |
JSDoc |
Fixture.spec.mjs reads both pages with it |
Brain ConnectionService.waitForSession |
— |
unchanged |
— |
— |
the diff touches only test/playwright/ |
Acceptance Criteria
Out of Scope
- Making Brain
waitForSession refuse an ambiguous appName. That is a Brain contract change, for a defect the Engine fixture can close by itself.
- Deleting the spec-local
readWorkerId copies. Their owners can drop them after this lands.
- The other quarantined intermittents.
Avoided Traps
- A longer
waitForFunction timeout: the predicate never polls, so the timeout is never reached.
- A sync predicate that caches the remote's Promise on
window: it writes test state into the app's global.
- Keeping a name fallback when the page reports no id: it keeps the oldest-session guess alive. An exact id needs no fallback: it is bound as given.
Related
#19655 · #19657 · #19705 · #12897 · #14859 · #19671 · #19711 · #17844 · #8851
Live latest-open sweep: the latest 20 open issues at 2026-10-13T14:02:50Z, plus gh issue list --search connectToApp, found no equivalent. A2A claim sweep at 14:03Z: no claim on this scope. Memory Core sweep: #12897's identity-bind design (June) and #14859's envelope fix (July). Neither recorded that the poll does not poll. Own open assignments: #14800 and #15000, which do not overlap.
Origin Session ID: 747f2b2a-718e-4049-adc4-c31c72be4160
Retrieval Hint: "connectToApp identity wait async waitForFunction never polls same-name oldest session"
Context
During #19671's run, @neo-gpt's fresh page called
neuralLink.connectToApp('Neo.examples.grid.bigData')and bound his older same-name sessionc763996a, which hadcolumnMenu: true. The test'ssetPropertieswrote to that older app. His trace: the identity step "returned a handle immediately and jsonValue null during bootstrap, then appName selected oldest." Captured as a defect-note on 2026-10-13; promoted here by triage: every Brain-gated diagnosis reads through this fixture, the tier now blocks every Engine PR (#19705), and its retry bridge ends on 2026-10-25.The Problem
connectToAppasks the page for its own App Worker id withpage.waitForFunction(async () => …). Playwright treats the async predicate's Promise as truthy, so the wait resolves on its first evaluation and never polls. A probe on the pinned Playwright 1.63.0, with a page that sets the value after 1500 ms:async () => window.id ?? nullnullafter 9 ms() => window.id ?? null"ready"after 1492 msDuring bootstrap the remote is not registered yet, so the first read is
null. The fixture then binds by appName (all 139connectToAppcalls pass one).ConnectionService.waitForSessionreturns the firstsessionDataentry with that appName, which is the oldest.Observed: locally, with another same-name app open, the test drives the other app. Inference, not yet observed in CI: a same-name session still registered from an earlier test would bind the same way. FiveBeat scene 2 (morph) needed the retry in both #19705's run and the post-merge
devrun 38403855169. Its first-attempt error was "no worker error reached the page console, and the page threw nothing", which is what a test driving another page's worker would see.Two specs already work around it locally:
WorkstationTopologyGroupsNL.spec.mjs:99(readWorkerId,expect.poll+page.evaluate), and #19711 adds another.The Architectural Reality
test/playwright/fixtures.mjs:287-330connectToApp: the asyncwaitForFunctionat :301-309, and the appName fallback at :319-326.ai/services/neural-link/ConnectionService.mjs:1035waitForSession:id === target || appNamematch, first in Map insertion order.{timeout: 2000}as the predicate'sargsince #8851, so it waits until the test timeout. On a closed page the read throws at once and the fallback runs.WorkstationTopologyGroupsNL:290 and :333 rely on that: they close the fixture's context and pass an exact worker id read from a page in a fresh context.The Fix
Engine-only, in
connectToApp:page.evaluatethroughexpect.poll, the shapereadWorkerIdalready proves, bounded and returning as soon as the remote answers.connectToAppthrows an error naming it. The unbounded Neo wait is deleted.Decision Record impact: none.
Contract Ledger
Every row is in
test/playwright/fixtures.mjsat #19715's headb0a0edc2fc. There is no Brain change.neuralLink.connectToApp(target),targeta name (139 of 139 call sites pass one)readWorkerId, throughNeuralLink_ConnectionService.waitForSession(id, 30000)'s exact-id match; never binds by nameidentityTimeout: throwsneuralLink.connectToApp('<target>'): the page's App Worker reported no id within <n> ms…Fixture.spec.mjs: the competitor test (the older session never binds) and the refusal testconnectToApp(target),targetan App Worker id (UUID, matched byWORKER_ID)WorkstationTopologyGroupsNL:183 and :583 rely onwaitForSessionrejects after 30 s (unchanged)Fixture.spec.mjsclosed-page arm; TopologyGroups :290 and :333connectToApp(),targetomittedWORKER_ID.test(undefined)is false)undefined. The oldappPathinference is gone; no call site omits the targetoptions.identityTimeout(new)waitForSessionalready usesreadWorkerId(page, timeout), exported (new)WorkstationTopologyGroupsNL.spec.mjs:99's proven helperNeo.worker.App.getWorkerId()throughexpect.polloverpage.evaluateuntil a non-empty id, unwrapping a SharedWorker{data}envelopenullon timeout or a closed page; never throwsFixture.spec.mjsreads both pages with itConnectionService.waitForSessiontest/playwright/Acceptance Criteria
test/playwright/e2e/neural-link/Fixture.spec.mjscase connects an older same-name page first, then callsconnectToAppon a freshly navigated page and asserts that the bound session is that page's own worker id. It is red on the merge-base and green on the head.identityTimeoutmakesconnectToAppthrow a message naming the appName; nothing binds by appName.devruns' retry lists show whether FiveBeat scene 2 still needs the retry. The answer goes on #19655.Out of Scope
waitForSessionrefuse an ambiguous appName. That is a Brain contract change, for a defect the Engine fixture can close by itself.readWorkerIdcopies. Their owners can drop them after this lands.Avoided Traps
waitForFunctiontimeout: the predicate never polls, so the timeout is never reached.window: it writes test state into the app's global.Related
#19655 · #19657 · #19705 · #12897 · #14859 · #19671 · #19711 · #17844 · #8851
Live latest-open sweep: the latest 20 open issues at 2026-10-13T14:02:50Z, plus
gh issue list --search connectToApp, found no equivalent. A2A claim sweep at 14:03Z: no claim on this scope. Memory Core sweep: #12897's identity-bind design (June) and #14859's envelope fix (July). Neither recorded that the poll does not poll. Own open assignments: #14800 and #15000, which do not overlap.Origin Session ID: 747f2b2a-718e-4049-adc4-c31c72be4160 Retrieval Hint: "connectToApp identity wait async waitForFunction never polls same-name oldest session"