pi-background-tasks 2.5.0 → 2.6.1

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 (204) hide show
  1. package/BACKGROUND-TASKS-INSTRUCTIONS.md +1 -1
  2. package/PUBLISHING.md +1 -1
  3. package/README.md +70 -25
  4. package/TESTING.md +44 -2
  5. package/TEST_PLAN.md +9 -5
  6. package/dist/extensions/anthropic-attribution-child.js +8 -0
  7. package/dist/extensions/anthropic-attribution-child.js.map +1 -0
  8. package/dist/extensions/anthropic-attribution.js +102 -0
  9. package/dist/extensions/anthropic-attribution.js.map +1 -0
  10. package/dist/extensions/background-tasks.js +2 -0
  11. package/dist/extensions/background-tasks.js.map +1 -0
  12. package/dist/extensions/delegate-child.js +2 -0
  13. package/dist/extensions/delegate-child.js.map +1 -0
  14. package/dist/extensions/fusion-child.js +2 -0
  15. package/dist/extensions/fusion-child.js.map +1 -0
  16. package/dist/package.json +5 -0
  17. package/dist/src/core/anthropic-attribution-path.js +24 -0
  18. package/dist/src/core/anthropic-attribution-path.js.map +1 -0
  19. package/dist/src/core/anthropic-attribution.js +2253 -0
  20. package/dist/src/core/anthropic-attribution.js.map +1 -0
  21. package/dist/src/core/attested-pi-contract.js +5 -0
  22. package/dist/src/core/attested-pi-contract.js.map +1 -0
  23. package/dist/src/core/attested-pi-run.js +749 -0
  24. package/dist/src/core/attested-pi-run.js.map +1 -0
  25. package/dist/src/core/canonical-json.js +19 -0
  26. package/dist/src/core/canonical-json.js.map +1 -0
  27. package/dist/src/core/common.js +787 -0
  28. package/dist/src/core/common.js.map +1 -0
  29. package/dist/src/core/config.js +78 -0
  30. package/dist/src/core/config.js.map +1 -0
  31. package/dist/src/core/context/parent-snapshot.js +75 -0
  32. package/dist/src/core/context/parent-snapshot.js.map +1 -0
  33. package/dist/src/core/context/token-budget.js +631 -0
  34. package/dist/src/core/context/token-budget.js.map +1 -0
  35. package/dist/src/core/context/visible-conversation-v2.js +390 -0
  36. package/dist/src/core/context/visible-conversation-v2.js.map +1 -0
  37. package/dist/src/core/delegate/artifacts.js +357 -0
  38. package/dist/src/core/delegate/artifacts.js.map +1 -0
  39. package/dist/src/core/delegate/budget.js +266 -0
  40. package/dist/src/core/delegate/budget.js.map +1 -0
  41. package/dist/src/core/delegate/facade-contract.js +7 -0
  42. package/dist/src/core/delegate/facade-contract.js.map +1 -0
  43. package/dist/src/core/delegate/hook-contract-evidence.json +18 -0
  44. package/dist/src/core/delegate/hook-contract.js +124 -0
  45. package/dist/src/core/delegate/hook-contract.js.map +1 -0
  46. package/dist/src/core/delegate/launch.js +366 -0
  47. package/dist/src/core/delegate/launch.js.map +1 -0
  48. package/dist/src/core/delegate/result-package.js +293 -0
  49. package/dist/src/core/delegate/result-package.js.map +1 -0
  50. package/dist/src/core/delegate/runner.js +398 -0
  51. package/dist/src/core/delegate/runner.js.map +1 -0
  52. package/dist/src/core/delegate/seed.js +334 -0
  53. package/dist/src/core/delegate/seed.js.map +1 -0
  54. package/dist/src/core/delegate/types.js +103 -0
  55. package/dist/src/core/delegate/types.js.map +1 -0
  56. package/dist/src/core/durable-fs.js +372 -0
  57. package/dist/src/core/durable-fs.js.map +1 -0
  58. package/dist/src/core/extension-api.js +382 -0
  59. package/dist/src/core/extension-api.js.map +1 -0
  60. package/dist/src/core/fusion/artifacts.js +654 -0
  61. package/dist/src/core/fusion/artifacts.js.map +1 -0
  62. package/dist/src/core/fusion/budget.js +949 -0
  63. package/dist/src/core/fusion/budget.js.map +1 -0
  64. package/dist/src/core/fusion/child-protocol.js +176 -0
  65. package/dist/src/core/fusion/child-protocol.js.map +1 -0
  66. package/dist/src/core/fusion/claude-cache.js +156 -0
  67. package/dist/src/core/fusion/claude-cache.js.map +1 -0
  68. package/dist/src/core/fusion/clean-context.js +64 -0
  69. package/dist/src/core/fusion/clean-context.js.map +1 -0
  70. package/dist/src/core/fusion/config.js +349 -0
  71. package/dist/src/core/fusion/config.js.map +1 -0
  72. package/dist/src/core/fusion/context.js +184 -0
  73. package/dist/src/core/fusion/context.js.map +1 -0
  74. package/dist/src/core/fusion/evaluation.js +696 -0
  75. package/dist/src/core/fusion/evaluation.js.map +1 -0
  76. package/dist/src/core/fusion/facade-contract.js +3 -0
  77. package/dist/src/core/fusion/facade-contract.js.map +1 -0
  78. package/dist/src/core/fusion/orchestrator.js +867 -0
  79. package/dist/src/core/fusion/orchestrator.js.map +1 -0
  80. package/dist/src/core/fusion/output-contract.js +27 -0
  81. package/dist/src/core/fusion/output-contract.js.map +1 -0
  82. package/dist/src/core/fusion/pi-child.js +1741 -0
  83. package/dist/src/core/fusion/pi-child.js.map +1 -0
  84. package/dist/src/core/fusion/prompts.js +260 -0
  85. package/dist/src/core/fusion/prompts.js.map +1 -0
  86. package/dist/src/core/fusion/result-package.js +857 -0
  87. package/dist/src/core/fusion/result-package.js.map +1 -0
  88. package/dist/src/core/fusion/source-policy.js +259 -0
  89. package/dist/src/core/fusion/source-policy.js.map +1 -0
  90. package/dist/src/core/fusion/types.js +207 -0
  91. package/dist/src/core/fusion/types.js.map +1 -0
  92. package/dist/src/core/fusion/web-fetch.js +777 -0
  93. package/dist/src/core/fusion/web-fetch.js.map +1 -0
  94. package/dist/src/core/fusion/workflows.js +126 -0
  95. package/dist/src/core/fusion/workflows.js.map +1 -0
  96. package/dist/src/core/lazy-module.js +181 -0
  97. package/dist/src/core/lazy-module.js.map +1 -0
  98. package/dist/src/core/pi-launch.js +431 -0
  99. package/dist/src/core/pi-launch.js.map +1 -0
  100. package/dist/src/core/registry.js +3331 -0
  101. package/dist/src/core/registry.js.map +1 -0
  102. package/dist/src/core/reload-shell-owner.js +1396 -0
  103. package/dist/src/core/reload-shell-owner.js.map +1 -0
  104. package/dist/src/core/shell-policy.js +80 -0
  105. package/dist/src/core/shell-policy.js.map +1 -0
  106. package/dist/src/core/task-durable.js +51 -0
  107. package/dist/src/core/task-durable.js.map +1 -0
  108. package/dist/src/core/update-check.js +92 -0
  109. package/dist/src/core/update-check.js.map +1 -0
  110. package/dist/src/core/windows-taskkill.js +185 -0
  111. package/dist/src/core/windows-taskkill.js.map +1 -0
  112. package/dist/src/delegate-child-extension.js +776 -0
  113. package/dist/src/delegate-child-extension.js.map +1 -0
  114. package/dist/src/delegate-extension.js +746 -0
  115. package/dist/src/delegate-extension.js.map +1 -0
  116. package/dist/src/extension.js +953 -0
  117. package/dist/src/extension.js.map +1 -0
  118. package/dist/src/fusion-child-extension.js +760 -0
  119. package/dist/src/fusion-child-extension.js.map +1 -0
  120. package/dist/src/fusion-extension.js +1030 -0
  121. package/dist/src/fusion-extension.js.map +1 -0
  122. package/dist/src/ui/background-tasks-manager.js +689 -0
  123. package/dist/src/ui/background-tasks-manager.js.map +1 -0
  124. package/dist/src/ui/fusion-model-selector.js +277 -0
  125. package/dist/src/ui/fusion-model-selector.js.map +1 -0
  126. package/docs/INDEX.md +35 -33
  127. package/docs/api/eventbus-v1.md +21 -5
  128. package/docs/choose-a-workflow.md +2 -0
  129. package/docs/commands/bg-clear.md +3 -3
  130. package/docs/commands/bg-update.md +3 -3
  131. package/docs/commands/bg.md +18 -6
  132. package/docs/commands/claude-cache.md +4 -4
  133. package/docs/commands/fusion-models.md +49 -4
  134. package/docs/commands/fusion.md +3 -3
  135. package/docs/commands/jobs.md +3 -3
  136. package/docs/commands/kill.md +3 -3
  137. package/docs/commands/logs.md +3 -3
  138. package/docs/commands/task-manager.md +5 -5
  139. package/docs/concepts/completion-delivery.md +6 -2
  140. package/docs/getting-started.md +7 -1
  141. package/docs/manifest.json +228 -42
  142. package/docs/operations/configuration.md +58 -5
  143. package/docs/operations/releasing.md +5 -3
  144. package/docs/operations/testing.md +22 -0
  145. package/docs/operations/troubleshooting.md +7 -2
  146. package/docs/read-before-edit.md +44 -31
  147. package/docs/reference/runtime-contracts.md +87 -51
  148. package/docs/reference/shortcuts-and-dock.md +28 -14
  149. package/docs/subsystems/anthropic-attribution.md +23 -11
  150. package/docs/subsystems/attested-pi-runs.md +12 -7
  151. package/docs/subsystems/background-task-runtime.md +53 -11
  152. package/docs/subsystems/child-launch-durability-and-safety.md +28 -9
  153. package/docs/subsystems/delegation.md +16 -3
  154. package/docs/subsystems/docs-freshness-gate.md +14 -7
  155. package/docs/subsystems/fusion.md +36 -11
  156. package/docs/subsystems/host-ui-and-telemetry.md +22 -9
  157. package/docs/tools/bg_delegate.md +3 -1
  158. package/docs/tools/bg_kill.md +3 -1
  159. package/docs/tools/bg_logs.md +3 -1
  160. package/docs/tools/bg_result.md +5 -3
  161. package/docs/tools/bg_run.md +31 -5
  162. package/docs/tools/bg_run_pi_attested.md +3 -1
  163. package/docs/tools/bg_status.md +3 -1
  164. package/docs/tools/fusion_investigate.md +3 -1
  165. package/docs/tools/fusion_reason.md +3 -1
  166. package/docs/tools/fusion_research.md +3 -1
  167. package/docs/tools/fusion_validate.md +3 -1
  168. package/extensions/anthropic-attribution-child.ts +10 -0
  169. package/extensions/anthropic-attribution.ts +157 -1
  170. package/package.json +13 -10
  171. package/src/core/anthropic-attribution-path.ts +5 -4
  172. package/src/core/anthropic-attribution.ts +306 -112
  173. package/src/core/attested-pi-contract.ts +4 -0
  174. package/src/core/attested-pi-run.ts +468 -81
  175. package/src/core/canonical-json.ts +21 -0
  176. package/src/core/common.ts +450 -40
  177. package/src/core/config.ts +121 -0
  178. package/src/core/context/visible-conversation-v2.ts +3 -6
  179. package/src/core/delegate/artifacts.ts +13 -9
  180. package/src/core/delegate/budget.ts +13 -6
  181. package/src/core/delegate/facade-contract.ts +6 -0
  182. package/src/core/delegate/result-package.ts +32 -14
  183. package/src/core/delegate/runner.ts +129 -58
  184. package/src/core/delegate/seed.ts +5 -7
  185. package/src/core/durable-fs.ts +178 -30
  186. package/src/core/extension-api.ts +36 -8
  187. package/src/core/fusion/artifacts.ts +17 -16
  188. package/src/core/fusion/clean-context.ts +8 -5
  189. package/src/core/fusion/config.ts +2 -1
  190. package/src/core/fusion/context.ts +2 -6
  191. package/src/core/fusion/facade-contract.ts +2 -0
  192. package/src/core/fusion/orchestrator.ts +3 -3
  193. package/src/core/fusion/prompts.ts +1 -1
  194. package/src/core/fusion/result-package.ts +259 -72
  195. package/src/core/fusion/source-policy.ts +43 -17
  196. package/src/core/lazy-module.ts +215 -0
  197. package/src/core/pi-launch.ts +390 -65
  198. package/src/core/registry.ts +2010 -381
  199. package/src/core/reload-shell-owner.ts +1662 -0
  200. package/src/core/shell-policy.ts +134 -0
  201. package/src/core/task-durable.ts +67 -0
  202. package/src/delegate-extension.ts +404 -144
  203. package/src/extension.ts +387 -114
  204. package/src/fusion-extension.ts +202 -96
@@ -1,7 +1,13 @@
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 type {
6
+ AssistantMessageEventStream as HostAssistantMessageEventStream,
7
+ Context as HostContext,
8
+ Model as HostModel,
9
+ SimpleStreamOptions as HostSimpleStreamOptions,
10
+ } from '@earendil-works/pi-ai';
5
11
 
6
12
  export const CLAUDE_CODE_SESSION_HEADER = 'X-Claude-Code-Session-Id';
7
13
 
@@ -72,6 +78,7 @@ const CLAUDE_AGENT_SDK_SYSTEM_TEXT =
72
78
  const FINGERPRINT_SALT = '59cf53e54c78';
73
79
  const AUDIT_ENV = 'PIPELINE_ANTHROPIC_ATTRIBUTION_AUDIT_PATH';
74
80
  const CACHE_RETENTION_ENV = 'PI_CACHE_RETENTION';
81
+ export const ANTHROPIC_ACCOUNT_CONFIG_PATH_ENV = 'PI_ANTHROPIC_ACCOUNT_CONFIG_PATH';
75
82
  export const ANTHROPIC_CACHE_RETENTION_ENTRY = 'pipeline-anthropic-cache-retention';
76
83
  const ANTHROPIC_CACHE_RETENTION_SCHEMA = 'pipeline.anthropic_cache_retention.v1';
77
84
  export const ANTHROPIC_ATTRIBUTION_CLAIM_CHANNEL = 'pi-anthropic-attribution:claim:v1';
@@ -146,8 +153,11 @@ interface PiModelCostLike extends PiCostRatesLike {
146
153
  export interface PiModelLike {
147
154
  readonly provider?: string;
148
155
  readonly id?: string;
156
+ readonly name?: string;
149
157
  readonly api?: string;
150
158
  readonly baseUrl?: string;
159
+ readonly input?: readonly ('text' | 'image')[];
160
+ readonly contextWindow?: number;
151
161
  readonly maxTokens?: number;
152
162
  readonly reasoning?: boolean;
153
163
  readonly compat?: {
@@ -394,7 +404,11 @@ interface PiAssistantDiagnosticLike {
394
404
  }
395
405
 
396
406
  type PiMessage =
397
- | { readonly role: 'user'; readonly content: string | readonly PiContentBlock[] }
407
+ | {
408
+ readonly role: 'user';
409
+ readonly content: string | readonly PiContentBlock[];
410
+ readonly timestamp?: number;
411
+ }
398
412
  | {
399
413
  readonly role: 'assistant';
400
414
  readonly content: readonly JsonObject[];
@@ -404,6 +418,7 @@ type PiMessage =
404
418
  readonly responseId?: string;
405
419
  readonly stopReason?: string;
406
420
  readonly diagnostics?: readonly PiAssistantDiagnosticLike[];
421
+ readonly timestamp?: number;
407
422
  }
408
423
  | {
409
424
  readonly role: 'toolResult';
@@ -411,6 +426,7 @@ type PiMessage =
411
426
  readonly toolName?: string;
412
427
  readonly content: readonly PiContentBlock[];
413
428
  readonly isError?: boolean;
429
+ readonly timestamp?: number;
414
430
  };
415
431
 
416
432
  export interface PiStreamContext {
@@ -449,8 +465,12 @@ export interface PiSimpleStreamOptions {
449
465
  ) => Promise<void> | void;
450
466
  }
451
467
 
468
+ export type HostAnthropicMessagesApiFactory =
469
+ typeof import('@earendil-works/pi-ai/compat').anthropicMessagesApi;
470
+
452
471
  export interface AnthropicTransportDependencies {
453
472
  readonly loadAccount?: () => ClaudeAttributionAccount;
473
+ readonly hostAnthropicMessagesApi?: HostAnthropicMessagesApiFactory;
454
474
  }
455
475
 
456
476
  export interface AssistantMessageLike {
@@ -465,14 +485,20 @@ export interface AssistantMessageLike {
465
485
  cacheRead: number;
466
486
  cacheWrite: number;
467
487
  cacheWrite1h?: number;
488
+ reasoning?: number;
468
489
  totalTokens: number;
469
490
  cost: { input: number; output: number; cacheRead: number; cacheWrite: number; total: number };
470
491
  };
471
- stopReason: 'stop' | 'length' | 'toolUse' | 'aborted' | 'error';
492
+ stopReason: 'pending' | 'stop' | 'length' | 'toolUse' | 'aborted' | 'error' | 'deferred';
472
493
  timestamp: number;
494
+ responseModel?: string;
473
495
  responseId?: string;
496
+ providerThinkingLevel?: string;
497
+ endTurn?: boolean;
474
498
  diagnostics?: PiAssistantDiagnosticLike[];
499
+ deferred?: unknown;
475
500
  errorMessage?: string;
501
+ rawStopReason?: string;
476
502
  }
477
503
 
478
504
  type AssistantMessageEvent =
@@ -862,20 +888,43 @@ export function extractClaudeAttributionAccount(
862
888
  };
863
889
  }
864
890
 
891
+ export function resolveClaudeAttributionConfigPath(
892
+ explicitConfigPath?: string,
893
+ env?: ProviderEnv,
894
+ homeDirectory = homedir(),
895
+ ): string {
896
+ const configuredPath = providerEnvValue(ANTHROPIC_ACCOUNT_CONFIG_PATH_ENV, env);
897
+ const selectedPath = explicitConfigPath ?? configuredPath ?? join(homeDirectory, '.claude.json');
898
+ const source =
899
+ explicitConfigPath !== undefined
900
+ ? 'explicit configPath'
901
+ : configuredPath !== undefined
902
+ ? ANTHROPIC_ACCOUNT_CONFIG_PATH_ENV
903
+ : 'default ~/.claude.json path';
904
+ if (selectedPath.trim().length === 0 || !isAbsolute(selectedPath)) {
905
+ throw new Error(
906
+ `Anthropic attribution ${source} must be a non-empty absolute file path; got ${JSON.stringify(selectedPath)}`,
907
+ );
908
+ }
909
+ return selectedPath;
910
+ }
911
+
865
912
  export function loadClaudeAttributionAccount(
866
- configPath = join(homedir(), '.claude.json'),
913
+ configPath?: string,
914
+ env?: ProviderEnv,
867
915
  ): ClaudeAttributionAccount {
916
+ const selectedPath = resolveClaudeAttributionConfigPath(configPath, env);
868
917
  let configText: string;
869
918
  try {
870
- configText = readFileSync(configPath, 'utf8');
919
+ configText = readFileSync(selectedPath, 'utf8');
871
920
  } catch (error) {
872
921
  throw new Error(
873
- `Anthropic attribution config ${configPath} could not be read: ${error instanceof Error ? error.message : String(error)}`,
922
+ `Anthropic attribution config ${selectedPath} could not be read: ${error instanceof Error ? error.message : String(error)}`,
874
923
  );
875
924
  }
876
925
  return extractClaudeAttributionAccount(
877
- parseJsonValue(configText, `Anthropic attribution config ${configPath}`),
878
- configPath,
926
+ parseJsonValue(configText, `Anthropic attribution config ${selectedPath}`),
927
+ selectedPath,
879
928
  );
880
929
  }
881
930
 
@@ -1414,6 +1463,10 @@ function isSha256(value: unknown): value is string {
1414
1463
  return typeof value === 'string' && /^[a-f0-9]{64}$/u.test(value);
1415
1464
  }
1416
1465
 
1466
+ function isNonEmptyString(value: unknown): value is string {
1467
+ return typeof value === 'string' && value.trim().length > 0;
1468
+ }
1469
+
1417
1470
  function blockWithoutCacheControl(block: unknown): unknown {
1418
1471
  if (!isPlainObject(block)) return block;
1419
1472
  const output = { ...block };
@@ -1466,31 +1519,82 @@ function parseAnthropicLineageDetails(
1466
1519
  .find((candidate) => candidate.type === ANTHROPIC_LINEAGE_DIAGNOSTIC_TYPE);
1467
1520
  const details = diagnostic?.details;
1468
1521
  if (!isPlainObject(details)) return undefined;
1522
+
1523
+ const sourceModel = details['source_model'];
1524
+ const responseId = details['response_id'];
1525
+ const assistantContentSha256 = details['assistant_content_sha256'];
1526
+ const conversationStaticSha256 = details['conversation_static_sha256'];
1527
+ const requestMessageCount = details['request_message_count'];
1528
+ const requestMessagesSha256 = details['request_messages_sha256'];
1529
+ const cacheProfileSha256 = details['cache_profile_sha256'];
1530
+ const cacheRetention = details['cache_retention'];
1531
+ const compactionBoundarySha256 = details['compaction_boundary_sha256'];
1532
+ const signatureEpochSha256 = details['signature_epoch_sha256'];
1533
+ const signatureEpochInheritsPrior = details['signature_epoch_inherits_prior'];
1534
+ const previousMessageId = details['previous_message_id'];
1469
1535
  if (
1470
1536
  details['schema_version'] !== ANTHROPIC_LINEAGE_SCHEMA ||
1471
1537
  details['projection_version'] !== ANTHROPIC_PROJECTION_VERSION ||
1472
1538
  details['source_provider'] !== 'anthropic' ||
1473
1539
  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')
1540
+ !isNonEmptyString(sourceModel) ||
1541
+ !isNonEmptyString(responseId) ||
1542
+ !isSha256(assistantContentSha256) ||
1543
+ !isSha256(conversationStaticSha256) ||
1544
+ typeof requestMessageCount !== 'number' ||
1545
+ !Number.isSafeInteger(requestMessageCount) ||
1546
+ requestMessageCount < 0 ||
1547
+ !isSha256(requestMessagesSha256) ||
1548
+ !isSha256(cacheProfileSha256) ||
1549
+ (cacheRetention !== 'none' && cacheRetention !== 'short' && cacheRetention !== 'long') ||
1550
+ (compactionBoundarySha256 !== null && !isSha256(compactionBoundarySha256)) ||
1551
+ !isSha256(signatureEpochSha256) ||
1552
+ typeof signatureEpochInheritsPrior !== 'boolean' ||
1553
+ (previousMessageId !== null && !isNonEmptyString(previousMessageId))
1490
1554
  ) {
1491
1555
  return undefined;
1492
1556
  }
1493
- return details as unknown as AnthropicLineageDetails;
1557
+
1558
+ return {
1559
+ schema_version: ANTHROPIC_LINEAGE_SCHEMA,
1560
+ projection_version: ANTHROPIC_PROJECTION_VERSION,
1561
+ source_provider: 'anthropic',
1562
+ source_api: 'anthropic-messages',
1563
+ source_model: sourceModel,
1564
+ response_id: responseId,
1565
+ assistant_content_sha256: assistantContentSha256,
1566
+ conversation_static_sha256: conversationStaticSha256,
1567
+ request_message_count: requestMessageCount,
1568
+ request_messages_sha256: requestMessagesSha256,
1569
+ cache_profile_sha256: cacheProfileSha256,
1570
+ cache_retention: cacheRetention,
1571
+ compaction_boundary_sha256: compactionBoundarySha256,
1572
+ signature_epoch_sha256: signatureEpochSha256,
1573
+ signature_epoch_inherits_prior: signatureEpochInheritsPrior,
1574
+ previous_message_id: previousMessageId,
1575
+ };
1576
+ }
1577
+
1578
+ function isSuccessfulLineageStopReason(
1579
+ stopReason: string | undefined,
1580
+ ): stopReason is 'stop' | 'length' | 'toolUse' {
1581
+ return stopReason === 'stop' || stopReason === 'length' || stopReason === 'toolUse';
1582
+ }
1583
+
1584
+ function lineageBindsContainingAssistant(
1585
+ message: Extract<PiMessage, { role: 'assistant' }>,
1586
+ lineage: AnthropicLineageDetails,
1587
+ ): boolean {
1588
+ return (
1589
+ message.provider === lineage.source_provider &&
1590
+ message.api === lineage.source_api &&
1591
+ isNonEmptyString(message.model) &&
1592
+ message.model === lineage.source_model &&
1593
+ isSuccessfulLineageStopReason(message.stopReason) &&
1594
+ isNonEmptyString(message.responseId) &&
1595
+ message.responseId === lineage.response_id &&
1596
+ lineage.assistant_content_sha256 === assistantContentHash(message.content)
1597
+ );
1494
1598
  }
1495
1599
 
1496
1600
  function canTargetReadAnthropicThinking(sourceModel: string, targetModel: string): boolean {
@@ -1529,11 +1633,25 @@ function resolveSignatureEpochPolicy(
1529
1633
  compactionBoundarySha256: string | null,
1530
1634
  ): SignatureEpochPolicy {
1531
1635
  const targetModelId = normalizedAnthropicModelId(model);
1532
- const latest = latestLineageForTarget({ messages }, targetModelId);
1533
- if (latest === undefined) {
1636
+ const latestState = targetLineageState({ messages }, targetModelId);
1637
+ if (latestState.kind === 'none') {
1534
1638
  return initialSignatureEpochPolicy(targetModelId, cacheRetention, compactionBoundarySha256);
1535
1639
  }
1640
+ if (latestState.kind === 'untrusted') {
1641
+ return {
1642
+ sha256: sha256Canonical({
1643
+ projection_version: ANTHROPIC_PROJECTION_VERSION,
1644
+ kind: 'untrusted-lineage-reset',
1645
+ target_model: targetModelId,
1646
+ cache_retention: cacheRetention,
1647
+ compaction_boundary_sha256: compactionBoundarySha256,
1648
+ latest_assistant_sha256: latestState.assistantSha256,
1649
+ }),
1650
+ inheritsPrior: false,
1651
+ };
1652
+ }
1536
1653
 
1654
+ const latest = latestState.lineage;
1537
1655
  const retentionChanged = latest.cache_retention !== cacheRetention;
1538
1656
  const declaredNewCompaction =
1539
1657
  compactionBoundarySha256 !== null &&
@@ -1565,22 +1683,12 @@ function isTrustedReplayableAnthropicAssistant(
1565
1683
  conversationStaticSha256: string,
1566
1684
  wireMessagesBeforeAssistant: readonly JsonObject[],
1567
1685
  ): 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
- }
1686
+ if (typeof message.model !== 'string') return false;
1578
1687
  const lineage = parseAnthropicLineageDetails(message);
1579
1688
  if (
1580
1689
  lineage === undefined ||
1581
- lineage.source_model !== message.model ||
1582
- lineage.response_id !== message.responseId ||
1583
- lineage.assistant_content_sha256 !== assistantContentHash(message.content) ||
1690
+ !lineageBindsContainingAssistant(message, lineage) ||
1691
+ !canTargetReadAnthropicThinking(message.model, normalizedAnthropicModelId(targetModel)) ||
1584
1692
  lineage.cache_retention !== targetCacheRetention ||
1585
1693
  lineage.conversation_static_sha256 !== conversationStaticSha256 ||
1586
1694
  lineage.request_message_count !== wireMessagesBeforeAssistant.length ||
@@ -1858,12 +1966,20 @@ export function buildAnthropicRequestParams(
1858
1966
  context: PiStreamContext,
1859
1967
  options?: PiSimpleStreamOptions,
1860
1968
  ): JsonObject {
1969
+ return buildAnthropicRequest(model, context, options).params;
1970
+ }
1971
+
1972
+ function buildAnthropicRequest(
1973
+ model: PiModelLike,
1974
+ context: PiStreamContext,
1975
+ options?: PiSimpleStreamOptions,
1976
+ ): { readonly params: JsonObject; readonly signatureEpoch: SignatureEpochPolicy } {
1861
1977
  const policy = resolveClaudeCodeModelPolicy(model);
1862
1978
  const maxTokens = resolveAnthropicMaxTokens(model);
1863
1979
  const cacheControl = resolveAnthropicCacheControl(model, options);
1864
1980
  const cacheRetention: CacheRetention =
1865
1981
  cacheControl === undefined ? 'none' : cacheControl.ttl === '1h' ? 'long' : 'short';
1866
- const signatureEpoch = resolveSignatureEpochPolicy(
1982
+ let signatureEpoch = resolveSignatureEpochPolicy(
1867
1983
  model,
1868
1984
  context.messages,
1869
1985
  cacheRetention,
@@ -1934,8 +2050,37 @@ export function buildAnthropicRequestParams(
1934
2050
  params['thinking'] = { type: 'disabled' };
1935
2051
  params['temperature'] = options?.temperature ?? 1;
1936
2052
  }
2053
+ const previous = latestLineageForTarget(context, normalizedAnthropicModelId(model));
2054
+ if (
2055
+ previous !== undefined &&
2056
+ signatureEpoch.sha256 === previous.signature_epoch_sha256 &&
2057
+ (previous.cache_profile_sha256 !== cacheProfileHash(model, policy, params, staticSha256) ||
2058
+ !hasLineageRequestPrefix(requestMessagesFromPayload(params), previous))
2059
+ ) {
2060
+ // Resume/reload and profile edits can invalidate a lane. Re-project before transport,
2061
+ // rather than just clearing the response ID while retaining prefix-bound signatures.
2062
+ signatureEpoch = {
2063
+ sha256: sha256Canonical({
2064
+ projection_version: ANTHROPIC_PROJECTION_VERSION,
2065
+ kind: 'lineage-reset',
2066
+ previous_signature_epoch_sha256: previous.signature_epoch_sha256,
2067
+ previous_response_id: previous.response_id,
2068
+ cache_profile_sha256: cacheProfileHash(model, policy, params, staticSha256),
2069
+ request_messages_sha256: promptMessagesHash(requestMessagesFromPayload(params)),
2070
+ }),
2071
+ inheritsPrior: false,
2072
+ };
2073
+ params['messages'] = convertMessages(
2074
+ model,
2075
+ context.messages,
2076
+ staticSha256,
2077
+ cacheRetention,
2078
+ signatureEpoch,
2079
+ cacheControl,
2080
+ );
2081
+ }
1937
2082
  assertCacheControlBreakpointLimit(params);
1938
- return params;
2083
+ return { params, signatureEpoch };
1939
2084
  }
1940
2085
 
1941
2086
  interface PreparedAnthropicLineage {
@@ -2001,24 +2146,68 @@ function cacheRetentionFromPayload(payload: JsonObject): CacheRetention {
2001
2146
  return inspectCacheControls(payload).retention ?? 'none';
2002
2147
  }
2003
2148
 
2004
- function latestLineageForTarget(
2149
+ type TargetLineageState =
2150
+ | { readonly kind: 'none' }
2151
+ | { readonly kind: 'trusted'; readonly lineage: AnthropicLineageDetails }
2152
+ | { readonly kind: 'untrusted'; readonly assistantSha256: string };
2153
+
2154
+ function untrustedAssistantSha256(
2155
+ message: Extract<PiMessage, { role: 'assistant' }>,
2156
+ ): string {
2157
+ return sha256Canonical({
2158
+ provider: message.provider ?? null,
2159
+ api: message.api ?? null,
2160
+ model: message.model ?? null,
2161
+ response_id: message.responseId ?? null,
2162
+ stop_reason: message.stopReason ?? null,
2163
+ assistant_content_sha256: assistantContentHash(message.content),
2164
+ lineage_diagnostics: (message.diagnostics ?? []).filter(
2165
+ (diagnostic) => diagnostic.type === ANTHROPIC_LINEAGE_DIAGNOSTIC_TYPE,
2166
+ ),
2167
+ });
2168
+ }
2169
+
2170
+ function targetLineageState(
2005
2171
  context: PiStreamContext,
2006
2172
  targetModelId: string,
2007
- ): AnthropicLineageDetails | undefined {
2173
+ ): TargetLineageState {
2008
2174
  for (let index = context.messages.length - 1; index >= 0; index -= 1) {
2009
2175
  const message = context.messages[index];
2176
+ if (message?.role !== 'assistant') continue;
2177
+
2178
+ const lineageDiagnostics = (message.diagnostics ?? []).filter(
2179
+ (diagnostic) => diagnostic.type === ANTHROPIC_LINEAGE_DIAGNOSTIC_TYPE,
2180
+ );
2181
+ const latestRawDetails = lineageDiagnostics.at(-1)?.details;
2182
+ const receiptClaimsTarget = latestRawDetails?.['source_model'] === targetModelId;
2183
+ const assistantClaimsTarget = message.model === targetModelId;
2184
+ if (!assistantClaimsTarget && !receiptClaimsTarget) continue;
2185
+
2010
2186
  if (
2011
- message?.role !== 'assistant' ||
2012
- message.model !== targetModelId ||
2013
- message.stopReason === 'error' ||
2014
- message.stopReason === 'aborted'
2187
+ (message.stopReason === 'error' || message.stopReason === 'aborted') &&
2188
+ lineageDiagnostics.length === 0
2015
2189
  ) {
2016
2190
  continue;
2017
2191
  }
2192
+
2018
2193
  const lineage = parseAnthropicLineageDetails(message);
2019
- return lineage?.source_model === targetModelId ? lineage : undefined;
2194
+ if (
2195
+ lineage?.source_model === targetModelId &&
2196
+ lineageBindsContainingAssistant(message, lineage)
2197
+ ) {
2198
+ return { kind: 'trusted', lineage };
2199
+ }
2200
+ return { kind: 'untrusted', assistantSha256: untrustedAssistantSha256(message) };
2020
2201
  }
2021
- return undefined;
2202
+ return { kind: 'none' };
2203
+ }
2204
+
2205
+ function latestLineageForTarget(
2206
+ context: PiStreamContext,
2207
+ targetModelId: string,
2208
+ ): AnthropicLineageDetails | undefined {
2209
+ const state = targetLineageState(context, targetModelId);
2210
+ return state.kind === 'trusted' ? state.lineage : undefined;
2022
2211
  }
2023
2212
 
2024
2213
  function declaredCompactionBoundarySha256(messages: readonly JsonObject[]): string | null {
@@ -2035,11 +2224,23 @@ function declaredCompactionBoundarySha256(messages: readonly JsonObject[]): stri
2035
2224
  return sha256Canonical(promptMessagesWithoutCacheControls([firstMessage])[0]);
2036
2225
  }
2037
2226
 
2227
+ function hasLineageRequestPrefix(
2228
+ messages: readonly JsonObject[],
2229
+ previous: AnthropicLineageDetails,
2230
+ ): boolean {
2231
+ return (
2232
+ messages.length >= previous.request_message_count &&
2233
+ promptMessagesHash(messages.slice(0, previous.request_message_count)) ===
2234
+ previous.request_messages_sha256
2235
+ );
2236
+ }
2237
+
2038
2238
  function prepareAnthropicLineageDetails(args: {
2039
2239
  readonly model: PiModelLike;
2040
2240
  readonly policy: ClaudeCodeModelPolicy;
2041
2241
  readonly context: PiStreamContext;
2042
2242
  readonly payload: JsonObject;
2243
+ readonly signatureEpoch: SignatureEpochPolicy;
2043
2244
  }): PreparedAnthropicLineage['details'] {
2044
2245
  const targetModelId = normalizedAnthropicModelId(args.model);
2045
2246
  const messages = requestMessagesFromPayload(args.payload);
@@ -2050,37 +2251,16 @@ function prepareAnthropicLineageDetails(args: {
2050
2251
  const profileSha256 = cacheProfileHash(args.model, args.policy, args.payload, staticSha256);
2051
2252
  const cacheRetention = cacheRetentionFromPayload(args.payload);
2052
2253
  const compactionBoundarySha256 = declaredCompactionBoundarySha256(messages);
2053
- const signatureEpoch = resolveSignatureEpochPolicy(
2054
- args.model,
2055
- args.context.messages,
2056
- cacheRetention,
2057
- compactionBoundarySha256,
2058
- );
2254
+ const signatureEpoch = args.signatureEpoch;
2059
2255
  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
- }
2256
+ if (
2257
+ previous !== undefined &&
2258
+ (signatureEpoch.sha256 !== previous.signature_epoch_sha256 ||
2259
+ !hasLineageRequestPrefix(messages, previous) ||
2260
+ previous.cache_profile_sha256 !== profileSha256 ||
2261
+ previous.cache_retention !== cacheRetention)
2262
+ ) {
2263
+ previous = undefined;
2084
2264
  }
2085
2265
  return {
2086
2266
  schema_version: ANTHROPIC_LINEAGE_SCHEMA,
@@ -2109,6 +2289,7 @@ class AnthropicLineageCoordinator {
2109
2289
  readonly policy: ClaudeCodeModelPolicy;
2110
2290
  readonly context: PiStreamContext;
2111
2291
  readonly payload: JsonObject;
2292
+ readonly signatureEpoch: SignatureEpochPolicy;
2112
2293
  }): PreparedAnthropicLineage {
2113
2294
  const targetModelId = normalizedAnthropicModelId(args.model);
2114
2295
  const key = `${args.sessionId}\u0000${targetModelId}`;
@@ -2136,6 +2317,14 @@ export function createAnthropicLineageDiagnostic(args: {
2136
2317
  readonly requestPayload: JsonObject;
2137
2318
  readonly previousMessageId?: string | null;
2138
2319
  }): PiAssistantDiagnosticLike {
2320
+ if (!isNonEmptyString(args.responseId)) {
2321
+ throw new Error('Anthropic lineage diagnostic requires a non-empty response ID');
2322
+ }
2323
+ if (args.previousMessageId !== undefined && args.previousMessageId !== null) {
2324
+ if (!isNonEmptyString(args.previousMessageId)) {
2325
+ throw new Error('Anthropic lineage diagnostic requires a non-empty previous response ID');
2326
+ }
2327
+ }
2139
2328
  const policy = resolveClaudeCodeModelPolicy(args.model);
2140
2329
  const messages = requestMessagesFromPayload(args.requestPayload);
2141
2330
  const staticSha256 = conversationStaticHash(
@@ -2177,7 +2366,7 @@ function appendLineageDiagnostic(
2177
2366
  output: AssistantMessageLike,
2178
2367
  prepared: PreparedAnthropicLineage,
2179
2368
  ): void {
2180
- if (typeof output.responseId !== 'string' || output.responseId.length === 0) {
2369
+ if (!isNonEmptyString(output.responseId)) {
2181
2370
  throw new Error(
2182
2371
  'Anthropic attribution successful response is missing responseId lineage proof',
2183
2372
  );
@@ -2488,33 +2677,37 @@ function createOutput(model: PiModelLike): AssistantMessageLike {
2488
2677
  };
2489
2678
  }
2490
2679
 
2491
- async function forwardToBuiltInAnthropic(
2680
+ type HostForwardingModel = PiModelLike & HostModel<'anthropic-messages'>;
2681
+ type HostForwardingContext = PiStreamContext & HostContext;
2682
+ type HostForwardingOptions = PiSimpleStreamOptions & HostSimpleStreamOptions;
2683
+ type HostForwardingStream = AssistantMessageEventStreamLike & HostAssistantMessageEventStream;
2684
+
2685
+ function forwardToBuiltInAnthropic(
2492
2686
  model: PiModelLike,
2493
2687
  context: PiStreamContext,
2494
2688
  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();
2689
+ dependencies: AnthropicTransportDependencies,
2690
+ ): AssistantMessageEventStreamLike {
2691
+ // The compiled gateway resolves this host-owned adapter through Pi's alias-aware
2692
+ // extension loader and injects it across the lazy native-import boundary. Keeping
2693
+ // the deferred core free of runtime Pi package imports prevents Node from trying to
2694
+ // resolve a private @earendil-works/pi-ai installation beside a managed package.
2695
+ const hostAnthropicMessagesApi = dependencies.hostAnthropicMessagesApi;
2696
+ if (hostAnthropicMessagesApi === undefined) {
2697
+ throw new Error(
2698
+ "pi_anthropic_attribution_host_adapter_missing: the extension gateway did not inject Pi's anthropic-messages adapter",
2699
+ );
2517
2700
  }
2701
+
2702
+ // The host owns both its legacy Context or normalized TranscriptContext input and
2703
+ // the complete stream/event/result shape. These intersections only mark that
2704
+ // host-owned boundary; no request, callback, event, or result is reconstructed.
2705
+ const delegated = hostAnthropicMessagesApi().streamSimple(
2706
+ model as HostForwardingModel,
2707
+ context as HostForwardingContext,
2708
+ options as HostForwardingOptions | undefined,
2709
+ );
2710
+ return delegated as HostForwardingStream;
2518
2711
  }
2519
2712
 
2520
2713
  export function streamAnthropicViaBetaMessages(
@@ -2523,14 +2716,12 @@ export function streamAnthropicViaBetaMessages(
2523
2716
  options?: PiSimpleStreamOptions,
2524
2717
  dependencies: AnthropicTransportDependencies = {},
2525
2718
  ): AssistantMessageEventStreamLike {
2526
- const stream = createAssistantMessageEventStream();
2527
- const output = createOutput(model);
2528
-
2529
2719
  if (model.provider !== 'anthropic') {
2530
- void forwardToBuiltInAnthropic(model, context, options, stream, output);
2531
- return stream;
2720
+ return forwardToBuiltInAnthropic(model, context, options, dependencies);
2532
2721
  }
2533
2722
 
2723
+ const stream = createAssistantMessageEventStream();
2724
+ const output = createOutput(model);
2534
2725
  void (async () => {
2535
2726
  let preparedLineage: PreparedAnthropicLineage | undefined;
2536
2727
  try {
@@ -2548,11 +2739,12 @@ export function streamAnthropicViaBetaMessages(
2548
2739
 
2549
2740
  const sessionId = requireSessionId(options?.sessionId, 'options.sessionId');
2550
2741
  const account = requireAttributionAccount(
2551
- (dependencies.loadAccount ?? loadClaudeAttributionAccount)(),
2742
+ dependencies.loadAccount?.() ?? loadClaudeAttributionAccount(undefined, options?.env),
2552
2743
  );
2553
2744
  const url = resolveAnthropicBetaMessagesUrl(model);
2554
2745
  const policy = resolveClaudeCodeModelPolicy(model);
2555
- let params = buildAnthropicRequestParams(model, context, options);
2746
+ const request = buildAnthropicRequest(model, context, options);
2747
+ let params = request.params;
2556
2748
  const billingSystemText = buildClaudeCodeBillingSystemText(
2557
2749
  firstUserMessageTextFromPayload(params),
2558
2750
  );
@@ -2561,6 +2753,7 @@ export function streamAnthropicViaBetaMessages(
2561
2753
  policy,
2562
2754
  context,
2563
2755
  payload: params,
2756
+ signatureEpoch: request.signatureEpoch,
2564
2757
  });
2565
2758
  if (policy.supportsCacheDiagnostics) {
2566
2759
  if (!policy.beta.split(',').includes(ANTHROPIC_CACHE_DIAGNOSTICS_BETA)) {
@@ -2608,6 +2801,7 @@ export function streamAnthropicViaBetaMessages(
2608
2801
  policy,
2609
2802
  context,
2610
2803
  payload: params,
2804
+ signatureEpoch: request.signatureEpoch,
2611
2805
  });
2612
2806
  if (
2613
2807
  preparedLineage.details.previous_message_id !== provisionalLineage.previous_message_id ||
@@ -2693,7 +2887,7 @@ export function streamAnthropicViaBetaMessages(
2693
2887
  'Anthropic beta messages stream emitted malformed/duplicate message_start',
2694
2888
  );
2695
2889
  }
2696
- if (typeof event['message']['id'] !== 'string' || event['message']['id'].length === 0) {
2890
+ if (!isNonEmptyString(event['message']['id'])) {
2697
2891
  throw new Error('Anthropic beta messages message_start is missing a response id');
2698
2892
  }
2699
2893
  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;