M6 — FDQ-70 root-cause SDK import fix (drop tsx) + FDQ-70b setBudget idempotency guard - #31
Merged
Conversation
…rop tsx) @virtuals-protocol/acp-node-v2@0.0.4 ships bun-authored ESM with extensionless relative imports (dist/index.js: `export * from "./acpAgent"` …). bun tolerates them; strict Node ESM rejects them (ERR_MODULE_NOT_FOUND .../dist/acpAgent), so the standalone `node` adapter process could not load the SDK. Root-cause fix: a committed pnpm patch appending `.js` to all 52 relative specifiers across the SDK dist (every target verified to be an existing sibling .js file; zero dynamic import()/require; no exports map). pnpm reapplies it deterministically on every install, and fails loudly (ERR_PNPM_UNUSED_PATCH) on any version bump. Keeps plain Node at runtime — no `--import tsx` loader, tsx stays a devDependency. Adds zero packages to the tree; the FDQ-69b native-build decline (bufferutil/utf-8-validate/es5-ext) is intact. Proof: on plain Node the unpatched SDK throws ERR_MODULE_NOT_FOUND; patched, it imports with all key exports present (AcpAgent, PrivyAlchemyEvmProviderAdapter, AssetToken, SseTransport, JobSession) — no wallet action. Supersedes PR #30 (tsx-as-runtime), which is branched off main and NOT merged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
job.created and requirement.message are DISTINCT eventTypes that both drive the accept path, so claimDispatch's per-event key admitted both, and markDecided fired only AFTER setBudget's await — a concurrent pair (SSE double-fire, or hydrateSessions re-firing a created-phase job at startup) could each pass the __decided check and each call setBudget (the #70220 double budget.set 0.25). Fix: claimAccept(chainId, jobId) — a synchronous check-and-set claimed in handleEntry BEFORE handleJobCreated's first await, released in a finally (same key shape and claim/release pattern as the funded inFlight guard). Exactly one setBudget; a transient accept failure still retries via a later event; __decided makes a successful accept permanent. An INVARIANT comment marks that markDecided must stay synchronous-adjacent to the setBudget await. submit needs no new guard: it is already claimed synchronously by claimDispatch (recentJobs[jobId:job.funded] + inFlight, set before any await) and re-checked against a fresh FUNDED status. The poll backstop routes THROUGH handleEntry (dispatchPolledJob) and emits job.funded only, so the accept chokepoint covers every dispatch origin. Tests (27/27): concurrent job.created+requirement.message → one setBudget; hydration re-fire → one; sequential-after-decided → no re-budget; concurrent job.funded → one submit; origin-independent accept guard (poll-shaped entry); real poll-timer vs SSE funded race → one submit. Verified the two accept-race tests fail (got 2) without the guard. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
3 tasks
Mayakovsky
added a commit
that referenced
this pull request
Aug 24, 2026
…seded pointer, GH reply draft - INVARIANTS.md #31: production hot-key manual-transfer-only, ratified 2026-08-23 - deploy.md: don't grep with a glob broad enough to match a real .env file - CDP-BAZAAR-STEP2 directive: marked superseded, points to the V2 fresh-wallet version - CDP-BAZAAR-REPLY-3045 draft: untracked GitHub-reply draft, still pending Forces' review/posting
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Supersedes #30. Branched fresh off
main(c7e966c), so the runtime is plainnodeandtsxis a devDependency by inheritance — no reverting hunk needed. Two logical commits.FDQ-70 — root-cause SDK import fix (committed pnpm patch, drop tsx)
@virtuals-protocol/acp-node-v2@0.0.4is bun-authored ESM with extensionless relative imports (dist/index.js:export * from "./acpAgent"…). bun tolerates them; strict Node ESM rejects them (ERR_MODULE_NOT_FOUND .../dist/acpAgent), so the standalonenodeadapter could not load the SDK. PR #30 worked around it with a productionnode --import tsxloader; Forces ruled: fix the root cause, keep plain Node..jsto all 52 relative specifiers across the SDK dist. Every target verified to be an existing sibling.jsfile; zero dynamicimport()/require; noexportsmap. pnpm reapplies it deterministically on every install.ERR_PNPM_UNUSED_PATCHblocks the install (tested); it can never silently drop.tsxstays a devDependency. Lockfile delta is pure patch-wiring — zero new packages; the FDQ-69b native-build decline (bufferutil/utf-8-validate/es5-ext) is intact.ERR_MODULE_NOT_FOUND; patched, it imports with all key exports present (AcpAgent,PrivyAlchemyEvmProviderAdapter,AssetToken,SseTransport,JobSession) — no wallet action. (13 KB patch is 52 mechanical+.js; verified 0 remaining extensionless, 0 double-appends.)FDQ-70b — setBudget idempotency guard
job.createdandrequirement.messageare distinct eventTypes that both drive the accept path, soclaimDispatch's per-event key admitted both, andmarkDecidedfired only aftersetBudget's await — a concurrent pair (SSE double-fire, orhydrateSessionsre-firing a created-phase job at startup) could each pass the__decidedcheck and each callsetBudget(the #70220 doublebudget.set 0.25).claimAccept(chainId, jobId)— a synchronous check-and-set claimed inhandleEntrybeforehandleJobCreated's first await, released in afinally(same key shape + claim/release pattern as the fundedinFlightguard). Exactly onesetBudget; a transient failure still retries via a later event;__decidedmakes success permanent. An INVARIANT comment marks thatmarkDecidedmust stay synchronous-adjacent to the await.claimDispatch(recentJobs[jobId:job.funded]+inFlight, before any await) and re-checked against a freshFUNDEDstatus. The poll backstop routes throughhandleEntryand emitsjob.fundedonly, so the accept chokepoint covers every dispatch origin.job.created+requirement.message→ onesetBudget; hydration re-fire → one; sequential-after-decided → no re-budget; concurrentjob.funded→ one submit; origin-independent accept guard (poll-shaped entry); real poll-timer vs SSE funded race → one submit. Verified the two accept-race tests fail (got 2) without the guard.Gates
typecheck ✓ · build ✓ ·
vitest run27/27 ✓ · tier-1 offline smoke ✓Not in this PR
mainnever received. On its own go: pull → install → swap unit to plain-nodeExecStart → restart → verify seller healthy on0xa966…. Abort unchanged:systemctl disable --now grey-acp-adapter→pm2 start grey. Never co-run.job.createdmissed during an SSE drop relies on SSE reconnect/replay orhydrateSessionsat restart.