@selesai/code 0.8.5 → 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 +28 -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/models.json +5 -0
- package/dist/defaults/settings.json +1 -0
- package/dist/extensions/agent-browser.test.ts +178 -2
- package/dist/extensions/agent-browser.ts +2 -0
- package/dist/extensions/copy-turn.test.ts +229 -0
- package/dist/extensions/copy-turn.ts +11 -0
- package/dist/extensions/grep-app/index.test.ts +356 -1
- package/dist/extensions/grep-app/index.ts +6 -1
- package/dist/extensions/handoff-new.test.ts +351 -161
- package/dist/extensions/handoff-new.ts +3 -0
- package/dist/extensions/inline-skills.test.ts +93 -0
- package/dist/extensions/inline-skills.ts +3 -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/builder.md +5 -2
- package/dist/extensions/pi-subagents/agents/commentator.md +1 -1
- 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 -11
- 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 +4 -4
- 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 +16 -15
- 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 +59 -0
- package/dist/extensions/pi-subagents/src/extension/index.ts +146 -23
- 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 +17 -6
- 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 +11 -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/notify.ts +5 -2
- package/dist/extensions/pi-subagents/src/runs/background/result-files.ts +437 -0
- package/dist/extensions/pi-subagents/src/runs/background/result-watcher.ts +224 -42
- 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 +35 -26
- package/dist/extensions/pi-subagents/src/runs/background/run-status.ts +108 -9
- 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/background/wait-subscriptions.ts +2 -1
- 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/foreground-history.ts +7 -4
- package/dist/extensions/pi-subagents/src/runs/foreground/prompt-audit.ts +4 -3
- package/dist/extensions/pi-subagents/src/runs/foreground/subagent-executor.ts +368 -50
- 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/session-lineage.ts +71 -0
- package/dist/extensions/pi-subagents/src/shared/types.ts +66 -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 +129 -17
- 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/src/workflows/workflow-auto-relaunch.ts +28 -0
- 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/handoff-adoption.test.ts +103 -0
- 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 +91 -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/session-lineage.test.ts +73 -0
- 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-auto-relaunch.test.ts +45 -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/ponytail/index.js +11 -0
- package/dist/extensions/ponytail/ponytail-config.cjs +2 -0
- package/dist/extensions/ponytail/ponytail-instructions.cjs +6 -0
- package/dist/extensions/ponytail/test/extension.test.js +274 -140
- package/dist/extensions/ponytail/test/helpers.test.js +280 -92
- package/dist/extensions/question/index.ts +33 -0
- package/dist/extensions/question/question-list.ts +28 -0
- package/dist/extensions/question/row-layout.ts +3 -0
- package/dist/extensions/question/tests/batch.test.ts +103 -65
- 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 +75 -35
- package/dist/extensions/question/tests/question-list.test.ts +640 -198
- package/dist/extensions/question/tests/row-layout.test.ts +193 -114
- package/dist/extensions/question/tests/schemas.test.ts +42 -19
- package/dist/extensions/question/tests/selection-mode.test.ts +54 -0
- package/dist/extensions/question/tests/shortcuts.test.ts +106 -84
- package/dist/extensions/question/tests/wizard.test.ts +771 -50
- 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/rtk.test.ts +180 -1
- package/dist/extensions/tokenin-onboarding.ts +3 -0
- package/dist/extensions/undo.test.ts +720 -0
- package/dist/extensions/undo.ts +6 -0
- package/dist/extensions/web-agent-onboarding.test.ts +415 -0
- package/dist/extensions/workflow/extension.ts +3 -3
- package/dist/extensions/workflow/modes.ts +41 -19
- 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 +16 -15
- 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/plans/workflow-autoloop-reference.md +1 -2
- package/docs/plans/workflow-handoff-carryover.md +108 -0
- package/docs/settings.md +6 -0
- package/package.json +4 -1
|
@@ -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,
|
|
@@ -168,7 +189,24 @@ export function createResultWatcher(
|
|
|
168
189
|
const ownsSession = (sessionId: string, epoch: number) => {
|
|
169
190
|
if (!deliveryActive || epoch !== deliveryEpoch) return false;
|
|
170
191
|
if (!activeSessionId && state.currentSessionId) activeSessionId = state.currentSessionId;
|
|
171
|
-
|
|
192
|
+
if (activeSessionId !== state.currentSessionId) return false;
|
|
193
|
+
if (sessionId === state.currentSessionId) return true;
|
|
194
|
+
return (state.sessionLineage ?? []).includes(sessionId);
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
const lineageOwnsResult = (identity: ResultFileIdentity, file: string): boolean => {
|
|
198
|
+
const lineage = state.sessionLineage;
|
|
199
|
+
if (!identity.sessionId || !lineage || !lineage.includes(identity.sessionId)) return false;
|
|
200
|
+
// Replay guard: only accept lineage results written around or after the
|
|
201
|
+
// adoption window. The previous session's watcher deleted every result it
|
|
202
|
+
// delivered, so older files are stale leftovers, not missed deliveries.
|
|
203
|
+
const windowStart = state.adoptedResultWindowStart;
|
|
204
|
+
if (windowStart === undefined) return false;
|
|
205
|
+
try {
|
|
206
|
+
return fsApi.statSync(path.join(resultsDir, file)).mtimeMs >= windowStart;
|
|
207
|
+
} catch {
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
172
210
|
};
|
|
173
211
|
|
|
174
212
|
const scheduleResult = (file: string, triggerTurn: boolean, delayMs = 0) => {
|
|
@@ -177,16 +215,55 @@ export function createResultWatcher(
|
|
|
177
215
|
state.resultFileCoalescer.schedule(file, delayMs);
|
|
178
216
|
};
|
|
179
217
|
|
|
180
|
-
const
|
|
181
|
-
|
|
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
|
+
}
|
|
182
246
|
try {
|
|
183
247
|
const stat = fsApi.statSync(resultPath);
|
|
184
|
-
|
|
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;
|
|
185
262
|
const cached = identityCache.get(file);
|
|
186
|
-
if (cached?.signature === signature) return cached.identity;
|
|
263
|
+
if (cached?.signature === signature) return { identity: cached.identity, signature };
|
|
187
264
|
const identity = resultFileIdentity(fsApi.readFileSync(resultPath, "utf-8"), file);
|
|
188
265
|
identityCache.set(file, { signature, identity });
|
|
189
|
-
return identity;
|
|
266
|
+
return { identity, signature };
|
|
190
267
|
} catch (error) {
|
|
191
268
|
identityCache.delete(file);
|
|
192
269
|
if (!isNotFound(error)) console.error(`Failed to inspect subagent result file '${resultPath}':`, error);
|
|
@@ -203,44 +280,94 @@ export function createResultWatcher(
|
|
|
203
280
|
}
|
|
204
281
|
};
|
|
205
282
|
|
|
206
|
-
const shouldProcessResult = (file: string, observed?: ReadonlySet<string
|
|
207
|
-
const
|
|
208
|
-
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;
|
|
209
287
|
// Missing identity stays on the normal parser path so malformed or legacy
|
|
210
288
|
// files keep their existing diagnostics and compatibility behavior.
|
|
211
289
|
if (!identity.sessionId) return true;
|
|
212
290
|
if (identity.sessionId === state.currentSessionId) return true;
|
|
291
|
+
if (lineageOwnsResult(identity, file)) return true;
|
|
213
292
|
if (identity.asyncDir && fsApi.existsSync(path.join(identity.asyncDir, MISSION_BINDING_FILE))) return true;
|
|
214
293
|
if (identity.runId && (observed ?? observedRunIds()).has(identity.runId)) return true;
|
|
215
294
|
return Boolean(deps.observeCompletion && !deps.observedCompletionRunIds);
|
|
216
295
|
};
|
|
217
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
|
+
};
|
|
218
311
|
const handleResult = async (file: string, triggerTurn: boolean) => {
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
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
|
+
}
|
|
222
319
|
processing.add(file);
|
|
320
|
+
let resultPath = publicResultPath(file);
|
|
223
321
|
try {
|
|
224
|
-
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);
|
|
225
346
|
if (typeof data.sessionId !== "string" || !data.sessionId) return;
|
|
347
|
+
const sessionId = data.sessionId;
|
|
226
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;
|
|
227
351
|
try {
|
|
228
352
|
syncMissionFromAsyncCompletion({ ...data, runId });
|
|
229
353
|
} catch (error) {
|
|
354
|
+
observerSucceeded = false;
|
|
230
355
|
console.error(`Mission completion sync failed for '${resultPath}':`, error);
|
|
231
356
|
}
|
|
232
357
|
try {
|
|
233
358
|
deps.observeCompletion?.({ ...data, runId });
|
|
234
359
|
} catch (error) {
|
|
360
|
+
observerSucceeded = false;
|
|
235
361
|
console.error(`Completion observer failed for '${resultPath}':`, error);
|
|
236
362
|
}
|
|
363
|
+
if (observerSucceeded) removeMissionObserverIndex(resultsDir, runId);
|
|
237
364
|
const epoch = deliveryEpoch;
|
|
238
|
-
if (!ownsSession(
|
|
365
|
+
if (!ownsSession(sessionId, epoch)) return;
|
|
239
366
|
// Recorded before dedupe and before the unlink below so subagent_wait can
|
|
240
367
|
// use the in-memory record or its bounded durable replay after cleanup.
|
|
241
368
|
recordWaitCompletion(state, runId, data, Date.now(), completionTtlMs, {
|
|
242
369
|
resultsDir,
|
|
243
|
-
sessionId
|
|
370
|
+
sessionId,
|
|
244
371
|
});
|
|
245
372
|
const hasExplicitNestedChildren = data.nestedChildren !== undefined;
|
|
246
373
|
let nestedChildren = compactNestedResultChildren(sanitizeNestedResultChildren(data.nestedChildren, resultPath, "nestedChildren"));
|
|
@@ -255,20 +382,17 @@ export function createResultWatcher(
|
|
|
255
382
|
}
|
|
256
383
|
|
|
257
384
|
const completionKey = buildCompletionKey(data, `result:${file}`);
|
|
385
|
+
const alreadyDelivered = hasDeliveredNotification(data);
|
|
258
386
|
const lastSeenAt = state.completionSeen.get(completionKey);
|
|
259
387
|
if (lastSeenAt !== undefined && Date.now() - lastSeenAt > completionTtlMs) {
|
|
260
388
|
state.completionSeen.delete(completionKey);
|
|
261
389
|
} else if (lastSeenAt !== undefined) {
|
|
262
|
-
if (!
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
identityCache.delete(file);
|
|
266
|
-
} catch (error) {
|
|
267
|
-
if (!isNotFound(error)) {
|
|
268
|
-
console.error(`Failed to remove delivered subagent result '${resultPath}'; will retry:`, error);
|
|
269
|
-
scheduleResult(file, triggerTurn, RETRY_DELAY_MS);
|
|
270
|
-
}
|
|
390
|
+
if (!observerSucceeded) {
|
|
391
|
+
scheduleResult(file, triggerTurn, RETRY_DELAY_MS);
|
|
392
|
+
return;
|
|
271
393
|
}
|
|
394
|
+
if (!ownsSession(sessionId, epoch)) return;
|
|
395
|
+
if (!removeDeliveredResult(file, sessionId, runId, toolCallId)) scheduleResult(file, triggerTurn, RETRY_DELAY_MS);
|
|
272
396
|
return;
|
|
273
397
|
}
|
|
274
398
|
|
|
@@ -316,6 +440,17 @@ export function createResultWatcher(
|
|
|
316
440
|
};
|
|
317
441
|
}), nestedChildren);
|
|
318
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
|
+
|
|
319
454
|
const intercomTarget = data.intercomTarget?.trim();
|
|
320
455
|
let intercomDelivered = false;
|
|
321
456
|
if (deliverIntercomResults && intercomTarget && triggerTurn) {
|
|
@@ -332,7 +467,7 @@ export function createResultWatcher(
|
|
|
332
467
|
asyncDir: data.asyncDir,
|
|
333
468
|
...(data.parallelHandoff ? { parallelHandoff: data.parallelHandoff } : {}),
|
|
334
469
|
}));
|
|
335
|
-
if (!ownsSession(
|
|
470
|
+
if (!ownsSession(sessionId, epoch)) return;
|
|
336
471
|
if (!intercomDelivered) console.error(`Subagent async grouped result intercom delivery was not acknowledged for '${resultPath}'.`);
|
|
337
472
|
}
|
|
338
473
|
|
|
@@ -356,11 +491,19 @@ export function createResultWatcher(
|
|
|
356
491
|
})) : [],
|
|
357
492
|
} : {}),
|
|
358
493
|
});
|
|
359
|
-
if (!ownsSession(
|
|
494
|
+
if (!ownsSession(sessionId, epoch)) return;
|
|
360
495
|
if (!accepted) {
|
|
361
496
|
scheduleResult(file, triggerTurn, RETRY_DELAY_MS);
|
|
362
497
|
return;
|
|
363
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
|
+
}
|
|
364
507
|
markSeenWithTtl(state.completionSeen, completionKey, Date.now(), completionTtlMs);
|
|
365
508
|
try {
|
|
366
509
|
pi.events.emit(SUBAGENT_ASYNC_COMPLETE_EVENT, {
|
|
@@ -385,16 +528,12 @@ export function createResultWatcher(
|
|
|
385
528
|
} catch (error) {
|
|
386
529
|
console.error(`Completion observer failed for '${resultPath}':`, error);
|
|
387
530
|
}
|
|
388
|
-
if (!
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
identityCache.delete(file);
|
|
392
|
-
} catch (error) {
|
|
393
|
-
if (!isNotFound(error)) {
|
|
394
|
-
console.error(`Failed to remove delivered subagent result '${resultPath}'; will retry:`, error);
|
|
395
|
-
scheduleResult(file, triggerTurn, RETRY_DELAY_MS);
|
|
396
|
-
}
|
|
531
|
+
if (!observerSucceeded) {
|
|
532
|
+
scheduleResult(file, triggerTurn, RETRY_DELAY_MS);
|
|
533
|
+
return;
|
|
397
534
|
}
|
|
535
|
+
if (!ownsSession(sessionId, epoch)) return;
|
|
536
|
+
if (!removeDeliveredResult(file, sessionId, runId, toolCallId)) scheduleResult(file, triggerTurn, RETRY_DELAY_MS);
|
|
398
537
|
} catch (error) {
|
|
399
538
|
if (!isNotFound(error)) console.error(`Failed to process subagent result file '${resultPath}':`, error);
|
|
400
539
|
} finally {
|
|
@@ -406,18 +545,56 @@ export function createResultWatcher(
|
|
|
406
545
|
const triggerTurn = pendingTriggerTurn.get(file) !== false;
|
|
407
546
|
pendingTriggerTurn.delete(file);
|
|
408
547
|
void handleResult(file, triggerTurn);
|
|
409
|
-
}, 50);
|
|
548
|
+
}, deps.coalesceDelayMs ?? 50);
|
|
410
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
|
+
};
|
|
411
565
|
const primeExistingResults = (options: { triggerTurn?: boolean } = {}) => {
|
|
412
566
|
try {
|
|
413
567
|
const triggerTurn = options.triggerTurn !== false;
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
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);
|
|
419
596
|
} catch (error) {
|
|
420
|
-
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);
|
|
421
598
|
}
|
|
422
599
|
};
|
|
423
600
|
|
|
@@ -474,6 +651,11 @@ export function createResultWatcher(
|
|
|
474
651
|
const fileName = file.toString();
|
|
475
652
|
if (fileName.endsWith(".json")) {
|
|
476
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
|
+
}
|
|
477
659
|
scheduleResult(fileName, true);
|
|
478
660
|
}
|
|
479
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,
|
|
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
|
}
|