@selesai/code 0.8.6 → 0.8.7
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 +16 -0
- package/dist/core/agent-session.js +13 -1
- package/dist/core/extensions/types.d.ts +2 -0
- package/dist/core/provider-composer.js +17 -4
- package/dist/core/settings-manager.d.ts +2 -0
- package/dist/core/settings-manager.js +3 -0
- package/dist/core/system-prompt.js +2 -8
- package/dist/core/system-prompt.test.js +6 -0
- package/dist/core/tools/index.d.ts +1 -0
- package/dist/core/tools/index.js +1 -0
- package/dist/core/tools/schema-prune.d.ts +16 -0
- package/dist/core/tools/schema-prune.js +39 -0
- package/dist/core/tools/schema-prune.test.d.ts +1 -0
- package/dist/core/tools/schema-prune.test.js +111 -0
- package/dist/defaults/settings.json +1 -0
- package/dist/extensions/model-prompt-injector/config.json +10 -0
- package/dist/extensions/model-prompt-injector/index.test.ts +112 -0
- package/dist/extensions/model-prompt-injector/index.ts +151 -0
- package/dist/extensions/package.json +1 -0
- package/dist/extensions/pi-subagents/CHANGELOG.md +158 -90
- package/dist/extensions/pi-subagents/UPSTREAM-V0.50-MAPPING.md +172 -0
- package/dist/extensions/pi-subagents/agents/oracle.md +7 -5
- package/dist/extensions/pi-subagents/agents/reviewer.md +2 -2
- package/dist/extensions/pi-subagents/agents/scout.md +1 -1
- package/dist/extensions/pi-subagents/agents/worker.md +1 -1
- package/dist/extensions/pi-subagents/docs/configuration.md +78 -19
- package/dist/extensions/pi-subagents/docs/extension-api.md +36 -0
- package/dist/extensions/pi-subagents/docs/missions.md +5 -3
- package/dist/extensions/pi-subagents/docs/models.md +1 -1
- package/dist/extensions/pi-subagents/docs/observability.md +42 -2
- package/dist/extensions/pi-subagents/docs/tool-reference.md +20 -3
- package/dist/extensions/pi-subagents/docs/workflows.md +5 -5
- package/dist/extensions/pi-subagents/install.mjs +2 -2
- package/dist/extensions/pi-subagents/package-lock.json +1470 -1472
- package/dist/extensions/pi-subagents/package.json +1 -1
- package/dist/extensions/pi-subagents/skills/pi-subagents/SKILL.md +1 -1
- package/dist/extensions/pi-subagents/skills/pi-subagents/references/constraints-and-recipes.md +5 -4
- package/dist/extensions/pi-subagents/skills/pi-subagents/references/execution-controls.md +15 -14
- package/dist/extensions/pi-subagents/skills/pi-subagents/references/management-authoring-rpc.md +5 -5
- package/dist/extensions/pi-subagents/skills/pi-subagents/references/prompting-and-roles.md +46 -26
- package/dist/extensions/pi-subagents/src/agents/agent-serializer.ts +2 -0
- package/dist/extensions/pi-subagents/src/agents/agents.ts +37 -12
- package/dist/extensions/pi-subagents/src/api/external-runs.ts +174 -84
- package/dist/extensions/pi-subagents/src/api/preflight.ts +13 -7
- package/dist/extensions/pi-subagents/src/extension/config.ts +53 -0
- package/dist/extensions/pi-subagents/src/extension/index.ts +67 -13
- package/dist/extensions/pi-subagents/src/extension/public-execution.ts +34 -6
- package/dist/extensions/pi-subagents/src/extension/rpc.ts +5 -1
- package/dist/extensions/pi-subagents/src/extension/schemas.ts +31 -30
- package/dist/extensions/pi-subagents/src/extension/tool-description.ts +10 -9
- package/dist/extensions/pi-subagents/src/inspectors/herdr/actions.ts +13 -8
- package/dist/extensions/pi-subagents/src/inspectors/herdr/inspector-runner.ts +16 -3
- package/dist/extensions/pi-subagents/src/inspectors/herdr/project-panes.ts +2 -6
- package/dist/extensions/pi-subagents/src/inspectors/herdr/shell-command.ts +16 -0
- package/dist/extensions/pi-subagents/src/intercom/intercom-bridge.ts +5 -4
- package/dist/extensions/pi-subagents/src/intercom/native-supervisor-channel.ts +19 -42
- package/dist/extensions/pi-subagents/src/missions/goal-driver.ts +3 -1
- package/dist/extensions/pi-subagents/src/missions/store.ts +8 -3
- package/dist/extensions/pi-subagents/src/runs/background/active-async-capacity.ts +82 -25
- package/dist/extensions/pi-subagents/src/runs/background/active-run-index.ts +71 -1
- package/dist/extensions/pi-subagents/src/runs/background/async-execution.ts +73 -43
- package/dist/extensions/pi-subagents/src/runs/background/async-job-tracker.ts +5 -0
- package/dist/extensions/pi-subagents/src/runs/background/async-resume.ts +14 -6
- package/dist/extensions/pi-subagents/src/runs/background/async-status-snapshot.ts +277 -0
- package/dist/extensions/pi-subagents/src/runs/background/async-status.ts +8 -3
- package/dist/extensions/pi-subagents/src/runs/background/chain-root-attachment.ts +2 -2
- package/dist/extensions/pi-subagents/src/runs/background/completion-replay.ts +11 -1
- package/dist/extensions/pi-subagents/src/runs/background/fleet-view.ts +21 -6
- package/dist/extensions/pi-subagents/src/runs/background/result-files.ts +437 -0
- package/dist/extensions/pi-subagents/src/runs/background/result-watcher.ts +205 -41
- package/dist/extensions/pi-subagents/src/runs/background/resume-guidance.ts +27 -7
- package/dist/extensions/pi-subagents/src/runs/background/retained-children.ts +76 -19
- package/dist/extensions/pi-subagents/src/runs/background/run-id-resolver.ts +30 -24
- package/dist/extensions/pi-subagents/src/runs/background/run-status.ts +101 -4
- package/dist/extensions/pi-subagents/src/runs/background/scheduled-runs.ts +54 -28
- package/dist/extensions/pi-subagents/src/runs/background/stale-run-reconciler.ts +27 -13
- package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +298 -33
- package/dist/extensions/pi-subagents/src/runs/background/subagent-wait.ts +2 -0
- package/dist/extensions/pi-subagents/src/runs/background/wait-completions.ts +5 -2
- package/dist/extensions/pi-subagents/src/runs/foreground/async-dismiss-action.ts +2 -1
- package/dist/extensions/pi-subagents/src/runs/foreground/chain-execution.ts +16 -0
- package/dist/extensions/pi-subagents/src/runs/foreground/execution.ts +219 -15
- package/dist/extensions/pi-subagents/src/runs/foreground/prompt-audit.ts +4 -3
- package/dist/extensions/pi-subagents/src/runs/foreground/subagent-executor.ts +333 -45
- package/dist/extensions/pi-subagents/src/runs/shared/completion-guard.ts +107 -1
- package/dist/extensions/pi-subagents/src/runs/shared/external-cli-runner.ts +4 -0
- package/dist/extensions/pi-subagents/src/runs/shared/llm-intent-arbiter.ts +39 -23
- package/dist/extensions/pi-subagents/src/runs/shared/model-fallback.ts +16 -2
- package/dist/extensions/pi-subagents/src/runs/shared/nested-events.ts +66 -62
- package/dist/extensions/pi-subagents/src/runs/shared/orca-progress-tabs.ts +376 -0
- package/dist/extensions/pi-subagents/src/runs/shared/parallel-utils.ts +2 -0
- package/dist/extensions/pi-subagents/src/runs/shared/subagent-control.ts +15 -0
- package/dist/extensions/pi-subagents/src/runs/shared/subagent-prompt-runtime.ts +1 -9
- package/dist/extensions/pi-subagents/src/runs/shared/subagent-startup-retry.ts +12 -0
- package/dist/extensions/pi-subagents/src/runs/shared/tool-timeout.ts +95 -0
- package/dist/extensions/pi-subagents/src/shared/agent-stream-options.ts +5 -0
- package/dist/extensions/pi-subagents/src/shared/artifacts.ts +0 -4
- package/dist/extensions/pi-subagents/src/shared/display-text.ts +50 -0
- package/dist/extensions/pi-subagents/src/shared/node-executable.ts +21 -0
- package/dist/extensions/pi-subagents/src/shared/types.ts +50 -4
- package/dist/extensions/pi-subagents/src/slash/slash-commands.ts +34 -25
- package/dist/extensions/pi-subagents/src/slash/slash-live-state.ts +4 -0
- package/dist/extensions/pi-subagents/src/tui/fleet-status.ts +160 -45
- package/dist/extensions/pi-subagents/src/tui/fleet-transcript.ts +1 -48
- package/dist/extensions/pi-subagents/src/tui/fleet.ts +128 -16
- package/dist/extensions/pi-subagents/src/tui/render.ts +122 -44
- package/dist/extensions/pi-subagents/src/watchdog/permission-arbiter.ts +2 -1
- package/dist/extensions/pi-subagents/src/watchdog/review.ts +4 -3
- package/dist/extensions/pi-subagents/src/workflows/chat-progress.ts +10 -2
- package/dist/extensions/pi-subagents/src/workflows/scripted-workflow.ts +272 -76
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/extensions/types.d.ts +2 -0
- package/dist/extensions/pi-subagents/test/integration/async-execution.test.ts +2 -2
- package/dist/extensions/pi-subagents/test/integration/async-job-tracker.test.ts +68 -9
- package/dist/extensions/pi-subagents/test/integration/async-status.test.ts +27 -1
- package/dist/extensions/pi-subagents/test/integration/chain-execution.test.ts +1 -1
- package/dist/extensions/pi-subagents/test/integration/error-handling.test.ts +49 -0
- package/dist/extensions/pi-subagents/test/integration/external-cli-runner.test.ts +1 -0
- package/dist/extensions/pi-subagents/test/integration/intercom-result-delivery.test.ts +26 -1
- package/dist/extensions/pi-subagents/test/integration/orca-progress-tabs.test.ts +144 -0
- package/dist/extensions/pi-subagents/test/integration/render-widget.test.ts +26 -0
- package/dist/extensions/pi-subagents/test/integration/result-watcher.test.ts +369 -98
- package/dist/extensions/pi-subagents/test/integration/single-execution.test.ts +111 -78
- package/dist/extensions/pi-subagents/test/integration/slash-commands.test.ts +49 -1
- package/dist/extensions/pi-subagents/test/integration/slash-live-state.test.ts +12 -0
- package/dist/extensions/pi-subagents/test/support/node-command.ts +15 -0
- package/dist/extensions/pi-subagents/test/unit/active-async-capacity.test.ts +50 -0
- package/dist/extensions/pi-subagents/test/unit/agent-frontmatter.test.ts +28 -0
- package/dist/extensions/pi-subagents/test/unit/agent-overrides.test.ts +61 -0
- package/dist/extensions/pi-subagents/test/unit/agent-stream-options.test.ts +14 -0
- package/dist/extensions/pi-subagents/test/unit/async-interrupt-action.test.ts +76 -0
- package/dist/extensions/pi-subagents/test/unit/async-resume.test.ts +2 -0
- package/dist/extensions/pi-subagents/test/unit/async-status-snapshot.test.ts +162 -0
- package/dist/extensions/pi-subagents/test/unit/completion-guard.test.ts +209 -10
- package/dist/extensions/pi-subagents/test/unit/completion-replay.test.ts +41 -1
- package/dist/extensions/pi-subagents/test/unit/external-runs.test.ts +135 -42
- package/dist/extensions/pi-subagents/test/unit/fleet-status.test.ts +252 -1
- package/dist/extensions/pi-subagents/test/unit/fleet.test.ts +192 -6
- package/dist/extensions/pi-subagents/test/unit/herdr-inspector-bootstrap.test.ts +32 -0
- package/dist/extensions/pi-subagents/test/unit/herdr-shell-command.test.ts +59 -0
- package/dist/extensions/pi-subagents/test/unit/index-child-registration.test.ts +69 -0
- package/dist/extensions/pi-subagents/test/unit/intercom-bridge.test.ts +23 -4
- package/dist/extensions/pi-subagents/test/unit/llm-intent-arbiter.test.ts +56 -1
- package/dist/extensions/pi-subagents/test/unit/mission-goal-driver.test.ts +67 -3
- package/dist/extensions/pi-subagents/test/unit/mission-lifecycle.test.ts +1 -1
- package/dist/extensions/pi-subagents/test/unit/mission-store.test.ts +14 -0
- package/dist/extensions/pi-subagents/test/unit/model-fallback.test.ts +18 -0
- package/dist/extensions/pi-subagents/test/unit/native-supervisor-channel.test.ts +2 -2
- package/dist/extensions/pi-subagents/test/unit/nested-events.test.ts +25 -2
- package/dist/extensions/pi-subagents/test/unit/node-executable.test.ts +26 -0
- package/dist/extensions/pi-subagents/test/unit/notify.test.ts +8 -0
- package/dist/extensions/pi-subagents/test/unit/orca-progress-tabs.test.ts +348 -0
- package/dist/extensions/pi-subagents/test/unit/package-manifest.test.ts +5 -2
- package/dist/extensions/pi-subagents/test/unit/pi-args.test.ts +1 -1
- package/dist/extensions/pi-subagents/test/unit/pi-coding-agent-dir.test.ts +34 -0
- package/dist/extensions/pi-subagents/test/unit/public-execution.test.ts +6 -1
- package/dist/extensions/pi-subagents/test/unit/render-helpers.test.ts +92 -0
- package/dist/extensions/pi-subagents/test/unit/result-files.test.ts +175 -0
- package/dist/extensions/pi-subagents/test/unit/retained-children.test.ts +127 -17
- package/dist/extensions/pi-subagents/test/unit/run-id-resolver.test.ts +44 -0
- package/dist/extensions/pi-subagents/test/unit/run-status.test.ts +247 -0
- package/dist/extensions/pi-subagents/test/unit/scheduled-runs.test.ts +107 -2
- package/dist/extensions/pi-subagents/test/unit/schemas.test.ts +5 -8
- package/dist/extensions/pi-subagents/test/unit/scripted-workflow.test.ts +456 -4
- package/dist/extensions/pi-subagents/test/unit/stale-run-reconciler.test.ts +128 -0
- package/dist/extensions/pi-subagents/test/unit/subagent-control.test.ts +28 -0
- package/dist/extensions/pi-subagents/test/unit/subagent-prompt-runtime.test.ts +16 -7
- package/dist/extensions/pi-subagents/test/unit/subagent-startup-retry.test.ts +15 -0
- package/dist/extensions/pi-subagents/test/unit/subagent-wait.test.ts +79 -15
- package/dist/extensions/pi-subagents/test/unit/tool-description.test.ts +12 -12
- package/dist/extensions/pi-subagents/test/unit/tool-timeout.test.ts +109 -0
- package/dist/extensions/pi-subagents/test/unit/wait-subscriptions.test.ts +34 -0
- package/dist/extensions/pi-subagents/test/unit/workflow-chat-progress.test.ts +94 -1
- package/dist/extensions/pi-subagents/test/unit/workflow-launch-params.test.ts +42 -2
- package/dist/extensions/question/index.ts +21 -6
- package/dist/extensions/question/question-list.ts +7 -1
- package/dist/extensions/question/row-layout.ts +3 -0
- package/dist/extensions/question/tests/exp.test.ts +70 -0
- package/dist/extensions/question/tests/exp2.test.ts +59 -0
- package/dist/extensions/question/tests/exp3.test.ts +47 -0
- package/dist/extensions/question/tests/helpers.test.ts +7 -0
- package/dist/extensions/question/tests/question-list.test.ts +140 -0
- package/dist/extensions/question/tests/row-layout.test.ts +13 -0
- package/dist/extensions/question/tests/selection-mode.test.ts +54 -0
- package/dist/extensions/question/tests/zz-ig3.test.ts +20 -0
- package/dist/extensions/question/tests/zz-probe.test.ts +29 -0
- package/dist/skills/pi-subagents/SKILL.md +1 -1
- package/dist/skills/pi-subagents/references/constraints-and-recipes.md +5 -4
- package/dist/skills/pi-subagents/references/execution-controls.md +15 -14
- package/dist/skills/pi-subagents/references/management-authoring-rpc.md +5 -5
- package/dist/skills/pi-subagents/references/prompting-and-roles.md +46 -26
- package/docs/settings.md +6 -0
- package/package.json +3 -3
|
@@ -214,6 +214,7 @@ Package skill content.
|
|
|
214
214
|
it("resolves configured artifact directory preferences", () => {
|
|
215
215
|
const sessionFile = path.join(agentDir, "sessions", "session-1", "session.jsonl");
|
|
216
216
|
|
|
217
|
+
// Selesai fork default: project-scoped .pi-subagents artifacts.
|
|
217
218
|
assert.equal(getArtifactsDir(sessionFile, cwd), getProjectArtifactsDir(cwd));
|
|
218
219
|
assert.equal(getArtifactsDir(sessionFile, cwd, "project"), getProjectArtifactsDir(cwd));
|
|
219
220
|
assert.equal(getArtifactsDir(sessionFile, cwd, "session"), path.join(path.dirname(sessionFile), "subagent-artifacts"));
|
|
@@ -241,6 +242,39 @@ Package skill content.
|
|
|
241
242
|
assert.throws(() => updateConfig((config) => config), /config\.fleetKeybindings\.pageUp entries must be non-empty strings/);
|
|
242
243
|
});
|
|
243
244
|
|
|
245
|
+
it("loads and validates the foreground detach shortcut", () => {
|
|
246
|
+
const configPath = path.join(agentDir, "extensions", "subagent", "config.json");
|
|
247
|
+
writeFile(configPath, JSON.stringify({ foregroundDetachShortcut: "ctrl+b" }));
|
|
248
|
+
assert.equal(loadConfig().foregroundDetachShortcut, "ctrl+b");
|
|
249
|
+
|
|
250
|
+
writeFile(configPath, JSON.stringify({ foregroundDetachShortcut: "banana" }));
|
|
251
|
+
assert.throws(() => updateConfig((config) => config), /config\.foregroundDetachShortcut must be a valid keybinding string/);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it("loads and validates main-window renderer density config", () => {
|
|
255
|
+
const configPath = path.join(agentDir, "extensions", "subagent", "config.json");
|
|
256
|
+
writeFile(configPath, JSON.stringify({ mainWindowRenderer: { horizontalSpacing: 0, compactResultMaxLines: 4 } }));
|
|
257
|
+
assert.deepEqual(loadConfig().mainWindowRenderer, { horizontalSpacing: 0, compactResultMaxLines: 4 });
|
|
258
|
+
|
|
259
|
+
writeFile(configPath, JSON.stringify({ mainWindowRenderer: { horizontalSpacing: -1 } }));
|
|
260
|
+
assert.throws(() => updateConfig((config) => config), /config\.mainWindowRenderer\.horizontalSpacing must be an integer from 0 to 4/);
|
|
261
|
+
|
|
262
|
+
writeFile(configPath, JSON.stringify({ mainWindowRenderer: { compactResultMaxLines: 0 } }));
|
|
263
|
+
assert.throws(() => updateConfig((config) => config), /config\.mainWindowRenderer\.compactResultMaxLines must be a positive integer/);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it("loads and validates experimental Orca progress-tab config", () => {
|
|
267
|
+
const configPath = path.join(agentDir, "extensions", "subagent", "config.json");
|
|
268
|
+
writeFile(configPath, JSON.stringify({ orcaProgressTabs: { enabled: true } }));
|
|
269
|
+
assert.deepEqual(loadConfig().orcaProgressTabs, { enabled: true });
|
|
270
|
+
|
|
271
|
+
writeFile(configPath, JSON.stringify({ orcaProgressTabs: { enabled: "yes" } }));
|
|
272
|
+
assert.throws(() => updateConfig((config) => config), /config\.orcaProgressTabs\.enabled must be a boolean/);
|
|
273
|
+
|
|
274
|
+
writeFile(configPath, JSON.stringify({ orcaProgressTabs: { enabled: true, focus: true } }));
|
|
275
|
+
assert.throws(() => updateConfig((config) => config), /config\.orcaProgressTabs\.focus is not supported/);
|
|
276
|
+
});
|
|
277
|
+
|
|
244
278
|
it("hardens and redacts existing run history while recording", () => {
|
|
245
279
|
const historyPath = path.join(agentDir, "run-history.jsonl");
|
|
246
280
|
fs.mkdirSync(agentDir, { recursive: true, mode: 0o755 });
|
|
@@ -5,7 +5,12 @@ import { normalizePublicSubagentExecution } from "../../src/extension/public-exe
|
|
|
5
5
|
describe("public subagent execution normalization", () => {
|
|
6
6
|
it("accepts all four execution modes, management, and direct-field schedules", () => {
|
|
7
7
|
assert.deepEqual(normalizePublicSubagentExecution({ workflowScript: "return 1" }), { ok: true, params: { workflowScript: "return 1" } });
|
|
8
|
-
|
|
8
|
+
const single = normalizePublicSubagentExecution({ agent: "worker", task: "work" });
|
|
9
|
+
assert.equal(single.ok, true);
|
|
10
|
+
if (single.ok) {
|
|
11
|
+
assert.equal(typeof (single.params as { workflowScript?: unknown }).workflowScript, "string");
|
|
12
|
+
assert.match((single.params as { workflowScript: string }).workflowScript, /runs\.run\("main", \{"agent":"worker","task":"work","output":true\}\)/);
|
|
13
|
+
}
|
|
9
14
|
assert.deepEqual(normalizePublicSubagentExecution({ tasks: [{ agent: "worker" }] }), { ok: true, params: { tasks: [{ agent: "worker" }] } });
|
|
10
15
|
assert.deepEqual(normalizePublicSubagentExecution({ chain: [{ agent: "worker" }] }), { ok: true, params: { chain: [{ agent: "worker" }] } });
|
|
11
16
|
assert.deepEqual(normalizePublicSubagentExecution({ action: " list " }), { ok: true, params: { action: "list" } });
|
|
@@ -80,6 +80,50 @@ test("multiline rendering omits two-column graphemes at one-column width", () =>
|
|
|
80
80
|
}
|
|
81
81
|
});
|
|
82
82
|
|
|
83
|
+
test("running single-subagent cards show the configured detach shortcut", () => {
|
|
84
|
+
const running = {
|
|
85
|
+
...result("reviewer", ""),
|
|
86
|
+
progress: { status: "running", index: 0, agent: "reviewer", toolCount: 0, tokens: 0, durationMs: 0 },
|
|
87
|
+
};
|
|
88
|
+
const toolResult = {
|
|
89
|
+
content: [{ type: "text", text: "running" }],
|
|
90
|
+
details: { mode: "single", results: [running] },
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const configured = componentText(renderSubagentResult(
|
|
94
|
+
toolResult as never,
|
|
95
|
+
{ expanded: false },
|
|
96
|
+
theme as any,
|
|
97
|
+
undefined,
|
|
98
|
+
undefined,
|
|
99
|
+
"ctrl+b",
|
|
100
|
+
));
|
|
101
|
+
assert.match(configured, /Ctrl\+B to run in background/);
|
|
102
|
+
|
|
103
|
+
const unconfigured = componentText(renderSubagentResult(toolResult as never, { expanded: false }, theme as any));
|
|
104
|
+
assert.doesNotMatch(unconfigured, /run in background/);
|
|
105
|
+
|
|
106
|
+
const alreadyBackground = componentText(renderSubagentResult(
|
|
107
|
+
{ ...toolResult, details: { ...toolResult.details, asyncId: "async-123" } } as never,
|
|
108
|
+
{ expanded: false },
|
|
109
|
+
theme as any,
|
|
110
|
+
undefined,
|
|
111
|
+
undefined,
|
|
112
|
+
"ctrl+b",
|
|
113
|
+
));
|
|
114
|
+
assert.doesNotMatch(alreadyBackground, /run in background/);
|
|
115
|
+
|
|
116
|
+
const pendingBackground = componentText(renderSubagentResult(
|
|
117
|
+
{ ...toolResult, details: { ...toolResult.details, background: true } } as never,
|
|
118
|
+
{ expanded: false },
|
|
119
|
+
theme as any,
|
|
120
|
+
undefined,
|
|
121
|
+
undefined,
|
|
122
|
+
"ctrl+b",
|
|
123
|
+
));
|
|
124
|
+
assert.doesNotMatch(pendingBackground, /run in background/);
|
|
125
|
+
});
|
|
126
|
+
|
|
83
127
|
test("compact chain rendering uses workflow graph spans for dynamic fanout results", () => {
|
|
84
128
|
const component = renderSubagentResult({
|
|
85
129
|
content: [{ type: "text", text: "done" }],
|
|
@@ -238,3 +282,51 @@ test("static sequential and static parallel chain rendering keep existing labels
|
|
|
238
282
|
assert.match(parallel, /Agent 2\/2: auditor/);
|
|
239
283
|
assert.match(parallel, /Step 3: writer/);
|
|
240
284
|
});
|
|
285
|
+
|
|
286
|
+
test("main-window renderer config removes compact result indentation without changing status glyphs", () => {
|
|
287
|
+
const component = renderSubagentResult({
|
|
288
|
+
content: [{ type: "text", text: "done" }],
|
|
289
|
+
details: {
|
|
290
|
+
mode: "parallel",
|
|
291
|
+
results: [result("scout", "a"), { ...result("reviewer", ""), exitCode: 1, error: "failed" }],
|
|
292
|
+
},
|
|
293
|
+
}, { expanded: false }, theme as any, undefined, { horizontalSpacing: 0 });
|
|
294
|
+
|
|
295
|
+
const text = componentText(component);
|
|
296
|
+
assert.match(text, /^✗ parallel/m);
|
|
297
|
+
assert.match(text, /^✓ Agent 1\/2: scout/m);
|
|
298
|
+
assert.match(text, /^✗ Agent 2\/2: reviewer/m);
|
|
299
|
+
assert.match(text, /^⎿ Error: failed/m);
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
test("main-window renderer config caps only collapsed rich result rows", () => {
|
|
303
|
+
const rendered = renderSubagentResult({
|
|
304
|
+
content: [{ type: "text", text: "done" }],
|
|
305
|
+
details: {
|
|
306
|
+
mode: "parallel",
|
|
307
|
+
results: [
|
|
308
|
+
result("scout", "a"),
|
|
309
|
+
result("reviewer", "b"),
|
|
310
|
+
result("writer", "c"),
|
|
311
|
+
],
|
|
312
|
+
},
|
|
313
|
+
}, { expanded: false }, theme as any, undefined, { compactResultMaxLines: 3 }).render(120);
|
|
314
|
+
|
|
315
|
+
assert.equal(rendered.length, 3);
|
|
316
|
+
assert.match(rendered[2]!, /rows hidden/);
|
|
317
|
+
|
|
318
|
+
const expanded = renderSubagentResult({
|
|
319
|
+
content: [{ type: "text", text: "done" }],
|
|
320
|
+
details: {
|
|
321
|
+
mode: "parallel",
|
|
322
|
+
results: [
|
|
323
|
+
result("scout", "a"),
|
|
324
|
+
result("reviewer", "b"),
|
|
325
|
+
result("writer", "c"),
|
|
326
|
+
],
|
|
327
|
+
},
|
|
328
|
+
}, { expanded: true }, theme as any, undefined, { compactResultMaxLines: 3 }).render(120);
|
|
329
|
+
|
|
330
|
+
assert.ok(expanded.length > 3);
|
|
331
|
+
assert.doesNotMatch(expanded.join("\n"), /rows hidden/);
|
|
332
|
+
});
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as os from "node:os";
|
|
4
|
+
import * as path from "node:path";
|
|
5
|
+
import { describe, it } from "node:test";
|
|
6
|
+
import { cleanupResultIndexes, removeResultIndex, resultCandidateFilesForSession, resultFilesForSession, resultPayloadPathForIndexedRun, resultPayloadPathForSessionRun, writeAsyncResultFile, writePendingAsyncResultFile, writeResultIndexForData } from "../../src/runs/background/result-files.ts";
|
|
7
|
+
|
|
8
|
+
function pendingPath(resultsDir: string, sessionId: string, runId: string): string {
|
|
9
|
+
return path.join(resultsDir, "result-pending", encodeURIComponent(sessionId), `${encodeURIComponent(runId)}.json`);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
describe("result file indexes", () => {
|
|
13
|
+
it("removes orphan index entries without deleting flat result files", () => {
|
|
14
|
+
const resultsDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-result-files-index-"));
|
|
15
|
+
try {
|
|
16
|
+
writeAsyncResultFile(path.join(resultsDir, "kept.json"), { id: "kept", runId: "kept", sessionId: "session-a", success: true });
|
|
17
|
+
writeAsyncResultFile(path.join(resultsDir, "missing.json"), { id: "missing", runId: "missing", sessionId: "session-a", toolCallId: "call-missing", success: true });
|
|
18
|
+
fs.rmSync(path.join(resultsDir, "missing.json"));
|
|
19
|
+
fs.writeFileSync(path.join(resultsDir, "unindexed.json"), JSON.stringify({ id: "unindexed", sessionId: "session-a" }), "utf-8");
|
|
20
|
+
|
|
21
|
+
assert.equal(cleanupResultIndexes(resultsDir, Date.now() + 86_400_001, 86_400_000), 2);
|
|
22
|
+
|
|
23
|
+
assert.deepEqual(resultFilesForSession(resultsDir, "session-a"), ["kept.json"]);
|
|
24
|
+
assert.equal(fs.existsSync(path.join(resultsDir, "kept.json")), true);
|
|
25
|
+
assert.equal(fs.existsSync(path.join(resultsDir, "unindexed.json")), true);
|
|
26
|
+
} finally {
|
|
27
|
+
fs.rmSync(resultsDir, { recursive: true, force: true });
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("promotes an indexed pending result payload", () => {
|
|
32
|
+
const resultsDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-result-files-pending-payload-"));
|
|
33
|
+
const originalError = console.error;
|
|
34
|
+
try {
|
|
35
|
+
console.error = () => {};
|
|
36
|
+
const resultPath = path.join(resultsDir, "late.json");
|
|
37
|
+
fs.mkdirSync(resultPath, { recursive: true });
|
|
38
|
+
|
|
39
|
+
assert.deepEqual(writeAsyncResultFile(resultPath, { id: "late", runId: "late", sessionId: "session-a", success: true }), { state: "pending" });
|
|
40
|
+
assert.equal(fs.existsSync(pendingPath(resultsDir, "session-a", "late")), true);
|
|
41
|
+
fs.rmSync(resultPath, { recursive: true, force: true });
|
|
42
|
+
|
|
43
|
+
assert.deepEqual(resultFilesForSession(resultsDir, "session-a"), ["late.json"]);
|
|
44
|
+
assert.equal(JSON.parse(fs.readFileSync(resultPath, "utf-8")).success, true);
|
|
45
|
+
assert.equal(fs.existsSync(pendingPath(resultsDir, "session-a", "late")), false);
|
|
46
|
+
} finally {
|
|
47
|
+
console.error = originalError;
|
|
48
|
+
fs.rmSync(resultsDir, { recursive: true, force: true });
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("writes an indexed pending result without publishing it", () => {
|
|
53
|
+
const resultsDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-result-files-pending-only-"));
|
|
54
|
+
const originalError = console.error;
|
|
55
|
+
try {
|
|
56
|
+
console.error = () => {};
|
|
57
|
+
const resultPath = path.join(resultsDir, "pending-only.json");
|
|
58
|
+
fs.mkdirSync(resultPath, { recursive: true });
|
|
59
|
+
|
|
60
|
+
writePendingAsyncResultFile(resultPath, { id: "pending-only", runId: "pending-only", sessionId: "session-a", success: true });
|
|
61
|
+
|
|
62
|
+
assert.equal(fs.statSync(resultPath).isDirectory(), true);
|
|
63
|
+
assert.deepEqual(resultFilesForSession(resultsDir, "session-a"), []);
|
|
64
|
+
assert.deepEqual(resultCandidateFilesForSession(resultsDir, "session-a"), ["pending-only.json"]);
|
|
65
|
+
assert.equal(resultPayloadPathForSessionRun(resultsDir, "session-a", "pending-only"), pendingPath(resultsDir, "session-a", "pending-only"));
|
|
66
|
+
assert.equal(resultPayloadPathForIndexedRun(resultsDir, "pending-only"), pendingPath(resultsDir, "session-a", "pending-only"));
|
|
67
|
+
} finally {
|
|
68
|
+
console.error = originalError;
|
|
69
|
+
fs.rmSync(resultsDir, { recursive: true, force: true });
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("keeps a legacy valid index while the result payload is not visible yet", () => {
|
|
74
|
+
const resultsDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-result-files-legacy-late-payload-"));
|
|
75
|
+
try {
|
|
76
|
+
const resultPath = path.join(resultsDir, "late.json");
|
|
77
|
+
writeResultIndexForData(resultPath, { id: "late", runId: "late", sessionId: "session-a", success: true });
|
|
78
|
+
|
|
79
|
+
assert.deepEqual(resultFilesForSession(resultsDir, "session-a"), []);
|
|
80
|
+
|
|
81
|
+
fs.writeFileSync(resultPath, JSON.stringify({ id: "late", runId: "late", sessionId: "session-a", success: true }), "utf-8");
|
|
82
|
+
assert.deepEqual(resultFilesForSession(resultsDir, "session-a"), ["late.json"]);
|
|
83
|
+
} finally {
|
|
84
|
+
fs.rmSync(resultsDir, { recursive: true, force: true });
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("keeps an unindexed pending result recoverable when its session index cannot be written", () => {
|
|
89
|
+
const resultsDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-result-files-index-failure-"));
|
|
90
|
+
try {
|
|
91
|
+
fs.writeFileSync(path.join(resultsDir, "result-index"), "not a directory", "utf-8");
|
|
92
|
+
const resultPath = path.join(resultsDir, "blocked.json");
|
|
93
|
+
|
|
94
|
+
assert.throws(() => writeAsyncResultFile(resultPath, { id: "blocked", runId: "blocked", sessionId: "session-a", success: true }));
|
|
95
|
+
assert.equal(fs.existsSync(resultPath), false);
|
|
96
|
+
assert.equal(fs.existsSync(pendingPath(resultsDir, "session-a", "blocked")), true);
|
|
97
|
+
assert.equal(resultPayloadPathForSessionRun(resultsDir, "session-a", "blocked"), pendingPath(resultsDir, "session-a", "blocked"));
|
|
98
|
+
assert.equal(resultPayloadPathForIndexedRun(resultsDir, "blocked"), pendingPath(resultsDir, "session-a", "blocked"));
|
|
99
|
+
assert.deepEqual(resultCandidateFilesForSession(resultsDir, "session-a"), ["blocked.json"]);
|
|
100
|
+
} finally {
|
|
101
|
+
fs.rmSync(resultsDir, { recursive: true, force: true });
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("does not commit a result payload without a session id", () => {
|
|
106
|
+
const resultsDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-result-files-no-session-"));
|
|
107
|
+
try {
|
|
108
|
+
const resultPath = path.join(resultsDir, "blocked.json");
|
|
109
|
+
|
|
110
|
+
assert.throws(() => writeAsyncResultFile(resultPath, { id: "blocked", runId: "blocked", success: true }), /sessionId/);
|
|
111
|
+
assert.equal(fs.existsSync(resultPath), false);
|
|
112
|
+
} finally {
|
|
113
|
+
fs.rmSync(resultsDir, { recursive: true, force: true });
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("prefers pending payload over an older public result", () => {
|
|
118
|
+
const resultsDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-result-files-pending-wins-"));
|
|
119
|
+
const originalError = console.error;
|
|
120
|
+
try {
|
|
121
|
+
console.error = () => {};
|
|
122
|
+
const resultPath = path.join(resultsDir, "blocked.json");
|
|
123
|
+
writeAsyncResultFile(resultPath, { id: "blocked", runId: "blocked", sessionId: "session-a", success: false });
|
|
124
|
+
fs.rmSync(resultPath, { force: true });
|
|
125
|
+
fs.mkdirSync(resultPath, { recursive: true });
|
|
126
|
+
|
|
127
|
+
assert.deepEqual(writeAsyncResultFile(resultPath, { id: "blocked", runId: "blocked", sessionId: "session-a", success: true }), { state: "pending" });
|
|
128
|
+
fs.rmSync(resultPath, { recursive: true, force: true });
|
|
129
|
+
fs.writeFileSync(resultPath, JSON.stringify({ id: "blocked", runId: "blocked", sessionId: "session-a", success: false }), "utf-8");
|
|
130
|
+
|
|
131
|
+
assert.deepEqual(resultFilesForSession(resultsDir, "session-a"), ["blocked.json"]);
|
|
132
|
+
assert.equal(JSON.parse(fs.readFileSync(resultPath, "utf-8")).success, true);
|
|
133
|
+
} finally {
|
|
134
|
+
console.error = originalError;
|
|
135
|
+
fs.rmSync(resultsDir, { recursive: true, force: true });
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("removes pending payloads with result indexes", () => {
|
|
140
|
+
const resultsDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-result-files-pending-cleanup-"));
|
|
141
|
+
const originalError = console.error;
|
|
142
|
+
try {
|
|
143
|
+
console.error = () => {};
|
|
144
|
+
const resultPath = path.join(resultsDir, "pending-cleanup.json");
|
|
145
|
+
fs.mkdirSync(resultPath, { recursive: true });
|
|
146
|
+
writeAsyncResultFile(resultPath, { id: "pending-cleanup", runId: "pending-cleanup", sessionId: "session-a", success: true });
|
|
147
|
+
|
|
148
|
+
assert.equal(fs.existsSync(pendingPath(resultsDir, "session-a", "pending-cleanup")), true);
|
|
149
|
+
removeResultIndex(resultsDir, "session-a", "pending-cleanup");
|
|
150
|
+
assert.equal(fs.existsSync(pendingPath(resultsDir, "session-a", "pending-cleanup")), false);
|
|
151
|
+
} finally {
|
|
152
|
+
console.error = originalError;
|
|
153
|
+
fs.rmSync(resultsDir, { recursive: true, force: true });
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it("commits a result payload when only an optional index fails", () => {
|
|
158
|
+
const resultsDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-result-files-optional-index-"));
|
|
159
|
+
const originalError = console.error;
|
|
160
|
+
try {
|
|
161
|
+
console.error = () => {};
|
|
162
|
+
fs.mkdirSync(path.join(resultsDir, "result-index"), { recursive: true });
|
|
163
|
+
fs.writeFileSync(path.join(resultsDir, "result-index", "tool-calls"), "not a directory", "utf-8");
|
|
164
|
+
const resultPath = path.join(resultsDir, "kept.json");
|
|
165
|
+
|
|
166
|
+
writeAsyncResultFile(resultPath, { id: "kept", runId: "kept", sessionId: "session-a", toolCallId: "call-a", success: true });
|
|
167
|
+
|
|
168
|
+
assert.equal(fs.existsSync(resultPath), true);
|
|
169
|
+
assert.deepEqual(resultFilesForSession(resultsDir, "session-a"), ["kept.json"]);
|
|
170
|
+
} finally {
|
|
171
|
+
console.error = originalError;
|
|
172
|
+
fs.rmSync(resultsDir, { recursive: true, force: true });
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
});
|
|
@@ -3,55 +3,165 @@ import * as fs from "node:fs";
|
|
|
3
3
|
import * as os from "node:os";
|
|
4
4
|
import * as path from "node:path";
|
|
5
5
|
import { describe, it } from "node:test";
|
|
6
|
-
import { listRetainedChildren } from "../../src/runs/background/retained-children.ts";
|
|
6
|
+
import { formatRetainedChildren, listRetainedChildren } from "../../src/runs/background/retained-children.ts";
|
|
7
|
+
import { createRunFanoutBudget } from "../../src/runs/shared/run-fanout-budget.ts";
|
|
7
8
|
|
|
8
|
-
|
|
9
|
+
interface WriteRunOptions {
|
|
10
|
+
sessionId?: string;
|
|
11
|
+
parentWorkflowRunId?: string | null;
|
|
12
|
+
state?: "complete" | "failed" | "paused" | "stopped";
|
|
13
|
+
stepStatus?: "complete" | "completed" | "failed" | "paused" | "stopped";
|
|
14
|
+
sessionFile?: "present" | "missing" | "omitted";
|
|
15
|
+
runner?: "external-cli";
|
|
16
|
+
recoveryDescriptor?: "present" | "missing" | "invalid";
|
|
17
|
+
recoverySourceRunId?: string;
|
|
18
|
+
recoveryAgent?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function writeRetainedRun(root: string, index: number, options: WriteRunOptions = {}): void {
|
|
9
22
|
const runId = `child-${index}`;
|
|
10
23
|
const sessionFile = path.join(root, "sessions", `${runId}.jsonl`);
|
|
11
|
-
|
|
12
|
-
|
|
24
|
+
if (options.sessionFile !== "missing" && options.sessionFile !== "omitted") {
|
|
25
|
+
fs.mkdirSync(path.dirname(sessionFile), { recursive: true });
|
|
26
|
+
fs.writeFileSync(sessionFile, "", "utf-8");
|
|
27
|
+
}
|
|
13
28
|
const asyncDir = path.join(root, "runs", runId);
|
|
14
29
|
fs.mkdirSync(asyncDir, { recursive: true });
|
|
30
|
+
const state = options.state ?? "complete";
|
|
31
|
+
const stepStatus = options.stepStatus ?? state;
|
|
15
32
|
fs.writeFileSync(path.join(asyncDir, "status.json"), JSON.stringify({
|
|
16
33
|
runId,
|
|
17
|
-
sessionId,
|
|
34
|
+
sessionId: options.sessionId ?? "parent-a",
|
|
18
35
|
mode: "single",
|
|
19
|
-
state
|
|
36
|
+
state,
|
|
20
37
|
startedAt: index,
|
|
21
38
|
endedAt: 1_000 + index,
|
|
22
39
|
lastUpdate: 1_000 + index,
|
|
23
|
-
|
|
40
|
+
cwd: root,
|
|
41
|
+
...(options.parentWorkflowRunId === null ? {} : { parentWorkflowRunId: options.parentWorkflowRunId ?? "workflow-a", workflowKey: `lane-${index}` }),
|
|
24
42
|
steps: [{
|
|
25
43
|
agent: "worker",
|
|
26
|
-
status:
|
|
44
|
+
status: stepStatus,
|
|
27
45
|
description: ` Task ${index} with spacing ${"x".repeat(140)} `,
|
|
28
46
|
endedAt: 1_000 + index,
|
|
29
|
-
|
|
47
|
+
...(options.runner === "external-cli" ? { runner: { type: "external-cli", command: "codex", args: [], promptDelivery: "stdin", capabilities: { stop: true, steer: false, resume: false, structuredOutput: false, toolEvents: false } } } : {}),
|
|
48
|
+
...(options.sessionFile === "omitted" ? {} : { sessionFile }),
|
|
30
49
|
tokens: { input: index, output: index + 1, total: index * 2 + 1 },
|
|
31
50
|
}],
|
|
32
51
|
}), "utf-8");
|
|
52
|
+
if (options.recoveryDescriptor === "invalid") {
|
|
53
|
+
fs.writeFileSync(path.join(asyncDir, "recovery-descriptor.json"), JSON.stringify({
|
|
54
|
+
version: 2,
|
|
55
|
+
sourceRunId: runId,
|
|
56
|
+
agent: "worker",
|
|
57
|
+
cwd: root,
|
|
58
|
+
systemPromptMode: "replace",
|
|
59
|
+
outputMode: "inline",
|
|
60
|
+
}), "utf-8");
|
|
61
|
+
} else if (options.recoveryDescriptor !== "missing") {
|
|
62
|
+
fs.writeFileSync(path.join(asyncDir, "recovery-descriptor.json"), JSON.stringify({
|
|
63
|
+
version: 1,
|
|
64
|
+
runFanoutBudget: createRunFanoutBudget(runId, 64),
|
|
65
|
+
sourceRunId: options.recoverySourceRunId ?? runId,
|
|
66
|
+
agent: options.recoveryAgent ?? "worker",
|
|
67
|
+
cwd: root,
|
|
68
|
+
systemPromptMode: "replace",
|
|
69
|
+
inheritProjectContext: false,
|
|
70
|
+
inheritSkills: false,
|
|
71
|
+
outputMode: "inline",
|
|
72
|
+
maxSubagentDepth: 2,
|
|
73
|
+
share: false,
|
|
74
|
+
}), "utf-8");
|
|
75
|
+
}
|
|
33
76
|
}
|
|
34
77
|
|
|
35
78
|
describe("retained child roster", () => {
|
|
36
|
-
it("returns
|
|
79
|
+
it("returns terminal workflow children newest first and formats a 10-row roster", () => {
|
|
37
80
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-retained-children-"));
|
|
38
81
|
try {
|
|
39
82
|
for (let index = 0; index < 12; index++) writeRetainedRun(root, index);
|
|
40
|
-
writeRetainedRun(root, 50, "parent-b");
|
|
41
|
-
writeRetainedRun(root, 51,
|
|
83
|
+
writeRetainedRun(root, 50, { sessionId: "parent-b" });
|
|
84
|
+
writeRetainedRun(root, 51, { parentWorkflowRunId: null });
|
|
85
|
+
writeRetainedRun(root, 52, { parentWorkflowRunId: "workflow-failed", state: "failed" });
|
|
42
86
|
|
|
43
87
|
const children = listRetainedChildren(path.join(root, "runs"), "parent-a");
|
|
88
|
+
const formatted = formatRetainedChildren(children);
|
|
44
89
|
|
|
45
|
-
assert.equal(children.length,
|
|
46
|
-
assert.deepEqual(children.map((child) => child.runId), Array.from({ length:
|
|
90
|
+
assert.equal(children.length, 13);
|
|
91
|
+
assert.deepEqual(children.map((child) => child.runId), ["child-52", ...Array.from({ length: 12 }, (_, offset) => `child-${11 - offset}`)]);
|
|
92
|
+
assert.equal(formatted.match(/^- child-/gm)?.length, 10);
|
|
93
|
+
assert.doesNotMatch(formatted, /^- child-2 /m);
|
|
47
94
|
assert.equal(children[0]?.agent, "worker");
|
|
48
|
-
assert.equal(children[0]?.completedAt,
|
|
95
|
+
assert.equal(children[0]?.completedAt, 1_052);
|
|
96
|
+
assert.equal(children[0]?.state, "failed");
|
|
97
|
+
assert.equal(children[0]?.parentRunId, "workflow-failed");
|
|
98
|
+
assert.equal(children[0]?.workflowKey, "lane-52");
|
|
49
99
|
assert.ok((children[0]?.taskSummary.length ?? 0) <= 120);
|
|
50
|
-
assert.equal(children[0]?.taskSummary.startsWith("Task
|
|
51
|
-
assert.deepEqual(children[0]?.tokenTotals, { input:
|
|
100
|
+
assert.equal(children[0]?.taskSummary.startsWith("Task 52 with spacing"), true);
|
|
101
|
+
assert.deepEqual(children[0]?.tokenTotals, { input: 52, output: 53, total: 105 });
|
|
102
|
+
assert.equal(children[0]?.resumability.state, "resumable");
|
|
103
|
+
assert.match(formatted, /resumability: resumable\n session: .*child-52\.jsonl\n resume: subagent\(\{ action: "resume", id: "child-52", message: "\.\.\." \}\)/);
|
|
52
104
|
assert.equal(children.some((child) => child.runId === "child-50" || child.runId === "child-51"), false);
|
|
53
105
|
} finally {
|
|
54
106
|
fs.rmSync(root, { recursive: true, force: true });
|
|
55
107
|
}
|
|
56
108
|
});
|
|
109
|
+
|
|
110
|
+
it("keeps an older resumable child visible behind ten newer non-resumable children", () => {
|
|
111
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-retained-children-"));
|
|
112
|
+
try {
|
|
113
|
+
writeRetainedRun(root, 0);
|
|
114
|
+
for (let index = 1; index <= 10; index++) writeRetainedRun(root, index, { recoveryDescriptor: "missing" });
|
|
115
|
+
|
|
116
|
+
const children = listRetainedChildren(path.join(root, "runs"), "parent-a");
|
|
117
|
+
const formatted = formatRetainedChildren(children);
|
|
118
|
+
|
|
119
|
+
assert.equal(children.length, 11);
|
|
120
|
+
assert.deepEqual(children.map((child) => child.runId), Array.from({ length: 11 }, (_, offset) => `child-${10 - offset}`));
|
|
121
|
+
assert.equal(formatted.match(/^- child-/gm)?.length, 10);
|
|
122
|
+
assert.match(formatted, /resume: subagent\(\{ action: "resume", id: "child-0"/);
|
|
123
|
+
assert.doesNotMatch(formatted, /No resumable retained child is listed/);
|
|
124
|
+
} finally {
|
|
125
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("keeps non-resumable retained children visible with exact reasons", () => {
|
|
130
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-retained-children-"));
|
|
131
|
+
try {
|
|
132
|
+
writeRetainedRun(root, 1, { sessionFile: "omitted" });
|
|
133
|
+
writeRetainedRun(root, 2, { sessionFile: "missing" });
|
|
134
|
+
writeRetainedRun(root, 3, { state: "stopped", stepStatus: "stopped" });
|
|
135
|
+
writeRetainedRun(root, 4, { runner: "external-cli" });
|
|
136
|
+
writeRetainedRun(root, 5, { recoveryDescriptor: "missing" });
|
|
137
|
+
writeRetainedRun(root, 6, { recoveryDescriptor: "invalid" });
|
|
138
|
+
writeRetainedRun(root, 7, { recoverySourceRunId: "other-run" });
|
|
139
|
+
writeRetainedRun(root, 8, { recoveryAgent: "reviewer" });
|
|
140
|
+
|
|
141
|
+
const children = listRetainedChildren(path.join(root, "runs"), "parent-a");
|
|
142
|
+
const formatted = formatRetainedChildren(children);
|
|
143
|
+
|
|
144
|
+
assert.deepEqual(children.map((child) => [child.runId, child.resumability]), [
|
|
145
|
+
["child-8", { state: "not-resumable", reason: "recovery descriptor belongs to agent reviewer" }],
|
|
146
|
+
["child-7", { state: "not-resumable", reason: "recovery descriptor belongs to run other-run" }],
|
|
147
|
+
["child-6", { state: "not-resumable", reason: `invalid recovery descriptor: Invalid async recovery descriptor '${path.join(root, "runs", "child-6", "recovery-descriptor.json")}': version must be 1.` }],
|
|
148
|
+
["child-5", { state: "not-resumable", reason: "missing recovery descriptor" }],
|
|
149
|
+
["child-4", { state: "not-resumable", reason: "external CLI runner" }],
|
|
150
|
+
["child-3", { state: "not-resumable", reason: "stopped run" }],
|
|
151
|
+
["child-2", { state: "not-resumable", reason: `persisted session file is missing: ${path.join(root, "sessions", "child-2.jsonl")}` }],
|
|
152
|
+
["child-1", { state: "not-resumable", reason: "no persisted session file" }],
|
|
153
|
+
]);
|
|
154
|
+
assert.match(formatted, /resumability: not resumable \(recovery descriptor belongs to agent reviewer\)/);
|
|
155
|
+
assert.match(formatted, /resumability: not resumable \(recovery descriptor belongs to run other-run\)/);
|
|
156
|
+
assert.match(formatted, /resumability: not resumable \(invalid recovery descriptor:/);
|
|
157
|
+
assert.match(formatted, /resumability: not resumable \(missing recovery descriptor\)/);
|
|
158
|
+
assert.match(formatted, /resumability: not resumable \(external CLI runner\)/);
|
|
159
|
+
assert.match(formatted, /resumability: not resumable \(stopped run\)/);
|
|
160
|
+
assert.match(formatted, /resumability: not resumable \(no persisted session file\)/);
|
|
161
|
+
assert.doesNotMatch(formatted, /resume: subagent/);
|
|
162
|
+
assert.match(formatted, /No resumable retained child is listed\. Launch a same-role fallback challenge and label it as fallback\./);
|
|
163
|
+
} finally {
|
|
164
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
165
|
+
}
|
|
166
|
+
});
|
|
57
167
|
});
|
|
@@ -4,6 +4,8 @@ import * as os from "node:os";
|
|
|
4
4
|
import * as path from "node:path";
|
|
5
5
|
import { afterEach, describe, it } from "node:test";
|
|
6
6
|
import type { SubagentState } from "../../src/shared/types.ts";
|
|
7
|
+
import { releaseActiveRunIndex, updateActiveRunIndex } from "../../src/runs/background/active-run-index.ts";
|
|
8
|
+
import { resultFilePath, writeAsyncResultFile } from "../../src/runs/background/result-files.ts";
|
|
7
9
|
import { resolveSubagentRunId } from "../../src/runs/background/run-id-resolver.ts";
|
|
8
10
|
import { createNestedRoute, writeNestedEvent } from "../../src/runs/shared/nested-events.ts";
|
|
9
11
|
|
|
@@ -139,6 +141,48 @@ describe("subagent run id resolver", () => {
|
|
|
139
141
|
}
|
|
140
142
|
});
|
|
141
143
|
|
|
144
|
+
it("does not resolve unindexed result files by prefix", () => {
|
|
145
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-run-id-result-prefix-"));
|
|
146
|
+
try {
|
|
147
|
+
const asyncRoot = path.join(root, "runs");
|
|
148
|
+
const resultsDir = path.join(root, "results");
|
|
149
|
+
fs.mkdirSync(resultsDir, { recursive: true });
|
|
150
|
+
fs.writeFileSync(path.join(resultsDir, "legacy-run.json"), JSON.stringify({ id: "legacy-run", sessionId: "session-a", success: true }), "utf-8");
|
|
151
|
+
|
|
152
|
+
const exact = resolveSubagentRunId("legacy-run", { asyncDirRoot: asyncRoot, resultsDir });
|
|
153
|
+
assert.equal(exact?.kind, "async");
|
|
154
|
+
assert.equal(exact?.id, "legacy-run");
|
|
155
|
+
assert.equal(resolveSubagentRunId("legacy", { asyncDirRoot: asyncRoot, resultsDir }), undefined);
|
|
156
|
+
} finally {
|
|
157
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it("resolves workflow tool-call ids through active and result indexes", () => {
|
|
162
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-run-resolver-tool-call-index-"));
|
|
163
|
+
try {
|
|
164
|
+
const asyncRoot = path.join(root, "async");
|
|
165
|
+
const resultsDir = path.join(root, "results");
|
|
166
|
+
const activeDir = path.join(asyncRoot, "workflow-active");
|
|
167
|
+
fs.mkdirSync(activeDir, { recursive: true });
|
|
168
|
+
fs.writeFileSync(path.join(activeDir, "status.json"), JSON.stringify({ runId: "workflow-active", toolCallId: "call-active", state: "running", mode: "workflow", startedAt: 1, lastUpdate: 1, steps: [] }), "utf-8");
|
|
169
|
+
updateActiveRunIndex(activeDir, "running", "call-active");
|
|
170
|
+
|
|
171
|
+
const active = resolveSubagentRunId("call-active", { asyncDirRoot: asyncRoot, resultsDir });
|
|
172
|
+
assert.equal(active?.kind, "async");
|
|
173
|
+
assert.equal(active?.id, "workflow-active");
|
|
174
|
+
releaseActiveRunIndex(activeDir);
|
|
175
|
+
assert.equal(resolveSubagentRunId("call-active", { asyncDirRoot: asyncRoot, resultsDir }), undefined);
|
|
176
|
+
|
|
177
|
+
writeAsyncResultFile(resultFilePath(resultsDir, "workflow-done"), { id: "workflow-done", runId: "workflow-done", toolCallId: "call-done", sessionId: "session-a", state: "complete", success: true });
|
|
178
|
+
const done = resolveSubagentRunId("call-done", { asyncDirRoot: asyncRoot, resultsDir });
|
|
179
|
+
assert.equal(done?.kind, "async");
|
|
180
|
+
assert.equal(done?.id, "workflow-done");
|
|
181
|
+
} finally {
|
|
182
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
|
|
142
186
|
it("rejects unsafe nested id tokens before lookup", () => {
|
|
143
187
|
assert.throws(() => resolveSubagentRunId("../run"), /safe id token/);
|
|
144
188
|
assert.throws(() => resolveSubagentRunId("a/b"), /safe id token/);
|