@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.
@@ -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
- enabled: z2.boolean().default(false),
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 Database from "better-sqlite3";
831
- import { existsSync as existsSync3, mkdirSync as mkdirSync2, readdirSync, statSync, unlinkSync, copyFileSync, rmSync as rmSync2 } from "fs";
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 join(dbBackupsRoot(home), slug);
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 (!existsSync3(dir)) return [];
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 = join(dir, f);
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 (existsSync3(dbPath) && statSync(dbPath).mtimeMs > newest.mtimeMs) return false;
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 = new Database(dbPath, { readonly: true });
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 (!existsSync3(dbPath)) {
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
- mkdirSync2(dir, { recursive: true });
1386
+ mkdirSync4(dir, { recursive: true });
887
1387
  const name = basename(dbPath, ".db");
888
- const dest = join(dir, `${name}-${backupStamp(nowMs)}.db`);
1388
+ const dest = join3(dir, `${name}-${backupStamp(nowMs)}.db`);
889
1389
  let src = null;
890
1390
  try {
891
- src = new Database(dbPath, { readonly: true });
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 (!existsSync3(dbPath)) return null;
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,21 +1552,21 @@ var init_rule_candidate_store = __esm({
1051
1552
  });
1052
1553
 
1053
1554
  // src/hooks/lib/hook-failure-signal.ts
1054
- import { appendFileSync, mkdirSync as mkdirSync3, existsSync as existsSync4 } from "fs";
1055
- import { join as join2, dirname as dirname3 } from "path";
1555
+ import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync5, existsSync as existsSync6 } from "fs";
1556
+ import { join as join4, dirname as dirname5 } from "path";
1056
1557
  function resolveFailureLogPath() {
1057
1558
  const explicit = process.env.MASSU_HOOK_FAILURE_LOG;
1058
1559
  if (explicit) return explicit;
1059
1560
  let dir = process.cwd();
1060
1561
  for (let i = 0; i < 12; i++) {
1061
- if (existsSync4(join2(dir, ".massu")) || existsSync4(join2(dir, "massu.config.yaml"))) {
1062
- return join2(dir, ".massu", "hook-failures.jsonl");
1562
+ if (existsSync6(join4(dir, ".massu")) || existsSync6(join4(dir, "massu.config.yaml"))) {
1563
+ return join4(dir, ".massu", "hook-failures.jsonl");
1063
1564
  }
1064
- const parent = dirname3(dir);
1565
+ const parent = dirname5(dir);
1065
1566
  if (parent === dir) break;
1066
1567
  dir = parent;
1067
1568
  }
1068
- return join2(process.cwd(), ".massu", "hook-failures.jsonl");
1569
+ return join4(process.cwd(), ".massu", "hook-failures.jsonl");
1069
1570
  }
1070
1571
  function recordHookFailure(hook, error, context) {
1071
1572
  const record = {
@@ -1078,8 +1579,8 @@ function recordHookFailure(hook, error, context) {
1078
1579
  let wroteSomething = false;
1079
1580
  try {
1080
1581
  const path = resolveFailureLogPath();
1081
- mkdirSync3(dirname3(path), { recursive: true });
1082
- appendFileSync(path, JSON.stringify(record) + "\n", "utf-8");
1582
+ mkdirSync5(dirname5(path), { recursive: true });
1583
+ appendFileSync2(path, JSON.stringify(record) + "\n", "utf-8");
1083
1584
  wroteSomething = true;
1084
1585
  } catch {
1085
1586
  }
@@ -1122,9 +1623,9 @@ var init_hook_failure_signal = __esm({
1122
1623
  });
1123
1624
 
1124
1625
  // src/memory-embedder-tokenizer.ts
1125
- import { readFileSync as readFileSync3 } from "fs";
1626
+ import { readFileSync as readFileSync5 } from "fs";
1126
1627
  function loadVocab(vocabPath) {
1127
- const lines = readFileSync3(vocabPath, "utf-8").split("\n");
1628
+ const lines = readFileSync5(vocabPath, "utf-8").split("\n");
1128
1629
  const vocab = /* @__PURE__ */ new Map();
1129
1630
  for (let i = 0; i < lines.length; i++) {
1130
1631
  const tok = lines[i].replace(/\r$/, "");
@@ -1244,9 +1745,9 @@ var init_memory_embedder_tokenizer = __esm({
1244
1745
 
1245
1746
  // src/memory-embedder.ts
1246
1747
  import { fileURLToPath } from "url";
1247
- import { dirname as dirname4, join as join3 } from "path";
1248
- import { existsSync as existsSync5 } from "fs";
1249
- import { createRequire } from "module";
1748
+ import { dirname as dirname6, join as join5 } from "path";
1749
+ import { existsSync as existsSync7 } from "fs";
1750
+ import { createRequire as createRequire3 } from "module";
1250
1751
  function getActiveEmbedModel() {
1251
1752
  return _activeModel;
1252
1753
  }
@@ -1267,21 +1768,21 @@ function embeddingsDisabled() {
1267
1768
  }
1268
1769
  function resolveModelDir() {
1269
1770
  try {
1270
- let dir = dirname4(fileURLToPath(import.meta.url));
1771
+ let dir = dirname6(fileURLToPath(import.meta.url));
1271
1772
  let root = null;
1272
1773
  for (let i = 0; i < 8; i++) {
1273
- if (existsSync5(join3(dir, "package.json"))) {
1774
+ if (existsSync7(join5(dir, "package.json"))) {
1274
1775
  root = dir;
1275
1776
  break;
1276
1777
  }
1277
- const parent = dirname4(dir);
1778
+ const parent = dirname6(dir);
1278
1779
  if (parent === dir) break;
1279
1780
  dir = parent;
1280
1781
  }
1281
1782
  if (!root) return null;
1282
1783
  for (const rel of ["dist/embedder", "assets/embedder"]) {
1283
- const candidate = join3(root, rel);
1284
- if (existsSync5(join3(candidate, "model_quantized.onnx"))) return candidate;
1784
+ const candidate = join5(root, rel);
1785
+ if (existsSync7(join5(candidate, "model_quantized.onnx"))) return candidate;
1285
1786
  }
1286
1787
  return null;
1287
1788
  } catch {
@@ -1290,9 +1791,9 @@ function resolveModelDir() {
1290
1791
  }
1291
1792
  function resolveWasmDir() {
1292
1793
  try {
1293
- const req = createRequire(import.meta.url);
1294
- const main2 = req.resolve(ORT_PKG);
1295
- return dirname4(main2);
1794
+ const req3 = createRequire3(import.meta.url);
1795
+ const main2 = req3.resolve(ORT_PKG);
1796
+ return dirname6(main2);
1296
1797
  } catch {
1297
1798
  return null;
1298
1799
  }
@@ -1312,8 +1813,8 @@ async function getSession() {
1312
1813
  ort.env.wasm.proxy = false;
1313
1814
  ort.env.wasm.wasmPaths = wasmDir.endsWith("/") ? wasmDir : wasmDir + "/";
1314
1815
  _ortTensor = ort.Tensor;
1315
- const modelPath = join3(modelDir, "model_quantized.onnx");
1316
- const vocabPath = join3(modelDir, "vocab.txt");
1816
+ const modelPath = join5(modelDir, "model_quantized.onnx");
1817
+ const vocabPath = join5(modelDir, "vocab.txt");
1317
1818
  _vocab = loadVocab(vocabPath);
1318
1819
  const session = await ort.InferenceSession.create(modelPath, {
1319
1820
  executionProviders: ["wasm"],
@@ -1634,6 +2135,7 @@ __export(memory_db_exports, {
1634
2135
  migrateAuditLogCheckExtension: () => migrateAuditLogCheckExtension,
1635
2136
  migrateMemoryFilesFor4B: () => migrateMemoryFilesFor4B,
1636
2137
  migrateObservationEmbeddingChunks: () => migrateObservationEmbeddingChunks,
2138
+ migrateSharedMemoryFor5B: () => migrateSharedMemoryFor5B,
1637
2139
  pruneOldConversationTurns: () => pruneOldConversationTurns,
1638
2140
  pruneOldObservations: () => pruneOldObservations,
1639
2141
  pruneToolCostEvents: () => pruneToolCostEvents,
@@ -1650,9 +2152,8 @@ __export(memory_db_exports, {
1650
2152
  upsertObservationEmbedding: () => upsertObservationEmbedding,
1651
2153
  usageWarmupElapsed: () => usageWarmupElapsed
1652
2154
  });
1653
- import Database2 from "better-sqlite3";
1654
- import { dirname as dirname5, basename as basename2 } from "path";
1655
- import { existsSync as existsSync6, mkdirSync as mkdirSync4 } from "fs";
2155
+ import { dirname as dirname7, basename as basename2 } from "path";
2156
+ import { existsSync as existsSync8, mkdirSync as mkdirSync6 } from "fs";
1656
2157
  function sanitizeFts5Query(raw) {
1657
2158
  const trimmed = raw.trim();
1658
2159
  if (!trimmed) return '""';
@@ -1668,12 +2169,12 @@ function sanitizeFts5QueryOr(raw) {
1668
2169
  }
1669
2170
  function getMemoryDb() {
1670
2171
  const dbPath = getResolvedPaths().memoryDbPath;
1671
- const dir = dirname5(dbPath);
1672
- if (!existsSync6(dir)) {
1673
- mkdirSync4(dir, { recursive: true });
2172
+ const dir = dirname7(dbPath);
2173
+ if (!existsSync8(dir)) {
2174
+ mkdirSync6(dir, { recursive: true });
1674
2175
  }
1675
- const preExisting = existsSync6(dbPath);
1676
- const db = new Database2(dbPath);
2176
+ const preExisting = existsSync8(dbPath);
2177
+ const db = openDatabase2(dbPath);
1677
2178
  db.pragma("journal_mode = WAL");
1678
2179
  db.pragma("foreign_keys = ON");
1679
2180
  const onDisk = db.pragma("user_version", { simple: true });
@@ -1749,6 +2250,39 @@ function migrateMemoryFilesFor4B(db) {
1749
2250
  db.exec(`ALTER TABLE ${add.table} ADD COLUMN ${add.name} ${add.decl}`);
1750
2251
  }
1751
2252
  }
2253
+ function migrateSharedMemoryFor5B(db) {
2254
+ const cols = db.prepare(`PRAGMA table_info(observations)`).all();
2255
+ if (cols.length > 0 && !cols.some((c) => c.name === "shareable")) {
2256
+ db.exec(`ALTER TABLE observations ADD COLUMN shareable INTEGER NOT NULL DEFAULT 0`);
2257
+ }
2258
+ db.exec(`
2259
+ CREATE TABLE IF NOT EXISTS shared_memory_pending (
2260
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
2261
+ record_hash TEXT NOT NULL UNIQUE, -- identity + idempotency key (hex sha256)
2262
+ origin_repo_id TEXT NOT NULL, -- the signed origin repo_id (v4 UUID)
2263
+ origin_repo_label TEXT NOT NULL, -- re-slugged on read; NEVER trusted from the wire
2264
+ envelope_raw TEXT NOT NULL, -- the VERBATIM signed bytes; B-05 re-verifies these
2265
+ record_json TEXT NOT NULL, -- the single record's canonical JSON
2266
+ received_at_epoch INTEGER NOT NULL, -- epoch SECONDS (Slice-2 convention)
2267
+ accepted_at_epoch INTEGER, -- set on accept (B-05); NULL while pending
2268
+ refused_at_epoch INTEGER, -- set on refuse (B-06)
2269
+ expired_at_epoch INTEGER -- revocation \u21D2 EXPIRE, never DELETE (B-07 / S-3)
2270
+ );
2271
+ CREATE INDEX IF NOT EXISTS idx_smp_origin ON shared_memory_pending(origin_repo_id);
2272
+ CREATE INDEX IF NOT EXISTS idx_smp_received ON shared_memory_pending(received_at_epoch DESC);
2273
+ `);
2274
+ db.exec(`
2275
+ CREATE TABLE IF NOT EXISTS shared_memory_outbound (
2276
+ record_hash TEXT PRIMARY KEY, -- the exported record's canonical hash
2277
+ observation_id INTEGER NOT NULL, -- the source observations.id (for B-07 revocation)
2278
+ origin_repo_id TEXT NOT NULL, -- this repo's own repo_id at export time
2279
+ seq INTEGER NOT NULL, -- the envelope seq this record first went out in
2280
+ exported_at_epoch INTEGER NOT NULL, -- epoch SECONDS
2281
+ revoked_at_epoch INTEGER -- set when the source row is superseded/expired (B-07)
2282
+ );
2283
+ CREATE INDEX IF NOT EXISTS idx_smo_obs ON shared_memory_outbound(observation_id);
2284
+ `);
2285
+ }
1752
2286
  function migrateAuditLogCheckExtension(db) {
1753
2287
  const row = db.prepare("SELECT sql FROM sqlite_master WHERE type='table' AND name='audit_log'").get();
1754
2288
  if (!row) return;
@@ -1771,7 +2305,16 @@ function migrateAuditLogCheckExtension(db) {
1771
2305
  "memory_file_adopted_human",
1772
2306
  "memory_file_rendered",
1773
2307
  "memory_file_render_refused",
1774
- "memory_file_tombstoned"
2308
+ "memory_file_tombstoned",
2309
+ // B-10 (Slice 5) — the seven cross-repo shared-memory events. An existing DB whose
2310
+ // CHECK lacks any of these triggers the table rebuild below.
2311
+ "shared_memory_exported",
2312
+ "shared_memory_export_refused",
2313
+ "shared_memory_imported",
2314
+ "shared_memory_dropped",
2315
+ "shared_memory_accepted",
2316
+ "shared_memory_refused",
2317
+ "shared_memory_revoked"
1775
2318
  ];
1776
2319
  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);
1777
2320
  if (checkClauseMatch) {
@@ -1790,7 +2333,12 @@ function migrateAuditLogCheckExtension(db) {
1790
2333
  'code_change', 'rule_enforced', 'approval', 'review', 'commit', 'compaction',
1791
2334
  'rule_candidate_emitted', 'rule_promoted', 'rule_dismissed',
1792
2335
  'memory_file_ingested', 'memory_file_expired', 'memory_file_adopted_human',
1793
- 'memory_file_rendered', 'memory_file_render_refused', 'memory_file_tombstoned'
2336
+ 'memory_file_rendered', 'memory_file_render_refused', 'memory_file_tombstoned',
2337
+ -- B-10 (Slice 5) \u2014 cross-repo shared-memory events (must mirror the CREATE +
2338
+ -- the expected[] list above, or the migration would loop rebuilding forever).
2339
+ 'shared_memory_exported', 'shared_memory_export_refused', 'shared_memory_imported',
2340
+ 'shared_memory_dropped', 'shared_memory_accepted', 'shared_memory_refused',
2341
+ 'shared_memory_revoked'
1794
2342
  )),
1795
2343
  actor TEXT NOT NULL DEFAULT 'ai' CHECK(actor IN ('ai', 'human', 'hook', 'agent')),
1796
2344
  model_id TEXT,
@@ -1867,6 +2415,12 @@ function initMemorySchema(db) {
1867
2415
  -- credential, or takes a snapshot. A Slice-5 synced memory arrives here with
1868
2416
  -- origin='team' and must never reach disk without CR-55's gates.
1869
2417
  origin TEXT NOT NULL DEFAULT 'local',
2418
+ -- B-01 (Slice 5, cross-repo surfacing): the per-decision SHARE opt-in. Set to 1
2419
+ -- ONLY by an explicit human act (massu memory share <id>); NEVER by a heuristic,
2420
+ -- score, LLM, hook, or consolidation pass. Export (B-02) reads WHERE shareable=1
2421
+ -- AND origin='local'. Defaults 0 so every pre-existing and machine-written row is
2422
+ -- un-shareable until a human says otherwise (fail-closed).
2423
+ shareable INTEGER NOT NULL DEFAULT 0,
1870
2424
  created_at TEXT NOT NULL,
1871
2425
  created_at_epoch INTEGER NOT NULL,
1872
2426
  FOREIGN KEY(session_id) REFERENCES sessions(session_id) ON DELETE CASCADE
@@ -2140,7 +2694,13 @@ function initMemorySchema(db) {
2140
2694
  'code_change', 'rule_enforced', 'approval', 'review', 'commit', 'compaction',
2141
2695
  'rule_candidate_emitted', 'rule_promoted', 'rule_dismissed',
2142
2696
  'memory_file_ingested', 'memory_file_expired', 'memory_file_adopted_human',
2143
- 'memory_file_rendered', 'memory_file_render_refused', 'memory_file_tombstoned'
2697
+ 'memory_file_rendered', 'memory_file_render_refused', 'memory_file_tombstoned',
2698
+ -- B-10 (Slice 5): cross-repo shared-memory observability. Without these seven,
2699
+ -- nobody could ever answer "what has crossed between my repos?" (and an ingest
2700
+ -- catch would swallow the CHECK violation, silently producing NOTHING).
2701
+ 'shared_memory_exported', 'shared_memory_export_refused', 'shared_memory_imported',
2702
+ 'shared_memory_dropped', 'shared_memory_accepted', 'shared_memory_refused',
2703
+ 'shared_memory_revoked'
2144
2704
  )),
2145
2705
  actor TEXT NOT NULL DEFAULT 'ai' CHECK(actor IN ('ai', 'human', 'hook', 'agent')),
2146
2706
  model_id TEXT,
@@ -2509,6 +3069,7 @@ function initMemorySchema(db) {
2509
3069
  `);
2510
3070
  migrateObservationEmbeddingChunks(db);
2511
3071
  migrateMemoryFilesFor4B(db);
3072
+ migrateSharedMemoryFor5B(db);
2512
3073
  ensureRuleCandidatesTable(db);
2513
3074
  migrateRuleDelivery(db);
2514
3075
  }
@@ -3379,6 +3940,7 @@ var MEMORY_SCHEMA_VERSION, TOOL_COST_EVENTS_RETENTION_DAYS, MAX_DRAFT_TEXT_LEN,
3379
3940
  var init_memory_db = __esm({
3380
3941
  "src/memory-db.ts"() {
3381
3942
  "use strict";
3943
+ init_db_driver();
3382
3944
  init_config();
3383
3945
  init_memory_vector();
3384
3946
  init_db_backup();
@@ -3401,9 +3963,9 @@ var init_memory_db = __esm({
3401
3963
  // src/hooks/classify-failure.ts
3402
3964
  init_config();
3403
3965
  init_memory_db();
3404
- import { existsSync as existsSync7, readFileSync as readFileSync4, readdirSync as readdirSync2, unlinkSync as unlinkSync2, writeFileSync as writeFileSync2 } from "fs";
3405
- import { tmpdir } from "os";
3406
- import { join as join4, basename as basename3 } from "path";
3966
+ import { existsSync as existsSync9, readFileSync as readFileSync6, readdirSync as readdirSync2, unlinkSync as unlinkSync2, writeFileSync as writeFileSync3 } from "fs";
3967
+ import { tmpdir as tmpdir2 } from "os";
3968
+ import { join as join6, basename as basename3 } from "path";
3407
3969
 
3408
3970
  // src/hooks/lib/write-hook-message.ts
3409
3971
  function writeHookMessage(message) {
@@ -3419,7 +3981,7 @@ var CODE_EXTENSIONS = /\.(ts|tsx|js|jsx|py|swift|rs|go|rb|sh)$/;
3419
3981
  function getDedupeMarkerPath(sessionId, filePath) {
3420
3982
  const day = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
3421
3983
  const hash = simpleHash(filePath);
3422
- return join4(tmpdir(), `massu-classify-${day}-${sessionId.slice(0, 8)}-${hash}`);
3984
+ return join6(tmpdir2(), `massu-classify-${day}-${sessionId.slice(0, 8)}-${hash}`);
3423
3985
  }
3424
3986
  function simpleHash(str) {
3425
3987
  let h = 0;
@@ -3429,13 +3991,13 @@ function simpleHash(str) {
3429
3991
  return Math.abs(h).toString(36);
3430
3992
  }
3431
3993
  function readFailureContextFiles() {
3432
- const dir = tmpdir();
3994
+ const dir = tmpdir2();
3433
3995
  let context = "";
3434
3996
  try {
3435
3997
  const files = readdirSync2(dir).filter((f) => f.startsWith("massu-failure-context-"));
3436
3998
  for (const file of files) {
3437
3999
  try {
3438
- context += " " + readFileSync4(join4(dir, file), "utf-8");
4000
+ context += " " + readFileSync6(join6(dir, file), "utf-8");
3439
4001
  } catch {
3440
4002
  }
3441
4003
  }
@@ -3444,12 +4006,12 @@ function readFailureContextFiles() {
3444
4006
  return context.trim();
3445
4007
  }
3446
4008
  function cleanupFailureContextFiles() {
3447
- const dir = tmpdir();
4009
+ const dir = tmpdir2();
3448
4010
  try {
3449
4011
  const files = readdirSync2(dir).filter((f) => f.startsWith("massu-failure-context-"));
3450
4012
  for (const file of files) {
3451
4013
  try {
3452
- unlinkSync2(join4(dir, file));
4014
+ unlinkSync2(join6(dir, file));
3453
4015
  } catch {
3454
4016
  }
3455
4017
  }
@@ -3511,12 +4073,12 @@ async function main() {
3511
4073
  return;
3512
4074
  }
3513
4075
  const dedupeMarker = getDedupeMarkerPath(hookInput.session_id, filePath);
3514
- if (existsSync7(dedupeMarker)) {
4076
+ if (existsSync9(dedupeMarker)) {
3515
4077
  process.exit(0);
3516
4078
  return;
3517
4079
  }
3518
4080
  try {
3519
- writeFileSync2(dedupeMarker, "1");
4081
+ writeFileSync3(dedupeMarker, "1");
3520
4082
  } catch {
3521
4083
  }
3522
4084
  const db = getMemoryDb();