@mastra/code-sdk 1.9.0-alpha.9 → 1.10.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/README.md +23 -0
  2. package/dist/agent-connections/tools.d.ts.map +1 -1
  3. package/dist/agent-connections/tools.js +5 -5
  4. package/dist/agent-connections/tools.js.map +1 -1
  5. package/dist/agents/tools.d.ts +1 -1
  6. package/dist/agents/tools.d.ts.map +1 -1
  7. package/dist/agents/tools.js +2 -1
  8. package/dist/agents/tools.js.map +1 -1
  9. package/dist/analytics.d.ts.map +1 -1
  10. package/dist/analytics.js +2 -3
  11. package/dist/analytics.js.map +1 -1
  12. package/dist/auth/account-rotation-processor.d.ts.map +1 -1
  13. package/dist/auth/account-rotation-processor.js +17 -10
  14. package/dist/auth/account-rotation-processor.js.map +1 -1
  15. package/dist/auth/providers/kimi-coding.d.ts.map +1 -1
  16. package/dist/auth/providers/kimi-coding.js +1 -2
  17. package/dist/auth/providers/kimi-coding.js.map +1 -1
  18. package/dist/auth/providers/openai-codex.d.ts +1 -1
  19. package/dist/auth/providers/openai-codex.d.ts.map +1 -1
  20. package/dist/auth/providers/openai-codex.js +7 -19
  21. package/dist/auth/providers/openai-codex.js.map +1 -1
  22. package/dist/auth/storage.js +3 -3
  23. package/dist/auth/storage.js.map +1 -1
  24. package/dist/experimental-agent.d.ts +11 -0
  25. package/dist/experimental-agent.d.ts.map +1 -0
  26. package/dist/experimental-agent.js +36 -0
  27. package/dist/experimental-agent.js.map +1 -0
  28. package/dist/goal-manager.d.ts +39 -3
  29. package/dist/goal-manager.d.ts.map +1 -1
  30. package/dist/goal-manager.js +113 -31
  31. package/dist/goal-manager.js.map +1 -1
  32. package/dist/index.d.ts +12 -0
  33. package/dist/index.d.ts.map +1 -1
  34. package/dist/index.js +95 -12
  35. package/dist/index.js.map +1 -1
  36. package/dist/knowledge-inspector.d.ts.map +1 -1
  37. package/dist/knowledge-inspector.js +1 -2
  38. package/dist/knowledge-inspector.js.map +1 -1
  39. package/dist/onboarding/settings.d.ts +11 -0
  40. package/dist/onboarding/settings.d.ts.map +1 -1
  41. package/dist/onboarding/settings.js +39 -5
  42. package/dist/onboarding/settings.js.map +1 -1
  43. package/dist/permissions.d.ts.map +1 -1
  44. package/dist/permissions.js +6 -1
  45. package/dist/permissions.js.map +1 -1
  46. package/dist/plugins/loader.d.ts.map +1 -1
  47. package/dist/plugins/loader.js +1 -2
  48. package/dist/plugins/loader.js.map +1 -1
  49. package/dist/process-memory-diagnostics.d.ts.map +1 -1
  50. package/dist/process-memory-diagnostics.js +2 -3
  51. package/dist/process-memory-diagnostics.js.map +1 -1
  52. package/dist/schedules/index.d.ts +10 -0
  53. package/dist/schedules/index.d.ts.map +1 -0
  54. package/dist/schedules/index.js +6 -0
  55. package/dist/schedules/interval.d.ts +49 -0
  56. package/dist/schedules/interval.d.ts.map +1 -0
  57. package/dist/schedules/interval.js +215 -0
  58. package/dist/schedules/interval.js.map +1 -0
  59. package/dist/schedules/prompt.d.ts +36 -0
  60. package/dist/schedules/prompt.d.ts.map +1 -0
  61. package/dist/schedules/prompt.js +39 -0
  62. package/dist/schedules/prompt.js.map +1 -0
  63. package/dist/schedules/run-script.d.ts +8 -0
  64. package/dist/schedules/run-script.d.ts.map +1 -0
  65. package/dist/schedules/run-script.js +83 -0
  66. package/dist/schedules/run-script.js.map +1 -0
  67. package/dist/schedules/scheduler.d.ts +101 -0
  68. package/dist/schedules/scheduler.d.ts.map +1 -0
  69. package/dist/schedules/scheduler.js +169 -0
  70. package/dist/schedules/scheduler.js.map +1 -0
  71. package/dist/schedules/tools.d.ts +57 -0
  72. package/dist/schedules/tools.d.ts.map +1 -0
  73. package/dist/schedules/tools.js +220 -0
  74. package/dist/schedules/tools.js.map +1 -0
  75. package/dist/tools/workflows/ephemeral-memory.d.ts +21 -0
  76. package/dist/tools/workflows/ephemeral-memory.d.ts.map +1 -1
  77. package/dist/tools/workflows/ephemeral-memory.js +1 -2
  78. package/dist/tools/workflows/ephemeral-memory.js.map +1 -1
  79. package/dist/utils/discarding-scores-storage.d.ts +33 -0
  80. package/dist/utils/discarding-scores-storage.d.ts.map +1 -0
  81. package/dist/utils/discarding-scores-storage.js +54 -0
  82. package/dist/utils/discarding-scores-storage.js.map +1 -0
  83. package/dist/utils/maintenance-lock.d.ts +42 -0
  84. package/dist/utils/maintenance-lock.d.ts.map +1 -0
  85. package/dist/utils/maintenance-lock.js +199 -0
  86. package/dist/utils/maintenance-lock.js.map +1 -0
  87. package/dist/utils/prune-cli.d.ts +24 -0
  88. package/dist/utils/prune-cli.d.ts.map +1 -0
  89. package/dist/utils/prune-cli.js +125 -0
  90. package/dist/utils/prune-cli.js.map +1 -0
  91. package/dist/utils/signals-pubsub.d.ts.map +1 -1
  92. package/dist/utils/signals-pubsub.js +1 -2
  93. package/dist/utils/signals-pubsub.js.map +1 -1
  94. package/dist/utils/storage-maintenance.d.ts.map +1 -1
  95. package/dist/utils/storage-maintenance.js +33 -3
  96. package/dist/utils/storage-maintenance.js.map +1 -1
  97. package/package.json +21 -15
@@ -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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"}
@@ -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 +1 @@
1
- {"version":3,"file":"signals-pubsub.d.ts","sourceRoot":"","sources":["../../src/utils/signals-pubsub.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,MAAM,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAC/D,OAAO,KAAK,EAAE,KAAK,EAAE,aAAa,EAAE,aAAa,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAIrH;;;;;;;;;;;;;;GAcG;AACH,cAAM,aAAc,SAAQ,MAAM;;gBAOpB,UAAU,EAAE,MAAM;IAM9B,IAAa,cAAc,IAAI,aAAa,CAAC,kBAAkB,CAAC,CAE/D;IAED,wFAAwF;IACxF,gBAAgB,IAAI,aAAa;IAI3B,OAAO,CACX,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,IAAI,GAAG,WAAW,CAAC,EACtC,OAAO,CAAC,EAAE;QAAE,SAAS,CAAC,EAAE,OAAO,CAAA;KAAE,GAChC,OAAO,CAAC,IAAI,CAAC;IAKV,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,aAAa,EAAE,OAAO,CAAC,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC;IAKtF,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC;IAM5D,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAItB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAM5B,sEAAsE;IACtE,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,gBAAgB,GAAG,SAAS;CAmEvD;AAED;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAAC,UAAU,EAAE,MAAM,GAAG,aAAa,CAErE"}
1
+ {"version":3,"file":"signals-pubsub.d.ts","sourceRoot":"","sources":["../../src/utils/signals-pubsub.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,MAAM,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAC/D,OAAO,KAAK,EAAE,KAAK,EAAE,aAAa,EAAE,aAAa,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAIrH;;;;;;;;;;;;;;GAcG;AACH,cAAM,aAAc,SAAQ,MAAM;;gBAOpB,UAAU,EAAE,MAAM;IAM9B,IAAa,cAAc,IAAI,aAAa,CAAC,kBAAkB,CAAC,CAE/D;IAED,wFAAwF;IACxF,gBAAgB,IAAI,aAAa;IAI3B,OAAO,CACX,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,IAAI,GAAG,WAAW,CAAC,EACtC,OAAO,CAAC,EAAE;QAAE,SAAS,CAAC,EAAE,OAAO,CAAA;KAAE,GAChC,OAAO,CAAC,IAAI,CAAC;IAKV,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,aAAa,EAAE,OAAO,CAAC,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC;IAKtF,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC;IAM5D,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAItB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAM5B,sEAAsE;IACtE,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,gBAAgB,GAAG,SAAS;CAqEvD;AAED;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAAC,UAAU,EAAE,MAAM,GAAG,aAAa,CAErE"}
@@ -1,5 +1,4 @@
1
1
  import { join } from "path";
2
- import { createHash } from "crypto";
3
2
  import { mkdir } from "fs/promises";
4
3
  import { PubSub, UnixSocketPubSub } from "@mastra/core/events";
5
4
  //#region src/utils/signals-pubsub.ts
@@ -91,7 +90,7 @@ var SignalsPubSub = class extends PubSub {
91
90
  await mkdir(dir, { recursive: true });
92
91
  const candidate = join(dir, `${key}.sock`);
93
92
  if (Buffer.byteLength(candidate) > 104) {
94
- key = createHash("sha256").update(key).digest("hex").slice(0, 16);
93
+ key = Buffer.from(await globalThis.crypto.subtle.digest("SHA-256", new TextEncoder().encode(key))).toString("hex").slice(0, 16);
95
94
  return join(dir, `${key}.sock`);
96
95
  }
97
96
  return candidate;
@@ -1 +1 @@
1
- {"version":3,"file":"signals-pubsub.js","names":["#resourceId","#leaseProvider","#sockets","#pending","#getOrCreate","#topicKey","#closed","#initSocket","#socketPath"],"sources":["../../src/utils/signals-pubsub.ts"],"sourcesContent":["import { createHash } from 'node:crypto';\nimport { mkdir } from 'node:fs/promises';\nimport { join } from 'node:path';\n\nimport { PubSub, UnixSocketPubSub } from '@mastra/core/events';\nimport type { Event, EventCallback, LeaseProvider, PubSubDeliveryMode, SubscribeOptions } from '@mastra/core/events';\n\nconst THREAD_STREAM_PREFIX = 'agent.thread-stream.';\n\n/**\n * A PubSub that manages one Unix socket per topic for cross-process signal\n * coordination within a mastracode resource.\n *\n * Socket paths use `/tmp/mc/<resourceId>/<sanitized-topic>.sock` for\n * inspectability and automatic OS cleanup. Each topic gets its own isolated\n * socket so broker election and message routing are per-topic. A dedicated\n * resource-wide socket exposes filesystem leases shared by every topic.\n *\n * Stale sockets from crashed processes are handled by\n * {@link UnixSocketPubSub}'s built-in election logic: it detects\n * ECONNREFUSED on a dead broker socket, unlinks it, and re-elects.\n * No blanket cleanup is needed here — that would break concurrent\n * mc instances sharing the same resourceId.\n */\nclass SignalsPubSub extends PubSub {\n readonly #resourceId: string;\n readonly #leaseProvider: UnixSocketPubSub;\n readonly #sockets = new Map<string, UnixSocketPubSub>();\n readonly #pending = new Map<string, Promise<UnixSocketPubSub>>();\n #closed = false;\n\n constructor(resourceId: string) {\n super();\n this.#resourceId = resourceId;\n this.#leaseProvider = new UnixSocketPubSub(join('/tmp/mc', resourceId, '.leases.sock'));\n }\n\n override get supportedModes(): ReadonlyArray<PubSubDeliveryMode> {\n return ['push'];\n }\n\n /** Exposes the resource-wide filesystem lease provider shared by every topic socket. */\n getLeaseProvider(): LeaseProvider {\n return this.#leaseProvider;\n }\n\n async publish(\n topic: string,\n event: Omit<Event, 'id' | 'createdAt'>,\n options?: { localOnly?: boolean },\n ): Promise<void> {\n const socket = await this.#getOrCreate(topic);\n await socket.publish(topic, event, options);\n }\n\n async subscribe(topic: string, cb: EventCallback, options?: SubscribeOptions): Promise<void> {\n const socket = await this.#getOrCreate(topic);\n await socket.subscribe(topic, cb, options);\n }\n\n async unsubscribe(topic: string, cb: EventCallback): Promise<void> {\n const socket = this.#sockets.get(this.#topicKey(topic));\n if (!socket) return;\n await socket.unsubscribe(topic, cb);\n }\n\n async flush(): Promise<void> {\n await Promise.all([...this.#sockets.values()].map(s => s.flush()));\n }\n\n async close(): Promise<void> {\n this.#closed = true;\n await Promise.allSettled([this.#leaseProvider.close(), ...[...this.#sockets.values()].map(s => s.close())]);\n this.#sockets.clear();\n }\n\n /** Get the underlying socket for a topic (for testing/inspection). */\n getSocket(topic: string): UnixSocketPubSub | undefined {\n return this.#sockets.get(this.#topicKey(topic));\n }\n\n async #getOrCreate(topic: string): Promise<UnixSocketPubSub> {\n if (this.#closed) throw new Error('SignalsPubSub is closed');\n const key = this.#topicKey(topic);\n const existing = this.#sockets.get(key);\n if (existing) return existing;\n // Deduplicate concurrent callers so only one socket is created per topic.\n let inflight = this.#pending.get(key);\n if (!inflight) {\n inflight = this.#initSocket(topic, key);\n this.#pending.set(key, inflight);\n }\n const socket = await inflight;\n if (this.#closed) throw new Error('SignalsPubSub is closed');\n return socket;\n }\n\n async #initSocket(topic: string, key: string): Promise<UnixSocketPubSub> {\n try {\n const socketPath = await this.#socketPath(topic);\n if (this.#closed) throw new Error('SignalsPubSub is closed');\n const socket = new UnixSocketPubSub(socketPath);\n this.#sockets.set(key, socket);\n return socket;\n } finally {\n this.#pending.delete(key);\n }\n }\n\n async #socketPath(topic: string): Promise<string> {\n let key = this.#topicKey(topic);\n const dir = join('/tmp/mc', this.#resourceId);\n await mkdir(dir, { recursive: true });\n const candidate = join(dir, `${key}.sock`);\n // macOS sun_path limit is 104 bytes; Linux is 108. Use 104 as the\n // conservative bound. When the path is too long, replace the key with\n // a short hash so the socket can still be created.\n if (Buffer.byteLength(candidate) > 104) {\n key = createHash('sha256').update(key).digest('hex').slice(0, 16);\n return join(dir, `${key}.sock`);\n }\n return candidate;\n }\n\n /**\n * Derive a filesystem-safe key for the topic. Thread-stream topics embed\n * a threadId; all other topics use a sanitized version of the topic name.\n */\n #topicKey(topic: string): string {\n if (topic.startsWith(THREAD_STREAM_PREFIX)) {\n const encoded = topic.slice(THREAD_STREAM_PREFIX.length);\n try {\n const decoded = decodeURIComponent(encoded);\n const separatorIdx = decoded.indexOf('\\0');\n if (separatorIdx !== -1) {\n return decoded.slice(separatorIdx + 1);\n }\n } catch {\n // Malformed URI — fall through to sanitized fallback.\n }\n }\n // Fallback: use the topic directly (sanitized for filesystem)\n return topic.replace(/[^a-zA-Z0-9_-]/g, '_');\n }\n}\n\n/**\n * Creates a per-topic PubSub backed by Unix sockets for cross-process signal\n * and workflow event coordination within a mastracode resource.\n *\n * Each topic gets its own Unix socket under `/tmp/mc/<resourceId>/`.\n * Stale sockets from crashed processes are handled by the underlying\n * {@link UnixSocketPubSub}'s broker election logic.\n */\nexport function createSignalsPubSub(resourceId: string): SignalsPubSub {\n return new SignalsPubSub(resourceId);\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"signals-pubsub.js","names":["#resourceId","#leaseProvider","#sockets","#pending","#getOrCreate","#topicKey","#closed","#initSocket","#socketPath"],"sources":["../../src/utils/signals-pubsub.ts"],"sourcesContent":["import { mkdir } from 'node:fs/promises';\nimport { join } from 'node:path';\n\nimport { PubSub, UnixSocketPubSub } from '@mastra/core/events';\nimport type { Event, EventCallback, LeaseProvider, PubSubDeliveryMode, SubscribeOptions } from '@mastra/core/events';\n\nconst THREAD_STREAM_PREFIX = 'agent.thread-stream.';\n\n/**\n * A PubSub that manages one Unix socket per topic for cross-process signal\n * coordination within a mastracode resource.\n *\n * Socket paths use `/tmp/mc/<resourceId>/<sanitized-topic>.sock` for\n * inspectability and automatic OS cleanup. Each topic gets its own isolated\n * socket so broker election and message routing are per-topic. A dedicated\n * resource-wide socket exposes filesystem leases shared by every topic.\n *\n * Stale sockets from crashed processes are handled by\n * {@link UnixSocketPubSub}'s built-in election logic: it detects\n * ECONNREFUSED on a dead broker socket, unlinks it, and re-elects.\n * No blanket cleanup is needed here — that would break concurrent\n * mc instances sharing the same resourceId.\n */\nclass SignalsPubSub extends PubSub {\n readonly #resourceId: string;\n readonly #leaseProvider: UnixSocketPubSub;\n readonly #sockets = new Map<string, UnixSocketPubSub>();\n readonly #pending = new Map<string, Promise<UnixSocketPubSub>>();\n #closed = false;\n\n constructor(resourceId: string) {\n super();\n this.#resourceId = resourceId;\n this.#leaseProvider = new UnixSocketPubSub(join('/tmp/mc', resourceId, '.leases.sock'));\n }\n\n override get supportedModes(): ReadonlyArray<PubSubDeliveryMode> {\n return ['push'];\n }\n\n /** Exposes the resource-wide filesystem lease provider shared by every topic socket. */\n getLeaseProvider(): LeaseProvider {\n return this.#leaseProvider;\n }\n\n async publish(\n topic: string,\n event: Omit<Event, 'id' | 'createdAt'>,\n options?: { localOnly?: boolean },\n ): Promise<void> {\n const socket = await this.#getOrCreate(topic);\n await socket.publish(topic, event, options);\n }\n\n async subscribe(topic: string, cb: EventCallback, options?: SubscribeOptions): Promise<void> {\n const socket = await this.#getOrCreate(topic);\n await socket.subscribe(topic, cb, options);\n }\n\n async unsubscribe(topic: string, cb: EventCallback): Promise<void> {\n const socket = this.#sockets.get(this.#topicKey(topic));\n if (!socket) return;\n await socket.unsubscribe(topic, cb);\n }\n\n async flush(): Promise<void> {\n await Promise.all([...this.#sockets.values()].map(s => s.flush()));\n }\n\n async close(): Promise<void> {\n this.#closed = true;\n await Promise.allSettled([this.#leaseProvider.close(), ...[...this.#sockets.values()].map(s => s.close())]);\n this.#sockets.clear();\n }\n\n /** Get the underlying socket for a topic (for testing/inspection). */\n getSocket(topic: string): UnixSocketPubSub | undefined {\n return this.#sockets.get(this.#topicKey(topic));\n }\n\n async #getOrCreate(topic: string): Promise<UnixSocketPubSub> {\n if (this.#closed) throw new Error('SignalsPubSub is closed');\n const key = this.#topicKey(topic);\n const existing = this.#sockets.get(key);\n if (existing) return existing;\n // Deduplicate concurrent callers so only one socket is created per topic.\n let inflight = this.#pending.get(key);\n if (!inflight) {\n inflight = this.#initSocket(topic, key);\n this.#pending.set(key, inflight);\n }\n const socket = await inflight;\n if (this.#closed) throw new Error('SignalsPubSub is closed');\n return socket;\n }\n\n async #initSocket(topic: string, key: string): Promise<UnixSocketPubSub> {\n try {\n const socketPath = await this.#socketPath(topic);\n if (this.#closed) throw new Error('SignalsPubSub is closed');\n const socket = new UnixSocketPubSub(socketPath);\n this.#sockets.set(key, socket);\n return socket;\n } finally {\n this.#pending.delete(key);\n }\n }\n\n async #socketPath(topic: string): Promise<string> {\n let key = this.#topicKey(topic);\n const dir = join('/tmp/mc', this.#resourceId);\n await mkdir(dir, { recursive: true });\n const candidate = join(dir, `${key}.sock`);\n // macOS sun_path limit is 104 bytes; Linux is 108. Use 104 as the\n // conservative bound. When the path is too long, replace the key with\n // a short hash so the socket can still be created.\n if (Buffer.byteLength(candidate) > 104) {\n key = Buffer.from(await globalThis.crypto.subtle.digest('SHA-256', new TextEncoder().encode(key)))\n .toString('hex')\n .slice(0, 16);\n return join(dir, `${key}.sock`);\n }\n return candidate;\n }\n\n /**\n * Derive a filesystem-safe key for the topic. Thread-stream topics embed\n * a threadId; all other topics use a sanitized version of the topic name.\n */\n #topicKey(topic: string): string {\n if (topic.startsWith(THREAD_STREAM_PREFIX)) {\n const encoded = topic.slice(THREAD_STREAM_PREFIX.length);\n try {\n const decoded = decodeURIComponent(encoded);\n const separatorIdx = decoded.indexOf('\\0');\n if (separatorIdx !== -1) {\n return decoded.slice(separatorIdx + 1);\n }\n } catch {\n // Malformed URI — fall through to sanitized fallback.\n }\n }\n // Fallback: use the topic directly (sanitized for filesystem)\n return topic.replace(/[^a-zA-Z0-9_-]/g, '_');\n }\n}\n\n/**\n * Creates a per-topic PubSub backed by Unix sockets for cross-process signal\n * and workflow event coordination within a mastracode resource.\n *\n * Each topic gets its own Unix socket under `/tmp/mc/<resourceId>/`.\n * Stale sockets from crashed processes are handled by the underlying\n * {@link UnixSocketPubSub}'s broker election logic.\n */\nexport function createSignalsPubSub(resourceId: string): SignalsPubSub {\n return new SignalsPubSub(resourceId);\n}\n"],"mappings":"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"}
@@ -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
2
  import { dirname } from "path";
3
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) {