@caupulican/pi-adaptative 0.81.32 → 0.81.37

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 (339) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/dist/bundled-resources/extensions/tmux-agent-manager/README.md +69 -0
  3. package/dist/bundled-resources/extensions/tmux-agent-manager/index.d.ts +53 -0
  4. package/dist/bundled-resources/extensions/tmux-agent-manager/index.d.ts.map +1 -0
  5. package/dist/bundled-resources/extensions/tmux-agent-manager/index.js +1282 -0
  6. package/dist/bundled-resources/extensions/tmux-agent-manager/index.js.map +1 -0
  7. package/dist/bundled-resources/extensions/tmux-agent-manager/index.ts +1453 -0
  8. package/dist/bundled-resources/extensions/tmux-agent-manager/templates/builder-validator.json +14 -0
  9. package/dist/bundled-resources/extensions/tmux-agent-manager/templates/claude-trio.json +32 -0
  10. package/dist/bundled-resources/extensions/tmux-agent-manager/templates/full-provider-review.json +15 -0
  11. package/dist/bundled-resources/extensions/tmux-agent-manager/templates/provider-prompt-smoke.json +14 -0
  12. package/dist/bundled-resources/prompts/extensionify.md +5 -4
  13. package/dist/bundled-resources/prompts/skillify.md +5 -5
  14. package/dist/bundled-resources/skills/pi-harness-learning/SKILL.md +6 -3
  15. package/dist/cli/args.d.ts.map +1 -1
  16. package/dist/cli/args.js +10 -8
  17. package/dist/cli/args.js.map +1 -1
  18. package/dist/config.d.ts +5 -1
  19. package/dist/config.d.ts.map +1 -1
  20. package/dist/config.js +10 -2
  21. package/dist/config.js.map +1 -1
  22. package/dist/core/agent-session.d.ts +12 -0
  23. package/dist/core/agent-session.d.ts.map +1 -1
  24. package/dist/core/agent-session.js +42 -2
  25. package/dist/core/agent-session.js.map +1 -1
  26. package/dist/core/autonomy/approval-gate.d.ts.map +1 -1
  27. package/dist/core/autonomy/approval-gate.js +2 -0
  28. package/dist/core/autonomy/approval-gate.js.map +1 -1
  29. package/dist/core/autonomy/foreground-envelope.d.ts.map +1 -1
  30. package/dist/core/autonomy/foreground-envelope.js +2 -0
  31. package/dist/core/autonomy/foreground-envelope.js.map +1 -1
  32. package/dist/core/autonomy/gates.d.ts.map +1 -1
  33. package/dist/core/autonomy/gates.js +9 -2
  34. package/dist/core/autonomy/gates.js.map +1 -1
  35. package/dist/core/autonomy/risk-assessment.d.ts.map +1 -1
  36. package/dist/core/autonomy/risk-assessment.js +2 -0
  37. package/dist/core/autonomy/risk-assessment.js.map +1 -1
  38. package/dist/core/background-lane-controller.d.ts +12 -3
  39. package/dist/core/background-lane-controller.d.ts.map +1 -1
  40. package/dist/core/background-lane-controller.js +67 -26
  41. package/dist/core/background-lane-controller.js.map +1 -1
  42. package/dist/core/bash-execution-controller.d.ts +9 -4
  43. package/dist/core/bash-execution-controller.d.ts.map +1 -1
  44. package/dist/core/bash-execution-controller.js +7 -6
  45. package/dist/core/bash-execution-controller.js.map +1 -1
  46. package/dist/core/bash-executor.d.ts +2 -0
  47. package/dist/core/bash-executor.d.ts.map +1 -1
  48. package/dist/core/bash-executor.js +10 -5
  49. package/dist/core/bash-executor.js.map +1 -1
  50. package/dist/core/context/context-store-retention.d.ts +4 -1
  51. package/dist/core/context/context-store-retention.d.ts.map +1 -1
  52. package/dist/core/context/context-store-retention.js +42 -151
  53. package/dist/core/context/context-store-retention.js.map +1 -1
  54. package/dist/core/context-pipeline.d.ts.map +1 -1
  55. package/dist/core/context-pipeline.js +6 -6
  56. package/dist/core/context-pipeline.js.map +1 -1
  57. package/dist/core/default-tool-surface.d.ts +7 -0
  58. package/dist/core/default-tool-surface.d.ts.map +1 -1
  59. package/dist/core/default-tool-surface.js +29 -10
  60. package/dist/core/default-tool-surface.js.map +1 -1
  61. package/dist/core/doctor.d.ts +10 -10
  62. package/dist/core/doctor.d.ts.map +1 -1
  63. package/dist/core/doctor.js +30 -20
  64. package/dist/core/doctor.js.map +1 -1
  65. package/dist/core/exec.d.ts +2 -0
  66. package/dist/core/exec.d.ts.map +1 -1
  67. package/dist/core/exec.js +26 -39
  68. package/dist/core/exec.js.map +1 -1
  69. package/dist/core/extensions/loader.d.ts +9 -2
  70. package/dist/core/extensions/loader.d.ts.map +1 -1
  71. package/dist/core/extensions/loader.js +105 -21
  72. package/dist/core/extensions/loader.js.map +1 -1
  73. package/dist/core/extensions/types.d.ts +13 -3
  74. package/dist/core/extensions/types.d.ts.map +1 -1
  75. package/dist/core/extensions/types.js +3 -0
  76. package/dist/core/extensions/types.js.map +1 -1
  77. package/dist/core/footer-data-provider.d.ts.map +1 -1
  78. package/dist/core/footer-data-provider.js +2 -0
  79. package/dist/core/footer-data-provider.js.map +1 -1
  80. package/dist/core/goals/goal-state.d.ts +10 -0
  81. package/dist/core/goals/goal-state.d.ts.map +1 -1
  82. package/dist/core/goals/goal-state.js +37 -0
  83. package/dist/core/goals/goal-state.js.map +1 -1
  84. package/dist/core/goals/goal-tool-core.d.ts +11 -0
  85. package/dist/core/goals/goal-tool-core.d.ts.map +1 -1
  86. package/dist/core/goals/goal-tool-core.js +51 -1
  87. package/dist/core/goals/goal-tool-core.js.map +1 -1
  88. package/dist/core/improvement-loop.d.ts.map +1 -1
  89. package/dist/core/improvement-loop.js +26 -5
  90. package/dist/core/improvement-loop.js.map +1 -1
  91. package/dist/core/memory/memory-manager.d.ts.map +1 -1
  92. package/dist/core/memory/memory-manager.js +1 -0
  93. package/dist/core/memory/memory-manager.js.map +1 -1
  94. package/dist/core/model-capability.d.ts.map +1 -1
  95. package/dist/core/model-capability.js +2 -0
  96. package/dist/core/model-capability.js.map +1 -1
  97. package/dist/core/model-router/tool-escalation.d.ts.map +1 -1
  98. package/dist/core/model-router/tool-escalation.js +17 -3
  99. package/dist/core/model-router/tool-escalation.js.map +1 -1
  100. package/dist/core/models/fitness-store.d.ts.map +1 -1
  101. package/dist/core/models/fitness-store.js +1 -1
  102. package/dist/core/models/fitness-store.js.map +1 -1
  103. package/dist/core/models/local-runtime.d.ts +6 -4
  104. package/dist/core/models/local-runtime.d.ts.map +1 -1
  105. package/dist/core/models/local-runtime.js +108 -36
  106. package/dist/core/models/local-runtime.js.map +1 -1
  107. package/dist/core/output-guard.d.ts.map +1 -1
  108. package/dist/core/output-guard.js +23 -8
  109. package/dist/core/output-guard.js.map +1 -1
  110. package/dist/core/package-manager.d.ts.map +1 -1
  111. package/dist/core/package-manager.js +44 -59
  112. package/dist/core/package-manager.js.map +1 -1
  113. package/dist/core/profile-resource-selection.d.ts +16 -0
  114. package/dist/core/profile-resource-selection.d.ts.map +1 -1
  115. package/dist/core/profile-resource-selection.js +41 -0
  116. package/dist/core/profile-resource-selection.js.map +1 -1
  117. package/dist/core/python-runtime.d.ts +46 -0
  118. package/dist/core/python-runtime.d.ts.map +1 -0
  119. package/dist/core/python-runtime.js +157 -0
  120. package/dist/core/python-runtime.js.map +1 -0
  121. package/dist/core/reflection-controller.d.ts.map +1 -1
  122. package/dist/core/reflection-controller.js +18 -1
  123. package/dist/core/reflection-controller.js.map +1 -1
  124. package/dist/core/reload-blockers.d.ts +1 -0
  125. package/dist/core/reload-blockers.d.ts.map +1 -1
  126. package/dist/core/reload-blockers.js +6 -2
  127. package/dist/core/reload-blockers.js.map +1 -1
  128. package/dist/core/resource-loader.d.ts +2 -0
  129. package/dist/core/resource-loader.d.ts.map +1 -1
  130. package/dist/core/resource-loader.js +50 -28
  131. package/dist/core/resource-loader.js.map +1 -1
  132. package/dist/core/runtime-builder.d.ts +4 -0
  133. package/dist/core/runtime-builder.d.ts.map +1 -1
  134. package/dist/core/runtime-builder.js +31 -9
  135. package/dist/core/runtime-builder.js.map +1 -1
  136. package/dist/core/security/untrusted-boundary.d.ts +2 -2
  137. package/dist/core/security/untrusted-boundary.d.ts.map +1 -1
  138. package/dist/core/security/untrusted-boundary.js +4 -3
  139. package/dist/core/security/untrusted-boundary.js.map +1 -1
  140. package/dist/core/settings-manager.d.ts.map +1 -1
  141. package/dist/core/settings-manager.js +3 -1
  142. package/dist/core/settings-manager.js.map +1 -1
  143. package/dist/core/slash-commands.d.ts.map +1 -1
  144. package/dist/core/slash-commands.js +9 -1
  145. package/dist/core/slash-commands.js.map +1 -1
  146. package/dist/core/system-prompt.d.ts.map +1 -1
  147. package/dist/core/system-prompt.js +13 -2
  148. package/dist/core/system-prompt.js.map +1 -1
  149. package/dist/core/tasks/session-task-state.d.ts +10 -0
  150. package/dist/core/tasks/session-task-state.d.ts.map +1 -0
  151. package/dist/core/tasks/session-task-state.js +29 -0
  152. package/dist/core/tasks/session-task-state.js.map +1 -0
  153. package/dist/core/tasks/task-command.d.ts +30 -0
  154. package/dist/core/tasks/task-command.d.ts.map +1 -0
  155. package/dist/core/tasks/task-command.js +79 -0
  156. package/dist/core/tasks/task-command.js.map +1 -0
  157. package/dist/core/tasks/task-state.d.ts +71 -0
  158. package/dist/core/tasks/task-state.d.ts.map +1 -0
  159. package/dist/core/tasks/task-state.js +372 -0
  160. package/dist/core/tasks/task-state.js.map +1 -0
  161. package/dist/core/tool-selection/tool-selection-controller.d.ts.map +1 -1
  162. package/dist/core/tool-selection/tool-selection-controller.js +1 -1
  163. package/dist/core/tool-selection/tool-selection-controller.js.map +1 -1
  164. package/dist/core/toolkit/script-runner.d.ts.map +1 -1
  165. package/dist/core/toolkit/script-runner.js +58 -24
  166. package/dist/core/toolkit/script-runner.js.map +1 -1
  167. package/dist/core/tools/bash.d.ts +20 -2
  168. package/dist/core/tools/bash.d.ts.map +1 -1
  169. package/dist/core/tools/bash.js +132 -220
  170. package/dist/core/tools/bash.js.map +1 -1
  171. package/dist/core/tools/extensionify.d.ts +2 -1
  172. package/dist/core/tools/extensionify.d.ts.map +1 -1
  173. package/dist/core/tools/extensionify.js +11 -8
  174. package/dist/core/tools/extensionify.js.map +1 -1
  175. package/dist/core/tools/find.d.ts.map +1 -1
  176. package/dist/core/tools/find.js +57 -48
  177. package/dist/core/tools/find.js.map +1 -1
  178. package/dist/core/tools/git-filter.d.ts.map +1 -1
  179. package/dist/core/tools/git-filter.js +20 -32
  180. package/dist/core/tools/git-filter.js.map +1 -1
  181. package/dist/core/tools/goal.d.ts +1 -1
  182. package/dist/core/tools/goal.d.ts.map +1 -1
  183. package/dist/core/tools/goal.js +10 -3
  184. package/dist/core/tools/goal.js.map +1 -1
  185. package/dist/core/tools/grep.d.ts.map +1 -1
  186. package/dist/core/tools/grep.js +92 -80
  187. package/dist/core/tools/grep.js.map +1 -1
  188. package/dist/core/tools/index.d.ts +9 -6
  189. package/dist/core/tools/index.d.ts.map +1 -1
  190. package/dist/core/tools/index.js +24 -9
  191. package/dist/core/tools/index.js.map +1 -1
  192. package/dist/core/tools/output-accumulator.d.ts +2 -0
  193. package/dist/core/tools/output-accumulator.d.ts.map +1 -1
  194. package/dist/core/tools/output-accumulator.js +7 -4
  195. package/dist/core/tools/output-accumulator.js.map +1 -1
  196. package/dist/core/tools/python.d.ts +64 -0
  197. package/dist/core/tools/python.d.ts.map +1 -0
  198. package/dist/core/tools/python.js +241 -0
  199. package/dist/core/tools/python.js.map +1 -0
  200. package/dist/core/tools/read.d.ts.map +1 -1
  201. package/dist/core/tools/read.js +4 -3
  202. package/dist/core/tools/read.js.map +1 -1
  203. package/dist/core/tools/shell-contract-router.d.ts +13 -0
  204. package/dist/core/tools/shell-contract-router.d.ts.map +1 -0
  205. package/dist/core/tools/shell-contract-router.js +295 -0
  206. package/dist/core/tools/shell-contract-router.js.map +1 -0
  207. package/dist/core/tools/task-steps.d.ts +46 -0
  208. package/dist/core/tools/task-steps.d.ts.map +1 -0
  209. package/dist/core/tools/task-steps.js +160 -0
  210. package/dist/core/tools/task-steps.js.map +1 -0
  211. package/dist/index.d.ts +2 -1
  212. package/dist/index.d.ts.map +1 -1
  213. package/dist/index.js +2 -1
  214. package/dist/index.js.map +1 -1
  215. package/dist/main.d.ts +2 -0
  216. package/dist/main.d.ts.map +1 -1
  217. package/dist/main.js +23 -4
  218. package/dist/main.js.map +1 -1
  219. package/dist/migrations.d.ts.map +1 -1
  220. package/dist/migrations.js +18 -4
  221. package/dist/migrations.js.map +1 -1
  222. package/dist/modes/interactive/auth-dialogs-controller.d.ts +10 -0
  223. package/dist/modes/interactive/auth-dialogs-controller.d.ts.map +1 -1
  224. package/dist/modes/interactive/auth-dialogs-controller.js +94 -27
  225. package/dist/modes/interactive/auth-dialogs-controller.js.map +1 -1
  226. package/dist/modes/interactive/auto-learn-controller.d.ts.map +1 -1
  227. package/dist/modes/interactive/auto-learn-controller.js +2 -1
  228. package/dist/modes/interactive/auto-learn-controller.js.map +1 -1
  229. package/dist/modes/interactive/components/bordered-loader.d.ts +1 -0
  230. package/dist/modes/interactive/components/bordered-loader.d.ts.map +1 -1
  231. package/dist/modes/interactive/components/bordered-loader.js +8 -0
  232. package/dist/modes/interactive/components/bordered-loader.js.map +1 -1
  233. package/dist/modes/interactive/components/extension-editor.d.ts.map +1 -1
  234. package/dist/modes/interactive/components/extension-editor.js +6 -15
  235. package/dist/modes/interactive/components/extension-editor.js.map +1 -1
  236. package/dist/modes/interactive/components/login-dialog.d.ts +3 -1
  237. package/dist/modes/interactive/components/login-dialog.d.ts.map +1 -1
  238. package/dist/modes/interactive/components/login-dialog.js +20 -8
  239. package/dist/modes/interactive/components/login-dialog.js.map +1 -1
  240. package/dist/modes/interactive/components/profile-resource-editor.d.ts +4 -1
  241. package/dist/modes/interactive/components/profile-resource-editor.d.ts.map +1 -1
  242. package/dist/modes/interactive/components/profile-resource-editor.js +22 -39
  243. package/dist/modes/interactive/components/profile-resource-editor.js.map +1 -1
  244. package/dist/modes/interactive/components/session-selector.d.ts.map +1 -1
  245. package/dist/modes/interactive/components/session-selector.js +1 -1
  246. package/dist/modes/interactive/components/session-selector.js.map +1 -1
  247. package/dist/modes/interactive/config-backup.d.ts +1 -0
  248. package/dist/modes/interactive/config-backup.d.ts.map +1 -1
  249. package/dist/modes/interactive/config-backup.js +14 -3
  250. package/dist/modes/interactive/config-backup.js.map +1 -1
  251. package/dist/modes/interactive/editor-overlay-host.d.ts +12 -0
  252. package/dist/modes/interactive/editor-overlay-host.d.ts.map +1 -1
  253. package/dist/modes/interactive/editor-overlay-host.js +55 -0
  254. package/dist/modes/interactive/editor-overlay-host.js.map +1 -1
  255. package/dist/modes/interactive/extension-ui-host.d.ts +2 -1
  256. package/dist/modes/interactive/extension-ui-host.d.ts.map +1 -1
  257. package/dist/modes/interactive/extension-ui-host.js +161 -96
  258. package/dist/modes/interactive/extension-ui-host.js.map +1 -1
  259. package/dist/modes/interactive/external-editor.d.ts.map +1 -1
  260. package/dist/modes/interactive/external-editor.js +23 -28
  261. package/dist/modes/interactive/external-editor.js.map +1 -1
  262. package/dist/modes/interactive/interactive-mode.d.ts +1 -0
  263. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  264. package/dist/modes/interactive/interactive-mode.js +26 -8
  265. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  266. package/dist/modes/interactive/profile-menu-controller.d.ts +5 -1
  267. package/dist/modes/interactive/profile-menu-controller.d.ts.map +1 -1
  268. package/dist/modes/interactive/profile-menu-controller.js +118 -24
  269. package/dist/modes/interactive/profile-menu-controller.js.map +1 -1
  270. package/dist/modes/interactive/resource-shell-commands.d.ts +1 -0
  271. package/dist/modes/interactive/resource-shell-commands.d.ts.map +1 -1
  272. package/dist/modes/interactive/resource-shell-commands.js +5 -1
  273. package/dist/modes/interactive/resource-shell-commands.js.map +1 -1
  274. package/dist/modes/interactive/session-flow-commands.d.ts +10 -0
  275. package/dist/modes/interactive/session-flow-commands.d.ts.map +1 -1
  276. package/dist/modes/interactive/session-flow-commands.js +143 -7
  277. package/dist/modes/interactive/session-flow-commands.js.map +1 -1
  278. package/dist/modes/interactive/session-io-commands.d.ts.map +1 -1
  279. package/dist/modes/interactive/session-io-commands.js +80 -50
  280. package/dist/modes/interactive/session-io-commands.js.map +1 -1
  281. package/dist/modes/interactive/startup-checks.d.ts.map +1 -1
  282. package/dist/modes/interactive/startup-checks.js +14 -17
  283. package/dist/modes/interactive/startup-checks.js.map +1 -1
  284. package/dist/modes/rpc/rpc-client.d.ts.map +1 -1
  285. package/dist/modes/rpc/rpc-client.js +20 -19
  286. package/dist/modes/rpc/rpc-client.js.map +1 -1
  287. package/dist/package-manager-cli.d.ts.map +1 -1
  288. package/dist/package-manager-cli.js +10 -19
  289. package/dist/package-manager-cli.js.map +1 -1
  290. package/dist/utils/child-process.d.ts +13 -7
  291. package/dist/utils/child-process.d.ts.map +1 -1
  292. package/dist/utils/child-process.js +56 -3
  293. package/dist/utils/child-process.js.map +1 -1
  294. package/dist/utils/clipboard-image.d.ts.map +1 -1
  295. package/dist/utils/clipboard-image.js +3 -2
  296. package/dist/utils/clipboard-image.js.map +1 -1
  297. package/dist/utils/clipboard.d.ts.map +1 -1
  298. package/dist/utils/clipboard.js +17 -10
  299. package/dist/utils/clipboard.js.map +1 -1
  300. package/dist/utils/external-editor-command.d.ts +8 -0
  301. package/dist/utils/external-editor-command.d.ts.map +1 -0
  302. package/dist/utils/external-editor-command.js +60 -0
  303. package/dist/utils/external-editor-command.js.map +1 -0
  304. package/dist/utils/paths.d.ts.map +1 -1
  305. package/dist/utils/paths.js +10 -2
  306. package/dist/utils/paths.js.map +1 -1
  307. package/dist/utils/shell.d.ts +6 -8
  308. package/dist/utils/shell.d.ts.map +1 -1
  309. package/dist/utils/shell.js +61 -70
  310. package/dist/utils/shell.js.map +1 -1
  311. package/dist/utils/tools-manager.d.ts +23 -3
  312. package/dist/utils/tools-manager.d.ts.map +1 -1
  313. package/dist/utils/tools-manager.js +117 -21
  314. package/dist/utils/tools-manager.js.map +1 -1
  315. package/dist/utils/work-directory.d.ts +36 -0
  316. package/dist/utils/work-directory.d.ts.map +1 -0
  317. package/dist/utils/work-directory.js +462 -0
  318. package/dist/utils/work-directory.js.map +1 -0
  319. package/docs/index.md +4 -0
  320. package/docs/python.md +43 -0
  321. package/docs/resources.md +2 -2
  322. package/docs/rpc.md +1 -1
  323. package/docs/settings.md +7 -3
  324. package/docs/task-steps.md +90 -0
  325. package/docs/termux.md +7 -0
  326. package/docs/tmux-agent-manager.md +48 -0
  327. package/docs/usage.md +5 -6
  328. package/docs/windows.md +104 -8
  329. package/docs/work-directory.md +41 -0
  330. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  331. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  332. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  333. package/examples/extensions/sandbox/package-lock.json +2 -2
  334. package/examples/extensions/sandbox/package.json +1 -1
  335. package/examples/extensions/with-deps/package-lock.json +2 -2
  336. package/examples/extensions/with-deps/package.json +1 -1
  337. package/npm-shrinkwrap.json +32 -37
  338. package/package.json +9 -7
  339. package/scripts/postinstall-python-runtime.mjs +20 -0
@@ -1 +1 @@
1
- {"version":3,"file":"bash-executor.d.ts","sourceRoot":"","sources":["../../src/core/bash-executor.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAUH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAOtD,MAAM,WAAW,mBAAmB;IACnC,+DAA+D;IAC/D,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,mCAAmC;IACnC,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,2FAA2F;IAC3F,eAAe,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED,MAAM,WAAW,UAAU;IAC1B,sEAAsE;IACtE,MAAM,EAAE,MAAM,CAAC;IACf,wDAAwD;IACxD,QAAQ,EAAE,MAAM,GAAG,SAAS,CAAC;IAC7B,mDAAmD;IACnD,SAAS,EAAE,OAAO,CAAC;IACnB,uCAAuC;IACvC,SAAS,EAAE,OAAO,CAAC;IACnB,yFAAyF;IACzF,cAAc,CAAC,EAAE,MAAM,CAAC;CACxB;AAMD;;;GAGG;AACH,wBAAsB,yBAAyB,CAC9C,OAAO,EAAE,MAAM,EACf,GAAG,EAAE,MAAM,EACX,UAAU,EAAE,cAAc,EAC1B,OAAO,CAAC,EAAE,mBAAmB,GAC3B,OAAO,CAAC,UAAU,CAAC,CAgKrB","sourcesContent":["/**\n * Bash command execution with streaming support and cancellation.\n *\n * This module provides a unified bash execution implementation used by:\n * - AgentSession.executeBash() for interactive and RPC modes\n * - Direct calls from modes that need bash execution\n */\n\nimport { randomBytes } from \"node:crypto\";\nimport type { WriteStream } from \"node:fs\";\nimport { tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\nimport { sanitizeBinaryOutput } from \"@caupulican/pi-agent-core\";\nimport { DEFAULT_MAX_BYTES, truncateTail } from \"@caupulican/pi-agent-core/node\";\nimport { stripAnsi } from \"../utils/ansi.ts\";\nimport { createSafeWriteStream, endWriteStream } from \"../utils/safe-write-stream.ts\";\nimport type { BashOperations } from \"./tools/bash.ts\";\nimport { classifyGitCommand, executeFilteredGit } from \"./tools/git-filter.ts\";\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface BashExecutorOptions {\n\t/** Callback for streaming output chunks (already sanitized) */\n\tonChunk?: (chunk: string) => void;\n\t/** AbortSignal for cancellation */\n\tsignal?: AbortSignal;\n\t/** Enable conservative pi-native git output filtering for local default execution paths */\n\tenableGitFilter?: boolean;\n}\n\nexport interface BashResult {\n\t/** Combined stdout + stderr output (sanitized, possibly truncated) */\n\toutput: string;\n\t/** Process exit code (undefined if killed/cancelled) */\n\texitCode: number | undefined;\n\t/** Whether the command was cancelled via signal */\n\tcancelled: boolean;\n\t/** Whether the output was truncated */\n\ttruncated: boolean;\n\t/** Path to temp file containing full output (if output exceeded truncation threshold) */\n\tfullOutputPath?: string;\n}\n\n// ============================================================================\n// Implementation\n// ============================================================================\n\n/**\n * Execute a bash command using custom BashOperations.\n * Used for remote execution (SSH, containers, etc.).\n */\nexport async function executeBashWithOperations(\n\tcommand: string,\n\tcwd: string,\n\toperations: BashOperations,\n\toptions?: BashExecutorOptions,\n): Promise<BashResult> {\n\tif (options?.enableGitFilter) {\n\t\tconst classification = classifyGitCommand(command, process.env);\n\t\tif (classification.eligible && classification.subcommand) {\n\t\t\tconst res = await executeFilteredGit(\n\t\t\t\tcwd,\n\t\t\t\tclassification.subcommand,\n\t\t\t\tclassification.globalOptions || [],\n\t\t\t\tclassification.subcommandArgs || [],\n\t\t\t\t{ signal: options.signal },\n\t\t\t);\n\t\t\tif (res.exitCode !== -100) {\n\t\t\t\tconst rawBytes = res.rawBytes ?? Buffer.from(res.rawOut, \"utf-8\");\n\t\t\t\t// The filter already spills oversized output to a temp file; reuse it\n\t\t\t\t// instead of materializing another full copy here.\n\t\t\t\tlet fullOutputPath = res.fullOutputPath;\n\t\t\t\tif (fullOutputPath === undefined && rawBytes.length > DEFAULT_MAX_BYTES) {\n\t\t\t\t\tconst id = randomBytes(8).toString(\"hex\");\n\t\t\t\t\tconst spillPath = join(tmpdir(), `pi-bash-${id}.log`);\n\t\t\t\t\tfullOutputPath = spillPath;\n\t\t\t\t\t// On stream failure (e.g. disk full), drop the advertised path instead of\n\t\t\t\t\t// pointing at a partial/missing file.\n\t\t\t\t\tconst tempFileStream = createSafeWriteStream(spillPath, () => {\n\t\t\t\t\t\tfullOutputPath = undefined;\n\t\t\t\t\t});\n\t\t\t\t\ttempFileStream.write(rawBytes);\n\t\t\t\t\t// Await the flush so the returned path points at a COMPLETE file, not one mid-write.\n\t\t\t\t\tawait endWriteStream(tempFileStream);\n\t\t\t\t}\n\t\t\t\toptions.onChunk?.(res.output);\n\t\t\t\treturn {\n\t\t\t\t\toutput: res.output,\n\t\t\t\t\texitCode: res.exitCode,\n\t\t\t\t\tcancelled: options.signal?.aborted ?? false,\n\t\t\t\t\ttruncated: res.fullOutputPath !== undefined || rawBytes.length > DEFAULT_MAX_BYTES,\n\t\t\t\t\tfullOutputPath,\n\t\t\t\t};\n\t\t\t}\n\t\t}\n\t}\n\n\tconst outputChunks: string[] = [];\n\tlet outputBytes = 0;\n\tconst maxOutputBytes = DEFAULT_MAX_BYTES * 2;\n\n\tlet tempFilePath: string | undefined;\n\tlet tempFileStream: WriteStream | undefined;\n\tlet totalBytes = 0;\n\n\tconst ensureTempFile = () => {\n\t\tif (tempFilePath) {\n\t\t\treturn;\n\t\t}\n\t\tconst id = randomBytes(8).toString(\"hex\");\n\t\ttempFilePath = join(tmpdir(), `pi-bash-${id}.log`);\n\t\t// On stream failure (e.g. disk full), drop the artifact instead of\n\t\t// crashing the process; the rolling in-memory output is still returned.\n\t\ttempFileStream = createSafeWriteStream(tempFilePath, () => {\n\t\t\ttempFileStream = undefined;\n\t\t\ttempFilePath = undefined;\n\t\t});\n\t\tfor (const chunk of outputChunks) {\n\t\t\ttempFileStream.write(chunk);\n\t\t}\n\t};\n\n\tconst decoder = new TextDecoder();\n\n\tconst onData = (data: Buffer) => {\n\t\ttotalBytes += data.length;\n\n\t\t// Sanitize: strip ANSI, replace binary garbage, normalize newlines\n\t\tconst text = sanitizeBinaryOutput(stripAnsi(decoder.decode(data, { stream: true }))).replace(/\\r/g, \"\");\n\n\t\t// Start writing to temp file if exceeds threshold\n\t\tif (totalBytes > DEFAULT_MAX_BYTES) {\n\t\t\tensureTempFile();\n\t\t}\n\n\t\t// Guard writableEnded: custom BashOperations may deliver late onData\n\t\t// callbacks after an abort path has already ended the stream.\n\t\tif (tempFileStream && !tempFileStream.writableEnded) {\n\t\t\ttempFileStream.write(text);\n\t\t}\n\n\t\t// Keep rolling buffer\n\t\toutputChunks.push(text);\n\t\toutputBytes += text.length;\n\t\twhile (outputBytes > maxOutputBytes && outputChunks.length > 1) {\n\t\t\tconst removed = outputChunks.shift()!;\n\t\t\toutputBytes -= removed.length;\n\t\t}\n\n\t\t// Stream to callback\n\t\tif (options?.onChunk) {\n\t\t\toptions.onChunk(text);\n\t\t}\n\t};\n\n\ttry {\n\t\tconst result = await operations.exec(command, cwd, {\n\t\t\tonData,\n\t\t\tsignal: options?.signal,\n\t\t});\n\n\t\tconst fullOutput = outputChunks.join(\"\");\n\t\tconst truncationResult = truncateTail(fullOutput);\n\t\tif (truncationResult.truncated) {\n\t\t\tensureTempFile();\n\t\t}\n\t\tif (tempFileStream) {\n\t\t\t// Await the flush so fullOutputPath refers to a fully-written file on return, not one still\n\t\t\t// draining its buffer — otherwise a fast reader can see partial/empty content.\n\t\t\tawait endWriteStream(tempFileStream);\n\t\t}\n\t\tconst cancelled = options?.signal?.aborted ?? false;\n\n\t\treturn {\n\t\t\toutput: truncationResult.truncated ? truncationResult.content : fullOutput,\n\t\t\texitCode: cancelled ? undefined : (result.exitCode ?? undefined),\n\t\t\tcancelled,\n\t\t\ttruncated: truncationResult.truncated,\n\t\t\tfullOutputPath: tempFilePath,\n\t\t};\n\t} catch (err) {\n\t\t// Check if it was an abort\n\t\tif (options?.signal?.aborted) {\n\t\t\tconst fullOutput = outputChunks.join(\"\");\n\t\t\tconst truncationResult = truncateTail(fullOutput);\n\t\t\tif (truncationResult.truncated) {\n\t\t\t\tensureTempFile();\n\t\t\t}\n\t\t\tif (tempFileStream) {\n\t\t\t\tawait endWriteStream(tempFileStream);\n\t\t\t}\n\t\t\treturn {\n\t\t\t\toutput: truncationResult.truncated ? truncationResult.content : fullOutput,\n\t\t\t\texitCode: undefined,\n\t\t\t\tcancelled: true,\n\t\t\t\ttruncated: truncationResult.truncated,\n\t\t\t\tfullOutputPath: tempFilePath,\n\t\t\t};\n\t\t}\n\n\t\tif (tempFileStream) {\n\t\t\tawait endWriteStream(tempFileStream);\n\t\t}\n\n\t\t// The silence watchdog (see tools/bash.ts) throws a raw `silence:<secs>` sentinel.\n\t\t// Map it to the same user-facing message the interactive bash tool shows, instead of\n\t\t// leaking the sentinel to callers of this path (interactive !cmd, RPC).\n\t\tif (err instanceof Error && err.message.startsWith(\"silence:\")) {\n\t\t\tconst secs = err.message.split(\":\")[1];\n\t\t\tthrow new Error(\n\t\t\t\t`Command killed after ${secs}s of silence (no output). If the command is legitimately quiet for long stretches, re-run it with an explicit timeout, or run it in the background with '&'.`,\n\t\t\t);\n\t\t}\n\n\t\tthrow err;\n\t}\n}\n"]}
1
+ {"version":3,"file":"bash-executor.d.ts","sourceRoot":"","sources":["../../src/core/bash-executor.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAWH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAOtD,MAAM,WAAW,mBAAmB;IACnC,+DAA+D;IAC/D,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,mCAAmC;IACnC,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,2FAA2F;IAC3F,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,wHAAwH;IACxH,OAAO,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,UAAU;IAC1B,sEAAsE;IACtE,MAAM,EAAE,MAAM,CAAC;IACf,wDAAwD;IACxD,QAAQ,EAAE,MAAM,GAAG,SAAS,CAAC;IAC7B,mDAAmD;IACnD,SAAS,EAAE,OAAO,CAAC;IACnB,uCAAuC;IACvC,SAAS,EAAE,OAAO,CAAC;IACnB,yFAAyF;IACzF,cAAc,CAAC,EAAE,MAAM,CAAC;CACxB;AAUD;;;GAGG;AACH,wBAAsB,yBAAyB,CAC9C,OAAO,EAAE,MAAM,EACf,GAAG,EAAE,MAAM,EACX,UAAU,EAAE,cAAc,EAC1B,OAAO,CAAC,EAAE,mBAAmB,GAC3B,OAAO,CAAC,UAAU,CAAC,CAiKrB","sourcesContent":["/**\n * Bash command execution with streaming support and cancellation.\n *\n * This module provides a unified bash execution implementation used by:\n * - AgentSession.executeBash() for interactive and RPC modes\n * - Direct calls from modes that need bash execution\n */\n\nimport { randomBytes } from \"node:crypto\";\nimport type { WriteStream } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { sanitizeBinaryOutput } from \"@caupulican/pi-agent-core\";\nimport { DEFAULT_MAX_BYTES, truncateTail } from \"@caupulican/pi-agent-core/node\";\nimport { getAgentDir } from \"../config.ts\";\nimport { stripAnsi } from \"../utils/ansi.ts\";\nimport { createSafeWriteStream, endWriteStream } from \"../utils/safe-write-stream.ts\";\nimport { getProcessWorkRun } from \"../utils/work-directory.ts\";\nimport type { BashOperations } from \"./tools/bash.ts\";\nimport { classifyGitCommand, executeFilteredGit } from \"./tools/git-filter.ts\";\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface BashExecutorOptions {\n\t/** Callback for streaming output chunks (already sanitized) */\n\tonChunk?: (chunk: string) => void;\n\t/** AbortSignal for cancellation */\n\tsignal?: AbortSignal;\n\t/** Enable conservative pi-native git output filtering for local default execution paths */\n\tenableGitFilter?: boolean;\n\t/** Wall-clock timeout in seconds. Direct interactive/RPC callers provide the same bounded default as the agent tool. */\n\ttimeout?: number;\n}\n\nexport interface BashResult {\n\t/** Combined stdout + stderr output (sanitized, possibly truncated) */\n\toutput: string;\n\t/** Process exit code (undefined if killed/cancelled) */\n\texitCode: number | undefined;\n\t/** Whether the command was cancelled via signal */\n\tcancelled: boolean;\n\t/** Whether the output was truncated */\n\ttruncated: boolean;\n\t/** Path to temp file containing full output (if output exceeded truncation threshold) */\n\tfullOutputPath?: string;\n}\n\n// ============================================================================\n// Implementation\n// ============================================================================\n\nfunction getBashOutputPath(id: string): string {\n\treturn join(getProcessWorkRun(getAgentDir(), \"outputs\", \"bash\").path, `pi-bash-${id}.log`);\n}\n\n/**\n * Execute a bash command using custom BashOperations.\n * Used for remote execution (SSH, containers, etc.).\n */\nexport async function executeBashWithOperations(\n\tcommand: string,\n\tcwd: string,\n\toperations: BashOperations,\n\toptions?: BashExecutorOptions,\n): Promise<BashResult> {\n\tif (options?.enableGitFilter) {\n\t\tconst classification = classifyGitCommand(command, process.env);\n\t\tif (classification.eligible && classification.subcommand) {\n\t\t\tconst res = await executeFilteredGit(\n\t\t\t\tcwd,\n\t\t\t\tclassification.subcommand,\n\t\t\t\tclassification.globalOptions || [],\n\t\t\t\tclassification.subcommandArgs || [],\n\t\t\t\t{ signal: options.signal, timeout: options.timeout },\n\t\t\t);\n\t\t\tif (res.exitCode !== -100) {\n\t\t\t\tconst rawBytes = res.rawBytes ?? Buffer.from(res.rawOut, \"utf-8\");\n\t\t\t\t// The filter already spills oversized output to a temp file; reuse it\n\t\t\t\t// instead of materializing another full copy here.\n\t\t\t\tlet fullOutputPath = res.fullOutputPath;\n\t\t\t\tif (fullOutputPath === undefined && rawBytes.length > DEFAULT_MAX_BYTES) {\n\t\t\t\t\tconst id = randomBytes(8).toString(\"hex\");\n\t\t\t\t\tconst spillPath = getBashOutputPath(id);\n\t\t\t\t\tfullOutputPath = spillPath;\n\t\t\t\t\t// On stream failure (e.g. disk full), drop the advertised path instead of\n\t\t\t\t\t// pointing at a partial/missing file.\n\t\t\t\t\tconst tempFileStream = createSafeWriteStream(spillPath, () => {\n\t\t\t\t\t\tfullOutputPath = undefined;\n\t\t\t\t\t});\n\t\t\t\t\ttempFileStream.write(rawBytes);\n\t\t\t\t\t// Await the flush so the returned path points at a COMPLETE file, not one mid-write.\n\t\t\t\t\tawait endWriteStream(tempFileStream);\n\t\t\t\t}\n\t\t\t\toptions.onChunk?.(res.output);\n\t\t\t\treturn {\n\t\t\t\t\toutput: res.output,\n\t\t\t\t\texitCode: res.exitCode,\n\t\t\t\t\tcancelled: options.signal?.aborted ?? false,\n\t\t\t\t\ttruncated: res.fullOutputPath !== undefined || rawBytes.length > DEFAULT_MAX_BYTES,\n\t\t\t\t\tfullOutputPath,\n\t\t\t\t};\n\t\t\t}\n\t\t}\n\t}\n\n\tconst outputChunks: string[] = [];\n\tlet outputBytes = 0;\n\tconst maxOutputBytes = DEFAULT_MAX_BYTES * 2;\n\n\tlet tempFilePath: string | undefined;\n\tlet tempFileStream: WriteStream | undefined;\n\tlet totalBytes = 0;\n\n\tconst ensureTempFile = () => {\n\t\tif (tempFilePath) {\n\t\t\treturn;\n\t\t}\n\t\tconst id = randomBytes(8).toString(\"hex\");\n\t\ttempFilePath = getBashOutputPath(id);\n\t\t// On stream failure (e.g. disk full), drop the artifact instead of\n\t\t// crashing the process; the rolling in-memory output is still returned.\n\t\ttempFileStream = createSafeWriteStream(tempFilePath, () => {\n\t\t\ttempFileStream = undefined;\n\t\t\ttempFilePath = undefined;\n\t\t});\n\t\tfor (const chunk of outputChunks) {\n\t\t\ttempFileStream.write(chunk);\n\t\t}\n\t};\n\n\tconst decoder = new TextDecoder();\n\n\tconst onData = (data: Buffer) => {\n\t\ttotalBytes += data.length;\n\n\t\t// Sanitize: strip ANSI, replace binary garbage, normalize newlines\n\t\tconst text = sanitizeBinaryOutput(stripAnsi(decoder.decode(data, { stream: true }))).replace(/\\r/g, \"\");\n\n\t\t// Start writing to temp file if exceeds threshold\n\t\tif (totalBytes > DEFAULT_MAX_BYTES) {\n\t\t\tensureTempFile();\n\t\t}\n\n\t\t// Guard writableEnded: custom BashOperations may deliver late onData\n\t\t// callbacks after an abort path has already ended the stream.\n\t\tif (tempFileStream && !tempFileStream.writableEnded) {\n\t\t\ttempFileStream.write(text);\n\t\t}\n\n\t\t// Keep rolling buffer\n\t\toutputChunks.push(text);\n\t\toutputBytes += text.length;\n\t\twhile (outputBytes > maxOutputBytes && outputChunks.length > 1) {\n\t\t\tconst removed = outputChunks.shift()!;\n\t\t\toutputBytes -= removed.length;\n\t\t}\n\n\t\t// Stream to callback\n\t\tif (options?.onChunk) {\n\t\t\toptions.onChunk(text);\n\t\t}\n\t};\n\n\ttry {\n\t\tconst result = await operations.exec(command, cwd, {\n\t\t\tonData,\n\t\t\tsignal: options?.signal,\n\t\t\ttimeout: options?.timeout,\n\t\t});\n\n\t\tconst fullOutput = outputChunks.join(\"\");\n\t\tconst truncationResult = truncateTail(fullOutput);\n\t\tif (truncationResult.truncated) {\n\t\t\tensureTempFile();\n\t\t}\n\t\tif (tempFileStream) {\n\t\t\t// Await the flush so fullOutputPath refers to a fully-written file on return, not one still\n\t\t\t// draining its buffer — otherwise a fast reader can see partial/empty content.\n\t\t\tawait endWriteStream(tempFileStream);\n\t\t}\n\t\tconst cancelled = options?.signal?.aborted ?? false;\n\n\t\treturn {\n\t\t\toutput: truncationResult.truncated ? truncationResult.content : fullOutput,\n\t\t\texitCode: cancelled ? undefined : (result.exitCode ?? undefined),\n\t\t\tcancelled,\n\t\t\ttruncated: truncationResult.truncated,\n\t\t\tfullOutputPath: tempFilePath,\n\t\t};\n\t} catch (err) {\n\t\t// Check if it was an abort\n\t\tif (options?.signal?.aborted) {\n\t\t\tconst fullOutput = outputChunks.join(\"\");\n\t\t\tconst truncationResult = truncateTail(fullOutput);\n\t\t\tif (truncationResult.truncated) {\n\t\t\t\tensureTempFile();\n\t\t\t}\n\t\t\tif (tempFileStream) {\n\t\t\t\tawait endWriteStream(tempFileStream);\n\t\t\t}\n\t\t\treturn {\n\t\t\t\toutput: truncationResult.truncated ? truncationResult.content : fullOutput,\n\t\t\t\texitCode: undefined,\n\t\t\t\tcancelled: true,\n\t\t\t\ttruncated: truncationResult.truncated,\n\t\t\t\tfullOutputPath: tempFilePath,\n\t\t\t};\n\t\t}\n\n\t\tif (tempFileStream) {\n\t\t\tawait endWriteStream(tempFileStream);\n\t\t}\n\n\t\t// The silence watchdog (see tools/bash.ts) throws a raw `silence:<secs>` sentinel.\n\t\t// Map it to the same user-facing message the interactive bash tool shows, instead of\n\t\t// leaking the sentinel to callers of this path (interactive !cmd, RPC).\n\t\tif (err instanceof Error && err.message.startsWith(\"silence:\")) {\n\t\t\tconst secs = err.message.split(\":\")[1];\n\t\t\tthrow new Error(\n\t\t\t\t`Command killed after ${secs}s of silence (no output). If the command is legitimately quiet for long stretches, re-run it with an explicit timeout.`,\n\t\t\t);\n\t\t}\n\n\t\tthrow err;\n\t}\n}\n"]}
@@ -6,16 +6,20 @@
6
6
  * - Direct calls from modes that need bash execution
7
7
  */
8
8
  import { randomBytes } from "node:crypto";
9
- import { tmpdir } from "node:os";
10
9
  import { join } from "node:path";
11
10
  import { sanitizeBinaryOutput } from "@caupulican/pi-agent-core";
12
11
  import { DEFAULT_MAX_BYTES, truncateTail } from "@caupulican/pi-agent-core/node";
12
+ import { getAgentDir } from "../config.js";
13
13
  import { stripAnsi } from "../utils/ansi.js";
14
14
  import { createSafeWriteStream, endWriteStream } from "../utils/safe-write-stream.js";
15
+ import { getProcessWorkRun } from "../utils/work-directory.js";
15
16
  import { classifyGitCommand, executeFilteredGit } from "./tools/git-filter.js";
16
17
  // ============================================================================
17
18
  // Implementation
18
19
  // ============================================================================
20
+ function getBashOutputPath(id) {
21
+ return join(getProcessWorkRun(getAgentDir(), "outputs", "bash").path, `pi-bash-${id}.log`);
22
+ }
19
23
  /**
20
24
  * Execute a bash command using custom BashOperations.
21
25
  * Used for remote execution (SSH, containers, etc.).
@@ -24,7 +28,7 @@ export async function executeBashWithOperations(command, cwd, operations, option
24
28
  if (options?.enableGitFilter) {
25
29
  const classification = classifyGitCommand(command, process.env);
26
30
  if (classification.eligible && classification.subcommand) {
27
- const res = await executeFilteredGit(cwd, classification.subcommand, classification.globalOptions || [], classification.subcommandArgs || [], { signal: options.signal });
31
+ const res = await executeFilteredGit(cwd, classification.subcommand, classification.globalOptions || [], classification.subcommandArgs || [], { signal: options.signal, timeout: options.timeout });
28
32
  if (res.exitCode !== -100) {
29
33
  const rawBytes = res.rawBytes ?? Buffer.from(res.rawOut, "utf-8");
30
34
  // The filter already spills oversized output to a temp file; reuse it
@@ -32,7 +36,7 @@ export async function executeBashWithOperations(command, cwd, operations, option
32
36
  let fullOutputPath = res.fullOutputPath;
33
37
  if (fullOutputPath === undefined && rawBytes.length > DEFAULT_MAX_BYTES) {
34
38
  const id = randomBytes(8).toString("hex");
35
- const spillPath = join(tmpdir(), `pi-bash-${id}.log`);
39
+ const spillPath = getBashOutputPath(id);
36
40
  fullOutputPath = spillPath;
37
41
  // On stream failure (e.g. disk full), drop the advertised path instead of
38
42
  // pointing at a partial/missing file.
@@ -65,7 +69,7 @@ export async function executeBashWithOperations(command, cwd, operations, option
65
69
  return;
66
70
  }
67
71
  const id = randomBytes(8).toString("hex");
68
- tempFilePath = join(tmpdir(), `pi-bash-${id}.log`);
72
+ tempFilePath = getBashOutputPath(id);
69
73
  // On stream failure (e.g. disk full), drop the artifact instead of
70
74
  // crashing the process; the rolling in-memory output is still returned.
71
75
  tempFileStream = createSafeWriteStream(tempFilePath, () => {
@@ -106,6 +110,7 @@ export async function executeBashWithOperations(command, cwd, operations, option
106
110
  const result = await operations.exec(command, cwd, {
107
111
  onData,
108
112
  signal: options?.signal,
113
+ timeout: options?.timeout,
109
114
  });
110
115
  const fullOutput = outputChunks.join("");
111
116
  const truncationResult = truncateTail(fullOutput);
@@ -153,7 +158,7 @@ export async function executeBashWithOperations(command, cwd, operations, option
153
158
  // leaking the sentinel to callers of this path (interactive !cmd, RPC).
154
159
  if (err instanceof Error && err.message.startsWith("silence:")) {
155
160
  const secs = err.message.split(":")[1];
156
- throw new Error(`Command killed after ${secs}s of silence (no output). If the command is legitimately quiet for long stretches, re-run it with an explicit timeout, or run it in the background with '&'.`);
161
+ throw new Error(`Command killed after ${secs}s of silence (no output). If the command is legitimately quiet for long stretches, re-run it with an explicit timeout.`);
157
162
  }
158
163
  throw err;
159
164
  }
@@ -1 +1 @@
1
- {"version":3,"file":"bash-executor.js","sourceRoot":"","sources":["../../src/core/bash-executor.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Bash command execution with streaming support and cancellation.\n *\n * This module provides a unified bash execution implementation used by:\n * - AgentSession.executeBash() for interactive and RPC modes\n * - Direct calls from modes that need bash execution\n */\n\nimport { randomBytes } from \"node:crypto\";\nimport type { WriteStream } from \"node:fs\";\nimport { tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\nimport { sanitizeBinaryOutput } from \"@caupulican/pi-agent-core\";\nimport { DEFAULT_MAX_BYTES, truncateTail } from \"@caupulican/pi-agent-core/node\";\nimport { stripAnsi } from \"../utils/ansi.ts\";\nimport { createSafeWriteStream, endWriteStream } from \"../utils/safe-write-stream.ts\";\nimport type { BashOperations } from \"./tools/bash.ts\";\nimport { classifyGitCommand, executeFilteredGit } from \"./tools/git-filter.ts\";\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface BashExecutorOptions {\n\t/** Callback for streaming output chunks (already sanitized) */\n\tonChunk?: (chunk: string) => void;\n\t/** AbortSignal for cancellation */\n\tsignal?: AbortSignal;\n\t/** Enable conservative pi-native git output filtering for local default execution paths */\n\tenableGitFilter?: boolean;\n}\n\nexport interface BashResult {\n\t/** Combined stdout + stderr output (sanitized, possibly truncated) */\n\toutput: string;\n\t/** Process exit code (undefined if killed/cancelled) */\n\texitCode: number | undefined;\n\t/** Whether the command was cancelled via signal */\n\tcancelled: boolean;\n\t/** Whether the output was truncated */\n\ttruncated: boolean;\n\t/** Path to temp file containing full output (if output exceeded truncation threshold) */\n\tfullOutputPath?: string;\n}\n\n// ============================================================================\n// Implementation\n// ============================================================================\n\n/**\n * Execute a bash command using custom BashOperations.\n * Used for remote execution (SSH, containers, etc.).\n */\nexport async function executeBashWithOperations(\n\tcommand: string,\n\tcwd: string,\n\toperations: BashOperations,\n\toptions?: BashExecutorOptions,\n): Promise<BashResult> {\n\tif (options?.enableGitFilter) {\n\t\tconst classification = classifyGitCommand(command, process.env);\n\t\tif (classification.eligible && classification.subcommand) {\n\t\t\tconst res = await executeFilteredGit(\n\t\t\t\tcwd,\n\t\t\t\tclassification.subcommand,\n\t\t\t\tclassification.globalOptions || [],\n\t\t\t\tclassification.subcommandArgs || [],\n\t\t\t\t{ signal: options.signal },\n\t\t\t);\n\t\t\tif (res.exitCode !== -100) {\n\t\t\t\tconst rawBytes = res.rawBytes ?? Buffer.from(res.rawOut, \"utf-8\");\n\t\t\t\t// The filter already spills oversized output to a temp file; reuse it\n\t\t\t\t// instead of materializing another full copy here.\n\t\t\t\tlet fullOutputPath = res.fullOutputPath;\n\t\t\t\tif (fullOutputPath === undefined && rawBytes.length > DEFAULT_MAX_BYTES) {\n\t\t\t\t\tconst id = randomBytes(8).toString(\"hex\");\n\t\t\t\t\tconst spillPath = join(tmpdir(), `pi-bash-${id}.log`);\n\t\t\t\t\tfullOutputPath = spillPath;\n\t\t\t\t\t// On stream failure (e.g. disk full), drop the advertised path instead of\n\t\t\t\t\t// pointing at a partial/missing file.\n\t\t\t\t\tconst tempFileStream = createSafeWriteStream(spillPath, () => {\n\t\t\t\t\t\tfullOutputPath = undefined;\n\t\t\t\t\t});\n\t\t\t\t\ttempFileStream.write(rawBytes);\n\t\t\t\t\t// Await the flush so the returned path points at a COMPLETE file, not one mid-write.\n\t\t\t\t\tawait endWriteStream(tempFileStream);\n\t\t\t\t}\n\t\t\t\toptions.onChunk?.(res.output);\n\t\t\t\treturn {\n\t\t\t\t\toutput: res.output,\n\t\t\t\t\texitCode: res.exitCode,\n\t\t\t\t\tcancelled: options.signal?.aborted ?? false,\n\t\t\t\t\ttruncated: res.fullOutputPath !== undefined || rawBytes.length > DEFAULT_MAX_BYTES,\n\t\t\t\t\tfullOutputPath,\n\t\t\t\t};\n\t\t\t}\n\t\t}\n\t}\n\n\tconst outputChunks: string[] = [];\n\tlet outputBytes = 0;\n\tconst maxOutputBytes = DEFAULT_MAX_BYTES * 2;\n\n\tlet tempFilePath: string | undefined;\n\tlet tempFileStream: WriteStream | undefined;\n\tlet totalBytes = 0;\n\n\tconst ensureTempFile = () => {\n\t\tif (tempFilePath) {\n\t\t\treturn;\n\t\t}\n\t\tconst id = randomBytes(8).toString(\"hex\");\n\t\ttempFilePath = join(tmpdir(), `pi-bash-${id}.log`);\n\t\t// On stream failure (e.g. disk full), drop the artifact instead of\n\t\t// crashing the process; the rolling in-memory output is still returned.\n\t\ttempFileStream = createSafeWriteStream(tempFilePath, () => {\n\t\t\ttempFileStream = undefined;\n\t\t\ttempFilePath = undefined;\n\t\t});\n\t\tfor (const chunk of outputChunks) {\n\t\t\ttempFileStream.write(chunk);\n\t\t}\n\t};\n\n\tconst decoder = new TextDecoder();\n\n\tconst onData = (data: Buffer) => {\n\t\ttotalBytes += data.length;\n\n\t\t// Sanitize: strip ANSI, replace binary garbage, normalize newlines\n\t\tconst text = sanitizeBinaryOutput(stripAnsi(decoder.decode(data, { stream: true }))).replace(/\\r/g, \"\");\n\n\t\t// Start writing to temp file if exceeds threshold\n\t\tif (totalBytes > DEFAULT_MAX_BYTES) {\n\t\t\tensureTempFile();\n\t\t}\n\n\t\t// Guard writableEnded: custom BashOperations may deliver late onData\n\t\t// callbacks after an abort path has already ended the stream.\n\t\tif (tempFileStream && !tempFileStream.writableEnded) {\n\t\t\ttempFileStream.write(text);\n\t\t}\n\n\t\t// Keep rolling buffer\n\t\toutputChunks.push(text);\n\t\toutputBytes += text.length;\n\t\twhile (outputBytes > maxOutputBytes && outputChunks.length > 1) {\n\t\t\tconst removed = outputChunks.shift()!;\n\t\t\toutputBytes -= removed.length;\n\t\t}\n\n\t\t// Stream to callback\n\t\tif (options?.onChunk) {\n\t\t\toptions.onChunk(text);\n\t\t}\n\t};\n\n\ttry {\n\t\tconst result = await operations.exec(command, cwd, {\n\t\t\tonData,\n\t\t\tsignal: options?.signal,\n\t\t});\n\n\t\tconst fullOutput = outputChunks.join(\"\");\n\t\tconst truncationResult = truncateTail(fullOutput);\n\t\tif (truncationResult.truncated) {\n\t\t\tensureTempFile();\n\t\t}\n\t\tif (tempFileStream) {\n\t\t\t// Await the flush so fullOutputPath refers to a fully-written file on return, not one still\n\t\t\t// draining its buffer — otherwise a fast reader can see partial/empty content.\n\t\t\tawait endWriteStream(tempFileStream);\n\t\t}\n\t\tconst cancelled = options?.signal?.aborted ?? false;\n\n\t\treturn {\n\t\t\toutput: truncationResult.truncated ? truncationResult.content : fullOutput,\n\t\t\texitCode: cancelled ? undefined : (result.exitCode ?? undefined),\n\t\t\tcancelled,\n\t\t\ttruncated: truncationResult.truncated,\n\t\t\tfullOutputPath: tempFilePath,\n\t\t};\n\t} catch (err) {\n\t\t// Check if it was an abort\n\t\tif (options?.signal?.aborted) {\n\t\t\tconst fullOutput = outputChunks.join(\"\");\n\t\t\tconst truncationResult = truncateTail(fullOutput);\n\t\t\tif (truncationResult.truncated) {\n\t\t\t\tensureTempFile();\n\t\t\t}\n\t\t\tif (tempFileStream) {\n\t\t\t\tawait endWriteStream(tempFileStream);\n\t\t\t}\n\t\t\treturn {\n\t\t\t\toutput: truncationResult.truncated ? truncationResult.content : fullOutput,\n\t\t\t\texitCode: undefined,\n\t\t\t\tcancelled: true,\n\t\t\t\ttruncated: truncationResult.truncated,\n\t\t\t\tfullOutputPath: tempFilePath,\n\t\t\t};\n\t\t}\n\n\t\tif (tempFileStream) {\n\t\t\tawait endWriteStream(tempFileStream);\n\t\t}\n\n\t\t// The silence watchdog (see tools/bash.ts) throws a raw `silence:<secs>` sentinel.\n\t\t// Map it to the same user-facing message the interactive bash tool shows, instead of\n\t\t// leaking the sentinel to callers of this path (interactive !cmd, RPC).\n\t\tif (err instanceof Error && err.message.startsWith(\"silence:\")) {\n\t\t\tconst secs = err.message.split(\":\")[1];\n\t\t\tthrow new Error(\n\t\t\t\t`Command killed after ${secs}s of silence (no output). If the command is legitimately quiet for long stretches, re-run it with an explicit timeout, or run it in the background with '&'.`,\n\t\t\t);\n\t\t}\n\n\t\tthrow err;\n\t}\n}\n"]}
1
+ {"version":3,"file":"bash-executor.js","sourceRoot":"","sources":["../../src/core/bash-executor.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Bash command execution with streaming support and cancellation.\n *\n * This module provides a unified bash execution implementation used by:\n * - AgentSession.executeBash() for interactive and RPC modes\n * - Direct calls from modes that need bash execution\n */\n\nimport { randomBytes } from \"node:crypto\";\nimport type { WriteStream } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { sanitizeBinaryOutput } from \"@caupulican/pi-agent-core\";\nimport { DEFAULT_MAX_BYTES, truncateTail } from \"@caupulican/pi-agent-core/node\";\nimport { getAgentDir } from \"../config.ts\";\nimport { stripAnsi } from \"../utils/ansi.ts\";\nimport { createSafeWriteStream, endWriteStream } from \"../utils/safe-write-stream.ts\";\nimport { getProcessWorkRun } from \"../utils/work-directory.ts\";\nimport type { BashOperations } from \"./tools/bash.ts\";\nimport { classifyGitCommand, executeFilteredGit } from \"./tools/git-filter.ts\";\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface BashExecutorOptions {\n\t/** Callback for streaming output chunks (already sanitized) */\n\tonChunk?: (chunk: string) => void;\n\t/** AbortSignal for cancellation */\n\tsignal?: AbortSignal;\n\t/** Enable conservative pi-native git output filtering for local default execution paths */\n\tenableGitFilter?: boolean;\n\t/** Wall-clock timeout in seconds. Direct interactive/RPC callers provide the same bounded default as the agent tool. */\n\ttimeout?: number;\n}\n\nexport interface BashResult {\n\t/** Combined stdout + stderr output (sanitized, possibly truncated) */\n\toutput: string;\n\t/** Process exit code (undefined if killed/cancelled) */\n\texitCode: number | undefined;\n\t/** Whether the command was cancelled via signal */\n\tcancelled: boolean;\n\t/** Whether the output was truncated */\n\ttruncated: boolean;\n\t/** Path to temp file containing full output (if output exceeded truncation threshold) */\n\tfullOutputPath?: string;\n}\n\n// ============================================================================\n// Implementation\n// ============================================================================\n\nfunction getBashOutputPath(id: string): string {\n\treturn join(getProcessWorkRun(getAgentDir(), \"outputs\", \"bash\").path, `pi-bash-${id}.log`);\n}\n\n/**\n * Execute a bash command using custom BashOperations.\n * Used for remote execution (SSH, containers, etc.).\n */\nexport async function executeBashWithOperations(\n\tcommand: string,\n\tcwd: string,\n\toperations: BashOperations,\n\toptions?: BashExecutorOptions,\n): Promise<BashResult> {\n\tif (options?.enableGitFilter) {\n\t\tconst classification = classifyGitCommand(command, process.env);\n\t\tif (classification.eligible && classification.subcommand) {\n\t\t\tconst res = await executeFilteredGit(\n\t\t\t\tcwd,\n\t\t\t\tclassification.subcommand,\n\t\t\t\tclassification.globalOptions || [],\n\t\t\t\tclassification.subcommandArgs || [],\n\t\t\t\t{ signal: options.signal, timeout: options.timeout },\n\t\t\t);\n\t\t\tif (res.exitCode !== -100) {\n\t\t\t\tconst rawBytes = res.rawBytes ?? Buffer.from(res.rawOut, \"utf-8\");\n\t\t\t\t// The filter already spills oversized output to a temp file; reuse it\n\t\t\t\t// instead of materializing another full copy here.\n\t\t\t\tlet fullOutputPath = res.fullOutputPath;\n\t\t\t\tif (fullOutputPath === undefined && rawBytes.length > DEFAULT_MAX_BYTES) {\n\t\t\t\t\tconst id = randomBytes(8).toString(\"hex\");\n\t\t\t\t\tconst spillPath = getBashOutputPath(id);\n\t\t\t\t\tfullOutputPath = spillPath;\n\t\t\t\t\t// On stream failure (e.g. disk full), drop the advertised path instead of\n\t\t\t\t\t// pointing at a partial/missing file.\n\t\t\t\t\tconst tempFileStream = createSafeWriteStream(spillPath, () => {\n\t\t\t\t\t\tfullOutputPath = undefined;\n\t\t\t\t\t});\n\t\t\t\t\ttempFileStream.write(rawBytes);\n\t\t\t\t\t// Await the flush so the returned path points at a COMPLETE file, not one mid-write.\n\t\t\t\t\tawait endWriteStream(tempFileStream);\n\t\t\t\t}\n\t\t\t\toptions.onChunk?.(res.output);\n\t\t\t\treturn {\n\t\t\t\t\toutput: res.output,\n\t\t\t\t\texitCode: res.exitCode,\n\t\t\t\t\tcancelled: options.signal?.aborted ?? false,\n\t\t\t\t\ttruncated: res.fullOutputPath !== undefined || rawBytes.length > DEFAULT_MAX_BYTES,\n\t\t\t\t\tfullOutputPath,\n\t\t\t\t};\n\t\t\t}\n\t\t}\n\t}\n\n\tconst outputChunks: string[] = [];\n\tlet outputBytes = 0;\n\tconst maxOutputBytes = DEFAULT_MAX_BYTES * 2;\n\n\tlet tempFilePath: string | undefined;\n\tlet tempFileStream: WriteStream | undefined;\n\tlet totalBytes = 0;\n\n\tconst ensureTempFile = () => {\n\t\tif (tempFilePath) {\n\t\t\treturn;\n\t\t}\n\t\tconst id = randomBytes(8).toString(\"hex\");\n\t\ttempFilePath = getBashOutputPath(id);\n\t\t// On stream failure (e.g. disk full), drop the artifact instead of\n\t\t// crashing the process; the rolling in-memory output is still returned.\n\t\ttempFileStream = createSafeWriteStream(tempFilePath, () => {\n\t\t\ttempFileStream = undefined;\n\t\t\ttempFilePath = undefined;\n\t\t});\n\t\tfor (const chunk of outputChunks) {\n\t\t\ttempFileStream.write(chunk);\n\t\t}\n\t};\n\n\tconst decoder = new TextDecoder();\n\n\tconst onData = (data: Buffer) => {\n\t\ttotalBytes += data.length;\n\n\t\t// Sanitize: strip ANSI, replace binary garbage, normalize newlines\n\t\tconst text = sanitizeBinaryOutput(stripAnsi(decoder.decode(data, { stream: true }))).replace(/\\r/g, \"\");\n\n\t\t// Start writing to temp file if exceeds threshold\n\t\tif (totalBytes > DEFAULT_MAX_BYTES) {\n\t\t\tensureTempFile();\n\t\t}\n\n\t\t// Guard writableEnded: custom BashOperations may deliver late onData\n\t\t// callbacks after an abort path has already ended the stream.\n\t\tif (tempFileStream && !tempFileStream.writableEnded) {\n\t\t\ttempFileStream.write(text);\n\t\t}\n\n\t\t// Keep rolling buffer\n\t\toutputChunks.push(text);\n\t\toutputBytes += text.length;\n\t\twhile (outputBytes > maxOutputBytes && outputChunks.length > 1) {\n\t\t\tconst removed = outputChunks.shift()!;\n\t\t\toutputBytes -= removed.length;\n\t\t}\n\n\t\t// Stream to callback\n\t\tif (options?.onChunk) {\n\t\t\toptions.onChunk(text);\n\t\t}\n\t};\n\n\ttry {\n\t\tconst result = await operations.exec(command, cwd, {\n\t\t\tonData,\n\t\t\tsignal: options?.signal,\n\t\t\ttimeout: options?.timeout,\n\t\t});\n\n\t\tconst fullOutput = outputChunks.join(\"\");\n\t\tconst truncationResult = truncateTail(fullOutput);\n\t\tif (truncationResult.truncated) {\n\t\t\tensureTempFile();\n\t\t}\n\t\tif (tempFileStream) {\n\t\t\t// Await the flush so fullOutputPath refers to a fully-written file on return, not one still\n\t\t\t// draining its buffer — otherwise a fast reader can see partial/empty content.\n\t\t\tawait endWriteStream(tempFileStream);\n\t\t}\n\t\tconst cancelled = options?.signal?.aborted ?? false;\n\n\t\treturn {\n\t\t\toutput: truncationResult.truncated ? truncationResult.content : fullOutput,\n\t\t\texitCode: cancelled ? undefined : (result.exitCode ?? undefined),\n\t\t\tcancelled,\n\t\t\ttruncated: truncationResult.truncated,\n\t\t\tfullOutputPath: tempFilePath,\n\t\t};\n\t} catch (err) {\n\t\t// Check if it was an abort\n\t\tif (options?.signal?.aborted) {\n\t\t\tconst fullOutput = outputChunks.join(\"\");\n\t\t\tconst truncationResult = truncateTail(fullOutput);\n\t\t\tif (truncationResult.truncated) {\n\t\t\t\tensureTempFile();\n\t\t\t}\n\t\t\tif (tempFileStream) {\n\t\t\t\tawait endWriteStream(tempFileStream);\n\t\t\t}\n\t\t\treturn {\n\t\t\t\toutput: truncationResult.truncated ? truncationResult.content : fullOutput,\n\t\t\t\texitCode: undefined,\n\t\t\t\tcancelled: true,\n\t\t\t\ttruncated: truncationResult.truncated,\n\t\t\t\tfullOutputPath: tempFilePath,\n\t\t\t};\n\t\t}\n\n\t\tif (tempFileStream) {\n\t\t\tawait endWriteStream(tempFileStream);\n\t\t}\n\n\t\t// The silence watchdog (see tools/bash.ts) throws a raw `silence:<secs>` sentinel.\n\t\t// Map it to the same user-facing message the interactive bash tool shows, instead of\n\t\t// leaking the sentinel to callers of this path (interactive !cmd, RPC).\n\t\tif (err instanceof Error && err.message.startsWith(\"silence:\")) {\n\t\t\tconst secs = err.message.split(\":\")[1];\n\t\t\tthrow new Error(\n\t\t\t\t`Command killed after ${secs}s of silence (no output). If the command is legitimately quiet for long stretches, re-run it with an explicit timeout.`,\n\t\t\t);\n\t\t}\n\n\t\tthrow err;\n\t}\n}\n"]}
@@ -5,8 +5,11 @@ export interface ContextStoreRetentionOptions {
5
5
  now?: number;
6
6
  }
7
7
  export interface ContextStoreRetentionLease {
8
+ readonly gcDir: string;
9
+ readonly artifactsDir: string;
8
10
  release(): void;
9
11
  }
10
- export declare function pruneContextStoreRoot(rootDir: string, options?: ContextStoreRetentionOptions): string[];
12
+ export declare function pruneContextStores(agentDir: string, tenant: "gc" | "artifacts", options?: ContextStoreRetentionOptions): string[];
13
+ export declare function getContextStoreDir(agentDir: string, tenant: "gc" | "artifacts", sessionId: string): string;
11
14
  export declare function acquireContextStoreRetention(agentDir: string, sessionId: string, options?: ContextStoreRetentionOptions): ContextStoreRetentionLease;
12
15
  //# sourceMappingURL=context-store-retention.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"context-store-retention.d.ts","sourceRoot":"","sources":["../../../src/core/context/context-store-retention.ts"],"names":[],"mappings":"AAsBA,MAAM,WAAW,4BAA4B;IAC5C,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,GAAG,CAAC,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,0BAA0B;IAC1C,OAAO,IAAI,IAAI,CAAC;CAChB;AAuFD,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,GAAE,4BAAiC,GAAG,MAAM,EAAE,CAyB3G;AAED,wBAAgB,4BAA4B,CAC3C,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,MAAM,EACjB,OAAO,GAAE,4BAAiC,GACxC,0BAA0B,CAkC5B","sourcesContent":["import { randomUUID } from \"node:crypto\";\nimport {\n\texistsSync,\n\tlstatSync,\n\tmkdirSync,\n\treaddirSync,\n\treadFileSync,\n\trmdirSync,\n\trmSync,\n\tstatSync,\n\tunlinkSync,\n\twriteFileSync,\n} from \"node:fs\";\nimport { dirname, join } from \"node:path\";\n\nconst LEASES_DIRECTORY = \".leases\";\nconst ACTIVE_MARKER_PREFIX = \"active-\";\nconst ACTIVE_MARKER_SUFFIX = \".json\";\nconst DEFAULT_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1000;\nconst DEFAULT_MAX_SESSION_DIRS = 64;\nconst DEFAULT_MAX_TOTAL_BYTES = 512 * 1024 * 1024;\n\nexport interface ContextStoreRetentionOptions {\n\tmaxAgeMs?: number;\n\tmaxSessionDirs?: number;\n\tmaxTotalBytes?: number;\n\tnow?: number;\n}\n\nexport interface ContextStoreRetentionLease {\n\trelease(): void;\n}\n\ninterface SessionDirectory {\n\tpath: string;\n\tmtimeMs: number;\n\tbytes: number;\n\tactive: boolean;\n}\n\nfunction positiveLimit(value: number | undefined, fallback: number): number {\n\treturn value !== undefined && Number.isFinite(value) && value >= 0 ? Math.floor(value) : fallback;\n}\n\nfunction directoryBytes(path: string): number {\n\tlet total = 0;\n\tfor (const entry of readdirSync(path, { withFileTypes: true })) {\n\t\tconst child = join(path, entry.name);\n\t\ttry {\n\t\t\tif (entry.isDirectory()) total += directoryBytes(child);\n\t\t\telse total += lstatSync(child).size;\n\t\t} catch {\n\t\t\t// Concurrent cleanup or an unreadable entry is ignored by best-effort retention.\n\t\t}\n\t}\n\treturn total;\n}\n\nfunction markerProcessIsAlive(path: string): boolean {\n\ttry {\n\t\tconst parsed = JSON.parse(readFileSync(path, \"utf-8\")) as { pid?: unknown };\n\t\tif (typeof parsed.pid !== \"number\" || !Number.isSafeInteger(parsed.pid) || parsed.pid <= 0) return false;\n\t\ttry {\n\t\t\tprocess.kill(parsed.pid, 0);\n\t\t\treturn true;\n\t\t} catch (error) {\n\t\t\treturn (error as NodeJS.ErrnoException).code === \"EPERM\";\n\t\t}\n\t} catch {\n\t\treturn false;\n\t}\n}\n\nfunction cleanupStaleLeases(rootDir: string): void {\n\tconst leasesRoot = join(rootDir, LEASES_DIRECTORY);\n\tif (!existsSync(leasesRoot)) return;\n\tfor (const entry of readdirSync(leasesRoot, { withFileTypes: true })) {\n\t\tif (!entry.isDirectory()) continue;\n\t\tconst leaseDir = join(leasesRoot, entry.name);\n\t\ttry {\n\t\t\tfor (const marker of readdirSync(leaseDir)) {\n\t\t\t\tconst markerPath = join(leaseDir, marker);\n\t\t\t\tif (!markerProcessIsAlive(markerPath)) unlinkSync(markerPath);\n\t\t\t}\n\t\t\trmdirSync(leaseDir);\n\t\t} catch {\n\t\t\t// A live marker or concurrent lease keeps the directory in place.\n\t\t}\n\t}\n}\n\nfunction inspectSessionDirectories(rootDir: string): SessionDirectory[] {\n\tif (!existsSync(rootDir)) return [];\n\tcleanupStaleLeases(rootDir);\n\tconst directories: SessionDirectory[] = [];\n\tfor (const entry of readdirSync(rootDir, { withFileTypes: true })) {\n\t\tif (!entry.isDirectory() || entry.name === LEASES_DIRECTORY) continue;\n\t\tconst path = join(rootDir, entry.name);\n\t\ttry {\n\t\t\tconst leaseDir = join(rootDir, LEASES_DIRECTORY, entry.name);\n\t\t\tconst markerPaths = existsSync(leaseDir)\n\t\t\t\t? readdirSync(leaseDir)\n\t\t\t\t\t\t.filter((name) => name.startsWith(ACTIVE_MARKER_PREFIX) && name.endsWith(ACTIVE_MARKER_SUFFIX))\n\t\t\t\t\t\t.map((name) => join(leaseDir, name))\n\t\t\t\t: [];\n\t\t\tdirectories.push({\n\t\t\t\tpath,\n\t\t\t\tmtimeMs: Math.max(statSync(path).mtimeMs, ...markerPaths.map((markerPath) => statSync(markerPath).mtimeMs)),\n\t\t\t\tbytes: directoryBytes(path),\n\t\t\t\tactive: markerPaths.some(markerProcessIsAlive),\n\t\t\t});\n\t\t} catch {\n\t\t\t// Ignore a directory that disappeared or became unreadable during the scan.\n\t\t}\n\t}\n\treturn directories;\n}\n\nexport function pruneContextStoreRoot(rootDir: string, options: ContextStoreRetentionOptions = {}): string[] {\n\tconst now = options.now ?? Date.now();\n\tconst maxAgeMs = positiveLimit(options.maxAgeMs, DEFAULT_MAX_AGE_MS);\n\tconst maxSessionDirs = positiveLimit(options.maxSessionDirs, DEFAULT_MAX_SESSION_DIRS);\n\tconst maxTotalBytes = positiveLimit(options.maxTotalBytes, DEFAULT_MAX_TOTAL_BYTES);\n\tconst directories = inspectSessionDirectories(rootDir);\n\tconst removed: string[] = [];\n\tlet retainedCount = directories.length;\n\tlet retainedBytes = directories.reduce((sum, directory) => sum + directory.bytes, 0);\n\tfor (const directory of directories.sort((left, right) => left.mtimeMs - right.mtimeMs)) {\n\t\tif (directory.active) continue;\n\t\tconst expired = now - directory.mtimeMs > maxAgeMs;\n\t\tconst overCount = retainedCount > maxSessionDirs;\n\t\tconst overBytes = retainedBytes > maxTotalBytes;\n\t\tif (!expired && !overCount && !overBytes) continue;\n\t\ttry {\n\t\t\trmSync(directory.path, { recursive: true, force: true });\n\t\t\tremoved.push(directory.path);\n\t\t\tretainedCount--;\n\t\t\tretainedBytes -= directory.bytes;\n\t\t} catch {\n\t\t\t// Retention is best-effort and must never block a live session.\n\t\t}\n\t}\n\treturn removed;\n}\n\nexport function acquireContextStoreRetention(\n\tagentDir: string,\n\tsessionId: string,\n\toptions: ContextStoreRetentionOptions = {},\n): ContextStoreRetentionLease {\n\tif (!/^[A-Za-z0-9_-]+$/.test(sessionId)) return { release: () => {} };\n\tconst roots = [join(agentDir, \"context-gc\"), join(agentDir, \"context-artifacts\")];\n\tconst markerPaths: string[] = [];\n\tfor (const root of roots) {\n\t\tconst sessionDir = join(root, sessionId);\n\t\ttry {\n\t\t\tmkdirSync(sessionDir, { recursive: true });\n\t\t\tconst leaseDir = join(root, LEASES_DIRECTORY, sessionId);\n\t\t\tmkdirSync(leaseDir, { recursive: true });\n\t\t\tconst markerPath = join(\n\t\t\t\tleaseDir,\n\t\t\t\t`${ACTIVE_MARKER_PREFIX}${process.pid}-${randomUUID()}${ACTIVE_MARKER_SUFFIX}`,\n\t\t\t);\n\t\t\twriteFileSync(markerPath, JSON.stringify({ pid: process.pid, touchedAt: Date.now() }), \"utf-8\");\n\t\t\tmarkerPaths.push(markerPath);\n\t\t\tpruneContextStoreRoot(root, options);\n\t\t} catch {\n\t\t\t// Retention is best-effort; the caller can still use its normal store path.\n\t\t}\n\t}\n\tlet released = false;\n\treturn {\n\t\trelease(): void {\n\t\t\tif (released) return;\n\t\t\treleased = true;\n\t\t\tfor (const markerPath of markerPaths) {\n\t\t\t\ttry {\n\t\t\t\t\tunlinkSync(markerPath);\n\t\t\t\t\trmdirSync(dirname(markerPath));\n\t\t\t\t} catch {}\n\t\t\t}\n\t\t},\n\t};\n}\n"]}
1
+ {"version":3,"file":"context-store-retention.d.ts","sourceRoot":"","sources":["../../../src/core/context/context-store-retention.ts"],"names":[],"mappings":"AAYA,MAAM,WAAW,4BAA4B;IAC5C,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,GAAG,CAAC,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,0BAA0B;IAC1C,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,OAAO,IAAI,IAAI,CAAC;CAChB;AAWD,wBAAgB,kBAAkB,CACjC,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,IAAI,GAAG,WAAW,EAC1B,OAAO,GAAE,4BAAiC,GACxC,MAAM,EAAE,CAEV;AAED,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,GAAG,WAAW,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAE1G;AAED,wBAAgB,4BAA4B,CAC3C,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,MAAM,EACjB,OAAO,GAAE,4BAAiC,GACxC,0BAA0B,CA4B5B","sourcesContent":["import {\n\tacquireWorkRun,\n\tgetWorkRunDir,\n\tpruneWorkTenant,\n\ttype WorkRetentionOptions,\n\ttype WorkRunLease,\n} from \"../../utils/work-directory.ts\";\n\nconst DEFAULT_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1000;\nconst DEFAULT_MAX_SESSION_DIRS = 64;\nconst DEFAULT_MAX_TOTAL_BYTES = 512 * 1024 * 1024;\n\nexport interface ContextStoreRetentionOptions {\n\tmaxAgeMs?: number;\n\tmaxSessionDirs?: number;\n\tmaxTotalBytes?: number;\n\tnow?: number;\n}\n\nexport interface ContextStoreRetentionLease {\n\treadonly gcDir: string;\n\treadonly artifactsDir: string;\n\trelease(): void;\n}\n\nfunction toWorkRetention(options: ContextStoreRetentionOptions): WorkRetentionOptions {\n\treturn {\n\t\tmaxAgeMs: options.maxAgeMs ?? DEFAULT_MAX_AGE_MS,\n\t\tmaxRuns: options.maxSessionDirs ?? DEFAULT_MAX_SESSION_DIRS,\n\t\tmaxTotalBytes: options.maxTotalBytes ?? DEFAULT_MAX_TOTAL_BYTES,\n\t\tnow: options.now,\n\t};\n}\n\nexport function pruneContextStores(\n\tagentDir: string,\n\ttenant: \"gc\" | \"artifacts\",\n\toptions: ContextStoreRetentionOptions = {},\n): string[] {\n\treturn pruneWorkTenant(agentDir, \"context\", tenant, toWorkRetention(options));\n}\n\nexport function getContextStoreDir(agentDir: string, tenant: \"gc\" | \"artifacts\", sessionId: string): string {\n\treturn getWorkRunDir(agentDir, \"context\", tenant, sessionId);\n}\n\nexport function acquireContextStoreRetention(\n\tagentDir: string,\n\tsessionId: string,\n\toptions: ContextStoreRetentionOptions = {},\n): ContextStoreRetentionLease {\n\tconst retention = toWorkRetention(options);\n\tconst leases: WorkRunLease[] = [];\n\ttry {\n\t\tconst gc = acquireWorkRun({ agentDir, category: \"context\", tenant: \"gc\", runId: sessionId, retention });\n\t\tleases.push(gc);\n\t\tconst artifacts = acquireWorkRun({\n\t\t\tagentDir,\n\t\t\tcategory: \"context\",\n\t\t\ttenant: \"artifacts\",\n\t\t\trunId: sessionId,\n\t\t\tretention,\n\t\t});\n\t\tleases.push(artifacts);\n\t\tlet released = false;\n\t\treturn {\n\t\t\tgcDir: gc.path,\n\t\t\tartifactsDir: artifacts.path,\n\t\t\trelease(): void {\n\t\t\t\tif (released) return;\n\t\t\t\treleased = true;\n\t\t\t\tfor (const lease of leases) lease.release();\n\t\t\t},\n\t\t};\n\t} catch (error) {\n\t\tfor (const lease of leases) lease.release();\n\t\tthrow error;\n\t}\n}\n"]}
@@ -1,161 +1,52 @@
1
- import { randomUUID } from "node:crypto";
2
- import { existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, rmdirSync, rmSync, statSync, unlinkSync, writeFileSync, } from "node:fs";
3
- import { dirname, join } from "node:path";
4
- const LEASES_DIRECTORY = ".leases";
5
- const ACTIVE_MARKER_PREFIX = "active-";
6
- const ACTIVE_MARKER_SUFFIX = ".json";
1
+ import { acquireWorkRun, getWorkRunDir, pruneWorkTenant, } from "../../utils/work-directory.js";
7
2
  const DEFAULT_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1000;
8
3
  const DEFAULT_MAX_SESSION_DIRS = 64;
9
4
  const DEFAULT_MAX_TOTAL_BYTES = 512 * 1024 * 1024;
10
- function positiveLimit(value, fallback) {
11
- return value !== undefined && Number.isFinite(value) && value >= 0 ? Math.floor(value) : fallback;
12
- }
13
- function directoryBytes(path) {
14
- let total = 0;
15
- for (const entry of readdirSync(path, { withFileTypes: true })) {
16
- const child = join(path, entry.name);
17
- try {
18
- if (entry.isDirectory())
19
- total += directoryBytes(child);
20
- else
21
- total += lstatSync(child).size;
22
- }
23
- catch {
24
- // Concurrent cleanup or an unreadable entry is ignored by best-effort retention.
25
- }
26
- }
27
- return total;
28
- }
29
- function markerProcessIsAlive(path) {
30
- try {
31
- const parsed = JSON.parse(readFileSync(path, "utf-8"));
32
- if (typeof parsed.pid !== "number" || !Number.isSafeInteger(parsed.pid) || parsed.pid <= 0)
33
- return false;
34
- try {
35
- process.kill(parsed.pid, 0);
36
- return true;
37
- }
38
- catch (error) {
39
- return error.code === "EPERM";
40
- }
41
- }
42
- catch {
43
- return false;
44
- }
45
- }
46
- function cleanupStaleLeases(rootDir) {
47
- const leasesRoot = join(rootDir, LEASES_DIRECTORY);
48
- if (!existsSync(leasesRoot))
49
- return;
50
- for (const entry of readdirSync(leasesRoot, { withFileTypes: true })) {
51
- if (!entry.isDirectory())
52
- continue;
53
- const leaseDir = join(leasesRoot, entry.name);
54
- try {
55
- for (const marker of readdirSync(leaseDir)) {
56
- const markerPath = join(leaseDir, marker);
57
- if (!markerProcessIsAlive(markerPath))
58
- unlinkSync(markerPath);
59
- }
60
- rmdirSync(leaseDir);
61
- }
62
- catch {
63
- // A live marker or concurrent lease keeps the directory in place.
64
- }
65
- }
5
+ function toWorkRetention(options) {
6
+ return {
7
+ maxAgeMs: options.maxAgeMs ?? DEFAULT_MAX_AGE_MS,
8
+ maxRuns: options.maxSessionDirs ?? DEFAULT_MAX_SESSION_DIRS,
9
+ maxTotalBytes: options.maxTotalBytes ?? DEFAULT_MAX_TOTAL_BYTES,
10
+ now: options.now,
11
+ };
66
12
  }
67
- function inspectSessionDirectories(rootDir) {
68
- if (!existsSync(rootDir))
69
- return [];
70
- cleanupStaleLeases(rootDir);
71
- const directories = [];
72
- for (const entry of readdirSync(rootDir, { withFileTypes: true })) {
73
- if (!entry.isDirectory() || entry.name === LEASES_DIRECTORY)
74
- continue;
75
- const path = join(rootDir, entry.name);
76
- try {
77
- const leaseDir = join(rootDir, LEASES_DIRECTORY, entry.name);
78
- const markerPaths = existsSync(leaseDir)
79
- ? readdirSync(leaseDir)
80
- .filter((name) => name.startsWith(ACTIVE_MARKER_PREFIX) && name.endsWith(ACTIVE_MARKER_SUFFIX))
81
- .map((name) => join(leaseDir, name))
82
- : [];
83
- directories.push({
84
- path,
85
- mtimeMs: Math.max(statSync(path).mtimeMs, ...markerPaths.map((markerPath) => statSync(markerPath).mtimeMs)),
86
- bytes: directoryBytes(path),
87
- active: markerPaths.some(markerProcessIsAlive),
88
- });
89
- }
90
- catch {
91
- // Ignore a directory that disappeared or became unreadable during the scan.
92
- }
93
- }
94
- return directories;
13
+ export function pruneContextStores(agentDir, tenant, options = {}) {
14
+ return pruneWorkTenant(agentDir, "context", tenant, toWorkRetention(options));
95
15
  }
96
- export function pruneContextStoreRoot(rootDir, options = {}) {
97
- const now = options.now ?? Date.now();
98
- const maxAgeMs = positiveLimit(options.maxAgeMs, DEFAULT_MAX_AGE_MS);
99
- const maxSessionDirs = positiveLimit(options.maxSessionDirs, DEFAULT_MAX_SESSION_DIRS);
100
- const maxTotalBytes = positiveLimit(options.maxTotalBytes, DEFAULT_MAX_TOTAL_BYTES);
101
- const directories = inspectSessionDirectories(rootDir);
102
- const removed = [];
103
- let retainedCount = directories.length;
104
- let retainedBytes = directories.reduce((sum, directory) => sum + directory.bytes, 0);
105
- for (const directory of directories.sort((left, right) => left.mtimeMs - right.mtimeMs)) {
106
- if (directory.active)
107
- continue;
108
- const expired = now - directory.mtimeMs > maxAgeMs;
109
- const overCount = retainedCount > maxSessionDirs;
110
- const overBytes = retainedBytes > maxTotalBytes;
111
- if (!expired && !overCount && !overBytes)
112
- continue;
113
- try {
114
- rmSync(directory.path, { recursive: true, force: true });
115
- removed.push(directory.path);
116
- retainedCount--;
117
- retainedBytes -= directory.bytes;
118
- }
119
- catch {
120
- // Retention is best-effort and must never block a live session.
121
- }
122
- }
123
- return removed;
16
+ export function getContextStoreDir(agentDir, tenant, sessionId) {
17
+ return getWorkRunDir(agentDir, "context", tenant, sessionId);
124
18
  }
125
19
  export function acquireContextStoreRetention(agentDir, sessionId, options = {}) {
126
- if (!/^[A-Za-z0-9_-]+$/.test(sessionId))
127
- return { release: () => { } };
128
- const roots = [join(agentDir, "context-gc"), join(agentDir, "context-artifacts")];
129
- const markerPaths = [];
130
- for (const root of roots) {
131
- const sessionDir = join(root, sessionId);
132
- try {
133
- mkdirSync(sessionDir, { recursive: true });
134
- const leaseDir = join(root, LEASES_DIRECTORY, sessionId);
135
- mkdirSync(leaseDir, { recursive: true });
136
- const markerPath = join(leaseDir, `${ACTIVE_MARKER_PREFIX}${process.pid}-${randomUUID()}${ACTIVE_MARKER_SUFFIX}`);
137
- writeFileSync(markerPath, JSON.stringify({ pid: process.pid, touchedAt: Date.now() }), "utf-8");
138
- markerPaths.push(markerPath);
139
- pruneContextStoreRoot(root, options);
140
- }
141
- catch {
142
- // Retention is best-effort; the caller can still use its normal store path.
143
- }
20
+ const retention = toWorkRetention(options);
21
+ const leases = [];
22
+ try {
23
+ const gc = acquireWorkRun({ agentDir, category: "context", tenant: "gc", runId: sessionId, retention });
24
+ leases.push(gc);
25
+ const artifacts = acquireWorkRun({
26
+ agentDir,
27
+ category: "context",
28
+ tenant: "artifacts",
29
+ runId: sessionId,
30
+ retention,
31
+ });
32
+ leases.push(artifacts);
33
+ let released = false;
34
+ return {
35
+ gcDir: gc.path,
36
+ artifactsDir: artifacts.path,
37
+ release() {
38
+ if (released)
39
+ return;
40
+ released = true;
41
+ for (const lease of leases)
42
+ lease.release();
43
+ },
44
+ };
45
+ }
46
+ catch (error) {
47
+ for (const lease of leases)
48
+ lease.release();
49
+ throw error;
144
50
  }
145
- let released = false;
146
- return {
147
- release() {
148
- if (released)
149
- return;
150
- released = true;
151
- for (const markerPath of markerPaths) {
152
- try {
153
- unlinkSync(markerPath);
154
- rmdirSync(dirname(markerPath));
155
- }
156
- catch { }
157
- }
158
- },
159
- };
160
51
  }
161
52
  //# sourceMappingURL=context-store-retention.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"context-store-retention.js","sourceRoot":"","sources":["../../../src/core/context/context-store-retention.ts"],"names":[],"mappings":"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","sourcesContent":["import { randomUUID } from \"node:crypto\";\nimport {\n\texistsSync,\n\tlstatSync,\n\tmkdirSync,\n\treaddirSync,\n\treadFileSync,\n\trmdirSync,\n\trmSync,\n\tstatSync,\n\tunlinkSync,\n\twriteFileSync,\n} from \"node:fs\";\nimport { dirname, join } from \"node:path\";\n\nconst LEASES_DIRECTORY = \".leases\";\nconst ACTIVE_MARKER_PREFIX = \"active-\";\nconst ACTIVE_MARKER_SUFFIX = \".json\";\nconst DEFAULT_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1000;\nconst DEFAULT_MAX_SESSION_DIRS = 64;\nconst DEFAULT_MAX_TOTAL_BYTES = 512 * 1024 * 1024;\n\nexport interface ContextStoreRetentionOptions {\n\tmaxAgeMs?: number;\n\tmaxSessionDirs?: number;\n\tmaxTotalBytes?: number;\n\tnow?: number;\n}\n\nexport interface ContextStoreRetentionLease {\n\trelease(): void;\n}\n\ninterface SessionDirectory {\n\tpath: string;\n\tmtimeMs: number;\n\tbytes: number;\n\tactive: boolean;\n}\n\nfunction positiveLimit(value: number | undefined, fallback: number): number {\n\treturn value !== undefined && Number.isFinite(value) && value >= 0 ? Math.floor(value) : fallback;\n}\n\nfunction directoryBytes(path: string): number {\n\tlet total = 0;\n\tfor (const entry of readdirSync(path, { withFileTypes: true })) {\n\t\tconst child = join(path, entry.name);\n\t\ttry {\n\t\t\tif (entry.isDirectory()) total += directoryBytes(child);\n\t\t\telse total += lstatSync(child).size;\n\t\t} catch {\n\t\t\t// Concurrent cleanup or an unreadable entry is ignored by best-effort retention.\n\t\t}\n\t}\n\treturn total;\n}\n\nfunction markerProcessIsAlive(path: string): boolean {\n\ttry {\n\t\tconst parsed = JSON.parse(readFileSync(path, \"utf-8\")) as { pid?: unknown };\n\t\tif (typeof parsed.pid !== \"number\" || !Number.isSafeInteger(parsed.pid) || parsed.pid <= 0) return false;\n\t\ttry {\n\t\t\tprocess.kill(parsed.pid, 0);\n\t\t\treturn true;\n\t\t} catch (error) {\n\t\t\treturn (error as NodeJS.ErrnoException).code === \"EPERM\";\n\t\t}\n\t} catch {\n\t\treturn false;\n\t}\n}\n\nfunction cleanupStaleLeases(rootDir: string): void {\n\tconst leasesRoot = join(rootDir, LEASES_DIRECTORY);\n\tif (!existsSync(leasesRoot)) return;\n\tfor (const entry of readdirSync(leasesRoot, { withFileTypes: true })) {\n\t\tif (!entry.isDirectory()) continue;\n\t\tconst leaseDir = join(leasesRoot, entry.name);\n\t\ttry {\n\t\t\tfor (const marker of readdirSync(leaseDir)) {\n\t\t\t\tconst markerPath = join(leaseDir, marker);\n\t\t\t\tif (!markerProcessIsAlive(markerPath)) unlinkSync(markerPath);\n\t\t\t}\n\t\t\trmdirSync(leaseDir);\n\t\t} catch {\n\t\t\t// A live marker or concurrent lease keeps the directory in place.\n\t\t}\n\t}\n}\n\nfunction inspectSessionDirectories(rootDir: string): SessionDirectory[] {\n\tif (!existsSync(rootDir)) return [];\n\tcleanupStaleLeases(rootDir);\n\tconst directories: SessionDirectory[] = [];\n\tfor (const entry of readdirSync(rootDir, { withFileTypes: true })) {\n\t\tif (!entry.isDirectory() || entry.name === LEASES_DIRECTORY) continue;\n\t\tconst path = join(rootDir, entry.name);\n\t\ttry {\n\t\t\tconst leaseDir = join(rootDir, LEASES_DIRECTORY, entry.name);\n\t\t\tconst markerPaths = existsSync(leaseDir)\n\t\t\t\t? readdirSync(leaseDir)\n\t\t\t\t\t\t.filter((name) => name.startsWith(ACTIVE_MARKER_PREFIX) && name.endsWith(ACTIVE_MARKER_SUFFIX))\n\t\t\t\t\t\t.map((name) => join(leaseDir, name))\n\t\t\t\t: [];\n\t\t\tdirectories.push({\n\t\t\t\tpath,\n\t\t\t\tmtimeMs: Math.max(statSync(path).mtimeMs, ...markerPaths.map((markerPath) => statSync(markerPath).mtimeMs)),\n\t\t\t\tbytes: directoryBytes(path),\n\t\t\t\tactive: markerPaths.some(markerProcessIsAlive),\n\t\t\t});\n\t\t} catch {\n\t\t\t// Ignore a directory that disappeared or became unreadable during the scan.\n\t\t}\n\t}\n\treturn directories;\n}\n\nexport function pruneContextStoreRoot(rootDir: string, options: ContextStoreRetentionOptions = {}): string[] {\n\tconst now = options.now ?? Date.now();\n\tconst maxAgeMs = positiveLimit(options.maxAgeMs, DEFAULT_MAX_AGE_MS);\n\tconst maxSessionDirs = positiveLimit(options.maxSessionDirs, DEFAULT_MAX_SESSION_DIRS);\n\tconst maxTotalBytes = positiveLimit(options.maxTotalBytes, DEFAULT_MAX_TOTAL_BYTES);\n\tconst directories = inspectSessionDirectories(rootDir);\n\tconst removed: string[] = [];\n\tlet retainedCount = directories.length;\n\tlet retainedBytes = directories.reduce((sum, directory) => sum + directory.bytes, 0);\n\tfor (const directory of directories.sort((left, right) => left.mtimeMs - right.mtimeMs)) {\n\t\tif (directory.active) continue;\n\t\tconst expired = now - directory.mtimeMs > maxAgeMs;\n\t\tconst overCount = retainedCount > maxSessionDirs;\n\t\tconst overBytes = retainedBytes > maxTotalBytes;\n\t\tif (!expired && !overCount && !overBytes) continue;\n\t\ttry {\n\t\t\trmSync(directory.path, { recursive: true, force: true });\n\t\t\tremoved.push(directory.path);\n\t\t\tretainedCount--;\n\t\t\tretainedBytes -= directory.bytes;\n\t\t} catch {\n\t\t\t// Retention is best-effort and must never block a live session.\n\t\t}\n\t}\n\treturn removed;\n}\n\nexport function acquireContextStoreRetention(\n\tagentDir: string,\n\tsessionId: string,\n\toptions: ContextStoreRetentionOptions = {},\n): ContextStoreRetentionLease {\n\tif (!/^[A-Za-z0-9_-]+$/.test(sessionId)) return { release: () => {} };\n\tconst roots = [join(agentDir, \"context-gc\"), join(agentDir, \"context-artifacts\")];\n\tconst markerPaths: string[] = [];\n\tfor (const root of roots) {\n\t\tconst sessionDir = join(root, sessionId);\n\t\ttry {\n\t\t\tmkdirSync(sessionDir, { recursive: true });\n\t\t\tconst leaseDir = join(root, LEASES_DIRECTORY, sessionId);\n\t\t\tmkdirSync(leaseDir, { recursive: true });\n\t\t\tconst markerPath = join(\n\t\t\t\tleaseDir,\n\t\t\t\t`${ACTIVE_MARKER_PREFIX}${process.pid}-${randomUUID()}${ACTIVE_MARKER_SUFFIX}`,\n\t\t\t);\n\t\t\twriteFileSync(markerPath, JSON.stringify({ pid: process.pid, touchedAt: Date.now() }), \"utf-8\");\n\t\t\tmarkerPaths.push(markerPath);\n\t\t\tpruneContextStoreRoot(root, options);\n\t\t} catch {\n\t\t\t// Retention is best-effort; the caller can still use its normal store path.\n\t\t}\n\t}\n\tlet released = false;\n\treturn {\n\t\trelease(): void {\n\t\t\tif (released) return;\n\t\t\treleased = true;\n\t\t\tfor (const markerPath of markerPaths) {\n\t\t\t\ttry {\n\t\t\t\t\tunlinkSync(markerPath);\n\t\t\t\t\trmdirSync(dirname(markerPath));\n\t\t\t\t} catch {}\n\t\t\t}\n\t\t},\n\t};\n}\n"]}
1
+ {"version":3,"file":"context-store-retention.js","sourceRoot":"","sources":["../../../src/core/context/context-store-retention.ts"],"names":[],"mappings":"AAAA,OAAO,EACN,cAAc,EACd,aAAa,EACb,eAAe,GAGf,MAAM,+BAA+B,CAAC;AAEvC,MAAM,kBAAkB,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;AACpD,MAAM,wBAAwB,GAAG,EAAE,CAAC;AACpC,MAAM,uBAAuB,GAAG,GAAG,GAAG,IAAI,GAAG,IAAI,CAAC;AAelD,SAAS,eAAe,CAAC,OAAqC,EAAwB;IACrF,OAAO;QACN,QAAQ,EAAE,OAAO,CAAC,QAAQ,IAAI,kBAAkB;QAChD,OAAO,EAAE,OAAO,CAAC,cAAc,IAAI,wBAAwB;QAC3D,aAAa,EAAE,OAAO,CAAC,aAAa,IAAI,uBAAuB;QAC/D,GAAG,EAAE,OAAO,CAAC,GAAG;KAChB,CAAC;AAAA,CACF;AAED,MAAM,UAAU,kBAAkB,CACjC,QAAgB,EAChB,MAA0B,EAC1B,OAAO,GAAiC,EAAE,EAC/B;IACX,OAAO,eAAe,CAAC,QAAQ,EAAE,SAAS,EAAE,MAAM,EAAE,eAAe,CAAC,OAAO,CAAC,CAAC,CAAC;AAAA,CAC9E;AAED,MAAM,UAAU,kBAAkB,CAAC,QAAgB,EAAE,MAA0B,EAAE,SAAiB,EAAU;IAC3G,OAAO,aAAa,CAAC,QAAQ,EAAE,SAAS,EAAE,MAAM,EAAE,SAAS,CAAC,CAAC;AAAA,CAC7D;AAED,MAAM,UAAU,4BAA4B,CAC3C,QAAgB,EAChB,SAAiB,EACjB,OAAO,GAAiC,EAAE,EACb;IAC7B,MAAM,SAAS,GAAG,eAAe,CAAC,OAAO,CAAC,CAAC;IAC3C,MAAM,MAAM,GAAmB,EAAE,CAAC;IAClC,IAAI,CAAC;QACJ,MAAM,EAAE,GAAG,cAAc,CAAC,EAAE,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,CAAC,CAAC;QACxG,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QAChB,MAAM,SAAS,GAAG,cAAc,CAAC;YAChC,QAAQ;YACR,QAAQ,EAAE,SAAS;YACnB,MAAM,EAAE,WAAW;YACnB,KAAK,EAAE,SAAS;YAChB,SAAS;SACT,CAAC,CAAC;QACH,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;QACvB,IAAI,QAAQ,GAAG,KAAK,CAAC;QACrB,OAAO;YACN,KAAK,EAAE,EAAE,CAAC,IAAI;YACd,YAAY,EAAE,SAAS,CAAC,IAAI;YAC5B,OAAO,GAAS;gBACf,IAAI,QAAQ;oBAAE,OAAO;gBACrB,QAAQ,GAAG,IAAI,CAAC;gBAChB,KAAK,MAAM,KAAK,IAAI,MAAM;oBAAE,KAAK,CAAC,OAAO,EAAE,CAAC;YAAA,CAC5C;SACD,CAAC;IACH,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QAChB,KAAK,MAAM,KAAK,IAAI,MAAM;YAAE,KAAK,CAAC,OAAO,EAAE,CAAC;QAC5C,MAAM,KAAK,CAAC;IACb,CAAC;AAAA,CACD","sourcesContent":["import {\n\tacquireWorkRun,\n\tgetWorkRunDir,\n\tpruneWorkTenant,\n\ttype WorkRetentionOptions,\n\ttype WorkRunLease,\n} from \"../../utils/work-directory.ts\";\n\nconst DEFAULT_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1000;\nconst DEFAULT_MAX_SESSION_DIRS = 64;\nconst DEFAULT_MAX_TOTAL_BYTES = 512 * 1024 * 1024;\n\nexport interface ContextStoreRetentionOptions {\n\tmaxAgeMs?: number;\n\tmaxSessionDirs?: number;\n\tmaxTotalBytes?: number;\n\tnow?: number;\n}\n\nexport interface ContextStoreRetentionLease {\n\treadonly gcDir: string;\n\treadonly artifactsDir: string;\n\trelease(): void;\n}\n\nfunction toWorkRetention(options: ContextStoreRetentionOptions): WorkRetentionOptions {\n\treturn {\n\t\tmaxAgeMs: options.maxAgeMs ?? DEFAULT_MAX_AGE_MS,\n\t\tmaxRuns: options.maxSessionDirs ?? DEFAULT_MAX_SESSION_DIRS,\n\t\tmaxTotalBytes: options.maxTotalBytes ?? DEFAULT_MAX_TOTAL_BYTES,\n\t\tnow: options.now,\n\t};\n}\n\nexport function pruneContextStores(\n\tagentDir: string,\n\ttenant: \"gc\" | \"artifacts\",\n\toptions: ContextStoreRetentionOptions = {},\n): string[] {\n\treturn pruneWorkTenant(agentDir, \"context\", tenant, toWorkRetention(options));\n}\n\nexport function getContextStoreDir(agentDir: string, tenant: \"gc\" | \"artifacts\", sessionId: string): string {\n\treturn getWorkRunDir(agentDir, \"context\", tenant, sessionId);\n}\n\nexport function acquireContextStoreRetention(\n\tagentDir: string,\n\tsessionId: string,\n\toptions: ContextStoreRetentionOptions = {},\n): ContextStoreRetentionLease {\n\tconst retention = toWorkRetention(options);\n\tconst leases: WorkRunLease[] = [];\n\ttry {\n\t\tconst gc = acquireWorkRun({ agentDir, category: \"context\", tenant: \"gc\", runId: sessionId, retention });\n\t\tleases.push(gc);\n\t\tconst artifacts = acquireWorkRun({\n\t\t\tagentDir,\n\t\t\tcategory: \"context\",\n\t\t\ttenant: \"artifacts\",\n\t\t\trunId: sessionId,\n\t\t\tretention,\n\t\t});\n\t\tleases.push(artifacts);\n\t\tlet released = false;\n\t\treturn {\n\t\t\tgcDir: gc.path,\n\t\t\tartifactsDir: artifacts.path,\n\t\t\trelease(): void {\n\t\t\t\tif (released) return;\n\t\t\t\treleased = true;\n\t\t\t\tfor (const lease of leases) lease.release();\n\t\t\t},\n\t\t};\n\t} catch (error) {\n\t\tfor (const lease of leases) lease.release();\n\t\tthrow error;\n\t}\n}\n"]}