@pi-unipi/background-tasks 2.16.1 → 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.
- package/README.md +21 -27
- package/package.json +3 -4
- package/src/cards.ts +76 -0
- package/src/child-process.ts +1 -1
- package/src/config.ts +0 -42
- package/src/context-visible-conversation-v2.ts +1 -1
- package/src/delegate/artifacts.ts +1 -1
- package/src/delegate/launch.ts +17 -30
- package/src/delegate/result-package.ts +1 -1
- package/src/delegate/runner.ts +1 -20
- package/src/delegate/seed.ts +1 -1
- package/src/delegate-extension.ts +16 -168
- package/src/index.ts +53 -25
- package/src/json-utils.ts +56 -0
- package/src/package-assets.ts +51 -0
- package/src/registry.ts +8 -459
- package/src/task-manager.ts +13 -2
- package/src/tools.ts +4 -189
- package/src/types.ts +17 -70
- package/extensions/anthropic-attribution.ts +0 -1
- package/extensions/fusion-child.ts +0 -1
- package/src/anthropic-attribution-path.ts +0 -21
- package/src/anthropic-attribution.ts +0 -1983
- package/src/attested-pi-run.ts +0 -612
- package/src/fixtures/fusion-golden-bytes.json +0 -310
- package/src/fixtures/fusion-validate-golden-bytes.json +0 -282
- package/src/fusion/artifacts.ts +0 -967
- package/src/fusion/budget.ts +0 -1162
- package/src/fusion/child-protocol.ts +0 -305
- package/src/fusion/claude-cache.ts +0 -207
- package/src/fusion/clean-context.ts +0 -91
- package/src/fusion/config.ts +0 -449
- package/src/fusion/context.ts +0 -265
- package/src/fusion/evaluation.ts +0 -800
- package/src/fusion/orchestrator.ts +0 -1288
- package/src/fusion/output-contract.ts +0 -34
- package/src/fusion/pi-child.ts +0 -2373
- package/src/fusion/prompts.ts +0 -345
- package/src/fusion/result-package.ts +0 -959
- package/src/fusion/source-policy.ts +0 -257
- package/src/fusion/types.ts +0 -1139
- package/src/fusion/web-fetch.ts +0 -1060
- package/src/fusion/workflows.ts +0 -184
- package/src/fusion-child-extension.ts +0 -1052
- package/src/fusion-extension.ts +0 -1293
- 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 =
|
|
1085
|
+
const stopWaitMs = this.stopWaitMs;
|
|
1525
1086
|
const stopped =
|
|
1526
|
-
this.platform === 'win32'
|
|
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.
|
|
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
|
-
? `
|
|
2275
|
-
: `
|
|
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>`,
|
package/src/task-manager.ts
CHANGED
|
@@ -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 =
|
|
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
|
-
|
|
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
|
}
|