@bastani/atomic 0.9.12 → 0.9.13-alpha.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 (264) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/dist/builtin/intercom/CHANGELOG.md +6 -0
  3. package/dist/builtin/intercom/README.md +5 -12
  4. package/dist/builtin/intercom/broker/client.ts +11 -2
  5. package/dist/builtin/intercom/contact-supervisor-tool.ts +13 -3
  6. package/dist/builtin/intercom/index-heavy.ts +13 -3
  7. package/dist/builtin/intercom/index.ts +43 -19
  8. package/dist/builtin/intercom/intercom-utils.ts +15 -29
  9. package/dist/builtin/intercom/package.json +1 -1
  10. package/dist/builtin/intercom/source-ownership.ts +9 -12
  11. package/dist/builtin/mcp/CHANGELOG.md +6 -0
  12. package/dist/builtin/mcp/README.md +3 -1
  13. package/dist/builtin/mcp/index.ts +10 -16
  14. package/dist/builtin/mcp/package.json +1 -1
  15. package/dist/builtin/mcp/startup-warmup.ts +20 -8
  16. package/dist/builtin/subagents/CHANGELOG.md +30 -0
  17. package/dist/builtin/subagents/README.md +22 -30
  18. package/dist/builtin/subagents/package.json +1 -1
  19. package/dist/builtin/subagents/src/extension/doctor.ts +1 -1
  20. package/dist/builtin/subagents/src/extension/fanout-child.ts +76 -56
  21. package/dist/builtin/subagents/src/extension/index.ts +23 -32
  22. package/dist/builtin/subagents/src/extension/startup-maintenance.ts +25 -44
  23. package/dist/builtin/subagents/src/intercom/result-intercom.ts +7 -7
  24. package/dist/builtin/subagents/src/runs/background/async-job-tracker.ts +166 -434
  25. package/dist/builtin/subagents/src/runs/background/notify.ts +29 -8
  26. package/dist/builtin/subagents/src/runs/foreground/chain-execution-dynamic-step.ts +2 -2
  27. package/dist/builtin/subagents/src/runs/foreground/chain-execution-parallel-runner.ts +4 -4
  28. package/dist/builtin/subagents/src/runs/foreground/chain-execution-parallel-step.ts +2 -2
  29. package/dist/builtin/subagents/src/runs/foreground/chain-execution-sequential-step.ts +2 -2
  30. package/dist/builtin/subagents/src/runs/foreground/execution-run-sync.ts +1 -281
  31. package/dist/builtin/subagents/src/runs/foreground/execution-structured-retries.ts +11 -75
  32. package/dist/builtin/subagents/src/runs/foreground/inprocess-run-sync.ts +457 -0
  33. package/dist/builtin/subagents/src/runs/foreground/subagent-executor-async.ts +119 -246
  34. package/dist/builtin/subagents/src/runs/foreground/subagent-executor-chain.ts +1 -1
  35. package/dist/builtin/subagents/src/runs/foreground/subagent-executor-context.ts +18 -9
  36. package/dist/builtin/subagents/src/runs/foreground/subagent-executor-parallel-task.ts +1 -1
  37. package/dist/builtin/subagents/src/runs/foreground/subagent-executor-parallel.ts +4 -4
  38. package/dist/builtin/subagents/src/runs/foreground/subagent-executor-resume.ts +1 -646
  39. package/dist/builtin/subagents/src/runs/foreground/subagent-executor-runtime.ts +2 -7
  40. package/dist/builtin/subagents/src/runs/foreground/subagent-executor-single.ts +5 -5
  41. package/dist/builtin/subagents/src/runs/foreground/subagent-executor-status.ts +7 -8
  42. package/dist/builtin/subagents/src/runs/foreground/subagent-executor-types.ts +14 -5
  43. package/dist/builtin/subagents/src/runs/foreground/subagent-executor.ts +158 -93
  44. package/dist/builtin/subagents/src/runs/inprocess/background-single.ts +179 -0
  45. package/dist/builtin/subagents/src/runs/inprocess/background.ts +89 -0
  46. package/dist/builtin/subagents/src/runs/inprocess/child-policy.ts +27 -0
  47. package/dist/builtin/subagents/src/runs/inprocess/control-registry.ts +55 -0
  48. package/dist/builtin/subagents/src/runs/inprocess/control-status.ts +106 -0
  49. package/dist/builtin/subagents/src/runs/inprocess/index.ts +50 -0
  50. package/dist/builtin/subagents/src/runs/inprocess/nested-routing.ts +218 -0
  51. package/dist/builtin/subagents/src/runs/{shared/subagent-prompt-runtime.ts → inprocess/prompt-behavior.ts} +37 -75
  52. package/dist/builtin/subagents/src/runs/inprocess/runner.ts +1138 -0
  53. package/dist/builtin/subagents/src/runs/{shared/nested-events.ts → inprocess/runtime-support/nested-api.ts} +13 -7
  54. package/dist/builtin/subagents/src/runs/{shared/nested-events-control.ts → inprocess/runtime-support/nested-control.ts} +18 -17
  55. package/dist/builtin/subagents/src/runs/{shared/nested-events-core.ts → inprocess/runtime-support/nested-core.ts} +14 -61
  56. package/dist/builtin/subagents/src/runs/{shared/nested-events-projection.ts → inprocess/runtime-support/nested-projection.ts} +4 -10
  57. package/dist/builtin/subagents/src/runs/{shared/nested-events-registry.ts → inprocess/runtime-support/nested-registry.ts} +19 -4
  58. package/dist/builtin/subagents/src/runs/{shared/nested-render.ts → inprocess/runtime-support/nested-rendering.ts} +3 -3
  59. package/dist/builtin/subagents/src/runs/{shared/nested-events-sanitize.ts → inprocess/runtime-support/nested-sanitize.ts} +3 -3
  60. package/dist/builtin/subagents/src/runs/inprocess.ts +1 -0
  61. package/dist/builtin/subagents/src/runs/shared/dynamic-fanout.ts +10 -4
  62. package/dist/builtin/subagents/src/runs/shared/model-candidate-filter.ts +0 -1
  63. package/dist/builtin/subagents/src/runs/shared/model-fallback.ts +15 -492
  64. package/dist/builtin/subagents/src/runs/shared/parallel-utils.ts +13 -11
  65. package/dist/builtin/subagents/src/runs/shared/run-history.ts +3 -5
  66. package/dist/builtin/subagents/src/runs/shared/single-output.ts +4 -4
  67. package/dist/builtin/subagents/src/runs/shared/workflow-graph.ts +5 -5
  68. package/dist/builtin/subagents/src/shared/types-async.ts +0 -1
  69. package/dist/builtin/subagents/src/shared/types-config.ts +18 -0
  70. package/dist/builtin/subagents/src/shared/types-results.ts +9 -2
  71. package/dist/builtin/subagents/src/slash/prompt-template-bridge.ts +2 -3
  72. package/dist/builtin/subagents/src/slash/slash-live-state.ts +2 -2
  73. package/dist/builtin/subagents/src/tui/render-chain-graph.ts +10 -4
  74. package/dist/builtin/subagents/src/tui/render-event-formatting.ts +12 -11
  75. package/dist/builtin/subagents/src/tui/render-result-compact.ts +49 -12
  76. package/dist/builtin/subagents/src/tui/render-result.ts +46 -17
  77. package/dist/builtin/subagents/src/tui/render-stable-output.ts +1 -1
  78. package/dist/builtin/subagents/src/tui/render-status-progress.ts +8 -5
  79. package/dist/builtin/subagents/src/tui/render-widget-graph.ts +0 -8
  80. package/dist/builtin/subagents/src/tui/render-widget.ts +0 -24
  81. package/dist/builtin/web-access/package.json +1 -1
  82. package/dist/builtin/workflows/CHANGELOG.md +25 -0
  83. package/dist/builtin/workflows/README.md +13 -3
  84. package/dist/builtin/workflows/package.json +1 -1
  85. package/dist/builtin/workflows/src/durable/backend.ts +7 -0
  86. package/dist/builtin/workflows/src/durable/dbos-metadata.ts +11 -1
  87. package/dist/builtin/workflows/src/durable/resume-runtime.ts +1 -0
  88. package/dist/builtin/workflows/src/durable/tool-primitive.ts +4 -28
  89. package/dist/builtin/workflows/src/durable/types.ts +5 -0
  90. package/dist/builtin/workflows/src/engine/run.ts +17 -1
  91. package/dist/builtin/workflows/src/extension/atomic-stage-session.ts +12 -39
  92. package/dist/builtin/workflows/src/extension/config-file-loader.ts +10 -1
  93. package/dist/builtin/workflows/src/extension/config-loader.ts +1 -1
  94. package/dist/builtin/workflows/src/extension/dispatcher.ts +4 -0
  95. package/dist/builtin/workflows/src/extension/index.bundle.mjs +1151 -737
  96. package/dist/builtin/workflows/src/extension/lifecycle-notifications.ts +303 -38
  97. package/dist/builtin/workflows/src/extension/render-result.ts +4 -0
  98. package/dist/builtin/workflows/src/extension/runtime-durable-resume.ts +6 -3
  99. package/dist/builtin/workflows/src/extension/runtime.ts +7 -1
  100. package/dist/builtin/workflows/src/extension/wiring.ts +53 -11
  101. package/dist/builtin/workflows/src/extension/workflow-command-registration.ts +12 -3
  102. package/dist/builtin/workflows/src/extension/workflow-durable-resume-command.ts +5 -2
  103. package/dist/builtin/workflows/src/extension/workflow-run-control-command.ts +9 -7
  104. package/dist/builtin/workflows/src/extension/workflow-status-summary.ts +11 -0
  105. package/dist/builtin/workflows/src/extension/workflow-tool-content.ts +18 -1
  106. package/dist/builtin/workflows/src/extension/workflow-tool-control.ts +10 -8
  107. package/dist/builtin/workflows/src/extension/workflow-tool.ts +9 -4
  108. package/dist/builtin/workflows/src/runs/background/quit.ts +28 -4
  109. package/dist/builtin/workflows/src/runs/background/resume-acknowledgements.ts +2 -1
  110. package/dist/builtin/workflows/src/runs/background/status.ts +55 -8
  111. package/dist/builtin/workflows/src/runs/foreground/executor-direct-helpers.ts +4 -0
  112. package/dist/builtin/workflows/src/runs/foreground/executor-prompt-nodes.ts +3 -1
  113. package/dist/builtin/workflows/src/runs/foreground/executor-stage-control.ts +1 -1
  114. package/dist/builtin/workflows/src/runs/foreground/stage-runner-context.ts +1 -1
  115. package/dist/builtin/workflows/src/runs/foreground/stage-runner-controller.ts +688 -38
  116. package/dist/builtin/workflows/src/runs/foreground/stage-runner-types.ts +3 -1
  117. package/dist/builtin/workflows/src/runs/shared/model-fallback-failures.ts +19 -514
  118. package/dist/builtin/workflows/src/runs/shared/retry.ts +44 -0
  119. package/dist/builtin/workflows/src/shared/authoring-contract-stage.d.ts +5 -0
  120. package/dist/builtin/workflows/src/shared/authoring-contract-stage.ts +6 -0
  121. package/dist/builtin/workflows/src/shared/authoring-contract-ui.d.ts +5 -1
  122. package/dist/builtin/workflows/src/shared/authoring-contract-ui.ts +5 -0
  123. package/dist/builtin/workflows/src/shared/persistence-restore-helpers.ts +5 -0
  124. package/dist/builtin/workflows/src/shared/persistence-restore.ts +3 -0
  125. package/dist/builtin/workflows/src/shared/persistence-session-entries.ts +4 -0
  126. package/dist/builtin/workflows/src/shared/run-indicator-status.ts +75 -0
  127. package/dist/builtin/workflows/src/shared/store-public-types.ts +43 -7
  128. package/dist/builtin/workflows/src/shared/store-run-methods.ts +42 -8
  129. package/dist/builtin/workflows/src/shared/store-stage-methods.ts +31 -8
  130. package/dist/builtin/workflows/src/shared/store-types.ts +34 -0
  131. package/dist/builtin/workflows/src/shared/timing.ts +10 -0
  132. package/dist/builtin/workflows/src/tui/chat-surface-message.ts +22 -2
  133. package/dist/builtin/workflows/src/tui/session-list.ts +1 -1
  134. package/dist/builtin/workflows/src/tui/session-overlays.ts +14 -10
  135. package/dist/builtin/workflows/src/tui/session-picker.ts +20 -4
  136. package/dist/builtin/workflows/src/tui/status-list.ts +31 -47
  137. package/dist/builtin/workflows/src/tui/store-widget-installer.ts +9 -1
  138. package/dist/builtin/workflows/src/tui/widget.ts +16 -46
  139. package/dist/core/agent-session-auto-compaction.d.ts.map +1 -1
  140. package/dist/core/agent-session-auto-compaction.js +54 -11
  141. package/dist/core/agent-session-auto-compaction.js.map +1 -1
  142. package/dist/core/agent-session-events.d.ts.map +1 -1
  143. package/dist/core/agent-session-events.js +49 -14
  144. package/dist/core/agent-session-events.js.map +1 -1
  145. package/dist/core/agent-session-methods.d.ts +13 -0
  146. package/dist/core/agent-session-methods.d.ts.map +1 -1
  147. package/dist/core/agent-session-methods.js.map +1 -1
  148. package/dist/core/agent-session-models.d.ts.map +1 -1
  149. package/dist/core/agent-session-models.js +12 -1
  150. package/dist/core/agent-session-models.js.map +1 -1
  151. package/dist/core/agent-session-prompt.d.ts.map +1 -1
  152. package/dist/core/agent-session-prompt.js +4 -0
  153. package/dist/core/agent-session-prompt.js.map +1 -1
  154. package/dist/core/agent-session-retry.d.ts +21 -4
  155. package/dist/core/agent-session-retry.d.ts.map +1 -1
  156. package/dist/core/agent-session-retry.js +232 -106
  157. package/dist/core/agent-session-retry.js.map +1 -1
  158. package/dist/core/agent-session-state.d.ts.map +1 -1
  159. package/dist/core/agent-session-state.js +2 -1
  160. package/dist/core/agent-session-state.js.map +1 -1
  161. package/dist/core/agent-session-tool-hooks.d.ts.map +1 -1
  162. package/dist/core/agent-session-tool-hooks.js +12 -0
  163. package/dist/core/agent-session-tool-hooks.js.map +1 -1
  164. package/dist/core/agent-session-tool-registry.d.ts.map +1 -1
  165. package/dist/core/agent-session-tool-registry.js +1 -1
  166. package/dist/core/agent-session-tool-registry.js.map +1 -1
  167. package/dist/core/agent-session-types.d.ts +3 -1
  168. package/dist/core/agent-session-types.d.ts.map +1 -1
  169. package/dist/core/agent-session-types.js.map +1 -1
  170. package/dist/core/agent-session.d.ts +12 -1
  171. package/dist/core/agent-session.d.ts.map +1 -1
  172. package/dist/core/agent-session.js +7 -0
  173. package/dist/core/agent-session.js.map +1 -1
  174. package/dist/core/extensions/context-types.d.ts +25 -0
  175. package/dist/core/extensions/context-types.d.ts.map +1 -1
  176. package/dist/core/extensions/context-types.js.map +1 -1
  177. package/dist/core/extensions/index.d.ts +1 -1
  178. package/dist/core/extensions/index.d.ts.map +1 -1
  179. package/dist/core/extensions/index.js.map +1 -1
  180. package/dist/core/extensions/runner-context.d.ts +2 -1
  181. package/dist/core/extensions/runner-context.d.ts.map +1 -1
  182. package/dist/core/extensions/runner-context.js +4 -0
  183. package/dist/core/extensions/runner-context.js.map +1 -1
  184. package/dist/core/extensions/runner.d.ts +3 -2
  185. package/dist/core/extensions/runner.d.ts.map +1 -1
  186. package/dist/core/extensions/runner.js +3 -1
  187. package/dist/core/extensions/runner.js.map +1 -1
  188. package/dist/core/model-fallback-failures.d.ts +30 -0
  189. package/dist/core/model-fallback-failures.d.ts.map +1 -0
  190. package/dist/core/model-fallback-failures.js +610 -0
  191. package/dist/core/model-fallback-failures.js.map +1 -0
  192. package/dist/core/retry-policy.d.ts +30 -0
  193. package/dist/core/retry-policy.d.ts.map +1 -0
  194. package/dist/core/retry-policy.js +25 -0
  195. package/dist/core/retry-policy.js.map +1 -0
  196. package/dist/core/sdk-types.d.ts +8 -2
  197. package/dist/core/sdk-types.d.ts.map +1 -1
  198. package/dist/core/sdk-types.js.map +1 -1
  199. package/dist/core/sdk.d.ts.map +1 -1
  200. package/dist/core/sdk.js +6 -1
  201. package/dist/core/sdk.js.map +1 -1
  202. package/dist/core/session-manager-core.d.ts +2 -0
  203. package/dist/core/session-manager-core.d.ts.map +1 -1
  204. package/dist/core/session-manager-core.js +7 -0
  205. package/dist/core/session-manager-core.js.map +1 -1
  206. package/dist/index-extensions.d.ts +1 -1
  207. package/dist/index-extensions.d.ts.map +1 -1
  208. package/dist/index-extensions.js.map +1 -1
  209. package/dist/index.d.ts +3 -0
  210. package/dist/index.d.ts.map +1 -1
  211. package/dist/index.js +2 -0
  212. package/dist/index.js.map +1 -1
  213. package/docs/intercom.md +5 -10
  214. package/docs/settings.md +7 -1
  215. package/docs/subagents.md +19 -19
  216. package/docs/workflows.md +21 -5
  217. package/examples/extensions/gondolin/package-lock.json +3 -3
  218. package/npm-shrinkwrap.json +29 -29
  219. package/package.json +2 -2
  220. package/dist/builtin/subagents/src/runs/background/async-event-journal.ts +0 -112
  221. package/dist/builtin/subagents/src/runs/background/async-execution-chain.ts +0 -543
  222. package/dist/builtin/subagents/src/runs/background/async-execution-common.ts +0 -140
  223. package/dist/builtin/subagents/src/runs/background/async-execution-single.ts +0 -279
  224. package/dist/builtin/subagents/src/runs/background/async-execution-types.ts +0 -99
  225. package/dist/builtin/subagents/src/runs/background/async-execution.ts +0 -17
  226. package/dist/builtin/subagents/src/runs/background/async-resume.ts +0 -402
  227. package/dist/builtin/subagents/src/runs/background/async-status.ts +0 -411
  228. package/dist/builtin/subagents/src/runs/background/completion-claims.ts +0 -196
  229. package/dist/builtin/subagents/src/runs/background/completion-dedupe.ts +0 -102
  230. package/dist/builtin/subagents/src/runs/background/parallel-groups.ts +0 -59
  231. package/dist/builtin/subagents/src/runs/background/result-delivery-processor.ts +0 -300
  232. package/dist/builtin/subagents/src/runs/background/result-file-claims.ts +0 -166
  233. package/dist/builtin/subagents/src/runs/background/result-quarantine.ts +0 -77
  234. package/dist/builtin/subagents/src/runs/background/result-retry-scheduler.ts +0 -48
  235. package/dist/builtin/subagents/src/runs/background/result-status.ts +0 -87
  236. package/dist/builtin/subagents/src/runs/background/result-watcher-data.ts +0 -72
  237. package/dist/builtin/subagents/src/runs/background/result-watcher.ts +0 -320
  238. package/dist/builtin/subagents/src/runs/background/run-id-resolver.ts +0 -105
  239. package/dist/builtin/subagents/src/runs/background/run-status.ts +0 -358
  240. package/dist/builtin/subagents/src/runs/background/stale-run-reconciler.ts +0 -457
  241. package/dist/builtin/subagents/src/runs/background/subagent-runner-dynamic.ts +0 -376
  242. package/dist/builtin/subagents/src/runs/background/subagent-runner-finalize.ts +0 -150
  243. package/dist/builtin/subagents/src/runs/background/subagent-runner-output.ts +0 -109
  244. package/dist/builtin/subagents/src/runs/background/subagent-runner-parallel-helpers.ts +0 -128
  245. package/dist/builtin/subagents/src/runs/background/subagent-runner-parallel.ts +0 -309
  246. package/dist/builtin/subagents/src/runs/background/subagent-runner-sequential.ts +0 -173
  247. package/dist/builtin/subagents/src/runs/background/subagent-runner-state.ts +0 -578
  248. package/dist/builtin/subagents/src/runs/background/subagent-runner-step.ts +0 -330
  249. package/dist/builtin/subagents/src/runs/background/subagent-runner-streaming.ts +0 -330
  250. package/dist/builtin/subagents/src/runs/background/subagent-runner-types.ts +0 -210
  251. package/dist/builtin/subagents/src/runs/background/subagent-runner-utils.ts +0 -69
  252. package/dist/builtin/subagents/src/runs/background/subagent-runner.ts +0 -90
  253. package/dist/builtin/subagents/src/runs/background/top-level-async.ts +0 -12
  254. package/dist/builtin/subagents/src/runs/foreground/execution-attempt-control.ts +0 -140
  255. package/dist/builtin/subagents/src/runs/foreground/execution-attempt-finalize.ts +0 -108
  256. package/dist/builtin/subagents/src/runs/foreground/execution-attempt-types.ts +0 -17
  257. package/dist/builtin/subagents/src/runs/foreground/execution-attempt.ts +0 -558
  258. package/dist/builtin/subagents/src/runs/shared/attempt-watchdog.ts +0 -126
  259. package/dist/builtin/subagents/src/runs/shared/final-drain.ts +0 -39
  260. package/dist/builtin/subagents/src/runs/shared/pi-args.ts +0 -339
  261. package/dist/builtin/subagents/src/runs/shared/pi-spawn.ts +0 -165
  262. package/dist/builtin/subagents/src/runs/shared/spawn-env.ts +0 -21
  263. package/dist/builtin/subagents/src/shared/post-exit-stdio-guard.ts +0 -86
  264. /package/dist/builtin/subagents/src/runs/{shared/nested-path.ts → inprocess/runtime-support/nested-paths.ts} +0 -0
@@ -1,102 +0,0 @@
1
- export interface CompletionSeenRecord {
2
- seenAt: number;
3
- signature?: string;
4
- }
5
-
6
- export type CompletionSeenMap = Map<string, number | CompletionSeenRecord>;
7
- export type CompletionSeenMatch = "miss" | "match" | "conflict";
8
-
9
- interface CompletionDataLike {
10
- runId?: unknown;
11
- id?: unknown;
12
- agent?: unknown;
13
- timestamp?: unknown;
14
- sessionId?: unknown;
15
- taskIndex?: unknown;
16
- totalTasks?: unknown;
17
- success?: unknown;
18
- }
19
-
20
- function asNonEmptyString(value: unknown): string | undefined {
21
- if (typeof value !== "string") return undefined;
22
- const trimmed = value.trim();
23
- return trimmed.length > 0 ? trimmed : undefined;
24
- }
25
-
26
- function asFiniteNumber(value: unknown): number | undefined {
27
- if (typeof value !== "number") return undefined;
28
- return Number.isFinite(value) ? value : undefined;
29
- }
30
-
31
- export function buildCompletionKey(data: CompletionDataLike, fallback: string): string {
32
- const runId = asNonEmptyString(data.runId);
33
- if (runId) return `run:${runId}`;
34
- const id = asNonEmptyString(data.id);
35
- if (id) return `id:${id}`;
36
- const sessionId = asNonEmptyString(data.sessionId) ?? "no-session";
37
- const agent = asNonEmptyString(data.agent) ?? "unknown";
38
- const timestamp = asFiniteNumber(data.timestamp);
39
- const taskIndex = asFiniteNumber(data.taskIndex);
40
- const totalTasks = asFiniteNumber(data.totalTasks);
41
- const success = typeof data.success === "boolean" ? (data.success ? "1" : "0") : "?";
42
- return [
43
- "meta",
44
- sessionId,
45
- agent,
46
- timestamp !== undefined ? String(timestamp) : "no-ts",
47
- taskIndex !== undefined ? String(taskIndex) : "-",
48
- totalTasks !== undefined ? String(totalTasks) : "-",
49
- success,
50
- fallback,
51
- ].join(":");
52
- }
53
-
54
- function recordTimestamp(record: number | CompletionSeenRecord): number {
55
- return typeof record === "number" ? record : record.seenAt;
56
- }
57
-
58
- function pruneSeenMap(seen: CompletionSeenMap, now: number, ttlMs: number): void {
59
- for (const [key, record] of seen.entries()) {
60
- if (now - recordTimestamp(record) > ttlMs) seen.delete(key);
61
- }
62
- }
63
-
64
- export function lookupSeenWithTtl(
65
- seen: CompletionSeenMap,
66
- key: string,
67
- signature: string | undefined,
68
- now: number,
69
- ttlMs: number,
70
- ): CompletionSeenMatch {
71
- pruneSeenMap(seen, now, ttlMs);
72
- const record = seen.get(key);
73
- if (record === undefined) return "miss";
74
- const recordedSignature = typeof record === "number" ? undefined : record.signature;
75
- return recordedSignature === undefined || signature === undefined || recordedSignature === signature
76
- ? "match"
77
- : "conflict";
78
- }
79
-
80
- export function hasSeenWithTtl(seen: CompletionSeenMap, key: string, now: number, ttlMs: number): boolean {
81
- return lookupSeenWithTtl(seen, key, undefined, now, ttlMs) !== "miss";
82
- }
83
-
84
- export function recordSeen(seen: CompletionSeenMap, key: string, now: number, signature?: string): void {
85
- seen.set(key, signature === undefined ? now : { seenAt: now, signature });
86
- }
87
-
88
- export function markSeenWithTtl(seen: CompletionSeenMap, key: string, now: number, ttlMs: number): boolean {
89
- pruneSeenMap(seen, now, ttlMs);
90
- if (seen.has(key)) return true;
91
- seen.set(key, now);
92
- return false;
93
- }
94
-
95
- export function getGlobalSeenMap(storeKey: string): CompletionSeenMap {
96
- const globalStore = globalThis as Record<string, unknown>;
97
- const existing = globalStore[storeKey];
98
- if (existing instanceof Map) return existing as CompletionSeenMap;
99
- const map: CompletionSeenMap = new Map();
100
- globalStore[storeKey] = map;
101
- return map;
102
- }
@@ -1,59 +0,0 @@
1
- import type { AsyncParallelGroupStatus } from "../../shared/types.ts";
2
-
3
- function isValidParallelGroup(
4
- group: unknown,
5
- stepCount: number,
6
- chainStepCount: number,
7
- ): group is AsyncParallelGroupStatus {
8
- if (typeof group !== "object" || group === null) return false;
9
- const { start, count, stepIndex } = group as Partial<AsyncParallelGroupStatus>;
10
- return (
11
- typeof start === "number" &&
12
- typeof count === "number" &&
13
- typeof stepIndex === "number" &&
14
- Number.isInteger(start) &&
15
- Number.isInteger(count) &&
16
- Number.isInteger(stepIndex) &&
17
- start >= 0 &&
18
- count > 0 &&
19
- stepIndex >= 0 &&
20
- stepIndex < chainStepCount &&
21
- start + count <= stepCount
22
- );
23
- }
24
-
25
- export function normalizeParallelGroups(
26
- groups: unknown,
27
- stepCount: number,
28
- chainStepCount: number,
29
- ): AsyncParallelGroupStatus[] {
30
- if (!Array.isArray(groups)) return [];
31
- return groups
32
- .filter((group): group is AsyncParallelGroupStatus => isValidParallelGroup(group, stepCount, chainStepCount))
33
- .sort((left, right) => left.stepIndex - right.stepIndex || left.start - right.start);
34
- }
35
-
36
- export function flatToLogicalStepIndex(
37
- flatIndex: number,
38
- chainStepCount: number,
39
- groups: AsyncParallelGroupStatus[],
40
- ): number {
41
- let logicalIndex = 0;
42
- let cursor = 0;
43
- for (const group of groups) {
44
- while (cursor < group.start && logicalIndex < chainStepCount) {
45
- if (cursor === flatIndex) return logicalIndex;
46
- cursor++;
47
- logicalIndex++;
48
- }
49
- if (flatIndex >= group.start && flatIndex < group.start + group.count) return group.stepIndex;
50
- cursor = group.start + group.count;
51
- logicalIndex = group.stepIndex + 1;
52
- }
53
- while (cursor <= flatIndex && logicalIndex < chainStepCount) {
54
- if (cursor === flatIndex) return logicalIndex;
55
- cursor++;
56
- logicalIndex++;
57
- }
58
- return Math.max(0, chainStepCount - 1);
59
- }
@@ -1,300 +0,0 @@
1
- import { createHash } from "node:crypto";
2
- import * as fs from "node:fs";
3
- import * as path from "node:path";
4
- import {
5
- attachNestedChildrenToResultChildren,
6
- buildSubagentResultIntercomPayload,
7
- compactNestedResultChildren,
8
- deliverSubagentResultIntercomEvent,
9
- resolveSubagentResultStatus,
10
- } from "../../intercom/result-intercom.ts";
11
- import type { IntercomEventBus, SubagentResultIntercomChild, SubagentState } from "../../shared/types.ts";
12
- import { projectNestedRegistryForRoot } from "../shared/nested-events.ts";
13
- import { type CompletionClaimSnapshot, deliverClaimedCompletion } from "./completion-claims.js";
14
- import { buildCompletionKey, lookupSeenWithTtl, recordSeen } from "./completion-dedupe.ts";
15
- import { deliverLocalCompletionNotification } from "./completion-notification.ts";
16
- import {
17
- claimIdFromScheduleKey,
18
- claimPublicResult,
19
- claimScheduleKey,
20
- type FrozenCompletionEnvelope,
21
- loadResultClaim,
22
- type ResultClaimFs,
23
- type ResultFileClaim,
24
- readClaimData,
25
- removeResultClaim,
26
- updateResultClaim,
27
- } from "./result-file-claims.js";
28
- import { quarantineResultFile } from "./result-quarantine.js";
29
- import { modernResultHasTerminalStatus, type ResultStatusFs } from "./result-status.js";
30
- import { buildCompletionSignature, type ResultFileData, sanitizeNestedResultChildren } from "./result-watcher-data.js";
31
-
32
- export interface ResultProcessOutcome {
33
- status: "done" | "status-pending" | "delivery-retry" | "inactive";
34
- entry?: string;
35
- }
36
-
37
- export interface ResultProcessorFs extends ResultClaimFs, Partial<ResultStatusFs> {
38
- existsSync?: typeof fs.existsSync;
39
- unlinkSync?: typeof fs.unlinkSync;
40
- linkSync?: typeof fs.linkSync;
41
- copyFileSync?: typeof fs.copyFileSync;
42
- openSync?: typeof fs.openSync;
43
- readSync?: typeof fs.readSync;
44
- closeSync?: typeof fs.closeSync;
45
- }
46
-
47
- export interface ResultProcessorContext {
48
- pi: { events: IntercomEventBus };
49
- state: SubagentState;
50
- resultsDir: string;
51
- completionTtlMs: number;
52
- fsApi?: ResultProcessorFs;
53
- allowedStatusRoots?: string[];
54
- maxStatusBytes?: number;
55
- maxNoProgressFailures?: number;
56
- intercomTimeoutMs?: number | false;
57
- isActive: () => boolean;
58
- ownsResult: (data: ResultFileData) => boolean;
59
- }
60
-
61
- function statusFs(fsApi: ResultProcessorFs): ResultStatusFs {
62
- return {
63
- realpathSync: fsApi.realpathSync ?? fs.realpathSync,
64
- lstatSync: fsApi.lstatSync ?? fs.lstatSync,
65
- openSync: fsApi.openSync ?? fs.openSync,
66
- fstatSync: fsApi.fstatSync ?? fs.fstatSync,
67
- readSync: fsApi.readSync ?? fs.readSync,
68
- closeSync: fsApi.closeSync ?? fs.closeSync,
69
- };
70
- }
71
-
72
- function readPublic(resultsDir: string, file: string, fsApi: ResultProcessorFs): ResultFileData {
73
- return JSON.parse((fsApi.readFileSync ?? fs.readFileSync)(path.join(resultsDir, file), "utf-8")) as ResultFileData;
74
- }
75
-
76
- function markClaimState(claim: ResultFileClaim, value: CompletionClaimSnapshot, fsApi: ResultProcessorFs): void {
77
- updateResultClaim(
78
- claim,
79
- {
80
- intercomDelivered: value.intercomDelivered,
81
- localDelivered: value.localDelivered,
82
- noProgressFailures: value.noProgressFailures,
83
- envelope: value.envelope,
84
- ...(value.terminalStatus === "delivered" ? { state: "delivered" as const } : {}),
85
- ...(value.terminalStatus === "exhausted" ? { state: "undelivered" as const } : {}),
86
- },
87
- fsApi,
88
- );
89
- }
90
-
91
- function buildEnvelope(data: ResultFileData, resultPath: string, fsApi: ResultProcessorFs): FrozenCompletionEnvelope {
92
- const runId = data.runId ?? data.id ?? path.basename(resultPath, ".json");
93
- const explicitNested = data.nestedChildren !== undefined;
94
- let nestedChildren = compactNestedResultChildren(
95
- sanitizeNestedResultChildren(data.nestedChildren, resultPath, "nestedChildren"),
96
- );
97
- if (!nestedChildren?.length && !explicitNested)
98
- nestedChildren = compactNestedResultChildren(projectNestedRegistryForRoot(runId)?.children);
99
- const hasResults = Array.isArray(data.results) && data.results.length > 0;
100
- const resultChildren = hasResults
101
- ? data.results!
102
- : [{ agent: data.agent, output: data.summary, success: data.success }];
103
- const normalized = attachNestedChildrenToResultChildren(
104
- runId,
105
- resultChildren.map((result = {}, index): SubagentResultIntercomChild => {
106
- const baseOutput = result.output ?? data.summary;
107
- const hasOutput = typeof baseOutput === "string" && baseOutput.trim().length > 0;
108
- const output = hasOutput ? baseOutput : "(no output)";
109
- const summary =
110
- result.success === false && result.error
111
- ? `${result.error}${hasOutput ? `\n\nOutput:\n${baseOutput}` : ""}`
112
- : output;
113
- const sessionPath = result.sessionFile ?? (resultChildren.length === 1 ? data.sessionFile : undefined);
114
- const children = sanitizeNestedResultChildren(result.children, resultPath, `results[${index}].children`);
115
- return {
116
- agent: result.agent ?? data.agent ?? `step-${index + 1}`,
117
- status: resolveSubagentResultStatus({
118
- success: result.success,
119
- state: data.state === "paused" || typeof result.success !== "boolean" ? data.state : undefined,
120
- }),
121
- summary,
122
- index,
123
- artifactPath: result.artifactPaths?.outputPath,
124
- ...(typeof sessionPath === "string" && (fsApi.existsSync ?? fs.existsSync)(sessionPath)
125
- ? { sessionPath }
126
- : {}),
127
- ...(result.intercomTarget ? { intercomTarget: result.intercomTarget } : {}),
128
- ...(children ? { children } : {}),
129
- };
130
- }),
131
- nestedChildren,
132
- );
133
- const local: Record<string, unknown> = {
134
- ...data,
135
- runId,
136
- ...(nestedChildren?.length ? { nestedChildren } : {}),
137
- ...(Array.isArray(data.results)
138
- ? {
139
- results: hasResults
140
- ? normalized.map((child, index) => ({
141
- ...data.results![index],
142
- agent: child.agent,
143
- status: child.status,
144
- summary: child.summary,
145
- index: child.index,
146
- artifactPath: child.artifactPath,
147
- sessionPath: child.sessionPath,
148
- children: child.children,
149
- }))
150
- : [],
151
- }
152
- : {}),
153
- };
154
- const target = data.intercomTarget?.trim();
155
- const mode =
156
- data.mode === "single" || data.mode === "parallel" || data.mode === "chain"
157
- ? data.mode
158
- : resultChildren.length > 1
159
- ? "chain"
160
- : "single";
161
- return {
162
- local,
163
- ...(target
164
- ? {
165
- intercom: {
166
- to: target,
167
- runId,
168
- mode,
169
- source: "async",
170
- children: normalized,
171
- asyncId: data.id,
172
- asyncDir: data.asyncDir,
173
- },
174
- }
175
- : {}),
176
- };
177
- }
178
-
179
- function quarantineClaim(context: ResultProcessorContext, claim: ResultFileClaim): void {
180
- const fsApi = context.fsApi ?? fs;
181
- updateResultClaim(claim, { state: "undelivered" }, fsApi);
182
- quarantineResultFile(context.resultsDir, claim.meta.originalFile, claim.payloadPath, fsApi as typeof fs);
183
- removeResultClaim(claim, fsApi);
184
- }
185
-
186
- export async function processResultEntry(
187
- entry: string,
188
- context: ResultProcessorContext,
189
- ): Promise<ResultProcessOutcome> {
190
- const fsApi = context.fsApi ?? fs;
191
- let claim: ResultFileClaim | undefined;
192
- const existingClaimId = claimIdFromScheduleKey(entry);
193
- if (existingClaimId) {
194
- claim = loadResultClaim(context.resultsDir, existingClaimId, fsApi);
195
- if (!claim) return { status: "done" };
196
- if (claim.meta.state === "delivered") {
197
- removeResultClaim(claim, fsApi);
198
- return { status: "done" };
199
- }
200
- if (claim.meta.state === "undelivered") {
201
- quarantineClaim(context, claim);
202
- return { status: "done" };
203
- }
204
- } else {
205
- const publicPath = path.join(context.resultsDir, entry);
206
- if (!(fsApi.existsSync ?? fs.existsSync)(publicPath)) return { status: "done" };
207
- const preliminary = readPublic(context.resultsDir, entry, fsApi);
208
- if (!context.ownsResult(preliminary)) return { status: "inactive" };
209
- if (
210
- !modernResultHasTerminalStatus(preliminary, statusFs(fsApi), {
211
- allowedRoots: context.allowedStatusRoots,
212
- maxBytes: context.maxStatusBytes,
213
- })
214
- )
215
- return { status: "status-pending", entry };
216
- claim = claimPublicResult(context.resultsDir, entry, fsApi);
217
- }
218
-
219
- const { data } = readClaimData(claim, fsApi);
220
- if (!context.ownsResult(data)) return { status: "inactive" };
221
- if (
222
- !modernResultHasTerminalStatus(data, statusFs(fsApi), {
223
- allowedRoots: context.allowedStatusRoots,
224
- maxBytes: context.maxStatusBytes,
225
- })
226
- )
227
- return { status: "status-pending", entry: claim ? claimScheduleKey(claim.id) : entry };
228
- const sourceSignature = buildCompletionSignature({ source: data });
229
- if (claim.meta.sourceSignature && claim.meta.sourceSignature !== sourceSignature) {
230
- quarantineClaim(context, claim);
231
- return { status: "done" };
232
- }
233
- let envelope = claim.meta.envelope;
234
- if (!envelope) {
235
- envelope = buildEnvelope(data, claim.payloadPath, fsApi);
236
- updateResultClaim(claim, { sourceSignature, envelope }, fsApi);
237
- }
238
- const completionKey = buildCompletionKey(data, `result:${claim.meta.originalFile}`);
239
- const claimKey = `${path.resolve(context.resultsDir)}:${completionKey}`;
240
- const stableHash = createHash("sha256").update(`${claimKey}:${sourceSignature}`).digest("hex");
241
- const seen = lookupSeenWithTtl(
242
- context.state.completionSeen,
243
- completionKey,
244
- sourceSignature,
245
- Date.now(),
246
- context.completionTtlMs,
247
- );
248
- if (seen === "match") {
249
- updateResultClaim(claim, { state: "delivered" }, fsApi);
250
- removeResultClaim(claim, fsApi);
251
- return { status: "done" };
252
- }
253
- if (seen === "conflict") {
254
- quarantineClaim(context, claim);
255
- return { status: "done" };
256
- }
257
- const claimResult = await deliverClaimedCompletion(
258
- claimKey,
259
- sourceSignature,
260
- context.completionTtlMs,
261
- {
262
- isOwned: () => context.isActive() && context.ownsResult(data),
263
- onState: (value) => markClaimState(claim!, value, fsApi),
264
- intercom: envelope.intercom
265
- ? async (frozen) => {
266
- const payload = buildSubagentResultIntercomPayload(frozen.intercom!);
267
- payload.requestId = `completion-${stableHash}`;
268
- return deliverSubagentResultIntercomEvent(
269
- context.pi.events,
270
- payload,
271
- context.intercomTimeoutMs ?? 500,
272
- );
273
- }
274
- : undefined,
275
- local: async (frozen) =>
276
- deliverLocalCompletionNotification(context.pi.events, frozen.local, `completion-notify-${stableHash}`),
277
- },
278
- context.maxNoProgressFailures,
279
- {
280
- envelope: claim.meta.envelope,
281
- intercomDelivered: claim.meta.intercomDelivered,
282
- localDelivered: claim.meta.localDelivered,
283
- noProgressFailures: claim.meta.noProgressFailures,
284
- },
285
- envelope,
286
- );
287
- if (claimResult.status === "retry" || claimResult.status === "released") {
288
- return context.isActive() && context.ownsResult(data)
289
- ? { status: "delivery-retry", entry: claimScheduleKey(claim.id) }
290
- : { status: "inactive" };
291
- }
292
- if (claimResult.status === "conflict" || claimResult.status === "exhausted") {
293
- quarantineClaim(context, claim);
294
- return { status: "done" };
295
- }
296
- recordSeen(context.state.completionSeen, completionKey, Date.now(), sourceSignature);
297
- updateResultClaim(claim, { state: "delivered" }, fsApi);
298
- removeResultClaim(claim, fsApi);
299
- return { status: "done" };
300
- }
@@ -1,166 +0,0 @@
1
- import { randomUUID } from "node:crypto";
2
- import * as fs from "node:fs";
3
- import * as path from "node:path";
4
- import type { GroupedResultIntercomMessageInput } from "../../intercom/result-intercom.ts";
5
- import type { ResultFileData } from "./result-watcher-data.js";
6
-
7
- const CLAIMS_DIR = ".claims";
8
- const CLAIM_SCHEDULE_PREFIX = "@claim/";
9
-
10
- export interface FrozenCompletionEnvelope {
11
- local: Record<string, unknown>;
12
- intercom?: GroupedResultIntercomMessageInput;
13
- }
14
- export interface ResultFileClaimMeta {
15
- version: 1;
16
- id: string;
17
- originalFile: string;
18
- state: "active" | "delivered" | "undelivered";
19
- createdAt: number;
20
- sourceSignature?: string;
21
- envelope?: FrozenCompletionEnvelope;
22
- intercomDelivered?: boolean;
23
- localDelivered?: boolean;
24
- noProgressFailures?: number;
25
- }
26
-
27
- export interface ResultFileClaim {
28
- id: string;
29
- dir: string;
30
- payloadPath: string;
31
- metaPath: string;
32
- meta: ResultFileClaimMeta;
33
- }
34
-
35
- export interface ResultClaimFs {
36
- mkdirSync?: typeof fs.mkdirSync;
37
- renameSync?: typeof fs.renameSync;
38
- readFileSync?: typeof fs.readFileSync;
39
- writeFileSync?: typeof fs.writeFileSync;
40
- readdirSync?: typeof fs.readdirSync;
41
- rmSync?: typeof fs.rmSync;
42
- rmdirSync?: typeof fs.rmdirSync;
43
- }
44
-
45
- function api(fsApi: ResultClaimFs) {
46
- return {
47
- mkdirSync: fsApi.mkdirSync ?? fs.mkdirSync,
48
- renameSync: fsApi.renameSync ?? fs.renameSync,
49
- readFileSync: fsApi.readFileSync ?? fs.readFileSync,
50
- writeFileSync: fsApi.writeFileSync ?? fs.writeFileSync,
51
- rmSync: fsApi.rmSync ?? fs.rmSync,
52
- rmdirSync: fsApi.rmdirSync ?? fs.rmdirSync,
53
- };
54
- }
55
-
56
- export function claimScheduleKey(id: string): string {
57
- return `${CLAIM_SCHEDULE_PREFIX}${id}`;
58
- }
59
-
60
- export function claimIdFromScheduleKey(key: string): string | undefined {
61
- return key.startsWith(CLAIM_SCHEDULE_PREFIX) ? key.slice(CLAIM_SCHEDULE_PREFIX.length) : undefined;
62
- }
63
-
64
- export function loadResultClaim(
65
- resultsDir: string,
66
- id: string,
67
- fsApi: ResultClaimFs = fs,
68
- ): ResultFileClaim | undefined {
69
- try {
70
- const dir = path.join(resultsDir, CLAIMS_DIR, id);
71
- const metaPath = path.join(dir, "claim.json");
72
- const payloadPath = path.join(dir, "result.json");
73
- const meta = JSON.parse((fsApi.readFileSync ?? fs.readFileSync)(metaPath, "utf-8")) as ResultFileClaimMeta;
74
- if (meta.version !== 1 || meta.id !== id || !path.basename(meta.originalFile)) return undefined;
75
- return { id, dir, payloadPath, metaPath, meta };
76
- } catch {
77
- return undefined;
78
- }
79
- }
80
-
81
- export function listResultClaims(resultsDir: string, fsApi: ResultClaimFs = fs): ResultFileClaim[] {
82
- const claimsDir = path.join(resultsDir, CLAIMS_DIR);
83
- try {
84
- return (fsApi.readdirSync ?? fs.readdirSync)(claimsDir, { withFileTypes: true })
85
- .filter((entry) => entry.isDirectory())
86
- .map((entry) => loadResultClaim(resultsDir, entry.name, fsApi))
87
- .filter((claim): claim is ResultFileClaim => Boolean(claim));
88
- } catch {
89
- return [];
90
- }
91
- }
92
-
93
- export function claimPublicResult(
94
- resultsDir: string,
95
- file: string,
96
- fsApi: ResultClaimFs = fs,
97
- createId: () => string = randomUUID,
98
- ): ResultFileClaim {
99
- const f = api(fsApi);
100
- const claimsDir = path.join(resultsDir, CLAIMS_DIR);
101
- f.mkdirSync(claimsDir, { recursive: true });
102
- for (let attempt = 0; attempt < 16; attempt += 1) {
103
- const id = createId();
104
- const dir = path.join(claimsDir, id);
105
- try {
106
- f.mkdirSync(dir);
107
- } catch (error) {
108
- if ((error as NodeJS.ErrnoException).code === "EEXIST") continue;
109
- throw error;
110
- }
111
- const metaPath = path.join(dir, "claim.json");
112
- const payloadPath = path.join(dir, "result.json");
113
- const meta: ResultFileClaimMeta = {
114
- version: 1,
115
- id,
116
- originalFile: path.basename(file),
117
- state: "active",
118
- createdAt: Date.now(),
119
- };
120
- try {
121
- f.writeFileSync(metaPath, `${JSON.stringify(meta)}\n`, "utf-8");
122
- f.renameSync(path.join(resultsDir, file), payloadPath);
123
- return { id, dir, payloadPath, metaPath, meta };
124
- } catch (error) {
125
- f.rmSync(dir, { recursive: true, force: true });
126
- throw error;
127
- }
128
- }
129
- throw new Error(`Unable to allocate a result claim for '${file}'`);
130
- }
131
-
132
- export function readClaimData(
133
- claim: ResultFileClaim,
134
- fsApi: ResultClaimFs = fs,
135
- ): { raw: string; data: ResultFileData } {
136
- const raw = (fsApi.readFileSync ?? fs.readFileSync)(claim.payloadPath, "utf-8");
137
- return { raw, data: JSON.parse(raw) as ResultFileData };
138
- }
139
-
140
- export function updateResultClaim(
141
- claim: ResultFileClaim,
142
- patch: Partial<Omit<ResultFileClaimMeta, "version" | "id" | "originalFile" | "createdAt">>,
143
- fsApi: ResultClaimFs = fs,
144
- ): void {
145
- const f = api(fsApi);
146
- claim.meta = { ...claim.meta, ...patch };
147
- const temporary = path.join(claim.dir, `.claim-${randomUUID()}.tmp`);
148
- f.writeFileSync(temporary, `${JSON.stringify(claim.meta)}\n`, "utf-8");
149
- f.renameSync(temporary, claim.metaPath);
150
- }
151
-
152
- export function removeResultClaim(claim: ResultFileClaim, fsApi: ResultClaimFs = fs): void {
153
- const f = api(fsApi);
154
- f.rmSync(claim.dir, { recursive: true, force: true });
155
- try {
156
- f.rmdirSync(path.dirname(claim.dir));
157
- } catch {
158
- /* another active claim keeps the shared directory */
159
- }
160
- }
161
-
162
- export function recoverTerminalClaim(claim: ResultFileClaim, fsApi: ResultClaimFs = fs): "active" | "removed" {
163
- if (claim.meta.state === "active") return "active";
164
- removeResultClaim(claim, fsApi);
165
- return "removed";
166
- }
@@ -1,77 +0,0 @@
1
- import { createHash, randomUUID } from "node:crypto";
2
- import * as fs from "node:fs";
3
- import * as path from "node:path";
4
- import {
5
- type ExclusivePublicationFs,
6
- errorCode,
7
- publishFileExclusive,
8
- unlinkIfPresent,
9
- } from "../../shared/exclusive-file-publication.js";
10
-
11
- interface QuarantineFs extends ExclusivePublicationFs {
12
- mkdirSync: typeof fs.mkdirSync;
13
- existsSync?: typeof fs.existsSync;
14
- openSync?: typeof fs.openSync;
15
- readSync?: typeof fs.readSync;
16
- closeSync?: typeof fs.closeSync;
17
- }
18
-
19
- function hashFile(file: string, fsApi: QuarantineFs): string {
20
- const hash = createHash("sha256");
21
- const open = fsApi.openSync ?? fs.openSync;
22
- const read = fsApi.readSync ?? fs.readSync;
23
- const close = fsApi.closeSync ?? fs.closeSync;
24
- const handle = open(file, "r");
25
- try {
26
- const chunk = Buffer.allocUnsafe(64 * 1024);
27
- let offset = 0;
28
- while (true) {
29
- const count = read(handle, chunk, 0, chunk.length, offset);
30
- if (count === 0) break;
31
- hash.update(chunk.subarray(0, count));
32
- offset += count;
33
- }
34
- } finally {
35
- close(handle);
36
- }
37
- return hash.digest("hex");
38
- }
39
-
40
- function stableClaimId(resultPath: string): string {
41
- const parent = path.dirname(resultPath);
42
- if (path.basename(path.dirname(parent)) === ".claims") return path.basename(parent);
43
- return createHash("sha256").update(path.resolve(resultPath)).digest("hex").slice(0, 16);
44
- }
45
-
46
- /** Retain one exact claimed result without overwriting or duplicating prior evidence. */
47
- export function quarantineResultFile(
48
- resultsDir: string,
49
- file: string,
50
- resultPath: string,
51
- fsApi: QuarantineFs = fs,
52
- createId: () => string = randomUUID,
53
- ): string {
54
- const quarantineDir = path.join(resultsDir, ".undelivered");
55
- fsApi.mkdirSync(quarantineDir, { recursive: true });
56
- const stem = path.basename(file, path.extname(file)).replace(/[^a-zA-Z0-9._-]/g, "_");
57
- const contentHash = hashFile(resultPath, fsApi);
58
- const claimId = stableClaimId(resultPath);
59
- for (let attempt = 0; attempt < 16; attempt += 1) {
60
- const suffix = attempt === 0 ? claimId : createId();
61
- const destination = path.join(quarantineDir, `${stem}-${suffix}-${contentHash.slice(0, 20)}.json`);
62
- const publication = publishFileExclusive(resultPath, destination, fsApi);
63
- if (publication === "exists") {
64
- try {
65
- if (hashFile(destination, fsApi) !== contentHash) continue;
66
- } catch (error) {
67
- if (errorCode(error) === "ENOENT") continue;
68
- throw error;
69
- }
70
- }
71
- // If cleanup fails, the deterministic destination remains authoritative.
72
- // A retry recognizes it and retries only this unlink instead of copying again.
73
- unlinkIfPresent(resultPath, fsApi);
74
- return destination;
75
- }
76
- throw new Error(`Unable to allocate a collision-free quarantine path for '${resultPath}'`);
77
- }