{"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":"e340d6dd-adb4-44ef-b4de-fea90abf2620","kind":"audit","nodes":[{"acceptedSubmissionHash":"6bcae4afc0bea5867215d359e6c4a9ef2b703491a6305503a7e2f9088775f21a","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":"25cfc873687adfe2418a408f53d0d9a29a898c7e5fcaee97f6c3577adabed9a3","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":"766b07181fb06c65cc9ddfe5621c2db66e24bf7e687f2f49414603e3213566f3","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":"9d0589a2d089920f9693e60de8853c07f87b0ff8daf468edd0b8abd725b528f0","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":"6b9539abba0497d6a8021ee722513d264dd52bfe9f86d16f458799c819008ae7","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 only the 0.12.3 delta against baseline 1f6f6d7ebb30dab9e14edda5a34da20f74e565e8 in this repository. Reuse the completed prior reports, especially job d80d33c1-afdd-48fc-96bc-88570c9d7a48 at https://api.imd.fun/jobs/d80d33c1-afdd-48fc-96bc-88570c9d7a48/submissions and report.md. Its findings 1 and 2 block public B and prepared C; 3 through 7 concern recovery and delivery liveness. Report remaining material defects honestly and distinguish them from already accepted bounded limits. Do not re-audit unrelated unchanged code.\n\nB means finite sponsored reviews and Sepolia noncustodial readonly projects, true Claus decisions and the bounded Inbox/callback flow. C means preparation of the same readonly mainnet profile only, requiring a separate exact human approval. No public B or C activation has occurred. No Solidity, authority, financial ceilings, custody, inference, token launch or signing algorithm changed.\n\nFinding 1: direct relays are decoded and re-encoded before comparison, so ABI-equivalent trailing bytes are accepted while changed signed arguments are rejected. Both real local EVM integrations now mine the padded calldata before the worker recovers it. Nested contract relays remain explicitly unsupported.\nFinding 2: all post-registration project/target state reads use the receipt block number; a missing/mismatched state response there uses retryable registration_state_unavailable, separate from definitive relay conflicts. The original job remains recoverable.\nFindings 3/4: failed sponsored delivery is distinct from a closed onchain request. Expired subsidy with no unsent transaction returns delivery failed / dispatch_subsidy_expired. A cancelled unsent transaction returns delivery failed / dispatch_cancelled unless actual resolve deadline or chain state proves closure. The request result and original transaction history remain intact; no replacement spend or rebinding to another budget. An external callback seen but not final still waits for finality. Worker persists those bounded reason codes, stops failed attempts and backs off ordinary pending delivery by one minute. Users can still read a finalized canonical result or externally deliver its callback; a stopped sponsorship is explicitly not promised automatic recovery.\nFinding 5: registration event recovery uses a new cursor namespace while retaining old journals. Before committing an empty page it checks finalized project state at the page end and before its start. A registration transition in an allegedly empty page returns retryable recovery_logs_inconsistent without advancing the cursor. A project that already existed before the eligible module history remains a conflict. No cursor is deleted.\nFinding 6: an unsent expired registration is not terminal merely from local time when finalized chain time is still before its expiry; recovery waits without signing another transaction.\nFinding 7: late submission of an expired prepared intent is allowed to queue original-signature recovery. Current API authority and one-active-intent limits still apply; the worker cannot freshly broadcast the expired signature.\n\nRegressions exercise all these cases. Full source gate: 419 pass, 10 optional environment skips. Real local chain1 and Sepolia padded-relay flows pass. Prior 32-event shared scan fairness, 16 lifetime intents, fee ceilings and small callback budget are unchanged and documented; the previous judge accepted these as non-blocking limits. Please provide separate B and C scope conclusions for this final bounded candidate, with any remaining findings. Test evidence is operator evidence, never a claim of independent acceptance. Nothing in a successful assessment authorizes mainnet activation.\n","parentJobId":null,"planHash":"9a31d3491a713f385352aab904c6298df43cfe4a6e27b64052b0850f07f48fce","previousHash":"0000000000000000000000000000000000000000000000000000000000000000","projectId":"e340d6dd-adb4-44ef-b4de-fea90abf2620","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":"52667","feedbackHash":"df9f147e6da6d9e10ce776a7cbce0bf3872ccf78839cda1a233232555244f658","nodeKey":"audit_economics","submissionHash":"6bcae4afc0bea5867215d359e6c4a9ef2b703491a6305503a7e2f9088775f21a","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51040","feedbackHash":"be52e9fb66f894701ee5b468b0f1855948bc322d5eaf6348cc81371169fc95ac","nodeKey":"audit_flow","submissionHash":"25cfc873687adfe2418a408f53d0d9a29a898c7e5fcaee97f6c3577adabed9a3","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52337","feedbackHash":"44e7337d80074fe894618064929d6465782f3d8fa0f4bf96c9e45dc598268b27","nodeKey":"audit_judge","submissionHash":"766b07181fb06c65cc9ddfe5621c2db66e24bf7e687f2f49414603e3213566f3","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52478","feedbackHash":"eeb2e599239d292eb2317744ff1d354c8bd2363428561742f7a51c737475f5ea","nodeKey":"audit_math","submissionHash":"9d0589a2d089920f9693e60de8853c07f87b0ff8daf468edd0b8abd725b528f0","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51162","feedbackHash":"d2f3e3ed077ee70f6aaa0abb8dd368bacb97e2d88d49289e3bbb1746aeb951fc","nodeKey":"audit_permissions","submissionHash":"6b9539abba0497d6a8021ee722513d264dd52bfe9f86d16f458799c819008ae7","tag1":"review:submission","tag2":"acceptance-v2","value":1}],"site":null,"snapshotHash":"fb0cfe9713baa8efae17de6c86e0a60a19c38462746eda000a54c9483c10bb02","state":"completed","submissions":[{"artifacts":[],"attempt":1,"bundleHash":null,"device":"df74f6c887684f20","findings":[{"citation":"resolved","description":"relayedRegistration is the only path that adopts a registration Claus did not broadcast itself: the honest direct relay, the late submission of an expired intent (Finding 7), and the padded front-run that Finding 1 was fixed for (Claus's own transaction reverts, the terminal branch at line 152 falls into this scan). It scans finalized eth_getLogs pages of 10,000 blocks starting at the module's receipt block, and 0.12.3's validateEmpty additionally reads projects() state at the end of every empty page. The production Sepolia worker is hard-wired to https://ethereum-sepolia-rpc.publicnode.com (runtime/chain-worker.mjs:28). I probed that endpoint on 12 October 2026 at head 11,900,367: eth_call and eth_getCode at head-9,000 succeed, at head-11,000 and deeper they return {code:4444, message:'pruned history unavailable: requested N, earliest available 11,890,282'}; eth_getLogs for head-9,900..head-100 succeeds and every range starting deeper than ~10,000 blocks returns the same 4444 error, while block headers are still served. The deployed executor and modules in docs/deployments.json were created at 11,896,893-11,898,105, i.e. only ~3,500 blocks before the probe, so the real local anvil runs and the current testnet state do not exercise this; from roughly a day after a module's deployment every recovery scan for it starts inside pruned history. The first getLogs page then throws a code-less error, runOne maps it to chain_execution_unavailable and retries every 60 seconds with no bound: the cursor is never written, recovery_logs_inconsistent and the finalized-absence rule are never reached, and the job is neither complete nor blocked. The project is registered on chain but never gets a chain-bindings row; the registration row stays 'registering', which keeps registration_in_progress raised for any new intent, and after a job cancel any new intent reverts on the already-registered projectId and re-enters the same stall. For an attacker this reproduces the original Finding 1 outcome (a project permanently unbindable for the price of one front-run registration) whenever the victim's module is more than ~33 hours old, which is the ordinary case for a reviewed and verified module. The fix for Findings 1, 5 and 7 therefore holds only on an archive endpoint, which neither the code nor docs/contract-intake.md:112 ('bounded, resumable finalized event ranges') state as a requirement. The scan does not need the module block: the signed intent cannot have been relayed before observedBlock (prepare reads latest, the owner signs afterwards), so starting at observedBlock keeps the scan within retention for any intent recovered within ~33 hours of preparation, and the 'registered before this intent' conflict can be detected with one pinned projects() read at observedBlock (non-zero target there means a pre-existing registration, terminal registered_project_mismatch). Alternatively, or in addition, configure an archive-capable RPC for the chain worker and document the retention requirement; the same applies to the operator-supplied CLAUS_MAINNET_RPC_URL for profile C.","line":72,"path":"adapters/chain-executor.mjs","reproduction":"State: Sepolia worker on the configured public RPC; chain-modules receipt.blockNumber = head-30,000 (a module deployed ~4 days ago); a registration intent prepared at head-200 and relayed directly by the owner (or padded and front-run by anyone) and mined at head-100 (ProjectRegistered emitted, projects(projectId).target set). Run project-register ticks. Expected: three empty-page ticks walk from the module block to the relay, the fourth adopts it and returns verified with a binding. Actual: every tick throws 'pruned history unavailable' from the first eth_getLogs page (fromBlock = head-30,000), no chain-event-recovery cursor is ever written, runOne records waiting/chain_execution_unavailable and retries every 60 s indefinitely. Reproduced with test/scratch/pruned-rpc-recovery.test.mjs, whose mock client answers eth_getLogs/eth_call/eth_getCode below head-10,000 exactly as the live endpoint does; after six ticks it reports state 'error', 0 cursors written, 6 pruned-history reads. Live evidence: curl eth_getLogs {fromBlock: head-11000, toBlock: head-1001} and eth_call at head-11000 against https://ethereum-sepolia-rpc.publicnode.com both return error 4444 'pruned history unavailable'.","severity":"medium","snippet":"      fromBlock:deployed < observed ? deployed : observed,","title":"Relayed-registration recovery scans from the module block through an RPC that serves only ~10,000 blocks of logs and state, so recovery for any module older than ~33 hours waits forever and the Findin"},{"citation":"resolved","description":"0.12.3 pins every post-registration project/target state read to the receipt block (registeredRead at lines 158-167) and makes a missing or mismatched answer the retryable registration_state_unavailable. The one remaining post-registration chain read, the target's code for binding.codeHash, is still taken at latest with no block pin and no non-empty check. The same lagging-backend answer that Finding 2 was fixed for (a load-balanced public RPC node that has not yet applied the registration block) returns 0x for the just-created target. keccak256('0x') = 0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470 is then written into the chain-bindings row, which src/chain-deployments/index.mjs:176 writes exactly once and never corrects. The step still returns verified, so the job completes and the wrong hash is permanent. The value is exported through binding() and portability and is part of the binding digest that src/vaults/index.mjs:19 compares against the operator's vault policy admission (admission.bindingHash === digest(value)), so a later vault admission would be keyed to a hash of empty code. No spend, authority or custody effect; the claim 'all post-registration state reads use the receipt block number' is simply not complete. Fix: read getCode with blockNumber: tx.receipt.blockNumber (or the same registeredRead pin) and requireThat the code is non-empty, throwing registration_state_unavailable otherwise so the job retries instead of persisting.","line":175,"path":"adapters/chain-executor.mjs","reproduction":"State: a project-register job whose transaction (own or relayed) is confirmed at block 101; the RPC answers every pinned read correctly (projects at block 101 returns the registered project, target.executor()/projectId() match) and only getCode({address: target}) with no blockNumber returns '0x'. Run executor.deployment.step. Expected: either the binding records the real target code hash or the step throws a retryable error. Actual: the step returns {state:'verified', binding:{codeHash:'0xc5d2...a470'}} and runOne persists that binding permanently. Reproduced with test/scratch/lagging-getcode.test.mjs (mock client built from test/registration-recovery.test.mjs; only the latest getCode for the target answers '0x'), which fails on this tree with 'binding persisted with the hash of empty code'.","severity":"low","snippet":"          owner: value.message.owner, target, codeHash: keccak256(await client.getCode({ address: target })),","title":"Target codeHash in the write-once verified binding is still read at latest, not at the receipt block"},{"citation":"resolved","description":"This is the cancelled-unsent edge from prior findings 3/4, now labelled honestly. It is reported here as an accepted bounded limit, not a blocking defect: the one-attempt-per-request rule (line 47 comment) and docs/contract-intake.md ('cancellation of a signed but unsent delivery ends that sponsored attempt as failed') cover it. What the documentation does not say is that the cancellation is not limited to subsidy expiry or lost authority. sepolia-transactions.submit cancels a prepared row whenever the authorize callback returns false, and the callback at line 166 returns false for !allowSubmit, i.e. whenever the intake host ticks while enabled() is false or throws an admission code (runtime/contract-intake-host.mjs passes enabled: () => ports.verifyAdmission(store); a disabled contract-intake capability during a release rollover is enough). The worker then persists delivery.state 'failed' and line 29 never revisits the row, so a request whose Inbox deadline, binding and budget are all still open loses its only sponsored attempt because of an operator pause that happened to coincide with a crash between prepare and submit. The cancellation itself is correct (never broadcast while unauthorized); only the terminal label is a choice. If the requester wants pauses to be survivable, the minimal change is to leave the prepared row untouched (answer pending with retry) when allowSubmit is false and the intent is unexpired, and cancel only on expiry or an authority denial from active().","line":203,"path":"adapters/contract-delivery.mjs","reproduction":"State: budget valid for 24h, request result_ready with deadline +1h, binding active. Tick 1: step with allowSubmit:true; prepare commits a 'prepared' chain-transactions row, then submit's getChainId fails (RPC error), so nothing is broadcast. Tick 2: the host is disabled (enabled() false), so the worker calls step with allowSubmit:false. Expected: 'pending' (nothing broadcast, request and budget open) so the attempt can resume when the host is re-enabled. Actual: the row is cancelled, the reservation released, the answer is {state:'failed', reason:'dispatch_cancelled'}, and the worker persists it and skips the row on every later tick, including after re-enabling and after a new allocation. Reproduced with test/scratch/paused-cancel.test.mjs, which fails on this tree with 'actual: failed / dispatch_cancelled'.","severity":"info","snippet":"          return answer('failed', { reason: 'dispatch_cancelled' });","title":"Accepted limit, broader trigger than documented: a signed-but-unsent delivery met by one disabled or paused tick becomes a permanent dispatch_cancelled failure"},{"citation":"resolved","description":"adapters/contract-delivery.mjs:188 now answers failed/dispatch_subsidy_expired for any request with no transaction once budget.expiresAt has passed, and this line makes that terminal. For the rebinding cases (a resolve already signed under the lapsed budget) that is exactly the documented no-replacement rule and is correct. For a request that was merely waiting in the queue (no transaction ever prepared, nothing bound to any budget) the same answer is produced, so an allocation gap of any length, including the ordinary gap between one allocation's expiresAt and the next admission, permanently strands every result_ready request that was queued at that moment, even though its Inbox deadline may be hours away and delivering it under the new allocation involves no replacement spend and no rebinding (createContractDelivery.step only rebinds when an original resolve transaction exists). 0.12.2 answered pending here, so a fresh allocation would have delivered those requests; 0.12.3 is a regression for this sub-case. The operator's own test ('a stopped subsidy preserves the delivery failure reason and is not polled forever') asserts the new behaviour, so it is intended; it is recorded as info so the requester decides it knowingly rather than by omission. A minimal alternative that keeps every stated safety property: when no chain-transactions row exists for the resolve, answer pending with a retry time instead of failed, or let the worker re-step failed/dispatch_subsidy_expired rows whose delivery has no transactionHash under a different budget id.","line":29,"path":"runtime/contract-intake-worker.mjs","reproduction":"State: budget A expires at T; request R is result_ready with observation.deadline = T + 3600s and no chain-transactions rows. At T+1 the worker ticks: delivery.step answers {state:'failed', reason:'dispatch_subsidy_expired'} (adapters/contract-delivery.mjs:188, covered by the operator test 'an expired subsidy stops sponsorship without declaring the onchain request closed') and the worker writes delivery.state 'failed'. Budget B is then admitted and the worker restarts bound to B. Expected (per 0.12.2 behaviour and the absence of any signed transaction): R is stepped under B and delivered before its deadline. Actual: this line skips R on every tick; delivery.step is never called again (the operator test asserts f.sends.length stays 1 after a renewed tick), and R is never delivered by any allocation although it was never attempted.","severity":"info","snippet":"      if (request.state !== 'result_ready' || ['delivered', 'closed', 'failed'].includes(request.delivery?.state)) continue;","title":"Accepted limit, flagged for an explicit decision: a request that was never attempted when the subsidy lapsed is persisted failed and a renewed allocation never delivers it (0.12.2 left it pending)"}],"hash":"25cfc873687adfe2418a408f53d0d9a29a898c7e5fcaee97f6c3577adabed9a3","nodeId":"4a484c8b-ee5f-408a-894d-b4707e0ea5d7","outcome":"completed","summary":"The findings file is complete and validated against the tree, the working tree is clean, and no anvil processes remain. The review is done.\n\n**Outcome.** The 0.12.3 delta fixes what it claims for the seven prior findings, and I confirmed each fix in code and by test. One material defect remains in the recovery path those fixes depend on, plus one low state-accuracy defect and two accepted limits that deserve an explicit decision. All four are in `.imd-findings.json` with reproductions.\n\n**What I verified independently**\n\n- Full suite on this tree: 419 pass, 10 environment skips, 0 fail. Both real local anvil runs with the padded relay pass on chain ids 11155111 and 1.\n- Finding 1 fix: the decode-and-re-encode comparison accepts every non-canonical calldata layout I could construct that the EVM accepts (trailing bytes, unpadded or reordered tails, gaps, dirty padding, unaligned offsets) and rejects changed arguments. For EOA owners the vendored OpenZeppelin v5 ECDSA admits exactly one 65-byte signature, so no equivalent-signature relay exists.\n- Finding 2, 3/4, 5, 6, 7 fixes behave as described. Cancellation, reservations, nonce and hash binding, one-attempt and no-replacement rules held in every path I traced.\n\n**Findings**\n\n1. **Medium, relay recovery against the production RPC.** The chain worker is hard-wired to the public Sepolia endpoint, and I probed it live: logs and state are served only for roughly the last 10,000 blocks (about 33 hours); older ranges return \"pruned history unavailable\". Relay recovery scans from the module's deployment block, so for any module older than that the first page throws, the cursor never advances, and the job retries every minute forever. The project is registered on chain but never bound, and a new intent re-enters the same stall. This reproduces the original Finding 1 outcome for an attacker whenever the victim's module is more than a day old, and it also makes the Finding 5 and 7 fixes unreachable there. The deployed contracts are about 3,500 blocks old today, so the live testnet and the anvil runs did not exercise this. Scanning from the intent's observed block with one pinned state read for the pre-existing conflict case, or an archive-capable endpoint, would resolve it.\n2. **Low, binding codeHash still read at latest.** One lagging `getCode` answer bakes the hash of empty code into the write-once verified binding. Reproduced.\n3. **Info, accepted limit.** A signed-but-unsent delivery that meets one disabled tick is cancelled and permanently failed, although the trigger includes operator pauses, not just expiry. Reproduced.\n4. **Info, accepted limit.** A request never attempted when the subsidy lapsed is persisted failed and a renewed allocation never delivers it. 0.12.2 left it pending. The operator's own test asserts the new behaviour.\n\n**Scope B conclusion.** The two prior blockers are fixed in code, but public Sepolia registration recovery is not clear: finding 1 above is attacker-triggerable on the configured endpoint for any module older than about a day, with the same permanent outcome the padded-calldata finding had. It should be fixed or the RPC requirement met before public B admission. Findings 2 through 4 can ship with notes. No authorization bypass, unbounded spend or custody exposure was found.\n\n**Scope C conclusion.** The same code path runs on chain id 1 against the operator-supplied mainnet RPC. Whether that endpoint serves full log and state history is not in the tree, so the same retention requirement must be confirmed as a preparation item. Nothing in this assessment authorizes mainnet activation, which still requires the separate exact human approval. Test evidence here is operator evidence; this review does not constitute acceptance.\n\n**Limits.** I reviewed the delta files, the recovery, executor, dispatcher and intake-worker paths, and the executor contract's registration rule. Unchanged intake scanning, payments, vaults and wallets were not re-audited. Scratch t","treeHash":null,"usage":{"cachedInputTokens":2812798,"inputTokens":706,"model":"claude-fable-5-1","outputTokens":73047,"runtime":"claude","turns":54,"wallClockMs":1142617}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"771f83f312eace21","findings":[{"citation":"resolved","description":"The 0.12.3 empty-page guard (Finding 5 fix) only skips pages that end before the module's receipt block. It does not check that the pinned executor exists at the page end. The registration scan starts at min(module receipt block, observedBlock) and pages by 10,000 blocks (adapters/chain-event-recovery.mjs:11). When the module was deployed more than 9,999 blocks before the executor the registration targets, the first page [moduleBlock, moduleBlock+9999] ends before the executor's deployment. eth_call against an address with no code at that block returns 0x, viem's readContract throws ContractFunctionZeroDataError (no .code), src/chain-deployments/index.mjs:189 maps that to a 60 s wait, and the same page is scanned again on every tick. The cursor is never written (validateEmpty runs before the store transaction), so the exact mined relay is never adopted: the job stays 'registering'/waiting forever, the project is registered onchain with no chain-bindings row, and because the job is active (queued/registering) the owner cannot prepare another intent (registration_in_progress). This state is reachable in the deployed system: executor rotation is a supported feature (network-capabilities.retainedExecutors, contracts/DEPLOYMENTS.json already lists one retained and one active Sepolia executor), prepareRegistration only requires module.chainId === capability.chainId and the module code to be unchanged, and chain.registration() pins the current default executor. Any module deployed about 33 h (Sepolia) or more before a later executor rotation, and later registered through a direct relay, hits it. On baseline 1f6f6d7 the page was simply committed as empty and the relay found on a later page. The same code is shared by chain id 1. Impact: liveness only (no spend, no authority effect), but with no operator recovery short of a code change because the failure repeats identically every tick. Fix: in validateEmpty, return early when client.getCode({address: value.executor, blockNumber: toBlock}) is '0x' (the executor cannot have registered anything on a page that ends before its own deployment), or catch the zero-data error from projectAt(toBlock) and treat it as 'not registered at this block'.","line":74,"path":"adapters/chain-executor.mjs","reproduction":"test/scratch/recovery-edges.test.mjs, test 'EDGE A' (fixture copied from test/registration-recovery.test.mjs). State: chain-modules 'module' receipt.blockNumber '80'; registration observedBlock '85'; executor code exists only from block 20000 (getCode returns '0x' below it, readContract('projects', blockNumber < 20000) throws 'The contract function \"projects\" returned no data (\"0x\")' exactly as viem does); ProjectRegistered log with matching owner/mandateHash at block 20100 whose transaction input is the exact signed calldata; finalized head 20200; latest projects() non-zero. Expected: tick 1 commits page [80,10079] as empty (executor absent), tick 2 commits [10080,20079], tick 3 finds the log and returns state 'verified'. Actual: every tick rejects with the viem zero-data error, store.list('chain-event-recovery') stays empty after three ticks, and the result is never 'verified'. Control test in the same file with the executor deployed at block 50 reaches 'verified' on the third tick. Run: node --test test/scratch/recovery-edges.test.mjs","severity":"low","snippet":"        if (toBlock < deployed || /^0x0{40}$/i.test((await projectAt(toBlock))[1])) return;","title":"validateEmpty reads projects() at a page end where the pinned executor has no code, so a relay for a module deployed more than one 10,000-block page before the executor is never recovered"},{"citation":"resolved","description":"Prior finding 5 (one empty eth_getLogs page advances the cursor past the event, after which the finalized state read is terminal) was fixed in 0.12.3 for ProjectRegistered recovery by passing validateEmpty. The sibling consumer of recoverChainEvent at adapters/chain-executor.mjs:236-238 (UpgradeExecuted recovery for an approval that a relayer executed first) passes no validateEmpty. Sequence: executionNonces[projectKey][nonce] is true at latest (line 231), the page that holds the UpgradeExecuted event comes back empty from a lagging log index, recoverChainEvent writes the cursor to head+1 and returns complete, line 242 reads executionNonces at 'finalized' (true, the nonce was consumed by the real execution) and line 243 returns {state:'blocked', error:'approval_cancelled'}. The approvals worker records the proposal as blocked/cancelled although the upgrade executed onchain exactly as Claus signed it, and the cursor (key 'execution:'+id) now starts past the event, so even a later tick with a correct log index returns the same answer. This is unchanged code, but it is the same mechanism the 0.12.3 delta set out to close and the fix was applied to one of the two call sites. The registration guard's shape transfers directly: before committing an empty page whose end is at or after the queue/evidence block, read executionNonces at toBlock; if consumed, require it to be consumed at fromBlock-1 as well, otherwise throw a retryable code without advancing the cursor. No spend or authority effect; the wrong terminal state for a correctly executed upgrade is a state-accuracy and liveness defect, and the public pool paid nothing extra.","line":236,"path":"adapters/chain-executor.mjs","reproduction":"test/scratch/recovery-edges.test.mjs, test 'EDGE B'. State: network-capabilities 'chain' enabled with approvalSigner = a generated key; claus-signed-approvals row with a real EIP-712 Approval signature from that key (hashTypedData/recoverTypedDataAddress pass); chain-bindings for the project; artifacts.get returns the proposal with evidence.chain.blockNumber 100; no chain-transactions rows (a relayer executed first); readContract executionNonces returns true; finalized head 200; UpgradeExecuted log at block 150 that getLogs omits on the first tick (stale index) and returns afterwards. Expected (the rule 0.12.3 applies to registrations): the first tick must not become terminal on an inconsistent empty page, and once the log is served the step returns 'executed'. Actual: the first tick returns {state:'blocked', error:'approval_cancelled'} and the second tick, with the log now visible, returns the same because the cursor is already at 201. Control test in the same file with the log visible from the start returns 'executed'. Run: node --test test/scratch/recovery-edges.test.mjs","severity":"low","snippet":"        const found = await recoverChainEvent({store,client,chainId,key:'execution:'+value.id,","title":"The empty-page guard was added only to registration recovery; execution recovery still commits an empty finalized page and then reads a consumed nonce as a permanent approval_cancelled"},{"citation":"resolved","description":"0.12.3 answers 'failed'/dispatch_subsidy_expired for the no-transaction path once budget.expiresAt <= now(), and runtime/contract-intake-worker.mjs:29 treats 'failed' as terminal forever. The accepted bounded limit (brief, Findings 3/4) is that a signed attempt is not replaced and a callback stays bound to the resolve's allocation. This case is different: no transaction row exists, so createContractDelivery.step (lines 53-62) would not rebind the request to the old allocation, and a replacement allocation prepares and broadcasts the resolve normally (control test). The hosted worker selects exactly one allocation at startup (runtime/contract-intake-host.mjs:23) and its enabled() check is ports.verifyAdmission, which does not read the allocation's expiry, so after the allocation lapses the host keeps scanning, ingesting and deciding public requests (inference cost incurred) and every result_ready row it visits is marked failed within one tick (at most a 60 s retryAt later), including requests whose results became ready after the expiry. On baseline 1f6f6d7 these rows stayed 'pending' and were delivered by the next allocation. docs/contract-intake.md now states 'Expired subsidy ... ends that sponsored attempt as delivery.state: failed', which describes the attempt case; for a request with no attempt it converts a reversible operator condition (allocation renewal) into a permanent per-request outcome. No spend, custody or authority effect; the onchain request stays open until its own deadline and the canonical result remains readable. If the requester keeps this as the intended behaviour it should be stated as such (requests decided while no allocation is live are never sponsored), and ideally intake should stop admitting new requests once the selected allocation has lapsed. Minimal fix otherwise: on the no-transaction path answer 'pending' with the existing retryAt when the request is still open onchain and no attempt exists, keeping 'failed' for the attempt cases (prepared/cancelled rows and callbacks bound to a lapsed resolve allocation).","line":188,"path":"adapters/contract-delivery.mjs","reproduction":"test/scratch/delivery-expired-budget.test.mjs (fixture copied from test/contract-delivery.test.mjs with the request deadline one day past the allocation expiry). Test 1: advance now to budget.expiresAt + 1; run createContractIntakeWorker.tick() with enabled () => true and delivery bound to the lapsed allocation; contract-requests.delivery becomes {state:'failed', reason:'dispatch_subsidy_expired'} with zero chain-transactions rows; admit a replacement allocation and tick a worker bound to it. Expected: the replacement allocation prepares and broadcasts the resolve (broadcasts === 1). Actual: broadcasts === 0, the row is still failed/dispatch_subsidy_expired, delivery.step is never called again. Test 2 (control, passes): stepping the delivery adapter directly with the replacement allocation on the same request returns 'pending', broadcasts 1 and reserves 135000000000 wei on the replacement allocation, proving the request was deliverable. Run: node --test test/scratch/delivery-expired-budget.test.mjs","severity":"low","snippet":"        if (expiresAt <= now()) return answer('failed', { reason: 'dispatch_subsidy_expired' });","title":"A request that never had a delivery transaction is permanently failed by one tick under a lapsed allocation, although a replacement allocation could deliver it and intake keeps admitting new requests "},{"citation":"resolved","description":"The Finding 2 fix pins projects()/executor()/projectId() reads to tx.receipt.blockNumber, and validateEmpty (lines 74-78) reads projects() and getCode() at arbitrary historical page boundaries. On baseline 1f6f6d7 the post-confirmation reads were at latest. For Claus's own transaction the receipt is a few blocks old and any RPC serves it. For a relay recovered late (now allowed by the Finding 7 change: an expired prepared intent may be submitted at any later time) the receipt block can be days old. An RPC without historical state (typical non-archive limit: 128 blocks) answers eth_call at that block with an error such as -32000 'missing trie node'; viem rethrows it without a code, src/chain-deployments/index.mjs:189 waits 60 s, and the registration never completes while the owner is locked out of a new intent (registration_in_progress). This is environment-dependent, not a logic defect: the fix is correct for an archive-capable endpoint, and the operator's RPC is not in the tree (CLAUS_*_RPC_URL). Report it as an operations requirement: document that the executor's RPC must serve eth_call and eth_getCode at arbitrary historical blocks, or fall back to a latest-block read when the pinned read fails with a state-unavailable error.","line":159,"path":"adapters/chain-executor.mjs","reproduction":"State: chain-registrations value whose exact relay was mined at block N, submitted after N + 128 blocks have passed (chain-integration relay flow with the API submission delayed); an RPC that rejects eth_call with blockNumber older than head-128 (mock readContract throwing Error('missing trie node') when blockNumber < head - 128n). Expected: the registration verifies or fails with a diagnosable code. Actual: deployment.step throws at line 161 on every tick, the job stays waiting with result.error 'chain_execution_unavailable', and chain-bindings is never written.","severity":"info","snippet":"      const registeredRead = (address, functionName, args = [], abi = executorAbi) =>","title":"Pinned post-registration state reads require historical eth_call state at the receipt block; on a non-archive RPC a late-recovered relay waits indefinitely"}],"hash":"6b9539abba0497d6a8021ee722513d264dd52bfe9f86d16f458799c819008ae7","nodeId":"81c7c0ad-02bd-4a76-9c80-078d5f9d8f67","outcome":"completed","summary":"No tracked file changed, the findings file validates, and every snippet matches its line. The review is complete.\n\n## Outcome\n\nThe 0.12.3 delta against baseline 1f6f6d7 fixes all seven prior findings as described. I found three new low-severity defects and one environment note, written to `.imd-findings.json`. None affects authority, custody, spend ceilings, signatures or the no-replacement rule.\n\n## What I verified independently\n\n- **Source gate.** Full suite on this tree: 419 pass, 10 environment skips, 0 fail.\n- **Real local chain runs.** Built the contracts and ran the integration test against anvil with the padded relay enabled for chain ids 11155111 and 1. Both pass.\n- **Finding 1.** The decode-and-re-encode comparison normalizes every Solidity-valid non-canonical layout I probed: trailing garbage, a single trailing byte, an unpadded signature tail, reordered dynamic tails and a gap word before the tails. OpenZeppelin 5.6's ECDSA rejects 64-byte and high-s signatures, and owners are EOAs from challenge login, so a mempool observer cannot produce a different valid calldata for the same intent. Changed arguments still fail. Nested relays remain the documented unsupported case.\n- **Finding 2.** Post-registration reads are pinned to the receipt block and throw a non-terminal code that the job runner retries after 60 seconds. A confirmed own transaction or an adopted relay cannot genuinely mismatch at its own receipt block, so retrying is correct.\n- **Findings 3 and 4.** Subsidy expiry and cancellation of an unsent transaction answer failed with bounded reason codes, never closed, unless the request deadline or chain state proves closure. Reservations are released, no replacement spend occurs, a new allocation is not consulted for a bound attempt, and the worker persists the reason and stops. Pending rows now carry a 60 second retry.\n- **Finding 5.** The new cursor namespace keeps old journals. An empty page that ends with the project registered, but not registered before the page start, throws a retryable code without advancing the cursor. A project registered before the module's history remains a conflict.\n- **Findings 6 and 7.** An expired unsent intent waits while finalized chain time precedes expiry and signs nothing. A late submission queues recovery only; the executor cancels before preparing any transaction when the window has passed.\n\n## Remaining findings\n\n1. **Low, registration recovery.** The new empty-page guard reads project state at a page end where the pinned executor may not exist yet. For a module deployed more than one 10,000-block page before a later executor rotation, every tick throws and the cursor never advances, so an exact mined relay is never adopted and the owner is locked out of a new intent. Baseline committed those pages. Reproduced in `test/scratch/recovery-edges.test.mjs`.\n2. **Low, execution recovery.** The same empty-page mechanism as prior finding 5 is still open on the sibling path: an empty page for a relayed UpgradeExecuted event is committed and the consumed nonce is then read as a permanent approval_cancelled. Unchanged code, but it is the mechanism this delta set out to close. Reproduced in the same file.\n3. **Low, delivery.** A request that never had a transaction is permanently failed by one tick under a lapsed allocation, while a replacement allocation would deliver it, and intake admission does not read allocation expiry so the host keeps deciding new requests into that state. This differs from the accepted limit, which concerns signed attempts and callbacks bound to a lapsed resolve allocation. If the requester keeps this behaviour it should be stated explicitly. Reproduced in `test/scratch/delivery-expired-budget.test.mjs`.\n4. **Info.** Pinned historical reads now require an archive-capable RPC for late-recovered relays. Operations requirement, not a logic defect.\n\n## Accepted bounded limits, unchanged\n\nThe 32-event shared scan page, 16 lifetime intents, the fee ceilings and the","treeHash":null,"usage":{"cachedInputTokens":2418592,"inputTokens":610,"model":"claude-fable-5-1","outputTokens":61376,"runtime":"claude","turns":44,"wallClockMs":948184}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"b38bea3c04869138","findings":[{"citation":"resolved","description":"The 0.12.3 delta turns an expired subsidy into a terminal delivery.state 'failed' with reason dispatch_subsidy_expired. On the callback action this line is reached after the resolve transaction has already been confirmed and finalized under the same allocation (resolveTransaction is set at line 211 only after the loop finishes, so it is dropped). The answer carries no transactionHash and no finalized flag, and runtime/contract-intake-worker.mjs:63-69 persists exactly that shape. The API's contract_request_get returns the contract-requests row minus observation; chain-transactions rows are internal and not exposed. So the public record for a request that IS resolved onchain (status 2/3, immutable result stored in the Inbox) is byte-for-byte the same as for a request that was never resolved: {state:'failed', reason:'dispatch_subsidy_expired', resultHash, observedAt}. The same holds for a cancelled unsent callback (dispatch_cancelled at line 203) and for a confirmed callback that did not deliver (line 212, which carries no reason at all). docs/contract-intake.md:106 promises that 'the immutable result and original transaction history remain available; a finalized result can be read directly or its callback delivered externally', but the API gives the caller no signal that the resolve exists or which transaction carried it. This is a reporting-accuracy defect in the bounded Inbox/callback flow, not a spend, custody or authorization issue. Suggested minimal fix: when the resolve action has completed in this step or a confirmed 'contract-delivery:<id>:resolve' row exists, include its hash (and a distinct reason such as dispatch_callback_subsidy_expired / dispatch_callback_cancelled / dispatch_callback_not_delivered) in the failed answer, and let the worker persist transactionHash for failed callbacks. No state machine change is needed.","line":188,"path":"adapters/contract-delivery.mjs","reproduction":"State: request result_ready with callbackGas > 0; allocation A with expiresAt T; resolve prepared, broadcast and confirmed+finalized under A before T; callback not yet prepared at T. Input: delivery.step at time T+1. Expected: a failed answer that still identifies the finalized resolve (transactionHash of the resolve, or a callback-specific reason). Actual: {state:'failed', reason:'dispatch_subsidy_expired', resultHash} with transactionHash undefined; the persisted contract-requests.delivery row has no transactionHash and no finalized flag; Object.keys of this answer equals Object.keys of the answer for a request with no resolve at all under an expired allocation. Reproduced with test/scratch/delivery-failed-reporting.test.mjs (first test) against this tree: the 'contract-delivery:<id>:resolve' row is state 'confirmed' while the answer and the persisted row omit it.","severity":"low","snippet":"        if (expiresAt <= now()) return answer('failed', { reason: 'dispatch_subsidy_expired' });","title":"A failed callback subsidy after a finalized resolve is reported identically to a never-resolved request"},{"citation":"resolved","description":"This is the behavior the 0.12.3 brief states for Findings 3/4 ('expired subsidy with no unsent transaction returns delivery failed / dispatch_subsidy_expired') and is documented in docs/contract-intake.md:106, so it is recorded here as an accepted bounded limit, not as a new defect. Its operational consequence is wider than the text suggests: adapters/contract-delivery.mjs:188 runs before the allowSubmit/active check at line 189, and the host pins one allocation by CLAUS_CONTRACT_DISPATCH_BUDGET (runtime/contract-intake-host.mjs:23) with no expiry check in verifyAdmission. After that allocation's expiresAt, every tick converts each result_ready request that has no transaction, no reservation and no signature into a terminal 'failed' row, including ticks where enabled() is false (operator pause). The worker skips failed rows forever (this line), so an allocation admitted a minute later cannot deliver them even though createContractDelivery would select the new allocation for a request with no original resolve transaction. There is no 'attempt' to stop here: nothing was signed or reserved. Expected by the doc's own rule for not-yet-attempted work ('insufficient gas allowance leaves delivery pending with a retry time') would be pending with retryAt until an unexpired allocation exists. Net effect for scope B: a sponsorship gap or pause silently loses sponsored delivery for every decision that completes during the gap; the canonical result stays readable, and only the dispatcher can call Inbox.resolve, so the requester cannot self-serve the resolution. If the requester wants to keep terminal failure here, the recommended mitigation is operational: never let the pinned allocation lapse before its successor is admitted, and have verifyAdmission refuse to run 'active' with an expired allocation. If not, swap lines 188/189 so an unattempted request under an expired allocation returns pending with retryAt.","line":29,"path":"runtime/contract-intake-worker.mjs","reproduction":"State: pinned allocation expiresAt T; request result_ready, deadline T + 7 days, no chain-transactions row. Input: worker tick at T+1 with enabled() false (allowSubmit=false). Expected (if deferred): pending with retryAt, deliverable by the next admitted allocation. Actual: delivery {state:'failed', reason:'dispatch_subsidy_expired'}; zero broadcasts, zero reservations. Then admit allocation B (expiresAt T + 1 day): a direct delivery.step with B returns pending and broadcasts the resolve, proving B could deliver; but the worker's next tick never steps the row (this line) so delivered stays 0. Reproduced with test/scratch/delivery-failed-reporting.test.mjs (second test).","severity":"info","snippet":"      if (request.state !== 'result_ready' || ['delivered', 'closed', 'failed'].includes(request.delivery?.state)) continue;","title":"Accepted design, consequence recorded: an expired pinned allocation permanently fails untouched requests, even while the worker is paused"},{"citation":"resolved","description":"The Finding 2 fix pins the post-registration reads of projects(), target.executor() and target.projectId() to the receipt block, and the Finding 5 fix (lines 74-79) reads projects() at the end of every empty log page and getCode/projects() at the block before the page start, which can be as old as the module deployment block. On the configured Sepolia endpoint (ethereum-sepolia-rpc.publicnode.com, archive-capable) this is fine, and on a chain-1 endpoint it is whatever CLAUS_MAINNET_RPC_URL points at. On a non-archive node (geth default retains state for about 128 blocks) eth_call at an older block fails with 'missing trie node'. viem surfaces that as an error without a .code, which src/chain-deployments/index.mjs:181-189 maps to 'waiting' with a 60 s retry, indefinitely. Concrete state where this matters: the executor worker is down for longer than the node's state window after Claus's own registration transaction confirmed (gas already paid), or a relay recovery has to walk pages older than the window. The job then never reaches verified or blocked, the chain-bindings row is never written, and prepareRegistration keeps answering registration_in_progress for that project. The 0.12.2 baseline read the same state at latest and would have completed. This is an operational dependency created by the delta rather than a logic defect; it is reported so the scope C operator checklist records 'archive-capable RPC required' next to CLAUS_MAINNET_RPC_URL. A minimal code mitigation is to fall back to a 'finalized'-tagged read when the pinned read fails and the receipt block is at or below the finalized head (the state at a finalized block cannot differ), keeping registration_state_unavailable for a genuine mismatch.","line":160,"path":"adapters/chain-executor.mjs","reproduction":"State: registration relay (or own tx) confirmed at block 101; finalized head 400; RPC answers eth_call with blockNumber <= 272 with 'missing trie node' and answers latest correctly. Input: deployment.step for the queued registration. Expected: verified (the state at a finalized block is immutable and readable at latest). Actual: every step throws a code-less 'missing trie node' error from the pinned projects() read, which the job runner turns into waiting/60 s forever; no binding is written. Reproduced with test/scratch/pinned-read-archive.test.mjs against this tree (three consecutive steps all reject the same way).","severity":"info","snippet":"        client.readContract({ address, abi, functionName, args, blockNumber });","title":"Block-pinned state reads introduced by the delta require an RPC with historical state; without it a mined registration never binds"}],"hash":"6bcae4afc0bea5867215d359e6c4a9ef2b703491a6305503a7e2f9088775f21a","nodeId":"c4b58eb3-7a2f-4839-a735-1b2074d96fa9","outcome":"completed","summary":"The review is complete. The findings file at the repository root holds three entries (one low, two info), each with a reproduction I ran against this tree. No tracked file was changed; the scratch tests are deleted and the working tree is clean.\n\n**What I verified on 2b31b66 against baseline 1f6f6d7**\n\n- Full node suite: 419 pass, 0 fail, 10 environment skips, matching the stated gate.\n- Real local anvil runs with the padded relay enabled pass for both chain ids 11155111 and 1. I ran them myself, so this is independent corroboration rather than operator evidence.\n- Finding 1 fix holds against the realistic frontrun variants. The decode and re-encode comparison accepts trailing bytes and non-canonical ABI offsets. An attacker cannot produce a different valid `ownerSignature` because the vendored OpenZeppelin ECDSA accepts only 65-byte low-s signatures, and the API's own verification is EOA-only. The only rejected equivalents are the owner re-signing with a non-deterministic signer or a nested relay, both documented as unsupported.\n- Finding 2 fix: receipt-pinned reads and the retryable `registration_state_unavailable` behave as described. A genuine mismatch is impossible at the receipt block without RPC error, so retry is the right mapping.\n- Finding 5 fix: the empty-page validation catches a lagging log index whenever the same node has state for the page end. The remaining window (a stale backend lacking the block) errors rather than returning zeros, so the cursor is not advanced. Pre-module registrations stay a conflict.\n- Findings 6 and 7: an expired local intent waits until finalized chain time passes expiry and never signs; an expired submission can only recover a mined relay. Both use one of the sixteen lifetime intents only on the owner's own action. A failed delivery does not hold the project's single active-request slot, so it cannot block later intake.\n- No spend path changed. Cancellation releases reservations, failed answers never rebind to a later allocation, and no new broadcast occurs in any failed or waiting state.\n\n**Findings**\n\n1. **Low, `adapters/contract-delivery.mjs:188`.** A callback whose subsidy lapses after the resolve has already finalized is reported exactly like a request that was never resolved: `failed` with `dispatch_subsidy_expired`, no transaction hash, no finalized flag. The API exposes only that row, so a caller cannot tell their request was resolved onchain. Suggested fix is to carry the resolve hash or a callback-specific reason in the failed answer.\n2. **Info, accepted design, `runtime/contract-intake-worker.mjs:29`.** An expired pinned allocation permanently fails every untouched request, including during a pause, before any later allocation is consulted. This is what the brief states and the docs record. The consequence worth noting is that an allocation gap loses sponsored delivery for all decisions completing in the gap, and only the dispatcher can resolve the Inbox. Mitigation is operational unless the requester chooses to defer unattempted requests.\n3. **Info, `adapters/chain-executor.mjs:160`.** The receipt-pinned and page-boundary state reads require an archive-capable RPC. Sepolia's configured publicnode endpoint qualifies. On a non-archive Mainnet endpoint a worker outage longer than the node's state window leaves a mined registration waiting forever with no binding. Record it as a C-profile RPC requirement.\n\n**Scope B (finite sponsored reviews, Sepolia readonly projects, Claus decisions, bounded Inbox and callback flow).** The two medium findings that blocked the prior round are fixed and I found no new authorization bypass, spend overrun, custody exposure, or attacker-triggerable permanent block on the registration path. The remaining items are a reporting-accuracy defect and two recorded operational limits. On this delta I see nothing that should hold public B back, provided the operator does not let the pinned allocation lapse before its successor is admitted.\n\n**Scope C (prep","treeHash":null,"usage":{"cachedInputTokens":2607121,"inputTokens":642,"model":"claude-fable-5-1","outputTokens":56055,"runtime":"claude","turns":41,"wallClockMs":900559}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"da7ed792c3bf5b80","findings":[{"citation":"resolved","description":"Merged from audit_math (medium), audit_flow (medium), audit_permissions (info) and audit_economics (info); one mechanism, reproduced against the code and against the live endpoint. The 0.12.3 delta adds three state reads at fixed historical blocks on the relayed-registration path: validateEmpty reads projects() at the end of every empty log page whose end is at or after the module receipt block (line 74) and, when inconsistent, getCode/projects() at the block before the page (lines 77-78); and the Finding 2 fix pins the post-registration projects()/executor()/projectId() reads to the relay receipt block (lines 158-167). The executor worker talks to one hard-wired endpoint, https://ethereum-sepolia-rpc.publicnode.com with retryCount 0 (runtime/chain-worker.mjs:28). Measured on 2026-10-12 at head 11,900,395 against the active executor 0x01a8d2e011dcb3fa65395e0343e1ab73d562b0f6: eth_call projects(bytes32), eth_getCode and a 10,000-block eth_getLogs range all succeed at depths 100, 1,000, 3,000 and 8,000 and all answer error 4444 \"pruned history unavailable: requested N, earliest available 11,890,xxx\" at depths 11,000, 12,000 and 25,000 (4 of 4 calls each). viem rethrows that as an error whose .code is 4444 (not a string the job runner knows), so src/chain-deployments/index.mjs:189 records waiting/chain_execution_unavailable and retries every 60 s with no bound, the cursor is never written (validateEmpty runs before the cursor commit in adapters/chain-event-recovery.mjs:15-21), and the registration row stays queued/registering so prepareRegistration keeps answering registration_in_progress for the project. relayedRegistration is entered exactly in the cases 0.12.3 set out to recover: the owner's direct relay, the padded front-run that Finding 1 was fixed for (Claus's own transaction reverts and line 152 falls into the scan), and the late submission of an expired intent allowed by Finding 7. The scan starts at the module receipt block (line 72), so once a verified module is more than about 20,000 blocks (about 2.8 days) old, the first page [deployed, deployed+9999] ends deeper than the retention window and line 74 throws on every tick even when the relay itself is recent. The log fetch for that page already depended on the endpoint's log retention under the baseline (measured 4444 for logs at the same depths; audit_math saw logs served intermittently at depth), but the baseline committed an empty page without any state read and never read state older than latest; the delta makes the dependency deterministic and extends it to the receipt-pinned reads, which fail for any relay mined more than about 10,000 blocks before the worker processes it (late submission of an expired intent, the Finding 7 case, or a worker outage after Claus's own confirmed registration). Consequences: for the attacker-triggered padded-relay case this restores the original Finding 1 outcome (a permanently unbindable project for the price of one front-run registration) whenever the victim's module is more than about a day old, which is the ordinary case for a reviewed module; for the honest relay and late-submission cases the owner is locked out of a new intent while the one onchain is never bound. No spend, custody or authority effect. The same code serves chain id 1 through CLAUS_MAINNET_RPC_URL, so the C profile needs the same guarantee. Fix (minimal): make the executor RPC URL a configured value, require an endpoint that serves eth_call, eth_getCode and eth_getLogs at arbitrary historical blocks for the Sepolia worker and the C checklist, and map pruned/historical-state errors (code 4444, -32000 \"missing trie node\") to an explicit diagnosable job error rather than a silent 60 s loop. Fix (structural): start the relay scan at the intent observedBlock rather than the module block, detect a pre-existing registration with one projects() read at observedBlock (non-zero target there is the terminal conflict the delta wants to keep), and for the receipt-pinned read","line":74,"path":"adapters/chain-executor.mjs","reproduction":"Scratch test test/scratch/judge-registration.test.mjs (node --test test/scratch/judge-registration.test.mjs), fixture copied from test/registration-recovery.test.mjs with a mock client that answers readContract/getCode/getLogs below latest-10,000 with Error(\"pruned history unavailable ...\") code 4444, as the live endpoint does. (1) Control, archive mode: module receipt 100000, observedBlock 100020, owner's exact relay mined at 130101, finalized head 130110: four deployment.step calls return registering, registering, registering, verified. (2) Pruned logs and state: the same state rejects with code 4444 on five consecutive ticks, chain-event-recovery stays empty, the only log range ever requested is [100000,109999]. (3) Pruned state, logs served: three ticks reject with 4444, the only state block ever requested is 109999 (the page end), no cursor is written and the same page is fetched three times. (4) Relay mined at 100050 (late submission): the log is found but the pinned projects() read at 100050 rejects with 4444 on every tick and no chain-transactions row is written. Expected in every case: verified, or a diagnosable terminal state. Live evidence: eth_call projects(0x0) on 0x01a8d2e011dcb3fa65395e0343e1ab73d562b0f6, eth_getCode and eth_getLogs over a 10,000-block range at head-11,000 and deeper on https://ethereum-sepolia-rpc.publicnode.com returned {\"code\":4444,\"message\":\"pruned history unavailable ...\"} on 12 of 12 attempts; at head-8,000 and shallower all 12 succeeded.","severity":"medium","snippet":"        if (toBlock < deployed || /^0x0{40}$/i.test((await projectAt(toBlock))[1])) return;","title":"Relay recovery and the pinned post-registration reads need historical state at old blocks, which the hard-wired Sepolia endpoint does not serve beyond about 10,000 blocks, so a relayed registration fo"},{"citation":"resolved","description":"From audit_permissions, reproduced. The guard skips only pages that end before the module receipt block; it does not check that the executor the intent targets exists at the page end. The scan starts at min(module receipt block, observedBlock) and pages by 10,000 blocks. When the module was deployed more than 9,999 blocks before the executor, the first page [moduleBlock, moduleBlock+9999] ends before the executor's creation; eth_call against an address with no code answers 0x and viem's readContract throws ContractFunctionExecutionError (\"returned no data\") with no .code, which src/chain-deployments/index.mjs:189 maps to a 60 s wait. The same page is scanned on every tick, the cursor is never written (validateEmpty runs before the commit), the exact mined relay is never adopted, and the owner cannot prepare another intent (registration_in_progress). The state is reachable: executor rotation is supported (network-capabilities.retainedExecutors; contracts/DEPLOYMENTS.json lists one retained and one active Sepolia executor, created 1,212 blocks apart, so the deployed set does not trigger it today), prepareRegistration requires only module.chainId === capability.chainId and unchanged module code, and chain.registration() pins the current default executor. Any module deployed about 33 h (Sepolia) or more before a later executor rotation and then registered through a direct relay hits it; the same code serves chain id 1. Baseline 1f6f6d7 committed the page as empty and found the relay on a later page. Liveness only. Fix: in validateEmpty return early when client.getCode({ address: value.executor, blockNumber: toBlock }) is 0x (nothing can have been registered on a page that ends before the executor exists), or treat the zero-data answer from projectAt(toBlock) as \"not registered at this block\".","line":74,"path":"adapters/chain-executor.mjs","reproduction":"test/scratch/judge-registration.test.mjs, test \"executor created more than one page after the module\". State: chain-modules module receipt block 80, intent observedBlock 85, executor code present only from block 20000 (getCode answers 0x below it and readContract(\"projects\") with blockNumber < 20000 throws the viem zero-data error), ProjectRegistered log with matching owner/mandateHash at block 20100 whose transaction input is the exact signed calldata, finalized head 20200. Expected: tick 1 commits [80,10079] as empty, tick 2 commits [10080,20079], tick 3 returns verified. Actual: three ticks reject with \"returned no data\", chain-event-recovery stays empty and the log range requested is [80,10079] on all three ticks. Control test in the same file with the executor present from block 50 returns registering, registering, verified.","severity":"low","snippet":"        if (toBlock < deployed || /^0x0{40}$/i.test((await projectAt(toBlock))[1])) return;","title":"validateEmpty reads projects() at a page end where the pinned executor has no code yet, so a relay for a module deployed more than one 10,000-block page before the executor is never recovered"},{"citation":"resolved","description":"From audit_permissions, reproduced. Prior finding 5 (one empty eth_getLogs page advances the cursor past the event, after which the finalized state read is terminal) was fixed in 0.12.3 for ProjectRegistered recovery by passing validateEmpty. The sibling consumer of recoverChainEvent for UpgradeExecuted (an approval a relayer executed first) passes no validateEmpty. Sequence: executionNonces is true at latest (line 231); the finalized page holding the UpgradeExecuted event comes back empty from a lagging log index; recoverChainEvent writes the cursor to head+1 and returns complete; line 242 reads executionNonces at finalized (true, the real execution consumed it) and line 243 returns blocked/approval_cancelled, which src/approvals/index.mjs:188-189 finishes as a blocked job and a blocked chain-proposals row. The cursor key execution:<id> now starts past the event, so a later tick with a correct log index returns the same answer. The upgrade executed onchain exactly as Claus signed it; the proposal is recorded as cancelled. This is unchanged code, but it is the mechanism the delta set out to close and the fix reached one of the two call sites. The registration guard transfers directly: before committing an empty page that ends at or after the evidence block, read executionNonces at toBlock; if consumed, require it to be consumed at fromBlock-1 as well, otherwise throw a retryable code without advancing the cursor. No spend or authority effect.","line":236,"path":"adapters/chain-executor.mjs","reproduction":"test/scratch/judge-execution.test.mjs (node --test test/scratch/judge-execution.test.mjs). State: network-capabilities chain enabled with approvalSigner = a generated key; claus-signed-approvals row with a real EIP-712 Approval signature from that key (hashTypedData and recoverTypedDataAddress pass); chain-bindings for the project; artifacts.get returns the proposal with evidence.chain.blockNumber 100; no chain-transactions rows; readContract executionNonces answers true; finalized head 200; UpgradeExecuted log at block 150 that getLogs omits on the first tick and serves afterwards. Actual: the first execution.step returns {state:\"blocked\", error:\"approval_cancelled\"} and writes chain-event-recovery nextBlock 201; after the log becomes visible the second step returns the same and the log range [100,200] is never requested again. Control test with the log visible from the start returns executed with the relayed transaction hash.","severity":"low","snippet":"        const found = await recoverChainEvent({store,client,chainId,key:'execution:'+value.id,\n          fromBlock:input.evidence?.chain?.blockNumber??queued?.receipt?.blockNumber??0,\n          query:{address:p.executor,event,args:{projectId:p.projectKey,approvalHash:signed.approvalHash}}});","title":"The empty-page guard was added only to registration recovery; execution recovery still commits an empty finalized page and then reads a consumed nonce as a terminal approval_cancelled"},{"citation":"resolved","description":"From audit_flow, reproduced. 0.12.3 pins every post-registration project/target state read to the receipt block and makes a missing answer the retryable registration_state_unavailable (lines 158-167). The one remaining post-registration chain read, the target's code for binding.codeHash, is taken at latest with no block pin and no non-empty check. The same lagging-backend answer that Finding 2 was fixed for (a load-balanced public node that has not yet applied the registration block) answers 0x for the just-created target; keccak256(\"0x\") = 0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470 is written into chain-bindings, which src/chain-deployments/index.mjs:174-177 writes exactly once and never corrects, and the step still returns verified so the job completes. The value is exported through binding() and portability and is part of the binding digest that src/vaults/index.mjs:19 compares with the operator's vault policy admission (admission.bindingHash === digest(value)), so a later vault admission would be keyed to a hash of empty code. No spend, authority or custody effect; the claim that all post-registration state reads use the receipt block is not complete. Fix: read getCode with blockNumber: tx.receipt.blockNumber (the same registeredRead pin) and requireThat the code is non-empty, throwing registration_state_unavailable otherwise so the job retries instead of persisting.","line":175,"path":"adapters/chain-executor.mjs","reproduction":"test/scratch/judge-registration.test.mjs, test \"a lagging latest getCode for the target persists the hash of empty code in a verified binding\". State: the registration-recovery fixture (relay confirmed at block 101, every pinned read answers correctly); only getCode({ address: target }) with no blockNumber answers 0x. Actual: deployment.step returns {state:\"verified\", binding:{codeHash:\"0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470\"}}, which runOne persists as the project's permanent binding. Expected: the real target code hash, or a retryable registration_state_unavailable.","severity":"low","snippet":"          owner: value.message.owner, target, codeHash: keccak256(await client.getCode({ address: target })),","title":"The target codeHash written into the write-once verified binding is still read at latest, not at the receipt block, so a lagging getCode persists the hash of empty code permanently"},{"citation":"resolved","description":"Merged from audit_math and audit_economics (one mechanism), reproduced through the worker. step() handles resolve and then callback. When the resolve is confirmed and finalized (lines 208-211) and the budget has since expired, the callback iteration has no transaction and returns answer(\"failed\", { reason: \"dispatch_subsidy_expired\" }) at line 188; the same shape is returned at lines 203 and 205 for a cancelled or reverted callback and at line 212 (no reason at all) for a confirmed callback that did not deliver. answer() carries only state, resultHash and reason; the resolve hash is attached only to the delivered answer at line 214, and resolveTransaction is set only after the loop completes. runtime/contract-intake-worker.mjs:63-69 persists exactly that shape, copies transactionHash/finalized only for delivered, and never revisits a failed row (line 29). contract_request_get (src/contract-intake/index.mjs request()) returns the stored delivery object unchanged, and chain-transactions rows are internal. So the public record for a request that IS resolved onchain (immutable result stored in the Inbox) is byte-for-byte the same as for one that was never resolved: { state: \"failed\", reason: \"dispatch_subsidy_expired\", resultHash, observedAt }. docs/contract-intake.md:106 promises \"the immutable result and original transaction history remain available; a finalized result can be read directly or its callback delivered externally\", but the API gives the caller no signal that the resolve exists or which transaction carried it. State-accuracy gap in the bounded Inbox/callback flow, not a spend or authorization defect. Fix: when the resolve action completed in this step or a confirmed contract-delivery:<id>:resolve row exists, include its hash (and a callback-specific reason such as dispatch_callback_subsidy_expired / dispatch_callback_cancelled / dispatch_callback_not_delivered) in the failed answer, and let the worker persist transactionHash for failed callbacks.","line":188,"path":"adapters/contract-delivery.mjs","reproduction":"test/scratch/judge-delivery.test.mjs, first test (node --test test/scratch/judge-delivery.test.mjs; fixture copied from test/contract-delivery.test.mjs with callback = true, observation.chainId set and the request deadline a day past the allocation expiry, driven through createContractIntakeWorker). Tick 1 prepares and broadcasts the resolve under fixture-budget (broadcasts 1); confirm() mines and finalizes it (chain-transactions resolve row state confirmed). Advance now to budget.expiresAt + 1 and tick again. Actual on 2b31b66 (the test fails here): contract-requests.delivery = { state: \"failed\", reason: \"dispatch_subsidy_expired\", resultHash, observedAt } with transactionHash undefined while the confirmed resolve row still exists; the second test shows the never-resolved request under the same expired allocation persists exactly the same four keys. Expected: a delivery record that names the finalized resolve transaction.","severity":"low","snippet":"        if (expiresAt <= now()) return answer('failed', { reason: 'dispatch_subsidy_expired' });","title":"A finalized resolve whose callback funding lapsed is recorded as delivery failed without its resolve transaction hash, so the request view cannot be told from a request that was never resolved"},{"citation":"resolved","description":"Merged from audit_permissions (low), audit_flow (info) and audit_economics (info); one mechanism, reproduced. The brief states this behaviour for Findings 3/4 (\"expired subsidy with no unsent transaction returns delivery failed / dispatch_subsidy_expired\") and the operator test asserts it, so it is intended; it is recorded so the requester decides it knowingly. For the attempt cases (a resolve already signed under the lapsed budget, or a callback bound to the resolve's allocation) the terminal answer is exactly the documented no-replacement rule. For a request that was merely waiting (no transaction row, nothing reserved or signed) the same answer is produced, line 188 runs before the allowSubmit/active check at line 189 (so a paused tick produces it too), and runtime/contract-intake-worker.mjs:29 makes it terminal. createContractDelivery.step rebinds only when an original resolve row exists, so a replacement allocation would deliver such a request normally (control in the reproduction). The hosted worker pins one allocation at startup (runtime/contract-intake-host.mjs:23) and its enabled() is ports.verifyAdmission, which does not read the allocation's expiry, so after it lapses the host keeps scanning, ingesting and deciding public requests (inference cost incurred) and every result_ready row it visits, including those whose results become ready after the expiry, is marked failed within one tick. Baseline 1f6f6d7 left these rows pending and the next allocation delivered them. docs/contract-intake.md:106 describes the attempt case (\"ends that sponsored attempt\"); it does not say that decisions completed while no allocation is live are never sponsored. No spend, custody or authority effect; the onchain request stays open until its own deadline and the canonical result remains readable, but only the dispatcher can call Inbox.resolve, so the requester cannot self-serve. If the requester keeps this behaviour: state it in the docs, and have verifyAdmission (or the host) refuse to run active with an expired pinned allocation so intake stops admitting work it will never deliver. Otherwise: on the no-transaction path answer pending with the existing retryAt while the request is open onchain and no attempt exists, keeping failed for prepared/cancelled rows and for callbacks bound to a lapsed resolve allocation.","line":188,"path":"adapters/contract-delivery.mjs","reproduction":"test/scratch/judge-delivery.test.mjs, tests 3 and 4. Test 3: advance now to budget.expiresAt + 1 and tick createContractIntakeWorker bound to the lapsed allocation: contract-requests.delivery becomes { state: \"failed\", reason: \"dispatch_subsidy_expired\" } with zero chain-transactions rows; admit a replacement allocation (expiresAt one day later) and tick a worker bound to it: broadcasts stays 0 and delivery.step is never called for the row. Control in the same test: stepping the delivery adapter directly with the replacement allocation returns pending, broadcasts 1 and reserves 135000000000 wei on the replacement, proving the request was deliverable. Test 4: the same state ticked with enabled() false (operator pause) also persists failed. Expected (baseline behaviour): pending until an unexpired allocation delivers it before the Inbox deadline.","severity":"low","snippet":"        if (expiresAt <= now()) return answer('failed', { reason: 'dispatch_subsidy_expired' });","title":"Accepted-limit consequence, broader than documented: a request that was never attempted when the pinned allocation lapsed is permanently failed, a replacement allocation never delivers it, and intake "},{"citation":"resolved","description":"From audit_flow, reproduced. This is the cancelled-unsent edge of prior findings 3/4, now labelled honestly as failed/dispatch_cancelled instead of closed, and it is recorded as an accepted bounded limit: the one-attempt-per-request rule (line 47 comment) and docs/contract-intake.md:106 (\"cancellation of a signed but unsent delivery ends that sponsored attempt as failed\") cover it. What the documentation does not say is that the cancellation is not limited to subsidy expiry or lost authority. sepolia-transactions.submit cancels a prepared row whenever the authorize callback returns false (adapters/sepolia-transactions.mjs:98-107), and the callback at line 166 returns false for !allowSubmit, i.e. whenever the intake host ticks while enabled() is false or throws an admission code (a disabled contract-intake capability during a release rollover is enough). The worker then persists failed and never revisits the row, so a request whose Inbox deadline, binding and budget are all open loses its only sponsored attempt because an operator pause coincided with a crash or RPC failure between prepare and submit. The cancellation itself is correct (never broadcast while unauthorized); only the terminal label is a choice. If pauses should be survivable, the minimal change is to leave the prepared row untouched (answer pending with retryAt) when allowSubmit is false and the intent is unexpired, cancelling only on expiry or an authority denial from active().","line":203,"path":"adapters/contract-delivery.mjs","reproduction":"test/scratch/judge-delivery.test.mjs, last test. State: budget valid for 24 h, request result_ready with an open deadline, binding active. Step 1: delivery.step with allowSubmit true; prepare commits a prepared chain-transactions row, then submit's getChainId fails (mock RPC error), nothing is broadcast. Step 2: tick the worker with enabled() false. Actual: the row becomes cancelled, the reservation is released, contract-requests.delivery = { state: \"failed\", reason: \"dispatch_cancelled\" }, and a later tick with enabled() true never re-steps the row (broadcasts stays 0). Expected if pauses are meant to be survivable: pending with retryAt, resumed when the host is re-enabled.","severity":"info","snippet":"          return answer('failed', { reason: 'dispatch_cancelled' });","title":"Accepted limit, broader trigger than documented: a signed-but-unsent delivery met by one disabled or paused tick becomes a permanent dispatch_cancelled failure while budget, binding and request are al"}],"hash":"766b07181fb06c65cc9ddfe5621c2db66e24bf7e687f2f49414603e3213566f3","nodeId":"1df0c4e3-b79b-43b0-8f95-9e77aeecbd2e","outcome":"completed","summary":"The review is complete. The findings file at the repository root holds seven entries, each reproduced on commit 2b31b66 with a scratch test under test/scratch/, and no tracked file was changed.\n\n**What I verified**\n\n- The full node suite on this tree gives 419 pass and 10 skips, matching the stated gate.\n- The seven prior findings are each addressed in the delta as the brief describes. Padded relays are adopted by ABI comparison, post-registration reads are pinned and retryable, delivery failures carry explicit reasons, empty pages are cross-checked against state for registrations, finalized chain time governs expiry, and late submission of an expired intent queues recovery without a fresh broadcast.\n- The thirteen specialist reports reduce to seven distinct mechanisms. Four reports describe the same archive-state dependency and were merged; three describe the same never-attempted-request consequence and were merged; two describe the same missing resolve hash and were merged.\n\n**Findings kept, ranked**\n\n1. **Medium, the delta's historical-state reads do not work on the configured Sepolia endpoint.** Page-end state validation and the receipt-pinned reads use fixed old blocks. I measured the hard-wired publicnode endpoint live: eth_call, eth_getCode and 10,000-block eth_getLogs all answer error 4444 \"pruned history unavailable\" beyond roughly 10,000 blocks and succeed at 8,000 and shallower. Once a verified module is about 2.8 days old, the first scan page ends in pruned history and every tick loops with a 60 second retry, no cursor is written, and the registration row stays active so the owner cannot prepare another intent. This reopens the padded front-run outcome from prior Finding 1 for any module older than about a day, and it also affects late submissions of an expired intent. Liveness only, no spend or authority effect. The same requirement applies to the C endpoint.\n2. **Low, the empty-page guard reads the executor at a page end before the executor existed.** A module deployed more than one page before a rotated executor hits a zero-data error on every tick. Reachable through supported executor rotation; the deployed pair today is only 1,212 blocks apart.\n3. **Low, execution recovery was not given the empty-page guard.** One stale empty log page for a relayed upgrade commits the cursor past the event and the consumed nonce is then read as a permanent approval_cancelled for an upgrade that executed exactly as signed.\n4. **Low, the binding's target codeHash is still read at latest.** A lagging getCode persists the keccak of empty code into the write-once binding while the job completes as verified. That hash feeds the vault policy binding digest.\n5. **Low, a failed callback after a finalized resolve records no resolve hash.** The public request view is byte-identical to a request that was never resolved, which conflicts with the documentation's promise that the original transaction history remains available.\n6. **Low, accepted-limit consequence flagged for decision.** A request that was never attempted when the pinned allocation lapsed is failed permanently, including on paused ticks. A replacement allocation would deliver it, and intake keeps admitting and deciding new requests that are then failed within one tick. The baseline left these pending.\n7. **Info, accepted limit with a broader trigger.** One disabled tick cancels a signed-but-unsent delivery into a permanent failure while budget, binding and request are all open.\n\nI also checked one lead that did not become a finding: a direct relay of the same message with different valid signature bytes is a terminal mismatch. The owner signs once through the API, so this requires the owner to produce a second signature themselves, and I left it out.\n\n**Scope conclusions**\n\n- **B, finite sponsored reviews and bounded Sepolia registration: not clean on the registration path.** Finding 1 is a regression of this delta on the advertised recovery path, is attacker-triggerable thro","treeHash":null,"usage":{"cachedInputTokens":2536525,"inputTokens":597,"model":"claude-fable-5-1","outputTokens":79306,"runtime":"claude","turns":45,"wallClockMs":1222068}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"2bdd77125ec75e6c","findings":[{"citation":"resolved","description":"The finding-5 fix validates every empty eth_getLogs page with eth_call at the page's last block (line 74, projectAt(toBlock)) and, when inconsistent, eth_getCode and eth_call at fromBlock-1 (lines 77-78); the finding-2 fix pins the post-registration reads to the receipt block (lines 159-160). All three are state reads at fixed historical blocks. The executor worker talks to one hardcoded endpoint, https://ethereum-sepolia-rpc.publicnode.com with retryCount 0 (runtime/chain-worker.mjs:28), which is not an archive service: measured live on 2026-10-12 (head 11,900,331), eth_call projects(bytes32) on the active executor succeeded 6/6 at depth 100 and 1,000, 5/6 at depth 300 and 3,000, 6/6 at depth 8,000, and 0/6 at depth 12,000 and 25,000 (errors 'pruned history unavailable: requested N, earliest available 11,890,240' code 4444 or 'historical state ... missing' code -32000). Block headers are served at every depth and 10k-block eth_getLogs ranges are served by some backends at depth 25,000 (3/4) and 12,000 (1/4), so the baseline scan (logs plus a finalized read at head, within the window) could progress, while the delta's page-end state read cannot. Boundary: client.readContract/getCode with blockNumber. Assumption: state at any block from the module's deployment onward is readable. Actual: nothing older than about 10,000 blocks (about 33 hours) is readable, and reads between roughly 128 and 10,000 blocks fail intermittently (1 of 6 at depths 300 and 3,000, single failures seen at 1,000 and 5,000) because one backend keeps only recent state. Consequence: relayedRegistration (entered whenever projects(projectId).target is non-zero at latest and no local transaction is pending or the local one is terminal, i.e. exactly the padded-relay, owner-relay and late-submission cases 0.12.3 is meant to recover) scans from the module receipt block in 10,000-block pages. When the module is at least about 20,000 blocks (about 2.8 days) older than the finalized head, the first page [deployed, deployed+9999] is empty whenever the relay is more recent, its end is more than 10,000 blocks deep, every tick throws from line 74 before the cursor commit in adapters/chain-event-recovery.mjs:17-21, runOne maps the RPC error to a 60-second 'waiting' retry (src/chain-deployments/index.mjs:189-190), the chain-registrations row keeps its queued/registering state, prepareRegistration keeps returning that active intent (index.mjs:102-109) and the project, registered onchain exactly as signed, never receives a chain-binding. The same happens on the pinned post-receipt read when the relay receipt itself is older than the window (late submission of an expired intent, finding 7), and intermittently for page ends between 128 and 10,000 blocks deep. No funds are spent and no authority is bypassed; this is a permanent liveness wedge of the public B registration path introduced by the delta, and it applies unchanged to a C endpoint without archive state. Fix: either require and document an archive-capable endpoint for the executor worker, or validate empty pages without historical state: compare the page against the finalized read at head only (zero at head means no event anywhere below), and when head is non-zero locate the registration by the ProjectRegistered log fetched with blockHash per page or by eth_getTransactionReceipt/eth_getLogs of the recent blocks rather than by eth_call at old page ends; for the post-receipt check, read at the receipt block and fall back to a latest read guarded by the receipt's own log when the historical read is rejected.","line":74,"path":"adapters/chain-executor.mjs","reproduction":"Scratch test test/scratch/pruned-state-recovery.test.mjs (node --test test/scratch/pruned-state-recovery.test.mjs; fixture copied from test/registration-recovery.test.mjs). Mock client serves headers and logs for any block but rejects readContract/getCode whose blockNumber is more than 10,000 blocks behind latest with an Error('pruned history unavailable ...', code 4444), mirroring the live publicnode measurements above. State: chain-modules 'module' receipt block 100000; intent observedBlock 100020, message.expiresAt 2000, now 2100000; owner's exact relay (identical decoded calldata) mined at block 130101; finalized head 130110 (timestamp 1900), latest 130174; projects(projectId) at latest and at any block >= 130101 returns the registered tuple, zero below. Expected (and actual with the same client in archive mode): four deployment.step calls page through [100000,109999], [110000,119999], [120000,129999], [130000,130110], find the ProjectRegistered log and return state 'verified'. Actual on 2b31b66: every step rejects with 'pruned history unavailable' from the state read at block 109999 (20,175 blocks behind latest); after five ticks chain-event-recovery is still empty, eth_getLogs was issued five times for the same page, and the only state block ever requested is 109999. Third test: module receipt 100000, relay mined at 100050, head 130110: the page finds the log but the pinned post-receipt read at block 100050 rejects the same way on every tick, so the mined registration is never bound. Live evidence: curl eth_call to 0x01a8d2e011dcb3fa65395e0343e1ab73d562b0f6 projects(0x0) at head-12000 and head-25000 on https://ethereum-sepolia-rpc.publicnode.com returned an error on 12 of 12 attempts; at head-100 it returned a result on 6 of 6.","severity":"medium","snippet":"        if (toBlock < deployed || /^0x0{40}$/i.test((await projectAt(toBlock))[1])) return;","title":"Relay recovery now needs historical contract state at every empty page end, which the hardcoded Sepolia RPC does not serve beyond its ~10,000-block window, so a valid relay for a module older than ~20"},{"citation":"resolved","description":"The loop in step() handles 'resolve' and then 'callback'. When the resolve transaction is confirmed and finalized (line 208-211 set resolveTransaction) and the budget has since expired, the callback iteration has no transaction and returns answer('failed', { reason: 'dispatch_subsidy_expired' }) at line 188 (the same shape is returned at lines 203 and 205 for a cancelled or reverted callback transaction). answer() carries only state, resultHash and reason; the hash of the finalized resolve is attached only to the 'delivered' answer at line 214. runtime/contract-intake-worker.mjs:63-69 persists exactly that shape and only copies transactionHash/finalized for state 'delivered', and the worker never revisits a 'failed' row (line 29). contract_request_get (src/contract-intake/index.mjs:82-85) returns the stored delivery object unchanged. Boundary: the delivery answer for a two-transaction delivery. Assumption (docs/contract-intake.md:106, 0.12.3 text): 'The immutable result and original transaction history remain available; a finalized result can be read directly'. Actual: the request's public record ends as { state: 'failed', reason: 'dispatch_subsidy_expired', resultHash, observedAt } with transactionHash and finalized both absent, although chain-transactions 'contract-delivery:<id>:resolve' is confirmed; the only way to find the resolve is to scan the Inbox contract. The same loss applies to a cancelled or reverted callback (lines 203, 205) and to the pre-existing 'failed' at line 212. This is a state-accuracy gap, not a spend or authorization defect, and the finalized resolve itself is intact onchain. Fix: when resolveTransaction is set, include { resolvedTransactionHash: resolveTransaction.hash, finalized: true } (or a distinct state such as 'resolved_callback_failed') in every failed answer for the callback action, and let the worker persist that hash alongside the reason.","line":188,"path":"adapters/contract-delivery.mjs","reproduction":"Scratch test test/scratch/delivery-failed-edges.test.mjs, first case (node --test test/scratch/delivery-failed-edges.test.mjs; fixture copied from test/contract-delivery.test.mjs with callback = true, observation.chainId set and a request deadline two days out, driven through createContractIntakeWorker). Tick 1 prepares and broadcasts the resolve under 'fixture-budget' (broadcasts 1). confirm() mines and finalizes it (chain-transactions row state 'confirmed'). Advance now to budget.expiresAt + 1 and tick again. Expected: delivery record that names the finalized resolve (its transaction hash) together with the callback failure reason. Actual on 2b31b66: contract-requests.delivery = { state: 'failed', reason: 'dispatch_subsidy_expired', resultHash, observedAt } with delivery.transactionHash === undefined and delivery.finalized === undefined, while the confirmed resolve row still exists in chain-transactions; the row is skipped on every later tick, so the hash is never attached.","severity":"low","snippet":"        if (expiresAt <= now()) return answer('failed', { reason: 'dispatch_subsidy_expired' });","title":"A finalized resolve whose callback funding lapsed is recorded as delivery failed without its resolve transaction hash, so the request view no longer identifies the canonical onchain result the docs te"}],"hash":"9d0589a2d089920f9693e60de8853c07f87b0ff8daf468edd0b8abd725b528f0","nodeId":"50abab11-3cc0-4a6c-9fe5-e2579261d2de","outcome":"completed","summary":"The review is complete. The findings file at the repository root holds two entries, both reproduced on commit 2b31b66 with scratch tests under `test/scratch/`, and no tracked file was changed.\n\n**What I verified**\n\n- Full suite on this tree: 419 pass, 10 environment skips, matching the stated gate.\n- Local anvil integration with padded relay enabled passes for chain ids 11155111 and 1 after a forge build.\n- Each of the seven 0.12.3 claims traced in code. ABI-argument comparison, receipt-pinned state reads with a retryable code, failed/dispatch reasons with a one-minute pending backoff, the new cursor namespace with empty-page validation, finalized-time expiry on the unsent path, and expired-intent submission all behave as described. OpenZeppelin v5 accepts only 65-byte signatures and the API verifies EOA signatures only, so no second signature encoding of the same intent exists. Retained-limit arithmetic re-checked and unchanged.\n\n**Findings**\n\n1. **Medium, relay recovery depends on historical state the hardcoded RPC does not serve.** The empty-page validation reads project state at each page's last block, and the post-registration reads are pinned to the receipt block. The executor worker uses publicnode Sepolia, which I measured live: historical `eth_call` succeeded on every try at depth 100 and failed on all 12 tries at depth 12,000 and 25,000, with intermittent failures in between, while block headers and some log ranges are still served. For a module deployed more than about 20,000 blocks before the finalized head, the first page's end is outside the window on every backend, so each tick throws before the cursor commits. The job retries every 60 seconds forever, the intent stays active, and the project is registered onchain with no binding. This is a regression of the delta, since the baseline read state only at the finalized head. Same mechanism applies to a C endpoint without archive state.\n2. **Low, callback-funding failure drops the finalized resolve hash.** When the resolve is finalized and the budget lapses before a callback exists, the request's delivery record becomes failed with a reason but no transaction hash, and it is never revisited. The docs tell users to read the finalized result directly, but the request view no longer identifies it.\n\nNot reported as findings: a cancelled unsent resolve for an externally resolved request answers failed while the no-transaction path answers pending; both are honest, and the pending path is baseline behaviour.\n\n**Scope B conclusion.** The two prior medium findings are fixed as described and findings 3 through 7 are closed. B is not clean because of finding 1 above. It sits on the public registration path, is deterministic for any project registering a module older than about three days when front-run or self-relayed, and the fix is either an archive-capable endpoint or validation that does not need old state. No authorization bypass, no unbounded spend, no custody exposure found.\n\n**Scope C conclusion.** The same code runs on chain 1 with an operator-configured endpoint. Preparation is not blocked by a code defect beyond finding 1, which must be resolved or the endpoint requirement documented before the human approval. Nothing here authorizes mainnet activation, and test evidence remains operator evidence.","treeHash":null,"usage":{"cachedInputTokens":3611753,"inputTokens":898,"model":"claude-fable-5-1","outputTokens":66445,"runtime":"claude","turns":57,"wallClockMs":1080326}}],"verification":[]}