pi-background-tasks 2.5.0 → 2.6.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/BACKGROUND-TASKS-INSTRUCTIONS.md +1 -1
- package/README.md +70 -25
- package/TESTING.md +42 -2
- package/TEST_PLAN.md +7 -4
- package/dist/extensions/anthropic-attribution-child.js +4 -0
- package/dist/extensions/anthropic-attribution-child.js.map +1 -0
- package/dist/extensions/anthropic-attribution.js +107 -0
- package/dist/extensions/anthropic-attribution.js.map +1 -0
- package/dist/extensions/background-tasks.js +2 -0
- package/dist/extensions/background-tasks.js.map +1 -0
- package/dist/extensions/delegate-child.js +2 -0
- package/dist/extensions/delegate-child.js.map +1 -0
- package/dist/extensions/fusion-child.js +2 -0
- package/dist/extensions/fusion-child.js.map +1 -0
- package/dist/package.json +5 -0
- package/dist/src/core/anthropic-attribution-path.js +24 -0
- package/dist/src/core/anthropic-attribution-path.js.map +1 -0
- package/dist/src/core/anthropic-attribution.js +2247 -0
- package/dist/src/core/anthropic-attribution.js.map +1 -0
- package/dist/src/core/attested-pi-contract.js +5 -0
- package/dist/src/core/attested-pi-contract.js.map +1 -0
- package/dist/src/core/attested-pi-run.js +749 -0
- package/dist/src/core/attested-pi-run.js.map +1 -0
- package/dist/src/core/canonical-json.js +19 -0
- package/dist/src/core/canonical-json.js.map +1 -0
- package/dist/src/core/common.js +787 -0
- package/dist/src/core/common.js.map +1 -0
- package/dist/src/core/config.js +78 -0
- package/dist/src/core/config.js.map +1 -0
- package/dist/src/core/context/parent-snapshot.js +75 -0
- package/dist/src/core/context/parent-snapshot.js.map +1 -0
- package/dist/src/core/context/token-budget.js +631 -0
- package/dist/src/core/context/token-budget.js.map +1 -0
- package/dist/src/core/context/visible-conversation-v2.js +390 -0
- package/dist/src/core/context/visible-conversation-v2.js.map +1 -0
- package/dist/src/core/delegate/artifacts.js +357 -0
- package/dist/src/core/delegate/artifacts.js.map +1 -0
- package/dist/src/core/delegate/budget.js +266 -0
- package/dist/src/core/delegate/budget.js.map +1 -0
- package/dist/src/core/delegate/facade-contract.js +7 -0
- package/dist/src/core/delegate/facade-contract.js.map +1 -0
- package/dist/src/core/delegate/hook-contract-evidence.json +18 -0
- package/dist/src/core/delegate/hook-contract.js +124 -0
- package/dist/src/core/delegate/hook-contract.js.map +1 -0
- package/dist/src/core/delegate/launch.js +366 -0
- package/dist/src/core/delegate/launch.js.map +1 -0
- package/dist/src/core/delegate/result-package.js +293 -0
- package/dist/src/core/delegate/result-package.js.map +1 -0
- package/dist/src/core/delegate/runner.js +398 -0
- package/dist/src/core/delegate/runner.js.map +1 -0
- package/dist/src/core/delegate/seed.js +334 -0
- package/dist/src/core/delegate/seed.js.map +1 -0
- package/dist/src/core/delegate/types.js +103 -0
- package/dist/src/core/delegate/types.js.map +1 -0
- package/dist/src/core/durable-fs.js +372 -0
- package/dist/src/core/durable-fs.js.map +1 -0
- package/dist/src/core/extension-api.js +382 -0
- package/dist/src/core/extension-api.js.map +1 -0
- package/dist/src/core/fusion/artifacts.js +654 -0
- package/dist/src/core/fusion/artifacts.js.map +1 -0
- package/dist/src/core/fusion/budget.js +949 -0
- package/dist/src/core/fusion/budget.js.map +1 -0
- package/dist/src/core/fusion/child-protocol.js +176 -0
- package/dist/src/core/fusion/child-protocol.js.map +1 -0
- package/dist/src/core/fusion/claude-cache.js +156 -0
- package/dist/src/core/fusion/claude-cache.js.map +1 -0
- package/dist/src/core/fusion/clean-context.js +64 -0
- package/dist/src/core/fusion/clean-context.js.map +1 -0
- package/dist/src/core/fusion/config.js +349 -0
- package/dist/src/core/fusion/config.js.map +1 -0
- package/dist/src/core/fusion/context.js +184 -0
- package/dist/src/core/fusion/context.js.map +1 -0
- package/dist/src/core/fusion/evaluation.js +696 -0
- package/dist/src/core/fusion/evaluation.js.map +1 -0
- package/dist/src/core/fusion/facade-contract.js +3 -0
- package/dist/src/core/fusion/facade-contract.js.map +1 -0
- package/dist/src/core/fusion/orchestrator.js +867 -0
- package/dist/src/core/fusion/orchestrator.js.map +1 -0
- package/dist/src/core/fusion/output-contract.js +27 -0
- package/dist/src/core/fusion/output-contract.js.map +1 -0
- package/dist/src/core/fusion/pi-child.js +1741 -0
- package/dist/src/core/fusion/pi-child.js.map +1 -0
- package/dist/src/core/fusion/prompts.js +260 -0
- package/dist/src/core/fusion/prompts.js.map +1 -0
- package/dist/src/core/fusion/result-package.js +857 -0
- package/dist/src/core/fusion/result-package.js.map +1 -0
- package/dist/src/core/fusion/source-policy.js +259 -0
- package/dist/src/core/fusion/source-policy.js.map +1 -0
- package/dist/src/core/fusion/types.js +207 -0
- package/dist/src/core/fusion/types.js.map +1 -0
- package/dist/src/core/fusion/web-fetch.js +777 -0
- package/dist/src/core/fusion/web-fetch.js.map +1 -0
- package/dist/src/core/fusion/workflows.js +126 -0
- package/dist/src/core/fusion/workflows.js.map +1 -0
- package/dist/src/core/lazy-module.js +181 -0
- package/dist/src/core/lazy-module.js.map +1 -0
- package/dist/src/core/pi-launch.js +431 -0
- package/dist/src/core/pi-launch.js.map +1 -0
- package/dist/src/core/registry.js +3331 -0
- package/dist/src/core/registry.js.map +1 -0
- package/dist/src/core/reload-shell-owner.js +1396 -0
- package/dist/src/core/reload-shell-owner.js.map +1 -0
- package/dist/src/core/shell-policy.js +80 -0
- package/dist/src/core/shell-policy.js.map +1 -0
- package/dist/src/core/task-durable.js +51 -0
- package/dist/src/core/task-durable.js.map +1 -0
- package/dist/src/core/update-check.js +92 -0
- package/dist/src/core/update-check.js.map +1 -0
- package/dist/src/core/windows-taskkill.js +185 -0
- package/dist/src/core/windows-taskkill.js.map +1 -0
- package/dist/src/delegate-child-extension.js +776 -0
- package/dist/src/delegate-child-extension.js.map +1 -0
- package/dist/src/delegate-extension.js +746 -0
- package/dist/src/delegate-extension.js.map +1 -0
- package/dist/src/extension.js +953 -0
- package/dist/src/extension.js.map +1 -0
- package/dist/src/fusion-child-extension.js +760 -0
- package/dist/src/fusion-child-extension.js.map +1 -0
- package/dist/src/fusion-extension.js +1030 -0
- package/dist/src/fusion-extension.js.map +1 -0
- package/dist/src/ui/background-tasks-manager.js +689 -0
- package/dist/src/ui/background-tasks-manager.js.map +1 -0
- package/dist/src/ui/fusion-model-selector.js +277 -0
- package/dist/src/ui/fusion-model-selector.js.map +1 -0
- package/docs/INDEX.md +35 -33
- package/docs/api/eventbus-v1.md +21 -5
- package/docs/choose-a-workflow.md +2 -0
- package/docs/commands/bg-clear.md +3 -3
- package/docs/commands/bg-update.md +3 -3
- package/docs/commands/bg.md +18 -6
- package/docs/commands/claude-cache.md +4 -4
- package/docs/commands/fusion-models.md +49 -4
- package/docs/commands/fusion.md +3 -3
- package/docs/commands/jobs.md +3 -3
- package/docs/commands/kill.md +3 -3
- package/docs/commands/logs.md +3 -3
- package/docs/commands/task-manager.md +5 -5
- package/docs/concepts/completion-delivery.md +6 -2
- package/docs/getting-started.md +7 -1
- package/docs/manifest.json +228 -42
- package/docs/operations/configuration.md +58 -5
- package/docs/operations/releasing.md +4 -2
- package/docs/operations/testing.md +22 -0
- package/docs/operations/troubleshooting.md +6 -2
- package/docs/read-before-edit.md +44 -31
- package/docs/reference/runtime-contracts.md +87 -51
- package/docs/reference/shortcuts-and-dock.md +28 -14
- package/docs/subsystems/anthropic-attribution.md +23 -11
- package/docs/subsystems/attested-pi-runs.md +12 -7
- package/docs/subsystems/background-task-runtime.md +53 -11
- package/docs/subsystems/child-launch-durability-and-safety.md +28 -9
- package/docs/subsystems/delegation.md +16 -3
- package/docs/subsystems/docs-freshness-gate.md +14 -7
- package/docs/subsystems/fusion.md +36 -11
- package/docs/subsystems/host-ui-and-telemetry.md +22 -9
- package/docs/tools/bg_delegate.md +3 -1
- package/docs/tools/bg_kill.md +3 -1
- package/docs/tools/bg_logs.md +3 -1
- package/docs/tools/bg_result.md +5 -3
- package/docs/tools/bg_run.md +31 -5
- package/docs/tools/bg_run_pi_attested.md +3 -1
- package/docs/tools/bg_status.md +3 -1
- package/docs/tools/fusion_investigate.md +3 -1
- package/docs/tools/fusion_reason.md +3 -1
- package/docs/tools/fusion_research.md +3 -1
- package/docs/tools/fusion_validate.md +3 -1
- package/extensions/anthropic-attribution-child.ts +3 -0
- package/extensions/anthropic-attribution.ts +166 -1
- package/package.json +12 -10
- package/src/core/anthropic-attribution-path.ts +5 -4
- package/src/core/anthropic-attribution.ts +293 -113
- package/src/core/attested-pi-contract.ts +4 -0
- package/src/core/attested-pi-run.ts +468 -81
- package/src/core/canonical-json.ts +21 -0
- package/src/core/common.ts +450 -40
- package/src/core/config.ts +121 -0
- package/src/core/context/visible-conversation-v2.ts +3 -6
- package/src/core/delegate/artifacts.ts +13 -9
- package/src/core/delegate/budget.ts +13 -6
- package/src/core/delegate/facade-contract.ts +6 -0
- package/src/core/delegate/result-package.ts +32 -14
- package/src/core/delegate/runner.ts +129 -58
- package/src/core/delegate/seed.ts +5 -7
- package/src/core/durable-fs.ts +178 -30
- package/src/core/extension-api.ts +36 -8
- package/src/core/fusion/artifacts.ts +17 -16
- package/src/core/fusion/clean-context.ts +8 -5
- package/src/core/fusion/config.ts +2 -1
- package/src/core/fusion/context.ts +2 -6
- package/src/core/fusion/facade-contract.ts +2 -0
- package/src/core/fusion/orchestrator.ts +3 -3
- package/src/core/fusion/prompts.ts +1 -1
- package/src/core/fusion/result-package.ts +259 -72
- package/src/core/fusion/source-policy.ts +43 -17
- package/src/core/lazy-module.ts +215 -0
- package/src/core/pi-launch.ts +390 -65
- package/src/core/registry.ts +2010 -381
- package/src/core/reload-shell-owner.ts +1662 -0
- package/src/core/shell-policy.ts +134 -0
- package/src/core/task-durable.ts +67 -0
- package/src/delegate-extension.ts +404 -144
- package/src/extension.ts +387 -114
- package/src/fusion-extension.ts +202 -96
package/src/core/durable-fs.ts
CHANGED
|
@@ -45,9 +45,13 @@ export interface DurableFileOperations {
|
|
|
45
45
|
temporaryPath(target: string): string;
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
export interface DurableWriteOptions {
|
|
49
|
+
readonly signal?: AbortSignal | undefined;
|
|
50
|
+
}
|
|
51
|
+
|
|
48
52
|
export interface DurableFileWriter {
|
|
49
|
-
write(path: string, data: DurableData): Promise<void>;
|
|
50
|
-
replace(path: string, data: DurableData): Promise<void>;
|
|
53
|
+
write(path: string, data: DurableData, options?: DurableWriteOptions): Promise<void>;
|
|
54
|
+
replace(path: string, data: DurableData, options?: DurableWriteOptions): Promise<void>;
|
|
51
55
|
}
|
|
52
56
|
|
|
53
57
|
interface DurableErrorInput {
|
|
@@ -63,6 +67,48 @@ interface DurableErrorInput {
|
|
|
63
67
|
interface WritableSequenceResult {
|
|
64
68
|
primaryFailure: DurableFailure | undefined;
|
|
65
69
|
cleanupFailures: DurableFailure[];
|
|
70
|
+
cancelled: boolean;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
interface RenameSequenceResult {
|
|
74
|
+
error: unknown | undefined;
|
|
75
|
+
cancelled: boolean;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export class DurableFileCancellationError extends Error {
|
|
79
|
+
readonly code = 'durable_file_cancelled';
|
|
80
|
+
readonly path: string;
|
|
81
|
+
readonly reason: unknown;
|
|
82
|
+
readonly cleanupFailures: readonly DurableFailure[];
|
|
83
|
+
readonly targetPath: string | undefined;
|
|
84
|
+
readonly temporaryPath: string | undefined;
|
|
85
|
+
readonly renameCompleted: boolean;
|
|
86
|
+
|
|
87
|
+
constructor(input: {
|
|
88
|
+
path: string;
|
|
89
|
+
reason: unknown;
|
|
90
|
+
cleanupFailures: readonly DurableFailure[];
|
|
91
|
+
targetPath: string | undefined;
|
|
92
|
+
temporaryPath: string | undefined;
|
|
93
|
+
renameCompleted: boolean;
|
|
94
|
+
}) {
|
|
95
|
+
const reasonText = describeCause(input.reason);
|
|
96
|
+
const cleanupText =
|
|
97
|
+
input.cleanupFailures.length === 0
|
|
98
|
+
? ''
|
|
99
|
+
: ` Cleanup failures: ${input.cleanupFailures.map(formatFailureForMessage).join('; ')}`;
|
|
100
|
+
const commitText = input.renameCompleted
|
|
101
|
+
? ` Replacement may already be visible at ${input.targetPath ?? input.path}.`
|
|
102
|
+
: '';
|
|
103
|
+
super(`Durable file operation cancelled for ${input.path}: ${reasonText}.${commitText}${cleanupText}`);
|
|
104
|
+
this.name = 'DurableFileCancellationError';
|
|
105
|
+
this.path = input.path;
|
|
106
|
+
this.reason = input.reason;
|
|
107
|
+
this.cleanupFailures = [...input.cleanupFailures];
|
|
108
|
+
this.targetPath = input.targetPath;
|
|
109
|
+
this.temporaryPath = input.temporaryPath;
|
|
110
|
+
this.renameCompleted = input.renameCompleted;
|
|
111
|
+
}
|
|
66
112
|
}
|
|
67
113
|
|
|
68
114
|
export class DurableFileError extends Error {
|
|
@@ -149,20 +195,26 @@ async function writeSyncClose(
|
|
|
149
195
|
handle: DurableWritableHandle,
|
|
150
196
|
path: string,
|
|
151
197
|
data: DurableData,
|
|
198
|
+
signal?: AbortSignal,
|
|
152
199
|
): Promise<WritableSequenceResult> {
|
|
153
200
|
const cleanupFailures: DurableFailure[] = [];
|
|
154
201
|
let primaryFailure: DurableFailure | undefined;
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
202
|
+
let cancelled = signal?.aborted === true;
|
|
203
|
+
if (!cancelled) {
|
|
204
|
+
try {
|
|
205
|
+
await handle.writeFile(data);
|
|
206
|
+
} catch (error) {
|
|
207
|
+
primaryFailure = failure('write_file', path, error);
|
|
208
|
+
}
|
|
209
|
+
cancelled = signal?.aborted === true;
|
|
159
210
|
}
|
|
160
|
-
if (primaryFailure === undefined) {
|
|
211
|
+
if (primaryFailure === undefined && !cancelled) {
|
|
161
212
|
try {
|
|
162
213
|
await handle.sync();
|
|
163
214
|
} catch (error) {
|
|
164
215
|
primaryFailure = failure('sync_file', path, error);
|
|
165
216
|
}
|
|
217
|
+
cancelled = signal?.aborted === true;
|
|
166
218
|
}
|
|
167
219
|
try {
|
|
168
220
|
await handle.close();
|
|
@@ -171,7 +223,8 @@ async function writeSyncClose(
|
|
|
171
223
|
if (primaryFailure === undefined) primaryFailure = closeFailure;
|
|
172
224
|
else cleanupFailures.push(closeFailure);
|
|
173
225
|
}
|
|
174
|
-
|
|
226
|
+
cancelled ||= signal?.aborted === true;
|
|
227
|
+
return { primaryFailure, cleanupFailures, cancelled };
|
|
175
228
|
}
|
|
176
229
|
|
|
177
230
|
async function syncCloseDirectory(
|
|
@@ -192,7 +245,31 @@ async function syncCloseDirectory(
|
|
|
192
245
|
if (primaryFailure === undefined) primaryFailure = closeFailure;
|
|
193
246
|
else cleanupFailures.push(closeFailure);
|
|
194
247
|
}
|
|
195
|
-
return { primaryFailure, cleanupFailures };
|
|
248
|
+
return { primaryFailure, cleanupFailures, cancelled: false };
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function cancellationReason(signal: AbortSignal): unknown {
|
|
252
|
+
return signal.reason ?? new Error('operation aborted');
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function throwCancellation(
|
|
256
|
+
signal: AbortSignal,
|
|
257
|
+
path: string,
|
|
258
|
+
cleanupFailures: readonly DurableFailure[],
|
|
259
|
+
targetPath: string | undefined,
|
|
260
|
+
temporaryPath: string | undefined,
|
|
261
|
+
renameCompleted: boolean,
|
|
262
|
+
): never {
|
|
263
|
+
const reason = cancellationReason(signal);
|
|
264
|
+
if (cleanupFailures.length === 0 && !renameCompleted && reason instanceof Error) throw reason;
|
|
265
|
+
throw new DurableFileCancellationError({
|
|
266
|
+
path,
|
|
267
|
+
reason,
|
|
268
|
+
cleanupFailures,
|
|
269
|
+
targetPath,
|
|
270
|
+
temporaryPath,
|
|
271
|
+
renameCompleted,
|
|
272
|
+
});
|
|
196
273
|
}
|
|
197
274
|
|
|
198
275
|
function throwDurable(
|
|
@@ -225,21 +302,34 @@ async function removeTemporary(
|
|
|
225
302
|
}
|
|
226
303
|
}
|
|
227
304
|
|
|
305
|
+
function isAborted(signal: AbortSignal | undefined): boolean {
|
|
306
|
+
return signal?.aborted === true;
|
|
307
|
+
}
|
|
308
|
+
|
|
228
309
|
async function writeWithOperations(
|
|
229
310
|
operations: DurableFileOperations,
|
|
230
311
|
path: string,
|
|
231
312
|
data: DurableData,
|
|
313
|
+
options: DurableWriteOptions,
|
|
232
314
|
): Promise<void> {
|
|
315
|
+
const signal = options.signal;
|
|
316
|
+
if (signal !== undefined && isAborted(signal))
|
|
317
|
+
throwCancellation(signal, path, [], path, undefined, false);
|
|
233
318
|
let handle: DurableWritableHandle;
|
|
234
319
|
try {
|
|
235
320
|
handle = await operations.openWritable(path, 'w');
|
|
236
321
|
} catch (error) {
|
|
322
|
+
if (signal !== undefined && isAborted(signal))
|
|
323
|
+
throwCancellation(signal, path, [], path, undefined, false);
|
|
237
324
|
throwDurable(failure('open_file', path, error), [], path, undefined, false);
|
|
238
325
|
}
|
|
239
|
-
const result = await writeSyncClose(handle, path, data);
|
|
326
|
+
const result = await writeSyncClose(handle, path, data, signal);
|
|
240
327
|
if (result.primaryFailure !== undefined) {
|
|
241
328
|
throwDurable(result.primaryFailure, result.cleanupFailures, path, undefined, false);
|
|
242
329
|
}
|
|
330
|
+
if (result.cancelled && signal !== undefined) {
|
|
331
|
+
throwCancellation(signal, path, result.cleanupFailures, path, undefined, false);
|
|
332
|
+
}
|
|
243
333
|
}
|
|
244
334
|
|
|
245
335
|
async function syncDirectoryAfterRename(
|
|
@@ -265,25 +355,52 @@ async function replaceWithOperations(
|
|
|
265
355
|
operations: DurableFileOperations,
|
|
266
356
|
path: string,
|
|
267
357
|
data: DurableData,
|
|
358
|
+
options: DurableWriteOptions,
|
|
268
359
|
): Promise<void> {
|
|
360
|
+
const signal = options.signal;
|
|
361
|
+
if (signal !== undefined && isAborted(signal))
|
|
362
|
+
throwCancellation(signal, path, [], path, undefined, false);
|
|
269
363
|
const temporaryPath = operations.temporaryPath(path);
|
|
270
364
|
let handle: DurableWritableHandle;
|
|
271
365
|
try {
|
|
272
366
|
handle = await operations.openWritable(temporaryPath, 'wx', 0o600);
|
|
273
367
|
} catch (error) {
|
|
368
|
+
if (signal !== undefined && isAborted(signal))
|
|
369
|
+
throwCancellation(signal, temporaryPath, [], path, temporaryPath, false);
|
|
274
370
|
throwDurable(failure('open_file', temporaryPath, error), [], path, temporaryPath, false);
|
|
275
371
|
}
|
|
276
|
-
const writeResult = await writeSyncClose(handle, temporaryPath, data);
|
|
372
|
+
const writeResult = await writeSyncClose(handle, temporaryPath, data, signal);
|
|
277
373
|
if (writeResult.primaryFailure !== undefined) {
|
|
278
374
|
await removeTemporary(operations, temporaryPath, writeResult.cleanupFailures);
|
|
279
375
|
throwDurable(writeResult.primaryFailure, writeResult.cleanupFailures, path, temporaryPath, false);
|
|
280
376
|
}
|
|
281
|
-
|
|
282
|
-
|
|
377
|
+
if (writeResult.cancelled && signal !== undefined) {
|
|
378
|
+
await removeTemporary(operations, temporaryPath, writeResult.cleanupFailures);
|
|
379
|
+
throwCancellation(
|
|
380
|
+
signal,
|
|
381
|
+
temporaryPath,
|
|
382
|
+
writeResult.cleanupFailures,
|
|
383
|
+
path,
|
|
384
|
+
temporaryPath,
|
|
385
|
+
false,
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
const renameResult = await renameWithWindowsContention(
|
|
389
|
+
operations,
|
|
390
|
+
temporaryPath,
|
|
391
|
+
path,
|
|
392
|
+
signal,
|
|
393
|
+
);
|
|
394
|
+
if (renameResult.cancelled && signal !== undefined) {
|
|
395
|
+
const cleanupFailures: DurableFailure[] = [];
|
|
396
|
+
await removeTemporary(operations, temporaryPath, cleanupFailures);
|
|
397
|
+
throwCancellation(signal, temporaryPath, cleanupFailures, path, temporaryPath, false);
|
|
398
|
+
}
|
|
399
|
+
if (renameResult.error !== undefined) {
|
|
283
400
|
const cleanupFailures: DurableFailure[] = [];
|
|
284
401
|
await removeTemporary(operations, temporaryPath, cleanupFailures);
|
|
285
402
|
throwDurable(
|
|
286
|
-
failure('rename_file', path,
|
|
403
|
+
failure('rename_file', path, renameResult.error),
|
|
287
404
|
cleanupFailures,
|
|
288
405
|
path,
|
|
289
406
|
temporaryPath,
|
|
@@ -291,6 +408,9 @@ async function replaceWithOperations(
|
|
|
291
408
|
);
|
|
292
409
|
}
|
|
293
410
|
await syncDirectoryAfterRename(operations, path, temporaryPath);
|
|
411
|
+
if (signal !== undefined && isAborted(signal)) {
|
|
412
|
+
throwCancellation(signal, path, [], path, temporaryPath, true);
|
|
413
|
+
}
|
|
294
414
|
}
|
|
295
415
|
|
|
296
416
|
/**
|
|
@@ -310,9 +430,18 @@ const WINDOWS_RENAME_CONTENTION_CODES: ReadonlySet<string> = new Set([
|
|
|
310
430
|
const WINDOWS_RENAME_ATTEMPTS = 10;
|
|
311
431
|
const WINDOWS_RENAME_RETRY_DELAY_MS = 20;
|
|
312
432
|
|
|
313
|
-
function delay(ms: number): Promise<void> {
|
|
433
|
+
function delay(ms: number, signal?: AbortSignal): Promise<void> {
|
|
434
|
+
if (isAborted(signal)) return Promise.resolve();
|
|
314
435
|
return new Promise((resolve) => {
|
|
315
|
-
setTimeout(
|
|
436
|
+
const timer = setTimeout(() => {
|
|
437
|
+
signal?.removeEventListener('abort', aborted);
|
|
438
|
+
resolve();
|
|
439
|
+
}, ms);
|
|
440
|
+
const aborted = () => {
|
|
441
|
+
clearTimeout(timer);
|
|
442
|
+
resolve();
|
|
443
|
+
};
|
|
444
|
+
signal?.addEventListener('abort', aborted, { once: true });
|
|
316
445
|
});
|
|
317
446
|
}
|
|
318
447
|
|
|
@@ -324,31 +453,34 @@ function delay(ms: number): Promise<void> {
|
|
|
324
453
|
* final failure is still returned and raised loudly, no alternative write path
|
|
325
454
|
* is taken, and no other platform or error code is retried.
|
|
326
455
|
*
|
|
327
|
-
* Returns
|
|
456
|
+
* Returns an explicit success/error/cancellation sequence result.
|
|
328
457
|
*/
|
|
329
458
|
async function renameWithWindowsContention(
|
|
330
459
|
operations: DurableFileOperations,
|
|
331
460
|
temporaryPath: string,
|
|
332
461
|
targetPath: string,
|
|
333
|
-
|
|
462
|
+
signal?: AbortSignal,
|
|
463
|
+
): Promise<RenameSequenceResult> {
|
|
334
464
|
const attempts = operations.platform === 'win32' ? WINDOWS_RENAME_ATTEMPTS : 1;
|
|
335
465
|
let lastError: unknown;
|
|
336
466
|
for (let attempt = 1; attempt <= attempts; attempt++) {
|
|
467
|
+
if (isAborted(signal)) return { error: undefined, cancelled: true };
|
|
337
468
|
try {
|
|
338
469
|
await operations.rename(temporaryPath, targetPath);
|
|
339
|
-
return undefined;
|
|
470
|
+
return { error: undefined, cancelled: false };
|
|
340
471
|
} catch (error) {
|
|
341
472
|
lastError = error;
|
|
473
|
+
if (isAborted(signal)) return { error: undefined, cancelled: true };
|
|
342
474
|
const code = nativeCodeForCause(error);
|
|
343
475
|
const retryable =
|
|
344
476
|
operations.platform === 'win32' &&
|
|
345
477
|
code !== undefined &&
|
|
346
478
|
WINDOWS_RENAME_CONTENTION_CODES.has(code);
|
|
347
|
-
if (!retryable || attempt === attempts) return error;
|
|
348
|
-
await delay(WINDOWS_RENAME_RETRY_DELAY_MS * attempt);
|
|
479
|
+
if (!retryable || attempt === attempts) return { error, cancelled: false };
|
|
480
|
+
await delay(WINDOWS_RENAME_RETRY_DELAY_MS * attempt, signal);
|
|
349
481
|
}
|
|
350
482
|
}
|
|
351
|
-
return lastError;
|
|
483
|
+
return { error: lastError, cancelled: false };
|
|
352
484
|
}
|
|
353
485
|
|
|
354
486
|
function temporaryPathForTarget(target: string): string {
|
|
@@ -382,19 +514,35 @@ const defaultWriter = createDurableFileWriter(nodeOperations);
|
|
|
382
514
|
|
|
383
515
|
export function createDurableFileWriter(operations: DurableFileOperations): DurableFileWriter {
|
|
384
516
|
return {
|
|
385
|
-
async write(
|
|
386
|
-
|
|
517
|
+
async write(
|
|
518
|
+
path: string,
|
|
519
|
+
data: DurableData,
|
|
520
|
+
options: DurableWriteOptions = {},
|
|
521
|
+
): Promise<void> {
|
|
522
|
+
await writeWithOperations(operations, path, data, options);
|
|
387
523
|
},
|
|
388
|
-
async replace(
|
|
389
|
-
|
|
524
|
+
async replace(
|
|
525
|
+
path: string,
|
|
526
|
+
data: DurableData,
|
|
527
|
+
options: DurableWriteOptions = {},
|
|
528
|
+
): Promise<void> {
|
|
529
|
+
await replaceWithOperations(operations, path, data, options);
|
|
390
530
|
},
|
|
391
531
|
};
|
|
392
532
|
}
|
|
393
533
|
|
|
394
|
-
export async function writeFileDurable(
|
|
395
|
-
|
|
534
|
+
export async function writeFileDurable(
|
|
535
|
+
path: string,
|
|
536
|
+
data: DurableData,
|
|
537
|
+
options: DurableWriteOptions = {},
|
|
538
|
+
): Promise<void> {
|
|
539
|
+
await defaultWriter.write(path, data, options);
|
|
396
540
|
}
|
|
397
541
|
|
|
398
|
-
export async function replaceFileDurable(
|
|
399
|
-
|
|
542
|
+
export async function replaceFileDurable(
|
|
543
|
+
path: string,
|
|
544
|
+
data: DurableData,
|
|
545
|
+
options: DurableWriteOptions = {},
|
|
546
|
+
): Promise<void> {
|
|
547
|
+
await defaultWriter.replace(path, data, options);
|
|
400
548
|
}
|
|
@@ -15,6 +15,18 @@ export const BG_TERMINAL_CHANNEL = 'pi-background-tasks:terminal:v1';
|
|
|
15
15
|
export const BG_REQUEST_SCHEMA = 'pi-background-tasks.extension-request.v1';
|
|
16
16
|
export const BG_RESPONSE_SCHEMA = 'pi-background-tasks.extension-response.v1';
|
|
17
17
|
export const BG_TERMINAL_SCHEMA = 'pi-background-tasks.extension-terminal.v1';
|
|
18
|
+
export const BG_EXTENSION_SERVICE_CLOSED_CODE = 'pi_background_tasks_eventbus_closed';
|
|
19
|
+
|
|
20
|
+
export type BackgroundTaskExtensionServiceState = 'open' | 'closed';
|
|
21
|
+
|
|
22
|
+
export class BackgroundTaskExtensionServiceClosedError extends Error {
|
|
23
|
+
readonly code = BG_EXTENSION_SERVICE_CLOSED_CODE;
|
|
24
|
+
|
|
25
|
+
constructor() {
|
|
26
|
+
super('pi-background-tasks EventBus service is closed');
|
|
27
|
+
this.name = 'BackgroundTaskExtensionServiceClosedError';
|
|
28
|
+
}
|
|
29
|
+
}
|
|
18
30
|
|
|
19
31
|
const MAX_ERROR_CHARS = 240;
|
|
20
32
|
const MAX_REQUEST_ID_CHARS = 200;
|
|
@@ -109,6 +121,7 @@ export interface BackgroundTaskExtensionTerminal {
|
|
|
109
121
|
}
|
|
110
122
|
|
|
111
123
|
export interface BackgroundTaskExtensionService {
|
|
124
|
+
readonly state: BackgroundTaskExtensionServiceState;
|
|
112
125
|
publishTerminal(task: BgTaskSnapshot): void;
|
|
113
126
|
close(): void;
|
|
114
127
|
}
|
|
@@ -406,7 +419,7 @@ class InstalledBackgroundTaskExtensionService implements BackgroundTaskExtension
|
|
|
406
419
|
private readonly logger: Pick<Console, 'error'>;
|
|
407
420
|
private readonly seenRequestIds = new Set<string>();
|
|
408
421
|
private readonly unsubscribe: () => void;
|
|
409
|
-
private
|
|
422
|
+
private serviceState: BackgroundTaskExtensionServiceState = 'open';
|
|
410
423
|
|
|
411
424
|
constructor(options: BackgroundTaskExtensionServiceOptions) {
|
|
412
425
|
this.events = options.events;
|
|
@@ -419,8 +432,16 @@ class InstalledBackgroundTaskExtensionService implements BackgroundTaskExtension
|
|
|
419
432
|
});
|
|
420
433
|
}
|
|
421
434
|
|
|
435
|
+
get state(): BackgroundTaskExtensionServiceState {
|
|
436
|
+
return this.serviceState;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
private isClosed(): boolean {
|
|
440
|
+
return this.serviceState === 'closed';
|
|
441
|
+
}
|
|
442
|
+
|
|
422
443
|
publishTerminal(task: BgTaskSnapshot): void {
|
|
423
|
-
if (this.closed) throw new
|
|
444
|
+
if (this.serviceState === 'closed') throw new BackgroundTaskExtensionServiceClosedError();
|
|
424
445
|
const terminal: BackgroundTaskExtensionTerminal = {
|
|
425
446
|
schema_version: BG_TERMINAL_SCHEMA,
|
|
426
447
|
task,
|
|
@@ -429,8 +450,9 @@ class InstalledBackgroundTaskExtensionService implements BackgroundTaskExtension
|
|
|
429
450
|
}
|
|
430
451
|
|
|
431
452
|
close(): void {
|
|
432
|
-
if (this.closed) return;
|
|
433
|
-
this.
|
|
453
|
+
if (this.serviceState === 'closed') return;
|
|
454
|
+
this.serviceState = 'closed';
|
|
455
|
+
this.registry.closeTerminalPublication('publisher_closed');
|
|
434
456
|
this.unsubscribe();
|
|
435
457
|
}
|
|
436
458
|
|
|
@@ -459,7 +481,7 @@ class InstalledBackgroundTaskExtensionService implements BackgroundTaskExtension
|
|
|
459
481
|
? createTerminalPublicationGate()
|
|
460
482
|
: undefined;
|
|
461
483
|
try {
|
|
462
|
-
if (this.closed) throw new
|
|
484
|
+
if (this.serviceState === 'closed') throw new BackgroundTaskExtensionServiceClosedError();
|
|
463
485
|
if (this.isShuttingDown() || this.registry.isShuttingDown()) {
|
|
464
486
|
throw new Error('pi-background-tasks EventBus service is shutting down');
|
|
465
487
|
}
|
|
@@ -467,10 +489,16 @@ class InstalledBackgroundTaskExtensionService implements BackgroundTaskExtension
|
|
|
467
489
|
if (ctx === undefined) {
|
|
468
490
|
throw new Error('pi-background-tasks EventBus service is unavailable before session_start');
|
|
469
491
|
}
|
|
470
|
-
this.
|
|
471
|
-
|
|
472
|
-
)
|
|
492
|
+
const result = await this.execute(ctx, request, terminalGate?.promise);
|
|
493
|
+
if (this.isClosed()) return;
|
|
494
|
+
if (this.isShuttingDown() || this.registry.isShuttingDown()) {
|
|
495
|
+
throw new Error('pi-background-tasks EventBus service is shutting down');
|
|
496
|
+
}
|
|
497
|
+
this.emitResponse(successResponse(request, result));
|
|
473
498
|
} catch (error) {
|
|
499
|
+
// A request accepted before close may report failure, but it must never
|
|
500
|
+
// report post-close success. Requests first emitted after close remain
|
|
501
|
+
// unhandled because the listener has already been removed.
|
|
474
502
|
this.emitResponse(errorResponse(request.request_id, request.operation, error));
|
|
475
503
|
} finally {
|
|
476
504
|
await terminalGate?.releaseAfterResponse();
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createHash, randomBytes } from 'node:crypto';
|
|
2
2
|
import { chmod, mkdir } from 'node:fs/promises';
|
|
3
3
|
import { basename, isAbsolute, join, relative, sep } from 'node:path';
|
|
4
|
-
import { canonicalJson, sha256Buffer } from '../
|
|
4
|
+
import { canonicalJson, sha256Buffer } from '../canonical-json.js';
|
|
5
5
|
import { sanitizePathSegment } from '../common.js';
|
|
6
6
|
import { replaceFileDurable } from '../durable-fs.js';
|
|
7
7
|
import {
|
|
@@ -443,7 +443,9 @@ function failureArtifactClassification(
|
|
|
443
443
|
return 'evidence_only';
|
|
444
444
|
}
|
|
445
445
|
|
|
446
|
-
function failureAttemptMetadata(
|
|
446
|
+
function failureAttemptMetadata(
|
|
447
|
+
manifest: FusionArtifactManifest,
|
|
448
|
+
): readonly FusionFailureAttemptMetadata[] {
|
|
447
449
|
return manifest.attempts
|
|
448
450
|
.map((attempt) => ({
|
|
449
451
|
stage: attempt.stage,
|
|
@@ -452,10 +454,11 @@ function failureAttemptMetadata(manifest: FusionArtifactManifest): readonly Fusi
|
|
|
452
454
|
status: attempt.status,
|
|
453
455
|
child_created: attempt.child_created,
|
|
454
456
|
}))
|
|
455
|
-
.sort(
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
457
|
+
.sort(
|
|
458
|
+
(left, right) =>
|
|
459
|
+
compareArtifactText(left.stage, right.stage) ||
|
|
460
|
+
(left.slot ?? 0) - (right.slot ?? 0) ||
|
|
461
|
+
left.attempt - right.attempt,
|
|
459
462
|
);
|
|
460
463
|
}
|
|
461
464
|
|
|
@@ -470,7 +473,9 @@ export function buildFusionFailureSummary(input: {
|
|
|
470
473
|
(input.terminalState !== 'failed' && input.terminalState !== 'cancelled') ||
|
|
471
474
|
input.manifest.state !== input.terminalState
|
|
472
475
|
) {
|
|
473
|
-
throw errorForArtifact(
|
|
476
|
+
throw errorForArtifact(
|
|
477
|
+
'failure summary requires a matching failed/cancelled terminal manifest',
|
|
478
|
+
);
|
|
474
479
|
}
|
|
475
480
|
if (input.manifest.error !== input.terminalError.message) {
|
|
476
481
|
throw errorForArtifact('failure summary terminal error does not match the durable manifest');
|
|
@@ -750,10 +755,7 @@ export class FusionArtifactStore {
|
|
|
750
755
|
* stage-output bodies.
|
|
751
756
|
*/
|
|
752
757
|
async writeFailureSummary(summary: FusionFailureSummaryV1): Promise<FusionArtifactRef> {
|
|
753
|
-
if (
|
|
754
|
-
this.manifest.state !== 'failed' &&
|
|
755
|
-
this.manifest.state !== 'cancelled'
|
|
756
|
-
) {
|
|
758
|
+
if (this.manifest.state !== 'failed' && this.manifest.state !== 'cancelled') {
|
|
757
759
|
throw errorForArtifact('failure summary requires a failed/cancelled terminal manifest');
|
|
758
760
|
}
|
|
759
761
|
if (summary.terminal_state !== this.manifest.state) {
|
|
@@ -766,10 +768,7 @@ export class FusionArtifactStore {
|
|
|
766
768
|
) {
|
|
767
769
|
throw errorForArtifact('failure summary identity does not match the terminal manifest');
|
|
768
770
|
}
|
|
769
|
-
if (
|
|
770
|
-
summary.answer?.present !== false ||
|
|
771
|
-
summary.answer.reason !== 'run_did_not_commit'
|
|
772
|
-
) {
|
|
771
|
+
if (summary.answer?.present !== false || summary.answer.reason !== 'run_did_not_commit') {
|
|
773
772
|
throw errorForArtifact('failure summary must assert that no answer was committed');
|
|
774
773
|
}
|
|
775
774
|
if (this.manifest.error === undefined || this.manifest.artifacts['error.json'] === undefined) {
|
|
@@ -781,7 +780,9 @@ export class FusionArtifactStore {
|
|
|
781
780
|
) {
|
|
782
781
|
throw errorForArtifact('failure summary terminal error metadata does not match the manifest');
|
|
783
782
|
}
|
|
784
|
-
if (
|
|
783
|
+
if (
|
|
784
|
+
canonicalJson(summary.progress) !== canonicalJson(buildFusionRunProgress(this.snapshot()))
|
|
785
|
+
) {
|
|
785
786
|
throw errorForArtifact('failure summary progress does not match the terminal manifest');
|
|
786
787
|
}
|
|
787
788
|
if (canonicalJson(summary.usage_so_far) !== canonicalJson(this.manifest.usage)) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
|
-
import { canonicalJson } from '../
|
|
2
|
+
import { canonicalJson } from '../canonical-json.js';
|
|
3
3
|
import { normalizeFusionDeclaredSources, type DeclaredFusionSourceInput } from './source-policy.js';
|
|
4
4
|
import {
|
|
5
5
|
FUSION_INPUT_SCHEMA_VERSION,
|
|
@@ -47,10 +47,13 @@ export function buildFusionCleanTaskCanonicalInput(
|
|
|
47
47
|
});
|
|
48
48
|
}
|
|
49
49
|
if (!['investigate', 'research', 'validate'].includes(options.workflow)) {
|
|
50
|
-
throw new FusionError(
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
50
|
+
throw new FusionError(
|
|
51
|
+
'clean-task fusion input is available only to investigate, research, and validate workflows',
|
|
52
|
+
{
|
|
53
|
+
code: 'context_capture_failed',
|
|
54
|
+
childCreated: false,
|
|
55
|
+
},
|
|
56
|
+
);
|
|
54
57
|
}
|
|
55
58
|
const declaredSources = normalizeFusionDeclaredSources(options.declaredSources ?? []);
|
|
56
59
|
if (options.workflow === 'research' && declaredSources.length === 0) {
|
|
@@ -6,6 +6,7 @@ import type { Api, Model } from '@earendil-works/pi-ai';
|
|
|
6
6
|
import { isJsonObject, parseJsonText, type JsonObject } from '../common.js';
|
|
7
7
|
import { replaceFileDurable } from '../durable-fs.js';
|
|
8
8
|
import { CLAUDE_CODE_200K_SUBSCRIPTION_CONTEXT_WINDOW } from '../anthropic-attribution.js';
|
|
9
|
+
import { CURRENT_MODEL_SELECTION } from './facade-contract.js';
|
|
9
10
|
import {
|
|
10
11
|
FUSION_MODEL_CONFIG_SCHEMA_VERSION,
|
|
11
12
|
FusionError,
|
|
@@ -19,7 +20,7 @@ import {
|
|
|
19
20
|
} from './types.js';
|
|
20
21
|
|
|
21
22
|
export const FUSION_MODEL_CONFIG_FILE = 'fusion-models.json';
|
|
22
|
-
export
|
|
23
|
+
export { CURRENT_MODEL_SELECTION } from './facade-contract.js';
|
|
23
24
|
|
|
24
25
|
export interface FusionModelRegistry {
|
|
25
26
|
getAll(): Model<Api>[];
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
|
-
import { canonicalJson } from '../
|
|
2
|
+
import { canonicalJson } from '../canonical-json.js';
|
|
3
3
|
import {
|
|
4
4
|
projectVisibleConversationV2,
|
|
5
5
|
type OmittedRunCounts,
|
|
@@ -100,11 +100,7 @@ function policyDescriptor(source: FusionSource): FusionContextPolicyDescriptor {
|
|
|
100
100
|
}
|
|
101
101
|
|
|
102
102
|
function compactOmissionCounts(counts: OmittedRunCounts): FusionProjectionOmissionCounts {
|
|
103
|
-
return [
|
|
104
|
-
counts.assistant_thinking ?? 0,
|
|
105
|
-
counts.tool_calls ?? 0,
|
|
106
|
-
counts.tool_result_texts ?? 0,
|
|
107
|
-
];
|
|
103
|
+
return [counts.assistant_thinking ?? 0, counts.tool_calls ?? 0, counts.tool_result_texts ?? 0];
|
|
108
104
|
}
|
|
109
105
|
|
|
110
106
|
function expandOmissionCounts(counts: FusionProjectionOmissionCounts): OmittedRunCounts {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createHash, randomBytes as nodeRandomBytes } from 'node:crypto';
|
|
2
|
-
import { canonicalJson } from '../
|
|
2
|
+
import { canonicalJson } from '../canonical-json.js';
|
|
3
3
|
import { parseJsonText } from '../common.js';
|
|
4
4
|
import { FUSION_BUDGET_POLICY, FusionBudget } from './budget.js';
|
|
5
5
|
import { assertChildOutputWithinContract } from './output-contract.js';
|
|
@@ -841,6 +841,7 @@ export class FusionOrchestrator {
|
|
|
841
841
|
const mergePrompt = buildMergePrompt(mergeInput);
|
|
842
842
|
budget.assertStagePrompt('merge', profile.mergerSystemPrompt, mergePrompt);
|
|
843
843
|
input.onProgress?.({ type: 'merge_started' });
|
|
844
|
+
// Stage policy, not caller input: evaluator and merger are always reasoning-only.
|
|
844
845
|
const merged = await this.runChildWithRetry(
|
|
845
846
|
input,
|
|
846
847
|
store,
|
|
@@ -850,7 +851,6 @@ export class FusionOrchestrator {
|
|
|
850
851
|
profile.mergerSystemPrompt,
|
|
851
852
|
mergePrompt,
|
|
852
853
|
input.signal ?? new AbortController().signal,
|
|
853
|
-
// Stage policy, not caller input: evaluator and merger are always reasoning-only.
|
|
854
854
|
FUSION_NO_TOOLS_CAPABILITY,
|
|
855
855
|
undefined,
|
|
856
856
|
'md',
|
|
@@ -1144,6 +1144,7 @@ export class FusionOrchestrator {
|
|
|
1144
1144
|
const systemPrompt = repair
|
|
1145
1145
|
? profile.evaluationRepairSystemPrompt
|
|
1146
1146
|
: profile.evaluatorSystemPrompt;
|
|
1147
|
+
// Stage policy, not caller input: evaluator and merger are always reasoning-only.
|
|
1147
1148
|
const result = await this.runChildWithRetry(
|
|
1148
1149
|
input,
|
|
1149
1150
|
store,
|
|
@@ -1153,7 +1154,6 @@ export class FusionOrchestrator {
|
|
|
1153
1154
|
systemPrompt,
|
|
1154
1155
|
prompt,
|
|
1155
1156
|
input.signal ?? new AbortController().signal,
|
|
1156
|
-
// Stage policy, not caller input: evaluator and merger are always reasoning-only.
|
|
1157
1157
|
FUSION_NO_TOOLS_CAPABILITY,
|
|
1158
1158
|
undefined,
|
|
1159
1159
|
'txt',
|