Skip to content

fix(observability): classify shared-pool sync lanes on the queue they draw on - #2107

Merged
Jurij89 merged 10 commits into
testnet-canaryfrom
fix/2075-sync-lane-limits
Aug 6, 2026
Merged

fix(observability): classify shared-pool sync lanes on the queue they draw on#2107
Jurij89 merged 10 commits into
testnet-canaryfrom
fix/2075-sync-lane-limits

Conversation

@Jurij89

@Jurij89 Jurij89 commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Summary

  • The bug. GET /api/diagnostics/backpressure classifies each lane with four signals, two of which are depth-based and need a per-lane queue limit. The sync admission queue publishes none, so for every sync-global lane both depth branches were dead and lane state degraded to a trailing indicator: a sync lane only left healthy after 15 s of queue age (degradedQueueAgeMs, overridden from the 5 s default), 120 s of active age, or a rejection that had already happened.

  • Why the issue's own suggested fix was not taken. Publishing the one global ceiling as each lane's queueLimit would satisfy the acceptance criteria literally and then rarely fire. sync-global lanes are fed by concurrent per-peer drivers (dkg-agent-lifecycle.ts:4238-4278 fans out un-awaited), so with the default queueLimit = 4 a pool full at durable: 2, changelog: 1, shared_memory: 1 still reports every lane healthy — one lane would have to hold 3 of 4 slots to trip anything. It would also publish a number no code enforces, turn dkg.backpressure.queue_limit{scheduler="sync-global"} from 1 series into 5, and disarm the lanes.some(l => l[field] === null) guard that is the only thing keeping sumLaneLimits from reporting 4× the real ceiling.

  • What this does instead. A scheduler declares how its lanes divide capacity. partitioned (the default, and StorePriorityScheduler) means private per-lane allocations — behaviour is unchanged, by construction. shared (PriorityAdmissionQueue) means every lane draws on one pool: a lane's ceilings resolve from the scheduler-level limits, and depth is measured against the pool's depth, charged only to lanes that actually hold queued work. So every lane waiting behind a full sync queue reports saturated the moment it fills and degraded at 75%, before anything is rejected; a lane with nothing queued stays healthy, so per-lane queued and queuedOperations remain the attribution signal.

  • Why not infer "shared" from a missing lane limit (the smaller diff, no producer change): queueLimit === null today means "shared pool" or "the producer has not published it" — and the second is exactly this bug. Collapsing them silently reclassifies the next scheduler that forgets, instead of leaving the omission visible.

Two things that provably cannot change

  • The /api/status rollup. For an all-shared scheduler totals.queued is the pool depth and totals.queueLimit is the pool ceiling, so a shared lane's new predicate is the predicate the rollup already evaluates at backpressure-observability.ts:295-307. maxState over lanes can only reproduce a state the totals branch was going to produce anyway.
  • Store lane states. With no model declared, laneCapacityFor returns a depthPressure whose numerator is the lane's own backlog — literally the expression the classifier used before — and the added laneQueued > 0 guard is implied by the comparisons it guards (queued >= limit with limit > 0; 0 / limit >= 0.75 is false). Pinned by two tests, including one where the store rollup is full while neither lane is near its own allocation.

Called out deliberately

  • sync-global stops emitting its "lane":"all" log record. That record is emitted only while the rollup strictly outranks every lane (:635), and the identity above makes that unreachable for a shared scheduler. Rather than relax :635 to >= — which would also raise the store scheduler's log volume, a scheduler this PR otherwise proves untouched — a lane record carries pressureQueued whenever it differs from queued, so the depth that classified the lane and the ceiling it was measured against are both on the line that replaced it. Every store record stays byte-identical. Log volume during a sync incident goes from 1 line/minute to k, where k is the number of lanes holding queued work (≤ 4) — which is exactly the set of lanes an operator needs named.
  • The acceptance criteria are met on a restated denominator. Backpressure diagnostics: sync lanes publish no per-lane limits, so depth-based lane state never fires #2075 says "at or above its queue limit"; a sync lane owns no limit (priority-admission-queue.ts:227 tests the whole queue; backpressure.ts:78 gates on one module-level counter), so this reads it as "the limit it is subject to".
  • Payload/metrics deltas. The snapshot gains an optional scheduler-level capacityModel (the authoritative value — capacity division is a scheduler invariant), and each lane row gains two optional fields: a derived capacityModel, so a row still explains its own queueLimit when it travels alone in a log line or a metric series, and pressureQueued — the depth the state was classified against, equal to queued under partitioned and the pool's depth under shared. Utilization is therefore pressureQueued / queueLimit on every scheduler, with no model-specific branch in any consumer. Both are optional so a hand-built BackpressureSource written against an older dkg-core still satisfies the type (absent capacityModel = partitioned, absent pressureQueued = queued). Shared lanes now report the pool's queueLimit/inflightLimit instead of nullone pool's ceilings repeated per lane, never to be summed (documented, and machine-readable via capacityModel). That adds 4 identical queue_limit/inflight_limit series for sync-global; no shipped dashboard or alert rule reads those gauges — the only backpressure metric any tooling consumes is oldest_queued_age_ms (tools/observability/w1/w1-rules.yaml:101,202).
  • Not covered: the sync responder limiter is a separate, uninstrumented queue, so pre_authorization and responder still appear in no snapshot, metric, or log line. Now stated in the guide so this fix cannot be read as covering it.
  • Not touched: the still-open feat(observability): unify scheduler backpressure diagnostics #2003 finding that the classifier ignores inflightLimit. This change touches only the two queue-depth terms and adds no inflight branch, so that finding's hard-capped-lane hazard is unaffected.
  • The 10.0.12 ### Known issues bullet (CHANGELOG.md:58) is left byte-for-byte — released sections are immutable; the fix is recorded under ## [Unreleased] and the item simply is not carried forward at the next release.

Related

Diagrams

A full sync-global queue, spread across lanes

Before:

sequenceDiagram
    participant Sync as Sync drivers (4 peers)
    participant Q as PriorityAdmissionQueue
    participant T as SchedulerPressureTracker
    participant Op as Operator
    Sync->>Q: acquire x4 (durable x2, changelog, shared_memory)
    Q->>T: updateCapacity({queueLimit: 4, inflightLimit: 2})
    Note over T: no lanes map -> every lane queueLimit = null
    Op->>T: GET /api/diagnostics/backpressure
    T-->>Op: scheduler saturated — durable healthy, changelog healthy, shared_memory healthy
    Note over Op: depth branches dead — lanes go non-healthy<br/>only at 15 s of age, or after a rejection
Loading

After:

sequenceDiagram
    participant Sync as Sync drivers (4 peers)
    participant Q as PriorityAdmissionQueue
    participant T as SchedulerPressureTracker
    participant Op as Operator
    Sync->>Q: acquire x4 (durable x2, changelog, shared_memory)
    Q->>T: updateCapacity({queueLimit: 4, inflightLimit: 2, capacityModel: 'shared'})
    Note over T: shared -> lane ceiling = pool ceiling,<br/>depth = pool depth, charged to lanes holding work
    Op->>T: GET /api/diagnostics/backpressure
    T-->>Op: scheduler saturated — durable saturated (2/4), changelog saturated (1/4), shared_memory saturated (1/4)
    Note over Op: fires at the instant the queue fills,<br/>with no rejection and no elapsed time
Loading

An idle lane on a full pool

Before:

sequenceDiagram
    participant T as SchedulerPressureTracker
    participant Op as Operator
    Note over T: durable 3/4 queued, swm_recovery running, nothing queued
    Op->>T: snapshot
    T-->>Op: durable healthy, swm_recovery healthy
Loading

After:

sequenceDiagram
    participant T as SchedulerPressureTracker
    participant Op as Operator
    Note over T: durable 3/4 queued, swm_recovery running, nothing queued
    Op->>T: snapshot
    T-->>Op: durable degraded (75% band), swm_recovery healthy
    Note over Op: an idle lane is not held back by a full queue —<br/>lane `queued` stays the attribution signal
Loading

Files changed

File What
packages/core/src/backpressure-observability.ts New SchedulerLaneCapacityModel; SchedulerPressureCapacity becomes a discriminated union so a shared capacity cannot carry private lanes; capacityModel on BackpressureSnapshot (authoritative — it is a scheduler invariant) and, derived, on each BackpressureLaneSnapshot so a row travelling alone in a log line or a metric series still explains its own queueLimit; plus pressureQueued per lane. A private laneCapacityFor owns the whole capacity decision and returns depthPressure: {queued, limit} | null, so laneSnapshot is a model-agnostic classifier and "does depth apply" is not inferred from a nullable ceiling. BackpressureMonitor.message reads the published pressureQueued and emits it only when it differs from queued. sumLaneLimits documents why a pool ceiling can never become a summand. All new fields are optional, so a hand-built BackpressureSource still satisfies the types.
packages/agent/src/sync/priority-admission-queue.ts Declares laneCapacity: 'shared' in the existing capacity publish. The top-level queueLimit/inflightLimit publish is load-bearing and unchanged.
packages/core/test/backpressure-observability.test.ts Seven tests. Six classifier tests with the clock frozen throughout and nothing ever rejected — shared/spread, shared/concentrated, the distributed 75% band (no lane near 75% on its own), the idle-lane guard, a partitioned lane on its own allocation, and a partitioned scheduler whose rollup is full while neither lane is — each guarding a different way the fix could be wrong. One BackpressureMonitor test over the emitted log JSON, registering a wholly shared and a wholly partitioned scheduler.
packages/agent/test/sync-backpressure.test.ts A local PriorityAdmissionQueue with a frozen clock driving the real producer path (spread shape), plus a witness that the production sync-global singleton publishes the model and the scheduler-level ceilings.
docs/use-dkg/backpressure-observability.md Capacity-model table, qualified degraded/saturated semantics for shared lanes, the idle-lane rule, poolQueued, and an explicit note that the responder limiter is uninstrumented.
CHANGELOG.md One ### Fixed bullet under ## [Unreleased].

Test plan

Run in a fresh worktree with its own pnpm install and a full pnpm run build — the agent package resolves @origintrail-official/dkg-core from dist, so a stale build silently fabricates results.

  • packages/core> npx vitest run test/backpressure-observability.test.ts12 passed (5 before)
  • packages/core> npx vitest run105 files / 1658 tests passed
  • packages/agent> npx vitest run test/sync-backpressure.test.ts (real CI config, Hardhat global setup) — 34 passed (32 before), exit 0
  • packages/storage> npx vitest run test/store-priority-scheduler.test.ts22 passed (store non-regression)
  • packages/cli> npx vitest run test/backpressure-route.test.ts3 passed, run without the chain globalSetup: the CLI lane's Hardhat harness does not come up in this worktree for any cli test file (verified against an unrelated one), so that failure is environmental and pre-existing, not from this change. CI runs the lane normally.
  • pnpm run build — exit 0; artifacts verified rather than assumed (grep -c capacityModel packages/core/dist/...js = 2, grep -c laneCapacity packages/agent/dist/...js = 1)

Fail-before, verified:

  • Core: revert packages/core/src/backpressure-observability.ts → the 3 new lane tests fail.
  • Agent: revert both source files and rebuild core (grep -c capacityModel packages/core/dist/...js = 0, so the failure is not a stale artifact) → both new tests fail.

Mutants, verified:

  • boundedDepth = queued.length (drop the pool denominator) → kills only the spread test. This is the point: a concentrated-shape test alone would pass under the "publish the ceiling per lane" alternative and would not discriminate.
  • Drop the queued.length > 0 guard → kills only the idle-lane test.
  • Drop pressureQueued from the emitted log JSON → kills only the new monitor test (added in review round 1, where nothing had covered the log path).
  • Publish the pool's depth on an idle shared lane → kills only the idle-lane test (the review-round-3 bug: a healthy lane reading as fully utilized).
  • Saturation on pool depth but the degraded branch back on per-lane depth → this survived all 11 tests until the distributed-75% test was written for it, which is why that test exists.

Every mutant above was re-run after each of the two structural refactors review asked for, and each is still killed by exactly the tests that killed it before.

Type-level, verified with tsc: a shared capacity carrying private lanes is rejected
(Type '{ durable: … }' is not assignable to type 'undefined'), while {queueLimit, lanes},
{laneCapacity:'shared', queueLimit, inflightLimit} and the pre-existing {queueLimit, inflightLimit}
all compile.

Diagrams: all four mermaid blocks were parsed and rendered with mermaid 11.16 in headless
Chromium before publishing (harness: mermaid.parse + mermaid.render, asserting real SVG output).

Live-node check (testnet, separate): drive sync-global to a full queue and confirm (a) lane state moves at the instant the queue fills with rejectedTotal still 0 and oldestQueuedAgeMs well under 15 000, (b) /api/status reports the same scheduler state as before, and (c) [backpressure] lines for shared lanes carry poolQueued.

… draw on

`GET /api/diagnostics/backpressure` classifies a lane's depth against a
per-lane queue limit. The sync admission queue publishes none — its lanes
order one shared queue by priority rather than partitioning it — so both
depth-based branches were dead for `sync-global` and a sync lane only left
`healthy` after 15 s of queue age, 120 s of active age, or a rejection that
had already happened. Lane `state` was a trailing indicator.

A scheduler now declares how its lanes divide capacity. `partitioned` (the
default, and the store scheduler) keeps today's behaviour exactly: the
classifier evaluates the same expressions, and the added `queued.length > 0`
guard is implied by the comparisons it guards. `shared` resolves a lane's
ceilings from the scheduler-level limits and measures depth against the pool,
charged only to lanes that hold queued work — so every lane waiting behind a
full sync queue reports `saturated` at the moment it fills, and `degraded` at
75%, before anything is rejected. An idle lane stays `healthy`.

The `/api/status` rollup cannot move: for an all-shared scheduler a lane's new
predicate is the predicate the totals already evaluate, so `maxState` over
lanes can only reproduce a state the rollup was going to report anyway. The
same identity means a shared scheduler's lanes now match its rollup, which
suppresses its `"lane":"all"` log record — shared lanes therefore carry
`poolQueued` beside `queued` so the pool's depth stays on the line.

Fixes #2075.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Comment thread packages/core/src/backpressure-observability.ts Outdated
Comment thread packages/core/src/backpressure-observability.ts Outdated
// its own backlog when the allocation is private, the whole pool when the
// ceiling is shared. Under `partitioned` this is `queued.length`, so the
// classifier below is unchanged for every scheduler that owns its lanes.
const boundedDepth = shared ? this.queued.size : queued.length;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Issue: Expose the pressure depth instead of mixing lane-local queued with pool capacity

What's wrong
The implementation fixes classification by adding an internal boundedDepth, but the public lane row still pairs a lane-local queued value with a pool-wide queueLimit. That overloaded shape is what forces the new log special case and is likely to spread more capacityModel === 'shared' branches into every consumer that wants to explain or visualize lane pressure.

Example
For a shared pool with four queued items and one item in changelog, the lane snapshot exposes queued: 1 and queueLimit: 4 but classifies using boundedDepth: 4. Any new consumer that computes lane utilization from the row sees 25%, while the state is saturated; the monitor already needs a bespoke poolQueued branch to explain that mismatch.

Suggested direction
Make the snapshot carry both concepts explicitly: lane-local attribution depth and the depth/capacity pair used for pressure classification. Then metrics, logs, diagnostics, and future consumers can read one canonical model instead of learning the shared-pool exception independently.

For Agents
In SchedulerPressureTracker.laneSnapshot and BackpressureMonitor.message, introduce an explicit lane pressure descriptor, for example pressureQueued/pressureQueueLimit or a nested capacity object containing the numerator used for state classification. Preserve lane-local queued for attribution and preserve existing state behavior; update the shared-pool tests to assert the explicit pressure depth and have logging read that field rather than reconstructing it from scheduler totals.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Issue: Capacity-model policy is threaded through the lane classifier

What's wrong
laneSnapshot was already responsible for collecting queued/active work, computing ages, classifying state, and building the DTO. This change embeds the new capacity-model semantics directly into that busy path with several coordinated branches. The behavior is understandable now, but the abstraction boundary is weak: the classifier has to know how every capacity model maps limits and pressure depth, which makes the next model or rule change more error-prone and harder to review.

Example
Today the model check fans out through queueLimit, inflightLimit, boundedDepth, depthCeiling, and pressureQueued. Any future capacity model or change to shared-lane idle handling would require editing this classifier in multiple places.

Suggested direction
Move the model-specific logic behind a small helper, e.g. capacityForLane(lane, laneQueuedCount), or normalize SchedulerPressureCapacity in updateCapacity. Have it return capacityModel, queueLimit, inflightLimit, pressureQueued, and whether depth pressure applies, so laneSnapshot can remain a model-agnostic state classifier.

For Agents
Look in SchedulerPressureTracker.laneSnapshot. Preserve current shared-pool behavior and partitioned behavior, but extract or normalize lane capacity into one descriptor before classification. Existing shared/partitioned tests should continue to prove the same states and snapshot fields.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed — this was the weakest part of the design and the diagnosis is exact. Fixed in 49d0744.

The lane row now publishes both concepts:

Field Meaning
queued this lane’s own backlog — the attribution signal
pressureQueued the depth state was classified against, and the numerator that belongs with queueLimit

pressureQueued equals queued for a private allocation and the pool’s depth for a shared one, so utilization is pressureQueued / queueLimit on every scheduler and nothing downstream learns a shared-pool exception. Your worked example now reports queued: 1, pressureQueued: 4, queueLimit: 4 alongside state: saturated — coherent on its own, with no cross-reference to totals.

The log special case is gone with it: BackpressureMonitor.message reads the published pressureQueued (falling back to queued for a source that predates it) instead of reaching into scheduler.totals.queued, and emits it only when it differs from queued — so the branch is now value-based rather than model-based, and every store record stays byte-identical. poolQueued never shipped outside the first commit of this PR.

Both new fields are optional, per the sibling capacityModel thread.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Bug: Idle shared lanes publish pool pressure they were not classified against

What's wrong
The new field is documented as the depth used to classify the lane and as the utilization numerator to pair with queueLimit. For shared schedulers, empty but previously-known lanes are deliberately kept healthy even when the pool is full, but this return value still publishes the full pool depth. API consumers now see a healthy lane with full utilization and must special-case queued === 0, which contradicts the new contract and can create false diagnostics for lanes that are not actually waiting.

Example
With the new sync test shape: one swm_recovery item is active, durable and changelog each have one queued item, and queueLimit is 2. The swm_recovery lane correctly reports state: "healthy" and queued: 0, but this line also reports pressureQueued: 2 and queueLimit: 2, so pressureQueued / queueLimit says 100% utilization for a lane whose depth was not classified and has no waiting work.

Suggested direction
Derive a separate published pressure depth from the guarded classification input, for example falling back to the lane’s own queued.length when the lane has no queued work, instead of returning the pool depth unconditionally.

For Agents
In SchedulerPressureTracker.laneSnapshot, keep the empty-lane guard and make the published pressureQueued match the same depth actually used for lane depth classification. Add/adjust a shared-pool test where a known lane has queued: 0 while the pool is full, and assert it remains healthy with a non-misleading pressureQueued value, likely 0 or equal to queued.

Keep pressureQueued aligned with the depth that actually classifies the lane

What's wrong
The new snapshot contract says pressureQueued is the depth the lane state was classified against and the numerator paired with queueLimit, but the implementation returns the shared pool depth even when the classifier intentionally ignored that depth because the lane has nothing queued. That makes the exported row self-contradictory and reintroduces a hidden rule consumers must know: for shared lanes, pressureQueued only means pressure when queued > 0. This weakens the new abstraction exactly where it is supposed to remove consumer special-casing.

Example
In the new shared-pool scenario, swm_recovery can have state: 'healthy', queued: 0, queueLimit: 2, but still emit pressureQueued: 2 because the shared pool is full. A generic consumer following the new pressureQueued / queueLimit rule would display that healthy idle lane as 100% utilized.

Suggested direction
Make pressureQueued mean the effective classifier numerator, for example queued.length === 0 ? queued.length : shared ? this.queued.size : queued.length. If the raw shared pool depth is useful even for idle lanes, expose it under a separate name such as poolQueued instead of overloading pressureQueued.

For Agents
Update SchedulerPressureTracker.laneSnapshot so one effective queue-pressure value is computed after the idle-lane guard and reused for both classification and the snapshot. Preserve shared lanes with queued work being classified by pool depth and partitioned lanes being classified by their own depth. Add/adjust a test proving an idle shared lane emits pressureQueued as 0/queued or omits it while remaining healthy, and queued shared lanes still degrade/saturate from pool depth.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re the nested "Capacity-model policy is threaded through the lane classifier" — same timing race as the sibling thread: you posted it ~12 s before my reply, so I resolved over it. Reopening in substance. Fixed in 5d9e8b6, taking your suggested direction almost verbatim.

laneCapacityFor(lane, laneQueued) now owns the whole capacity decision and returns {capacityModel, queueLimit, inflightLimit, pressureQueued, depthCeiling}. laneSnapshot destructures that and is a model-agnostic classifier again — the five coordinated locals you counted (queueLimit, inflightLimit, boundedDepth, depthCeiling, pressureQueued) are gone from it, and shared is not referenced there at all. Adding a model means extending one descriptor rather than editing branches.

depthCeiling carries the "does depth pressure apply" bit you asked for, so the idle-lane rule also lives with the model rather than in the classifier.

Behaviour is unchanged and that is checked, not asserted: the same 12 tests pass, and all three mutants were re-run against the new structure — pool-denominator, idle-lane guard, and log-field — each still killed by exactly the tests that killed it before the refactor. Store lanes and the /api/status rollup are covered by the two dedicated partitioned tests.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two comments landed here since my last reply — taking them separately.


🔴 "Idle shared lanes publish full-pool pressureQueued" (07:03:17) — already fixed at the commit you reviewed.

This review is recorded against 52e107f8a, which is the commit that fixes it (pushed ~07:02, four minutes before the review). Running your exact scenario against that build — queueLimit: 2, one active swm_recovery, one queued each in durable/changelog:

{"lane":"changelog","state":"saturated","queued":1,"pressureQueued":2,"queueLimit":2}
{"lane":"durable","state":"saturated","queued":1,"pressureQueued":2,"queueLimit":2}
{"lane":"swm_recovery","state":"healthy","queued":0,"pressureQueued":0,"queueLimit":2}

The idle row reports pressureQueued: 0, not 2. Both suites assert it, and reintroducing the old expression is a mutant that kills those assertions and nothing else. Reads like the finding was re-derived from the full base...head diff rather than the tip — flagging it rather than re-fixing.


🟡 "Separate depth classification from capacity reporting" (07:07:50) — agreed, fixed in d1c11aa.

You are right that depthCeiling: number | null was carrying three meanings, and that the invariant lived in comments instead of the type. It is now:

depthPressure: { queued: number; limit: number } | null

Either depth applies — with both of its terms — or it does not. The reported ceilings (queueLimit/inflightLimit) stay separate, exactly as you suggested, and pressureQueued now falls out of the pair (depthPressure?.queued ?? queued.length) rather than being re-derived from a sentinel. Both classifier branches read the pair, so the "no usable limit" and "idle lane" cases are no longer distinguishable only by comment.

Behaviour is unchanged and checked: the same 12 tests pass, and all three mutants were re-run against the new structure — idle-lane pool depth, always-lane-local depth, and the log field — each still killed by exactly the tests that killed it before.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Issue: Shared lane metrics pair lane backlog with the pool limit

What's wrong
For shared lanes, this change reports the pool limit on each lane while leaving the existing metrics exporter to record the lane's own backlog as queue depth. The JSON/log path gained pressureQueued, but metrics did not, so Prometheus/OpenTelemetry dashboards that combine dkg.backpressure.queue_depth and dkg.backpressure.queue_limit now see an internally inconsistent pair and can miss the pressure condition this PR is meant to surface.

Example
With a shared sync-global pool of 4 and three lanes holding one queued item each, diagnostics reports each active lane as degraded with pressureQueued: 3 and queueLimit: 4. The exported lane metrics still become queue_depth=1 and queue_limit=4, so a metric-only utilization alert reads 25% instead of the intended 75%. A full pool spread across four lanes would similarly read 25% while the lane state is saturated.

Suggested direction
Update the metric contract alongside the snapshot contract. Either emit a separate pressure-depth gauge, record pressureQueued for the existing pressure utilization metric while keeping attribution elsewhere, or include enough model/numerator data in metrics so queue_depth / queue_limit does not silently underreport shared-pool pressure.

For Agents
Look at packages/core/src/backpressure-observability.ts: the lane snapshot now exposes pressureQueued, but recordBackpressureSnapshotMetrics still emits only queued against the lane queueLimit. Preserve lane-local backlog attribution, but add/export the pressure numerator for shared lanes or otherwise avoid publishing an incompatible depth/limit pair. Add a metric-focused test for a shared pool spread across lanes proving metric consumers can observe the same degraded/saturated pressure as diagnostics.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Issue: The new pressure depth model stops short of the metrics boundary

What's wrong
The PR’s model says consumers should use pressureQueued / queueLimit, but only snapshots and log messages get that value. Metrics still publish lane queued beside the shared pool queueLimit, so the observability abstraction is split across surfaces and future consumers have to relearn the shared-lane special case.

Example
For a shared pool with queueLimit=4 and four queued items spread across lanes, a lane can report queued: 1, pressureQueued: 4, queueLimit: 4, and state: saturated. The snapshot/log row is self-consistent, but the lane metric pair still looks like queue_depth=1 and queue_limit=4.

Suggested direction
Treat pressureQueued as a first-class normalized field at the observability boundary. Either add a dedicated pressure-depth gauge or centralize lane normalization so metrics, logs, and diagnostics do not each need their own interpretation of queued versus pressure depth.

For Agents
In packages/core/src/backpressure-observability.ts, route snapshot/log/metric consumers through one normalized lane shape. Preserve queued as attribution, but expose/record the pressure depth separately or make the metric contract explicitly use it. Add a metric-focused test for a shared lane where queued !== pressureQueued.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both metrics findings (09:04:19 and 09:04:20 — same defect) are correct, and it is one this PR introduced. Fixed in 47c6bc2.

Before this change a shared lane published queueLimit: null, so recordBackpressureSnapshotMetrics never recorded the limit gauge for it — there was no pair to be inconsistent. Publishing the pool ceiling per lane created one, and I extended the numerator to the snapshot and the log line but not to metrics. Your worked example is exact: queue_depth=1 against queue_limit=4 while the lane is degraded at 75%. That is the surface W1 alerting actually reads (tools/observability/w1/w1-rules.yaml), so it is the worst of the three to have left behind.

New instrument, dkg.backpressure.pressure_depth{scheduler,lane} — the numerator that belongs with queue_limit:

gauge meaning
queue_depth this lane own backlog — attribution, unchanged
pressure_depth the depth the lane state was classified against
queue_limit the ceiling that depth was measured against

So utilization is pressure_depth / queue_limit on every scheduler, and on a partitioned lane the two depths are equal — no model-specific branch for a metric consumer, which is the "one normalized lane shape" the second comment asks for. The rollup row (lane="all") reports its own depth as its pressure.

Test added as requested: a shared pool spread across three lanes, asserting queue_depth=1, pressure_depth=3, queue_limit=4 per lane and pressure_depth / queue_limit >= 0.75 alongside the degraded state, plus a partitioned lane where both depths are 1. Exporting the lane backlog as pressure_depth is a mutant that kills only that test.

One note on how it is tested, since it bit me: the test binds a stand-in meter provider rather than patching the instrument objects. With no global provider registered the OTel API hands back one shared no-op instrument, so stubbing backpressureQueueDepth.record stubs every gauge — my first attempt captured 24 records across all of them and asserted against the wrong thing. Binding a provider also pins the published metric names, which is the part a dashboard actually depends on.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both comments in this thread are addressed at abcbc663e, which landed after each was written. Recording where, rather than resolving — that's the author's call.

The original comment (2026-08-05) asks for "an explicit lane pressure descriptor, for example pressureQueued". It quotes boundedDepth and poolQueued, identifiers from a revision that no longer exists. pressureQueued is now a public field on BackpressureLaneSnapshot (backpressure-observability.ts:96-104), set at :500, and read by BackpressureMonitor.message at :873 rather than reconstructed from scheduler totals — which is what the comment asked for. queued is preserved as the lane-local attribution signal.

The newest comment (2026-08-06 09:04, against the previous head) is the metrics half, and it is right — I raised the same thing independently. It shipped fixed ~40 minutes later:

  • dkg.backpressure.pressure_depth (telemetry-api.ts:214, :410), recorded at backpressure-observability.ts:703 as values.pressureQueued ?? values.queued. This is the comment's first suggested direction — "emit a separate pressure-depth gauge" — with queue_depth left as the lane's own backlog for attribution, as it asked.
  • The same treatment for concurrency: dkg.backpressure.pressure_inflight (telemetry-api.ts:224, :420), recorded at :705.
  • The metric-focused test the comment asks for exists at packages/core/test/backpressure-observability.test.ts:617. It pins values and metric names, not just that an instrument fired: for a shared pool of 4 with three lanes holding one item each — the comment's own example — it asserts queue_depth = 1 alongside pressure_depth = 3 against queue_limit = 4, i.e. the 75% the comment says a metric-only alert should have seen instead of 25%. A partitioned row asserts the two gauges equal.

Verified by execution against the pinned head rather than by reading: at pool capacity the exported ratio is 1.00 where the lane state is saturated, and 0.75 where it is degraded; on the store scheduler pressure_depth == queue_depth on all five rows with zero exceptions. The ?? values.queued fallback keeps a hand-built BackpressureSource written against an older dkg-core working unchanged.

One residue worth carrying forward, raised in my round-2 review: pressure_inflight and the pressureInflight field have no test coverage anywhere in the repo — reverting :506 to active.length leaves both suites green. The depth half is pinned; the inflight half is not.

Comment thread packages/core/src/backpressure-observability.ts Outdated
Jurij89 and others added 4 commits August 6, 2026 08:50
…city, log test

Four review findings from otReviewAgent on #2107.

1. `capacityModel` was a required field on an exported snapshot type, so a
   hand-built `BackpressureSource` written against an older `dkg-core` stopped
   compiling for a scheduler that is still plainly partitioned. It is now
   optional, absent meaning `partitioned`; the tracker still always emits it.
   The monitor test's source is deliberately left in the pre-`capacityModel`
   shape so the old shape stays exercised.

2. `SchedulerPressureCapacity` allowed a shared pool to carry private `lanes`,
   which the implementation silently ignored. It is now a discriminated union
   where the shared arm forbids `lanes`, so a contradictory capacity cannot be
   published. Verified with tsc: the illegal shape is rejected and all three
   legal shapes still compile.

3. The lane row paired a lane-local `queued` with a pool-wide `queueLimit`, so
   a consumer computing utilization saw 25% next to `saturated`, and the log
   needed a bespoke shared-only branch. The row now also carries
   `pressureQueued` — the depth the state was classified against, equal to
   `queued` under `partitioned` and the pool's depth under `shared`. Utilization
   is `pressureQueued / queueLimit` everywhere, and the monitor reads the
   published field instead of reaching into scheduler totals, emitting it only
   where it differs from `queued`, which leaves every store record identical.
   `poolQueued` never shipped; it is `pressureQueued` now.

4. Nothing exercised the monitor's emission path, so a regression that dropped
   the field would have left every test green. A BackpressureMonitor test now
   asserts the emitted JSON for a shared and a private lane of the same
   scheduler, and that the `lane: all` record is absent once a lane matches the
   rollup. Verified by mutation: deleting the emission kills only that test.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The capacity input called it `laneCapacity` while the snapshot reported it as
`capacityModel`, so a reviewer reading the declaration and the payload met two
names for one concept. Both are `capacityModel` now, as asked in review.

Pure rename: no behaviour, no test expectation, and no emitted value changes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…lift the model out of the classifier

Two findings that arrived as nested replies on existing threads.

The 75% shared-pool band was only proven with the pool already full, where the
pool depth and a lane's own depth agree often enough to hide a regression. The
reviewer named the exact surviving mutant — saturation on pool depth, the
degraded branch back on `queued.length` — and it did survive all 11 tests.
`opens the 75% band on a shared pool no single lane is anywhere near filling`
is the shape where the two denominators disagree: three lanes holding one entry
each against a ceiling of four, so the pool is at 75% while every lane is at
25%. It fails against that mutant and passes without it.

`laneSnapshot` had grown the model's semantics across five coordinated locals,
so the next model would have to be threaded through the classifier again.
`laneCapacityFor` now returns the whole capacity decision — model, both
ceilings, the depth to classify against, and whether depth pressure applies —
and `laneSnapshot` is model-agnostic once more. No behaviour change: the same
12 tests pass, and all three mutants were re-run against the new structure,
each still killed by exactly the tests that killed it before.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Reported as a 🔴 on #2107, and correct. `pressureQueued` is documented as the
depth the lane's state was classified against and the numerator to pair with
`queueLimit`, but a shared lane with an empty backlog published the pool's whole
depth — while the classifier had deliberately ignored that depth and left the
lane `healthy`. A consumer following the documented rule would render a lane
with nothing waiting as fully utilized, and would have to special-case
`queued === 0`, which is exactly the consumer-side branching the field exists to
remove. The reviewer's example came from this PR's own test shape, where
`swm_recovery` sits healthy and idle beside a full pool.

The published value is now taken after the idle-lane guard, from the same
`depthCeiling` that decides whether depth applies at all, so it is always the
number the classifier used: the pool's depth for a shared lane with work
waiting, its own backlog otherwise. Both the core and the sync test now assert
`pressureQueued: 0` on the idle lane, and reintroducing the bug is a mutant that
kills that assertion.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
// `oldestActiveAgeMs` are 0) and nothing is ever rejected, so queue depth is
// the only signal that can move a lane off `healthy`. Without that, a lane
// state assertion here would pass for the wrong reason.
it('classifies a shared-pool lane against the pool depth, not its own share of it', () => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Suggestion: Table-drive the capacity-model matrix instead of growing bespoke scenario tests

Why it matters
The new coverage is valuable, but the test file more than doubles and repeats the same setup/assertion structure. A table-driven shape would make the actual model differences easier to scan and reduce future edit points when lane snapshot fields change.

Suggestion
Extract small helpers like trackerWith(capacity), enqueueLanes(tracker, entries), and a table of {name, capacity, work, expected} cases for the shared/partitioned pressure matrix. Keep one dedicated test only where the setup truly differs, such as active idle-lane handling.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks — declining this one, with reasons, and taking the half of it that applies.

Where I agree: the monitor test had the repeated-setup problem you describe, and it already uses a laneRow(lane, queued, extra) helper for exactly that. Its scheduler shape also just changed in d1c11aa (two realistic sources instead of one hybrid), so the duplication there is down.

Where I would rather not go table-driven: these cases are not the same assertion over varying data — each one exists to kill a different way the fix could be wrong, and the reason is the payload:

Test The mutant it exists to kill
shared / spread pool denominator → per-lane depth (a concentrated-shape test survives this, which is the whole reason the spread shape is there)
shared / concentrated the issue as literally filed
distributed 75% saturation on pool depth while the degraded branch regressed to per-lane — this one survived 11 tests until it was written
idle lane the laneQueued > 0 guard, and the idle row publishing pool depth
partitioned, own allocation shared semantics leaking into store
partitioned, rollup full / lanes not the pool denominator leaking in via the totals path

Folded into a {name, capacity, work, expected} table, those six rationales become a column of names, and the next person to edit a lane snapshot field sees a data change rather than "this row is the only thing standing between us and a silently inert fix". Each case also carries a comment naming the trap, which is what made the distributed-75% gap findable in the first place. The setup they share is three lines; the reasoning they do not share is the point.

I have kept the shared-setup reduction (laneRow, and constants rather than repeated literals) and left the case bodies explicit. If a seventh and eighth shared/partitioned case land, that calculus changes and a table is the right answer — happy to revisit then.

…l capacity model

Two review findings on 52e107f, both about the shape of the boundary rather
than behaviour. No state, no published value, and no test expectation changes.

`depthCeiling: number | null` was doing three jobs — the reported ceiling,
whether depth classification applies, and the idle-lane exemption — so a
maintainer touching one had to preserve an invariant only the comments
explained. It is now `depthPressure: { queued, limit } | null`: either depth
applies, with both of its terms, or it does not. `pressureQueued` falls out of
it (`depthPressure?.queued ?? queued.length`) rather than being derived from a
sentinel, and both classifier branches read the pair.

The capacity model is a scheduler invariant, but only the lane rows carried it,
so a snapshot could describe one scheduler with lanes in different models — and
the monitor test exercised exactly that impossible shape. `BackpressureSnapshot`
now carries `capacityModel` as the authoritative value (optional, absent means
partitioned, like the lane copy), the lane copy stays as derived data so a row
still explains its own `queueLimit` when it travels alone in a log line, and the
monitor test registers two realistic schedulers — one wholly shared, one wholly
partitioned — instead of one hybrid.

All three mutants re-run against the new structure and are still each killed by
exactly the tests that killed them before.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@Jurij89

Jurij89 commented Aug 6, 2026

Copy link
Copy Markdown
Contributor Author

CI note — the two red checks on this PR are pre-existing on the base branch, not from this change.

Check Status
Tornado: agent [3/10] fail
CI gate fail (gated on the above)

Tornado: agent [3/10] fails in test/publish-finalized-agent-lane.test.ts > … > reads finalized assertions from the explicitly selected non-default agent lane, on expect(rfc64CatalogCalls).toHaveLength(1) — an RFC-64 catalog publish path with no connection to backpressure.

The same job fails identically on testnet-canary itself, at 82ddb0358 (run 31061793199, 01:06 UTC — before this PR head):

FAIL test/publish-finalized-agent-lane.test.ts > DKGAgent publishFromFinalizedAssertion agent lane
     > reads finalized assertions from the explicitly selected non-default agent lane
AssertionError: expected [] to have a length of 1 but got +0
Tests  1 failed | 331 passed | 3 skipped (335)

Same file, same test, same assertion, same 1/331/3 counts as this PR run. It also passes locally on this branch (vitest --config vitest.unit.config.ts test/publish-finalized-agent-lane.test.ts → 7/7), so it looks shard- or environment-dependent rather than deterministic.

Every lane that covers this change is green: the core lane, the other nine agent shards, all four CLI shards, storage, and query. I have not touched it — it is the base branch's to fix, and I could not find an open issue tracking it. Happy to file one if it is not already known.

@Jurij89

Jurij89 commented Aug 6, 2026

Copy link
Copy Markdown
Contributor Author

Correction and follow-up to my previous CI note. I re-ran the two failed jobs on the same commit — they fail again, identically. It is not a flake. My earlier "looks shard- or environment-dependent rather than deterministic" was wrong; scratch that line. The rest of that comment stands, and the picture is now sharper.

It is a deterministic breakage on testnet-canary, introduced between two base commits.

testnet-canary run commit Tornado: agent [3/10]
31057888726 23:53 e99a5926c pass
31061793199 01:06 82ddb0358 fail
31080849334 07:25 9cc29ca8d fail

The window e99a5926c..82ddb0358 is #2099 + #2101 (RFC-64 SWM inventory), and it edits the failing test itself (packages/agent/test/publish-finalized-agent-lane.test.ts, +15).

Why this PR is not involved, two independent ways:

  1. Shard 3 runs 27 files | 335 tests on both the base run and this PR run — identical. This PR adds two tests to packages/agent/test/sync-backpressure.test.ts, so if that file were in shard 3 the count would read 337. It is not in this shard; the shard composition is untouched.
  2. This branch is based on 26c27a99e, which predates 82ddb0358 (git merge-base --is-ancestor says no). The failing test passes in this worktree at 7/7 because this tree still has the pre-feat(rfc64): maintain SWM-only inventory in shadow mode #2101 version of it. CI red here is inherited from the base through the PR merge ref, not produced by the diff.

Where it goes wrong (packages/agent/src/dkg-agent-rfc64-catalog-auto-publish.ts @ 82ddb0358): observeRfc64ConfirmedVmV1 opens with a validation gate over contextGraphId, assertionCoordinate, subGraphName and the seal, and on failure warns "RFC-64 post-confirmation observer input was invalid: …" and returns early — before the Promise.all that runs both observers. So neither recordConfirmedRfc64PublicCatalogAssetV1 nor removeRfc64SwmAuthorInventoryShadowV1 is ever called, which is exactly the observed expect(rfc64CatalogCalls).toHaveLength(1)0. Either the new gate rejects a shape it should accept, or the test fixture no longer satisfies it.

That belongs to #2099/#2101, not here — I have not touched it. Happy to open an issue against it if one is not already tracked; say the word and I will.

Every lane that covers this change is green: core, the other nine agent shards, all four CLI shards, storage and query.

Merge-readiness review, three findings on d1c11aa.

Two of them are the same defect, and it is one this PR introduced. Before this
change a shared lane published `queueLimit: null`, so the limit gauge was never
recorded for it and there was no pair to be inconsistent. Publishing the pool's
ceiling per lane created one: `recordBackpressureSnapshotMetrics` still emitted
`queue_depth` as the lane's own backlog, so a shared lane exported
`queue_depth=1` against `queue_limit=4` and a metric-only utilization alert read
25% while the lane was classified `degraded` at 75% — underreporting exactly the
pressure this PR exists to surface, on the surface W1 alerting actually reads.

`dkg.backpressure.pressure_depth{scheduler,lane}` now carries the numerator that
belongs with `queue_limit`. `queue_depth` is unchanged and remains the
attribution signal; on a partitioned lane the two are equal, so no consumer
needs to know the model. Snapshot, log line and metrics now expose the same
normalized shape.

The third finding is a comment that lied: `SchedulerPressureCapacity` told
maintainers that partitioned lane allocations "add up to the scheduler's
ceiling", while nothing validates that and this PR's own store test deliberately
uses a scheduler ceiling below the sum. The scheduler-level limit is documented
as an independent rollup ceiling now, with the sum language dropped.

Test: metric emission is captured through a stand-in meter provider, which also
pins the published instrument NAMES. Patching the instrument objects does not
work — with no global provider the API returns one shared no-op instrument, so
stubbing one gauge stubs them all, and the first version of this test silently
captured 24 records across every gauge. Exporting the lane backlog as
`pressure_depth` is a mutant that kills only this test.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@otReviewAgent otReviewAgent left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Operational Notice: Review Agent could not complete this review.

Verification reviewer failed: WARNING: failed to clean up stale arg0 temp dirs: Permission denied (os error 13)

@otReviewAgent otReviewAgent left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Operational Notice: Review Agent could not complete this review.

Business logic reviewer failed: WARNING: failed to clean up stale arg0 temp dirs: Permission denied (os error 13)

@Jurij89 Jurij89 left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Independent review at d1c11aad7

No blockers. The core fix is correct, and all four load-bearing invariants survive execution rather than argument — I built a differential harness that loads the base and head blobs byte-identically and ran them side by side under a frozen clock. Three things I would address before merge, all cheap, plus a set of claim/code mismatches that matter because the CHANGELOG bullet becomes release notes.


CI red is not attributable to this PR

  • Tornado: agent [3/10] — the single failure is packages/agent/test/publish-finalized-agent-lane.test.ts:238, expected [] to have a length of 1 but got +0 (331 passed / 1 failed / 3 skipped). The same file, test name, assertion and tally fail on the merge base 82ddb03584fd without this PR (push run 31061793199, job 92491484234), and again on the newer base head 9cc29ca8de22, where re-sharding moved the same failure to [7/10]. That is the #1786 author-selectable VM publish area; none of the six changed files is on its path.
  • CI gate — job 92553933752 contains no test output at all. Its only errors are tornado-agent ended with failure / Process completed with exit code 1. It is the downstream aggregate, and it is red on testnet-canary at both base heads.

This PR's own lanes are green: Tornado: core + RDF + storage shows ✓ test/backpressure-observability.test.ts (12 tests) inside 106 passed / 1673 passed.


What holds — verified by execution, not reading

Claim Verdict Evidence
(1) /api/status rollup cannot change for an all-shared scheduler HOLDS 0 differences in snapshot().state or totals across 172,800 exhaustive type-legal shared shapes, 288,000 including pathological thresholds, and 30,000 randomized op sequences with mid-run updateCapacity
(3) sumLaneLimits can never sum a pool ceiling HOLDS totals.queueLimit/inflightLimit were always exactly normalizeLimit(pool) or null — never a multiple — across all 288,000, including every mixed set/null combination
(4) "lane":"all" is unreachable for a shared scheduler HOLDS 0 reachable hits; the only hits (2,016/288,000) need an empty scheduler with a zeroed age threshold, and sync-global sets 15,000/120,000. Store [backpressure] records byte-identical in 500/500 randomized 40-step monitor runs
(5) No shipped rule or dashboard reads queue_limit/inflight_limit HOLDS repo-wide grep: the only dkg.backpressure.* consumer anywhere is oldest_queued_age_ms at w1-rules.yaml:101,202. packages/node-ui has no backpressure reference at all
(6a) k ≤ 4 lanes reach sync-global HOLDS SyncSchedulerLane has 6 members, but pre_authorization/responder go to the responder limiter, whose PriorityAdmissionQueue (sync-handler.ts:261) passes no observability block — so it never calls updatePressureCapacity and is never registered

And on the question I most wanted settled — does this swap an always-healthy signal for an always-saturated one? It does not. At the real limits (queueLimit = 4, inflightLimit = 2) the executed ladder is: pool 0–2 healthy, 3 degraded, 4 saturated, and an idle lane correctly stays healthy however full the pool. Against #2006's measured distribution a lane holding work reads saturated ~55% (receiver 1) / ~39% (receiver 2) of a 2 h incident.


Three to address before merge

① The fix stops at the JSON and the log — metrics still mix the pair

recordBackpressureSnapshotMetrics is untouched (no hunk in :654-688). It calls record(lane.lane, lane) at :687, and the closure reads values.queued (:668) and values.queueLimit (:673). Because a shared lane's queueLimit is no longer null, the if (values.queueLimit !== null) gate at :672 now emits per-lane queue_limit (and :675 inflight_limit) series for sync-global where base emitted none — carrying the pool ceiling, repeated on every lane, including idle ones. pressureQueued reaches no metric and capacityModel is no attribute.

Executed against the pinned head with production capacity, pool full at 4 split changelog=3 / durable=1: the API reports both lanes saturated (pressureQueued 4/4), while the metric stream at the same instant emits queue_depth{lane="durable"}=1 against queue_limit{lane="durable"}=425% utilization on a lane the API calls saturated.

Three things genuinely reduce the severity here, and I checked each rather than assuming:

  • The correct pair is already emitted. For a shared lane with work waiting, pressureQueued is totals.queued and its ceiling is totals.queueLimit, so queue_depth{lane="all"} / queue_limit{lane="all"} is exactly the new predicate — on base and head alike. The number is not missing; per-lane replication and a model label are.
  • sum() was never safe on this metric. record('all', …) at :679 shares the instrument name with the lane rows, so sum(dkg_backpressure_queue_limit{scheduler="store"}) already double-counted on base. This PR widens an existing hazard rather than creating one.
  • No shipped consumer, per claim (5) above.

So this is not live breakage — it is a trap laid precisely for the W1 central-collector work that #2075 explicitly defers to, on a function with zero test coverage anywhere in the repo. Either record lane.pressureQueued ?? lane.queued as the lane depth, or gate the per-lane ceilings on capacityModel !== 'shared' and keep them on the all row — or scope it out explicitly in the description, which is the cheapest honest option.

② Depth-first classification masks the per-lane age signal

Precedence is saturated > degraded, and the depth branch is the else if immediately ahead of the age branch (:490-508). Under shared the depth term is the pool's depth, identical on every lane, so when the pool is full it fires everywhere at once and outranks oldestQueuedAgeMs >= degradedQueueAgeMs — the only per-lane term in the classifier.

Executed, pool 4/4, durable queued 90 s ago and the other three at 0 ms: base gives durable=degraded, others healthy (two states, the stuck lane visible); head gives all four saturated (one state). Against #2006's counters, rejections explain at most 5.8% / 1.7% of the saturated windows, so >90% of that saturation is depth-driven — this is the common case for roughly half an incident.

The description discloses that lane state replicates the pool's pressure and that queued/queuedOperations remain the attribution signal. That covers who is waiting; it does not cover how long, which is what the age branch carried. Bounded — oldestQueuedAgeMs stays on the row and is the only metric the shipped alert rules read, so alerting is unaffected — but the state column is where the triage docs currently point first.

Filed as #2109 with three options and an acceptance test; a stateReason: 'depth' | 'age' | 'rejection' | 'active_age' discriminator is the smallest fix and would also resolve the saturated-with-pressureQueued: 0 oddity below. Fine to take there rather than here — but it should be a decision, not an inheritance.

③ The inflight half of the same mismatch is left in place

laneCapacityFor resolves inflightLimit from the pool (:453-455) while laneSnapshot still publishes lane-local inflight: active.length (:517), and there is no pressureInflight. Pre-PR this was invisible for sync-global: lane inflightLimit was null, so the metric was skipped and the only record carrying an inflight pair was the lane:"all" rollup, which reported 2/2 correctly. Claim (4) removes that record.

Executed on the real PriorityAdmissionQueue, queue 4 / inflight 2, two admissions running and one queued per lane: a row now reads lane=swm_recovery state=saturated queued=1 pressureQueued=4 queueLimit=4 inflight=0 inflightLimit=2, and the same shape lands in daemon.log. An operator reads that as the concurrency pool is idle when it is 100% occupied and is the reason nothing drains. Pre-PR the same incident produced one correct record.

This is the same defect class the PR exists to fix, on a surface the PR newly populates. Minimum fix: report inflightLimit: null on shared lane rows so no consumer can form the ratio; better: extend the pressure pair to concurrency with the same omit-when-equal rule.


Claim / code mismatches

These are worth correcting because the ### Fixed bullet ships as release notes.

  • Claim (2)'s stated proof is false as written. "The added laneQueued > 0 guard is implied by the comparisons it guards" holds only for degradedQueueUtilization > 0. The old degraded branch evaluated queued.length / queueLimit >= threshold, which is 0 >= 0true — on an idle lane at threshold 0 or negative, while depthApplies (:449) is false. Executed: frozen clock, {lanes: {interactive: {queueLimit: 4}}}, degradedQueueUtilization: 0 → base degraded, head healthy; identical at −0.5. 3,761/30,000 randomized hits once that threshold entered the generator, and exactly 0/30,000 without it. No in-tree caller sets it, but SchedulerPressureThresholds is exported public API. Either scope the guard (… && (!shared || laneQueued > 0)) or soften the claim to "unchanged for every positive utilization threshold", which is what was actually proven.

  • Claim (6)'s log-volume arithmetic is wrong in both directions. "1 line/minute to k (≤ 4)" takes the 60 s summary cadence as the baseline, but per-lane records are emitted on transition too (observeSample :794-803) at the 5 s sample interval, and sync lanes were already leaving healthy on oldestQueuedAgeMs >= 15 s pre-PR — so the base was never 1/min. Two measurements against the shipped cadence: on a trace calibrated to #2006 receiver 1's distribution, 754 → 954 lines per 2 h (1.27×, base ≈ 6.3/min); on an adversarially oscillating pool across the 75%/100% band, 7 → 48 lines/min. The ceiling is lanes × samples, not lanes. The realistic number is more favourable than the claim — but the claim as stated is wrong in both the baseline and the bound.

  • capacityModel reads "partitioned" until the first acquire(). PriorityAdmissionQueue registers in its constructor (:125) while the only updatePressureCapacity call lives in acquireInternal (:177), so a freshly booted daemon advertises the wrong model on the field the docstring calls authoritative. It is permanent under DKG_SYNC_GLOBAL_MAX_INFLIGHT=0, where withGlobalSyncBackpressure short-circuits at sync/backpressure.ts:293-303 without ever acquiring. Zero code readers today, never wrong on a lane row (updatePressureCapacity strictly precedes and is co-gated with every lane-creating call), and no effect on classification — so LOW. The one-line fix was verified inert: pass capacity: { capacityModel: 'shared' } in the super({…}) call; post-acquire output is byte-identical because updateCapacity replaces wholesale.

  • docs:85 overstates pressureQueued for an unbounded pool. With capacityModel: 'shared' and queueLimit: null, depthPressure is null (:449), so each lane falls back to its own backlog at :480. Executed with 2 queued in durable and 1 in changelog: rows report pressureQueued 2 and 1 while the pool holds 3. Harmless (the row's queueLimit is null so no ratio is computable) but not what the table says.

  • docs:125 says 5 × 7; the closed set has 8 members. SYNC_ADMISSION_SOURCES (policy.ts:45-54) has 8, and the code comment at sync/backpressure.ts:66 already says 5 × 8. Pre-existing, but this PR edits the same file three lines below.

  • The description says poolQueued; the code and docs say pressureQueued. Stale from an earlier revision — same vocabulary the still-open 🟡 thread quotes.


Tests

The clock is genuinely frozen in every state-asserting test (now: () => 1_000; the agent queue forwards hooks.now into the tracker at priority-admission-queue.ts:118-124), nothing is ever rejected, and the distributed-75% test genuinely and uniquely discriminates its mutant — queueLimit: 4, three lanes at one entry each, so every lane sits at 25% on its own while the pool is at exactly 0.75. That test earns its place, and I could not find another that kills the same mutation.

Two holes worth closing:

  • The snapshot-level capacityModel — the field the doc comment calls authoritative — is asserted by no test built from a real tracker. All nine new tests assert capacityModel only inside a lanes: array; the two top-level occurrences (test:441, :462) are hand-built input to a registry, not an assertion on tracker output. Delete src:380 and every new and pre-existing test stays green, while the API would report {"capacityModel":"partitioned", …, "lanes":[{"capacityModel":"shared", …}]} — the authoritative field contradicting every row beneath it. Fix is two lines: add it as a sibling of state/totals at test:119 and test:264.

  • expect(line('all')).toBeUndefined() (test:494) is vacuous with respect to this PR. The suppression it exercises lives at src:761, which this PR does not touch, and the test feeds it hand-built sources whose equal states the test itself chose (:412, :439, :460). So load-bearing claim (4) has no evidence from a real shared tracker. Concretely: change the shared numerator at :460 to this.queued.size - laneQueued; core tests 1 and 3 and agent test A die, but the property this assertion claims to prove is now false — with pool 4/4 and durable holding 2, that lane reads 2/4 healthy while the rollup is saturated, so sync-global resumes emitting "lane":"all" records, the opposite of what is documented, and nothing observes it. Driving the monitor from a real tracker with the spread shape would make the identity, not the author's chosen states, the thing under test.

Smaller:

  • "fills the queue on its own" (test:140) has a kill set that is a strict subset of the spread test's. With this.queued.size === laneQueued === 4 the pool-vs-lane distinction — the entire subject of the fix — is invisible to it, and it asserts neither capacityModel nor pressureQueued. Mutate :460 to laneQueued and it still passes while test:99 and agent test A die; mutate :449 to laneQueued > 1 and it survives while the spread test dies on changelog. This contradicts "each mutant kills exactly one test". (The partitioned pair does not collapse the same way — test:274 is the only test in the suite where the rollup strictly outranks every lane, so it uniquely guards totals.queued >= totals.queueLimit.)
  • Claim (3) is exercised but never asserted. test:169 reaches sumLaneLimits('inflightLimit', …) and does not assert totals.inflightLimit. Relax the :547 early return to skip nulls and sum the rest — a plausible future "improvement" — and all nine tests stay green while a shared scheduler publishing only inflightLimit would report N× the pool. Adding inflightLimit: null to that test's totals, plus one case with {inflightLimit: 2, capacityModel: 'shared'} and no queueLimit, pins it.
  • pressureQueued's ?? queued.length fallback is untested where it differs from ?? 0. Only the empty-backlog case is covered. Mutate :480 to ?? 0 and all nine pass, while an unbounded lane would report pressureQueued: 0 beside queued: 7 and the monitor's inequality at :847 would stamp a fabricated "pressureQueued":0 on every record.
  • Agent test B's lane scoping is decorative. lanes.find(l => l.lane === 'durable') asserts capacityModel, queueLimit and inflightLimit, all three of which resolve from scheduler-level capacity under shared and are therefore identical on every row — and this.lanes is never pruned, so the find can match a stale row left by an earlier test against the module singleton. Change the lane default at sync/backpressure.ts:305 from 'durable' to 'changelog' and this test still passes. lanes.every(l => l.capacityModel === 'shared') plus one lane-specific fact would be a real witness.
  • recordBackpressureSnapshotMetrics has no test in the repo, and this PR changes what it emits.

One calibration note

The restored degraded band is one queue slot wide: queueLimit = limit * 2 = 4 and degradedQueueUtilization falls through to the 0.75 default, so the ladder is 0–2 healthy, exactly 3 degraded, 4 saturated. The sync thresholds block (sync/backpressure.ts:100-103) overrides degradedQueueAgeMs and stalledActiveAgeMs but not the utilization. Since reconcileSyncFromConnectedPeers (dkg-agent-lifecycle.ts:4238-4278) fires un-awaited across 7–14 peers, depth goes 0→4 faster than the 5 s monitor sample, so the warning band is frequently skipped — consistent with #2006's own counters showing only ~9% degraded samples against 55%/39% saturated. Setting degradedQueueUtilization: 0.5 explicitly for sync-global would put the warning at depth 2 and buy two slots of headroom instead of one. Worth a line of rationale in the docs either way.


Summary. The mechanism is right, the hard invariants are proven, store is genuinely untouched, and the additive fields break no consumer — I checked for strict-equality and snapshot assertions on a lane row across the repo and there are none. What I would want first: ① and ③ resolved or explicitly scoped out, the two test holes closed, and the overstated claims in the CHANGELOG corrected. ② is tracked at #2109.

@Jurij89

Jurij89 commented Aug 6, 2026

Copy link
Copy Markdown
Contributor Author

Third CI note, and one more correction to my own reasoning.

On the new head 47c6bc27a the failure moved shards: Tornado: agent [3/10] now passes and Tornado: agent [7/10] fails, with the same file, same test, same assertion:

FAIL test/publish-finalized-agent-lane.test.ts > … > reads finalized assertions from the explicitly selected non-default agent lane
AssertionError: expected [] to have a length of 1 but got +0

Correction: in my earlier note I argued this PR was not involved partly because "shard 3 runs 27 files / 335 tests on both base and this PR — identical". The shard plan is evidently dynamic — shard 7 here ran 38 files / 538 tests — so that argument does not hold as stated. Withdrawing it.

What still holds, and is sufficient:

  1. The identical failure occurs on testnet-canary with none of this PR present — run 31061793199 at 82ddb0358, same file, same test, same assertion. It first appears in the window e99a5926c..82ddb0358 (RFC-64 P3 2/3: persist signed SWM-only inventory #2099 + feat(rfc64): maintain SWM-only inventory in shadow mode #2101), which also edits that test file.
  2. This branch is based on 26c27a99e, which predates 82ddb0358. The red arrives through the PR merge ref, not the diff.
  3. No coupling. The failing test builds its subject with Object.create(DKGAgent.prototype) and stubs the methods it exercises; it imports nothing from backpressure-observability, telemetry-api, or priority-admission-queue — the only files this PR changes — and never admits work through a scheduler. Its outcome is decided by observeRfc64ConfirmedVmV1 and by the test fixture, both base-branch code this PR does not touch.
  4. Nine of ten agent shards pass, as do core, all four CLI shards, storage and query. Exactly one test fails, and it is the same one that fails on base.

Still not mine to fix, and still blocking the gate — #2107 cannot go green until that base-branch failure is resolved.

…e proofs

Merge-readiness review at d1c11aa. ① was already fixed in 47c6bc2; this
takes ③, the claim/code mismatches, and the test holes. ② is deferred to #2109
as a decision — see below.

③ The inflight half of the same defect. `laneCapacityFor` resolved
`inflightLimit` from the pool while the row still published a lane-local
`inflight`, so a shared row read `inflight: 0, inflightLimit: 2` — idle
concurrency, while the pool was fully occupied and was the reason nothing
drained. Pre-PR this was invisible: the lane limit was null, so no ratio was
formable. `pressureInflight` now applies the `pressureQueued` rule to
concurrency, on the row, in the log line under the same omit-when-equal rule,
and as `dkg.backpressure.pressure_inflight`.

Claim (2) was false as written. "The guard is implied by the comparisons it
guards" holds only for a positive `degradedQueueUtilization`, and that threshold
is caller-supplied public API — at 0 the old branch read `0 / limit >= 0` as
degraded on an idle lane. Verified: base degraded, head healthy. The guard is
now scoped to `shared`, so a private allocation is untouched by construction
rather than by argument.

`capacityModel` read `partitioned` until the first acquire, because the queue
registers in its constructor and publishes capacity in `acquireInternal` — and
permanently so when sync admission is disabled. The model is a property of the
queue, so it is declared in the `super()` call.

Also corrected: the CHANGELOG's log-volume arithmetic (per-lane records emit on
transition at the 5 s interval, so the base was never 1/min and the bound is
lanes x samples), the doc claim that partitioned lane allocations sum to the
scheduler ceiling, `pressureQueued` on an unbounded pool, and 5 x 7 -> 5 x 8.

Tests, each verified against the mutant it exists to kill:
- the scheduler-level `capacityModel` is asserted from a real tracker (deleting
  it previously left every test green while the API contradicted its own rows);
- the monitor's `"lane":"all"` suppression is driven from a real tracker, so the
  identity is under test rather than states this test chose — the old assertion
  survived the mutant that breaks it, this one does not;
- an unbounded shared pool pins the `?? queued.length` fallback (`?? 0` would
  stamp a fabricated `pressureQueued: 0` beside `queued: 2` on every record);
- `totals.inflightLimit` is asserted, so relaxing `sumLaneLimits` to skip nulls
  is caught;
- the concentrated test asserts `capacityModel`/`pressureQueued` so its kill set
  is no longer a strict subset of the spread test's;
- the agent witness asserts over ALL lane rows plus one genuinely lane-scoped
  fact, instead of a `find` that matched fields identical on every row and could
  hit a stale row on the module singleton.

Deferred, deliberately: ② depth-first classification outranks the per-lane age
signal, so a lane stuck 90 s and a lane queued 0 ms both read `saturated` when
the pool is full. Real, and #2109 has three options and an acceptance test. The
smallest one there — a `stateReason` discriminator — does not actually separate
those two lanes, since both would report `depth`; that deserves the dedicated
issue rather than a guess inherited from this PR.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@otReviewAgent otReviewAgent left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Operational Notice: Review Agent could not complete this review.

Business logic reviewer failed: WARNING: failed to clean up stale arg0 temp dirs: Permission denied (os error 13)

@Jurij89

Jurij89 commented Aug 6, 2026

Copy link
Copy Markdown
Contributor Author

Thanks — this is the most useful review the PR has had, and the differential harness caught things argument would not have. Everything actionable is now in, at abcbc663e. Taking the findings in order.

① Metrics mixed the pair — already fixed, before the review landed

Fixed in 47c6bc27a, which you would not have seen at d1c11aad7. I went with a third option rather than either you listed: a separate dkg.backpressure.pressure_depth{scheduler,lane} gauge. queue_depth stays this lane own backlog, so attribution survives, and pressure_depth / queue_limit is utilization on every scheduler — equal to queue_depth on a partitioned lane, so no consumer branches on the model. An idle shared lane now exports pressure_depth: 0 against the pool ceiling, i.e. 0%, rather than looking full.

Your point that recordBackpressureSnapshotMetrics had no test anywhere in the repo was right, and it now has one.

③ The inflight half — correct, and the same defect

inflightLimit resolved from the pool while the row published a lane-local inflight, so a shared row read inflight: 0, inflightLimit: 2 — idle concurrency, while the pool was 100% occupied and was the reason nothing drained. Exactly as you say, pre-PR this was invisible because the lane limit was null and no ratio was formable. I took the "better" option: pressureInflight applies the pressureQueued rule to concurrency, on the row, in the log under the same omit-when-equal rule, and as dkg.backpressure.pressure_inflight.

② Depth-first classification masking the age signal — deferred to #2109, as a decision

Real, and I am not shipping it as an inheritance. My reasoning for leaving it there: the smallest option you name, a stateReason discriminator, does not actually separate the two lanes in your own example — a lane stuck 90 s and a lane queued 0 ms are both saturated for reason depth when the pool is full, because age only ever produces degraded. So the fix needs the design work #2109 already frames, not a guess made under this PR. oldestQueuedAgeMs stays on every row and in every record, and it is the only backpressure metric the shipped alert rules read, so nothing regresses for alerting in the meantime.

Claim/code mismatches — all corrected

  • Claim (2) was false as written, and this is the one I am most glad you executed. "Implied by the comparisons it guards" holds only for a positive degradedQueueUtilization, which is caller-supplied public API. I reproduced it: partitioned, idle lane, threshold 0 → base degraded, head healthy. Rather than soften the claim I scoped the guard to shared, so a private allocation is now untouched by construction instead of by argument.
  • Claim (6) log volume — corrected in the CHANGELOG to your measurement: per-lane records emit on transition at the 5 s interval, so the base was never 1/min, and the bound is lanes x samples, not lanes.
  • capacityModel before the first acquire — fixed with your one-line suggestion, declared in the super() call, since the model is a property of the queue rather than of a call.
  • docs:85 unbounded pool, docs:125 5x7 → 5x8, and the "lane allocations sum to the scheduler ceiling" claim in the type and the docs — all corrected. The last one was the same false invariant the 🟡 thread flagged.
  • The description said poolQueued; it says pressureQueued now.

Test holes — closed, each against its mutant

Hole Now Mutant that kills it
snapshot-level capacityModel asserted by no real tracker asserted in the shared and partitioned tests deleting the field kills 3 tests (previously 0)
expect(line("all")).toBeUndefined() vacuous new test drives the monitor from a real tracker your exact this.queued.size - laneQueued — the old assertion survived it, the new one dies
?? queued.length fallback untested new unbounded-pool test ?? 0
claim (3) exercised, never asserted totals.inflightLimit asserted relaxing sumLaneLimits to skip nulls
concentrated test kill set a strict subset asserts capacityModel + pressureQueued no longer survives the pool-vs-lane mutation
agent witness decorative asserts over all rows + one lane-scoped fact (inflight and activeOperations on durable) changing the lane default no longer passes

Calibration note

Taken as a docs point rather than a code change — degradedQueueUtilization stays at the 0.75 default, and I would rather not retune the band in the same PR that makes it reachable for the first time. Your ladder (0–2 healthy, 3 degraded, 4 saturated, one slot wide) and the 0.5 suggestion are worth their own issue with the #2006 numbers attached.

Not addressed, and out of scope by design: the still-open #2003 finding that the classifier ignores inflightLimit. pressureInflight is reported, never judged.

@Jurij89 Jurij89 left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Round-2 review at abcbc663e

All six round-1 findings are fixed, and all four invariants re-proven by execution at far greater scale than round 1. One ask before merge — a test — plus a short list of documentation corrections, one of which is an over-correction my own round-1 review prompted.

The invariants, re-proven rather than assumed

The round-2 delta touched laneSnapshot twice, so these were re-established from scratch against the base blob, not carried over:

Invariant Result
Store lane states unchanged vs base — now unconditionally HOLDS. 86,400 differential scenarios sweeping lane limits (0, null, undefined, negative, fractional, NaN, ±Infinity, MAX_SAFE_INTEGER), depths, ages either side of every threshold, ghost lanes in the map with no traffic and traffic on lanes absent from it — and degradedQueueUtilization across {-1, 0, 0.0001, …, 1.5, NaN, ±Inf}. Zero divergence. The round-1 head does diverge in the same sweep, which is what proves fix (3) closed it
/api/status rollup unchanged for an all-shared scheduler HOLDS. 1,400 shared scenarios, zero divergence in snapshot.state or the whole totals object
sumLaneLimits can never sum a pool ceiling HOLDS. totals.queueLimit was always the normalized pool ceiling or null, never a multiple
No "lane":"all" for sync-global; store [backpressure] records byte-identical HOLDS. 500 randomized store scenarios / 20,262 records byte-identical to base, none carrying pressureInflight. 19,525 shared records, zero "lane":"all" against base's 724

Also verified: packages/core 15/15 and packages/agent 34/34 pass with the dependency closure actually built and the artifact grepped rather than assumed; tsc --noEmit clean; and tools/observability/verify-w1-render.mjs parses telemetry-api.ts 71/71 instruments with both new gauges recognised and no duplicate name.

Fix-by-fix

Round-1 finding Verdict
① Metrics path not carried through FIXED. pressure_depth exports the pool depth beside a lane-local queue_depth; executed at pool capacity it gives utilization 1.00 against saturated and 0.75 against degraded, and equals queue_depth on every partitioned row. The ?? values.queued fallback preserves older hand-built sources
② Inflight half left in place FIXED, with a caveat — see below. The value is right where it matters: this.active.size equals totals.inflight on every shared row across 600 scenarios, and it is genuinely the count canRun compares against
③ Claim (2)'s proof false at degradedQueueUtilization ≤ 0 FIXED, and fixed the better way — the guard is scoped to shared rather than the claim being softened, so invariant 2 is now unconditionally true instead of true-in-practice
capacityModel wrong before first acquire() FIXED. Verified through the real PriorityAdmissionQueue: shared from registration onward, with limits still null (no ceiling fabricated). The DKG_SYNC_GLOBAL_MAX_INFLIGHT=0 permanent case is closed too
⑤ Log-volume arithmetic FIXED — 1.27× measured, stated as ~1.3× with the worst case named
⑥ Docs (5 × 7, unbounded-pool wording) FIXED, and the unbounded-pool case now has a real test

On the tests: four of six round-1 test findings are fixed properly. The vacuous expect(line('all')).toBeUndefined() is now driven from a real shared tracker (test:574) — mutating the shared numerator to this.queued.size - laneQueued now fails three assertions, which is exactly the kill that was missing. The snapshot-level capacityModel is asserted from real trackers at :161, :200, :340 and on the production singleton at agent :1388. The pressure_depth metrics test pins values and metric names, not just "an instrument fired".


The one ask: three of the four round-2 source fixes are revertible with the suite green

  • pressureInflight has zero test references repo-wide (grep returns hits only in src, telemetry-api and docs). Reverting :506 to active.length — an exact revert of the fix requested in round 1 — leaves both suites passing. So do deleting the log field at :890, dropping it from the snapshot at :544, or misspelling the gauge at telemetry-api.ts:420.
  • Fix (3) is pinned by nothingdegradedQueueUtilization appears in zero test files in the repo, so the scoped and unscoped forms are observationally identical across the whole suite. The invariant it restores rests on reading alone.
  • Fix (4) is pinned by nothing — every agent-test snapshot is taken post-acquire (:1246, :1269, :1320, :1381), so deleting priority-admission-queue.ts:128 leaves 70/70 green.

Nothing is wrong today and none of these can move a lane state, the rollup, or /api/status — the classifier reads neither pressure field. But this is precisely the mechanism by which a review finding comes back silently, and each guard is a few lines. The pressure_inflight one is the one I would insist on: a shared tracker with two lanes each holding one active record, asserting inflight === 1 while pressure_inflight === 2, plus an equality check on a partitioned row.

One caution on that test — the obvious place to put it doesn't work. Extending the real-tracker monitor test at :574 can't express it: that test enqueues 4 into a queueLimit of 4 and starts none, so this.active.size === 0 and the omit-when-equal rule suppresses the field on every record; calling start() needs a ticket, but the four enqueue() return values are discarded, and starting one drops pool depth 4→3, flipping that test's asserted saturated to degraded. It needs its own tracker.


Documentation

The partitioned correction over-corrected — and this one is on me. :11 now says lane allocations "are not summed into it and are not validated against it". The second half is right; the first is false in both directions. sumLaneLimits derives the rollup ceiling as exactly the lane sum when a scheduler publishes none, and StorePriorityScheduler publishes its scheduler-level ceiling as literally that sum:

// packages/storage/src/store-priority-scheduler.ts:314
queueLimit: Object.values(this.queueLimits).reduce((sum, value) => sum + value, 0),

My round-1 review relayed "sum() was never safe on this metric" — which is true of the lane="all" metric row sharing an instrument name with the lane rows — and that got generalised into "not summed", which is a different claim. The accurate statement is the independence one: nothing validates the scheduler ceiling against the lane allocations, so do not derive either from the other — while noting that a scheduler may publish the sum (store does) and that sumLaneLimits falls back to it. Same correction applies to docs:74.

pressureInflight does not follow "the same rule as pressureQueued", stated at :110-114, docs:86, and in the commit message. pressureQueued has two carve-outs and pressureInflight has neither:

  1. Idle lane. depthApplies requires laneQueued > 0 under shared (:469), so an idle lane reports its own 0. pressureInflight at :506 keys off capacityModel alone, so an idle shared lane publishes the whole pool — executed on the production shape, three healthy lanes with inflight: 0 export pressure_inflight: 2 against inflight_limit: 2.
  2. No ceiling. pressureQueued falls back to the lane's own backlog when queueLimit is null; pressureInflight never consults inflightLimit, so an unbounded shared pool publishes pressureInflight: 2 beside inflightLimit: null.

I went in expecting this to be a bug and came out satisfied it is not. pressureQueued must equal the classifier's numerator — otherwise a row reads pressureQueued/queueLimit = 4/4 next to state: healthy, a self-contradiction — whereas :503-504 says of the inflight pair, correctly, "the classifier reads neither — this pair is reported, not judged". No branch of laneSnapshot reads inflight or inflightLimit, so no contradiction is constructible, and on a shared row the denominator is the pool's too, making the ratio a pool ratio by construction, identical across rows and equal to the all row. So it is the three "same rule" statements that are wrong, not the code. Say what it actually is: on a shared row, the pool's occupancy, on every row, always. (I'd also drop the gating suggestion if it comes up — keying the numerator on queued.length > 0 would make the series step 0→2 on an unrelated enqueue with no change in real concurrency, which is worse than a constant.)

A comment that states a kill the code makes impossibletest:291-294: "Relax sumLaneLimits to skip nulls and sum the rest … and this reads 2× the pool while every other assertion stays green." That test's capacity is { queueLimit: 4, inflightLimit: 2 }, both non-null, so normalizeLimit short-circuits and sumLaneLimits is never reached. The mutant cannot be killed there. It is killed — by the unbounded-pool test at :246, where totals.queueLimit: null becomes 0 under that mutation. So the coverage is real and the comment points at the wrong test; move or correct it. Worth flagging because an unrun claim in a comment reads exactly like a passing check.

Smaller:

  • CHANGELOG never mentions pressureInflight or dkg.backpressure.pressure_inflight, and still says "two optional fields" where there are now three. Release-note only — docs:86 and docs:232 cover both correctly. Note the entry was accurate at round 1 and was falsified by the same commit that edited it.
  • docs:80 "Two depths sit on every lane row" is stale now that the table has three rows.
  • docs:85's "where no depth applies — an empty backlog" describes only shared since fix (3) scoped the guard; a partitioned lane now always has depth applied when it has a limit.
  • The metrics table doesn't say the two new pressure gauges must never be summed across lanes — the same warning the capacity table gives for queueLimit.
  • Still not fixed from round 1: the "fills the queue on its own" test (:183) remains a strict subset of the spread test's kill set. Its three added assertions all already exist at :161/:167-169, in a configuration where the two denominators actually disagree (2/1/1 of a pool of 4) rather than coincide (4 of 4). Low stakes; deleting it or giving it a shape only it can cover would both be fine.
  • New surface is +20 gauge series per node, 12 of which are always-equal duplicates on partitioned rows. Reasonable for a uniform consumer contract, but worth being deliberate about.

CI

Red again — Tornado: agent [7/10] and CI gate — and it is the same pre-existing failure, re-sharded from [3/10]: publish-finalized-agent-lane.test.ts > reads finalized assertions from the explicitly selected non-default agent lane, AssertionError: expected [] to have a length of 1 but got +0, with 36 other files passing in that shard. CI gate carries no test output and fails only because it gates on tornado-agent. Not attributable to this PR, as at round 1.


Merge-ready. The mechanism is right, the invariants are proven at scale, store is untouched down to the byte, and every round-1 finding landed — fix (3) in the stronger form. Add the pressure_inflight guard, correct the partitioned comment (my error to fix, not yours) and the three "same rule" statements, and this is done. #2109 tracks the lane-state age-masking design question separately.

…tated claims

Round-2 review at abcbc66. Merge-ready verdict, with one ask and a list of
documentation corrections.

The ask: three of the four round-2 source fixes were revertible with both
suites green — which is the mechanism by which a review finding comes back
silently. Each now has a guard, and each guard was verified against the exact
mutant it exists to kill:

- `pressureInflight` had zero test references repo-wide. A dedicated tracker (as
  the review notes, the monitor test cannot express it: it starts nothing, so
  `active.size` is 0 and the omit-when-equal rule suppresses the field) asserts
  `inflight: 1` beside `pressureInflight: 2` on two shared lanes, the log line,
  and the metric BY NAME. Kills all four reverts: the field, the log field, the
  snapshot value, and a misspelt gauge.
- `degradedQueueUtilization` appears in no other test in the repo, so scoping
  the empty-lane guard to `shared` rested on reading alone. A zero-threshold
  idle lane now asserts `degraded` under `partitioned` and `healthy` under
  `shared`. Unscoping the guard kills it.
- Every agent snapshot was taken post-acquire, so the constructor's declaration
  was unpinned. A fresh queue now asserts `capacityModel: 'shared'` with null
  limits before any admission. Deleting the declaration kills it.

Corrections, three of which are claims of mine that were wrong:

- "lane allocations are not summed into it" over-corrected a round-1 note.
  `sumLaneLimits` derives the rollup as exactly the lane sum when a scheduler
  publishes no ceiling, and `StorePriorityScheduler` publishes its ceiling as
  literally that sum. The accurate claim is the independence one: nothing
  VALIDATES the two against each other, so derive neither from the other.
- `pressureInflight` does not follow "the same rule as `pressureQueued`" —
  `pressureQueued` falls back to the lane's own backlog where no depth was
  classified, and `pressureInflight` has no such fallback. The code is right and
  the three statements were wrong: on a shared row it is the pool's occupancy,
  always. Gating it on queued work would step the series 0 -> N on an unrelated
  enqueue with no change in real concurrency.
- A test comment named a mutant its own fixture makes unreachable: with both
  ceilings non-null, `normalizeLimit` short-circuits and `sumLaneLimits` is
  never reached there. The kill happens in the unbounded-pool test; the comment
  now says so. An unrun claim in a comment reads exactly like a passing check.
- The concentrated test's kill set is a strict subset of the spread test's, and
  the comment now states that outright rather than the PR claiming each mutant
  kills exactly one test. Its role is the issue's literal acceptance criterion
  and the case where both denominators agree.

Plus: CHANGELOG now names `pressureInflight` and the second gauge and says three
optional fields, not two; the metrics table carries the never-sum warning the
capacity table already had; "two depths" and the unbounded-pool wording are
accurate; and the +20 series cost is stated rather than left implicit.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@Jurij89

Jurij89 commented Aug 6, 2026

Copy link
Copy Markdown
Contributor Author

Round-2 review addressed at dfdec049e. The ask, the corrections, and one item I left alone on your reasoning.

The ask — three fixes were revertible with the suite green

You were right that this is how a finding comes back silently, and all three now have a guard. Each was verified against the exact revert you named, not just added:

Fix Guard Mutant it kills
pressureInflight own tracker: two shared lanes each with one active record → inflight: 1, pressureInflight: 2; plus the log line and the metric by name reverting :506 to active.length; deleting the log field; dropping it from the snapshot; misspelling the gauge — all four
depth guard scoped to shared zero-threshold idle lane asserts degraded under partitioned and healthy under shared unscoping the guard
constructor capacityModel fresh queue asserts shared with null limits before any acquire deleting the declaration

Your caution about the placement was right and saved me the detour: the :574 monitor test cannot express the inflight case — it starts nothing, so active.size is 0 and omit-when-equal suppresses the field on every record. It got its own tracker.

Documentation — three of these were my errors, one was the over-correction

  • "not summed into it" — over-corrected, and your diagnosis of how is exact. sumLaneLimits derives the rollup as the lane sum when no scheduler ceiling is published, and StorePriorityScheduler publishes its ceiling as literally Object.values(this.queueLimits).reduce(...). Both halves of my sentence were wrong in different directions. It now says only the independence claim — nothing validates the two against each other, a scheduler may publish the sum, and sumLaneLimits falls back to it. Fixed in the type and at docs:74.
  • The three "same rule" statements — you talked yourself out of a bug and landed where I would have: the code is right and the statements are wrong. pressureQueued must equal the classifier numerator or a row contradicts its own state; nothing classifies on concurrency, so no contradiction is constructible and a shared row ratio is a pool ratio by construction. All three now say what it is — on a shared row, the pool occupancy, always. I also took your parenthetical and did not gate it on queued.length > 0: stepping the series 0 → N on an unrelated enqueue with no change in real concurrency is worse than a constant, and the reason is recorded next to the code so the next reader does not "fix" it.
  • The comment naming an unreachable mutant — correct, and this is the class I keep having to be caught on. With both ceilings non-null, normalizeLimit short-circuits and sumLaneLimits is never reached in that test; the kill is in the unbounded-pool test. The comment now points there. An unrun claim in a comment reads exactly like a passing check.
  • The concentrated test — kept, with the subset stated outright instead of the PR claiming each mutant kills exactly one test. Its role is the issue own literal acceptance criterion and the case where the two denominators agree, so it guards that the fix did not break the shape a naive per-lane denominator would also have caught. If you would still rather it went, say so and it goes.
  • CHANGELOG now names pressureInflight and the second gauge and says three optional fields; docs:80 says three counts; docs:85 scopes the no-depth case to shared; the metrics table carries the never-sum warning the capacity table already had; and the +20 series, 12 of them always-equal duplicates is now stated rather than left implicit.

Calibration and #2109

degradedQueueUtilization: 0.5 for sync-global: still not taken here, for the same reason as before — I would rather not retune the band in the PR that makes it reachable for the first time. Your ladder and the #2006 numbers deserve their own issue. #2109 continues to track the age-masking design question.

Verification on this head: core 15 → 17 backpressure tests and 1664 suite-wide, agent 34 → 35, storage 22, CLI 3, tsc --noEmit clean, and verify-w1-render.mjs reports the same instruments=9 rules=66 selectors=106 as base, so the two new gauges disturb nothing there.

CI red remains the pre-existing publish-finalized-agent-lane failure from #2099/#2101, re-sharded again — not attributable here, and still the one thing standing between this and a green gate.

@otReviewAgent otReviewAgent left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Operational Notice: Review Agent could not complete this review.

Business logic reviewer failed: WARNING: failed to clean up stale arg0 temp dirs: Permission denied (os error 13)

@Jurij89 Jurij89 left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Final review at dfdec049e — ship it

Short pass, because the source delta since abcbc663e is provably comment-only: after stripping comments, backpressure-observability.ts, priority-admission-queue.ts and telemetry-api.ts are byte-identical, and no @ts-ignore / eslint-disable / v8 ignore directive was added (those would have made a strip-diff look clean while changing what the suite measures). So round 2's execution proofs — 86,400 partitioned scenarios with zero divergence from base, 20,262 store log records byte-identical, zero "lane":"all" — carry over untouched rather than needing a re-run.

The three unpinned fixes are now genuinely pinned

Each mutant applied one at a time, restored from a pre-made copy, tree provably clean beforehand and the mutation re-grepped as still present afterwards, with the Tests … matcher proven able to match on the green baseline first.

Mutant Sole failing test Result
pressureInflightactive.length pairs a shared row concurrency count with the ceiling that bounds it KILLED
(!shared || laneQueued > 0)laneQueued > 0 applies the empty-lane guard only where a pool is shared KILLED
delete capacity: { capacityModel: 'shared' } declares the shared pool from registration, before any admission KILLED

Kill sets are disjoint and singleton — no other test fails under any of them, so each new test is the sole guard for its fix and none is redundant with existing coverage.

Two things worth recording because they are the kind of thing that usually goes unchecked:

  • The pressureInflight test's three surfaces each discriminate independently. Under the single revert the test fails at the first assertion and expect throws, so the log-line and metric assertions never execute — that mutant alone proves only the snapshot. Two further targeted mutants that leave the snapshot correct (dropping the omit-when-equal spread from the log record; recording values.inflight instead of values.pressureInflight) each kill the same test at the later assertion. So the four-reverts claim holds — they are four distinct mutants, not four assertions behind one.
  • applies the empty-lane guard only where a pool is shared differs in outcome, not just input. Partitioned idle lane → degraded, shared idle lane → healthy, and the mutant fails on the partitioned direction specifically. That is the exact point where round 1 diverged from base, so the invariant that used to rest on reading now rests on a test.

Baselines: packages/core backpressure suite 17/17, packages/agent sync-backpressure 35/35 (--config vitest.unit.config.ts), and the full packages/core suite 1665/1665 green — the pre-existing failures I expected to have to discount did not reproduce at this head. The agent lane was built and the artifact grepped in packages/core/dist rather than assumed, and re-confirmed after the core-src mutants so neither leaked into dist.

The four overstated claims are now accurate

  • The partitioned comment no longer says allocations are "not summed" — it says nothing validates the scheduler limit against them, names store as a scheduler that publishes their sum, and notes sumLaneLimits falls back to the sum. That is true in both directions, where the previous wording was false in both. (My round-1 review prompted the over-correction, so: fixed correctly, and thank you for taking the note rather than the wording.)
  • pressureInflight is now documented as the pool's occupancy always, with the asymmetry against pressureQueued explained — no state is ever classified on concurrency, so a shared row's ratio is a pool ratio by construction and cannot contradict its own state. That is the right resolution: the code was correct and the "same rule" claim was the defect.
  • The pressure_inflight metrics row dropped "on the same rule as pressure_depth", and a new paragraph states that neither pressure gauge may be summed across lanes — correctly noting the lane="all" row shares the instrument names, so an unfiltered sum() double-counts on every scheduler.
  • test:294's comment now says plainly that the sumLaneLimits mutant cannot be killed by that test and names the unbounded-pool test that does kill it. An unrun claim in a comment reads exactly like a passing check, so this one mattered more than its size.

CI

Fully green: 57 pass, 0 fail, all 10 agent shards passing. publish-finalized-agent-lane.test.ts, which was red at r1 and r2, passes here with no related change — so it is flaky rather than a deterministic base failure. Worth correcting my earlier wording: "pre-existing red lane" was right about attribution, but "flaky" is the accurate description.

One residue

docs/use-dkg/backpressure-observability.md:89 still reads:

Both fields are optional on the type … absent capacityModel means partitioned, absent pressureQueued means queued.

There are three optional fields now, and the fallback list is missing absent pressureInflight means inflight. The CHANGELOG was corrected to "three optional fields" in this same commit; this sentence sits two lines under the table that gained the third row and was missed. One-line fix, and the last thing I have.

Also unchanged and fine: the concentrated "fills the queue on its own" test keeps a kill set that is a subset of the spread test's, now with a comment saying so deliberately — it is the issue's literal acceptance criterion and the case where the two denominators agree. Documenting the intent is a legitimate resolution.


Verdict: merge. Every finding from both prior rounds is closed, the fixes that were unpinned are now killed by disjoint single-purpose tests, the source delta cannot have changed behaviour, and CI is clean. #2109 tracks the lane-state age-masking design question on its own, as intended.

New surface for the record: three optional lane fields, one optional snapshot field, two gauges, ~+20 series per node.

The sentence under the lane-field table still said "both fields" and listed two
fallbacks; it sits two lines below the row that gained `pressureInflight` and was
missed when the CHANGELOG was corrected to three in the same commit.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@Jurij89

Jurij89 commented Aug 6, 2026

Copy link
Copy Markdown
Contributor Author

Residue fixed at 8f7392eea — docs:89 now reads "All three are optional" and lists the third fallback (absent pressureInflight means inflight). Docs-only; no source or test change.

And a correction I owe on the CI question. You are right that "flaky" is the accurate word, and my earlier wording was wrong — not just imprecise.

The sequence: agent [3/10] failed, I re-ran the failed jobs on the same commit, it failed again identically, and I posted that this was "deterministic, not a flake". Two consecutive reproductions is weak evidence for determinism and I stated it as strong. It has since passed at dfdec049e and 8f7392eea with no related change, and all ten agent shards are green — so the honest description is an intermittent failure that happened to reproduce twice.

What survives from those notes is only the attribution: the same test failed on testnet-canary at 82ddb0358 with none of this PR present, and this branch predates that commit. That part was evidenced and still holds. The "deterministic" and "blocks the gate until the base is fixed" framing was mine and was wrong — CI is now 58 pass, 0 fail.

Worth flagging for whoever owns #2099/#2101: publish-finalized-agent-lane.test.ts > reads finalized assertions from the explicitly selected non-default agent lane is intermittently red on testnet-canary and reproduced twice in a row on 2026-08-06, which makes it more than a rare blip. Runs 31061793199 and 31080849334 have the failing output; the early-return in observeRfc64ConfirmedVmV1 I traced earlier is still the most likely mechanism.

Thanks for the three rounds — the differential harness caught two things I would have shipped: the metric pair I had made inconsistent, and the degradedQueueUtilization <= 0 hole in a proof I had stated unconditionally.

A short live confirmation on testnet is running now — boot, force sync-global to a full queue, and check the leading-indicator claim, the payload and log shape, and the two new gauges against a real daemon. Results will be posted here before merge.

@otReviewAgent otReviewAgent left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Operational Notice: Review Agent could not complete this review.

Business logic reviewer failed: WARNING: failed to clean up stale arg0 temp dirs: Permission denied (os error 13)

@Jurij89

Jurij89 commented Aug 6, 2026

Copy link
Copy Markdown
Contributor Author

Live testnet validation done, on the PR head 8f7392eea against Base Sepolia. PASS, with one metric inconclusive for a reason the plan predicted. No spend.

The load was real sync traffic — peer dials and on-connect catch-up against the public network. Only the admission ceilings were tuned down (DKG_SYNC_GLOBAL_MAX_INFLIGHT=1, DKG_SYNC_GLOBAL_QUEUE_LIMIT=4) so a single node could reach its own pool.

Guard first, because the plan says a run that never fills the pool is inconclusive rather than a pass: max(totals.queued / queueLimit) reached 1.00. Scheduler states across 1214 paired samples: healthy 310 / degraded 582 / saturated 322.

The claim the PR exists to establish

{"lane":"durable","state":"saturated","queued":4,"pressureQueued":4,"queueLimit":4,
 "oldestQueuedAgeMs":3277,"rejectedTotal":0,"lastRejectedAgeMs":null}

saturated at 4/4 with nothing rejected and 3.3 s of queue age. On pre-fix code that row is healthy — the sync scheduler own degraded-age threshold is 15 s and no rejection had occurred. 20 such samples. The 75% band likewise opened at 254 ms of age (15 samples), where pre-fix it needed 15 s.

Results

ID Claim Result
M1 saturated from depth, before any rejection or age threshold PASS (20)
M2 Spread shape — ≥2 lanes non-healthy, none alone full INCONCLUSIVE — only durable ever materialised
M3 75% band without 15 s of age PASS (15)
M4 Idle lane never dragged into a full pool PASS (0 violations)
M5 Payload self-describing and correct PASS (0 / 1214)
R1 /api/status agrees with diagnostics PASS (1213 / 1214)
R2 No store lane non-healthy under its own thresholds PASS (0)
Log sync-global no longer emits "lane":"all" PASS — 11 records, all lane-scoped, zero all

M5 in detail: sync-global and every one of its lanes reported capacityModel: shared with lane queueLimit always === totals.queueLimit; store and its lanes reported partitioned with pressureQueued === queued and pressureInflight === inflight, on every sample.

The one R1 disagreement, since a bare 1213/1214 would be hand-waving

At 11:50:04.537Z diagnostics said saturated, /api/status said degraded. The row shows queued: 3, pressureQueued: 3, queueLimit: 4 — the depth branch did not fire — and lastRejectedAgeMs: 59994, i.e. 6 ms inside the 60 s rejection window. The two surfaces are separate HTTP requests each computing its own snapshot; one landed inside the window, the other a few ms outside. Both individually correct, both neighbouring samples agree on both sides, and shared-pool classification is not involved. A property of the pre-existing rejection window and my probe design, not a divergence.

Honest gaps

  • M2 is unproven live. Only the durable lane was ever exercised, so a pool split across lanes never happened. The plan called this out in advance; it stays covered by the core spread test, which is the sole killer of the pool-denominator mutant.
  • The log fields were correctly absent, not missing: with one lane, pressureQueued === queued, so the omit-when-equal rule suppresses them. That confirms the rule; the differing case remains unit-tested.
  • Metric emission was not observable — no OTel provider is registered on this node, so the gauges bind to the API no-op meter. The names are pinned by unit test instead.
  • A first capture started ~5 min after boot showed 0 M1 hits. Not a failure — a late start: every sample by then carried a prior rejection and ~70 s of queue age. The cold-start rerun is what is reported here, and I mention it because the first numbers would have looked like a failed run.

Full report with environment, evidence paths and teardown: run-reports/2026-08-06-issue-2075-testnet-validation.md. Node and Oxigraph stopped, supervisor killed, ports released.

@Jurij89
Jurij89 merged commit 91335ef into testnet-canary Aug 6, 2026
61 checks passed
@Jurij89

Jurij89 commented Aug 6, 2026

Copy link
Copy Markdown
Contributor Author

Post-merge correction — I retracted a claim that was actually right, and should put that straight.

I first said the agent [3/10] failure was deterministic. When it later went green I retracted that in favour of flaky. The retraction was wrong.

#2108 (fix(rfc64): canonicalize stored seal timestamps) merged into testnet-canary at 09:49:35Z. Every red run of this PR predates it — d1c11aad7 at 07:13 and the re-run at 07:49 — and every green run postdates it. PR CI builds the merge ref, so from 09:49 onward my runs contained the fix. The test did not start passing on its own; it was repaired underneath the PR.

So: deterministic, as originally described, and now fixed at source. What I got wrong was the inference, not the observation — "it passed on a later head" is not evidence of flakiness when the base moved in between, and I did not check whether it had.

#2108 also confirms the mechanism I traced from the log: a store-canonicalized xsd:dateTime (…56.000Z…56Z) failed the strict RFC-64 wire validator, so the fail-open observer skipped both catalog advancement and the SWM inventory removal — the early return in observeRfc64ConfirmedVmV1. That is why rfc64CatalogCalls was empty.

Nothing here changes the merged code or the validation. Recording it so the thread does not leave a correct call retracted.

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.

2 participants