pi-subagents 0.67.0 → 0.69.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 +90 -0
- package/README.md +1 -1
- package/docs/agents.md +41 -12
- package/docs/configuration.md +61 -19
- package/docs/extension-api.md +5 -1
- package/docs/missions.md +2 -2
- package/docs/models.md +11 -79
- package/docs/observability.md +18 -8
- package/docs/standalone-background.md +13 -3
- package/docs/tool-reference.md +38 -14
- package/docs/watchdog.md +10 -12
- package/docs/workflows.md +59 -1
- package/index.ts +5 -2
- package/package.json +4 -2
- package/runner-peer-loader.mjs +24 -0
- package/runner-peer-preload.mjs +25 -11
- package/skills/pi-subagents/SKILL.md +18 -21
- package/skills/pi-subagents/references/constraints-and-recipes.md +3 -2
- package/skills/pi-subagents/references/execution-controls.md +6 -4
- package/skills/pi-subagents/references/management-authoring-rpc.md +0 -1
- package/skills/pi-subagents/references/multi-lane-orchestration.md +1 -1
- package/skills/pi-subagents/references/prompting-and-roles.md +16 -12
- package/skills/pi-subagents/references/review-and-validation.md +3 -3
- package/src/agents/agent-management.ts +57 -58
- package/src/agents/agent-serializer.ts +4 -3
- package/src/agents/agents.ts +185 -72
- package/src/agents/chain-serializer.ts +5 -0
- package/src/agents/runtime-agent-registry.ts +7 -6
- package/src/agents/skills.ts +1 -1
- package/src/api/preflight.ts +20 -16
- package/src/api/required-child-extensions.ts +6 -0
- package/src/extension/config.ts +10 -37
- package/src/extension/fanout-child.ts +3 -0
- package/src/extension/herdr-pi-bridge.ts +160 -0
- package/src/extension/index.ts +42 -31
- package/src/extension/public-execution.ts +3 -3
- package/src/extension/schemas.ts +23 -6
- package/src/extension/tool-description.ts +8 -7
- package/src/inspectors/ghostty/plugin.ts +13 -1
- package/src/intercom/native-supervisor-channel.ts +22 -18
- package/src/policy/authority.ts +4 -0
- package/src/profiles/profiles.ts +12 -6
- package/src/runs/background/active-run-index.ts +17 -1
- package/src/runs/background/async-execution.ts +309 -126
- package/src/runs/background/async-job-tracker.ts +8 -6
- package/src/runs/background/async-resume.ts +13 -4
- package/src/runs/background/async-status.ts +15 -4
- package/src/runs/background/auto-drain.ts +20 -10
- package/src/runs/background/binary-bootstrap.ts +5 -0
- package/src/runs/background/chain-append.ts +1 -1
- package/src/runs/background/chain-root-attachment.ts +14 -33
- package/src/runs/background/notify.ts +74 -6
- package/src/runs/background/result-files.ts +8 -4
- package/src/runs/background/result-watcher.ts +19 -2
- package/src/runs/background/run-child-session.ts +20 -29
- package/src/runs/background/runner-aliases.ts +4 -33
- package/src/runs/background/runner-child-launch.ts +4 -1
- package/src/runs/background/runner-child-sessions.ts +2 -2
- package/src/runs/background/runner-http-dispatcher.ts +119 -0
- package/src/runs/background/scheduled-runs.ts +11 -5
- package/src/runs/background/stale-run-reconciler.ts +35 -11
- package/src/runs/background/subagent-runner.ts +413 -276
- package/src/runs/background/subagent-wait.ts +128 -23
- package/src/runs/background/wait-completions.ts +75 -27
- package/src/runs/background/wait-subscriptions.ts +9 -3
- package/src/runs/background/wait-tool.ts +4 -2
- package/src/runs/foreground/async-stop-action.ts +93 -3
- package/src/runs/foreground/execution.ts +115 -219
- package/src/runs/foreground/foreground-history.ts +2 -1
- package/src/runs/foreground/subagent-executor.ts +281 -80
- package/src/runs/shared/acceptance.ts +194 -37
- package/src/runs/shared/async-status-projection.ts +123 -33
- package/src/runs/shared/child-launch-plan.ts +15 -3
- package/src/runs/shared/child-launch.ts +19 -6
- package/src/runs/shared/child-runtime-config.ts +5 -0
- package/src/runs/shared/child-session.ts +94 -50
- package/src/runs/shared/child-tool-plan.ts +28 -16
- package/src/runs/shared/dynamic-fanout.ts +2 -2
- package/src/runs/shared/external-cli-contract.ts +11 -1
- package/src/runs/shared/external-cli-preflight.ts +6 -2
- package/src/runs/shared/herdr-connection.ts +134 -0
- package/src/runs/shared/herdr-external-adapters.ts +169 -0
- package/src/runs/shared/herdr-machine.ts +279 -0
- package/src/runs/shared/herdr-pi-protocol.ts +59 -0
- package/src/runs/shared/herdr-placed-run.ts +263 -0
- package/src/runs/shared/model-resolution-diagnostic.ts +76 -0
- package/src/runs/shared/{model-fallback.ts → model-resolution.ts} +22 -237
- package/src/runs/shared/model-scope.ts +1 -1
- package/src/runs/shared/nested-events.ts +11 -2
- package/src/runs/shared/parallel-utils.ts +7 -2
- package/src/runs/shared/pi-spawn.ts +1 -1
- package/src/runs/shared/subagent-prompt-runtime.ts +4 -2
- package/src/runs/shared/worktree-setup-command.ts +27 -4
- package/src/runs/shared/worktree.ts +30 -8
- package/src/shared/child-cache-retention.ts +43 -0
- package/src/shared/launch-contract.ts +6 -9
- package/src/shared/pruned-fork.ts +1 -1
- package/src/shared/required-child-extensions.ts +81 -0
- package/src/shared/settings.ts +5 -2
- package/src/shared/shortcuts.ts +0 -4
- package/src/shared/types.ts +81 -29
- package/src/slash/slash-commands.ts +0 -6
- package/src/slash/subagents-admin.ts +13 -9
- package/src/tui/render.ts +20 -10
- package/src/watchdog/child-status.ts +28 -36
- package/src/watchdog/lsp-diagnostics.ts +1 -1
- package/src/watchdog/model-selection.ts +1 -1
- package/src/watchdog/register-child.ts +10 -3
- package/src/watchdog/register-main.ts +20 -20
- package/src/watchdog/render.ts +1 -1
- package/src/watchdog/review.ts +14 -30
- package/src/watchdog/rules.ts +1 -1
- package/src/watchdog/runtime.ts +23 -12
- package/src/watchdog/settings.ts +3 -6
- package/src/watchdog/types.ts +3 -5
- package/src/watchdog/warning-format.ts +1 -1
- package/src/workflows/scripted-workflow.ts +68 -7
- package/src/workflows/workflow-receipt.ts +21 -3
- package/src/workflows/workflow-resources.ts +13 -2
- package/src/runs/shared/model-exclusions.ts +0 -374
- package/src/runs/shared/readonly-model-continuation.ts +0 -69
- package/src/runs/shared/readonly-session-evidence.ts +0 -307
|
@@ -241,10 +241,15 @@ function pendingResultExists(resultsDir: string, sessionId: string, runId: strin
|
|
|
241
241
|
return firstExistingResultFile(resultPendingPaths(resultsDir, sessionId, runId)) !== undefined;
|
|
242
242
|
}
|
|
243
243
|
|
|
244
|
+
export function resultPayloadMatchesSessionRun(data: unknown, sessionId: string, runId: string): boolean {
|
|
245
|
+
if (!data || typeof data !== "object" || Array.isArray(data)) return false;
|
|
246
|
+
const record = data as Record<string, unknown>;
|
|
247
|
+
return (nonEmptyString(record.runId) ?? nonEmptyString(record.id)) === runId && nonEmptyString(record.sessionId) === sessionId;
|
|
248
|
+
}
|
|
249
|
+
|
|
244
250
|
function pendingResultPayloadMatches(filePath: string, sessionId: string, runId: string): boolean {
|
|
245
251
|
try {
|
|
246
|
-
|
|
247
|
-
return (nonEmptyString(data.runId) ?? nonEmptyString(data.id)) === runId && nonEmptyString(data.sessionId) === sessionId;
|
|
252
|
+
return resultPayloadMatchesSessionRun(JSON.parse(fs.readFileSync(filePath, "utf-8")), sessionId, runId);
|
|
248
253
|
} catch (error) {
|
|
249
254
|
if ((error as NodeJS.ErrnoException).code !== "ENOENT" && !isUnaddressableResultCandidate(error)) console.error(`Ignoring invalid pending async result '${filePath}':`, error);
|
|
250
255
|
return false;
|
|
@@ -252,8 +257,7 @@ function pendingResultPayloadMatches(filePath: string, sessionId: string, runId:
|
|
|
252
257
|
}
|
|
253
258
|
|
|
254
259
|
function assertPendingResultPayloadMatches(filePath: string, sessionId: string, runId: string): boolean {
|
|
255
|
-
|
|
256
|
-
return (nonEmptyString(data.runId) ?? nonEmptyString(data.id)) === runId && nonEmptyString(data.sessionId) === sessionId;
|
|
260
|
+
return resultPayloadMatchesSessionRun(JSON.parse(fs.readFileSync(filePath, "utf-8")), sessionId, runId);
|
|
257
261
|
}
|
|
258
262
|
|
|
259
263
|
export function promotePendingResultFile(resultsDir: string, sessionId: string, runId: string, file = resultFileName(runId), options: { logFailure?: boolean } = {}): "none" | "promoted" | "pending" {
|
|
@@ -70,6 +70,7 @@ type ResultFileChild = {
|
|
|
70
70
|
sessionName?: string;
|
|
71
71
|
output?: string;
|
|
72
72
|
structuredOutput?: unknown;
|
|
73
|
+
structuredOutputPath?: string;
|
|
73
74
|
outputState?: SubagentOutputState;
|
|
74
75
|
error?: string;
|
|
75
76
|
success?: boolean;
|
|
@@ -81,6 +82,8 @@ type ResultFileChild = {
|
|
|
81
82
|
processSignal?: string | null;
|
|
82
83
|
sessionFile?: string;
|
|
83
84
|
artifactPaths?: { outputPath?: string };
|
|
85
|
+
outputSaveError?: string;
|
|
86
|
+
artifactOutputSaveFailed?: true;
|
|
84
87
|
intercomTarget?: string;
|
|
85
88
|
children?: unknown;
|
|
86
89
|
};
|
|
@@ -408,7 +411,9 @@ export function createResultWatcher(
|
|
|
408
411
|
if (typeof data.sessionId !== "string" || !data.sessionId) return;
|
|
409
412
|
const sessionId = data.sessionId;
|
|
410
413
|
const completionOwnerId = data.completionOwnerId;
|
|
411
|
-
const runId = data.runId
|
|
414
|
+
const runId = typeof data.runId === "string" && data.runId
|
|
415
|
+
? data.runId
|
|
416
|
+
: typeof data.id === "string" && data.id ? data.id : file.replace(/\.json$/i, "");
|
|
412
417
|
const toolCallId = typeof data.toolCallId === "string" ? data.toolCallId : undefined;
|
|
413
418
|
let observerSucceeded = true;
|
|
414
419
|
try {
|
|
@@ -428,7 +433,7 @@ export function createResultWatcher(
|
|
|
428
433
|
if (!ownsCompletion(sessionId, completionOwnerId, epoch)) return;
|
|
429
434
|
// Recorded before dedupe and before the unlink below so bg_wait can
|
|
430
435
|
// use the in-memory record or its bounded durable replay after cleanup.
|
|
431
|
-
recordWaitCompletion(state, runId, data, Date.now(), completionTtlMs, {
|
|
436
|
+
const completionPersisted = recordWaitCompletion(state, runId, data, Date.now(), completionTtlMs, {
|
|
432
437
|
resultsDir,
|
|
433
438
|
sessionId,
|
|
434
439
|
});
|
|
@@ -456,6 +461,10 @@ export function createResultWatcher(
|
|
|
456
461
|
}
|
|
457
462
|
if (!ownsCompletion(sessionId, completionOwnerId, epoch)) return;
|
|
458
463
|
if (markReplacedPayload()) return;
|
|
464
|
+
if (!completionPersisted) {
|
|
465
|
+
scheduleResult(file, triggerTurn, RETRY_DELAY_MS);
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
459
468
|
if (!removeDeliveredResult(file, sessionId, runId, toolCallId)) scheduleResult(file, triggerTurn, RETRY_DELAY_MS);
|
|
460
469
|
return;
|
|
461
470
|
}
|
|
@@ -513,6 +522,10 @@ export function createResultWatcher(
|
|
|
513
522
|
}
|
|
514
523
|
if (!ownsCompletion(sessionId, completionOwnerId, epoch)) return;
|
|
515
524
|
if (markReplacedPayload()) return;
|
|
525
|
+
if (!completionPersisted) {
|
|
526
|
+
scheduleResult(file, triggerTurn, RETRY_DELAY_MS);
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
516
529
|
if (!removeDeliveredResult(file, sessionId, runId, toolCallId)) scheduleResult(file, triggerTurn, RETRY_DELAY_MS);
|
|
517
530
|
return;
|
|
518
531
|
}
|
|
@@ -600,6 +613,10 @@ export function createResultWatcher(
|
|
|
600
613
|
return;
|
|
601
614
|
}
|
|
602
615
|
if (!ownsCompletion(sessionId, completionOwnerId, epoch)) return;
|
|
616
|
+
if (!completionPersisted) {
|
|
617
|
+
scheduleResult(file, triggerTurn, RETRY_DELAY_MS);
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
603
620
|
if (!removeDeliveredResult(file, sessionId, runId, toolCallId)) scheduleResult(file, triggerTurn, RETRY_DELAY_MS);
|
|
604
621
|
} catch (error) {
|
|
605
622
|
if (isAccessDenied(error)) {
|
|
@@ -19,13 +19,13 @@ import {
|
|
|
19
19
|
type ChildWatchdogStatusEvent,
|
|
20
20
|
} from "../../watchdog/child-status.ts";
|
|
21
21
|
import { projectChildLifecycle, type ChildLifecycleAction, type ChildLifecycleState } from "../shared/child-lifecycle.ts";
|
|
22
|
-
import { formatSubagentModelVerificationError } from "../shared/model-
|
|
22
|
+
import { formatSubagentModelVerificationError } from "../shared/model-resolution.ts";
|
|
23
|
+
import { formatChildModelResolutionDiagnostic, isChildModelResolutionFailure } from "../shared/model-resolution-diagnostic.ts";
|
|
23
24
|
import { isMutatingTool, resolveCurrentPath } from "../shared/long-running-guard.ts";
|
|
24
25
|
import { effectiveToolTimeoutMs, formatToolTimeoutMessage, toolTimeoutCallKey } from "../shared/tool-timeout.ts";
|
|
25
26
|
import { createReportedChildSessionInput, type InProcessChildLaunch } from "../shared/child-launch.ts";
|
|
26
27
|
import { childSessionHasQueuedMessages, projectChildSessionEventForJson, type ChildSession, type ChildSessionEvent, type ChildSessionFactory } from "../shared/child-session.ts";
|
|
27
28
|
import { formatSteerMessage } from "../shared/subagent-prompt-runtime.ts";
|
|
28
|
-
import { getReadonlySessionEvidence, requestReadonlySessionEvidence, type SettledReadonlyEvidence } from "../shared/readonly-session-evidence.ts";
|
|
29
29
|
import type { SteerDeliveryStatus, SteerRequest } from "./control-channel.ts";
|
|
30
30
|
import { takeMatchingAcceptedSteer, unconsumedSteerReason } from "./steering.ts";
|
|
31
31
|
|
|
@@ -99,16 +99,6 @@ export interface RunChildSessionInput {
|
|
|
99
99
|
modelVerificationRegistry?: Array<{ provider: string; id: string; fullId: string }>;
|
|
100
100
|
modelResponseAliases?: Record<string, string[]>;
|
|
101
101
|
mutationTools?: readonly string[];
|
|
102
|
-
/** Internal guarded continuation handoff; never part of persisted results. */
|
|
103
|
-
readonlyContinuation?: { source: ChildSession; expected: SettledReadonlyEvidence; modelId: string };
|
|
104
|
-
collectReadonlyEvidence?: boolean;
|
|
105
|
-
canContinue?: () => boolean;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
const settledChildren = new WeakMap<RunChildSessionResult, ChildSession>();
|
|
109
|
-
export function getSettledReadonlyChild(result: RunChildSessionResult): ChildSession | undefined {
|
|
110
|
-
const child = settledChildren.get(result);
|
|
111
|
-
return child && getReadonlySessionEvidence(child) ? child : undefined;
|
|
112
102
|
}
|
|
113
103
|
|
|
114
104
|
export interface RunChildSessionResult {
|
|
@@ -118,6 +108,7 @@ export interface RunChildSessionResult {
|
|
|
118
108
|
toolCount: number;
|
|
119
109
|
durationMs: number;
|
|
120
110
|
model?: string;
|
|
111
|
+
nativeMachine?: { provider: "herdr"; machineId: string; initialGit?: import("../../shared/types.ts").HerdrRemoteGitStatus; finalGit?: import("../../shared/types.ts").HerdrRemoteGitStatus };
|
|
121
112
|
error?: string;
|
|
122
113
|
finalOutput: string;
|
|
123
114
|
outputState: SubagentOutputState;
|
|
@@ -133,12 +124,12 @@ export interface RunChildSessionResult {
|
|
|
133
124
|
currentToolArgs?: string;
|
|
134
125
|
currentPath?: string;
|
|
135
126
|
afterCompactionSettlement?: boolean;
|
|
127
|
+
abortRecoveryDiagnostic?: string;
|
|
136
128
|
/** Set by the runner while it finalizes the attempt. */
|
|
137
129
|
toolBudget?: ToolBudgetState;
|
|
138
130
|
toolBudgetBlocked?: boolean;
|
|
139
131
|
structuredOutput?: unknown;
|
|
140
132
|
runtimeAcknowledgedExtensions?: RuntimeAcknowledgedChildExtensions;
|
|
141
|
-
abortRecoveryDiagnostic?: string;
|
|
142
133
|
effects?: EffectsProjection;
|
|
143
134
|
}
|
|
144
135
|
|
|
@@ -575,8 +566,21 @@ export function runChildSession(input: RunChildSessionInput): Promise<RunChildSe
|
|
|
575
566
|
const closed = finish();
|
|
576
567
|
const finalOutput = getFinalOutput(messages);
|
|
577
568
|
let finalError = error ?? assistantError;
|
|
578
|
-
|
|
579
|
-
|
|
569
|
+
const promptErrorMessage = promptError === undefined ? undefined : promptError instanceof Error ? promptError.message : String(promptError);
|
|
570
|
+
if (!finalError && promptErrorMessage !== undefined) {
|
|
571
|
+
finalError = promptErrorMessage;
|
|
572
|
+
}
|
|
573
|
+
// A child launched without the ambient extensions resolves a provider
|
|
574
|
+
// extension's model as "not found". Annotate only a creation/prompt failure
|
|
575
|
+
// that produced no turn; keep the core error and add the rule and the
|
|
576
|
+
// remedies that load the extension for this child.
|
|
577
|
+
if (promptErrorMessage !== undefined
|
|
578
|
+
&& finalError === promptErrorMessage
|
|
579
|
+
&& isChildModelResolutionFailure(promptErrorMessage)
|
|
580
|
+
&& messages.length === 0
|
|
581
|
+
&& usage.turns === 0
|
|
582
|
+
&& !input.launch.session.ambientExtensions) {
|
|
583
|
+
finalError = `${promptErrorMessage}\n\n${formatChildModelResolutionDiagnostic({ agent: input.launch.config.agent, model: input.launch.session.model, host: "runner", capabilityCeiling: input.launch.toolPlan.capabilityCeiling })}`;
|
|
580
584
|
}
|
|
581
585
|
const forcedDrainAfterFinalSuccess = (forced || forcedTermination) && (cleanTerminalAssistantStopReceived || agentSettledReceived) && !finalError;
|
|
582
586
|
const forcedDrainAfterEmptyTerminal = forcedDrainAfterFinalSuccess && hasEmptyTerminalAssistantResponse(messages);
|
|
@@ -596,6 +600,7 @@ export function runChildSession(input: RunChildSessionInput): Promise<RunChildSe
|
|
|
596
600
|
toolCount,
|
|
597
601
|
durationMs: Date.now() - startedAt,
|
|
598
602
|
model,
|
|
603
|
+
nativeMachine: session?.machineEvidence ? { provider: "herdr", machineId: session.machineEvidence.machineId, ...(session.machineEvidence.initial ? { initialGit: session.machineEvidence.initial } : {}), ...(session.machineEvidence.final ? { finalGit: session.machineEvidence.final } : {}) } : undefined,
|
|
599
604
|
error: stopped ? stopMessage() : timedOut ? (error ?? timeoutMessage()) : interrupted || (forcedDrainAfterFinalSuccess && !forcedDrainAfterEmptyTerminal) ? undefined : finalError,
|
|
600
605
|
finalOutput: (timedOut || stopped) && !finalOutput.trim() ? (stopped ? stopMessage() : error ?? timeoutMessage()) : finalOutput,
|
|
601
606
|
outputState: finalOutput.trim() ? "present" : "absent",
|
|
@@ -612,7 +617,6 @@ export function runChildSession(input: RunChildSessionInput): Promise<RunChildSe
|
|
|
612
617
|
currentPath,
|
|
613
618
|
afterCompactionSettlement: afterCompactionSettlement || undefined,
|
|
614
619
|
});
|
|
615
|
-
if (session && !forced && !forcedTermination && !interrupted && !timedOut && !stopped && getReadonlySessionEvidence(session)) settledChildren.set(result, session);
|
|
616
620
|
resolve(result);
|
|
617
621
|
});
|
|
618
622
|
};
|
|
@@ -634,17 +638,7 @@ export function runChildSession(input: RunChildSessionInput): Promise<RunChildSe
|
|
|
634
638
|
|
|
635
639
|
void (async () => {
|
|
636
640
|
try {
|
|
637
|
-
const continuation = input.readonlyContinuation;
|
|
638
|
-
const checkContinuation = () => {
|
|
639
|
-
if (!continuation) return;
|
|
640
|
-
if (interrupted || timedOut || stopped || input.canContinue?.() !== true
|
|
641
|
-
|| (input.runDeadlineAt !== undefined && Date.now() >= input.runDeadlineAt)
|
|
642
|
-
|| getReadonlySessionEvidence(continuation.source) !== continuation.expected
|
|
643
|
-
|| continuation.source.detached || continuation.source.shutDown) throw new Error("Read-only continuation handoff vetoed");
|
|
644
|
-
};
|
|
645
|
-
checkContinuation();
|
|
646
641
|
const createInput = createReportedChildSessionInput(input.launch, input.transcriptWriter);
|
|
647
|
-
if (input.collectReadonlyEvidence || continuation) requestReadonlySessionEvidence(createInput, continuation?.expected);
|
|
648
642
|
const created = await input.factory.create(createInput);
|
|
649
643
|
if (settled) {
|
|
650
644
|
void created.dispose();
|
|
@@ -661,8 +655,6 @@ export function runChildSession(input: RunChildSessionInput): Promise<RunChildSe
|
|
|
661
655
|
if (cleanTerminalAssistantStopReceived || agentSettledReceived) queuedDrainHold = true;
|
|
662
656
|
return followUp(text);
|
|
663
657
|
};
|
|
664
|
-
checkContinuation();
|
|
665
|
-
if (continuation && (created.modelId !== continuation.modelId || input.launch.capture.completionIntentContext?.()?.model?.api !== continuation.expected.api)) throw new Error("Read-only continuation model changed");
|
|
666
658
|
unsubscribe = created.subscribe(processEvent);
|
|
667
659
|
input.registerWatchdogStatus?.((event) => processEvent(event as unknown as ChildSessionEvent));
|
|
668
660
|
input.registerSteer?.(async (request) => {
|
|
@@ -686,7 +678,6 @@ export function runChildSession(input: RunChildSessionInput): Promise<RunChildSe
|
|
|
686
678
|
return queued;
|
|
687
679
|
});
|
|
688
680
|
if (interrupted || timedOut || stopped) abortChild();
|
|
689
|
-
checkContinuation();
|
|
690
681
|
await created.prompt(input.prompt);
|
|
691
682
|
promptSettled = true;
|
|
692
683
|
settle(undefined);
|
|
@@ -8,11 +8,9 @@
|
|
|
8
8
|
* aliases those specifiers to the copies shipped inside the installed pi
|
|
9
9
|
* package; the parent computes the same map and hands it to the runner
|
|
10
10
|
* through `JITI_ALIAS`, so child sessions and hooks retain host API identity.
|
|
11
|
-
* Only Pi 0.85.0's missing server exports may come from this extension.
|
|
12
11
|
*/
|
|
13
12
|
import * as fs from "node:fs";
|
|
14
13
|
import * as path from "node:path";
|
|
15
|
-
import { fileURLToPath } from "node:url";
|
|
16
14
|
|
|
17
15
|
export const JITI_ALIAS_ENV = "JITI_ALIAS";
|
|
18
16
|
|
|
@@ -37,13 +35,6 @@ const CHORD_PEER_ALIASES = [
|
|
|
37
35
|
{ specifier: "@earendil-works/chord/context", pkg: "@earendil-works/chord", subpath: "./context" },
|
|
38
36
|
];
|
|
39
37
|
|
|
40
|
-
/** Experimental dependencies accidentally published in exactly Pi 0.85.0. */
|
|
41
|
-
const PI0850_PEER_ALIASES = [
|
|
42
|
-
{ specifier: "@earendil-works/pi-server", pkg: "@earendil-works/pi-server", subpath: "." },
|
|
43
|
-
{ specifier: "@earendil-works/pi-server/unix", pkg: "@earendil-works/pi-server", subpath: "./unix" },
|
|
44
|
-
{ specifier: "@earendil-works/pi-client/unix", pkg: "@earendil-works/pi-client", subpath: "./unix" },
|
|
45
|
-
];
|
|
46
|
-
|
|
47
38
|
interface PackageManifest {
|
|
48
39
|
name?: unknown;
|
|
49
40
|
version?: unknown;
|
|
@@ -132,40 +123,20 @@ function findPeerPackageDir(piPackageRoot: string, pkg: string, hostName: unknow
|
|
|
132
123
|
}
|
|
133
124
|
|
|
134
125
|
/** The alias map the runner needs, or the specifiers that could not be resolved. */
|
|
135
|
-
export function resolveHostPeerAliases(
|
|
136
|
-
piPackageRoot: string,
|
|
137
|
-
extensionRoot = fileURLToPath(new URL("../../../", import.meta.url)),
|
|
138
|
-
): { aliases: Record<string, string>; missing: string[]; supplemental: string[] } {
|
|
126
|
+
export function resolveHostPeerAliases(piPackageRoot: string): { aliases: Record<string, string>; missing: string[] } {
|
|
139
127
|
const aliases: Record<string, string> = {};
|
|
140
128
|
const missing: string[] = [];
|
|
141
|
-
const supplemental: string[] = [];
|
|
142
129
|
const hostManifest = readManifest(piPackageRoot);
|
|
143
|
-
const isPi0850 = hostManifest?.version === "0.85.0";
|
|
144
130
|
// Only known stable pre-chord versions may omit it. Unknown/prerelease
|
|
145
131
|
// hosts retain the required aliases, rather than hiding a broken install.
|
|
146
132
|
const stableVersion = typeof hostManifest?.version === "string" ? /^0\.(\d+)\.\d+$/.exec(hostManifest.version) : null;
|
|
147
133
|
const isPreChord = stableVersion !== null && Number(stableVersion[1]) < 85;
|
|
148
|
-
const required = [...HOST_PEER_ALIASES, ...(isPreChord ? [] : CHORD_PEER_ALIASES)
|
|
134
|
+
const required = [...HOST_PEER_ALIASES, ...(isPreChord ? [] : CHORD_PEER_ALIASES)];
|
|
149
135
|
for (const { specifier, pkg, subpath } of required) {
|
|
150
136
|
const packageDir = findPeerPackageDir(piPackageRoot, pkg, hostManifest?.name);
|
|
151
|
-
|
|
152
|
-
// Pi 0.85.0 omitted this runtime dependency. Never replace working host
|
|
153
|
-
// exports or extend this exact version contract to other peers/hosts.
|
|
154
|
-
if ((!target || !fs.existsSync(target))
|
|
155
|
-
&& (specifier === "@earendil-works/pi-server" || specifier === "@earendil-works/pi-server/unix")
|
|
156
|
-
&& isPi0850
|
|
157
|
-
&& (!packageDir || readManifest(packageDir)?.version === "0.85.0")) {
|
|
158
|
-
const localDir = findHostPeerPackageDir(extensionRoot, pkg);
|
|
159
|
-
if (localDir && readManifest(localDir)?.version === "0.85.0") {
|
|
160
|
-
const localTarget = resolvePackageSubpath(localDir, subpath);
|
|
161
|
-
if (localTarget && fs.existsSync(localTarget)) {
|
|
162
|
-
target = localTarget;
|
|
163
|
-
supplemental.push(specifier);
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
}
|
|
137
|
+
const target = packageDir ? resolvePackageSubpath(packageDir, subpath) : undefined;
|
|
167
138
|
if (target && fs.existsSync(target)) aliases[specifier] = target;
|
|
168
139
|
else missing.push(specifier);
|
|
169
140
|
}
|
|
170
|
-
return { aliases, missing
|
|
141
|
+
return { aliases, missing };
|
|
171
142
|
}
|
|
@@ -35,6 +35,9 @@ export function buildRunnerChildLaunch(step: RunnerSubagentStep, ctx: RunnerChil
|
|
|
35
35
|
? formatAcceptancePrompt(step.effectiveAcceptance, { reportOptional: isAgentContract(step.agentContract), structuredOutput: Boolean(step.structuredOutput?.acceptanceReportPath) })
|
|
36
36
|
: "";
|
|
37
37
|
return buildInProcessChildLaunch({
|
|
38
|
+
machine: step.machine,
|
|
39
|
+
remoteSkillNames: step.skills,
|
|
40
|
+
remoteReads: step.remoteReads,
|
|
38
41
|
parentSessionId: step.parentSessionId,
|
|
39
42
|
forkCacheKey: step.context === "fork" ? deriveForkPromptCacheKey(step.parentSessionId) : undefined,
|
|
40
43
|
sessionEnabled: attempt.sessionEnabled,
|
|
@@ -50,8 +53,8 @@ export function buildRunnerChildLaunch(step: RunnerSubagentStep, ctx: RunnerChil
|
|
|
50
53
|
allowNestedSubagents: step.allowNestedSubagents,
|
|
51
54
|
extensions: step.extensions,
|
|
52
55
|
subagentOnlyExtensions: step.subagentOnlyExtensions,
|
|
56
|
+
requiredExtensions: step.requiredExtensions,
|
|
53
57
|
fast: step.fast,
|
|
54
|
-
modelCandidates: step.modelCandidates,
|
|
55
58
|
systemPrompt: acceptancePrompt ? `${step.systemPrompt ?? ""}\n${acceptancePrompt}` : step.systemPrompt ?? "",
|
|
56
59
|
systemPromptMode: step.systemPromptMode,
|
|
57
60
|
mcpDirectTools: step.mcpDirectTools,
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import * as path from "node:path";
|
|
12
12
|
import { pathToFileURL } from "node:url";
|
|
13
|
-
import {
|
|
13
|
+
import { createPlacementChildSessionFactory, type ChildSessionFactory, type DefaultChildSessionFactoryOptions } from "../shared/child-session.ts";
|
|
14
14
|
|
|
15
15
|
export interface RunnerChildSessionConfig {
|
|
16
16
|
/** Test seam: module whose default export is a `ChildSessionFactory`, or a function returning one. */
|
|
@@ -22,7 +22,7 @@ function isChildSessionFactory(value: unknown): value is ChildSessionFactory {
|
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
export async function loadRunnerChildSessionFactory(config: RunnerChildSessionConfig, options?: DefaultChildSessionFactoryOptions): Promise<ChildSessionFactory> {
|
|
25
|
-
if (!config.childSessionFactoryModule) return
|
|
25
|
+
if (!config.childSessionFactoryModule) return createPlacementChildSessionFactory(options);
|
|
26
26
|
const loaded = await import(pathToFileURL(path.resolve(config.childSessionFactoryModule)).href) as { default?: unknown };
|
|
27
27
|
const candidate = typeof loaded.default === "function" ? (loaded.default as () => unknown)() : loaded.default;
|
|
28
28
|
if (!isChildSessionFactory(candidate)) {
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { EventEmitter } from "node:events";
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import { createRequire } from "node:module";
|
|
4
|
+
import * as path from "node:path";
|
|
5
|
+
import { getConfigDirName } from "../../shared/utils.ts";
|
|
6
|
+
|
|
7
|
+
/** Pi's default undici header/body idle timeout. */
|
|
8
|
+
export const DEFAULT_HTTP_IDLE_TIMEOUT_MS = 300_000;
|
|
9
|
+
|
|
10
|
+
export interface HttpIdleTimeoutResolution {
|
|
11
|
+
timeoutMs: number;
|
|
12
|
+
source: "project" | "global" | "default";
|
|
13
|
+
warning?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Mirrors pi-coding-agent's `parseHttpIdleTimeoutMs`: numbers (or numeric
|
|
18
|
+
* strings) are floored, `"disabled"` means 0, anything else is invalid.
|
|
19
|
+
*/
|
|
20
|
+
export function parseHttpIdleTimeoutMs(value: unknown): number | undefined {
|
|
21
|
+
if (typeof value === "string") {
|
|
22
|
+
const trimmed = value.trim();
|
|
23
|
+
if (trimmed.toLowerCase() === "disabled") return 0;
|
|
24
|
+
if (trimmed.length === 0) return undefined;
|
|
25
|
+
return parseHttpIdleTimeoutMs(Number(trimmed));
|
|
26
|
+
}
|
|
27
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0) return undefined;
|
|
28
|
+
return Math.floor(value);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function readSetting(file: string): { present: boolean; value?: unknown; error?: string } {
|
|
32
|
+
let raw: string;
|
|
33
|
+
try {
|
|
34
|
+
raw = fs.readFileSync(file, "utf8");
|
|
35
|
+
} catch (error) {
|
|
36
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return { present: false };
|
|
37
|
+
return { present: false, error: `cannot read ${file}: ${error instanceof Error ? error.message : String(error)}` };
|
|
38
|
+
}
|
|
39
|
+
try {
|
|
40
|
+
const parsed = JSON.parse(raw) as unknown;
|
|
41
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return { present: false };
|
|
42
|
+
if (!Object.hasOwn(parsed, "httpIdleTimeoutMs")) return { present: false };
|
|
43
|
+
return { present: true, value: (parsed as Record<string, unknown>).httpIdleTimeoutMs };
|
|
44
|
+
} catch (error) {
|
|
45
|
+
return { present: false, error: `cannot parse ${file}: ${error instanceof Error ? error.message : String(error)}` };
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Resolves Pi's `httpIdleTimeoutMs` the way the host does: the project
|
|
51
|
+
* `.pi/settings.json` overrides the global `<agentDir>/settings.json`, and an
|
|
52
|
+
* unset value falls back to 300 000 ms. Detached runners install their own
|
|
53
|
+
* undici dispatcher before pi-coding-agent is loaded, so they cannot borrow
|
|
54
|
+
* Pi's SettingsManager and must read the setting themselves.
|
|
55
|
+
*/
|
|
56
|
+
export function resolveHttpIdleTimeoutMs(options: { agentDir: string; cwd: string }): HttpIdleTimeoutResolution {
|
|
57
|
+
const candidates: Array<{ source: "project" | "global"; file: string }> = [
|
|
58
|
+
{ source: "project", file: path.join(options.cwd, getConfigDirName(), "settings.json") },
|
|
59
|
+
{ source: "global", file: path.join(options.agentDir, "settings.json") },
|
|
60
|
+
];
|
|
61
|
+
const warnings: string[] = [];
|
|
62
|
+
for (const candidate of candidates) {
|
|
63
|
+
const read = readSetting(candidate.file);
|
|
64
|
+
if (read.error) {
|
|
65
|
+
warnings.push(read.error);
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
if (!read.present) continue;
|
|
69
|
+
const timeoutMs = parseHttpIdleTimeoutMs(read.value);
|
|
70
|
+
if (timeoutMs === undefined) {
|
|
71
|
+
// Pi rejects the merged value outright; a malformed override must not
|
|
72
|
+
// silently fall through to a different scope's setting.
|
|
73
|
+
warnings.push(`invalid httpIdleTimeoutMs in ${candidate.file}: ${String(read.value)}`);
|
|
74
|
+
return { timeoutMs: DEFAULT_HTTP_IDLE_TIMEOUT_MS, source: "default", warning: warnings.join("; ") };
|
|
75
|
+
}
|
|
76
|
+
return { timeoutMs, source: candidate.source, ...(warnings.length ? { warning: warnings.join("; ") } : {}) };
|
|
77
|
+
}
|
|
78
|
+
return { timeoutMs: DEFAULT_HTTP_IDLE_TIMEOUT_MS, source: "default", ...(warnings.length ? { warning: warnings.join("; ") } : {}) };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Dispatcher options shared by the runner and its regression test. */
|
|
82
|
+
export function runnerHttpDispatcherOptions(timeoutMs: number): {
|
|
83
|
+
allowH2: false;
|
|
84
|
+
proxyTunnel: true;
|
|
85
|
+
headersTimeout: number;
|
|
86
|
+
bodyTimeout: number;
|
|
87
|
+
} {
|
|
88
|
+
return {
|
|
89
|
+
allowH2: false,
|
|
90
|
+
// Keep HTTP origins on CONNECT tunnels, matching Pi's dispatcher.
|
|
91
|
+
proxyTunnel: true,
|
|
92
|
+
headersTimeout: timeoutMs,
|
|
93
|
+
bodyTimeout: timeoutMs,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Installs the runner's proxy-aware undici dispatcher as the process global and
|
|
99
|
+
* routes global fetch through it, with header/body idle clocks taken from Pi's
|
|
100
|
+
* `httpIdleTimeoutMs`. Detached runners skip Pi's CLI dispatcher setup: the Node
|
|
101
|
+
* entrypoint never runs it, and the binary bootstrap runs inside the extension
|
|
102
|
+
* factory, before Pi applies the setting to its own dispatcher. Best effort: a
|
|
103
|
+
* failure logs and leaves the existing dispatcher in place.
|
|
104
|
+
*/
|
|
105
|
+
export function installRunnerHttpDispatcher(options: { agentDir: string; cwd: string }): void {
|
|
106
|
+
try {
|
|
107
|
+
// SAFETY: require loads the pinned direct dependency described by these types.
|
|
108
|
+
const undici = createRequire(import.meta.url)("undici") as typeof import("undici");
|
|
109
|
+
const idle = resolveHttpIdleTimeoutMs(options);
|
|
110
|
+
if (idle.warning) console.error(`[pi-subagents] httpIdleTimeoutMs: ${idle.warning}; using ${idle.timeoutMs}ms`);
|
|
111
|
+
const dispatcher = new undici.EnvHttpProxyAgent(runnerHttpDispatcherOptions(idle.timeoutMs));
|
|
112
|
+
// Fetch rejects stream errors; the listener prevents an unhandled EventEmitter error.
|
|
113
|
+
EventEmitter.prototype.on.call(dispatcher, "error", () => {});
|
|
114
|
+
undici.setGlobalDispatcher(dispatcher);
|
|
115
|
+
undici.install();
|
|
116
|
+
} catch (error) {
|
|
117
|
+
console.error(`[pi-subagents] proxy-aware HTTP dispatcher not installed: ${error instanceof Error ? error.message : String(error)}`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
@@ -15,6 +15,7 @@ import { previewSimpleWorkflowRun } from "../../workflows/scripted-workflow.ts";
|
|
|
15
15
|
import { resolveGitRepositoryIdentity } from "../../workflows/chat-progress.ts";
|
|
16
16
|
import { getConfigDirName } from "../../shared/utils.ts";
|
|
17
17
|
import { normalizeWorktreeBaseRef } from "../shared/worktree.ts";
|
|
18
|
+
import { deepFreezeWorkflowArgs, normalizeWorkflowArgs } from "../../workflows/workflow-resources.ts";
|
|
18
19
|
|
|
19
20
|
export const SCHEDULED_RUN_ACTIONS = [
|
|
20
21
|
"schedule.create",
|
|
@@ -40,7 +41,7 @@ export type ScheduleRunState = "running" | "skipped" | "missed" | "completed" |
|
|
|
40
41
|
export type ScheduleTrigger =
|
|
41
42
|
| { kind: "once"; at: string; nextRunAt?: string }
|
|
42
43
|
| { kind: "interval"; every: string; everyMs: number; anchorAt: string; nextRunAt: string };
|
|
43
|
-
export type ScheduleTarget = { workflowScript: string; baseRef?: string };
|
|
44
|
+
export type ScheduleTarget = { workflowScript: string; args: Record<string, unknown>; baseRef?: string };
|
|
44
45
|
|
|
45
46
|
export interface ScheduleRecord {
|
|
46
47
|
schemaVersion: 1;
|
|
@@ -283,14 +284,17 @@ function readJson(file: string, label: string): unknown {
|
|
|
283
284
|
|
|
284
285
|
function parseScheduleTarget(value: unknown, file: string): ScheduleTarget {
|
|
285
286
|
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`Schedule record '${file}' has invalid trigger or target.`);
|
|
286
|
-
const target = value as { workflowScript?: unknown; baseRef?: unknown; agent?: unknown; task?: unknown };
|
|
287
|
+
const target = value as { workflowScript?: unknown; args?: unknown; baseRef?: unknown; agent?: unknown; task?: unknown };
|
|
287
288
|
if (typeof target.workflowScript === "string" && target.workflowScript.trim()) {
|
|
289
|
+
let baseRef: string | undefined;
|
|
288
290
|
try {
|
|
289
|
-
|
|
290
|
-
return { workflowScript: target.workflowScript.trim(), ...(baseRef === undefined ? {} : { baseRef }) };
|
|
291
|
+
baseRef = normalizeWorktreeBaseRef(target.baseRef);
|
|
291
292
|
} catch (error) {
|
|
292
293
|
throw new Error(`Schedule record '${file}' has an invalid baseRef: ${error instanceof Error ? error.message : String(error)}`);
|
|
293
294
|
}
|
|
295
|
+
const normalizedArgs = normalizeWorkflowArgs(target.args);
|
|
296
|
+
if ("error" in normalizedArgs) throw new Error(`Schedule record '${file}' has invalid args: ${normalizedArgs.error}`);
|
|
297
|
+
return { workflowScript: target.workflowScript.trim(), args: deepFreezeWorkflowArgs(normalizedArgs.args), ...(baseRef === undefined ? {} : { baseRef }) };
|
|
294
298
|
}
|
|
295
299
|
if (target.agent !== undefined || target.task !== undefined) throw new Error(`Schedule record '${file}' uses a removed legacy agent target; recreate it with target.workflowScript.`);
|
|
296
300
|
throw new Error(`Schedule record '${file}' requires a workflowScript target.`);
|
|
@@ -447,7 +451,9 @@ function sanitizeTarget(params: SubagentParamsLike): { target?: ScheduleTarget;
|
|
|
447
451
|
}
|
|
448
452
|
const acceptanceErrors = validateExecutionAcceptance(params as Parameters<typeof validateExecutionAcceptance>[0]);
|
|
449
453
|
if (acceptanceErrors.length) return { error: acceptanceErrors.join(" ") };
|
|
450
|
-
|
|
454
|
+
const normalizedArgs = normalizeWorkflowArgs(params.args);
|
|
455
|
+
if ("error" in normalizedArgs) return { error: normalizedArgs.error };
|
|
456
|
+
return { target: { workflowScript: params.workflowScript.trim(), args: deepFreezeWorkflowArgs(normalizedArgs.args), ...(baseRef === undefined ? {} : { baseRef }) } };
|
|
451
457
|
}
|
|
452
458
|
|
|
453
459
|
function executionParams(schedule: ScheduleRecord, quiet = false): SubagentParamsLike {
|
|
@@ -99,13 +99,13 @@ interface ResultChildOutcome {
|
|
|
99
99
|
sessionFile?: string;
|
|
100
100
|
model?: string;
|
|
101
101
|
thinking?: string;
|
|
102
|
-
|
|
103
|
-
modelAttempts?: NonNullable<AsyncStatus["steps"]>[number]["modelAttempts"];
|
|
102
|
+
requestedModel?: string;
|
|
104
103
|
contextOverflow?: boolean;
|
|
105
104
|
}
|
|
106
105
|
|
|
107
106
|
interface ResultRepairData {
|
|
108
107
|
state: "complete" | "failed" | "partial" | "paused" | "stopped" | "rejected";
|
|
108
|
+
error?: string;
|
|
109
109
|
results?: ResultChildOutcome[];
|
|
110
110
|
}
|
|
111
111
|
|
|
@@ -120,7 +120,8 @@ function regularFileExists(filePath: string): boolean {
|
|
|
120
120
|
|
|
121
121
|
function readResultRepairData(resultPath: string): ResultRepairData | undefined {
|
|
122
122
|
try {
|
|
123
|
-
const data = JSON.parse(fs.readFileSync(resultPath, "utf-8")) as { success?: boolean; state?: string; exitCode?: number; results?: unknown };
|
|
123
|
+
const data = JSON.parse(fs.readFileSync(resultPath, "utf-8")) as { success?: boolean; state?: string; exitCode?: number; error?: unknown; results?: unknown };
|
|
124
|
+
if (data.error !== undefined && typeof data.error !== "string") throw new Error(`Invalid async result file '${resultPath}': error must be a string.`);
|
|
124
125
|
const state = data.success ? "complete" : data.state === "stopped" ? "stopped" : data.state === "rejected" ? "rejected" : data.state === "partial" ? "partial" : data.state === "paused" || data.exitCode === 0 ? "paused" : "failed";
|
|
125
126
|
const results = Array.isArray(data.results)
|
|
126
127
|
? data.results.map((entry, index) => {
|
|
@@ -131,7 +132,7 @@ function readResultRepairData(resultPath: string): ResultRepairData | undefined
|
|
|
131
132
|
return child;
|
|
132
133
|
})
|
|
133
134
|
: undefined;
|
|
134
|
-
return { state, ...(results ? { results } : {}) };
|
|
135
|
+
return { state, ...(data.error ? { error: data.error } : {}), ...(results ? { results } : {}) };
|
|
135
136
|
} catch (error) {
|
|
136
137
|
if (isNotFoundError(error)) return undefined;
|
|
137
138
|
throw new Error(`Failed to read async result file '${resultPath}': ${getErrorMessage(error)}`, {
|
|
@@ -146,9 +147,16 @@ function childState(overallState: ResultRepairData["state"], child: ResultChildO
|
|
|
146
147
|
return overallState;
|
|
147
148
|
}
|
|
148
149
|
|
|
150
|
+
function failureDiagnostic(value: unknown): string | undefined {
|
|
151
|
+
return typeof value === "string" && value.trim() ? value : undefined;
|
|
152
|
+
}
|
|
153
|
+
|
|
149
154
|
function terminalStatusFromResult(status: AsyncStatus, resultPath: string, now: number): AsyncStatus | undefined {
|
|
150
155
|
const repair = readResultRepairData(resultPath);
|
|
151
156
|
if (!repair) return undefined;
|
|
157
|
+
const rootError = failureDiagnostic(status.error)
|
|
158
|
+
?? failureDiagnostic(repair.error)
|
|
159
|
+
?? repair.results?.map((child) => child.success === false ? failureDiagnostic(child.error) : undefined).find(Boolean);
|
|
152
160
|
const steps = (status.steps ?? []).map((step, index) => {
|
|
153
161
|
if (step.status !== "running" && step.status !== "pending") return step;
|
|
154
162
|
const child = repair.results?.[index];
|
|
@@ -167,14 +175,14 @@ function terminalStatusFromResult(status: AsyncStatus, resultPath: string, now:
|
|
|
167
175
|
sessionFile: step.sessionFile ?? child?.sessionFile,
|
|
168
176
|
model,
|
|
169
177
|
thinking,
|
|
170
|
-
|
|
171
|
-
modelAttempts: child?.modelAttempts ?? step.modelAttempts,
|
|
178
|
+
requestedModel: child?.requestedModel ?? step.requestedModel,
|
|
172
179
|
contextOverflow: child?.contextOverflow ?? step.contextOverflow,
|
|
173
180
|
};
|
|
174
181
|
});
|
|
175
182
|
const terminalStatus: AsyncStatus = {
|
|
176
183
|
...status,
|
|
177
184
|
state: repair.state,
|
|
185
|
+
...(rootError && (repair.state === "failed" || repair.state === "partial") ? { error: rootError } : {}),
|
|
178
186
|
...(status.lifecycleArtifactVersion === 3 && (!status.processTerminal || status.processTerminal.state === "pending") ? {
|
|
179
187
|
processTerminal: { version: 1 as const, state: "unknown" as const, runId: status.runId, runnerProcessInstanceId: "observer-unavailable", reason: "observer-unavailable" as const },
|
|
180
188
|
} : {}),
|
|
@@ -264,8 +272,7 @@ function buildFailedRepair(status: AsyncStatus, asyncDir: string, now: number, r
|
|
|
264
272
|
error: step.status === "complete" || step.status === "completed" ? undefined : step.error ?? message,
|
|
265
273
|
success: step.status === "complete" || step.status === "completed",
|
|
266
274
|
model: step.model,
|
|
267
|
-
|
|
268
|
-
modelAttempts: step.modelAttempts,
|
|
275
|
+
requestedModel: step.requestedModel,
|
|
269
276
|
contextOverflow: step.contextOverflow,
|
|
270
277
|
sessionFile: step.sessionFile,
|
|
271
278
|
})),
|
|
@@ -378,9 +385,26 @@ export function reconcileAsyncRun(asyncDir: string, options: ReconcileAsyncRunOp
|
|
|
378
385
|
? terminalStatusFromResult(effectiveStatus, existingResultPath, now)
|
|
379
386
|
: undefined;
|
|
380
387
|
if (terminalStatus) {
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
388
|
+
const currentStatus = readStatus(asyncDir);
|
|
389
|
+
const currentRootError = failureDiagnostic(currentStatus?.error);
|
|
390
|
+
const currentTerminal = currentStatus?.processTerminal?.state === "observed" || currentStatus?.processTerminal?.state === "unknown"
|
|
391
|
+
? currentStatus.processTerminal
|
|
392
|
+
: undefined;
|
|
393
|
+
const steps = terminalStatus.steps?.map((step, index) => {
|
|
394
|
+
const currentStepTerminal = currentStatus?.steps?.[index]?.processTerminal;
|
|
395
|
+
return currentStepTerminal?.state === "observed" || currentStepTerminal?.state === "unknown"
|
|
396
|
+
? { ...step, processTerminal: currentStepTerminal }
|
|
397
|
+
: step;
|
|
398
|
+
});
|
|
399
|
+
const statusToWrite: AsyncStatus = {
|
|
400
|
+
...terminalStatus,
|
|
401
|
+
...(currentRootError ? { error: currentRootError } : {}),
|
|
402
|
+
...(currentTerminal ? { processTerminal: currentTerminal } : {}),
|
|
403
|
+
...(steps ? { steps } : {}),
|
|
404
|
+
};
|
|
405
|
+
writeAtomicJson(statusPath, statusToWrite);
|
|
406
|
+
updateActiveRunIndex(asyncDir, statusToWrite.state, statusToWrite.toolCallId);
|
|
407
|
+
return { status: statusToWrite, repaired: true, resultPath: existingResultPath, message: "Existing async result file was used to repair stale running status." };
|
|
384
408
|
}
|
|
385
409
|
if (effectiveStatus.displayDismissedAt === undefined) return { status: effectiveStatus, repaired: false, resultPath: existingResultPath };
|
|
386
410
|
}
|