@selesai/code 0.8.6 → 0.8.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -0
- package/dist/core/agent-session.js +13 -1
- package/dist/core/extensions/types.d.ts +2 -0
- package/dist/core/provider-composer.js +17 -4
- package/dist/core/settings-manager.d.ts +2 -0
- package/dist/core/settings-manager.js +3 -0
- package/dist/core/system-prompt.js +2 -8
- package/dist/core/system-prompt.test.js +6 -0
- package/dist/core/tools/index.d.ts +1 -0
- package/dist/core/tools/index.js +1 -0
- package/dist/core/tools/schema-prune.d.ts +16 -0
- package/dist/core/tools/schema-prune.js +39 -0
- package/dist/core/tools/schema-prune.test.d.ts +1 -0
- package/dist/core/tools/schema-prune.test.js +111 -0
- package/dist/defaults/settings.json +1 -0
- package/dist/extensions/model-prompt-injector/config.json +10 -0
- package/dist/extensions/model-prompt-injector/index.test.ts +112 -0
- package/dist/extensions/model-prompt-injector/index.ts +151 -0
- package/dist/extensions/package.json +1 -0
- package/dist/extensions/pi-subagents/CHANGELOG.md +158 -90
- package/dist/extensions/pi-subagents/UPSTREAM-V0.50-MAPPING.md +172 -0
- package/dist/extensions/pi-subagents/agents/oracle.md +7 -5
- package/dist/extensions/pi-subagents/agents/reviewer.md +2 -2
- package/dist/extensions/pi-subagents/agents/scout.md +1 -1
- package/dist/extensions/pi-subagents/agents/worker.md +1 -1
- package/dist/extensions/pi-subagents/docs/configuration.md +78 -19
- package/dist/extensions/pi-subagents/docs/extension-api.md +36 -0
- package/dist/extensions/pi-subagents/docs/missions.md +5 -3
- package/dist/extensions/pi-subagents/docs/models.md +1 -1
- package/dist/extensions/pi-subagents/docs/observability.md +42 -2
- package/dist/extensions/pi-subagents/docs/tool-reference.md +20 -3
- package/dist/extensions/pi-subagents/docs/workflows.md +5 -5
- package/dist/extensions/pi-subagents/install.mjs +2 -2
- package/dist/extensions/pi-subagents/package-lock.json +1470 -1472
- package/dist/extensions/pi-subagents/package.json +1 -1
- package/dist/extensions/pi-subagents/skills/pi-subagents/SKILL.md +1 -1
- package/dist/extensions/pi-subagents/skills/pi-subagents/references/constraints-and-recipes.md +5 -4
- package/dist/extensions/pi-subagents/skills/pi-subagents/references/execution-controls.md +15 -14
- package/dist/extensions/pi-subagents/skills/pi-subagents/references/management-authoring-rpc.md +5 -5
- package/dist/extensions/pi-subagents/skills/pi-subagents/references/prompting-and-roles.md +46 -26
- package/dist/extensions/pi-subagents/src/agents/agent-serializer.ts +2 -0
- package/dist/extensions/pi-subagents/src/agents/agents.ts +37 -12
- package/dist/extensions/pi-subagents/src/api/external-runs.ts +174 -84
- package/dist/extensions/pi-subagents/src/api/preflight.ts +13 -7
- package/dist/extensions/pi-subagents/src/extension/config.ts +53 -0
- package/dist/extensions/pi-subagents/src/extension/index.ts +67 -13
- package/dist/extensions/pi-subagents/src/extension/public-execution.ts +34 -6
- package/dist/extensions/pi-subagents/src/extension/rpc.ts +5 -1
- package/dist/extensions/pi-subagents/src/extension/schemas.ts +31 -30
- package/dist/extensions/pi-subagents/src/extension/tool-description.ts +10 -9
- package/dist/extensions/pi-subagents/src/inspectors/herdr/actions.ts +13 -8
- package/dist/extensions/pi-subagents/src/inspectors/herdr/inspector-runner.ts +16 -3
- package/dist/extensions/pi-subagents/src/inspectors/herdr/project-panes.ts +2 -6
- package/dist/extensions/pi-subagents/src/inspectors/herdr/shell-command.ts +16 -0
- package/dist/extensions/pi-subagents/src/intercom/intercom-bridge.ts +5 -4
- package/dist/extensions/pi-subagents/src/intercom/native-supervisor-channel.ts +19 -42
- package/dist/extensions/pi-subagents/src/missions/goal-driver.ts +3 -1
- package/dist/extensions/pi-subagents/src/missions/store.ts +8 -3
- package/dist/extensions/pi-subagents/src/runs/background/active-async-capacity.ts +82 -25
- package/dist/extensions/pi-subagents/src/runs/background/active-run-index.ts +71 -1
- package/dist/extensions/pi-subagents/src/runs/background/async-execution.ts +73 -43
- package/dist/extensions/pi-subagents/src/runs/background/async-job-tracker.ts +5 -0
- package/dist/extensions/pi-subagents/src/runs/background/async-resume.ts +14 -6
- package/dist/extensions/pi-subagents/src/runs/background/async-status-snapshot.ts +277 -0
- package/dist/extensions/pi-subagents/src/runs/background/async-status.ts +8 -3
- package/dist/extensions/pi-subagents/src/runs/background/chain-root-attachment.ts +2 -2
- package/dist/extensions/pi-subagents/src/runs/background/completion-replay.ts +11 -1
- package/dist/extensions/pi-subagents/src/runs/background/fleet-view.ts +21 -6
- package/dist/extensions/pi-subagents/src/runs/background/result-files.ts +437 -0
- package/dist/extensions/pi-subagents/src/runs/background/result-watcher.ts +205 -41
- package/dist/extensions/pi-subagents/src/runs/background/resume-guidance.ts +27 -7
- package/dist/extensions/pi-subagents/src/runs/background/retained-children.ts +76 -19
- package/dist/extensions/pi-subagents/src/runs/background/run-id-resolver.ts +30 -24
- package/dist/extensions/pi-subagents/src/runs/background/run-status.ts +101 -4
- package/dist/extensions/pi-subagents/src/runs/background/scheduled-runs.ts +54 -28
- package/dist/extensions/pi-subagents/src/runs/background/stale-run-reconciler.ts +27 -13
- package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +298 -33
- package/dist/extensions/pi-subagents/src/runs/background/subagent-wait.ts +2 -0
- package/dist/extensions/pi-subagents/src/runs/background/wait-completions.ts +5 -2
- package/dist/extensions/pi-subagents/src/runs/foreground/async-dismiss-action.ts +2 -1
- package/dist/extensions/pi-subagents/src/runs/foreground/chain-execution.ts +16 -0
- package/dist/extensions/pi-subagents/src/runs/foreground/execution.ts +219 -15
- package/dist/extensions/pi-subagents/src/runs/foreground/prompt-audit.ts +4 -3
- package/dist/extensions/pi-subagents/src/runs/foreground/subagent-executor.ts +333 -45
- package/dist/extensions/pi-subagents/src/runs/shared/completion-guard.ts +107 -1
- package/dist/extensions/pi-subagents/src/runs/shared/external-cli-runner.ts +4 -0
- package/dist/extensions/pi-subagents/src/runs/shared/llm-intent-arbiter.ts +39 -23
- package/dist/extensions/pi-subagents/src/runs/shared/model-fallback.ts +16 -2
- package/dist/extensions/pi-subagents/src/runs/shared/nested-events.ts +66 -62
- package/dist/extensions/pi-subagents/src/runs/shared/orca-progress-tabs.ts +376 -0
- package/dist/extensions/pi-subagents/src/runs/shared/parallel-utils.ts +2 -0
- package/dist/extensions/pi-subagents/src/runs/shared/subagent-control.ts +15 -0
- package/dist/extensions/pi-subagents/src/runs/shared/subagent-prompt-runtime.ts +1 -9
- package/dist/extensions/pi-subagents/src/runs/shared/subagent-startup-retry.ts +12 -0
- package/dist/extensions/pi-subagents/src/runs/shared/tool-timeout.ts +95 -0
- package/dist/extensions/pi-subagents/src/shared/agent-stream-options.ts +5 -0
- package/dist/extensions/pi-subagents/src/shared/artifacts.ts +0 -4
- package/dist/extensions/pi-subagents/src/shared/display-text.ts +50 -0
- package/dist/extensions/pi-subagents/src/shared/node-executable.ts +21 -0
- package/dist/extensions/pi-subagents/src/shared/types.ts +50 -4
- package/dist/extensions/pi-subagents/src/slash/slash-commands.ts +34 -25
- package/dist/extensions/pi-subagents/src/slash/slash-live-state.ts +4 -0
- package/dist/extensions/pi-subagents/src/tui/fleet-status.ts +160 -45
- package/dist/extensions/pi-subagents/src/tui/fleet-transcript.ts +1 -48
- package/dist/extensions/pi-subagents/src/tui/fleet.ts +128 -16
- package/dist/extensions/pi-subagents/src/tui/render.ts +122 -44
- package/dist/extensions/pi-subagents/src/watchdog/permission-arbiter.ts +2 -1
- package/dist/extensions/pi-subagents/src/watchdog/review.ts +4 -3
- package/dist/extensions/pi-subagents/src/workflows/chat-progress.ts +10 -2
- package/dist/extensions/pi-subagents/src/workflows/scripted-workflow.ts +272 -76
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/extensions/types.d.ts +2 -0
- package/dist/extensions/pi-subagents/test/integration/async-execution.test.ts +2 -2
- package/dist/extensions/pi-subagents/test/integration/async-job-tracker.test.ts +68 -9
- package/dist/extensions/pi-subagents/test/integration/async-status.test.ts +27 -1
- package/dist/extensions/pi-subagents/test/integration/chain-execution.test.ts +1 -1
- package/dist/extensions/pi-subagents/test/integration/error-handling.test.ts +49 -0
- package/dist/extensions/pi-subagents/test/integration/external-cli-runner.test.ts +1 -0
- package/dist/extensions/pi-subagents/test/integration/intercom-result-delivery.test.ts +26 -1
- package/dist/extensions/pi-subagents/test/integration/orca-progress-tabs.test.ts +144 -0
- package/dist/extensions/pi-subagents/test/integration/render-widget.test.ts +26 -0
- package/dist/extensions/pi-subagents/test/integration/result-watcher.test.ts +369 -98
- package/dist/extensions/pi-subagents/test/integration/single-execution.test.ts +111 -78
- package/dist/extensions/pi-subagents/test/integration/slash-commands.test.ts +49 -1
- package/dist/extensions/pi-subagents/test/integration/slash-live-state.test.ts +12 -0
- package/dist/extensions/pi-subagents/test/support/node-command.ts +15 -0
- package/dist/extensions/pi-subagents/test/unit/active-async-capacity.test.ts +50 -0
- package/dist/extensions/pi-subagents/test/unit/agent-frontmatter.test.ts +28 -0
- package/dist/extensions/pi-subagents/test/unit/agent-overrides.test.ts +61 -0
- package/dist/extensions/pi-subagents/test/unit/agent-stream-options.test.ts +14 -0
- package/dist/extensions/pi-subagents/test/unit/async-interrupt-action.test.ts +76 -0
- package/dist/extensions/pi-subagents/test/unit/async-resume.test.ts +2 -0
- package/dist/extensions/pi-subagents/test/unit/async-status-snapshot.test.ts +162 -0
- package/dist/extensions/pi-subagents/test/unit/completion-guard.test.ts +209 -10
- package/dist/extensions/pi-subagents/test/unit/completion-replay.test.ts +41 -1
- package/dist/extensions/pi-subagents/test/unit/external-runs.test.ts +135 -42
- package/dist/extensions/pi-subagents/test/unit/fleet-status.test.ts +252 -1
- package/dist/extensions/pi-subagents/test/unit/fleet.test.ts +192 -6
- package/dist/extensions/pi-subagents/test/unit/herdr-inspector-bootstrap.test.ts +32 -0
- package/dist/extensions/pi-subagents/test/unit/herdr-shell-command.test.ts +59 -0
- package/dist/extensions/pi-subagents/test/unit/index-child-registration.test.ts +69 -0
- package/dist/extensions/pi-subagents/test/unit/intercom-bridge.test.ts +23 -4
- package/dist/extensions/pi-subagents/test/unit/llm-intent-arbiter.test.ts +56 -1
- package/dist/extensions/pi-subagents/test/unit/mission-goal-driver.test.ts +67 -3
- package/dist/extensions/pi-subagents/test/unit/mission-lifecycle.test.ts +1 -1
- package/dist/extensions/pi-subagents/test/unit/mission-store.test.ts +14 -0
- package/dist/extensions/pi-subagents/test/unit/model-fallback.test.ts +18 -0
- package/dist/extensions/pi-subagents/test/unit/native-supervisor-channel.test.ts +2 -2
- package/dist/extensions/pi-subagents/test/unit/nested-events.test.ts +25 -2
- package/dist/extensions/pi-subagents/test/unit/node-executable.test.ts +26 -0
- package/dist/extensions/pi-subagents/test/unit/notify.test.ts +8 -0
- package/dist/extensions/pi-subagents/test/unit/orca-progress-tabs.test.ts +348 -0
- package/dist/extensions/pi-subagents/test/unit/package-manifest.test.ts +5 -2
- package/dist/extensions/pi-subagents/test/unit/pi-args.test.ts +1 -1
- package/dist/extensions/pi-subagents/test/unit/pi-coding-agent-dir.test.ts +34 -0
- package/dist/extensions/pi-subagents/test/unit/public-execution.test.ts +6 -1
- package/dist/extensions/pi-subagents/test/unit/render-helpers.test.ts +92 -0
- package/dist/extensions/pi-subagents/test/unit/result-files.test.ts +175 -0
- package/dist/extensions/pi-subagents/test/unit/retained-children.test.ts +127 -17
- package/dist/extensions/pi-subagents/test/unit/run-id-resolver.test.ts +44 -0
- package/dist/extensions/pi-subagents/test/unit/run-status.test.ts +247 -0
- package/dist/extensions/pi-subagents/test/unit/scheduled-runs.test.ts +107 -2
- package/dist/extensions/pi-subagents/test/unit/schemas.test.ts +5 -8
- package/dist/extensions/pi-subagents/test/unit/scripted-workflow.test.ts +456 -4
- package/dist/extensions/pi-subagents/test/unit/stale-run-reconciler.test.ts +128 -0
- package/dist/extensions/pi-subagents/test/unit/subagent-control.test.ts +28 -0
- package/dist/extensions/pi-subagents/test/unit/subagent-prompt-runtime.test.ts +16 -7
- package/dist/extensions/pi-subagents/test/unit/subagent-startup-retry.test.ts +15 -0
- package/dist/extensions/pi-subagents/test/unit/subagent-wait.test.ts +79 -15
- package/dist/extensions/pi-subagents/test/unit/tool-description.test.ts +12 -12
- package/dist/extensions/pi-subagents/test/unit/tool-timeout.test.ts +109 -0
- package/dist/extensions/pi-subagents/test/unit/wait-subscriptions.test.ts +34 -0
- package/dist/extensions/pi-subagents/test/unit/workflow-chat-progress.test.ts +94 -1
- package/dist/extensions/pi-subagents/test/unit/workflow-launch-params.test.ts +42 -2
- package/dist/extensions/question/index.ts +21 -6
- package/dist/extensions/question/question-list.ts +7 -1
- package/dist/extensions/question/row-layout.ts +3 -0
- package/dist/extensions/question/tests/exp.test.ts +70 -0
- package/dist/extensions/question/tests/exp2.test.ts +59 -0
- package/dist/extensions/question/tests/exp3.test.ts +47 -0
- package/dist/extensions/question/tests/helpers.test.ts +7 -0
- package/dist/extensions/question/tests/question-list.test.ts +140 -0
- package/dist/extensions/question/tests/row-layout.test.ts +13 -0
- package/dist/extensions/question/tests/selection-mode.test.ts +54 -0
- package/dist/extensions/question/tests/zz-ig3.test.ts +20 -0
- package/dist/extensions/question/tests/zz-probe.test.ts +29 -0
- package/dist/skills/pi-subagents/SKILL.md +1 -1
- package/dist/skills/pi-subagents/references/constraints-and-recipes.md +5 -4
- package/dist/skills/pi-subagents/references/execution-controls.md +15 -14
- package/dist/skills/pi-subagents/references/management-authoring-rpc.md +5 -5
- package/dist/skills/pi-subagents/references/prompting-and-roles.md +46 -26
- package/docs/settings.md +6 -0
- package/package.json +3 -3
|
@@ -3,6 +3,8 @@ import * as path from "node:path";
|
|
|
3
3
|
import { DIRS, type SubagentState } from "../../shared/types.ts";
|
|
4
4
|
import { readStatus } from "../../shared/utils.ts";
|
|
5
5
|
import { findAsyncRunPrefixMatches, type AsyncRunLocation } from "./async-resume.ts";
|
|
6
|
+
import { resultFilePath, resultFilesForToolCall } from "./result-files.ts";
|
|
7
|
+
import { readActiveRunToolCallIndex } from "./active-run-index.ts";
|
|
6
8
|
import { assertSafeNestedId, findNestedRunMatchesById, type NestedRoute, type NestedRunMatch, type NestedRunResolutionScope } from "../shared/nested-events.ts";
|
|
7
9
|
|
|
8
10
|
export type ResolvedSubagentRunId =
|
|
@@ -19,7 +21,7 @@ export interface ResolveSubagentRunIdDeps {
|
|
|
19
21
|
|
|
20
22
|
function exactAsyncLocation(id: string, asyncDirRoot: string, resultsDir: string): AsyncRunLocation | undefined {
|
|
21
23
|
const asyncDir = path.join(asyncDirRoot, id);
|
|
22
|
-
const resultPath =
|
|
24
|
+
const resultPath = resultFilePath(resultsDir, id);
|
|
23
25
|
if (!fs.existsSync(asyncDir) && !fs.existsSync(resultPath)) return undefined;
|
|
24
26
|
return {
|
|
25
27
|
asyncDir: fs.existsSync(asyncDir) ? asyncDir : null,
|
|
@@ -52,39 +54,28 @@ function readWorkflowResultIdentity(resultPath: string): WorkflowResultIdentity
|
|
|
52
54
|
};
|
|
53
55
|
}
|
|
54
56
|
|
|
55
|
-
function directoryEntries(root: string): string[] {
|
|
56
|
-
try {
|
|
57
|
-
return fs.readdirSync(root);
|
|
58
|
-
} catch (error) {
|
|
59
|
-
if ((error as NodeJS.ErrnoException).code === "ENOENT") return [];
|
|
60
|
-
throw error;
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
57
|
function resultPathFor(resultsDir: string, runId: string): string | null {
|
|
65
|
-
const resultPath =
|
|
58
|
+
const resultPath = resultFilePath(resultsDir, runId);
|
|
66
59
|
return fs.existsSync(resultPath) ? resultPath : null;
|
|
67
60
|
}
|
|
68
61
|
|
|
69
|
-
function toolCallIdMatches(value: string | undefined, query: string
|
|
70
|
-
|
|
71
|
-
return options.prefix === true ? value.startsWith(query) : value === query;
|
|
62
|
+
function toolCallIdMatches(value: string | undefined, query: string): boolean {
|
|
63
|
+
return value === query;
|
|
72
64
|
}
|
|
73
65
|
|
|
74
|
-
function
|
|
66
|
+
function indexedToolCallIdAsyncLocations(toolCallId: string, asyncDirRoot: string, resultsDir: string): AsyncRunMatch[] {
|
|
75
67
|
const byId = new Map<string, AsyncRunLocation>();
|
|
76
|
-
for (const entry of
|
|
68
|
+
for (const entry of readActiveRunToolCallIndex(asyncDirRoot, toolCallId)) {
|
|
77
69
|
const asyncDir = path.join(asyncDirRoot, entry);
|
|
78
70
|
const status = readStatus(asyncDir);
|
|
79
|
-
if (!status || !toolCallIdMatches(status.toolCallId, toolCallId
|
|
71
|
+
if (!status || !toolCallIdMatches(status.toolCallId, toolCallId)) continue;
|
|
80
72
|
const runId = status.runId || entry;
|
|
81
73
|
byId.set(runId, { asyncDir, resultPath: resultPathFor(resultsDir, runId), resolvedId: runId });
|
|
82
74
|
}
|
|
83
|
-
for (const entry of
|
|
84
|
-
if (!entry.endsWith(".json")) continue;
|
|
75
|
+
for (const entry of resultFilesForToolCall(resultsDir, toolCallId)) {
|
|
85
76
|
const resultPath = path.join(resultsDir, entry);
|
|
86
77
|
const identity = readWorkflowResultIdentity(resultPath);
|
|
87
|
-
if (!identity || !toolCallIdMatches(identity.toolCallId, toolCallId
|
|
78
|
+
if (!identity || !toolCallIdMatches(identity.toolCallId, toolCallId)) continue;
|
|
88
79
|
const runId = identity.runId ?? identity.id ?? entry.slice(0, -".json".length);
|
|
89
80
|
const asyncDir = path.join(asyncDirRoot, runId);
|
|
90
81
|
byId.set(runId, { asyncDir: fs.existsSync(asyncDir) ? asyncDir : null, resultPath, resolvedId: runId });
|
|
@@ -112,6 +103,22 @@ function hasExactForegroundId(state: SubagentState | undefined, id: string): boo
|
|
|
112
103
|
return acceptedSessions.has(remembered.sessionId);
|
|
113
104
|
}
|
|
114
105
|
|
|
106
|
+
function exactLiveAsyncToolCallMatch(state: SubagentState | undefined, toolCallId: string, asyncDirRoot: string, resultsDir: string): AsyncRunMatch | undefined {
|
|
107
|
+
if (!state?.asyncJobs) return undefined;
|
|
108
|
+
const matches = [...state.asyncJobs.values()].filter((job) => job.toolCallId === toolCallId);
|
|
109
|
+
if (matches.length > 1) throw new Error(`Subagent tool-call id '${toolCallId}' is ambiguous across async runs. Use the returned asyncId instead.`);
|
|
110
|
+
const match = matches[0];
|
|
111
|
+
if (!match) return undefined;
|
|
112
|
+
return {
|
|
113
|
+
id: match.asyncId,
|
|
114
|
+
location: {
|
|
115
|
+
asyncDir: fs.existsSync(match.asyncDir) ? match.asyncDir : path.join(asyncDirRoot, match.asyncId),
|
|
116
|
+
resultPath: resultPathFor(resultsDir, match.asyncId),
|
|
117
|
+
resolvedId: match.asyncId,
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
115
122
|
function nestedScopeFromState(state: SubagentState | undefined): NestedRunResolutionScope | undefined {
|
|
116
123
|
if (!state) return undefined;
|
|
117
124
|
const routes: NestedRoute[] = [];
|
|
@@ -141,7 +148,9 @@ export function resolveSubagentRunId(id: string, deps: ResolveSubagentRunIdDeps
|
|
|
141
148
|
if (hasExactForegroundId(deps.state, id)) return { kind: "foreground", id };
|
|
142
149
|
const exactAsync = exactAsyncLocation(id, asyncDirRoot, resultsDir);
|
|
143
150
|
if (exactAsync) return { kind: "async", id, location: exactAsync };
|
|
144
|
-
const
|
|
151
|
+
const exactLiveToolCallMatch = exactLiveAsyncToolCallMatch(deps.state, id, asyncDirRoot, resultsDir);
|
|
152
|
+
if (exactLiveToolCallMatch) return { kind: "async", id: exactLiveToolCallMatch.id, location: exactLiveToolCallMatch.location };
|
|
153
|
+
const exactToolCallIdMatches = indexedToolCallIdAsyncLocations(id, asyncDirRoot, resultsDir);
|
|
145
154
|
if (exactToolCallIdMatches.length > 1) throw new Error(`Subagent tool-call id '${id}' is ambiguous across async runs. Use the returned asyncId instead.`);
|
|
146
155
|
if (exactToolCallIdMatches[0]) return { kind: "async", id: exactToolCallIdMatches[0].id, location: exactToolCallIdMatches[0].location };
|
|
147
156
|
const exactNested = findNestedRunMatchesById(id, nestedScope ? { scope: nestedScope } : {});
|
|
@@ -155,9 +164,6 @@ export function resolveSubagentRunId(id: string, deps: ResolveSubagentRunIdDeps
|
|
|
155
164
|
for (const match of asyncPrefixMatches(id, asyncDirRoot, resultsDir)) {
|
|
156
165
|
matches.push({ kind: "async", id: match.id, location: match.location });
|
|
157
166
|
}
|
|
158
|
-
for (const match of toolCallIdAsyncLocations(id, asyncDirRoot, resultsDir, { prefix: true })) {
|
|
159
|
-
matches.push({ kind: "async", id: match.id, location: match.location });
|
|
160
|
-
}
|
|
161
167
|
for (const match of findNestedRunMatchesById(id, nestedScope ? { prefix: true, scope: nestedScope } : { prefix: true })) {
|
|
162
168
|
matches.push({ kind: "nested", id: match.run.id, match });
|
|
163
169
|
}
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import type { AgentToolResult } from "@earendil-works/pi-agent-core";
|
|
4
|
+
import { safeTerminalText } from "../../shared/display-text.ts";
|
|
4
5
|
import { formatAsyncRunList, formatAsyncRunOutputPath, formatAsyncRunProgressLabel, listAsyncRuns } from "./async-status.ts";
|
|
5
6
|
import { formatAsyncResultTranscript, formatAsyncRunTranscript, formatNestedRunTranscript, inspectSubagentFleet } from "./fleet-view.ts";
|
|
6
7
|
import { formatNestedRunStatusLines } from "../shared/nested-render.ts";
|
|
7
8
|
import { formatModelThinking } from "../../shared/formatters.ts";
|
|
8
9
|
import { formatActivityLabel } from "../../shared/status-format.ts";
|
|
9
10
|
import { DIRS, type AsyncStatus, type Details, type ForegroundResumeRun, type NestedRunSummary, type SteeringStatus, type SubagentState } from "../../shared/types.ts";
|
|
11
|
+
import { inspectActiveAsyncCapacityOwner, type ActiveAsyncCapacityInspection } from "./active-async-capacity.ts";
|
|
10
12
|
import { readStatus } from "../../shared/utils.ts";
|
|
11
13
|
import { resolveSubagentIntercomTarget } from "../../intercom/intercom-bridge.ts";
|
|
12
14
|
import { resolveSubagentResultStatus } from "../../intercom/result-intercom.ts";
|
|
@@ -31,6 +33,66 @@ interface RunStatusParams {
|
|
|
31
33
|
lines?: number;
|
|
32
34
|
}
|
|
33
35
|
|
|
36
|
+
function formatProcessTerminal(value: AsyncStatus["processTerminal"] | undefined): string {
|
|
37
|
+
if (!value) return "missing";
|
|
38
|
+
return `${value.state}${value.reason ? ` (${value.reason})` : ""}${value.runnerProcessInstanceId ? ` · runner ${value.runnerProcessInstanceId}` : ""}`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function debugProcessTerminal(asyncDir: string, status: AsyncStatus): { sidecar?: AsyncStatus["processTerminal"]; overlay?: AsyncStatus["processTerminal"] } {
|
|
42
|
+
const expected = { runId: status.runId, runnerProcessInstanceId: status.processTerminal?.runnerProcessInstanceId };
|
|
43
|
+
return {
|
|
44
|
+
sidecar: readProcessTerminal(asyncDir, expected),
|
|
45
|
+
overlay: sanitizeProcessTerminal(status.processTerminal, expected, path.join(asyncDir, "status.json")),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function formatCapacityOwner(inspect: ActiveAsyncCapacityInspection): string[] {
|
|
50
|
+
if (!inspect.owner) return [`Active capacity: ${inspect.release.state} — ${inspect.release.reason}`];
|
|
51
|
+
const owner = inspect.owner;
|
|
52
|
+
return [
|
|
53
|
+
`Active capacity: ${inspect.release.state} — ${inspect.release.reason}`,
|
|
54
|
+
`Capacity owner: ${inspect.relation} slot ${owner.slot}, ${owner.kind}, generation ${owner.generation}`,
|
|
55
|
+
`Capacity session: ${owner.ownerSessionId}`,
|
|
56
|
+
owner.sourceRunId ? `Capacity source run: ${owner.sourceRunId}` : undefined,
|
|
57
|
+
`Capacity async dir: ${owner.asyncDir}`,
|
|
58
|
+
owner.runnerProcessInstanceId ? `Capacity runner: ${owner.runnerProcessInstanceId}` : undefined,
|
|
59
|
+
owner.runnerStartedAt !== undefined ? `Capacity runner started: ${new Date(owner.runnerStartedAt).toISOString()}` : undefined,
|
|
60
|
+
].filter((line): line is string => line !== undefined);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function formatWorkflowDebug(status: AsyncStatus): string[] {
|
|
64
|
+
if (status.mode !== "workflow" && !status.parentWorkflowRunId && !status.workflowKey) return [];
|
|
65
|
+
const lines = [
|
|
66
|
+
status.parentWorkflowRunId ? `Workflow parent: ${status.parentWorkflowRunId}${status.workflowKey ? ` (${status.workflowKey})` : ""}` : undefined,
|
|
67
|
+
status.mode === "workflow" ? `Workflow children: ${(status.steps ?? []).length}` : undefined,
|
|
68
|
+
].filter((line): line is string => line !== undefined);
|
|
69
|
+
for (const [index, step] of (status.steps ?? []).entries()) {
|
|
70
|
+
lines.push(` ${index + 1}. key ${step.workflowKey ?? "n/a"} · ${step.agent} · ${step.status} · async ${step.async === undefined ? "unknown" : step.async ? "yes" : "no"}${step.runId ? ` · run ${step.runId}` : ""}`);
|
|
71
|
+
}
|
|
72
|
+
return lines;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function formatRunLifecycleDebug(input: { status: AsyncStatus; asyncDir: string; sidecarProcessTerminal: AsyncStatus["processTerminal"] | undefined; overlayProcessTerminal: AsyncStatus["processTerminal"] | undefined; capacity: ActiveAsyncCapacityInspection }): string {
|
|
76
|
+
const { status, asyncDir, sidecarProcessTerminal, overlayProcessTerminal, capacity } = input;
|
|
77
|
+
const lines = [
|
|
78
|
+
"Run lifecycle debug",
|
|
79
|
+
`Run: ${status.runId}`,
|
|
80
|
+
`Dir: ${asyncDir}`,
|
|
81
|
+
`Status file: ${path.join(asyncDir, "status.json")}`,
|
|
82
|
+
`Process terminal file: ${path.join(asyncDir, "process-terminal.json")}`,
|
|
83
|
+
`Session: ${status.sessionId ?? "unknown"}`,
|
|
84
|
+
`State: ${status.state}`,
|
|
85
|
+
`Mode: ${status.mode}`,
|
|
86
|
+
status.parentWorkflowRunId ? `Workflow parent: ${status.parentWorkflowRunId}` : undefined,
|
|
87
|
+
status.workflowKey ? `Workflow key: ${status.workflowKey}` : undefined,
|
|
88
|
+
`Status process terminal: ${formatProcessTerminal(overlayProcessTerminal)}`,
|
|
89
|
+
`Sidecar process terminal: ${formatProcessTerminal(sidecarProcessTerminal)}`,
|
|
90
|
+
...formatCapacityOwner(capacity),
|
|
91
|
+
...formatWorkflowDebug(status),
|
|
92
|
+
].filter((line): line is string => line !== undefined);
|
|
93
|
+
return lines.join("\n");
|
|
94
|
+
}
|
|
95
|
+
|
|
34
96
|
interface RunStatusDeps {
|
|
35
97
|
asyncDirRoot?: string;
|
|
36
98
|
resultsDir?: string;
|
|
@@ -39,6 +101,7 @@ interface RunStatusDeps {
|
|
|
39
101
|
state?: SubagentState;
|
|
40
102
|
nested?: NestedRunResolutionScope;
|
|
41
103
|
sessionRoots?: string[];
|
|
104
|
+
activeCapacityRoot?: string;
|
|
42
105
|
}
|
|
43
106
|
|
|
44
107
|
function hasExistingSessionFile(value: unknown): value is string {
|
|
@@ -50,12 +113,21 @@ function formatCheckpointGuidance(runId: string | undefined, checkpoint: AsyncSt
|
|
|
50
113
|
return `Checkpoint: ${checkpoint.name}${checkpoint.message ? ` — ${checkpoint.message}` : ""}\nApprove: subagent({ action: "approve-checkpoint", id: "${runId}" })\nReject: subagent({ action: "reject-checkpoint", id: "${runId}" })`;
|
|
51
114
|
}
|
|
52
115
|
|
|
53
|
-
function formatResumeGuidance(runId: string | undefined, children: Array<{ agent?: unknown; sessionFile?: unknown }>, fallbackSessionFile?: unknown, options: { stopped?: boolean } = {}): string {
|
|
116
|
+
function formatResumeGuidance(runId: string | undefined, children: Array<{ agent?: unknown; sessionFile?: unknown; runId?: unknown; workflowKey?: unknown; status?: unknown; activityState?: unknown }>, fallbackSessionFile?: unknown, options: { stopped?: boolean } = {}): string {
|
|
54
117
|
if (options.stopped) return "Resume: unavailable; stopped runs are not resumable. Start a new run instead.";
|
|
55
118
|
const knownChildren = children
|
|
56
119
|
.map((child, index) => ({ child, index }))
|
|
57
120
|
.filter(({ child }) => typeof child.agent === "string");
|
|
58
121
|
if (!runId || knownChildren.length === 0) return "Resume: unavailable; no child session file was persisted.";
|
|
122
|
+
const workflowChildren = knownChildren.filter(({ child }) => typeof child.runId === "string" && child.runId.trim() && hasExistingSessionFile(child.sessionFile));
|
|
123
|
+
const supervisorDetachedWorkflowChildren = workflowChildren.filter(({ child }) => child.status === "paused" && child.activityState === "needs_attention");
|
|
124
|
+
const resumableWorkflowChildren = workflowChildren.filter(({ child }) => !(child.status === "paused" && child.activityState === "needs_attention"));
|
|
125
|
+
if (workflowChildren.length > 0) {
|
|
126
|
+
return [
|
|
127
|
+
...supervisorDetachedWorkflowChildren.map(({ child }) => `Recovery workflow child${typeof child.workflowKey === "string" && child.workflowKey.trim() ? ` '${child.workflowKey}'` : ""}: reply to the supervisor request first, then wait with subagent_wait({ id: "${child.runId}" }). Use subagent({ action: "status", id: "${child.runId}" }) to recover the result; do not resume or launch a replacement while it remains detached.`),
|
|
128
|
+
...resumableWorkflowChildren.map(({ child }) => `Revive workflow child${typeof child.workflowKey === "string" && child.workflowKey.trim() ? ` '${child.workflowKey}'` : ""}: subagent({ action: "resume", id: "${child.runId}", message: "..." })`),
|
|
129
|
+
].join("\n");
|
|
130
|
+
}
|
|
59
131
|
const singleSessionFile = knownChildren[0]?.child.sessionFile ?? fallbackSessionFile;
|
|
60
132
|
if (children.length === 1 && knownChildren.length === 1 && hasExistingSessionFile(singleSessionFile)) {
|
|
61
133
|
return `Revive: subagent({ action: "resume", id: "${runId}", message: "..." })`;
|
|
@@ -178,7 +250,7 @@ function formatRememberedForegroundTranscript(run: ForegroundResumeRun, options:
|
|
|
178
250
|
lines.push("Result transcript tail:");
|
|
179
251
|
if (outputLines.length === 0) lines.push(" (no recovered final output available yet)");
|
|
180
252
|
else for (const line of outputLines) lines.push(` ${line}`);
|
|
181
|
-
return lines.join("\n");
|
|
253
|
+
return lines.map((line) => safeTerminalText(line)).join("\n");
|
|
182
254
|
}
|
|
183
255
|
|
|
184
256
|
function formatNestedExactStatus(rootRunId: string, run: NestedRunSummary): string {
|
|
@@ -265,7 +337,9 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
|
|
|
265
337
|
let location;
|
|
266
338
|
try {
|
|
267
339
|
const requestedId = params.id ?? params.runId;
|
|
268
|
-
if (
|
|
340
|
+
if (params.action === "debug.run") {
|
|
341
|
+
location = resolveAsyncRunLocation(params, asyncDirRoot, resultsDir);
|
|
342
|
+
} else if (!params.dir && requestedId) {
|
|
269
343
|
const resolved = resolveSubagentRunId(requestedId, { asyncDirRoot, resultsDir, state: deps.state, nested: deps.nested });
|
|
270
344
|
if (resolved?.kind === "foreground") {
|
|
271
345
|
const run = deps.state?.foregroundRuns?.get(resolved.id);
|
|
@@ -333,6 +407,14 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
|
|
|
333
407
|
}
|
|
334
408
|
const status = reconciliation.status;
|
|
335
409
|
if (!status && diskStatus?.displayDismissedAt !== undefined) {
|
|
410
|
+
if (params.action === "debug.run") {
|
|
411
|
+
const { sidecar, overlay } = debugProcessTerminal(asyncDir, diskStatus);
|
|
412
|
+
const capacity = inspectActiveAsyncCapacityOwner({ runId: diskStatus.runId, sessionId: diskStatus.sessionId, asyncDir }, { rootDir: deps.activeCapacityRoot, liveWorkflowRunIds: new Set(deps.state?.workflowControllers?.keys() ?? []) });
|
|
413
|
+
return {
|
|
414
|
+
content: [{ type: "text", text: formatRunLifecycleDebug({ status: diskStatus, asyncDir, sidecarProcessTerminal: sidecar, overlayProcessTerminal: overlay, capacity }) }],
|
|
415
|
+
details: { mode: "single", results: [], ...((sidecar ?? overlay) ? { lifecycleStatus: { processTerminal: sidecar ?? overlay } } : {}) },
|
|
416
|
+
};
|
|
417
|
+
}
|
|
336
418
|
if (params.view === "transcript") {
|
|
337
419
|
if (currentSessionId && !ownsSession(diskStatus.sessionId)) {
|
|
338
420
|
return {
|
|
@@ -352,6 +434,14 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
|
|
|
352
434
|
const logPath = path.join(asyncDir, `subagent-log-${effectiveRunId}.md`);
|
|
353
435
|
const eventsPath = path.join(asyncDir, "events.jsonl");
|
|
354
436
|
if (status) {
|
|
437
|
+
if (params.action === "debug.run") {
|
|
438
|
+
const { sidecar, overlay } = debugProcessTerminal(asyncDir, status);
|
|
439
|
+
const capacity = inspectActiveAsyncCapacityOwner({ runId: status.runId, sessionId: status.sessionId, asyncDir }, { rootDir: deps.activeCapacityRoot, liveWorkflowRunIds: new Set(deps.state?.workflowControllers?.keys() ?? []) });
|
|
440
|
+
return {
|
|
441
|
+
content: [{ type: "text", text: formatRunLifecycleDebug({ status, asyncDir, sidecarProcessTerminal: sidecar, overlayProcessTerminal: overlay, capacity }) }],
|
|
442
|
+
details: { mode: "single", results: [], ...((sidecar ?? overlay) ? { lifecycleStatus: { processTerminal: sidecar ?? overlay } } : {}) },
|
|
443
|
+
};
|
|
444
|
+
}
|
|
355
445
|
if (params.view === "transcript") {
|
|
356
446
|
if (currentSessionId && !ownsSession(status.sessionId)) {
|
|
357
447
|
return {
|
|
@@ -477,9 +567,16 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
|
|
|
477
567
|
}
|
|
478
568
|
|
|
479
569
|
if (resultPath) {
|
|
570
|
+
if (params.action === "debug.run") {
|
|
571
|
+
return {
|
|
572
|
+
content: [{ type: "text", text: "Run lifecycle debug needs an async run directory with status.json." }],
|
|
573
|
+
isError: true,
|
|
574
|
+
details: { mode: "single", results: [] },
|
|
575
|
+
};
|
|
576
|
+
}
|
|
480
577
|
try {
|
|
481
578
|
const raw = fs.readFileSync(resultPath, "utf-8");
|
|
482
|
-
const data = JSON.parse(raw) as { id?: string; runId?: string; toolCallId?: string; agent?: string; success?: boolean; summary?: string; output?: string; exitCode?: number; state?: string; stopped?: boolean; timedOut?: boolean; turnBudgetExceeded?: boolean; processSignal?: string | null; sessionFile?: string; parallelHandoff?: { path?: string }; results?: Array<{ agent?: string; output?: string; summary?: string; sessionFile?: string; state?: string; success?: boolean; exitCode?: number | null; stopped?: boolean; timedOut?: boolean; turnBudgetExceeded?: boolean; interrupted?: boolean; processSignal?: string | null }> };
|
|
579
|
+
const data = JSON.parse(raw) as { id?: string; runId?: string; toolCallId?: string; agent?: string; success?: boolean; summary?: string; output?: string; exitCode?: number; state?: string; stopped?: boolean; timedOut?: boolean; turnBudgetExceeded?: boolean; processSignal?: string | null; sessionFile?: string; parallelHandoff?: { path?: string }; results?: Array<{ agent?: string; runId?: string; workflowKey?: string; output?: string; summary?: string; sessionFile?: string; state?: string; success?: boolean; exitCode?: number | null; stopped?: boolean; timedOut?: boolean; turnBudgetExceeded?: boolean; interrupted?: boolean; processSignal?: string | null }> };
|
|
483
580
|
if (params.view === "transcript") {
|
|
484
581
|
try {
|
|
485
582
|
return { content: [{ type: "text", text: formatAsyncResultTranscript(data, resultPath, { index: params.index, lines: params.lines }) }], details: { mode: "single", results: [] } };
|
|
@@ -239,8 +239,15 @@ class ScheduleStore {
|
|
|
239
239
|
}
|
|
240
240
|
|
|
241
241
|
get(id: string): ScheduleRecord {
|
|
242
|
+
const record = this.find(id);
|
|
243
|
+
if (!record) throw new Error(`Schedule '${id}' not found.`);
|
|
244
|
+
return record;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/** Like {@link get}, but returns undefined when the schedule no longer exists. */
|
|
248
|
+
find(id: string): ScheduleRecord | undefined {
|
|
242
249
|
const file = path.join(scheduleDir(this.root, id, false, this.projectCwd), "schedule.json");
|
|
243
|
-
if (!fs.existsSync(file))
|
|
250
|
+
if (!fs.existsSync(file)) return undefined;
|
|
244
251
|
return parseSchedule(readJson(file, "schedule record"), file);
|
|
245
252
|
}
|
|
246
253
|
|
|
@@ -361,6 +368,7 @@ export class ScheduledRunManager {
|
|
|
361
368
|
private readonly stores = new Map<string, ScheduleStore>();
|
|
362
369
|
private readonly contexts = new Map<string, ExtensionContext>();
|
|
363
370
|
private readonly timers = new Map<string, ReturnType<typeof setTimeout>>();
|
|
371
|
+
private readonly observedAsyncIds = new Set<string>();
|
|
364
372
|
private readonly now: () => number;
|
|
365
373
|
private readonly randomId: () => string;
|
|
366
374
|
private readonly timersApi: ScheduledRunTimers;
|
|
@@ -383,6 +391,7 @@ export class ScheduledRunManager {
|
|
|
383
391
|
this.store = undefined;
|
|
384
392
|
this.stores.clear();
|
|
385
393
|
this.contexts.clear();
|
|
394
|
+
this.observedAsyncIds.clear();
|
|
386
395
|
}
|
|
387
396
|
|
|
388
397
|
async handleToolCall(params: SubagentParamsLike, ctx: ExtensionContext): Promise<AgentToolResult<Details>> {
|
|
@@ -407,26 +416,7 @@ export class ScheduledRunManager {
|
|
|
407
416
|
}
|
|
408
417
|
|
|
409
418
|
observedCompletionRunIds(): Set<string> {
|
|
410
|
-
|
|
411
|
-
for (const store of this.stores.values()) {
|
|
412
|
-
let ids: string[];
|
|
413
|
-
try {
|
|
414
|
-
ids = store.ids();
|
|
415
|
-
} catch (error) {
|
|
416
|
-
console.error(`Failed to inspect schedule store '${store.root}' during async completion discovery:`, error);
|
|
417
|
-
continue;
|
|
418
|
-
}
|
|
419
|
-
for (const id of ids) {
|
|
420
|
-
try {
|
|
421
|
-
for (const run of store.history(id)) {
|
|
422
|
-
if (run.state === "running" && run.asyncId) runIds.add(run.asyncId);
|
|
423
|
-
}
|
|
424
|
-
} catch (error) {
|
|
425
|
-
console.error(`Failed to inspect schedule '${id}' in '${store.root}' during async completion discovery:`, error);
|
|
426
|
-
}
|
|
427
|
-
}
|
|
428
|
-
}
|
|
429
|
-
return runIds;
|
|
419
|
+
return new Set(this.observedAsyncIds);
|
|
430
420
|
}
|
|
431
421
|
|
|
432
422
|
handleAsyncCompletion(payload: unknown): void {
|
|
@@ -563,9 +553,10 @@ export class ScheduledRunManager {
|
|
|
563
553
|
for (const schedule of store.list()) this.restoreOne(store, schedule);
|
|
564
554
|
}
|
|
565
555
|
|
|
566
|
-
private restoreOne(store: ScheduleStore, schedule: ScheduleRecord): void {
|
|
556
|
+
private restoreOne(store: ScheduleStore, schedule: ScheduleRecord, notBefore?: number, rearm = true): void {
|
|
567
557
|
if (schedule.activeRunId) {
|
|
568
558
|
const run = store.history(schedule.id).find((item) => item.id === schedule.activeRunId);
|
|
559
|
+
if (run?.state === "running" && run.asyncId) this.observedAsyncIds.add(run.asyncId);
|
|
569
560
|
const startedAt = run?.startedAt ? Date.parse(run.startedAt) : Number.NaN;
|
|
570
561
|
if (run?.state === "running" && run.asyncDir) {
|
|
571
562
|
try {
|
|
@@ -588,26 +579,59 @@ export class ScheduledRunManager {
|
|
|
588
579
|
fs.rmSync(path.join(store.directory(schedule.id), "active.lock"), { force: true });
|
|
589
580
|
}
|
|
590
581
|
}
|
|
591
|
-
if (schedule.paused) return;
|
|
582
|
+
if (!rearm || schedule.paused) return;
|
|
592
583
|
const next = nextRunAt(schedule);
|
|
593
584
|
if (next === undefined) return;
|
|
594
|
-
if (!schedule.activeRunId && next < this.now() && schedule.catchUp === "none")
|
|
595
|
-
|
|
585
|
+
if (!schedule.activeRunId && next < this.now() && schedule.catchUp === "none") {
|
|
586
|
+
try {
|
|
587
|
+
this.recordMissed(store, schedule, next, "timer");
|
|
588
|
+
} catch (error) {
|
|
589
|
+
if (notBefore !== undefined) this.arm(schedule, store, notBefore);
|
|
590
|
+
throw error;
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
this.arm(schedule, store, notBefore);
|
|
596
594
|
}
|
|
597
595
|
|
|
598
|
-
private arm(schedule: ScheduleRecord, store: ScheduleStore): void {
|
|
596
|
+
private arm(schedule: ScheduleRecord, store: ScheduleStore, notBefore?: number): void {
|
|
599
597
|
this.clearTimer(store, schedule.id);
|
|
600
598
|
if (schedule.paused) return;
|
|
601
599
|
const next = nextRunAt(schedule);
|
|
602
600
|
if (next === undefined) return;
|
|
603
|
-
const timer = this.timersApi.setTimeout(() =>
|
|
601
|
+
const timer = this.timersApi.setTimeout(() => {
|
|
602
|
+
// A timer callback is outside every caller's try/catch, so an escaping
|
|
603
|
+
// rejection here reaches the process as an uncaught exception and exits
|
|
604
|
+
// Pi. Contain every failure to this one schedule.
|
|
605
|
+
void this.fire(store, schedule.id).catch((error) => {
|
|
606
|
+
console.warn(`[pi-subagents] Scheduled run '${schedule.id}' failed to fire: ${error instanceof Error ? error.message : String(error)}`);
|
|
607
|
+
this.restoreAfterFireError(store, schedule.id);
|
|
608
|
+
});
|
|
609
|
+
}, Math.min(Math.max(0, next - this.now(), (notBefore ?? 0) - this.now()), MAX_TIMER_DELAY_MS));
|
|
604
610
|
timer.unref?.();
|
|
605
611
|
this.timers.set(this.timerKey(store, schedule.id), timer);
|
|
606
612
|
}
|
|
607
613
|
|
|
614
|
+
private restoreAfterFireError(store: ScheduleStore, id: string): void {
|
|
615
|
+
try {
|
|
616
|
+
const schedule = store.find(id);
|
|
617
|
+
if (!schedule) return;
|
|
618
|
+
const now = this.now();
|
|
619
|
+
const planned = schedule.trigger.kind === "interval" ? duePlannedAt(schedule, now) : undefined;
|
|
620
|
+
const notBefore = planned !== undefined && planned <= now ? Date.parse(nextAfter(schedule.trigger, planned, now)!) : undefined;
|
|
621
|
+
this.restoreOne(store, schedule, notBefore, schedule.trigger.kind === "interval");
|
|
622
|
+
} catch (error) {
|
|
623
|
+
console.warn(`[pi-subagents] Scheduled run '${id}' could not be restored after fire failure: ${error instanceof Error ? error.message : String(error)}`);
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
|
|
608
627
|
private async fire(store: ScheduleStore, id: string): Promise<void> {
|
|
609
628
|
this.clearTimer(store, id);
|
|
610
|
-
|
|
629
|
+
// Schedules are project-scoped and shared, and `delete` only clears the
|
|
630
|
+
// timer inside the deleting process. Another session can therefore remove
|
|
631
|
+
// a schedule while this process still holds an armed timer for it; there
|
|
632
|
+
// is then nothing to run and nothing to re-arm.
|
|
633
|
+
const schedule = store.find(id);
|
|
634
|
+
if (!schedule) return;
|
|
611
635
|
const planned = duePlannedAt(schedule, this.now());
|
|
612
636
|
if (planned === undefined || schedule.paused) return;
|
|
613
637
|
if (planned > this.now()) return this.arm(schedule, store);
|
|
@@ -661,6 +685,7 @@ export class ScheduledRunManager {
|
|
|
661
685
|
if (result.isError || !asyncId) throw new Error(result.content.find((item) => item.type === "text")?.text ?? "Scheduled launch failed.");
|
|
662
686
|
run.asyncId = asyncId;
|
|
663
687
|
run.asyncDir = result.details?.asyncDir;
|
|
688
|
+
this.observedAsyncIds.add(asyncId);
|
|
664
689
|
store.writeRun(schedule, run, "schedule.run.attached_async");
|
|
665
690
|
this.arm(schedule, store);
|
|
666
691
|
return run;
|
|
@@ -695,6 +720,7 @@ export class ScheduledRunManager {
|
|
|
695
720
|
schedule.trigger.nextRunAt = nextAfter(schedule.trigger, planned, now);
|
|
696
721
|
store.writeRun(schedule, skipped, "schedule.skipped_overlap");
|
|
697
722
|
}
|
|
723
|
+
if (run.asyncId) this.observedAsyncIds.delete(run.asyncId);
|
|
698
724
|
run.state = success ? "completed" : "failed_run";
|
|
699
725
|
run.completedAt = timestamp(now);
|
|
700
726
|
if (!success && error) run.error = error;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import { writeAtomicJson } from "../../shared/atomic-json.ts";
|
|
4
|
+
import { resultFilePath, resultPayloadPathForSessionRun, writeAsyncResultFile } from "./result-files.ts";
|
|
4
5
|
import { releaseActiveRunIndex, updateActiveRunIndex } from "./active-run-index.ts";
|
|
5
6
|
import { readStatus } from "../../shared/utils.ts";
|
|
6
7
|
import { DIRS, type AsyncParallelGroupStatus, type AsyncStatus, type NestedRunSummary, type SubagentRunMode } from "../../shared/types.ts";
|
|
@@ -98,14 +99,23 @@ interface ResultChildOutcome {
|
|
|
98
99
|
}
|
|
99
100
|
|
|
100
101
|
interface ResultRepairData {
|
|
101
|
-
state: "complete" | "failed" | "paused" | "stopped";
|
|
102
|
+
state: "complete" | "failed" | "paused" | "stopped" | "rejected";
|
|
102
103
|
results?: ResultChildOutcome[];
|
|
103
104
|
}
|
|
104
105
|
|
|
106
|
+
function regularFileExists(filePath: string): boolean {
|
|
107
|
+
try {
|
|
108
|
+
return fs.statSync(filePath).isFile();
|
|
109
|
+
} catch (error) {
|
|
110
|
+
if (isNotFoundError(error)) return false;
|
|
111
|
+
throw error;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
105
115
|
function readResultRepairData(resultPath: string): ResultRepairData | undefined {
|
|
106
116
|
try {
|
|
107
117
|
const data = JSON.parse(fs.readFileSync(resultPath, "utf-8")) as { success?: boolean; state?: string; exitCode?: number; results?: unknown };
|
|
108
|
-
const state = data.success ? "complete" : data.state === "stopped" ? "stopped" : data.state === "paused" || data.exitCode === 0 ? "paused" : "failed";
|
|
118
|
+
const state = data.success ? "complete" : data.state === "stopped" ? "stopped" : data.state === "rejected" ? "rejected" : data.state === "paused" || data.exitCode === 0 ? "paused" : "failed";
|
|
109
119
|
const results = Array.isArray(data.results)
|
|
110
120
|
? data.results.map((entry, index) => {
|
|
111
121
|
if (!entry || typeof entry !== "object" || Array.isArray(entry)) return {};
|
|
@@ -124,7 +134,7 @@ function readResultRepairData(resultPath: string): ResultRepairData | undefined
|
|
|
124
134
|
}
|
|
125
135
|
}
|
|
126
136
|
|
|
127
|
-
function childState(overallState: ResultRepairData["state"], child: ResultChildOutcome | undefined): "
|
|
137
|
+
function childState(overallState: ResultRepairData["state"], child: ResultChildOutcome | undefined): ResultRepairData["state"] {
|
|
128
138
|
if (child?.success === true) return "complete";
|
|
129
139
|
if (child?.success === false) return "failed";
|
|
130
140
|
return overallState;
|
|
@@ -197,7 +207,7 @@ function buildStartedStatus(asyncDir: string, startedRun: StartedRunMetadata, no
|
|
|
197
207
|
};
|
|
198
208
|
}
|
|
199
209
|
|
|
200
|
-
function buildFailedRepair(status: AsyncStatus, asyncDir: string, now: number, reason?: string): { status: AsyncStatus; result:
|
|
210
|
+
function buildFailedRepair(status: AsyncStatus, asyncDir: string, now: number, reason?: string): { status: AsyncStatus; result: Record<string, unknown>; message: string } {
|
|
201
211
|
const runId = status.runId || path.basename(asyncDir);
|
|
202
212
|
const pid = typeof status.pid === "number" ? status.pid : "unknown";
|
|
203
213
|
const baseMessage = reason ?? `Async runner process ${pid} exited or disappeared before writing a result. Marked run failed by stale-run reconciliation.`;
|
|
@@ -259,7 +269,7 @@ function buildFailedRepair(status: AsyncStatus, asyncDir: string, now: number, r
|
|
|
259
269
|
|
|
260
270
|
function writeFailedRepair(asyncDir: string, status: AsyncStatus, resultPath: string, now: number, reason?: string): ReconcileAsyncRunResult {
|
|
261
271
|
const repair = buildFailedRepair(status, asyncDir, now, reason);
|
|
262
|
-
|
|
272
|
+
if (repair.result.sessionId) writeAsyncResultFile(resultPath, repair.result);
|
|
263
273
|
writeAtomicJson(path.join(asyncDir, "status.json"), repair.status);
|
|
264
274
|
if (repair.status.processTerminal?.state === "observed") releaseActiveRunIndex(asyncDir);
|
|
265
275
|
appendJsonlBestEffort(path.join(asyncDir, "events.jsonl"), {
|
|
@@ -267,14 +277,14 @@ function writeFailedRepair(asyncDir: string, status: AsyncStatus, resultPath: st
|
|
|
267
277
|
ts: now,
|
|
268
278
|
runId: repair.status.runId,
|
|
269
279
|
pid: status.pid,
|
|
270
|
-
resultPath,
|
|
280
|
+
...(repair.result.sessionId ? { resultPath } : {}),
|
|
271
281
|
message: repair.message,
|
|
272
282
|
});
|
|
273
|
-
return { status: repair.status, repaired: true, resultPath, message: repair.message };
|
|
283
|
+
return { status: repair.status, repaired: true, ...(repair.result.sessionId ? { resultPath } : {}), message: repair.message };
|
|
274
284
|
}
|
|
275
285
|
|
|
276
286
|
function terminal(state: AsyncStatus["state"]): boolean {
|
|
277
|
-
return state === "complete" || state === "failed" || state === "paused" || state === "stopped";
|
|
287
|
+
return state === "complete" || state === "failed" || state === "paused" || state === "stopped" || state === "rejected";
|
|
278
288
|
}
|
|
279
289
|
|
|
280
290
|
function* nestedRuns(children: NestedRunSummary[] | undefined): Generator<NestedRunSummary> {
|
|
@@ -344,17 +354,21 @@ export function reconcileAsyncRun(asyncDir: string, options: ReconcileAsyncRunOp
|
|
|
344
354
|
if (stepRecord.thinking !== undefined && typeof stepRecord.thinking !== "string") throw new Error(`Invalid async status file '${statusPath}': steps[${index}].thinking must be a string.`);
|
|
345
355
|
}
|
|
346
356
|
const runId = effectiveStatus.runId || path.basename(asyncDir);
|
|
347
|
-
const
|
|
348
|
-
|
|
357
|
+
const resultsDir = options.resultsDir ?? DIRS.results;
|
|
358
|
+
const resultPath = resultFilePath(resultsDir, runId);
|
|
359
|
+
const existingResultPath = effectiveStatus.sessionId
|
|
360
|
+
? resultPayloadPathForSessionRun(resultsDir, effectiveStatus.sessionId, runId) ?? (regularFileExists(resultPath) ? resultPath : undefined)
|
|
361
|
+
: regularFileExists(resultPath) ? resultPath : undefined;
|
|
362
|
+
if (existingResultPath) {
|
|
349
363
|
const terminalStatus = effectiveStatus.state === "running" || effectiveStatus.state === "queued"
|
|
350
|
-
? terminalStatusFromResult(effectiveStatus,
|
|
364
|
+
? terminalStatusFromResult(effectiveStatus, existingResultPath, now)
|
|
351
365
|
: undefined;
|
|
352
366
|
if (terminalStatus) {
|
|
353
367
|
writeAtomicJson(path.join(asyncDir, "status.json"), terminalStatus);
|
|
354
368
|
if (terminalStatus.processTerminal?.state === "observed") releaseActiveRunIndex(asyncDir);
|
|
355
|
-
return { status: terminalStatus, repaired: true, resultPath, message: "Existing async result file was used to repair stale running status." };
|
|
369
|
+
return { status: terminalStatus, repaired: true, resultPath: existingResultPath, message: "Existing async result file was used to repair stale running status." };
|
|
356
370
|
}
|
|
357
|
-
if (effectiveStatus.displayDismissedAt === undefined) return { status: effectiveStatus, repaired: false, resultPath };
|
|
371
|
+
if (effectiveStatus.displayDismissedAt === undefined) return { status: effectiveStatus, repaired: false, resultPath: existingResultPath };
|
|
358
372
|
}
|
|
359
373
|
if (effectiveStatus.displayDismissedAt !== undefined) {
|
|
360
374
|
return { status: null, repaired: false, resultPath };
|