feat(tools): break a failed tool for the rest of its turn #1007
No reviewers
Labels
No labels
move-to-repo
coilyco-bridge-deploy
move-to-repo
coilyco-flight-deck-agent-compose
move-to-repo
coilyco-gaming-eco-app
move-to-repo
coilysiren-inbox
move-to-repo
unknown
🔒⚠️📦⚠️🔒 SANDBOXED 🔒⚠️📦⚠️🔒
autonomy
async-consult
autonomy
epic
autonomy
headless
autonomy
live-collab
c#
priority
P0
priority
P1
priority
P2
priority
P3
priority
P4
role/ai
role/creator
role/design
role/director
role/engineer
role/exec
role/human
role/ops
role/qa
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
coilyco-gaming/sirens-echo!1007
Loading…
Reference in a new issue
No description provided.
Delete branch "aos/claude/tool-breaker"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes #943. Implements the breaker that issue specified, and settles the question it left open.
The question #943 could not answer from telemetry
It measured
configured: 12, listed: 0, reached: 0, cached: trueon 21 of 25 listings and asked whether that is "a cache that stores an empty roster and serves it, or a counter that is not written on the cached path."Neither.
reachedcounts servers that took a round trip, and it is legitimately zero in two unrelated states: every server warm, and every server backing off.mcp.tools.cachedis derived fromreached == 0, so it readstruefor both. On the warm pathOpenregisters fromentry.toolsand the model gets the full roster. That reading matches the one the engineer seat posted on #939, and I confirmed it against the code rather than inheriting it.So the 21 spans are healthy warm turns, not a collapse. The instrument was the defect.
Fixed by emitting
mcp.tools.unavailableandmcp.tools.registeredbesidereached.cachedis kept rather than replaced, so existing queries do not silently break. A warm roster now readsunavailable: 0, registered: 86; a dead one readsunavailable: 12, registered: 0. A test builds both states against a real in-process MCP server and asserts they differ.Still unexplained, and I am not claiming otherwise: rounds 9 and 11 of trace
6d1efdd560dd868db1fdbea1394cd3cdoffered 0 tools while the turn continued. Both states above are accounted for and neither is that one, so #943's first finding survives this change with better instruments pointed at it. Whoever picks it up next will be able to readregistereddirectly instead of inferring fromcached.The breaker
Per tool, per turn, on the session, which is already per-turn. Once a tool returns a result with
IsError, the next call to that same tool returns the recorded error with a line saying the call was skipped.outcomeOfatfailed, so the disclosure footer, the metric, andmcp.tool.outcomeall stay consistent with what the model was told.A transport error needs no breaker: it ends the turn, so it cannot repeat.
Verification
just gatepasses: build, policy-check, vet, test, test-skips, pre-commit.go test -race ./...clean, which matters because the breaker adds mutable per-turn state.Five new tests. Non-vacuity checked by reverting the breaker rather than assumed, and the fixture is built so the proof is unambiguous: the registered tool has a nil session, so a dispatched call panics.
With the breaker in place it returns cleanly. Surviving the call is the evidence no call was made.
Not done, deliberately
The across-turn half. #943 asks for a server with a standing failure record to be marked unavailable rather than called, and that is what stops Deep rediscovering the playwright outage every turn. I did not build it, because it is a bigger design than it looks: the roster caches
entry.toolsfrom a previous successful listing, so a tool stays offered after its session dies, and fixing that properly means deciding when a server is re-probed and how a recovery is noticed. That is not a change I would make 15 hours before a live recording.The within-turn breaker takes the observed cost from six dead calls to one. The across-turn one takes it from one per turn to zero. The first is most of the win and all of the safety, so I would rather land it and leave the second specified than half-build both.
Doc note, same as #1003 and #1001
docs/sirens-echo-tools.mdwas at 7993 of its 8000-char cap, so the breaker gets one sentence and the reasoning lives here. I compressed three sentences elsewhere on the page to fit it, without dropping a fact. That is the fourth page in a row I have hit this on;sirens-echo-mcp.mdis at exactly 8000. Worth a band decision rather than more shaving.