@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
|
@@ -237,6 +237,32 @@ var init_config_memory_schema = __esm({
|
|
|
237
237
|
// turn of EVERY session, so an unbounded index is a permanent context tax — and the
|
|
238
238
|
// per-session cap bounds only the RATE, never the total.
|
|
239
239
|
indexMaxLines: z.number().int().min(1).default(50)
|
|
240
|
+
}).default({}),
|
|
241
|
+
// --- Cross-repo memory surfacing (plan-living-memory-slice-5-cross-repo-surfacing) ---
|
|
242
|
+
// A decision made in one of your repos can surface in another — opt-in per
|
|
243
|
+
// decision AND opt-in per repo, signed, verified, and materialized ONLY on
|
|
244
|
+
// explicit human acceptance. Local transport is FREE and zero-network.
|
|
245
|
+
//
|
|
246
|
+
// ⛔ BOTH switches default OFF, and OFF means NOTHING EXISTS — no registry, no
|
|
247
|
+
// keys, no inbox, no behavioural difference. Two INDEPENDENT opt-ins:
|
|
248
|
+
// • enabled — may this repo EXPORT its shareable decisions? (opt-in #1)
|
|
249
|
+
// • subscribe — which repo LABELS may this repo IMPORT from? (opt-in #2)
|
|
250
|
+
// `subscribe: []` means import NOTHING. There is deliberately NO `subscribe: all`
|
|
251
|
+
// — a repo you never named is a repo Massu never reads from.
|
|
252
|
+
share: z.object({
|
|
253
|
+
enabled: z.boolean().default(false),
|
|
254
|
+
subscribe: z.array(z.string()).default([]),
|
|
255
|
+
// C-04 — recall surfacing of cross-repo memories. `enabled` defaults true but is
|
|
256
|
+
// CONDITIONAL on `subscribe` being non-empty (empty by default), so the effective
|
|
257
|
+
// default is DORMANT: with `subscribe: []` the recall hook output is byte-identical
|
|
258
|
+
// to today's. `maxCrossRepoItems` caps how many accepted cross-repo items may appear
|
|
259
|
+
// per recall block (default 1); `minScore` is an OPTIONAL strictly-higher floor for
|
|
260
|
+
// cross-repo items (they are, by construction, less relevant than local ones).
|
|
261
|
+
recall: z.object({
|
|
262
|
+
enabled: z.boolean().default(true),
|
|
263
|
+
maxCrossRepoItems: z.number().int().min(0).default(1),
|
|
264
|
+
minScore: z.number().optional()
|
|
265
|
+
}).default({})
|
|
240
266
|
}).default({})
|
|
241
267
|
}).optional();
|
|
242
268
|
}
|
|
@@ -366,9 +392,13 @@ Hint: run \`massu config refresh\` to regenerate a valid config or fix the liste
|
|
|
366
392
|
const resolvedEndpoint = resolveEndpoint({ configEndpoint: _config.cloud?.endpoint });
|
|
367
393
|
if (resolvedKey.apiKey) {
|
|
368
394
|
_config.cloud = {
|
|
369
|
-
enabled: true,
|
|
370
395
|
sync: { memory: true, analytics: true, audit: true },
|
|
371
396
|
..._config.cloud,
|
|
397
|
+
// Spread FIRST, then decide `enabled`, so a workspace that declares a `cloud:`
|
|
398
|
+
// block only to tune `requestTimeoutMs` does not disable its own sync. An
|
|
399
|
+
// EXPLICIT `enabled: false` is still honoured; `undefined` (not stated) means
|
|
400
|
+
// "a key resolved, so turn it on". (plan-2026-07-20-cloud-sync-timeout)
|
|
401
|
+
enabled: _config.cloud?.enabled ?? true,
|
|
372
402
|
apiKey: resolvedKey.apiKey,
|
|
373
403
|
endpoint: resolvedEndpoint
|
|
374
404
|
};
|
|
@@ -486,7 +516,6 @@ var init_config = __esm({
|
|
|
486
516
|
custom_patterns: z2.array(CustomPatternSchema).default([])
|
|
487
517
|
}).optional(),
|
|
488
518
|
adr: z2.object({
|
|
489
|
-
detection_phrases: z2.array(z2.string()).default(["chose", "decided", "switching to", "moving from", "going with"]),
|
|
490
519
|
template: z2.string().default("default"),
|
|
491
520
|
storage: z2.string().default("database"),
|
|
492
521
|
output_dir: z2.string().default("docs/adr")
|
|
@@ -588,9 +617,22 @@ var init_config = __esm({
|
|
|
588
617
|
})).default([])
|
|
589
618
|
}).optional();
|
|
590
619
|
CloudConfigSchema = z2.object({
|
|
591
|
-
|
|
620
|
+
// OPTIONAL, not `.default(false)`. A zod default is indistinguishable from an
|
|
621
|
+
// explicit value once parsed, so `.default(false)` meant that merely declaring a
|
|
622
|
+
// `cloud:` block (to set any OTHER key) silently produced `enabled: false`, which
|
|
623
|
+
// then overrode the `enabled: true` auto-enable below via object spread — turning
|
|
624
|
+
// cloud sync OFF as a side effect of tuning it. `undefined` now means "not stated",
|
|
625
|
+
// and the auto-enable preserves it. (plan-2026-07-20-cloud-sync-timeout)
|
|
626
|
+
enabled: z2.boolean().optional(),
|
|
592
627
|
apiKey: z2.string().optional(),
|
|
593
628
|
endpoint: z2.string().optional(),
|
|
629
|
+
// Per-request POST budget for the `/sync` ingest. cloud-sync.ts reads this
|
|
630
|
+
// (`(cloud as { requestTimeoutMs?: number }).requestTimeoutMs`) but it was ABSENT
|
|
631
|
+
// from this schema, and zod strips unknown keys — so the knob was unreachable from
|
|
632
|
+
// massu.config.yaml and the default could never be tuned. Measured 2026-07-20: a
|
|
633
|
+
// 423-observation payload takes ~9.2s against the live ingest. Capped at
|
|
634
|
+
// SYNC_DEADLINE_MS (20s) since the overall deadline clamps each attempt anyway.
|
|
635
|
+
requestTimeoutMs: z2.number().int().positive().max(2e4).optional(),
|
|
594
636
|
sync: z2.object({
|
|
595
637
|
memory: z2.boolean().default(true),
|
|
596
638
|
analytics: z2.boolean().default(true),
|
|
@@ -807,6 +849,465 @@ var init_config = __esm({
|
|
|
807
849
|
}
|
|
808
850
|
});
|
|
809
851
|
|
|
852
|
+
// src/lib/fileLock.ts
|
|
853
|
+
import { mkdirSync as mkdirSync2, readFileSync as readFileSync3, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
854
|
+
import { dirname as dirname2 } from "path";
|
|
855
|
+
import * as lockfile from "proper-lockfile";
|
|
856
|
+
function readLockHolderPid(lockPath) {
|
|
857
|
+
try {
|
|
858
|
+
const raw = readFileSync3(`${lockPath}.pid`, "utf-8").trim();
|
|
859
|
+
const pid = Number.parseInt(raw, 10);
|
|
860
|
+
if (!Number.isFinite(pid) || pid <= 0) return null;
|
|
861
|
+
return pid;
|
|
862
|
+
} catch {
|
|
863
|
+
return null;
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
function busyWaitSync(ms) {
|
|
867
|
+
if (typeof SharedArrayBuffer === "undefined" || typeof Atomics === "undefined") {
|
|
868
|
+
throw new Error(
|
|
869
|
+
`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.`
|
|
870
|
+
);
|
|
871
|
+
}
|
|
872
|
+
const sab = new SharedArrayBuffer(4);
|
|
873
|
+
const view = new Int32Array(sab);
|
|
874
|
+
Atomics.wait(view, 0, 0, ms);
|
|
875
|
+
}
|
|
876
|
+
function withFileLockSync(lockPath, fn, opts = {}) {
|
|
877
|
+
mkdirSync2(dirname2(lockPath), { recursive: true });
|
|
878
|
+
const staleMs = opts.staleMs ?? 3e4;
|
|
879
|
+
const blockMs = opts.retries === 0 ? 0 : opts.blockMs ?? 3e4;
|
|
880
|
+
const pollIntervalMs = opts.pollIntervalMs ?? 100;
|
|
881
|
+
const now = opts.now ?? Date.now;
|
|
882
|
+
const sleep = opts.sleep ?? busyWaitSync;
|
|
883
|
+
const makeBusyError = opts.errorFactory ?? ((path, pid, retrySeconds, code) => new FileLockBusyError(path, pid, retrySeconds, code));
|
|
884
|
+
let release = null;
|
|
885
|
+
const deadline = now() + blockMs;
|
|
886
|
+
for (; ; ) {
|
|
887
|
+
try {
|
|
888
|
+
release = lockfile.lockSync(lockPath, {
|
|
889
|
+
stale: staleMs,
|
|
890
|
+
retries: 0,
|
|
891
|
+
realpath: false
|
|
892
|
+
});
|
|
893
|
+
try {
|
|
894
|
+
writeFileSync2(`${lockPath}.pid`, String(process.pid), "utf-8");
|
|
895
|
+
} catch {
|
|
896
|
+
}
|
|
897
|
+
break;
|
|
898
|
+
} catch (err) {
|
|
899
|
+
const e = err;
|
|
900
|
+
const code = e.code;
|
|
901
|
+
if (code !== "ELOCKED" && code !== "EBUSY") {
|
|
902
|
+
throw err;
|
|
903
|
+
}
|
|
904
|
+
if (now() >= deadline) {
|
|
905
|
+
const holderPid = readLockHolderPid(lockPath);
|
|
906
|
+
const remainingMs = Math.max(0, deadline - now());
|
|
907
|
+
const retryAfterSeconds = blockMs === 0 ? Math.round(staleMs / 1e3) : Math.round(remainingMs / 1e3);
|
|
908
|
+
throw makeBusyError(lockPath, holderPid, retryAfterSeconds, code);
|
|
909
|
+
}
|
|
910
|
+
sleep(pollIntervalMs);
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
try {
|
|
914
|
+
return fn();
|
|
915
|
+
} finally {
|
|
916
|
+
try {
|
|
917
|
+
if (release) release();
|
|
918
|
+
} catch {
|
|
919
|
+
}
|
|
920
|
+
try {
|
|
921
|
+
rmSync2(`${lockPath}.pid`, { force: true });
|
|
922
|
+
} catch {
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
var FileLockBusyError;
|
|
927
|
+
var init_fileLock = __esm({
|
|
928
|
+
"src/lib/fileLock.ts"() {
|
|
929
|
+
"use strict";
|
|
930
|
+
FileLockBusyError = class extends Error {
|
|
931
|
+
constructor(lockPath, holderPid, retryAfterSeconds, causeCode) {
|
|
932
|
+
const pidPart = holderPid != null ? `(PID=${holderPid})` : "(PID=unknown)";
|
|
933
|
+
super(`File lock at ${lockPath} held by another process ${pidPart} \u2014 try again in ${retryAfterSeconds}s`);
|
|
934
|
+
this.lockPath = lockPath;
|
|
935
|
+
this.holderPid = holderPid;
|
|
936
|
+
this.retryAfterSeconds = retryAfterSeconds;
|
|
937
|
+
this.causeCode = causeCode;
|
|
938
|
+
this.name = "FileLockBusyError";
|
|
939
|
+
}
|
|
940
|
+
lockPath;
|
|
941
|
+
holderPid;
|
|
942
|
+
retryAfterSeconds;
|
|
943
|
+
causeCode;
|
|
944
|
+
};
|
|
945
|
+
}
|
|
946
|
+
});
|
|
947
|
+
|
|
948
|
+
// src/lib/sqlite-loader.ts
|
|
949
|
+
import { createRequire } from "module";
|
|
950
|
+
import { spawnSync } from "child_process";
|
|
951
|
+
import { accessSync, appendFileSync, chmodSync as chmodSync2, constants as fsConstants, existsSync as existsSync3, mkdirSync as mkdirSync3 } from "fs";
|
|
952
|
+
import { dirname as dirname3, join } from "path";
|
|
953
|
+
function isNativeAbiError(err) {
|
|
954
|
+
const code = err?.code;
|
|
955
|
+
if (code === "ERR_DLOPEN_FAILED") return true;
|
|
956
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
957
|
+
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);
|
|
958
|
+
}
|
|
959
|
+
function loadBetterSqlite3(opts = {}) {
|
|
960
|
+
if (_testCtor) return _testCtor;
|
|
961
|
+
if (opts.fresh) bustNativeCache();
|
|
962
|
+
return req("better-sqlite3");
|
|
963
|
+
}
|
|
964
|
+
function bustNativeCache() {
|
|
965
|
+
try {
|
|
966
|
+
const cache = req.cache;
|
|
967
|
+
if (!cache) return;
|
|
968
|
+
for (const key of Object.keys(cache)) {
|
|
969
|
+
if (/better[-_]sqlite3|better_sqlite3\.node|[\\/]bindings[\\/]/.test(key)) {
|
|
970
|
+
delete cache[key];
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
} catch {
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
function openDatabase(dbPath, opts = {}) {
|
|
977
|
+
const { selfHeal: selfHealOpt = true, ...ctorOpts } = opts;
|
|
978
|
+
const selfHeal = selfHealOpt && process.env.MASSU_HOOK_RUNTIME !== "1";
|
|
979
|
+
const Ctor = loadBetterSqlite3();
|
|
980
|
+
try {
|
|
981
|
+
return new Ctor(dbPath, ctorOpts);
|
|
982
|
+
} catch (err) {
|
|
983
|
+
if (!isNativeAbiError(err)) {
|
|
984
|
+
throw err;
|
|
985
|
+
}
|
|
986
|
+
if (!selfHeal) {
|
|
987
|
+
recordHealEvent({ phase: "skipped", reason: "self-heal-disabled", abiTo: process.versions.modules });
|
|
988
|
+
throw new MemoryEngineUnusableError("abi-mismatch", detailOf(err));
|
|
989
|
+
}
|
|
990
|
+
const result = (_testHeal ?? attemptNativeHeal)(err);
|
|
991
|
+
if (!result.healed && !result.contended) {
|
|
992
|
+
throw new MemoryEngineUnusableError(result.reason ?? "heal-failed", result.detail ?? detailOf(err));
|
|
993
|
+
}
|
|
994
|
+
try {
|
|
995
|
+
const FreshCtor = loadBetterSqlite3({ fresh: true });
|
|
996
|
+
return new FreshCtor(dbPath, ctorOpts);
|
|
997
|
+
} catch (retryErr) {
|
|
998
|
+
throw new MemoryEngineUnusableError("heal-failed", detailOf(retryErr));
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
function attemptNativeHeal(err) {
|
|
1003
|
+
const start = Date.now();
|
|
1004
|
+
const abiTo = process.versions.modules;
|
|
1005
|
+
const abiFrom = parseAbiFrom(err);
|
|
1006
|
+
let pkgDir;
|
|
1007
|
+
try {
|
|
1008
|
+
pkgDir = dirname3(req.resolve("better-sqlite3/package.json"));
|
|
1009
|
+
} catch {
|
|
1010
|
+
recordHealEvent({ phase: "skipped", reason: "not-resolvable", abiFrom, abiTo });
|
|
1011
|
+
return { healed: false, reason: "missing", abiFrom, abiTo, detail: "better-sqlite3 not resolvable" };
|
|
1012
|
+
}
|
|
1013
|
+
if (!isWritable(pkgDir)) {
|
|
1014
|
+
recordHealEvent({ phase: "skipped", reason: "dir-not-writable", abiFrom, abiTo });
|
|
1015
|
+
return { healed: false, reason: "heal-failed", abiFrom, abiTo, detail: `install dir not writable: ${pkgDir}` };
|
|
1016
|
+
}
|
|
1017
|
+
recordHealEvent({ phase: "attempt", reason: "abi-mismatch", abiFrom, abiTo });
|
|
1018
|
+
try {
|
|
1019
|
+
return withFileLockSync(
|
|
1020
|
+
join(credentialsDir(), "native-heal.lock"),
|
|
1021
|
+
() => runRebuild(pkgDir, abiFrom, abiTo, start),
|
|
1022
|
+
{ blockMs: 6e4, staleMs: 3e5 }
|
|
1023
|
+
);
|
|
1024
|
+
} catch (lockErr) {
|
|
1025
|
+
const detail = lockErr instanceof Error ? lockErr.message : String(lockErr);
|
|
1026
|
+
recordHealEvent({ phase: "failed", reason: "heal-failed", abiFrom, abiTo, detail: `lock: ${detail}` });
|
|
1027
|
+
return { healed: false, contended: true, reason: "heal-failed", abiFrom, abiTo, detail: `heal lock contended: ${detail}` };
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
function runRebuild(pkgDir, abiFrom, abiTo, start) {
|
|
1031
|
+
try {
|
|
1032
|
+
const FreshCtor = loadBetterSqlite3({ fresh: true });
|
|
1033
|
+
new FreshCtor(":memory:").close();
|
|
1034
|
+
const res2 = {
|
|
1035
|
+
healed: true,
|
|
1036
|
+
abiFrom,
|
|
1037
|
+
abiTo,
|
|
1038
|
+
durationMs: Date.now() - start,
|
|
1039
|
+
detail: "already healed by a concurrent process"
|
|
1040
|
+
};
|
|
1041
|
+
recordHealEvent({ phase: "success", reason: "already-healed", abiFrom, abiTo, durationMs: res2.durationMs });
|
|
1042
|
+
return res2;
|
|
1043
|
+
} catch (probeErr) {
|
|
1044
|
+
if (!isNativeAbiError(probeErr)) {
|
|
1045
|
+
return { healed: false, reason: "heal-failed", abiFrom, abiTo, durationMs: Date.now() - start, detail: detailOf(probeErr) };
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
const prebuildBin = resolveLocalBin(pkgDir, "prebuild-install");
|
|
1049
|
+
if (prebuildBin) {
|
|
1050
|
+
const r = spawnSync(process.execPath, [prebuildBin], {
|
|
1051
|
+
cwd: pkgDir,
|
|
1052
|
+
encoding: "utf-8",
|
|
1053
|
+
timeout: 12e4
|
|
1054
|
+
});
|
|
1055
|
+
if (r.status === 0) {
|
|
1056
|
+
const res2 = { healed: true, method: "prebuild-install", abiFrom, abiTo, durationMs: Date.now() - start };
|
|
1057
|
+
recordHealEvent({ phase: "success", reason: "abi-mismatch", ...res2 });
|
|
1058
|
+
return res2;
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
const gypBin = resolveNodeGypBin();
|
|
1062
|
+
if (gypBin && hasCompiler()) {
|
|
1063
|
+
const r = spawnSync(process.execPath, [gypBin, "rebuild", "--release"], {
|
|
1064
|
+
cwd: pkgDir,
|
|
1065
|
+
encoding: "utf-8",
|
|
1066
|
+
timeout: 3e5
|
|
1067
|
+
});
|
|
1068
|
+
if (r.status === 0) {
|
|
1069
|
+
const res2 = { healed: true, method: "node-gyp", abiFrom, abiTo, durationMs: Date.now() - start };
|
|
1070
|
+
recordHealEvent({ phase: "success", reason: "abi-mismatch", ...res2 });
|
|
1071
|
+
return res2;
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
const res = {
|
|
1075
|
+
healed: false,
|
|
1076
|
+
reason: "heal-failed",
|
|
1077
|
+
abiFrom,
|
|
1078
|
+
abiTo,
|
|
1079
|
+
durationMs: Date.now() - start,
|
|
1080
|
+
detail: prebuildBin ? "prebuild-install failed; node-gyp fallback unavailable or failed" : "no prebuild-install bin resolvable"
|
|
1081
|
+
};
|
|
1082
|
+
recordHealEvent({ phase: "failed", reason: "heal-failed", abiFrom, abiTo, durationMs: res.durationMs });
|
|
1083
|
+
return res;
|
|
1084
|
+
}
|
|
1085
|
+
function recordHealEvent(event) {
|
|
1086
|
+
try {
|
|
1087
|
+
const rec = {
|
|
1088
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1089
|
+
node: process.versions.node,
|
|
1090
|
+
platform: process.platform,
|
|
1091
|
+
arch: process.arch,
|
|
1092
|
+
...event
|
|
1093
|
+
};
|
|
1094
|
+
const dir = credentialsDir();
|
|
1095
|
+
mkdirSync3(dir, { recursive: true });
|
|
1096
|
+
const file = join(dir, "native-heal-events.jsonl");
|
|
1097
|
+
appendFileSync(file, JSON.stringify(rec) + "\n", "utf-8");
|
|
1098
|
+
try {
|
|
1099
|
+
chmodSync2(file, 384);
|
|
1100
|
+
} catch {
|
|
1101
|
+
}
|
|
1102
|
+
} catch {
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
function parseAbiFrom(err) {
|
|
1106
|
+
const msg = err instanceof Error ? err.message : String(err ?? "");
|
|
1107
|
+
const m = /NODE_MODULE_VERSION (\d+)/.exec(msg);
|
|
1108
|
+
if (m && /^\d+$/.test(m[1])) return m[1];
|
|
1109
|
+
return void 0;
|
|
1110
|
+
}
|
|
1111
|
+
function detailOf(err) {
|
|
1112
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1113
|
+
return msg.slice(0, 300);
|
|
1114
|
+
}
|
|
1115
|
+
function isWritable(dir) {
|
|
1116
|
+
try {
|
|
1117
|
+
accessSync(dir, fsConstants.W_OK);
|
|
1118
|
+
return true;
|
|
1119
|
+
} catch {
|
|
1120
|
+
return false;
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
function resolveLocalBin(pkgDir, name) {
|
|
1124
|
+
const candidates = [
|
|
1125
|
+
join(pkgDir, "node_modules", ".bin", name),
|
|
1126
|
+
// nested install
|
|
1127
|
+
join(pkgDir, "..", ".bin", name),
|
|
1128
|
+
// hoisted (node_modules/.bin)
|
|
1129
|
+
join(pkgDir, "..", "..", "node_modules", ".bin", name)
|
|
1130
|
+
// workspace hoist
|
|
1131
|
+
];
|
|
1132
|
+
for (const c of candidates) {
|
|
1133
|
+
if (existsSync3(c)) return c;
|
|
1134
|
+
}
|
|
1135
|
+
return void 0;
|
|
1136
|
+
}
|
|
1137
|
+
function resolveNodeGypBin() {
|
|
1138
|
+
try {
|
|
1139
|
+
return req.resolve("node-gyp/bin/node-gyp.js");
|
|
1140
|
+
} catch {
|
|
1141
|
+
return void 0;
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
function hasCompiler() {
|
|
1145
|
+
for (const cc of ["cc", "clang", "gcc"]) {
|
|
1146
|
+
try {
|
|
1147
|
+
const r = spawnSync(cc, ["--version"], { encoding: "utf-8", timeout: 5e3 });
|
|
1148
|
+
if (r.status === 0) return true;
|
|
1149
|
+
} catch {
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
return false;
|
|
1153
|
+
}
|
|
1154
|
+
var req, NATIVE_DB_REMEDY, MemoryEngineUnusableError, _testCtor, _testHeal;
|
|
1155
|
+
var init_sqlite_loader = __esm({
|
|
1156
|
+
"src/lib/sqlite-loader.ts"() {
|
|
1157
|
+
"use strict";
|
|
1158
|
+
init_credentials();
|
|
1159
|
+
init_fileLock();
|
|
1160
|
+
req = createRequire(import.meta.url);
|
|
1161
|
+
NATIVE_DB_REMEDY = "Run 'massu heal' to rebuild the database engine for your Node version, then restart your MCP client / Claude Code.";
|
|
1162
|
+
MemoryEngineUnusableError = class extends Error {
|
|
1163
|
+
reason;
|
|
1164
|
+
remedy;
|
|
1165
|
+
detail;
|
|
1166
|
+
constructor(reason, detail) {
|
|
1167
|
+
super(
|
|
1168
|
+
`Massu memory engine is unusable (${reason}). ${NATIVE_DB_REMEDY}` + (detail ? ` [${detail}]` : "")
|
|
1169
|
+
);
|
|
1170
|
+
this.name = "MemoryEngineUnusableError";
|
|
1171
|
+
this.reason = reason;
|
|
1172
|
+
this.remedy = NATIVE_DB_REMEDY;
|
|
1173
|
+
this.detail = detail;
|
|
1174
|
+
}
|
|
1175
|
+
};
|
|
1176
|
+
_testCtor = null;
|
|
1177
|
+
_testHeal = null;
|
|
1178
|
+
}
|
|
1179
|
+
});
|
|
1180
|
+
|
|
1181
|
+
// src/db-driver.ts
|
|
1182
|
+
import { createRequire as createRequire2 } from "module";
|
|
1183
|
+
import { closeSync, existsSync as existsSync4, mkdtempSync, openSync, readFileSync as readFileSync4, rmSync as rmSync3, fsyncSync } from "fs";
|
|
1184
|
+
import { tmpdir } from "os";
|
|
1185
|
+
import { join as join2 } from "path";
|
|
1186
|
+
function resolveDbEngine() {
|
|
1187
|
+
return process.env[DB_ENGINE_ENV] === "better-sqlite3" ? "better-sqlite3" : DEFAULT_DB_ENGINE;
|
|
1188
|
+
}
|
|
1189
|
+
function nodeSqliteCtor() {
|
|
1190
|
+
if (!_nodeCtor) _nodeCtor = req2("node:sqlite").DatabaseSync;
|
|
1191
|
+
return _nodeCtor;
|
|
1192
|
+
}
|
|
1193
|
+
function plainRow(row) {
|
|
1194
|
+
if (row == null || typeof row !== "object") return row;
|
|
1195
|
+
return { ...row };
|
|
1196
|
+
}
|
|
1197
|
+
function openNodeSqlite(dbPath, opts) {
|
|
1198
|
+
const Ctor = nodeSqliteCtor();
|
|
1199
|
+
const raw = new Ctor(dbPath, {
|
|
1200
|
+
open: true,
|
|
1201
|
+
readOnly: !!opts.readonly,
|
|
1202
|
+
// FAITHFULNESS: better-sqlite3's ACTUAL default is foreign_keys=ON (empirically
|
|
1203
|
+
// verified — SQLite's own default is OFF, but bs3 enables it). massu was written +
|
|
1204
|
+
// tested against that default, so the adapter must match it, or a store that relies
|
|
1205
|
+
// on FK-ON-by-default (without an explicit pragma) would silently lose enforcement.
|
|
1206
|
+
// Explicit `foreign_keys=OFF/ON` pragmas (e.g. memory-db bulk ops) still override.
|
|
1207
|
+
enableForeignKeyConstraints: true,
|
|
1208
|
+
allowExtension: false
|
|
1209
|
+
});
|
|
1210
|
+
let savepointSeq = 0;
|
|
1211
|
+
const wrapStmt = (sql) => {
|
|
1212
|
+
const st = raw.prepare(sql);
|
|
1213
|
+
return {
|
|
1214
|
+
run: (...p) => {
|
|
1215
|
+
const r = st.run(...p);
|
|
1216
|
+
return {
|
|
1217
|
+
changes: typeof r.changes === "bigint" ? Number(r.changes) : r.changes,
|
|
1218
|
+
lastInsertRowid: r.lastInsertRowid
|
|
1219
|
+
};
|
|
1220
|
+
},
|
|
1221
|
+
get: (...p) => plainRow(st.get(...p)),
|
|
1222
|
+
all: (...p) => st.all(...p).map(plainRow)
|
|
1223
|
+
};
|
|
1224
|
+
};
|
|
1225
|
+
const pragma = (source, options) => {
|
|
1226
|
+
const s = String(source).trim();
|
|
1227
|
+
if (s.includes("=")) {
|
|
1228
|
+
raw.exec(`PRAGMA ${s}`);
|
|
1229
|
+
return void 0;
|
|
1230
|
+
}
|
|
1231
|
+
const rows = raw.prepare(`PRAGMA ${s}`).all().map(plainRow);
|
|
1232
|
+
if (options?.simple) {
|
|
1233
|
+
const first = rows[0];
|
|
1234
|
+
return first ? Object.values(first)[0] : void 0;
|
|
1235
|
+
}
|
|
1236
|
+
return rows;
|
|
1237
|
+
};
|
|
1238
|
+
const transaction = (fn) => {
|
|
1239
|
+
const run = (...args) => {
|
|
1240
|
+
const nested = raw.isTransaction;
|
|
1241
|
+
const name = `msp_${savepointSeq++}`;
|
|
1242
|
+
if (nested) raw.exec(`SAVEPOINT ${name}`);
|
|
1243
|
+
else raw.exec("BEGIN");
|
|
1244
|
+
try {
|
|
1245
|
+
const out = fn(...args);
|
|
1246
|
+
if (nested) raw.exec(`RELEASE ${name}`);
|
|
1247
|
+
else raw.exec("COMMIT");
|
|
1248
|
+
return out;
|
|
1249
|
+
} catch (e) {
|
|
1250
|
+
if (nested) {
|
|
1251
|
+
raw.exec(`ROLLBACK TO ${name}`);
|
|
1252
|
+
raw.exec(`RELEASE ${name}`);
|
|
1253
|
+
} else {
|
|
1254
|
+
raw.exec("ROLLBACK");
|
|
1255
|
+
}
|
|
1256
|
+
throw e;
|
|
1257
|
+
}
|
|
1258
|
+
};
|
|
1259
|
+
return run;
|
|
1260
|
+
};
|
|
1261
|
+
const serialize = () => {
|
|
1262
|
+
if (raw.isTransaction) {
|
|
1263
|
+
throw new Error(
|
|
1264
|
+
"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."
|
|
1265
|
+
);
|
|
1266
|
+
}
|
|
1267
|
+
const dir = mkdtempSync(join2(tmpdir(), "massu-serialize-"));
|
|
1268
|
+
const out = join2(dir, "snapshot.db");
|
|
1269
|
+
try {
|
|
1270
|
+
raw.exec(`VACUUM INTO '${out.replace(/'/g, "''")}'`);
|
|
1271
|
+
const fd = openSync(out, "r");
|
|
1272
|
+
try {
|
|
1273
|
+
fsyncSync(fd);
|
|
1274
|
+
} finally {
|
|
1275
|
+
closeSync(fd);
|
|
1276
|
+
}
|
|
1277
|
+
return readFileSync4(out);
|
|
1278
|
+
} finally {
|
|
1279
|
+
rmSync3(dir, { recursive: true, force: true });
|
|
1280
|
+
}
|
|
1281
|
+
};
|
|
1282
|
+
const handle = {
|
|
1283
|
+
prepare: (sql) => wrapStmt(sql),
|
|
1284
|
+
exec: (sql) => raw.exec(sql),
|
|
1285
|
+
pragma,
|
|
1286
|
+
transaction,
|
|
1287
|
+
serialize,
|
|
1288
|
+
close: () => raw.close()
|
|
1289
|
+
};
|
|
1290
|
+
return handle;
|
|
1291
|
+
}
|
|
1292
|
+
function openBetterSqlite3(dbPath, opts) {
|
|
1293
|
+
return openDatabase(dbPath, opts);
|
|
1294
|
+
}
|
|
1295
|
+
function openDatabase2(dbPath, opts = {}) {
|
|
1296
|
+
return resolveDbEngine() === "better-sqlite3" ? openBetterSqlite3(dbPath, opts) : openNodeSqlite(dbPath, opts);
|
|
1297
|
+
}
|
|
1298
|
+
var req2, DEFAULT_DB_ENGINE, DB_ENGINE_ENV, _nodeCtor;
|
|
1299
|
+
var init_db_driver = __esm({
|
|
1300
|
+
"src/db-driver.ts"() {
|
|
1301
|
+
"use strict";
|
|
1302
|
+
init_sqlite_loader();
|
|
1303
|
+
init_sqlite_loader();
|
|
1304
|
+
req2 = createRequire2(import.meta.url);
|
|
1305
|
+
DEFAULT_DB_ENGINE = "node-sqlite";
|
|
1306
|
+
DB_ENGINE_ENV = "MASSU_DB_ENGINE";
|
|
1307
|
+
_nodeCtor = null;
|
|
1308
|
+
}
|
|
1309
|
+
});
|
|
1310
|
+
|
|
810
1311
|
// src/memory-vector.ts
|
|
811
1312
|
function float32ToBlob(vec) {
|
|
812
1313
|
return Buffer.from(new Uint8Array(vec.buffer, vec.byteOffset, vec.byteLength));
|
|
@@ -827,27 +1328,26 @@ var init_memory_vector = __esm({
|
|
|
827
1328
|
});
|
|
828
1329
|
|
|
829
1330
|
// src/db-backup.ts
|
|
830
|
-
import
|
|
831
|
-
import {
|
|
832
|
-
import { resolve as resolve3, join, basename, dirname as dirname2 } from "path";
|
|
1331
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync4, readdirSync, statSync, unlinkSync, copyFileSync, rmSync as rmSync4 } from "fs";
|
|
1332
|
+
import { resolve as resolve3, join as join3, basename, dirname as dirname4 } from "path";
|
|
833
1333
|
import { homedir as homedir3 } from "os";
|
|
834
1334
|
function dbBackupsRoot(home = homedir3()) {
|
|
835
1335
|
return resolve3(home, ".massu", "db-backups");
|
|
836
1336
|
}
|
|
837
1337
|
function projectBackupDir(projectRoot, home = homedir3()) {
|
|
838
1338
|
const slug = basename(projectRoot).replace(/[^A-Za-z0-9._-]/g, "_") || "project";
|
|
839
|
-
return
|
|
1339
|
+
return join3(dbBackupsRoot(home), slug);
|
|
840
1340
|
}
|
|
841
1341
|
function backupStamp(nowMs) {
|
|
842
1342
|
return new Date(nowMs).toISOString().replace(/[:.]/g, "-");
|
|
843
1343
|
}
|
|
844
1344
|
function listDbBackups(projectRoot, home = homedir3()) {
|
|
845
1345
|
const dir = projectBackupDir(projectRoot, home);
|
|
846
|
-
if (!
|
|
1346
|
+
if (!existsSync5(dir)) return [];
|
|
847
1347
|
const out = [];
|
|
848
1348
|
for (const f of readdirSync(dir)) {
|
|
849
1349
|
if (!f.endsWith(".db")) continue;
|
|
850
|
-
const p =
|
|
1350
|
+
const p = join3(dir, f);
|
|
851
1351
|
const st = statSync(p);
|
|
852
1352
|
const db = f.replace(/-\d{4}-\d{2}-\d{2}T.*$/, "");
|
|
853
1353
|
out.push({ db, path: p, bytes: st.size, mtimeMs: st.mtimeMs });
|
|
@@ -860,13 +1360,13 @@ function hasFreshDbBackup(projectRoot, dbPath, nowMs = Date.now(), home = homedi
|
|
|
860
1360
|
if (backups.length === 0) return false;
|
|
861
1361
|
const newest = backups[0];
|
|
862
1362
|
if (nowMs - newest.mtimeMs > FRESH_WINDOW_MS) return false;
|
|
863
|
-
if (
|
|
1363
|
+
if (existsSync5(dbPath) && statSync(dbPath).mtimeMs > newest.mtimeMs) return false;
|
|
864
1364
|
return true;
|
|
865
1365
|
}
|
|
866
1366
|
function integrityOk(dbPath) {
|
|
867
1367
|
let db = null;
|
|
868
1368
|
try {
|
|
869
|
-
db =
|
|
1369
|
+
db = openDatabase2(dbPath, { readonly: true });
|
|
870
1370
|
const rows = db.pragma("integrity_check");
|
|
871
1371
|
return rows.length === 1 && rows[0].integrity_check === "ok";
|
|
872
1372
|
} catch {
|
|
@@ -879,16 +1379,16 @@ function integrityOk(dbPath) {
|
|
|
879
1379
|
}
|
|
880
1380
|
}
|
|
881
1381
|
function backupDb(projectRoot, dbPath, nowMs = Date.now(), home = homedir3()) {
|
|
882
|
-
if (!
|
|
1382
|
+
if (!existsSync5(dbPath)) {
|
|
883
1383
|
throw new DbBackupError(`cannot back up a database that does not exist: ${dbPath}`);
|
|
884
1384
|
}
|
|
885
1385
|
const dir = projectBackupDir(projectRoot, home);
|
|
886
|
-
|
|
1386
|
+
mkdirSync4(dir, { recursive: true });
|
|
887
1387
|
const name = basename(dbPath, ".db");
|
|
888
|
-
const dest =
|
|
1388
|
+
const dest = join3(dir, `${name}-${backupStamp(nowMs)}.db`);
|
|
889
1389
|
let src = null;
|
|
890
1390
|
try {
|
|
891
|
-
src =
|
|
1391
|
+
src = openDatabase2(dbPath, { readonly: true });
|
|
892
1392
|
src.exec(`VACUUM INTO '${dest.replace(/'/g, "''")}'`);
|
|
893
1393
|
} catch (err) {
|
|
894
1394
|
throw new DbBackupError(
|
|
@@ -927,7 +1427,7 @@ function pruneDbBackups(projectRoot, dbName, keep = DEFAULT_RETENTION, home = ho
|
|
|
927
1427
|
}
|
|
928
1428
|
function backupBeforeSchemaChange(projectRoot, dbPath, onError, nowMs = Date.now(), home = homedir3()) {
|
|
929
1429
|
try {
|
|
930
|
-
if (!
|
|
1430
|
+
if (!existsSync5(dbPath)) return null;
|
|
931
1431
|
if (hasFreshDbBackup(projectRoot, dbPath, nowMs, home)) return null;
|
|
932
1432
|
return backupDb(projectRoot, dbPath, nowMs, home);
|
|
933
1433
|
} catch (err) {
|
|
@@ -939,6 +1439,7 @@ var DEFAULT_RETENTION, FRESH_WINDOW_MS, DbBackupError;
|
|
|
939
1439
|
var init_db_backup = __esm({
|
|
940
1440
|
"src/db-backup.ts"() {
|
|
941
1441
|
"use strict";
|
|
1442
|
+
init_db_driver();
|
|
942
1443
|
DEFAULT_RETENTION = 5;
|
|
943
1444
|
FRESH_WINDOW_MS = 24 * 60 * 60 * 1e3;
|
|
944
1445
|
DbBackupError = class extends Error {
|
|
@@ -1051,9 +1552,9 @@ var init_rule_candidate_store = __esm({
|
|
|
1051
1552
|
});
|
|
1052
1553
|
|
|
1053
1554
|
// src/memory-embedder-tokenizer.ts
|
|
1054
|
-
import { readFileSync as
|
|
1555
|
+
import { readFileSync as readFileSync5 } from "fs";
|
|
1055
1556
|
function loadVocab(vocabPath) {
|
|
1056
|
-
const lines =
|
|
1557
|
+
const lines = readFileSync5(vocabPath, "utf-8").split("\n");
|
|
1057
1558
|
const vocab = /* @__PURE__ */ new Map();
|
|
1058
1559
|
for (let i = 0; i < lines.length; i++) {
|
|
1059
1560
|
const tok = lines[i].replace(/\r$/, "");
|
|
@@ -1173,9 +1674,9 @@ var init_memory_embedder_tokenizer = __esm({
|
|
|
1173
1674
|
|
|
1174
1675
|
// src/memory-embedder.ts
|
|
1175
1676
|
import { fileURLToPath } from "url";
|
|
1176
|
-
import { dirname as
|
|
1177
|
-
import { existsSync as
|
|
1178
|
-
import { createRequire } from "module";
|
|
1677
|
+
import { dirname as dirname5, join as join4 } from "path";
|
|
1678
|
+
import { existsSync as existsSync6 } from "fs";
|
|
1679
|
+
import { createRequire as createRequire3 } from "module";
|
|
1179
1680
|
function getActiveEmbedModel() {
|
|
1180
1681
|
return _activeModel;
|
|
1181
1682
|
}
|
|
@@ -1196,21 +1697,21 @@ function embeddingsDisabled() {
|
|
|
1196
1697
|
}
|
|
1197
1698
|
function resolveModelDir() {
|
|
1198
1699
|
try {
|
|
1199
|
-
let dir =
|
|
1700
|
+
let dir = dirname5(fileURLToPath(import.meta.url));
|
|
1200
1701
|
let root = null;
|
|
1201
1702
|
for (let i = 0; i < 8; i++) {
|
|
1202
|
-
if (
|
|
1703
|
+
if (existsSync6(join4(dir, "package.json"))) {
|
|
1203
1704
|
root = dir;
|
|
1204
1705
|
break;
|
|
1205
1706
|
}
|
|
1206
|
-
const parent =
|
|
1707
|
+
const parent = dirname5(dir);
|
|
1207
1708
|
if (parent === dir) break;
|
|
1208
1709
|
dir = parent;
|
|
1209
1710
|
}
|
|
1210
1711
|
if (!root) return null;
|
|
1211
1712
|
for (const rel of ["dist/embedder", "assets/embedder"]) {
|
|
1212
|
-
const candidate =
|
|
1213
|
-
if (
|
|
1713
|
+
const candidate = join4(root, rel);
|
|
1714
|
+
if (existsSync6(join4(candidate, "model_quantized.onnx"))) return candidate;
|
|
1214
1715
|
}
|
|
1215
1716
|
return null;
|
|
1216
1717
|
} catch {
|
|
@@ -1219,9 +1720,9 @@ function resolveModelDir() {
|
|
|
1219
1720
|
}
|
|
1220
1721
|
function resolveWasmDir() {
|
|
1221
1722
|
try {
|
|
1222
|
-
const
|
|
1223
|
-
const main2 =
|
|
1224
|
-
return
|
|
1723
|
+
const req3 = createRequire3(import.meta.url);
|
|
1724
|
+
const main2 = req3.resolve(ORT_PKG);
|
|
1725
|
+
return dirname5(main2);
|
|
1225
1726
|
} catch {
|
|
1226
1727
|
return null;
|
|
1227
1728
|
}
|
|
@@ -1241,8 +1742,8 @@ async function getSession() {
|
|
|
1241
1742
|
ort.env.wasm.proxy = false;
|
|
1242
1743
|
ort.env.wasm.wasmPaths = wasmDir.endsWith("/") ? wasmDir : wasmDir + "/";
|
|
1243
1744
|
_ortTensor = ort.Tensor;
|
|
1244
|
-
const modelPath =
|
|
1245
|
-
const vocabPath =
|
|
1745
|
+
const modelPath = join4(modelDir, "model_quantized.onnx");
|
|
1746
|
+
const vocabPath = join4(modelDir, "vocab.txt");
|
|
1246
1747
|
_vocab = loadVocab(vocabPath);
|
|
1247
1748
|
const session = await ort.InferenceSession.create(modelPath, {
|
|
1248
1749
|
executionProviders: ["wasm"],
|
|
@@ -1563,6 +2064,7 @@ __export(memory_db_exports, {
|
|
|
1563
2064
|
migrateAuditLogCheckExtension: () => migrateAuditLogCheckExtension,
|
|
1564
2065
|
migrateMemoryFilesFor4B: () => migrateMemoryFilesFor4B,
|
|
1565
2066
|
migrateObservationEmbeddingChunks: () => migrateObservationEmbeddingChunks,
|
|
2067
|
+
migrateSharedMemoryFor5B: () => migrateSharedMemoryFor5B,
|
|
1566
2068
|
pruneOldConversationTurns: () => pruneOldConversationTurns,
|
|
1567
2069
|
pruneOldObservations: () => pruneOldObservations,
|
|
1568
2070
|
pruneToolCostEvents: () => pruneToolCostEvents,
|
|
@@ -1579,9 +2081,8 @@ __export(memory_db_exports, {
|
|
|
1579
2081
|
upsertObservationEmbedding: () => upsertObservationEmbedding,
|
|
1580
2082
|
usageWarmupElapsed: () => usageWarmupElapsed
|
|
1581
2083
|
});
|
|
1582
|
-
import
|
|
1583
|
-
import {
|
|
1584
|
-
import { existsSync as existsSync5, mkdirSync as mkdirSync3 } from "fs";
|
|
2084
|
+
import { dirname as dirname6, basename as basename2 } from "path";
|
|
2085
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync5 } from "fs";
|
|
1585
2086
|
function sanitizeFts5Query(raw) {
|
|
1586
2087
|
const trimmed = raw.trim();
|
|
1587
2088
|
if (!trimmed) return '""';
|
|
@@ -1597,12 +2098,12 @@ function sanitizeFts5QueryOr(raw) {
|
|
|
1597
2098
|
}
|
|
1598
2099
|
function getMemoryDb() {
|
|
1599
2100
|
const dbPath = getResolvedPaths().memoryDbPath;
|
|
1600
|
-
const dir =
|
|
1601
|
-
if (!
|
|
1602
|
-
|
|
2101
|
+
const dir = dirname6(dbPath);
|
|
2102
|
+
if (!existsSync7(dir)) {
|
|
2103
|
+
mkdirSync5(dir, { recursive: true });
|
|
1603
2104
|
}
|
|
1604
|
-
const preExisting =
|
|
1605
|
-
const db =
|
|
2105
|
+
const preExisting = existsSync7(dbPath);
|
|
2106
|
+
const db = openDatabase2(dbPath);
|
|
1606
2107
|
db.pragma("journal_mode = WAL");
|
|
1607
2108
|
db.pragma("foreign_keys = ON");
|
|
1608
2109
|
const onDisk = db.pragma("user_version", { simple: true });
|
|
@@ -1678,6 +2179,39 @@ function migrateMemoryFilesFor4B(db) {
|
|
|
1678
2179
|
db.exec(`ALTER TABLE ${add.table} ADD COLUMN ${add.name} ${add.decl}`);
|
|
1679
2180
|
}
|
|
1680
2181
|
}
|
|
2182
|
+
function migrateSharedMemoryFor5B(db) {
|
|
2183
|
+
const cols = db.prepare(`PRAGMA table_info(observations)`).all();
|
|
2184
|
+
if (cols.length > 0 && !cols.some((c) => c.name === "shareable")) {
|
|
2185
|
+
db.exec(`ALTER TABLE observations ADD COLUMN shareable INTEGER NOT NULL DEFAULT 0`);
|
|
2186
|
+
}
|
|
2187
|
+
db.exec(`
|
|
2188
|
+
CREATE TABLE IF NOT EXISTS shared_memory_pending (
|
|
2189
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
2190
|
+
record_hash TEXT NOT NULL UNIQUE, -- identity + idempotency key (hex sha256)
|
|
2191
|
+
origin_repo_id TEXT NOT NULL, -- the signed origin repo_id (v4 UUID)
|
|
2192
|
+
origin_repo_label TEXT NOT NULL, -- re-slugged on read; NEVER trusted from the wire
|
|
2193
|
+
envelope_raw TEXT NOT NULL, -- the VERBATIM signed bytes; B-05 re-verifies these
|
|
2194
|
+
record_json TEXT NOT NULL, -- the single record's canonical JSON
|
|
2195
|
+
received_at_epoch INTEGER NOT NULL, -- epoch SECONDS (Slice-2 convention)
|
|
2196
|
+
accepted_at_epoch INTEGER, -- set on accept (B-05); NULL while pending
|
|
2197
|
+
refused_at_epoch INTEGER, -- set on refuse (B-06)
|
|
2198
|
+
expired_at_epoch INTEGER -- revocation \u21D2 EXPIRE, never DELETE (B-07 / S-3)
|
|
2199
|
+
);
|
|
2200
|
+
CREATE INDEX IF NOT EXISTS idx_smp_origin ON shared_memory_pending(origin_repo_id);
|
|
2201
|
+
CREATE INDEX IF NOT EXISTS idx_smp_received ON shared_memory_pending(received_at_epoch DESC);
|
|
2202
|
+
`);
|
|
2203
|
+
db.exec(`
|
|
2204
|
+
CREATE TABLE IF NOT EXISTS shared_memory_outbound (
|
|
2205
|
+
record_hash TEXT PRIMARY KEY, -- the exported record's canonical hash
|
|
2206
|
+
observation_id INTEGER NOT NULL, -- the source observations.id (for B-07 revocation)
|
|
2207
|
+
origin_repo_id TEXT NOT NULL, -- this repo's own repo_id at export time
|
|
2208
|
+
seq INTEGER NOT NULL, -- the envelope seq this record first went out in
|
|
2209
|
+
exported_at_epoch INTEGER NOT NULL, -- epoch SECONDS
|
|
2210
|
+
revoked_at_epoch INTEGER -- set when the source row is superseded/expired (B-07)
|
|
2211
|
+
);
|
|
2212
|
+
CREATE INDEX IF NOT EXISTS idx_smo_obs ON shared_memory_outbound(observation_id);
|
|
2213
|
+
`);
|
|
2214
|
+
}
|
|
1681
2215
|
function migrateAuditLogCheckExtension(db) {
|
|
1682
2216
|
const row = db.prepare("SELECT sql FROM sqlite_master WHERE type='table' AND name='audit_log'").get();
|
|
1683
2217
|
if (!row) return;
|
|
@@ -1700,7 +2234,16 @@ function migrateAuditLogCheckExtension(db) {
|
|
|
1700
2234
|
"memory_file_adopted_human",
|
|
1701
2235
|
"memory_file_rendered",
|
|
1702
2236
|
"memory_file_render_refused",
|
|
1703
|
-
"memory_file_tombstoned"
|
|
2237
|
+
"memory_file_tombstoned",
|
|
2238
|
+
// B-10 (Slice 5) — the seven cross-repo shared-memory events. An existing DB whose
|
|
2239
|
+
// CHECK lacks any of these triggers the table rebuild below.
|
|
2240
|
+
"shared_memory_exported",
|
|
2241
|
+
"shared_memory_export_refused",
|
|
2242
|
+
"shared_memory_imported",
|
|
2243
|
+
"shared_memory_dropped",
|
|
2244
|
+
"shared_memory_accepted",
|
|
2245
|
+
"shared_memory_refused",
|
|
2246
|
+
"shared_memory_revoked"
|
|
1704
2247
|
];
|
|
1705
2248
|
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);
|
|
1706
2249
|
if (checkClauseMatch) {
|
|
@@ -1719,7 +2262,12 @@ function migrateAuditLogCheckExtension(db) {
|
|
|
1719
2262
|
'code_change', 'rule_enforced', 'approval', 'review', 'commit', 'compaction',
|
|
1720
2263
|
'rule_candidate_emitted', 'rule_promoted', 'rule_dismissed',
|
|
1721
2264
|
'memory_file_ingested', 'memory_file_expired', 'memory_file_adopted_human',
|
|
1722
|
-
'memory_file_rendered', 'memory_file_render_refused', 'memory_file_tombstoned'
|
|
2265
|
+
'memory_file_rendered', 'memory_file_render_refused', 'memory_file_tombstoned',
|
|
2266
|
+
-- B-10 (Slice 5) \u2014 cross-repo shared-memory events (must mirror the CREATE +
|
|
2267
|
+
-- the expected[] list above, or the migration would loop rebuilding forever).
|
|
2268
|
+
'shared_memory_exported', 'shared_memory_export_refused', 'shared_memory_imported',
|
|
2269
|
+
'shared_memory_dropped', 'shared_memory_accepted', 'shared_memory_refused',
|
|
2270
|
+
'shared_memory_revoked'
|
|
1723
2271
|
)),
|
|
1724
2272
|
actor TEXT NOT NULL DEFAULT 'ai' CHECK(actor IN ('ai', 'human', 'hook', 'agent')),
|
|
1725
2273
|
model_id TEXT,
|
|
@@ -1796,6 +2344,12 @@ function initMemorySchema(db) {
|
|
|
1796
2344
|
-- credential, or takes a snapshot. A Slice-5 synced memory arrives here with
|
|
1797
2345
|
-- origin='team' and must never reach disk without CR-55's gates.
|
|
1798
2346
|
origin TEXT NOT NULL DEFAULT 'local',
|
|
2347
|
+
-- B-01 (Slice 5, cross-repo surfacing): the per-decision SHARE opt-in. Set to 1
|
|
2348
|
+
-- ONLY by an explicit human act (massu memory share <id>); NEVER by a heuristic,
|
|
2349
|
+
-- score, LLM, hook, or consolidation pass. Export (B-02) reads WHERE shareable=1
|
|
2350
|
+
-- AND origin='local'. Defaults 0 so every pre-existing and machine-written row is
|
|
2351
|
+
-- un-shareable until a human says otherwise (fail-closed).
|
|
2352
|
+
shareable INTEGER NOT NULL DEFAULT 0,
|
|
1799
2353
|
created_at TEXT NOT NULL,
|
|
1800
2354
|
created_at_epoch INTEGER NOT NULL,
|
|
1801
2355
|
FOREIGN KEY(session_id) REFERENCES sessions(session_id) ON DELETE CASCADE
|
|
@@ -2069,7 +2623,13 @@ function initMemorySchema(db) {
|
|
|
2069
2623
|
'code_change', 'rule_enforced', 'approval', 'review', 'commit', 'compaction',
|
|
2070
2624
|
'rule_candidate_emitted', 'rule_promoted', 'rule_dismissed',
|
|
2071
2625
|
'memory_file_ingested', 'memory_file_expired', 'memory_file_adopted_human',
|
|
2072
|
-
'memory_file_rendered', 'memory_file_render_refused', 'memory_file_tombstoned'
|
|
2626
|
+
'memory_file_rendered', 'memory_file_render_refused', 'memory_file_tombstoned',
|
|
2627
|
+
-- B-10 (Slice 5): cross-repo shared-memory observability. Without these seven,
|
|
2628
|
+
-- nobody could ever answer "what has crossed between my repos?" (and an ingest
|
|
2629
|
+
-- catch would swallow the CHECK violation, silently producing NOTHING).
|
|
2630
|
+
'shared_memory_exported', 'shared_memory_export_refused', 'shared_memory_imported',
|
|
2631
|
+
'shared_memory_dropped', 'shared_memory_accepted', 'shared_memory_refused',
|
|
2632
|
+
'shared_memory_revoked'
|
|
2073
2633
|
)),
|
|
2074
2634
|
actor TEXT NOT NULL DEFAULT 'ai' CHECK(actor IN ('ai', 'human', 'hook', 'agent')),
|
|
2075
2635
|
model_id TEXT,
|
|
@@ -2438,6 +2998,7 @@ function initMemorySchema(db) {
|
|
|
2438
2998
|
`);
|
|
2439
2999
|
migrateObservationEmbeddingChunks(db);
|
|
2440
3000
|
migrateMemoryFilesFor4B(db);
|
|
3001
|
+
migrateSharedMemoryFor5B(db);
|
|
2441
3002
|
ensureRuleCandidatesTable(db);
|
|
2442
3003
|
migrateRuleDelivery(db);
|
|
2443
3004
|
}
|
|
@@ -3308,6 +3869,7 @@ var MEMORY_SCHEMA_VERSION, TOOL_COST_EVENTS_RETENTION_DAYS, MAX_DRAFT_TEXT_LEN,
|
|
|
3308
3869
|
var init_memory_db = __esm({
|
|
3309
3870
|
"src/memory-db.ts"() {
|
|
3310
3871
|
"use strict";
|
|
3872
|
+
init_db_driver();
|
|
3311
3873
|
init_config();
|
|
3312
3874
|
init_memory_vector();
|
|
3313
3875
|
init_db_backup();
|
|
@@ -3328,21 +3890,21 @@ var init_memory_db = __esm({
|
|
|
3328
3890
|
});
|
|
3329
3891
|
|
|
3330
3892
|
// src/hooks/lib/hook-failure-signal.ts
|
|
3331
|
-
import { appendFileSync, mkdirSync as
|
|
3332
|
-
import { join as
|
|
3893
|
+
import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync6, existsSync as existsSync8 } from "fs";
|
|
3894
|
+
import { join as join5, dirname as dirname7 } from "path";
|
|
3333
3895
|
function resolveFailureLogPath() {
|
|
3334
3896
|
const explicit = process.env.MASSU_HOOK_FAILURE_LOG;
|
|
3335
3897
|
if (explicit) return explicit;
|
|
3336
3898
|
let dir = process.cwd();
|
|
3337
3899
|
for (let i = 0; i < 12; i++) {
|
|
3338
|
-
if (
|
|
3339
|
-
return
|
|
3900
|
+
if (existsSync8(join5(dir, ".massu")) || existsSync8(join5(dir, "massu.config.yaml"))) {
|
|
3901
|
+
return join5(dir, ".massu", "hook-failures.jsonl");
|
|
3340
3902
|
}
|
|
3341
|
-
const parent =
|
|
3903
|
+
const parent = dirname7(dir);
|
|
3342
3904
|
if (parent === dir) break;
|
|
3343
3905
|
dir = parent;
|
|
3344
3906
|
}
|
|
3345
|
-
return
|
|
3907
|
+
return join5(process.cwd(), ".massu", "hook-failures.jsonl");
|
|
3346
3908
|
}
|
|
3347
3909
|
function recordHookFailure(hook, error, context) {
|
|
3348
3910
|
const record = {
|
|
@@ -3355,8 +3917,8 @@ function recordHookFailure(hook, error, context) {
|
|
|
3355
3917
|
let wroteSomething = false;
|
|
3356
3918
|
try {
|
|
3357
3919
|
const path = resolveFailureLogPath();
|
|
3358
|
-
|
|
3359
|
-
|
|
3920
|
+
mkdirSync6(dirname7(path), { recursive: true });
|
|
3921
|
+
appendFileSync2(path, JSON.stringify(record) + "\n", "utf-8");
|
|
3360
3922
|
wroteSomething = true;
|
|
3361
3923
|
} catch {
|
|
3362
3924
|
}
|
|
@@ -3400,7 +3962,7 @@ var init_hook_failure_signal = __esm({
|
|
|
3400
3962
|
|
|
3401
3963
|
// src/hooks/rule-enforcement-pipeline.ts
|
|
3402
3964
|
init_config();
|
|
3403
|
-
import { existsSync as
|
|
3965
|
+
import { existsSync as existsSync9, readFileSync as readFileSync6, readdirSync as readdirSync2 } from "fs";
|
|
3404
3966
|
import { basename as basename3, resolve as resolve5 } from "path";
|
|
3405
3967
|
|
|
3406
3968
|
// src/hooks/lib/write-hook-message.ts
|
|
@@ -3438,7 +4000,7 @@ async function main() {
|
|
|
3438
4000
|
process.exit(0);
|
|
3439
4001
|
return;
|
|
3440
4002
|
}
|
|
3441
|
-
if (!
|
|
4003
|
+
if (!existsSync9(filePath)) {
|
|
3442
4004
|
process.exit(0);
|
|
3443
4005
|
return;
|
|
3444
4006
|
}
|
|
@@ -3446,18 +4008,18 @@ async function main() {
|
|
|
3446
4008
|
process.exit(0);
|
|
3447
4009
|
return;
|
|
3448
4010
|
}
|
|
3449
|
-
const content =
|
|
4011
|
+
const content = readFileSync6(filePath, "utf-8");
|
|
3450
4012
|
const nameMatch = content.match(/^name:\s*(.+)/m);
|
|
3451
4013
|
const descMatch = content.match(/^description:\s*(.+)/m);
|
|
3452
4014
|
const ruleName = nameMatch?.[1]?.trim() ?? fileName;
|
|
3453
4015
|
const ruleDesc = descMatch?.[1]?.trim() ?? "";
|
|
3454
4016
|
const enforcementDirAbs = resolve5(root, enforcementDir);
|
|
3455
4017
|
let hasEnforcement = false;
|
|
3456
|
-
if (
|
|
4018
|
+
if (existsSync9(enforcementDirAbs)) {
|
|
3457
4019
|
const hookFiles = readdirSync2(enforcementDirAbs).filter((f) => f.endsWith(".sh") || f.endsWith(".ts") || f.endsWith(".js"));
|
|
3458
4020
|
for (const hookFile of hookFiles) {
|
|
3459
4021
|
try {
|
|
3460
|
-
const hookContent =
|
|
4022
|
+
const hookContent = readFileSync6(resolve5(enforcementDirAbs, hookFile), "utf-8");
|
|
3461
4023
|
if (hookContent.includes(fileName)) {
|
|
3462
4024
|
hasEnforcement = true;
|
|
3463
4025
|
break;
|