@pi-unipi/background-tasks 2.16.0 → 2.17.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.
Files changed (46) hide show
  1. package/README.md +21 -27
  2. package/package.json +3 -4
  3. package/src/cards.ts +76 -0
  4. package/src/child-process.ts +1 -1
  5. package/src/config.ts +0 -42
  6. package/src/context-visible-conversation-v2.ts +1 -1
  7. package/src/delegate/artifacts.ts +1 -1
  8. package/src/delegate/launch.ts +17 -30
  9. package/src/delegate/result-package.ts +1 -1
  10. package/src/delegate/runner.ts +1 -20
  11. package/src/delegate/seed.ts +1 -1
  12. package/src/delegate-extension.ts +16 -168
  13. package/src/index.ts +53 -25
  14. package/src/json-utils.ts +56 -0
  15. package/src/package-assets.ts +51 -0
  16. package/src/registry.ts +8 -459
  17. package/src/task-manager.ts +13 -2
  18. package/src/tools.ts +4 -189
  19. package/src/types.ts +17 -70
  20. package/extensions/anthropic-attribution.ts +0 -1
  21. package/extensions/fusion-child.ts +0 -1
  22. package/src/anthropic-attribution-path.ts +0 -21
  23. package/src/anthropic-attribution.ts +0 -1983
  24. package/src/attested-pi-run.ts +0 -612
  25. package/src/fixtures/fusion-golden-bytes.json +0 -310
  26. package/src/fixtures/fusion-validate-golden-bytes.json +0 -282
  27. package/src/fusion/artifacts.ts +0 -967
  28. package/src/fusion/budget.ts +0 -1162
  29. package/src/fusion/child-protocol.ts +0 -305
  30. package/src/fusion/claude-cache.ts +0 -207
  31. package/src/fusion/clean-context.ts +0 -91
  32. package/src/fusion/config.ts +0 -449
  33. package/src/fusion/context.ts +0 -265
  34. package/src/fusion/evaluation.ts +0 -800
  35. package/src/fusion/orchestrator.ts +0 -1288
  36. package/src/fusion/output-contract.ts +0 -34
  37. package/src/fusion/pi-child.ts +0 -2373
  38. package/src/fusion/prompts.ts +0 -345
  39. package/src/fusion/result-package.ts +0 -959
  40. package/src/fusion/source-policy.ts +0 -257
  41. package/src/fusion/types.ts +0 -1139
  42. package/src/fusion/web-fetch.ts +0 -1060
  43. package/src/fusion/workflows.ts +0 -184
  44. package/src/fusion-child-extension.ts +0 -1052
  45. package/src/fusion-extension.ts +0 -1293
  46. package/src/ui/fusion-model-selector.ts +0 -322
package/src/registry.ts CHANGED
@@ -7,6 +7,7 @@ import type { Api, Model } from '@earendil-works/pi-ai';
7
7
  import type { ExtensionContext } from '@earendil-works/pi-coding-agent';
8
8
  import { formatSize } from '@earendil-works/pi-coding-agent';
9
9
  import {
10
+ appendOutputTail,
10
11
  boundedRead,
11
12
  deriveTaskNameFromCommand,
12
13
  escapeXml,
@@ -26,9 +27,7 @@ import {
26
27
  type BgTaskSnapshot,
27
28
  type JsonObject,
28
29
  type KillKind,
29
- type StartAttestedPiTaskOptions,
30
30
  type StartDelegateTaskOptions,
31
- type StartManagedTaskOptions,
32
31
  type StartTaskOptions,
33
32
  type TaskContextUsage,
34
33
  type TaskStatus,
@@ -41,30 +40,13 @@ import type {
41
40
  BackgroundTaskSpawn,
42
41
  } from './child-process.js';
43
42
  export type { BackgroundTaskChildProcess, BackgroundTaskContext, BackgroundTaskSpawn };
44
- import {
45
- ATTESTED_TASK_ID_PATTERN,
46
- attestedPiChildEnv,
47
- buildAttestedPiArgv,
48
- buildPiTaskAttestation,
49
- closeAndFsyncOutputStream,
50
- gitAuthoritySnapshot,
51
- gitRepoRoot,
52
- makeAttestedTaskId,
53
- makeAttestedTaskPaths,
54
- observePiOAuth,
55
- parsePiJsonEvents,
56
- resolveReportPath,
57
- spawnAndCapturePi,
58
- writeFileFsynced,
59
- writeJsonAtomic,
60
- } from './attested-pi-run.js';
43
+ import { closeAndFsyncOutputStream, writeJsonAtomic } from './json-utils.js';
61
44
  import {
62
45
  assertWindowsCommandLineWithinLimit,
63
46
  piLaunchArgv,
64
47
  resolvePiLaunch,
65
48
  type PiLaunchSpec,
66
49
  } from './pi-launch.js';
67
- import { resolveAnthropicAttributionExtensionPath } from './anthropic-attribution-path.js';
68
50
  import {
69
51
  runWindowsTaskkill,
70
52
  type TaskkillOutcome,
@@ -939,128 +921,6 @@ export class BackgroundTaskRegistry {
939
921
  }
940
922
  }
941
923
 
942
- /**
943
- * Track an in-process asynchronous workflow through the same durable task,
944
- * notification, status, log, and cancellation surfaces as child processes.
945
- * The supplied completion promise must own all workflow cleanup before it
946
- * settles; terminal publication happens only after that settlement.
947
- */
948
- async startManagedTask(
949
- ctx: BackgroundTaskContext,
950
- request: StartManagedTaskOptions,
951
- ): Promise<BgTask> {
952
- if (this.shuttingDown)
953
- throw new Error('Cannot start a managed background task while Pi is shutting down');
954
- if (!/^[a-zA-Z0-9_.-]+$/u.test(request.id))
955
- throw new Error(`Managed background task id is invalid: ${request.id}`);
956
- if (this.tasks.has(request.id))
957
- throw new Error(`Background task id already exists: ${request.id}`);
958
-
959
- const dir = await this.ensureRuntimeDir(ctx);
960
- const outputAbsPath = join(dir.abs, `${request.id}.output`);
961
- const metadataAbsPath = join(dir.abs, `${request.id}.json`);
962
- const outputPath = join(dir.display, `${request.id}.output`);
963
- const task: BgTask = {
964
- id: request.id,
965
- name: normalizeTaskName(request.name) ?? 'Managed background task',
966
- command: request.command,
967
- description: request.description,
968
- status: 'running',
969
- outputPath,
970
- outputAbsPath,
971
- metadataAbsPath,
972
- cwd: ctx.cwd,
973
- startTime: this.now(),
974
- exitCode: undefined,
975
- pid: undefined,
976
- bytesWritten: 0,
977
- isAgent: request.isAgent,
978
- notified: false,
979
- notifyOnCompletion: request.notifyOnCompletion,
980
- triggerOnCompletion: request.triggerOnCompletion,
981
- fusion: request.fusion,
982
- managedCancel: request.cancel,
983
- managedStopWaitMs: request.stopWaitMs,
984
- terminalPublicationGate: request.terminalPublicationGate,
985
- waiters: [],
986
- };
987
- this.tasks.set(task.id, task);
988
- const stream = createWriteStream(outputAbsPath, { flags: 'a', encoding: 'utf8' });
989
- task.stream = stream;
990
- stream.on('error', (error) => {
991
- task.error = `Output file write failed: ${error.message}`;
992
- if (task.status === 'running' && !task.managedCancelRequested) {
993
- task.managedCancelRequested = true;
994
- try {
995
- request.cancel();
996
- } catch (cancelError) {
997
- task.error = `${task.error}; cancellation failed: ${BackgroundTaskRegistry.errorMessage(cancelError)}`;
998
- }
999
- }
1000
- });
1001
-
1002
- try {
1003
- await this.writeMetadata(task);
1004
- this.onChange();
1005
- } catch (error) {
1006
- this.tasks.delete(task.id);
1007
- if (!stream.destroyed) stream.destroy();
1008
- try {
1009
- request.cancel();
1010
- } catch (cancelError) {
1011
- this.logger.error(
1012
- `[background-tasks] managed task cancellation after metadata failure also failed for ${task.id}:`,
1013
- cancelError,
1014
- );
1015
- }
1016
- throw new Error(
1017
- `Failed to register managed background task: ${BackgroundTaskRegistry.errorMessage(error)}`,
1018
- );
1019
- }
1020
-
1021
- void request.completion
1022
- .then(
1023
- () => this.finalizeTask(task, 'completed', 0),
1024
- (error: unknown) => {
1025
- const message = BackgroundTaskRegistry.errorMessage(error);
1026
- const killed = task.killKind === 'user' || task.killKind === 'shutdown';
1027
- return this.finalizeTask(task, killed ? 'killed' : 'failed', null, undefined, message);
1028
- },
1029
- )
1030
- .catch((error: unknown) => {
1031
- this.logger.error(
1032
- `[background-tasks] managed task finalization failed for ${task.id}:`,
1033
- error,
1034
- );
1035
- });
1036
- return task;
1037
- }
1038
-
1039
- async updateManagedTask(task: BgTask, state: string, line?: string): Promise<void> {
1040
- if (task.status !== 'running' || task.fusion === undefined) return;
1041
- task.fusion.state = state;
1042
- if (line !== undefined && line.length > 0) this.writeNotice(task, `${line}\n`);
1043
- await this.writeMetadata(task);
1044
- this.onChange();
1045
- }
1046
-
1047
- /** Claim deferred Fusion usage exactly once before returning it from bg_result. */
1048
- async claimFusionUsage(task: BgTask): Promise<boolean> {
1049
- if (task.fusion === undefined) throw new Error(`Task ${task.id} is not a Fusion task`);
1050
- let claimed = false;
1051
- const write = async () => {
1052
- if (!task.fusion || task.fusion.usageDelivered) return;
1053
- task.fusion.usageDelivered = true;
1054
- await writeJsonAtomic(task.metadataAbsPath, snapshot(task));
1055
- claimed = true;
1056
- };
1057
- const previous = task.metadataWriteChain ?? Promise.resolve();
1058
- const next = previous.then(write, write);
1059
- task.metadataWriteChain = next.catch(() => undefined);
1060
- await next;
1061
- return claimed;
1062
- }
1063
-
1064
924
  /**
1065
925
  * Start a prepared delegate child.
1066
926
  *
@@ -1200,305 +1060,6 @@ export class BackgroundTaskRegistry {
1200
1060
  }
1201
1061
  }
1202
1062
 
1203
- async startAttestedPiTask(
1204
- ctx: BackgroundTaskContext,
1205
- request: StartAttestedPiTaskOptions,
1206
- ): Promise<BgTask> {
1207
- if (this.shuttingDown)
1208
- throw new Error('Cannot start an attested Pi task while Pi is shutting down');
1209
-
1210
- const attributionExtensionPath =
1211
- request.provider === 'anthropic' ? resolveAnthropicAttributionExtensionPath() : undefined;
1212
- const argv = buildAttestedPiArgv(request, attributionExtensionPath);
1213
- const attestedPiLaunch = resolvePiLaunch({ platform: this.platform });
1214
- assertWindowsCommandLineWithinLimit(
1215
- attestedPiLaunch,
1216
- argv.slice(1),
1217
- this.platform,
1218
- 'attested-pi-run',
1219
- );
1220
-
1221
- const dir = await this.ensureRuntimeDir(ctx);
1222
- const id = makeAttestedTaskId();
1223
- if (!ATTESTED_TASK_ID_PATTERN.test(id))
1224
- throw new Error('Generated attested task id is invalid');
1225
- const paths = makeAttestedTaskPaths(dir.abs, dir.display, id);
1226
- const promptBytes = Buffer.from(request.prompt, 'utf8');
1227
- const reportAbsPath = await resolveReportPath(ctx.cwd, request.reportPath);
1228
- const auth = observePiOAuth(ctx, request.provider, request.model);
1229
- const repoRootRealpath = await gitRepoRoot(ctx.cwd);
1230
- const cwdRealpath = await realpath(ctx.cwd);
1231
- const startAuthority = await gitAuthoritySnapshot(ctx.cwd);
1232
- if (!startAuthority.clean)
1233
- throw new Error('Attested Pi task requires a clean worktree at start');
1234
- const timeoutSeconds =
1235
- typeof request.timeoutSeconds === 'number' &&
1236
- Number.isFinite(request.timeoutSeconds) &&
1237
- request.timeoutSeconds > 0
1238
- ? Math.floor(request.timeoutSeconds)
1239
- : undefined;
1240
-
1241
- const task: BgTask = {
1242
- id,
1243
- name: normalizeTaskName(request.name) ?? 'Attested Pi task',
1244
- command: argv.map(shellQuote).join(' '),
1245
- status: 'running',
1246
- outputPath: paths.outputPath,
1247
- outputAbsPath: paths.outputAbsPath,
1248
- metadataAbsPath: paths.metadataAbsPath,
1249
- eventsAbsPath: paths.eventsAbsPath,
1250
- stderrAbsPath: paths.stderrAbsPath,
1251
- wrapperAbsPath: paths.wrapperAbsPath,
1252
- attestationAbsPath: paths.attestationAbsPath,
1253
- cwd: ctx.cwd,
1254
- startTime: this.now(),
1255
- exitCode: undefined,
1256
- pid: undefined,
1257
- bytesWritten: 0,
1258
- isAgent: true,
1259
- notified: false,
1260
- notifyOnCompletion: false,
1261
- triggerOnCompletion: false,
1262
- timeoutSeconds,
1263
- attestationPath: paths.attestationPath,
1264
- attestedPi: {
1265
- eventsPath: paths.eventsPath,
1266
- stderrPath: paths.stderrPath,
1267
- wrapperPath: paths.wrapperPath,
1268
- attestationPath: paths.attestationPath,
1269
- },
1270
- waiters: [],
1271
- };
1272
- this.tasks.set(id, task);
1273
-
1274
- await writeFileFsynced(paths.outputAbsPath, '');
1275
- await writeFileFsynced(paths.eventsAbsPath, '');
1276
- await writeFileFsynced(paths.stderrAbsPath, '');
1277
- await writeFileFsynced(
1278
- paths.wrapperAbsPath,
1279
- 'direct-spawn attested Pi task; no shell telemetry wrapper is used\n',
1280
- );
1281
- await this.writeMetadata(task);
1282
-
1283
- const captured = spawnAndCapturePi(
1284
- this.spawn,
1285
- argv,
1286
- {
1287
- cwd: ctx.cwd,
1288
- detached: this.platform !== 'win32',
1289
- shell: false,
1290
- stdio: ['ignore', 'pipe', 'pipe'],
1291
- env: attestedPiChildEnv(this.env),
1292
- windowsHide: true,
1293
- },
1294
- this.platform,
1295
- attestedPiLaunch,
1296
- );
1297
- task.child = captured.child;
1298
- task.pid = captured.child.pid;
1299
- await this.writeMetadata(task);
1300
- this.onChange();
1301
-
1302
- captured.child.on('error', (error) => {
1303
- void this.finalizeAttestedPiTask(
1304
- task,
1305
- paths,
1306
- argv,
1307
- cwdRealpath,
1308
- repoRootRealpath,
1309
- startAuthority,
1310
- auth,
1311
- promptBytes,
1312
- reportAbsPath,
1313
- captured.stdoutChunks,
1314
- captured.stderrChunks,
1315
- 'failed',
1316
- null,
1317
- null,
1318
- error.message,
1319
- );
1320
- });
1321
-
1322
- captured.child.on('close', (code, signalName) => {
1323
- let status: TaskStatus = (code ?? 0) === 0 && signalName === null ? 'completed' : 'failed';
1324
- let error: string | undefined;
1325
- if (task.killKind === 'timeout') {
1326
- status = 'failed';
1327
- error = task.error ?? `Timed out after ${String(timeoutSeconds)}s`;
1328
- } else if (task.killKind === 'user' || task.killKind === 'shutdown') {
1329
- status = 'killed';
1330
- error = task.error;
1331
- } else if (status === 'failed') {
1332
- const exitCode = code === null ? 'null' : String(code);
1333
- error = `Exited with code ${exitCode}${signalName ? ` (${signalName})` : ''}`;
1334
- }
1335
- void this.finalizeAttestedPiTask(
1336
- task,
1337
- paths,
1338
- argv,
1339
- cwdRealpath,
1340
- repoRootRealpath,
1341
- startAuthority,
1342
- auth,
1343
- promptBytes,
1344
- reportAbsPath,
1345
- captured.stdoutChunks,
1346
- captured.stderrChunks,
1347
- status,
1348
- code,
1349
- signalName,
1350
- error,
1351
- );
1352
- });
1353
-
1354
- if (timeoutSeconds !== undefined) {
1355
- task.timeoutHandle = setTimeout(() => {
1356
- if (task.status !== 'running') return;
1357
- task.killKind = 'timeout';
1358
- task.error = `Timed out after ${String(timeoutSeconds)}s`;
1359
- try {
1360
- this.requestKill(task, 'SIGTERM');
1361
- } catch (error) {
1362
- void this.finalizeAttestedPiTask(
1363
- task,
1364
- paths,
1365
- argv,
1366
- cwdRealpath,
1367
- repoRootRealpath,
1368
- startAuthority,
1369
- auth,
1370
- promptBytes,
1371
- reportAbsPath,
1372
- captured.stdoutChunks,
1373
- captured.stderrChunks,
1374
- 'failed',
1375
- null,
1376
- null,
1377
- error instanceof Error ? error.message : String(error),
1378
- );
1379
- }
1380
- }, timeoutSeconds * 1000);
1381
- }
1382
-
1383
- return task;
1384
- }
1385
-
1386
- private async finalizeAttestedPiTask(
1387
- task: BgTask,
1388
- paths: ReturnType<typeof makeAttestedTaskPaths>,
1389
- argv: string[],
1390
- cwdRealpath: string,
1391
- repoRootRealpath: string,
1392
- startAuthority: Awaited<ReturnType<typeof gitAuthoritySnapshot>>,
1393
- auth: ReturnType<typeof observePiOAuth>,
1394
- promptBytes: Buffer,
1395
- reportAbsPath: string,
1396
- stdoutChunks: Buffer[],
1397
- stderrChunks: Buffer[],
1398
- status: TaskStatus,
1399
- exitCode: number | null,
1400
- signal: NodeJS.Signals | null,
1401
- error?: string,
1402
- ): Promise<void> {
1403
- if (task.finalized) return;
1404
- task.finalized = true;
1405
- if (task.timeoutHandle) clearTimeout(task.timeoutHandle);
1406
- if (task.killEscalationTimer !== undefined) {
1407
- clearTimeout(task.killEscalationTimer);
1408
- task.killEscalationTimer = undefined;
1409
- }
1410
- let finalStatus = status;
1411
- let finalError = error;
1412
- const forceFailure = await this.awaitWindowsForceBeforeTerminal(task);
1413
- if (forceFailure !== undefined) {
1414
- finalStatus = 'failed';
1415
- finalError = BackgroundTaskRegistry.appendTaskError(finalError, forceFailure.message);
1416
- }
1417
- task.exitCode = exitCode;
1418
- task.signal = signal;
1419
- task.endTime = this.now();
1420
- if (finalError) task.error = finalError;
1421
-
1422
- const rawEvents = Buffer.concat(stdoutChunks);
1423
- const rawStderr = Buffer.concat(stderrChunks);
1424
- await writeFileFsynced(paths.eventsAbsPath, rawEvents);
1425
- await writeFileFsynced(paths.stderrAbsPath, rawStderr);
1426
-
1427
- let parsed: ReturnType<typeof parsePiJsonEvents> | undefined;
1428
- if (finalStatus === 'completed') {
1429
- try {
1430
- parsed = parsePiJsonEvents(rawEvents);
1431
- task.model = parsed.providerScopedModelId;
1432
- task.tokenUsage = {
1433
- input: parsed.tokenUsage.input,
1434
- output: parsed.tokenUsage.output,
1435
- cacheRead: parsed.tokenUsage.cacheRead,
1436
- cacheWrite: parsed.tokenUsage.cacheWrite,
1437
- totalTokens: parsed.tokenUsage.totalTokens,
1438
- };
1439
- if (parsed.tokenUsage.costTotal !== undefined)
1440
- task.tokenUsage.costTotal = parsed.tokenUsage.costTotal;
1441
- task.toolUsage = parsed.toolUsage;
1442
- const outputBytes = Buffer.from(parsed.humanTranscript, 'utf8');
1443
- task.bytesWritten = outputBytes.length;
1444
- await writeFileFsynced(paths.outputAbsPath, outputBytes);
1445
- } catch (parseError) {
1446
- finalStatus = 'failed';
1447
- task.error = parseError instanceof Error ? parseError.message : String(parseError);
1448
- const outputBytes = Buffer.from(`[attested Pi task error: ${task.error}]\n`, 'utf8');
1449
- task.bytesWritten = outputBytes.length;
1450
- await writeFileFsynced(paths.outputAbsPath, outputBytes);
1451
- }
1452
- } else {
1453
- const outputBytes = Buffer.from(rawStderr.toString('utf8'), 'utf8');
1454
- task.bytesWritten = outputBytes.length;
1455
- await writeFileFsynced(paths.outputAbsPath, outputBytes);
1456
- }
1457
-
1458
- try {
1459
- if (finalStatus === 'completed' && parsed) {
1460
- const finishAuthority = await gitAuthoritySnapshot(task.cwd);
1461
- const completedSnapshot: BgTaskSnapshot = { ...snapshot(task), status: 'completed' };
1462
- await this.writeMetadataSnapshot(task, completedSnapshot);
1463
- const attestation = await buildPiTaskAttestation({
1464
- task: completedSnapshot,
1465
- paths,
1466
- sessionDir: dirNameFromDisplay(paths.outputPath),
1467
- argv,
1468
- cwdRealpath,
1469
- repoRootRealpath,
1470
- startAuthority,
1471
- finishAuthority,
1472
- parsedEvents: parsed,
1473
- auth,
1474
- prompt: promptBytes,
1475
- reportAbsPath,
1476
- });
1477
- await writeJsonAtomic(paths.attestationAbsPath, attestation);
1478
- } else {
1479
- await this.writeMetadataSnapshot(task, { ...snapshot(task), status: finalStatus });
1480
- }
1481
- } catch (attestationError) {
1482
- finalStatus = 'failed';
1483
- task.error =
1484
- attestationError instanceof Error ? attestationError.message : String(attestationError);
1485
- await this.writeMetadataSnapshot(task, { ...snapshot(task), status: 'failed' }).catch(
1486
- (metadataError: unknown) => {
1487
- this.logger.error(
1488
- `[background-tasks] failed to write failed attested metadata for ${task.id}:`,
1489
- metadataError,
1490
- );
1491
- },
1492
- );
1493
- }
1494
-
1495
- task.status = finalStatus;
1496
- for (const waiter of task.waiters.splice(0)) waiter();
1497
- this.onChange();
1498
- this.publishTerminal(task);
1499
- this.pruneOldTasks();
1500
- }
1501
-
1502
1063
  resolveTask(idOrPrefix: string): BgTask {
1503
1064
  const id = idOrPrefix.trim();
1504
1065
  if (!id) throw new Error('Task ID is required');
@@ -1521,9 +1082,9 @@ export class BackgroundTaskRegistry {
1521
1082
  task.killKind = kind;
1522
1083
  if (reason) task.error = reason;
1523
1084
  this.requestKill(task, 'SIGTERM');
1524
- const stopWaitMs = task.managedStopWaitMs ?? this.stopWaitMs;
1085
+ const stopWaitMs = this.stopWaitMs;
1525
1086
  const stopped =
1526
- this.platform === 'win32' && task.managedCancel === undefined
1087
+ this.platform === 'win32'
1527
1088
  ? await this.waitForEndOrWindowsForceFailure(task, stopWaitMs)
1528
1089
  : await this.waitForEnd(task, stopWaitMs);
1529
1090
  const forceFailure = this.windowsKillStates.get(task)?.forceFailure;
@@ -1687,6 +1248,7 @@ export class BackgroundTaskRegistry {
1687
1248
  if (buffer.length === 0) return;
1688
1249
 
1689
1250
  const nextBytes = task.bytesWritten + buffer.length;
1251
+ appendOutputTail(task, buffer.toString('utf8'));
1690
1252
  if (nextBytes <= this.maxOutputBytes) {
1691
1253
  task.stream.write(buffer);
1692
1254
  task.bytesWritten = nextBytes;
@@ -2074,19 +1636,6 @@ export class BackgroundTaskRegistry {
2074
1636
  if (task.status !== 'running') {
2075
1637
  throw new Error(`Task ${task.id} is ${task.status}, not running`);
2076
1638
  }
2077
- if (task.managedCancel !== undefined) {
2078
- if (task.managedCancelRequested) return;
2079
- task.managedCancelRequested = true;
2080
- try {
2081
- task.managedCancel();
2082
- } catch (error) {
2083
- throw new Error(
2084
- `Could not cancel managed task ${task.id}: ${BackgroundTaskRegistry.errorMessage(error)}`,
2085
- );
2086
- }
2087
- task.killSignalSent = true;
2088
- return;
2089
- }
2090
1639
  if (!task.child) {
2091
1640
  throw new Error(`Task ${task.id} has no child process handle`);
2092
1641
  }
@@ -2268,11 +1817,11 @@ export class BackgroundTaskRegistry {
2268
1817
  const error = task.error ? `\n <error>${escapeXml(task.error)}</error>` : '';
2269
1818
  const taskName = taskDisplayName(task);
2270
1819
  const guidance =
2271
- task.fusion === undefined
1820
+ task.delegate === undefined
2272
1821
  ? 'Terminal state and output metadata are durable. Do not call bg_status to reconfirm; use bg_logs only if output is needed.'
2273
1822
  : task.status === 'completed'
2274
- ? `Fusion result is durably committed at ${task.fusion.artifactDir}. Call bg_result({taskId:${JSON.stringify(task.id)}}) once to retrieve it; do not poll.`
2275
- : `Fusion ended ${task.status}. Inspect the preserved artifacts at ${task.fusion.artifactDir}; do not poll.`;
1823
+ ? `Delegate result is durably committed. Call bg_result({taskId:${JSON.stringify(task.id)}}) once to retrieve it; do not poll.`
1824
+ : `Delegate ended ${task.status}. Inspect the preserved artifacts at ${task.delegate.artifactDir}; do not poll.`;
2276
1825
  const content = [
2277
1826
  '<background-task-notification>',
2278
1827
  ` <task-id>${task.id}</task-id>`,
@@ -653,7 +653,10 @@ export class BackgroundTasksManager implements Component {
653
653
  const runtime = taskAge(task);
654
654
  const size = formatSize(task.bytesWritten);
655
655
  const context = formatContextUsage(task);
656
- const contextText = ` ${contextColor(this.theme, task, `ctx ${context}`)}`;
656
+ const contextText =
657
+ task.isAgent || task.contextUsage !== undefined
658
+ ? ` ${contextColor(this.theme, task, `ctx ${context}`)}`
659
+ : '';
657
660
  const model = formatModel(task);
658
661
  const modelText = model ? ` ${this.theme.fg('dim', model)}` : '';
659
662
  const tokenUsage = formatTokenUsage(task);
@@ -664,7 +667,15 @@ export class BackgroundTasksManager implements Component {
664
667
  const activityText = activity ? ` ${this.theme.fg('warning', activity)}` : '';
665
668
  const exit =
666
669
  task.status !== 'running' ? this.theme.fg('dim', formatExitCodeText(task.exitCode)) : '';
667
- let row = ` ${pointer} ${unreadMark} ${name} ${this.theme.fg('dim', task.id)} ${this.theme.fg('dim', '·')} ${status}${exit} ${this.theme.fg('dim', `${runtime} ${size}`)}${contextText}${modelText}${tokenText}${toolText}${activityText}`;
670
+ const wake =
671
+ task.status === 'running' && task.triggerOnCompletion
672
+ ? ` ${this.theme.fg('accent', '⏰ wakes agent')}`
673
+ : '';
674
+ const lastLine =
675
+ task.status === 'running' && !task.isAgent && task.outputTail && task.outputTail.length > 0
676
+ ? ` ${this.theme.fg('dim', `› ${truncateChars(task.outputTail[task.outputTail.length - 1] ?? '', 40)}`)}`
677
+ : '';
678
+ let row = ` ${pointer} ${unreadMark} ${name} ${this.theme.fg('dim', task.id)} ${this.theme.fg('dim', '·')} ${status}${exit} ${this.theme.fg('dim', `${runtime} ${size}`)}${wake}${contextText}${modelText}${tokenText}${toolText}${activityText}${lastLine}`;
668
679
  if (selected) row = lightBlue(padAnsi(truncateToWidth(row, width - 4), width - 4));
669
680
  body.push(row);
670
681
  }