@maxiaochao/pi-toolkit 0.13.13 → 0.13.14
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 +4 -0
- package/package.json +1 -1
- package/packages/herdr-subagents/ARCHITECTURE.md +1 -1
- package/packages/herdr-subagents/CHANGELOG.md +4 -0
- package/packages/herdr-subagents/README.md +3 -1
- package/packages/herdr-subagents/TESTING.md +2 -2
- package/packages/herdr-subagents/extensions/herdr-subagents.ts +1 -1
- package/packages/herdr-subagents/extensions/subagent-report.ts +23 -9
- package/packages/herdr-subagents/package.json +1 -1
- package/packages/herdr-subagents/src/config.ts +5 -3
- package/packages/herdr-subagents/tests/core-state-machine.mjs +0 -2
- package/packages/herdr-subagents/tests/report-boundary.mjs +37 -4
- package/packages/herdr-subagents/tests/report-lifecycle.mjs +2 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
## 2026-10-26 - v0.13.14
|
|
2
|
+
|
|
3
|
+
- herdr-subagents v0.7.6: Esc in a child waits abortGraceSeconds (default 30) for a correction before publishing child_aborted
|
|
4
|
+
|
|
1
5
|
## 2026-10-26 - v0.13.13
|
|
2
6
|
|
|
3
7
|
- background-tasks v0.1.2: restrict background tasks to model-only and use concise automatic-notification guidance
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@maxiaochao/pi-toolkit",
|
|
3
|
-
"version": "0.13.
|
|
3
|
+
"version": "0.13.14",
|
|
4
4
|
"description": "Personal pi coding agent toolkit: background tasks, scheduler, /btw, /cache_export, apply_patch, browser automation, and bundled theme/skills.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"workspaces": [
|
|
@@ -82,7 +82,7 @@ There is no global dispatcher, persisted background queue, blocked response lane
|
|
|
82
82
|
|
|
83
83
|
Each task owns one Herdr pane and one single-task Pi process. Herdr first starts and detects the interactive Pi, then submits the prompt exactly once. At Pi’s final `agent_settled` boundary, the child extension records settled state but leaves the interactive process alive. The atomic report remains the authoritative task result. Early-finishing children remain visible while sibling tasks continue. Once the whole run settles, the orchestrator closes an unfocused owned tab immediately and defers a focused tab until the user leaves it, so Pi can restore the terminal normally.
|
|
84
84
|
|
|
85
|
-
A completed round without a terminal report gets one child-side reminder at `agent_before_settle`. The marker is written before continuation. Another omission publishes an unconfirmed `missing_report` diagnostic; final errors use `child_error`.
|
|
85
|
+
A completed round without a terminal report gets one child-side reminder at `agent_before_settle`. The marker is written before continuation. Another omission publishes an unconfirmed `missing_report` diagnostic; final errors use `child_error`. A user abort (Esc) skips that boundary. `agent_settled(aborted)` starts a grace timer (`abortGraceSeconds`, default 30); a new round (`before_agent_start`) or session shutdown clears it, otherwise it publishes `child_aborted`. Existing terminal reports are not replaced. None of this adds a parent monitoring loop.
|
|
86
86
|
|
|
87
87
|
The child writes:
|
|
88
88
|
|
|
@@ -175,6 +175,7 @@ These are all supported top-level fields in `herdr-subagents.json`:
|
|
|
175
175
|
| `defaultConcurrency` | Positive integer | `1` | Concurrency used when a call omits `concurrency`; capped to `maxConcurrency`. |
|
|
176
176
|
| `maxConcurrency` | Positive integer | `4` | Hard ceiling for requested/default read-only concurrency. It does not make write-capable batches safe. |
|
|
177
177
|
| `stalledWarningSeconds` | Positive integer | `300` | Emit a warning after a child runs this long. This is not a timeout and does not stop it. |
|
|
178
|
+
| `abortGraceSeconds` | Positive integer | `30` | After the user aborts a child with Esc, wait this long for a correction. If no new round starts, publish an unconfirmed `child_aborted` failure. |
|
|
178
179
|
| `defaultModel` | Non-empty Pi model ID, normally `provider/model` | Unset | Fallback child model. If unset, the parent model is inherited. |
|
|
179
180
|
| `defaultThinking` | Non-empty Pi thinking value | Unset | Fallback thinking level below Persona-specific settings. Typical Pi values are `off`, `minimal`, `low`, `medium`, `high`, `xhigh`, and `max`; model support varies. |
|
|
180
181
|
| `defaultExtensions` | Array of non-empty Pi source strings | Unset (no optional extensions) | Fallback extension sources for Personas without an explicit `extensions` list. Project replaces global; `[]` clears inherited defaults. |
|
|
@@ -219,6 +220,7 @@ Save this as `~/.pi/agent/herdr-subagents.json`. Model IDs, skills, and extensio
|
|
|
219
220
|
"defaultConcurrency": 2,
|
|
220
221
|
"maxConcurrency": 4,
|
|
221
222
|
"stalledWarningSeconds": 300,
|
|
223
|
+
"abortGraceSeconds": 30,
|
|
222
224
|
"defaultModel": "anthropic/claude-sonnet-4-5",
|
|
223
225
|
"defaultThinking": "medium",
|
|
224
226
|
"defaultExtensions": ["~/.pi/agent/extensions/provider-routing.ts"],
|
|
@@ -354,6 +356,6 @@ See [TESTING.md](TESTING.md) for deterministic tests, extension loading, packagi
|
|
|
354
356
|
|
|
355
357
|
The child checks `reports/<attempt-id>.json` at `agent_before_settle`. A valid completed or failed report adds zero requests. A normally completed round without a report receives one reminder to submit existing results, not redo the task. `attempts/<attempt-id>.reminded` is persisted before continuing; rounds and reloads cannot reset it.
|
|
356
358
|
|
|
357
|
-
A second omission produces a `failed` protocol report with `error: missing_report`, an explicitly **unconfirmed** result, and the child session path. This does not assert the delegated work itself failed. Final provider errors produce `child_error` without a reminder.
|
|
359
|
+
A second omission produces a `failed` protocol report with `error: missing_report`, an explicitly **unconfirmed** result, and the child session path. This does not assert the delegated work itself failed. Final provider errors produce `child_error` without a reminder. A user abort (Esc) skips before-settle. The child waits `abortGraceSeconds` (default 30) for a correction; a new round cancels the timer and the child continues. If nothing starts in time, it publishes an unconfirmed `child_aborted` failure. Parent-requested cancellation retains its existing cancelled lifecycle.
|
|
358
360
|
|
|
359
361
|
The parent still waits for an atomic attempt report or process exit. It does not infer success from idle/settled or run a second reminder monitor. Report idempotency and focus-aware tab cleanup are unchanged.
|
|
@@ -60,9 +60,9 @@ For real smoke testing, verify that each child appears in a visible pane and can
|
|
|
60
60
|
|
|
61
61
|
## Missing-report regressions
|
|
62
62
|
|
|
63
|
-
`tests/report-boundary.mjs` checks completed/failed report idempotency, one durable reminder across rounds and extension reload, conflicting report rejection, unconfirmed diagnostics with a session path, and
|
|
63
|
+
`tests/report-boundary.mjs` checks completed/failed report idempotency, one durable reminder across rounds and extension reload, conflicting report rejection, unconfirmed diagnostics with a session path, Esc grace period (timer cancelled by steering or shutdown, `child_aborted` after expiry), and error without continuation.
|
|
64
64
|
|
|
65
|
-
`tests/report-lifecycle.mjs` uses the real installed Pi SDK and a fully offline provider. It covers ten cases: normal report, failed report, successful reminder, repeated omission, transient retry, exhausted retry, queued follow-up, another before-settle continuation,
|
|
65
|
+
`tests/report-lifecycle.mjs` uses the real installed Pi SDK and a fully offline provider. It covers ten cases: normal report, failed report, successful reminder, repeated omission, transient retry, exhausted retry, queued follow-up, another before-settle continuation, Esc abort, and a fresh prompt from a settled listener. A normal report makes one request; a reminder adds at most one. Native abort does not emit before-settle; its terminal diagnostic appears only after `abortGraceSeconds` (set to 1 in the test). These are native lifecycle checks, not a claim of real-Herdr pane/concurrency coverage.
|
|
66
66
|
|
|
67
67
|
Use `PI_TEST_BINARY` to select Pi 1.4.0 for these checks rather than an older workspace-local Pi. No paid model or real credential is used by the new regression fixtures.
|
|
68
68
|
|
|
@@ -89,7 +89,7 @@ async function handleHerdrCommand(args: string, ctx: ExtensionCommandContext, fl
|
|
|
89
89
|
}
|
|
90
90
|
if (section === "settings") {
|
|
91
91
|
const settings = configuration.settings;
|
|
92
|
-
const values: Record<string, string | number | undefined> = { defaultConcurrency: settings.defaultConcurrency, maxConcurrency: settings.maxConcurrency, stalledWarningSeconds: settings.stalledWarningSeconds, defaultModel: settings.defaultModel, defaultThinking: settings.defaultThinking, defaultExtensions: JSON.stringify(settings.defaultExtensions ?? []) };
|
|
92
|
+
const values: Record<string, string | number | undefined> = { defaultConcurrency: settings.defaultConcurrency, maxConcurrency: settings.maxConcurrency, stalledWarningSeconds: settings.stalledWarningSeconds, abortGraceSeconds: settings.abortGraceSeconds, defaultModel: settings.defaultModel, defaultThinking: settings.defaultThinking, defaultExtensions: JSON.stringify(settings.defaultExtensions ?? []) };
|
|
93
93
|
const lines = [`exposure=${registeredExposure.value ?? "Pi default"} (${registeredExposure.source}; loaded at extension registration)`, ...Object.entries(values).map(([key, value]) => `${key}=${String(value ?? "unset")} (${configuration.settingSources[key] ?? "built-in"})`)];
|
|
94
94
|
if (configuration.diagnostics.length) lines.push("Diagnostics:", ...configuration.diagnostics);
|
|
95
95
|
ctx.ui.notify(lines.join("\n"), "info");
|
|
@@ -9,6 +9,10 @@ import { parseReport } from "../src/records.ts";
|
|
|
9
9
|
import { resultPathForAttemptId } from "../src/result-path.ts";
|
|
10
10
|
import { PARENT_PANE_ENV } from "../src/live-agent.ts";
|
|
11
11
|
import { attemptReportPath, attemptStatePath, requireAttemptId, type AttemptState } from "../src/attempts.ts";
|
|
12
|
+
import { loadSubagentConfiguration } from "../src/config.ts";
|
|
13
|
+
import { fileURLToPath } from "node:url";
|
|
14
|
+
|
|
15
|
+
const packageAgentsDir = fileURLToPath(new URL("../agents/", import.meta.url));
|
|
12
16
|
|
|
13
17
|
const RETURN_SHORTCUT = "alt+p";
|
|
14
18
|
|
|
@@ -108,7 +112,10 @@ function reportHerdrState(state: "working" | "blocked" | "idle", message?: strin
|
|
|
108
112
|
export default function (pi: ExtensionAPI) {
|
|
109
113
|
let activeAttemptId: string | undefined;
|
|
110
114
|
let blockedCount = 0;
|
|
115
|
+
let abortTimer: ReturnType<typeof setTimeout> | undefined;
|
|
116
|
+
const clearAbortTimer = () => { if (abortTimer) clearTimeout(abortTimer); abortTimer = undefined; };
|
|
111
117
|
pi.on("before_agent_start", async () => {
|
|
118
|
+
clearAbortTimer(); // The user steered the child after Esc.
|
|
112
119
|
const taskDirectory = process.env.PI_HERDR_SUBAGENTS_TASK_DIR?.trim();
|
|
113
120
|
if (!taskDirectory) return;
|
|
114
121
|
activeAttemptId = currentAttemptId(taskDirectory);
|
|
@@ -144,19 +151,25 @@ export default function (pi: ExtensionAPI) {
|
|
|
144
151
|
if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
|
|
145
152
|
}
|
|
146
153
|
}
|
|
147
|
-
|
|
154
|
+
// Esc normally skips this boundary; if it lands mid-boundary, still let the user steer.
|
|
155
|
+
if (event.outcome === "aborted") return;
|
|
156
|
+
const reason = event.outcome === "completed" ? "missing_report" : "child_error";
|
|
148
157
|
saveUnconfirmed(taskDirectory, attemptId, reason, ctx.sessionManager.getSessionFile());
|
|
149
|
-
//
|
|
158
|
+
// Final provider errors receive a diagnostic, never a wake-up.
|
|
150
159
|
});
|
|
151
160
|
pi.on("agent_settled", (event, ctx) => {
|
|
152
161
|
const taskDirectory = process.env.PI_HERDR_SUBAGENTS_TASK_DIR?.trim();
|
|
153
|
-
if (taskDirectory && activeAttemptId) {
|
|
154
|
-
//
|
|
155
|
-
//
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
162
|
+
if (event.aborted && taskDirectory && activeAttemptId) {
|
|
163
|
+
// Esc skips before-settle. Give the user time to steer; if nothing starts a new
|
|
164
|
+
// round within the grace period, close the attempt with an unconfirmed diagnostic.
|
|
165
|
+
const attemptId = activeAttemptId;
|
|
166
|
+
const sessionFile = ctx.sessionManager.getSessionFile();
|
|
167
|
+
const graceMs = loadSubagentConfiguration(ctx.cwd, packageAgentsDir).settings.abortGraceSeconds * 1000;
|
|
168
|
+
clearAbortTimer();
|
|
169
|
+
abortTimer = setTimeout(() => {
|
|
170
|
+
abortTimer = undefined;
|
|
171
|
+
if (!existsSync(attemptReportPath(taskDirectory, attemptId))) saveUnconfirmed(taskDirectory, attemptId, "child_aborted", sessionFile);
|
|
172
|
+
}, graceMs);
|
|
160
173
|
}
|
|
161
174
|
// Keep the interactive child alive at Pi's final automatic-work boundary.
|
|
162
175
|
// The orchestrator settles each task from its atomic report. Once every task in
|
|
@@ -176,6 +189,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
176
189
|
description: "Return to parent Agent",
|
|
177
190
|
async handler(ctx) { await returnToParent(pi, ctx); },
|
|
178
191
|
});
|
|
192
|
+
pi.on("session_shutdown", clearAbortTimer);
|
|
179
193
|
pi.on("session_start", async (_event, ctx) => {
|
|
180
194
|
// Remove the legacy hint on reload without touching editor/plugin status.
|
|
181
195
|
ctx.ui.setStatus("herdr-subagents-parent", undefined);
|
|
@@ -11,6 +11,7 @@ export interface SubagentConfig {
|
|
|
11
11
|
defaultConcurrency: number;
|
|
12
12
|
maxConcurrency: number;
|
|
13
13
|
stalledWarningSeconds: number;
|
|
14
|
+
abortGraceSeconds: number;
|
|
14
15
|
defaultModel?: string;
|
|
15
16
|
defaultThinking?: string;
|
|
16
17
|
defaultExtensions?: string[];
|
|
@@ -39,6 +40,7 @@ interface ConfigFile {
|
|
|
39
40
|
defaultConcurrency?: number;
|
|
40
41
|
maxConcurrency?: number;
|
|
41
42
|
stalledWarningSeconds?: number;
|
|
43
|
+
abortGraceSeconds?: number;
|
|
42
44
|
defaultModel?: string;
|
|
43
45
|
defaultThinking?: string;
|
|
44
46
|
defaultExtensions?: string[];
|
|
@@ -50,7 +52,7 @@ export interface ConfigurationPaths {
|
|
|
50
52
|
globalAgents?: string;
|
|
51
53
|
projectAgents?: string;
|
|
52
54
|
}
|
|
53
|
-
const DEFAULTS: SubagentConfig = { defaultConcurrency: 1, maxConcurrency: 4, stalledWarningSeconds: 300, personas: {} };
|
|
55
|
+
const DEFAULTS: SubagentConfig = { defaultConcurrency: 1, maxConcurrency: 4, stalledWarningSeconds: 300, abortGraceSeconds: 30, personas: {} };
|
|
54
56
|
|
|
55
57
|
function readConfig(path: string, source: string, diagnostics: string[]): ConfigFile {
|
|
56
58
|
if (!existsSync(path)) return {};
|
|
@@ -70,7 +72,7 @@ function mergeConfig(base: SubagentConfig, input: ConfigFile, source: string, so
|
|
|
70
72
|
if (input.exposure === "direct" || input.exposure === "model-only") { result.exposure = input.exposure; sources.exposure = source; }
|
|
71
73
|
else diagnostics.push(`${source} exposure must be 'direct' or 'model-only'`);
|
|
72
74
|
}
|
|
73
|
-
for (const key of ["defaultConcurrency", "maxConcurrency", "stalledWarningSeconds"] as const) {
|
|
75
|
+
for (const key of ["defaultConcurrency", "maxConcurrency", "stalledWarningSeconds", "abortGraceSeconds"] as const) {
|
|
74
76
|
const value = input[key];
|
|
75
77
|
if (value === undefined) continue;
|
|
76
78
|
if (!Number.isInteger(value) || Number(value) <= 0) { diagnostics.push(`${source} ${key} must be a positive integer`); continue; }
|
|
@@ -121,7 +123,7 @@ export function loadSubagentConfiguration(cwd: string, packageAgentsDirectory: s
|
|
|
121
123
|
const projectConfigPath = paths.projectConfig ?? join(cwd, ".pi", "herdr-subagents.json");
|
|
122
124
|
const globalAgentsDirectory = paths.globalAgents ?? join(dirname(globalConfigPath), "herdr-subagents", "agents");
|
|
123
125
|
const projectAgentsDirectory = paths.projectAgents ?? join(cwd, ".pi", "herdr-subagents", "agents");
|
|
124
|
-
const settingSources: Record<string, string> = { defaultConcurrency: "built-in", maxConcurrency: "built-in", stalledWarningSeconds: "built-in" };
|
|
126
|
+
const settingSources: Record<string, string> = { defaultConcurrency: "built-in", maxConcurrency: "built-in", stalledWarningSeconds: "built-in", abortGraceSeconds: "built-in" };
|
|
125
127
|
const globalInput = readConfig(globalConfigPath, "global", diagnostics);
|
|
126
128
|
const projectInput = readConfig(projectConfigPath, "project", diagnostics);
|
|
127
129
|
const globalSettings = mergeConfig(DEFAULTS, globalInput, "global", settingSources, diagnostics);
|
|
@@ -357,8 +357,6 @@ assert.throws(() => applyTaskStatus(terminal, "running"), /terminal tasks are im
|
|
|
357
357
|
process.exit = originalExit;
|
|
358
358
|
}
|
|
359
359
|
assert.equal(exitCode, undefined, "settled child must remain alive for focus-aware tab cleanup");
|
|
360
|
-
const attempt = JSON.parse(readFileSync(join(directory, "attempts", `${attemptId}.json`), "utf8"));
|
|
361
|
-
assert.equal(attempt.state, "settled");
|
|
362
360
|
const theme = { fg: (_color, text) => text, bold: (text) => text };
|
|
363
361
|
const renderedCall = reportTool.renderCall({ ...report, result: "FULL RESULT MUST NOT RENDER" }, theme).render(120).join("\n");
|
|
364
362
|
assert.match(renderedCall, /completed.*saved to disk/);
|
|
@@ -3,6 +3,7 @@ import { randomUUID } from "node:crypto";
|
|
|
3
3
|
import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { tmpdir } from "node:os";
|
|
5
5
|
import { join } from "node:path";
|
|
6
|
+
import { mkdirSync } from "node:fs";
|
|
6
7
|
import registerReport from "../extensions/subagent-report.ts";
|
|
7
8
|
|
|
8
9
|
const root = mkdtempSync(join(tmpdir(), "herdr-report-boundary-"));
|
|
@@ -10,7 +11,9 @@ const previous = process.env.PI_HERDR_SUBAGENTS_TASK_DIR;
|
|
|
10
11
|
process.env.PI_HERDR_SUBAGENTS_TASK_DIR = root;
|
|
11
12
|
const sessionFile = join(root, "session.jsonl");
|
|
12
13
|
writeFileSync(sessionFile, "");
|
|
13
|
-
|
|
14
|
+
mkdirSync(join(root, ".pi"));
|
|
15
|
+
writeFileSync(join(root, ".pi", "herdr-subagents.json"), JSON.stringify({ abortGraceSeconds: 1 }));
|
|
16
|
+
const ctx = { cwd: root, sessionManager: { getSessionFile: () => sessionFile }, isIdle: () => true };
|
|
14
17
|
function fixture() {
|
|
15
18
|
const handlers = new Map();
|
|
16
19
|
let tool;
|
|
@@ -58,14 +61,44 @@ try {
|
|
|
58
61
|
assert.ok(diagnostic.documents.some(({ path }) => path === sessionFile));
|
|
59
62
|
assert.equal(await reloaded.handlers.get("agent_before_settle")(boundary(), ctx), undefined);
|
|
60
63
|
|
|
61
|
-
|
|
64
|
+
{
|
|
62
65
|
const current = attempt();
|
|
63
66
|
const run = fixture();
|
|
64
67
|
await run.handlers.get("before_agent_start")();
|
|
65
|
-
assert.equal(await run.handlers.get("agent_before_settle")(boundary(
|
|
68
|
+
assert.equal(await run.handlers.get("agent_before_settle")(boundary("aborted"), ctx), undefined);
|
|
69
|
+
assert.equal(existsSync(current.report), false, "Esc abort must not publish a terminal report; the user may steer");
|
|
70
|
+
}
|
|
71
|
+
{
|
|
72
|
+
// Esc skips before-settle and only emits agent_settled(aborted). Grace period, configurable.
|
|
73
|
+
const current = attempt();
|
|
74
|
+
const run = fixture();
|
|
75
|
+
await run.handlers.get("before_agent_start")();
|
|
76
|
+
await run.handlers.get("agent_settled")({ aborted: true }, ctx);
|
|
77
|
+
assert.equal(existsSync(current.report), false, "no report inside the grace period");
|
|
78
|
+
await run.handlers.get("before_agent_start")(); // user steers: timer must be cancelled
|
|
79
|
+
await new Promise((resolve) => setTimeout(resolve, 1300));
|
|
80
|
+
assert.equal(existsSync(current.report), false, "steering within the grace period cancels the diagnostic");
|
|
81
|
+
await run.handlers.get("agent_settled")({ aborted: true }, ctx);
|
|
82
|
+
await new Promise((resolve) => setTimeout(resolve, 1300));
|
|
83
|
+
const report = JSON.parse(readFileSync(current.report, "utf8"));
|
|
84
|
+
assert.equal(report.error, "child_aborted");
|
|
85
|
+
assert.match(report.summary, /unconfirmed/);
|
|
86
|
+
const shutdown = fixture(); // shutdown must clear a pending timer
|
|
87
|
+
const other = attempt();
|
|
88
|
+
await shutdown.handlers.get("before_agent_start")();
|
|
89
|
+
await shutdown.handlers.get("agent_settled")({ aborted: true }, ctx);
|
|
90
|
+
shutdown.handlers.get("session_shutdown")();
|
|
91
|
+
await new Promise((resolve) => setTimeout(resolve, 1300));
|
|
92
|
+
assert.equal(existsSync(other.report), false, "session shutdown clears the grace timer");
|
|
93
|
+
}
|
|
94
|
+
{
|
|
95
|
+
const current = attempt();
|
|
96
|
+
const run = fixture();
|
|
97
|
+
await run.handlers.get("before_agent_start")();
|
|
98
|
+
assert.equal(await run.handlers.get("agent_before_settle")(boundary("error"), ctx), undefined, "error: do not wake or retry the child");
|
|
66
99
|
const report = JSON.parse(readFileSync(current.report, "utf8"));
|
|
67
100
|
assert.equal(report.status, "failed");
|
|
68
|
-
assert.equal(report.error,
|
|
101
|
+
assert.equal(report.error, "child_error");
|
|
69
102
|
assert.match(report.summary, /unconfirmed/);
|
|
70
103
|
}
|
|
71
104
|
const repaired = attempt();
|
|
@@ -18,6 +18,7 @@ try{
|
|
|
18
18
|
const dir=join(root,scenario);mkdirSync(dir);
|
|
19
19
|
const attemptId=randomUUID();const statePath=join(dir,'attempts',attemptId+'.json');
|
|
20
20
|
process.env.PI_HERDR_SUBAGENTS_TASK_DIR=dir;
|
|
21
|
+
if(scenario==='abort'){mkdirSync(join(dir,'.pi'));writeFileSync(join(dir,'.pi','herdr-subagents.json'),JSON.stringify({abortGraceSeconds:1}));}
|
|
21
22
|
writeFileSync(join(dir,'task.json'),JSON.stringify({currentAttemptId:attemptId}));
|
|
22
23
|
const trace=[],states=[],errors=[],inputs=[];let requests=0,ended=0,boundaries=0,settled=0;
|
|
23
24
|
const entered=Promise.withResolvers();let session;
|
|
@@ -52,6 +53,7 @@ try{
|
|
|
52
53
|
const running=session.prompt('Run offline lifecycle check');
|
|
53
54
|
if(scenario==='abort'){await entered.promise;await session.abort();}
|
|
54
55
|
await running;
|
|
56
|
+
if(scenario==='abort'){assert.equal(existsSync(join(dir,'reports',attemptId+'.json')),false,'no report inside the grace period');await new Promise(r=>setTimeout(r,1500));}
|
|
55
57
|
assert.equal(errors.length,0,JSON.stringify(errors));
|
|
56
58
|
const expectedRequests={reported:1,failed_report:1,remind_success:2,normal_no_report:2,transient_retry:3,retry_exhausted:2,queued_followup:3,boundary_continue:2,abort:1,settled_hook_new_prompt:3}[scenario];
|
|
57
59
|
assert.equal(requests,expectedRequests,scenario);
|