@maxiaochao/pi-toolkit 0.10.20 → 0.10.22
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.
- package/CHANGELOG.md +12 -0
- package/package.json +1 -1
- package/packages/herdr-subagents/CHANGELOG.md +26 -0
- package/packages/herdr-subagents/README.md +21 -7
- package/packages/herdr-subagents/RESUME-LIFECYCLE-DESIGN.md +84 -0
- package/packages/herdr-subagents/TESTING.md +15 -2
- package/packages/herdr-subagents/extensions/herdr-subagents.ts +15 -21
- package/packages/herdr-subagents/extensions/subagent-report.ts +2 -2
- package/packages/herdr-subagents/package.json +1 -1
- package/packages/herdr-subagents/src/batch-runner.ts +58 -11
- package/packages/herdr-subagents/src/blocking.ts +115 -72
- package/packages/herdr-subagents/src/continue-run.ts +21 -4
- package/packages/herdr-subagents/src/dispatcher.ts +44 -4
- package/packages/herdr-subagents/src/fleet.ts +2 -1
- package/packages/herdr-subagents/src/herdr.ts +10 -1
- package/packages/herdr-subagents/src/historical-resume.ts +184 -0
- package/packages/herdr-subagents/src/history.ts +11 -128
- package/packages/herdr-subagents/src/live-agent.ts +99 -35
- package/packages/herdr-subagents/src/parameters.ts +2 -2
- package/packages/herdr-subagents/src/protocol.ts +3 -1
- package/packages/herdr-subagents/src/reconcile.ts +156 -123
- package/packages/herdr-subagents/src/records.ts +50 -6
- package/packages/herdr-subagents/src/resume-state.ts +46 -0
- package/packages/herdr-subagents/src/run-locator.ts +17 -2
- package/packages/herdr-subagents/src/run-pane-allocator.ts +39 -37
- package/packages/herdr-subagents/src/run-state.ts +120 -0
- package/packages/herdr-subagents/src/status.ts +8 -3
- package/packages/herdr-subagents/src/tab-cleanup.ts +16 -16
- package/packages/herdr-subagents/src/tool.ts +14 -62
- package/packages/herdr-subagents/src/validation.ts +1 -1
- package/packages/herdr-subagents/tests/extension-contract.ts +78 -3
- package/packages/herdr-subagents/tests/run-tests.mjs +608 -64
- package/packages/herdr-subagents/src/runner.ts +0 -234
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
-
import { join } from "node:path";
|
|
1
|
+
import { chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { basename, dirname, join } from "node:path";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { getEventListeners } from "node:events";
|
|
5
5
|
import { visibleWidth } from "@earendil-works/pi-tui";
|
|
6
6
|
import { discoverPackageAgents, parseAgentMarkdown } from "../src/personas.ts";
|
|
7
7
|
import { loadSubagentConfiguration } from "../src/config.ts";
|
|
8
|
-
import {
|
|
8
|
+
import { runHerdrSubagentsBatch } from "../src/batch-runner.ts";
|
|
9
9
|
import { executeHerdrSubagents } from "../src/tool.ts";
|
|
10
10
|
import { activityCounts, focusActiveTask } from "../src/monitor.ts";
|
|
11
11
|
import { RunDispatcher } from "../src/dispatcher.ts";
|
|
@@ -23,8 +23,28 @@ import { applyTaskStatus, isSettledRunStatus, projectTaskActivity, readTaskRecor
|
|
|
23
23
|
import { RunPaneAllocator } from "../src/run-pane-allocator.ts";
|
|
24
24
|
import { aggregateTaskStatus } from "../src/run-status.ts";
|
|
25
25
|
import { isAbortError } from "../src/errors.ts";
|
|
26
|
+
import { acquireDirectoryLock, withRunStateLock } from "../src/run-state.ts";
|
|
26
27
|
import registerSubagentReport from "../extensions/subagent-report.ts";
|
|
27
28
|
|
|
29
|
+
/** Drives one task through the batch runner and projects its single result like a task-scoped run. */
|
|
30
|
+
async function runSingleTask(prompt, { label, agent, model, tools, systemPrompt, onStalled, ...options } = {}) {
|
|
31
|
+
const name = label ?? "task";
|
|
32
|
+
const batch = await runHerdrSubagentsBatch([{
|
|
33
|
+
name, prompt, agent: agent ?? "worker", access: "write", skills: [],
|
|
34
|
+
...(model ? { model } : {}),
|
|
35
|
+
...(tools ? { tools } : {}),
|
|
36
|
+
systemPrompt: systemPrompt ?? "You are a focused worker subagent.",
|
|
37
|
+
}], {
|
|
38
|
+
label: name, concurrency: 1, maxConcurrency: 1, ...options,
|
|
39
|
+
onUpdate: (partial) => {
|
|
40
|
+
if (onStalled && partial.summary.includes("appears stalled")) onStalled(partial.summary);
|
|
41
|
+
options.onUpdate?.(partial);
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
const task = batch.tasks[0];
|
|
45
|
+
return { ...batch, status: task.status, summary: task.summary, documents: task.documents, errorMessage: task.error ?? task.question, recordDirectory: task.recordDirectory, output: task.summary };
|
|
46
|
+
}
|
|
47
|
+
|
|
28
48
|
let passed = 0;
|
|
29
49
|
let failed = 0;
|
|
30
50
|
function check(name, condition, detail = "") {
|
|
@@ -81,10 +101,19 @@ function fakeHerdr(mode = "completed") {
|
|
|
81
101
|
calls.push(["promptAgent", request]);
|
|
82
102
|
const current = agents.get(request.target) ?? { directory: taskDirectory };
|
|
83
103
|
const directory = current.directory;
|
|
84
|
-
|
|
104
|
+
const runNumber = ++taskRuns;
|
|
85
105
|
active += 1;
|
|
86
106
|
maxActive = Math.max(maxActive, active);
|
|
87
|
-
await new Promise((resolve) => setTimeout(resolve, request.prompt.includes("slow") ? 15 : 2));
|
|
107
|
+
await new Promise((resolve) => setTimeout(resolve, mode === "ui-blocked" ? 30 : request.prompt.includes("slow") ? 15 : 2));
|
|
108
|
+
if (mode === "delivery-unknown-late") {
|
|
109
|
+
const task = JSON.parse(readFileSync(join(directory, "task.json"), "utf8"));
|
|
110
|
+
const attempts = join(directory, "attempts");
|
|
111
|
+
mkdirSync(attempts, { recursive: true });
|
|
112
|
+
writeFileSync(join(attempts, `${task.currentAttemptId}.json`), JSON.stringify({ attemptId: task.currentAttemptId, state: "working" }));
|
|
113
|
+
setTimeout(() => writeAttemptReport(directory, { status: "completed", summary: "Late authoritative report completed.", documents: [] }), 25);
|
|
114
|
+
active -= 1;
|
|
115
|
+
throw new Error("agent_prompt_stalled: delivery acknowledgement timed out");
|
|
116
|
+
}
|
|
88
117
|
if (mode === "delayed-report") {
|
|
89
118
|
const task = JSON.parse(readFileSync(join(directory, "task.json"), "utf8"));
|
|
90
119
|
const attempts = join(directory, "attempts");
|
|
@@ -115,9 +144,9 @@ function fakeHerdr(mode = "completed") {
|
|
|
115
144
|
active -= 1;
|
|
116
145
|
throw new Error("connection lost after authoritative report");
|
|
117
146
|
}
|
|
118
|
-
if (mode === "post-fail" || (mode === "blocked-then-post-fail" &&
|
|
147
|
+
if (mode === "post-fail" || (mode === "blocked-then-post-fail" && runNumber > 1)) { active -= 1; throw new Error("connection lost after prompt submission"); }
|
|
119
148
|
writeFileSync(join(directory, "session.jsonl"), `${JSON.stringify({ type: "session", version: 3, id: request.target, cwd: root })}\n`);
|
|
120
|
-
if (mode === "completed" || mode === "close-failed") {
|
|
149
|
+
if (mode === "completed" || mode === "close-failed" || mode === "ui-blocked") {
|
|
121
150
|
const failed = request.prompt.includes("FAIL");
|
|
122
151
|
writeFileSync(join(directory, "result.md"), request.prompt.startsWith("Review") ? "FULL RESULT THAT STAYS OUT OF PARENT CONTEXT" : `FULL ${request.prompt}`);
|
|
123
152
|
writeAttemptReport(directory, {
|
|
@@ -136,16 +165,22 @@ function fakeHerdr(mode = "completed") {
|
|
|
136
165
|
});
|
|
137
166
|
} else if (mode === "malformed") {
|
|
138
167
|
writeAttemptReport(directory, "{truncated");
|
|
139
|
-
} else if (mode === "blocked" || mode === "blocked-twice" || mode === "blocked-then-post-fail") {
|
|
140
|
-
const needsInput = ((mode === "blocked" || mode === "blocked-then-post-fail") &&
|
|
168
|
+
} else if (mode === "blocked" || mode === "blocked-twice" || mode === "two-blocked-then-complete" || mode === "blocked-then-post-fail") {
|
|
169
|
+
const needsInput = ((mode === "blocked" || mode === "blocked-then-post-fail") && runNumber === 1) || (mode === "blocked-twice" && (runNumber === 1 || runNumber === 3)) || (mode === "two-blocked-then-complete" && runNumber <= 2);
|
|
141
170
|
const failed = request.prompt.includes("FAIL");
|
|
142
|
-
const question =
|
|
171
|
+
const question = runNumber === 3 || (mode === "two-blocked-then-complete" && runNumber === 2) ? "Which environment should I inspect?" : "Which branch should I inspect?";
|
|
143
172
|
writeFileSync(join(directory, "result.md"), `FULL ${request.prompt}`);
|
|
144
|
-
writeAttemptReport(directory, { status: needsInput ? "
|
|
173
|
+
writeAttemptReport(directory, { status: needsInput ? "blocked" : failed ? "failed" : "completed", summary: needsInput ? "Need more input." : failed ? `${request.prompt} failed.` : `${request.prompt} completed.`, ...(needsInput ? { question } : {}), ...(failed ? { error: "expected failure" } : {}), documents: [] });
|
|
145
174
|
}
|
|
146
175
|
active -= 1;
|
|
147
176
|
return { status: mode === "failed" ? "done" : "idle" };
|
|
148
177
|
},
|
|
178
|
+
async getAgentState(target) {
|
|
179
|
+
calls.push(["getAgentState", { target }]);
|
|
180
|
+
if (mode === "ui-blocked") return active > 0 ? "blocked" : "idle";
|
|
181
|
+
if (mode === "delivery-unknown-late") return "working";
|
|
182
|
+
return active > 0 ? "working" : "idle";
|
|
183
|
+
},
|
|
149
184
|
async splitPane(request) {
|
|
150
185
|
calls.push(["splitPane", request]);
|
|
151
186
|
paneSequence += 1;
|
|
@@ -185,6 +220,7 @@ try {
|
|
|
185
220
|
validateControlParams({ action: "status" }),
|
|
186
221
|
validateControlParams({ action: "history", runId: "not-allowed" }),
|
|
187
222
|
validateControlParams({ action: "respond", runId: "run" }),
|
|
223
|
+
validateControlParams({ action: "respond", runId: "run", answer: "named nothing" }),
|
|
188
224
|
validateControlParams({ action: "resume", runId: "run", task: 1 }),
|
|
189
225
|
].every((message) => typeof message === "string" && message.length > 0));
|
|
190
226
|
let cancelledRunId;
|
|
@@ -347,8 +383,10 @@ try {
|
|
|
347
383
|
|
|
348
384
|
setEnv({ HERDR_ENV: "0", PI_HERDR_SUBAGENTS_PI_BINARY: "/definitely/missing/pi" });
|
|
349
385
|
const unavailableHerdr = fakeHerdr();
|
|
350
|
-
|
|
351
|
-
|
|
386
|
+
let outsideError;
|
|
387
|
+
try { await runSingleTask("hello", { cwd: root, herdr: unavailableHerdr }); }
|
|
388
|
+
catch (error) { outsideError = error; }
|
|
389
|
+
check("Herdr is required", outsideError?.message.includes("inside Herdr") && unavailableHerdr.calls.length === 0);
|
|
352
390
|
|
|
353
391
|
setEnv({ HERDR_ENV: "1", HERDR_WORKSPACE_ID: "w1", HERDR_PANE_ID: "w1:p-parent", PI_HERDR_SUBAGENTS_PI_BINARY: "/definitely/missing/pi" });
|
|
354
392
|
const childCommands = new Map();
|
|
@@ -389,7 +427,7 @@ try {
|
|
|
389
427
|
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
428
|
delete process.env.PI_HERDR_SUBAGENTS_TASK_DIR;
|
|
391
429
|
const herdr = fakeHerdr();
|
|
392
|
-
const success = await
|
|
430
|
+
const success = await runSingleTask("Review authentication", {
|
|
393
431
|
cwd: root,
|
|
394
432
|
label: "auth-review",
|
|
395
433
|
agent: "worker",
|
|
@@ -399,7 +437,7 @@ try {
|
|
|
399
437
|
herdr,
|
|
400
438
|
});
|
|
401
439
|
check("compact successful report", success.ok && success.status === "completed" && success.summary === "Authentication review completed." && success.documents.length === 1 && !success.output.includes("FULL RESULT"));
|
|
402
|
-
check("Herdr lifecycle", herdr.calls.map(([name]) => name).join(",") === "createTab,renamePane,startAgent,promptAgent,isTabFocused,closeTab");
|
|
440
|
+
check("Herdr lifecycle", herdr.calls.map(([name]) => name).join(",") === "createTab,renamePane,startAgent,promptAgent,getAgentState,isTabFocused,closeTab");
|
|
403
441
|
const createRequest = herdr.calls.find(([name]) => name === "createTab")[1];
|
|
404
442
|
const startRequest = herdr.calls.find(([name]) => name === "startAgent")[1];
|
|
405
443
|
check("named unfocused tab in parent workspace", createRequest.workspaceId === "w1" && createRequest.cwd === root && createRequest.label === "SA · auth-review" && createRequest.focus === false);
|
|
@@ -417,31 +455,84 @@ try {
|
|
|
417
455
|
check("full result and session stay on disk", readFileSync(join(taskDirectory, "result.md"), "utf8").startsWith("FULL RESULT") && existsSync(join(taskDirectory, "session.jsonl")));
|
|
418
456
|
|
|
419
457
|
const extensionHerdr = fakeHerdr();
|
|
420
|
-
const publicResult = await executeHerdrSubagents({
|
|
458
|
+
const publicResult = await executeHerdrSubagents({ tasks: [{ name: "public", prompt: "Review through the public tool", agent: "worker" }] }, undefined, undefined, {
|
|
421
459
|
cwd: root,
|
|
422
460
|
model: { provider: "fake", id: "model" },
|
|
423
461
|
thinkingLevel: "medium",
|
|
424
462
|
}, { agentsDirectory: join(process.cwd(), "agents"), herdr: extensionHerdr });
|
|
425
|
-
check("public extension behavior uses injectable Herdr", !publicResult.isError && publicResult.content[0].text.includes("Authentication review completed.") && !publicResult.content[0].text.includes("FULL RESULT") && extensionHerdr.calls.map(([name]) => name).join(",") === "createTab,renamePane,startAgent,promptAgent,isTabFocused,closeTab", JSON.stringify({ publicResult, calls: extensionHerdr.calls.map(([name]) => name) }));
|
|
463
|
+
check("public extension behavior uses injectable Herdr", !publicResult.isError && publicResult.content[0].text.includes("Authentication review completed.") && !publicResult.content[0].text.includes("FULL RESULT") && extensionHerdr.calls.map(([name]) => name).join(",") === "createTab,renamePane,startAgent,promptAgent,getAgentState,isTabFocused,closeTab", JSON.stringify({ publicResult, calls: extensionHerdr.calls.map(([name]) => name) }));
|
|
426
464
|
let singleStalledWarning = "";
|
|
427
|
-
await
|
|
465
|
+
await runSingleTask("single slow task", { cwd: root, herdr: fakeHerdr(), stalledWarningSeconds: 0.001, onStalled: (message) => { singleStalledWarning = message; } });
|
|
428
466
|
check("single-task execution emits configured stalled warning", singleStalledWarning.includes("appears stalled"));
|
|
429
467
|
|
|
430
468
|
const failedHerdr = fakeHerdr("failed");
|
|
431
|
-
const failedRun = await
|
|
469
|
+
const failedRun = await runSingleTask("Fail cleanly", { cwd: root, herdr: failedHerdr });
|
|
432
470
|
check("structured child failure", !failedRun.ok && failedRun.status === "failed" && failedRun.summary === "Authentication review failed." && failedRun.errorMessage === "provider unavailable" && failedHerdr.calls.at(-1)[0] === "closeTab");
|
|
433
471
|
|
|
434
472
|
const silentHerdr = fakeHerdr("silent");
|
|
435
|
-
const silent = await
|
|
473
|
+
const silent = await runSingleTask("Forget report", { cwd: root, herdr: silentHerdr });
|
|
436
474
|
check("missing report is visible failure", !silent.ok && silent.errorMessage?.includes("did not submit") && silentHerdr.calls.at(-1)[0] === "closeTab");
|
|
437
|
-
const
|
|
475
|
+
const initialIdleHerdr = fakeHerdr();
|
|
476
|
+
const initialIdlePrompt = initialIdleHerdr.promptAgent.bind(initialIdleHerdr);
|
|
477
|
+
let initialIdleAccepted = false;
|
|
478
|
+
initialIdleHerdr.promptAgent = async (request) => {
|
|
479
|
+
await new Promise((resolve) => setTimeout(resolve, 150));
|
|
480
|
+
initialIdleAccepted = true;
|
|
481
|
+
return initialIdlePrompt(request);
|
|
482
|
+
};
|
|
483
|
+
initialIdleHerdr.getAgentState = async () => initialIdleAccepted ? "working" : "idle";
|
|
484
|
+
const initialIdle = await runSingleTask("Wait for prompt acceptance", { cwd: root, herdr: initialIdleHerdr });
|
|
485
|
+
check("pre-prompt idle is not mistaken for a settled turn", initialIdle.ok && initialIdle.summary.includes("completed"));
|
|
486
|
+
const delayedReport = await runSingleTask("Retry after a transient provider error", { cwd: root, herdr: fakeHerdr("delayed-report") });
|
|
438
487
|
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
|
|
488
|
+
const crashedAttempt = await runSingleTask("Crash after reporting working", { cwd: root, herdr: fakeHerdr("working-state-crash") });
|
|
440
489
|
check("a dead child cannot leave an authoritative working attempt waiting forever", !crashedAttempt.ok && crashedAttempt.errorMessage?.includes("child process exited"));
|
|
441
|
-
const deadWorkingReturn = await
|
|
490
|
+
const deadWorkingReturn = await runSingleTask("Return idle then exit", { cwd: root, herdr: fakeHerdr("working-state-dead-return") });
|
|
442
491
|
check("a normal wait return cannot leave a dead working attempt waiting forever", !deadWorkingReturn.ok && deadWorkingReturn.errorMessage?.includes("Child Pi stopped while"));
|
|
492
|
+
const uiBlockedSnapshots = [];
|
|
493
|
+
const uiBlocked = await runSingleTask("Wait for an approval", { cwd: root, herdr: fakeHerdr("ui-blocked"), onUpdate: (update) => {
|
|
494
|
+
const taskDirectory = update.activity?.[0]?.sessionFile && dirname(update.activity[0].sessionFile);
|
|
495
|
+
if (taskDirectory && existsSync(join(taskDirectory, "task.json"))) uiBlockedSnapshots.push(JSON.parse(readFileSync(join(taskDirectory, "task.json"), "utf8")));
|
|
496
|
+
} });
|
|
497
|
+
check("Herdr blocked is neutral runtime telemetry and never business blocked", uiBlocked.ok && uiBlockedSnapshots.some((task) => task.status === "running" && task.runtimeState === "waiting_interaction") && !uiBlockedSnapshots.some((task) => task.status === "blocked"));
|
|
498
|
+
const deliverySnapshots = [];
|
|
499
|
+
const deliveryUnknown = await runSingleTask("Wait for a late report", { cwd: root, herdr: fakeHerdr("delivery-unknown-late"), onUpdate: (update) => {
|
|
500
|
+
const taskDirectory = update.activity?.[0]?.sessionFile && dirname(update.activity[0].sessionFile);
|
|
501
|
+
if (taskDirectory && existsSync(join(taskDirectory, "task.json"))) deliverySnapshots.push(JSON.parse(readFileSync(join(taskDirectory, "task.json"), "utf8")));
|
|
502
|
+
} });
|
|
503
|
+
check("delivery_unknown keeps running and accepts a late authoritative report", deliveryUnknown.ok && deliveryUnknown.summary === "Late authoritative report completed." && deliverySnapshots.some((task) => task.status === "running" && task.runtimeState === "delivery_unknown"));
|
|
504
|
+
const observationHerdr = fakeHerdr("delayed-report");
|
|
505
|
+
observationHerdr.getAgentState = async () => { throw new Error("temporary agent get failure"); };
|
|
506
|
+
const observationResult = await runSingleTask("Survive observation errors", { cwd: root, herdr: observationHerdr });
|
|
507
|
+
check("agent get failures remain diagnostic until an authoritative report arrives", observationResult.ok && observationResult.summary === "Retry eventually completed.");
|
|
508
|
+
const reportFirstHerdr = fakeHerdr();
|
|
509
|
+
let reportFirstDirectory;
|
|
510
|
+
let promptWaitReaped = false;
|
|
511
|
+
const reportFirstStart = reportFirstHerdr.startAgent.bind(reportFirstHerdr);
|
|
512
|
+
reportFirstHerdr.startAgent = async (request) => {
|
|
513
|
+
const sessionIndex = request.args.indexOf("--session");
|
|
514
|
+
reportFirstDirectory = join(request.args[sessionIndex + 1], "..");
|
|
515
|
+
return reportFirstStart(request);
|
|
516
|
+
};
|
|
517
|
+
reportFirstHerdr.promptAgent = async (request) => {
|
|
518
|
+
const task = JSON.parse(readFileSync(join(reportFirstDirectory, "task.json"), "utf8"));
|
|
519
|
+
mkdirSync(join(reportFirstDirectory, "reports"), { recursive: true });
|
|
520
|
+
writeFileSync(join(reportFirstDirectory, "reports", `${task.currentAttemptId}.json`), JSON.stringify({ attemptId: task.currentAttemptId, status: "completed", summary: "Report won the race.", documents: [] }));
|
|
521
|
+
await new Promise((resolve) => request.signal.addEventListener("abort", resolve, { once: true }));
|
|
522
|
+
promptWaitReaped = true;
|
|
523
|
+
return { status: "idle" };
|
|
524
|
+
};
|
|
525
|
+
const reportFirst = await runSingleTask("Report before prompt wait", { cwd: root, herdr: reportFirstHerdr });
|
|
526
|
+
const reportFirstTask = JSON.parse(readFileSync(join(reportFirst.recordDirectory, "task.json"), "utf8"));
|
|
527
|
+
check("authoritative report aborts and awaits prompt wait without reverting terminal state", reportFirst.ok && promptWaitReaped && reportFirstTask.status === "completed" && reportFirstTask.summary === undefined);
|
|
528
|
+
const terminalError = await runSingleTask("No report after API failure", { cwd: root, herdr: fakeHerdr("post-fail") });
|
|
529
|
+
const terminalErrorTask = JSON.parse(readFileSync(join(terminalError.recordDirectory, "task.json"), "utf8"));
|
|
530
|
+
check("terminal prompt API error without a report fails the business task", !terminalError.ok && terminalErrorTask.status === "failed" && terminalError.errorMessage.includes("connection lost"));
|
|
531
|
+
const retryProjection = projectTaskActivity({ id: "retry", runId: "run", order: 1, name: "retrying", agent: "worker", status: "running", runtimeState: "retrying" });
|
|
532
|
+
check("retrying support remains running and is visible in Fleet", retryProjection.status === "running" && retryProjection.runtimeState === "retrying" && fleetLines({ label: "retry", activity: [retryProjection] }, new FleetSelection())[1].includes("retrying"));
|
|
533
|
+
|
|
443
534
|
const malformedExecutionHerdr = fakeHerdr("malformed");
|
|
444
|
-
const malformedExecution = await
|
|
535
|
+
const malformedExecution = await runSingleTask("Write malformed report", { cwd: root, herdr: malformedExecutionHerdr });
|
|
445
536
|
const malformedExecutionTaskDirectory = malformedExecution.recordDirectory;
|
|
446
537
|
const repairedExecutionReport = JSON.parse(readFileSync(join(malformedExecutionTaskDirectory, "report.json"), "utf8"));
|
|
447
538
|
check("malformed child report becomes a durable usable failure", !malformedExecution.ok && repairedExecutionReport.status === "failed" && readdirSync(malformedExecutionTaskDirectory).some((entry) => entry.startsWith("report.json.invalid-")) && (await listSubagentHistory(root)).some((run) => run.id === malformedExecution.runId));
|
|
@@ -450,7 +541,7 @@ try {
|
|
|
450
541
|
check("historical attempts use distinct full-result documents", firstResultPath.endsWith("/result.md") && secondResultPath.endsWith("/turns/02-result.md") && firstResultPath !== secondResultPath);
|
|
451
542
|
|
|
452
543
|
const cleanupHerdr = fakeHerdr("close-failed");
|
|
453
|
-
const cleanupFailure = await
|
|
544
|
+
const cleanupFailure = await runSingleTask("Complete then fail cleanup", { cwd: root, herdr: cleanupHerdr });
|
|
454
545
|
const cleanupRunDirectory = latestRunDirectory();
|
|
455
546
|
const cleanupRunRecord = JSON.parse(readFileSync(join(cleanupRunDirectory, "run.json"), "utf8"));
|
|
456
547
|
const cleanupTaskDirectory = join(cleanupRunDirectory, "tasks", readdirSync(join(cleanupRunDirectory, "tasks"))[0]);
|
|
@@ -503,7 +594,7 @@ try {
|
|
|
503
594
|
check("invalid project default thinking is diagnosed and global default remains effective", invalidExplorer?.thinking === "medium" && invalidExplorer.thinkingSource === "global" && invalidConfiguration.diagnostics.some((message) => message === "project defaultThinking must be a non-empty string"));
|
|
504
595
|
writeFileSync(projectConfig, JSON.stringify({ maxConcurrency: 3, defaultThinking: "high", personas: { worker: { model: "project/worker", thinking: "high", skills: ["project-skill"] } } }));
|
|
505
596
|
const configuredHerdr = fakeHerdr();
|
|
506
|
-
const configuredRun = await executeHerdrSubagents({
|
|
597
|
+
const configuredRun = await executeHerdrSubagents({ tasks: [{ name: "configured", prompt: "Use configured values", agent: "worker", model: "task/model" }] }, undefined, undefined, { cwd: configRoot, model: { provider: "parent", id: "model" }, thinkingLevel: "medium" }, { agentsDirectory: packageAgents, configurationPaths, herdr: configuredHerdr });
|
|
507
598
|
const configuredStart = configuredHerdr.calls.find(([name]) => name === "startAgent")[1];
|
|
508
599
|
check("configured persona uses displayed effective values", !configuredRun.isError && configuredStart.args.includes("task/model") && configuredStart.args.includes("high") && configuredStart.args.includes("--skill") && configuredStart.args.includes("project-skill"));
|
|
509
600
|
|
|
@@ -533,6 +624,29 @@ try {
|
|
|
533
624
|
check("read-only batch uses bounded concurrency", batchDetails.effectiveConcurrency === 2 && batchHerdr.maxActive === 2 && batchHerdr.calls.filter(([name]) => name === "splitPane").length === 3);
|
|
534
625
|
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);
|
|
535
626
|
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");
|
|
627
|
+
const parallelQuestionHerdr = fakeHerdr("blocked");
|
|
628
|
+
const parallelQuestion = await executeHerdrSubagents({ concurrency: 2, tasks: [
|
|
629
|
+
{ name: "missing-input", prompt: "question", agent: "explorer" },
|
|
630
|
+
{ name: "independent", prompt: "continue", agent: "reviewer" },
|
|
631
|
+
] }, undefined, undefined, { cwd: root }, { agentsDirectory: join(process.cwd(), "agents"), herdr: parallelQuestionHerdr });
|
|
632
|
+
const parallelQuestionTask = JSON.parse(readFileSync(join(parallelQuestion.details.tasks[0].recordDirectory, "task.json"), "utf8"));
|
|
633
|
+
const parallelQuestionReport = JSON.parse(readFileSync(join(parallelQuestion.details.tasks[0].recordDirectory, "report.json"), "utf8"));
|
|
634
|
+
const parallelQuestionAttemptReport = JSON.parse(readFileSync(join(parallelQuestion.details.tasks[0].recordDirectory, "reports", `${parallelQuestionTask.currentAttemptId}.json`), "utf8"));
|
|
635
|
+
check("a parallel blocked attempt report stays blocked with its question", parallelQuestionAttemptReport.status === "blocked" && parallelQuestionAttemptReport.question === "Which branch should I inspect?" && parallelQuestionReport.question === parallelQuestionAttemptReport.question);
|
|
636
|
+
check("a parallel task blocks like a serial one while its independent sibling finishes", !parallelQuestion.isError && parallelQuestion.details.status === "blocked" && parallelQuestion.details.question === "Which branch should I inspect?" && parallelQuestion.details.allowedActions?.join(",") === "respond,cancel" && parallelQuestion.details.tasks[0].status === "blocked" && parallelQuestion.details.tasks[1].status === "completed" && parallelQuestionTask.status === "blocked" && parallelQuestionReport.status === "blocked" && !parallelQuestionHerdr.calls.some(([name]) => name === "closeTab"));
|
|
637
|
+
const parallelAnswered = await executeHerdrSubagents({ action: "respond", runId: parallelQuestion.details.runId, task: 1, answer: "Inspect main." }, undefined, undefined, { cwd: root, model: { provider: "fake", id: "model" } }, { agentsDirectory: join(process.cwd(), "agents"), herdr: parallelQuestionHerdr });
|
|
638
|
+
check("answering a blocked task in a parallel run settles and closes that run", !parallelAnswered.isError && parallelAnswered.details.status === "completed" && JSON.parse(readFileSync(join(parallelQuestion.details.tasks[0].recordDirectory, "task.json"), "utf8")).status === "completed" && parallelQuestionHerdr.calls.some(([name]) => name === "closeTab"));
|
|
639
|
+
|
|
640
|
+
const bothBlockedHerdr = fakeHerdr("two-blocked-then-complete");
|
|
641
|
+
const bothBlocked = await executeHerdrSubagents({ concurrency: 2, tasks: [
|
|
642
|
+
{ name: "first-question", prompt: "ask", agent: "explorer" },
|
|
643
|
+
{ name: "second-question", prompt: "ask too", agent: "reviewer" },
|
|
644
|
+
] }, undefined, undefined, { cwd: root }, { agentsDirectory: join(process.cwd(), "agents"), herdr: bothBlockedHerdr });
|
|
645
|
+
check("both lanes of a parallel batch may block at once", !bothBlocked.isError && bothBlocked.details.status === "blocked" && bothBlocked.details.tasks[0].status === "blocked" && bothBlocked.details.tasks[1].status === "blocked" && bothBlocked.details.question === "Which branch should I inspect?" && !bothBlockedHerdr.calls.some(([name]) => name === "closeTab"));
|
|
646
|
+
const firstOfBoth = await executeHerdrSubagents({ action: "respond", runId: bothBlocked.details.runId, task: 1, answer: "main" }, undefined, undefined, { cwd: root, model: { provider: "fake", id: "model" } }, { agentsDirectory: join(process.cwd(), "agents"), herdr: bothBlockedHerdr });
|
|
647
|
+
check("answering one blocked lane exposes the other lane's question and keeps the tab", !firstOfBoth.isError && firstOfBoth.details.status === "blocked" && firstOfBoth.details.question === "Which environment should I inspect?" && firstOfBoth.details.tasks[0].status === "completed" && firstOfBoth.details.tasks[1].status === "blocked" && !bothBlockedHerdr.calls.some(([name]) => name === "closeTab"));
|
|
648
|
+
const secondOfBoth = await executeHerdrSubagents({ action: "respond", runId: bothBlocked.details.runId, task: 2, answer: "staging" }, undefined, undefined, { cwd: root, model: { provider: "fake", id: "model" } }, { agentsDirectory: join(process.cwd(), "agents"), herdr: bothBlockedHerdr });
|
|
649
|
+
check("the parallel run closes only after its last blocked lane is answered", !secondOfBoth.isError && secondOfBoth.details.status === "completed" && secondOfBoth.details.tasks.every((entry) => entry.status === "completed") && bothBlockedHerdr.calls.filter(([name]) => name === "closeTab").length === 1);
|
|
536
650
|
const mixedCancelHerdr = fakeHerdr();
|
|
537
651
|
const mixedCancelPrompt = mixedCancelHerdr.promptAgent.bind(mixedCancelHerdr);
|
|
538
652
|
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);
|
|
@@ -547,6 +661,21 @@ try {
|
|
|
547
661
|
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 });
|
|
548
662
|
const batchCleanupRecord = JSON.parse(readFileSync(join(batchCleanup.details.recordDirectory, "run.json"), "utf8"));
|
|
549
663
|
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);
|
|
664
|
+
const concurrentWriterHerdr = fakeHerdr();
|
|
665
|
+
let concurrentWriterRunFile;
|
|
666
|
+
const concurrentWriterStart = concurrentWriterHerdr.startAgent.bind(concurrentWriterHerdr);
|
|
667
|
+
concurrentWriterHerdr.startAgent = async (request) => {
|
|
668
|
+
concurrentWriterRunFile = join(request.args[request.args.indexOf("--session") + 1], "..", "..", "..", "run.json");
|
|
669
|
+
return concurrentWriterStart(request);
|
|
670
|
+
};
|
|
671
|
+
const concurrentWriterPrompt = concurrentWriterHerdr.promptAgent.bind(concurrentWriterHerdr);
|
|
672
|
+
concurrentWriterHerdr.promptAgent = async (request) => {
|
|
673
|
+
await withRunStateLock(concurrentWriterRunFile, undefined, (run) => Object.assign(run, { resume: { generation: 1, phase: "active", tabId: "w1:t9", paneIds: [], active: {} } }));
|
|
674
|
+
return concurrentWriterPrompt(request);
|
|
675
|
+
};
|
|
676
|
+
await executeHerdrSubagents({ tasks: [{ name: "concurrent-writer", prompt: "inspect", agent: "explorer" }] }, undefined, undefined, { cwd: root, model: { provider: "fake", id: "model" } }, { agentsDirectory: join(process.cwd(), "agents"), herdr: concurrentWriterHerdr });
|
|
677
|
+
const concurrentWriterRecord = JSON.parse(readFileSync(concurrentWriterRunFile, "utf8"));
|
|
678
|
+
check("settling a batch preserves run state written concurrently", concurrentWriterRecord.status === "completed" && concurrentWriterRecord.resume?.tabId === "w1:t9");
|
|
550
679
|
const cleanupProbeHerdr = fakeHerdr();
|
|
551
680
|
cleanupProbeHerdr.isTabFocused = async () => { throw new Error("tab lookup failed"); };
|
|
552
681
|
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 });
|
|
@@ -584,7 +713,7 @@ try {
|
|
|
584
713
|
const pendingCleanupRecord = JSON.parse(readFileSync(join(pendingCleanupRun.details.recordDirectory, "run.json"), "utf8"));
|
|
585
714
|
const cleanupWhilePending = await executeHerdrSubagents({ action: "cleanup", runId: pendingCleanupRun.details.runId }, undefined, undefined, { cwd: root }, { agentsDirectory: join(process.cwd(), "agents"), herdr: pendingCleanupHerdr });
|
|
586
715
|
const resumeWhilePending = await executeHerdrSubagents({ action: "resume", runId: pendingCleanupRun.details.runId, task: 1, prompt: "too soon" }, undefined, undefined, { cwd: root }, { agentsDirectory: join(process.cwd(), "agents"), herdr: pendingCleanupHerdr });
|
|
587
|
-
check("pending tab cleanup blocks
|
|
716
|
+
check("pending tab cleanup blocks deletion but permits an isolated resume generation", pendingCleanupRecord.cleanupPendingTabIds?.includes(pendingCleanupRun.details.tabId) && cleanupWhilePending.isError && !resumeWhilePending.isError);
|
|
588
717
|
releasePendingCleanup();
|
|
589
718
|
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
590
719
|
const releasedCleanupRecord = JSON.parse(readFileSync(join(pendingCleanupRun.details.recordDirectory, "run.json"), "utf8"));
|
|
@@ -614,10 +743,10 @@ try {
|
|
|
614
743
|
deferredSingleHerdr.isTabFocused = async () => true;
|
|
615
744
|
deferredSingleHerdr.closeTab = async () => { throw new Error("single deferred close failed"); };
|
|
616
745
|
const deferredSingleUpdates = [];
|
|
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 });
|
|
746
|
+
const deferredSingle = await executeHerdrSubagents({ tasks: [{ name: "deferred-single", 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 });
|
|
618
747
|
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
619
|
-
const deferredSingleRun = JSON.parse(readFileSync(join(deferredSingle.details.recordDirectory, "
|
|
620
|
-
check("deferred
|
|
748
|
+
const deferredSingleRun = JSON.parse(readFileSync(join(deferredSingle.details.recordDirectory, "run.json"), "utf8"));
|
|
749
|
+
check("deferred 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.cleanupError));
|
|
621
750
|
const cancelledHerdr = fakeHerdr();
|
|
622
751
|
cancelledHerdr.isTabFocused = async (tabId) => { cancelledHerdr.calls.push(["isTabFocused", { tabId }]); return true; };
|
|
623
752
|
cancelledHerdr.waitForTabUnfocused = async () => { throw new Error("shutdown cleanup must not wait for focus"); };
|
|
@@ -644,7 +773,7 @@ try {
|
|
|
644
773
|
});
|
|
645
774
|
};
|
|
646
775
|
const singleCancellation = new AbortController();
|
|
647
|
-
const cancelledSinglePromise =
|
|
776
|
+
const cancelledSinglePromise = runSingleTask("wait", { cwd: root, herdr: cancelledSingleHerdr, signal: singleCancellation.signal });
|
|
648
777
|
await new Promise((resolve) => setTimeout(resolve, 5));
|
|
649
778
|
singleCancellation.abort(new Error("Parent Pi session closed."));
|
|
650
779
|
const cancelledSingle = await cancelledSinglePromise;
|
|
@@ -677,6 +806,20 @@ try {
|
|
|
677
806
|
check("dispatcher cancels only the selected queued run", cancellingDispatcher.cancelQueuedRun("remove-me", new Error("cancelled by user")) && await removedQueued === "cancelled by user" && cancellingDispatcher.snapshot().queuedRunIds.join(",") === "keep-me");
|
|
678
807
|
cancellingDispatcher.resume();
|
|
679
808
|
check("dispatcher still executes uncancelled queued siblings", await keptQueued === "kept");
|
|
809
|
+
const abortingDispatcher = new RunDispatcher();
|
|
810
|
+
abortingDispatcher.pause();
|
|
811
|
+
const queuedAbort = new AbortController();
|
|
812
|
+
let abortedQueuedStarted = false;
|
|
813
|
+
const abortedQueued = abortingDispatcher.submit("abort-me", async () => { abortedQueuedStarted = true; return "unexpected"; }, { signal: queuedAbort.signal }).catch((error) => error.message);
|
|
814
|
+
queuedAbort.abort(new Error("foreground tool call cancelled"));
|
|
815
|
+
check("queued foreground dispatch responds to caller cancellation", await abortedQueued === "foreground tool call cancelled" && !abortedQueuedStarted && abortingDispatcher.snapshot().queuedRunIds.length === 0);
|
|
816
|
+
const retainingDispatcher = new RunDispatcher();
|
|
817
|
+
let settleRetainingOwner;
|
|
818
|
+
let retainedWaiterStarted = false;
|
|
819
|
+
const retainingOwner = retainingDispatcher.submit("retaining-owner", async () => { await new Promise((resolve) => { settleRetainingOwner = resolve; }); retainingDispatcher.retain("retaining-owner"); return "blocked"; });
|
|
820
|
+
const retainedWaiter = retainingDispatcher.submit("foreground-waiter", async () => { retainedWaiterStarted = true; return "unexpected"; }, { onRetained: (runId) => `blocked by ${runId}` });
|
|
821
|
+
settleRetainingOwner();
|
|
822
|
+
check("retaining an active run rejects already-queued foreground waiters", await retainingOwner === "blocked" && await retainedWaiter === "blocked by retaining-owner" && !retainedWaiterStarted && retainingDispatcher.snapshot().queuedRunIds.length === 0);
|
|
680
823
|
const retainedDispatcher = new RunDispatcher();
|
|
681
824
|
const retainedStarts = [];
|
|
682
825
|
const retainedFirst = retainedDispatcher.submit("blocked-run", async () => { retainedStarts.push("blocked-run"); retainedDispatcher.retain("blocked-run"); return "blocked"; });
|
|
@@ -729,29 +872,32 @@ try {
|
|
|
729
872
|
mkdirSync(staleStatusTaskDirectory, { recursive: true });
|
|
730
873
|
writeFileSync(join(staleStatusTaskDirectory, "..", "..", "run.json"), JSON.stringify({ id: "stale-status", label: "stale-status", status: "running", startedAt: "2026-10-08T00:00:00.000Z" }));
|
|
731
874
|
writeFileSync(join(staleStatusTaskDirectory, "task.json"), JSON.stringify({ id: "stale-task", runId: "stale-status", order: 1, name: "stale-task", agent: "reviewer", status: "running" }));
|
|
732
|
-
writeFileSync(join(staleStatusTaskDirectory, "report.json"), JSON.stringify({ status: "
|
|
875
|
+
writeFileSync(join(staleStatusTaskDirectory, "report.json"), JSON.stringify({ status: "blocked", summary: "Old blocked report.", question: "Old question?", documents: [] }));
|
|
733
876
|
const runningStatus = await executeHerdrSubagents({ action: "status", runId: "stale-status" }, undefined, undefined, { cwd: staleStatusRoot }, { agentsDirectory: join(process.cwd(), "agents") });
|
|
734
877
|
check("running status does not expose a stale report from an earlier blocked turn", runningStatus.details.tasks[0].status === "running" && !runningStatus.details.tasks[0].question && !runningStatus.details.tasks[0].summary && !runningStatus.content[0].text.includes("Old question"));
|
|
735
878
|
|
|
736
879
|
const blockedHerdr = fakeHerdr("blocked");
|
|
737
880
|
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 });
|
|
738
|
-
check("
|
|
739
|
-
const blockedStatus = await executeHerdrSubagents({ action: "status", runId: blocked.details.
|
|
881
|
+
check("a blocked report returns foreground control with its full run ID and allowed actions", !blocked.isError && blocked.details.status === "blocked" && blocked.details.question === "Which branch should I inspect?" && blocked.details.tasks[0].question === blocked.details.question && blocked.details.allowedActions?.join(",") === "respond,cancel" && blocked.content[0].text.includes(blocked.details.runId) && !blockedHerdr.calls.some(([name]) => name === "closeTab"));
|
|
882
|
+
const blockedStatus = await executeHerdrSubagents({ action: "status", runId: basename(blocked.details.recordDirectory) }, undefined, undefined, { cwd: root }, { agentsDirectory: join(process.cwd(), "agents") });
|
|
740
883
|
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?"));
|
|
741
884
|
const activeCleanup = await executeHerdrSubagents({ action: "cleanup", runId: blocked.details.runId }, undefined, undefined, { cwd: root }, { agentsDirectory: join(process.cwd(), "agents"), herdr: blockedHerdr });
|
|
742
885
|
check("cleanup refuses to delete an active blocked run", activeCleanup.isError && activeCleanup.content[0].text.includes("still active") && existsSync(blocked.details.recordDirectory));
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
886
|
+
const blockedTaskLock = join(blocked.details.tasks[0].recordDirectory, ".resume.lock");
|
|
887
|
+
mkdirSync(blockedTaskLock);
|
|
888
|
+
writeFileSync(join(blockedTaskLock, "owner.json"), JSON.stringify({ token: "live-owner", processId: process.pid, createdAt: new Date().toISOString() }));
|
|
889
|
+
const concurrentResponse = await executeHerdrSubagents({ action: "respond", runId: blocked.details.runId, task: 1, answer: "duplicate" }, undefined, undefined, { cwd: root }, { agentsDirectory: join(process.cwd(), "agents"), herdr: blockedHerdr });
|
|
890
|
+
rmSync(blockedTaskLock, { recursive: true });
|
|
891
|
+
check("concurrent blocked responses are rejected by the task-scoped lock before a second prompt", concurrentResponse.isError && concurrentResponse.content[0].text.includes("response or resume in progress"));
|
|
747
892
|
const staleLockHerdr = fakeHerdr("blocked");
|
|
748
893
|
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
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
894
|
+
const staleResponseTaskLock = join(staleLockBlocked.details.tasks[0].recordDirectory, ".resume.lock");
|
|
895
|
+
mkdirSync(staleResponseTaskLock);
|
|
896
|
+
writeFileSync(join(staleResponseTaskLock, "owner.json"), JSON.stringify({ token: "dead-owner", processId: 99999999, createdAt: "2000-01-01T00:00:00.000Z" }));
|
|
897
|
+
const diagnosedStaleResponse = await executeHerdrSubagents({ action: "respond", runId: staleLockBlocked.details.runId, task: 1, answer: "Inspect main." }, undefined, undefined, { cwd: root }, { agentsDirectory: join(process.cwd(), "agents"), herdr: staleLockHerdr });
|
|
898
|
+
check("blocked response automatically reclaims a task lock owned by a dead process", !diagnosedStaleResponse.isError && !existsSync(staleResponseTaskLock));
|
|
753
899
|
const resumeUpdates = [];
|
|
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 });
|
|
900
|
+
const resumed = await executeHerdrSubagents({ action: "respond", runId: blocked.details.runId.slice(0, 8), task: 1, answer: "Inspect main." }, undefined, (update) => resumeUpdates.push(update.details), { cwd: root, model: { provider: "fake", id: "model" } }, { agentsDirectory: join(process.cwd(), "agents"), herdr: blockedHerdr });
|
|
755
901
|
const blockedStarts = blockedHerdr.calls.filter(([name]) => name === "startAgent").map(([, request]) => request);
|
|
756
902
|
const blockedPrompts = blockedHerdr.calls.filter(([name]) => name === "promptAgent").map(([, request]) => request);
|
|
757
903
|
check("answer resumes original live Agent and queued sibling gets a new pane", !resumed.isError && resumed.details.status === "completed" && blockedPrompts.length === 3 && blockedPrompts[1].target === blockedPrompts[0].target && blockedStarts.length === 2 && new Set(blockedStarts.map((request) => request.paneId)).size === 2);
|
|
@@ -762,22 +908,241 @@ try {
|
|
|
762
908
|
check("blocked continuation preserves newly allocated pane IDs", resumedTasks.every((task) => resumedRunRecord.paneIds.includes(task.paneId)));
|
|
763
909
|
const resumedTurn = JSON.parse(readFileSync(resumedTasks[0].currentTurnFile, "utf8"));
|
|
764
910
|
check("authoritative response clears stale blocked fields", !("question" in resumedTasks[0]) && !("error" in resumedTasks[0]) && !("question" in resumedTurn) && !("error" in resumedTurn));
|
|
911
|
+
const runsIn = (base) => {
|
|
912
|
+
const directory = join(base, ".pi", "herdr-subagents", "runs");
|
|
913
|
+
const entries = readdirSync(directory).sort();
|
|
914
|
+
return join(directory, entries[entries.length - 1]);
|
|
915
|
+
};
|
|
916
|
+
const tasksIn = (runDirectory) => readdirSync(join(runDirectory, "tasks")).sort().map((entry) => join(runDirectory, "tasks", entry));
|
|
917
|
+
const readTaskJson = (directory) => JSON.parse(readFileSync(join(directory, "task.json"), "utf8"));
|
|
918
|
+
const settleWait = async (predicate) => {
|
|
919
|
+
for (let elapsed = 0; elapsed < 3000; elapsed += 5) {
|
|
920
|
+
if (predicate()) return true;
|
|
921
|
+
await new Promise((resolve) => setTimeout(resolve, 5));
|
|
922
|
+
}
|
|
923
|
+
return false;
|
|
924
|
+
};
|
|
925
|
+
|
|
926
|
+
const midBatchRoot = join(root, "mid-batch-followup");
|
|
927
|
+
const midBatchHerdr = fakeHerdr();
|
|
928
|
+
const midBatchPrompt = midBatchHerdr.promptAgent.bind(midBatchHerdr);
|
|
929
|
+
let releaseMidBatch;
|
|
930
|
+
const midBatchGate = new Promise((resolve) => { releaseMidBatch = resolve; });
|
|
931
|
+
let reportMidBatchGated;
|
|
932
|
+
const midBatchGated = new Promise((resolve) => { reportMidBatchGated = resolve; });
|
|
933
|
+
midBatchHerdr.promptAgent = async (request) => {
|
|
934
|
+
if (request.prompt.includes("WAIT")) { reportMidBatchGated(); await midBatchGate; }
|
|
935
|
+
return midBatchPrompt(request);
|
|
936
|
+
};
|
|
937
|
+
const midBatchPromise = executeHerdrSubagents({ label: "mid-batch", concurrency: 1, tasks: [
|
|
938
|
+
{ name: "fails", prompt: "FAIL immediately", agent: "explorer" },
|
|
939
|
+
{ name: "holds", prompt: "WAIT for the gate", agent: "explorer" },
|
|
940
|
+
{ name: "last", prompt: "third task", agent: "explorer" },
|
|
941
|
+
] }, undefined, undefined, { cwd: midBatchRoot, model: { provider: "fake", id: "model" } }, { agentsDirectory: join(process.cwd(), "agents"), herdr: midBatchHerdr });
|
|
942
|
+
await midBatchGated;
|
|
943
|
+
const midBatchRun = runsIn(midBatchRoot);
|
|
944
|
+
const midBatchTasks = tasksIn(midBatchRun);
|
|
945
|
+
check("a later task stays queued while an earlier task of the same batch fails", readTaskJson(midBatchTasks[0]).status === "failed" && readTaskJson(midBatchTasks[2]).status === "queued");
|
|
946
|
+
const midBatchStartsBefore = midBatchHerdr.calls.filter(([name]) => name === "startAgent").length;
|
|
947
|
+
const midBatchFollowUp = await executeHerdrSubagents({ action: "respond", runId: basename(midBatchRun), task: 1, answer: "retry against the other branch" }, undefined, undefined, { cwd: midBatchRoot, model: { provider: "fake", id: "model" } }, { agentsDirectory: join(process.cwd(), "agents"), herdr: midBatchHerdr });
|
|
948
|
+
check("a failed task continues in its own live pane while its batch is still running", !midBatchFollowUp.isError && readTaskJson(midBatchTasks[0]).status === "completed" && midBatchHerdr.calls.filter(([name]) => name === "createTab").length === 1);
|
|
949
|
+
check("continuing a failed task leaves queued siblings to the running batch", readTaskJson(midBatchTasks[2]).status === "queued" && midBatchHerdr.calls.filter(([name]) => name === "startAgent").length === midBatchStartsBefore);
|
|
950
|
+
releaseMidBatch();
|
|
951
|
+
const midBatch = await midBatchPromise;
|
|
952
|
+
const midBatchPrompts = midBatchHerdr.calls.filter(([name]) => name === "promptAgent").map(([, request]) => request.prompt);
|
|
953
|
+
check("the batch still runs its own queued work exactly once after a follow-up", midBatchPrompts.filter((prompt) => prompt === "third task").length === 1 && tasksIn(midBatchRun).every((directory) => readTaskJson(directory).status === "completed"));
|
|
954
|
+
check("a settling batch records the task records instead of its own stale view", JSON.parse(readFileSync(join(midBatchRun, "run.json"), "utf8")).status === "completed" && midBatch.details.tasks[0].status === "failed");
|
|
955
|
+
|
|
956
|
+
const queueOwnerRoot = join(root, "parallel-queue-ownership");
|
|
957
|
+
const queueOwnerHerdr = fakeHerdr("blocked");
|
|
958
|
+
const queueOwnerPrompt = queueOwnerHerdr.promptAgent.bind(queueOwnerHerdr);
|
|
959
|
+
let releaseQueueOwner;
|
|
960
|
+
const queueOwnerGate = new Promise((resolve) => { releaseQueueOwner = resolve; });
|
|
961
|
+
let reportQueueOwnerGated;
|
|
962
|
+
const queueOwnerGated = new Promise((resolve) => { reportQueueOwnerGated = resolve; });
|
|
963
|
+
queueOwnerHerdr.promptAgent = async (request) => {
|
|
964
|
+
const result = await queueOwnerPrompt(request);
|
|
965
|
+
if (request.prompt.includes("WAIT")) { reportQueueOwnerGated(); await queueOwnerGate; }
|
|
966
|
+
return result;
|
|
967
|
+
};
|
|
968
|
+
const queueOwnerPromise = executeHerdrSubagents({ label: "queue-owner", concurrency: 2, tasks: [
|
|
969
|
+
{ name: "blocks", prompt: "needs an answer", agent: "explorer" },
|
|
970
|
+
{ name: "holds", prompt: "WAIT for the gate", agent: "explorer" },
|
|
971
|
+
{ name: "last", prompt: "queued work", agent: "explorer" },
|
|
972
|
+
] }, undefined, undefined, { cwd: queueOwnerRoot, model: { provider: "fake", id: "model" } }, { agentsDirectory: join(process.cwd(), "agents"), herdr: queueOwnerHerdr });
|
|
973
|
+
await queueOwnerGated;
|
|
974
|
+
const queueOwnerRun = runsIn(queueOwnerRoot);
|
|
975
|
+
const queueOwnerTasks = tasksIn(queueOwnerRun);
|
|
976
|
+
check("a parallel lane blocks without stopping its sibling or starting queued work", await settleWait(() => readTaskJson(queueOwnerTasks[0]).status === "blocked") && readTaskJson(queueOwnerTasks[1]).status === "running" && readTaskJson(queueOwnerTasks[2]).status === "queued");
|
|
977
|
+
const queueOwnerStarts = queueOwnerHerdr.calls.filter(([name]) => name === "startAgent").length;
|
|
978
|
+
const queueOwnerAnswer = await executeHerdrSubagents({ action: "respond", runId: basename(queueOwnerRun), task: 1, answer: "use the main branch" }, undefined, undefined, { cwd: queueOwnerRoot, model: { provider: "fake", id: "model" } }, { agentsDirectory: join(process.cwd(), "agents"), herdr: queueOwnerHerdr });
|
|
979
|
+
check("answering a blocked lane leaves queued work to the batch that owns it", !queueOwnerAnswer.isError && queueOwnerAnswer.details.status === "running" && readTaskJson(queueOwnerTasks[0]).status === "completed" && readTaskJson(queueOwnerTasks[2]).status === "queued" && queueOwnerHerdr.calls.filter(([name]) => name === "startAgent").length === queueOwnerStarts);
|
|
980
|
+
releaseQueueOwner();
|
|
981
|
+
const queueOwner = await queueOwnerPromise;
|
|
982
|
+
check("the batch runs its own queued work exactly once after a blocked lane is answered", queueOwnerHerdr.calls.filter(([name, request]) => name === "promptAgent" && request.prompt === "queued work").length === 1);
|
|
983
|
+
check("an adopted lane drops the lifecycle fields its blocked record carried", queueOwner.details.activity[0].status === "completed" && Boolean(queueOwner.details.activity[0].completedAt) && !("updatedAt" in queueOwner.details.activity[0]));
|
|
984
|
+
check("a settled batch adopts a lane that was answered while its siblings ran", !queueOwner.isError && queueOwner.details.status === "completed" && queueOwner.details.tasks[0].status === "completed" && !queueOwner.details.question && JSON.parse(readFileSync(join(queueOwnerRun, "run.json"), "utf8")).status === "completed" && queueOwnerHerdr.calls.some(([name]) => name === "closeTab"));
|
|
985
|
+
|
|
986
|
+
const revivedRoot = join(root, "revived-during-settle");
|
|
987
|
+
const revivedHerdr = fakeHerdr();
|
|
988
|
+
const revivedPrompt = revivedHerdr.promptAgent.bind(revivedHerdr);
|
|
989
|
+
let revivedFlipped = false;
|
|
990
|
+
revivedHerdr.promptAgent = async (request) => {
|
|
991
|
+
if (request.prompt.includes("WAIT")) {
|
|
992
|
+
const failedFirst = await settleWait(() => {
|
|
993
|
+
const directories = tasksIn(runsIn(revivedRoot));
|
|
994
|
+
return directories.length > 0 && existsSync(join(directories[0], "task.json")) && readTaskJson(directories[0]).status === "failed";
|
|
995
|
+
});
|
|
996
|
+
if (failedFirst) {
|
|
997
|
+
const first = tasksIn(runsIn(revivedRoot))[0];
|
|
998
|
+
const record = readTaskJson(first);
|
|
999
|
+
delete record.completedAt;
|
|
1000
|
+
delete record.error;
|
|
1001
|
+
writeFileSync(join(first, "task.json"), JSON.stringify({ ...record, status: "running", updatedAt: new Date().toISOString() }));
|
|
1002
|
+
revivedFlipped = true;
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
return revivedPrompt(request);
|
|
1006
|
+
};
|
|
1007
|
+
const revived = await executeHerdrSubagents({ label: "revived", concurrency: 2, tasks: [
|
|
1008
|
+
{ name: "fails", prompt: "FAIL immediately", agent: "explorer" },
|
|
1009
|
+
{ name: "holds", prompt: "WAIT then settle", agent: "explorer" },
|
|
1010
|
+
] }, undefined, undefined, { cwd: revivedRoot, model: { provider: "fake", id: "model" } }, { agentsDirectory: join(process.cwd(), "agents"), herdr: revivedHerdr });
|
|
1011
|
+
const revivedRunRecord = JSON.parse(readFileSync(join(runsIn(revivedRoot), "run.json"), "utf8"));
|
|
1012
|
+
check("a settling batch never closes its tab or finishes the run while a continued task works", revivedFlipped && revivedRunRecord.status === "running" && !revivedHerdr.calls.some(([name]) => name === "closeTab") && revived.details.status === "partial");
|
|
1013
|
+
|
|
1014
|
+
const continueRoot = join(root, "continue-failed-task");
|
|
1015
|
+
const continueHerdr = fakeHerdr();
|
|
1016
|
+
const failedOnly = await executeHerdrSubagents({ label: "solo", tasks: [{ name: "solo", prompt: "FAIL alone", agent: "explorer" }] }, undefined, undefined, { cwd: continueRoot, model: { provider: "fake", id: "model" } }, { agentsDirectory: join(process.cwd(), "agents"), herdr: continueHerdr });
|
|
1017
|
+
const continueRun = runsIn(continueRoot);
|
|
1018
|
+
const continueTask = tasksIn(continueRun)[0];
|
|
1019
|
+
check("a settled failed task keeps its saved Pi session on disk", failedOnly.isError && readTaskJson(continueTask).status === "failed" && existsSync(join(continueTask, "session.jsonl")));
|
|
1020
|
+
const unqualifiedResponse = await executeHerdrSubagents({ action: "respond", runId: basename(continueRun), answer: "guess" }, undefined, undefined, { cwd: continueRoot }, { agentsDirectory: join(process.cwd(), "agents"), herdr: continueHerdr });
|
|
1021
|
+
check("respond requires its task number instead of selecting a task implicitly", unqualifiedResponse.isError && unqualifiedResponse.content[0].text.includes("task are required") && continueHerdr.calls.every(([name]) => name !== "promptAgent" || !name.includes("guess")));
|
|
1022
|
+
renameSync(join(continueTask, "session.jsonl"), join(continueTask, "session.jsonl.moved"));
|
|
1023
|
+
const withoutSession = await executeHerdrSubagents({ action: "respond", runId: basename(continueRun), task: 1, answer: "continue" }, undefined, undefined, { cwd: continueRoot }, { agentsDirectory: join(process.cwd(), "agents"), herdr: continueHerdr });
|
|
1024
|
+
check("a failed task without a saved session reports that it cannot continue", withoutSession.isError && withoutSession.content[0].text.includes("saved Pi session"));
|
|
1025
|
+
renameSync(join(continueTask, "session.jsonl.moved"), join(continueTask, "session.jsonl"));
|
|
1026
|
+
const continued = await executeHerdrSubagents({ action: "respond", runId: basename(continueRun), task: 1, answer: "try a different approach" }, undefined, undefined, { cwd: continueRoot, model: { provider: "fake", id: "model" } }, { agentsDirectory: join(process.cwd(), "agents"), herdr: continueHerdr });
|
|
1027
|
+
check("an explicitly selected failed task continues from its saved session", !continued.isError && readTaskJson(continueTask).status === "completed" && JSON.parse(readFileSync(join(continueRun, "run.json"), "utf8")).status === "completed");
|
|
1028
|
+
|
|
1029
|
+
const sharedTabRoot = join(root, "shared-tab-continuations");
|
|
1030
|
+
const sharedHerdr = fakeHerdr();
|
|
1031
|
+
await executeHerdrSubagents({ label: "both-fail", concurrency: 2, tasks: [
|
|
1032
|
+
{ name: "first", prompt: "FAIL first", agent: "explorer" },
|
|
1033
|
+
{ name: "second", prompt: "FAIL second", agent: "explorer" },
|
|
1034
|
+
] }, undefined, undefined, { cwd: sharedTabRoot, model: { provider: "fake", id: "model" } }, { agentsDirectory: join(process.cwd(), "agents"), herdr: sharedHerdr });
|
|
1035
|
+
const sharedRun = runsIn(sharedTabRoot);
|
|
1036
|
+
const sharedTasks = tasksIn(sharedRun);
|
|
1037
|
+
const sharedClosesAfterBatch = sharedHerdr.calls.filter(([name]) => name === "closeTab").length;
|
|
1038
|
+
const sharedSecondPane = readTaskJson(sharedTasks[1]).paneId;
|
|
1039
|
+
sharedHerdr.paneExists = async (paneId) => { sharedHerdr.calls.push(["paneExists", { paneId }]); return paneId !== sharedSecondPane; };
|
|
1040
|
+
let releaseSharedSecond;
|
|
1041
|
+
const sharedSecondGate = new Promise((resolve) => { releaseSharedSecond = resolve; });
|
|
1042
|
+
let reportSharedSecondGated;
|
|
1043
|
+
const sharedSecondGated = new Promise((resolve) => { reportSharedSecondGated = resolve; });
|
|
1044
|
+
const sharedRename = sharedHerdr.renamePane.bind(sharedHerdr);
|
|
1045
|
+
sharedHerdr.renamePane = async (paneId, label) => {
|
|
1046
|
+
if (label.startsWith("02 ")) { reportSharedSecondGated(); await sharedSecondGate; }
|
|
1047
|
+
return sharedRename(paneId, label);
|
|
1048
|
+
};
|
|
1049
|
+
const sharedSecondContinuation = executeHerdrSubagents({ action: "respond", runId: basename(sharedRun), task: 2, answer: "continue the second" }, undefined, undefined, { cwd: sharedTabRoot, model: { provider: "fake", id: "model" } }, { agentsDirectory: join(process.cwd(), "agents"), herdr: sharedHerdr });
|
|
1050
|
+
await sharedSecondGated;
|
|
1051
|
+
const sharedFirstContinuation = await executeHerdrSubagents({ action: "respond", runId: basename(sharedRun), task: 1, answer: "continue the first" }, undefined, undefined, { cwd: sharedTabRoot, model: { provider: "fake", id: "model" } }, { agentsDirectory: join(process.cwd(), "agents"), herdr: sharedHerdr });
|
|
1052
|
+
check("a finished continuation never closes a tab a sibling continuation is still claiming", !sharedFirstContinuation.isError && sharedFirstContinuation.details.status === "running" && sharedHerdr.calls.filter(([name]) => name === "closeTab").length === sharedClosesAfterBatch);
|
|
1053
|
+
releaseSharedSecond();
|
|
1054
|
+
const sharedSecondResult = await sharedSecondContinuation;
|
|
1055
|
+
check("the last continuation to settle closes the shared tab exactly once", !sharedSecondResult.isError && sharedSecondResult.details.status === "completed" && sharedHerdr.calls.filter(([name]) => name === "closeTab").length === sharedClosesAfterBatch + 1);
|
|
1056
|
+
|
|
1057
|
+
const queuedSiblingRoot = join(root, "queued-sibling-projection");
|
|
1058
|
+
const queuedSiblingHerdr = fakeHerdr();
|
|
1059
|
+
let releaseQueuedSibling;
|
|
1060
|
+
const queuedSiblingGate = new Promise((resolve) => { releaseQueuedSibling = resolve; });
|
|
1061
|
+
let reportQueuedSiblingGated;
|
|
1062
|
+
const queuedSiblingGated = new Promise((resolve) => { reportQueuedSiblingGated = resolve; });
|
|
1063
|
+
const queuedSiblingRename = queuedSiblingHerdr.renamePane.bind(queuedSiblingHerdr);
|
|
1064
|
+
queuedSiblingHerdr.renamePane = async (paneId, label) => {
|
|
1065
|
+
if (label.startsWith("02 ")) { reportQueuedSiblingGated(); await queuedSiblingGate; }
|
|
1066
|
+
return queuedSiblingRename(paneId, label);
|
|
1067
|
+
};
|
|
1068
|
+
const queuedSiblingBatch = executeHerdrSubagents({ label: "queued-sibling", concurrency: 1, tasks: [
|
|
1069
|
+
{ name: "fails", prompt: "FAIL immediately", agent: "explorer" },
|
|
1070
|
+
{ name: "next", prompt: "second task", agent: "explorer" },
|
|
1071
|
+
] }, undefined, undefined, { cwd: queuedSiblingRoot, model: { provider: "fake", id: "model" } }, { agentsDirectory: join(process.cwd(), "agents"), herdr: queuedSiblingHerdr });
|
|
1072
|
+
await queuedSiblingGated;
|
|
1073
|
+
const queuedSiblingRun = runsIn(queuedSiblingRoot);
|
|
1074
|
+
const queuedSiblingTasks = tasksIn(queuedSiblingRun);
|
|
1075
|
+
check("a batch task is still queued while its pane is being prepared", readTaskJson(queuedSiblingTasks[0]).status === "failed" && readTaskJson(queuedSiblingTasks[1]).status === "queued");
|
|
1076
|
+
const duringQueuedSibling = await executeHerdrSubagents({ action: "respond", runId: basename(queuedSiblingRun), task: 1, answer: "retry now" }, undefined, undefined, { cwd: queuedSiblingRoot, model: { provider: "fake", id: "model" } }, { agentsDirectory: join(process.cwd(), "agents"), herdr: queuedSiblingHerdr });
|
|
1077
|
+
check("a queued sibling is reported as running work, never as a fabricated input request", !duringQueuedSibling.isError && duringQueuedSibling.details.status === "running" && !duringQueuedSibling.details.question && !duringQueuedSibling.content[0].text.includes("needs input"));
|
|
1078
|
+
releaseQueuedSibling();
|
|
1079
|
+
await queuedSiblingBatch;
|
|
1080
|
+
|
|
1081
|
+
const revivePaneRoot = join(root, "continue-dead-pane");
|
|
1082
|
+
const revivePaneContinueHerdr = fakeHerdr();
|
|
1083
|
+
await executeHerdrSubagents({ label: "dead-pane", tasks: [{ name: "gone", prompt: "FAIL alone", agent: "explorer" }] }, undefined, undefined, { cwd: revivePaneRoot, model: { provider: "fake", id: "model" } }, { agentsDirectory: join(process.cwd(), "agents"), herdr: revivePaneContinueHerdr });
|
|
1084
|
+
const revivePaneRun = runsIn(revivePaneRoot);
|
|
1085
|
+
const revivePaneTask = tasksIn(revivePaneRun)[0];
|
|
1086
|
+
const revivePaneAttempt = Number(readTaskJson(revivePaneTask).attempt ?? 1);
|
|
1087
|
+
const revivePaneTabs = revivePaneContinueHerdr.calls.filter(([name]) => name === "createTab").length;
|
|
1088
|
+
revivePaneContinueHerdr.paneExists = async (paneId) => { revivePaneContinueHerdr.calls.push(["paneExists", { paneId }]); return false; };
|
|
1089
|
+
const revivePaneContinued = await executeHerdrSubagents({ action: "respond", runId: basename(revivePaneRun), task: 1, answer: "restart from the saved session" }, undefined, undefined, { cwd: revivePaneRoot, model: { provider: "fake", id: "model" } }, { agentsDirectory: join(process.cwd(), "agents"), herdr: revivePaneContinueHerdr });
|
|
1090
|
+
check("a failed task whose pane is gone restarts from its saved session in a new pane", !revivePaneContinued.isError && readTaskJson(revivePaneTask).status === "completed" && Number(readTaskJson(revivePaneTask).attempt) === revivePaneAttempt + 1 && revivePaneContinueHerdr.calls.filter(([name]) => name === "createTab").length === revivePaneTabs + 1);
|
|
1091
|
+
|
|
1092
|
+
const reblockRoot = join(root, "continue-then-block");
|
|
1093
|
+
const reblockHerdr = fakeHerdr();
|
|
1094
|
+
await executeHerdrSubagents({ label: "reblock", tasks: [{ name: "reblock", prompt: "FAIL alone", agent: "explorer" }] }, undefined, undefined, { cwd: reblockRoot, model: { provider: "fake", id: "model" } }, { agentsDirectory: join(process.cwd(), "agents"), herdr: reblockHerdr });
|
|
1095
|
+
const reblockRun = runsIn(reblockRoot);
|
|
1096
|
+
const reblockTask = tasksIn(reblockRun)[0];
|
|
1097
|
+
const reblockPromptAgent = reblockHerdr.promptAgent.bind(reblockHerdr);
|
|
1098
|
+
reblockHerdr.promptAgent = async (request) => {
|
|
1099
|
+
if (!request.prompt.includes("ASK AGAIN")) return reblockPromptAgent(request);
|
|
1100
|
+
reblockHerdr.calls.push(["promptAgent", request]);
|
|
1101
|
+
const attemptId = readTaskJson(reblockTask).currentAttemptId;
|
|
1102
|
+
mkdirSync(join(reblockTask, "reports"), { recursive: true });
|
|
1103
|
+
writeFileSync(join(reblockTask, "reports", `${attemptId}.json`), JSON.stringify({ attemptId, status: "blocked", summary: "Still missing a detail.", question: "Which account?", documents: [] }));
|
|
1104
|
+
return { status: "idle" };
|
|
1105
|
+
};
|
|
1106
|
+
const reblocked = await executeHerdrSubagents({ action: "respond", runId: basename(reblockRun), task: 1, answer: "ASK AGAIN please" }, undefined, undefined, { cwd: reblockRoot, model: { provider: "fake", id: "model" } }, { agentsDirectory: join(process.cwd(), "agents"), herdr: reblockHerdr });
|
|
1107
|
+
check("a continued failed task may request input and becomes a normal blocked task", !reblocked.isError && reblocked.details.status === "blocked" && reblocked.details.question === "Which account?" && readTaskJson(reblockTask).status === "blocked");
|
|
1108
|
+
const reblockAnswer = await executeHerdrSubagents({ action: "respond", runId: basename(reblockRun), task: 1, answer: "the main account" }, undefined, undefined, { cwd: reblockRoot, model: { provider: "fake", id: "model" } }, { agentsDirectory: join(process.cwd(), "agents"), herdr: reblockHerdr });
|
|
1109
|
+
check("a task that blocked during its continuation is answerable like any blocked task", !reblockAnswer.isError && readTaskJson(reblockTask).status === "completed");
|
|
1110
|
+
|
|
1111
|
+
const rejectRoot = join(root, "continue-reject-matrix");
|
|
1112
|
+
const rejectRunDirectory = join(rejectRoot, ".pi", "herdr-subagents", "runs", "reject");
|
|
1113
|
+
mkdirSync(join(rejectRunDirectory, "tasks"), { recursive: true });
|
|
1114
|
+
writeFileSync(join(rejectRunDirectory, "run.json"), JSON.stringify({ id: "reject", label: "reject", status: "running", startedAt: new Date().toISOString() }));
|
|
1115
|
+
const rejectedStatuses = ["completed", "cancelled", "queued", "running"];
|
|
1116
|
+
rejectedStatuses.forEach((status, index) => {
|
|
1117
|
+
const directory = join(rejectRunDirectory, "tasks", `0${index + 1}-task`);
|
|
1118
|
+
mkdirSync(directory, { recursive: true });
|
|
1119
|
+
const sessionFile = join(directory, "session.jsonl");
|
|
1120
|
+
writeFileSync(sessionFile, "{}\n");
|
|
1121
|
+
writeFileSync(join(directory, "task.json"), JSON.stringify({ id: `reject-${index + 1}`, runId: "reject", order: index + 1, name: status, agent: "explorer", status, paneId: "w1:p9", sessionFile }));
|
|
1122
|
+
});
|
|
1123
|
+
const rejectHerdr = fakeHerdr();
|
|
1124
|
+
const rejections = [];
|
|
1125
|
+
for (const order of [1, 2, 3, 4, 9]) {
|
|
1126
|
+
rejections.push(await executeHerdrSubagents({ action: "respond", runId: "reject", task: order, answer: "continue" }, undefined, undefined, { cwd: rejectRoot }, { agentsDirectory: join(process.cwd(), "agents"), herdr: rejectHerdr }));
|
|
1127
|
+
}
|
|
1128
|
+
check("respond refuses every task state other than blocked or failed, without side effects", rejections.every((result) => result.isError && result.content[0].text.includes("neither blocked nor a failed task")) && rejectHerdr.calls.length === 0 && JSON.parse(readFileSync(join(rejectRunDirectory, "run.json"), "utf8")).status === "running");
|
|
1129
|
+
|
|
765
1130
|
const twiceBlockedHerdr = fakeHerdr("blocked-twice");
|
|
766
1131
|
const twiceBlocked = await executeHerdrSubagents({ concurrency: 1, tasks: [{ name: "first-question", prompt: "first", agent: "explorer" }, { name: "second-question", prompt: "second", agent: "reviewer" }] }, undefined, undefined, { cwd: root, model: { provider: "fake", id: "model" } }, { agentsDirectory: join(process.cwd(), "agents"), herdr: twiceBlockedHerdr });
|
|
767
|
-
const secondQuestion = await executeHerdrSubagents({ action: "respond", runId: twiceBlocked.details.runId, answer: "main" }, undefined, undefined, { cwd: root }, { agentsDirectory: join(process.cwd(), "agents"), herdr: twiceBlockedHerdr });
|
|
768
|
-
check("queued sibling blocking returns its current question and Fleet pane", secondQuestion.details.status === "blocked" && secondQuestion.details.question === "Which environment should I inspect?" && secondQuestion.content[0].text.includes(
|
|
1132
|
+
const secondQuestion = await executeHerdrSubagents({ action: "respond", runId: twiceBlocked.details.runId, task: 1, answer: "main" }, undefined, undefined, { cwd: root }, { agentsDirectory: join(process.cwd(), "agents"), herdr: twiceBlockedHerdr });
|
|
1133
|
+
check("queued sibling blocking returns its current question, actions, and Fleet pane", secondQuestion.details.status === "blocked" && secondQuestion.details.question === "Which environment should I inspect?" && secondQuestion.details.allowedActions?.join(",") === "respond,cancel" && secondQuestion.content[0].text.includes(secondQuestion.details.runId) && secondQuestion.details.activity[1].status === "blocked" && Boolean(secondQuestion.details.activity[1].paneId));
|
|
769
1134
|
const failedAfterBlockHerdr = fakeHerdr("blocked");
|
|
770
1135
|
const failedAfterBlock = await executeHerdrSubagents({ concurrency: 1, tasks: [{ name: "question-first", prompt: "question", agent: "explorer" }, { name: "fails-later", prompt: "FAIL sibling", agent: "reviewer" }] }, undefined, undefined, { cwd: root, model: { provider: "fake", id: "model" } }, { agentsDirectory: join(process.cwd(), "agents"), herdr: failedAfterBlockHerdr });
|
|
771
|
-
const failedAnswer = await executeHerdrSubagents({ action: "respond", runId: failedAfterBlock.details.runId, answer: "main" }, undefined, undefined, { cwd: root }, { agentsDirectory: join(process.cwd(), "agents"), herdr: failedAfterBlockHerdr });
|
|
1136
|
+
const failedAnswer = await executeHerdrSubagents({ action: "respond", runId: failedAfterBlock.details.runId, task: 1, answer: "main" }, undefined, undefined, { cwd: root }, { agentsDirectory: join(process.cwd(), "agents"), herdr: failedAfterBlockHerdr });
|
|
772
1137
|
check("failed sibling after answer appears in compact result", failedAnswer.isError && failedAnswer.details.status === "partial" && failedAnswer.content[0].text.includes("fails-later") && failedAnswer.content[0].text.includes("failed"));
|
|
773
1138
|
const responseFailureHerdr = fakeHerdr("blocked-then-post-fail");
|
|
774
1139
|
const responseFailureRun = await executeHerdrSubagents({ tasks: [{ name: "response-fails", prompt: "question", agent: "explorer" }] }, undefined, undefined, { cwd: root }, { agentsDirectory: join(process.cwd(), "agents"), herdr: responseFailureHerdr });
|
|
775
|
-
const responseFailure = await executeHerdrSubagents({ action: "respond", runId: responseFailureRun.details.runId, answer: "main" }, undefined, undefined, { cwd: root }, { agentsDirectory: join(process.cwd(), "agents"), herdr: responseFailureHerdr });
|
|
1140
|
+
const responseFailure = await executeHerdrSubagents({ action: "respond", runId: responseFailureRun.details.runId, task: 1, answer: "main" }, undefined, undefined, { cwd: root }, { agentsDirectory: join(process.cwd(), "agents"), herdr: responseFailureHerdr });
|
|
776
1141
|
const responseFailureTask = JSON.parse(readFileSync(join(responseFailureRun.details.recordDirectory, "tasks", readdirSync(join(responseFailureRun.details.recordDirectory, "tasks"))[0], "task.json"), "utf8"));
|
|
777
1142
|
check("failed blocked response settles durable task instead of leaving running", responseFailure.isError && responseFailure.details.status === "failed" && responseFailureTask.status === "failed" && responseFailureHerdr.calls.at(-1)[0] === "closeTab");
|
|
778
1143
|
const siblingFailureHerdr = fakeHerdr("blocked-then-post-fail");
|
|
779
1144
|
const siblingFailureRun = await executeHerdrSubagents({ concurrency: 1, tasks: [{ name: "response-fails", prompt: "question", agent: "explorer" }, { name: "queued-sibling", prompt: "later", agent: "reviewer" }] }, undefined, undefined, { cwd: root }, { agentsDirectory: join(process.cwd(), "agents"), herdr: siblingFailureHerdr });
|
|
780
|
-
await executeHerdrSubagents({ action: "respond", runId: siblingFailureRun.details.runId, answer: "main" }, undefined, undefined, { cwd: root }, { agentsDirectory: join(process.cwd(), "agents"), herdr: siblingFailureHerdr });
|
|
1145
|
+
await executeHerdrSubagents({ action: "respond", runId: siblingFailureRun.details.runId, task: 1, answer: "main" }, undefined, undefined, { cwd: root }, { agentsDirectory: join(process.cwd(), "agents"), herdr: siblingFailureHerdr });
|
|
781
1146
|
const siblingFailureTasks = readdirSync(join(siblingFailureRun.details.recordDirectory, "tasks")).map((entry) => JSON.parse(readFileSync(join(siblingFailureRun.details.recordDirectory, "tasks", entry, "task.json"), "utf8")));
|
|
782
1147
|
check("failed blocked response releases its slot and continues queued siblings", siblingFailureTasks.every((task) => ["failed", "completed"].includes(task.status)) && siblingFailureTasks.every((task) => task.status !== "cancelled") && siblingFailureHerdr.calls.filter(([name]) => name === "promptAgent").length === 3);
|
|
783
1148
|
const abortedContinuationHerdr = fakeHerdr("blocked");
|
|
@@ -796,7 +1161,7 @@ try {
|
|
|
796
1161
|
const deadPaneHerdr = fakeHerdr("blocked");
|
|
797
1162
|
const deadPaneRun = await executeHerdrSubagents({ tasks: [{ name: "dead-pane", prompt: "question", agent: "explorer" }] }, undefined, undefined, { cwd: root }, { agentsDirectory: join(process.cwd(), "agents"), herdr: deadPaneHerdr });
|
|
798
1163
|
deadPaneHerdr.paneExists = async () => false;
|
|
799
|
-
const deadPaneResume = await executeHerdrSubagents({ action: "respond", runId: deadPaneRun.details.runId, answer: "main" }, undefined, undefined, { cwd: root }, { agentsDirectory: join(process.cwd(), "agents"), herdr: deadPaneHerdr });
|
|
1164
|
+
const deadPaneResume = await executeHerdrSubagents({ action: "respond", runId: deadPaneRun.details.runId, task: 1, answer: "main" }, undefined, undefined, { cwd: root }, { agentsDirectory: join(process.cwd(), "agents"), herdr: deadPaneHerdr });
|
|
800
1165
|
const deadPaneTaskDirectory = join(deadPaneRun.details.recordDirectory, "tasks", readdirSync(join(deadPaneRun.details.recordDirectory, "tasks"))[0]);
|
|
801
1166
|
const deadPaneTask = JSON.parse(readFileSync(join(deadPaneTaskDirectory, "task.json"), "utf8"));
|
|
802
1167
|
check("dead blocked pane recovers a coherent attempt with its persona prompt", !deadPaneResume.isError && deadPaneHerdr.calls.filter(([name]) => name === "createTab").length === 2 && deadPaneTask.tabId === "w1:t2" && deadPaneTask.attempt === 2 && deadPaneTask.currentTurnFile.endsWith("02.json") && existsSync(deadPaneTask.currentTurnFile) && readFileSync(join(deadPaneTaskDirectory, "resume-system-prompt.md"), "utf8").includes("Investigate the assigned question without modifying files"));
|
|
@@ -806,7 +1171,7 @@ try {
|
|
|
806
1171
|
staleTabHerdr.createTab = async (request) => { staleTabHerdr.calls.push(["createTab", request]); return { tabId: "w1:t-new", paneId: "w1:p-new" }; };
|
|
807
1172
|
staleTabHerdr.closeTab = async (tabId) => { staleTabHerdr.calls.push(["closeTab", { tabId }]); if (tabId === "w1:t2") throw new Error("stale tab still busy"); };
|
|
808
1173
|
const staleTabUpdates = [];
|
|
809
|
-
const staleTabResume = await executeHerdrSubagents({ action: "respond", runId: staleTabRun.details.runId, answer: "main" }, undefined, (update) => staleTabUpdates.push(update), { cwd: root }, { agentsDirectory: join(process.cwd(), "agents"), herdr: staleTabHerdr });
|
|
1174
|
+
const staleTabResume = await executeHerdrSubagents({ action: "respond", runId: staleTabRun.details.runId, task: 1, answer: "main" }, undefined, (update) => staleTabUpdates.push(update), { cwd: root }, { agentsDirectory: join(process.cwd(), "agents"), herdr: staleTabHerdr });
|
|
810
1175
|
const staleTabRecord = JSON.parse(readFileSync(join(staleTabRun.details.recordDirectory, "run.json"), "utf8"));
|
|
811
1176
|
check("dead-pane migration persists and surfaces stale tab cleanup failure", !staleTabResume.isError && staleTabRecord.cleanupError === "stale tab still busy" && staleTabRecord.staleTabIds?.includes("w1:t2") && staleTabUpdates.some((update) => update.content[0].text.includes("Could not close stale Herdr tab")));
|
|
812
1177
|
const invalidTurnHerdr = fakeHerdr("blocked");
|
|
@@ -820,12 +1185,12 @@ try {
|
|
|
820
1185
|
invalidTurnTask.currentTurnFile = invalidTurnFile;
|
|
821
1186
|
writeFileSync(invalidTurnTaskFile, JSON.stringify(invalidTurnTask));
|
|
822
1187
|
const invalidTurnUpdates = [];
|
|
823
|
-
await executeHerdrSubagents({ action: "respond", runId: invalidTurnRun.details.runId, answer: "main" }, undefined, (update) => invalidTurnUpdates.push(update), { cwd: root }, { agentsDirectory: join(process.cwd(), "agents"), herdr: invalidTurnHerdr });
|
|
1188
|
+
await executeHerdrSubagents({ action: "respond", runId: invalidTurnRun.details.runId, task: 1, answer: "main" }, undefined, (update) => invalidTurnUpdates.push(update), { cwd: root }, { agentsDirectory: join(process.cwd(), "agents"), herdr: invalidTurnHerdr });
|
|
824
1189
|
check("invalid durable turn is archived and surfaced before recovery", readdirSync(join(invalidTurnTaskDirectory, "turns")).some((name) => name.startsWith("01.json.invalid-")) && invalidTurnUpdates.some((update) => update.content[0].text.includes("Archived invalid durable turn")));
|
|
825
1190
|
const corruptLookupRoot = join(root, "corrupt-run-lookup");
|
|
826
1191
|
mkdirSync(join(corruptLookupRoot, ".pi", "herdr-subagents", "runs", "bad"), { recursive: true });
|
|
827
1192
|
writeFileSync(join(corruptLookupRoot, ".pi", "herdr-subagents", "runs", "bad", "run.json"), "{truncated");
|
|
828
|
-
const corruptLookup = await executeHerdrSubagents({ action: "respond", runId: "missing", answer: "main" }, undefined, undefined, { cwd: corruptLookupRoot }, { agentsDirectory: join(process.cwd(), "agents"), herdr: fakeHerdr() });
|
|
1193
|
+
const corruptLookup = await executeHerdrSubagents({ action: "respond", runId: "missing", task: 1, answer: "main" }, undefined, undefined, { cwd: corruptLookupRoot }, { agentsDirectory: join(process.cwd(), "agents"), herdr: fakeHerdr() });
|
|
829
1194
|
check("blocked response surfaces unreadable run records instead of reporting unknown", corruptLookup.isError && corruptLookup.content[0].text.includes("Could not inspect Subagent runs"));
|
|
830
1195
|
const survivingPaneRunFile = join(root, "surviving-pane-run.json");
|
|
831
1196
|
const survivingPaneRun = { id: "surviving-pane", label: "surviving-pane", status: "blocked", cwd: root, tabId: "w1:t-surviving", paneIds: ["w1:p-live", "w1:p-dead"] };
|
|
@@ -865,15 +1230,75 @@ try {
|
|
|
865
1230
|
check("history lists compact project-local runs and tasks", Boolean(stableHistory?.tasks.length) && historyText(history).includes("stable") && !historyText(history).includes("FULL stable"));
|
|
866
1231
|
const historyHerdr = fakeHerdr();
|
|
867
1232
|
const historicalUpdates = [];
|
|
868
|
-
const historicalResume = await executeHerdrSubagents({ action: "resume", runId: backgroundId, task: 1, prompt: "follow up" }, undefined, (update) => historicalUpdates.push(update.details), { cwd: root, model: { provider: "fake", id: "model" } }, { agentsDirectory: join(process.cwd(), "agents"), herdr: historyHerdr });
|
|
1233
|
+
const historicalResume = await executeHerdrSubagents({ action: "resume", runId: backgroundId.slice(0, 8), task: 1, prompt: "follow up" }, undefined, (update) => historicalUpdates.push(update.details), { cwd: root, model: { provider: "fake", id: "model" } }, { agentsDirectory: join(process.cwd(), "agents"), herdr: historyHerdr });
|
|
869
1234
|
const historyStartRequest = historyHerdr.calls.find(([name]) => name === "startAgent")[1];
|
|
870
1235
|
check("historical resume preserves the saved Pi session and persona prompt", !historicalResume.isError && historyHerdr.calls[0][0] === "createTab" && historyStartRequest.args.includes(stableHistory.tasks[0].sessionFile) && historyStartRequest.args.includes(stableHistory.tasks[0].model) && historyStartRequest.args.some((value) => String(value).includes("subagent_report")) && existsSync(join(stableHistory.tasks[0].recordDirectory, "turns", "02.json")) && readFileSync(join(stableHistory.tasks[0].recordDirectory, "followup-system-prompt.md"), "utf8").includes("Investigate the assigned question without modifying files"));
|
|
871
1236
|
check("historical resume streams authoritative Fleet activity", historicalUpdates.some((update) => update.status === "running" && update.activity?.[0]?.agent === "explorer" && update.activity[0].sessionFile === stableHistory.tasks[0].sessionFile));
|
|
872
1237
|
const historicalTaskRecord = JSON.parse(readFileSync(join(stableHistory.tasks[0].recordDirectory, "task.json"), "utf8"));
|
|
873
1238
|
const historicalTurnRecord = JSON.parse(readFileSync(join(stableHistory.tasks[0].recordDirectory, "turns", "02.json"), "utf8"));
|
|
874
1239
|
check("historical task and turn retain their tab identity", historicalTaskRecord.tabId === historicalResume.details.tabId && historicalTurnRecord.tabId === historicalResume.details.tabId);
|
|
1240
|
+
const lockPublicationDirectory = join(root, "lock-publication", ".state.lock");
|
|
1241
|
+
mkdirSync(join(root, "lock-publication"), { recursive: true });
|
|
1242
|
+
let publishEntered;
|
|
1243
|
+
const publicationEntered = new Promise((resolve) => { publishEntered = resolve; });
|
|
1244
|
+
let releasePublication;
|
|
1245
|
+
const publicationGate = new Promise((resolve) => { releasePublication = resolve; });
|
|
1246
|
+
let firstLockAcquired = false;
|
|
1247
|
+
const firstLockPromise = acquireDirectoryLock(lockPublicationDirectory, { wait: true, beforePublish: async () => { publishEntered(); await publicationGate; } }).then((lock) => { firstLockAcquired = true; return lock; });
|
|
1248
|
+
await publicationEntered;
|
|
1249
|
+
const secondPublishedLock = await acquireDirectoryLock(lockPublicationDirectory, { wait: false });
|
|
1250
|
+
releasePublication();
|
|
1251
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
1252
|
+
const publicationWasExclusive = !firstLockAcquired;
|
|
1253
|
+
await secondPublishedLock.release();
|
|
1254
|
+
const firstPublishedLock = await firstLockPromise;
|
|
1255
|
+
await firstPublishedLock.release();
|
|
1256
|
+
check("lock owner publication gate cannot produce two owners", publicationWasExclusive && !existsSync(lockPublicationDirectory));
|
|
1257
|
+
|
|
1258
|
+
const allocationCrashRoot = join(root, "allocation-crash-recovery");
|
|
1259
|
+
const allocationCrashRun = join(allocationCrashRoot, ".pi", "herdr-subagents", "runs", "run");
|
|
1260
|
+
const allocationCrashTask = join(allocationCrashRun, "tasks", "01-task");
|
|
1261
|
+
mkdirSync(allocationCrashTask, { recursive: true });
|
|
1262
|
+
writeFileSync(join(allocationCrashRun, "run.json"), JSON.stringify({ id: "allocation-crash", label: "allocation-crash", status: "running", tabId: "w1:t-allocation", resume: { generation: 1, phase: "active", tabId: "w1:t-allocation", paneIds: ["w1:p-allocation"], active: { task: { paneId: "w1:p-allocation", generation: 1, agentName: "agent", attemptId: "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa", ownerProcessId: 99999999, ownerToken: "dead", phase: "allocating" } } } }));
|
|
1263
|
+
writeFileSync(join(allocationCrashTask, "task.json"), JSON.stringify({ id: "task", runId: "allocation-crash", order: 1, name: "task", status: "completed", sessionFile: join(allocationCrashTask, "session.jsonl") }));
|
|
1264
|
+
const allocationCrashHerdr = fakeHerdr();
|
|
1265
|
+
await reconcileSubagentRuns(allocationCrashRoot, allocationCrashHerdr);
|
|
1266
|
+
const recoveredAllocationRun = JSON.parse(readFileSync(join(allocationCrashRun, "run.json"), "utf8"));
|
|
1267
|
+
const recoveredAllocationTask = JSON.parse(readFileSync(join(allocationCrashTask, "task.json"), "utf8"));
|
|
1268
|
+
check("allocating member crash window is failed and claims final tab cleanup", recoveredAllocationTask.status === "cancelled" && recoveredAllocationTask.currentAttemptId === "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa" && !recoveredAllocationRun.resume && allocationCrashHerdr.calls.some(([name]) => name === "closeTab"));
|
|
1269
|
+
|
|
1270
|
+
const staleReportedRoot = join(root, "stale-reported-resume-member");
|
|
1271
|
+
const staleReportedRun = join(staleReportedRoot, ".pi", "herdr-subagents", "runs", "run");
|
|
1272
|
+
const staleReportedTask = join(staleReportedRun, "tasks", "01-task");
|
|
1273
|
+
const staleReportedAttempt = "cccccccc-cccc-4ccc-8ccc-cccccccccccc";
|
|
1274
|
+
mkdirSync(join(staleReportedTask, "reports"), { recursive: true });
|
|
1275
|
+
writeFileSync(join(staleReportedRun, "run.json"), JSON.stringify({ id: "stale-reported", label: "stale-reported", status: "running", tabId: "w1:t-stale-reported", resume: { generation: 1, phase: "active", tabId: "w1:t-stale-reported", paneIds: ["w1:p-stale-reported"], active: { task: { paneId: "w1:p-stale-reported", generation: 1, agentName: "agent", attemptId: staleReportedAttempt, ownerSessionId: "dead-owner", ownerProcessId: 99999999, ownerToken: "dead", phase: "running" } } } }));
|
|
1276
|
+
writeFileSync(join(staleReportedTask, "task.json"), JSON.stringify({ id: "task", runId: "stale-reported", order: 1, name: "task", status: "running", currentAttemptId: staleReportedAttempt, sessionFile: join(staleReportedTask, "session.jsonl") }));
|
|
1277
|
+
const staleReportedPayload = { attemptId: staleReportedAttempt, status: "completed", summary: "Completed before the resume owner disappeared.", documents: [] };
|
|
1278
|
+
writeFileSync(join(staleReportedTask, "reports", `${staleReportedAttempt}.json`), JSON.stringify(staleReportedPayload));
|
|
1279
|
+
const staleReportedReconciliation = await reconcileSubagentRuns(staleReportedRoot, fakeHerdr(), { sessionId: "new-owner", processId: process.pid });
|
|
1280
|
+
const staleReportedTaskRecord = JSON.parse(readFileSync(join(staleReportedTask, "task.json"), "utf8"));
|
|
1281
|
+
const staleReportedCanonical = JSON.parse(readFileSync(join(staleReportedTask, "report.json"), "utf8"));
|
|
1282
|
+
check("stale historical resume member adopts its completed scoped report without cancellation", staleReportedReconciliation.settledTasks === 1 && staleReportedReconciliation.cancelledTasks === 0 && staleReportedTaskRecord.status === "completed" && staleReportedCanonical.status === "completed" && staleReportedCanonical.attemptId === staleReportedAttempt && staleReportedCanonical.summary === staleReportedPayload.summary);
|
|
1283
|
+
|
|
1284
|
+
const ownerMembersRoot = join(root, "owner-member-cancel");
|
|
1285
|
+
const ownerMembersRun = join(ownerMembersRoot, ".pi", "herdr-subagents", "runs", "run");
|
|
1286
|
+
mkdirSync(join(ownerMembersRun, "tasks", "01-a"), { recursive: true });
|
|
1287
|
+
mkdirSync(join(ownerMembersRun, "tasks", "02-b"), { recursive: true });
|
|
1288
|
+
const ownerMembers = {
|
|
1289
|
+
a: { paneId: "w1:p-a", generation: 1, agentName: "a", attemptId: "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaa1", ownerSessionId: "owner-a", ownerProcessId: process.pid, ownerToken: "a", phase: "running" },
|
|
1290
|
+
b: { paneId: "w1:p-b", generation: 1, agentName: "b", attemptId: "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbb2", ownerSessionId: "owner-b", ownerProcessId: process.pid, ownerToken: "b", phase: "running" },
|
|
1291
|
+
};
|
|
1292
|
+
writeFileSync(join(ownerMembersRun, "run.json"), JSON.stringify({ id: "owner-members", label: "owner-members", status: "running", tabId: "w1:t-members", resume: { generation: 1, phase: "active", tabId: "w1:t-members", paneIds: ["w1:p-a", "w1:p-b"], active: ownerMembers } }));
|
|
1293
|
+
writeFileSync(join(ownerMembersRun, "tasks", "01-a", "task.json"), JSON.stringify({ id: "a", runId: "owner-members", order: 1, status: "running", currentAttemptId: ownerMembers.a.attemptId, sessionFile: join(ownerMembersRun, "tasks", "01-a", "session.jsonl") }));
|
|
1294
|
+
writeFileSync(join(ownerMembersRun, "tasks", "02-b", "task.json"), JSON.stringify({ id: "b", runId: "owner-members", order: 2, status: "running", currentAttemptId: ownerMembers.b.attemptId, sessionFile: join(ownerMembersRun, "tasks", "02-b", "session.jsonl") }));
|
|
1295
|
+
const ownerMembersHerdr = fakeHerdr();
|
|
1296
|
+
await cancelActiveSubagentRuns(ownerMembersRoot, ownerMembersHerdr, { owner: { sessionId: "owner-a", processId: process.pid } });
|
|
1297
|
+
const ownerMembersAfter = JSON.parse(readFileSync(join(ownerMembersRun, "run.json"), "utf8"));
|
|
1298
|
+
check("member cancellation cannot cancel a sibling owner or close its shared tab", !ownerMembersAfter.resume.active.a && ownerMembersAfter.resume.active.b && JSON.parse(readFileSync(join(ownerMembersRun, "tasks", "02-b", "task.json"), "utf8")).status === "running" && !ownerMembersHerdr.calls.some(([name]) => name === "closeTab"));
|
|
1299
|
+
|
|
875
1300
|
const resumeRaceRoot = join(root, "resume-race");
|
|
876
|
-
const resumeRaceSeed = await
|
|
1301
|
+
const resumeRaceSeed = await runSingleTask("original", { cwd: resumeRaceRoot, herdr: fakeHerdr() });
|
|
877
1302
|
const resumeRaceHerdr = fakeHerdr();
|
|
878
1303
|
const createRaceTab = resumeRaceHerdr.createTab.bind(resumeRaceHerdr);
|
|
879
1304
|
let beginFirstCreate;
|
|
@@ -892,7 +1317,90 @@ try {
|
|
|
892
1317
|
releaseFirstCreate();
|
|
893
1318
|
await firstResume;
|
|
894
1319
|
check("concurrent historical resume cannot start a second Pi session writer", competingResume.isError && raceCreates === 1 && resumeRaceHerdr.calls.filter(([name]) => name === "startAgent").length === 1);
|
|
895
|
-
|
|
1320
|
+
const resumeRaceTaskDirectory = (await listSubagentHistory(resumeRaceRoot)).find((run) => run.id === resumeRaceSeed.runId).tasks[0].recordDirectory;
|
|
1321
|
+
check("historical resume releases the task lock after settlement", !existsSync(join(resumeRaceTaskDirectory, ".resume.lock")));
|
|
1322
|
+
const staleTaskLock = join(resumeRaceTaskDirectory, ".resume.lock");
|
|
1323
|
+
mkdirSync(staleTaskLock);
|
|
1324
|
+
writeFileSync(join(staleTaskLock, "owner.json"), JSON.stringify({ token: "dead-owner", processId: 99999999, createdAt: "2000-01-01T00:00:00.000Z" }));
|
|
1325
|
+
const recoveredTaskResume = await executeHerdrSubagents({ action: "resume", runId: resumeRaceSeed.runId, task: 1, prompt: "recover stale task lock" }, undefined, undefined, { cwd: resumeRaceRoot }, { agentsDirectory: join(process.cwd(), "agents"), herdr: fakeHerdr() });
|
|
1326
|
+
check("historical resume automatically reclaims a dead-PID task lock", !recoveredTaskResume.isError && !existsSync(staleTaskLock));
|
|
1327
|
+
|
|
1328
|
+
const parallelResumeRoot = join(root, "parallel-historical-resume");
|
|
1329
|
+
const parallelSeed = (await executeHerdrSubagents({ tasks: [{ name: "one", prompt: "one", agent: "explorer" }, { name: "two", prompt: "two", agent: "reviewer" }], concurrency: 2 }, undefined, undefined, { cwd: parallelResumeRoot }, { agentsDirectory: join(process.cwd(), "agents"), herdr: fakeHerdr() })).details;
|
|
1330
|
+
const parallelHerdr = fakeHerdr();
|
|
1331
|
+
const parallelPrompt = parallelHerdr.promptAgent.bind(parallelHerdr);
|
|
1332
|
+
const parallelRename = parallelHerdr.renamePane.bind(parallelHerdr);
|
|
1333
|
+
let secondAllocationEntered;
|
|
1334
|
+
const secondAllocation = new Promise((resolve) => { secondAllocationEntered = resolve; });
|
|
1335
|
+
let releaseSecondAllocation;
|
|
1336
|
+
const secondAllocationGate = new Promise((resolve) => { releaseSecondAllocation = resolve; });
|
|
1337
|
+
let parallelRenames = 0;
|
|
1338
|
+
parallelHerdr.renamePane = async (paneId, label) => {
|
|
1339
|
+
parallelRenames += 1;
|
|
1340
|
+
if (parallelRenames === 2) { secondAllocationEntered(); await secondAllocationGate; }
|
|
1341
|
+
return parallelRename(paneId, label);
|
|
1342
|
+
};
|
|
1343
|
+
let firstParallelEntered;
|
|
1344
|
+
let secondParallelEntered;
|
|
1345
|
+
const firstEntered = new Promise((resolve) => { firstParallelEntered = resolve; });
|
|
1346
|
+
const secondEntered = new Promise((resolve) => { secondParallelEntered = resolve; });
|
|
1347
|
+
let releaseFirstParallel;
|
|
1348
|
+
let releaseSecondParallel;
|
|
1349
|
+
const firstGate = new Promise((resolve) => { releaseFirstParallel = resolve; });
|
|
1350
|
+
const secondGate = new Promise((resolve) => { releaseSecondParallel = resolve; });
|
|
1351
|
+
let parallelPrompts = 0;
|
|
1352
|
+
parallelHerdr.promptAgent = async (request) => {
|
|
1353
|
+
parallelPrompts += 1;
|
|
1354
|
+
if (parallelPrompts === 1) { firstParallelEntered(); await firstGate; }
|
|
1355
|
+
else { secondParallelEntered(); await secondGate; }
|
|
1356
|
+
return parallelPrompt(request);
|
|
1357
|
+
};
|
|
1358
|
+
const parallelFirst = executeHerdrSubagents({ action: "resume", runId: parallelSeed.runId, task: 1, prompt: "resume one" }, undefined, undefined, { cwd: parallelResumeRoot }, { agentsDirectory: join(process.cwd(), "agents"), herdr: parallelHerdr });
|
|
1359
|
+
await firstEntered;
|
|
1360
|
+
const parallelSecond = executeHerdrSubagents({ action: "resume", runId: parallelSeed.runId, task: 2, prompt: "resume two" }, undefined, undefined, { cwd: parallelResumeRoot }, { agentsDirectory: join(process.cwd(), "agents"), herdr: parallelHerdr });
|
|
1361
|
+
await secondAllocation;
|
|
1362
|
+
const activeParallel = JSON.parse(readFileSync(join(parallelSeed.recordDirectory, "run.json"), "utf8"));
|
|
1363
|
+
const activeMembers = Object.values(activeParallel.resume.active);
|
|
1364
|
+
check("different historical tasks share one tab with distinct panes without losing state", parallelHerdr.calls.filter(([name]) => name === "createTab").length === 1 && parallelHerdr.calls.filter(([name]) => name === "splitPane").length === 1 && activeMembers.length === 2 && new Set(activeMembers.map((member) => member.paneId)).size === 2 && activeParallel.resume.paneIds.length === 2);
|
|
1365
|
+
releaseFirstParallel();
|
|
1366
|
+
await parallelFirst;
|
|
1367
|
+
const afterFirstParallel = JSON.parse(readFileSync(join(parallelSeed.recordDirectory, "run.json"), "utf8"));
|
|
1368
|
+
check("active resume membership keeps aggregate running before sibling task.json is persisted", afterFirstParallel.status === "running" && Object.keys(afterFirstParallel.resume.active).length === 1);
|
|
1369
|
+
check("first historical sibling completion does not close the shared tab", Object.keys(afterFirstParallel.resume.active).length === 1 && parallelHerdr.calls.filter(([name]) => name === "closeTab").length === 0);
|
|
1370
|
+
releaseSecondAllocation();
|
|
1371
|
+
await secondEntered;
|
|
1372
|
+
releaseSecondParallel();
|
|
1373
|
+
await parallelSecond;
|
|
1374
|
+
const afterParallel = JSON.parse(readFileSync(join(parallelSeed.recordDirectory, "run.json"), "utf8"));
|
|
1375
|
+
check("last historical sibling atomically claims and closes the shared tab", parallelHerdr.calls.filter(([name]) => name === "closeTab").length === 1 && !afterParallel.resume && !afterParallel.cleanupPendingTabIds);
|
|
1376
|
+
|
|
1377
|
+
const failureSiblingRoot = join(root, "failed-historical-sibling");
|
|
1378
|
+
const failureSiblingSeed = (await executeHerdrSubagents({ tasks: [{ name: "fails", prompt: "seed one", agent: "explorer" }, { name: "continues", prompt: "seed two", agent: "reviewer" }], concurrency: 2 }, undefined, undefined, { cwd: failureSiblingRoot }, { agentsDirectory: join(process.cwd(), "agents"), herdr: fakeHerdr() })).details;
|
|
1379
|
+
const failureSiblingHerdr = fakeHerdr();
|
|
1380
|
+
const failurePrompt = failureSiblingHerdr.promptAgent.bind(failureSiblingHerdr);
|
|
1381
|
+
let failureEnteredCount = 0;
|
|
1382
|
+
let releaseFailure;
|
|
1383
|
+
let releaseSurvivor;
|
|
1384
|
+
const failureGate = new Promise((resolve) => { releaseFailure = resolve; });
|
|
1385
|
+
const survivorGate = new Promise((resolve) => { releaseSurvivor = resolve; });
|
|
1386
|
+
let bothFailureEntered;
|
|
1387
|
+
const bothEntered = new Promise((resolve) => { bothFailureEntered = resolve; });
|
|
1388
|
+
failureSiblingHerdr.promptAgent = async (request) => {
|
|
1389
|
+
failureEnteredCount += 1;
|
|
1390
|
+
if (failureEnteredCount === 2) bothFailureEntered();
|
|
1391
|
+
await (request.prompt.includes("FAIL") ? failureGate : survivorGate);
|
|
1392
|
+
return failurePrompt(request);
|
|
1393
|
+
};
|
|
1394
|
+
const failingResume = executeHerdrSubagents({ action: "resume", runId: failureSiblingSeed.runId, task: 1, prompt: "FAIL this turn" }, undefined, undefined, { cwd: failureSiblingRoot }, { agentsDirectory: join(process.cwd(), "agents"), herdr: failureSiblingHerdr });
|
|
1395
|
+
const survivingResume = executeHerdrSubagents({ action: "resume", runId: failureSiblingSeed.runId, task: 2, prompt: "keep running" }, undefined, undefined, { cwd: failureSiblingRoot }, { agentsDirectory: join(process.cwd(), "agents"), herdr: failureSiblingHerdr });
|
|
1396
|
+
await bothEntered;
|
|
1397
|
+
releaseFailure();
|
|
1398
|
+
const failedSiblingResult = await failingResume;
|
|
1399
|
+
const whileSiblingRuns = JSON.parse(readFileSync(join(failureSiblingSeed.recordDirectory, "run.json"), "utf8"));
|
|
1400
|
+
check("one historical failure leaves its sibling running", failedSiblingResult.isError && Object.keys(whileSiblingRuns.resume.active).length === 1 && failureSiblingHerdr.calls.filter(([name]) => name === "closeTab").length === 0);
|
|
1401
|
+
releaseSurvivor();
|
|
1402
|
+
await survivingResume;
|
|
1403
|
+
|
|
896
1404
|
const partialResumeHerdr = fakeHerdr();
|
|
897
1405
|
const partialResumeTaskFile = join((await listSubagentHistory(root)).find((run) => run.id === failedAfterBlock.details.runId).tasks[0].recordDirectory, "task.json");
|
|
898
1406
|
const stalePartialTask = JSON.parse(readFileSync(partialResumeTaskFile, "utf8"));
|
|
@@ -914,16 +1422,25 @@ try {
|
|
|
914
1422
|
historicalCancellationController.abort(new Error("Parent Pi session closed."));
|
|
915
1423
|
await historicalCancellationPromise;
|
|
916
1424
|
const historicalCancelledRun = JSON.parse(readFileSync(join(failedAfterBlock.details.recordDirectory, "run.json"), "utf8"));
|
|
917
|
-
check("parent shutdown
|
|
1425
|
+
check("parent shutdown cancels only its historical member and preserves sibling aggregation", historicalCancelledRun.status === "partial");
|
|
918
1426
|
const followupBlockedHerdr = fakeHerdr("blocked");
|
|
919
1427
|
const blockedFollowup = await executeHerdrSubagents({ action: "resume", runId: backgroundId, task: 1, prompt: "ask a follow-up" }, undefined, undefined, { cwd: root }, { agentsDirectory: join(process.cwd(), "agents"), herdr: followupBlockedHerdr });
|
|
920
|
-
const duplicateResumeHerdr = fakeHerdr();
|
|
921
|
-
const duplicateResume = await executeHerdrSubagents({ action: "resume", runId: backgroundId, task: 1, prompt: "duplicate live follow-up" }, undefined, undefined, { cwd: root }, { agentsDirectory: join(process.cwd(), "agents"), herdr: duplicateResumeHerdr });
|
|
922
|
-
check("historical resume rejects an active run without starting a second Agent", duplicateResume.isError && duplicateResume.content[0].text.includes("still active") && duplicateResumeHerdr.calls.length === 0);
|
|
923
|
-
const answeredFollowup = await executeHerdrSubagents({ action: "respond", runId: backgroundId, answer: "follow-up answer" }, undefined, undefined, { cwd: root }, { agentsDirectory: join(process.cwd(), "agents"), herdr: followupBlockedHerdr });
|
|
924
1428
|
const historicalTurns = join(stableHistory.tasks[0].recordDirectory, "turns");
|
|
925
|
-
const
|
|
926
|
-
|
|
1429
|
+
const blockedInputTurn = JSON.parse(readFileSync(join(historicalTurns, "03.json"), "utf8"));
|
|
1430
|
+
const blockedInputTask = JSON.parse(readFileSync(join(stableHistory.tasks[0].recordDirectory, "task.json"), "utf8"));
|
|
1431
|
+
check("historical input requests stay blocked in the shared resume tab", !blockedFollowup.isError && blockedFollowup.details.status === "blocked" && blockedInputTurn.status === "blocked" && blockedInputTask.status === "blocked" && !followupBlockedHerdr.calls.some(([name]) => name === "closeTab"));
|
|
1432
|
+
const answeredHistorical = await executeHerdrSubagents({ action: "respond", runId: backgroundId, task: 1, answer: "Inspect main." }, undefined, undefined, { cwd: root }, { agentsDirectory: join(process.cwd(), "agents"), herdr: followupBlockedHerdr });
|
|
1433
|
+
check("historical blocked resume settles through respond before tab cleanup", !answeredHistorical.isError && answeredHistorical.details.status === "completed" && followupBlockedHerdr.calls.at(-1)[0] === "closeTab");
|
|
1434
|
+
|
|
1435
|
+
const siblingInputRoot = join(root, "historical-sibling-input");
|
|
1436
|
+
const siblingInputSeed = (await executeHerdrSubagents({ tasks: [{ name: "branch", prompt: "seed branch", agent: "explorer" }, { name: "environment", prompt: "seed environment", agent: "reviewer" }], concurrency: 2 }, undefined, undefined, { cwd: siblingInputRoot }, { agentsDirectory: join(process.cwd(), "agents"), herdr: fakeHerdr() })).details;
|
|
1437
|
+
const siblingInputHerdr = fakeHerdr("two-blocked-then-complete");
|
|
1438
|
+
const firstSiblingInput = await executeHerdrSubagents({ action: "resume", runId: siblingInputSeed.runId, task: 1, prompt: "ask for branch" }, undefined, undefined, { cwd: siblingInputRoot }, { agentsDirectory: join(process.cwd(), "agents"), herdr: siblingInputHerdr });
|
|
1439
|
+
const secondSiblingInput = await executeHerdrSubagents({ action: "resume", runId: siblingInputSeed.runId, task: 2, prompt: "ask for environment" }, undefined, undefined, { cwd: siblingInputRoot }, { agentsDirectory: join(process.cwd(), "agents"), herdr: siblingInputHerdr });
|
|
1440
|
+
const answeredOneSibling = await executeHerdrSubagents({ action: "respond", runId: siblingInputSeed.runId, task: 1, answer: "Inspect main." }, undefined, undefined, { cwd: siblingInputRoot }, { agentsDirectory: join(process.cwd(), "agents"), herdr: siblingInputHerdr });
|
|
1441
|
+
const siblingInputRun = JSON.parse(readFileSync(join(siblingInputSeed.recordDirectory, "run.json"), "utf8"));
|
|
1442
|
+
check("responding to one historical blocked sibling exposes the remaining sibling question", firstSiblingInput.details.status === "blocked" && secondSiblingInput.details.status === "blocked" && !answeredOneSibling.isError && answeredOneSibling.details.status === "blocked" && answeredOneSibling.details.question === "Which environment should I inspect?" && answeredOneSibling.details.tasks[1].question === "Which environment should I inspect?" && siblingInputRun.status === "blocked" && siblingInputRun.question === "Which environment should I inspect?");
|
|
1443
|
+
|
|
927
1444
|
const prePromptFailureTaskFile = join(stableHistory.tasks[0].recordDirectory, "task.json");
|
|
928
1445
|
const prePromptStaleTask = JSON.parse(readFileSync(prePromptFailureTaskFile, "utf8"));
|
|
929
1446
|
Object.assign(prePromptStaleTask, { question: "stale question", error: "stale error", completedAt: "2000-01-01T00:00:00.000Z" });
|
|
@@ -952,7 +1469,7 @@ try {
|
|
|
952
1469
|
const ignoreText = readFileSync(ignoreFile, "utf8");
|
|
953
1470
|
check("runtime archive is ignored without hiding personas or overwriting unrelated rules", ignoreText.includes("herdr-subagents/runs/") && !ignoreText.split(/\r?\n/).includes("herdr-subagents/") && ignoreText.includes("unrelated-state/") && ignoreText.match(/herdr-subagents\/runs\//g)?.length === 1);
|
|
954
1471
|
const remainingRunId = history.find((run) => run.id !== backgroundId)?.id;
|
|
955
|
-
const cleaned = await executeHerdrSubagents({ action: "cleanup", runId:
|
|
1472
|
+
const cleaned = await executeHerdrSubagents({ action: "cleanup", runId: basename(stableHistory.recordDirectory) }, undefined, undefined, { cwd: root }, { agentsDirectory: join(process.cwd(), "agents"), herdr: fakeHerdr() });
|
|
956
1473
|
const afterCleanup = await listSubagentHistory(root);
|
|
957
1474
|
check("explicit cleanup removes only the selected run", !cleaned.isError && !afterCleanup.some((run) => run.id === backgroundId) && (!remainingRunId || afterCleanup.some((run) => run.id === remainingRunId)));
|
|
958
1475
|
|
|
@@ -1065,7 +1582,7 @@ try {
|
|
|
1065
1582
|
const currentAttemptId = "22222222-2222-4222-8222-222222222222";
|
|
1066
1583
|
writeFileSync(join(isolatedAttemptRun, "run.json"), JSON.stringify({ id: "isolated-attempt", label: "isolated-attempt", status: "running", startedAt: new Date().toISOString() }));
|
|
1067
1584
|
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: "
|
|
1585
|
+
writeFileSync(join(isolatedAttemptTask, "report.json"), JSON.stringify({ attemptId: "11111111-1111-4111-8111-111111111111", status: "blocked", summary: "Prior attempt blocked.", question: "Old question?", documents: [] }));
|
|
1069
1586
|
const isolatedReconciliation = await reconcileSubagentRuns(isolatedAttemptRoot, fakeHerdr());
|
|
1070
1587
|
const isolatedTaskRecord = JSON.parse(readFileSync(join(isolatedAttemptTask, "task.json"), "utf8"));
|
|
1071
1588
|
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.");
|
|
@@ -1079,6 +1596,33 @@ try {
|
|
|
1079
1596
|
writeFileSync(join(ownerLossTask, "reports", `${ownerLossAttempt}.json`), JSON.stringify({ attemptId: ownerLossAttempt, status: "completed", summary: "Completed before owner loss.", documents: [] }));
|
|
1080
1597
|
await reconcileSubagentRuns(ownerLossRoot, fakeHerdr(), { sessionId: "new-owner", processId: process.pid });
|
|
1081
1598
|
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);
|
|
1599
|
+
const blockedOwnerLossRoot = join(root, "blocked-owner-loss-recovery");
|
|
1600
|
+
const blockedOwnerLossRun = join(blockedOwnerLossRoot, ".pi", "herdr-subagents", "runs", "run");
|
|
1601
|
+
const blockedOwnerLossTask = join(blockedOwnerLossRun, "tasks", "01-task");
|
|
1602
|
+
const blockedOwnerLossAttempt = "66666666-6666-4666-8666-666666666666";
|
|
1603
|
+
mkdirSync(join(blockedOwnerLossTask, "reports"), { recursive: true });
|
|
1604
|
+
writeFileSync(join(blockedOwnerLossRun, "run.json"), JSON.stringify({ id: "blocked-owner-loss", label: "blocked-owner-loss", status: "running", effectiveConcurrency: 1, ownerSessionId: "dead-owner", ownerProcessId: 99999999, tabId: "w1:t-blocked-owner-loss", startedAt: new Date().toISOString() }));
|
|
1605
|
+
writeFileSync(join(blockedOwnerLossTask, "task.json"), JSON.stringify({ id: "blocked-owner-loss-task", runId: "blocked-owner-loss", order: 1, status: "running", paneId: "w1:p-blocked-owner-loss", currentAttemptId: blockedOwnerLossAttempt, sessionFile: join(blockedOwnerLossTask, "session.jsonl") }));
|
|
1606
|
+
writeFileSync(join(blockedOwnerLossTask, "reports", `${blockedOwnerLossAttempt}.json`), JSON.stringify({ attemptId: blockedOwnerLossAttempt, status: "blocked", summary: "Need input before owner loss.", question: "Which branch?", documents: [] }));
|
|
1607
|
+
const blockedOwnerLossHerdr = fakeHerdr();
|
|
1608
|
+
await reconcileSubagentRuns(blockedOwnerLossRoot, blockedOwnerLossHerdr, { sessionId: "new-owner", processId: process.pid });
|
|
1609
|
+
const cancelledBlockedOwnerTask = JSON.parse(readFileSync(join(blockedOwnerLossTask, "task.json"), "utf8"));
|
|
1610
|
+
const cancelledBlockedOwnerReport = JSON.parse(readFileSync(join(blockedOwnerLossTask, "report.json"), "utf8"));
|
|
1611
|
+
const cancelledBlockedOwnerAttempt = JSON.parse(readFileSync(join(blockedOwnerLossTask, "reports", `${blockedOwnerLossAttempt}.json`), "utf8"));
|
|
1612
|
+
check("owner-loss reconciliation never leaves blocked work without dispatcher ownership", cancelledBlockedOwnerTask.status === "cancelled" && cancelledBlockedOwnerReport.status === "failed" && !cancelledBlockedOwnerReport.question && cancelledBlockedOwnerAttempt.status === "failed" && !cancelledBlockedOwnerAttempt.question && JSON.parse(readFileSync(join(blockedOwnerLossRun, "run.json"), "utf8")).status === "cancelled" && blockedOwnerLossHerdr.calls.some(([name]) => name === "closeTab"));
|
|
1613
|
+
const parallelInputRecoveryRoot = join(root, "parallel-input-recovery");
|
|
1614
|
+
const parallelInputRecoveryRun = join(parallelInputRecoveryRoot, ".pi", "herdr-subagents", "runs", "run");
|
|
1615
|
+
const parallelInputRecoveryTask = join(parallelInputRecoveryRun, "tasks", "01-task");
|
|
1616
|
+
const parallelInputRecoveryAttempt = "55555555-5555-4555-8555-555555555555";
|
|
1617
|
+
mkdirSync(join(parallelInputRecoveryTask, "reports"), { recursive: true });
|
|
1618
|
+
writeFileSync(join(parallelInputRecoveryRun, "run.json"), JSON.stringify({ id: "parallel-input-recovery", label: "parallel-input-recovery", status: "running", effectiveConcurrency: 2, ownerSessionId: "dead-owner", ownerProcessId: 99999999, tabId: "w1:t-parallel-input", startedAt: new Date().toISOString() }));
|
|
1619
|
+
writeFileSync(join(parallelInputRecoveryTask, "task.json"), JSON.stringify({ id: "parallel-input-task", runId: "parallel-input-recovery", order: 1, status: "running", paneId: "w1:p-parallel-input", currentAttemptId: parallelInputRecoveryAttempt, sessionFile: join(parallelInputRecoveryTask, "session.jsonl") }));
|
|
1620
|
+
writeFileSync(join(parallelInputRecoveryTask, "reports", `${parallelInputRecoveryAttempt}.json`), JSON.stringify({ attemptId: parallelInputRecoveryAttempt, status: "blocked", summary: "Need a branch.", question: "Which branch?", documents: [] }));
|
|
1621
|
+
await reconcileSubagentRuns(parallelInputRecoveryRoot, fakeHerdr(), { sessionId: "new-owner", processId: process.pid });
|
|
1622
|
+
const recoveredParallelInputTask = JSON.parse(readFileSync(join(parallelInputRecoveryTask, "task.json"), "utf8"));
|
|
1623
|
+
const recoveredParallelInputReport = JSON.parse(readFileSync(join(parallelInputRecoveryTask, "report.json"), "utf8"));
|
|
1624
|
+
const recoveredParallelInputAttempt = JSON.parse(readFileSync(join(parallelInputRecoveryTask, "reports", `${parallelInputRecoveryAttempt}.json`), "utf8"));
|
|
1625
|
+
check("reconciliation treats a blocked report identically at any concurrency", recoveredParallelInputTask.status === "cancelled" && recoveredParallelInputReport.status === "failed" && !recoveredParallelInputReport.question && recoveredParallelInputAttempt.status === "failed" && !recoveredParallelInputAttempt.question && JSON.parse(readFileSync(join(parallelInputRecoveryRun, "run.json"), "utf8")).status === "cancelled");
|
|
1082
1626
|
const malformedRoot = join(root, "malformed-recovery");
|
|
1083
1627
|
const malformedRun = join(malformedRoot, ".pi", "herdr-subagents", "runs", "run");
|
|
1084
1628
|
const malformedTask = join(malformedRun, "tasks", "01-task");
|