@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.
@@ -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 r = spawnSync(process.execPath, [prebuildBin], {
295
+ const r2 = spawnSync(process.execPath, [prebuildBin], {
296
296
  cwd: pkgDir,
297
297
  encoding: "utf-8",
298
298
  timeout: 12e4
299
299
  });
300
- if (r.status === 0) {
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 r = spawnSync(process.execPath, [gypBin, "rebuild", "--release"], {
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 (r.status === 0) {
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 r = spawnSync(cc, ["--version"], { encoding: "utf-8", timeout: 5e3 });
393
- if (r.status === 0) return true;
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 r = st.run(...p);
467
+ const r2 = st.run(...p);
468
468
  return {
469
- changes: typeof r.changes === "bigint" ? Number(r.changes) : r.changes,
470
- lastInsertRowid: r.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((r) => r.k));
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((r) => r.name));
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((r) => ({
1594
- prompt_hash: r.prompt_hash,
1595
- destination: r.destination,
1596
- draft_text: r.draft_text,
1597
- ...r.score !== null ? { score: r.score } : {},
1598
- signals: safeParseArray(r.signals_json),
1599
- content_hash: r.content_hash,
1600
- ...r.hardened ? { hardened: true } : {},
1601
- ...r.review_attestation_json != null ? { review_attestation: safeParseUnknown(r.review_attestation_json) } : {}
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((r) => r.prompt_hash) : [];
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((r) => ({
1612
- prompt_hash: r.prompt_hash,
1613
- event_type: r.event_type,
1614
- created_at: r.created_at,
1615
- metadata: safeParseUnknown(r.metadata_json) ?? {}
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 = (t) => {
1651
- if (!tableExists(db, t)) return 0;
1652
- const r = db.prepare(`SELECT COUNT(*) AS n FROM ${t}`).get();
1653
- return r.n;
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 t of RULE_OUTBOUND_TABLES) {
1661
- if (!tableExists(db, t)) continue;
1662
- if (!columns(db, t).has("attempts")) continue;
1663
- const r = db.prepare(`SELECT MAX(attempts) AS a, MIN(created_at) AS c FROM ${t}`).get();
1664
- if (r.a !== null && r.a > maxAttempts) maxAttempts = r.a;
1665
- if (r.c !== null && (oldest === null || r.c < oldest)) oldest = r.c;
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 t of basic) {
1961
- for (const sub of wordpieceTokenize(t, vocab)) wpTokens.push(sub);
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((t) => vocab.get(t) ?? unkId);
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 r = getConfig().memory?.recall;
1992
+ const r2 = getConfig().memory?.recall;
1992
1993
  return {
1993
- enabled: r?.embedEnabled ?? true,
1994
- endpoint: r?.embedEndpoint,
1995
- model: r?.embedModel
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 t = 0; t < seqLen; t++) {
2069
- const m = mask[t];
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 = t * hiddenSize;
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 t = text.trim();
2081
- if (!t) return [];
2082
- if (t.length <= CHUNK_TARGET_CHARS) return [t];
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 < t.length && chunks.length < MAX_CHUNKS_PER_RECORD) {
2086
- let end = Math.min(start + CHUNK_TARGET_CHARS, t.length);
2087
- if (end < t.length) {
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 = t.slice(start, end);
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 = t.slice(start, end).trim();
2102
+ const chunk = t2.slice(start, end).trim();
2102
2103
  if (chunk) chunks.push(chunk);
2103
- if (end >= t.length) break;
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 t of texts) {
2189
- if (!t || !t.trim()) {
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(t, settings);
2197
+ vec = await embedTier0(t2, settings);
2197
2198
  }
2198
- if (!vec) vec = await embedTier1(t);
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 cursorKey = `embed_sweep_cursor_${cfg.sourceLabel}`;
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, cursorKey);
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, cursorKey, 0);
2266
+ writeCursor(db, cfg.metaTable, cursorKey2, 0);
2266
2267
  break;
2267
2268
  }
2268
2269
  const units = [];
2269
- for (const r of rows) {
2270
+ for (const r2 of rows) {
2270
2271
  if (cfg.chunked) {
2271
- const parts = chunkForEmbedding(r.text);
2272
+ const parts = chunkForEmbedding(r2.text);
2272
2273
  if (parts.length === 0) continue;
2273
- parts.forEach((text, chunkIx) => units.push({ id: r.id, chunkIx, text }));
2274
+ parts.forEach((text, chunkIx) => units.push({ id: r2.id, chunkIx, text }));
2274
2275
  } else {
2275
- units.push({ id: r.id, chunkIx: 0, text: r.text });
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, cursorKey, cursor);
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((t) => `"${t}"`).join(" ");
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((t) => t.replace(/[^a-zA-Z0-9]/g, "").length >= 3);
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((t) => `"${t}"`).join(" OR ");
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 t = (title ?? "").trim();
3367
+ const t2 = (title ?? "").trim();
3344
3368
  const d = (detail ?? "").trim();
3345
- return d ? `${t}. ${d}` : t;
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((r) => ({ id: r.id, text: observationEmbedText(r.title, r.detail) }));
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 r of revocations) {
3466
- if (typeof r?.prompt_hash === "string") {
3467
- enqueueTeamRevocation(db, r.prompt_hash);
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 = 1;
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
- return { entries, totalLines: lineNumber };
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
- async function syncToCloud(db, payload) {
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 || `sync deadline exceeded (${SYNC_DEADLINE_MS}ms)`;
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
- try {
4677
- enqueueSyncPayload(db, JSON.stringify(stripLearningFromPayload(payload)));
4678
- } catch (err) {
4679
- process.stderr.write(
4680
- `[massu] WARNING: could not queue sync payload for retry: ${err instanceof Error ? err.message : String(err)}
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 (const item of pending) {
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((resolve8) => setTimeout(resolve8, ms));
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 r = p;
5332
- return typeof r.prompt_hash === "string" && PROMPT_HASH_RE.test(r.prompt_hash) && typeof r.destination === "string" && typeof r.draft_text === "string" && typeof r.promoted_by === "string" && typeof r.promoted_at === "string";
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 r = parsed;
5519
- if (!Array.isArray(r.repos)) return emptyRegistry();
5520
- const repos = r.repos.filter(
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 r.version === "number" ? r.version : REPOS_REGISTRY_VERSION, repos };
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 cursorKey = SHARED_CURSOR_PREFIX + originRepoId;
5710
- const since = parseCursor2(getMemoryMeta(db, cursorKey));
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, cursorKey, String(maxSeq));
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(r) {
5806
- if (!r || typeof r !== "object") return false;
5807
- const o = r;
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(r) {
5854
+ function stripHash(r2) {
5811
5855
  return {
5812
- type: r.type,
5813
- title: r.title,
5814
- detail: r.detail,
5815
- importance: r.importance,
5816
- created_at_epoch: r.created_at_epoch,
5817
- superseded_by_hash: r.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 t = text.trim();
5913
- if (t.length < 25) return false;
5914
- return !NOISE_PATTERNS.some((re) => re.test(t));
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 r of rows) {
6103
- if (!isLocalOrigin(r.origin)) {
6104
- refusals.push({ observation_id: r.id, reason: "non_local_origin" });
6105
- auditExportRefused(db, r.id, "non_local_origin");
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 = r.title ?? "";
6109
- const detail = r.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: r.id, reason });
6115
- auditExportRefused(db, r.id, reason);
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: r.id, reason: "home_path" });
6120
- auditExportRefused(db, r.id, "home_path");
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: r.type,
6168
+ type: r2.type,
6125
6169
  title,
6126
6170
  detail,
6127
- importance: r.importance,
6128
- created_at_epoch: r.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: r.id, reason: "record_too_large" });
6136
- auditExportRefused(db, r.id, "record_too_large");
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: r.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((r) => r.h);
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
- var DEFAULT_MODEL_PRICING = {
6360
- "claude-opus-4-6": { input_per_million: 15, output_per_million: 75, cache_read_per_million: 1.5, cache_write_per_million: 18.75 },
6361
- "claude-sonnet-4-6": { input_per_million: 3, output_per_million: 15, cache_read_per_million: 0.3, cache_write_per_million: 3.75 },
6362
- "claude-sonnet-4-5": { input_per_million: 3, output_per_million: 15, cache_read_per_million: 0.3, cache_write_per_million: 3.75 },
6363
- "claude-haiku-4-5-20251001": { input_per_million: 0.8, output_per_million: 4, cache_read_per_million: 0.08, cache_write_per_million: 1 },
6364
- "default": { input_per_million: 3, output_per_million: 15, cache_read_per_million: 0.3, cache_write_per_million: 3.75 }
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 getModelPricing() {
6367
- return getConfig().analytics?.cost?.models ?? DEFAULT_MODEL_PRICING;
6368
- }
6369
- function getCurrency() {
6370
- return getConfig().analytics?.cost?.currency ?? "USD";
6371
- }
6372
- function extractTokenUsage(entries) {
6373
- let inputTokens = 0;
6374
- let outputTokens = 0;
6375
- let cacheReadTokens = 0;
6376
- let cacheWriteTokens = 0;
6377
- let model = "unknown";
6378
- for (const entry of entries) {
6379
- const msg = entry.message;
6380
- if (entry.type === "assistant" && msg?.usage) {
6381
- const usage = msg.usage;
6382
- inputTokens += usage.input_tokens ?? 0;
6383
- outputTokens += usage.output_tokens ?? 0;
6384
- cacheReadTokens += usage.cache_read_input_tokens ?? usage.cache_read_tokens ?? 0;
6385
- cacheWriteTokens += usage.cache_creation_input_tokens ?? usage.cache_write_tokens ?? 0;
6386
- }
6387
- if (entry.type === "assistant" && msg?.model) {
6388
- model = msg.model;
6389
- }
6390
- }
6391
- return { inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens, model };
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
- totalCost: inputCost + outputCost + cacheReadCost + cacheWriteCost,
6402
- inputCost,
6403
- outputCost,
6404
- cacheReadCost,
6405
- cacheWriteCost,
6406
- currency: getCurrency()
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 storeSessionCost(db, sessionId, usage, cost) {
6410
- const totalTokens = usage.inputTokens + usage.outputTokens + usage.cacheReadTokens + usage.cacheWriteTokens;
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
- (session_id, model, input_tokens, output_tokens, cache_read_tokens, cache_write_tokens,
6414
- total_tokens, estimated_cost_usd)
6415
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)
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
- usage.model,
6419
- usage.inputTokens,
6420
- usage.outputTokens,
6421
- usage.cacheReadTokens,
6422
- usage.cacheWriteTokens,
6423
- totalTokens,
6424
- cost.totalCost
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 t = Math.floor(Number(asOf) / 1e3);
6546
- return `${c("ingested_at_epoch")} <= ${t} AND (${c("expired_at_epoch")} IS NULL OR ${c("expired_at_epoch")} > ${t}) AND ${c("valid_from_epoch")} <= ${t} AND (${c("valid_to_epoch")} IS NULL OR ${c("valid_to_epoch")} > ${t})`;
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((r, i) => {
6593
- map.set(r.id, {
6594
- id: r.id,
7238
+ rows.forEach((r2, i) => {
7239
+ map.set(r2.id, {
7240
+ id: r2.id,
6595
7241
  source: "observation",
6596
- title: r.title,
6597
- snippet: snippetOf(r.detail ?? r.title),
6598
- importance: r.importance,
6599
- ageDays: ageDaysFrom(r.created_at_epoch * 1e3, nowMs),
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 r of recent) {
6611
- if (!map.has(r.id)) {
6612
- map.set(r.id, {
6613
- id: r.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: r.title,
6616
- snippet: snippetOf(r.detail ?? r.title),
6617
- importance: r.importance,
6618
- ageDays: ageDaysFrom(r.created_at_epoch * 1e3, nowMs),
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((t) => t.length >= 3).slice(0, 8);
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 t of tokens) params.push(`%${t}%`);
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((r, i) => {
6659
- map.set(r.id, {
6660
- id: r.id,
7304
+ rows.forEach((r2, i) => {
7305
+ map.set(r2.id, {
7306
+ id: r2.id,
6661
7307
  source,
6662
- title: r.title,
6663
- snippet: snippetOf(r.snippet ?? r.title),
7308
+ title: r2.title,
7309
+ snippet: snippetOf(r2.snippet ?? r2.title),
6664
7310
  importance: 3,
6665
- ageDays: ageExpr(r.created),
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 r of recent) {
6679
- if (!map.has(r.id)) {
6680
- map.set(r.id, {
6681
- id: r.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: r.title,
6684
- snippet: snippetOf(r.snippet ?? r.title),
7329
+ title: r2.title,
7330
+ snippet: snippetOf(r2.snippet ?? r2.title),
6685
7331
  importance: 3,
6686
- ageDays: ageExpr(r.created),
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((r, i) => {
6707
- map.set(r.id, {
6708
- id: r.id,
7352
+ rows.forEach((r2, i) => {
7353
+ map.set(r2.id, {
7354
+ id: r2.id,
6709
7355
  source: "knowledge_chunk",
6710
- title: r.heading || snippetOf(r.content, 60),
6711
- snippet: snippetOf(r.content),
7356
+ title: r2.heading || snippetOf(r2.content, 60),
7357
+ snippet: snippetOf(r2.content),
6712
7358
  importance: 3,
6713
- ageDays: ageDaysFrom((r.epoch ?? 0) * 1e3, nowMs),
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 r of rows) {
6746
- const v = blobToFloat32(r.vec);
6747
- if (v && v.length === dim) assign(r.id, v);
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((r) => r.score >= minScore).sort((a, b) => b.score - a.score || a.ageDays - b.ageDays).slice(0, limit);
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 r of top) {
6860
- const table = r.source === "observation" ? "observations" : r.source === "architecture_decision" ? "architecture_decisions" : null;
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(r.id);
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
- r.snippet = `(superseded on ${when}${by}) ${r.snippet}`;
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 r of results) {
6990
- if (r.id === args.newId) continue;
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, r.id);
6993
- if (vec) scored.push({ id: r.id, source: args.source, cosine: cosineSim(queryVec, vec) });
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 existsSync16, mkdirSync as mkdirSync14, writeFileSync as writeFileSync10 } from "fs";
7069
- import { join as join11 } from "path";
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
- const raw = getMemoryMeta(db, LEASE_KEY);
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
- setMemoryMeta(db, LEASE_KEY, "");
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 r of rows) {
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(r.id);
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: `${r.title}
7127
- ${r.detail ?? ""}`.trim(),
7766
+ text: `${r2.title}
7767
+ ${r2.detail ?? ""}`.trim(),
7128
7768
  source: "observation",
7129
- newId: r.id,
7769
+ newId: r2.id,
7130
7770
  nowEpochSec: now
7131
7771
  });
7132
7772
  if (res.superseded != null) deduped++;
7133
- setMemoryMeta(db, DEDUPE_CURSOR, String(r.id));
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 r of rows) {
7851
+ for (const r2 of rows) {
7212
7852
  if (Date.now() > deadline) break;
7213
- const v = await embed(`${r.title}
7214
- ${r.detail ?? ""}`.trim());
7215
- if (v) vecs.set(r.id, v);
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 r of rows) {
7224
- if (used.has(r.id) || !vecs.has(r.id)) continue;
7225
- const cluster = [r];
7226
- used.add(r.id);
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(r.id), vecs.get(other.id)) >= CLUSTER_THRESHOLD) {
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 = join11(projectRoot, ".massu", "rule-candidates");
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 = join11(candidateDir, `${promptHash}.json`);
7250
- if (existsSync16(candidatePath)) continue;
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
- mkdirSync14(candidateDir, { recursive: true });
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 r of promote) {
7329
- bump.run(r.id);
7330
- mark.run(r.id, now);
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 r of demote) {
7334
- drop2.run(r.id);
7335
- mark.run(r.id, now);
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 { session_id, cwd } = hookInput;
8113
+ const budgetMs = resolveSessionEndBudgetMs();
8114
+ let watchdog;
7431
8115
  const db = getMemoryDb();
7432
8116
  try {
7433
- createSession(db, session_id);
7434
- const observations = db.prepare(
7435
- "SELECT * FROM observations WHERE session_id = ? ORDER BY created_at_epoch ASC LIMIT 10000"
7436
- ).all(session_id);
7437
- const prompts = db.prepare(
7438
- "SELECT prompt_text FROM user_prompts WHERE session_id = ? ORDER BY prompt_number ASC LIMIT 10000"
7439
- ).all(session_id);
7440
- const summary = buildSummaryFromObservations(observations, prompts);
7441
- addSummary(db, session_id, summary);
7442
- try {
7443
- await captureConversationData(db, session_id, hookInput.transcript_path);
7444
- } catch (_captureErr) {
7445
- }
7446
- try {
7447
- const { score, breakdown } = calculateQualityScore(db, session_id);
7448
- if (score !== 50) {
7449
- storeQualityScore(db, session_id, score, breakdown);
7450
- }
7451
- backfillQualityScores(db);
7452
- } catch (_qualityErr) {
7453
- }
7454
- try {
7455
- const { entries } = await parseTranscriptFrom(hookInput.transcript_path, 0);
7456
- const tokenUsage = extractTokenUsage(entries);
7457
- const cost = calculateCost(tokenUsage);
7458
- storeSessionCost(db, session_id, tokenUsage, cost);
7459
- } catch (_costErr) {
7460
- }
7461
- try {
7462
- analyzeSessionPrompts(db, session_id);
7463
- } catch (_promptErr) {
7464
- }
7465
- try {
7466
- await embedMissingObservations(db, { budgetMs: 3e3 });
7467
- } catch (_embedErr) {
7468
- }
7469
- try {
7470
- await runSessionSupersedeSweep(db, session_id, { budgetMs: 4e3 });
7471
- } catch (_supersedeErr) {
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
- try {
7474
- const consolidationCfg = resolveConsolidationConfig();
7475
- if (consolidationCfg.enabled && consolidationCfg.sessionSweepEnabled) {
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: consolidationCfg.budgetMs,
8188
+ budgetMs: budget,
7479
8189
  projectRoot: cwd
7480
8190
  });
7481
8191
  }
7482
- } catch (_consolidateErr) {
7483
8192
  }
7484
- endSession(db, session_id, "completed");
7485
- archiveAndRegenerate(db, session_id);
7486
- let openLease = null;
7487
- try {
7488
- await drainSyncQueue(db);
7489
- const cloudCfg = getConfig().cloud;
7490
- const willTransmit = !!cloudCfg?.enabled && !!cloudCfg?.apiKey && !!cloudCfg?.endpoint && cloudCfg?.sync?.memory !== false;
7491
- const lease = willTransmit ? leaseLearning(db) : null;
7492
- openLease = lease;
7493
- const syncPayload = buildSyncPayload(db, session_id, observations, summary, lease);
7494
- const result = await syncToCloud(db, syncPayload);
7495
- if (lease) {
7496
- if (result.transmitted === true) {
7497
- ackLearning(db, lease);
7498
- } else {
7499
- nackLearning(db, lease, result.error ?? "not transmitted (sync disabled or unreachable)");
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
- openLease = null;
7503
- try {
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
- try {
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 (syncErr) {
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
- } finally {
7519
- db.close();
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((resolve8) => {
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", () => resolve8(data));
7782
- setTimeout(() => resolve8(data), 5e3);
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
+ };