/** * Integration tests for async (background) agent execution. * * Tests the async support utilities: jiti availability check, * status file reading/caching. * * Requires pi packages to be importable. Skips gracefully if unavailable. */ import { after, afterEach, before, beforeEach, describe, it } from "node:test"; import assert from "node:assert/strict"; import { spawnSync } from "node:child_process"; import * as fs from "node:fs"; import * as os from "node:os"; import * as path from "node:path"; import { createEventBus, createMockPi, createTempDir, events, makeAgent, makeMinimalCtx, removeTempDir, tryImport } from "../support/helpers.ts"; import type { MockPi } from "../support/helpers.ts"; import { deliverInterruptRequest, deliverStopRequest, deliverTimeoutRequest } from "../../src/runs/background/control-channel.ts"; import { waitForSubagents } from "../../src/runs/background/subagent-wait.ts"; import { writeAtomicJson } from "../../src/shared/atomic-json.ts"; import { CHILD_WATCHDOG_STATUS_EVENT } from "../../src/watchdog/child-status.ts"; import { MAX_CHILD_PENDING_LINE_BYTES, MAX_CHILD_STDERR_BYTES } from "../../src/runs/shared/child-protocol.ts"; import { SUBAGENT_ASYNC_STARTED_EVENT, SUBAGENT_LIFECYCLE_ARTIFACT_VERSION } from "../../src/shared/types.ts"; import { registerSubagentCapabilityCeiling } from "../../src/api/capability-ceiling.ts"; import { resolveSubagentLaunchContract } from "../../src/api/preflight.ts"; import { discoverAgents } from "../../src/agents/agents.ts"; import { runSync } from "../../src/runs/foreground/execution.ts"; interface AsyncExecutionResult { content: Array<{ text?: string }>; isError?: boolean; details: { asyncId?: string; asyncDir?: string; launchContractDigest?: string }; } interface AsyncResultPayload { lifecycleArtifactVersion?: number; success: boolean; state?: string; exitCode?: number; sessionId?: string; mode?: string; launchContractDigest?: string; summary?: string; error?: string; timeoutMs?: number; deadlineAt?: number; timedOut?: boolean; stopped?: boolean; turnBudget?: { maxTurns: number; graceTurns: number; outcome: string; turnCount: number; wrapUpRequestedAtTurn?: number; terminationDeferredAtTurn?: number; exceededAtTurn?: number }; turnBudgetExceeded?: boolean; wrapUpRequested?: boolean; totalTokens?: { input: number; output: number; total: number }; totalCost?: { inputTokens: number; outputTokens: number; costUsd: number }; results: Array<{ agent?: string; launchContractDigest?: string; output?: string; success?: boolean; error?: string; protocolError?: { code?: string; stream?: string; limitBytes?: number; observedBytes?: number }; timedOut?: boolean; stopped?: boolean; turnBudget?: { maxTurns: number; graceTurns: number; outcome: string; turnCount: number; wrapUpRequestedAtTurn?: number; terminationDeferredAtTurn?: number; exceededAtTurn?: number }; turnBudgetExceeded?: boolean; wrapUpRequested?: boolean; model?: string; attemptedModels?: string[]; modelAttempts?: Array<{ success?: boolean; error?: string }>; totalCost?: { inputTokens: number; outputTokens: number; costUsd: number }; structuredOutput?: unknown; agentContract?: { version: 1 }; execution?: { status?: string; success?: boolean; exitCode?: number }; effects?: { fileMutation?: { status?: string; expected?: boolean; attempted?: boolean } }; intercomTarget?: string; acceptance?: { status?: string; effectiveAcceptance?: { level?: string }; childReport?: unknown; runtimeChecks?: Array<{ id?: string; status?: string; message?: string }> }; artifactPaths?: { outputPath?: string; inputPath?: string; metadataPath?: string }; capabilityCeiling?: { version?: number; allowedTools?: string[]; denyExtensions?: boolean; sources?: string[] }; capabilityAudit?: { effectiveTools?: string[]; removedTools?: string[]; extensionsDenied?: boolean } }>; outputs?: Record; workflowGraph?: { nodes?: Array<{ kind?: string; label?: string; phase?: string; status?: string; acceptanceStatus?: string; error?: string; outputName?: string; structured?: boolean; children?: Array<{ label?: string; outputName?: string; itemKey?: string; status?: string; acceptanceStatus?: string; error?: string }> }> }; parallelHandoff?: { version?: number; path?: string; groupCount?: number; childCount?: number; changedPatches?: number; cleanupState?: string }; capabilityCeiling?: { version?: number; allowedTools?: string[]; denyExtensions?: boolean; sources?: string[] }; capabilityAudit?: { effectiveTools?: string[]; removedTools?: string[]; extensionsDenied?: boolean }; } interface AsyncStatusPayload { lifecycleArtifactVersion?: number; sessionId?: string; pid?: number; activityState?: string; currentTool?: string; currentPath?: string; state?: string; launchContractDigest?: string; error?: string; timeoutMs?: number; deadlineAt?: number; timedOut?: boolean; stopped?: boolean; turnBudget?: { maxTurns: number; graceTurns: number; outcome: string; turnCount: number; wrapUpRequestedAtTurn?: number; terminationDeferredAtTurn?: number; exceededAtTurn?: number }; turnBudgetExceeded?: boolean; wrapUpRequested?: boolean; totalTokens?: { total: number }; totalCost?: { inputTokens: number; outputTokens: number; costUsd: number }; parallelGroups?: Array<{ start: number; count: number; stepIndex: number }>; parallelHandoff?: { version?: number; path?: string; groupCount?: number; childCount?: number; changedPatches?: number; cleanupState?: string }; capabilityCeiling?: { version?: number; allowedTools?: string[]; denyExtensions?: boolean; sources?: string[] }; capabilityAudit?: { effectiveTools?: string[]; removedTools?: string[]; extensionsDenied?: boolean }; steps?: Array<{ label?: string; phase?: string; outputName?: string; structured?: boolean; skills?: string[]; activityState?: string; currentTool?: string; status?: string; exitCode?: number; timedOut?: boolean; error?: string; model?: string; thinking?: string; tokens?: { total: number }; totalCost?: { inputTokens: number; outputTokens: number; costUsd: number }; agentContract?: { version: 1 }; launchContractDigest?: string; execution?: { status?: string; success?: boolean; exitCode?: number }; effects?: { fileMutation?: { status?: string; expected?: boolean; attempted?: boolean } }; acceptance?: { status?: string }; turnBudget?: { maxTurns: number; graceTurns: number; outcome: string; turnCount: number; wrapUpRequestedAtTurn?: number; terminationDeferredAtTurn?: number; exceededAtTurn?: number }; turnBudgetExceeded?: boolean; wrapUpRequested?: boolean; capabilityCeiling?: { version?: number; allowedTools?: string[]; denyExtensions?: boolean; sources?: string[] }; capabilityAudit?: { effectiveTools?: string[]; removedTools?: string[]; extensionsDenied?: boolean }; }>; } interface MockPiCallRecord { args?: string[]; systemPrompts?: Array<{ mode?: string; path?: string; text?: string; error?: string }>; } function writeWatchdogSettings(projectDir: string, tailMs = 120_000): void { const settingsPath = path.join(projectDir, ".selesai", "settings.json"); fs.mkdirSync(path.dirname(settingsPath), { recursive: true }); fs.writeFileSync(settingsPath, JSON.stringify({ subagents: { watchdog: { enabled: true, children: { enabled: true, watchdogTailTimeoutMs: tailMs, }, }, }, }, null, 2), "utf-8"); } async function withIsolatedWatchdogSettings(projectDir: string, run: () => Promise): Promise { const previousAgentDir = process.env.SELESAI_CODING_AGENT_DIR; const previousHome = process.env.HOME; const previousUserProfile = process.env.USERPROFILE; const isolatedHome = path.join(projectDir, "isolated-home"); process.env.SELESAI_CODING_AGENT_DIR = path.join(isolatedHome, ".selesai", "agent"); process.env.HOME = isolatedHome; process.env.USERPROFILE = isolatedHome; try { return await run(); } finally { if (previousAgentDir === undefined) delete process.env.SELESAI_CODING_AGENT_DIR; else process.env.SELESAI_CODING_AGENT_DIR = previousAgentDir; if (previousHome === undefined) delete process.env.HOME; else process.env.HOME = previousHome; if (previousUserProfile === undefined) delete process.env.USERPROFILE; else process.env.USERPROFILE = previousUserProfile; } } function childWatchdogStatus(runId: string, phase: "idle" | "reviewing" | "autofollow" | "settling" | "stale" | "failed", seq: number, followUpPending = false) { return { type: CHILD_WATCHDOG_STATUS_EVENT, runId, agent: "worker", childIndex: 0, stepIndex: 0, seq, phase, ts: Date.now() + seq, followUpPending, }; } function mockAssistantMessage(text: string, stopReason: "stop" | "tool_use" = "stop") { return { type: "message_end", message: { role: "assistant", content: stopReason === "tool_use" ? [{ type: "text", text }, { type: "toolCall", name: "bash", arguments: { command: "echo test" } }] : [{ type: "text", text }], model: "mock/test-model", stopReason, usage: { input: 10, output: 5, cacheRead: 0, cacheWrite: 0, cost: { total: 0.001 }, }, }, }; } interface AsyncExecutionModule { isAsyncAvailable(): boolean; executeAsyncSingle(id: string, params: Record): AsyncExecutionResult; executeAsyncChain(id: string, params: Record): AsyncExecutionResult; } interface UtilsModule { readStatus(dir: string): { runId: string; state: string; mode: string } | null; } interface TypesModule { ASYNC_DIR: string; RESULTS_DIR: string; TEMP_ROOT_DIR: string; } interface ExecutorModule { createSubagentExecutor?: (...args: unknown[]) => { execute: (...args: unknown[]) => Promise<{ content: Array<{ text?: string }>; isError?: boolean; details?: { asyncId?: string } }>; }; } const asyncMod = await tryImport("./src/runs/background/async-execution.ts"); const utils = await tryImport("./src/shared/utils.ts"); const typesMod = await tryImport("./src/shared/types.ts"); const executorMod = await tryImport("./src/runs/foreground/subagent-executor.ts"); const available = !!(asyncMod && utils && typesMod); const isAsyncAvailable = asyncMod?.isAsyncAvailable; const executeAsyncSingle = asyncMod?.executeAsyncSingle; const executeAsyncChain = asyncMod?.executeAsyncChain; const readStatus = utils?.readStatus; const ASYNC_DIR = typesMod?.ASYNC_DIR; const RESULTS_DIR = typesMod?.RESULTS_DIR; const TEMP_ROOT_DIR = typesMod?.TEMP_ROOT_DIR; const createSubagentExecutor = executorMod?.createSubagentExecutor; function escapeRegExp(value: string): string { return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); } function git(cwd: string, args: string[]): string { const result = spawnSync("git", ["-C", cwd, ...args], { encoding: "utf-8" }); if (result.status !== 0) { throw new Error(result.stderr.trim() || result.stdout.trim() || `git ${args.join(" ")} failed`); } return result.stdout.trim(); } function createRepo(prefix: string): string { const repoDir = fs.mkdtempSync(path.join(os.tmpdir(), prefix)); git(repoDir, ["init"]); git(repoDir, ["config", "user.email", "tests@example.com"]); git(repoDir, ["config", "user.name", "Async Tests"]); fs.writeFileSync(path.join(repoDir, "input.md"), "input\n", "utf-8"); git(repoDir, ["add", "-A"]); git(repoDir, ["commit", "-m", "initial commit"]); return repoDir; } function writePackageSkill(packageRoot: string, skillName: string): void { const skillDir = path.join(packageRoot, "skills", skillName); fs.mkdirSync(skillDir, { recursive: true }); fs.writeFileSync( path.join(packageRoot, "package.json"), JSON.stringify({ name: `${skillName}-pkg`, version: "1.0.0", pi: { skills: [`./skills/${skillName}`] } }, null, 2), "utf-8", ); fs.writeFileSync( path.join(skillDir, "SKILL.md"), `---\nname: ${skillName}\ndescription: test skill\n---\nbody\n`, "utf-8", ); } function readIfExists(filePath: string): string | undefined { try { const text = fs.readFileSync(filePath, "utf-8").trim(); return text || undefined; } catch { return undefined; } } async function waitForAsyncResultFile(id: string, timeoutMs = 15_000): Promise { const resultPath = path.join(RESULTS_DIR, `${id}.json`); const deadline = Date.now() + timeoutMs; while (!fs.existsSync(resultPath)) { if (Date.now() > deadline) { const asyncDir = path.join(ASYNC_DIR, id); const status = readIfExists(path.join(asyncDir, "status.json")); const stdout = readIfExists(path.join(asyncDir, "runner.stdout.log")); const stderr = readIfExists(path.join(asyncDir, "runner.stderr.log")); assert.fail([ `Timed out waiting for async result file: ${resultPath}`, status ? `status.json: ${status}` : undefined, stdout ? `runner stdout: ${stdout}` : undefined, stderr ? `runner stderr: ${stderr}` : undefined, ].filter(Boolean).join("\n")); } await new Promise((resolve) => setTimeout(resolve, 100)); } return resultPath; } async function waitForDeferredTurnBudget(id: string, timeoutMs = 10_000): Promise { const statusPath = path.join(ASYNC_DIR, id, "status.json"); const deadline = Date.now() + timeoutMs; while (Date.now() <= deadline) { if (fs.existsSync(statusPath)) { const status = JSON.parse(fs.readFileSync(statusPath, "utf-8")) as AsyncStatusPayload; if (status.state === "running" && status.turnBudget?.outcome === "termination-deferred" && status.steps?.[0]?.turnBudget?.outcome === "termination-deferred" && status.steps[0].currentTool === "bash") { return status; } } await new Promise((resolve) => setTimeout(resolve, 50)); } assert.fail(`Timed out waiting for deferred turn-budget status: ${statusPath}`); } async function waitForMockPiCall(mockPi: MockPi, index: number, timeoutMs = 30_000): Promise<{ args: string[]; systemPrompts: NonNullable }> { const deadline = Date.now() + timeoutMs; for (;;) { const callFile = fs.readdirSync(mockPi.dir) .filter((name) => name.startsWith("call-") && name.endsWith(".json")) .sort() .at(index); if (callFile) { const payload = JSON.parse(fs.readFileSync(path.join(mockPi.dir, callFile), "utf-8")) as MockPiCallRecord; assert.ok(Array.isArray(payload.args), "expected recorded args"); return { args: payload.args, systemPrompts: payload.systemPrompts ?? [] }; } if (Date.now() > deadline) assert.fail(`Timed out waiting for recorded mock pi call ${index}`); await new Promise((resolve) => setTimeout(resolve, 100)); } } async function waitForMockPiArgs(mockPi: MockPi, index: number, timeoutMs = 30_000): Promise { return (await waitForMockPiCall(mockPi, index, timeoutMs)).args; } function readLastMockPiArgs(mockPi: MockPi): string[] { const callFile = fs.readdirSync(mockPi.dir) .filter((name) => name.startsWith("call-") && name.endsWith(".json")) .sort() .at(-1); assert.ok(callFile, "expected a recorded mock pi call"); const payload = JSON.parse(fs.readFileSync(path.join(mockPi.dir, callFile), "utf-8")) as MockPiCallRecord; assert.ok(Array.isArray(payload.args), "expected recorded args"); return payload.args; } function readMockPiArgs(mockPi: MockPi, index: number): string[] { const callFile = fs.readdirSync(mockPi.dir) .filter((name) => name.startsWith("call-") && name.endsWith(".json")) .sort() .at(index); assert.ok(callFile, `expected recorded call ${index}`); const payload = JSON.parse(fs.readFileSync(path.join(mockPi.dir, callFile), "utf-8")) as MockPiCallRecord; assert.ok(Array.isArray(payload.args), "expected recorded args"); return payload.args; } function readMockPiArgsMatching(mockPi: MockPi, text: string): string[] { const callFiles = fs.readdirSync(mockPi.dir) .filter((name) => name.startsWith("call-") && name.endsWith(".json")) .sort(); for (const callFile of callFiles) { const payload = JSON.parse(fs.readFileSync(path.join(mockPi.dir, callFile), "utf-8")) as { args?: string[] }; assert.ok(Array.isArray(payload.args), "expected recorded args"); if (payload.args.join("\n").includes(text)) return payload.args; } assert.fail(`expected recorded call containing ${text}`); } describe("async execution utilities", { skip: !available ? "pi packages not available" : undefined }, () => { let tempDir: string; let mockPi: MockPi; before(() => { mockPi = createMockPi(); mockPi.install(); }); after(() => { mockPi.uninstall(); }); beforeEach(() => { tempDir = createTempDir(); mockPi.reset(); }); afterEach(() => { removeTempDir(tempDir); }); function makeAsyncExecutor(agents: ReturnType[], config: Record = {}) { return createSubagentExecutor!({ pi: { events: createEventBus(), getSessionName: () => undefined }, state: { baseCwd: tempDir, currentSessionId: null, asyncJobs: new Map(), foregroundControls: new Map(), lastForegroundControlId: null }, config, asyncByDefault: false, tempArtifactsDir: tempDir, getSubagentSessionRoot: () => tempDir, expandTilde: (p: string) => p, discoverAgents: () => ({ agents }), }); } async function readAsyncPayload(id: string): Promise { const resultPath = await waitForAsyncResultFile(id, 10_000); return JSON.parse(fs.readFileSync(resultPath, "utf-8")) as AsyncResultPayload; } function launchProtocolTest(id: string): void { executeAsyncSingle(id, { agent: "worker", task: "Exercise child protocol", agentConfig: makeAgent("worker", { completionGuard: false }), ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" }, artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7 }, shareEnabled: false, sessionRoot: path.join(tempDir, "sessions"), maxSubagentDepth: 2, acceptance: false, }); } it("reports jiti availability as boolean", () => { const result = isAsyncAvailable(); assert.equal(typeof result, "boolean"); }); it("background parses split UTF-8 JSON and a final unterminated protocol line", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => { const line = Buffer.from(JSON.stringify(events.assistantMessage("你好 from fragmented async JSON"))); const unicodeStart = line.indexOf(Buffer.from("你")); mockPi.onCall({ stdoutBase64Chunks: [line.subarray(0, unicodeStart + 1).toString("base64"), line.subarray(unicodeStart + 1).toString("base64")] }); const id = `async-protocol-utf8-${Date.now().toString(36)}`; launchProtocolTest(id); const payload = await readAsyncPayload(id); assert.equal(payload.success, true); assert.equal(payload.results[0]?.output, "你好 from fragmented async JSON"); }); it("matches preflight launch digest in equivalent foreground and async execution", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => { const agentName = `contract-worker-${Date.now().toString(36)}`; const task = "Compare the resolved launch inputs."; const turnBudget = { maxTurns: 2, graceTurns: 1 } as const; const agentPath = path.join(tempDir, ".selesai", "agents", `${agentName}.md`); fs.mkdirSync(path.dirname(agentPath), { recursive: true }); fs.writeFileSync(agentPath, `---\nname: ${agentName}\ndescription: Contract comparison worker\n---\n`, "utf-8"); const discovered = discoverAgents(tempDir).agents.find((agent) => agent.name === agentName); assert.ok(discovered, "expected temporary agent definition to be discovered"); const preflight = await resolveSubagentLaunchContract({ agent: agentName, cwd: tempDir, task, turnBudget, runId: "contract-preflight" }); assert.equal(preflight.ok, true); mockPi.onCall({ output: "foreground contract comparison" }); const foreground = await runSync(tempDir, [discovered], agentName, task, { runId: "contract-foreground", acceptance: false, turnBudget }); assert.equal(foreground.exitCode, 0); assert.equal(foreground.launchContractDigest, preflight.contract.launchContractDigest); mockPi.onCall({ output: "async contract comparison" }); const asyncId = `async-contract-equivalence-${Date.now().toString(36)}`; const launch = executeAsyncSingle(asyncId, { agent: agentName, task, agentConfig: discovered, ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" }, artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7 }, shareEnabled: false, sessionRoot: path.join(tempDir, "sessions"), maxSubagentDepth: 2, acceptance: false, turnBudget, }); const payload = await readAsyncPayload(asyncId); assert.equal(launch.details.launchContractDigest, preflight.contract.launchContractDigest); assert.equal(payload.launchContractDigest, preflight.contract.launchContractDigest); assert.equal(payload.results[0]?.launchContractDigest, preflight.contract.launchContractDigest); }); it("persists the actual launch digest in async status and result metadata", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => { mockPi.onCall({ output: "digest-bound async done" }); const id = `async-launch-digest-${Date.now().toString(36)}`; const launch = executeAsyncSingle(id, { agent: "worker", task: "Exercise launch digest reporting", agentConfig: makeAgent("worker", { completionGuard: false }), ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" }, artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7 }, shareEnabled: false, sessionRoot: path.join(tempDir, "sessions"), maxSubagentDepth: 2, acceptance: false, }); assert.match(launch.details.launchContractDigest ?? "", /^[a-f0-9]{64}$/); const payload = await readAsyncPayload(id); const status = JSON.parse(fs.readFileSync(path.join(ASYNC_DIR, id, "status.json"), "utf-8")) as AsyncStatusPayload; assert.equal(payload.launchContractDigest, launch.details.launchContractDigest); assert.equal(payload.results[0]?.launchContractDigest, launch.details.launchContractDigest); assert.equal(status.launchContractDigest, launch.details.launchContractDigest); assert.equal(status.steps?.[0]?.launchContractDigest, launch.details.launchContractDigest); }); it("background fails with protocol_output_limit for an oversized stdout line", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => { mockPi.onCall({ stdoutRaw: "x".repeat(MAX_CHILD_PENDING_LINE_BYTES + 1) }); const id = `async-protocol-limit-${Date.now().toString(36)}`; launchProtocolTest(id); const payload = await readAsyncPayload(id); assert.equal(payload.success, false); assert.equal(payload.results[0]?.protocolError?.code, "protocol_output_limit"); assert.equal(payload.results[0]?.protocolError?.stream, "stdout"); assert.match(payload.results[0]?.error ?? "", /protocol_output_limit/); }); it("routes async artifacts to the configured session directory", { skip: !isAsyncAvailable() || !createSubagentExecutor ? "jiti or executor not available" : undefined }, async () => { mockPi.onCall({ output: "async session artifact" }); const sessionFile = path.join(tempDir, "sessions", "parent-session", "session.jsonl"); const ctx = makeMinimalCtx(tempDir); ctx.sessionManager.getSessionFile = () => sessionFile; const executor = makeAsyncExecutor([makeAgent("worker", { completionGuard: false })], { artifactDir: "session" }); const launch = await executor.execute( "async-session-artifact-dir", { agent: "worker", task: "Write async session artifacts", async: true, runId: "async-session-artifacts", acceptance: false }, new AbortController().signal, undefined, ctx, ) as AsyncExecutionResult; const expectedDir = path.join(path.dirname(sessionFile), "subagent-artifacts"); assert.equal(launch.isError, undefined); assert.ok(launch.details.asyncId); const descriptor = JSON.parse(fs.readFileSync(path.join(launch.details.asyncDir!, "recovery-descriptor.json"), "utf-8")); assert.equal(descriptor.artifactsDir, expectedDir); assert.equal(descriptor.artifactConfig.dir, "session"); const payload = await readAsyncPayload(launch.details.asyncId); const outputPath = payload.results[0]?.artifactPaths?.outputPath; assert.ok(outputPath?.startsWith(`${expectedDir}${path.sep}`)); assert.equal(fs.readFileSync(outputPath, "utf-8"), "async session artifact"); assert.equal(fs.existsSync(path.join(tempDir, ".pi-subagents", "artifacts")), false); }); it("persists async capability ceiling audit to status, results, events, and metadata", { skip: !isAsyncAvailable() || !createSubagentExecutor ? "jiti or executor not available" : undefined }, async () => { mockPi.onCall({ output: "restricted async done" }); const sessionId = `session-capability-${Date.now().toString(36)}`; const handle = registerSubagentCapabilityCeiling({ sessionId, ceiling: { allowedTools: ["read"], denyExtensions: true }, source: "test" }); try { const executor = makeAsyncExecutor([makeAgent("worker", { tools: ["read", "write"], completionGuard: false })]); const id = `async-capability-${Date.now().toString(36)}`; const ctx = makeMinimalCtx(tempDir); ctx.sessionManager.getSessionId = () => sessionId; const launch = await executor.execute( id, { agent: "worker", task: "Run with a restricted capability ceiling", async: true, runId: id, acceptance: false, artifacts: true }, new AbortController().signal, undefined, ctx, ) as AsyncExecutionResult; assert.equal(launch.isError, undefined); const asyncId = launch.details.asyncId; assert.ok(asyncId); const resultPath = await waitForAsyncResultFile(asyncId, 10_000); const payload = JSON.parse(fs.readFileSync(resultPath, "utf-8")) as AsyncResultPayload; const status = JSON.parse(fs.readFileSync(path.join(ASYNC_DIR, asyncId, "status.json"), "utf-8")) as AsyncStatusPayload; assert.deepEqual(payload.capabilityCeiling, { version: 1, allowedTools: ["read"], denyExtensions: true, sources: ["test"] }); assert.deepEqual(payload.results[0]?.capabilityCeiling, payload.capabilityCeiling); assert.deepEqual(status.capabilityCeiling, payload.capabilityCeiling); assert.deepEqual(status.steps?.[0]?.capabilityCeiling, payload.capabilityCeiling); assert.deepEqual(payload.capabilityAudit?.effectiveTools, ["read"]); assert.deepEqual(payload.capabilityAudit?.removedTools, ["write", "intercom", "contact_supervisor"]); assert.equal(payload.capabilityAudit?.extensionsDenied, true); const events = fs.readFileSync(path.join(ASYNC_DIR, asyncId, "events.jsonl"), "utf-8").trim().split("\n").map((line) => JSON.parse(line)); assert.ok(events.some((event) => event.type === "subagent.capability-ceiling.applied" && event.stepIndex === 0 && event.capabilityAudit?.removedTools?.includes("write"))); const metadataPath = payload.results[0]?.artifactPaths?.metadataPath; assert.ok(metadataPath); const metadata = JSON.parse(fs.readFileSync(metadataPath, "utf-8")) as { launchContractDigest?: string; capabilityCeiling?: unknown; capabilityAudit?: { removedTools?: string[] } }; assert.equal(metadata.launchContractDigest, payload.results[0]?.launchContractDigest); assert.deepEqual(metadata.capabilityCeiling, payload.capabilityCeiling); assert.deepEqual(metadata.capabilityAudit?.removedTools, ["write", "intercom", "contact_supervisor"]); } finally { handle.dispose(); } }); it("background writes a failure stub to output artifacts when no output was produced", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => { mockPi.onCall({ output: "", stderr: "model unavailable", exitCode: 1 }); const id = `async-empty-failure-artifact-${Date.now().toString(36)}`; executeAsyncSingle(id, { agent: "worker", task: "Fail before output", agentConfig: makeAgent("worker", { completionGuard: false }), ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" }, artifactConfig: { enabled: true, includeInput: true, includeOutput: true, includeJsonl: false, includeMetadata: true, cleanupDays: 7 }, artifactsDir: path.join(tempDir, ".pi-subagents", "artifacts"), shareEnabled: false, maxSubagentDepth: 2, acceptance: false, }); const payload = await readAsyncPayload(id); assert.equal(payload.success, false); const outputPath = payload.results[0]?.artifactPaths?.outputPath; assert.ok(outputPath, "should expose an output artifact path"); const artifact = fs.readFileSync(outputPath, "utf-8"); assert.match(artifact, /Subagent run failed before producing output\./); assert.match(artifact, /Error:\nmodel unavailable/); assert.match(artifact, /Transcript:/); assert.match(artifact, /Metadata:/); }); it("background keeps only a bounded UTF-8 stderr tail", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => { mockPi.onCall({ output: "failed", stderr: `${"x".repeat(MAX_CHILD_STDERR_BYTES + 1024)}终`, exitCode: 1 }); const id = `async-stderr-tail-${Date.now().toString(36)}`; launchProtocolTest(id); const payload = await readAsyncPayload(id); assert.equal(payload.success, false); assert.ok(Buffer.byteLength(payload.results[0]?.error ?? "") <= MAX_CHILD_STDERR_BYTES); assert.match(payload.results[0]?.error ?? "", /终$/); }); it("background cancels final drain while agent_end reports a retry and waits for agent_settled", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => { mockPi.onCall({ steps: [ { jsonl: [events.assistantMessage("retrying async response"), { type: "agent_end", willRetry: true }] }, { delay: 1400, jsonl: [events.assistantMessage("settled async response"), { type: "agent_end", willRetry: false }, { type: "agent_settled" }] }, ] }); const id = `async-lifecycle-retry-${Date.now().toString(36)}`; const startedAt = Date.now(); launchProtocolTest(id); const payload = await readAsyncPayload(id); assert.equal(payload.success, true); assert.equal(payload.results[0]?.output, "settled async response"); assert.ok(Date.now() - startedAt >= 1200, "background runner must not terminate during the retry delay"); }); it("background treats agent_settled as a clean terminal watermark", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => { mockPi.onCall({ jsonl: [mockAssistantMessage("settled async without a terminal assistant stop", "tool_use"), { type: "agent_settled" }], keepAliveAfterFinalMessageMs: 5000 }); const id = `async-lifecycle-settled-${Date.now().toString(36)}`; const startedAt = Date.now(); launchProtocolTest(id); const payload = await readAsyncPayload(id); assert.equal(payload.success, true); assert.equal(payload.results[0]?.error, undefined); assert.equal(payload.results[0]?.output, "settled async without a terminal assistant stop"); assert.ok(Date.now() - startedAt < 4000, "agent_settled should trigger bounded child cleanup"); }); it("keeps named output references literal in async single tasks", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => { const task = "Reply with OK. You may reference {outputs.name} if it helps."; mockPi.onCall({ output: "OK" }); const id = `async-single-literal-output-ref-${Date.now().toString(36)}`; const result = executeAsyncSingle(id, { agent: "worker", task, agentConfig: makeAgent("worker"), ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" }, artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7, }, shareEnabled: false, sessionRoot: path.join(tempDir, "sessions"), maxSubagentDepth: 2, }); assert.equal(result.isError, undefined); const call = await waitForMockPiCall(mockPi, 0, 10_000); assert.match(call.args.at(-1) ?? "", /\{outputs\.name\}/); const payload = await readAsyncPayload(id); assert.equal(payload.success, true); assert.equal(payload.results[0]?.output, "OK"); }); it("spawns the async runner with node when process.execPath is not node", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => { const originalExecPath = process.execPath; process.execPath = path.join(tempDir, process.platform === "win32" ? "pi.exe" : "pi"); try { mockPi.onCall({ output: "non-node exec async done" }); const id = `async-non-node-exec-${Date.now().toString(36)}`; const result = executeAsyncSingle(id, { agent: "worker", task: "Say non-node exec async done. Do not edit files.", agentConfig: makeAgent("worker"), ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" }, artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7, }, shareEnabled: false, sessionRoot: path.join(tempDir, "sessions"), maxSubagentDepth: 2, }); assert.equal(result.isError, undefined); const resultPath = await waitForAsyncResultFile(id, 30_000); const payload = JSON.parse(fs.readFileSync(resultPath, "utf-8")) as AsyncResultPayload; assert.equal(payload.success, true); assert.equal(payload.results[0]?.output, "non-node exec async done"); } finally { process.execPath = originalExecPath; } }); it("falls back to PATH node when node-like process.execPath is stale", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => { const originalExecPath = process.execPath; process.execPath = path.join(tempDir, "deleted-node-install", "bin", process.platform === "win32" ? "node.exe" : "node"); try { mockPi.onCall({ output: "stale node exec async done" }); const id = `async-stale-node-exec-${Date.now().toString(36)}`; const result = executeAsyncSingle(id, { agent: "worker", task: "Say stale node exec async done. Do not edit files.", agentConfig: makeAgent("worker"), ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" }, artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7, }, shareEnabled: false, sessionRoot: path.join(tempDir, "sessions"), maxSubagentDepth: 2, }); assert.equal(result.isError, undefined); const resultPath = await waitForAsyncResultFile(id, 10_000); const payload = JSON.parse(fs.readFileSync(resultPath, "utf-8")) as AsyncResultPayload; assert.equal(payload.success, true); assert.equal(payload.results[0]?.output, "stale node exec async done"); } finally { process.execPath = originalExecPath; } }); it("readStatus returns null for missing directory", () => { const status = readStatus("/nonexistent/path/abc123"); assert.equal(status, null); }); it("readStatus parses valid status file", () => { const dir = createTempDir(); try { const statusData = { runId: "test-123", state: "running", mode: "single", startedAt: Date.now(), lastUpdate: Date.now(), steps: [{ agent: "test", status: "running" }], }; fs.writeFileSync(path.join(dir, "status.json"), JSON.stringify(statusData)); const status = readStatus(dir); assert.ok(status, "should parse status"); assert.equal(status.runId, "test-123"); assert.equal(status.state, "running"); assert.equal(status.mode, "single"); } finally { removeTempDir(dir); } }); it("interrupts every active async parallel child", { skip: !isAsyncAvailable() ? "jiti not available" : process.platform === "win32" ? "cross-process interrupt delivery unreliable on Windows CI" : undefined }, async () => { mockPi.onCall({ delay: 5_000, output: "one done" }); mockPi.onCall({ delay: 5_000, output: "two done" }); mockPi.onCall({ delay: 5_000, output: "three done" }); const id = `async-interrupt-parallel-${Date.now().toString(36)}`; executeAsyncChain(id, { chain: [{ parallel: [ { agent: "one", task: "Wait" }, { agent: "two", task: "Wait" }, { agent: "three", task: "Wait" }, ], concurrency: 3, }], resultMode: "parallel", agents: [makeAgent("one"), makeAgent("two"), makeAgent("three")], ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" }, artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7, }, shareEnabled: false, maxSubagentDepth: 2, }); await waitForMockPiCall(mockPi, 2, 10_000); const asyncDir = path.join(ASYNC_DIR, id); const statusPath = path.join(asyncDir, "status.json"); const statusBeforeInterrupt = JSON.parse(fs.readFileSync(statusPath, "utf-8")) as AsyncStatusPayload & { pid?: number }; deliverInterruptRequest({ asyncDir, pid: statusBeforeInterrupt.pid, source: "test" }); const resultPath = await waitForAsyncResultFile(id, 30_000); const payload = JSON.parse(fs.readFileSync(resultPath, "utf-8")) as AsyncResultPayload; const status = JSON.parse(fs.readFileSync(statusPath, "utf-8")) as AsyncStatusPayload; assert.equal(payload.state, "paused"); assert.equal(payload.success, false); assert.deepEqual(status.steps?.map((step) => step.status), ["paused", "paused", "paused"]); assert.equal(mockPi.callCount(), 3); }); it("marks async parallel runs that exceed timeoutMs as timed out", { skip: !isAsyncAvailable() ? "jiti not available" : process.platform === "win32" ? "timeout signal delivery intermittent on Windows CI" : undefined }, async () => { mockPi.onCall({ delay: 5_000, output: "one done" }); mockPi.onCall({ delay: 5_000, output: "two done" }); const id = `async-timeout-parallel-${Date.now().toString(36)}`; executeAsyncChain(id, { chain: [{ parallel: [ { agent: "one", task: "Wait" }, { agent: "two", task: "Wait" }, ], concurrency: 2, }], resultMode: "parallel", agents: [makeAgent("one"), makeAgent("two")], ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" }, artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7, }, shareEnabled: false, maxSubagentDepth: 2, timeoutMs: 1_500, }); await waitForMockPiCall(mockPi, 1, 10_000); const resultPath = await waitForAsyncResultFile(id, 8_000); const payload = JSON.parse(fs.readFileSync(resultPath, "utf-8")) as AsyncResultPayload; const status = JSON.parse(fs.readFileSync(path.join(ASYNC_DIR, id, "status.json"), "utf-8")) as AsyncStatusPayload; assert.equal(payload.state, "failed"); assert.equal(payload.success, false); assert.equal(payload.exitCode, 1); assert.equal(payload.timeoutMs, 1_500); assert.equal(payload.timedOut, true); assert.match(payload.summary ?? "", /Subagent timed out after 1500ms\./); assert.equal(status.state, "failed"); assert.equal(status.timeoutMs, 1_500); assert.equal(status.timedOut, true); assert.match(status.error ?? "", /Subagent timed out after 1500ms\./); assert.deepEqual(status.steps?.map((step) => step.status), ["failed", "failed"]); assert.deepEqual(status.steps?.map((step) => step.timedOut), [true, true]); assert.deepEqual(status.steps?.map((step) => step.error), ["Subagent timed out after 1500ms.", "Subagent timed out after 1500ms."]); assert.deepEqual(payload.results.map((result) => result.timedOut), [true, true]); assert.equal(mockPi.callCount(), 2); }); it("hard-kills async children that ignore timeout SIGTERM", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => { mockPi.onCall({ delay: 60_000, ignoreSigterm: true, output: "too late" }); const id = `async-timeout-hard-kill-${Date.now().toString(36)}`; const timeoutMs = process.platform === "win32" ? 5_000 : 1_500; const startedAt = Date.now(); executeAsyncSingle(id, { agent: "stubborn", task: "Ignore soft termination", agentConfig: makeAgent("stubborn", { model: "primary-model", fallbackModels: ["fallback-model"] }), ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" }, artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7, }, shareEnabled: false, maxSubagentDepth: 2, timeoutMs, }); await waitForMockPiCall(mockPi, 0, 10_000); const resultPath = await waitForAsyncResultFile(id, 10_000); const elapsedMs = Date.now() - startedAt; const payload = JSON.parse(fs.readFileSync(resultPath, "utf-8")) as AsyncResultPayload; const status = JSON.parse(fs.readFileSync(path.join(ASYNC_DIR, id, "status.json"), "utf-8")) as AsyncStatusPayload; assert.equal(payload.state, "failed"); assert.equal(payload.timedOut, true); assert.equal(payload.results[0]?.timedOut, true); assert.equal(payload.results[0]?.error, `Subagent timed out after ${timeoutMs}ms.`); assert.equal(status.timedOut, true); assert.equal(status.steps?.[0]?.timedOut, true); assert.ok(elapsedMs < timeoutMs + 4_000, `timeout result should settle after hard kill, elapsed ${elapsedMs}ms`); assert.equal(mockPi.callCount(), 1); }); it("cancels async acceptance verification when the run times out", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => { mockPi.onCall({ output: "implementation complete" }); const id = `async-timeout-acceptance-${Date.now().toString(36)}`; const timeoutMs = 1_000; const startedAt = Date.now(); executeAsyncSingle(id, { agent: "worker", task: "Implement with verified acceptance", agentConfig: makeAgent("worker"), ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" }, artifactConfig: { enabled: true, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: true, cleanupDays: 7, }, artifactsDir: path.join(tempDir, ".pi-subagents", "artifacts"), shareEnabled: false, maxSubagentDepth: 2, timeoutMs, acceptance: { level: "verified", verify: [{ id: "slow", command: `${process.execPath} -e "setTimeout(()=>process.exit(0), 30000)"`, timeoutMs: 60_000 }], }, }); const resultPath = await waitForAsyncResultFile(id, 5_000); const elapsedMs = Date.now() - startedAt; const payload = JSON.parse(fs.readFileSync(resultPath, "utf-8")) as AsyncResultPayload; const status = JSON.parse(fs.readFileSync(path.join(ASYNC_DIR, id, "status.json"), "utf-8")) as AsyncStatusPayload; assert.equal(payload.state, "failed"); assert.equal(payload.timedOut, true); assert.equal(payload.results[0]?.timedOut, true); assert.equal(payload.results[0]?.acceptance?.status, "rejected"); assert.equal(payload.results[0]?.acceptance?.runtimeChecks?.[0]?.id, "timeout"); assert.equal(status.steps?.[0]?.timedOut, true); const metadataPath = payload.results[0]?.artifactPaths?.metadataPath; assert.ok(metadataPath); const metadata = JSON.parse(fs.readFileSync(metadataPath, "utf-8")) as { acceptance?: { status?: string; runtimeChecks?: Array<{ id?: string }> } }; assert.equal(metadata.acceptance?.status, "rejected"); assert.equal(metadata.acceptance?.runtimeChecks?.[0]?.id, "timeout"); assert.ok(elapsedMs < timeoutMs + 4_000, `timeout should cancel acceptance verification well before the verify command completes, elapsed ${elapsedMs}ms`); }); it("async turn budget allows a terminal final grace turn", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => { mockPi.onCall({ jsonl: [ mockAssistantMessage("working before wrap-up", "tool_use"), mockAssistantMessage("final wrapped output", "stop"), ], }); const id = `async-turn-budget-soft-${Date.now().toString(36)}`; executeAsyncSingle(id, { agent: "worker", task: "Use the final grace turn to wrap up.", agentConfig: makeAgent("worker"), ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" }, artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7 }, shareEnabled: false, maxSubagentDepth: 2, turnBudget: { maxTurns: 1, graceTurns: 1 }, }); const resultPath = await waitForAsyncResultFile(id, 10_000); const payload = JSON.parse(fs.readFileSync(resultPath, "utf-8")) as AsyncResultPayload; const status = JSON.parse(fs.readFileSync(path.join(ASYNC_DIR, id, "status.json"), "utf-8")) as AsyncStatusPayload; assert.equal(payload.success, true); assert.equal(payload.state, "complete"); assert.equal(payload.turnBudgetExceeded, undefined); assert.equal(payload.wrapUpRequested, true); assert.equal(payload.turnBudget?.outcome, "wrap-up-requested"); assert.equal(payload.turnBudget?.turnCount, 2); assert.equal(payload.results[0]?.wrapUpRequested, true); assert.equal(payload.results[0]?.turnBudget?.turnCount, 2); assert.match(payload.results[0]?.output ?? "", /Turn budget wrap-up was requested after 1 assistant turn/); assert.match(payload.results[0]?.output ?? "", /final wrapped output/); assert.equal(status.wrapUpRequested, true); assert.equal(status.turnBudgetExceeded, undefined); assert.equal(status.steps?.[0]?.wrapUpRequested, true); assert.equal(status.steps?.[0]?.turnBudget?.turnCount, 2); }); it("async turn budget hard-aborts beyond the final grace turn at a safe assistant boundary", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => { mockPi.onCall({ jsonl: [ mockAssistantMessage("working before wrap-up", "tool_use"), mockAssistantMessage("starting final grace tool work", "tool_use"), mockAssistantMessage("safe assistant boundary after tool work", "stop"), ], }); const id = `async-turn-budget-hard-${Date.now().toString(36)}`; executeAsyncSingle(id, { agent: "worker", task: "Exceed the turn budget.", agentConfig: makeAgent("worker"), ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" }, artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7 }, shareEnabled: false, maxSubagentDepth: 2, turnBudget: { maxTurns: 1, graceTurns: 1 }, }); const resultPath = await waitForAsyncResultFile(id, 10_000); const payload = JSON.parse(fs.readFileSync(resultPath, "utf-8")) as AsyncResultPayload; const status = JSON.parse(fs.readFileSync(path.join(ASYNC_DIR, id, "status.json"), "utf-8")) as AsyncStatusPayload; assert.equal(payload.success, false); assert.equal(payload.state, "failed"); assert.equal(payload.exitCode, 1); assert.equal(payload.turnBudgetExceeded, true); assert.equal(payload.wrapUpRequested, true); assert.equal(payload.turnBudget?.outcome, "exceeded"); assert.equal(payload.turnBudget?.turnCount, 3); assert.equal(payload.turnBudget?.exceededAtTurn, 3); assert.equal(payload.results[0]?.turnBudgetExceeded, true); assert.match(payload.results[0]?.output ?? "", /Partial output before turn-budget abort:/); assert.match(payload.results[0]?.output ?? "", /safe assistant boundary after tool work/); assert.equal(status.state, "failed"); assert.equal(status.turnBudgetExceeded, true); assert.equal(status.steps?.[0]?.turnBudgetExceeded, true); assert.equal(status.steps?.[0]?.turnBudget?.outcome, "exceeded"); }); it("defers an async hard turn limit through active tool work and aborts at the next assistant boundary", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => { mockPi.onCall({ steps: [ { jsonl: [ mockAssistantMessage("starting required tool work", "tool_use"), events.toolStart("bash", { command: "node build.mjs" }), ], }, { delay: 750, jsonl: [ events.toolResult("bash", "build completed"), events.toolEnd("bash"), mockAssistantMessage("safe assistant boundary reached", "stop"), ], }, ], }); const id = `async-turn-budget-deferred-${Date.now().toString(36)}`; executeAsyncSingle(id, { agent: "worker", task: "Finish active tool work before enforcing the hard limit.", agentConfig: makeAgent("worker"), ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" }, artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7 }, shareEnabled: false, maxSubagentDepth: 2, turnBudget: { maxTurns: 1, graceTurns: 0 }, }); const pending = await waitForDeferredTurnBudget(id); assert.equal(pending.error, undefined); assert.equal(pending.turnBudgetExceeded, undefined); assert.equal(pending.turnBudget?.terminationDeferredAtTurn, 1); assert.equal(pending.steps?.[0]?.status, "running"); assert.equal(pending.steps?.[0]?.turnBudgetExceeded, undefined); const resultPath = await waitForAsyncResultFile(id, 10_000); const payload = JSON.parse(fs.readFileSync(resultPath, "utf-8")) as AsyncResultPayload; const status = JSON.parse(fs.readFileSync(path.join(ASYNC_DIR, id, "status.json"), "utf-8")) as AsyncStatusPayload; assert.equal(payload.state, "failed"); assert.equal(payload.turnBudgetExceeded, true); assert.equal(payload.turnBudget?.outcome, "exceeded"); assert.equal(payload.turnBudget?.turnCount, 2); assert.equal(payload.results[0]?.turnBudgetExceeded, true); assert.match(payload.results[0]?.output ?? "", /safe assistant boundary reached/); assert.equal(status.state, "failed"); assert.equal(status.turnBudgetExceeded, true); assert.equal(status.steps?.[0]?.turnBudget?.outcome, "exceeded"); }); it("lets timeout delivery preempt deferred turn-budget termination", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => { mockPi.onCall({ steps: [ { jsonl: [ mockAssistantMessage("starting long tool work", "tool_use"), events.toolStart("bash", { command: "node build.mjs" }), ], }, { delay: 5_000, jsonl: [events.toolEnd("bash")] }, ], }); const id = `async-turn-budget-timeout-${Date.now().toString(36)}`; executeAsyncSingle(id, { agent: "worker", task: "Timeout while hard termination is deferred.", agentConfig: makeAgent("worker"), ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" }, artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7 }, shareEnabled: false, maxSubagentDepth: 2, turnBudget: { maxTurns: 1, graceTurns: 0 }, timeoutMs: 30_000, }); const pending = await waitForDeferredTurnBudget(id); const asyncDir = path.join(ASYNC_DIR, id); deliverTimeoutRequest({ asyncDir, pid: pending.pid, source: "test" }); const resultPath = await waitForAsyncResultFile(id, 10_000); const payload = JSON.parse(fs.readFileSync(resultPath, "utf-8")) as AsyncResultPayload; const status = JSON.parse(fs.readFileSync(path.join(asyncDir, "status.json"), "utf-8")) as AsyncStatusPayload; assert.equal(payload.state, "failed"); assert.equal(payload.timedOut, true); assert.equal(payload.turnBudgetExceeded, undefined); assert.match(payload.error ?? payload.summary ?? "", /timed out after 30000ms/); assert.equal(payload.results[0]?.timedOut, true); assert.equal(payload.results[0]?.turnBudgetExceeded, undefined); assert.equal(status.timedOut, true); assert.equal(status.turnBudgetExceeded, undefined); assert.equal(status.steps?.[0]?.timedOut, true); }); it("lets explicit stop preempt deferred turn-budget termination", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => { mockPi.onCall({ steps: [ { jsonl: [ mockAssistantMessage("starting long tool work", "tool_use"), events.toolStart("bash", { command: "node build.mjs" }), ], }, { delay: 5_000, jsonl: [events.toolEnd("bash")] }, ], }); const id = `async-turn-budget-stop-${Date.now().toString(36)}`; executeAsyncSingle(id, { agent: "worker", task: "Stop while hard termination is deferred.", agentConfig: makeAgent("worker"), ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" }, artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7 }, shareEnabled: false, maxSubagentDepth: 2, turnBudget: { maxTurns: 1, graceTurns: 0 }, }); const pending = await waitForDeferredTurnBudget(id); const asyncDir = path.join(ASYNC_DIR, id); deliverStopRequest({ asyncDir, pid: pending.pid, source: "test" }); const resultPath = await waitForAsyncResultFile(id, 10_000); const payload = JSON.parse(fs.readFileSync(resultPath, "utf-8")) as AsyncResultPayload; const status = JSON.parse(fs.readFileSync(path.join(asyncDir, "status.json"), "utf-8")) as AsyncStatusPayload; assert.equal(payload.state, "stopped"); assert.equal(payload.stopped, true); assert.equal(payload.turnBudgetExceeded, undefined); assert.match(payload.error ?? payload.summary ?? "", /stopped by user/i); assert.equal(payload.results[0]?.stopped, true); assert.equal(payload.results[0]?.turnBudgetExceeded, undefined); assert.equal(status.state, "stopped"); assert.equal(status.stopped, true); assert.equal(status.turnBudgetExceeded, undefined); assert.equal(status.steps?.[0]?.status, "stopped"); }); it("async launch messages tell the parent not to sleep-poll", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => { const artifactConfig = { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7, }; const emitted: Array<{ channel: string; data: unknown }> = []; const commonParams = { ctx: { pi: { events: { emit(channel: string, data: unknown) { emitted.push({ channel, data }); } } }, cwd: tempDir, currentSessionId: "session-1", }, artifactConfig, shareEnabled: false, maxSubagentDepth: 2, }; const startedEvent = (id: string): { task?: string; goal?: string } => { const event = emitted.find((entry) => entry.channel === SUBAGENT_ASYNC_STARTED_EVENT && (entry.data as { id?: string }).id === id); assert.ok(event, `missing async-started event for ${id}`); return event.data as { task?: string; goal?: string }; }; mockPi.onCall({ output: "single done" }); const singleId = `async-handoff-single-${Date.now().toString(36)}`; const wrappedTask = `Fork preamble: ${"execution ".repeat(20)}`; const rawGoal = `Caller-facing goal: ${"raw ".repeat(40)}`; const singleResult = executeAsyncSingle(singleId, { agent: "worker", task: wrappedTask, goal: rawGoal, agentConfig: makeAgent("worker"), ...commonParams, }); assert.match(singleResult.content[0]?.text ?? "", /Async: worker \[/); assert.match(singleResult.content[0]?.text ?? "", /Do not run sleep timers or polling loops/); assert.match(singleResult.content[0]?.text ?? "", /call subagent_wait\(\)/i); assert.match(singleResult.content[0]?.text ?? "", /non-interactive run: Pi auto-drains current-session background work at agent_end/); assert.equal(startedEvent(singleId).task, wrappedTask.slice(0, 50)); assert.equal(startedEvent(singleId).goal, rawGoal.slice(0, 120)); await waitForAsyncResultFile(singleId, 30_000); mockPi.onCall({ output: "interactive done" }); const interactiveId = `async-handoff-interactive-${Date.now().toString(36)}`; const interactiveResult = executeAsyncSingle(interactiveId, { agent: "worker", task: "Interactive handoff", agentConfig: makeAgent("worker"), ...commonParams, ctx: { ...commonParams.ctx, interactive: true }, }); assert.match(interactiveResult.content[0]?.text ?? "", /interactive session/); assert.match(interactiveResult.content[0]?.text ?? "", /return control to the user/); assert.match(interactiveResult.content[0]?.text ?? "", /Do NOT call subagent_wait\(\) merely to wait/); assert.doesNotMatch(interactiveResult.content[0]?.text ?? "", /auto-drain/); await waitForAsyncResultFile(interactiveId, 30_000); mockPi.onCall({ output: "parallel one done" }); mockPi.onCall({ output: "parallel two done" }); const parallelId = `async-handoff-parallel-${Date.now().toString(36)}`; const parallelResult = executeAsyncChain(parallelId, { chain: [{ parallel: [{ agent: "worker", task: "Do one" }, { agent: "reviewer", task: "Do two" }] }], resultMode: "parallel", agents: [makeAgent("worker"), makeAgent("reviewer")], ...commonParams, }); assert.match(parallelResult.content[0]?.text ?? "", /Async parallel:/); assert.match(parallelResult.content[0]?.text ?? "", /Do not run sleep timers or polling loops/); assert.match(parallelResult.content[0]?.text ?? "", /call subagent_wait\(\)/i); assert.equal(startedEvent(parallelId).goal, "Do one"); const parallelResultPath = await waitForAsyncResultFile(parallelId, 10_000); const parallelPayload = JSON.parse(fs.readFileSync(parallelResultPath, "utf-8")) as { agent?: string; mode?: string }; assert.equal(parallelPayload.mode, "parallel"); assert.equal(parallelPayload.agent, "parallel:worker+reviewer"); mockPi.onCall({ output: "chain done" }); const chainId = `async-handoff-chain-${Date.now().toString(36)}`; const chainGoal = `Coordinate the complete workflow ${"goal ".repeat(30)}`; const chainChildTask = `Do chained work ${"child ".repeat(15)}`; const chainResult = executeAsyncChain(chainId, { task: chainGoal, chain: [{ agent: "worker", task: chainChildTask }], agents: [makeAgent("worker")], ...commonParams, }); assert.match(chainResult.content[0]?.text ?? "", /Async chain:/); assert.match(chainResult.content[0]?.text ?? "", /Do not run sleep timers or polling loops/); const chainEvent = startedEvent(chainId); assert.equal(chainEvent.task, chainChildTask.slice(0, 50)); assert.equal(chainEvent.goal, chainGoal.slice(0, 120)); await waitForAsyncResultFile(chainId, 10_000); }); it("fails background chains when requested extension tools are unavailable", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => { mockPi.onCall({ output: "Model incorrectly claimed success", missingTools: ["fixture_search"] }); const id = `async-missing-extension-tool-${Date.now().toString(36)}`; executeAsyncChain(id, { chain: [{ agent: "extension-worker", task: "Use fixture search" }], agents: [makeAgent("extension-worker", { tools: ["read", "fixture_search"] })], ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" }, artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7 }, shareEnabled: false, maxSubagentDepth: 2, }); const resultPath = await waitForAsyncResultFile(id, 10_000); const payload = JSON.parse(fs.readFileSync(resultPath, "utf-8")) as AsyncResultPayload; assert.equal(payload.success, false); assert.equal(payload.state, "failed"); assert.match(payload.results[0]?.error ?? "", /requested unavailable child tools: fixture_search/); assert.match(payload.results[0]?.error ?? "", /subagentOnlyExtensions/); }); it("applies agent acceptance roles to inferred async acceptance", { skip: !isAsyncAvailable() || !createSubagentExecutor ? "jiti or executor not available" : undefined }, async () => { mockPi.onCall({ output: "exploration complete" }); const executor = makeAsyncExecutor([makeAgent("worker", { acceptanceRole: "read-only" })]); const result = await executor.execute( "async-agent-acceptance-role", { agent: "worker", task: "Explore the authentication flow", async: true, clarify: false }, new AbortController().signal, undefined, makeMinimalCtx(tempDir), ); const asyncId = result.details?.asyncId; assert.ok(asyncId, "expected asyncId"); const payload = await readAsyncPayload(asyncId); assert.equal(payload.results[0]?.acceptance?.effectiveAcceptance.level, "attested"); }); it("applies agent acceptance roles to inferred async parallel acceptance", { skip: !isAsyncAvailable() || !createSubagentExecutor ? "jiti or executor not available" : undefined }, async () => { mockPi.onCall({ output: "parallel exploration complete" }); const executor = makeAsyncExecutor([makeAgent("worker", { acceptanceRole: "read-only" })]); const result = await executor.execute( "async-parallel-agent-acceptance-role", { tasks: [{ agent: "worker", task: "Explore the authentication flow" }], async: true, clarify: false }, new AbortController().signal, undefined, makeMinimalCtx(tempDir), ); const asyncId = result.details?.asyncId; assert.ok(asyncId, "expected asyncId"); const payload = await readAsyncPayload(asyncId); assert.equal(payload.results[0]?.acceptance?.effectiveAcceptance.level, "attested"); }); it("infers async chain acceptance after expanding top-level task templates", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => { mockPi.onCall({ output: "patched" }); mockPi.onCall({ output: "reviewed" }); const patchId = `async-role-task-template-patch-${Date.now().toString(36)}`; executeAsyncChain(patchId, { task: "Patch src/auth.ts", chain: [{ agent: "explorer", task: "{task}" }], agents: [makeAgent("explorer", { acceptanceRole: "read-only" })], ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-role-task-patch" }, artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7 }, shareEnabled: false, maxSubagentDepth: 2, }); const patchPayload = await readAsyncPayload(patchId); assert.equal(patchPayload.results[0]?.acceptance?.effectiveAcceptance?.level, "checked"); const reviewId = `async-role-task-template-review-${Date.now().toString(36)}`; executeAsyncChain(reviewId, { task: "Review only; do not edit files", chain: [{ agent: "implementer", task: "{task}" }], agents: [makeAgent("implementer", { acceptanceRole: "writer" })], ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-role-task-review" }, artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7 }, shareEnabled: false, maxSubagentDepth: 2, }); const reviewPayload = await readAsyncPayload(reviewId); assert.equal(reviewPayload.results[0]?.acceptance?.effectiveAcceptance?.level, "attested"); }); it("top-level async parallel conversion preserves output, reads, and progress", { skip: !isAsyncAvailable() || !createSubagentExecutor ? "jiti or executor not available" : undefined }, async () => { mockPi.onCall({ output: "Async top-level report" }); const executor = createSubagentExecutor!({ pi: { events: createEventBus(), getSessionName: () => undefined }, state: { baseCwd: tempDir, currentSessionId: null, asyncJobs: new Map(), foregroundControls: new Map(), lastForegroundControlId: null }, config: {}, asyncByDefault: false, tempArtifactsDir: tempDir, getSubagentSessionRoot: () => tempDir, expandTilde: (p: string) => p, discoverAgents: () => ({ agents: [makeAgent("worker", { defaultProgress: true })] }), }); const result = await executor.execute( "async-parallel-fields", { tasks: [{ agent: "worker", task: "Do async work", output: "async-top-output.md", reads: ["input.md"] }], async: true, clarify: false, }, new AbortController().signal, undefined, makeMinimalCtx(tempDir), ); const asyncId = result.details?.asyncId; assert.ok(asyncId, "expected asyncId"); const resultPath = path.join(RESULTS_DIR, `${asyncId}.json`); const statusPath = path.join(ASYNC_DIR, asyncId, "status.json"); const deadline = Date.now() + 10_000; while (!fs.existsSync(resultPath)) { if (Date.now() > deadline) assert.fail(`Timed out waiting for async result file: ${resultPath}`); await new Promise((resolve) => setTimeout(resolve, 100)); } const payload = JSON.parse(fs.readFileSync(resultPath, "utf-8")) as AsyncResultPayload; const status = JSON.parse(fs.readFileSync(statusPath, "utf-8")) as AsyncStatusPayload; assert.equal(payload.mode, "parallel"); assert.equal(payload.sessionId, "session-123"); assert.equal(payload.results[0]?.acceptance?.status, "review-required"); assert.equal(payload.results[0]?.acceptance?.evidenceStatus, "checked"); assert.equal(status.sessionId, "session-123"); assert.equal(status.steps?.[0]?.acceptance?.status, "review-required"); assert.equal(status.steps?.[0]?.acceptance?.evidenceStatus, "checked"); const outputPath = path.join(tempDir, ".pi-subagents", "artifacts", "outputs", asyncId, "async-top-output.md"); const outputDeadline = Date.now() + 5_000; while (!fs.existsSync(outputPath)) { if (Date.now() > outputDeadline) { assert.fail(`Timed out waiting for saved output file: ${outputPath}`); } await new Promise((resolve) => setTimeout(resolve, 50)); } assert.equal(fs.readFileSync(outputPath, "utf-8"), "Async top-level report"); const callFile = fs.readdirSync(mockPi.dir).find((name) => name.startsWith("call-")); assert.ok(callFile, "expected a recorded mock pi call"); const args = JSON.parse(fs.readFileSync(path.join(mockPi.dir, callFile), "utf-8")).args as string[]; const taskArg = args.at(-1) ?? ""; const progressPath = path.join(tempDir, ".pi-subagents", "artifacts", "progress", asyncId, "progress.md"); assert.ok(taskArg.includes(`[Read from: ${path.join(tempDir, "input.md")}]`)); assert.ok(taskArg.includes(`Update progress at: ${progressPath}`)); assert.ok(taskArg.includes(`Write your findings to exactly this path: ${outputPath}`)); assert.equal(fs.existsSync(progressPath), true); assert.equal(fs.existsSync(path.join(tempDir, "progress.md")), false); }); for (const outputOverride of [undefined, true] as const) { it(`async top-level parallel isolates inherited output (${outputOverride === true ? "output:true" : "omitted output"})`, { skip: !isAsyncAvailable() || !createSubagentExecutor ? "jiti or executor not available" : undefined }, async () => { mockPi.onCall({ matchArgIncludes: "Write the first report", output: "first async report" }); mockPi.onCall({ matchArgIncludes: "Write the second report", output: "second async report" }); const agent = makeAgent("worker", { output: "context.md", tools: ["read", "grep", "find", "ls"] }); const executor = makeAsyncExecutor([agent]); const tasks = [ { agent: "worker", task: "Write the first report", ...(outputOverride !== undefined ? { output: outputOverride } : {}) }, { agent: "worker", task: "Write the second report", ...(outputOverride !== undefined ? { output: outputOverride } : {}) }, ]; const launch = await executor.execute( `async-inherited-output-${outputOverride === true ? "true" : "omitted"}`, { tasks, async: true, clarify: false }, new AbortController().signal, undefined, makeMinimalCtx(tempDir), ); assert.equal(launch.isError, undefined); const payload = await readAsyncPayload(launch.details?.asyncId as string); assert.equal(payload.success, true); assert.equal(payload.results[0]?.output?.split("\n\nOutput saved to:")[0], "first async report"); assert.equal(payload.results[1]?.output?.split("\n\nOutput saved to:")[0], "second async report"); const outputDir = path.join(tempDir, ".pi-subagents", "artifacts", "outputs", launch.details?.asyncId as string); const authoritativePaths = [ path.join(outputDir, "parallel-0", "0-worker", "context.md"), path.join(outputDir, "parallel-0", "1-worker", "context.md"), ]; assert.equal(fs.readFileSync(authoritativePaths[0]!, "utf-8"), "first async report"); assert.equal(fs.readFileSync(authoritativePaths[1]!, "utf-8"), "second async report"); const artifactPaths = payload.results.map((result) => result.artifactPaths?.outputPath); assert.ok(artifactPaths[0] && artifactPaths[1]); assert.notEqual(artifactPaths[0], artifactPaths[1]); assert.equal(fs.readFileSync(artifactPaths[0], "utf-8"), "first async report"); assert.equal(fs.readFileSync(artifactPaths[1], "utf-8"), "second async report"); const calls = fs.readdirSync(mockPi.dir).filter((name) => name.startsWith("call-")).sort(); const taskArgs = calls.map((name) => (JSON.parse(fs.readFileSync(path.join(mockPi.dir, name), "utf-8")) as MockPiCallRecord).args?.at(-1) ?? ""); const firstTask = taskArgs.find((task) => task.includes("Write the first report")) ?? ""; const secondTask = taskArgs.find((task) => task.includes("Write the second report")) ?? ""; assert.ok(firstTask.includes(path.join("parallel-0", "0-worker", "context.md"))); assert.ok(secondTask.includes(path.join("parallel-0", "1-worker", "context.md"))); for (const taskArg of [firstTask, secondTask]) { assert.match(taskArg, /Return the complete artifact in your final response\./); assert.match(taskArg, /Do not call contact_supervisor merely because no write-capable tool is available\./); assert.doesNotMatch(taskArg, /Write your findings to exactly this path/); } }); } it("async top-level parallel rejects duplicate explicit output paths before spawning", { skip: !isAsyncAvailable() || !createSubagentExecutor ? "jiti or executor not available" : undefined }, async () => { const executor = makeAsyncExecutor([makeAgent("worker")]); const result = await executor.execute( "async-duplicate-output", { tasks: [ { agent: "worker", task: "Write A", output: "same.md" }, { agent: "worker", task: "Write B", output: "same.md" }, ], async: true, clarify: false, }, new AbortController().signal, undefined, makeMinimalCtx(tempDir), ); assert.equal(result.isError, true); assert.match(result.content[0]?.text ?? "", /Parallel tasks 1 \(worker\) and 2 \(worker\).*same\.md/); assert.equal(mockPi.callCount(), 0); }); it("async top-level parallel preserves distinct explicit output destinations", { skip: !isAsyncAvailable() || !createSubagentExecutor ? "jiti or executor not available" : undefined }, async () => { mockPi.onCall({ matchArgIncludes: "first.md", output: "first explicit report" }); mockPi.onCall({ matchArgIncludes: "second.md", output: "second explicit report" }); const executor = makeAsyncExecutor([makeAgent("worker", { output: "context.md" })]); const launch = await executor.execute( "async-distinct-explicit-output", { tasks: [ { agent: "worker", task: "Write A", output: "first.md" }, { agent: "worker", task: "Write B", output: "second.md" }, ], async: true, clarify: false, }, new AbortController().signal, undefined, makeMinimalCtx(tempDir), ); const payload = await readAsyncPayload(launch.details?.asyncId as string); assert.equal(payload.success, true); const outputDir = path.join(tempDir, ".pi-subagents", "artifacts", "outputs", launch.details?.asyncId as string); assert.equal(fs.readFileSync(path.join(outputDir, "first.md"), "utf-8"), "first explicit report"); assert.equal(fs.readFileSync(path.join(outputDir, "second.md"), "utf-8"), "second explicit report"); assert.ok(payload.results[0]?.artifactPaths?.outputPath?.endsWith("_worker_0_output.md")); assert.ok(payload.results[1]?.artifactPaths?.outputPath?.endsWith("_worker_1_output.md")); }); it("async chain static parallel namespaces inherited default outputs", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => { mockPi.onCall({ matchArgIncludes: "Write first", output: "chain first report" }); mockPi.onCall({ matchArgIncludes: "Write second", output: "chain second report" }); const id = `async-chain-parallel-output-${Date.now().toString(36)}`; const launch = executeAsyncChain(id, { chain: [{ parallel: [{ agent: "worker", task: "Write first" }, { agent: "worker", task: "Write second" }] }], agents: [makeAgent("worker", { output: "context.md" })], ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-chain-output" }, artifactConfig: { enabled: true, includeInput: false, includeOutput: true, includeJsonl: false, includeMetadata: false, cleanupDays: 7 }, artifactsDir: path.join(tempDir, ".pi-subagents", "artifacts"), shareEnabled: false, maxSubagentDepth: 2, }); assert.equal(launch.isError, undefined); const payload = await readAsyncPayload(id); assert.equal(payload.success, true); const outputDir = path.join(tempDir, ".pi-subagents", "artifacts", "outputs", id); const authoritativePaths = [ path.join(outputDir, "parallel-0", "0-worker", "context.md"), path.join(outputDir, "parallel-0", "1-worker", "context.md"), ]; assert.equal(fs.readFileSync(authoritativePaths[0]!, "utf-8"), "chain first report"); assert.equal(fs.readFileSync(authoritativePaths[1]!, "utf-8"), "chain second report"); const artifactPaths = payload.results.map((result) => result.artifactPaths?.outputPath); assert.ok(artifactPaths[0] && artifactPaths[1]); assert.notEqual(artifactPaths[0], artifactPaths[1]); assert.equal(fs.readFileSync(artifactPaths[0], "utf-8"), "chain first report"); assert.equal(fs.readFileSync(artifactPaths[1], "utf-8"), "chain second report"); const calls = fs.readdirSync(mockPi.dir).filter((name) => name.startsWith("call-")).sort(); const taskArgs = calls.map((name) => (JSON.parse(fs.readFileSync(path.join(mockPi.dir, name), "utf-8")) as MockPiCallRecord).args?.at(-1) ?? ""); assert.ok(taskArgs.find((task) => task.includes("Write first"))?.includes(path.join("parallel-0", "0-worker", "context.md"))); assert.ok(taskArgs.find((task) => task.includes("Write second"))?.includes(path.join("parallel-0", "1-worker", "context.md"))); }); it("async single preserves checked evidence while independent review is pending", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => { mockPi.onCall({ output: [ "implemented", "```acceptance-report", JSON.stringify({ criteriaSatisfied: [{ id: "criterion-1", status: "satisfied", evidence: "patched" }], changedFiles: ["src/file.ts"], testsAddedOrUpdated: ["test/file.test.ts"], commandsRun: [{ command: "npm test", result: "passed", summary: "passed" }], validationOutput: ["passed"], residualRisks: [], noStagedFiles: true, notes: "done", }), "```", ].join("\n"), }); const artifactConfig = { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7, }; const id = `async-acceptance-${Date.now().toString(36)}`; executeAsyncSingle(id, { agent: "worker", task: "Implement acceptance-covered fix", agentConfig: makeAgent("worker", { completionGuard: false }), ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-acceptance" }, artifactConfig, shareEnabled: false, maxSubagentDepth: 2, acceptance: { level: "checked", criteria: ["Patch bug"], review: { agent: "reviewer", required: true } }, }); const resultPath = await waitForAsyncResultFile(id, 10_000); const result = JSON.parse(fs.readFileSync(resultPath, "utf-8")) as AsyncResultPayload; const status = JSON.parse(fs.readFileSync(path.join(ASYNC_DIR, id, "status.json"), "utf-8")) as AsyncStatusPayload; assert.equal(result.success, true); assert.equal(result.results[0]?.acceptance?.status, "review-required"); assert.equal(result.results[0]?.acceptance?.evidenceStatus, "checked"); assert.ok(result.results[0]?.acceptance?.childReport); assert.equal(result.results[0]?.acceptance?.reviewResult?.status, "review-required"); assert.equal(status.steps?.[0]?.acceptance?.status, "review-required"); }); it("top-level async chain suppresses progress for {task} review-only tasks", { skip: !isAsyncAvailable() || !createSubagentExecutor ? "jiti or executor not available" : undefined }, async () => { mockPi.onCall({ output: "Async review" }); const executor = createSubagentExecutor!({ pi: { events: createEventBus(), getSessionName: () => undefined }, state: { baseCwd: tempDir, currentSessionId: null, asyncJobs: new Map(), foregroundControls: new Map(), lastForegroundControlId: null }, config: {}, asyncByDefault: false, tempArtifactsDir: tempDir, getSubagentSessionRoot: () => tempDir, expandTilde: (p: string) => p, discoverAgents: () => ({ agents: [makeAgent("reviewer", { defaultProgress: true })] }), }); const result = await executor.execute( "async-chain-read-only-progress", { chain: [{ agent: "reviewer" }], task: "Review-only. Do not edit files. Return findings.", async: true, clarify: false, }, new AbortController().signal, undefined, makeMinimalCtx(tempDir), ); const asyncId = result.details?.asyncId; assert.ok(asyncId, "expected asyncId"); const resultPath = path.join(RESULTS_DIR, `${asyncId}.json`); const deadline = Date.now() + 10_000; while (!fs.existsSync(resultPath)) { if (Date.now() > deadline) assert.fail(`Timed out waiting for async result file: ${resultPath}`); await new Promise((resolve) => setTimeout(resolve, 100)); } const callFile = fs.readdirSync(mockPi.dir).find((name) => name.startsWith("call-")); assert.ok(callFile, "expected a recorded mock pi call"); const args = JSON.parse(fs.readFileSync(path.join(mockPi.dir, callFile), "utf-8")).args as string[]; assert.doesNotMatch(args.at(-1) ?? "", /progress\.md/); assert.equal(fs.existsSync(path.join(tempDir, "progress.md")), false); }); it("async chains reject malformed named output references before spawning", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => { const id = `async-malformed-output-ref-${Date.now().toString(36)}`; const result = executeAsyncChain(id, { chain: [{ agent: "consumer", task: "Use {outputs.bad-name}" }], agents: [makeAgent("consumer")], ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-malformed" }, artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7 }, shareEnabled: false, maxSubagentDepth: 2, }); assert.equal(result.isError, true); assert.match(result.content[0]?.text ?? "", /Invalid chain output reference '\{outputs\.bad-name\}'/); assert.equal(mockPi.callCount(), 0); }); it("async chains persist structured outputs, named outputs, and graph labels", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => { const schema = { type: "object", required: ["value"], properties: { value: { type: "string" } }, }; mockPi.onCall({ structuredOutput: { value: "Alpha structured" } }); mockPi.onCall({ output: "used named output" }); const id = `async-structured-chain-${Date.now().toString(36)}`; const result = executeAsyncChain(id, { chain: [ { agent: "producer", task: "Produce data", phase: "Collect", label: "Produce structured data", as: "data", outputSchema: schema, }, { agent: "consumer", task: "Use {outputs.data}", phase: "Use", label: "Consume data" }, ], agents: [makeAgent("producer"), makeAgent("consumer")], ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-structured" }, artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7 }, shareEnabled: false, maxSubagentDepth: 2, }); assert.ok(!result.isError); const resultPath = await waitForAsyncResultFile(id, 10_000); const payload = JSON.parse(fs.readFileSync(resultPath, "utf-8")) as AsyncResultPayload; const status = JSON.parse(fs.readFileSync(path.join(ASYNC_DIR, id, "status.json"), "utf-8")) as AsyncStatusPayload; assert.deepEqual(payload.results[0]?.structuredOutput, { value: "Alpha structured" }); assert.deepEqual(payload.outputs?.data?.structured, { value: "Alpha structured" }); assert.match(readMockPiArgs(mockPi, 1).at(-1) ?? "", /Alpha structured/); assert.equal(status.steps?.[0]?.label, "Produce structured data"); assert.equal(status.steps?.[0]?.phase, "Collect"); assert.equal(status.steps?.[0]?.outputName, "data"); assert.equal(status.steps?.[0]?.structured, true); assert.equal(payload.workflowGraph?.nodes?.[0]?.label, "Produce structured data"); assert.equal(payload.workflowGraph?.nodes?.[0]?.outputName, "data"); assert.equal(payload.workflowGraph?.nodes?.[0]?.status, "completed"); assert.equal(payload.workflowGraph?.nodes?.[1]?.status, "completed"); }); it("async chains can start parallel, funnel into one step, then fan back out", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => { mockPi.onCall({ matchArgIncludes: "Scout API", output: "Scout A async findings" }); mockPi.onCall({ matchArgIncludes: "Scout UI", output: "Scout B async findings" }); mockPi.onCall({ matchArgIncludes: "Synthesize:", output: "Async funnel synthesis" }); mockPi.onCall({ matchArgIncludes: "Review funnel A:", output: "Async reviewer A done" }); mockPi.onCall({ matchArgIncludes: "Review funnel B:", output: "Async reviewer B done" }); const id = `async-parallel-funnel-fanout-${Date.now().toString(36)}`; const result = executeAsyncChain(id, { chain: [ { parallel: [ { agent: "scout-a", task: "Scout API" }, { agent: "scout-b", task: "Scout UI" }, ], concurrency: 2, }, { agent: "synthesizer", task: "Synthesize:\n{previous}" }, { parallel: [ { agent: "review-a", task: "Review funnel A:\n{previous}" }, { agent: "review-b", task: "Review funnel B:\n{previous}" }, ], concurrency: 2, }, ], agents: [makeAgent("scout-a"), makeAgent("scout-b"), makeAgent("synthesizer"), makeAgent("review-a"), makeAgent("review-b")], ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-parallel-funnel-fanout" }, artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7 }, shareEnabled: false, maxSubagentDepth: 2, }); assert.ok(!result.isError, `should launch: ${JSON.stringify(result.content)}`); const resultPath = await waitForAsyncResultFile(id, 10_000); const payload = JSON.parse(fs.readFileSync(resultPath, "utf-8")) as AsyncResultPayload; const status = JSON.parse(fs.readFileSync(path.join(ASYNC_DIR, id, "status.json"), "utf-8")) as AsyncStatusPayload; assert.equal(payload.success, true); assert.deepEqual(payload.results.map((entry) => entry.output), [ "Scout A async findings", "Scout B async findings", "Async funnel synthesis", "Async reviewer A done", "Async reviewer B done", ]); assert.deepEqual(status.steps?.map((step) => step.status), ["complete", "complete", "complete", "complete", "complete"]); assert.deepEqual(status.parallelGroups, [ { start: 0, count: 2, stepIndex: 0 }, { start: 3, count: 2, stepIndex: 2 }, ]); const funnelTask = readMockPiArgsMatching(mockPi, "Synthesize:").at(-1) ?? ""; assert.match(funnelTask, /=== Parallel Task 1 \(scout-a\) ===/); assert.match(funnelTask, /Scout A async findings/); assert.match(funnelTask, /=== Parallel Task 2 \(scout-b\) ===/); assert.match(funnelTask, /Scout B async findings/); assert.match(readMockPiArgsMatching(mockPi, "Review funnel A:").at(-1) ?? "", /Review funnel A:\nAsync funnel synthesis/); assert.match(readMockPiArgsMatching(mockPi, "Review funnel B:").at(-1) ?? "", /Review funnel B:\nAsync funnel synthesis/); assert.equal(payload.workflowGraph?.nodes?.[0]?.kind, "parallel-group"); assert.equal(payload.workflowGraph?.nodes?.[0]?.status, "completed"); assert.equal(payload.workflowGraph?.nodes?.[1]?.kind, "step"); assert.equal(payload.workflowGraph?.nodes?.[1]?.status, "completed"); assert.equal(payload.workflowGraph?.nodes?.[2]?.kind, "parallel-group"); assert.equal(payload.workflowGraph?.nodes?.[2]?.status, "completed"); }); it("async dynamic status shows a placeholder before materialization", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => { mockPi.onCall({ delay: 800, output: "targets", structuredOutput: { items: [{ path: "src/a.ts" }, { path: "src/b.ts" }] } }); mockPi.onCall({ output: "review-a", structuredOutput: { ok: "a" } }); mockPi.onCall({ output: "review-b", structuredOutput: { ok: "b" } }); mockPi.onCall({ output: "used reviews" }); const id = `async-dynamic-placeholder-${Date.now().toString(36)}`; const result = executeAsyncChain(id, { chain: [ { agent: "producer", task: "Produce targets", as: "targets", outputSchema: { type: "object" } }, { expand: { from: { output: "targets", path: "/items" }, item: "target", key: "/path", maxItems: 4 }, parallel: { agent: "reviewer", task: "Review {target.path}", label: "Review {target.path}", outputSchema: { type: "object" } }, collect: { as: "reviews" }, concurrency: 1, }, { agent: "consumer", task: "Use {outputs.reviews}" }, ], agents: [makeAgent("producer"), makeAgent("reviewer"), makeAgent("consumer")], ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-dynamic-placeholder" }, artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7 }, shareEnabled: false, maxSubagentDepth: 2, }); assert.ok(!result.isError); const statusPath = path.join(ASYNC_DIR, id, "status.json"); const deadline = Date.now() + 5_000; let status: AsyncStatusPayload | undefined; while (!status) { if (Date.now() > deadline) assert.fail(`Timed out waiting for async status file: ${statusPath}`); if (fs.existsSync(statusPath)) status = JSON.parse(fs.readFileSync(statusPath, "utf-8")) as AsyncStatusPayload; else await new Promise((resolve) => setTimeout(resolve, 50)); } assert.deepEqual(status.steps?.map((step) => step.agent), ["producer", "expand:reviewer", "consumer"]); assert.equal(status.steps?.[1]?.label, "Review {target.path}"); assert.equal(status.steps?.[1]?.outputName, "reviews"); assert.deepEqual(status.parallelGroups, [{ start: 1, count: 1, stepIndex: 1 }]); const resultPath = await waitForAsyncResultFile(id, 10_000); const finalStatus = JSON.parse(fs.readFileSync(statusPath, "utf-8")) as AsyncStatusPayload; const payload = JSON.parse(fs.readFileSync(resultPath, "utf-8")) as AsyncResultPayload; assert.equal(payload.success, true); assert.deepEqual(finalStatus.steps?.map((step) => step.agent), ["producer", "reviewer", "reviewer", "consumer"]); assert.deepEqual(finalStatus.parallelGroups, [{ start: 1, count: 2, stepIndex: 1 }]); }); it("async chains expand dynamic fanout and persist collected output", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => { mockPi.onCall({ output: "targets", structuredOutput: { items: [{ path: "src/a.ts" }, { path: "src/b.ts" }] } }); mockPi.onCall({ matchArgIncludes: "Review src/a.ts", output: "review-a", structuredOutput: { ok: "a" } }); mockPi.onCall({ matchArgIncludes: "Review src/b.ts", output: "review-b", structuredOutput: { ok: "b" } }); mockPi.onCall({ output: "used reviews" }); const id = `async-dynamic-chain-${Date.now().toString(36)}`; const result = executeAsyncChain(id, { chain: [ { agent: "producer", task: "Produce targets", as: "targets", outputSchema: { type: "object" } }, { expand: { from: { output: "targets", path: "/items" }, item: "target", key: "/path", maxItems: 4 }, parallel: { agent: "reviewer", task: "Review {target.path}", label: "Review {target.path}", outputSchema: { type: "object" }, }, collect: { as: "reviews" }, concurrency: 1, }, { agent: "consumer", task: "Use {outputs.reviews}" }, ], agents: [makeAgent("producer"), makeAgent("reviewer", { output: "context.md" }), makeAgent("consumer")], ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-dynamic" }, artifactConfig: { enabled: true, includeInput: false, includeOutput: true, includeJsonl: false, includeMetadata: false, cleanupDays: 7 }, artifactsDir: path.join(tempDir, ".pi-subagents", "artifacts"), shareEnabled: false, maxSubagentDepth: 2, }); assert.ok(!result.isError); const resultPath = await waitForAsyncResultFile(id, 10_000); const payload = JSON.parse(fs.readFileSync(resultPath, "utf-8")) as AsyncResultPayload; const status = JSON.parse(fs.readFileSync(path.join(ASYNC_DIR, id, "status.json"), "utf-8")) as AsyncStatusPayload; assert.equal(payload.success, true); assert.equal(mockPi.callCount(), 4); assert.match(readMockPiArgs(mockPi, 1).at(-1) ?? "", /Review src\/a\.ts/); assert.match(readMockPiArgs(mockPi, 2).at(-1) ?? "", /Review src\/b\.ts/); assert.match(readMockPiArgs(mockPi, 3).at(-1) ?? "", /"key":"src\/a\.ts"/); const collected = payload.outputs?.reviews?.structured as Array<{ key: string; structured: unknown }>; assert.deepEqual(collected.map((item) => item.key), ["src/a.ts", "src/b.ts"]); assert.deepEqual(collected.map((item) => item.structured), [{ ok: "a" }, { ok: "b" }]); const outputDir = path.join(tempDir, ".pi-subagents", "artifacts", "outputs", id); const dynamicOutputPaths = [ path.join(outputDir, "dynamic-1", "0-reviewer", "context.md"), path.join(outputDir, "dynamic-1", "1-reviewer", "context.md"), ]; assert.equal(fs.readFileSync(dynamicOutputPaths[0]!, "utf-8"), "review-a"); assert.equal(fs.readFileSync(dynamicOutputPaths[1]!, "utf-8"), "review-b"); const reviewerArtifacts = payload.results.slice(1, 3).map((result) => result.artifactPaths?.outputPath); assert.ok(reviewerArtifacts[0] && reviewerArtifacts[1]); assert.notEqual(reviewerArtifacts[0], reviewerArtifacts[1]); assert.equal(fs.readFileSync(reviewerArtifacts[0], "utf-8"), "review-a"); assert.equal(fs.readFileSync(reviewerArtifacts[1], "utf-8"), "review-b"); assert.match(readMockPiArgs(mockPi, 1).at(-1) ?? "", new RegExp(dynamicOutputPaths[0]!.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))); assert.match(readMockPiArgs(mockPi, 2).at(-1) ?? "", new RegExp(dynamicOutputPaths[1]!.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))); assert.equal(status.steps?.length, 4); assert.deepEqual(status.parallelGroups, [{ start: 1, count: 2, stepIndex: 1 }]); assert.equal(payload.workflowGraph?.nodes?.[1]?.kind, "dynamic-parallel-group"); assert.deepEqual(payload.workflowGraph?.nodes?.[1]?.children?.map((child) => child.itemKey), ["src/a.ts", "src/b.ts"]); assert.equal(payload.workflowGraph?.nodes?.[2]?.flatIndex, 3); }); it("rejects a shared explicit output before dynamic fanout children start", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => { mockPi.onCall({ matchArgIncludes: "Produce targets", output: "targets", structuredOutput: { items: [{ path: "src/a.ts" }, { path: "src/b.ts" }] } }); const id = `async-dynamic-explicit-output-${Date.now().toString(36)}`; const launch = executeAsyncChain(id, { chain: [ { agent: "producer", task: "Produce targets", as: "targets", outputSchema: { type: "object" } }, { expand: { from: { output: "targets", path: "/items" }, item: "target", maxItems: 2 }, parallel: { agent: "reviewer", task: "Review {target.path}", output: "shared.md" }, collect: { as: "reviews" }, concurrency: 2, }, ], agents: [makeAgent("producer"), makeAgent("reviewer")], ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-dynamic-explicit-output" }, artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7 }, shareEnabled: false, maxSubagentDepth: 2, }); assert.equal(launch.isError, undefined); const payload = await readAsyncPayload(id); assert.equal(payload.success, false); const error = payload.results.find((result) => result.error)?.error ?? ""; assert.match(error, /materialized 2 items that resolve output to the same path/); assert.match(error, /shared\.md/); assert.equal(mockPi.callCount(), 1); }); it("async dynamic fanout applies fork session files and thinking overrides to materialized children", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => { mockPi.onCall({ output: "targets", structuredOutput: { items: [{ path: "src/a.ts" }, { path: "src/b.ts" }] } }); mockPi.onCall({ output: "review-a", structuredOutput: { ok: "a" } }); mockPi.onCall({ output: "review-b", structuredOutput: { ok: "b" } }); const id = `async-dynamic-fork-thinking-${Date.now().toString(36)}`; const sessionA = path.join(tempDir, "dynamic-a.jsonl"); const sessionB = path.join(tempDir, "dynamic-b.jsonl"); const result = executeAsyncChain(id, { chain: [ { agent: "producer", task: "Produce targets", as: "targets", outputSchema: { type: "object" } }, { expand: { from: { output: "targets", path: "/items" }, item: "target", key: "/path", maxItems: 2 }, parallel: { agent: "reviewer", task: "Review {target.path}", label: "Review {target.path}", outputSchema: { type: "object" }, }, collect: { as: "reviews" }, concurrency: 1, }, ], agents: [makeAgent("producer"), makeAgent("reviewer", { model: "anthropic/claude-sonnet-4-5:high", thinking: "high" })], ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-dynamic" }, artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7 }, shareEnabled: false, sessionFilesByFlatIndex: [undefined, sessionA, sessionB], thinkingOverridesByFlatIndex: [undefined, "off", "off"], maxSubagentDepth: 2, }); assert.ok(!result.isError); const resultPath = await waitForAsyncResultFile(id, 10_000); const payload = JSON.parse(fs.readFileSync(resultPath, "utf-8")) as AsyncResultPayload; const status = JSON.parse(fs.readFileSync(path.join(ASYNC_DIR, id, "status.json"), "utf-8")) as AsyncStatusPayload; const firstDynamicArgs = readMockPiArgs(mockPi, 1); const secondDynamicArgs = readMockPiArgs(mockPi, 2); assert.equal(payload.success, true); assert.equal(firstDynamicArgs[firstDynamicArgs.indexOf("--session") + 1], sessionA); assert.equal(secondDynamicArgs[secondDynamicArgs.indexOf("--session") + 1], sessionB); assert.equal(firstDynamicArgs[firstDynamicArgs.indexOf("--model") + 1], "anthropic/claude-sonnet-4-5:off"); assert.equal(secondDynamicArgs[secondDynamicArgs.indexOf("--model") + 1], "anthropic/claude-sonnet-4-5:off"); assert.deepEqual(status.steps?.slice(1).map((step) => step.sessionFile), [sessionA, sessionB]); assert.deepEqual(status.steps?.slice(1).map((step) => step.thinking), ["off", "off"]); }); it("applies read-only acceptance roles to async dynamic children and their aggregate group", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => { mockPi.onCall({ output: "targets", structuredOutput: { items: [{ path: "src/a.ts" }, { path: "src/b.ts" }] } }); const readOnlyReport = [ "done", "```acceptance-report", JSON.stringify({ criteriaSatisfied: [{ id: "criterion-1", status: "satisfied", evidence: "inspection complete" }], changedFiles: [], testsAddedOrUpdated: [], commandsRun: [], validationOutput: [], reviewFindings: ["No blocking findings"], residualRisks: [], noStagedFiles: true, }), "```", ].join("\n"); mockPi.onCall({ output: readOnlyReport, structuredOutput: { ok: "a" } }); mockPi.onCall({ output: readOnlyReport, structuredOutput: { ok: "b" } }); const id = `async-dynamic-acceptance-role-${Date.now().toString(36)}`; const result = executeAsyncChain(id, { chain: [ { agent: "producer", task: "Produce targets", as: "targets", outputSchema: { type: "object" } }, { expand: { from: { output: "targets", path: "/items" }, item: "target", key: "/path", maxItems: 2 }, parallel: { agent: "explorer", task: "Explore {target.path}", outputSchema: { type: "object" } }, collect: { as: "reviews" }, concurrency: 1, }, ], agents: [makeAgent("producer"), makeAgent("explorer", { acceptanceRole: "read-only" })], ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-dynamic-role" }, artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7 }, shareEnabled: false, maxSubagentDepth: 2, }); assert.ok(!result.isError); const payload = await readAsyncPayload(id); const explorerResults = payload.results.filter((child) => child.agent === "explorer"); assert.deepEqual(explorerResults.map((child) => child.acceptance?.effectiveAcceptance?.level), ["attested", "attested"]); const dynamicNode = payload.workflowGraph?.nodes?.[1]; assert.equal(dynamicNode?.acceptanceStatus, "attested"); assert.deepEqual(dynamicNode?.children?.map((child) => child.acceptanceStatus), ["attested", "attested"]); }); it("infers async dynamic acceptance after materializing item templates", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => { mockPi.onCall({ output: "targets", structuredOutput: { items: [{ path: "src/a.ts" }, { path: "src/b.ts" }] } }); const writerReport = [ "done", "```acceptance-report", JSON.stringify({ criteriaSatisfied: [{ id: "criterion-1", status: "satisfied", evidence: "patch complete" }], changedFiles: ["src/a.ts"], testsAddedOrUpdated: ["test/a.test.ts"], commandsRun: [{ command: "npm test", result: "passed", summary: "passed" }], validationOutput: ["tests passed"], residualRisks: [], noStagedFiles: true, }), "```", ].join("\n"); mockPi.onCall({ output: writerReport, structuredOutput: { ok: "a" } }); mockPi.onCall({ output: writerReport, structuredOutput: { ok: "b" } }); const id = `async-dynamic-role-item-template-${Date.now().toString(36)}`; executeAsyncChain(id, { chain: [ { agent: "producer", task: "Produce targets", as: "targets", outputSchema: { type: "object" } }, { expand: { from: { output: "targets", path: "/items" }, item: "target", key: "/path", maxItems: 2 }, parallel: { agent: "explorer", task: "Patch {target.path}", outputSchema: { type: "object" } }, collect: { as: "reviews" }, concurrency: 1, }, ], agents: [makeAgent("producer"), makeAgent("explorer", { acceptanceRole: "read-only" })], ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-dynamic-role-item" }, artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7 }, shareEnabled: false, maxSubagentDepth: 2, }); const payload = await readAsyncPayload(id); const explorerResults = payload.results.filter((child) => child.agent === "explorer"); assert.deepEqual(explorerResults.map((child) => child.acceptance?.effectiveAcceptance?.level), ["checked", "checked"]); const dynamicNode = payload.workflowGraph?.nodes?.[1]; assert.equal(payload.success, true); assert.equal(dynamicNode?.acceptanceStatus, "rejected"); assert.deepEqual(dynamicNode?.children?.map((child) => child.acceptanceStatus), ["rejected", "rejected"]); }); it("cancels dynamic fanout aggregate acceptance when the run times out", { skip: !isAsyncAvailable() ? "jiti not available" : process.platform === "win32" ? "timeout signal delivery intermittent on Windows CI" : undefined }, async () => { mockPi.onCall({ output: "targets", structuredOutput: { items: [{ path: "src/a.ts" }] } }); mockPi.onCall({ output: "review-a", structuredOutput: { ok: "a" } }); const id = `async-dynamic-acceptance-timeout-${Date.now().toString(36)}`; const startedAt = Date.now(); executeAsyncChain(id, { chain: [ { agent: "producer", task: "Produce targets", as: "targets", outputSchema: { type: "object" } }, { expand: { from: { output: "targets", path: "/items" }, item: "target", key: "/path", maxItems: 4 }, parallel: { agent: "reviewer", task: "Review {target.path}", outputSchema: { type: "object" }, acceptance: { level: "checked" } }, collect: { as: "reviews" }, acceptance: { level: "verified", verify: [{ id: "slow", command: `${process.execPath} -e "setTimeout(()=>process.exit(0), 5000)"`, timeoutMs: 10_000 }], }, }, ], agents: [makeAgent("producer"), makeAgent("reviewer")], ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-dynamic-acceptance-timeout" }, artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7 }, shareEnabled: false, maxSubagentDepth: 2, timeoutMs: 1_000, }); const resultPath = await waitForAsyncResultFile(id, 5_000); const elapsedMs = Date.now() - startedAt; const payload = JSON.parse(fs.readFileSync(resultPath, "utf-8")) as AsyncResultPayload; const status = JSON.parse(fs.readFileSync(path.join(ASYNC_DIR, id, "status.json"), "utf-8")) as AsyncStatusPayload; const dynamicNode = payload.workflowGraph?.nodes?.[1] as { status?: string; error?: string; acceptanceStatus?: string } | undefined; assert.equal(payload.state, "failed"); assert.equal(payload.timedOut, true); assert.equal(payload.results.at(-1)?.timedOut, true); assert.equal(payload.results.at(-1)?.acceptance, undefined); assert.equal(dynamicNode?.status, "failed"); assert.match(dynamicNode?.error ?? "", /Subagent timed out after 1000ms\./); assert.notEqual(dynamicNode?.acceptanceStatus, "verified"); assert.equal(status.timedOut, true); assert.ok(elapsedMs < 3_000, `timeout should cancel dynamic aggregate acceptance promptly, elapsed ${elapsedMs}ms`); }); it("async dynamic fanout recomputes later child intercom targets by final flat index", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => { mockPi.onCall({ output: "targets", structuredOutput: { items: [{ path: "src/a.ts" }, { path: "src/b.ts" }] } }); mockPi.onCall({ output: "review-a", structuredOutput: { ok: "a" } }); mockPi.onCall({ output: "review-b", structuredOutput: { ok: "b" } }); mockPi.onCall({ echoEnv: ["SELESAI_SUBAGENT_INTERCOM_SESSION_NAME"] }); const id = `async-dynamic-targets-${Date.now().toString(36)}`; const result = executeAsyncChain(id, { chain: [ { agent: "producer", task: "Produce targets", as: "targets", outputSchema: { type: "object" } }, { expand: { from: { output: "targets", path: "/items" }, item: "target", key: "/path", maxItems: 4 }, parallel: { agent: "reviewer", task: "Review {target.path}", outputSchema: { type: "object" } }, collect: { as: "reviews" }, concurrency: 1, }, { agent: "consumer", task: "Use {outputs.reviews}" }, ], agents: [makeAgent("producer"), makeAgent("reviewer"), makeAgent("consumer")], ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-dynamic-targets" }, artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7 }, shareEnabled: false, maxSubagentDepth: 2, controlIntercomTarget: "subagent-orchestrator-test", childIntercomTarget: (agent: string, index: number) => `subagent-${agent}-${id}-${index + 1}`, }); assert.ok(!result.isError); const resultPath = await waitForAsyncResultFile(id, 10_000); const payload = JSON.parse(fs.readFileSync(resultPath, "utf-8")) as AsyncResultPayload; const expectedConsumerTarget = `subagent-consumer-${id}-4`; assert.equal(payload.success, true); assert.equal(payload.results[3]?.intercomTarget, expectedConsumerTarget); assert.deepEqual(JSON.parse(payload.results[3]?.output ?? "{}"), { SELESAI_SUBAGENT_INTERCOM_SESSION_NAME: expectedConsumerTarget }); }); it("async dynamic pre-spawn failures persist failed graph status and error", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => { mockPi.onCall({ output: "targets", structuredOutput: { items: [{ path: "src/a.ts" }, { path: "src/b.ts" }] } }); const id = `async-dynamic-prespawn-fail-${Date.now().toString(36)}`; const result = executeAsyncChain(id, { chain: [ { agent: "producer", task: "Produce targets", as: "targets", outputSchema: { type: "object" } }, { expand: { from: { output: "targets", path: "/items" }, item: "target", key: "/path", maxItems: 1 }, parallel: { agent: "reviewer", task: "Review {target.path}" }, collect: { as: "reviews" }, }, ], agents: [makeAgent("producer"), makeAgent("reviewer")], ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-dynamic-fail" }, artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7 }, shareEnabled: false, maxSubagentDepth: 2, }); assert.ok(!result.isError); const resultPath = await waitForAsyncResultFile(id, 10_000); const payload = JSON.parse(fs.readFileSync(resultPath, "utf-8")) as AsyncResultPayload; const status = JSON.parse(fs.readFileSync(path.join(ASYNC_DIR, id, "status.json"), "utf-8")) as AsyncStatusPayload & { workflowGraph?: AsyncResultPayload["workflowGraph"]; error?: string }; assert.equal(payload.success, false); assert.match(payload.results.at(-1)?.error ?? "", /exceeding maxItems 1/); assert.equal(payload.workflowGraph?.nodes?.[1]?.status, "failed"); assert.match(payload.workflowGraph?.nodes?.[1]?.error ?? "", /exceeding maxItems 1/); assert.equal(status.state, "failed"); assert.match(status.error ?? "", /exceeding maxItems 1/); assert.equal(status.workflowGraph?.nodes?.[1]?.status, "failed"); }); it("async dynamic collect schema failures persist failed graph status and details", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => { mockPi.onCall({ output: "targets", structuredOutput: { items: [{ path: "src/a.ts" }] } }); mockPi.onCall({ output: "review-a", structuredOutput: { ok: "a" } }); const id = `async-dynamic-collect-fail-${Date.now().toString(36)}`; const result = executeAsyncChain(id, { chain: [ { agent: "producer", task: "Produce targets", as: "targets", outputSchema: { type: "object" } }, { expand: { from: { output: "targets", path: "/items" }, item: "target", key: "/path", maxItems: 4 }, parallel: { agent: "reviewer", task: "Review {target.path}", outputSchema: { type: "object" } }, collect: { as: "reviews", outputSchema: { type: "object" } }, }, ], agents: [makeAgent("producer"), makeAgent("reviewer")], ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-dynamic-collect-fail" }, artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7 }, shareEnabled: false, maxSubagentDepth: 2, }); assert.ok(!result.isError); const resultPath = await waitForAsyncResultFile(id, 10_000); const payload = JSON.parse(fs.readFileSync(resultPath, "utf-8")) as AsyncResultPayload; assert.equal(payload.success, false); assert.match(payload.results.at(-1)?.error ?? "", /Collected output validation failed/); assert.ok(Array.isArray(payload.results.at(-1)?.structuredOutput), "failed collect result should preserve ordered collection details"); assert.equal(payload.workflowGraph?.nodes?.[1]?.status, "failed"); assert.match(payload.workflowGraph?.nodes?.[1]?.error ?? "", /Collected output validation failed/); }); it("top-level async worktree parallel resolves reads against the worktree and output under project artifacts", { skip: !isAsyncAvailable() || !createSubagentExecutor ? "jiti or executor not available" : process.platform === "win32" ? "worktree path separators unreliable on Windows CI" : undefined }, async () => { const repoDir = createRepo("pi-subagent-async-worktree-"); try { mockPi.onCall({ output: "Worktree report" }); const executor = createSubagentExecutor!({ pi: { events: createEventBus(), getSessionName: () => undefined }, state: { baseCwd: repoDir, currentSessionId: null, asyncJobs: new Map(), foregroundControls: new Map(), lastForegroundControlId: null }, config: {}, asyncByDefault: false, tempArtifactsDir: repoDir, getSubagentSessionRoot: () => repoDir, expandTilde: (p: string) => p, discoverAgents: () => ({ agents: [makeAgent("worker")] }), }); const result = await executor.execute( "async-parallel-worktree-fields", { tasks: [{ agent: "worker", task: "Do worktree work", output: "report.md", reads: ["input.md"] }], async: true, clarify: false, worktree: true, }, new AbortController().signal, undefined, makeMinimalCtx(repoDir), ); const asyncId = result.details?.asyncId; assert.ok(asyncId, "expected asyncId"); const worktreeCwd = path.join(os.tmpdir(), `pi-worktree-${asyncId}-s0-0`); const args = await waitForMockPiArgs(mockPi, 0); const taskArg = args.at(-1) ?? ""; assert.ok(taskArg.includes(`[Read from: ${path.join(worktreeCwd, "input.md")}]`)); assert.ok(taskArg.includes(`Write your findings to exactly this path: ${path.join(repoDir, ".pi-subagents", "artifacts", "outputs", asyncId, "report.md")}`)); const resultPath = await waitForAsyncResultFile(asyncId, 90_000); const payload = JSON.parse(fs.readFileSync(resultPath, "utf-8")) as AsyncResultPayload; const status = JSON.parse(fs.readFileSync(path.join(result.details!.asyncDir!, "status.json"), "utf-8")) as AsyncStatusPayload; assert.equal(payload.parallelHandoff?.version, 1); assert.equal(payload.parallelHandoff?.path, path.join(result.details!.asyncDir!, "handoff.json")); assert.deepEqual(status.parallelHandoff, payload.parallelHandoff); assert.equal(payload.parallelHandoff?.childCount, 1); assert.equal(payload.parallelHandoff?.cleanupState, "complete"); assert.equal(fs.existsSync(worktreeCwd), false, "temporary worktree should be removed before handoff publication"); const handoff = JSON.parse(fs.readFileSync(payload.parallelHandoff!.path!, "utf-8")) as { version: number; groups: Array<{ children: Array<{ agent: string; status: string; patch: { path: string } }>; cleanup: { state: string } }>; }; assert.equal(handoff.version, 1); assert.equal(handoff.groups[0]!.children[0]!.agent, "worker"); assert.equal(handoff.groups[0]!.children[0]!.status, "completed"); assert.equal(handoff.groups[0]!.cleanup.state, "complete"); assert.equal(fs.existsSync(handoff.groups[0]!.children[0]!.patch.path), true, "patch artifact should outlive cleanup"); } finally { removeTempDir(repoDir); } }); it("readStatus caches unchanged files and invalidates same-mtime replacements", () => { const dir = createTempDir(); try { const statusPath = path.join(dir, "status.json"); const fixedTimestamp = new Date(1_700_000_000_000); const statusData = { runId: "cache-test", state: "running", mode: "single", startedAt: fixedTimestamp.getTime(), }; fs.writeFileSync(statusPath, JSON.stringify(statusData)); fs.utimesSync(statusPath, fixedTimestamp, fixedTimestamp); const cached = readStatus(dir); assert.ok(cached); assert.strictEqual(readStatus(dir), cached); writeAtomicJson(statusPath, { ...statusData, state: "stopped" }); fs.utimesSync(statusPath, fixedTimestamp, fixedTimestamp); assert.equal(fs.statSync(statusPath).mtimeMs, fixedTimestamp.getTime()); const replaced = readStatus(dir); assert.ok(replaced); assert.equal(replaced.state, "stopped"); assert.notStrictEqual(replaced, cached); } finally { removeTempDir(dir); } }); it("readStatus throws for malformed status files", () => { const dir = createTempDir(); try { fs.writeFileSync(path.join(dir, "status.json"), "{bad-json", "utf-8"); assert.throws(() => readStatus(dir), /Failed to parse async status file/); } finally { removeTempDir(dir); } }); it("background runs record fallback attempts and final model", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => { mockPi.onCall({ jsonl: [{ type: "message_end", message: { role: "assistant", content: [{ type: "text", text: "primary failed" }], model: "openai/gpt-5-mini", errorMessage: "rate limit exceeded", usage: { input: 10, output: 5, cacheRead: 0, cacheWrite: 0, cost: { total: 0.01 } }, }, }], exitCode: 1, }); mockPi.onCall({ output: "Recovered asynchronously" }); const id = `async-fallback-${Date.now().toString(36)}`; const sessionRoot = path.join(tempDir, "sessions"); const asyncDir = path.join(ASYNC_DIR, id); const resultPath = path.join(RESULTS_DIR, `${id}.json`); const run = executeAsyncSingle(id, { agent: "worker", task: "Do work", acceptance: { level: "none", reason: "descriptor persistence coverage" }, agentConfig: makeAgent("worker", { model: "openai/gpt-5-mini:high", fallbackModels: ["anthropic/claude-sonnet-4:low"], }), ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" }, availableModels: [ { provider: "openai", id: "gpt-5-mini", fullId: "openai/gpt-5-mini" }, { provider: "anthropic", id: "claude-sonnet-4", fullId: "anthropic/claude-sonnet-4" }, ], artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7, }, shareEnabled: false, sessionRoot, maxSubagentDepth: 2, }); assert.equal(run.details.asyncId, id); const started = Date.now(); while (!fs.existsSync(resultPath)) { if (Date.now() - started > 15000) { assert.fail(`Timed out waiting for async result file: ${resultPath}`); } await new Promise((resolve) => setTimeout(resolve, 100)); } const descriptorPath = path.join(asyncDir, "recovery-descriptor.json"); const descriptor = JSON.parse(fs.readFileSync(descriptorPath, "utf-8")); assert.equal(descriptor.sourceRunId, id); assert.equal(descriptor.agent, "worker"); assert.equal(descriptor.model, "openai/gpt-5-mini:high"); assert.deepEqual(descriptor.fallbackModels, ["anthropic/claude-sonnet-4:low"]); assert.equal(descriptor.cwd, tempDir); assert.equal(descriptor.sessionDir, path.join(sessionRoot, `async-${id}`)); assert.deepEqual(descriptor.acceptance, { level: "none", reason: "descriptor persistence coverage" }); assert.equal(Object.hasOwn(descriptor.acceptance, "explicit"), false); assert.equal(Object.hasOwn(descriptor.acceptance, "inferredReason"), false); assert.equal(Object.hasOwn(descriptor, "task"), false); if (process.platform !== "win32") assert.equal(fs.statSync(descriptorPath).mode & 0o777, 0o600); const payload = JSON.parse(fs.readFileSync(resultPath, "utf-8")); assert.equal(payload.lifecycleArtifactVersion, SUBAGENT_LIFECYCLE_ARTIFACT_VERSION); assert.equal(payload.success, true); assert.equal(payload.results[0].model, "anthropic/claude-sonnet-4:low"); assert.deepEqual(payload.results[0].attemptedModels, ["openai/gpt-5-mini:high", "anthropic/claude-sonnet-4:low"]); assert.equal(payload.results[0].modelAttempts.length, 2); assert.deepEqual(payload.results[0].totalCost, { inputTokens: 110, outputTokens: 55, costUsd: 0.011 }); assert.deepEqual(payload.totalCost, { inputTokens: 110, outputTokens: 55, costUsd: 0.011 }); const statusPayload = JSON.parse(fs.readFileSync(path.join(asyncDir, "status.json"), "utf-8")) as AsyncStatusPayload; assert.equal(statusPayload.lifecycleArtifactVersion, SUBAGENT_LIFECYCLE_ARTIFACT_VERSION); assert.equal(statusPayload.steps[0]?.model, "anthropic/claude-sonnet-4:low"); assert.equal(statusPayload.steps[0]?.thinking, "low"); assert.ok(statusPayload.totalTokens!.total > 0); assert.ok(statusPayload.steps[0]?.tokens!.total > 0); assert.deepEqual(statusPayload.steps[0]?.totalCost, { inputTokens: 110, outputTokens: 55, costUsd: 0.011 }); assert.deepEqual(statusPayload.totalCost, { inputTokens: 110, outputTokens: 55, costUsd: 0.011 }); const events = fs.readFileSync(path.join(asyncDir, "events.jsonl"), "utf-8").trim().split("\n").map((line) => JSON.parse(line)); assert.equal(events.find((event) => event.type === "subagent.run.started")?.lifecycleArtifactVersion, SUBAGENT_LIFECYCLE_ARTIFACT_VERSION); const completed = events.find((event) => event.type === "subagent.run.completed"); assert.equal(completed?.lifecycleArtifactVersion, SUBAGENT_LIFECYCLE_ARTIFACT_VERSION); assert.deepEqual(completed?.totalCost, { inputTokens: 110, outputTokens: 55, costUsd: 0.011 }); assert.match(fs.readFileSync(path.join(asyncDir, "output-0.log"), "utf-8"), /Recovered asynchronously/); assert.equal(mockPi.callCount(), 2); }); it("background runs retry the fallback model when the provider stream ends without finish_reason", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => { mockPi.onCall({ jsonl: [{ type: "message_end", message: { role: "assistant", content: [{ type: "text", text: "stream broke mid-response" }], model: "openai/gpt-5-mini", errorMessage: "Stream ended without finish_reason", usage: { input: 10, output: 0, cacheRead: 0, cacheWrite: 0, cost: { total: 0.001 } }, }, }], exitCode: 1, }); mockPi.onCall({ output: "Recovered after stream failure" }); const id = `async-fallback-stream-${Date.now().toString(36)}`; executeAsyncSingle(id, { agent: "worker", task: "Do work", agentConfig: makeAgent("worker", { model: "openai/gpt-5-mini:high", fallbackModels: ["anthropic/claude-sonnet-4:low"], }), ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" }, availableModels: [ { provider: "openai", id: "gpt-5-mini", fullId: "openai/gpt-5-mini" }, { provider: "anthropic", id: "claude-sonnet-4", fullId: "anthropic/claude-sonnet-4" }, ], artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7, }, shareEnabled: false, maxSubagentDepth: 2, }); const payload = JSON.parse(fs.readFileSync(await waitForAsyncResultFile(id), "utf-8")); assert.equal(payload.success, true); assert.deepEqual(payload.results[0].attemptedModels, ["openai/gpt-5-mini:high", "anthropic/claude-sonnet-4:low"]); assert.match(payload.results[0].output ?? "", /Recovered after stream failure/); assert.equal(mockPi.callCount(), 2); }); it("background runs do not retry the fallback model for a trailing tool failure", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => { mockPi.onCall({ jsonl: [ mockAssistantMessage("checking connectivity", "tool_use"), events.toolResult("bash", "curl: (28) Connection timed out after 5000 ms\nCommand exited with code 1", true), ], exitCode: 0, }); mockPi.onCall({ output: "fallback must not run" }); const id = `async-fallback-toolfail-${Date.now().toString(36)}`; executeAsyncSingle(id, { agent: "worker", task: "Do work", agentConfig: makeAgent("worker", { model: "openai/gpt-5-mini:high", fallbackModels: ["anthropic/claude-sonnet-4:low"], }), ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" }, availableModels: [ { provider: "openai", id: "gpt-5-mini", fullId: "openai/gpt-5-mini" }, { provider: "anthropic", id: "claude-sonnet-4", fullId: "anthropic/claude-sonnet-4" }, ], artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7, }, shareEnabled: false, maxSubagentDepth: 2, }); const payload = JSON.parse(fs.readFileSync(await waitForAsyncResultFile(id), "utf-8")); assert.equal(payload.success, false); assert.equal(payload.results[0].modelAttempts.length, 1); assert.match(payload.results[0].error ?? "", /^bash failed \(exit 1\)/); assert.match(payload.results[0].error ?? "", /timed out/i); assert.equal(mockPi.callCount(), 1); }); it("background single thinking override replaces primary and fallback suffixes", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => { mockPi.onCall({ jsonl: [{ type: "message_end", message: { role: "assistant", content: [{ type: "text", text: "primary failed" }], model: "openai/gpt-5-mini", errorMessage: "rate limit exceeded", usage: { input: 10, output: 5, cacheRead: 0, cacheWrite: 0, cost: { total: 0.01 } }, }, }], exitCode: 1, }); mockPi.onCall({ output: "Recovered asynchronously" }); const id = `async-fallback-thinking-off-${Date.now().toString(36)}`; const run = executeAsyncSingle(id, { agent: "worker", task: "Do work", agentConfig: makeAgent("worker", { model: "openai/gpt-5-mini:high", fallbackModels: ["anthropic/claude-sonnet-4:low"], thinking: "high", }), ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" }, availableModels: [ { provider: "openai", id: "gpt-5-mini", fullId: "openai/gpt-5-mini" }, { provider: "anthropic", id: "claude-sonnet-4", fullId: "anthropic/claude-sonnet-4" }, ], artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7, }, shareEnabled: false, sessionRoot: path.join(tempDir, "sessions"), thinkingOverride: "off", maxSubagentDepth: 2, }); assert.equal(run.details.asyncId, id); const resultPath = await waitForAsyncResultFile(id); const payload = JSON.parse(fs.readFileSync(resultPath, "utf-8")) as AsyncResultPayload; const firstArgs = readMockPiArgs(mockPi, 0); const secondArgs = readMockPiArgs(mockPi, 1); assert.equal(payload.success, true); assert.equal(payload.results[0].model, "anthropic/claude-sonnet-4:off"); assert.deepEqual(payload.results[0].attemptedModels, ["openai/gpt-5-mini:off", "anthropic/claude-sonnet-4:off"]); assert.equal(firstArgs[firstArgs.indexOf("--model") + 1], "openai/gpt-5-mini:off"); assert.equal(secondArgs[secondArgs.indexOf("--model") + 1], "anthropic/claude-sonnet-4:off"); }); it("background runs retry fallback models when a zero-exit attempt has empty output", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => { mockPi.onCall({ jsonl: [{ type: "message_end", message: { role: "assistant", content: [{ type: "text", text: "" }], model: "openai/gpt-5-mini", stopReason: "error", usage: { input: 10, output: 0, cacheRead: 0, cacheWrite: 0, cost: { total: 0.01 } }, }, }], exitCode: 0, }); mockPi.onCall({ output: "Recovered asynchronously from empty output" }); const id = `async-empty-output-fallback-${Date.now().toString(36)}`; executeAsyncSingle(id, { agent: "worker", task: "Do work", agentConfig: makeAgent("worker", { model: "openai/gpt-5-mini", fallbackModels: ["anthropic/claude-sonnet-4"], }), ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" }, artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7, }, shareEnabled: false, maxSubagentDepth: 2, }); const resultPath = await waitForAsyncResultFile(id); const payload = JSON.parse(fs.readFileSync(resultPath, "utf-8")) as AsyncResultPayload; assert.equal(payload.success, true); assert.equal(payload.results[0]?.model, "anthropic/claude-sonnet-4"); assert.match(payload.results[0]?.output ?? "", /Recovered asynchronously from empty output/); assert.match(payload.results[0]?.modelAttempts?.[0]?.error ?? "", /no output/i); assert.deepEqual(payload.results[0]?.modelAttempts?.map((attempt) => attempt.success), [false, true]); assert.equal(mockPi.callCount(), 2); }); it("background runs prefer empty-output fallback over an earlier tool error", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => { mockPi.onCall({ jsonl: [ events.toolResult("read", "ENOENT: no such file or directory", true), events.toolResult("read", "recovered file contents"), { type: "message_end", message: { role: "assistant", content: [], model: "openai/gpt-5-mini", stopReason: "stop", usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: { total: 0 } }, }, }, ], exitCode: 0, }); mockPi.onCall({ output: "Recovered asynchronously on fallback" }); const id = `async-empty-output-after-tool-error-${Date.now().toString(36)}`; executeAsyncSingle(id, { agent: "worker", task: "Do work", agentConfig: makeAgent("worker", { model: "openai/gpt-5-mini", fallbackModels: ["anthropic/claude-sonnet-4"], }), ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" }, artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7, }, shareEnabled: false, maxSubagentDepth: 2, }); const resultPath = await waitForAsyncResultFile(id); const payload = JSON.parse(fs.readFileSync(resultPath, "utf-8")) as AsyncResultPayload; assert.equal(payload.success, true); assert.equal(payload.results[0]?.model, "anthropic/claude-sonnet-4"); assert.match(payload.results[0]?.modelAttempts?.[0]?.error ?? "", /no output/i); assert.equal(mockPi.callCount(), 2); }); it("background runs fail zero-exit provider errors when no fallback succeeds", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => { mockPi.onCall({ jsonl: [{ type: "message_end", message: { role: "assistant", content: [{ type: "text", text: "quota hit" }], model: "openai/gpt-5-mini", errorMessage: "429 quota exceeded", usage: { input: 10, output: 5, cacheRead: 0, cacheWrite: 0, cost: { total: 0.01 } }, }, }], exitCode: 0, }); const id = `async-zero-exit-provider-error-${Date.now().toString(36)}`; const asyncDir = path.join(ASYNC_DIR, id); executeAsyncSingle(id, { agent: "worker", task: "Do work", agentConfig: makeAgent("worker", { model: "openai/gpt-5-mini" }), ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" }, artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7, }, shareEnabled: false, maxSubagentDepth: 2, }); const resultPath = await waitForAsyncResultFile(id); const payload = JSON.parse(fs.readFileSync(resultPath, "utf-8")) as AsyncResultPayload; assert.equal(payload.success, false); assert.match(payload.results[0]?.error ?? "", /429 quota exceeded/); const statusPayload = JSON.parse(fs.readFileSync(path.join(asyncDir, "status.json"), "utf-8")) as AsyncStatusPayload; assert.equal(statusPayload.state, "failed"); assert.match(statusPayload.steps?.[0]?.error ?? "", /429 quota exceeded/); }); it("background runs treat recovered child errors as successful", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => { mockPi.onCall({ jsonl: [ events.toolResult("read", "EISDIR: illegal operation on a directory", true), { type: "message_end", message: { role: "assistant", content: [{ type: "text", text: "temporary provider failure" }], model: "openai/gpt-5-mini", stopReason: "error", errorMessage: "provider transport failed", usage: { input: 10, output: 5, cacheRead: 0, cacheWrite: 0, cost: { total: 0.01 } }, }, }, events.assistantMessage("Recovered asynchronously"), ], }); const id = `async-recovered-child-error-${Date.now().toString(36)}`; const asyncDir = path.join(ASYNC_DIR, id); executeAsyncSingle(id, { agent: "worker", task: "Do work", agentConfig: makeAgent("worker", { model: "openai/gpt-5-mini" }), ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" }, artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7, }, shareEnabled: false, maxSubagentDepth: 2, }); const resultPath = await waitForAsyncResultFile(id); const payload = JSON.parse(fs.readFileSync(resultPath, "utf-8")) as AsyncResultPayload; assert.equal(payload.success, true); assert.equal(payload.state, "complete"); assert.equal(payload.exitCode, 0); assert.equal(payload.results[0]?.success, true); assert.equal(payload.results[0]?.error, undefined); assert.equal(payload.results[0]?.output, "Recovered asynchronously"); const statusPayload = JSON.parse(fs.readFileSync(path.join(asyncDir, "status.json"), "utf-8")) as AsyncStatusPayload; assert.equal(statusPayload.state, "complete"); assert.equal(statusPayload.steps?.[0]?.status, "complete"); assert.equal(statusPayload.steps?.[0]?.exitCode, 0); }); it("background runs keep provider errors failed when followed only by empty assistant output", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => { mockPi.onCall({ jsonl: [ { type: "message_end", message: { role: "assistant", content: [{ type: "text", text: "temporary provider failure" }], model: "openai/gpt-5-mini", stopReason: "error", errorMessage: "provider transport failed", usage: { input: 10, output: 5, cacheRead: 0, cacheWrite: 0, cost: { total: 0.01 } }, }, }, events.assistantMessage(""), ], }); const id = `async-provider-error-empty-stop-${Date.now().toString(36)}`; const asyncDir = path.join(ASYNC_DIR, id); executeAsyncSingle(id, { agent: "worker", task: "Do work", agentConfig: makeAgent("worker", { model: "openai/gpt-5-mini" }), ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" }, artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7, }, shareEnabled: false, maxSubagentDepth: 2, }); const resultPath = await waitForAsyncResultFile(id); const payload = JSON.parse(fs.readFileSync(resultPath, "utf-8")) as AsyncResultPayload; assert.equal(payload.success, false); assert.equal(payload.state, "failed"); assert.equal(payload.exitCode, 1); assert.equal(payload.results[0]?.success, false); assert.match(payload.results[0]?.error ?? "", /provider transport failed/); assert.equal(payload.results[0]?.output, ""); const statusPayload = JSON.parse(fs.readFileSync(path.join(asyncDir, "status.json"), "utf-8")) as AsyncStatusPayload; assert.equal(statusPayload.state, "failed"); assert.equal(statusPayload.steps?.[0]?.status, "failed"); assert.equal(statusPayload.steps?.[0]?.exitCode, 1); }); it("background file-only runs write full output but return only a file reference", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => { mockPi.onCall({ output: "async full output\nwith details" }); const id = `async-file-only-${Date.now().toString(36)}`; const resultPath = path.join(RESULTS_DIR, `${id}.json`); const outputPath = path.join(tempDir, "async-file-only.md"); const run = executeAsyncSingle(id, { agent: "analyst", task: "Analyze without modifying files", agentConfig: makeAgent("analyst", { tools: ["read", "grep", "find", "ls"] }), ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" }, artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7, }, shareEnabled: false, sessionRoot: path.join(tempDir, "sessions"), output: outputPath, outputMode: "file-only", maxSubagentDepth: 2, }); assert.equal(run.details.asyncId, id); const call = await waitForMockPiCall(mockPi, 0); const taskArg = call.args.at(-1) ?? ""; const systemPrompt = call.systemPrompts[0]?.text ?? ""; for (const instruction of [taskArg, systemPrompt]) { assert.match(instruction, /Return the complete artifact in your final response\./); assert.match(instruction, /runtime will persist it to exactly this path:/); assert.match(instruction, /Do not call contact_supervisor merely because no write-capable tool is available\./); assert.doesNotMatch(instruction, /Write your findings to exactly this path/); } const deadline = Date.now() + 10_000; while (!fs.existsSync(resultPath)) { if (Date.now() > deadline) assert.fail(`Timed out waiting for async result file: ${resultPath}`); await new Promise((resolve) => setTimeout(resolve, 100)); } const payload = JSON.parse(fs.readFileSync(resultPath, "utf-8")) as AsyncResultPayload; assert.equal(payload.success, true); assert.match(payload.summary ?? "", /Output saved to:/); assert.match(payload.summary ?? "", /2 lines/); assert.doesNotMatch(payload.summary ?? "", /async full output/); assert.match(payload.results[0]?.output ?? "", /Output saved to:/); assert.doesNotMatch(payload.results[0]?.output ?? "", /async full output/); assert.equal(fs.readFileSync(outputPath, "utf-8"), "async full output\nwith details"); }); it("background single runs route relative outputs to outputBaseDir", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => { mockPi.onCall({ output: "async configured report" }); const id = `async-configured-output-base-${Date.now().toString(36)}`; const outputBaseDir = path.join(tempDir, "async-configured-outputs"); const run = executeAsyncSingle(id, { agent: "researcher", task: "Write report", agentConfig: makeAgent("researcher", { output: "context.md" }), ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" }, artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7, }, shareEnabled: false, sessionRoot: path.join(tempDir, "sessions"), output: "context.md", outputBaseDir, maxSubagentDepth: 2, }); assert.equal(run.details.asyncId, id); const outputPath = path.join(outputBaseDir, "context.md"); const call = await waitForMockPiCall(mockPi, 0); const taskArg = call.args.at(-1) ?? ""; assert.match(taskArg, new RegExp(`Write your findings to exactly this path: ${escapeRegExp(outputPath)}`)); const resultPath = await waitForAsyncResultFile(id); const payload = JSON.parse(fs.readFileSync(resultPath, "utf-8")) as AsyncResultPayload; assert.equal(payload.success, true); assert.equal(fs.readFileSync(outputPath, "utf-8"), "async configured report"); assert.equal(fs.existsSync(path.join(tempDir, "context.md")), false); }); it("background single runs make output overrides authoritative in the child system prompt", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => { mockPi.onCall({ output: "async override report" }); const id = `async-output-override-system-prompt-${Date.now().toString(36)}`; const outputPath = path.join(tempDir, "async-custom-report.md"); const run = executeAsyncSingle(id, { agent: "researcher", task: "Write report", agentConfig: makeAgent("researcher", { output: "default-report.md", systemPrompt: "Output format (`default-report.md`):\n\nWrite the full report to default-report.md.", }), ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" }, artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7, }, shareEnabled: false, sessionRoot: path.join(tempDir, "sessions"), output: outputPath, maxSubagentDepth: 2, }); assert.equal(run.details.asyncId, id); const call = await waitForMockPiCall(mockPi, 0); const taskArg = call.args.at(-1) ?? ""; const systemPrompt = call.systemPrompts[0]?.text ?? ""; assert.match(taskArg, new RegExp(`Write your findings to exactly this path: ${escapeRegExp(outputPath)}`)); assert.match(systemPrompt, /Output format \(`default-report\.md`\):/); assert.match(systemPrompt, /Runtime output path override:/); assert.match(systemPrompt, new RegExp(`Write your findings to exactly this path: ${escapeRegExp(outputPath)}`)); assert.match(systemPrompt, /Ignore any other output filename or output path mentioned elsewhere/); await waitForAsyncResultFile(id); }); it("background single runs treat string false as disabled output", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => { mockPi.onCall({ output: "async inline report" }); const id = `async-string-false-output-${Date.now().toString(36)}`; const run = executeAsyncSingle(id, { agent: "worker", task: "Do work", agentConfig: makeAgent("worker", { output: "default-report.md" }), ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" }, artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7, }, shareEnabled: false, sessionRoot: path.join(tempDir, "sessions"), output: "false", maxSubagentDepth: 2, }); assert.equal(run.details.asyncId, id); const resultPath = await waitForAsyncResultFile(id); const payload = JSON.parse(fs.readFileSync(resultPath, "utf-8")) as AsyncResultPayload; assert.equal(payload.success, true); assert.equal(payload.results[0]?.output, "async inline report"); assert.doesNotMatch(payload.summary ?? "", /Output saved to:/); assert.equal(fs.existsSync(path.join(tempDir, "false")), false); assert.equal(fs.existsSync(path.join(tempDir, "default-report.md")), false); assert.doesNotMatch(readLastMockPiArgs(mockPi).at(-1) ?? "", /Write your findings to(?: exactly this path)?:/); }); it("background runs detect hidden tool failures even when the child exits 0", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => { mockPi.onCall({ jsonl: [events.toolResult("bash", "connection refused")], }); const id = `async-hidden-failure-${Date.now().toString(36)}`; const resultPath = path.join(RESULTS_DIR, `${id}.json`); const sessionRoot = path.join(tempDir, "sessions"); executeAsyncSingle(id, { agent: "worker", task: "Deploy app", agentConfig: makeAgent("worker"), ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" }, artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7, }, shareEnabled: false, sessionRoot, maxSubagentDepth: 2, }); const deadline = Date.now() + 10_000; while (!fs.existsSync(resultPath)) { if (Date.now() > deadline) { assert.fail(`Timed out waiting for async result file: ${resultPath}`); } await new Promise((resolve) => setTimeout(resolve, 100)); } const payload = JSON.parse(fs.readFileSync(resultPath, "utf-8")); assert.equal(payload.success, false); assert.equal(payload.exitCode, 1); assert.equal(payload.results[0].success, false); }); it("background implementation runs fail when no mutation attempt occurred", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => { mockPi.onCall({ output: "I’ll do that now and report back after implementing." }); const id = `async-no-mutation-${Date.now().toString(36)}`; const resultPath = path.join(RESULTS_DIR, `${id}.json`); const sessionRoot = path.join(tempDir, "sessions"); executeAsyncSingle(id, { agent: "worker", task: "Implement the approved fixes", agentConfig: makeAgent("worker"), ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" }, artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7, }, shareEnabled: false, sessionRoot, maxSubagentDepth: 2, }); const deadline = Date.now() + 10_000; while (!fs.existsSync(resultPath)) { if (Date.now() > deadline) { assert.fail(`Timed out waiting for async result file: ${resultPath}`); } await new Promise((resolve) => setTimeout(resolve, 100)); } const payload = JSON.parse(fs.readFileSync(resultPath, "utf-8")); assert.equal(payload.success, false); assert.equal(payload.exitCode, 1); assert.equal(payload.results[0].success, false); assert.match(String(payload.results[0].error ?? ""), /completed without making edits/); assert.match(String(payload.results[0].modelAttempts?.[0]?.error ?? ""), /completed without making edits/); const eventsPath = path.join(ASYNC_DIR, id, "events.jsonl"); const eventsText = fs.readFileSync(eventsPath, "utf-8"); assert.match(eventsText, /"reason":"completion_guard"/); assert.match(eventsText, /Subagent failed: worker/); assert.doesNotMatch(eventsText, /Status:/); assert.doesNotMatch(eventsText, /Interrupt:/); }); it("agent contract v1 keeps async acceptance and file-mutation effects separate from execution", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => { mockPi.onCall({ output: "I’ll do that now and report back after implementing.\n```acceptance-report\n{\"criteriaSatisfied\":[{\"id\":\"criterion-1\",\"status\":\"not-satisfied\",\"evidence\":\"no proof\"}]}\n```" }); const id = `async-v1-separate-${Date.now().toString(36)}`; executeAsyncSingle(id, { agent: "worker", task: "Implement the approved fixes", agentConfig: makeAgent("worker", { completionGuard: true }), ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" }, artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7, }, shareEnabled: false, sessionRoot: path.join(tempDir, "sessions"), maxSubagentDepth: 2, agentContract: { version: 1 }, acceptance: { level: "checked", criteria: ["Return required proof"] }, }); const resultPath = await waitForAsyncResultFile(id, 10_000); const payload = JSON.parse(fs.readFileSync(resultPath, "utf-8")) as AsyncResultPayload; const statusPayload = JSON.parse(fs.readFileSync(path.join(ASYNC_DIR, id, "status.json"), "utf-8")) as AsyncStatusPayload; assert.equal(payload.success, true); assert.equal(payload.state, "complete"); assert.equal(payload.exitCode, 0); assert.equal(payload.results[0]?.agentContract?.version, 1); assert.equal(payload.results[0]?.execution?.status, "completed"); assert.equal(payload.results[0]?.execution?.success, true); assert.equal(payload.results[0]?.acceptance?.status, "rejected"); assert.equal(payload.results[0]?.effects?.fileMutation?.status, "missing"); assert.equal(statusPayload.state, "complete"); assert.equal(statusPayload.steps?.[0]?.agentContract?.version, 1); assert.equal(statusPayload.steps?.[0]?.execution?.status, "completed"); assert.equal(statusPayload.steps?.[0]?.effects?.fileMutation?.status, "missing"); }); it("background single runs support outputSchema", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => { mockPi.onCall({ output: "structured", structuredOutput: { ok: true, note: "async" } }); const id = `async-single-schema-${Date.now().toString(36)}`; executeAsyncSingle(id, { agent: "worker", task: "Return structured data", agentConfig: makeAgent("worker", { completionGuard: false }), ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" }, artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7, }, shareEnabled: false, sessionRoot: path.join(tempDir, "sessions"), maxSubagentDepth: 2, acceptance: false, structuredOutputSchema: { type: "object", required: ["ok"], properties: { ok: { type: "boolean" }, note: { type: "string" } } }, }); const payload = JSON.parse(fs.readFileSync(await waitForAsyncResultFile(id, 10_000), "utf-8")) as AsyncResultPayload; assert.equal(payload.success, true); assert.deepEqual(payload.results[0]?.structuredOutput, { ok: true, note: "async" }); }); it("background bash-enabled non-implementation agents can opt out of the completion guard", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => { mockPi.onCall({ output: "cold start test after patch" }); const id = `async-completion-guard-optout-${Date.now().toString(36)}`; const sessionRoot = path.join(tempDir, "sessions"); executeAsyncSingle(id, { agent: "test-runner", task: "Run cold start test after patch", agentConfig: makeAgent("test-runner", { tools: ["read", "grep", "bash", "ls"], completionGuard: false }), ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" }, artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7, }, shareEnabled: false, sessionRoot, maxSubagentDepth: 2, }); const resultPath = await waitForAsyncResultFile(id, 10_000); const payload = JSON.parse(fs.readFileSync(resultPath, "utf-8")); assert.equal(payload.success, true); assert.equal(payload.exitCode, 0); assert.equal(payload.results[0].success, true); assert.equal(payload.results[0].output, "cold start test after patch"); const eventsPath = path.join(ASYNC_DIR, id, "events.jsonl"); const eventsText = fs.readFileSync(eventsPath, "utf-8"); assert.doesNotMatch(eventsText, /"reason":"completion_guard"/); }); it("background runs prefer the parent session provider for ambiguous bare model ids", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => { mockPi.onCall({ output: "Done asynchronously" }); const id = `async-provider-${Date.now().toString(36)}`; const resultPath = path.join(RESULTS_DIR, `${id}.json`); const sessionRoot = path.join(tempDir, "sessions"); executeAsyncSingle(id, { agent: "worker", task: "Do work", agentConfig: makeAgent("worker", { model: "gpt-5-mini" }), ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1", currentModelProvider: "github-copilot", }, availableModels: [ { provider: "openai", id: "gpt-5-mini", fullId: "openai/gpt-5-mini" }, { provider: "github-copilot", id: "gpt-5-mini", fullId: "github-copilot/gpt-5-mini" }, ], artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7, }, shareEnabled: false, sessionRoot, maxSubagentDepth: 2, }); const deadline = Date.now() + 10_000; while (!fs.existsSync(resultPath)) { if (Date.now() > deadline) { assert.fail(`Timed out waiting for async result file: ${resultPath}`); } await new Promise((resolve) => setTimeout(resolve, 100)); } const payload = JSON.parse(fs.readFileSync(resultPath, "utf-8")); assert.equal(payload.success, true); assert.equal(payload.results[0].model, "github-copilot/gpt-5-mini"); assert.deepEqual(payload.results[0].attemptedModels, ["github-copilot/gpt-5-mini"]); }); it("background single runs inherit the parent session model when no model is set", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => { mockPi.onCall({ output: "Done asynchronously" }); const id = `async-single-parent-model-${Date.now().toString(36)}`; executeAsyncSingle(id, { agent: "worker", task: "Do work", agentConfig: makeAgent("worker"), ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1", currentModelProvider: "deepseek", currentModel: { provider: "deepseek", id: "deepseek-v4-flash" }, }, artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7, }, shareEnabled: false, sessionRoot: path.join(tempDir, "sessions"), maxSubagentDepth: 2, }); const resultPath = await waitForAsyncResultFile(id, 10_000); const payload = JSON.parse(fs.readFileSync(resultPath, "utf-8")) as AsyncResultPayload; assert.equal(payload.success, true); assert.equal(payload.results[0].model, "deepseek/deepseek-v4-flash"); assert.deepEqual(payload.results[0].attemptedModels, ["deepseek/deepseek-v4-flash"]); const args = readMockPiArgs(mockPi, 0); assert.equal(args[args.indexOf("--model") + 1], "deepseek/deepseek-v4-flash"); }); it("background chains inherit the parent session model when no step or agent model is set", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => { mockPi.onCall({ output: "Done asynchronously" }); const id = `async-chain-parent-model-${Date.now().toString(36)}`; executeAsyncChain(id, { chain: [{ agent: "worker", task: "Do work" }], agents: [makeAgent("worker")], ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1", currentModelProvider: "deepseek", currentModel: { provider: "deepseek", id: "deepseek-v4-flash" }, }, artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7, }, shareEnabled: false, sessionRoot: path.join(tempDir, "sessions"), maxSubagentDepth: 2, }); const resultPath = await waitForAsyncResultFile(id, 10_000); const payload = JSON.parse(fs.readFileSync(resultPath, "utf-8")) as AsyncResultPayload; assert.equal(payload.success, true); assert.equal(payload.results[0].model, "deepseek/deepseek-v4-flash"); assert.deepEqual(payload.results[0].attemptedModels, ["deepseek/deepseek-v4-flash"]); const args = readMockPiArgs(mockPi, 0); assert.equal(args[args.indexOf("--model") + 1], "deepseek/deepseek-v4-flash"); }); it("background chains treat empty step models as parent inheritance", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => { mockPi.onCall({ output: "Done asynchronously" }); const id = `async-chain-empty-model-${Date.now().toString(36)}`; executeAsyncChain(id, { chain: [{ agent: "worker", task: "Do work", model: "" }], agents: [makeAgent("worker", { model: "anthropic/claude-sonnet-4-5", thinking: "high" })], ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1", currentModelProvider: "openai", currentModel: { provider: "openai", id: "gpt-5-mini" }, }, availableModels: [ { provider: "anthropic", id: "claude-sonnet-4-5", fullId: "anthropic/claude-sonnet-4-5", api: "anthropic-messages" }, { provider: "openai", id: "gpt-5-mini", fullId: "openai/gpt-5-mini", api: "openai-responses" }, ], artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7, }, shareEnabled: false, sessionRoot: path.join(tempDir, "sessions"), maxSubagentDepth: 2, }); const resultPath = await waitForAsyncResultFile(id, 10_000); const payload = JSON.parse(fs.readFileSync(resultPath, "utf-8")) as AsyncResultPayload; assert.equal(payload.success, true); assert.equal(payload.results[0].model, "openai/gpt-5-mini:high"); assert.deepEqual(payload.results[0].attemptedModels, ["openai/gpt-5-mini:high"]); const args = readMockPiArgs(mockPi, 0); assert.equal(args[args.indexOf("--model") + 1], "openai/gpt-5-mini:high"); }); it("background chains keep agent fallback models inherited for scope warnings", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => { mockPi.onCall({ output: "Done asynchronously" }); const warnings: string[] = []; const originalWarn = console.warn; console.warn = (message?: unknown) => warnings.push(String(message)); try { for (const [index, requestedModel] of [undefined, "", "inherit"].entries()) { const id = `async-chain-no-parent-${index}-${Date.now().toString(36)}`; executeAsyncChain(id, { chain: [{ agent: "worker", task: "Do work", ...(requestedModel !== undefined ? { model: requestedModel } : {}) }], agents: [makeAgent("worker", { model: "openai/gpt-5-mini", thinking: "high" })], ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1", modelScope: { enforce: true, allow: ["anthropic/*"] }, }, availableModels: [ { provider: "openai", id: "gpt-5-mini", fullId: "openai/gpt-5-mini", api: "openai-responses" }, ], artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7, }, shareEnabled: false, sessionRoot: path.join(tempDir, "sessions"), maxSubagentDepth: 2, }); const resultPath = await waitForAsyncResultFile(id, 10_000); const payload = JSON.parse(fs.readFileSync(resultPath, "utf-8")) as AsyncResultPayload; assert.equal(payload.success, true); assert.equal(payload.results[0].model, "openai/gpt-5-mini:high"); assert.deepEqual(payload.results[0].attemptedModels, ["openai/gpt-5-mini:high"]); const args = readMockPiArgs(mockPi, index); assert.equal(args[args.indexOf("--model") + 1], "openai/gpt-5-mini:high"); } assert.equal(warnings.length, 3); assert.equal(warnings.every((warning) => warning.includes("outside the configured subagent model scope")), true); } finally { console.warn = originalWarn; } }); it("background runs resolve skills from the effective task cwd", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => { mockPi.onCall({ output: "Done asynchronously" }); const taskCwd = createTempDir("pi-subagent-async-task-cwd-"); const id = `async-skill-cwd-${Date.now().toString(36)}`; const asyncDir = path.join(ASYNC_DIR, id); const resultPath = path.join(RESULTS_DIR, `${id}.json`); const statusPath = path.join(asyncDir, "status.json"); try { writePackageSkill(taskCwd, "async-task-cwd-skill"); executeAsyncSingle(id, { agent: "worker", task: "Do work", agentConfig: makeAgent("worker", { skills: ["async-task-cwd-skill"] }), ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" }, cwd: taskCwd, artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7, }, shareEnabled: false, sessionRoot: path.join(tempDir, "sessions"), maxSubagentDepth: 2, }); const deadline = Date.now() + 10_000; while (!fs.existsSync(resultPath)) { if (Date.now() > deadline) { assert.fail(`Timed out waiting for async result file: ${resultPath}`); } await new Promise((resolve) => setTimeout(resolve, 100)); } const payload = JSON.parse(fs.readFileSync(resultPath, "utf-8")) as AsyncResultPayload; const status = JSON.parse(fs.readFileSync(statusPath, "utf-8")) as AsyncStatusPayload; assert.equal(payload.success, true); assert.deepEqual(status.steps?.[0]?.skills, ["async-task-cwd-skill"]); } finally { removeTempDir(taskCwd); } }); it("injects agent-file-relative local skills into background single child prompts", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => { mockPi.onCall({ output: "Done asynchronously" }); const id = `async-local-skill-${Date.now().toString(36)}`; const agentFile = path.join(tempDir, "agents", "worker", "worker.md"); const skillFile = path.join(path.dirname(agentFile), "skills", "local", "SKILL.md"); fs.mkdirSync(path.dirname(skillFile), { recursive: true }); fs.writeFileSync(skillFile, "---\ndescription: async local skill\n---\nLocal skill body\n", "utf-8"); executeAsyncSingle(id, { agent: "worker", task: "Do work", agentConfig: makeAgent("worker", { filePath: agentFile, skills: ["local"], skillPath: ["./skills"] }), ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" }, artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7 }, shareEnabled: false, maxSubagentDepth: 2, }); await waitForAsyncResultFile(id); const call = await waitForMockPiCall(mockPi, 0); assert.match(call.systemPrompts.map((record) => record.text ?? "").join("\n"), /async local skill/); }); it("isolates agent-local skills between background parallel chain children", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => { mockPi.onCall({ output: "one" }); mockPi.onCall({ output: "two" }); const id = `async-parallel-local-skills-${Date.now().toString(36)}`; const agents = ["one", "two"].map((name) => { const agentFile = path.join(tempDir, "agents", name, `${name}.md`); const skillFile = path.join(path.dirname(agentFile), "skills", "local", "SKILL.md"); fs.mkdirSync(path.dirname(skillFile), { recursive: true }); fs.writeFileSync(skillFile, `---\ndescription: ${name} async local skill\n---\nbody\n`, "utf-8"); return makeAgent(name, { filePath: agentFile, skills: ["local"], skillPath: ["./skills"] }); }); executeAsyncChain(id, { chain: [{ parallel: [{ agent: "one", task: "One" }, { agent: "two", task: "Two" }], concurrency: 2 }], resultMode: "parallel", agents, ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" }, artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7 }, shareEnabled: false, maxSubagentDepth: 2, }); await waitForAsyncResultFile(id); const prompts = await Promise.all([0, 1].map(async (index) => { const call = await waitForMockPiCall(mockPi, index); return call.systemPrompts.map((record) => record.text ?? "").join("\n"); })); assert.equal(prompts.filter((prompt) => /one async local skill/.test(prompt) && !/two async local skill/.test(prompt)).length, 1); assert.equal(prompts.filter((prompt) => /two async local skill/.test(prompt) && !/one async local skill/.test(prompt)).length, 1); }); it("background single runs report unavailable pi-subagents skill requests", () => { const id = `async-pi-subagents-skill-${Date.now().toString(36)}`; const result = executeAsyncSingle(id, { agent: "worker", task: "Do work", agentConfig: makeAgent("worker"), ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" }, cwd: tempDir, artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7, }, shareEnabled: false, sessionRoot: path.join(tempDir, "sessions"), skills: ["pi-subagents"], maxSubagentDepth: 2, }); assert.equal(result.isError, true); assert.match(result.content[0]?.text ?? "", /Skills not found: pi-subagents/); }); it("background chains report unavailable pi-subagents skill requests", () => { const id = `async-chain-pi-subagents-skill-${Date.now().toString(36)}`; const result = executeAsyncChain(id, { chain: [{ agent: "worker", task: "Do work", skill: ["pi-subagents"] }], agents: [makeAgent("worker")], ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" }, cwd: tempDir, artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7, }, shareEnabled: false, sessionRoot: path.join(tempDir, "sessions"), maxSubagentDepth: 2, }); assert.equal(result.isError, true); assert.match(result.content[0]?.text ?? "", /Skills not found: pi-subagents/); }); it("background chains resolve relative step cwd values against the shared cwd", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => { mockPi.onCall({ output: "Done asynchronously" }); const chainCwd = createTempDir("pi-subagent-async-chain-cwd-"); const id = `async-chain-skill-cwd-${Date.now().toString(36)}`; const asyncDir = path.join(ASYNC_DIR, id); const resultPath = path.join(RESULTS_DIR, `${id}.json`); const statusPath = path.join(asyncDir, "status.json"); try { writePackageSkill(path.join(chainCwd, "packages", "app"), "async-chain-step-skill"); executeAsyncChain(id, { chain: [{ agent: "worker", task: "Do work", cwd: "packages/app", skill: ["async-chain-step-skill"] }], agents: [makeAgent("worker")], ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" }, cwd: chainCwd, artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7, }, shareEnabled: false, sessionRoot: path.join(tempDir, "sessions"), maxSubagentDepth: 2, }); const deadline = Date.now() + 10_000; while (!fs.existsSync(resultPath)) { if (Date.now() > deadline) { assert.fail(`Timed out waiting for async result file: ${resultPath}`); } await new Promise((resolve) => setTimeout(resolve, 100)); } const payload = JSON.parse(fs.readFileSync(resultPath, "utf-8")) as AsyncResultPayload; const status = JSON.parse(fs.readFileSync(statusPath, "utf-8")) as AsyncStatusPayload; assert.equal(payload.success, true); assert.equal(payload.sessionId, "session-1"); assert.equal(status.sessionId, "session-1"); assert.deepEqual(status.steps?.[0]?.skills, ["async-chain-step-skill"]); } finally { removeTempDir(chainCwd); } }); it("keeps top-level current tool/path aligned with still-running parallel children", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => { mockPi.onCall({ steps: [ { jsonl: [events.toolStart("read", { path: "README.md" })] }, { delay: 900, jsonl: [events.toolEnd("read"), events.toolResult("read", "done"), events.assistantMessage("reader done")] }, ], }); mockPi.onCall({ steps: [ { delay: 100, jsonl: [events.toolStart("edit", { path: "docs.md" })] }, { delay: 100, jsonl: [events.toolEnd("edit"), events.toolResult("edit", "ok")] }, { delay: 700, jsonl: [events.assistantMessage("editor done")] }, ], }); const id = `async-parallel-tool-sync-${Date.now().toString(36)}`; const asyncDir = path.join(ASYNC_DIR, id); const resultPath = path.join(RESULTS_DIR, `${id}.json`); executeAsyncChain(id, { chain: [{ parallel: [{ agent: "reader", task: "Read" }, { agent: "editor", task: "Edit" }] }], agents: [makeAgent("reader"), makeAgent("editor")], ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" }, artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7 }, shareEnabled: false, sessionRoot: path.join(tempDir, "sessions"), maxSubagentDepth: 2, }); const statusPath = path.join(asyncDir, "status.json"); const doneDeadline = Date.now() + 10_000; let sawRunningTool = false; let invariantViolated = false; while (!fs.existsSync(resultPath) && Date.now() < doneDeadline) { if (fs.existsSync(statusPath)) { const status = JSON.parse(fs.readFileSync(statusPath, "utf-8")) as AsyncStatusPayload; const runningTools = (status.steps ?? []) .filter((step) => step.status === "running" && typeof step.currentTool === "string") .map((step) => step.currentTool as string); if (runningTools.length > 0) { sawRunningTool = true; if (!status.currentTool || !runningTools.includes(status.currentTool)) { invariantViolated = true; break; } } } await new Promise((resolve) => setTimeout(resolve, 50)); } if (!fs.existsSync(resultPath)) { assert.fail(`Timed out waiting for async result file: ${resultPath}`); } assert.equal(sawRunningTool, true, "expected at least one polling interval with a running step tool"); assert.equal(invariantViolated, false, "top-level currentTool drifted from running step tools"); }); it("returns a tool error when the detached runner config cannot be written", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, () => { const id = `async-write-fail-${Date.now().toString(36)}`; assert.ok(TEMP_ROOT_DIR, "TEMP_ROOT_DIR should be available for async tests"); fs.mkdirSync(TEMP_ROOT_DIR, { recursive: true }); fs.mkdirSync(path.join(TEMP_ROOT_DIR, `async-cfg-${id}.json`), { recursive: true }); const result = executeAsyncSingle(id, { agent: "worker", task: "Do work", agentConfig: makeAgent("worker"), ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" }, artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7, }, shareEnabled: false, sessionRoot: path.join(tempDir, "sessions"), maxSubagentDepth: 2, }); assert.equal(result.isError, true); assert.match(result.content[0]?.text ?? "", /Failed to start async run/); assert.match(result.content[0]?.text ?? "", /async-cfg-/); }); it("returns a tool error when an async run uses a missing cwd", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, () => { const id = `async-missing-cwd-${Date.now().toString(36)}`; const missingCwd = path.join(tempDir, "missing-cwd"); const singleResult = executeAsyncSingle(id, { agent: "worker", task: "Do work", agentConfig: makeAgent("worker"), ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" }, cwd: missingCwd, artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7, }, shareEnabled: false, sessionRoot: path.join(tempDir, "sessions"), maxSubagentDepth: 2, }); assert.equal(singleResult.isError, true); assert.match(singleResult.content[0]?.text ?? "", /Failed to start async run/); assert.match(singleResult.content[0]?.text ?? "", /cwd does not exist/); const chainId = `async-missing-cwd-chain-${Date.now().toString(36)}`; const chainResult = executeAsyncChain(chainId, { chain: [{ agent: "worker", task: "Do work" }], agents: [makeAgent("worker")], ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" }, cwd: missingCwd, artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7, }, shareEnabled: false, sessionRoot: path.join(tempDir, "sessions"), maxSubagentDepth: 2, }); assert.equal(chainResult.isError, true); assert.match(chainResult.content[0]?.text ?? "", /Failed to start async chain/); assert.match(chainResult.content[0]?.text ?? "", /cwd does not exist/); }); it("returns a tool error when the async runner process cannot spawn", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, () => { const originalExecPath = process.execPath; const pathKey = process.platform === "win32" ? "Path" : "PATH"; const originalPath = process.env[pathKey]; process.execPath = path.join(tempDir, process.platform === "win32" ? "pi.exe" : "pi"); process.env[pathKey] = tempDir; try { const id = `async-spawn-fail-${Date.now().toString(36)}`; const result = executeAsyncSingle(id, { agent: "worker", task: "Do work", agentConfig: makeAgent("worker"), ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" }, artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7, }, shareEnabled: false, sessionRoot: path.join(tempDir, "sessions"), maxSubagentDepth: 2, }); assert.equal(result.isError, true); assert.match(result.content[0]?.text ?? "", /Failed to start async run/); assert.match(result.content[0]?.text ?? "", /async runner did not produce a pid/); } finally { process.execPath = originalExecPath; if (originalPath === undefined) { delete process.env[pathKey]; } else { process.env[pathKey] = originalPath; } } }); it("returns a tool error when an async chain cannot write its detached runner config", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, () => { const id = `async-chain-write-fail-${Date.now().toString(36)}`; assert.ok(TEMP_ROOT_DIR, "TEMP_ROOT_DIR should be available for async tests"); fs.mkdirSync(TEMP_ROOT_DIR, { recursive: true }); fs.mkdirSync(path.join(TEMP_ROOT_DIR, `async-cfg-${id}.json`), { recursive: true }); const result = executeAsyncChain(id, { chain: [{ agent: "worker", task: "Do work" }], agents: [makeAgent("worker")], ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" }, artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7, }, shareEnabled: false, sessionRoot: path.join(tempDir, "sessions"), maxSubagentDepth: 2, }); assert.equal(result.isError, true); assert.match(result.content[0]?.text ?? "", /Failed to start async chain/); assert.match(result.content[0]?.text ?? "", /async-cfg-/); }); it("background ignores child watchdog status when child watchdogs are not configured", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => { await withIsolatedWatchdogSettings(tempDir, async () => { const id = `async-watchdog-unconfigured-${Date.now().toString(36)}`; mockPi.onCall({ jsonl: [events.assistantMessage("async-done-without-watchdog-config"), childWatchdogStatus(id, "reviewing", 1)], keepAliveAfterFinalMessageMs: 10000, }); const start = Date.now(); executeAsyncSingle(id, { agent: "worker", task: "Do work", agentConfig: makeAgent("worker"), ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" }, artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7 }, shareEnabled: false, sessionRoot: path.join(tempDir, "sessions"), maxSubagentDepth: 2, }); const resultPath = await waitForAsyncResultFile(id, 10_000); const elapsed = Date.now() - start; const payload = JSON.parse(fs.readFileSync(resultPath, "utf-8")) as AsyncResultPayload; assert.ok(elapsed < 6000, `unconfigured watchdog status should not delay async final drain, took ${elapsed}ms`); assert.equal(payload.success, true); assert.equal(payload.results[0]?.output, "async-done-without-watchdog-config"); assert.equal((payload.results[0] as { watchdog?: unknown }).watchdog, undefined); }); }); it("background final-drain waits for child watchdog settlement", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => { await withIsolatedWatchdogSettings(tempDir, async () => { writeWatchdogSettings(tempDir); const id = `async-watchdog-drain-${Date.now().toString(36)}`; mockPi.onCall({ steps: [ { jsonl: [events.assistantMessage("async-done-before-watchdog"), childWatchdogStatus(id, "reviewing", 1)] }, { delay: 1400, jsonl: [childWatchdogStatus(id, "idle", 2)] }, ], keepAliveAfterFinalMessageMs: 10000, }); const start = Date.now(); executeAsyncSingle(id, { agent: "worker", task: "Do work", agentConfig: makeAgent("worker"), ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" }, artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7 }, shareEnabled: false, sessionRoot: path.join(tempDir, "sessions"), maxSubagentDepth: 2, }); const resultPath = await waitForAsyncResultFile(id, 10_000); const elapsed = Date.now() - start; const payload = JSON.parse(fs.readFileSync(resultPath, "utf-8")) as AsyncResultPayload; assert.ok(elapsed >= 1200, `watchdog settlement should delay async final drain, took ${elapsed}ms`); assert.ok(elapsed < 9000, `settled watchdog should still allow async cleanup, took ${elapsed}ms`); assert.equal(payload.success, true); assert.equal(payload.results[0]?.output, "async-done-before-watchdog"); assert.equal((payload.results[0] as { watchdog?: { phase?: string } }).watchdog?.phase, "idle"); }); }); it("background child watchdog tail timeout still finalizes successful output", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => { await withIsolatedWatchdogSettings(tempDir, async () => { writeWatchdogSettings(tempDir, 150); const id = `async-watchdog-timeout-${Date.now().toString(36)}`; mockPi.onCall({ jsonl: [events.assistantMessage("async-done-before-watchdog-timeout"), childWatchdogStatus(id, "reviewing", 1)], keepAliveAfterFinalMessageMs: 10000, }); const start = Date.now(); executeAsyncSingle(id, { agent: "worker", task: "Do work", agentConfig: makeAgent("worker"), ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" }, artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7 }, shareEnabled: false, sessionRoot: path.join(tempDir, "sessions"), maxSubagentDepth: 2, }); const resultPath = await waitForAsyncResultFile(id, 10_000); const elapsed = Date.now() - start; const payload = JSON.parse(fs.readFileSync(resultPath, "utf-8")) as AsyncResultPayload; assert.ok(elapsed < 6000, `watchdog tail fallback should not hang async final drain, took ${elapsed}ms`); assert.equal(payload.success, true); assert.equal(payload.results[0]?.output, "async-done-before-watchdog-timeout"); const watchdog = (payload.results[0] as { watchdog?: { phase?: string; timedOut?: boolean } }).watchdog; assert.equal(watchdog?.phase, "stale"); assert.equal(watchdog?.timedOut, true); }); }); it("background forced drain after final assistant output is cleanup success", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => { mockPi.onCall({ jsonl: [events.assistantMessage("async-done-before-drain")], stderr: "Done after 1 turn(s). Ready for input.\n", keepAliveAfterFinalMessageMs: 10000, }); const id = `async-final-drain-${Date.now().toString(36)}`; const resultPath = path.join(RESULTS_DIR, `${id}.json`); const sessionRoot = path.join(tempDir, "sessions"); const start = Date.now(); executeAsyncSingle(id, { agent: "worker", task: "Do work", agentConfig: makeAgent("worker"), ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" }, artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7, }, shareEnabled: false, sessionRoot, maxSubagentDepth: 2, }); const deadline = Date.now() + 10_000; while (!fs.existsSync(resultPath)) { if (Date.now() > deadline) { assert.fail(`Timed out waiting for async result file: ${resultPath}`); } await new Promise((resolve) => setTimeout(resolve, 100)); } const elapsed = Date.now() - start; const payload = JSON.parse(fs.readFileSync(resultPath, "utf-8")); assert.ok(elapsed < 9000, `should clean up async child before the mock's natural keepalive exit, took ${elapsed}ms`); assert.equal(payload.success, true); assert.equal(payload.exitCode, 0); assert.equal(payload.results[0].success, true); assert.equal(payload.results[0].output, "async-done-before-drain"); }); it("background forced drain after empty terminal assistant output is cleanup success", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => { mockPi.onCall({ jsonl: [events.assistantMessage("")], keepAliveAfterFinalMessageMs: 10000, }); const id = `async-final-drain-empty-${Date.now().toString(36)}`; const resultPath = path.join(RESULTS_DIR, `${id}.json`); const start = Date.now(); executeAsyncSingle(id, { agent: "scout", task: "Inspect something", agentConfig: makeAgent("scout"), ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" }, artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7 }, shareEnabled: false, sessionRoot: path.join(tempDir, "sessions"), maxSubagentDepth: 2, }); const deadline = Date.now() + 10_000; while (!fs.existsSync(resultPath)) { if (Date.now() > deadline) assert.fail(`Timed out waiting for async result file: ${resultPath}`); await new Promise((resolve) => setTimeout(resolve, 100)); } const elapsed = Date.now() - start; const payload = JSON.parse(fs.readFileSync(resultPath, "utf-8")); assert.ok(elapsed < 9000, `should clean up async child before the mock's natural keepalive exit, took ${elapsed}ms`); assert.equal(payload.success, true); assert.equal(payload.exitCode, 0); assert.equal(payload.results[0].success, true); assert.equal(payload.results[0].output, ""); }); it("background final-drain cleanup preserves explicit assistant errors", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => { mockPi.onCall({ jsonl: [{ type: "message_end", message: { role: "assistant", content: [{ type: "text", text: "failed" }], model: "mock/test-model", stopReason: "stop", errorMessage: "provider exploded", usage: { input: 100, output: 0, cacheRead: 0, cacheWrite: 0, cost: { total: 0.001 } }, }, }], keepAliveAfterFinalMessageMs: 10000, }); const id = `async-final-drain-error-${Date.now().toString(36)}`; const resultPath = path.join(RESULTS_DIR, `${id}.json`); executeAsyncSingle(id, { agent: "worker", task: "Do work", agentConfig: makeAgent("worker"), ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" }, artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7 }, shareEnabled: false, sessionRoot: path.join(tempDir, "sessions"), maxSubagentDepth: 2, }); const deadline = Date.now() + 10_000; while (!fs.existsSync(resultPath)) { if (Date.now() > deadline) assert.fail(`Timed out waiting for async result file: ${resultPath}`); await new Promise((resolve) => setTimeout(resolve, 100)); } const payload = JSON.parse(fs.readFileSync(resultPath, "utf-8")); assert.equal(payload.success, false); assert.equal(payload.exitCode, 1); assert.equal(payload.results[0].success, false); assert.equal(payload.results[0].error, "provider exploded"); }); it("background runs emit active-long-running control events from child turns", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => { mockPi.onCall({ steps: [ { jsonl: [events.assistantMessage("still working")] }, { delay: 2_000, jsonl: [events.assistantMessage("done")] }, ], }); const id = `async-active-long-${Date.now().toString(36)}`; const asyncDir = path.join(ASYNC_DIR, id); const eventsPath = path.join(asyncDir, "events.jsonl"); const resultPath = path.join(RESULTS_DIR, `${id}.json`); executeAsyncSingle(id, { agent: "scout", task: "Investigate behavior", agentConfig: makeAgent("scout"), ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" }, artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7 }, shareEnabled: false, sessionRoot: path.join(tempDir, "sessions"), maxSubagentDepth: 2, controlConfig: { enabled: true, needsAttentionAfterMs: 999_999, activeNoticeAfterTurns: 1, activeNoticeAfterMs: 999_999, activeNoticeAfterTokens: 999_999, failedToolAttemptsBeforeAttention: 3, notifyOn: ["active_long_running", "needs_attention"], notifyChannels: ["event", "async", "intercom"], }, }); const statusPath = path.join(asyncDir, "status.json"); const deadline = Date.now() + 10_000; let eventText = ""; let statusDuringEvent: AsyncStatusPayload | undefined; while (Date.now() < deadline) { if (fs.existsSync(eventsPath)) { eventText = fs.readFileSync(eventsPath, "utf-8"); } if (eventText.includes('"type":"active_long_running"') && fs.existsSync(statusPath)) { const status = JSON.parse(fs.readFileSync(statusPath, "utf-8")) as AsyncStatusPayload; if (status.activityState === "active_long_running" && status.steps?.[0]?.activityState === "active_long_running") { statusDuringEvent = status; break; } } if (eventText.includes('"type":"active_long_running"') && fs.existsSync(resultPath)) { assert.fail("run completed before status.json exposed active_long_running"); } await new Promise((resolve) => setTimeout(resolve, 100)); } assert.match(eventText, /"type":"active_long_running"/); assert.match(eventText, /"reason":"turn_threshold"/); assert.ok(statusDuringEvent, "expected status.json to expose active_long_running while the run is still active"); assert.equal(statusDuringEvent.activityState, "active_long_running"); assert.equal(statusDuringEvent.steps?.[0]?.activityState, "active_long_running"); const doneDeadline = Date.now() + 10_000; while (!fs.existsSync(resultPath)) { if (Date.now() > doneDeadline) assert.fail(`Timed out waiting for async result file: ${resultPath}`); await new Promise((resolve) => setTimeout(resolve, 100)); } }); it("does not flag a delayed active tool as idle attention", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => { mockPi.onCall({ steps: [ { jsonl: [events.toolStart("bash", { command: "sleep 2" })] }, { delay: 2_500, jsonl: [events.toolEnd("bash"), events.toolResult("bash", "done")] }, { jsonl: [events.assistantMessage("Done")] }, ], }); const id = `async-delayed-tool-attention-${Date.now().toString(36)}`; const asyncDir = path.join(ASYNC_DIR, id); const eventsPath = path.join(asyncDir, "events.jsonl"); const resultPath = path.join(RESULTS_DIR, `${id}.json`); executeAsyncSingle(id, { agent: "worker", task: "Run the command", agentConfig: makeAgent("worker"), ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" }, artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7 }, shareEnabled: false, sessionRoot: path.join(tempDir, "sessions"), maxSubagentDepth: 2, controlConfig: { enabled: true, needsAttentionAfterMs: 200, activeNoticeAfterMs: 999_999, failedToolAttemptsBeforeAttention: 3, notifyOn: ["active_long_running", "needs_attention"], notifyChannels: ["event", "async", "intercom"], }, }); const deadline = Date.now() + 10_000; let statusDuringTool: AsyncStatusPayload | undefined; while (Date.now() < deadline && !fs.existsSync(resultPath)) { if (fs.existsSync(asyncDir) && fs.existsSync(path.join(asyncDir, "status.json"))) { const status = JSON.parse(fs.readFileSync(path.join(asyncDir, "status.json"), "utf-8")) as AsyncStatusPayload; const toolStartedAt = status.steps?.[0]?.currentToolStartedAt; if (status.currentTool === "bash" && status.steps?.[0]?.currentTool === "bash" && toolStartedAt && Date.now() - toolStartedAt >= 1_500) { statusDuringTool = status; break; } } await new Promise((resolve) => setTimeout(resolve, 100)); } assert.ok(statusDuringTool, "expected status.json to expose the active tool"); assert.equal(statusDuringTool?.activityState, undefined); assert.equal(statusDuringTool?.steps?.[0]?.activityState, undefined); const eventText = fs.existsSync(eventsPath) ? fs.readFileSync(eventsPath, "utf-8") : ""; assert.doesNotMatch(eventText, /"type":"needs_attention"/); await waitForAsyncResultFile(id); const payload = JSON.parse(fs.readFileSync(resultPath, "utf-8")) as AsyncResultPayload; assert.equal(payload.success, true); }); it("subagent_wait wakes when an async child is waiting on contact_supervisor", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => { mockPi.onCall({ steps: [ { jsonl: [events.toolStart("contact_supervisor", { reason: "need_decision", message: "Need a decision" })] }, { delay: 5_000, jsonl: [events.toolEnd("contact_supervisor"), events.toolResult("contact_supervisor", "**Reply from supervisor:**\nProceed")] }, { delay: 2_500, jsonl: [events.assistantMessage("Done")] }, ], }); const id = `async-supervisor-attention-${Date.now().toString(36)}`; const asyncDir = path.join(ASYNC_DIR, id); const eventsPath = path.join(asyncDir, "events.jsonl"); const resultPath = path.join(RESULTS_DIR, `${id}.json`); const statusPath = path.join(asyncDir, "status.json"); executeAsyncSingle(id, { agent: "worker", task: "Ask the supervisor for a blocking decision", agentConfig: makeAgent("worker"), ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" }, artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7 }, shareEnabled: false, sessionRoot: path.join(tempDir, "sessions"), maxSubagentDepth: 2, controlConfig: { enabled: true, needsAttentionAfterMs: 999_999, activeNoticeAfterMs: 999_999, failedToolAttemptsBeforeAttention: 3, notifyOn: ["active_long_running", "needs_attention"], notifyChannels: ["event", "async", "intercom"], }, }); const attentionDeadline = Date.now() + 10_000; let statusDuringAttention: AsyncStatusPayload | undefined; while (Date.now() < attentionDeadline && !fs.existsSync(resultPath)) { if (fs.existsSync(statusPath)) { const nextStatus = JSON.parse(fs.readFileSync(statusPath, "utf-8")) as AsyncStatusPayload; if (nextStatus.currentTool === "contact_supervisor" && nextStatus.activityState === "needs_attention") { statusDuringAttention = nextStatus; break; } } await new Promise((resolve) => setTimeout(resolve, 100)); } assert.ok(statusDuringAttention, "expected status.json to expose the blocking supervisor request"); const waitResult = await waitForSubagents({ id, timeoutMs: 3_500 }, undefined, { state: { currentSessionId: "session-1", foregroundRuns: new Map(), asyncJobs: new Map(), cleanupTimers: new Map(), resultFileCoalescer: new Map() }, pollIntervalMs: 100, events: createEventBus(), }); const waitText = waitResult.content[0]?.text ?? ""; assert.equal(waitResult.isError, undefined); assert.match(waitText, /attention required/i); assert.match(waitText, new RegExp(id)); assert.match(waitText, /intercom\(\{ action: "pending" \}\)/); assert.equal(fs.existsSync(resultPath), false, "wait should return before the child completes"); const eventText = fs.existsSync(eventsPath) ? fs.readFileSync(eventsPath, "utf-8") : ""; assert.match(eventText, /"type":"needs_attention"/); assert.match(eventText, /"reason":"supervisor_request"/); assert.equal(statusDuringAttention.activityState, "needs_attention"); assert.equal(statusDuringAttention.steps?.[0]?.activityState, "needs_attention"); assert.equal(statusDuringAttention.currentTool, "contact_supervisor"); assert.equal(statusDuringAttention.steps?.[0]?.currentTool, "contact_supervisor"); const clearDeadline = Date.now() + 10_000; let statusAfterReply: AsyncStatusPayload | undefined; while (Date.now() < clearDeadline && !fs.existsSync(resultPath)) { const nextStatus = JSON.parse(fs.readFileSync(statusPath, "utf-8")) as AsyncStatusPayload; if (nextStatus.state === "running" && !nextStatus.currentTool && !nextStatus.steps?.[0]?.currentTool) { statusAfterReply = nextStatus; break; } await new Promise((resolve) => setTimeout(resolve, 100)); } assert.ok(statusAfterReply, "expected the child to keep running after the supervisor reply"); assert.equal(statusAfterReply.activityState, undefined); assert.equal(statusAfterReply.steps?.[0]?.activityState, undefined); await waitForAsyncResultFile(id); }); it("background runs escalate repeated mutating tool failures", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => { mockPi.onCall({ steps: [ { jsonl: [events.toolStart("edit", { path: "src/runs/background/subagent-runner.ts" }), events.toolEnd("edit"), events.toolResult("edit", "No exact match found for subagent-runner.ts", true)] }, { jsonl: [events.toolStart("edit", { path: "src/runs/background/subagent-runner.ts" }), events.toolEnd("edit"), events.toolResult("edit", "No exact match found for subagent-runner.ts", true)] }, { jsonl: [events.toolStart("edit", { path: "src/runs/background/subagent-runner.ts" }), events.toolEnd("edit"), events.toolResult("edit", "No exact match found for subagent-runner.ts", true)] }, { delay: 2_000, jsonl: [events.assistantMessage("I need another attempt.")] }, ], }); const id = `async-tool-failures-${Date.now().toString(36)}`; const asyncDir = path.join(ASYNC_DIR, id); const eventsPath = path.join(asyncDir, "events.jsonl"); const resultPath = path.join(RESULTS_DIR, `${id}.json`); executeAsyncSingle(id, { agent: "worker", task: "Implement the approved fixes", agentConfig: makeAgent("worker"), ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" }, artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7 }, shareEnabled: false, sessionRoot: path.join(tempDir, "sessions"), maxSubagentDepth: 2, controlConfig: { enabled: true, needsAttentionAfterMs: 999_999, activeNoticeAfterTurns: 999_999, activeNoticeAfterMs: 999_999, activeNoticeAfterTokens: 999_999, failedToolAttemptsBeforeAttention: 3, notifyOn: ["active_long_running", "needs_attention"], notifyChannels: ["event", "async", "intercom"], }, }); const statusPath = path.join(asyncDir, "status.json"); const deadline = Date.now() + 10_000; let eventText = ""; let statusDuringEvent: AsyncStatusPayload | undefined; while (Date.now() < deadline) { if (fs.existsSync(eventsPath)) { eventText = fs.readFileSync(eventsPath, "utf-8"); } if (eventText.includes('"reason":"tool_failures"') && fs.existsSync(statusPath)) { const status = JSON.parse(fs.readFileSync(statusPath, "utf-8")) as AsyncStatusPayload; if (status.activityState === "needs_attention" && status.steps?.[0]?.activityState === "needs_attention") { statusDuringEvent = status; break; } } if (eventText.includes('"reason":"tool_failures"') && fs.existsSync(resultPath)) { assert.fail("run completed before status.json exposed needs_attention"); } await new Promise((resolve) => setTimeout(resolve, 100)); } assert.match(eventText, /"type":"needs_attention"/); assert.match(eventText, /"reason":"tool_failures"/); assert.match(eventText, /subagent-runner\.ts/); assert.ok(statusDuringEvent, "expected status.json to expose needs_attention while the run is still active"); assert.equal(statusDuringEvent.activityState, "needs_attention"); assert.equal(statusDuringEvent.steps?.[0]?.activityState, "needs_attention"); const doneDeadline = Date.now() + 10_000; while (!fs.existsSync(resultPath)) { if (Date.now() > doneDeadline) assert.fail(`Timed out waiting for async result file: ${resultPath}`); await new Promise((resolve) => setTimeout(resolve, 100)); } }); it("background event logs drop noisy message updates and cap child diagnostics", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => { const previousMaxBytes = process.env.SELESAI_SUBAGENT_ASYNC_EVENTS_MAX_BYTES; process.env.SELESAI_SUBAGENT_ASYNC_EVENTS_MAX_BYTES = "900"; try { mockPi.onCall({ steps: [ { jsonl: [ { type: "message_update", assistantMessageEvent: { type: "thinking_delta", delta: "NOISY_PARTIAL_DELTA", partial: { role: "assistant", content: [{ type: "text", text: "NOISY_PARTIAL_SNAPSHOT".repeat(200) }] }, }, message: { role: "assistant", content: [{ type: "text", text: "NOISY_PARTIAL_MESSAGE".repeat(200) }] }, }, events.toolStart("bash", { command: `echo ${"BIG_COMMAND_PAYLOAD".repeat(200)}` }), events.assistantMessage("Done after noisy stream"), ], }, ], }); const id = `async-noisy-events-${Date.now().toString(36)}`; const asyncDir = path.join(ASYNC_DIR, id); const sessionRoot = path.join(tempDir, "sessions"); executeAsyncSingle(id, { agent: "worker", task: "Stream noisy diagnostics", agentConfig: makeAgent("worker"), ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" }, artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7, }, shareEnabled: false, sessionRoot, maxSubagentDepth: 2, }); const resultPath = await waitForAsyncResultFile(id, 10_000); const payload = JSON.parse(fs.readFileSync(resultPath, "utf-8")) as AsyncResultPayload; assert.equal(payload.success, true); assert.equal(payload.results[0]?.output, "Done after noisy stream"); const eventsText = fs.readFileSync(path.join(asyncDir, "events.jsonl"), "utf-8"); assert.doesNotMatch(eventsText, /"type":"message_update"/); assert.doesNotMatch(eventsText, /NOISY_PARTIAL_/); assert.doesNotMatch(eventsText, /BIG_COMMAND_PAYLOAD/); assert.match(eventsText, /"type":"subagent\.events\.truncated"/); assert.match(eventsText, /"droppedEventType":"tool_execution_start"/); } finally { if (previousMaxBytes === undefined) delete process.env.SELESAI_SUBAGENT_ASYNC_EVENTS_MAX_BYTES; else process.env.SELESAI_SUBAGENT_ASYNC_EVENTS_MAX_BYTES = previousMaxBytes; } }); it("background runs stream child events and live output while active", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => { mockPi.onCall({ steps: [ { delay: 200, jsonl: [events.toolStart("bash", { command: "ls" })] }, { delay: 600, jsonl: [events.toolEnd("bash"), events.toolResult("bash", "file-a\nfile-b")] }, { delay: 600, jsonl: [events.assistantMessage("Done streaming")], stderr: "warning: mock stderr\n" }, ], }); const id = `async-stream-${Date.now().toString(36)}`; const asyncDir = path.join(ASYNC_DIR, id); const eventsPath = path.join(asyncDir, "events.jsonl"); const outputPath = path.join(asyncDir, "output-0.log"); const resultPath = path.join(RESULTS_DIR, `${id}.json`); const sessionRoot = path.join(tempDir, "sessions"); executeAsyncSingle(id, { agent: "worker", task: "Stream detailed progress", agentConfig: makeAgent("worker"), ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" }, artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7, }, shareEnabled: false, sessionRoot, maxSubagentDepth: 2, }); const liveDeadline = Date.now() + 10_000; let sawChildEvent = false; let sawLiveOutput = false; while (Date.now() < liveDeadline && (!sawChildEvent || !sawLiveOutput)) { if (fs.existsSync(eventsPath)) { const content = fs.readFileSync(eventsPath, "utf-8"); sawChildEvent = content.includes('"type":"tool_execution_start"') && content.includes('"subagentSource":"child"'); } if (fs.existsSync(outputPath)) { const content = fs.readFileSync(outputPath, "utf-8"); sawLiveOutput = content.includes("bash: ls") || content.includes("file-a") || content.includes("warning: mock stderr"); } if (sawChildEvent && sawLiveOutput) break; assert.equal(fs.existsSync(resultPath), false, "run finished before live observability was written"); await new Promise((resolve) => setTimeout(resolve, 100)); } assert.equal(sawChildEvent, true, "expected child JSON events to be streamed into events.jsonl"); assert.equal(sawLiveOutput, true, "expected output-0.log to receive live child output"); const doneDeadline = Date.now() + 10_000; while (!fs.existsSync(resultPath)) { if (Date.now() > doneDeadline) { assert.fail(`Timed out waiting for async result file: ${resultPath}`); } await new Promise((resolve) => setTimeout(resolve, 100)); } const payload = JSON.parse(fs.readFileSync(resultPath, "utf-8")); assert.equal(payload.success, true); assert.equal(payload.results[0].output, "Done streaming"); const status = JSON.parse(fs.readFileSync(path.join(asyncDir, "status.json"), "utf-8")); assert.deepEqual(status.steps[0].recentTools.map((tool: { tool: string; args: string }) => ({ tool: tool.tool, args: tool.args })), [{ tool: "bash", args: "ls" }]); assert.deepEqual(status.steps[0].recentOutput, ["file-a", "file-b", "Done streaming"]); }); });