@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
|
@@ -5,7 +5,8 @@ import * as path from "node:path";
|
|
|
5
5
|
import { describe, it } from "node:test";
|
|
6
6
|
import type { ExtensionContext } from "@selesai/code";
|
|
7
7
|
import { buildCompletionKey } from "../../src/runs/background/completion-dedupe.ts";
|
|
8
|
-
import { createResultWatcher } from "../../src/runs/background/result-watcher.ts";
|
|
8
|
+
import { createResultWatcher as createRawResultWatcher } from "../../src/runs/background/result-watcher.ts";
|
|
9
|
+
import { writeAsyncResultFile, writePendingAsyncResultFile } from "../../src/runs/background/result-files.ts";
|
|
9
10
|
import { createScheduledRunManager, scheduledRunStorePath } from "../../src/runs/background/scheduled-runs.ts";
|
|
10
11
|
import { prepareMissionLaunch, writeMissionAsyncBinding } from "../../src/missions/lifecycle.ts";
|
|
11
12
|
import { readMission, updateMission } from "../../src/missions/store.ts";
|
|
@@ -32,6 +33,24 @@ function createState(): SubagentState {
|
|
|
32
33
|
};
|
|
33
34
|
}
|
|
34
35
|
|
|
36
|
+
function createResultWatcher(
|
|
37
|
+
pi: Parameters<typeof createRawResultWatcher>[0],
|
|
38
|
+
state: Parameters<typeof createRawResultWatcher>[1],
|
|
39
|
+
resultsDir: Parameters<typeof createRawResultWatcher>[2],
|
|
40
|
+
completionTtlMs: Parameters<typeof createRawResultWatcher>[3],
|
|
41
|
+
deps: Parameters<typeof createRawResultWatcher>[4] = {},
|
|
42
|
+
): ReturnType<typeof createRawResultWatcher> {
|
|
43
|
+
return createRawResultWatcher(pi, state, resultsDir, completionTtlMs, { coalesceDelayMs: 0, ...deps });
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function writeIndexedResult(filePath: string, data: Record<string, unknown>): void {
|
|
47
|
+
writeAsyncResultFile(filePath, data);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function pendingResultPath(resultsDir: string, sessionId: string, runId: string): string {
|
|
51
|
+
return path.join(resultsDir, "result-pending", encodeURIComponent(sessionId), `${encodeURIComponent(runId)}.json`);
|
|
52
|
+
}
|
|
53
|
+
|
|
35
54
|
async function waitForPredicate(predicate: () => boolean, timeoutMs = 2_500): Promise<boolean> {
|
|
36
55
|
const deadline = Date.now() + timeoutMs;
|
|
37
56
|
while (!predicate()) {
|
|
@@ -55,24 +74,25 @@ describe("result watcher", () => {
|
|
|
55
74
|
},
|
|
56
75
|
};
|
|
57
76
|
const state = createState();
|
|
77
|
+
state.asyncJobs.set("session-run", { asyncId: "session-run", asyncDir: path.join(resultsDir, "session-run"), status: "running", startedAt: Date.now(), updatedAt: Date.now() });
|
|
58
78
|
const resultPath = path.join(resultsDir, "session-run.json");
|
|
59
|
-
|
|
79
|
+
writeIndexedResult(resultPath, {
|
|
60
80
|
id: "session-run",
|
|
61
81
|
sessionId: "session-current",
|
|
62
82
|
success: true,
|
|
63
83
|
summary: "done",
|
|
64
|
-
})
|
|
84
|
+
});
|
|
65
85
|
|
|
66
86
|
const watcher = createResultWatcher(pi, state, resultsDir, 60_000);
|
|
67
87
|
try {
|
|
68
88
|
watcher.primeExistingResults();
|
|
69
|
-
await new Promise((resolve) => setTimeout(resolve,
|
|
89
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
70
90
|
assert.equal(emitted.length, 0);
|
|
71
91
|
assert.equal(fs.existsSync(resultPath), true);
|
|
72
92
|
|
|
73
93
|
state.currentSessionId = "session-current";
|
|
74
94
|
watcher.primeExistingResults();
|
|
75
|
-
await new Promise((resolve) => setTimeout(resolve,
|
|
95
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
76
96
|
} finally {
|
|
77
97
|
watcher.stopResultWatcher();
|
|
78
98
|
}
|
|
@@ -84,32 +104,22 @@ describe("result watcher", () => {
|
|
|
84
104
|
}
|
|
85
105
|
});
|
|
86
106
|
|
|
87
|
-
it("
|
|
107
|
+
it("includes scheduled observer ids while priming current-session results", async () => {
|
|
88
108
|
const resultsDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-result-watcher-current-prime-"));
|
|
89
109
|
try {
|
|
90
110
|
const state = createState();
|
|
91
111
|
state.currentSessionId = "session-current";
|
|
92
|
-
let
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
sessionId: "session-current",
|
|
96
|
-
success: true,
|
|
97
|
-
summary: "done",
|
|
98
|
-
}), "utf-8");
|
|
99
|
-
const watcher = createResultWatcher({
|
|
100
|
-
events: {
|
|
101
|
-
on: () => () => {},
|
|
102
|
-
emit() {},
|
|
103
|
-
},
|
|
104
|
-
}, state, resultsDir, 60_000, {
|
|
112
|
+
let observedRunIdLookups = 0;
|
|
113
|
+
writeIndexedResult(path.join(resultsDir, "current.json"), { id: "current", sessionId: "session-current", success: true, summary: "done" });
|
|
114
|
+
const watcher = createResultWatcher({ events: { on: () => () => {}, emit() {} } }, state, resultsDir, 60_000, {
|
|
105
115
|
deliverIntercomResults: false,
|
|
106
116
|
observedCompletionRunIds() {
|
|
107
|
-
|
|
108
|
-
return [];
|
|
117
|
+
observedRunIdLookups += 1;
|
|
118
|
+
return ["scheduled-a"];
|
|
109
119
|
},
|
|
110
120
|
});
|
|
111
121
|
watcher.primeExistingResults();
|
|
112
|
-
assert.equal(
|
|
122
|
+
assert.equal(observedRunIdLookups, 1);
|
|
113
123
|
watcher.stopResultWatcher();
|
|
114
124
|
} finally {
|
|
115
125
|
fs.rmSync(resultsDir, { recursive: true, force: true });
|
|
@@ -133,13 +143,13 @@ describe("result watcher", () => {
|
|
|
133
143
|
unref() {},
|
|
134
144
|
} as fs.FSWatcher;
|
|
135
145
|
const writeResult = (id: string, sessionId: string) => {
|
|
136
|
-
|
|
146
|
+
writeIndexedResult(path.join(resultsDir, `${id}.json`), {
|
|
137
147
|
id,
|
|
138
148
|
results: [{ structuredOutput: { sessionId: "nested-not-owner" } }],
|
|
139
149
|
sessionId,
|
|
140
150
|
success: true,
|
|
141
151
|
summary: "done",
|
|
142
|
-
})
|
|
152
|
+
});
|
|
143
153
|
};
|
|
144
154
|
writeResult("startup-current", "session-current");
|
|
145
155
|
writeResult("startup-stale", "session-stale");
|
|
@@ -224,7 +234,7 @@ describe("result watcher", () => {
|
|
|
224
234
|
const binding = prepareMissionLaunch({
|
|
225
235
|
params: { mission: { title: "Workflow mission" }, task: "Run async child" },
|
|
226
236
|
projectRoot: project,
|
|
227
|
-
config: { globalIndexDir: path.join(root, "global-index") },
|
|
237
|
+
config: { directory: path.join(root, "missions"), globalIndexDir: path.join(root, "global-index") },
|
|
228
238
|
ownerSessionId: "session-current",
|
|
229
239
|
});
|
|
230
240
|
assert.ok(binding);
|
|
@@ -239,7 +249,7 @@ describe("result watcher", () => {
|
|
|
239
249
|
heartbeat: { status: "running" },
|
|
240
250
|
}],
|
|
241
251
|
});
|
|
242
|
-
|
|
252
|
+
writeIndexedResult(path.join(resultsDir, "async-child.json"), {
|
|
243
253
|
id: "async-child",
|
|
244
254
|
runId: "async-child",
|
|
245
255
|
sessionId: "session-current",
|
|
@@ -251,7 +261,7 @@ describe("result watcher", () => {
|
|
|
251
261
|
parentWorkflowRunId: "workflow-1",
|
|
252
262
|
workflowKey: "background",
|
|
253
263
|
results: [{ agent: "worker", success: true, output: "done", artifactPaths: { outputPath } }],
|
|
254
|
-
})
|
|
264
|
+
});
|
|
255
265
|
const state = createState();
|
|
256
266
|
state.currentSessionId = "session-current";
|
|
257
267
|
const watcher = createResultWatcher({ events: { on: () => () => {}, emit() {} } }, state, resultsDir, 60_000, {
|
|
@@ -259,7 +269,7 @@ describe("result watcher", () => {
|
|
|
259
269
|
});
|
|
260
270
|
try {
|
|
261
271
|
watcher.primeExistingResults();
|
|
262
|
-
await new Promise((resolve) => setTimeout(resolve,
|
|
272
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
263
273
|
} finally {
|
|
264
274
|
watcher.stopResultWatcher();
|
|
265
275
|
}
|
|
@@ -279,6 +289,91 @@ describe("result watcher", () => {
|
|
|
279
289
|
}
|
|
280
290
|
});
|
|
281
291
|
|
|
292
|
+
it("keeps result files until failed completion observers retry successfully", async () => {
|
|
293
|
+
const resultsDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-result-watcher-observer-retry-"));
|
|
294
|
+
const originalError = console.error;
|
|
295
|
+
try {
|
|
296
|
+
console.error = () => {};
|
|
297
|
+
const resultPath = path.join(resultsDir, "observer-retry.json");
|
|
298
|
+
writeIndexedResult(resultPath, { id: "observer-retry", runId: "observer-retry", sessionId: "session-current", success: true, summary: "done" });
|
|
299
|
+
const state = createState();
|
|
300
|
+
state.currentSessionId = "session-current";
|
|
301
|
+
let observerCalls = 0;
|
|
302
|
+
let deliveries = 0;
|
|
303
|
+
const watcher = createResultWatcher({ events: { on: () => () => {}, emit() {} } }, state, resultsDir, 60_000, {
|
|
304
|
+
observeCompletion: () => {
|
|
305
|
+
observerCalls += 1;
|
|
306
|
+
if (observerCalls === 1) throw new Error("observer unavailable");
|
|
307
|
+
},
|
|
308
|
+
notifier: { deliver: async () => { deliveries += 1; return true; } },
|
|
309
|
+
});
|
|
310
|
+
try {
|
|
311
|
+
watcher.primeExistingResults();
|
|
312
|
+
assert.equal(await waitForPredicate(() => !fs.existsSync(resultPath) && observerCalls >= 2), true);
|
|
313
|
+
} finally {
|
|
314
|
+
watcher.stopResultWatcher();
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
assert.equal(observerCalls, 2);
|
|
318
|
+
assert.equal(deliveries, 1);
|
|
319
|
+
} finally {
|
|
320
|
+
console.error = originalError;
|
|
321
|
+
fs.rmSync(resultsDir, { recursive: true, force: true });
|
|
322
|
+
}
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
it("does not redeliver user notification after reload while observer retry is pending", async () => {
|
|
326
|
+
const resultsDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-result-watcher-observer-reload-"));
|
|
327
|
+
const originalError = console.error;
|
|
328
|
+
try {
|
|
329
|
+
console.error = () => {};
|
|
330
|
+
const resultPath = path.join(resultsDir, "observer-reload.json");
|
|
331
|
+
writeIndexedResult(resultPath, { id: "observer-reload", runId: "observer-reload", sessionId: "session-current", success: true, summary: "done" });
|
|
332
|
+
let observerCalls = 0;
|
|
333
|
+
let deliveries = 0;
|
|
334
|
+
let emitted = 0;
|
|
335
|
+
const pi = { events: { on: () => () => {}, emit: () => { emitted += 1; } } };
|
|
336
|
+
const firstState = createState();
|
|
337
|
+
firstState.currentSessionId = "session-current";
|
|
338
|
+
const firstWatcher = createResultWatcher(pi, firstState, resultsDir, 60_000, {
|
|
339
|
+
observeCompletion: () => {
|
|
340
|
+
observerCalls += 1;
|
|
341
|
+
throw new Error("observer unavailable");
|
|
342
|
+
},
|
|
343
|
+
notifier: { deliver: async () => { deliveries += 1; return true; } },
|
|
344
|
+
});
|
|
345
|
+
try {
|
|
346
|
+
firstWatcher.primeExistingResults();
|
|
347
|
+
assert.equal(await waitForPredicate(() => {
|
|
348
|
+
if (deliveries !== 1 || !fs.existsSync(resultPath)) return false;
|
|
349
|
+
return typeof (JSON.parse(fs.readFileSync(resultPath, "utf-8")) as { notificationDeliveredAt?: unknown }).notificationDeliveredAt === "number";
|
|
350
|
+
}), true);
|
|
351
|
+
} finally {
|
|
352
|
+
firstWatcher.stopResultWatcher();
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const secondState = createState();
|
|
356
|
+
secondState.currentSessionId = "session-current";
|
|
357
|
+
const secondWatcher = createResultWatcher(pi, secondState, resultsDir, 60_000, {
|
|
358
|
+
observeCompletion: () => { observerCalls += 1; },
|
|
359
|
+
notifier: { deliver: async () => { deliveries += 1; return true; } },
|
|
360
|
+
});
|
|
361
|
+
try {
|
|
362
|
+
secondWatcher.primeExistingResults();
|
|
363
|
+
assert.equal(await waitForPredicate(() => !fs.existsSync(resultPath)), true);
|
|
364
|
+
} finally {
|
|
365
|
+
secondWatcher.stopResultWatcher();
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
assert.equal(observerCalls, 2);
|
|
369
|
+
assert.equal(deliveries, 1);
|
|
370
|
+
assert.equal(emitted, 1);
|
|
371
|
+
} finally {
|
|
372
|
+
console.error = originalError;
|
|
373
|
+
fs.rmSync(resultsDir, { recursive: true, force: true });
|
|
374
|
+
}
|
|
375
|
+
});
|
|
376
|
+
|
|
282
377
|
it("observes retained-project completions without changing active-session delivery", async () => {
|
|
283
378
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-result-watcher-scheduled-"));
|
|
284
379
|
const resultsDir = path.join(root, "results");
|
|
@@ -308,7 +403,7 @@ describe("result watcher", () => {
|
|
|
308
403
|
const state = createState();
|
|
309
404
|
state.currentSessionId = "session-b";
|
|
310
405
|
const resultPath = path.join(resultsDir, "scheduled-a.json");
|
|
311
|
-
|
|
406
|
+
writeIndexedResult(resultPath, { id: "scheduled-a", sessionId: "session-a", success: true, summary: "done" });
|
|
312
407
|
const watcher = createResultWatcher({
|
|
313
408
|
events: {
|
|
314
409
|
on: () => () => {},
|
|
@@ -316,12 +411,13 @@ describe("result watcher", () => {
|
|
|
316
411
|
},
|
|
317
412
|
}, state, resultsDir, 60_000, {
|
|
318
413
|
observeCompletion: (result) => manager.handleAsyncCompletion(result),
|
|
414
|
+
observedCompletionRunIds: () => manager.observedCompletionRunIds(),
|
|
319
415
|
notifier: { deliver: async () => assert.fail("inactive-session completion must not reach the live notifier") },
|
|
320
416
|
});
|
|
321
417
|
try {
|
|
322
418
|
watcher.startResultWatcher();
|
|
323
419
|
watcher.primeExistingResults();
|
|
324
|
-
await new Promise((resolve) => setTimeout(resolve,
|
|
420
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
325
421
|
} finally {
|
|
326
422
|
watcher.stopResultWatcher();
|
|
327
423
|
}
|
|
@@ -337,6 +433,192 @@ describe("result watcher", () => {
|
|
|
337
433
|
}
|
|
338
434
|
});
|
|
339
435
|
|
|
436
|
+
it("delivers indexed pending results during reload when public promotion is blocked", async () => {
|
|
437
|
+
const resultsDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-result-watcher-pending-index-"));
|
|
438
|
+
const originalError = console.error;
|
|
439
|
+
try {
|
|
440
|
+
console.error = () => {};
|
|
441
|
+
const emitted: Array<{ event: string; data: unknown }> = [];
|
|
442
|
+
const delivered: unknown[] = [];
|
|
443
|
+
const state = createState();
|
|
444
|
+
state.currentSessionId = "session-current";
|
|
445
|
+
const resultPath = path.join(resultsDir, "pending-run.json");
|
|
446
|
+
fs.mkdirSync(resultPath, { recursive: true });
|
|
447
|
+
writePendingAsyncResultFile(resultPath, {
|
|
448
|
+
id: "pending-run",
|
|
449
|
+
runId: "pending-run",
|
|
450
|
+
sessionId: "session-current",
|
|
451
|
+
success: true,
|
|
452
|
+
state: "complete",
|
|
453
|
+
summary: "done from pending",
|
|
454
|
+
});
|
|
455
|
+
const watcher = createResultWatcher({
|
|
456
|
+
events: {
|
|
457
|
+
on: () => () => {},
|
|
458
|
+
emit(event: string, data: unknown) { emitted.push({ event, data }); },
|
|
459
|
+
},
|
|
460
|
+
}, state, resultsDir, 60_000, {
|
|
461
|
+
deliverIntercomResults: false,
|
|
462
|
+
notifier: { deliver: async (result) => { delivered.push(result); return true; } },
|
|
463
|
+
});
|
|
464
|
+
try {
|
|
465
|
+
watcher.primeExistingResults();
|
|
466
|
+
assert.equal(await waitForPredicate(() => emitted.some((entry) => entry.event === "subagent:async-complete")), true);
|
|
467
|
+
} finally {
|
|
468
|
+
watcher.stopResultWatcher();
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
assert.equal((delivered[0] as { summary?: string } | undefined)?.summary, "done from pending");
|
|
472
|
+
assert.equal(fs.existsSync(pendingResultPath(resultsDir, "session-current", "pending-run")), false);
|
|
473
|
+
assert.equal(fs.statSync(resultPath).isDirectory(), true);
|
|
474
|
+
} finally {
|
|
475
|
+
console.error = originalError;
|
|
476
|
+
fs.rmSync(resultsDir, { recursive: true, force: true });
|
|
477
|
+
}
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
it("uses indexed result files and ignores unindexed stale files during reload", async () => {
|
|
481
|
+
const resultsDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-result-watcher-index-"));
|
|
482
|
+
try {
|
|
483
|
+
for (let i = 0; i < 300; i += 1) {
|
|
484
|
+
fs.writeFileSync(path.join(resultsDir, `stale-${i}.json`), JSON.stringify({ id: `stale-${i}`, sessionId: "session-stale", success: true, summary: "done" }), "utf-8");
|
|
485
|
+
}
|
|
486
|
+
writeIndexedResult(path.join(resultsDir, "current.json"), { id: "current", runId: "current", sessionId: "session-current", success: true, summary: "done" });
|
|
487
|
+
const parsedIds: string[] = [];
|
|
488
|
+
const state = createState();
|
|
489
|
+
state.currentSessionId = "session-current";
|
|
490
|
+
const watcher = createResultWatcher({ events: { on: () => () => {}, emit() {} } }, state, resultsDir, 60_000, {
|
|
491
|
+
deliverIntercomResults: false,
|
|
492
|
+
parseResult(raw) {
|
|
493
|
+
const parsed = JSON.parse(raw) as { id: string };
|
|
494
|
+
parsedIds.push(parsed.id);
|
|
495
|
+
return parsed;
|
|
496
|
+
},
|
|
497
|
+
});
|
|
498
|
+
try {
|
|
499
|
+
watcher.primeExistingResults();
|
|
500
|
+
assert.equal(await waitForPredicate(() => !fs.existsSync(path.join(resultsDir, "current.json"))), true);
|
|
501
|
+
} finally {
|
|
502
|
+
watcher.stopResultWatcher();
|
|
503
|
+
}
|
|
504
|
+
assert.deepEqual(parsedIds, ["current"]);
|
|
505
|
+
assert.equal(fs.existsSync(path.join(resultsDir, "stale-0.json")), true);
|
|
506
|
+
} finally {
|
|
507
|
+
fs.rmSync(resultsDir, { recursive: true, force: true });
|
|
508
|
+
}
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
it("includes observer-known run ids when current-session results are also indexed", async () => {
|
|
512
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-result-watcher-mixed-observer-"));
|
|
513
|
+
try {
|
|
514
|
+
const resultsDir = path.join(root, "results");
|
|
515
|
+
writeIndexedResult(path.join(resultsDir, "current.json"), { id: "current", runId: "current", sessionId: "session-current", success: true, summary: "current" });
|
|
516
|
+
writeIndexedResult(path.join(resultsDir, "scheduled-a.json"), { id: "scheduled-a", runId: "scheduled-a", sessionId: "session-a", success: true, summary: "scheduled" });
|
|
517
|
+
let observations = 0;
|
|
518
|
+
const state = createState();
|
|
519
|
+
state.currentSessionId = "session-current";
|
|
520
|
+
const watcher = createResultWatcher({ events: { on: () => () => {}, emit() {} } }, state, resultsDir, 60_000, {
|
|
521
|
+
observedCompletionRunIds: () => ["scheduled-a"],
|
|
522
|
+
observeCompletion: (result) => { if (result.runId === "scheduled-a") observations += 1; },
|
|
523
|
+
notifier: { deliver: async () => true },
|
|
524
|
+
});
|
|
525
|
+
try {
|
|
526
|
+
watcher.primeExistingResults();
|
|
527
|
+
assert.equal(await waitForPredicate(() => observations === 1), true);
|
|
528
|
+
assert.equal(await waitForPredicate(() => !fs.existsSync(path.join(resultsDir, "current.json"))), true);
|
|
529
|
+
} finally {
|
|
530
|
+
watcher.stopResultWatcher();
|
|
531
|
+
}
|
|
532
|
+
} finally {
|
|
533
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
534
|
+
}
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
it("delivers observed indexed pending results when public promotion is blocked", async () => {
|
|
538
|
+
const resultsDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-result-watcher-observed-pending-"));
|
|
539
|
+
const originalError = console.error;
|
|
540
|
+
try {
|
|
541
|
+
console.error = () => {};
|
|
542
|
+
for (let i = 0; i < 300; i += 1) {
|
|
543
|
+
fs.writeFileSync(path.join(resultsDir, `stale-${i}.json`), JSON.stringify({ id: `stale-${i}`, sessionId: "session-stale", success: true, summary: "done" }), "utf-8");
|
|
544
|
+
}
|
|
545
|
+
const resultPath = path.join(resultsDir, "scheduled-pending.json");
|
|
546
|
+
fs.mkdirSync(resultPath, { recursive: true });
|
|
547
|
+
writePendingAsyncResultFile(resultPath, {
|
|
548
|
+
id: "scheduled-pending",
|
|
549
|
+
runId: "scheduled-pending",
|
|
550
|
+
sessionId: "session-a",
|
|
551
|
+
success: true,
|
|
552
|
+
state: "complete",
|
|
553
|
+
summary: "scheduled pending done",
|
|
554
|
+
});
|
|
555
|
+
let observations = 0;
|
|
556
|
+
const state = createState();
|
|
557
|
+
state.currentSessionId = "session-b";
|
|
558
|
+
const watcher = createResultWatcher({ events: { on: () => () => {}, emit() {} } }, state, resultsDir, 60_000, {
|
|
559
|
+
observedCompletionRunIds: () => ["scheduled-pending"],
|
|
560
|
+
observeCompletion: (result) => {
|
|
561
|
+
if (result.runId === "scheduled-pending") observations += 1;
|
|
562
|
+
},
|
|
563
|
+
notifier: { deliver: async () => assert.fail("observer-owned completion must not reach active delivery") },
|
|
564
|
+
});
|
|
565
|
+
try {
|
|
566
|
+
watcher.primeExistingResults();
|
|
567
|
+
assert.equal(await waitForPredicate(() => observations === 1), true);
|
|
568
|
+
} finally {
|
|
569
|
+
watcher.stopResultWatcher();
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
assert.equal(fs.existsSync(pendingResultPath(resultsDir, "session-a", "scheduled-pending")), true);
|
|
573
|
+
assert.equal(fs.statSync(resultPath).isDirectory(), true);
|
|
574
|
+
assert.equal(fs.existsSync(path.join(resultsDir, "stale-0.json")), true);
|
|
575
|
+
} finally {
|
|
576
|
+
console.error = originalError;
|
|
577
|
+
fs.rmSync(resultsDir, { recursive: true, force: true });
|
|
578
|
+
}
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
it("uses observed run ids directly without scanning a stale result pile", async () => {
|
|
582
|
+
const resultsDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-result-watcher-observed-index-"));
|
|
583
|
+
try {
|
|
584
|
+
for (let i = 0; i < 300; i += 1) {
|
|
585
|
+
fs.writeFileSync(path.join(resultsDir, `stale-${i}.json`), JSON.stringify({ id: `stale-${i}`, sessionId: "session-stale", success: true, summary: "done" }), "utf-8");
|
|
586
|
+
}
|
|
587
|
+
const resultPath = path.join(resultsDir, "scheduled-a.json");
|
|
588
|
+
writeIndexedResult(resultPath, { id: "scheduled-a", runId: "scheduled-a", sessionId: "session-a", success: true, summary: "done" });
|
|
589
|
+
let observations = 0;
|
|
590
|
+
let observed = true;
|
|
591
|
+
const makeWatcher = () => {
|
|
592
|
+
const state = createState();
|
|
593
|
+
state.currentSessionId = "session-b";
|
|
594
|
+
return createResultWatcher({ events: { on: () => () => {}, emit() {} } }, state, resultsDir, 60_000, {
|
|
595
|
+
observedCompletionRunIds: () => observed ? ["scheduled-a"] : [],
|
|
596
|
+
observeCompletion: () => { observations += 1; observed = false; },
|
|
597
|
+
notifier: { deliver: async () => assert.fail("observer-owned completion must not reach active delivery") },
|
|
598
|
+
});
|
|
599
|
+
};
|
|
600
|
+
let watcher = makeWatcher();
|
|
601
|
+
try {
|
|
602
|
+
watcher.primeExistingResults();
|
|
603
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
604
|
+
} finally {
|
|
605
|
+
watcher.stopResultWatcher();
|
|
606
|
+
}
|
|
607
|
+
watcher = makeWatcher();
|
|
608
|
+
try {
|
|
609
|
+
watcher.primeExistingResults();
|
|
610
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
611
|
+
} finally {
|
|
612
|
+
watcher.stopResultWatcher();
|
|
613
|
+
}
|
|
614
|
+
assert.equal(observations, 1);
|
|
615
|
+
assert.equal(fs.existsSync(resultPath), true);
|
|
616
|
+
assert.equal(fs.existsSync(path.join(resultsDir, "stale-0.json")), true);
|
|
617
|
+
} finally {
|
|
618
|
+
fs.rmSync(resultsDir, { recursive: true, force: true });
|
|
619
|
+
}
|
|
620
|
+
});
|
|
621
|
+
|
|
340
622
|
it("uses native completion delivery without attempting external grouped intercom when disabled", async () => {
|
|
341
623
|
const resultsDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-result-watcher-native-delivery-"));
|
|
342
624
|
try {
|
|
@@ -351,7 +633,7 @@ describe("result watcher", () => {
|
|
|
351
633
|
const state = createState();
|
|
352
634
|
state.currentSessionId = "session-native";
|
|
353
635
|
const resultPath = path.join(resultsDir, "native-run.json");
|
|
354
|
-
|
|
636
|
+
writeIndexedResult(resultPath, {
|
|
355
637
|
id: "native-run",
|
|
356
638
|
runId: "native-run",
|
|
357
639
|
sessionId: "session-native",
|
|
@@ -361,14 +643,14 @@ describe("result watcher", () => {
|
|
|
361
643
|
summary: "Subagent process terminated by signal SIGTERM.",
|
|
362
644
|
results: [{ agent: "worker", output: "", success: false, exitCode: 1, processSignal: "SIGTERM" }],
|
|
363
645
|
intercomTarget: "native-parent",
|
|
364
|
-
})
|
|
646
|
+
});
|
|
365
647
|
const watcher = createResultWatcher(pi, state, resultsDir, 60_000, {
|
|
366
648
|
deliverIntercomResults: false,
|
|
367
649
|
notifier: { deliver: async (result) => { delivered.push(result); return true; } },
|
|
368
650
|
});
|
|
369
651
|
try {
|
|
370
652
|
watcher.primeExistingResults();
|
|
371
|
-
await new Promise((resolve) => setTimeout(resolve,
|
|
653
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
372
654
|
} finally {
|
|
373
655
|
watcher.stopResultWatcher();
|
|
374
656
|
}
|
|
@@ -419,9 +701,8 @@ describe("result watcher", () => {
|
|
|
419
701
|
const ownerWatcher = createResultWatcher(owner.pi, ownerState, resultsDir, 60_000);
|
|
420
702
|
const otherWatcher = createResultWatcher(other.pi, otherState, resultsDir, 60_000);
|
|
421
703
|
const ownerResultPath = path.join(resultsDir, "owner-run.json");
|
|
422
|
-
const sessionlessResultPath = path.join(resultsDir, "sessionless-run.json");
|
|
423
704
|
try {
|
|
424
|
-
|
|
705
|
+
writeIndexedResult(ownerResultPath, {
|
|
425
706
|
id: "owner-run",
|
|
426
707
|
agent: "worker",
|
|
427
708
|
mode: "single",
|
|
@@ -432,23 +713,12 @@ describe("result watcher", () => {
|
|
|
432
713
|
sessionId: "session-owner",
|
|
433
714
|
cwd: "/repo",
|
|
434
715
|
intercomTarget: "owner-target",
|
|
435
|
-
})
|
|
436
|
-
fs.writeFileSync(sessionlessResultPath, JSON.stringify({
|
|
437
|
-
id: "sessionless-run",
|
|
438
|
-
agent: "worker",
|
|
439
|
-
mode: "single",
|
|
440
|
-
success: true,
|
|
441
|
-
state: "complete",
|
|
442
|
-
summary: "legacy cwd-scoped output",
|
|
443
|
-
results: [{ agent: "worker", output: "legacy cwd-scoped output", success: true }],
|
|
444
|
-
cwd: "/repo",
|
|
445
|
-
intercomTarget: "sessionless-target",
|
|
446
|
-
}), "utf-8");
|
|
716
|
+
});
|
|
447
717
|
|
|
448
718
|
otherWatcher.primeExistingResults();
|
|
449
|
-
await new Promise((resolve) => setTimeout(resolve,
|
|
719
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
450
720
|
ownerWatcher.primeExistingResults();
|
|
451
|
-
await
|
|
721
|
+
assert.equal(await waitForPredicate(() => owner.emitted.some((entry) => entry.event === "subagent:async-complete")), true);
|
|
452
722
|
} finally {
|
|
453
723
|
ownerWatcher.stopResultWatcher();
|
|
454
724
|
otherWatcher.stopResultWatcher();
|
|
@@ -462,7 +732,6 @@ describe("result watcher", () => {
|
|
|
462
732
|
assert.equal(other.emitted.some((entry) => entry.event === "subagent:async-complete"), false);
|
|
463
733
|
assert.equal(other.emitted.some((entry) => entry.event === "subagent:result-intercom"), false);
|
|
464
734
|
assert.equal(fs.existsSync(ownerResultPath), false);
|
|
465
|
-
assert.equal(fs.existsSync(sessionlessResultPath), true);
|
|
466
735
|
} finally {
|
|
467
736
|
fs.rmSync(resultsDir, { recursive: true, force: true });
|
|
468
737
|
}
|
|
@@ -482,6 +751,7 @@ describe("result watcher", () => {
|
|
|
482
751
|
},
|
|
483
752
|
};
|
|
484
753
|
const state = createState();
|
|
754
|
+
state.asyncJobs.set("bad", { asyncId: "bad", asyncDir: path.join(resultsDir, "bad"), status: "running", startedAt: Date.now(), updatedAt: Date.now() });
|
|
485
755
|
const watcher = createResultWatcher(pi, state, resultsDir, 60_000);
|
|
486
756
|
const originalError = console.error;
|
|
487
757
|
const logged: unknown[][] = [];
|
|
@@ -490,7 +760,7 @@ describe("result watcher", () => {
|
|
|
490
760
|
};
|
|
491
761
|
try {
|
|
492
762
|
watcher.primeExistingResults();
|
|
493
|
-
await new Promise((resolve) => setTimeout(resolve,
|
|
763
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
494
764
|
} finally {
|
|
495
765
|
console.error = originalError;
|
|
496
766
|
watcher.stopResultWatcher();
|
|
@@ -590,16 +860,16 @@ describe("result watcher", () => {
|
|
|
590
860
|
watcher.startResultWatcher();
|
|
591
861
|
assert.equal(state.watcher, fakeWatcher);
|
|
592
862
|
for (const id of ["missed-a", "missed-b"]) {
|
|
593
|
-
|
|
863
|
+
writeIndexedResult(path.join(resultsDir, `${id}.json`), {
|
|
594
864
|
id,
|
|
595
865
|
sessionId: "session-1",
|
|
596
866
|
success: true,
|
|
597
867
|
state: "complete",
|
|
598
868
|
summary: "done",
|
|
599
|
-
})
|
|
869
|
+
});
|
|
600
870
|
}
|
|
601
871
|
scan?.();
|
|
602
|
-
await new Promise((resolve) => setTimeout(resolve,
|
|
872
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
603
873
|
} finally {
|
|
604
874
|
watcher.stopResultWatcher();
|
|
605
875
|
}
|
|
@@ -662,6 +932,7 @@ describe("result watcher", () => {
|
|
|
662
932
|
});
|
|
663
933
|
const originalError = console.error;
|
|
664
934
|
const childSessionPath = path.join(resultsDir, "a-session.jsonl");
|
|
935
|
+
const resultPath = path.join(resultsDir, "async-fallback.json");
|
|
665
936
|
console.error = () => {};
|
|
666
937
|
try {
|
|
667
938
|
watcher.startResultWatcher();
|
|
@@ -669,7 +940,7 @@ describe("result watcher", () => {
|
|
|
669
940
|
assert.notEqual(state.watcherRestartTimer, null);
|
|
670
941
|
|
|
671
942
|
fs.writeFileSync(childSessionPath, "", "utf-8");
|
|
672
|
-
|
|
943
|
+
writeIndexedResult(resultPath, {
|
|
673
944
|
id: "async-fallback",
|
|
674
945
|
runId: "run-fallback",
|
|
675
946
|
agent: "parallel:a+b",
|
|
@@ -683,9 +954,9 @@ describe("result watcher", () => {
|
|
|
683
954
|
],
|
|
684
955
|
sessionId: "session-1",
|
|
685
956
|
intercomTarget: "subagent-chat-main",
|
|
686
|
-
})
|
|
957
|
+
});
|
|
687
958
|
poll?.();
|
|
688
|
-
await
|
|
959
|
+
assert.equal(await waitForPredicate(() => emitted.some((entry) => entry.event === "subagent:async-complete") && !fs.existsSync(resultPath)), true);
|
|
689
960
|
} finally {
|
|
690
961
|
console.error = originalError;
|
|
691
962
|
watcher.stopResultWatcher();
|
|
@@ -694,7 +965,7 @@ describe("result watcher", () => {
|
|
|
694
965
|
const intercomEvents = emitted.filter((entry) => entry.event === "subagent:result-intercom");
|
|
695
966
|
assert.equal(intercomEvents.length, 1);
|
|
696
967
|
assert.equal(emitted.some((entry) => entry.event === "subagent:async-complete"), true);
|
|
697
|
-
assert.equal(fs.existsSync(
|
|
968
|
+
assert.equal(fs.existsSync(resultPath), false);
|
|
698
969
|
const payload = intercomEvents[0]?.data as { mode?: string; status?: string; message?: string; children?: Array<{ status?: string; summary?: string; sessionPath?: string }> };
|
|
699
970
|
const completion = emitted.find((entry) => entry.event === "subagent:async-complete")?.data as { results?: Array<{ status?: string; summary?: string; sessionPath?: string }> } | undefined;
|
|
700
971
|
assert.equal(payload.mode, "parallel");
|
|
@@ -764,9 +1035,9 @@ describe("result watcher", () => {
|
|
|
764
1035
|
assert.equal(state.watcher, null);
|
|
765
1036
|
assert.notEqual(state.watcherRestartTimer, null);
|
|
766
1037
|
|
|
767
|
-
|
|
1038
|
+
writeIndexedResult(path.join(resultsDir, "done.json"), { sessionId: "session-1", summary: "done" });
|
|
768
1039
|
poll?.();
|
|
769
|
-
await new Promise((resolve) => setTimeout(resolve,
|
|
1040
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
770
1041
|
} finally {
|
|
771
1042
|
console.error = originalError;
|
|
772
1043
|
watcher.stopResultWatcher();
|
|
@@ -811,7 +1082,7 @@ describe("result watcher", () => {
|
|
|
811
1082
|
const missingSession = path.join(resultsDir, "b-session.jsonl");
|
|
812
1083
|
try {
|
|
813
1084
|
fs.writeFileSync(firstSession, "", "utf-8");
|
|
814
|
-
|
|
1085
|
+
writeIndexedResult(path.join(resultsDir, "async-1.json"), {
|
|
815
1086
|
id: "async-1",
|
|
816
1087
|
runId: "run-123",
|
|
817
1088
|
agent: "parallel:a+b",
|
|
@@ -828,9 +1099,9 @@ describe("result watcher", () => {
|
|
|
828
1099
|
asyncDir: "/tmp/async-1",
|
|
829
1100
|
parallelHandoff: { version: 1, path: "/tmp/async-1/handoff.json", groupCount: 1, childCount: 2, changedPatches: 1, cleanupState: "complete" },
|
|
830
1101
|
intercomTarget: "subagent-chat-main",
|
|
831
|
-
})
|
|
1102
|
+
});
|
|
832
1103
|
watcher.primeExistingResults();
|
|
833
|
-
await
|
|
1104
|
+
assert.equal(await waitForPredicate(() => emitted.some((entry) => entry.event === "subagent:async-complete") && emitted.some((entry) => entry.event === "subagent:result-intercom")), true);
|
|
834
1105
|
} finally {
|
|
835
1106
|
watcher.stopResultWatcher();
|
|
836
1107
|
}
|
|
@@ -882,7 +1153,7 @@ describe("result watcher", () => {
|
|
|
882
1153
|
state.currentSessionId = "session-1";
|
|
883
1154
|
const watcher = createResultWatcher(pi, state, resultsDir, 60_000);
|
|
884
1155
|
try {
|
|
885
|
-
|
|
1156
|
+
writeIndexedResult(path.join(resultsDir, "async-stopped.json"), {
|
|
886
1157
|
id: "async-stopped",
|
|
887
1158
|
runId: "async-stopped",
|
|
888
1159
|
agent: "parallel:a+b",
|
|
@@ -897,9 +1168,9 @@ describe("result watcher", () => {
|
|
|
897
1168
|
],
|
|
898
1169
|
sessionId: "session-1",
|
|
899
1170
|
intercomTarget: "subagent-chat-main",
|
|
900
|
-
}
|
|
1171
|
+
});
|
|
901
1172
|
watcher.primeExistingResults();
|
|
902
|
-
await new Promise((resolve) => setTimeout(resolve,
|
|
1173
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
903
1174
|
} finally {
|
|
904
1175
|
watcher.stopResultWatcher();
|
|
905
1176
|
}
|
|
@@ -964,7 +1235,7 @@ describe("result watcher", () => {
|
|
|
964
1235
|
const watcher = createResultWatcher(pi, state, resultsDir, 60_000);
|
|
965
1236
|
const resultPath = path.join(resultsDir, "async-nested-root.json");
|
|
966
1237
|
try {
|
|
967
|
-
|
|
1238
|
+
writeIndexedResult(resultPath, {
|
|
968
1239
|
id: "async-nested-root",
|
|
969
1240
|
runId: "async-nested-root",
|
|
970
1241
|
agent: "owner",
|
|
@@ -975,9 +1246,9 @@ describe("result watcher", () => {
|
|
|
975
1246
|
results: [{ agent: "owner", output: "owner done", success: true }],
|
|
976
1247
|
sessionId: "session-1",
|
|
977
1248
|
intercomTarget: "subagent-chat-main",
|
|
978
|
-
})
|
|
1249
|
+
});
|
|
979
1250
|
watcher.primeExistingResults();
|
|
980
|
-
await
|
|
1251
|
+
assert.equal(await waitForPredicate(() => !fs.existsSync(resultPath) && emitted.some((entry) => entry.event === "subagent:async-complete") && emitted.some((entry) => entry.event === "subagent:result-intercom")), true);
|
|
981
1252
|
} finally {
|
|
982
1253
|
watcher.stopResultWatcher();
|
|
983
1254
|
}
|
|
@@ -1032,7 +1303,7 @@ describe("result watcher", () => {
|
|
|
1032
1303
|
logged.push(args);
|
|
1033
1304
|
};
|
|
1034
1305
|
try {
|
|
1035
|
-
|
|
1306
|
+
writeIndexedResult(resultPath, {
|
|
1036
1307
|
id: "async-explicit-nested",
|
|
1037
1308
|
runId: "async-explicit-nested",
|
|
1038
1309
|
agent: "owner",
|
|
@@ -1055,9 +1326,9 @@ describe("result watcher", () => {
|
|
|
1055
1326
|
],
|
|
1056
1327
|
sessionId: "session-1",
|
|
1057
1328
|
intercomTarget: "subagent-chat-main",
|
|
1058
|
-
})
|
|
1329
|
+
});
|
|
1059
1330
|
watcher.primeExistingResults();
|
|
1060
|
-
await
|
|
1331
|
+
assert.equal(await waitForPredicate(() => !fs.existsSync(resultPath) && emitted.some((entry) => entry.event === "subagent:async-complete")), true);
|
|
1061
1332
|
} finally {
|
|
1062
1333
|
console.error = originalError;
|
|
1063
1334
|
watcher.stopResultWatcher();
|
|
@@ -1123,7 +1394,7 @@ describe("result watcher", () => {
|
|
|
1123
1394
|
logged.push(args);
|
|
1124
1395
|
};
|
|
1125
1396
|
try {
|
|
1126
|
-
|
|
1397
|
+
writeIndexedResult(resultPath, {
|
|
1127
1398
|
id: "async-nested-retry",
|
|
1128
1399
|
runId: "async-nested-retry",
|
|
1129
1400
|
agent: "owner",
|
|
@@ -1132,9 +1403,9 @@ describe("result watcher", () => {
|
|
|
1132
1403
|
summary: "owner done",
|
|
1133
1404
|
sessionId: "session-1",
|
|
1134
1405
|
intercomTarget: "subagent-chat-main",
|
|
1135
|
-
})
|
|
1406
|
+
});
|
|
1136
1407
|
watcher.primeExistingResults();
|
|
1137
|
-
await new Promise((resolve) => setTimeout(resolve,
|
|
1408
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
1138
1409
|
|
|
1139
1410
|
assert.equal(fs.existsSync(resultPath), true);
|
|
1140
1411
|
assert.equal(emitted.length, 0);
|
|
@@ -1186,7 +1457,7 @@ describe("result watcher", () => {
|
|
|
1186
1457
|
state.currentSessionId = "session-1";
|
|
1187
1458
|
const watcher = createResultWatcher(pi, state, resultsDir, 60_000);
|
|
1188
1459
|
try {
|
|
1189
|
-
|
|
1460
|
+
writeIndexedResult(path.join(resultsDir, "async-top-session.json"), {
|
|
1190
1461
|
id: "async-top-session",
|
|
1191
1462
|
mode: "parallel",
|
|
1192
1463
|
success: false,
|
|
@@ -1198,9 +1469,9 @@ describe("result watcher", () => {
|
|
|
1198
1469
|
sessionId: "session-1",
|
|
1199
1470
|
sessionFile: "/tmp/top-session.jsonl",
|
|
1200
1471
|
intercomTarget: "subagent-chat-main",
|
|
1201
|
-
})
|
|
1472
|
+
});
|
|
1202
1473
|
watcher.primeExistingResults();
|
|
1203
|
-
await new Promise((resolve) => setTimeout(resolve,
|
|
1474
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
1204
1475
|
} finally {
|
|
1205
1476
|
watcher.stopResultWatcher();
|
|
1206
1477
|
}
|
|
@@ -1243,7 +1514,7 @@ describe("result watcher", () => {
|
|
|
1243
1514
|
state.currentSessionId = "session-1";
|
|
1244
1515
|
const watcher = createResultWatcher(pi, state, resultsDir, 60_000);
|
|
1245
1516
|
try {
|
|
1246
|
-
|
|
1517
|
+
writeIndexedResult(path.join(resultsDir, "async-paused.json"), {
|
|
1247
1518
|
id: "async-paused",
|
|
1248
1519
|
runId: "run-paused",
|
|
1249
1520
|
agent: "chain:a->b",
|
|
@@ -1257,9 +1528,9 @@ describe("result watcher", () => {
|
|
|
1257
1528
|
],
|
|
1258
1529
|
sessionId: "session-1",
|
|
1259
1530
|
intercomTarget: "subagent-chat-main",
|
|
1260
|
-
})
|
|
1531
|
+
});
|
|
1261
1532
|
watcher.primeExistingResults();
|
|
1262
|
-
await new Promise((resolve) => setTimeout(resolve,
|
|
1533
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
1263
1534
|
} finally {
|
|
1264
1535
|
watcher.stopResultWatcher();
|
|
1265
1536
|
}
|
|
@@ -1301,7 +1572,7 @@ describe("result watcher", () => {
|
|
|
1301
1572
|
logged.push(args);
|
|
1302
1573
|
};
|
|
1303
1574
|
try {
|
|
1304
|
-
|
|
1575
|
+
writeIndexedResult(path.join(resultsDir, "async-2.json"), {
|
|
1305
1576
|
id: "async-2",
|
|
1306
1577
|
runId: "run-456",
|
|
1307
1578
|
agent: "worker",
|
|
@@ -1310,9 +1581,9 @@ describe("result watcher", () => {
|
|
|
1310
1581
|
summary: "Worker summary",
|
|
1311
1582
|
sessionId: "session-1",
|
|
1312
1583
|
intercomTarget: "orchestrator",
|
|
1313
|
-
})
|
|
1584
|
+
});
|
|
1314
1585
|
watcher.primeExistingResults();
|
|
1315
|
-
await new Promise((resolve) => setTimeout(resolve,
|
|
1586
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
1316
1587
|
} finally {
|
|
1317
1588
|
console.error = originalError;
|
|
1318
1589
|
watcher.stopResultWatcher();
|
|
@@ -1354,7 +1625,7 @@ describe("result watcher", () => {
|
|
|
1354
1625
|
const loggedWarning = () => logged.some((entry) => /Subagent async grouped result intercom delivery was not acknowledged/.test(String(entry[0] ?? "")));
|
|
1355
1626
|
let warned = false;
|
|
1356
1627
|
try {
|
|
1357
|
-
|
|
1628
|
+
writeIndexedResult(path.join(resultsDir, "unacknowledged.json"), {
|
|
1358
1629
|
id: "unacknowledged",
|
|
1359
1630
|
runId: "run-unacknowledged",
|
|
1360
1631
|
agent: "worker",
|
|
@@ -1363,7 +1634,7 @@ describe("result watcher", () => {
|
|
|
1363
1634
|
summary: "Worker summary",
|
|
1364
1635
|
sessionId: "session-1",
|
|
1365
1636
|
intercomTarget: "orchestrator",
|
|
1366
|
-
})
|
|
1637
|
+
});
|
|
1367
1638
|
watcher.primeExistingResults();
|
|
1368
1639
|
warned = await waitForPredicate(loggedWarning);
|
|
1369
1640
|
} finally {
|
|
@@ -1410,7 +1681,7 @@ describe("result watcher", () => {
|
|
|
1410
1681
|
notifier: { async deliver(result) { delivered.push({ intercomDelivered: result.intercomDelivered }); return true; } },
|
|
1411
1682
|
});
|
|
1412
1683
|
try {
|
|
1413
|
-
|
|
1684
|
+
writeIndexedResult(path.join(resultsDir, "acknowledged.json"), {
|
|
1414
1685
|
id: "acknowledged",
|
|
1415
1686
|
runId: "run-acknowledged",
|
|
1416
1687
|
agent: "worker",
|
|
@@ -1419,9 +1690,9 @@ describe("result watcher", () => {
|
|
|
1419
1690
|
summary: "Worker summary",
|
|
1420
1691
|
sessionId: "session-1",
|
|
1421
1692
|
intercomTarget: "orchestrator",
|
|
1422
|
-
})
|
|
1693
|
+
});
|
|
1423
1694
|
watcher.primeExistingResults();
|
|
1424
|
-
await new Promise((resolve) => setTimeout(resolve,
|
|
1695
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
1425
1696
|
} finally {
|
|
1426
1697
|
watcher.stopResultWatcher();
|
|
1427
1698
|
}
|
|
@@ -1444,11 +1715,11 @@ describe("result watcher", () => {
|
|
|
1444
1715
|
const resultFile = "expired.json";
|
|
1445
1716
|
const result = { sessionId: "session-1", agent: "worker", success: true, summary: "new result", timestamp: 123 };
|
|
1446
1717
|
const resultPath = path.join(resultsDir, resultFile);
|
|
1447
|
-
|
|
1718
|
+
writeIndexedResult(resultPath, result);
|
|
1448
1719
|
state.completionSeen.set(buildCompletionKey(result, `result:${resultFile}`), Date.now() - 61_000);
|
|
1449
1720
|
try {
|
|
1450
1721
|
watcher.primeExistingResults();
|
|
1451
|
-
await new Promise((resolve) => setTimeout(resolve,
|
|
1722
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
1452
1723
|
} finally {
|
|
1453
1724
|
watcher.stopResultWatcher();
|
|
1454
1725
|
}
|
|
@@ -1471,10 +1742,10 @@ describe("result watcher", () => {
|
|
|
1471
1742
|
notifier: { async deliver() { attempts += 1; return attempts > 1; } },
|
|
1472
1743
|
});
|
|
1473
1744
|
const resultPath = path.join(resultsDir, "retry.json");
|
|
1474
|
-
|
|
1745
|
+
writeIndexedResult(resultPath, { id: "retry", sessionId: "session-1", agent: "worker", success: true, summary: "done", timestamp: 1 });
|
|
1475
1746
|
try {
|
|
1476
1747
|
watcher.primeExistingResults();
|
|
1477
|
-
await new Promise((resolve) => setTimeout(resolve,
|
|
1748
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
1478
1749
|
assert.equal(fs.existsSync(resultPath), true);
|
|
1479
1750
|
await new Promise((resolve) => setTimeout(resolve, 180));
|
|
1480
1751
|
} finally {
|
|
@@ -1499,12 +1770,12 @@ describe("result watcher", () => {
|
|
|
1499
1770
|
notifier: { deliver: () => new Promise<boolean>((resolve) => { accept = resolve; }) },
|
|
1500
1771
|
});
|
|
1501
1772
|
const resultPath = path.join(resultsDir, "stale.json");
|
|
1502
|
-
|
|
1773
|
+
writeIndexedResult(resultPath, { id: "stale", sessionId: "session-1", agent: "worker", success: true, summary: "done", timestamp: 1 });
|
|
1503
1774
|
watcher.primeExistingResults();
|
|
1504
|
-
await new Promise((resolve) => setTimeout(resolve,
|
|
1775
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
1505
1776
|
watcher.stopResultWatcher();
|
|
1506
1777
|
accept(true);
|
|
1507
|
-
await new Promise((resolve) => setTimeout(resolve,
|
|
1778
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
1508
1779
|
assert.equal(fs.existsSync(resultPath), true);
|
|
1509
1780
|
assert.deepEqual(emitted, []);
|
|
1510
1781
|
} finally {
|
|
@@ -1522,10 +1793,10 @@ describe("result watcher", () => {
|
|
|
1522
1793
|
const watcher = createResultWatcher({ events: { on: () => () => {}, emit(event) { emitted.push(event); } } }, state, resultsDir, 60_000, {
|
|
1523
1794
|
notifier: { async deliver(result) { delivered.push({ triggerTurn: result.triggerTurn }); return true; } },
|
|
1524
1795
|
});
|
|
1525
|
-
|
|
1796
|
+
writeIndexedResult(path.join(resultsDir, "quiet.json"), { id: "quiet", sessionId: "session-1", agent: "worker", success: true, summary: "done", timestamp: 1, intercomTarget: "host" });
|
|
1526
1797
|
try {
|
|
1527
1798
|
watcher.primeExistingResults({ triggerTurn: false });
|
|
1528
|
-
await new Promise((resolve) => setTimeout(resolve,
|
|
1799
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
1529
1800
|
} finally {
|
|
1530
1801
|
watcher.stopResultWatcher();
|
|
1531
1802
|
}
|