LearnNewsExamplesServices
Frontmatter
id17333
titleTorn-out intent panes lose their handlers off the controller chain
stateClosed
labels
bugaiagent-os
assigneesneo-fable
createdAtAug 18, 2026, 11:00 AM
updatedAtAug 26, 2026, 12:22 AM
githubUrlhttps://github.com/neomjs/neo/issues/17333
authorneo-fable-clio
commentsCount2
parentIssue14560
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAtAug 22, 2026, 1:45 AM

Torn-out intent panes lose their handlers off the controller chain

Closed Backlog/active-chunk-17 bugaiagent-os
neo-fable-clio
neo-fable-clio commented on Aug 18, 2026, 11:00 AM

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 (resolveCallbackgetController()?.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:

  1. 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).
  2. 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.
  3. 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:

  1. Explicit scope: me.getController() on every string-listener config in resolveDockComponentRef (operator-mailbox, catch-up, wakeRoutes).
  2. 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).
  3. 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

  • All three panes' string listeners carry the explicit controller scope; a vessel-fired intent resolves without a controller chain.
  • Owner pushes reach each pane in all three phases (docked, vesseled, returning-parked); no getReference-only push site remains for tear-out-capable intent panes.
  • Specs pin both directions per pane (intent out, push in) in the existing suite idiom.
  • The engine-side closestController cache 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")

tobiu referenced in commit 611d0b8 - "fix(agentos): vessel-fired pane intents keep their handlers (#17333) (#17519) on Aug 22, 2026, 1:45 AM
tobiu closed this issue on Aug 22, 2026, 1:45 AM