Repository navigation
Conversation
…ports back to the caller's conversation (#3295) A plain sequential delegation (parallel=false → POST /chat) could never post its outcome into the Slack, Telegram or Workspace thread the caller serves: the request had no field for the caller's turn, the row was created without a channel context, and the /chat terminals never spawned the completion report. When the MCP server gave up at 25 s and handed back a receipt, the person heard nothing when the work finished. The parent still never travels in the /chat body. The MCP server asks for the report at the one moment it knows the caller did NOT get the reply — when it builds a queued_timeout receipt (its own abort, a 409 in-flight replay, or the backend's own 504, now recovered through the same execution lookup) — through a new POST /api/agents/{name}/executions/{id}/report-back naming the caller's turn. A call that answers inline is never armed, so it cannot post a second "done". Backend: arm_chat_report_back admits only the row's dispatcher (agent key = source_agent_name, person = source_user_id; connector 403; anything else one uniform 404), only a /chat row, and inherits through the unchanged _inherited_channel_context provenance guard; the stamp is ent#498's add-only stamp_execution_channel_context. The push /chat finalizers and the pull lock-busy write spawn spawn_completion_report on a CAS-won write (a no-op unless armed). report_completion reads the row fresh and effect_guard keys on the destination, so stamp-then-terminal and terminal-then-stamp both deliver exactly once. MCP: resolveReportBack defaults the chat route on under the /task rules (header turn first, typed id held to the header's format, manual opts out, MCP_REPORT_BACK_ENABLED=false sends nothing); chatRouteFields picks the result fields from the outcome — requested on an armed receipt, off/not_armed on a refused one, off/answered_inline for a typed id whose reply came back. The sequential_chat reason is retired. Fixes #3295 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
merge-train (2026-10-09): ejected — the |
|
Resolve by running |
|
Resolve by merging |
|
merge-train (2026-10-11): ejected. This PR's own
The last four all go through the budget-exhaustion finalizer in |
Summary
chat_with_agent(parallel=false→POST /chat) could never post its outcome into the Slack, Telegram or Workspace thread the caller serves. When the MCP server gave up at 25 s and returned a receipt, the person heard nothing when the work finished./chatbody. The MCP server asks for the report at the one moment it knows the caller did not get the reply — when it builds aqueued_timeoutreceipt (its own abort, a 409 in-flight replay, or the backend's own 504, now recovered through the same execution lookup) — via a newPOST /api/agents/{name}/executions/{id}/report-backnaming the caller's turn. A call that answers inline is never armed, so it cannot post a second "done"./chatrow creation): stamping at creation double-posts on a pull pilot (the sink reports before the waiting handler returns the reply), and a probe on the real image showedrequest.is_disconnected()reads False behind the app's two@app.middleware("http")wrappers, so a "caller still waiting" hold could not be released. Recorded indocs/memory/learnings.md.Changes
src/backend/services/chat_execution_service.py—arm_chat_report_back(dispatcher-of-row gate: agent key =source_agent_name, person =source_user_id, connector 403, anything else one uniform 404;/chatrows only; inheritance through the unchanged_inherited_channel_context; ent#498's add-only stamp; re-read + spawn if already terminal) andspawn_completion_reporton the four push/chatterminal writes (CAS-won only).src/backend/routers/chat.py,src/backend/models.py— the route andReportBackRequest(extra=forbid, 1..128).src/mcp-server/src/client.ts—armChatReportBack(own 2 s deadline, never throws; only an explicit refusal reads asoff), backend 504 on/chat→ the bug: chat_with_agent MCP tool returns 'fetch failed' but still queues execution — silent duplicates burn budget on naive retry #914 lookup → receipt.src/mcp-server/src/tools/chat.ts—resolveReportBackdefaults the chat route on under the/taskrules;chatRouteFieldspicks result fields from the outcome; reasonsanswered_inline/not_armedreplacesequential_chat; one clause added toEXECUTION_ID_PARAM_DESCRIPTION.tests/unit/_route_census.py— the route is listed agent-callable with its reason.channel-completion-report.md(route table, new "How a sequential /chat call is armed" section, chokepoint table),api-endpoints.md,agent-to-agent-collaboration.md, user docs,learnings.md, CSO diff report.Test Plan
tests/unit/test_3295_chat_report_back.py— 19 tests against the real DB layer and the real reporter +effect_guard: both provenance arms read back, every refusal, arm-after-terminal and arm-before-terminal each deliver exactly once, the reporter reads the row fresh, an un-armed row reports nothing, the success finalizer spawns the sanitized response on a CAS win only.src/mcp-server/src/chat-parent-execution.test.ts— 64 tests over a real MCP transport: a receipt arms once with the header turn (both tools), the 504 path, a refused arm,manual/ manual header / no header / kill switch arm nothing, the oracle over the full input product; T5/T6/T13 consciously re-pinned.tsc --noEmitclean./review: 0 critical./cso --diff: 0 findings (one hardening applied — the not-dispatcher refusal is the same uniform 404 as a missing row).chat_with_agent(agent, long_task)sequentially; after the 25 s receipt (report_back: requested), the child's note lands in the thread when it finishes; a call that answers in time posts nothing extra.Fixes #3295
🤖 Generated with Claude Code