Headless plain-sendMessage turn dies (finishReason tool-calls, no text) when a monitored background bash settles before task_await
- 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