pi-background-tasks 2.1.3 → 2.3.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 (44) hide show
  1. package/BACKGROUND-TASKS-INSTRUCTIONS.md +1 -1
  2. package/PUBLISHING.md +2 -0
  3. package/README.md +16 -9
  4. package/TESTING.md +15 -10
  5. package/TEST_PLAN.md +12 -11
  6. package/THIRD_PARTY_NOTICES.md +30 -0
  7. package/docs/INDEX.md +8 -4
  8. package/docs/choose-a-workflow.md +5 -2
  9. package/docs/commands/claude-cache.md +50 -0
  10. package/docs/getting-started.md +3 -0
  11. package/docs/manifest.json +84 -19
  12. package/docs/operations/configuration.md +15 -1
  13. package/docs/operations/releasing.md +6 -3
  14. package/docs/read-before-edit.md +4 -1
  15. package/docs/reference/runtime-contracts.md +72 -71
  16. package/docs/subsystems/anthropic-attribution.md +63 -0
  17. package/docs/subsystems/attested-pi-runs.md +2 -2
  18. package/docs/subsystems/child-launch-durability-and-safety.md +3 -3
  19. package/docs/subsystems/delegation.md +12 -4
  20. package/docs/subsystems/docs-freshness-gate.md +6 -6
  21. package/docs/subsystems/fusion.md +6 -2
  22. package/docs/tools/bg_delegate.md +21 -9
  23. package/docs/tools/bg_result.md +8 -1
  24. package/docs/tools/bg_run.md +5 -0
  25. package/extensions/anthropic-attribution.ts +1 -0
  26. package/package.json +4 -2
  27. package/src/core/anthropic-attribution-path.ts +26 -0
  28. package/src/core/{fusion/anthropic-attribution.ts → anthropic-attribution.ts} +61 -8
  29. package/src/core/attested-pi-run.ts +10 -1
  30. package/src/core/common.ts +2 -1
  31. package/src/core/delegate/artifacts.ts +4 -0
  32. package/src/core/delegate/launch.ts +44 -14
  33. package/src/core/delegate/runner.ts +24 -0
  34. package/src/core/delegate/seed.ts +12 -0
  35. package/src/core/delegate/types.ts +10 -2
  36. package/src/core/fusion/artifacts.ts +265 -3
  37. package/src/core/fusion/config.ts +1 -1
  38. package/src/core/fusion/orchestrator.ts +47 -57
  39. package/src/core/fusion/pi-child.ts +7 -124
  40. package/src/core/fusion/result-package.ts +550 -3
  41. package/src/core/fusion/types.ts +87 -0
  42. package/src/core/registry.ts +4 -1
  43. package/src/delegate-child-extension.ts +9 -2
  44. package/src/delegate-extension.ts +119 -9
@@ -5,6 +5,8 @@ import { FUSION_BUDGET_POLICY, FusionBudget } from './budget.js';
5
5
  import { assertChildOutputWithinContract } from './output-contract.js';
6
6
  import {
7
7
  FusionArtifactStore,
8
+ buildFusionFailureSummary,
9
+ buildFusionRunProgress as deriveFusionRunProgress,
8
10
  type CreateFusionArtifactStoreOptions,
9
11
  type RecordFusionFailedAttemptInput,
10
12
  } from './artifacts.js';
@@ -40,9 +42,7 @@ import {
40
42
  FUSION_VALIDATE_CANDIDATE_SCHEMA_VERSION,
41
43
  FusionError,
42
44
  addFusionUsage,
43
- cloneFusionUsage,
44
45
  createEmptyFusionUsage,
45
- type FusionArtifactManifest,
46
46
  type FusionCalibrationViolation,
47
47
  type FusionCapability,
48
48
  type FusionCanonicalInputV3,
@@ -176,58 +176,7 @@ function asFusionError(error: unknown, artifactDir: string, messageOverride?: st
176
176
  });
177
177
  }
178
178
 
179
- function fusionStageProgress(
180
- manifest: FusionArtifactManifest,
181
- stage: FusionStage,
182
- ): FusionRunProgress['candidates'] {
183
- const attempts = manifest.attempts.filter((attempt) => attempt.stage === stage);
184
- const created = attempts.filter((attempt) => attempt.child_created).length;
185
- const completed = attempts.filter(
186
- (attempt) => attempt.child_created && attempt.status === 'completed',
187
- ).length;
188
- const failed = attempts.filter(
189
- (attempt) => attempt.child_created && attempt.status === 'failed',
190
- ).length;
191
- const cancelled = attempts.filter(
192
- (attempt) => attempt.child_created && attempt.status === 'cancelled',
193
- ).length;
194
- const completedByState =
195
- stage === 'candidate'
196
- ? completed >= 3
197
- : stage === 'evaluation'
198
- ? manifest.artifacts['evaluation.json'] !== undefined ||
199
- manifest.state === 'evaluation_complete' ||
200
- manifest.state === 'merging' ||
201
- manifest.state === 'completed'
202
- : manifest.artifacts['merged.md'] !== undefined || manifest.state === 'completed';
203
- const progress: FusionRunProgress['candidates'] = {
204
- status: completedByState ? 'completed' : created === 0 ? 'not_started' : 'incomplete',
205
- attempts_recorded: attempts.length,
206
- children_created: created,
207
- children_completed: completed,
208
- children_failed: failed,
209
- children_cancelled: cancelled,
210
- };
211
- if (stage === 'candidate') {
212
- const createdSlots = new Set(
213
- attempts.flatMap((attempt) =>
214
- attempt.child_created && attempt.slot !== undefined ? [attempt.slot] : [],
215
- ),
216
- );
217
- progress.not_started_slots = 3 - createdSlots.size;
218
- }
219
- return progress;
220
- }
221
-
222
- export function buildFusionRunProgress(manifest: FusionArtifactManifest): FusionRunProgress {
223
- return {
224
- manifest_state: manifest.state,
225
- candidates: fusionStageProgress(manifest, 'candidate'),
226
- evaluation: fusionStageProgress(manifest, 'evaluation'),
227
- merge: fusionStageProgress(manifest, 'merge'),
228
- usage_so_far: cloneFusionUsage(manifest.usage),
229
- };
230
- }
179
+ export { buildFusionRunProgress } from './artifacts.js';
231
180
 
232
181
  function formatFusionRunStage(name: string, stage: FusionRunProgress['candidates']): string {
233
182
  const notStarted =
@@ -237,7 +186,31 @@ function formatFusionRunStage(name: string, stage: FusionRunProgress['candidates
237
186
  return `${name}=${stage.status} (${String(stage.children_created)} created, ${String(stage.children_completed)} completed, ${String(stage.children_failed)} failed, ${String(stage.children_cancelled)} cancelled${notStarted})`;
238
187
  }
239
188
 
240
- export function formatFusionRunProgress(progress: FusionRunProgress): string {
189
+ export function summaryUnavailableNote(error: unknown): string {
190
+ const detail = errorText(error);
191
+ const detailBytes = Buffer.from(detail, 'utf8');
192
+ if (detailBytes.length > 1024) {
193
+ return 'failure-summary.json unavailable after terminal publication; write failure detail omitted because it exceeds the 1024-byte diagnostic cap.';
194
+ }
195
+ return `failure-summary.json unavailable after terminal publication: ${detail}`;
196
+ }
197
+
198
+ function withSummaryUnavailableNote(error: FusionError, summaryError: unknown): FusionError {
199
+ const details: FusionErrorDetails = {
200
+ code: error.code,
201
+ transient: error.transient,
202
+ childCreated: error.childCreated,
203
+ };
204
+ if (error.artifactDir !== undefined) details.artifactDir = error.artifactDir;
205
+ if (error.stage !== undefined) details.stage = error.stage;
206
+ if (error.slot !== undefined) details.slot = error.slot;
207
+ if (error.attempt !== undefined) details.attempt = error.attempt;
208
+ if (error.budget !== undefined) details.budget = error.budget;
209
+ if (error.runProgress !== undefined) details.runProgress = error.runProgress;
210
+ return new FusionError(`${error.message}\n${summaryUnavailableNote(summaryError)}`, details);
211
+ }
212
+
213
+ function formatFusionRunProgress(progress: FusionRunProgress): string {
241
214
  const usage = progress.usage_so_far;
242
215
  const optionalUsage = [
243
216
  usage.cacheWrite1h === undefined ? undefined : `cacheWrite1h=${String(usage.cacheWrite1h)}`,
@@ -962,9 +935,26 @@ export class FusionOrchestrator {
962
935
  terminalError = withRunProgress(
963
936
  error,
964
937
  store.artifactDir,
965
- buildFusionRunProgress(store.snapshot()),
938
+ deriveFusionRunProgress(store.snapshot()),
966
939
  );
967
- await store.writeError(cancelled ? 'cancelled' : 'failed', terminalError.message);
940
+ const terminalState = cancelled ? 'cancelled' : 'failed';
941
+ await store.writeError(terminalState, terminalError.message);
942
+ // The terminal manifest/error are authoritative. Summary persistence is
943
+ // subordinate and intentionally attempted once from that fresh snapshot.
944
+ try {
945
+ const terminalManifest = store.snapshot();
946
+ await store.writeFailureSummary(
947
+ buildFusionFailureSummary({
948
+ manifest: terminalManifest,
949
+ terminalError,
950
+ progress: deriveFusionRunProgress(terminalManifest),
951
+ terminalState,
952
+ createdAt: terminalManifest.updated_at,
953
+ }),
954
+ );
955
+ } catch (summaryError) {
956
+ terminalError = withSummaryUnavailableNote(terminalError, summaryError);
957
+ }
968
958
  } catch (artifactError) {
969
959
  throw withTerminalArtifactFailure(error, store.artifactDir, artifactError);
970
960
  }
@@ -1,8 +1,7 @@
1
1
  import { spawn as nodeSpawn, type SpawnOptions } from 'node:child_process';
2
2
  import { createHash } from 'node:crypto';
3
- import { constants, existsSync, readFileSync } from 'node:fs';
3
+ import { constants, existsSync } from 'node:fs';
4
4
  import { open } from 'node:fs/promises';
5
- import { createRequire } from 'node:module';
6
5
  import { dirname, resolve } from 'node:path';
7
6
  import { fileURLToPath } from 'node:url';
8
7
  import {
@@ -73,6 +72,7 @@ import {
73
72
  resolvePiLaunch,
74
73
  type PiLaunchDependencies,
75
74
  } from '../pi-launch.js';
75
+ import { resolveAnthropicAttributionExtensionPath } from '../anthropic-attribution-path.js';
76
76
 
77
77
  // The response cap now applies to one final full answer, not cumulative Pi JSON events.
78
78
  export const FUSION_CHILD_STDOUT_LIMIT_BYTES = 32 * 1024 * 1024;
@@ -274,21 +274,6 @@ export function fusionPiChildEnv(
274
274
  return out;
275
275
  }
276
276
 
277
- export function resolveFusionAnthropicAttributionExtensionPath(
278
- moduleUrl = import.meta.url,
279
- pathExists: (path: string) => boolean = existsSync,
280
- ): string {
281
- const modulePath = fileURLToPath(moduleUrl);
282
- const extension = modulePath.endsWith('.ts')
283
- ? 'anthropic-attribution.ts'
284
- : 'anthropic-attribution.js';
285
- const candidate = resolve(dirname(modulePath), extension);
286
- if (!pathExists(candidate)) {
287
- throw new Error(`Fusion Anthropic attribution extension is missing: ${candidate}`);
288
- }
289
- return candidate;
290
- }
291
-
292
277
  export function resolveFusionChildExtensionPath(
293
278
  moduleUrl = import.meta.url,
294
279
  pathExists: (path: string) => boolean = existsSync,
@@ -303,109 +288,10 @@ export function resolveFusionChildExtensionPath(
303
288
  }
304
289
 
305
290
  /**
306
- * Provider whose children require the Anthropic system-prompt sanitizer.
307
- *
308
- * Pi's own system prompt contains documentation lines that Anthropic rejects, so a
309
- * Claude child launched without the sanitizer fails at the provider rather than
310
- * producing an answer. The parent session loads the sanitizer through ordinary
311
- * extension discovery, but Fusion children run with `--no-extensions` for
312
- * isolation and therefore inherit nothing; the sanitizer must be re-supplied
313
- * explicitly per child.
291
+ * Provider whose isolated children require the package-owned attribution and
292
+ * exact-match system-prompt sanitization extension.
314
293
  */
315
294
  export const FUSION_SANITIZED_PROVIDER = 'anthropic';
316
- export const FUSION_ANTHROPIC_SANITIZER_PACKAGE = '@ravshansbox/pi-anthropic-sps';
317
- const FUSION_ANTHROPIC_SANITIZER_MANIFEST = `${FUSION_ANTHROPIC_SANITIZER_PACKAGE}/package.json`;
318
-
319
- export interface FusionSanitizerDependencies {
320
- resolvePackageJson?: ((specifier: string) => string) | undefined;
321
- readManifest?: ((path: string) => string) | undefined;
322
- pathExists?: ((path: string) => boolean) | undefined;
323
- }
324
-
325
- function manifestExtensionEntry(manifestText: string, manifestPath: string): string {
326
- let parsed: unknown;
327
- try {
328
- parsed = parseJsonText(manifestText);
329
- } catch (error) {
330
- throw new FusionError(
331
- `Anthropic sanitizer manifest ${manifestPath} is not valid JSON: ${error instanceof Error ? error.message : String(error)}`,
332
- { code: 'orchestration_failed', childCreated: false },
333
- );
334
- }
335
- if (!isJsonObject(parsed)) {
336
- throw new FusionError(`Anthropic sanitizer manifest ${manifestPath} must be an object`, {
337
- code: 'orchestration_failed',
338
- childCreated: false,
339
- });
340
- }
341
- const pi = parsed['pi'];
342
- if (!isJsonObject(pi)) {
343
- throw new FusionError(
344
- `Anthropic sanitizer manifest ${manifestPath} has no "pi" section declaring its extension`,
345
- { code: 'orchestration_failed', childCreated: false },
346
- );
347
- }
348
- const extensions = pi['extensions'];
349
- if (!Array.isArray(extensions) || extensions.length === 0) {
350
- throw new FusionError(
351
- `Anthropic sanitizer manifest ${manifestPath} declares no pi.extensions entries`,
352
- { code: 'orchestration_failed', childCreated: false },
353
- );
354
- }
355
- const [entry] = extensions;
356
- if (typeof entry !== 'string' || entry.trim().length === 0) {
357
- throw new FusionError(
358
- `Anthropic sanitizer manifest ${manifestPath} pi.extensions[0] must be a non-blank string`,
359
- { code: 'orchestration_failed', childCreated: false },
360
- );
361
- }
362
- return entry;
363
- }
364
-
365
- /**
366
- * Resolve the sanitizer extension file shipped by the sanitizer package.
367
- *
368
- * The package intentionally publishes no `main`/`exports`, so the entry cannot be
369
- * required directly; its manifest is resolved and the declared `pi.extensions[0]`
370
- * path is joined against the package root. Every failure is loud: a Claude child
371
- * launched without the sanitizer would fail at the provider with a far less
372
- * actionable error, so silently omitting it is never correct.
373
- */
374
- export function resolveAnthropicSanitizerExtensionPath(
375
- dependencies: FusionSanitizerDependencies = {},
376
- ): string {
377
- const resolvePackageJson =
378
- dependencies.resolvePackageJson ?? createRequire(import.meta.url).resolve;
379
- const readManifest = dependencies.readManifest ?? ((path: string) => readFileSync(path, 'utf8'));
380
- const pathExists = dependencies.pathExists ?? existsSync;
381
- let manifestPath: string;
382
- try {
383
- manifestPath = resolvePackageJson(FUSION_ANTHROPIC_SANITIZER_MANIFEST);
384
- } catch (error) {
385
- throw new FusionError(
386
- `Anthropic sanitizer package ${FUSION_ANTHROPIC_SANITIZER_PACKAGE} could not be resolved: ${error instanceof Error ? error.message : String(error)}. Claude children cannot be launched without it.`,
387
- { code: 'orchestration_failed', childCreated: false },
388
- );
389
- }
390
- let manifestText: string;
391
- try {
392
- manifestText = readManifest(manifestPath);
393
- } catch (error) {
394
- throw new FusionError(
395
- `Anthropic sanitizer manifest ${manifestPath} could not be read: ${error instanceof Error ? error.message : String(error)}`,
396
- { code: 'orchestration_failed', childCreated: false },
397
- );
398
- }
399
- const entry = manifestExtensionEntry(manifestText, manifestPath);
400
- const extensionPath = resolve(dirname(manifestPath), entry);
401
- if (!pathExists(extensionPath)) {
402
- throw new FusionError(
403
- `Anthropic sanitizer extension is missing: ${extensionPath} (declared by ${manifestPath})`,
404
- { code: 'orchestration_failed', childCreated: false },
405
- );
406
- }
407
- return extensionPath;
408
- }
409
295
 
410
296
  export function assertFusionToolPolicyDisjoint(
411
297
  allowlist: readonly string[] = FUSION_INSPECT_TOOLS,
@@ -468,11 +354,10 @@ function fusionToolArgv(capability: FusionCapability): string[] {
468
354
  export function fusionChildExtensionPaths(
469
355
  model: ResolvedFusionModel,
470
356
  childExtensionPath: string,
471
- resolveSanitizer: () => string = resolveAnthropicSanitizerExtensionPath,
472
- resolveAttribution: () => string = resolveFusionAnthropicAttributionExtensionPath,
357
+ resolveAttribution: () => string = resolveAnthropicAttributionExtensionPath,
473
358
  ): readonly string[] {
474
359
  if (model.provider !== FUSION_SANITIZED_PROVIDER) return [childExtensionPath];
475
- return [resolveAttribution(), resolveSanitizer(), childExtensionPath];
360
+ return [resolveAttribution(), childExtensionPath];
476
361
  }
477
362
 
478
363
  export function buildFusionPiChildArgv(
@@ -480,13 +365,11 @@ export function buildFusionPiChildArgv(
480
365
  systemPrompt: string,
481
366
  childExtensionPath = resolveFusionChildExtensionPath(),
482
367
  capability: FusionCapability = FUSION_NO_TOOLS_CAPABILITY,
483
- resolveSanitizer: () => string = resolveAnthropicSanitizerExtensionPath,
484
- resolveAttribution: () => string = resolveFusionAnthropicAttributionExtensionPath,
368
+ resolveAttribution: () => string = resolveAnthropicAttributionExtensionPath,
485
369
  ): string[] {
486
370
  const extensionArgs = fusionChildExtensionPaths(
487
371
  model,
488
372
  childExtensionPath,
489
- resolveSanitizer,
490
373
  resolveAttribution,
491
374
  ).flatMap((path) => ['--extension', path]);
492
375
  return [