@massu/core 1.16.2 → 2.0.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.
- package/CHANGELOG.md +158 -0
- package/dist/cli.js +2266 -863
- package/dist/hooks/auto-learning-pipeline.js +622 -60
- package/dist/hooks/classify-failure.js +622 -60
- package/dist/hooks/cost-tracker.js +631 -69
- package/dist/hooks/fix-detector.js +631 -69
- package/dist/hooks/incident-pipeline.js +616 -54
- package/dist/hooks/intent-suggester.js +631 -69
- package/dist/hooks/memory-recall.js +771 -75
- package/dist/hooks/post-edit-context.js +633 -71
- package/dist/hooks/post-tool-use.js +659 -197
- package/dist/hooks/pre-compact.js +631 -69
- package/dist/hooks/pre-delete-check.js +635 -73
- package/dist/hooks/pre-tool-use-gate.js +635 -73
- package/dist/hooks/quality-event.js +631 -69
- package/dist/hooks/rule-enforcement-pipeline.js +617 -55
- package/dist/hooks/security-gate.js +631 -69
- package/dist/hooks/session-end.js +1729 -281
- package/dist/hooks/session-start.js +1018 -425
- package/dist/hooks/user-prompt.js +666 -104
- package/package.json +2 -2
|
@@ -243,6 +243,32 @@ var init_config_memory_schema = __esm({
|
|
|
243
243
|
// turn of EVERY session, so an unbounded index is a permanent context tax — and the
|
|
244
244
|
// per-session cap bounds only the RATE, never the total.
|
|
245
245
|
indexMaxLines: z.number().int().min(1).default(50)
|
|
246
|
+
}).default({}),
|
|
247
|
+
// --- Cross-repo memory surfacing (plan-living-memory-slice-5-cross-repo-surfacing) ---
|
|
248
|
+
// A decision made in one of your repos can surface in another — opt-in per
|
|
249
|
+
// decision AND opt-in per repo, signed, verified, and materialized ONLY on
|
|
250
|
+
// explicit human acceptance. Local transport is FREE and zero-network.
|
|
251
|
+
//
|
|
252
|
+
// ⛔ BOTH switches default OFF, and OFF means NOTHING EXISTS — no registry, no
|
|
253
|
+
// keys, no inbox, no behavioural difference. Two INDEPENDENT opt-ins:
|
|
254
|
+
// • enabled — may this repo EXPORT its shareable decisions? (opt-in #1)
|
|
255
|
+
// • subscribe — which repo LABELS may this repo IMPORT from? (opt-in #2)
|
|
256
|
+
// `subscribe: []` means import NOTHING. There is deliberately NO `subscribe: all`
|
|
257
|
+
// — a repo you never named is a repo Massu never reads from.
|
|
258
|
+
share: z.object({
|
|
259
|
+
enabled: z.boolean().default(false),
|
|
260
|
+
subscribe: z.array(z.string()).default([]),
|
|
261
|
+
// C-04 — recall surfacing of cross-repo memories. `enabled` defaults true but is
|
|
262
|
+
// CONDITIONAL on `subscribe` being non-empty (empty by default), so the effective
|
|
263
|
+
// default is DORMANT: with `subscribe: []` the recall hook output is byte-identical
|
|
264
|
+
// to today's. `maxCrossRepoItems` caps how many accepted cross-repo items may appear
|
|
265
|
+
// per recall block (default 1); `minScore` is an OPTIONAL strictly-higher floor for
|
|
266
|
+
// cross-repo items (they are, by construction, less relevant than local ones).
|
|
267
|
+
recall: z.object({
|
|
268
|
+
enabled: z.boolean().default(true),
|
|
269
|
+
maxCrossRepoItems: z.number().int().min(0).default(1),
|
|
270
|
+
minScore: z.number().optional()
|
|
271
|
+
}).default({})
|
|
246
272
|
}).default({})
|
|
247
273
|
}).optional();
|
|
248
274
|
}
|
|
@@ -372,9 +398,13 @@ Hint: run \`massu config refresh\` to regenerate a valid config or fix the liste
|
|
|
372
398
|
const resolvedEndpoint = resolveEndpoint({ configEndpoint: _config.cloud?.endpoint });
|
|
373
399
|
if (resolvedKey.apiKey) {
|
|
374
400
|
_config.cloud = {
|
|
375
|
-
enabled: true,
|
|
376
401
|
sync: { memory: true, analytics: true, audit: true },
|
|
377
402
|
..._config.cloud,
|
|
403
|
+
// Spread FIRST, then decide `enabled`, so a workspace that declares a `cloud:`
|
|
404
|
+
// block only to tune `requestTimeoutMs` does not disable its own sync. An
|
|
405
|
+
// EXPLICIT `enabled: false` is still honoured; `undefined` (not stated) means
|
|
406
|
+
// "a key resolved, so turn it on". (plan-2026-07-20-cloud-sync-timeout)
|
|
407
|
+
enabled: _config.cloud?.enabled ?? true,
|
|
378
408
|
apiKey: resolvedKey.apiKey,
|
|
379
409
|
endpoint: resolvedEndpoint
|
|
380
410
|
};
|
|
@@ -492,7 +522,6 @@ var init_config = __esm({
|
|
|
492
522
|
custom_patterns: z2.array(CustomPatternSchema).default([])
|
|
493
523
|
}).optional(),
|
|
494
524
|
adr: z2.object({
|
|
495
|
-
detection_phrases: z2.array(z2.string()).default(["chose", "decided", "switching to", "moving from", "going with"]),
|
|
496
525
|
template: z2.string().default("default"),
|
|
497
526
|
storage: z2.string().default("database"),
|
|
498
527
|
output_dir: z2.string().default("docs/adr")
|
|
@@ -594,9 +623,22 @@ var init_config = __esm({
|
|
|
594
623
|
})).default([])
|
|
595
624
|
}).optional();
|
|
596
625
|
CloudConfigSchema = z2.object({
|
|
597
|
-
|
|
626
|
+
// OPTIONAL, not `.default(false)`. A zod default is indistinguishable from an
|
|
627
|
+
// explicit value once parsed, so `.default(false)` meant that merely declaring a
|
|
628
|
+
// `cloud:` block (to set any OTHER key) silently produced `enabled: false`, which
|
|
629
|
+
// then overrode the `enabled: true` auto-enable below via object spread — turning
|
|
630
|
+
// cloud sync OFF as a side effect of tuning it. `undefined` now means "not stated",
|
|
631
|
+
// and the auto-enable preserves it. (plan-2026-07-20-cloud-sync-timeout)
|
|
632
|
+
enabled: z2.boolean().optional(),
|
|
598
633
|
apiKey: z2.string().optional(),
|
|
599
634
|
endpoint: z2.string().optional(),
|
|
635
|
+
// Per-request POST budget for the `/sync` ingest. cloud-sync.ts reads this
|
|
636
|
+
// (`(cloud as { requestTimeoutMs?: number }).requestTimeoutMs`) but it was ABSENT
|
|
637
|
+
// from this schema, and zod strips unknown keys — so the knob was unreachable from
|
|
638
|
+
// massu.config.yaml and the default could never be tuned. Measured 2026-07-20: a
|
|
639
|
+
// 423-observation payload takes ~9.2s against the live ingest. Capped at
|
|
640
|
+
// SYNC_DEADLINE_MS (20s) since the overall deadline clamps each attempt anyway.
|
|
641
|
+
requestTimeoutMs: z2.number().int().positive().max(2e4).optional(),
|
|
600
642
|
sync: z2.object({
|
|
601
643
|
memory: z2.boolean().default(true),
|
|
602
644
|
analytics: z2.boolean().default(true),
|
|
@@ -813,6 +855,465 @@ var init_config = __esm({
|
|
|
813
855
|
}
|
|
814
856
|
});
|
|
815
857
|
|
|
858
|
+
// src/lib/fileLock.ts
|
|
859
|
+
import { mkdirSync as mkdirSync2, readFileSync as readFileSync3, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
860
|
+
import { dirname as dirname2 } from "path";
|
|
861
|
+
import * as lockfile from "proper-lockfile";
|
|
862
|
+
function readLockHolderPid(lockPath) {
|
|
863
|
+
try {
|
|
864
|
+
const raw = readFileSync3(`${lockPath}.pid`, "utf-8").trim();
|
|
865
|
+
const pid = Number.parseInt(raw, 10);
|
|
866
|
+
if (!Number.isFinite(pid) || pid <= 0) return null;
|
|
867
|
+
return pid;
|
|
868
|
+
} catch {
|
|
869
|
+
return null;
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
function busyWaitSync(ms) {
|
|
873
|
+
if (typeof SharedArrayBuffer === "undefined" || typeof Atomics === "undefined") {
|
|
874
|
+
throw new Error(
|
|
875
|
+
`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.`
|
|
876
|
+
);
|
|
877
|
+
}
|
|
878
|
+
const sab = new SharedArrayBuffer(4);
|
|
879
|
+
const view = new Int32Array(sab);
|
|
880
|
+
Atomics.wait(view, 0, 0, ms);
|
|
881
|
+
}
|
|
882
|
+
function withFileLockSync(lockPath, fn, opts = {}) {
|
|
883
|
+
mkdirSync2(dirname2(lockPath), { recursive: true });
|
|
884
|
+
const staleMs = opts.staleMs ?? 3e4;
|
|
885
|
+
const blockMs = opts.retries === 0 ? 0 : opts.blockMs ?? 3e4;
|
|
886
|
+
const pollIntervalMs = opts.pollIntervalMs ?? 100;
|
|
887
|
+
const now = opts.now ?? Date.now;
|
|
888
|
+
const sleep = opts.sleep ?? busyWaitSync;
|
|
889
|
+
const makeBusyError = opts.errorFactory ?? ((path, pid, retrySeconds, code) => new FileLockBusyError(path, pid, retrySeconds, code));
|
|
890
|
+
let release = null;
|
|
891
|
+
const deadline = now() + blockMs;
|
|
892
|
+
for (; ; ) {
|
|
893
|
+
try {
|
|
894
|
+
release = lockfile.lockSync(lockPath, {
|
|
895
|
+
stale: staleMs,
|
|
896
|
+
retries: 0,
|
|
897
|
+
realpath: false
|
|
898
|
+
});
|
|
899
|
+
try {
|
|
900
|
+
writeFileSync2(`${lockPath}.pid`, String(process.pid), "utf-8");
|
|
901
|
+
} catch {
|
|
902
|
+
}
|
|
903
|
+
break;
|
|
904
|
+
} catch (err) {
|
|
905
|
+
const e = err;
|
|
906
|
+
const code = e.code;
|
|
907
|
+
if (code !== "ELOCKED" && code !== "EBUSY") {
|
|
908
|
+
throw err;
|
|
909
|
+
}
|
|
910
|
+
if (now() >= deadline) {
|
|
911
|
+
const holderPid = readLockHolderPid(lockPath);
|
|
912
|
+
const remainingMs = Math.max(0, deadline - now());
|
|
913
|
+
const retryAfterSeconds = blockMs === 0 ? Math.round(staleMs / 1e3) : Math.round(remainingMs / 1e3);
|
|
914
|
+
throw makeBusyError(lockPath, holderPid, retryAfterSeconds, code);
|
|
915
|
+
}
|
|
916
|
+
sleep(pollIntervalMs);
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
try {
|
|
920
|
+
return fn();
|
|
921
|
+
} finally {
|
|
922
|
+
try {
|
|
923
|
+
if (release) release();
|
|
924
|
+
} catch {
|
|
925
|
+
}
|
|
926
|
+
try {
|
|
927
|
+
rmSync2(`${lockPath}.pid`, { force: true });
|
|
928
|
+
} catch {
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
var FileLockBusyError;
|
|
933
|
+
var init_fileLock = __esm({
|
|
934
|
+
"src/lib/fileLock.ts"() {
|
|
935
|
+
"use strict";
|
|
936
|
+
FileLockBusyError = class extends Error {
|
|
937
|
+
constructor(lockPath, holderPid, retryAfterSeconds, causeCode) {
|
|
938
|
+
const pidPart = holderPid != null ? `(PID=${holderPid})` : "(PID=unknown)";
|
|
939
|
+
super(`File lock at ${lockPath} held by another process ${pidPart} \u2014 try again in ${retryAfterSeconds}s`);
|
|
940
|
+
this.lockPath = lockPath;
|
|
941
|
+
this.holderPid = holderPid;
|
|
942
|
+
this.retryAfterSeconds = retryAfterSeconds;
|
|
943
|
+
this.causeCode = causeCode;
|
|
944
|
+
this.name = "FileLockBusyError";
|
|
945
|
+
}
|
|
946
|
+
lockPath;
|
|
947
|
+
holderPid;
|
|
948
|
+
retryAfterSeconds;
|
|
949
|
+
causeCode;
|
|
950
|
+
};
|
|
951
|
+
}
|
|
952
|
+
});
|
|
953
|
+
|
|
954
|
+
// src/lib/sqlite-loader.ts
|
|
955
|
+
import { createRequire } from "module";
|
|
956
|
+
import { spawnSync } from "child_process";
|
|
957
|
+
import { accessSync, appendFileSync, chmodSync as chmodSync2, constants as fsConstants, existsSync as existsSync3, mkdirSync as mkdirSync3 } from "fs";
|
|
958
|
+
import { dirname as dirname3, join } from "path";
|
|
959
|
+
function isNativeAbiError(err) {
|
|
960
|
+
const code = err?.code;
|
|
961
|
+
if (code === "ERR_DLOPEN_FAILED") return true;
|
|
962
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
963
|
+
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);
|
|
964
|
+
}
|
|
965
|
+
function loadBetterSqlite3(opts = {}) {
|
|
966
|
+
if (_testCtor) return _testCtor;
|
|
967
|
+
if (opts.fresh) bustNativeCache();
|
|
968
|
+
return req("better-sqlite3");
|
|
969
|
+
}
|
|
970
|
+
function bustNativeCache() {
|
|
971
|
+
try {
|
|
972
|
+
const cache = req.cache;
|
|
973
|
+
if (!cache) return;
|
|
974
|
+
for (const key of Object.keys(cache)) {
|
|
975
|
+
if (/better[-_]sqlite3|better_sqlite3\.node|[\\/]bindings[\\/]/.test(key)) {
|
|
976
|
+
delete cache[key];
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
} catch {
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
function openDatabase(dbPath, opts = {}) {
|
|
983
|
+
const { selfHeal: selfHealOpt = true, ...ctorOpts } = opts;
|
|
984
|
+
const selfHeal = selfHealOpt && process.env.MASSU_HOOK_RUNTIME !== "1";
|
|
985
|
+
const Ctor = loadBetterSqlite3();
|
|
986
|
+
try {
|
|
987
|
+
return new Ctor(dbPath, ctorOpts);
|
|
988
|
+
} catch (err) {
|
|
989
|
+
if (!isNativeAbiError(err)) {
|
|
990
|
+
throw err;
|
|
991
|
+
}
|
|
992
|
+
if (!selfHeal) {
|
|
993
|
+
recordHealEvent({ phase: "skipped", reason: "self-heal-disabled", abiTo: process.versions.modules });
|
|
994
|
+
throw new MemoryEngineUnusableError("abi-mismatch", detailOf(err));
|
|
995
|
+
}
|
|
996
|
+
const result = (_testHeal ?? attemptNativeHeal)(err);
|
|
997
|
+
if (!result.healed && !result.contended) {
|
|
998
|
+
throw new MemoryEngineUnusableError(result.reason ?? "heal-failed", result.detail ?? detailOf(err));
|
|
999
|
+
}
|
|
1000
|
+
try {
|
|
1001
|
+
const FreshCtor = loadBetterSqlite3({ fresh: true });
|
|
1002
|
+
return new FreshCtor(dbPath, ctorOpts);
|
|
1003
|
+
} catch (retryErr) {
|
|
1004
|
+
throw new MemoryEngineUnusableError("heal-failed", detailOf(retryErr));
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
function attemptNativeHeal(err) {
|
|
1009
|
+
const start = Date.now();
|
|
1010
|
+
const abiTo = process.versions.modules;
|
|
1011
|
+
const abiFrom = parseAbiFrom(err);
|
|
1012
|
+
let pkgDir;
|
|
1013
|
+
try {
|
|
1014
|
+
pkgDir = dirname3(req.resolve("better-sqlite3/package.json"));
|
|
1015
|
+
} catch {
|
|
1016
|
+
recordHealEvent({ phase: "skipped", reason: "not-resolvable", abiFrom, abiTo });
|
|
1017
|
+
return { healed: false, reason: "missing", abiFrom, abiTo, detail: "better-sqlite3 not resolvable" };
|
|
1018
|
+
}
|
|
1019
|
+
if (!isWritable(pkgDir)) {
|
|
1020
|
+
recordHealEvent({ phase: "skipped", reason: "dir-not-writable", abiFrom, abiTo });
|
|
1021
|
+
return { healed: false, reason: "heal-failed", abiFrom, abiTo, detail: `install dir not writable: ${pkgDir}` };
|
|
1022
|
+
}
|
|
1023
|
+
recordHealEvent({ phase: "attempt", reason: "abi-mismatch", abiFrom, abiTo });
|
|
1024
|
+
try {
|
|
1025
|
+
return withFileLockSync(
|
|
1026
|
+
join(credentialsDir(), "native-heal.lock"),
|
|
1027
|
+
() => runRebuild(pkgDir, abiFrom, abiTo, start),
|
|
1028
|
+
{ blockMs: 6e4, staleMs: 3e5 }
|
|
1029
|
+
);
|
|
1030
|
+
} catch (lockErr) {
|
|
1031
|
+
const detail = lockErr instanceof Error ? lockErr.message : String(lockErr);
|
|
1032
|
+
recordHealEvent({ phase: "failed", reason: "heal-failed", abiFrom, abiTo, detail: `lock: ${detail}` });
|
|
1033
|
+
return { healed: false, contended: true, reason: "heal-failed", abiFrom, abiTo, detail: `heal lock contended: ${detail}` };
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
function runRebuild(pkgDir, abiFrom, abiTo, start) {
|
|
1037
|
+
try {
|
|
1038
|
+
const FreshCtor = loadBetterSqlite3({ fresh: true });
|
|
1039
|
+
new FreshCtor(":memory:").close();
|
|
1040
|
+
const res2 = {
|
|
1041
|
+
healed: true,
|
|
1042
|
+
abiFrom,
|
|
1043
|
+
abiTo,
|
|
1044
|
+
durationMs: Date.now() - start,
|
|
1045
|
+
detail: "already healed by a concurrent process"
|
|
1046
|
+
};
|
|
1047
|
+
recordHealEvent({ phase: "success", reason: "already-healed", abiFrom, abiTo, durationMs: res2.durationMs });
|
|
1048
|
+
return res2;
|
|
1049
|
+
} catch (probeErr) {
|
|
1050
|
+
if (!isNativeAbiError(probeErr)) {
|
|
1051
|
+
return { healed: false, reason: "heal-failed", abiFrom, abiTo, durationMs: Date.now() - start, detail: detailOf(probeErr) };
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
const prebuildBin = resolveLocalBin(pkgDir, "prebuild-install");
|
|
1055
|
+
if (prebuildBin) {
|
|
1056
|
+
const r = spawnSync(process.execPath, [prebuildBin], {
|
|
1057
|
+
cwd: pkgDir,
|
|
1058
|
+
encoding: "utf-8",
|
|
1059
|
+
timeout: 12e4
|
|
1060
|
+
});
|
|
1061
|
+
if (r.status === 0) {
|
|
1062
|
+
const res2 = { healed: true, method: "prebuild-install", abiFrom, abiTo, durationMs: Date.now() - start };
|
|
1063
|
+
recordHealEvent({ phase: "success", reason: "abi-mismatch", ...res2 });
|
|
1064
|
+
return res2;
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
const gypBin = resolveNodeGypBin();
|
|
1068
|
+
if (gypBin && hasCompiler()) {
|
|
1069
|
+
const r = spawnSync(process.execPath, [gypBin, "rebuild", "--release"], {
|
|
1070
|
+
cwd: pkgDir,
|
|
1071
|
+
encoding: "utf-8",
|
|
1072
|
+
timeout: 3e5
|
|
1073
|
+
});
|
|
1074
|
+
if (r.status === 0) {
|
|
1075
|
+
const res2 = { healed: true, method: "node-gyp", abiFrom, abiTo, durationMs: Date.now() - start };
|
|
1076
|
+
recordHealEvent({ phase: "success", reason: "abi-mismatch", ...res2 });
|
|
1077
|
+
return res2;
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
const res = {
|
|
1081
|
+
healed: false,
|
|
1082
|
+
reason: "heal-failed",
|
|
1083
|
+
abiFrom,
|
|
1084
|
+
abiTo,
|
|
1085
|
+
durationMs: Date.now() - start,
|
|
1086
|
+
detail: prebuildBin ? "prebuild-install failed; node-gyp fallback unavailable or failed" : "no prebuild-install bin resolvable"
|
|
1087
|
+
};
|
|
1088
|
+
recordHealEvent({ phase: "failed", reason: "heal-failed", abiFrom, abiTo, durationMs: res.durationMs });
|
|
1089
|
+
return res;
|
|
1090
|
+
}
|
|
1091
|
+
function recordHealEvent(event) {
|
|
1092
|
+
try {
|
|
1093
|
+
const rec = {
|
|
1094
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1095
|
+
node: process.versions.node,
|
|
1096
|
+
platform: process.platform,
|
|
1097
|
+
arch: process.arch,
|
|
1098
|
+
...event
|
|
1099
|
+
};
|
|
1100
|
+
const dir = credentialsDir();
|
|
1101
|
+
mkdirSync3(dir, { recursive: true });
|
|
1102
|
+
const file = join(dir, "native-heal-events.jsonl");
|
|
1103
|
+
appendFileSync(file, JSON.stringify(rec) + "\n", "utf-8");
|
|
1104
|
+
try {
|
|
1105
|
+
chmodSync2(file, 384);
|
|
1106
|
+
} catch {
|
|
1107
|
+
}
|
|
1108
|
+
} catch {
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
function parseAbiFrom(err) {
|
|
1112
|
+
const msg = err instanceof Error ? err.message : String(err ?? "");
|
|
1113
|
+
const m = /NODE_MODULE_VERSION (\d+)/.exec(msg);
|
|
1114
|
+
if (m && /^\d+$/.test(m[1])) return m[1];
|
|
1115
|
+
return void 0;
|
|
1116
|
+
}
|
|
1117
|
+
function detailOf(err) {
|
|
1118
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1119
|
+
return msg.slice(0, 300);
|
|
1120
|
+
}
|
|
1121
|
+
function isWritable(dir) {
|
|
1122
|
+
try {
|
|
1123
|
+
accessSync(dir, fsConstants.W_OK);
|
|
1124
|
+
return true;
|
|
1125
|
+
} catch {
|
|
1126
|
+
return false;
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
function resolveLocalBin(pkgDir, name) {
|
|
1130
|
+
const candidates = [
|
|
1131
|
+
join(pkgDir, "node_modules", ".bin", name),
|
|
1132
|
+
// nested install
|
|
1133
|
+
join(pkgDir, "..", ".bin", name),
|
|
1134
|
+
// hoisted (node_modules/.bin)
|
|
1135
|
+
join(pkgDir, "..", "..", "node_modules", ".bin", name)
|
|
1136
|
+
// workspace hoist
|
|
1137
|
+
];
|
|
1138
|
+
for (const c of candidates) {
|
|
1139
|
+
if (existsSync3(c)) return c;
|
|
1140
|
+
}
|
|
1141
|
+
return void 0;
|
|
1142
|
+
}
|
|
1143
|
+
function resolveNodeGypBin() {
|
|
1144
|
+
try {
|
|
1145
|
+
return req.resolve("node-gyp/bin/node-gyp.js");
|
|
1146
|
+
} catch {
|
|
1147
|
+
return void 0;
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
function hasCompiler() {
|
|
1151
|
+
for (const cc of ["cc", "clang", "gcc"]) {
|
|
1152
|
+
try {
|
|
1153
|
+
const r = spawnSync(cc, ["--version"], { encoding: "utf-8", timeout: 5e3 });
|
|
1154
|
+
if (r.status === 0) return true;
|
|
1155
|
+
} catch {
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
return false;
|
|
1159
|
+
}
|
|
1160
|
+
var req, NATIVE_DB_REMEDY, MemoryEngineUnusableError, _testCtor, _testHeal;
|
|
1161
|
+
var init_sqlite_loader = __esm({
|
|
1162
|
+
"src/lib/sqlite-loader.ts"() {
|
|
1163
|
+
"use strict";
|
|
1164
|
+
init_credentials();
|
|
1165
|
+
init_fileLock();
|
|
1166
|
+
req = createRequire(import.meta.url);
|
|
1167
|
+
NATIVE_DB_REMEDY = "Run 'massu heal' to rebuild the database engine for your Node version, then restart your MCP client / Claude Code.";
|
|
1168
|
+
MemoryEngineUnusableError = class extends Error {
|
|
1169
|
+
reason;
|
|
1170
|
+
remedy;
|
|
1171
|
+
detail;
|
|
1172
|
+
constructor(reason, detail) {
|
|
1173
|
+
super(
|
|
1174
|
+
`Massu memory engine is unusable (${reason}). ${NATIVE_DB_REMEDY}` + (detail ? ` [${detail}]` : "")
|
|
1175
|
+
);
|
|
1176
|
+
this.name = "MemoryEngineUnusableError";
|
|
1177
|
+
this.reason = reason;
|
|
1178
|
+
this.remedy = NATIVE_DB_REMEDY;
|
|
1179
|
+
this.detail = detail;
|
|
1180
|
+
}
|
|
1181
|
+
};
|
|
1182
|
+
_testCtor = null;
|
|
1183
|
+
_testHeal = null;
|
|
1184
|
+
}
|
|
1185
|
+
});
|
|
1186
|
+
|
|
1187
|
+
// src/db-driver.ts
|
|
1188
|
+
import { createRequire as createRequire2 } from "module";
|
|
1189
|
+
import { closeSync, existsSync as existsSync4, mkdtempSync, openSync, readFileSync as readFileSync4, rmSync as rmSync3, fsyncSync } from "fs";
|
|
1190
|
+
import { tmpdir } from "os";
|
|
1191
|
+
import { join as join2 } from "path";
|
|
1192
|
+
function resolveDbEngine() {
|
|
1193
|
+
return process.env[DB_ENGINE_ENV] === "better-sqlite3" ? "better-sqlite3" : DEFAULT_DB_ENGINE;
|
|
1194
|
+
}
|
|
1195
|
+
function nodeSqliteCtor() {
|
|
1196
|
+
if (!_nodeCtor) _nodeCtor = req2("node:sqlite").DatabaseSync;
|
|
1197
|
+
return _nodeCtor;
|
|
1198
|
+
}
|
|
1199
|
+
function plainRow(row) {
|
|
1200
|
+
if (row == null || typeof row !== "object") return row;
|
|
1201
|
+
return { ...row };
|
|
1202
|
+
}
|
|
1203
|
+
function openNodeSqlite(dbPath, opts) {
|
|
1204
|
+
const Ctor = nodeSqliteCtor();
|
|
1205
|
+
const raw = new Ctor(dbPath, {
|
|
1206
|
+
open: true,
|
|
1207
|
+
readOnly: !!opts.readonly,
|
|
1208
|
+
// FAITHFULNESS: better-sqlite3's ACTUAL default is foreign_keys=ON (empirically
|
|
1209
|
+
// verified — SQLite's own default is OFF, but bs3 enables it). massu was written +
|
|
1210
|
+
// tested against that default, so the adapter must match it, or a store that relies
|
|
1211
|
+
// on FK-ON-by-default (without an explicit pragma) would silently lose enforcement.
|
|
1212
|
+
// Explicit `foreign_keys=OFF/ON` pragmas (e.g. memory-db bulk ops) still override.
|
|
1213
|
+
enableForeignKeyConstraints: true,
|
|
1214
|
+
allowExtension: false
|
|
1215
|
+
});
|
|
1216
|
+
let savepointSeq = 0;
|
|
1217
|
+
const wrapStmt = (sql) => {
|
|
1218
|
+
const st = raw.prepare(sql);
|
|
1219
|
+
return {
|
|
1220
|
+
run: (...p) => {
|
|
1221
|
+
const r = st.run(...p);
|
|
1222
|
+
return {
|
|
1223
|
+
changes: typeof r.changes === "bigint" ? Number(r.changes) : r.changes,
|
|
1224
|
+
lastInsertRowid: r.lastInsertRowid
|
|
1225
|
+
};
|
|
1226
|
+
},
|
|
1227
|
+
get: (...p) => plainRow(st.get(...p)),
|
|
1228
|
+
all: (...p) => st.all(...p).map(plainRow)
|
|
1229
|
+
};
|
|
1230
|
+
};
|
|
1231
|
+
const pragma = (source, options) => {
|
|
1232
|
+
const s = String(source).trim();
|
|
1233
|
+
if (s.includes("=")) {
|
|
1234
|
+
raw.exec(`PRAGMA ${s}`);
|
|
1235
|
+
return void 0;
|
|
1236
|
+
}
|
|
1237
|
+
const rows = raw.prepare(`PRAGMA ${s}`).all().map(plainRow);
|
|
1238
|
+
if (options?.simple) {
|
|
1239
|
+
const first = rows[0];
|
|
1240
|
+
return first ? Object.values(first)[0] : void 0;
|
|
1241
|
+
}
|
|
1242
|
+
return rows;
|
|
1243
|
+
};
|
|
1244
|
+
const transaction = (fn) => {
|
|
1245
|
+
const run = (...args) => {
|
|
1246
|
+
const nested = raw.isTransaction;
|
|
1247
|
+
const name = `msp_${savepointSeq++}`;
|
|
1248
|
+
if (nested) raw.exec(`SAVEPOINT ${name}`);
|
|
1249
|
+
else raw.exec("BEGIN");
|
|
1250
|
+
try {
|
|
1251
|
+
const out = fn(...args);
|
|
1252
|
+
if (nested) raw.exec(`RELEASE ${name}`);
|
|
1253
|
+
else raw.exec("COMMIT");
|
|
1254
|
+
return out;
|
|
1255
|
+
} catch (e) {
|
|
1256
|
+
if (nested) {
|
|
1257
|
+
raw.exec(`ROLLBACK TO ${name}`);
|
|
1258
|
+
raw.exec(`RELEASE ${name}`);
|
|
1259
|
+
} else {
|
|
1260
|
+
raw.exec("ROLLBACK");
|
|
1261
|
+
}
|
|
1262
|
+
throw e;
|
|
1263
|
+
}
|
|
1264
|
+
};
|
|
1265
|
+
return run;
|
|
1266
|
+
};
|
|
1267
|
+
const serialize = () => {
|
|
1268
|
+
if (raw.isTransaction) {
|
|
1269
|
+
throw new Error(
|
|
1270
|
+
"db-driver: .serialize() (node:sqlite engine) cannot run inside an open transaction \u2014 VACUUM is forbidden in a transaction. Serialize before BEGIN, or use MASSU_DB_ENGINE=better-sqlite3 for mid-transaction snapshots."
|
|
1271
|
+
);
|
|
1272
|
+
}
|
|
1273
|
+
const dir = mkdtempSync(join2(tmpdir(), "massu-serialize-"));
|
|
1274
|
+
const out = join2(dir, "snapshot.db");
|
|
1275
|
+
try {
|
|
1276
|
+
raw.exec(`VACUUM INTO '${out.replace(/'/g, "''")}'`);
|
|
1277
|
+
const fd = openSync(out, "r");
|
|
1278
|
+
try {
|
|
1279
|
+
fsyncSync(fd);
|
|
1280
|
+
} finally {
|
|
1281
|
+
closeSync(fd);
|
|
1282
|
+
}
|
|
1283
|
+
return readFileSync4(out);
|
|
1284
|
+
} finally {
|
|
1285
|
+
rmSync3(dir, { recursive: true, force: true });
|
|
1286
|
+
}
|
|
1287
|
+
};
|
|
1288
|
+
const handle = {
|
|
1289
|
+
prepare: (sql) => wrapStmt(sql),
|
|
1290
|
+
exec: (sql) => raw.exec(sql),
|
|
1291
|
+
pragma,
|
|
1292
|
+
transaction,
|
|
1293
|
+
serialize,
|
|
1294
|
+
close: () => raw.close()
|
|
1295
|
+
};
|
|
1296
|
+
return handle;
|
|
1297
|
+
}
|
|
1298
|
+
function openBetterSqlite3(dbPath, opts) {
|
|
1299
|
+
return openDatabase(dbPath, opts);
|
|
1300
|
+
}
|
|
1301
|
+
function openDatabase2(dbPath, opts = {}) {
|
|
1302
|
+
return resolveDbEngine() === "better-sqlite3" ? openBetterSqlite3(dbPath, opts) : openNodeSqlite(dbPath, opts);
|
|
1303
|
+
}
|
|
1304
|
+
var req2, DEFAULT_DB_ENGINE, DB_ENGINE_ENV, _nodeCtor;
|
|
1305
|
+
var init_db_driver = __esm({
|
|
1306
|
+
"src/db-driver.ts"() {
|
|
1307
|
+
"use strict";
|
|
1308
|
+
init_sqlite_loader();
|
|
1309
|
+
init_sqlite_loader();
|
|
1310
|
+
req2 = createRequire2(import.meta.url);
|
|
1311
|
+
DEFAULT_DB_ENGINE = "node-sqlite";
|
|
1312
|
+
DB_ENGINE_ENV = "MASSU_DB_ENGINE";
|
|
1313
|
+
_nodeCtor = null;
|
|
1314
|
+
}
|
|
1315
|
+
});
|
|
1316
|
+
|
|
816
1317
|
// src/memory-vector.ts
|
|
817
1318
|
function float32ToBlob(vec) {
|
|
818
1319
|
return Buffer.from(new Uint8Array(vec.buffer, vec.byteOffset, vec.byteLength));
|
|
@@ -833,27 +1334,26 @@ var init_memory_vector = __esm({
|
|
|
833
1334
|
});
|
|
834
1335
|
|
|
835
1336
|
// src/db-backup.ts
|
|
836
|
-
import
|
|
837
|
-
import {
|
|
838
|
-
import { resolve as resolve3, join, basename, dirname as dirname2 } from "path";
|
|
1337
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync4, readdirSync, statSync, unlinkSync, copyFileSync, rmSync as rmSync4 } from "fs";
|
|
1338
|
+
import { resolve as resolve3, join as join3, basename, dirname as dirname4 } from "path";
|
|
839
1339
|
import { homedir as homedir3 } from "os";
|
|
840
1340
|
function dbBackupsRoot(home = homedir3()) {
|
|
841
1341
|
return resolve3(home, ".massu", "db-backups");
|
|
842
1342
|
}
|
|
843
1343
|
function projectBackupDir(projectRoot, home = homedir3()) {
|
|
844
1344
|
const slug = basename(projectRoot).replace(/[^A-Za-z0-9._-]/g, "_") || "project";
|
|
845
|
-
return
|
|
1345
|
+
return join3(dbBackupsRoot(home), slug);
|
|
846
1346
|
}
|
|
847
1347
|
function backupStamp(nowMs) {
|
|
848
1348
|
return new Date(nowMs).toISOString().replace(/[:.]/g, "-");
|
|
849
1349
|
}
|
|
850
1350
|
function listDbBackups(projectRoot, home = homedir3()) {
|
|
851
1351
|
const dir = projectBackupDir(projectRoot, home);
|
|
852
|
-
if (!
|
|
1352
|
+
if (!existsSync5(dir)) return [];
|
|
853
1353
|
const out = [];
|
|
854
1354
|
for (const f of readdirSync(dir)) {
|
|
855
1355
|
if (!f.endsWith(".db")) continue;
|
|
856
|
-
const p =
|
|
1356
|
+
const p = join3(dir, f);
|
|
857
1357
|
const st = statSync(p);
|
|
858
1358
|
const db = f.replace(/-\d{4}-\d{2}-\d{2}T.*$/, "");
|
|
859
1359
|
out.push({ db, path: p, bytes: st.size, mtimeMs: st.mtimeMs });
|
|
@@ -866,13 +1366,13 @@ function hasFreshDbBackup(projectRoot, dbPath, nowMs = Date.now(), home = homedi
|
|
|
866
1366
|
if (backups.length === 0) return false;
|
|
867
1367
|
const newest = backups[0];
|
|
868
1368
|
if (nowMs - newest.mtimeMs > FRESH_WINDOW_MS) return false;
|
|
869
|
-
if (
|
|
1369
|
+
if (existsSync5(dbPath) && statSync(dbPath).mtimeMs > newest.mtimeMs) return false;
|
|
870
1370
|
return true;
|
|
871
1371
|
}
|
|
872
1372
|
function integrityOk(dbPath) {
|
|
873
1373
|
let db = null;
|
|
874
1374
|
try {
|
|
875
|
-
db =
|
|
1375
|
+
db = openDatabase2(dbPath, { readonly: true });
|
|
876
1376
|
const rows = db.pragma("integrity_check");
|
|
877
1377
|
return rows.length === 1 && rows[0].integrity_check === "ok";
|
|
878
1378
|
} catch {
|
|
@@ -885,16 +1385,16 @@ function integrityOk(dbPath) {
|
|
|
885
1385
|
}
|
|
886
1386
|
}
|
|
887
1387
|
function backupDb(projectRoot, dbPath, nowMs = Date.now(), home = homedir3()) {
|
|
888
|
-
if (!
|
|
1388
|
+
if (!existsSync5(dbPath)) {
|
|
889
1389
|
throw new DbBackupError(`cannot back up a database that does not exist: ${dbPath}`);
|
|
890
1390
|
}
|
|
891
1391
|
const dir = projectBackupDir(projectRoot, home);
|
|
892
|
-
|
|
1392
|
+
mkdirSync4(dir, { recursive: true });
|
|
893
1393
|
const name = basename(dbPath, ".db");
|
|
894
|
-
const dest =
|
|
1394
|
+
const dest = join3(dir, `${name}-${backupStamp(nowMs)}.db`);
|
|
895
1395
|
let src = null;
|
|
896
1396
|
try {
|
|
897
|
-
src =
|
|
1397
|
+
src = openDatabase2(dbPath, { readonly: true });
|
|
898
1398
|
src.exec(`VACUUM INTO '${dest.replace(/'/g, "''")}'`);
|
|
899
1399
|
} catch (err) {
|
|
900
1400
|
throw new DbBackupError(
|
|
@@ -933,7 +1433,7 @@ function pruneDbBackups(projectRoot, dbName, keep = DEFAULT_RETENTION, home = ho
|
|
|
933
1433
|
}
|
|
934
1434
|
function backupBeforeSchemaChange(projectRoot, dbPath, onError, nowMs = Date.now(), home = homedir3()) {
|
|
935
1435
|
try {
|
|
936
|
-
if (!
|
|
1436
|
+
if (!existsSync5(dbPath)) return null;
|
|
937
1437
|
if (hasFreshDbBackup(projectRoot, dbPath, nowMs, home)) return null;
|
|
938
1438
|
return backupDb(projectRoot, dbPath, nowMs, home);
|
|
939
1439
|
} catch (err) {
|
|
@@ -945,6 +1445,7 @@ var DEFAULT_RETENTION, FRESH_WINDOW_MS, DbBackupError;
|
|
|
945
1445
|
var init_db_backup = __esm({
|
|
946
1446
|
"src/db-backup.ts"() {
|
|
947
1447
|
"use strict";
|
|
1448
|
+
init_db_driver();
|
|
948
1449
|
DEFAULT_RETENTION = 5;
|
|
949
1450
|
FRESH_WINDOW_MS = 24 * 60 * 60 * 1e3;
|
|
950
1451
|
DbBackupError = class extends Error {
|
|
@@ -1057,9 +1558,9 @@ var init_rule_candidate_store = __esm({
|
|
|
1057
1558
|
});
|
|
1058
1559
|
|
|
1059
1560
|
// src/memory-embedder-tokenizer.ts
|
|
1060
|
-
import { readFileSync as
|
|
1561
|
+
import { readFileSync as readFileSync5 } from "fs";
|
|
1061
1562
|
function loadVocab(vocabPath) {
|
|
1062
|
-
const lines =
|
|
1563
|
+
const lines = readFileSync5(vocabPath, "utf-8").split("\n");
|
|
1063
1564
|
const vocab = /* @__PURE__ */ new Map();
|
|
1064
1565
|
for (let i = 0; i < lines.length; i++) {
|
|
1065
1566
|
const tok = lines[i].replace(/\r$/, "");
|
|
@@ -1179,9 +1680,9 @@ var init_memory_embedder_tokenizer = __esm({
|
|
|
1179
1680
|
|
|
1180
1681
|
// src/memory-embedder.ts
|
|
1181
1682
|
import { fileURLToPath } from "url";
|
|
1182
|
-
import { dirname as
|
|
1183
|
-
import { existsSync as
|
|
1184
|
-
import { createRequire } from "module";
|
|
1683
|
+
import { dirname as dirname5, join as join4 } from "path";
|
|
1684
|
+
import { existsSync as existsSync6 } from "fs";
|
|
1685
|
+
import { createRequire as createRequire3 } from "module";
|
|
1185
1686
|
function getActiveEmbedModel() {
|
|
1186
1687
|
return _activeModel;
|
|
1187
1688
|
}
|
|
@@ -1202,21 +1703,21 @@ function embeddingsDisabled() {
|
|
|
1202
1703
|
}
|
|
1203
1704
|
function resolveModelDir() {
|
|
1204
1705
|
try {
|
|
1205
|
-
let dir =
|
|
1706
|
+
let dir = dirname5(fileURLToPath(import.meta.url));
|
|
1206
1707
|
let root = null;
|
|
1207
1708
|
for (let i = 0; i < 8; i++) {
|
|
1208
|
-
if (
|
|
1709
|
+
if (existsSync6(join4(dir, "package.json"))) {
|
|
1209
1710
|
root = dir;
|
|
1210
1711
|
break;
|
|
1211
1712
|
}
|
|
1212
|
-
const parent =
|
|
1713
|
+
const parent = dirname5(dir);
|
|
1213
1714
|
if (parent === dir) break;
|
|
1214
1715
|
dir = parent;
|
|
1215
1716
|
}
|
|
1216
1717
|
if (!root) return null;
|
|
1217
1718
|
for (const rel of ["dist/embedder", "assets/embedder"]) {
|
|
1218
|
-
const candidate =
|
|
1219
|
-
if (
|
|
1719
|
+
const candidate = join4(root, rel);
|
|
1720
|
+
if (existsSync6(join4(candidate, "model_quantized.onnx"))) return candidate;
|
|
1220
1721
|
}
|
|
1221
1722
|
return null;
|
|
1222
1723
|
} catch {
|
|
@@ -1225,9 +1726,9 @@ function resolveModelDir() {
|
|
|
1225
1726
|
}
|
|
1226
1727
|
function resolveWasmDir() {
|
|
1227
1728
|
try {
|
|
1228
|
-
const
|
|
1229
|
-
const main2 =
|
|
1230
|
-
return
|
|
1729
|
+
const req3 = createRequire3(import.meta.url);
|
|
1730
|
+
const main2 = req3.resolve(ORT_PKG);
|
|
1731
|
+
return dirname5(main2);
|
|
1231
1732
|
} catch {
|
|
1232
1733
|
return null;
|
|
1233
1734
|
}
|
|
@@ -1247,8 +1748,8 @@ async function getSession() {
|
|
|
1247
1748
|
ort.env.wasm.proxy = false;
|
|
1248
1749
|
ort.env.wasm.wasmPaths = wasmDir.endsWith("/") ? wasmDir : wasmDir + "/";
|
|
1249
1750
|
_ortTensor = ort.Tensor;
|
|
1250
|
-
const modelPath =
|
|
1251
|
-
const vocabPath =
|
|
1751
|
+
const modelPath = join4(modelDir, "model_quantized.onnx");
|
|
1752
|
+
const vocabPath = join4(modelDir, "vocab.txt");
|
|
1252
1753
|
_vocab = loadVocab(vocabPath);
|
|
1253
1754
|
const session = await ort.InferenceSession.create(modelPath, {
|
|
1254
1755
|
executionProviders: ["wasm"],
|
|
@@ -1569,6 +2070,7 @@ __export(memory_db_exports, {
|
|
|
1569
2070
|
migrateAuditLogCheckExtension: () => migrateAuditLogCheckExtension,
|
|
1570
2071
|
migrateMemoryFilesFor4B: () => migrateMemoryFilesFor4B,
|
|
1571
2072
|
migrateObservationEmbeddingChunks: () => migrateObservationEmbeddingChunks,
|
|
2073
|
+
migrateSharedMemoryFor5B: () => migrateSharedMemoryFor5B,
|
|
1572
2074
|
pruneOldConversationTurns: () => pruneOldConversationTurns,
|
|
1573
2075
|
pruneOldObservations: () => pruneOldObservations,
|
|
1574
2076
|
pruneToolCostEvents: () => pruneToolCostEvents,
|
|
@@ -1585,9 +2087,8 @@ __export(memory_db_exports, {
|
|
|
1585
2087
|
upsertObservationEmbedding: () => upsertObservationEmbedding,
|
|
1586
2088
|
usageWarmupElapsed: () => usageWarmupElapsed
|
|
1587
2089
|
});
|
|
1588
|
-
import
|
|
1589
|
-
import {
|
|
1590
|
-
import { existsSync as existsSync5, mkdirSync as mkdirSync3 } from "fs";
|
|
2090
|
+
import { dirname as dirname6, basename as basename2 } from "path";
|
|
2091
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync5 } from "fs";
|
|
1591
2092
|
function sanitizeFts5Query(raw) {
|
|
1592
2093
|
const trimmed = raw.trim();
|
|
1593
2094
|
if (!trimmed) return '""';
|
|
@@ -1603,12 +2104,12 @@ function sanitizeFts5QueryOr(raw) {
|
|
|
1603
2104
|
}
|
|
1604
2105
|
function getMemoryDb() {
|
|
1605
2106
|
const dbPath = getResolvedPaths().memoryDbPath;
|
|
1606
|
-
const dir =
|
|
1607
|
-
if (!
|
|
1608
|
-
|
|
2107
|
+
const dir = dirname6(dbPath);
|
|
2108
|
+
if (!existsSync7(dir)) {
|
|
2109
|
+
mkdirSync5(dir, { recursive: true });
|
|
1609
2110
|
}
|
|
1610
|
-
const preExisting =
|
|
1611
|
-
const db =
|
|
2111
|
+
const preExisting = existsSync7(dbPath);
|
|
2112
|
+
const db = openDatabase2(dbPath);
|
|
1612
2113
|
db.pragma("journal_mode = WAL");
|
|
1613
2114
|
db.pragma("foreign_keys = ON");
|
|
1614
2115
|
const onDisk = db.pragma("user_version", { simple: true });
|
|
@@ -1684,6 +2185,39 @@ function migrateMemoryFilesFor4B(db) {
|
|
|
1684
2185
|
db.exec(`ALTER TABLE ${add.table} ADD COLUMN ${add.name} ${add.decl}`);
|
|
1685
2186
|
}
|
|
1686
2187
|
}
|
|
2188
|
+
function migrateSharedMemoryFor5B(db) {
|
|
2189
|
+
const cols = db.prepare(`PRAGMA table_info(observations)`).all();
|
|
2190
|
+
if (cols.length > 0 && !cols.some((c) => c.name === "shareable")) {
|
|
2191
|
+
db.exec(`ALTER TABLE observations ADD COLUMN shareable INTEGER NOT NULL DEFAULT 0`);
|
|
2192
|
+
}
|
|
2193
|
+
db.exec(`
|
|
2194
|
+
CREATE TABLE IF NOT EXISTS shared_memory_pending (
|
|
2195
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
2196
|
+
record_hash TEXT NOT NULL UNIQUE, -- identity + idempotency key (hex sha256)
|
|
2197
|
+
origin_repo_id TEXT NOT NULL, -- the signed origin repo_id (v4 UUID)
|
|
2198
|
+
origin_repo_label TEXT NOT NULL, -- re-slugged on read; NEVER trusted from the wire
|
|
2199
|
+
envelope_raw TEXT NOT NULL, -- the VERBATIM signed bytes; B-05 re-verifies these
|
|
2200
|
+
record_json TEXT NOT NULL, -- the single record's canonical JSON
|
|
2201
|
+
received_at_epoch INTEGER NOT NULL, -- epoch SECONDS (Slice-2 convention)
|
|
2202
|
+
accepted_at_epoch INTEGER, -- set on accept (B-05); NULL while pending
|
|
2203
|
+
refused_at_epoch INTEGER, -- set on refuse (B-06)
|
|
2204
|
+
expired_at_epoch INTEGER -- revocation \u21D2 EXPIRE, never DELETE (B-07 / S-3)
|
|
2205
|
+
);
|
|
2206
|
+
CREATE INDEX IF NOT EXISTS idx_smp_origin ON shared_memory_pending(origin_repo_id);
|
|
2207
|
+
CREATE INDEX IF NOT EXISTS idx_smp_received ON shared_memory_pending(received_at_epoch DESC);
|
|
2208
|
+
`);
|
|
2209
|
+
db.exec(`
|
|
2210
|
+
CREATE TABLE IF NOT EXISTS shared_memory_outbound (
|
|
2211
|
+
record_hash TEXT PRIMARY KEY, -- the exported record's canonical hash
|
|
2212
|
+
observation_id INTEGER NOT NULL, -- the source observations.id (for B-07 revocation)
|
|
2213
|
+
origin_repo_id TEXT NOT NULL, -- this repo's own repo_id at export time
|
|
2214
|
+
seq INTEGER NOT NULL, -- the envelope seq this record first went out in
|
|
2215
|
+
exported_at_epoch INTEGER NOT NULL, -- epoch SECONDS
|
|
2216
|
+
revoked_at_epoch INTEGER -- set when the source row is superseded/expired (B-07)
|
|
2217
|
+
);
|
|
2218
|
+
CREATE INDEX IF NOT EXISTS idx_smo_obs ON shared_memory_outbound(observation_id);
|
|
2219
|
+
`);
|
|
2220
|
+
}
|
|
1687
2221
|
function migrateAuditLogCheckExtension(db) {
|
|
1688
2222
|
const row = db.prepare("SELECT sql FROM sqlite_master WHERE type='table' AND name='audit_log'").get();
|
|
1689
2223
|
if (!row) return;
|
|
@@ -1706,7 +2240,16 @@ function migrateAuditLogCheckExtension(db) {
|
|
|
1706
2240
|
"memory_file_adopted_human",
|
|
1707
2241
|
"memory_file_rendered",
|
|
1708
2242
|
"memory_file_render_refused",
|
|
1709
|
-
"memory_file_tombstoned"
|
|
2243
|
+
"memory_file_tombstoned",
|
|
2244
|
+
// B-10 (Slice 5) — the seven cross-repo shared-memory events. An existing DB whose
|
|
2245
|
+
// CHECK lacks any of these triggers the table rebuild below.
|
|
2246
|
+
"shared_memory_exported",
|
|
2247
|
+
"shared_memory_export_refused",
|
|
2248
|
+
"shared_memory_imported",
|
|
2249
|
+
"shared_memory_dropped",
|
|
2250
|
+
"shared_memory_accepted",
|
|
2251
|
+
"shared_memory_refused",
|
|
2252
|
+
"shared_memory_revoked"
|
|
1710
2253
|
];
|
|
1711
2254
|
const checkClauseMatch = row.sql.match(/event_type\s+TEXT\s+NOT\s+NULL\s+CHECK\s*\(\s*event_type\s+IN\s*\(([\s\S]*?)\)\s*\)/i);
|
|
1712
2255
|
if (checkClauseMatch) {
|
|
@@ -1725,7 +2268,12 @@ function migrateAuditLogCheckExtension(db) {
|
|
|
1725
2268
|
'code_change', 'rule_enforced', 'approval', 'review', 'commit', 'compaction',
|
|
1726
2269
|
'rule_candidate_emitted', 'rule_promoted', 'rule_dismissed',
|
|
1727
2270
|
'memory_file_ingested', 'memory_file_expired', 'memory_file_adopted_human',
|
|
1728
|
-
'memory_file_rendered', 'memory_file_render_refused', 'memory_file_tombstoned'
|
|
2271
|
+
'memory_file_rendered', 'memory_file_render_refused', 'memory_file_tombstoned',
|
|
2272
|
+
-- B-10 (Slice 5) \u2014 cross-repo shared-memory events (must mirror the CREATE +
|
|
2273
|
+
-- the expected[] list above, or the migration would loop rebuilding forever).
|
|
2274
|
+
'shared_memory_exported', 'shared_memory_export_refused', 'shared_memory_imported',
|
|
2275
|
+
'shared_memory_dropped', 'shared_memory_accepted', 'shared_memory_refused',
|
|
2276
|
+
'shared_memory_revoked'
|
|
1729
2277
|
)),
|
|
1730
2278
|
actor TEXT NOT NULL DEFAULT 'ai' CHECK(actor IN ('ai', 'human', 'hook', 'agent')),
|
|
1731
2279
|
model_id TEXT,
|
|
@@ -1802,6 +2350,12 @@ function initMemorySchema(db) {
|
|
|
1802
2350
|
-- credential, or takes a snapshot. A Slice-5 synced memory arrives here with
|
|
1803
2351
|
-- origin='team' and must never reach disk without CR-55's gates.
|
|
1804
2352
|
origin TEXT NOT NULL DEFAULT 'local',
|
|
2353
|
+
-- B-01 (Slice 5, cross-repo surfacing): the per-decision SHARE opt-in. Set to 1
|
|
2354
|
+
-- ONLY by an explicit human act (massu memory share <id>); NEVER by a heuristic,
|
|
2355
|
+
-- score, LLM, hook, or consolidation pass. Export (B-02) reads WHERE shareable=1
|
|
2356
|
+
-- AND origin='local'. Defaults 0 so every pre-existing and machine-written row is
|
|
2357
|
+
-- un-shareable until a human says otherwise (fail-closed).
|
|
2358
|
+
shareable INTEGER NOT NULL DEFAULT 0,
|
|
1805
2359
|
created_at TEXT NOT NULL,
|
|
1806
2360
|
created_at_epoch INTEGER NOT NULL,
|
|
1807
2361
|
FOREIGN KEY(session_id) REFERENCES sessions(session_id) ON DELETE CASCADE
|
|
@@ -2075,7 +2629,13 @@ function initMemorySchema(db) {
|
|
|
2075
2629
|
'code_change', 'rule_enforced', 'approval', 'review', 'commit', 'compaction',
|
|
2076
2630
|
'rule_candidate_emitted', 'rule_promoted', 'rule_dismissed',
|
|
2077
2631
|
'memory_file_ingested', 'memory_file_expired', 'memory_file_adopted_human',
|
|
2078
|
-
'memory_file_rendered', 'memory_file_render_refused', 'memory_file_tombstoned'
|
|
2632
|
+
'memory_file_rendered', 'memory_file_render_refused', 'memory_file_tombstoned',
|
|
2633
|
+
-- B-10 (Slice 5): cross-repo shared-memory observability. Without these seven,
|
|
2634
|
+
-- nobody could ever answer "what has crossed between my repos?" (and an ingest
|
|
2635
|
+
-- catch would swallow the CHECK violation, silently producing NOTHING).
|
|
2636
|
+
'shared_memory_exported', 'shared_memory_export_refused', 'shared_memory_imported',
|
|
2637
|
+
'shared_memory_dropped', 'shared_memory_accepted', 'shared_memory_refused',
|
|
2638
|
+
'shared_memory_revoked'
|
|
2079
2639
|
)),
|
|
2080
2640
|
actor TEXT NOT NULL DEFAULT 'ai' CHECK(actor IN ('ai', 'human', 'hook', 'agent')),
|
|
2081
2641
|
model_id TEXT,
|
|
@@ -2444,6 +3004,7 @@ function initMemorySchema(db) {
|
|
|
2444
3004
|
`);
|
|
2445
3005
|
migrateObservationEmbeddingChunks(db);
|
|
2446
3006
|
migrateMemoryFilesFor4B(db);
|
|
3007
|
+
migrateSharedMemoryFor5B(db);
|
|
2447
3008
|
ensureRuleCandidatesTable(db);
|
|
2448
3009
|
migrateRuleDelivery(db);
|
|
2449
3010
|
}
|
|
@@ -3314,6 +3875,7 @@ var MEMORY_SCHEMA_VERSION, TOOL_COST_EVENTS_RETENTION_DAYS, MAX_DRAFT_TEXT_LEN,
|
|
|
3314
3875
|
var init_memory_db = __esm({
|
|
3315
3876
|
"src/memory-db.ts"() {
|
|
3316
3877
|
"use strict";
|
|
3878
|
+
init_db_driver();
|
|
3317
3879
|
init_config();
|
|
3318
3880
|
init_memory_vector();
|
|
3319
3881
|
init_db_backup();
|
|
@@ -3334,21 +3896,21 @@ var init_memory_db = __esm({
|
|
|
3334
3896
|
});
|
|
3335
3897
|
|
|
3336
3898
|
// src/hooks/lib/hook-failure-signal.ts
|
|
3337
|
-
import { appendFileSync, mkdirSync as
|
|
3338
|
-
import { join as
|
|
3899
|
+
import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync6, existsSync as existsSync8 } from "fs";
|
|
3900
|
+
import { join as join5, dirname as dirname7 } from "path";
|
|
3339
3901
|
function resolveFailureLogPath() {
|
|
3340
3902
|
const explicit = process.env.MASSU_HOOK_FAILURE_LOG;
|
|
3341
3903
|
if (explicit) return explicit;
|
|
3342
3904
|
let dir = process.cwd();
|
|
3343
3905
|
for (let i = 0; i < 12; i++) {
|
|
3344
|
-
if (
|
|
3345
|
-
return
|
|
3906
|
+
if (existsSync8(join5(dir, ".massu")) || existsSync8(join5(dir, "massu.config.yaml"))) {
|
|
3907
|
+
return join5(dir, ".massu", "hook-failures.jsonl");
|
|
3346
3908
|
}
|
|
3347
|
-
const parent =
|
|
3909
|
+
const parent = dirname7(dir);
|
|
3348
3910
|
if (parent === dir) break;
|
|
3349
3911
|
dir = parent;
|
|
3350
3912
|
}
|
|
3351
|
-
return
|
|
3913
|
+
return join5(process.cwd(), ".massu", "hook-failures.jsonl");
|
|
3352
3914
|
}
|
|
3353
3915
|
function recordHookFailure(hook, error, context) {
|
|
3354
3916
|
const record = {
|
|
@@ -3361,8 +3923,8 @@ function recordHookFailure(hook, error, context) {
|
|
|
3361
3923
|
let wroteSomething = false;
|
|
3362
3924
|
try {
|
|
3363
3925
|
const path = resolveFailureLogPath();
|
|
3364
|
-
|
|
3365
|
-
|
|
3926
|
+
mkdirSync6(dirname7(path), { recursive: true });
|
|
3927
|
+
appendFileSync2(path, JSON.stringify(record) + "\n", "utf-8");
|
|
3366
3928
|
wroteSomething = true;
|
|
3367
3929
|
} catch {
|
|
3368
3930
|
}
|
|
@@ -3407,9 +3969,9 @@ var init_hook_failure_signal = __esm({
|
|
|
3407
3969
|
// src/hooks/fix-detector.ts
|
|
3408
3970
|
init_config();
|
|
3409
3971
|
import { execFileSync } from "child_process";
|
|
3410
|
-
import { existsSync as
|
|
3411
|
-
import { tmpdir } from "os";
|
|
3412
|
-
import { join as
|
|
3972
|
+
import { existsSync as existsSync9, appendFileSync as appendFileSync3, mkdirSync as mkdirSync7, readFileSync as readFileSync6 } from "fs";
|
|
3973
|
+
import { tmpdir as tmpdir2 } from "os";
|
|
3974
|
+
import { join as join6 } from "path";
|
|
3413
3975
|
|
|
3414
3976
|
// src/hooks/lib/write-hook-message.ts
|
|
3415
3977
|
function writeHookMessage(message) {
|
|
@@ -3460,16 +4022,16 @@ var FIX_HEURISTICS = [
|
|
|
3460
4022
|
}
|
|
3461
4023
|
];
|
|
3462
4024
|
function getSessionFlagPath(sessionId) {
|
|
3463
|
-
const dir =
|
|
3464
|
-
if (!
|
|
3465
|
-
|
|
4025
|
+
const dir = join6(tmpdir2(), "massu-auto-learning");
|
|
4026
|
+
if (!existsSync9(dir)) {
|
|
4027
|
+
mkdirSync7(dir, { recursive: true });
|
|
3466
4028
|
}
|
|
3467
|
-
return
|
|
4029
|
+
return join6(dir, `fixes-${sessionId.slice(0, 12)}.jsonl`);
|
|
3468
4030
|
}
|
|
3469
4031
|
function _stateDir() {
|
|
3470
|
-
const dir =
|
|
3471
|
-
if (!
|
|
3472
|
-
|
|
4032
|
+
const dir = join6(tmpdir2(), "massu-fix-detector-state");
|
|
4033
|
+
if (!existsSync9(dir)) {
|
|
4034
|
+
mkdirSync7(dir, { recursive: true });
|
|
3473
4035
|
}
|
|
3474
4036
|
return dir;
|
|
3475
4037
|
}
|
|
@@ -3482,9 +4044,9 @@ function _cwdHash(cwd) {
|
|
|
3482
4044
|
}
|
|
3483
4045
|
function isGitWorkTreeFast(cwd) {
|
|
3484
4046
|
try {
|
|
3485
|
-
const cachePath =
|
|
3486
|
-
if (
|
|
3487
|
-
const cached = JSON.parse(
|
|
4047
|
+
const cachePath = join6(_stateDir(), `worktree-${_cwdHash(cwd)}.json`);
|
|
4048
|
+
if (existsSync9(cachePath)) {
|
|
4049
|
+
const cached = JSON.parse(readFileSync6(cachePath, "utf-8"));
|
|
3488
4050
|
const ageMs = Date.now() - cached.ts;
|
|
3489
4051
|
if (ageMs < 36e5) return cached.isWorkTree;
|
|
3490
4052
|
}
|
|
@@ -3496,7 +4058,7 @@ function isGitWorkTreeFast(cwd) {
|
|
|
3496
4058
|
isWorkTree = false;
|
|
3497
4059
|
}
|
|
3498
4060
|
try {
|
|
3499
|
-
|
|
4061
|
+
appendFileSync3(cachePath, "");
|
|
3500
4062
|
const fs = __require("fs");
|
|
3501
4063
|
fs.writeFileSync(cachePath, JSON.stringify({ isWorkTree, ts: Date.now() }));
|
|
3502
4064
|
} catch {
|
|
@@ -3507,11 +4069,11 @@ function isGitWorkTreeFast(cwd) {
|
|
|
3507
4069
|
}
|
|
3508
4070
|
}
|
|
3509
4071
|
function _disabledFlagPath(sessionId) {
|
|
3510
|
-
return
|
|
4072
|
+
return join6(_stateDir(), `disabled-${sessionId.slice(0, 12)}.flag`);
|
|
3511
4073
|
}
|
|
3512
4074
|
function isSessionAutoDisabled(sessionId) {
|
|
3513
4075
|
try {
|
|
3514
|
-
return
|
|
4076
|
+
return existsSync9(_disabledFlagPath(sessionId));
|
|
3515
4077
|
} catch {
|
|
3516
4078
|
return false;
|
|
3517
4079
|
}
|
|
@@ -3529,7 +4091,7 @@ async function main() {
|
|
|
3529
4091
|
const input = await readStdin();
|
|
3530
4092
|
const hookInput = JSON.parse(input);
|
|
3531
4093
|
const filePath = hookInput.tool_input?.file_path;
|
|
3532
|
-
if (!filePath || !
|
|
4094
|
+
if (!filePath || !existsSync9(filePath)) {
|
|
3533
4095
|
process.exit(0);
|
|
3534
4096
|
return;
|
|
3535
4097
|
}
|
|
@@ -3589,8 +4151,8 @@ async function main() {
|
|
|
3589
4151
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
3590
4152
|
};
|
|
3591
4153
|
const flagPath = getSessionFlagPath(hookInput.session_id);
|
|
3592
|
-
|
|
3593
|
-
const lines =
|
|
4154
|
+
appendFileSync3(flagPath, JSON.stringify(signal) + "\n");
|
|
4155
|
+
const lines = readFileSync6(flagPath, "utf-8").split("\n").filter(Boolean);
|
|
3594
4156
|
if (lines.length === 1) {
|
|
3595
4157
|
writeHookMessage(
|
|
3596
4158
|
`[Massu Auto-Learning] Bug fix detected in ${filePath} (signals: ${detected.join(", ")}). The auto-learning pipeline will prompt you at session end to create an incident report, derive a prevention rule, and add enforcement.`
|