@mastra/code-sdk 1.0.3-alpha.1 → 1.1.0-alpha.2

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 (258) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/dist/acp/agent.js +155 -167
  3. package/dist/acp/agent.js.map +1 -1
  4. package/dist/acp/event-mapper.js +193 -168
  5. package/dist/acp/event-mapper.js.map +1 -1
  6. package/dist/acp/index.js +60 -49
  7. package/dist/acp/index.js.map +1 -1
  8. package/dist/acp/server.js +31 -30
  9. package/dist/acp/server.js.map +1 -1
  10. package/dist/acp.js +1 -4
  11. package/dist/agents/credential-resolver.js +40 -29
  12. package/dist/agents/credential-resolver.js.map +1 -1
  13. package/dist/agents/custom-provider-source.js +16 -9
  14. package/dist/agents/custom-provider-source.js.map +1 -1
  15. package/dist/agents/instructions.js +27 -36
  16. package/dist/agents/instructions.js.map +1 -1
  17. package/dist/agents/mastracode-gateway.js +355 -428
  18. package/dist/agents/mastracode-gateway.js.map +1 -1
  19. package/dist/agents/memory.js +87 -78
  20. package/dist/agents/memory.js.map +1 -1
  21. package/dist/agents/model.js +114 -119
  22. package/dist/agents/model.js.map +1 -1
  23. package/dist/agents/modes/build.js +12 -13
  24. package/dist/agents/modes/build.js.map +1 -1
  25. package/dist/agents/modes/explore.js +14 -13
  26. package/dist/agents/modes/explore.js.map +1 -1
  27. package/dist/agents/modes/plan.js +14 -15
  28. package/dist/agents/modes/plan.js.map +1 -1
  29. package/dist/agents/prompts/agent-instructions.js +78 -69
  30. package/dist/agents/prompts/agent-instructions.js.map +1 -1
  31. package/dist/agents/prompts/build.js +19 -16
  32. package/dist/agents/prompts/build.js.map +1 -1
  33. package/dist/agents/prompts/fast.js +8 -4
  34. package/dist/agents/prompts/fast.js.map +1 -1
  35. package/dist/agents/prompts/index.js +48 -47
  36. package/dist/agents/prompts/index.js.map +1 -1
  37. package/dist/agents/prompts/model.js +6 -5
  38. package/dist/agents/prompts/model.js.map +1 -1
  39. package/dist/agents/prompts/plan.js +20 -16
  40. package/dist/agents/prompts/plan.js.map +1 -1
  41. package/dist/agents/prompts/tool-guidance.js +77 -109
  42. package/dist/agents/prompts/tool-guidance.js.map +1 -1
  43. package/dist/agents/sandbox-filesystem.js +245 -259
  44. package/dist/agents/sandbox-filesystem.js.map +1 -1
  45. package/dist/agents/sandbox-reattach.js +9 -11
  46. package/dist/agents/sandbox-reattach.js.map +1 -1
  47. package/dist/agents/thread-caveman-state.js +92 -81
  48. package/dist/agents/thread-caveman-state.js.map +1 -1
  49. package/dist/agents/tool-availability.js +62 -80
  50. package/dist/agents/tool-availability.js.map +1 -1
  51. package/dist/agents/tools.js +99 -139
  52. package/dist/agents/tools.js.map +1 -1
  53. package/dist/agents/workspace.js +232 -225
  54. package/dist/agents/workspace.js.map +1 -1
  55. package/dist/analytics.js +139 -128
  56. package/dist/analytics.js.map +1 -1
  57. package/dist/auth/authorization-input.js +39 -25
  58. package/dist/auth/authorization-input.js.map +1 -1
  59. package/dist/auth/device-code.js +112 -80
  60. package/dist/auth/device-code.js.map +1 -1
  61. package/dist/auth/index.js +2 -9
  62. package/dist/auth/pkce.js +27 -16
  63. package/dist/auth/pkce.js.map +1 -1
  64. package/dist/auth/providers/anthropic.js +107 -93
  65. package/dist/auth/providers/anthropic.js.map +1 -1
  66. package/dist/auth/providers/github-copilot.js +304 -293
  67. package/dist/auth/providers/github-copilot.js.map +1 -1
  68. package/dist/auth/providers/openai-codex.js +442 -460
  69. package/dist/auth/providers/openai-codex.js.map +1 -1
  70. package/dist/auth/providers/xai.js +179 -145
  71. package/dist/auth/providers/xai.js.map +1 -1
  72. package/dist/auth/storage.js +182 -175
  73. package/dist/auth/storage.js.map +1 -1
  74. package/dist/auth/types.js +1 -1
  75. package/dist/clipboard/index.js +206 -172
  76. package/dist/clipboard/index.js.map +1 -1
  77. package/dist/constants.js +11 -16
  78. package/dist/constants.js.map +1 -1
  79. package/dist/error-classification.js +19 -11
  80. package/dist/error-classification.js.map +1 -1
  81. package/dist/evals/context-builder.js +145 -104
  82. package/dist/evals/context-builder.js.map +1 -1
  83. package/dist/evals/index.js +4 -7
  84. package/dist/evals/scorers/classify-command.js +62 -41
  85. package/dist/evals/scorers/classify-command.js.map +1 -1
  86. package/dist/evals/scorers/efficiency.js +239 -181
  87. package/dist/evals/scorers/efficiency.js.map +1 -1
  88. package/dist/evals/scorers/extract-tools.js +51 -47
  89. package/dist/evals/scorers/extract-tools.js.map +1 -1
  90. package/dist/evals/scorers/index.js +1 -5
  91. package/dist/evals/scorers/outcome.js +321 -212
  92. package/dist/evals/scorers/outcome.js.map +1 -1
  93. package/dist/goal-manager.js +266 -261
  94. package/dist/goal-manager.js.map +1 -1
  95. package/dist/goal-signal.js +14 -13
  96. package/dist/goal-signal.js.map +1 -1
  97. package/dist/headless/cli.js +153 -154
  98. package/dist/headless/cli.js.map +1 -1
  99. package/dist/headless/flags.js +180 -178
  100. package/dist/headless/flags.js.map +1 -1
  101. package/dist/headless/format.js +72 -71
  102. package/dist/headless/format.js.map +1 -1
  103. package/dist/headless/index.js +5 -31
  104. package/dist/headless/policy.js +35 -31
  105. package/dist/headless/policy.js.map +1 -1
  106. package/dist/headless/run-mc.js +332 -334
  107. package/dist/headless/run-mc.js.map +1 -1
  108. package/dist/headless/types.js +16 -7
  109. package/dist/headless/types.js.map +1 -1
  110. package/dist/hooks/config.js +56 -61
  111. package/dist/hooks/config.js.map +1 -1
  112. package/dist/hooks/executor.js +118 -116
  113. package/dist/hooks/executor.js.map +1 -1
  114. package/dist/hooks/index.js +4 -15
  115. package/dist/hooks/manager.js +221 -280
  116. package/dist/hooks/manager.js.map +1 -1
  117. package/dist/hooks/types.js +5 -4
  118. package/dist/hooks/types.js.map +1 -1
  119. package/dist/index.d.ts.map +1 -1
  120. package/dist/index.js +632 -712
  121. package/dist/index.js.map +1 -1
  122. package/dist/ipc/ipc-reporter.js +19 -22
  123. package/dist/ipc/ipc-reporter.js.map +1 -1
  124. package/dist/lsp/client.js +248 -306
  125. package/dist/lsp/client.js.map +1 -1
  126. package/dist/lsp/index.js +1 -6
  127. package/dist/lsp/language.js +39 -43
  128. package/dist/lsp/language.js.map +1 -1
  129. package/dist/lsp/manager.js +78 -93
  130. package/dist/lsp/manager.js.map +1 -1
  131. package/dist/lsp/server.js +151 -120
  132. package/dist/lsp/server.js.map +1 -1
  133. package/dist/lsp/workspace.js +50 -47
  134. package/dist/lsp/workspace.js.map +1 -1
  135. package/dist/mcp/config.js +210 -199
  136. package/dist/mcp/config.js.map +1 -1
  137. package/dist/mcp/index.js +2 -9
  138. package/dist/mcp/manager.js +482 -475
  139. package/dist/mcp/manager.js.map +1 -1
  140. package/dist/mcp/types.js +1 -1
  141. package/dist/onboarding/custom-packs.js +14 -17
  142. package/dist/onboarding/custom-packs.js.map +1 -1
  143. package/dist/onboarding/custom-providers.js +22 -20
  144. package/dist/onboarding/custom-providers.js.map +1 -1
  145. package/dist/onboarding/index.d.ts +1 -1
  146. package/dist/onboarding/index.d.ts.map +1 -1
  147. package/dist/onboarding/index.js +3 -21
  148. package/dist/onboarding/om-settings.d.ts +5 -0
  149. package/dist/onboarding/om-settings.d.ts.map +1 -1
  150. package/dist/onboarding/om-settings.js +41 -20
  151. package/dist/onboarding/om-settings.js.map +1 -1
  152. package/dist/onboarding/packs.d.ts +4 -6
  153. package/dist/onboarding/packs.d.ts.map +1 -1
  154. package/dist/onboarding/packs.js +154 -105
  155. package/dist/onboarding/packs.js.map +1 -1
  156. package/dist/onboarding/settings.js +575 -517
  157. package/dist/onboarding/settings.js.map +1 -1
  158. package/dist/permissions.js +112 -99
  159. package/dist/permissions.js.map +1 -1
  160. package/dist/plugin.js +21 -23
  161. package/dist/plugin.js.map +1 -1
  162. package/dist/plugins/dependencies.js +64 -87
  163. package/dist/plugins/dependencies.js.map +1 -1
  164. package/dist/plugins/install.js +159 -169
  165. package/dist/plugins/install.js.map +1 -1
  166. package/dist/plugins/loader.js +170 -185
  167. package/dist/plugins/loader.js.map +1 -1
  168. package/dist/plugins/manager.js +374 -380
  169. package/dist/plugins/manager.js.map +1 -1
  170. package/dist/plugins/manifest.js +38 -63
  171. package/dist/plugins/manifest.js.map +1 -1
  172. package/dist/plugins/package-link.js +48 -64
  173. package/dist/plugins/package-link.js.map +1 -1
  174. package/dist/plugins/paths.js +18 -19
  175. package/dist/plugins/paths.js.map +1 -1
  176. package/dist/plugins/registry.js +97 -81
  177. package/dist/plugins/registry.js.map +1 -1
  178. package/dist/plugins/scaffold.js +62 -98
  179. package/dist/plugins/scaffold.js.map +1 -1
  180. package/dist/plugins/types.js +1 -1
  181. package/dist/processors/plan-rejection-abort.js +45 -44
  182. package/dist/processors/plan-rejection-abort.js.map +1 -1
  183. package/dist/providers/amazon-bedrock-gateway.js +134 -125
  184. package/dist/providers/amazon-bedrock-gateway.js.map +1 -1
  185. package/dist/providers/amazon-bedrock.js +68 -47
  186. package/dist/providers/amazon-bedrock.js.map +1 -1
  187. package/dist/providers/claude-max.js +153 -133
  188. package/dist/providers/claude-max.js.map +1 -1
  189. package/dist/providers/github-copilot.js +232 -230
  190. package/dist/providers/github-copilot.js.map +1 -1
  191. package/dist/providers/openai-codex.js +271 -260
  192. package/dist/providers/openai-codex.js.map +1 -1
  193. package/dist/providers/xai.js +57 -50
  194. package/dist/providers/xai.js.map +1 -1
  195. package/dist/schema.js +91 -89
  196. package/dist/schema.js.map +1 -1
  197. package/dist/theme-palette.js +18 -14
  198. package/dist/theme-palette.js.map +1 -1
  199. package/dist/tool-names.js +46 -37
  200. package/dist/tool-names.js.map +1 -1
  201. package/dist/tools/index.js +2 -8
  202. package/dist/tools/request-sandbox-access.js +71 -80
  203. package/dist/tools/request-sandbox-access.js.map +1 -1
  204. package/dist/tools/utils.js +26 -16
  205. package/dist/tools/utils.js.map +1 -1
  206. package/dist/tools/web-search.js +51 -55
  207. package/dist/tools/web-search.js.map +1 -1
  208. package/dist/utils/binaries.js +65 -61
  209. package/dist/utils/binaries.js.map +1 -1
  210. package/dist/utils/debug-log.js +51 -47
  211. package/dist/utils/debug-log.js.map +1 -1
  212. package/dist/utils/errors.js +157 -192
  213. package/dist/utils/errors.js.map +1 -1
  214. package/dist/utils/gateway-sync.js +49 -37
  215. package/dist/utils/gateway-sync.js.map +1 -1
  216. package/dist/utils/path-security.js +7 -6
  217. package/dist/utils/path-security.js.map +1 -1
  218. package/dist/utils/plan-diff.js +58 -39
  219. package/dist/utils/plan-diff.js.map +1 -1
  220. package/dist/utils/plans.js +92 -74
  221. package/dist/utils/plans.js.map +1 -1
  222. package/dist/utils/project.js +318 -256
  223. package/dist/utils/project.js.map +1 -1
  224. package/dist/utils/signals-pubsub.js +117 -99
  225. package/dist/utils/signals-pubsub.js.map +1 -1
  226. package/dist/utils/slash-command-loader.js +118 -154
  227. package/dist/utils/slash-command-loader.js.map +1 -1
  228. package/dist/utils/slash-command-processor.js +92 -83
  229. package/dist/utils/slash-command-processor.js.map +1 -1
  230. package/dist/utils/stdin-pipe.js +87 -63
  231. package/dist/utils/stdin-pipe.js.map +1 -1
  232. package/dist/utils/storage-factory.js +104 -84
  233. package/dist/utils/storage-factory.js.map +1 -1
  234. package/dist/utils/storage-maintenance.js +249 -211
  235. package/dist/utils/storage-maintenance.js.map +1 -1
  236. package/dist/utils/thread-lock.js +96 -92
  237. package/dist/utils/thread-lock.js.map +1 -1
  238. package/dist/utils/token-estimator.js +12 -12
  239. package/dist/utils/token-estimator.js.map +1 -1
  240. package/dist/utils/update-check.d.ts +1 -1
  241. package/dist/utils/update-check.js +341 -259
  242. package/dist/utils/update-check.js.map +1 -1
  243. package/dist/voice/stt-registry.js +131 -113
  244. package/dist/voice/stt-registry.js.map +1 -1
  245. package/package.json +12 -12
  246. package/dist/acp.js.map +0 -1
  247. package/dist/auth/index.js.map +0 -1
  248. package/dist/auth/types.js.map +0 -1
  249. package/dist/evals/index.js.map +0 -1
  250. package/dist/evals/scorers/index.js.map +0 -1
  251. package/dist/headless/index.js.map +0 -1
  252. package/dist/hooks/index.js.map +0 -1
  253. package/dist/lsp/index.js.map +0 -1
  254. package/dist/mcp/index.js.map +0 -1
  255. package/dist/mcp/types.js.map +0 -1
  256. package/dist/onboarding/index.js.map +0 -1
  257. package/dist/plugins/types.js.map +0 -1
  258. package/dist/tools/index.js.map +0 -1
@@ -1,236 +1,274 @@
1
- import { closeSync, existsSync, openSync, readSync, renameSync, rmSync, statSync, statfsSync } from "node:fs";
2
- import { dirname } from "node:path";
3
- import Database from "libsql";
4
1
  import { getDatabasePath, getVectorDatabasePath } from "./project.js";
2
+ import { closeSync, existsSync, openSync, readSync, renameSync, rmSync, statSync, statfsSync } from "fs";
3
+ import { dirname } from "path";
4
+ import Database from "libsql";
5
+ //#region src/utils/storage-maintenance.ts
6
+ /**
7
+ * Storage maintenance — retention defaults and disk reclamation for mastracode.
8
+ *
9
+ * Retention (`DEFAULT_RETENTION`) is wired into the inner store by the storage
10
+ * factory so `storage.prune()` can delete aged rows across every domain,
11
+ * including the legacy libsql observability spans table.
12
+ *
13
+ * `prune()` only deletes rows — SQLite keeps the freed pages inside the file.
14
+ * `reclaimLibSQLDisk()` returns that space to the OS by streaming a
15
+ * `VACUUM INTO` copy of each file and swapping it into place. It requires all
16
+ * connections to be closed first, so it only runs from the post-TUI
17
+ * maintenance flow. We deliberately do NOT expose VACUUM as a public API on
18
+ * LibSQLStore.
19
+ */
20
+ /**
21
+ * Default retention for mastracode's own storage. Tables not listed are kept
22
+ * forever. Ages chosen for a local dev tool where observability data dwarfs
23
+ * everything else (spans have been observed at 17+ GB):
24
+ * - spans/logs: short-lived debugging telemetry → 14d
25
+ * - scorer results & workflow snapshots: medium-lived run records → 30d
26
+ * - chat history (messages/threads): long-lived, user-visible → 90d
27
+ */
5
28
  const DEFAULT_RETENTION = {
6
- memory: {
7
- messages: { maxAge: "90d" },
8
- threads: { maxAge: "90d" }
9
- },
10
- observability: {
11
- spans: { maxAge: "14d" },
12
- logs: { maxAge: "14d" }
13
- },
14
- scores: {
15
- scorers: { maxAge: "30d" }
16
- },
17
- workflows: {
18
- workflowSnapshot: { maxAge: "30d" }
19
- }
29
+ memory: {
30
+ messages: { maxAge: "90d" },
31
+ threads: { maxAge: "90d" }
32
+ },
33
+ observability: {
34
+ spans: { maxAge: "14d" },
35
+ logs: { maxAge: "14d" }
36
+ },
37
+ scores: { scorers: { maxAge: "30d" } },
38
+ workflows: { workflowSnapshot: { maxAge: "30d" } }
20
39
  };
40
+ /** db file size + its WAL sidecar, in bytes. Missing files count as 0. */
21
41
  function fileSizeWithWal(dbFile) {
22
- let total = 0;
23
- for (const candidate of [dbFile, `${dbFile}-wal`]) {
24
- try {
25
- total += statSync(candidate).size;
26
- } catch {
27
- }
28
- }
29
- return total;
42
+ let total = 0;
43
+ for (const candidate of [dbFile, `${dbFile}-wal`]) try {
44
+ total += statSync(candidate).size;
45
+ } catch {}
46
+ return total;
30
47
  }
48
+ /** Free bytes required before compacting: the compacted copy plus 20% + 256 MB headroom. */
31
49
  function requiredFreeBytes(liveBytes) {
32
- return Math.ceil(liveBytes * 1.2) + 256 * 1024 * 1024;
50
+ return Math.ceil(liveBytes * 1.2) + 256 * 1024 * 1024;
33
51
  }
52
+ /** First column of the first row of a PRAGMA result, as a number. */
34
53
  function pragmaNumber(db, pragma) {
35
- const rows = db.pragma(pragma);
36
- const row = rows?.[0];
37
- return row ? Number(Object.values(row)[0]) : 0;
54
+ const row = db.pragma(pragma)?.[0];
55
+ return row ? Number(Object.values(row)[0]) : 0;
38
56
  }
57
+ /**
58
+ * Journal mode straight from the SQLite file header (bytes 18/19:
59
+ * 1 = rollback journal, 2 = WAL) — readable without opening a connection.
60
+ */
39
61
  function journalModeFromHeader(file) {
40
- const fd = openSync(file, "r");
41
- try {
42
- const buf = Buffer.alloc(2);
43
- if (readSync(fd, buf, 0, 2, 18) !== 2) return "unknown";
44
- if (buf[0] === 2 && buf[1] === 2) return "wal";
45
- if (buf[0] === 1 && buf[1] === 1) return "delete";
46
- return "unknown";
47
- } finally {
48
- closeSync(fd);
49
- }
62
+ const fd = openSync(file, "r");
63
+ try {
64
+ const buf = Buffer.alloc(2);
65
+ if (readSync(fd, buf, 0, 2, 18) !== 2) return "unknown";
66
+ if (buf[0] === 2 && buf[1] === 2) return "wal";
67
+ if (buf[0] === 1 && buf[1] === 1) return "delete";
68
+ return "unknown";
69
+ } finally {
70
+ closeSync(fd);
71
+ }
50
72
  }
73
+ /**
74
+ * Compact each local libsql db file by streaming a `VACUUM INTO` copy next to
75
+ * it, then swapping the copy into place. Reports before/after sizes.
76
+ *
77
+ * Deliberately NOT an in-place `VACUUM`: that copies all live content into a
78
+ * temp store (held in memory by libsql builds with `temp_store=MEMORY`) and
79
+ * then rewrites the entire database back through the WAL — on multi-GB
80
+ * databases that has frozen whole machines via memory/IO exhaustion.
81
+ * `VACUUM INTO` reads the logical database once and writes the compacted copy
82
+ * sequentially with bounded memory, and the WAL never grows.
83
+ *
84
+ * Each file is preflighted: live bytes are estimated from
85
+ * `page_count - freelist_count` and the copy is refused unless the volume has
86
+ * `requiredFreeBytes()` available.
87
+ *
88
+ * MUST run with every connection to these files closed (the swap replaces the
89
+ * inode — a surviving connection would keep writing to the unlinked old file).
90
+ * `runStorageMaintenance()` closes storage before calling this.
91
+ */
51
92
  async function reclaimLibSQLDisk(dbFiles, onFileStart) {
52
- const results = [];
53
- for (const file of dbFiles) {
54
- if (!existsSync(file) || statSync(file).size < 100) continue;
55
- const bytesBefore = fileSizeWithWal(file);
56
- const tmp = `${file}.vacuum-tmp`;
57
- rmSync(tmp, { force: true });
58
- const probe = new Database(file);
59
- try {
60
- probe.exec("PRAGMA busy_timeout = 2000");
61
- probe.exec("PRAGMA journal_mode = WAL");
62
- probe.exec("PRAGMA journal_mode = DELETE");
63
- } catch {
64
- } finally {
65
- probe.close();
66
- }
67
- if (journalModeFromHeader(file) !== "delete") {
68
- throw new Error(
69
- `${file} is in use by another process \u2014 is another Mastra Code session running? Close other sessions and run /prune vacuum again.`
70
- );
71
- }
72
- const db = new Database(file);
73
- try {
74
- db.exec("PRAGMA busy_timeout = 2000");
75
- const pageSize = pragmaNumber(db, "page_size");
76
- const pageCount = pragmaNumber(db, "page_count");
77
- const freelistCount = pragmaNumber(db, "freelist_count");
78
- const liveBytes = Math.max(0, pageCount - freelistCount) * pageSize;
79
- const { bsize, bavail } = statfsSync(dirname(file));
80
- const freeBytes = bsize * bavail;
81
- if (freeBytes < requiredFreeBytes(liveBytes)) {
82
- throw new Error(
83
- `Not enough free disk space to compact ${file}: need ${formatBytes(requiredFreeBytes(liveBytes))} for a ${formatBytes(liveBytes)} compacted copy, but only ${formatBytes(freeBytes)} is free.`
84
- );
85
- }
86
- onFileStart?.(file, bytesBefore, liveBytes);
87
- db.exec(`VACUUM INTO '${tmp.replace(/'/g, "''")}'`);
88
- } catch (err) {
89
- rmSync(tmp, { force: true });
90
- throw err;
91
- } finally {
92
- db.close();
93
- }
94
- if (journalModeFromHeader(file) !== "delete") {
95
- rmSync(tmp, { force: true });
96
- throw new Error(
97
- `${file} was opened by another process during compaction \u2014 is another Mastra Code session running? Close other sessions and run /prune vacuum again.`
98
- );
99
- }
100
- renameSync(file, `${file}.old`);
101
- try {
102
- rmSync(`${file}-wal`, { force: true });
103
- rmSync(`${file}-shm`, { force: true });
104
- renameSync(tmp, file);
105
- } catch (err) {
106
- try {
107
- renameSync(`${file}.old`, file);
108
- } catch {
109
- throw new Error(
110
- `Failed to swap compacted copy into place AND failed to restore the original \u2014 your data is intact at ${file}.old; rename it back to ${file} manually. Original error: ${err instanceof Error ? err.message : String(err)}`
111
- );
112
- }
113
- rmSync(tmp, { force: true });
114
- throw err;
115
- }
116
- rmSync(`${file}.old`, { force: true });
117
- results.push({ file, bytesBefore, bytesAfter: fileSizeWithWal(file) });
118
- }
119
- return results;
93
+ const results = [];
94
+ for (const file of dbFiles) {
95
+ if (!existsSync(file) || statSync(file).size < 100) continue;
96
+ const bytesBefore = fileSizeWithWal(file);
97
+ const tmp = `${file}.vacuum-tmp`;
98
+ rmSync(tmp, { force: true });
99
+ const probe = new Database(file);
100
+ try {
101
+ probe.exec("PRAGMA busy_timeout = 2000");
102
+ probe.exec("PRAGMA journal_mode = WAL");
103
+ probe.exec("PRAGMA journal_mode = DELETE");
104
+ } catch {} finally {
105
+ probe.close();
106
+ }
107
+ if (journalModeFromHeader(file) !== "delete") throw new Error(`${file} is in use by another process — is another Mastra Code session running? Close other sessions and run /prune vacuum again.`);
108
+ const db = new Database(file);
109
+ try {
110
+ db.exec("PRAGMA busy_timeout = 2000");
111
+ const pageSize = pragmaNumber(db, "page_size");
112
+ const pageCount = pragmaNumber(db, "page_count");
113
+ const freelistCount = pragmaNumber(db, "freelist_count");
114
+ const liveBytes = Math.max(0, pageCount - freelistCount) * pageSize;
115
+ const { bsize, bavail } = statfsSync(dirname(file));
116
+ const freeBytes = bsize * bavail;
117
+ if (freeBytes < requiredFreeBytes(liveBytes)) throw new Error(`Not enough free disk space to compact ${file}: need ${formatBytes(requiredFreeBytes(liveBytes))} for a ${formatBytes(liveBytes)} compacted copy, but only ${formatBytes(freeBytes)} is free.`);
118
+ onFileStart?.(file, bytesBefore, liveBytes);
119
+ db.exec(`VACUUM INTO '${tmp.replace(/'/g, "''")}'`);
120
+ } catch (err) {
121
+ rmSync(tmp, { force: true });
122
+ throw err;
123
+ } finally {
124
+ db.close();
125
+ }
126
+ if (journalModeFromHeader(file) !== "delete") {
127
+ rmSync(tmp, { force: true });
128
+ throw new Error(`${file} was opened by another process during compaction — is another Mastra Code session running? Close other sessions and run /prune vacuum again.`);
129
+ }
130
+ renameSync(file, `${file}.old`);
131
+ try {
132
+ rmSync(`${file}-wal`, { force: true });
133
+ rmSync(`${file}-shm`, { force: true });
134
+ renameSync(tmp, file);
135
+ } catch (err) {
136
+ try {
137
+ renameSync(`${file}.old`, file);
138
+ } catch {
139
+ throw new Error(`Failed to swap compacted copy into place AND failed to restore the original — your data is intact at ${file}.old; rename it back to ${file} manually. Original error: ${err instanceof Error ? err.message : String(err)}`);
140
+ }
141
+ rmSync(tmp, { force: true });
142
+ throw err;
143
+ }
144
+ rmSync(`${file}.old`, { force: true });
145
+ results.push({
146
+ file,
147
+ bytesBefore,
148
+ bytesAfter: fileSizeWithWal(file)
149
+ });
150
+ }
151
+ return results;
120
152
  }
153
+ /** `file:/path` or `file:///path` → filesystem path; undefined for non-file urls. */
121
154
  function fileUrlToPath(url) {
122
- if (!url.startsWith("file:")) return void 0;
123
- return url.replace(/^file:\/\//, "").replace(/^file:/, "");
155
+ if (!url.startsWith("file:")) return void 0;
156
+ return url.replace(/^file:\/\//, "").replace(/^file:/, "");
124
157
  }
158
+ /**
159
+ * Resolve the local libsql db files eligible for checkpoint + VACUUM.
160
+ * Returns [] for remote (turso) urls and PG backends. When PG was requested
161
+ * but the factory fell back to libsql, the fallback uses the default files.
162
+ */
125
163
  function resolveLocalDbFiles(config, effectiveBackend) {
126
- if (effectiveBackend !== "libsql") return [];
127
- if (config.backend === "pg") {
128
- return [getDatabasePath(), getVectorDatabasePath()];
129
- }
130
- if (config.isRemote) return [];
131
- const files = [];
132
- const main = fileUrlToPath(config.url);
133
- if (main) files.push(main);
134
- const vector = config.vectorUrl ? fileUrlToPath(config.vectorUrl) : getVectorDatabasePath();
135
- if (vector) files.push(vector);
136
- return files;
164
+ if (effectiveBackend !== "libsql") return [];
165
+ if (config.backend === "pg") return [getDatabasePath(), getVectorDatabasePath()];
166
+ if (config.isRemote) return [];
167
+ const files = [];
168
+ const main = fileUrlToPath(config.url);
169
+ if (main) files.push(main);
170
+ const vector = config.vectorUrl ? fileUrlToPath(config.vectorUrl) : getVectorDatabasePath();
171
+ if (vector) files.push(vector);
172
+ return files;
137
173
  }
174
+ /**
175
+ * Build the maintenance handle for the effective storage backend.
176
+ * `localDbFiles` are the local libsql file paths eligible for VACUUM;
177
+ * pass an empty array for remote (turso) or PG backends.
178
+ */
138
179
  function createStorageMaintenance(opts) {
139
- const { storage, backend, retention, localDbFiles, closeVector } = opts;
140
- return {
141
- backend,
142
- retention,
143
- prune: (options) => storage.prune(options),
144
- closeStorage: async () => {
145
- await storage.close?.();
146
- await closeVector?.();
147
- },
148
- ...backend === "libsql" && localDbFiles.length > 0 ? {
149
- reclaimDisk: (onFileStart) => reclaimLibSQLDisk(localDbFiles, onFileStart)
150
- } : {}
151
- };
180
+ const { storage, backend, retention, localDbFiles, closeVector } = opts;
181
+ return {
182
+ backend,
183
+ retention,
184
+ prune: (options) => storage.prune(options),
185
+ closeStorage: async () => {
186
+ await storage.close?.();
187
+ await closeVector?.();
188
+ },
189
+ ...backend === "libsql" && localDbFiles.length > 0 ? { reclaimDisk: (onFileStart) => reclaimLibSQLDisk(localDbFiles, onFileStart) } : {}
190
+ };
152
191
  }
153
192
  function formatBytes(bytes) {
154
- if (bytes < 1024) return `${bytes} B`;
155
- const units = ["KB", "MB", "GB", "TB"];
156
- let value = bytes;
157
- let unit = "B";
158
- for (const next of units) {
159
- if (value < 1024) break;
160
- value /= 1024;
161
- unit = next;
162
- }
163
- return `${value.toFixed(1)} ${unit}`;
193
+ if (bytes < 1024) return `${bytes} B`;
194
+ const units = [
195
+ "KB",
196
+ "MB",
197
+ "GB",
198
+ "TB"
199
+ ];
200
+ let value = bytes;
201
+ let unit = "B";
202
+ for (const next of units) {
203
+ if (value < 1024) break;
204
+ value /= 1024;
205
+ unit = next;
206
+ }
207
+ return `${value.toFixed(1)} ${unit}`;
164
208
  }
209
+ /** Rows deleted per table per pass: default batchSize (1000) × 20 batches. */
165
210
  const PASS_MAX_BATCHES = 20;
211
+ /**
212
+ * Run storage maintenance with exclusive access to the database, reporting
213
+ * progress through `log` (one line per pass so large deletes are visible).
214
+ *
215
+ * Must run AFTER the TUI is stopped and background writers are quiesced —
216
+ * batched deletes and VACUUM contend with live writes (SQLITE_BUSY) and can
217
+ * starve the event loop that renders the TUI. The storage connection is
218
+ * closed at the end (checkpointing the WAL for local libsql), so the process
219
+ * must exit afterwards.
220
+ */
166
221
  async function runStorageMaintenance(opts) {
167
- const { maintenance, vacuum, keepMemory = false, log } = opts;
168
- const { memory: _memory, ...withoutMemory } = maintenance.retention;
169
- const retentionForRun = keepMemory ? { retention: withoutMemory } : {};
170
- log("Pruning rows older than the retention policies:");
171
- for (const [domain, tables] of Object.entries(maintenance.retention)) {
172
- for (const [table, policy] of Object.entries(tables ?? {})) {
173
- const kept = keepMemory && domain === "memory";
174
- log(` ${domain}.${table}: ${kept ? "kept (keep-memory)" : policy.maxAge}`);
175
- }
176
- }
177
- const totals = /* @__PURE__ */ new Map();
178
- for (; ; ) {
179
- const results = await maintenance.prune({ maxBatches: PASS_MAX_BATCHES, ...retentionForRun });
180
- if (results.length === 0) break;
181
- let remaining = false;
182
- let deletedThisPass = 0;
183
- for (const r of results) {
184
- const key = `${r.domain}.${r.table}`;
185
- totals.set(key, (totals.get(key) ?? 0) + r.deleted);
186
- deletedThisPass += r.deleted;
187
- if (!r.done) remaining = true;
188
- if (r.deleted > 0) {
189
- log(` ${key}: ${totals.get(key)} rows deleted${r.done ? "" : " so far\u2026"}`);
190
- }
191
- }
192
- if (!remaining) break;
193
- if (deletedThisPass === 0) {
194
- log(" stopping: no progress in the last pass.");
195
- break;
196
- }
197
- }
198
- const totalDeleted = [...totals.values()].reduce((sum, n) => sum + n, 0);
199
- log(
200
- totalDeleted === 0 ? "Nothing to prune \u2014 no retention-eligible rows found." : `Prune complete: ${totalDeleted} rows deleted.`
201
- );
202
- await maintenance.closeStorage?.();
203
- if (!vacuum) {
204
- if (maintenance.reclaimDisk) {
205
- log("Deleted rows free pages inside the db file but not on disk. Run /prune vacuum to reclaim disk space.");
206
- }
207
- return;
208
- }
209
- if (!maintenance.reclaimDisk) {
210
- log("Disk reclamation (VACUUM) is only available for local libsql storage.");
211
- return;
212
- }
213
- log("Reclaiming disk (compacting database files)\u2026 this can take several minutes on large databases.");
214
- const reclaimed = await maintenance.reclaimDisk((file, bytesBefore, liveBytes) => {
215
- log(` vacuuming ${file} (${formatBytes(bytesBefore)}, ~${formatBytes(liveBytes)} live)\u2026`);
216
- });
217
- if (reclaimed.length === 0) {
218
- log("No local database files found to vacuum.");
219
- return;
220
- }
221
- for (const r of reclaimed) {
222
- log(` ${r.file}: ${formatBytes(r.bytesBefore)} \u2192 ${formatBytes(r.bytesAfter)}`);
223
- }
224
- const saved = reclaimed.reduce((sum, r) => sum + Math.max(0, r.bytesBefore - r.bytesAfter), 0);
225
- log(`Reclaimed ${formatBytes(saved)}.`);
222
+ const { maintenance, vacuum, keepMemory = false, log } = opts;
223
+ const { memory: _memory, ...withoutMemory } = maintenance.retention;
224
+ const retentionForRun = keepMemory ? { retention: withoutMemory } : {};
225
+ log("Pruning rows older than the retention policies:");
226
+ for (const [domain, tables] of Object.entries(maintenance.retention)) for (const [table, policy] of Object.entries(tables ?? {})) log(` ${domain}.${table}: ${keepMemory && domain === "memory" ? "kept (keep-memory)" : policy.maxAge}`);
227
+ const totals = /* @__PURE__ */ new Map();
228
+ for (;;) {
229
+ const results = await maintenance.prune({
230
+ maxBatches: PASS_MAX_BATCHES,
231
+ ...retentionForRun
232
+ });
233
+ if (results.length === 0) break;
234
+ let remaining = false;
235
+ let deletedThisPass = 0;
236
+ for (const r of results) {
237
+ const key = `${r.domain}.${r.table}`;
238
+ totals.set(key, (totals.get(key) ?? 0) + r.deleted);
239
+ deletedThisPass += r.deleted;
240
+ if (!r.done) remaining = true;
241
+ if (r.deleted > 0) log(` ${key}: ${totals.get(key)} rows deleted${r.done ? "" : " so far…"}`);
242
+ }
243
+ if (!remaining) break;
244
+ if (deletedThisPass === 0) {
245
+ log(" stopping: no progress in the last pass.");
246
+ break;
247
+ }
248
+ }
249
+ const totalDeleted = [...totals.values()].reduce((sum, n) => sum + n, 0);
250
+ log(totalDeleted === 0 ? "Nothing to prune — no retention-eligible rows found." : `Prune complete: ${totalDeleted} rows deleted.`);
251
+ await maintenance.closeStorage?.();
252
+ if (!vacuum) {
253
+ if (maintenance.reclaimDisk) log("Deleted rows free pages inside the db file but not on disk. Run /prune vacuum to reclaim disk space.");
254
+ return;
255
+ }
256
+ if (!maintenance.reclaimDisk) {
257
+ log("Disk reclamation (VACUUM) is only available for local libsql storage.");
258
+ return;
259
+ }
260
+ log("Reclaiming disk (compacting database files)… this can take several minutes on large databases.");
261
+ const reclaimed = await maintenance.reclaimDisk((file, bytesBefore, liveBytes) => {
262
+ log(` vacuuming ${file} (${formatBytes(bytesBefore)}, ~${formatBytes(liveBytes)} live)…`);
263
+ });
264
+ if (reclaimed.length === 0) {
265
+ log("No local database files found to vacuum.");
266
+ return;
267
+ }
268
+ for (const r of reclaimed) log(` ${r.file}: ${formatBytes(r.bytesBefore)} ${formatBytes(r.bytesAfter)}`);
269
+ log(`Reclaimed ${formatBytes(reclaimed.reduce((sum, r) => sum + Math.max(0, r.bytesBefore - r.bytesAfter), 0))}.`);
226
270
  }
227
- export {
228
- DEFAULT_RETENTION,
229
- createStorageMaintenance,
230
- formatBytes,
231
- reclaimLibSQLDisk,
232
- requiredFreeBytes,
233
- resolveLocalDbFiles,
234
- runStorageMaintenance
235
- };
271
+ //#endregion
272
+ export { DEFAULT_RETENTION, createStorageMaintenance, formatBytes, reclaimLibSQLDisk, requiredFreeBytes, resolveLocalDbFiles, runStorageMaintenance };
273
+
236
274
  //# sourceMappingURL=storage-maintenance.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/utils/storage-maintenance.ts"],"sourcesContent":["/**\n * Storage maintenance — retention defaults and disk reclamation for mastracode.\n *\n * Retention (`DEFAULT_RETENTION`) is wired into the inner store by the storage\n * factory so `storage.prune()` can delete aged rows across every domain,\n * including the legacy libsql observability spans table.\n *\n * `prune()` only deletes rows — SQLite keeps the freed pages inside the file.\n * `reclaimLibSQLDisk()` returns that space to the OS by streaming a\n * `VACUUM INTO` copy of each file and swapping it into place. It requires all\n * connections to be closed first, so it only runs from the post-TUI\n * maintenance flow. We deliberately do NOT expose VACUUM as a public API on\n * LibSQLStore.\n */\n\nimport { closeSync, existsSync, openSync, readSync, renameSync, rmSync, statSync, statfsSync } from 'node:fs';\nimport { dirname } from 'node:path';\n\nimport type { MastraCompositeStore, PruneOptions, PruneResult, RetentionConfig } from '@mastra/core/storage';\n// Native `libsql` driver, deliberately alongside `@libsql/client` (used by the\n// stores): maintenance needs deterministic connection close for the\n// exclusivity probe, and the wrapper's close() can leave the file lock held\n// until GC finalizes cached statements. Don't \"clean up\" either dependency.\n// Upstream report: https://github.com/tursodatabase/libsql-js/issues/228\nimport Database from 'libsql';\n\nimport type { StorageConfig } from './project.js';\nimport { getDatabasePath, getVectorDatabasePath } from './project.js';\n\n/**\n * Default retention for mastracode's own storage. Tables not listed are kept\n * forever. Ages chosen for a local dev tool where observability data dwarfs\n * everything else (spans have been observed at 17+ GB):\n * - spans/logs: short-lived debugging telemetry → 14d\n * - scorer results & workflow snapshots: medium-lived run records → 30d\n * - chat history (messages/threads): long-lived, user-visible → 90d\n */\nexport const DEFAULT_RETENTION: RetentionConfig = {\n memory: {\n messages: { maxAge: '90d' },\n threads: { maxAge: '90d' },\n },\n observability: {\n spans: { maxAge: '14d' },\n logs: { maxAge: '14d' },\n },\n scores: {\n scorers: { maxAge: '30d' },\n },\n workflows: {\n workflowSnapshot: { maxAge: '30d' },\n },\n};\n\n/** Result of reclaiming disk for one database file (sizes include the WAL). */\nexport interface ReclaimResult {\n file: string;\n bytesBefore: number;\n bytesAfter: number;\n}\n\n/** Handle the TUI uses to run storage maintenance without reaching into store internals. */\nexport interface StorageMaintenance {\n backend: 'libsql' | 'pg';\n /** Retention policies in effect (for display in /prune output). */\n retention: RetentionConfig;\n /** Delete rows older than the retention policies. Safe to re-run; batched and cancellable. `options.retention` replaces the standing policies for this call only. */\n prune: (options?: PruneOptions) => Promise<PruneResult[]>;\n /** Close the long-lived storage connection (checkpoints WAL for local libsql). Nothing can use the store afterwards. */\n closeStorage?: () => Promise<void>;\n /** Compact local libsql files (VACUUM INTO + swap) to return freed pages to the OS. Only set for local libsql backends. */\n reclaimDisk?: (\n onFileStart?: (file: string, bytesBefore: number, liveBytes: number) => void,\n ) => Promise<ReclaimResult[]>;\n}\n\n/** db file size + its WAL sidecar, in bytes. Missing files count as 0. */\nfunction fileSizeWithWal(dbFile: string): number {\n let total = 0;\n for (const candidate of [dbFile, `${dbFile}-wal`]) {\n try {\n total += statSync(candidate).size;\n } catch {\n // file may not exist (e.g. WAL already truncated)\n }\n }\n return total;\n}\n\n/** Free bytes required before compacting: the compacted copy plus 20% + 256 MB headroom. */\nexport function requiredFreeBytes(liveBytes: number): number {\n return Math.ceil(liveBytes * 1.2) + 256 * 1024 * 1024;\n}\n\n/** First column of the first row of a PRAGMA result, as a number. */\nfunction pragmaNumber(db: InstanceType<typeof Database>, pragma: string): number {\n const rows = db.pragma(pragma) as Array<Record<string, unknown>> | undefined;\n const row = rows?.[0];\n return row ? Number(Object.values(row)[0]) : 0;\n}\n\n/**\n * Journal mode straight from the SQLite file header (bytes 18/19:\n * 1 = rollback journal, 2 = WAL) — readable without opening a connection.\n */\nfunction journalModeFromHeader(file: string): 'wal' | 'delete' | 'unknown' {\n const fd = openSync(file, 'r');\n try {\n const buf = Buffer.alloc(2);\n if (readSync(fd, buf, 0, 2, 18) !== 2) return 'unknown';\n if (buf[0] === 2 && buf[1] === 2) return 'wal';\n if (buf[0] === 1 && buf[1] === 1) return 'delete';\n return 'unknown';\n } finally {\n closeSync(fd);\n }\n}\n\n/**\n * Compact each local libsql db file by streaming a `VACUUM INTO` copy next to\n * it, then swapping the copy into place. Reports before/after sizes.\n *\n * Deliberately NOT an in-place `VACUUM`: that copies all live content into a\n * temp store (held in memory by libsql builds with `temp_store=MEMORY`) and\n * then rewrites the entire database back through the WAL — on multi-GB\n * databases that has frozen whole machines via memory/IO exhaustion.\n * `VACUUM INTO` reads the logical database once and writes the compacted copy\n * sequentially with bounded memory, and the WAL never grows.\n *\n * Each file is preflighted: live bytes are estimated from\n * `page_count - freelist_count` and the copy is refused unless the volume has\n * `requiredFreeBytes()` available.\n *\n * MUST run with every connection to these files closed (the swap replaces the\n * inode — a surviving connection would keep writing to the unlinked old file).\n * `runStorageMaintenance()` closes storage before calling this.\n */\nexport async function reclaimLibSQLDisk(\n dbFiles: string[],\n onFileStart?: (file: string, bytesBefore: number, liveBytes: number) => void,\n): Promise<ReclaimResult[]> {\n const results: ReclaimResult[] = [];\n for (const file of dbFiles) {\n // Skip missing files and files too small to have a SQLite header — nothing to reclaim\n if (!existsSync(file) || statSync(file).size < 100) continue;\n const bytesBefore = fileSizeWithWal(file);\n const tmp = `${file}.vacuum-tmp`;\n rmSync(tmp, { force: true });\n // Prove we're the only connection before the swap: every open WAL-mode\n // connection (all Mastra Code sessions run WAL) holds a shared-memory\n // lock, so switching WAL -> DELETE only succeeds with exclusive access.\n // A surviving connection would keep writing to the unlinked old inode\n // after the swap and silently lose data.\n //\n // The probe uses exec() only and verifies the outcome from the SQLite\n // file header: prepared statements (including db.pragma()) pin a libsql\n // connection open past close(), so a failed in-connection probe would\n // itself hold the WAL lock and poison every retry in this process.\n // Upstream bug (libsql 0.5.x / @libsql/client 0.17.x): Database.close()\n // does not finalize outstanding statements — they are only finalized by\n // GC, so lock release after close() is nondeterministic. Reported as\n // https://github.com/tursodatabase/libsql-js/issues/228 (fix in flight in\n // PR #214). Once that lands, the header check can become a plain\n // journal_mode query.\n const probe = new Database(file);\n try {\n probe.exec('PRAGMA busy_timeout = 2000');\n // Round-trip through WAL so the probe also works when a clean shutdown\n // already left the file in rollback mode.\n probe.exec('PRAGMA journal_mode = WAL');\n probe.exec('PRAGMA journal_mode = DELETE');\n } catch {\n // busy — the header check below reports the failure\n } finally {\n probe.close();\n }\n if (journalModeFromHeader(file) !== 'delete') {\n throw new Error(\n `${file} is in use by another process — is another Mastra Code session running? ` +\n `Close other sessions and run /prune vacuum again.`,\n );\n }\n // Native `libsql` driver, not `@libsql/client`: the wrapper's close() can\n // leave the file lock held in-process (cached statements), which would\n // block other connections afterwards. This connection stays in rollback\n // mode, so even a pinned statement holds no lock once we're done.\n const db = new Database(file);\n try {\n db.exec('PRAGMA busy_timeout = 2000');\n const pageSize = pragmaNumber(db, 'page_size');\n const pageCount = pragmaNumber(db, 'page_count');\n const freelistCount = pragmaNumber(db, 'freelist_count');\n const liveBytes = Math.max(0, pageCount - freelistCount) * pageSize;\n const { bsize, bavail } = statfsSync(dirname(file));\n const freeBytes = bsize * bavail;\n if (freeBytes < requiredFreeBytes(liveBytes)) {\n throw new Error(\n `Not enough free disk space to compact ${file}: need ${formatBytes(requiredFreeBytes(liveBytes))} ` +\n `for a ${formatBytes(liveBytes)} compacted copy, but only ${formatBytes(freeBytes)} is free.`,\n );\n }\n onFileStart?.(file, bytesBefore, liveBytes);\n db.exec(`VACUUM INTO '${tmp.replace(/'/g, \"''\")}'`);\n } catch (err) {\n rmSync(tmp, { force: true });\n throw err;\n } finally {\n db.close();\n }\n // Re-probe right before the swap: `VACUUM INTO` can take tens of seconds\n // on multi-GB files, and a session started in that window reopens the db\n // in WAL mode (flipping the header back). Swapping under it would orphan\n // that session's inode and silently lose its writes.\n if (journalModeFromHeader(file) !== 'delete') {\n rmSync(tmp, { force: true });\n throw new Error(\n `${file} was opened by another process during compaction — is another Mastra Code session running? ` +\n `Close other sessions and run /prune vacuum again.`,\n );\n }\n // Swap the compacted copy into place. The old WAL/SHM sidecars belong to\n // the old inode — they must never be paired with the new file. If any step\n // after the first rename fails, restore the original so the db path is\n // never left empty (a naive restart would otherwise create a fresh db).\n renameSync(file, `${file}.old`);\n try {\n rmSync(`${file}-wal`, { force: true });\n rmSync(`${file}-shm`, { force: true });\n renameSync(tmp, file);\n } catch (err) {\n try {\n renameSync(`${file}.old`, file);\n } catch {\n throw new Error(\n `Failed to swap compacted copy into place AND failed to restore the original — ` +\n `your data is intact at ${file}.old; rename it back to ${file} manually. ` +\n `Original error: ${err instanceof Error ? err.message : String(err)}`,\n );\n }\n rmSync(tmp, { force: true });\n throw err;\n }\n rmSync(`${file}.old`, { force: true });\n results.push({ file, bytesBefore, bytesAfter: fileSizeWithWal(file) });\n }\n return results;\n}\n\n/** `file:/path` or `file:///path` → filesystem path; undefined for non-file urls. */\nfunction fileUrlToPath(url: string): string | undefined {\n if (!url.startsWith('file:')) return undefined;\n return url.replace(/^file:\\/\\//, '').replace(/^file:/, '');\n}\n\n/**\n * Resolve the local libsql db files eligible for checkpoint + VACUUM.\n * Returns [] for remote (turso) urls and PG backends. When PG was requested\n * but the factory fell back to libsql, the fallback uses the default files.\n */\nexport function resolveLocalDbFiles(config: StorageConfig, effectiveBackend: 'libsql' | 'pg'): string[] {\n if (effectiveBackend !== 'libsql') return [];\n if (config.backend === 'pg') {\n // PG requested but fell back to the default local libsql files\n return [getDatabasePath(), getVectorDatabasePath()];\n }\n if (config.isRemote) return [];\n const files: string[] = [];\n const main = fileUrlToPath(config.url);\n if (main) files.push(main);\n const vector = config.vectorUrl ? fileUrlToPath(config.vectorUrl) : getVectorDatabasePath();\n if (vector) files.push(vector);\n return files;\n}\n\n/**\n * Build the maintenance handle for the effective storage backend.\n * `localDbFiles` are the local libsql file paths eligible for VACUUM;\n * pass an empty array for remote (turso) or PG backends.\n */\nexport function createStorageMaintenance(opts: {\n storage: MastraCompositeStore;\n backend: 'libsql' | 'pg';\n retention: RetentionConfig;\n localDbFiles: string[];\n /** Closes the vector store's connection; required for local libsql so the compaction swap isn't blocked by our own handle. */\n closeVector?: () => Promise<void>;\n}): StorageMaintenance {\n const { storage, backend, retention, localDbFiles, closeVector } = opts;\n return {\n backend,\n retention,\n prune: options => storage.prune(options),\n closeStorage: async () => {\n await storage.close?.();\n await closeVector?.();\n },\n ...(backend === 'libsql' && localDbFiles.length > 0\n ? {\n reclaimDisk: (onFileStart?: (file: string, bytesBefore: number, liveBytes: number) => void) =>\n reclaimLibSQLDisk(localDbFiles, onFileStart),\n }\n : {}),\n };\n}\n\nexport function formatBytes(bytes: number): string {\n if (bytes < 1024) return `${bytes} B`;\n const units = ['KB', 'MB', 'GB', 'TB'];\n let value = bytes;\n let unit = 'B';\n for (const next of units) {\n if (value < 1024) break;\n value /= 1024;\n unit = next;\n }\n return `${value.toFixed(1)} ${unit}`;\n}\n\n/** Rows deleted per table per pass: default batchSize (1000) × 20 batches. */\nconst PASS_MAX_BATCHES = 20;\n\n/**\n * Run storage maintenance with exclusive access to the database, reporting\n * progress through `log` (one line per pass so large deletes are visible).\n *\n * Must run AFTER the TUI is stopped and background writers are quiesced —\n * batched deletes and VACUUM contend with live writes (SQLITE_BUSY) and can\n * starve the event loop that renders the TUI. The storage connection is\n * closed at the end (checkpointing the WAL for local libsql), so the process\n * must exit afterwards.\n */\nexport async function runStorageMaintenance(opts: {\n maintenance: StorageMaintenance;\n vacuum: boolean;\n /** Skip the memory domain (messages/threads) so chat history is preserved. */\n keepMemory?: boolean;\n log: (line: string) => void;\n}): Promise<void> {\n const { maintenance, vacuum, keepMemory = false, log } = opts;\n\n // keep-memory: prune with the standing policies minus the memory domain,\n // for this run only. The configured retention is untouched.\n const { memory: _memory, ...withoutMemory } = maintenance.retention;\n const retentionForRun = keepMemory ? { retention: withoutMemory } : {};\n\n log('Pruning rows older than the retention policies:');\n for (const [domain, tables] of Object.entries(maintenance.retention)) {\n for (const [table, policy] of Object.entries(tables ?? {})) {\n const kept = keepMemory && domain === 'memory';\n log(` ${domain}.${table}: ${kept ? 'kept (keep-memory)' : (policy as { maxAge: string | number }).maxAge}`);\n }\n }\n\n const totals = new Map<string, number>();\n for (;;) {\n const results = await maintenance.prune({ maxBatches: PASS_MAX_BATCHES, ...retentionForRun });\n if (results.length === 0) break;\n let remaining = false;\n let deletedThisPass = 0;\n for (const r of results) {\n const key = `${r.domain}.${r.table}`;\n totals.set(key, (totals.get(key) ?? 0) + r.deleted);\n deletedThisPass += r.deleted;\n if (!r.done) remaining = true;\n if (r.deleted > 0) {\n log(` ${key}: ${totals.get(key)} rows deleted${r.done ? '' : ' so far…'}`);\n }\n }\n if (!remaining) break;\n // Safety valve: a table reported more eligible rows but nothing was\n // deleted this pass — bail instead of spinning forever.\n if (deletedThisPass === 0) {\n log(' stopping: no progress in the last pass.');\n break;\n }\n }\n const totalDeleted = [...totals.values()].reduce((sum, n) => sum + n, 0);\n log(\n totalDeleted === 0\n ? 'Nothing to prune — no retention-eligible rows found.'\n : `Prune complete: ${totalDeleted} rows deleted.`,\n );\n\n // Release the long-lived connection: checkpoints the WAL for local libsql\n // and guarantees VACUUM below isn't blocked by our own open handles.\n await maintenance.closeStorage?.();\n\n if (!vacuum) {\n if (maintenance.reclaimDisk) {\n log('Deleted rows free pages inside the db file but not on disk. Run /prune vacuum to reclaim disk space.');\n }\n return;\n }\n\n if (!maintenance.reclaimDisk) {\n log('Disk reclamation (VACUUM) is only available for local libsql storage.');\n return;\n }\n\n log('Reclaiming disk (compacting database files)… this can take several minutes on large databases.');\n const reclaimed = await maintenance.reclaimDisk((file, bytesBefore, liveBytes) => {\n log(` vacuuming ${file} (${formatBytes(bytesBefore)}, ~${formatBytes(liveBytes)} live)…`);\n });\n if (reclaimed.length === 0) {\n log('No local database files found to vacuum.');\n return;\n }\n for (const r of reclaimed) {\n log(` ${r.file}: ${formatBytes(r.bytesBefore)} → ${formatBytes(r.bytesAfter)}`);\n }\n const saved = reclaimed.reduce((sum, r) => sum + Math.max(0, r.bytesBefore - r.bytesAfter), 0);\n log(`Reclaimed ${formatBytes(saved)}.`);\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"file":"storage-maintenance.js","names":[],"sources":["../../src/utils/storage-maintenance.ts"],"sourcesContent":["/**\n * Storage maintenance — retention defaults and disk reclamation for mastracode.\n *\n * Retention (`DEFAULT_RETENTION`) is wired into the inner store by the storage\n * factory so `storage.prune()` can delete aged rows across every domain,\n * including the legacy libsql observability spans table.\n *\n * `prune()` only deletes rows — SQLite keeps the freed pages inside the file.\n * `reclaimLibSQLDisk()` returns that space to the OS by streaming a\n * `VACUUM INTO` copy of each file and swapping it into place. It requires all\n * connections to be closed first, so it only runs from the post-TUI\n * maintenance flow. We deliberately do NOT expose VACUUM as a public API on\n * LibSQLStore.\n */\n\nimport { closeSync, existsSync, openSync, readSync, renameSync, rmSync, statSync, statfsSync } from 'node:fs';\nimport { dirname } from 'node:path';\n\nimport type { MastraCompositeStore, PruneOptions, PruneResult, RetentionConfig } from '@mastra/core/storage';\n// Native `libsql` driver, deliberately alongside `@libsql/client` (used by the\n// stores): maintenance needs deterministic connection close for the\n// exclusivity probe, and the wrapper's close() can leave the file lock held\n// until GC finalizes cached statements. Don't \"clean up\" either dependency.\n// Upstream report: https://github.com/tursodatabase/libsql-js/issues/228\nimport Database from 'libsql';\n\nimport type { StorageConfig } from './project.js';\nimport { getDatabasePath, getVectorDatabasePath } from './project.js';\n\n/**\n * Default retention for mastracode's own storage. Tables not listed are kept\n * forever. Ages chosen for a local dev tool where observability data dwarfs\n * everything else (spans have been observed at 17+ GB):\n * - spans/logs: short-lived debugging telemetry → 14d\n * - scorer results & workflow snapshots: medium-lived run records → 30d\n * - chat history (messages/threads): long-lived, user-visible → 90d\n */\nexport const DEFAULT_RETENTION: RetentionConfig = {\n memory: {\n messages: { maxAge: '90d' },\n threads: { maxAge: '90d' },\n },\n observability: {\n spans: { maxAge: '14d' },\n logs: { maxAge: '14d' },\n },\n scores: {\n scorers: { maxAge: '30d' },\n },\n workflows: {\n workflowSnapshot: { maxAge: '30d' },\n },\n};\n\n/** Result of reclaiming disk for one database file (sizes include the WAL). */\nexport interface ReclaimResult {\n file: string;\n bytesBefore: number;\n bytesAfter: number;\n}\n\n/** Handle the TUI uses to run storage maintenance without reaching into store internals. */\nexport interface StorageMaintenance {\n backend: 'libsql' | 'pg';\n /** Retention policies in effect (for display in /prune output). */\n retention: RetentionConfig;\n /** Delete rows older than the retention policies. Safe to re-run; batched and cancellable. `options.retention` replaces the standing policies for this call only. */\n prune: (options?: PruneOptions) => Promise<PruneResult[]>;\n /** Close the long-lived storage connection (checkpoints WAL for local libsql). Nothing can use the store afterwards. */\n closeStorage?: () => Promise<void>;\n /** Compact local libsql files (VACUUM INTO + swap) to return freed pages to the OS. Only set for local libsql backends. */\n reclaimDisk?: (\n onFileStart?: (file: string, bytesBefore: number, liveBytes: number) => void,\n ) => Promise<ReclaimResult[]>;\n}\n\n/** db file size + its WAL sidecar, in bytes. Missing files count as 0. */\nfunction fileSizeWithWal(dbFile: string): number {\n let total = 0;\n for (const candidate of [dbFile, `${dbFile}-wal`]) {\n try {\n total += statSync(candidate).size;\n } catch {\n // file may not exist (e.g. WAL already truncated)\n }\n }\n return total;\n}\n\n/** Free bytes required before compacting: the compacted copy plus 20% + 256 MB headroom. */\nexport function requiredFreeBytes(liveBytes: number): number {\n return Math.ceil(liveBytes * 1.2) + 256 * 1024 * 1024;\n}\n\n/** First column of the first row of a PRAGMA result, as a number. */\nfunction pragmaNumber(db: InstanceType<typeof Database>, pragma: string): number {\n const rows = db.pragma(pragma) as Array<Record<string, unknown>> | undefined;\n const row = rows?.[0];\n return row ? Number(Object.values(row)[0]) : 0;\n}\n\n/**\n * Journal mode straight from the SQLite file header (bytes 18/19:\n * 1 = rollback journal, 2 = WAL) — readable without opening a connection.\n */\nfunction journalModeFromHeader(file: string): 'wal' | 'delete' | 'unknown' {\n const fd = openSync(file, 'r');\n try {\n const buf = Buffer.alloc(2);\n if (readSync(fd, buf, 0, 2, 18) !== 2) return 'unknown';\n if (buf[0] === 2 && buf[1] === 2) return 'wal';\n if (buf[0] === 1 && buf[1] === 1) return 'delete';\n return 'unknown';\n } finally {\n closeSync(fd);\n }\n}\n\n/**\n * Compact each local libsql db file by streaming a `VACUUM INTO` copy next to\n * it, then swapping the copy into place. Reports before/after sizes.\n *\n * Deliberately NOT an in-place `VACUUM`: that copies all live content into a\n * temp store (held in memory by libsql builds with `temp_store=MEMORY`) and\n * then rewrites the entire database back through the WAL — on multi-GB\n * databases that has frozen whole machines via memory/IO exhaustion.\n * `VACUUM INTO` reads the logical database once and writes the compacted copy\n * sequentially with bounded memory, and the WAL never grows.\n *\n * Each file is preflighted: live bytes are estimated from\n * `page_count - freelist_count` and the copy is refused unless the volume has\n * `requiredFreeBytes()` available.\n *\n * MUST run with every connection to these files closed (the swap replaces the\n * inode — a surviving connection would keep writing to the unlinked old file).\n * `runStorageMaintenance()` closes storage before calling this.\n */\nexport async function reclaimLibSQLDisk(\n dbFiles: string[],\n onFileStart?: (file: string, bytesBefore: number, liveBytes: number) => void,\n): Promise<ReclaimResult[]> {\n const results: ReclaimResult[] = [];\n for (const file of dbFiles) {\n // Skip missing files and files too small to have a SQLite header — nothing to reclaim\n if (!existsSync(file) || statSync(file).size < 100) continue;\n const bytesBefore = fileSizeWithWal(file);\n const tmp = `${file}.vacuum-tmp`;\n rmSync(tmp, { force: true });\n // Prove we're the only connection before the swap: every open WAL-mode\n // connection (all Mastra Code sessions run WAL) holds a shared-memory\n // lock, so switching WAL -> DELETE only succeeds with exclusive access.\n // A surviving connection would keep writing to the unlinked old inode\n // after the swap and silently lose data.\n //\n // The probe uses exec() only and verifies the outcome from the SQLite\n // file header: prepared statements (including db.pragma()) pin a libsql\n // connection open past close(), so a failed in-connection probe would\n // itself hold the WAL lock and poison every retry in this process.\n // Upstream bug (libsql 0.5.x / @libsql/client 0.17.x): Database.close()\n // does not finalize outstanding statements — they are only finalized by\n // GC, so lock release after close() is nondeterministic. Reported as\n // https://github.com/tursodatabase/libsql-js/issues/228 (fix in flight in\n // PR #214). Once that lands, the header check can become a plain\n // journal_mode query.\n const probe = new Database(file);\n try {\n probe.exec('PRAGMA busy_timeout = 2000');\n // Round-trip through WAL so the probe also works when a clean shutdown\n // already left the file in rollback mode.\n probe.exec('PRAGMA journal_mode = WAL');\n probe.exec('PRAGMA journal_mode = DELETE');\n } catch {\n // busy — the header check below reports the failure\n } finally {\n probe.close();\n }\n if (journalModeFromHeader(file) !== 'delete') {\n throw new Error(\n `${file} is in use by another process — is another Mastra Code session running? ` +\n `Close other sessions and run /prune vacuum again.`,\n );\n }\n // Native `libsql` driver, not `@libsql/client`: the wrapper's close() can\n // leave the file lock held in-process (cached statements), which would\n // block other connections afterwards. This connection stays in rollback\n // mode, so even a pinned statement holds no lock once we're done.\n const db = new Database(file);\n try {\n db.exec('PRAGMA busy_timeout = 2000');\n const pageSize = pragmaNumber(db, 'page_size');\n const pageCount = pragmaNumber(db, 'page_count');\n const freelistCount = pragmaNumber(db, 'freelist_count');\n const liveBytes = Math.max(0, pageCount - freelistCount) * pageSize;\n const { bsize, bavail } = statfsSync(dirname(file));\n const freeBytes = bsize * bavail;\n if (freeBytes < requiredFreeBytes(liveBytes)) {\n throw new Error(\n `Not enough free disk space to compact ${file}: need ${formatBytes(requiredFreeBytes(liveBytes))} ` +\n `for a ${formatBytes(liveBytes)} compacted copy, but only ${formatBytes(freeBytes)} is free.`,\n );\n }\n onFileStart?.(file, bytesBefore, liveBytes);\n db.exec(`VACUUM INTO '${tmp.replace(/'/g, \"''\")}'`);\n } catch (err) {\n rmSync(tmp, { force: true });\n throw err;\n } finally {\n db.close();\n }\n // Re-probe right before the swap: `VACUUM INTO` can take tens of seconds\n // on multi-GB files, and a session started in that window reopens the db\n // in WAL mode (flipping the header back). Swapping under it would orphan\n // that session's inode and silently lose its writes.\n if (journalModeFromHeader(file) !== 'delete') {\n rmSync(tmp, { force: true });\n throw new Error(\n `${file} was opened by another process during compaction — is another Mastra Code session running? ` +\n `Close other sessions and run /prune vacuum again.`,\n );\n }\n // Swap the compacted copy into place. The old WAL/SHM sidecars belong to\n // the old inode — they must never be paired with the new file. If any step\n // after the first rename fails, restore the original so the db path is\n // never left empty (a naive restart would otherwise create a fresh db).\n renameSync(file, `${file}.old`);\n try {\n rmSync(`${file}-wal`, { force: true });\n rmSync(`${file}-shm`, { force: true });\n renameSync(tmp, file);\n } catch (err) {\n try {\n renameSync(`${file}.old`, file);\n } catch {\n throw new Error(\n `Failed to swap compacted copy into place AND failed to restore the original — ` +\n `your data is intact at ${file}.old; rename it back to ${file} manually. ` +\n `Original error: ${err instanceof Error ? err.message : String(err)}`,\n );\n }\n rmSync(tmp, { force: true });\n throw err;\n }\n rmSync(`${file}.old`, { force: true });\n results.push({ file, bytesBefore, bytesAfter: fileSizeWithWal(file) });\n }\n return results;\n}\n\n/** `file:/path` or `file:///path` → filesystem path; undefined for non-file urls. */\nfunction fileUrlToPath(url: string): string | undefined {\n if (!url.startsWith('file:')) return undefined;\n return url.replace(/^file:\\/\\//, '').replace(/^file:/, '');\n}\n\n/**\n * Resolve the local libsql db files eligible for checkpoint + VACUUM.\n * Returns [] for remote (turso) urls and PG backends. When PG was requested\n * but the factory fell back to libsql, the fallback uses the default files.\n */\nexport function resolveLocalDbFiles(config: StorageConfig, effectiveBackend: 'libsql' | 'pg'): string[] {\n if (effectiveBackend !== 'libsql') return [];\n if (config.backend === 'pg') {\n // PG requested but fell back to the default local libsql files\n return [getDatabasePath(), getVectorDatabasePath()];\n }\n if (config.isRemote) return [];\n const files: string[] = [];\n const main = fileUrlToPath(config.url);\n if (main) files.push(main);\n const vector = config.vectorUrl ? fileUrlToPath(config.vectorUrl) : getVectorDatabasePath();\n if (vector) files.push(vector);\n return files;\n}\n\n/**\n * Build the maintenance handle for the effective storage backend.\n * `localDbFiles` are the local libsql file paths eligible for VACUUM;\n * pass an empty array for remote (turso) or PG backends.\n */\nexport function createStorageMaintenance(opts: {\n storage: MastraCompositeStore;\n backend: 'libsql' | 'pg';\n retention: RetentionConfig;\n localDbFiles: string[];\n /** Closes the vector store's connection; required for local libsql so the compaction swap isn't blocked by our own handle. */\n closeVector?: () => Promise<void>;\n}): StorageMaintenance {\n const { storage, backend, retention, localDbFiles, closeVector } = opts;\n return {\n backend,\n retention,\n prune: options => storage.prune(options),\n closeStorage: async () => {\n await storage.close?.();\n await closeVector?.();\n },\n ...(backend === 'libsql' && localDbFiles.length > 0\n ? {\n reclaimDisk: (onFileStart?: (file: string, bytesBefore: number, liveBytes: number) => void) =>\n reclaimLibSQLDisk(localDbFiles, onFileStart),\n }\n : {}),\n };\n}\n\nexport function formatBytes(bytes: number): string {\n if (bytes < 1024) return `${bytes} B`;\n const units = ['KB', 'MB', 'GB', 'TB'];\n let value = bytes;\n let unit = 'B';\n for (const next of units) {\n if (value < 1024) break;\n value /= 1024;\n unit = next;\n }\n return `${value.toFixed(1)} ${unit}`;\n}\n\n/** Rows deleted per table per pass: default batchSize (1000) × 20 batches. */\nconst PASS_MAX_BATCHES = 20;\n\n/**\n * Run storage maintenance with exclusive access to the database, reporting\n * progress through `log` (one line per pass so large deletes are visible).\n *\n * Must run AFTER the TUI is stopped and background writers are quiesced —\n * batched deletes and VACUUM contend with live writes (SQLITE_BUSY) and can\n * starve the event loop that renders the TUI. The storage connection is\n * closed at the end (checkpointing the WAL for local libsql), so the process\n * must exit afterwards.\n */\nexport async function runStorageMaintenance(opts: {\n maintenance: StorageMaintenance;\n vacuum: boolean;\n /** Skip the memory domain (messages/threads) so chat history is preserved. */\n keepMemory?: boolean;\n log: (line: string) => void;\n}): Promise<void> {\n const { maintenance, vacuum, keepMemory = false, log } = opts;\n\n // keep-memory: prune with the standing policies minus the memory domain,\n // for this run only. The configured retention is untouched.\n const { memory: _memory, ...withoutMemory } = maintenance.retention;\n const retentionForRun = keepMemory ? { retention: withoutMemory } : {};\n\n log('Pruning rows older than the retention policies:');\n for (const [domain, tables] of Object.entries(maintenance.retention)) {\n for (const [table, policy] of Object.entries(tables ?? {})) {\n const kept = keepMemory && domain === 'memory';\n log(` ${domain}.${table}: ${kept ? 'kept (keep-memory)' : (policy as { maxAge: string | number }).maxAge}`);\n }\n }\n\n const totals = new Map<string, number>();\n for (;;) {\n const results = await maintenance.prune({ maxBatches: PASS_MAX_BATCHES, ...retentionForRun });\n if (results.length === 0) break;\n let remaining = false;\n let deletedThisPass = 0;\n for (const r of results) {\n const key = `${r.domain}.${r.table}`;\n totals.set(key, (totals.get(key) ?? 0) + r.deleted);\n deletedThisPass += r.deleted;\n if (!r.done) remaining = true;\n if (r.deleted > 0) {\n log(` ${key}: ${totals.get(key)} rows deleted${r.done ? '' : ' so far…'}`);\n }\n }\n if (!remaining) break;\n // Safety valve: a table reported more eligible rows but nothing was\n // deleted this pass — bail instead of spinning forever.\n if (deletedThisPass === 0) {\n log(' stopping: no progress in the last pass.');\n break;\n }\n }\n const totalDeleted = [...totals.values()].reduce((sum, n) => sum + n, 0);\n log(\n totalDeleted === 0\n ? 'Nothing to prune — no retention-eligible rows found.'\n : `Prune complete: ${totalDeleted} rows deleted.`,\n );\n\n // Release the long-lived connection: checkpoints the WAL for local libsql\n // and guarantees VACUUM below isn't blocked by our own open handles.\n await maintenance.closeStorage?.();\n\n if (!vacuum) {\n if (maintenance.reclaimDisk) {\n log('Deleted rows free pages inside the db file but not on disk. Run /prune vacuum to reclaim disk space.');\n }\n return;\n }\n\n if (!maintenance.reclaimDisk) {\n log('Disk reclamation (VACUUM) is only available for local libsql storage.');\n return;\n }\n\n log('Reclaiming disk (compacting database files)… this can take several minutes on large databases.');\n const reclaimed = await maintenance.reclaimDisk((file, bytesBefore, liveBytes) => {\n log(` vacuuming ${file} (${formatBytes(bytesBefore)}, ~${formatBytes(liveBytes)} live)…`);\n });\n if (reclaimed.length === 0) {\n log('No local database files found to vacuum.');\n return;\n }\n for (const r of reclaimed) {\n log(` ${r.file}: ${formatBytes(r.bytesBefore)} → ${formatBytes(r.bytesAfter)}`);\n }\n const saved = reclaimed.reduce((sum, r) => sum + Math.max(0, r.bytesBefore - r.bytesAfter), 0);\n log(`Reclaimed ${formatBytes(saved)}.`);\n}\n"],"mappings":"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"}