pi-subagents 0.49.0 → 0.51.0
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 +97 -0
- package/agents/gpt-pro.md +17 -0
- package/agents/oracle.md +7 -5
- package/agents/researcher.md +1 -1
- package/agents/reviewer.md +2 -2
- package/agents/scout.md +1 -1
- package/agents/worker.md +1 -1
- package/async-retention-discovery-worker.mjs +180 -0
- package/docs/agents.md +37 -2
- package/docs/configuration.md +76 -14
- package/docs/extension-api.md +78 -1
- package/docs/missions.md +1 -1
- package/docs/observability.md +20 -4
- package/docs/tool-reference.md +55 -39
- package/docs/workflows.md +171 -5
- package/package.json +4 -2
- package/skills/pi-subagents/SKILL.md +5 -4
- package/skills/pi-subagents/references/constraints-and-recipes.md +9 -6
- package/skills/pi-subagents/references/execution-controls.md +22 -18
- package/skills/pi-subagents/references/management-authoring-rpc.md +5 -5
- package/skills/pi-subagents/references/prompting-and-roles.md +33 -17
- package/src/agents/agent-management.ts +100 -345
- package/src/agents/agent-serializer.ts +2 -0
- package/src/agents/agents.ts +135 -25
- package/src/api/external-job-provider.ts +185 -0
- package/src/api/external-runs.ts +174 -84
- package/src/api/preflight.ts +42 -11
- package/src/api/shared-types.ts +2 -0
- package/src/extension/config.ts +36 -3
- package/src/extension/doctor.ts +3 -6
- package/src/extension/fanout-child.ts +2 -2
- package/src/extension/index.ts +210 -90
- package/src/extension/public-execution.ts +31 -2
- package/src/extension/rpc.ts +5 -1
- package/src/extension/schemas.ts +14 -36
- package/src/extension/tool-description.ts +37 -24
- package/src/inspectors/herdr/actions.ts +5 -9
- package/src/inspectors/herdr/inspector-runner.ts +2 -1
- package/src/inspectors/herdr/project-panes.ts +4 -8
- package/src/inspectors/herdr/shell-command.ts +16 -0
- package/src/intercom/intercom-bridge.ts +2 -3
- package/src/intercom/native-supervisor-channel.ts +49 -51
- package/src/missions/goal-driver.ts +3 -1
- package/src/missions/lifecycle.ts +6 -1
- package/src/missions/store.ts +4 -9
- package/src/profiles/profiles.ts +3 -1
- package/src/runs/background/active-run-index.ts +94 -1
- package/src/runs/background/async-execution.ts +98 -38
- package/src/runs/background/async-job-tracker.ts +21 -4
- package/src/runs/background/async-resume.ts +30 -17
- package/src/runs/background/async-retention.ts +888 -0
- package/src/runs/background/async-status-snapshot.ts +277 -0
- package/src/runs/background/async-status.ts +47 -56
- package/src/runs/background/chain-append.ts +3 -33
- package/src/runs/background/chain-root-attachment.ts +2 -2
- package/src/runs/background/completion-replay.ts +11 -1
- package/src/runs/background/control-channel.ts +14 -68
- package/src/runs/background/fleet-view.ts +3 -1
- package/src/runs/background/index-segment.ts +59 -0
- package/src/runs/background/notify.ts +3 -1
- package/src/runs/background/result-files.ts +505 -0
- package/src/runs/background/result-watcher.ts +250 -51
- package/src/runs/background/retained-children.ts +79 -20
- package/src/runs/background/run-id-query.ts +7 -0
- package/src/runs/background/run-id-resolver.ts +37 -29
- package/src/runs/background/run-status.ts +34 -20
- package/src/runs/background/scheduled-runs.ts +71 -27
- package/src/runs/background/stale-run-reconciler.ts +33 -16
- package/src/runs/background/steering.ts +11 -1
- package/src/runs/background/subagent-runner.ts +535 -161
- package/src/runs/background/subagent-wait.ts +9 -7
- package/src/runs/background/terminal-run-index.ts +129 -0
- package/src/runs/background/wait-completions.ts +22 -2
- package/src/runs/background/wait-subscriptions.ts +80 -1
- package/src/runs/foreground/async-dismiss-action.ts +2 -1
- package/src/runs/foreground/async-steering-action.ts +21 -14
- package/src/runs/foreground/execution.ts +221 -15
- package/src/runs/foreground/subagent-executor.ts +529 -1519
- package/src/runs/foreground/workflow-foreground-steering.ts +6 -5
- package/src/runs/shared/chain-outputs.ts +1 -3
- package/src/runs/shared/completion-guard.ts +96 -6
- package/src/runs/shared/external-cli-runner.ts +4 -0
- package/src/runs/shared/external-job-bridge.ts +450 -0
- package/src/runs/shared/external-job-runner.ts +286 -0
- package/src/runs/shared/mcp-direct-tool-allowlist.ts +14 -0
- package/src/runs/shared/model-fallback.ts +34 -3
- package/src/runs/shared/nested-events.ts +66 -62
- package/src/runs/shared/orca-progress-tabs.ts +437 -0
- package/src/runs/shared/parallel-handoff.ts +46 -4
- package/src/runs/shared/parallel-utils.ts +6 -15
- package/src/runs/shared/permissions.ts +5 -1
- package/src/runs/shared/pi-args.ts +8 -1
- package/src/runs/shared/subagent-control.ts +41 -4
- package/src/runs/shared/subagent-prompt-runtime.ts +13 -15
- package/src/runs/shared/subagent-startup-retry.ts +12 -0
- package/src/runs/shared/tool-timeout.ts +93 -0
- package/src/runs/shared/workflow-graph.ts +1 -23
- package/src/runs/shared/worktree.ts +12 -1
- package/src/shared/atomic-json.ts +22 -2
- package/src/shared/capacity-resilient-json.ts +102 -0
- package/src/shared/completion-owner.ts +14 -0
- package/src/shared/file-system-retry.ts +49 -1
- package/src/shared/fork-context.ts +42 -0
- package/src/shared/prompt-resources.ts +0 -40
- package/src/shared/settings.ts +3 -27
- package/src/shared/types.ts +88 -27
- package/src/shared/utils.ts +8 -0
- package/src/shared/watch-strategy.ts +10 -0
- package/src/slash/slash-commands.ts +45 -28
- package/src/slash/slash-live-state.ts +3 -0
- package/src/tui/fleet-status.ts +160 -45
- package/src/tui/fleet.ts +186 -28
- package/src/tui/render.ts +41 -10
- package/src/workflows/chat-progress.ts +7 -5
- package/src/workflows/scripted-workflow.ts +424 -125
- package/src/runs/foreground/chain-clarify.ts +0 -1354
- package/src/runs/foreground/chain-execution.ts +0 -1565
|
@@ -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 { resultCandidateFilesForToolCall, resultFilePath, resultPayloadPathForIndexedRun } 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,11 +21,12 @@ 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
|
|
23
|
-
|
|
24
|
+
const asyncDirExists = fs.existsSync(asyncDir);
|
|
25
|
+
const resultPath = resultPathFor(resultsDir, id);
|
|
26
|
+
if (!asyncDirExists && !resultPath) return undefined;
|
|
24
27
|
return {
|
|
25
|
-
asyncDir:
|
|
26
|
-
resultPath
|
|
28
|
+
asyncDir: asyncDirExists ? asyncDir : null,
|
|
29
|
+
resultPath,
|
|
27
30
|
resolvedId: id,
|
|
28
31
|
};
|
|
29
32
|
}
|
|
@@ -52,40 +55,30 @@ function readWorkflowResultIdentity(resultPath: string): WorkflowResultIdentity
|
|
|
52
55
|
};
|
|
53
56
|
}
|
|
54
57
|
|
|
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
58
|
function resultPathFor(resultsDir: string, runId: string): string | null {
|
|
65
|
-
const resultPath =
|
|
59
|
+
const resultPath = resultPayloadPathForIndexedRun(resultsDir, runId) ?? resultFilePath(resultsDir, runId);
|
|
66
60
|
return fs.existsSync(resultPath) ? resultPath : null;
|
|
67
61
|
}
|
|
68
62
|
|
|
69
|
-
function toolCallIdMatches(value: string | undefined, query: string
|
|
70
|
-
|
|
71
|
-
return options.prefix === true ? value.startsWith(query) : value === query;
|
|
63
|
+
function toolCallIdMatches(value: string | undefined, query: string): boolean {
|
|
64
|
+
return value === query;
|
|
72
65
|
}
|
|
73
66
|
|
|
74
|
-
function
|
|
67
|
+
function indexedToolCallIdAsyncLocations(toolCallId: string, asyncDirRoot: string, resultsDir: string): AsyncRunMatch[] {
|
|
75
68
|
const byId = new Map<string, AsyncRunLocation>();
|
|
76
|
-
for (const entry of
|
|
69
|
+
for (const entry of readActiveRunToolCallIndex(asyncDirRoot, toolCallId)) {
|
|
77
70
|
const asyncDir = path.join(asyncDirRoot, entry);
|
|
78
71
|
const status = readStatus(asyncDir);
|
|
79
|
-
if (!status || !toolCallIdMatches(status.toolCallId, toolCallId
|
|
72
|
+
if (!status || !toolCallIdMatches(status.toolCallId, toolCallId)) continue;
|
|
80
73
|
const runId = status.runId || entry;
|
|
81
74
|
byId.set(runId, { asyncDir, resultPath: resultPathFor(resultsDir, runId), resolvedId: runId });
|
|
82
75
|
}
|
|
83
|
-
for (const entry of
|
|
84
|
-
|
|
85
|
-
const resultPath = path.join(resultsDir, entry);
|
|
76
|
+
for (const entry of resultCandidateFilesForToolCall(resultsDir, toolCallId)) {
|
|
77
|
+
const runIdFromFile = entry.slice(0, -".json".length);
|
|
78
|
+
const resultPath = resultPayloadPathForIndexedRun(resultsDir, runIdFromFile) ?? path.join(resultsDir, entry);
|
|
86
79
|
const identity = readWorkflowResultIdentity(resultPath);
|
|
87
|
-
if (!identity || !toolCallIdMatches(identity.toolCallId, toolCallId
|
|
88
|
-
const runId = identity.runId ?? identity.id ??
|
|
80
|
+
if (!identity || !toolCallIdMatches(identity.toolCallId, toolCallId)) continue;
|
|
81
|
+
const runId = identity.runId ?? identity.id ?? runIdFromFile;
|
|
89
82
|
const asyncDir = path.join(asyncDirRoot, runId);
|
|
90
83
|
byId.set(runId, { asyncDir: fs.existsSync(asyncDir) ? asyncDir : null, resultPath, resolvedId: runId });
|
|
91
84
|
}
|
|
@@ -109,6 +102,22 @@ function hasExactForegroundId(state: SubagentState | undefined, id: string): boo
|
|
|
109
102
|
return Boolean(remembered && state.currentSessionId && remembered.sessionId === state.currentSessionId);
|
|
110
103
|
}
|
|
111
104
|
|
|
105
|
+
function exactLiveAsyncToolCallMatch(state: SubagentState | undefined, toolCallId: string, asyncDirRoot: string, resultsDir: string): AsyncRunMatch | undefined {
|
|
106
|
+
if (!state?.asyncJobs) return undefined;
|
|
107
|
+
const matches = [...state.asyncJobs.values()].filter((job) => job.toolCallId === toolCallId);
|
|
108
|
+
if (matches.length > 1) throw new Error(`Subagent tool-call id '${toolCallId}' is ambiguous across async runs. Use the returned asyncId instead.`);
|
|
109
|
+
const match = matches[0];
|
|
110
|
+
if (!match) return undefined;
|
|
111
|
+
return {
|
|
112
|
+
id: match.asyncId,
|
|
113
|
+
location: {
|
|
114
|
+
asyncDir: fs.existsSync(match.asyncDir) ? match.asyncDir : path.join(asyncDirRoot, match.asyncId),
|
|
115
|
+
resultPath: resultPathFor(resultsDir, match.asyncId),
|
|
116
|
+
resolvedId: match.asyncId,
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
112
121
|
function nestedScopeFromState(state: SubagentState | undefined): NestedRunResolutionScope | undefined {
|
|
113
122
|
if (!state) return undefined;
|
|
114
123
|
const routes: NestedRoute[] = [];
|
|
@@ -138,7 +147,9 @@ export function resolveSubagentRunId(id: string, deps: ResolveSubagentRunIdDeps
|
|
|
138
147
|
if (hasExactForegroundId(deps.state, id)) return { kind: "foreground", id };
|
|
139
148
|
const exactAsync = exactAsyncLocation(id, asyncDirRoot, resultsDir);
|
|
140
149
|
if (exactAsync) return { kind: "async", id, location: exactAsync };
|
|
141
|
-
const
|
|
150
|
+
const exactLiveToolCallMatch = exactLiveAsyncToolCallMatch(deps.state, id, asyncDirRoot, resultsDir);
|
|
151
|
+
if (exactLiveToolCallMatch) return { kind: "async", id: exactLiveToolCallMatch.id, location: exactLiveToolCallMatch.location };
|
|
152
|
+
const exactToolCallIdMatches = indexedToolCallIdAsyncLocations(id, asyncDirRoot, resultsDir);
|
|
142
153
|
if (exactToolCallIdMatches.length > 1) throw new Error(`Subagent tool-call id '${id}' is ambiguous across async runs. Use the returned asyncId instead.`);
|
|
143
154
|
if (exactToolCallIdMatches[0]) return { kind: "async", id: exactToolCallIdMatches[0].id, location: exactToolCallIdMatches[0].location };
|
|
144
155
|
const exactNested = findNestedRunMatchesById(id, nestedScope ? { scope: nestedScope } : {});
|
|
@@ -152,9 +163,6 @@ export function resolveSubagentRunId(id: string, deps: ResolveSubagentRunIdDeps
|
|
|
152
163
|
for (const match of asyncPrefixMatches(id, asyncDirRoot, resultsDir)) {
|
|
153
164
|
matches.push({ kind: "async", id: match.id, location: match.location });
|
|
154
165
|
}
|
|
155
|
-
for (const match of toolCallIdAsyncLocations(id, asyncDirRoot, resultsDir, { prefix: true })) {
|
|
156
|
-
matches.push({ kind: "async", id: match.id, location: match.location });
|
|
157
|
-
}
|
|
158
166
|
for (const match of findNestedRunMatchesById(id, nestedScope ? { prefix: true, scope: nestedScope } : { prefix: true })) {
|
|
159
167
|
matches.push({ kind: "nested", id: match.run.id, match });
|
|
160
168
|
}
|
|
@@ -108,20 +108,20 @@ function hasExistingSessionFile(value: unknown): value is string {
|
|
|
108
108
|
return typeof value === "string" && fs.existsSync(value);
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
-
function
|
|
112
|
-
if (!runId || !checkpoint || checkpoint.status !== "pending") return undefined;
|
|
113
|
-
return `Checkpoint: ${checkpoint.name}${checkpoint.message ? ` — ${checkpoint.message}` : ""}\nApprove: subagent({ action: "approve-checkpoint", id: "${runId}" })\nReject: subagent({ action: "reject-checkpoint", id: "${runId}" })`;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
function formatResumeGuidance(runId: string | undefined, children: Array<{ agent?: unknown; sessionFile?: unknown; runId?: unknown; workflowKey?: unknown }>, fallbackSessionFile?: unknown, options: { stopped?: boolean } = {}): string {
|
|
111
|
+
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 {
|
|
117
112
|
if (options.stopped) return "Resume: unavailable; stopped runs are not resumable. Start a new run instead.";
|
|
118
113
|
const knownChildren = children
|
|
119
114
|
.map((child, index) => ({ child, index }))
|
|
120
115
|
.filter(({ child }) => typeof child.agent === "string");
|
|
121
116
|
if (!runId || knownChildren.length === 0) return "Resume: unavailable; no child session file was persisted.";
|
|
122
117
|
const workflowChildren = knownChildren.filter(({ child }) => typeof child.runId === "string" && child.runId.trim() && hasExistingSessionFile(child.sessionFile));
|
|
118
|
+
const supervisorDetachedWorkflowChildren = workflowChildren.filter(({ child }) => child.status === "paused" && child.activityState === "needs_attention");
|
|
119
|
+
const resumableWorkflowChildren = workflowChildren.filter(({ child }) => !(child.status === "paused" && child.activityState === "needs_attention"));
|
|
123
120
|
if (workflowChildren.length > 0) {
|
|
124
|
-
return
|
|
121
|
+
return [
|
|
122
|
+
...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.`),
|
|
123
|
+
...resumableWorkflowChildren.map(({ child }) => `Revive workflow child${typeof child.workflowKey === "string" && child.workflowKey.trim() ? ` '${child.workflowKey}'` : ""}: subagent({ action: "resume", id: "${child.runId}", message: "..." })`),
|
|
124
|
+
].join("\n");
|
|
125
125
|
}
|
|
126
126
|
const singleSessionFile = knownChildren[0]?.child.sessionFile ?? fallbackSessionFile;
|
|
127
127
|
if (children.length === 1 && knownChildren.length === 1 && hasExistingSessionFile(singleSessionFile)) {
|
|
@@ -205,12 +205,6 @@ function formatRememberedForegroundStatus(run: ForegroundResumeRun): string {
|
|
|
205
205
|
else lines.push(`Transcript: subagent({ action: "status", id: "${run.runId}", index: 0, view: "transcript" })`);
|
|
206
206
|
const detached = run.children.some((child) => child.status === "detached");
|
|
207
207
|
const resumable = run.children.find((child) => hasExistingSessionFile(child.sessionFile));
|
|
208
|
-
if (run.checkpoint?.status === "pending") {
|
|
209
|
-
lines.push(`Checkpoint: ${run.checkpoint.name}${run.checkpoint.message ? ` — ${run.checkpoint.message}` : ""}`);
|
|
210
|
-
lines.push(`Approve: subagent({ action: "approve-checkpoint", id: "${run.runId}" })`);
|
|
211
|
-
lines.push(`Reject: subagent({ action: "reject-checkpoint", id: "${run.runId}" })`);
|
|
212
|
-
return lines.join("\n");
|
|
213
|
-
}
|
|
214
208
|
if (detached) {
|
|
215
209
|
lines.push(`Recovery: reply to the supervisor request first, then wait with subagent_wait({ id: "${run.runId}" }); do not resume or launch a replacement while any child remains detached.`);
|
|
216
210
|
} else if (resumable) {
|
|
@@ -513,6 +507,12 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
|
|
|
513
507
|
reconciliation.message ? `Diagnosis: ${reconciliation.message}` : undefined,
|
|
514
508
|
reconciliation.resultPath && fs.existsSync(reconciliation.resultPath) ? `Result: ${reconciliation.resultPath}` : undefined,
|
|
515
509
|
].filter((line): line is string => Boolean(line));
|
|
510
|
+
const liveWorkflowControls = status.mode === "workflow" && deps.state?.currentSessionId === status.sessionId && deps.state?.workflowControllers?.has(status.runId)
|
|
511
|
+
? [...(deps.state?.foregroundControls.values() ?? [])].filter((control) => control.parentWorkflowRunId === status.runId
|
|
512
|
+
&& control.sessionId === status.sessionId
|
|
513
|
+
&& Boolean(control.workflowSteeringDir && fs.existsSync(control.workflowSteeringDir))
|
|
514
|
+
&& (control.activeChildren?.size ?? 0) > 0)
|
|
515
|
+
: [];
|
|
516
516
|
for (const [index, step] of (status.steps ?? []).entries()) {
|
|
517
517
|
const stepActivityText = step.status === "running" ? formatActivityLabel(step.lastActivityAt, step.activityState) : undefined;
|
|
518
518
|
const modelThinking = formatModelThinking(step.model, step.thinking);
|
|
@@ -529,28 +529,42 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
|
|
|
529
529
|
lines.push(` Runner: external-cli (${step.runner.command}${step.runner.args.length ? ` ${step.runner.args.join(" ")}` : ""})`);
|
|
530
530
|
if (step.externalProcess?.pid !== undefined) lines.push(` Process: ${step.externalProcess.pid}`);
|
|
531
531
|
if (step.externalProcess) lines.push(` Stdout: ${step.externalProcess.stdoutPath}`, ` Stderr: ${step.externalProcess.stderrPath}`);
|
|
532
|
+
} else if (step.runner?.type === "external-job") {
|
|
533
|
+
lines.push(` Runner: external-job (${step.runner.provider})`);
|
|
534
|
+
if (step.externalJob?.providerJobId) lines.push(` Provider job: ${step.externalJob.providerJobId}`);
|
|
535
|
+
if (step.externalJob?.state) lines.push(` Provider state: ${step.externalJob.state}`);
|
|
536
|
+
if (step.externalJob?.conversationUrl) lines.push(` Conversation: ${step.externalJob.conversationUrl}`);
|
|
537
|
+
if (step.externalJob?.resultArtifactPath) lines.push(` Result artifact: ${step.externalJob.resultArtifactPath}`);
|
|
532
538
|
}
|
|
533
539
|
lines.push(...formatNestedRunStatusLines(step.children, { indent: " ", commandHints: true, maxLines: 20 }));
|
|
534
540
|
const stepOutputPath = path.join(asyncDir, `output-${index}.log`);
|
|
535
541
|
if (stepOutputPath !== outputPath && fs.existsSync(stepOutputPath)) lines.push(` Output: ${stepOutputPath}`);
|
|
536
|
-
if (step.status === "running" && step.runner?.type !== "external-cli") {
|
|
542
|
+
if (step.status === "running" && step.runner?.type !== "external-cli" && step.runner?.type !== "external-job" && status.mode !== "workflow") {
|
|
537
543
|
lines.push(` Intercom target: ${resolveSubagentIntercomTarget(status.runId, step.agent, index)} (if registered)`);
|
|
538
544
|
lines.push(` Steer: subagent({ action: "steer", id: "${status.runId}", index: ${index}, message: "..." })`);
|
|
539
|
-
} else if (step.status === "running" && step.runner?.type === "external-cli") {
|
|
540
|
-
lines.push(" Steer: unavailable;
|
|
545
|
+
} else if (step.status === "running" && (step.runner?.type === "external-cli" || step.runner?.type === "external-job")) {
|
|
546
|
+
lines.push(" Steer: unavailable; external runners do not accept live messages.");
|
|
541
547
|
}
|
|
542
548
|
}
|
|
543
549
|
const attached = new Set((status.steps ?? []).flatMap((step) => step.children?.map((child) => child.id) ?? []));
|
|
544
550
|
const unattached = nestedChildren.filter((child) => !attached.has(child.id));
|
|
545
551
|
lines.push(...formatNestedRunStatusLines(unattached, { indent: "", commandHints: true, maxLines: 20 }));
|
|
552
|
+
if (status.mode === "workflow" && status.state === "running") {
|
|
553
|
+
if (liveWorkflowControls.length === 0) lines.push("Steer: unavailable; no live foreground route is registered in the active session.");
|
|
554
|
+
else for (const control of liveWorkflowControls) {
|
|
555
|
+
for (const index of [...control.activeChildren!.keys()].sort((left, right) => left - right)) {
|
|
556
|
+
lines.push(`Steer live foreground child: subagent({ action: "steer", id: "${control.runId}", index: ${index}, message: "..." })`);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
}
|
|
546
560
|
if (nestedWarning) lines.push(`Warning: ${nestedWarning}`);
|
|
547
561
|
if (status.sessionFile) lines.push(`Session: ${status.sessionFile}`);
|
|
548
|
-
const allExternal = (status.steps?.length ?? 0) > 0 && status.steps!.every((step) => step.runner?.type === "external-cli");
|
|
549
|
-
if (status.state === "running" && !allExternal) lines.push(`Steer running child: subagent({ action: "steer", id: "${status.runId}", message: "..." })`);
|
|
562
|
+
const allExternal = (status.steps?.length ?? 0) > 0 && status.steps!.every((step) => step.runner?.type === "external-cli" || step.runner?.type === "external-job");
|
|
563
|
+
if (status.state === "running" && !allExternal && status.mode !== "workflow") lines.push(`Steer running child: subagent({ action: "steer", id: "${status.runId}", message: "..." })`);
|
|
550
564
|
if (status.state !== "running") {
|
|
551
565
|
lines.push(allExternal
|
|
552
|
-
? "Resume: unavailable;
|
|
553
|
-
:
|
|
566
|
+
? "Resume: unavailable; external runners do not persist Pi sessions."
|
|
567
|
+
: formatResumeGuidance(status.runId, status.steps ?? [], status.sessionFile, { stopped: status.state === "stopped" || status.stopped === true }));
|
|
554
568
|
}
|
|
555
569
|
if (fs.existsSync(logPath)) lines.push(`Log: ${logPath}`);
|
|
556
570
|
if (fs.existsSync(eventsPath)) lines.push(`Events: ${eventsPath}`);
|
|
@@ -152,7 +152,13 @@ function assertScheduleRoot(root: string, projectCwd: string | undefined, create
|
|
|
152
152
|
if (create) fs.mkdirSync(root, { recursive: true, mode: 0o700 });
|
|
153
153
|
return;
|
|
154
154
|
}
|
|
155
|
-
|
|
155
|
+
let projectPath: string;
|
|
156
|
+
try {
|
|
157
|
+
projectPath = fs.realpathSync(projectCwd);
|
|
158
|
+
} catch (error) {
|
|
159
|
+
if (!create && (error as NodeJS.ErrnoException).code === "ENOENT") return;
|
|
160
|
+
throw error;
|
|
161
|
+
}
|
|
156
162
|
let existing = root;
|
|
157
163
|
while (!fs.existsSync(existing)) {
|
|
158
164
|
const parent = path.dirname(existing);
|
|
@@ -235,12 +241,19 @@ class ScheduleStore {
|
|
|
235
241
|
}
|
|
236
242
|
|
|
237
243
|
list(): ScheduleRecord[] {
|
|
238
|
-
return this.ids().map((id) => this.
|
|
244
|
+
return this.ids().map((id) => this.find(id)).filter((record): record is ScheduleRecord => record !== undefined);
|
|
239
245
|
}
|
|
240
246
|
|
|
241
247
|
get(id: string): ScheduleRecord {
|
|
248
|
+
const record = this.find(id);
|
|
249
|
+
if (!record) throw new Error(`Schedule '${id}' not found.`);
|
|
250
|
+
return record;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/** Like {@link get}, but returns undefined when the schedule no longer exists. */
|
|
254
|
+
find(id: string): ScheduleRecord | undefined {
|
|
242
255
|
const file = path.join(scheduleDir(this.root, id, false, this.projectCwd), "schedule.json");
|
|
243
|
-
if (!fs.existsSync(file))
|
|
256
|
+
if (!fs.existsSync(file)) return undefined;
|
|
244
257
|
return parseSchedule(readJson(file, "schedule record"), file);
|
|
245
258
|
}
|
|
246
259
|
|
|
@@ -361,6 +374,7 @@ export class ScheduledRunManager {
|
|
|
361
374
|
private readonly stores = new Map<string, ScheduleStore>();
|
|
362
375
|
private readonly contexts = new Map<string, ExtensionContext>();
|
|
363
376
|
private readonly timers = new Map<string, ReturnType<typeof setTimeout>>();
|
|
377
|
+
private readonly observedAsyncIds = new Set<string>();
|
|
364
378
|
private readonly now: () => number;
|
|
365
379
|
private readonly randomId: () => string;
|
|
366
380
|
private readonly timersApi: ScheduledRunTimers;
|
|
@@ -383,6 +397,7 @@ export class ScheduledRunManager {
|
|
|
383
397
|
this.store = undefined;
|
|
384
398
|
this.stores.clear();
|
|
385
399
|
this.contexts.clear();
|
|
400
|
+
this.observedAsyncIds.clear();
|
|
386
401
|
}
|
|
387
402
|
|
|
388
403
|
async handleToolCall(params: SubagentParamsLike, ctx: ExtensionContext): Promise<AgentToolResult<Details>> {
|
|
@@ -407,22 +422,15 @@ export class ScheduledRunManager {
|
|
|
407
422
|
}
|
|
408
423
|
|
|
409
424
|
observedCompletionRunIds(): Set<string> {
|
|
410
|
-
|
|
425
|
+
return new Set(this.observedAsyncIds);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
referencedAsyncRunIds(): Set<string> {
|
|
429
|
+
const runIds = new Set(this.observedAsyncIds);
|
|
411
430
|
for (const store of this.stores.values()) {
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
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);
|
|
431
|
+
for (const scheduleId of store.ids()) {
|
|
432
|
+
for (const run of store.history(scheduleId)) {
|
|
433
|
+
if (run.asyncId) runIds.add(run.asyncId);
|
|
426
434
|
}
|
|
427
435
|
}
|
|
428
436
|
}
|
|
@@ -471,7 +479,7 @@ export class ScheduledRunManager {
|
|
|
471
479
|
if (this.deps.resolveCapabilityCeiling?.(sessionId)) return textResult("Cannot persist a schedule while a capability ceiling is active.", undefined, undefined, true);
|
|
472
480
|
if (store.list().length >= resolveMaxPending(this.deps.config)) return textResult(`Schedule limit reached (${resolveMaxPending(this.deps.config)}).`, undefined, undefined, true);
|
|
473
481
|
const id = validateScheduleId((params.id?.trim() || this.randomId()));
|
|
474
|
-
if (store.
|
|
482
|
+
if (store.ids().includes(id)) return textResult(`Schedule '${id}' already exists.`, undefined, undefined, true);
|
|
475
483
|
const now = this.now();
|
|
476
484
|
let trigger: ScheduleTrigger;
|
|
477
485
|
if (at) {
|
|
@@ -484,7 +492,7 @@ export class ScheduledRunManager {
|
|
|
484
492
|
const schedule: ScheduleRecord = {
|
|
485
493
|
schemaVersion: 1,
|
|
486
494
|
id,
|
|
487
|
-
name: params.name?.trim() ||
|
|
495
|
+
name: params.name?.trim() || targetLabel(target.target!),
|
|
488
496
|
cwd: path.resolve(params.cwd ?? ctx.cwd),
|
|
489
497
|
trigger,
|
|
490
498
|
target: target.target!,
|
|
@@ -563,9 +571,10 @@ export class ScheduledRunManager {
|
|
|
563
571
|
for (const schedule of store.list()) this.restoreOne(store, schedule);
|
|
564
572
|
}
|
|
565
573
|
|
|
566
|
-
private restoreOne(store: ScheduleStore, schedule: ScheduleRecord): void {
|
|
574
|
+
private restoreOne(store: ScheduleStore, schedule: ScheduleRecord, notBefore?: number, rearm = true): void {
|
|
567
575
|
if (schedule.activeRunId) {
|
|
568
576
|
const run = store.history(schedule.id).find((item) => item.id === schedule.activeRunId);
|
|
577
|
+
if (run?.state === "running" && run.asyncId) this.observedAsyncIds.add(run.asyncId);
|
|
569
578
|
const startedAt = run?.startedAt ? Date.parse(run.startedAt) : Number.NaN;
|
|
570
579
|
if (run?.state === "running" && run.asyncDir) {
|
|
571
580
|
try {
|
|
@@ -588,26 +597,59 @@ export class ScheduledRunManager {
|
|
|
588
597
|
fs.rmSync(path.join(store.directory(schedule.id), "active.lock"), { force: true });
|
|
589
598
|
}
|
|
590
599
|
}
|
|
591
|
-
if (schedule.paused) return;
|
|
600
|
+
if (!rearm || schedule.paused) return;
|
|
592
601
|
const next = nextRunAt(schedule);
|
|
593
602
|
if (next === undefined) return;
|
|
594
|
-
if (!schedule.activeRunId && next < this.now() && schedule.catchUp === "none")
|
|
595
|
-
|
|
603
|
+
if (!schedule.activeRunId && next < this.now() && schedule.catchUp === "none") {
|
|
604
|
+
try {
|
|
605
|
+
this.recordMissed(store, schedule, next, "timer");
|
|
606
|
+
} catch (error) {
|
|
607
|
+
if (notBefore !== undefined) this.arm(schedule, store, notBefore);
|
|
608
|
+
throw error;
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
this.arm(schedule, store, notBefore);
|
|
596
612
|
}
|
|
597
613
|
|
|
598
|
-
private arm(schedule: ScheduleRecord, store: ScheduleStore): void {
|
|
614
|
+
private arm(schedule: ScheduleRecord, store: ScheduleStore, notBefore?: number): void {
|
|
599
615
|
this.clearTimer(store, schedule.id);
|
|
600
616
|
if (schedule.paused) return;
|
|
601
617
|
const next = nextRunAt(schedule);
|
|
602
618
|
if (next === undefined) return;
|
|
603
|
-
const timer = this.timersApi.setTimeout(() =>
|
|
619
|
+
const timer = this.timersApi.setTimeout(() => {
|
|
620
|
+
// A timer callback is outside every caller's try/catch, so an escaping
|
|
621
|
+
// rejection here reaches the process as an uncaught exception and exits
|
|
622
|
+
// Pi. Contain every failure to this one schedule.
|
|
623
|
+
void this.fire(store, schedule.id).catch((error) => {
|
|
624
|
+
console.warn(`[pi-subagents] Scheduled run '${schedule.id}' failed to fire: ${error instanceof Error ? error.message : String(error)}`);
|
|
625
|
+
this.restoreAfterFireError(store, schedule.id);
|
|
626
|
+
});
|
|
627
|
+
}, Math.min(Math.max(0, next - this.now(), (notBefore ?? 0) - this.now()), MAX_TIMER_DELAY_MS));
|
|
604
628
|
timer.unref?.();
|
|
605
629
|
this.timers.set(this.timerKey(store, schedule.id), timer);
|
|
606
630
|
}
|
|
607
631
|
|
|
632
|
+
private restoreAfterFireError(store: ScheduleStore, id: string): void {
|
|
633
|
+
try {
|
|
634
|
+
const schedule = store.find(id);
|
|
635
|
+
if (!schedule) return;
|
|
636
|
+
const now = this.now();
|
|
637
|
+
const planned = schedule.trigger.kind === "interval" ? duePlannedAt(schedule, now) : undefined;
|
|
638
|
+
const notBefore = planned !== undefined && planned <= now ? Date.parse(nextAfter(schedule.trigger, planned, now)!) : undefined;
|
|
639
|
+
this.restoreOne(store, schedule, notBefore, schedule.trigger.kind === "interval");
|
|
640
|
+
} catch (error) {
|
|
641
|
+
console.warn(`[pi-subagents] Scheduled run '${id}' could not be restored after fire failure: ${error instanceof Error ? error.message : String(error)}`);
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
|
|
608
645
|
private async fire(store: ScheduleStore, id: string): Promise<void> {
|
|
609
646
|
this.clearTimer(store, id);
|
|
610
|
-
|
|
647
|
+
// Schedules are project-scoped and shared, and `delete` only clears the
|
|
648
|
+
// timer inside the deleting process. Another session can therefore remove
|
|
649
|
+
// a schedule while this process still holds an armed timer for it; there
|
|
650
|
+
// is then nothing to run and nothing to re-arm.
|
|
651
|
+
const schedule = store.find(id);
|
|
652
|
+
if (!schedule) return;
|
|
611
653
|
const planned = duePlannedAt(schedule, this.now());
|
|
612
654
|
if (planned === undefined || schedule.paused) return;
|
|
613
655
|
if (planned > this.now()) return this.arm(schedule, store);
|
|
@@ -661,6 +703,7 @@ export class ScheduledRunManager {
|
|
|
661
703
|
if (result.isError || !asyncId) throw new Error(result.content.find((item) => item.type === "text")?.text ?? "Scheduled launch failed.");
|
|
662
704
|
run.asyncId = asyncId;
|
|
663
705
|
run.asyncDir = result.details?.asyncDir;
|
|
706
|
+
this.observedAsyncIds.add(asyncId);
|
|
664
707
|
store.writeRun(schedule, run, "schedule.run.attached_async");
|
|
665
708
|
this.arm(schedule, store);
|
|
666
709
|
return run;
|
|
@@ -695,6 +738,7 @@ export class ScheduledRunManager {
|
|
|
695
738
|
schedule.trigger.nextRunAt = nextAfter(schedule.trigger, planned, now);
|
|
696
739
|
store.writeRun(schedule, skipped, "schedule.skipped_overlap");
|
|
697
740
|
}
|
|
741
|
+
if (run.asyncId) this.observedAsyncIds.delete(run.asyncId);
|
|
698
742
|
run.state = success ? "completed" : "failed_run";
|
|
699
743
|
run.completedAt = timestamp(now);
|
|
700
744
|
if (!success && error) run.error = error;
|
|
@@ -1,7 +1,8 @@
|
|
|
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 {
|
|
4
|
+
import { resultFilePath, resultPayloadPathForSessionRun, writeAsyncResultFile } from "./result-files.ts";
|
|
5
|
+
import { 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";
|
|
7
8
|
import { resolveEffectiveThinking } from "../../shared/model-info.ts";
|
|
@@ -16,6 +17,7 @@ interface StartedRunMetadata {
|
|
|
16
17
|
runId: string;
|
|
17
18
|
pid?: number;
|
|
18
19
|
sessionId?: string;
|
|
20
|
+
completionOwnerId?: string;
|
|
19
21
|
mode?: SubagentRunMode;
|
|
20
22
|
agents?: string[];
|
|
21
23
|
chainStepCount?: number;
|
|
@@ -98,14 +100,23 @@ interface ResultChildOutcome {
|
|
|
98
100
|
}
|
|
99
101
|
|
|
100
102
|
interface ResultRepairData {
|
|
101
|
-
state: "complete" | "failed" | "paused" | "stopped";
|
|
103
|
+
state: "complete" | "failed" | "paused" | "stopped" | "rejected";
|
|
102
104
|
results?: ResultChildOutcome[];
|
|
103
105
|
}
|
|
104
106
|
|
|
107
|
+
function regularFileExists(filePath: string): boolean {
|
|
108
|
+
try {
|
|
109
|
+
return fs.statSync(filePath).isFile();
|
|
110
|
+
} catch (error) {
|
|
111
|
+
if (isNotFoundError(error)) return false;
|
|
112
|
+
throw error;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
105
116
|
function readResultRepairData(resultPath: string): ResultRepairData | undefined {
|
|
106
117
|
try {
|
|
107
118
|
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";
|
|
119
|
+
const state = data.success ? "complete" : data.state === "stopped" ? "stopped" : data.state === "rejected" ? "rejected" : data.state === "paused" || data.exitCode === 0 ? "paused" : "failed";
|
|
109
120
|
const results = Array.isArray(data.results)
|
|
110
121
|
? data.results.map((entry, index) => {
|
|
111
122
|
if (!entry || typeof entry !== "object" || Array.isArray(entry)) return {};
|
|
@@ -124,7 +135,7 @@ function readResultRepairData(resultPath: string): ResultRepairData | undefined
|
|
|
124
135
|
}
|
|
125
136
|
}
|
|
126
137
|
|
|
127
|
-
function childState(overallState: ResultRepairData["state"], child: ResultChildOutcome | undefined): "
|
|
138
|
+
function childState(overallState: ResultRepairData["state"], child: ResultChildOutcome | undefined): ResultRepairData["state"] {
|
|
128
139
|
if (child?.success === true) return "complete";
|
|
129
140
|
if (child?.success === false) return "failed";
|
|
130
141
|
return overallState;
|
|
@@ -180,6 +191,7 @@ function buildStartedStatus(asyncDir: string, startedRun: StartedRunMetadata, no
|
|
|
180
191
|
return {
|
|
181
192
|
runId: startedRun.runId || path.basename(asyncDir),
|
|
182
193
|
...(startedRun.sessionId ? { sessionId: startedRun.sessionId } : {}),
|
|
194
|
+
...(startedRun.completionOwnerId ? { completionOwnerId: startedRun.completionOwnerId } : {}),
|
|
183
195
|
mode: startedRun.mode ?? "single",
|
|
184
196
|
state: "running",
|
|
185
197
|
pid: startedRun.pid,
|
|
@@ -197,7 +209,7 @@ function buildStartedStatus(asyncDir: string, startedRun: StartedRunMetadata, no
|
|
|
197
209
|
};
|
|
198
210
|
}
|
|
199
211
|
|
|
200
|
-
function buildFailedRepair(status: AsyncStatus, asyncDir: string, now: number, reason?: string): { status: AsyncStatus; result:
|
|
212
|
+
function buildFailedRepair(status: AsyncStatus, asyncDir: string, now: number, reason?: string): { status: AsyncStatus; result: Record<string, unknown>; message: string } {
|
|
201
213
|
const runId = status.runId || path.basename(asyncDir);
|
|
202
214
|
const pid = typeof status.pid === "number" ? status.pid : "unknown";
|
|
203
215
|
const baseMessage = reason ?? `Async runner process ${pid} exited or disappeared before writing a result. Marked run failed by stale-run reconciliation.`;
|
|
@@ -234,6 +246,7 @@ function buildFailedRepair(status: AsyncStatus, asyncDir: string, now: number, r
|
|
|
234
246
|
id: runId,
|
|
235
247
|
agent: resultAgent,
|
|
236
248
|
mode: status.mode,
|
|
249
|
+
...(status.completionOwnerId ? { completionOwnerId: status.completionOwnerId } : {}),
|
|
237
250
|
success: false,
|
|
238
251
|
state: "failed",
|
|
239
252
|
summary: message,
|
|
@@ -259,22 +272,22 @@ function buildFailedRepair(status: AsyncStatus, asyncDir: string, now: number, r
|
|
|
259
272
|
|
|
260
273
|
function writeFailedRepair(asyncDir: string, status: AsyncStatus, resultPath: string, now: number, reason?: string): ReconcileAsyncRunResult {
|
|
261
274
|
const repair = buildFailedRepair(status, asyncDir, now, reason);
|
|
262
|
-
|
|
275
|
+
if (repair.result.sessionId) writeAsyncResultFile(resultPath, repair.result);
|
|
263
276
|
writeAtomicJson(path.join(asyncDir, "status.json"), repair.status);
|
|
264
|
-
|
|
277
|
+
updateActiveRunIndex(asyncDir, repair.status.state, repair.status.toolCallId);
|
|
265
278
|
appendJsonlBestEffort(path.join(asyncDir, "events.jsonl"), {
|
|
266
279
|
type: "subagent.run.repaired_stale",
|
|
267
280
|
ts: now,
|
|
268
281
|
runId: repair.status.runId,
|
|
269
282
|
pid: status.pid,
|
|
270
|
-
resultPath,
|
|
283
|
+
...(repair.result.sessionId ? { resultPath } : {}),
|
|
271
284
|
message: repair.message,
|
|
272
285
|
});
|
|
273
|
-
return { status: repair.status, repaired: true, resultPath, message: repair.message };
|
|
286
|
+
return { status: repair.status, repaired: true, ...(repair.result.sessionId ? { resultPath } : {}), message: repair.message };
|
|
274
287
|
}
|
|
275
288
|
|
|
276
289
|
function terminal(state: AsyncStatus["state"]): boolean {
|
|
277
|
-
return state === "complete" || state === "failed" || state === "paused" || state === "stopped";
|
|
290
|
+
return state === "complete" || state === "failed" || state === "paused" || state === "stopped" || state === "rejected";
|
|
278
291
|
}
|
|
279
292
|
|
|
280
293
|
function* nestedRuns(children: NestedRunSummary[] | undefined): Generator<NestedRunSummary> {
|
|
@@ -344,17 +357,21 @@ export function reconcileAsyncRun(asyncDir: string, options: ReconcileAsyncRunOp
|
|
|
344
357
|
if (stepRecord.thinking !== undefined && typeof stepRecord.thinking !== "string") throw new Error(`Invalid async status file '${statusPath}': steps[${index}].thinking must be a string.`);
|
|
345
358
|
}
|
|
346
359
|
const runId = effectiveStatus.runId || path.basename(asyncDir);
|
|
347
|
-
const
|
|
348
|
-
|
|
360
|
+
const resultsDir = options.resultsDir ?? DIRS.results;
|
|
361
|
+
const resultPath = resultFilePath(resultsDir, runId);
|
|
362
|
+
const existingResultPath = effectiveStatus.sessionId
|
|
363
|
+
? resultPayloadPathForSessionRun(resultsDir, effectiveStatus.sessionId, runId) ?? (regularFileExists(resultPath) ? resultPath : undefined)
|
|
364
|
+
: regularFileExists(resultPath) ? resultPath : undefined;
|
|
365
|
+
if (existingResultPath) {
|
|
349
366
|
const terminalStatus = effectiveStatus.state === "running" || effectiveStatus.state === "queued"
|
|
350
|
-
? terminalStatusFromResult(effectiveStatus,
|
|
367
|
+
? terminalStatusFromResult(effectiveStatus, existingResultPath, now)
|
|
351
368
|
: undefined;
|
|
352
369
|
if (terminalStatus) {
|
|
353
370
|
writeAtomicJson(path.join(asyncDir, "status.json"), terminalStatus);
|
|
354
|
-
|
|
355
|
-
return { status: terminalStatus, repaired: true, resultPath, message: "Existing async result file was used to repair stale running status." };
|
|
371
|
+
updateActiveRunIndex(asyncDir, terminalStatus.state, terminalStatus.toolCallId);
|
|
372
|
+
return { status: terminalStatus, repaired: true, resultPath: existingResultPath, message: "Existing async result file was used to repair stale running status." };
|
|
356
373
|
}
|
|
357
|
-
if (effectiveStatus.displayDismissedAt === undefined) return { status: effectiveStatus, repaired: false, resultPath };
|
|
374
|
+
if (effectiveStatus.displayDismissedAt === undefined) return { status: effectiveStatus, repaired: false, resultPath: existingResultPath };
|
|
358
375
|
}
|
|
359
376
|
if (effectiveStatus.displayDismissedAt !== undefined) {
|
|
360
377
|
return { status: null, repaired: false, resultPath };
|
|
@@ -13,10 +13,20 @@ import type {
|
|
|
13
13
|
SteeringTargetStatus,
|
|
14
14
|
} from "../../shared/types.ts";
|
|
15
15
|
import { readStatus } from "../../shared/utils.ts";
|
|
16
|
+
import { previewDisplayText } from "../../shared/display-text.ts";
|
|
17
|
+
import { redactSecretValues } from "../shared/permissions.ts";
|
|
16
18
|
|
|
17
19
|
export const MAX_STEERING_REQUESTS = 20;
|
|
18
20
|
export const STEERING_MESSAGE_PREVIEW_LIMIT = 160;
|
|
19
21
|
|
|
22
|
+
export function steeringMessagePreview(message: string): string {
|
|
23
|
+
return previewDisplayText(redactSecretValues(message), STEERING_MESSAGE_PREVIEW_LIMIT);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function steeringReceipt(message: string, receipt: string): string {
|
|
27
|
+
return `${receipt} Message: ${JSON.stringify(steeringMessagePreview(message))}`;
|
|
28
|
+
}
|
|
29
|
+
|
|
20
30
|
export function createSteeringStatus(): SteeringStatus {
|
|
21
31
|
return { requested: 0, scheduled: 0, pending: 0, delivered: 0, failed: 0, recovered: 0, recent: [] };
|
|
22
32
|
}
|
|
@@ -35,7 +45,7 @@ export function recordSteeringRequest(
|
|
|
35
45
|
id: input.id,
|
|
36
46
|
requestedAt: input.requestedAt,
|
|
37
47
|
...(input.source ? { source: input.source } : {}),
|
|
38
|
-
messagePreview: input.message
|
|
48
|
+
messagePreview: steeringMessagePreview(input.message),
|
|
39
49
|
targets: input.targets.map((target) => ({ index: target.index, state: target.state, ...(target.reason ? { reason: target.reason } : {}) })),
|
|
40
50
|
};
|
|
41
51
|
status.requested++;
|