@wascartech/devbot 0.0.0-stage → 2.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/README.md +638 -2
- package/dist/access-grant-store.js +99 -0
- package/dist/access-grant-store.js.map +1 -0
- package/dist/access-resolve.js +11 -0
- package/dist/access-resolve.js.map +1 -0
- package/dist/access-rule-store.js +99 -0
- package/dist/access-rule-store.js.map +1 -0
- package/dist/admin/access-routes.js +73 -0
- package/dist/admin/access-routes.js.map +1 -0
- package/dist/admin/access-rule-routes.js +107 -0
- package/dist/admin/access-rule-routes.js.map +1 -0
- package/dist/admin/custom-repos-routes.js +93 -0
- package/dist/admin/custom-repos-routes.js.map +1 -0
- package/dist/admin/env-routes.js +74 -0
- package/dist/admin/env-routes.js.map +1 -0
- package/dist/admin/global-settings-routes.js +254 -0
- package/dist/admin/global-settings-routes.js.map +1 -0
- package/dist/admin/mcp-cleanup.js +12 -0
- package/dist/admin/mcp-cleanup.js.map +1 -0
- package/dist/admin/mcp-routes.js +169 -0
- package/dist/admin/mcp-routes.js.map +1 -0
- package/dist/admin/query-params.js +80 -0
- package/dist/admin/query-params.js.map +1 -0
- package/dist/admin/routes.js +150 -0
- package/dist/admin/routes.js.map +1 -0
- package/dist/admin/run-history-routes.js +44 -0
- package/dist/admin/run-history-routes.js.map +1 -0
- package/dist/admin/run-routes.js +99 -0
- package/dist/admin/run-routes.js.map +1 -0
- package/dist/admin/settings-routes.js +119 -0
- package/dist/admin/settings-routes.js.map +1 -0
- package/dist/admin/stats-routes.js +51 -0
- package/dist/admin/stats-routes.js.map +1 -0
- package/dist/admin/task-routes.js +213 -0
- package/dist/admin/task-routes.js.map +1 -0
- package/dist/admin/test-routes.js +38 -0
- package/dist/admin/test-routes.js.map +1 -0
- package/dist/admin/volume-routes.js +82 -0
- package/dist/admin/volume-routes.js.map +1 -0
- package/dist/admin/webhook-routes.js +174 -0
- package/dist/admin/webhook-routes.js.map +1 -0
- package/dist/agents.js +48 -0
- package/dist/agents.js.map +1 -0
- package/dist/assignee-value.js +6 -0
- package/dist/assignee-value.js.map +1 -0
- package/dist/attachments.js +76 -0
- package/dist/attachments.js.map +1 -0
- package/dist/auto-commands.js +88 -0
- package/dist/auto-commands.js.map +1 -0
- package/dist/auto-features.js +32 -0
- package/dist/auto-features.js.map +1 -0
- package/dist/base-branch-store.js +40 -0
- package/dist/base-branch-store.js.map +1 -0
- package/dist/base-branches.js +9 -0
- package/dist/base-branches.js.map +1 -0
- package/dist/build-workflow.js +116 -0
- package/dist/build-workflow.js.map +1 -0
- package/dist/change-request-lifecycle.js +16 -0
- package/dist/change-request-lifecycle.js.map +1 -0
- package/dist/change-request-store.js +72 -0
- package/dist/change-request-store.js.map +1 -0
- package/dist/ci-failure.js +80 -0
- package/dist/ci-failure.js.map +1 -0
- package/dist/cli.js +3 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.js +17 -0
- package/dist/config.js.map +1 -0
- package/dist/conflict-fix.js +178 -0
- package/dist/conflict-fix.js.map +1 -0
- package/dist/cookie.js +70 -0
- package/dist/cookie.js.map +1 -0
- package/dist/crypto-util.js +40 -0
- package/dist/crypto-util.js.map +1 -0
- package/dist/custom-repos-store.js +90 -0
- package/dist/custom-repos-store.js.map +1 -0
- package/dist/custom-repos-sync.js +118 -0
- package/dist/custom-repos-sync.js.map +1 -0
- package/dist/db.js +12 -0
- package/dist/db.js.map +1 -0
- package/dist/devbot-scripts.js +18 -0
- package/dist/devbot-scripts.js.map +1 -0
- package/dist/devcontainer.js +150 -0
- package/dist/devcontainer.js.map +1 -0
- package/dist/env-config-store.js +132 -0
- package/dist/env-config-store.js.map +1 -0
- package/dist/error-log-store.js +193 -0
- package/dist/error-log-store.js.map +1 -0
- package/dist/git.js +605 -0
- package/dist/git.js.map +1 -0
- package/dist/global-queue.js +127 -0
- package/dist/global-queue.js.map +1 -0
- package/dist/global-settings-store.js +307 -0
- package/dist/global-settings-store.js.map +1 -0
- package/dist/help.js +41 -0
- package/dist/help.js.map +1 -0
- package/dist/http-csp.js +15 -0
- package/dist/http-csp.js.map +1 -0
- package/dist/http-util.js +40 -0
- package/dist/http-util.js.map +1 -0
- package/dist/idempotency.js +50 -0
- package/dist/idempotency.js.map +1 -0
- package/dist/lifecycle.js +234 -0
- package/dist/lifecycle.js.map +1 -0
- package/dist/logger.js +33 -0
- package/dist/logger.js.map +1 -0
- package/dist/lsp-servers.js +81 -0
- package/dist/lsp-servers.js.map +1 -0
- package/dist/mcp/auth.js +62 -0
- package/dist/mcp/auth.js.map +1 -0
- package/dist/mcp/effective-mcp.js +42 -0
- package/dist/mcp/effective-mcp.js.map +1 -0
- package/dist/mcp/oauth-refresh.js +138 -0
- package/dist/mcp/oauth-refresh.js.map +1 -0
- package/dist/mcp/peer-sync.js +109 -0
- package/dist/mcp/peer-sync.js.map +1 -0
- package/dist/mcp/registry.js +54 -0
- package/dist/mcp/registry.js.map +1 -0
- package/dist/mcp/runner-bridge.js +93 -0
- package/dist/mcp/runner-bridge.js.map +1 -0
- package/dist/mcp/server.js +173 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/mcp/tools/_helpers.js +88 -0
- package/dist/mcp/tools/_helpers.js.map +1 -0
- package/dist/mcp/tools/attachments.js +117 -0
- package/dist/mcp/tools/attachments.js.map +1 -0
- package/dist/mcp/tools/cicd.js +105 -0
- package/dist/mcp/tools/cicd.js.map +1 -0
- package/dist/mcp/tools/discovery.js +34 -0
- package/dist/mcp/tools/discovery.js.map +1 -0
- package/dist/mcp/tools/issues.js +56 -0
- package/dist/mcp/tools/issues.js.map +1 -0
- package/dist/mcp/tools/merge_requests.js +67 -0
- package/dist/mcp/tools/merge_requests.js.map +1 -0
- package/dist/mcp/tools/metadata.js +127 -0
- package/dist/mcp/tools/metadata.js.map +1 -0
- package/dist/mcp/tools/peer_sync.js +43 -0
- package/dist/mcp/tools/peer_sync.js.map +1 -0
- package/dist/mcp/tools/repository.js +178 -0
- package/dist/mcp/tools/repository.js.map +1 -0
- package/dist/mcp/tools/work_tracking.js +125 -0
- package/dist/mcp/tools/work_tracking.js.map +1 -0
- package/dist/mcp/tools/workitems.js +30 -0
- package/dist/mcp/tools/workitems.js.map +1 -0
- package/dist/mcp/volume-files.js +187 -0
- package/dist/mcp/volume-files.js.map +1 -0
- package/dist/mcp-auth-store.js +134 -0
- package/dist/mcp-auth-store.js.map +1 -0
- package/dist/mcp-config-store.js +83 -0
- package/dist/mcp-config-store.js.map +1 -0
- package/dist/mention.js +512 -0
- package/dist/mention.js.map +1 -0
- package/dist/migrate.js +438 -0
- package/dist/migrate.js.map +1 -0
- package/dist/mr-assignee.js +71 -0
- package/dist/mr-assignee.js.map +1 -0
- package/dist/mr-content.js +90 -0
- package/dist/mr-content.js.map +1 -0
- package/dist/oauth-routes.js +54 -0
- package/dist/oauth-routes.js.map +1 -0
- package/dist/onboarding-allowlist.js +32 -0
- package/dist/onboarding-allowlist.js.map +1 -0
- package/dist/opencode.js +291 -0
- package/dist/opencode.js.map +1 -0
- package/dist/outbound-webhook-store.js +229 -0
- package/dist/outbound-webhook-store.js.map +1 -0
- package/dist/outbound-webhook.js +314 -0
- package/dist/outbound-webhook.js.map +1 -0
- package/dist/path-sandbox.js +200 -0
- package/dist/path-sandbox.js.map +1 -0
- package/dist/platform/git-auth.js +34 -0
- package/dist/platform/git-auth.js.map +1 -0
- package/dist/platform/gitealike/attachments.js +62 -0
- package/dist/platform/gitealike/attachments.js.map +1 -0
- package/dist/platform/gitealike/base.js +935 -0
- package/dist/platform/gitealike/base.js.map +1 -0
- package/dist/platform/gitealike/forgejo.js +16 -0
- package/dist/platform/gitealike/forgejo.js.map +1 -0
- package/dist/platform/gitealike/gitea.js +16 -0
- package/dist/platform/gitealike/gitea.js.map +1 -0
- package/dist/platform/gitealike/http.js +152 -0
- package/dist/platform/gitealike/http.js.map +1 -0
- package/dist/platform/gitealike/schemas.js +284 -0
- package/dist/platform/gitealike/schemas.js.map +1 -0
- package/dist/platform/github/attachments.js +52 -0
- package/dist/platform/github/attachments.js.map +1 -0
- package/dist/platform/github/http.js +153 -0
- package/dist/platform/github/http.js.map +1 -0
- package/dist/platform/github/index.js +936 -0
- package/dist/platform/github/index.js.map +1 -0
- package/dist/platform/github/schemas.js +308 -0
- package/dist/platform/github/schemas.js.map +1 -0
- package/dist/platform/github/zip.js +85 -0
- package/dist/platform/github/zip.js.map +1 -0
- package/dist/platform/gitlab/admin.js +105 -0
- package/dist/platform/gitlab/admin.js.map +1 -0
- package/dist/platform/gitlab/api.js +229 -0
- package/dist/platform/gitlab/api.js.map +1 -0
- package/dist/platform/gitlab/attachments.js +79 -0
- package/dist/platform/gitlab/attachments.js.map +1 -0
- package/dist/platform/gitlab/http.js +165 -0
- package/dist/platform/gitlab/http.js.map +1 -0
- package/dist/platform/gitlab/index.js +630 -0
- package/dist/platform/gitlab/index.js.map +1 -0
- package/dist/platform/gitlab/oauth.js +58 -0
- package/dist/platform/gitlab/oauth.js.map +1 -0
- package/dist/platform/gitlab/read.js +182 -0
- package/dist/platform/gitlab/read.js.map +1 -0
- package/dist/platform/gitlab/schemas-read.js +274 -0
- package/dist/platform/gitlab/schemas-read.js.map +1 -0
- package/dist/platform/gitlab/schemas.js +251 -0
- package/dist/platform/gitlab/schemas.js.map +1 -0
- package/dist/platform/gitlab/signature.js +52 -0
- package/dist/platform/gitlab/signature.js.map +1 -0
- package/dist/platform/index.js +38 -0
- package/dist/platform/index.js.map +1 -0
- package/dist/platform/port.js +26 -0
- package/dist/platform/port.js.map +1 -0
- package/dist/platform/types.js +80 -0
- package/dist/platform/types.js.map +1 -0
- package/dist/podman.js +119 -0
- package/dist/podman.js.map +1 -0
- package/dist/project-sync.js +69 -0
- package/dist/project-sync.js.map +1 -0
- package/dist/prompt.js +47 -0
- package/dist/prompt.js.map +1 -0
- package/dist/queue-store.js +236 -0
- package/dist/queue-store.js.map +1 -0
- package/dist/queue.js +129 -0
- package/dist/queue.js.map +1 -0
- package/dist/redact.js +23 -0
- package/dist/redact.js.map +1 -0
- package/dist/run-files.js +297 -0
- package/dist/run-files.js.map +1 -0
- package/dist/run-filters.js +54 -0
- package/dist/run-filters.js.map +1 -0
- package/dist/run-history-store.js +229 -0
- package/dist/run-history-store.js.map +1 -0
- package/dist/run-log-sink.js +270 -0
- package/dist/run-log-sink.js.map +1 -0
- package/dist/run-log.js +97 -0
- package/dist/run-log.js.map +1 -0
- package/dist/run-usage.js +29 -0
- package/dist/run-usage.js.map +1 -0
- package/dist/run-view.js +112 -0
- package/dist/run-view.js.map +1 -0
- package/dist/runner.js +1094 -0
- package/dist/runner.js.map +1 -0
- package/dist/runtime-apply.js +26 -0
- package/dist/runtime-apply.js.map +1 -0
- package/dist/runtime-settings.js +71 -0
- package/dist/runtime-settings.js.map +1 -0
- package/dist/runtime-state.js +19 -0
- package/dist/runtime-state.js.map +1 -0
- package/dist/server.js +119 -0
- package/dist/server.js.map +1 -0
- package/dist/settings-store.js +320 -0
- package/dist/settings-store.js.map +1 -0
- package/dist/status.js +121 -0
- package/dist/status.js.map +1 -0
- package/dist/stop.js +56 -0
- package/dist/stop.js.map +1 -0
- package/dist/sweeps.js +87 -0
- package/dist/sweeps.js.map +1 -0
- package/dist/task-log.js +279 -0
- package/dist/task-log.js.map +1 -0
- package/dist/task-view.js +62 -0
- package/dist/task-view.js.map +1 -0
- package/dist/token-store.js +68 -0
- package/dist/token-store.js.map +1 -0
- package/dist/types/access.js +74 -0
- package/dist/types/access.js.map +1 -0
- package/dist/types/custom-repos.js +28 -0
- package/dist/types/custom-repos.js.map +1 -0
- package/dist/types/env-config.js +25 -0
- package/dist/types/env-config.js.map +1 -0
- package/dist/types/env.js +190 -0
- package/dist/types/env.js.map +1 -0
- package/dist/types/global-settings.js +178 -0
- package/dist/types/global-settings.js.map +1 -0
- package/dist/types/lsp-servers.js +145 -0
- package/dist/types/lsp-servers.js.map +1 -0
- package/dist/types/mcp-config.js +77 -0
- package/dist/types/mcp-config.js.map +1 -0
- package/dist/types/opencode.js +59 -0
- package/dist/types/opencode.js.map +1 -0
- package/dist/types/outbound-webhook.js +56 -0
- package/dist/types/outbound-webhook.js.map +1 -0
- package/dist/types/project-settings.js +114 -0
- package/dist/types/project-settings.js.map +1 -0
- package/dist/types/provider-registry.js +37 -0
- package/dist/types/provider-registry.js.map +1 -0
- package/dist/types/runtime-settings.js +78 -0
- package/dist/types/runtime-settings.js.map +1 -0
- package/dist/usage-stats.js +159 -0
- package/dist/usage-stats.js.map +1 -0
- package/dist/version.js +19 -0
- package/dist/version.js.map +1 -0
- package/dist/volume-gc.js +193 -0
- package/dist/volume-gc.js.map +1 -0
- package/dist/volume-manage.js +191 -0
- package/dist/volume-manage.js.map +1 -0
- package/dist/volume-store.js +122 -0
- package/dist/volume-store.js.map +1 -0
- package/dist/webhook.js +1106 -0
- package/dist/webhook.js.map +1 -0
- package/dist/work-item-auto-store.js +75 -0
- package/dist/work-item-auto-store.js.map +1 -0
- package/dist/work-item-branch-store.js +37 -0
- package/dist/work-item-branch-store.js.map +1 -0
- package/migrations/0001_initial.sql +59 -0
- package/migrations/0002_queue_status.sql +15 -0
- package/migrations/0003_work_item_base_branch.sql +14 -0
- package/migrations/0004_project_settings.sql +15 -0
- package/migrations/0005_global_settings.sql +34 -0
- package/migrations/0006_model_variants.sql +8 -0
- package/migrations/0007_container_error_logs.sql +21 -0
- package/migrations/0008_project_access_grants.sql +16 -0
- package/migrations/0009_drop_imported_legacy.sql +4 -0
- package/migrations/0010_container_error_log_model.sql +6 -0
- package/migrations/0011_container_error_log_title.sql +5 -0
- package/migrations/0012_pending_task_cancel.sql +6 -0
- package/migrations/0013_builder_image.sql +6 -0
- package/migrations/0014_build_workflow.sql +7 -0
- package/migrations/0015_access_rules.sql +24 -0
- package/migrations/0016_trigger_policy.sql +17 -0
- package/migrations/0017_attachment_upload.sql +8 -0
- package/migrations/0018_work_item_branch.sql +11 -0
- package/migrations/0019_auto_push_mr.sql +18 -0
- package/migrations/0020_outbound_webhooks.sql +47 -0
- package/migrations/0021_runtime_settings.sql +12 -0
- package/migrations/0022_auto_resolve_conflicts.sql +17 -0
- package/migrations/0023_ci_fail_auto_report.sql +27 -0
- package/migrations/0024_work_item_auto_override.sql +22 -0
- package/migrations/0025_run_history.sql +37 -0
- package/migrations/0026_run_history_task_seq.sql +6 -0
- package/migrations/0027_custom_repos.sql +11 -0
- package/migrations/0028_run_usage.sql +14 -0
- package/migrations/0029_drop_ocr_langs.sql +6 -0
- package/migrations/0030_mr_assignee.sql +9 -0
- package/migrations/0031_lsp_servers.sql +14 -0
- package/opencode-config/managed/devbot-agent-policy.md +61 -0
- package/opencode-config/managed/opencode.json +77 -0
- package/opencode-config/opencode.json +211 -0
- package/opencode-config/plugins/devbot-path-guard.js +224 -0
- package/package.json +158 -4
- package/public/assets/DashboardSidebarCollapse-rIuzCmVm.js +1 -0
- package/public/assets/DashboardToolbar-DFCUqZ4Z.js +1 -0
- package/public/assets/ErrorLogModal.vue_vue_type_script_setup_true_lang-BCyNIf1E.js +1 -0
- package/public/assets/ErrorLogsPage-DyYK2Rop.js +1 -0
- package/public/assets/LogSlideover.vue_vue_type_script_setup_true_lang-33xie7TE.js +1 -0
- package/public/assets/OverviewPage-CFfa0uwg.js +1 -0
- package/public/assets/PageGrid-CBROIU87.js +1 -0
- package/public/assets/PeriodRangePicker.vue_vue_type_script_setup_true_lang-DiFUNem7.js +1 -0
- package/public/assets/ProjectDetailPage-CreyvUit.js +1 -0
- package/public/assets/ProjectsPage-CFx8-elJ.js +1 -0
- package/public/assets/RunDetailSlideover.vue_vue_type_script_setup_true_lang-DopYYmCT.js +1 -0
- package/public/assets/RunsPage-CRnnXU4P.js +1 -0
- package/public/assets/Select-Bvx32Diz.js +1 -0
- package/public/assets/SelectMenu-BjMxc4VB.js +1 -0
- package/public/assets/SettingsPage-Bb6LM3wO.js +60 -0
- package/public/assets/TimeCell.vue_vue_type_script_setup_true_lang-DW_UB0Fu.js +1 -0
- package/public/assets/UsagePage-BLMDEsv-.js +332 -0
- package/public/assets/UsagePage-DodEeAjB.css +1 -0
- package/public/assets/VolumesPage-CxcLc3L2.js +1 -0
- package/public/assets/WebhookConfigPanel.vue_vue_type_script_setup_true_lang-B2V2GF6b.js +1 -0
- package/public/assets/WorkItemCell.vue_vue_type_script_setup_true_lang-BQn6vTjd.js +1 -0
- package/public/assets/index-BMbMibyb.js +4 -0
- package/public/assets/index-C5RBBrPj.js +30 -0
- package/public/assets/index-CgX29UgC.css +1 -0
- package/public/assets/usage-format-jsb31_O8.js +1 -0
- package/public/assets/useManagedProjects-8rtjRqVJ.js +1 -0
- package/public/assets/useRouteTab-DfwrcMic.js +1 -0
- package/public/favicon.ico +0 -0
- package/public/favicon.png +0 -0
- 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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"}
|