@dungle-scrubs/tallow 0.8.26 → 0.8.27
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/dist/config.d.ts +1 -1
- package/dist/config.js +1 -1
- package/dist/interactive-mode-patch.d.ts +1 -0
- package/dist/interactive-mode-patch.d.ts.map +1 -1
- package/dist/interactive-mode-patch.js +40 -1
- package/dist/interactive-mode-patch.js.map +1 -1
- package/dist/pid-manager.d.ts +2 -9
- package/dist/pid-manager.d.ts.map +1 -1
- package/dist/pid-manager.js +1 -58
- package/dist/pid-manager.js.map +1 -1
- package/dist/pid-schema.d.ts +51 -0
- package/dist/pid-schema.d.ts.map +1 -0
- package/dist/pid-schema.js +70 -0
- package/dist/pid-schema.js.map +1 -0
- package/dist/sdk.js +4 -8
- package/dist/sdk.js.map +1 -1
- package/extensions/__integration__/audit-findings.test.ts +309 -0
- package/extensions/__integration__/tasks-runtime.test.ts +63 -12
- package/extensions/_shared/lazy-init.ts +88 -3
- package/extensions/_shared/pid-registry.ts +8 -82
- package/extensions/cheatsheet/__tests__/cheatsheet.test.ts +47 -0
- package/extensions/clear/__tests__/clear.test.ts +38 -0
- package/extensions/git-status/__tests__/git-status.test.ts +32 -0
- package/extensions/mcp-adapter-tool/index.ts +1 -1
- package/extensions/minimal-skill-display/__tests__/minimal-skill-display.test.ts +20 -0
- package/extensions/permissions/__tests__/permissions.test.ts +213 -0
- package/extensions/progress-indicator/__tests__/progress-indicator.test.ts +104 -0
- package/extensions/random-spinner/__tests__/random-spinner.test.ts +35 -0
- package/extensions/show-system-prompt/__tests__/show-system-prompt.test.ts +51 -0
- package/extensions/subagent-tool/__tests__/presentation-rendering.test.ts +5 -4
- package/extensions/subagent-tool/__tests__/process-liveness.test.ts +51 -0
- package/extensions/subagent-tool/__tests__/subprocess-args.test.ts +120 -0
- package/extensions/subagent-tool/formatting.ts +2 -0
- package/extensions/subagent-tool/index.ts +156 -95
- package/extensions/subagent-tool/process.ts +126 -32
- package/extensions/tasks/commands/register-tasks-extension.ts +64 -20
- package/extensions/tasks/extension.json +1 -0
- package/extensions/tasks/index.ts +2 -12
- package/extensions/tasks/state/index.ts +26 -0
- package/extensions/teams-tool/dashboard.ts +13 -1
- package/extensions/teams-tool/tools/register-extension.ts +10 -2
- package/extensions/upstream-check/__tests__/upstream-check.test.ts +49 -0
- package/extensions/wezterm-notify/__tests__/index.test.ts +49 -11
- package/extensions/wezterm-notify/index.ts +5 -3
- package/extensions/write-tool-enhanced/__tests__/write-tool-enhanced.test.ts +296 -0
- package/package.json +3 -2
- package/runtime/pid-schema.ts +13 -0
- package/skills/tallow-expert/SKILL.md +1 -1
|
@@ -50,6 +50,7 @@ import {
|
|
|
50
50
|
applyBackgroundResultRetention,
|
|
51
51
|
mapWithConcurrencyLimit,
|
|
52
52
|
type OnUpdateCallback,
|
|
53
|
+
resolveRetryPhaseTimeoutMs,
|
|
53
54
|
runSingleAgent,
|
|
54
55
|
setPiRef,
|
|
55
56
|
setTelemetryHandle,
|
|
@@ -181,6 +182,25 @@ export default function (pi: ExtensionAPI) {
|
|
|
181
182
|
}
|
|
182
183
|
});
|
|
183
184
|
|
|
185
|
+
// Kill all running background subagents on session shutdown (SIGTERM during user input).
|
|
186
|
+
// Unlike agent_end, this fires when the entire session is exiting — we don't need to
|
|
187
|
+
// retain results, just ensure orphaned subagent processes are terminated promptly.
|
|
188
|
+
pi.on("session_shutdown", async () => {
|
|
189
|
+
let mutated = false;
|
|
190
|
+
for (const [_id, bg] of backgroundSubagents) {
|
|
191
|
+
if (bg.status === "running" && bg.process && !bg.process.killed) {
|
|
192
|
+
bg.process.kill("SIGTERM");
|
|
193
|
+
bg.completedAt = Date.now();
|
|
194
|
+
bg.status = "failed";
|
|
195
|
+
bg.result.stopReason = "shutdown";
|
|
196
|
+
mutated = true;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
if (mutated) {
|
|
200
|
+
publishSubagentSnapshot(pi.events);
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
|
|
184
204
|
pi.registerTool({
|
|
185
205
|
name: "subagent",
|
|
186
206
|
label: "subagent",
|
|
@@ -674,6 +694,7 @@ async function executeParallel(
|
|
|
674
694
|
const allResults: SingleResult[] = new Array(tasks.length);
|
|
675
695
|
|
|
676
696
|
// Initialize placeholder results
|
|
697
|
+
const parallelStartTime = Date.now();
|
|
677
698
|
for (let i = 0; i < tasks.length; i++) {
|
|
678
699
|
allResults[i] = {
|
|
679
700
|
agent: tasks[i].agent,
|
|
@@ -692,6 +713,7 @@ async function executeParallel(
|
|
|
692
713
|
turns: 0,
|
|
693
714
|
denials: 0,
|
|
694
715
|
},
|
|
716
|
+
startTime: parallelStartTime,
|
|
695
717
|
};
|
|
696
718
|
}
|
|
697
719
|
|
|
@@ -788,12 +810,34 @@ async function executeParallel(
|
|
|
788
810
|
const retrySummaryLines: string[] = [];
|
|
789
811
|
const totalRetries = initialStalledIndexes.length;
|
|
790
812
|
|
|
813
|
+
// Cap the entire retry phase with a wall-clock timeout so N sequential
|
|
814
|
+
// retries × per-worker watchdog timeouts don't block the parent for 30+ min.
|
|
815
|
+
const retryPhaseTimeoutMs = resolveRetryPhaseTimeoutMs();
|
|
816
|
+
const retryAbort = new AbortController();
|
|
817
|
+
const retryTimer = setTimeout(() => retryAbort.abort(), retryPhaseTimeoutMs);
|
|
818
|
+
// Propagate parent abort to the retry phase.
|
|
819
|
+
const parentAbortHandler = signal ? () => retryAbort.abort() : undefined;
|
|
820
|
+
if (signal && parentAbortHandler) {
|
|
821
|
+
if (signal.aborted) retryAbort.abort();
|
|
822
|
+
else signal.addEventListener("abort", parentAbortHandler, { once: true });
|
|
823
|
+
}
|
|
824
|
+
const retrySignal = retryAbort.signal;
|
|
825
|
+
|
|
791
826
|
try {
|
|
792
827
|
ctx.ui.setWorkingMessage(
|
|
793
828
|
`Rerunning ${totalRetries} stalled worker${totalRetries === 1 ? "" : "s"} individually`
|
|
794
829
|
);
|
|
795
830
|
|
|
796
831
|
for (let retryIndex = 0; retryIndex < initialStalledIndexes.length; retryIndex++) {
|
|
832
|
+
// Bail out if the retry phase deadline has elapsed.
|
|
833
|
+
if (retrySignal.aborted) {
|
|
834
|
+
for (let remaining = retryIndex; remaining < initialStalledIndexes.length; remaining++) {
|
|
835
|
+
const idx = initialStalledIndexes[remaining];
|
|
836
|
+
retrySummaryLines.push(`- [${tasks[idx].agent}] skipped (retry phase timeout)`);
|
|
837
|
+
}
|
|
838
|
+
break;
|
|
839
|
+
}
|
|
840
|
+
|
|
797
841
|
const stalledIndex = initialStalledIndexes[retryIndex];
|
|
798
842
|
const stalledTask = tasks[stalledIndex];
|
|
799
843
|
const priorResult = allResults[stalledIndex];
|
|
@@ -810,33 +854,49 @@ async function executeParallel(
|
|
|
810
854
|
`Retrying stalled worker ${retryIndex + 1}/${totalRetries}: ${stalledTask.agent}`
|
|
811
855
|
);
|
|
812
856
|
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
857
|
+
let retryResult: SingleResult;
|
|
858
|
+
try {
|
|
859
|
+
retryResult = await runSingleAgent(
|
|
860
|
+
ctx.cwd,
|
|
861
|
+
agents,
|
|
862
|
+
stalledTask.agent,
|
|
863
|
+
retryTask,
|
|
864
|
+
stalledTask.cwd,
|
|
865
|
+
undefined,
|
|
866
|
+
retrySignal,
|
|
867
|
+
(partial) => {
|
|
868
|
+
if (partial.details?.results[0]) {
|
|
869
|
+
const partialResult = {
|
|
870
|
+
...partial.details.results[0],
|
|
871
|
+
task: stalledTask.task,
|
|
872
|
+
};
|
|
873
|
+
allResults[stalledIndex] = partialResult;
|
|
874
|
+
emitParallelUpdate();
|
|
875
|
+
}
|
|
876
|
+
},
|
|
877
|
+
makeDetails("parallel"),
|
|
878
|
+
pi.events,
|
|
879
|
+
undefined,
|
|
880
|
+
explicitRetryModel,
|
|
881
|
+
parentModelId,
|
|
882
|
+
defaults,
|
|
883
|
+
retryRoutingHints,
|
|
884
|
+
stalledTask.isolation
|
|
885
|
+
);
|
|
886
|
+
} catch {
|
|
887
|
+
// Retry-phase timeout aborted this worker. Mark remaining
|
|
888
|
+
// workers as skipped and exit the loop.
|
|
889
|
+
retrySummaryLines.push(`- [${stalledTask.agent}] aborted (retry phase timeout)`);
|
|
890
|
+
for (
|
|
891
|
+
let remaining = retryIndex + 1;
|
|
892
|
+
remaining < initialStalledIndexes.length;
|
|
893
|
+
remaining++
|
|
894
|
+
) {
|
|
895
|
+
const idx = initialStalledIndexes[remaining];
|
|
896
|
+
retrySummaryLines.push(`- [${tasks[idx].agent}] skipped (retry phase timeout)`);
|
|
897
|
+
}
|
|
898
|
+
break;
|
|
899
|
+
}
|
|
840
900
|
|
|
841
901
|
retryResult.task = stalledTask.task;
|
|
842
902
|
retryResult.stderr = appendStderrNote(
|
|
@@ -848,6 +908,10 @@ async function executeParallel(
|
|
|
848
908
|
emitParallelUpdate();
|
|
849
909
|
}
|
|
850
910
|
} finally {
|
|
911
|
+
clearTimeout(retryTimer);
|
|
912
|
+
if (signal && parentAbortHandler) {
|
|
913
|
+
signal.removeEventListener("abort", parentAbortHandler);
|
|
914
|
+
}
|
|
851
915
|
ctx.ui.setWorkingMessage();
|
|
852
916
|
}
|
|
853
917
|
|
|
@@ -1066,8 +1130,6 @@ interface DisplayRenderOptions {
|
|
|
1066
1130
|
* Shared preview budgets for compact subagent presentation lines.
|
|
1067
1131
|
*/
|
|
1068
1132
|
const SUBAGENT_PREVIEW_LIMITS = {
|
|
1069
|
-
callCentipedeStep: 90,
|
|
1070
|
-
callParallelTask: 90,
|
|
1071
1133
|
collapsedParallelResult: 88,
|
|
1072
1134
|
} as const;
|
|
1073
1135
|
|
|
@@ -1333,65 +1395,27 @@ function renderSubagentCall(args: Record<string, unknown>, theme: Theme) {
|
|
|
1333
1395
|
args.tasks as { agent: string; model?: string; task: string }[] | string | undefined
|
|
1334
1396
|
);
|
|
1335
1397
|
const lines: string[] = [];
|
|
1398
|
+
// Only show scope when non-default (user is the default)
|
|
1399
|
+
const scopeEntry = scope !== "user" ? `scope:${scope}` : undefined;
|
|
1336
1400
|
|
|
1337
1401
|
if (centipedeArr && centipedeArr.length > 0) {
|
|
1338
1402
|
appendSection(lines, [formatSubagentHeader(theme, `centipede (${centipedeArr.length} steps)`)]);
|
|
1339
|
-
const metaLine = formatMetaLine(theme, [
|
|
1340
|
-
`scope:${scope}`,
|
|
1341
|
-
model ? `model:${model}` : undefined,
|
|
1342
|
-
]);
|
|
1403
|
+
const metaLine = formatMetaLine(theme, [scopeEntry, model ? `model:${model}` : undefined]);
|
|
1343
1404
|
if (metaLine) appendSection(lines, [metaLine]);
|
|
1344
|
-
|
|
1345
|
-
const previewLines = centipedeArr.slice(0, 3).map((step, index) => {
|
|
1346
|
-
const task = step.task.replace(/\{previous\}/g, "").trim();
|
|
1347
|
-
const preview = toCompactPreview(
|
|
1348
|
-
task || "(uses previous output)",
|
|
1349
|
-
SUBAGENT_PREVIEW_LIMITS.callCentipedeStep
|
|
1350
|
-
);
|
|
1351
|
-
const modelTag = formatModelTag(theme, step.model);
|
|
1352
|
-
const identity = modelTag
|
|
1353
|
-
? `${formatSubagentIdentity(step.agent)} ${modelTag}`
|
|
1354
|
-
: formatSubagentIdentity(step.agent);
|
|
1355
|
-
return `${formatPresentationText(theme, "meta", `${index + 1}.`)} ${identity} ${formatPresentationText(theme, "process_output", preview)}`;
|
|
1356
|
-
});
|
|
1357
|
-
if (previewLines.length > 0) appendSection(lines, previewLines, { blankBefore: true });
|
|
1358
|
-
if (centipedeArr.length > 3) {
|
|
1359
|
-
appendSection(lines, [
|
|
1360
|
-
formatPresentationText(theme, "hint", `… +${centipedeArr.length - 3} more steps`),
|
|
1361
|
-
]);
|
|
1362
|
-
}
|
|
1363
1405
|
return new Text(lines.join("\n"), 0, 0);
|
|
1364
1406
|
}
|
|
1365
1407
|
|
|
1366
1408
|
if (tasksArr && tasksArr.length > 0) {
|
|
1367
1409
|
appendSection(lines, [formatSubagentHeader(theme, `parallel (${tasksArr.length} tasks)`)]);
|
|
1368
|
-
const metaLine = formatMetaLine(theme, [
|
|
1369
|
-
`scope:${scope}`,
|
|
1370
|
-
model ? `model:${model}` : undefined,
|
|
1371
|
-
]);
|
|
1410
|
+
const metaLine = formatMetaLine(theme, [scopeEntry, model ? `model:${model}` : undefined]);
|
|
1372
1411
|
if (metaLine) appendSection(lines, [metaLine]);
|
|
1373
|
-
|
|
1374
|
-
const previewLines = tasksArr.slice(0, 2).map((task, index) => {
|
|
1375
|
-
const taskPreview = toCompactPreview(task.task, SUBAGENT_PREVIEW_LIMITS.callParallelTask);
|
|
1376
|
-
const modelTag = formatModelTag(theme, task.model);
|
|
1377
|
-
const identity = modelTag
|
|
1378
|
-
? `${formatSubagentIdentity(task.agent)} ${modelTag}`
|
|
1379
|
-
: formatSubagentIdentity(task.agent);
|
|
1380
|
-
return `${formatPresentationText(theme, "meta", `${index + 1}.`)} ${identity} ${formatPresentationText(theme, "process_output", taskPreview)}`;
|
|
1381
|
-
});
|
|
1382
|
-
if (previewLines.length > 0) appendSection(lines, previewLines, { blankBefore: true });
|
|
1383
|
-
if (tasksArr.length > 2) {
|
|
1384
|
-
appendSection(lines, [
|
|
1385
|
-
formatPresentationText(theme, "hint", `… +${tasksArr.length - 2} more tasks`),
|
|
1386
|
-
]);
|
|
1387
|
-
}
|
|
1388
1412
|
return new Text(lines.join("\n"), 0, 0);
|
|
1389
1413
|
}
|
|
1390
1414
|
|
|
1391
1415
|
const agentName = (args.agent as string) || "...";
|
|
1392
1416
|
const task = typeof args.task === "string" ? args.task : "...";
|
|
1393
1417
|
appendSection(lines, [formatSubagentHeader(theme, "single", agentName)]);
|
|
1394
|
-
const metaLine = formatMetaLine(theme, [
|
|
1418
|
+
const metaLine = formatMetaLine(theme, [scopeEntry, model ? `model:${model}` : undefined]);
|
|
1395
1419
|
if (metaLine) appendSection(lines, [metaLine]);
|
|
1396
1420
|
appendSection(
|
|
1397
1421
|
lines,
|
|
@@ -1523,7 +1547,14 @@ function renderSingleResult(
|
|
|
1523
1547
|
: isError
|
|
1524
1548
|
? theme.fg("error", getIcon("error"))
|
|
1525
1549
|
: theme.fg("success", getIcon("success"));
|
|
1526
|
-
const statusLabel =
|
|
1550
|
+
const statusLabel =
|
|
1551
|
+
isRunning && r.startTime
|
|
1552
|
+
? `running ${formatDuration(Date.now() - r.startTime)}`
|
|
1553
|
+
: isRunning
|
|
1554
|
+
? "running"
|
|
1555
|
+
: isError
|
|
1556
|
+
? "failed"
|
|
1557
|
+
: "completed";
|
|
1527
1558
|
const headerLine = formatSubagentHeader(theme, statusLabel, r.agent, icon);
|
|
1528
1559
|
const metaLine = formatMetaLine(theme, [
|
|
1529
1560
|
`source:${r.agentSource}`,
|
|
@@ -1727,17 +1758,25 @@ function renderCentipedeResult(
|
|
|
1727
1758
|
: failCount > 0
|
|
1728
1759
|
? theme.fg("error", getIcon("error"))
|
|
1729
1760
|
: theme.fg("success", getIcon("success"));
|
|
1761
|
+
const earliestStart = details.results.reduce(
|
|
1762
|
+
(min, r) => (r.startTime && r.startTime < min ? r.startTime : min),
|
|
1763
|
+
Number.POSITIVE_INFINITY
|
|
1764
|
+
);
|
|
1765
|
+
const elapsed = Number.isFinite(earliestStart)
|
|
1766
|
+
? formatDuration(Date.now() - earliestStart)
|
|
1767
|
+
: undefined;
|
|
1730
1768
|
const summaryLine = formatMetaLine(theme, [
|
|
1731
1769
|
`${successCount + failCount}/${totalSteps} done`,
|
|
1732
1770
|
runningCount > 0 ? `${runningCount} running` : undefined,
|
|
1733
1771
|
failCount > 0 ? `${failCount} failed` : undefined,
|
|
1772
|
+
elapsed,
|
|
1734
1773
|
]);
|
|
1735
1774
|
|
|
1736
1775
|
if (expanded) {
|
|
1737
1776
|
const container = new Container();
|
|
1738
|
-
const headerLines: string[] = [
|
|
1739
|
-
if (summaryLine)
|
|
1740
|
-
container.addChild(new Text(headerLines.join("\n"), 0, 0));
|
|
1777
|
+
const headerLines: string[] = [];
|
|
1778
|
+
if (summaryLine) headerLines.push(`${icon} ${summaryLine}`);
|
|
1779
|
+
if (headerLines.length > 0) container.addChild(new Text(headerLines.join("\n"), 0, 0));
|
|
1741
1780
|
|
|
1742
1781
|
for (let si = 0; si < totalSteps; si++) {
|
|
1743
1782
|
const stepNum = si + 1;
|
|
@@ -1746,11 +1785,13 @@ function renderCentipedeResult(
|
|
|
1746
1785
|
stepResult?.agent ?? details.centipedeSteps?.[si]?.agent ?? `step ${stepNum}`;
|
|
1747
1786
|
const stepStatus = !stepResult
|
|
1748
1787
|
? "pending"
|
|
1749
|
-
: stepResult.exitCode === -1
|
|
1750
|
-
?
|
|
1751
|
-
:
|
|
1752
|
-
? "
|
|
1753
|
-
:
|
|
1788
|
+
: stepResult.exitCode === -1 && stepResult.startTime
|
|
1789
|
+
? `running ${formatDuration(Date.now() - stepResult.startTime)}`
|
|
1790
|
+
: stepResult.exitCode === -1
|
|
1791
|
+
? "running"
|
|
1792
|
+
: isResultError(stepResult)
|
|
1793
|
+
? "failed"
|
|
1794
|
+
: "completed";
|
|
1754
1795
|
const stepStatusRole = !stepResult
|
|
1755
1796
|
? "meta"
|
|
1756
1797
|
: stepResult.exitCode === -1
|
|
@@ -1839,8 +1880,8 @@ function renderCentipedeResult(
|
|
|
1839
1880
|
return container;
|
|
1840
1881
|
}
|
|
1841
1882
|
|
|
1842
|
-
const lines: string[] = [
|
|
1843
|
-
if (summaryLine)
|
|
1883
|
+
const lines: string[] = [];
|
|
1884
|
+
if (summaryLine) lines.push(`${icon} ${summaryLine}`);
|
|
1844
1885
|
|
|
1845
1886
|
for (let si = 0; si < totalSteps; si++) {
|
|
1846
1887
|
const stepNum = si + 1;
|
|
@@ -1851,11 +1892,13 @@ function renderCentipedeResult(
|
|
|
1851
1892
|
const stem = isLast ? " " : `${formatPresentationText(theme, "meta", "│")} `;
|
|
1852
1893
|
const stepStatus = !stepResult
|
|
1853
1894
|
? "pending"
|
|
1854
|
-
: stepResult.exitCode === -1
|
|
1855
|
-
?
|
|
1856
|
-
:
|
|
1857
|
-
? "
|
|
1858
|
-
:
|
|
1895
|
+
: stepResult.exitCode === -1 && stepResult.startTime
|
|
1896
|
+
? `running ${formatDuration(Date.now() - stepResult.startTime)}`
|
|
1897
|
+
: stepResult.exitCode === -1
|
|
1898
|
+
? "running"
|
|
1899
|
+
: isResultError(stepResult)
|
|
1900
|
+
? "failed"
|
|
1901
|
+
: "done";
|
|
1859
1902
|
const statusRole = !stepResult
|
|
1860
1903
|
? "meta"
|
|
1861
1904
|
: stepResult.exitCode === -1
|
|
@@ -1922,23 +1965,36 @@ function renderParallelResult(
|
|
|
1922
1965
|
: counts.stalled > 0
|
|
1923
1966
|
? theme.fg("warning", getIcon("blocked"))
|
|
1924
1967
|
: theme.fg("success", getIcon("success"));
|
|
1968
|
+
const earliestStart = details.results.reduce(
|
|
1969
|
+
(min, r) => (r.startTime && r.startTime < min ? r.startTime : min),
|
|
1970
|
+
Number.POSITIVE_INFINITY
|
|
1971
|
+
);
|
|
1972
|
+
const elapsed = Number.isFinite(earliestStart)
|
|
1973
|
+
? formatDuration(Date.now() - earliestStart)
|
|
1974
|
+
: undefined;
|
|
1925
1975
|
const summaryLine = formatMetaLine(theme, [
|
|
1926
1976
|
`${counts.finished}/${details.results.length} done`,
|
|
1927
1977
|
`${counts.completed} completed`,
|
|
1928
1978
|
counts.failed > 0 ? `${counts.failed} failed` : undefined,
|
|
1929
1979
|
`${counts.stalled} stalled`,
|
|
1930
1980
|
counts.running > 0 ? `${counts.running} running` : undefined,
|
|
1981
|
+
elapsed,
|
|
1931
1982
|
]);
|
|
1932
1983
|
|
|
1933
1984
|
if (expanded && !isRunning) {
|
|
1934
1985
|
const container = new Container();
|
|
1935
|
-
const headerLines = [
|
|
1936
|
-
if (summaryLine)
|
|
1937
|
-
container.addChild(new Text(headerLines.join("\n"), 0, 0));
|
|
1986
|
+
const headerLines: string[] = [];
|
|
1987
|
+
if (summaryLine) headerLines.push(`${icon} ${summaryLine}`);
|
|
1988
|
+
if (headerLines.length > 0) container.addChild(new Text(headerLines.join("\n"), 0, 0));
|
|
1938
1989
|
|
|
1939
1990
|
for (const result of details.results) {
|
|
1940
1991
|
const resultState = getParallelResultState(result);
|
|
1941
|
-
const resultStatus =
|
|
1992
|
+
const resultStatus =
|
|
1993
|
+
resultState === "completed"
|
|
1994
|
+
? "completed"
|
|
1995
|
+
: resultState === "running" && result.startTime
|
|
1996
|
+
? `running ${formatDuration(Date.now() - result.startTime)}`
|
|
1997
|
+
: resultState;
|
|
1942
1998
|
const resultStatusRole =
|
|
1943
1999
|
resultState === "failed"
|
|
1944
2000
|
? "status_error"
|
|
@@ -2028,8 +2084,8 @@ function renderParallelResult(
|
|
|
2028
2084
|
return container;
|
|
2029
2085
|
}
|
|
2030
2086
|
|
|
2031
|
-
const lines: string[] = [
|
|
2032
|
-
if (summaryLine)
|
|
2087
|
+
const lines: string[] = [];
|
|
2088
|
+
if (summaryLine) lines.push(`${icon} ${summaryLine}`);
|
|
2033
2089
|
|
|
2034
2090
|
for (let index = 0; index < details.results.length; index++) {
|
|
2035
2091
|
const result = details.results[index];
|
|
@@ -2037,7 +2093,12 @@ function renderParallelResult(
|
|
|
2037
2093
|
const branch = formatPresentationText(theme, "meta", isLast ? "└─" : "├─");
|
|
2038
2094
|
const stem = isLast ? " " : `${formatPresentationText(theme, "meta", "│")} `;
|
|
2039
2095
|
const resultState = getParallelResultState(result);
|
|
2040
|
-
const resultStatus =
|
|
2096
|
+
const resultStatus =
|
|
2097
|
+
resultState === "completed"
|
|
2098
|
+
? "done"
|
|
2099
|
+
: resultState === "running" && result.startTime
|
|
2100
|
+
? `running ${formatDuration(Date.now() - result.startTime)}`
|
|
2101
|
+
: resultState;
|
|
2041
2102
|
const statusRole =
|
|
2042
2103
|
resultState === "failed"
|
|
2043
2104
|
? "status_error"
|
|
@@ -449,6 +449,7 @@ export interface ForegroundWatchdogThresholds {
|
|
|
449
449
|
readonly killGraceMs: number;
|
|
450
450
|
readonly startupTimeoutMs: number;
|
|
451
451
|
readonly toolExecutionTimeoutMs: number;
|
|
452
|
+
readonly wallClockTimeoutMs: number;
|
|
452
453
|
}
|
|
453
454
|
|
|
454
455
|
/** Heartbeat state tracked by the foreground subagent liveness watchdog. */
|
|
@@ -464,7 +465,7 @@ export type WatchdogStatus =
|
|
|
464
465
|
| {
|
|
465
466
|
readonly elapsedMs: number;
|
|
466
467
|
readonly kind: "stalled";
|
|
467
|
-
readonly phase: "inactivity" | "startup" | "tool_execution";
|
|
468
|
+
readonly phase: "inactivity" | "startup" | "tool_execution" | "wall_clock";
|
|
468
469
|
readonly timeoutMs: number;
|
|
469
470
|
};
|
|
470
471
|
|
|
@@ -477,17 +478,33 @@ export const SUBAGENT_INACTIVITY_TIMEOUT_MS_ENV = "TALLOW_SUBAGENT_INACTIVITY_TI
|
|
|
477
478
|
/** Env var overriding the foreground timeout while a tool call is still running. */
|
|
478
479
|
export const SUBAGENT_TOOL_EXECUTION_TIMEOUT_MS_ENV = "TALLOW_SUBAGENT_TOOL_EXECUTION_TIMEOUT_MS";
|
|
479
480
|
|
|
481
|
+
/** Env var overriding the total wall-clock timeout per foreground subagent. */
|
|
482
|
+
export const SUBAGENT_WALL_CLOCK_TIMEOUT_MS_ENV = "TALLOW_SUBAGENT_WALL_CLOCK_TIMEOUT_MS";
|
|
483
|
+
|
|
480
484
|
/** Env var overriding the SIGTERM → SIGKILL grace window for stalled workers. */
|
|
481
485
|
export const SUBAGENT_WATCHDOG_KILL_GRACE_MS_ENV = "TALLOW_SUBAGENT_WATCHDOG_KILL_GRACE_MS";
|
|
482
486
|
|
|
487
|
+
/** Env var overriding the total wall-clock timeout for the stalled-worker retry phase. */
|
|
488
|
+
export const SUBAGENT_RETRY_PHASE_TIMEOUT_MS_ENV = "TALLOW_SUBAGENT_RETRY_PHASE_TIMEOUT_MS";
|
|
489
|
+
|
|
483
490
|
/** Default watchdog thresholds used by foreground subagents in runSingleAgent. */
|
|
484
491
|
export const FOREGROUND_WATCHDOG_THRESHOLDS: ForegroundWatchdogThresholds = {
|
|
485
|
-
inactivityTimeoutMs:
|
|
492
|
+
inactivityTimeoutMs: 120_000, // 2 min without any heartbeat event
|
|
486
493
|
killGraceMs: 5_000,
|
|
487
|
-
startupTimeoutMs:
|
|
488
|
-
toolExecutionTimeoutMs:
|
|
494
|
+
startupTimeoutMs: 30_000, // 30s to emit first event
|
|
495
|
+
toolExecutionTimeoutMs: 300_000, // 5 min per tool call (was 10 min)
|
|
496
|
+
wallClockTimeoutMs: 480_000, // 8 min total (was 15 min)
|
|
489
497
|
};
|
|
490
498
|
|
|
499
|
+
/**
|
|
500
|
+
* Default total wall-clock timeout for the entire stalled-worker retry phase.
|
|
501
|
+
*
|
|
502
|
+
* Without a cap, N stalled retries × per-worker watchdog timeout can block
|
|
503
|
+
* the parent for 30+ minutes. This limits the total retry phase so the
|
|
504
|
+
* parent agent can recover sooner.
|
|
505
|
+
*/
|
|
506
|
+
export const DEFAULT_RETRY_PHASE_TIMEOUT_MS = 180_000; // 3 minutes
|
|
507
|
+
|
|
491
508
|
/** How often the foreground watchdog checks for stalled subagents. */
|
|
492
509
|
const FOREGROUND_WATCHDOG_CHECK_INTERVAL_MS = 500;
|
|
493
510
|
|
|
@@ -532,9 +549,25 @@ export function resolveForegroundWatchdogThresholds(
|
|
|
532
549
|
toolExecutionTimeoutMs:
|
|
533
550
|
parseTimeoutOverrideMs(env[SUBAGENT_TOOL_EXECUTION_TIMEOUT_MS_ENV]) ??
|
|
534
551
|
FOREGROUND_WATCHDOG_THRESHOLDS.toolExecutionTimeoutMs,
|
|
552
|
+
wallClockTimeoutMs:
|
|
553
|
+
parseTimeoutOverrideMs(env[SUBAGENT_WALL_CLOCK_TIMEOUT_MS_ENV]) ??
|
|
554
|
+
FOREGROUND_WATCHDOG_THRESHOLDS.wallClockTimeoutMs,
|
|
535
555
|
};
|
|
536
556
|
}
|
|
537
557
|
|
|
558
|
+
/**
|
|
559
|
+
* Resolve the effective retry-phase wall-clock timeout from env overrides.
|
|
560
|
+
*
|
|
561
|
+
* @param env - Environment lookup map
|
|
562
|
+
* @returns Timeout in milliseconds for the entire stalled-worker retry phase
|
|
563
|
+
*/
|
|
564
|
+
export function resolveRetryPhaseTimeoutMs(env: EnvLookup = process.env): number {
|
|
565
|
+
return (
|
|
566
|
+
parseTimeoutOverrideMs(env[SUBAGENT_RETRY_PHASE_TIMEOUT_MS_ENV]) ??
|
|
567
|
+
DEFAULT_RETRY_PHASE_TIMEOUT_MS
|
|
568
|
+
);
|
|
569
|
+
}
|
|
570
|
+
|
|
538
571
|
/**
|
|
539
572
|
* Return whether an event type counts as watchdog progress.
|
|
540
573
|
* @param eventType - Raw child-process event type
|
|
@@ -619,6 +652,18 @@ export function evaluateWatchdogStatus(
|
|
|
619
652
|
nowMs: number,
|
|
620
653
|
thresholds: ForegroundWatchdogThresholds
|
|
621
654
|
): WatchdogStatus {
|
|
655
|
+
// Wall-clock timeout: hard cap on total execution time, regardless of activity.
|
|
656
|
+
// Catches "slow but active" agents that keep making tool calls without finishing.
|
|
657
|
+
const totalElapsedMs = nowMs - state.startedAtMs;
|
|
658
|
+
if (totalElapsedMs >= thresholds.wallClockTimeoutMs) {
|
|
659
|
+
return {
|
|
660
|
+
elapsedMs: totalElapsedMs,
|
|
661
|
+
kind: "stalled",
|
|
662
|
+
phase: "wall_clock",
|
|
663
|
+
timeoutMs: thresholds.wallClockTimeoutMs,
|
|
664
|
+
};
|
|
665
|
+
}
|
|
666
|
+
|
|
622
667
|
if (state.lastHeartbeatAtMs === null) {
|
|
623
668
|
const startupElapsedMs = nowMs - state.startedAtMs;
|
|
624
669
|
if (startupElapsedMs >= thresholds.startupTimeoutMs) {
|
|
@@ -655,6 +700,15 @@ export function createStalledSubagentErrorMessage(
|
|
|
655
700
|
stalledStatus: Extract<WatchdogStatus, { kind: "stalled" }>
|
|
656
701
|
): string {
|
|
657
702
|
const timeoutSeconds = Math.max(1, Math.round(stalledStatus.timeoutMs / 1000));
|
|
703
|
+
const timeoutMinutes = Math.round(timeoutSeconds / 60);
|
|
704
|
+
if (stalledStatus.phase === "wall_clock") {
|
|
705
|
+
return (
|
|
706
|
+
`Subagent terminated after ${timeoutMinutes}m wall-clock timeout. ` +
|
|
707
|
+
"The agent was still active but exceeded the maximum allowed execution time. " +
|
|
708
|
+
"Action: break the task into smaller pieces, or increase " +
|
|
709
|
+
"TALLOW_SUBAGENT_WALL_CLOCK_TIMEOUT_MS for legitimately long work."
|
|
710
|
+
);
|
|
711
|
+
}
|
|
658
712
|
const phaseDescription =
|
|
659
713
|
stalledStatus.phase === "startup"
|
|
660
714
|
? "no startup activity was received"
|
|
@@ -743,6 +797,54 @@ export function terminateProcessWithGrace(
|
|
|
743
797
|
/** Callback for streaming partial results during subagent execution. */
|
|
744
798
|
export type OnUpdateCallback = (partial: AgentToolResult<SubagentDetails>) => void;
|
|
745
799
|
|
|
800
|
+
// ── Subprocess Arg Construction ──────────────────────────────────────────────
|
|
801
|
+
|
|
802
|
+
/** Options for building subprocess CLI arguments. */
|
|
803
|
+
export interface SubprocessArgsOptions {
|
|
804
|
+
/** Session file path for persistent teammates (omit for --no-session). */
|
|
805
|
+
session?: string;
|
|
806
|
+
/** Provider-qualified model display name (e.g. "anthropic/claude-sonnet-4-6"). */
|
|
807
|
+
modelDisplayName?: string;
|
|
808
|
+
/** Effective tool allowlist (already filtered by denylist). */
|
|
809
|
+
tools?: string[];
|
|
810
|
+
/** Skill names to pass via --skill flags. */
|
|
811
|
+
skills?: string[];
|
|
812
|
+
/** Path to temp file containing the system prompt. */
|
|
813
|
+
systemPromptPath?: string;
|
|
814
|
+
/** Task text (will be prefixed with "Task: "). */
|
|
815
|
+
task: string;
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
/**
|
|
819
|
+
* Build the CLI argument array for a subagent child process.
|
|
820
|
+
*
|
|
821
|
+
* **Invariant:** `-p <task>` is always the last pair in the array.
|
|
822
|
+
* Commander consumes the next token after `-p` as its required `<prompt>`
|
|
823
|
+
* value. Placing `-p` before other flags (e.g. `--no-session`) causes
|
|
824
|
+
* Commander to swallow the flag as the prompt text, leaving the real
|
|
825
|
+
* task as a stray positional argument.
|
|
826
|
+
*
|
|
827
|
+
* @param opts - Subprocess argument options
|
|
828
|
+
* @returns CLI argument array safe for child_process.spawn
|
|
829
|
+
*/
|
|
830
|
+
export function buildSubprocessArgs(opts: SubprocessArgsOptions): string[] {
|
|
831
|
+
const args: string[] = opts.session
|
|
832
|
+
? ["--mode", "json", "--session", opts.session]
|
|
833
|
+
: ["--mode", "json", "--no-session"];
|
|
834
|
+
|
|
835
|
+
if (opts.modelDisplayName) args.push("--model", opts.modelDisplayName);
|
|
836
|
+
if (opts.tools && opts.tools.length > 0) args.push("--tools", opts.tools.join(","));
|
|
837
|
+
if (opts.skills && opts.skills.length > 0) {
|
|
838
|
+
for (const skill of opts.skills) args.push("--skill", skill);
|
|
839
|
+
}
|
|
840
|
+
if (opts.systemPromptPath) args.push("--append-system-prompt", opts.systemPromptPath);
|
|
841
|
+
|
|
842
|
+
// CRITICAL: -p must be last — see JSDoc above.
|
|
843
|
+
args.push("-p", `Task: ${opts.task}`);
|
|
844
|
+
|
|
845
|
+
return args;
|
|
846
|
+
}
|
|
847
|
+
|
|
746
848
|
// ── Background Spawning ──────────────────────────────────────────────────────
|
|
747
849
|
|
|
748
850
|
/**
|
|
@@ -813,18 +915,6 @@ export async function spawnBackgroundSubagent(
|
|
|
813
915
|
const agent = { ...resolved.agent, model: routing.model.id };
|
|
814
916
|
const agentSource = resolved.resolution === "ephemeral" ? ("ephemeral" as const) : agent.source;
|
|
815
917
|
|
|
816
|
-
const args: string[] = session
|
|
817
|
-
? ["--mode", "json", "-p", "--session", session]
|
|
818
|
-
: ["--mode", "json", "-p", "--no-session"];
|
|
819
|
-
// Use provider-qualified name (e.g. "openai-codex/gpt-5.1") so the child process
|
|
820
|
-
// resolves to the exact provider the router selected, not just the first match.
|
|
821
|
-
if (agent.model) args.push("--model", routing.model.displayName);
|
|
822
|
-
const effectiveTools = computeEffectiveTools(agent.tools, agent.disallowedTools);
|
|
823
|
-
if (effectiveTools && effectiveTools.length > 0) args.push("--tools", effectiveTools.join(","));
|
|
824
|
-
if (agent.skills && agent.skills.length > 0) {
|
|
825
|
-
for (const skill of agent.skills) args.push("--skill", skill);
|
|
826
|
-
}
|
|
827
|
-
|
|
828
918
|
let tmpPromptDir: string | undefined;
|
|
829
919
|
let tmpPromptPath: string | undefined;
|
|
830
920
|
|
|
@@ -839,7 +929,6 @@ export async function spawnBackgroundSubagent(
|
|
|
839
929
|
const tmp = writePromptToTempFile(agent.name, systemPrompt);
|
|
840
930
|
tmpPromptDir = tmp.dir;
|
|
841
931
|
tmpPromptPath = tmp.filePath;
|
|
842
|
-
args.push("--append-system-prompt", tmpPromptPath);
|
|
843
932
|
}
|
|
844
933
|
|
|
845
934
|
let expandedTask: string;
|
|
@@ -850,7 +939,16 @@ export async function spawnBackgroundSubagent(
|
|
|
850
939
|
const reason = error instanceof Error ? error.message : String(error);
|
|
851
940
|
return `Failed to expand task references for ${agentName}: ${reason}`;
|
|
852
941
|
}
|
|
853
|
-
|
|
942
|
+
|
|
943
|
+
const effectiveTools = computeEffectiveTools(agent.tools, agent.disallowedTools);
|
|
944
|
+
const args = buildSubprocessArgs({
|
|
945
|
+
session,
|
|
946
|
+
modelDisplayName: agent.model ? routing.model.displayName : undefined,
|
|
947
|
+
tools: effectiveTools && effectiveTools.length > 0 ? effectiveTools : undefined,
|
|
948
|
+
skills: agent.skills && agent.skills.length > 0 ? agent.skills : undefined,
|
|
949
|
+
systemPromptPath: tmpPromptPath,
|
|
950
|
+
task: expandedTask,
|
|
951
|
+
});
|
|
854
952
|
|
|
855
953
|
const childEnv: Record<string, string> = { ...process.env, PI_IS_SUBAGENT: "1" } as Record<
|
|
856
954
|
string,
|
|
@@ -1236,18 +1334,6 @@ export async function runSingleAgent(
|
|
|
1236
1334
|
background: false,
|
|
1237
1335
|
} satisfies SubagentStartEvent);
|
|
1238
1336
|
|
|
1239
|
-
const args: string[] = session
|
|
1240
|
-
? ["--mode", "json", "-p", "--session", session]
|
|
1241
|
-
: ["--mode", "json", "-p", "--no-session"];
|
|
1242
|
-
// Use provider-qualified name so the child process resolves to the exact provider.
|
|
1243
|
-
if (agent.model) args.push("--model", routing.model.displayName);
|
|
1244
|
-
const fgEffectiveTools = computeEffectiveTools(agent.tools, agent.disallowedTools);
|
|
1245
|
-
if (fgEffectiveTools && fgEffectiveTools.length > 0)
|
|
1246
|
-
args.push("--tools", fgEffectiveTools.join(","));
|
|
1247
|
-
if (agent.skills && agent.skills.length > 0) {
|
|
1248
|
-
for (const skill of agent.skills) args.push("--skill", skill);
|
|
1249
|
-
}
|
|
1250
|
-
|
|
1251
1337
|
let tmpPromptDir: string | null = null;
|
|
1252
1338
|
let tmpPromptPath: string | null = null;
|
|
1253
1339
|
|
|
@@ -1290,6 +1376,7 @@ export async function runSingleAgent(
|
|
|
1290
1376
|
},
|
|
1291
1377
|
model: agent.model,
|
|
1292
1378
|
step,
|
|
1379
|
+
startTime: Date.now(),
|
|
1293
1380
|
};
|
|
1294
1381
|
|
|
1295
1382
|
/** Timestamp of the last emitted update, used for throttling. */
|
|
@@ -1327,11 +1414,18 @@ export async function runSingleAgent(
|
|
|
1327
1414
|
const tmp = writePromptToTempFile(agent.name, fgSystemPrompt);
|
|
1328
1415
|
tmpPromptDir = tmp.dir;
|
|
1329
1416
|
tmpPromptPath = tmp.filePath;
|
|
1330
|
-
args.push("--append-system-prompt", tmpPromptPath);
|
|
1331
1417
|
}
|
|
1332
1418
|
|
|
1333
1419
|
const expandedTask = await expandFileReferences(task, effectiveCwd);
|
|
1334
|
-
|
|
1420
|
+
const fgEffectiveTools = computeEffectiveTools(agent.tools, agent.disallowedTools);
|
|
1421
|
+
const args = buildSubprocessArgs({
|
|
1422
|
+
session,
|
|
1423
|
+
modelDisplayName: agent.model ? routing.model.displayName : undefined,
|
|
1424
|
+
tools: fgEffectiveTools && fgEffectiveTools.length > 0 ? fgEffectiveTools : undefined,
|
|
1425
|
+
skills: agent.skills && agent.skills.length > 0 ? agent.skills : undefined,
|
|
1426
|
+
systemPromptPath: tmpPromptPath ?? undefined,
|
|
1427
|
+
task: expandedTask,
|
|
1428
|
+
});
|
|
1335
1429
|
let wasAborted = false;
|
|
1336
1430
|
|
|
1337
1431
|
const fgChildEnv: Record<string, string> = {
|