Skip to content

fix(zcode): wait for composer before first worker dispatch - #23374

Merged
nwparker merged 2 commits into
stablyai:mainfrom
Alex-wangyang:codex/zcode-native-readiness
Sep 28, 2026
Merged

nwparker merged 2 commits into
stablyai:mainfrom
Alex-wangyang:codex/zcode-native-readiness

Conversation

@Alex-wangyang

Copy link
Copy Markdown
Contributor

ELI5

A fresh ZCode worker can show its input box while Orca still waits for it to become idle, so the first task never arrives. Use the ZCode composer signal Orca already recognizes to deliver that first task once the input box is ready.

What Changed

  • Route only newly launched ZCode terminal workers through the existing startup composer scanner. Reused terminals and other agents retain the normal idle wait; structured workers retain their setup gate.
  • Require the composer marker, honor the worker-start timeout, and revalidate the original PTY using the existing handle identity assertion. This also handles the renderer adopting the terminal while startup is in progress.
  • Register the deadline before replaying buffered output and ignore observations after settlement, preventing a replay-triggered completion from leaving a timer behind.
  • Add regressions for captured ZCode output, negative startup-dialog evidence, replay timer cleanup, dispatch ordering/timeout, terminal reuse, and renderer adoption.

Why

The shipped ZCode configuration already has zcode-composer-prompt, but terminal worker-start still waits for generic tui-idle. ZCode continually redraws its banner, and the idle text tail loses the composer evidence. In the tested ZCode version, SessionStart hooks run on the first turn, after input, so those hooks cannot unblock this first dispatch.

Reusing the existing scanner connects the native readiness mechanism to worker-start without another status producer, fake terminal titles, external launcher, permission override, or changes to ZCode itself. This is a narrow fix for the released interactive path; the broader client/transcript work in #16228 is separate.

Linked Issue

Fixes #23373

Visual Proof

No visual change: no renderer or TUI drawing code changes. The observable change is automated CLI task delivery. Before/after receipt excerpts from actual runs:

// Official 1.4.214, fresh ZCode worker, 45-second deadline
{"state":"failed","stage":"agent_readiness","lastError":"timeout"}
// Patched packaged app, same native worker-start path
{"state":"ready","stage":"input_accepted"}

The after receipt alone is not used as completion evidence: two actual packaged-app canaries also wrote/read back the exact requested file, sent worker_done, and released their terminal resources. No title manipulation or external starter was used.

Testing

  • Manually tested locally: macOS ARM64, official ZCode v3.14.3 distribution, configured model with tool use.
  • Automated regression tests added.
  • 54 tests passed across runtime-worktree-startup-readiness.test.ts, worker/zcode-worker-readiness.test.ts, zcode-readiness-transcript.test.ts, draft-paste-ready-scanner.test.ts, runtime-local-worktree-terminal-startup.test.ts, and worker/worker-terminal-custody-at-creation.test.ts.
  • Node typecheck passed: node node_modules/typescript/bin/tsc --noEmit -p config/tsconfig.node.json.
  • Changed-code quality checks, formatting and git diff --check passed.
  • Main/renderer/CLI/relay/web/mobile-web and macOS ARM64 packaging completed; packaged main bytes matched the source build. Two live canaries passed, including one after installation.
  • The default all-platform build command was not claimed as passing: local Swift toolchain compatibility needed native SwiftPM output and single-architecture keyboard/notification helper builds. No build-script changes are included here.
  • Full-repository lint/typecheck/test and Windows/Linux/remote live execution were not run; CI and maintainer testing remain necessary.

AI Disclosure

Implemented and reviewed with OpenAI Codex (GPT-6).

Review

  • Cross-platform: no OS-specific paths or APIs added; uses the existing runtime PTY and scanner abstractions. Live validation is macOS ARM64 only.
  • SSH/remote/local: readiness is evaluated in the worker's executing runtime; no coordinator-local filesystem lookup added. Remote setup/transport behavior is not live-tested.
  • Agents/integrations: guarded by fresh agent === 'zcode'; other agents, explicit terminal reuse, setup sequencing, and structured worker gates keep their existing paths.
  • Performance: one bounded PTY subscription using the existing incremental scanner; timer/subscription cleanup on completion or timeout.
  • UI: no rendering changes; renderer adoption of the terminal handle is covered by a runtime regression.
  • Security: existing PTY identity/liveness checks remain before dispatch. No new permission bypass, provider credentials, hooks, or network configuration.

Agent skill upstream boundary

  • Not applicable: no skill or upstream skill-installer material is changed or copied.

Author

GitHub: @Alex-wangyang. X/Twitter handle: not provided.

Checklist

  • Small and focused on fresh ZCode first-dispatch readiness.
  • Explained user-visible before/after, mechanism, and why reuse this implementation.
  • Before/after CLI evidence provided; no visual rendering changes.
  • Self-reviewed correctness, security, and performance.
  • Cross-platform and SSH/remote implications documented.
  • Full repository lint, typecheck, test, and default build gates: focused local results and remaining coverage are listed above.

@coderabbitai

coderabbitai Bot commented Sep 27, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

📝 Walkthrough

Walkthrough

Fresh ZCode workers without an explicit terminal now wait for a composer marker before dispatch. Reused terminals continue to use the tui-idle wait. The readiness helper supports a configurable timeout and tests cover composer detection, replayed output, timeout, and worker startup.

Priority: ➖ Normal

Severity of issue fixed: Medium

Merge Risk: 🟡 Moderate · up to 816b3

Fresh ZCode workers can receive their first task while setup remains recorded as running. Settle the setup outcome before merging.

Security Architecture Review

Security architecture risk: 🟡 Moderate · up to 816b3

A fresh ZCode worker may receive its task while a required worktree setup is still recorded as running. Terminal identity checks remain in place, but the setup transition needs resolution before the changed startup path can be treated as equivalent to the prior path.

Retained concerns

  • Medium · security · inferred: For a newly created, non-structured ZCode worker with a running wait-for-setup receipt, composer readiness bypasses setup-outcome persistence before authority preparation and task dispatch. The required setup can remain recorded as running after dispatch.
Security review details

Security Blast Radius

  • inferred — The changed readiness route is limited to fresh local ZCode terminal workers; a bypassed setup gate would affect their worktree and task dispatch, not the structured or explicitly reused terminal paths.

Security Findings and Attack Paths

  • inferred — When a fresh ZCode worktree has a running wait-for-setup policy, composer output can advance task delivery and authority preparation without settling that policy. Whether this enables a concrete attacker-controlled operation depends on what the worktree setup enforces; that was not established.

Trust Boundaries and Controls

  • observed — The worker-start handler requires a coordinator bound to the current run; the fresh-composer runtime method checks terminal identity and connection after readiness. These controls remain in the inspected path.

Resilience and Maintainability Implications

  • observed — Composer timeout rejects before task delivery, while turn-start observation distinguishes an unobserved dispatch from a ready one. The inspected paths do not establish what fences delivery if an already-starting dispatch is abandoned during the composer wait.

Hardening Proposals

  • proposed — Preserve an explicit setup-gate outcome on composer success and verify the pending-readiness versus abandonment transition before preparing authority or delivering input.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 4 functions across 6 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely identifies the main change: waiting for the ZCode composer before the first worker dispatch.
Description check ✅ Passed The description is detailed and covers the required change, rationale, linked issue, behavior evidence, testing, AI disclosure, review considerations, scope boundary, and checklist. The Notes heading …
Linked Issues check ✅ Passed Issue #23373 requires first dispatch for a fresh ZCode worker after the composer is ready. startLocalWorker now calls waitForFreshWorkerComposer for zcode when no terminal is supplied. The helpe…
Out of Scope Changes check ✅ Passed The changed runtime code implements the fresh ZCode readiness path described by #23373. The readiness-helper changes support composer-marker detection, timeout handling, settlement cleanup, and PTY id…
  • Fix all pre-merge checks with AI

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Advanced

Run ID: f7c2a267-cf93-449f-8a47-cde8a5735474

📥 Commits

Reviewing files that changed from the base of the PR and between 90bae01 and 816b371.

📒 Files selected for processing (6)
  • src/main/runtime/orca-runtime-activate-managed-worktree.ts
  • src/main/runtime/rpc/methods/orchestration/worker/local-worker-start.ts
  • src/main/runtime/rpc/methods/orchestration/worker/zcode-worker-readiness.test.ts
  • src/main/runtime/runtime-worktree-startup-readiness.test.ts
  • src/main/runtime/runtime-worktree-startup-readiness.ts
  • src/main/runtime/zcode-readiness-transcript.test.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 9 remain after this review.

Comment on lines +193 to +197
? await runtime.waitForFreshWorkerComposer(
terminalHandle,
agent,
params.timeoutMs ?? 60_000
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🔎 Supported by static analysis

🏁 Script executed:

sed -n '1,140p' src/main/runtime/rpc/methods/orchestration/worker/worker-setup-gate.ts
sed -n '140,235p' src/main/runtime/rpc/methods/orchestration/worker/local-worker-start.ts
rg -n 'composer_ready|type TerminalWait|interface TerminalWait|wait-for-setup|setup_settled' src/main/runtime/rpc/methods/orchestration/worker src/shared

Repository: stablyai/orca

Length of output: 9125


🏁 Script executed:

sed -n '1,190p' src/main/runtime/rpc/methods/orchestration/worker/worker-topology.ts
sed -n '1,120p' src/main/runtime/rpc/methods/orchestration/worker/worker-start-structured-setup-gate.ts
sed -n '180,235p' src/main/runtime/orca-runtime-activate-managed-worktree.ts
rg -n -C 5 'waitForFreshWorkerComposer|composer_ready|applyWaitForSetupOutcome|WaitResult|TerminalWait|status:' src/main/runtime src/shared
sed -n '340,470p' src/main/runtime/rpc/methods/orchestration/worker/workers-new-worktree.test.ts

Repository: stablyai/orca

Length of output: 45658


🏁 Script executed:

rg -n -C 8 'waitForWorktreeStartupDraft|setup runner|agent-first|startupPolicy|agent === .zcode.|requireComposerMarker|SessionStart' src/main/runtime/rpc/methods/orchestration/worker src/main/runtime src/shared
fd -i 'worker-worktree-creation' src
rg -n -C 10 'zcode|composer|wait-for-setup' src/main/runtime/rpc/methods/orchestration/worker/*test.ts src/main/runtime/*test.ts

Repository: stablyai/orca

Length of output: 45665


Record ZCode composer readiness as the setup wait outcome.

For a fresh non-structured ZCode worker, waitForFreshWorkerComposer returns void. The if (wait) guard therefore skips persistWorkerSetupWaitOutcome, leaving a running wait-for-setup receipt and stage unsettled. PTY startup runs behind the setup runner, so composer readiness is sufficient for this setup gate. Return a satisfied wait result with status: 'composer_ready'.

Suggested fix
           await runtime.waitForFreshWorkerComposer(
             terminalHandle,
             agent,
             params.timeoutMs ?? 60_000
-          )
+          ).then(() => ({
+            satisfied: true,
+            status: 'composer_ready'
+          }))
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
? await runtime.waitForFreshWorkerComposer(
terminalHandle,
agent,
params.timeoutMs ?? 60_000
)
? await runtime.waitForFreshWorkerComposer(
terminalHandle,
agent,
params.timeoutMs ?? 60_000
).then(() => ({
satisfied: true,
status: 'composer_ready'
}))

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Important

The zcode first-dispatch branch skips setup-wait settlement and drops the PTY-exit signal from the wait it replaced. Details inline.

Reviewed changes

  • zcode first-dispatch gate — a freshly launched zcode terminal worker (agent === 'zcode' && !params.terminal) now waits on waitForFreshWorkerComposer (composer-marker scan) instead of waitForTerminal('tui-idle'); reused terminals, other agents, and structured workers keep their existing paths.
  • Composer scanner options — waitForWorktreeStartupDraft gains timeoutMs / requireComposerMarker, disables the quiet-timer fallback under the strict marker, adds a settled guard in observe, and registers the deadline before replaying buffered output.
  • Regressions added — captured-composer acceptance, negative startup-dialog evidence, replay-timer cleanup, dispatch ordering/timeout, terminal reuse, and renderer adoption.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

})
: // ZCode emits SessionStart only after input; its first dispatch must wait for the composer.
agent === 'zcode' && !params.terminal
? await runtime.waitForFreshWorkerComposer(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The zcode branch awaits waitForFreshWorkerComposer, whose return type is Promise<void>, so wait is undefined and the if (wait) block below (line 202) is skipped. On a --worktree new-child zcode start where the repo policy is wait-for-setup, persistWorkerSetupWaitOutcome / applyWaitForSetupOutcome never run: the setup receipt stays running while the dispatch is reported ready, no setup_settled stage is recorded, and a setup that ran and failed surfaces as an agent_readiness timeout rather than a setup failure.

Technical details
# Settle the setup wait outcome on the zcode branch

## Affected sites
- `src/main/runtime/rpc/methods/orchestration/worker/local-worker-start.ts:193` — branch result is `void`, so `if (wait)` at `:202` never runs
- `src/main/runtime/orca-runtime-activate-managed-worktree.ts:208` — `waitForFreshWorkerComposer(...): Promise<void>`
- `src/main/runtime/rpc/methods/orchestration/worker/worker-setup-gate.ts:53` — `persistWorkerSetupWaitOutcome` → `applyWaitForSetupOutcome`
- `src/main/runtime/rpc/methods/orchestration/worker/worker-topology.ts:130` — the only local path that flips a `wait-for-setup` receipt `running` → `succeeded`

## Required outcome
- A successful composer wait on a `wait-for-setup` worktree must settle the setup receipt (`state: 'succeeded'`) and record the `setup_settled` stage before task input is delivered, matching the invariant asserted for the non-zcode path at `workers-new-worktree.test.ts:369`.

## Suggested approach
- Return a satisfied wait result (e.g. `{ satisfied: true, status: 'running' }`) from `waitForFreshWorkerComposer` so the existing `if (wait)` block applies, or call `persistWorkerSetupWaitOutcome({ ...setupStage, wait: { satisfied: true, status: 'running' } })` on the zcode success path.

## Open question
- `monitorWorkerSetup` bails for `wait-for-setup`, so nothing reconciles the receipt later; confirm the receipt is intended to stay `running` if this branch is left as-is.

): Promise<void> {
const initialPtyId =
this.getLivePtyForHandle(handle)?.pty.ptyId ?? this.getLiveLeafForHandle(handle).leaf.ptyId
const ptyId = await waitForWorktreeStartupDraft(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unlike the replaced waitForTerminal('tui-idle'), waitForWorktreeStartupDraft has no exit/disconnect subscription — it settles only on the composer marker or the hard timer. A zcode worker that crashes or loses its PTY before painting ╭ therefore stalls for the full worker-start timeout (60s default) before failing with a generic timeout, where the old wait rejected immediately with terminal_exited.

Technical details
# Detect terminal exit while waiting for the composer

## Affected sites
- `src/main/runtime/orca-runtime-activate-managed-worktree.ts:215` — delegates to the composer wait with no exit signal
- `src/main/runtime/runtime-worktree-startup-readiness.ts:137` — the hard timer is the only failure backstop under `requireComposerMarker`
- `src/main/runtime/orca-runtime-resolve-exit-waiters.ts:29` — the `terminal_exited` rejection the old `tui-idle` wait relied on

## Required outcome
- A zcode launch whose PTY exits before the composer appears should fail promptly, not after `timeoutMs`.

## Suggested approach
- Race the composer wait against a pty-exit/disconnect signal (or re-check liveness on the existing data stream) and settle `null`/throw when the terminal dies.

## Open question
- If the agent process exits but the shell PTY survives, the old wait also ran to timeout — confirm the regression is limited to the PTY-exit/disconnect case and decide whether the latency is an accepted trade-off.

@nwparker

Copy link
Copy Markdown
Contributor

@Alex-wangyang this is a very good catch and a very good fix. Your diagnosis is exactly right, and it's a gap I left.

I'm the author of the ZCode harness this builds on, and I want to be plain about the hole: I proved the negative half of this and then didn't follow it through. zcode-readiness-transcript.test.ts already asserts that ZCode leaves no durable readiness evidence in the line-folded idle tail — I recorded the transcript specifically to establish that. I then concluded readiness could come from the synthetic hook title and stopped there. What I missed is precisely what you found: SessionStart fires on the first turn, so a freshly launched worker has no hook yet and tui-idle has nothing to settle on. My own failing test was pointing straight at it.

What I like about the fix:

  • It reuses zcode-composer-prompt rather than adding another status producer. That was the one mechanism that survives ZCode's forever-animating banner, and connecting it to worker-start is the right move.
  • The gate is genuinely narrow — agent === 'zcode' && !params.terminal. Reused terminals keep the ordinary idle wait, structured workers keep their setup gate, other agents are untouched.
  • You added to my transcript test instead of weakening it. The negative assertions still stand and the new case covers the renderer-adoption path. That's the right instinct, and it's the thing I'd have pushed back on if it had gone the other way.
  • Registering the deadline before replaying buffered output, and ignoring post-settlement observations, is the detail I'd expect someone to miss. My earlier attempt at a related problem died on exactly that class of race.

Verified locally on your branch: pnpm tc clean, 320 tests pass across the readiness and worker suites.

One coordination note — please read before anyone merges: #23452 (@brennanb2025) is solving the general form of this. It lets an agent declare how its composer signals readiness, backed by a recorded screen, and it's driven by goose. You overlap on three files, including local-worker-start.ts and runtime-worktree-startup-readiness.ts, and it does not mention ZCode anywhere.

My read: yours should land first. It's non-draft, narrow, and fixes a bug in a released version today; #23452 is a 56-file draft. When that lands, the natural follow-up is for ZCode to become a declaration entry and this special case to disappear. Flagging it on that PR too so it isn't a surprise to either of you.

Thanks for the packaged-app canaries and the before/after receipts — {"state":"failed","stage":"agent_readiness"} → {"state":"ready","stage":"input_accepted"} is exactly the evidence that makes this reviewable.

@nwparker

Copy link
Copy Markdown
Contributor

Closing and immediately reopening to trigger the full CI matrix — only pullfrog and the community tracker ran on this fork, and I'd rather not merge a runtime/orchestration change without the test shards, typecheck, and packaging jobs behind it. Nothing wrong with the PR; purely a workflow-trigger mechanic. Apologies for the notification noise.

@nwparker nwparker closed this Sep 28, 2026
@nwparker nwparker reopened this Sep 28, 2026
@nwparker

Copy link
Copy Markdown
Contributor

Full CI matrix is green (31/31) now that the fork workflows ran. Security review: no network, exec, filesystem, env, or credential access — every added line is readiness logic, with assertLiveTerminalHandleTargetsPty plus a connected check revalidating the PTY after the wait. Locally verified: pnpm tc clean, 320 tests pass across the readiness and worker suites.

Merging. Thanks again @Alex-wangyang — this closed a real gap in the harness I shipped, and the fact that you extended my transcript test rather than loosening it is exactly the right instinct.

@nwparker
nwparker merged commit 47b8408 into stablyai:main Sep 28, 2026
44 checks passed
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.

[Bug]: Fresh ZCode worker times out before its first task is delivered

2 participants