fix: reset web-chat worktree status from owned claude -p run - #273
Conversation
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
webmux | 02951db | Commit Preview URL Branch Preview URL |
Jun 19 2026, 11:01 PM |
|
Claude finished @centdix's task in 1m 46s —— View job Code Review: PR #273 — reset web-chat worktree status from owned
|
|
Claude finished @centdix's task in 2m 0s —— View job Code Review: PR #273 — reset web-chat worktree status from owned
|
Summary
The Claude streaming PR (#262) added a busy-gate to the web chat: it rejects a message with "Claude is already running in the terminal. Wait for it to finish…" whenever
worktree.statusisrunning/starting. That status is derived from the worktree'sagent.lifecycle, which is driven only by best-effort,asyncfire-and-forget hooks (UserPromptSubmit/PostToolUse→ running,Stop→ stopped). Those POSTs get lost/reordered, so the lifecycle sticks atrunningand the gate then blocks web chat even though nothing is running.Repro (web→web): send a first web-chat message → the owned
claude -prun firesUserPromptSubmit(running); the turn ends but theStophook's POST never lands → status stuckrunning→ the next web-chat message is blocked.The backend already owns the
claude -psubprocess, so it has a reliable end-of-turn signal (the streamresultline / process exit). This PR uses that owned signal to drive the worktree lifecycle directly instead of trusting the lossy hook.Changes
claude-conversation-stream-service.ts: add anonRunSettled?callback to the run input; it fires exactly once fromfinishRun(covers normal completion, error, and interrupt — all funnel throughfinishRun, which already early-returns oncompleted).server.ts: addsetWorktreeAgentLifecycle(worktree, "running" | "stopped")(guardedgetWorktreeByBranch→applyEvent, mirroringsetAgentTerminalStale). InsendClaudeStreamingMessage, setrunningwhen the owned run starts and passonRunSettledto setstoppedwhen it settles.stoppedmaps to the UI done state — the same state a terminal turn finishing produces — notidle/waiting.onRunSettledcontract (fires once on complete/error/interrupt) + an integration repro proving the worktree flipsrunning→stoppedon completion so the next web message passes the gate.Test plan
bun test(backend) — 461 pass / 0 fail; 3 new tests added (failed first, pass after the fix)bun run check(backend tsc --noEmit) — exit 0Known limitation / follow-up
The web-chat
claude -psubprocess still runs the worktree's.claudehooks. For a turn that ends on a tool call (no trailing text), a late asyncPostToolUse → runningPOST can land after ouronComplete → stoppedand re-stickrunning. Text-ending turns (the common case) are unaffected. Full robustness = suppress the status hooks for ownedclaude -pruns (keep PR-detection) so the owned signal is the sole driver. Tracked as a follow-up.🤖 Generated with Claude Code