Torn-out intent panes lose their handlers off the controller chain
Context
Found during the memories click pop-out implementation (PR for the memories pop-out ticket, filed same day). The cockpit's intent panes wire their events as STRING listeners (listeners: {<event>: 'onHandlerName'}); Neo resolves string handlers at FIRE time through the firing component's controller chain (resolveCallback → getController()?.getHandlerScope(...)). A pane reparented into a tear-out vessel window has NO controller above it — the vessel childapp is a bare render target by design.
The Problem
Two defect layers, verified in source:
- Dead intents in the vessel: a gesture-torn
OperatorMailbox (compose, inboxPageRequest), CatchUpPane (historyRequest, markCaughtUpRequest, liveSurfaceRequest) or WakeRoutePane (wakeRoutesRequest) fires into a chain with no controller — the handler never resolves, and the interaction dies (worst case a TypeError in the App Worker per fire).
- The null-cache aggravator:
component.Base#getController caches its resolution in the closestController symbol and NEVER invalidates it — not on reparent, not on unmount (src/component/Base.mjs, the closestController writes). A pane whose FIRST fire happens inside the vessel caches null permanently: its intents stay dead even after the pane returns home. Conversely, today's working cases work only because a warm cache from the docked phase survives the move — an accident, not a contract.
- Blind owner pushes: owner-side pushes resolve panes via
getReference(...), which cannot see a vesseled or returning-parked instance. The mailbox already has a phase-blind accessor (getOperatorMailboxPane — handles only); catch-up and wakeRoutes have none, and no accessor covers the returningTearOutPanes parking window.
The Architectural Reality
apps/agentos/view/fleet/FleetCockpit.mjs resolveDockComponentRef — the listener configs per pane case; the memories case now carries scope: me.getController() (the shipped fix pattern).
getMemoriesPane() — the completed accessor shape: tearOutPaneHandles → returningTearOutPanes → getReference, consumed by snapshot pushes, roster-option refreshes and the reconnect re-drive.
src/util/Function.mjs resolveCallback + src/core/Observable.mjs fire — the fire-time resolution semantics.
src/component/Base.mjs getController — the never-invalidated cache (engine-side; whether invalidate-on-parent-change is wanted is an ENGINE decision this ticket surfaces but does not prescribe).
The Fix
Apply the memories pattern to the three remaining intent panes, app-side only:
- Explicit
scope: me.getController() on every string-listener config in resolveDockComponentRef (operator-mailbox, catch-up, wakeRoutes).
- Phase-blind accessors per pane (extend
getOperatorMailboxPane with the returning-parked tier; add getCatchUpPane / getWakeRoutesPane), and route every owner push through them (snapshot writes, option refreshes, reconnectFleet re-drives).
- Spec coverage in the established idiom: vessel-fired intent reaches the controller; owner push reaches the vesseled AND returning-parked pane (the pop-out suite + owner-seam suite carry the memories precedent to lift).
Acceptance Criteria
Out of Scope
The engine cache-invalidation change itself (engine decision, separate ticket if confirmed) · the memories pane (shipped) · agent-detail (push-based by design, no string-listener intents) · pane content design.
Related
Epic #14560 (parent) · the memories pop-out ticket + its PR (the shipped pattern, filed same day) · the shell navigation-model ticket (pane placement changes do not alter this defect — it rides tear-out capability, not position).
Live latest-open sweep: latest 10 re-checked 2026-08-18T08:55Z, no equivalent; A2A herd window clean (engine-grid lane #17327/PR #17332 and an unrelated private-repo MR, no overlap).
Origin Session ID: ca3c67ac-a3d6-4e93-98e0-c5f7f65011ee
Retrieval Hint: query_raw_memories("torn out pane intent listeners controller chain scope phase blind accessor")
Torn-out intent panes lose their handlers off the controller chain
Context
Found during the memories click pop-out implementation (PR for the memories pop-out ticket, filed same day). The cockpit's intent panes wire their events as STRING listeners (
listeners: {<event>: 'onHandlerName'}); Neo resolves string handlers at FIRE time through the firing component's controller chain (resolveCallback→getController()?.getHandlerScope(...)). A pane reparented into a tear-out vessel window has NO controller above it — the vessel childapp is a bare render target by design.The Problem
Two defect layers, verified in source:
OperatorMailbox(compose,inboxPageRequest),CatchUpPane(historyRequest,markCaughtUpRequest,liveSurfaceRequest) orWakeRoutePane(wakeRoutesRequest) fires into a chain with no controller — the handler never resolves, and the interaction dies (worst case a TypeError in the App Worker per fire).component.Base#getControllercaches its resolution in theclosestControllersymbol and NEVER invalidates it — not on reparent, not on unmount (src/component/Base.mjs, theclosestControllerwrites). A pane whose FIRST fire happens inside the vessel cachesnullpermanently: its intents stay dead even after the pane returns home. Conversely, today's working cases work only because a warm cache from the docked phase survives the move — an accident, not a contract.getReference(...), which cannot see a vesseled or returning-parked instance. The mailbox already has a phase-blind accessor (getOperatorMailboxPane— handles only); catch-up and wakeRoutes have none, and no accessor covers thereturningTearOutPanesparking window.The Architectural Reality
apps/agentos/view/fleet/FleetCockpit.mjsresolveDockComponentRef— the listener configs per pane case; the memories case now carriesscope: me.getController()(the shipped fix pattern).getMemoriesPane()— the completed accessor shape:tearOutPaneHandles → returningTearOutPanes → getReference, consumed by snapshot pushes, roster-option refreshes and the reconnect re-drive.src/util/Function.mjsresolveCallback+src/core/Observable.mjsfire— the fire-time resolution semantics.src/component/Base.mjsgetController— the never-invalidated cache (engine-side; whether invalidate-on-parent-change is wanted is an ENGINE decision this ticket surfaces but does not prescribe).The Fix
Apply the memories pattern to the three remaining intent panes, app-side only:
scope: me.getController()on every string-listener config inresolveDockComponentRef(operator-mailbox, catch-up, wakeRoutes).getOperatorMailboxPanewith the returning-parked tier; addgetCatchUpPane/getWakeRoutesPane), and route every owner push through them (snapshotwrites, option refreshes,reconnectFleetre-drives).Acceptance Criteria
getReference-only push site remains for tear-out-capable intent panes.closestControllercache behavior is surfaced to the engine owners as its own narrow ticket IF implementation confirms the null-cache path is reachable in production flows (verify, then file — never silently absorbed here).Out of Scope
The engine cache-invalidation change itself (engine decision, separate ticket if confirmed) · the memories pane (shipped) · agent-detail (push-based by design, no string-listener intents) · pane content design.
Related
Epic #14560 (parent) · the memories pop-out ticket + its PR (the shipped pattern, filed same day) · the shell navigation-model ticket (pane placement changes do not alter this defect — it rides tear-out capability, not position).
Live latest-open sweep: latest 10 re-checked 2026-08-18T08:55Z, no equivalent; A2A herd window clean (engine-grid lane #17327/PR #17332 and an unrelated private-repo MR, no overlap).
Origin Session ID: ca3c67ac-a3d6-4e93-98e0-c5f7f65011ee
Retrieval Hint:
query_raw_memories("torn out pane intent listeners controller chain scope phase blind accessor")