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.
Files changed (203) hide show
  1. package/BACKGROUND-TASKS-INSTRUCTIONS.md +1 -1
  2. package/README.md +70 -25
  3. package/TESTING.md +42 -2
  4. package/TEST_PLAN.md +7 -4
  5. package/dist/extensions/anthropic-attribution-child.js +4 -0
  6. package/dist/extensions/anthropic-attribution-child.js.map +1 -0
  7. package/dist/extensions/anthropic-attribution.js +107 -0
  8. package/dist/extensions/anthropic-attribution.js.map +1 -0
  9. package/dist/extensions/background-tasks.js +2 -0
  10. package/dist/extensions/background-tasks.js.map +1 -0
  11. package/dist/extensions/delegate-child.js +2 -0
  12. package/dist/extensions/delegate-child.js.map +1 -0
  13. package/dist/extensions/fusion-child.js +2 -0
  14. package/dist/extensions/fusion-child.js.map +1 -0
  15. package/dist/package.json +5 -0
  16. package/dist/src/core/anthropic-attribution-path.js +24 -0
  17. package/dist/src/core/anthropic-attribution-path.js.map +1 -0
  18. package/dist/src/core/anthropic-attribution.js +2247 -0
  19. package/dist/src/core/anthropic-attribution.js.map +1 -0
  20. package/dist/src/core/attested-pi-contract.js +5 -0
  21. package/dist/src/core/attested-pi-contract.js.map +1 -0
  22. package/dist/src/core/attested-pi-run.js +749 -0
  23. package/dist/src/core/attested-pi-run.js.map +1 -0
  24. package/dist/src/core/canonical-json.js +19 -0
  25. package/dist/src/core/canonical-json.js.map +1 -0
  26. package/dist/src/core/common.js +787 -0
  27. package/dist/src/core/common.js.map +1 -0
  28. package/dist/src/core/config.js +78 -0
  29. package/dist/src/core/config.js.map +1 -0
  30. package/dist/src/core/context/parent-snapshot.js +75 -0
  31. package/dist/src/core/context/parent-snapshot.js.map +1 -0
  32. package/dist/src/core/context/token-budget.js +631 -0
  33. package/dist/src/core/context/token-budget.js.map +1 -0
  34. package/dist/src/core/context/visible-conversation-v2.js +390 -0
  35. package/dist/src/core/context/visible-conversation-v2.js.map +1 -0
  36. package/dist/src/core/delegate/artifacts.js +357 -0
  37. package/dist/src/core/delegate/artifacts.js.map +1 -0
  38. package/dist/src/core/delegate/budget.js +266 -0
  39. package/dist/src/core/delegate/budget.js.map +1 -0
  40. package/dist/src/core/delegate/facade-contract.js +7 -0
  41. package/dist/src/core/delegate/facade-contract.js.map +1 -0
  42. package/dist/src/core/delegate/hook-contract-evidence.json +18 -0
  43. package/dist/src/core/delegate/hook-contract.js +124 -0
  44. package/dist/src/core/delegate/hook-contract.js.map +1 -0
  45. package/dist/src/core/delegate/launch.js +366 -0
  46. package/dist/src/core/delegate/launch.js.map +1 -0
  47. package/dist/src/core/delegate/result-package.js +293 -0
  48. package/dist/src/core/delegate/result-package.js.map +1 -0
  49. package/dist/src/core/delegate/runner.js +398 -0
  50. package/dist/src/core/delegate/runner.js.map +1 -0
  51. package/dist/src/core/delegate/seed.js +334 -0
  52. package/dist/src/core/delegate/seed.js.map +1 -0
  53. package/dist/src/core/delegate/types.js +103 -0
  54. package/dist/src/core/delegate/types.js.map +1 -0
  55. package/dist/src/core/durable-fs.js +372 -0
  56. package/dist/src/core/durable-fs.js.map +1 -0
  57. package/dist/src/core/extension-api.js +382 -0
  58. package/dist/src/core/extension-api.js.map +1 -0
  59. package/dist/src/core/fusion/artifacts.js +654 -0
  60. package/dist/src/core/fusion/artifacts.js.map +1 -0
  61. package/dist/src/core/fusion/budget.js +949 -0
  62. package/dist/src/core/fusion/budget.js.map +1 -0
  63. package/dist/src/core/fusion/child-protocol.js +176 -0
  64. package/dist/src/core/fusion/child-protocol.js.map +1 -0
  65. package/dist/src/core/fusion/claude-cache.js +156 -0
  66. package/dist/src/core/fusion/claude-cache.js.map +1 -0
  67. package/dist/src/core/fusion/clean-context.js +64 -0
  68. package/dist/src/core/fusion/clean-context.js.map +1 -0
  69. package/dist/src/core/fusion/config.js +349 -0
  70. package/dist/src/core/fusion/config.js.map +1 -0
  71. package/dist/src/core/fusion/context.js +184 -0
  72. package/dist/src/core/fusion/context.js.map +1 -0
  73. package/dist/src/core/fusion/evaluation.js +696 -0
  74. package/dist/src/core/fusion/evaluation.js.map +1 -0
  75. package/dist/src/core/fusion/facade-contract.js +3 -0
  76. package/dist/src/core/fusion/facade-contract.js.map +1 -0
  77. package/dist/src/core/fusion/orchestrator.js +867 -0
  78. package/dist/src/core/fusion/orchestrator.js.map +1 -0
  79. package/dist/src/core/fusion/output-contract.js +27 -0
  80. package/dist/src/core/fusion/output-contract.js.map +1 -0
  81. package/dist/src/core/fusion/pi-child.js +1741 -0
  82. package/dist/src/core/fusion/pi-child.js.map +1 -0
  83. package/dist/src/core/fusion/prompts.js +260 -0
  84. package/dist/src/core/fusion/prompts.js.map +1 -0
  85. package/dist/src/core/fusion/result-package.js +857 -0
  86. package/dist/src/core/fusion/result-package.js.map +1 -0
  87. package/dist/src/core/fusion/source-policy.js +259 -0
  88. package/dist/src/core/fusion/source-policy.js.map +1 -0
  89. package/dist/src/core/fusion/types.js +207 -0
  90. package/dist/src/core/fusion/types.js.map +1 -0
  91. package/dist/src/core/fusion/web-fetch.js +777 -0
  92. package/dist/src/core/fusion/web-fetch.js.map +1 -0
  93. package/dist/src/core/fusion/workflows.js +126 -0
  94. package/dist/src/core/fusion/workflows.js.map +1 -0
  95. package/dist/src/core/lazy-module.js +181 -0
  96. package/dist/src/core/lazy-module.js.map +1 -0
  97. package/dist/src/core/pi-launch.js +431 -0
  98. package/dist/src/core/pi-launch.js.map +1 -0
  99. package/dist/src/core/registry.js +3331 -0
  100. package/dist/src/core/registry.js.map +1 -0
  101. package/dist/src/core/reload-shell-owner.js +1396 -0
  102. package/dist/src/core/reload-shell-owner.js.map +1 -0
  103. package/dist/src/core/shell-policy.js +80 -0
  104. package/dist/src/core/shell-policy.js.map +1 -0
  105. package/dist/src/core/task-durable.js +51 -0
  106. package/dist/src/core/task-durable.js.map +1 -0
  107. package/dist/src/core/update-check.js +92 -0
  108. package/dist/src/core/update-check.js.map +1 -0
  109. package/dist/src/core/windows-taskkill.js +185 -0
  110. package/dist/src/core/windows-taskkill.js.map +1 -0
  111. package/dist/src/delegate-child-extension.js +776 -0
  112. package/dist/src/delegate-child-extension.js.map +1 -0
  113. package/dist/src/delegate-extension.js +746 -0
  114. package/dist/src/delegate-extension.js.map +1 -0
  115. package/dist/src/extension.js +953 -0
  116. package/dist/src/extension.js.map +1 -0
  117. package/dist/src/fusion-child-extension.js +760 -0
  118. package/dist/src/fusion-child-extension.js.map +1 -0
  119. package/dist/src/fusion-extension.js +1030 -0
  120. package/dist/src/fusion-extension.js.map +1 -0
  121. package/dist/src/ui/background-tasks-manager.js +689 -0
  122. package/dist/src/ui/background-tasks-manager.js.map +1 -0
  123. package/dist/src/ui/fusion-model-selector.js +277 -0
  124. package/dist/src/ui/fusion-model-selector.js.map +1 -0
  125. package/docs/INDEX.md +35 -33
  126. package/docs/api/eventbus-v1.md +21 -5
  127. package/docs/choose-a-workflow.md +2 -0
  128. package/docs/commands/bg-clear.md +3 -3
  129. package/docs/commands/bg-update.md +3 -3
  130. package/docs/commands/bg.md +18 -6
  131. package/docs/commands/claude-cache.md +4 -4
  132. package/docs/commands/fusion-models.md +49 -4
  133. package/docs/commands/fusion.md +3 -3
  134. package/docs/commands/jobs.md +3 -3
  135. package/docs/commands/kill.md +3 -3
  136. package/docs/commands/logs.md +3 -3
  137. package/docs/commands/task-manager.md +5 -5
  138. package/docs/concepts/completion-delivery.md +6 -2
  139. package/docs/getting-started.md +7 -1
  140. package/docs/manifest.json +228 -42
  141. package/docs/operations/configuration.md +58 -5
  142. package/docs/operations/releasing.md +4 -2
  143. package/docs/operations/testing.md +22 -0
  144. package/docs/operations/troubleshooting.md +6 -2
  145. package/docs/read-before-edit.md +44 -31
  146. package/docs/reference/runtime-contracts.md +87 -51
  147. package/docs/reference/shortcuts-and-dock.md +28 -14
  148. package/docs/subsystems/anthropic-attribution.md +23 -11
  149. package/docs/subsystems/attested-pi-runs.md +12 -7
  150. package/docs/subsystems/background-task-runtime.md +53 -11
  151. package/docs/subsystems/child-launch-durability-and-safety.md +28 -9
  152. package/docs/subsystems/delegation.md +16 -3
  153. package/docs/subsystems/docs-freshness-gate.md +14 -7
  154. package/docs/subsystems/fusion.md +36 -11
  155. package/docs/subsystems/host-ui-and-telemetry.md +22 -9
  156. package/docs/tools/bg_delegate.md +3 -1
  157. package/docs/tools/bg_kill.md +3 -1
  158. package/docs/tools/bg_logs.md +3 -1
  159. package/docs/tools/bg_result.md +5 -3
  160. package/docs/tools/bg_run.md +31 -5
  161. package/docs/tools/bg_run_pi_attested.md +3 -1
  162. package/docs/tools/bg_status.md +3 -1
  163. package/docs/tools/fusion_investigate.md +3 -1
  164. package/docs/tools/fusion_reason.md +3 -1
  165. package/docs/tools/fusion_research.md +3 -1
  166. package/docs/tools/fusion_validate.md +3 -1
  167. package/extensions/anthropic-attribution-child.ts +3 -0
  168. package/extensions/anthropic-attribution.ts +166 -1
  169. package/package.json +12 -10
  170. package/src/core/anthropic-attribution-path.ts +5 -4
  171. package/src/core/anthropic-attribution.ts +293 -113
  172. package/src/core/attested-pi-contract.ts +4 -0
  173. package/src/core/attested-pi-run.ts +468 -81
  174. package/src/core/canonical-json.ts +21 -0
  175. package/src/core/common.ts +450 -40
  176. package/src/core/config.ts +121 -0
  177. package/src/core/context/visible-conversation-v2.ts +3 -6
  178. package/src/core/delegate/artifacts.ts +13 -9
  179. package/src/core/delegate/budget.ts +13 -6
  180. package/src/core/delegate/facade-contract.ts +6 -0
  181. package/src/core/delegate/result-package.ts +32 -14
  182. package/src/core/delegate/runner.ts +129 -58
  183. package/src/core/delegate/seed.ts +5 -7
  184. package/src/core/durable-fs.ts +178 -30
  185. package/src/core/extension-api.ts +36 -8
  186. package/src/core/fusion/artifacts.ts +17 -16
  187. package/src/core/fusion/clean-context.ts +8 -5
  188. package/src/core/fusion/config.ts +2 -1
  189. package/src/core/fusion/context.ts +2 -6
  190. package/src/core/fusion/facade-contract.ts +2 -0
  191. package/src/core/fusion/orchestrator.ts +3 -3
  192. package/src/core/fusion/prompts.ts +1 -1
  193. package/src/core/fusion/result-package.ts +259 -72
  194. package/src/core/fusion/source-policy.ts +43 -17
  195. package/src/core/lazy-module.ts +215 -0
  196. package/src/core/pi-launch.ts +390 -65
  197. package/src/core/registry.ts +2010 -381
  198. package/src/core/reload-shell-owner.ts +1662 -0
  199. package/src/core/shell-policy.ts +134 -0
  200. package/src/core/task-durable.ts +67 -0
  201. package/src/delegate-extension.ts +404 -144
  202. package/src/extension.ts +387 -114
  203. package/src/fusion-extension.ts +202 -96
@@ -1,3 +1,4 @@
1
+ import { createHash } from 'node:crypto';
1
2
  import { readFile } from 'node:fs/promises';
2
3
  import { fileURLToPath } from 'node:url';
3
4
  import type {
@@ -8,15 +9,17 @@ import type {
8
9
  } from '@earendil-works/pi-coding-agent';
9
10
  import { Text } from '@earendil-works/pi-tui';
10
11
  import { Type, type Static } from 'typebox';
11
- import type { BgTask, BgTaskSnapshot, StartDelegateTaskOptions } from './core/common.js';
12
+ import type {
13
+ BgTask,
14
+ BgTaskSnapshot,
15
+ DelegateTaskFacts,
16
+ StartDelegateTaskOptions,
17
+ } from './core/common.js';
12
18
  import { truncateChars } from './core/common.js';
13
- import { sha256Buffer } from './core/attested-pi-run.js';
14
- import { readFusionCommittedResult, readFusionFailureResult } from './core/fusion/result-package.js';
15
- import {
16
- cloneFusionUsage,
17
- type FusionFailureResultView,
18
- type FusionUsage,
19
- type FusionWorkflowId,
19
+ import type {
20
+ FusionFailureResultView,
21
+ FusionUsage,
22
+ FusionWorkflowId,
20
23
  } from './core/fusion/types.js';
21
24
  import {
22
25
  DELEGATE_AUTO_DELIVER_MODES,
@@ -32,21 +35,17 @@ import {
32
35
  type DelegateExtensionMode,
33
36
  type DelegateRoute,
34
37
  } from './core/delegate/types.js';
38
+ import type { DelegateHookContractEvidence } from './core/delegate/hook-contract.js';
35
39
  import {
36
- DELEGATE_INLINE_ANSWER_BYTES,
37
40
  DELEGATE_DEFAULT_MAX_TOOL_CALLS,
38
41
  DELEGATE_DEFAULT_MAX_TURNS,
39
42
  DELEGATE_DEFAULT_TIMEOUT_SECONDS,
40
- } from './core/delegate/budget.js';
41
- import { resolveDelegateRoute } from './core/delegate/launch.js';
43
+ DELEGATE_INLINE_ANSWER_BYTES,
44
+ } from './core/delegate/facade-contract.js';
42
45
  import {
43
- decideDelegateDelivery,
44
- evaluateDelegateTerminal,
45
- inlineTooLarge,
46
- prepareDelegateLaunch,
47
- } from './core/delegate/runner.js';
48
- import { loadDelegateHookContractEvidence } from './core/delegate/launch.js';
49
- import type { DelegateHookContractEvidence } from './core/delegate/hook-contract.js';
46
+ LazyModule,
47
+ SynchronousActivationCloseFence,
48
+ } from './core/lazy-module.js';
50
49
 
51
50
  /**
52
51
  * `bg_delegate` and `bg_result` registration.
@@ -224,6 +223,29 @@ function textContent(text: string) {
224
223
  return [{ type: 'text' as const, text }];
225
224
  }
226
225
 
226
+ function sha256Buffer(buffer: Buffer): string {
227
+ return `sha256:${createHash('sha256').update(buffer).digest('hex')}`;
228
+ }
229
+
230
+ function cloneFusionUsage(usage: FusionUsage): FusionUsage {
231
+ return {
232
+ input: usage.input,
233
+ output: usage.output,
234
+ cacheRead: usage.cacheRead,
235
+ cacheWrite: usage.cacheWrite,
236
+ ...(usage.cacheWrite1h === undefined ? {} : { cacheWrite1h: usage.cacheWrite1h }),
237
+ ...(usage.reasoning === undefined ? {} : { reasoning: usage.reasoning }),
238
+ totalTokens: usage.totalTokens,
239
+ cost: {
240
+ input: usage.cost.input,
241
+ output: usage.cost.output,
242
+ cacheRead: usage.cost.cacheRead,
243
+ cacheWrite: usage.cost.cacheWrite,
244
+ total: usage.cost.total,
245
+ },
246
+ };
247
+ }
248
+
227
249
  function isRecord(value: unknown): value is Record<PropertyKey, unknown> {
228
250
  return typeof value === 'object' && value !== null && !Array.isArray(value);
229
251
  }
@@ -302,16 +324,102 @@ function optionalPositiveInteger(value: unknown, label: string): number | undefi
302
324
  return value;
303
325
  }
304
326
 
327
+ export interface DelegatePreparedLaunch {
328
+ preflight: {
329
+ childSessionId: string;
330
+ seed: { serialized: string };
331
+ limits: { max_turns: number; max_tool_calls: number; timeout_seconds: number };
332
+ plan: {
333
+ child_prompt_utf8_bytes: number;
334
+ launch_input_tokens_upper_bound: number;
335
+ route: { allowed_input_tokens: number };
336
+ retained_growth_budget_tokens: number;
337
+ };
338
+ };
339
+ argv: readonly string[];
340
+ stdinBytes: Buffer;
341
+ env: NodeJS.ProcessEnv;
342
+ facts: DelegateTaskFacts;
343
+ /** Producer-owned rollback, valid only before registry ownership transfers. */
344
+ rollback: () => Promise<void>;
345
+ }
346
+
347
+ export interface DelegateExtensionRuntime {
348
+ loadDelegateHookContractEvidence: (
349
+ raw: string,
350
+ ) => DelegateHookContractEvidence;
351
+ resolveDelegateRoute: typeof import('./core/delegate/launch.js').resolveDelegateRoute;
352
+ prepareDelegateLaunch: (
353
+ input: Parameters<typeof import('./core/delegate/runner.js').prepareDelegateLaunch>[0],
354
+ ) => Promise<DelegatePreparedLaunch>;
355
+ }
356
+
357
+ interface DelegateResultRuntime {
358
+ decideDelegateDelivery: typeof import('./core/delegate/runner.js').decideDelegateDelivery;
359
+ evaluateDelegateTerminal: typeof import('./core/delegate/runner.js').evaluateDelegateTerminal;
360
+ inlineTooLarge: typeof import('./core/delegate/runner.js').inlineTooLarge;
361
+ }
362
+
363
+ interface FusionResultRuntime {
364
+ readFusionCommittedResult: typeof import('./core/fusion/result-package.js').readFusionCommittedResult;
365
+ readFusionFailureResult: typeof import('./core/fusion/result-package.js').readFusionFailureResult;
366
+ }
367
+
368
+ async function importDelegateExtensionRuntime(): Promise<DelegateExtensionRuntime> {
369
+ const [launch, runner] = await Promise.all([
370
+ import('./core/delegate/launch.js'),
371
+ import('./core/delegate/runner.js'),
372
+ ]);
373
+ return {
374
+ loadDelegateHookContractEvidence: launch.loadDelegateHookContractEvidence,
375
+ resolveDelegateRoute: launch.resolveDelegateRoute,
376
+ prepareDelegateLaunch: runner.prepareDelegateLaunch,
377
+ };
378
+ }
379
+
380
+ async function importDelegateResultRuntime(): Promise<DelegateResultRuntime> {
381
+ const runner = await import('./core/delegate/runner.js');
382
+ return {
383
+ decideDelegateDelivery: runner.decideDelegateDelivery,
384
+ evaluateDelegateTerminal: runner.evaluateDelegateTerminal,
385
+ inlineTooLarge: runner.inlineTooLarge,
386
+ };
387
+ }
388
+
389
+ async function importFusionResultRuntime(): Promise<FusionResultRuntime> {
390
+ const resultPackage = await import('./core/fusion/result-package.js');
391
+ return {
392
+ readFusionCommittedResult: resultPackage.readFusionCommittedResult,
393
+ readFusionFailureResult: resultPackage.readFusionFailureResult,
394
+ };
395
+ }
396
+
305
397
  export interface DelegateExtensionDependencies {
306
398
  startDelegateTask: (ctx: ExtensionContext, options: StartDelegateTaskOptions) => Promise<BgTask>;
307
399
  snapshot: (task: BgTask) => BgTaskSnapshot;
308
- resolveTask: (idOrPrefix: string) => BgTask;
309
- claimFusionUsage: (task: BgTask) => Promise<boolean>;
400
+ /** Exact ownership check used only after a rejected starter. */
401
+ isDelegateTaskRegistered: (taskId: string) => boolean;
402
+ /** Activation-local fence installed synchronously before asynchronous cleanup. */
403
+ activationCloseFence: SynchronousActivationCloseFence;
310
404
  /** Overridable so tests can supply observed evidence without touching disk. */
311
405
  loadHookEvidence?: (() => Promise<DelegateHookContractEvidence>) | undefined;
406
+ /** Internal deterministic deferred-import seam. */
407
+ loadRuntime?: (() => Promise<DelegateExtensionRuntime>) | undefined;
408
+ }
409
+
410
+ export interface BackgroundResultExtensionDependencies {
411
+ resolveTask: (idOrPrefix: string) => BgTask;
412
+ claimFusionUsage: (task: BgTask) => Promise<boolean>;
413
+ /** Activation-local fence installed synchronously before asynchronous cleanup. */
414
+ activationCloseFence: SynchronousActivationCloseFence;
415
+ /** Internal deterministic deferred-import seams. */
416
+ loadDelegateResultRuntime?: (() => Promise<DelegateResultRuntime>) | undefined;
417
+ loadFusionResultRuntime?: (() => Promise<FusionResultRuntime>) | undefined;
312
418
  }
313
419
 
314
- async function defaultHookEvidence(): Promise<DelegateHookContractEvidence> {
420
+ async function defaultHookEvidence(
421
+ runtime: LazyModule<DelegateExtensionRuntime>,
422
+ ): Promise<DelegateHookContractEvidence> {
315
423
  let raw: string;
316
424
  try {
317
425
  raw = await readFile(HOOK_EVIDENCE_PATH, 'utf8');
@@ -328,14 +436,66 @@ async function defaultHookEvidence(): Promise<DelegateHookContractEvidence> {
328
436
  },
329
437
  );
330
438
  }
331
- return loadDelegateHookContractEvidence(raw);
439
+ return runtime.run((loaded) => loaded.loadDelegateHookContractEvidence(raw));
440
+ }
441
+
442
+ const DELEGATE_ROLLBACK_ERROR_MAX_CHARS = 320;
443
+
444
+ function boundedDelegateRollbackError(error: unknown): string {
445
+ const text = (error instanceof Error ? error.message : String(error)).replace(/\s+/gu, ' ').trim();
446
+ if (text.length <= DELEGATE_ROLLBACK_ERROR_MAX_CHARS) return text;
447
+ return `${text.slice(0, DELEGATE_ROLLBACK_ERROR_MAX_CHARS)}…`;
448
+ }
449
+
450
+ async function rollbackUnregisteredDelegatePreparation(
451
+ prepared: DelegatePreparedLaunch,
452
+ originalError: unknown,
453
+ ): Promise<void> {
454
+ try {
455
+ await prepared.rollback();
456
+ } catch (cleanupError) {
457
+ const failure = new AggregateError(
458
+ [originalError, cleanupError],
459
+ `bg_delegate could not remove an unregistered preparation: ${boundedDelegateRollbackError(cleanupError)}`,
460
+ );
461
+ console.error(`[delegate] ${failure.message}`);
462
+ throw failure;
463
+ }
332
464
  }
333
465
 
334
466
  export function registerDelegateExtension(
335
467
  pi: ExtensionAPI,
336
468
  deps: DelegateExtensionDependencies,
337
469
  ): void {
338
- const loadEvidence = deps.loadHookEvidence ?? defaultHookEvidence;
470
+ const runtime = new LazyModule(
471
+ 'delegate-producer',
472
+ deps.loadRuntime ?? importDelegateExtensionRuntime,
473
+ );
474
+ const loadEvidence = deps.loadHookEvidence ?? (() => defaultHookEvidence(runtime));
475
+ const preparationAbort = new AbortController();
476
+ const pendingPreparations = new Set<Promise<void>>();
477
+
478
+ const trackPreparation = <T>(operation: Promise<T>): Promise<T> => {
479
+ const drained = operation.then(
480
+ () => undefined,
481
+ () => undefined,
482
+ );
483
+ pendingPreparations.add(drained);
484
+ void drained.then(() => {
485
+ pendingPreparations.delete(drained);
486
+ });
487
+ return operation;
488
+ };
489
+
490
+ const closeDelegateActivation = (): void => {
491
+ runtime.close('Pi session shutdown');
492
+ preparationAbort.abort(new Error('Pi session shutdown cancelled delegate preparation'));
493
+ };
494
+ deps.activationCloseFence.add(closeDelegateActivation);
495
+
496
+ pi.on('session_shutdown', async () => {
497
+ await Promise.all([...pendingPreparations]);
498
+ });
339
499
 
340
500
  pi.registerTool<typeof DelegateParams, DelegateLaunchDetails>({
341
501
  name: DELEGATE_TOOL_NAME,
@@ -401,100 +561,148 @@ export function registerDelegateExtension(
401
561
  const capability = requireCapability(params.capability);
402
562
  const extensionMode = requireExtensionMode(params.extensionMode);
403
563
  const autoDeliver = requireAutoDeliver(params.autoDeliver);
404
- const hookEvidence = await loadEvidence();
405
- const route = resolveDelegateRoute({
406
- requested: params.route,
407
- currentModel:
408
- ctx.model === undefined
409
- ? undefined
410
- : {
411
- provider: ctx.model.provider,
412
- id: ctx.model.id,
413
- contextWindow: ctx.model.contextWindow,
564
+ return runtime.run(async (loaded) => {
565
+ const hookEvidence = await loadEvidence();
566
+ runtime.assertOpen();
567
+ const route = loaded.resolveDelegateRoute({
568
+ requested: params.route,
569
+ currentModel:
570
+ ctx.model === undefined
571
+ ? undefined
572
+ : {
573
+ provider: ctx.model.provider,
574
+ id: ctx.model.id,
575
+ contextWindow: ctx.model.contextWindow,
576
+ },
577
+ availableModels: ctx.modelRegistry.getAll().map((model) => ({
578
+ provider: model.provider,
579
+ id: model.id,
580
+ contextWindow: model.contextWindow,
581
+ })),
582
+ thinkingLevel: pi.getThinkingLevel(),
583
+ });
584
+
585
+ runtime.assertOpen();
586
+ const transaction = (async () => {
587
+ let prepared: DelegatePreparedLaunch | undefined;
588
+ try {
589
+ prepared = await loaded.prepareDelegateLaunch({
590
+ ctx: {
591
+ cwd: ctx.cwd,
592
+ sessionManager: ctx.sessionManager,
593
+ getSystemPrompt: () => ctx.getSystemPrompt(),
414
594
  },
415
- availableModels: ctx.modelRegistry.getAll().map((model) => ({
416
- provider: model.provider,
417
- id: model.id,
418
- contextWindow: model.contextWindow,
419
- })),
420
- thinkingLevel: pi.getThinkingLevel(),
421
- });
595
+ toolCallId,
596
+ prompt: params.prompt,
597
+ capability,
598
+ extensionMode,
599
+ route,
600
+ limitOverrides: {
601
+ maxTurns: params.maxTurns,
602
+ maxToolCalls: params.maxToolCalls,
603
+ timeoutSeconds: params.timeoutSeconds,
604
+ },
605
+ hookEvidence,
606
+ cwd: ctx.cwd,
607
+ sessionId: ctx.sessionManager.getSessionId(),
608
+ autoDeliver,
609
+ signal: preparationAbort.signal,
610
+ });
611
+ runtime.assertOpen();
612
+ } catch (error) {
613
+ if (prepared !== undefined) {
614
+ await rollbackUnregisteredDelegatePreparation(prepared, error);
615
+ }
616
+ if (preparationAbort.signal.aborted) {
617
+ if (error instanceof AggregateError) {
618
+ console.error(
619
+ `[delegate] cancelled preparation cleanup failed: ${boundedDelegateRollbackError(error)}`,
620
+ );
621
+ } else {
622
+ runtime.assertOpen();
623
+ }
624
+ }
625
+ throw error;
626
+ }
422
627
 
423
- const prepared = await prepareDelegateLaunch({
424
- ctx: {
425
- cwd: ctx.cwd,
426
- sessionManager: ctx.sessionManager,
427
- getSystemPrompt: () => ctx.getSystemPrompt(),
428
- },
429
- toolCallId,
430
- prompt: params.prompt,
431
- capability,
432
- extensionMode,
433
- route,
434
- limitOverrides: {
435
- maxTurns: params.maxTurns,
436
- maxToolCalls: params.maxToolCalls,
437
- timeoutSeconds: params.timeoutSeconds,
438
- },
439
- hookEvidence,
440
- cwd: ctx.cwd,
441
- sessionId: ctx.sessionManager.getSessionId(),
442
- autoDeliver,
443
- });
628
+ const launchOptions: StartDelegateTaskOptions = {
629
+ name: params.name,
630
+ argv: prepared.argv,
631
+ stdinBytes: prepared.stdinBytes,
632
+ env: prepared.env,
633
+ facts: prepared.facts,
634
+ notifyOnCompletion: params.notifyOnCompletion ?? true,
635
+ triggerOnCompletion: params.triggerOnCompletion ?? true,
636
+ timeoutSeconds: prepared.preflight.limits.timeout_seconds,
637
+ };
638
+ let task: BgTask;
639
+ try {
640
+ task = await deps.startDelegateTask(ctx, launchOptions);
641
+ } catch (error) {
642
+ let registered: boolean;
643
+ try {
644
+ registered = deps.isDelegateTaskRegistered(prepared.facts.taskId);
645
+ } catch (ownershipError) {
646
+ const failure = new AggregateError(
647
+ [error, ownershipError],
648
+ `bg_delegate could not determine artifact ownership after starter failure: ${boundedDelegateRollbackError(ownershipError)}`,
649
+ );
650
+ console.error(`[delegate] ${failure.message}`);
651
+ throw failure;
652
+ }
653
+ if (!registered) {
654
+ await rollbackUnregisteredDelegatePreparation(prepared, error);
655
+ }
656
+ if (preparationAbort.signal.aborted) runtime.assertOpen();
657
+ throw error;
658
+ }
659
+ return { prepared, launchOptions, task };
660
+ })();
661
+ const { prepared, launchOptions, task } = await trackPreparation(transaction);
662
+ runtime.assertOpen();
444
663
 
445
- const launchOptions: StartDelegateTaskOptions = {
446
- name: params.name,
447
- argv: prepared.argv,
448
- stdinBytes: prepared.stdinBytes,
449
- env: prepared.env,
450
- facts: prepared.facts,
451
- notifyOnCompletion: params.notifyOnCompletion ?? true,
452
- triggerOnCompletion: params.triggerOnCompletion ?? true,
453
- timeoutSeconds: prepared.preflight.limits.timeout_seconds,
454
- };
455
- const task = await deps.startDelegateTask(ctx, launchOptions);
456
-
457
- const details: DelegateLaunchDetails = {
458
- schema_version: 'pi-background-tasks.delegate-launch.v1',
459
- task: deps.snapshot(task),
460
- route: {
461
- provider: route.provider,
462
- model: route.model,
463
- qualified_id: route.qualified_id,
464
- origin: route.origin,
465
- },
466
- child_session_id: prepared.preflight.childSessionId,
467
- artifact_dir: prepared.facts.artifactDir,
468
- seed_sha256: prepared.facts.seedSha256,
469
- seed_utf8_bytes: Buffer.byteLength(prepared.preflight.seed.serialized, 'utf8'),
470
- budget: prepared.facts.budget,
471
- extension_mode: extensionMode,
472
- auto_deliver: autoDeliver,
473
- notify_on_completion: launchOptions.notifyOnCompletion,
474
- trigger_on_completion: launchOptions.triggerOnCompletion,
475
- };
476
- return {
477
- content: textContent(
478
- [
479
- `Started delegate ${params.name} (${task.id})`,
480
- `Route pinned: ${route.qualified_id} (${route.origin}); it is never substituted.`,
481
- `Child session: ${prepared.preflight.childSessionId} (separate from this session)`,
482
- `Artifacts: ${prepared.facts.artifactDir}`,
483
- `Seed: ${String(Buffer.byteLength(prepared.preflight.seed.serialized, 'utf8'))} bytes, sha256 ${prepared.facts.seedSha256}`,
484
- `Child prompt: ${String(prepared.preflight.plan.child_prompt_utf8_bytes)} bytes; launch estimate ${String(prepared.preflight.plan.launch_input_tokens_upper_bound)} / ${String(prepared.preflight.plan.route.allowed_input_tokens)} allowed input tokens; protected retained-growth runway ${String(prepared.preflight.plan.retained_growth_budget_tokens)} tokens.`,
485
- `Estimator: family ${prepared.facts.budget.family}, source ${prepared.facts.budget.rate_source.source}, rate ${String(prepared.facts.budget.rate_source.effective_rate_bytes_per_token_x100)}/100 B/tok + ${String(prepared.facts.budget.rate_source.affine_f_tokens)} tokens${prepared.facts.budget.rate_source.warning === null ? '' : `; warning: ${prepared.facts.budget.rate_source.warning}`}`,
486
- `Capability: ${capability} (read/search/list only)`,
487
- `Extension mode: ${extensionMode}${extensionMode === 'ambient' ? ' — WARNING: arbitrary discovered extension code executes in the child; the tool allowlist does not sandbox it, so inspect-only process isolation is weakened.' : ' (ambient extension discovery disabled)'}`,
488
- `Limits: ${String(prepared.preflight.limits.max_turns)} turns, ${String(prepared.preflight.limits.max_tool_calls)} tool calls, ${String(prepared.preflight.limits.timeout_seconds)}s`,
489
- `Auto-deliver: ${autoDeliver}`,
490
- launchOptions.notifyOnCompletion
491
- ? `Terminal notification: enabled.${launchOptions.triggerOnCompletion ? ' It will start a follow-up turn.' : ' It will not start a turn.'}`
492
- : 'Terminal notification: disabled.',
493
- `Retrieve the verified answer with ${DELEGATE_RESULT_TOOL_NAME}({taskId:"${task.id}"}). Do not poll.`,
494
- ].join('\n'),
495
- ),
496
- details,
497
- };
664
+ const details: DelegateLaunchDetails = {
665
+ schema_version: 'pi-background-tasks.delegate-launch.v1',
666
+ task: deps.snapshot(task),
667
+ route: {
668
+ provider: route.provider,
669
+ model: route.model,
670
+ qualified_id: route.qualified_id,
671
+ origin: route.origin,
672
+ },
673
+ child_session_id: prepared.preflight.childSessionId,
674
+ artifact_dir: prepared.facts.artifactDir,
675
+ seed_sha256: prepared.facts.seedSha256,
676
+ seed_utf8_bytes: Buffer.byteLength(prepared.preflight.seed.serialized, 'utf8'),
677
+ budget: prepared.facts.budget,
678
+ extension_mode: extensionMode,
679
+ auto_deliver: autoDeliver,
680
+ notify_on_completion: launchOptions.notifyOnCompletion,
681
+ trigger_on_completion: launchOptions.triggerOnCompletion,
682
+ };
683
+ return {
684
+ content: textContent(
685
+ [
686
+ `Started delegate ${params.name} (${task.id})`,
687
+ `Route pinned: ${route.qualified_id} (${route.origin}); it is never substituted.`,
688
+ `Child session: ${prepared.preflight.childSessionId} (separate from this session)`,
689
+ `Artifacts: ${prepared.facts.artifactDir}`,
690
+ `Seed: ${String(Buffer.byteLength(prepared.preflight.seed.serialized, 'utf8'))} bytes, sha256 ${prepared.facts.seedSha256}`,
691
+ `Child prompt: ${String(prepared.preflight.plan.child_prompt_utf8_bytes)} bytes; launch estimate ${String(prepared.preflight.plan.launch_input_tokens_upper_bound)} / ${String(prepared.preflight.plan.route.allowed_input_tokens)} allowed input tokens; protected retained-growth runway ${String(prepared.preflight.plan.retained_growth_budget_tokens)} tokens.`,
692
+ `Estimator: family ${prepared.facts.budget.family}, source ${prepared.facts.budget.rate_source.source}, rate ${String(prepared.facts.budget.rate_source.effective_rate_bytes_per_token_x100)}/100 B/tok + ${String(prepared.facts.budget.rate_source.affine_f_tokens)} tokens${prepared.facts.budget.rate_source.warning === null ? '' : `; warning: ${prepared.facts.budget.rate_source.warning}`}`,
693
+ `Capability: ${capability} (read/search/list only)`,
694
+ `Extension mode: ${extensionMode}${extensionMode === 'ambient' ? ' — WARNING: arbitrary discovered extension code executes in the child; the tool allowlist does not sandbox it, so inspect-only process isolation is weakened.' : ' (ambient extension discovery disabled)'}`,
695
+ `Limits: ${String(prepared.preflight.limits.max_turns)} turns, ${String(prepared.preflight.limits.max_tool_calls)} tool calls, ${String(prepared.preflight.limits.timeout_seconds)}s`,
696
+ `Auto-deliver: ${autoDeliver}`,
697
+ launchOptions.notifyOnCompletion
698
+ ? `Terminal notification: enabled.${launchOptions.triggerOnCompletion ? ' It will start a follow-up turn.' : ' It will not start a turn.'}`
699
+ : 'Terminal notification: disabled.',
700
+ `Retrieve the verified answer with ${DELEGATE_RESULT_TOOL_NAME}({taskId:"${task.id}"}). Do not poll.`,
701
+ ].join('\n'),
702
+ ),
703
+ details,
704
+ };
705
+ });
498
706
  },
499
707
  renderCall(args, theme) {
500
708
  return new Text(
@@ -513,6 +721,39 @@ export function registerDelegateExtension(
513
721
  },
514
722
  });
515
723
 
724
+ pi.on('session_start', () => {
725
+ const active = pi.getActiveTools();
726
+ const missing = [DELEGATE_TOOL_NAME].filter((name) => !active.includes(name));
727
+ if (missing.length > 0) pi.setActiveTools([...active, ...missing]);
728
+ });
729
+ }
730
+
731
+ export function registerBackgroundResultExtension(
732
+ pi: ExtensionAPI,
733
+ deps: BackgroundResultExtensionDependencies,
734
+ ): void {
735
+ const delegateResultRuntime = new LazyModule(
736
+ 'delegate-result-verifier',
737
+ deps.loadDelegateResultRuntime ?? importDelegateResultRuntime,
738
+ );
739
+ const fusionResultRuntime = new LazyModule(
740
+ 'fusion-result-verifier',
741
+ deps.loadFusionResultRuntime ?? importFusionResultRuntime,
742
+ );
743
+ let resultClosedError: Error | undefined;
744
+ const assertResultActive = (): void => {
745
+ if (resultClosedError !== undefined) throw resultClosedError;
746
+ };
747
+
748
+ const closeResultActivation = (): void => {
749
+ resultClosedError ??= new Error(
750
+ 'lazy_module_closed: background-result facade belongs to a closed activation (Pi session shutdown)',
751
+ );
752
+ delegateResultRuntime.close('Pi session shutdown');
753
+ fusionResultRuntime.close('Pi session shutdown');
754
+ };
755
+ deps.activationCloseFence.add(closeResultActivation);
756
+
516
757
  pi.registerTool<typeof ResultParams, BackgroundResultDetails>({
517
758
  name: DELEGATE_RESULT_TOOL_NAME,
518
759
  label: 'Background Result',
@@ -542,6 +783,8 @@ export function registerDelegateExtension(
542
783
  return prepared;
543
784
  },
544
785
  async execute(_toolCallId, params) {
786
+ // Reject stale tools before touching the activation-local registry API.
787
+ assertResultActive();
545
788
  let task: BgTask;
546
789
  try {
547
790
  task = deps.resolveTask(params.taskId);
@@ -571,12 +814,15 @@ export function registerDelegateExtension(
571
814
  };
572
815
  }
573
816
  if (task.status !== 'completed' || fusion.outcome?.status !== 'committed') {
574
- const terminal = await readFusionFailureResult({
575
- artifactDirAbs: fusion.artifactDirAbs,
576
- artifactDir: fusion.artifactDir,
577
- runId: fusion.runId,
578
- workflow: fusion.workflow,
579
- });
817
+ const terminal = await fusionResultRuntime.run((loaded) =>
818
+ loaded.readFusionFailureResult({
819
+ artifactDirAbs: fusion.artifactDirAbs,
820
+ artifactDir: fusion.artifactDir,
821
+ runId: fusion.runId,
822
+ workflow: fusion.workflow,
823
+ }),
824
+ );
825
+ fusionResultRuntime.assertOpen();
580
826
  const state =
581
827
  fusion.outcome?.status === 'cancelled' || task.status === 'killed'
582
828
  ? 'cancelled'
@@ -614,12 +860,15 @@ export function registerDelegateExtension(
614
860
  details,
615
861
  };
616
862
  }
617
- const verified = await readFusionCommittedResult({
618
- artifactDirAbs: fusion.artifactDirAbs,
619
- artifactDir: fusion.artifactDir,
620
- runId: fusion.runId,
621
- workflow: fusion.workflow,
622
- });
863
+ const verified = await fusionResultRuntime.run((loaded) =>
864
+ loaded.readFusionCommittedResult({
865
+ artifactDirAbs: fusion.artifactDirAbs,
866
+ artifactDir: fusion.artifactDir,
867
+ runId: fusion.runId,
868
+ workflow: fusion.workflow,
869
+ }),
870
+ );
871
+ fusionResultRuntime.assertOpen();
623
872
  const answerBytes = Buffer.byteLength(verified.mergedText, 'utf8');
624
873
  const answerSha256 = sha256Buffer(Buffer.from(verified.mergedText, 'utf8'));
625
874
  const useArtifact =
@@ -630,6 +879,12 @@ export function registerDelegateExtension(
630
879
  `Fusion result ${task.id} is ${String(answerBytes)} bytes, above the ${String(DELEGATE_INLINE_ANSWER_BYTES)}-byte inline limit. Use delivery:"artifact"; nothing was truncated.`,
631
880
  );
632
881
  }
882
+ fusionResultRuntime.assertOpen();
883
+ // The durable claim is the settlement point. Once started, this verified
884
+ // retrieval must return its usage even if shutdown closes the facade
885
+ // while registry metadata is being written. Everything after the await
886
+ // is activation-local result construction with no host side effects.
887
+ const usage = cloneFusionUsage(verified.details.usage);
633
888
  const usageDelivered = await deps.claimFusionUsage(task);
634
889
  const details: FusionBackgroundResultDetails = {
635
890
  schema_version: 'pi-background-tasks.fusion-result-view.v1',
@@ -661,7 +916,7 @@ export function registerDelegateExtension(
661
916
  if (!usageDelivered) return result;
662
917
  const resultWithUsage: typeof result & { usage: FusionUsage } = {
663
918
  ...result,
664
- usage: cloneFusionUsage(verified.details.usage),
919
+ usage,
665
920
  };
666
921
  return resultWithUsage;
667
922
  }
@@ -673,7 +928,8 @@ export function registerDelegateExtension(
673
928
  { code: 'task_unknown', childCreated: false },
674
929
  );
675
930
  }
676
- if (task.status === 'running') {
931
+ const delegateTaskStatus = task.status;
932
+ if (delegateTaskStatus === 'running') {
677
933
  const details: DelegateResultDetails = {
678
934
  schema_version: 'pi-background-tasks.delegate-result-view.v1',
679
935
  task_id: task.id,
@@ -691,17 +947,23 @@ export function registerDelegateExtension(
691
947
  };
692
948
  }
693
949
 
694
- const terminal = await evaluateDelegateTerminal({
695
- artifactDirAbs: facts.artifactDirAbs,
696
- taskId: facts.taskId,
697
- launchNonce: facts.launchNonce,
698
- seedSha256: facts.seedSha256,
699
- route: { provider: facts.route.provider, model: facts.route.model },
700
- taskStatus: task.status === 'completed' ? 'completed' : task.status,
701
- taskError: task.error,
702
- taskOutputPath: task.outputPath,
703
- taskOutputAbsPath: task.outputAbsPath,
704
- });
950
+ const evaluation = await delegateResultRuntime.run(async (loaded) => ({
951
+ loaded,
952
+ terminal: await loaded.evaluateDelegateTerminal({
953
+ artifactDirAbs: facts.artifactDirAbs,
954
+ taskId: facts.taskId,
955
+ launchNonce: facts.launchNonce,
956
+ seedSha256: facts.seedSha256,
957
+ route: { provider: facts.route.provider, model: facts.route.model },
958
+ taskStatus:
959
+ delegateTaskStatus === 'completed' ? 'completed' : delegateTaskStatus,
960
+ taskError: task.error,
961
+ taskOutputPath: task.outputPath,
962
+ taskOutputAbsPath: task.outputAbsPath,
963
+ }),
964
+ }));
965
+ delegateResultRuntime.assertOpen();
966
+ const { loaded: verifier, terminal } = evaluation;
705
967
 
706
968
  if (terminal.error !== undefined || terminal.result === undefined) {
707
969
  const failure =
@@ -717,12 +979,12 @@ export function registerDelegateExtension(
717
979
 
718
980
  const verified = terminal.result;
719
981
  const requestedDelivery = requireDelivery(params.delivery);
720
- const decision = decideDelegateDelivery(
982
+ const decision = verifier.decideDelegateDelivery(
721
983
  verified.package.answer.byte_length,
722
984
  requestedDelivery,
723
985
  );
724
986
  if (requestedDelivery === 'inline' && decision.mode === 'artifact') {
725
- const failure = inlineTooLarge(
987
+ const failure = verifier.inlineTooLarge(
726
988
  task.id,
727
989
  facts.artifactDir,
728
990
  verified.package.answer.byte_length,
@@ -796,9 +1058,7 @@ export function registerDelegateExtension(
796
1058
 
797
1059
  pi.on('session_start', () => {
798
1060
  const active = pi.getActiveTools();
799
- const missing = [DELEGATE_TOOL_NAME, DELEGATE_RESULT_TOOL_NAME].filter(
800
- (name) => !active.includes(name),
801
- );
1061
+ const missing = [DELEGATE_RESULT_TOOL_NAME].filter((name) => !active.includes(name));
802
1062
  if (missing.length > 0) pi.setActiveTools([...active, ...missing]);
803
1063
  });
804
1064
  }