From 962d3b429d487a315402fdc37f04e7d4677f792d Mon Sep 17 00:00:00 2001 From: Koji Wakayama Date: Fri, 14 Aug 2026 23:04:10 +0200 Subject: [PATCH 1/2] Allow the final esbuild stop loss before close A managed esbuild child can emit exit before close, leaving the dead child tracked while stop consumes the final restart budget. The stop path now remembers that it already charged that observed loss, so it does not immediately convert the final permitted loss into a permanent ownership failure before close clears tracking. Constraint: PR #3718 was merged and its source branch deleted before this repair could be pushed. Rejected: Recreate the deleted PR branch | the PR is already merged and branch recreation would mutate closed PR state outside the requested guarded push. Confidence: high Scope-risk: narrow Directive: Keep stop budget accounting tied to whether the current stop call charged the observed service loss; do not latch on the post-decrement zero state for the same loss. Tested: Red failure with esbuild-bundler.test.ts before implementation. Tested: deno test --preload=src/testing/preload.ts --no-check --allow-all extensions/ext-bundler-esbuild/src/esbuild-bundler.test.ts Tested: deno test --preload=src/testing/preload.ts --no-check --allow-all extensions/ext-bundler-esbuild/src/*.test.ts Tested: deno check extensions/ext-bundler-esbuild/src/esbuild-bundler.ts extensions/ext-bundler-esbuild/src/esbuild-bundler.test.ts Tested: deno lint extensions/ext-bundler-esbuild/src/esbuild-bundler.ts extensions/ext-bundler-esbuild/src/esbuild-bundler.test.ts Tested: deno fmt --check extensions/ext-bundler-esbuild/src/esbuild-bundler.ts extensions/ext-bundler-esbuild/src/esbuild-bundler.test.ts Not-tested: Full pre-push E2E gate; follow-up PR remains draft for CI. --- .../src/esbuild-bundler.test.ts | 62 +++++++++++++++++++ .../src/esbuild-bundler.ts | 4 +- 2 files changed, 65 insertions(+), 1 deletion(-) diff --git a/extensions/ext-bundler-esbuild/src/esbuild-bundler.test.ts b/extensions/ext-bundler-esbuild/src/esbuild-bundler.test.ts index 7aee5bacb4..25fc5bdce7 100644 --- a/extensions/ext-bundler-esbuild/src/esbuild-bundler.test.ts +++ b/extensions/ext-bundler-esbuild/src/esbuild-bundler.test.ts @@ -1479,6 +1479,68 @@ describe("EsbuildBundler service crash recovery", () => { } }); + it("allows the final budgeted stop loss before the child close event clears tracking", async () => { + const observation = observeEsbuildServices(); + const { services } = observation; + const bundler = new EsbuildBundler(); + let finalStop: Promise | undefined; + const finalStopStarted = Promise.withResolvers(); + + try { + await bundler.transform({ code: "export const seed: number = 0;", loader: "ts" }); + + for (let restart = 1; restart < MAX_SERVICE_RESTARTS; restart++) { + const current = services[services.length - 1]!; + current.child.ref(); + current.child.kill("SIGKILL"); + await current.close; + + await bundler.stop(); + const recovered = await bundler.transform({ + code: `export const stopReset${restart}: number = ${restart};`, + loader: "ts", + }); + assertStringIncludes(recovered.code, `stopReset${restart} = ${restart}`); + } + + const finalBudgeted = services[services.length - 1]!; + finalBudgeted.child.once("exit", () => { + finalStop = bundler.stop(); + finalStopStarted.resolve(); + }); + finalBudgeted.child.ref(); + finalBudgeted.child.kill("SIGKILL"); + + await finalStopStarted.promise; + await finalStop; + await finalBudgeted.close; + + const recovered = await bundler.transform({ + code: "export const afterFinalStopLoss: number = 3;", + loader: "ts", + }); + assertStringIncludes(recovered.code, "afterFinalStopLoss = 3"); + + const exhausted = services[services.length - 1]!; + exhausted.child.ref(); + exhausted.child.kill("SIGKILL"); + await exhausted.close; + + const stopError = await assertRejects(() => bundler.stop()); + assertStringIncludes((stopError as Error).message, "module-wide adapter"); + assertStringIncludes((stopError as Error).message, "exited unexpectedly"); + } finally { + await finalStop?.catch(() => undefined); + await bundler.stop().catch(() => undefined); + __resetServiceRecoveryForTests(); + try { + await bundler.stop(); + } finally { + observation.restore(); + } + } + }); + it("latches exhaustion when stop observes a closed lost service", async () => { const observation = observeEsbuildServices(); const { services } = observation; diff --git a/extensions/ext-bundler-esbuild/src/esbuild-bundler.ts b/extensions/ext-bundler-esbuild/src/esbuild-bundler.ts index d666d7a736..b04773a7f1 100644 --- a/extensions/ext-bundler-esbuild/src/esbuild-bundler.ts +++ b/extensions/ext-bundler-esbuild/src/esbuild-bundler.ts @@ -816,8 +816,10 @@ export class EsbuildBundler implements Bundler { uninstallServiceLossSpawnGuard(); throw error; } + let chargedLostService = false; if (esbuildServiceLost) { remainingServiceRestarts -= 1; + chargedLostService = true; uninstallServiceLossSpawnGuard(); } @@ -825,7 +827,7 @@ export class EsbuildBundler implements Bundler { const trackedService = esbuildService; if ( trackedService && !trackedService.expectedClose && !isLiveService(trackedService) && - remainingServiceRestarts <= 0 + remainingServiceRestarts <= 0 && !chargedLostService ) { // A dead managed child within the restart budget is a crash, which a // stop resets anyway; only an exhausted budget still means giving up. From 5c3e5e27f0fe121bbbf4bc528bede539680aab0c Mon Sep 17 00:00:00 2001 From: Koji Wakayama Date: Sat, 15 Aug 2026 01:09:00 +0200 Subject: [PATCH 2/2] Keep esbuild tests on shared assertions The follow-up recovery tests must use the repository assertion surface so Deno and Node test environments stay aligned. This changes only the assertion import and leaves the recovery behavior untouched. Constraint: Codex review requires new assertions to come from #veryfront/testing/assert.ts rather than @std/assert. Confidence: high Scope-risk: narrow Tested: PATH=/tmp/deno-2.7.7-aarch64-apple-darwin:/opt/homebrew/bin:/opt/homebrew/sbin:/usr/local/bin:/System/Cryptexes/App/usr/bin:/usr/bin:/bin:/usr/sbin:/sbin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/local/bin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/bin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/appleinternal/bin:/opt/pmk/env/global/bin:/Library/Apple/usr/bin:/Applications/VMware Fusion.app/Contents/Public:/opt/homebrew/lib/node_modules/@openai/codex/node_modules/@openai/codex-darwin-arm64/vendor/aarch64-apple-darwin/codex-path:/Users/kojiwakayama/.codex/tmp/arg0/codex-arg0xChlGW:/private/tmp/claude-501/-Users-kojiwakayama-Documents-CODE-veryfront-platform-veryfront-code/9f9d23dd-dfdc-4463-9a99-bd5a663046c9/scratchpad/idx09/deno277/bin:/private/tmp/claude-501/-Users-kojiwakayama-Documents-CODE-veryfront-platform-veryfront-code/9f9d23dd-dfdc-4463-9a99-bd5a663046c9/scratchpad/idx13/deno277/bin:/private/tmp/claude-501/-Users-kojiwakayama-Documents-CODE-veryfront-platform-veryfront-code/9f9d23dd-dfdc-4463-9a99-bd5a663046c9/scratchpad/idx26-deno277/bin:/Users/kojiwakayama/Documents/CODE/agent-lab/.venv/bin:/Users/kojiwakayama/.veryfront/bin:/Users/kojiwakayama/.opencode/bin:/Users/kojiwakayama/.local/bin:/Users/kojiwakayama/.antigravity/antigravity/bin:/opt/homebrew/share/google-cloud-sdk/bin:/opt/homebrew/opt/openjdk/bin:/Users/kojiwakayama/.bun/bin:/Users/kojiwakayama/.krew/bin:/Applications/Visual Studio Code.app/Contents/Resources/app/bin:/Users/kojiwakayama/.nvm/versions/node/v24.18.0/bin:/Users/kojiwakayama/.cargo/bin:/Applications/Docker.app/Contents/Resources/bin/:/Users/kojiwakayama/.lmstudio/bin:/Applications/Warp.app/Contents/Resources/bin deno check extensions/ext-bundler-esbuild/src/esbuild-bundler.test.ts Tested: git diff --check Not-tested: Focused esbuild-bundler test completes assertions but exits with a pre-existing pending-promise teardown error on this machine; same failure reproduces with the original @std/assert import. --- extensions/ext-bundler-esbuild/src/esbuild-bundler.test.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/extensions/ext-bundler-esbuild/src/esbuild-bundler.test.ts b/extensions/ext-bundler-esbuild/src/esbuild-bundler.test.ts index 25fc5bdce7..d3015865f6 100644 --- a/extensions/ext-bundler-esbuild/src/esbuild-bundler.test.ts +++ b/extensions/ext-bundler-esbuild/src/esbuild-bundler.test.ts @@ -5,7 +5,12 @@ * @module extensions/ext-bundler-esbuild/esbuild-bundler.test */ -import { assertEquals, assertExists, assertRejects, assertStringIncludes } from "@std/assert"; +import { + assertEquals, + assertExists, + assertRejects, + assertStringIncludes, +} from "#veryfront/testing/assert.ts"; import { afterEach, beforeEach, describe, it } from "#veryfront/testing/bdd.ts"; import { createRequire } from "node:module"; import type { BuildContext } from "veryfront/extensions/bundler";