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
@@ -0,0 +1,654 @@
1
+ import { createHash, randomBytes } from 'node:crypto';
2
+ import { chmod, mkdir } from 'node:fs/promises';
3
+ import { basename, isAbsolute, join, relative, sep } from 'node:path';
4
+ import { canonicalJson, sha256Buffer } from '../canonical-json.js';
5
+ import { sanitizePathSegment } from '../common.js';
6
+ import { replaceFileDurable } from '../durable-fs.js';
7
+ import { EMPTY_FUSION_USAGE, FUSION_COMMITTED_RESULT_SCHEMA_VERSION, FUSION_FAILURE_SUMMARY_SCHEMA_VERSION, FUSION_MANIFEST_SCHEMA_VERSION, FUSION_VALIDATE_CANDIDATE_CONTRACT_EVENT_SCHEMA_VERSION, FusionError, cloneFusionUsage, } from './types.js';
8
+ import { fusionWorkflowProfile } from './workflows.js';
9
+ import { FUSION_CANDIDATE_MAX_OUTPUT_BYTES, fusionJsonRenderedTextBytes, } from './output-contract.js';
10
+ /**
11
+ * Run ids are prefixed by workflow so an artifact directory is self-describing.
12
+ * The prefix set is closed: an unknown prefix must fail rather than be accepted.
13
+ */
14
+ const RUN_ID_PATTERN = /^(reason|investigate|research|validate)-[0-9a-f]{32}$/;
15
+ function makeRunId(profile) {
16
+ return `${profile.runIdPrefix}${randomBytes(16).toString('hex')}`;
17
+ }
18
+ function modelsForManifest(models) {
19
+ const first = models.candidates[0].qualifiedId;
20
+ const second = models.candidates[1].qualifiedId;
21
+ const third = models.candidates[2].qualifiedId;
22
+ return {
23
+ candidates: [first, second, third],
24
+ evaluator: models.evaluator.qualifiedId,
25
+ merger: models.merger.qualifiedId,
26
+ thinking_level: models.evaluator.thinkingLevel,
27
+ };
28
+ }
29
+ function terminalStates() {
30
+ return new Set(['completed', 'failed', 'cancelled']);
31
+ }
32
+ const TERMINAL_STATES = terminalStates();
33
+ const NEXT_STATES = {
34
+ initializing: ['candidates_running', 'failed', 'cancelled'],
35
+ candidates_running: ['candidates_complete', 'failed', 'cancelled'],
36
+ candidates_complete: ['evaluating', 'failed', 'cancelled'],
37
+ evaluating: ['evaluation_complete', 'failed', 'cancelled'],
38
+ evaluation_complete: ['merging', 'failed', 'cancelled'],
39
+ merging: ['completed', 'failed', 'cancelled'],
40
+ completed: [],
41
+ failed: [],
42
+ cancelled: [],
43
+ };
44
+ function canTransition(from, to) {
45
+ return NEXT_STATES[from].includes(to);
46
+ }
47
+ function pathInside(parent, child) {
48
+ const rel = relative(parent, child);
49
+ return (rel === '' || (!rel.startsWith('..') && !isAbsolute(rel) && !rel.split(sep).includes('..')));
50
+ }
51
+ function errorForArtifact(message) {
52
+ return new FusionError(message, { code: 'artifact_error', childCreated: false });
53
+ }
54
+ async function writePrivateFile(absPath, data) {
55
+ const bytes = Buffer.isBuffer(data) ? data : Buffer.from(data, 'utf8');
56
+ await replaceFileDurable(absPath, data);
57
+ return { path: basename(absPath), byte_length: bytes.length, sha256: sha256Buffer(bytes) };
58
+ }
59
+ async function writeJsonAtomic(absPath, value) {
60
+ return writePrivateFile(absPath, `${JSON.stringify(value, null, 2)}\n`);
61
+ }
62
+ function publicManifest(manifest) {
63
+ const out = {
64
+ schema_version: manifest.schema_version,
65
+ run_id: manifest.run_id,
66
+ workflow: manifest.workflow,
67
+ source: manifest.source,
68
+ state: manifest.state,
69
+ created_at: manifest.created_at,
70
+ updated_at: manifest.updated_at,
71
+ cwd: manifest.cwd,
72
+ config: manifest.config,
73
+ models: manifest.models,
74
+ capabilities: manifest.capabilities,
75
+ context: { ...manifest.context },
76
+ tool_policy: {
77
+ candidate_tools: [...manifest.tool_policy.candidate_tools],
78
+ evaluation_tools: [],
79
+ merge_tools: [],
80
+ },
81
+ usage: cloneFusionUsage(manifest.usage),
82
+ attempts: [...manifest.attempts],
83
+ artifacts: { ...manifest.artifacts },
84
+ };
85
+ if (manifest.anonymous_map !== undefined)
86
+ out.anonymous_map = { ...manifest.anonymous_map };
87
+ if (manifest.error !== undefined)
88
+ out.error = manifest.error;
89
+ return out;
90
+ }
91
+ function attemptPrefix(stage, slot, attempt) {
92
+ if (stage === 'candidate') {
93
+ if (slot === undefined)
94
+ throw errorForArtifact('candidate attempt requires slot');
95
+ return `candidate-${String(slot)}.attempt-${String(attempt)}`;
96
+ }
97
+ if (slot !== undefined)
98
+ throw errorForArtifact(`${stage} attempt must not include candidate slot`);
99
+ return `${stage === 'evaluation' ? 'evaluation' : 'merge'}.attempt-${String(attempt)}`;
100
+ }
101
+ function responseName(prefix, kind) {
102
+ return `${prefix}.response.${kind}`;
103
+ }
104
+ function calibrationViolationName(prefix) {
105
+ return `${prefix}.calibration-violation.json`;
106
+ }
107
+ function oversizedResponseName(prefix, kind) {
108
+ return `${prefix}.response.oversized.${kind}`;
109
+ }
110
+ function validateOutputRecovery(recovery, replacementText) {
111
+ if (recovery.limit_bytes !== FUSION_CANDIDATE_MAX_OUTPUT_BYTES) {
112
+ throw errorForArtifact('fusion output recovery limit mismatches the candidate contract');
113
+ }
114
+ const originalBytes = Buffer.from(recovery.original_text, 'utf8');
115
+ if (createHash('sha256').update(originalBytes).digest('hex') !== recovery.original_text_sha256) {
116
+ throw errorForArtifact('fusion output recovery original text hash mismatch');
117
+ }
118
+ if (fusionJsonRenderedTextBytes(recovery.original_text) !== recovery.original_json_rendered_bytes) {
119
+ throw errorForArtifact('fusion output recovery original JSON-rendered byte count mismatch');
120
+ }
121
+ if (recovery.original_json_rendered_bytes <= recovery.limit_bytes) {
122
+ throw errorForArtifact('fusion output recovery original did not exceed the candidate contract');
123
+ }
124
+ if (replacementText !== undefined) {
125
+ const replacementBytes = fusionJsonRenderedTextBytes(replacementText);
126
+ if (replacementBytes !== recovery.replacement_json_rendered_bytes) {
127
+ throw errorForArtifact('fusion output recovery replacement JSON-rendered byte count mismatch');
128
+ }
129
+ if (recovery.status === 'completed' && replacementBytes > recovery.limit_bytes) {
130
+ throw errorForArtifact('completed fusion output recovery replacement exceeds the contract');
131
+ }
132
+ }
133
+ }
134
+ function outputRecoveryRecord(recovery, path) {
135
+ return {
136
+ kind: recovery.kind,
137
+ status: recovery.status,
138
+ limit_bytes: recovery.limit_bytes,
139
+ original_response_path: path,
140
+ original_record_index: recovery.original_record_index,
141
+ replacement_record_index: recovery.replacement_record_index,
142
+ original_json_rendered_bytes: recovery.original_json_rendered_bytes,
143
+ replacement_json_rendered_bytes: recovery.replacement_json_rendered_bytes,
144
+ original_text_sha256: recovery.original_text_sha256,
145
+ };
146
+ }
147
+ function artifactRefSha256Hex(value) {
148
+ const hex = value.startsWith('sha256:') ? value.slice('sha256:'.length) : value;
149
+ if (!/^[0-9a-f]{64}$/u.test(hex)) {
150
+ throw errorForArtifact(`fusion artifact sha256 is not a lowercase hex digest: ${value}`);
151
+ }
152
+ return hex;
153
+ }
154
+ /** A manifest artifact reference is always one safe basename below its run directory. */
155
+ export function assertFusionArtifactBasename(name) {
156
+ if (name.length === 0 ||
157
+ name !== basename(name) ||
158
+ name.includes('/') ||
159
+ name.includes('\\') ||
160
+ name === '.' ||
161
+ name === '..' ||
162
+ Buffer.byteLength(name, 'utf8') > 255) {
163
+ throw errorForArtifact(`invalid fusion artifact name: ${name}`);
164
+ }
165
+ return name;
166
+ }
167
+ export const FUSION_FAILURE_SUMMARY_INLINE_MESSAGE_BYTES = 1024;
168
+ export const FUSION_FAILURE_SUMMARY_ATTEMPT_CAP = 12;
169
+ export const FUSION_FAILURE_SUMMARY_EVIDENCE_CAP = 24;
170
+ export const FUSION_FAILURE_SUMMARY_MAX_BYTES = 32 * 1024;
171
+ function compareArtifactText(left, right) {
172
+ return left < right ? -1 : left > right ? 1 : 0;
173
+ }
174
+ function failureStageProgress(manifest, stage) {
175
+ const attempts = manifest.attempts.filter((attempt) => attempt.stage === stage);
176
+ const created = attempts.filter((attempt) => attempt.child_created).length;
177
+ const completed = attempts.filter((attempt) => attempt.child_created && attempt.status === 'completed').length;
178
+ const failed = attempts.filter((attempt) => attempt.child_created && attempt.status === 'failed').length;
179
+ const cancelled = attempts.filter((attempt) => attempt.child_created && attempt.status === 'cancelled').length;
180
+ const completedByState = stage === 'candidate'
181
+ ? completed >= 3
182
+ : stage === 'evaluation'
183
+ ? manifest.artifacts['evaluation.json'] !== undefined ||
184
+ manifest.state === 'evaluation_complete' ||
185
+ manifest.state === 'merging' ||
186
+ manifest.state === 'completed'
187
+ : manifest.artifacts['merged.md'] !== undefined || manifest.state === 'completed';
188
+ const progress = {
189
+ status: completedByState ? 'completed' : created === 0 ? 'not_started' : 'incomplete',
190
+ attempts_recorded: attempts.length,
191
+ children_created: created,
192
+ children_completed: completed,
193
+ children_failed: failed,
194
+ children_cancelled: cancelled,
195
+ };
196
+ if (stage === 'candidate') {
197
+ const createdSlots = new Set(attempts.flatMap((attempt) => attempt.child_created && attempt.slot !== undefined ? [attempt.slot] : []));
198
+ progress.not_started_slots = 3 - createdSlots.size;
199
+ }
200
+ return progress;
201
+ }
202
+ /** Derive terminal progress solely from the durable manifest. */
203
+ export function buildFusionRunProgress(manifest) {
204
+ return {
205
+ manifest_state: manifest.state,
206
+ candidates: failureStageProgress(manifest, 'candidate'),
207
+ evaluation: failureStageProgress(manifest, 'evaluation'),
208
+ merge: failureStageProgress(manifest, 'merge'),
209
+ usage_so_far: cloneFusionUsage(manifest.usage),
210
+ };
211
+ }
212
+ function terminalMessageMetadata(message) {
213
+ const bytes = Buffer.from(message, 'utf8');
214
+ return {
215
+ byte_length: bytes.length,
216
+ sha256: sha256Buffer(bytes),
217
+ ...(bytes.length <= FUSION_FAILURE_SUMMARY_INLINE_MESSAGE_BYTES
218
+ ? { inline_message: message }
219
+ : { omission_reason: 'exceeds_inline_message_bytes_cap' }),
220
+ };
221
+ }
222
+ function failureArtifactClassification(name, ref, manifest) {
223
+ for (const attempt of manifest.attempts) {
224
+ if (attempt.response_path === name) {
225
+ return ref.byte_length === 0 && attempt.status !== 'completed'
226
+ ? 'empty_rejected_output'
227
+ : 'complete_stage_output';
228
+ }
229
+ if (attempt.partial_response_path === name)
230
+ return 'partial_stage_output';
231
+ if (attempt.output_recovery?.original_response_path === name)
232
+ return 'oversized_original';
233
+ }
234
+ return 'evidence_only';
235
+ }
236
+ function failureAttemptMetadata(manifest) {
237
+ return manifest.attempts
238
+ .map((attempt) => ({
239
+ stage: attempt.stage,
240
+ ...(attempt.slot === undefined ? {} : { slot: attempt.slot }),
241
+ attempt: attempt.attempt,
242
+ status: attempt.status,
243
+ child_created: attempt.child_created,
244
+ }))
245
+ .sort((left, right) => compareArtifactText(left.stage, right.stage) ||
246
+ (left.slot ?? 0) - (right.slot ?? 0) ||
247
+ left.attempt - right.attempt);
248
+ }
249
+ export function buildFusionFailureSummary(input) {
250
+ if ((input.terminalState !== 'failed' && input.terminalState !== 'cancelled') ||
251
+ input.manifest.state !== input.terminalState) {
252
+ throw errorForArtifact('failure summary requires a matching failed/cancelled terminal manifest');
253
+ }
254
+ if (input.manifest.error !== input.terminalError.message) {
255
+ throw errorForArtifact('failure summary terminal error does not match the durable manifest');
256
+ }
257
+ if (canonicalJson(input.progress) !== canonicalJson(buildFusionRunProgress(input.manifest))) {
258
+ throw errorForArtifact('failure summary progress does not match the durable terminal manifest');
259
+ }
260
+ if (input.manifest.artifacts['failure-summary.json'] !== undefined) {
261
+ throw errorForArtifact('failure summary already exists in the terminal manifest');
262
+ }
263
+ const attempts = failureAttemptMetadata(input.manifest);
264
+ const evidence = Object.entries(input.manifest.artifacts)
265
+ .map(([name, ref]) => ({
266
+ name: assertFusionArtifactBasename(name),
267
+ classification: failureArtifactClassification(name, ref, input.manifest),
268
+ ref: { ...ref },
269
+ }))
270
+ .sort((left, right) => compareArtifactText(left.name, right.name));
271
+ return {
272
+ schema_version: FUSION_FAILURE_SUMMARY_SCHEMA_VERSION,
273
+ run_id: input.manifest.run_id,
274
+ workflow: input.manifest.workflow,
275
+ source: input.manifest.source,
276
+ terminal_state: input.terminalState,
277
+ created_at: input.createdAt,
278
+ answer: { present: false, reason: 'run_did_not_commit' },
279
+ failure: {
280
+ code: input.terminalError.code,
281
+ ...(input.terminalError.stage === undefined ? {} : { stage: input.terminalError.stage }),
282
+ ...(input.terminalError.slot === undefined ? {} : { slot: input.terminalError.slot }),
283
+ ...(input.terminalError.attempt === undefined
284
+ ? {}
285
+ : { attempt: input.terminalError.attempt }),
286
+ child_created: input.terminalError.childCreated,
287
+ message: terminalMessageMetadata(input.terminalError.message),
288
+ },
289
+ progress: input.progress,
290
+ usage_so_far: cloneFusionUsage(input.manifest.usage),
291
+ attempts: {
292
+ listed: attempts.filter((_attempt, index) => index < FUSION_FAILURE_SUMMARY_ATTEMPT_CAP),
293
+ omitted_count: attempts.length - Math.min(attempts.length, FUSION_FAILURE_SUMMARY_ATTEMPT_CAP),
294
+ },
295
+ evidence_artifacts: {
296
+ listed: evidence.filter((_artifact, index) => index < FUSION_FAILURE_SUMMARY_EVIDENCE_CAP),
297
+ omitted_count: evidence.length - Math.min(evidence.length, FUSION_FAILURE_SUMMARY_EVIDENCE_CAP),
298
+ },
299
+ remediation_ids: [
300
+ 'inspect_manifest_bound_evidence',
301
+ 'inspect_terminal_error',
302
+ 'split_or_reduce_work',
303
+ 'retry_same_route_after_operator_review',
304
+ ],
305
+ };
306
+ }
307
+ export class FusionArtifactStore {
308
+ runDirAbs;
309
+ runDirDisplay;
310
+ now;
311
+ manifest;
312
+ manifestWriteChain = Promise.resolve();
313
+ constructor(runDirAbs, runDirDisplay, now, manifest) {
314
+ this.runDirAbs = runDirAbs;
315
+ this.runDirDisplay = runDirDisplay;
316
+ this.now = now;
317
+ this.manifest = manifest;
318
+ }
319
+ static async create(options) {
320
+ const profile = fusionWorkflowProfile(options.profile?.id ?? 'reason');
321
+ const runId = options.runId ?? makeRunId(profile);
322
+ if (!RUN_ID_PATTERN.test(runId))
323
+ throw errorForArtifact(`invalid fusion run id: ${runId}`);
324
+ if (!runId.startsWith(profile.runIdPrefix)) {
325
+ throw errorForArtifact(`fusion run id ${runId} does not carry the ${profile.id} workflow prefix ${profile.runIdPrefix}`);
326
+ }
327
+ const sessionSegment = sanitizePathSegment(options.sessionId ?? `session-${String(process.pid)}`);
328
+ const sessionDirName = `${sessionSegment}-${String(process.pid)}`;
329
+ const runDirAbs = join(options.cwd, '.pi', 'fusion', sessionDirName, runId);
330
+ const runDirDisplay = join('.pi', 'fusion', sessionDirName, runId);
331
+ await mkdir(runDirAbs, { recursive: true, mode: 0o700 });
332
+ await chmod(runDirAbs, 0o700);
333
+ const timestamp = (options.now ?? (() => new Date()))().toISOString();
334
+ const manifest = {
335
+ schema_version: FUSION_MANIFEST_SCHEMA_VERSION,
336
+ run_id: runId,
337
+ workflow: profile.id,
338
+ source: options.source,
339
+ state: 'initializing',
340
+ created_at: timestamp,
341
+ updated_at: timestamp,
342
+ cwd: options.cwd,
343
+ config: options.config,
344
+ models: modelsForManifest(options.models),
345
+ capabilities: options.capabilities ?? {
346
+ candidate: 'reason',
347
+ evaluation: 'reason',
348
+ merge: 'reason',
349
+ },
350
+ context: {
351
+ kind: profile.contextKind,
352
+ policy_id: profile.contextKind === 'session_projection'
353
+ ? 'fusion-session-projection-v1'
354
+ : 'fusion-clean-task-v1',
355
+ },
356
+ tool_policy: {
357
+ candidate_tools: profile.candidateTools,
358
+ evaluation_tools: [],
359
+ merge_tools: [],
360
+ },
361
+ usage: cloneFusionUsage(EMPTY_FUSION_USAGE),
362
+ attempts: [],
363
+ artifacts: {},
364
+ };
365
+ const store = new FusionArtifactStore(runDirAbs, runDirDisplay, options.now ?? (() => new Date()), manifest);
366
+ await store.writeManifest();
367
+ return store;
368
+ }
369
+ get runId() {
370
+ return this.manifest.run_id;
371
+ }
372
+ get artifactDir() {
373
+ return this.runDirDisplay;
374
+ }
375
+ get artifactDirAbs() {
376
+ return this.runDirAbs;
377
+ }
378
+ childToolCallLogPath(stage, slot, attempt) {
379
+ return this.artifactPath(`${attemptPrefix(stage, slot, attempt)}.tool-calls.jsonl`);
380
+ }
381
+ childOutputRecoveryPath(slot, attempt, responseKind) {
382
+ return this.artifactPath(oversizedResponseName(attemptPrefix('candidate', slot, attempt), responseKind));
383
+ }
384
+ snapshot() {
385
+ return publicManifest(this.manifest);
386
+ }
387
+ async transition(to) {
388
+ await this.updateManifest((manifest) => {
389
+ if (!canTransition(manifest.state, to)) {
390
+ throw new FusionError(`illegal fusion state transition ${manifest.state} -> ${to}`, {
391
+ code: 'state_transition_invalid',
392
+ childCreated: false,
393
+ });
394
+ }
395
+ if (to === 'completed' &&
396
+ (manifest.artifacts['merged.md'] === undefined ||
397
+ manifest.artifacts['result.json'] === undefined)) {
398
+ throw new FusionError('fusion cannot complete before merged.md and result.json are durable', {
399
+ code: 'state_transition_invalid',
400
+ childCreated: false,
401
+ });
402
+ }
403
+ manifest.state = to;
404
+ });
405
+ }
406
+ async setAnonymousMap(map) {
407
+ await this.updateManifest((manifest) => {
408
+ manifest.anonymous_map = { ...map };
409
+ });
410
+ }
411
+ async setUsage(usage) {
412
+ await this.updateManifest((manifest) => {
413
+ manifest.usage = cloneFusionUsage(usage);
414
+ });
415
+ }
416
+ async writeCanonicalInput(serialized) {
417
+ await this.writeArtifact('canonical-input.json', serialized);
418
+ }
419
+ /**
420
+ * Complete, source-ordered ledger of every omitted conversation event. Kept in
421
+ * a separate artifact so canonical input carries only compact run receipts
422
+ * while the full omission accounting stays locally auditable.
423
+ */
424
+ async writeContextLedger(ledger) {
425
+ const ref = await this.writeArtifact('context-omission-ledger.json', canonicalJson(ledger));
426
+ await this.updateManifest((manifest) => {
427
+ manifest.context.ledger_artifact = ref.path;
428
+ });
429
+ }
430
+ async writeSourcePolicy(serialized) {
431
+ const ref = await this.writeArtifact('source-policy.private.json', serialized);
432
+ await this.updateManifest((manifest) => {
433
+ manifest.context.source_policy_artifact = ref.path;
434
+ });
435
+ }
436
+ sourcePolicyLaunchReference() {
437
+ const ref = this.manifest.artifacts['source-policy.private.json'];
438
+ if (ref === undefined)
439
+ throw errorForArtifact('research source policy has not been written');
440
+ return { path: this.artifactPath(ref.path), sha256: artifactRefSha256Hex(ref.sha256) };
441
+ }
442
+ /** Route capacities and the pre-candidate whole-workflow feasibility decision. */
443
+ async writeBudgetPlan(plan) {
444
+ await this.writeArtifact('budget-plan.json', canonicalJson(plan));
445
+ }
446
+ async writeBlindCandidates(serialized) {
447
+ await this.writeArtifact('blind-candidates.json', serialized);
448
+ }
449
+ async writeEvaluationJson(value) {
450
+ await this.writeArtifact('evaluation.json', canonicalJson(value));
451
+ }
452
+ async writeMerged(text) {
453
+ return this.writeArtifact('merged.md', text);
454
+ }
455
+ async writeCommittedResult(merged, details) {
456
+ const value = {
457
+ schema_version: FUSION_COMMITTED_RESULT_SCHEMA_VERSION,
458
+ run_id: this.runId,
459
+ merged,
460
+ details,
461
+ };
462
+ return this.writeArtifact('result.json', `${canonicalJson(value)}\n`);
463
+ }
464
+ async writeError(state, error) {
465
+ await this.writeArtifact('error.json', `${JSON.stringify({ state, error }, null, 2)}\n`);
466
+ await this.updateManifest((manifest) => {
467
+ if (!TERMINAL_STATES.has(state))
468
+ throw errorForArtifact(`invalid terminal state ${state}`);
469
+ if (!canTransition(manifest.state, state)) {
470
+ throw new FusionError(`illegal fusion state transition ${manifest.state} -> ${state}`, {
471
+ code: 'state_transition_invalid',
472
+ childCreated: false,
473
+ });
474
+ }
475
+ manifest.state = state;
476
+ manifest.error = error;
477
+ });
478
+ }
479
+ /**
480
+ * Writes the terminal evidence summary exactly once after writeError has made
481
+ * the manifest terminal. The summary deliberately contains refs only, never
482
+ * stage-output bodies.
483
+ */
484
+ async writeFailureSummary(summary) {
485
+ if (this.manifest.state !== 'failed' && this.manifest.state !== 'cancelled') {
486
+ throw errorForArtifact('failure summary requires a failed/cancelled terminal manifest');
487
+ }
488
+ if (summary.terminal_state !== this.manifest.state) {
489
+ throw errorForArtifact('failure summary terminal state does not match the manifest');
490
+ }
491
+ if (summary.run_id !== this.manifest.run_id ||
492
+ summary.workflow !== this.manifest.workflow ||
493
+ summary.source !== this.manifest.source) {
494
+ throw errorForArtifact('failure summary identity does not match the terminal manifest');
495
+ }
496
+ if (summary.answer?.present !== false || summary.answer.reason !== 'run_did_not_commit') {
497
+ throw errorForArtifact('failure summary must assert that no answer was committed');
498
+ }
499
+ if (this.manifest.error === undefined || this.manifest.artifacts['error.json'] === undefined) {
500
+ throw errorForArtifact('failure summary requires durable terminal error evidence');
501
+ }
502
+ if (canonicalJson(summary.failure.message) !==
503
+ canonicalJson(terminalMessageMetadata(this.manifest.error))) {
504
+ throw errorForArtifact('failure summary terminal error metadata does not match the manifest');
505
+ }
506
+ if (canonicalJson(summary.progress) !== canonicalJson(buildFusionRunProgress(this.snapshot()))) {
507
+ throw errorForArtifact('failure summary progress does not match the terminal manifest');
508
+ }
509
+ if (canonicalJson(summary.usage_so_far) !== canonicalJson(this.manifest.usage)) {
510
+ throw errorForArtifact('failure summary usage does not match the terminal manifest');
511
+ }
512
+ if (this.manifest.artifacts['failure-summary.json'] !== undefined) {
513
+ throw errorForArtifact('failure summary is already bound in the manifest');
514
+ }
515
+ const bytes = Buffer.from(`${canonicalJson(summary)}\n`, 'utf8');
516
+ if (bytes.length > FUSION_FAILURE_SUMMARY_MAX_BYTES) {
517
+ throw errorForArtifact('failure summary exceeds its bounded diagnostics artifact limit');
518
+ }
519
+ return this.writeArtifact('failure-summary.json', bytes);
520
+ }
521
+ async recordChildAttempt(input) {
522
+ const prefix = attemptPrefix(input.result.stage, input.result.slot, input.result.attempt);
523
+ await this.writeArtifact(`${prefix}.system-prompt.txt`, input.systemPrompt);
524
+ const promptRef = await this.writeArtifact(`${prefix}.prompt.txt`, input.prompt);
525
+ const eventsRef = await this.writeArtifact(`${prefix}.events.jsonl`, input.result.events);
526
+ const stderrRef = await this.writeArtifact(`${prefix}.stderr.txt`, input.result.stderr);
527
+ const responseRef = await this.writeArtifact(responseName(prefix, input.responseKind), input.result.text);
528
+ const toolCallsRef = input.result.toolCallTrace === undefined
529
+ ? undefined
530
+ : await this.writeArtifact(`${prefix}.tool-calls.jsonl`, input.result.toolCallTrace.bytes);
531
+ if (input.result.outputRecovery !== undefined) {
532
+ validateOutputRecovery(input.result.outputRecovery, input.result.text);
533
+ }
534
+ const outputRecoveryRef = input.result.outputRecovery === undefined
535
+ ? undefined
536
+ : await this.writeArtifact(oversizedResponseName(prefix, input.responseKind), input.result.outputRecovery.original_text);
537
+ await this.updateManifest((manifest) => {
538
+ const record = {
539
+ stage: input.result.stage,
540
+ attempt: input.result.attempt,
541
+ status: 'completed',
542
+ child_created: true,
543
+ prompt_path: promptRef.path,
544
+ events_path: eventsRef.path,
545
+ stderr_path: stderrRef.path,
546
+ response_path: responseRef.path,
547
+ provider: input.result.provider,
548
+ model: input.result.model,
549
+ qualifiedId: input.result.qualifiedId,
550
+ usage: cloneFusionUsage(input.result.usage),
551
+ };
552
+ if (toolCallsRef !== undefined && input.result.toolCallTrace !== undefined) {
553
+ record.tool_calls_path = toolCallsRef.path;
554
+ record.tool_calls = { ...input.result.toolCallTrace.summary };
555
+ }
556
+ if (outputRecoveryRef !== undefined && input.result.outputRecovery !== undefined) {
557
+ record.output_recovery = outputRecoveryRecord(input.result.outputRecovery, outputRecoveryRef.path);
558
+ }
559
+ if (input.result.slot !== undefined)
560
+ record.slot = input.result.slot;
561
+ manifest.attempts.push(record);
562
+ });
563
+ }
564
+ async recordCalibrationViolation(input) {
565
+ const prefix = attemptPrefix(input.stage, input.slot, input.attempt);
566
+ return this.writeArtifact(calibrationViolationName(prefix), `${canonicalJson(input.violation)}\n`);
567
+ }
568
+ async recordValidationCandidateContractEvent(input) {
569
+ const name = `candidate-${String(input.slot)}.output-contract-${input.status}.json`;
570
+ return this.writeArtifact(name, `${canonicalJson({
571
+ schema_version: FUSION_VALIDATE_CANDIDATE_CONTRACT_EVENT_SCHEMA_VERSION,
572
+ ...input.detail,
573
+ candidate_id: input.candidateId,
574
+ slot: input.slot,
575
+ status: input.status,
576
+ })}\n`);
577
+ }
578
+ async recordFailedAttempt(input) {
579
+ const prefix = attemptPrefix(input.stage, input.slot, input.attempt);
580
+ await this.writeArtifact(`${prefix}.system-prompt.txt`, input.systemPrompt);
581
+ const promptRef = await this.writeArtifact(`${prefix}.prompt.txt`, input.prompt);
582
+ const eventsRef = await this.writeArtifact(`${prefix}.events.jsonl`, input.events);
583
+ const stderrRef = await this.writeArtifact(`${prefix}.stderr.txt`, input.stderr);
584
+ const responseRef = await this.writeArtifact(responseName(prefix, input.responseKind), '');
585
+ const partialResponseRef = input.partialResponse.length === 0
586
+ ? undefined
587
+ : await this.writeArtifact(`${prefix}.response.partial.${input.responseKind}`, input.partialResponse);
588
+ if (input.outputRecovery !== undefined)
589
+ validateOutputRecovery(input.outputRecovery);
590
+ const outputRecoveryRef = input.outputRecovery === undefined
591
+ ? undefined
592
+ : await this.writeArtifact(oversizedResponseName(prefix, input.responseKind), input.outputRecovery.original_text);
593
+ await this.updateManifest((manifest) => {
594
+ const record = {
595
+ stage: input.stage,
596
+ attempt: input.attempt,
597
+ status: input.status,
598
+ child_created: input.childCreated,
599
+ prompt_path: promptRef.path,
600
+ events_path: eventsRef.path,
601
+ stderr_path: stderrRef.path,
602
+ response_path: responseRef.path,
603
+ error: input.error,
604
+ };
605
+ if (partialResponseRef !== undefined)
606
+ record.partial_response_path = partialResponseRef.path;
607
+ if (outputRecoveryRef !== undefined && input.outputRecovery !== undefined) {
608
+ record.output_recovery = outputRecoveryRecord(input.outputRecovery, outputRecoveryRef.path);
609
+ }
610
+ if (input.provider !== undefined)
611
+ record.provider = input.provider;
612
+ if (input.model !== undefined)
613
+ record.model = input.model;
614
+ if (input.qualifiedId !== undefined)
615
+ record.qualifiedId = input.qualifiedId;
616
+ if (input.usage !== undefined)
617
+ record.usage = cloneFusionUsage(input.usage);
618
+ if (input.slot !== undefined)
619
+ record.slot = input.slot;
620
+ manifest.attempts.push(record);
621
+ });
622
+ }
623
+ /** Writes a durable artifact then binds its exact bytes in the manifest. */
624
+ async writeArtifact(name, data) {
625
+ const absPath = this.artifactPath(name);
626
+ const ref = await writePrivateFile(absPath, data);
627
+ await this.updateManifest((manifest) => {
628
+ manifest.artifacts[name] = ref;
629
+ });
630
+ return ref;
631
+ }
632
+ artifactPath(name) {
633
+ assertFusionArtifactBasename(name);
634
+ const absPath = join(this.runDirAbs, name);
635
+ if (!pathInside(this.runDirAbs, absPath)) {
636
+ throw errorForArtifact(`fusion artifact path escapes run directory: ${name}`);
637
+ }
638
+ return absPath;
639
+ }
640
+ async writeManifest() {
641
+ await writeJsonAtomic(join(this.runDirAbs, 'manifest.json'), publicManifest(this.manifest));
642
+ }
643
+ async updateManifest(mutator) {
644
+ const write = async () => {
645
+ mutator(this.manifest);
646
+ this.manifest.updated_at = this.now().toISOString();
647
+ await this.writeManifest();
648
+ };
649
+ const next = this.manifestWriteChain.then(write, write);
650
+ this.manifestWriteChain = next.catch(() => undefined);
651
+ await next;
652
+ }
653
+ }
654
+ //# sourceMappingURL=artifacts.js.map