@massu/core 1.16.2 → 1.16.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +68 -0
- package/dist/cli.js +1089 -696
- package/dist/hooks/auto-learning-pipeline.js +401 -55
- package/dist/hooks/classify-failure.js +399 -53
- package/dist/hooks/cost-tracker.js +411 -65
- package/dist/hooks/fix-detector.js +410 -64
- package/dist/hooks/incident-pipeline.js +396 -50
- package/dist/hooks/intent-suggester.js +411 -65
- package/dist/hooks/memory-recall.js +415 -69
- package/dist/hooks/post-edit-context.js +413 -67
- package/dist/hooks/post-tool-use.js +439 -93
- package/dist/hooks/pre-compact.js +411 -65
- package/dist/hooks/pre-delete-check.js +415 -69
- package/dist/hooks/pre-tool-use-gate.js +415 -69
- package/dist/hooks/quality-event.js +411 -65
- package/dist/hooks/rule-enforcement-pipeline.js +397 -51
- package/dist/hooks/security-gate.js +411 -65
- package/dist/hooks/session-end.js +440 -94
- package/dist/hooks/session-start.js +532 -282
- package/dist/hooks/user-prompt.js +444 -98
- package/package.json +1 -1
|
@@ -26,16 +26,6 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
26
26
|
};
|
|
27
27
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
28
28
|
|
|
29
|
-
// src/lib/memory-path.ts
|
|
30
|
-
function encodeMemoryDirName(projectRoot) {
|
|
31
|
-
return projectRoot.replace(/\//g, "-");
|
|
32
|
-
}
|
|
33
|
-
var init_memory_path = __esm({
|
|
34
|
-
"src/lib/memory-path.ts"() {
|
|
35
|
-
"use strict";
|
|
36
|
-
}
|
|
37
|
-
});
|
|
38
|
-
|
|
39
29
|
// src/credentials.ts
|
|
40
30
|
import { homedir } from "os";
|
|
41
31
|
import { resolve } from "path";
|
|
@@ -104,6 +94,345 @@ var init_credentials = __esm({
|
|
|
104
94
|
}
|
|
105
95
|
});
|
|
106
96
|
|
|
97
|
+
// src/lib/fileLock.ts
|
|
98
|
+
import { mkdirSync as mkdirSync2, readFileSync as readFileSync2, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
99
|
+
import { dirname } from "path";
|
|
100
|
+
import * as lockfile from "proper-lockfile";
|
|
101
|
+
function readLockHolderPid(lockPath) {
|
|
102
|
+
try {
|
|
103
|
+
const raw = readFileSync2(`${lockPath}.pid`, "utf-8").trim();
|
|
104
|
+
const pid = Number.parseInt(raw, 10);
|
|
105
|
+
if (!Number.isFinite(pid) || pid <= 0) return null;
|
|
106
|
+
return pid;
|
|
107
|
+
} catch {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
function busyWaitSync(ms) {
|
|
112
|
+
if (typeof SharedArrayBuffer === "undefined" || typeof Atomics === "undefined") {
|
|
113
|
+
throw new Error(
|
|
114
|
+
`withFileLockSync requires SharedArrayBuffer + Atomics for its retry-loop wait. This Node runtime does not provide them \u2014 refusing to fall back to a CPU spinloop. If you hit this in a sandboxed serverless env, the fix is to perform the lock-protected operation in a host runtime that supports Atomics.`
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
const sab = new SharedArrayBuffer(4);
|
|
118
|
+
const view = new Int32Array(sab);
|
|
119
|
+
Atomics.wait(view, 0, 0, ms);
|
|
120
|
+
}
|
|
121
|
+
function withFileLockSync(lockPath, fn, opts = {}) {
|
|
122
|
+
mkdirSync2(dirname(lockPath), { recursive: true });
|
|
123
|
+
const staleMs = opts.staleMs ?? 3e4;
|
|
124
|
+
const blockMs = opts.retries === 0 ? 0 : opts.blockMs ?? 3e4;
|
|
125
|
+
const pollIntervalMs = opts.pollIntervalMs ?? 100;
|
|
126
|
+
const now = opts.now ?? Date.now;
|
|
127
|
+
const sleep = opts.sleep ?? busyWaitSync;
|
|
128
|
+
const makeBusyError = opts.errorFactory ?? ((path, pid, retrySeconds, code) => new FileLockBusyError(path, pid, retrySeconds, code));
|
|
129
|
+
let release = null;
|
|
130
|
+
const deadline = now() + blockMs;
|
|
131
|
+
for (; ; ) {
|
|
132
|
+
try {
|
|
133
|
+
release = lockfile.lockSync(lockPath, {
|
|
134
|
+
stale: staleMs,
|
|
135
|
+
retries: 0,
|
|
136
|
+
realpath: false
|
|
137
|
+
});
|
|
138
|
+
try {
|
|
139
|
+
writeFileSync2(`${lockPath}.pid`, String(process.pid), "utf-8");
|
|
140
|
+
} catch {
|
|
141
|
+
}
|
|
142
|
+
break;
|
|
143
|
+
} catch (err) {
|
|
144
|
+
const e = err;
|
|
145
|
+
const code = e.code;
|
|
146
|
+
if (code !== "ELOCKED" && code !== "EBUSY") {
|
|
147
|
+
throw err;
|
|
148
|
+
}
|
|
149
|
+
if (now() >= deadline) {
|
|
150
|
+
const holderPid = readLockHolderPid(lockPath);
|
|
151
|
+
const remainingMs = Math.max(0, deadline - now());
|
|
152
|
+
const retryAfterSeconds = blockMs === 0 ? Math.round(staleMs / 1e3) : Math.round(remainingMs / 1e3);
|
|
153
|
+
throw makeBusyError(lockPath, holderPid, retryAfterSeconds, code);
|
|
154
|
+
}
|
|
155
|
+
sleep(pollIntervalMs);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
try {
|
|
159
|
+
return fn();
|
|
160
|
+
} finally {
|
|
161
|
+
try {
|
|
162
|
+
if (release) release();
|
|
163
|
+
} catch {
|
|
164
|
+
}
|
|
165
|
+
try {
|
|
166
|
+
rmSync2(`${lockPath}.pid`, { force: true });
|
|
167
|
+
} catch {
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
var FileLockBusyError;
|
|
172
|
+
var init_fileLock = __esm({
|
|
173
|
+
"src/lib/fileLock.ts"() {
|
|
174
|
+
"use strict";
|
|
175
|
+
FileLockBusyError = class extends Error {
|
|
176
|
+
constructor(lockPath, holderPid, retryAfterSeconds, causeCode) {
|
|
177
|
+
const pidPart = holderPid != null ? `(PID=${holderPid})` : "(PID=unknown)";
|
|
178
|
+
super(`File lock at ${lockPath} held by another process ${pidPart} \u2014 try again in ${retryAfterSeconds}s`);
|
|
179
|
+
this.lockPath = lockPath;
|
|
180
|
+
this.holderPid = holderPid;
|
|
181
|
+
this.retryAfterSeconds = retryAfterSeconds;
|
|
182
|
+
this.causeCode = causeCode;
|
|
183
|
+
this.name = "FileLockBusyError";
|
|
184
|
+
}
|
|
185
|
+
lockPath;
|
|
186
|
+
holderPid;
|
|
187
|
+
retryAfterSeconds;
|
|
188
|
+
causeCode;
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
// src/lib/sqlite-loader.ts
|
|
194
|
+
import { createRequire } from "module";
|
|
195
|
+
import { spawnSync } from "child_process";
|
|
196
|
+
import { accessSync, appendFileSync, chmodSync as chmodSync2, constants as fsConstants, existsSync as existsSync2, mkdirSync as mkdirSync3 } from "fs";
|
|
197
|
+
import { dirname as dirname2, join } from "path";
|
|
198
|
+
function isNativeAbiError(err) {
|
|
199
|
+
const code = err?.code;
|
|
200
|
+
if (code === "ERR_DLOPEN_FAILED") return true;
|
|
201
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
202
|
+
return /NODE_MODULE_VERSION/.test(msg) || /was compiled against a different Node\.js version/.test(msg) || /ERR_DLOPEN_FAILED/.test(msg) || /\.node\b/.test(msg) && /dlopen|invalid ELF|mach-o|image not found|no such file|not a valid Win32/i.test(msg);
|
|
203
|
+
}
|
|
204
|
+
function loadBetterSqlite3(opts = {}) {
|
|
205
|
+
if (_testCtor) return _testCtor;
|
|
206
|
+
if (opts.fresh) bustNativeCache();
|
|
207
|
+
return req("better-sqlite3");
|
|
208
|
+
}
|
|
209
|
+
function bustNativeCache() {
|
|
210
|
+
try {
|
|
211
|
+
const cache = req.cache;
|
|
212
|
+
if (!cache) return;
|
|
213
|
+
for (const key of Object.keys(cache)) {
|
|
214
|
+
if (/better[-_]sqlite3|better_sqlite3\.node|[\\/]bindings[\\/]/.test(key)) {
|
|
215
|
+
delete cache[key];
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
} catch {
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
function openDatabase(dbPath, opts = {}) {
|
|
222
|
+
const { selfHeal: selfHealOpt = true, ...ctorOpts } = opts;
|
|
223
|
+
const selfHeal = selfHealOpt && process.env.MASSU_HOOK_RUNTIME !== "1";
|
|
224
|
+
const Ctor = loadBetterSqlite3();
|
|
225
|
+
try {
|
|
226
|
+
return new Ctor(dbPath, ctorOpts);
|
|
227
|
+
} catch (err) {
|
|
228
|
+
if (!isNativeAbiError(err)) {
|
|
229
|
+
throw err;
|
|
230
|
+
}
|
|
231
|
+
if (!selfHeal) {
|
|
232
|
+
recordHealEvent({ phase: "skipped", reason: "self-heal-disabled", abiTo: process.versions.modules });
|
|
233
|
+
throw new MemoryEngineUnusableError("abi-mismatch", detailOf(err));
|
|
234
|
+
}
|
|
235
|
+
const result = (_testHeal ?? attemptNativeHeal)(err);
|
|
236
|
+
if (!result.healed && !result.contended) {
|
|
237
|
+
throw new MemoryEngineUnusableError(result.reason ?? "heal-failed", result.detail ?? detailOf(err));
|
|
238
|
+
}
|
|
239
|
+
try {
|
|
240
|
+
const FreshCtor = loadBetterSqlite3({ fresh: true });
|
|
241
|
+
return new FreshCtor(dbPath, ctorOpts);
|
|
242
|
+
} catch (retryErr) {
|
|
243
|
+
throw new MemoryEngineUnusableError("heal-failed", detailOf(retryErr));
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
function attemptNativeHeal(err) {
|
|
248
|
+
const start = Date.now();
|
|
249
|
+
const abiTo = process.versions.modules;
|
|
250
|
+
const abiFrom = parseAbiFrom(err);
|
|
251
|
+
let pkgDir;
|
|
252
|
+
try {
|
|
253
|
+
pkgDir = dirname2(req.resolve("better-sqlite3/package.json"));
|
|
254
|
+
} catch {
|
|
255
|
+
recordHealEvent({ phase: "skipped", reason: "not-resolvable", abiFrom, abiTo });
|
|
256
|
+
return { healed: false, reason: "missing", abiFrom, abiTo, detail: "better-sqlite3 not resolvable" };
|
|
257
|
+
}
|
|
258
|
+
if (!isWritable(pkgDir)) {
|
|
259
|
+
recordHealEvent({ phase: "skipped", reason: "dir-not-writable", abiFrom, abiTo });
|
|
260
|
+
return { healed: false, reason: "heal-failed", abiFrom, abiTo, detail: `install dir not writable: ${pkgDir}` };
|
|
261
|
+
}
|
|
262
|
+
recordHealEvent({ phase: "attempt", reason: "abi-mismatch", abiFrom, abiTo });
|
|
263
|
+
try {
|
|
264
|
+
return withFileLockSync(
|
|
265
|
+
join(credentialsDir(), "native-heal.lock"),
|
|
266
|
+
() => runRebuild(pkgDir, abiFrom, abiTo, start),
|
|
267
|
+
{ blockMs: 6e4, staleMs: 3e5 }
|
|
268
|
+
);
|
|
269
|
+
} catch (lockErr) {
|
|
270
|
+
const detail = lockErr instanceof Error ? lockErr.message : String(lockErr);
|
|
271
|
+
recordHealEvent({ phase: "failed", reason: "heal-failed", abiFrom, abiTo, detail: `lock: ${detail}` });
|
|
272
|
+
return { healed: false, contended: true, reason: "heal-failed", abiFrom, abiTo, detail: `heal lock contended: ${detail}` };
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
function runRebuild(pkgDir, abiFrom, abiTo, start) {
|
|
276
|
+
try {
|
|
277
|
+
const FreshCtor = loadBetterSqlite3({ fresh: true });
|
|
278
|
+
new FreshCtor(":memory:").close();
|
|
279
|
+
const res2 = {
|
|
280
|
+
healed: true,
|
|
281
|
+
abiFrom,
|
|
282
|
+
abiTo,
|
|
283
|
+
durationMs: Date.now() - start,
|
|
284
|
+
detail: "already healed by a concurrent process"
|
|
285
|
+
};
|
|
286
|
+
recordHealEvent({ phase: "success", reason: "already-healed", abiFrom, abiTo, durationMs: res2.durationMs });
|
|
287
|
+
return res2;
|
|
288
|
+
} catch (probeErr) {
|
|
289
|
+
if (!isNativeAbiError(probeErr)) {
|
|
290
|
+
return { healed: false, reason: "heal-failed", abiFrom, abiTo, durationMs: Date.now() - start, detail: detailOf(probeErr) };
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
const prebuildBin = resolveLocalBin(pkgDir, "prebuild-install");
|
|
294
|
+
if (prebuildBin) {
|
|
295
|
+
const r = spawnSync(process.execPath, [prebuildBin], {
|
|
296
|
+
cwd: pkgDir,
|
|
297
|
+
encoding: "utf-8",
|
|
298
|
+
timeout: 12e4
|
|
299
|
+
});
|
|
300
|
+
if (r.status === 0) {
|
|
301
|
+
const res2 = { healed: true, method: "prebuild-install", abiFrom, abiTo, durationMs: Date.now() - start };
|
|
302
|
+
recordHealEvent({ phase: "success", reason: "abi-mismatch", ...res2 });
|
|
303
|
+
return res2;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
const gypBin = resolveNodeGypBin();
|
|
307
|
+
if (gypBin && hasCompiler()) {
|
|
308
|
+
const r = spawnSync(process.execPath, [gypBin, "rebuild", "--release"], {
|
|
309
|
+
cwd: pkgDir,
|
|
310
|
+
encoding: "utf-8",
|
|
311
|
+
timeout: 3e5
|
|
312
|
+
});
|
|
313
|
+
if (r.status === 0) {
|
|
314
|
+
const res2 = { healed: true, method: "node-gyp", abiFrom, abiTo, durationMs: Date.now() - start };
|
|
315
|
+
recordHealEvent({ phase: "success", reason: "abi-mismatch", ...res2 });
|
|
316
|
+
return res2;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
const res = {
|
|
320
|
+
healed: false,
|
|
321
|
+
reason: "heal-failed",
|
|
322
|
+
abiFrom,
|
|
323
|
+
abiTo,
|
|
324
|
+
durationMs: Date.now() - start,
|
|
325
|
+
detail: prebuildBin ? "prebuild-install failed; node-gyp fallback unavailable or failed" : "no prebuild-install bin resolvable"
|
|
326
|
+
};
|
|
327
|
+
recordHealEvent({ phase: "failed", reason: "heal-failed", abiFrom, abiTo, durationMs: res.durationMs });
|
|
328
|
+
return res;
|
|
329
|
+
}
|
|
330
|
+
function recordHealEvent(event) {
|
|
331
|
+
try {
|
|
332
|
+
const rec = {
|
|
333
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
334
|
+
node: process.versions.node,
|
|
335
|
+
platform: process.platform,
|
|
336
|
+
arch: process.arch,
|
|
337
|
+
...event
|
|
338
|
+
};
|
|
339
|
+
const dir = credentialsDir();
|
|
340
|
+
mkdirSync3(dir, { recursive: true });
|
|
341
|
+
const file = join(dir, "native-heal-events.jsonl");
|
|
342
|
+
appendFileSync(file, JSON.stringify(rec) + "\n", "utf-8");
|
|
343
|
+
try {
|
|
344
|
+
chmodSync2(file, 384);
|
|
345
|
+
} catch {
|
|
346
|
+
}
|
|
347
|
+
} catch {
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
function parseAbiFrom(err) {
|
|
351
|
+
const msg = err instanceof Error ? err.message : String(err ?? "");
|
|
352
|
+
const m = /NODE_MODULE_VERSION (\d+)/.exec(msg);
|
|
353
|
+
if (m && /^\d+$/.test(m[1])) return m[1];
|
|
354
|
+
return void 0;
|
|
355
|
+
}
|
|
356
|
+
function detailOf(err) {
|
|
357
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
358
|
+
return msg.slice(0, 300);
|
|
359
|
+
}
|
|
360
|
+
function isWritable(dir) {
|
|
361
|
+
try {
|
|
362
|
+
accessSync(dir, fsConstants.W_OK);
|
|
363
|
+
return true;
|
|
364
|
+
} catch {
|
|
365
|
+
return false;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
function resolveLocalBin(pkgDir, name) {
|
|
369
|
+
const candidates = [
|
|
370
|
+
join(pkgDir, "node_modules", ".bin", name),
|
|
371
|
+
// nested install
|
|
372
|
+
join(pkgDir, "..", ".bin", name),
|
|
373
|
+
// hoisted (node_modules/.bin)
|
|
374
|
+
join(pkgDir, "..", "..", "node_modules", ".bin", name)
|
|
375
|
+
// workspace hoist
|
|
376
|
+
];
|
|
377
|
+
for (const c of candidates) {
|
|
378
|
+
if (existsSync2(c)) return c;
|
|
379
|
+
}
|
|
380
|
+
return void 0;
|
|
381
|
+
}
|
|
382
|
+
function resolveNodeGypBin() {
|
|
383
|
+
try {
|
|
384
|
+
return req.resolve("node-gyp/bin/node-gyp.js");
|
|
385
|
+
} catch {
|
|
386
|
+
return void 0;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
function hasCompiler() {
|
|
390
|
+
for (const cc of ["cc", "clang", "gcc"]) {
|
|
391
|
+
try {
|
|
392
|
+
const r = spawnSync(cc, ["--version"], { encoding: "utf-8", timeout: 5e3 });
|
|
393
|
+
if (r.status === 0) return true;
|
|
394
|
+
} catch {
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
return false;
|
|
398
|
+
}
|
|
399
|
+
var req, NATIVE_DB_REMEDY, MemoryEngineUnusableError, _testCtor, _testHeal;
|
|
400
|
+
var init_sqlite_loader = __esm({
|
|
401
|
+
"src/lib/sqlite-loader.ts"() {
|
|
402
|
+
"use strict";
|
|
403
|
+
init_credentials();
|
|
404
|
+
init_fileLock();
|
|
405
|
+
req = createRequire(import.meta.url);
|
|
406
|
+
NATIVE_DB_REMEDY = "Run 'massu heal' to rebuild the database engine for your Node version, then restart your MCP client / Claude Code.";
|
|
407
|
+
MemoryEngineUnusableError = class extends Error {
|
|
408
|
+
reason;
|
|
409
|
+
remedy;
|
|
410
|
+
detail;
|
|
411
|
+
constructor(reason, detail) {
|
|
412
|
+
super(
|
|
413
|
+
`Massu memory engine is unusable (${reason}). ${NATIVE_DB_REMEDY}` + (detail ? ` [${detail}]` : "")
|
|
414
|
+
);
|
|
415
|
+
this.name = "MemoryEngineUnusableError";
|
|
416
|
+
this.reason = reason;
|
|
417
|
+
this.remedy = NATIVE_DB_REMEDY;
|
|
418
|
+
this.detail = detail;
|
|
419
|
+
}
|
|
420
|
+
};
|
|
421
|
+
_testCtor = null;
|
|
422
|
+
_testHeal = null;
|
|
423
|
+
}
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
// src/lib/memory-path.ts
|
|
427
|
+
function encodeMemoryDirName(projectRoot) {
|
|
428
|
+
return projectRoot.replace(/\//g, "-");
|
|
429
|
+
}
|
|
430
|
+
var init_memory_path = __esm({
|
|
431
|
+
"src/lib/memory-path.ts"() {
|
|
432
|
+
"use strict";
|
|
433
|
+
}
|
|
434
|
+
});
|
|
435
|
+
|
|
107
436
|
// src/config-memory-schema.ts
|
|
108
437
|
import { z } from "zod";
|
|
109
438
|
var MemoryConfigSchema;
|
|
@@ -243,8 +572,8 @@ var init_config_memory_schema = __esm({
|
|
|
243
572
|
});
|
|
244
573
|
|
|
245
574
|
// src/config.ts
|
|
246
|
-
import { resolve as resolve2, dirname } from "path";
|
|
247
|
-
import { existsSync as
|
|
575
|
+
import { resolve as resolve2, dirname as dirname3 } from "path";
|
|
576
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
|
|
248
577
|
import { homedir as homedir2 } from "os";
|
|
249
578
|
import { parse as parseYaml } from "yaml";
|
|
250
579
|
import { z as z2 } from "zod";
|
|
@@ -252,22 +581,22 @@ function findProjectRoot() {
|
|
|
252
581
|
const cwd = process.cwd();
|
|
253
582
|
let dir = cwd;
|
|
254
583
|
while (true) {
|
|
255
|
-
if (
|
|
584
|
+
if (existsSync3(resolve2(dir, "massu.config.yaml"))) {
|
|
256
585
|
return dir;
|
|
257
586
|
}
|
|
258
|
-
const parent =
|
|
587
|
+
const parent = dirname3(dir);
|
|
259
588
|
if (parent === dir) break;
|
|
260
589
|
dir = parent;
|
|
261
590
|
}
|
|
262
591
|
dir = cwd;
|
|
263
592
|
while (true) {
|
|
264
|
-
if (
|
|
593
|
+
if (existsSync3(resolve2(dir, "package.json"))) {
|
|
265
594
|
return dir;
|
|
266
595
|
}
|
|
267
|
-
if (
|
|
596
|
+
if (existsSync3(resolve2(dir, ".git"))) {
|
|
268
597
|
return dir;
|
|
269
598
|
}
|
|
270
|
-
const parent =
|
|
599
|
+
const parent = dirname3(dir);
|
|
271
600
|
if (parent === dir) break;
|
|
272
601
|
dir = parent;
|
|
273
602
|
}
|
|
@@ -284,8 +613,8 @@ function getConfig() {
|
|
|
284
613
|
const root = getProjectRoot();
|
|
285
614
|
const configPath = resolve2(root, "massu.config.yaml");
|
|
286
615
|
let rawYaml = {};
|
|
287
|
-
if (
|
|
288
|
-
const content =
|
|
616
|
+
if (existsSync3(configPath)) {
|
|
617
|
+
const content = readFileSync3(configPath, "utf-8");
|
|
289
618
|
rawYaml = parseYaml(content) ?? {};
|
|
290
619
|
}
|
|
291
620
|
const result = RawConfigSchema.safeParse(rawYaml);
|
|
@@ -366,9 +695,13 @@ Hint: run \`massu config refresh\` to regenerate a valid config or fix the liste
|
|
|
366
695
|
const resolvedEndpoint = resolveEndpoint({ configEndpoint: _config.cloud?.endpoint });
|
|
367
696
|
if (resolvedKey.apiKey) {
|
|
368
697
|
_config.cloud = {
|
|
369
|
-
enabled: true,
|
|
370
698
|
sync: { memory: true, analytics: true, audit: true },
|
|
371
699
|
..._config.cloud,
|
|
700
|
+
// Spread FIRST, then decide `enabled`, so a workspace that declares a `cloud:`
|
|
701
|
+
// block only to tune `requestTimeoutMs` does not disable its own sync. An
|
|
702
|
+
// EXPLICIT `enabled: false` is still honoured; `undefined` (not stated) means
|
|
703
|
+
// "a key resolved, so turn it on". (plan-2026-07-20-cloud-sync-timeout)
|
|
704
|
+
enabled: _config.cloud?.enabled ?? true,
|
|
372
705
|
apiKey: resolvedKey.apiKey,
|
|
373
706
|
endpoint: resolvedEndpoint
|
|
374
707
|
};
|
|
@@ -588,9 +921,22 @@ var init_config = __esm({
|
|
|
588
921
|
})).default([])
|
|
589
922
|
}).optional();
|
|
590
923
|
CloudConfigSchema = z2.object({
|
|
591
|
-
|
|
924
|
+
// OPTIONAL, not `.default(false)`. A zod default is indistinguishable from an
|
|
925
|
+
// explicit value once parsed, so `.default(false)` meant that merely declaring a
|
|
926
|
+
// `cloud:` block (to set any OTHER key) silently produced `enabled: false`, which
|
|
927
|
+
// then overrode the `enabled: true` auto-enable below via object spread — turning
|
|
928
|
+
// cloud sync OFF as a side effect of tuning it. `undefined` now means "not stated",
|
|
929
|
+
// and the auto-enable preserves it. (plan-2026-07-20-cloud-sync-timeout)
|
|
930
|
+
enabled: z2.boolean().optional(),
|
|
592
931
|
apiKey: z2.string().optional(),
|
|
593
932
|
endpoint: z2.string().optional(),
|
|
933
|
+
// Per-request POST budget for the `/sync` ingest. cloud-sync.ts reads this
|
|
934
|
+
// (`(cloud as { requestTimeoutMs?: number }).requestTimeoutMs`) but it was ABSENT
|
|
935
|
+
// from this schema, and zod strips unknown keys — so the knob was unreachable from
|
|
936
|
+
// massu.config.yaml and the default could never be tuned. Measured 2026-07-20: a
|
|
937
|
+
// 423-observation payload takes ~9.2s against the live ingest. Capped at
|
|
938
|
+
// SYNC_DEADLINE_MS (20s) since the overall deadline clamps each attempt anyway.
|
|
939
|
+
requestTimeoutMs: z2.number().int().positive().max(2e4).optional(),
|
|
594
940
|
sync: z2.object({
|
|
595
941
|
memory: z2.boolean().default(true),
|
|
596
942
|
analytics: z2.boolean().default(true),
|
|
@@ -827,27 +1173,26 @@ var init_memory_vector = __esm({
|
|
|
827
1173
|
});
|
|
828
1174
|
|
|
829
1175
|
// src/db-backup.ts
|
|
830
|
-
import
|
|
831
|
-
import {
|
|
832
|
-
import { resolve as resolve3, join, basename, dirname as dirname2 } from "path";
|
|
1176
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync4, readdirSync, statSync, unlinkSync, copyFileSync, rmSync as rmSync3 } from "fs";
|
|
1177
|
+
import { resolve as resolve3, join as join2, basename, dirname as dirname4 } from "path";
|
|
833
1178
|
import { homedir as homedir3 } from "os";
|
|
834
1179
|
function dbBackupsRoot(home = homedir3()) {
|
|
835
1180
|
return resolve3(home, ".massu", "db-backups");
|
|
836
1181
|
}
|
|
837
1182
|
function projectBackupDir(projectRoot, home = homedir3()) {
|
|
838
1183
|
const slug = basename(projectRoot).replace(/[^A-Za-z0-9._-]/g, "_") || "project";
|
|
839
|
-
return
|
|
1184
|
+
return join2(dbBackupsRoot(home), slug);
|
|
840
1185
|
}
|
|
841
1186
|
function backupStamp(nowMs) {
|
|
842
1187
|
return new Date(nowMs).toISOString().replace(/[:.]/g, "-");
|
|
843
1188
|
}
|
|
844
1189
|
function listDbBackups(projectRoot, home = homedir3()) {
|
|
845
1190
|
const dir = projectBackupDir(projectRoot, home);
|
|
846
|
-
if (!
|
|
1191
|
+
if (!existsSync4(dir)) return [];
|
|
847
1192
|
const out = [];
|
|
848
1193
|
for (const f of readdirSync(dir)) {
|
|
849
1194
|
if (!f.endsWith(".db")) continue;
|
|
850
|
-
const p =
|
|
1195
|
+
const p = join2(dir, f);
|
|
851
1196
|
const st = statSync(p);
|
|
852
1197
|
const db = f.replace(/-\d{4}-\d{2}-\d{2}T.*$/, "");
|
|
853
1198
|
out.push({ db, path: p, bytes: st.size, mtimeMs: st.mtimeMs });
|
|
@@ -860,13 +1205,13 @@ function hasFreshDbBackup(projectRoot, dbPath, nowMs = Date.now(), home = homedi
|
|
|
860
1205
|
if (backups.length === 0) return false;
|
|
861
1206
|
const newest = backups[0];
|
|
862
1207
|
if (nowMs - newest.mtimeMs > FRESH_WINDOW_MS) return false;
|
|
863
|
-
if (
|
|
1208
|
+
if (existsSync4(dbPath) && statSync(dbPath).mtimeMs > newest.mtimeMs) return false;
|
|
864
1209
|
return true;
|
|
865
1210
|
}
|
|
866
1211
|
function integrityOk(dbPath) {
|
|
867
1212
|
let db = null;
|
|
868
1213
|
try {
|
|
869
|
-
db =
|
|
1214
|
+
db = openDatabase(dbPath, { readonly: true });
|
|
870
1215
|
const rows = db.pragma("integrity_check");
|
|
871
1216
|
return rows.length === 1 && rows[0].integrity_check === "ok";
|
|
872
1217
|
} catch {
|
|
@@ -879,16 +1224,16 @@ function integrityOk(dbPath) {
|
|
|
879
1224
|
}
|
|
880
1225
|
}
|
|
881
1226
|
function backupDb(projectRoot, dbPath, nowMs = Date.now(), home = homedir3()) {
|
|
882
|
-
if (!
|
|
1227
|
+
if (!existsSync4(dbPath)) {
|
|
883
1228
|
throw new DbBackupError(`cannot back up a database that does not exist: ${dbPath}`);
|
|
884
1229
|
}
|
|
885
1230
|
const dir = projectBackupDir(projectRoot, home);
|
|
886
|
-
|
|
1231
|
+
mkdirSync4(dir, { recursive: true });
|
|
887
1232
|
const name = basename(dbPath, ".db");
|
|
888
|
-
const dest =
|
|
1233
|
+
const dest = join2(dir, `${name}-${backupStamp(nowMs)}.db`);
|
|
889
1234
|
let src = null;
|
|
890
1235
|
try {
|
|
891
|
-
src =
|
|
1236
|
+
src = openDatabase(dbPath, { readonly: true });
|
|
892
1237
|
src.exec(`VACUUM INTO '${dest.replace(/'/g, "''")}'`);
|
|
893
1238
|
} catch (err) {
|
|
894
1239
|
throw new DbBackupError(
|
|
@@ -927,7 +1272,7 @@ function pruneDbBackups(projectRoot, dbName, keep = DEFAULT_RETENTION, home = ho
|
|
|
927
1272
|
}
|
|
928
1273
|
function backupBeforeSchemaChange(projectRoot, dbPath, onError, nowMs = Date.now(), home = homedir3()) {
|
|
929
1274
|
try {
|
|
930
|
-
if (!
|
|
1275
|
+
if (!existsSync4(dbPath)) return null;
|
|
931
1276
|
if (hasFreshDbBackup(projectRoot, dbPath, nowMs, home)) return null;
|
|
932
1277
|
return backupDb(projectRoot, dbPath, nowMs, home);
|
|
933
1278
|
} catch (err) {
|
|
@@ -939,6 +1284,7 @@ var DEFAULT_RETENTION, FRESH_WINDOW_MS, DbBackupError;
|
|
|
939
1284
|
var init_db_backup = __esm({
|
|
940
1285
|
"src/db-backup.ts"() {
|
|
941
1286
|
"use strict";
|
|
1287
|
+
init_sqlite_loader();
|
|
942
1288
|
DEFAULT_RETENTION = 5;
|
|
943
1289
|
FRESH_WINDOW_MS = 24 * 60 * 60 * 1e3;
|
|
944
1290
|
DbBackupError = class extends Error {
|
|
@@ -1051,21 +1397,21 @@ var init_rule_candidate_store = __esm({
|
|
|
1051
1397
|
});
|
|
1052
1398
|
|
|
1053
1399
|
// src/hooks/lib/hook-failure-signal.ts
|
|
1054
|
-
import { appendFileSync, mkdirSync as
|
|
1055
|
-
import { join as
|
|
1400
|
+
import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync5, existsSync as existsSync5 } from "fs";
|
|
1401
|
+
import { join as join3, dirname as dirname5 } from "path";
|
|
1056
1402
|
function resolveFailureLogPath() {
|
|
1057
1403
|
const explicit = process.env.MASSU_HOOK_FAILURE_LOG;
|
|
1058
1404
|
if (explicit) return explicit;
|
|
1059
1405
|
let dir = process.cwd();
|
|
1060
1406
|
for (let i = 0; i < 12; i++) {
|
|
1061
|
-
if (
|
|
1062
|
-
return
|
|
1407
|
+
if (existsSync5(join3(dir, ".massu")) || existsSync5(join3(dir, "massu.config.yaml"))) {
|
|
1408
|
+
return join3(dir, ".massu", "hook-failures.jsonl");
|
|
1063
1409
|
}
|
|
1064
|
-
const parent =
|
|
1410
|
+
const parent = dirname5(dir);
|
|
1065
1411
|
if (parent === dir) break;
|
|
1066
1412
|
dir = parent;
|
|
1067
1413
|
}
|
|
1068
|
-
return
|
|
1414
|
+
return join3(process.cwd(), ".massu", "hook-failures.jsonl");
|
|
1069
1415
|
}
|
|
1070
1416
|
function recordHookFailure(hook, error, context) {
|
|
1071
1417
|
const record = {
|
|
@@ -1078,8 +1424,8 @@ function recordHookFailure(hook, error, context) {
|
|
|
1078
1424
|
let wroteSomething = false;
|
|
1079
1425
|
try {
|
|
1080
1426
|
const path = resolveFailureLogPath();
|
|
1081
|
-
|
|
1082
|
-
|
|
1427
|
+
mkdirSync5(dirname5(path), { recursive: true });
|
|
1428
|
+
appendFileSync2(path, JSON.stringify(record) + "\n", "utf-8");
|
|
1083
1429
|
wroteSomething = true;
|
|
1084
1430
|
} catch {
|
|
1085
1431
|
}
|
|
@@ -1122,9 +1468,9 @@ var init_hook_failure_signal = __esm({
|
|
|
1122
1468
|
});
|
|
1123
1469
|
|
|
1124
1470
|
// src/memory-embedder-tokenizer.ts
|
|
1125
|
-
import { readFileSync as
|
|
1471
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
1126
1472
|
function loadVocab(vocabPath) {
|
|
1127
|
-
const lines =
|
|
1473
|
+
const lines = readFileSync4(vocabPath, "utf-8").split("\n");
|
|
1128
1474
|
const vocab = /* @__PURE__ */ new Map();
|
|
1129
1475
|
for (let i = 0; i < lines.length; i++) {
|
|
1130
1476
|
const tok = lines[i].replace(/\r$/, "");
|
|
@@ -1244,9 +1590,9 @@ var init_memory_embedder_tokenizer = __esm({
|
|
|
1244
1590
|
|
|
1245
1591
|
// src/memory-embedder.ts
|
|
1246
1592
|
import { fileURLToPath } from "url";
|
|
1247
|
-
import { dirname as
|
|
1248
|
-
import { existsSync as
|
|
1249
|
-
import { createRequire } from "module";
|
|
1593
|
+
import { dirname as dirname6, join as join4 } from "path";
|
|
1594
|
+
import { existsSync as existsSync6 } from "fs";
|
|
1595
|
+
import { createRequire as createRequire2 } from "module";
|
|
1250
1596
|
function getActiveEmbedModel() {
|
|
1251
1597
|
return _activeModel;
|
|
1252
1598
|
}
|
|
@@ -1267,21 +1613,21 @@ function embeddingsDisabled() {
|
|
|
1267
1613
|
}
|
|
1268
1614
|
function resolveModelDir() {
|
|
1269
1615
|
try {
|
|
1270
|
-
let dir =
|
|
1616
|
+
let dir = dirname6(fileURLToPath(import.meta.url));
|
|
1271
1617
|
let root = null;
|
|
1272
1618
|
for (let i = 0; i < 8; i++) {
|
|
1273
|
-
if (
|
|
1619
|
+
if (existsSync6(join4(dir, "package.json"))) {
|
|
1274
1620
|
root = dir;
|
|
1275
1621
|
break;
|
|
1276
1622
|
}
|
|
1277
|
-
const parent =
|
|
1623
|
+
const parent = dirname6(dir);
|
|
1278
1624
|
if (parent === dir) break;
|
|
1279
1625
|
dir = parent;
|
|
1280
1626
|
}
|
|
1281
1627
|
if (!root) return null;
|
|
1282
1628
|
for (const rel of ["dist/embedder", "assets/embedder"]) {
|
|
1283
|
-
const candidate =
|
|
1284
|
-
if (
|
|
1629
|
+
const candidate = join4(root, rel);
|
|
1630
|
+
if (existsSync6(join4(candidate, "model_quantized.onnx"))) return candidate;
|
|
1285
1631
|
}
|
|
1286
1632
|
return null;
|
|
1287
1633
|
} catch {
|
|
@@ -1290,9 +1636,9 @@ function resolveModelDir() {
|
|
|
1290
1636
|
}
|
|
1291
1637
|
function resolveWasmDir() {
|
|
1292
1638
|
try {
|
|
1293
|
-
const
|
|
1294
|
-
const main2 =
|
|
1295
|
-
return
|
|
1639
|
+
const req2 = createRequire2(import.meta.url);
|
|
1640
|
+
const main2 = req2.resolve(ORT_PKG);
|
|
1641
|
+
return dirname6(main2);
|
|
1296
1642
|
} catch {
|
|
1297
1643
|
return null;
|
|
1298
1644
|
}
|
|
@@ -1312,8 +1658,8 @@ async function getSession() {
|
|
|
1312
1658
|
ort.env.wasm.proxy = false;
|
|
1313
1659
|
ort.env.wasm.wasmPaths = wasmDir.endsWith("/") ? wasmDir : wasmDir + "/";
|
|
1314
1660
|
_ortTensor = ort.Tensor;
|
|
1315
|
-
const modelPath =
|
|
1316
|
-
const vocabPath =
|
|
1661
|
+
const modelPath = join4(modelDir, "model_quantized.onnx");
|
|
1662
|
+
const vocabPath = join4(modelDir, "vocab.txt");
|
|
1317
1663
|
_vocab = loadVocab(vocabPath);
|
|
1318
1664
|
const session = await ort.InferenceSession.create(modelPath, {
|
|
1319
1665
|
executionProviders: ["wasm"],
|
|
@@ -1650,9 +1996,8 @@ __export(memory_db_exports, {
|
|
|
1650
1996
|
upsertObservationEmbedding: () => upsertObservationEmbedding,
|
|
1651
1997
|
usageWarmupElapsed: () => usageWarmupElapsed
|
|
1652
1998
|
});
|
|
1653
|
-
import
|
|
1654
|
-
import {
|
|
1655
|
-
import { existsSync as existsSync6, mkdirSync as mkdirSync4 } from "fs";
|
|
1999
|
+
import { dirname as dirname7, basename as basename2 } from "path";
|
|
2000
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync6 } from "fs";
|
|
1656
2001
|
function sanitizeFts5Query(raw) {
|
|
1657
2002
|
const trimmed = raw.trim();
|
|
1658
2003
|
if (!trimmed) return '""';
|
|
@@ -1668,12 +2013,12 @@ function sanitizeFts5QueryOr(raw) {
|
|
|
1668
2013
|
}
|
|
1669
2014
|
function getMemoryDb() {
|
|
1670
2015
|
const dbPath = getResolvedPaths().memoryDbPath;
|
|
1671
|
-
const dir =
|
|
1672
|
-
if (!
|
|
1673
|
-
|
|
2016
|
+
const dir = dirname7(dbPath);
|
|
2017
|
+
if (!existsSync7(dir)) {
|
|
2018
|
+
mkdirSync6(dir, { recursive: true });
|
|
1674
2019
|
}
|
|
1675
|
-
const preExisting =
|
|
1676
|
-
const db =
|
|
2020
|
+
const preExisting = existsSync7(dbPath);
|
|
2021
|
+
const db = openDatabase(dbPath);
|
|
1677
2022
|
db.pragma("journal_mode = WAL");
|
|
1678
2023
|
db.pragma("foreign_keys = ON");
|
|
1679
2024
|
const onDisk = db.pragma("user_version", { simple: true });
|
|
@@ -3379,6 +3724,7 @@ var MEMORY_SCHEMA_VERSION, TOOL_COST_EVENTS_RETENTION_DAYS, MAX_DRAFT_TEXT_LEN,
|
|
|
3379
3724
|
var init_memory_db = __esm({
|
|
3380
3725
|
"src/memory-db.ts"() {
|
|
3381
3726
|
"use strict";
|
|
3727
|
+
init_sqlite_loader();
|
|
3382
3728
|
init_config();
|
|
3383
3729
|
init_memory_vector();
|
|
3384
3730
|
init_db_backup();
|