pi-subagents 0.67.0 → 0.68.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 +70 -0
- package/README.md +1 -1
- package/docs/agents.md +37 -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 +15 -12
- package/docs/watchdog.md +10 -12
- package/docs/workflows.md +11 -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 +4 -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 +184 -71
- package/src/agents/chain-serializer.ts +5 -0
- package/src/agents/runtime-agent-registry.ts +7 -6
- 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 +16 -5
- package/src/extension/tool-description.ts +8 -7
- 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 +396 -275
- 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 +91 -218
- package/src/runs/foreground/foreground-history.ts +2 -1
- package/src/runs/foreground/subagent-executor.ts +266 -80
- package/src/runs/shared/acceptance.ts +34 -10
- 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 +3 -3
- 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 +70 -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 +42 -3
- 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
|
@@ -52,11 +52,14 @@ const DEFAULT_LIVENESS_INTERVAL_MS = 5000;
|
|
|
52
52
|
const EVENT_REFRESH_DEBOUNCE_MS = 25;
|
|
53
53
|
const WATCH_ATTACHMENT_RETRY_MS = 100;
|
|
54
54
|
|
|
55
|
+
const isTerminalJobStatus = (status: AsyncJobState["status"]): boolean =>
|
|
56
|
+
status === "complete" || status === "failed" || status === "partial" || status === "paused" || status === "rejected" || status === "stopped";
|
|
57
|
+
|
|
55
58
|
function rememberFleetJob(state: SubagentState, job: AsyncJobState): void {
|
|
56
59
|
state.fleetJobs ??= new Map();
|
|
57
60
|
state.fleetJobs.set(job.asyncId, job);
|
|
58
61
|
const terminal = [...state.fleetJobs.values()]
|
|
59
|
-
.filter((candidate) => candidate.status
|
|
62
|
+
.filter((candidate) => isTerminalJobStatus(candidate.status))
|
|
60
63
|
.sort((left, right) => (right.updatedAt ?? right.startedAt ?? 0) - (left.updatedAt ?? left.startedAt ?? 0));
|
|
61
64
|
for (const stale of terminal.slice(MAX_RECENT_FLEET_JOBS)) state.fleetJobs.delete(stale.asyncId);
|
|
62
65
|
}
|
|
@@ -100,7 +103,6 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
100
103
|
let nextWidgetAnimationAt = Date.now() + WIDGET_ANIMATION_INTERVAL_MS;
|
|
101
104
|
const watch = options.watch ?? fs.watch;
|
|
102
105
|
const useNativeWatcher = () => shouldUseNativeFsWatch("async-job-tracker", options.platform);
|
|
103
|
-
const terminalStatus = (status: string) => status === "complete" || status === "failed" || status === "paused" || status === "stopped";
|
|
104
106
|
const withLastUiContext = <T>(run: (ctx: ExtensionContext) => T): T | undefined => {
|
|
105
107
|
const cached = state.lastUiContext;
|
|
106
108
|
return withCachedUiContext(cached, () => {
|
|
@@ -445,10 +447,10 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
445
447
|
if (status) {
|
|
446
448
|
const previousStatus = job.status;
|
|
447
449
|
job.status = status.state;
|
|
448
|
-
if (!
|
|
450
|
+
if (!isTerminalJobStatus(job.status)) terminalPublications.delete(job.asyncId);
|
|
449
451
|
if (job.status === "running") runningJobIds.add(job.asyncId);
|
|
450
452
|
else runningJobIds.delete(job.asyncId);
|
|
451
|
-
if (job.status
|
|
453
|
+
if (!isTerminalJobStatus(job.status)) cancelCleanup(job.asyncId);
|
|
452
454
|
job.sessionId = status.sessionId ?? job.sessionId;
|
|
453
455
|
job.activityState = status.activityState;
|
|
454
456
|
job.lastActivityAt = status.lastActivityAt ?? job.lastActivityAt;
|
|
@@ -502,7 +504,7 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
502
504
|
job.turnBudgetExceeded = status.turnBudgetExceeded ?? job.turnBudgetExceeded;
|
|
503
505
|
job.wrapUpRequested = status.wrapUpRequested ?? job.wrapUpRequested;
|
|
504
506
|
job.sessionFile = status.sessionFile ?? job.sessionFile;
|
|
505
|
-
if (
|
|
507
|
+
if (isTerminalJobStatus(job.status)) {
|
|
506
508
|
let publication = terminalPublications.get(job.asyncId);
|
|
507
509
|
if (!publication && status.mode !== "workflow" && status.processTerminal?.state === "pending"
|
|
508
510
|
&& status.runId === job.asyncId && status.sessionId === state.currentSessionId
|
|
@@ -524,7 +526,7 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
524
526
|
} else cancelCleanup(job.asyncId);
|
|
525
527
|
}
|
|
526
528
|
// Scan on close too: publication may have raced the payload check.
|
|
527
|
-
if (!
|
|
529
|
+
if (!isTerminalJobStatus(previousStatus) || (wasPending && !publication?.pending)) options.onJobTerminal?.();
|
|
528
530
|
rememberFleetJob(state, job);
|
|
529
531
|
if (!publication?.pending && !nestedRefreshFailed && !hasLiveNestedDescendants(job.nestedChildren) && (previousStatus !== job.status || !state.cleanupTimers.has(job.asyncId))) {
|
|
530
532
|
scheduleCleanup(job.asyncId);
|
|
@@ -3,6 +3,7 @@ import * as path from "node:path";
|
|
|
3
3
|
import { DIRS, type AcceptanceInput, type AsyncStatus, type SteeringRecoveryDescriptor, type SubagentRunMode } from "../../shared/types.ts";
|
|
4
4
|
import type { AgentConfig } from "../../agents/agents.ts";
|
|
5
5
|
import { normalizeExtensionBindings } from "../shared/extension-bindings.ts";
|
|
6
|
+
import { snapshotRequiredChildExtensions } from "../../shared/required-child-extensions.ts";
|
|
6
7
|
import { normalizeWorkflowLaneMetadata } from "../shared/lane-metadata.ts";
|
|
7
8
|
import { validateAcceptanceInput } from "../shared/acceptance.ts";
|
|
8
9
|
import { validateToolBudgetConfig } from "../shared/tool-budget.ts";
|
|
@@ -319,12 +320,13 @@ export function readAsyncRecoveryDescriptor(asyncDir: string | undefined): Steer
|
|
|
319
320
|
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': expected an object.`);
|
|
320
321
|
const parsed = value as Record<string, unknown>;
|
|
321
322
|
const allowedFields = new Set([
|
|
322
|
-
"modelResponseAliases", "version", "launchContractDigest", "sourceRunId", "agentContract", "agent", "sessionFile", "cwd", "model", "modelProvider", "modelOverrideFromParent", "modelOrigin", "
|
|
323
|
+
"modelResponseAliases", "version", "launchContractDigest", "sourceRunId", "agentContract", "agent", "sessionFile", "cwd", "model", "modelProvider", "modelOverrideFromParent", "modelOrigin", "fast", "thinking", "thinkingCeiling", "tools", "allowNestedSubagents", "extensions",
|
|
323
324
|
"subagentOnlyExtensions", "mcpDirectTools", "excludeTools", "mutationTools", "systemPrompt", "systemPromptMode", "inheritProjectContext", "inheritGlobalContext", "inheritSkills", "skills",
|
|
324
325
|
"skillPath", "agentFilePath", "completionGuard", "memory", "outputPath", "outputMode", "structuredOutputSchema", "acceptance", "sessionDir", "artifactConfig",
|
|
325
326
|
"artifactsDir", "maxOutput", "controlConfig", "context", "intercomBridge", "absoluteDeadlineAt", "initialTurnBudget", "initialToolBudget", "maxSubagentDepth", "share", "capabilityCeiling",
|
|
326
327
|
"launchResolvedExtensions", "runFanoutBudget", "lane", "baseRef",
|
|
327
328
|
"extensionBindings",
|
|
329
|
+
"requiredExtensions",
|
|
328
330
|
]);
|
|
329
331
|
for (const field of Object.keys(parsed)) {
|
|
330
332
|
if (!allowedFields.has(field)) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': unknown field '${field}'.`);
|
|
@@ -343,6 +345,10 @@ export function readAsyncRecoveryDescriptor(asyncDir: string | undefined): Steer
|
|
|
343
345
|
if (parsed.capabilityCeiling !== undefined) parsed.capabilityCeiling = parseSubagentCapabilityCeiling(parsed.capabilityCeiling, `async recovery descriptor '${descriptorPath}' capabilityCeiling`);
|
|
344
346
|
if (parsed.thinkingCeiling !== undefined) parsed.thinkingCeiling = parseThinkingLevel(parsed.thinkingCeiling, `async recovery descriptor '${descriptorPath}' thinkingCeiling`);
|
|
345
347
|
if (parsed.extensionBindings !== undefined) parsed.extensionBindings = normalizeExtensionBindings(parsed.extensionBindings)!.value;
|
|
348
|
+
if (parsed.requiredExtensions !== undefined) {
|
|
349
|
+
try { parsed.requiredExtensions = snapshotRequiredChildExtensions(parsed.requiredExtensions, "requiredExtensions"); }
|
|
350
|
+
catch (error) { throw new Error(`Invalid async recovery descriptor '${descriptorPath}': ${error instanceof Error ? error.message : String(error)}`); }
|
|
351
|
+
}
|
|
346
352
|
if (parsed.lane !== undefined) parsed.lane = normalizeWorkflowLaneMetadata(parsed.lane, `Invalid async recovery descriptor '${descriptorPath}': lane`);
|
|
347
353
|
if (parsed.agentContract !== undefined) {
|
|
348
354
|
if (!parsed.agentContract || typeof parsed.agentContract !== "object" || Array.isArray(parsed.agentContract)) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': agentContract must be an object.`);
|
|
@@ -363,7 +369,7 @@ export function readAsyncRecoveryDescriptor(asyncDir: string | undefined): Steer
|
|
|
363
369
|
else if (typeof parsed.inheritGlobalContext !== "boolean") throw new Error(`Invalid async recovery descriptor '${descriptorPath}': inheritGlobalContext must be a boolean.`);
|
|
364
370
|
if (parsed.allowNestedSubagents !== undefined && typeof parsed.allowNestedSubagents !== "boolean") throw new Error(`Invalid async recovery descriptor '${descriptorPath}': allowNestedSubagents must be a boolean.`);
|
|
365
371
|
if (!Number.isInteger(parsed.maxSubagentDepth) || (parsed.maxSubagentDepth as number) < 0) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': maxSubagentDepth must be a non-negative integer.`);
|
|
366
|
-
for (const field of ["
|
|
372
|
+
for (const field of ["tools", "excludeTools", "extensions", "subagentOnlyExtensions", "mcpDirectTools", "mutationTools", "skills", "skillPath"] as const) {
|
|
367
373
|
const item = parsed[field];
|
|
368
374
|
if (item !== undefined && (!Array.isArray(item) || item.some((entry) => typeof entry !== "string" || !entry.trim()))) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': ${field} must contain non-empty strings.`);
|
|
369
375
|
}
|
|
@@ -551,7 +557,11 @@ export function resolveAsyncResumeTarget(params: AsyncResumeParams, deps: AsyncR
|
|
|
551
557
|
const index = requestedIndex ?? 0;
|
|
552
558
|
if (!Number.isInteger(index)) throw new Error(`Async run '${runId}' index must be an integer.`);
|
|
553
559
|
if (index < 0 || index >= stepCount) throw new Error(`Async run '${runId}' has ${stepCount} children. Index ${index} is out of range.`);
|
|
554
|
-
const
|
|
560
|
+
const selectedStatusStep = statusSteps[index];
|
|
561
|
+
if (selectedStatusStep?.status === "stopped" || selectedStatusStep?.stopped === true) {
|
|
562
|
+
throw new Error(`Async run '${runId}' child ${index} was stopped and cannot be resumed. Start a new run instead.`);
|
|
563
|
+
}
|
|
564
|
+
const agent = selectedStatusStep?.agent ?? resultSteps[index]?.agent ?? result?.agent;
|
|
555
565
|
if (!agent) throw new Error(`Could not determine child agent for async run '${runId}'.`);
|
|
556
566
|
if (recoveryDescriptor && recoveryDescriptor.agent !== agent) throw new Error(`Async run '${runId}' has a recovery descriptor for '${recoveryDescriptor.agent}', not '${agent}'.`);
|
|
557
567
|
const sessionFile = statusSteps[index]?.sessionFile
|
|
@@ -596,7 +606,6 @@ export function applySteeringRecoveryAgentConfig(agentConfig: AgentConfig, descr
|
|
|
596
606
|
...agentConfig,
|
|
597
607
|
model: descriptor.model,
|
|
598
608
|
modelProvider: descriptor.modelProvider,
|
|
599
|
-
fallbackModels: descriptor.fallbackModels ? [...descriptor.fallbackModels] : undefined,
|
|
600
609
|
thinking: descriptor.thinking,
|
|
601
610
|
maxThinking: intersectThinkingCeilings(descriptor.thinkingCeiling, agentConfig.maxThinking),
|
|
602
611
|
tools: descriptor.tools ? [...descriptor.tools] : undefined,
|
|
@@ -26,6 +26,7 @@ import { workflowGraphStageNodes } from "../shared/workflow-graph.ts";
|
|
|
26
26
|
import { formatTimeoutRecoveryLines, projectTimeoutRecovery } from "../shared/mutation-evidence.ts";
|
|
27
27
|
import { formatWorkflowChecklistText, projectWorkflowChecklist } from "../../workflows/workflow-checklist.ts";
|
|
28
28
|
import type { RawDrainStatusObserver } from "../shared/readonly-drain-observation.ts";
|
|
29
|
+
import { childWatchdogProgressForModel } from "../../watchdog/child-status.ts";
|
|
29
30
|
|
|
30
31
|
interface AsyncRunStepSummary {
|
|
31
32
|
index: number;
|
|
@@ -66,7 +67,7 @@ interface AsyncRunStepSummary {
|
|
|
66
67
|
model?: string;
|
|
67
68
|
contextLimit?: number;
|
|
68
69
|
thinking?: string;
|
|
69
|
-
|
|
70
|
+
requestedModel?: string;
|
|
70
71
|
sessionFile?: string;
|
|
71
72
|
transcriptPath?: string;
|
|
72
73
|
error?: string;
|
|
@@ -367,7 +368,7 @@ function statusToSummary(asyncDir: string, status: AsyncStatus & { cwd?: string
|
|
|
367
368
|
...(step.contextLimit !== undefined ? { contextLimit: step.contextLimit } : {}),
|
|
368
369
|
...(step.thinking ? { thinking: step.thinking } : {}),
|
|
369
370
|
...(step.thinkingCeiling ? { thinkingCeiling: step.thinkingCeiling } : {}),
|
|
370
|
-
...(step.
|
|
371
|
+
...(step.requestedModel ? { requestedModel: step.requestedModel } : {}),
|
|
371
372
|
...(step.sessionFile ? { sessionFile: step.sessionFile } : {}),
|
|
372
373
|
...(step.transcriptPath ? { transcriptPath: step.transcriptPath } : {}),
|
|
373
374
|
...(step.error ? { error: step.error } : {}),
|
|
@@ -387,7 +388,7 @@ function statusToSummary(asyncDir: string, status: AsyncStatus & { cwd?: string
|
|
|
387
388
|
...(step.execution ? { execution: step.execution } : {}),
|
|
388
389
|
...(step.review ? { review: step.review } : {}),
|
|
389
390
|
...(step.effects ? { effects: step.effects } : {}),
|
|
390
|
-
...(step.watchdog ? { watchdog: step.watchdog } : {}),
|
|
391
|
+
...(step.watchdog ? { watchdog: childWatchdogProgressForModel(step.watchdog) } : {}),
|
|
391
392
|
...(step.processTerminal ? { processTerminal: sanitizeProcessTerminal(step.processTerminal, { runId: status.runId, runnerProcessInstanceId: step.processTerminal.runnerProcessInstanceId }, `${path.join(asyncDir, "status.json")} step ${index}`) } : {}),
|
|
392
393
|
...(timeoutRecovery ? { timeoutRecovery } : {}),
|
|
393
394
|
...(step.capabilityCeiling ? { capabilityCeiling: step.capabilityCeiling } : {}),
|
|
@@ -545,6 +546,14 @@ export function listAsyncRuns(asyncDirRoot: string, options: AsyncRunListOptions
|
|
|
545
546
|
const asyncDir = path.join(asyncDirRoot, entry);
|
|
546
547
|
let status: (AsyncStatus & { cwd?: string }) | null;
|
|
547
548
|
try {
|
|
549
|
+
// Reconciliation can rewrite state; session-scoped discovery does not own foreign runs.
|
|
550
|
+
if (options.sessionId !== undefined) {
|
|
551
|
+
const stored = readStatus(asyncDir);
|
|
552
|
+
if (stored && stored.sessionId !== options.sessionId) {
|
|
553
|
+
observeStatus?.(stored);
|
|
554
|
+
continue;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
548
557
|
const reconciliation = options.reconcile === false
|
|
549
558
|
? undefined
|
|
550
559
|
: reconcileAsyncRun(asyncDir, { resultsDir: options.resultsDir, kill: options.kill, now: options.now }, observeStatus);
|
|
@@ -563,7 +572,9 @@ export function listAsyncRuns(asyncDirRoot: string, options: AsyncRunListOptions
|
|
|
563
572
|
}
|
|
564
573
|
if (activeEntries.has(entry) && !isActiveAsyncState(status.state)) {
|
|
565
574
|
const processTerminal = readProcessTerminal(asyncDir, { runId: status.runId, runnerProcessInstanceId: status.processTerminal?.runnerProcessInstanceId });
|
|
566
|
-
if (processTerminal?.state === "observed" || (activeRunMarkerAgeMs(asyncDir, options.now?.()) ?? 0) > DEFAULT_STALE_TERMINAL_ACTIVE_MARKER_MS)
|
|
575
|
+
if (processTerminal?.state === "observed" || (activeRunMarkerAgeMs(asyncDir, options.now?.()) ?? 0) > DEFAULT_STALE_TERMINAL_ACTIVE_MARKER_MS) {
|
|
576
|
+
updateActiveRunIndex(asyncDir, status.state, status.toolCallId, { terminalIndexBeforeRelease: true });
|
|
577
|
+
}
|
|
567
578
|
}
|
|
568
579
|
if (status.displayDismissedAt !== undefined) continue;
|
|
569
580
|
// Filter before the nested-route lookup: the lookup builds an index over
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import type { AgentToolResult } from "@earendil-works/pi-agent-core";
|
|
2
2
|
import { snapshotBackgroundWork } from "../../api/background-work.ts";
|
|
3
|
-
import {
|
|
3
|
+
import type { Details, SubagentState } from "../../shared/types.ts";
|
|
4
4
|
import { listAsyncRuns } from "./async-status.ts";
|
|
5
5
|
import type { ReadonlyDrainObservation } from "../shared/readonly-drain-observation.ts";
|
|
6
|
-
import { waitForSubagents, type SubagentWaitDeps, type SubagentWaitParams, type WaitEventBus } from "./subagent-wait.ts";
|
|
6
|
+
import { waitForSubagents, waitRunScopes, type SubagentWaitDeps, type SubagentWaitParams, type WaitEventBus } from "./subagent-wait.ts";
|
|
7
7
|
|
|
8
8
|
export const DEFAULT_AUTO_DRAIN_TIMEOUT_MS = 30 * 60 * 1000;
|
|
9
9
|
|
|
10
|
-
export interface AutoDrainDeps {
|
|
10
|
+
export interface AutoDrainDeps extends Pick<SubagentWaitDeps, "asyncDirRoot" | "resultsDir" | "nestedRootRunId"> {
|
|
11
11
|
state: SubagentState;
|
|
12
12
|
events?: WaitEventBus;
|
|
13
13
|
timeoutMs?: number;
|
|
@@ -18,20 +18,22 @@ export interface AutoDrainDeps {
|
|
|
18
18
|
deps: SubagentWaitDeps,
|
|
19
19
|
) => Promise<AgentToolResult<Details>>;
|
|
20
20
|
hasWork?: (sessionId: string, nowMs: number) => boolean;
|
|
21
|
+
hasPendingSupervisorRequest?: () => boolean;
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
function resultText(value: AgentToolResult<Details>): string {
|
|
24
25
|
return value.content.map((part) => part.type === "text" ? part.text : "").join(" ").trim();
|
|
25
26
|
}
|
|
26
27
|
|
|
27
|
-
function hasOutstandingWork(
|
|
28
|
-
const asyncRuns =
|
|
28
|
+
function hasOutstandingWork(deps: AutoDrainDeps, sessionId: string, nowMs: number, observation?: ReadonlyDrainObservation): boolean {
|
|
29
|
+
const asyncRuns = waitRunScopes(deps).flatMap(({ asyncDirRoot, resultsDir }) => listAsyncRuns(asyncDirRoot, {
|
|
29
30
|
states: ["queued", "running"],
|
|
30
31
|
sessionId,
|
|
31
|
-
resultsDir
|
|
32
|
+
resultsDir,
|
|
32
33
|
now: () => nowMs,
|
|
33
|
-
|
|
34
|
-
|
|
34
|
+
includeNested: false,
|
|
35
|
+
}, observation?.status));
|
|
36
|
+
const detachedForeground = [...(deps.state.foregroundRuns?.values() ?? [])].some((run) =>
|
|
35
37
|
run.sessionId === sessionId && run.children.some((child) => child.status === "detached")
|
|
36
38
|
);
|
|
37
39
|
return asyncRuns.length > 0 || snapshotBackgroundWork(sessionId, nowMs).items.length > 0 || detachedForeground;
|
|
@@ -40,17 +42,19 @@ function hasOutstandingWork(state: SubagentState, sessionId: string, nowMs: numb
|
|
|
40
42
|
/** Drain all work owned by the current headless session, including work added while draining. */
|
|
41
43
|
export async function drainOutstandingWork(deps: AutoDrainDeps, observation?: ReadonlyDrainObservation): Promise<void> {
|
|
42
44
|
const sessionId = deps.state.currentSessionId;
|
|
43
|
-
observation?.begin(sessionId, !deps.hasWork && !deps.wait && !deps.now);
|
|
45
|
+
observation?.begin(sessionId, !deps.hasWork && !deps.wait && !deps.now && !deps.hasPendingSupervisorRequest);
|
|
44
46
|
try {
|
|
45
47
|
if (!sessionId) throw new Error("Cannot auto-drain background work without an active session identity.");
|
|
46
48
|
const now = deps.now ?? Date.now;
|
|
47
49
|
const timeoutMs = deps.timeoutMs ?? DEFAULT_AUTO_DRAIN_TIMEOUT_MS;
|
|
48
50
|
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) throw new Error("Auto-drain timeoutMs must be a positive finite number.");
|
|
49
51
|
const deadlineAt = now() + timeoutMs;
|
|
50
|
-
const hasWork = deps.hasWork ?? ((id: string, time: number) => hasOutstandingWork(deps
|
|
52
|
+
const hasWork = deps.hasWork ?? ((id: string, time: number) => hasOutstandingWork(deps, id, time, observation));
|
|
51
53
|
const wait = deps.wait ?? waitForSubagents;
|
|
52
54
|
|
|
53
55
|
while (true) {
|
|
56
|
+
if (deps.state.currentSessionId !== sessionId) throw new Error("Auto-drain stopped because the active session changed.");
|
|
57
|
+
if (deps.hasPendingSupervisorRequest?.()) break;
|
|
54
58
|
const work = hasWork(sessionId, now());
|
|
55
59
|
observation?.predicate(work);
|
|
56
60
|
if (!work) break;
|
|
@@ -63,16 +67,22 @@ export async function drainOutstandingWork(deps: AutoDrainDeps, observation?: Re
|
|
|
63
67
|
undefined,
|
|
64
68
|
{
|
|
65
69
|
state: deps.state,
|
|
70
|
+
asyncDirRoot: deps.asyncDirRoot,
|
|
71
|
+
resultsDir: deps.resultsDir,
|
|
72
|
+
nestedRootRunId: deps.nestedRootRunId,
|
|
66
73
|
events: deps.events,
|
|
67
74
|
now,
|
|
68
75
|
stopOnAttention: false,
|
|
69
76
|
failOnFailedRuns: true,
|
|
70
77
|
failOnAttention: true,
|
|
78
|
+
hasPendingSupervisorRequest: deps.hasPendingSupervisorRequest,
|
|
71
79
|
},
|
|
72
80
|
);
|
|
73
81
|
if (waitResult.isError) {
|
|
74
82
|
throw new Error(`Auto-drain failed for session '${sessionId}': ${resultText(waitResult) || "bg_wait returned an error without details"}.`);
|
|
75
83
|
}
|
|
84
|
+
if (waitResult.details.wait?.reason === "supervisor_request") break;
|
|
85
|
+
if (deps.hasPendingSupervisorRequest?.()) break;
|
|
76
86
|
}
|
|
77
87
|
observation?.complete();
|
|
78
88
|
} catch (error) {
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import * as sdk from "@earendil-works/pi-coding-agent";
|
|
4
|
+
import { installRunnerHttpDispatcher } from "./runner-http-dispatcher.ts";
|
|
4
5
|
import { runConfiguredSubagent, type SubagentRunConfig } from "./subagent-runner.ts";
|
|
6
|
+
import { getAgentDir } from "../../shared/utils.ts";
|
|
5
7
|
|
|
6
8
|
/**
|
|
7
9
|
* Pi's extension loader supplies the embedded SDK. Bare Bun/Node cannot replace
|
|
@@ -24,6 +26,9 @@ export default async function runBinaryBootstrap(): Promise<never> {
|
|
|
24
26
|
} catch {
|
|
25
27
|
// Temp-config cleanup is best effort, as in the Node entrypoint.
|
|
26
28
|
}
|
|
29
|
+
// Pi applies httpIdleTimeoutMs to its dispatcher only after extension
|
|
30
|
+
// factories return; this factory never does, so install the runner's own.
|
|
31
|
+
installRunnerHttpDispatcher({ agentDir: getAgentDir(), cwd: process.cwd() });
|
|
27
32
|
await runConfiguredSubagent(config, { loadPiCodingAgent: async () => sdk });
|
|
28
33
|
process.exit(0);
|
|
29
34
|
} catch (error) {
|
|
@@ -177,7 +177,7 @@ function statusStepForTask(task: RunnerSubagentStep): StatusStep {
|
|
|
177
177
|
model: task.model,
|
|
178
178
|
...(task.contextLimit !== undefined ? { contextLimit: task.contextLimit } : {}),
|
|
179
179
|
thinking: task.thinking,
|
|
180
|
-
|
|
180
|
+
requestedModel: task.requestedModel,
|
|
181
181
|
recentTools: [],
|
|
182
182
|
recentOutput: [],
|
|
183
183
|
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import { resultFilePath, resultPayloadPathForSessionRun } from "./result-files.ts";
|
|
4
|
-
import type { AcceptanceLedger, ArtifactPaths, AsyncStatus, CostSummary, EffectsProjection, ExecutionProjection,
|
|
4
|
+
import type { AcceptanceLedger, ArtifactPaths, AsyncStatus, CostSummary, EffectsProjection, ExecutionProjection, Usage } from "../../shared/types.ts";
|
|
5
5
|
import { readStatus } from "../../shared/utils.ts";
|
|
6
6
|
|
|
7
7
|
export interface ImportedAsyncRoot {
|
|
@@ -13,6 +13,7 @@ export interface ImportedAsyncRoot {
|
|
|
13
13
|
|
|
14
14
|
export interface ImportedAsyncRootResult {
|
|
15
15
|
agent: string;
|
|
16
|
+
importedPublication?: { sessionId?: string; toolCallId?: string };
|
|
16
17
|
/** Human-readable display name for the child session, when derived at launch. */
|
|
17
18
|
sessionName?: string;
|
|
18
19
|
output: string;
|
|
@@ -22,8 +23,7 @@ export interface ImportedAsyncRootResult {
|
|
|
22
23
|
sessionFile?: string;
|
|
23
24
|
intercomTarget?: string;
|
|
24
25
|
model?: string;
|
|
25
|
-
|
|
26
|
-
modelAttempts?: ModelAttempt[];
|
|
26
|
+
requestedModel?: string;
|
|
27
27
|
contextOverflow?: boolean;
|
|
28
28
|
totalCost?: CostSummary;
|
|
29
29
|
usage?: Usage;
|
|
@@ -43,6 +43,8 @@ export interface ImportedAsyncRootResult {
|
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
interface AsyncResultFile {
|
|
46
|
+
sessionId?: string;
|
|
47
|
+
toolCallId?: string;
|
|
46
48
|
state?: string;
|
|
47
49
|
success?: boolean;
|
|
48
50
|
summary?: string;
|
|
@@ -62,8 +64,7 @@ interface AsyncResultFile {
|
|
|
62
64
|
sessionFile?: string;
|
|
63
65
|
intercomTarget?: string;
|
|
64
66
|
model?: string;
|
|
65
|
-
|
|
66
|
-
modelAttempts?: ModelAttempt[];
|
|
67
|
+
requestedModel?: string;
|
|
67
68
|
contextOverflow?: boolean;
|
|
68
69
|
totalCost?: CostSummary;
|
|
69
70
|
usage?: Usage;
|
|
@@ -82,23 +83,6 @@ interface AsyncResultFile {
|
|
|
82
83
|
const TERMINAL_STATES = new Set(["complete", "failed", "partial", "paused", "stopped"]);
|
|
83
84
|
const TERMINAL_STEP_STATUSES = new Set(["complete", "completed", "failed", "partial", "paused", "stopped"]);
|
|
84
85
|
|
|
85
|
-
function usageFromAttempts(attempts: ModelAttempt[] | undefined): Usage | undefined {
|
|
86
|
-
if (!attempts?.length) return undefined;
|
|
87
|
-
const usage: Usage = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 0 };
|
|
88
|
-
for (const attempt of attempts) {
|
|
89
|
-
if (!attempt.usage) continue;
|
|
90
|
-
usage.input += attempt.usage.input;
|
|
91
|
-
usage.output += attempt.usage.output;
|
|
92
|
-
usage.cacheRead += attempt.usage.cacheRead;
|
|
93
|
-
usage.cacheWrite += attempt.usage.cacheWrite;
|
|
94
|
-
usage.cost += attempt.usage.cost;
|
|
95
|
-
usage.turns += attempt.usage.turns;
|
|
96
|
-
}
|
|
97
|
-
return usage.input !== 0 || usage.output !== 0 || usage.cacheRead !== 0 || usage.cacheWrite !== 0 || usage.cost !== 0 || usage.turns !== 0
|
|
98
|
-
? usage
|
|
99
|
-
: undefined;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
86
|
function readResultFile(resultPath: string): AsyncResultFile | undefined {
|
|
103
87
|
try {
|
|
104
88
|
return JSON.parse(fs.readFileSync(resultPath, "utf-8")) as AsyncResultFile;
|
|
@@ -153,7 +137,6 @@ function outputFromTerminalStatus(root: ImportedAsyncRoot, status: AsyncStatus,
|
|
|
153
137
|
const execution = step?.execution ?? (partialStatus
|
|
154
138
|
? { status: "partial" as const, success: false, exitCode: 1, error: message }
|
|
155
139
|
: undefined);
|
|
156
|
-
const usage = usageFromAttempts(step?.modelAttempts);
|
|
157
140
|
return {
|
|
158
141
|
agent,
|
|
159
142
|
output: message,
|
|
@@ -165,11 +148,9 @@ function outputFromTerminalStatus(root: ImportedAsyncRoot, status: AsyncStatus,
|
|
|
165
148
|
...(step?.sessionName ? { sessionName: step.sessionName } : {}),
|
|
166
149
|
...(step?.sessionFile ?? status.sessionFile ? { sessionFile: step?.sessionFile ?? status.sessionFile } : {}),
|
|
167
150
|
...(step?.model ? { model: step.model } : {}),
|
|
168
|
-
...(step?.
|
|
169
|
-
...(step?.modelAttempts ? { modelAttempts: step.modelAttempts } : {}),
|
|
151
|
+
...(step?.requestedModel ? { requestedModel: step.requestedModel } : {}),
|
|
170
152
|
...(step?.contextOverflow ? { contextOverflow: true } : {}),
|
|
171
153
|
...(step?.totalCost ? { totalCost: step.totalCost } : {}),
|
|
172
|
-
...(usage ? { usage } : {}),
|
|
173
154
|
...(step?.structuredOutput !== undefined ? { structuredOutput: step.structuredOutput } : {}),
|
|
174
155
|
...(step?.structuredOutputPath ? { structuredOutputPath: step.structuredOutputPath } : {}),
|
|
175
156
|
...(step?.structuredOutputSchemaPath ? { structuredOutputSchemaPath: step.structuredOutputSchemaPath } : {}),
|
|
@@ -182,7 +163,6 @@ function outputFromTerminalStatus(root: ImportedAsyncRoot, status: AsyncStatus,
|
|
|
182
163
|
|
|
183
164
|
function outputFromTimeout(root: ImportedAsyncRoot, status: AsyncStatus | null, message: string): ImportedAsyncRootResult {
|
|
184
165
|
const step = selectedStatusStep(status, root.index);
|
|
185
|
-
const usage = usageFromAttempts(step?.modelAttempts);
|
|
186
166
|
return {
|
|
187
167
|
agent: step?.agent ?? status?.steps?.[root.index]?.agent ?? "subagent",
|
|
188
168
|
output: message,
|
|
@@ -193,11 +173,9 @@ function outputFromTimeout(root: ImportedAsyncRoot, status: AsyncStatus | null,
|
|
|
193
173
|
...(step?.sessionName ? { sessionName: step.sessionName } : {}),
|
|
194
174
|
...(step?.sessionFile ?? status?.sessionFile ? { sessionFile: step?.sessionFile ?? status?.sessionFile } : {}),
|
|
195
175
|
...(step?.model ? { model: step.model } : {}),
|
|
196
|
-
...(step?.
|
|
197
|
-
...(step?.modelAttempts ? { modelAttempts: step.modelAttempts } : {}),
|
|
176
|
+
...(step?.requestedModel ? { requestedModel: step.requestedModel } : {}),
|
|
198
177
|
...(step?.contextOverflow ? { contextOverflow: true } : {}),
|
|
199
178
|
...(step?.totalCost ? { totalCost: step.totalCost } : {}),
|
|
200
|
-
...(usage ? { usage } : {}),
|
|
201
179
|
...(step?.transcriptPath ? { transcriptPath: step.transcriptPath } : {}),
|
|
202
180
|
};
|
|
203
181
|
}
|
|
@@ -213,9 +191,13 @@ function buildImportedResult(root: ImportedAsyncRoot, status: AsyncStatus | null
|
|
|
213
191
|
const success = state === "complete" && !timedOut && !stopped;
|
|
214
192
|
const error = child?.error ?? (success ? undefined : stopped ? "Subagent stopped by user." : result.error ?? result.summary ?? status?.error ?? `Attached async root ${root.runId} did not complete successfully.`);
|
|
215
193
|
const execution = state ? { status: state === "complete" ? "completed" as const : state, success, exitCode: success ? 0 : 1, ...(error ? { error } : {}) } : undefined;
|
|
216
|
-
const usage = child?.usage
|
|
194
|
+
const usage = child?.usage;
|
|
217
195
|
return {
|
|
218
196
|
agent,
|
|
197
|
+
importedPublication: {
|
|
198
|
+
...(typeof result.sessionId === "string" ? { sessionId: result.sessionId } : {}),
|
|
199
|
+
...(typeof result.toolCallId === "string" ? { toolCallId: result.toolCallId } : {}),
|
|
200
|
+
},
|
|
219
201
|
output: success ? output : (output || error || ""),
|
|
220
202
|
success,
|
|
221
203
|
exitCode: success ? 0 : 1,
|
|
@@ -226,8 +208,7 @@ function buildImportedResult(root: ImportedAsyncRoot, status: AsyncStatus | null
|
|
|
226
208
|
...(child?.sessionFile ?? step?.sessionFile ?? status?.sessionFile ? { sessionFile: child?.sessionFile ?? step?.sessionFile ?? status?.sessionFile } : {}),
|
|
227
209
|
...(child?.intercomTarget ? { intercomTarget: child.intercomTarget } : {}),
|
|
228
210
|
...(child?.model ?? step?.model ? { model: child?.model ?? step?.model } : {}),
|
|
229
|
-
...(child?.
|
|
230
|
-
...(child?.modelAttempts ?? step?.modelAttempts ? { modelAttempts: child?.modelAttempts ?? step?.modelAttempts } : {}),
|
|
211
|
+
...(child?.requestedModel ?? step?.requestedModel ? { requestedModel: child?.requestedModel ?? step?.requestedModel } : {}),
|
|
231
212
|
...(child?.contextOverflow || step?.contextOverflow ? { contextOverflow: true } : {}),
|
|
232
213
|
...(child?.totalCost ?? step?.totalCost ? { totalCost: child?.totalCost ?? step?.totalCost } : {}),
|
|
233
214
|
...(usage ? { usage } : {}),
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* observation channel, not a delivery acknowledgement.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
+
import { statSync } from "node:fs";
|
|
9
10
|
import { debuglog } from "node:util";
|
|
10
11
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
11
12
|
import { buildCompletionKey, markSeenWithTtl } from "./completion-dedupe.ts";
|
|
@@ -27,6 +28,10 @@ export interface SubagentNotifyChildOutput {
|
|
|
27
28
|
agent?: string;
|
|
28
29
|
status: string;
|
|
29
30
|
savedOutputPath?: string;
|
|
31
|
+
outputArtifactPath?: string;
|
|
32
|
+
structuredOutputPath?: string;
|
|
33
|
+
outputArtifactError?: RetainedPathError;
|
|
34
|
+
structuredOutputError?: RetainedPathError;
|
|
30
35
|
preview: string;
|
|
31
36
|
previewTruncated?: boolean;
|
|
32
37
|
previewUnavailableReason?: string;
|
|
@@ -36,6 +41,7 @@ export type SubagentNotifyWatchdogBlocker = Pick<ChildWatchdogWarningSummary, "s
|
|
|
36
41
|
|
|
37
42
|
export interface SubagentNotifyDetails {
|
|
38
43
|
workflowReceiptPath?: string;
|
|
44
|
+
asyncDir?: string;
|
|
39
45
|
agent: string;
|
|
40
46
|
status: "completed" | "failed" | "paused" | "stopped";
|
|
41
47
|
source?: "async" | "foreground";
|
|
@@ -90,9 +96,12 @@ export interface CompletionNotification {
|
|
|
90
96
|
success?: boolean;
|
|
91
97
|
output?: string;
|
|
92
98
|
structuredOutput?: unknown;
|
|
99
|
+
structuredOutputPath?: string;
|
|
93
100
|
outputState?: "present" | "absent" | "unknown";
|
|
94
101
|
outputReference?: string | { path?: string };
|
|
95
102
|
artifactPaths?: { outputPath?: string };
|
|
103
|
+
outputSaveError?: string;
|
|
104
|
+
artifactOutputSaveFailed?: true;
|
|
96
105
|
detached?: boolean;
|
|
97
106
|
exitCode?: number | null;
|
|
98
107
|
processSignal?: string | null;
|
|
@@ -119,6 +128,7 @@ export interface CompletionNotification {
|
|
|
119
128
|
intercomDelivered?: boolean;
|
|
120
129
|
parallelHandoff?: ParallelHandoffReference;
|
|
121
130
|
scheduleOrigin?: ScheduleOrigin;
|
|
131
|
+
asyncDir?: string;
|
|
122
132
|
}
|
|
123
133
|
|
|
124
134
|
interface NotifyTimerApi {
|
|
@@ -143,6 +153,11 @@ const CHILD_OUTPUT_PREVIEW_MAX_BYTES = 4 * 1024;
|
|
|
143
153
|
const CHILD_OUTPUT_PREVIEW_COUNT = 8;
|
|
144
154
|
const PREVIEW_TRUNCATION_MARKER = "...[preview truncated]";
|
|
145
155
|
type CompletionChild = NonNullable<CompletionNotification["results"]>[number];
|
|
156
|
+
interface RetainedPathError { path: string; code?: string; message: string }
|
|
157
|
+
type RetainedPathProjection =
|
|
158
|
+
| { status: "verified"; path: string }
|
|
159
|
+
| { status: "unavailable" }
|
|
160
|
+
| { status: "error"; error: RetainedPathError };
|
|
146
161
|
|
|
147
162
|
function truncateUtf8Head(value: string, maxBytes: number): { text: string; truncated: boolean } {
|
|
148
163
|
const bytes = Buffer.from(value, "utf8");
|
|
@@ -171,6 +186,27 @@ function childSavedOutputPath(child: CompletionChild): string | undefined {
|
|
|
171
186
|
return outputPathFromReference(child.outputReference);
|
|
172
187
|
}
|
|
173
188
|
|
|
189
|
+
function nonemptyPath(value: unknown): string | undefined {
|
|
190
|
+
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function retainedChildPath(value: unknown, producerConfirmed = false): RetainedPathProjection {
|
|
194
|
+
const path = nonemptyPath(value);
|
|
195
|
+
if (!path) return { status: "unavailable" };
|
|
196
|
+
if (producerConfirmed) return { status: "verified", path };
|
|
197
|
+
try {
|
|
198
|
+
return statSync(path).isFile() ? { status: "verified", path } : { status: "unavailable" };
|
|
199
|
+
} catch (error) {
|
|
200
|
+
const code = typeof error === "object" && error !== null && "code" in error && typeof error.code === "string" ? error.code : undefined;
|
|
201
|
+
if (code === "ENOENT" || code === "ENOTDIR") return { status: "unavailable" };
|
|
202
|
+
return { status: "error", error: { path, ...(code ? { code } : {}), message: error instanceof Error ? error.message : String(error) } };
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function formatRetainedPathError(label: string, error: RetainedPathError): string {
|
|
207
|
+
return ` ${label} verification failed: stat ${error.code ? boundedSafeText(error.code, 32) : "unknown"}; path=${boundedSafeText(error.path, 384)}; ${boundedSafeText(error.message, 512)}`;
|
|
208
|
+
}
|
|
209
|
+
|
|
174
210
|
function childStatus(child: CompletionChild, workflowState?: string): string {
|
|
175
211
|
const knownStatus = child.status === "complete"
|
|
176
212
|
? "completed"
|
|
@@ -231,6 +267,11 @@ function formatChildOutputBlock(children: SubagentNotifyChildOutput[] | undefine
|
|
|
231
267
|
const status = boundedSafeText(child.status) || "unavailable";
|
|
232
268
|
lines.push(`- key=${key} run=${runId} status=${status}`);
|
|
233
269
|
lines.push(` Saved output: ${child.savedOutputPath ? boundedSafeText(child.savedOutputPath) : "unavailable"}`);
|
|
270
|
+
if (child.outputArtifactPath) lines.push(` Output artifact (retention-managed): ${boundedSafeText(child.outputArtifactPath)}`);
|
|
271
|
+
if (child.structuredOutputPath) lines.push(` Structured output (retention-managed): ${boundedSafeText(child.structuredOutputPath)}`);
|
|
272
|
+
if (child.outputArtifactError) lines.push(formatRetainedPathError("Output artifact", child.outputArtifactError));
|
|
273
|
+
if (child.structuredOutputError) lines.push(formatRetainedPathError("Structured output", child.structuredOutputError));
|
|
274
|
+
if (child.previewTruncated && !child.savedOutputPath && !child.outputArtifactPath) lines.push(" Full output unavailable");
|
|
234
275
|
if (index >= CHILD_OUTPUT_PREVIEW_COUNT) {
|
|
235
276
|
lines.push(" Preview: unavailable (notice preview budget exceeded)");
|
|
236
277
|
continue;
|
|
@@ -267,6 +308,7 @@ function formatChildRun(child: { runId: string; workflowKey?: string; agent?: st
|
|
|
267
308
|
|
|
268
309
|
function formatCorrelationLines(details: SubagentNotifyDetails): string[] {
|
|
269
310
|
return [
|
|
311
|
+
details.asyncDir ? `Retention-managed async directory: ${boundedSafeText(details.asyncDir)}` : undefined,
|
|
270
312
|
details.workflowRunId ? `Workflow run: ${details.workflowRunId}` : undefined,
|
|
271
313
|
details.childRuns?.length ? `Child runs: ${details.childRuns.map(formatChildRun).join(", ")}` : undefined,
|
|
272
314
|
details.reconciledFromDetachedChild ? `Reconciled detached child: ${details.reconciledFromDetachedChild}` : undefined,
|
|
@@ -322,6 +364,20 @@ export function scheduledCompletionTriggersTurn(origin: ScheduleOrigin | undefin
|
|
|
322
364
|
return !(origin?.quiet === true && outcome === "completed");
|
|
323
365
|
}
|
|
324
366
|
|
|
367
|
+
/**
|
|
368
|
+
* Child settlement is useful context, but an ordinary successful child does not
|
|
369
|
+
* establish the workflow's dependency barrier while its workflow is running.
|
|
370
|
+
* Keep actionable outcomes waking the parent, and preserve a terminal child as
|
|
371
|
+
* the barrier for hosts that do not emit a separate workflow completion wake.
|
|
372
|
+
*/
|
|
373
|
+
export function incrementalChildCompletionTriggersTurn(
|
|
374
|
+
child: Pick<IncrementalChildCompletion, "outcome" | "workflowRunning">,
|
|
375
|
+
origin: ScheduleOrigin | undefined,
|
|
376
|
+
): boolean {
|
|
377
|
+
if (child.workflowRunning && child.outcome === "completed") return false;
|
|
378
|
+
return scheduledCompletionTriggersTurn(origin, child.outcome);
|
|
379
|
+
}
|
|
380
|
+
|
|
325
381
|
export function formatIncrementalChildCompletion(child: IncrementalChildCompletion): string {
|
|
326
382
|
const statusText = child.outcome === "completed" ? "completed"
|
|
327
383
|
: child.outcome === "failed" ? "failed"
|
|
@@ -559,12 +615,20 @@ export function buildCompletionDetails(result: CompletionNotification): Subagent
|
|
|
559
615
|
const workflowKey = typeof child.workflowKey === "string" && child.workflowKey.trim() ? child.workflowKey.trim() : undefined;
|
|
560
616
|
const runId = typeof child.runId === "string" && child.runId.trim() ? child.runId.trim() : undefined;
|
|
561
617
|
const savedOutputPath = childSavedOutputPath(child);
|
|
618
|
+
const outputArtifact = child.artifactOutputSaveFailed === true ? { status: "unavailable" as const } : retainedChildPath(child.artifactPaths?.outputPath);
|
|
619
|
+
const structuredOutput = child.stopped === true || child.timedOut === true
|
|
620
|
+
? { status: "unavailable" as const }
|
|
621
|
+
: retainedChildPath(child.structuredOutputPath, child.structuredOutput !== undefined);
|
|
562
622
|
return {
|
|
563
623
|
...(workflowKey ? { workflowKey } : {}),
|
|
564
624
|
...(runId ? { runId } : {}),
|
|
565
625
|
...(typeof child.agent === "string" ? { agent: child.agent } : {}),
|
|
566
626
|
status: childStatus(child, result.state),
|
|
567
627
|
...(savedOutputPath ? { savedOutputPath } : {}),
|
|
628
|
+
...(outputArtifact.status === "verified" ? { outputArtifactPath: outputArtifact.path } : {}),
|
|
629
|
+
...(structuredOutput.status === "verified" ? { structuredOutputPath: structuredOutput.path } : {}),
|
|
630
|
+
...(outputArtifact.status === "error" ? { outputArtifactError: outputArtifact.error } : {}),
|
|
631
|
+
...(structuredOutput.status === "error" ? { structuredOutputError: structuredOutput.error } : {}),
|
|
568
632
|
...(inline.preview ? { preview: inline.preview } : { preview: "" }),
|
|
569
633
|
...(inline.truncated ? { previewTruncated: true } : {}),
|
|
570
634
|
...(inline.unavailableReason ? { previewUnavailableReason: inline.unavailableReason } : {}),
|
|
@@ -572,15 +636,18 @@ export function buildCompletionDetails(result: CompletionNotification): Subagent
|
|
|
572
636
|
})
|
|
573
637
|
: undefined;
|
|
574
638
|
const reconciledFromDetachedChild = typeof result.reconciledFromDetachedChild === "string" ? result.reconciledFromDetachedChild : undefined;
|
|
639
|
+
const asyncDir = nonemptyPath(result.asyncDir);
|
|
575
640
|
const watchdogBlockers: SubagentNotifyWatchdogBlocker[] = [];
|
|
576
|
-
const
|
|
641
|
+
const watchdogConcerns: string[] = [];
|
|
642
|
+
const collectWatchdogFindings = (owner: string, progress: ChildWatchdogProgress | undefined) => {
|
|
577
643
|
for (const warning of progress?.warnings ?? []) {
|
|
578
|
-
if (warning.
|
|
579
|
-
watchdogBlockers.push({ agent: owner, summary: warning.summary, addressed: warning.addressed, stalemate: warning.stalemate });
|
|
644
|
+
if (warning.importance !== "high") continue;
|
|
645
|
+
if (warning.severity === "blocker") watchdogBlockers.push({ agent: owner, summary: warning.summary, addressed: warning.addressed, stalemate: warning.stalemate });
|
|
646
|
+
else watchdogConcerns.push(`${owner}: ${warning.summary}\nEvidence: ${warning.evidence}\nRecommended action: ${warning.recommendedAction}`);
|
|
580
647
|
}
|
|
581
648
|
};
|
|
582
|
-
|
|
583
|
-
for (const child of result.results ?? [])
|
|
649
|
+
collectWatchdogFindings(agent, result.watchdog);
|
|
650
|
+
for (const child of result.results ?? []) collectWatchdogFindings(typeof child.agent === "string" ? child.agent : agent, child.watchdog);
|
|
584
651
|
const session =
|
|
585
652
|
result.shareUrl
|
|
586
653
|
? { label: "Session", value: result.shareUrl }
|
|
@@ -598,9 +665,10 @@ export function buildCompletionDetails(result: CompletionNotification): Subagent
|
|
|
598
665
|
status,
|
|
599
666
|
...(scheduleOrigin ? { scheduleOrigin } : {}),
|
|
600
667
|
...(workflowReceiptPath ? { workflowReceiptPath } : {}),
|
|
668
|
+
...(asyncDir ? { asyncDir } : {}),
|
|
601
669
|
...(result.source ? { source: result.source } : {}),
|
|
602
670
|
...(taskInfo ? { taskInfo } : {}),
|
|
603
|
-
resultPreview,
|
|
671
|
+
resultPreview: watchdogConcerns.length ? `${resultPreview}\n\nHigh-importance watchdog concerns:\n${watchdogConcerns.map((warning) => `- ${warning}`).join("\n")}` : resultPreview,
|
|
604
672
|
...(typeof result.durationMs === "number" ? { durationMs: result.durationMs } : {}),
|
|
605
673
|
...(handoffPath ? { handoffPath } : {}),
|
|
606
674
|
...(workflowRunId ? { workflowRunId } : {}),
|