@wascartech/devbot 0.0.0-stage → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (376) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +638 -2
  3. package/dist/access-grant-store.js +99 -0
  4. package/dist/access-grant-store.js.map +1 -0
  5. package/dist/access-resolve.js +11 -0
  6. package/dist/access-resolve.js.map +1 -0
  7. package/dist/access-rule-store.js +99 -0
  8. package/dist/access-rule-store.js.map +1 -0
  9. package/dist/admin/access-routes.js +73 -0
  10. package/dist/admin/access-routes.js.map +1 -0
  11. package/dist/admin/access-rule-routes.js +107 -0
  12. package/dist/admin/access-rule-routes.js.map +1 -0
  13. package/dist/admin/custom-repos-routes.js +93 -0
  14. package/dist/admin/custom-repos-routes.js.map +1 -0
  15. package/dist/admin/env-routes.js +74 -0
  16. package/dist/admin/env-routes.js.map +1 -0
  17. package/dist/admin/global-settings-routes.js +254 -0
  18. package/dist/admin/global-settings-routes.js.map +1 -0
  19. package/dist/admin/mcp-cleanup.js +12 -0
  20. package/dist/admin/mcp-cleanup.js.map +1 -0
  21. package/dist/admin/mcp-routes.js +169 -0
  22. package/dist/admin/mcp-routes.js.map +1 -0
  23. package/dist/admin/query-params.js +80 -0
  24. package/dist/admin/query-params.js.map +1 -0
  25. package/dist/admin/routes.js +150 -0
  26. package/dist/admin/routes.js.map +1 -0
  27. package/dist/admin/run-history-routes.js +44 -0
  28. package/dist/admin/run-history-routes.js.map +1 -0
  29. package/dist/admin/run-routes.js +99 -0
  30. package/dist/admin/run-routes.js.map +1 -0
  31. package/dist/admin/settings-routes.js +119 -0
  32. package/dist/admin/settings-routes.js.map +1 -0
  33. package/dist/admin/stats-routes.js +51 -0
  34. package/dist/admin/stats-routes.js.map +1 -0
  35. package/dist/admin/task-routes.js +213 -0
  36. package/dist/admin/task-routes.js.map +1 -0
  37. package/dist/admin/test-routes.js +38 -0
  38. package/dist/admin/test-routes.js.map +1 -0
  39. package/dist/admin/volume-routes.js +82 -0
  40. package/dist/admin/volume-routes.js.map +1 -0
  41. package/dist/admin/webhook-routes.js +174 -0
  42. package/dist/admin/webhook-routes.js.map +1 -0
  43. package/dist/agents.js +48 -0
  44. package/dist/agents.js.map +1 -0
  45. package/dist/assignee-value.js +6 -0
  46. package/dist/assignee-value.js.map +1 -0
  47. package/dist/attachments.js +76 -0
  48. package/dist/attachments.js.map +1 -0
  49. package/dist/auto-commands.js +88 -0
  50. package/dist/auto-commands.js.map +1 -0
  51. package/dist/auto-features.js +32 -0
  52. package/dist/auto-features.js.map +1 -0
  53. package/dist/base-branch-store.js +40 -0
  54. package/dist/base-branch-store.js.map +1 -0
  55. package/dist/base-branches.js +9 -0
  56. package/dist/base-branches.js.map +1 -0
  57. package/dist/build-workflow.js +116 -0
  58. package/dist/build-workflow.js.map +1 -0
  59. package/dist/change-request-lifecycle.js +16 -0
  60. package/dist/change-request-lifecycle.js.map +1 -0
  61. package/dist/change-request-store.js +72 -0
  62. package/dist/change-request-store.js.map +1 -0
  63. package/dist/ci-failure.js +80 -0
  64. package/dist/ci-failure.js.map +1 -0
  65. package/dist/cli.js +3 -0
  66. package/dist/cli.js.map +1 -0
  67. package/dist/config.js +17 -0
  68. package/dist/config.js.map +1 -0
  69. package/dist/conflict-fix.js +178 -0
  70. package/dist/conflict-fix.js.map +1 -0
  71. package/dist/cookie.js +70 -0
  72. package/dist/cookie.js.map +1 -0
  73. package/dist/crypto-util.js +40 -0
  74. package/dist/crypto-util.js.map +1 -0
  75. package/dist/custom-repos-store.js +90 -0
  76. package/dist/custom-repos-store.js.map +1 -0
  77. package/dist/custom-repos-sync.js +118 -0
  78. package/dist/custom-repos-sync.js.map +1 -0
  79. package/dist/db.js +12 -0
  80. package/dist/db.js.map +1 -0
  81. package/dist/devbot-scripts.js +18 -0
  82. package/dist/devbot-scripts.js.map +1 -0
  83. package/dist/devcontainer.js +141 -0
  84. package/dist/devcontainer.js.map +1 -0
  85. package/dist/env-config-store.js +132 -0
  86. package/dist/env-config-store.js.map +1 -0
  87. package/dist/error-log-store.js +193 -0
  88. package/dist/error-log-store.js.map +1 -0
  89. package/dist/git.js +605 -0
  90. package/dist/git.js.map +1 -0
  91. package/dist/global-queue.js +127 -0
  92. package/dist/global-queue.js.map +1 -0
  93. package/dist/global-settings-store.js +307 -0
  94. package/dist/global-settings-store.js.map +1 -0
  95. package/dist/help.js +41 -0
  96. package/dist/help.js.map +1 -0
  97. package/dist/http-csp.js +15 -0
  98. package/dist/http-csp.js.map +1 -0
  99. package/dist/http-util.js +40 -0
  100. package/dist/http-util.js.map +1 -0
  101. package/dist/idempotency.js +50 -0
  102. package/dist/idempotency.js.map +1 -0
  103. package/dist/lifecycle.js +234 -0
  104. package/dist/lifecycle.js.map +1 -0
  105. package/dist/logger.js +33 -0
  106. package/dist/logger.js.map +1 -0
  107. package/dist/lsp-servers.js +81 -0
  108. package/dist/lsp-servers.js.map +1 -0
  109. package/dist/mcp/auth.js +62 -0
  110. package/dist/mcp/auth.js.map +1 -0
  111. package/dist/mcp/effective-mcp.js +42 -0
  112. package/dist/mcp/effective-mcp.js.map +1 -0
  113. package/dist/mcp/oauth-refresh.js +138 -0
  114. package/dist/mcp/oauth-refresh.js.map +1 -0
  115. package/dist/mcp/peer-sync.js +109 -0
  116. package/dist/mcp/peer-sync.js.map +1 -0
  117. package/dist/mcp/registry.js +54 -0
  118. package/dist/mcp/registry.js.map +1 -0
  119. package/dist/mcp/runner-bridge.js +93 -0
  120. package/dist/mcp/runner-bridge.js.map +1 -0
  121. package/dist/mcp/server.js +173 -0
  122. package/dist/mcp/server.js.map +1 -0
  123. package/dist/mcp/tools/_helpers.js +88 -0
  124. package/dist/mcp/tools/_helpers.js.map +1 -0
  125. package/dist/mcp/tools/attachments.js +117 -0
  126. package/dist/mcp/tools/attachments.js.map +1 -0
  127. package/dist/mcp/tools/cicd.js +105 -0
  128. package/dist/mcp/tools/cicd.js.map +1 -0
  129. package/dist/mcp/tools/discovery.js +34 -0
  130. package/dist/mcp/tools/discovery.js.map +1 -0
  131. package/dist/mcp/tools/issues.js +56 -0
  132. package/dist/mcp/tools/issues.js.map +1 -0
  133. package/dist/mcp/tools/merge_requests.js +67 -0
  134. package/dist/mcp/tools/merge_requests.js.map +1 -0
  135. package/dist/mcp/tools/metadata.js +127 -0
  136. package/dist/mcp/tools/metadata.js.map +1 -0
  137. package/dist/mcp/tools/peer_sync.js +43 -0
  138. package/dist/mcp/tools/peer_sync.js.map +1 -0
  139. package/dist/mcp/tools/repository.js +178 -0
  140. package/dist/mcp/tools/repository.js.map +1 -0
  141. package/dist/mcp/tools/work_tracking.js +125 -0
  142. package/dist/mcp/tools/work_tracking.js.map +1 -0
  143. package/dist/mcp/tools/workitems.js +30 -0
  144. package/dist/mcp/tools/workitems.js.map +1 -0
  145. package/dist/mcp/volume-files.js +187 -0
  146. package/dist/mcp/volume-files.js.map +1 -0
  147. package/dist/mcp-auth-store.js +134 -0
  148. package/dist/mcp-auth-store.js.map +1 -0
  149. package/dist/mcp-config-store.js +83 -0
  150. package/dist/mcp-config-store.js.map +1 -0
  151. package/dist/mention.js +512 -0
  152. package/dist/mention.js.map +1 -0
  153. package/dist/migrate.js +73 -0
  154. package/dist/migrate.js.map +1 -0
  155. package/dist/mr-assignee.js +71 -0
  156. package/dist/mr-assignee.js.map +1 -0
  157. package/dist/mr-content.js +90 -0
  158. package/dist/mr-content.js.map +1 -0
  159. package/dist/oauth-routes.js +54 -0
  160. package/dist/oauth-routes.js.map +1 -0
  161. package/dist/onboarding-allowlist.js +32 -0
  162. package/dist/onboarding-allowlist.js.map +1 -0
  163. package/dist/opencode.js +291 -0
  164. package/dist/opencode.js.map +1 -0
  165. package/dist/outbound-webhook-store.js +229 -0
  166. package/dist/outbound-webhook-store.js.map +1 -0
  167. package/dist/outbound-webhook.js +314 -0
  168. package/dist/outbound-webhook.js.map +1 -0
  169. package/dist/path-sandbox.js +200 -0
  170. package/dist/path-sandbox.js.map +1 -0
  171. package/dist/platform/git-auth.js +34 -0
  172. package/dist/platform/git-auth.js.map +1 -0
  173. package/dist/platform/gitealike/attachments.js +62 -0
  174. package/dist/platform/gitealike/attachments.js.map +1 -0
  175. package/dist/platform/gitealike/base.js +935 -0
  176. package/dist/platform/gitealike/base.js.map +1 -0
  177. package/dist/platform/gitealike/forgejo.js +16 -0
  178. package/dist/platform/gitealike/forgejo.js.map +1 -0
  179. package/dist/platform/gitealike/gitea.js +16 -0
  180. package/dist/platform/gitealike/gitea.js.map +1 -0
  181. package/dist/platform/gitealike/http.js +152 -0
  182. package/dist/platform/gitealike/http.js.map +1 -0
  183. package/dist/platform/gitealike/schemas.js +284 -0
  184. package/dist/platform/gitealike/schemas.js.map +1 -0
  185. package/dist/platform/github/attachments.js +52 -0
  186. package/dist/platform/github/attachments.js.map +1 -0
  187. package/dist/platform/github/http.js +153 -0
  188. package/dist/platform/github/http.js.map +1 -0
  189. package/dist/platform/github/index.js +936 -0
  190. package/dist/platform/github/index.js.map +1 -0
  191. package/dist/platform/github/schemas.js +308 -0
  192. package/dist/platform/github/schemas.js.map +1 -0
  193. package/dist/platform/github/zip.js +85 -0
  194. package/dist/platform/github/zip.js.map +1 -0
  195. package/dist/platform/gitlab/admin.js +105 -0
  196. package/dist/platform/gitlab/admin.js.map +1 -0
  197. package/dist/platform/gitlab/api.js +229 -0
  198. package/dist/platform/gitlab/api.js.map +1 -0
  199. package/dist/platform/gitlab/attachments.js +79 -0
  200. package/dist/platform/gitlab/attachments.js.map +1 -0
  201. package/dist/platform/gitlab/http.js +165 -0
  202. package/dist/platform/gitlab/http.js.map +1 -0
  203. package/dist/platform/gitlab/index.js +630 -0
  204. package/dist/platform/gitlab/index.js.map +1 -0
  205. package/dist/platform/gitlab/oauth.js +58 -0
  206. package/dist/platform/gitlab/oauth.js.map +1 -0
  207. package/dist/platform/gitlab/read.js +182 -0
  208. package/dist/platform/gitlab/read.js.map +1 -0
  209. package/dist/platform/gitlab/schemas-read.js +274 -0
  210. package/dist/platform/gitlab/schemas-read.js.map +1 -0
  211. package/dist/platform/gitlab/schemas.js +251 -0
  212. package/dist/platform/gitlab/schemas.js.map +1 -0
  213. package/dist/platform/gitlab/signature.js +52 -0
  214. package/dist/platform/gitlab/signature.js.map +1 -0
  215. package/dist/platform/index.js +38 -0
  216. package/dist/platform/index.js.map +1 -0
  217. package/dist/platform/port.js +26 -0
  218. package/dist/platform/port.js.map +1 -0
  219. package/dist/platform/types.js +80 -0
  220. package/dist/platform/types.js.map +1 -0
  221. package/dist/podman.js +119 -0
  222. package/dist/podman.js.map +1 -0
  223. package/dist/project-sync.js +69 -0
  224. package/dist/project-sync.js.map +1 -0
  225. package/dist/prompt.js +47 -0
  226. package/dist/prompt.js.map +1 -0
  227. package/dist/queue-store.js +236 -0
  228. package/dist/queue-store.js.map +1 -0
  229. package/dist/queue.js +129 -0
  230. package/dist/queue.js.map +1 -0
  231. package/dist/redact.js +23 -0
  232. package/dist/redact.js.map +1 -0
  233. package/dist/run-files.js +297 -0
  234. package/dist/run-files.js.map +1 -0
  235. package/dist/run-filters.js +54 -0
  236. package/dist/run-filters.js.map +1 -0
  237. package/dist/run-history-store.js +229 -0
  238. package/dist/run-history-store.js.map +1 -0
  239. package/dist/run-log-sink.js +270 -0
  240. package/dist/run-log-sink.js.map +1 -0
  241. package/dist/run-log.js +97 -0
  242. package/dist/run-log.js.map +1 -0
  243. package/dist/run-usage.js +29 -0
  244. package/dist/run-usage.js.map +1 -0
  245. package/dist/run-view.js +112 -0
  246. package/dist/run-view.js.map +1 -0
  247. package/dist/runner.js +1094 -0
  248. package/dist/runner.js.map +1 -0
  249. package/dist/runtime-apply.js +26 -0
  250. package/dist/runtime-apply.js.map +1 -0
  251. package/dist/runtime-settings.js +71 -0
  252. package/dist/runtime-settings.js.map +1 -0
  253. package/dist/runtime-state.js +19 -0
  254. package/dist/runtime-state.js.map +1 -0
  255. package/dist/server.js +119 -0
  256. package/dist/server.js.map +1 -0
  257. package/dist/settings-store.js +320 -0
  258. package/dist/settings-store.js.map +1 -0
  259. package/dist/status.js +121 -0
  260. package/dist/status.js.map +1 -0
  261. package/dist/stop.js +56 -0
  262. package/dist/stop.js.map +1 -0
  263. package/dist/sweeps.js +87 -0
  264. package/dist/sweeps.js.map +1 -0
  265. package/dist/task-log.js +279 -0
  266. package/dist/task-log.js.map +1 -0
  267. package/dist/task-view.js +62 -0
  268. package/dist/task-view.js.map +1 -0
  269. package/dist/token-store.js +68 -0
  270. package/dist/token-store.js.map +1 -0
  271. package/dist/types/access.js +74 -0
  272. package/dist/types/access.js.map +1 -0
  273. package/dist/types/custom-repos.js +28 -0
  274. package/dist/types/custom-repos.js.map +1 -0
  275. package/dist/types/env-config.js +25 -0
  276. package/dist/types/env-config.js.map +1 -0
  277. package/dist/types/env.js +190 -0
  278. package/dist/types/env.js.map +1 -0
  279. package/dist/types/global-settings.js +178 -0
  280. package/dist/types/global-settings.js.map +1 -0
  281. package/dist/types/lsp-servers.js +145 -0
  282. package/dist/types/lsp-servers.js.map +1 -0
  283. package/dist/types/mcp-config.js +77 -0
  284. package/dist/types/mcp-config.js.map +1 -0
  285. package/dist/types/opencode.js +59 -0
  286. package/dist/types/opencode.js.map +1 -0
  287. package/dist/types/outbound-webhook.js +56 -0
  288. package/dist/types/outbound-webhook.js.map +1 -0
  289. package/dist/types/project-settings.js +114 -0
  290. package/dist/types/project-settings.js.map +1 -0
  291. package/dist/types/provider-registry.js +37 -0
  292. package/dist/types/provider-registry.js.map +1 -0
  293. package/dist/types/runtime-settings.js +78 -0
  294. package/dist/types/runtime-settings.js.map +1 -0
  295. package/dist/usage-stats.js +159 -0
  296. package/dist/usage-stats.js.map +1 -0
  297. package/dist/version.js +19 -0
  298. package/dist/version.js.map +1 -0
  299. package/dist/volume-gc.js +193 -0
  300. package/dist/volume-gc.js.map +1 -0
  301. package/dist/volume-manage.js +191 -0
  302. package/dist/volume-manage.js.map +1 -0
  303. package/dist/volume-store.js +122 -0
  304. package/dist/volume-store.js.map +1 -0
  305. package/dist/webhook.js +1106 -0
  306. package/dist/webhook.js.map +1 -0
  307. package/dist/work-item-auto-store.js +75 -0
  308. package/dist/work-item-auto-store.js.map +1 -0
  309. package/dist/work-item-branch-store.js +37 -0
  310. package/dist/work-item-branch-store.js.map +1 -0
  311. package/migrations/0001_initial.sql +59 -0
  312. package/migrations/0002_queue_status.sql +15 -0
  313. package/migrations/0003_work_item_base_branch.sql +14 -0
  314. package/migrations/0004_project_settings.sql +15 -0
  315. package/migrations/0005_global_settings.sql +34 -0
  316. package/migrations/0006_model_variants.sql +8 -0
  317. package/migrations/0007_container_error_logs.sql +21 -0
  318. package/migrations/0008_project_access_grants.sql +16 -0
  319. package/migrations/0009_drop_imported_legacy.sql +4 -0
  320. package/migrations/0010_container_error_log_model.sql +6 -0
  321. package/migrations/0011_container_error_log_title.sql +5 -0
  322. package/migrations/0012_pending_task_cancel.sql +6 -0
  323. package/migrations/0013_builder_image.sql +6 -0
  324. package/migrations/0014_build_workflow.sql +7 -0
  325. package/migrations/0015_access_rules.sql +24 -0
  326. package/migrations/0016_trigger_policy.sql +17 -0
  327. package/migrations/0017_attachment_upload.sql +8 -0
  328. package/migrations/0018_work_item_branch.sql +11 -0
  329. package/migrations/0019_auto_push_mr.sql +18 -0
  330. package/migrations/0020_outbound_webhooks.sql +47 -0
  331. package/migrations/0021_runtime_settings.sql +12 -0
  332. package/migrations/0022_auto_resolve_conflicts.sql +17 -0
  333. package/migrations/0023_ci_fail_auto_report.sql +27 -0
  334. package/migrations/0024_work_item_auto_override.sql +22 -0
  335. package/migrations/0025_run_history.sql +37 -0
  336. package/migrations/0026_run_history_task_seq.sql +6 -0
  337. package/migrations/0027_custom_repos.sql +11 -0
  338. package/migrations/0028_run_usage.sql +14 -0
  339. package/migrations/0029_drop_ocr_langs.sql +6 -0
  340. package/migrations/0030_mr_assignee.sql +9 -0
  341. package/migrations/0031_lsp_servers.sql +14 -0
  342. package/opencode-config/managed/devbot-agent-policy.md +61 -0
  343. package/opencode-config/managed/opencode.json +77 -0
  344. package/opencode-config/opencode.json +211 -0
  345. package/opencode-config/plugins/devbot-path-guard.js +224 -0
  346. package/package.json +157 -4
  347. package/public/assets/DashboardSidebarCollapse-rIuzCmVm.js +1 -0
  348. package/public/assets/DashboardToolbar-DFCUqZ4Z.js +1 -0
  349. package/public/assets/ErrorLogModal.vue_vue_type_script_setup_true_lang-BCyNIf1E.js +1 -0
  350. package/public/assets/ErrorLogsPage-DyYK2Rop.js +1 -0
  351. package/public/assets/LogSlideover.vue_vue_type_script_setup_true_lang-33xie7TE.js +1 -0
  352. package/public/assets/OverviewPage-CFfa0uwg.js +1 -0
  353. package/public/assets/PageGrid-CBROIU87.js +1 -0
  354. package/public/assets/PeriodRangePicker.vue_vue_type_script_setup_true_lang-DiFUNem7.js +1 -0
  355. package/public/assets/ProjectDetailPage-CreyvUit.js +1 -0
  356. package/public/assets/ProjectsPage-CFx8-elJ.js +1 -0
  357. package/public/assets/RunDetailSlideover.vue_vue_type_script_setup_true_lang-DopYYmCT.js +1 -0
  358. package/public/assets/RunsPage-CRnnXU4P.js +1 -0
  359. package/public/assets/Select-Bvx32Diz.js +1 -0
  360. package/public/assets/SelectMenu-BjMxc4VB.js +1 -0
  361. package/public/assets/SettingsPage-Bb6LM3wO.js +60 -0
  362. package/public/assets/TimeCell.vue_vue_type_script_setup_true_lang-DW_UB0Fu.js +1 -0
  363. package/public/assets/UsagePage-BLMDEsv-.js +332 -0
  364. package/public/assets/UsagePage-DodEeAjB.css +1 -0
  365. package/public/assets/VolumesPage-CxcLc3L2.js +1 -0
  366. package/public/assets/WebhookConfigPanel.vue_vue_type_script_setup_true_lang-B2V2GF6b.js +1 -0
  367. package/public/assets/WorkItemCell.vue_vue_type_script_setup_true_lang-BQn6vTjd.js +1 -0
  368. package/public/assets/index-BMbMibyb.js +4 -0
  369. package/public/assets/index-C5RBBrPj.js +30 -0
  370. package/public/assets/index-CgX29UgC.css +1 -0
  371. package/public/assets/usage-format-jsb31_O8.js +1 -0
  372. package/public/assets/useManagedProjects-8rtjRqVJ.js +1 -0
  373. package/public/assets/useRouteTab-DfwrcMic.js +1 -0
  374. package/public/favicon.ico +0 -0
  375. package/public/favicon.png +0 -0
  376. package/public/index.html +15 -0
@@ -0,0 +1,193 @@
1
+ import { config } from "./config.js";
2
+ import { runtime } from "./runtime-state.js";
3
+ import { log } from "./logger.js";
4
+ import { podman } from "./podman.js";
5
+ import { getPlatform } from "./platform/index.js";
6
+ import { volumeName, volumeExists, podmanVolumeName, xdgVolumeName } from "./git.js";
7
+ import { destroyWorkItemVolume } from "./runner.js";
8
+ import { listInactiveVolumes, listTrackedVolumes, deleteVolumeRow, recordOrphanVolume, realWorkItemIds, } from "./volume-store.js";
9
+ // Sweep all volumes eligible for purging. A row is eligible when EITHER:
10
+ // - it has been inactive for at least VOLUME_GC_TTL_MS (the normal TTL path),
11
+ // or
12
+ // - its work item is closed or deleted (liveness path — a deleted work item
13
+ // can never run again, and a closed one shouldn't keep a volume, matching
14
+ // the close hook). This reclaims volumes promptly instead of waiting out
15
+ // the TTL.
16
+ // Orphans (project_id === 0) have no thread to query and only qualify via TTL.
17
+ //
18
+ // For each eligible row:
19
+ // - if the volume is already gone (close/reset removed it), just drop the
20
+ // ledger row;
21
+ // - else destroyWorkItemVolume (force-stop in-flight containers + volume rm);
22
+ // - on success, post a top-level purge note to the work item when the row
23
+ // carries a real (projectId, iid) and the item still exists — reconciled
24
+ // orphans (0/0) and deleted items are purged silently;
25
+ // - drop the ledger row regardless of note success.
26
+ // Each row is isolated in try/catch so one failure doesn't abort the sweep.
27
+ // DRY_RUN: log intent, skip rm + note. GC disabled: no-op.
28
+ export async function purgeInactiveVolumes() {
29
+ if (!runtime().volumeGcEnabled)
30
+ return;
31
+ const inactive = new Set(listInactiveVolumes(runtime().volumeGcTtlMs).map((r) => r.work_item_id));
32
+ const tracked = listTrackedVolumes();
33
+ // Classify every tracked row: TTL-inactive and/or closed/deleted. The state
34
+ // read is per-row and bounded (one GET each, once a day); a failure on one
35
+ // row leaves it on the TTL path instead of aborting the sweep.
36
+ const eligible = [];
37
+ for (const row of tracked) {
38
+ const ttl = inactive.has(row.work_item_id);
39
+ // Orphans carry no project/iid → TTL only, no API call.
40
+ if (row.project_id === 0) {
41
+ if (ttl)
42
+ eligible.push({ row, deleted: false });
43
+ continue;
44
+ }
45
+ // undefined = the check itself failed (network/5xx). Fail closed: fall back
46
+ // to the TTL path so an unverifiable read never purges the volume. null
47
+ // is an authoritative 404 (deleted).
48
+ let state;
49
+ try {
50
+ state = await getPlatform().workItems.getWorkItemStateOrNull(row.project_id, row.work_item_iid);
51
+ }
52
+ catch (e) {
53
+ log.warn("volume gc liveness check failed", { workItemId: row.work_item_id, err: e });
54
+ state = undefined;
55
+ }
56
+ if (state === "closed" || state === null) {
57
+ eligible.push({ row, deleted: state === null });
58
+ }
59
+ else if (ttl) {
60
+ eligible.push({ row, deleted: false });
61
+ }
62
+ }
63
+ if (eligible.length === 0)
64
+ return;
65
+ log.info("volume gc sweep", { candidates: eligible.length, tracked: tracked.length, ttlMs: runtime().volumeGcTtlMs });
66
+ for (const { row, deleted } of eligible) {
67
+ try {
68
+ await purgeOne(row, deleted);
69
+ }
70
+ catch (e) {
71
+ log.warn("volume gc row failed", { workItemId: row.work_item_id, err: e });
72
+ }
73
+ }
74
+ }
75
+ async function purgeOne(row, deleted = false) {
76
+ const { work_item_id: workItemId, project_id: projectId, work_item_iid: workItemNumber, last_run_at: lastRunAt } = row;
77
+ // Already gone (close/reset beat the sweep): just clean the ledger row. The
78
+ // sibling volumes (nested-Podman store, xdg config/data) are removed
79
+ // alongside, if they exist.
80
+ if (!(await volumeExists(volumeName(workItemId)))) {
81
+ for (const sibling of [podmanVolumeName(workItemId), xdgVolumeName(workItemId)]) {
82
+ if (await volumeExists(sibling)) {
83
+ await podman(["volume", "rm", sibling]).catch(() => { });
84
+ }
85
+ }
86
+ deleteVolumeRow(workItemId);
87
+ return;
88
+ }
89
+ if (config.DRY_RUN) {
90
+ log.debug("DRY_RUN — would purge volume", { workItemId, vol: volumeName(workItemId) });
91
+ return;
92
+ }
93
+ const result = await destroyWorkItemVolume(workItemId);
94
+ if (!result.ok) {
95
+ log.warn("volume gc destroy failed", { workItemId, message: result.message });
96
+ return;
97
+ }
98
+ // Post a note only for tracked volumes (projectId != 0) whose work item still
99
+ // exists. Reconciled orphans (projectId === 0) carry no resolvable thread and
100
+ // deleted work items have nowhere to post → purge silently.
101
+ if (projectId !== 0 && !deleted) {
102
+ const days = Math.max(1, Math.round((Date.now() / 1000 - lastRunAt) / (24 * 60 * 60)));
103
+ const body = `🧹 Volume purged after ${days} day(s) of inactivity. Mention @devbot to resume.`;
104
+ await getPlatform().workItems.postComment(projectId, workItemNumber, body).catch((e) => log.warn("volume gc note failed", { workItemId, err: e }));
105
+ }
106
+ deleteVolumeRow(workItemId);
107
+ log.info("volume gc purged", { workItemId, orphan: projectId === 0, deleted });
108
+ }
109
+ // Parse a `devbot-wi-<id>`, `devbot-wi-<id>-podman`, or `devbot-wi-<id>-xdg`
110
+ // volume name into its work item id. Returns null for anything that isn't one
111
+ // of those shapes (other names, empty id, non-numeric id, unknown suffixes,
112
+ // id <= 0). Exported as a pure helper so the reconciler's suffix handling is
113
+ // unit-testable. The `-podman` suffix is the private nested-Podman store
114
+ // (created when dev containers are enabled) and `-xdg` is the out-of-tree
115
+ // opencode config/data volume; both map back to the same ledger row as the
116
+ // workspace volume.
117
+ export function parseVolumeWorkItemId(name, prefix) {
118
+ if (!name.startsWith(prefix))
119
+ return null;
120
+ const rest = name.slice(prefix.length);
121
+ const m = /^(\d+)(?:-podman|-xdg)?$/.exec(rest);
122
+ if (!m)
123
+ return null;
124
+ const id = Number(m[1]);
125
+ if (!Number.isFinite(id) || id <= 0)
126
+ return null;
127
+ return id;
128
+ }
129
+ // Parse podman's volume CreatedAt into unix seconds. podman reports ISO-8601
130
+ // with a numeric offset and nanosecond precision (e.g.
131
+ // "2026-07-30T23:02:46.330803742+02:00"), which `new Date` parses fine. Returns
132
+ // null for anything unparseable so the caller can fall back to `now` (a volume
133
+ // with an unknown age must not be trusted as fresh, but it also must not crash
134
+ // the reconcile). Exported pure for unit testing.
135
+ export function parseVolumeCreatedAt(raw) {
136
+ const d = new Date(raw);
137
+ if (Number.isNaN(d.getTime()))
138
+ return null;
139
+ return Math.floor(d.getTime() / 1000);
140
+ }
141
+ // One-time reconciliation at startup: discover devbot-wi-* volumes that exist
142
+ // in podman but have no ledger row (created before the tracking table existed,
143
+ // or via a path that bypassed recordVolume) and insert orphan placeholders so
144
+ // the TTL sweep can eventually free them. The volume's ACTUAL CreatedAt is
145
+ // recorded (not reconcile time) so a months-old orphan is immediately TTL-
146
+ // eligible rather than getting a fresh clock on every reconcile. project_id=0
147
+ // → silent purge later. Existing real rows are left untouched (the orphan
148
+ // upsert only rewrites project_id=0 rows). Cheap (one podman volume ls) and
149
+ // runs only at startup. Best-effort: a podman failure just skips reconcile.
150
+ export async function reconcileVolumes() {
151
+ if (!runtime().volumeGcEnabled)
152
+ return;
153
+ const prefix = config.INSTANCE_ID ? `${config.INSTANCE_ID}-devbot-wi-` : "devbot-wi-";
154
+ // JSON so we get CreatedAt alongside Name; `--format {{.Name}}` omits it,
155
+ // which is what forced the old `now` stamp.
156
+ const ls = await podman(["volume", "ls", "--filter", `name=${prefix}`, "--format", "json"]);
157
+ if (ls.code !== 0) {
158
+ log.warn("volume reconcile: podman volume ls failed, skipping", { code: ls.code });
159
+ return;
160
+ }
161
+ let entries;
162
+ try {
163
+ entries = JSON.parse(ls.stdout);
164
+ if (!Array.isArray(entries))
165
+ throw new Error("not an array");
166
+ }
167
+ catch (e) {
168
+ log.warn("volume reconcile: could not parse podman volume ls json, skipping", { err: e });
169
+ return;
170
+ }
171
+ // Skip work items backed by a real row; revisit orphan rows so a previously
172
+ // mis-stamped orphan is self-healed with the volume's real CreatedAt.
173
+ const real = new Set(realWorkItemIds());
174
+ const nowSec = Math.floor(Date.now() / 1000);
175
+ let inserted = 0;
176
+ for (const entry of entries) {
177
+ const name = typeof entry.Name === "string" ? entry.Name : "";
178
+ if (!name)
179
+ continue;
180
+ const workItemId = parseVolumeWorkItemId(name, prefix);
181
+ if (workItemId === null)
182
+ continue;
183
+ if (real.has(workItemId))
184
+ continue;
185
+ const createdAt = typeof entry.CreatedAt === "string" ? parseVolumeCreatedAt(entry.CreatedAt) : null;
186
+ recordOrphanVolume(workItemId, createdAt ?? nowSec);
187
+ inserted++;
188
+ }
189
+ if (inserted > 0) {
190
+ log.info("volume reconcile: reconciled orphan placeholders", { count: inserted });
191
+ }
192
+ }
193
+ //# sourceMappingURL=volume-gc.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"volume-gc.js","sourceRoot":"","sources":["../src/volume-gc.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,191 @@
1
+ import { config } from "./config.js";
2
+ import { runtime } from "./runtime-state.js";
3
+ import { log } from "./logger.js";
4
+ import { podman } from "./podman.js";
5
+ import { getPlatform } from "./platform/index.js";
6
+ import { volumeName } from "./git.js";
7
+ import { parseVolumeWorkItemId } from "./volume-gc.js";
8
+ import { destroyWorkItemVolume } from "./runner.js";
9
+ import { getProject } from "./token-store.js";
10
+ import { db } from "./db.js";
11
+ import { getVolumeRow, listTrackedVolumes, deleteVolumeRow } from "./volume-store.js";
12
+ function volumePrefix() {
13
+ return config.INSTANCE_ID ? `${config.INSTANCE_ID}-devbot-wi-` : "devbot-wi-";
14
+ }
15
+ // Most recent run-history title for a work item, if the store has one. Best-
16
+ // effort: a pre-#61 DB without run_history still renders the row without a title.
17
+ function workItemTitle(workItemId) {
18
+ try {
19
+ const row = db
20
+ .prepare("SELECT work_item_title FROM run_history WHERE work_item_id = ? ORDER BY id DESC LIMIT 1")
21
+ .get(workItemId);
22
+ return row?.work_item_title ?? "";
23
+ }
24
+ catch {
25
+ return "";
26
+ }
27
+ }
28
+ // Map of workItemId -> set of podman volume suffixes present ("" = workspace,
29
+ // "-podman" = nested store, "-xdg" = opencode config/data). A failed ls returns
30
+ // null (podman unavailable), distinct from an empty map ("no volumes").
31
+ async function listPodmanVolumes() {
32
+ const prefix = volumePrefix();
33
+ const ls = await podman(["volume", "ls", "--filter", `name=${prefix}`, "--format", "json"]);
34
+ if (ls.code !== 0) {
35
+ log.warn("volume manage: podman volume ls failed", { code: ls.code });
36
+ return null;
37
+ }
38
+ let entries;
39
+ try {
40
+ entries = JSON.parse(ls.stdout);
41
+ if (!Array.isArray(entries))
42
+ throw new Error("not an array");
43
+ }
44
+ catch (e) {
45
+ log.warn("volume manage: could not parse podman volume ls json", { err: e });
46
+ return null;
47
+ }
48
+ const map = new Map();
49
+ for (const entry of entries) {
50
+ const name = typeof entry.Name === "string" ? entry.Name : "";
51
+ if (!name)
52
+ continue;
53
+ const id = parseVolumeWorkItemId(name, prefix);
54
+ if (id === null)
55
+ continue;
56
+ const suffix = name.slice(prefix.length + String(id).length);
57
+ let set = map.get(id);
58
+ if (!set) {
59
+ set = new Set();
60
+ map.set(id, set);
61
+ }
62
+ set.add(suffix);
63
+ }
64
+ return map;
65
+ }
66
+ // Classify a tracked row from its ledger timestamps. Orphans (projectId 0) have
67
+ // no resolvable work item — the sweep purges them silently on TTL only, so they
68
+ // report `unmapped` rather than a TTL countdown.
69
+ function classify(row, podmanExists, nowMs, ttlMs) {
70
+ if (!row.project_id) {
71
+ return { state: "unmapped", purgeEligibleAt: null, remainingMs: null };
72
+ }
73
+ if (!podmanExists) {
74
+ return { state: "missing", purgeEligibleAt: null, remainingMs: null };
75
+ }
76
+ const eligibleAt = (row.last_run_at + Math.ceil(ttlMs / 1000)) * 1000;
77
+ const remaining = Math.max(0, eligibleAt - nowMs);
78
+ return { state: remaining > 0 ? "expiring" : "eligible", purgeEligibleAt: eligibleAt, remainingMs: remaining };
79
+ }
80
+ // Build the full, unpaginated list. The admin route slices it for the page, so
81
+ // the total is exact without a second podman call.
82
+ export async function listVolumeViews() {
83
+ const ttlMs = runtime().volumeGcTtlMs;
84
+ const nowMs = Date.now();
85
+ const rows = listTrackedVolumes();
86
+ const podmanMap = await listPodmanVolumes();
87
+ const podmanAvailable = podmanMap !== null;
88
+ const podman = podmanMap ?? new Map();
89
+ const byId = new Map();
90
+ for (const row of rows) {
91
+ const id = row.work_item_id;
92
+ const suffixes = podman.get(id) ?? new Set();
93
+ const podmanExists = suffixes.has("") || suffixes.has("-podman") || suffixes.has("-xdg");
94
+ const cls = classify(row, podmanExists, nowMs, ttlMs);
95
+ const projectPath = getProject(row.project_id)?.projectPath ?? "";
96
+ const workItemUrl = row.project_id
97
+ ? getPlatform().links.workItemUrl(projectPath, row.work_item_iid)
98
+ : "";
99
+ byId.set(id, {
100
+ workItemId: id,
101
+ projectId: row.project_id,
102
+ workItemNumber: row.work_item_iid,
103
+ workItemTitle: workItemTitle(id),
104
+ workItemUrl,
105
+ projectPath,
106
+ createdAt: row.created_at,
107
+ lastRunAt: row.last_run_at,
108
+ purgeEligibleAt: cls.purgeEligibleAt,
109
+ remainingMs: cls.remainingMs,
110
+ tracked: true,
111
+ podmanExists,
112
+ volumes: {
113
+ workspace: suffixes.has(""),
114
+ podman: suffixes.has("-podman"),
115
+ xdg: suffixes.has("-xdg"),
116
+ },
117
+ state: cls.state,
118
+ });
119
+ }
120
+ // Podman-only volumes: present but with no ledger row (GC disabled, or a
121
+ // volume created by a path that bypassed recordVolume). Shown as `untracked`
122
+ // and silently deletable; the startup reconcile would only adopt them when GC
123
+ // is enabled.
124
+ for (const [id, suffixes] of podman) {
125
+ if (byId.has(id))
126
+ continue;
127
+ byId.set(id, {
128
+ workItemId: id,
129
+ projectId: 0,
130
+ workItemNumber: 0,
131
+ workItemTitle: "",
132
+ workItemUrl: "",
133
+ projectPath: "",
134
+ createdAt: 0,
135
+ lastRunAt: 0,
136
+ purgeEligibleAt: null,
137
+ remainingMs: null,
138
+ tracked: false,
139
+ podmanExists: true,
140
+ volumes: {
141
+ workspace: suffixes.has(""),
142
+ podman: suffixes.has("-podman"),
143
+ xdg: suffixes.has("-xdg"),
144
+ },
145
+ state: "untracked",
146
+ });
147
+ }
148
+ // Newest activity first; untracked rows (no timestamps) sort last, then by id.
149
+ const volumes = [...byId.values()].sort((a, b) => {
150
+ if (a.lastRunAt !== b.lastRunAt)
151
+ return b.lastRunAt - a.lastRunAt;
152
+ return b.workItemId - a.workItemId;
153
+ });
154
+ return { volumes, total: volumes.length, gcEnabled: runtime().volumeGcEnabled, ttlMs, podmanAvailable };
155
+ }
156
+ export function filterVolumeViews(views, f) {
157
+ return views.filter((v) => {
158
+ if (f.state !== undefined && v.state !== f.state)
159
+ return false;
160
+ if (f.projectId !== undefined && v.projectId !== f.projectId)
161
+ return false;
162
+ if (f.title !== undefined && !v.workItemTitle.toLowerCase().includes(f.title.toLowerCase()))
163
+ return false;
164
+ return true;
165
+ });
166
+ }
167
+ // Delete one work item's volumes from the WebUI. Reuses the /reset teardown and
168
+ // then drops the ledger row. When the ledger row maps to a real work item that
169
+ // is still open, posts a note in the thread naming the admin who removed it;
170
+ // closed/deleted items and untracked volumes are removed silently. Never throws.
171
+ export async function deleteVolumeForWebUi(workItemId, actorUsername) {
172
+ const row = getVolumeRow(workItemId);
173
+ const result = await destroyWorkItemVolume(workItemId);
174
+ deleteVolumeRow(workItemId);
175
+ let notePosted = false;
176
+ if (result.ok && row && row.project_id !== 0) {
177
+ try {
178
+ const state = await getPlatform().workItems.getWorkItemStateOrNull(row.project_id, row.work_item_iid);
179
+ if (state === "open") {
180
+ const body = `🧹 Volume \`${volumeName(workItemId)}\` removed from the WebUI by ${actorUsername}. Mention @devbot to resume.`;
181
+ await getPlatform().workItems.postComment(row.project_id, row.work_item_iid, body);
182
+ notePosted = true;
183
+ }
184
+ }
185
+ catch (e) {
186
+ log.warn("volume manage: delete note failed", { workItemId, err: e });
187
+ }
188
+ }
189
+ return { ok: result.ok, message: result.message, notePosted };
190
+ }
191
+ //# sourceMappingURL=volume-manage.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"volume-manage.js","sourceRoot":"","sources":["../src/volume-manage.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,122 @@
1
+ import { db } from "./db.js";
2
+ import { log } from "./logger.js";
3
+ // Per-work-item volume ledger. Tracks (projectId, workItemNumber, workItemId)
4
+ // so the GC sweep can post a purge note for the tracked work item without
5
+ // needing to re-resolve it from the forge. Timestamps are unix seconds
6
+ // (INTEGER), matching the mcp_thread_tokens / project_mcp_servers convention.
7
+ // Same DB file (devbot.db, 0600).
8
+ //
9
+ // The DB column stays `work_item_iid` (a storage name predating the neutral
10
+ // `number` rename); the TS-facing parameter is `workItemNumber`.
11
+ //
12
+ // Orphan rows: volumes created before this table existed (or via /reset
13
+ // re-creation outside the runner) are reconciled at startup with
14
+ // project_id=0, work_item_iid=0. purgeInactiveVolumes skips the note for
15
+ // those rows (no project/number to post to) but still frees the volume and the
16
+ // row — so pre-feature orphans stop leaking without a misleading note.
17
+ export function initVolumeTables() {
18
+ db.exec(`
19
+ CREATE TABLE IF NOT EXISTS work_item_volumes (
20
+ work_item_id INTEGER PRIMARY KEY,
21
+ project_id INTEGER NOT NULL,
22
+ work_item_iid INTEGER NOT NULL,
23
+ created_at INTEGER NOT NULL,
24
+ last_run_at INTEGER NOT NULL
25
+ )
26
+ `);
27
+ }
28
+ // Record a volume on first creation (or first observed run for a reconciled
29
+ // orphan). Upsert: preserves created_at if the row already exists, otherwise
30
+ // sets both created_at and last_run_at to now. Sets project_id + work_item_iid
31
+ // — overwrites the 0/0 placeholder for a reconciled orphan once a real run
32
+ // lands, so future purges can post a note.
33
+ export function recordVolume(workItemId, projectId, workItemNumber) {
34
+ const now = Math.floor(Date.now() / 1000);
35
+ db.prepare(`INSERT OR REPLACE INTO work_item_volumes
36
+ (work_item_id, project_id, work_item_iid, created_at, last_run_at)
37
+ VALUES (?, ?, ?,
38
+ COALESCE((SELECT created_at FROM work_item_volumes WHERE work_item_id = ?), ?),
39
+ ?)`).run(workItemId, projectId, workItemNumber, workItemId, now, now);
40
+ }
41
+ // Reconciled-orphan insert: project_id=0, work_item_iid=0 (unmappable). The GC
42
+ // sweep purges these silently (no note). If a real run lands on the same
43
+ // workItemId later, recordVolume overwrites the 0/0 with the real ids.
44
+ //
45
+ // createdAtSec is the volume's ACTUAL creation time (from `podman volume ls`),
46
+ // not reconcile time. Using `now` here is the bug that stranded pre-feature
47
+ // orphans: a volume created months ago got a fresh TTL clock every reconcile
48
+ // and never became eligible. `now` is only a last-resort fallback.
49
+ //
50
+ // Self-heal: ON CONFLICT UPDATE corrects a previously mis-stamped orphan row
51
+ // (project_id = 0 only). The WHERE guard means a real row — a live work item
52
+ // tracked by recordVolume — is never overwritten, preserving the "orphan never
53
+ // clobbers real" invariant.
54
+ export function recordOrphanVolume(workItemId, createdAtSec) {
55
+ const created = Number.isFinite(createdAtSec) && createdAtSec > 0
56
+ ? Math.floor(createdAtSec)
57
+ : Math.floor(Date.now() / 1000);
58
+ db.prepare(`INSERT INTO work_item_volumes
59
+ (work_item_id, project_id, work_item_iid, created_at, last_run_at)
60
+ VALUES (?, 0, 0, ?, ?)
61
+ ON CONFLICT(work_item_id) DO UPDATE SET
62
+ created_at = excluded.created_at,
63
+ last_run_at = excluded.last_run_at
64
+ WHERE work_item_volumes.project_id = 0`).run(workItemId, created, created);
65
+ }
66
+ // Bump last_run_at for a volume the runner is about to use / has just used.
67
+ // No-op if the row doesn't exist (shouldn't happen — recordVolume runs at
68
+ // creation; harmless if it does, the sweep will just use created_at).
69
+ export function touchVolume(workItemId) {
70
+ const now = Math.floor(Date.now() / 1000);
71
+ db.prepare("UPDATE work_item_volumes SET last_run_at = ? WHERE work_item_id = ?").run(now, workItemId);
72
+ }
73
+ // Return rows whose last_run_at is older than or equal to (now - maxAgeMs). The
74
+ // sweep uses last_run_at as the inactivity clock; a volume inactive for *at
75
+ // least* TTL qualifies, hence `<=` (not `<`). A volume with zero runs relies on
76
+ // created_at, but recordVolume sets last_run_at = created_at at creation, so
77
+ // the single column covers both.
78
+ export function listInactiveVolumes(maxAgeMs) {
79
+ const cutoff = Math.floor((Date.now() - maxAgeMs) / 1000);
80
+ return db
81
+ .prepare("SELECT work_item_id, project_id, work_item_iid, created_at, last_run_at FROM work_item_volumes WHERE last_run_at <= ?")
82
+ .all(cutoff);
83
+ }
84
+ // Every tracked row, regardless of age. The sweep needs the full set to run its
85
+ // liveness pass (closed/deleted work items are purged immediately, before the
86
+ // TTL is due), not just the TTL-inactive subset.
87
+ export function listTrackedVolumes() {
88
+ return db
89
+ .prepare("SELECT work_item_id, project_id, work_item_iid, created_at, last_run_at FROM work_item_volumes")
90
+ .all();
91
+ }
92
+ // One ledger row by work item id, or null when untracked. Backs the WebUI
93
+ // volume manager, which needs the stored (projectId, workItemNumber) to decide
94
+ // whether a manual delete may post a note.
95
+ export function getVolumeRow(workItemId) {
96
+ const row = db
97
+ .prepare("SELECT work_item_id, project_id, work_item_iid, created_at, last_run_at FROM work_item_volumes WHERE work_item_id = ?")
98
+ .get(workItemId);
99
+ return row ?? null;
100
+ }
101
+ // Has a row for this workItemId? Used by reconciliation to decide whether to
102
+ // insert an orphan placeholder.
103
+ export function hasVolumeRow(workItemId) {
104
+ const r = db.prepare("SELECT 1 FROM work_item_volumes WHERE work_item_id = ?").get(workItemId);
105
+ return r !== undefined;
106
+ }
107
+ // work_item_ids backed by a REAL row (project_id != 0), i.e. a live work item
108
+ // the runner recorded. Reconciliation must never touch these. Orphan rows
109
+ // (project_id = 0) are intentionally excluded so they can be re-examined and
110
+ // self-healed with the volume's real creation time.
111
+ export function realWorkItemIds() {
112
+ const rows = db
113
+ .prepare("SELECT work_item_id FROM work_item_volumes WHERE project_id != 0")
114
+ .all();
115
+ return rows.map((r) => r.work_item_id);
116
+ }
117
+ export function deleteVolumeRow(workItemId) {
118
+ const r = db.prepare("DELETE FROM work_item_volumes WHERE work_item_id = ?").run(workItemId);
119
+ if (r.changes > 0)
120
+ log.debug("volume ledger row deleted", { workItemId });
121
+ }
122
+ //# sourceMappingURL=volume-store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"volume-store.js","sourceRoot":"","sources":["../src/volume-store.ts"],"names":[],"mappings":"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"}