@massu/core 2.5.4 → 2.5.6
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 +66 -0
- package/dist/cli.js +505 -407
- package/dist/hooks/auto-learning-pipeline.js +26 -2
- package/dist/hooks/classify-failure.js +53 -656
- package/dist/hooks/cost-tracker.js +26 -2
- package/dist/hooks/fix-detector.js +30 -4
- package/dist/hooks/incident-pipeline.js +26 -2
- package/dist/hooks/intent-suggester.js +26 -2
- package/dist/hooks/memory-recall.js +26 -2
- package/dist/hooks/post-edit-context.js +26 -2
- package/dist/hooks/post-tool-use.js +26 -2
- package/dist/hooks/pre-compact.js +26 -2
- package/dist/hooks/pre-delete-check.js +405 -71
- package/dist/hooks/pre-tool-use-gate.js +196 -223
- package/dist/hooks/quality-event.js +26 -2
- package/dist/hooks/rule-enforcement-pipeline.js +26 -2
- package/dist/hooks/security-gate.js +570 -181
- package/dist/hooks/session-end.js +1081 -354
- package/dist/hooks/session-start.js +26 -2
- package/dist/hooks/user-prompt.js +26 -2
- package/package.json +1 -1
|
@@ -292,12 +292,12 @@ function runRebuild(pkgDir, abiFrom, abiTo, start) {
|
|
|
292
292
|
}
|
|
293
293
|
const prebuildBin = resolveLocalBin(pkgDir, "prebuild-install");
|
|
294
294
|
if (prebuildBin) {
|
|
295
|
-
const
|
|
295
|
+
const r2 = spawnSync(process.execPath, [prebuildBin], {
|
|
296
296
|
cwd: pkgDir,
|
|
297
297
|
encoding: "utf-8",
|
|
298
298
|
timeout: 12e4
|
|
299
299
|
});
|
|
300
|
-
if (
|
|
300
|
+
if (r2.status === 0) {
|
|
301
301
|
const res2 = { healed: true, method: "prebuild-install", abiFrom, abiTo, durationMs: Date.now() - start };
|
|
302
302
|
recordHealEvent({ phase: "success", reason: "abi-mismatch", ...res2 });
|
|
303
303
|
return res2;
|
|
@@ -305,12 +305,12 @@ function runRebuild(pkgDir, abiFrom, abiTo, start) {
|
|
|
305
305
|
}
|
|
306
306
|
const gypBin = resolveNodeGypBin();
|
|
307
307
|
if (gypBin && hasCompiler()) {
|
|
308
|
-
const
|
|
308
|
+
const r2 = spawnSync(process.execPath, [gypBin, "rebuild", "--release"], {
|
|
309
309
|
cwd: pkgDir,
|
|
310
310
|
encoding: "utf-8",
|
|
311
311
|
timeout: 3e5
|
|
312
312
|
});
|
|
313
|
-
if (
|
|
313
|
+
if (r2.status === 0) {
|
|
314
314
|
const res2 = { healed: true, method: "node-gyp", abiFrom, abiTo, durationMs: Date.now() - start };
|
|
315
315
|
recordHealEvent({ phase: "success", reason: "abi-mismatch", ...res2 });
|
|
316
316
|
return res2;
|
|
@@ -389,8 +389,8 @@ function resolveNodeGypBin() {
|
|
|
389
389
|
function hasCompiler() {
|
|
390
390
|
for (const cc of ["cc", "clang", "gcc"]) {
|
|
391
391
|
try {
|
|
392
|
-
const
|
|
393
|
-
if (
|
|
392
|
+
const r2 = spawnSync(cc, ["--version"], { encoding: "utf-8", timeout: 5e3 });
|
|
393
|
+
if (r2.status === 0) return true;
|
|
394
394
|
} catch {
|
|
395
395
|
}
|
|
396
396
|
}
|
|
@@ -464,10 +464,10 @@ function openNodeSqlite(dbPath, opts) {
|
|
|
464
464
|
const st = raw.prepare(sql);
|
|
465
465
|
return {
|
|
466
466
|
run: (...p) => {
|
|
467
|
-
const
|
|
467
|
+
const r2 = st.run(...p);
|
|
468
468
|
return {
|
|
469
|
-
changes: typeof
|
|
470
|
-
lastInsertRowid:
|
|
469
|
+
changes: typeof r2.changes === "bigint" ? Number(r2.changes) : r2.changes,
|
|
470
|
+
lastInsertRowid: r2.lastInsertRowid
|
|
471
471
|
};
|
|
472
472
|
},
|
|
473
473
|
get: (...p) => plainRow(st.get(...p)),
|
|
@@ -951,7 +951,8 @@ var init_config = __esm({
|
|
|
951
951
|
input_per_million: z2.number(),
|
|
952
952
|
output_per_million: z2.number(),
|
|
953
953
|
cache_read_per_million: z2.number().optional(),
|
|
954
|
-
cache_write_per_million: z2.number().optional()
|
|
954
|
+
cache_write_per_million: z2.number().optional(),
|
|
955
|
+
cache_write_1h_per_million: z2.number().optional()
|
|
955
956
|
});
|
|
956
957
|
AnalyticsConfigSchema = z2.object({
|
|
957
958
|
quality: z2.object({
|
|
@@ -1557,11 +1558,11 @@ function claimPage(db, table, pk, orderBy, limit, token, nowMs) {
|
|
|
1557
1558
|
const placeholders = ids.map(() => "?").join(",");
|
|
1558
1559
|
db.prepare(
|
|
1559
1560
|
`UPDATE ${table} SET lease_token = ?, leased_at_ms = ? WHERE ${pk} IN (${placeholders})`
|
|
1560
|
-
).run(token, nowMs, ...ids.map((
|
|
1561
|
+
).run(token, nowMs, ...ids.map((r2) => r2.k));
|
|
1561
1562
|
}
|
|
1562
1563
|
function columns(db, table) {
|
|
1563
1564
|
const rows = db.prepare(`PRAGMA table_info(${table})`).all();
|
|
1564
|
-
return new Set(rows.map((
|
|
1565
|
+
return new Set(rows.map((r2) => r2.name));
|
|
1565
1566
|
}
|
|
1566
1567
|
function migrateRuleDelivery(db) {
|
|
1567
1568
|
const ADDITIONS = [
|
|
@@ -1590,29 +1591,29 @@ function leaseLearning(db, nowMs = Date.now()) {
|
|
|
1590
1591
|
content_hash, hardened, review_attestation_json
|
|
1591
1592
|
FROM team_promotion_outbound WHERE lease_token = ?
|
|
1592
1593
|
ORDER BY created_at ASC LIMIT ?`
|
|
1593
|
-
).all(token, PROMOTION_LEASE_LIMIT).map((
|
|
1594
|
-
prompt_hash:
|
|
1595
|
-
destination:
|
|
1596
|
-
draft_text:
|
|
1597
|
-
...
|
|
1598
|
-
signals: safeParseArray(
|
|
1599
|
-
content_hash:
|
|
1600
|
-
...
|
|
1601
|
-
...
|
|
1594
|
+
).all(token, PROMOTION_LEASE_LIMIT).map((r2) => ({
|
|
1595
|
+
prompt_hash: r2.prompt_hash,
|
|
1596
|
+
destination: r2.destination,
|
|
1597
|
+
draft_text: r2.draft_text,
|
|
1598
|
+
...r2.score !== null ? { score: r2.score } : {},
|
|
1599
|
+
signals: safeParseArray(r2.signals_json),
|
|
1600
|
+
content_hash: r2.content_hash,
|
|
1601
|
+
...r2.hardened ? { hardened: true } : {},
|
|
1602
|
+
...r2.review_attestation_json != null ? { review_attestation: safeParseUnknown(r2.review_attestation_json) } : {}
|
|
1602
1603
|
})) : [];
|
|
1603
1604
|
const revocations = tableExists(db, "team_revocation_outbound") ? db.prepare(
|
|
1604
1605
|
`SELECT prompt_hash FROM team_revocation_outbound
|
|
1605
1606
|
WHERE lease_token = ? ORDER BY created_at ASC LIMIT ?`
|
|
1606
|
-
).all(token, PROMOTION_LEASE_LIMIT).map((
|
|
1607
|
+
).all(token, PROMOTION_LEASE_LIMIT).map((r2) => r2.prompt_hash) : [];
|
|
1607
1608
|
const events = tableExists(db, TELEMETRY_OUTBOUND_TABLE) ? db.prepare(
|
|
1608
1609
|
`SELECT prompt_hash, event_type, metadata_json, created_at
|
|
1609
1610
|
FROM ${TELEMETRY_OUTBOUND_TABLE} WHERE lease_token = ?
|
|
1610
1611
|
ORDER BY id ASC LIMIT ?`
|
|
1611
|
-
).all(token, TELEMETRY_LEASE_LIMIT).map((
|
|
1612
|
-
prompt_hash:
|
|
1613
|
-
event_type:
|
|
1614
|
-
created_at:
|
|
1615
|
-
metadata: safeParseUnknown(
|
|
1612
|
+
).all(token, TELEMETRY_LEASE_LIMIT).map((r2) => ({
|
|
1613
|
+
prompt_hash: r2.prompt_hash,
|
|
1614
|
+
event_type: r2.event_type,
|
|
1615
|
+
created_at: r2.created_at,
|
|
1616
|
+
metadata: safeParseUnknown(r2.metadata_json) ?? {}
|
|
1616
1617
|
})) : [];
|
|
1617
1618
|
return { token, promotions, revocations, events };
|
|
1618
1619
|
}
|
|
@@ -1647,22 +1648,22 @@ function nackLearning(db, lease, error) {
|
|
|
1647
1648
|
return snap;
|
|
1648
1649
|
}
|
|
1649
1650
|
function undeliveredSnapshot(db) {
|
|
1650
|
-
const count = (
|
|
1651
|
-
if (!tableExists(db,
|
|
1652
|
-
const
|
|
1653
|
-
return
|
|
1651
|
+
const count = (t2) => {
|
|
1652
|
+
if (!tableExists(db, t2)) return 0;
|
|
1653
|
+
const r2 = db.prepare(`SELECT COUNT(*) AS n FROM ${t2}`).get();
|
|
1654
|
+
return r2.n;
|
|
1654
1655
|
};
|
|
1655
1656
|
const promotions = count("team_promotion_outbound");
|
|
1656
1657
|
const revocations = count("team_revocation_outbound");
|
|
1657
1658
|
const events = count(TELEMETRY_OUTBOUND_TABLE);
|
|
1658
1659
|
let maxAttempts = 0;
|
|
1659
1660
|
let oldest = null;
|
|
1660
|
-
for (const
|
|
1661
|
-
if (!tableExists(db,
|
|
1662
|
-
if (!columns(db,
|
|
1663
|
-
const
|
|
1664
|
-
if (
|
|
1665
|
-
if (
|
|
1661
|
+
for (const t2 of RULE_OUTBOUND_TABLES) {
|
|
1662
|
+
if (!tableExists(db, t2)) continue;
|
|
1663
|
+
if (!columns(db, t2).has("attempts")) continue;
|
|
1664
|
+
const r2 = db.prepare(`SELECT MAX(attempts) AS a, MIN(created_at) AS c FROM ${t2}`).get();
|
|
1665
|
+
if (r2.a !== null && r2.a > maxAttempts) maxAttempts = r2.a;
|
|
1666
|
+
if (r2.c !== null && (oldest === null || r2.c < oldest)) oldest = r2.c;
|
|
1666
1667
|
}
|
|
1667
1668
|
return {
|
|
1668
1669
|
promotions,
|
|
@@ -1957,13 +1958,13 @@ function encode(text, vocab, opts = {}) {
|
|
|
1957
1958
|
const maxLen = opts.maxLen ?? 256;
|
|
1958
1959
|
const basic = basicTokenize(text);
|
|
1959
1960
|
const wpTokens = [];
|
|
1960
|
-
for (const
|
|
1961
|
-
for (const sub of wordpieceTokenize(
|
|
1961
|
+
for (const t2 of basic) {
|
|
1962
|
+
for (const sub of wordpieceTokenize(t2, vocab)) wpTokens.push(sub);
|
|
1962
1963
|
}
|
|
1963
1964
|
const truncated = wpTokens.slice(0, maxLen - 2);
|
|
1964
1965
|
const tokens = [CLS, ...truncated, SEP];
|
|
1965
1966
|
const unkId = vocab.get(UNK) ?? 100;
|
|
1966
|
-
const input_ids = tokens.map((
|
|
1967
|
+
const input_ids = tokens.map((t2) => vocab.get(t2) ?? unkId);
|
|
1967
1968
|
const attention_mask = tokens.map(() => 1);
|
|
1968
1969
|
const token_type_ids = tokens.map(() => 0);
|
|
1969
1970
|
return { tokens, input_ids, attention_mask, token_type_ids };
|
|
@@ -1988,11 +1989,11 @@ function getActiveEmbedModel() {
|
|
|
1988
1989
|
}
|
|
1989
1990
|
function loadEmbedSettings() {
|
|
1990
1991
|
try {
|
|
1991
|
-
const
|
|
1992
|
+
const r2 = getConfig().memory?.recall;
|
|
1992
1993
|
return {
|
|
1993
|
-
enabled:
|
|
1994
|
-
endpoint:
|
|
1995
|
-
model:
|
|
1994
|
+
enabled: r2?.embedEnabled ?? true,
|
|
1995
|
+
endpoint: r2?.embedEndpoint,
|
|
1996
|
+
model: r2?.embedModel
|
|
1996
1997
|
};
|
|
1997
1998
|
} catch {
|
|
1998
1999
|
return { enabled: true };
|
|
@@ -2065,11 +2066,11 @@ async function getSession() {
|
|
|
2065
2066
|
function meanPool(hidden, mask, seqLen, hiddenSize) {
|
|
2066
2067
|
const out = new Float32Array(hiddenSize);
|
|
2067
2068
|
let maskSum = 0;
|
|
2068
|
-
for (let
|
|
2069
|
-
const m = mask[
|
|
2069
|
+
for (let t2 = 0; t2 < seqLen; t2++) {
|
|
2070
|
+
const m = mask[t2];
|
|
2070
2071
|
if (m === 0) continue;
|
|
2071
2072
|
maskSum += m;
|
|
2072
|
-
const base =
|
|
2073
|
+
const base = t2 * hiddenSize;
|
|
2073
2074
|
for (let h = 0; h < hiddenSize; h++) out[h] += hidden[base + h] * m;
|
|
2074
2075
|
}
|
|
2075
2076
|
const denom = Math.max(maskSum, 1e-9);
|
|
@@ -2077,16 +2078,16 @@ function meanPool(hidden, mask, seqLen, hiddenSize) {
|
|
|
2077
2078
|
return out;
|
|
2078
2079
|
}
|
|
2079
2080
|
function chunkForEmbedding(text) {
|
|
2080
|
-
const
|
|
2081
|
-
if (!
|
|
2082
|
-
if (
|
|
2081
|
+
const t2 = text.trim();
|
|
2082
|
+
if (!t2) return [];
|
|
2083
|
+
if (t2.length <= CHUNK_TARGET_CHARS) return [t2];
|
|
2083
2084
|
const chunks = [];
|
|
2084
2085
|
let start = 0;
|
|
2085
|
-
while (start <
|
|
2086
|
-
let end = Math.min(start + CHUNK_TARGET_CHARS,
|
|
2087
|
-
if (end <
|
|
2086
|
+
while (start < t2.length && chunks.length < MAX_CHUNKS_PER_RECORD) {
|
|
2087
|
+
let end = Math.min(start + CHUNK_TARGET_CHARS, t2.length);
|
|
2088
|
+
if (end < t2.length) {
|
|
2088
2089
|
const floor = start + Math.floor(CHUNK_TARGET_CHARS / 2);
|
|
2089
|
-
const slice =
|
|
2090
|
+
const slice = t2.slice(start, end);
|
|
2090
2091
|
const para = slice.lastIndexOf("\n\n");
|
|
2091
2092
|
const sent = Math.max(slice.lastIndexOf(". "), slice.lastIndexOf(".\n"));
|
|
2092
2093
|
const space = slice.lastIndexOf(" ");
|
|
@@ -2098,9 +2099,9 @@ function chunkForEmbedding(text) {
|
|
|
2098
2099
|
}
|
|
2099
2100
|
}
|
|
2100
2101
|
}
|
|
2101
|
-
const chunk =
|
|
2102
|
+
const chunk = t2.slice(start, end).trim();
|
|
2102
2103
|
if (chunk) chunks.push(chunk);
|
|
2103
|
-
if (end >=
|
|
2104
|
+
if (end >= t2.length) break;
|
|
2104
2105
|
start = Math.max(end - CHUNK_OVERLAP_CHARS, start + 1);
|
|
2105
2106
|
}
|
|
2106
2107
|
return chunks;
|
|
@@ -2185,17 +2186,17 @@ async function embedBatch(texts) {
|
|
|
2185
2186
|
const settings = loadEmbedSettings();
|
|
2186
2187
|
if (!settings.enabled) return texts.map(() => null);
|
|
2187
2188
|
const results = [];
|
|
2188
|
-
for (const
|
|
2189
|
-
if (!
|
|
2189
|
+
for (const t2 of texts) {
|
|
2190
|
+
if (!t2 || !t2.trim()) {
|
|
2190
2191
|
results.push(null);
|
|
2191
2192
|
continue;
|
|
2192
2193
|
}
|
|
2193
2194
|
try {
|
|
2194
2195
|
let vec = null;
|
|
2195
2196
|
if (settings.endpoint) {
|
|
2196
|
-
vec = await embedTier0(
|
|
2197
|
+
vec = await embedTier0(t2, settings);
|
|
2197
2198
|
}
|
|
2198
|
-
if (!vec) vec = await embedTier1(
|
|
2199
|
+
if (!vec) vec = await embedTier1(t2);
|
|
2199
2200
|
results.push(vec);
|
|
2200
2201
|
} catch {
|
|
2201
2202
|
results.push(null);
|
|
@@ -2250,11 +2251,11 @@ async function runEmbedSweep(db, cfg, opts = {}) {
|
|
|
2250
2251
|
const limit = opts.limit ?? Infinity;
|
|
2251
2252
|
const budgetMs = opts.budgetMs;
|
|
2252
2253
|
const start = Date.now();
|
|
2253
|
-
const
|
|
2254
|
+
const cursorKey2 = `embed_sweep_cursor_${cfg.sourceLabel}`;
|
|
2254
2255
|
let embedded = 0;
|
|
2255
2256
|
let scanned = 0;
|
|
2256
2257
|
let active = getActiveEmbedModel();
|
|
2257
|
-
let cursor = readCursor(db, cfg.metaTable,
|
|
2258
|
+
let cursor = readCursor(db, cfg.metaTable, cursorKey2);
|
|
2258
2259
|
try {
|
|
2259
2260
|
while (true) {
|
|
2260
2261
|
if (embedded >= limit) break;
|
|
@@ -2262,17 +2263,17 @@ async function runEmbedSweep(db, cfg, opts = {}) {
|
|
|
2262
2263
|
const remaining = limit === Infinity ? batchSize : Math.min(batchSize, limit - embedded);
|
|
2263
2264
|
const rows = cfg.selectMissing(db, cursor, active, Math.max(1, remaining));
|
|
2264
2265
|
if (rows.length === 0) {
|
|
2265
|
-
writeCursor(db, cfg.metaTable,
|
|
2266
|
+
writeCursor(db, cfg.metaTable, cursorKey2, 0);
|
|
2266
2267
|
break;
|
|
2267
2268
|
}
|
|
2268
2269
|
const units = [];
|
|
2269
|
-
for (const
|
|
2270
|
+
for (const r2 of rows) {
|
|
2270
2271
|
if (cfg.chunked) {
|
|
2271
|
-
const parts = chunkForEmbedding(
|
|
2272
|
+
const parts = chunkForEmbedding(r2.text);
|
|
2272
2273
|
if (parts.length === 0) continue;
|
|
2273
|
-
parts.forEach((text, chunkIx) => units.push({ id:
|
|
2274
|
+
parts.forEach((text, chunkIx) => units.push({ id: r2.id, chunkIx, text }));
|
|
2274
2275
|
} else {
|
|
2275
|
-
units.push({ id:
|
|
2276
|
+
units.push({ id: r2.id, chunkIx: 0, text: r2.text });
|
|
2276
2277
|
}
|
|
2277
2278
|
}
|
|
2278
2279
|
const vecs = await embedBatch(units.map((u) => u.text));
|
|
@@ -2316,7 +2317,7 @@ async function runEmbedSweep(db, cfg, opts = {}) {
|
|
|
2316
2317
|
tx();
|
|
2317
2318
|
scanned += rows.length;
|
|
2318
2319
|
cursor = rows[rows.length - 1].id;
|
|
2319
|
-
writeCursor(db, cfg.metaTable,
|
|
2320
|
+
writeCursor(db, cfg.metaTable, cursorKey2, cursor);
|
|
2320
2321
|
}
|
|
2321
2322
|
} catch {
|
|
2322
2323
|
}
|
|
@@ -2391,6 +2392,7 @@ __export(memory_db_exports, {
|
|
|
2391
2392
|
pruneToolCostEvents: () => pruneToolCostEvents,
|
|
2392
2393
|
recordRecallHits: () => recordRecallHits,
|
|
2393
2394
|
recordTelemetry: () => recordTelemetry,
|
|
2395
|
+
releaseMetaLease: () => releaseMetaLease,
|
|
2394
2396
|
removePendingSync: () => removePendingSync,
|
|
2395
2397
|
sanitizeFts5Query: () => sanitizeFts5Query,
|
|
2396
2398
|
sanitizeFts5QueryOr: () => sanitizeFts5QueryOr,
|
|
@@ -2399,6 +2401,7 @@ __export(memory_db_exports, {
|
|
|
2399
2401
|
searchObservations: () => searchObservations,
|
|
2400
2402
|
setLastProcessedLine: () => setLastProcessedLine,
|
|
2401
2403
|
setMemoryMeta: () => setMemoryMeta,
|
|
2404
|
+
tryAcquireMetaLease: () => tryAcquireMetaLease,
|
|
2402
2405
|
upsertObservationEmbedding: () => upsertObservationEmbedding,
|
|
2403
2406
|
usageWarmupElapsed: () => usageWarmupElapsed
|
|
2404
2407
|
});
|
|
@@ -2408,14 +2411,14 @@ function sanitizeFts5Query(raw) {
|
|
|
2408
2411
|
const trimmed = raw.trim();
|
|
2409
2412
|
if (!trimmed) return '""';
|
|
2410
2413
|
const tokens = trimmed.replace(/"/g, "").split(/\s+/).filter(Boolean);
|
|
2411
|
-
return tokens.map((
|
|
2414
|
+
return tokens.map((t2) => `"${t2}"`).join(" ");
|
|
2412
2415
|
}
|
|
2413
2416
|
function sanitizeFts5QueryOr(raw) {
|
|
2414
2417
|
const trimmed = raw.trim();
|
|
2415
2418
|
if (!trimmed) return '""';
|
|
2416
|
-
const tokens = trimmed.replace(/"/g, "").split(/\s+/).filter((
|
|
2419
|
+
const tokens = trimmed.replace(/"/g, "").split(/\s+/).filter((t2) => t2.replace(/[^a-zA-Z0-9]/g, "").length >= 3);
|
|
2417
2420
|
if (tokens.length === 0) return '""';
|
|
2418
|
-
return tokens.map((
|
|
2421
|
+
return tokens.map((t2) => `"${t2}"`).join(" OR ");
|
|
2419
2422
|
}
|
|
2420
2423
|
function getMemoryDb() {
|
|
2421
2424
|
const dbPath = getResolvedPaths().memoryDbPath;
|
|
@@ -2904,6 +2907,15 @@ function initMemorySchema(db) {
|
|
|
2904
2907
|
);
|
|
2905
2908
|
CREATE INDEX IF NOT EXISTS idx_sc_session ON session_costs(session_id);
|
|
2906
2909
|
`);
|
|
2910
|
+
const costCols = db.prepare(`PRAGMA table_info(session_costs)`).all();
|
|
2911
|
+
const addCostCol = (name, ddl) => {
|
|
2912
|
+
if (!costCols.some((c) => c.name === name)) db.exec(`ALTER TABLE session_costs ADD COLUMN ${ddl}`);
|
|
2913
|
+
};
|
|
2914
|
+
addCostCol("cost_status", `cost_status TEXT NOT NULL DEFAULT 'unknown'`);
|
|
2915
|
+
addCostCol("unpriced_models", `unpriced_models TEXT NOT NULL DEFAULT ''`);
|
|
2916
|
+
addCostCol("priced_responses", `priced_responses INTEGER NOT NULL DEFAULT 0`);
|
|
2917
|
+
addCostCol("unpriced_responses", `unpriced_responses INTEGER NOT NULL DEFAULT 0`);
|
|
2918
|
+
addCostCol("pricing_version", `pricing_version INTEGER NOT NULL DEFAULT 0`);
|
|
2907
2919
|
db.exec(`
|
|
2908
2920
|
CREATE TABLE IF NOT EXISTS feature_costs (
|
|
2909
2921
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
@@ -3333,6 +3345,18 @@ function getMemoryMeta(db, key) {
|
|
|
3333
3345
|
function setMemoryMeta(db, key, value) {
|
|
3334
3346
|
db.prepare("INSERT OR REPLACE INTO memory_meta (key, value) VALUES (?, ?)").run(key, value);
|
|
3335
3347
|
}
|
|
3348
|
+
function tryAcquireMetaLease(db, key, ttlSec, nowSec = Math.floor(Date.now() / 1e3)) {
|
|
3349
|
+
const r2 = db.prepare(
|
|
3350
|
+
`INSERT INTO memory_meta (key, value) VALUES (?, ?)
|
|
3351
|
+
ON CONFLICT(key) DO UPDATE SET value = excluded.value
|
|
3352
|
+
WHERE memory_meta.value = ''
|
|
3353
|
+
OR CAST(substr(memory_meta.value, instr(memory_meta.value, ':') + 1) AS INTEGER) <= ?`
|
|
3354
|
+
).run(key, `${process.pid}:${nowSec + ttlSec}`, nowSec);
|
|
3355
|
+
return Number(r2.changes) === 1;
|
|
3356
|
+
}
|
|
3357
|
+
function releaseMetaLease(db, key) {
|
|
3358
|
+
db.prepare(`UPDATE memory_meta SET value = '' WHERE key = ? AND value LIKE ?`).run(key, `${process.pid}:%`);
|
|
3359
|
+
}
|
|
3336
3360
|
function upsertObservationEmbedding(db, observationId, vec, modelId, dim) {
|
|
3337
3361
|
db.prepare(
|
|
3338
3362
|
`INSERT OR REPLACE INTO observation_embeddings (observation_id, model_id, dim, vec, created_at)
|
|
@@ -3340,9 +3364,9 @@ function upsertObservationEmbedding(db, observationId, vec, modelId, dim) {
|
|
|
3340
3364
|
).run(observationId, modelId, dim, float32ToBlob(vec));
|
|
3341
3365
|
}
|
|
3342
3366
|
function observationEmbedText(title, detail) {
|
|
3343
|
-
const
|
|
3367
|
+
const t2 = (title ?? "").trim();
|
|
3344
3368
|
const d = (detail ?? "").trim();
|
|
3345
|
-
return d ? `${
|
|
3369
|
+
return d ? `${t2}. ${d}` : t2;
|
|
3346
3370
|
}
|
|
3347
3371
|
async function embedMissingObservations(db, opts = {}) {
|
|
3348
3372
|
return runEmbedSweep(
|
|
@@ -3374,7 +3398,7 @@ async function embedMissingObservations(db, opts = {}) {
|
|
|
3374
3398
|
)
|
|
3375
3399
|
ORDER BY o.id LIMIT ?`
|
|
3376
3400
|
).all(cursor, batchSize);
|
|
3377
|
-
return rows.map((
|
|
3401
|
+
return rows.map((r2) => ({ id: r2.id, text: observationEmbedText(r2.title, r2.detail) }));
|
|
3378
3402
|
}
|
|
3379
3403
|
},
|
|
3380
3404
|
opts
|
|
@@ -3462,9 +3486,9 @@ function rescueLearningFromStalePayloads(db, stale) {
|
|
|
3462
3486
|
rescued++;
|
|
3463
3487
|
}
|
|
3464
3488
|
const revocations = Array.isArray(payload.rule_revocations) ? payload.rule_revocations : [];
|
|
3465
|
-
for (const
|
|
3466
|
-
if (typeof
|
|
3467
|
-
enqueueTeamRevocation(db,
|
|
3489
|
+
for (const r2 of revocations) {
|
|
3490
|
+
if (typeof r2?.prompt_hash === "string") {
|
|
3491
|
+
enqueueTeamRevocation(db, r2.prompt_hash);
|
|
3468
3492
|
rescued++;
|
|
3469
3493
|
}
|
|
3470
3494
|
}
|
|
@@ -4201,7 +4225,7 @@ var init_memory_db = __esm({
|
|
|
4201
4225
|
init_rule_candidate_store();
|
|
4202
4226
|
init_hook_failure_signal();
|
|
4203
4227
|
init_memory_embed_sweep();
|
|
4204
|
-
MEMORY_SCHEMA_VERSION =
|
|
4228
|
+
MEMORY_SCHEMA_VERSION = 2;
|
|
4205
4229
|
TOOL_COST_EVENTS_RETENTION_DAYS = 90;
|
|
4206
4230
|
MAX_DRAFT_TEXT_LEN = 16384;
|
|
4207
4231
|
FUNNEL_EVENT_OUTBOX_CAP = 2e4;
|
|
@@ -4446,6 +4470,11 @@ function parseEntry(raw) {
|
|
|
4446
4470
|
role: msgRaw.role ?? entryType,
|
|
4447
4471
|
content: normalizeContent(msgRaw.content)
|
|
4448
4472
|
};
|
|
4473
|
+
if (typeof msgRaw.id === "string") base.message.id = msgRaw.id;
|
|
4474
|
+
if (typeof msgRaw.model === "string") base.message.model = msgRaw.model;
|
|
4475
|
+
if (msgRaw.usage && typeof msgRaw.usage === "object") {
|
|
4476
|
+
base.message.usage = msgRaw.usage;
|
|
4477
|
+
}
|
|
4449
4478
|
}
|
|
4450
4479
|
}
|
|
4451
4480
|
if (entryType === "progress") {
|
|
@@ -4468,6 +4497,7 @@ function normalizeContent(content) {
|
|
|
4468
4497
|
async function parseTranscriptFrom(filePath, startLine) {
|
|
4469
4498
|
const entries = [];
|
|
4470
4499
|
let lineNumber = 0;
|
|
4500
|
+
let lastUnparseableLine = 0;
|
|
4471
4501
|
const rl = createInterface({
|
|
4472
4502
|
input: createReadStream(filePath, { encoding: "utf-8" }),
|
|
4473
4503
|
crlfDelay: Infinity
|
|
@@ -4484,10 +4514,12 @@ async function parseTranscriptFrom(filePath, startLine) {
|
|
|
4484
4514
|
entries.push(entry);
|
|
4485
4515
|
}
|
|
4486
4516
|
} catch (_e) {
|
|
4517
|
+
lastUnparseableLine = lineNumber;
|
|
4487
4518
|
continue;
|
|
4488
4519
|
}
|
|
4489
4520
|
}
|
|
4490
|
-
|
|
4521
|
+
const totalLines = lastUnparseableLine === lineNumber && lineNumber > startLine ? lineNumber - 1 : lineNumber;
|
|
4522
|
+
return { entries, totalLines };
|
|
4491
4523
|
}
|
|
4492
4524
|
function estimateTokens(text) {
|
|
4493
4525
|
return Math.ceil(text.length / 4);
|
|
@@ -4533,7 +4565,8 @@ var RETRY_DELAYS = [1e3, 2e3, 4e3];
|
|
|
4533
4565
|
var DEFAULT_CLOUD_REQUEST_TIMEOUT_MS = 15e3;
|
|
4534
4566
|
var SYNC_DEADLINE_MS = 2e4;
|
|
4535
4567
|
var MIN_ATTEMPT_BUDGET_MS = 1e3;
|
|
4536
|
-
|
|
4568
|
+
var MIN_SYNC_ATTEMPT_MS = 3e3;
|
|
4569
|
+
async function syncToCloud(db, payload, opts = {}) {
|
|
4537
4570
|
const config = getConfig();
|
|
4538
4571
|
const cloud = config.cloud;
|
|
4539
4572
|
if (!cloud?.enabled) {
|
|
@@ -4616,11 +4649,11 @@ async function syncToCloud(db, payload) {
|
|
|
4616
4649
|
}
|
|
4617
4650
|
let lastError = "";
|
|
4618
4651
|
const configuredTimeoutMs = cloud.requestTimeoutMs ?? DEFAULT_CLOUD_REQUEST_TIMEOUT_MS;
|
|
4619
|
-
const deadlineAt = Date.now() + SYNC_DEADLINE_MS;
|
|
4652
|
+
const deadlineAt = Math.min(Date.now() + SYNC_DEADLINE_MS, opts.deadlineAt ?? Infinity);
|
|
4620
4653
|
for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
|
|
4621
4654
|
const remainingMs = deadlineAt - Date.now();
|
|
4622
4655
|
if (remainingMs < MIN_ATTEMPT_BUDGET_MS) {
|
|
4623
|
-
lastError = lastError ||
|
|
4656
|
+
lastError = lastError || "sync deadline exceeded";
|
|
4624
4657
|
break;
|
|
4625
4658
|
}
|
|
4626
4659
|
const requestTimeoutMs = Math.min(configuredTimeoutMs, remainingMs);
|
|
@@ -4673,13 +4706,15 @@ async function syncToCloud(db, payload) {
|
|
|
4673
4706
|
}
|
|
4674
4707
|
}
|
|
4675
4708
|
}
|
|
4676
|
-
|
|
4677
|
-
|
|
4678
|
-
|
|
4679
|
-
|
|
4680
|
-
|
|
4709
|
+
if (opts.enqueueOnFailure !== false) {
|
|
4710
|
+
try {
|
|
4711
|
+
enqueueSyncPayload(db, JSON.stringify(stripLearningFromPayload(payload)));
|
|
4712
|
+
} catch (err) {
|
|
4713
|
+
process.stderr.write(
|
|
4714
|
+
`[massu] WARNING: could not queue sync payload for retry: ${err instanceof Error ? err.message : String(err)}
|
|
4681
4715
|
`
|
|
4682
|
-
|
|
4716
|
+
);
|
|
4717
|
+
}
|
|
4683
4718
|
}
|
|
4684
4719
|
return {
|
|
4685
4720
|
success: false,
|
|
@@ -4687,14 +4722,22 @@ async function syncToCloud(db, payload) {
|
|
|
4687
4722
|
error: lastError
|
|
4688
4723
|
};
|
|
4689
4724
|
}
|
|
4690
|
-
async function drainSyncQueue(db) {
|
|
4725
|
+
async function drainSyncQueue(db, deadlineAt) {
|
|
4726
|
+
const out = { attempted: 0, deferred: 0 };
|
|
4691
4727
|
const config = getConfig();
|
|
4692
|
-
if (!config.cloud?.enabled || !config.cloud?.apiKey) return;
|
|
4728
|
+
if (!config.cloud?.enabled || !config.cloud?.apiKey) return out;
|
|
4693
4729
|
const pending = dequeuePendingSync(db, 10);
|
|
4694
|
-
for (
|
|
4730
|
+
for (let i = 0; i < pending.length; i++) {
|
|
4731
|
+
const item = pending[i];
|
|
4732
|
+
const remaining = deadlineAt === void 0 ? Infinity : deadlineAt - Date.now();
|
|
4733
|
+
if (remaining < MIN_SYNC_ATTEMPT_MS) {
|
|
4734
|
+
out.deferred = pending.length - i;
|
|
4735
|
+
break;
|
|
4736
|
+
}
|
|
4737
|
+
out.attempted++;
|
|
4695
4738
|
try {
|
|
4696
4739
|
const payload = JSON.parse(item.payload);
|
|
4697
|
-
const result = await syncToCloud(db, payload);
|
|
4740
|
+
const result = await syncToCloud(db, payload, { deadlineAt, enqueueOnFailure: false });
|
|
4698
4741
|
if (result.success) {
|
|
4699
4742
|
removePendingSync(db, item.id);
|
|
4700
4743
|
} else {
|
|
@@ -4704,9 +4747,10 @@ async function drainSyncQueue(db) {
|
|
|
4704
4747
|
incrementRetryCount(db, item.id, err instanceof Error ? err.message : String(err));
|
|
4705
4748
|
}
|
|
4706
4749
|
}
|
|
4750
|
+
return out;
|
|
4707
4751
|
}
|
|
4708
4752
|
function sleep(ms) {
|
|
4709
|
-
return new Promise((
|
|
4753
|
+
return new Promise((resolve9) => setTimeout(resolve9, ms));
|
|
4710
4754
|
}
|
|
4711
4755
|
|
|
4712
4756
|
// src/team-rule-sync.ts
|
|
@@ -5328,8 +5372,8 @@ function countUntrusted(envelope) {
|
|
|
5328
5372
|
}
|
|
5329
5373
|
function isValidWirePromotion(p) {
|
|
5330
5374
|
if (!p || typeof p !== "object") return false;
|
|
5331
|
-
const
|
|
5332
|
-
return typeof
|
|
5375
|
+
const r2 = p;
|
|
5376
|
+
return typeof r2.prompt_hash === "string" && PROMPT_HASH_RE.test(r2.prompt_hash) && typeof r2.destination === "string" && typeof r2.draft_text === "string" && typeof r2.promoted_by === "string" && typeof r2.promoted_at === "string";
|
|
5333
5377
|
}
|
|
5334
5378
|
function sidecarPath(projectRoot, promptHash) {
|
|
5335
5379
|
return join6(projectRoot, ".massu", "rule-candidates", `${promptHash}.json`);
|
|
@@ -5515,12 +5559,12 @@ function readReposRegistry(home = homedir7()) {
|
|
|
5515
5559
|
try {
|
|
5516
5560
|
const parsed = JSON.parse(readFileSync9(p, "utf-8"));
|
|
5517
5561
|
if (!parsed || typeof parsed !== "object") return emptyRegistry();
|
|
5518
|
-
const
|
|
5519
|
-
if (!Array.isArray(
|
|
5520
|
-
const repos =
|
|
5562
|
+
const r2 = parsed;
|
|
5563
|
+
if (!Array.isArray(r2.repos)) return emptyRegistry();
|
|
5564
|
+
const repos = r2.repos.filter(
|
|
5521
5565
|
(e) => !!e && typeof e === "object" && typeof e.repo_id === "string" && typeof e.label === "string"
|
|
5522
5566
|
);
|
|
5523
|
-
return { version: typeof
|
|
5567
|
+
return { version: typeof r2.version === "number" ? r2.version : REPOS_REGISTRY_VERSION, repos };
|
|
5524
5568
|
} catch {
|
|
5525
5569
|
return emptyRegistry();
|
|
5526
5570
|
}
|
|
@@ -5706,8 +5750,8 @@ async function importSharedMemories(db, transport, opts = {}) {
|
|
|
5706
5750
|
const originRepoId = entry.repo_id;
|
|
5707
5751
|
if (ownRepoId && originRepoId === ownRepoId) continue;
|
|
5708
5752
|
const pinned = getSharedPin(db, originRepoId) ?? tofuPinSharedFingerprint(db, originRepoId, entry.pubkey_fingerprint);
|
|
5709
|
-
const
|
|
5710
|
-
const since = parseCursor2(getMemoryMeta(db,
|
|
5753
|
+
const cursorKey2 = SHARED_CURSOR_PREFIX + originRepoId;
|
|
5754
|
+
const since = parseCursor2(getMemoryMeta(db, cursorKey2));
|
|
5711
5755
|
let envelopes;
|
|
5712
5756
|
try {
|
|
5713
5757
|
envelopes = await transport.fetchSince(originRepoId, since);
|
|
@@ -5772,7 +5816,7 @@ async function importSharedMemories(db, transport, opts = {}) {
|
|
|
5772
5816
|
});
|
|
5773
5817
|
}
|
|
5774
5818
|
}
|
|
5775
|
-
if (maxSeq > since) setMemoryMeta(db,
|
|
5819
|
+
if (maxSeq > since) setMemoryMeta(db, cursorKey2, String(maxSeq));
|
|
5776
5820
|
}
|
|
5777
5821
|
return result;
|
|
5778
5822
|
}
|
|
@@ -5802,19 +5846,19 @@ function parseRecords(json) {
|
|
|
5802
5846
|
return [];
|
|
5803
5847
|
}
|
|
5804
5848
|
}
|
|
5805
|
-
function isWellFormedRecord(
|
|
5806
|
-
if (!
|
|
5807
|
-
const o =
|
|
5849
|
+
function isWellFormedRecord(r2) {
|
|
5850
|
+
if (!r2 || typeof r2 !== "object") return false;
|
|
5851
|
+
const o = r2;
|
|
5808
5852
|
return typeof o.record_hash === "string" && typeof o.type === "string" && typeof o.title === "string" && typeof o.detail === "string" && typeof o.importance === "number" && typeof o.created_at_epoch === "number" && (o.superseded_by_hash === null || typeof o.superseded_by_hash === "string");
|
|
5809
5853
|
}
|
|
5810
|
-
function stripHash(
|
|
5854
|
+
function stripHash(r2) {
|
|
5811
5855
|
return {
|
|
5812
|
-
type:
|
|
5813
|
-
title:
|
|
5814
|
-
detail:
|
|
5815
|
-
importance:
|
|
5816
|
-
created_at_epoch:
|
|
5817
|
-
superseded_by_hash:
|
|
5856
|
+
type: r2.type,
|
|
5857
|
+
title: r2.title,
|
|
5858
|
+
detail: r2.detail,
|
|
5859
|
+
importance: r2.importance,
|
|
5860
|
+
created_at_epoch: r2.created_at_epoch,
|
|
5861
|
+
superseded_by_hash: r2.superseded_by_hash
|
|
5818
5862
|
};
|
|
5819
5863
|
}
|
|
5820
5864
|
function pendingExists(db, recordHash) {
|
|
@@ -5909,9 +5953,9 @@ var NOISE_PATTERNS = [
|
|
|
5909
5953
|
/system-reminder/i
|
|
5910
5954
|
];
|
|
5911
5955
|
function isSummarizableSignal(text) {
|
|
5912
|
-
const
|
|
5913
|
-
if (
|
|
5914
|
-
return !NOISE_PATTERNS.some((re) => re.test(
|
|
5956
|
+
const t2 = text.trim();
|
|
5957
|
+
if (t2.length < 25) return false;
|
|
5958
|
+
return !NOISE_PATTERNS.some((re) => re.test(t2));
|
|
5915
5959
|
}
|
|
5916
5960
|
var CREDENTIAL_LABELS = ["api[_-]?key", "secret", "password", "token"];
|
|
5917
5961
|
var LABELLED_CREDENTIAL = new RegExp(
|
|
@@ -6099,45 +6143,45 @@ async function exportSharedMemories(db, transport, opts = {}) {
|
|
|
6099
6143
|
const refusals = [];
|
|
6100
6144
|
const records = [];
|
|
6101
6145
|
const outboundInserts = [];
|
|
6102
|
-
for (const
|
|
6103
|
-
if (!isLocalOrigin(
|
|
6104
|
-
refusals.push({ observation_id:
|
|
6105
|
-
auditExportRefused(db,
|
|
6146
|
+
for (const r2 of rows) {
|
|
6147
|
+
if (!isLocalOrigin(r2.origin)) {
|
|
6148
|
+
refusals.push({ observation_id: r2.id, reason: "non_local_origin" });
|
|
6149
|
+
auditExportRefused(db, r2.id, "non_local_origin");
|
|
6106
6150
|
continue;
|
|
6107
6151
|
}
|
|
6108
|
-
const title =
|
|
6109
|
-
const detail =
|
|
6152
|
+
const title = r2.title ?? "";
|
|
6153
|
+
const detail = r2.detail ?? "";
|
|
6110
6154
|
const sTitle = containsSecret(title);
|
|
6111
6155
|
const sDetail = containsSecret(detail);
|
|
6112
6156
|
if (sTitle.matched || sDetail.matched) {
|
|
6113
6157
|
const reason = `secret:${sTitle.patternName ?? sDetail.patternName}`;
|
|
6114
|
-
refusals.push({ observation_id:
|
|
6115
|
-
auditExportRefused(db,
|
|
6158
|
+
refusals.push({ observation_id: r2.id, reason });
|
|
6159
|
+
auditExportRefused(db, r2.id, reason);
|
|
6116
6160
|
continue;
|
|
6117
6161
|
}
|
|
6118
6162
|
if (home && (title.includes(home) || detail.includes(home))) {
|
|
6119
|
-
refusals.push({ observation_id:
|
|
6120
|
-
auditExportRefused(db,
|
|
6163
|
+
refusals.push({ observation_id: r2.id, reason: "home_path" });
|
|
6164
|
+
auditExportRefused(db, r2.id, "home_path");
|
|
6121
6165
|
continue;
|
|
6122
6166
|
}
|
|
6123
6167
|
const base = {
|
|
6124
|
-
type:
|
|
6168
|
+
type: r2.type,
|
|
6125
6169
|
title,
|
|
6126
6170
|
detail,
|
|
6127
|
-
importance:
|
|
6128
|
-
created_at_epoch:
|
|
6171
|
+
importance: r2.importance,
|
|
6172
|
+
created_at_epoch: r2.created_at_epoch,
|
|
6129
6173
|
superseded_by_hash: null
|
|
6130
6174
|
};
|
|
6131
6175
|
const record_hash = hashSharedMemoryRecord(base);
|
|
6132
6176
|
if (wasExported.get(record_hash)) continue;
|
|
6133
6177
|
const recordBytes = Buffer.byteLength(JSON.stringify({ ...base, record_hash }), "utf-8");
|
|
6134
6178
|
if (recordBytes > caps.maxBytesPerRecord) {
|
|
6135
|
-
refusals.push({ observation_id:
|
|
6136
|
-
auditExportRefused(db,
|
|
6179
|
+
refusals.push({ observation_id: r2.id, reason: "record_too_large" });
|
|
6180
|
+
auditExportRefused(db, r2.id, "record_too_large");
|
|
6137
6181
|
continue;
|
|
6138
6182
|
}
|
|
6139
6183
|
records.push({ ...base, record_hash });
|
|
6140
|
-
outboundInserts.push({ hash: record_hash, id:
|
|
6184
|
+
outboundInserts.push({ hash: record_hash, id: r2.id });
|
|
6141
6185
|
}
|
|
6142
6186
|
const revokeRows = db.prepare(
|
|
6143
6187
|
`SELECT o.record_hash AS h
|
|
@@ -6147,7 +6191,7 @@ async function exportSharedMemories(db, transport, opts = {}) {
|
|
|
6147
6191
|
AND obs.expired_at IS NOT NULL
|
|
6148
6192
|
LIMIT ?`
|
|
6149
6193
|
).all(caps.maxRecords);
|
|
6150
|
-
const revokedHashes = revokeRows.map((
|
|
6194
|
+
const revokedHashes = revokeRows.map((r2) => r2.h);
|
|
6151
6195
|
if (records.length === 0 && revokedHashes.length === 0) {
|
|
6152
6196
|
return { ...zero(true), refused: refusals.length, refusals };
|
|
6153
6197
|
}
|
|
@@ -6354,75 +6398,677 @@ function backfillQualityScores(db) {
|
|
|
6354
6398
|
return backfilled;
|
|
6355
6399
|
}
|
|
6356
6400
|
|
|
6401
|
+
// src/cost-reprice.ts
|
|
6402
|
+
init_config();
|
|
6403
|
+
import { existsSync as existsSync17 } from "fs";
|
|
6404
|
+
import { dirname as dirname14, join as join12 } from "path";
|
|
6405
|
+
|
|
6406
|
+
// src/db.ts
|
|
6407
|
+
init_db_driver();
|
|
6408
|
+
import { dirname as dirname13, join as join11 } from "path";
|
|
6409
|
+
import { existsSync as existsSync16, mkdirSync as mkdirSync14, readdirSync as readdirSync3, statSync as statSync4 } from "fs";
|
|
6410
|
+
|
|
6411
|
+
// src/preflight.ts
|
|
6412
|
+
init_db_driver();
|
|
6413
|
+
init_config();
|
|
6414
|
+
|
|
6415
|
+
// src/db.ts
|
|
6416
|
+
init_config();
|
|
6417
|
+
|
|
6418
|
+
// src/lib/sql-table-names.ts
|
|
6419
|
+
init_config();
|
|
6420
|
+
function t(suffix) {
|
|
6421
|
+
return `${getConfig().toolPrefix}_${suffix}`;
|
|
6422
|
+
}
|
|
6423
|
+
|
|
6424
|
+
// src/db.ts
|
|
6425
|
+
function getDataDb() {
|
|
6426
|
+
const dbPath = getResolvedPaths().dataDbPath;
|
|
6427
|
+
const dir = dirname13(dbPath);
|
|
6428
|
+
if (!existsSync16(dir)) {
|
|
6429
|
+
mkdirSync14(dir, { recursive: true });
|
|
6430
|
+
}
|
|
6431
|
+
const db = openDatabase2(dbPath);
|
|
6432
|
+
db.pragma("journal_mode = WAL");
|
|
6433
|
+
db.pragma("foreign_keys = ON");
|
|
6434
|
+
initDataSchema(db);
|
|
6435
|
+
return db;
|
|
6436
|
+
}
|
|
6437
|
+
function initDataSchema(db) {
|
|
6438
|
+
db.exec(`
|
|
6439
|
+
CREATE TABLE IF NOT EXISTS ${t("imports")} (
|
|
6440
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
6441
|
+
source_file TEXT NOT NULL,
|
|
6442
|
+
target_file TEXT NOT NULL,
|
|
6443
|
+
import_type TEXT NOT NULL CHECK(import_type IN ('named', 'default', 'namespace', 'side_effect', 'dynamic')),
|
|
6444
|
+
imported_names TEXT NOT NULL DEFAULT '[]',
|
|
6445
|
+
line INTEGER NOT NULL DEFAULT 0
|
|
6446
|
+
);
|
|
6447
|
+
|
|
6448
|
+
CREATE INDEX IF NOT EXISTS idx_massu_imports_source ON ${t("imports")}(source_file);
|
|
6449
|
+
CREATE INDEX IF NOT EXISTS idx_massu_imports_target ON ${t("imports")}(target_file);
|
|
6450
|
+
|
|
6451
|
+
CREATE TABLE IF NOT EXISTS ${t("trpc_procedures")} (
|
|
6452
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
6453
|
+
router_file TEXT NOT NULL,
|
|
6454
|
+
router_name TEXT NOT NULL,
|
|
6455
|
+
procedure_name TEXT NOT NULL,
|
|
6456
|
+
procedure_type TEXT NOT NULL CHECK(procedure_type IN ('query', 'mutation')),
|
|
6457
|
+
has_ui_caller INTEGER NOT NULL DEFAULT 0
|
|
6458
|
+
);
|
|
6459
|
+
|
|
6460
|
+
CREATE INDEX IF NOT EXISTS idx_massu_trpc_router ON ${t("trpc_procedures")}(router_name);
|
|
6461
|
+
|
|
6462
|
+
CREATE TABLE IF NOT EXISTS ${t("trpc_call_sites")} (
|
|
6463
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
6464
|
+
procedure_id INTEGER NOT NULL,
|
|
6465
|
+
file TEXT NOT NULL,
|
|
6466
|
+
line INTEGER NOT NULL DEFAULT 0,
|
|
6467
|
+
call_pattern TEXT NOT NULL,
|
|
6468
|
+
FOREIGN KEY (procedure_id) REFERENCES ${t("trpc_procedures")}(id)
|
|
6469
|
+
);
|
|
6470
|
+
|
|
6471
|
+
CREATE INDEX IF NOT EXISTS idx_massu_call_sites_proc ON ${t("trpc_call_sites")}(procedure_id);
|
|
6472
|
+
|
|
6473
|
+
CREATE TABLE IF NOT EXISTS ${t("page_deps")} (
|
|
6474
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
6475
|
+
page_file TEXT NOT NULL,
|
|
6476
|
+
route TEXT NOT NULL,
|
|
6477
|
+
portal TEXT NOT NULL DEFAULT 'unknown',
|
|
6478
|
+
components TEXT NOT NULL DEFAULT '[]',
|
|
6479
|
+
hooks TEXT NOT NULL DEFAULT '[]',
|
|
6480
|
+
routers TEXT NOT NULL DEFAULT '[]',
|
|
6481
|
+
tables_touched TEXT NOT NULL DEFAULT '[]'
|
|
6482
|
+
);
|
|
6483
|
+
|
|
6484
|
+
CREATE INDEX IF NOT EXISTS idx_massu_page_deps_page ON ${t("page_deps")}(page_file);
|
|
6485
|
+
|
|
6486
|
+
CREATE TABLE IF NOT EXISTS ${t("middleware_tree")} (
|
|
6487
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
6488
|
+
file TEXT NOT NULL UNIQUE
|
|
6489
|
+
);
|
|
6490
|
+
|
|
6491
|
+
CREATE TABLE IF NOT EXISTS ${t("meta")} (
|
|
6492
|
+
key TEXT PRIMARY KEY,
|
|
6493
|
+
value TEXT NOT NULL
|
|
6494
|
+
);
|
|
6495
|
+
|
|
6496
|
+
-- ============================================================
|
|
6497
|
+
-- Python Code Intelligence Tables
|
|
6498
|
+
-- ============================================================
|
|
6499
|
+
|
|
6500
|
+
CREATE TABLE IF NOT EXISTS ${t("py_imports")} (
|
|
6501
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
6502
|
+
source_file TEXT NOT NULL,
|
|
6503
|
+
target_file TEXT NOT NULL,
|
|
6504
|
+
import_type TEXT NOT NULL CHECK(import_type IN ('absolute', 'relative', 'from_absolute', 'from_relative')),
|
|
6505
|
+
imported_names TEXT NOT NULL DEFAULT '[]',
|
|
6506
|
+
line INTEGER NOT NULL DEFAULT 0
|
|
6507
|
+
);
|
|
6508
|
+
CREATE INDEX IF NOT EXISTS idx_massu_py_imports_source ON ${t("py_imports")}(source_file);
|
|
6509
|
+
CREATE INDEX IF NOT EXISTS idx_massu_py_imports_target ON ${t("py_imports")}(target_file);
|
|
6510
|
+
|
|
6511
|
+
CREATE TABLE IF NOT EXISTS ${t("py_meta")} (
|
|
6512
|
+
key TEXT PRIMARY KEY,
|
|
6513
|
+
value TEXT NOT NULL
|
|
6514
|
+
);
|
|
6515
|
+
|
|
6516
|
+
CREATE TABLE IF NOT EXISTS ${t("py_routes")} (
|
|
6517
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
6518
|
+
file TEXT NOT NULL,
|
|
6519
|
+
method TEXT NOT NULL,
|
|
6520
|
+
path TEXT NOT NULL,
|
|
6521
|
+
function_name TEXT NOT NULL,
|
|
6522
|
+
dependencies TEXT NOT NULL DEFAULT '[]',
|
|
6523
|
+
request_model TEXT,
|
|
6524
|
+
response_model TEXT,
|
|
6525
|
+
is_authenticated INTEGER NOT NULL DEFAULT 0,
|
|
6526
|
+
line INTEGER NOT NULL DEFAULT 0
|
|
6527
|
+
);
|
|
6528
|
+
CREATE INDEX IF NOT EXISTS idx_massu_py_routes_path ON ${t("py_routes")}(path);
|
|
6529
|
+
CREATE INDEX IF NOT EXISTS idx_massu_py_routes_file ON ${t("py_routes")}(file);
|
|
6530
|
+
|
|
6531
|
+
CREATE TABLE IF NOT EXISTS ${t("py_route_callers")} (
|
|
6532
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
6533
|
+
route_id INTEGER NOT NULL REFERENCES ${t("py_routes")}(id) ON DELETE CASCADE,
|
|
6534
|
+
frontend_file TEXT NOT NULL,
|
|
6535
|
+
line INTEGER NOT NULL DEFAULT 0,
|
|
6536
|
+
call_pattern TEXT NOT NULL
|
|
6537
|
+
);
|
|
6538
|
+
CREATE INDEX IF NOT EXISTS idx_massu_py_route_callers_route ON ${t("py_route_callers")}(route_id);
|
|
6539
|
+
|
|
6540
|
+
CREATE TABLE IF NOT EXISTS ${t("py_models")} (
|
|
6541
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
6542
|
+
class_name TEXT NOT NULL,
|
|
6543
|
+
table_name TEXT,
|
|
6544
|
+
file TEXT NOT NULL,
|
|
6545
|
+
line INTEGER NOT NULL DEFAULT 0,
|
|
6546
|
+
columns TEXT NOT NULL DEFAULT '[]',
|
|
6547
|
+
relationships TEXT NOT NULL DEFAULT '[]',
|
|
6548
|
+
foreign_keys TEXT NOT NULL DEFAULT '[]'
|
|
6549
|
+
);
|
|
6550
|
+
CREATE INDEX IF NOT EXISTS idx_massu_py_models_file ON ${t("py_models")}(file);
|
|
6551
|
+
CREATE INDEX IF NOT EXISTS idx_massu_py_models_table ON ${t("py_models")}(table_name);
|
|
6552
|
+
|
|
6553
|
+
CREATE TABLE IF NOT EXISTS ${t("py_fk_edges")} (
|
|
6554
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
6555
|
+
source_table TEXT NOT NULL,
|
|
6556
|
+
source_column TEXT NOT NULL,
|
|
6557
|
+
target_table TEXT NOT NULL,
|
|
6558
|
+
target_column TEXT NOT NULL
|
|
6559
|
+
);
|
|
6560
|
+
CREATE INDEX IF NOT EXISTS idx_massu_py_fk_source ON ${t("py_fk_edges")}(source_table);
|
|
6561
|
+
CREATE INDEX IF NOT EXISTS idx_massu_py_fk_target ON ${t("py_fk_edges")}(target_table);
|
|
6562
|
+
|
|
6563
|
+
CREATE TABLE IF NOT EXISTS ${t("py_migrations")} (
|
|
6564
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
6565
|
+
revision TEXT NOT NULL UNIQUE,
|
|
6566
|
+
down_revision TEXT,
|
|
6567
|
+
file TEXT NOT NULL,
|
|
6568
|
+
description TEXT,
|
|
6569
|
+
operations TEXT NOT NULL DEFAULT '[]',
|
|
6570
|
+
is_head INTEGER NOT NULL DEFAULT 0
|
|
6571
|
+
);
|
|
6572
|
+
CREATE INDEX IF NOT EXISTS idx_massu_py_migrations_rev ON ${t("py_migrations")}(revision);
|
|
6573
|
+
|
|
6574
|
+
-- ============================================================
|
|
6575
|
+
-- Sentinel: Feature Registry Tables
|
|
6576
|
+
-- ============================================================
|
|
6577
|
+
|
|
6578
|
+
-- Core feature definition
|
|
6579
|
+
CREATE TABLE IF NOT EXISTS ${t("sentinel")} (
|
|
6580
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
6581
|
+
feature_key TEXT UNIQUE NOT NULL,
|
|
6582
|
+
domain TEXT NOT NULL,
|
|
6583
|
+
subdomain TEXT,
|
|
6584
|
+
title TEXT NOT NULL,
|
|
6585
|
+
description TEXT,
|
|
6586
|
+
status TEXT NOT NULL DEFAULT 'active'
|
|
6587
|
+
CHECK(status IN ('planned', 'active', 'deprecated', 'removed')),
|
|
6588
|
+
priority TEXT DEFAULT 'standard'
|
|
6589
|
+
CHECK(priority IN ('critical', 'standard', 'nice-to-have')),
|
|
6590
|
+
portal_scope TEXT DEFAULT '[]',
|
|
6591
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
6592
|
+
updated_at TEXT DEFAULT (datetime('now')),
|
|
6593
|
+
removed_at TEXT,
|
|
6594
|
+
removed_reason TEXT
|
|
6595
|
+
);
|
|
6596
|
+
|
|
6597
|
+
CREATE INDEX IF NOT EXISTS idx_sentinel_domain ON ${t("sentinel")}(domain);
|
|
6598
|
+
CREATE INDEX IF NOT EXISTS idx_sentinel_status ON ${t("sentinel")}(status);
|
|
6599
|
+
CREATE INDEX IF NOT EXISTS idx_sentinel_key ON ${t("sentinel")}(feature_key);
|
|
6600
|
+
|
|
6601
|
+
-- Feature-to-component mapping (many-to-many)
|
|
6602
|
+
CREATE TABLE IF NOT EXISTS ${t("sentinel_components")} (
|
|
6603
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
6604
|
+
feature_id INTEGER NOT NULL REFERENCES ${t("sentinel")}(id) ON DELETE CASCADE,
|
|
6605
|
+
component_file TEXT NOT NULL,
|
|
6606
|
+
component_name TEXT,
|
|
6607
|
+
role TEXT DEFAULT 'implementation'
|
|
6608
|
+
CHECK(role IN ('implementation', 'ui', 'data', 'utility')),
|
|
6609
|
+
is_primary BOOLEAN DEFAULT 0,
|
|
6610
|
+
UNIQUE(feature_id, component_file, component_name)
|
|
6611
|
+
);
|
|
6612
|
+
|
|
6613
|
+
-- Feature-to-procedure mapping (many-to-many)
|
|
6614
|
+
CREATE TABLE IF NOT EXISTS ${t("sentinel_procedures")} (
|
|
6615
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
6616
|
+
feature_id INTEGER NOT NULL REFERENCES ${t("sentinel")}(id) ON DELETE CASCADE,
|
|
6617
|
+
router_name TEXT NOT NULL,
|
|
6618
|
+
procedure_name TEXT NOT NULL,
|
|
6619
|
+
procedure_type TEXT,
|
|
6620
|
+
UNIQUE(feature_id, router_name, procedure_name)
|
|
6621
|
+
);
|
|
6622
|
+
|
|
6623
|
+
-- Feature-to-page mapping (where feature is accessible)
|
|
6624
|
+
CREATE TABLE IF NOT EXISTS ${t("sentinel_pages")} (
|
|
6625
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
6626
|
+
feature_id INTEGER NOT NULL REFERENCES ${t("sentinel")}(id) ON DELETE CASCADE,
|
|
6627
|
+
page_route TEXT NOT NULL,
|
|
6628
|
+
portal TEXT,
|
|
6629
|
+
UNIQUE(feature_id, page_route, portal)
|
|
6630
|
+
);
|
|
6631
|
+
|
|
6632
|
+
-- Feature dependency graph
|
|
6633
|
+
CREATE TABLE IF NOT EXISTS ${t("sentinel_deps")} (
|
|
6634
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
6635
|
+
feature_id INTEGER NOT NULL REFERENCES ${t("sentinel")}(id) ON DELETE CASCADE,
|
|
6636
|
+
depends_on_feature_id INTEGER NOT NULL REFERENCES ${t("sentinel")}(id) ON DELETE CASCADE,
|
|
6637
|
+
dependency_type TEXT DEFAULT 'requires'
|
|
6638
|
+
CHECK(dependency_type IN ('requires', 'enhances', 'replaces')),
|
|
6639
|
+
UNIQUE(feature_id, depends_on_feature_id)
|
|
6640
|
+
);
|
|
6641
|
+
|
|
6642
|
+
-- Feature change log (audit trail)
|
|
6643
|
+
CREATE TABLE IF NOT EXISTS ${t("sentinel_changelog")} (
|
|
6644
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
6645
|
+
feature_id INTEGER NOT NULL REFERENCES ${t("sentinel")}(id) ON DELETE CASCADE,
|
|
6646
|
+
change_type TEXT NOT NULL,
|
|
6647
|
+
changed_by TEXT,
|
|
6648
|
+
change_detail TEXT,
|
|
6649
|
+
commit_hash TEXT,
|
|
6650
|
+
created_at TEXT DEFAULT (datetime('now'))
|
|
6651
|
+
);
|
|
6652
|
+
|
|
6653
|
+
CREATE INDEX IF NOT EXISTS idx_sentinel_components_file ON ${t("sentinel_components")}(component_file);
|
|
6654
|
+
CREATE INDEX IF NOT EXISTS idx_sentinel_procedures_router ON ${t("sentinel_procedures")}(router_name);
|
|
6655
|
+
CREATE INDEX IF NOT EXISTS idx_sentinel_pages_route ON ${t("sentinel_pages")}(page_route);
|
|
6656
|
+
CREATE INDEX IF NOT EXISTS idx_sentinel_changelog_feature ON ${t("sentinel_changelog")}(feature_id);
|
|
6657
|
+
`);
|
|
6658
|
+
db.exec(`
|
|
6659
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS ${t("sentinel_fts")} USING fts5(
|
|
6660
|
+
feature_key, title, description, domain, subdomain,
|
|
6661
|
+
content=${t("sentinel")}, content_rowid=id
|
|
6662
|
+
);
|
|
6663
|
+
`);
|
|
6664
|
+
db.exec(`
|
|
6665
|
+
CREATE TRIGGER IF NOT EXISTS massu_sentinel_ai AFTER INSERT ON ${t("sentinel")} BEGIN
|
|
6666
|
+
INSERT INTO ${t("sentinel_fts")}(rowid, feature_key, title, description, domain, subdomain)
|
|
6667
|
+
VALUES (new.id, new.feature_key, new.title, new.description, new.domain, new.subdomain);
|
|
6668
|
+
END;
|
|
6669
|
+
|
|
6670
|
+
CREATE TRIGGER IF NOT EXISTS massu_sentinel_ad AFTER DELETE ON ${t("sentinel")} BEGIN
|
|
6671
|
+
INSERT INTO ${t("sentinel_fts")}(${t("sentinel_fts")}, rowid, feature_key, title, description, domain, subdomain)
|
|
6672
|
+
VALUES ('delete', old.id, old.feature_key, old.title, old.description, old.domain, old.subdomain);
|
|
6673
|
+
END;
|
|
6674
|
+
|
|
6675
|
+
CREATE TRIGGER IF NOT EXISTS massu_sentinel_au AFTER UPDATE ON ${t("sentinel")} BEGIN
|
|
6676
|
+
INSERT INTO ${t("sentinel_fts")}(${t("sentinel_fts")}, rowid, feature_key, title, description, domain, subdomain)
|
|
6677
|
+
VALUES ('delete', old.id, old.feature_key, old.title, old.description, old.domain, old.subdomain);
|
|
6678
|
+
INSERT INTO ${t("sentinel_fts")}(rowid, feature_key, title, description, domain, subdomain)
|
|
6679
|
+
VALUES (new.id, new.feature_key, new.title, new.description, new.domain, new.subdomain);
|
|
6680
|
+
END;
|
|
6681
|
+
`);
|
|
6682
|
+
}
|
|
6683
|
+
|
|
6684
|
+
// src/feature-cost-attribution.ts
|
|
6685
|
+
import { isAbsolute as isAbsolute2, relative as relative2, resolve as resolve8 } from "path";
|
|
6686
|
+
|
|
6687
|
+
// src/sentinel-db.ts
|
|
6688
|
+
init_config();
|
|
6689
|
+
init_memory_db();
|
|
6690
|
+
var PROJECT_ROOT = getProjectRoot();
|
|
6691
|
+
function parsePortalScope(raw) {
|
|
6692
|
+
if (!raw) return [];
|
|
6693
|
+
try {
|
|
6694
|
+
return JSON.parse(raw);
|
|
6695
|
+
} catch {
|
|
6696
|
+
return [];
|
|
6697
|
+
}
|
|
6698
|
+
}
|
|
6699
|
+
function toFeature(row) {
|
|
6700
|
+
return {
|
|
6701
|
+
id: row.id,
|
|
6702
|
+
feature_key: row.feature_key,
|
|
6703
|
+
domain: row.domain,
|
|
6704
|
+
subdomain: row.subdomain || null,
|
|
6705
|
+
title: row.title,
|
|
6706
|
+
description: row.description || null,
|
|
6707
|
+
status: row.status,
|
|
6708
|
+
priority: row.priority,
|
|
6709
|
+
portal_scope: parsePortalScope(row.portal_scope),
|
|
6710
|
+
created_at: row.created_at,
|
|
6711
|
+
updated_at: row.updated_at,
|
|
6712
|
+
removed_at: row.removed_at || null,
|
|
6713
|
+
removed_reason: row.removed_reason || null
|
|
6714
|
+
};
|
|
6715
|
+
}
|
|
6716
|
+
function getFeaturesByFile(db, filePath) {
|
|
6717
|
+
const rows = db.prepare(`
|
|
6718
|
+
SELECT DISTINCT s.* FROM ${t("sentinel")} s
|
|
6719
|
+
JOIN ${t("sentinel_components")} c ON c.feature_id = s.id
|
|
6720
|
+
WHERE c.component_file = ?
|
|
6721
|
+
ORDER BY s.feature_key
|
|
6722
|
+
LIMIT 1000
|
|
6723
|
+
`).all(filePath);
|
|
6724
|
+
return rows.map(toFeature);
|
|
6725
|
+
}
|
|
6726
|
+
|
|
6727
|
+
// src/feature-cost-attribution.ts
|
|
6728
|
+
var UNATTRIBUTED_FEATURE = "(no registered feature)";
|
|
6729
|
+
var MAX_SESSION_FILES = 2e4;
|
|
6730
|
+
function sessionFileTouches(memDb, sessionId, projectRoot) {
|
|
6731
|
+
const rows = memDb.prepare(
|
|
6732
|
+
`SELECT j.value AS file, COUNT(*) AS n
|
|
6733
|
+
FROM tool_call_details t, json_each(t.files_involved) j
|
|
6734
|
+
WHERE t.session_id = ? AND json_valid(t.files_involved)
|
|
6735
|
+
AND json_type(t.files_involved) = 'array' AND j.type = 'text' AND j.value <> ''
|
|
6736
|
+
GROUP BY j.value
|
|
6737
|
+
LIMIT ?`
|
|
6738
|
+
).all(sessionId, MAX_SESSION_FILES + 1);
|
|
6739
|
+
if (rows.length > MAX_SESSION_FILES) {
|
|
6740
|
+
throw new Error(`session ${sessionId} touched more than ${MAX_SESSION_FILES} distinct files; not allocating a truncated list`);
|
|
6741
|
+
}
|
|
6742
|
+
const touches = /* @__PURE__ */ new Map();
|
|
6743
|
+
for (const { file, n } of rows) {
|
|
6744
|
+
const abs = isAbsolute2(file) ? file : resolve8(projectRoot, file);
|
|
6745
|
+
const rel = relative2(projectRoot, abs);
|
|
6746
|
+
const key = rel.startsWith("..") || isAbsolute2(rel) ? abs : rel;
|
|
6747
|
+
touches.set(key, (touches.get(key) ?? 0) + Number(n));
|
|
6748
|
+
}
|
|
6749
|
+
return touches;
|
|
6750
|
+
}
|
|
6751
|
+
function allocateSessionCost(touches, featuresOf, totals) {
|
|
6752
|
+
const units = /* @__PURE__ */ new Map();
|
|
6753
|
+
let all = 0;
|
|
6754
|
+
for (const [file, n] of touches) {
|
|
6755
|
+
all += n;
|
|
6756
|
+
const features = [...new Set(featuresOf(file))];
|
|
6757
|
+
if (features.length === 0) {
|
|
6758
|
+
units.set(UNATTRIBUTED_FEATURE, (units.get(UNATTRIBUTED_FEATURE) ?? 0) + n);
|
|
6759
|
+
} else {
|
|
6760
|
+
for (const k of features) units.set(k, (units.get(k) ?? 0) + n / features.length);
|
|
6761
|
+
}
|
|
6762
|
+
}
|
|
6763
|
+
if (all === 0) return [{ featureKey: UNATTRIBUTED_FEATURE, share: 1, tokens: totals.tokens, costUsd: totals.costUsd }];
|
|
6764
|
+
return [...units].map(([featureKey, u]) => ({
|
|
6765
|
+
featureKey,
|
|
6766
|
+
share: u / all,
|
|
6767
|
+
tokens: Math.round(totals.tokens * u / all),
|
|
6768
|
+
costUsd: totals.costUsd * u / all
|
|
6769
|
+
})).sort((a, b) => b.costUsd - a.costUsd || a.featureKey.localeCompare(b.featureKey));
|
|
6770
|
+
}
|
|
6771
|
+
function attributeSessionCost(memDb, dataDb, sessionId, projectRoot) {
|
|
6772
|
+
const cost = memDb.prepare(
|
|
6773
|
+
`SELECT total_tokens, estimated_cost_usd, cost_status FROM session_costs WHERE session_id = ?`
|
|
6774
|
+
).get(sessionId);
|
|
6775
|
+
const replace = memDb.transaction((rows2) => {
|
|
6776
|
+
memDb.prepare("DELETE FROM feature_costs WHERE session_id = ?").run(sessionId);
|
|
6777
|
+
const ins = memDb.prepare(
|
|
6778
|
+
"INSERT INTO feature_costs (feature_key, session_id, tokens_used, estimated_cost_usd) VALUES (?, ?, ?, ?)"
|
|
6779
|
+
);
|
|
6780
|
+
for (const r2 of rows2) ins.run(r2.featureKey, sessionId, r2.tokens, r2.costUsd);
|
|
6781
|
+
});
|
|
6782
|
+
if (!cost || cost.cost_status === "unknown") {
|
|
6783
|
+
replace([]);
|
|
6784
|
+
return [];
|
|
6785
|
+
}
|
|
6786
|
+
const touches = sessionFileTouches(memDb, sessionId, projectRoot);
|
|
6787
|
+
const featuresOf = (file) => {
|
|
6788
|
+
const keys = getFeaturesByFile(dataDb, file).map((f) => f.feature_key);
|
|
6789
|
+
if (!isAbsolute2(file)) keys.push(...getFeaturesByFile(dataDb, resolve8(projectRoot, file)).map((f) => f.feature_key));
|
|
6790
|
+
return keys;
|
|
6791
|
+
};
|
|
6792
|
+
const rows = allocateSessionCost(touches, featuresOf, { tokens: cost.total_tokens, costUsd: cost.estimated_cost_usd });
|
|
6793
|
+
replace(rows);
|
|
6794
|
+
return rows;
|
|
6795
|
+
}
|
|
6796
|
+
|
|
6797
|
+
// src/cost-reprice.ts
|
|
6798
|
+
init_hook_failure_signal();
|
|
6799
|
+
init_memory_db();
|
|
6800
|
+
|
|
6357
6801
|
// src/cost-tracker.ts
|
|
6358
6802
|
init_config();
|
|
6359
|
-
|
|
6360
|
-
|
|
6361
|
-
|
|
6362
|
-
|
|
6363
|
-
|
|
6364
|
-
|
|
6803
|
+
|
|
6804
|
+
// src/model-pricing.ts
|
|
6805
|
+
var PRICING_VERSION = 1;
|
|
6806
|
+
var r = (input, cacheWrite5m, cacheWrite1h, cacheRead, output) => ({ input, cacheWrite5m, cacheWrite1h, cacheRead, output });
|
|
6807
|
+
var US = 1.1;
|
|
6808
|
+
var BUILT_IN_PRICES = {
|
|
6809
|
+
"claude-fable-5-1": { ...r(10, 12.5, 20, 0.25, 50), usGeoMultiplier: US },
|
|
6810
|
+
"claude-mythos-5-1": { ...r(10, 12.5, 20, 0.25, 50), usGeoMultiplier: US },
|
|
6811
|
+
"claude-fable-5": { ...r(10, 12.5, 20, 1, 50), usGeoMultiplier: US },
|
|
6812
|
+
"claude-mythos-5": { ...r(10, 12.5, 20, 1, 50), usGeoMultiplier: US },
|
|
6813
|
+
"claude-opus-5-5": { ...r(4, 5, 8, 0.2, 20), fast: { input: 8, output: 40 }, usGeoMultiplier: US },
|
|
6814
|
+
"claude-opus-5": { ...r(5, 6.25, 10, 0.5, 25), fast: { input: 10, output: 50 }, usGeoMultiplier: US },
|
|
6815
|
+
"claude-opus-4-8": { ...r(5, 6.25, 10, 0.5, 25), fast: { input: 10, output: 50 }, usGeoMultiplier: US },
|
|
6816
|
+
"claude-opus-4-7": { ...r(5, 6.25, 10, 0.5, 25), usGeoMultiplier: US },
|
|
6817
|
+
"claude-opus-4-6": { ...r(5, 6.25, 10, 0.5, 25), usGeoMultiplier: US },
|
|
6818
|
+
"claude-opus-4-5": r(5, 6.25, 10, 0.5, 25),
|
|
6819
|
+
"claude-opus-4-1": r(15, 18.75, 30, 1.5, 75),
|
|
6820
|
+
"claude-opus-4": r(15, 18.75, 30, 1.5, 75),
|
|
6821
|
+
"claude-sonnet-5-5": { ...r(2, 2.5, 4, 0.1, 10), usGeoMultiplier: US },
|
|
6822
|
+
"claude-sonnet-5": { ...r(2, 2.5, 4, 0.2, 10), usGeoMultiplier: US },
|
|
6823
|
+
"claude-sonnet-4-6": { ...r(3, 3.75, 6, 0.3, 15), usGeoMultiplier: US },
|
|
6824
|
+
"claude-sonnet-4-5": r(3, 3.75, 6, 0.3, 15),
|
|
6825
|
+
"claude-sonnet-4": r(3, 3.75, 6, 0.3, 15),
|
|
6826
|
+
"claude-haiku-5-5": {
|
|
6827
|
+
...r(0.1, 0.125, 0.2, 0.01, 0.5),
|
|
6828
|
+
longPrompt: { overTokens: 1e5, rates: r(0.5, 0.625, 1, 0.05, 2.5) },
|
|
6829
|
+
usGeoMultiplier: US
|
|
6830
|
+
},
|
|
6831
|
+
"claude-haiku-4-5": r(1, 1.25, 2, 0.1, 5)
|
|
6365
6832
|
};
|
|
6366
|
-
function
|
|
6367
|
-
return
|
|
6368
|
-
}
|
|
6369
|
-
function
|
|
6370
|
-
|
|
6371
|
-
|
|
6372
|
-
|
|
6373
|
-
|
|
6374
|
-
|
|
6375
|
-
|
|
6376
|
-
|
|
6377
|
-
|
|
6378
|
-
|
|
6379
|
-
|
|
6380
|
-
|
|
6381
|
-
|
|
6382
|
-
|
|
6383
|
-
|
|
6384
|
-
|
|
6385
|
-
|
|
6386
|
-
|
|
6387
|
-
|
|
6388
|
-
|
|
6389
|
-
|
|
6390
|
-
|
|
6391
|
-
|
|
6392
|
-
}
|
|
6393
|
-
function calculateCost(usage) {
|
|
6394
|
-
const pricing = getModelPricing();
|
|
6395
|
-
const modelPricing = pricing[usage.model] ?? pricing["default"] ?? pricing["claude-sonnet-4-5"] ?? { input_per_million: 3, output_per_million: 15 };
|
|
6396
|
-
const inputCost = usage.inputTokens / 1e6 * modelPricing.input_per_million;
|
|
6397
|
-
const outputCost = usage.outputTokens / 1e6 * modelPricing.output_per_million;
|
|
6398
|
-
const cacheReadCost = usage.cacheReadTokens / 1e6 * (modelPricing.cache_read_per_million ?? 0);
|
|
6399
|
-
const cacheWriteCost = usage.cacheWriteTokens / 1e6 * (modelPricing.cache_write_per_million ?? 0);
|
|
6833
|
+
function normalizeModelId(model) {
|
|
6834
|
+
return model.trim().replace(/-\d{8}$/, "");
|
|
6835
|
+
}
|
|
6836
|
+
function resolveModelPrice(model, overrides = {}) {
|
|
6837
|
+
if (!model) return null;
|
|
6838
|
+
const id = normalizeModelId(model);
|
|
6839
|
+
const builtIn = BUILT_IN_PRICES[id];
|
|
6840
|
+
const o = overrides[model] ?? overrides[id];
|
|
6841
|
+
if (!o) return builtIn ?? null;
|
|
6842
|
+
const input = o.input_per_million;
|
|
6843
|
+
return {
|
|
6844
|
+
...builtIn,
|
|
6845
|
+
input,
|
|
6846
|
+
output: o.output_per_million,
|
|
6847
|
+
cacheRead: o.cache_read_per_million ?? builtIn?.cacheRead ?? input * 0.1,
|
|
6848
|
+
cacheWrite5m: o.cache_write_per_million ?? builtIn?.cacheWrite5m ?? input * 1.25,
|
|
6849
|
+
cacheWrite1h: o.cache_write_1h_per_million ?? builtIn?.cacheWrite1h ?? input * 2
|
|
6850
|
+
};
|
|
6851
|
+
}
|
|
6852
|
+
function tokenCounts(u) {
|
|
6853
|
+
const n = (v) => typeof v === "number" && Number.isFinite(v) && v > 0 ? v : 0;
|
|
6854
|
+
const total = n(u.cache_creation_input_tokens);
|
|
6855
|
+
const has5m = typeof u.cache_creation?.ephemeral_5m_input_tokens === "number";
|
|
6856
|
+
const has1h = typeof u.cache_creation?.ephemeral_1h_input_tokens === "number";
|
|
6857
|
+
const w1h = has1h ? n(u.cache_creation?.ephemeral_1h_input_tokens) : 0;
|
|
6858
|
+
const w5m = has5m ? n(u.cache_creation?.ephemeral_5m_input_tokens) : total - w1h;
|
|
6400
6859
|
return {
|
|
6401
|
-
|
|
6402
|
-
|
|
6403
|
-
|
|
6404
|
-
|
|
6405
|
-
|
|
6406
|
-
|
|
6860
|
+
input: n(u.input_tokens),
|
|
6861
|
+
output: n(u.output_tokens),
|
|
6862
|
+
cacheRead: n(u.cache_read_input_tokens),
|
|
6863
|
+
cacheWrite5m: Math.max(0, w5m),
|
|
6864
|
+
cacheWrite1h: w1h
|
|
6865
|
+
};
|
|
6866
|
+
}
|
|
6867
|
+
function priceResponse(price, u) {
|
|
6868
|
+
if (!price) return null;
|
|
6869
|
+
const t2 = tokenCounts(u);
|
|
6870
|
+
const prompt = t2.input + t2.cacheRead + t2.cacheWrite5m + t2.cacheWrite1h;
|
|
6871
|
+
let rates = price.longPrompt && prompt > price.longPrompt.overTokens ? price.longPrompt.rates : price;
|
|
6872
|
+
if (u.speed === "fast" && price.fast) {
|
|
6873
|
+
const k = price.fast.input / rates.input;
|
|
6874
|
+
rates = {
|
|
6875
|
+
input: price.fast.input,
|
|
6876
|
+
output: price.fast.output,
|
|
6877
|
+
cacheRead: rates.cacheRead * k,
|
|
6878
|
+
cacheWrite5m: rates.cacheWrite5m * k,
|
|
6879
|
+
cacheWrite1h: rates.cacheWrite1h * k
|
|
6880
|
+
};
|
|
6881
|
+
}
|
|
6882
|
+
const geo = u.inference_geo === "us" ? price.usGeoMultiplier ?? 1 : 1;
|
|
6883
|
+
const dollars = t2.input * rates.input + t2.output * rates.output + t2.cacheRead * rates.cacheRead + t2.cacheWrite5m * rates.cacheWrite5m + t2.cacheWrite1h * rates.cacheWrite1h;
|
|
6884
|
+
return dollars / 1e6 * geo;
|
|
6885
|
+
}
|
|
6886
|
+
|
|
6887
|
+
// src/cost-tracker.ts
|
|
6888
|
+
function configPriceOverrides() {
|
|
6889
|
+
return getConfig().analytics?.cost?.models ?? {};
|
|
6890
|
+
}
|
|
6891
|
+
function costTranscriptEntries(entries, opts = {}) {
|
|
6892
|
+
const overrides = configPriceOverrides();
|
|
6893
|
+
const out = {
|
|
6894
|
+
inputTokens: 0,
|
|
6895
|
+
outputTokens: 0,
|
|
6896
|
+
cacheReadTokens: 0,
|
|
6897
|
+
cacheWriteTokens: 0,
|
|
6898
|
+
costUsd: 0,
|
|
6899
|
+
pricedResponses: 0,
|
|
6900
|
+
unpricedResponses: 0,
|
|
6901
|
+
unpricedModels: [],
|
|
6902
|
+
model: "",
|
|
6903
|
+
lastMessageId: null
|
|
6407
6904
|
};
|
|
6905
|
+
const seen = /* @__PURE__ */ new Set();
|
|
6906
|
+
if (opts.skipMessageId) seen.add(opts.skipMessageId);
|
|
6907
|
+
const unpriced = /* @__PURE__ */ new Set();
|
|
6908
|
+
for (const entry of entries) {
|
|
6909
|
+
const msg = entry.message;
|
|
6910
|
+
if (entry.type !== "assistant" || !msg?.usage) continue;
|
|
6911
|
+
if (msg.id) {
|
|
6912
|
+
if (seen.has(msg.id)) continue;
|
|
6913
|
+
seen.add(msg.id);
|
|
6914
|
+
out.lastMessageId = msg.id;
|
|
6915
|
+
}
|
|
6916
|
+
const usage = msg.usage;
|
|
6917
|
+
const t2 = tokenCounts(usage);
|
|
6918
|
+
if (t2.input + t2.output + t2.cacheRead + t2.cacheWrite5m + t2.cacheWrite1h === 0) continue;
|
|
6919
|
+
out.inputTokens += t2.input;
|
|
6920
|
+
out.outputTokens += t2.output;
|
|
6921
|
+
out.cacheReadTokens += t2.cacheRead;
|
|
6922
|
+
out.cacheWriteTokens += t2.cacheWrite5m + t2.cacheWrite1h;
|
|
6923
|
+
const dollars = priceResponse(resolveModelPrice(msg.model, overrides), usage);
|
|
6924
|
+
if (dollars === null) {
|
|
6925
|
+
out.unpricedResponses++;
|
|
6926
|
+
unpriced.add(msg.model ?? "unknown");
|
|
6927
|
+
} else {
|
|
6928
|
+
out.costUsd += dollars;
|
|
6929
|
+
out.pricedResponses++;
|
|
6930
|
+
}
|
|
6931
|
+
if (msg.model) out.model = msg.model;
|
|
6932
|
+
}
|
|
6933
|
+
out.unpricedModels = [...unpriced].sort();
|
|
6934
|
+
return out;
|
|
6408
6935
|
}
|
|
6409
|
-
function
|
|
6410
|
-
|
|
6936
|
+
function costStatusOf(priced, unpriced) {
|
|
6937
|
+
if (unpriced === 0) return "priced";
|
|
6938
|
+
return priced === 0 ? "unknown" : "partial";
|
|
6939
|
+
}
|
|
6940
|
+
function storeSessionCost(db, sessionId, slice, mode = "replace") {
|
|
6941
|
+
const prev = mode === "add" ? db.prepare(
|
|
6942
|
+
`SELECT input_tokens, output_tokens, cache_read_tokens, cache_write_tokens, estimated_cost_usd,
|
|
6943
|
+
priced_responses, unpriced_responses, unpriced_models, model
|
|
6944
|
+
FROM session_costs WHERE session_id = ?`
|
|
6945
|
+
).get(sessionId) : void 0;
|
|
6946
|
+
const num = (k) => Number(prev?.[k] ?? 0) || 0;
|
|
6947
|
+
const input = num("input_tokens") + slice.inputTokens;
|
|
6948
|
+
const output = num("output_tokens") + slice.outputTokens;
|
|
6949
|
+
const cacheRead = num("cache_read_tokens") + slice.cacheReadTokens;
|
|
6950
|
+
const cacheWrite = num("cache_write_tokens") + slice.cacheWriteTokens;
|
|
6951
|
+
const priced = num("priced_responses") + slice.pricedResponses;
|
|
6952
|
+
const unpricedCount = num("unpriced_responses") + slice.unpricedResponses;
|
|
6953
|
+
const unpricedModels = [.../* @__PURE__ */ new Set([
|
|
6954
|
+
...String(prev?.unpriced_models ?? "").split(",").filter(Boolean),
|
|
6955
|
+
...slice.unpricedModels
|
|
6956
|
+
])].sort().join(",");
|
|
6957
|
+
const model = slice.model || String(prev?.model ?? "") || null;
|
|
6411
6958
|
db.prepare(`
|
|
6412
6959
|
INSERT INTO session_costs
|
|
6413
|
-
|
|
6414
|
-
|
|
6415
|
-
|
|
6960
|
+
(session_id, model, input_tokens, output_tokens, cache_read_tokens, cache_write_tokens,
|
|
6961
|
+
total_tokens, estimated_cost_usd, priced_responses, unpriced_responses, unpriced_models,
|
|
6962
|
+
cost_status, pricing_version)
|
|
6963
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
6964
|
+
ON CONFLICT(session_id) DO UPDATE SET
|
|
6965
|
+
model = excluded.model, input_tokens = excluded.input_tokens,
|
|
6966
|
+
output_tokens = excluded.output_tokens, cache_read_tokens = excluded.cache_read_tokens,
|
|
6967
|
+
cache_write_tokens = excluded.cache_write_tokens, total_tokens = excluded.total_tokens,
|
|
6968
|
+
estimated_cost_usd = excluded.estimated_cost_usd,
|
|
6969
|
+
priced_responses = excluded.priced_responses, unpriced_responses = excluded.unpriced_responses,
|
|
6970
|
+
unpriced_models = excluded.unpriced_models, cost_status = excluded.cost_status,
|
|
6971
|
+
pricing_version = excluded.pricing_version
|
|
6416
6972
|
`).run(
|
|
6417
6973
|
sessionId,
|
|
6418
|
-
|
|
6419
|
-
|
|
6420
|
-
|
|
6421
|
-
|
|
6422
|
-
|
|
6423
|
-
|
|
6424
|
-
|
|
6974
|
+
model,
|
|
6975
|
+
input,
|
|
6976
|
+
output,
|
|
6977
|
+
cacheRead,
|
|
6978
|
+
cacheWrite,
|
|
6979
|
+
input + output + cacheRead + cacheWrite,
|
|
6980
|
+
num("estimated_cost_usd") + slice.costUsd,
|
|
6981
|
+
priced,
|
|
6982
|
+
unpricedCount,
|
|
6983
|
+
unpricedModels,
|
|
6984
|
+
costStatusOf(priced, unpricedCount),
|
|
6985
|
+
PRICING_VERSION
|
|
6986
|
+
);
|
|
6987
|
+
}
|
|
6988
|
+
function markSessionCostUnknown(db, sessionId) {
|
|
6989
|
+
db.prepare(`
|
|
6990
|
+
INSERT INTO session_costs (session_id, cost_status, pricing_version) VALUES (?, 'unknown', ?)
|
|
6991
|
+
ON CONFLICT(session_id) DO UPDATE SET
|
|
6992
|
+
input_tokens = 0, output_tokens = 0, cache_read_tokens = 0, cache_write_tokens = 0,
|
|
6993
|
+
total_tokens = 0, estimated_cost_usd = 0, priced_responses = 0, unpriced_responses = 0,
|
|
6994
|
+
unpriced_models = '', cost_status = 'unknown', pricing_version = excluded.pricing_version
|
|
6995
|
+
`).run(sessionId, PRICING_VERSION);
|
|
6996
|
+
}
|
|
6997
|
+
|
|
6998
|
+
// src/cost-reprice.ts
|
|
6999
|
+
var cursorKey = (sid) => `cost_processed_line:${sid}`;
|
|
7000
|
+
var lastIdKey = (sid) => `cost_last_message:${sid}`;
|
|
7001
|
+
var sessionLeaseKey = (sid) => `session_end_lease:${sid}`;
|
|
7002
|
+
async function updateSessionCost(db, sessionId, transcriptPath, attribute = attributeFeatureCosts) {
|
|
7003
|
+
const row = db.prepare("SELECT pricing_version FROM session_costs WHERE session_id = ?").get(sessionId);
|
|
7004
|
+
const cursor = Number(getMemoryMeta(db, cursorKey(sessionId)) ?? "0") || 0;
|
|
7005
|
+
const incremental = row?.pricing_version === PRICING_VERSION && cursor > 0;
|
|
7006
|
+
const from = incremental ? cursor : 0;
|
|
7007
|
+
const { entries, totalLines } = await parseTranscriptFrom(transcriptPath, from);
|
|
7008
|
+
const slice = costTranscriptEntries(entries, {
|
|
7009
|
+
skipMessageId: incremental ? getMemoryMeta(db, lastIdKey(sessionId)) : null
|
|
7010
|
+
});
|
|
7011
|
+
storeSessionCost(db, sessionId, slice, incremental ? "add" : "replace");
|
|
7012
|
+
setMemoryMeta(db, cursorKey(sessionId), String(totalLines));
|
|
7013
|
+
if (slice.lastMessageId) setMemoryMeta(db, lastIdKey(sessionId), slice.lastMessageId);
|
|
7014
|
+
attribute(db, sessionId);
|
|
7015
|
+
}
|
|
7016
|
+
var attributeFeatureCosts = (db, sessionId) => {
|
|
7017
|
+
let dataDb = null;
|
|
7018
|
+
try {
|
|
7019
|
+
dataDb = getDataDb();
|
|
7020
|
+
attributeSessionCost(db, dataDb, sessionId, getProjectRoot());
|
|
7021
|
+
} catch (err) {
|
|
7022
|
+
recordHookFailure("cost:feature-attribution", err, { sessionId });
|
|
7023
|
+
} finally {
|
|
7024
|
+
dataDb?.close();
|
|
7025
|
+
}
|
|
7026
|
+
};
|
|
7027
|
+
function defaultTranscriptFor(sessionId) {
|
|
7028
|
+
if (!/^[A-Za-z0-9_-]+$/.test(sessionId)) return null;
|
|
7029
|
+
const path = join12(dirname14(getResolvedPaths().memoryDir), `${sessionId}.jsonl`);
|
|
7030
|
+
return existsSync17(path) ? path : null;
|
|
7031
|
+
}
|
|
7032
|
+
async function repriceStoredSessions(db, opts = {}) {
|
|
7033
|
+
const deadlineAt = opts.deadlineAt ?? Infinity;
|
|
7034
|
+
const find = opts.findTranscript ?? defaultTranscriptFor;
|
|
7035
|
+
const out = { repriced: 0, markedUnknown: 0, busy: 0, failed: 0, remaining: 0 };
|
|
7036
|
+
const stale = db.prepare(
|
|
7037
|
+
`SELECT session_id FROM session_costs WHERE pricing_version < ? ORDER BY id LIMIT ?`
|
|
6425
7038
|
);
|
|
7039
|
+
const ttlSec = 120;
|
|
7040
|
+
const tried = /* @__PURE__ */ new Set();
|
|
7041
|
+
outer: while (Date.now() < deadlineAt) {
|
|
7042
|
+
const ids = stale.all(PRICING_VERSION, opts.batch ?? 50).map((r2) => r2.session_id).filter((sid) => !tried.has(sid));
|
|
7043
|
+
if (ids.length === 0) break;
|
|
7044
|
+
for (const sid of ids) {
|
|
7045
|
+
if (Date.now() >= deadlineAt) break outer;
|
|
7046
|
+
tried.add(sid);
|
|
7047
|
+
if (!tryAcquireMetaLease(db, sessionLeaseKey(sid), ttlSec)) {
|
|
7048
|
+
out.busy++;
|
|
7049
|
+
continue;
|
|
7050
|
+
}
|
|
7051
|
+
try {
|
|
7052
|
+
const path = find(sid);
|
|
7053
|
+
if (path) {
|
|
7054
|
+
await updateSessionCost(db, sid, path, opts.attribute);
|
|
7055
|
+
out.repriced++;
|
|
7056
|
+
} else {
|
|
7057
|
+
markSessionCostUnknown(db, sid);
|
|
7058
|
+
(opts.attribute ?? attributeFeatureCosts)(db, sid);
|
|
7059
|
+
out.markedUnknown++;
|
|
7060
|
+
}
|
|
7061
|
+
} catch (err) {
|
|
7062
|
+
out.failed++;
|
|
7063
|
+
process.stderr.write(`[massu] cost reprice: ${sid}: ${err instanceof Error ? err.message : String(err)}
|
|
7064
|
+
`);
|
|
7065
|
+
} finally {
|
|
7066
|
+
releaseMetaLease(db, sessionLeaseKey(sid));
|
|
7067
|
+
}
|
|
7068
|
+
}
|
|
7069
|
+
}
|
|
7070
|
+
out.remaining = db.prepare("SELECT COUNT(*) n FROM session_costs WHERE pricing_version < ?").get(PRICING_VERSION).n;
|
|
7071
|
+
return out;
|
|
6426
7072
|
}
|
|
6427
7073
|
|
|
6428
7074
|
// src/prompt-analyzer.ts
|
|
@@ -6542,8 +7188,8 @@ init_memory_vector();
|
|
|
6542
7188
|
function temporalPredicate(asOf, includeSuperseded, prefix) {
|
|
6543
7189
|
const c = (name) => `${prefix}${name}`;
|
|
6544
7190
|
if (asOf != null && Number.isFinite(asOf)) {
|
|
6545
|
-
const
|
|
6546
|
-
return `${c("ingested_at_epoch")} <= ${
|
|
7191
|
+
const t2 = Math.floor(Number(asOf) / 1e3);
|
|
7192
|
+
return `${c("ingested_at_epoch")} <= ${t2} AND (${c("expired_at_epoch")} IS NULL OR ${c("expired_at_epoch")} > ${t2}) AND ${c("valid_from_epoch")} <= ${t2} AND (${c("valid_to_epoch")} IS NULL OR ${c("valid_to_epoch")} > ${t2})`;
|
|
6547
7193
|
}
|
|
6548
7194
|
if (!includeSuperseded) {
|
|
6549
7195
|
return `${c("expired_at_epoch")} IS NULL`;
|
|
@@ -6589,14 +7235,14 @@ function collectObservations(memDb, queryText, pool, nowMs, loadVec, modelId, di
|
|
|
6589
7235
|
WHERE observations_fts MATCH ?${bm25Temporal ? ` AND ${bm25Temporal}` : ""}
|
|
6590
7236
|
ORDER BY rank LIMIT ?`
|
|
6591
7237
|
).all(sanitizeFts5QueryOr(queryText), pool);
|
|
6592
|
-
rows.forEach((
|
|
6593
|
-
map.set(
|
|
6594
|
-
id:
|
|
7238
|
+
rows.forEach((r2, i) => {
|
|
7239
|
+
map.set(r2.id, {
|
|
7240
|
+
id: r2.id,
|
|
6595
7241
|
source: "observation",
|
|
6596
|
-
title:
|
|
6597
|
-
snippet: snippetOf(
|
|
6598
|
-
importance:
|
|
6599
|
-
ageDays: ageDaysFrom(
|
|
7242
|
+
title: r2.title,
|
|
7243
|
+
snippet: snippetOf(r2.detail ?? r2.title),
|
|
7244
|
+
importance: r2.importance,
|
|
7245
|
+
ageDays: ageDaysFrom(r2.created_at_epoch * 1e3, nowMs),
|
|
6600
7246
|
bm25Order: i,
|
|
6601
7247
|
vecs: []
|
|
6602
7248
|
});
|
|
@@ -6607,15 +7253,15 @@ function collectObservations(memDb, queryText, pool, nowMs, loadVec, modelId, di
|
|
|
6607
7253
|
`SELECT id, title, detail, importance, created_at_epoch
|
|
6608
7254
|
FROM observations${plainTemporal ? ` WHERE ${plainTemporal}` : ""} ORDER BY created_at_epoch DESC LIMIT ?`
|
|
6609
7255
|
).all(pool);
|
|
6610
|
-
for (const
|
|
6611
|
-
if (!map.has(
|
|
6612
|
-
map.set(
|
|
6613
|
-
id:
|
|
7256
|
+
for (const r2 of recent) {
|
|
7257
|
+
if (!map.has(r2.id)) {
|
|
7258
|
+
map.set(r2.id, {
|
|
7259
|
+
id: r2.id,
|
|
6614
7260
|
source: "observation",
|
|
6615
|
-
title:
|
|
6616
|
-
snippet: snippetOf(
|
|
6617
|
-
importance:
|
|
6618
|
-
ageDays: ageDaysFrom(
|
|
7261
|
+
title: r2.title,
|
|
7262
|
+
snippet: snippetOf(r2.detail ?? r2.title),
|
|
7263
|
+
importance: r2.importance,
|
|
7264
|
+
ageDays: ageDaysFrom(r2.created_at_epoch * 1e3, nowMs),
|
|
6619
7265
|
bm25Order: null,
|
|
6620
7266
|
vecs: []
|
|
6621
7267
|
});
|
|
@@ -6639,7 +7285,7 @@ function collectObservations(memDb, queryText, pool, nowMs, loadVec, modelId, di
|
|
|
6639
7285
|
}
|
|
6640
7286
|
function collectLikeSource(db, source, cfg, queryText, pool, nowMs) {
|
|
6641
7287
|
const map = /* @__PURE__ */ new Map();
|
|
6642
|
-
const tokens = queryText.toLowerCase().replace(/[^a-z0-9\s]/g, " ").split(/\s+/).filter((
|
|
7288
|
+
const tokens = queryText.toLowerCase().replace(/[^a-z0-9\s]/g, " ").split(/\s+/).filter((t2) => t2.length >= 3).slice(0, 8);
|
|
6643
7289
|
const ageExpr = (created) => {
|
|
6644
7290
|
const ms = cfg.createdIsEpoch ? Number(created) * 1e3 : Date.parse(String(created));
|
|
6645
7291
|
return Number.isFinite(ms) ? ageDaysFrom(ms, nowMs) : 0;
|
|
@@ -6647,7 +7293,7 @@ function collectLikeSource(db, source, cfg, queryText, pool, nowMs) {
|
|
|
6647
7293
|
if (tokens.length > 0) {
|
|
6648
7294
|
const likeClause = cfg.searchCols.map((col) => tokens.map(() => `${col} LIKE ?`).join(" OR ")).join(" OR ");
|
|
6649
7295
|
const params = [];
|
|
6650
|
-
for (const _col of cfg.searchCols) for (const
|
|
7296
|
+
for (const _col of cfg.searchCols) for (const t2 of tokens) params.push(`%${t2}%`);
|
|
6651
7297
|
try {
|
|
6652
7298
|
const rows = db.prepare(
|
|
6653
7299
|
`SELECT ${cfg.idCol} AS id, ${cfg.titleExpr} AS title,
|
|
@@ -6655,14 +7301,14 @@ function collectLikeSource(db, source, cfg, queryText, pool, nowMs) {
|
|
|
6655
7301
|
FROM ${cfg.table} WHERE (${likeClause})${cfg.temporalClause ? ` AND ${cfg.temporalClause}` : ""}
|
|
6656
7302
|
ORDER BY ${cfg.createdCol} DESC LIMIT ?`
|
|
6657
7303
|
).all(...params, pool);
|
|
6658
|
-
rows.forEach((
|
|
6659
|
-
map.set(
|
|
6660
|
-
id:
|
|
7304
|
+
rows.forEach((r2, i) => {
|
|
7305
|
+
map.set(r2.id, {
|
|
7306
|
+
id: r2.id,
|
|
6661
7307
|
source,
|
|
6662
|
-
title:
|
|
6663
|
-
snippet: snippetOf(
|
|
7308
|
+
title: r2.title,
|
|
7309
|
+
snippet: snippetOf(r2.snippet ?? r2.title),
|
|
6664
7310
|
importance: 3,
|
|
6665
|
-
ageDays: ageExpr(
|
|
7311
|
+
ageDays: ageExpr(r2.created),
|
|
6666
7312
|
bm25Order: i,
|
|
6667
7313
|
vecs: []
|
|
6668
7314
|
});
|
|
@@ -6675,15 +7321,15 @@ function collectLikeSource(db, source, cfg, queryText, pool, nowMs) {
|
|
|
6675
7321
|
${cfg.snippetCol} AS snippet, ${cfg.createdCol} AS created
|
|
6676
7322
|
FROM ${cfg.table}${cfg.temporalClause ? ` WHERE ${cfg.temporalClause}` : ""} ORDER BY ${cfg.createdCol} DESC LIMIT ?`
|
|
6677
7323
|
).all(pool);
|
|
6678
|
-
for (const
|
|
6679
|
-
if (!map.has(
|
|
6680
|
-
map.set(
|
|
6681
|
-
id:
|
|
7324
|
+
for (const r2 of recent) {
|
|
7325
|
+
if (!map.has(r2.id)) {
|
|
7326
|
+
map.set(r2.id, {
|
|
7327
|
+
id: r2.id,
|
|
6682
7328
|
source,
|
|
6683
|
-
title:
|
|
6684
|
-
snippet: snippetOf(
|
|
7329
|
+
title: r2.title,
|
|
7330
|
+
snippet: snippetOf(r2.snippet ?? r2.title),
|
|
6685
7331
|
importance: 3,
|
|
6686
|
-
ageDays: ageExpr(
|
|
7332
|
+
ageDays: ageExpr(r2.created),
|
|
6687
7333
|
bm25Order: null,
|
|
6688
7334
|
vecs: []
|
|
6689
7335
|
});
|
|
@@ -6703,14 +7349,14 @@ function collectKnowledgeChunks(knowledgeDb, queryText, pool, nowMs, loadVec, mo
|
|
|
6703
7349
|
WHERE knowledge_fts MATCH ?
|
|
6704
7350
|
ORDER BY rank LIMIT ?`
|
|
6705
7351
|
).all(sanitizeFts5QueryOr(queryText), pool);
|
|
6706
|
-
rows.forEach((
|
|
6707
|
-
map.set(
|
|
6708
|
-
id:
|
|
7352
|
+
rows.forEach((r2, i) => {
|
|
7353
|
+
map.set(r2.id, {
|
|
7354
|
+
id: r2.id,
|
|
6709
7355
|
source: "knowledge_chunk",
|
|
6710
|
-
title:
|
|
6711
|
-
snippet: snippetOf(
|
|
7356
|
+
title: r2.heading || snippetOf(r2.content, 60),
|
|
7357
|
+
snippet: snippetOf(r2.content),
|
|
6712
7358
|
importance: 3,
|
|
6713
|
-
ageDays: ageDaysFrom((
|
|
7359
|
+
ageDays: ageDaysFrom((r2.epoch ?? 0) * 1e3, nowMs),
|
|
6714
7360
|
bm25Order: i,
|
|
6715
7361
|
vecs: []
|
|
6716
7362
|
});
|
|
@@ -6742,9 +7388,9 @@ function loadEmbeddings(db, table, idCol, ids, modelId, dim, assign) {
|
|
|
6742
7388
|
WHERE ${idCol} IN (${placeholders}) AND model_id = ? AND dim = ?
|
|
6743
7389
|
LIMIT ?`
|
|
6744
7390
|
).all(...ids, modelId, dim, ids.length);
|
|
6745
|
-
for (const
|
|
6746
|
-
const v = blobToFloat32(
|
|
6747
|
-
if (v && v.length === dim) assign(
|
|
7391
|
+
for (const r2 of rows) {
|
|
7392
|
+
const v = blobToFloat32(r2.vec);
|
|
7393
|
+
if (v && v.length === dim) assign(r2.id, v);
|
|
6748
7394
|
}
|
|
6749
7395
|
} catch {
|
|
6750
7396
|
}
|
|
@@ -6854,17 +7500,17 @@ function hybridSearch(memDb, knowledgeDb, opts) {
|
|
|
6854
7500
|
ageDays: c.ageDays
|
|
6855
7501
|
};
|
|
6856
7502
|
});
|
|
6857
|
-
const top = ranked.filter((
|
|
7503
|
+
const top = ranked.filter((r2) => r2.score >= minScore).sort((a, b) => b.score - a.score || a.ageDays - b.ageDays).slice(0, limit);
|
|
6858
7504
|
if (includeSuperseded && opts.asOf == null) {
|
|
6859
|
-
for (const
|
|
6860
|
-
const table =
|
|
7505
|
+
for (const r2 of top) {
|
|
7506
|
+
const table = r2.source === "observation" ? "observations" : r2.source === "architecture_decision" ? "architecture_decisions" : null;
|
|
6861
7507
|
if (!table) continue;
|
|
6862
7508
|
try {
|
|
6863
|
-
const row = memDb.prepare(`SELECT valid_to, superseded_by, expired_at_epoch FROM ${table} WHERE id = ?`).get(
|
|
7509
|
+
const row = memDb.prepare(`SELECT valid_to, superseded_by, expired_at_epoch FROM ${table} WHERE id = ?`).get(r2.id);
|
|
6864
7510
|
if (row && row.expired_at_epoch != null) {
|
|
6865
7511
|
const when = row.valid_to ? row.valid_to.slice(0, 10) : "an earlier date";
|
|
6866
7512
|
const by = row.superseded_by != null ? ` by #${row.superseded_by}` : "";
|
|
6867
|
-
|
|
7513
|
+
r2.snippet = `(superseded on ${when}${by}) ${r2.snippet}`;
|
|
6868
7514
|
}
|
|
6869
7515
|
} catch {
|
|
6870
7516
|
}
|
|
@@ -6986,11 +7632,11 @@ async function supersedeIfContradicted(memDb, knowledgeDb, args) {
|
|
|
6986
7632
|
candidatePool: 20
|
|
6987
7633
|
});
|
|
6988
7634
|
const scored = [];
|
|
6989
|
-
for (const
|
|
6990
|
-
if (
|
|
7635
|
+
for (const r2 of results) {
|
|
7636
|
+
if (r2.id === args.newId) continue;
|
|
6991
7637
|
if (Date.now() - start > cfg.budgetMs) break;
|
|
6992
|
-
const vec = await candidateVec(memDb, args.source,
|
|
6993
|
-
if (vec) scored.push({ id:
|
|
7638
|
+
const vec = await candidateVec(memDb, args.source, r2.id);
|
|
7639
|
+
if (vec) scored.push({ id: r2.id, source: args.source, cosine: cosineSim(queryVec, vec) });
|
|
6994
7640
|
}
|
|
6995
7641
|
let verdict = null;
|
|
6996
7642
|
if (cfg.judgeEndpoint && Date.now() - start < cfg.budgetMs) {
|
|
@@ -7065,8 +7711,8 @@ ${d.decision}`.trim(),
|
|
|
7065
7711
|
// src/memory-consolidate.ts
|
|
7066
7712
|
init_memory_db();
|
|
7067
7713
|
import { createHash as createHash6 } from "crypto";
|
|
7068
|
-
import { existsSync as
|
|
7069
|
-
import { join as
|
|
7714
|
+
import { existsSync as existsSync18, mkdirSync as mkdirSync15, writeFileSync as writeFileSync10 } from "fs";
|
|
7715
|
+
import { join as join13 } from "path";
|
|
7070
7716
|
init_memory_embedder();
|
|
7071
7717
|
init_memory_vector();
|
|
7072
7718
|
var LEASE_KEY = "consolidate_lease";
|
|
@@ -7089,17 +7735,11 @@ function emptyResult() {
|
|
|
7089
7735
|
};
|
|
7090
7736
|
}
|
|
7091
7737
|
function acquireLease(db, now, ttlSec) {
|
|
7092
|
-
|
|
7093
|
-
if (raw) {
|
|
7094
|
-
const expiry = Number(raw.split(":")[1]);
|
|
7095
|
-
if (Number.isFinite(expiry) && expiry > now) return false;
|
|
7096
|
-
}
|
|
7097
|
-
setMemoryMeta(db, LEASE_KEY, `${process.pid}:${now + ttlSec}`);
|
|
7098
|
-
return true;
|
|
7738
|
+
return tryAcquireMetaLease(db, LEASE_KEY, ttlSec, now);
|
|
7099
7739
|
}
|
|
7100
7740
|
function releaseLease(db) {
|
|
7101
7741
|
try {
|
|
7102
|
-
|
|
7742
|
+
releaseMetaLease(db, LEASE_KEY);
|
|
7103
7743
|
} catch {
|
|
7104
7744
|
}
|
|
7105
7745
|
}
|
|
@@ -7118,19 +7758,19 @@ async function stageDedupe(db, cfg, deadline, now) {
|
|
|
7118
7758
|
return 0;
|
|
7119
7759
|
}
|
|
7120
7760
|
let deduped = 0;
|
|
7121
|
-
for (const
|
|
7761
|
+
for (const r2 of rows) {
|
|
7122
7762
|
if (Date.now() > deadline) break;
|
|
7123
|
-
const live = db.prepare(`SELECT expired_at FROM observations WHERE id = ?`).get(
|
|
7763
|
+
const live = db.prepare(`SELECT expired_at FROM observations WHERE id = ?`).get(r2.id);
|
|
7124
7764
|
if (!live || live.expired_at != null) continue;
|
|
7125
7765
|
const res = await supersedeIfContradicted(db, null, {
|
|
7126
|
-
text: `${
|
|
7127
|
-
${
|
|
7766
|
+
text: `${r2.title}
|
|
7767
|
+
${r2.detail ?? ""}`.trim(),
|
|
7128
7768
|
source: "observation",
|
|
7129
|
-
newId:
|
|
7769
|
+
newId: r2.id,
|
|
7130
7770
|
nowEpochSec: now
|
|
7131
7771
|
});
|
|
7132
7772
|
if (res.superseded != null) deduped++;
|
|
7133
|
-
setMemoryMeta(db, DEDUPE_CURSOR, String(
|
|
7773
|
+
setMemoryMeta(db, DEDUPE_CURSOR, String(r2.id));
|
|
7134
7774
|
}
|
|
7135
7775
|
return deduped;
|
|
7136
7776
|
}
|
|
@@ -7208,11 +7848,11 @@ async function stagePromote(db, cfg, projectRoot, deadline, now, dryRun) {
|
|
|
7208
7848
|
).all(MAX_ROWS_PER_STAGE);
|
|
7209
7849
|
if (rows.length === 0) return { promoted: 0, refusedByTier: 0, embedderUnavailable: false };
|
|
7210
7850
|
const vecs = /* @__PURE__ */ new Map();
|
|
7211
|
-
for (const
|
|
7851
|
+
for (const r2 of rows) {
|
|
7212
7852
|
if (Date.now() > deadline) break;
|
|
7213
|
-
const v = await embed(`${
|
|
7214
|
-
${
|
|
7215
|
-
if (v) vecs.set(
|
|
7853
|
+
const v = await embed(`${r2.title}
|
|
7854
|
+
${r2.detail ?? ""}`.trim());
|
|
7855
|
+
if (v) vecs.set(r2.id, v);
|
|
7216
7856
|
}
|
|
7217
7857
|
if (vecs.size === 0) {
|
|
7218
7858
|
return { promoted: 0, refusedByTier: 0, embedderUnavailable: true };
|
|
@@ -7220,20 +7860,20 @@ ${r.detail ?? ""}`.trim());
|
|
|
7220
7860
|
const CLUSTER_THRESHOLD = 0.8;
|
|
7221
7861
|
const used = /* @__PURE__ */ new Set();
|
|
7222
7862
|
const clusters = [];
|
|
7223
|
-
for (const
|
|
7224
|
-
if (used.has(
|
|
7225
|
-
const cluster = [
|
|
7226
|
-
used.add(
|
|
7863
|
+
for (const r2 of rows) {
|
|
7864
|
+
if (used.has(r2.id) || !vecs.has(r2.id)) continue;
|
|
7865
|
+
const cluster = [r2];
|
|
7866
|
+
used.add(r2.id);
|
|
7227
7867
|
for (const other of rows) {
|
|
7228
7868
|
if (used.has(other.id) || !vecs.has(other.id)) continue;
|
|
7229
|
-
if (cosineSim(vecs.get(
|
|
7869
|
+
if (cosineSim(vecs.get(r2.id), vecs.get(other.id)) >= CLUSTER_THRESHOLD) {
|
|
7230
7870
|
cluster.push(other);
|
|
7231
7871
|
used.add(other.id);
|
|
7232
7872
|
}
|
|
7233
7873
|
}
|
|
7234
7874
|
clusters.push(cluster);
|
|
7235
7875
|
}
|
|
7236
|
-
const candidateDir =
|
|
7876
|
+
const candidateDir = join13(projectRoot, ".massu", "rule-candidates");
|
|
7237
7877
|
let promoted = 0;
|
|
7238
7878
|
for (const cluster of clusters) {
|
|
7239
7879
|
const occurrences = cluster.reduce((n, c) => n + (c.recurrence_count || 1), 0);
|
|
@@ -7246,13 +7886,13 @@ ${r.detail ?? ""}`.trim());
|
|
|
7246
7886
|
${representative.detail}` : ""}`;
|
|
7247
7887
|
const clusterKey = cluster.map((c) => c.title.toLowerCase().replace(/\s+/g, " ").trim()).sort().join("|");
|
|
7248
7888
|
const promptHash = createHash6("sha256").update(clusterKey).digest("hex").slice(0, 16);
|
|
7249
|
-
const candidatePath =
|
|
7250
|
-
if (
|
|
7889
|
+
const candidatePath = join13(candidateDir, `${promptHash}.json`);
|
|
7890
|
+
if (existsSync18(candidatePath)) continue;
|
|
7251
7891
|
if (dryRun) {
|
|
7252
7892
|
promoted++;
|
|
7253
7893
|
continue;
|
|
7254
7894
|
}
|
|
7255
|
-
|
|
7895
|
+
mkdirSync15(candidateDir, { recursive: true });
|
|
7256
7896
|
writeFileSync10(
|
|
7257
7897
|
candidatePath,
|
|
7258
7898
|
JSON.stringify(
|
|
@@ -7325,14 +7965,14 @@ function stageReweight(db, cfg, now) {
|
|
|
7325
7965
|
ON CONFLICT(source, record_id) DO UPDATE SET last_reweight_epoch = excluded.last_reweight_epoch`
|
|
7326
7966
|
);
|
|
7327
7967
|
const tx = db.transaction(() => {
|
|
7328
|
-
for (const
|
|
7329
|
-
bump.run(
|
|
7330
|
-
mark.run(
|
|
7968
|
+
for (const r2 of promote) {
|
|
7969
|
+
bump.run(r2.id);
|
|
7970
|
+
mark.run(r2.id, now);
|
|
7331
7971
|
changed++;
|
|
7332
7972
|
}
|
|
7333
|
-
for (const
|
|
7334
|
-
drop2.run(
|
|
7335
|
-
mark.run(
|
|
7973
|
+
for (const r2 of demote) {
|
|
7974
|
+
drop2.run(r2.id);
|
|
7975
|
+
mark.run(r2.id, now);
|
|
7336
7976
|
changed++;
|
|
7337
7977
|
}
|
|
7338
7978
|
});
|
|
@@ -7423,105 +8063,184 @@ function isDirectInvocation(moduleUrl) {
|
|
|
7423
8063
|
}
|
|
7424
8064
|
|
|
7425
8065
|
// src/hooks/session-end.ts
|
|
8066
|
+
var SESSION_END_BUDGET_ENV = "MASSU_SESSION_END_BUDGET_MS";
|
|
8067
|
+
var DEFAULT_SESSION_END_BUDGET_MS = 2e4;
|
|
8068
|
+
var SESSION_END_WATCHDOG_GRACE_MS = 15e3;
|
|
8069
|
+
var MAX_COALESCED_RUNS = 3;
|
|
8070
|
+
var MIN_PHASE_BUDGET_MS = 500;
|
|
8071
|
+
function resolveSessionEndBudgetMs() {
|
|
8072
|
+
const raw = process.env[SESSION_END_BUDGET_ENV];
|
|
8073
|
+
if (raw === void 0 || raw === "") return DEFAULT_SESSION_END_BUDGET_MS;
|
|
8074
|
+
const n = Number(raw);
|
|
8075
|
+
return Number.isFinite(n) && n > 0 ? Math.floor(n) : DEFAULT_SESSION_END_BUDGET_MS;
|
|
8076
|
+
}
|
|
8077
|
+
var leaseKey = sessionLeaseKey;
|
|
8078
|
+
var rerunKey = (sessionId) => `session_end_rerun:${sessionId}`;
|
|
8079
|
+
async function runCoalesced(db, sessionId, budgetMs, work) {
|
|
8080
|
+
const ttlSec = Math.ceil((budgetMs + SESSION_END_WATCHDOG_GRACE_MS) / 1e3) + 1;
|
|
8081
|
+
let runs = 0;
|
|
8082
|
+
while (runs < MAX_COALESCED_RUNS) {
|
|
8083
|
+
if (!tryAcquireMetaLease(db, leaseKey(sessionId), ttlSec)) {
|
|
8084
|
+
setMemoryMeta(db, rerunKey(sessionId), "1");
|
|
8085
|
+
if (!tryAcquireMetaLease(db, leaseKey(sessionId), ttlSec)) {
|
|
8086
|
+
return runs === 0 ? "handed-off" : "ran";
|
|
8087
|
+
}
|
|
8088
|
+
}
|
|
8089
|
+
runs++;
|
|
8090
|
+
try {
|
|
8091
|
+
setMemoryMeta(db, rerunKey(sessionId), "");
|
|
8092
|
+
await work(Date.now() + budgetMs);
|
|
8093
|
+
} finally {
|
|
8094
|
+
releaseMetaLease(db, leaseKey(sessionId));
|
|
8095
|
+
}
|
|
8096
|
+
if (getMemoryMeta(db, rerunKey(sessionId)) !== "1") return "ran";
|
|
8097
|
+
}
|
|
8098
|
+
return "ran";
|
|
8099
|
+
}
|
|
8100
|
+
function phaseBudget(deadlineAt, phase, cap) {
|
|
8101
|
+
const left = deadlineAt - Date.now();
|
|
8102
|
+
if (left < MIN_PHASE_BUDGET_MS) {
|
|
8103
|
+
process.stderr.write(`[massu] session-end: skipped ${phase} \u2014 ${Math.max(0, left)}ms of budget left
|
|
8104
|
+
`);
|
|
8105
|
+
return null;
|
|
8106
|
+
}
|
|
8107
|
+
return Math.min(cap, left);
|
|
8108
|
+
}
|
|
7426
8109
|
async function main() {
|
|
7427
8110
|
try {
|
|
7428
8111
|
const input = await readStdin();
|
|
7429
8112
|
const hookInput = JSON.parse(input);
|
|
7430
|
-
const
|
|
8113
|
+
const budgetMs = resolveSessionEndBudgetMs();
|
|
8114
|
+
let watchdog;
|
|
7431
8115
|
const db = getMemoryDb();
|
|
7432
8116
|
try {
|
|
7433
|
-
|
|
7434
|
-
|
|
7435
|
-
|
|
7436
|
-
|
|
7437
|
-
|
|
7438
|
-
|
|
7439
|
-
|
|
7440
|
-
|
|
7441
|
-
|
|
7442
|
-
|
|
7443
|
-
await
|
|
7444
|
-
}
|
|
7445
|
-
|
|
7446
|
-
|
|
7447
|
-
|
|
7448
|
-
|
|
7449
|
-
|
|
7450
|
-
|
|
7451
|
-
|
|
7452
|
-
|
|
7453
|
-
|
|
7454
|
-
|
|
7455
|
-
|
|
7456
|
-
|
|
7457
|
-
|
|
7458
|
-
|
|
7459
|
-
|
|
7460
|
-
|
|
7461
|
-
|
|
7462
|
-
|
|
7463
|
-
|
|
7464
|
-
|
|
7465
|
-
|
|
7466
|
-
|
|
7467
|
-
|
|
7468
|
-
|
|
7469
|
-
|
|
7470
|
-
|
|
7471
|
-
}
|
|
8117
|
+
await runCoalesced(db, hookInput.session_id, budgetMs, async (deadlineAt) => {
|
|
8118
|
+
clearTimeout(watchdog);
|
|
8119
|
+
watchdog = setTimeout(() => {
|
|
8120
|
+
recordHookFailure(
|
|
8121
|
+
"session-end:watchdog",
|
|
8122
|
+
new Error(`a run exceeded its ${budgetMs}ms budget by ${SESSION_END_WATCHDOG_GRACE_MS}ms`)
|
|
8123
|
+
);
|
|
8124
|
+
process.exit(0);
|
|
8125
|
+
}, budgetMs + SESSION_END_WATCHDOG_GRACE_MS);
|
|
8126
|
+
watchdog.unref();
|
|
8127
|
+
await runSessionEnd(db, hookInput, deadlineAt);
|
|
8128
|
+
});
|
|
8129
|
+
} finally {
|
|
8130
|
+
clearTimeout(watchdog);
|
|
8131
|
+
db.close();
|
|
8132
|
+
}
|
|
8133
|
+
} catch (err) {
|
|
8134
|
+
recordHookFailure("session-end", err);
|
|
8135
|
+
}
|
|
8136
|
+
process.exit(0);
|
|
8137
|
+
}
|
|
8138
|
+
async function runSessionEnd(db, hookInput, deadlineAt) {
|
|
8139
|
+
const { session_id, cwd } = hookInput;
|
|
8140
|
+
{
|
|
8141
|
+
createSession(db, session_id);
|
|
8142
|
+
const observations = db.prepare(
|
|
8143
|
+
"SELECT * FROM observations WHERE session_id = ? ORDER BY created_at_epoch ASC LIMIT 10000"
|
|
8144
|
+
).all(session_id);
|
|
8145
|
+
const prompts = db.prepare(
|
|
8146
|
+
"SELECT prompt_text FROM user_prompts WHERE session_id = ? ORDER BY prompt_number ASC LIMIT 10000"
|
|
8147
|
+
).all(session_id);
|
|
8148
|
+
const summary = buildSummaryFromObservations(observations, prompts);
|
|
8149
|
+
addSummary(db, session_id, summary);
|
|
8150
|
+
try {
|
|
8151
|
+
await captureConversationData(db, session_id, hookInput.transcript_path);
|
|
8152
|
+
} catch (_captureErr) {
|
|
8153
|
+
}
|
|
8154
|
+
try {
|
|
8155
|
+
const { score, breakdown } = calculateQualityScore(db, session_id);
|
|
8156
|
+
if (score !== 50) {
|
|
8157
|
+
storeQualityScore(db, session_id, score, breakdown);
|
|
7472
8158
|
}
|
|
7473
|
-
|
|
7474
|
-
|
|
7475
|
-
|
|
8159
|
+
backfillQualityScores(db);
|
|
8160
|
+
} catch (_qualityErr) {
|
|
8161
|
+
}
|
|
8162
|
+
try {
|
|
8163
|
+
await updateSessionCost(db, session_id, hookInput.transcript_path);
|
|
8164
|
+
} catch (costErr) {
|
|
8165
|
+
recordHookFailure("session-end:cost", costErr);
|
|
8166
|
+
}
|
|
8167
|
+
try {
|
|
8168
|
+
analyzeSessionPrompts(db, session_id);
|
|
8169
|
+
} catch (_promptErr) {
|
|
8170
|
+
}
|
|
8171
|
+
try {
|
|
8172
|
+
const budget = phaseBudget(deadlineAt, "embed sweep", 3e3);
|
|
8173
|
+
if (budget !== null) await embedMissingObservations(db, { budgetMs: budget });
|
|
8174
|
+
} catch (_embedErr) {
|
|
8175
|
+
}
|
|
8176
|
+
try {
|
|
8177
|
+
const budget = phaseBudget(deadlineAt, "supersede sweep", 4e3);
|
|
8178
|
+
if (budget !== null) await runSessionSupersedeSweep(db, session_id, { budgetMs: budget });
|
|
8179
|
+
} catch (_supersedeErr) {
|
|
8180
|
+
}
|
|
8181
|
+
try {
|
|
8182
|
+
const consolidationCfg = resolveConsolidationConfig();
|
|
8183
|
+
if (consolidationCfg.enabled && consolidationCfg.sessionSweepEnabled) {
|
|
8184
|
+
const budget = phaseBudget(deadlineAt, "consolidation", consolidationCfg.budgetMs);
|
|
8185
|
+
if (budget !== null) {
|
|
7476
8186
|
await runConsolidation(db, {
|
|
7477
8187
|
config: consolidationCfg,
|
|
7478
|
-
budgetMs:
|
|
8188
|
+
budgetMs: budget,
|
|
7479
8189
|
projectRoot: cwd
|
|
7480
8190
|
});
|
|
7481
8191
|
}
|
|
7482
|
-
} catch (_consolidateErr) {
|
|
7483
8192
|
}
|
|
7484
|
-
|
|
7485
|
-
|
|
7486
|
-
|
|
7487
|
-
|
|
7488
|
-
|
|
7489
|
-
|
|
7490
|
-
|
|
7491
|
-
|
|
7492
|
-
|
|
7493
|
-
|
|
7494
|
-
|
|
7495
|
-
|
|
7496
|
-
|
|
7497
|
-
|
|
7498
|
-
|
|
7499
|
-
|
|
7500
|
-
|
|
8193
|
+
} catch (_consolidateErr) {
|
|
8194
|
+
}
|
|
8195
|
+
try {
|
|
8196
|
+
const budget = phaseBudget(deadlineAt, "cost reprice", 3e3);
|
|
8197
|
+
if (budget !== null) await repriceStoredSessions(db, { deadlineAt: Date.now() + budget });
|
|
8198
|
+
} catch (repriceErr) {
|
|
8199
|
+
recordHookFailure("session-end:cost-reprice", repriceErr);
|
|
8200
|
+
}
|
|
8201
|
+
endSession(db, session_id, "completed");
|
|
8202
|
+
archiveAndRegenerate(db, session_id);
|
|
8203
|
+
let openLease = null;
|
|
8204
|
+
try {
|
|
8205
|
+
const drained = await drainSyncQueue(db, deadlineAt);
|
|
8206
|
+
if (drained.deferred > 0) {
|
|
8207
|
+
process.stderr.write(`[massu] session-end: deferred ${drained.deferred} queued sync item(s) to the next run
|
|
8208
|
+
`);
|
|
8209
|
+
}
|
|
8210
|
+
const cloudCfg = getConfig().cloud;
|
|
8211
|
+
const willTransmit = !!cloudCfg?.enabled && !!cloudCfg?.apiKey && !!cloudCfg?.endpoint && cloudCfg?.sync?.memory !== false;
|
|
8212
|
+
const lease = willTransmit ? leaseLearning(db) : null;
|
|
8213
|
+
openLease = lease;
|
|
8214
|
+
const syncPayload = buildSyncPayload(db, session_id, observations, summary, lease);
|
|
8215
|
+
const result = await syncToCloud(db, syncPayload, { deadlineAt });
|
|
8216
|
+
if (lease) {
|
|
8217
|
+
if (result.transmitted === true) {
|
|
8218
|
+
ackLearning(db, lease);
|
|
8219
|
+
} else {
|
|
8220
|
+
nackLearning(db, lease, result.error ?? "not transmitted (sync disabled or unreachable)");
|
|
7501
8221
|
}
|
|
7502
|
-
|
|
7503
|
-
|
|
8222
|
+
}
|
|
8223
|
+
openLease = null;
|
|
8224
|
+
try {
|
|
8225
|
+
if (phaseBudget(deadlineAt, "team promotion pull", Infinity) !== null) {
|
|
7504
8226
|
await pullTeamPromotions(db);
|
|
7505
|
-
} catch (_pullErr) {
|
|
7506
8227
|
}
|
|
7507
|
-
|
|
8228
|
+
} catch (_pullErr) {
|
|
8229
|
+
}
|
|
8230
|
+
try {
|
|
8231
|
+
if (phaseBudget(deadlineAt, "shared-memory exchange", Infinity) !== null) {
|
|
7508
8232
|
const shareTransport = new LocalFsTransport();
|
|
7509
8233
|
await exportSharedMemories(db, shareTransport);
|
|
7510
8234
|
await importSharedMemories(db, shareTransport);
|
|
7511
|
-
} catch (_shareErr) {
|
|
7512
8235
|
}
|
|
7513
|
-
} catch (
|
|
7514
|
-
const msg = syncErr instanceof Error ? syncErr.message : String(syncErr);
|
|
7515
|
-
if (openLease) nackLearning(db, openLease, msg);
|
|
7516
|
-
recordHookFailure("session-end:cloud-sync", syncErr);
|
|
8236
|
+
} catch (_shareErr) {
|
|
7517
8237
|
}
|
|
7518
|
-
}
|
|
7519
|
-
|
|
8238
|
+
} catch (syncErr) {
|
|
8239
|
+
const msg = syncErr instanceof Error ? syncErr.message : String(syncErr);
|
|
8240
|
+
if (openLease) nackLearning(db, openLease, msg);
|
|
8241
|
+
recordHookFailure("session-end:cloud-sync", syncErr);
|
|
7520
8242
|
}
|
|
7521
|
-
} catch (err) {
|
|
7522
|
-
recordHookFailure("session-end", err);
|
|
7523
8243
|
}
|
|
7524
|
-
process.exit(0);
|
|
7525
8244
|
}
|
|
7526
8245
|
function buildSyncPayload(db, sessionId, observations, summary, lease) {
|
|
7527
8246
|
const promotions = lease?.promotions ?? [];
|
|
@@ -7772,16 +8491,24 @@ function extractFilesFromToolCall(toolName, input) {
|
|
|
7772
8491
|
return [];
|
|
7773
8492
|
}
|
|
7774
8493
|
function readStdin() {
|
|
7775
|
-
return new Promise((
|
|
8494
|
+
return new Promise((resolve9) => {
|
|
7776
8495
|
let data = "";
|
|
7777
8496
|
process.stdin.setEncoding("utf-8");
|
|
7778
8497
|
process.stdin.on("data", (chunk) => {
|
|
7779
8498
|
data += chunk;
|
|
7780
8499
|
});
|
|
7781
|
-
process.stdin.on("end", () =>
|
|
7782
|
-
setTimeout(() =>
|
|
8500
|
+
process.stdin.on("end", () => resolve9(data));
|
|
8501
|
+
setTimeout(() => resolve9(data), 5e3);
|
|
7783
8502
|
});
|
|
7784
8503
|
}
|
|
7785
8504
|
if (isDirectInvocation(import.meta.url)) {
|
|
7786
8505
|
main();
|
|
7787
8506
|
}
|
|
8507
|
+
export {
|
|
8508
|
+
DEFAULT_SESSION_END_BUDGET_MS,
|
|
8509
|
+
MAX_COALESCED_RUNS,
|
|
8510
|
+
SESSION_END_BUDGET_ENV,
|
|
8511
|
+
SESSION_END_WATCHDOG_GRACE_MS,
|
|
8512
|
+
resolveSessionEndBudgetMs,
|
|
8513
|
+
runCoalesced
|
|
8514
|
+
};
|