@massu/core 1.16.1 → 1.16.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +95 -0
- package/dist/cli.js +1090 -697
- package/dist/hooks/auto-learning-pipeline.js +401 -55
- package/dist/hooks/classify-failure.js +399 -53
- package/dist/hooks/cost-tracker.js +411 -65
- package/dist/hooks/fix-detector.js +410 -64
- package/dist/hooks/incident-pipeline.js +396 -50
- package/dist/hooks/intent-suggester.js +411 -65
- package/dist/hooks/memory-recall.js +415 -69
- package/dist/hooks/post-edit-context.js +413 -67
- package/dist/hooks/post-tool-use.js +439 -93
- package/dist/hooks/pre-compact.js +411 -65
- package/dist/hooks/pre-delete-check.js +415 -69
- package/dist/hooks/pre-tool-use-gate.js +415 -69
- package/dist/hooks/quality-event.js +411 -65
- package/dist/hooks/rule-enforcement-pipeline.js +397 -51
- package/dist/hooks/security-gate.js +411 -65
- package/dist/hooks/session-end.js +441 -95
- package/dist/hooks/session-start.js +533 -283
- package/dist/hooks/user-prompt.js +445 -99
- package/package.json +1 -1
|
@@ -366,9 +366,13 @@ Hint: run \`massu config refresh\` to regenerate a valid config or fix the liste
|
|
|
366
366
|
const resolvedEndpoint = resolveEndpoint({ configEndpoint: _config.cloud?.endpoint });
|
|
367
367
|
if (resolvedKey.apiKey) {
|
|
368
368
|
_config.cloud = {
|
|
369
|
-
enabled: true,
|
|
370
369
|
sync: { memory: true, analytics: true, audit: true },
|
|
371
370
|
..._config.cloud,
|
|
371
|
+
// Spread FIRST, then decide `enabled`, so a workspace that declares a `cloud:`
|
|
372
|
+
// block only to tune `requestTimeoutMs` does not disable its own sync. An
|
|
373
|
+
// EXPLICIT `enabled: false` is still honoured; `undefined` (not stated) means
|
|
374
|
+
// "a key resolved, so turn it on". (plan-2026-07-20-cloud-sync-timeout)
|
|
375
|
+
enabled: _config.cloud?.enabled ?? true,
|
|
372
376
|
apiKey: resolvedKey.apiKey,
|
|
373
377
|
endpoint: resolvedEndpoint
|
|
374
378
|
};
|
|
@@ -588,9 +592,22 @@ var init_config = __esm({
|
|
|
588
592
|
})).default([])
|
|
589
593
|
}).optional();
|
|
590
594
|
CloudConfigSchema = z2.object({
|
|
591
|
-
|
|
595
|
+
// OPTIONAL, not `.default(false)`. A zod default is indistinguishable from an
|
|
596
|
+
// explicit value once parsed, so `.default(false)` meant that merely declaring a
|
|
597
|
+
// `cloud:` block (to set any OTHER key) silently produced `enabled: false`, which
|
|
598
|
+
// then overrode the `enabled: true` auto-enable below via object spread — turning
|
|
599
|
+
// cloud sync OFF as a side effect of tuning it. `undefined` now means "not stated",
|
|
600
|
+
// and the auto-enable preserves it. (plan-2026-07-20-cloud-sync-timeout)
|
|
601
|
+
enabled: z2.boolean().optional(),
|
|
592
602
|
apiKey: z2.string().optional(),
|
|
593
603
|
endpoint: z2.string().optional(),
|
|
604
|
+
// Per-request POST budget for the `/sync` ingest. cloud-sync.ts reads this
|
|
605
|
+
// (`(cloud as { requestTimeoutMs?: number }).requestTimeoutMs`) but it was ABSENT
|
|
606
|
+
// from this schema, and zod strips unknown keys — so the knob was unreachable from
|
|
607
|
+
// massu.config.yaml and the default could never be tuned. Measured 2026-07-20: a
|
|
608
|
+
// 423-observation payload takes ~9.2s against the live ingest. Capped at
|
|
609
|
+
// SYNC_DEADLINE_MS (20s) since the overall deadline clamps each attempt anyway.
|
|
610
|
+
requestTimeoutMs: z2.number().int().positive().max(2e4).optional(),
|
|
594
611
|
sync: z2.object({
|
|
595
612
|
memory: z2.boolean().default(true),
|
|
596
613
|
analytics: z2.boolean().default(true),
|
|
@@ -807,6 +824,335 @@ var init_config = __esm({
|
|
|
807
824
|
}
|
|
808
825
|
});
|
|
809
826
|
|
|
827
|
+
// src/lib/fileLock.ts
|
|
828
|
+
import { mkdirSync as mkdirSync2, readFileSync as readFileSync3, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
829
|
+
import { dirname as dirname2 } from "path";
|
|
830
|
+
import * as lockfile from "proper-lockfile";
|
|
831
|
+
function readLockHolderPid(lockPath) {
|
|
832
|
+
try {
|
|
833
|
+
const raw = readFileSync3(`${lockPath}.pid`, "utf-8").trim();
|
|
834
|
+
const pid = Number.parseInt(raw, 10);
|
|
835
|
+
if (!Number.isFinite(pid) || pid <= 0) return null;
|
|
836
|
+
return pid;
|
|
837
|
+
} catch {
|
|
838
|
+
return null;
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
function busyWaitSync(ms) {
|
|
842
|
+
if (typeof SharedArrayBuffer === "undefined" || typeof Atomics === "undefined") {
|
|
843
|
+
throw new Error(
|
|
844
|
+
`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.`
|
|
845
|
+
);
|
|
846
|
+
}
|
|
847
|
+
const sab = new SharedArrayBuffer(4);
|
|
848
|
+
const view = new Int32Array(sab);
|
|
849
|
+
Atomics.wait(view, 0, 0, ms);
|
|
850
|
+
}
|
|
851
|
+
function withFileLockSync(lockPath, fn, opts = {}) {
|
|
852
|
+
mkdirSync2(dirname2(lockPath), { recursive: true });
|
|
853
|
+
const staleMs = opts.staleMs ?? 3e4;
|
|
854
|
+
const blockMs = opts.retries === 0 ? 0 : opts.blockMs ?? 3e4;
|
|
855
|
+
const pollIntervalMs = opts.pollIntervalMs ?? 100;
|
|
856
|
+
const now = opts.now ?? Date.now;
|
|
857
|
+
const sleep = opts.sleep ?? busyWaitSync;
|
|
858
|
+
const makeBusyError = opts.errorFactory ?? ((path, pid, retrySeconds, code) => new FileLockBusyError(path, pid, retrySeconds, code));
|
|
859
|
+
let release = null;
|
|
860
|
+
const deadline = now() + blockMs;
|
|
861
|
+
for (; ; ) {
|
|
862
|
+
try {
|
|
863
|
+
release = lockfile.lockSync(lockPath, {
|
|
864
|
+
stale: staleMs,
|
|
865
|
+
retries: 0,
|
|
866
|
+
realpath: false
|
|
867
|
+
});
|
|
868
|
+
try {
|
|
869
|
+
writeFileSync2(`${lockPath}.pid`, String(process.pid), "utf-8");
|
|
870
|
+
} catch {
|
|
871
|
+
}
|
|
872
|
+
break;
|
|
873
|
+
} catch (err) {
|
|
874
|
+
const e = err;
|
|
875
|
+
const code = e.code;
|
|
876
|
+
if (code !== "ELOCKED" && code !== "EBUSY") {
|
|
877
|
+
throw err;
|
|
878
|
+
}
|
|
879
|
+
if (now() >= deadline) {
|
|
880
|
+
const holderPid = readLockHolderPid(lockPath);
|
|
881
|
+
const remainingMs = Math.max(0, deadline - now());
|
|
882
|
+
const retryAfterSeconds = blockMs === 0 ? Math.round(staleMs / 1e3) : Math.round(remainingMs / 1e3);
|
|
883
|
+
throw makeBusyError(lockPath, holderPid, retryAfterSeconds, code);
|
|
884
|
+
}
|
|
885
|
+
sleep(pollIntervalMs);
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
try {
|
|
889
|
+
return fn();
|
|
890
|
+
} finally {
|
|
891
|
+
try {
|
|
892
|
+
if (release) release();
|
|
893
|
+
} catch {
|
|
894
|
+
}
|
|
895
|
+
try {
|
|
896
|
+
rmSync2(`${lockPath}.pid`, { force: true });
|
|
897
|
+
} catch {
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
var FileLockBusyError;
|
|
902
|
+
var init_fileLock = __esm({
|
|
903
|
+
"src/lib/fileLock.ts"() {
|
|
904
|
+
"use strict";
|
|
905
|
+
FileLockBusyError = class extends Error {
|
|
906
|
+
constructor(lockPath, holderPid, retryAfterSeconds, causeCode) {
|
|
907
|
+
const pidPart = holderPid != null ? `(PID=${holderPid})` : "(PID=unknown)";
|
|
908
|
+
super(`File lock at ${lockPath} held by another process ${pidPart} \u2014 try again in ${retryAfterSeconds}s`);
|
|
909
|
+
this.lockPath = lockPath;
|
|
910
|
+
this.holderPid = holderPid;
|
|
911
|
+
this.retryAfterSeconds = retryAfterSeconds;
|
|
912
|
+
this.causeCode = causeCode;
|
|
913
|
+
this.name = "FileLockBusyError";
|
|
914
|
+
}
|
|
915
|
+
lockPath;
|
|
916
|
+
holderPid;
|
|
917
|
+
retryAfterSeconds;
|
|
918
|
+
causeCode;
|
|
919
|
+
};
|
|
920
|
+
}
|
|
921
|
+
});
|
|
922
|
+
|
|
923
|
+
// src/lib/sqlite-loader.ts
|
|
924
|
+
import { createRequire } from "module";
|
|
925
|
+
import { spawnSync } from "child_process";
|
|
926
|
+
import { accessSync, appendFileSync, chmodSync as chmodSync2, constants as fsConstants, existsSync as existsSync3, mkdirSync as mkdirSync3 } from "fs";
|
|
927
|
+
import { dirname as dirname3, join } from "path";
|
|
928
|
+
function isNativeAbiError(err) {
|
|
929
|
+
const code = err?.code;
|
|
930
|
+
if (code === "ERR_DLOPEN_FAILED") return true;
|
|
931
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
932
|
+
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);
|
|
933
|
+
}
|
|
934
|
+
function loadBetterSqlite3(opts = {}) {
|
|
935
|
+
if (_testCtor) return _testCtor;
|
|
936
|
+
if (opts.fresh) bustNativeCache();
|
|
937
|
+
return req("better-sqlite3");
|
|
938
|
+
}
|
|
939
|
+
function bustNativeCache() {
|
|
940
|
+
try {
|
|
941
|
+
const cache = req.cache;
|
|
942
|
+
if (!cache) return;
|
|
943
|
+
for (const key of Object.keys(cache)) {
|
|
944
|
+
if (/better[-_]sqlite3|better_sqlite3\.node|[\\/]bindings[\\/]/.test(key)) {
|
|
945
|
+
delete cache[key];
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
} catch {
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
function openDatabase(dbPath, opts = {}) {
|
|
952
|
+
const { selfHeal: selfHealOpt = true, ...ctorOpts } = opts;
|
|
953
|
+
const selfHeal = selfHealOpt && process.env.MASSU_HOOK_RUNTIME !== "1";
|
|
954
|
+
const Ctor = loadBetterSqlite3();
|
|
955
|
+
try {
|
|
956
|
+
return new Ctor(dbPath, ctorOpts);
|
|
957
|
+
} catch (err) {
|
|
958
|
+
if (!isNativeAbiError(err)) {
|
|
959
|
+
throw err;
|
|
960
|
+
}
|
|
961
|
+
if (!selfHeal) {
|
|
962
|
+
recordHealEvent({ phase: "skipped", reason: "self-heal-disabled", abiTo: process.versions.modules });
|
|
963
|
+
throw new MemoryEngineUnusableError("abi-mismatch", detailOf(err));
|
|
964
|
+
}
|
|
965
|
+
const result = (_testHeal ?? attemptNativeHeal)(err);
|
|
966
|
+
if (!result.healed && !result.contended) {
|
|
967
|
+
throw new MemoryEngineUnusableError(result.reason ?? "heal-failed", result.detail ?? detailOf(err));
|
|
968
|
+
}
|
|
969
|
+
try {
|
|
970
|
+
const FreshCtor = loadBetterSqlite3({ fresh: true });
|
|
971
|
+
return new FreshCtor(dbPath, ctorOpts);
|
|
972
|
+
} catch (retryErr) {
|
|
973
|
+
throw new MemoryEngineUnusableError("heal-failed", detailOf(retryErr));
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
function attemptNativeHeal(err) {
|
|
978
|
+
const start = Date.now();
|
|
979
|
+
const abiTo = process.versions.modules;
|
|
980
|
+
const abiFrom = parseAbiFrom(err);
|
|
981
|
+
let pkgDir;
|
|
982
|
+
try {
|
|
983
|
+
pkgDir = dirname3(req.resolve("better-sqlite3/package.json"));
|
|
984
|
+
} catch {
|
|
985
|
+
recordHealEvent({ phase: "skipped", reason: "not-resolvable", abiFrom, abiTo });
|
|
986
|
+
return { healed: false, reason: "missing", abiFrom, abiTo, detail: "better-sqlite3 not resolvable" };
|
|
987
|
+
}
|
|
988
|
+
if (!isWritable(pkgDir)) {
|
|
989
|
+
recordHealEvent({ phase: "skipped", reason: "dir-not-writable", abiFrom, abiTo });
|
|
990
|
+
return { healed: false, reason: "heal-failed", abiFrom, abiTo, detail: `install dir not writable: ${pkgDir}` };
|
|
991
|
+
}
|
|
992
|
+
recordHealEvent({ phase: "attempt", reason: "abi-mismatch", abiFrom, abiTo });
|
|
993
|
+
try {
|
|
994
|
+
return withFileLockSync(
|
|
995
|
+
join(credentialsDir(), "native-heal.lock"),
|
|
996
|
+
() => runRebuild(pkgDir, abiFrom, abiTo, start),
|
|
997
|
+
{ blockMs: 6e4, staleMs: 3e5 }
|
|
998
|
+
);
|
|
999
|
+
} catch (lockErr) {
|
|
1000
|
+
const detail = lockErr instanceof Error ? lockErr.message : String(lockErr);
|
|
1001
|
+
recordHealEvent({ phase: "failed", reason: "heal-failed", abiFrom, abiTo, detail: `lock: ${detail}` });
|
|
1002
|
+
return { healed: false, contended: true, reason: "heal-failed", abiFrom, abiTo, detail: `heal lock contended: ${detail}` };
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
function runRebuild(pkgDir, abiFrom, abiTo, start) {
|
|
1006
|
+
try {
|
|
1007
|
+
const FreshCtor = loadBetterSqlite3({ fresh: true });
|
|
1008
|
+
new FreshCtor(":memory:").close();
|
|
1009
|
+
const res2 = {
|
|
1010
|
+
healed: true,
|
|
1011
|
+
abiFrom,
|
|
1012
|
+
abiTo,
|
|
1013
|
+
durationMs: Date.now() - start,
|
|
1014
|
+
detail: "already healed by a concurrent process"
|
|
1015
|
+
};
|
|
1016
|
+
recordHealEvent({ phase: "success", reason: "already-healed", abiFrom, abiTo, durationMs: res2.durationMs });
|
|
1017
|
+
return res2;
|
|
1018
|
+
} catch (probeErr) {
|
|
1019
|
+
if (!isNativeAbiError(probeErr)) {
|
|
1020
|
+
return { healed: false, reason: "heal-failed", abiFrom, abiTo, durationMs: Date.now() - start, detail: detailOf(probeErr) };
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
const prebuildBin = resolveLocalBin(pkgDir, "prebuild-install");
|
|
1024
|
+
if (prebuildBin) {
|
|
1025
|
+
const r = spawnSync(process.execPath, [prebuildBin], {
|
|
1026
|
+
cwd: pkgDir,
|
|
1027
|
+
encoding: "utf-8",
|
|
1028
|
+
timeout: 12e4
|
|
1029
|
+
});
|
|
1030
|
+
if (r.status === 0) {
|
|
1031
|
+
const res2 = { healed: true, method: "prebuild-install", abiFrom, abiTo, durationMs: Date.now() - start };
|
|
1032
|
+
recordHealEvent({ phase: "success", reason: "abi-mismatch", ...res2 });
|
|
1033
|
+
return res2;
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
const gypBin = resolveNodeGypBin();
|
|
1037
|
+
if (gypBin && hasCompiler()) {
|
|
1038
|
+
const r = spawnSync(process.execPath, [gypBin, "rebuild", "--release"], {
|
|
1039
|
+
cwd: pkgDir,
|
|
1040
|
+
encoding: "utf-8",
|
|
1041
|
+
timeout: 3e5
|
|
1042
|
+
});
|
|
1043
|
+
if (r.status === 0) {
|
|
1044
|
+
const res2 = { healed: true, method: "node-gyp", abiFrom, abiTo, durationMs: Date.now() - start };
|
|
1045
|
+
recordHealEvent({ phase: "success", reason: "abi-mismatch", ...res2 });
|
|
1046
|
+
return res2;
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
const res = {
|
|
1050
|
+
healed: false,
|
|
1051
|
+
reason: "heal-failed",
|
|
1052
|
+
abiFrom,
|
|
1053
|
+
abiTo,
|
|
1054
|
+
durationMs: Date.now() - start,
|
|
1055
|
+
detail: prebuildBin ? "prebuild-install failed; node-gyp fallback unavailable or failed" : "no prebuild-install bin resolvable"
|
|
1056
|
+
};
|
|
1057
|
+
recordHealEvent({ phase: "failed", reason: "heal-failed", abiFrom, abiTo, durationMs: res.durationMs });
|
|
1058
|
+
return res;
|
|
1059
|
+
}
|
|
1060
|
+
function recordHealEvent(event) {
|
|
1061
|
+
try {
|
|
1062
|
+
const rec = {
|
|
1063
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1064
|
+
node: process.versions.node,
|
|
1065
|
+
platform: process.platform,
|
|
1066
|
+
arch: process.arch,
|
|
1067
|
+
...event
|
|
1068
|
+
};
|
|
1069
|
+
const dir = credentialsDir();
|
|
1070
|
+
mkdirSync3(dir, { recursive: true });
|
|
1071
|
+
const file = join(dir, "native-heal-events.jsonl");
|
|
1072
|
+
appendFileSync(file, JSON.stringify(rec) + "\n", "utf-8");
|
|
1073
|
+
try {
|
|
1074
|
+
chmodSync2(file, 384);
|
|
1075
|
+
} catch {
|
|
1076
|
+
}
|
|
1077
|
+
} catch {
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
function parseAbiFrom(err) {
|
|
1081
|
+
const msg = err instanceof Error ? err.message : String(err ?? "");
|
|
1082
|
+
const m = /NODE_MODULE_VERSION (\d+)/.exec(msg);
|
|
1083
|
+
if (m && /^\d+$/.test(m[1])) return m[1];
|
|
1084
|
+
return void 0;
|
|
1085
|
+
}
|
|
1086
|
+
function detailOf(err) {
|
|
1087
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1088
|
+
return msg.slice(0, 300);
|
|
1089
|
+
}
|
|
1090
|
+
function isWritable(dir) {
|
|
1091
|
+
try {
|
|
1092
|
+
accessSync(dir, fsConstants.W_OK);
|
|
1093
|
+
return true;
|
|
1094
|
+
} catch {
|
|
1095
|
+
return false;
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
function resolveLocalBin(pkgDir, name) {
|
|
1099
|
+
const candidates = [
|
|
1100
|
+
join(pkgDir, "node_modules", ".bin", name),
|
|
1101
|
+
// nested install
|
|
1102
|
+
join(pkgDir, "..", ".bin", name),
|
|
1103
|
+
// hoisted (node_modules/.bin)
|
|
1104
|
+
join(pkgDir, "..", "..", "node_modules", ".bin", name)
|
|
1105
|
+
// workspace hoist
|
|
1106
|
+
];
|
|
1107
|
+
for (const c of candidates) {
|
|
1108
|
+
if (existsSync3(c)) return c;
|
|
1109
|
+
}
|
|
1110
|
+
return void 0;
|
|
1111
|
+
}
|
|
1112
|
+
function resolveNodeGypBin() {
|
|
1113
|
+
try {
|
|
1114
|
+
return req.resolve("node-gyp/bin/node-gyp.js");
|
|
1115
|
+
} catch {
|
|
1116
|
+
return void 0;
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
function hasCompiler() {
|
|
1120
|
+
for (const cc of ["cc", "clang", "gcc"]) {
|
|
1121
|
+
try {
|
|
1122
|
+
const r = spawnSync(cc, ["--version"], { encoding: "utf-8", timeout: 5e3 });
|
|
1123
|
+
if (r.status === 0) return true;
|
|
1124
|
+
} catch {
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
return false;
|
|
1128
|
+
}
|
|
1129
|
+
var req, NATIVE_DB_REMEDY, MemoryEngineUnusableError, _testCtor, _testHeal;
|
|
1130
|
+
var init_sqlite_loader = __esm({
|
|
1131
|
+
"src/lib/sqlite-loader.ts"() {
|
|
1132
|
+
"use strict";
|
|
1133
|
+
init_credentials();
|
|
1134
|
+
init_fileLock();
|
|
1135
|
+
req = createRequire(import.meta.url);
|
|
1136
|
+
NATIVE_DB_REMEDY = "Run 'massu heal' to rebuild the database engine for your Node version, then restart your MCP client / Claude Code.";
|
|
1137
|
+
MemoryEngineUnusableError = class extends Error {
|
|
1138
|
+
reason;
|
|
1139
|
+
remedy;
|
|
1140
|
+
detail;
|
|
1141
|
+
constructor(reason, detail) {
|
|
1142
|
+
super(
|
|
1143
|
+
`Massu memory engine is unusable (${reason}). ${NATIVE_DB_REMEDY}` + (detail ? ` [${detail}]` : "")
|
|
1144
|
+
);
|
|
1145
|
+
this.name = "MemoryEngineUnusableError";
|
|
1146
|
+
this.reason = reason;
|
|
1147
|
+
this.remedy = NATIVE_DB_REMEDY;
|
|
1148
|
+
this.detail = detail;
|
|
1149
|
+
}
|
|
1150
|
+
};
|
|
1151
|
+
_testCtor = null;
|
|
1152
|
+
_testHeal = null;
|
|
1153
|
+
}
|
|
1154
|
+
});
|
|
1155
|
+
|
|
810
1156
|
// src/memory-vector.ts
|
|
811
1157
|
function float32ToBlob(vec) {
|
|
812
1158
|
return Buffer.from(new Uint8Array(vec.buffer, vec.byteOffset, vec.byteLength));
|
|
@@ -827,27 +1173,26 @@ var init_memory_vector = __esm({
|
|
|
827
1173
|
});
|
|
828
1174
|
|
|
829
1175
|
// src/db-backup.ts
|
|
830
|
-
import
|
|
831
|
-
import {
|
|
832
|
-
import { resolve as resolve3, join, basename, dirname as dirname2 } from "path";
|
|
1176
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync4, readdirSync, statSync, unlinkSync, copyFileSync, rmSync as rmSync3 } from "fs";
|
|
1177
|
+
import { resolve as resolve3, join as join2, basename, dirname as dirname4 } from "path";
|
|
833
1178
|
import { homedir as homedir3 } from "os";
|
|
834
1179
|
function dbBackupsRoot(home = homedir3()) {
|
|
835
1180
|
return resolve3(home, ".massu", "db-backups");
|
|
836
1181
|
}
|
|
837
1182
|
function projectBackupDir(projectRoot, home = homedir3()) {
|
|
838
1183
|
const slug = basename(projectRoot).replace(/[^A-Za-z0-9._-]/g, "_") || "project";
|
|
839
|
-
return
|
|
1184
|
+
return join2(dbBackupsRoot(home), slug);
|
|
840
1185
|
}
|
|
841
1186
|
function backupStamp(nowMs) {
|
|
842
1187
|
return new Date(nowMs).toISOString().replace(/[:.]/g, "-");
|
|
843
1188
|
}
|
|
844
1189
|
function listDbBackups(projectRoot, home = homedir3()) {
|
|
845
1190
|
const dir = projectBackupDir(projectRoot, home);
|
|
846
|
-
if (!
|
|
1191
|
+
if (!existsSync4(dir)) return [];
|
|
847
1192
|
const out = [];
|
|
848
1193
|
for (const f of readdirSync(dir)) {
|
|
849
1194
|
if (!f.endsWith(".db")) continue;
|
|
850
|
-
const p =
|
|
1195
|
+
const p = join2(dir, f);
|
|
851
1196
|
const st = statSync(p);
|
|
852
1197
|
const db = f.replace(/-\d{4}-\d{2}-\d{2}T.*$/, "");
|
|
853
1198
|
out.push({ db, path: p, bytes: st.size, mtimeMs: st.mtimeMs });
|
|
@@ -860,13 +1205,13 @@ function hasFreshDbBackup(projectRoot, dbPath, nowMs = Date.now(), home = homedi
|
|
|
860
1205
|
if (backups.length === 0) return false;
|
|
861
1206
|
const newest = backups[0];
|
|
862
1207
|
if (nowMs - newest.mtimeMs > FRESH_WINDOW_MS) return false;
|
|
863
|
-
if (
|
|
1208
|
+
if (existsSync4(dbPath) && statSync(dbPath).mtimeMs > newest.mtimeMs) return false;
|
|
864
1209
|
return true;
|
|
865
1210
|
}
|
|
866
1211
|
function integrityOk(dbPath) {
|
|
867
1212
|
let db = null;
|
|
868
1213
|
try {
|
|
869
|
-
db =
|
|
1214
|
+
db = openDatabase(dbPath, { readonly: true });
|
|
870
1215
|
const rows = db.pragma("integrity_check");
|
|
871
1216
|
return rows.length === 1 && rows[0].integrity_check === "ok";
|
|
872
1217
|
} catch {
|
|
@@ -879,16 +1224,16 @@ function integrityOk(dbPath) {
|
|
|
879
1224
|
}
|
|
880
1225
|
}
|
|
881
1226
|
function backupDb(projectRoot, dbPath, nowMs = Date.now(), home = homedir3()) {
|
|
882
|
-
if (!
|
|
1227
|
+
if (!existsSync4(dbPath)) {
|
|
883
1228
|
throw new DbBackupError(`cannot back up a database that does not exist: ${dbPath}`);
|
|
884
1229
|
}
|
|
885
1230
|
const dir = projectBackupDir(projectRoot, home);
|
|
886
|
-
|
|
1231
|
+
mkdirSync4(dir, { recursive: true });
|
|
887
1232
|
const name = basename(dbPath, ".db");
|
|
888
|
-
const dest =
|
|
1233
|
+
const dest = join2(dir, `${name}-${backupStamp(nowMs)}.db`);
|
|
889
1234
|
let src = null;
|
|
890
1235
|
try {
|
|
891
|
-
src =
|
|
1236
|
+
src = openDatabase(dbPath, { readonly: true });
|
|
892
1237
|
src.exec(`VACUUM INTO '${dest.replace(/'/g, "''")}'`);
|
|
893
1238
|
} catch (err) {
|
|
894
1239
|
throw new DbBackupError(
|
|
@@ -927,7 +1272,7 @@ function pruneDbBackups(projectRoot, dbName, keep = DEFAULT_RETENTION, home = ho
|
|
|
927
1272
|
}
|
|
928
1273
|
function backupBeforeSchemaChange(projectRoot, dbPath, onError, nowMs = Date.now(), home = homedir3()) {
|
|
929
1274
|
try {
|
|
930
|
-
if (!
|
|
1275
|
+
if (!existsSync4(dbPath)) return null;
|
|
931
1276
|
if (hasFreshDbBackup(projectRoot, dbPath, nowMs, home)) return null;
|
|
932
1277
|
return backupDb(projectRoot, dbPath, nowMs, home);
|
|
933
1278
|
} catch (err) {
|
|
@@ -939,6 +1284,7 @@ var DEFAULT_RETENTION, FRESH_WINDOW_MS, DbBackupError;
|
|
|
939
1284
|
var init_db_backup = __esm({
|
|
940
1285
|
"src/db-backup.ts"() {
|
|
941
1286
|
"use strict";
|
|
1287
|
+
init_sqlite_loader();
|
|
942
1288
|
DEFAULT_RETENTION = 5;
|
|
943
1289
|
FRESH_WINDOW_MS = 24 * 60 * 60 * 1e3;
|
|
944
1290
|
DbBackupError = class extends Error {
|
|
@@ -1051,9 +1397,9 @@ var init_rule_candidate_store = __esm({
|
|
|
1051
1397
|
});
|
|
1052
1398
|
|
|
1053
1399
|
// src/memory-embedder-tokenizer.ts
|
|
1054
|
-
import { readFileSync as
|
|
1400
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
1055
1401
|
function loadVocab(vocabPath) {
|
|
1056
|
-
const lines =
|
|
1402
|
+
const lines = readFileSync4(vocabPath, "utf-8").split("\n");
|
|
1057
1403
|
const vocab = /* @__PURE__ */ new Map();
|
|
1058
1404
|
for (let i = 0; i < lines.length; i++) {
|
|
1059
1405
|
const tok = lines[i].replace(/\r$/, "");
|
|
@@ -1173,9 +1519,9 @@ var init_memory_embedder_tokenizer = __esm({
|
|
|
1173
1519
|
|
|
1174
1520
|
// src/memory-embedder.ts
|
|
1175
1521
|
import { fileURLToPath } from "url";
|
|
1176
|
-
import { dirname as
|
|
1177
|
-
import { existsSync as
|
|
1178
|
-
import { createRequire } from "module";
|
|
1522
|
+
import { dirname as dirname5, join as join3 } from "path";
|
|
1523
|
+
import { existsSync as existsSync5 } from "fs";
|
|
1524
|
+
import { createRequire as createRequire2 } from "module";
|
|
1179
1525
|
function getActiveEmbedModel() {
|
|
1180
1526
|
return _activeModel;
|
|
1181
1527
|
}
|
|
@@ -1196,21 +1542,21 @@ function embeddingsDisabled() {
|
|
|
1196
1542
|
}
|
|
1197
1543
|
function resolveModelDir() {
|
|
1198
1544
|
try {
|
|
1199
|
-
let dir =
|
|
1545
|
+
let dir = dirname5(fileURLToPath(import.meta.url));
|
|
1200
1546
|
let root = null;
|
|
1201
1547
|
for (let i = 0; i < 8; i++) {
|
|
1202
|
-
if (
|
|
1548
|
+
if (existsSync5(join3(dir, "package.json"))) {
|
|
1203
1549
|
root = dir;
|
|
1204
1550
|
break;
|
|
1205
1551
|
}
|
|
1206
|
-
const parent =
|
|
1552
|
+
const parent = dirname5(dir);
|
|
1207
1553
|
if (parent === dir) break;
|
|
1208
1554
|
dir = parent;
|
|
1209
1555
|
}
|
|
1210
1556
|
if (!root) return null;
|
|
1211
1557
|
for (const rel of ["dist/embedder", "assets/embedder"]) {
|
|
1212
|
-
const candidate =
|
|
1213
|
-
if (
|
|
1558
|
+
const candidate = join3(root, rel);
|
|
1559
|
+
if (existsSync5(join3(candidate, "model_quantized.onnx"))) return candidate;
|
|
1214
1560
|
}
|
|
1215
1561
|
return null;
|
|
1216
1562
|
} catch {
|
|
@@ -1219,9 +1565,9 @@ function resolveModelDir() {
|
|
|
1219
1565
|
}
|
|
1220
1566
|
function resolveWasmDir() {
|
|
1221
1567
|
try {
|
|
1222
|
-
const
|
|
1223
|
-
const main2 =
|
|
1224
|
-
return
|
|
1568
|
+
const req2 = createRequire2(import.meta.url);
|
|
1569
|
+
const main2 = req2.resolve(ORT_PKG);
|
|
1570
|
+
return dirname5(main2);
|
|
1225
1571
|
} catch {
|
|
1226
1572
|
return null;
|
|
1227
1573
|
}
|
|
@@ -1241,8 +1587,8 @@ async function getSession() {
|
|
|
1241
1587
|
ort.env.wasm.proxy = false;
|
|
1242
1588
|
ort.env.wasm.wasmPaths = wasmDir.endsWith("/") ? wasmDir : wasmDir + "/";
|
|
1243
1589
|
_ortTensor = ort.Tensor;
|
|
1244
|
-
const modelPath =
|
|
1245
|
-
const vocabPath =
|
|
1590
|
+
const modelPath = join3(modelDir, "model_quantized.onnx");
|
|
1591
|
+
const vocabPath = join3(modelDir, "vocab.txt");
|
|
1246
1592
|
_vocab = loadVocab(vocabPath);
|
|
1247
1593
|
const session = await ort.InferenceSession.create(modelPath, {
|
|
1248
1594
|
executionProviders: ["wasm"],
|
|
@@ -1579,9 +1925,8 @@ __export(memory_db_exports, {
|
|
|
1579
1925
|
upsertObservationEmbedding: () => upsertObservationEmbedding,
|
|
1580
1926
|
usageWarmupElapsed: () => usageWarmupElapsed
|
|
1581
1927
|
});
|
|
1582
|
-
import
|
|
1583
|
-
import {
|
|
1584
|
-
import { existsSync as existsSync5, mkdirSync as mkdirSync3 } from "fs";
|
|
1928
|
+
import { dirname as dirname6, basename as basename2 } from "path";
|
|
1929
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync5 } from "fs";
|
|
1585
1930
|
function sanitizeFts5Query(raw) {
|
|
1586
1931
|
const trimmed = raw.trim();
|
|
1587
1932
|
if (!trimmed) return '""';
|
|
@@ -1597,12 +1942,12 @@ function sanitizeFts5QueryOr(raw) {
|
|
|
1597
1942
|
}
|
|
1598
1943
|
function getMemoryDb() {
|
|
1599
1944
|
const dbPath = getResolvedPaths().memoryDbPath;
|
|
1600
|
-
const dir =
|
|
1601
|
-
if (!
|
|
1602
|
-
|
|
1945
|
+
const dir = dirname6(dbPath);
|
|
1946
|
+
if (!existsSync6(dir)) {
|
|
1947
|
+
mkdirSync5(dir, { recursive: true });
|
|
1603
1948
|
}
|
|
1604
|
-
const preExisting =
|
|
1605
|
-
const db =
|
|
1949
|
+
const preExisting = existsSync6(dbPath);
|
|
1950
|
+
const db = openDatabase(dbPath);
|
|
1606
1951
|
db.pragma("journal_mode = WAL");
|
|
1607
1952
|
db.pragma("foreign_keys = ON");
|
|
1608
1953
|
const onDisk = db.pragma("user_version", { simple: true });
|
|
@@ -3308,6 +3653,7 @@ var MEMORY_SCHEMA_VERSION, TOOL_COST_EVENTS_RETENTION_DAYS, MAX_DRAFT_TEXT_LEN,
|
|
|
3308
3653
|
var init_memory_db = __esm({
|
|
3309
3654
|
"src/memory-db.ts"() {
|
|
3310
3655
|
"use strict";
|
|
3656
|
+
init_sqlite_loader();
|
|
3311
3657
|
init_config();
|
|
3312
3658
|
init_memory_vector();
|
|
3313
3659
|
init_db_backup();
|
|
@@ -3328,21 +3674,21 @@ var init_memory_db = __esm({
|
|
|
3328
3674
|
});
|
|
3329
3675
|
|
|
3330
3676
|
// src/hooks/lib/hook-failure-signal.ts
|
|
3331
|
-
import { appendFileSync, mkdirSync as
|
|
3332
|
-
import { join as
|
|
3677
|
+
import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync6, existsSync as existsSync7 } from "fs";
|
|
3678
|
+
import { join as join4, dirname as dirname7 } from "path";
|
|
3333
3679
|
function resolveFailureLogPath() {
|
|
3334
3680
|
const explicit = process.env.MASSU_HOOK_FAILURE_LOG;
|
|
3335
3681
|
if (explicit) return explicit;
|
|
3336
3682
|
let dir = process.cwd();
|
|
3337
3683
|
for (let i = 0; i < 12; i++) {
|
|
3338
|
-
if (
|
|
3339
|
-
return
|
|
3684
|
+
if (existsSync7(join4(dir, ".massu")) || existsSync7(join4(dir, "massu.config.yaml"))) {
|
|
3685
|
+
return join4(dir, ".massu", "hook-failures.jsonl");
|
|
3340
3686
|
}
|
|
3341
|
-
const parent =
|
|
3687
|
+
const parent = dirname7(dir);
|
|
3342
3688
|
if (parent === dir) break;
|
|
3343
3689
|
dir = parent;
|
|
3344
3690
|
}
|
|
3345
|
-
return
|
|
3691
|
+
return join4(process.cwd(), ".massu", "hook-failures.jsonl");
|
|
3346
3692
|
}
|
|
3347
3693
|
function recordHookFailure(hook, error, context) {
|
|
3348
3694
|
const record = {
|
|
@@ -3355,8 +3701,8 @@ function recordHookFailure(hook, error, context) {
|
|
|
3355
3701
|
let wroteSomething = false;
|
|
3356
3702
|
try {
|
|
3357
3703
|
const path = resolveFailureLogPath();
|
|
3358
|
-
|
|
3359
|
-
|
|
3704
|
+
mkdirSync6(dirname7(path), { recursive: true });
|
|
3705
|
+
appendFileSync2(path, JSON.stringify(record) + "\n", "utf-8");
|
|
3360
3706
|
wroteSomething = true;
|
|
3361
3707
|
} catch {
|
|
3362
3708
|
}
|
|
@@ -3401,9 +3747,9 @@ var init_hook_failure_signal = __esm({
|
|
|
3401
3747
|
// src/hooks/auto-learning-pipeline.ts
|
|
3402
3748
|
init_config();
|
|
3403
3749
|
import { execFileSync } from "child_process";
|
|
3404
|
-
import { existsSync as
|
|
3750
|
+
import { existsSync as existsSync8, readFileSync as readFileSync5, unlinkSync as unlinkSync2, readdirSync as readdirSync2, statSync as statSync2 } from "fs";
|
|
3405
3751
|
import { tmpdir } from "os";
|
|
3406
|
-
import { join as
|
|
3752
|
+
import { join as join5 } from "path";
|
|
3407
3753
|
|
|
3408
3754
|
// src/hooks/lib/write-hook-message.ts
|
|
3409
3755
|
function writeHookMessage(message) {
|
|
@@ -3414,7 +3760,7 @@ function writeHookMessage(message) {
|
|
|
3414
3760
|
init_hook_failure_signal();
|
|
3415
3761
|
var MAX_FULL_DIFF_BYTES = 2 * 1024 * 1024;
|
|
3416
3762
|
function getSessionFlagPath(sessionId) {
|
|
3417
|
-
return
|
|
3763
|
+
return join5(tmpdir(), "massu-auto-learning", `fixes-${sessionId.slice(0, 12)}.jsonl`);
|
|
3418
3764
|
}
|
|
3419
3765
|
async function main() {
|
|
3420
3766
|
try {
|
|
@@ -3431,9 +3777,9 @@ async function main() {
|
|
|
3431
3777
|
const autoLearn = config.autoLearning;
|
|
3432
3778
|
const flagPath = getSessionFlagPath(hookInput.session_id);
|
|
3433
3779
|
let sessionFixes = [];
|
|
3434
|
-
if (
|
|
3780
|
+
if (existsSync8(flagPath)) {
|
|
3435
3781
|
try {
|
|
3436
|
-
sessionFixes =
|
|
3782
|
+
sessionFixes = readFileSync5(flagPath, "utf-8").split("\n").filter(Boolean).map((line) => JSON.parse(line));
|
|
3437
3783
|
} catch {
|
|
3438
3784
|
}
|
|
3439
3785
|
}
|
|
@@ -3539,12 +3885,12 @@ async function main() {
|
|
|
3539
3885
|
}
|
|
3540
3886
|
function cleanup(flagPath) {
|
|
3541
3887
|
try {
|
|
3542
|
-
if (
|
|
3543
|
-
const dir =
|
|
3544
|
-
if (
|
|
3888
|
+
if (existsSync8(flagPath)) unlinkSync2(flagPath);
|
|
3889
|
+
const dir = join5(tmpdir(), "massu-auto-learning");
|
|
3890
|
+
if (existsSync8(dir)) {
|
|
3545
3891
|
const now = Date.now();
|
|
3546
3892
|
for (const file of readdirSync2(dir)) {
|
|
3547
|
-
const fullPath =
|
|
3893
|
+
const fullPath = join5(dir, file);
|
|
3548
3894
|
try {
|
|
3549
3895
|
const stat = statSync2(fullPath);
|
|
3550
3896
|
if (now - stat.mtimeMs > 864e5) {
|