@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
|
@@ -22,12 +22,14 @@ import { projectNestedRegistryForRoot, sanitizeSummary } from "../shared/nested-
|
|
|
22
22
|
import { resolveWatchPath } from "../../shared/utils.ts";
|
|
23
23
|
import { recordWaitCompletion } from "./wait-completions.ts";
|
|
24
24
|
import { MISSION_BINDING_FILE, syncMissionFromAsyncCompletion } from "../../missions/lifecycle.ts";
|
|
25
|
+
import { missionObserverResultCandidateFiles, promotePendingResultFile, removeMissionObserverIndex, removeResultIndex, resultCandidateFilesForSession, resultPayloadPathForIndexedRun, resultPayloadPathForMissionObserverRun, resultPayloadPathForSessionRun, writeAsyncResultFile, writeResultIndexForData } from "./result-files.ts";
|
|
25
26
|
import type { CompletionNotifier, CompletionNotification } from "./notify.ts";
|
|
26
27
|
|
|
27
28
|
const WATCHER_RESTART_DELAY_MS = 3000;
|
|
28
29
|
const POLL_INTERVAL_MS = 3000;
|
|
29
30
|
const HEALTHY_SCAN_INTERVAL_MS = 60_000;
|
|
30
31
|
const RETRY_DELAY_MS = 100;
|
|
32
|
+
const SLOW_RESULT_SCAN_MS = 500;
|
|
31
33
|
|
|
32
34
|
type ResultWatcherFs = Pick<typeof fs, "existsSync" | "readFileSync" | "unlinkSync" | "readdirSync" | "mkdirSync" | "realpathSync" | "statSync" | "watch">;
|
|
33
35
|
|
|
@@ -50,6 +52,8 @@ type ResultWatcherDeps = {
|
|
|
50
52
|
parseResult?: (raw: string) => ResultFileData;
|
|
51
53
|
/** External grouped-result transport. Disable when native completion notifications own delivery. */
|
|
52
54
|
deliverIntercomResults?: boolean;
|
|
55
|
+
/** Coalesces result-file events. Tests can lower this without changing retry timing. */
|
|
56
|
+
coalesceDelayMs?: number;
|
|
53
57
|
};
|
|
54
58
|
|
|
55
59
|
type ResultFileChild = {
|
|
@@ -79,6 +83,7 @@ type ResultFileData = CompletionNotification & {
|
|
|
79
83
|
asyncDir?: string;
|
|
80
84
|
intercomTarget?: string;
|
|
81
85
|
parallelHandoff?: ParallelHandoffReference;
|
|
86
|
+
notificationDeliveredAt?: unknown;
|
|
82
87
|
};
|
|
83
88
|
|
|
84
89
|
type ResultFileIdentity = {
|
|
@@ -87,6 +92,12 @@ type ResultFileIdentity = {
|
|
|
87
92
|
asyncDir?: string;
|
|
88
93
|
};
|
|
89
94
|
|
|
95
|
+
interface ResultScanStats {
|
|
96
|
+
files: number;
|
|
97
|
+
scheduled: number;
|
|
98
|
+
startedAt: number;
|
|
99
|
+
}
|
|
100
|
+
|
|
90
101
|
function jsonStringProperty(raw: string, property: string): string | undefined {
|
|
91
102
|
const matches = raw.matchAll(new RegExp(`"${property}"\\s*:\\s*("(?:\\\\.|[^"\\\\])*")`, "g"));
|
|
92
103
|
let encoded: string | undefined;
|
|
@@ -134,6 +145,16 @@ function shouldPoll(error: unknown): boolean {
|
|
|
134
145
|
return code === "EMFILE" || code === "ENOSPC";
|
|
135
146
|
}
|
|
136
147
|
|
|
148
|
+
function hasDeliveredNotification(data: ResultFileData): boolean {
|
|
149
|
+
return typeof data.notificationDeliveredAt === "number" && Number.isFinite(data.notificationDeliveredAt);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function markDeliveredNotification(resultPath: string, data: ResultFileData, runId: string, now: number): ResultFileData {
|
|
153
|
+
const marked = { ...data, runId, notificationDeliveredAt: now };
|
|
154
|
+
writeAsyncResultFile(resultPath, marked);
|
|
155
|
+
return marked;
|
|
156
|
+
}
|
|
157
|
+
|
|
137
158
|
/**
|
|
138
159
|
* Watches persisted async results for the session currently owned by this
|
|
139
160
|
* runtime. `stopResultWatcher()` revokes ownership before closing resources,
|
|
@@ -194,16 +215,55 @@ export function createResultWatcher(
|
|
|
194
215
|
state.resultFileCoalescer.schedule(file, delayMs);
|
|
195
216
|
};
|
|
196
217
|
|
|
197
|
-
const
|
|
198
|
-
|
|
218
|
+
const publicResultPath = (file: string): string => path.join(resultsDir, file);
|
|
219
|
+
const publicResultFileExists = (file: string): boolean => {
|
|
220
|
+
try {
|
|
221
|
+
return fsApi.statSync(publicResultPath(file)).isFile();
|
|
222
|
+
} catch (error) {
|
|
223
|
+
if (!isNotFound(error)) console.error(`Failed to inspect subagent result file '${publicResultPath(file)}':`, error);
|
|
224
|
+
return false;
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
const resultPayloadPath = (file: string, observed?: ReadonlySet<string>): string | undefined => {
|
|
228
|
+
if (file !== path.basename(file) || !file.endsWith(".json")) return undefined;
|
|
229
|
+
const runId = file.replace(/\.json$/i, "");
|
|
230
|
+
const sessionResult = state.currentSessionId ? resultPayloadPathForSessionRun(resultsDir, state.currentSessionId, runId) : undefined;
|
|
231
|
+
if (sessionResult) return sessionResult;
|
|
232
|
+
const observerResult = resultPayloadPathForMissionObserverRun(resultsDir, runId);
|
|
233
|
+
if (observerResult) return observerResult;
|
|
234
|
+
if (observed?.has(runId)) {
|
|
235
|
+
const indexedResult = resultPayloadPathForIndexedRun(resultsDir, runId);
|
|
236
|
+
if (indexedResult) return indexedResult;
|
|
237
|
+
}
|
|
238
|
+
return publicResultFileExists(file) ? publicResultPath(file) : undefined;
|
|
239
|
+
};
|
|
240
|
+
const resultSignature = (file: string, observed?: ReadonlySet<string>): string | undefined => {
|
|
241
|
+
const resultPath = resultPayloadPath(file, observed);
|
|
242
|
+
if (!resultPath) {
|
|
243
|
+
identityCache.delete(file);
|
|
244
|
+
return undefined;
|
|
245
|
+
}
|
|
199
246
|
try {
|
|
200
247
|
const stat = fsApi.statSync(resultPath);
|
|
201
|
-
|
|
248
|
+
if (!stat.isFile()) return undefined;
|
|
249
|
+
return `${resultPath}:${stat.size}:${stat.mtimeMs}`;
|
|
250
|
+
} catch (error) {
|
|
251
|
+
identityCache.delete(file);
|
|
252
|
+
if (!isNotFound(error)) console.error(`Failed to inspect subagent result file '${resultPath}':`, error);
|
|
253
|
+
return undefined;
|
|
254
|
+
}
|
|
255
|
+
};
|
|
256
|
+
const inspectResult = (file: string, knownSignature?: string, observed?: ReadonlySet<string>): { identity: ResultFileIdentity; signature: string } | undefined => {
|
|
257
|
+
const resultPath = resultPayloadPath(file, observed);
|
|
258
|
+
if (!resultPath) return undefined;
|
|
259
|
+
try {
|
|
260
|
+
const signature = knownSignature ?? resultSignature(file, observed);
|
|
261
|
+
if (!signature) return undefined;
|
|
202
262
|
const cached = identityCache.get(file);
|
|
203
|
-
if (cached?.signature === signature) return cached.identity;
|
|
263
|
+
if (cached?.signature === signature) return { identity: cached.identity, signature };
|
|
204
264
|
const identity = resultFileIdentity(fsApi.readFileSync(resultPath, "utf-8"), file);
|
|
205
265
|
identityCache.set(file, { signature, identity });
|
|
206
|
-
return identity;
|
|
266
|
+
return { identity, signature };
|
|
207
267
|
} catch (error) {
|
|
208
268
|
identityCache.delete(file);
|
|
209
269
|
if (!isNotFound(error)) console.error(`Failed to inspect subagent result file '${resultPath}':`, error);
|
|
@@ -220,9 +280,10 @@ export function createResultWatcher(
|
|
|
220
280
|
}
|
|
221
281
|
};
|
|
222
282
|
|
|
223
|
-
const shouldProcessResult = (file: string, observed?: ReadonlySet<string
|
|
224
|
-
const
|
|
225
|
-
if (!
|
|
283
|
+
const shouldProcessResult = (file: string, observed?: ReadonlySet<string>, knownSignature?: string): boolean => {
|
|
284
|
+
const inspected = inspectResult(file, knownSignature, observed);
|
|
285
|
+
if (!inspected) return false;
|
|
286
|
+
const { identity } = inspected;
|
|
226
287
|
// Missing identity stays on the normal parser path so malformed or legacy
|
|
227
288
|
// files keep their existing diagnostics and compatibility behavior.
|
|
228
289
|
if (!identity.sessionId) return true;
|
|
@@ -233,32 +294,80 @@ export function createResultWatcher(
|
|
|
233
294
|
return Boolean(deps.observeCompletion && !deps.observedCompletionRunIds);
|
|
234
295
|
};
|
|
235
296
|
|
|
297
|
+
const removeDeliveredResult = (file: string, sessionId: string, runId: string, toolCallId: string | undefined): boolean => {
|
|
298
|
+
try {
|
|
299
|
+
if (publicResultFileExists(file)) fsApi.unlinkSync(publicResultPath(file));
|
|
300
|
+
identityCache.delete(file);
|
|
301
|
+
removeResultIndex(resultsDir, sessionId, runId, toolCallId);
|
|
302
|
+
return true;
|
|
303
|
+
} catch (error) {
|
|
304
|
+
if (!isNotFound(error)) {
|
|
305
|
+
console.error(`Failed to remove delivered subagent result '${publicResultPath(file)}'; will retry:`, error);
|
|
306
|
+
return false;
|
|
307
|
+
}
|
|
308
|
+
return true;
|
|
309
|
+
}
|
|
310
|
+
};
|
|
236
311
|
const handleResult = async (file: string, triggerTurn: boolean) => {
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
if (!shouldProcessResult(file))
|
|
312
|
+
if (processing.has(file)) return;
|
|
313
|
+
let observed: ReadonlySet<string> | undefined;
|
|
314
|
+
if (!shouldProcessResult(file)) {
|
|
315
|
+
const runId = file === path.basename(file) && file.endsWith(".json") ? file.replace(/\.json$/i, "") : undefined;
|
|
316
|
+
observed = observedRunIds();
|
|
317
|
+
if (!runId || !observed.has(runId) || !shouldProcessResult(file, observed)) return;
|
|
318
|
+
}
|
|
240
319
|
processing.add(file);
|
|
320
|
+
let resultPath = publicResultPath(file);
|
|
241
321
|
try {
|
|
242
|
-
const
|
|
322
|
+
const payloadPath = resultPayloadPath(file, observed);
|
|
323
|
+
if (!payloadPath) return;
|
|
324
|
+
resultPath = payloadPath;
|
|
325
|
+
let raw = fsApi.readFileSync(resultPath, "utf-8");
|
|
326
|
+
let identity = resultFileIdentity(raw, file);
|
|
327
|
+
if (identity.sessionId && identity.runId) {
|
|
328
|
+
const pendingState = promotePendingResultFile(resultsDir, identity.sessionId, identity.runId, file);
|
|
329
|
+
if (pendingState === "promoted") {
|
|
330
|
+
identityCache.delete(file);
|
|
331
|
+
resultPath = publicResultPath(file);
|
|
332
|
+
raw = fsApi.readFileSync(resultPath, "utf-8");
|
|
333
|
+
identity = resultFileIdentity(raw, file);
|
|
334
|
+
} else if (pendingState === "pending") {
|
|
335
|
+
const pendingPath = resultPayloadPathForSessionRun(resultsDir, identity.sessionId, identity.runId);
|
|
336
|
+
if (!pendingPath) {
|
|
337
|
+
scheduleResult(file, triggerTurn, RETRY_DELAY_MS);
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
resultPath = pendingPath;
|
|
341
|
+
raw = fsApi.readFileSync(resultPath, "utf-8");
|
|
342
|
+
identity = resultFileIdentity(raw, file);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
let data = parseResult(raw);
|
|
243
346
|
if (typeof data.sessionId !== "string" || !data.sessionId) return;
|
|
347
|
+
const sessionId = data.sessionId;
|
|
244
348
|
const runId = data.runId ?? data.id ?? file.replace(/\.json$/i, "");
|
|
349
|
+
const toolCallId = typeof data.toolCallId === "string" ? data.toolCallId : undefined;
|
|
350
|
+
let observerSucceeded = true;
|
|
245
351
|
try {
|
|
246
352
|
syncMissionFromAsyncCompletion({ ...data, runId });
|
|
247
353
|
} catch (error) {
|
|
354
|
+
observerSucceeded = false;
|
|
248
355
|
console.error(`Mission completion sync failed for '${resultPath}':`, error);
|
|
249
356
|
}
|
|
250
357
|
try {
|
|
251
358
|
deps.observeCompletion?.({ ...data, runId });
|
|
252
359
|
} catch (error) {
|
|
360
|
+
observerSucceeded = false;
|
|
253
361
|
console.error(`Completion observer failed for '${resultPath}':`, error);
|
|
254
362
|
}
|
|
363
|
+
if (observerSucceeded) removeMissionObserverIndex(resultsDir, runId);
|
|
255
364
|
const epoch = deliveryEpoch;
|
|
256
|
-
if (!ownsSession(
|
|
365
|
+
if (!ownsSession(sessionId, epoch)) return;
|
|
257
366
|
// Recorded before dedupe and before the unlink below so subagent_wait can
|
|
258
367
|
// use the in-memory record or its bounded durable replay after cleanup.
|
|
259
368
|
recordWaitCompletion(state, runId, data, Date.now(), completionTtlMs, {
|
|
260
369
|
resultsDir,
|
|
261
|
-
sessionId
|
|
370
|
+
sessionId,
|
|
262
371
|
});
|
|
263
372
|
const hasExplicitNestedChildren = data.nestedChildren !== undefined;
|
|
264
373
|
let nestedChildren = compactNestedResultChildren(sanitizeNestedResultChildren(data.nestedChildren, resultPath, "nestedChildren"));
|
|
@@ -273,20 +382,17 @@ export function createResultWatcher(
|
|
|
273
382
|
}
|
|
274
383
|
|
|
275
384
|
const completionKey = buildCompletionKey(data, `result:${file}`);
|
|
385
|
+
const alreadyDelivered = hasDeliveredNotification(data);
|
|
276
386
|
const lastSeenAt = state.completionSeen.get(completionKey);
|
|
277
387
|
if (lastSeenAt !== undefined && Date.now() - lastSeenAt > completionTtlMs) {
|
|
278
388
|
state.completionSeen.delete(completionKey);
|
|
279
389
|
} else if (lastSeenAt !== undefined) {
|
|
280
|
-
if (!
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
identityCache.delete(file);
|
|
284
|
-
} catch (error) {
|
|
285
|
-
if (!isNotFound(error)) {
|
|
286
|
-
console.error(`Failed to remove delivered subagent result '${resultPath}'; will retry:`, error);
|
|
287
|
-
scheduleResult(file, triggerTurn, RETRY_DELAY_MS);
|
|
288
|
-
}
|
|
390
|
+
if (!observerSucceeded) {
|
|
391
|
+
scheduleResult(file, triggerTurn, RETRY_DELAY_MS);
|
|
392
|
+
return;
|
|
289
393
|
}
|
|
394
|
+
if (!ownsSession(sessionId, epoch)) return;
|
|
395
|
+
if (!removeDeliveredResult(file, sessionId, runId, toolCallId)) scheduleResult(file, triggerTurn, RETRY_DELAY_MS);
|
|
290
396
|
return;
|
|
291
397
|
}
|
|
292
398
|
|
|
@@ -334,6 +440,17 @@ export function createResultWatcher(
|
|
|
334
440
|
};
|
|
335
441
|
}), nestedChildren);
|
|
336
442
|
|
|
443
|
+
if (alreadyDelivered) {
|
|
444
|
+
markSeenWithTtl(state.completionSeen, completionKey, Date.now(), completionTtlMs);
|
|
445
|
+
if (!observerSucceeded) {
|
|
446
|
+
scheduleResult(file, triggerTurn, RETRY_DELAY_MS);
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
if (!ownsSession(sessionId, epoch)) return;
|
|
450
|
+
if (!removeDeliveredResult(file, sessionId, runId, toolCallId)) scheduleResult(file, triggerTurn, RETRY_DELAY_MS);
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
|
|
337
454
|
const intercomTarget = data.intercomTarget?.trim();
|
|
338
455
|
let intercomDelivered = false;
|
|
339
456
|
if (deliverIntercomResults && intercomTarget && triggerTurn) {
|
|
@@ -350,7 +467,7 @@ export function createResultWatcher(
|
|
|
350
467
|
asyncDir: data.asyncDir,
|
|
351
468
|
...(data.parallelHandoff ? { parallelHandoff: data.parallelHandoff } : {}),
|
|
352
469
|
}));
|
|
353
|
-
if (!ownsSession(
|
|
470
|
+
if (!ownsSession(sessionId, epoch)) return;
|
|
354
471
|
if (!intercomDelivered) console.error(`Subagent async grouped result intercom delivery was not acknowledged for '${resultPath}'.`);
|
|
355
472
|
}
|
|
356
473
|
|
|
@@ -374,11 +491,19 @@ export function createResultWatcher(
|
|
|
374
491
|
})) : [],
|
|
375
492
|
} : {}),
|
|
376
493
|
});
|
|
377
|
-
if (!ownsSession(
|
|
494
|
+
if (!ownsSession(sessionId, epoch)) return;
|
|
378
495
|
if (!accepted) {
|
|
379
496
|
scheduleResult(file, triggerTurn, RETRY_DELAY_MS);
|
|
380
497
|
return;
|
|
381
498
|
}
|
|
499
|
+
try {
|
|
500
|
+
data = markDeliveredNotification(publicResultPath(file), data, runId, Date.now());
|
|
501
|
+
identityCache.delete(file);
|
|
502
|
+
} catch (error) {
|
|
503
|
+
console.error(`Failed to mark subagent result notification delivered for '${resultPath}'; will retry:`, error);
|
|
504
|
+
scheduleResult(file, triggerTurn, RETRY_DELAY_MS);
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
382
507
|
markSeenWithTtl(state.completionSeen, completionKey, Date.now(), completionTtlMs);
|
|
383
508
|
try {
|
|
384
509
|
pi.events.emit(SUBAGENT_ASYNC_COMPLETE_EVENT, {
|
|
@@ -403,16 +528,12 @@ export function createResultWatcher(
|
|
|
403
528
|
} catch (error) {
|
|
404
529
|
console.error(`Completion observer failed for '${resultPath}':`, error);
|
|
405
530
|
}
|
|
406
|
-
if (!
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
identityCache.delete(file);
|
|
410
|
-
} catch (error) {
|
|
411
|
-
if (!isNotFound(error)) {
|
|
412
|
-
console.error(`Failed to remove delivered subagent result '${resultPath}'; will retry:`, error);
|
|
413
|
-
scheduleResult(file, triggerTurn, RETRY_DELAY_MS);
|
|
414
|
-
}
|
|
531
|
+
if (!observerSucceeded) {
|
|
532
|
+
scheduleResult(file, triggerTurn, RETRY_DELAY_MS);
|
|
533
|
+
return;
|
|
415
534
|
}
|
|
535
|
+
if (!ownsSession(sessionId, epoch)) return;
|
|
536
|
+
if (!removeDeliveredResult(file, sessionId, runId, toolCallId)) scheduleResult(file, triggerTurn, RETRY_DELAY_MS);
|
|
416
537
|
} catch (error) {
|
|
417
538
|
if (!isNotFound(error)) console.error(`Failed to process subagent result file '${resultPath}':`, error);
|
|
418
539
|
} finally {
|
|
@@ -424,18 +545,56 @@ export function createResultWatcher(
|
|
|
424
545
|
const triggerTurn = pendingTriggerTurn.get(file) !== false;
|
|
425
546
|
pendingTriggerTurn.delete(file);
|
|
426
547
|
void handleResult(file, triggerTurn);
|
|
427
|
-
}, 50);
|
|
548
|
+
}, deps.coalesceDelayMs ?? 50);
|
|
428
549
|
|
|
550
|
+
const logScanStats = (stats: ResultScanStats) => {
|
|
551
|
+
const elapsed = Date.now() - stats.startedAt;
|
|
552
|
+
if (elapsed < SLOW_RESULT_SCAN_MS) return;
|
|
553
|
+
console.error(`Subagent result scan inspected ${stats.files} indexed result file(s), scheduled ${stats.scheduled} in ${elapsed}ms (${resultsDir}).`);
|
|
554
|
+
};
|
|
555
|
+
const indexedResultCandidates = (observed: ReadonlySet<string>): string[] => {
|
|
556
|
+
const files = new Set<string>();
|
|
557
|
+
if (state.currentSessionId) {
|
|
558
|
+
for (const file of resultCandidateFilesForSession(resultsDir, state.currentSessionId)) files.add(file);
|
|
559
|
+
}
|
|
560
|
+
for (const runId of state.asyncJobs.keys()) files.add(`${runId}.json`);
|
|
561
|
+
for (const file of missionObserverResultCandidateFiles(resultsDir)) files.add(file);
|
|
562
|
+
for (const runId of observed) files.add(`${runId}.json`);
|
|
563
|
+
return [...files];
|
|
564
|
+
};
|
|
429
565
|
const primeExistingResults = (options: { triggerTurn?: boolean } = {}) => {
|
|
430
566
|
try {
|
|
431
567
|
const triggerTurn = options.triggerTurn !== false;
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
568
|
+
const stats: ResultScanStats = { files: 0, scheduled: 0, startedAt: Date.now() };
|
|
569
|
+
const observed = observedRunIds();
|
|
570
|
+
for (const file of indexedResultCandidates(observed)) {
|
|
571
|
+
stats.files += 1;
|
|
572
|
+
const signature = resultSignature(file, observed);
|
|
573
|
+
if (!signature) continue;
|
|
574
|
+
if (!shouldProcessResult(file, observed, signature)) continue;
|
|
575
|
+
stats.scheduled += 1;
|
|
576
|
+
scheduleResult(file, triggerTurn);
|
|
577
|
+
}
|
|
578
|
+
// Selesai fork: during the lineage adoption window, also scan flat
|
|
579
|
+
// result files written directly under the results root by a previous
|
|
580
|
+
// session's runtime, so completions that raced the session switch are
|
|
581
|
+
// adopted instead of left behind. Bounded to the window; outside it,
|
|
582
|
+
// flat files are stale leftovers and only the indexed inbox is scanned.
|
|
583
|
+
if (state.adoptedResultWindowStart !== undefined) {
|
|
584
|
+
const indexed = indexedResultCandidates(observed);
|
|
585
|
+
for (const file of fsApi.readdirSync(resultsDir)) {
|
|
586
|
+
if (!file.endsWith(".json") || indexed.includes(file)) continue;
|
|
587
|
+
stats.files += 1;
|
|
588
|
+
const signature = resultSignature(file, observed);
|
|
589
|
+
if (!signature) continue;
|
|
590
|
+
if (!shouldProcessResult(file, observed, signature)) continue;
|
|
591
|
+
stats.scheduled += 1;
|
|
592
|
+
scheduleResult(file, triggerTurn);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
logScanStats(stats);
|
|
437
596
|
} catch (error) {
|
|
438
|
-
if (!isNotFound(error)) console.error(`Failed to scan subagent result
|
|
597
|
+
if (!isNotFound(error)) console.error(`Failed to scan subagent result index in '${resultsDir}':`, error);
|
|
439
598
|
}
|
|
440
599
|
};
|
|
441
600
|
|
|
@@ -492,6 +651,11 @@ export function createResultWatcher(
|
|
|
492
651
|
const fileName = file.toString();
|
|
493
652
|
if (fileName.endsWith(".json")) {
|
|
494
653
|
identityCache.delete(fileName);
|
|
654
|
+
try {
|
|
655
|
+
writeResultIndexForData(path.join(resultsDir, fileName), JSON.parse(fsApi.readFileSync(path.join(resultsDir, fileName), "utf-8")) as Record<string, unknown>);
|
|
656
|
+
} catch {
|
|
657
|
+
// The writer may still be renaming the file; handleResult will retry from the normal result path.
|
|
658
|
+
}
|
|
495
659
|
scheduleResult(fileName, true);
|
|
496
660
|
}
|
|
497
661
|
});
|
|
@@ -1,6 +1,19 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import type { AsyncRunSummary } from "./async-status.ts";
|
|
3
3
|
|
|
4
|
+
const INTERCOM_DETACH_ERROR = "detached for intercom coordination";
|
|
5
|
+
|
|
6
|
+
function isIntercomDetached(run: AsyncRunSummary): boolean {
|
|
7
|
+
return run.steps.some((step) => step.execution?.status === "detached"
|
|
8
|
+
|| step.error?.toLowerCase().includes(INTERCOM_DETACH_ERROR) === true)
|
|
9
|
+
|| run.error?.toLowerCase().includes(INTERCOM_DETACH_ERROR) === true;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function formatIntercomDetachGuidance(run: AsyncRunSummary): string | undefined {
|
|
13
|
+
if (!isIntercomDetached(run)) return undefined;
|
|
14
|
+
return `Run "${run.id}" detached for intercom coordination. Reply to the supervisor request first, then wait with subagent_wait({ id: "${run.id}" }). Use subagent({ action: "status", id: "${run.id}" }) to recover the result; do not resume or launch a replacement while it remains detached.`;
|
|
15
|
+
}
|
|
16
|
+
|
|
4
17
|
export function formatAsyncReviveCommand(run: AsyncRunSummary): string | undefined {
|
|
5
18
|
const step = run.steps.find((candidate) => candidate.status === "failed" && candidate.sessionFile && fs.existsSync(candidate.sessionFile));
|
|
6
19
|
if (!step) {
|
|
@@ -15,19 +28,26 @@ export function formatAsyncReviveCommand(run: AsyncRunSummary): string | undefin
|
|
|
15
28
|
|
|
16
29
|
export function formatResumeFirstFailedRunDetail(run: AsyncRunSummary): string | undefined {
|
|
17
30
|
if (run.state !== "failed") return undefined;
|
|
31
|
+
const detachGuidance = formatIntercomDetachGuidance(run);
|
|
32
|
+
if (detachGuidance) return detachGuidance;
|
|
18
33
|
const command = formatAsyncReviveCommand(run);
|
|
19
34
|
if (!command) return undefined;
|
|
20
35
|
return `Resume-first: failed run "${run.id}" has a persisted child session. Revive the original run with ${command} before reporting failure or launching a replacement. Launch a replacement only if revive fails or the user explicitly asks for one.`;
|
|
21
36
|
}
|
|
22
37
|
|
|
23
38
|
export function formatResumeFirstFailedRunsNote(runs: AsyncRunSummary[]): string {
|
|
24
|
-
const
|
|
25
|
-
|
|
39
|
+
const failedRuns = runs.filter((run) => run.state === "failed");
|
|
40
|
+
const detachGuidance = failedRuns
|
|
41
|
+
.map(formatIntercomDetachGuidance)
|
|
42
|
+
.filter((guidance): guidance is string => Boolean(guidance));
|
|
43
|
+
const resumable = failedRuns
|
|
44
|
+
.filter((run) => !isIntercomDetached(run))
|
|
26
45
|
.map((run) => ({ run, command: formatAsyncReviveCommand(run) }))
|
|
27
46
|
.filter((entry): entry is { run: AsyncRunSummary; command: string } => Boolean(entry.command));
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
47
|
+
const resumeGuidance = resumable.length === 0
|
|
48
|
+
? ""
|
|
49
|
+
: resumable.length === 1
|
|
50
|
+
? ` Resume-first: failed run "${resumable[0]!.run.id}" has a persisted child session. Revive the original run with ${resumable[0]!.command} before reporting failure or launching a replacement. Launch a replacement only if revive fails or the user explicitly asks for one.`
|
|
51
|
+
: ` Resume-first: ${resumable.length} failed runs have persisted child sessions. Inspect status and revive each original run before reporting failure or launching a replacement. Launch a replacement only if revive fails or the user explicitly asks for one.`;
|
|
52
|
+
return `${detachGuidance.length > 0 ? ` ${detachGuidance.join(" ")}` : ""}${resumeGuidance}`;
|
|
33
53
|
}
|
|
@@ -1,30 +1,68 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
|
-
import { listAsyncRuns } from "./async-status.ts";
|
|
2
|
+
import { listAsyncRuns, type AsyncRunSummary } from "./async-status.ts";
|
|
3
|
+
import { readAsyncRecoveryDescriptor } from "./async-resume.ts";
|
|
3
4
|
import type { TokenUsage } from "../../shared/types.ts";
|
|
4
5
|
|
|
5
6
|
const MAX_RETAINED_CHILDREN = 10;
|
|
6
7
|
const MAX_TASK_SUMMARY_LENGTH = 120;
|
|
7
8
|
|
|
9
|
+
type RetainedChildState = "complete" | "failed" | "paused" | "stopped";
|
|
10
|
+
type RetainedChildResumability =
|
|
11
|
+
| { state: "resumable"; sessionPath: string }
|
|
12
|
+
| { state: "not-resumable"; reason: string };
|
|
13
|
+
|
|
8
14
|
export interface RetainedChild {
|
|
9
15
|
runId: string;
|
|
10
16
|
parentRunId?: string;
|
|
17
|
+
workflowKey?: string;
|
|
18
|
+
state: RetainedChildState;
|
|
11
19
|
agent: string;
|
|
12
20
|
taskSummary: string;
|
|
13
21
|
completedAt: number;
|
|
14
|
-
|
|
22
|
+
resumability: RetainedChildResumability;
|
|
23
|
+
sessionPath?: string;
|
|
15
24
|
tokenTotals?: TokenUsage;
|
|
16
25
|
}
|
|
17
26
|
|
|
18
|
-
function
|
|
19
|
-
|
|
27
|
+
function isRetainedChildState(state: AsyncRunSummary["state"]): state is RetainedChildState {
|
|
28
|
+
return state === "complete" || state === "failed" || state === "paused" || state === "stopped";
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function isTerminalStepStatus(status: AsyncRunSummary["steps"][number]["status"]): boolean {
|
|
32
|
+
return status === "complete" || status === "completed" || status === "failed" || status === "paused" || status === "stopped";
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function retainedSessionFile(sessionFile: string | undefined): RetainedChildResumability {
|
|
36
|
+
if (!sessionFile) return { state: "not-resumable", reason: "no persisted session file" };
|
|
37
|
+
if (!sessionFile.endsWith(".jsonl")) return { state: "not-resumable", reason: "persisted session file is not a .jsonl file" };
|
|
20
38
|
try {
|
|
21
39
|
const stat = fs.lstatSync(sessionFile);
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
40
|
+
if (!stat.isFile() || stat.isSymbolicLink()) return { state: "not-resumable", reason: "persisted session file is not a regular file" };
|
|
41
|
+
return { state: "resumable", sessionPath: sessionFile };
|
|
42
|
+
} catch (error) {
|
|
43
|
+
if (typeof error === "object" && error !== null && "code" in error && (error as NodeJS.ErrnoException).code === "ENOENT") {
|
|
44
|
+
return { state: "not-resumable", reason: `persisted session file is missing: ${sessionFile}` };
|
|
45
|
+
}
|
|
46
|
+
return { state: "not-resumable", reason: `persisted session file could not be inspected: ${error instanceof Error ? error.message : String(error)}` };
|
|
25
47
|
}
|
|
26
48
|
}
|
|
27
49
|
|
|
50
|
+
function childResumability(run: AsyncRunSummary, step: AsyncRunSummary["steps"][number]): RetainedChildResumability {
|
|
51
|
+
if (run.state === "stopped" || step.status === "stopped") return { state: "not-resumable", reason: "stopped run" };
|
|
52
|
+
if (step.runner?.type === "external-cli") return { state: "not-resumable", reason: "external CLI runner" };
|
|
53
|
+
const session = retainedSessionFile(step.sessionFile ?? run.sessionFile);
|
|
54
|
+
if (session.state === "not-resumable") return session;
|
|
55
|
+
try {
|
|
56
|
+
const recoveryDescriptor = readAsyncRecoveryDescriptor(run.asyncDir);
|
|
57
|
+
if (!recoveryDescriptor) return { state: "not-resumable", reason: "missing recovery descriptor" };
|
|
58
|
+
if (recoveryDescriptor.sourceRunId !== run.id) return { state: "not-resumable", reason: `recovery descriptor belongs to run ${recoveryDescriptor.sourceRunId}` };
|
|
59
|
+
if (recoveryDescriptor.agent !== step.agent) return { state: "not-resumable", reason: `recovery descriptor belongs to agent ${recoveryDescriptor.agent}` };
|
|
60
|
+
} catch (error) {
|
|
61
|
+
return { state: "not-resumable", reason: `invalid recovery descriptor: ${error instanceof Error ? error.message : String(error)}` };
|
|
62
|
+
}
|
|
63
|
+
return session;
|
|
64
|
+
}
|
|
65
|
+
|
|
28
66
|
function boundedTaskSummary(value: string | undefined): string {
|
|
29
67
|
const normalized = value?.replace(/\s+/g, " ").trim() ?? "";
|
|
30
68
|
return normalized.length > MAX_TASK_SUMMARY_LENGTH
|
|
@@ -32,37 +70,56 @@ function boundedTaskSummary(value: string | undefined): string {
|
|
|
32
70
|
: normalized;
|
|
33
71
|
}
|
|
34
72
|
|
|
35
|
-
export function listRetainedChildren(asyncDirRoot: string, sessionIds: string[]): RetainedChild[] {
|
|
36
|
-
|
|
73
|
+
export function listRetainedChildren(asyncDirRoot: string, sessionIds: string | string[]): RetainedChild[] {
|
|
74
|
+
const children = listAsyncRuns(asyncDirRoot, { sessionIds: Array.isArray(sessionIds) ? sessionIds : [sessionIds], states: ["complete", "failed", "paused", "stopped"], reconcile: false })
|
|
37
75
|
.flatMap((run) => {
|
|
38
76
|
if (!run.parentWorkflowRunId || run.steps.length !== 1) return [];
|
|
77
|
+
if (!isRetainedChildState(run.state)) return [];
|
|
39
78
|
const step = run.steps[0]!;
|
|
40
|
-
if ((step.status
|
|
41
|
-
const completedAt = run.endedAt;
|
|
79
|
+
if (!isTerminalStepStatus(step.status)) return [];
|
|
80
|
+
const completedAt = run.endedAt ?? run.lastUpdate;
|
|
42
81
|
if (completedAt === undefined) return [];
|
|
82
|
+
const resumability = childResumability(run, step);
|
|
43
83
|
return [{
|
|
44
84
|
runId: run.id,
|
|
45
85
|
...(run.parentWorkflowRunId ? { parentRunId: run.parentWorkflowRunId } : {}),
|
|
86
|
+
...(run.workflowKey ? { workflowKey: run.workflowKey } : {}),
|
|
87
|
+
state: run.state,
|
|
46
88
|
agent: step.agent,
|
|
47
89
|
taskSummary: boundedTaskSummary(step.description),
|
|
48
90
|
completedAt,
|
|
49
|
-
|
|
91
|
+
resumability,
|
|
92
|
+
...(resumability.state === "resumable" ? { sessionPath: resumability.sessionPath } : {}),
|
|
50
93
|
...(step.tokens ?? run.totalTokens ? { tokenTotals: step.tokens ?? run.totalTokens } : {}),
|
|
51
94
|
}];
|
|
52
95
|
})
|
|
53
|
-
.sort((left, right) => right.completedAt - left.completedAt)
|
|
54
|
-
|
|
96
|
+
.sort((left, right) => right.completedAt - left.completedAt);
|
|
97
|
+
return children;
|
|
55
98
|
}
|
|
56
99
|
|
|
57
100
|
export function formatRetainedChildren(children: RetainedChild[]): string {
|
|
58
|
-
if (children.length === 0) return "No
|
|
101
|
+
if (children.length === 0) return "No retained workflow children in the active parent session. If a retained-writer challenge is required, launch a same-role fallback challenge and label it as fallback.";
|
|
102
|
+
const retained = children.slice(0, MAX_RETAINED_CHILDREN);
|
|
103
|
+
if (!retained.some((child) => child.resumability.state === "resumable")) {
|
|
104
|
+
const resumable = children.slice(MAX_RETAINED_CHILDREN).find((child) => child.resumability.state === "resumable");
|
|
105
|
+
if (resumable && retained.length === MAX_RETAINED_CHILDREN) retained[MAX_RETAINED_CHILDREN - 1] = resumable;
|
|
106
|
+
}
|
|
107
|
+
const hasResumableChild = retained.some((child) => child.resumability.state === "resumable");
|
|
59
108
|
return [
|
|
60
|
-
`
|
|
61
|
-
...
|
|
62
|
-
`- ${child.runId} | ${child.agent} | ${new Date(child.completedAt).toISOString()}`,
|
|
109
|
+
`Retained workflow children (up to ${MAX_RETAINED_CHILDREN}; newest first, with a resumable child retained when available):`,
|
|
110
|
+
...retained.flatMap((child) => [
|
|
111
|
+
`- ${child.runId} | ${child.agent} | ${child.state} | ${new Date(child.completedAt).toISOString()}`,
|
|
112
|
+
...(child.parentRunId ? [` workflow: ${child.parentRunId}${child.workflowKey ? ` (${child.workflowKey})` : ""}`] : []),
|
|
63
113
|
` task: ${child.taskSummary || "(no task summary)"}`,
|
|
64
|
-
|
|
114
|
+
child.resumability.state === "resumable"
|
|
115
|
+
? " resumability: resumable"
|
|
116
|
+
: ` resumability: not resumable (${child.resumability.reason})`,
|
|
117
|
+
...(child.resumability.state === "resumable" ? [
|
|
118
|
+
` session: ${child.resumability.sessionPath}`,
|
|
119
|
+
` resume: subagent({ action: "resume", id: "${child.runId}", message: "..." })`,
|
|
120
|
+
] : []),
|
|
65
121
|
...(child.tokenTotals ? [` tokens: input ${child.tokenTotals.input}, output ${child.tokenTotals.output}, total ${child.tokenTotals.total}`] : []),
|
|
66
122
|
]),
|
|
123
|
+
...(hasResumableChild ? [] : ["No resumable retained child is listed. Launch a same-role fallback challenge and label it as fallback."]),
|
|
67
124
|
].join("\n");
|
|
68
125
|
}
|