{"assessments":[],"deployments":[],"fuzz":[],"identity":{"adapter":"0xde152afb7db5373f34876e1499fbd893a82dd336","chainId":1,"collection":"0x0000ec93127baa929e58e97dd0095a2bfb38ec1d","registry":"0x8004a169fb4a3325136eb29fa0ceb6d2e539a432"},"interpretation":"Records acceptance and evidence. Neither completion nor an AI assessment establishes correctness, safety, or independent review.","jobId":"2bb2335f-3d83-4171-b1cd-b7153912baaf","kind":"audit","nodes":[{"acceptedSubmissionHash":"1fecd385e39cc40a80c2f67017a7ed3baa4869e4c3215216cb761db1c07c942c","dependsOn":[],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","tools":[]},"key":"audit_economics","kind":"code","role":"review","skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","state":"accepted"},{"acceptedSubmissionHash":"4cc44c9b5083b1eb8c5d154b936a246a425cedd05f8a4f30bb6a14e015976647","dependsOn":[],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","tools":[]},"key":"audit_flow","kind":"code","role":"review","skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","state":"accepted"},{"acceptedSubmissionHash":"13cdb6ad22f3d16bf0314c8cd0b328e5ba99bee313867e644e13afa870ee6daf","dependsOn":["audit_math","audit_permissions","audit_economics","audit_flow"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"af3aa01159bbf354f621cafa5c0006f6169e0938a8b015be508b7377dbf165bc","skillId":"audit-judge","tools":[]},"key":"audit_judge","kind":"code","role":"review","skillHash":"af3aa01159bbf354f621cafa5c0006f6169e0938a8b015be508b7377dbf165bc","skillId":"audit-judge","state":"accepted"},{"acceptedSubmissionHash":"278c62db5456f2f3a34982adbab78fb69f1d5e1e197fc3d5107fe619ffd5473e","dependsOn":[],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","tools":[]},"key":"audit_math","kind":"code","role":"review","skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","state":"accepted"},{"acceptedSubmissionHash":"f394500c9fab62f6b31dd3a22c90840be2ec9989f17e553237e73015e9b72800","dependsOn":[],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","tools":[]},"key":"audit_permissions","kind":"code","role":"review","skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","state":"accepted"}],"objective":"Independently assess the final consolidated 0.12.1 release for (B) public sponsored reviews and Sepolia readonly projects/contract intake and (C) the prepared Ethereum mainnet noncustodial readonly scope. Public B remains OFF pending this assessment; C additionally requires exact human approval. Review only material changes from b9603c83766d8c3e23e19a79b0c5f85be338b092, reusing completed earlier independent evidence. Prior jobs: https://api.imdnotdead.fun/jobs/05097b72-02e2-40e8-b9b7-4f7087fe18f8/submissions for 0.11.3 public admission and https://api.imdnotdead.fun/jobs/9929337d-2983-4143-bac9-0dd85e02e726/submissions for 0.12.0 Mainnet. Older admission jobs a91da909-3bc4-468b-b5fb-cff58907da1a and 3af99ba5-482b-484a-9199-59d040f5e96c are complete. The unchanged bounded core contract https://github.com/hazarxyz/claus-network-contracts at af8cf6370d5b3893fa5b38df251d4b27b5db5f8d was reviewed in job 93f20ace-67f2-489e-affa-f10b94e1f359. Do not treat the prior Sepolia scope as automatic Mainnet acceptance.\n\nVerify that concrete findings are addressed: bounded single active registration and finite lifetime records; exact receipt-based relayed registration/execution recovery; identical-byte rebroadcast under original nonce/hash/fees/authority; expired unsent actions terminate; externally completed callbacks use the original resolution evidence; same-source retry only after proven void; expired pilots receive public decision/durable-agent limits before first gas reservation; intake RPC filters registered binding/requester topics and exact pairs before its page cap; dispatch maximumTransaction <= maximumProject; enrollment capacity uses 15-minute backoff; daily decision limits wait only within the original deadline; concurrent onchain rejection is explicitly terminal and visible in binding.latestRejection (not a new queue); chain worker role mismatch refuses before jobs; missing chain port defers before beginEffect; HTTPS mainnet RPC; bounded resumable finalized event lookup for cancellation and relays; Mainnet release requires distinct roles, fixed 172800 recovery, explicit bounded gas limit, retained target artifact and stored exact verification proof.\n\nScope and intentional limits: only chains 1/11155111, project chain fixed; public gas is independent per chain, model decision limits share one operator pool. One active intake per project is intentional; a rejected second request gets no callback and must be explicitly resubmitted after the active request ends. Unknown payment, dropped/replaced transaction and spent gas are never treated as free without proof. A mined revert still spends gas. Shared review concurrency and finite global/project gas caps are ceilings, not reserved slots or guaranteed lifecycle completion. Mainnet owner approval is a protected operator-side record of the direct human authorization, not a cryptographic signature or public API input; no new remote authorization endpoint is claimed. Config/source access already trusts the operator. A revoked/expired unsent bootstrap is terminal; renewal requires an explicitly authorized new plan identity, not clearing journals. Mainnet's 30M gas bootstrap cap is subordinate to its 0.0046 ETH cost cap. Higher fees require a revised owner-approved allocation, not automatic funding. The deterministic low-fee local fixture is not a promise to work at arbitrary gas prices.\n\nB authority: 4 IMD total, <=0.5 per public review, 7 days, no refill; 0.003 Sepolia ETH total, including 0.002 executor operations, 0.0004 dispatcher, 0.0004 reference/revocation, 0.0002 bootstrap. Public dispatch per-project and per-tx are both 0.00005 test ETH. C proposed (not authorized): 0.02 ETH total = 0.0004 owner funding gas + 0.0046 bootstrap + 0.015 operations for 7 days, 8 projects, 0.002/project, 0.001/tx, no refill. One reference owner gas transfer/revocation is charged conservatively inside that same operating pool. No public wallets/vaults, deposits, tokens, Mainnet Inbox or inference. The agent schema patch remains staged until C approval.\n\nInspect independently, reproduce any concrete remaining defect with severity and exploitability, and state whether each of B and C's bounded scope is acceptable or blocked. Distinguish operator trust and intentionally finite capacity from an authorization bypass. Do not modify sources, deploy, sign, post, open paid subjobs or spend anything.\n","parentJobId":null,"planHash":"45e5fe56e5cc937bb00fdaa538461a6713cc8645305e8636e6eaaf64b317aa64","previousHash":"0000000000000000000000000000000000000000000000000000000000000000","projectId":"2bb2335f-3d83-4171-b1cd-b7153912baaf","publication":{"commit":null,"deliveredAt":null,"repoUrl":null},"receiptIdentity":{"adapter":"0xde152afb7db5373f34876e1499fbd893a82dd336","chainId":1,"collection":"0x0000ec93127baa929e58e97dd0095a2bfb38ec1d","registry":"0x8004a169fb4a3325136eb29fa0ceb6d2e539a432"},"registry":"0xb6d0a187b050fa5bb0b87033a203f37becf4a775","research":[],"schema":"identitymd-work-v1","signals":[{"agentId":"51315","feedbackHash":"0577470c626406db9a2363296f51a6787c4c6732b7c6cafcb494c215d5817b21","nodeKey":"audit_economics","submissionHash":"1fecd385e39cc40a80c2f67017a7ed3baa4869e4c3215216cb761db1c07c942c","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52163","feedbackHash":"312b9b9363658754e16a181fbac8c5b63b63e8c8152eb91df18a4988889a9e7c","nodeKey":"audit_flow","submissionHash":"4cc44c9b5083b1eb8c5d154b936a246a425cedd05f8a4f30bb6a14e015976647","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52289","feedbackHash":"a6bdd4dc58f799885167585ba6236d463a03785b883e5be8415f11979cd3e11c","nodeKey":"audit_judge","submissionHash":"13cdb6ad22f3d16bf0314c8cd0b328e5ba99bee313867e644e13afa870ee6daf","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52606","feedbackHash":"806322945b88689b1af623c5e5fea22a32adf4b7cae3441c5ee2452e0ec06cd7","nodeKey":"audit_math","submissionHash":"278c62db5456f2f3a34982adbab78fb69f1d5e1e197fc3d5107fe619ffd5473e","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52158","feedbackHash":"05edcd536e74e05ae252675bcb1845fff0d20eec696e1dd1cbef885fd5047947","nodeKey":"audit_permissions","submissionHash":"f394500c9fab62f6b31dd3a22c90840be2ec9989f17e553237e73015e9b72800","tag1":"review:submission","tag2":"acceptance-v2","value":1}],"site":null,"snapshotHash":"0253373fb9139c2a3be919bd9e22682f6b256f1611ee53877bf8f280cb6861c7","state":"completed","submissions":[{"artifacts":[],"attempt":1,"bundleHash":null,"device":"ee9fbaf2480d1034","findings":[{"citation":"resolved","description":"Merged from three specialist reports (audit_math, audit_flow, audit_permissions). relayedRegistration() is entered whenever projects(projectKey).target is non-zero at latest (line 63-64). It scans ProjectRegistered logs only inside finalized ranges starting at value.observedBlock (recoverChainEvent returns complete:true, logs:[] when start is above the finalized head). Whenever the relayer's registerProject block is above the finalized head, which on Sepolia and Mainnet is the normal state for roughly 13-15 minutes after any relayed registration, it returns {state:'submitted', relayed:true} with no hash. deployment.step then returns { state: 'registering', transactionHash: tx.hash } with tx.hash === undefined (line 119). runOne (src/chain-deployments/index.mjs:171) spreads that result into the chain-registrations row; canonical() in src/shared/index.mjs:53 rejects an undefined property value with invalid_json, so the store transaction rolls back. Because runOne does `return jobs.finish(...)` inside its try block without await (src/chain-deployments/index.mjs:168), the rejected promise bypasses the catch at line 179 that would have produced a bounded waiting retry. runOne rejects, runtime/chain-worker.mjs tick() rejects, worker-health is not written and app.approvals.runExecution is skipped for that tick. The job stays state running / effect outcome_unknown under its 120 s lease with no retryAt, and is re-claimed and throws again at every lease expiry. The registration row never leaves queued; the project cannot prepare another registration (registration_in_progress) and cannot cancel the job (effect already started). On the anvil fixture the finalized tag equals latest, so the window is only exercised when the fixture mines few blocks per advance: the repository's own relay fixture fails on the Sepolia chain id and passes on chain id 1 only because the chain-1 fixture mines 0x60 blocks per advance. This is a liveness/robustness defect in the headline exact receipt-based relayed registration recovery path, not an authorization bypass; no funds are spent. The same undefined hash is produced on the terminal-fallback path at line 117. Fix: never return a result containing undefined (return hash: null or omit transactionHash when no event is found yet), and change `return jobs.finish(...)` to `return await jobs.finish(...)` in runOne so commit failures reach the catch and become a bounded waiting retry (the same pattern recurs in runDecision/runExecution/intake.run).","line":71,"path":"adapters/chain-executor.mjs","reproduction":"Real-chain reproduction with the repository fixture: `anvil --chain-id 11155111 --port 18547 &` then `CLAUS_TEST_CHAIN=http://127.0.0.1:18547 CLAUS_TEST_CHAIN_ID=11155111 CLAUS_TEST_RELAY=1 node --test test/chain-integration.test.mjs`. Expected: pass (the same command passes without CLAUS_TEST_RELAY, and passes with CLAUS_TEST_CHAIN_ID=1 against `anvil --chain-id 1`). Actual: `not ok 1 ... Error: invalid_json` with stack src/shared/index.mjs:53 canonical <- src/storage/index.mjs:70 write <- src/chain-deployments/index.mjs:171 <- src/jobs/index.mjs:104. Mock reproduction (no chain): store with network-capabilities chain {enabled, chainId 11155111, executor E, executorPayer = signer}, review-pilots p active, chain-modules module verified, chain-registrations reg-1 queued with observedBlock '100' and message.expiresAt = now+3600, no chain-transactions row register:reg-1; client mock: readContract(projects) returns [owner, non-zero target, mandateHash, policyVersion, 0, ...], getBlock({blockTag:'finalized'}).number = 100n, getBlockNumber() = 103n, getLogs() = []. executor.deployment.step({kind:'project-register', value, signature, authorize}) returns exactly { state: 'registering', transactionHash: undefined } and canonical(result) throws code invalid_json. Feeding that result through createChainDeployments(...).runOne with a real createJobs store: runOne rejects with code invalid_json; workflow-jobs row stays state running, effect outcome_unknown, lease.until > now; chain-registrations reg-1 stays queued; jobs.claim is idle until the lease expires, after which runOne rejects identically again.","severity":"medium","snippet":"    if(!logs.length)return {state:'submitted',relayed:true};","title":"Relayed registration not yet in the finalized scan range returns transactionHash undefined; the job commit rejects it as invalid_json and the un-awaited jobs.finish rejection escapes runOne, crashing "},{"citation":"resolved","description":"Merged from audit_flow and audit_math. deployment.step calls relayedRegistration(value) before transact() whenever no local register:<id> transaction exists. The only place where an unsent action's window becomes terminal is transact() line 23 (`if (!status && expiresAt <= now()) return { state: 'cancelled' }`), and the relayed path never reaches it. Two sub-cases: (a) projects(projectKey).target is non-zero but the complete finalized history from value.observedBlock holds no ProjectRegistered event, because the onchain registration precedes observedBlock. projectKey = keccak(owner, keccak(projectId)) is fixed per project, so this is the state after any earlier owner-signed registration A was relayed and mined (owner relayed it directly, or the executor broadcast it and the local job was cancelled before a binding was written) and the owner later prepared and submitted registration B with a new nonce. relayedRegistration returns {state:'submitted'} regardless of found.complete and regardless of value.message.expiresAt, so B stays registering indefinitely, days after its one-hour signing window closed, no chain-binding is written, prepareRegistration for any other parameters throws registration_in_progress, and the job cannot be cancelled (effect started). Combined with the undefined-hash finding this is a permanent crash loop of the executor tick on that job. (b) The found event carries a different owner/mandateHash: requireThat throws registered_project_mismatch, which is not in runOne's blocked list (src/chain-deployments/index.mjs:181) and not in authorityDenied, so the job is finished waiting with a 60 s retry forever, performing 5-6 RPC reads per minute, long after message.expiresAt. The stated requirement that expired unsent actions terminate does not hold for this path. Fix: apply the `value.message.expiresAt * 1000 <= now()` cut-off before or inside relayedRegistration; when found.complete is true and no matching event exists, return a terminal state such as {state:'blocked', error:'project_already_registered'} (or scan from the module receipt block so an earlier same-owner registration is recovered); and return a terminal blocked state instead of throwing on a mandate mismatch.","line":106,"path":"adapters/chain-executor.mjs","reproduction":"Mock A (never terminates): same fixture as the undefined-hash finding but finalized head 190n, latest 200n, getLogs() = [] for every range from observedBlock 100 (complete history, no event). step() returns {state:'registering'}; advance the clock 10 days past message.expiresAt and call step() again with identical inputs. Expected: a terminal state (blocked/failed) as transact() would return for any other expired unsent registration. Actual: {state:'registering', transactionHash: undefined} again and no chain-transactions row register:reg-1 exists. Mock B (mismatch): chain-registrations reg-1 with message.mandateHash 0xa2..a2 and message.expiresAt = now-10 s; readContract(projects) returns mandateHash 0xa1..a1; getLogs returns one ProjectRegistered log with args.mandateHash 0xa1..a1 in a finalized block. Expected: terminal blocked. Actual: step() rejects with code registered_project_mismatch; through runOne this becomes state waiting with retryAt now+60000 on every attempt.","severity":"medium","snippet":"      let tx = !pending ? await relayedRegistration(value) : null;","title":"Relayed-registration recovery has no terminal outcome: an expired unsent registration whose project is already registered onchain is never blocked, and a mismatching mandate is retried every 60 s fore"},{"citation":"resolved","description":"From audit_economics; reproduced. 0.12.1 replaced the earlier `if (!allowSubmit || expiresAt <= now() || !await active(store, request)) return answer('pending')` with a terminal closed answer whenever expiresAt <= now(). For the resolve action expiresAt = min(resolution.expiresAt, budget.expiresAt) and for the callback action it is budget.expiresAt, so the lapse of a finite operator allocation (not an onchain fact) produces the same terminal state as the Inbox deadline. runtime/contract-intake-worker.mjs:29 skips every request whose delivery.state is closed forever. Budget rotation is an explicitly supported operation (createContractDelivery re-binds pending work to its original budget), but a request that had no transaction yet when the old budget expired is permanently unreachable by a new budget even though the Inbox still accepts resolve/deliverCallback until the request deadline; likewise the callback of a request whose resolve confirmed before the budget expired is closed for good. This is consistent with the stated no-refill scope as far as the first window goes, but it misreports state (closed while the onchain request is open) and makes a separately authorized second allocation unable to deliver last-day results. No funds are lost and nothing is sent. Minimal fix: answer closed only when the onchain deadline has passed (action === 'resolve' && resolution.expiresAt <= now()), and answer pending when only the budget window has lapsed, as before 0.12.1.","line":187,"path":"adapters/contract-delivery.mjs","reproduction":"Budget first with expiresAt = T0 + 1 day; contract-request in result_ready whose observation.deadline = T0 + 3 days; binding active until T0 + 10 days; chain.deliveryState reports pending:true, resolved:false, closed:false. Run createContractIntakeWorker({delivery: createContractDelivery({budget: first})}).tick() at clock = T0 + 1 day + 60 s. Expected: delivery.state pending. Actual: delivery.state closed, zero broadcasts. Then admitContractDispatchBudget(second, same inbox/dispatcher, expiresAt T0 + 5 days) and tick the worker with createContractDelivery({budget: second}) at T0 + 1 day + 120 s. Expected: one resolve signed and broadcast, second.reserved = 135000000000. Actual: the request is skipped by the [delivered, closed, failed] filter, broadcasts stay 0 and second.reserved stays 0; calling delivery.step directly with the second budget on the same request answers pending and broadcasts one resolve, proving the closed state was premature.","severity":"low","snippet":"        if (expiresAt <= now()) return answer('closed');","title":"Dispatcher budget expiry is reported as a terminal closed delivery while the Inbox request is still open, so a later admitted allocation can never deliver it"},{"citation":"resolved","description":"From audit_economics; reproduced. The 0.12.1 RPC topic filter and exact pair check (line 54-67) exclude anonymous and cross-product spam before the page cap, which closes the prior finding for unadmitted traffic. The cap is still applied to the raw count of events that pass the pair filter. A binding admits at most maxRequests (1..100) requests and a project may hold one queued intake request, so at most a handful of events per binding can ever be ingested; every further event from the same pair is a deterministic contract_binding_exhausted / contract_request_in_progress rejection that still consumes one of the 32 page slots and the observe RPC calls. The worker's bindings list cannot exclude the pair until an ingest increments used, and the emitter can keep used below maxRequests by never completing. Any admitted project (durable-agent credential plus one binding, Sepolia gas only) can therefore delay every other admitted project's intake by N/32 ticks. This is finite shared capacity among admitted parties rather than an authorization bypass, but it defeats the purpose of the pair filter for the public B window where up to 64 projects share one scanner. Minimal fix: when bindings is supplied, keep at most the binding's remaining quota (maxRequests - used, or a small constant) of events per binding before the 32-event slice; dropped events are exactly those that would be rejected anyway.","line":71,"path":"adapters/contract-intake.mjs","reproduction":"scan({afterBlock: 0, bindings: [{id: A, requester: spammer}, {id: B, requester: victim}]}) with a mock getLogs returning 640 events for the (A, spammer) pair in block 1 and one event for the (B, victim) pair in block 2 (mock client as in test/contract-intake-scan.test.mjs). Expected: the victim event is in the first page since binding A can admit at most maxRequests events. Actual: the first 20 pages hold only (A, spammer) events with cursors {afterBlock: 1, afterLogIndex: 31, 63, ...}; the victim event is first returned on the 21st page.","severity":"low","snippet":"      const remaining = logs.length > 32;","title":"An admitted requester/binding pair can fill the 32-event intake page with its own rejected Requested events, deferring every other admitted project by ceil(N/32) ticks"},{"citation":"resolved","description":"From audit_flow; reproduced. recoverChainEvent stores {nextBlock, blockNumber, blockHash} after each empty finalized range and, on the next call, requires the block at blockNumber to still carry blockHash. If it does not (an RPC returned a wrong block hash when the cursor was written, the endpoint was switched to a node serving a different view, or a finality incident), every call throws recovery_block_changed, never rewinds to fromBlock and never advances. The error is neither in authorityDenied nor in the blocked lists of runOne/runExecution, so the owning job is finished waiting and retried every 60 s indefinitely; the registration or execution never reaches a terminal state and the only recourse is manual deletion of the chain-event-recovery row, which contradicts the stated rule that journals are never cleared. Operational impact only; no spend or authority is affected. Fix: on a hash mismatch, discard the cursor and restart the scan from fromBlock (a changed block invalidates exactly the previous no-event conclusion), or surface a terminal blocked state.","line":8,"path":"adapters/chain-event-recovery.mjs","reproduction":"Store row chain-event-recovery/<digest({chainId:1,key:K,query})> = {nextBlock:'10500', blockNumber:'10499', blockHash:'0xold10499'}; client.getBlock({blockNumber:10499n}).hash = '0xnew10499', finalized head 25000. Call recoverChainEvent({store, client, chainId:1, key:K, query, fromBlock:500n}) three times. Expected: the scan restarts from fromBlock or returns a terminal result. Actual: every call rejects with code recovery_block_changed, getLogs is never called, and the stored row still has blockNumber 10499.","severity":"low","snippet":"  if(old) requireThat((await client.getBlock({blockNumber:BigInt(old.value.blockNumber)})).hash===old.value.blockHash,'recovery_block_changed');","title":"A recovery cursor whose stored block hash no longer matches wedges event recovery permanently: the cursor is never reset and recovery_block_changed is retried forever"},{"citation":"resolved","description":"From audit_permissions; reproduced in a mock but with narrower real preconditions than reported, so kept as info. The cancelled/reverted branch for action callback (line 193-198) accepts the external delivery only when deliveryState(request, 'finalized') already shows callbackDelivered, otherwise it returns a terminal closed/failed that runtime/contract-intake-worker.mjs:29 never revisits. The !transaction branch (line 180-184) handles the same race correctly by answering pending. The specialist's reverted scenario is not realistic: ClausRequestInbox.deliverCallback reverts only when callbackDelivered is already true, so the external delivery is in an earlier block than our reverted transaction and is finalized whenever reconcile (finalized: true) reports reverted. The cancelled scenario is real but narrow: it needs a prepared-but-unsent callback transaction to survive a tick (submit threw after prepare, e.g. an RPC failure in chain()), and an external deliverCallback to land inside the finality window before the next tick; submit then cancels the unsent transaction because observedForSubmit.callbackDelivered is true, the finalized read still lags, and the request is recorded closed although the callback was delivered and the resolution receipt exists. No funds are lost and nothing extra is sent. Fix: in the cancelled/reverted branch for action callback, answer pending when the latest state shows callbackDelivered but the finalized state does not yet, mirroring line 182-184.","line":198,"path":"adapters/contract-delivery.mjs","reproduction":"Mock (as test/contract-delivery.test.mjs with a blockTag-aware deliveryState): resolve broadcasts and confirms; callback is prepared and broadcast; the mock then marks callbackDelivered true at latest only and returns a receipt with status reverted (or cancels at submit). step() answers failed/closed. After the mock marks callbackDelivered at finalized, a second step() with identical inputs answers delivered, showing the earlier terminal answer was premature; the production worker never issues that second step.","severity":"info","snippet":"        return answer(transaction.state === 'cancelled' ? 'closed' : 'failed');","title":"A callback cancelled at submit because someone else delivered it is answered closed before the external delivery is finalized, and the worker never re-checks"},{"citation":"resolved","description":"From audit_permissions; reproduced. The bound counts every chain-registrations row of the project, including awaiting_signature intents that expired after their one-hour window and blocked/failed/cancelled rows. Each prepareRegistration after the previous intent expired creates a new row because the digest includes expiresAt and observedBlock. An owner who prepares and does not sign within an hour sixteen times, or accumulates sixteen terminal attempts, can never prepare again: registration_limit is returned forever, there is no prune, no owner-side cancel for awaiting_signature rows and no documented operator operation to clear them. This matches the stated intent of finite lifetime records and is reachable only by the project owner, so it is recorded as an intentionally finite capacity with an irrecoverable consequence rather than a bypass. Suggested fix: count only live rows (queued/registering/verifying or unexpired awaiting_signature) plus a bounded number of terminal rows, or let the owner expire awaiting_signature intents.","line":111,"path":"src/chain-deployments/index.mjs","reproduction":"Pilot project with a verified module and valid mandate (fixture of test/registration-delay.test.mjs). Call prepareRegistration with the same {moduleId, mandateId, configuration:'0x01', minDelaySeconds:0} sixteen times, advancing the clock 3600001 ms between calls so each intent expires unsigned (sixteen distinct ids). Expected: a later preparation for an unbound project is accepted. Actual: the seventeenth call rejects with code registration_limit and still rejects 30 days later; chain-bindings for the project is null.","severity":"info","snippet":"        requireThat(registrations.length < 16, 'registration_limit');","title":"Sixteen expired unsigned registration preparations permanently lock a project out of chain registration; the bound counts dead intents and nothing prunes them"},{"citation":"resolved","description":"From audit_math; arithmetic verified. Not an authorization or accounting defect: a numerical boundary the C decision should be made with. prepare() in adapters/sepolia-transactions.mjs:63-66 computes gas = estimate*125/100 + 10000 and requires gas * fees.maxFeePerGas <= maxCost, where maxCost is plan.bootstrapMaximumWei (proposed 4.6e15 wei). With the ~19.5M-gas creation documented at adapters/sepolia-transactions.mjs:6-7 the padded limit is 24,385,000 gas, so the bootstrap is preparable only when viem estimateFeesPerGas().maxFeePerGas (about baseFee*1.2 + priority) is <= 4.6e15 / 24,385,000 = 188,640,557 wei (0.189 gwei); at the full 30M-gas plan cap the ceiling is 0.153 gwei. The check uses the padded limit, not expected usage, so the effective ceiling is 25% tighter than 0.0046 ETH / 19.5M gas suggests. docs/mainnet.md says to estimate before funding and that the release waits or fails closed, but the fee level the allocation actually tolerates is not recorded, and any revised bootstrapMaximumWei needs a re-issued owner approval (planHash). Recommend recording the tolerated fee ceiling next to the 0.0046 ETH figure before approval. The deterministic fixture in test/mainnet-release.test.mjs sets the anvil base fee to 0.1 gwei, below the ceiling.","line":44,"path":"integration/mainnet-release.mjs","reproduction":"plan.bootstrapMaximumWei = '4600000000000000', plan.bootstrapMaximumGas = 30000000, client.estimateGas = 19,500,000, client.estimateFeesPerGas = { maxFeePerGas: 200,000,000 wei (0.20 gwei), maxPriorityFeePerGas: 1 }. gas = 19,500,000*125/100 + 10,000 = 24,385,000; gas*maxFeePerGas = 4.877e15 > 4.6e15. Expected per the plan: a 0.0046 ETH bootstrap. Actual: deployMainnetRelease -> txs.prepare throws transaction_cost_limit before any reservation or signing; nothing is spent and no journal entry is created. At maxFeePerGas = 188,000,000 wei the same inputs pass (4.584e15 <= 4.6e15).","severity":"info","snippet":"    limits: { gas: String(plan.bootstrapMaximumGas), cost: plan.bootstrapMaximumWei },","title":"C bootstrap: the 0.0046 ETH cost cap tolerates a maxFeePerGas of only about 0.19 gwei at the documented ~19.5M-gas executor creation; above that the release fails closed"},{"citation":"resolved","description":"From audit_math; arithmetic verified. Capacity observation for the B scope, not an authorization bypass: nothing is spent beyond the ceilings. The public allocation sets maximumTransaction = maximumProject = 5e13 wei. prepare() requires (estimate*125/100 + 10000) * maxFeePerGas <= 5e13. ClausRequestInbox lets a requester choose callbackGas up to MAX_CALLBACK_GAS = 1,000,000 (contracts/src/ClausRequestInbox.sol:28) and deliverCallback forwards that much gas, so the estimate is at least callbackGas plus overhead. For callbackGas = 1,000,000 the padded limit is >= 1,260,000 gas and the callback can only be prepared when maxFeePerGas <= 5e13 / 1,260,000 = 39,682,539 wei (0.040 gwei); a ~200k-gas callback tolerates about 0.19 gwei; the resolve itself (~80-120k gas) tolerates 0.31-0.45 gwei. Above those fees the worker records delivery.reason transaction_cost_limit with a 15-minute retryAt (runtime/contract-intake-worker.mjs:45-52) until budget.expiresAt, after which the callback answers closed (line 187) and the requester receives only the onchain resolve. Because per-project and per-transaction ceilings are both 0.00005, a project whose resolve cost 3e13 wei has at most 2e13 wei left for its callback regardless of fee level. The docs say these are ceilings, not guarantees, but do not tell a requester which callbackGas values are realistically deliverable. Recommend publishing a recommended maximum callbackGas, or stating that callbacks are best-effort below about 0.2 gwei, with the public B announcement.","line":160,"path":"adapters/contract-delivery.mjs","reproduction":"Public budget with maximumTransaction = maximumProject = '50000000000000'; an observed request with callbackGas = 1,000,000; estimateFeesPerGas().maxFeePerGas = 100,000,000 wei (0.1 gwei); estimateGas(deliverCallback) >= 1,000,000. prepare(): gas >= 1,260,000; gas*maxFeePerGas >= 1.26e14 > 5e13 -> throws transaction_cost_limit. Expected by a requester reading docs/contract-intake.md: a callback to its contract. Actual: delivery.state pending with reason transaction_cost_limit and retryAt +15 min on every tick while fees stay above 0.0397 gwei; after budget.expiresAt the callback closes unsent. With callbackGas = 0 the same request completes with the resolve only.","severity":"info","snippet":"        limits: { gas: budget.maximumGas, feePerGas: budget.maximumFeePerGas, cost: budget.maximumTransaction },","title":"B dispatch: the 0.00005 test ETH per-transaction ceiling makes callbacks with large requester-chosen callbackGas deliverable only below about 0.04 gwei"}],"hash":"13cdb6ad22f3d16bf0314c8cd0b328e5ba99bee313867e644e13afa870ee6daf","nodeId":"dc9d1a86-5591-4e17-a6d8-8273a84444de","outcome":"completed","summary":"Review complete. The findings file holds nine reproduced findings; the working tree is otherwise unchanged (only `.imd-findings.json` and my scratch tests under `test/scratch/`, which are not kept).\n\n**What I verified**\n\n- Baseline suite on this tree passes (402 pass, 12 environment skips), matching the release note.\n- Every specialist claim was re-derived against the code and reproduced with a mock test or the repository's own fixture. All reproduced mechanically; two were re-scoped after inspection.\n- Specialist proofs were node tests, not Foundry, and none of the findings are in Solidity, so no `proof` fields were attached.\n\n**Findings kept (merged, ranked)**\n\n1. **Medium, relayed registration with no hash crashes the executor tick.** When a relayer's `registerProject` is mined above the finalized head, `relayedRegistration` returns no hash, `runOne` tries to store `transactionHash: undefined`, the commit throws `invalid_json`, and because `jobs.finish` is returned without `await` the rejection escapes the catch. The repository's own relay fixture reproduces it: with `CLAUS_TEST_CHAIN_ID=11155111 CLAUS_TEST_RELAY=1` against anvil the test fails with `invalid_json`; it passes on chain id 1 only because that fixture mines enough blocks to finalize. Merged three specialist reports.\n2. **Medium, relayed path has no terminal outcome.** The relayed branch runs before `transact`, so the \"expired unsent actions terminate\" guard is never reached. A project already registered before `observedBlock` stays `registering` forever, and a mandate mismatch throws and retries every 60 seconds. The project cannot prepare another registration or cancel the job. Merged two reports.\n3. **Low, budget lapse reported as terminal `closed`.** The 0.12.1 diff replaced `pending` with `closed` when only the dispatcher budget expired, so a later admitted allocation can never deliver a request whose Inbox deadline is still open.\n4. **Low, admitted-pair page starvation.** One admitted binding can fill the 32-event page with rejected events; the victim's event surfaces on the 21st page in the reproduction.\n5. **Low, recovery cursor wedge.** A mismatched cursor block hash throws `recovery_block_changed` forever and is never reset.\n6. **Info, callback race.** Re-scoped: the specialist's reverted scenario is not realistic because the Inbox only reverts when already delivered, which means the external delivery is finalized before ours is. Only a narrow cancelled-at-submit path remains.\n7. **Info, sixteen expired preparations lock a project.** Finite lifetime records as intended, but irrecoverable.\n8. **Info, C bootstrap fee ceiling** of about 0.19 gwei at 19.5M gas under the 0.0046 ETH cap. Arithmetic verified.\n9. **Info, B callback gas ceiling** of about 0.04 gwei for a 1,000,000-gas callback under 0.00005 ETH per transaction. Arithmetic verified.\n\n**Scope verdicts**\n\n- **B (public sponsored reviews, Sepolia readonly intake): blocked** until findings 1 and 2 are fixed. Both sit on the advertised relayed-registration recovery path that public owners will exercise, and finding 1 takes down the shared executor tick for every relayed registration. Findings 3 to 5 are liveness and state-accuracy defects within intentionally finite capacity, not authorization bypasses, and could ship with a note. No authorization bypass, unbounded spend, or custody exposure was found; the per-transaction and per-project caps, 15-minute backoffs, deadline-bounded daily waits, binding rejection visibility, role mismatch refusal, and preflight-before-effect checks all hold as stated.\n- **C (Mainnet noncustodial readonly): blocked** for the same two findings, which apply unchanged to chain id 1 where the finality lag is identical. The release guard itself (distinct roles, fixed 172800 recovery, 30M gas and 0.0046 ETH caps, retained target artifact, stored exact verification proof, HTTPS RPC, human approval record) checks out in code. The owner should see finding 8 before approving the a","treeHash":null,"usage":{"cachedInputTokens":2253034,"inputTokens":578,"model":"claude-fable-5-1","outputTokens":38566,"runtime":"claude","turns":47,"wallClockMs":666062}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"e3a598aae0640402","findings":[{"citation":"resolved","description":"0.12.1 replaced the previous `if (!allowSubmit || expiresAt <= now() || !await active(store, request)) return answer('pending')` with a terminal `closed` answer whenever `expiresAt <= now()`. For the resolve action `expiresAt = Math.min(resolution.expiresAt, budget.expiresAt)` and for the callback action it is `budget.expiresAt`, so the dispatcher budget's own expiry (a finite operator allocation, not an onchain fact) is reported with the same terminal state as the Inbox deadline. The intake worker (runtime/contract-intake-worker.mjs:29) skips every request whose `delivery.state` is `closed` forever. Budget rotation is an explicitly supported operation (`createContractDelivery` re-binds pending work to its original budget and documents that 'Pending transactions keep their original budget after a budget rotation'), but a request that had no transaction yet when the old budget expired is now permanently unreachable by the new budget even though the Inbox still accepts `resolve`/`deliverCallback` until the request deadline. The same applies to the callback of a request whose resolve was confirmed before the budget expired: the resolve receipt is retained but the callback is closed forever. Impact is loss of a deliverable public result (the requester's onchain request then expires unresolved) and a misleading terminal state; no funds are lost. For the proposed B window with 'no refill' this makes the last-day results that miss the window unrecoverable even by an explicit, separately authorized second allocation. Minimal fix: return `closed` only when the onchain deadline has passed (`action === 'resolve' && resolution.expiresAt <= now()`), and answer `pending` when only the budget window has lapsed, exactly as before 0.12.1.","line":187,"path":"adapters/contract-delivery.mjs","reproduction":"State: budget `first` with expiresAt = T0 + 1 day; contract-request in `result_ready` whose observation.deadline = T0 + 3 days; binding active until T0 + 10 days; chain.deliveryState reports pending:true, resolved:false, closed:false. Run the intake worker with createContractDelivery({budget: first}) at clock = T0 + 1 day + 60 s. Actual: step returns `closed`, the worker writes `delivery.state = 'closed'`, zero broadcasts. Expected: `pending`. Then admit budget `second` (same inbox/dispatcher, expiresAt = T0 + 5 days) and tick the worker with createContractDelivery({budget: second}) at T0 + 1 day + 120 s. Expected: one resolve is signed and broadcast and `second.reserved` becomes 135000000000. Actual: the request is skipped by the `['delivered','closed','failed']` filter, broadcasts stay 0, nothing is ever delivered although the Inbox deadline is two days away. Node test (fails on this tree, passes with the fix above): test/scratch/delivery-budget-expiry.test.mjs, run with `node --test test/scratch/delivery-budget-expiry.test.mjs`; its source is the fixture from test/contract-delivery.test.mjs with a mutable clock, a 3-day deadline, a 1-day budget and the real createContractIntakeWorker.","severity":"low","snippet":"        if (expiresAt <= now()) return answer('closed');","title":"Dispatcher budget expiry is reported as a terminal 'closed' delivery while the onchain request is still open, so a later admitted allocation can never deliver it"},{"citation":"resolved","description":"The new RPC topic filter and exact pair check remove anonymous and cross-product spam before the page cap, which closes the earlier finding for unadmitted traffic. The cap is still applied to the raw count of events that pass the pair filter. A binding admits at most `maxRequests` (1..100, default use is 1) requests and a public project may hold only one queued/waiting intake request, so at most a handful of events per binding per page can ever be ingested; every further event from the same pair is a deterministic `contract_binding_exhausted` / `contract_request_in_progress` rejection that nevertheless consumes one of the 32 page slots and four RPC calls in `observe`. Any admitted project (a durable-agent credential plus one binding, Sepolia gas only, no IMD or sponsor cost) can therefore emit N events from its own pair and push every other admitted project's request back by ceil(N/32) worker ticks; the worker's `bindings` list cannot exclude the pair until an ingest increments `used`, and the attacker simply never lets that happen (second and later events are rejected, `used` stays below `maxRequests`). This is a finite shared-capacity issue among admitted parties rather than an authorization bypass, but it defeats the purpose of the pair filter for the public B window where up to 64 projects share one scanner. Minimal fix: have the worker pass each binding's remaining quota (`maxRequests - used`) and, when `bindings` is supplied, keep at most that many events per binding (or a small constant) before the 32-event slice, so a single pair cannot occupy the page; dropped events are exactly those that would be rejected anyway.","line":71,"path":"adapters/contract-intake.mjs","reproduction":"Inputs: scan({afterBlock: 0, bindings: [{id: A, requester: spammer}, {id: B, requester: victim}]}) where getLogs returns 640 events for the (A, spammer) pair in block 1 and one event for the (B, victim) pair in block 2. Actual: the page holds 32 (A, spammer) events, cursor {afterBlock: 1, afterLogIndex: 31}; the victim's request is first returned on the 21st page (640/32 ticks later), and with 10,000 spam events on the 313th tick (about 5 hours at one tick per minute). Expected: the admitted victim event is present in the first page since binding A can admit at most `maxRequests` events. Node test that fails on this tree: test/scratch/intake-page-starvation.test.mjs (`node --test test/scratch/intake-page-starvation.test.mjs`), built on the mock client of test/contract-intake-scan.test.mjs; it passes with a per-binding cap applied before the 32-event slice.","severity":"low","snippet":"      const remaining = logs.length > 32;\n      if (remaining) logs = logs.slice(0, 32);","title":"An admitted requester/binding pair can still fill the 32-event intake page with its own Requested events, deferring every other admitted project's request"}],"hash":"1fecd385e39cc40a80c2f67017a7ed3baa4869e4c3215216cb761db1c07c942c","nodeId":"449b87ef-42fa-4f8c-81ff-5d24e6372936","outcome":"completed","summary":"The review is complete. The findings file holds two reproduced low findings, no tracked file was changed, and the full suite ran clean on this tree (402 pass, 12 environment skips, 0 fail).\n\n**Verification of the claimed 0.12.1 repairs against the diff from b9603c8**\n\nEvery item in the task's checklist is present in code and I traced each one:\n\n- Single active registration, 16-record lifetime cap, submit-time rebinding checks: `src/chain-deployments/index.mjs` lines 101 to 128, using the project-filtered list that both stores implement.\n- Relayed registration and execution recovery: exact event, canonical receipt, finality, and owner, mandate hash, policy version and delay readbacks. The reverted local attempt stays charged and only the actual successful hash is reported.\n- Identical-byte rebroadcast: same sealed raw bytes, hash re-verified, 60-second spacing, authority re-checked inside the transaction, never on expiry or revocation. No new nonce, fee or reservation.\n- Expired unsent actions terminate: `transact` returns cancelled before any prepare, and a prepared record is cancelled at submit with its reservation returned.\n- Externally delivered callbacks complete against the original resolve receipt. Same-source review retry requires `work === 'cancelled'` and `billing === 'void'`.\n- Expired pilots now hit public decision limits and the durable-agent rule at proposal creation, before gas enrollment. Capacity errors back off 15 minutes. Daily limits wait only inside the request deadline. Concurrent rejections surface via the binding read.\n- The execution recovery lower bound is the evidence block captured at proposal creation, so the earlier \"scan from block 0\" wedge is closed. Ranges are bounded at 10,000 blocks per tick and resumable.\n- Worker role check runs before any claim, missing chain port defers before `beginEffect`, mainnet RPC must be https, release gates require distinct roles, 172800 delay, bounded bootstrap gas, retained target artifact and stored verification identical to the deployment's proof.\n\n**Findings (both low, both in the material diff, both reproduced with scratch tests)**\n\n1. Budget expiry is now a terminal `closed` delivery. `adapters/contract-delivery.mjs:187` returns `closed` when `expiresAt <= now()`, but for resolve that value is the minimum of the Inbox deadline and the dispatcher budget expiry, and for callback it is the budget expiry alone. The worker then skips the request forever. A result that becomes ready after the budget window but before the onchain deadline can never be delivered, even by a separately authorized second allocation. Expected `pending`, as before 0.12.1.\n2. An admitted pair can still fill the 32-event page. `adapters/contract-intake.mjs:71` slices after the pair filter, so one admitted binding emitting N events from its own requester defers every other admitted project by N/32 ticks at Sepolia gas cost only. The scanner cannot exclude the pair until `used` rises, and rejected events never raise it.\n\n**Assessment of B and C**\n\nB (public sponsored reviews and Sepolia readonly intake) is acceptable within its stated bounds. The two findings above degrade liveness and fairness among admitted parties; neither bypasses authorization, spends beyond a ceiling, or loses funds. The 0.00005 test ETH per-transaction ceiling has roughly 190x headroom against the operator's own resolve receipt (0.00000026 ETH) and fails closed above roughly 0.15 gwei. I recommend fixing finding 1 before the window opens because the no-refill policy makes last-day results unrecoverable.\n\nC (Ethereum mainnet noncustodial readonly) has no remaining concrete code defect in the material changes. Every earlier mainnet finding is addressed or explicitly accepted as an operator trust boundary. It remains blocked only by the task's own gate: exact human approval, and the fee reality that 0.001 ETH per transaction and 0.0046 ETH bootstrap bind near 1 gwei for registration and well below that for module deploym","treeHash":null,"usage":{"cachedInputTokens":3299613,"inputTokens":738,"model":"claude-fable-5-1","outputTokens":56583,"runtime":"claude","turns":50,"wallClockMs":952306}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"829d84438fb3fdc8","findings":[{"citation":"resolved","description":"relayedRegistration() returns {state:'submitted',relayed:true} with no `hash` whenever the executor's projects(projectKey) read at 'latest' already shows a non-zero target (a relayer registered the project) but the bounded finalized-range scan has not yet seen the ProjectRegistered event (the registration block is above the finalized head, which on Sepolia and Mainnet is normally ~2 epochs / ~13 minutes behind). deployment.step then returns { state: 'registering', transactionHash: tx.hash } with tx.hash === undefined (line 119). In src/chain-deployments/index.mjs:171 the jobs.finish commit does tx.write(collection, id, { ...old.value, ...result }); canonical() in src/shared/index.mjs:53 rejects an undefined property value with 'invalid_json', so the store transaction rolls back. Because runOne returns the jobs.finish promise without awaiting it inside the try block (src/chain-deployments/index.mjs:168), the rejection bypasses the catch: runOne itself rejects, the hosted chain tick answers 503 worker_unavailable (worker/chain-tick.mjs:13), the worker-health row is not written, and the job stays in state 'running' with effect 'outcome_unknown' and a held lease until the 120 s lease expires. The registration record is never advanced to 'registering'. The cycle repeats every lease expiry until the relayer's block becomes finalized, after which the same code path returns a real hash and recovers. The same undefined-hash value is also produced on the terminal-fallback path at line 117. This is the headline 'exact receipt-based relayed registration recovery' scenario of 0.12.1; it works on the anvil fixture only because anvil's finalized tag equals latest, so the unfinalized window is never exercised there. No funds are at risk; impact is deterministic worker tick failure and a stalled registration job for the whole finality window on every relayed registration. Fix: return a storable waiting result (e.g. omit transactionHash or set it to null / found.complete flag) when no event is found yet, and `await` jobs.finish inside the try blocks (27 occurrences of `return jobs.finish(` across src) so commit failures are handled by the existing catch.","line":71,"path":"adapters/chain-executor.mjs","reproduction":"State: chain-registrations/reg-1 in state 'queued' with message.expiresAt in the future and observedBlock '90'; executor.projects(projectKey) at latest returns a non-zero target; getBlock('finalized').number = 100; getLogs over [90,100] returns [] (the relayer's registerProject was mined in block 105 > finalized). Call executor.deployment.step({kind:'project-register', value, signature, authorize}) -> returns { state:'registering', transactionHash: undefined }. Then call app.deployments.runOne(executor.deployment) with the enqueued project-register job. Expected: job finishes 'waiting' (retryAt +30 s) and chain-registrations/reg-1 shows state 'registering'. Actual: runOne rejects with Error code 'invalid_json' thrown from src/storage/index.mjs write -> canonical (stack: chain-deployments/index.mjs:171 <- jobs/index.mjs:104); chain-registrations/reg-1 stays 'queued'; workflow-jobs row stays state 'running', effect 'outcome_unknown', lease.until = now+120000. Scratch test test/scratch/relayed-registration.scratch.test.mjs (first test) shows this; a minimal stub executor returning { state:'registering', transactionHash: undefined } reproduces the escape through runOne on its own.","severity":"medium","snippet":"    if(!logs.length)return {state:'submitted',relayed:true};","title":"Relayed-but-unfinalized registration returns an undefined transactionHash; the job commit throws invalid_json and escapes runOne's error handling"},{"citation":"resolved","description":"deployment.step calls relayedRegistration(value) before transact() whenever no local 'register:<id>' transaction exists (line 105-107). The expiry guard that terminates an unsent action (transact, line 23: `if (!status && expiresAt <= now()) return { state: 'cancelled' }`) is therefore never reached when relayedRegistration throws. If the onchain project for the same projectKey (keccak(owner, keccak(projectId))) was registered with a different mandateHash — e.g. the owner relayed an earlier signed registration R1 (mandate M1) themselves or after R1's local attempt reverted, then prepared R2 under a new mandate M2 once R1 was no longer active — every tick of R2's job throws 'registered_project_mismatch'. runOne's catch (src/chain-deployments/index.mjs:187) maps it to state 'waiting' with a 60 s retry and never to 'blocked'/'failed', so the job retries every minute indefinitely, long after value.message.expiresAt has passed, performing 5-6 RPC reads each time, and the project has an onchain registration with no chain-bindings record. The requirement 'expired unsent actions terminate' does not hold for this state. Fix: treat a mismatch on an expired registration as terminal (e.g. check expiry before relayedRegistration, or return { state:'blocked', error:'registered_project_mismatch' } instead of throwing).","line":72,"path":"adapters/chain-executor.mjs","reproduction":"State: chain-registrations/reg-1 queued with message.mandateHash = 0xa2..a2 and message.expiresAt = now-10 s (signature window closed); no chain-transactions 'register:reg-1'; executor.projects(projectKey) returns target != 0 with mandateHash 0xa1..a1; getLogs returns one ProjectRegistered log with args.mandateHash 0xa1..a1 for a finalized block. Call executor.deployment.step({kind:'project-register', value, signature, authorize}). Expected: a terminal result ({ state:'blocked', error:'authority_or_window_closed' } as transact would return for any other expired unsent registration). Actual: throws Error code 'registered_project_mismatch'; through runOne the job is re-queued 'waiting' with retryAt now+60000 on every attempt, forever. Scratch test test/scratch/relayed-registration.scratch.test.mjs (second test) prints {\"thrown\":\"registered_project_mismatch\"}.","severity":"low","snippet":"    requireThat(logs.length === 1 && logs[0].args.owner.toLowerCase() === value.message.owner.toLowerCase() &&\n      logs[0].args.mandateHash === value.message.mandateHash, 'registered_project_mismatch');","title":"Expired registration whose project was registered onchain under a different mandate retries forever instead of terminating"},{"citation":"resolved","description":"Not an authorization or accounting defect; a numerical boundary the C decision should be made with. createReadonlyTransactions.prepare (adapters/sepolia-transactions.mjs:63-66) computes gas = estimate*125/100 + 10000 and requires gas * fees.maxFeePerGas <= maxCost, where maxCost is plan.bootstrapMaximumWei (proposed 4.6e15 wei). With the documented ~19.5M-gas executor creation (adapters/sepolia-transactions.mjs:6-7), the gas limit is 24,385,000, so the bootstrap is only preparable when viem's estimateFeesPerGas().maxFeePerGas (baseFee*1.2 + priority) is <= 4.6e15 / 24,385,000 = 188,640,557 wei (0.189 gwei); at the full 30,000,000-gas plan cap the ceiling is 0.153 gwei. The check uses the padded gas limit, not expected usage, so the effective fee ceiling is 25% tighter than 0.0046 ETH / 19.5M gas would suggest. docs/mainnet.md acknowledges waiting or failing closed and says to estimate before funding; the stated figure of what fee level the allocation actually tolerates is not recorded anywhere, and the owner approval file (planHash) must be re-issued for any revised bootstrapMaximumWei. Recommend recording the tolerated fee ceiling alongside the 0.0046 ETH figure in the plan/assessment before approval.","line":44,"path":"integration/mainnet-release.mjs","reproduction":"Inputs: plan.bootstrapMaximumWei='4600000000000000', plan.bootstrapMaximumGas=30000000, client.estimateGas = 19,500,000, client.estimateFeesPerGas = { maxFeePerGas: 200,000,000 wei (0.20 gwei), maxPriorityFeePerGas: 1 }. gas = 19,500,000*125/100 + 10,000 = 24,385,000; gas*maxFeePerGas = 4.877e15 > 4.6e15. Expected per the plan: a 0.0046 ETH bootstrap. Actual: deployMainnetRelease -> txs.prepare throws 'transaction_cost_limit' before any reservation or signing; nothing is spent and no transaction journal entry is created, so the operator retries manually. At maxFeePerGas = 188,000,000 wei the same inputs pass (4.584e15 <= 4.6e15). The deterministic fixture in test/mainnet-release.test.mjs sets anvil base fee to 0x5f5e100 = 0.1 gwei, below the ceiling.","severity":"info","snippet":"    limits: { gas: String(plan.bootstrapMaximumGas), cost: plan.bootstrapMaximumWei },","title":"C bootstrap: the 0.0046 ETH cost cap limits maxFeePerGas to about 0.19 gwei at the ~19.5M-gas executor creation; above that the release fails closed"},{"citation":"resolved","description":"Capacity observation for the B scope, not an authorization bypass: nothing is spent and the per-project/per-tx ceilings are honoured. The public allocation sets maximumTransaction = maximumProject = 5e13 wei (0.00005 test ETH). prepare() requires (estimate*125/100 + 10000) * maxFeePerGas <= 5e13. The Inbox lets a requester choose callbackGas up to MAX_CALLBACK_GAS = 1,000,000 (contracts/src/ClausRequestInbox.sol:28); deliverCallback forwards that much gas, so the estimate is at least callbackGas plus overhead. For callbackGas = 1,000,000 the padded limit is >= 1,260,000 gas and the callback can only be prepared when maxFeePerGas <= 5e13/1,260,000 = 39,682,539 wei (0.040 gwei); for a ~200k-gas callback the ceiling is 0.19 gwei; the resolve itself (~80-120k gas) tolerates 0.31-0.45 gwei. Above those fees the worker records delivery.reason 'transaction_cost_limit' with a 15-minute retryAt (runtime/contract-intake-worker.mjs:45-52) until budget.expiresAt, after which the callback action answers 'closed' (adapters/contract-delivery.mjs:187) and the requester receives only the onchain resolve, never the callback. Because both the per-project and per-transaction ceilings are 0.00005, a project whose resolve actually cost e.g. 3e13 wei has at most 2e13 wei left for its callback regardless of fee level. The docs state these are ceilings, not guarantees; the client-facing docs do not tell a requester which callbackGas values are realistically deliverable under the public allocation. Recommend publishing a recommended maximum callbackGas (or stating that callbacks are best-effort below ~0.2 gwei) with the public B announcement.","line":160,"path":"adapters/contract-delivery.mjs","reproduction":"Inputs: public budget with maximumTransaction='50000000000000', maximumProject='50000000000000'; an observed request with callbackGas=1,000,000; Sepolia estimateFeesPerGas().maxFeePerGas = 100,000,000 wei (0.1 gwei); estimateGas(deliverCallback) >= 1,000,000. prepare(): gas >= 1,260,000; gas*maxFeePerGas >= 1.26e14 > 5e13 -> throws 'transaction_cost_limit'. Expected by a requester reading docs/contract-intake.md: a callback to its contract. Actual: delivery.state 'pending' with reason 'transaction_cost_limit' and retryAt +15 min on every tick while fees stay above 0.0397 gwei; after budget.expiresAt the callback closes unsent. With callbackGas=0 the same request completes with the resolve only.","severity":"info","snippet":"        limits: { gas: budget.maximumGas, feePerGas: budget.maximumFeePerGas, cost: budget.maximumTransaction },","title":"B dispatch: 0.00005 test ETH per transaction makes callbacks with large requester-chosen callbackGas deliverable only at sub-0.04 gwei fees"}],"hash":"278c62db5456f2f3a34982adbab78fb69f1d5e1e197fc3d5107fe619ffd5473e","nodeId":"8ef569a5-9f9e-44ad-9541-9e83c92d6487","outcome":"completed","summary":"Findings file is complete and validated (all four snippets match the tree, JSON parses). Final recap:\n\n**Verdict on scope**\n\n- **B (public sponsored reviews and Sepolia readonly intake):** acceptable to enable from a correctness standpoint. No authorization bypass or accounting defect found in the material changes since b9603c8. The one medium finding below degrades liveness during relayer scenarios but does not spend or misattribute funds.\n- **C (Mainnet noncustodial readonly):** the release guards hold as specified. Distinct roles, fixed 172800 recovery delay, bounded gas, retained target artifact and stored verification proof are all enforced. I recommend fixing the medium finding before activation, because Mainnet's finality lag makes it trigger on every relayed registration. The proposed 0.0046 ETH bootstrap only fits at fees at or below about 0.19 gwei, which the owner should see before approving.\n\n**Findings written to `.imd-findings.json`**\n\n1. **Medium.** A registration that a relayer mined but that is not yet finalized makes `relayedRegistration` return a result with no hash. The job commit then fails on canonical JSON (`invalid_json`), and because `runOne` returns the `jobs.finish` promise without awaiting it, the rejection escapes the catch. The hosted tick answers 503, health is not written, and the job sits in `running` until its lease expires. This repeats for the whole finality window (about 13 minutes on Sepolia and Mainnet). The anvil fixture cannot see it because finalized equals latest there. Reproduced with a scratch test.\n2. **Low.** An expired registration whose project was registered onchain under a different mandate throws `registered_project_mismatch` before the expiry guard runs, so the job retries every 60 seconds forever rather than terminating. Reproduced.\n3. **Info.** C bootstrap fee arithmetic: at the documented 19.5M gas estimate, the padded gas limit is 24,385,000, so the 0.0046 ETH cap tolerates maxFeePerGas of at most 0.189 gwei. Above that the release fails closed with `transaction_cost_limit`.\n4. **Info.** B dispatch: with 0.00005 test ETH per transaction, a requester choosing the maximum 1,000,000 callbackGas can only receive its callback at fees under 0.04 gwei. Ceilings are documented, but requesters are not told which callbackGas values are realistically deliverable.\n\n**Verified as addressed:** single active registration with 16 lifetime records, receipt-based relayed execution recovery (queue does not consume the nonce, so the reconciliation cannot misread a queued approval as cancelled), identical-byte rebroadcast, externally delivered callbacks completing against the original resolve receipt, void-only same-source retry, expired-pilot public limits, binding/requester topic filtering before the page cap, per-tx at most per-project dispatch cap, 15-minute capacity backoff, daily limits waiting only within the deadline, terminal concurrent rejections visible in `latestRejection`, role check before claims, port preflight before `beginEffect`, HTTPS Mainnet RPC, and bounded resumable finalized event scans.\n\n**Limits of this review:** baseline suite ran clean (402 pass, 12 environment skips). I did not run the anvil-backed chain fixtures or reach any RPC. The live Sepolia `chain` capability record's `executorPayer` field could not be inspected; the new per-tick role check depends on it, so confirm it is present before deploying the executor worker.","treeHash":null,"usage":{"cachedInputTokens":3434855,"inputTokens":802,"model":"claude-fable-5-1","outputTokens":58373,"runtime":"claude","turns":59,"wallClockMs":993124}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"52c98c0dc01791cd","findings":[{"citation":"resolved","description":"relayedRegistration() (lines 62-81) returns {state:'submitted',relayed:true} with no hash whenever projects[projectKey].target is already non-zero but the bounded finalized scan has not yet reached the block that holds the ProjectRegistered event (recoverChainEvent returns complete:true,logs:[] when start > finalized head, line 16 of chain-event-recovery.mjs, and complete:false otherwise). Line 119 then returns {state:'registering', transactionHash: undefined}. src/chain-deployments/index.mjs:171 spreads that result into the registration row and canonical() rejects an undefined property with invalid_json (src/shared/index.mjs:53). Because runOne does `return jobs.finish(...)` inside its try block without await (src/chain-deployments/index.mjs:168), the rejected promise is not caught by the catch at line 179; runOne rejects, runtime/chain-worker.mjs tick() rejects, worker-health is not written, and the job stays leased for its 120 s lease and then fails identically on every reclaim. This is exactly the advertised recovery path (owner relays the signed registration, worker recognises it from the exact receipt): on Sepolia and Mainnet the finalized tag lags the head by roughly 13 minutes, so every relayed registration produces a crash loop of the executor tick for that whole window, and the state described in the sibling finding (event never found) makes the loop permanent. The repository's own fixture reproduces it: with anvil --chain-id 11155111 and CLAUS_TEST_RELAY=1, test/chain-integration.test.mjs fails with invalid_json at src/chain-deployments/index.mjs:171 (it passes with --chain-id 1 only because that fixture mines 96 blocks per advance so the event is already finalized). Fix: never return an undefined transactionHash (omit the key or use tx.hash ?? null) and `return await jobs.finish(...)` inside the try so finish/commit failures reach the catch and become a bounded 'waiting' retry.","line":119,"path":"adapters/chain-executor.mjs","reproduction":"State: network-capabilities 'chain' enabled for executor E, executorPayer = worker signer; review-pilots p active; chain-modules 'module' verified; a queued project-register job for chain-registrations 'reg' with observedBlock '100' and message.expiresAt = now+3600; no 'chain-transactions' row 'register:reg'. Chain: executor.projects(K) = [owner, target non-zero, mandateHash, policyVersion, 0, ...] (owner-relayed registerProject mined at block 101), latest block 103, finalized block 98. Call deployments.runOne(executor.deployment). Expected: {state:'waiting'} and chain-registrations 'reg' state 'registering', recovered once the event is finalized. Actual: runOne rejects with code 'invalid_json'; the registration row is unchanged and the job remains leased. Deterministic unit reproduction: test/scratch/relayed-prefinality-crash.test.mjs (node --test) fails with error 'invalid_json'. Fixture reproduction: `anvil --chain-id 11155111 --port 8546 & CLAUS_TEST_CHAIN=http://127.0.0.1:8546 CLAUS_TEST_CHAIN_ID=11155111 CLAUS_TEST_RELAY=1 node --test test/chain-integration.test.mjs` fails with invalid_json from src/chain-deployments/index.mjs:171.","severity":"medium","snippet":"      if (tx.state !== 'confirmed') return { state: 'registering', transactionHash: tx.hash };","title":"Relayer-mined registration that is not yet finalized returns transactionHash undefined; persisting it throws invalid_json and the un-awaited jobs.finish rejection escapes runOne, so the executor tick "},{"citation":"resolved","description":"relayedRegistration() runs before transact() whenever no local 'register:<id>' transaction exists. When projects[projectKey].target is non-zero it scans ProjectRegistered logs only from value.observedBlock (the block observed when THIS registration was prepared, adapters/chain-read.mjs:99) and, when the complete finalized history from that block holds no event, still returns {state:'submitted'} regardless of found.complete and regardless of value.message.expiresAt. transact() (the only place where an unsent action's window `expiresAt <= now()` becomes 'cancelled'/'blocked') is never reached on this path, so the registration never terminates even days after its signing window closed. Since prepareRegistration() treats any 'queued'/'registering' row as active regardless of expiry (src/chain-deployments/index.mjs:102-110), the project can never prepare a registration with other parameters, and no chain-binding is written, so the project is permanently locked out of proposals. The state needs no attacker: registration A (same owner, same mandate) was signed and mined onchain (owner-relayed, or broadcast by the executor and then cancelled locally via job.cancel) but left no chain-binding; the owner prepares registration B later (new expiresAt => new nonce and id, observedBlock after A's block) and submits it. A mismatching event (different mandateHash) is likewise non-terminal: registered_project_mismatch is not in runOne's blocked list, so it retries every 60 s forever. Combined with the sibling finding, the symptom is an invalid_json crash of the executor tick at every claim of that job, permanently. Fix: on the relayed path, when found.complete is true and no matching event exists (or it mismatches), return a terminal state such as {state:'blocked', error:'project_already_registered'}, and apply the same `value.message.expiresAt*1000 <= now()` cut-off that transact() applies; alternatively scan from the module receipt block instead of observedBlock so an earlier registration by the same owner/mandate is recovered.","line":71,"path":"adapters/chain-executor.mjs","reproduction":"State: network-capabilities 'chain' enabled for executor E with executorPayer = worker signer; chain-modules 'module' verified with receipt.blockNumber 10; registration value {id:'reg-b', observedBlock:'100', message:{projectId:K, owner:O, mandateHash:M, policyVersion:V, minDelaySeconds:0, expiresAt: now+3600}}; no 'chain-transactions' row 'register:reg-b'. Chain: executor.projects(K) = [O, T non-zero, M, V, 0, ...] (registered at block 50 by an earlier owner-signed registration), finalized head 190, latest 200, getLogs(ProjectRegistered, projectId K, fromBlock >= 100) returns []. Call executor.deployment.step({kind:'project-register', value, signature, authorize}) -> {state:'registering', transactionHash: undefined}. Advance the clock 10 days (signing window long expired) and call step again. Expected: a terminal state (blocked/failed) and the project free to prepare another registration. Actual: {state:'registering'} again, no 'register:reg-b' transaction exists, and deployments.prepareRegistration for the same project with another module throws registration_in_progress. Scratch test test/scratch/relayed-registration-stuck.test.mjs (node --test) fails on the current code with 'expected a terminal state, got {\"state\":\"registering\"}'.","severity":"medium","snippet":"    if(!logs.length)return {state:'submitted',relayed:true};","title":"Relayed-registration recovery has no terminal outcome: a project already registered onchain before observedBlock is never blocked, never expires, and blocks every later registration"},{"citation":"resolved","description":"recoverChainEvent stores {nextBlock, blockNumber, blockHash} after each empty range and, on the next call, requires the block at blockNumber to still carry blockHash. If it does not (the RPC endpoint was switched to a node serving a different view of the chain, an RPC returned a bad block earlier, or a finality incident reorganised the cursor block), it throws recovery_block_changed every time, never rewinds to fromBlock and never advances. The error is neither in authorityDenied nor in runOne/runExecution's blocked lists, so the job is finished as 'waiting' and retried every 60 s indefinitely; the registration or execution it belongs to never reaches a terminal state and the only recourse is manual deletion of the 'chain-event-recovery' row, which contradicts the stated rule that journals are never cleared. Expected: on a hash mismatch, discard the cursor and restart the scan from fromBlock (the previous 'no event in range' conclusion is exactly what a changed block invalidates), or surface it as a terminal blocked state. Operational impact only; no spend or authority is affected.","line":8,"path":"adapters/chain-event-recovery.mjs","reproduction":"State: 'chain-event-recovery' row for key K with {nextBlock:'10500', blockNumber:'10499', blockHash:H}. Chain: client.getBlock({blockNumber:10499n}).hash !== H (any differing hash), finalized head 25000. Call recoverChainEvent({store, client, chainId:1, key:K, query, fromBlock:500n}) three times. Expected: the scan restarts from fromBlock (or returns a terminal result). Actual: every call throws code 'recovery_block_changed' and the stored row still has blockNumber '10499'. Scratch test test/scratch/recovery-cursor-wedge.test.mjs (node --test) passes on the current code, demonstrating the persistent wedge.","severity":"low","snippet":"  if(old) requireThat((await client.getBlock({blockNumber:BigInt(old.value.blockNumber)})).hash===old.value.blockHash,'recovery_block_changed');","title":"A recovery cursor whose finalized block hash no longer matches wedges event recovery permanently: the cursor is never reset and the error is retried forever"}],"hash":"4cc44c9b5083b1eb8c5d154b936a246a425cedd05f8a4f30bb6a14e015976647","nodeId":"d79d8788-c741-4c1f-bf94-ea5ee7fb9967","outcome":"completed","summary":"Review complete. The findings file holds three substantiated defects; everything else on the checklist traced clean.\n\n**What I verified**\n\n- Full Node suite passes on this tree (402 pass, 12 skipped). Chain-1 local anvil fixture with relay passes.\n- Checked each named item against the diff from b9603c8: single active registration and the 16-record cap, receipt-based relayed registration and execution recovery, same-byte rebroadcast under the original nonce and fees, expired unsent delivery actions closing, external callback completion against the original resolve receipt, void-only review retry, indexed intake topic filter plus exact pair check before the 32-event page, per-transaction ≤ per-project dispatch cap, 15-minute backoff on capacity, day-limit waits bounded by the request deadline, rejection visible on the binding read, worker role check before claims, missing port deferral before beginEffect, HTTPS Mainnet RPC, bounded resumable finalized log scans, and the Mainnet staging guards for distinct roles, the fixed 172800 delay, explicit gas cap, retained target artifact and stored verification proof. All hold as described, with the exceptions below.\n\n**Findings** (all in the relayed-registration recovery path, no authorization bypass, no spend)\n\n1. **Medium, adapters/chain-executor.mjs:119.** A relayer-mined registration that is not yet in the finalized range makes the step return an undefined transaction hash. Persisting it throws `invalid_json`, and because runOne returns the finish promise without awaiting it, the rejection escapes the catch and the executor tick crashes on every reclaim until finality. The repository's own fixture reproduces this: the Sepolia-chain-id anvil run with `CLAUS_TEST_RELAY=1` fails, so the release note's claim that the Sepolia fixture passed does not cover the relay variant.\n2. **Medium, adapters/chain-executor.mjs:71.** When the project is already registered onchain but the event precedes `observedBlock`, or carries a different mandate, the registration never reaches a terminal state and the signing-window expiry is never applied. The stuck `registering` row blocks every later registration for that project.\n3. **Low, adapters/chain-event-recovery.mjs:8.** A cursor whose block hash no longer matches throws forever and is never reset.\n\nScratch reproductions are under `test/scratch/` and are described in each finding's reproduction field.\n\n**Scope verdicts**\n\n- **B (public reviews and Sepolia readonly intake):** blocked on findings 1 and 2. Both are small fixes, but finding 1 crashes the executor tick for roughly 13 minutes per owner-relayed registration, which is the recovery path this release advertises. The review-sponsorship, intake, delivery and admission-limit changes themselves are acceptable within the stated finite-capacity and operator-trust assumptions.\n- **C (Mainnet readonly):** the release guards and worker checks are correct as specified and distinguish operator trust from bypass. C uses the same executor code path, so it should wait for the same fix. It otherwise remains correctly unauthorized pending the exact human approval.\n\nNo sources were modified, nothing was deployed, signed, posted or spent.","treeHash":null,"usage":{"cachedInputTokens":2963823,"inputTokens":738,"model":"claude-fable-5-1","outputTokens":52884,"runtime":"claude","turns":44,"wallClockMs":864872}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"4dd67dae195771b6","findings":[{"citation":"resolved","description":"relayedRegistration() is entered whenever projects[projectKey].target is non-zero on chain (line 63-64). It then scans only finalized ranges from value.observedBlock, which chain-read records from the *latest* block at preparation time. Whenever the ProjectRegistered event is not inside [observedBlock, finalized] the function returns {state:'submitted', relayed:true} with no hash, and deployment.step turns that into {state:'registering', transactionHash: undefined} (line 119). That is the normal state for roughly 13-15 minutes after any relayed registerProject on Sepolia or Mainnet (finality lag), and it is the permanent state when the on-chain registration precedes observedBlock (owner signed registration A, relayed it directly, let A expire locally, then prepared/submitted B). In src/chain-deployments/index.mjs:171 the job commit writes {...old.value, ...result} and canonical() rejects the undefined value with invalid_json. The rejection is not caught: runOne does `return jobs.finish(claim, ..., commit)` inside its try block without `await` (src/chain-deployments/index.mjs:168), so the promise rejection bypasses the catch that would have produced a 60 s waiting retry. The error propagates out of runOne, chain-worker tick() throws, worker/chain-tick.mjs answers 503 worker_unavailable, worker-health is not written and app.approvals.runExecution is skipped for that tick. The job stays `running` under its 120 s lease with no retryAt and is re-claimed (and throws again) every lease expiry. The registration record keeps state 'queued' and shows nothing. The shipped relay fixture only passes with CLAUS_TEST_CHAIN_ID=1 because the chain-1 fixture mines 0x60 blocks per advance; with the default Sepolia chain id the same fixture (CLAUS_TEST_RELAY=1 against a local anvil) fails with code 'invalid_json'. Impact for B: a mempool front-runner who relays the executor's own registerProject calldata makes the local attempt revert (gas charged to the public pool) and then puts the executor role into this throwing loop for the finality window; for the pre-observedBlock variant the executor throws on this job forever and the project can neither bind nor prepare another registration (registration_in_progress) nor cancel the job (effect already started). This is a liveness/robustness defect, not an authorization bypass. Fix: never return a result containing undefined (return {state:'submitted', relayed:true, hash:null} and build {state:'registering', ...(tx.hash ? {transactionHash: tx.hash} : {})}); in runOne/runDecision/runExecution/intake.run change `return jobs.finish(...)` to `return await jobs.finish(...)` so commit failures reach the catch and defer; and when the recovery scan is complete with no event while the project is registered, fall back to scanning from the module/executor creation block or return a terminal registered_project_mismatch instead of an endless 'registering'.","line":71,"path":"adapters/chain-executor.mjs","reproduction":"Mock reproduction (no chain): test/scratch/relayed-registration-hashless.test.mjs. State: network-capabilities 'chain' with executorPayer=signer, an active review-pilot, chain-registrations 'reg-1' with observedBlock:'150' and a queued project-register job; client mock: readContract('projects') returns a non-zero target with matching owner/mandateHash/policyVersion, getBlock({blockTag:'finalized'}) returns number 100n, getBlockNumber 200n, getLogs returns []. Expected: deployment.step defers with a serialisable result, or runOne finishes the job as 'waiting' with a retryAt. Actual: deployment.step returns {state:'registering', transactionHash: undefined}; canonical(result) throws code 'invalid_json'; createChainDeployments(...).runOne(executor) rejects with code 'invalid_json'; the workflow job remains state 'running' with lease.until in the future and result null; a second jobs.claim returns idle. Real-chain reproduction: `anvil --chain-id 11155111 --port 18547`, then `CLAUS_TEST_CHAIN=http://127.0.0.1:18547 CLAUS_TEST_RELAY=1 node --test test/chain-integration.test.mjs` fails: `not ok 1 ... error: 'invalid_json'` with stack src/storage/index.mjs:70 -> src/chain-deployments/index.mjs:171 -> src/jobs/index.mjs:104; the same command with CLAUS_TEST_CHAIN_ID=1 against `anvil --chain-id 1` passes.","severity":"medium","snippet":"    if(!logs.length)return {state:'submitted',relayed:true};","title":"Relayed registration not yet finalized returns a hash-less result; the job commit rejects it as invalid_json and, because runOne returns jobs.finish without awaiting, the executor tick throws instead "},{"citation":"resolved","description":"The new external-callback handling (lines 193-199) only accepts the external delivery when chain.deliveryState(request,'finalized') already shows resolved && callbackDelivered. If our own deliverCallback transaction was mined and reverted (InvalidResult because r.callbackDelivered was already true) or was cancelled at submit time because observedForSubmit saw callbackDelivered at latest, and the external delivery is still inside the finality window (about 13-15 minutes on Sepolia/Mainnet), the step returns answer('failed') or answer('closed'). runtime/contract-intake-worker.mjs:227 treats delivered/closed/failed as terminal and skips the row forever, so the request's delivery record permanently says failed/closed although the Inbox callback was delivered and the resolution receipt exists. The !transaction branch (line 180-184) handles the same race correctly by answering 'pending'. No funds are lost and no extra transaction is sent; it is a state-accuracy/visibility defect for the project reading contract_request_get. Fix: in the cancelled/reverted branch for action==='callback', when the latest state shows callbackDelivered but the finalized state does not yet, return answer('pending') so the next tick re-evaluates, mirroring line 182-184.","line":198,"path":"adapters/contract-delivery.mjs","reproduction":"test/scratch/external-callback-race.test.mjs: fixture as in test/contract-delivery.test.mjs but deliveryState is blockTag-aware (latest vs finalized). Resolve broadcasts and confirms. During the callback broadcast the mock marks callbackDelivered=true at latest only (external delivery landed first) and returns a receipt with status 'reverted' for our callback. Expected: step answers 'pending' (external delivery observed, awaiting finality) and later 'delivered'. Actual: first step answers state 'failed' (two broadcasts, gas charged); after the mock marks callbackDelivered at finalized, an explicit second step with the identical inputs answers 'delivered', proving the earlier terminal answer was premature; the production worker never issues that second step because delivery.state 'failed' is terminal.","severity":"low","snippet":"        return answer(transaction.state === 'cancelled' ? 'closed' : 'failed');","title":"A callback reverted or cancelled because someone else delivered it first is answered 'failed'/'closed' when the external delivery is visible at latest but not yet finalized, and the worker never re-ch"},{"citation":"resolved","description":"The bound counts every chain-registrations row of the project, including intents that expired after their one-hour signing window, were blocked or failed. Each prepareRegistration call after the previous intent expired creates a new row because the prepared digest includes expiresAt and observedBlock. An owner who prepares and does not sign within an hour sixteen times (or accumulates sixteen blocked/failed attempts) can never prepare again: registration_limit is returned forever, there is no prune, no owner-side cancel for awaiting_signature rows, and no operator operation documented to clear them. This matches the documented intent of finite lifetime records and is reachable only by the project owner, so it is reported as an intentionally finite capacity with an irrecoverable consequence rather than a bypass. Suggested fix: count only rows that are active (queued/registering/verifying or unexpired awaiting_signature) plus a bounded number of terminal rows, or let the owner cancel/expire awaiting_signature intents.","line":111,"path":"src/chain-deployments/index.mjs","reproduction":"test/scratch/registration-limit-lockout.test.mjs: pilot project with a verified module and valid mandate; call prepareRegistration with the same {moduleId, mandateId, configuration:'0x01', minDelaySeconds:0} sixteen times, advancing the clock 3600001 ms between calls so each intent expires unsigned (sixteen distinct ids). Expected: a later preparation for an unbound project is accepted. Actual: the seventeenth call rejects with code 'registration_limit', and still rejects 30 days later; chain-bindings for the project is null.","severity":"info","snippet":"        requireThat(registrations.length < 16, 'registration_limit');","title":"Sixteen expired unsigned registration preparations permanently lock a project out of chain registration; nothing prunes or counts only live intents"}],"hash":"f394500c9fab62f6b31dd3a22c90840be2ec9989f17e553237e73015e9b72800","nodeId":"6438c3db-697d-4f12-9559-ca01e0bde00e","outcome":"completed","summary":"The review is complete. The findings file is written and validated, no tracked sources were changed, and the scratch reproductions all run.\n\n**Outcome**\n\nThree substantiated findings are in `.imd-findings.json`. The headline one is a real liveness defect introduced by the 0.12.1 relayed-registration recovery, reproduced both with mocks and against a local anvil.\n\n- **Medium, adapters/chain-executor.mjs:71.** When a project is already registered on chain but its ProjectRegistered event is not inside the finalized range scanned from `observedBlock`, the relayed path returns a hash-less result. The job commit rejects `transactionHash: undefined` as invalid JSON, and because `runOne` returns `jobs.finish(...)` without awaiting it, the rejection escapes the catch. The executor tick throws (503 from the cron handler), execution jobs are skipped for that tick, and the job is re-claimed and throws again at every 120-second lease expiry. On Sepolia this lasts the finality window after any relayed or front-run registration. It is permanent when the on-chain registration precedes `observedBlock`, leaving the project unable to bind, re-prepare or cancel. The shipped relay fixture passes only on chain 1 because that fixture mines 96 blocks per advance; with the Sepolia chain id it fails with `invalid_json`.\n- **Low, adapters/contract-delivery.mjs:198.** A callback that reverts or is cancelled because a third party delivered it first is answered `failed` or `closed` when the external delivery is at latest but not yet finalized. The intake worker treats that as terminal, so the request permanently shows a failed delivery despite a delivered callback. No funds or extra transactions are involved.\n- **Info, src/chain-deployments/index.mjs:111.** Sixteen expired unsigned preparations lock a project out of registration forever. This is the documented finite-records design, reported as an irrecoverable consequence rather than a bypass.\n\n**Verification of the prior-job items**\n\nEvery concrete item from the 0.11.3 and 0.12.0 reports is addressed in the diff: expired-pilot limits and durable-agent rule, RPC topic filtering with pair checks before the page cap, `maximumTransaction <= maximumProject`, void-review re-request, daily-limit waiting within the deadline, `latestRejection` visibility, 15-minute capacity backoff, expiry termination of unsent actions, identical-byte rebroadcast, bounded resumable finalized event recovery, worker role check before claims, preflight before `beginEffect`, HTTPS mainnet RPC, distinct roles, fixed 172800 recovery delay, bounded bootstrap gas, retained target artifact and stored verification proof. The full suite passes (402 of 414, 12 environment skips), and the local-chain fixtures pass for Sepolia plain, Sepolia intake, chain-1 relay and the guarded chain-1 release.\n\n**Assessment**\n\n- **B (public Sepolia reviews and readonly intake):** blocked on the medium finding only. A mempool front-runner can turn the executor role into a repeating throw for the finality window at the cost of one reverted registration from the public gas pool, and a self-relayed earlier registration wedges a project permanently. The fix is small (serialisable result, `return await jobs.finish`, and a fallback scan or terminal mismatch when the complete scan finds nothing). Everything else in B's scope is operator trust or intentionally finite capacity, not an authorization bypass.\n- **C (Mainnet noncustodial readonly):** the release gates now enforce what was promised, and the chain-1 fixtures pass. The same relayed-registration defect applies to Mainnet, where finality lag is the same, so C should carry the same fix before the human approval. The unsigned owner-approval record, shared decision pool, and fee-ceiling behaviour remain documented trust boundaries and are not reported as defects.\n\nScratch reproductions live under `test/scratch/` and are run with `node --test test/scratch/<file>`.","treeHash":null,"usage":{"cachedInputTokens":9710215,"inputTokens":1634,"model":"claude-fable-5-1","outputTokens":83968,"runtime":"claude","turns":52,"wallClockMs":1324486}}],"verification":[]}