@ryan_nookpi/pi-extension-subagent 0.5.7 → 0.5.9

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/commands.ts CHANGED
@@ -11,6 +11,13 @@ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-a
11
11
  import { Container, Key, matchesKey, Spacer, Text, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
12
12
  import { createRunActivityRecorder } from "./activity.js";
13
13
  import { discoverAgents } from "./agents.js";
14
+ import {
15
+ asyncInvocationDetails,
16
+ completeAsyncInvocation,
17
+ discardRemovedAsyncInvocation,
18
+ isExpectedTrackedCancellation,
19
+ retainAsyncCompletion,
20
+ } from "./async-task-lifecycle.js";
14
21
  import { loadSubagentConfig } from "./config.js";
15
22
  import {
16
23
  COMMAND_COMPLETION_LIMIT,
@@ -871,6 +878,7 @@ function restoreRunsFromSession(store: SubagentStore, ctx: any, pi?: ExtensionAP
871
878
  if (entry.runState.status === "running") continue;
872
879
  const pendingSince = entry.pendingCompletion.createdAt ?? entry.runState.startedAt + entry.runState.elapsedMs;
873
880
  if (Date.now() - pendingSince > STALE_PENDING_COMPLETION_MS) {
881
+ store.asyncTasks?.expirePending(entry.pendingCompletion.message);
874
882
  store.globalLiveRuns.delete(runId);
875
883
  }
876
884
  }
@@ -879,6 +887,7 @@ function restoreRunsFromSession(store: SubagentStore, ctx: any, pi?: ExtensionAP
879
887
  if (!batch.pendingCompletion) continue;
880
888
  const pendingSince = batch.pendingCompletion.createdAt ?? batch.createdAt;
881
889
  if (Date.now() - pendingSince > STALE_PENDING_COMPLETION_MS) {
890
+ store.asyncTasks?.expirePending(batch.pendingCompletion.message);
882
891
  clearPendingGroupCompletion("batch", batchId);
883
892
  store.batchGroups.delete(batchId);
884
893
  }
@@ -888,6 +897,7 @@ function restoreRunsFromSession(store: SubagentStore, ctx: any, pi?: ExtensionAP
888
897
  if (!pipeline.pendingCompletion) continue;
889
898
  const pendingSince = pipeline.pendingCompletion.createdAt ?? pipeline.createdAt;
890
899
  if (Date.now() - pendingSince > STALE_PENDING_COMPLETION_MS) {
900
+ store.asyncTasks?.expirePending(pipeline.pendingCompletion.message);
891
901
  clearPendingGroupCompletion("chain", pipelineId);
892
902
  store.pipelines.delete(pipelineId);
893
903
  }
@@ -927,6 +937,9 @@ function deliverOrQueueCompletion(
927
937
  runState: CommandRunState,
928
938
  message: { customType: string; content: string; display: boolean; details: Record<string, unknown> },
929
939
  ): void {
940
+ completeAsyncInvocation(String(message.details.status ?? "completed"));
941
+ message.details = { ...message.details, ...asyncInvocationDetails() };
942
+ retainAsyncCompletion(message);
930
943
  const options = { deliverAs: "followUp" as const };
931
944
  const globalEntry = store.globalLiveRuns.get(runId);
932
945
  let currentSessionFile: string | null = null;
@@ -964,7 +977,9 @@ function finalizeHumanOnlyCompletion(
964
977
  notifyMessage: string,
965
978
  notifyLevel: "info" | "error" | "warning",
966
979
  ): void {
967
- ctx.ui.notify(notifyMessage, notifyLevel);
980
+ completeAsyncInvocation(notifyLevel === "info" ? "completed" : notifyLevel === "warning" ? "aborted" : "error");
981
+ retainAsyncCompletion({ content: _content });
982
+ if (notifyLevel !== "warning" || !isExpectedTrackedCancellation()) ctx.ui.notify(notifyMessage, notifyLevel);
968
983
  store.globalLiveRuns.delete(runId);
969
984
  }
970
985
 
@@ -975,6 +990,7 @@ export interface SubagentRegistrations {
975
990
  }
976
991
 
977
992
  export function registerAll(pi: ExtensionAPI, store: SubagentStore): SubagentRegistrations {
993
+ pi = store.asyncTasks?.wrap(pi) ?? pi;
978
994
  const commandDefinitions = new Map<SubagentCommandName, SubagentCommandDefinition>();
979
995
  const defineCommand = (name: SubagentCommandName, definition: SubagentCommandDefinition): void => {
980
996
  commandDefinitions.set(name, definition);
@@ -1301,441 +1317,455 @@ export function registerAll(pi: ExtensionAPI, store: SubagentStore): SubagentReg
1301
1317
  return;
1302
1318
  }
1303
1319
 
1304
- let runId: number;
1305
- let runState: CommandRunState;
1320
+ const launch = async () => {
1321
+ let runId: number;
1322
+ let runState: CommandRunState;
1306
1323
 
1307
- if (continuedFromRunId !== undefined) {
1308
- const existingRun = store.commandRuns.get(continuedFromRunId);
1309
- if (!existingRun) {
1310
- ctx.ui.notify(`Unknown subagent run #${continuedFromRunId}.`, "error");
1311
- return;
1312
- }
1324
+ if (continuedFromRunId !== undefined) {
1325
+ const existingRun = store.commandRuns.get(continuedFromRunId);
1326
+ if (!existingRun) {
1327
+ ctx.ui.notify(`Unknown subagent run #${continuedFromRunId}.`, "error");
1328
+ return;
1329
+ }
1313
1330
 
1314
- runId = existingRun.id;
1315
- runState = existingRun;
1316
- runState.agent = selectedAgent;
1317
- runState.task = taskForDisplay;
1318
- runState.displayTask = buildSubagentDisplayTaskFallback(taskForDisplay);
1319
- runState.status = "running";
1320
- runState.startedAt = Date.now();
1321
- runState.lastActivityAt = Date.now();
1322
- runState.elapsedMs = 0;
1323
- runState.toolCalls = 0;
1324
- runState.lastLine = "";
1325
- runState.lastOutput = "";
1326
- runState.continuedFromRunId = continuedFromRunId;
1327
- runState.usage = undefined;
1328
- runState.model = undefined;
1329
- runState.retryCount = 0;
1330
- runState.lastRetryReason = undefined;
1331
- runState.errorClass = undefined;
1332
- runState.autoAbortReason = undefined;
1333
- runState.abortPending = false;
1334
- runState.removed = false;
1335
- runState.deliveryMode = deliveryMode;
1336
- runState.turnCount = Math.max(DEFAULT_TURN_COUNT, runState.turnCount || DEFAULT_TURN_COUNT) + 1;
1337
- // NOTE(user-approved): continuations preserve the existing context and session.
1338
- // Switching between /sub:main and /sub:isolate does not retroactively affect existing runs.
1339
- runState.contextMode = runState.contextMode ?? (forceMainContext ? "main" : "sub");
1340
- runState.sessionFile = runState.sessionFile ?? sessionFileForRun ?? makeSubagentSessionFile(runId);
1341
- runState.persistedSessionBaseOffset = getSessionFileSize(runState.sessionFile);
1342
- sessionFileForRun = runState.sessionFile;
1343
- } else {
1344
- runId = store.nextCommandRunId++;
1345
- if (forceMainContext) {
1346
- // Extract main session context as text instead of copying the session file.
1347
- // This prevents subagents from inheriting the main agent's persona.
1348
- const subContextResult = buildMainContextText(ctx);
1349
- const subContextText = typeof subContextResult === "string" ? subContextResult : subContextResult.text;
1350
- const totalMessageCount = typeof subContextResult === "string" ? 0 : subContextResult.totalMessageCount;
1351
- const rawMainSessionFile = ctx.sessionManager?.getSessionFile?.() ?? undefined;
1352
- const mainSessionFile =
1353
- typeof rawMainSessionFile === "string"
1354
- ? rawMainSessionFile.replace(/[\r\n\t]+/g, "").trim() || undefined
1355
- : undefined;
1356
- if (subContextText || mainSessionFile) {
1357
- taskForAgent = wrapTaskWithMainContext(taskForAgent, subContextText, {
1358
- mainSessionFile,
1359
- totalMessageCount,
1360
- });
1331
+ runId = existingRun.id;
1332
+ runState = existingRun;
1333
+ runState.agent = selectedAgent;
1334
+ runState.task = taskForDisplay;
1335
+ runState.displayTask = buildSubagentDisplayTaskFallback(taskForDisplay);
1336
+ runState.status = "running";
1337
+ runState.startedAt = Date.now();
1338
+ runState.lastActivityAt = Date.now();
1339
+ runState.elapsedMs = 0;
1340
+ runState.toolCalls = 0;
1341
+ runState.lastLine = "";
1342
+ runState.lastOutput = "";
1343
+ runState.continuedFromRunId = continuedFromRunId;
1344
+ runState.usage = undefined;
1345
+ runState.model = undefined;
1346
+ runState.retryCount = 0;
1347
+ runState.lastRetryReason = undefined;
1348
+ runState.errorClass = undefined;
1349
+ runState.autoAbortReason = undefined;
1350
+ runState.abortPending = false;
1351
+ runState.removed = false;
1352
+ runState.deliveryMode = deliveryMode;
1353
+ runState.turnCount = Math.max(DEFAULT_TURN_COUNT, runState.turnCount || DEFAULT_TURN_COUNT) + 1;
1354
+ // NOTE(user-approved): continuations preserve the existing context and session.
1355
+ // Switching between /sub:main and /sub:isolate does not retroactively affect existing runs.
1356
+ runState.contextMode = runState.contextMode ?? (forceMainContext ? "main" : "sub");
1357
+ runState.sessionFile = runState.sessionFile ?? sessionFileForRun ?? makeSubagentSessionFile(runId);
1358
+ runState.persistedSessionBaseOffset = getSessionFileSize(runState.sessionFile);
1359
+ sessionFileForRun = runState.sessionFile;
1360
+ } else {
1361
+ runId = store.nextCommandRunId++;
1362
+ if (forceMainContext) {
1363
+ // Extract main session context as text instead of copying the session file.
1364
+ // This prevents subagents from inheriting the main agent's persona.
1365
+ const subContextResult = buildMainContextText(ctx);
1366
+ const subContextText = typeof subContextResult === "string" ? subContextResult : subContextResult.text;
1367
+ const totalMessageCount = typeof subContextResult === "string" ? 0 : subContextResult.totalMessageCount;
1368
+ const rawMainSessionFile = ctx.sessionManager?.getSessionFile?.() ?? undefined;
1369
+ const mainSessionFile =
1370
+ typeof rawMainSessionFile === "string"
1371
+ ? rawMainSessionFile.replace(/[\r\n\t]+/g, "").trim() || undefined
1372
+ : undefined;
1373
+ if (subContextText || mainSessionFile) {
1374
+ taskForAgent = wrapTaskWithMainContext(taskForAgent, subContextText, {
1375
+ mainSessionFile,
1376
+ totalMessageCount,
1377
+ });
1378
+ } else {
1379
+ ctx.ui.notify(
1380
+ "Main session context is unavailable in this mode. Running with dedicated sub-session.",
1381
+ "warning",
1382
+ );
1383
+ forceMainContext = false;
1384
+ }
1385
+ sessionFileForRun = makeSubagentSessionFile(runId);
1361
1386
  } else {
1362
- ctx.ui.notify(
1363
- "Main session context is unavailable in this mode. Running with dedicated sub-session.",
1364
- "warning",
1365
- );
1366
- forceMainContext = false;
1387
+ sessionFileForRun = makeSubagentSessionFile(runId);
1367
1388
  }
1368
- sessionFileForRun = makeSubagentSessionFile(runId);
1369
- } else {
1370
- sessionFileForRun = makeSubagentSessionFile(runId);
1371
- }
1372
-
1373
- runState = {
1374
- id: runId,
1375
- agent: selectedAgent,
1376
- task: taskForDisplay,
1377
- displayTask: buildSubagentDisplayTaskFallback(taskForDisplay),
1378
- status: "running",
1379
- startedAt: Date.now(),
1380
- lastActivityAt: Date.now(),
1381
- elapsedMs: 0,
1382
- toolCalls: 0,
1383
- lastLine: "",
1384
- lastOutput: "",
1385
- continuedFromRunId,
1386
- turnCount: DEFAULT_TURN_COUNT,
1387
- sessionFile: sessionFileForRun,
1388
- persistedSessionBaseOffset: getSessionFileSize(sessionFileForRun),
1389
- removed: false,
1390
- contextMode: forceMainContext ? "main" : "sub",
1391
- retryCount: 0,
1392
- deliveryMode,
1393
- };
1394
- store.commandRuns.set(runId, runState);
1395
- }
1396
1389
 
1397
- const abortController = new AbortController();
1398
- runState.abortController = abortController;
1390
+ runState = {
1391
+ id: runId,
1392
+ agent: selectedAgent,
1393
+ task: taskForDisplay,
1394
+ displayTask: buildSubagentDisplayTaskFallback(taskForDisplay),
1395
+ status: "running",
1396
+ startedAt: Date.now(),
1397
+ lastActivityAt: Date.now(),
1398
+ elapsedMs: 0,
1399
+ toolCalls: 0,
1400
+ lastLine: "",
1401
+ lastOutput: "",
1402
+ continuedFromRunId,
1403
+ turnCount: DEFAULT_TURN_COUNT,
1404
+ sessionFile: sessionFileForRun,
1405
+ persistedSessionBaseOffset: getSessionFileSize(sessionFileForRun),
1406
+ removed: false,
1407
+ contextMode: forceMainContext ? "main" : "sub",
1408
+ retryCount: 0,
1409
+ deliveryMode,
1410
+ };
1411
+ store.commandRuns.set(runId, runState);
1412
+ }
1399
1413
 
1400
- // Register for abort and completion handling within this session runtime.
1401
- let originSessionFile = "";
1402
- try {
1403
- originSessionFile = normalizePath(ctx.sessionManager.getSessionFile()) ?? "";
1404
- } catch {
1405
- /* ignore */
1406
- }
1407
- store.globalLiveRuns.set(runId, {
1408
- runState,
1409
- abortController,
1410
- originSessionFile,
1411
- });
1414
+ const abortController = new AbortController();
1415
+ runState.abortController = abortController;
1412
1416
 
1413
- store.commandWidgetCtx = ctx as unknown as WidgetRenderCtx;
1414
- updateCommandRunsWidget(store, ctx as unknown as WidgetRenderCtx);
1415
- refreshDisplayTaskInBackground(store, runState, taskForDisplay, ctx);
1417
+ // Register for abort and completion handling within this session runtime.
1418
+ let originSessionFile = "";
1419
+ try {
1420
+ originSessionFile = normalizePath(ctx.sessionManager.getSessionFile()) ?? "";
1421
+ } catch {
1422
+ /* ignore */
1423
+ }
1424
+ store.globalLiveRuns.set(runId, {
1425
+ runState,
1426
+ abortController,
1427
+ originSessionFile,
1428
+ });
1416
1429
 
1417
- const makeDetails = (results: SingleResult[]): SubagentDetails => ({
1418
- mode: "single",
1419
- inheritMainContext: runState.contextMode === "main",
1420
- projectAgentsDir: discovery.projectAgentsDir,
1421
- results,
1422
- });
1430
+ store.commandWidgetCtx = ctx as unknown as WidgetRenderCtx;
1431
+ updateCommandRunsWidget(store, ctx as unknown as WidgetRenderCtx);
1432
+ refreshDisplayTaskInBackground(store, runState, taskForDisplay, ctx);
1423
1433
 
1424
- const contextLabel = hiddenFromMain
1425
- ? "hidden sub-session"
1426
- : runState.contextMode === "main"
1427
- ? "main context"
1428
- : "dedicated sub-session";
1429
- const startedState = continuedFromRunId !== undefined ? "resumed" : "started";
1434
+ const makeDetails = (results: SingleResult[]): SubagentDetails => ({
1435
+ mode: "single",
1436
+ inheritMainContext: runState.contextMode === "main",
1437
+ projectAgentsDir: discovery.projectAgentsDir,
1438
+ results,
1439
+ });
1430
1440
 
1431
- if (!hiddenFromMain) {
1432
- pi.sendMessage(
1433
- {
1434
- customType: "subagent-command",
1435
- content:
1436
- `[subagent:${selectedAgent}#${runId}] ${startedState}` +
1437
- `\nContext: ${contextLabel} · turn ${runState.turnCount}` +
1438
- ``,
1439
- display: false,
1440
- details: {
1441
- runId,
1442
- agent: selectedAgent,
1443
- task: taskForDisplay,
1444
- displayTask: runState.displayTask,
1445
- continuedFromRunId,
1446
- turnCount: runState.turnCount,
1447
- contextMode: runState.contextMode,
1448
- sessionFile: runState.sessionFile,
1449
- persistedSessionBaseOffset: runState.persistedSessionBaseOffset,
1450
- status: startedState,
1451
- startedAt: runState.startedAt,
1452
- elapsedMs: runState.elapsedMs,
1453
- lastActivityAt: runState.lastActivityAt,
1454
- thoughtText: runState.thoughtText,
1455
- runtime: runState.runtime,
1456
- claudeSessionId: runState.claudeSessionId,
1457
- claudeProjectDir: runState.claudeProjectDir,
1441
+ const contextLabel = hiddenFromMain
1442
+ ? "hidden sub-session"
1443
+ : runState.contextMode === "main"
1444
+ ? "main context"
1445
+ : "dedicated sub-session";
1446
+ const startedState = continuedFromRunId !== undefined ? "resumed" : "started";
1447
+
1448
+ if (!hiddenFromMain) {
1449
+ pi.sendMessage(
1450
+ {
1451
+ customType: "subagent-command",
1452
+ content:
1453
+ `[subagent:${selectedAgent}#${runId}] ${startedState}` +
1454
+ `\nContext: ${contextLabel} · turn ${runState.turnCount}` +
1455
+ ``,
1456
+ display: false,
1457
+ details: {
1458
+ runId,
1459
+ agent: selectedAgent,
1460
+ task: taskForDisplay,
1461
+ displayTask: runState.displayTask,
1462
+ continuedFromRunId,
1463
+ turnCount: runState.turnCount,
1464
+ contextMode: runState.contextMode,
1465
+ sessionFile: runState.sessionFile,
1466
+ persistedSessionBaseOffset: runState.persistedSessionBaseOffset,
1467
+ status: startedState,
1468
+ startedAt: runState.startedAt,
1469
+ elapsedMs: runState.elapsedMs,
1470
+ lastActivityAt: runState.lastActivityAt,
1471
+ thoughtText: runState.thoughtText,
1472
+ runtime: runState.runtime,
1473
+ claudeSessionId: runState.claudeSessionId,
1474
+ claudeProjectDir: runState.claudeProjectDir,
1475
+ },
1458
1476
  },
1459
- },
1460
- { deliverAs: "followUp", triggerTurn: false },
1461
- );
1462
- }
1477
+ { deliverAs: "followUp", triggerTurn: false },
1478
+ );
1479
+ }
1463
1480
 
1464
- ctx.ui.notify(
1465
- `${
1466
- continuedFromRunId !== undefined
1467
- ? `Resumed subagent #${runId}: ${selectedAgent}`
1468
- : `Started subagent #${runId}: ${selectedAgent}`
1469
- } (${contextLabel} · turn ${runState.turnCount})`,
1470
- "info",
1471
- );
1481
+ ctx.ui.notify(
1482
+ `${
1483
+ continuedFromRunId !== undefined
1484
+ ? `Resumed subagent #${runId}: ${selectedAgent}`
1485
+ : `Started subagent #${runId}: ${selectedAgent}`
1486
+ } (${contextLabel} · turn ${runState.turnCount})`,
1487
+ "info",
1488
+ );
1472
1489
 
1473
- const tick = setInterval(() => {
1474
- const current = store.commandRuns.get(runId);
1475
- if (current?.status !== "running") {
1476
- clearInterval(tick);
1477
- return;
1478
- }
1479
- current.elapsedMs = Date.now() - current.startedAt;
1480
- updateCommandRunsWidget(store);
1481
- }, RUN_TICK_INTERVAL_MS);
1482
-
1483
- let claudeCheckpointSent = !!runState.claudeSessionId;
1484
- const recordActivity = hiddenFromMain
1485
- ? () => {}
1486
- : createRunActivityRecorder(pi, runState, (model) => resolveContextWindow(ctx, model));
1487
- void (async () => {
1488
- try {
1489
- const { result, retryCount } = await invokeWithAutoRetry({
1490
- maxRetries: MAX_SUBAGENT_AUTO_RETRIES,
1491
- signal: abortController.signal,
1492
- onRetryScheduled: ({ retryIndex, maxRetries, delayMs, reason }) => {
1493
- runState.retryCount = retryIndex;
1494
- runState.lastRetryReason = reason;
1495
- runState.lastActivityAt = Date.now();
1496
- runState.lastLine = `Auto-retrying ${retryIndex}/${maxRetries} in ${Math.ceil(delayMs / 1000)}s: ${reason}`;
1497
- runState.lastOutput = runState.lastLine;
1498
- updateCommandRunsWidget(store);
1499
- ctx.ui.notify(`subagent #${runId} retry ${retryIndex}/${maxRetries}: ${reason}`, "warning");
1500
- },
1501
- invoke: () => {
1502
- runState.persistedSessionBaseOffset = getSessionFileSize(runState.sessionFile);
1503
- return enqueueSubagentInvocation(() =>
1504
- runSingleAgent(
1505
- ctx.cwd,
1506
- agents,
1507
- selectedAgent,
1508
- taskForAgent,
1509
- undefined,
1510
- abortController.signal,
1511
- (partial) => {
1512
- if (runState.removed || store.disposed) return;
1513
- const current = partial.details?.results?.[0];
1514
- if (!current) return;
1515
- updateRunFromResult(runState, current);
1516
- recordActivity();
1517
- if (!claudeCheckpointSent && runState.claudeSessionId) {
1518
- claudeCheckpointSent = true;
1519
- if (!hiddenFromMain) {
1520
- pi.sendMessage(
1521
- {
1522
- customType: "subagent-command" as const,
1523
- content: `[subagent:${selectedAgent}#${runId}] checkpoint`,
1524
- display: false,
1525
- details: {
1526
- runId,
1527
- agent: selectedAgent,
1528
- task: taskForDisplay,
1529
- displayTask: runState.displayTask,
1530
- continuedFromRunId,
1531
- turnCount: runState.turnCount,
1532
- contextMode: runState.contextMode,
1533
- sessionFile: runState.sessionFile,
1534
- persistedSessionBaseOffset: runState.persistedSessionBaseOffset,
1535
- status: "started",
1536
- startedAt: runState.startedAt,
1537
- elapsedMs: runState.elapsedMs,
1538
- lastActivityAt: runState.lastActivityAt,
1539
- runtime: runState.runtime,
1540
- claudeSessionId: runState.claudeSessionId,
1541
- claudeProjectDir: runState.claudeProjectDir,
1542
- },
1543
- },
1544
- { deliverAs: "followUp", triggerTurn: false },
1545
- );
1546
- }
1547
- }
1548
- updateCommandRunsWidget(store);
1549
- },
1550
- makeDetails,
1551
- {
1552
- sessionFile: runState.sessionFile,
1553
- resumeSessionId: runState.claudeSessionId,
1554
- sidecarSessionFile: runState.sessionFile,
1555
- persistedSessionBaseOffset: runState.persistedSessionBaseOffset,
1556
- onDiagnostic: createRunDiagnosticSink(pi, runState),
1557
- },
1558
- ),
1559
- );
1560
- },
1561
- });
1562
- runState.retryCount = retryCount;
1563
-
1564
- if (runState.removed || store.disposed) return;
1565
-
1566
- updateRunFromResult(runState, result);
1567
- const isError = result.exitCode !== 0 || result.stopReason === "error" || result.stopReason === "aborted";
1568
- runState.status = isError ? "error" : "done";
1569
- if (isError) {
1570
- runState.errorClass =
1571
- result.errorClass ??
1572
- classifySubagentFailure({
1573
- failed: true,
1574
- stopReason: result.stopReason,
1575
- exitCode: result.exitCode,
1576
- errorMessage: result.errorMessage,
1577
- stderr: result.stderr,
1578
- output: getFinalOutput(result.messages),
1579
- });
1490
+ const tick = setInterval(() => {
1491
+ const current = store.commandRuns.get(runId);
1492
+ if (current?.status !== "running") {
1493
+ clearInterval(tick);
1494
+ return;
1580
1495
  }
1581
- const terminalLabel = getSubagentTerminalLabel(isError, {
1582
- stopReason: result.stopReason,
1583
- errorClass: runState.errorClass,
1584
- });
1585
- runState.elapsedMs = Date.now() - runState.startedAt;
1496
+ current.elapsedMs = Date.now() - current.startedAt;
1586
1497
  updateCommandRunsWidget(store);
1587
-
1588
- const rawOutput = isError
1589
- ? result.errorMessage || result.stderr || getFinalOutput(result.messages) || "(no output)"
1590
- : getFinalOutput(result.messages) || "(no output)";
1591
- const output =
1592
- isError && rawOutput.length > RUN_OUTPUT_MESSAGE_MAX_CHARS
1593
- ? `${rawOutput.slice(0, RUN_OUTPUT_MESSAGE_MAX_CHARS)}\n\n... [truncated]`
1594
- : rawOutput;
1595
- const usage = formatUsageStats(result.usage, result.model);
1596
-
1597
- runState.lastOutput = rawOutput;
1598
- if (rawOutput) runState.lastLine = getLastNonEmptyLine(rawOutput);
1599
-
1600
- const completionMessage = {
1601
- customType: "subagent-command" as const,
1602
- content:
1603
- `[subagent:${selectedAgent}#${runId}] ${terminalLabel}` +
1604
- `\nPrompt: ${truncateLines(taskForDisplay, 2)}` +
1605
- (usage ? `\nUsage: ${usage}` : "") +
1606
- (runState.retryCount ? `\nRetries: ${runState.retryCount}/${MAX_SUBAGENT_AUTO_RETRIES}` : "") +
1607
- (runState.thoughtText ? `\nThought: ${runState.thoughtText}` : "") +
1608
- `\n\n${output}`,
1609
- display: true,
1610
- details: {
1611
- runId,
1612
- agent: selectedAgent,
1613
- task: taskForDisplay,
1614
- displayTask: runState.displayTask,
1615
- continuedFromRunId,
1616
- turnCount: runState.turnCount,
1617
- contextMode: runState.contextMode,
1618
- sessionFile: runState.sessionFile,
1619
- persistedSessionBaseOffset: runState.persistedSessionBaseOffset,
1620
- startedAt: runState.startedAt,
1621
- elapsedMs: runState.elapsedMs,
1622
- lastActivityAt: runState.lastActivityAt,
1623
- exitCode: result.exitCode,
1498
+ }, RUN_TICK_INTERVAL_MS);
1499
+
1500
+ let claudeCheckpointSent = !!runState.claudeSessionId;
1501
+ const recordActivity = hiddenFromMain
1502
+ ? () => {}
1503
+ : createRunActivityRecorder(pi, runState, (model) => resolveContextWindow(ctx, model));
1504
+ void (async () => {
1505
+ try {
1506
+ const { result, retryCount } = await invokeWithAutoRetry({
1507
+ maxRetries: MAX_SUBAGENT_AUTO_RETRIES,
1508
+ signal: abortController.signal,
1509
+ onRetryScheduled: ({ retryIndex, maxRetries, delayMs, reason }) => {
1510
+ runState.retryCount = retryIndex;
1511
+ runState.lastRetryReason = reason;
1512
+ runState.lastActivityAt = Date.now();
1513
+ runState.lastLine = `Auto-retrying ${retryIndex}/${maxRetries} in ${Math.ceil(delayMs / 1000)}s: ${reason}`;
1514
+ runState.lastOutput = runState.lastLine;
1515
+ updateCommandRunsWidget(store);
1516
+ ctx.ui.notify(`subagent #${runId} retry ${retryIndex}/${maxRetries}: ${reason}`, "warning");
1517
+ },
1518
+ invoke: () => {
1519
+ runState.persistedSessionBaseOffset = getSessionFileSize(runState.sessionFile);
1520
+ return enqueueSubagentInvocation(() =>
1521
+ runSingleAgent(
1522
+ ctx.cwd,
1523
+ agents,
1524
+ selectedAgent,
1525
+ taskForAgent,
1526
+ undefined,
1527
+ abortController.signal,
1528
+ (partial) => {
1529
+ if (runState.removed || store.disposed) return;
1530
+ const current = partial.details?.results?.[0];
1531
+ if (!current) return;
1532
+ updateRunFromResult(runState, current);
1533
+ recordActivity();
1534
+ if (!claudeCheckpointSent && runState.claudeSessionId) {
1535
+ claudeCheckpointSent = true;
1536
+ if (!hiddenFromMain) {
1537
+ pi.sendMessage(
1538
+ {
1539
+ customType: "subagent-command" as const,
1540
+ content: `[subagent:${selectedAgent}#${runId}] checkpoint`,
1541
+ display: false,
1542
+ details: {
1543
+ runId,
1544
+ agent: selectedAgent,
1545
+ task: taskForDisplay,
1546
+ displayTask: runState.displayTask,
1547
+ continuedFromRunId,
1548
+ turnCount: runState.turnCount,
1549
+ contextMode: runState.contextMode,
1550
+ sessionFile: runState.sessionFile,
1551
+ persistedSessionBaseOffset: runState.persistedSessionBaseOffset,
1552
+ status: "started",
1553
+ startedAt: runState.startedAt,
1554
+ elapsedMs: runState.elapsedMs,
1555
+ lastActivityAt: runState.lastActivityAt,
1556
+ runtime: runState.runtime,
1557
+ claudeSessionId: runState.claudeSessionId,
1558
+ claudeProjectDir: runState.claudeProjectDir,
1559
+ },
1560
+ },
1561
+ { deliverAs: "followUp", triggerTurn: false },
1562
+ );
1563
+ }
1564
+ }
1565
+ updateCommandRunsWidget(store);
1566
+ },
1567
+ makeDetails,
1568
+ {
1569
+ sessionFile: runState.sessionFile,
1570
+ resumeSessionId: runState.claudeSessionId,
1571
+ sidecarSessionFile: runState.sessionFile,
1572
+ persistedSessionBaseOffset: runState.persistedSessionBaseOffset,
1573
+ ...(store.asyncTasks?.provider.supported ? { asyncRunId: runState.id } : {}),
1574
+ onDiagnostic: createRunDiagnosticSink(pi, runState),
1575
+ },
1576
+ ),
1577
+ );
1578
+ },
1579
+ });
1580
+ runState.retryCount = retryCount;
1581
+
1582
+ if (runState.removed || store.disposed) {
1583
+ discardRemovedAsyncInvocation(getFinalOutput(result.messages));
1584
+ return;
1585
+ }
1586
+
1587
+ updateRunFromResult(runState, result);
1588
+ const isError = result.exitCode !== 0 || result.stopReason === "error" || result.stopReason === "aborted";
1589
+ runState.status = isError ? "error" : "done";
1590
+ if (isError) {
1591
+ runState.errorClass =
1592
+ result.errorClass ??
1593
+ classifySubagentFailure({
1594
+ failed: true,
1595
+ stopReason: result.stopReason,
1596
+ exitCode: result.exitCode,
1597
+ errorMessage: result.errorMessage,
1598
+ stderr: result.stderr,
1599
+ output: getFinalOutput(result.messages),
1600
+ });
1601
+ }
1602
+ const terminalLabel = getSubagentTerminalLabel(isError, {
1624
1603
  stopReason: result.stopReason,
1625
- usage: result.usage,
1626
- model: result.model,
1627
- source: result.agentSource,
1628
- errorClass: runState.errorClass,
1629
- peakContextTokens: runState.peakContextTokens,
1630
- lastToolName: runState.lastToolName,
1631
- lastToolOutputChars: runState.lastToolOutputChars,
1632
- thoughtText: runState.thoughtText,
1633
- retryCount: runState.retryCount,
1634
- status: runState.status,
1635
- runtime: runState.runtime,
1636
- claudeSessionId: runState.claudeSessionId,
1637
- claudeProjectDir: runState.claudeProjectDir,
1638
- },
1639
- };
1640
- if (hiddenFromMain) {
1641
- finalizeHumanOnlyCompletion(
1642
- store,
1643
- ctx,
1644
- runId,
1645
- `Hidden subagent #${runId} · ${selectedAgent}`,
1646
- completionMessage.content,
1647
- `hidden subagent #${runId} (${selectedAgent}) ${terminalLabel}`,
1648
- terminalLabel === "completed" ? "info" : terminalLabel === "aborted" ? "warning" : "error",
1649
- );
1650
- } else {
1651
- deliverOrQueueCompletion(store, pi, ctx, runId, runState, completionMessage);
1652
- ctx.ui.notify(
1653
- `subagent #${runId} (${selectedAgent}) ${terminalLabel}`,
1654
- terminalLabel === "completed" ? "info" : terminalLabel === "aborted" ? "warning" : "error",
1655
- );
1656
- }
1657
- } catch (error: any) {
1658
- if (runState.removed || store.disposed) return;
1659
- runState.status = "error";
1660
- runState.errorClass = isSubagentAbort({
1661
- signal: runState.abortController?.signal,
1662
- autoAbortReason: runState.autoAbortReason,
1663
- error,
1664
- })
1665
- ? "aborted"
1666
- : "process_error";
1667
- const terminalLabel = getSubagentTerminalLabel(true, { errorClass: runState.errorClass });
1668
- runState.elapsedMs = Date.now() - runState.startedAt;
1669
- runState.lastLine =
1670
- runState.autoAbortReason ?? (error?.message ? String(error.message) : "Subagent execution failed");
1671
- runState.lastOutput = runState.lastLine;
1672
-
1673
- const cmdErrorMessage = {
1674
- customType: "subagent-command" as const,
1675
- content:
1676
- `[subagent:${selectedAgent}#${runId}] ${terminalLabel}` +
1677
- `\nPrompt: ${truncateLines(taskForDisplay, 2)}` +
1678
- `\n\n${runState.lastLine}`,
1679
- display: true,
1680
- details: {
1681
- runId,
1682
- agent: selectedAgent,
1683
- task: taskForDisplay,
1684
- displayTask: runState.displayTask,
1685
- continuedFromRunId,
1686
- turnCount: runState.turnCount,
1687
- contextMode: runState.contextMode,
1688
- sessionFile: runState.sessionFile,
1689
- persistedSessionBaseOffset: runState.persistedSessionBaseOffset,
1690
- startedAt: runState.startedAt,
1691
- elapsedMs: runState.elapsedMs,
1692
- lastActivityAt: runState.lastActivityAt,
1693
- error: runState.lastLine,
1694
1604
  errorClass: runState.errorClass,
1695
- stopReason: runState.errorClass === "aborted" ? "aborted" : undefined,
1696
- peakContextTokens: runState.peakContextTokens,
1697
- lastToolName: runState.lastToolName,
1698
- lastToolOutputChars: runState.lastToolOutputChars,
1699
- thoughtText: runState.thoughtText,
1700
- status: runState.status,
1701
- runtime: runState.runtime,
1702
- claudeSessionId: runState.claudeSessionId,
1703
- claudeProjectDir: runState.claudeProjectDir,
1704
- },
1705
- };
1706
- if (hiddenFromMain) {
1707
- finalizeHumanOnlyCompletion(
1708
- store,
1709
- ctx,
1710
- runId,
1711
- `Hidden subagent #${runId} · ${selectedAgent}`,
1712
- cmdErrorMessage.content,
1713
- `hidden subagent #${runId} ${terminalLabel}: ${runState.lastLine}`,
1714
- terminalLabel === "aborted" ? "warning" : "error",
1715
- );
1716
- } else {
1717
- deliverOrQueueCompletion(store, pi, ctx, runId, runState, cmdErrorMessage);
1718
- ctx.ui.notify(
1719
- `subagent #${runId} ${terminalLabel}: ${runState.lastLine}`,
1720
- terminalLabel === "aborted" ? "warning" : "error",
1721
- );
1722
- }
1723
- } finally {
1724
- clearInterval(tick);
1725
- runState.abortController = undefined;
1726
- runState.abortPending = false;
1727
- if (!store.disposed) {
1728
- trimCommandRunHistory(store, {
1729
- maxRuns: 10,
1730
- ctx,
1731
- pi,
1732
- updateWidget: false,
1733
- removalReason: "trim",
1734
1605
  });
1606
+ runState.elapsedMs = Date.now() - runState.startedAt;
1735
1607
  updateCommandRunsWidget(store);
1608
+
1609
+ const rawOutput = isError
1610
+ ? result.errorMessage || result.stderr || getFinalOutput(result.messages) || "(no output)"
1611
+ : getFinalOutput(result.messages) || "(no output)";
1612
+ const output =
1613
+ isError && rawOutput.length > RUN_OUTPUT_MESSAGE_MAX_CHARS
1614
+ ? `${rawOutput.slice(0, RUN_OUTPUT_MESSAGE_MAX_CHARS)}\n\n... [truncated]`
1615
+ : rawOutput;
1616
+ const usage = formatUsageStats(result.usage, result.model);
1617
+
1618
+ runState.lastOutput = rawOutput;
1619
+ if (rawOutput) runState.lastLine = getLastNonEmptyLine(rawOutput);
1620
+
1621
+ const completionMessage = {
1622
+ customType: "subagent-command" as const,
1623
+ content:
1624
+ `[subagent:${selectedAgent}#${runId}] ${terminalLabel}` +
1625
+ `\nPrompt: ${truncateLines(taskForDisplay, 2)}` +
1626
+ (usage ? `\nUsage: ${usage}` : "") +
1627
+ (runState.retryCount ? `\nRetries: ${runState.retryCount}/${MAX_SUBAGENT_AUTO_RETRIES}` : "") +
1628
+ (runState.thoughtText ? `\nThought: ${runState.thoughtText}` : "") +
1629
+ `\n\n${output}`,
1630
+ display: true,
1631
+ details: {
1632
+ runId,
1633
+ agent: selectedAgent,
1634
+ task: taskForDisplay,
1635
+ displayTask: runState.displayTask,
1636
+ continuedFromRunId,
1637
+ turnCount: runState.turnCount,
1638
+ contextMode: runState.contextMode,
1639
+ sessionFile: runState.sessionFile,
1640
+ persistedSessionBaseOffset: runState.persistedSessionBaseOffset,
1641
+ startedAt: runState.startedAt,
1642
+ elapsedMs: runState.elapsedMs,
1643
+ lastActivityAt: runState.lastActivityAt,
1644
+ exitCode: result.exitCode,
1645
+ stopReason: result.stopReason,
1646
+ usage: result.usage,
1647
+ model: result.model,
1648
+ source: result.agentSource,
1649
+ errorClass: runState.errorClass,
1650
+ peakContextTokens: runState.peakContextTokens,
1651
+ lastToolName: runState.lastToolName,
1652
+ lastToolOutputChars: runState.lastToolOutputChars,
1653
+ thoughtText: runState.thoughtText,
1654
+ retryCount: runState.retryCount,
1655
+ status: runState.status,
1656
+ runtime: runState.runtime,
1657
+ claudeSessionId: runState.claudeSessionId,
1658
+ claudeProjectDir: runState.claudeProjectDir,
1659
+ },
1660
+ };
1661
+ if (hiddenFromMain) {
1662
+ finalizeHumanOnlyCompletion(
1663
+ store,
1664
+ ctx,
1665
+ runId,
1666
+ `Hidden subagent #${runId} · ${selectedAgent}`,
1667
+ completionMessage.content,
1668
+ `hidden subagent #${runId} (${selectedAgent}) ${terminalLabel}`,
1669
+ terminalLabel === "completed" ? "info" : terminalLabel === "aborted" ? "warning" : "error",
1670
+ );
1671
+ } else {
1672
+ deliverOrQueueCompletion(store, pi, ctx, runId, runState, completionMessage);
1673
+ if (terminalLabel !== "aborted" || !isExpectedTrackedCancellation())
1674
+ ctx.ui.notify(
1675
+ `subagent #${runId} (${selectedAgent}) ${terminalLabel}`,
1676
+ terminalLabel === "completed" ? "info" : terminalLabel === "aborted" ? "warning" : "error",
1677
+ );
1678
+ }
1679
+ } catch (error: any) {
1680
+ if (runState.removed || store.disposed) {
1681
+ discardRemovedAsyncInvocation(String(error));
1682
+ return;
1683
+ }
1684
+ runState.status = "error";
1685
+ runState.errorClass = isSubagentAbort({
1686
+ signal: runState.abortController?.signal,
1687
+ autoAbortReason: runState.autoAbortReason,
1688
+ error,
1689
+ })
1690
+ ? "aborted"
1691
+ : "process_error";
1692
+ const terminalLabel = getSubagentTerminalLabel(true, { errorClass: runState.errorClass });
1693
+ runState.elapsedMs = Date.now() - runState.startedAt;
1694
+ runState.lastLine =
1695
+ runState.autoAbortReason ?? (error?.message ? String(error.message) : "Subagent execution failed");
1696
+ runState.lastOutput = runState.lastLine;
1697
+
1698
+ const cmdErrorMessage = {
1699
+ customType: "subagent-command" as const,
1700
+ content:
1701
+ `[subagent:${selectedAgent}#${runId}] ${terminalLabel}` +
1702
+ `\nPrompt: ${truncateLines(taskForDisplay, 2)}` +
1703
+ `\n\n${runState.lastLine}`,
1704
+ display: true,
1705
+ details: {
1706
+ runId,
1707
+ agent: selectedAgent,
1708
+ task: taskForDisplay,
1709
+ displayTask: runState.displayTask,
1710
+ continuedFromRunId,
1711
+ turnCount: runState.turnCount,
1712
+ contextMode: runState.contextMode,
1713
+ sessionFile: runState.sessionFile,
1714
+ persistedSessionBaseOffset: runState.persistedSessionBaseOffset,
1715
+ startedAt: runState.startedAt,
1716
+ elapsedMs: runState.elapsedMs,
1717
+ lastActivityAt: runState.lastActivityAt,
1718
+ error: runState.lastLine,
1719
+ errorClass: runState.errorClass,
1720
+ stopReason: runState.errorClass === "aborted" ? "aborted" : undefined,
1721
+ peakContextTokens: runState.peakContextTokens,
1722
+ lastToolName: runState.lastToolName,
1723
+ lastToolOutputChars: runState.lastToolOutputChars,
1724
+ thoughtText: runState.thoughtText,
1725
+ status: runState.status,
1726
+ runtime: runState.runtime,
1727
+ claudeSessionId: runState.claudeSessionId,
1728
+ claudeProjectDir: runState.claudeProjectDir,
1729
+ },
1730
+ };
1731
+ if (hiddenFromMain) {
1732
+ finalizeHumanOnlyCompletion(
1733
+ store,
1734
+ ctx,
1735
+ runId,
1736
+ `Hidden subagent #${runId} · ${selectedAgent}`,
1737
+ cmdErrorMessage.content,
1738
+ `hidden subagent #${runId} ${terminalLabel}: ${runState.lastLine}`,
1739
+ terminalLabel === "aborted" ? "warning" : "error",
1740
+ );
1741
+ } else {
1742
+ deliverOrQueueCompletion(store, pi, ctx, runId, runState, cmdErrorMessage);
1743
+ if (terminalLabel !== "aborted" || !isExpectedTrackedCancellation())
1744
+ ctx.ui.notify(
1745
+ `subagent #${runId} ${terminalLabel}: ${runState.lastLine}`,
1746
+ terminalLabel === "aborted" ? "warning" : "error",
1747
+ );
1748
+ }
1749
+ } finally {
1750
+ clearInterval(tick);
1751
+ runState.abortController = undefined;
1752
+ runState.abortPending = false;
1753
+ if (!store.disposed) {
1754
+ trimCommandRunHistory(store, {
1755
+ maxRuns: 10,
1756
+ ctx,
1757
+ pi,
1758
+ updateWidget: false,
1759
+ removalReason: "trim",
1760
+ });
1761
+ updateCommandRunsWidget(store);
1762
+ }
1736
1763
  }
1737
- }
1738
- })();
1764
+ })();
1765
+ };
1766
+ return store.asyncTasks
1767
+ ? store.asyncTasks.invoke(taskForDisplay.slice(0, 500), undefined, launch, hiddenFromMain)
1768
+ : launch();
1739
1769
  },
1740
1770
  };
1741
1771