clinkx 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (331) hide show
  1. package/CHANGELOG.md +86 -0
  2. package/README.md +72 -0
  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 +865 -83
  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/package.json +2 -1
  330. package/clinkx-workflows/dist/dashboard-ui/assets/index-DOkMXTfB.js +0 -25
  331. package/clinkx-workflows/dist/dashboard-ui/assets/style-ChJsn0KD.css +0 -1
@@ -1,50 +1,171 @@
1
1
  import { createHash, randomUUID } from "node:crypto";
2
- import { chmod, mkdir, open, readFile, readdir, rename, rm, stat, } from "node:fs/promises";
3
- import { join } from "node:path";
4
- import { ACTIVITY_RESULT_FILENAME, ACTIVITY_STATE_FILENAME, ActivityIdSchema, ActivityStateV1Schema, DEFAULT_ACTIVITY_HEARTBEAT_INTERVAL_MS, DEFAULT_ACTIVITY_MAX_AGE_MS, DEFAULT_MAX_ACTIVITY_ENTRIES, DEFAULT_MAX_ACTIVITY_RESULT_BYTES, DEFAULT_MAX_ACTIVITY_TOTAL_BYTES, MAX_ACTIVITY_STATE_BYTES, } from "../clinkx-workflows/dist/dashboard/activity-contract.js";
2
+ import { constants as fsConstants } from "node:fs";
3
+ import { chmod, lstat, mkdir, open, readFile, readdir, rename, rm, stat, } from "node:fs/promises";
4
+ import { dirname, join } from "node:path";
5
+ import { ACTIVITY_RESULT_FILENAME, ACTIVITY_STATE_FILENAME, ActivityIdSchema, ActivityStateReadSchema, ActivityStateV1Schema, ActivityStateV2Schema, ActivityStateV3Schema, DEFAULT_ACTIVITY_HEARTBEAT_INTERVAL_MS, DEFAULT_ACTIVITY_MAX_AGE_MS, DEFAULT_MAX_ACTIVITY_ENTRIES, DEFAULT_MAX_ACTIVITY_RESULT_BYTES, DEFAULT_MAX_ACTIVITY_TOTAL_BYTES, MAX_ACTIVITY_STATE_BYTES, } from "../clinkx-workflows/dist/dashboard/activity-contract.js";
5
6
  import { createDashboardActivityReader } from "../clinkx-workflows/dist/dashboard/activity-reader.js";
7
+ import { deriveActivityGeneration } from "../clinkx-workflows/dist/dashboard/generation.js";
6
8
  import { removeSessionLogGeneration } from "../clinkx-workflows/dist/dashboard/session-log-retention.js";
7
9
  import { createSessionLogGeneration, readSessionLogGeneration, } from "../clinkx-workflows/dist/dashboard/session-log-identity.js";
10
+ import { invocationAuditV2PersistenceEnabled, observedInvocationPersistenceEnabled, } from "../clinkx-workflows/dist/config.js";
11
+ import { resolveOpenedDescriptorPath } from "../clinkx-workflows/dist/contained-file.js";
8
12
  import { logger } from "./logger.js";
9
- export { ACTIVITY_RESULT_FILENAME, ACTIVITY_STATE_FILENAME, ActivityIdSchema, ActivityKindSchema, ActivityOutcomeKindSchema, ActivityPhaseSchema, ActivityResultReferenceSchema, ActivityStateV1Schema, ActivityTerminalStatusSchema, DEFAULT_ACTIVITY_HEARTBEAT_INTERVAL_MS, DEFAULT_ACTIVITY_MAX_AGE_MS, DEFAULT_MAX_ACTIVITY_ENTRIES, DEFAULT_MAX_ACTIVITY_RESULT_BYTES, DEFAULT_MAX_ACTIVITY_TOTAL_BYTES, MAX_ACTIVITY_STATE_BYTES, } from "../clinkx-workflows/dist/dashboard/activity-contract.js";
13
+ export { ACTIVITY_RESULT_FILENAME, ACTIVITY_STATE_FILENAME, ActivityIdSchema, ActivityKindSchema, ActivityOutcomeKindSchema, ActivityPhaseSchema, ActivityResultReferenceSchema, ActivityStateReadSchema, ActivityStateV1Schema, ActivityStateV2Schema, ActivityStateV3Schema, ActivityTerminalStatusSchema, DEFAULT_ACTIVITY_HEARTBEAT_INTERVAL_MS, DEFAULT_ACTIVITY_MAX_AGE_MS, DEFAULT_MAX_ACTIVITY_ENTRIES, DEFAULT_MAX_ACTIVITY_RESULT_BYTES, DEFAULT_MAX_ACTIVITY_TOTAL_BYTES, MAX_ACTIVITY_STATE_BYTES, } from "../clinkx-workflows/dist/dashboard/activity-contract.js";
10
14
  export const ACTIVITY_RETAIN_RESULT_ENV = "CLINKX_DASHBOARD_ACTIVITY_RETAIN_RESULT";
11
15
  const SESSION_LOG_GENERATION_FILE_BYTES = 44;
12
16
  const NOOP_OBSERVATION = Object.freeze({
13
17
  async transition() { },
18
+ async observe() { },
14
19
  async complete() { },
15
20
  });
16
- async function writeFileAtomic(filePath, content) {
17
- const temporaryPath = `${filePath}.${randomUUID()}.tmp`;
18
- const handle = await open(temporaryPath, "wx", 0o600);
21
+ class UnsafeActivityDirectoryError extends Error {
22
+ }
23
+ async function inspectActivityDirectory(activityDir, activityId) {
24
+ let linked;
25
+ try {
26
+ linked = await lstat(activityDir);
27
+ }
28
+ catch (error) {
29
+ if (isFileSystemError(error, "ENOENT"))
30
+ return undefined;
31
+ throw error;
32
+ }
33
+ if (linked.isSymbolicLink() || !linked.isDirectory()) {
34
+ throw new UnsafeActivityDirectoryError();
35
+ }
36
+ let handle;
19
37
  try {
20
- await handle.writeFile(content);
21
- await handle.sync();
22
- await handle.close();
23
- await rename(temporaryPath, filePath);
24
- await chmod(filePath, 0o600);
38
+ handle = await open(activityDir, fsConstants.O_RDONLY |
39
+ (fsConstants.O_DIRECTORY ?? 0) |
40
+ (fsConstants.O_NOFOLLOW ?? 0));
41
+ }
42
+ catch {
43
+ throw new UnsafeActivityDirectoryError();
44
+ }
45
+ try {
46
+ const opened = await handle.stat({ bigint: true });
47
+ const canonicalPath = await resolveOpenedDescriptorPath(handle.fd, activityDir);
48
+ if (!opened.isDirectory() ||
49
+ opened.nlink === 0n ||
50
+ BigInt(linked.dev) !== opened.dev ||
51
+ BigInt(linked.ino) !== opened.ino ||
52
+ canonicalPath === undefined) {
53
+ throw new UnsafeActivityDirectoryError();
54
+ }
55
+ return {
56
+ activityId,
57
+ path: canonicalPath,
58
+ handle,
59
+ identity: { dev: opened.dev, ino: opened.ino },
60
+ localGeneration: deriveActivityGeneration(activityId, linked),
61
+ };
25
62
  }
26
63
  catch (error) {
27
64
  await handle.close().catch(() => undefined);
28
- await rm(temporaryPath, { force: true }).catch(() => undefined);
29
65
  throw error;
30
66
  }
31
67
  }
68
+ async function verifyActivityDirectory(path, admitted) {
69
+ const linked = await lstat(path).catch(() => undefined);
70
+ const opened = await admitted.handle.stat({ bigint: true }).catch(() => undefined);
71
+ if (linked === undefined ||
72
+ opened === undefined ||
73
+ linked.isSymbolicLink() ||
74
+ !linked.isDirectory() ||
75
+ !opened.isDirectory() ||
76
+ opened.nlink === 0n ||
77
+ BigInt(linked.dev) !== admitted.identity.dev ||
78
+ BigInt(linked.ino) !== admitted.identity.ino ||
79
+ opened.dev !== admitted.identity.dev ||
80
+ opened.ino !== admitted.identity.ino ||
81
+ deriveActivityGeneration(admitted.activityId, linked) !==
82
+ admitted.localGeneration ||
83
+ await resolveOpenedDescriptorPath(admitted.handle.fd, path) !== path) {
84
+ throw new UnsafeActivityDirectoryError();
85
+ }
86
+ }
87
+ function captureActivityDirectoryIncarnation(admitted) {
88
+ return Object.freeze({
89
+ activityId: admitted.activityId,
90
+ path: admitted.path,
91
+ identity: admitted.identity,
92
+ localGeneration: admitted.localGeneration,
93
+ });
94
+ }
95
+ async function reopenActivityDirectory(incarnation) {
96
+ const admitted = await inspectActivityDirectory(incarnation.path, incarnation.activityId);
97
+ if (admitted === undefined)
98
+ return undefined;
99
+ if (admitted.path !== incarnation.path ||
100
+ admitted.identity.dev !== incarnation.identity.dev ||
101
+ admitted.identity.ino !== incarnation.identity.ino ||
102
+ admitted.localGeneration !== incarnation.localGeneration) {
103
+ await admitted.handle.close().catch(() => undefined);
104
+ throw new UnsafeActivityDirectoryError();
105
+ }
106
+ return admitted;
107
+ }
108
+ async function writeActivityFileAtomic(directory, filename, content) {
109
+ const admitted = await reopenActivityDirectory(directory);
110
+ if (admitted === undefined)
111
+ throw new UnsafeActivityDirectoryError();
112
+ try {
113
+ const filePath = join(admitted.path, filename);
114
+ const temporaryPath = `${filePath}.${randomUUID()}.tmp`;
115
+ const handle = await open(temporaryPath, "wx", 0o600);
116
+ try {
117
+ await verifyActivityDirectory(admitted.path, admitted);
118
+ await handle.writeFile(content);
119
+ await handle.sync();
120
+ await handle.chmod(0o600);
121
+ // Bind the commit, not merely the temp creation, to the short-lived
122
+ // descriptor re-admitted for this exact incarnation.
123
+ await verifyActivityDirectory(admitted.path, admitted);
124
+ await rename(temporaryPath, filePath);
125
+ }
126
+ catch (error) {
127
+ await rm(temporaryPath, { force: true }).catch(() => undefined);
128
+ throw error;
129
+ }
130
+ finally {
131
+ await handle.close().catch(() => undefined);
132
+ }
133
+ }
134
+ finally {
135
+ await admitted.handle.close().catch(() => undefined);
136
+ }
137
+ }
138
+ function isFileSystemError(error, code) {
139
+ return typeof error === "object" && error !== null &&
140
+ "code" in error && error.code === code;
141
+ }
32
142
  function serializedState(state) {
33
- const serialized = `${JSON.stringify(ActivityStateV1Schema.parse(state), null, 2)}\n`;
143
+ const parsed = state.schemaVersion === "clinkx-dashboard-activity-v3"
144
+ ? ActivityStateV3Schema.parse(state)
145
+ : state.schemaVersion === "clinkx-dashboard-activity-v2"
146
+ ? ActivityStateV2Schema.parse(state)
147
+ : ActivityStateV1Schema.parse(state);
148
+ const serialized = `${JSON.stringify(parsed, null, 2)}\n`;
34
149
  if (Buffer.byteLength(serialized, "utf8") > MAX_ACTIVITY_STATE_BYTES) {
35
150
  throw new Error("activity state exceeds the producer byte cap");
36
151
  }
37
152
  return serialized;
38
153
  }
39
- async function pruneActivities(rootDir, sessionLogRootDir, activityIdentityReader, now, maxAgeMs, maxEntries, maxTotalBytes, reservedBytes) {
154
+ async function pruneActivities(rootDir, sessionLogRootDir, now, maxAgeMs, maxEntries, maxTotalBytes, reservedBytes, testHooks) {
40
155
  const entries = await readdir(rootDir, { withFileTypes: true });
41
156
  await Promise.all(entries.map(async (entry) => {
42
157
  if (!entry.isDirectory() || !ActivityIdSchema.safeParse(entry.name).success) {
43
158
  return;
44
159
  }
45
160
  const activityDir = join(rootDir, entry.name);
161
+ let admitted;
46
162
  try {
47
- const parsed = ActivityStateV1Schema.safeParse(JSON.parse(await readFile(join(activityDir, ACTIVITY_STATE_FILENAME), "utf8")));
163
+ admitted = await inspectActivityDirectory(activityDir, entry.name);
164
+ if (admitted === undefined)
165
+ return;
166
+ const incarnation = captureActivityDirectoryIncarnation(admitted);
167
+ const parsed = ActivityStateReadSchema.safeParse(JSON.parse(await readFile(join(admitted.path, ACTIVITY_STATE_FILENAME), "utf8")));
168
+ await verifyActivityDirectory(admitted.path, admitted);
48
169
  if (!parsed.success ||
49
170
  parsed.data.phase !== "terminal" ||
50
171
  parsed.data.completedAt === undefined) {
@@ -52,11 +173,10 @@ async function pruneActivities(rootDir, sessionLogRootDir, activityIdentityReade
52
173
  }
53
174
  if (now.getTime() - Date.parse(parsed.data.completedAt) > maxAgeMs) {
54
175
  await pruneActivityDirectory({
55
- rootDir,
56
176
  sessionLogRootDir,
57
177
  activityId: entry.name,
58
- activityDir,
59
- activityIdentityReader,
178
+ incarnation,
179
+ testHooks,
60
180
  });
61
181
  }
62
182
  }
@@ -68,6 +188,9 @@ async function pruneActivities(rootDir, sessionLogRootDir, activityIdentityReade
68
188
  }, "activity journal could not inspect a retention candidate");
69
189
  }
70
190
  }
191
+ finally {
192
+ await admitted?.handle.close().catch(() => undefined);
193
+ }
71
194
  }));
72
195
  const survivors = await readdir(rootDir, { withFileTypes: true });
73
196
  const terminal = [];
@@ -76,35 +199,69 @@ async function pruneActivities(rootDir, sessionLogRootDir, activityIdentityReade
76
199
  if (!entry.isDirectory())
77
200
  continue;
78
201
  const activityDir = join(rootDir, entry.name);
202
+ let admitted;
79
203
  let activityBytes = 0;
204
+ let bytesBoundToIncarnation = false;
205
+ if (!ActivityIdSchema.safeParse(entry.name).success) {
206
+ try {
207
+ for (const child of await readdir(activityDir, { withFileTypes: true })) {
208
+ if (!child.isFile())
209
+ continue;
210
+ activityBytes += (await stat(join(activityDir, child.name))).size;
211
+ }
212
+ }
213
+ catch {
214
+ // An unreadable non-activity directory still occupies entry capacity.
215
+ }
216
+ totalBytes += activityBytes;
217
+ continue;
218
+ }
80
219
  try {
81
- for (const child of await readdir(activityDir, { withFileTypes: true })) {
220
+ admitted = await inspectActivityDirectory(activityDir, entry.name);
221
+ if (admitted === undefined)
222
+ continue;
223
+ const incarnation = captureActivityDirectoryIncarnation(admitted);
224
+ for (const child of await readdir(admitted.path, { withFileTypes: true })) {
82
225
  if (!child.isFile())
83
226
  continue;
84
- activityBytes += (await stat(join(activityDir, child.name))).size;
227
+ activityBytes += (await stat(join(admitted.path, child.name))).size;
85
228
  }
86
- }
87
- catch {
88
- // A concurrent terminal update is reconciled at the next admission.
89
- }
90
- totalBytes += activityBytes;
91
- if (!ActivityIdSchema.safeParse(entry.name).success)
92
- continue;
93
- try {
94
- const parsed = ActivityStateV1Schema.safeParse(JSON.parse(await readFile(join(activityDir, ACTIVITY_STATE_FILENAME), "utf8")));
229
+ await verifyActivityDirectory(admitted.path, admitted);
230
+ totalBytes += activityBytes;
231
+ bytesBoundToIncarnation = true;
232
+ const parsed = ActivityStateReadSchema.safeParse(JSON.parse(await readFile(join(admitted.path, ACTIVITY_STATE_FILENAME), "utf8")));
233
+ await verifyActivityDirectory(admitted.path, admitted);
95
234
  if (parsed.success &&
96
235
  parsed.data.phase === "terminal" &&
97
236
  parsed.data.completedAt !== undefined) {
98
237
  terminal.push({
99
238
  activityId: entry.name,
100
- path: activityDir,
101
239
  completedAtMs: Date.parse(parsed.data.completedAt),
102
240
  bytes: activityBytes,
241
+ incarnation,
103
242
  });
104
243
  }
105
244
  }
106
245
  catch {
107
- // Unreadable entries are not producer-owned retention candidates.
246
+ // Unreadable entries are not producer-owned retention candidates. If
247
+ // identity churn prevented a bound scan, account the current path only.
248
+ if (!bytesBoundToIncarnation) {
249
+ activityBytes = 0;
250
+ try {
251
+ for (const child of await readdir(activityDir, { withFileTypes: true })) {
252
+ if (!child.isFile())
253
+ continue;
254
+ activityBytes += (await stat(join(activityDir, child.name))).size;
255
+ }
256
+ }
257
+ catch {
258
+ // Entry capacity still prevents unsafe admission at the count cap.
259
+ }
260
+ totalBytes += activityBytes;
261
+ }
262
+ }
263
+ finally {
264
+ await admitted?.handle.close().catch(() => undefined);
108
265
  }
109
266
  }
110
267
  terminal.sort((left, right) => left.completedAtMs - right.completedAtMs);
@@ -112,11 +269,10 @@ async function pruneActivities(rootDir, sessionLogRootDir, activityIdentityReade
112
269
  while (entryCount >= maxEntries && terminal.length > 0) {
113
270
  const victim = terminal.shift();
114
271
  if (await pruneActivityDirectory({
115
- rootDir,
116
272
  sessionLogRootDir,
117
273
  activityId: victim.activityId,
118
- activityDir: victim.path,
119
- activityIdentityReader,
274
+ incarnation: victim.incarnation,
275
+ testHooks,
120
276
  })) {
121
277
  entryCount -= 1;
122
278
  totalBytes -= victim.bytes;
@@ -126,34 +282,99 @@ async function pruneActivities(rootDir, sessionLogRootDir, activityIdentityReade
126
282
  terminal.length > 0) {
127
283
  const victim = terminal.shift();
128
284
  if (await pruneActivityDirectory({
129
- rootDir,
130
285
  sessionLogRootDir,
131
286
  activityId: victim.activityId,
132
- activityDir: victim.path,
133
- activityIdentityReader,
287
+ incarnation: victim.incarnation,
288
+ testHooks,
134
289
  })) {
135
290
  entryCount -= 1;
136
291
  totalBytes -= victim.bytes;
137
292
  }
138
293
  }
139
- return entryCount < maxEntries && totalBytes + reservedBytes <= maxTotalBytes;
294
+ return entryCount < maxEntries &&
295
+ totalBytes + reservedBytes <= maxTotalBytes;
140
296
  }
141
297
  async function pruneActivityDirectory(input) {
142
- const identity = await input.activityIdentityReader.resolveCurrent(input.rootDir, input.activityId);
143
- if (identity.kind !== "resolved") {
144
- logger.debug({ activity_id: input.activityId }, "activity retention skipped because exact generation was unavailable");
298
+ const removed = await removeActivityDirectoryIncarnation({
299
+ activityId: input.activityId,
300
+ incarnation: input.incarnation,
301
+ reason: "retention",
302
+ testHooks: input.testHooks,
303
+ });
304
+ if (removed.kind !== "removed") {
305
+ logger.debug({ activity_id: input.activityId, removal: removed.kind }, "activity retention skipped because exact generation was unavailable");
145
306
  return false;
146
307
  }
147
- const sessionLogIdentity = await readSessionLogGeneration(input.activityDir);
148
- const sessionLogGeneration = sessionLogIdentity.kind === "available"
149
- ? sessionLogIdentity.sessionLogGeneration
150
- : sessionLogIdentity.kind === "legacy"
151
- ? identity.activityGeneration
308
+ await cleanupActivitySessionLog({
309
+ sessionLogRootDir: input.sessionLogRootDir,
310
+ activityId: input.activityId,
311
+ localGeneration: input.incarnation.localGeneration,
312
+ sessionLogIdentity: removed.sessionLogIdentity,
313
+ });
314
+ return true;
315
+ }
316
+ async function removeActivityDirectoryIncarnation(input) {
317
+ let admitted;
318
+ try {
319
+ admitted = await reopenActivityDirectory(input.incarnation);
320
+ if (admitted === undefined)
321
+ return { kind: "missing" };
322
+ const activityDir = admitted.path;
323
+ const quarantinePath = join(dirname(activityDir), `.retired-${input.activityId}-${randomUUID()}`);
324
+ await verifyActivityDirectory(activityDir, admitted);
325
+ await input.testHooks.beforeActivityDirectoryQuarantine?.(input.activityId, input.reason);
326
+ try {
327
+ await rename(activityDir, quarantinePath);
328
+ }
329
+ catch (error) {
330
+ return isFileSystemError(error, "ENOENT")
331
+ ? { kind: "missing" }
332
+ : { kind: "unavailable" };
333
+ }
334
+ try {
335
+ await verifyActivityDirectory(quarantinePath, admitted);
336
+ }
337
+ catch (error) {
338
+ // A same-ID replacement was moved instead of the admitted incarnation.
339
+ // Restore it only while the public name remains free; never delete it.
340
+ if (await lstat(activityDir).catch(() => undefined) === undefined) {
341
+ await rename(quarantinePath, activityDir).catch(() => undefined);
342
+ }
343
+ throw error;
344
+ }
345
+ const sessionLogIdentity = await readSessionLogGeneration(quarantinePath);
346
+ await verifyActivityDirectory(quarantinePath, admitted);
347
+ try {
348
+ await rm(quarantinePath, { recursive: true, force: false });
349
+ }
350
+ catch (error) {
351
+ if (await lstat(activityDir).catch(() => undefined) === undefined) {
352
+ await rename(quarantinePath, activityDir).catch(() => undefined);
353
+ }
354
+ return isFileSystemError(error, "ENOENT")
355
+ ? { kind: "unsafe" }
356
+ : { kind: "unavailable" };
357
+ }
358
+ return { kind: "removed", sessionLogIdentity };
359
+ }
360
+ catch (error) {
361
+ return error instanceof UnsafeActivityDirectoryError
362
+ ? { kind: "unsafe" }
363
+ : { kind: "unavailable" };
364
+ }
365
+ finally {
366
+ await admitted?.handle.close().catch(() => undefined);
367
+ }
368
+ }
369
+ async function cleanupActivitySessionLog(input) {
370
+ const sessionLogGeneration = input.sessionLogIdentity.kind === "available"
371
+ ? input.sessionLogIdentity.sessionLogGeneration
372
+ : input.sessionLogIdentity.kind === "legacy"
373
+ ? input.localGeneration
152
374
  : undefined;
153
- await rm(input.activityDir, { recursive: true, force: true });
154
375
  if (sessionLogGeneration === undefined) {
155
376
  logger.warn({ activity_id: input.activityId }, "activity parent pruned but portable session-log identity was unsafe");
156
- return true;
377
+ return;
157
378
  }
158
379
  try {
159
380
  const cleanup = await removeSessionLogGeneration({
@@ -168,7 +389,6 @@ async function pruneActivityDirectory(input) {
168
389
  catch {
169
390
  logger.warn({ activity_id: input.activityId }, "activity parent pruned but exact session-log cleanup failed");
170
391
  }
171
- return true;
172
392
  }
173
393
  function truncateUtf8(text, maxBytes) {
174
394
  const source = Buffer.from(text, "utf8");
@@ -190,15 +410,15 @@ function truncateUtf8(text, maxBytes) {
190
410
  return { bytes: Buffer.alloc(0), truncated: true };
191
411
  }
192
412
  class FileActivityObservation {
193
- activityDir;
413
+ activityDirectory;
194
414
  options;
195
415
  identity;
196
416
  state;
197
417
  writeChain = Promise.resolve();
198
418
  terminal = false;
199
419
  heartbeat;
200
- constructor(activityDir, options, initialState, identity) {
201
- this.activityDir = activityDir;
420
+ constructor(activityDirectory, options, initialState, identity) {
421
+ this.activityDirectory = activityDirectory;
202
422
  this.options = options;
203
423
  this.identity = identity;
204
424
  this.state = initialState;
@@ -233,6 +453,27 @@ class FileActivityObservation {
233
453
  await this.persistState();
234
454
  });
235
455
  }
456
+ async observe(observed) {
457
+ await this.enqueue(async () => {
458
+ if (this.terminal)
459
+ return;
460
+ if (!observedInvocationPersistenceEnabled())
461
+ return;
462
+ // The first observation is sticky; later conflicting ones are ignored.
463
+ if (this.observedInvocationRecorded())
464
+ return;
465
+ try {
466
+ this.state = this.withObservedInvocation(this.state, observed);
467
+ await this.persistState();
468
+ }
469
+ catch (error) {
470
+ logger.warn({
471
+ activity_id: this.state.activityId,
472
+ err: error instanceof Error ? error.message : String(error),
473
+ }, "activity observed-invocation persistence failed; clink execution remains authoritative");
474
+ }
475
+ });
476
+ }
236
477
  async complete(completion) {
237
478
  await this.enqueue(async () => {
238
479
  if (this.terminal)
@@ -253,7 +494,7 @@ class FileActivityObservation {
253
494
  }, "activity result persistence failed; committing terminal metadata without content");
254
495
  }
255
496
  const timestamp = this.options.now().toISOString();
256
- this.state = {
497
+ let nextState = {
257
498
  ...this.state,
258
499
  phase: "terminal",
259
500
  status: completion.status,
@@ -262,9 +503,39 @@ class FileActivityObservation {
262
503
  outcome: { kind: completion.outcomeKind },
263
504
  ...(result === undefined ? {} : { result }),
264
505
  };
506
+ if (completion.observedInvocation !== undefined &&
507
+ observedInvocationPersistenceEnabled() &&
508
+ !this.observedInvocationRecorded()) {
509
+ try {
510
+ nextState = this.withObservedInvocation(nextState, completion.observedInvocation);
511
+ }
512
+ catch (error) {
513
+ logger.warn({
514
+ activity_id: this.state.activityId,
515
+ err: error instanceof Error ? error.message : String(error),
516
+ }, "activity observed-invocation persistence failed; committing terminal metadata without it");
517
+ }
518
+ }
519
+ this.state = nextState;
265
520
  await this.persistState();
266
521
  });
267
522
  }
523
+ observedInvocationRecorded() {
524
+ return this.state.schemaVersion === "clinkx-dashboard-activity-v3" &&
525
+ this.state.observedInvocation !== undefined;
526
+ }
527
+ /**
528
+ * Best-effort V3 upgrade carrying any existing pre-dispatch audit (a V2
529
+ * begin) plus the runtime observation; a V1 begin upgrades with the
530
+ * observation alone.
531
+ */
532
+ withObservedInvocation(state, observed) {
533
+ return ActivityStateV3Schema.parse({
534
+ ...state,
535
+ schemaVersion: "clinkx-dashboard-activity-v3",
536
+ observedInvocation: observed,
537
+ });
538
+ }
268
539
  async enqueue(task) {
269
540
  const next = this.writeChain.then(task, task).catch((error) => {
270
541
  logger.warn({
@@ -279,7 +550,7 @@ class FileActivityObservation {
279
550
  if (!this.options.retainResult || resultText === undefined)
280
551
  return undefined;
281
552
  const { bytes, truncated } = truncateUtf8(resultText, this.options.maxResultBytes);
282
- await writeFileAtomic(join(this.activityDir, ACTIVITY_RESULT_FILENAME), bytes);
553
+ await writeActivityFileAtomic(this.activityDirectory, ACTIVITY_RESULT_FILENAME, bytes);
283
554
  return {
284
555
  path: ACTIVITY_RESULT_FILENAME,
285
556
  bytes: bytes.byteLength,
@@ -288,7 +559,7 @@ class FileActivityObservation {
288
559
  };
289
560
  }
290
561
  async persistState() {
291
- await writeFileAtomic(join(this.activityDir, ACTIVITY_STATE_FILENAME), serializedState(this.state));
562
+ await writeActivityFileAtomic(this.activityDirectory, ACTIVITY_STATE_FILENAME, serializedState(this.state));
292
563
  }
293
564
  }
294
565
  export function createActivityJournal(options) {
@@ -296,6 +567,8 @@ export function createActivityJournal(options) {
296
567
  const sessionLogRootDir = options.sessionLogRootDir ??
297
568
  join(options.rootDir, "..", "session-logs");
298
569
  const now = options.now ?? (() => new Date());
570
+ const persistInvocationAuditV2 = invocationAuditV2PersistenceEnabled();
571
+ const testHooks = options.testHooks ?? {};
299
572
  const maxResultBytes = Number.isSafeInteger(options.maxResultBytes) &&
300
573
  (options.maxResultBytes ?? 0) > 0
301
574
  ? Math.min(options.maxResultBytes, DEFAULT_MAX_ACTIVITY_RESULT_BYTES)
@@ -314,12 +587,43 @@ export function createActivityJournal(options) {
314
587
  (normalized.retainResult ? normalized.maxResultBytes : 0);
315
588
  let admissionChain = Promise.resolve();
316
589
  async function beginOne(input) {
590
+ let claimedDirectory;
317
591
  try {
318
592
  const parsed = ActivityIdSchema.parse(input.activityId);
593
+ const admissionTime = now();
594
+ const timestamp = admissionTime.toISOString();
595
+ const commonState = {
596
+ activityId: parsed,
597
+ kind: input.kind,
598
+ phase: "resolving",
599
+ status: "running",
600
+ adapter: input.adapter,
601
+ role: input.role,
602
+ startedAt: timestamp,
603
+ stateUpdatedAt: timestamp,
604
+ leaseUpdatedAt: timestamp,
605
+ ...(input.projectFolders === undefined || input.projectFolders.length === 0
606
+ ? {}
607
+ : { projectFolders: [...input.projectFolders] }),
608
+ };
609
+ const initialState = !persistInvocationAuditV2 || input.invocation === undefined
610
+ ? ActivityStateV1Schema.parse({
611
+ schemaVersion: "clinkx-dashboard-activity-v1",
612
+ ...commonState,
613
+ })
614
+ : ActivityStateV2Schema.parse({
615
+ schemaVersion: "clinkx-dashboard-activity-v2",
616
+ ...commonState,
617
+ invocation: input.invocation,
618
+ });
619
+ // Admission is transactional from this point forward: validate both the
620
+ // complete schema and the producer byte cap before mkdir or retention can
621
+ // mutate observable state.
622
+ const initialStateBytes = serializedState(initialState);
319
623
  await mkdir(options.rootDir, { recursive: true, mode: 0o700 });
320
624
  await chmod(options.rootDir, 0o700);
321
625
  try {
322
- const admitted = await pruneActivities(options.rootDir, sessionLogRootDir, activityIdentityReader, now(), maxAgeMs, maxEntries, maxTotalBytes, reservedBytes);
626
+ const admitted = await pruneActivities(options.rootDir, sessionLogRootDir, admissionTime, maxAgeMs, maxEntries, maxTotalBytes, reservedBytes, testHooks);
323
627
  if (!admitted) {
324
628
  logger.warn({ activity_id: input.activityId }, "activity journal capacity is occupied by live or unreadable records; observation disabled");
325
629
  return NOOP_OBSERVATION;
@@ -334,35 +638,56 @@ export function createActivityJournal(options) {
334
638
  }
335
639
  const activityDir = join(options.rootDir, parsed);
336
640
  await mkdir(activityDir, { mode: 0o700 });
337
- await chmod(activityDir, 0o700);
338
- const sessionLogGeneration = await createSessionLogGeneration(activityDir);
339
- const timestamp = now().toISOString();
340
- const initialState = ActivityStateV1Schema.parse({
341
- schemaVersion: "clinkx-dashboard-activity-v1",
342
- activityId: parsed,
343
- kind: input.kind,
344
- phase: "resolving",
345
- status: "running",
346
- adapter: input.adapter,
347
- role: input.role,
348
- startedAt: timestamp,
349
- stateUpdatedAt: timestamp,
350
- leaseUpdatedAt: timestamp,
351
- ...(input.projectFolders === undefined || input.projectFolders.length === 0
352
- ? {}
353
- : { projectFolders: [...input.projectFolders] }),
354
- });
355
- await writeFileAtomic(join(activityDir, ACTIVITY_STATE_FILENAME), serializedState(initialState));
641
+ const admittedDirectory = await inspectActivityDirectory(activityDir, parsed);
642
+ if (admittedDirectory === undefined) {
643
+ throw new Error("activity journal directory identity unavailable");
644
+ }
645
+ claimedDirectory = captureActivityDirectoryIncarnation(admittedDirectory);
646
+ const createdSessionLogGeneration = await (async () => {
647
+ try {
648
+ await admittedDirectory.handle.chmod(0o700);
649
+ await verifyActivityDirectory(admittedDirectory.path, admittedDirectory);
650
+ const generation = await createSessionLogGeneration(admittedDirectory.path);
651
+ await verifyActivityDirectory(admittedDirectory.path, admittedDirectory);
652
+ return generation;
653
+ }
654
+ finally {
655
+ await admittedDirectory.handle.close().catch(() => undefined);
656
+ }
657
+ })();
658
+ await writeActivityFileAtomic(claimedDirectory, ACTIVITY_STATE_FILENAME, initialStateBytes);
356
659
  const identity = await activityIdentityReader.resolveCurrent(options.rootDir, parsed);
357
- if (identity.kind !== "resolved") {
660
+ if (identity.kind !== "resolved" ||
661
+ identity.activityGeneration !== claimedDirectory.localGeneration) {
358
662
  throw new Error("activity journal directory identity unavailable");
359
663
  }
360
- return new FileActivityObservation(activityDir, normalized, initialState, Object.freeze({
664
+ return new FileActivityObservation(claimedDirectory, normalized, initialState, Object.freeze({
361
665
  activityId: parsed,
362
- activityGeneration: sessionLogGeneration,
666
+ activityGeneration: createdSessionLogGeneration,
363
667
  }));
364
668
  }
365
669
  catch (error) {
670
+ if (claimedDirectory !== undefined) {
671
+ try {
672
+ const removed = await removeActivityDirectoryIncarnation({
673
+ activityId: claimedDirectory.activityId,
674
+ incarnation: claimedDirectory,
675
+ reason: "admission_cleanup",
676
+ testHooks,
677
+ });
678
+ if (removed.kind === "removed") {
679
+ await cleanupActivitySessionLog({
680
+ sessionLogRootDir,
681
+ activityId: claimedDirectory.activityId,
682
+ localGeneration: claimedDirectory.localGeneration,
683
+ sessionLogIdentity: removed.sessionLogIdentity,
684
+ });
685
+ }
686
+ }
687
+ catch {
688
+ // Cleanup is best-effort, but never widens beyond the exact incarnation.
689
+ }
690
+ }
366
691
  logger.warn({
367
692
  activity_id: input.activityId,
368
693
  err: error instanceof Error ? error.message : String(error),