Rounds that are not answers
Three defects with one shape: a round's record claimed something its own evidence did not support, and every reader downstream — the PR steward, the bug-fix pool, triage — acted on the claim.
1. A selected agent the catalog does not hold is an Error, never Completed (Plugins #2797)
When a round's explicitly selected agent matches no loaded agent (after the bounded catalog wait,
AgentChatClient.MissingAgentGrace), AgentChatClient streams the sentence "Selected agent 'X' was
not found among the available agents (…)" as the round's only output. No model runs and no tool runs.
RoundOutcome.Classify read any non-empty text with no tool call as an answer, so the cell settled
Completed — measured on the control instance as
Hosting/Triage/_Thread/review-pull-request-…-6c03/c400d55d (2026-10-03). Every control-plane caller
counts a Completed round as a spent attempt, so a catalog gap silently consumed their budgets.
The rule: an unresolved selection is its own verdict, RoundVerdict.SelectionUnresolved, decided
ahead of every text rule and never Completed.
AgentChatClient.UnresolvedSelectionErrorlatches the sentence only when the failed selection was an explicit pick that matched nothing — never for a matched-but-unbuildable agent, whose truthful error (no model, no factory) is a different condition.ThreadExecutionpasses it toRoundOutcome.Classify; the round writes*Error: Selected agent …*as its text,Error: …as its Summary, and tells a delegating parent it failed. That is the same terminal shape as no model can serve this round (#476).- Readers:
PullRequestSweep.ClassifyRoundreads theErrorshape (and still the legacyCompletedone) as infrastructure,reviewer agent not found, only when the round ran no model and no tool. The round carries no output tokens, which is what the bug-fix pool's infrastructure classifier keys on.
The "pr-reviewer not found" wave itself (fresh and resumed rounds dispatching without the space layer)
was fixed by Plugins #2729 and the AI build that carried it; on the control instance the sentence has
not appeared in any Hosting/Triage cell since 2026-10-04 06:30Z, over hundreds of pr-reviewer
rounds. This section is about what such a round is CALLED when it does happen.
2. The supervisor's "last error" is the stuck round's, never an earlier round's (Plugins #3050)
ThreadSupervisor.Diagnosis embeds last error: in the Error cell it settles with and in the feedback
it files. It took that text from Thread.Summary. A round writes Summary only in the write that
ends it (the terminal Idle write), so on an executing thread — every Stale verdict — any Summary
was written by an EARLIER round, possibly another episode, possibly days old.
Measured on the control instance (2026-10-07): round d0cb3e56 of
Hosting/Triage/pull-request/systemorph-meshweaver-plugins-2969/_Thread/owner-…-2969 was cut three
times by its host shutting down during successive rolls (Loki on the control instance:
Host is shutting down, cannot route to …/d0cb3e56 from three different pods, at 02:02:32Z,
02:38:13Z and 03:10:02Z). The supervisor relaunched it twice and gave
up at 03:15:36Z, quoting as last error an OpenRouter HTTP 403 … Key limit exceeded (daily limit) —
written by round 233a0419 on 2026-10-05 23:48Z, 27 hours earlier. Triage filed it as "Agent thread
wedges on an exhausted model key". The key was not involved: the next round that did hit the exhausted
key (04:07:15Z, 271046c3) failed in 43 ms with a named Error cell and the dispatch pool closed the
provider until midnight.
The rule: on an executing thread the last error is the round's own ExecutionStatus, or "none —
the round in flight has written no error"; an earlier Summary is still reported, labelled "left by an
EARLIER round, not this one". On an idle thread (Parked) the Summary IS the last round's terminal
write and is read as before — including the #2236 cases after a wake or a relaunch
(ThreadSupervisorSettleLoopTest).
3. An answer cut at the output cap is continued once, then named a truncation (Plugins #4459)
A provider that ends a call at the output cap SAYS so: the stream's finish reason is length. Nothing
read it. The function-invoking loop saw a call with no tool call and stopped; RoundOutcome.Classify
then judged the round by its TEXT alone. Reported twice in one build session on the public instance
(2026-10-23): a delegated Worker generating code hit an output cap of exactly 16,384 tokens and its cell
said "The model ran its tools but never wrote a closing answer" — which reads as a model that gave up,
not one that was cut off. The same evidence on a call that HAD written text was worse: the round settled
Completed over half an answer.
The rules:
- The ledger records how each call ended.
ProviderCallTimingChatClientwrites the provider's finish reason (ModelCallTiming.FinishReason, the wire value), the output cap the call was SENT with (MaxOutputTokens) and whether the call was the platform's continuation (Continuation). A completed call with no finish reason is a finding in itself: the endpoint closed the response without saying why. - A capped call is continued once, when that is safe (
OutputCapContinuationChatClient, between the context-window guard and the provider retry). Safe means the call streamed nothing but text, reasoning and usage: the same request is issued again with the partial text as an assistant turn and a user turn asking the model to carry on, the continuation's updates carry the first call's message id (one assistant message, never two), and the intermediatelengthis hidden from everything above. A call that streamed a tool call is never continued — the function-invoking loop may already be acting on it.AiStreamingLimits.OutputCapContinuations(default 1, 0 = off) bounds it; the cap itself is the declared one (OutputCap) and is not raised. - The round reads the LAST call's finish reason before its text rules.
length→RoundVerdict.OutputCapReached,content_filter→RoundVerdict.ContentFiltered, bothError, after an unfinished tool call (still the actionable fact) and before no output / no final answer. A call that stopped withstopkeeps the old rules, so "never wrote a closing answer" now means exactly that. - The banner names it and quotes the call's figures.
RoundEndTexts(a module text table, English and German, following the viewer's locale) says the answer was cut off at its output limit of N tokens — a truncation, not a refusal, adds that an automatic continuation was cut too when it was, and appends to EVERY unanswered round's diagnosis a line such as "Last model call: first output after 15.6 s, 18.2 s in all, 24 output tokens, 246,017 input tokens, no finish reason reported." — what tells a slow or capped provider from a wedged agent. The finish reason is quoted as the provider's wire value.
A stall the guard catches (the connection stays open and the bytes stop) or a body cut mid-answer that surfaces as an exception is a different condition with its own handling (Provider stream faults): the stall guard names it and the retry regenerates a text-only call. A CLI harness round records no model calls, so it keeps the text rules.
4. A stream that ENDS with no finish reason was cut, not finished (Plugins #4459, the stall half)
The report's second occurrence was the main thread's last call: 15.6 s to first output, 24 output tokens,
then the stream ended. No exception arrived, so neither the stall guard nor the retry saw anything; the
ledger records such a call as Outcome: Completed with no finishReason. The round judged it by its text
and said "never wrote a closing answer" — and on a call that had written text after the last tool
result, it settled Completed over the fragment.
The rules:
- The round names it.
RoundVerdict.StreamEndedWithoutFinish(Error), decided after an unfinished tool call and afterlength/content_filter. It replaces no output / no final answer unconditionally — both are errors already, and the provider not saying why the stream ended is the more specific fact. It outranks text that reads as an ANSWER only when an EARLIER call of the same round reported a finish reason (RoundOutcome.ReportsFinishReasons). Not every provider adapter reports one, so on a provider that never does, a missing finish reason is no evidence of a cut, and a whole answer must not turn into an error. The round's own evidence about its own provider path decides. A closing call that is itself the platform's continuation counts as that evidence too: a continuation is issued only after the previous call was judged cut, so one that ends the same way is never an answer. - The retry layer passes a finish chunk on. An OpenAI-compatible wire sends the finish chunk and THEN a
usage chunk; on a textless response usage is the first output, and
ProviderCallRetryChatClientused to drop the held finish chunk as content-less middle, so an explicitstopread as no finish reason and was continued. A held chunk that carries a finish reason is now released in order, and a later content-less chunk never displaces it. - It is continued once, like a cap (
OutputCapContinuationChatClient): same safety rules (nothing but text, reasoning and usage streamed; never after a tool call), same budget (AiStreamingLimits.OutputCapContinuations), same context-window check, its own model-facing prompt (the connection ended early, not the output limit). Only once the client has seen its provider report a finish reason on some call, for the same reason as above. - The banner (
RoundEndTexts, English and German) says the stream ended without the provider saying it was finished — a provider stall or a dropped connection, not the model choosing to stop, adds that an automatic continuation ended the same way when it did, and carries the call's figures line, which already prints no finish reason reported. - The Anthropic client passes stop reasons on verbatim. It used to drop any
stop_reasonit did not map (refusal,pause_turn) to no finish reason, which the rules above would now read as a cut;refusalmaps tocontent_filter, anything else unmapped is passed through as its wire value.
Tests
UnresolvedAgentSelectionRoundTest— a real round withAgent/ghost-agent-2797settlesErrorwith the not-found line; the control (default agent, same mesh, same model) completes. Negative control: without theThreadExecutionchange the first case fails with "found Completed".RoundOutcomeTest.AnUnresolvedSelection_IsNotAnAnswer_EvenWithText— the marker decides, not the wording.ThreadSupervisorLastErrorProvenanceTest— the measured thread's shape; negative control: without theDiagnosischange both executing cases fail quoting the 403.PullRequestSweepTests.ARound_IsInfrastructureOnlyOnWhatThePlatformWrites— theErrorshape is infrastructure; the same line after a model call is not.TruncatedCompletionRoundTest— real rounds through a realChatClientAgentFactorypipeline against a provider that truncates: a capped answer is continued into oneCompletedanswer (two ledger calls, the secondContinuation); a cap hit again settlesErrornaming the truncation and the figures, never "never wrote a closing answer"; thestopcontrol is one call andCompleted. Negative control: without the change the first two fail — the round completes over the half answer.OutputCapContinuationTest— under the realFunctionInvokingChatClient: one assistant message, no empty assistant turn for a reasoning-only cap, a capped tool call never continued, one-shot; the classifier's order; the English and German sentences.StalledCompletionRoundTest— real rounds that run a tool, then cut the closing call with no finish reason: a silent cut (24 output tokens, nothing visible) and its continuation settleErrornaming the stall with the figures, never "never wrote a closing answer"; a cut after text is continued into oneCompletedanswer. Controls: a closing call that endsstopwith no text is still "never wrote a closing answer" and is not continued; one that endsstopafter its answer completes. Negative control: without the change the first two fail.OutputCapContinuationTest(stall cases) — the classifier with and without the round's proof that its provider reports finish reasons; continuation only after the provider reported one, never after a tool call; the English and German stall sentence.
Not established
- Why three rolls landed inside 70 minutes on the control instance, and whether a host-shutdown interruption should count against the supervisor's relaunch cap at all — the supervisor sees only the node and cannot tell a dead pod from a wedged round.
- Whether an exhausted key fails over to another key or rung for the round that HITS it: the dispatch
pool closes the provider for later rounds ("its rounds move to alternatives or wait"); the round that
met the 403 ends
Error. Not changed here. - Which finish reason the main thread's last call in #4459 carried (24 output tokens after 15.6 s, then
the stream ended): the ledger did not record finish reasons then. Section 4 assumes it was none; had it
been
length, section 3 already covers it. - Which provider adapters other than the OpenAI-compatible and Anthropic clients report finish reasons; the per-round proof makes the rules safe either way, at the cost of not naming a cut on one that never does.
- The 600-second delegation timeout in the same report (a parent waiting on a Worker that was still working) is a separate condition, not changed here.