@caupulican/pi-adaptative 0.81.39 → 0.81.41

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 (884) hide show
  1. package/CHANGELOG.md +77 -0
  2. package/README.md +2 -0
  3. package/dist/bundled-resources/extensions/tmux-agent-manager/dispatch-grant.d.ts +2 -0
  4. package/dist/bundled-resources/extensions/tmux-agent-manager/dispatch-grant.d.ts.map +1 -1
  5. package/dist/bundled-resources/extensions/tmux-agent-manager/dispatch-grant.js +3 -0
  6. package/dist/bundled-resources/extensions/tmux-agent-manager/dispatch-grant.js.map +1 -1
  7. package/dist/bundled-resources/extensions/tmux-agent-manager/dispatch-grant.ts +4 -0
  8. package/dist/bundled-resources/extensions/tmux-agent-manager/index.d.ts +9 -0
  9. package/dist/bundled-resources/extensions/tmux-agent-manager/index.d.ts.map +1 -1
  10. package/dist/bundled-resources/extensions/tmux-agent-manager/index.js +290 -107
  11. package/dist/bundled-resources/extensions/tmux-agent-manager/index.js.map +1 -1
  12. package/dist/bundled-resources/extensions/tmux-agent-manager/index.ts +357 -122
  13. package/dist/bundled-resources/skills/tool-call-repair/references/repair-catalogue.md +2 -2
  14. package/dist/cli/args.d.ts +4 -0
  15. package/dist/cli/args.d.ts.map +1 -1
  16. package/dist/cli/args.js +31 -1
  17. package/dist/cli/args.js.map +1 -1
  18. package/dist/core/agent-directory-layout.d.ts +15 -0
  19. package/dist/core/agent-directory-layout.d.ts.map +1 -0
  20. package/dist/core/agent-directory-layout.js +72 -0
  21. package/dist/core/agent-directory-layout.js.map +1 -0
  22. package/dist/core/agent-paths.d.ts +25 -2
  23. package/dist/core/agent-paths.d.ts.map +1 -1
  24. package/dist/core/agent-paths.js +79 -4
  25. package/dist/core/agent-paths.js.map +1 -1
  26. package/dist/core/agent-session-contracts.d.ts +293 -0
  27. package/dist/core/agent-session-contracts.d.ts.map +1 -0
  28. package/dist/core/agent-session-contracts.js +26 -0
  29. package/dist/core/agent-session-contracts.js.map +1 -0
  30. package/dist/core/agent-session-runtime.d.ts +52 -8
  31. package/dist/core/agent-session-runtime.d.ts.map +1 -1
  32. package/dist/core/agent-session-runtime.js +400 -79
  33. package/dist/core/agent-session-runtime.js.map +1 -1
  34. package/dist/core/agent-session-services.d.ts +1 -0
  35. package/dist/core/agent-session-services.d.ts.map +1 -1
  36. package/dist/core/agent-session-services.js +1 -0
  37. package/dist/core/agent-session-services.js.map +1 -1
  38. package/dist/core/agent-session.d.ts +74 -518
  39. package/dist/core/agent-session.d.ts.map +1 -1
  40. package/dist/core/agent-session.js +370 -1432
  41. package/dist/core/agent-session.js.map +1 -1
  42. package/dist/core/auth-storage.d.ts.map +1 -1
  43. package/dist/core/auth-storage.js +3 -1
  44. package/dist/core/auth-storage.js.map +1 -1
  45. package/dist/core/autonomy/approval-gate.d.ts +3 -2
  46. package/dist/core/autonomy/approval-gate.d.ts.map +1 -1
  47. package/dist/core/autonomy/approval-gate.js +6 -28
  48. package/dist/core/autonomy/approval-gate.js.map +1 -1
  49. package/dist/core/autonomy/bounded-completion.d.ts +1 -1
  50. package/dist/core/autonomy/bounded-completion.d.ts.map +1 -1
  51. package/dist/core/autonomy/bounded-completion.js +1 -1
  52. package/dist/core/autonomy/bounded-completion.js.map +1 -1
  53. package/dist/core/autonomy/contracts.d.ts +20 -12
  54. package/dist/core/autonomy/contracts.d.ts.map +1 -1
  55. package/dist/core/autonomy/contracts.js.map +1 -1
  56. package/dist/core/autonomy/foreground-envelope.d.ts +8 -2
  57. package/dist/core/autonomy/foreground-envelope.d.ts.map +1 -1
  58. package/dist/core/autonomy/foreground-envelope.js +8 -25
  59. package/dist/core/autonomy/foreground-envelope.js.map +1 -1
  60. package/dist/core/autonomy/gates.d.ts.map +1 -1
  61. package/dist/core/autonomy/gates.js +8 -10
  62. package/dist/core/autonomy/gates.js.map +1 -1
  63. package/dist/core/autonomy/lane-private-paths.d.ts +2 -0
  64. package/dist/core/autonomy/lane-private-paths.d.ts.map +1 -0
  65. package/dist/core/autonomy/lane-private-paths.js +16 -0
  66. package/dist/core/autonomy/lane-private-paths.js.map +1 -0
  67. package/dist/core/autonomy/lane-tool-surface.d.ts +10 -0
  68. package/dist/core/autonomy/lane-tool-surface.d.ts.map +1 -1
  69. package/dist/core/autonomy/lane-tool-surface.js +48 -9
  70. package/dist/core/autonomy/lane-tool-surface.js.map +1 -1
  71. package/dist/core/autonomy/lane-tracker.d.ts +14 -0
  72. package/dist/core/autonomy/lane-tracker.d.ts.map +1 -1
  73. package/dist/core/autonomy/lane-tracker.js +38 -0
  74. package/dist/core/autonomy/lane-tracker.js.map +1 -1
  75. package/dist/core/autonomy/risk-assessment.d.ts +1 -2
  76. package/dist/core/autonomy/risk-assessment.d.ts.map +1 -1
  77. package/dist/core/autonomy/risk-assessment.js +1 -123
  78. package/dist/core/autonomy/risk-assessment.js.map +1 -1
  79. package/dist/core/autonomy/session-lane-record.d.ts +4 -4
  80. package/dist/core/autonomy/session-lane-record.d.ts.map +1 -1
  81. package/dist/core/autonomy/session-lane-record.js +16 -29
  82. package/dist/core/autonomy/session-lane-record.js.map +1 -1
  83. package/dist/core/autonomy/telemetry-events.d.ts +1 -1
  84. package/dist/core/autonomy/telemetry-events.d.ts.map +1 -1
  85. package/dist/core/autonomy/telemetry-events.js +1 -1
  86. package/dist/core/autonomy/telemetry-events.js.map +1 -1
  87. package/dist/core/autonomy-telemetry.d.ts +7 -7
  88. package/dist/core/autonomy-telemetry.d.ts.map +1 -1
  89. package/dist/core/autonomy-telemetry.js +9 -9
  90. package/dist/core/autonomy-telemetry.js.map +1 -1
  91. package/dist/core/background-lane-controller.d.ts +56 -236
  92. package/dist/core/background-lane-controller.d.ts.map +1 -1
  93. package/dist/core/background-lane-controller.js +141 -1273
  94. package/dist/core/background-lane-controller.js.map +1 -1
  95. package/dist/core/bedrock-sso-login.d.ts +16 -0
  96. package/dist/core/bedrock-sso-login.d.ts.map +1 -0
  97. package/dist/core/bedrock-sso-login.js +98 -0
  98. package/dist/core/bedrock-sso-login.js.map +1 -0
  99. package/dist/core/capability-contract.d.ts +4 -0
  100. package/dist/core/capability-contract.d.ts.map +1 -0
  101. package/dist/core/capability-contract.js +28 -0
  102. package/dist/core/capability-contract.js.map +1 -0
  103. package/dist/core/compaction-controller.d.ts +90 -0
  104. package/dist/core/compaction-controller.d.ts.map +1 -0
  105. package/dist/core/compaction-controller.js +452 -0
  106. package/dist/core/compaction-controller.js.map +1 -0
  107. package/dist/core/compaction-support.d.ts +13 -17
  108. package/dist/core/compaction-support.d.ts.map +1 -1
  109. package/dist/core/compaction-support.js +3 -3
  110. package/dist/core/compaction-support.js.map +1 -1
  111. package/dist/core/context/context-artifacts.d.ts +6 -2
  112. package/dist/core/context/context-artifacts.d.ts.map +1 -1
  113. package/dist/core/context/context-artifacts.js +34 -2
  114. package/dist/core/context/context-artifacts.js.map +1 -1
  115. package/dist/core/context/context-store-retention.d.ts +8 -2
  116. package/dist/core/context/context-store-retention.d.ts.map +1 -1
  117. package/dist/core/context/context-store-retention.js +158 -35
  118. package/dist/core/context/context-store-retention.js.map +1 -1
  119. package/dist/core/context/okf-memory.d.ts.map +1 -1
  120. package/dist/core/context/okf-memory.js +9 -11
  121. package/dist/core/context/okf-memory.js.map +1 -1
  122. package/dist/core/context/sqlite-runtime-index.d.ts.map +1 -1
  123. package/dist/core/context/sqlite-runtime-index.js +6 -8
  124. package/dist/core/context/sqlite-runtime-index.js.map +1 -1
  125. package/dist/core/context-gc.d.ts +3 -0
  126. package/dist/core/context-gc.d.ts.map +1 -1
  127. package/dist/core/context-gc.js +10 -5
  128. package/dist/core/context-gc.js.map +1 -1
  129. package/dist/core/context-pipeline.d.ts +1 -1
  130. package/dist/core/context-pipeline.d.ts.map +1 -1
  131. package/dist/core/context-pipeline.js +16 -37
  132. package/dist/core/context-pipeline.js.map +1 -1
  133. package/dist/core/cost/cost-summary.d.ts.map +1 -1
  134. package/dist/core/cost/cost-summary.js +5 -3
  135. package/dist/core/cost/cost-summary.js.map +1 -1
  136. package/dist/core/cost/daily-usage.d.ts.map +1 -1
  137. package/dist/core/cost/daily-usage.js +4 -5
  138. package/dist/core/cost/daily-usage.js.map +1 -1
  139. package/dist/core/default-tool-surface.d.ts.map +1 -1
  140. package/dist/core/default-tool-surface.js +2 -0
  141. package/dist/core/default-tool-surface.js.map +1 -1
  142. package/dist/core/delegation/managed-lane-controller.d.ts +34 -0
  143. package/dist/core/delegation/managed-lane-controller.d.ts.map +1 -0
  144. package/dist/core/delegation/managed-lane-controller.js +217 -0
  145. package/dist/core/delegation/managed-lane-controller.js.map +1 -0
  146. package/dist/core/delegation/session-worker-claim.d.ts +44 -0
  147. package/dist/core/delegation/session-worker-claim.d.ts.map +1 -0
  148. package/dist/core/delegation/session-worker-claim.js +98 -0
  149. package/dist/core/delegation/session-worker-claim.js.map +1 -0
  150. package/dist/core/delegation/worker-actions.d.ts +7 -5
  151. package/dist/core/delegation/worker-actions.d.ts.map +1 -1
  152. package/dist/core/delegation/worker-actions.js +20 -4
  153. package/dist/core/delegation/worker-actions.js.map +1 -1
  154. package/dist/core/delegation/{worker-result.d.ts → worker-claim.d.ts} +13 -13
  155. package/dist/core/delegation/worker-claim.d.ts.map +1 -0
  156. package/dist/core/delegation/{worker-result.js → worker-claim.js} +61 -51
  157. package/dist/core/delegation/worker-claim.js.map +1 -0
  158. package/dist/core/delegation/worker-delegation-controller.d.ts +110 -0
  159. package/dist/core/delegation/worker-delegation-controller.d.ts.map +1 -0
  160. package/dist/core/delegation/worker-delegation-controller.js +861 -0
  161. package/dist/core/delegation/worker-delegation-controller.js.map +1 -0
  162. package/dist/core/delegation/worker-delegation-request.d.ts +11 -0
  163. package/dist/core/delegation/worker-delegation-request.d.ts.map +1 -0
  164. package/dist/core/delegation/worker-delegation-request.js +2 -0
  165. package/dist/core/delegation/worker-delegation-request.js.map +1 -0
  166. package/dist/core/delegation/worker-dispatch-scheduler.d.ts +40 -0
  167. package/dist/core/delegation/worker-dispatch-scheduler.d.ts.map +1 -0
  168. package/dist/core/delegation/worker-dispatch-scheduler.js +93 -0
  169. package/dist/core/delegation/worker-dispatch-scheduler.js.map +1 -0
  170. package/dist/core/delegation/worker-execution-policy.d.ts +62 -0
  171. package/dist/core/delegation/worker-execution-policy.d.ts.map +1 -0
  172. package/dist/core/delegation/worker-execution-policy.js +160 -0
  173. package/dist/core/delegation/worker-execution-policy.js.map +1 -0
  174. package/dist/core/delegation/worker-lane-projection.d.ts +14 -0
  175. package/dist/core/delegation/worker-lane-projection.d.ts.map +1 -0
  176. package/dist/core/delegation/worker-lane-projection.js +100 -0
  177. package/dist/core/delegation/worker-lane-projection.js.map +1 -0
  178. package/dist/core/delegation/worker-lifecycle.d.ts +97 -0
  179. package/dist/core/delegation/worker-lifecycle.d.ts.map +1 -0
  180. package/dist/core/delegation/worker-lifecycle.js +339 -0
  181. package/dist/core/delegation/worker-lifecycle.js.map +1 -0
  182. package/dist/core/delegation/worker-notification-coordinator.d.ts +36 -0
  183. package/dist/core/delegation/worker-notification-coordinator.d.ts.map +1 -0
  184. package/dist/core/delegation/worker-notification-coordinator.js +90 -0
  185. package/dist/core/delegation/worker-notification-coordinator.js.map +1 -0
  186. package/dist/core/delegation/worker-profile-resolver.d.ts +55 -0
  187. package/dist/core/delegation/worker-profile-resolver.d.ts.map +1 -0
  188. package/dist/core/delegation/worker-profile-resolver.js +113 -0
  189. package/dist/core/delegation/worker-profile-resolver.js.map +1 -0
  190. package/dist/core/delegation/worker-runner.d.ts +25 -14
  191. package/dist/core/delegation/worker-runner.d.ts.map +1 -1
  192. package/dist/core/delegation/worker-runner.js +122 -53
  193. package/dist/core/delegation/worker-runner.js.map +1 -1
  194. package/dist/core/delegation/worker-task-label.d.ts +3 -0
  195. package/dist/core/delegation/worker-task-label.d.ts.map +1 -0
  196. package/dist/core/delegation/worker-task-label.js +11 -0
  197. package/dist/core/delegation/worker-task-label.js.map +1 -0
  198. package/dist/core/doctor.d.ts +8 -0
  199. package/dist/core/doctor.d.ts.map +1 -1
  200. package/dist/core/doctor.js +32 -1
  201. package/dist/core/doctor.js.map +1 -1
  202. package/dist/core/extension-import-authority.d.ts +6 -0
  203. package/dist/core/extension-import-authority.d.ts.map +1 -0
  204. package/dist/core/extension-import-authority.js +10 -0
  205. package/dist/core/extension-import-authority.js.map +1 -0
  206. package/dist/core/extensions/index.d.ts +1 -1
  207. package/dist/core/extensions/index.d.ts.map +1 -1
  208. package/dist/core/extensions/index.js.map +1 -1
  209. package/dist/core/extensions/loader.d.ts +5 -1
  210. package/dist/core/extensions/loader.d.ts.map +1 -1
  211. package/dist/core/extensions/loader.js +52 -18
  212. package/dist/core/extensions/loader.js.map +1 -1
  213. package/dist/core/extensions/runner.d.ts +3 -0
  214. package/dist/core/extensions/runner.d.ts.map +1 -1
  215. package/dist/core/extensions/runner.js +20 -5
  216. package/dist/core/extensions/runner.js.map +1 -1
  217. package/dist/core/extensions/stale-context.d.ts +2 -0
  218. package/dist/core/extensions/stale-context.d.ts.map +1 -0
  219. package/dist/core/extensions/stale-context.js +2 -0
  220. package/dist/core/extensions/stale-context.js.map +1 -0
  221. package/dist/core/extensions/storage.d.ts +8 -0
  222. package/dist/core/extensions/storage.d.ts.map +1 -0
  223. package/dist/core/extensions/storage.js +28 -0
  224. package/dist/core/extensions/storage.js.map +1 -0
  225. package/dist/core/extensions/types.d.ts +78 -21
  226. package/dist/core/extensions/types.d.ts.map +1 -1
  227. package/dist/core/extensions/types.js.map +1 -1
  228. package/dist/core/failure-corpus.d.ts +1 -15
  229. package/dist/core/failure-corpus.d.ts.map +1 -1
  230. package/dist/core/failure-corpus.js +9 -37
  231. package/dist/core/failure-corpus.js.map +1 -1
  232. package/dist/core/foreground-recovery-controller.d.ts +53 -0
  233. package/dist/core/foreground-recovery-controller.d.ts.map +1 -0
  234. package/dist/core/foreground-recovery-controller.js +111 -0
  235. package/dist/core/foreground-recovery-controller.js.map +1 -0
  236. package/dist/core/gateways/channel-provider.d.ts +3 -0
  237. package/dist/core/gateways/channel-provider.d.ts.map +1 -1
  238. package/dist/core/gateways/channel-provider.js +31 -6
  239. package/dist/core/gateways/channel-provider.js.map +1 -1
  240. package/dist/core/goal-loop-controller.d.ts +14 -9
  241. package/dist/core/goal-loop-controller.d.ts.map +1 -1
  242. package/dist/core/goal-loop-controller.js +47 -75
  243. package/dist/core/goal-loop-controller.js.map +1 -1
  244. package/dist/core/goals/compact-goal-context.d.ts +11 -0
  245. package/dist/core/goals/compact-goal-context.d.ts.map +1 -0
  246. package/dist/core/goals/compact-goal-context.js +59 -0
  247. package/dist/core/goals/compact-goal-context.js.map +1 -0
  248. package/dist/core/goals/goal-acceptance.d.ts +11 -0
  249. package/dist/core/goals/goal-acceptance.d.ts.map +1 -0
  250. package/dist/core/goals/goal-acceptance.js +27 -0
  251. package/dist/core/goals/goal-acceptance.js.map +1 -0
  252. package/dist/core/goals/goal-auto-continue-controller.d.ts +28 -0
  253. package/dist/core/goals/goal-auto-continue-controller.d.ts.map +1 -0
  254. package/dist/core/goals/goal-auto-continue-controller.js +90 -0
  255. package/dist/core/goals/goal-auto-continue-controller.js.map +1 -0
  256. package/dist/core/goals/goal-continuation-controller.d.ts +2 -2
  257. package/dist/core/goals/goal-continuation-controller.d.ts.map +1 -1
  258. package/dist/core/goals/goal-continuation-controller.js +52 -11
  259. package/dist/core/goals/goal-continuation-controller.js.map +1 -1
  260. package/dist/core/goals/goal-continuation-defaults.d.ts +4 -29
  261. package/dist/core/goals/goal-continuation-defaults.d.ts.map +1 -1
  262. package/dist/core/goals/goal-continuation-defaults.js +4 -32
  263. package/dist/core/goals/goal-continuation-defaults.js.map +1 -1
  264. package/dist/core/goals/goal-continuation-prompt.d.ts +8 -18
  265. package/dist/core/goals/goal-continuation-prompt.d.ts.map +1 -1
  266. package/dist/core/goals/goal-continuation-prompt.js +9 -179
  267. package/dist/core/goals/goal-continuation-prompt.js.map +1 -1
  268. package/dist/core/goals/goal-lifecycle.d.ts +43 -0
  269. package/dist/core/goals/goal-lifecycle.d.ts.map +1 -0
  270. package/dist/core/goals/goal-lifecycle.js +102 -0
  271. package/dist/core/goals/goal-lifecycle.js.map +1 -0
  272. package/dist/core/goals/goal-record.d.ts +17 -0
  273. package/dist/core/goals/goal-record.d.ts.map +1 -0
  274. package/dist/core/goals/goal-record.js +31 -0
  275. package/dist/core/goals/goal-record.js.map +1 -0
  276. package/dist/core/goals/goal-runtime-snapshot.d.ts +11 -9
  277. package/dist/core/goals/goal-runtime-snapshot.d.ts.map +1 -1
  278. package/dist/core/goals/goal-runtime-snapshot.js +10 -8
  279. package/dist/core/goals/goal-runtime-snapshot.js.map +1 -1
  280. package/dist/core/goals/goal-session-controller.d.ts +42 -0
  281. package/dist/core/goals/goal-session-controller.d.ts.map +1 -0
  282. package/dist/core/goals/goal-session-controller.js +128 -0
  283. package/dist/core/goals/goal-session-controller.js.map +1 -0
  284. package/dist/core/goals/goal-state.d.ts +46 -25
  285. package/dist/core/goals/goal-state.d.ts.map +1 -1
  286. package/dist/core/goals/goal-state.js +91 -24
  287. package/dist/core/goals/goal-state.js.map +1 -1
  288. package/dist/core/goals/goal-tool-core.d.ts +9 -10
  289. package/dist/core/goals/goal-tool-core.d.ts.map +1 -1
  290. package/dist/core/goals/goal-tool-core.js +60 -75
  291. package/dist/core/goals/goal-tool-core.js.map +1 -1
  292. package/dist/core/goals/session-goal-state.d.ts +20 -12
  293. package/dist/core/goals/session-goal-state.d.ts.map +1 -1
  294. package/dist/core/goals/session-goal-state.js +133 -33
  295. package/dist/core/goals/session-goal-state.js.map +1 -1
  296. package/dist/core/human-input-controller.d.ts +47 -0
  297. package/dist/core/human-input-controller.d.ts.map +1 -0
  298. package/dist/core/human-input-controller.js +181 -0
  299. package/dist/core/human-input-controller.js.map +1 -0
  300. package/dist/core/human-input.d.ts +100 -0
  301. package/dist/core/human-input.d.ts.map +1 -0
  302. package/dist/core/human-input.js +362 -0
  303. package/dist/core/human-input.js.map +1 -0
  304. package/dist/core/index.d.ts +9 -1
  305. package/dist/core/index.d.ts.map +1 -1
  306. package/dist/core/index.js +9 -1
  307. package/dist/core/index.js.map +1 -1
  308. package/dist/core/{telemetry.d.ts → install-telemetry.d.ts} +1 -1
  309. package/dist/core/install-telemetry.d.ts.map +1 -0
  310. package/dist/core/{telemetry.js → install-telemetry.js} +1 -1
  311. package/dist/core/install-telemetry.js.map +1 -0
  312. package/dist/core/keybindings.d.ts +17 -1
  313. package/dist/core/keybindings.d.ts.map +1 -1
  314. package/dist/core/keybindings.js +24 -6
  315. package/dist/core/keybindings.js.map +1 -1
  316. package/dist/core/learning/learning-audit.d.ts +2 -4
  317. package/dist/core/learning/learning-audit.d.ts.map +1 -1
  318. package/dist/core/learning/learning-audit.js +9 -29
  319. package/dist/core/learning/learning-audit.js.map +1 -1
  320. package/dist/core/learning/learning-gate.d.ts.map +1 -1
  321. package/dist/core/learning/learning-gate.js +1 -6
  322. package/dist/core/learning/learning-gate.js.map +1 -1
  323. package/dist/core/learning/session-learning-decision.d.ts +2 -4
  324. package/dist/core/learning/session-learning-decision.d.ts.map +1 -1
  325. package/dist/core/learning/session-learning-decision.js +9 -29
  326. package/dist/core/learning/session-learning-decision.js.map +1 -1
  327. package/dist/core/local-runtime-controller.d.ts +1 -1
  328. package/dist/core/local-runtime-controller.d.ts.map +1 -1
  329. package/dist/core/local-runtime-controller.js.map +1 -1
  330. package/dist/core/memory-controller.d.ts.map +1 -1
  331. package/dist/core/memory-controller.js +4 -4
  332. package/dist/core/memory-controller.js.map +1 -1
  333. package/dist/core/model-capability.d.ts +0 -19
  334. package/dist/core/model-capability.d.ts.map +1 -1
  335. package/dist/core/model-capability.js +1 -19
  336. package/dist/core/model-capability.js.map +1 -1
  337. package/dist/core/model-registry.d.ts +6 -1
  338. package/dist/core/model-registry.d.ts.map +1 -1
  339. package/dist/core/model-registry.js +13 -5
  340. package/dist/core/model-registry.js.map +1 -1
  341. package/dist/core/model-resolver.d.ts +17 -2
  342. package/dist/core/model-resolver.d.ts.map +1 -1
  343. package/dist/core/model-resolver.js +38 -25
  344. package/dist/core/model-resolver.js.map +1 -1
  345. package/dist/core/model-router/intent-classifier.d.ts +1 -4
  346. package/dist/core/model-router/intent-classifier.d.ts.map +1 -1
  347. package/dist/core/model-router/intent-classifier.js +1 -156
  348. package/dist/core/model-router/intent-classifier.js.map +1 -1
  349. package/dist/core/model-router-controller.d.ts +1 -1
  350. package/dist/core/model-router-controller.d.ts.map +1 -1
  351. package/dist/core/model-router-controller.js +2 -2
  352. package/dist/core/model-router-controller.js.map +1 -1
  353. package/dist/core/model-selection-controller.d.ts +5 -5
  354. package/dist/core/model-selection-controller.d.ts.map +1 -1
  355. package/dist/core/model-selection-controller.js.map +1 -1
  356. package/dist/core/model-tool-protocol.d.ts +23 -0
  357. package/dist/core/model-tool-protocol.d.ts.map +1 -0
  358. package/dist/core/model-tool-protocol.js +43 -0
  359. package/dist/core/model-tool-protocol.js.map +1 -0
  360. package/dist/core/models/adaptation-store.d.ts +4 -6
  361. package/dist/core/models/adaptation-store.d.ts.map +1 -1
  362. package/dist/core/models/adaptation-store.js +95 -83
  363. package/dist/core/models/adaptation-store.js.map +1 -1
  364. package/dist/core/models/fitness-store.d.ts +2 -12
  365. package/dist/core/models/fitness-store.d.ts.map +1 -1
  366. package/dist/core/models/fitness-store.js +84 -57
  367. package/dist/core/models/fitness-store.js.map +1 -1
  368. package/dist/core/models/host-state-store.d.ts +34 -0
  369. package/dist/core/models/host-state-store.d.ts.map +1 -0
  370. package/dist/core/models/host-state-store.js +86 -0
  371. package/dist/core/models/host-state-store.js.map +1 -0
  372. package/dist/core/orchestration/agent-resume.d.ts +24 -0
  373. package/dist/core/orchestration/agent-resume.d.ts.map +1 -0
  374. package/dist/core/orchestration/agent-resume.js +90 -0
  375. package/dist/core/orchestration/agent-resume.js.map +1 -0
  376. package/dist/core/orchestration/capability-gateway.d.ts +61 -0
  377. package/dist/core/orchestration/capability-gateway.d.ts.map +1 -0
  378. package/dist/core/orchestration/capability-gateway.js +152 -0
  379. package/dist/core/orchestration/capability-gateway.js.map +1 -0
  380. package/dist/core/orchestration/contracts.d.ts +343 -0
  381. package/dist/core/orchestration/contracts.d.ts.map +1 -0
  382. package/dist/core/orchestration/contracts.js +135 -0
  383. package/dist/core/orchestration/contracts.js.map +1 -0
  384. package/dist/core/orchestration/delegation-ledger.d.ts +56 -0
  385. package/dist/core/orchestration/delegation-ledger.d.ts.map +1 -0
  386. package/dist/core/orchestration/delegation-ledger.js +200 -0
  387. package/dist/core/orchestration/delegation-ledger.js.map +1 -0
  388. package/dist/core/orchestration/event-store.d.ts +83 -0
  389. package/dist/core/orchestration/event-store.d.ts.map +1 -0
  390. package/dist/core/orchestration/event-store.js +560 -0
  391. package/dist/core/orchestration/event-store.js.map +1 -0
  392. package/dist/core/orchestration/lane-tool-manifests.d.ts +9 -0
  393. package/dist/core/orchestration/lane-tool-manifests.d.ts.map +1 -0
  394. package/dist/core/orchestration/lane-tool-manifests.js +37 -0
  395. package/dist/core/orchestration/lane-tool-manifests.js.map +1 -0
  396. package/dist/core/orchestration/model-binding.d.ts +15 -0
  397. package/dist/core/orchestration/model-binding.d.ts.map +1 -0
  398. package/dist/core/orchestration/model-binding.js +29 -0
  399. package/dist/core/orchestration/model-binding.js.map +1 -0
  400. package/dist/core/orchestration/policy-compiler.d.ts +49 -0
  401. package/dist/core/orchestration/policy-compiler.d.ts.map +1 -0
  402. package/dist/core/orchestration/policy-compiler.js +191 -0
  403. package/dist/core/orchestration/policy-compiler.js.map +1 -0
  404. package/dist/core/orchestration/policy-gate.d.ts +18 -0
  405. package/dist/core/orchestration/policy-gate.d.ts.map +1 -0
  406. package/dist/core/orchestration/policy-gate.js +24 -0
  407. package/dist/core/orchestration/policy-gate.js.map +1 -0
  408. package/dist/core/orchestration/profile-registry.d.ts +51 -0
  409. package/dist/core/orchestration/profile-registry.d.ts.map +1 -0
  410. package/dist/core/orchestration/profile-registry.js +393 -0
  411. package/dist/core/orchestration/profile-registry.js.map +1 -0
  412. package/dist/core/orchestration/profile-store.d.ts +31 -0
  413. package/dist/core/orchestration/profile-store.d.ts.map +1 -0
  414. package/dist/core/orchestration/profile-store.js +136 -0
  415. package/dist/core/orchestration/profile-store.js.map +1 -0
  416. package/dist/core/orchestration/risk-budget.d.ts +9 -0
  417. package/dist/core/orchestration/risk-budget.d.ts.map +1 -0
  418. package/dist/core/orchestration/risk-budget.js +48 -0
  419. package/dist/core/orchestration/risk-budget.js.map +1 -0
  420. package/dist/core/orchestration/task-runtime.d.ts +168 -0
  421. package/dist/core/orchestration/task-runtime.d.ts.map +1 -0
  422. package/dist/core/orchestration/task-runtime.js +1600 -0
  423. package/dist/core/orchestration/task-runtime.js.map +1 -0
  424. package/dist/core/orchestration/work-state-projection.d.ts +32 -0
  425. package/dist/core/orchestration/work-state-projection.d.ts.map +1 -0
  426. package/dist/core/orchestration/work-state-projection.js +116 -0
  427. package/dist/core/orchestration/work-state-projection.js.map +1 -0
  428. package/dist/core/orchestration/worker-execution-contract.d.ts +21 -0
  429. package/dist/core/orchestration/worker-execution-contract.d.ts.map +1 -0
  430. package/dist/core/orchestration/worker-execution-contract.js +179 -0
  431. package/dist/core/orchestration/worker-execution-contract.js.map +1 -0
  432. package/dist/core/orchestration/worker-result-adapter.d.ts +21 -0
  433. package/dist/core/orchestration/worker-result-adapter.d.ts.map +1 -0
  434. package/dist/core/orchestration/worker-result-adapter.js +109 -0
  435. package/dist/core/orchestration/worker-result-adapter.js.map +1 -0
  436. package/dist/core/package-manager.d.ts.map +1 -1
  437. package/dist/core/package-manager.js +20 -3
  438. package/dist/core/package-manager.js.map +1 -1
  439. package/dist/core/process-identity.d.ts +11 -0
  440. package/dist/core/process-identity.d.ts.map +1 -0
  441. package/dist/core/process-identity.js +26 -0
  442. package/dist/core/process-identity.js.map +1 -0
  443. package/dist/core/process-matrix/codes.d.ts +14 -3
  444. package/dist/core/process-matrix/codes.d.ts.map +1 -1
  445. package/dist/core/process-matrix/codes.js.map +1 -1
  446. package/dist/core/process-matrix/resume-launcher.d.ts +26 -0
  447. package/dist/core/process-matrix/resume-launcher.d.ts.map +1 -0
  448. package/dist/core/process-matrix/resume-launcher.js +62 -0
  449. package/dist/core/process-matrix/resume-launcher.js.map +1 -0
  450. package/dist/core/process-matrix/runtime.d.ts +37 -22
  451. package/dist/core/process-matrix/runtime.d.ts.map +1 -1
  452. package/dist/core/process-matrix/runtime.js +399 -76
  453. package/dist/core/process-matrix/runtime.js.map +1 -1
  454. package/dist/core/process-matrix/store.d.ts +5 -2
  455. package/dist/core/process-matrix/store.d.ts.map +1 -1
  456. package/dist/core/process-matrix/store.js +123 -14
  457. package/dist/core/process-matrix/store.js.map +1 -1
  458. package/dist/core/process-matrix/supervisor.d.ts +10 -7
  459. package/dist/core/process-matrix/supervisor.d.ts.map +1 -1
  460. package/dist/core/process-matrix/supervisor.js +45 -14
  461. package/dist/core/process-matrix/supervisor.js.map +1 -1
  462. package/dist/core/profile-filter-controller.d.ts +8 -1
  463. package/dist/core/profile-filter-controller.d.ts.map +1 -1
  464. package/dist/core/profile-filter-controller.js +23 -5
  465. package/dist/core/profile-filter-controller.js.map +1 -1
  466. package/dist/core/profile-registry.d.ts.map +1 -1
  467. package/dist/core/profile-registry.js +6 -8
  468. package/dist/core/profile-registry.js.map +1 -1
  469. package/dist/core/prompt-templates.d.ts +1 -0
  470. package/dist/core/prompt-templates.d.ts.map +1 -1
  471. package/dist/core/prompt-templates.js +18 -29
  472. package/dist/core/prompt-templates.js.map +1 -1
  473. package/dist/core/reflection-controller.d.ts +5 -5
  474. package/dist/core/reflection-controller.d.ts.map +1 -1
  475. package/dist/core/reflection-controller.js +5 -2
  476. package/dist/core/reflection-controller.js.map +1 -1
  477. package/dist/core/reload-blockers.d.ts.map +1 -1
  478. package/dist/core/reload-blockers.js +6 -8
  479. package/dist/core/reload-blockers.js.map +1 -1
  480. package/dist/core/request-auth.d.ts +7 -0
  481. package/dist/core/request-auth.d.ts.map +1 -0
  482. package/dist/core/request-auth.js +8 -0
  483. package/dist/core/request-auth.js.map +1 -0
  484. package/dist/core/research/evidence-bundle.d.ts.map +1 -1
  485. package/dist/core/research/evidence-bundle.js +1 -6
  486. package/dist/core/research/evidence-bundle.js.map +1 -1
  487. package/dist/core/research/lane-model-resolver.d.ts +33 -0
  488. package/dist/core/research/lane-model-resolver.d.ts.map +1 -0
  489. package/dist/core/research/lane-model-resolver.js +65 -0
  490. package/dist/core/research/lane-model-resolver.js.map +1 -0
  491. package/dist/core/research/model-fitness-controller.d.ts +38 -0
  492. package/dist/core/research/model-fitness-controller.d.ts.map +1 -0
  493. package/dist/core/research/model-fitness-controller.js +106 -0
  494. package/dist/core/research/model-fitness-controller.js.map +1 -0
  495. package/dist/core/research/model-fitness.d.ts +1 -1
  496. package/dist/core/research/model-fitness.d.ts.map +1 -1
  497. package/dist/core/research/model-fitness.js +4 -4
  498. package/dist/core/research/model-fitness.js.map +1 -1
  499. package/dist/core/research/research-gate.d.ts.map +1 -1
  500. package/dist/core/research/research-gate.js +7 -7
  501. package/dist/core/research/research-gate.js.map +1 -1
  502. package/dist/core/research/research-lane-controller.d.ts +62 -0
  503. package/dist/core/research/research-lane-controller.d.ts.map +1 -0
  504. package/dist/core/research/research-lane-controller.js +292 -0
  505. package/dist/core/research/research-lane-controller.js.map +1 -0
  506. package/dist/core/research/session-evidence-bundle.d.ts +2 -4
  507. package/dist/core/research/session-evidence-bundle.d.ts.map +1 -1
  508. package/dist/core/research/session-evidence-bundle.js +10 -46
  509. package/dist/core/research/session-evidence-bundle.js.map +1 -1
  510. package/dist/core/resource-loader.d.ts.map +1 -1
  511. package/dist/core/resource-loader.js +23 -8
  512. package/dist/core/resource-loader.js.map +1 -1
  513. package/dist/core/risk-classifier.d.ts +6 -0
  514. package/dist/core/risk-classifier.d.ts.map +1 -0
  515. package/dist/core/risk-classifier.js +233 -0
  516. package/dist/core/risk-classifier.js.map +1 -0
  517. package/dist/core/runtime-builder.d.ts +41 -23
  518. package/dist/core/runtime-builder.d.ts.map +1 -1
  519. package/dist/core/runtime-builder.js +365 -253
  520. package/dist/core/runtime-builder.js.map +1 -1
  521. package/dist/core/sdk.d.ts +6 -3
  522. package/dist/core/sdk.d.ts.map +1 -1
  523. package/dist/core/sdk.js +68 -25
  524. package/dist/core/sdk.js.map +1 -1
  525. package/dist/core/secrets/credential-exposure-guard.d.ts +9 -0
  526. package/dist/core/secrets/credential-exposure-guard.d.ts.map +1 -0
  527. package/dist/core/secrets/credential-exposure-guard.js +241 -0
  528. package/dist/core/secrets/credential-exposure-guard.js.map +1 -0
  529. package/dist/core/secrets/secret-dotenv.d.ts +27 -0
  530. package/dist/core/secrets/secret-dotenv.d.ts.map +1 -0
  531. package/dist/core/secrets/secret-dotenv.js +183 -0
  532. package/dist/core/secrets/secret-dotenv.js.map +1 -0
  533. package/dist/core/secrets/secret-vault-format.d.ts +112 -0
  534. package/dist/core/secrets/secret-vault-format.d.ts.map +1 -0
  535. package/dist/core/secrets/secret-vault-format.js +397 -0
  536. package/dist/core/secrets/secret-vault-format.js.map +1 -0
  537. package/dist/core/secrets/secret-vault.d.ts +72 -0
  538. package/dist/core/secrets/secret-vault.d.ts.map +1 -0
  539. package/dist/core/secrets/secret-vault.js +671 -0
  540. package/dist/core/secrets/secret-vault.js.map +1 -0
  541. package/dist/core/session-analytics.d.ts +4 -23
  542. package/dist/core/session-analytics.d.ts.map +1 -1
  543. package/dist/core/session-analytics.js +67 -70
  544. package/dist/core/session-analytics.js.map +1 -1
  545. package/dist/core/session-image-store.d.ts +33 -0
  546. package/dist/core/session-image-store.d.ts.map +1 -0
  547. package/dist/core/session-image-store.js +283 -0
  548. package/dist/core/session-image-store.js.map +1 -0
  549. package/dist/core/session-role.d.ts +11 -4
  550. package/dist/core/session-role.d.ts.map +1 -1
  551. package/dist/core/session-role.js +24 -5
  552. package/dist/core/session-role.js.map +1 -1
  553. package/dist/core/session-snapshot.d.ts +34 -0
  554. package/dist/core/session-snapshot.d.ts.map +1 -0
  555. package/dist/core/session-snapshot.js +68 -0
  556. package/dist/core/session-snapshot.js.map +1 -0
  557. package/dist/core/session-supervision-runtime.d.ts +31 -0
  558. package/dist/core/session-supervision-runtime.d.ts.map +1 -0
  559. package/dist/core/session-supervision-runtime.js +97 -0
  560. package/dist/core/session-supervision-runtime.js.map +1 -0
  561. package/dist/core/session-tree-navigator.d.ts +4 -6
  562. package/dist/core/session-tree-navigator.d.ts.map +1 -1
  563. package/dist/core/session-tree-navigator.js +4 -1
  564. package/dist/core/session-tree-navigator.js.map +1 -1
  565. package/dist/core/settings-manager.d.ts +10 -5
  566. package/dist/core/settings-manager.d.ts.map +1 -1
  567. package/dist/core/settings-manager.js +81 -29
  568. package/dist/core/settings-manager.js.map +1 -1
  569. package/dist/core/slash-commands.d.ts.map +1 -1
  570. package/dist/core/slash-commands.js +2 -2
  571. package/dist/core/slash-commands.js.map +1 -1
  572. package/dist/core/tasks/session-task-state.d.ts +2 -4
  573. package/dist/core/tasks/session-task-state.d.ts.map +1 -1
  574. package/dist/core/tasks/session-task-state.js +10 -28
  575. package/dist/core/tasks/session-task-state.js.map +1 -1
  576. package/dist/core/tasks/task-projection.d.ts +20 -0
  577. package/dist/core/tasks/task-projection.d.ts.map +1 -0
  578. package/dist/core/tasks/task-projection.js +47 -0
  579. package/dist/core/tasks/task-projection.js.map +1 -0
  580. package/dist/core/tasks/task-state.d.ts +4 -0
  581. package/dist/core/tasks/task-state.d.ts.map +1 -1
  582. package/dist/core/tasks/task-state.js +28 -11
  583. package/dist/core/tasks/task-state.js.map +1 -1
  584. package/dist/core/tool-capability-policy.d.ts +13 -0
  585. package/dist/core/tool-capability-policy.d.ts.map +1 -0
  586. package/dist/core/tool-capability-policy.js +86 -0
  587. package/dist/core/tool-capability-policy.js.map +1 -0
  588. package/dist/core/tool-gate-controller.d.ts.map +1 -1
  589. package/dist/core/tool-gate-controller.js +8 -2
  590. package/dist/core/tool-gate-controller.js.map +1 -1
  591. package/dist/core/tool-protocol-controller.d.ts +89 -0
  592. package/dist/core/tool-protocol-controller.d.ts.map +1 -0
  593. package/dist/core/tool-protocol-controller.js +512 -0
  594. package/dist/core/tool-protocol-controller.js.map +1 -0
  595. package/dist/core/tool-recovery-log-records.d.ts +2 -3
  596. package/dist/core/tool-recovery-log-records.d.ts.map +1 -1
  597. package/dist/core/tool-recovery-log-records.js +9 -35
  598. package/dist/core/tool-recovery-log-records.js.map +1 -1
  599. package/dist/core/tool-recovery-stats.d.ts +21 -1
  600. package/dist/core/tool-recovery-stats.d.ts.map +1 -1
  601. package/dist/core/tool-recovery-stats.js.map +1 -1
  602. package/dist/core/tool-selection/tool-performance-store.d.ts +3 -7
  603. package/dist/core/tool-selection/tool-performance-store.d.ts.map +1 -1
  604. package/dist/core/tool-selection/tool-performance-store.js +97 -138
  605. package/dist/core/tool-selection/tool-performance-store.js.map +1 -1
  606. package/dist/core/tools/ask-question.d.ts +124 -0
  607. package/dist/core/tools/ask-question.d.ts.map +1 -0
  608. package/dist/core/tools/ask-question.js +693 -0
  609. package/dist/core/tools/ask-question.js.map +1 -0
  610. package/dist/core/tools/bash.d.ts +5 -0
  611. package/dist/core/tools/bash.d.ts.map +1 -1
  612. package/dist/core/tools/bash.js +59 -5
  613. package/dist/core/tools/bash.js.map +1 -1
  614. package/dist/core/tools/delegate-status.d.ts +31 -3
  615. package/dist/core/tools/delegate-status.d.ts.map +1 -1
  616. package/dist/core/tools/delegate-status.js +148 -29
  617. package/dist/core/tools/delegate-status.js.map +1 -1
  618. package/dist/core/tools/delegate.d.ts +15 -13
  619. package/dist/core/tools/delegate.d.ts.map +1 -1
  620. package/dist/core/tools/delegate.js +106 -23
  621. package/dist/core/tools/delegate.js.map +1 -1
  622. package/dist/core/tools/extensionify.d.ts.map +1 -1
  623. package/dist/core/tools/extensionify.js +1 -0
  624. package/dist/core/tools/extensionify.js.map +1 -1
  625. package/dist/core/tools/git-filter.d.ts +1 -8
  626. package/dist/core/tools/git-filter.d.ts.map +1 -1
  627. package/dist/core/tools/git-filter.js +2 -63
  628. package/dist/core/tools/git-filter.js.map +1 -1
  629. package/dist/core/tools/goal.d.ts +10 -8
  630. package/dist/core/tools/goal.d.ts.map +1 -1
  631. package/dist/core/tools/goal.js +106 -28
  632. package/dist/core/tools/goal.js.map +1 -1
  633. package/dist/core/tools/index.d.ts +1 -0
  634. package/dist/core/tools/index.d.ts.map +1 -1
  635. package/dist/core/tools/index.js +1 -0
  636. package/dist/core/tools/index.js.map +1 -1
  637. package/dist/core/tools/model-fitness.d.ts.map +1 -1
  638. package/dist/core/tools/model-fitness.js +2 -2
  639. package/dist/core/tools/model-fitness.js.map +1 -1
  640. package/dist/core/tools/orchestration-panel.d.ts +40 -0
  641. package/dist/core/tools/orchestration-panel.d.ts.map +1 -0
  642. package/dist/core/tools/orchestration-panel.js +124 -0
  643. package/dist/core/tools/orchestration-panel.js.map +1 -0
  644. package/dist/core/tools/output-accumulator.d.ts +11 -0
  645. package/dist/core/tools/output-accumulator.d.ts.map +1 -1
  646. package/dist/core/tools/output-accumulator.js +36 -3
  647. package/dist/core/tools/output-accumulator.js.map +1 -1
  648. package/dist/core/tools/python.d.ts +2 -0
  649. package/dist/core/tools/python.d.ts.map +1 -1
  650. package/dist/core/tools/python.js +3 -1
  651. package/dist/core/tools/python.js.map +1 -1
  652. package/dist/core/tools/read.d.ts.map +1 -1
  653. package/dist/core/tools/read.js +1 -1
  654. package/dist/core/tools/read.js.map +1 -1
  655. package/dist/core/tools/run-process.d.ts +26 -0
  656. package/dist/core/tools/run-process.d.ts.map +1 -0
  657. package/dist/core/tools/run-process.js +138 -0
  658. package/dist/core/tools/run-process.js.map +1 -0
  659. package/dist/core/tools/search-command-guard.d.ts +14 -0
  660. package/dist/core/tools/search-command-guard.d.ts.map +1 -0
  661. package/dist/core/tools/search-command-guard.js +333 -0
  662. package/dist/core/tools/search-command-guard.js.map +1 -0
  663. package/dist/core/tools/secret-store.d.ts +38 -0
  664. package/dist/core/tools/secret-store.d.ts.map +1 -0
  665. package/dist/core/tools/secret-store.js +412 -0
  666. package/dist/core/tools/secret-store.js.map +1 -0
  667. package/dist/core/tools/shell-command-parser.d.ts +16 -0
  668. package/dist/core/tools/shell-command-parser.d.ts.map +1 -0
  669. package/dist/core/tools/shell-command-parser.js +113 -0
  670. package/dist/core/tools/shell-command-parser.js.map +1 -0
  671. package/dist/core/tools/task-steps.d.ts +2 -0
  672. package/dist/core/tools/task-steps.d.ts.map +1 -1
  673. package/dist/core/tools/task-steps.js +99 -0
  674. package/dist/core/tools/task-steps.js.map +1 -1
  675. package/dist/core/tools/tmux-dispatch.d.ts +3 -5
  676. package/dist/core/tools/tmux-dispatch.d.ts.map +1 -1
  677. package/dist/core/tools/tmux-dispatch.js +4 -4
  678. package/dist/core/tools/tmux-dispatch.js.map +1 -1
  679. package/dist/core/tools/windows-shell-engine.d.ts.map +1 -1
  680. package/dist/core/tools/windows-shell-engine.js +4 -6
  681. package/dist/core/tools/windows-shell-engine.js.map +1 -1
  682. package/dist/core/util/atomic-file.d.ts +6 -2
  683. package/dist/core/util/atomic-file.d.ts.map +1 -1
  684. package/dist/core/util/atomic-file.js +4 -4
  685. package/dist/core/util/atomic-file.js.map +1 -1
  686. package/dist/core/util/bounded-jsonl.d.ts +10 -0
  687. package/dist/core/util/bounded-jsonl.d.ts.map +1 -0
  688. package/dist/core/util/bounded-jsonl.js +75 -0
  689. package/dist/core/util/bounded-jsonl.js.map +1 -0
  690. package/dist/core/util/value-guards.d.ts +5 -0
  691. package/dist/core/util/value-guards.d.ts.map +1 -0
  692. package/dist/core/util/value-guards.js +12 -0
  693. package/dist/core/util/value-guards.js.map +1 -0
  694. package/dist/core/worktree-sync/store.d.ts +3 -3
  695. package/dist/core/worktree-sync/store.d.ts.map +1 -1
  696. package/dist/core/worktree-sync/store.js +10 -5
  697. package/dist/core/worktree-sync/store.js.map +1 -1
  698. package/dist/index.d.ts +15 -4
  699. package/dist/index.d.ts.map +1 -1
  700. package/dist/index.js +14 -3
  701. package/dist/index.js.map +1 -1
  702. package/dist/main.d.ts.map +1 -1
  703. package/dist/main.js +149 -51
  704. package/dist/main.js.map +1 -1
  705. package/dist/migrations.d.ts +4 -6
  706. package/dist/migrations.d.ts.map +1 -1
  707. package/dist/migrations.js +186 -28
  708. package/dist/migrations.js.map +1 -1
  709. package/dist/modes/interactive/auth-dialogs-controller.d.ts +4 -0
  710. package/dist/modes/interactive/auth-dialogs-controller.d.ts.map +1 -1
  711. package/dist/modes/interactive/auth-dialogs-controller.js +60 -1
  712. package/dist/modes/interactive/auth-dialogs-controller.js.map +1 -1
  713. package/dist/modes/interactive/clipboard-input.d.ts +7 -1
  714. package/dist/modes/interactive/clipboard-input.d.ts.map +1 -1
  715. package/dist/modes/interactive/clipboard-input.js +44 -6
  716. package/dist/modes/interactive/clipboard-input.js.map +1 -1
  717. package/dist/modes/interactive/components/activity-lane.d.ts +55 -0
  718. package/dist/modes/interactive/components/activity-lane.d.ts.map +1 -0
  719. package/dist/modes/interactive/components/activity-lane.js +304 -0
  720. package/dist/modes/interactive/components/activity-lane.js.map +1 -0
  721. package/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
  722. package/dist/modes/interactive/components/assistant-message.js +19 -10
  723. package/dist/modes/interactive/components/assistant-message.js.map +1 -1
  724. package/dist/modes/interactive/components/expandable-text.d.ts +12 -0
  725. package/dist/modes/interactive/components/expandable-text.d.ts.map +1 -0
  726. package/dist/modes/interactive/components/expandable-text.js +17 -0
  727. package/dist/modes/interactive/components/expandable-text.js.map +1 -0
  728. package/dist/modes/interactive/components/extension-editor.d.ts +15 -1
  729. package/dist/modes/interactive/components/extension-editor.d.ts.map +1 -1
  730. package/dist/modes/interactive/components/extension-editor.js +30 -21
  731. package/dist/modes/interactive/components/extension-editor.js.map +1 -1
  732. package/dist/modes/interactive/components/extension-input.d.ts +1 -0
  733. package/dist/modes/interactive/components/extension-input.d.ts.map +1 -1
  734. package/dist/modes/interactive/components/extension-input.js +6 -1
  735. package/dist/modes/interactive/components/extension-input.js.map +1 -1
  736. package/dist/modes/interactive/components/scoped-models-selector.d.ts +2 -2
  737. package/dist/modes/interactive/components/scoped-models-selector.d.ts.map +1 -1
  738. package/dist/modes/interactive/components/scoped-models-selector.js +23 -14
  739. package/dist/modes/interactive/components/scoped-models-selector.js.map +1 -1
  740. package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  741. package/dist/modes/interactive/components/settings-selector.js +6 -6
  742. package/dist/modes/interactive/components/settings-selector.js.map +1 -1
  743. package/dist/modes/interactive/components/tool-execution.d.ts +19 -11
  744. package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  745. package/dist/modes/interactive/components/tool-execution.js +53 -20
  746. package/dist/modes/interactive/components/tool-execution.js.map +1 -1
  747. package/dist/modes/interactive/components/tool-group.d.ts.map +1 -1
  748. package/dist/modes/interactive/components/tool-group.js +3 -0
  749. package/dist/modes/interactive/components/tool-group.js.map +1 -1
  750. package/dist/modes/interactive/components/usage-action-selector.d.ts +16 -0
  751. package/dist/modes/interactive/components/usage-action-selector.d.ts.map +1 -0
  752. package/dist/modes/interactive/components/usage-action-selector.js +46 -0
  753. package/dist/modes/interactive/components/usage-action-selector.js.map +1 -0
  754. package/dist/modes/interactive/config-backup.d.ts.map +1 -1
  755. package/dist/modes/interactive/config-backup.js +5 -4
  756. package/dist/modes/interactive/config-backup.js.map +1 -1
  757. package/dist/modes/interactive/extension-ui-host.d.ts +14 -0
  758. package/dist/modes/interactive/extension-ui-host.d.ts.map +1 -1
  759. package/dist/modes/interactive/extension-ui-host.js +77 -15
  760. package/dist/modes/interactive/extension-ui-host.js.map +1 -1
  761. package/dist/modes/interactive/external-editor.d.ts +15 -0
  762. package/dist/modes/interactive/external-editor.d.ts.map +1 -1
  763. package/dist/modes/interactive/external-editor.js +27 -19
  764. package/dist/modes/interactive/external-editor.js.map +1 -1
  765. package/dist/modes/interactive/history-reload-math.d.ts +6 -2
  766. package/dist/modes/interactive/history-reload-math.d.ts.map +1 -1
  767. package/dist/modes/interactive/history-reload-math.js +10 -5
  768. package/dist/modes/interactive/history-reload-math.js.map +1 -1
  769. package/dist/modes/interactive/interactive-event-controller.d.ts +74 -0
  770. package/dist/modes/interactive/interactive-event-controller.d.ts.map +1 -0
  771. package/dist/modes/interactive/interactive-event-controller.js +328 -0
  772. package/dist/modes/interactive/interactive-event-controller.js.map +1 -0
  773. package/dist/modes/interactive/interactive-mode.d.ts +22 -24
  774. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  775. package/dist/modes/interactive/interactive-mode.js +245 -595
  776. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  777. package/dist/modes/interactive/key-handlers.d.ts.map +1 -1
  778. package/dist/modes/interactive/key-handlers.js +1 -1
  779. package/dist/modes/interactive/key-handlers.js.map +1 -1
  780. package/dist/modes/interactive/loaded-resources-view.d.ts +20 -0
  781. package/dist/modes/interactive/loaded-resources-view.d.ts.map +1 -0
  782. package/dist/modes/interactive/loaded-resources-view.js +147 -0
  783. package/dist/modes/interactive/loaded-resources-view.js.map +1 -0
  784. package/dist/modes/interactive/profile-menu-controller.d.ts.map +1 -1
  785. package/dist/modes/interactive/profile-menu-controller.js +7 -7
  786. package/dist/modes/interactive/profile-menu-controller.js.map +1 -1
  787. package/dist/modes/interactive/report-commands.d.ts.map +1 -1
  788. package/dist/modes/interactive/report-commands.js +21 -0
  789. package/dist/modes/interactive/report-commands.js.map +1 -1
  790. package/dist/modes/interactive/resource-display.d.ts.map +1 -1
  791. package/dist/modes/interactive/resource-display.js +8 -2
  792. package/dist/modes/interactive/resource-display.js.map +1 -1
  793. package/dist/modes/interactive/resource-shell-commands.d.ts +1 -5
  794. package/dist/modes/interactive/resource-shell-commands.d.ts.map +1 -1
  795. package/dist/modes/interactive/resource-shell-commands.js +0 -5
  796. package/dist/modes/interactive/resource-shell-commands.js.map +1 -1
  797. package/dist/modes/interactive/session-flow-commands.d.ts +27 -8
  798. package/dist/modes/interactive/session-flow-commands.d.ts.map +1 -1
  799. package/dist/modes/interactive/session-flow-commands.js +150 -64
  800. package/dist/modes/interactive/session-flow-commands.js.map +1 -1
  801. package/dist/modes/interactive/session-io-commands.d.ts.map +1 -1
  802. package/dist/modes/interactive/session-io-commands.js +16 -6
  803. package/dist/modes/interactive/session-io-commands.js.map +1 -1
  804. package/dist/modes/interactive/session-replacement-errors.d.ts +7 -0
  805. package/dist/modes/interactive/session-replacement-errors.d.ts.map +1 -0
  806. package/dist/modes/interactive/session-replacement-errors.js +15 -0
  807. package/dist/modes/interactive/session-replacement-errors.js.map +1 -0
  808. package/dist/modes/interactive/startup-checks.d.ts.map +1 -1
  809. package/dist/modes/interactive/startup-checks.js +1 -1
  810. package/dist/modes/interactive/startup-checks.js.map +1 -1
  811. package/dist/modes/interactive/usage-commands.d.ts +34 -0
  812. package/dist/modes/interactive/usage-commands.d.ts.map +1 -0
  813. package/dist/modes/interactive/usage-commands.js +225 -0
  814. package/dist/modes/interactive/usage-commands.js.map +1 -0
  815. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  816. package/dist/modes/rpc/rpc-mode.js +82 -23
  817. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  818. package/dist/modes/rpc/rpc-types.d.ts +15 -0
  819. package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
  820. package/dist/modes/rpc/rpc-types.js +138 -1
  821. package/dist/modes/rpc/rpc-types.js.map +1 -1
  822. package/dist/utils/clipboard-image.d.ts.map +1 -1
  823. package/dist/utils/clipboard-image.js +2 -13
  824. package/dist/utils/clipboard-image.js.map +1 -1
  825. package/dist/utils/clipboard-native.d.ts +1 -0
  826. package/dist/utils/clipboard-native.d.ts.map +1 -1
  827. package/dist/utils/clipboard-native.js.map +1 -1
  828. package/dist/utils/clipboard.d.ts +1 -0
  829. package/dist/utils/clipboard.d.ts.map +1 -1
  830. package/dist/utils/clipboard.js +52 -1
  831. package/dist/utils/clipboard.js.map +1 -1
  832. package/dist/utils/platform.d.ts +3 -0
  833. package/dist/utils/platform.d.ts.map +1 -0
  834. package/dist/utils/platform.js +8 -0
  835. package/dist/utils/platform.js.map +1 -0
  836. package/dist/utils/shell.d.ts.map +1 -1
  837. package/dist/utils/shell.js +5 -3
  838. package/dist/utils/shell.js.map +1 -1
  839. package/dist/utils/version-check.d.ts.map +1 -1
  840. package/dist/utils/version-check.js +3 -1
  841. package/dist/utils/version-check.js.map +1 -1
  842. package/dist/utils/work-directory.d.ts +11 -0
  843. package/dist/utils/work-directory.d.ts.map +1 -1
  844. package/dist/utils/work-directory.js +30 -9
  845. package/dist/utils/work-directory.js.map +1 -1
  846. package/docs/UI_POLISH_ORCHESTRATION.md +38 -0
  847. package/docs/UI_QUALITY_ORCHESTRATION.md +28 -0
  848. package/docs/docs.json +4 -0
  849. package/docs/extensions.md +62 -2
  850. package/docs/harness-architecture.md +156 -0
  851. package/docs/keybindings.md +11 -1
  852. package/docs/process-matrix.md +59 -26
  853. package/docs/providers.md +9 -1
  854. package/docs/rpc.md +45 -2
  855. package/docs/sessions.md +15 -0
  856. package/docs/settings.md +125 -11
  857. package/docs/task-steps.md +8 -0
  858. package/docs/telemetry.md +19 -0
  859. package/docs/tmux-agent-manager.md +10 -3
  860. package/docs/ui-reference/README.md +9 -0
  861. package/docs/ui-reference/orchestration-after.txt +14 -0
  862. package/docs/ui-reference/orchestration-before.txt +13 -0
  863. package/docs/ui-reference/orchestration-reference.txt +16 -0
  864. package/docs/usage.md +22 -2
  865. package/docs/work-directory.md +19 -8
  866. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  867. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  868. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  869. package/examples/extensions/question.ts +34 -245
  870. package/examples/extensions/questionnaire.ts +41 -407
  871. package/examples/extensions/sandbox/package-lock.json +2 -2
  872. package/examples/extensions/sandbox/package.json +1 -1
  873. package/examples/extensions/with-deps/package-lock.json +2 -2
  874. package/examples/extensions/with-deps/package.json +1 -1
  875. package/npm-shrinkwrap.json +13 -13
  876. package/package.json +4 -4
  877. package/dist/core/delegation/session-worker-result.d.ts +0 -46
  878. package/dist/core/delegation/session-worker-result.d.ts.map +0 -1
  879. package/dist/core/delegation/session-worker-result.js +0 -113
  880. package/dist/core/delegation/session-worker-result.js.map +0 -1
  881. package/dist/core/delegation/worker-result.d.ts.map +0 -1
  882. package/dist/core/delegation/worker-result.js.map +0 -1
  883. package/dist/core/telemetry.d.ts.map +0 -1
  884. package/dist/core/telemetry.js.map +0 -1
@@ -10,6 +10,7 @@ import { getBundledResourcesDir } from "../../config.js";
10
10
  import { spawnProcess, waitForChildProcessWithTermination } from "../../utils/child-process.js";
11
11
  import { getShellEnv, trackDetachedChildPid, untrackDetachedChildPid } from "../../utils/shell.js";
12
12
  import { ensurePythonRuntime } from "../python-runtime.js";
13
+ import { isRecordObject } from "../util/value-guards.js";
13
14
  import { applyEngineFrame, getOrCreateWindowsShellState, mergeEffectiveEnv, resolveEffectiveCwd, } from "./windows-shell-state.js";
14
15
  const ENGINE_FRAME_SENTINEL = 0x1e;
15
16
  export class WindowsShellEngineFailure extends Error {
@@ -24,9 +25,6 @@ function resolveEngineScriptPath() {
24
25
  return join(getBundledResourcesDir(), "runtimes", "pi-shell-engine", "main.py");
25
26
  }
26
27
  const MAX_CONTROL_FRAME_BYTES = 64 * 1024;
27
- function isRecord(value) {
28
- return typeof value === "object" && value !== null && !Array.isArray(value);
29
- }
30
28
  /** Parse the terminal frame from the dedicated control stream. Command output is never retained here. */
31
29
  function parseControlFrame(buffer) {
32
30
  if (buffer.length < 2 || buffer[buffer.length - 1] !== ENGINE_FRAME_SENTINEL)
@@ -36,13 +34,13 @@ function parseControlFrame(buffer) {
36
34
  return undefined;
37
35
  try {
38
36
  const raw = JSON.parse(buffer.subarray(openIndex + 1, buffer.length - 1).toString("utf8"));
39
- if (!isRecord(raw))
37
+ if (!isRecordObject(raw))
40
38
  return undefined;
41
39
  if (typeof raw.exitCode !== "number" || !Number.isInteger(raw.exitCode))
42
40
  return undefined;
43
- if (typeof raw.cwd !== "string" || !isRecord(raw.envDelta))
41
+ if (typeof raw.cwd !== "string" || !isRecordObject(raw.envDelta))
44
42
  return undefined;
45
- if (raw.unsupported !== null && !isRecord(raw.unsupported))
43
+ if (raw.unsupported !== null && !isRecordObject(raw.unsupported))
46
44
  return undefined;
47
45
  if (raw.unsupported !== null &&
48
46
  (raw.unsupported.code !== "unsupported" ||
@@ -1 +1 @@
1
- {"version":3,"file":"windows-shell-engine.js","sourceRoot":"","sources":["../../../src/core/tools/windows-shell-engine.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The `python-engine` tier: per-command spawn of the bundled `pi-shell-engine` (D3 in the\n * blueprint). Wires the frozen §1.2/§1.3 stdin-JSON-request / 0x1e-framed-stdout protocol into a\n * `BashOperations`, threads `WindowsShellState` through both directions (D4), and degrades to a\n * named, actionable error when the Python runtime is not ready — the PowerShell floor keeps\n * working regardless (the router never routes simple commands here when the engine is off).\n */\n\nimport type { ChildProcess, SpawnOptions } from \"node:child_process\";\nimport { join } from \"node:path\";\nimport { getBundledResourcesDir } from \"../../config.ts\";\nimport { spawnProcess, waitForChildProcessWithTermination } from \"../../utils/child-process.ts\";\nimport { getShellEnv, trackDetachedChildPid, untrackDetachedChildPid } from \"../../utils/shell.ts\";\nimport { ensurePythonRuntime, type PythonRuntimeOutcome } from \"../python-runtime.ts\";\nimport type { BashOperations } from \"./bash.ts\";\nimport {\n\tapplyEngineFrame,\n\tgetOrCreateWindowsShellState,\n\tmergeEffectiveEnv,\n\tresolveEffectiveCwd,\n\ttype WindowsShellState,\n} from \"./windows-shell-state.ts\";\n\nconst ENGINE_FRAME_SENTINEL = 0x1e;\n\n/** The full §1.3 control-frame shape. */\nexport interface WindowsShellEngineFrame {\n\texitCode: number;\n\tcwd: string;\n\tenvDelta: Record<string, string | null>;\n\tunsupported: { code: \"unsupported\"; construct: string; message: string } | null;\n}\n\nexport class WindowsShellEngineFailure extends Error {\n\treadonly capturedOutput: string;\n\n\tconstructor(message: string, capturedOutput: string) {\n\t\tsuper(message);\n\t\tthis.name = \"WindowsShellEngineFailure\";\n\t\tthis.capturedOutput = capturedOutput;\n\t}\n}\n\nfunction resolveEngineScriptPath(): string {\n\treturn join(getBundledResourcesDir(), \"runtimes\", \"pi-shell-engine\", \"main.py\");\n}\n\nconst MAX_CONTROL_FRAME_BYTES = 64 * 1024;\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\n/** Parse the terminal frame from the dedicated control stream. Command output is never retained here. */\nfunction parseControlFrame(buffer: Buffer): WindowsShellEngineFrame | undefined {\n\tif (buffer.length < 2 || buffer[buffer.length - 1] !== ENGINE_FRAME_SENTINEL) return undefined;\n\tconst openIndex = buffer.lastIndexOf(ENGINE_FRAME_SENTINEL, buffer.length - 2);\n\tif (openIndex === -1) return undefined;\n\ttry {\n\t\tconst raw: unknown = JSON.parse(buffer.subarray(openIndex + 1, buffer.length - 1).toString(\"utf8\"));\n\t\tif (!isRecord(raw)) return undefined;\n\t\tif (typeof raw.exitCode !== \"number\" || !Number.isInteger(raw.exitCode)) return undefined;\n\t\tif (typeof raw.cwd !== \"string\" || !isRecord(raw.envDelta)) return undefined;\n\t\tif (raw.unsupported !== null && !isRecord(raw.unsupported)) return undefined;\n\t\tif (\n\t\t\traw.unsupported !== null &&\n\t\t\t(raw.unsupported.code !== \"unsupported\" ||\n\t\t\t\ttypeof raw.unsupported.construct !== \"string\" ||\n\t\t\t\ttypeof raw.unsupported.message !== \"string\")\n\t\t)\n\t\t\treturn undefined;\n\t\tfor (const value of Object.values(raw.envDelta)) {\n\t\t\tif (value !== null && typeof value !== \"string\") return undefined;\n\t\t}\n\t\treturn raw as unknown as WindowsShellEngineFrame;\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\nexport interface WindowsShellEngineOptions {\n\t/** Override for tests: resolves the Python runtime outcome. Default: `ensurePythonRuntime`. */\n\tresolveRuntime?: () => Promise<PythonRuntimeOutcome>;\n\t/** Override for tests: absolute path to the engine's `main.py`. Default: the bundled runtime. */\n\tengineScriptPath?: string;\n\t/** Override for tests: the per-session state store lookup. Default: the shared module store. */\n\tgetState?: (sessionKey: string) => WindowsShellState;\n\t/** Override for tests: spawns the engine child process. Default: `spawnProcess` (cross-spawn on win32). */\n\tspawn?: (command: string, args: string[], options: SpawnOptions) => ChildProcess;\n}\n\nfunction degradationError(\n\toutcome: Extract<PythonRuntimeOutcome, { status: \"offline\" | \"uv-unavailable\" | \"python-unavailable\" }>,\n) {\n\treturn new Error(\n\t\t`The Windows shell engine (Python) is unavailable: ${outcome.reason} The simple-command PowerShell floor still works; fix the Python runtime (uv/network) to restore pipelines, redirection, expansion, and chaining.`,\n\t);\n}\n\n/**\n * Create the `python-engine` tier `BashOperations` for one bash-tool session. `exec`'s `command`\n * argument is the RAW Bash source from the `python-engine` route (never PowerShell-translated).\n */\nexport function createWindowsShellEngineOperations(\n\tsessionKey: string,\n\toptions: WindowsShellEngineOptions = {},\n): BashOperations {\n\tconst resolveRuntime = options.resolveRuntime ?? (() => ensurePythonRuntime({ silent: true }));\n\tconst engineScriptPath = options.engineScriptPath ?? resolveEngineScriptPath();\n\tconst getState = options.getState ?? getOrCreateWindowsShellState;\n\tconst spawn = options.spawn ?? spawnProcess;\n\n\treturn {\n\t\tasync exec(command, cwd, { onData, signal, timeout, env }) {\n\t\t\tconst runtime = await resolveRuntime();\n\t\t\tif (runtime.status !== \"ready\") throw degradationError(runtime);\n\n\t\t\tconst state = getState(sessionKey);\n\t\t\tconst effectiveCwd = resolveEffectiveCwd(state, cwd);\n\t\t\tconst effectiveEnv = mergeEffectiveEnv(state, env ?? getShellEnv());\n\t\t\tconst timeoutMs = timeout !== undefined && timeout > 0 ? timeout * 1000 : undefined;\n\t\t\t// The engine's own soft deadline must fire and emit its cooperative exit-124 frame (which\n\t\t\t// preserves partial output) BEFORE the hard tree-kill backstop below, or the hard kill\n\t\t\t// always wins the race and the soft path never gets to run. Give it a head start; the hard\n\t\t\t// kill still bounds a hung engine that never reads its own deadline.\n\t\t\tconst requestTimeoutMs = timeoutMs !== undefined ? Math.max(timeoutMs - 500, 500) : undefined;\n\n\t\t\tconst request = {\n\t\t\t\tcommand,\n\t\t\t\tcwd: effectiveCwd,\n\t\t\t\tenv: effectiveEnv,\n\t\t\t\t...(requestTimeoutMs !== undefined ? { timeoutMs: requestTimeoutMs } : {}),\n\t\t\t};\n\n\t\t\tconst child = spawn(runtime.pythonPath, [\"-B\", engineScriptPath], {\n\t\t\t\tcwd: effectiveCwd,\n\t\t\t\tenv: {\n\t\t\t\t\t...effectiveEnv,\n\t\t\t\t\tPYTHONDONTWRITEBYTECODE: \"1\",\n\t\t\t\t\tPYTHONIOENCODING: \"utf-8\",\n\t\t\t\t\tPYTHONUNBUFFERED: \"1\",\n\t\t\t\t\tPYTHONUTF8: \"1\",\n\t\t\t\t},\n\t\t\t\tstdio: [\"pipe\", \"pipe\", \"pipe\"],\n\t\t\t});\n\t\t\tif (child.pid) trackDetachedChildPid(child.pid);\n\n\t\t\tlet controlBuffer: Buffer<ArrayBufferLike> = Buffer.alloc(0);\n\t\t\tlet controlOverflow = false;\n\n\t\t\t// Command output is the data stream: forward it immediately and never retain a\n\t\t\t// second full copy merely to find the terminal frame.\n\t\t\tchild.stdout?.on(\"data\", (chunk: Buffer) => {\n\t\t\t\tonData(chunk);\n\t\t\t});\n\t\t\t// stderr is the bounded control stream. The Python engine routes command stderr\n\t\t\t// into stdout, so this channel contains only the terminal frame/diagnostics.\n\t\t\tchild.stderr?.on(\"data\", (chunk: Buffer) => {\n\t\t\t\tif (controlOverflow) return;\n\t\t\t\tif (controlBuffer.length + chunk.length > MAX_CONTROL_FRAME_BYTES) {\n\t\t\t\t\tcontrolOverflow = true;\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tcontrolBuffer = Buffer.concat([controlBuffer, chunk]);\n\t\t\t});\n\n\t\t\ttry {\n\t\t\t\tchild.stdin?.end(JSON.stringify(request), \"utf8\");\n\t\t\t\tconst terminal = await waitForChildProcessWithTermination(child, {\n\t\t\t\t\tsignal,\n\t\t\t\t\ttimeoutMs,\n\t\t\t\t\tkillGraceMs: 2_000,\n\t\t\t\t});\n\t\t\t\tif (signal?.aborted) throw new Error(\"aborted\");\n\n\t\t\t\tconst frame = controlOverflow ? undefined : parseControlFrame(controlBuffer);\n\t\t\t\tif (!frame) {\n\t\t\t\t\tconst capturedOutput = controlOverflow\n\t\t\t\t\t\t? `control frame exceeded ${MAX_CONTROL_FRAME_BYTES} bytes`\n\t\t\t\t\t\t: controlBuffer.toString(\"utf8\");\n\t\t\t\t\tthrow new WindowsShellEngineFailure(\n\t\t\t\t\t\t`Windows shell engine failed (process exit ${terminal.code ?? \"null\"}) without a parseable control frame.\\n${capturedOutput}`,\n\t\t\t\t\t\tcapturedOutput,\n\t\t\t\t\t);\n\t\t\t\t}\n\n\t\t\t\tapplyEngineFrame(state, frame);\n\t\t\t\tif (frame.unsupported) throw new Error(frame.unsupported.message);\n\t\t\t\treturn { exitCode: frame.exitCode };\n\t\t\t} finally {\n\t\t\t\tif (child.pid) untrackDetachedChildPid(child.pid);\n\t\t\t}\n\t\t},\n\t};\n}\n"]}
1
+ {"version":3,"file":"windows-shell-engine.js","sourceRoot":"","sources":["../../../src/core/tools/windows-shell-engine.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,sBAAsB,EAAE,MAAM,iBAAiB,CAAC;AACzD,OAAO,EAAE,YAAY,EAAE,kCAAkC,EAAE,MAAM,8BAA8B,CAAC;AAChG,OAAO,EAAE,WAAW,EAAE,qBAAqB,EAAE,uBAAuB,EAAE,MAAM,sBAAsB,CAAC;AACnG,OAAO,EAAE,mBAAmB,EAA6B,MAAM,sBAAsB,CAAC;AACtF,OAAO,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAC;AAEzD,OAAO,EACN,gBAAgB,EAChB,4BAA4B,EAC5B,iBAAiB,EACjB,mBAAmB,GAEnB,MAAM,0BAA0B,CAAC;AAElC,MAAM,qBAAqB,GAAG,IAAI,CAAC;AAUnC,MAAM,OAAO,yBAA0B,SAAQ,KAAK;IAC1C,cAAc,CAAS;IAEhC,YAAY,OAAe,EAAE,cAAsB,EAAE;QACpD,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,2BAA2B,CAAC;QACxC,IAAI,CAAC,cAAc,GAAG,cAAc,CAAC;IAAA,CACrC;CACD;AAED,SAAS,uBAAuB,GAAW;IAC1C,OAAO,IAAI,CAAC,sBAAsB,EAAE,EAAE,UAAU,EAAE,iBAAiB,EAAE,SAAS,CAAC,CAAC;AAAA,CAChF;AAED,MAAM,uBAAuB,GAAG,EAAE,GAAG,IAAI,CAAC;AAE1C,yGAAyG;AACzG,SAAS,iBAAiB,CAAC,MAAc,EAAuC;IAC/E,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC,IAAI,MAAM,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,KAAK,qBAAqB;QAAE,OAAO,SAAS,CAAC;IAC/F,MAAM,SAAS,GAAG,MAAM,CAAC,WAAW,CAAC,qBAAqB,EAAE,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;IAC/E,IAAI,SAAS,KAAK,CAAC,CAAC;QAAE,OAAO,SAAS,CAAC;IACvC,IAAI,CAAC;QACJ,MAAM,GAAG,GAAY,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,QAAQ,CAAC,SAAS,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC;QACpG,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC;YAAE,OAAO,SAAS,CAAC;QAC3C,IAAI,OAAO,GAAG,CAAC,QAAQ,KAAK,QAAQ,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,GAAG,CAAC,QAAQ,CAAC;YAAE,OAAO,SAAS,CAAC;QAC1F,IAAI,OAAO,GAAG,CAAC,GAAG,KAAK,QAAQ,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,QAAQ,CAAC;YAAE,OAAO,SAAS,CAAC;QACnF,IAAI,GAAG,CAAC,WAAW,KAAK,IAAI,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,WAAW,CAAC;YAAE,OAAO,SAAS,CAAC;QACnF,IACC,GAAG,CAAC,WAAW,KAAK,IAAI;YACxB,CAAC,GAAG,CAAC,WAAW,CAAC,IAAI,KAAK,aAAa;gBACtC,OAAO,GAAG,CAAC,WAAW,CAAC,SAAS,KAAK,QAAQ;gBAC7C,OAAO,GAAG,CAAC,WAAW,CAAC,OAAO,KAAK,QAAQ,CAAC;YAE7C,OAAO,SAAS,CAAC;QAClB,KAAK,MAAM,KAAK,IAAI,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,CAAC;YACjD,IAAI,KAAK,KAAK,IAAI,IAAI,OAAO,KAAK,KAAK,QAAQ;gBAAE,OAAO,SAAS,CAAC;QACnE,CAAC;QACD,OAAO,GAAyC,CAAC;IAClD,CAAC;IAAC,MAAM,CAAC;QACR,OAAO,SAAS,CAAC;IAClB,CAAC;AAAA,CACD;AAaD,SAAS,gBAAgB,CACxB,OAAuG,EACtG;IACD,OAAO,IAAI,KAAK,CACf,qDAAqD,OAAO,CAAC,MAAM,mJAAmJ,CACtN,CAAC;AAAA,CACF;AAED;;;GAGG;AACH,MAAM,UAAU,kCAAkC,CACjD,UAAkB,EAClB,OAAO,GAA8B,EAAE,EACtB;IACjB,MAAM,cAAc,GAAG,OAAO,CAAC,cAAc,IAAI,CAAC,GAAG,EAAE,CAAC,mBAAmB,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC;IAC/F,MAAM,gBAAgB,GAAG,OAAO,CAAC,gBAAgB,IAAI,uBAAuB,EAAE,CAAC;IAC/E,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,IAAI,4BAA4B,CAAC;IAClE,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,IAAI,YAAY,CAAC;IAE5C,OAAO;QACN,KAAK,CAAC,IAAI,CAAC,OAAO,EAAE,GAAG,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,GAAG,EAAE,EAAE;YAC1D,MAAM,OAAO,GAAG,MAAM,cAAc,EAAE,CAAC;YACvC,IAAI,OAAO,CAAC,MAAM,KAAK,OAAO;gBAAE,MAAM,gBAAgB,CAAC,OAAO,CAAC,CAAC;YAEhE,MAAM,KAAK,GAAG,QAAQ,CAAC,UAAU,CAAC,CAAC;YACnC,MAAM,YAAY,GAAG,mBAAmB,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;YACrD,MAAM,YAAY,GAAG,iBAAiB,CAAC,KAAK,EAAE,GAAG,IAAI,WAAW,EAAE,CAAC,CAAC;YACpE,MAAM,SAAS,GAAG,OAAO,KAAK,SAAS,IAAI,OAAO,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,GAAG,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC;YACpF,0FAA0F;YAC1F,uFAAuF;YACvF,2FAA2F;YAC3F,qEAAqE;YACrE,MAAM,gBAAgB,GAAG,SAAS,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,SAAS,GAAG,GAAG,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;YAE9F,MAAM,OAAO,GAAG;gBACf,OAAO;gBACP,GAAG,EAAE,YAAY;gBACjB,GAAG,EAAE,YAAY;gBACjB,GAAG,CAAC,gBAAgB,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,gBAAgB,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;aAC1E,CAAC;YAEF,MAAM,KAAK,GAAG,KAAK,CAAC,OAAO,CAAC,UAAU,EAAE,CAAC,IAAI,EAAE,gBAAgB,CAAC,EAAE;gBACjE,GAAG,EAAE,YAAY;gBACjB,GAAG,EAAE;oBACJ,GAAG,YAAY;oBACf,uBAAuB,EAAE,GAAG;oBAC5B,gBAAgB,EAAE,OAAO;oBACzB,gBAAgB,EAAE,GAAG;oBACrB,UAAU,EAAE,GAAG;iBACf;gBACD,KAAK,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC;aAC/B,CAAC,CAAC;YACH,IAAI,KAAK,CAAC,GAAG;gBAAE,qBAAqB,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;YAEhD,IAAI,aAAa,GAA4B,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;YAC7D,IAAI,eAAe,GAAG,KAAK,CAAC;YAE5B,+EAA+E;YAC/E,sDAAsD;YACtD,KAAK,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,EAAE,CAAC,KAAa,EAAE,EAAE,CAAC;gBAC3C,MAAM,CAAC,KAAK,CAAC,CAAC;YAAA,CACd,CAAC,CAAC;YACH,gFAAgF;YAChF,6EAA6E;YAC7E,KAAK,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,EAAE,CAAC,KAAa,EAAE,EAAE,CAAC;gBAC3C,IAAI,eAAe;oBAAE,OAAO;gBAC5B,IAAI,aAAa,CAAC,MAAM,GAAG,KAAK,CAAC,MAAM,GAAG,uBAAuB,EAAE,CAAC;oBACnE,eAAe,GAAG,IAAI,CAAC;oBACvB,OAAO;gBACR,CAAC;gBACD,aAAa,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,aAAa,EAAE,KAAK,CAAC,CAAC,CAAC;YAAA,CACtD,CAAC,CAAC;YAEH,IAAI,CAAC;gBACJ,KAAK,CAAC,KAAK,EAAE,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC,CAAC;gBAClD,MAAM,QAAQ,GAAG,MAAM,kCAAkC,CAAC,KAAK,EAAE;oBAChE,MAAM;oBACN,SAAS;oBACT,WAAW,EAAE,KAAK;iBAClB,CAAC,CAAC;gBACH,IAAI,MAAM,EAAE,OAAO;oBAAE,MAAM,IAAI,KAAK,CAAC,SAAS,CAAC,CAAC;gBAEhD,MAAM,KAAK,GAAG,eAAe,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,iBAAiB,CAAC,aAAa,CAAC,CAAC;gBAC7E,IAAI,CAAC,KAAK,EAAE,CAAC;oBACZ,MAAM,cAAc,GAAG,eAAe;wBACrC,CAAC,CAAC,0BAA0B,uBAAuB,QAAQ;wBAC3D,CAAC,CAAC,aAAa,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;oBAClC,MAAM,IAAI,yBAAyB,CAClC,6CAA6C,QAAQ,CAAC,IAAI,IAAI,MAAM,yCAAyC,cAAc,EAAE,EAC7H,cAAc,CACd,CAAC;gBACH,CAAC;gBAED,gBAAgB,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;gBAC/B,IAAI,KAAK,CAAC,WAAW;oBAAE,MAAM,IAAI,KAAK,CAAC,KAAK,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC;gBAClE,OAAO,EAAE,QAAQ,EAAE,KAAK,CAAC,QAAQ,EAAE,CAAC;YACrC,CAAC;oBAAS,CAAC;gBACV,IAAI,KAAK,CAAC,GAAG;oBAAE,uBAAuB,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;YACnD,CAAC;QAAA,CACD;KACD,CAAC;AAAA,CACF","sourcesContent":["/**\n * The `python-engine` tier: per-command spawn of the bundled `pi-shell-engine` (D3 in the\n * blueprint). Wires the frozen §1.2/§1.3 stdin-JSON-request / 0x1e-framed-stdout protocol into a\n * `BashOperations`, threads `WindowsShellState` through both directions (D4), and degrades to a\n * named, actionable error when the Python runtime is not ready — the PowerShell floor keeps\n * working regardless (the router never routes simple commands here when the engine is off).\n */\n\nimport type { ChildProcess, SpawnOptions } from \"node:child_process\";\nimport { join } from \"node:path\";\nimport { getBundledResourcesDir } from \"../../config.ts\";\nimport { spawnProcess, waitForChildProcessWithTermination } from \"../../utils/child-process.ts\";\nimport { getShellEnv, trackDetachedChildPid, untrackDetachedChildPid } from \"../../utils/shell.ts\";\nimport { ensurePythonRuntime, type PythonRuntimeOutcome } from \"../python-runtime.ts\";\nimport { isRecordObject } from \"../util/value-guards.ts\";\nimport type { BashOperations } from \"./bash.ts\";\nimport {\n\tapplyEngineFrame,\n\tgetOrCreateWindowsShellState,\n\tmergeEffectiveEnv,\n\tresolveEffectiveCwd,\n\ttype WindowsShellState,\n} from \"./windows-shell-state.ts\";\n\nconst ENGINE_FRAME_SENTINEL = 0x1e;\n\n/** The full §1.3 control-frame shape. */\nexport interface WindowsShellEngineFrame {\n\texitCode: number;\n\tcwd: string;\n\tenvDelta: Record<string, string | null>;\n\tunsupported: { code: \"unsupported\"; construct: string; message: string } | null;\n}\n\nexport class WindowsShellEngineFailure extends Error {\n\treadonly capturedOutput: string;\n\n\tconstructor(message: string, capturedOutput: string) {\n\t\tsuper(message);\n\t\tthis.name = \"WindowsShellEngineFailure\";\n\t\tthis.capturedOutput = capturedOutput;\n\t}\n}\n\nfunction resolveEngineScriptPath(): string {\n\treturn join(getBundledResourcesDir(), \"runtimes\", \"pi-shell-engine\", \"main.py\");\n}\n\nconst MAX_CONTROL_FRAME_BYTES = 64 * 1024;\n\n/** Parse the terminal frame from the dedicated control stream. Command output is never retained here. */\nfunction parseControlFrame(buffer: Buffer): WindowsShellEngineFrame | undefined {\n\tif (buffer.length < 2 || buffer[buffer.length - 1] !== ENGINE_FRAME_SENTINEL) return undefined;\n\tconst openIndex = buffer.lastIndexOf(ENGINE_FRAME_SENTINEL, buffer.length - 2);\n\tif (openIndex === -1) return undefined;\n\ttry {\n\t\tconst raw: unknown = JSON.parse(buffer.subarray(openIndex + 1, buffer.length - 1).toString(\"utf8\"));\n\t\tif (!isRecordObject(raw)) return undefined;\n\t\tif (typeof raw.exitCode !== \"number\" || !Number.isInteger(raw.exitCode)) return undefined;\n\t\tif (typeof raw.cwd !== \"string\" || !isRecordObject(raw.envDelta)) return undefined;\n\t\tif (raw.unsupported !== null && !isRecordObject(raw.unsupported)) return undefined;\n\t\tif (\n\t\t\traw.unsupported !== null &&\n\t\t\t(raw.unsupported.code !== \"unsupported\" ||\n\t\t\t\ttypeof raw.unsupported.construct !== \"string\" ||\n\t\t\t\ttypeof raw.unsupported.message !== \"string\")\n\t\t)\n\t\t\treturn undefined;\n\t\tfor (const value of Object.values(raw.envDelta)) {\n\t\t\tif (value !== null && typeof value !== \"string\") return undefined;\n\t\t}\n\t\treturn raw as unknown as WindowsShellEngineFrame;\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\nexport interface WindowsShellEngineOptions {\n\t/** Override for tests: resolves the Python runtime outcome. Default: `ensurePythonRuntime`. */\n\tresolveRuntime?: () => Promise<PythonRuntimeOutcome>;\n\t/** Override for tests: absolute path to the engine's `main.py`. Default: the bundled runtime. */\n\tengineScriptPath?: string;\n\t/** Override for tests: the per-session state store lookup. Default: the shared module store. */\n\tgetState?: (sessionKey: string) => WindowsShellState;\n\t/** Override for tests: spawns the engine child process. Default: `spawnProcess` (cross-spawn on win32). */\n\tspawn?: (command: string, args: string[], options: SpawnOptions) => ChildProcess;\n}\n\nfunction degradationError(\n\toutcome: Extract<PythonRuntimeOutcome, { status: \"offline\" | \"uv-unavailable\" | \"python-unavailable\" }>,\n) {\n\treturn new Error(\n\t\t`The Windows shell engine (Python) is unavailable: ${outcome.reason} The simple-command PowerShell floor still works; fix the Python runtime (uv/network) to restore pipelines, redirection, expansion, and chaining.`,\n\t);\n}\n\n/**\n * Create the `python-engine` tier `BashOperations` for one bash-tool session. `exec`'s `command`\n * argument is the RAW Bash source from the `python-engine` route (never PowerShell-translated).\n */\nexport function createWindowsShellEngineOperations(\n\tsessionKey: string,\n\toptions: WindowsShellEngineOptions = {},\n): BashOperations {\n\tconst resolveRuntime = options.resolveRuntime ?? (() => ensurePythonRuntime({ silent: true }));\n\tconst engineScriptPath = options.engineScriptPath ?? resolveEngineScriptPath();\n\tconst getState = options.getState ?? getOrCreateWindowsShellState;\n\tconst spawn = options.spawn ?? spawnProcess;\n\n\treturn {\n\t\tasync exec(command, cwd, { onData, signal, timeout, env }) {\n\t\t\tconst runtime = await resolveRuntime();\n\t\t\tif (runtime.status !== \"ready\") throw degradationError(runtime);\n\n\t\t\tconst state = getState(sessionKey);\n\t\t\tconst effectiveCwd = resolveEffectiveCwd(state, cwd);\n\t\t\tconst effectiveEnv = mergeEffectiveEnv(state, env ?? getShellEnv());\n\t\t\tconst timeoutMs = timeout !== undefined && timeout > 0 ? timeout * 1000 : undefined;\n\t\t\t// The engine's own soft deadline must fire and emit its cooperative exit-124 frame (which\n\t\t\t// preserves partial output) BEFORE the hard tree-kill backstop below, or the hard kill\n\t\t\t// always wins the race and the soft path never gets to run. Give it a head start; the hard\n\t\t\t// kill still bounds a hung engine that never reads its own deadline.\n\t\t\tconst requestTimeoutMs = timeoutMs !== undefined ? Math.max(timeoutMs - 500, 500) : undefined;\n\n\t\t\tconst request = {\n\t\t\t\tcommand,\n\t\t\t\tcwd: effectiveCwd,\n\t\t\t\tenv: effectiveEnv,\n\t\t\t\t...(requestTimeoutMs !== undefined ? { timeoutMs: requestTimeoutMs } : {}),\n\t\t\t};\n\n\t\t\tconst child = spawn(runtime.pythonPath, [\"-B\", engineScriptPath], {\n\t\t\t\tcwd: effectiveCwd,\n\t\t\t\tenv: {\n\t\t\t\t\t...effectiveEnv,\n\t\t\t\t\tPYTHONDONTWRITEBYTECODE: \"1\",\n\t\t\t\t\tPYTHONIOENCODING: \"utf-8\",\n\t\t\t\t\tPYTHONUNBUFFERED: \"1\",\n\t\t\t\t\tPYTHONUTF8: \"1\",\n\t\t\t\t},\n\t\t\t\tstdio: [\"pipe\", \"pipe\", \"pipe\"],\n\t\t\t});\n\t\t\tif (child.pid) trackDetachedChildPid(child.pid);\n\n\t\t\tlet controlBuffer: Buffer<ArrayBufferLike> = Buffer.alloc(0);\n\t\t\tlet controlOverflow = false;\n\n\t\t\t// Command output is the data stream: forward it immediately and never retain a\n\t\t\t// second full copy merely to find the terminal frame.\n\t\t\tchild.stdout?.on(\"data\", (chunk: Buffer) => {\n\t\t\t\tonData(chunk);\n\t\t\t});\n\t\t\t// stderr is the bounded control stream. The Python engine routes command stderr\n\t\t\t// into stdout, so this channel contains only the terminal frame/diagnostics.\n\t\t\tchild.stderr?.on(\"data\", (chunk: Buffer) => {\n\t\t\t\tif (controlOverflow) return;\n\t\t\t\tif (controlBuffer.length + chunk.length > MAX_CONTROL_FRAME_BYTES) {\n\t\t\t\t\tcontrolOverflow = true;\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tcontrolBuffer = Buffer.concat([controlBuffer, chunk]);\n\t\t\t});\n\n\t\t\ttry {\n\t\t\t\tchild.stdin?.end(JSON.stringify(request), \"utf8\");\n\t\t\t\tconst terminal = await waitForChildProcessWithTermination(child, {\n\t\t\t\t\tsignal,\n\t\t\t\t\ttimeoutMs,\n\t\t\t\t\tkillGraceMs: 2_000,\n\t\t\t\t});\n\t\t\t\tif (signal?.aborted) throw new Error(\"aborted\");\n\n\t\t\t\tconst frame = controlOverflow ? undefined : parseControlFrame(controlBuffer);\n\t\t\t\tif (!frame) {\n\t\t\t\t\tconst capturedOutput = controlOverflow\n\t\t\t\t\t\t? `control frame exceeded ${MAX_CONTROL_FRAME_BYTES} bytes`\n\t\t\t\t\t\t: controlBuffer.toString(\"utf8\");\n\t\t\t\t\tthrow new WindowsShellEngineFailure(\n\t\t\t\t\t\t`Windows shell engine failed (process exit ${terminal.code ?? \"null\"}) without a parseable control frame.\\n${capturedOutput}`,\n\t\t\t\t\t\tcapturedOutput,\n\t\t\t\t\t);\n\t\t\t\t}\n\n\t\t\t\tapplyEngineFrame(state, frame);\n\t\t\t\tif (frame.unsupported) throw new Error(frame.unsupported.message);\n\t\t\t\treturn { exitCode: frame.exitCode };\n\t\t\t} finally {\n\t\t\t\tif (child.pid) untrackDetachedChildPid(child.pid);\n\t\t\t}\n\t\t},\n\t};\n}\n"]}
@@ -42,6 +42,10 @@ export interface AtomicFileLockOptions {
42
42
  /** Lock staleness window in ms (proper-lockfile `stale`); omitted = proper-lockfile's own default. */
43
43
  stale?: number;
44
44
  }
45
+ export interface AtomicFileWriteOptions {
46
+ /** POSIX permission bits applied to the temporary file and inherited by the renamed destination. */
47
+ mode?: number;
48
+ }
45
49
  /**
46
50
  * Hold an exclusive advisory lock on `filePath` for the duration of `fn` (sync). Always releases,
47
51
  * including when `fn` throws.
@@ -49,7 +53,7 @@ export interface AtomicFileLockOptions {
49
53
  export declare function withFileLockSync<T>(filePath: string, fn: () => T, options?: AtomicFileLockOptions): T;
50
54
  /** Async counterpart of {@link withFileLockSync}. Always releases, including when `fn` throws/rejects. */
51
55
  export declare function withFileLock<T>(filePath: string, fn: () => Promise<T> | T, options?: AtomicFileLockOptions): Promise<T>;
52
- export declare function writeFileAtomicSync(filePath: string, content: string): void;
56
+ export declare function writeFileAtomicSync(filePath: string, content: string, options?: AtomicFileWriteOptions): void;
53
57
  /** Async counterpart of {@link writeFileAtomicSync}. */
54
- export declare function writeFileAtomic(filePath: string, content: string): Promise<void>;
58
+ export declare function writeFileAtomic(filePath: string, content: string, options?: AtomicFileWriteOptions): Promise<void>;
55
59
  //# sourceMappingURL=atomic-file.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"atomic-file.d.ts","sourceRoot":"","sources":["../../../src/core/util/atomic-file.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAiEH,MAAM,WAAW,qBAAqB;IACrC;;;;;;;;;;;;;;;;;OAiBG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,iGAAiG;IACjG,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,sGAAsG;IACtG,KAAK,CAAC,EAAE,MAAM,CAAC;CACf;AA2ED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,CAAC,EAAE,OAAO,CAAC,EAAE,qBAAqB,GAAG,CAAC,CAcrG;AAED,0GAA0G;AAC1G,wBAAsB,YAAY,CAAC,CAAC,EACnC,QAAQ,EAAE,MAAM,EAChB,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,EACxB,OAAO,CAAC,EAAE,qBAAqB,GAC7B,OAAO,CAAC,CAAC,CAAC,CAiBZ;AA6BD,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAY3E;AAED,wDAAwD;AACxD,wBAAsB,eAAe,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAYtF","sourcesContent":["/**\n * Shared atomic-file helper: a single lock+tmp+rename implementation reused by every\n * on-disk store that does a read-modify-write against a JSON/text file. Modeled on the pattern\n * already proven in `memory/providers/file-store.ts` (proper-lockfile advisory lock + write-tmp-\n * then-rename); before this helper existed the pattern was copy-pasted per store, and most copies\n * were missing either the lock, the atomic write, or both.\n *\n * Two call shapes:\n * - `withFileLock(Sync)` — hold an exclusive advisory lock across an arbitrary read-modify-write\n * callback. The lock spans BOTH the read and the write, closing the classic RMW race where two\n * writers each read the old content before either writes back.\n * - `writeFileAtomic(Sync)` — write-tmp-then-rename. Used INSIDE a `withFileLock` callback (or\n * standalone, for a pure overwrite that has no read step to race).\n *\n * Sync and async variants are both exported. Most existing stores expose a synchronous public API\n * called from hot, non-async paths (e.g. a per-token-stream perf sample), so the sync variant lets\n * them gain locking without forcing an async ripple through their callers; the async variant is for\n * call sites that are already async.\n */\n\nimport { randomUUID } from \"node:crypto\";\nimport { promises as fsPromises, mkdirSync, renameSync, unlinkSync, writeFileSync } from \"node:fs\";\nimport { dirname } from \"node:path\";\nimport lockfile from \"proper-lockfile\";\n\n/**\n * Bounded rename retry budget for the win32-only transient-rename handling below (see\n * {@link isTransientRenameErrorOnWin32}). Backoff doubles each attempt starting at\n * {@link RENAME_RETRY_MIN_TIMEOUT_MS} and capped at {@link RENAME_RETRY_MAX_TIMEOUT_MS}, mirroring\n * the doubling-backoff shape used for lock acquisition above. With {@link RENAME_RETRY_ATTEMPTS}\n * extra attempts (10, 20, 40, 80, ... capped at 200) the worst-case total wait is ~1.03s\n * (10+20+40+80+160+200+200+200+200 ~= 1110ms across 9 gaps), which comfortably covers the\n * millisecond-scale window Defender/the Windows Search indexer hold a freshly-written file open\n * without FILE_SHARE_DELETE before releasing it.\n */\nconst RENAME_RETRY_ATTEMPTS = 9;\nconst RENAME_RETRY_MIN_TIMEOUT_MS = 10;\nconst RENAME_RETRY_MAX_TIMEOUT_MS = 200;\n\n/**\n * On win32, antivirus (e.g. Windows Defender's real-time scanner) and the Windows Search indexer\n * routinely open a freshly-written file for a brief scan without `FILE_SHARE_DELETE`, which makes a\n * rename over/of that file fail transiently with EPERM/EACCES/EBUSY for a few milliseconds. This is\n * a well-documented platform semantic, not a bug in our write path — it's exactly why the\n * `graceful-fs` package ships its own win32 rename-retry wrapper. POSIX platforms never exhibit this\n * transient (a POSIX rename either succeeds or fails for a real, non-transient reason), so retry is\n * gated strictly to win32 to keep POSIX behavior byte-identical to a bare rename.\n */\nfunction isTransientRenameErrorOnWin32(err: unknown): boolean {\n\tif (process.platform !== \"win32\") return false;\n\tif (typeof err !== \"object\" || err === null) return false;\n\tconst code = (err as { code?: string }).code;\n\treturn code === \"EPERM\" || code === \"EACCES\" || code === \"EBUSY\";\n}\n\n/** Sync counterpart of the rename-retry policy; see {@link isTransientRenameErrorOnWin32}. */\nfunction renameSyncWithRetry(tmpPath: string, filePath: string): void {\n\tfor (let attempt = 0; attempt <= RENAME_RETRY_ATTEMPTS; attempt++) {\n\t\ttry {\n\t\t\trenameSync(tmpPath, filePath);\n\t\t\treturn;\n\t\t} catch (err) {\n\t\t\tif (!isTransientRenameErrorOnWin32(err) || attempt === RENAME_RETRY_ATTEMPTS) throw err;\n\t\t\tconst backoffMs = Math.min(RENAME_RETRY_MIN_TIMEOUT_MS * 2 ** attempt, RENAME_RETRY_MAX_TIMEOUT_MS);\n\t\t\tblockingSleepMs(backoffMs);\n\t\t}\n\t}\n}\n\n/** Async counterpart of the rename-retry policy; see {@link isTransientRenameErrorOnWin32}. */\nasync function renameWithRetry(tmpPath: string, filePath: string): Promise<void> {\n\tfor (let attempt = 0; attempt <= RENAME_RETRY_ATTEMPTS; attempt++) {\n\t\ttry {\n\t\t\tawait fsPromises.rename(tmpPath, filePath);\n\t\t\treturn;\n\t\t} catch (err) {\n\t\t\tif (!isTransientRenameErrorOnWin32(err) || attempt === RENAME_RETRY_ATTEMPTS) throw err;\n\t\t\tconst backoffMs = Math.min(RENAME_RETRY_MIN_TIMEOUT_MS * 2 ** attempt, RENAME_RETRY_MAX_TIMEOUT_MS);\n\t\t\tawait new Promise((resolve) => setTimeout(resolve, backoffMs));\n\t\t}\n\t}\n}\n\nexport interface AtomicFileLockOptions {\n\t/**\n\t * Bounded retry attempts while waiting for a lock already held elsewhere. Both variants use a\n\t * SHORT, capped backoff (see {@link RETRY_MIN_TIMEOUT_MS}/{@link RETRY_MAX_TIMEOUT_MS}) — these\n\t * stores' critical sections are sub-millisecond reads+writes of small JSON/text files, so\n\t * contention should clear in milliseconds, not the multi-second-to-31-second worst case\n\t * proper-lockfile's OWN default backoff produces for a bare numeric `retries` (its default\n\t * `minTimeout` is 1000ms with factor 2 — see node_modules/retry/lib/retry.js). Passing a bare\n\t * number straight through would turn brief contention into a multi-second stall on a hot path\n\t * (e.g. a per-token-stream perf sample), so both variants instead build an explicit short-backoff\n\t * `retry` options object.\n\t * - Async (`withFileLock`): forwarded as `{retries, minTimeout, maxTimeout}` to proper-lockfile.\n\t * - Sync (`withFileLockSync`): proper-lockfile's sync API REJECTS `retries > 0` outright (it\n\t * requires the whole acquire flow to be synchronous — see proper-lockfile/lib/adapter.js\n\t * `toSyncOptions`, which throws `ESYNC`). So the sync path implements its own bounded retry\n\t * around single `lockfile.lockSync` attempts, blocking briefly between them (Atomics.wait) —\n\t * callers are already fully synchronous fs code, so a short blocking wait on contention matches\n\t * the existing execution model rather than introducing a new one.\n\t */\n\tretries?: number;\n\t/** Resolve symlinks before locking (proper-lockfile `realpath`); false matches file-store.ts. */\n\trealpath?: boolean;\n\t/** Lock staleness window in ms (proper-lockfile `stale`); omitted = proper-lockfile's own default. */\n\tstale?: number;\n}\n\nconst DEFAULT_RETRIES = 10;\nconst DEFAULT_REALPATH = false;\n/**\n * Capped doubling backoff shared by both variants — see {@link AtomicFileLockOptions.retries}.\n * With the current defaults (10 retries, 25ms floor, 500ms cap) the total worst-case contention\n * budget is ~3.3s (25+50+100+200+400+500+500+500+500+500), comfortably over the >= 2s floor this\n * needs to survive a loaded CI runner (e.g. the Windows CI mkdir/rmdir lock-directory churn under\n * two-real-OS-thread contention).\n */\nconst RETRY_MIN_TIMEOUT_MS = 25;\nconst RETRY_MAX_TIMEOUT_MS = 500;\nfunction ensureLockDirSync(filePath: string): void {\n\tmkdirSync(dirname(filePath), { recursive: true });\n}\n\nasync function ensureLockDir(filePath: string): Promise<void> {\n\tawait fsPromises.mkdir(dirname(filePath), { recursive: true });\n}\n\nfunction isLockedError(err: unknown): boolean {\n\tif (typeof err !== \"object\" || err === null) return false;\n\tconst code = (err as { code?: string }).code;\n\tif (code === \"ELOCKED\") return true;\n\t// On win32, mkdir-ing the lock directory can transiently surface EPERM (rather than the\n\t// expected EEXIST) when a previous incarnation of that directory is concurrently being\n\t// rmdir'd by the racing releaser — the mkdir lands mid-teardown and the OS reports \"operation\n\t// not permitted\" instead of \"already exists\". This is contention, not a real permissions\n\t// failure, so treat it identically to ELOCKED: retry with the existing backoff rather than\n\t// letting it escape as fatal. POSIX platforms don't exhibit this transient and keep surfacing\n\t// real EPERM (e.g. an actually unwritable directory) as fatal.\n\tif (code === \"EPERM\" && process.platform === \"win32\") return true;\n\treturn false;\n}\n\n/** Block the calling thread for `ms` without spinning the CPU (Atomics.wait on a private buffer). */\nfunction blockingSleepMs(ms: number): void {\n\tAtomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);\n}\n\n/**\n * Acquire a proper-lockfile sync lock with bounded retry on `ELOCKED` (proper-lockfile's sync API\n * itself forbids `retries > 0`; see {@link AtomicFileLockOptions.retries}).\n *\n * Backoff doubles each attempt (mirroring the `retry` module's own `factor: 2` default that the\n * async path gets for free from proper-lockfile), starting at {@link RETRY_MIN_TIMEOUT_MS} and\n * capped at {@link RETRY_MAX_TIMEOUT_MS}.\n *\n * This is budget PARITY between two implementations of the same lock-acquisition contract, not\n * flake-masking: `options.retries` (a count of RETRIES, i.e. attempts after the first) means the\n * same thing on both paths, so both must make `retries + 1` total acquisition attempts with the\n * same per-gap backoff schedule. The async path gets `retries + 1` attempts for free from the\n * `retry` module (it retries `options.retries` times after its initial attempt); the sync path\n * used to loop exactly `attempts` times total (one fewer gap, and one fewer attempt than the\n * async path for the same `retries` value), which under-budgeted it relative to its async sibling\n * and surfaced as spurious ELOCKED failures under real contention (e.g. two OS threads hammering\n * the same file, or mkdir/rmdir lock-directory churn on a loaded Windows CI runner).\n */\nfunction lockSyncWithRetry(filePath: string, options: AtomicFileLockOptions): () => void {\n\tconst attempts = Math.max(1, options.retries ?? DEFAULT_RETRIES) + 1;\n\tconst lockOptions = { realpath: options.realpath ?? DEFAULT_REALPATH, stale: options.stale };\n\tfor (let attempt = 1; attempt <= attempts; attempt++) {\n\t\ttry {\n\t\t\treturn lockfile.lockSync(filePath, lockOptions);\n\t\t} catch (err) {\n\t\t\tif (!isLockedError(err) || attempt === attempts) throw err;\n\t\t\tconst backoffMs = Math.min(RETRY_MIN_TIMEOUT_MS * 2 ** (attempt - 1), RETRY_MAX_TIMEOUT_MS);\n\t\t\tblockingSleepMs(backoffMs);\n\t\t}\n\t}\n\t// Unreachable (the loop always returns or throws), but keeps the function's return type honest.\n\tthrow new Error(`Failed to acquire lock for ${filePath}`);\n}\n\n/**\n * Hold an exclusive advisory lock on `filePath` for the duration of `fn` (sync). Always releases,\n * including when `fn` throws.\n */\nexport function withFileLockSync<T>(filePath: string, fn: () => T, options?: AtomicFileLockOptions): T {\n\tensureLockDirSync(filePath);\n\tconst release = lockSyncWithRetry(filePath, options ?? {});\n\ttry {\n\t\treturn fn();\n\t} finally {\n\t\t// A lock-cleanup failure must never mask fn()'s result (or replace fn()'s own thrown error) —\n\t\t// by this point fn() has already durably committed or failed on its own terms.\n\t\ttry {\n\t\t\trelease();\n\t\t} catch {\n\t\t\t// best-effort cleanup; a stale lock self-expires via proper-lockfile's `stale` window\n\t\t}\n\t}\n}\n\n/** Async counterpart of {@link withFileLockSync}. Always releases, including when `fn` throws/rejects. */\nexport async function withFileLock<T>(\n\tfilePath: string,\n\tfn: () => Promise<T> | T,\n\toptions?: AtomicFileLockOptions,\n): Promise<T> {\n\tawait ensureLockDir(filePath);\n\tconst release = await lockfile.lock(filePath, {\n\t\trealpath: options?.realpath ?? DEFAULT_REALPATH,\n\t\tretries: {\n\t\t\tretries: options?.retries ?? DEFAULT_RETRIES,\n\t\t\tminTimeout: RETRY_MIN_TIMEOUT_MS,\n\t\t\tmaxTimeout: RETRY_MAX_TIMEOUT_MS,\n\t\t},\n\t\tstale: options?.stale,\n\t});\n\ttry {\n\t\treturn await fn();\n\t} finally {\n\t\t// See {@link withFileLockSync} — cleanup failures must not mask fn()'s outcome.\n\t\tawait release().catch(() => {});\n\t}\n}\n\n/**\n * Write `content` to `filePath` via write-tmp-then-rename (sync); the rename is atomic on the same\n * filesystem, so a concurrent reader never observes a partially-written file. Does NOT itself lock —\n * call from inside {@link withFileLockSync} when the write follows a read that must not race another\n * writer's read+write; call standalone for an unconditional overwrite with no read step.\n */\nfunction temporaryPath(filePath: string): string {\n\treturn `${filePath}.${process.pid}.${randomUUID()}.tmp`;\n}\n\n/** Remove one invocation's temporary path without masking the original write/rename failure. */\nfunction removeTemporaryPathSync(tmpPath: string): void {\n\ttry {\n\t\tunlinkSync(tmpPath);\n\t} catch {\n\t\t// The rename may have consumed the path, or another failure may have prevented its creation.\n\t}\n}\n\nasync function removeTemporaryPath(tmpPath: string): Promise<void> {\n\ttry {\n\t\tawait fsPromises.unlink(tmpPath);\n\t} catch {\n\t\t// The rename may have consumed the path, or another failure may have prevented its creation.\n\t}\n}\n\nexport function writeFileAtomicSync(filePath: string, content: string): void {\n\tmkdirSync(dirname(filePath), { recursive: true });\n\tconst tmpPath = temporaryPath(filePath);\n\tlet renamed = false;\n\ttry {\n\t\t// `wx` makes a nonce collision harmless: never truncate another writer's temporary file.\n\t\twriteFileSync(tmpPath, content, { encoding: \"utf-8\", flag: \"wx\" });\n\t\trenameSyncWithRetry(tmpPath, filePath);\n\t\trenamed = true;\n\t} finally {\n\t\tif (!renamed) removeTemporaryPathSync(tmpPath);\n\t}\n}\n\n/** Async counterpart of {@link writeFileAtomicSync}. */\nexport async function writeFileAtomic(filePath: string, content: string): Promise<void> {\n\tawait fsPromises.mkdir(dirname(filePath), { recursive: true });\n\tconst tmpPath = temporaryPath(filePath);\n\tlet renamed = false;\n\ttry {\n\t\t// `wx` makes a nonce collision harmless: never truncate another writer's temporary file.\n\t\tawait fsPromises.writeFile(tmpPath, content, { encoding: \"utf-8\", flag: \"wx\" });\n\t\tawait renameWithRetry(tmpPath, filePath);\n\t\trenamed = true;\n\t} finally {\n\t\tif (!renamed) await removeTemporaryPath(tmpPath);\n\t}\n}\n"]}
1
+ {"version":3,"file":"atomic-file.d.ts","sourceRoot":"","sources":["../../../src/core/util/atomic-file.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAiEH,MAAM,WAAW,qBAAqB;IACrC;;;;;;;;;;;;;;;;;OAiBG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,iGAAiG;IACjG,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,sGAAsG;IACtG,KAAK,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,sBAAsB;IACtC,oGAAoG;IACpG,IAAI,CAAC,EAAE,MAAM,CAAC;CACd;AA2ED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,CAAC,EAAE,OAAO,CAAC,EAAE,qBAAqB,GAAG,CAAC,CAcrG;AAED,0GAA0G;AAC1G,wBAAsB,YAAY,CAAC,CAAC,EACnC,QAAQ,EAAE,MAAM,EAChB,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,EACxB,OAAO,CAAC,EAAE,qBAAqB,GAC7B,OAAO,CAAC,CAAC,CAAC,CAiBZ;AA6BD,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,sBAAsB,GAAG,IAAI,CAY7G;AAED,wDAAwD;AACxD,wBAAsB,eAAe,CACpC,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,EACf,OAAO,CAAC,EAAE,sBAAsB,GAC9B,OAAO,CAAC,IAAI,CAAC,CAYf","sourcesContent":["/**\n * Shared atomic-file helper: a single lock+tmp+rename implementation reused by every\n * on-disk store that does a read-modify-write against a JSON/text file. Modeled on the pattern\n * already proven in `memory/providers/file-store.ts` (proper-lockfile advisory lock + write-tmp-\n * then-rename); before this helper existed the pattern was copy-pasted per store, and most copies\n * were missing either the lock, the atomic write, or both.\n *\n * Two call shapes:\n * - `withFileLock(Sync)` — hold an exclusive advisory lock across an arbitrary read-modify-write\n * callback. The lock spans BOTH the read and the write, closing the classic RMW race where two\n * writers each read the old content before either writes back.\n * - `writeFileAtomic(Sync)` — write-tmp-then-rename. Used INSIDE a `withFileLock` callback (or\n * standalone, for a pure overwrite that has no read step to race).\n *\n * Sync and async variants are both exported. Most existing stores expose a synchronous public API\n * called from hot, non-async paths (e.g. a per-token-stream perf sample), so the sync variant lets\n * them gain locking without forcing an async ripple through their callers; the async variant is for\n * call sites that are already async.\n */\n\nimport { randomUUID } from \"node:crypto\";\nimport { promises as fsPromises, mkdirSync, renameSync, unlinkSync, writeFileSync } from \"node:fs\";\nimport { dirname } from \"node:path\";\nimport lockfile from \"proper-lockfile\";\n\n/**\n * Bounded rename retry budget for the win32-only transient-rename handling below (see\n * {@link isTransientRenameErrorOnWin32}). Backoff doubles each attempt starting at\n * {@link RENAME_RETRY_MIN_TIMEOUT_MS} and capped at {@link RENAME_RETRY_MAX_TIMEOUT_MS}, mirroring\n * the doubling-backoff shape used for lock acquisition above. With {@link RENAME_RETRY_ATTEMPTS}\n * extra attempts (10, 20, 40, 80, ... capped at 200) the worst-case total wait is ~1.03s\n * (10+20+40+80+160+200+200+200+200 ~= 1110ms across 9 gaps), which comfortably covers the\n * millisecond-scale window Defender/the Windows Search indexer hold a freshly-written file open\n * without FILE_SHARE_DELETE before releasing it.\n */\nconst RENAME_RETRY_ATTEMPTS = 9;\nconst RENAME_RETRY_MIN_TIMEOUT_MS = 10;\nconst RENAME_RETRY_MAX_TIMEOUT_MS = 200;\n\n/**\n * On win32, antivirus (e.g. Windows Defender's real-time scanner) and the Windows Search indexer\n * routinely open a freshly-written file for a brief scan without `FILE_SHARE_DELETE`, which makes a\n * rename over/of that file fail transiently with EPERM/EACCES/EBUSY for a few milliseconds. This is\n * a well-documented platform semantic, not a bug in our write path — it's exactly why the\n * `graceful-fs` package ships its own win32 rename-retry wrapper. POSIX platforms never exhibit this\n * transient (a POSIX rename either succeeds or fails for a real, non-transient reason), so retry is\n * gated strictly to win32 to keep POSIX behavior byte-identical to a bare rename.\n */\nfunction isTransientRenameErrorOnWin32(err: unknown): boolean {\n\tif (process.platform !== \"win32\") return false;\n\tif (typeof err !== \"object\" || err === null) return false;\n\tconst code = (err as { code?: string }).code;\n\treturn code === \"EPERM\" || code === \"EACCES\" || code === \"EBUSY\";\n}\n\n/** Sync counterpart of the rename-retry policy; see {@link isTransientRenameErrorOnWin32}. */\nfunction renameSyncWithRetry(tmpPath: string, filePath: string): void {\n\tfor (let attempt = 0; attempt <= RENAME_RETRY_ATTEMPTS; attempt++) {\n\t\ttry {\n\t\t\trenameSync(tmpPath, filePath);\n\t\t\treturn;\n\t\t} catch (err) {\n\t\t\tif (!isTransientRenameErrorOnWin32(err) || attempt === RENAME_RETRY_ATTEMPTS) throw err;\n\t\t\tconst backoffMs = Math.min(RENAME_RETRY_MIN_TIMEOUT_MS * 2 ** attempt, RENAME_RETRY_MAX_TIMEOUT_MS);\n\t\t\tblockingSleepMs(backoffMs);\n\t\t}\n\t}\n}\n\n/** Async counterpart of the rename-retry policy; see {@link isTransientRenameErrorOnWin32}. */\nasync function renameWithRetry(tmpPath: string, filePath: string): Promise<void> {\n\tfor (let attempt = 0; attempt <= RENAME_RETRY_ATTEMPTS; attempt++) {\n\t\ttry {\n\t\t\tawait fsPromises.rename(tmpPath, filePath);\n\t\t\treturn;\n\t\t} catch (err) {\n\t\t\tif (!isTransientRenameErrorOnWin32(err) || attempt === RENAME_RETRY_ATTEMPTS) throw err;\n\t\t\tconst backoffMs = Math.min(RENAME_RETRY_MIN_TIMEOUT_MS * 2 ** attempt, RENAME_RETRY_MAX_TIMEOUT_MS);\n\t\t\tawait new Promise((resolve) => setTimeout(resolve, backoffMs));\n\t\t}\n\t}\n}\n\nexport interface AtomicFileLockOptions {\n\t/**\n\t * Bounded retry attempts while waiting for a lock already held elsewhere. Both variants use a\n\t * SHORT, capped backoff (see {@link RETRY_MIN_TIMEOUT_MS}/{@link RETRY_MAX_TIMEOUT_MS}) — these\n\t * stores' critical sections are sub-millisecond reads+writes of small JSON/text files, so\n\t * contention should clear in milliseconds, not the multi-second-to-31-second worst case\n\t * proper-lockfile's OWN default backoff produces for a bare numeric `retries` (its default\n\t * `minTimeout` is 1000ms with factor 2 — see node_modules/retry/lib/retry.js). Passing a bare\n\t * number straight through would turn brief contention into a multi-second stall on a hot path\n\t * (e.g. a per-token-stream perf sample), so both variants instead build an explicit short-backoff\n\t * `retry` options object.\n\t * - Async (`withFileLock`): forwarded as `{retries, minTimeout, maxTimeout}` to proper-lockfile.\n\t * - Sync (`withFileLockSync`): proper-lockfile's sync API REJECTS `retries > 0` outright (it\n\t * requires the whole acquire flow to be synchronous — see proper-lockfile/lib/adapter.js\n\t * `toSyncOptions`, which throws `ESYNC`). So the sync path implements its own bounded retry\n\t * around single `lockfile.lockSync` attempts, blocking briefly between them (Atomics.wait) —\n\t * callers are already fully synchronous fs code, so a short blocking wait on contention matches\n\t * the existing execution model rather than introducing a new one.\n\t */\n\tretries?: number;\n\t/** Resolve symlinks before locking (proper-lockfile `realpath`); false matches file-store.ts. */\n\trealpath?: boolean;\n\t/** Lock staleness window in ms (proper-lockfile `stale`); omitted = proper-lockfile's own default. */\n\tstale?: number;\n}\n\nexport interface AtomicFileWriteOptions {\n\t/** POSIX permission bits applied to the temporary file and inherited by the renamed destination. */\n\tmode?: number;\n}\n\nconst DEFAULT_RETRIES = 10;\nconst DEFAULT_REALPATH = false;\n/**\n * Capped doubling backoff shared by both variants — see {@link AtomicFileLockOptions.retries}.\n * With the current defaults (10 retries, 25ms floor, 500ms cap) the total worst-case contention\n * budget is ~3.3s (25+50+100+200+400+500+500+500+500+500), comfortably over the >= 2s floor this\n * needs to survive a loaded CI runner (e.g. the Windows CI mkdir/rmdir lock-directory churn under\n * two-real-OS-thread contention).\n */\nconst RETRY_MIN_TIMEOUT_MS = 25;\nconst RETRY_MAX_TIMEOUT_MS = 500;\nfunction ensureLockDirSync(filePath: string): void {\n\tmkdirSync(dirname(filePath), { recursive: true });\n}\n\nasync function ensureLockDir(filePath: string): Promise<void> {\n\tawait fsPromises.mkdir(dirname(filePath), { recursive: true });\n}\n\nfunction isLockedError(err: unknown): boolean {\n\tif (typeof err !== \"object\" || err === null) return false;\n\tconst code = (err as { code?: string }).code;\n\tif (code === \"ELOCKED\") return true;\n\t// On win32, mkdir-ing the lock directory can transiently surface EPERM (rather than the\n\t// expected EEXIST) when a previous incarnation of that directory is concurrently being\n\t// rmdir'd by the racing releaser — the mkdir lands mid-teardown and the OS reports \"operation\n\t// not permitted\" instead of \"already exists\". This is contention, not a real permissions\n\t// failure, so treat it identically to ELOCKED: retry with the existing backoff rather than\n\t// letting it escape as fatal. POSIX platforms don't exhibit this transient and keep surfacing\n\t// real EPERM (e.g. an actually unwritable directory) as fatal.\n\tif (code === \"EPERM\" && process.platform === \"win32\") return true;\n\treturn false;\n}\n\n/** Block the calling thread for `ms` without spinning the CPU (Atomics.wait on a private buffer). */\nfunction blockingSleepMs(ms: number): void {\n\tAtomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);\n}\n\n/**\n * Acquire a proper-lockfile sync lock with bounded retry on `ELOCKED` (proper-lockfile's sync API\n * itself forbids `retries > 0`; see {@link AtomicFileLockOptions.retries}).\n *\n * Backoff doubles each attempt (mirroring the `retry` module's own `factor: 2` default that the\n * async path gets for free from proper-lockfile), starting at {@link RETRY_MIN_TIMEOUT_MS} and\n * capped at {@link RETRY_MAX_TIMEOUT_MS}.\n *\n * This is budget PARITY between two implementations of the same lock-acquisition contract, not\n * flake-masking: `options.retries` (a count of RETRIES, i.e. attempts after the first) means the\n * same thing on both paths, so both must make `retries + 1` total acquisition attempts with the\n * same per-gap backoff schedule. The async path gets `retries + 1` attempts for free from the\n * `retry` module (it retries `options.retries` times after its initial attempt); the sync path\n * used to loop exactly `attempts` times total (one fewer gap, and one fewer attempt than the\n * async path for the same `retries` value), which under-budgeted it relative to its async sibling\n * and surfaced as spurious ELOCKED failures under real contention (e.g. two OS threads hammering\n * the same file, or mkdir/rmdir lock-directory churn on a loaded Windows CI runner).\n */\nfunction lockSyncWithRetry(filePath: string, options: AtomicFileLockOptions): () => void {\n\tconst attempts = Math.max(1, options.retries ?? DEFAULT_RETRIES) + 1;\n\tconst lockOptions = { realpath: options.realpath ?? DEFAULT_REALPATH, stale: options.stale };\n\tfor (let attempt = 1; attempt <= attempts; attempt++) {\n\t\ttry {\n\t\t\treturn lockfile.lockSync(filePath, lockOptions);\n\t\t} catch (err) {\n\t\t\tif (!isLockedError(err) || attempt === attempts) throw err;\n\t\t\tconst backoffMs = Math.min(RETRY_MIN_TIMEOUT_MS * 2 ** (attempt - 1), RETRY_MAX_TIMEOUT_MS);\n\t\t\tblockingSleepMs(backoffMs);\n\t\t}\n\t}\n\t// Unreachable (the loop always returns or throws), but keeps the function's return type honest.\n\tthrow new Error(`Failed to acquire lock for ${filePath}`);\n}\n\n/**\n * Hold an exclusive advisory lock on `filePath` for the duration of `fn` (sync). Always releases,\n * including when `fn` throws.\n */\nexport function withFileLockSync<T>(filePath: string, fn: () => T, options?: AtomicFileLockOptions): T {\n\tensureLockDirSync(filePath);\n\tconst release = lockSyncWithRetry(filePath, options ?? {});\n\ttry {\n\t\treturn fn();\n\t} finally {\n\t\t// A lock-cleanup failure must never mask fn()'s result (or replace fn()'s own thrown error) —\n\t\t// by this point fn() has already durably committed or failed on its own terms.\n\t\ttry {\n\t\t\trelease();\n\t\t} catch {\n\t\t\t// best-effort cleanup; a stale lock self-expires via proper-lockfile's `stale` window\n\t\t}\n\t}\n}\n\n/** Async counterpart of {@link withFileLockSync}. Always releases, including when `fn` throws/rejects. */\nexport async function withFileLock<T>(\n\tfilePath: string,\n\tfn: () => Promise<T> | T,\n\toptions?: AtomicFileLockOptions,\n): Promise<T> {\n\tawait ensureLockDir(filePath);\n\tconst release = await lockfile.lock(filePath, {\n\t\trealpath: options?.realpath ?? DEFAULT_REALPATH,\n\t\tretries: {\n\t\t\tretries: options?.retries ?? DEFAULT_RETRIES,\n\t\t\tminTimeout: RETRY_MIN_TIMEOUT_MS,\n\t\t\tmaxTimeout: RETRY_MAX_TIMEOUT_MS,\n\t\t},\n\t\tstale: options?.stale,\n\t});\n\ttry {\n\t\treturn await fn();\n\t} finally {\n\t\t// See {@link withFileLockSync} — cleanup failures must not mask fn()'s outcome.\n\t\tawait release().catch(() => {});\n\t}\n}\n\n/**\n * Write `content` to `filePath` via write-tmp-then-rename (sync); the rename is atomic on the same\n * filesystem, so a concurrent reader never observes a partially-written file. Does NOT itself lock —\n * call from inside {@link withFileLockSync} when the write follows a read that must not race another\n * writer's read+write; call standalone for an unconditional overwrite with no read step.\n */\nfunction temporaryPath(filePath: string): string {\n\treturn `${filePath}.${process.pid}.${randomUUID()}.tmp`;\n}\n\n/** Remove one invocation's temporary path without masking the original write/rename failure. */\nfunction removeTemporaryPathSync(tmpPath: string): void {\n\ttry {\n\t\tunlinkSync(tmpPath);\n\t} catch {\n\t\t// The rename may have consumed the path, or another failure may have prevented its creation.\n\t}\n}\n\nasync function removeTemporaryPath(tmpPath: string): Promise<void> {\n\ttry {\n\t\tawait fsPromises.unlink(tmpPath);\n\t} catch {\n\t\t// The rename may have consumed the path, or another failure may have prevented its creation.\n\t}\n}\n\nexport function writeFileAtomicSync(filePath: string, content: string, options?: AtomicFileWriteOptions): void {\n\tmkdirSync(dirname(filePath), { recursive: true });\n\tconst tmpPath = temporaryPath(filePath);\n\tlet renamed = false;\n\ttry {\n\t\t// `wx` makes a nonce collision harmless: never truncate another writer's temporary file.\n\t\twriteFileSync(tmpPath, content, { encoding: \"utf-8\", flag: \"wx\", mode: options?.mode });\n\t\trenameSyncWithRetry(tmpPath, filePath);\n\t\trenamed = true;\n\t} finally {\n\t\tif (!renamed) removeTemporaryPathSync(tmpPath);\n\t}\n}\n\n/** Async counterpart of {@link writeFileAtomicSync}. */\nexport async function writeFileAtomic(\n\tfilePath: string,\n\tcontent: string,\n\toptions?: AtomicFileWriteOptions,\n): Promise<void> {\n\tawait fsPromises.mkdir(dirname(filePath), { recursive: true });\n\tconst tmpPath = temporaryPath(filePath);\n\tlet renamed = false;\n\ttry {\n\t\t// `wx` makes a nonce collision harmless: never truncate another writer's temporary file.\n\t\tawait fsPromises.writeFile(tmpPath, content, { encoding: \"utf-8\", flag: \"wx\", mode: options?.mode });\n\t\tawait renameWithRetry(tmpPath, filePath);\n\t\trenamed = true;\n\t} finally {\n\t\tif (!renamed) await removeTemporaryPath(tmpPath);\n\t}\n}\n"]}
@@ -221,13 +221,13 @@ async function removeTemporaryPath(tmpPath) {
221
221
  // The rename may have consumed the path, or another failure may have prevented its creation.
222
222
  }
223
223
  }
224
- export function writeFileAtomicSync(filePath, content) {
224
+ export function writeFileAtomicSync(filePath, content, options) {
225
225
  mkdirSync(dirname(filePath), { recursive: true });
226
226
  const tmpPath = temporaryPath(filePath);
227
227
  let renamed = false;
228
228
  try {
229
229
  // `wx` makes a nonce collision harmless: never truncate another writer's temporary file.
230
- writeFileSync(tmpPath, content, { encoding: "utf-8", flag: "wx" });
230
+ writeFileSync(tmpPath, content, { encoding: "utf-8", flag: "wx", mode: options?.mode });
231
231
  renameSyncWithRetry(tmpPath, filePath);
232
232
  renamed = true;
233
233
  }
@@ -237,13 +237,13 @@ export function writeFileAtomicSync(filePath, content) {
237
237
  }
238
238
  }
239
239
  /** Async counterpart of {@link writeFileAtomicSync}. */
240
- export async function writeFileAtomic(filePath, content) {
240
+ export async function writeFileAtomic(filePath, content, options) {
241
241
  await fsPromises.mkdir(dirname(filePath), { recursive: true });
242
242
  const tmpPath = temporaryPath(filePath);
243
243
  let renamed = false;
244
244
  try {
245
245
  // `wx` makes a nonce collision harmless: never truncate another writer's temporary file.
246
- await fsPromises.writeFile(tmpPath, content, { encoding: "utf-8", flag: "wx" });
246
+ await fsPromises.writeFile(tmpPath, content, { encoding: "utf-8", flag: "wx", mode: options?.mode });
247
247
  await renameWithRetry(tmpPath, filePath);
248
248
  renamed = true;
249
249
  }
@@ -1 +1 @@
1
- {"version":3,"file":"atomic-file.js","sourceRoot":"","sources":["../../../src/core/util/atomic-file.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Shared atomic-file helper: a single lock+tmp+rename implementation reused by every\n * on-disk store that does a read-modify-write against a JSON/text file. Modeled on the pattern\n * already proven in `memory/providers/file-store.ts` (proper-lockfile advisory lock + write-tmp-\n * then-rename); before this helper existed the pattern was copy-pasted per store, and most copies\n * were missing either the lock, the atomic write, or both.\n *\n * Two call shapes:\n * - `withFileLock(Sync)` — hold an exclusive advisory lock across an arbitrary read-modify-write\n * callback. The lock spans BOTH the read and the write, closing the classic RMW race where two\n * writers each read the old content before either writes back.\n * - `writeFileAtomic(Sync)` — write-tmp-then-rename. Used INSIDE a `withFileLock` callback (or\n * standalone, for a pure overwrite that has no read step to race).\n *\n * Sync and async variants are both exported. Most existing stores expose a synchronous public API\n * called from hot, non-async paths (e.g. a per-token-stream perf sample), so the sync variant lets\n * them gain locking without forcing an async ripple through their callers; the async variant is for\n * call sites that are already async.\n */\n\nimport { randomUUID } from \"node:crypto\";\nimport { promises as fsPromises, mkdirSync, renameSync, unlinkSync, writeFileSync } from \"node:fs\";\nimport { dirname } from \"node:path\";\nimport lockfile from \"proper-lockfile\";\n\n/**\n * Bounded rename retry budget for the win32-only transient-rename handling below (see\n * {@link isTransientRenameErrorOnWin32}). Backoff doubles each attempt starting at\n * {@link RENAME_RETRY_MIN_TIMEOUT_MS} and capped at {@link RENAME_RETRY_MAX_TIMEOUT_MS}, mirroring\n * the doubling-backoff shape used for lock acquisition above. With {@link RENAME_RETRY_ATTEMPTS}\n * extra attempts (10, 20, 40, 80, ... capped at 200) the worst-case total wait is ~1.03s\n * (10+20+40+80+160+200+200+200+200 ~= 1110ms across 9 gaps), which comfortably covers the\n * millisecond-scale window Defender/the Windows Search indexer hold a freshly-written file open\n * without FILE_SHARE_DELETE before releasing it.\n */\nconst RENAME_RETRY_ATTEMPTS = 9;\nconst RENAME_RETRY_MIN_TIMEOUT_MS = 10;\nconst RENAME_RETRY_MAX_TIMEOUT_MS = 200;\n\n/**\n * On win32, antivirus (e.g. Windows Defender's real-time scanner) and the Windows Search indexer\n * routinely open a freshly-written file for a brief scan without `FILE_SHARE_DELETE`, which makes a\n * rename over/of that file fail transiently with EPERM/EACCES/EBUSY for a few milliseconds. This is\n * a well-documented platform semantic, not a bug in our write path — it's exactly why the\n * `graceful-fs` package ships its own win32 rename-retry wrapper. POSIX platforms never exhibit this\n * transient (a POSIX rename either succeeds or fails for a real, non-transient reason), so retry is\n * gated strictly to win32 to keep POSIX behavior byte-identical to a bare rename.\n */\nfunction isTransientRenameErrorOnWin32(err: unknown): boolean {\n\tif (process.platform !== \"win32\") return false;\n\tif (typeof err !== \"object\" || err === null) return false;\n\tconst code = (err as { code?: string }).code;\n\treturn code === \"EPERM\" || code === \"EACCES\" || code === \"EBUSY\";\n}\n\n/** Sync counterpart of the rename-retry policy; see {@link isTransientRenameErrorOnWin32}. */\nfunction renameSyncWithRetry(tmpPath: string, filePath: string): void {\n\tfor (let attempt = 0; attempt <= RENAME_RETRY_ATTEMPTS; attempt++) {\n\t\ttry {\n\t\t\trenameSync(tmpPath, filePath);\n\t\t\treturn;\n\t\t} catch (err) {\n\t\t\tif (!isTransientRenameErrorOnWin32(err) || attempt === RENAME_RETRY_ATTEMPTS) throw err;\n\t\t\tconst backoffMs = Math.min(RENAME_RETRY_MIN_TIMEOUT_MS * 2 ** attempt, RENAME_RETRY_MAX_TIMEOUT_MS);\n\t\t\tblockingSleepMs(backoffMs);\n\t\t}\n\t}\n}\n\n/** Async counterpart of the rename-retry policy; see {@link isTransientRenameErrorOnWin32}. */\nasync function renameWithRetry(tmpPath: string, filePath: string): Promise<void> {\n\tfor (let attempt = 0; attempt <= RENAME_RETRY_ATTEMPTS; attempt++) {\n\t\ttry {\n\t\t\tawait fsPromises.rename(tmpPath, filePath);\n\t\t\treturn;\n\t\t} catch (err) {\n\t\t\tif (!isTransientRenameErrorOnWin32(err) || attempt === RENAME_RETRY_ATTEMPTS) throw err;\n\t\t\tconst backoffMs = Math.min(RENAME_RETRY_MIN_TIMEOUT_MS * 2 ** attempt, RENAME_RETRY_MAX_TIMEOUT_MS);\n\t\t\tawait new Promise((resolve) => setTimeout(resolve, backoffMs));\n\t\t}\n\t}\n}\n\nexport interface AtomicFileLockOptions {\n\t/**\n\t * Bounded retry attempts while waiting for a lock already held elsewhere. Both variants use a\n\t * SHORT, capped backoff (see {@link RETRY_MIN_TIMEOUT_MS}/{@link RETRY_MAX_TIMEOUT_MS}) — these\n\t * stores' critical sections are sub-millisecond reads+writes of small JSON/text files, so\n\t * contention should clear in milliseconds, not the multi-second-to-31-second worst case\n\t * proper-lockfile's OWN default backoff produces for a bare numeric `retries` (its default\n\t * `minTimeout` is 1000ms with factor 2 — see node_modules/retry/lib/retry.js). Passing a bare\n\t * number straight through would turn brief contention into a multi-second stall on a hot path\n\t * (e.g. a per-token-stream perf sample), so both variants instead build an explicit short-backoff\n\t * `retry` options object.\n\t * - Async (`withFileLock`): forwarded as `{retries, minTimeout, maxTimeout}` to proper-lockfile.\n\t * - Sync (`withFileLockSync`): proper-lockfile's sync API REJECTS `retries > 0` outright (it\n\t * requires the whole acquire flow to be synchronous — see proper-lockfile/lib/adapter.js\n\t * `toSyncOptions`, which throws `ESYNC`). So the sync path implements its own bounded retry\n\t * around single `lockfile.lockSync` attempts, blocking briefly between them (Atomics.wait) —\n\t * callers are already fully synchronous fs code, so a short blocking wait on contention matches\n\t * the existing execution model rather than introducing a new one.\n\t */\n\tretries?: number;\n\t/** Resolve symlinks before locking (proper-lockfile `realpath`); false matches file-store.ts. */\n\trealpath?: boolean;\n\t/** Lock staleness window in ms (proper-lockfile `stale`); omitted = proper-lockfile's own default. */\n\tstale?: number;\n}\n\nconst DEFAULT_RETRIES = 10;\nconst DEFAULT_REALPATH = false;\n/**\n * Capped doubling backoff shared by both variants — see {@link AtomicFileLockOptions.retries}.\n * With the current defaults (10 retries, 25ms floor, 500ms cap) the total worst-case contention\n * budget is ~3.3s (25+50+100+200+400+500+500+500+500+500), comfortably over the >= 2s floor this\n * needs to survive a loaded CI runner (e.g. the Windows CI mkdir/rmdir lock-directory churn under\n * two-real-OS-thread contention).\n */\nconst RETRY_MIN_TIMEOUT_MS = 25;\nconst RETRY_MAX_TIMEOUT_MS = 500;\nfunction ensureLockDirSync(filePath: string): void {\n\tmkdirSync(dirname(filePath), { recursive: true });\n}\n\nasync function ensureLockDir(filePath: string): Promise<void> {\n\tawait fsPromises.mkdir(dirname(filePath), { recursive: true });\n}\n\nfunction isLockedError(err: unknown): boolean {\n\tif (typeof err !== \"object\" || err === null) return false;\n\tconst code = (err as { code?: string }).code;\n\tif (code === \"ELOCKED\") return true;\n\t// On win32, mkdir-ing the lock directory can transiently surface EPERM (rather than the\n\t// expected EEXIST) when a previous incarnation of that directory is concurrently being\n\t// rmdir'd by the racing releaser — the mkdir lands mid-teardown and the OS reports \"operation\n\t// not permitted\" instead of \"already exists\". This is contention, not a real permissions\n\t// failure, so treat it identically to ELOCKED: retry with the existing backoff rather than\n\t// letting it escape as fatal. POSIX platforms don't exhibit this transient and keep surfacing\n\t// real EPERM (e.g. an actually unwritable directory) as fatal.\n\tif (code === \"EPERM\" && process.platform === \"win32\") return true;\n\treturn false;\n}\n\n/** Block the calling thread for `ms` without spinning the CPU (Atomics.wait on a private buffer). */\nfunction blockingSleepMs(ms: number): void {\n\tAtomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);\n}\n\n/**\n * Acquire a proper-lockfile sync lock with bounded retry on `ELOCKED` (proper-lockfile's sync API\n * itself forbids `retries > 0`; see {@link AtomicFileLockOptions.retries}).\n *\n * Backoff doubles each attempt (mirroring the `retry` module's own `factor: 2` default that the\n * async path gets for free from proper-lockfile), starting at {@link RETRY_MIN_TIMEOUT_MS} and\n * capped at {@link RETRY_MAX_TIMEOUT_MS}.\n *\n * This is budget PARITY between two implementations of the same lock-acquisition contract, not\n * flake-masking: `options.retries` (a count of RETRIES, i.e. attempts after the first) means the\n * same thing on both paths, so both must make `retries + 1` total acquisition attempts with the\n * same per-gap backoff schedule. The async path gets `retries + 1` attempts for free from the\n * `retry` module (it retries `options.retries` times after its initial attempt); the sync path\n * used to loop exactly `attempts` times total (one fewer gap, and one fewer attempt than the\n * async path for the same `retries` value), which under-budgeted it relative to its async sibling\n * and surfaced as spurious ELOCKED failures under real contention (e.g. two OS threads hammering\n * the same file, or mkdir/rmdir lock-directory churn on a loaded Windows CI runner).\n */\nfunction lockSyncWithRetry(filePath: string, options: AtomicFileLockOptions): () => void {\n\tconst attempts = Math.max(1, options.retries ?? DEFAULT_RETRIES) + 1;\n\tconst lockOptions = { realpath: options.realpath ?? DEFAULT_REALPATH, stale: options.stale };\n\tfor (let attempt = 1; attempt <= attempts; attempt++) {\n\t\ttry {\n\t\t\treturn lockfile.lockSync(filePath, lockOptions);\n\t\t} catch (err) {\n\t\t\tif (!isLockedError(err) || attempt === attempts) throw err;\n\t\t\tconst backoffMs = Math.min(RETRY_MIN_TIMEOUT_MS * 2 ** (attempt - 1), RETRY_MAX_TIMEOUT_MS);\n\t\t\tblockingSleepMs(backoffMs);\n\t\t}\n\t}\n\t// Unreachable (the loop always returns or throws), but keeps the function's return type honest.\n\tthrow new Error(`Failed to acquire lock for ${filePath}`);\n}\n\n/**\n * Hold an exclusive advisory lock on `filePath` for the duration of `fn` (sync). Always releases,\n * including when `fn` throws.\n */\nexport function withFileLockSync<T>(filePath: string, fn: () => T, options?: AtomicFileLockOptions): T {\n\tensureLockDirSync(filePath);\n\tconst release = lockSyncWithRetry(filePath, options ?? {});\n\ttry {\n\t\treturn fn();\n\t} finally {\n\t\t// A lock-cleanup failure must never mask fn()'s result (or replace fn()'s own thrown error) —\n\t\t// by this point fn() has already durably committed or failed on its own terms.\n\t\ttry {\n\t\t\trelease();\n\t\t} catch {\n\t\t\t// best-effort cleanup; a stale lock self-expires via proper-lockfile's `stale` window\n\t\t}\n\t}\n}\n\n/** Async counterpart of {@link withFileLockSync}. Always releases, including when `fn` throws/rejects. */\nexport async function withFileLock<T>(\n\tfilePath: string,\n\tfn: () => Promise<T> | T,\n\toptions?: AtomicFileLockOptions,\n): Promise<T> {\n\tawait ensureLockDir(filePath);\n\tconst release = await lockfile.lock(filePath, {\n\t\trealpath: options?.realpath ?? DEFAULT_REALPATH,\n\t\tretries: {\n\t\t\tretries: options?.retries ?? DEFAULT_RETRIES,\n\t\t\tminTimeout: RETRY_MIN_TIMEOUT_MS,\n\t\t\tmaxTimeout: RETRY_MAX_TIMEOUT_MS,\n\t\t},\n\t\tstale: options?.stale,\n\t});\n\ttry {\n\t\treturn await fn();\n\t} finally {\n\t\t// See {@link withFileLockSync} — cleanup failures must not mask fn()'s outcome.\n\t\tawait release().catch(() => {});\n\t}\n}\n\n/**\n * Write `content` to `filePath` via write-tmp-then-rename (sync); the rename is atomic on the same\n * filesystem, so a concurrent reader never observes a partially-written file. Does NOT itself lock —\n * call from inside {@link withFileLockSync} when the write follows a read that must not race another\n * writer's read+write; call standalone for an unconditional overwrite with no read step.\n */\nfunction temporaryPath(filePath: string): string {\n\treturn `${filePath}.${process.pid}.${randomUUID()}.tmp`;\n}\n\n/** Remove one invocation's temporary path without masking the original write/rename failure. */\nfunction removeTemporaryPathSync(tmpPath: string): void {\n\ttry {\n\t\tunlinkSync(tmpPath);\n\t} catch {\n\t\t// The rename may have consumed the path, or another failure may have prevented its creation.\n\t}\n}\n\nasync function removeTemporaryPath(tmpPath: string): Promise<void> {\n\ttry {\n\t\tawait fsPromises.unlink(tmpPath);\n\t} catch {\n\t\t// The rename may have consumed the path, or another failure may have prevented its creation.\n\t}\n}\n\nexport function writeFileAtomicSync(filePath: string, content: string): void {\n\tmkdirSync(dirname(filePath), { recursive: true });\n\tconst tmpPath = temporaryPath(filePath);\n\tlet renamed = false;\n\ttry {\n\t\t// `wx` makes a nonce collision harmless: never truncate another writer's temporary file.\n\t\twriteFileSync(tmpPath, content, { encoding: \"utf-8\", flag: \"wx\" });\n\t\trenameSyncWithRetry(tmpPath, filePath);\n\t\trenamed = true;\n\t} finally {\n\t\tif (!renamed) removeTemporaryPathSync(tmpPath);\n\t}\n}\n\n/** Async counterpart of {@link writeFileAtomicSync}. */\nexport async function writeFileAtomic(filePath: string, content: string): Promise<void> {\n\tawait fsPromises.mkdir(dirname(filePath), { recursive: true });\n\tconst tmpPath = temporaryPath(filePath);\n\tlet renamed = false;\n\ttry {\n\t\t// `wx` makes a nonce collision harmless: never truncate another writer's temporary file.\n\t\tawait fsPromises.writeFile(tmpPath, content, { encoding: \"utf-8\", flag: \"wx\" });\n\t\tawait renameWithRetry(tmpPath, filePath);\n\t\trenamed = true;\n\t} finally {\n\t\tif (!renamed) await removeTemporaryPath(tmpPath);\n\t}\n}\n"]}
1
+ {"version":3,"file":"atomic-file.js","sourceRoot":"","sources":["../../../src/core/util/atomic-file.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Shared atomic-file helper: a single lock+tmp+rename implementation reused by every\n * on-disk store that does a read-modify-write against a JSON/text file. Modeled on the pattern\n * already proven in `memory/providers/file-store.ts` (proper-lockfile advisory lock + write-tmp-\n * then-rename); before this helper existed the pattern was copy-pasted per store, and most copies\n * were missing either the lock, the atomic write, or both.\n *\n * Two call shapes:\n * - `withFileLock(Sync)` — hold an exclusive advisory lock across an arbitrary read-modify-write\n * callback. The lock spans BOTH the read and the write, closing the classic RMW race where two\n * writers each read the old content before either writes back.\n * - `writeFileAtomic(Sync)` — write-tmp-then-rename. Used INSIDE a `withFileLock` callback (or\n * standalone, for a pure overwrite that has no read step to race).\n *\n * Sync and async variants are both exported. Most existing stores expose a synchronous public API\n * called from hot, non-async paths (e.g. a per-token-stream perf sample), so the sync variant lets\n * them gain locking without forcing an async ripple through their callers; the async variant is for\n * call sites that are already async.\n */\n\nimport { randomUUID } from \"node:crypto\";\nimport { promises as fsPromises, mkdirSync, renameSync, unlinkSync, writeFileSync } from \"node:fs\";\nimport { dirname } from \"node:path\";\nimport lockfile from \"proper-lockfile\";\n\n/**\n * Bounded rename retry budget for the win32-only transient-rename handling below (see\n * {@link isTransientRenameErrorOnWin32}). Backoff doubles each attempt starting at\n * {@link RENAME_RETRY_MIN_TIMEOUT_MS} and capped at {@link RENAME_RETRY_MAX_TIMEOUT_MS}, mirroring\n * the doubling-backoff shape used for lock acquisition above. With {@link RENAME_RETRY_ATTEMPTS}\n * extra attempts (10, 20, 40, 80, ... capped at 200) the worst-case total wait is ~1.03s\n * (10+20+40+80+160+200+200+200+200 ~= 1110ms across 9 gaps), which comfortably covers the\n * millisecond-scale window Defender/the Windows Search indexer hold a freshly-written file open\n * without FILE_SHARE_DELETE before releasing it.\n */\nconst RENAME_RETRY_ATTEMPTS = 9;\nconst RENAME_RETRY_MIN_TIMEOUT_MS = 10;\nconst RENAME_RETRY_MAX_TIMEOUT_MS = 200;\n\n/**\n * On win32, antivirus (e.g. Windows Defender's real-time scanner) and the Windows Search indexer\n * routinely open a freshly-written file for a brief scan without `FILE_SHARE_DELETE`, which makes a\n * rename over/of that file fail transiently with EPERM/EACCES/EBUSY for a few milliseconds. This is\n * a well-documented platform semantic, not a bug in our write path — it's exactly why the\n * `graceful-fs` package ships its own win32 rename-retry wrapper. POSIX platforms never exhibit this\n * transient (a POSIX rename either succeeds or fails for a real, non-transient reason), so retry is\n * gated strictly to win32 to keep POSIX behavior byte-identical to a bare rename.\n */\nfunction isTransientRenameErrorOnWin32(err: unknown): boolean {\n\tif (process.platform !== \"win32\") return false;\n\tif (typeof err !== \"object\" || err === null) return false;\n\tconst code = (err as { code?: string }).code;\n\treturn code === \"EPERM\" || code === \"EACCES\" || code === \"EBUSY\";\n}\n\n/** Sync counterpart of the rename-retry policy; see {@link isTransientRenameErrorOnWin32}. */\nfunction renameSyncWithRetry(tmpPath: string, filePath: string): void {\n\tfor (let attempt = 0; attempt <= RENAME_RETRY_ATTEMPTS; attempt++) {\n\t\ttry {\n\t\t\trenameSync(tmpPath, filePath);\n\t\t\treturn;\n\t\t} catch (err) {\n\t\t\tif (!isTransientRenameErrorOnWin32(err) || attempt === RENAME_RETRY_ATTEMPTS) throw err;\n\t\t\tconst backoffMs = Math.min(RENAME_RETRY_MIN_TIMEOUT_MS * 2 ** attempt, RENAME_RETRY_MAX_TIMEOUT_MS);\n\t\t\tblockingSleepMs(backoffMs);\n\t\t}\n\t}\n}\n\n/** Async counterpart of the rename-retry policy; see {@link isTransientRenameErrorOnWin32}. */\nasync function renameWithRetry(tmpPath: string, filePath: string): Promise<void> {\n\tfor (let attempt = 0; attempt <= RENAME_RETRY_ATTEMPTS; attempt++) {\n\t\ttry {\n\t\t\tawait fsPromises.rename(tmpPath, filePath);\n\t\t\treturn;\n\t\t} catch (err) {\n\t\t\tif (!isTransientRenameErrorOnWin32(err) || attempt === RENAME_RETRY_ATTEMPTS) throw err;\n\t\t\tconst backoffMs = Math.min(RENAME_RETRY_MIN_TIMEOUT_MS * 2 ** attempt, RENAME_RETRY_MAX_TIMEOUT_MS);\n\t\t\tawait new Promise((resolve) => setTimeout(resolve, backoffMs));\n\t\t}\n\t}\n}\n\nexport interface AtomicFileLockOptions {\n\t/**\n\t * Bounded retry attempts while waiting for a lock already held elsewhere. Both variants use a\n\t * SHORT, capped backoff (see {@link RETRY_MIN_TIMEOUT_MS}/{@link RETRY_MAX_TIMEOUT_MS}) — these\n\t * stores' critical sections are sub-millisecond reads+writes of small JSON/text files, so\n\t * contention should clear in milliseconds, not the multi-second-to-31-second worst case\n\t * proper-lockfile's OWN default backoff produces for a bare numeric `retries` (its default\n\t * `minTimeout` is 1000ms with factor 2 — see node_modules/retry/lib/retry.js). Passing a bare\n\t * number straight through would turn brief contention into a multi-second stall on a hot path\n\t * (e.g. a per-token-stream perf sample), so both variants instead build an explicit short-backoff\n\t * `retry` options object.\n\t * - Async (`withFileLock`): forwarded as `{retries, minTimeout, maxTimeout}` to proper-lockfile.\n\t * - Sync (`withFileLockSync`): proper-lockfile's sync API REJECTS `retries > 0` outright (it\n\t * requires the whole acquire flow to be synchronous — see proper-lockfile/lib/adapter.js\n\t * `toSyncOptions`, which throws `ESYNC`). So the sync path implements its own bounded retry\n\t * around single `lockfile.lockSync` attempts, blocking briefly between them (Atomics.wait) —\n\t * callers are already fully synchronous fs code, so a short blocking wait on contention matches\n\t * the existing execution model rather than introducing a new one.\n\t */\n\tretries?: number;\n\t/** Resolve symlinks before locking (proper-lockfile `realpath`); false matches file-store.ts. */\n\trealpath?: boolean;\n\t/** Lock staleness window in ms (proper-lockfile `stale`); omitted = proper-lockfile's own default. */\n\tstale?: number;\n}\n\nexport interface AtomicFileWriteOptions {\n\t/** POSIX permission bits applied to the temporary file and inherited by the renamed destination. */\n\tmode?: number;\n}\n\nconst DEFAULT_RETRIES = 10;\nconst DEFAULT_REALPATH = false;\n/**\n * Capped doubling backoff shared by both variants — see {@link AtomicFileLockOptions.retries}.\n * With the current defaults (10 retries, 25ms floor, 500ms cap) the total worst-case contention\n * budget is ~3.3s (25+50+100+200+400+500+500+500+500+500), comfortably over the >= 2s floor this\n * needs to survive a loaded CI runner (e.g. the Windows CI mkdir/rmdir lock-directory churn under\n * two-real-OS-thread contention).\n */\nconst RETRY_MIN_TIMEOUT_MS = 25;\nconst RETRY_MAX_TIMEOUT_MS = 500;\nfunction ensureLockDirSync(filePath: string): void {\n\tmkdirSync(dirname(filePath), { recursive: true });\n}\n\nasync function ensureLockDir(filePath: string): Promise<void> {\n\tawait fsPromises.mkdir(dirname(filePath), { recursive: true });\n}\n\nfunction isLockedError(err: unknown): boolean {\n\tif (typeof err !== \"object\" || err === null) return false;\n\tconst code = (err as { code?: string }).code;\n\tif (code === \"ELOCKED\") return true;\n\t// On win32, mkdir-ing the lock directory can transiently surface EPERM (rather than the\n\t// expected EEXIST) when a previous incarnation of that directory is concurrently being\n\t// rmdir'd by the racing releaser — the mkdir lands mid-teardown and the OS reports \"operation\n\t// not permitted\" instead of \"already exists\". This is contention, not a real permissions\n\t// failure, so treat it identically to ELOCKED: retry with the existing backoff rather than\n\t// letting it escape as fatal. POSIX platforms don't exhibit this transient and keep surfacing\n\t// real EPERM (e.g. an actually unwritable directory) as fatal.\n\tif (code === \"EPERM\" && process.platform === \"win32\") return true;\n\treturn false;\n}\n\n/** Block the calling thread for `ms` without spinning the CPU (Atomics.wait on a private buffer). */\nfunction blockingSleepMs(ms: number): void {\n\tAtomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);\n}\n\n/**\n * Acquire a proper-lockfile sync lock with bounded retry on `ELOCKED` (proper-lockfile's sync API\n * itself forbids `retries > 0`; see {@link AtomicFileLockOptions.retries}).\n *\n * Backoff doubles each attempt (mirroring the `retry` module's own `factor: 2` default that the\n * async path gets for free from proper-lockfile), starting at {@link RETRY_MIN_TIMEOUT_MS} and\n * capped at {@link RETRY_MAX_TIMEOUT_MS}.\n *\n * This is budget PARITY between two implementations of the same lock-acquisition contract, not\n * flake-masking: `options.retries` (a count of RETRIES, i.e. attempts after the first) means the\n * same thing on both paths, so both must make `retries + 1` total acquisition attempts with the\n * same per-gap backoff schedule. The async path gets `retries + 1` attempts for free from the\n * `retry` module (it retries `options.retries` times after its initial attempt); the sync path\n * used to loop exactly `attempts` times total (one fewer gap, and one fewer attempt than the\n * async path for the same `retries` value), which under-budgeted it relative to its async sibling\n * and surfaced as spurious ELOCKED failures under real contention (e.g. two OS threads hammering\n * the same file, or mkdir/rmdir lock-directory churn on a loaded Windows CI runner).\n */\nfunction lockSyncWithRetry(filePath: string, options: AtomicFileLockOptions): () => void {\n\tconst attempts = Math.max(1, options.retries ?? DEFAULT_RETRIES) + 1;\n\tconst lockOptions = { realpath: options.realpath ?? DEFAULT_REALPATH, stale: options.stale };\n\tfor (let attempt = 1; attempt <= attempts; attempt++) {\n\t\ttry {\n\t\t\treturn lockfile.lockSync(filePath, lockOptions);\n\t\t} catch (err) {\n\t\t\tif (!isLockedError(err) || attempt === attempts) throw err;\n\t\t\tconst backoffMs = Math.min(RETRY_MIN_TIMEOUT_MS * 2 ** (attempt - 1), RETRY_MAX_TIMEOUT_MS);\n\t\t\tblockingSleepMs(backoffMs);\n\t\t}\n\t}\n\t// Unreachable (the loop always returns or throws), but keeps the function's return type honest.\n\tthrow new Error(`Failed to acquire lock for ${filePath}`);\n}\n\n/**\n * Hold an exclusive advisory lock on `filePath` for the duration of `fn` (sync). Always releases,\n * including when `fn` throws.\n */\nexport function withFileLockSync<T>(filePath: string, fn: () => T, options?: AtomicFileLockOptions): T {\n\tensureLockDirSync(filePath);\n\tconst release = lockSyncWithRetry(filePath, options ?? {});\n\ttry {\n\t\treturn fn();\n\t} finally {\n\t\t// A lock-cleanup failure must never mask fn()'s result (or replace fn()'s own thrown error) —\n\t\t// by this point fn() has already durably committed or failed on its own terms.\n\t\ttry {\n\t\t\trelease();\n\t\t} catch {\n\t\t\t// best-effort cleanup; a stale lock self-expires via proper-lockfile's `stale` window\n\t\t}\n\t}\n}\n\n/** Async counterpart of {@link withFileLockSync}. Always releases, including when `fn` throws/rejects. */\nexport async function withFileLock<T>(\n\tfilePath: string,\n\tfn: () => Promise<T> | T,\n\toptions?: AtomicFileLockOptions,\n): Promise<T> {\n\tawait ensureLockDir(filePath);\n\tconst release = await lockfile.lock(filePath, {\n\t\trealpath: options?.realpath ?? DEFAULT_REALPATH,\n\t\tretries: {\n\t\t\tretries: options?.retries ?? DEFAULT_RETRIES,\n\t\t\tminTimeout: RETRY_MIN_TIMEOUT_MS,\n\t\t\tmaxTimeout: RETRY_MAX_TIMEOUT_MS,\n\t\t},\n\t\tstale: options?.stale,\n\t});\n\ttry {\n\t\treturn await fn();\n\t} finally {\n\t\t// See {@link withFileLockSync} — cleanup failures must not mask fn()'s outcome.\n\t\tawait release().catch(() => {});\n\t}\n}\n\n/**\n * Write `content` to `filePath` via write-tmp-then-rename (sync); the rename is atomic on the same\n * filesystem, so a concurrent reader never observes a partially-written file. Does NOT itself lock —\n * call from inside {@link withFileLockSync} when the write follows a read that must not race another\n * writer's read+write; call standalone for an unconditional overwrite with no read step.\n */\nfunction temporaryPath(filePath: string): string {\n\treturn `${filePath}.${process.pid}.${randomUUID()}.tmp`;\n}\n\n/** Remove one invocation's temporary path without masking the original write/rename failure. */\nfunction removeTemporaryPathSync(tmpPath: string): void {\n\ttry {\n\t\tunlinkSync(tmpPath);\n\t} catch {\n\t\t// The rename may have consumed the path, or another failure may have prevented its creation.\n\t}\n}\n\nasync function removeTemporaryPath(tmpPath: string): Promise<void> {\n\ttry {\n\t\tawait fsPromises.unlink(tmpPath);\n\t} catch {\n\t\t// The rename may have consumed the path, or another failure may have prevented its creation.\n\t}\n}\n\nexport function writeFileAtomicSync(filePath: string, content: string, options?: AtomicFileWriteOptions): void {\n\tmkdirSync(dirname(filePath), { recursive: true });\n\tconst tmpPath = temporaryPath(filePath);\n\tlet renamed = false;\n\ttry {\n\t\t// `wx` makes a nonce collision harmless: never truncate another writer's temporary file.\n\t\twriteFileSync(tmpPath, content, { encoding: \"utf-8\", flag: \"wx\", mode: options?.mode });\n\t\trenameSyncWithRetry(tmpPath, filePath);\n\t\trenamed = true;\n\t} finally {\n\t\tif (!renamed) removeTemporaryPathSync(tmpPath);\n\t}\n}\n\n/** Async counterpart of {@link writeFileAtomicSync}. */\nexport async function writeFileAtomic(\n\tfilePath: string,\n\tcontent: string,\n\toptions?: AtomicFileWriteOptions,\n): Promise<void> {\n\tawait fsPromises.mkdir(dirname(filePath), { recursive: true });\n\tconst tmpPath = temporaryPath(filePath);\n\tlet renamed = false;\n\ttry {\n\t\t// `wx` makes a nonce collision harmless: never truncate another writer's temporary file.\n\t\tawait fsPromises.writeFile(tmpPath, content, { encoding: \"utf-8\", flag: \"wx\", mode: options?.mode });\n\t\tawait renameWithRetry(tmpPath, filePath);\n\t\trenamed = true;\n\t} finally {\n\t\tif (!renamed) await removeTemporaryPath(tmpPath);\n\t}\n}\n"]}
@@ -0,0 +1,10 @@
1
+ export interface BoundedJsonlLimits {
2
+ maxBytes: number;
3
+ targetBytes: number;
4
+ maxRecords: number;
5
+ }
6
+ /** Append one JSON record and rotate to the newest low-water tail under one exclusive lock. */
7
+ export declare function appendBoundedJsonLineSync(filePath: string, value: unknown, limits: BoundedJsonlLimits): void;
8
+ /** Async counterpart of {@link appendBoundedJsonLineSync}. */
9
+ export declare function appendBoundedJsonLine(filePath: string, value: unknown, limits: BoundedJsonlLimits): Promise<void>;
10
+ //# sourceMappingURL=bounded-jsonl.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bounded-jsonl.d.ts","sourceRoot":"","sources":["../../../src/core/util/bounded-jsonl.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,kBAAkB;IAClC,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;CACnB;AAyCD,+FAA+F;AAC/F,wBAAgB,yBAAyB,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,kBAAkB,GAAG,IAAI,CAQ5G;AAED,8DAA8D;AAC9D,wBAAsB,qBAAqB,CAC1C,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,OAAO,EACd,MAAM,EAAE,kBAAkB,GACxB,OAAO,CAAC,IAAI,CAAC,CAmBf","sourcesContent":["import { appendFileSync, promises as fsPromises, readFileSync, statSync } from \"node:fs\";\nimport { withFileLock, withFileLockSync, writeFileAtomic, writeFileAtomicSync } from \"./atomic-file.ts\";\n\nexport interface BoundedJsonlLimits {\n\tmaxBytes: number;\n\ttargetBytes: number;\n\tmaxRecords: number;\n}\n\nconst asyncAppendTails = new Map<string, Promise<void>>();\n\nfunction validateLimits(limits: BoundedJsonlLimits): void {\n\tif (\n\t\t!Number.isSafeInteger(limits.maxBytes) ||\n\t\t!Number.isSafeInteger(limits.targetBytes) ||\n\t\t!Number.isSafeInteger(limits.maxRecords) ||\n\t\tlimits.maxBytes <= 0 ||\n\t\tlimits.targetBytes <= 0 ||\n\t\tlimits.targetBytes > limits.maxBytes ||\n\t\tlimits.maxRecords <= 0\n\t) {\n\t\tthrow new TypeError(\"Bounded JSONL limits require positive integers and targetBytes <= maxBytes.\");\n\t}\n}\n\nfunction retainedContent(content: string, limits: BoundedJsonlLimits): string {\n\tconst lines = content.split(\"\\n\").filter((line) => line.trim().length > 0);\n\tconst retained: string[] = [];\n\tlet retainedBytes = 0;\n\tfor (let index = lines.length - 1; index >= 0 && retained.length < limits.maxRecords; index--) {\n\t\tconst line = lines[index]!;\n\t\tconst lineBytes = Buffer.byteLength(line, \"utf-8\") + 1;\n\t\tif (lineBytes > limits.maxBytes) continue;\n\t\tif (retained.length > 0 && retainedBytes + lineBytes > limits.targetBytes) break;\n\t\tretained.push(line);\n\t\tretainedBytes += lineBytes;\n\t\tif (retainedBytes >= limits.targetBytes) break;\n\t}\n\tretained.reverse();\n\treturn retained.length > 0 ? `${retained.join(\"\\n\")}\\n` : \"\";\n}\n\nfunction serializeLine(value: unknown): string {\n\tconst encoded = JSON.stringify(value);\n\tif (encoded === undefined) throw new TypeError(\"Bounded JSONL records must be JSON-serializable values.\");\n\treturn `${encoded}\\n`;\n}\n\n/** Append one JSON record and rotate to the newest low-water tail under one exclusive lock. */\nexport function appendBoundedJsonLineSync(filePath: string, value: unknown, limits: BoundedJsonlLimits): void {\n\tvalidateLimits(limits);\n\tconst line = serializeLine(value);\n\twithFileLockSync(filePath, () => {\n\t\tappendFileSync(filePath, line, \"utf-8\");\n\t\tif (statSync(filePath).size <= limits.maxBytes) return;\n\t\twriteFileAtomicSync(filePath, retainedContent(readFileSync(filePath, \"utf-8\"), limits));\n\t});\n}\n\n/** Async counterpart of {@link appendBoundedJsonLineSync}. */\nexport async function appendBoundedJsonLine(\n\tfilePath: string,\n\tvalue: unknown,\n\tlimits: BoundedJsonlLimits,\n): Promise<void> {\n\tvalidateLimits(limits);\n\tconst line = serializeLine(value);\n\tconst previous = asyncAppendTails.get(filePath) ?? Promise.resolve();\n\tconst operation = previous\n\t\t.catch(() => undefined)\n\t\t.then(async () => {\n\t\t\tawait withFileLock(filePath, async () => {\n\t\t\t\tawait fsPromises.appendFile(filePath, line, \"utf-8\");\n\t\t\t\tif ((await fsPromises.stat(filePath)).size <= limits.maxBytes) return;\n\t\t\t\tawait writeFileAtomic(filePath, retainedContent(await fsPromises.readFile(filePath, \"utf-8\"), limits));\n\t\t\t});\n\t\t});\n\tasyncAppendTails.set(filePath, operation);\n\ttry {\n\t\tawait operation;\n\t} finally {\n\t\tif (asyncAppendTails.get(filePath) === operation) asyncAppendTails.delete(filePath);\n\t}\n}\n"]}
@@ -0,0 +1,75 @@
1
+ import { appendFileSync, promises as fsPromises, readFileSync, statSync } from "node:fs";
2
+ import { withFileLock, withFileLockSync, writeFileAtomic, writeFileAtomicSync } from "./atomic-file.js";
3
+ const asyncAppendTails = new Map();
4
+ function validateLimits(limits) {
5
+ if (!Number.isSafeInteger(limits.maxBytes) ||
6
+ !Number.isSafeInteger(limits.targetBytes) ||
7
+ !Number.isSafeInteger(limits.maxRecords) ||
8
+ limits.maxBytes <= 0 ||
9
+ limits.targetBytes <= 0 ||
10
+ limits.targetBytes > limits.maxBytes ||
11
+ limits.maxRecords <= 0) {
12
+ throw new TypeError("Bounded JSONL limits require positive integers and targetBytes <= maxBytes.");
13
+ }
14
+ }
15
+ function retainedContent(content, limits) {
16
+ const lines = content.split("\n").filter((line) => line.trim().length > 0);
17
+ const retained = [];
18
+ let retainedBytes = 0;
19
+ for (let index = lines.length - 1; index >= 0 && retained.length < limits.maxRecords; index--) {
20
+ const line = lines[index];
21
+ const lineBytes = Buffer.byteLength(line, "utf-8") + 1;
22
+ if (lineBytes > limits.maxBytes)
23
+ continue;
24
+ if (retained.length > 0 && retainedBytes + lineBytes > limits.targetBytes)
25
+ break;
26
+ retained.push(line);
27
+ retainedBytes += lineBytes;
28
+ if (retainedBytes >= limits.targetBytes)
29
+ break;
30
+ }
31
+ retained.reverse();
32
+ return retained.length > 0 ? `${retained.join("\n")}\n` : "";
33
+ }
34
+ function serializeLine(value) {
35
+ const encoded = JSON.stringify(value);
36
+ if (encoded === undefined)
37
+ throw new TypeError("Bounded JSONL records must be JSON-serializable values.");
38
+ return `${encoded}\n`;
39
+ }
40
+ /** Append one JSON record and rotate to the newest low-water tail under one exclusive lock. */
41
+ export function appendBoundedJsonLineSync(filePath, value, limits) {
42
+ validateLimits(limits);
43
+ const line = serializeLine(value);
44
+ withFileLockSync(filePath, () => {
45
+ appendFileSync(filePath, line, "utf-8");
46
+ if (statSync(filePath).size <= limits.maxBytes)
47
+ return;
48
+ writeFileAtomicSync(filePath, retainedContent(readFileSync(filePath, "utf-8"), limits));
49
+ });
50
+ }
51
+ /** Async counterpart of {@link appendBoundedJsonLineSync}. */
52
+ export async function appendBoundedJsonLine(filePath, value, limits) {
53
+ validateLimits(limits);
54
+ const line = serializeLine(value);
55
+ const previous = asyncAppendTails.get(filePath) ?? Promise.resolve();
56
+ const operation = previous
57
+ .catch(() => undefined)
58
+ .then(async () => {
59
+ await withFileLock(filePath, async () => {
60
+ await fsPromises.appendFile(filePath, line, "utf-8");
61
+ if ((await fsPromises.stat(filePath)).size <= limits.maxBytes)
62
+ return;
63
+ await writeFileAtomic(filePath, retainedContent(await fsPromises.readFile(filePath, "utf-8"), limits));
64
+ });
65
+ });
66
+ asyncAppendTails.set(filePath, operation);
67
+ try {
68
+ await operation;
69
+ }
70
+ finally {
71
+ if (asyncAppendTails.get(filePath) === operation)
72
+ asyncAppendTails.delete(filePath);
73
+ }
74
+ }
75
+ //# sourceMappingURL=bounded-jsonl.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bounded-jsonl.js","sourceRoot":"","sources":["../../../src/core/util/bounded-jsonl.ts"],"names":[],"mappings":"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","sourcesContent":["import { appendFileSync, promises as fsPromises, readFileSync, statSync } from \"node:fs\";\nimport { withFileLock, withFileLockSync, writeFileAtomic, writeFileAtomicSync } from \"./atomic-file.ts\";\n\nexport interface BoundedJsonlLimits {\n\tmaxBytes: number;\n\ttargetBytes: number;\n\tmaxRecords: number;\n}\n\nconst asyncAppendTails = new Map<string, Promise<void>>();\n\nfunction validateLimits(limits: BoundedJsonlLimits): void {\n\tif (\n\t\t!Number.isSafeInteger(limits.maxBytes) ||\n\t\t!Number.isSafeInteger(limits.targetBytes) ||\n\t\t!Number.isSafeInteger(limits.maxRecords) ||\n\t\tlimits.maxBytes <= 0 ||\n\t\tlimits.targetBytes <= 0 ||\n\t\tlimits.targetBytes > limits.maxBytes ||\n\t\tlimits.maxRecords <= 0\n\t) {\n\t\tthrow new TypeError(\"Bounded JSONL limits require positive integers and targetBytes <= maxBytes.\");\n\t}\n}\n\nfunction retainedContent(content: string, limits: BoundedJsonlLimits): string {\n\tconst lines = content.split(\"\\n\").filter((line) => line.trim().length > 0);\n\tconst retained: string[] = [];\n\tlet retainedBytes = 0;\n\tfor (let index = lines.length - 1; index >= 0 && retained.length < limits.maxRecords; index--) {\n\t\tconst line = lines[index]!;\n\t\tconst lineBytes = Buffer.byteLength(line, \"utf-8\") + 1;\n\t\tif (lineBytes > limits.maxBytes) continue;\n\t\tif (retained.length > 0 && retainedBytes + lineBytes > limits.targetBytes) break;\n\t\tretained.push(line);\n\t\tretainedBytes += lineBytes;\n\t\tif (retainedBytes >= limits.targetBytes) break;\n\t}\n\tretained.reverse();\n\treturn retained.length > 0 ? `${retained.join(\"\\n\")}\\n` : \"\";\n}\n\nfunction serializeLine(value: unknown): string {\n\tconst encoded = JSON.stringify(value);\n\tif (encoded === undefined) throw new TypeError(\"Bounded JSONL records must be JSON-serializable values.\");\n\treturn `${encoded}\\n`;\n}\n\n/** Append one JSON record and rotate to the newest low-water tail under one exclusive lock. */\nexport function appendBoundedJsonLineSync(filePath: string, value: unknown, limits: BoundedJsonlLimits): void {\n\tvalidateLimits(limits);\n\tconst line = serializeLine(value);\n\twithFileLockSync(filePath, () => {\n\t\tappendFileSync(filePath, line, \"utf-8\");\n\t\tif (statSync(filePath).size <= limits.maxBytes) return;\n\t\twriteFileAtomicSync(filePath, retainedContent(readFileSync(filePath, \"utf-8\"), limits));\n\t});\n}\n\n/** Async counterpart of {@link appendBoundedJsonLineSync}. */\nexport async function appendBoundedJsonLine(\n\tfilePath: string,\n\tvalue: unknown,\n\tlimits: BoundedJsonlLimits,\n): Promise<void> {\n\tvalidateLimits(limits);\n\tconst line = serializeLine(value);\n\tconst previous = asyncAppendTails.get(filePath) ?? Promise.resolve();\n\tconst operation = previous\n\t\t.catch(() => undefined)\n\t\t.then(async () => {\n\t\t\tawait withFileLock(filePath, async () => {\n\t\t\t\tawait fsPromises.appendFile(filePath, line, \"utf-8\");\n\t\t\t\tif ((await fsPromises.stat(filePath)).size <= limits.maxBytes) return;\n\t\t\t\tawait writeFileAtomic(filePath, retainedContent(await fsPromises.readFile(filePath, \"utf-8\"), limits));\n\t\t\t});\n\t\t});\n\tasyncAppendTails.set(filePath, operation);\n\ttry {\n\t\tawait operation;\n\t} finally {\n\t\tif (asyncAppendTails.get(filePath) === operation) asyncAppendTails.delete(filePath);\n\t}\n}\n"]}
@@ -0,0 +1,5 @@
1
+ /** Accepts non-null object records while excluding arrays; custom prototypes remain valid. */
2
+ export declare function isRecordObject(value: unknown): value is Record<string, unknown>;
3
+ /** Accepts only ordinary JSON-style records, including null-prototype dictionaries. */
4
+ export declare function isPlainRecord(value: unknown): value is Record<string, unknown>;
5
+ //# sourceMappingURL=value-guards.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"value-guards.d.ts","sourceRoot":"","sources":["../../../src/core/util/value-guards.ts"],"names":[],"mappings":"AAAA,8FAA8F;AAC9F,wBAAgB,cAAc,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAE/E;AAED,uFAAuF;AACvF,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAI9E","sourcesContent":["/** Accepts non-null object records while excluding arrays; custom prototypes remain valid. */\nexport function isRecordObject(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\n/** Accepts only ordinary JSON-style records, including null-prototype dictionaries. */\nexport function isPlainRecord(value: unknown): value is Record<string, unknown> {\n\tif (!isRecordObject(value)) return false;\n\tconst prototype = Object.getPrototypeOf(value);\n\treturn prototype === Object.prototype || prototype === null;\n}\n"]}
@@ -0,0 +1,12 @@
1
+ /** Accepts non-null object records while excluding arrays; custom prototypes remain valid. */
2
+ export function isRecordObject(value) {
3
+ return typeof value === "object" && value !== null && !Array.isArray(value);
4
+ }
5
+ /** Accepts only ordinary JSON-style records, including null-prototype dictionaries. */
6
+ export function isPlainRecord(value) {
7
+ if (!isRecordObject(value))
8
+ return false;
9
+ const prototype = Object.getPrototypeOf(value);
10
+ return prototype === Object.prototype || prototype === null;
11
+ }
12
+ //# sourceMappingURL=value-guards.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"value-guards.js","sourceRoot":"","sources":["../../../src/core/util/value-guards.ts"],"names":[],"mappings":"AAAA,8FAA8F;AAC9F,MAAM,UAAU,cAAc,CAAC,KAAc,EAAoC;IAChF,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;AAAA,CAC5E;AAED,uFAAuF;AACvF,MAAM,UAAU,aAAa,CAAC,KAAc,EAAoC;IAC/E,IAAI,CAAC,cAAc,CAAC,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC;IACzC,MAAM,SAAS,GAAG,MAAM,CAAC,cAAc,CAAC,KAAK,CAAC,CAAC;IAC/C,OAAO,SAAS,KAAK,MAAM,CAAC,SAAS,IAAI,SAAS,KAAK,IAAI,CAAC;AAAA,CAC5D","sourcesContent":["/** Accepts non-null object records while excluding arrays; custom prototypes remain valid. */\nexport function isRecordObject(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\n/** Accepts only ordinary JSON-style records, including null-prototype dictionaries. */\nexport function isPlainRecord(value: unknown): value is Record<string, unknown> {\n\tif (!isRecordObject(value)) return false;\n\tconst prototype = Object.getPrototypeOf(value);\n\treturn prototype === Object.prototype || prototype === null;\n}\n"]}
@@ -9,9 +9,9 @@
9
9
  * git by the engine (`git-engine.ts`). A deleted or corrupt store is rebuildable via reconcile --
10
10
  * every reader here treats missing/corrupt files as "absent", never as an error.
11
11
  *
12
- * Writes are tmp+rename atomic (`util/atomic-file.ts`); the audit log is append-only single-line
13
- * JSON (O_APPEND, one line per event -- atomic enough for the small records involved). The
14
- * integration lock is a mkdir-atomic directory with an owner manifest: mkdir either succeeds
12
+ * Writes are tmp+rename atomic (`util/atomic-file.ts`); the audit log uses the shared bounded JSONL
13
+ * sink, which serializes concurrent writers and atomically rotates to its newest low-water tail.
14
+ * The integration lock is a mkdir-atomic directory with an owner manifest: mkdir either succeeds
15
15
  * (lock acquired) or fails EEXIST (held), with takeover ONLY for a provably-dead same-host owner.
16
16
  */
17
17
  import type { IntegrationLockAcquisition, IntegrationLockOwner, LandingTransaction, LaneRegistration, WorktreeSyncEpoch } from "./codes.ts";
@@ -1 +1 @@
1
- {"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../../src/core/worktree-sync/store.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Worktree-sync on-disk store: coordination state shared by EVERY process working on one repo.\n *\n * Lives under `<git-common-dir>/pi-worktree-sync/` -- all worktrees of a repository share the git\n * common dir, so the store is repo-scoped and cross-process by construction, survives `/reload`\n * and crashes, is invisible to `git status`, and needs no gitignore entry.\n *\n * The store is an INDEX, not the truth: freshness/dirtiness/progress are always re-derived from\n * git by the engine (`git-engine.ts`). A deleted or corrupt store is rebuildable via reconcile --\n * every reader here treats missing/corrupt files as \"absent\", never as an error.\n *\n * Writes are tmp+rename atomic (`util/atomic-file.ts`); the audit log is append-only single-line\n * JSON (O_APPEND, one line per event -- atomic enough for the small records involved). The\n * integration lock is a mkdir-atomic directory with an owner manifest: mkdir either succeeds\n * (lock acquired) or fails EEXIST (held), with takeover ONLY for a provably-dead same-host owner.\n */\n\nimport { createHash, randomUUID } from \"node:crypto\";\nimport { promises as fsPromises, realpathSync } from \"node:fs\";\nimport { hostname as osHostname } from \"node:os\";\nimport { basename, dirname, join } from \"node:path\";\nimport { withFileLock, writeFileAtomic } from \"../util/atomic-file.ts\";\nimport type {\n\tIntegrationLockAcquisition,\n\tIntegrationLockOwner,\n\tLandingTransaction,\n\tLaneRegistration,\n\tWorktreeSyncEpoch,\n} from \"./codes.ts\";\n\nexport interface SyncStorePaths {\n\troot: string;\n\tepochFile: string;\n\tlandingTransactionFile: string;\n\tlanesDir: string;\n\tlockDir: string;\n\tlockOwnerFile: string;\n\tlockGuardFile: string;\n\tlifecycleLockFile: string;\n\teventsFile: string;\n}\n\n/** Grace window after which an ownerless lock dir (crash between mkdir and owner write) counts as\n * dead and may be taken over. Fresh ownerless dirs are conservatively treated as held. */\nexport const OWNERLESS_LOCK_STALE_MS = 30_000;\n\nconst STORE_DIR_NAME = \"pi-worktree-sync\";\n\nexport function syncStorePaths(gitCommonDir: string): SyncStorePaths {\n\tconst root = join(gitCommonDir, STORE_DIR_NAME);\n\treturn {\n\t\troot,\n\t\tepochFile: join(root, \"epoch.json\"),\n\t\tlandingTransactionFile: join(root, \"landing-transaction.json\"),\n\t\tlanesDir: join(root, \"lanes\"),\n\t\tlockDir: join(root, \"locks\", \"integration\"),\n\t\tlockOwnerFile: join(root, \"locks\", \"integration\", \"owner.json\"),\n\t\tlockGuardFile: join(root, \"locks\", \"integration-metadata\"),\n\t\tlifecycleLockFile: join(root, \"locks\", \"lifecycle\"),\n\t\teventsFile: join(root, \"events.jsonl\"),\n\t};\n}\n\n/**\n * Stable per-repo slug for the lane-worktree checkout root: sanitized repo basename plus 8 hex of\n * sha256 over the REALPATH of the git common dir -- same-named repos in different places cannot\n * collide, and every worktree of one repo resolves the same slug.\n */\nexport function repoSlug(repoTopLevel: string, gitCommonDir: string): string {\n\tlet canonical = gitCommonDir;\n\ttry {\n\t\tcanonical = realpathSync(gitCommonDir);\n\t} catch {\n\t\t// Unresolvable (already-deleted repo during teardown): hash the raw path deterministically.\n\t}\n\tconst hash = createHash(\"sha256\").update(canonical).digest(\"hex\").slice(0, 8);\n\tconst base = basename(repoTopLevel)\n\t\t.toLowerCase()\n\t\t.replace(/[^a-z0-9-]+/g, \"-\")\n\t\t.replace(/^-+|-+$/g, \"\")\n\t\t.slice(0, 48);\n\treturn `${base || \"repo\"}-${hash}`;\n}\n\nasync function readJsonFile<T>(filePath: string): Promise<T | undefined> {\n\tlet raw: string;\n\ttry {\n\t\traw = await fsPromises.readFile(filePath, \"utf-8\");\n\t} catch {\n\t\treturn undefined;\n\t}\n\ttry {\n\t\treturn JSON.parse(raw) as T;\n\t} catch {\n\t\t// Corrupt store files are treated as absent -- git remains the truth and reconcile rebuilds.\n\t\treturn undefined;\n\t}\n}\n\nexport async function readEpoch(paths: SyncStorePaths): Promise<WorktreeSyncEpoch | undefined> {\n\treturn readJsonFile<WorktreeSyncEpoch>(paths.epochFile);\n}\n\nexport async function writeEpoch(paths: SyncStorePaths, epoch: WorktreeSyncEpoch): Promise<void> {\n\tawait writeFileAtomic(paths.epochFile, `${JSON.stringify(epoch, null, \"\\t\")}\\n`);\n}\n\nexport async function readLandingTransaction(paths: SyncStorePaths): Promise<LandingTransaction | undefined> {\n\tconst value = await readJsonFile<unknown>(paths.landingTransactionFile);\n\tif (typeof value !== \"object\" || value === null || Array.isArray(value)) return undefined;\n\tconst record = value as Record<string, unknown>;\n\tif (\n\t\ttypeof record.laneKey !== \"string\" ||\n\t\ttypeof record.priorMainSha !== \"string\" ||\n\t\ttypeof record.testedTipSha !== \"string\" ||\n\t\t!Array.isArray(record.changedPaths) ||\n\t\t!record.changedPaths.every((path) => typeof path === \"string\") ||\n\t\ttypeof record.changedPathsTruncated !== \"boolean\" ||\n\t\ttypeof record.lockToken !== \"string\" ||\n\t\t(record.gate !== \"passed\" && record.gate !== \"off\") ||\n\t\t(record.stage !== \"ready_to_merge\" &&\n\t\t\trecord.stage !== \"main_moved\" &&\n\t\t\trecord.stage !== \"epoch_written\" &&\n\t\t\trecord.stage !== \"audit_logged\")\n\t) {\n\t\treturn undefined;\n\t}\n\treturn record as unknown as LandingTransaction;\n}\n\nexport async function writeLandingTransaction(paths: SyncStorePaths, transaction: LandingTransaction): Promise<void> {\n\tawait writeFileAtomic(paths.landingTransactionFile, `${JSON.stringify(transaction, null, \"\\t\")}\\n`);\n}\n\nexport async function clearLandingTransaction(paths: SyncStorePaths): Promise<void> {\n\ttry {\n\t\tawait fsPromises.unlink(paths.landingTransactionFile);\n\t} catch (error) {\n\t\tif ((error as NodeJS.ErrnoException).code !== \"ENOENT\") throw error;\n\t}\n}\n\nfunction laneFile(paths: SyncStorePaths, laneKey: string): string {\n\treturn join(paths.lanesDir, `${laneKey}.json`);\n}\n\nexport async function readLane(paths: SyncStorePaths, laneKey: string): Promise<LaneRegistration | undefined> {\n\treturn readJsonFile<LaneRegistration>(laneFile(paths, laneKey));\n}\n\nexport async function writeLane(paths: SyncStorePaths, lane: LaneRegistration): Promise<void> {\n\tawait writeFileAtomic(laneFile(paths, lane.laneKey), `${JSON.stringify(lane, null, \"\\t\")}\\n`);\n}\n\nexport async function listLanes(paths: SyncStorePaths): Promise<LaneRegistration[]> {\n\tlet entries: string[];\n\ttry {\n\t\tentries = await fsPromises.readdir(paths.lanesDir);\n\t} catch {\n\t\treturn [];\n\t}\n\tconst lanes: LaneRegistration[] = [];\n\tfor (const entry of entries.sort()) {\n\t\tif (!entry.endsWith(\".json\")) continue;\n\t\tconst lane = await readJsonFile<LaneRegistration>(join(paths.lanesDir, entry));\n\t\tif (lane?.laneKey) lanes.push(lane);\n\t}\n\treturn lanes;\n}\n\n/** Append one audit event as a single JSON line. `at` is stamped here so every event carries it. */\nexport async function appendAuditEvent(\n\tpaths: SyncStorePaths,\n\tevent: { event: string } & Record<string, unknown>,\n\tat: string,\n): Promise<void> {\n\tawait fsPromises.mkdir(dirname(paths.eventsFile), { recursive: true });\n\tawait fsPromises.appendFile(paths.eventsFile, `${JSON.stringify({ at, ...event })}\\n`, \"utf-8\");\n}\n\nexport interface IntegrationLockDeps {\n\t/** Injectable liveness probe (deterministic tests). Default: same-host `process.kill(pid, 0)`. */\n\tisPidAlive?: (owner: IntegrationLockOwner) => boolean;\n\tnow?: () => string;\n\tnowMs?: () => number;\n}\n\n/** Same-host pid liveness. A foreign-host owner is ALWAYS treated as alive -- never taken over. */\nexport function defaultIsPidAlive(owner: IntegrationLockOwner): boolean {\n\tif (owner.hostname !== osHostname()) return true;\n\ttry {\n\t\tprocess.kill(owner.pid, 0);\n\t\treturn true;\n\t} catch (err) {\n\t\t// EPERM means the pid exists but belongs to another user -- alive. Only ESRCH is dead.\n\t\treturn (err as NodeJS.ErrnoException).code === \"EPERM\";\n\t}\n}\n\nasync function readLockOwner(paths: SyncStorePaths): Promise<IntegrationLockOwner | undefined> {\n\treturn readJsonFile<IntegrationLockOwner>(paths.lockOwnerFile);\n}\n\nasync function tryMkdirLock(paths: SyncStorePaths): Promise<boolean> {\n\ttry {\n\t\tawait fsPromises.mkdir(paths.lockDir, { recursive: false });\n\t\treturn true;\n\t} catch (err) {\n\t\tif ((err as NodeJS.ErrnoException).code === \"ENOENT\") {\n\t\t\t// Parent `locks/` missing on first use: create it, then retry the ATOMIC non-recursive mkdir.\n\t\t\tawait fsPromises.mkdir(dirname(paths.lockDir), { recursive: true });\n\t\t\ttry {\n\t\t\t\tawait fsPromises.mkdir(paths.lockDir, { recursive: false });\n\t\t\t\treturn true;\n\t\t\t} catch {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\t\treturn false;\n\t}\n}\n\n/**\n * Acquire the single integration lock (G1: landing is serialized). Non-blocking: a held lock\n * returns `{acquired:false, holder, holderAlive}` immediately -- callers surface `lock_busy` and\n * retry on their own cadence rather than spinning here. Takeover happens ONLY when the recorded\n * owner is provably dead on this host (or the lock dir is ownerless and older than\n * {@link OWNERLESS_LOCK_STALE_MS}); every takeover is audited.\n */\nexport async function acquireIntegrationLock(\n\tpaths: SyncStorePaths,\n\towner: Omit<IntegrationLockOwner, \"acquiredAt\" | \"token\">,\n\tdeps: IntegrationLockDeps = {},\n): Promise<IntegrationLockAcquisition> {\n\treturn withFileLock(paths.lockGuardFile, async () => {\n\t\tconst now = deps.now ?? (() => new Date().toISOString());\n\t\tconst nowMs = deps.nowMs ?? (() => Date.now());\n\t\tconst isPidAlive = deps.isPidAlive ?? defaultIsPidAlive;\n\t\tfor (let attempt = 0; attempt < 2; attempt++) {\n\t\t\tif (await tryMkdirLock(paths)) {\n\t\t\t\tconst token = randomUUID();\n\t\t\t\tconst manifest: IntegrationLockOwner = { ...owner, token, acquiredAt: now() };\n\t\t\t\tawait fsPromises.writeFile(paths.lockOwnerFile, `${JSON.stringify(manifest)}\\n`, \"utf-8\");\n\t\t\t\tawait appendAuditEvent(\n\t\t\t\t\tpaths,\n\t\t\t\t\t{ event: \"lock_acquired\", ...(attempt > 0 ? { takeover: true } : {}) },\n\t\t\t\t\tnow(),\n\t\t\t\t);\n\t\t\t\treturn { acquired: true, takeover: attempt > 0, token };\n\t\t\t}\n\n\t\t\tconst holder = await readLockOwner(paths);\n\t\t\tif (holder) {\n\t\t\t\tif (isPidAlive(holder)) return { acquired: false, holder, holderAlive: true };\n\t\t\t} else {\n\t\t\t\t// Ownerless dir: a crash between mkdir and the owner write. Only stale ones are dead.\n\t\t\t\tlet dirMtimeMs: number | undefined;\n\t\t\t\ttry {\n\t\t\t\t\tdirMtimeMs = (await fsPromises.stat(paths.lockDir)).mtimeMs;\n\t\t\t\t} catch {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tif (nowMs() - dirMtimeMs < OWNERLESS_LOCK_STALE_MS) return { acquired: false, holderAlive: true };\n\t\t\t}\n\n\t\t\t// Metadata serialization makes the stale observation and directory removal one guarded\n\t\t\t// operation; a successor cannot be removed by a stale observer.\n\t\t\tawait fsPromises.rm(paths.lockDir, { recursive: true, force: true });\n\t\t\tawait appendAuditEvent(\n\t\t\t\tpaths,\n\t\t\t\t{\n\t\t\t\t\tevent: \"lock_takeover\",\n\t\t\t\t\t...(holder ? { deadOwnerPid: holder.pid, deadOwnerLaneKey: holder.laneKey } : {}),\n\t\t\t\t},\n\t\t\t\tnow(),\n\t\t\t);\n\t\t}\n\t\tconst holder = await readLockOwner(paths);\n\t\treturn { acquired: false, holder, holderAlive: holder ? isPidAlive(holder) : true };\n\t});\n}\n\nexport async function releaseIntegrationLock(\n\tpaths: SyncStorePaths,\n\ttoken: string,\n\tdeps: IntegrationLockDeps = {},\n): Promise<boolean> {\n\treturn withFileLock(paths.lockGuardFile, async () => {\n\t\tconst now = deps.now ?? (() => new Date().toISOString());\n\t\tconst holder = await readLockOwner(paths);\n\t\tif (!holder || holder.token !== token) return false;\n\t\tawait fsPromises.rm(paths.lockDir, { recursive: true, force: true });\n\t\tawait appendAuditEvent(paths, { event: \"lock_released\" }, now());\n\t\treturn true;\n\t});\n}\n\nexport async function readLockHolder(paths: SyncStorePaths): Promise<IntegrationLockOwner | undefined> {\n\treturn readLockOwner(paths);\n}\n"]}
1
+ {"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../../src/core/worktree-sync/store.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Worktree-sync on-disk store: coordination state shared by EVERY process working on one repo.\n *\n * Lives under `<git-common-dir>/pi-worktree-sync/` -- all worktrees of a repository share the git\n * common dir, so the store is repo-scoped and cross-process by construction, survives `/reload`\n * and crashes, is invisible to `git status`, and needs no gitignore entry.\n *\n * The store is an INDEX, not the truth: freshness/dirtiness/progress are always re-derived from\n * git by the engine (`git-engine.ts`). A deleted or corrupt store is rebuildable via reconcile --\n * every reader here treats missing/corrupt files as \"absent\", never as an error.\n *\n * Writes are tmp+rename atomic (`util/atomic-file.ts`); the audit log uses the shared bounded JSONL\n * sink, which serializes concurrent writers and atomically rotates to its newest low-water tail.\n * The integration lock is a mkdir-atomic directory with an owner manifest: mkdir either succeeds\n * (lock acquired) or fails EEXIST (held), with takeover ONLY for a provably-dead same-host owner.\n */\n\nimport { createHash, randomUUID } from \"node:crypto\";\nimport { promises as fsPromises, realpathSync } from \"node:fs\";\nimport { hostname as osHostname } from \"node:os\";\nimport { basename, dirname, join } from \"node:path\";\nimport { withFileLock, writeFileAtomic } from \"../util/atomic-file.ts\";\nimport { appendBoundedJsonLine, type BoundedJsonlLimits } from \"../util/bounded-jsonl.ts\";\nimport type {\n\tIntegrationLockAcquisition,\n\tIntegrationLockOwner,\n\tLandingTransaction,\n\tLaneRegistration,\n\tWorktreeSyncEpoch,\n} from \"./codes.ts\";\n\nexport interface SyncStorePaths {\n\troot: string;\n\tepochFile: string;\n\tlandingTransactionFile: string;\n\tlanesDir: string;\n\tlockDir: string;\n\tlockOwnerFile: string;\n\tlockGuardFile: string;\n\tlifecycleLockFile: string;\n\teventsFile: string;\n}\n\n/** Grace window after which an ownerless lock dir (crash between mkdir and owner write) counts as\n * dead and may be taken over. Fresh ownerless dirs are conservatively treated as held. */\nexport const OWNERLESS_LOCK_STALE_MS = 30_000;\n\nconst STORE_DIR_NAME = \"pi-worktree-sync\";\nconst WORKTREE_AUDIT_EVENT_LIMITS: BoundedJsonlLimits = {\n\tmaxBytes: 4 * 1024 * 1024,\n\ttargetBytes: Math.floor(4 * 1024 * 1024 * 0.75),\n\tmaxRecords: 10_000,\n};\n\nexport function syncStorePaths(gitCommonDir: string): SyncStorePaths {\n\tconst root = join(gitCommonDir, STORE_DIR_NAME);\n\treturn {\n\t\troot,\n\t\tepochFile: join(root, \"epoch.json\"),\n\t\tlandingTransactionFile: join(root, \"landing-transaction.json\"),\n\t\tlanesDir: join(root, \"lanes\"),\n\t\tlockDir: join(root, \"locks\", \"integration\"),\n\t\tlockOwnerFile: join(root, \"locks\", \"integration\", \"owner.json\"),\n\t\tlockGuardFile: join(root, \"locks\", \"integration-metadata\"),\n\t\tlifecycleLockFile: join(root, \"locks\", \"lifecycle\"),\n\t\teventsFile: join(root, \"events.jsonl\"),\n\t};\n}\n\n/**\n * Stable per-repo slug for the lane-worktree checkout root: sanitized repo basename plus 8 hex of\n * sha256 over the REALPATH of the git common dir -- same-named repos in different places cannot\n * collide, and every worktree of one repo resolves the same slug.\n */\nexport function repoSlug(repoTopLevel: string, gitCommonDir: string): string {\n\tlet canonical = gitCommonDir;\n\ttry {\n\t\tcanonical = realpathSync(gitCommonDir);\n\t} catch {\n\t\t// Unresolvable (already-deleted repo during teardown): hash the raw path deterministically.\n\t}\n\tconst hash = createHash(\"sha256\").update(canonical).digest(\"hex\").slice(0, 8);\n\tconst base = basename(repoTopLevel)\n\t\t.toLowerCase()\n\t\t.replace(/[^a-z0-9-]+/g, \"-\")\n\t\t.replace(/^-+|-+$/g, \"\")\n\t\t.slice(0, 48);\n\treturn `${base || \"repo\"}-${hash}`;\n}\n\nasync function readJsonFile<T>(filePath: string): Promise<T | undefined> {\n\tlet raw: string;\n\ttry {\n\t\traw = await fsPromises.readFile(filePath, \"utf-8\");\n\t} catch {\n\t\treturn undefined;\n\t}\n\ttry {\n\t\treturn JSON.parse(raw) as T;\n\t} catch {\n\t\t// Corrupt store files are treated as absent -- git remains the truth and reconcile rebuilds.\n\t\treturn undefined;\n\t}\n}\n\nexport async function readEpoch(paths: SyncStorePaths): Promise<WorktreeSyncEpoch | undefined> {\n\treturn readJsonFile<WorktreeSyncEpoch>(paths.epochFile);\n}\n\nexport async function writeEpoch(paths: SyncStorePaths, epoch: WorktreeSyncEpoch): Promise<void> {\n\tawait writeFileAtomic(paths.epochFile, `${JSON.stringify(epoch, null, \"\\t\")}\\n`);\n}\n\nexport async function readLandingTransaction(paths: SyncStorePaths): Promise<LandingTransaction | undefined> {\n\tconst value = await readJsonFile<unknown>(paths.landingTransactionFile);\n\tif (typeof value !== \"object\" || value === null || Array.isArray(value)) return undefined;\n\tconst record = value as Record<string, unknown>;\n\tif (\n\t\ttypeof record.laneKey !== \"string\" ||\n\t\ttypeof record.priorMainSha !== \"string\" ||\n\t\ttypeof record.testedTipSha !== \"string\" ||\n\t\t!Array.isArray(record.changedPaths) ||\n\t\t!record.changedPaths.every((path) => typeof path === \"string\") ||\n\t\ttypeof record.changedPathsTruncated !== \"boolean\" ||\n\t\ttypeof record.lockToken !== \"string\" ||\n\t\t(record.gate !== \"passed\" && record.gate !== \"off\") ||\n\t\t(record.stage !== \"ready_to_merge\" &&\n\t\t\trecord.stage !== \"main_moved\" &&\n\t\t\trecord.stage !== \"epoch_written\" &&\n\t\t\trecord.stage !== \"audit_logged\")\n\t) {\n\t\treturn undefined;\n\t}\n\treturn record as unknown as LandingTransaction;\n}\n\nexport async function writeLandingTransaction(paths: SyncStorePaths, transaction: LandingTransaction): Promise<void> {\n\tawait writeFileAtomic(paths.landingTransactionFile, `${JSON.stringify(transaction, null, \"\\t\")}\\n`);\n}\n\nexport async function clearLandingTransaction(paths: SyncStorePaths): Promise<void> {\n\ttry {\n\t\tawait fsPromises.unlink(paths.landingTransactionFile);\n\t} catch (error) {\n\t\tif ((error as NodeJS.ErrnoException).code !== \"ENOENT\") throw error;\n\t}\n}\n\nfunction laneFile(paths: SyncStorePaths, laneKey: string): string {\n\treturn join(paths.lanesDir, `${laneKey}.json`);\n}\n\nexport async function readLane(paths: SyncStorePaths, laneKey: string): Promise<LaneRegistration | undefined> {\n\treturn readJsonFile<LaneRegistration>(laneFile(paths, laneKey));\n}\n\nexport async function writeLane(paths: SyncStorePaths, lane: LaneRegistration): Promise<void> {\n\tawait writeFileAtomic(laneFile(paths, lane.laneKey), `${JSON.stringify(lane, null, \"\\t\")}\\n`);\n}\n\nexport async function listLanes(paths: SyncStorePaths): Promise<LaneRegistration[]> {\n\tlet entries: string[];\n\ttry {\n\t\tentries = await fsPromises.readdir(paths.lanesDir);\n\t} catch {\n\t\treturn [];\n\t}\n\tconst lanes: LaneRegistration[] = [];\n\tfor (const entry of entries.sort()) {\n\t\tif (!entry.endsWith(\".json\")) continue;\n\t\tconst lane = await readJsonFile<LaneRegistration>(join(paths.lanesDir, entry));\n\t\tif (lane?.laneKey) lanes.push(lane);\n\t}\n\treturn lanes;\n}\n\n/** Append one audit event as a single JSON line. `at` is stamped here so every event carries it. */\nexport async function appendAuditEvent(\n\tpaths: SyncStorePaths,\n\tevent: { event: string } & Record<string, unknown>,\n\tat: string,\n): Promise<void> {\n\tawait appendBoundedJsonLine(paths.eventsFile, { at, ...event }, WORKTREE_AUDIT_EVENT_LIMITS);\n}\n\nexport interface IntegrationLockDeps {\n\t/** Injectable liveness probe (deterministic tests). Default: same-host `process.kill(pid, 0)`. */\n\tisPidAlive?: (owner: IntegrationLockOwner) => boolean;\n\tnow?: () => string;\n\tnowMs?: () => number;\n}\n\n/** Same-host pid liveness. A foreign-host owner is ALWAYS treated as alive -- never taken over. */\nexport function defaultIsPidAlive(owner: IntegrationLockOwner): boolean {\n\tif (owner.hostname !== osHostname()) return true;\n\ttry {\n\t\tprocess.kill(owner.pid, 0);\n\t\treturn true;\n\t} catch (err) {\n\t\t// EPERM means the pid exists but belongs to another user -- alive. Only ESRCH is dead.\n\t\treturn (err as NodeJS.ErrnoException).code === \"EPERM\";\n\t}\n}\n\nasync function readLockOwner(paths: SyncStorePaths): Promise<IntegrationLockOwner | undefined> {\n\treturn readJsonFile<IntegrationLockOwner>(paths.lockOwnerFile);\n}\n\nasync function tryMkdirLock(paths: SyncStorePaths): Promise<boolean> {\n\ttry {\n\t\tawait fsPromises.mkdir(paths.lockDir, { recursive: false });\n\t\treturn true;\n\t} catch (err) {\n\t\tif ((err as NodeJS.ErrnoException).code === \"ENOENT\") {\n\t\t\t// Parent `locks/` missing on first use: create it, then retry the ATOMIC non-recursive mkdir.\n\t\t\tawait fsPromises.mkdir(dirname(paths.lockDir), { recursive: true });\n\t\t\ttry {\n\t\t\t\tawait fsPromises.mkdir(paths.lockDir, { recursive: false });\n\t\t\t\treturn true;\n\t\t\t} catch {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\t\treturn false;\n\t}\n}\n\n/**\n * Acquire the single integration lock (G1: landing is serialized). Non-blocking: a held lock\n * returns `{acquired:false, holder, holderAlive}` immediately -- callers surface `lock_busy` and\n * retry on their own cadence rather than spinning here. Takeover happens ONLY when the recorded\n * owner is provably dead on this host (or the lock dir is ownerless and older than\n * {@link OWNERLESS_LOCK_STALE_MS}); every takeover is audited.\n */\nexport async function acquireIntegrationLock(\n\tpaths: SyncStorePaths,\n\towner: Omit<IntegrationLockOwner, \"acquiredAt\" | \"token\">,\n\tdeps: IntegrationLockDeps = {},\n): Promise<IntegrationLockAcquisition> {\n\treturn withFileLock(paths.lockGuardFile, async () => {\n\t\tconst now = deps.now ?? (() => new Date().toISOString());\n\t\tconst nowMs = deps.nowMs ?? (() => Date.now());\n\t\tconst isPidAlive = deps.isPidAlive ?? defaultIsPidAlive;\n\t\tfor (let attempt = 0; attempt < 2; attempt++) {\n\t\t\tif (await tryMkdirLock(paths)) {\n\t\t\t\tconst token = randomUUID();\n\t\t\t\tconst manifest: IntegrationLockOwner = { ...owner, token, acquiredAt: now() };\n\t\t\t\tawait fsPromises.writeFile(paths.lockOwnerFile, `${JSON.stringify(manifest)}\\n`, \"utf-8\");\n\t\t\t\tawait appendAuditEvent(\n\t\t\t\t\tpaths,\n\t\t\t\t\t{ event: \"lock_acquired\", ...(attempt > 0 ? { takeover: true } : {}) },\n\t\t\t\t\tnow(),\n\t\t\t\t);\n\t\t\t\treturn { acquired: true, takeover: attempt > 0, token };\n\t\t\t}\n\n\t\t\tconst holder = await readLockOwner(paths);\n\t\t\tif (holder) {\n\t\t\t\tif (isPidAlive(holder)) return { acquired: false, holder, holderAlive: true };\n\t\t\t} else {\n\t\t\t\t// Ownerless dir: a crash between mkdir and the owner write. Only stale ones are dead.\n\t\t\t\tlet dirMtimeMs: number | undefined;\n\t\t\t\ttry {\n\t\t\t\t\tdirMtimeMs = (await fsPromises.stat(paths.lockDir)).mtimeMs;\n\t\t\t\t} catch {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tif (nowMs() - dirMtimeMs < OWNERLESS_LOCK_STALE_MS) return { acquired: false, holderAlive: true };\n\t\t\t}\n\n\t\t\t// Metadata serialization makes the stale observation and directory removal one guarded\n\t\t\t// operation; a successor cannot be removed by a stale observer.\n\t\t\tawait fsPromises.rm(paths.lockDir, { recursive: true, force: true });\n\t\t\tawait appendAuditEvent(\n\t\t\t\tpaths,\n\t\t\t\t{\n\t\t\t\t\tevent: \"lock_takeover\",\n\t\t\t\t\t...(holder ? { deadOwnerPid: holder.pid, deadOwnerLaneKey: holder.laneKey } : {}),\n\t\t\t\t},\n\t\t\t\tnow(),\n\t\t\t);\n\t\t}\n\t\tconst holder = await readLockOwner(paths);\n\t\treturn { acquired: false, holder, holderAlive: holder ? isPidAlive(holder) : true };\n\t});\n}\n\nexport async function releaseIntegrationLock(\n\tpaths: SyncStorePaths,\n\ttoken: string,\n\tdeps: IntegrationLockDeps = {},\n): Promise<boolean> {\n\treturn withFileLock(paths.lockGuardFile, async () => {\n\t\tconst now = deps.now ?? (() => new Date().toISOString());\n\t\tconst holder = await readLockOwner(paths);\n\t\tif (!holder || holder.token !== token) return false;\n\t\tawait fsPromises.rm(paths.lockDir, { recursive: true, force: true });\n\t\tawait appendAuditEvent(paths, { event: \"lock_released\" }, now());\n\t\treturn true;\n\t});\n}\n\nexport async function readLockHolder(paths: SyncStorePaths): Promise<IntegrationLockOwner | undefined> {\n\treturn readLockOwner(paths);\n}\n"]}