@maxiaochao/pi-toolkit 0.10.14 → 0.10.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -19,6 +19,7 @@ export interface PersistedReport {
19
19
  status: "completed" | "needs-input" | "failed";
20
20
  summary: string;
21
21
  documents: PersistedDocument[];
22
+ attemptId?: string;
22
23
  error?: string;
23
24
  question?: string;
24
25
  reportedAt?: string;
@@ -74,6 +75,7 @@ export interface TaskRecord {
74
75
  agentName?: string;
75
76
  attempt?: number;
76
77
  currentTurnFile?: string;
78
+ currentAttemptId?: string;
77
79
  question?: string;
78
80
  error?: string;
79
81
  }
@@ -176,6 +178,7 @@ export function parseReport(value: unknown, source = "structured report"): Persi
176
178
  if (report.status === "needs-input" && (typeof report.question !== "string" || !report.question.trim())) throw new Error(`Missing report question in ${source}.`);
177
179
  if (report.error !== undefined && typeof report.error !== "string") throw new Error(`Invalid report error in ${source}.`);
178
180
  if (report.question !== undefined && typeof report.question !== "string") throw new Error(`Invalid report question in ${source}.`);
181
+ if (report.attemptId !== undefined && (typeof report.attemptId !== "string" || !report.attemptId.trim())) throw new Error(`Invalid report attemptId in ${source}.`);
179
182
  return report as unknown as PersistedReport;
180
183
  }
181
184
 
@@ -5,3 +5,7 @@ export function resultPathForAttempt(taskDirectory: string, attempt: number): st
5
5
  ? resolve(taskDirectory, "turns", `${String(attempt).padStart(2, "0")}-result.md`)
6
6
  : resolve(taskDirectory, "result.md");
7
7
  }
8
+
9
+ export function resultPathForAttemptId(taskDirectory: string, attemptId: string): string {
10
+ return resolve(taskDirectory, "attempt-results", `${attemptId}.md`);
11
+ }
@@ -7,9 +7,9 @@ export function aggregateTaskStatus(statuses: readonly TaskStatus[]): AggregateR
7
7
  if (statuses.length === 0) return "failed";
8
8
  if (statuses.some((status) => status === "running" || status === "starting")) return "running";
9
9
  if (statuses.some((status) => status === "blocked" || status === "queued")) return "blocked";
10
- const completed = statuses.some((status) => status === "completed");
11
- if (statuses.some((status) => status === "cancelled")) return completed ? "partial" : "cancelled";
12
- if (statuses.some((status) => status === "failed")) return completed ? "partial" : "failed";
10
+ if (statuses.every((status) => status === "cancelled")) return "cancelled";
11
+ if (statuses.every((status) => status === "failed")) return "failed";
12
+ if (statuses.some((status) => status === "cancelled" || status === "failed")) return "partial";
13
13
  if (statuses.every((status) => status === "completed")) return "completed";
14
14
  throw new Error(`Cannot aggregate unknown task status: ${statuses.join(", ")}`);
15
15
  }
@@ -184,7 +184,9 @@ export async function runHerdrSubagents(prompt: string, options: HerdrSubagentsO
184
184
  await writeJson(taskFile, taskRecord);
185
185
  update("starting", "Starting child Pi in Herdr...");
186
186
  update("running", "Child Pi is working...");
187
- const report = (await promptLiveAgent({ herdr, task: { ...taskRecord, id: taskId, sessionFile }, paneId, prompt, systemPromptFile, agentName: name, signal: options.signal, stalledWarningMs: options.stalledWarningSeconds ? options.stalledWarningSeconds * 1000 : undefined, onStalled: () => options.onStalled?.(`${label} appears stalled; it is still running.`) })).report;
187
+ const prompted = await promptLiveAgent({ herdr, task: { ...taskRecord, id: taskId, sessionFile }, paneId, prompt, systemPromptFile, agentName: name, signal: options.signal, stalledWarningMs: options.stalledWarningSeconds ? options.stalledWarningSeconds * 1000 : undefined, onStalled: () => options.onStalled?.(`${label} appears stalled; it is still running.`) });
188
+ taskRecord.currentAttemptId = prompted.attemptId;
189
+ const report = prompted.report;
188
190
  attempt = { status: applyReport(taskRecord, report), report };
189
191
  } catch (error) {
190
192
  const settled = await settleAttemptFailure(taskRecord, reportFile, {
@@ -224,19 +226,7 @@ export async function runHerdrSubagents(prompt: string, options: HerdrSubagentsO
224
226
  }
225
227
  } finally {
226
228
  if (tabId && result?.status !== "blocked") {
227
- try {
228
- await cleanupRunTab({ herdr, tabId, runFile, runRecord, signal: options.signal, onError: options.onCleanupError, failureStatus: "failed" });
229
- } catch (error) {
230
- const cleanupError = error instanceof Error ? error.message : String(error);
231
- if (result?.ok) {
232
- const errorMessage = `Task completed but Herdr tab cleanup failed: ${cleanupError}`;
233
- const completedAt = new Date().toISOString();
234
- applyRunStatus(runRecord, "failed", { at: completedAt, error: errorMessage });
235
- applyTaskStatus(taskRecord, "failed", { at: completedAt, error: errorMessage });
236
- await Promise.all([writeJson(runFile, runRecord), writeJson(taskFile, taskRecord)]);
237
- result = failure(cwd, startedAt, errorMessage, common());
238
- }
239
- }
229
+ await cleanupRunTab({ herdr, tabId, runFile, runRecord, signal: options.signal, onError: options.onCleanupError });
240
230
  }
241
231
  }
242
232
 
@@ -15,7 +15,6 @@ export async function cleanupRunTab(options: {
15
15
  signal?: AbortSignal;
16
16
  onError?: (message: string) => void;
17
17
  onClosed?: (runRecord: RunRecord) => void | Promise<void>;
18
- failureStatus?: "failed";
19
18
  errorPrefix?: string;
20
19
  }): Promise<{ deferred: boolean }> {
21
20
  const mergeLatest = async (update: (run: RunRecord) => void | Promise<void>, requirePending = false, clearPending = true) => {
@@ -34,14 +33,17 @@ export async function cleanupRunTab(options: {
34
33
  const reportFailure = async (error: unknown, requirePending = false) => {
35
34
  const detail = error instanceof Error ? error.message : String(error);
36
35
  try {
37
- await mergeLatest((latest) => Object.assign(latest, { cleanupPendingTabIds: [...new Set([...(latest.cleanupPendingTabIds ?? []), options.tabId])], cleanupError: detail, ...(options.failureStatus ? { status: options.failureStatus } : {}) }), requirePending, false);
36
+ await mergeLatest((latest) => Object.assign(latest, { cleanupPendingTabIds: [...new Set([...(latest.cleanupPendingTabIds ?? []), options.tabId])], cleanupError: detail }), requirePending, false);
38
37
  } catch (persistError) {
39
38
  options.onError?.(`${options.errorPrefix ?? "Herdr tab cleanup failed"}: ${detail}; could not persist cleanup state: ${persistError instanceof Error ? persistError.message : String(persistError)}`);
40
39
  return;
41
40
  }
42
41
  options.onError?.(`${options.errorPrefix ?? "Herdr tab cleanup failed"}: ${detail}`);
43
42
  };
44
- const finishClosed = async (requirePending = false) => mergeLatest(async (latest) => options.onClosed?.(latest), requirePending);
43
+ const finishClosed = async (requirePending = false) => mergeLatest(async (latest) => {
44
+ await options.onClosed?.(latest);
45
+ if (!latest.cleanupPendingTabIds?.length) delete latest.cleanupError;
46
+ }, requirePending);
45
47
  try {
46
48
  if (options.signal?.aborted) {
47
49
  await options.herdr.closeTab(options.tabId);
@@ -78,7 +80,7 @@ export async function cleanupRunTab(options: {
78
80
  return { deferred: false };
79
81
  }
80
82
  await reportFailure(error);
81
- throw error;
83
+ return { deferred: false };
82
84
  }
83
85
  return { deferred: false };
84
86
  }
@@ -216,7 +216,8 @@ async function executeBatch({ params, signal, onUpdate, ctx, dependencies, disco
216
216
  });
217
217
  const lines = [result.summary, ...result.tasks.map((task) => `- ${task.name}: ${task.status} — ${task.summary}`)];
218
218
  if (result.documents.length) lines.push("", "Documents:", ...result.documents.map((document) => `- ${document.description}: ${document.path}`));
219
- return { content: [{ type: "text", text: lines.join("\n") }], details: { ...result, prompts: tasks.map((task) => task.prompt) }, ...(result.ok ? {} : { isError: true }) };
219
+ const isError = result.status === "failed" || result.status === "partial" || result.status === "cancelled";
220
+ return { content: [{ type: "text", text: lines.join("\n") }], details: { ...result, prompts: tasks.map((task) => task.prompt) }, ...(isError ? { isError: true } : {}) };
220
221
  } catch (error) {
221
222
  const text = error instanceof Error ? error.message : String(error);
222
223
  const diagnostic = error as { recordDirectory?: string; runId?: string };
@@ -248,10 +249,11 @@ async function executeSingle({ params, signal, onUpdate, ctx, dependencies, disc
248
249
  herdr: dependencies.herdr,
249
250
  stalledWarningSeconds: discovery.settings.stalledWarningSeconds,
250
251
  onStalled: (text) => onUpdate?.({ content: [{ type: "text", text }], details: { status: "running", stalled: true } }),
251
- onCleanupError: (text) => onUpdate?.({ content: [{ type: "text", text }], details: { status: "failed", cleanupError: text } }),
252
+ onCleanupError: (text) => onUpdate?.({ content: [{ type: "text", text }], details: { status: "warning", cleanupError: text } }),
252
253
  onUpdate: (partial) => onUpdate?.({ content: [{ type: "text", text: partial.summary }], details: details(partial) }),
253
254
  });
254
- return { content: [{ type: "text", text: summaryText(result) }], details: details(result), ...(result.ok ? {} : { isError: true }) };
255
+ const isError = result.status === "failed" || result.status === "cancelled";
256
+ return { content: [{ type: "text", text: summaryText(result) }], details: details(result), ...(isError ? { isError: true } : {}) };
255
257
  }
256
258
 
257
259
  export async function executeHerdrSubagents(
@@ -56,6 +56,14 @@ function fakeHerdr(mode = "completed") {
56
56
  let maxActive = 0;
57
57
  let taskRuns = 0;
58
58
  const agents = new Map();
59
+ const writeAttemptReport = (directory, report) => {
60
+ const task = JSON.parse(readFileSync(join(directory, "task.json"), "utf8"));
61
+ const attemptId = task.currentAttemptId;
62
+ if (!attemptId) throw new Error("test task has no currentAttemptId");
63
+ const reports = join(directory, "reports");
64
+ mkdirSync(reports, { recursive: true });
65
+ writeFileSync(join(reports, `${attemptId}.json`), typeof report === "string" ? report : JSON.stringify({ attemptId, ...report }));
66
+ };
59
67
  return {
60
68
  calls,
61
69
  get maxActive() { return maxActive; },
@@ -77,8 +85,33 @@ function fakeHerdr(mode = "completed") {
77
85
  active += 1;
78
86
  maxActive = Math.max(maxActive, active);
79
87
  await new Promise((resolve) => setTimeout(resolve, request.prompt.includes("slow") ? 15 : 2));
88
+ if (mode === "delayed-report") {
89
+ const task = JSON.parse(readFileSync(join(directory, "task.json"), "utf8"));
90
+ const attempts = join(directory, "attempts");
91
+ mkdirSync(attempts, { recursive: true });
92
+ writeFileSync(join(attempts, `${task.currentAttemptId}.json`), JSON.stringify({ attemptId: task.currentAttemptId, state: "working" }));
93
+ setTimeout(() => writeAttemptReport(directory, { status: "completed", summary: "Retry eventually completed.", documents: [] }), 25);
94
+ active -= 1;
95
+ return { status: "idle" };
96
+ }
97
+ if (mode === "working-state-crash") {
98
+ const task = JSON.parse(readFileSync(join(directory, "task.json"), "utf8"));
99
+ const attempts = join(directory, "attempts");
100
+ mkdirSync(attempts, { recursive: true });
101
+ writeFileSync(join(attempts, `${task.currentAttemptId}.json`), JSON.stringify({ attemptId: task.currentAttemptId, state: "working" }));
102
+ active -= 1;
103
+ throw new Error("child process exited");
104
+ }
105
+ if (mode === "working-state-dead-return") {
106
+ const task = JSON.parse(readFileSync(join(directory, "task.json"), "utf8"));
107
+ const attempts = join(directory, "attempts");
108
+ mkdirSync(attempts, { recursive: true });
109
+ writeFileSync(join(attempts, `${task.currentAttemptId}.json`), JSON.stringify({ attemptId: task.currentAttemptId, state: "working" }));
110
+ active -= 1;
111
+ return { status: "idle" };
112
+ }
80
113
  if (mode === "report-then-post-fail") {
81
- writeFileSync(join(directory, "report.json"), JSON.stringify({ status: "completed", summary: "Authoritative report completed before transport failed.", documents: [] }));
114
+ writeAttemptReport(directory, { status: "completed", summary: "Authoritative report completed before transport failed.", documents: [] });
82
115
  active -= 1;
83
116
  throw new Error("connection lost after authoritative report");
84
117
  }
@@ -87,28 +120,28 @@ function fakeHerdr(mode = "completed") {
87
120
  if (mode === "completed" || mode === "close-failed") {
88
121
  const failed = request.prompt.includes("FAIL");
89
122
  writeFileSync(join(directory, "result.md"), request.prompt.startsWith("Review") ? "FULL RESULT THAT STAYS OUT OF PARENT CONTEXT" : `FULL ${request.prompt}`);
90
- writeFileSync(join(directory, "report.json"), JSON.stringify({
123
+ writeAttemptReport(directory, {
91
124
  status: failed ? "failed" : "completed",
92
125
  summary: request.prompt.startsWith("Review") ? "Authentication review completed." : failed ? `${request.prompt} failed.` : `${request.prompt} completed.`,
93
126
  ...(failed ? { error: "expected failure" } : {}),
94
127
  documents: [{ path: join(directory, "result.md"), description: "Full result" }],
95
- }));
128
+ });
96
129
  } else if (mode === "failed") {
97
130
  writeFileSync(join(directory, "result.md"), "Provider stopped before completing the review.");
98
- writeFileSync(join(directory, "report.json"), JSON.stringify({
131
+ writeAttemptReport(directory, {
99
132
  status: "failed",
100
133
  summary: "Authentication review failed.",
101
134
  error: "provider unavailable",
102
135
  documents: [],
103
- }));
136
+ });
104
137
  } else if (mode === "malformed") {
105
- writeFileSync(join(directory, "report.json"), "{truncated");
138
+ writeAttemptReport(directory, "{truncated");
106
139
  } else if (mode === "blocked" || mode === "blocked-twice" || mode === "blocked-then-post-fail") {
107
140
  const needsInput = ((mode === "blocked" || mode === "blocked-then-post-fail") && taskRuns === 1) || (mode === "blocked-twice" && (taskRuns === 1 || taskRuns === 3));
108
141
  const failed = request.prompt.includes("FAIL");
109
142
  const question = taskRuns === 3 ? "Which environment should I inspect?" : "Which branch should I inspect?";
110
143
  writeFileSync(join(directory, "result.md"), `FULL ${request.prompt}`);
111
- writeFileSync(join(directory, "report.json"), JSON.stringify({ status: needsInput ? "needs-input" : failed ? "failed" : "completed", summary: needsInput ? "Need more input." : failed ? `${request.prompt} failed.` : `${request.prompt} completed.`, ...(needsInput ? { question } : {}), ...(failed ? { error: "expected failure" } : {}), documents: [] }));
144
+ writeAttemptReport(directory, { status: needsInput ? "needs-input" : failed ? "failed" : "completed", summary: needsInput ? "Need more input." : failed ? `${request.prompt} failed.` : `${request.prompt} completed.`, ...(needsInput ? { question } : {}), ...(failed ? { error: "expected failure" } : {}), documents: [] });
112
145
  }
113
146
  active -= 1;
114
147
  return { status: mode === "failed" ? "done" : "idle" };
@@ -124,7 +157,7 @@ function fakeHerdr(mode = "completed") {
124
157
  async isTabFocused(tabId) { calls.push(["isTabFocused", { tabId }]); return mode === "focused"; },
125
158
  async waitForTabUnfocused(tabId) { calls.push(["waitForTabUnfocused", { tabId }]); },
126
159
  async paneExists(paneId) { calls.push(["paneExists", { paneId }]); return mode !== "missing-pane"; },
127
- async isTaskRunning(paneId) { calls.push(["isTaskRunning", { paneId }]); return mode !== "missing-pane" && mode !== "shell-only"; },
160
+ async isTaskRunning(paneId) { calls.push(["isTaskRunning", { paneId }]); return mode !== "missing-pane" && mode !== "shell-only" && mode !== "working-state-crash" && mode !== "working-state-dead-return"; },
128
161
  async closeTab(tabId) {
129
162
  calls.push(["closeTab", { tabId }]);
130
163
  if (mode === "close-failed") throw new Error("tab still busy");
@@ -308,6 +341,7 @@ try {
308
341
  let unknownStatusRejected = false;
309
342
  try { aggregateTaskStatus(["unknown"]); } catch { unknownStatusRejected = true; }
310
343
  check("task status aggregation rejects unknown states", unknownStatusRejected);
344
+ check("mixed failed and cancelled tasks remain partial", aggregateTaskStatus(["failed", "cancelled"]) === "partial" && aggregateTaskStatus(["failed", "failed"]) === "failed" && aggregateTaskStatus(["cancelled", "cancelled"]) === "cancelled");
311
345
  check("all execution paths share abort classification", isAbortError(Object.assign(new Error("stopped"), { name: "AbortError" })) && isAbortError(new Error("operation aborted")) && !isAbortError(new Error("provider unavailable")));
312
346
  check("respond releases ownership only for explicit terminal statuses", ["completed", "partial", "failed", "cancelled"].every(isSettledRunStatus) && !isSettledRunStatus("blocked") && !isSettledRunStatus(undefined));
313
347
 
@@ -322,11 +356,13 @@ try {
322
356
  const childEvents = new Map();
323
357
  const childExecCalls = [];
324
358
  const childStatuses = [];
359
+ let childReportTool;
325
360
  registerSubagentReport({
326
- registerTool() {},
361
+ registerTool(definition) { childReportTool = definition; },
327
362
  registerCommand(name, options) { childCommands.set(name, options); },
328
363
  registerShortcut(key, options) { childShortcuts.set(key, options); },
329
364
  on(name, handler) { childEvents.set(name, handler); },
365
+ events: { on() {} },
330
366
  async exec(command, args) { childExecCalls.push([command, args]); return { stdout: "", stderr: "", code: 0, killed: false }; },
331
367
  });
332
368
  process.env.PI_HERDR_SUBAGENTS_PARENT_PANE_ID = "w1:p-parent";
@@ -335,6 +371,23 @@ try {
335
371
  await childCommands.get("parent")?.handler("", childContext);
336
372
  await childShortcuts.get("alt+p")?.handler(childContext);
337
373
  check("child command and shortcut return to the exact parent Agent pane", childExecCalls.length === 2 && childExecCalls.every(([command, args]) => command === "herdr" && args.join(",") === "agent,focus,w1:p-parent") && childStatuses.some(([, value]) => value.includes("alt+p")));
374
+ const reportToolDirectory = join(root, "child-report-tool");
375
+ const reportAttemptId = "11111111-1111-4111-8111-111111111111";
376
+ mkdirSync(reportToolDirectory, { recursive: true });
377
+ writeFileSync(join(reportToolDirectory, "task.json"), JSON.stringify({ id: "report-task", runId: "report-run", order: 1, status: "running", currentAttemptId: reportAttemptId }));
378
+ process.env.PI_HERDR_SUBAGENTS_TASK_DIR = reportToolDirectory;
379
+ await childEvents.get("before_agent_start")?.({}, childContext);
380
+ await childReportTool.execute("report", { status: "completed", summary: "Attempt completed.", result: "Full attempt result.", documents: [] });
381
+ const scopedReport = JSON.parse(readFileSync(join(reportToolDirectory, "reports", `${reportAttemptId}.json`), "utf8"));
382
+ check("child report commits only to its captured attempt identity", scopedReport.attemptId === reportAttemptId && scopedReport.status === "completed" && !existsSync(join(reportToolDirectory, "report.json")) && JSON.parse(readFileSync(join(reportToolDirectory, "attempts", `${reportAttemptId}.json`), "utf8")).state === "working");
383
+ const firstAttemptResult = scopedReport.documents[0].path;
384
+ const nextReportAttemptId = "33333333-3333-4333-8333-333333333333";
385
+ writeFileSync(join(reportToolDirectory, "task.json"), JSON.stringify({ id: "report-task", runId: "report-run", order: 1, status: "running", currentAttemptId: nextReportAttemptId }));
386
+ await childEvents.get("before_agent_start")?.({}, childContext);
387
+ await childReportTool.execute("report", { status: "completed", summary: "New attempt completed.", result: "New full result.", documents: [] });
388
+ const nextScopedReport = JSON.parse(readFileSync(join(reportToolDirectory, "reports", `${nextReportAttemptId}.json`), "utf8"));
389
+ check("full results are isolated by attempt identity", nextScopedReport.documents[0].path !== firstAttemptResult && readFileSync(firstAttemptResult, "utf8") === "Full attempt result." && readFileSync(nextScopedReport.documents[0].path, "utf8") === "New full result.");
390
+ delete process.env.PI_HERDR_SUBAGENTS_TASK_DIR;
338
391
  const herdr = fakeHerdr();
339
392
  const success = await runHerdrSubagents("Review authentication", {
340
393
  cwd: root,
@@ -381,6 +434,12 @@ try {
381
434
  const silentHerdr = fakeHerdr("silent");
382
435
  const silent = await runHerdrSubagents("Forget report", { cwd: root, herdr: silentHerdr });
383
436
  check("missing report is visible failure", !silent.ok && silent.errorMessage?.includes("did not submit") && silentHerdr.calls.at(-1)[0] === "closeTab");
437
+ const delayedReport = await runHerdrSubagents("Retry after a transient provider error", { cwd: root, herdr: fakeHerdr("delayed-report") });
438
+ check("authoritative lifecycle waits for a report after a false screen-settled state", delayedReport.ok && delayedReport.status === "completed" && delayedReport.summary === "Retry eventually completed.");
439
+ const crashedAttempt = await runHerdrSubagents("Crash after reporting working", { cwd: root, herdr: fakeHerdr("working-state-crash") });
440
+ check("a dead child cannot leave an authoritative working attempt waiting forever", !crashedAttempt.ok && crashedAttempt.errorMessage?.includes("child process exited"));
441
+ const deadWorkingReturn = await runHerdrSubagents("Return idle then exit", { cwd: root, herdr: fakeHerdr("working-state-dead-return") });
442
+ check("a normal wait return cannot leave a dead working attempt waiting forever", !deadWorkingReturn.ok && deadWorkingReturn.errorMessage?.includes("Child Pi stopped while"));
384
443
  const malformedExecutionHerdr = fakeHerdr("malformed");
385
444
  const malformedExecution = await runHerdrSubagents("Write malformed report", { cwd: root, herdr: malformedExecutionHerdr });
386
445
  const malformedExecutionTaskDirectory = malformedExecution.recordDirectory;
@@ -396,7 +455,7 @@ try {
396
455
  const cleanupRunRecord = JSON.parse(readFileSync(join(cleanupRunDirectory, "run.json"), "utf8"));
397
456
  const cleanupTaskDirectory = join(cleanupRunDirectory, "tasks", readdirSync(join(cleanupRunDirectory, "tasks"))[0]);
398
457
  const cleanupTaskRecord = JSON.parse(readFileSync(join(cleanupTaskDirectory, "task.json"), "utf8"));
399
- check("tab cleanup failure settles records as failed", !cleanupFailure.ok && cleanupFailure.errorMessage?.includes("cleanup failed") && cleanupRunRecord.status === "failed" && cleanupTaskRecord.status === "failed");
458
+ check("tab cleanup failure preserves completed business records", cleanupFailure.ok && cleanupRunRecord.status === "completed" && cleanupTaskRecord.status === "completed" && cleanupRunRecord.cleanupError.includes("tab still busy"));
400
459
 
401
460
  const definition = parseAgentMarkdown(`---\nname: reviewer\ndescription: Review code\nmodel: fake/model\ntools: read, grep\n---\nReview carefully.`, "reviewer.md");
402
461
  check("persona frontmatter parsing", definition.name === "reviewer" && definition.model === "fake/model" && definition.tools?.join(",") === "read,grep" && definition.systemPrompt === "Review carefully.");
@@ -474,16 +533,25 @@ try {
474
533
  check("read-only batch uses bounded concurrency", batchDetails.effectiveConcurrency === 2 && batchHerdr.maxActive === 2 && batchHerdr.calls.filter(([name]) => name === "splitPane").length === 3);
475
534
  check("batch is FIFO with a dedicated pane per task", batchPrompts.join(",") === "one slow,two,three FAIL,four" && new Set(batchStarts.map((request) => request.paneId)).size === 4);
476
535
  check("failed sibling does not cancel batch", batch.isError && batchDetails.status === "partial" && batchDetails.tasks.map((task) => task.name).join(",") === "one,two,three,four" && batchDetails.tasks[2].status === "failed" && batchDetails.tasks[3].status === "completed");
536
+ const mixedCancelHerdr = fakeHerdr();
537
+ const mixedCancelPrompt = mixedCancelHerdr.promptAgent.bind(mixedCancelHerdr);
538
+ mixedCancelHerdr.promptAgent = async (request) => request.prompt.includes("WAIT") ? await new Promise((_resolve, reject) => request.signal?.addEventListener("abort", () => reject(request.signal.reason), { once: true })) : mixedCancelPrompt(request);
539
+ const mixedCancelController = new AbortController();
540
+ const mixedCancelPromise = executeHerdrSubagents({ concurrency: 2, tasks: [{ name: "fail-first", prompt: "FAIL", agent: "explorer" }, { name: "cancel-second", prompt: "WAIT", agent: "reviewer" }] }, mixedCancelController.signal, undefined, { cwd: root }, { agentsDirectory: join(process.cwd(), "agents"), herdr: mixedCancelHerdr });
541
+ await new Promise((resolve) => setTimeout(resolve, 10));
542
+ mixedCancelController.abort(new Error("Parent Pi session closed."));
543
+ const mixedCancel = await mixedCancelPromise;
544
+ check("failed and cancelled production tasks aggregate to partial", mixedCancel.details.status === "partial" && mixedCancel.details.tasks.some((task) => task.status === "failed") && mixedCancel.details.tasks.some((task) => task.status === "cancelled"));
477
545
  const batchCleanupHerdr = fakeHerdr("close-failed");
478
546
  const batchCleanupUpdates = [];
479
547
  const batchCleanup = await executeHerdrSubagents({ tasks: [{ name: "cleanup", prompt: "cleanup", agent: "explorer" }] }, undefined, (update) => batchCleanupUpdates.push(update), { cwd: root, model: { provider: "fake", id: "model" } }, { agentsDirectory: join(process.cwd(), "agents"), herdr: batchCleanupHerdr });
480
548
  const batchCleanupRecord = JSON.parse(readFileSync(join(batchCleanup.details.recordDirectory, "run.json"), "utf8"));
481
- check("batch cleanup failure is persisted and surfaced once", batchCleanup.isError && batchCleanup.content[0].text.includes("cleanup failed") && batchCleanupRecord.status === "failed" && batchCleanupRecord.cleanupError.includes("tab still busy") && batchCleanupUpdates.filter((update) => update.content[0]?.text.includes("cleanup failed")).length === 1);
549
+ check("batch cleanup failure is persisted without changing the business result", !batchCleanup.isError && batchCleanupRecord.status === "completed" && batchCleanupRecord.cleanupError.includes("tab still busy") && batchCleanupUpdates.filter((update) => update.content[0]?.text.includes("cleanup failed")).length === 1);
482
550
  const cleanupProbeHerdr = fakeHerdr();
483
551
  cleanupProbeHerdr.isTabFocused = async () => { throw new Error("tab lookup failed"); };
484
552
  const cleanupProbe = await executeHerdrSubagents({ tasks: [{ name: "cleanup-probe", prompt: "cleanup probe", agent: "explorer" }] }, undefined, undefined, { cwd: root, model: { provider: "fake", id: "model" } }, { agentsDirectory: join(process.cwd(), "agents"), herdr: cleanupProbeHerdr });
485
553
  const cleanupProbeRecord = JSON.parse(readFileSync(join(cleanupProbe.details.recordDirectory, "run.json"), "utf8"));
486
- check("cleanup focus-probe failure is persisted and surfaced", cleanupProbe.isError && cleanupProbe.content[0].text.includes("tab lookup failed") && cleanupProbeRecord.status === "failed" && cleanupProbeRecord.cleanupError === "tab lookup failed");
554
+ check("cleanup focus-probe failure is persisted without changing the business result", !cleanupProbe.isError && cleanupProbeRecord.status === "completed" && cleanupProbeRecord.cleanupError === "tab lookup failed");
487
555
  const missingTabHerdr = fakeHerdr();
488
556
  missingTabHerdr.isTabFocused = async () => { throw new Error('{"error":{"code":"tab_not_found","message":"tab w1:t2 not found"}}'); };
489
557
  const missingTabCleanup = await executeHerdrSubagents({ tasks: [{ name: "already-closed", prompt: "already closed", agent: "explorer" }] }, undefined, undefined, { cwd: root, model: { provider: "fake", id: "model" } }, { agentsDirectory: join(process.cwd(), "agents"), herdr: missingTabHerdr });
@@ -549,7 +617,7 @@ try {
549
617
  const deferredSingle = await executeHerdrSubagents({ prompt: "single deferred cleanup", agent: "explorer" }, undefined, (update) => deferredSingleUpdates.push(update), { cwd: root, model: { provider: "fake", id: "model" } }, { agentsDirectory: join(process.cwd(), "agents"), herdr: deferredSingleHerdr });
550
618
  await new Promise((resolve) => setTimeout(resolve, 10));
551
619
  const deferredSingleRun = JSON.parse(readFileSync(join(deferredSingle.details.recordDirectory, "..", "..", "run.json"), "utf8"));
552
- check("deferred single-task cleanup failure is persisted and surfaced", deferredSingleRun.status === "failed" && deferredSingleRun.cleanupError === "single deferred close failed" && deferredSingleUpdates.some((update) => update.content[0].text.includes("cleanup failed")));
620
+ check("deferred single-task cleanup failure is persisted and surfaced without changing completion", deferredSingleRun.status === "completed" && deferredSingleRun.cleanupError === "single deferred close failed" && deferredSingleUpdates.some((update) => update.content[0].text.includes("cleanup failed") && update.details.status === "warning"));
553
621
  const cancelledHerdr = fakeHerdr();
554
622
  cancelledHerdr.isTabFocused = async (tabId) => { cancelledHerdr.calls.push(["isTabFocused", { tabId }]); return true; };
555
623
  cancelledHerdr.waitForTabUnfocused = async () => { throw new Error("shutdown cleanup must not wait for focus"); };
@@ -667,11 +735,21 @@ try {
667
735
 
668
736
  const blockedHerdr = fakeHerdr("blocked");
669
737
  const blocked = await executeHerdrSubagents({ concurrency: 1, tasks: [{ name: "question", prompt: "inspect branch", agent: "explorer" }, { name: "after-answer", prompt: "continue queued", agent: "reviewer" }] }, undefined, undefined, { cwd: root, model: { provider: "fake", id: "model" } }, { agentsDirectory: join(process.cwd(), "agents"), herdr: blockedHerdr });
670
- check("needs-input returns foreground control and preserves tab", blocked.details.status === "blocked" && blocked.details.tasks[0].question === "Which branch should I inspect?" && !blockedHerdr.calls.some(([name]) => name === "closeTab"));
738
+ check("needs-input returns foreground control without a tool error and preserves tab", !blocked.isError && blocked.details.status === "blocked" && blocked.details.tasks[0].question === "Which branch should I inspect?" && !blockedHerdr.calls.some(([name]) => name === "closeTab"));
671
739
  const blockedStatus = await executeHerdrSubagents({ action: "status", runId: blocked.details.runId }, undefined, undefined, { cwd: root }, { agentsDirectory: join(process.cwd(), "agents") });
672
740
  check("status exposes blocked task questions for respond", blockedStatus.details.status === "blocked" && blockedStatus.details.settled === false && blockedStatus.details.tasks[0].question === "Which branch should I inspect?" && blockedStatus.content[0].text.includes("Which branch should I inspect?"));
673
741
  const activeCleanup = await executeHerdrSubagents({ action: "cleanup", runId: blocked.details.runId }, undefined, undefined, { cwd: root }, { agentsDirectory: join(process.cwd(), "agents"), herdr: blockedHerdr });
674
742
  check("cleanup refuses to delete an active blocked run", activeCleanup.isError && activeCleanup.content[0].text.includes("still active") && existsSync(blocked.details.recordDirectory));
743
+ mkdirSync(join(blocked.details.recordDirectory, ".respond.lock"));
744
+ const concurrentResponse = await executeHerdrSubagents({ action: "respond", runId: blocked.details.runId, answer: "duplicate" }, undefined, undefined, { cwd: root }, { agentsDirectory: join(process.cwd(), "agents"), herdr: blockedHerdr });
745
+ rmSync(join(blocked.details.recordDirectory, ".respond.lock"), { recursive: true });
746
+ check("concurrent blocked responses are rejected before a second prompt", concurrentResponse.isError && concurrentResponse.content[0].text.includes("already has a blocked response in progress"));
747
+ const staleLockHerdr = fakeHerdr("blocked");
748
+ const staleLockBlocked = await executeHerdrSubagents({ tasks: [{ name: "stale-lock-question", prompt: "question", agent: "explorer" }] }, undefined, undefined, { cwd: root }, { agentsDirectory: join(process.cwd(), "agents"), herdr: staleLockHerdr });
749
+ writeFileSync(join(staleLockBlocked.details.recordDirectory, ".respond.lock"), JSON.stringify({ processId: 99999999, createdAt: "2000-01-01T00:00:00.000Z" }));
750
+ const diagnosedStaleResponse = await executeHerdrSubagents({ action: "respond", runId: staleLockBlocked.details.runId, answer: "Inspect main." }, undefined, undefined, { cwd: root }, { agentsDirectory: join(process.cwd(), "agents"), herdr: staleLockHerdr });
751
+ check("blocked response diagnoses a lock owned by a dead process without unsafe reclamation", diagnosedStaleResponse.isError && diagnosedStaleResponse.content[0].text.includes("stale blocked-response lock"));
752
+ rmSync(join(staleLockBlocked.details.recordDirectory, ".respond.lock"));
675
753
  const resumeUpdates = [];
676
754
  const resumed = await executeHerdrSubagents({ action: "respond", runId: blocked.details.runId, answer: "Inspect main." }, undefined, (update) => resumeUpdates.push(update.details), { cwd: root, model: { provider: "fake", id: "model" } }, { agentsDirectory: join(process.cwd(), "agents"), herdr: blockedHerdr });
677
755
  const blockedStarts = blockedHerdr.calls.filter(([name]) => name === "startAgent").map(([, request]) => request);
@@ -714,7 +792,7 @@ try {
714
792
  const cancelledContinuationTask = JSON.parse(readFileSync(join(abortedTaskDirectories[1], "task.json"), "utf8"));
715
793
  const cancelledContinuationRun = JSON.parse(readFileSync(join(abortedContinuationRun.details.recordDirectory, "run.json"), "utf8"));
716
794
  const cancelledContinuationReport = JSON.parse(readFileSync(join(abortedTaskDirectories[1], "report.json"), "utf8"));
717
- check("aborted queued continuation is persisted as cancelled", cancelledContinuationTask.status === "cancelled" && cancelledContinuationRun.status === "cancelled" && cancelledContinuationReport.summary.includes("cancelled"));
795
+ check("aborted queued continuation preserves a partial completed/cancelled run", cancelledContinuationTask.status === "cancelled" && cancelledContinuationRun.status === "partial" && cancelledContinuationReport.summary.includes("cancelled"));
718
796
  const deadPaneHerdr = fakeHerdr("blocked");
719
797
  const deadPaneRun = await executeHerdrSubagents({ tasks: [{ name: "dead-pane", prompt: "question", agent: "explorer" }] }, undefined, undefined, { cwd: root }, { agentsDirectory: join(process.cwd(), "agents"), herdr: deadPaneHerdr });
720
798
  deadPaneHerdr.paneExists = async () => false;
@@ -980,6 +1058,27 @@ try {
980
1058
  writeFileSync(join(recoveryTask, "task.json"), JSON.stringify({ id: "task", runId: "recover", order: 1, name: "lost", status: "running", paneId: "w1:missing", sessionFile: join(recoveryTask, "session.jsonl") }));
981
1059
  const reconciliation = await reconcileSubagentRuns(recoveryRoot, fakeHerdr("missing-pane"), { sessionId: "new-owner" });
982
1060
  check("reconciliation marks stale child cancelled", reconciliation.cancelledTasks === 1 && JSON.parse(readFileSync(join(recoveryTask, "task.json"), "utf8")).status === "cancelled");
1061
+ const isolatedAttemptRoot = join(root, "isolated-attempt-recovery");
1062
+ const isolatedAttemptRun = join(isolatedAttemptRoot, ".pi", "herdr-subagents", "runs", "run");
1063
+ const isolatedAttemptTask = join(isolatedAttemptRun, "tasks", "01-task");
1064
+ mkdirSync(isolatedAttemptTask, { recursive: true });
1065
+ const currentAttemptId = "22222222-2222-4222-8222-222222222222";
1066
+ writeFileSync(join(isolatedAttemptRun, "run.json"), JSON.stringify({ id: "isolated-attempt", label: "isolated-attempt", status: "running", startedAt: new Date().toISOString() }));
1067
+ writeFileSync(join(isolatedAttemptTask, "task.json"), JSON.stringify({ id: "isolated-attempt-task", runId: "isolated-attempt", order: 1, status: "running", paneId: "w1:p-live", currentAttemptId, sessionFile: join(isolatedAttemptTask, "session.jsonl") }));
1068
+ writeFileSync(join(isolatedAttemptTask, "report.json"), JSON.stringify({ attemptId: "11111111-1111-4111-8111-111111111111", status: "needs-input", summary: "Prior attempt blocked.", question: "Old question?", documents: [] }));
1069
+ const isolatedReconciliation = await reconcileSubagentRuns(isolatedAttemptRoot, fakeHerdr());
1070
+ const isolatedTaskRecord = JSON.parse(readFileSync(join(isolatedAttemptTask, "task.json"), "utf8"));
1071
+ check("reconciliation ignores a prior canonical report while the current attempt is live", isolatedReconciliation.liveTasks === 1 && isolatedTaskRecord.status === "running" && isolatedTaskRecord.currentAttemptId === currentAttemptId && JSON.parse(readFileSync(join(isolatedAttemptTask, "report.json"), "utf8")).summary === "Prior attempt blocked.");
1072
+ const ownerLossRoot = join(root, "owner-loss-report-recovery");
1073
+ const ownerLossRun = join(ownerLossRoot, ".pi", "herdr-subagents", "runs", "run");
1074
+ const ownerLossTask = join(ownerLossRun, "tasks", "01-task");
1075
+ const ownerLossAttempt = "44444444-4444-4444-8444-444444444444";
1076
+ mkdirSync(join(ownerLossTask, "reports"), { recursive: true });
1077
+ writeFileSync(join(ownerLossRun, "run.json"), JSON.stringify({ id: "owner-loss", label: "owner-loss", status: "running", ownerSessionId: "dead-owner", ownerProcessId: 99999999, tabId: "w1:t-owner-loss", startedAt: new Date().toISOString() }));
1078
+ writeFileSync(join(ownerLossTask, "task.json"), JSON.stringify({ id: "owner-loss-task", runId: "owner-loss", order: 1, status: "running", paneId: "w1:p-owner-loss", currentAttemptId: ownerLossAttempt, sessionFile: join(ownerLossTask, "session.jsonl") }));
1079
+ writeFileSync(join(ownerLossTask, "reports", `${ownerLossAttempt}.json`), JSON.stringify({ attemptId: ownerLossAttempt, status: "completed", summary: "Completed before owner loss.", documents: [] }));
1080
+ await reconcileSubagentRuns(ownerLossRoot, fakeHerdr(), { sessionId: "new-owner", processId: process.pid });
1081
+ check("matching attempt report survives prior owner loss", JSON.parse(readFileSync(join(ownerLossTask, "task.json"), "utf8")).status === "completed" && JSON.parse(readFileSync(join(ownerLossRun, "run.json"), "utf8")).status === "completed" && JSON.parse(readFileSync(join(ownerLossTask, "report.json"), "utf8")).attemptId === ownerLossAttempt);
983
1082
  const malformedRoot = join(root, "malformed-recovery");
984
1083
  const malformedRun = join(malformedRoot, ".pi", "herdr-subagents", "runs", "run");
985
1084
  const malformedTask = join(malformedRun, "tasks", "01-task");
@@ -989,7 +1088,7 @@ try {
989
1088
  writeFileSync(join(malformedTask, "report.json"), "{truncated");
990
1089
  const malformedHerdr = fakeHerdr();
991
1090
  const malformedReconciliation = await reconcileSubagentRuns(malformedRoot, malformedHerdr, { sessionId: "new-owner", processId: process.pid });
992
- check("malformed report cannot abort orphan reconciliation", malformedReconciliation.cancelledTasks === 1 && malformedReconciliation.cleanupErrors.some((message) => message.includes("Invalid Subagent report")) && JSON.parse(readFileSync(join(malformedTask, "task.json"), "utf8")).status === "cancelled" && malformedHerdr.calls.some(([name]) => name === "closeTab"));
1091
+ check("malformed report cannot abort orphan reconciliation", malformedReconciliation.settledTasks === 1 && malformedReconciliation.cleanupErrors.some((message) => message.includes("Invalid Subagent report")) && JSON.parse(readFileSync(join(malformedTask, "task.json"), "utf8")).status === "failed" && malformedHerdr.calls.some(([name]) => name === "closeTab"));
993
1092
  const invalidReportRoot = join(root, "invalid-report-recovery");
994
1093
  const invalidReportRun = join(invalidReportRoot, ".pi", "herdr-subagents", "runs", "run");
995
1094
  const invalidReportTask = join(invalidReportRun, "tasks", "01-task");