@mastra/code-sdk 1.8.4-alpha.6 → 1.9.0-alpha.10

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 (126) hide show
  1. package/README.md +190 -0
  2. package/dist/acp/agent.d.ts +30 -17
  3. package/dist/acp/agent.d.ts.map +1 -1
  4. package/dist/acp/agent.js +294 -103
  5. package/dist/acp/agent.js.map +1 -1
  6. package/dist/acp/errors.d.ts +3 -0
  7. package/dist/acp/errors.d.ts.map +1 -0
  8. package/dist/acp/errors.js +14 -0
  9. package/dist/acp/errors.js.map +1 -0
  10. package/dist/acp/event-mapper.d.ts +6 -1
  11. package/dist/acp/event-mapper.d.ts.map +1 -1
  12. package/dist/acp/event-mapper.js +116 -56
  13. package/dist/acp/event-mapper.js.map +1 -1
  14. package/dist/acp/index.d.ts +1 -4
  15. package/dist/acp/index.d.ts.map +1 -1
  16. package/dist/acp/index.js +6 -48
  17. package/dist/acp/index.js.map +1 -1
  18. package/dist/acp/runtime.d.ts +5 -0
  19. package/dist/acp/runtime.d.ts.map +1 -0
  20. package/dist/acp/runtime.js +80 -0
  21. package/dist/acp/runtime.js.map +1 -0
  22. package/dist/acp/server.d.ts +3 -6
  23. package/dist/acp/server.d.ts.map +1 -1
  24. package/dist/acp/server.js +22 -19
  25. package/dist/acp/server.js.map +1 -1
  26. package/dist/acp/skills.d.ts +6 -0
  27. package/dist/acp/skills.d.ts.map +1 -0
  28. package/dist/acp/skills.js +33 -0
  29. package/dist/acp/skills.js.map +1 -0
  30. package/dist/agents/model.js +1 -1
  31. package/dist/agents/prompts/agent-instructions.js +2 -2
  32. package/dist/agents/tools.d.ts +1 -1
  33. package/dist/agents/tools.d.ts.map +1 -1
  34. package/dist/agents/tools.js +2 -1
  35. package/dist/agents/tools.js.map +1 -1
  36. package/dist/agents/workspace.js +1 -1
  37. package/dist/analytics.js +2 -2
  38. package/dist/auth/providers/kimi-coding.js +1 -1
  39. package/dist/auth/storage.js +2 -2
  40. package/dist/clipboard/index.js +2 -2
  41. package/dist/headless/run-mc.d.ts.map +1 -1
  42. package/dist/headless/run-mc.js +14 -0
  43. package/dist/headless/run-mc.js.map +1 -1
  44. package/dist/hooks/config.js +1 -1
  45. package/dist/index.d.ts +14 -0
  46. package/dist/index.d.ts.map +1 -1
  47. package/dist/index.js +75 -8
  48. package/dist/index.js.map +1 -1
  49. package/dist/lsp/server.js +1 -1
  50. package/dist/lsp/workspace.js +1 -1
  51. package/dist/mcp/config.js +1 -1
  52. package/dist/mcp/manager.d.ts.map +1 -1
  53. package/dist/mcp/manager.js +2 -1
  54. package/dist/mcp/manager.js.map +1 -1
  55. package/dist/mcp/state.js +1 -1
  56. package/dist/mcp/types.d.ts +2 -0
  57. package/dist/mcp/types.d.ts.map +1 -1
  58. package/dist/onboarding/settings.d.ts +2 -0
  59. package/dist/onboarding/settings.d.ts.map +1 -1
  60. package/dist/onboarding/settings.js +6 -4
  61. package/dist/onboarding/settings.js.map +1 -1
  62. package/dist/permissions.d.ts.map +1 -1
  63. package/dist/permissions.js +6 -1
  64. package/dist/permissions.js.map +1 -1
  65. package/dist/plugins/dependencies.js +1 -1
  66. package/dist/plugins/install.js +1 -1
  67. package/dist/plugins/loader.js +3 -3
  68. package/dist/plugins/manager.js +1 -1
  69. package/dist/plugins/manifest.js +1 -1
  70. package/dist/plugins/package-link.js +1 -1
  71. package/dist/plugins/paths.js +1 -1
  72. package/dist/plugins/registry.js +1 -1
  73. package/dist/plugins/scaffold.js +1 -1
  74. package/dist/process-memory-diagnostics.js +3 -3
  75. package/dist/providers/amazon-bedrock-gateway.js +1 -1
  76. package/dist/providers/openai-codex.js +1 -1
  77. package/dist/schedules/index.d.ts +10 -0
  78. package/dist/schedules/index.d.ts.map +1 -0
  79. package/dist/schedules/index.js +6 -0
  80. package/dist/schedules/interval.d.ts +49 -0
  81. package/dist/schedules/interval.d.ts.map +1 -0
  82. package/dist/schedules/interval.js +215 -0
  83. package/dist/schedules/interval.js.map +1 -0
  84. package/dist/schedules/prompt.d.ts +36 -0
  85. package/dist/schedules/prompt.d.ts.map +1 -0
  86. package/dist/schedules/prompt.js +39 -0
  87. package/dist/schedules/prompt.js.map +1 -0
  88. package/dist/schedules/run-script.d.ts +8 -0
  89. package/dist/schedules/run-script.d.ts.map +1 -0
  90. package/dist/schedules/run-script.js +83 -0
  91. package/dist/schedules/run-script.js.map +1 -0
  92. package/dist/schedules/scheduler.d.ts +101 -0
  93. package/dist/schedules/scheduler.d.ts.map +1 -0
  94. package/dist/schedules/scheduler.js +169 -0
  95. package/dist/schedules/scheduler.js.map +1 -0
  96. package/dist/schedules/tools.d.ts +57 -0
  97. package/dist/schedules/tools.d.ts.map +1 -0
  98. package/dist/schedules/tools.js +220 -0
  99. package/dist/schedules/tools.js.map +1 -0
  100. package/dist/schema.js +1 -1
  101. package/dist/tools/request-sandbox-access.js +5 -4
  102. package/dist/tools/request-sandbox-access.js.map +1 -1
  103. package/dist/utils/debug-log.js +1 -1
  104. package/dist/utils/discarding-scores-storage.d.ts +33 -0
  105. package/dist/utils/discarding-scores-storage.d.ts.map +1 -0
  106. package/dist/utils/discarding-scores-storage.js +54 -0
  107. package/dist/utils/discarding-scores-storage.js.map +1 -0
  108. package/dist/utils/maintenance-lock.d.ts +42 -0
  109. package/dist/utils/maintenance-lock.d.ts.map +1 -0
  110. package/dist/utils/maintenance-lock.js +199 -0
  111. package/dist/utils/maintenance-lock.js.map +1 -0
  112. package/dist/utils/project.js +3 -3
  113. package/dist/utils/prune-cli.d.ts +24 -0
  114. package/dist/utils/prune-cli.d.ts.map +1 -0
  115. package/dist/utils/prune-cli.js +125 -0
  116. package/dist/utils/prune-cli.js.map +1 -0
  117. package/dist/utils/signals-pubsub.js +1 -1
  118. package/dist/utils/slash-command-loader.js +1 -1
  119. package/dist/utils/slash-command-processor.js +1 -1
  120. package/dist/utils/storage-maintenance.d.ts.map +1 -1
  121. package/dist/utils/storage-maintenance.js +34 -4
  122. package/dist/utils/storage-maintenance.js.map +1 -1
  123. package/dist/utils/thread-lock.js +1 -1
  124. package/dist/utils/update-check.js +2 -2
  125. package/dist/workflows/register-primitives.js +1 -1
  126. package/package.json +22 -12
@@ -0,0 +1,54 @@
1
+ import { ScoresStorage } from "@mastra/core/storage";
2
+ //#region src/utils/discarding-scores-storage.ts
3
+ /**
4
+ * A scores domain that accepts writes and keeps nothing.
5
+ *
6
+ * mastracode's `outcome` and `efficiency` scorers persist every result through
7
+ * the scores domain (`validateAndSaveScore` → `getStore('scores')`), but nothing
8
+ * in mastracode reads scores back. Writing them to libsql grew mastra.db without
9
+ * bound, and an in-memory store would grow the heap for the life of the process
10
+ * instead (each payload carries the system prompt, messages, input and output).
11
+ * Disabling the domain is not an option: core throws
12
+ * MASTRA_SCORES_STORAGE_NOT_AVAILABLE on every scorer run when it is missing.
13
+ */
14
+ var DiscardingScoresStorage = class extends ScoresStorage {
15
+ async getScoreById() {
16
+ return null;
17
+ }
18
+ async saveScore(score) {
19
+ const now = /* @__PURE__ */ new Date();
20
+ return { score: {
21
+ ...score,
22
+ id: score.id ?? crypto.randomUUID(),
23
+ createdAt: now,
24
+ updatedAt: now
25
+ } };
26
+ }
27
+ async listScoresByScorerId({ pagination }) {
28
+ return empty(pagination);
29
+ }
30
+ async listScoresByRunId({ pagination }) {
31
+ return empty(pagination);
32
+ }
33
+ async listScoresByEntityId({ pagination }) {
34
+ return empty(pagination);
35
+ }
36
+ async listScoresBySpan({ pagination }) {
37
+ return empty(pagination);
38
+ }
39
+ };
40
+ function empty(pagination) {
41
+ return {
42
+ pagination: {
43
+ total: 0,
44
+ page: pagination.page,
45
+ perPage: pagination.perPage,
46
+ hasMore: false
47
+ },
48
+ scores: []
49
+ };
50
+ }
51
+ //#endregion
52
+ export { DiscardingScoresStorage };
53
+
54
+ //# sourceMappingURL=discarding-scores-storage.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"discarding-scores-storage.js","names":[],"sources":["../../src/utils/discarding-scores-storage.ts"],"sourcesContent":["import type { ListScoresResponse, SaveScorePayload, ScoreRowData } from '@mastra/core/evals';\nimport { ScoresStorage } from '@mastra/core/storage';\nimport type { StoragePagination } from '@mastra/core/storage';\n\n/**\n * A scores domain that accepts writes and keeps nothing.\n *\n * mastracode's `outcome` and `efficiency` scorers persist every result through\n * the scores domain (`validateAndSaveScore` → `getStore('scores')`), but nothing\n * in mastracode reads scores back. Writing them to libsql grew mastra.db without\n * bound, and an in-memory store would grow the heap for the life of the process\n * instead (each payload carries the system prompt, messages, input and output).\n * Disabling the domain is not an option: core throws\n * MASTRA_SCORES_STORAGE_NOT_AVAILABLE on every scorer run when it is missing.\n */\nexport class DiscardingScoresStorage extends ScoresStorage {\n async getScoreById(): Promise<ScoreRowData | null> {\n return null;\n }\n\n async saveScore(score: SaveScorePayload): Promise<{ score: ScoreRowData }> {\n const now = new Date();\n return { score: { ...score, id: score.id ?? crypto.randomUUID(), createdAt: now, updatedAt: now } as ScoreRowData };\n }\n\n async listScoresByScorerId({ pagination }: { pagination: StoragePagination }): Promise<ListScoresResponse> {\n return empty(pagination);\n }\n\n async listScoresByRunId({ pagination }: { pagination: StoragePagination }): Promise<ListScoresResponse> {\n return empty(pagination);\n }\n\n async listScoresByEntityId({ pagination }: { pagination: StoragePagination }): Promise<ListScoresResponse> {\n return empty(pagination);\n }\n\n async listScoresBySpan({ pagination }: { pagination: StoragePagination }): Promise<ListScoresResponse> {\n return empty(pagination);\n }\n}\n\nfunction empty(pagination: StoragePagination): ListScoresResponse {\n return { pagination: { total: 0, page: pagination.page, perPage: pagination.perPage, hasMore: false }, scores: [] };\n}\n"],"mappings":";;;;;;;;;;;;;AAeA,IAAa,0BAAb,cAA6C,cAAc;CACzD,MAAM,eAA6C;EACjD,OAAO;CACT;CAEA,MAAM,UAAU,OAA2D;EACzE,MAAM,sBAAM,IAAI,KAAK;EACrB,OAAO,EAAE,OAAO;GAAE,GAAG;GAAO,IAAI,MAAM,MAAM,OAAO,WAAW;GAAG,WAAW;GAAK,WAAW;EAAI,EAAkB;CACpH;CAEA,MAAM,qBAAqB,EAAE,cAA8E;EACzG,OAAO,MAAM,UAAU;CACzB;CAEA,MAAM,kBAAkB,EAAE,cAA8E;EACtG,OAAO,MAAM,UAAU;CACzB;CAEA,MAAM,qBAAqB,EAAE,cAA8E;EACzG,OAAO,MAAM,UAAU;CACzB;CAEA,MAAM,iBAAiB,EAAE,cAA8E;EACrG,OAAO,MAAM,UAAU;CACzB;AACF;AAEA,SAAS,MAAM,YAAmD;CAChE,OAAO;EAAE,YAAY;GAAE,OAAO;GAAG,MAAM,WAAW;GAAM,SAAS,WAAW;GAAS,SAAS;EAAM;EAAG,QAAQ,CAAC;CAAE;AACpH"}
@@ -0,0 +1,42 @@
1
+ export declare class MaintenanceLockError extends Error {
2
+ readonly ownerPids: number[];
3
+ constructor(message: string, ownerPids: number[]);
4
+ }
5
+ export declare function getMaintenanceLockPath(): string;
6
+ /** Owner of a lock whose pid file is still being written (PID not known yet). */
7
+ export declare const UNKNOWN_OWNER = -1;
8
+ /**
9
+ * Creating a pid file (open, then write) is not atomic, so an empty or
10
+ * unparseable file may belong to a process that is mid-write. Only treat it as
11
+ * stale once it is older than this.
12
+ */
13
+ export declare const MALFORMED_PID_FILE_GRACE_MS = 5000;
14
+ /**
15
+ * PID holding the maintenance lock, `UNKNOWN_OWNER` while its pid file is still
16
+ * being written, or null. Stale locks are removed.
17
+ */
18
+ export declare function getMaintenanceLockOwner(): number | null;
19
+ /** PIDs of other live registered sessions. Stale registrations are removed. */
20
+ export declare function getLiveSessionPids(): number[];
21
+ /** Test-only: drop every registration held by this process and forget lock ownership. */
22
+ export declare function resetSessionRegistrationsForTesting(): void;
23
+ /** Register one running session in this process. Pair with exactly one unregisterSession(). */
24
+ export declare function registerSession(): void;
25
+ /** Release one registration made by registerSession(). */
26
+ export declare function unregisterSession(): void;
27
+ /**
28
+ * Register this session, then wait for any running maintenance to finish.
29
+ * Waits indefinitely by default: a vacuum on a multi-GB database can take
30
+ * minutes. With `timeoutMs`, throws after that long naming the holder.
31
+ */
32
+ export declare function registerSessionAndWaitForMaintenance({ timeoutMs, pollMs, onWait, }?: {
33
+ timeoutMs?: number;
34
+ pollMs?: number;
35
+ onWait?: (ownerPid: number) => void;
36
+ }): Promise<void>;
37
+ /**
38
+ * Acquire the maintenance lock, then refuse if any mastracode session is live.
39
+ * Returns a release function.
40
+ */
41
+ export declare function acquireMaintenanceLock(): () => void;
42
+ //# sourceMappingURL=maintenance-lock.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"maintenance-lock.d.ts","sourceRoot":"","sources":["../../src/utils/maintenance-lock.ts"],"names":[],"mappings":"AAiBA,qBAAa,oBAAqB,SAAQ,KAAK;aAG3B,SAAS,EAAE,MAAM,EAAE;gBADnC,OAAO,EAAE,MAAM,EACC,SAAS,EAAE,MAAM,EAAE;CAKtC;AAcD,wBAAgB,sBAAsB,IAAI,MAAM,CAE/C;AAED,iFAAiF;AACjF,eAAO,MAAM,aAAa,KAAK,CAAC;AAEhC;;;;GAIG;AACH,eAAO,MAAM,2BAA2B,OAAQ,CAAC;AAuCjD;;;GAGG;AACH,wBAAgB,uBAAuB,IAAI,MAAM,GAAG,IAAI,CAgBvD;AAED,+EAA+E;AAC/E,wBAAgB,kBAAkB,IAAI,MAAM,EAAE,CAmB7C;AAOD,yFAAyF;AACzF,wBAAgB,mCAAmC,IAAI,IAAI,CAG1D;AAYD,+FAA+F;AAC/F,wBAAgB,eAAe,IAAI,IAAI,CAStC;AAED,0DAA0D;AAC1D,wBAAgB,iBAAiB,IAAI,IAAI,CAIxC;AAED;;;;GAIG;AACH,wBAAsB,oCAAoC,CAAC,EACzD,SAAoB,EACpB,MAAY,EACZ,MAAM,GACP,GAAE;IAAE,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAA;CAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAqBnG;AAED;;;GAGG;AACH,wBAAgB,sBAAsB,IAAI,MAAM,IAAI,CA0CnD"}
@@ -0,0 +1,199 @@
1
+ import { getAppDataDir } from "./project.js";
2
+ import path from "path";
3
+ import fs from "fs";
4
+ //#region src/utils/maintenance-lock.ts
5
+ /**
6
+ * Mutual exclusion between storage maintenance (`mastracode prune`) and running
7
+ * mastracode sessions, using the same pid-file / stale-PID pattern as
8
+ * thread-lock.ts.
9
+ *
10
+ * - Every process that opens storage registers a session pid file.
11
+ * - `prune` holds `maintenance.lock` for its whole run.
12
+ *
13
+ * The checks are ordered so the race is closed both ways: `prune` acquires the
14
+ * lock and THEN looks for live sessions; a starter registers and THEN looks for
15
+ * the lock. Whichever acts second always sees the other.
16
+ */
17
+ var MaintenanceLockError = class extends Error {
18
+ ownerPids;
19
+ constructor(message, ownerPids) {
20
+ super(message);
21
+ this.ownerPids = ownerPids;
22
+ this.name = "MaintenanceLockError";
23
+ }
24
+ };
25
+ function getLocksDir() {
26
+ const dir = path.join(getAppDataDir(), "locks");
27
+ fs.mkdirSync(dir, { recursive: true });
28
+ return dir;
29
+ }
30
+ function getSessionsDir() {
31
+ const dir = path.join(getLocksDir(), "sessions");
32
+ fs.mkdirSync(dir, { recursive: true });
33
+ return dir;
34
+ }
35
+ function getMaintenanceLockPath() {
36
+ return path.join(getLocksDir(), "maintenance.lock");
37
+ }
38
+ /** Owner of a lock whose pid file is still being written (PID not known yet). */
39
+ const UNKNOWN_OWNER = -1;
40
+ /**
41
+ * Creating a pid file (open, then write) is not atomic, so an empty or
42
+ * unparseable file may belong to a process that is mid-write. Only treat it as
43
+ * stale once it is older than this.
44
+ */
45
+ const MALFORMED_PID_FILE_GRACE_MS = 5e3;
46
+ function isProcessAlive(pid) {
47
+ try {
48
+ process.kill(pid, 0);
49
+ return true;
50
+ } catch (error) {
51
+ return error.code !== "ESRCH";
52
+ }
53
+ }
54
+ /** Whether a pid file with no readable PID is old enough to reap. Missing files count as reapable. */
55
+ function isMalformedPidFileStale(file) {
56
+ try {
57
+ return Date.now() - fs.statSync(file).mtimeMs > MALFORMED_PID_FILE_GRACE_MS;
58
+ } catch {
59
+ return true;
60
+ }
61
+ }
62
+ function describeOwners(pids) {
63
+ const known = pids.filter((pid) => pid !== -1);
64
+ return known.length > 0 ? ` (PID ${known.join(", ")})` : "";
65
+ }
66
+ function readPid(file) {
67
+ try {
68
+ const pid = parseInt(fs.readFileSync(file, "utf-8").trim(), 10);
69
+ return isNaN(pid) ? null : pid;
70
+ } catch {
71
+ return null;
72
+ }
73
+ }
74
+ let lockHeld = false;
75
+ /**
76
+ * PID holding the maintenance lock, `UNKNOWN_OWNER` while its pid file is still
77
+ * being written, or null. Stale locks are removed.
78
+ */
79
+ function getMaintenanceLockOwner() {
80
+ const lockPath = getMaintenanceLockPath();
81
+ if (!fs.existsSync(lockPath)) return null;
82
+ const pid = readPid(lockPath);
83
+ if (pid === process.pid) {
84
+ if (lockHeld) return pid;
85
+ } else if (pid !== null && isProcessAlive(pid)) return pid;
86
+ if (pid === null && !isMalformedPidFileStale(lockPath)) return -1;
87
+ try {
88
+ fs.unlinkSync(lockPath);
89
+ } catch {}
90
+ return null;
91
+ }
92
+ /** PIDs of other live registered sessions. Stale registrations are removed. */
93
+ function getLiveSessionPids() {
94
+ const dir = getSessionsDir();
95
+ const live = [];
96
+ for (const file of fs.readdirSync(dir)) {
97
+ if (!file.endsWith(".pid")) continue;
98
+ const filePath = path.join(dir, file);
99
+ const pid = readPid(filePath);
100
+ if (pid === process.pid) continue;
101
+ if (pid !== null && isProcessAlive(pid)) live.push(pid);
102
+ else if (pid === null && !isMalformedPidFileStale(filePath)) live.push(-1);
103
+ else try {
104
+ fs.unlinkSync(filePath);
105
+ } catch {}
106
+ }
107
+ return live;
108
+ }
109
+ let sessionRefs = 0;
110
+ let exitHookInstalled = false;
111
+ /** Test-only: drop every registration held by this process and forget lock ownership. */
112
+ function resetSessionRegistrationsForTesting() {
113
+ while (sessionRefs > 0) unregisterSession();
114
+ lockHeld = false;
115
+ }
116
+ function getSessionPidPath() {
117
+ return path.join(getSessionsDir(), `${process.pid}.pid`);
118
+ }
119
+ function removeSessionPidFile() {
120
+ try {
121
+ fs.unlinkSync(getSessionPidPath());
122
+ } catch {}
123
+ }
124
+ /** Register one running session in this process. Pair with exactly one unregisterSession(). */
125
+ function registerSession() {
126
+ if (sessionRefs === 0) fs.writeFileSync(getSessionPidPath(), String(process.pid), { mode: 420 });
127
+ sessionRefs++;
128
+ if (!exitHookInstalled) {
129
+ exitHookInstalled = true;
130
+ process.once("exit", removeSessionPidFile);
131
+ }
132
+ }
133
+ /** Release one registration made by registerSession(). */
134
+ function unregisterSession() {
135
+ if (sessionRefs === 0) return;
136
+ sessionRefs--;
137
+ if (sessionRefs === 0) removeSessionPidFile();
138
+ }
139
+ /**
140
+ * Register this session, then wait for any running maintenance to finish.
141
+ * Waits indefinitely by default: a vacuum on a multi-GB database can take
142
+ * minutes. With `timeoutMs`, throws after that long naming the holder.
143
+ */
144
+ async function registerSessionAndWaitForMaintenance({ timeoutMs = Infinity, pollMs = 250, onWait } = {}) {
145
+ registerSession();
146
+ const deadline = Date.now() + timeoutMs;
147
+ let notified = false;
148
+ for (;;) {
149
+ const owner = getMaintenanceLockOwner();
150
+ if (owner === null) return;
151
+ if (!notified) {
152
+ notified = true;
153
+ onWait?.(owner);
154
+ }
155
+ if (Date.now() >= deadline) {
156
+ unregisterSession();
157
+ throw new MaintenanceLockError(`Storage maintenance (mastracode prune${describeOwners([owner])}) is still running after ${Math.round(timeoutMs / 1e3)}s. Wait for it to finish, or stop it, then start mastracode again.`, [owner]);
158
+ }
159
+ await new Promise((resolve) => setTimeout(resolve, pollMs));
160
+ }
161
+ }
162
+ /**
163
+ * Acquire the maintenance lock, then refuse if any mastracode session is live.
164
+ * Returns a release function.
165
+ */
166
+ function acquireMaintenanceLock() {
167
+ const lockPath = getMaintenanceLockPath();
168
+ for (let attempt = 0;; attempt++) try {
169
+ fs.writeFileSync(lockPath, String(process.pid), {
170
+ flag: "wx",
171
+ mode: 420
172
+ });
173
+ lockHeld = true;
174
+ break;
175
+ } catch (error) {
176
+ if (error.code !== "EEXIST") throw error;
177
+ const owner = getMaintenanceLockOwner();
178
+ if (owner !== null || attempt > 0) throw new MaintenanceLockError(`Another mastracode prune is already running${owner === null ? "" : describeOwners([owner])}.`, owner === null ? [] : [owner]);
179
+ }
180
+ let released = false;
181
+ const release = () => {
182
+ if (released) return;
183
+ released = true;
184
+ lockHeld = false;
185
+ if (readPid(lockPath) === process.pid) try {
186
+ fs.unlinkSync(lockPath);
187
+ } catch {}
188
+ };
189
+ const sessions = getLiveSessionPids();
190
+ if (sessions.length > 0) {
191
+ release();
192
+ throw new MaintenanceLockError(`mastracode is running${describeOwners(sessions)}. Exit every mastracode session before running prune.`, sessions);
193
+ }
194
+ return release;
195
+ }
196
+ //#endregion
197
+ export { MALFORMED_PID_FILE_GRACE_MS, MaintenanceLockError, UNKNOWN_OWNER, acquireMaintenanceLock, getLiveSessionPids, getMaintenanceLockOwner, getMaintenanceLockPath, registerSession, registerSessionAndWaitForMaintenance, resetSessionRegistrationsForTesting, unregisterSession };
198
+
199
+ //# sourceMappingURL=maintenance-lock.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"maintenance-lock.js","names":[],"sources":["../../src/utils/maintenance-lock.ts"],"sourcesContent":["/**\n * Mutual exclusion between storage maintenance (`mastracode prune`) and running\n * mastracode sessions, using the same pid-file / stale-PID pattern as\n * thread-lock.ts.\n *\n * - Every process that opens storage registers a session pid file.\n * - `prune` holds `maintenance.lock` for its whole run.\n *\n * The checks are ordered so the race is closed both ways: `prune` acquires the\n * lock and THEN looks for live sessions; a starter registers and THEN looks for\n * the lock. Whichever acts second always sees the other.\n */\nimport fs from 'node:fs';\nimport path from 'node:path';\n\nimport { getAppDataDir } from './project.js';\n\nexport class MaintenanceLockError extends Error {\n constructor(\n message: string,\n public readonly ownerPids: number[],\n ) {\n super(message);\n this.name = 'MaintenanceLockError';\n }\n}\n\nfunction getLocksDir(): string {\n const dir = path.join(getAppDataDir(), 'locks');\n fs.mkdirSync(dir, { recursive: true });\n return dir;\n}\n\nfunction getSessionsDir(): string {\n const dir = path.join(getLocksDir(), 'sessions');\n fs.mkdirSync(dir, { recursive: true });\n return dir;\n}\n\nexport function getMaintenanceLockPath(): string {\n return path.join(getLocksDir(), 'maintenance.lock');\n}\n\n/** Owner of a lock whose pid file is still being written (PID not known yet). */\nexport const UNKNOWN_OWNER = -1;\n\n/**\n * Creating a pid file (open, then write) is not atomic, so an empty or\n * unparseable file may belong to a process that is mid-write. Only treat it as\n * stale once it is older than this.\n */\nexport const MALFORMED_PID_FILE_GRACE_MS = 5_000;\n\nfunction isProcessAlive(pid: number): boolean {\n try {\n process.kill(pid, 0);\n return true;\n } catch (error) {\n // Only ESRCH proves the process is gone; EPERM etc. mean it exists.\n return (error as NodeJS.ErrnoException).code !== 'ESRCH';\n }\n}\n\n/** Whether a pid file with no readable PID is old enough to reap. Missing files count as reapable. */\nfunction isMalformedPidFileStale(file: string): boolean {\n try {\n return Date.now() - fs.statSync(file).mtimeMs > MALFORMED_PID_FILE_GRACE_MS;\n } catch {\n return true;\n }\n}\n\nfunction describeOwners(pids: number[]): string {\n const known = pids.filter(pid => pid !== UNKNOWN_OWNER);\n return known.length > 0 ? ` (PID ${known.join(', ')})` : '';\n}\n\nfunction readPid(file: string): number | null {\n try {\n const pid = parseInt(fs.readFileSync(file, 'utf-8').trim(), 10);\n return isNaN(pid) ? null : pid;\n } catch {\n return null;\n }\n}\n\n// True while this process holds maintenance.lock, so an own-PID lock can be told\n// apart from a stale leftover whose PID we reused.\nlet lockHeld = false;\n\n/**\n * PID holding the maintenance lock, `UNKNOWN_OWNER` while its pid file is still\n * being written, or null. Stale locks are removed.\n */\nexport function getMaintenanceLockOwner(): number | null {\n const lockPath = getMaintenanceLockPath();\n if (!fs.existsSync(lockPath)) return null;\n const pid = readPid(lockPath);\n if (pid === process.pid) {\n // We hold it (e.g. a concurrent prune in this process): a live owner.\n if (lockHeld) return pid;\n // Otherwise it is a leftover from a dead process whose PID we reused.\n } else if (pid !== null && isProcessAlive(pid)) {\n return pid;\n }\n if (pid === null && !isMalformedPidFileStale(lockPath)) return UNKNOWN_OWNER;\n try {\n fs.unlinkSync(lockPath);\n } catch {}\n return null;\n}\n\n/** PIDs of other live registered sessions. Stale registrations are removed. */\nexport function getLiveSessionPids(): number[] {\n const dir = getSessionsDir();\n const live: number[] = [];\n for (const file of fs.readdirSync(dir)) {\n if (!file.endsWith('.pid')) continue;\n const filePath = path.join(dir, file);\n const pid = readPid(filePath);\n if (pid === process.pid) continue;\n if (pid !== null && isProcessAlive(pid)) {\n live.push(pid);\n } else if (pid === null && !isMalformedPidFileStale(filePath)) {\n live.push(UNKNOWN_OWNER);\n } else {\n try {\n fs.unlinkSync(filePath);\n } catch {}\n }\n }\n return live;\n}\n\n// Refcounted: one process may start several sessions (e.g. repeated\n// createMastraCode() calls). The pid file exists while the count is > 0.\nlet sessionRefs = 0;\nlet exitHookInstalled = false;\n\n/** Test-only: drop every registration held by this process and forget lock ownership. */\nexport function resetSessionRegistrationsForTesting(): void {\n while (sessionRefs > 0) unregisterSession();\n lockHeld = false;\n}\n\nfunction getSessionPidPath(): string {\n return path.join(getSessionsDir(), `${process.pid}.pid`);\n}\n\nfunction removeSessionPidFile(): void {\n try {\n fs.unlinkSync(getSessionPidPath());\n } catch {}\n}\n\n/** Register one running session in this process. Pair with exactly one unregisterSession(). */\nexport function registerSession(): void {\n if (sessionRefs === 0) {\n fs.writeFileSync(getSessionPidPath(), String(process.pid), { mode: 0o644 });\n }\n sessionRefs++;\n if (!exitHookInstalled) {\n exitHookInstalled = true;\n process.once('exit', removeSessionPidFile);\n }\n}\n\n/** Release one registration made by registerSession(). */\nexport function unregisterSession(): void {\n if (sessionRefs === 0) return;\n sessionRefs--;\n if (sessionRefs === 0) removeSessionPidFile();\n}\n\n/**\n * Register this session, then wait for any running maintenance to finish.\n * Waits indefinitely by default: a vacuum on a multi-GB database can take\n * minutes. With `timeoutMs`, throws after that long naming the holder.\n */\nexport async function registerSessionAndWaitForMaintenance({\n timeoutMs = Infinity,\n pollMs = 250,\n onWait,\n}: { timeoutMs?: number; pollMs?: number; onWait?: (ownerPid: number) => void } = {}): Promise<void> {\n registerSession();\n const deadline = Date.now() + timeoutMs;\n let notified = false;\n for (;;) {\n const owner = getMaintenanceLockOwner();\n if (owner === null) return;\n if (!notified) {\n notified = true;\n onWait?.(owner);\n }\n if (Date.now() >= deadline) {\n unregisterSession();\n throw new MaintenanceLockError(\n `Storage maintenance (mastracode prune${describeOwners([owner])}) is still running after ${Math.round(timeoutMs / 1000)}s. ` +\n `Wait for it to finish, or stop it, then start mastracode again.`,\n [owner],\n );\n }\n await new Promise(resolve => setTimeout(resolve, pollMs));\n }\n}\n\n/**\n * Acquire the maintenance lock, then refuse if any mastracode session is live.\n * Returns a release function.\n */\nexport function acquireMaintenanceLock(): () => void {\n const lockPath = getMaintenanceLockPath();\n for (let attempt = 0; ; attempt++) {\n try {\n // 'wx' makes creation atomic, so two prunes cannot both win.\n fs.writeFileSync(lockPath, String(process.pid), { flag: 'wx', mode: 0o644 });\n lockHeld = true;\n break;\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code !== 'EEXIST') throw error;\n // getMaintenanceLockOwner() removes a stale lock, so the retry can win.\n const owner = getMaintenanceLockOwner();\n if (owner !== null || attempt > 0) {\n throw new MaintenanceLockError(\n `Another mastracode prune is already running${owner === null ? '' : describeOwners([owner])}.`,\n owner === null ? [] : [owner],\n );\n }\n }\n }\n\n let released = false;\n const release = () => {\n if (released) return;\n released = true;\n lockHeld = false;\n if (readPid(lockPath) === process.pid) {\n try {\n fs.unlinkSync(lockPath);\n } catch {}\n }\n };\n\n const sessions = getLiveSessionPids();\n if (sessions.length > 0) {\n release();\n throw new MaintenanceLockError(\n `mastracode is running${describeOwners(sessions)}. Exit every mastracode session before running prune.`,\n sessions,\n );\n }\n return release;\n}\n"],"mappings":"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"}
@@ -1,9 +1,9 @@
1
1
  import { DEFAULT_CONFIG_DIR } from "../constants.js";
2
- import { createHash } from "crypto";
3
- import fs from "fs";
4
- import os from "os";
5
2
  import path from "path";
6
3
  import { execFile, execSync } from "child_process";
4
+ import fs from "fs";
5
+ import os from "os";
6
+ import { createHash } from "crypto";
7
7
  //#region src/utils/project.ts
8
8
  /**
9
9
  * Project detection utilities
@@ -0,0 +1,24 @@
1
+ /**
2
+ * `mastracode prune` — storage maintenance from a non-interactive shell.
3
+ *
4
+ * The in-TUI `/prune` command has to stop the UI, quiesce background writers
5
+ * (MCP, workers, intervals) and then exit the process, because retention
6
+ * deletes and VACUUM need the database to themselves. A standalone process
7
+ * already satisfies that contract — and, unlike `/prune`, it does not require a
8
+ * rendered prompt first, so maintenance stays reachable when the TUI cannot
9
+ * start. Without it a large mastra.db has no in-app remedy (issue #22056).
10
+ *
11
+ * Usage:
12
+ * mastracode prune delete rows older than the retention policies
13
+ * mastracode prune --vacuum prune, then checkpoint WAL + VACUUM to return disk to the OS
14
+ * (local libsql only; remote libsql and Postgres only prune rows)
15
+ * mastracode prune --keep-memory prune, but keep chat history (messages/threads)
16
+ */
17
+ import type { StorageConfig } from './project.js';
18
+ /**
19
+ * Run storage maintenance for the project in the current directory.
20
+ * Returns the process exit code (0 on success).
21
+ */
22
+ export declare function runPruneCommand(args: string[]): Promise<number>;
23
+ export declare function describeStorageTarget(config: StorageConfig): string;
24
+ //# sourceMappingURL=prune-cli.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prune-cli.d.ts","sourceRoot":"","sources":["../../src/utils/prune-cli.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAYH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAiBlD;;;GAGG;AACH,wBAAsB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAuCrE;AA2ED,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,aAAa,GAAG,MAAM,CASnE"}
@@ -0,0 +1,125 @@
1
+ import { DEFAULT_CONFIG_DIR } from "../constants.js";
2
+ import { detectProject, getStorageConfig } from "./project.js";
3
+ import { loadSettings } from "../onboarding/settings.js";
4
+ import { acquireMaintenanceLock } from "./maintenance-lock.js";
5
+ import { DEFAULT_RETENTION, createStorageMaintenance, resolveLocalDbFiles, runStorageMaintenance } from "./storage-maintenance.js";
6
+ import { createStorage, createVectorStore } from "./storage-factory.js";
7
+ import path from "path";
8
+ import { LibSQLVector } from "@mastra/libsql";
9
+ //#region src/utils/prune-cli.ts
10
+ /**
11
+ * `mastracode prune` — storage maintenance from a non-interactive shell.
12
+ *
13
+ * The in-TUI `/prune` command has to stop the UI, quiesce background writers
14
+ * (MCP, workers, intervals) and then exit the process, because retention
15
+ * deletes and VACUUM need the database to themselves. A standalone process
16
+ * already satisfies that contract — and, unlike `/prune`, it does not require a
17
+ * rendered prompt first, so maintenance stays reachable when the TUI cannot
18
+ * start. Without it a large mastra.db has no in-app remedy (issue #22056).
19
+ *
20
+ * Usage:
21
+ * mastracode prune delete rows older than the retention policies
22
+ * mastracode prune --vacuum prune, then checkpoint WAL + VACUUM to return disk to the OS
23
+ * (local libsql only; remote libsql and Postgres only prune rows)
24
+ * mastracode prune --keep-memory prune, but keep chat history (messages/threads)
25
+ */
26
+ const USAGE = `Usage: mastracode prune [--vacuum] [--keep-memory]
27
+
28
+ --vacuum After pruning, compact the local database files so freed
29
+ pages are returned to the OS. Needs free disk for the copy.
30
+ Local libsql only; remote libsql and Postgres only prune rows.
31
+ --keep-memory Prune everything except chat history (messages/threads).
32
+ `;
33
+ /**
34
+ * Run storage maintenance for the project in the current directory.
35
+ * Returns the process exit code (0 on success).
36
+ */
37
+ async function runPruneCommand(args) {
38
+ if (args.includes("--help") || args.includes("-h")) {
39
+ console.log(USAGE);
40
+ return 0;
41
+ }
42
+ const flags = new Set(args.map((a) => a.toLowerCase()));
43
+ const unknown = [...flags].filter((f) => f !== "--vacuum" && f !== "--keep-memory");
44
+ if (unknown.length > 0) {
45
+ console.error(`Unknown prune option: ${unknown.join(", ")}\n${USAGE}`);
46
+ return 1;
47
+ }
48
+ const vacuum = flags.has("--vacuum");
49
+ const keepMemory = flags.has("--keep-memory");
50
+ try {
51
+ process.loadEnvFile(path.join(process.cwd(), ".env"));
52
+ } catch {}
53
+ let releaseLock;
54
+ try {
55
+ releaseLock = acquireMaintenanceLock();
56
+ } catch (err) {
57
+ console.error(err instanceof Error ? err.message : String(err));
58
+ return 1;
59
+ }
60
+ try {
61
+ return await prune({
62
+ vacuum,
63
+ keepMemory
64
+ });
65
+ } finally {
66
+ releaseLock();
67
+ }
68
+ }
69
+ async function prune({ vacuum, keepMemory }) {
70
+ const settings = loadSettings();
71
+ const storageConfig = getStorageConfig(detectProject(process.cwd()).rootPath, settings.storage, DEFAULT_CONFIG_DIR);
72
+ console.log(`Storage target: ${describeStorageTarget(storageConfig)}`);
73
+ const { storage, backend, warning } = await createStorage(storageConfig);
74
+ if (warning) console.log(warning);
75
+ let vector;
76
+ try {
77
+ await storage.init();
78
+ const localDbFiles = resolveLocalDbFiles(storageConfig, backend);
79
+ if (localDbFiles.length > 0) console.log(`Database: ${localDbFiles.join(", ")}`);
80
+ if (vacuum && localDbFiles.length > 0) vector = await createVectorStore(storageConfig, backend);
81
+ const libsqlVector = vector instanceof LibSQLVector ? vector : void 0;
82
+ await runStorageMaintenance({
83
+ maintenance: createStorageMaintenance({
84
+ storage,
85
+ backend,
86
+ retention: DEFAULT_RETENTION,
87
+ localDbFiles,
88
+ ...libsqlVector ? { closeVector: () => libsqlVector.close() } : {}
89
+ }),
90
+ vacuum,
91
+ keepMemory,
92
+ log: (line) => console.log(line)
93
+ });
94
+ console.log("Storage maintenance complete.");
95
+ return 0;
96
+ } catch (err) {
97
+ console.error(`Storage maintenance failed: ${err instanceof Error ? err.message : String(err)}`);
98
+ try {
99
+ await storage.close?.();
100
+ if (vector instanceof LibSQLVector) await vector.close();
101
+ } catch {}
102
+ return 1;
103
+ }
104
+ }
105
+ /** Credential-free URL: drops userinfo and the query string (e.g. ?authToken=). */
106
+ function redactUrl(raw) {
107
+ try {
108
+ const url = new URL(raw);
109
+ url.username = "";
110
+ url.password = "";
111
+ url.search = "";
112
+ return url.toString();
113
+ } catch {
114
+ return "<unparseable URL>";
115
+ }
116
+ }
117
+ function describeStorageTarget(config) {
118
+ if (config.backend === "libsql") return config.url.startsWith("file:") ? `libsql file ${config.url.slice(5)}` : `libsql ${redactUrl(config.url)}`;
119
+ if (config.connectionString) return `pg ${redactUrl(config.connectionString)}`;
120
+ return `pg postgres://${`${config.host ?? "localhost"}${config.port ? `:${config.port}` : ""}`}/${config.database ?? ""}`;
121
+ }
122
+ //#endregion
123
+ export { describeStorageTarget, runPruneCommand };
124
+
125
+ //# sourceMappingURL=prune-cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prune-cli.js","names":[],"sources":["../../src/utils/prune-cli.ts"],"sourcesContent":["/**\n * `mastracode prune` — storage maintenance from a non-interactive shell.\n *\n * The in-TUI `/prune` command has to stop the UI, quiesce background writers\n * (MCP, workers, intervals) and then exit the process, because retention\n * deletes and VACUUM need the database to themselves. A standalone process\n * already satisfies that contract — and, unlike `/prune`, it does not require a\n * rendered prompt first, so maintenance stays reachable when the TUI cannot\n * start. Without it a large mastra.db has no in-app remedy (issue #22056).\n *\n * Usage:\n * mastracode prune delete rows older than the retention policies\n * mastracode prune --vacuum prune, then checkpoint WAL + VACUUM to return disk to the OS\n * (local libsql only; remote libsql and Postgres only prune rows)\n * mastracode prune --keep-memory prune, but keep chat history (messages/threads)\n */\n\nimport path from 'node:path';\n\nimport type { MastraVector } from '@mastra/core/vector';\nimport { LibSQLVector } from '@mastra/libsql';\n\nimport { DEFAULT_CONFIG_DIR } from '../constants.js';\nimport { loadSettings } from '../onboarding/settings.js';\n\nimport { acquireMaintenanceLock } from './maintenance-lock.js';\nimport { detectProject, getStorageConfig } from './project.js';\nimport type { StorageConfig } from './project.js';\nimport { createStorage, createVectorStore } from './storage-factory.js';\nimport {\n DEFAULT_RETENTION,\n createStorageMaintenance,\n resolveLocalDbFiles,\n runStorageMaintenance,\n} from './storage-maintenance.js';\n\nconst USAGE = `Usage: mastracode prune [--vacuum] [--keep-memory]\n\n --vacuum After pruning, compact the local database files so freed\n pages are returned to the OS. Needs free disk for the copy.\n Local libsql only; remote libsql and Postgres only prune rows.\n --keep-memory Prune everything except chat history (messages/threads).\n`;\n\n/**\n * Run storage maintenance for the project in the current directory.\n * Returns the process exit code (0 on success).\n */\nexport async function runPruneCommand(args: string[]): Promise<number> {\n if (args.includes('--help') || args.includes('-h')) {\n console.log(USAGE);\n return 0;\n }\n\n const flags = new Set(args.map(a => a.toLowerCase()));\n const unknown = [...flags].filter(f => f !== '--vacuum' && f !== '--keep-memory');\n if (unknown.length > 0) {\n console.error(`Unknown prune option: ${unknown.join(', ')}\\n${USAGE}`);\n return 1;\n }\n const vacuum = flags.has('--vacuum');\n const keepMemory = flags.has('--keep-memory');\n\n // Startup loads the project .env before registering its session and resolving\n // storage (MASTRA_APP_DATA_DIR, MASTRA_DB_PATH, ...). Do the same before taking\n // the lock, or prune locks/scans a different app data dir or opens another DB.\n try {\n process.loadEnvFile(path.join(process.cwd(), '.env'));\n } catch {\n // No .env file — keys may be in the shell environment\n }\n\n // Maintenance needs the database to itself: hold the lock for the whole run\n // and refuse while any mastracode session is registered.\n let releaseLock: () => void;\n try {\n releaseLock = acquireMaintenanceLock();\n } catch (err) {\n console.error(err instanceof Error ? err.message : String(err));\n return 1;\n }\n\n try {\n return await prune({ vacuum, keepMemory });\n } finally {\n releaseLock();\n }\n}\n\nasync function prune({ vacuum, keepMemory }: { vacuum: boolean; keepMemory: boolean }): Promise<number> {\n // Resolve storage exactly the way startup does, so prune targets the same\n // database the TUI uses — honoring MASTRA_DB_PATH / MASTRA_STORAGE_BACKEND,\n // global settings, and a project's own .mastracode/database.json.\n const settings = loadSettings();\n const project = detectProject(process.cwd());\n const storageConfig = getStorageConfig(project.rootPath, settings.storage, DEFAULT_CONFIG_DIR);\n\n // A project .env can point storage elsewhere (startup honors it too), so say\n // exactly what is about to be pruned before anything is deleted.\n console.log(`Storage target: ${describeStorageTarget(storageConfig)}`);\n\n const { storage, backend, warning } = await createStorage(storageConfig);\n if (warning) console.log(warning);\n\n let vector: MastraVector | undefined;\n try {\n // The libsql factory does not init (the PG path already has); init is\n // coalesced, so tables are guaranteed to exist before we delete from them.\n await storage.init();\n\n const localDbFiles = resolveLocalDbFiles(storageConfig, backend);\n if (localDbFiles.length > 0) {\n console.log(`Database: ${localDbFiles.join(', ')}`);\n }\n\n // Only the local-libsql compaction needs the vector connection (its file\n // swap refuses while any connection is open). Opening it otherwise would\n // create mastra-vectors.db as a side effect.\n if (vacuum && localDbFiles.length > 0) {\n vector = await createVectorStore(storageConfig, backend);\n }\n const libsqlVector = vector instanceof LibSQLVector ? vector : undefined;\n\n const maintenance = createStorageMaintenance({\n storage,\n backend,\n retention: DEFAULT_RETENTION,\n localDbFiles,\n ...(libsqlVector ? { closeVector: () => libsqlVector.close() } : {}),\n });\n\n await runStorageMaintenance({ maintenance, vacuum, keepMemory, log: line => console.log(line) });\n console.log('Storage maintenance complete.');\n return 0;\n } catch (err) {\n console.error(`Storage maintenance failed: ${err instanceof Error ? err.message : String(err)}`);\n // runStorageMaintenance closes storage on its success path. If it threw\n // first, release the connections here so a retry isn't blocked by our own\n // open handles. Best effort — the original failure is what matters.\n try {\n await storage.close?.();\n if (vector instanceof LibSQLVector) await vector.close();\n } catch {\n // ignore cleanup errors\n }\n return 1;\n }\n}\n\n/** Credential-free URL: drops userinfo and the query string (e.g. ?authToken=). */\nfunction redactUrl(raw: string): string {\n try {\n const url = new URL(raw);\n url.username = '';\n url.password = '';\n url.search = '';\n return url.toString();\n } catch {\n return '<unparseable URL>';\n }\n}\n\nexport function describeStorageTarget(config: StorageConfig): string {\n if (config.backend === 'libsql') {\n return config.url.startsWith('file:')\n ? `libsql file ${config.url.slice('file:'.length)}`\n : `libsql ${redactUrl(config.url)}`;\n }\n if (config.connectionString) return `pg ${redactUrl(config.connectionString)}`;\n const host = `${config.host ?? 'localhost'}${config.port ? `:${config.port}` : ''}`;\n return `pg postgres://${host}/${config.database ?? ''}`;\n}\n"],"mappings":"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"}
@@ -1,5 +1,5 @@
1
- import { createHash } from "crypto";
2
1
  import { join } from "path";
2
+ import { createHash } from "crypto";
3
3
  import { mkdir } from "fs/promises";
4
4
  import { PubSub, UnixSocketPubSub } from "@mastra/core/events";
5
5
  //#region src/utils/signals-pubsub.ts
@@ -1,7 +1,7 @@
1
1
  import { DEFAULT_CONFIG_DIR } from "../constants.js";
2
2
  import { isPathWithinRoot } from "./path-security.js";
3
- import { promises } from "fs";
4
3
  import * as path$1 from "path";
4
+ import { promises } from "fs";
5
5
  import { parse } from "yaml";
6
6
  //#region src/utils/slash-command-loader.ts
7
7
  /**
@@ -1,6 +1,6 @@
1
- import { promises } from "fs";
2
1
  import * as path$1 from "path";
3
2
  import { execSync } from "child_process";
3
+ import { promises } from "fs";
4
4
  //#region src/utils/slash-command-processor.ts
5
5
  /**
6
6
  * Process a slash command by replacing variables and executing shell commands
@@ -1 +1 @@
1
- {"version":3,"file":"storage-maintenance.d.ts","sourceRoot":"","sources":["../../src/utils/storage-maintenance.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAKH,OAAO,KAAK,EAAE,oBAAoB,EAAE,YAAY,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAQ7G,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAGlD;;;;;;;GAOG;AACH,eAAO,MAAM,iBAAiB,EAAE,eAe/B,CAAC;AAEF,+EAA+E;AAC/E,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB;;;;;;;;;;;OAWG;IACH,OAAO,CAAC,EAAE,cAAc,GAAG,kBAAkB,CAAC;CAC/C;AAED,4FAA4F;AAC5F,MAAM,WAAW,kBAAkB;IACjC,OAAO,EAAE,QAAQ,GAAG,IAAI,CAAC;IACzB,mEAAmE;IACnE,SAAS,EAAE,eAAe,CAAC;IAC3B,qKAAqK;IACrK,KAAK,EAAE,CAAC,OAAO,CAAC,EAAE,YAAY,KAAK,OAAO,CAAC,WAAW,EAAE,CAAC,CAAC;IAC1D,wHAAwH;IACxH,YAAY,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IACnC,2HAA2H;IAC3H,WAAW,CAAC,EAAE,CACZ,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,KAAK,IAAI,KACzE,OAAO,CAAC,aAAa,EAAE,CAAC,CAAC;CAC/B;AAeD,4FAA4F;AAC5F,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAE3D;AAkDD;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAsB,iBAAiB,CACrC,OAAO,EAAE,MAAM,EAAE,EACjB,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,KAAK,IAAI,GAC3E,OAAO,CAAC,aAAa,EAAE,CAAC,CAuH1B;AAQD;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,aAAa,EAAE,gBAAgB,EAAE,QAAQ,GAAG,IAAI,GAAG,MAAM,EAAE,CAatG;AAED;;;;GAIG;AACH,wBAAgB,wBAAwB,CAAC,IAAI,EAAE;IAC7C,OAAO,EAAE,oBAAoB,CAAC;IAC9B,OAAO,EAAE,QAAQ,GAAG,IAAI,CAAC;IACzB,SAAS,EAAE,eAAe,CAAC;IAC3B,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,8HAA8H;IAC9H,WAAW,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CACnC,GAAG,kBAAkB,CAiBrB;AAED,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAWjD;AAKD;;;;;;;;;GASG;AACH,wBAAsB,qBAAqB,CAAC,IAAI,EAAE;IAChD,WAAW,EAAE,kBAAkB,CAAC;IAChC,MAAM,EAAE,OAAO,CAAC;IAChB,8EAA8E;IAC9E,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,GAAG,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;CAC7B,GAAG,OAAO,CAAC,IAAI,CAAC,CAgGhB"}
1
+ {"version":3,"file":"storage-maintenance.d.ts","sourceRoot":"","sources":["../../src/utils/storage-maintenance.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAOH,OAAO,KAAK,EAAE,oBAAoB,EAAE,YAAY,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAQ7G,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAGlD;;;;;;;GAOG;AACH,eAAO,MAAM,iBAAiB,EAAE,eAe/B,CAAC;AAEF,+EAA+E;AAC/E,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB;;;;;;;;;;;OAWG;IACH,OAAO,CAAC,EAAE,cAAc,GAAG,kBAAkB,CAAC;CAC/C;AAED,4FAA4F;AAC5F,MAAM,WAAW,kBAAkB;IACjC,OAAO,EAAE,QAAQ,GAAG,IAAI,CAAC;IACzB,mEAAmE;IACnE,SAAS,EAAE,eAAe,CAAC;IAC3B,qKAAqK;IACrK,KAAK,EAAE,CAAC,OAAO,CAAC,EAAE,YAAY,KAAK,OAAO,CAAC,WAAW,EAAE,CAAC,CAAC;IAC1D,wHAAwH;IACxH,YAAY,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IACnC,2HAA2H;IAC3H,WAAW,CAAC,EAAE,CACZ,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,KAAK,IAAI,KACzE,OAAO,CAAC,aAAa,EAAE,CAAC,CAAC;CAC/B;AAeD,4FAA4F;AAC5F,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAE3D;AAkFD;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAsB,iBAAiB,CACrC,OAAO,EAAE,MAAM,EAAE,EACjB,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,KAAK,IAAI,GAC3E,OAAO,CAAC,aAAa,EAAE,CAAC,CA4H1B;AAQD;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,aAAa,EAAE,gBAAgB,EAAE,QAAQ,GAAG,IAAI,GAAG,MAAM,EAAE,CAatG;AAED;;;;GAIG;AACH,wBAAgB,wBAAwB,CAAC,IAAI,EAAE;IAC7C,OAAO,EAAE,oBAAoB,CAAC;IAC9B,OAAO,EAAE,QAAQ,GAAG,IAAI,CAAC;IACzB,SAAS,EAAE,eAAe,CAAC;IAC3B,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,8HAA8H;IAC9H,WAAW,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CACnC,GAAG,kBAAkB,CAiBrB;AAED,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAWjD;AAKD;;;;;;;;;GASG;AACH,wBAAsB,qBAAqB,CAAC,IAAI,EAAE;IAChD,WAAW,EAAE,kBAAkB,CAAC;IAChC,MAAM,EAAE,OAAO,CAAC;IAChB,8EAA8E;IAC9E,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,GAAG,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;CAC7B,GAAG,OAAO,CAAC,IAAI,CAAC,CAkGhB"}
@@ -1,6 +1,8 @@
1
1
  import { getDatabasePath, getVectorDatabasePath } from "./project.js";
2
- import { closeSync, existsSync, openSync, readSync, renameSync, rmSync, statSync, statfsSync } from "fs";
3
2
  import { dirname } from "path";
3
+ import { closeSync, existsSync, openSync, readSync, renameSync, rmSync, statSync, statfsSync } from "fs";
4
+ import { setFlagsFromString } from "v8";
5
+ import { runInNewContext } from "vm";
4
6
  import Database from "libsql";
5
7
  //#region src/utils/storage-maintenance.ts
6
8
  /**
@@ -71,6 +73,33 @@ function journalModeFromHeader(file) {
71
73
  }
72
74
  }
73
75
  /**
76
+ * Collect our own closed-but-unfinalized libsql connections before probing.
77
+ *
78
+ * libsql-js does not finalize prepared statements on `close()` (they are only
79
+ * released by GC; tursodatabase/libsql-js#228), so a connection we already
80
+ * closed can still hold the WAL lock and make the reclaim probe fail.
81
+ *
82
+ * `gc` is only global under `--expose-gc`, so we set that V8 flag at runtime
83
+ * and read `gc` from a fresh VM context. The flag is process-wide, but it only
84
+ * makes `gc` available to contexts created afterward (the main context's
85
+ * globals are untouched) and changes no GC behavior. A forced
86
+ * collection only frees unreachable objects. This runs only on the explicit
87
+ * vacuum path (`mastracode prune --vacuum` or `/prune vacuum`), and both exit
88
+ * the process right after, so the flag never outlives maintenance in practice.
89
+ *
90
+ * Remove this once libsql-js#228 lands and `close()` finalizes statements.
91
+ */
92
+ async function releaseClosedConnections() {
93
+ await new Promise((resolve) => setImmediate(resolve));
94
+ const gc = globalThis.gc ?? (() => {
95
+ setFlagsFromString("--expose-gc");
96
+ return runInNewContext("gc");
97
+ })();
98
+ gc();
99
+ await new Promise((resolve) => setTimeout(resolve, 50));
100
+ gc();
101
+ }
102
+ /**
74
103
  * Does this database carry a libsql vector index?
75
104
  *
76
105
  * Takes an already-open connection on purpose: the query needs a prepared
@@ -121,6 +150,7 @@ async function reclaimLibSQLDisk(dbFiles, onFileStart) {
121
150
  const bytesBefore = fileSizeWithWal(file);
122
151
  const tmp = `${file}.vacuum-tmp`;
123
152
  rmSync(tmp, { force: true });
153
+ await releaseClosedConnections();
124
154
  const probe = new Database(file);
125
155
  try {
126
156
  probe.exec("PRAGMA busy_timeout = 2000");
@@ -129,7 +159,7 @@ async function reclaimLibSQLDisk(dbFiles, onFileStart) {
129
159
  } catch {} finally {
130
160
  probe.close();
131
161
  }
132
- 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.`);
162
+ if (journalModeFromHeader(file) !== "delete") throw new Error(`${file} is in use by another process — is another Mastra Code session running? Close other sessions, then run /prune vacuum (or mastracode prune --vacuum) again.`);
133
163
  const db = new Database(file);
134
164
  try {
135
165
  db.exec("PRAGMA busy_timeout = 2000");
@@ -165,7 +195,7 @@ async function reclaimLibSQLDisk(dbFiles, onFileStart) {
165
195
  }
166
196
  if (journalModeFromHeader(file) !== "delete") {
167
197
  rmSync(tmp, { force: true });
168
- 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.`);
198
+ throw new Error(`${file} was opened by another process during compaction — is another Mastra Code session running? Close other sessions, then run /prune vacuum (or mastracode prune --vacuum) again.`);
169
199
  }
170
200
  renameSync(file, `${file}.old`);
171
201
  try {
@@ -290,7 +320,7 @@ async function runStorageMaintenance(opts) {
290
320
  log(totalDeleted === 0 ? "Nothing to prune — no retention-eligible rows found." : `Prune complete: ${totalDeleted} rows deleted.`);
291
321
  await maintenance.closeStorage?.();
292
322
  if (!vacuum) {
293
- if (maintenance.reclaimDisk) log("Deleted rows free pages inside the db file but not on disk. Run /prune vacuum to reclaim disk space.");
323
+ if (maintenance.reclaimDisk) log("Deleted rows free pages inside the db file but not on disk. Run /prune vacuum (or mastracode prune --vacuum) to reclaim disk space.");
294
324
  return;
295
325
  }
296
326
  if (!maintenance.reclaimDisk) {