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,7 +1,14 @@
1
1
  import { createHash } from 'node:crypto';
2
2
  import { appendFileSync, readFileSync } from 'node:fs';
3
3
  import { homedir } from 'node:os';
4
- import { join } from 'node:path';
4
+ import { isAbsolute, join } from 'node:path';
5
+ import { anthropicMessagesApi } from '@earendil-works/pi-ai/compat';
6
+ import type {
7
+ AssistantMessageEventStream as HostAssistantMessageEventStream,
8
+ Context as HostContext,
9
+ Model as HostModel,
10
+ SimpleStreamOptions as HostSimpleStreamOptions,
11
+ } from '@earendil-works/pi-ai';
5
12
 
6
13
  export const CLAUDE_CODE_SESSION_HEADER = 'X-Claude-Code-Session-Id';
7
14
 
@@ -72,6 +79,7 @@ const CLAUDE_AGENT_SDK_SYSTEM_TEXT =
72
79
  const FINGERPRINT_SALT = '59cf53e54c78';
73
80
  const AUDIT_ENV = 'PIPELINE_ANTHROPIC_ATTRIBUTION_AUDIT_PATH';
74
81
  const CACHE_RETENTION_ENV = 'PI_CACHE_RETENTION';
82
+ export const ANTHROPIC_ACCOUNT_CONFIG_PATH_ENV = 'PI_ANTHROPIC_ACCOUNT_CONFIG_PATH';
75
83
  export const ANTHROPIC_CACHE_RETENTION_ENTRY = 'pipeline-anthropic-cache-retention';
76
84
  const ANTHROPIC_CACHE_RETENTION_SCHEMA = 'pipeline.anthropic_cache_retention.v1';
77
85
  export const ANTHROPIC_ATTRIBUTION_CLAIM_CHANNEL = 'pi-anthropic-attribution:claim:v1';
@@ -146,8 +154,11 @@ interface PiModelCostLike extends PiCostRatesLike {
146
154
  export interface PiModelLike {
147
155
  readonly provider?: string;
148
156
  readonly id?: string;
157
+ readonly name?: string;
149
158
  readonly api?: string;
150
159
  readonly baseUrl?: string;
160
+ readonly input?: readonly ('text' | 'image')[];
161
+ readonly contextWindow?: number;
151
162
  readonly maxTokens?: number;
152
163
  readonly reasoning?: boolean;
153
164
  readonly compat?: {
@@ -394,7 +405,11 @@ interface PiAssistantDiagnosticLike {
394
405
  }
395
406
 
396
407
  type PiMessage =
397
- | { readonly role: 'user'; readonly content: string | readonly PiContentBlock[] }
408
+ | {
409
+ readonly role: 'user';
410
+ readonly content: string | readonly PiContentBlock[];
411
+ readonly timestamp?: number;
412
+ }
398
413
  | {
399
414
  readonly role: 'assistant';
400
415
  readonly content: readonly JsonObject[];
@@ -404,6 +419,7 @@ type PiMessage =
404
419
  readonly responseId?: string;
405
420
  readonly stopReason?: string;
406
421
  readonly diagnostics?: readonly PiAssistantDiagnosticLike[];
422
+ readonly timestamp?: number;
407
423
  }
408
424
  | {
409
425
  readonly role: 'toolResult';
@@ -411,6 +427,7 @@ type PiMessage =
411
427
  readonly toolName?: string;
412
428
  readonly content: readonly PiContentBlock[];
413
429
  readonly isError?: boolean;
430
+ readonly timestamp?: number;
414
431
  };
415
432
 
416
433
  export interface PiStreamContext {
@@ -465,14 +482,20 @@ export interface AssistantMessageLike {
465
482
  cacheRead: number;
466
483
  cacheWrite: number;
467
484
  cacheWrite1h?: number;
485
+ reasoning?: number;
468
486
  totalTokens: number;
469
487
  cost: { input: number; output: number; cacheRead: number; cacheWrite: number; total: number };
470
488
  };
471
- stopReason: 'stop' | 'length' | 'toolUse' | 'aborted' | 'error';
489
+ stopReason: 'pending' | 'stop' | 'length' | 'toolUse' | 'aborted' | 'error' | 'deferred';
472
490
  timestamp: number;
491
+ responseModel?: string;
473
492
  responseId?: string;
493
+ providerThinkingLevel?: string;
494
+ endTurn?: boolean;
474
495
  diagnostics?: PiAssistantDiagnosticLike[];
496
+ deferred?: unknown;
475
497
  errorMessage?: string;
498
+ rawStopReason?: string;
476
499
  }
477
500
 
478
501
  type AssistantMessageEvent =
@@ -862,20 +885,43 @@ export function extractClaudeAttributionAccount(
862
885
  };
863
886
  }
864
887
 
888
+ export function resolveClaudeAttributionConfigPath(
889
+ explicitConfigPath?: string,
890
+ env?: ProviderEnv,
891
+ homeDirectory = homedir(),
892
+ ): string {
893
+ const configuredPath = providerEnvValue(ANTHROPIC_ACCOUNT_CONFIG_PATH_ENV, env);
894
+ const selectedPath = explicitConfigPath ?? configuredPath ?? join(homeDirectory, '.claude.json');
895
+ const source =
896
+ explicitConfigPath !== undefined
897
+ ? 'explicit configPath'
898
+ : configuredPath !== undefined
899
+ ? ANTHROPIC_ACCOUNT_CONFIG_PATH_ENV
900
+ : 'default ~/.claude.json path';
901
+ if (selectedPath.trim().length === 0 || !isAbsolute(selectedPath)) {
902
+ throw new Error(
903
+ `Anthropic attribution ${source} must be a non-empty absolute file path; got ${JSON.stringify(selectedPath)}`,
904
+ );
905
+ }
906
+ return selectedPath;
907
+ }
908
+
865
909
  export function loadClaudeAttributionAccount(
866
- configPath = join(homedir(), '.claude.json'),
910
+ configPath?: string,
911
+ env?: ProviderEnv,
867
912
  ): ClaudeAttributionAccount {
913
+ const selectedPath = resolveClaudeAttributionConfigPath(configPath, env);
868
914
  let configText: string;
869
915
  try {
870
- configText = readFileSync(configPath, 'utf8');
916
+ configText = readFileSync(selectedPath, 'utf8');
871
917
  } catch (error) {
872
918
  throw new Error(
873
- `Anthropic attribution config ${configPath} could not be read: ${error instanceof Error ? error.message : String(error)}`,
919
+ `Anthropic attribution config ${selectedPath} could not be read: ${error instanceof Error ? error.message : String(error)}`,
874
920
  );
875
921
  }
876
922
  return extractClaudeAttributionAccount(
877
- parseJsonValue(configText, `Anthropic attribution config ${configPath}`),
878
- configPath,
923
+ parseJsonValue(configText, `Anthropic attribution config ${selectedPath}`),
924
+ selectedPath,
879
925
  );
880
926
  }
881
927
 
@@ -1414,6 +1460,10 @@ function isSha256(value: unknown): value is string {
1414
1460
  return typeof value === 'string' && /^[a-f0-9]{64}$/u.test(value);
1415
1461
  }
1416
1462
 
1463
+ function isNonEmptyString(value: unknown): value is string {
1464
+ return typeof value === 'string' && value.trim().length > 0;
1465
+ }
1466
+
1417
1467
  function blockWithoutCacheControl(block: unknown): unknown {
1418
1468
  if (!isPlainObject(block)) return block;
1419
1469
  const output = { ...block };
@@ -1466,31 +1516,82 @@ function parseAnthropicLineageDetails(
1466
1516
  .find((candidate) => candidate.type === ANTHROPIC_LINEAGE_DIAGNOSTIC_TYPE);
1467
1517
  const details = diagnostic?.details;
1468
1518
  if (!isPlainObject(details)) return undefined;
1519
+
1520
+ const sourceModel = details['source_model'];
1521
+ const responseId = details['response_id'];
1522
+ const assistantContentSha256 = details['assistant_content_sha256'];
1523
+ const conversationStaticSha256 = details['conversation_static_sha256'];
1524
+ const requestMessageCount = details['request_message_count'];
1525
+ const requestMessagesSha256 = details['request_messages_sha256'];
1526
+ const cacheProfileSha256 = details['cache_profile_sha256'];
1527
+ const cacheRetention = details['cache_retention'];
1528
+ const compactionBoundarySha256 = details['compaction_boundary_sha256'];
1529
+ const signatureEpochSha256 = details['signature_epoch_sha256'];
1530
+ const signatureEpochInheritsPrior = details['signature_epoch_inherits_prior'];
1531
+ const previousMessageId = details['previous_message_id'];
1469
1532
  if (
1470
1533
  details['schema_version'] !== ANTHROPIC_LINEAGE_SCHEMA ||
1471
1534
  details['projection_version'] !== ANTHROPIC_PROJECTION_VERSION ||
1472
1535
  details['source_provider'] !== 'anthropic' ||
1473
1536
  details['source_api'] !== 'anthropic-messages' ||
1474
- typeof details['source_model'] !== 'string' ||
1475
- typeof details['response_id'] !== 'string' ||
1476
- !isSha256(details['assistant_content_sha256']) ||
1477
- !isSha256(details['conversation_static_sha256']) ||
1478
- !Number.isSafeInteger(details['request_message_count']) ||
1479
- (details['request_message_count'] as number) < 0 ||
1480
- !isSha256(details['request_messages_sha256']) ||
1481
- !isSha256(details['cache_profile_sha256']) ||
1482
- (details['cache_retention'] !== 'none' &&
1483
- details['cache_retention'] !== 'short' &&
1484
- details['cache_retention'] !== 'long') ||
1485
- (details['compaction_boundary_sha256'] !== null &&
1486
- !isSha256(details['compaction_boundary_sha256'])) ||
1487
- !isSha256(details['signature_epoch_sha256']) ||
1488
- typeof details['signature_epoch_inherits_prior'] !== 'boolean' ||
1489
- (details['previous_message_id'] !== null && typeof details['previous_message_id'] !== 'string')
1537
+ !isNonEmptyString(sourceModel) ||
1538
+ !isNonEmptyString(responseId) ||
1539
+ !isSha256(assistantContentSha256) ||
1540
+ !isSha256(conversationStaticSha256) ||
1541
+ typeof requestMessageCount !== 'number' ||
1542
+ !Number.isSafeInteger(requestMessageCount) ||
1543
+ requestMessageCount < 0 ||
1544
+ !isSha256(requestMessagesSha256) ||
1545
+ !isSha256(cacheProfileSha256) ||
1546
+ (cacheRetention !== 'none' && cacheRetention !== 'short' && cacheRetention !== 'long') ||
1547
+ (compactionBoundarySha256 !== null && !isSha256(compactionBoundarySha256)) ||
1548
+ !isSha256(signatureEpochSha256) ||
1549
+ typeof signatureEpochInheritsPrior !== 'boolean' ||
1550
+ (previousMessageId !== null && !isNonEmptyString(previousMessageId))
1490
1551
  ) {
1491
1552
  return undefined;
1492
1553
  }
1493
- return details as unknown as AnthropicLineageDetails;
1554
+
1555
+ return {
1556
+ schema_version: ANTHROPIC_LINEAGE_SCHEMA,
1557
+ projection_version: ANTHROPIC_PROJECTION_VERSION,
1558
+ source_provider: 'anthropic',
1559
+ source_api: 'anthropic-messages',
1560
+ source_model: sourceModel,
1561
+ response_id: responseId,
1562
+ assistant_content_sha256: assistantContentSha256,
1563
+ conversation_static_sha256: conversationStaticSha256,
1564
+ request_message_count: requestMessageCount,
1565
+ request_messages_sha256: requestMessagesSha256,
1566
+ cache_profile_sha256: cacheProfileSha256,
1567
+ cache_retention: cacheRetention,
1568
+ compaction_boundary_sha256: compactionBoundarySha256,
1569
+ signature_epoch_sha256: signatureEpochSha256,
1570
+ signature_epoch_inherits_prior: signatureEpochInheritsPrior,
1571
+ previous_message_id: previousMessageId,
1572
+ };
1573
+ }
1574
+
1575
+ function isSuccessfulLineageStopReason(
1576
+ stopReason: string | undefined,
1577
+ ): stopReason is 'stop' | 'length' | 'toolUse' {
1578
+ return stopReason === 'stop' || stopReason === 'length' || stopReason === 'toolUse';
1579
+ }
1580
+
1581
+ function lineageBindsContainingAssistant(
1582
+ message: Extract<PiMessage, { role: 'assistant' }>,
1583
+ lineage: AnthropicLineageDetails,
1584
+ ): boolean {
1585
+ return (
1586
+ message.provider === lineage.source_provider &&
1587
+ message.api === lineage.source_api &&
1588
+ isNonEmptyString(message.model) &&
1589
+ message.model === lineage.source_model &&
1590
+ isSuccessfulLineageStopReason(message.stopReason) &&
1591
+ isNonEmptyString(message.responseId) &&
1592
+ message.responseId === lineage.response_id &&
1593
+ lineage.assistant_content_sha256 === assistantContentHash(message.content)
1594
+ );
1494
1595
  }
1495
1596
 
1496
1597
  function canTargetReadAnthropicThinking(sourceModel: string, targetModel: string): boolean {
@@ -1529,11 +1630,25 @@ function resolveSignatureEpochPolicy(
1529
1630
  compactionBoundarySha256: string | null,
1530
1631
  ): SignatureEpochPolicy {
1531
1632
  const targetModelId = normalizedAnthropicModelId(model);
1532
- const latest = latestLineageForTarget({ messages }, targetModelId);
1533
- if (latest === undefined) {
1633
+ const latestState = targetLineageState({ messages }, targetModelId);
1634
+ if (latestState.kind === 'none') {
1534
1635
  return initialSignatureEpochPolicy(targetModelId, cacheRetention, compactionBoundarySha256);
1535
1636
  }
1637
+ if (latestState.kind === 'untrusted') {
1638
+ return {
1639
+ sha256: sha256Canonical({
1640
+ projection_version: ANTHROPIC_PROJECTION_VERSION,
1641
+ kind: 'untrusted-lineage-reset',
1642
+ target_model: targetModelId,
1643
+ cache_retention: cacheRetention,
1644
+ compaction_boundary_sha256: compactionBoundarySha256,
1645
+ latest_assistant_sha256: latestState.assistantSha256,
1646
+ }),
1647
+ inheritsPrior: false,
1648
+ };
1649
+ }
1536
1650
 
1651
+ const latest = latestState.lineage;
1537
1652
  const retentionChanged = latest.cache_retention !== cacheRetention;
1538
1653
  const declaredNewCompaction =
1539
1654
  compactionBoundarySha256 !== null &&
@@ -1565,22 +1680,12 @@ function isTrustedReplayableAnthropicAssistant(
1565
1680
  conversationStaticSha256: string,
1566
1681
  wireMessagesBeforeAssistant: readonly JsonObject[],
1567
1682
  ): boolean {
1568
- if (
1569
- message.provider !== 'anthropic' ||
1570
- message.api !== 'anthropic-messages' ||
1571
- typeof message.model !== 'string' ||
1572
- message.stopReason === 'error' ||
1573
- message.stopReason === 'aborted' ||
1574
- !canTargetReadAnthropicThinking(message.model, normalizedAnthropicModelId(targetModel))
1575
- ) {
1576
- return false;
1577
- }
1683
+ if (typeof message.model !== 'string') return false;
1578
1684
  const lineage = parseAnthropicLineageDetails(message);
1579
1685
  if (
1580
1686
  lineage === undefined ||
1581
- lineage.source_model !== message.model ||
1582
- lineage.response_id !== message.responseId ||
1583
- lineage.assistant_content_sha256 !== assistantContentHash(message.content) ||
1687
+ !lineageBindsContainingAssistant(message, lineage) ||
1688
+ !canTargetReadAnthropicThinking(message.model, normalizedAnthropicModelId(targetModel)) ||
1584
1689
  lineage.cache_retention !== targetCacheRetention ||
1585
1690
  lineage.conversation_static_sha256 !== conversationStaticSha256 ||
1586
1691
  lineage.request_message_count !== wireMessagesBeforeAssistant.length ||
@@ -1858,12 +1963,20 @@ export function buildAnthropicRequestParams(
1858
1963
  context: PiStreamContext,
1859
1964
  options?: PiSimpleStreamOptions,
1860
1965
  ): JsonObject {
1966
+ return buildAnthropicRequest(model, context, options).params;
1967
+ }
1968
+
1969
+ function buildAnthropicRequest(
1970
+ model: PiModelLike,
1971
+ context: PiStreamContext,
1972
+ options?: PiSimpleStreamOptions,
1973
+ ): { readonly params: JsonObject; readonly signatureEpoch: SignatureEpochPolicy } {
1861
1974
  const policy = resolveClaudeCodeModelPolicy(model);
1862
1975
  const maxTokens = resolveAnthropicMaxTokens(model);
1863
1976
  const cacheControl = resolveAnthropicCacheControl(model, options);
1864
1977
  const cacheRetention: CacheRetention =
1865
1978
  cacheControl === undefined ? 'none' : cacheControl.ttl === '1h' ? 'long' : 'short';
1866
- const signatureEpoch = resolveSignatureEpochPolicy(
1979
+ let signatureEpoch = resolveSignatureEpochPolicy(
1867
1980
  model,
1868
1981
  context.messages,
1869
1982
  cacheRetention,
@@ -1934,8 +2047,37 @@ export function buildAnthropicRequestParams(
1934
2047
  params['thinking'] = { type: 'disabled' };
1935
2048
  params['temperature'] = options?.temperature ?? 1;
1936
2049
  }
2050
+ const previous = latestLineageForTarget(context, normalizedAnthropicModelId(model));
2051
+ if (
2052
+ previous !== undefined &&
2053
+ signatureEpoch.sha256 === previous.signature_epoch_sha256 &&
2054
+ (previous.cache_profile_sha256 !== cacheProfileHash(model, policy, params, staticSha256) ||
2055
+ !hasLineageRequestPrefix(requestMessagesFromPayload(params), previous))
2056
+ ) {
2057
+ // Resume/reload and profile edits can invalidate a lane. Re-project before transport,
2058
+ // rather than just clearing the response ID while retaining prefix-bound signatures.
2059
+ signatureEpoch = {
2060
+ sha256: sha256Canonical({
2061
+ projection_version: ANTHROPIC_PROJECTION_VERSION,
2062
+ kind: 'lineage-reset',
2063
+ previous_signature_epoch_sha256: previous.signature_epoch_sha256,
2064
+ previous_response_id: previous.response_id,
2065
+ cache_profile_sha256: cacheProfileHash(model, policy, params, staticSha256),
2066
+ request_messages_sha256: promptMessagesHash(requestMessagesFromPayload(params)),
2067
+ }),
2068
+ inheritsPrior: false,
2069
+ };
2070
+ params['messages'] = convertMessages(
2071
+ model,
2072
+ context.messages,
2073
+ staticSha256,
2074
+ cacheRetention,
2075
+ signatureEpoch,
2076
+ cacheControl,
2077
+ );
2078
+ }
1937
2079
  assertCacheControlBreakpointLimit(params);
1938
- return params;
2080
+ return { params, signatureEpoch };
1939
2081
  }
1940
2082
 
1941
2083
  interface PreparedAnthropicLineage {
@@ -2001,24 +2143,68 @@ function cacheRetentionFromPayload(payload: JsonObject): CacheRetention {
2001
2143
  return inspectCacheControls(payload).retention ?? 'none';
2002
2144
  }
2003
2145
 
2004
- function latestLineageForTarget(
2146
+ type TargetLineageState =
2147
+ | { readonly kind: 'none' }
2148
+ | { readonly kind: 'trusted'; readonly lineage: AnthropicLineageDetails }
2149
+ | { readonly kind: 'untrusted'; readonly assistantSha256: string };
2150
+
2151
+ function untrustedAssistantSha256(
2152
+ message: Extract<PiMessage, { role: 'assistant' }>,
2153
+ ): string {
2154
+ return sha256Canonical({
2155
+ provider: message.provider ?? null,
2156
+ api: message.api ?? null,
2157
+ model: message.model ?? null,
2158
+ response_id: message.responseId ?? null,
2159
+ stop_reason: message.stopReason ?? null,
2160
+ assistant_content_sha256: assistantContentHash(message.content),
2161
+ lineage_diagnostics: (message.diagnostics ?? []).filter(
2162
+ (diagnostic) => diagnostic.type === ANTHROPIC_LINEAGE_DIAGNOSTIC_TYPE,
2163
+ ),
2164
+ });
2165
+ }
2166
+
2167
+ function targetLineageState(
2005
2168
  context: PiStreamContext,
2006
2169
  targetModelId: string,
2007
- ): AnthropicLineageDetails | undefined {
2170
+ ): TargetLineageState {
2008
2171
  for (let index = context.messages.length - 1; index >= 0; index -= 1) {
2009
2172
  const message = context.messages[index];
2173
+ if (message?.role !== 'assistant') continue;
2174
+
2175
+ const lineageDiagnostics = (message.diagnostics ?? []).filter(
2176
+ (diagnostic) => diagnostic.type === ANTHROPIC_LINEAGE_DIAGNOSTIC_TYPE,
2177
+ );
2178
+ const latestRawDetails = lineageDiagnostics.at(-1)?.details;
2179
+ const receiptClaimsTarget = latestRawDetails?.['source_model'] === targetModelId;
2180
+ const assistantClaimsTarget = message.model === targetModelId;
2181
+ if (!assistantClaimsTarget && !receiptClaimsTarget) continue;
2182
+
2010
2183
  if (
2011
- message?.role !== 'assistant' ||
2012
- message.model !== targetModelId ||
2013
- message.stopReason === 'error' ||
2014
- message.stopReason === 'aborted'
2184
+ (message.stopReason === 'error' || message.stopReason === 'aborted') &&
2185
+ lineageDiagnostics.length === 0
2015
2186
  ) {
2016
2187
  continue;
2017
2188
  }
2189
+
2018
2190
  const lineage = parseAnthropicLineageDetails(message);
2019
- return lineage?.source_model === targetModelId ? lineage : undefined;
2191
+ if (
2192
+ lineage?.source_model === targetModelId &&
2193
+ lineageBindsContainingAssistant(message, lineage)
2194
+ ) {
2195
+ return { kind: 'trusted', lineage };
2196
+ }
2197
+ return { kind: 'untrusted', assistantSha256: untrustedAssistantSha256(message) };
2020
2198
  }
2021
- return undefined;
2199
+ return { kind: 'none' };
2200
+ }
2201
+
2202
+ function latestLineageForTarget(
2203
+ context: PiStreamContext,
2204
+ targetModelId: string,
2205
+ ): AnthropicLineageDetails | undefined {
2206
+ const state = targetLineageState(context, targetModelId);
2207
+ return state.kind === 'trusted' ? state.lineage : undefined;
2022
2208
  }
2023
2209
 
2024
2210
  function declaredCompactionBoundarySha256(messages: readonly JsonObject[]): string | null {
@@ -2035,11 +2221,23 @@ function declaredCompactionBoundarySha256(messages: readonly JsonObject[]): stri
2035
2221
  return sha256Canonical(promptMessagesWithoutCacheControls([firstMessage])[0]);
2036
2222
  }
2037
2223
 
2224
+ function hasLineageRequestPrefix(
2225
+ messages: readonly JsonObject[],
2226
+ previous: AnthropicLineageDetails,
2227
+ ): boolean {
2228
+ return (
2229
+ messages.length >= previous.request_message_count &&
2230
+ promptMessagesHash(messages.slice(0, previous.request_message_count)) ===
2231
+ previous.request_messages_sha256
2232
+ );
2233
+ }
2234
+
2038
2235
  function prepareAnthropicLineageDetails(args: {
2039
2236
  readonly model: PiModelLike;
2040
2237
  readonly policy: ClaudeCodeModelPolicy;
2041
2238
  readonly context: PiStreamContext;
2042
2239
  readonly payload: JsonObject;
2240
+ readonly signatureEpoch: SignatureEpochPolicy;
2043
2241
  }): PreparedAnthropicLineage['details'] {
2044
2242
  const targetModelId = normalizedAnthropicModelId(args.model);
2045
2243
  const messages = requestMessagesFromPayload(args.payload);
@@ -2050,37 +2248,16 @@ function prepareAnthropicLineageDetails(args: {
2050
2248
  const profileSha256 = cacheProfileHash(args.model, args.policy, args.payload, staticSha256);
2051
2249
  const cacheRetention = cacheRetentionFromPayload(args.payload);
2052
2250
  const compactionBoundarySha256 = declaredCompactionBoundarySha256(messages);
2053
- const signatureEpoch = resolveSignatureEpochPolicy(
2054
- args.model,
2055
- args.context.messages,
2056
- cacheRetention,
2057
- compactionBoundarySha256,
2058
- );
2251
+ const signatureEpoch = args.signatureEpoch;
2059
2252
  let previous = latestLineageForTarget(args.context, targetModelId);
2060
- if (previous !== undefined) {
2061
- const prefixStillExists =
2062
- messages.length >= previous.request_message_count &&
2063
- promptMessagesHash(messages.slice(0, previous.request_message_count)) ===
2064
- previous.request_messages_sha256;
2065
- if (
2066
- !prefixStillExists &&
2067
- compactionBoundarySha256 !== null &&
2068
- compactionBoundarySha256 !== previous.compaction_boundary_sha256
2069
- ) {
2070
- previous = undefined;
2071
- } else {
2072
- if (!prefixStillExists) {
2073
- throw new Error(
2074
- 'Anthropic cache lineage diverged before transport: message history is not append-only',
2075
- );
2076
- }
2077
- if (previous.cache_profile_sha256 !== profileSha256) {
2078
- throw new Error(
2079
- 'Anthropic cache lineage diverged before transport: model/system/tools/thinking/beta profile changed',
2080
- );
2081
- }
2082
- if (previous.cache_retention !== cacheRetention) previous = undefined;
2083
- }
2253
+ if (
2254
+ previous !== undefined &&
2255
+ (signatureEpoch.sha256 !== previous.signature_epoch_sha256 ||
2256
+ !hasLineageRequestPrefix(messages, previous) ||
2257
+ previous.cache_profile_sha256 !== profileSha256 ||
2258
+ previous.cache_retention !== cacheRetention)
2259
+ ) {
2260
+ previous = undefined;
2084
2261
  }
2085
2262
  return {
2086
2263
  schema_version: ANTHROPIC_LINEAGE_SCHEMA,
@@ -2109,6 +2286,7 @@ class AnthropicLineageCoordinator {
2109
2286
  readonly policy: ClaudeCodeModelPolicy;
2110
2287
  readonly context: PiStreamContext;
2111
2288
  readonly payload: JsonObject;
2289
+ readonly signatureEpoch: SignatureEpochPolicy;
2112
2290
  }): PreparedAnthropicLineage {
2113
2291
  const targetModelId = normalizedAnthropicModelId(args.model);
2114
2292
  const key = `${args.sessionId}\u0000${targetModelId}`;
@@ -2136,6 +2314,14 @@ export function createAnthropicLineageDiagnostic(args: {
2136
2314
  readonly requestPayload: JsonObject;
2137
2315
  readonly previousMessageId?: string | null;
2138
2316
  }): PiAssistantDiagnosticLike {
2317
+ if (!isNonEmptyString(args.responseId)) {
2318
+ throw new Error('Anthropic lineage diagnostic requires a non-empty response ID');
2319
+ }
2320
+ if (args.previousMessageId !== undefined && args.previousMessageId !== null) {
2321
+ if (!isNonEmptyString(args.previousMessageId)) {
2322
+ throw new Error('Anthropic lineage diagnostic requires a non-empty previous response ID');
2323
+ }
2324
+ }
2139
2325
  const policy = resolveClaudeCodeModelPolicy(args.model);
2140
2326
  const messages = requestMessagesFromPayload(args.requestPayload);
2141
2327
  const staticSha256 = conversationStaticHash(
@@ -2177,7 +2363,7 @@ function appendLineageDiagnostic(
2177
2363
  output: AssistantMessageLike,
2178
2364
  prepared: PreparedAnthropicLineage,
2179
2365
  ): void {
2180
- if (typeof output.responseId !== 'string' || output.responseId.length === 0) {
2366
+ if (!isNonEmptyString(output.responseId)) {
2181
2367
  throw new Error(
2182
2368
  'Anthropic attribution successful response is missing responseId lineage proof',
2183
2369
  );
@@ -2488,33 +2674,26 @@ function createOutput(model: PiModelLike): AssistantMessageLike {
2488
2674
  };
2489
2675
  }
2490
2676
 
2491
- async function forwardToBuiltInAnthropic(
2677
+ type HostForwardingModel = PiModelLike & HostModel<'anthropic-messages'>;
2678
+ type HostForwardingContext = PiStreamContext & HostContext;
2679
+ type HostForwardingOptions = PiSimpleStreamOptions & HostSimpleStreamOptions;
2680
+ type HostForwardingStream = AssistantMessageEventStreamLike & HostAssistantMessageEventStream;
2681
+
2682
+ function forwardToBuiltInAnthropic(
2492
2683
  model: PiModelLike,
2493
2684
  context: PiStreamContext,
2494
2685
  options: PiSimpleStreamOptions | undefined,
2495
- stream: AssistantMessageEventStreamLike,
2496
- output: AssistantMessageLike,
2497
- ): Promise<void> {
2498
- try {
2499
- const dynamicImport = new Function('specifier', 'return import(specifier)') as (
2500
- specifier: string,
2501
- ) => Promise<{
2502
- streamSimpleAnthropic: (
2503
- model: PiModelLike,
2504
- context: PiStreamContext,
2505
- options?: PiSimpleStreamOptions,
2506
- ) => AssistantMessageEventStreamLike;
2507
- }>;
2508
- const mod = await dynamicImport('@earendil-works/pi-ai/anthropic');
2509
- const delegated = mod.streamSimpleAnthropic(model, context, options);
2510
- for await (const event of delegated) stream.push(event);
2511
- stream.end(await delegated.result());
2512
- } catch (error) {
2513
- output.stopReason = 'error';
2514
- output.errorMessage = `Anthropic attribution could not delegate non-target provider ${JSON.stringify(model.provider)}: ${error instanceof Error ? error.message : String(error)}`;
2515
- stream.push({ type: 'error', reason: 'error', error: output });
2516
- stream.end();
2517
- }
2686
+ ): AssistantMessageEventStreamLike {
2687
+ // Pi aliases these SDK imports to its own version while loading extensions.
2688
+ // The host therefore owns both its legacy Context or normalized TranscriptContext
2689
+ // input and the complete stream/event/result shape. These intersections only mark
2690
+ // that host-owned boundary; no request, callback, event, or result is reconstructed.
2691
+ const delegated = anthropicMessagesApi().streamSimple(
2692
+ model as HostForwardingModel,
2693
+ context as HostForwardingContext,
2694
+ options as HostForwardingOptions | undefined,
2695
+ );
2696
+ return delegated as HostForwardingStream;
2518
2697
  }
2519
2698
 
2520
2699
  export function streamAnthropicViaBetaMessages(
@@ -2523,14 +2702,12 @@ export function streamAnthropicViaBetaMessages(
2523
2702
  options?: PiSimpleStreamOptions,
2524
2703
  dependencies: AnthropicTransportDependencies = {},
2525
2704
  ): AssistantMessageEventStreamLike {
2526
- const stream = createAssistantMessageEventStream();
2527
- const output = createOutput(model);
2528
-
2529
2705
  if (model.provider !== 'anthropic') {
2530
- void forwardToBuiltInAnthropic(model, context, options, stream, output);
2531
- return stream;
2706
+ return forwardToBuiltInAnthropic(model, context, options);
2532
2707
  }
2533
2708
 
2709
+ const stream = createAssistantMessageEventStream();
2710
+ const output = createOutput(model);
2534
2711
  void (async () => {
2535
2712
  let preparedLineage: PreparedAnthropicLineage | undefined;
2536
2713
  try {
@@ -2548,11 +2725,12 @@ export function streamAnthropicViaBetaMessages(
2548
2725
 
2549
2726
  const sessionId = requireSessionId(options?.sessionId, 'options.sessionId');
2550
2727
  const account = requireAttributionAccount(
2551
- (dependencies.loadAccount ?? loadClaudeAttributionAccount)(),
2728
+ dependencies.loadAccount?.() ?? loadClaudeAttributionAccount(undefined, options?.env),
2552
2729
  );
2553
2730
  const url = resolveAnthropicBetaMessagesUrl(model);
2554
2731
  const policy = resolveClaudeCodeModelPolicy(model);
2555
- let params = buildAnthropicRequestParams(model, context, options);
2732
+ const request = buildAnthropicRequest(model, context, options);
2733
+ let params = request.params;
2556
2734
  const billingSystemText = buildClaudeCodeBillingSystemText(
2557
2735
  firstUserMessageTextFromPayload(params),
2558
2736
  );
@@ -2561,6 +2739,7 @@ export function streamAnthropicViaBetaMessages(
2561
2739
  policy,
2562
2740
  context,
2563
2741
  payload: params,
2742
+ signatureEpoch: request.signatureEpoch,
2564
2743
  });
2565
2744
  if (policy.supportsCacheDiagnostics) {
2566
2745
  if (!policy.beta.split(',').includes(ANTHROPIC_CACHE_DIAGNOSTICS_BETA)) {
@@ -2608,6 +2787,7 @@ export function streamAnthropicViaBetaMessages(
2608
2787
  policy,
2609
2788
  context,
2610
2789
  payload: params,
2790
+ signatureEpoch: request.signatureEpoch,
2611
2791
  });
2612
2792
  if (
2613
2793
  preparedLineage.details.previous_message_id !== provisionalLineage.previous_message_id ||
@@ -2693,7 +2873,7 @@ export function streamAnthropicViaBetaMessages(
2693
2873
  'Anthropic beta messages stream emitted malformed/duplicate message_start',
2694
2874
  );
2695
2875
  }
2696
- if (typeof event['message']['id'] !== 'string' || event['message']['id'].length === 0) {
2876
+ if (!isNonEmptyString(event['message']['id'])) {
2697
2877
  throw new Error('Anthropic beta messages message_start is missing a response id');
2698
2878
  }
2699
2879
  sawMessageStart = true;
@@ -0,0 +1,4 @@
1
+ export const ATTESTED_TASK_ID_PATTERN = /^b[0-9a-f]{32}$/;
2
+ export const ATTESTED_GIT_TIMEOUT_MS = 30_000;
3
+ export const ATTESTED_GIT_KILL_GRACE_MS = 250;
4
+ export const ATTESTED_GIT_MAX_OUTPUT_BYTES = 4 * 1024 * 1024;