Skip to content

fix(pipeline): route a Manager DISPATCH from its reply text (#539) - #545

Draft
nt-nerdtechnic wants to merge 1 commit into
mainfrom
fix/manager-dispatch-turn-text
Draft

nt-nerdtechnic wants to merge 1 commit into
mainfrom
fix/manager-dispatch-turn-text

Conversation

@nt-nerdtechnic

@nt-nerdtechnic nt-nerdtechnic commented Oct 20, 2026 •

Copy link
Copy Markdown
Collaborator

Refs #539. This covers suggestions 1 and 2 (the DISPATCH reaches its worker, and a DISPATCH before READY counts as READY). Suggestions 3 and 4 are not in this PR, so the issue stays open.

Problem

A stage-2 Manager (Claude) printed a DISPATCH for 畫師 and ended its turn without ---MANAGER-READY---. The issue reads this as "dropped by design", but the stage router does not hold DISPATCH for READY: managerRouterScan routes any DISPATCH it parses. What happened is that no router parsed this one. Both the stage router and the global Manager router read DISPATCH only from the Manager's screen cleanBuffer, and the run log has neither a 🎯 → 畫師 line nor a slot not found line. So parseDispatchBlocks found no block there. A Claude screen merges rows: its status bar and spinner land on the sentinel's row, which is why findSentinel tolerates them. A to:/message: pair that does not sit on separate lines parses as nothing, and nothing was logged. The router had no other source, as its own comment said ("the router has no turn-text fallback of any kind").

Meanwhile queueOrRouteWorkerMsg held the workers' REPORTs for READY, so all three panes went idle, each waiting on another, and the stage and its nodes kept showing running.

Fix

  • routeManagerTurnText is called on every turn_complete. It finds the router of the running stage whose Manager is this pane, ignores a replayed turn older than the router's arm (the same tolerance judgeTurnText uses), and reads the reply text:
    • for ---MANAGER-READY---, which marks the Manager ready;
    • for DISPATCH blocks, which are routed.
  • routeManagerDispatch is now the one routing path for both the screen scan and the reply text. It records each block (the target plus the message with whitespace dropped, so the TUI's wrapping and indentation do not count) and routes it once.
  • A DISPATCH before READY implies READY: markManagerReady logs Manager READY (implied by its DISPATCH) and releases the held messages. Before READY the Manager can only be coordinating, and holding its workers' REPORTs for a line it may never print is what deadlocked the stage.
  • The MANAGER-READY screen scan uses the same markManagerReady, and its floor-cursor logic is unchanged.

Not in this PR

  • When a pipeline has one commander, deriveGlobalManager also makes it the global Manager. So the global router and the stage router both scan the same panes. The global router routes worker REPORTs to the Manager at once, bypassing the pre-READY queue: the run log's 01:37:12 🎯 Global → Manager: [REPORT from 畫師] comes before READY. When the Manager's screen does parse, both routers inject the same DISPATCH. That is a separate root cause, and a follow-up.
  • STAGE-DONE is still read from the screen only. A Manager whose screen does not show it is caught by the stage watchdog.
  • Suggestions 3 and 4 in Pipeline Manager mode (0.2.27): DISPATCH sent before ---MANAGER-READY--- is silently dropped, stage deadlocks while showing "running" #539 (a deadlock warning in pipeline_status, and a stage-editor hint about DISPATCH/REPORT) are not done.

Tests (red first, then green)

  • App.managerDispatchTurnText.test.ts (new) lifts the routing functions out and runs them against stubs:

    • a reply DISPATCH reaches its worker;
    • a DISPATCH before READY implies READY and releases the held REPORT;
    • a block the screen already routed (wrapped and indented) is not sent again;
    • MANAGER-READY in the reply releases the queue;
    • a worker pane, a replayed old turn and a finished stage are ignored;
    • the scan and the turn handler both use the shared path.

    All 6 failed before the fix.

  • Also run locally: App.managerStageWatchdog, App.pipelineScanWindow, App.pipelineLinearRegression and App.pipelineStagePrewarm (71 tests in total), and pnpm typecheck. All green.

Protection check

  • Credential, account-switch and quota files are not touched.
  • No function is deleted. The inline READY drain and DISPATCH routing moved into markManagerReady and routeManagerDispatch, with the same log lines.
  • No signature changes. Added the markManagerReady, routeManagerDispatch and routeManagerTurnText functions, and the StageRouter.routedDispatches field.

🤖 Generated with Claude Code

The stage router read DISPATCH only from the Manager's screen buffer. A block
that did not parse there was dropped without a log line, the workers' REPORTs
stayed queued for MANAGER-READY, and the stage sat idle as running. The
Manager's turn_complete text now feeds the same routing, each block is routed
once, and a DISPATCH before MANAGER-READY implies it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant