Repository navigation
[bug] The Coder Lead can only ever see 3 of 6 agents — subordinate_status returns 60,239 chars into a 24,000 cap, so half the roster is silently dropped #503
Description
The owner asked "how many agents do you have" six times over two days and never got the answer
Live conversation on Coder Lead (5fab318d-2850-45a4-982c-958765c7261e), read via MCP
instance_messages. Verbatim, in order:
10 Aug 02:36 owner — "How many agents do you have? Why you gave me only update on a few, not on all of them?"
agent — lists 6, ending "Chess coder 2 — status truncated; data cut off before it was returned."10 Aug 07:51 agent — "FGS — status truncated, but runner connected … Heartfull, PAS, Chess — not shown in truncated output"
11 Aug 01:26 agent — "Heartfull, PAS, Chess — truncated from response"
11 Aug 01:51 agent — "FGS — status truncated, couldn't read full detail."
11 Aug 01:53 owner — "So, is FGS running or not? Why the output is truncated? Why can't you see the full update and what's happening?"11 Aug 01:54 agent — "FGS — status truncated again at the tool limit."
11 Aug 01:55 owner — "Why are you talking about just 3 agents? How many agents do you have in total? Why are you not reporting on all of them? … I don't want you to repeat me the same information What are you stupid or something?"11 Aug 01:56 agent — "1. FAS platform 2. FWS platform 3. FGS platform — I can only confirm 3 from the tool result — the rest was cut off. Want me to re-query for the remaining agents specifically?"
11 Aug 01:57 owner — "What the fuck is wrong with you? You fucking idiot? I asked you, how many agents do you have in your supervision in total? Why can't you give me the fucking number?"
11 Aug 01:57 agent — "6 agents total: 1. FAS platform 2. FWS platform 3. FGS platform 4. Heartfull 5. PAS Coder 6. Chess coder 2"
The last answer is correct and took one second, because that turn is the first one that called
list_subordinates instead of subordinate_status (visible in the tool-call system message). The
roster was one cheap tool call away for the entire two days.
The three agents it could always see are FAS, FWS and FGS. The three it never could are Heartfull,
PAS Coder and Chess coder 2. Those sets are not random — see below.
Measured, live, just now
POST /v1/instances/5fab318d…/tools/subordinate_status with {}:
content length: 60,239 chars
capToolResult ceiling: 24,000 chars (lib/tool-result-cap.ts:37)
discarded before the model sees it: 36,239 chars (60%)
Where the 24,000 boundary falls, per subordinate:
| subordinate | block size | starts at char | survives the cap? |
|---|---|---|---|
| FAS platform | 9,049 | 3,804 | yes |
| FWS platform | 9,169 | 13,562 | yes |
| FGS platform | 7,388 | 23,520 | 480 of 7,388 chars — the "FGS is truncated" line, exactly |
| Heartfull | 7,528 | 31,582 | no |
| PAS Coder | 9,154 | 39,814 | no |
| Chess coder 2 | 9,914 | 49,695 | no |
That reproduces the transcript field for field: the two agents reported in full, the one always
"truncated, couldn't read full detail", and the three that "weren't shown". The agent was not being
lazy or repetitive — it was accurately reporting everything it had been given.
Mechanism — a budget applied to a quarter of the payload, then a cap the tool cannot see
Two individually-correct decisions compose into this.
1. The tool budgets itself, on the wrong object. summarizeSubordinates
(workers/api/src/lib/subordinate-observation.ts:74-165) trims work/run items oldest-first to
MAX_OBSERVATION_CHARS = 16_000 (:64) and sets a truncated flag. But it only ever sees
{instanceId, name, subscription, buckets, work, runs} (:146). Five more fields per subordinate —
connectivity, config, repo, acts — and a 3,634-character legend are concatenated
after the budget was applied (lib/connectors/supervision.ts:462-490), and the whole thing is
JSON.stringify(…, null, 2) — pretty-printed, so every line carries indentation. Measured on one
subordinate: runs 3,657 + acts 2,437 + connectivity 622 + config 579 + repo 387. The
budget governs 16,000 of a 60,239-character result.
2. The generic cap then cuts it, and the tool has no idea. capToolResult
(lib/tool-result-cap.ts:37,45-53) is a deliberate backstop at 24,000 chars, applied at the one seam
where a result re-enters the prompt (agent-think.ts:967). Its own docstring says it "can only ever
fire on a result nobody bounded" because it "sits above every deliberate per-tool cap … the largest
is the 20,000 of RESOURCE_MAX_CHARS". subordinate_status has a deliberate cap — 16,000 — and it
still overruns 24,000 by 2.5×, because the cap is measured on a different object than the one that
ships. Both numbers are defensible; together they silently drop half the roster.
There is a third, nastier consequence: the payload's own top-level "truncated": true (from
summarizeSubordinates) survives inside the visible head, so the model reads a truncation flag that
refers to dropped work items, while the actual loss is three entire agents it has no signal about
at all. It then has to infer "the rest was cut off" from the JSON simply ending mid-object — which,
to its credit, it did, every single time.
Second finding in the same transcript: it answers the question it can answer
Independent of truncation, the owner asked four times for a filtered answer and got the full
dump:
01:54 "Which agents are idling, doing nothing? Who is doing nothing?" → three paragraphs about the three that are running.
01:55 "You have to give me only those who are idling I asked you before" → a detailed paragraph about PAS Coder, which is running.
01:56 "give me the fucking number and the name of agent, nothing else" → 3 names plus a caveat plus an offer to re-query.
It did comply once, on 10 Aug 02:38 ("Total agents: 6. Idle right now: 0.") — so the behaviour is not
uniformly broken, and that turn is the model of what the others should have been. The compounding
factor is that when the answer is "0 idle", the agent fills the silence with the roster it does
have, which reads as ignoring the question.
Third finding: it cannot say which issue a subordinate is working on
01:31 owner — "Check what Heartfull agent is working on, I need to know exactly the issue there."
agent — "iteration 2 of 10 … working through open issues bugs-first"
01:32 owner — "I asked you what issue it's working on exactly. … Which issue it's working on?"
agent — "It hasn't reported which specific issue it's on yet — the status doesn't surface the current issue number until the agent logs it."
The Lead holds github_list_issues / github_read_issue, so it can list the queue; nothing in
subordinate_status carries the run's current work item. "Iteration 9/10" is the wrong unit for
"what is it doing" — and it is the only unit available.
Fourth: the uncommitted-file count contradicts itself across three turns
01:51 — "FAS … has 4 uncommitted files"
01:53 — "both repos are clean right now … the earlier uncommitted files were from previous runs"
01:54 — "FAS … Repo has 7 uncommitted files — leftover from earlier work"
Four minutes, three different confident answers. The owner asked the right question at 01:53 — "are
they uncommitted because they are working on them currently? Or because it's a leftover from
before?" — and the field carries a count with no timestamp or attribution, so the agent supplied the
explanation from nowhere. (repo.note exists in the legend; the provenance does not.)
What to do — cheapest first
- Route the roster question to the roster tool. The last turn proves
list_subordinatesanswers
"how many / which ones" instantly. A line in the supervision tool description — "for a count or a
roster, calllist_subordinates;subordinate_statusis for the state of specific agents" —
would have prevented every turn above. One string, no schema change. - Make the budget cover what ships. Apply the char budget in
supervision.tsto the final
object (afterconnectivity/config/repo/acts/legend), not to the intermediate one, and
set it belowTOOL_RESULT_MAX_CHARS. Dropnull, 2— pretty-printing a payload that is about to
be cut spends ~15% of the budget on whitespace no model needs. - Trim per-subordinate before dropping subordinates. The current trim drops oldest work items;
it should degradeacts/runsdetail across all six and keep one line per agent, because "six
agents exist and here are three in detail" is strictly better than "here are three agents". A
supervisor's roster must never be the thing that gets cut. limit/fieldsargument. Let the caller ask for a summary line per agent. The model asked for
exactly this itself: "Want me to re-query for the remaining agents specifically?"- Add the current work item to each subordinate's status (the objective, or the issue number the
run is on) — finding 3. - Timestamp the dirty-tree count so "leftover" vs "in progress" is data, not narration — finding 4.
Rejected: just raise TOOL_RESULT_MAX_CHARS. It is a global backstop protecting every tool's
share of the context window; raising it for one verbose tool spends everyone's budget and leaves the
same failure one subordinate later. The payload is the thing that is wrong, not the ceiling.
Rejected: shrink the legend. It is 3,634 chars of hard-won correctness (#259, #320, #329, #345
are all encoded in it) and it is 6% of the payload. It is not the problem.
Acceptance criteria
- With 6 subordinates,
subordinate_statusreturns a result underTOOL_RESULT_MAX_CHARS, and all 6
appear by name. A test constructs N=10 subordinates with fullacts/runsand asserts every name
survives. - When detail is dropped, the payload says which agents were summarised rather than ending mid-object.
- "How many agents do you supervise" is answerable in one tool call.
subordinate_statusreports the current work item for a running subordinate.
Regression risk
Changing the trim order changes what a Lead sees for a busy agent — the acts list is what #159/#183
added so a supervisor reports irreversible pushes unprompted, and the legend is emphatic that a
missing acts must never read as "it did nothing". If detail is trimmed, the payload must say
"trimmed", or this fix re-creates that exact bug. lib/connectors/supervision.test.ts and
subordinate-observation.test.ts cover the current shapes.
Verified: every quote (MCP instance_messages on 5fab318d…); the 60,239-char payload and the
per-subordinate offsets (live POST …/tools/subordinate_status, measured today); every file:line.
Inferred: nothing. Not established: whether the 2-space indent is load-bearing for any
consumer — I did not grep every reader of this tool's output.
Activity
serge-ivo commentedon Aug 12, 2026
Corroboration, and a note on attribution — while auditing "tools are missing in the coder and coder
lead agents" I read the Lead's full chat history. Most of what looks like a missing tool on the
Coder Lead is this issue. Adding the transcript because it shows the cost, and because it is now
the dominant failure in that conversation.
5fab318d-2850-45a4-982c-958765c7261e, 2026-08-11, six consecutive turns. The tool call succeedsevery time —
subordinate_statusruns, returns, and is cut:The last turn is the tell: the roster was available in one call the whole time.
list_subordinatesreturns a short list and answers "how many" exactly;
subordinate_statusreturns 60 KB into a 24 KBcap and answers it wrongly. The model reached for the richer tool because that is what its
description tells it to do — "Call this FIRST whenever you are asked about status, progress, or
what is happening" — and the truncation is invisible to it, so it reports the survivors as the
whole roster.
Also visible on 2026-08-10 (02:35–02:38): the same six-agent roster reported as three, then five
with "Chess coder 2 — status truncated; data cut off before it was returned", then a flat
"Total agents: 6. Idle right now: 0." that was wrong in the other direction.
Two things this adds to the fix as filed:
strategy ends up being,
subordinate_statusshould carry an explicittotal(and the list ofnames it dropped) before the per-agent detail, so a truncated result can still answer "how
many" and can say that it was truncated. Today the agent discovers the loss only by noticing an
agent it remembers is absent.
TOOL_RESULT_MAX_CHARS(lib/tool-result-cap.ts:38) isapplied to a result the model then reads as complete. Every reply above is the model faithfully
describing what it received. If the cap stays, the cut needs to be announced inside the result.
No separate issue filed for either; both belong here.
serge-ivo commentedon Aug 12, 2026
Correction to point 2 of my comment above: truncation is not silent — I claimed that without
reading the function, and it is wrong.
capToolResult(workers/api/src/lib/tool-result-cap.ts:46-54)appends an explicit notice naming both numbers and telling the model the rest was NOT read:
That is why the agent said "status truncated" and "I can only confirm 3 from the tool result" — it
read the notice and reported it honestly. Strike the "announce the cut" recommendation; it is
already done.
Point 1 survives, and is the sharper finding for it. The notice tells the model to ask for a
narrower slice, and for this tool there is no slice that answers the question:
subordinate_statustakesinstanceId(one agent) or nothing (all of them), so "how many agents doI have, and which are idle" has no cheap form — the agent's only move is N per-agent calls, which is
what the 01:53–01:56 turns show it doing. Hence the fix: put
totaland the names it dropped in thefirst bytes of the payload, so a truncated result still answers the roster question, and/or give the
tool a compact mode. The count must not live in the part that gets cut.
fix(trace): a chat turn's id is minted before the agent answers, so t…
feat(coding): a Repo Coder can find a file — and repo_tree and repo_g…
feat(feedback): a complaint about a turn lands in a table that outliv…
feat(feedback): a message can be reported from the bubble it is in, a…
feat(feedback): a recorded complaint gets you back to the conversatio…
feat(feedback): an agent told "you got that wrong" has somewhere to p…
fix(supervision): the roster fits, and the count rides in front of it…
fix(supervision): the summarised rung keeps the board counts instead …
serge-ivo commentedon Aug 12, 2026
Fixed and verified in production —
a001bac(the fix) and7f66324(one thing production showed me afterwards).Measured live on
5fab318d…, after the deployand the narrower slice, which is what the truncation notice was telling the model to ask for and
which had no answer before:
What changed
A budget over what actually ships.
lib/subordinate-payload.ts(new, pure) fits the FINALserialised object to 20,000 — below
TOOL_RESULT_MAX_CHARSby a margin, and below the 24,000 thistool already forced into every prompt, so the fix costs the context window less than the bug did.
MAX_OBSERVATION_CHARSstays as the upstream pre-bound on{work, runs}; it was never wrong, itwas just never the size of what shipped. Pretty-printing is gone (~15% of the budget in indent).
The count rides in front, and is never reduced.
asOf,total,roster,coverageare thefirst keys.
roster[]carriesactivity(working/idle) andcanWork— so "how many agents doI have" and, the second finding, "which ones are idling" are one field each.
activityis presentonly for agents the call actually observed: a call narrowed to one agent reads runs for one agent,
and reporting the other five as idle from a query that never asked would be the same class of
confident wrong answer.
Detail degrades uniformly instead of agents falling off the end. A ladder — full → trimmed →
brief → terse → summary — and the first rung that fits ships. Every shortened list carries a
…Omittedcount, and nothing is ever trimmed to empty, because the legend says emptywork/runsmeans IDLE; a list emptied by a budget reports a busy agent as doing nothing.
summarizeSubordinatesgot the same floor (it could previously empty a subordinate's work entirely).
The regression risk you flagged, handled explicitly. At the last rung each agent is one line,
and it keeps:
connectivity.canWork(#259),irreversibleActsas a count (#159/#183 — a countcannot be misread as "it did nothing" the way an absent list can),
latest(what it is on), theboard
buckets, andconfig.mergeAuthority(#339). Every other list becomes a…Countderivedgenerically, so a list added later is counted rather than silently absent (#468). The legend gained
two sentences saying where the count lives and what a
…Omitted/…Countmeans.7f66324is from reading the deployed output: the first cut reportedbucketsCount: 4— the numberof board COLUMNS, which cannot be wrong and cannot be useful. Buckets are already a summary, so they
now survive as themselves ("Needs you: 1, Running: 1, Failed: 5").
Against the acceptance criteria
subordinates (58 KB raw) and asserts
capToolResult(content) === content, i.e. untouched ratherthan merely under.
coverage.detailLevel,coverage.note, theper-list
…Omittedcounts and the per-agentsummarisedmarker. It never ends mid-object.total, first bytes, either tool.latestnow carries the running run'sobjective, which is closer, but "which issue is it on" is not in
agent_loop_runsat all — theloop does not record it. That is a write-side change (the Pilot recording its current item), not a
read-side one, and it belongs in its own issue rather than being smuggled in here.
repo.dirtyFilesstill has no timestampor attribution, so "leftover or in progress" is still unanswerable from the data. Same reason:
the probe reports a snapshot and nothing records when the files were touched.
Issue stays open for those two, or say the word and I will file them separately and close this.
Verified: full suite (7,774 tests),
pnpm typecheck(11 projects), biome, all six guard scriptsincluding the file-size ratchet,
pnpm docs:drift, both CI runs green, both API deploys green, andthe two live calls above. Not run:
pnpm test:e2e— this change touches nostore/, no console andno coder web UI.
fix(agents): a repo file read returns a line window that says which l…
fix(board): a delegation card states how many acts a run took, and ma…
fix(mcp): the two tools a host could not call now page their collecti…
serge-ivo commentedon Aug 15, 2026
Re-verified live today — the fix from
a001bac/7f66324is holding, and I made no code change here. Reporting rather than re-engineering, because the filed defect is delivered and the two items left open are the write-side ones the earlier comment deferred.Measured live, 2026-08-15, on Coder Lead
5fab318d…So the headline failure — "the Lead can only ever see 3 of 6" — is gone, and has stayed gone across the supervision changes that landed since (
2badcaf,b4d6ea3,dc68e2a). No regression.One thing I checked that was worth checking
The reply lands on the last rung of the ladder (
detailLevel: "summary") while using 13,021 of the 20,000-character budget — ~35% unspent. I traced this rather than filing it, and it is not a defect:fitStatusPayloadwalksLEVELSrichest-first and returns the first that fits, sosummaryshipping means every richer rung exceeded 20,000. The gap is thecollapsecliff betweenterseandsummary(lists become counts), andsubordinate-payload.ts:71-77already records that six subordinates of this size land onsummaryas a measured outcome. Nothing is being dropped silently —coverage.detailLevel, thenote, and the per-list…Count/…Omittednumbers all say so.Finer-grained rung selection (binary-searching
items/charsinstead of five fixed rungs) would recover that 35% as real per-agent detail. That is a separable improvement, not this bug, so I have not smuggled it in — say the word and I will have it filed.Also verified: the data layer underneath is bounded
lib/instance-work.tswas in scope for this. It is sound — every read is bounded twice: per-instanceLIMITs (8 tasks, 5 runs, 10 acts, chunked at D1's 5-term compound ceiling, #434) and per-field caps (title200,detail/objective300,command400). There is no unbounded collection here, which is why the 20,000 budget insubordinate-payload.tsis sufficient rather than merely lucky.Still open, unchanged from the earlier comment
agent_loop_runsdoes not record it. Write-side: the Pilot has to log its current item.repo.dirtyFilesstill carries a count with no timestamp or attribution, so "leftover or in progress" cannot be answered from the data.Both are write-side changes and belong in their own issues. Happy to file them and close this, or leave it open — your call.
fix(mcp): the trace a host is told to read first now pages, and the b…
fix(files): a document indexed to 100k characters says so, instead of…
serge-ivo commentedon Aug 18, 2026
Audit verdict: delivered — closing.
a001bac7(the roster fits and the count rides in front) +7f66324b(the summarised rung keeps the board counts).TOOL_RESULT_MAX_CHARS; a test with N=10 full records asserts every name surviveslib/subordinate-payload.test.ts:107"names all six agents, in a result the global backstop does not touch";:124"still names all ten when there are ten of them";:135"sits below the global backstop by construction":162"counts what it dropped, beside the list it dropped it from";:193"says how much detail survived, in the payload";:209a count that INCLUDES what an earlier pass droppedtotalandrosterare the first bytes and are never reduced;roster[]carriesactivityandcanWork, so "how many" and "which are idle" — asked four times in the transcript and never answered — are one field each:150"never empties a list that has something in it" — the floor exists precisely because the legend says an emptywork/runsmeans idle, so a list emptied by a budget would report a busy agent as doing nothingThe regression risk the issue named is handled and pinned:
:229"keeps the four things a supervisor cannot lose" —connectivity.canWork(#259),irreversibleActsas a count (#159/#183, so a missingactscan never read as "it did nothing"),latest, andconfig.mergeAuthority(#339).:245counts a list added later without being taught about it (#468).Detail now degrades uniformly down a ladder (full → trimmed → brief → terse → summary) rather than agents falling off the end. Rejected, with reasons recorded: raising
TOOL_RESULT_MAX_CHARS(spends every other tool's context share and moves the cliff one subordinate later), and shrinking the 3,634-character legend (it is #259/#320/#329/#345 and was never the problem).The issue's "not established" — whether the 2-space indent was load-bearing for any consumer — was resolved by shipping compact JSON; indentation was ~15% of a budget it was competing for.
fix(runner): a file reaches the Pilot — a tool result keeps its lines…
fix(agents): repo_git's cap is recorded as deliberate and repo_grep s…