Skip to content

fix(decisions): make dispatch_decision work (agent-by-id + channel inference)#207

Merged
Fullstop000 merged 3 commits into
mainfrom
fix/dispatch-decision-agent-lookup
May 23, 2026
Merged

fix(decisions): make dispatch_decision work (agent-by-id + channel inference)#207
Fullstop000 merged 3 commits into
mainfrom
fix/dispatch-decision-agent-lookup

Conversation

@Fullstop000
Copy link
Copy Markdown
Owner

@Fullstop000 Fullstop000 commented May 23, 2026

Makes dispatch_decision actually work end-to-end. Two distinct root causes, both found via live dogfood of the reviewer loop (#206), where a reviewer agent's escalation failed. They sit in the same handler and block the same goal, so they land together.

Bug #1 — agent resolved by name, not id

handle_create_decision looked up the agent with get_agent(name), but the bridge addresses the callback as /internal/agent/{agent_id}/decisions with the UUID id (agents are id-keyed since #142). Every real agent 404'd ("agent not found"); only test fixtures whose name equaled their id matched. Fixed to get_agent_by_id.

Bug #2 — channel inferred from dead code

With #1 fixed, the next guard always failed: the decision's channel came from lifecycle.run_channel_id, backed by an in-memory agent-scoped value set only by set_run_channel — which has no non-test caller. So in production it was always None and every escalation 400'd. The Decision Inbox never worked; tests passed only because the mock lifecycle set the channel by hand.

The channel is display context only (an inbox label) — delivery is agent-scoped (resume_with_prompt), inbox scoping is by workspace_id. So it's now resolved from durable reads, most-specific first:

  1. the agent's claimed active task → its sub-channel (task work drives status without a run-stamped chat, so this is the reliable origin for a task agent that escalates before chatting — the dogfood case)
  2. the agent's current run → channel of its first message this run
  3. the agent's most recent chat channel
  4. else an honest 400

Deleted the dead set_run_channel / run_channel_id from the AgentLifecycle trait, AgentManager, and the test mock — the trap that hid bug #2.

Tests

  • New decision_channel_resolves_from_claimed_task: a silent task agent (claims a task, never chats) escalates → decision attributed to the task's sub-channel. Exercises the production path with no mock channel.
  • Existing decision tests reworked off the mock's set_run_channel to use real channel sources.

Verification

cargo test (lib 576, server_tests 89 incl. 7 decision tests) green · cargo clippy --all-targets -- -D warnings clean · cargo fmt --check clean.

🤖 Generated with Claude Code

handle_create_decision looked up the agent with get_agent(name) while the
bridge addresses the callback as /internal/agent/{agent_id}/decisions with
the agent's UUID id (agents are keyed by id since #142). The id never
matched the name column, so every real agent's dispatch_decision 500'd with
"agent not found" — the resume path in the same file already used
get_agent_by_id. Switch the create path to get_agent_by_id.

The existing test masked it: its fixture seeds an agent whose name == id
("bot1"), so the by-name lookup happened to match. Added a regression test
with a real UUID id != name (verified it 404s without the fix).

Found by dogfooding the reviewer loop's escalation path (#206): a reviewer
agent correctly chose to escalate via dispatch_decision and hit this.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…race lookup

dispatch_decision inferred its inbox channel from `lifecycle.run_channel_id`,
backed by AgentTraceStore's in-memory agent-scoped channel. But `set_run_channel`
had no non-test caller, so the lookup always returned None and every escalation
400'd ("no active-run channel"). The Decision Inbox never worked in production;
tests passed only because the mock lifecycle set the channel by hand. Bug #1
(agent-by-name) failed earlier in the same handler and masked this.

The channel is display context (inbox label) only — delivery is agent-scoped
(resume_with_prompt), scoping is by workspace_id — so resolve it from durable
reads: claimed-task sub-channel, else current-run channel, else last chat
channel, else an honest 400. Task work drives status without a run-stamped
chat, so the claimed task's sub-channel is the reliable origin for a task
agent that escalates before chatting (the reviewer-loop case the dogfood hit).

Delete the dead set_run_channel / run_channel_id from the AgentLifecycle trait,
AgentManager, and the test mock — the trap that hid this. Add a regression test
(decision_channel_resolves_from_claimed_task) exercising the production path
with no mock channel; rework the existing decision tests off set_run_channel.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@Fullstop000 Fullstop000 changed the title fix(decisions): resolve dispatch_decision agent by id, not name fix(decisions): make dispatch_decision work (agent-by-id + channel inference) May 23, 2026
- CHANGELOG: dispatch_decision now reaches the Decision Inbox (#207)
- VERSION: 0.18.0 -> 0.18.1

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@Fullstop000 Fullstop000 merged commit 765327a into main May 23, 2026
3 checks passed
@Fullstop000 Fullstop000 deleted the fix/dispatch-decision-agent-lookup branch May 23, 2026 15:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant