diff --git a/scripts/check-runtime.ts b/scripts/check-runtime.ts index fc92590..1c20e7b 100755 --- a/scripts/check-runtime.ts +++ b/scripts/check-runtime.ts @@ -8,6 +8,7 @@ import { fileURLToPath, pathToFileURL } from "node:url"; import { formatVersionOutput, readDistributionVersion } from "../src/launcher.ts"; import { patchRuntimeContextCacheFromParts, + patchRuntimeGoalFailurePause, patchRuntimeLoginModelDefaults, supportsMultiMessageFileRewind } from "./sync-runtime.ts"; @@ -36,6 +37,7 @@ if (packageManifest.dependencies?.["playwright-core"] !== "1.59.1" const runtimeSource = await Bun.file(runtime).text(); if (patchRuntimeLoginModelDefaults(runtimeSource) !== runtimeSource || patchRuntimeContextCacheFromParts(runtimeSource) !== runtimeSource + || patchRuntimeGoalFailurePause(runtimeSource) !== runtimeSource || !runtimeSource.includes('"plugin://"') || !runtimeSource.includes('return await import("playwright-core")') || !runtimeSource.includes('pluginsReferenceCatalog:"plugins/referenceCatalog"') diff --git a/scripts/sync-runtime.ts b/scripts/sync-runtime.ts index 310abb4..1917f41 100755 --- a/scripts/sync-runtime.ts +++ b/scripts/sync-runtime.ts @@ -295,6 +295,21 @@ export function patchRuntimeTerminalToolProjection(runtime: string): string { return patched; } +/** Pause an active goal when its continuation turn fails instead of leaving it active. */ +export function patchRuntimeGoalFailurePause(runtime: string): string { + const alreadyPatchedPattern = /finishTargetTurnAccounting\(\{[^{}]*?status:"paused",traceContext:/u; + if (alreadyPatchedPattern.test(runtime)) return runtime; + + const failureStatusPattern = /(finishTargetTurnAccounting\(\{[^{}]*?status:)[A-Za-z_$][\w$]*\.type===[A-Za-z_$][\w$]*\.TurnCancelled\?"paused":void 0(,traceContext:)/u; + if (!failureStatusPattern.test(runtime)) { + throw new Error("ZCode runtime is incompatible with the goal failure pause patch."); + } + return runtime.replace( + failureStatusPattern, + '$1"paused"$2' + ); +} + /** Exclude foreground Agent calls from the TUI background task projection. */ export function patchRuntimeBackgroundTaskProjection(runtime: string): string { const filteredProjectionPattern = /runtimeTaskRegistry\?\.all\?\.\(\)\?\?\{\}\)\.filter\(([A-Za-z_$][\w$]*)=>\1\.isBackgrounded===!0\)\.map\(/u; @@ -822,7 +837,9 @@ async function installTuiBridge(nextVendor: string): Promise { patchRuntimeAgentAutoBackground( patchRuntimeDetachedAgentLifecycle( patchRuntimeTerminalToolProjection( - patchRuntimeBackgroundTaskProjection(patchRuntimeTuiBridge(runtime)) + patchRuntimeGoalFailurePause( + patchRuntimeBackgroundTaskProjection(patchRuntimeTuiBridge(runtime)) + ) ) ) ) diff --git a/test/sync-runtime.test.ts b/test/sync-runtime.test.ts index 4e3a85c..47ac169 100644 --- a/test/sync-runtime.test.ts +++ b/test/sync-runtime.test.ts @@ -9,6 +9,7 @@ import { patchRuntimeBackgroundTaskProjection, patchRuntimeContextCacheFromParts, patchRuntimeDetachedAgentLifecycle, + patchRuntimeGoalFailurePause, patchRuntimeLoginModelDefaults, patchRuntimeOAuthHttpErrors, patchRuntimeTerminalToolProjection, @@ -712,4 +713,35 @@ describe("runtime synchronization", () => { expect(patchRuntimeTerminalToolProjection(patched)).toBe(patched); expect(() => patchRuntimeTerminalToolProjection("incompatible runtime")).toThrow(/incompatible/); }); + + test("pauses active goals when a continuation turn fails", async () => { + const runtime = [ + "async function execute(e){", + "await this.finishTargetTurnAccounting({endedAtMs:Date.now(),inputID:i,startedTarget:t,status:e.type===CoreErrorType.TurnCancelled?\"paused\":void 0,traceContext:c});", + "}" + ].join(""); + const patched = patchRuntimeGoalFailurePause(runtime); + const statuses: unknown[] = []; + const execute = new Function( + "CoreErrorType", + "i", + "t", + "c", + `${patched};return execute;` + )({ TurnCancelled: "cancelled" }, "input", {}, {}) as ( + this: { finishTargetTurnAccounting: (input: { status?: string }) => Promise }, + error: { type: string } + ) => Promise; + + await execute.call({ + finishTargetTurnAccounting: async (input) => { + statuses.push(input.status); + } + }, { type: "model_request_failed" }); + + expect(patched).toContain('startedTarget:t,status:"paused",traceContext:c'); + expect(statuses).toEqual(["paused"]); + expect(patchRuntimeGoalFailurePause(patched)).toBe(patched); + expect(() => patchRuntimeGoalFailurePause("incompatible runtime")).toThrow(/incompatible/); + }); });