Summary
Dashboard/webchat-originated sessions can enter sourceReplyDeliveryMode: message_tool_only even when there is no external source conversation target for the message tool to infer.
When that happens, the prompt tells the model that visible replies must use message(action="send") and that the target defaults to the current source conversation. In a dashboard-only turn, the dynamic message tool does not have a usable currentChannelId, so message.send fails with:
Action send requires a target.
The assistant can then still produce a normal final answer such as Sent., which is related to the broader fail-closed issue in #49876, but the target-default mismatch here is narrower.
Observed example
In a dashboard session, the user asked the agent to describe its tools. The agent generated a good message(action="send") payload but omitted target, following the prompt guidance. The tool result was Action send requires a target., then the assistant finaled with Sent..
Root cause
The source reply delivery policy can choose message_tool_only for direct sessions from config or harness defaults without excluding internal dashboard/webchat turns.
For external providers, this is fine because the runtime can pass a source target such as currentChannelId / OriginatingTo. For dashboard-only webchat turns, Provider, Surface, and OriginatingChannel are internal webchat, and there is no external OriginatingTo. The low-level target validator is correct to reject the send; the policy should not have selected message-tool-only delivery for that source in the first place.
Proposed fix
Keep automatic final delivery for internal dashboard/webchat-only turns, even if config or a harness default prefers message-tool delivery. Preserve message_tool_only when webchat is merely operating on behalf of an external origin or explicit deliver route.
Concretely:
- Detect internal-only webchat sources in
resolveSourceReplyDeliveryMode.
- If the mode would be
message_tool_only, coerce it to automatic when there is no external origin/deliver route.
- Leave
message.send target validation unchanged.
- Add regression coverage for:
- direct dashboard/webchat turns with global or harness
message_tool defaults falling back to automatic
- webchat routing an external source remaining
message_tool_only
- full
dispatchReplyFromConfig dashboard/webchat turn queueing a normal final reply instead of suppressing it
Local validation
I tested this locally with a small patch following the above approach:
pnpm test src/auto-reply/reply/source-reply-delivery-mode.test.ts src/auto-reply/reply/dispatch-from-config.test.ts
# 2 Vitest shards passed: 19 policy tests, 107 auto-reply dispatch tests
pnpm exec oxfmt --check --threads=1 src/auto-reply/reply/source-reply-delivery-mode.ts src/auto-reply/reply/source-reply-delivery-mode.test.ts src/auto-reply/reply/dispatch-from-config.test.ts
# All matched files use the correct format.
pnpm tsgo:core:test
# passed
Repo-wide pnpm format:check still reports unrelated pre-existing formatting issues in other files; the three changed files pass oxfmt --check directly.
Summary
Dashboard/webchat-originated sessions can enter
sourceReplyDeliveryMode: message_tool_onlyeven when there is no external source conversation target for the message tool to infer.When that happens, the prompt tells the model that visible replies must use
message(action="send")and that the target defaults to the current source conversation. In a dashboard-only turn, the dynamic message tool does not have a usablecurrentChannelId, somessage.sendfails with:The assistant can then still produce a normal final answer such as
Sent., which is related to the broader fail-closed issue in #49876, but the target-default mismatch here is narrower.Observed example
In a dashboard session, the user asked the agent to describe its tools. The agent generated a good
message(action="send")payload but omittedtarget, following the prompt guidance. The tool result wasAction send requires a target., then the assistant finaled withSent..Root cause
The source reply delivery policy can choose
message_tool_onlyfor direct sessions from config or harness defaults without excluding internal dashboard/webchat turns.For external providers, this is fine because the runtime can pass a source target such as
currentChannelId/OriginatingTo. For dashboard-only webchat turns,Provider,Surface, andOriginatingChannelare internalwebchat, and there is no externalOriginatingTo. The low-level target validator is correct to reject the send; the policy should not have selected message-tool-only delivery for that source in the first place.Proposed fix
Keep automatic final delivery for internal dashboard/webchat-only turns, even if config or a harness default prefers message-tool delivery. Preserve
message_tool_onlywhen webchat is merely operating on behalf of an external origin or explicit deliver route.Concretely:
resolveSourceReplyDeliveryMode.message_tool_only, coerce it toautomaticwhen there is no external origin/deliver route.message.sendtarget validation unchanged.message_tooldefaults falling back toautomaticmessage_tool_onlydispatchReplyFromConfigdashboard/webchat turn queueing a normal final reply instead of suppressing itLocal validation
I tested this locally with a small patch following the above approach:
Repo-wide
pnpm format:checkstill reports unrelated pre-existing formatting issues in other files; the three changed files passoxfmt --checkdirectly.