Review only the final Phase3 recovery delta from 024c414799661334cdc530a16dcf8ecaa1f43593 to 7661efa9ad06c5b740ad364e37a244d00356c59a in contractclaus/claus-network. Previous six independent reviews are already retained; do not repeat the entire repository audit.

Relevant paths: runtime/chain-worker.mjs passes the already configured Sepolia client and Mainnet client to createApplication, so internal signal consumer validation uses the same intended chains. src/approvals/limits.mjs permits an operator-only expiring per-project daily allocation up to the EXISTING project lifetime ceiling, never changing counters, global/lifetime limits, caller scopes or monetary limits.

Allocation records are private operator configuration with no public write endpoint.

For this release only one extra Sepolia reference attempt is planned: daily maximum 3, today only; two prior attempts have terminal verified zero-cost provider timeout outcomes and no approval. test/public-decision-admission.test.mjs covers concurrent admission and unchanged caps. docs/agent-review-transport.patch changes only the internal agent's HTTP transport/deadlines to its existing research timing; no model, prompt, quote ceiling, saved request, decision schema or signing behavior changes.

Two real model calls timed out at the prior 240s provider deadline; canonical relay proof marks both authorizations expired and unused, accounting zero.

New transport: provider600s, caller650s, reservation760s, missing-request fence800s. The actual model still makes the decision; there is no operator-forged approval, no automatic new model retry, and unknown requests remain held. Evaluate this precise delta for real privilege, accounting, recovery or chain confusion issues.

No contract, token, custody, public inference, fee or source-verification change. Report concrete actionable findings; distinguish trusted-operator policy from unprivileged caller control. Project/global lifetime and global daily caps must remain unchanged.

Published · Audit

4 findings

Four agents audited the code as it is at 7661efa, each in one area, and a judge reproduced, merged and ranked what they found, then read the code once more itself. Nothing in the code was changed or deployed.

Download the report (Markdown)

1 low3 info

  • 1.lowOperator allocation record format (key derivation, millisecond expiresAt, numeric day) is undocumented and any mismatch silently falls back to the default cap, so the planned single extra attempt can src/approvals/limits.mjs:14

        Number.isSafeInteger(allocation.expiresAt) && allocation.expiresAt > now && allocation.expiresAt <= (day + 1) * 86400000 &&

    Merged from four specialist reports (audit_economics, audit_permissions, audit_flow, audit_math), which all describe the same mechanism.

    The allocation gate itself is correct and bounded: the record is read at id digest({projectId, day}) and accepted only when projectId matches, day is the numeric UTC day index Math.floor(now/86400000), expiresAt is a millisecond epoch inside that day, and maximumProjectDay is an integer in [2,4]; lifetime caps, the global daily cap (8), counters and caller scopes are untouched (verified by the delta's tests and the full suite: 481 tests, 470 pass, 0 fail).

    No public endpoint writes 'public-decision-allocations': the only reference in src/, interfaces/, runtime/, adapters/ is the read in limits.mjs; portability import writes only its own fixed collections. This is therefore trusted-operator policy, not unprivileged-caller control.

    The defect is operational: the record must be hand-written into the document store (no script, admin route, CLI or validator exists), and the only documentation, docs/operations.md line 126, says 'keyed by the digest of {projectId, day}' and 'expiresAt must fall within that UTC day' without stating that the key is 'sha256:' + sha256 of canonical sorted compact JSON (src/shared/index.mjs digest/canonical), that day is an integer day index rather than a date, or that expiresAt is in milliseconds.

    Every other operator-facing expiresAt in this repository's public docs is Unix seconds (signal_grant_prepare in docs/openapi.json, docs/signals.md). Every mismatch is indistinguishable at runtime from 'no allocation': the third attempt is rejected with decision_capacity_reached and retryAt = next UTC day, with no event, log, health field or error detail saying an allocation row exists but was ignored.

    For this release the recovery plan is exactly one extra Sepolia reference attempt today; a mis-formatted record means that attempt does not happen and the operator learns only from the generic cap error. Fail-closed, so no privilege, accounting or chain impact.

    Fix: document the exact key derivation (canonical sorted compact JSON with the sha256: prefix, worked example), the integer day index and the millisecond unit in docs/operations.md, and preferably add an operator-only helper analogous to admitSignalPublisherBudget in adapters/signal-publisher.mjs that validates the shape with shape()/integer() and derives the key itself.

    Optionally expose whether an allocation row for the project/day exists but fails validation (worker-health field or error detail) so an ineffective record is visible before the attempt is spent. The guard itself should stay as it is.

    Run against this tree with an in-memory sqlite store (src/storage/index.mjs openStore(':memory:')), now = 1800000000000 (day index 20833), projectId 'reference'.

    Correct key is digest({projectId:'reference', day:20833}) = 'sha256:03f3ef0ec6ec881876dce00388a2b80698b6c9d60abc132ba1292a983d8de959'.

    For each variant: write the allocation record, call publicDecisionCapacity(tx,'reference',now,true) in three separate transactions.

    Observed output of the script: (1) canonical record {projectId:'reference',day:20833,expiresAt:1800057600000,maximumProjectDay:3} at the correct key -> third admitted.

    (2) same record with expiresAt:1800057600 (Unix seconds, end of day) -> third rejected: decision_capacity_reached retryAt 1800057600000.

    (3) day:'20833' (string) at digest({projectId,day:'20833'}) -> third rejected identically.

    (4) correct record at the id without the 'sha256:' prefix -> third rejected identically.

    (5) correct record at 'sha256:' + sha256(JSON.stringify({projectId,day})) (unsorted, non-canonical) -> third rejected identically.

    (6) expiresAt:1800057600001 (1 ms past the day end) -> third rejected identically.

    Expected per docs/operations.md ('daily maximum 3 today'): third attempt admitted, or a distinguishable indication that the allocation is ineffective.

    Actual: silent fallback to the two-attempt default with the same error and retryAt as having no allocation at all; the refused transaction consumes no admission count.

  • 2.infoNo test exercises the chain-worker application wiring this delta fixes; reverting it to chainPorts: { chain } leaves the suite greenruntime/chain-worker.mjs:47

      const app = createApplication({ store, origin: env.CLAUS_NETWORK_ORIGIN, rateSalt: env.CLAUS_NETWORK_RATE_SALT, chainPorts: { chain, client, mainnetClient } });

    The change is correct and introduces no chain confusion: createApplication (runtime/server.mjs) builds the signal router from chainPorts.client for Sepolia and chainPorts.mainnetClient for mainnet, createSignalRead rechecks client.getChainId() === chainId on every read (adapters/signal-read.mjs correct()), and createChainRead does the same (adapters/chain-read.mjs correctChain()).

    Before this commit the worker passed only { chain }, so signals.consumer -> chain.assertTrackedStore fell back to the hard-coded public Sepolia RPC instead of the operator's verified archive RPC and a mainnet-bound project had no mainnet signal reader; 'signal_chain_unavailable' is not in runDecision's blocked list (src/approvals/index.mjs lines 177-179), so such a decision retried every 60 s indefinitely.

    None of this is asserted by a test. test/chain-recovery-guards.test.mjs only calls createChainWorker with invalid environments and expects 'historical_rpc_required' (it throws before line 47); test/chain-integration.test.mjs constructs createApplication with chainPorts: { chain } only, the same shape that was wrong in the worker.

    Fix: factor the chainPorts construction out of createChainWorker so a test can build it from fake clients reporting chain ids 11155111 and 1 and assert that signals.consumer for a mainnet-bound project reads through the mainnet client, or have createApplication reject a chainPorts.chain router supplied without matching client/mainnetClient.

    Extracted 7661efa into a scratch directory (git archive, outside the repository), changed line 47 back to the 024c414 form chainPorts: { chain }, linked node_modules and ran node --test test/*.test.mjs.

    Result: tests 481, pass 470, fail 0, identical to the unmodified tree.

    Expected: at least one test fails when the worker's signal router is not bound to the configured clients.

    Actual: the regression is invisible to the suite.

  • 3.infoWorker mainnet client omits cacheTime: 0, unlike the Sepolia client and readonly-ports, so head reads it now also serves to the signal reader are cached for up to 4 sruntime/chain-worker.mjs:41

      const mainnetClient = configuredMainnet ? createPublicClient({ transport: http(env.CLAUS_MAINNET_RPC_URL, { retryCount: 0, timeout: 15000 }) }) : null;

    Pre-existing line (commit 48c611ad, an ancestor of 024c414), but this delta newly hands the same mainnetClient to createApplication for mainnet signal reads. The Sepolia client on line 32 and runtime/readonly-ports.mjs line 9 both pass cacheTime: 0; this one does not, so viem uses its default cacheTime = pollingInterval = 4000 ms (node_modules/viem/_esm/clients/createClient.js line 12) and getBlockNumber results are reused within that window.

    The same client object is used by the chain-1 executor (adapters/chain-executor.mjs lines 114 and 262 compute finality as head - receipt.blockNumber >= 2n from getBlockNumber) and by chain-read's observedBlock (adapters/chain-read.mjs line 99).

    Effect is conservative: a head up to 4 s stale can only delay a finality decision, never advance it, and chain identity is still verified on every read. Consistency item only; no privilege, accounting or chain-confusion impact.

    Fix: add cacheTime: 0 to this createPublicClient call to match the other two.

    Script using the vendored viem (2.57.1) with a custom transport that counts eth_blockNumber requests: createPublicClient({transport}) then two getBlockNumber() calls -> 1 RPC request (second served from cache); createPublicClient({transport, cacheTime: 0}) then two getBlockNumber() calls -> 2 RPC requests.

    Expected: the worker's mainnet client behaves like its Sepolia client and readonly-ports (uncached).

    Actual: mainnet head reads are cached for the 4000 ms default.

  • 4.infoAgent transport patch cannot be verified from this tree: the new inferenceFetch module is absent, the only test mocks it out, and the deadline ordering rests on payments-side constants not present herdocs/agent-review-transport.patch:6

    +import {inferenceFetch} from './inference-transport.js';

    Merged from three specialist reports (audit_economics, audit_permissions, audit_flow). Unverified lead rather than a confirmed defect, because the agent and payments sources are not in this repository.

    What is checkable here: the patch keeps Idempotency-Key, X-Max-Quote-Micro-USDC 2000000, the saved request body and the decision/finish path unchanged, adds the X-Inference-Purpose: research header, raises the caller abort from 270000 to 650000 ms and the not_found fence from 300000 to 800000 ms, and swaps the default fetch for inferenceFetch from ./inference-transport.js.

    That module appears nowhere in this tree (grep for 'inference-transport' matches only this patch line), and the patch's test injects a mock fetcher, so inferenceFetch is never executed by any test shown.

    The assignment's claims 'no automatic new model retry' and 'unknown requests remain held' therefore depend on an unreviewed transport: if it retried on network error or 5xx, a second POST /api/completion with the same Idempotency-Key would reach the relay and idempotency would rest solely on relay-side behaviour not shown here.

    Three further timing dependencies are outside this tree: (a) the 650 s wait must run under an agent function budget of at least that (this repository's vercel.json limits api/gateway.mjs to 60 s and api/compile.mjs to 120 s; the agent's own maxDuration is not inherited from here), otherwise the catch that moves the row to 'uncertain' never runs; (b) the 800 s fence is measured from row.started_at (set at claim) while the 760 s reservation starts at relay admission, leaving a 40 s margin, and it is sound only if the relay never answers 404 not_found for a request it admitted; (c) the ordering provider 600 < caller 650 < reservation 760 < fence 800 is asserted by the patch's test against completionTiming(false,'research') in payments/vendor/src/response.js, which is not here.

    On the claus-network side the change is safe regardless: adapters/claus-controller.mjs polls the agent with 25 s calls, re-POSTs /reviews only on 404 or an elapsed queued retryAt, treats failed as terminal, and admission is consumed once per proposal at creation, so a longer agent window cannot consume extra admission from this repository.

    Fix: include agent/src/inference-transport.ts in the reviewed delta (or quote its retry policy), add one agent test that drives runNetworkReview through the real inferenceFetch against a failing local server and asserts exactly one POST, record the agent function's maxDuration beside the deadlines, and confirm in the payments vendor that /api/requests/{id} returns a terminal status rather than 404 for every admitted id.

    At 7661efa, grep -rn inference-transport . --exclude-dir=node_modules --exclude-dir=.git returns only docs/agent-review-transport.patch:6, and grep -rn completionTiming returns only the patch's test lines 47 and 59; no X-Inference-Purpose handling exists in this repository.

    Applying the patch to an agent tree without agent/src/inference-transport.ts fails at import resolution; applying it to a tree that has it changes the transport to code no reviewer of this delta has seen.

    Expected: a delta claiming 'only transport/deadlines changed, no automatic retry' contains or references the transport implementation and a test exercising it.

    Actual: the module is missing and the only test mocks it out.

    Not reproducible as a runtime defect from this tree.

Work

  1. Posted7 minto the first attempt
  2. Audit flowAgent #1530found 1 low, 1 info

    The review is complete. Findings are in .imd-findings.json and the working tree is otherwise untouched.

    Outcome: the Phase3 recovery delta (024c414 to 7661efa) introduces no privilege, accounting, recovery or chain-confusion defect that I could substantiate. One low finding and one informational note were recorded.

    What was verified

    • Operator allocation in src/approvals/limits.mjs. The record is read only, keyed by project and UTC day, revalidated against project id, day, expiry inside that day, and a ceiling between the default daily cap and the unchanged lifetime cap. Lifetime and global checks run first in the same transaction, counters are never reset, and no HTTP route writes any arbitrary collection. Both storage backends use a generic document table, so a production read of the new collection cannot throw. The new tests plus the full suite in a dependency-installed scratch copy pass (470 passed, 11 skipped, 0 failed).
    • Chain worker wiring. Passing the configured Sepolia and Mainnet clients to createApplication only replaces the public-node fallbacks. Every reader fed by them (signal, wallet, vault, chain) asserts getChainId against its configured chain, so a misrouted client fails with wrong_chain rather than reading the wrong network.
    • Agent transport patch. The controller in this repo polls the agent with short status calls, resubmits only on a 404 or an elapsed queued retry and is deduplicated by input digest, and treats a failed review as terminal. Admission is consumed once per proposal at creation, so the longer agent delivery window cannot create a second paid model call or extra admission from this side.

    Findings

    • Low, src/approvals/limits.mjs:14. The allocation's expiresAt must be a millisecond epoch, but the only documentation never states a unit while the public API defines the same field name in Unix seconds. A seconds value silently fails validation and the operator's planned extra attempt is refused with a generic cap error indistinguishable from having no allocation. Fail-closed, so no cap or counter is affected. Reproduced with a scratch script.
    • Info, docs/agent-review-transport.patch:37. The ordering provider 600s, caller 650s, reservation 760s, fence 800s is internally consistent, but the payments-side timing constants are not in this tree and could only be checked against the brief's stated numbers.

    Not covered: the agent and payments repositories referenced by the patch, and the Postgres-backed tests that the suite skipped without a database.

    ran onclaude · claude-fable-5-1 · 20 turns · 6m 23s · 610 in · 21.9K out · 1.7M cached
    submission830bd2098239e4ff1e078f239b93996d6abdb35329b9f6fc711db5e650453454
    deviceb273d407784470b47d335f4d3171227a0ffa0b170a60519e141a13a80ecc83bb
    started from7661efa9ad06c5b740ad364e37a244d00356c59a
    bundlenone
    • lowAllocation expiresAt unit is undocumented and a seconds value silently disables the operator allocationsrc/approvals/limits.mjs:14

      The new public-decision-allocations record is operator configuration written directly into the document store, with no schema check at write time. publicDecisionCapacity compares allocation.expiresAt against now in milliseconds and against (day + 1) * 86400000, so expiresAt must be a millisecond epoch. docs/operations.md:126 (the only description of the record) says only that expiresAt must fall within that UTC day and never states the unit, while the rest of the operator-facing documentation and the public API (signal_grant_prepare in docs/openapi.json and docs/signals.md:16) define expiresAt as Unix seconds.

      An operator who follows that convention produces a record that fails the allocation.expiresAt > now test, so allocated is false and the default two-attempt daily cap stays in force. Nothing records that an allocation existed and was rejected: no store event, no field on the decision_capacity_reached error, and the retryAt the caller receives is the next UTC day, exactly what it would see with no allocation at all.

      For the planned Phase3 recovery (one extra Sepolia attempt today, maximum 3) this means the operator's single intended extra attempt is refused and the only diagnostic is a generic cap error. This is fail-closed, so it is not a privilege or accounting defect: no counter, lifetime cap, global cap or caller scope is affected.

      Fix: state in docs/operations.md that expiresAt is a millisecond epoch (same convention as identity grant expiresAt), or accept both forms by normalising a value below 1e11 as seconds, and surface a distinguishable reason (for example an allocation_invalid event or error detail) when a record exists for the key but fails validation.

      State: in-memory store, now = 1800000000000 (day 20833, project 'reference').

      Write public-decision-allocations[digest({projectId:'reference', day:20833})] = { projectId:'reference', day:20833, expiresAt: 1800057600, maximumProjectDay: 3 } (expiresAt as Unix seconds for the end of that UTC day).

      Run publicDecisionCapacity(tx,'reference',now,true) three times.

      Expected per the operator's intent and docs/operations.md: third call admitted (daily cap 3).

      Actual: third call rejects with code decision_capacity_reached and retryAt 1800057600000, identical to the no-allocation result.

      Observed with a scratch script importing src/storage/index.mjs and src/approvals/limits.mjs: output third rejected: decision_capacity_reached retryAt 1800057600000.

      With expiresAt: 1800057600000 (milliseconds) the third call is admitted.

    • infoAgent transport patch timing fences depend on payments-side constants that are not in this treedocs/agent-review-transport.patch:37

      The patch is internally consistent: the caller abort (650000 ms) exceeds the stated provider deadline (600 s), and the missing-request fence (800000 ms from started_at) exceeds the stated reservation window (760 s), so a request row can only be fenced as inference_not_reserved after any reservation for it must already have expired.

      The patch keeps Idempotency-Key, X-Max-Quote-Micro-USDC 2000000 and the saved request unchanged, and its test asserts the abort sits between completionTiming(false,'research').providerMs and .activeMs. However, inference-transport.ts, completionTiming and the 'research' purpose timings (provider 600 s, reservation 760 s) live in the agent and payments repositories, neither of which is in this tree; grep of this repository finds no X-Inference-Purpose handling.

      The ordering claim provider 600 < caller 650 < reservation 760 < fence 800 was therefore verified only against the brief's stated numbers, not against the code that enforces them.

      On the claus-network side the change is safe regardless: adapters/claus-controller.mjs polls the agent with 25 s status calls, re-POSTs /reviews only on 404 or an elapsed queued retryAt (deduplicated by the agent on the input digest), treats failed as terminal with no automatic resubmission, and admission is consumed once per proposal at creation, so a longer agent delivery window cannot consume extra admission or spawn a second paid model call from this repository.

      No action is required here; the requester should confirm the research timing constants in the payments vendor before relying on the 800 s fence.

      Failing state that would break the fence, not reproducible from this tree: if completionTiming(false,'research') in payments/vendor/src/response.js returned a reservation (activeMs) shorter than 650000 ms, the patch's own test assertion timeouts.at(-1) < timing.activeMs would fail and an admitted request could have its reservation released while the agent is still waiting on the 650 s call. With the brief's stated values (provider 600 s, reservation 760 s) the assertion holds and the 800 s fence is strictly after reservation expiry.

  3. Audit mathAgent #179found 1 info

    Review of the Phase 3 recovery delta (024c414 to 7661efa) is complete. The findings file holds one info-level note and no defects.

    Outcome: no privilege, accounting, recovery or chain-confusion defect found in the delta. Checks I ran and what they showed:

    • Allocation guard in src/approvals/limits.mjs. The record must match the project and UTC day, expire inside that day, and carry an integer cap in the range 2 to 4. Lifetime project, lifetime global and global daily caps are checked first and unchanged. Counters are never reset. A refused attempt rolls back without consuming a count. A probe of nine edge cases (seconds vs milliseconds, string day, unprefixed key, expiry at exact day end, cap equal to lifetime) all behaved as the documentation states, with every malformed variant falling back to the default two-attempt cap.
    • No unprivileged write path. No HTTP route, the transfer import inventory, or the one dynamic-collection write in identity can reach the allocations collection. Both stores accept the new collection name without schema changes, and the Postgres pruner only touches auth challenges and rate limits.
    • Chain worker change. Passing the configured clients means the signal router now reaches Mainnet when configured instead of throwing a chain-unavailable error that left the review job retrying every minute. Each signal read verifies the client's chain id before use, so a mis-pointed RPC fails closed as a wrong-chain error.
    • Docs arithmetic. The signed gas 1,222,706, the estimate ceiling 1,592,000 and the fee ceiling 817,858,095 wei all reproduce exactly from the signer formula in adapters/sepolia-transactions.mjs. The grant expiry window in the OpenAPI text matches the code. The regenerated docs are byte-identical to the committed ones.
    • Tests. The repository tree has no installed dependencies, so I installed them in a copy under /tmp. All 45 tests across the nine files covering the touched areas pass there, including the new concurrent-admission test.

    What I could not verify. The agent transport patch targets a repository not in this tree, and the relay's timing table is not available. Its internal logic is consistent: the 650 second caller timeout sits between the 600 second provider deadline and the 760 second reservation, and the 800 second fence only acts on an explicit not-found response, so it cannot expire a live reservation.

    The one recorded note is operational, not a vulnerability: the planned extra attempt depends on an operator hand-writing the allocation record with exactly the right key and millisecond expiry, and any slip is indistinguishable at runtime from having no allocation. A small validating helper, or an exact key example in the operations doc, would remove that risk.

    ran onclaude · claude-fable-5-1 · 56 turns · 7m 44s · 482 in · 32.9K out · 1.6M cached
    submission123d0557ef6cdb1185d5ec61801d66ced2c1840c33e93ccfef03489534c56f32
    device9812c5faf226f7c724ec63b46895bd6ebfde6f3565b0c2897ffbecbb6daed04f
    started from7661efa9ad06c5b740ad364e37a244d00356c59a
    bundlenone
    • infoHand-written allocation record has no validation path; a seconds-based expiresAt or string day silently keeps the default capsrc/approvals/limits.mjs:14

      Not a privilege, accounting or chain defect: every malformed variant falls back to the unchanged two-attempt project daily cap, so unprivileged callers gain nothing and no counter is consumed on refusal. It is an operational gap for the planned single extra Sepolia attempt.

      The allocation is operator configuration written straight into the store (no script, no admit function, no test of the key derivation), and the guard accepts only a record whose id is digest({projectId, day}) with day as a number, whose expiresAt is in milliseconds within the UTC day, and whose maximumProjectDay is an integer in [2, 4].

      Any slip (expiresAt in Unix seconds as used elsewhere in the API such as signal grant expiresAt, day stored as a string, id without the sha256: prefix) is indistinguishable at runtime from 'no allocation'. Verified by running publicDecisionCapacity against an in-memory store: canonical record admits the third attempt; the same record with expiresAt in seconds, day as a string, or an unprefixed id returns decision_capacity_reached.

      Fix: add a small operator helper (like admitSignalPublisherBudget in adapters/signal-publisher.mjs) that validates and writes the record with shape()/integer() checks and derives the key itself, or at minimum document the exact canonical key and millisecond unit in docs/operations.md. The guard itself should stay as is.

      now = 1800000000000 (day 20833).

      Operator intends a 3-attempt cap for project 'reference' today.

      Write to collection public-decision-allocations: id = digest({projectId:'reference', day:20833}) = sha256 of canonical {"day":20833,"projectId":"reference"}; value {projectId:'reference', day:20833, expiresAt:1800057600000, maximumProjectDay:3}.

      Two reserved attempts then a third: expected 'admitted', actual 'admitted'.

      Now write the same record with expiresAt:1800057600 (seconds) or day:'20833' (string) or id without the 'sha256:' prefix: expected per docs/operations.md 'daily maximum 3 today', actual third attempt rejects with decision_capacity_reached and retryAt = 1800057600000, identical to having no allocation.

      The refusal consumes no admission count (transaction rolls back).

  4. Audit permissionsAgent #1871found 1 low, 2 info

    The review is complete and the findings file is written and validated against the tree.

    Scope reviewed. The delta from 024c414 to 7661efa: the chain-worker port wiring, the operator allocation in src/approvals/limits.mjs, the admission tests, the agent transport patch, and the docs changes. I ran the full Node suite with dependencies installed (470 pass, 11 skipped, 0 fail) and checked the documented gas and fee arithmetic, which is correct.

    Conclusion. No privilege, accounting, or chain-confusion defect was found in the delta. The allocation is operator-only with no public write path, is clamped to the unchanged project lifetime ceiling, and leaves counters, global daily, and lifetime caps intact. The worker now binds signal reads to the configured Sepolia and mainnet clients, and every signal read re-verifies the chain id, so there is no chain confusion.

    Findings recorded in .imd-findings.json:

    • Low, allocation record format. The record must be hand-written to the store with no tooling and no documented units. A record written in Unix seconds, with a date-string day, or expiring one millisecond past the day boundary is silently treated as absent, and the planned single extra attempt is rejected with no diagnostic. Reproduced with a script against the sqlite store.
    • Info, transport patch not self-contained. The patch imports an inference-transport.js module that is neither in the patch nor in this repository, and the only test mocks it out. The no-retry claim therefore rests on unreviewed code. The description also flags the agent function's hosting duration limit and the 40-second margin between the reservation and the missing-request fence as items to confirm in the agent repository.
    • Info, test coverage. No test exercises the chain-worker wiring this commit fixes. Reverting the line to the previous form still passes the whole suite.

    Nothing in the repository was modified. Dependencies were installed into the gitignored node_modules only.

    ran onclaude · claude-fable-5-1 · 42 turns · 7m 51s · 450 in · 29.7K out · 1.2M cached
    submissione2551afdc9e8f9a3fd3995952e261835faf1e43d9c2045e9ccf818eed60e2b0f
    device2a9662a76cb5f51d178c6d5ff9e9a5da33ad63feb5a9ef85547ee127dbf9fd6f
    started from7661efa9ad06c5b740ad364e37a244d00356c59a
    bundlenone
    • lowOperator allocation record format is undocumented and fails closed silently, so the planned extra attempt can be lost without any signalsrc/approvals/limits.mjs:14

      The allocation gate is correct and bounded (operator-only store write, maximumProjectDay clamped to [2,4], day-keyed, expiry inside the UTC day, counters/lifetime/global caps untouched; verified by the delta's tests and the full suite: 470 pass, 0 fail).

      The defect is operational: the record must be hand-written into the document store (there is no CLI, admin route or validator), and neither docs/operations.md line 126 nor the code states that expiresAt is Unix milliseconds, that day is Math.floor(nowMs / 86400000) (an integer day index, not a date), or that the key is digest({projectId, day}) over the canonical JSON form.

      Every other time field an operator touches in this repo's public docs is in Unix seconds (e.g. signal grants: openapi.json 'expiresAt is Unix seconds'). A record written in the seconds convention, with a date-string day, or with expiry one millisecond past the day boundary is silently treated as absent: the third attempt is rejected with decision_capacity_reached / retryAt = next day, no log, no health field, and nothing reports why.

      For this release the operator plans exactly one extra Sepolia reference attempt today; a mis-formatted record means the attempt does not happen and the operator only learns from the rejection.

      This is a trusted-operator policy surface, not an unprivileged-caller control: no public endpoint writes 'public-decision-allocations' (grep over src/, interfaces/, api/, runtime/ finds only the read in limits.mjs and the delta's tests; the portability import only writes 'projects', 'artifacts', 'snapshots').

      Fix: document the units and key derivation in docs/operations.md (expiresAt ms, day index, canonical digest), and add a small operator-facing check, e.g. an allocation field in worker-health or a script that reads the record back through publicDecisionCapacity's predicate and prints whether it is effective, so an ineffective record is visible before the attempt is spent.

      Script run against this tree (sqlite ':memory:' store, now = 1800000000000 ms, day index 20833, projectId 'reference'):

      1. write {projectId:'reference', day:20833, expiresAt:1800057599 (Unix SECONDS end of day), maximumProjectDay:3} at key digest({projectId,day}) -> attempts 1,2 admitted, attempt 3 rejected: decision_capacity_reached, retryAt 1800057600000.
      2. write the same record with day:'2027-01-15' (date string) at digest({projectId, day:'2027-01-15'}) -> attempt 3 rejected identically.
      3. write expiresAt = (day+1)*86400000 + 1 (one ms past the UTC day end) -> attempt 3 rejected identically. Expected: either the third attempt is admitted (record intended as valid) or the operator receives an explicit indication that the allocation is ineffective. Actual: silent fallback to the default two-attempt cap with no diagnostic anywhere. Correct record {projectId:'reference', day:20833, expiresAt:1800057600000, maximumProjectDay:3} admits 3 and rejects the 4th (test/public-decision-admission.test.mjs line 44 covers this path).
    • infoAgent transport patch is not self-contained: the new fetcher module is absent, so the 'no automatic retry' and timing claims cannot be verified from the deltadocs/agent-review-transport.patch:6

      The patch swaps the default fetch for inferenceFetch from ./inference-transport.js, but that file is neither in the patch nor anywhere in this repository (grep 'inference-transport' matches only this line). The agent test added by the patch injects a mock fetcher, so inferenceFetch is never executed by any test shown.

      The assignment's invariants (one send per Idempotency-Key, no automatic model retry, unknown requests held) therefore rest on an unreviewed transport: if inferenceFetch retries on network error or on 5xx, a second POST /api/completion with the same Idempotency-Key reaches the relay after the first was lost, and whether that is a no-op depends solely on relay-side idempotency that this delta does not show.

      Three further points to check in the agent repository before relying on the stated deadlines: (a) the previous caller deadline of 270 s sat just under a 300 s serverless function limit; the new 650 s only works if the agent function's maxDuration is at least ~700 s (this repo's own vercel.json caps api/gateway.mjs at 60 s and api/compile.mjs at 120 s, so the agent's limit is not inherited from here).

      If the host kills the invocation at 300 s the catch that moves the row to 'uncertain' never runs and recovery depends entirely on the status path reconciling rows still in 'running'. (b) The 800 s missing-request fence is measured from row.started_at (set at claim), while the 760 s reservation starts at relay admission; the margin is only 40 s of admission latency, after which a 404 can be fenced as 'inference_not_reserved' while the relay reservation is still live.

      (c) The ordering provider 600 < caller 650 < reservation 760 < fence 800 is internally consistent as stated and the agent test asserts caller between providerMs and activeMs.

      Fix: include agent/src/inference-transport.ts in the reviewed patch (or state that it already exists and quote its retry policy), add one test that drives runNetworkReview through the real inferenceFetch against a failing local server and asserts exactly one POST, and record the agent function's maxDuration alongside the deadlines.

      State: this repository at 7661efa. grep -rn inference-transport . (excluding node_modules) returns only docs/agent-review-transport.patch:6.

      Applying the patch to an agent tree that lacks agent/src/inference-transport.ts fails at import resolution; applying it to a tree that has it changes the transport to code that no reviewer in this delta has seen.

      Expected: the delta that claims 'only transport/deadlines changed, no automatic retry' contains or references the transport implementation and a test exercising it.

      Actual: the transport module is missing and the only test mocks it out.

    • infoNo test covers the chain-worker wiring that this delta fixes; the regression (worker signal reads on an unconfigured chain) would pass the suite againruntime/chain-worker.mjs:47

      The change itself is correct and is the right chain for each reader: createApplication (runtime/server.mjs lines 71-74) builds the signal router from chainPorts.client for Sepolia and chainPorts.mainnetClient for mainnet, every createSignalRead call re-checks client.getChainId() === chainId (adapters/signal-read.mjs line 39), and the router refuses any other chainId, so there is no chain confusion.

      Before this commit the worker passed only { chain }, so signals.consumer -> chain.assertTrackedStore used a hard-coded public Sepolia RPC instead of the operator's verified archive RPC, and a mainnet-bound project hit 'signal_chain_unavailable' which is not in runDecision's blocked list and therefore retried every 60 s forever (approvals/index.mjs lines 177-181).

      None of that is asserted by a test: test/chain-recovery-guards.test.mjs only calls createChainWorker with invalid environments and expects 'historical_rpc_required'; test/chain-integration.test.mjs constructs createApplication with { chain } only, i.e. the same shape that was wrong in the worker.

      Fix: factor the chainPorts construction out of createChainWorker (or give createApplication a strict mode that rejects a chain router without matching client/mainnetClient) and add a test that builds the worker ports from fake clients reporting chain ids 11155111 and 1 and asserts signals.consumer for a mainnet-bound project calls the mainnet client, not the Sepolia one.

      State: revert line 47 to chainPorts: { chain } (the 024c414 form).

      Run node --test test/*.test.mjs: the suite still reports 470 pass / 0 fail, because no test instantiates the worker's application with a signal-bearing project.

      Expected: a test fails when the worker's signal router is not bound to the configured clients.

      Actual: the regression is invisible to the suite.

  5. Audit economicsAgent #586found 1 low, 2 info

    The review is complete and the findings file is written and validated. Nothing further is needed from the tree.

    Outcome: no privilege, accounting, recovery or chain-confusion defect was substantiated in the Phase3 delta. The findings file holds one low and two info items.

    What I reviewed (HEAD state of the changed files, since the base-commit blobs are not fetchable offline):

    • src/approvals/limits.mjs allocation logic. The allocation can only raise the project daily cap, is bound to one project and one UTC day, must expire within that day, and is capped at the unchanged project lifetime of 4. Lifetime, global daily and global lifetime rows are still checked and reserved in the same transaction. Invalid rows fall back to the default cap. No route in the API contract writes the collection, and the portability import only accepts collections in its own export coverage, so it is operator-only.
    • runtime/chain-worker.mjs now passes the configured Sepolia and mainnet clients into the application, so signal consumer validation in the decision and execution paths reads the archive RPCs instead of the public Sepolia fallback. The signal and readonly routers select by the project binding's chain id and verify the RPC chain id, and a missing mainnet client fails closed as a retryable wait.
    • The agent transport patch changes only the completion deadline, the purpose header and the 404 fence. Caller 650 s sits between the stated provider 600 s and reservation 760 s, and the fence at 800 s exceeds the caller deadline. Proposal expiry is at least 24 hours, so the longer decision window cannot trip the controller's expiry check.
    • Tests: the admission test file passes 4 of 4, and the controller, approval consumer, chain routing, recovery guard, executor capability, mainnet release, signal and chain integration files pass 25 with 3 environment skips and no failures. The boundary check passes and the packaged OpenAPI matches the generated output.

    Findings written to .imd-findings.json:

    • Low, operations doc. The allocation key is the canonical sorted compact digest with a sha256: prefix and millisecond expiresAt, but the doc only says "digest of {projectId, day}" and there is no script or route. A hand-computed key is silently ignored and the planned third attempt gets decision_capacity_reached. Fail-closed, trusted-operator only.
    • Info, chain worker. The mainnet client lacks cacheTime: 0, unlike the Sepolia client and the gateway ports. Only delays finality reads by up to 4 s.
    • Info, agent patch. Three assumptions live outside this tree and are not covered by the patch's tests: the new transport honoring the abort signal, the host function budget covering a 650 s wait, and the relay never returning 404 for an admitted request after the 760 s reservation.

    Not done: the base commit's blobs could not be fetched, so the exact line-level diff was inferred from commit messages and HEAD content. Dependencies were installed in a temporary directory and the link was removed afterwards. No files in the tree were changed.

    ran onclaude · claude-fable-5-1 · 50 turns · 9m 41s · 930 in · 36.5K out · 2.8M cached
    submissionf22f46c53ca746750d7f76147525cda5b26028a5a3d57e7265114ff1584b9ee5
    device4ebee02fc506f27c1b0fadc62728da6e6bb08e89eb86b6d3f22e7fbf1df539ab
    started from7661efa9ad06c5b740ad364e37a244d00356c59a
    bundlenone
    • lowAllocation key derivation is undocumented; a hand-computed key silently falls back to the default capdocs/operations.md:126

      The planned recovery (one extra Sepolia reference attempt today, maximumProjectDay 3) is executed by the operator writing a row directly into the documents table, because there is no API route, CLI command or script for public-decision-allocations. src/approvals/limits.mjs:12 reads the row at id digest({ projectId, day }), which is 'sha256:' + sha256(canonical(value)) where canonical() sorts object keys and emits compact JSON (src/shared/index.mjs:45-61).

      The operations doc only says 'the digest of {projectId, day}'; it does not state the canonical form, the sha256: prefix, that day is Math.floor(nowMs / 86400000), or that expiresAt is in milliseconds. Every mismatch (unsorted keys, whitespace, missing prefix, seconds instead of milliseconds) is treated by limits.mjs as 'no allocation' and the request fails with the unchanged decision_capacity_reached, with no diagnostic that an allocation row exists but was ignored.

      This is fail-closed (no privilege or accounting effect: caps never exceed the stated bounds), but it is the single mechanism this release relies on for the recovery attempt, and the first indication of a wrong key is the recovery not happening. Trusted-operator policy only; no unprivileged caller can write the collection (no route in interfaces/contract.mjs, portability import coverage excludes it).

      State: project 'reference' is in public-chain-projects with today's day counter at 2 (the two terminal timeout attempts).

      Operator computes the id as hex sha256 of JSON.stringify({projectId:'reference',day:20739}) = 'ee0e8a31e10cf6b033c8ee1131303a54cb15f068b4d320bb2ac41816572abe0c' (or the same without the 'sha256:' prefix, or with expiresAt in seconds) and inserts {projectId:'reference',day:20739,expiresAt:(20740)*86400000,maximumProjectDay:3}.

      Expected: third approvals.create for 'reference' today is admitted.

      Actual: limits.mjs reads id 'sha256:35fff7ef9175f5bcac7ec2e70865d2b62d156cab241654c6e747c77a164ba812' (digest of '{"day":20739,"projectId":"reference"}'), finds nothing, keeps projectDay=2 and throws decision_capacity_reached with retryAt = next UTC day.

      Verified with: node -e "import('./src/shared/index.mjs').then(m=>console.log(m.digest({projectId:'reference',day:20739})))".

      Fix: document the exact key (canonical sorted compact JSON, sha256: prefix, day = floor(ms/86400000), expiresAt in ms) with a worked example, or add an operator-only script/CLI that writes the row via digest(), and optionally log when an allocation row for the project/day exists but fails validation.

    • infoChain-worker mainnet client omits cacheTime: 0 unlike the Sepolia client and the gateway's readonly-portsruntime/chain-worker.mjs:41

      This delta hands the worker's client and mainnetClient to createApplication so that signal consumer validation uses the configured chains. The Sepolia client on line 32 and the mainnet client built in runtime/readonly-ports.mjs:9 both pass cacheTime: 0; the worker's mainnet client does not, so viem caches getBlockNumber for its default cacheTime (pollingInterval, 4000 ms).

      The same client object is used by the chain-1 executor port (adapters/chain-executor.mjs:114 and :262 compute finality as head - receipt.blockNumber >= 2 from getBlockNumber). Effect is conservative (a head up to 4 s stale can only delay, never advance, a finality decision) and no chain confusion results because createSignalRead/createChainRead verify getChainId() against the pinned chain id. Reported as a consistency item only.

      State: controller/executor worker with CLAUS_MAINNET_RPC_URL set.

      Two getBlockNumber calls within 4 s on mainnetClient return the first cached value (viem withCache keyed by client uid), while the same two calls on client (cacheTime: 0) hit the RPC each time.

      Expected: both workers' clients behave like readonly-ports (uncached).

      Actual: mainnet worker head reads are cached for up to 4 s.

      Fix: add cacheTime: 0 to the mainnet createPublicClient call.

    • infoAgent transport patch relies on three timing guarantees that are outside this tree and untested by the patchdocs/agent-review-transport.patch:28

      Unverified lead, not a confirmed defect: the agent and relay sources are not in this repository, so the following cannot be checked here. (1) The 650 s caller deadline only works if inferenceFetch (new import, agent/src/inference-transport.js) propagates the AbortSignal and has no shorter internal timeout; the patch's test mocks fetcher, so inferenceFetch itself is never exercised.

      (2) runNetworkReview is driven from a hosted HTTP handler that the controller calls with a 25 s AbortSignal (adapters/claus-controller.mjs:22); the 650 s wait must run under a function maxDuration/background budget of at least that, otherwise the wait is killed and the row remains 'uncertain' until the 800 s fence.

      (3) The 800 s not_found fence is sound only if the relay never answers 404 not_found for a request it did admit (the 760 s reservation must not purge the request row); otherwise a reserved-and-settled request could be marked inference_not_reserved, contradicting 'unknown requests remain held'. The patch adds a test for the fence timing but none for an admitted request that reports 404 after 800 s.

      None of these changes caps, prompts or accounting; the planned retry is still bounded by the unchanged 2 000 000 micro-USDC ceiling and the one-attempt allocation in limits.mjs.

      State to check before shipping: review row claimed at T0, relay admits at T0+30 s and reserves until T0+790 s, provider answers at T0+700 s (after provider600 s cut) with a settled failure.

      If the relay's request row disappears at reservation end, networkReviewStatus at T0+801 s receives 404 not_found and writes state='failed', result={error:'inference_not_reserved'} even though the request was reserved.

      Expected per the stated design: the row stays 'uncertain' (held) until the relay returns a terminal status.

      Confirm in payments/vendor that /api/requests/{id} returns a terminal status, not 404, for every admitted id indefinitely, and add that case to agent/tests/network-review.test.ts.

  6. Audit judgeAgent #1309found 1 low, 3 info

    Review complete. The findings file holds four judged findings, all snippet-verified against the tree, and the working tree is otherwise unchanged.

    Outcome. The Phase3 delta introduces no privilege, accounting, recovery or chain-confusion defect. The allocation gate in src/approvals/limits.mjs is bounded exactly as described: the extra daily attempts are capped at the unchanged project lifetime ceiling, counters are never reset, global and lifetime caps still bind, and no public endpoint writes the allocation collection. The chain-worker change binds the signal readers to the configured clients, and both reader adapters re-check the chain id on every read. Full suite: 481 tests, 470 pass, 0 fail.

    Findings kept, after reproduction and merging the nine specialist reports:

    • Low, allocation record format undocumented and fail-silent. Four specialists reported the same mechanism, merged into one. I reproduced six variants against an in-memory store: the canonical millisecond record admits the third attempt, while seconds-based expiry, string day, unprefixed id, non-canonical JSON key, or expiry one millisecond past the day end all fall back to the two-attempt cap with the identical error and retryAt. Trusted-operator policy only, but it is the sole mechanism this release relies on for the one planned extra Sepolia attempt.
    • Info, chain-worker wiring has no test. Reverting line 47 to the old form in a scratch copy outside the repository leaves the suite at 470 pass, 0 fail.
    • Info, worker mainnet client lacks cacheTime 0. Pre-existing line, but the delta newly routes signal reads through it. Reproduced with the vendored viem: two head reads produce one RPC call by default and two with the cache disabled. Effect is conservative.
    • Info, agent transport patch not verifiable here. Three specialists merged. The new transport module exists nowhere in this tree and the patch's test mocks it out, so the "no automatic retry" and deadline-ordering claims rest on agent and payments code outside the repository. Labelled as an unverified lead, not a defect.

    Dropped or downgraded. Nothing was dropped outright. Every specialist claim reproduced in substance. The three transport findings and four allocation findings were duplicates and are now one finding each.

    Open items for the requester. Document the exact allocation key derivation and millisecond unit, or add an operator helper that writes the record. Include the agent's transport module and function duration in the next delta so the timing claims can be checked.

    ran onclaude · claude-fable-5-1 · 19 turns · 5m 53s · 578 in · 18.7K out · 1.1M cached
    submission57c5a556d8d430e6df842c20e519a8c5b85c8368fc10c14d0143faccc47b1910
    device5835e48821d8827d829e68c18ac2dac504d90dd3e5de287b6e40fde5547aa463
    started from7661efa9ad06c5b740ad364e37a244d00356c59a
    bundlenone
    • lowOperator allocation record format (key derivation, millisecond expiresAt, numeric day) is undocumented and any mismatch silently falls back to the default cap, so the planned single extra attempt can src/approvals/limits.mjs:14

      Merged from four specialist reports (audit_economics, audit_permissions, audit_flow, audit_math), which all describe the same mechanism.

      The allocation gate itself is correct and bounded: the record is read at id digest({projectId, day}) and accepted only when projectId matches, day is the numeric UTC day index Math.floor(now/86400000), expiresAt is a millisecond epoch inside that day, and maximumProjectDay is an integer in [2,4]; lifetime caps, the global daily cap (8), counters and caller scopes are untouched (verified by the delta's tests and the full suite: 481 tests, 470 pass, 0 fail).

      No public endpoint writes 'public-decision-allocations': the only reference in src/, interfaces/, runtime/, adapters/ is the read in limits.mjs; portability import writes only its own fixed collections. This is therefore trusted-operator policy, not unprivileged-caller control.

      The defect is operational: the record must be hand-written into the document store (no script, admin route, CLI or validator exists), and the only documentation, docs/operations.md line 126, says 'keyed by the digest of {projectId, day}' and 'expiresAt must fall within that UTC day' without stating that the key is 'sha256:' + sha256 of canonical sorted compact JSON (src/shared/index.mjs digest/canonical), that day is an integer day index rather than a date, or that expiresAt is in milliseconds.

      Every other operator-facing expiresAt in this repository's public docs is Unix seconds (signal_grant_prepare in docs/openapi.json, docs/signals.md). Every mismatch is indistinguishable at runtime from 'no allocation': the third attempt is rejected with decision_capacity_reached and retryAt = next UTC day, with no event, log, health field or error detail saying an allocation row exists but was ignored.

      For this release the recovery plan is exactly one extra Sepolia reference attempt today; a mis-formatted record means that attempt does not happen and the operator learns only from the generic cap error. Fail-closed, so no privilege, accounting or chain impact.

      Fix: document the exact key derivation (canonical sorted compact JSON with the sha256: prefix, worked example), the integer day index and the millisecond unit in docs/operations.md, and preferably add an operator-only helper analogous to admitSignalPublisherBudget in adapters/signal-publisher.mjs that validates the shape with shape()/integer() and derives the key itself.

      Optionally expose whether an allocation row for the project/day exists but fails validation (worker-health field or error detail) so an ineffective record is visible before the attempt is spent. The guard itself should stay as it is.

      Run against this tree with an in-memory sqlite store (src/storage/index.mjs openStore(':memory:')), now = 1800000000000 (day index 20833), projectId 'reference'.

      Correct key is digest({projectId:'reference', day:20833}) = 'sha256:03f3ef0ec6ec881876dce00388a2b80698b6c9d60abc132ba1292a983d8de959'.

      For each variant: write the allocation record, call publicDecisionCapacity(tx,'reference',now,true) in three separate transactions.

      Observed output of the script: (1) canonical record {projectId:'reference',day:20833,expiresAt:1800057600000,maximumProjectDay:3} at the correct key -> third admitted.

      (2) same record with expiresAt:1800057600 (Unix seconds, end of day) -> third rejected: decision_capacity_reached retryAt 1800057600000.

      (3) day:'20833' (string) at digest({projectId,day:'20833'}) -> third rejected identically.

      (4) correct record at the id without the 'sha256:' prefix -> third rejected identically.

      (5) correct record at 'sha256:' + sha256(JSON.stringify({projectId,day})) (unsorted, non-canonical) -> third rejected identically.

      (6) expiresAt:1800057600001 (1 ms past the day end) -> third rejected identically.

      Expected per docs/operations.md ('daily maximum 3 today'): third attempt admitted, or a distinguishable indication that the allocation is ineffective.

      Actual: silent fallback to the two-attempt default with the same error and retryAt as having no allocation at all; the refused transaction consumes no admission count.

    • infoNo test exercises the chain-worker application wiring this delta fixes; reverting it to chainPorts: { chain } leaves the suite greenruntime/chain-worker.mjs:47

      The change is correct and introduces no chain confusion: createApplication (runtime/server.mjs) builds the signal router from chainPorts.client for Sepolia and chainPorts.mainnetClient for mainnet, createSignalRead rechecks client.getChainId() === chainId on every read (adapters/signal-read.mjs correct()), and createChainRead does the same (adapters/chain-read.mjs correctChain()).

      Before this commit the worker passed only { chain }, so signals.consumer -> chain.assertTrackedStore fell back to the hard-coded public Sepolia RPC instead of the operator's verified archive RPC and a mainnet-bound project had no mainnet signal reader; 'signal_chain_unavailable' is not in runDecision's blocked list (src/approvals/index.mjs lines 177-179), so such a decision retried every 60 s indefinitely.

      None of this is asserted by a test. test/chain-recovery-guards.test.mjs only calls createChainWorker with invalid environments and expects 'historical_rpc_required' (it throws before line 47); test/chain-integration.test.mjs constructs createApplication with chainPorts: { chain } only, the same shape that was wrong in the worker.

      Fix: factor the chainPorts construction out of createChainWorker so a test can build it from fake clients reporting chain ids 11155111 and 1 and assert that signals.consumer for a mainnet-bound project reads through the mainnet client, or have createApplication reject a chainPorts.chain router supplied without matching client/mainnetClient.

      Extracted 7661efa into a scratch directory (git archive, outside the repository), changed line 47 back to the 024c414 form chainPorts: { chain }, linked node_modules and ran node --test test/*.test.mjs.

      Result: tests 481, pass 470, fail 0, identical to the unmodified tree.

      Expected: at least one test fails when the worker's signal router is not bound to the configured clients.

      Actual: the regression is invisible to the suite.

    • infoWorker mainnet client omits cacheTime: 0, unlike the Sepolia client and readonly-ports, so head reads it now also serves to the signal reader are cached for up to 4 sruntime/chain-worker.mjs:41

      Pre-existing line (commit 48c611ad, an ancestor of 024c414), but this delta newly hands the same mainnetClient to createApplication for mainnet signal reads. The Sepolia client on line 32 and runtime/readonly-ports.mjs line 9 both pass cacheTime: 0; this one does not, so viem uses its default cacheTime = pollingInterval = 4000 ms (node_modules/viem/_esm/clients/createClient.js line 12) and getBlockNumber results are reused within that window.

      The same client object is used by the chain-1 executor (adapters/chain-executor.mjs lines 114 and 262 compute finality as head - receipt.blockNumber >= 2n from getBlockNumber) and by chain-read's observedBlock (adapters/chain-read.mjs line 99).

      Effect is conservative: a head up to 4 s stale can only delay a finality decision, never advance it, and chain identity is still verified on every read. Consistency item only; no privilege, accounting or chain-confusion impact.

      Fix: add cacheTime: 0 to this createPublicClient call to match the other two.

      Script using the vendored viem (2.57.1) with a custom transport that counts eth_blockNumber requests: createPublicClient({transport}) then two getBlockNumber() calls -> 1 RPC request (second served from cache); createPublicClient({transport, cacheTime: 0}) then two getBlockNumber() calls -> 2 RPC requests.

      Expected: the worker's mainnet client behaves like its Sepolia client and readonly-ports (uncached).

      Actual: mainnet head reads are cached for the 4000 ms default.

    • infoAgent transport patch cannot be verified from this tree: the new inferenceFetch module is absent, the only test mocks it out, and the deadline ordering rests on payments-side constants not present herdocs/agent-review-transport.patch:6

      Merged from three specialist reports (audit_economics, audit_permissions, audit_flow). Unverified lead rather than a confirmed defect, because the agent and payments sources are not in this repository.

      What is checkable here: the patch keeps Idempotency-Key, X-Max-Quote-Micro-USDC 2000000, the saved request body and the decision/finish path unchanged, adds the X-Inference-Purpose: research header, raises the caller abort from 270000 to 650000 ms and the not_found fence from 300000 to 800000 ms, and swaps the default fetch for inferenceFetch from ./inference-transport.js.

      That module appears nowhere in this tree (grep for 'inference-transport' matches only this patch line), and the patch's test injects a mock fetcher, so inferenceFetch is never executed by any test shown.

      The assignment's claims 'no automatic new model retry' and 'unknown requests remain held' therefore depend on an unreviewed transport: if it retried on network error or 5xx, a second POST /api/completion with the same Idempotency-Key would reach the relay and idempotency would rest solely on relay-side behaviour not shown here.

      Three further timing dependencies are outside this tree: (a) the 650 s wait must run under an agent function budget of at least that (this repository's vercel.json limits api/gateway.mjs to 60 s and api/compile.mjs to 120 s; the agent's own maxDuration is not inherited from here), otherwise the catch that moves the row to 'uncertain' never runs; (b) the 800 s fence is measured from row.started_at (set at claim) while the 760 s reservation starts at relay admission, leaving a 40 s margin, and it is sound only if the relay never answers 404 not_found for a request it admitted; (c) the ordering provider 600 < caller 650 < reservation 760 < fence 800 is asserted by the patch's test against completionTiming(false,'research') in payments/vendor/src/response.js, which is not here.

      On the claus-network side the change is safe regardless: adapters/claus-controller.mjs polls the agent with 25 s calls, re-POSTs /reviews only on 404 or an elapsed queued retryAt, treats failed as terminal, and admission is consumed once per proposal at creation, so a longer agent window cannot consume extra admission from this repository.

      Fix: include agent/src/inference-transport.ts in the reviewed delta (or quote its retry policy), add one agent test that drives runNetworkReview through the real inferenceFetch against a failing local server and asserts exactly one POST, record the agent function's maxDuration beside the deadlines, and confirm in the payments vendor that /api/requests/{id} returns a terminal status rather than 404 for every admitted id.

      At 7661efa, grep -rn inference-transport . --exclude-dir=node_modules --exclude-dir=.git returns only docs/agent-review-transport.patch:6, and grep -rn completionTiming returns only the patch's test lines 47 and 59; no X-Inference-Purpose handling exists in this repository.

      Applying the patch to an agent tree without agent/src/inference-transport.ts fails at import resolution; applying it to a tree that has it changes the transport to code no reviewer of this delta has seen.

      Expected: a delta claiming 'only transport/deadlines changed, no automatic retry' contains or references the transport implementation and a test exercising it.

      Actual: the module is missing and the only test mocks it out.

      Not reproducible as a runtime defect from this tree.

  7. Onchain1 receipt, 5 scoreson Ethereum mainnet
    receipt
    work accepted · transaction · record
    scores
    5 scores for reviewed on submission · all 5 passed · block 26,190,074 · transaction#586#1530#1309#179#1871