clinkx 1.0.0 → 1.1.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 (333) hide show
  1. package/CHANGELOG.md +95 -0
  2. package/README.md +184 -112
  3. package/clinkx-workflows/dist/chairman-knobs.d.ts +24 -0
  4. package/clinkx-workflows/dist/chairman-knobs.js +27 -0
  5. package/clinkx-workflows/dist/chairman-knobs.js.map +1 -0
  6. package/clinkx-workflows/dist/clink-client.d.ts +10 -0
  7. package/clinkx-workflows/dist/clink-client.js +39 -1
  8. package/clinkx-workflows/dist/clink-client.js.map +1 -1
  9. package/clinkx-workflows/dist/config.d.ts +29 -1
  10. package/clinkx-workflows/dist/config.js +35 -2
  11. package/clinkx-workflows/dist/config.js.map +1 -1
  12. package/clinkx-workflows/dist/context-preflight.d.ts +5 -0
  13. package/clinkx-workflows/dist/context-preflight.js.map +1 -1
  14. package/clinkx-workflows/dist/council-execution-policy.d.ts +68 -0
  15. package/clinkx-workflows/dist/council-execution-policy.js +110 -0
  16. package/clinkx-workflows/dist/council-execution-policy.js.map +1 -0
  17. package/clinkx-workflows/dist/council-expander.d.ts +9 -8
  18. package/clinkx-workflows/dist/council-expander.js +18 -9
  19. package/clinkx-workflows/dist/council-expander.js.map +1 -1
  20. package/clinkx-workflows/dist/dashboard/activity-contract.d.ts +5557 -4
  21. package/clinkx-workflows/dist/dashboard/activity-contract.js +55 -3
  22. package/clinkx-workflows/dist/dashboard/activity-contract.js.map +1 -1
  23. package/clinkx-workflows/dist/dashboard/activity-reader.d.ts +9 -5
  24. package/clinkx-workflows/dist/dashboard/activity-reader.js +15 -2
  25. package/clinkx-workflows/dist/dashboard/activity-reader.js.map +1 -1
  26. package/clinkx-workflows/dist/dashboard/api-routes.d.ts +4 -0
  27. package/clinkx-workflows/dist/dashboard/api-routes.js +133 -25
  28. package/clinkx-workflows/dist/dashboard/api-routes.js.map +1 -1
  29. package/clinkx-workflows/dist/dashboard/context-builder-invocation-contract.d.ts +3865 -0
  30. package/clinkx-workflows/dist/dashboard/context-builder-invocation-contract.js +69 -0
  31. package/clinkx-workflows/dist/dashboard/context-builder-invocation-contract.js.map +1 -0
  32. package/clinkx-workflows/dist/dashboard/context-run-reader.d.ts +36 -2
  33. package/clinkx-workflows/dist/dashboard/context-run-reader.js +142 -19
  34. package/clinkx-workflows/dist/dashboard/context-run-reader.js.map +1 -1
  35. package/clinkx-workflows/dist/dashboard/contracts.d.ts +20027 -480
  36. package/clinkx-workflows/dist/dashboard/contracts.js +133 -6
  37. package/clinkx-workflows/dist/dashboard/contracts.js.map +1 -1
  38. package/clinkx-workflows/dist/dashboard/index.d.ts +4 -0
  39. package/clinkx-workflows/dist/dashboard/index.js +4 -0
  40. package/clinkx-workflows/dist/dashboard/index.js.map +1 -1
  41. package/clinkx-workflows/dist/dashboard/invocation-contract.d.ts +786 -0
  42. package/clinkx-workflows/dist/dashboard/invocation-contract.js +201 -0
  43. package/clinkx-workflows/dist/dashboard/invocation-contract.js.map +1 -0
  44. package/clinkx-workflows/dist/dashboard/lazy-content.d.ts +14 -1
  45. package/clinkx-workflows/dist/dashboard/lazy-content.js +53 -2
  46. package/clinkx-workflows/dist/dashboard/lazy-content.js.map +1 -1
  47. package/clinkx-workflows/dist/dashboard/native-transcript/contract.d.ts +90 -0
  48. package/clinkx-workflows/dist/dashboard/native-transcript/contract.js +31 -0
  49. package/clinkx-workflows/dist/dashboard/native-transcript/contract.js.map +1 -0
  50. package/clinkx-workflows/dist/dashboard/native-transcript/projection.d.ts +12 -0
  51. package/clinkx-workflows/dist/dashboard/native-transcript/projection.js +286 -0
  52. package/clinkx-workflows/dist/dashboard/native-transcript/projection.js.map +1 -0
  53. package/clinkx-workflows/dist/dashboard/progress-report-contract.d.ts +147 -4
  54. package/clinkx-workflows/dist/dashboard/progress-report-contract.js +11 -11
  55. package/clinkx-workflows/dist/dashboard/progress-report-contract.js.map +1 -1
  56. package/clinkx-workflows/dist/dashboard/progress-report-reader.js +4 -56
  57. package/clinkx-workflows/dist/dashboard/progress-report-reader.js.map +1 -1
  58. package/clinkx-workflows/dist/dashboard/progress-report-status-store.d.ts +2 -1
  59. package/clinkx-workflows/dist/dashboard/progress-report-status-store.js +6 -5
  60. package/clinkx-workflows/dist/dashboard/progress-report-status-store.js.map +1 -1
  61. package/clinkx-workflows/dist/dashboard/prompt-builder-library-contract.d.ts +194 -0
  62. package/clinkx-workflows/dist/dashboard/prompt-builder-library-contract.js +89 -0
  63. package/clinkx-workflows/dist/dashboard/prompt-builder-library-contract.js.map +1 -0
  64. package/clinkx-workflows/dist/dashboard/prompt-builder-library-store.d.ts +26 -0
  65. package/clinkx-workflows/dist/dashboard/prompt-builder-library-store.js +102 -0
  66. package/clinkx-workflows/dist/dashboard/prompt-builder-library-store.js.map +1 -0
  67. package/clinkx-workflows/dist/dashboard/request-admission.js +4 -0
  68. package/clinkx-workflows/dist/dashboard/request-admission.js.map +1 -1
  69. package/clinkx-workflows/dist/dashboard/scanner.d.ts +3 -0
  70. package/clinkx-workflows/dist/dashboard/scanner.js +52 -8
  71. package/clinkx-workflows/dist/dashboard/scanner.js.map +1 -1
  72. package/clinkx-workflows/dist/dashboard/server-factory.d.ts +14 -1
  73. package/clinkx-workflows/dist/dashboard/server-factory.js +73 -21
  74. package/clinkx-workflows/dist/dashboard/server-factory.js.map +1 -1
  75. package/clinkx-workflows/dist/dashboard/session-log-contract.d.ts +223 -56
  76. package/clinkx-workflows/dist/dashboard/session-log-contract.js +26 -0
  77. package/clinkx-workflows/dist/dashboard/session-log-contract.js.map +1 -1
  78. package/clinkx-workflows/dist/dashboard/session-log-journal.js +10 -3
  79. package/clinkx-workflows/dist/dashboard/session-log-journal.js.map +1 -1
  80. package/clinkx-workflows/dist/dashboard/session-log-redaction.d.ts +9 -1
  81. package/clinkx-workflows/dist/dashboard/session-log-redaction.js +793 -59
  82. package/clinkx-workflows/dist/dashboard/session-log-redaction.js.map +1 -1
  83. package/clinkx-workflows/dist/dashboard-ui/assets/index-JPHQVtn9.js +106 -0
  84. package/clinkx-workflows/dist/dashboard-ui/assets/style-DOMHk-j6.css +1 -0
  85. package/clinkx-workflows/dist/dashboard-ui/index.html +2 -2
  86. package/clinkx-workflows/dist/delegate-finalizer-policy.d.ts +26 -0
  87. package/clinkx-workflows/dist/delegate-finalizer-policy.js +72 -0
  88. package/clinkx-workflows/dist/delegate-finalizer-policy.js.map +1 -0
  89. package/clinkx-workflows/dist/delegate-signature.d.ts +14 -0
  90. package/clinkx-workflows/dist/delegate-signature.js +14 -0
  91. package/clinkx-workflows/dist/delegate-signature.js.map +1 -0
  92. package/clinkx-workflows/dist/delegate-validate.d.ts +32 -2
  93. package/clinkx-workflows/dist/delegate-validate.js +51 -0
  94. package/clinkx-workflows/dist/delegate-validate.js.map +1 -1
  95. package/clinkx-workflows/dist/engine.d.ts +113 -3
  96. package/clinkx-workflows/dist/engine.js +476 -80
  97. package/clinkx-workflows/dist/engine.js.map +1 -1
  98. package/clinkx-workflows/dist/errors.d.ts +22 -2
  99. package/clinkx-workflows/dist/errors.js +30 -2
  100. package/clinkx-workflows/dist/errors.js.map +1 -1
  101. package/clinkx-workflows/dist/fidelity.d.ts +47 -1
  102. package/clinkx-workflows/dist/fidelity.js +54 -1
  103. package/clinkx-workflows/dist/fidelity.js.map +1 -1
  104. package/clinkx-workflows/dist/fingerprint.d.ts +24 -6
  105. package/clinkx-workflows/dist/fingerprint.js +32 -9
  106. package/clinkx-workflows/dist/fingerprint.js.map +1 -1
  107. package/clinkx-workflows/dist/index.js +53 -1
  108. package/clinkx-workflows/dist/index.js.map +1 -1
  109. package/clinkx-workflows/dist/logger.d.ts +1 -1
  110. package/clinkx-workflows/dist/logger.js +17 -3
  111. package/clinkx-workflows/dist/logger.js.map +1 -1
  112. package/clinkx-workflows/dist/monotonic-clock.d.ts +19 -0
  113. package/clinkx-workflows/dist/monotonic-clock.js +22 -0
  114. package/clinkx-workflows/dist/monotonic-clock.js.map +1 -0
  115. package/clinkx-workflows/dist/plan-review/contract.d.ts +52 -52
  116. package/clinkx-workflows/dist/plan-review/publication.d.ts +4 -4
  117. package/clinkx-workflows/dist/plan-review/run-state.d.ts +16 -16
  118. package/clinkx-workflows/dist/roster-contract.d.ts +889 -0
  119. package/clinkx-workflows/dist/roster-contract.js +180 -0
  120. package/clinkx-workflows/dist/roster-contract.js.map +1 -0
  121. package/clinkx-workflows/dist/rosters.d.ts +14 -0
  122. package/clinkx-workflows/dist/rosters.js +169 -0
  123. package/clinkx-workflows/dist/rosters.js.map +1 -0
  124. package/clinkx-workflows/dist/run-health.d.ts +7 -0
  125. package/clinkx-workflows/dist/run-health.js +25 -0
  126. package/clinkx-workflows/dist/run-health.js.map +1 -0
  127. package/clinkx-workflows/dist/schema.d.ts +64 -0
  128. package/clinkx-workflows/dist/schema.js +24 -0
  129. package/clinkx-workflows/dist/schema.js.map +1 -1
  130. package/clinkx-workflows/dist/server.js +2 -0
  131. package/clinkx-workflows/dist/server.js.map +1 -1
  132. package/clinkx-workflows/dist/shutdown.d.ts +15 -2
  133. package/clinkx-workflows/dist/shutdown.js +60 -16
  134. package/clinkx-workflows/dist/shutdown.js.map +1 -1
  135. package/clinkx-workflows/dist/state-schema.d.ts +23 -0
  136. package/clinkx-workflows/dist/state.d.ts +5 -0
  137. package/clinkx-workflows/dist/state.js +128 -21
  138. package/clinkx-workflows/dist/state.js.map +1 -1
  139. package/clinkx-workflows/dist/stdio-watchdog.d.ts +52 -0
  140. package/clinkx-workflows/dist/stdio-watchdog.js +212 -0
  141. package/clinkx-workflows/dist/stdio-watchdog.js.map +1 -0
  142. package/clinkx-workflows/dist/types.d.ts +6 -0
  143. package/clinkx-workflows/dist/validators/council.d.ts +50 -50
  144. package/clinkx-workflows/dist/validators/plan-review.d.ts +3 -3
  145. package/clinkx-workflows/dist/workflow-tools.d.ts +70 -0
  146. package/clinkx-workflows/dist/workflow-tools.js +760 -168
  147. package/clinkx-workflows/dist/workflow-tools.js.map +1 -1
  148. package/clinkx-workflows/templates/council/answer-chairman.txt +3 -0
  149. package/clinkx-workflows/templates/council/answer-cross-reviewer.txt +3 -0
  150. package/clinkx-workflows/templates/council/answer-specialist.txt +3 -0
  151. package/clinkx-workflows/templates/council/chairman.txt +3 -0
  152. package/clinkx-workflows/templates/council/code-review-chairman.txt +3 -0
  153. package/clinkx-workflows/templates/council/code-review-cross-reviewer.txt +3 -0
  154. package/clinkx-workflows/templates/council/code-review-reviewer.txt +3 -0
  155. package/clinkx-workflows/templates/council/debug-chairman.txt +3 -0
  156. package/clinkx-workflows/templates/council/debug-cross-reviewer.txt +3 -0
  157. package/clinkx-workflows/templates/council/debug-debugger.txt +3 -0
  158. package/clinkx-workflows/templates/council/discover-analyst.txt +3 -0
  159. package/clinkx-workflows/templates/council/discover-chairman.txt +3 -0
  160. package/clinkx-workflows/templates/council/discover-cross-reviewer.txt +3 -0
  161. package/clinkx-workflows/templates/council/member.txt +3 -0
  162. package/clinkx-workflows/templates/council/plan-review-chairman.txt +3 -0
  163. package/clinkx-workflows/templates/council/plan-review-cross-reviewer.txt +3 -0
  164. package/clinkx-workflows/templates/council/plan-review-reviewer.txt +3 -0
  165. package/clinkx-workflows/templates/council/reviewer.txt +3 -0
  166. package/clinkx-workflows/workflows/council-answer.yaml +3 -0
  167. package/clinkx-workflows/workflows/council-code-review.yaml +3 -0
  168. package/clinkx-workflows/workflows/council-debug.yaml +3 -0
  169. package/clinkx-workflows/workflows/council-default.yaml +7 -0
  170. package/clinkx-workflows/workflows/council-discover.yaml +3 -0
  171. package/clinkx-workflows/workflows/council-plan-review.yaml +3 -0
  172. package/conf/adapters/agy.json +9 -2
  173. package/conf/adapters/claude.json +7 -0
  174. package/conf/adapters/codex.json +7 -0
  175. package/conf/adapters/glm.json +19 -3
  176. package/conf/adapters/grok.json +9 -2
  177. package/conf/adapters/hapi/agy.json +9 -2
  178. package/conf/adapters/hapi/claude.json +7 -0
  179. package/conf/adapters/hapi/codex.json +7 -0
  180. package/conf/adapters/hapi/glm.json +20 -4
  181. package/conf/rosters/default-council.json +21 -0
  182. package/dist/activity-journal.d.ts +17 -3
  183. package/dist/activity-journal.js +404 -79
  184. package/dist/activity-journal.js.map +1 -1
  185. package/dist/codex/agent-tools.js +36 -0
  186. package/dist/codex/agent-tools.js.map +1 -1
  187. package/dist/codex/app-server-client.d.ts +3 -0
  188. package/dist/codex/app-server-client.js +11 -0
  189. package/dist/codex/app-server-client.js.map +1 -1
  190. package/dist/codex/app-server-registry.d.ts +18 -0
  191. package/dist/codex/app-server-registry.js +88 -2
  192. package/dist/codex/app-server-registry.js.map +1 -1
  193. package/dist/codex/client-types.d.ts +7 -1
  194. package/dist/codex/protocol.d.ts +4 -4
  195. package/dist/config.d.ts +220 -23
  196. package/dist/config.js +104 -1
  197. package/dist/config.js.map +1 -1
  198. package/dist/context-builder/bundle.d.ts +13 -0
  199. package/dist/context-builder/bundle.js +46 -0
  200. package/dist/context-builder/bundle.js.map +1 -1
  201. package/dist/context-builder/delegate-finalizer-effective.d.ts +33 -0
  202. package/dist/context-builder/delegate-finalizer-effective.js +82 -0
  203. package/dist/context-builder/delegate-finalizer-effective.js.map +1 -0
  204. package/dist/context-builder/delegate-packet.d.ts +18 -0
  205. package/dist/context-builder/delegate-packet.js +73 -9
  206. package/dist/context-builder/delegate-packet.js.map +1 -1
  207. package/dist/context-builder/delegate-reconstruction.d.ts +29 -0
  208. package/dist/context-builder/delegate-reconstruction.js +110 -41
  209. package/dist/context-builder/delegate-reconstruction.js.map +1 -1
  210. package/dist/context-builder/delegate-validation.d.ts +31 -0
  211. package/dist/context-builder/delegate-validation.js +87 -10
  212. package/dist/context-builder/delegate-validation.js.map +1 -1
  213. package/dist/context-builder/delegate.d.ts +9 -0
  214. package/dist/context-builder/delegate.js +210 -59
  215. package/dist/context-builder/delegate.js.map +1 -1
  216. package/dist/context-builder/diagnostics.d.ts +1 -1
  217. package/dist/context-builder/diagnostics.js.map +1 -1
  218. package/dist/context-builder/discovery-progress.d.ts +25 -0
  219. package/dist/context-builder/discovery-progress.js +64 -0
  220. package/dist/context-builder/discovery-progress.js.map +1 -0
  221. package/dist/context-builder/discovery-watchdog.d.ts +15 -0
  222. package/dist/context-builder/discovery-watchdog.js +39 -0
  223. package/dist/context-builder/discovery-watchdog.js.map +1 -0
  224. package/dist/context-builder/discovery.d.ts +7 -0
  225. package/dist/context-builder/discovery.js +119 -7
  226. package/dist/context-builder/discovery.js.map +1 -1
  227. package/dist/context-builder/external-gaps.d.ts +73 -0
  228. package/dist/context-builder/external-gaps.js +248 -0
  229. package/dist/context-builder/external-gaps.js.map +1 -0
  230. package/dist/context-builder/globs.d.ts +1 -0
  231. package/dist/context-builder/globs.js +15 -0
  232. package/dist/context-builder/globs.js.map +1 -1
  233. package/dist/context-builder/handler.d.ts +42 -1
  234. package/dist/context-builder/handler.js +690 -62
  235. package/dist/context-builder/handler.js.map +1 -1
  236. package/dist/context-builder/invocation-sidecar.d.ts +41 -0
  237. package/dist/context-builder/invocation-sidecar.js +189 -0
  238. package/dist/context-builder/invocation-sidecar.js.map +1 -0
  239. package/dist/context-builder/lifecycle-contract.d.ts +51 -51
  240. package/dist/context-builder/mutation-lock.d.ts +32 -0
  241. package/dist/context-builder/mutation-lock.js +321 -0
  242. package/dist/context-builder/mutation-lock.js.map +1 -0
  243. package/dist/context-builder/review-coverage.d.ts +32 -0
  244. package/dist/context-builder/review-coverage.js +130 -23
  245. package/dist/context-builder/review-coverage.js.map +1 -1
  246. package/dist/context-builder/schema.d.ts +76 -1
  247. package/dist/context-builder/schema.js +21 -1
  248. package/dist/context-builder/schema.js.map +1 -1
  249. package/dist/context-builder/secrets.d.ts +31 -0
  250. package/dist/context-builder/secrets.js +871 -84
  251. package/dist/context-builder/secrets.js.map +1 -1
  252. package/dist/context-builder/status.d.ts +1 -1
  253. package/dist/context-builder/status.js +1 -0
  254. package/dist/context-builder/status.js.map +1 -1
  255. package/dist/context-builder/storage.d.ts +29 -5
  256. package/dist/context-builder/storage.js +311 -236
  257. package/dist/context-builder/storage.js.map +1 -1
  258. package/dist/context-builder/versions.d.ts +19 -4
  259. package/dist/context-builder/versions.js +19 -4
  260. package/dist/context-builder/versions.js.map +1 -1
  261. package/dist/context-builder/workflow-runner.d.ts +6 -0
  262. package/dist/context-builder/workflow-runner.js +363 -173
  263. package/dist/context-builder/workflow-runner.js.map +1 -1
  264. package/dist/dashboard-adapter-catalog.js +7 -0
  265. package/dist/dashboard-adapter-catalog.js.map +1 -1
  266. package/dist/dashboard-cli.d.ts +2 -0
  267. package/dist/dashboard-cli.js +40 -2
  268. package/dist/dashboard-cli.js.map +1 -1
  269. package/dist/dashboard-companion/client.d.ts +1 -0
  270. package/dist/dashboard-companion/client.js +22 -3
  271. package/dist/dashboard-companion/client.js.map +1 -1
  272. package/dist/dashboard-companion/protocol.js +4 -1
  273. package/dist/dashboard-companion/protocol.js.map +1 -1
  274. package/dist/dashboard-gateway/index.d.ts +1 -0
  275. package/dist/dashboard-gateway/index.js +33 -2
  276. package/dist/dashboard-gateway/index.js.map +1 -1
  277. package/dist/handler.js +5 -1
  278. package/dist/handler.js.map +1 -1
  279. package/dist/index.d.ts +18 -1
  280. package/dist/index.js +145 -7
  281. package/dist/index.js.map +1 -1
  282. package/dist/logger.d.ts +1 -1
  283. package/dist/logger.js +18 -3
  284. package/dist/logger.js.map +1 -1
  285. package/dist/parsers/agy-stream-json.d.ts +14 -0
  286. package/dist/parsers/agy-stream-json.js +172 -0
  287. package/dist/parsers/agy-stream-json.js.map +1 -0
  288. package/dist/parsers/extract.js +5 -1
  289. package/dist/parsers/extract.js.map +1 -1
  290. package/dist/parsers/observed-invocation.d.ts +20 -0
  291. package/dist/parsers/observed-invocation.js +42 -0
  292. package/dist/parsers/observed-invocation.js.map +1 -0
  293. package/dist/pipeline.d.ts +13 -0
  294. package/dist/pipeline.js +60 -10
  295. package/dist/pipeline.js.map +1 -1
  296. package/dist/progress-report/lock.d.ts +74 -0
  297. package/dist/progress-report/lock.js +343 -0
  298. package/dist/progress-report/lock.js.map +1 -0
  299. package/dist/progress-report/schema.d.ts +808 -0
  300. package/dist/progress-report/schema.js +119 -0
  301. package/dist/progress-report/schema.js.map +1 -0
  302. package/dist/progress-report/store.d.ts +66 -0
  303. package/dist/progress-report/store.js +766 -0
  304. package/dist/progress-report/store.js.map +1 -0
  305. package/dist/progress-report/tools.d.ts +20 -0
  306. package/dist/progress-report/tools.js +141 -0
  307. package/dist/progress-report/tools.js.map +1 -0
  308. package/dist/prompt-mode.d.ts +1 -1
  309. package/dist/prompt-mode.js +7 -2
  310. package/dist/prompt-mode.js.map +1 -1
  311. package/dist/runner.d.ts +7 -0
  312. package/dist/runner.js +10 -3
  313. package/dist/runner.js.map +1 -1
  314. package/dist/safety.d.ts +50 -0
  315. package/dist/safety.js +182 -1
  316. package/dist/safety.js.map +1 -1
  317. package/dist/schema.d.ts +8 -0
  318. package/dist/schema.js +20 -0
  319. package/dist/schema.js.map +1 -1
  320. package/dist/server.d.ts +6 -1
  321. package/dist/server.js +94 -8
  322. package/dist/server.js.map +1 -1
  323. package/dist/session-log-stream.d.ts +1 -1
  324. package/dist/session-log-stream.js +280 -33
  325. package/dist/session-log-stream.js.map +1 -1
  326. package/dist/stdio-activity.d.ts +28 -0
  327. package/dist/stdio-activity.js +72 -0
  328. package/dist/stdio-activity.js.map +1 -0
  329. package/docs/configuration.md +426 -0
  330. package/docs/security.md +293 -0
  331. package/package.json +5 -5
  332. package/clinkx-workflows/dist/dashboard-ui/assets/index-DOkMXTfB.js +0 -25
  333. package/clinkx-workflows/dist/dashboard-ui/assets/style-ChJsn0KD.css +0 -1
@@ -1,4 +1,4 @@
1
- import { link as fsLink, open as fsOpen, mkdir as fsMkdir, readdir as fsReaddir, readFile as fsReadFile, rename as fsRename, rm as fsRm, stat as fsStat, writeFile as fsWriteFile, } from "node:fs/promises";
1
+ import { link as fsLink, lstat as fsLstat, open as fsOpen, mkdir as fsMkdir, readdir as fsReaddir, readFile as fsReadFile, rename as fsRename, rm as fsRm, stat as fsStat, writeFile as fsWriteFile, } from "node:fs/promises";
2
2
  import { randomBytes } from "node:crypto";
3
3
  import { hostname } from "node:os";
4
4
  import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
@@ -9,6 +9,8 @@ import { boundContextBuilderOmissions, boundContextBuilderWarnings, } from "./di
9
9
  import { cloneContextBuilderDelegateReceiptMetadata, CONTEXT_BUILDER_DELEGATE_SCHEMA_VERSION, CONTEXT_BUILDER_LEGACY_ARTIFACT_KEYS, CONTEXT_BUILDER_NO_DELEGATE_ARTIFACT_KEYS, CONTEXT_BUILDER_NO_DELEGATE_SCHEMA_VERSION, CONTEXT_BUILDER_SCHEMA_VERSION, isContextBuilderDelegateReceiptMetadata, } from "./receipt.js";
10
10
  import { validateCallerRunId } from "./run-id.js";
11
11
  import { scanSecretPath } from "./secrets.js";
12
+ import { CONTEXT_BUILDER_INVOCATION_AUDIT_FILENAME, readContextBuilderInvocationAuditSidecar, } from "./invocation-sidecar.js";
13
+ import { withContextBuilderMutationLock, } from "./mutation-lock.js";
12
14
  export const CONTEXT_BUILDER_ARTIFACT_FILE_NAMES = Object.freeze({
13
15
  manifest_json: "manifest.json",
14
16
  bundle_md: "bundle.md",
@@ -26,6 +28,7 @@ const DEFAULT_STORAGE_OPS = {
26
28
  rename: fsRename,
27
29
  rm: fsRm,
28
30
  open: fsOpen,
31
+ lstat: fsLstat,
29
32
  link: fsLink,
30
33
  stat: fsStat,
31
34
  readdir: fsReaddir,
@@ -46,15 +49,38 @@ const CONTEXT_BUILDER_METRIC_TYPES = Object.freeze({
46
49
  truncated: "boolean",
47
50
  });
48
51
  const CONTEXT_BUILDER_INTERNAL_CAP_KEYS = Object.keys(CONTEXT_BUILDER_INTERNAL_CAP_DEFAULTS);
49
- const CONTEXT_BUILDER_MUTATION_LOCK_SUFFIX = ".mutation.lock";
50
- const CONTEXT_BUILDER_MUTATION_LOCK_ATTEMPTS = 1_000;
51
- const CONTEXT_BUILDER_MUTATION_LOCK_RETRY_MS = 10;
52
- const CONTEXT_BUILDER_STALE_MUTATION_LOCK_MS = 10 * 60 * 1_000;
53
52
  export const WORKFLOW_CONTEXT_CACHE_MAX_ENTRIES_ENV = "CLINKX_WORKFLOWS_CONTEXT_CACHE_MAX_ENTRIES";
54
53
  export const WORKFLOW_CONTEXT_CACHE_TTL_SECONDS_ENV = "CLINKX_WORKFLOWS_CONTEXT_CACHE_TTL_SECONDS";
55
54
  export const DEFAULT_WORKFLOW_CONTEXT_CACHE_MAX_ENTRIES = 50;
56
55
  export const DEFAULT_WORKFLOW_CONTEXT_CACHE_TTL_SECONDS = 86_400;
57
56
  const DEFAULT_WORKFLOW_CONTEXT_TEMP_TTL_SECONDS = 3_600;
57
+ export class ContextBuilderAncillaryPromotionError extends ExecutionError {
58
+ reasonCode;
59
+ constructor(reasonCode) {
60
+ super("context_builder invocation audit ancillary promotion failed");
61
+ this.name = "ContextBuilderAncillaryPromotionError";
62
+ this.reasonCode = reasonCode;
63
+ }
64
+ }
65
+ export class ContextBuilderRunClaimConflictError extends InvalidParamsError {
66
+ constructor(runId) {
67
+ super(`context_builder run_id already exists: ${runId}`);
68
+ this.name = "ContextBuilderRunClaimConflictError";
69
+ }
70
+ }
71
+ export class ContextBuilderActiveRunClaimConflictError extends ExecutionError {
72
+ constructor(runId) {
73
+ super(`context_builder run_id conflicts with an incomplete or active run: ${runId}`);
74
+ this.name = "ContextBuilderActiveRunClaimConflictError";
75
+ }
76
+ }
77
+ export class ContextBuilderCommittedRunChangedError extends ExecutionError {
78
+ constructor(runId) {
79
+ super(`context_builder committed run changed before lease admission: ${runId}`);
80
+ this.name = "ContextBuilderCommittedRunChangedError";
81
+ }
82
+ }
83
+ const HELD_CONTEXT_BUILDER_MUTATION_LOCKS = Symbol("heldContextBuilderMutationLocks");
58
84
  export function resolveContextBuilderRunPaths(runsDir, runId) {
59
85
  validateCallerRunId(runId);
60
86
  const resolvedRunsDir = resolve(runsDir);
@@ -119,7 +145,7 @@ export async function createContextBuilderRun(input, ops = DEFAULT_STORAGE_OPS)
119
145
  }
120
146
  export async function writeContextBuilderArtifact(storage, key, content, ops = DEFAULT_STORAGE_OPS) {
121
147
  const canonicalStorage = canonicalizeContextBuilderStorage(storage);
122
- await withContextBuilderMutationLock(canonicalStorage, ops, async () => {
148
+ await withContextBuilderStorageMutationLock(canonicalStorage, ops, async () => {
123
149
  await assertManifestAbsentForMutation(canonicalStorage, ops);
124
150
  await writeAtomicText(canonicalStorage.artifacts[key], content, ops, {
125
151
  mustNotExistPath: canonicalStorage.manifestJson,
@@ -130,7 +156,7 @@ export async function commitContextBuilderRun(storage, manifest, optionsOrOps =
130
156
  const { options, ops } = normalizeCommitArguments(optionsOrOps, maybeOps);
131
157
  const canonicalStorage = canonicalizeContextBuilderStorage(storage);
132
158
  const ownerNonce = options.ownerNonce ?? canonicalStorage.ownerNonce;
133
- await withContextBuilderMutationLock(canonicalStorage, ops, async () => {
159
+ await withContextBuilderStorageMutationLock(canonicalStorage, ops, async () => {
134
160
  await assertManifestAbsentForMutation(canonicalStorage, ops);
135
161
  await assertRequiredArtifactsReadable(canonicalStorage, ops);
136
162
  throwIfCancelledBeforeManifest(options);
@@ -154,7 +180,7 @@ export async function writeContextBuilderArtifactsAndCommit(storage, contents, m
154
180
  const ops = options.ops ?? DEFAULT_STORAGE_OPS;
155
181
  const canonicalStorage = canonicalizeContextBuilderStorage(storage);
156
182
  const ownerNonce = options.ownerNonce ?? canonicalStorage.ownerNonce;
157
- await withContextBuilderMutationLock(canonicalStorage, ops, async () => {
183
+ await withContextBuilderStorageMutationLock(canonicalStorage, ops, async () => {
158
184
  await assertManifestAbsentForMutation(canonicalStorage, ops);
159
185
  for (const [key, content] of Object.entries(contents)) {
160
186
  if (options.signal?.aborted === true) {
@@ -182,66 +208,92 @@ export async function writeContextBuilderArtifactsAndCommit(storage, contents, m
182
208
  }, { ownerNonce });
183
209
  }
184
210
  export async function promoteContextBuilderRun(tempRunDir, targetRunId, ops = DEFAULT_STORAGE_OPS) {
211
+ return await withPromotedContextBuilderRun(tempRunDir, targetRunId, async (storage) => storage, ops);
212
+ }
213
+ /**
214
+ * Acquire source before target, then retain the target lease through its
215
+ * caller's manifest re-read and materialization callback.
216
+ */
217
+ export async function withPromotedContextBuilderRun(tempRunDir, targetRunId, callback, ops = DEFAULT_STORAGE_OPS) {
185
218
  const resolvedTempRunDir = resolve(tempRunDir);
186
- const tempManifest = await readContextBuilderManifest(join(resolvedTempRunDir, "manifest.json"), ops);
187
- const tempStorage = resolveContextBuilderRunPaths(dirname(resolvedTempRunDir), tempManifest.run_id);
188
- if (tempStorage.runDir !== resolvedTempRunDir
189
- || !await isContextBuilderRunCommitted(resolvedTempRunDir, ops)) {
190
- throw new ExecutionError("context_builder cannot promote an uncommitted or mismatched temp run");
191
- }
192
- const targetStorage = resolveContextBuilderRunPaths(tempStorage.runsDir, targetRunId);
193
- await claimContextBuilderRunDir(targetStorage, ops);
194
- try {
195
- await ops.mkdir(targetStorage.artifactDir, { recursive: false, mode: 0o700 });
196
- await withContextBuilderMutationLock(targetStorage, ops, async () => {
197
- await assertManifestAbsentForMutation(targetStorage, ops);
198
- const artifactEntries = Object.entries(tempManifest.artifacts);
199
- for (const [key, sourcePath] of artifactEntries) {
200
- if (key === "manifest_json") {
201
- continue;
219
+ const tempStorage = resolveContextBuilderRunPaths(dirname(resolvedTempRunDir), basename(resolvedTempRunDir));
220
+ return await withCommittedContextBuilderRunMutationLease(tempStorage, async (sourceOps) => {
221
+ const tempManifest = await readContextBuilderManifest(tempStorage.manifestJson, sourceOps);
222
+ if (tempStorage.runDir !== resolvedTempRunDir
223
+ || tempManifest.run_id !== tempStorage.runId) {
224
+ throw new ExecutionError("context_builder cannot promote an uncommitted or mismatched temp run");
225
+ }
226
+ let invocationAudit;
227
+ try {
228
+ invocationAudit = await readContextBuilderInvocationAuditSidecar(tempStorage.runDir);
229
+ }
230
+ catch {
231
+ throw new ContextBuilderAncillaryPromotionError("invalid_source");
232
+ }
233
+ const targetStorage = resolveContextBuilderRunPaths(tempStorage.runsDir, targetRunId);
234
+ await claimContextBuilderRunDir(targetStorage, sourceOps);
235
+ let targetCommitted = false;
236
+ try {
237
+ await sourceOps.mkdir(targetStorage.artifactDir, { recursive: false, mode: 0o700 });
238
+ return await withContextBuilderStorageMutationLock(targetStorage, sourceOps, async (targetOps) => {
239
+ await assertManifestAbsentForMutation(targetStorage, targetOps);
240
+ const artifactEntries = Object.entries(tempManifest.artifacts);
241
+ for (const [key, sourcePath] of artifactEntries) {
242
+ if (key === "manifest_json") {
243
+ continue;
244
+ }
245
+ const targetPath = targetStorage.artifacts[key];
246
+ const sourceContent = await targetOps.readFile(sourcePath, "utf8");
247
+ const targetContent = key === "summary_json"
248
+ ? rewriteContextBuilderSummaryRunId(sourceContent, targetRunId)
249
+ : sourceContent;
250
+ await writeAtomicText(targetPath, targetContent, targetOps, {
251
+ mustNotExistPath: targetStorage.manifestJson,
252
+ });
253
+ }
254
+ if (invocationAudit !== undefined) {
255
+ try {
256
+ await writeAtomicText(join(targetStorage.runDir, CONTEXT_BUILDER_INVOCATION_AUDIT_FILENAME), invocationAudit.serialized, targetOps, { mustNotExistPath: targetStorage.manifestJson });
257
+ }
258
+ catch {
259
+ throw new ContextBuilderAncillaryPromotionError("copy_failed");
260
+ }
261
+ }
262
+ const targetArtifacts = Object.fromEntries(Object.keys(tempManifest.artifacts).map((key) => [
263
+ key,
264
+ targetStorage.artifacts[key],
265
+ ]));
266
+ const targetManifest = {
267
+ ...tempManifest,
268
+ run_id: targetRunId,
269
+ artifacts: targetArtifacts,
270
+ };
271
+ await writeExclusiveText(targetStorage.manifestJson, `${JSON.stringify(targetManifest)}\n`, targetOps);
272
+ if (!await isContextBuilderRunCommitted(targetStorage.runDir, targetOps)) {
273
+ throw new ExecutionError(`context_builder promoted run failed validation: ${targetRunId}`);
202
274
  }
203
- const targetPath = targetStorage.artifacts[key];
204
- const sourceContent = await ops.readFile(sourcePath, "utf8");
205
- const targetContent = key === "summary_json"
206
- ? rewriteContextBuilderSummaryRunId(sourceContent, targetRunId)
207
- : sourceContent;
208
- await writeAtomicText(targetPath, targetContent, ops, {
209
- mustNotExistPath: targetStorage.manifestJson,
210
- });
275
+ targetCommitted = true;
276
+ await (targetOps.rm ?? fsRm)(resolvedTempRunDir, { recursive: true, force: true }).catch(() => { });
277
+ await syncDirectoryBestEffort(tempStorage.runsDir, targetOps);
278
+ return await callback(targetStorage, targetOps);
279
+ });
280
+ }
281
+ catch (error) {
282
+ if (!targetCommitted) {
283
+ // This call claimed the uncommitted target, so it owns cleanup.
284
+ await (sourceOps.rm ?? fsRm)(targetStorage.runDir, {
285
+ recursive: true,
286
+ force: true,
287
+ }).catch(() => { });
288
+ await syncDirectoryBestEffort(targetStorage.runsDir, sourceOps);
211
289
  }
212
- const targetArtifacts = Object.fromEntries(Object.keys(tempManifest.artifacts).map((key) => [
213
- key,
214
- targetStorage.artifacts[key],
215
- ]));
216
- const targetManifest = {
217
- ...tempManifest,
218
- run_id: targetRunId,
219
- artifacts: targetArtifacts,
220
- };
221
- await writeExclusiveText(targetStorage.manifestJson, `${JSON.stringify(targetManifest)}\n`, ops);
222
- });
223
- if (!await isContextBuilderRunCommitted(targetStorage.runDir, ops)) {
224
- throw new ExecutionError(`context_builder promoted run failed validation: ${targetRunId}`);
290
+ throw error;
225
291
  }
226
- }
227
- catch (error) {
228
- // The target dir was claimed above, so always clean it up on failure.
229
- await (ops.rm ?? fsRm)(targetStorage.runDir, {
230
- recursive: true,
231
- force: true,
232
- }).catch(() => { });
233
- await syncDirectoryBestEffort(targetStorage.runsDir, ops);
234
- throw error;
235
- }
236
- await (ops.rm ?? fsRm)(resolvedTempRunDir, { recursive: true, force: true })
237
- .catch(() => { });
238
- await syncDirectoryBestEffort(tempStorage.runsDir, ops);
239
- return targetStorage;
292
+ }, ops);
240
293
  }
241
294
  export async function evictWorkflowContextRuns(runsDir, options = {}) {
242
295
  const ops = options.ops ?? DEFAULT_STORAGE_OPS;
243
296
  const readdir = ops.readdir ?? fsReaddir;
244
- const stat = ops.stat ?? fsStat;
245
297
  const resolvedRunsDir = await resolveContextBuilderRunsDir(runsDir);
246
298
  const bounds = resolveWorkflowContextEvictionBounds(options);
247
299
  const nowMs = options.nowMs ?? Date.now();
@@ -259,43 +311,52 @@ export async function evictWorkflowContextRuns(runsDir, options = {}) {
259
311
  scanned += 1;
260
312
  try {
261
313
  const storage = resolveContextBuilderRunPaths(resolvedRunsDir, entry.name);
262
- const runStat = await stat(storage.runDir).catch((error) => {
263
- if (isNodeError(error) && error.code === "ENOENT") {
264
- return undefined;
265
- }
266
- throw error;
267
- });
268
- if (runStat === undefined) {
314
+ const directoryAdmission = await admitContextBuilderRunDirectory(storage, ops);
315
+ if (directoryAdmission === undefined) {
269
316
  skipped += 1;
270
317
  continue;
271
318
  }
272
- if (!await isContextBuilderRunCommitted(storage.runDir, ops)) {
319
+ const admission = await admitCommittedContextBuilderRun(storage, ops);
320
+ if (admission === undefined) {
273
321
  skipped += 1;
274
322
  continue;
275
323
  }
324
+ const currentDirectoryAdmission = await admitContextBuilderRunDirectory(storage, ops);
325
+ if (currentDirectoryAdmission === undefined
326
+ || !contextBuilderRunDirectoryAdmissionsEqual(directoryAdmission, currentDirectoryAdmission)) {
327
+ skipped += 1;
328
+ continue;
329
+ }
330
+ const mtimeMs = Number(directoryAdmission.mtimeNs) / 1_000_000;
331
+ const candidate = {
332
+ storage,
333
+ mtimeMs,
334
+ admission,
335
+ directoryAdmission,
336
+ };
276
337
  const isProtected = protectedRunIds.has(storage.runId);
277
338
  if (entry.name.startsWith("wfctx-")) {
278
339
  // Protected cache entries are excluded from LRU survivor accounting so
279
340
  // they can never be selected for the max-entries trim below either.
280
341
  if (!isProtected) {
281
- committedCacheEntries.push({ storage, mtimeMs: runStat.mtimeMs });
342
+ committedCacheEntries.push(candidate);
282
343
  }
283
- if (nowMs - runStat.mtimeMs > bounds.cacheTtlMs) {
344
+ if (nowMs - mtimeMs > bounds.cacheTtlMs) {
284
345
  if (isProtected) {
285
346
  skipped += 1;
286
347
  }
287
348
  else {
288
- removeSet.set(storage.runId, storage);
349
+ removeSet.set(storage.runId, candidate);
289
350
  }
290
351
  }
291
352
  continue;
292
353
  }
293
- if (nowMs - runStat.mtimeMs > bounds.tempTtlMs) {
354
+ if (nowMs - mtimeMs > bounds.tempTtlMs) {
294
355
  if (isProtected) {
295
356
  skipped += 1;
296
357
  }
297
358
  else {
298
- removeSet.set(storage.runId, storage);
359
+ removeSet.set(storage.runId, candidate);
299
360
  }
300
361
  }
301
362
  }
@@ -305,15 +366,16 @@ export async function evictWorkflowContextRuns(runsDir, options = {}) {
305
366
  }
306
367
  const cacheSurvivors = committedCacheEntries
307
368
  .filter(({ storage }) => !removeSet.has(storage.runId))
308
- .sort((a, b) => a.mtimeMs - b.mtimeMs)
309
- .map(({ storage }) => storage);
369
+ .sort((a, b) => a.mtimeMs - b.mtimeMs);
310
370
  cacheSurvivors
311
371
  .slice(0, Math.max(0, cacheSurvivors.length - bounds.maxEntries))
312
- .forEach((storage) => removeSet.set(storage.runId, storage));
372
+ .forEach((candidate) => {
373
+ removeSet.set(candidate.storage.runId, candidate);
374
+ });
313
375
  let evicted = 0;
314
- for (const storage of removeSet.values()) {
376
+ for (const { storage, admission, directoryAdmission } of removeSet.values()) {
315
377
  try {
316
- const didRemove = await removeCommittedWorkflowContextRun(storage, ops);
378
+ const didRemove = await removeCommittedWorkflowContextRun(storage, admission, directoryAdmission, ops);
317
379
  if (didRemove) {
318
380
  evicted += 1;
319
381
  }
@@ -337,19 +399,165 @@ export async function evictWorkflowContextRuns(runsDir, options = {}) {
337
399
  }
338
400
  export async function cleanupContextBuilderIncompleteRun(storage, ownerNonce, ops = DEFAULT_STORAGE_OPS) {
339
401
  const canonicalStorage = canonicalizeContextBuilderStorage(storage);
340
- return withContextBuilderMutationLock(canonicalStorage, ops, async () => cleanupContextBuilderIncompleteRunLocked(canonicalStorage, ownerNonce, ops), {
402
+ return withContextBuilderStorageMutationLock(canonicalStorage, ops, async () => cleanupContextBuilderIncompleteRunLocked(canonicalStorage, ownerNonce, ops), {
341
403
  ownerNonce,
342
- recoverStaleForCleanup: true,
404
+ canRecoverStale: async () => (await canRemoveIncompleteRunDir(canonicalStorage, ownerNonce, ops)).removable,
343
405
  });
344
406
  }
407
+ /** Hold one run's mutation lock, reusing it for nested storage operations. */
408
+ export async function withContextBuilderRunMutationLease(storage, callback, ops = DEFAULT_STORAGE_OPS, options = {}) {
409
+ const canonicalStorage = canonicalizeContextBuilderStorage(storage);
410
+ const effectiveOps = ops ?? DEFAULT_STORAGE_OPS;
411
+ return await withContextBuilderStorageMutationLock(canonicalStorage, effectiveOps, callback, options);
412
+ }
345
413
  /**
346
- * Hold the existing per-run mutation lock while a committed run is consumed.
347
- * Writers and pruning use the same lock, so the callback may safely materialize
348
- * artifact paths without the run directory disappearing underneath it.
414
+ * Admit one exact committed-run incarnation, recover only a stale lock bound to
415
+ * that admission, and recheck the manifest after acquiring the lease.
349
416
  */
350
- export async function withContextBuilderRunMutationLease(storage, callback, ops = DEFAULT_STORAGE_OPS) {
417
+ export async function withCommittedContextBuilderRunMutationLease(storage, callback, ops = DEFAULT_STORAGE_OPS) {
351
418
  const canonicalStorage = canonicalizeContextBuilderStorage(storage);
352
- return await withContextBuilderMutationLock(canonicalStorage, ops, callback);
419
+ const admitted = await admitCommittedContextBuilderRun(canonicalStorage, ops);
420
+ if (admitted === undefined) {
421
+ throw new ContextBuilderCommittedRunChangedError(canonicalStorage.runId);
422
+ }
423
+ return await withAdmittedCommittedContextBuilderRunMutationLease(canonicalStorage, admitted, callback, ops);
424
+ }
425
+ async function withAdmittedCommittedContextBuilderRunMutationLease(storage, admitted, callback, ops) {
426
+ return await withContextBuilderStorageMutationLock(storage, ops, async (leasedOps) => {
427
+ const current = await admitCommittedContextBuilderRun(storage, leasedOps);
428
+ if (current === undefined
429
+ || !committedRunAdmissionsEqual(admitted, current)) {
430
+ throw new ContextBuilderCommittedRunChangedError(storage.runId);
431
+ }
432
+ return await callback(leasedOps);
433
+ }, {
434
+ canRecoverStale: async () => {
435
+ const current = await admitCommittedContextBuilderRun(storage, ops);
436
+ return current !== undefined
437
+ && committedRunAdmissionsEqual(admitted, current);
438
+ },
439
+ });
440
+ }
441
+ async function withContextBuilderStorageMutationLock(storage, ops, callback, options = {}) {
442
+ const heldLocks = ops[HELD_CONTEXT_BUILDER_MUTATION_LOCKS];
443
+ if (heldLocks?.has(storage.runDir) === true) {
444
+ return await callback(ops);
445
+ }
446
+ return await withContextBuilderMutationLock(storage, ops, async () => await callback(markContextBuilderMutationLockHeld(ops, storage.runDir)), options);
447
+ }
448
+ function markContextBuilderMutationLockHeld(ops, runDir) {
449
+ const inherited = ops[HELD_CONTEXT_BUILDER_MUTATION_LOCKS];
450
+ const heldLocks = new Set(inherited ?? []);
451
+ heldLocks.add(runDir);
452
+ const leasedOps = Object.create(ops);
453
+ Object.defineProperty(leasedOps, HELD_CONTEXT_BUILDER_MUTATION_LOCKS, {
454
+ value: heldLocks,
455
+ });
456
+ return leasedOps;
457
+ }
458
+ async function admitCommittedContextBuilderRun(storage, ops) {
459
+ const lstat = ops.lstat ?? fsLstat;
460
+ const open = ops.open ?? fsOpen;
461
+ let handle;
462
+ try {
463
+ const pathStats = await lstat(storage.manifestJson, { bigint: true });
464
+ if (!pathStats.isFile()) {
465
+ return undefined;
466
+ }
467
+ handle = await open(storage.manifestJson, "r");
468
+ const before = await handle.stat({ bigint: true });
469
+ if (!before.isFile()
470
+ || before.nlink === 0n
471
+ || !sameCommittedRunIncarnation(committedRunFingerprintFromStats(pathStats), committedRunFingerprintFromStats(before))) {
472
+ return undefined;
473
+ }
474
+ const serializedManifest = await handle.readFile({ encoding: "utf8" });
475
+ const afterRead = await handle.stat({ bigint: true });
476
+ const beforeFingerprint = committedRunFingerprintFromStats(before);
477
+ const afterReadFingerprint = committedRunFingerprintFromStats(afterRead);
478
+ if (afterRead.nlink === 0n
479
+ || !committedRunFingerprintsEqual(beforeFingerprint, afterReadFingerprint)) {
480
+ return undefined;
481
+ }
482
+ const parsed = JSON.parse(serializedManifest);
483
+ if (!isValidContextBuilderManifest(parsed, storage.runId)) {
484
+ return undefined;
485
+ }
486
+ const artifactKeys = supportedArtifactKeys(parsed.artifacts);
487
+ if (artifactKeys == null
488
+ || !artifactPathsExactlyMatch(parsed.artifacts, storage.artifacts, artifactKeys)) {
489
+ return undefined;
490
+ }
491
+ await assertRequiredArtifactsReadable(storage, ops, artifactKeys);
492
+ const afterArtifacts = await handle.stat({ bigint: true });
493
+ const finalFingerprint = committedRunFingerprintFromStats(afterArtifacts);
494
+ if (afterArtifacts.nlink === 0n
495
+ || !committedRunFingerprintsEqual(afterReadFingerprint, finalFingerprint)) {
496
+ return undefined;
497
+ }
498
+ return { serializedManifest, fingerprint: finalFingerprint };
499
+ }
500
+ catch {
501
+ return undefined;
502
+ }
503
+ finally {
504
+ await handle?.close().catch(() => { });
505
+ }
506
+ }
507
+ function committedRunFingerprintFromStats(stats) {
508
+ return {
509
+ dev: stats.dev,
510
+ ino: stats.ino,
511
+ mtimeNs: stats.mtimeNs,
512
+ ctimeNs: stats.ctimeNs,
513
+ size: stats.size,
514
+ nlink: stats.nlink,
515
+ };
516
+ }
517
+ function sameCommittedRunIncarnation(left, right) {
518
+ return left.dev === right.dev && left.ino === right.ino;
519
+ }
520
+ function committedRunFingerprintsEqual(left, right) {
521
+ return sameCommittedRunIncarnation(left, right)
522
+ && left.mtimeNs === right.mtimeNs
523
+ && left.ctimeNs === right.ctimeNs
524
+ && left.size === right.size
525
+ && left.nlink === right.nlink;
526
+ }
527
+ function committedRunAdmissionsEqual(left, right) {
528
+ return left.serializedManifest === right.serializedManifest
529
+ && committedRunFingerprintsEqual(left.fingerprint, right.fingerprint);
530
+ }
531
+ async function admitContextBuilderRunDirectory(storage, ops) {
532
+ const lstat = ops.lstat ?? fsLstat;
533
+ try {
534
+ const stats = await lstat(storage.runDir, { bigint: true });
535
+ if (!stats.isDirectory() || stats.nlink === 0n) {
536
+ return undefined;
537
+ }
538
+ return {
539
+ dev: stats.dev,
540
+ ino: stats.ino,
541
+ mtimeNs: stats.mtimeNs,
542
+ ctimeNs: stats.ctimeNs,
543
+ size: stats.size,
544
+ nlink: stats.nlink,
545
+ };
546
+ }
547
+ catch (error) {
548
+ if (isNodeError(error) && error.code === "ENOENT") {
549
+ return undefined;
550
+ }
551
+ throw error;
552
+ }
553
+ }
554
+ function contextBuilderRunDirectoryAdmissionsEqual(left, right) {
555
+ return left.dev === right.dev
556
+ && left.ino === right.ino
557
+ && left.mtimeNs === right.mtimeNs
558
+ && left.ctimeNs === right.ctimeNs
559
+ && left.size === right.size
560
+ && left.nlink === right.nlink;
353
561
  }
354
562
  async function cleanupContextBuilderIncompleteRunLocked(storage, ownerNonce, ops) {
355
563
  if (!await mayRemoveIncompleteRunDir(storage, ownerNonce, ops)) {
@@ -480,9 +688,9 @@ async function claimContextBuilderRunDir(paths, ops) {
480
688
  catch (error) {
481
689
  if (isNodeError(error) && error.code === "EEXIST") {
482
690
  if (await isContextBuilderRunCommitted(paths.runDir, ops)) {
483
- throw new InvalidParamsError(`context_builder run_id already exists: ${paths.runId}`);
691
+ throw new ContextBuilderRunClaimConflictError(paths.runId);
484
692
  }
485
- throw new ExecutionError(`context_builder run_id conflicts with an incomplete or active run: ${paths.runId}`);
693
+ throw new ContextBuilderActiveRunClaimConflictError(paths.runId);
486
694
  }
487
695
  throw error;
488
696
  }
@@ -508,146 +716,6 @@ function isStorageOps(value) {
508
716
  || "open" in value
509
717
  || "link" in value);
510
718
  }
511
- async function withContextBuilderMutationLock(storage, ops, callback, options = {}) {
512
- const lockPath = mutationLockPathForStorage(storage);
513
- const lockHandle = await acquireContextBuilderMutationLock(storage, lockPath, ops, options);
514
- try {
515
- return await callback();
516
- }
517
- finally {
518
- await lockHandle.close().catch(() => { });
519
- await (ops.rm ?? fsRm)(lockPath, { force: true }).catch(() => { });
520
- }
521
- }
522
- async function acquireContextBuilderMutationLock(storage, lockPath, ops, options) {
523
- const open = ops.open ?? fsOpen;
524
- for (let attempt = 0; attempt < CONTEXT_BUILDER_MUTATION_LOCK_ATTEMPTS; attempt += 1) {
525
- try {
526
- const handle = await open(lockPath, "wx", 0o600);
527
- try {
528
- await writeMutationLockMetadata(handle, options.ownerNonce);
529
- return handle;
530
- }
531
- catch (error) {
532
- await handle.close().catch(() => { });
533
- await (ops.rm ?? fsRm)(lockPath, { force: true }).catch(() => { });
534
- throw error;
535
- }
536
- }
537
- catch (error) {
538
- if (isNodeError(error) && error.code === "EEXIST") {
539
- if (options.recoverStaleForCleanup === true
540
- && await breakStaleCleanupMutationLock(storage, lockPath, options.ownerNonce, ops)) {
541
- continue;
542
- }
543
- await delay(CONTEXT_BUILDER_MUTATION_LOCK_RETRY_MS);
544
- continue;
545
- }
546
- throw error;
547
- }
548
- }
549
- throw new ExecutionError("context_builder timed out waiting for the run mutation lock");
550
- }
551
- function mutationLockPathForStorage(storage) {
552
- validateCallerRunId(storage.runId);
553
- const resolvedRunsDir = resolve(storage.runsDir);
554
- const resolvedRunDir = resolve(storage.runDir);
555
- if (!isPathInsideDirectory(resolvedRunDir, resolvedRunsDir)) {
556
- throw new ExecutionError("context_builder refused mutation lock outside runs directory");
557
- }
558
- return join(resolvedRunsDir, `.${storage.runId}${CONTEXT_BUILDER_MUTATION_LOCK_SUFFIX}`);
559
- }
560
- async function writeMutationLockMetadata(handle, ownerNonce) {
561
- const metadata = {
562
- pid: process.pid,
563
- hostname: hostname(),
564
- created_at: new Date().toISOString(),
565
- ...(ownerNonce == null ? {} : { owner_nonce: ownerNonce }),
566
- };
567
- await handle.writeFile(`${JSON.stringify(metadata)}\n`, { encoding: "utf8" });
568
- }
569
- async function breakStaleCleanupMutationLock(storage, lockPath, ownerNonce, ops) {
570
- if (ownerNonce == null) {
571
- return false;
572
- }
573
- const cleanupDecision = await canRemoveIncompleteRunDir(storage, ownerNonce, ops);
574
- if (!cleanupDecision.removable) {
575
- return false;
576
- }
577
- if (!await isMutationLockOldEnough(lockPath, ops)) {
578
- return false;
579
- }
580
- const metadata = await readMutationLockMetadata(lockPath, ops);
581
- if (metadata !== "invalid" && !isStaleMutationLockMetadata(metadata)) {
582
- return false;
583
- }
584
- await (ops.rm ?? fsRm)(lockPath, { force: true }).catch(() => { });
585
- return true;
586
- }
587
- async function isMutationLockOldEnough(lockPath, ops) {
588
- const stat = ops.stat ?? fsStat;
589
- try {
590
- const lockStat = await stat(lockPath);
591
- return Date.now() - lockStat.mtimeMs >= CONTEXT_BUILDER_STALE_MUTATION_LOCK_MS;
592
- }
593
- catch {
594
- return false;
595
- }
596
- }
597
- async function readMutationLockMetadata(lockPath, ops) {
598
- let parsed;
599
- try {
600
- parsed = JSON.parse(await ops.readFile(lockPath, "utf8"));
601
- }
602
- catch {
603
- return "invalid";
604
- }
605
- if (parsed === null || typeof parsed !== "object") {
606
- return "invalid";
607
- }
608
- const metadata = parsed;
609
- if (typeof metadata.pid !== "number"
610
- || !Number.isInteger(metadata.pid)
611
- || typeof metadata.hostname !== "string"
612
- || typeof metadata.created_at !== "string") {
613
- return "invalid";
614
- }
615
- if (metadata.owner_nonce !== undefined
616
- && typeof metadata.owner_nonce !== "string") {
617
- return "invalid";
618
- }
619
- return {
620
- pid: metadata.pid,
621
- hostname: metadata.hostname,
622
- created_at: metadata.created_at,
623
- ...(metadata.owner_nonce === undefined
624
- ? {}
625
- : { owner_nonce: metadata.owner_nonce }),
626
- };
627
- }
628
- function isStaleMutationLockMetadata(metadata) {
629
- const createdAtMs = Date.parse(metadata.created_at);
630
- if (!Number.isFinite(createdAtMs)
631
- || Date.now() - createdAtMs < CONTEXT_BUILDER_STALE_MUTATION_LOCK_MS) {
632
- return false;
633
- }
634
- if (metadata.hostname !== hostname()) {
635
- return false;
636
- }
637
- return !isProcessAlive(metadata.pid);
638
- }
639
- function isProcessAlive(pid) {
640
- if (pid <= 0) {
641
- return false;
642
- }
643
- try {
644
- process.kill(pid, 0);
645
- return true;
646
- }
647
- catch (error) {
648
- return !(isNodeError(error) && error.code === "ESRCH");
649
- }
650
- }
651
719
  async function assertRequiredArtifactsReadable(storage, ops, artifactKeys = CONTEXT_BUILDER_NO_DELEGATE_ARTIFACT_KEYS) {
652
720
  const missing = [];
653
721
  for (const key of artifactKeys) {
@@ -876,17 +944,27 @@ async function syncDirectoryBestEffort(path, ops) {
876
944
  }
877
945
  }
878
946
  }
879
- async function removeCommittedWorkflowContextRun(storage, ops) {
947
+ async function removeCommittedWorkflowContextRun(storage, admission, directoryAdmission, ops) {
880
948
  if (!isWorkflowContextRunId(storage.runId)) {
881
949
  return false;
882
950
  }
883
- return await withContextBuilderMutationLock(storage, ops, async () => {
884
- if (!await isContextBuilderRunCommitted(storage.runDir, ops)) {
951
+ try {
952
+ return await withAdmittedCommittedContextBuilderRunMutationLease(storage, admission, async (leasedOps) => {
953
+ const currentDirectoryAdmission = await admitContextBuilderRunDirectory(storage, leasedOps);
954
+ if (currentDirectoryAdmission === undefined
955
+ || !contextBuilderRunDirectoryAdmissionsEqual(directoryAdmission, currentDirectoryAdmission)) {
956
+ return false;
957
+ }
958
+ await (leasedOps.rm ?? fsRm)(storage.runDir, { recursive: true, force: true });
959
+ return true;
960
+ }, ops);
961
+ }
962
+ catch (error) {
963
+ if (error instanceof ContextBuilderCommittedRunChangedError) {
885
964
  return false;
886
965
  }
887
- await (ops.rm ?? fsRm)(storage.runDir, { recursive: true, force: true });
888
- return true;
889
- });
966
+ throw error;
967
+ }
890
968
  }
891
969
  function isWorkflowContextRunId(runId) {
892
970
  return runId.startsWith("wfctx-") || runId.startsWith("wfpre-");
@@ -993,9 +1071,6 @@ function diagnosticText(value, fallback) {
993
1071
  function isRecord(value) {
994
1072
  return value !== null && typeof value === "object";
995
1073
  }
996
- async function delay(ms) {
997
- await new Promise((resolve) => setTimeout(resolve, ms));
998
- }
999
1074
  function isPathInsideDirectory(candidate, directory) {
1000
1075
  const relation = relative(directory, candidate);
1001
1076
  return relation !== "" && !relation.startsWith("..") && !isAbsolute(relation);