coder / coder/xum

Headless plain-sendMessage turn dies (finishReason tool-calls, no text) when a monitored background bash settles before task_await

Open
#4,093 3 comments 0 reactions 0 assignees View on GitHub
approved
Dominant language
TypeScript
Stars
2k
Forks
134
Avg merge
14h 57m
Merged PRs (30d)
307

Description

## Summary

A headless workspace turn started with plain `workspace/sendMessage` (`{model, agentId}`, no client attached) is silently cut — `finishReason: "tool-calls"`, no assistant text, nothing resumes — when the turn runs a background `bash` with a `monitor` whose process settles **before** `task_await` is issued.

## Deterministic repro (v0.28.3, Linux server mode)

1. `mux api workspace create --project-path --trunk-branch main --title t --branch-name b`
2. `mux api workspace send-message --workspace-id --options '{"model":"anthropic:claude-fable-5","agentId":"exec"}' --message ''` where the prompt instructs: run the bash tool with script `true`, `run_in_background: true`, `monitor: {"filter":"x","wake_on_exit":true}`; then `task_await` that task; then reply with the single word SURVIVED.
3. Inspect `~/.mux/sessions//chat.jsonl`: the last assistant row ends `finishReason: "tool-calls"` with only dynamic-tool parts and no text. The turn never continues.

Control: same prompt with `sleep 5` instead of `true` → the model replies `SURVIVED` (`finishReason: "stop"`).

Notes: the same prompt sent as a delegated `task(kind="workspace")` turn does NOT reproduce (delegated-turn continuation machinery resumes it), and interactive UI sessions are unaffected (wake resolves no send options and stays pending).

## Mechanism (from reading dist 0.28.3)

1. The monitor settlement wake (`workspaceService.js` `dispatchBashMonitorWake`) resolves send options via `getDelegatedTurnContinuationSendOptions() ?? getWorkflowContinuationSendOptions()`. For a plain headless turn the delegated lookup is null, but `getWorkflowContinuationSendOptions` → `getGoalContinuationKickoffSendOptions` has **no goal guard** — it returns kickoff options for any registered workspace. The wake is therefore queued with `queueDispatchMode: "tool-end"`.
2. The queued tool-end message arms a stream cut (`requestQueuedProviderToolEndDispatch` soft-stops the stream at the next tool boundary).
3. The in-flight blocking `task_await` auto-consumes the settlement signal; `bashMonitorWakeReconciler.reconcileOnce` collects zero signals and aborts the dispatch, removing the queued wake (`removableQueueDedupeKey` / `cancelBeforeAcceptance`).
4. The stream-abort handler deliberately skips auto-retry for queued-provider-tool-end aborts (it expects the queued message to continue the turn); `dispatchQueuedProviderToolEndMessageAfterAbort` finds an empty queue → phase IDLE. `"aborted"` is non-retryable, so nothing ever resumes the turn.

## Suggested fix directions

Either (a) do not arm the tool-end cut until the wake dispatch is accepted, or (b) when a tool-end-queued message is canceled/removed after the cut was armed and nothing else was dispatched, resume the cut turn (the auto-retry resume machinery already handles this: `lastAutoRetryResumeRequest` survives the abort). We are running (b) locally as a dist patch: https://github.com/unthought/nixpkgs/pull/704

Also worth considering: `getWorkflowContinuationSendOptions` returning kickoff options for workspaces with no goal/workflow looks unintended and is what exposes the plain-sendMessage path to this race at all.

Contributor guide

No contributing guide indexed for this repository

Research direction

Reproduce the plain headless sendMessage case on Linux, then trace workspaceService.js, dispatchBashMonitorWake, getWorkflowContinuationSendOptions, bashMonitorWakeReconciler.reconcileOnce, and the queued tool-end abort handlers. Compare the true and sleep cases. Done means the headless turn resumes and replies SURVIVED without regressing delegated turns or interactive sessions.

Written by the indexing model from the issue text.

Assessment

Tech stack
bash, typescript
Domain
backend, devtools
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
48/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.