pi-subagents 0.34.0 → 0.35.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 +78 -9
- package/README.md +213 -32
- package/index.ts +1 -0
- package/install.mjs +1 -1
- package/package.json +23 -8
- package/prompts/review-loop.md +3 -1
- package/skills/pi-subagents/SKILL.md +87 -25
- package/src/agents/agent-management.ts +82 -15
- package/src/agents/agent-serializer.ts +19 -0
- package/src/agents/agents.ts +91 -49
- package/src/agents/frontmatter.ts +67 -13
- package/src/agents/skills.ts +25 -12
- package/src/api/background-work.ts +197 -0
- package/src/api/delegation.ts +158 -0
- package/src/extension/chain-validation.ts +165 -0
- package/src/extension/doctor.ts +15 -0
- package/src/extension/fanout-child.ts +3 -1
- package/src/extension/index.ts +65 -124
- package/src/extension/rpc.ts +10 -2
- package/src/extension/schemas.ts +18 -14
- package/src/extension/steering-notices.ts +35 -0
- package/src/extension/tool-description.ts +20 -9
- package/src/intercom/intercom-bridge.ts +3 -2
- package/src/intercom/native-supervisor-channel.ts +9 -1
- package/src/intercom/result-intercom.ts +4 -0
- package/src/runs/background/async-execution.ts +293 -44
- package/src/runs/background/async-job-tracker.ts +56 -9
- package/src/runs/background/async-resume.ts +159 -52
- package/src/runs/background/async-status.ts +25 -18
- package/src/runs/background/auto-drain.ts +67 -0
- package/src/runs/background/chain-root-attachment.ts +16 -8
- package/src/runs/background/control-channel.ts +260 -13
- package/src/runs/background/fleet-view.ts +23 -2
- package/src/runs/background/notify.ts +79 -10
- package/src/runs/background/result-watcher.ts +12 -9
- package/src/runs/background/run-id-resolver.ts +14 -2
- package/src/runs/background/run-status.ts +23 -15
- package/src/runs/background/scheduled-runs.ts +3 -0
- package/src/runs/background/stale-run-reconciler.ts +32 -10
- package/src/runs/background/steering.ts +237 -0
- package/src/runs/background/subagent-runner.ts +898 -236
- package/src/runs/background/subagent-wait.ts +484 -0
- package/src/runs/background/top-level-async.ts +2 -1
- package/src/runs/background/wait-config.ts +36 -0
- package/src/runs/background/wait-tool.ts +26 -0
- package/src/runs/foreground/async-steering-action.ts +230 -0
- package/src/runs/foreground/chain-clarify.ts +22 -6
- package/src/runs/foreground/chain-execution.ts +50 -32
- package/src/runs/foreground/execution.ts +308 -94
- package/src/runs/foreground/subagent-executor.ts +592 -268
- package/src/runs/shared/acceptance.ts +355 -97
- package/src/runs/shared/child-protocol.ts +121 -0
- package/src/runs/shared/completion-guard.ts +8 -127
- package/src/runs/shared/dynamic-fanout.ts +6 -4
- package/src/runs/shared/model-fallback.ts +36 -0
- package/src/runs/shared/nested-events.ts +9 -4
- package/src/runs/shared/nested-render.ts +4 -1
- package/src/runs/shared/parallel-utils.ts +7 -0
- package/src/runs/shared/pi-args.ts +34 -7
- package/src/runs/shared/pi-spawn.ts +18 -12
- package/src/runs/shared/session-lease.ts +279 -0
- package/src/runs/shared/single-output.ts +61 -6
- package/src/runs/shared/spawn-budget.ts +128 -0
- package/src/runs/shared/subagent-control.ts +10 -6
- package/src/runs/shared/subagent-prompt-runtime.ts +127 -26
- package/src/runs/shared/task-intent.ts +176 -0
- package/src/runs/shared/tool-availability.ts +65 -0
- package/src/runs/shared/turn-budget.ts +49 -4
- package/src/shared/atomic-json.ts +4 -1
- package/src/shared/fork-context.ts +28 -3
- package/src/shared/model-info.ts +7 -4
- package/src/shared/status-format.ts +7 -1
- package/src/shared/types.ts +203 -25
- package/src/shared/utils.ts +35 -7
- package/src/slash/delegation-adapters.ts +457 -0
- package/src/slash/delegation-request.ts +103 -0
- package/src/slash/prompt-template-bridge.ts +167 -344
- package/src/slash/slash-commands.ts +239 -6
- package/src/slash/subagents-admin.ts +428 -0
- package/src/slash/subagents-editor.ts +86 -0
- package/src/tui/fleet.ts +405 -0
- package/src/tui/render.ts +90 -16
- package/src/watchdog/change-signature.ts +127 -0
- package/src/watchdog/child-status.ts +205 -0
- package/src/watchdog/emission-guard.ts +123 -0
- package/src/watchdog/lsp-diagnostics.ts +532 -0
- package/src/watchdog/model-selection.ts +167 -0
- package/src/watchdog/register-child.ts +117 -0
- package/src/watchdog/register-main.ts +433 -0
- package/src/watchdog/render.ts +54 -0
- package/src/watchdog/review.ts +293 -0
- package/src/watchdog/runtime.ts +712 -0
- package/src/watchdog/settings.ts +528 -0
- package/src/watchdog/tool-actions.ts +155 -0
- package/src/watchdog/turn-delta.ts +161 -0
- package/src/watchdog/types.ts +188 -0
- package/src/watchdog/warning-format.ts +73 -0
- package/src/runs/background/wait.ts +0 -394
package/src/tui/render.ts
CHANGED
|
@@ -268,6 +268,7 @@ function firstOutputLine(text: string): string {
|
|
|
268
268
|
|
|
269
269
|
function resultStatusLine(result: Details["results"][number], output: string): string {
|
|
270
270
|
if (result.detached) return result.detachedReason ? `Detached: ${result.detachedReason}` : "Detached";
|
|
271
|
+
if (result.stopped) return "Stopped";
|
|
271
272
|
if (result.interrupted) return "Paused";
|
|
272
273
|
if (result.exitCode !== 0) return `Error: ${result.error ?? (firstOutputLine(output) || `exit ${result.exitCode}`)}`;
|
|
273
274
|
if (result.acceptance?.status && result.acceptance.status !== "not-required") return `Done · acceptance: ${result.acceptance.status}`;
|
|
@@ -281,6 +282,7 @@ function resultGlyph(result: Details["results"][number], output: string, theme:
|
|
|
281
282
|
return theme.fg("accent", runningGlyph(seed));
|
|
282
283
|
}
|
|
283
284
|
if (result.detached) return theme.fg("warning", "■");
|
|
285
|
+
if (result.stopped) return theme.fg("warning", "■");
|
|
284
286
|
if (result.interrupted) return theme.fg("warning", "■");
|
|
285
287
|
if (result.exitCode !== 0) return theme.fg("error", "✗");
|
|
286
288
|
if (hasEmptyTextOutputWithoutOutputTarget(result.task, output)) return theme.fg("warning", "✓");
|
|
@@ -349,6 +351,7 @@ function widgetActivity(job: AsyncJobState): string {
|
|
|
349
351
|
if (job.status === "running") return "thinking…";
|
|
350
352
|
if (job.status === "queued") return "queued…";
|
|
351
353
|
if (job.status === "paused") return "Paused";
|
|
354
|
+
if (job.status === "stopped") return "Stopped";
|
|
352
355
|
if (job.status === "failed") return "Failed";
|
|
353
356
|
return "Done";
|
|
354
357
|
}
|
|
@@ -397,6 +400,7 @@ function widgetStatusGlyph(job: AsyncJobState, theme: Theme): string {
|
|
|
397
400
|
if (job.status === "queued") return theme.fg("muted", "◦");
|
|
398
401
|
if (job.status === "complete") return theme.fg("success", "✓");
|
|
399
402
|
if (job.status === "paused") return theme.fg("warning", "■");
|
|
403
|
+
if (job.status === "stopped") return theme.fg("warning", "■");
|
|
400
404
|
return theme.fg("error", "✗");
|
|
401
405
|
}
|
|
402
406
|
|
|
@@ -405,6 +409,7 @@ function widgetStepGlyph(status: AsyncJobStep["status"], theme: Theme, seed?: nu
|
|
|
405
409
|
if (status === "complete" || status === "completed") return theme.fg("success", "✓");
|
|
406
410
|
if (status === "failed") return theme.fg("error", "✗");
|
|
407
411
|
if (status === "paused") return theme.fg("warning", "■");
|
|
412
|
+
if (status === "stopped") return theme.fg("warning", "■");
|
|
408
413
|
return theme.fg("muted", "◦");
|
|
409
414
|
}
|
|
410
415
|
|
|
@@ -413,6 +418,7 @@ function widgetStepStatus(status: AsyncJobStep["status"], theme: Theme): string
|
|
|
413
418
|
if (status === "complete" || status === "completed") return theme.fg("success", "complete");
|
|
414
419
|
if (status === "failed") return theme.fg("error", "failed");
|
|
415
420
|
if (status === "paused") return theme.fg("warning", "paused");
|
|
421
|
+
if (status === "stopped") return theme.fg("warning", "stopped");
|
|
416
422
|
return theme.fg("dim", status);
|
|
417
423
|
}
|
|
418
424
|
|
|
@@ -620,7 +626,7 @@ function buildMultiProgressLabel(details: Pick<Details, "mode" | "results" | "pr
|
|
|
620
626
|
|
|
621
627
|
if (details.mode === "parallel") {
|
|
622
628
|
const totalCount = details.totalSteps ?? details.results.length;
|
|
623
|
-
const statuses = new Array(totalCount).fill("pending") as Array<"pending" | "running" | "completed" | "failed" | "detached">;
|
|
629
|
+
const statuses = new Array(totalCount).fill("pending") as Array<"pending" | "running" | "completed" | "failed" | "stopped" | "detached">;
|
|
624
630
|
for (const progress of details.progress ?? []) {
|
|
625
631
|
if (progress.index >= 0 && progress.index < totalCount) statuses[progress.index] = progress.status;
|
|
626
632
|
}
|
|
@@ -631,11 +637,13 @@ function buildMultiProgressLabel(details: Pick<Details, "mode" | "results" | "pr
|
|
|
631
637
|
const index = result.progress?.index ?? progressFromArray?.index ?? i;
|
|
632
638
|
if (index < 0 || index >= totalCount) continue;
|
|
633
639
|
const status = result.progress?.status
|
|
634
|
-
?? (result.
|
|
635
|
-
? "
|
|
636
|
-
: result.
|
|
637
|
-
? "
|
|
638
|
-
:
|
|
640
|
+
?? (result.stopped
|
|
641
|
+
? "stopped"
|
|
642
|
+
: result.interrupted || result.detached
|
|
643
|
+
? "detached"
|
|
644
|
+
: result.exitCode === 0
|
|
645
|
+
? "completed"
|
|
646
|
+
: "failed");
|
|
639
647
|
statuses[index] = status;
|
|
640
648
|
}
|
|
641
649
|
const running = statuses.filter((status) => status === "running").length;
|
|
@@ -788,6 +796,7 @@ function nestedStatusGlyph(state: NestedRunSummary["state"] | NestedStepSummary[
|
|
|
788
796
|
if (state === "complete" || state === "completed") return theme.fg("success", "✓");
|
|
789
797
|
if (state === "failed") return theme.fg("error", "✗");
|
|
790
798
|
if (state === "paused") return theme.fg("warning", "■");
|
|
799
|
+
if (state === "stopped") return theme.fg("warning", "■");
|
|
791
800
|
return theme.fg("muted", "◦");
|
|
792
801
|
}
|
|
793
802
|
|
|
@@ -795,6 +804,29 @@ function nestedRunSeed(run: NestedRunSummary): number | undefined {
|
|
|
795
804
|
return runningSeed(run.lastUpdate, run.lastActivityAt, run.currentStep, run.toolCount, run.turnCount, run.totalTokens?.total, run.currentToolStartedAt);
|
|
796
805
|
}
|
|
797
806
|
|
|
807
|
+
function formatClockTime(ms: number | undefined): string | undefined {
|
|
808
|
+
if (ms === undefined || !Number.isFinite(ms)) return undefined;
|
|
809
|
+
const date = new Date(ms);
|
|
810
|
+
const pad = (value: number) => value.toString().padStart(2, "0");
|
|
811
|
+
return `${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
function nestedRunEventTime(run: NestedRunSummary): number | undefined {
|
|
815
|
+
return run.state === "running"
|
|
816
|
+
? (run.lastActivityAt ?? run.currentToolStartedAt ?? run.lastUpdate ?? run.startedAt)
|
|
817
|
+
: (run.endedAt ?? run.lastUpdate ?? run.lastActivityAt ?? run.startedAt);
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
function nestedStepTimestamp(step: NestedStepSummary, fallback?: number): string | undefined {
|
|
821
|
+
return formatClockTime(step.status === "running"
|
|
822
|
+
? (step.lastActivityAt ?? step.currentToolStartedAt ?? fallback ?? step.startedAt)
|
|
823
|
+
: (step.endedAt ?? step.lastActivityAt ?? fallback ?? step.startedAt));
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
function nestedTimestampPrefix(timestamp: string | undefined): string {
|
|
827
|
+
return timestamp ? `[${timestamp}] ` : "";
|
|
828
|
+
}
|
|
829
|
+
|
|
798
830
|
function nestedActivity(input: Pick<NestedRunSummary | NestedStepSummary, "activityState" | "lastActivityAt" | "currentTool" | "currentToolStartedAt" | "currentPath" | "turnCount" | "toolCount">, state: NestedRunSummary["state"] | NestedStepSummary["status"], snapshotNow?: number): string {
|
|
799
831
|
const facts: string[] = [];
|
|
800
832
|
if (input.currentTool && input.currentToolStartedAt !== undefined && snapshotNow !== undefined) facts.push(`${input.currentTool} ${formatDuration(Math.max(0, snapshotNow - input.currentToolStartedAt))}`);
|
|
@@ -809,6 +841,7 @@ function nestedActivity(input: Pick<NestedRunSummary | NestedStepSummary, "activ
|
|
|
809
841
|
if (state === "running") return "thinking…";
|
|
810
842
|
if (state === "queued" || state === "pending") return "queued…";
|
|
811
843
|
if (state === "paused") return "Paused";
|
|
844
|
+
if (state === "stopped") return "Stopped";
|
|
812
845
|
if (state === "failed") return "Failed";
|
|
813
846
|
return "Done";
|
|
814
847
|
}
|
|
@@ -817,7 +850,8 @@ function formatNestedWidgetLines(children: NestedRunSummary[] | undefined, theme
|
|
|
817
850
|
if (!children?.length || lineBudget <= 0) return [];
|
|
818
851
|
if (!expanded) {
|
|
819
852
|
const aggregate = formatNestedAggregate(children);
|
|
820
|
-
|
|
853
|
+
const latest = children.reduce((acc, child) => Math.max(acc, nestedRunEventTime(child) ?? 0), 0);
|
|
854
|
+
return aggregate ? [theme.fg("dim", `↳ ${nestedTimestampPrefix(formatClockTime(latest))}${aggregate}`)] : [];
|
|
821
855
|
}
|
|
822
856
|
const lines: string[] = [];
|
|
823
857
|
const maxDepth = 2;
|
|
@@ -837,7 +871,7 @@ function formatNestedWidgetLines(children: NestedRunSummary[] | undefined, theme
|
|
|
837
871
|
}
|
|
838
872
|
const activity = nestedActivity(child, child.state, snapshotNow ?? child.lastUpdate);
|
|
839
873
|
const error = child.error ? ` · ${child.error}` : "";
|
|
840
|
-
lines.push(theme.fg("dim", `${prefix}↳ ${nestedStatusGlyph(child.state, theme, nestedRunSeed(child))} ${nestedRunName(child)} · ${child.state} · ${activity}${error}`));
|
|
874
|
+
lines.push(theme.fg("dim", `${prefix}↳ ${nestedTimestampPrefix(formatClockTime(nestedRunEventTime(child)))}${nestedStatusGlyph(child.state, theme, nestedRunSeed(child))} ${nestedRunName(child)} · ${child.state} · ${activity}${error}`));
|
|
841
875
|
if (depth === maxDepth) {
|
|
842
876
|
const aggregate = formatNestedAggregate([...(child.steps?.flatMap((step) => step.children ?? []) ?? []), ...(child.children ?? [])]);
|
|
843
877
|
if (aggregate && lines.length < lineBudget) lines.push(theme.fg("dim", `${prefix} ↳ ${aggregate}`));
|
|
@@ -845,7 +879,7 @@ function formatNestedWidgetLines(children: NestedRunSummary[] | undefined, theme
|
|
|
845
879
|
}
|
|
846
880
|
for (const step of child.steps ?? []) {
|
|
847
881
|
if (lines.length >= lineBudget) return;
|
|
848
|
-
lines.push(theme.fg("dim", `${prefix} ↳ ${nestedStatusGlyph(step.status, theme)} ${step.agent} · ${step.status} · ${nestedActivity(step, step.status, snapshotNow ?? child.lastUpdate)}`));
|
|
882
|
+
lines.push(theme.fg("dim", `${prefix} ↳ ${nestedTimestampPrefix(nestedStepTimestamp(step, child.lastUpdate))}${nestedStatusGlyph(step.status, theme)} ${step.agent} · ${step.status} · ${nestedActivity(step, step.status, snapshotNow ?? child.lastUpdate)}`));
|
|
849
883
|
append(step.children, depth + 1, `${prefix} `);
|
|
850
884
|
}
|
|
851
885
|
append(child.children, depth + 1, `${prefix} `);
|
|
@@ -984,13 +1018,14 @@ function widgetSessionMatches(expanded: boolean): boolean {
|
|
|
984
1018
|
&& widgetLayoutSession.columns === currentTerminalColumns();
|
|
985
1019
|
}
|
|
986
1020
|
|
|
987
|
-
function widgetHeaderCounts(jobs: AsyncJobState[]): { running: AsyncJobState[]; queued: AsyncJobState[]; complete: AsyncJobState[]; failed: AsyncJobState[]; paused: AsyncJobState[] } {
|
|
1021
|
+
function widgetHeaderCounts(jobs: AsyncJobState[]): { running: AsyncJobState[]; queued: AsyncJobState[]; complete: AsyncJobState[]; failed: AsyncJobState[]; paused: AsyncJobState[]; stopped: AsyncJobState[] } {
|
|
988
1022
|
return {
|
|
989
1023
|
running: jobs.filter((job) => job.status === "running"),
|
|
990
1024
|
queued: jobs.filter((job) => job.status === "queued"),
|
|
991
1025
|
complete: jobs.filter((job) => job.status === "complete"),
|
|
992
1026
|
failed: jobs.filter((job) => job.status === "failed"),
|
|
993
1027
|
paused: jobs.filter((job) => job.status === "paused"),
|
|
1028
|
+
stopped: jobs.filter((job) => job.status === "stopped"),
|
|
994
1029
|
};
|
|
995
1030
|
}
|
|
996
1031
|
|
|
@@ -1002,6 +1037,7 @@ function buildSingleLineWidgetLines(jobs: AsyncJobState[], theme: Theme, width:
|
|
|
1002
1037
|
if (counts.running.length > 0) parts.push(`${counts.running.length}/${jobs.length} running`);
|
|
1003
1038
|
if (counts.queued.length > 0) parts.push(`${counts.queued.length} queued`);
|
|
1004
1039
|
if (counts.failed.length > 0) parts.push(`${counts.failed.length} failed`);
|
|
1040
|
+
if (counts.stopped.length > 0) parts.push(`${counts.stopped.length} stopped`);
|
|
1005
1041
|
if (counts.paused.length > 0) parts.push(`${counts.paused.length} paused`);
|
|
1006
1042
|
if (!hasActive && counts.complete.length > 0) parts.push(`${counts.complete.length}/${jobs.length} done`);
|
|
1007
1043
|
return [truncLine(`${theme.fg(hasActive ? "accent" : "dim", glyph)} ${theme.fg(hasActive ? "accent" : "dim", "subagents")} (${parts.join(", ") || `${jobs.length} total`})`, width)];
|
|
@@ -1065,6 +1101,7 @@ function progressiveHeaderLine(jobs: AsyncJobState[], theme: Theme, width: numbe
|
|
|
1065
1101
|
if (counts.queued.length > 0) parts.push(`${counts.queued.length} queued`);
|
|
1066
1102
|
if (!hasActive) {
|
|
1067
1103
|
if (counts.failed.length > 0) parts.push(`${counts.failed.length} failed`);
|
|
1104
|
+
if (counts.stopped.length > 0) parts.push(`${counts.stopped.length} stopped`);
|
|
1068
1105
|
if (counts.paused.length > 0) parts.push(`${counts.paused.length} paused`);
|
|
1069
1106
|
if (counts.complete.length > 0) parts.push(`${counts.complete.length}/${jobs.length} done`);
|
|
1070
1107
|
}
|
|
@@ -1089,7 +1126,7 @@ function progressiveHiddenLine(hiddenJobs: AsyncJobState[], theme: Theme, width:
|
|
|
1089
1126
|
const parts: string[] = [];
|
|
1090
1127
|
if (counts.running.length > 0) parts.push(`${counts.running.length} running`);
|
|
1091
1128
|
if (counts.queued.length > 0) parts.push(`${counts.queued.length} queued`);
|
|
1092
|
-
const finished = counts.complete.length + counts.failed.length + counts.paused.length;
|
|
1129
|
+
const finished = counts.complete.length + counts.failed.length + counts.paused.length + counts.stopped.length;
|
|
1093
1130
|
if (finished > 0) parts.push(`${finished} finished`);
|
|
1094
1131
|
return truncLine(theme.fg("dim", ` +${hiddenJobs.length} more${parts.length ? ` (${parts.join(", ")})` : ""}`), width);
|
|
1095
1132
|
}
|
|
@@ -1292,11 +1329,17 @@ function renderSingleCompact(d: Details, r: Details["results"][number], theme: T
|
|
|
1292
1329
|
c.addChild(new Text(truncLine(theme.fg("dim", ` ⎿ ${activity}`), width), 0, 0));
|
|
1293
1330
|
const liveStatus = buildLiveStatusLine(r.progress, progressSnapshotNow);
|
|
1294
1331
|
if (liveStatus && liveStatus !== activity) c.addChild(new Text(truncLine(theme.fg("dim", ` ${liveStatus}`), width), 0, 0));
|
|
1332
|
+
for (const nestedLine of formatNestedWidgetLines(r.children, theme, width, false, progressSnapshotNow)) {
|
|
1333
|
+
c.addChild(new Text(truncLine(` ${nestedLine}`, width), 0, 0));
|
|
1334
|
+
}
|
|
1295
1335
|
c.addChild(new Text(truncLine(theme.fg("accent", ` ${liveDetailHintText()}`), width), 0, 0));
|
|
1296
1336
|
if (r.artifactPaths) c.addChild(new Text(truncLine(theme.fg("dim", ` output: ${shortenPath(r.artifactPaths.outputPath)}`), width), 0, 0));
|
|
1297
1337
|
return c;
|
|
1298
1338
|
}
|
|
1299
1339
|
|
|
1340
|
+
for (const nestedLine of formatNestedWidgetLines(r.children, theme, width, false, r.progress?.lastActivityAt)) {
|
|
1341
|
+
c.addChild(new Text(truncLine(` ${nestedLine}`, width), 0, 0));
|
|
1342
|
+
}
|
|
1300
1343
|
c.addChild(new Text(truncLine(theme.fg("dim", ` ⎿ ${resultStatusLine(r, output)}`), width), 0, 0));
|
|
1301
1344
|
const preview = firstOutputLine(output);
|
|
1302
1345
|
if (preview && r.exitCode === 0 && !hasEmptyTextOutputWithoutOutputTarget(r.task, output)) {
|
|
@@ -1312,7 +1355,9 @@ function renderMultiCompact(d: Details, theme: Theme, frame?: number): Component
|
|
|
1312
1355
|
const hasRunning = d.progress?.some((p) => p.status === "running")
|
|
1313
1356
|
|| d.results.some((r) => r.progress?.status === "running")
|
|
1314
1357
|
|| workflowGraphHasStatus(d, ["running"]);
|
|
1315
|
-
const
|
|
1358
|
+
const stopped = d.results.some((r) => r.stopped && r.progress?.status !== "running")
|
|
1359
|
+
|| workflowGraphHasStatus(d, ["stopped"]);
|
|
1360
|
+
const failed = d.results.some((r) => !r.stopped && r.exitCode !== 0 && r.progress?.status !== "running")
|
|
1316
1361
|
|| workflowGraphHasStatus(d, ["failed"]);
|
|
1317
1362
|
const paused = d.results.some((r) => (r.interrupted || r.detached) && r.progress?.status !== "running")
|
|
1318
1363
|
|| workflowGraphHasStatus(d, ["paused", "detached"]);
|
|
@@ -1335,8 +1380,10 @@ function renderMultiCompact(d: Details, theme: Theme, frame?: number): Component
|
|
|
1335
1380
|
const stats = statJoin(theme, [multiLabel.headerLabel, formatProgressStats(theme, totalSummary), formatTotalCostStat(d.totalCost)]);
|
|
1336
1381
|
const glyph = hasRunning
|
|
1337
1382
|
? theme.fg("accent", runningGlyph(frame !== undefined ? (runningSeed(progressRunningSeed(totalSummary), d.currentStepIndex) ?? 0) + frame : runningSeed(progressRunningSeed(totalSummary), d.currentStepIndex)))
|
|
1338
|
-
:
|
|
1339
|
-
? theme.fg("
|
|
1383
|
+
: stopped
|
|
1384
|
+
? theme.fg("warning", "■")
|
|
1385
|
+
: failed
|
|
1386
|
+
? theme.fg("error", "✗")
|
|
1340
1387
|
: paused
|
|
1341
1388
|
? theme.fg("warning", "■")
|
|
1342
1389
|
: theme.fg("success", "✓");
|
|
@@ -1388,10 +1435,18 @@ function renderMultiCompact(d: Details, theme: Theme, frame?: number): Component
|
|
|
1388
1435
|
if (rRunning && rProg && "status" in rProg) {
|
|
1389
1436
|
const activity = compactCurrentActivity(rProg);
|
|
1390
1437
|
c.addChild(new Text(truncLine(theme.fg("dim", ` ⎿ ${activity}`), width), 0, 0));
|
|
1438
|
+
for (const nestedLine of formatNestedWidgetLines(r.children, theme, width, false, snapshotNowForProgress(rProg))) {
|
|
1439
|
+
c.addChild(new Text(truncLine(` ${nestedLine}`, width), 0, 0));
|
|
1440
|
+
}
|
|
1391
1441
|
c.addChild(new Text(truncLine(theme.fg("accent", ` ${liveDetailHintText()}`), width), 0, 0));
|
|
1392
1442
|
} else if (!rPending && (r.exitCode !== 0 || r.interrupted || r.detached || hasEmptyTextOutputWithoutOutputTarget(r.task, output))) {
|
|
1393
1443
|
c.addChild(new Text(truncLine(theme.fg(r.exitCode !== 0 ? "error" : "dim", ` ⎿ ${resultStatusLine(r, output)}`), width), 0, 0));
|
|
1394
1444
|
}
|
|
1445
|
+
if (!rRunning && !rPending) {
|
|
1446
|
+
for (const nestedLine of formatNestedWidgetLines(r.children, theme, width, false, r.progress?.lastActivityAt)) {
|
|
1447
|
+
c.addChild(new Text(truncLine(` ${nestedLine}`, width), 0, 0));
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1395
1450
|
const outputTarget = extractOutputTarget(r.task);
|
|
1396
1451
|
if (outputTarget) c.addChild(new Text(truncLine(theme.fg("dim", ` output: ${outputTarget}`), width), 0, 0));
|
|
1397
1452
|
if (r.artifactPaths) c.addChild(new Text(truncLine(theme.fg("dim", ` output: ${shortenPath(r.artifactPaths.outputPath)}`), width), 0, 0));
|
|
@@ -1462,6 +1517,9 @@ export function renderSubagentResult(
|
|
|
1462
1517
|
|
|
1463
1518
|
if (isRunning && r.progress) {
|
|
1464
1519
|
const progressSnapshotNow = snapshotNowForProgress(r.progress);
|
|
1520
|
+
for (const nestedLine of formatNestedWidgetLines(r.children, theme, w, true, progressSnapshotNow, 12)) {
|
|
1521
|
+
c.addChild(new Text(fit(` ${nestedLine}`), 0, 0));
|
|
1522
|
+
}
|
|
1465
1523
|
const toolLine = formatCurrentToolLine(r.progress, w, expanded, progressSnapshotNow);
|
|
1466
1524
|
if (toolLine) {
|
|
1467
1525
|
c.addChild(new Text(fit(theme.fg("warning", `> ${toolLine}`)), 0, 0));
|
|
@@ -1489,6 +1547,10 @@ export function renderSubagentResult(
|
|
|
1489
1547
|
if (toolLine || liveStatusLine || r.progress.recentTools?.length || r.progress.recentOutput?.length || r.artifactPaths) {
|
|
1490
1548
|
c.addChild(new Spacer(1));
|
|
1491
1549
|
}
|
|
1550
|
+
} else {
|
|
1551
|
+
for (const nestedLine of formatNestedWidgetLines(r.children, theme, w, true, r.progress?.lastActivityAt, 8)) {
|
|
1552
|
+
c.addChild(new Text(fit(` ${nestedLine}`), 0, 0));
|
|
1553
|
+
}
|
|
1492
1554
|
}
|
|
1493
1555
|
|
|
1494
1556
|
if (expanded) {
|
|
@@ -1533,6 +1595,7 @@ export function renderSubagentResult(
|
|
|
1533
1595
|
&& hasEmptyTextOutputWithoutOutputTarget(r.task, getSingleResultOutput(r)),
|
|
1534
1596
|
);
|
|
1535
1597
|
const hasWorkflowFailure = workflowGraphHasStatus(d, ["failed"]);
|
|
1598
|
+
const hasWorkflowStop = d.results.some((r) => r.stopped && r.progress?.status !== "running") || workflowGraphHasStatus(d, ["stopped"]);
|
|
1536
1599
|
const hasWorkflowPause = workflowGraphHasStatus(d, ["paused", "detached"]);
|
|
1537
1600
|
const icon = hasRunning
|
|
1538
1601
|
? theme.fg("warning", "running")
|
|
@@ -1540,8 +1603,10 @@ export function renderSubagentResult(
|
|
|
1540
1603
|
? theme.fg("warning", "warning")
|
|
1541
1604
|
: hasWorkflowFailure
|
|
1542
1605
|
? theme.fg("error", "failed")
|
|
1543
|
-
:
|
|
1544
|
-
? theme.fg("warning", "
|
|
1606
|
+
: hasWorkflowStop
|
|
1607
|
+
? theme.fg("warning", "stopped")
|
|
1608
|
+
: hasWorkflowPause
|
|
1609
|
+
? theme.fg("warning", "paused")
|
|
1545
1610
|
: ok === d.results.length
|
|
1546
1611
|
? theme.fg("success", "ok")
|
|
1547
1612
|
: theme.fg("error", "failed");
|
|
@@ -1707,6 +1772,9 @@ export function renderSubagentResult(
|
|
|
1707
1772
|
if (liveStatusLine) {
|
|
1708
1773
|
c.addChild(new Text(fit(theme.fg("accent", ` ${liveStatusLine}`)), 0, 0));
|
|
1709
1774
|
}
|
|
1775
|
+
for (const nestedLine of formatNestedWidgetLines(r.children, theme, w, true, progressSnapshotNow, 8)) {
|
|
1776
|
+
c.addChild(new Text(fit(` ${nestedLine}`), 0, 0));
|
|
1777
|
+
}
|
|
1710
1778
|
c.addChild(new Text(fit(theme.fg("accent", ` ${liveDetailHintText()}`)), 0, 0));
|
|
1711
1779
|
if (r.artifactPaths) {
|
|
1712
1780
|
c.addChild(new Text(fit(theme.fg("dim", ` artifacts: ${shortenPath(r.artifactPaths.outputPath)}`)), 0, 0));
|
|
@@ -1726,6 +1794,12 @@ export function renderSubagentResult(
|
|
|
1726
1794
|
}
|
|
1727
1795
|
}
|
|
1728
1796
|
|
|
1797
|
+
if (!rRunning) {
|
|
1798
|
+
for (const nestedLine of formatNestedWidgetLines(r.children, theme, w, true, r.progress?.lastActivityAt, 8)) {
|
|
1799
|
+
c.addChild(new Text(fit(` ${nestedLine}`), 0, 0));
|
|
1800
|
+
}
|
|
1801
|
+
}
|
|
1802
|
+
|
|
1729
1803
|
if (!rRunning && r.artifactPaths) {
|
|
1730
1804
|
c.addChild(new Text(fit(theme.fg("dim", ` artifacts: ${shortenPath(r.artifactPaths.outputPath)}`)), 0, 0));
|
|
1731
1805
|
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import * as fs from "node:fs";
|
|
4
|
+
import * as path from "node:path";
|
|
5
|
+
|
|
6
|
+
const IGNORED_CHANGE_PREFIXES = [".pi-subagents/", "tmp/", "node_modules/"];
|
|
7
|
+
const IGNORED_CHANGE_PATHS = new Set([".pi-subagents", "tmp", "node_modules"]);
|
|
8
|
+
|
|
9
|
+
export interface WatchdogRepoChangeSignature {
|
|
10
|
+
root: string;
|
|
11
|
+
key: string;
|
|
12
|
+
changedPaths: string[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function git(cwd: string, args: string[]): string | undefined {
|
|
16
|
+
const result = spawnSync("git", ["-C", cwd, ...args], { encoding: "utf-8", maxBuffer: 10 * 1024 * 1024 });
|
|
17
|
+
if (result.status !== 0) return undefined;
|
|
18
|
+
return result.stdout;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function normalizeRelPath(value: string): string {
|
|
22
|
+
return value.replaceAll(path.sep, "/").replace(/^\.\//, "");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function ignoredRelPath(relPath: string): boolean {
|
|
26
|
+
const normalized = normalizeRelPath(relPath);
|
|
27
|
+
return IGNORED_CHANGE_PATHS.has(normalized) || IGNORED_CHANGE_PREFIXES.some((prefix) => normalized.startsWith(prefix));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function hashFile(filePath: string): string {
|
|
31
|
+
return createHash("sha256").update(fs.readFileSync(filePath)).digest("hex");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function hashPath(root: string, relPath: string): unknown {
|
|
35
|
+
const normalized = normalizeRelPath(relPath);
|
|
36
|
+
const fullPath = path.join(root, normalized);
|
|
37
|
+
let stat: fs.Stats;
|
|
38
|
+
try {
|
|
39
|
+
stat = fs.lstatSync(fullPath);
|
|
40
|
+
} catch (error) {
|
|
41
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return { path: normalized, state: "deleted" };
|
|
42
|
+
throw error;
|
|
43
|
+
}
|
|
44
|
+
if (stat.isSymbolicLink()) {
|
|
45
|
+
return { path: normalized, state: "symlink", target: fs.readlinkSync(fullPath) };
|
|
46
|
+
}
|
|
47
|
+
if (stat.isDirectory()) {
|
|
48
|
+
const entries = fs.readdirSync(fullPath)
|
|
49
|
+
.map((entry) => normalizeRelPath(path.posix.join(normalized, entry)))
|
|
50
|
+
.filter((entry) => !ignoredRelPath(entry))
|
|
51
|
+
.sort();
|
|
52
|
+
return { path: normalized, state: "dir", entries: entries.map((entry) => hashPath(root, entry)) };
|
|
53
|
+
}
|
|
54
|
+
if (stat.isFile()) {
|
|
55
|
+
return { path: normalized, state: "file", mode: stat.mode & 0o777, size: stat.size, hash: hashFile(fullPath) };
|
|
56
|
+
}
|
|
57
|
+
return { path: normalized, state: "other", mode: stat.mode };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function parsePorcelainZ(raw: string): Array<{ status: string; paths: string[] }> {
|
|
61
|
+
const tokens = raw.split("\0").filter(Boolean);
|
|
62
|
+
const entries: Array<{ status: string; paths: string[] }> = [];
|
|
63
|
+
for (let index = 0; index < tokens.length; index++) {
|
|
64
|
+
const token = tokens[index]!;
|
|
65
|
+
if (token.length < 4) continue;
|
|
66
|
+
const status = token.slice(0, 2);
|
|
67
|
+
const relPath = token.slice(3);
|
|
68
|
+
const paths = [relPath];
|
|
69
|
+
if (status[0] === "R" || status[0] === "C") {
|
|
70
|
+
const originalPath = tokens[++index];
|
|
71
|
+
if (originalPath) paths.push(originalPath);
|
|
72
|
+
}
|
|
73
|
+
entries.push({ status, paths });
|
|
74
|
+
}
|
|
75
|
+
return entries;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function computeWatchdogRepoChangeSignature(cwd: string): WatchdogRepoChangeSignature | undefined {
|
|
79
|
+
const root = git(cwd, ["rev-parse", "--show-toplevel"])?.trim();
|
|
80
|
+
if (!root) return undefined;
|
|
81
|
+
const statusOutput = git(root, ["status", "--porcelain=v1", "-z", "--untracked-files=all"]);
|
|
82
|
+
if (statusOutput === undefined) return undefined;
|
|
83
|
+
const entries = parsePorcelainZ(statusOutput)
|
|
84
|
+
.map((entry) => ({
|
|
85
|
+
status: entry.status,
|
|
86
|
+
paths: entry.paths.map(normalizeRelPath).filter((relPath) => !ignoredRelPath(relPath)),
|
|
87
|
+
}))
|
|
88
|
+
.filter((entry) => entry.paths.length > 0)
|
|
89
|
+
.sort((a, b) => `${a.status} ${a.paths.join("\0")}`.localeCompare(`${b.status} ${b.paths.join("\0")}`));
|
|
90
|
+
const changedPaths = [...new Set(entries.flatMap((entry) => entry.paths))].sort();
|
|
91
|
+
const payload = entries.map((entry) => ({
|
|
92
|
+
status: entry.status,
|
|
93
|
+
paths: entry.paths,
|
|
94
|
+
content: entry.paths.map((relPath) => hashPath(root, relPath)),
|
|
95
|
+
}));
|
|
96
|
+
const key = createHash("sha256").update(JSON.stringify(payload)).digest("hex");
|
|
97
|
+
return { root, key, changedPaths };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function toolNameFromMessage(message: Record<string, unknown>): string {
|
|
101
|
+
const value = message.toolName ?? message.name;
|
|
102
|
+
return typeof value === "string" ? value : "";
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function toolResultSucceeded(message: Record<string, unknown>): boolean {
|
|
106
|
+
return message.isError !== true && message.error === undefined;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function messageIndicatesRepoEdit(message: unknown): boolean {
|
|
110
|
+
if (!message || typeof message !== "object") return false;
|
|
111
|
+
const input = message as Record<string, unknown>;
|
|
112
|
+
const role = input.role;
|
|
113
|
+
if (role !== "toolResult" && role !== "tool") return false;
|
|
114
|
+
const toolName = toolNameFromMessage(input);
|
|
115
|
+
return (toolName === "edit" || toolName === "write") && toolResultSucceeded(input);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function eventIndicatesRepoEdit(event: unknown): boolean {
|
|
119
|
+
if (!event || typeof event !== "object") return false;
|
|
120
|
+
const input = event as Record<string, unknown>;
|
|
121
|
+
if (input.type === "turn_end" || input.event === "turn_end") {
|
|
122
|
+
return [input.message, ...(Array.isArray(input.toolResults) ? input.toolResults : [])].some(messageIndicatesRepoEdit);
|
|
123
|
+
}
|
|
124
|
+
if (input.type === "tool_result" || input.event === "tool_result") return messageIndicatesRepoEdit({ role: "toolResult", ...input });
|
|
125
|
+
if (input.type !== "tool_result_end" && input.event !== "tool_result_end") return false;
|
|
126
|
+
return messageIndicatesRepoEdit(input.message);
|
|
127
|
+
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import type { ResolvedWatchdogConfig, WatchdogLspConfig } from "./types.ts";
|
|
2
|
+
|
|
3
|
+
export const CHILD_WATCHDOG_CONFIG_ENV = "PI_SUBAGENT_WATCHDOG_CHILD_CONFIG";
|
|
4
|
+
export const CHILD_WATCHDOG_STATUS_EVENT = "subagent.watchdog.status";
|
|
5
|
+
|
|
6
|
+
export const CHILD_WATCHDOG_PHASES = ["idle", "reviewing", "autofollow", "settling", "stale", "failed"] as const;
|
|
7
|
+
export type ChildWatchdogPhase = typeof CHILD_WATCHDOG_PHASES[number];
|
|
8
|
+
|
|
9
|
+
export interface ChildWatchdogConfig {
|
|
10
|
+
enabled: boolean;
|
|
11
|
+
runId?: string;
|
|
12
|
+
agent?: string;
|
|
13
|
+
childIndex?: number;
|
|
14
|
+
watchdogTailTimeoutMs: number;
|
|
15
|
+
agentEndTimeoutMs: number;
|
|
16
|
+
maxWarnings: number | null;
|
|
17
|
+
model?: string;
|
|
18
|
+
thinking?: string | false;
|
|
19
|
+
lsp: WatchdogLspConfig;
|
|
20
|
+
autoFollowBlockers: boolean;
|
|
21
|
+
autoFollowMaxAttempts: number | null;
|
|
22
|
+
stalemateRepeats: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface ChildWatchdogStatusEvent {
|
|
26
|
+
type: typeof CHILD_WATCHDOG_STATUS_EVENT;
|
|
27
|
+
runId?: string;
|
|
28
|
+
agent?: string;
|
|
29
|
+
childIndex?: number;
|
|
30
|
+
stepIndex?: number;
|
|
31
|
+
seq: number;
|
|
32
|
+
phase: ChildWatchdogPhase;
|
|
33
|
+
ts: number;
|
|
34
|
+
followUpPending: boolean;
|
|
35
|
+
reason?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface ChildWatchdogStateSnapshot {
|
|
39
|
+
phase: ChildWatchdogPhase;
|
|
40
|
+
seq: number;
|
|
41
|
+
lastUpdate: number;
|
|
42
|
+
followUpPending: boolean;
|
|
43
|
+
reason?: string;
|
|
44
|
+
timedOut?: boolean;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function resolveChildWatchdogConfig(input: {
|
|
48
|
+
config: ResolvedWatchdogConfig;
|
|
49
|
+
agent?: string;
|
|
50
|
+
runId?: string;
|
|
51
|
+
childIndex?: number;
|
|
52
|
+
}): ChildWatchdogConfig | undefined {
|
|
53
|
+
const override = input.agent ? input.config.children.overrides[input.agent] : undefined;
|
|
54
|
+
const enabled = input.config.enabled && (override?.enabled ?? input.config.children.enabled);
|
|
55
|
+
if (!enabled) return undefined;
|
|
56
|
+
const model = override?.model ?? input.config.children.model;
|
|
57
|
+
const thinking = override?.thinking ?? input.config.children.thinking;
|
|
58
|
+
return {
|
|
59
|
+
enabled: true,
|
|
60
|
+
...(input.runId ? { runId: input.runId } : {}),
|
|
61
|
+
...(input.agent ? { agent: input.agent } : {}),
|
|
62
|
+
...(input.childIndex !== undefined ? { childIndex: input.childIndex } : {}),
|
|
63
|
+
watchdogTailTimeoutMs: input.config.children.watchdogTailTimeoutMs,
|
|
64
|
+
agentEndTimeoutMs: input.config.agentEndTimeoutMs,
|
|
65
|
+
maxWarnings: input.config.maxWarnings,
|
|
66
|
+
...(model ? { model } : {}),
|
|
67
|
+
...(thinking !== undefined ? { thinking } : {}),
|
|
68
|
+
lsp: { ...input.config.lsp },
|
|
69
|
+
autoFollowBlockers: input.config.children.autoFollow.blockers,
|
|
70
|
+
autoFollowMaxAttempts: input.config.children.autoFollow.maxAttempts,
|
|
71
|
+
stalemateRepeats: input.config.children.autoFollow.stalemateRepeats,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function encodeChildWatchdogConfig(config: ChildWatchdogConfig | undefined): string | undefined {
|
|
76
|
+
return config ? JSON.stringify(config) : undefined;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function childConfigObject(value: unknown, field: string): Record<string, unknown> {
|
|
80
|
+
if (value && typeof value === "object" && !Array.isArray(value)) return value as Record<string, unknown>;
|
|
81
|
+
throw new Error(`Invalid child watchdog config: ${field} must be an object.`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function childConfigOptionalString(input: Record<string, unknown>, field: string): string | undefined {
|
|
85
|
+
if (!(field in input)) return undefined;
|
|
86
|
+
const value = input[field];
|
|
87
|
+
if (typeof value === "string" && value.trim()) return value;
|
|
88
|
+
throw new Error(`Invalid child watchdog config: ${field} must be a non-empty string.`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function childConfigOptionalIndex(input: Record<string, unknown>, field: string): number | undefined {
|
|
92
|
+
if (!(field in input)) return undefined;
|
|
93
|
+
const value = input[field];
|
|
94
|
+
if (typeof value === "number" && Number.isInteger(value) && value >= 0) return value;
|
|
95
|
+
throw new Error(`Invalid child watchdog config: ${field} must be a non-negative integer.`);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function childConfigPositiveInteger(input: Record<string, unknown>, field: string): number {
|
|
99
|
+
const value = input[field];
|
|
100
|
+
if (typeof value === "number" && Number.isInteger(value) && value >= 1) return value;
|
|
101
|
+
throw new Error(`Invalid child watchdog config: ${field} must be a positive integer.`);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function childConfigNullableNonNegativeInteger(input: Record<string, unknown>, field: string): number | null {
|
|
105
|
+
const value = input[field];
|
|
106
|
+
if (value === null) return null;
|
|
107
|
+
if (typeof value === "number" && Number.isInteger(value) && value >= 0) return value;
|
|
108
|
+
throw new Error(`Invalid child watchdog config: ${field} must be null or a non-negative integer.`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function childConfigBoolean(input: Record<string, unknown>, field: string): boolean {
|
|
112
|
+
const value = input[field];
|
|
113
|
+
if (typeof value === "boolean") return value;
|
|
114
|
+
throw new Error(`Invalid child watchdog config: ${field} must be a boolean.`);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function childConfigLsp(value: unknown): WatchdogLspConfig {
|
|
118
|
+
const input = childConfigObject(value, "lsp");
|
|
119
|
+
if (typeof input.enabled !== "boolean") throw new Error("Invalid child watchdog config: lsp.enabled must be a boolean.");
|
|
120
|
+
if (typeof input.timeoutMs !== "number" || !Number.isInteger(input.timeoutMs) || input.timeoutMs < 1) {
|
|
121
|
+
throw new Error("Invalid child watchdog config: lsp.timeoutMs must be a positive integer.");
|
|
122
|
+
}
|
|
123
|
+
if (typeof input.maxFiles !== "number" || !Number.isInteger(input.maxFiles) || input.maxFiles < 1) {
|
|
124
|
+
throw new Error("Invalid child watchdog config: lsp.maxFiles must be a positive integer.");
|
|
125
|
+
}
|
|
126
|
+
if (typeof input.maxDiagnostics !== "number" || !Number.isInteger(input.maxDiagnostics) || input.maxDiagnostics < 0) {
|
|
127
|
+
throw new Error("Invalid child watchdog config: lsp.maxDiagnostics must be a non-negative integer.");
|
|
128
|
+
}
|
|
129
|
+
return {
|
|
130
|
+
enabled: input.enabled,
|
|
131
|
+
timeoutMs: input.timeoutMs,
|
|
132
|
+
maxFiles: input.maxFiles,
|
|
133
|
+
maxDiagnostics: input.maxDiagnostics,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function decodeChildWatchdogConfig(raw: string | undefined): ChildWatchdogConfig | undefined {
|
|
138
|
+
if (!raw) return undefined;
|
|
139
|
+
const parsed = childConfigObject(JSON.parse(raw), "root");
|
|
140
|
+
if (parsed.enabled === false) return undefined;
|
|
141
|
+
if (parsed.enabled !== true) throw new Error("Invalid child watchdog config: enabled must be true or false.");
|
|
142
|
+
const thinking = parsed.thinking;
|
|
143
|
+
if (thinking !== undefined && typeof thinking !== "string" && thinking !== false) {
|
|
144
|
+
throw new Error("Invalid child watchdog config: thinking must be a string or false.");
|
|
145
|
+
}
|
|
146
|
+
const runId = childConfigOptionalString(parsed, "runId");
|
|
147
|
+
const agent = childConfigOptionalString(parsed, "agent");
|
|
148
|
+
const childIndex = childConfigOptionalIndex(parsed, "childIndex");
|
|
149
|
+
const model = childConfigOptionalString(parsed, "model");
|
|
150
|
+
return {
|
|
151
|
+
enabled: true,
|
|
152
|
+
...(runId ? { runId } : {}),
|
|
153
|
+
...(agent ? { agent } : {}),
|
|
154
|
+
...(childIndex !== undefined ? { childIndex } : {}),
|
|
155
|
+
watchdogTailTimeoutMs: childConfigPositiveInteger(parsed, "watchdogTailTimeoutMs"),
|
|
156
|
+
agentEndTimeoutMs: childConfigPositiveInteger(parsed, "agentEndTimeoutMs"),
|
|
157
|
+
maxWarnings: childConfigNullableNonNegativeInteger(parsed, "maxWarnings"),
|
|
158
|
+
...(model ? { model } : {}),
|
|
159
|
+
...(thinking !== undefined ? { thinking: thinking as string | false } : {}),
|
|
160
|
+
lsp: childConfigLsp(parsed.lsp),
|
|
161
|
+
autoFollowBlockers: childConfigBoolean(parsed, "autoFollowBlockers"),
|
|
162
|
+
autoFollowMaxAttempts: childConfigNullableNonNegativeInteger(parsed, "autoFollowMaxAttempts"),
|
|
163
|
+
stalemateRepeats: childConfigPositiveInteger(parsed, "stalemateRepeats"),
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function isChildWatchdogStatusEvent(value: unknown): value is ChildWatchdogStatusEvent {
|
|
168
|
+
if (!value || typeof value !== "object") return false;
|
|
169
|
+
const event = value as Partial<ChildWatchdogStatusEvent>;
|
|
170
|
+
return event.type === CHILD_WATCHDOG_STATUS_EVENT
|
|
171
|
+
&& typeof event.seq === "number"
|
|
172
|
+
&& Number.isInteger(event.seq)
|
|
173
|
+
&& event.seq >= 0
|
|
174
|
+
&& typeof event.ts === "number"
|
|
175
|
+
&& Number.isFinite(event.ts)
|
|
176
|
+
&& typeof event.followUpPending === "boolean"
|
|
177
|
+
&& typeof event.phase === "string"
|
|
178
|
+
&& (CHILD_WATCHDOG_PHASES as readonly string[]).includes(event.phase);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function childWatchdogIsActive(snapshot: ChildWatchdogStateSnapshot | undefined): boolean {
|
|
182
|
+
if (!snapshot) return false;
|
|
183
|
+
return snapshot.followUpPending || snapshot.phase === "reviewing" || snapshot.phase === "autofollow" || snapshot.phase === "settling";
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export function acceptChildWatchdogEvent(input: {
|
|
187
|
+
current: ChildWatchdogStateSnapshot | undefined;
|
|
188
|
+
event: ChildWatchdogStatusEvent;
|
|
189
|
+
runId?: string;
|
|
190
|
+
agent?: string;
|
|
191
|
+
childIndex?: number;
|
|
192
|
+
}): ChildWatchdogStateSnapshot | undefined {
|
|
193
|
+
if (input.runId !== undefined && input.event.runId !== input.runId) return undefined;
|
|
194
|
+
if (input.agent !== undefined && input.event.agent !== input.agent) return undefined;
|
|
195
|
+
const eventIndex = input.event.childIndex ?? input.event.stepIndex;
|
|
196
|
+
if (input.childIndex !== undefined && eventIndex !== input.childIndex) return undefined;
|
|
197
|
+
if (input.current && input.event.seq <= input.current.seq) return undefined;
|
|
198
|
+
return {
|
|
199
|
+
phase: input.event.phase,
|
|
200
|
+
seq: input.event.seq,
|
|
201
|
+
lastUpdate: input.event.ts,
|
|
202
|
+
followUpPending: input.event.followUpPending,
|
|
203
|
+
...(input.event.reason ? { reason: input.event.reason } : {}),
|
|
204
|
+
};
|
|
205
|
+
}
|