From c6cfe37eae28cde6a497a731639f9cbed111b2a0 Mon Sep 17 00:00:00 2001 From: Kingsword Date: Wed, 23 Sep 2026 19:29:30 +0800 Subject: [PATCH] refactor(runtime): extract typed TUI background task bridge --- docs/DEVELOPMENT.md | 11 +++ scripts/check-runtime.ts | 19 +--- scripts/sync-runtime.ts | 55 +++--------- src/runtime-background-restore.ts | 129 +++++++++++++++++++++++++++ src/runtime-config-bridge.ts | 2 + src/runtime-tui-bridge.ts | 142 ++++++++++++++++++++++++++++++ test/runtime-tui-bridge.test.ts | 100 +++++++++++++++++++++ test/sync-runtime.test.ts | 45 ++++------ 8 files changed, 417 insertions(+), 86 deletions(-) create mode 100644 src/runtime-background-restore.ts create mode 100644 src/runtime-tui-bridge.ts create mode 100644 test/runtime-tui-bridge.test.ts diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index ca2f4a3..f887596 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -25,6 +25,17 @@ without a manual build step. ## Validation +Runtime TUI projection and background-agent messaging live in +`src/runtime-tui-bridge.ts`; persisted task recovery lives in +`src/runtime-background-restore.ts`. Both compile into the existing +`vendor/cli-config.cjs` helper. Bundle patches capture the active app and delegate +to these functions. Recovery reads optional metadata asynchronously and shares +one in-flight restoration per app, so a concurrent send waits for registration. +The runtime continues to own task lifecycle, registry state and message routing; +the helper merges read models and preserves the existing restore/stop/send order. +`test/runtime-tui-bridge.test.ts` tests these operations directly, while +`test/sync-runtime.test.ts` exercises the injected calls and patch idempotency. + Run all validation layers: ```bash diff --git a/scripts/check-runtime.ts b/scripts/check-runtime.ts index 004cc3b..a2590eb 100755 --- a/scripts/check-runtime.ts +++ b/scripts/check-runtime.ts @@ -109,38 +109,25 @@ if (patchRuntimeLoginModelDefaults(runtimeSource) !== runtimeSource || !runtimeSource.includes(".setMode=async") || !runtimeSource.includes(".setPlanEnabled=async") || !runtimeSource.includes(".readExecutionState=async") - || !runtimeSource.includes("...$zExecutionState") || !runtimeSource.includes(".readSessionModel=async") || !runtimeSource.includes(".listSkills=async()=>await") || !runtimeSource.includes(".subscribeSessionEvents=") || !runtimeSource.includes(".sendBackgroundTaskMessage=async") - || !runtimeSource.includes("backgroundTaskDetails") + || !runtimeSource.includes(".restoreTuiBackgroundTasks(") + || !runtimeSource.includes(".readTuiRuntimeProjection(") + || !runtimeSource.includes(".sendTuiBackgroundTaskMessage(") || !runtimeSource.includes(".$zRestorePersistedBackgroundTasks=async $zApp=>") || !runtimeSource.includes(".$zRestorePersistedBackgroundTasks?.(") - || !runtimeSource.includes("$zApp.loadSessionTranscript?.()") - || !runtimeSource.includes('catch{$zApp.$zRestoredBackgroundTasksSession=void 0}') - || !runtimeSource.includes('.$zRestoredBackgroundTasksLog=[]') - || !runtimeSource.includes('restoredBackgroundTasks:Array.isArray(') - || !runtimeSource.includes('childSessionId:"sess_subagent_"+$zAgentId') - || !runtimeSource.includes('$zOutput.includes("Async agent launched successfully.")') - || !runtimeSource.includes('.addAttachment?.("task_status"') - || !runtimeSource.includes("error:typeof $zMetadata.error") || !runtimeSource.includes(".$zSendInputWithoutBackgroundRestore=") - || !runtimeSource.includes("s[$zIndex]={...s[$zIndex]") - || !runtimeSource.includes(".$zRestorePersistedBackgroundTasks?.(t)") - || !runtimeSource.includes('error:o.status==="running"?null:') - || !runtimeSource.includes('completedAt:o.status==="running"?null:') || (patchEnabled("agent-auto-background") && !runtimeSource.includes("autoBackgroundMs:this.config.subagents?.autoBackgroundMs??1e3,outputRootDir:")) || (patchEnabled("detached-agent-lifecycle") && runtimeSource.split("Detached background agent lifecycle failed").length < 3) - || !runtimeSource.includes('if(e?.restart===!0&&o.status==="running")') || !runtimeSource.includes('e?.waitForIdle===!0&&t.runtime?.getActiveForegroundExecutionId') || (patchEnabled("terminal-tool-projection") && !runtimeSource.includes('status:"idle",currentTurnId:void 0,activeToolCalls:[],totalTokenCount:')) || (patchEnabled("terminal-tool-projection") && !runtimeSource.includes('status:"error",currentTurnId:void 0,activeToolCalls:[],lastError:')) - || !/runtimeTaskRegistry\?\.all\?\.\(\)\?\?\{\}\)\.filter\(([A-Za-z_$][\w$]*)=>\1\.isBackgrounded===!0\)\.map\(/u.test(runtimeSource) || !supportsMultiMessageFileRewind(runtimeSource) || !/messageId:[A-Za-z_$][\w$]*\.info\.id,role:"user"/u.test(runtimeSource) || !/messageId:[A-Za-z_$][\w$]*\.info\.id,role:"agent"/u.test(runtimeSource) diff --git a/scripts/sync-runtime.ts b/scripts/sync-runtime.ts index 0428818..07acfae 100755 --- a/scripts/sync-runtime.ts +++ b/scripts/sync-runtime.ts @@ -521,20 +521,11 @@ export function patchRuntimeTuiBridge(runtime: string): string { const sessionEventsOptionPattern = /subscribeSessionEvents:[A-Za-z_$][\w$]*\.subscribeSessionEvents/u; const taskMessageBridgePattern = /\.sendBackgroundTaskMessage=async [A-Za-z_$][\w$]*=>/u; const taskMessageOptionPattern = /sendBackgroundTaskMessage:[A-Za-z_$][\w$]*\.sendBackgroundTaskMessage/u; - const taskMessageRestartMarker = "e?.restart===!0"; + const taskMessageHelperMarker = ".sendTuiBackgroundTaskMessage("; + const projectionHelperMarker = ".readTuiRuntimeProjection("; const backgroundRestoreMarker = ".$zRestorePersistedBackgroundTasks=async $zApp=>"; - const backgroundTasksMergeMarker = "for(let $zDetail of r)"; - const backgroundRestoreSourceMarker = "$zApp.loadSessionTranscript?.()"; - const backgroundRestoreRetryMarker = "catch{$zApp.$zRestoredBackgroundTasksSession=void 0}"; - const backgroundRestoreLogMarker = ".$zRestoredBackgroundTasksLog=[]"; - const backgroundTasksRestoredFieldMarker = "restoredBackgroundTasks:Array.isArray("; - const backgroundRestoreModelTextMarker = 'childSessionId:"sess_subagent_"+$zAgentId'; - const backgroundRestoreAsyncModelTextMarker = '$zOutput.includes("Async agent launched successfully.")'; - const backgroundRestoreReminderMarker = '.addAttachment?.("task_status"'; - const backgroundRestoreMetadataErrorMarker = "error:typeof $zMetadata.error"; + const backgroundRestoreHelperMarker = ".restoreTuiBackgroundTasks("; const backgroundRestoreBeforeSendMarker = ".$zSendInputWithoutBackgroundRestore="; - const backgroundTasksReplaceMarker = "s[$zIndex]={...s[$zIndex]"; - const taskMessageRestoreMarker = ".$zRestorePersistedBackgroundTasks?.(t)"; const interruptTurnMarker = ".interruptTurn=async e=>"; const interruptWaitForIdleMarker = "e?.waitForIdle===!0"; const queuedInputPromotionMarker = "r?.pendingInputReservationId??r?.queryId??"; @@ -549,7 +540,7 @@ export function patchRuntimeTuiBridge(runtime: string): string { && runtime.includes(".readGoal=async()=>await(await") && runtime.includes(".readTodos=async()=>await(await") && runtime.includes(".readRuntimeProjection=async()=>") - && runtime.includes("backgroundTaskDetails") + && runtime.includes(".readTuiRuntimeProjection(") && runtime.includes(".readSessionUsage=async()=>await(await") && runtime.includes(".cancelBackgroundTask=async") && runtime.includes(".previewFileRewind=async e=>") @@ -586,7 +577,6 @@ export function patchRuntimeTuiBridge(runtime: string): string { && (!cliModeOverrideBridge.test(runtime) || runtime.includes("$zTuiBridge.setMode=async")) && runtime.includes(".readExecutionState=async") && runtime.includes(".setPlanEnabled=async") - && runtime.includes("...$zExecutionState") && transientModelBridgePattern.test(runtime) && transientModelOptionPattern.test(runtime) && runtime.includes(sessionModelStateMarker) @@ -594,21 +584,11 @@ export function patchRuntimeTuiBridge(runtime: string): string { && sessionEventsOptionPattern.test(runtime) && taskMessageBridgePattern.test(runtime) && taskMessageOptionPattern.test(runtime) - && runtime.includes(taskMessageRestartMarker) + && runtime.includes(taskMessageHelperMarker) && runtime.includes(projectionBridgeMarker) + && runtime.includes(backgroundRestoreHelperMarker) && runtime.includes(backgroundRestoreMarker) - && runtime.includes(backgroundTasksMergeMarker) - && runtime.includes(backgroundRestoreSourceMarker) - && runtime.includes(backgroundRestoreRetryMarker) - && runtime.includes(backgroundRestoreLogMarker) - && runtime.includes(backgroundTasksRestoredFieldMarker) - && runtime.includes(backgroundRestoreModelTextMarker) - && runtime.includes(backgroundRestoreAsyncModelTextMarker) - && runtime.includes(backgroundRestoreReminderMarker) - && runtime.includes(backgroundRestoreMetadataErrorMarker) && runtime.includes(backgroundRestoreBeforeSendMarker) - && runtime.includes(backgroundTasksReplaceMarker) - && runtime.includes(taskMessageRestoreMarker) && runtime.includes(".loadSessionContextMessages=async()=>await(await") && /loadSessionContextMessages:[A-Za-z_$][\w$]*\.loadSessionContextMessages/u.test(runtime); if (alreadyPatched) return runtime; @@ -724,13 +704,10 @@ export function patchRuntimeTuiBridge(runtime: string): string { // resume-from-child-session path (sendMessage on a stopped local_agent task), // so re-registering spawn records from the already active-branch-filtered // transcript is enough to make /tasks and /tasks resume work again. - const backgroundRestoreAssignment = `${bridge}.$zRestorePersistedBackgroundTasks=async $zApp=>{let $zRuntime=$zApp?.runtime,$zRegistry=$zRuntime?.runtimeTaskRegistry;if(!$zRegistry?.register||typeof $zApp?.sessionId!="string"||$zApp.sessionId===$zApp.$zRestoredBackgroundTasksSession)return;$zApp.$zRestoredBackgroundTasksSession=$zApp.sessionId,$zApp.$zRestoredBackgroundTasksLog=[];try{let $zMessages=await $zApp.loadSessionTranscript?.()??[],$zRestored=[];for(let $zMessage of $zMessages){let $zParts=Array.isArray($zMessage?.parts)?$zMessage.parts:[];for(let $zPart of $zParts){if($zPart?.type!=="tool")continue;let $zToolName=typeof $zPart.toolName==="string"?$zPart.toolName:typeof $zPart.tool==="string"?$zPart.tool:"";$zToolName=$zToolName.trim().toLowerCase();if($zToolName!=="agent"&&$zToolName!=="subagent"&&$zToolName!=="task")continue;let $zInput=$zPart.input??$zPart.state?.input,$zOutput=typeof $zPart.output==="string"?$zPart.output:$zPart.state?.output;if(typeof $zOutput!=="string"||$zOutput.trim().length===0)continue;let $zSpawn;try{$zSpawn=JSON.parse($zOutput)}catch{}if(!$zSpawn||typeof $zSpawn!=="object"||Array.isArray($zSpawn)){let $zAgentId=/(?:^|\\n)agentId:\\s*([^\\s(]+)/u.exec($zOutput)?.[1],$zOutputFile=/(?:^|\\n)output_file:\\s*([^\\r\\n]+)/u.exec($zOutput)?.[1]?.trim();if(typeof $zAgentId!=="string"||$zAgentId.length===0)continue;$zSpawn={status:"async_launched",agentId:$zAgentId,agentType:typeof $zInput?.subagent_type==="string"&&$zInput.subagent_type.length>0?$zInput.subagent_type:typeof $zInput?.agentType==="string"&&$zInput.agentType.length>0?$zInput.agentType:void 0,childSessionId:"sess_subagent_"+$zAgentId,description:typeof $zInput?.description==="string"&&$zInput.description.length>0?$zInput.description:void 0,prompt:typeof $zInput?.prompt==="string"&&$zInput.prompt.length>0?$zInput.prompt:void 0,...$zOutputFile?{outputFile:$zOutputFile}:{}}}if(($zSpawn.status!=="async_launched"&&$zSpawn.status!=="backgrounded")||typeof $zSpawn.agentId!=="string"||$zSpawn.agentId.length===0)continue;let $zMetadata;if(typeof $zSpawn.outputFile==="string"&&$zSpawn.outputFile.length>0)try{let $zBuiltin=process.getBuiltinModule;if(typeof $zBuiltin==="function"){let $zFs=$zBuiltin.call(process,"node:fs"),$zPath=$zBuiltin.call(process,"node:path");$zMetadata=JSON.parse($zFs.readFileSync($zPath.join($zPath.dirname($zSpawn.outputFile),"metadata.json"),"utf8"))}}catch{}if($zMetadata&&typeof $zMetadata==="object"&&!Array.isArray($zMetadata)&&(typeof $zMetadata.agentId!=="string"||$zMetadata.agentId===$zSpawn.agentId)&&(typeof $zMetadata.parentSessionId!=="string"||$zMetadata.parentSessionId===$zApp.sessionId))$zSpawn={...$zSpawn,agentType:typeof $zMetadata.profileId==="string"&&$zMetadata.profileId.length>0?$zMetadata.profileId:$zSpawn.agentType,childSessionId:typeof $zMetadata.childSessionId==="string"&&$zMetadata.childSessionId.length>0?$zMetadata.childSessionId:$zSpawn.childSessionId,description:typeof $zMetadata.description==="string"&&$zMetadata.description.length>0?$zMetadata.description:$zSpawn.description,outputFile:typeof $zMetadata.outputFile==="string"&&$zMetadata.outputFile.length>0?$zMetadata.outputFile:$zSpawn.outputFile,prompt:typeof $zMetadata.prompt==="string"&&$zMetadata.prompt.length>0?$zMetadata.prompt:$zSpawn.prompt,error:typeof $zMetadata.error==="string"&&$zMetadata.error.length>0?$zMetadata.error:$zSpawn.error};if(typeof $zSpawn.childSessionId!=="string"||$zSpawn.childSessionId.length===0)$zSpawn.childSessionId="sess_subagent_"+$zSpawn.agentId;if($zRegistry.get?.($zSpawn.agentId))continue;let $zStatus="stopped";if($zMetadata?.status==="completed")$zStatus="completed";else if($zMetadata?.status==="failed")$zStatus="failed";let $zTask={taskId:$zSpawn.agentId,agentId:$zSpawn.agentId,agentType:typeof $zSpawn.agentType==="string"&&$zSpawn.agentType.length>0?$zSpawn.agentType:"general-purpose",childSessionId:$zSpawn.childSessionId,description:typeof $zSpawn.description==="string"&&$zSpawn.description.length>0?$zSpawn.description:void 0,error:typeof $zSpawn.error==="string"&&$zSpawn.error.length>0?$zSpawn.error:void 0,isBackgrounded:!0,outputFile:typeof $zSpawn.outputFile==="string"&&$zSpawn.outputFile.length>0?$zSpawn.outputFile:void 0,parentToolCallId:typeof $zPart.toolCallId==="string"&&$zPart.toolCallId.length>0?$zPart.toolCallId:typeof $zPart.callID==="string"&&$zPart.callID.length>0?$zPart.callID:void 0,parentSessionId:$zApp.sessionId,prompt:typeof $zSpawn.prompt==="string"&&$zSpawn.prompt.length>0?$zSpawn.prompt:void 0,startedAt:$zPart.state?.time?.start,status:$zStatus,taskType:"local_agent",type:"local_agent"};$zRegistry.register($zTask),$zRestored.push($zTask)}}if($zRestored.length>0){let $zTaskIds=$zRestored.slice(-32).map($zTask=>"- "+$zTask.taskId+" ("+$zTask.status+")").join("\\n").slice(0,4e3);$zRuntime?.messageHistory?.addAttachment?.("task_status","Background agent tasks from this resumed session have been restored and are available again.\\nEarlier TaskOutput errors saying \\"No task found\\" occurred before restoration and are stale.\\nUse TaskOutput to collect results or SendMessage to continue a task. Do not assume these tasks were lost.\\nRestored task IDs:\\n"+$zTaskIds),$zApp.$zRestoredBackgroundTasksLog=[...($zApp.$zRestoredBackgroundTasksLog??[]),...$zRestored].slice(-64)}}catch{$zApp.$zRestoredBackgroundTasksSession=void 0}}`; - const guardedBackgroundRestoreAssignment = backgroundRestoreAssignment.replace( - 'if(!$zSpawn||typeof $zSpawn!=="object"||Array.isArray($zSpawn)){let $zAgentId=', - 'if(!$zSpawn||typeof $zSpawn!=="object"||Array.isArray($zSpawn)){if(!$zOutput.includes("Async agent launched successfully."))continue;let $zAgentId=' - ); - const projectionAssignment = `${bridge}.readRuntimeProjection=async()=>{let $zRuntimeProjectionBridge=await ${getApp}();let $zExecutionState=await $zRuntimeProjectionBridge.readExecutionState();await ${bridge}.$zRestorePersistedBackgroundTasks?.($zRuntimeProjectionBridge);let t=await $zRuntimeProjectionBridge.runtime?.getProjection?.();if(!t)return null;let r=Object.values($zRuntimeProjectionBridge.runtime?.runtimeTaskRegistry?.all?.()??{}).filter(o=>o.isBackgrounded===!0).map(o=>({taskId:o.taskId,taskKind:o.taskType??o.type,agentId:o.agentId,agentType:o.agentType,childSessionId:o.childSessionId,parentSessionId:o.parentSessionId,parentToolCallId:o.parentToolCallId,turnId:o.turnId,prompt:o.prompt,error:o.status==="running"?null:o.error instanceof Error?o.error.message:typeof o.error==="string"?o.error:void 0,outputPath:o.outputFile,status:o.status,description:o.description,startedAt:o.startedAt,completedAt:o.status==="running"?null:o.completedAt,cancelRequestedAt:o.status==="running"?null:o.cancelRequestedAt,blocked:o.status==="running"?null:o.blocked,blockedReason:o.status==="running"?null:o.blockedReason,cancellable:(o.taskType??o.type)==="local_agent"?o.status==="running":o.cancellable}));let s=Array.isArray(t.backgroundTasks)?t.backgroundTasks.slice():[];for(let $zDetail of r){let $zIndex=s.findIndex($zExisting=>$zExisting.taskId===$zDetail.taskId);if($zIndex<0)s.push($zDetail);else s[$zIndex]={...s[$zIndex],...Object.fromEntries(Object.entries($zDetail).filter(([,v])=>v!==void 0))}}return{...t,...$zExecutionState,backgroundTasks:s,backgroundTaskDetails:r,restoredBackgroundTasks:Array.isArray($zRuntimeProjectionBridge.$zRestoredBackgroundTasksLog)?$zRuntimeProjectionBridge.$zRestoredBackgroundTasksLog:[]}}`; - const taskMessageAssignment = `${bridge}.sendBackgroundTaskMessage=async e=>{let t=await ${getApp}();await ${bridge}.$zRestorePersistedBackgroundTasks?.(t);let r=t.runtime,o=r?.runtimeTaskRegistry?.get?.(e?.taskId);if(!o&&typeof t.sessionId==="string"){t.$zRestoredBackgroundTasksSession=void 0;await ${bridge}.$zRestorePersistedBackgroundTasks?.(t);o=r?.runtimeTaskRegistry?.get?.(e?.taskId)}if(!r?.subagentPort?.sendMessage)throw new Error("Background agent messaging is unavailable in this runtime.");if(!o||(o.type??o.taskType)!=="local_agent")throw new Error("The selected task is not a local agent.");if(typeof e?.message!=="string"||!e.message.trim())throw new Error("Enter a message for the background agent.");let n=e.message.trim().slice(0,2e4),i=(typeof e.summary==="string"?e.summary:n).replace(/\\s+/g," ").trim().slice(0,200);if(e?.restart===!0&&o.status==="running"){if(!r.subagentPort.stopTask)throw new Error("Background agent restart is unavailable in this runtime.");await r.subagentPort.stopTask(e.taskId),o=r.runtimeTaskRegistry?.get?.(e.taskId);if(!o)throw new Error("The background agent stopped but could not be restored.")}return await r.subagentPort.sendMessage({sessionId:o.parentSessionId??r.getSessionId?.(),turnId:o.turnId??"tui-task-message",parentToolCallId:o.parentToolCallId??"tui-task-message",to:o.agentId??e.taskId,summary:i,message:n,workingDirectory:o.workingDirectory??r.workingDirectory,workspaceRoot:o.workspaceRoot??r.workingDirectory,trace:o.traceContext??r.rootTraceContext})}`; + const helper = 'require(require("node:path").join(__dirname,"cli-config.cjs"))'; + const guardedBackgroundRestoreAssignment = `${bridge}.$zRestorePersistedBackgroundTasks=async $zApp=>await ${helper}.restoreTuiBackgroundTasks($zApp)`; + const projectionAssignment = `${bridge}.readRuntimeProjection=async()=>{let $zRuntimeProjectionBridge=await ${getApp}();return await ${helper}.readTuiRuntimeProjection($zRuntimeProjectionBridge,${bridge})}`; + const taskMessageAssignment = `${bridge}.sendBackgroundTaskMessage=async e=>await ${helper}.sendTuiBackgroundTaskMessage(await ${getApp}(),${bridge},e)`; if (!listSkillsBridgePattern.test(patched)) { const listSkillsFactory = /listSkills:[A-Za-z_$][\w$]*\(\(\)=>([A-Za-z_$][\w$]*)\(([A-Za-z_$][\w$]*)\),"listSkills"\)/u .exec(patched); @@ -765,8 +742,7 @@ export function patchRuntimeTuiBridge(runtime: string): string { assignments.push(`${bridge}.readTodos=async()=>await(await ${getApp}()).readTodos?.()??[]`); } if (!patched.includes(projectionBridgeMarker) || !patched.includes(backgroundRestoreMarker) - || !patched.includes(backgroundTasksMergeMarker) || !patched.includes(backgroundTasksReplaceMarker) - || !patched.includes(backgroundTasksRestoredFieldMarker) || !patched.includes("...$zExecutionState")) { + || !patched.includes(projectionHelperMarker)) { const existingProjectionStart = `${bridge}.readRuntimeProjection=async()=>`; const existingProjectionIndex = patched.indexOf(existingProjectionStart); if (existingProjectionIndex >= 0) { @@ -779,12 +755,7 @@ export function patchRuntimeTuiBridge(runtime: string): string { assignments.push(projectionAssignment); } } - if (!patched.includes(backgroundRestoreMarker) || !patched.includes(backgroundRestoreSourceMarker) - || !patched.includes(backgroundRestoreRetryMarker) || !patched.includes(backgroundRestoreLogMarker) - || !patched.includes(backgroundRestoreModelTextMarker) - || !patched.includes(backgroundRestoreAsyncModelTextMarker) - || !patched.includes(backgroundRestoreReminderMarker) - || !patched.includes(backgroundRestoreMetadataErrorMarker)) { + if (!patched.includes(backgroundRestoreHelperMarker)) { const existingRestoreStart = patched.indexOf(`${bridge}.$zRestorePersistedBackgroundTasks=async $zApp=>`); if (existingRestoreStart >= 0) { const existingRestoreEnd = patched.indexOf(`,${bridge}.`, existingRestoreStart); @@ -846,7 +817,7 @@ export function patchRuntimeTuiBridge(runtime: string): string { } if (!taskMessageBridgePattern.test(patched)) { assignments.push(taskMessageAssignment); - } else if (!patched.includes(taskMessageRestartMarker) || !patched.includes(taskMessageRestoreMarker)) { + } else if (!patched.includes(taskMessageHelperMarker)) { const existingTaskMessageStart = patched.indexOf(`${bridge}.sendBackgroundTaskMessage=async e=>`); const existingTaskMessageEnd = existingTaskMessageStart < 0 ? -1 diff --git a/src/runtime-background-restore.ts b/src/runtime-background-restore.ts new file mode 100644 index 0000000..38f37db --- /dev/null +++ b/src/runtime-background-restore.ts @@ -0,0 +1,129 @@ +import { readFile } from "node:fs/promises"; +import { dirname, join } from "node:path"; +import type { RuntimeTask, RuntimeTuiApp } from "./runtime-tui-bridge.ts"; + +type RecordValue = Record; +const maximumReminderTasks = 32; +const maximumReminderLength = 4_000; +const maximumRestoredTasks = 64; + +function record(value: unknown): RecordValue | undefined { + return value !== null && typeof value === "object" && !Array.isArray(value) ? value as RecordValue : undefined; +} + +function text(value: unknown): string | undefined { + return typeof value === "string" && value.length > 0 ? value : undefined; +} + +function parseSpawn(output: string, input: RecordValue | undefined): RecordValue | undefined { + try { + const parsed = record(JSON.parse(output)); + if (parsed) return parsed; + } catch { + // Older runtimes returned a formatted launch result rather than JSON. + } + if (!output.includes("Async agent launched successfully.")) return undefined; + const agentId = /(?:^|\n)agentId:\s*([^\s(]+)/u.exec(output)?.[1]; + if (!agentId) return undefined; + return { + status: "async_launched", agentId, + agentType: text(input?.subagent_type) ?? text(input?.agentType), + childSessionId: `sess_subagent_${agentId}`, + description: text(input?.description), prompt: text(input?.prompt), + outputFile: /(?:^|\n)output_file:\s*([^\r\n]+)/u.exec(output)?.[1]?.trim() + }; +} + +async function readMetadata(outputFile: unknown): Promise { + if (!text(outputFile)) return undefined; + try { + return record(JSON.parse(await readFile(join(dirname(outputFile as string), "metadata.json"), "utf8"))); + } catch { + // Metadata is optional; the persisted launch result still restores a task. + return undefined; + } +} + +function enrichSpawn(spawn: RecordValue, metadata: RecordValue | undefined, sessionId: string): RecordValue { + if (!metadata || typeof metadata.agentId === "string" && metadata.agentId !== spawn.agentId + || typeof metadata.parentSessionId === "string" && metadata.parentSessionId !== sessionId) return spawn; + return { + ...spawn, + agentType: text(metadata.profileId) ?? spawn.agentType, + childSessionId: text(metadata.childSessionId) ?? spawn.childSessionId, + description: text(metadata.description) ?? spawn.description, + outputFile: text(metadata.outputFile) ?? spawn.outputFile, + prompt: text(metadata.prompt) ?? spawn.prompt, + error: text(metadata.error) ?? spawn.error + }; +} + +async function restore(app: RuntimeTuiApp): Promise { + const runtime = app.runtime; + const registry = runtime?.runtimeTaskRegistry; + const sessionId = app.sessionId; + if (!registry?.register || typeof sessionId !== "string" || sessionId === app.$zRestoredBackgroundTasksSession) return; + app.$zRestoredBackgroundTasksSession = sessionId; + app.$zRestoredBackgroundTasksLog = []; + try { + const messages = await app.loadSessionTranscript?.() ?? []; + const restored: RuntimeTask[] = []; + for (const messageValue of messages) { + const parts = record(messageValue)?.parts; + if (!Array.isArray(parts)) continue; + for (const value of parts) { + const part = record(value); + if (part?.type !== "tool") continue; + const tool = (typeof part.toolName === "string" ? part.toolName + : typeof part.tool === "string" ? part.tool : "").trim().toLowerCase(); + if (!["agent", "subagent", "task"].includes(tool)) continue; + const state = record(part.state); + const output = typeof part.output === "string" ? part.output : state?.output; + if (typeof output !== "string" || !output.trim()) continue; + let spawn = parseSpawn(output, record(part.input ?? state?.input)); + if (!spawn || !["async_launched", "backgrounded"].includes(String(spawn.status)) || !text(spawn.agentId)) continue; + const metadata = await readMetadata(spawn.outputFile); + spawn = enrichSpawn(spawn, metadata, sessionId); + const agentId = spawn.agentId as string; + if (registry.get?.(agentId)) continue; + const task: RuntimeTask = { + taskId: agentId, agentId, + agentType: text(spawn.agentType) ?? "general-purpose", + childSessionId: text(spawn.childSessionId) ?? `sess_subagent_${agentId}`, + description: text(spawn.description), error: text(spawn.error), + isBackgrounded: true, outputFile: text(spawn.outputFile), + parentToolCallId: text(part.toolCallId) ?? text(part.callID), + parentSessionId: sessionId, prompt: text(spawn.prompt), + startedAt: record(state?.time)?.start, + status: metadata?.status === "completed" ? "completed" : metadata?.status === "failed" ? "failed" : "stopped", + taskType: "local_agent", type: "local_agent" + }; + registry.register(task); + restored.push(task); + } + } + if (restored.length === 0) return; + const taskIds = restored.slice(-maximumReminderTasks) + .map((task) => `- ${task.taskId} (${task.status})`).join("\n").slice(0, maximumReminderLength); + runtime?.messageHistory?.addAttachment?.("task_status", + "Background agent tasks from this resumed session have been restored and are available again.\n" + + 'Earlier TaskOutput errors saying "No task found" occurred before restoration and are stale.\n' + + "Use TaskOutput to collect results or SendMessage to continue a task. Do not assume these tasks were lost.\n" + + `Restored task IDs:\n${taskIds}`); + app.$zRestoredBackgroundTasksLog = [...(app.$zRestoredBackgroundTasksLog ?? []), ...restored].slice(-maximumRestoredTasks); + } catch { + // Restoration is supplementary. Let the next query retry a failed read. + app.$zRestoredBackgroundTasksSession = undefined; + } +} + +const pendingRestores = new WeakMap>(); + +export async function restoreTuiBackgroundTasks(app: RuntimeTuiApp): Promise { + const pending = pendingRestores.get(app); + if (pending) return pending; + const operation = restore(app); + pendingRestores.set(app, operation); + try { await operation; } + finally { pendingRestores.delete(app); } +} diff --git a/src/runtime-config-bridge.ts b/src/runtime-config-bridge.ts index 7e8962c..c60d63a 100644 --- a/src/runtime-config-bridge.ts +++ b/src/runtime-config-bridge.ts @@ -4,6 +4,8 @@ import { dirname, resolve } from "node:path"; import { cliSettingsPath, legacyCliConfigPath, providerConfigPath, providerMigrationMarkerPath, readDesktopSettings } from "./config-paths.ts"; export { assertSessionModelReady, readSessionModelState } from "./session-model-recovery.ts"; +export { readTuiRuntimeProjection, sendTuiBackgroundTaskMessage } from "./runtime-tui-bridge.ts"; +export { restoreTuiBackgroundTasks } from "./runtime-background-restore.ts"; function record(value: unknown): Record | undefined { return value && typeof value === "object" && !Array.isArray(value) ? value as Record : undefined; diff --git a/src/runtime-tui-bridge.ts b/src/runtime-tui-bridge.ts new file mode 100644 index 0000000..7a2f7a1 --- /dev/null +++ b/src/runtime-tui-bridge.ts @@ -0,0 +1,142 @@ +/** Narrow structural boundary; the extracted runtime remains the state owner. */ +export interface RuntimeTask extends Record { + taskId: string; + type?: string; + taskType?: string; + status?: string; + isBackgrounded?: boolean; + error?: unknown; +} + +export interface RuntimeTuiApp { + sessionId?: string; + $zRestoredBackgroundTasksSession?: string; + $zRestoredBackgroundTasksLog?: unknown[]; + loadSessionTranscript?(): Promise; + readExecutionState(): Promise | unknown; + runtime?: { + getProjection?(): Promise | undefined> | Record | undefined; + getSessionId?(): string; + runtimeTaskRegistry?: { + all?(): Record; + get?(taskId: string): RuntimeTask | undefined; + register?(task: RuntimeTask): unknown; + }; + messageHistory?: { addAttachment?(source: string, text: string): unknown }; + subagentPort?: { + sendMessage?(input: Record): Promise; + stopTask?(taskId: string): Promise; + }; + workingDirectory?: string; + rootTraceContext?: unknown; + }; +} + +export interface RuntimeTuiBridge { + $zRestorePersistedBackgroundTasks?(app: RuntimeTuiApp): Promise; +} + +const maximumTaskMessageLength = 20_000; +const maximumTaskSummaryLength = 200; + +function taskDetail(task: RuntimeTask): Record { + const running = task.status === "running"; + const kind = task.taskType ?? task.type; + return { + taskId: task.taskId, + taskKind: kind, + agentId: task.agentId, + agentType: task.agentType, + childSessionId: task.childSessionId, + parentSessionId: task.parentSessionId, + parentToolCallId: task.parentToolCallId, + turnId: task.turnId, + prompt: task.prompt, + error: running ? null : task.error instanceof Error ? task.error.message + : typeof task.error === "string" ? task.error : undefined, + outputPath: task.outputFile, + status: task.status, + description: task.description, + startedAt: task.startedAt, + completedAt: running ? null : task.completedAt, + cancelRequestedAt: running ? null : task.cancelRequestedAt, + blocked: running ? null : task.blocked, + blockedReason: running ? null : task.blockedReason, + cancellable: kind === "local_agent" ? running : task.cancellable + }; +} + +export async function readTuiRuntimeProjection( + app: RuntimeTuiApp, + bridge: RuntimeTuiBridge +): Promise | null> { + const executionState = await app.readExecutionState(); + await bridge.$zRestorePersistedBackgroundTasks?.(app); + const projection = await app.runtime?.getProjection?.(); + if (!projection) return null; + const details = Object.values(app.runtime?.runtimeTaskRegistry?.all?.() ?? {}) + .filter((task) => task.isBackgrounded === true).map(taskDetail); + const tasks: Array> = Array.isArray(projection.backgroundTasks) + ? projection.backgroundTasks.slice() : []; + for (const detail of details) { + const index = tasks.findIndex((existing) => existing.taskId === detail.taskId); + if (index < 0) tasks.push(detail); + else tasks[index] = { + ...tasks[index], + ...Object.fromEntries(Object.entries(detail).filter(([, value]) => value !== undefined)) + }; + } + return { + ...projection, + ...(executionState && typeof executionState === "object" ? executionState : {}), + backgroundTasks: tasks, + backgroundTaskDetails: details, + restoredBackgroundTasks: Array.isArray(app.$zRestoredBackgroundTasksLog) + ? app.$zRestoredBackgroundTasksLog : [] + }; +} + +export interface BackgroundTaskMessage { + taskId: string; + message: string; + summary?: string; + restart?: boolean; +} + +export async function sendTuiBackgroundTaskMessage( + app: RuntimeTuiApp, + bridge: RuntimeTuiBridge, + input: BackgroundTaskMessage +): Promise { + await bridge.$zRestorePersistedBackgroundTasks?.(app); + const runtime = app.runtime; + let task = runtime?.runtimeTaskRegistry?.get?.(input?.taskId); + if (!task && typeof app.sessionId === "string") { + app.$zRestoredBackgroundTasksSession = undefined; + await bridge.$zRestorePersistedBackgroundTasks?.(app); + task = runtime?.runtimeTaskRegistry?.get?.(input?.taskId); + } + if (!runtime?.subagentPort?.sendMessage) throw new Error("Background agent messaging is unavailable in this runtime."); + if (!task || (task.type ?? task.taskType) !== "local_agent") throw new Error("The selected task is not a local agent."); + if (typeof input?.message !== "string" || !input.message.trim()) throw new Error("Enter a message for the background agent."); + const message = input.message.trim().slice(0, maximumTaskMessageLength); + const summary = (typeof input.summary === "string" ? input.summary : message) + .replace(/\s+/g, " ").trim().slice(0, maximumTaskSummaryLength); + if (input.restart === true && task.status === "running") { + if (!runtime.subagentPort.stopTask) throw new Error("Background agent restart is unavailable in this runtime."); + await runtime.subagentPort.stopTask(input.taskId); + task = runtime.runtimeTaskRegistry?.get?.(input.taskId); + if (!task) throw new Error("The background agent stopped but could not be restored."); + } + return await runtime.subagentPort.sendMessage({ + sessionId: task.parentSessionId ?? runtime.getSessionId?.(), + turnId: task.turnId ?? "tui-task-message", + parentToolCallId: task.parentToolCallId ?? "tui-task-message", + to: task.agentId ?? input.taskId, + summary, + message, + workingDirectory: task.workingDirectory ?? runtime.workingDirectory, + workspaceRoot: task.workspaceRoot ?? runtime.workingDirectory, + trace: task.traceContext ?? runtime.rootTraceContext + }); +} diff --git a/test/runtime-tui-bridge.test.ts b/test/runtime-tui-bridge.test.ts new file mode 100644 index 0000000..e6bb853 --- /dev/null +++ b/test/runtime-tui-bridge.test.ts @@ -0,0 +1,100 @@ +import { expect, test } from "bun:test"; +import { restoreTuiBackgroundTasks } from "../src/runtime-background-restore.ts"; +import { readTuiRuntimeProjection, sendTuiBackgroundTaskMessage, type RuntimeTask, type RuntimeTuiApp } from "../src/runtime-tui-bridge.ts"; + +test("projection merges registry state without mutating persisted entries", async () => { + const original = { taskId: "agent", description: "Saved description", status: "failed", error: "Old failure", completedAt: 10 }; + const app: RuntimeTuiApp = { + readExecutionState: () => ({ mode: "edit", planEnabled: true }), + runtime: { + getProjection: () => ({ backgroundTasks: [original] }), + runtimeTaskRegistry: { all: () => ({ + agent: { taskId: "agent", type: "local_agent", status: "running", isBackgrounded: true }, + foreground: { taskId: "foreground", type: "local_agent", status: "running" } + }) } + } + }; + let restored = false; + const projection = await readTuiRuntimeProjection(app, { $zRestorePersistedBackgroundTasks: async () => { restored = true; } }); + expect(restored).toBeTrue(); + expect(projection).toMatchObject({ mode: "edit", planEnabled: true, backgroundTasks: [{ + taskId: "agent", description: "Saved description", status: "running", error: null, completedAt: null, cancellable: true + }] }); + expect(projection?.backgroundTaskDetails).toHaveLength(1); + expect(original.status).toBe("failed"); +}); + +test("task restart awaits stop and uses refreshed routing and trace data", async () => { + let task: RuntimeTask = { taskId: "agent", type: "local_agent", status: "running" }; + const calls: string[] = []; + let message: Record | undefined; + const app: RuntimeTuiApp = { + readExecutionState: () => ({}), + runtime: { + workingDirectory: "/workspace", + rootTraceContext: { traceId: "root" }, + runtimeTaskRegistry: { get: () => task }, + subagentPort: { + stopTask: async () => { + calls.push("stop"); + task = { ...task, status: "stopped", parentSessionId: "parent", parentToolCallId: "call", agentId: "restored" }; + }, + sendMessage: async (input) => { calls.push("send"); message = input; return { status: "success" }; } + } + } + }; + await sendTuiBackgroundTaskMessage(app, {}, { taskId: "agent", message: "x".repeat(21_000), summary: " Continue\n here ", restart: true }); + expect(calls).toEqual(["stop", "send"]); + expect(message).toMatchObject({ to: "restored", parentToolCallId: "call", sessionId: "parent", summary: "Continue here", trace: { traceId: "root" } }); + expect(String(message?.message)).toHaveLength(20_000); +}); + +test("task messaging retries restoration and rejects unsupported or invalid actions", async () => { + let task: RuntimeTask | undefined; + let restores = 0; + let sends = 0; + const app: RuntimeTuiApp = { + sessionId: "parent", readExecutionState: () => ({}), + runtime: { + runtimeTaskRegistry: { get: () => task }, + subagentPort: { sendMessage: async () => { sends++; return "sent"; } } + } + }; + const bridge = { $zRestorePersistedBackgroundTasks: async () => { + if (++restores === 2) task = { taskId: "agent", type: "local_agent", status: "stopped" }; + } }; + expect(await sendTuiBackgroundTaskMessage(app, bridge, { taskId: "agent", message: "Continue" })).toBe("sent"); + expect(restores).toBe(2); + await expect(sendTuiBackgroundTaskMessage(app, {}, { taskId: "agent", message: " " })).rejects.toThrow("Enter a message"); + task = { taskId: "agent", type: "local_agent", status: "running" }; + await expect(sendTuiBackgroundTaskMessage(app, {}, { taskId: "agent", message: "Continue", restart: true })).rejects.toThrow("restart is unavailable"); + task = { taskId: "agent", type: "local_bash", status: "running" }; + await expect(sendTuiBackgroundTaskMessage(app, {}, { taskId: "agent", message: "Continue" })).rejects.toThrow("not a local agent"); + expect(sends).toBe(1); +}); + +test("concurrent task sends wait for an in-flight transcript restoration", async () => { + let release!: (messages: unknown[]) => void; + let reads = 0; + let sends = 0; + const tasks = new Map(); + const app: RuntimeTuiApp = { + sessionId: "parent", readExecutionState: () => ({}), + loadSessionTranscript: () => { reads++; return new Promise((resolve) => { release = resolve; }); }, + runtime: { + runtimeTaskRegistry: { register: (task) => tasks.set(task.taskId, task), get: (id) => tasks.get(id) }, + subagentPort: { sendMessage: async () => { sends++; return "sent"; } } + } + }; + const restoration = restoreTuiBackgroundTasks(app); + const send = sendTuiBackgroundTaskMessage(app, { $zRestorePersistedBackgroundTasks: restoreTuiBackgroundTasks }, { + taskId: "agent", message: "Continue" + }); + await Promise.resolve(); + expect(sends).toBe(0); + release([{ parts: [{ type: "tool", toolName: "Agent", output: JSON.stringify({ status: "async_launched", agentId: "agent" }) }] }]); + await restoration; + expect(await send).toBe("sent"); + expect(reads).toBe(1); + expect(sends).toBe(1); +}); diff --git a/test/sync-runtime.test.ts b/test/sync-runtime.test.ts index 1a4d934..07d27cb 100644 --- a/test/sync-runtime.test.ts +++ b/test/sync-runtime.test.ts @@ -1,8 +1,10 @@ +import { restoreTuiBackgroundTasks } from "../src/runtime-background-restore.ts"; import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { describe, expect, test } from "bun:test"; +import { readTuiRuntimeProjection, sendTuiBackgroundTaskMessage } from "../src/runtime-tui-bridge.ts"; import { assertSessionModelReady } from "../src/session-model-recovery.ts"; import { @@ -51,6 +53,13 @@ import { syncedReleaseVersion } from "../scripts/release-version.ts"; +function bridgeFunction(...args: string[]): (...values: unknown[]) => unknown { + const body = args.pop()!; + const factory = new Function("require", "__dirname", ...args, body); + return factory.bind(null, (id: string) => id === "node:path" ? { join } + : { readTuiRuntimeProjection, sendTuiBackgroundTaskMessage, restoreTuiBackgroundTasks }, "/fixture"); +} + describe("runtime synchronization", () => { test("adds session recovery at native restore and execution boundaries and can run twice", async () => { const runtime = 'async function app(options){let read=()=>runtime,restore=label(async()=>{let ok=saved&®istry.validateSelection(saved);return read().setSessionModelSelection(ok?.ok?saved:void 0),saved},"restorePersistedModelSelection"),prepare=label(async args=>{await options.prepareUserExecutionBoundary(args)},"preparePromptBoundary");return{restore,prepare,getModelOption:label(model=>lookup(context.providerRegistry,model),"getModelOption")}}'; @@ -791,22 +800,8 @@ describe("runtime synchronization", () => { expect(patched).toContain("E.listSkills=async()=>await H(e)"); expect(patched).toContain("E.readGoal=async()=>await(await S()).readTarget?.()??null"); expect(patched).toContain("E.readTodos=async()=>await(await S()).readTodos?.()??[]"); - expect(patched).toContain("E.readRuntimeProjection=async()=>{let $zRuntimeProjectionBridge=await S();let $zExecutionState=await $zRuntimeProjectionBridge.readExecutionState();await E.$zRestorePersistedBackgroundTasks?.($zRuntimeProjectionBridge);let t=await $zRuntimeProjectionBridge.runtime?.getProjection?.();if(!t)return null;"); - expect(patched).toContain(".filter(o=>o.isBackgrounded===!0).map(o=>"); - expect(patched).toContain("backgroundTaskDetails:r"); expect(patched).toContain("E.$zRestorePersistedBackgroundTasks=async $zApp=>"); - expect(patched).toContain('$zApp.$zRestoredBackgroundTasksSession=$zApp.sessionId'); - expect(patched).toContain("$zApp.loadSessionTranscript?.()"); - expect(patched).toContain("error:typeof $zMetadata.error"); - expect(patched).toContain('$zToolName!=="agent"&&$zToolName!=="subagent"&&$zToolName!=="task"'); - expect(patched).toContain('childSessionId:"sess_subagent_"+$zAgentId'); - expect(patched).toContain('$zOutput.includes("Async agent launched successfully.")'); - expect(patched).toContain('.addAttachment?.("task_status"'); expect(patched).toContain("E.$zSendInputWithoutBackgroundRestore=E.sendInput"); - expect(patched).toContain("s[$zIndex]={...s[$zIndex]"); - expect(patched).toContain("E.$zRestorePersistedBackgroundTasks?.(t)"); - expect(patched).toContain('$zSpawn.status!=="async_launched"&&$zSpawn.status!=="backgrounded"'); - expect(patched).toContain('status:$zStatus,taskType:"local_agent",type:"local_agent"'); expect(patched).toContain('loadSessionContextMessages:a(async()=>await e.sessionStore.messages({sessionID:e.sessionId}),"loadSessionContextMessages")'); expect(patched).toContain('role:"agent",model:{providerId:r.info.providerID,modelId:r.info.modelID}'); expect(patched).not.toContain("$zRuntimeProjectionBridge.loadSessionContextMessages?.()"); @@ -815,9 +810,8 @@ describe("runtime synchronization", () => { expect(patched).toContain("E.cancelBackgroundTask=async e=>await(await S()).cancelBackgroundTask?.(e)??null"); expect(patched).toContain("E.subscribeSessionEvents=e=>{let t=!1,r;S().then(o=>{t||(r=o.runtime?.subscribeEvents?.({onSessionEvent:e}))});return()=>{t=!0,r?.()}}"); expect(patched).toContain("E.sendBackgroundTaskMessage=async e=>"); - expect(patched).toContain('if(e?.restart===!0&&o.status==="running")'); - expect(patched).toContain("await r.subagentPort.stopTask(e.taskId)"); - expect(patched).toContain("r.subagentPort.sendMessage({sessionId:o.parentSessionId??r.getSessionId?.()"); + expect(patched).toContain(".readTuiRuntimeProjection("); + expect(patched).toContain(".sendTuiBackgroundTaskMessage("); expect(patched).toContain("E.previewFileRewind=async e=>{let t=await S();return await t.runtime?.previewWorkspaceFileRewind?.({targetMessageIds:e})??null}"); expect(patched).toContain("E.applyFileRewind=async e=>{let t=await S();return await t.runtime?.applyWorkspaceFileRewind?.({targetMessageIds:e})??null}"); expect(patched).toContain("E.setMode=async e=>{return await(await S()).setMode(e)}"); @@ -866,7 +860,7 @@ describe("runtime synchronization", () => { const projectionEnd = patched.indexOf(",E.readSessionUsage=", projectionStart); const projectionAssignment = patched.slice(projectionStart, projectionEnd); const bridge: { readRuntimeProjection?: () => Promise> } = {}; - const readRuntimeProjection = new Function( + const readRuntimeProjection = bridgeFunction( "E", "S", `${projectionAssignment};return E.readRuntimeProjection;` @@ -892,7 +886,7 @@ describe("runtime synchronization", () => { }); const liveProjectionBridge: { readRuntimeProjection?: () => Promise> } = {}; - const liveReadRuntimeProjection = new Function( + const liveReadRuntimeProjection = bridgeFunction( "E", "S", `${projectionAssignment};return E.readRuntimeProjection;` @@ -946,7 +940,7 @@ describe("runtime synchronization", () => { const restoreEnd = patched.indexOf(",E.recallPreviousInput=", restoreStart); expect(restoreStart).toBeGreaterThan(0); expect(restoreEnd).toBeGreaterThan(restoreStart); - const restoreBackgroundTasks = new Function( + const restoreBackgroundTasks = bridgeFunction( "E", `${patched.slice(restoreStart, restoreEnd)};return E.$zRestorePersistedBackgroundTasks;` )({}) as (app: unknown) => Promise; @@ -1313,7 +1307,7 @@ describe("runtime synchronization", () => { isBackgrounded: true }); }; - const taskMessage = new Function( + const taskMessage = bridgeFunction( "E", "S", `${patched.slice(taskMessageStart, taskMessageEnd)};return E.sendBackgroundTaskMessage;` @@ -1336,13 +1330,8 @@ describe("runtime synchronization", () => { }); expect(patchRuntimeTuiBridge(patched)).toBe(patched); - const unsafeBackgroundRestorePatch = patched.replace( - 'if(!$zOutput.includes("Async agent launched successfully."))continue;', - "" - ); - expect(patchRuntimeTuiBridge(unsafeBackgroundRestorePatch)).toContain( - '$zOutput.includes("Async agent launched successfully.")' - ); + const legacyRestorePatch = patched.replace(".restoreTuiBackgroundTasks(", ".legacyRestore("); + expect(patchRuntimeTuiBridge(legacyRestorePatch)).toContain(".restoreTuiBackgroundTasks("); const previousInterruptPatch = patched.replace("e?.waitForIdle===!0", "e?.waitForIdle===!1"); expect(patchRuntimeTuiBridge(previousInterruptPatch)).toContain("e?.waitForIdle===!0"); expect(() => patchRuntimeTuiBridge("incompatible runtime")).toThrow(/incompatible/);