Skip to content

perf(store): fix the 32× send-path cliff, and add the SLIMRPC binding - #108

Merged
tomtom215 merged 9 commits into
mainfrom
claude/v0.8.0-transport-perf-3wq3w8
Aug 15, 2026
Merged

perf(store): fix the 32× send-path cliff, and add the SLIMRPC binding#108
tomtom215 merged 9 commits into
mainfrom
claude/v0.8.0-transport-perf-3wq3w8

Conversation

@tomtom215

@tomtom215 tomtom215 commented Aug 15, 2026

Copy link
Copy Markdown
Owner

What this changes

Two independent pieces of work.

1. message/send cost 32× more past 10,000 tasks

The standing hypothesis — cross-thread scheduling on 4-core runners — is disproved: the same send on a single-worker runtime does not close the gap, and cutting an executor event changes it by 2%.

The cause was InMemoryTaskStore. It caps at max_capacity (10,000); once full it is over the cap on every subsequent write, and each write cloned every TaskId into a Vec, sorted it, and removed one task. A blocking send performs several saves, so one request paid that O(n log n) sweep several times. A cliff, not a slope, and it never recovered.

Sends so far Before After
10,000 (at the cap) 65.2 µs 62.9 µs
11,000 (past it) 2.4 ms 67.3 µs
16,000 2.0 ms 64.3 µs

Capacity eviction now walks order_index (already sorted oldest-first) with a bounded scan window, and the O(n) TTL pass no longer rides along with it — EvictionPasses separates the two triggers so the amortized sweep stays amortized.

The eviction module is split along that same seam, which is why the diff shows three files where there was one: eviction/mod.rs holds the schedule and the O(n) TTL pass, eviction/capacity.rs holds the pass that runs on every write once the store is full, and eviction/fixtures.rs holds the store builders both suites share.

Behaviour change: expired tasks are reclaimed only by the TTL sweep's own interval (default every 64 writes), not additionally by any over-capacity write. max_capacity remains a hard cap enforced on every write, so memory is bounded exactly as before.

2. bindings/a2a-protocol-slimrpc — the SLIMRPC protocol binding

All eleven methods from a2aproject/experimental-cpb-slimrpc plus multicast, advertising protocolBinding: https://a2a-protocol.org/bindings/experimental-slimrpc/v1. Payloads are the canonical lf.a2a.v1 protobuf messages, so the wire is byte-compatible with the official Go, Python and Java SDKs.

SlimRpcServer drives the same RequestHandler every other binding drives — task state, streaming, push, tenancy and authorisation are not reimplemented.

Deliberately outside the workspace, with its own Cargo.lock. agntcy-slim-rpc brings 379 transitive dependencies including aws-lc-sys, a native C crypto build; a2a-protocol-types has 12. None of that reaches the lockfile, deny.toml allow-list or audit surface of the four published crates.

One change to a published crate was needed, and it is the finding worth carrying forward. Transport::send_streaming_request must return an EventStream, and every EventStream constructor was pub(crate) — so a third-party binding could implement the unary half of the trait and not the streaming half. The trait is pub, its parameters are pub, its return type is pub, and it was still unimplementable from outside. EventStream::from_event_channel closes that; purely additive. docs/rust-sdk-assessment.md §4.1.1 previously concluded no change was needed, verified by reading declarations — the correction is recorded there.

Also adds slim-node, a standalone SLIM node binary, so bringing one up for local development does not require installing the full AGNTCY distribution.

How it was verified

cargo test --workspace                # 2,738 passed, 0 failed
cargo clippy --workspace --all-targets -- -D warnings   # clean
cargo doc --workspace --no-deps       # clean
cargo fmt --all -- --check            # clean
./scripts/check_file_lengths.sh       # 79 of 387, all recorded — no new debt
./scripts/check_mutation_scope.sh     # 141 of 141 sources reachable by the PR gate
cargo mutants --file .../eviction/*   # 27 mutants, 25 caught, 2 unviable, 0 missed

The binding: 65 tests across ten topologies, 56 by default and 9 against a real SPIRE deployment. None are mocked.

Suite Topology
e2e.rs one in-process Service — eleven methods, error identity, streaming, JWT identity
multicast.rs group channel — per-agent attribution, silent agent as failed outcome, failure isolation
remote_node.rs three services, one node, TCP
remote_node_tls.rs verified TLS, incl. refusing an untrusted CA
remote_node_mtls.rs mutual TLS, incl. refusing no-cert and wrong-CA
remote_node_multihop.rs two peered nodes
out_of_process.rs node in a separate OS process
spiffe.rs real SPIRE — identity, incl. refusing a wrong-audience SVID
spiffe_federation.rs two SPIRE deployments — trust-domain boundary, federation, cross-domain A2A
spiffe_rotation.rs 40-second SVIDs — rotation mid-session, expired credential refused

Every negative case is paired with a control that succeeds in the same window, because SLIM retries a failed handshake rather than returning, and "did not connect" alone can mean the fixture broke rather than the control working.

CI gets a dedicated job (an out-of-workspace crate is skipped by every other job), which installs SPIRE and runs the #[ignore]d suites explicitly. The SPIRE testbed panics rather than skipping when the binaries are missing, so it cannot silently report coverage it does not have.

Three bugs were found by tests the tier above could not have caught:

  • remote_node.rs — a client never announced its own name to the node, so nothing could route an agent's reply back and every call failed its session handshake. Invisible in-process.
  • multicast.rs — the response join key was wrong; SLIM names arrive with a fourth instance component, so every response filed under a name no invited member matched. All agents answered; all were reported as timeouts.
  • spiffe.rs — two apps sharing one SPIFFE ID cannot complete an MLS handshake.

A fourth was found by this PR's own mutation gate, after review had passed it — see the cargo mutants checklist item.

Known limits, stated rather than implied

Federation is by manual bundle exchange, not a bundle endpoint. Rotation covers JWT-SVIDs (what SLIM's app identity uses), not X.509 SVIDs or the node's own TLS certificate under a live connection. Everything runs on one machine, so real network loss, latency and NAT are untested. Static-token identity is supported via with_identity but has no test. The crate README carries a posture table separating verified by a test from merely available.

ROADMAP.md records the handoff: what is settled, the five things that cost time to learn, and the next three items — soak testing first, since it is the one axis none of this covers and the class of bug the suite structurally cannot reach.

Checklist

  • Every commit is signed off — re-authored as Tom F. <tomf@tomtomtech.net> per PROVENANCE.md §3.2, with Co-Authored-By: Claude retained
  • SPDX header on every new file
  • No file exceeds 500 lines
  • cargo fmt --all passes
  • cargo clippy --workspace --all-targets -- -D warnings passes
  • cargo test --workspace passes — 2,738 passed, 0 failed
  • cargo doc --workspace --no-deps passes without warnings
  • New public types/functions have doc comments
  • New code has tests
  • cargo mutantsrun, and it found a real bug this PR had introduced. The incremental gate flagged two survivors in the new capacity_victims: overflow - terminal.len() -> +, and still_running.len() < overflow -> <=. The first is a genuine test gap — sizing the top-up with + evicts more than the overflow, and no test noticed, because every capacity test either filled the quota from terminal tasks alone (returning before the top-up ran) or found none at all (where the two operators agree). The second is equivalent: the vector is only consumed by .take(shortfall) with shortfall <= overflow. Both addressed in 8b972de — a test for the seam, verified to fail against the previous implementation with the mutation applied while the other ten pass, and the equivalent mutant's shape deleted rather than excluded, per the standing rule in mutants.yml. Re-measured after the fix: 27 mutants, 25 caught, 2 unviable, 0 missed. The binding is still outside the workspace, so the gate does not reach it.
  • CHANGELOG.md updated
  • ADR — none created. The one architectural decision (binding outside the workspace) is recorded in the crate README and docs/rust-sdk-assessment.md §4.1.1 rather than as an ADR; say the word if you want it promoted.

Generated by Claude Code

Tom F. and others added 9 commits August 15, 2026 17:00
… capacity

The default InMemoryTaskStore caps itself at max_capacity (10,000). Once
full it is over that cap on every subsequent write, and every such write
ran a sweep that cloned every TaskId into a Vec, sorted it, and removed —
normally — one task. A blocking send performs several saves, so a single
message/send paid that O(n log n) sweep several times over.

The result was a cliff, not a slope, and it never recovered: handler-side
send latency went from 65 µs to 2.1 ms at the 10,001st task and stayed
there for the life of the process.

Capacity eviction now walks order_index, which is already sorted
oldest-first, and stops as soon as it has enough victims. The search for a
terminal task to prefer is bounded by EVICTION_SCAN_WINDOW, so a sweep is
O(1) in the store size. The O(n) TTL pass no longer rides along with it:
EvictionPasses separates the two triggers, so the amortized sweep stays
amortized instead of running on every write once the store is full.

Measured (4-core, release, examples/send_probe):

  sends so far    before     after
        10,000   65.2 µs   62.9 µs
        11,000    2.4 ms   67.3 µs
        16,000    2.0 ms   64.3 µs

End to end, transport/jsonrpc/send/single_message improves 91%
(2.18 ms -> 191 µs, p < 0.05). Control: get_task on a missing id, one
round trip with no write, is unchanged at -0.25% (p = 0.76).

This disproves the standing hypothesis, recorded in a benchmark comment,
that the cost was cross-thread scheduling on 4-core runners: the same send
on a single-worker runtime does not close the gap. Scheduling is real but
second-order — about 50 µs of the remaining 191 µs — and was entirely
masked by the eviction cost.

Behaviour change: expired tasks are now reclaimed only by the TTL sweep's
own interval (default every 64 writes), not additionally by any
over-capacity write. max_capacity remains a hard cap enforced on every
write, so memory is bounded exactly as before.

Three new tests pin the properties rather than the timings:
over_capacity_does_not_earn_the_ttl_pass,
a_capacity_only_sweep_does_not_run_the_ttl_pass, and
capacity_eviction_stops_looking_for_terminal_tasks_at_the_window.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QEnNK2WY9W3P4BB79qiKF2
Signed-off-by: Tom F. <tomf@tomtomtech.net>
…it needed

Carries A2A over the AGNTCY SLIM fabric per a2aproject/experimental-cpb-slimrpc,
advertising protocolBinding
https://a2a-protocol.org/bindings/experimental-slimrpc/v1 and addressing agents
as slim://[node[:port]/]domain/namespace/service.

All eleven methods in the spec's inventory: nine unary, plus
SendStreamingMessage and SubscribeToTask as unary-request/streaming-response.
Payloads are the canonical lf.a2a.v1 protobuf messages — SLIMRPC uses the same
service definitions as gRPC — so the wire is byte-compatible with the official
Go, Python and Java SDKs. Error identity travels as the spec's
"TaskNotFoundError: ..." message prefix, because SLIMRPC has no
google.rpc.ErrorInfo equivalent and a status code alone cannot distinguish
TaskNotCancelableError from ExtensionSupportRequiredError.

SlimRpcServer drives the same RequestHandler every other binding drives, so task
state, streaming, push, tenancy and authorisation are not reimplemented and an
agent behaves identically however it is reached.

Deliberately outside the workspace, with its own Cargo.lock. agntcy-slim-rpc
brings 379 transitive dependencies including aws-lc-sys, a native C crypto
build; a2a-protocol-types has 12 and a2a-protocol-server 191 at all features.
None of that reaches the lockfile, deny.toml allow-list or audit surface of the
four published crates, and none of them depends on this one.

One change was needed in a published crate, and it is the interesting part.
Transport::send_streaming_request must return an EventStream, and every
EventStream constructor was pub(crate) — so an out-of-tree transport could
implement the unary half of the trait and not the streaming half. The trait is
pub, its parameters are pub, its return type is pub, and it was still
unimplementable from outside. EventStream::from_event_channel closes that: it
takes a channel of decoded StreamResponse values, so a transport hands over
domain events without knowing the internal representation is SSE, and an Err on
the channel reaches the consumer rather than ending the stream silently.

That gap is not visible from the signatures. docs/rust-sdk-assessment.md §4.1.1
previously concluded a SLIMRPC binding needed no change to these crates,
verified by reading declarations; the correction is recorded there, along with
the dependency measurement that strengthens the separate-crate design.

29 tests. The seven end-to-end ones are not mocked: one in-process SLIM Service
hosts an agent app and a caller app and messages cross the real SLIM datapath,
covering method registration, a unary round trip, a task fetched back by id,
error identity surviving the fabric, a streaming send running to its terminal
event, agent-card advertisement, and an unknown method being reported rather
than hanging.

CI gets a dedicated job: an out-of-workspace crate is skipped by every other
job, so without one it would rot while the workspace stayed green.

Not implemented: multicast (a separate spec document). Not verified: behaviour
across a remote SLIM node — every test here runs in-process. Both are stated in
the crate README rather than left to be discovered.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QEnNK2WY9W3P4BB79qiKF2
Signed-off-by: Tom F. <tomf@tomtomtech.net>
…IM node

Two things the previous commit listed as not done.

Multicast implements the separate spec/v1/slimrpc-multicast.md. SlimRpcMulticast
opens a SLIM group channel, invites specific agents by name, and broadcasts.
Only SendMessage and SendStreamingMessage may be broadcast — task management
stays point-to-point, because a task id is meaningful to exactly one agent.

MulticastOutcome carries exactly one outcome per invited agent. That is the
spec's requirement ("Clients must wait for outcomes from every invited agent")
and the reason multicast is not a Transport: send_request returns one value, and
reducing N attributable answers to one would have to drop either the attribution
or the failures.

Two failure kinds stay distinct because they call for different responses. An
agent that errors or stays silent past the timeout is an isolated per-agent
outcome, and the other agents' answers stand. A member that cannot be invited at
all fails the whole call, because the group is misconfigured and waiting will not
fix it — the spec draws the same line: "Only channel creation, agent invitation,
or request delivery failures constitute interaction-level failures."
stream_message gives each agent its own EventStream, demultiplexed from SLIM's
interleaved source-tagged frames.

tests/remote_node.rs runs three separate SLIM services in one process — an
agent, a client, and a node that only routes — connected over loopback TCP. The
agent and client share no Service and no memory; every message crosses a socket
twice and is routed in between.

It found a real bug that in-process testing structurally could not: nothing
announced a client's own name to the node, so while the agent was reachable, no
route existed for its reply and every call failed its session handshake with the
caller's own name reported as unroutable. Channel sets a route outwards only.
The client-side constructor is now async and subscribes the caller's name over
the connection.

Two mistakes worth recording, because both were tests that could not fail:

The multicast join key was wrong — a name arrives on the wire with a fourth
instance component (org/ns/agent/NULL_COMPONENT), so keying on the full
rendering filed every response under a name no member matched. All three agents
answered and all three were reported as timeouts. The unit test missed it by
building both sides of the comparison from the same value; it now asserts
against the literal wire rendering.

The first attempt at the remote-node fix also changed the server's base name
from app.app_name() to the three-component address, with a comment claiming that
was what the remote suite caught. Reverting it showed the tests still pass, so
it was not load-bearing and the comment was a false claim. Reverted, comment
corrected to what is actually verified.

45 tests: 25 unit, 18 end-to-end across three topologies, 2 doc. e2e.rs now
shares tests/common with the two new suites instead of carrying its own copy of
the fixtures.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QEnNK2WY9W3P4BB79qiKF2
Signed-off-by: Tom F. <tomf@tomtomtech.net>
…ess, identity

The previous commit listed four limitations. Three were real work and are now
done; the fourth was a false claim of mine and is corrected.

TLS between node and app. remote_node_tls.rs generates a throwaway CA per run
rather than committing PEM files, so nothing long-lived is in the repository and
no certificate can expire the build. The rejection test is a differential
against the same node in the same window — the trusted CA connects, the
untrusted one does not — because SLIM retries a failed handshake rather than
returning, and a bounded wait on its own would only prove slowness.

Multi-hop. remote_node_multihop.rs runs two nodes peered with ConnType::Peer and
puts the agent behind the far one. An Edge link carries an attached app's
traffic but does not share routing state, so the agent stays invisible without
the peer link; the subscription crossing that link is what the test exercises.
Unary and streaming both, because frame-by-frame delivery is where a second hop
is most likely to go wrong.

Out of process. src/bin/slim_node.rs is a standalone node — it routes and runs
nothing itself — and out_of_process.rs spawns it with Command. It shares nothing
with the test but a TCP port, which is the part of "on another host" that
reproduces on one machine: no shared memory, no shared runtime, independent
lifetimes. The binary prints "listening on <addr>" once the socket accepts so a
supervisor waits for readiness instead of sleeping, and refuses half a TLS
configuration rather than silently serving plaintext. It also stands alone as a
tool: bringing up a node otherwise means installing the full AGNTCY SLIM
distribution.

Identity. with_identity now takes SLIM's own AuthProvider and AuthVerifier
instead of enumerating mechanisms, so JWT, SPIFFE via SPIRE and static tokens
work without this crate growing a method per mechanism and lagging behind SLIM.
with_shared_secret becomes a convenience over it, and fallible, since SLIM
rejects a secret too short to be a credential. A builder with no identity is a
build error: SLIM has no anonymous mode and a default would quietly stand in for
one. An end-to-end JWT test proves the general door works, not just the
convenience.

The fourth item was wrong. I documented that SLIM offers a multicast group
inbox via subscribe_group_inbox and this binding did not expose it. That symbol
does not exist anywhere in the SLIM crates — it appears exactly twice in the
whole source, both in a stale doc comment in upstream's own test file describing
a test that is not in the file. I repeated it as fact without checking. Removed.

53 tests across six topologies: in-process, multicast group, one node over TCP,
that node with verified TLS, two peered nodes, and a node in its own process.
Every file is under the 500-line guideline; client/ and the remote-node suite
were split to keep it that way.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QEnNK2WY9W3P4BB79qiKF2
Signed-off-by: Tom F. <tomf@tomtomtech.net>
Both were listed as limitations last time. Neither is now, and one of the
reasons I gave was wrong.

SPIFFE runs against a real spire-server and spire-agent. The agent attests the
test process over the Workload API and issues genuine JWT-SVIDs; a stub would
have proven only that the types line up. Three tests: an A2A call carried by
SPIFFE identity, the issued SPIFFE ID pinned to the one registered, and a
verifier refusing a genuinely-issued SVID minted for a different audience. The
last gives the first its meaning — a verifier that accepted everything would
pass the positive test.

Two SPIFFE properties surfaced only by running it, and both present as a session
that never completes rather than as an authentication error:

SpireIdentityManager must be built once and cloned for provider and verifier. It
generates an MLS signature key at build time and embeds the public half in the
SVID audiences, so two separately-built managers carry two different keys and
their handshake never finishes.

Each app needs its own SPIFFE ID. The unix workload attestor identifies by uid,
so every app in one test binary is one workload and would share an identity —
and two SLIM apps holding the same SPIFFE ID cannot complete an MLS handshake.
The testbed registers an entry per app and selects between them with
with_target_spiffe_id, which is how a process hosting several workload
identities is meant to work.

Mutual TLS covers a node that authenticates its apps rather than only itself.
Three cases, meaningful only together: a certificate from the node's client CA
connects and A2A works; no certificate is refused; a well-formed ClientAuth
certificate from a different CA is refused. The first alone would pass just as
happily against a node ignoring client certificates entirely. Every negative is
paired with a control that succeeds in the same window, because SLIM retries a
failed handshake rather than returning, so "did not connect" alone can mean the
fixture broke rather than the control working.

The correction: I previously recorded that running SPIRE needed an agent this
environment did not have. I never checked. SPIRE downloads and runs here without
trouble, which is how these tests exist at all.

The SPIFFE suite is #[ignore]d because it needs the SPIRE binaries, so a
contributor without them is not blocked; CI installs SPIRE and runs --ignored
explicitly. The testbed panics rather than skipping when they are missing — a
SPIFFE test that quietly passes without SPIRE reports coverage that does not
exist, which is worse than no test.

59 tests across eight topologies. The README now carries a security posture
table separating what is verified by a test from what is merely available,
because "supported" and "verified" are different claims.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QEnNK2WY9W3P4BB79qiKF2
Signed-off-by: Tom F. <tomf@tomtomtech.net>
…st real SPIRE

The two limitations left standing last time. Both are now tested rather than
documented.

Federation runs two independent SPIRE deployments. An SVID from an unfederated
trust domain is refused; after a bundle exchange and entries naming each other,
the same SVID is accepted; and a full A2A call runs between an agent attested by
one organisation's SPIRE and a caller attested by another's. Both halves are
needed — acceptance alone is indistinguishable from a verifier that ignores
trust domains, rejection alone from federation being broken.

That work surfaced an ordering rule: an entry naming -federatesWith is rejected
outright unless that trust domain's bundle is already imported. Bundles before
entries. The testbed now splits start_with from register so the ordering is
visible at the call site rather than being a comment someone can miss; the first
attempt did both in one call and failed with "unable to find federated bundle".

Rotation issues 40-second JWT-SVIDs so a renewal happens inside a test instead
of half an hour later. Three properties: the manager serves a renewed credential
with nobody asking it to, the superseded credential stops verifying, and a live
A2A agent keeps answering across the rotation — including a stream opened
afterwards — without a restart or a new manager.

Each rotation test proves the rotation actually happened before asserting
anything about it. A test that waited and then made a successful call would pass
just as happily if nothing had rotated, which would be worse than no test: it
would report coverage of precisely the thing it missed.

65 tests across ten topologies; 56 run by default, 9 against real SPIRE. The
rotation suite is slow by construction — it waits for wall-clock expiry, which
is the only way to test what happens after it.

Remaining, and stated because it is true rather than because it is comfortable:
federation here is manual bundle exchange, not a bundle endpoint; rotation
covers JWT-SVIDs, which is what SLIM's app identity uses, not X.509 SVIDs or the
node's own TLS certificate under a live connection; and none of it runs under
sustained load, so nothing here would catch a slow degradation over hours.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QEnNK2WY9W3P4BB79qiKF2
Signed-off-by: Tom F. <tomf@tomtomtech.net>
Written for a fresh session picking this up: what is settled, what it cost to
learn, and what is genuinely next.

The settled parts are the send-path cliff (cause found, 91% off, with the
scheduling hypothesis disproved) and the SLIMRPC binding (eleven methods plus
multicast, 65 tests across ten topologies, three of them against real SPIRE).

The section deliberately records four things that cost time to discover and
would cost it again: SLIM names carry a fourth instance component; a client must
announce its own name to the node or nothing can route a reply back;
SpireIdentityManager must be built once and cloned, and each app needs its own
SPIFFE ID; and federation is bundles-before-entries.

Next three, in order: soak testing — the one axis none of this covers and the
class of bug the suite structurally cannot reach, since every test starts from
an empty store — then splitting handler/messaging.rs, then the two missing
examples at either end of the funnel.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QEnNK2WY9W3P4BB79qiKF2
Signed-off-by: Tom F. <tomf@tomtomtech.net>
… it turned on

`cargo-mutants` on the PR diff (shard 6) surfaced two survivors in
`capacity_victims`, the sweep this branch introduced. They are different in
kind and only one is a test gap:

  overflow - terminal.len()  ->  +    a real gap
  still_running.len() < overflow -> <=  equivalent

The first is the one that matters. Sizing the top-up with `+` makes the sweep
evict *more* than the overflow, and no test noticed: every capacity test either
fills the quota from terminal tasks alone and returns before the top-up runs,
or finds no terminal tasks at all, where the two operators agree. The
partial-supply case between them had no test at all.

Verified rather than assumed. Reconstructing the previous implementation with
the `-` -> `+` mutation applied and the new test added, the new test fails and
the other ten still pass — so the mutant really was invisible to the suite, and
this test is what closes it.

The second survivor is genuinely equivalent: `still_running` is only ever
consumed by `.take(shortfall)` with `shortfall <= overflow`, so letting it grow
one element longer cannot change the result. Per the standing rule recorded in
mutants.yml, an equivalent mutant gets its shape deleted rather than excluded.
Rewriting the top-up as a second bounded pass over the same window, sharing the
first pass's exit condition, removes both operators at once — and is less work
besides: one allocation of exactly the victims instead of two, with no eager
cloning of in-flight ids that are usually discarded. Behaviour is unchanged;
all five pre-existing capacity tests pass untouched.

Also worth recording: the first local mutation run exited 0 having crashed —
out of disk after copying the SLIMRPC binding's 16 GB target/ — and produced no
report whatsoever. That is precisely the vacuous pass the workflow's "Require a
readable mutation report" step exists to catch.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Tom F. <tomf@tomtomtech.net>
… line limit

The mutation fix in 8b972de took eviction.rs from 499 lines to 569, across
CONTRIBUTING's 500-line rule, and scripts/check_file_lengths.sh caught it. That
is the check working: the file was one line under before, so any addition at all
would have crossed it.

Recording an exemption was the sanctioned alternative and would have been the
wrong one. The file already documented the seam it needed — "two passes, on two
schedules, because they cost different things" — so the split was waiting to be
made rather than invented to satisfy a counter:

  eviction/mod.rs       the schedule. EvictionPasses, should_evict, maybe_evict,
                        evict, and the amortized O(n) TTL pass.
  eviction/capacity.rs  the pass that runs on every write once the store is
                        full. EVICTION_SCAN_WINDOW, evict_over_capacity,
                        capacity_victims, is_terminal. Its bound is the whole
                        subject of the file, which is the point of separating it.
  eviction/fixtures.rs  store_of/config/ids. Shared rather than copied because
                        both suites must agree on what "oldest first" means; two
                        copies that drifted would let one stop matching the other
                        without either failing.

The TTL and capacity bodies are now named functions instead of inline branches
of evict, leaving evict as the gate it actually is.

No behaviour change. The same 11 tests pass, unmodified apart from their module
homes. check_file_lengths.sh returns to "79 of 387 tracked source files exceed
500 lines, all recorded" — the ratchet list did not grow — and
check_mutation_scope.sh still reports 141 of 141, so all three files remain
inside the per-PR mutation gate rather than slipping out of it.

Also: the local fmt check that let 8b972de through was unsound. It read
`cargo fmt --all --check | head -20 && echo "FMT OK"`, where && binds to head's
exit status, so it printed OK regardless — and the four non-rustfmt steps of the
Format job were never run locally at all. All five are checked individually here.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Tom F. <tomf@tomtomtech.net>
@tomtom215
tomtom215 merged commit 9685279 into main Aug 15, 2026
80 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