-
Notifications
You must be signed in to change notification settings - Fork 3
plat 107
| Field | Value |
|---|---|
| Status |
partially implemented — projection, self-parent collapse, event-only phantom suppression, and selection repaired 2026-08-15; runtime verification not yet performed
|
| Priority | P1 |
| Owner | execution-tree terminal projection and selection |
| Reported | 2026-08-15 |
| Related | PLAT-035), PLAT-095), PLAT-100) |
Pulse Gate, Review/Fix, and Finalizer are sequential messages sent to the same scheduled main-agent conversation. During the Build in Public run, the terminal rail instead showed the Finalizer as:
PULSE FINALIZER … · Child of PULSE FINALIZER …
and selected a blank pane. Backend evidence showed the same Schedule session was still processing the Finalizer and backup work. The workflow was not waiting for a new child terminal; the UI had invented one.
This hides real progress, creates a nonsensical self-parent relationship, and makes a healthy run look stalled.
frontend/src/utils/terminalExecutionProjection.ts synthesizes a terminal row
for each live execution-tree node that it considers a visible child. The
selection logic in frontend/src/components/TerminalCenter.tsx can then prefer
the latest active synthetic row. A sequential main turn whose execution identity
is not normalized back to the root can therefore become a child placeholder and
take focus before any real terminal exists.
PLAT-095 established the query-rooted execution tree and shared lifecycle projection. This is a distinct projection regression at that boundary, not a failure of the Schedule turn-completion contract itself.
- Normalize execution identity before projection. Nodes with
kind=main_agent, the root session owner, or the same canonical owner as the root are main turns, not child terminals. - Reject self-parent edges and collapse sequential main turns into the existing main conversation timeline.
- Create placeholder rows only for genuine child/background executions with a distinct owner and execution identity.
- Never auto-select an unpublished placeholder. Keep the current main terminal selected until a concrete child terminal is published or the user explicitly selects the placeholder.
- If the user explicitly opens a genuine placeholder, show the existing “Waiting for terminal” state; never render an unexplained blank pane.
The self-parent mechanism was confirmed exactly as described.
terminalExecutionProjection.ts resolved nodesByID.get(node.parent_execution_id)
with no guard that the parent differs from the node, then rendered
display_meta: `Child of ${parent.name}` . When parent_execution_id === execution_id the lookup returned the node itself, so title and meta both read
"PULSE FINALIZER" — reproducing the reported string precisely.
Correction (same day). The first attempt only relabelled the self-parent
row: it removed the "Child of PULSE FINALIZER" caption but still emitted a
synthetic "Asynchronous child" placeholder, and its test asserted
toHaveLength(1) — encoding the incomplete behaviour as correct and
contradicting acceptance 2 ("no Child of PULSE FINALIZER row"). A phantom rail
row still hides real progress and makes a healthy run look stalled, which is the
defect this ticket reports. A self-parent node is now discarded from the rail
entirely and the test asserts no extra entry exists.
Changes in frontend/src/utils/terminalExecutionProjection.ts:
-
isMainConversationExecution()treats a self-parent edge as proof of a misprojected sequential main turn and collapses it into the main conversation, so no rail row is produced at all; -
parentExecutionIDOf()rejects a self-parent edge and is now the single source of the parent ID, used by the placeholder, the enrichment path, and the message-sequence terminal bridge; -
isMainConversationExecution()replaces the kind-only test. Repair 1 needed to be broader than the ticket implied:main_agent,session_root, andsynthetic_turnwere already hidden andmain:IDs already filtered, so the stated fix was largely in place. The real hole wasHIDDEN_EXECUTION_KINDS.has((node.kind || '')…)— a node with an empty or unknown kind was not hidden, and the placeholder then defaulted it toexecution_kind: node.kind || 'background_agent'. An unclassified node was silently promoted to a background child. Ownership is now checked independently of kind, and an unclassified node with no distinct parent stays in the main timeline.
Change in frontend/src/utils/terminalIdentity.ts:
-
preferredTerminalForContextnow skipsexecution_tree_placeholderrows in every context. The workflow branch already refused to auto-select a child, but a Schedule session is not a workflow context, so the non-workflow fallback could still take a placeholder. This is also the blank-pane path: a selected placeholder that later disappears from the projection leaves the pane with no matching terminal to render.
Repair 5 was already satisfied — TerminalCenter renders TerminalWaitingPane
with an explanatory message for an explicitly opened placeholder.
The Sales Outreach reproduction exposed a second path to the same phantom pane.
The execution tree contained a live source="event_stream" child named after
the current command while the runtime reported no live background process and
the terminal API had no corresponding terminal. The rail nevertheless projected
it and could only show Waiting for terminal forever.
An event-stream node is not evidence of terminal ownership. Tool calls, turns, and lifecycle receipts can carry distinct execution and parent IDs while still running inside an existing terminal. The frontend projection now permits an event-stream node to enrich a terminal that has already been published, but it cannot synthesize an unpublished terminal. Temporary child rows remain owned by the background/tracked execution registries, which are the sources that can truthfully promise a concrete terminal will follow.
Regression coverage proves both halves: event-only activity produces no phantom row, while event activity still updates a matching retained terminal.
Regression coverage in terminalExecutionProjection.test.ts — a self-parent
node produces no rail row, a self-parent node alongside a genuine sibling
suppresses only itself (acceptance 6), a sequential main turn is absent, an
unclassified node invents nothing, and a real child with a distinct parent still
projects — plus terminalIdentity.test.ts (no placeholder auto-select; a real
terminal still wins). The self-parent test was verified to fail against the
previous behaviour before the guard was added. Full frontend suite passes
(481 tests).
Still required before this can be marked implemented: runtime verification of the P0 acceptance against a real Gate → Review/Fix → Finalizer Schedule run. Unit tests prove the projection contract; they do not prove the live rail renders one main-agent entry for a real sequential Pulse run.
- Run Gate → Review/Fix → Finalizer as sequential turns in one Schedule session.
- The rail contains one main-agent entry and no
Child of PULSE FINALIZERrow. - Every sequential turn appears in the main formatted transcript.
- A real background child still receives its own rail entry and terminal.
- A child placeholder cannot steal selection from the main terminal.
- Malformed self-parent input is ignored and covered by a regression test.
Auto-synced from docs/ on main. Edit there, not here.