A resumed round runs with the agent it was started with
What was seen
On the control instance on 2026-10-02 (17:09–19:21Z) and 2026-10-03 (09:44–10:17Z), review, triage
and bug threads under Hosting/Triage/_Thread/… ended rounds with this message:
Selected agent 'pr-reviewer' was not found among the available agents ([Agent/Assistant, Agent/Researcher,
Agent/TrainingSim, Agent/Tutor, Agent/Worker, Agent/ExecutiveAssistant, Agent/LogTriage, Agent/NotificationTriage,
Agent/EmailRouter, Agent/PullRequestWriter, Agent/DescriptionWriter, Agent/NodeInitializer, Agent/ThreadNamer]).
The same message appeared for triage and bug-triage, each written as a Completed response cell.
- Timing. Both windows were portal rolls. One failure was a supervisor relaunch ("Relaunch 1/2 by the supervisor … The activation was recycled so the hub re-reads its node: a Streaming cell resumes").
- Pairs. Every failure appears twice in Loki, 30 s apart. That is the catalog wait
(
SelectAgentAwaitingCatalogAsync,MissingAgentGrace) running and still finding nothing. - The agent nodes were fine. They existed and had not changed:
Hosting/Agent/pr-reviewerwas last modified 2026-10-02 20:31Z, andHosting/Agent/triageon 2026-09-30. - Fresh submissions resolved. On the same pods, newly submitted rounds resolved the same agents.
Why
A resume carries no selection. Both resume entries, DispatchAfterClaim and the interrupted-round
recovery in ThreadSubmission, build a RoundDispatch with AgentName, ModelName, Harness and
ContextPath all null. ThreadExecution.ExecuteMessageAsync then recovered the agent, model and
harness from the response cell, but it lost two things.
| Lost | Because | Registry row that went with it |
|---|---|---|
The agent's install path (Essentials/Agent/bug-triage) |
The cell stamps the SHORT name (bug-triage) |
AgentPickerProjection.PinnedAgentQuery. This is the only row that reaches an agent outside the context's own layers, which is how a Hosting/Triage thread reaches Essentials/Agent (Plugins#2566) |
The context (Hosting/Triage/pull-request/…) |
It was never recovered | The space layer {space}/Agent. This is how Hosting/Agent/pr-reviewer and Hosting/Agent/triage are found |
What remained was the platform namespace Agent, which is exactly the roster the message printed.
Waiting longer cannot help, because no later catalog emission adds a row that the query never asked
for.
What a resume recovers now
ThreadExecution.RecoverResumeSelection reads the response cell and the thread node:
- Agent. The thread composer's agent path, when it names the same agent as the cell (compared by id). Otherwise it uses the cell's agent. If the composer was changed after the round started, the resume still uses the agent that ran.
- Context. The composer's context, under the same guard as the agent. The composer is the NEXT message's selection, so its context is adopted only while it still describes this round: it names no agent, or the same agent as the cell. If it names a different agent, the composer has moved on and its context is not used. Otherwise the thread's main node is used, unless the main node is the thread itself.
- Model. The cell's model, keeping its full node path.
- Harness. The cell's harness, as a bare id.
A value the request already carries always wins. The cell and the thread node are read together,
each bounded by 5 s. The thread read is an own-node read: the hub running the resume is the thread
hub, already active. If it fails anyway, the resume falls back to the cell alone, is never blocked,
and logs a warning (Resume: could not read thread node …). That way a roll on which resumed rounds
degrade shows up in Loki before the rounds end. ResumeSelectionRecoveryTest (MeshWeaver.AI.Test) pins each
branch.
Not established
A cell that recorded no agent. Then nothing cross-checks the composer: its agent and its context are both adopted as they stand, even if a newer submission repointed the composer. The composer is the only selection left in that case, and without it the round would run the default agent with no context, the very state this fix removes.
ACellWithoutAnAgent_TakesTheComposersandAComposerWithoutAnAgent_StillLendsItsContextpin this choice.A context changed under the same agent. The cell records no context. A submission that kept the agent but changed the composer's context between the interruption and the resume still hands the resumed round the new context. The guard catches only a composer that names a different agent.
The catalog's identity on a resume. The rows the composer path and context restore are read under the round's identity. For these control-plane threads that identity falls back to the thread's
CreatedBy(system-security), which bypasses row-level security. This was not measured: theDispatchRound identityline is Information level and does not reach Loki.Who executes control-plane rounds. Running them as an event-triggered job in the control instance's build queue is separate work (the bug-fix process). It does not change what a resume has to recover.