Skip to content

fix(detectors): key repeats on the call, not the tool name; declare act-only surfaces - #8

Merged
hchittanuru3 merged 4 commits into
mainfrom
claude/github-issue-7-94fcf2
Aug 19, 2026
Merged

fix(detectors): key repeats on the call, not the tool name; declare act-only surfaces#8
hchittanuru3 merged 4 commits into
mainfrom
claude/github-issue-7-94fcf2

Conversation

@hchittanuru3

@hchittanuru3 hchittanuru3 commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

Fixes both detectors in #7. Neither was tunable from _CONFIG: both were the detector reasoning from a signal too coarse to tell flailing from correct fan-out.

1. REPEATED_TOOL groups on the call, not the tool name

Two keys, because a name-only Counter cannot separate a thrash from fan-out over N ids the input supplied:

  • arguments — calls that asked for the same thing. Order-insensitive over dict keys, recursive through nested containers. arguments=None collapses to one key per name, so an adapter that maps no input keeps exactly the old behaviour rather than silently losing the detector.
  • results — calls that got the same nothing, whatever they asked for. A thrash usually varies one id per attempt, so arguments alone would stop detecting the case the detector exists for. Every shape of empty ({}, [], "", None) shares one key.

The results arm is restricted to calls that errored, declined, or came back empty. Keying on results generally would have re-flagged a bulk write returning identical {"ok": true} per row, which is the fan-out this change exists to stop flagging; there is a test pinning that. The cost of the arm, stated in the docstring, is N searches in one turn that legitimately found nothing.

detail now carries basis ({"tool": ["arguments"]} / ["results"]) so a reader knows which key fired.

2. act_only_kinds and ACTED_SILENTLY

A third declaration for surfaces where a turn that acts and says nothing is a designed outcome, because "is there work here" and "does anyone need an answer" are separate decisions there. Those turns report ACTED_SILENTLY at INFO — countable rather than merely un-flagged — and work-then-silence stays EMPTY_REPLY everywhere else, so the real bug survives.

The field covers acting quietly, not being idle, which is what keeps it orthogonal to conversational_kinds. A kind is often both: a shared channel holds people who sometimes get an answer and also lets the agent file work without broadcasting. The two govern different turns on the same surface:

declaration acted, said nothing did nothing, said nothing
conversational only EMPTY_REPLY (fault) EMPTY_REPLY (fault)
act-only ACTED_SILENTLY (info) nothing
act-only + conversational ACTED_SILENTLY (info) EMPTY_REPLY (fault)
act-only + quiet ACTED_SILENTLY (info) GATE_FILTERED (info)

Row three is signal the old shape threw away: on a channel with people in it, a turn that did nothing and said nothing is still worth flagging. Unset, every existing path is byte-identical, and making the declaration never adds a fault.

Coverage

New ACTED_SILENTLY row, and REPEATED_TOOL now reports MISLEADING when no tool call carries arguments — the wiring where name-only keying makes fan-out read as thrashing.

Tests

All 15 pinning cases from the issue, plus guards for: identical-success fan-out, key-order-insensitivity, adapter degradation, both declarations on one kind, and declaring an act-only kind never producing a fault. 113 passed, ruff clean.

🤖 Generated with Claude Code

hchittanuru3 and others added 3 commits August 18, 2026 23:56
…ilent-work surfaces

REPEATED_TOOL counted calls per tool NAME, so one tool called over N distinct ids
read as thrashing when it was fan-out. It now groups on arguments, degrading to
name-only where an adapter maps none, plus a second group for N calls that got the
same nothing back — a thrash varies an id per attempt, so arguments alone would miss
the case the detector exists for. Identical success bodies stay clear: those are a
bulk write.

EMPTY_REPLY had no way to say a surface acts without answering. quiet_kinds cannot
express it, since it describes silence BEFORE work and asserts silence after work is
suspicious. silent_work_kinds declares the surface where acting and answering are
separate decisions; work-then-silence reports ACTED_SILENTLY at INFO there and stays
EMPTY_REPLY everywhere else. Silence without work keeps reporting GATE_FILTERED.

coverage() gains an ACTED_SILENTLY row and reports REPEATED_TOOL as MISLEADING when
no tool call carries arguments, which is the wiring where fan-out reads as a thrash.

Closes #7

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Running the tests through uv writes one, and a lockfile for a package with no
dependencies pins nothing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…onversational too

silent_work_kinds sat next to quiet_kinds using a synonym for a different rule, which
is unreadable. reply_optional_kinds says what the field asserts and pairs with
conversational_kinds.

Rejecting a kind declared both was wrong. conversational_kinds means a human is on the
surface, not that every silence there is a fault, and the two are routinely true at
once: a shared channel holds people who sometimes get an answer AND lets the agent act
without broadcasting. The rejection also forced a consumer to strip the kind from
conversational_kinds, and where it was the only entry that silently demoted every
EMPTY_REPLY in the report from FAULT to INFO.

The declarations now govern different turns on one surface: reply_optional_kinds the
ones that did work, conversational_kinds the rest. ACTED_SILENTLY is checked before the
reply-expectation gate, so declaring the kind is the whole statement.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@hchittanuru3 hchittanuru3 changed the title fix(detectors): key repeats on the call, not the tool name; declare silent-work surfaces fix(detectors): key repeats on the call, not the tool name; declare reply-optional surfaces Aug 19, 2026
…inds

reply_optional_kinds promised more than it delivered. It reads as "no reply is ever
owed here", but the declaration only covers turns that DID work: a turn on the same
kind that did nothing and said nothing is still an EMPTY_REPLY, deliberately, since an
agent that no-ops in front of people is the bug the detector exists for.

act_only_kinds names the same scope ACTED_SILENTLY observes, and carries no synonym
collision with quiet_kinds. Comments say which turns the field covers rather than
implying a blanket permission.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@hchittanuru3 hchittanuru3 changed the title fix(detectors): key repeats on the call, not the tool name; declare reply-optional surfaces fix(detectors): key repeats on the call, not the tool name; declare act-only surfaces Aug 19, 2026
@hchittanuru3
hchittanuru3 merged commit 0dd3a15 into main Aug 19, 2026
5 checks passed
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