You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
From the #2032 investigation (record); this is the prerequisite for every budget in #2033.
Problem
pkgs/id has no per-peer counters. The five existing limiters are keyed by file name, world capability, e-mail address or nonce — none by the remote endpoint public key — and there is no byte accounting anywhere. "Max MB per window, per IP or per ID" therefore cannot be evaluated at all today, and a rate refusal is a prose string with no retry value ("chat rate exceeded; try again later", world/core.rs:955-975).
Work
Per-(local node, remote endpoint key) counters for frames and bytes, incremented at the frame read/write boundary in pkgs/id/src/frames.rs before decode.
Token bucket over those counters (defaults from the record: 60 frames/s burst 180, 512 KiB/s burst 4 MiB authorized; 10/s burst 30 before authorization; 4 connections per peer).
One structured refusal type carrying reason (rate_limited | budget_exhausted | blocked_by_operator | unauthorized) and retry_after_ms (1 s floor, 60 s cap), replacing the prose refusals; HTTP routes map it to 429 + Retry-After.
Counters observable (metrics or id peers), so an operator can tune what they can see.
Done when
A peer exceeding the bucket is refused with a machine-readable retry value, the counters are visible, and no existing limit is duplicated — the world chat limiter becomes a consumer of the same bucket.
Verdict: CONFIRMED at origin/main (064ce9e). Every claim in the issue checks out against current code; the viable route is the #2033 design's P0-A row (PR #3834, doc/2026-11-01_design_id2033.md, branch design/id-2033 — still OPEN, not yet on main).
Claim-by-claim:
No per-peer counters exist. The only rate structures in pkgs/id/src are SaveRateLimiter (web/mod.rs:95, keyed by file name) and the world chat limiter (world/core.rs:38chat_rates: HashMap<u64, VecDeque<Instant>>, keyed by world participant id). No counter is keyed by remote endpoint public key, and there is no byte accounting anywhere. Confirmed.
The frame boundary is the right charge point, and it is uncharged today.frames.rsread_frame (L150–165) reads the 4-byte length, enforces the 1 MiB cap (MAX_FRAME_BYTES, L16–20), then allocates the body — the design's charge point (after length prefix, before allocation/decode) exists as a clean seam. Writers all funnel through encode_frame/write_frame (L27–39). Confirmed.
Rate refusal is prose, with no retry value.world/core.rs:1000 — "chat rate exceeded; try again later" (the issue cited L955-975; the line has drifted to 1000). The web save path returns 429 with a prose body and no Retry-After header (web/routes.rs:1218). Several localRefusal enums exist (event_store/ingest.rs:109, membership/layer.rs:58, groups/fold.rs:44, directory.rs:94; directory_auth.rs's RateLimited carries a prose String) — none is a shared structured rate/budget refusal with reason + retry_after_ms. Confirmed, with that nuance: the work is one shared contract, not another local enum.
Implementation route — two phased, independently mergeable packets, enqueued on queue repo-health-code-pilot:
Packet A (core) — dedupe repo-health:v1:fix:developing-today/code:issue:2244:p0a-core: shared abuse evaluator module — typed subjects (endpoint / connection), token buckets (frame + byte) with the defaults above, one structured refusal type (rate_limited | budget_exhausted | blocked_by_operator | unauthorized, retry_after_ms bounded 1 s–60 s), and a counter registry with a snapshot API for observability. No behavior change to existing paths.
Packet B (integration) — dedupe repo-health:v1:fix:developing-today/code:issue:2244:p0a-integration (after A): charge counters at the frames.rs seam with the peer's EndpointId threaded from the adapters; HTTP mapping 429 + Retry-After at the existing 429 sites (web/routes.rs, web/world_ws.rs:350); world chat limiter becomes a consumer of the same bucket and returns the structured refusal; counters surfaced (id peers / metrics).
Invariants the implementation must hold (from #2033): a frame is charged before body allocation/decode; the 1 MiB hard cap is retained, not duplicated; endpoint allowance is shared across connections; a refusal sheds the request/stream, never the whole peer connection; semantic caps (chat, save) keep their semantics — they consume the shared accounting, they are not relabeled as transport frames.
Drew questions: none.#2033 records "None" for its open choices (scoped defaults chosen; relay-attested IP deferred as a separate privacy decision), and nothing in this issue needs a Drew ruling to start Packet A.
The dedupe keys named in the triage record above used phase suffixes the queue's key grammar does not accept, so the phases were filed as their own issues (the fleet's established pattern) and the packets enqueued against them:
Verdict: implemented and merged at main 1e3763fa; closing with evidence. The prior triage (task 5839/5907) scoped the work and routed the phases; both phases and the design record have since landed, and this pass verified the shipped code rather than re-planning it.
Claim-by-claim at refs/remotes/origin/main (1e3763fa):
Token buckets with the specified defaults — done.AbusePolicy::default() (abuse.rs:248) is authorized 60 frames/s burst 180 and 512 KiB/s burst 4 MiB, pre-authorization 10/s burst 30, max_connections_per_peer: 4. Endpoint allowance is owned by the endpoint, so extra connections do not multiply it (world_net.rs shares one evaluator per remote EndpointId).
One structured refusal with a bounded retry, prose replaced, HTTP 429 + Retry-After — done.abuse::Refusal / RefusalReason { rate_limited, budget_exhausted, blocked_by_operator, unauthorized } with retry_after_ms clamped to 1 s–60 s (none for operator blocks); JSON body + Retry-After at web/routes.rs:1326 and web/world_ws.rs:491-506. The world chat limiter is now a consumer of the same evaluator (world/core.rschat_abuse, burst/rate preserved); the prose string is gone (git grep "chat rate exceeded" → no hits).
Counters observable — partial, tracked. Counters are correct and exposed as WorldProtocol::abuse_counters() (world_net.rs:156), but no shipped surface prints them — no CLI verb and no metrics route calls it, so an operator cannot yet read a counter value. That last mile is filed as id(abuse): surface per-peer frame/byte counters to operators (metrics or id peers) #4918 with queue task 8033, rather than held here: nothing in the counter/refusal work itself is missing.
Residual items, deliberately not part of this issue:#2247 (rolling-window budgets) now has the counters it was blocked on, and #3927 (oversize request refused as a temporaryBudgetExhausted) is a real bug in the shipped evaluator, both already tracked.
Disposition: close #2244 as completed. No duplicate packets created; no held/Drew-owned artifacts touched.
agent-note:v1
state: triaged (coverage pass)
Coverage pass 2026-11-03: reviewed in the fleet-wide per-issue pass (drift / overlap / already-handled / stale / blockers / ordering). Verdict: still live; queued in repo-health-code-pilot as triage at priority 30 (priority/none). If this is already handled at current main, the triage closes it with evidence; if it needs splitting, the triage files the split.
Found via: coverage pass 2026-11-03
Activity
developing-today commentedon Oct 29, 2026
CODE-1111
dezren39 commentedon Nov 1, 2026
agent-note:v1 — triage record (task 5839, lane D)
Verdict: CONFIRMED at origin/main (064ce9e). Every claim in the issue checks out against current code; the viable route is the #2033 design's P0-A row (PR #3834,
doc/2026-11-01_design_id2033.md, branchdesign/id-2033— still OPEN, not yet on main).Claim-by-claim:
pkgs/id/srcareSaveRateLimiter(web/mod.rs:95, keyed by file name) and the world chat limiter (world/core.rs:38chat_rates: HashMap<u64, VecDeque<Instant>>, keyed by world participant id). No counter is keyed by remote endpoint public key, and there is no byte accounting anywhere. Confirmed.frames.rsread_frame(L150–165) reads the 4-byte length, enforces the 1 MiB cap (MAX_FRAME_BYTES, L16–20), then allocates the body — the design's charge point (after length prefix, before allocation/decode) exists as a clean seam. Writers all funnel throughencode_frame/write_frame(L27–39). Confirmed.world/core.rs:1000—"chat rate exceeded; try again later"(the issue cited L955-975; the line has drifted to 1000). The web save path returns 429 with a prose body and noRetry-Afterheader (web/routes.rs:1218). Several localRefusalenums exist (event_store/ingest.rs:109,membership/layer.rs:58,groups/fold.rs:44,directory.rs:94;directory_auth.rs'sRateLimitedcarries a proseString) — none is a shared structured rate/budget refusal withreason+retry_after_ms. Confirmed, with that nuance: the work is one shared contract, not another local enum.Implementation route — two phased, independently mergeable packets, enqueued on queue
repo-health-code-pilot:repo-health:v1:fix:developing-today/code:issue:2244:p0a-core: shared abuse evaluator module — typed subjects (endpoint / connection), token buckets (frame + byte) with the defaults above, one structured refusal type (rate_limited | budget_exhausted | blocked_by_operator | unauthorized,retry_after_msbounded 1 s–60 s), and a counter registry with a snapshot API for observability. No behavior change to existing paths.repo-health:v1:fix:developing-today/code:issue:2244:p0a-integration(after A): charge counters at theframes.rsseam with the peer'sEndpointIdthreaded from the adapters; HTTP mapping 429 +Retry-Afterat the existing 429 sites (web/routes.rs,web/world_ws.rs:350); world chat limiter becomes a consumer of the same bucket and returns the structured refusal; counters surfaced (id peers/ metrics).Invariants the implementation must hold (from #2033): a frame is charged before body allocation/decode; the 1 MiB hard cap is retained, not duplicated; endpoint allowance is shared across connections; a refusal sheds the request/stream, never the whole peer connection; semantic caps (chat, save) keep their semantics — they consume the shared accounting, they are not relabeled as transport frames.
Drew questions: none. #2033 records "None" for its open choices (scoped defaults chosen; relay-attested IP deferred as a separate privacy decision), and nothing in this issue needs a Drew ruling to start Packet A.
dezren39 commentedon Nov 1, 2026
agent-note:v1 — triage addendum (task 5839): packet identifiers, corrected
The dedupe keys named in the triage record above used phase suffixes the queue's key grammar does not accept, so the phases were filed as their own issues (the fleet's established pattern) and the packets enqueued against them:
repo-health:v1:fix:developing-today/code:issue:3881, priority 30, queued.repo-health:v1:fix:developing-today/code:issue:3882, priority 30, queued. Sequenced after task 5845 lands.Triage task 5839 completes as
triaged. Drew questions: none (per the triage record).dezren39 commentedon Nov 1, 2026
agent-note:v1 — state: picked-up (triage task 5907, OpenChamber scheduled lane)
dezren39 commentedon Nov 1, 2026
agent-note:v1 — triaged (task 5907, OpenChamber scheduled lane)
bc76a7ed1a44fd056c8f1669dfec78d50de2344e); integration issue id(comms): P0-A integration — frame-seam charging, HTTP 429+Retry-After, chat limiter consumer, counter visibility (phase of #2244) #3882 remains OPEN and queue-claimed/in-progress with PR feat(id): P0-A integration — frame-seam charging, HTTP 429+Retry-After, chat limiter consumer, counter visibility (#3882) #3960 OPEN. Design PR design(id): #2033 abuse-control primitives v1 #3834 remains OPEN.dezren39 commentedon Nov 3, 2026
agent-note:v1 — state: picked-up (triage task 7867, OpenChamber scheduled lane ec118cad)
bc76a7e, feat(id): P0-A integration — frame-seam charging, HTTP 429+Retry-After, chat limiter consumer, counter visibility (#3882) #3960762f32a, design(id): #2033 abuse-control primitives v1 #38342b18593). Verifying the shipped code at current main before disposition — no duplicate packets will be created.dezren39 commentedon Nov 3, 2026
agent-note:v1 — triaged / done (task 7867, OpenChamber scheduled lane ec118cad)
Verdict: implemented and merged at main
1e3763fa; closing with evidence. The prior triage (task 5839/5907) scoped the work and routed the phases; both phases and the design record have since landed, and this pass verified the shipped code rather than re-planning it.Claim-by-claim at
refs/remotes/origin/main(1e3763fa):CounterRegistryinpkgs/id/src/abuse.rskeys counters by(NodeId, EndpointId)and records both directions;pkgs/id/src/frames.rsexposescharge_frame/read_frame_len+read_frame_body/read_frame_charged/write_frame_charged, so a charged reader charges after the 4-byte length prefix and before body allocation, withMAX_FRAME_BYTESretained (not duplicated). (PR feat(id): shared abuse evaluator core — buckets, structured refusal, counter registry (#3881) #3891bc76a7ecore, PR feat(id): P0-A integration — frame-seam charging, HTTP 429+Retry-After, chat limiter consumer, counter visibility (#3882) #3960762f32aintegration.)AbusePolicy::default()(abuse.rs:248) is authorized 60 frames/s burst 180 and 512 KiB/s burst 4 MiB, pre-authorization 10/s burst 30,max_connections_per_peer: 4. Endpoint allowance is owned by the endpoint, so extra connections do not multiply it (world_net.rsshares one evaluator per remoteEndpointId).Retry-After— done.abuse::Refusal/RefusalReason { rate_limited, budget_exhausted, blocked_by_operator, unauthorized }withretry_after_msclamped to 1 s–60 s (none for operator blocks); JSON body +Retry-Afteratweb/routes.rs:1326andweb/world_ws.rs:491-506. The world chat limiter is now a consumer of the same evaluator (world/core.rschat_abuse, burst/rate preserved); the prose string is gone (git grep "chat rate exceeded"→ no hits).WorldProtocol::abuse_counters()(world_net.rs:156), but no shipped surface prints them — no CLI verb and no metrics route calls it, so an operator cannot yet read a counter value. That last mile is filed as id(abuse): surface per-peer frame/byte counters to operators (metrics orid peers) #4918 with queue task 8033, rather than held here: nothing in the counter/refusal work itself is missing.Residual items, deliberately not part of this issue: #2247 (rolling-window budgets) now has the counters it was blocked on, and #3927 (oversize request refused as a temporary
BudgetExhausted) is a real bug in the shipped evaluator, both already tracked.Disposition: close #2244 as completed. No duplicate packets created; no held/Drew-owned artifacts touched.
Links: #3881 · #3891 · #3882 · #3960 · #3834 · #4918 · #2247 · #3927
developing-today commentedon Nov 3, 2026
agent-note:v1
state: triaged (coverage pass)
Coverage pass 2026-11-03: reviewed in the fleet-wide per-issue pass (drift / overlap / already-handled / stale / blockers / ordering). Verdict: still live; queued in repo-health-code-pilot as triage at priority 30 (priority/none). If this is already handled at current main, the triage closes it with evidence; if it needs splitting, the triage files the split.
Found via: coverage pass 2026-11-03