Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions docs/DEVELOPMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
19 changes: 3 additions & 16 deletions scripts/check-runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
55 changes: 13 additions & 42 deletions scripts/sync-runtime.ts

Large diffs are not rendered by default.

129 changes: 129 additions & 0 deletions src/runtime-background-restore.ts
Original file line number Diff line number Diff line change
@@ -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<string, unknown>;
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<RecordValue | undefined> {
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<void> {
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<RuntimeTuiApp, Promise<void>>();

export async function restoreTuiBackgroundTasks(app: RuntimeTuiApp): Promise<void> {
const pending = pendingRestores.get(app);
if (pending) return pending;
const operation = restore(app);
pendingRestores.set(app, operation);
try { await operation; }
finally { pendingRestores.delete(app); }
}
2 changes: 2 additions & 0 deletions src/runtime-config-bridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, unknown> | undefined {
return value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : undefined;
Expand Down
142 changes: 142 additions & 0 deletions src/runtime-tui-bridge.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
/** Narrow structural boundary; the extracted runtime remains the state owner. */
export interface RuntimeTask extends Record<string, unknown> {
taskId: string;
type?: string;
taskType?: string;
status?: string;
isBackgrounded?: boolean;
error?: unknown;
}

export interface RuntimeTuiApp {
sessionId?: string;
$zRestoredBackgroundTasksSession?: string;
$zRestoredBackgroundTasksLog?: unknown[];
loadSessionTranscript?(): Promise<unknown[]>;
readExecutionState(): Promise<unknown> | unknown;
runtime?: {
getProjection?(): Promise<Record<string, unknown> | undefined> | Record<string, unknown> | undefined;
getSessionId?(): string;
runtimeTaskRegistry?: {
all?(): Record<string, RuntimeTask>;
get?(taskId: string): RuntimeTask | undefined;
register?(task: RuntimeTask): unknown;
};
messageHistory?: { addAttachment?(source: string, text: string): unknown };
subagentPort?: {
sendMessage?(input: Record<string, unknown>): Promise<unknown>;
stopTask?(taskId: string): Promise<unknown>;
};
workingDirectory?: string;
rootTraceContext?: unknown;
};
}

export interface RuntimeTuiBridge {
$zRestorePersistedBackgroundTasks?(app: RuntimeTuiApp): Promise<void>;
}

const maximumTaskMessageLength = 20_000;
const maximumTaskSummaryLength = 200;

function taskDetail(task: RuntimeTask): Record<string, unknown> {
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<Record<string, unknown> | 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<Record<string, unknown>> = 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<unknown> {
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
});
}
Loading
Loading