@selesai/code 0.8.6 → 0.8.8
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 +31 -1
- package/dist/cli/args.d.ts +1 -0
- package/dist/cli/args.js +11 -0
- package/dist/cli/args.test.d.ts +1 -0
- package/dist/cli/args.test.js +24 -0
- package/dist/core/agent-session.d.ts +4 -1
- package/dist/core/agent-session.js +18 -7
- package/dist/core/experimental.d.ts +4 -0
- package/dist/core/experimental.js +4 -0
- package/dist/core/extensions/types.d.ts +6 -0
- package/dist/core/keybindings.d.ts +24 -0
- package/dist/core/model-config.d.ts +5 -0
- package/dist/core/model-config.js +1 -0
- package/dist/core/project-trust.js +2 -2
- package/dist/core/provider-composer.js +17 -4
- package/dist/core/sdk.d.ts +5 -3
- package/dist/core/sdk.js +2 -1
- package/dist/core/session-manager.js +2 -2
- package/dist/core/settings-manager.d.ts +8 -0
- package/dist/core/settings-manager.js +15 -0
- package/dist/core/system-prompt.js +3 -9
- package/dist/core/system-prompt.test.js +6 -0
- package/dist/core/tools/bash.js +2 -0
- package/dist/core/tools/edit.js +2 -0
- package/dist/core/tools/find.js +1 -1
- package/dist/core/tools/grep.js +1 -1
- package/dist/core/tools/index.d.ts +1 -0
- package/dist/core/tools/index.js +1 -0
- package/dist/core/tools/read.js +2 -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/core/tools/write.js +2 -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 +187 -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 +395 -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/extensions/tokenin-onboarding.ts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/main.js +4 -0
- package/dist/modes/interactive/components/model-selector.js +2 -1
- package/dist/modes/interactive/components/settings-selector.d.ts +3 -1
- package/dist/modes/interactive/components/settings-selector.js +10 -0
- package/dist/modes/interactive/components/tool-execution.js +10 -1
- package/dist/modes/interactive/interactive-mode.d.ts +8 -2
- package/dist/modes/interactive/interactive-mode.js +81 -25
- package/dist/modes/interactive/model-catalog-refresh.d.ts +6 -0
- package/dist/modes/interactive/model-catalog-refresh.js +33 -0
- package/dist/modes/interactive/theme/dark.json +2 -0
- package/dist/modes/interactive/theme/light.json +2 -0
- package/dist/modes/interactive/theme/theme-controller.d.ts +10 -2
- package/dist/modes/interactive/theme/theme-controller.js +25 -11
- package/dist/modes/interactive/theme/theme-schema.json +9 -1
- package/dist/modes/interactive/theme/theme.d.ts +5 -3
- package/dist/modes/interactive/theme/theme.js +12 -2
- 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/dist/utils/abort.d.ts +4 -0
- package/dist/utils/abort.js +47 -0
- package/dist/utils/tools-manager.d.ts +5 -1
- package/dist/utils/tools-manager.js +12 -21
- package/docs/settings.md +6 -0
- package/package.json +6 -6
|
@@ -22,6 +22,7 @@ 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;
|
|
@@ -50,6 +51,8 @@ type ResultWatcherDeps = {
|
|
|
50
51
|
parseResult?: (raw: string) => ResultFileData;
|
|
51
52
|
/** External grouped-result transport. Disable when native completion notifications own delivery. */
|
|
52
53
|
deliverIntercomResults?: boolean;
|
|
54
|
+
/** Coalesces result-file events. Tests can lower this without changing retry timing. */
|
|
55
|
+
coalesceDelayMs?: number;
|
|
53
56
|
};
|
|
54
57
|
|
|
55
58
|
type ResultFileChild = {
|
|
@@ -79,6 +82,7 @@ type ResultFileData = CompletionNotification & {
|
|
|
79
82
|
asyncDir?: string;
|
|
80
83
|
intercomTarget?: string;
|
|
81
84
|
parallelHandoff?: ParallelHandoffReference;
|
|
85
|
+
notificationDeliveredAt?: unknown;
|
|
82
86
|
};
|
|
83
87
|
|
|
84
88
|
type ResultFileIdentity = {
|
|
@@ -134,6 +138,16 @@ function shouldPoll(error: unknown): boolean {
|
|
|
134
138
|
return code === "EMFILE" || code === "ENOSPC";
|
|
135
139
|
}
|
|
136
140
|
|
|
141
|
+
function hasDeliveredNotification(data: ResultFileData): boolean {
|
|
142
|
+
return typeof data.notificationDeliveredAt === "number" && Number.isFinite(data.notificationDeliveredAt);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function markDeliveredNotification(resultPath: string, data: ResultFileData, runId: string, now: number): ResultFileData {
|
|
146
|
+
const marked = { ...data, runId, notificationDeliveredAt: now };
|
|
147
|
+
writeAsyncResultFile(resultPath, marked);
|
|
148
|
+
return marked;
|
|
149
|
+
}
|
|
150
|
+
|
|
137
151
|
/**
|
|
138
152
|
* Watches persisted async results for the session currently owned by this
|
|
139
153
|
* runtime. `stopResultWatcher()` revokes ownership before closing resources,
|
|
@@ -194,16 +208,55 @@ export function createResultWatcher(
|
|
|
194
208
|
state.resultFileCoalescer.schedule(file, delayMs);
|
|
195
209
|
};
|
|
196
210
|
|
|
197
|
-
const
|
|
198
|
-
|
|
211
|
+
const publicResultPath = (file: string): string => path.join(resultsDir, file);
|
|
212
|
+
const publicResultFileExists = (file: string): boolean => {
|
|
213
|
+
try {
|
|
214
|
+
return fsApi.statSync(publicResultPath(file)).isFile();
|
|
215
|
+
} catch (error) {
|
|
216
|
+
if (!isNotFound(error)) console.error(`Failed to inspect subagent result file '${publicResultPath(file)}':`, error);
|
|
217
|
+
return false;
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
const resultPayloadPath = (file: string, observed?: ReadonlySet<string>): string | undefined => {
|
|
221
|
+
if (file !== path.basename(file) || !file.endsWith(".json")) return undefined;
|
|
222
|
+
const runId = file.replace(/\.json$/i, "");
|
|
223
|
+
const sessionResult = state.currentSessionId ? resultPayloadPathForSessionRun(resultsDir, state.currentSessionId, runId) : undefined;
|
|
224
|
+
if (sessionResult) return sessionResult;
|
|
225
|
+
const observerResult = resultPayloadPathForMissionObserverRun(resultsDir, runId);
|
|
226
|
+
if (observerResult) return observerResult;
|
|
227
|
+
if (observed?.has(runId)) {
|
|
228
|
+
const indexedResult = resultPayloadPathForIndexedRun(resultsDir, runId);
|
|
229
|
+
if (indexedResult) return indexedResult;
|
|
230
|
+
}
|
|
231
|
+
return publicResultFileExists(file) ? publicResultPath(file) : undefined;
|
|
232
|
+
};
|
|
233
|
+
const resultSignature = (file: string, observed?: ReadonlySet<string>): string | undefined => {
|
|
234
|
+
const resultPath = resultPayloadPath(file, observed);
|
|
235
|
+
if (!resultPath) {
|
|
236
|
+
identityCache.delete(file);
|
|
237
|
+
return undefined;
|
|
238
|
+
}
|
|
199
239
|
try {
|
|
200
240
|
const stat = fsApi.statSync(resultPath);
|
|
201
|
-
|
|
241
|
+
if (!stat.isFile()) return undefined;
|
|
242
|
+
return `${resultPath}:${stat.size}:${stat.mtimeMs}`;
|
|
243
|
+
} catch (error) {
|
|
244
|
+
identityCache.delete(file);
|
|
245
|
+
if (!isNotFound(error)) console.error(`Failed to inspect subagent result file '${resultPath}':`, error);
|
|
246
|
+
return undefined;
|
|
247
|
+
}
|
|
248
|
+
};
|
|
249
|
+
const inspectResult = (file: string, knownSignature?: string, observed?: ReadonlySet<string>): { identity: ResultFileIdentity; signature: string } | undefined => {
|
|
250
|
+
const resultPath = resultPayloadPath(file, observed);
|
|
251
|
+
if (!resultPath) return undefined;
|
|
252
|
+
try {
|
|
253
|
+
const signature = knownSignature ?? resultSignature(file, observed);
|
|
254
|
+
if (!signature) return undefined;
|
|
202
255
|
const cached = identityCache.get(file);
|
|
203
|
-
if (cached?.signature === signature) return cached.identity;
|
|
256
|
+
if (cached?.signature === signature) return { identity: cached.identity, signature };
|
|
204
257
|
const identity = resultFileIdentity(fsApi.readFileSync(resultPath, "utf-8"), file);
|
|
205
258
|
identityCache.set(file, { signature, identity });
|
|
206
|
-
return identity;
|
|
259
|
+
return { identity, signature };
|
|
207
260
|
} catch (error) {
|
|
208
261
|
identityCache.delete(file);
|
|
209
262
|
if (!isNotFound(error)) console.error(`Failed to inspect subagent result file '${resultPath}':`, error);
|
|
@@ -220,9 +273,10 @@ export function createResultWatcher(
|
|
|
220
273
|
}
|
|
221
274
|
};
|
|
222
275
|
|
|
223
|
-
const shouldProcessResult = (file: string, observed?: ReadonlySet<string
|
|
224
|
-
const
|
|
225
|
-
if (!
|
|
276
|
+
const shouldProcessResult = (file: string, observed?: ReadonlySet<string>, knownSignature?: string): boolean => {
|
|
277
|
+
const inspected = inspectResult(file, knownSignature, observed);
|
|
278
|
+
if (!inspected) return false;
|
|
279
|
+
const { identity } = inspected;
|
|
226
280
|
// Missing identity stays on the normal parser path so malformed or legacy
|
|
227
281
|
// files keep their existing diagnostics and compatibility behavior.
|
|
228
282
|
if (!identity.sessionId) return true;
|
|
@@ -233,32 +287,80 @@ export function createResultWatcher(
|
|
|
233
287
|
return Boolean(deps.observeCompletion && !deps.observedCompletionRunIds);
|
|
234
288
|
};
|
|
235
289
|
|
|
290
|
+
const removeDeliveredResult = (file: string, sessionId: string, runId: string, toolCallId: string | undefined): boolean => {
|
|
291
|
+
try {
|
|
292
|
+
if (publicResultFileExists(file)) fsApi.unlinkSync(publicResultPath(file));
|
|
293
|
+
identityCache.delete(file);
|
|
294
|
+
removeResultIndex(resultsDir, sessionId, runId, toolCallId);
|
|
295
|
+
return true;
|
|
296
|
+
} catch (error) {
|
|
297
|
+
if (!isNotFound(error)) {
|
|
298
|
+
console.error(`Failed to remove delivered subagent result '${publicResultPath(file)}'; will retry:`, error);
|
|
299
|
+
return false;
|
|
300
|
+
}
|
|
301
|
+
return true;
|
|
302
|
+
}
|
|
303
|
+
};
|
|
236
304
|
const handleResult = async (file: string, triggerTurn: boolean) => {
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
if (!shouldProcessResult(file))
|
|
305
|
+
if (processing.has(file)) return;
|
|
306
|
+
let observed: ReadonlySet<string> | undefined;
|
|
307
|
+
if (!shouldProcessResult(file)) {
|
|
308
|
+
const runId = file === path.basename(file) && file.endsWith(".json") ? file.replace(/\.json$/i, "") : undefined;
|
|
309
|
+
observed = observedRunIds();
|
|
310
|
+
if (!runId || !observed.has(runId) || !shouldProcessResult(file, observed)) return;
|
|
311
|
+
}
|
|
240
312
|
processing.add(file);
|
|
313
|
+
let resultPath = publicResultPath(file);
|
|
241
314
|
try {
|
|
242
|
-
const
|
|
315
|
+
const payloadPath = resultPayloadPath(file, observed);
|
|
316
|
+
if (!payloadPath) return;
|
|
317
|
+
resultPath = payloadPath;
|
|
318
|
+
let raw = fsApi.readFileSync(resultPath, "utf-8");
|
|
319
|
+
let identity = resultFileIdentity(raw, file);
|
|
320
|
+
if (identity.sessionId && identity.runId) {
|
|
321
|
+
const pendingState = promotePendingResultFile(resultsDir, identity.sessionId, identity.runId, file);
|
|
322
|
+
if (pendingState === "promoted") {
|
|
323
|
+
identityCache.delete(file);
|
|
324
|
+
resultPath = publicResultPath(file);
|
|
325
|
+
raw = fsApi.readFileSync(resultPath, "utf-8");
|
|
326
|
+
identity = resultFileIdentity(raw, file);
|
|
327
|
+
} else if (pendingState === "pending") {
|
|
328
|
+
const pendingPath = resultPayloadPathForSessionRun(resultsDir, identity.sessionId, identity.runId);
|
|
329
|
+
if (!pendingPath) {
|
|
330
|
+
scheduleResult(file, triggerTurn, RETRY_DELAY_MS);
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
resultPath = pendingPath;
|
|
334
|
+
raw = fsApi.readFileSync(resultPath, "utf-8");
|
|
335
|
+
identity = resultFileIdentity(raw, file);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
let data = parseResult(raw);
|
|
243
339
|
if (typeof data.sessionId !== "string" || !data.sessionId) return;
|
|
340
|
+
const sessionId = data.sessionId;
|
|
244
341
|
const runId = data.runId ?? data.id ?? file.replace(/\.json$/i, "");
|
|
342
|
+
const toolCallId = typeof data.toolCallId === "string" ? data.toolCallId : undefined;
|
|
343
|
+
let observerSucceeded = true;
|
|
245
344
|
try {
|
|
246
345
|
syncMissionFromAsyncCompletion({ ...data, runId });
|
|
247
346
|
} catch (error) {
|
|
347
|
+
observerSucceeded = false;
|
|
248
348
|
console.error(`Mission completion sync failed for '${resultPath}':`, error);
|
|
249
349
|
}
|
|
250
350
|
try {
|
|
251
351
|
deps.observeCompletion?.({ ...data, runId });
|
|
252
352
|
} catch (error) {
|
|
353
|
+
observerSucceeded = false;
|
|
253
354
|
console.error(`Completion observer failed for '${resultPath}':`, error);
|
|
254
355
|
}
|
|
356
|
+
if (observerSucceeded) removeMissionObserverIndex(resultsDir, runId);
|
|
255
357
|
const epoch = deliveryEpoch;
|
|
256
|
-
if (!ownsSession(
|
|
358
|
+
if (!ownsSession(sessionId, epoch)) return;
|
|
257
359
|
// Recorded before dedupe and before the unlink below so subagent_wait can
|
|
258
360
|
// use the in-memory record or its bounded durable replay after cleanup.
|
|
259
361
|
recordWaitCompletion(state, runId, data, Date.now(), completionTtlMs, {
|
|
260
362
|
resultsDir,
|
|
261
|
-
sessionId
|
|
363
|
+
sessionId,
|
|
262
364
|
});
|
|
263
365
|
const hasExplicitNestedChildren = data.nestedChildren !== undefined;
|
|
264
366
|
let nestedChildren = compactNestedResultChildren(sanitizeNestedResultChildren(data.nestedChildren, resultPath, "nestedChildren"));
|
|
@@ -273,20 +375,17 @@ export function createResultWatcher(
|
|
|
273
375
|
}
|
|
274
376
|
|
|
275
377
|
const completionKey = buildCompletionKey(data, `result:${file}`);
|
|
378
|
+
const alreadyDelivered = hasDeliveredNotification(data);
|
|
276
379
|
const lastSeenAt = state.completionSeen.get(completionKey);
|
|
277
380
|
if (lastSeenAt !== undefined && Date.now() - lastSeenAt > completionTtlMs) {
|
|
278
381
|
state.completionSeen.delete(completionKey);
|
|
279
382
|
} 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
|
-
}
|
|
383
|
+
if (!observerSucceeded) {
|
|
384
|
+
scheduleResult(file, triggerTurn, RETRY_DELAY_MS);
|
|
385
|
+
return;
|
|
289
386
|
}
|
|
387
|
+
if (!ownsSession(sessionId, epoch)) return;
|
|
388
|
+
if (!removeDeliveredResult(file, sessionId, runId, toolCallId)) scheduleResult(file, triggerTurn, RETRY_DELAY_MS);
|
|
290
389
|
return;
|
|
291
390
|
}
|
|
292
391
|
|
|
@@ -334,6 +433,17 @@ export function createResultWatcher(
|
|
|
334
433
|
};
|
|
335
434
|
}), nestedChildren);
|
|
336
435
|
|
|
436
|
+
if (alreadyDelivered) {
|
|
437
|
+
markSeenWithTtl(state.completionSeen, completionKey, Date.now(), completionTtlMs);
|
|
438
|
+
if (!observerSucceeded) {
|
|
439
|
+
scheduleResult(file, triggerTurn, RETRY_DELAY_MS);
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
if (!ownsSession(sessionId, epoch)) return;
|
|
443
|
+
if (!removeDeliveredResult(file, sessionId, runId, toolCallId)) scheduleResult(file, triggerTurn, RETRY_DELAY_MS);
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
|
|
337
447
|
const intercomTarget = data.intercomTarget?.trim();
|
|
338
448
|
let intercomDelivered = false;
|
|
339
449
|
if (deliverIntercomResults && intercomTarget && triggerTurn) {
|
|
@@ -350,7 +460,7 @@ export function createResultWatcher(
|
|
|
350
460
|
asyncDir: data.asyncDir,
|
|
351
461
|
...(data.parallelHandoff ? { parallelHandoff: data.parallelHandoff } : {}),
|
|
352
462
|
}));
|
|
353
|
-
if (!ownsSession(
|
|
463
|
+
if (!ownsSession(sessionId, epoch)) return;
|
|
354
464
|
if (!intercomDelivered) console.error(`Subagent async grouped result intercom delivery was not acknowledged for '${resultPath}'.`);
|
|
355
465
|
}
|
|
356
466
|
|
|
@@ -374,11 +484,19 @@ export function createResultWatcher(
|
|
|
374
484
|
})) : [],
|
|
375
485
|
} : {}),
|
|
376
486
|
});
|
|
377
|
-
if (!ownsSession(
|
|
487
|
+
if (!ownsSession(sessionId, epoch)) return;
|
|
378
488
|
if (!accepted) {
|
|
379
489
|
scheduleResult(file, triggerTurn, RETRY_DELAY_MS);
|
|
380
490
|
return;
|
|
381
491
|
}
|
|
492
|
+
try {
|
|
493
|
+
data = markDeliveredNotification(publicResultPath(file), data, runId, Date.now());
|
|
494
|
+
identityCache.delete(file);
|
|
495
|
+
} catch (error) {
|
|
496
|
+
console.error(`Failed to mark subagent result notification delivered for '${resultPath}'; will retry:`, error);
|
|
497
|
+
scheduleResult(file, triggerTurn, RETRY_DELAY_MS);
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
382
500
|
markSeenWithTtl(state.completionSeen, completionKey, Date.now(), completionTtlMs);
|
|
383
501
|
try {
|
|
384
502
|
pi.events.emit(SUBAGENT_ASYNC_COMPLETE_EVENT, {
|
|
@@ -403,16 +521,12 @@ export function createResultWatcher(
|
|
|
403
521
|
} catch (error) {
|
|
404
522
|
console.error(`Completion observer failed for '${resultPath}':`, error);
|
|
405
523
|
}
|
|
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
|
-
}
|
|
524
|
+
if (!observerSucceeded) {
|
|
525
|
+
scheduleResult(file, triggerTurn, RETRY_DELAY_MS);
|
|
526
|
+
return;
|
|
415
527
|
}
|
|
528
|
+
if (!ownsSession(sessionId, epoch)) return;
|
|
529
|
+
if (!removeDeliveredResult(file, sessionId, runId, toolCallId)) scheduleResult(file, triggerTurn, RETRY_DELAY_MS);
|
|
416
530
|
} catch (error) {
|
|
417
531
|
if (!isNotFound(error)) console.error(`Failed to process subagent result file '${resultPath}':`, error);
|
|
418
532
|
} finally {
|
|
@@ -424,18 +538,45 @@ export function createResultWatcher(
|
|
|
424
538
|
const triggerTurn = pendingTriggerTurn.get(file) !== false;
|
|
425
539
|
pendingTriggerTurn.delete(file);
|
|
426
540
|
void handleResult(file, triggerTurn);
|
|
427
|
-
}, 50);
|
|
541
|
+
}, deps.coalesceDelayMs ?? 50);
|
|
428
542
|
|
|
543
|
+
const indexedResultCandidates = (observed: ReadonlySet<string>): string[] => {
|
|
544
|
+
const files = new Set<string>();
|
|
545
|
+
if (state.currentSessionId) {
|
|
546
|
+
for (const file of resultCandidateFilesForSession(resultsDir, state.currentSessionId)) files.add(file);
|
|
547
|
+
}
|
|
548
|
+
for (const runId of state.asyncJobs.keys()) files.add(`${runId}.json`);
|
|
549
|
+
for (const file of missionObserverResultCandidateFiles(resultsDir)) files.add(file);
|
|
550
|
+
for (const runId of observed) files.add(`${runId}.json`);
|
|
551
|
+
return [...files];
|
|
552
|
+
};
|
|
429
553
|
const primeExistingResults = (options: { triggerTurn?: boolean } = {}) => {
|
|
430
554
|
try {
|
|
431
555
|
const triggerTurn = options.triggerTurn !== false;
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
556
|
+
const observed = observedRunIds();
|
|
557
|
+
for (const file of indexedResultCandidates(observed)) {
|
|
558
|
+
const signature = resultSignature(file, observed);
|
|
559
|
+
if (!signature) continue;
|
|
560
|
+
if (!shouldProcessResult(file, observed, signature)) continue;
|
|
561
|
+
scheduleResult(file, triggerTurn);
|
|
562
|
+
}
|
|
563
|
+
// Selesai fork: during the lineage adoption window, also scan flat
|
|
564
|
+
// result files written directly under the results root by a previous
|
|
565
|
+
// session's runtime, so completions that raced the session switch are
|
|
566
|
+
// adopted instead of left behind. Bounded to the window; outside it,
|
|
567
|
+
// flat files are stale leftovers and only the indexed inbox is scanned.
|
|
568
|
+
if (state.adoptedResultWindowStart !== undefined) {
|
|
569
|
+
const indexed = indexedResultCandidates(observed);
|
|
570
|
+
for (const file of fsApi.readdirSync(resultsDir)) {
|
|
571
|
+
if (!file.endsWith(".json") || indexed.includes(file)) continue;
|
|
572
|
+
const signature = resultSignature(file, observed);
|
|
573
|
+
if (!signature) continue;
|
|
574
|
+
if (!shouldProcessResult(file, observed, signature)) continue;
|
|
575
|
+
scheduleResult(file, triggerTurn);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
437
578
|
} catch (error) {
|
|
438
|
-
if (!isNotFound(error)) console.error(`Failed to scan subagent result
|
|
579
|
+
if (!isNotFound(error)) console.error(`Failed to scan subagent result index in '${resultsDir}':`, error);
|
|
439
580
|
}
|
|
440
581
|
};
|
|
441
582
|
|
|
@@ -492,6 +633,11 @@ export function createResultWatcher(
|
|
|
492
633
|
const fileName = file.toString();
|
|
493
634
|
if (fileName.endsWith(".json")) {
|
|
494
635
|
identityCache.delete(fileName);
|
|
636
|
+
try {
|
|
637
|
+
writeResultIndexForData(path.join(resultsDir, fileName), JSON.parse(fsApi.readFileSync(path.join(resultsDir, fileName), "utf-8")) as Record<string, unknown>);
|
|
638
|
+
} catch {
|
|
639
|
+
// The writer may still be renaming the file; handleResult will retry from the normal result path.
|
|
640
|
+
}
|
|
495
641
|
scheduleResult(fileName, true);
|
|
496
642
|
}
|
|
497
643
|
});
|
|
@@ -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
|
}
|