@massu/core 2.5.5 → 2.5.7

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.
@@ -1648,9 +1648,9 @@ function nackLearning(db, lease, error) {
1648
1648
  return snap;
1649
1649
  }
1650
1650
  function undeliveredSnapshot(db) {
1651
- const count = (t) => {
1652
- if (!tableExists(db, t)) return 0;
1653
- const r2 = db.prepare(`SELECT COUNT(*) AS n FROM ${t}`).get();
1651
+ const count = (t2) => {
1652
+ if (!tableExists(db, t2)) return 0;
1653
+ const r2 = db.prepare(`SELECT COUNT(*) AS n FROM ${t2}`).get();
1654
1654
  return r2.n;
1655
1655
  };
1656
1656
  const promotions = count("team_promotion_outbound");
@@ -1658,10 +1658,10 @@ function undeliveredSnapshot(db) {
1658
1658
  const events = count(TELEMETRY_OUTBOUND_TABLE);
1659
1659
  let maxAttempts = 0;
1660
1660
  let oldest = null;
1661
- for (const t of RULE_OUTBOUND_TABLES) {
1662
- if (!tableExists(db, t)) continue;
1663
- if (!columns(db, t).has("attempts")) continue;
1664
- const r2 = db.prepare(`SELECT MAX(attempts) AS a, MIN(created_at) AS c FROM ${t}`).get();
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
1665
  if (r2.a !== null && r2.a > maxAttempts) maxAttempts = r2.a;
1666
1666
  if (r2.c !== null && (oldest === null || r2.c < oldest)) oldest = r2.c;
1667
1667
  }
@@ -1958,13 +1958,13 @@ function encode(text, vocab, opts = {}) {
1958
1958
  const maxLen = opts.maxLen ?? 256;
1959
1959
  const basic = basicTokenize(text);
1960
1960
  const wpTokens = [];
1961
- for (const t of basic) {
1962
- 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);
1963
1963
  }
1964
1964
  const truncated = wpTokens.slice(0, maxLen - 2);
1965
1965
  const tokens = [CLS, ...truncated, SEP];
1966
1966
  const unkId = vocab.get(UNK) ?? 100;
1967
- const input_ids = tokens.map((t) => vocab.get(t) ?? unkId);
1967
+ const input_ids = tokens.map((t2) => vocab.get(t2) ?? unkId);
1968
1968
  const attention_mask = tokens.map(() => 1);
1969
1969
  const token_type_ids = tokens.map(() => 0);
1970
1970
  return { tokens, input_ids, attention_mask, token_type_ids };
@@ -2066,11 +2066,11 @@ async function getSession() {
2066
2066
  function meanPool(hidden, mask, seqLen, hiddenSize) {
2067
2067
  const out = new Float32Array(hiddenSize);
2068
2068
  let maskSum = 0;
2069
- for (let t = 0; t < seqLen; t++) {
2070
- const m = mask[t];
2069
+ for (let t2 = 0; t2 < seqLen; t2++) {
2070
+ const m = mask[t2];
2071
2071
  if (m === 0) continue;
2072
2072
  maskSum += m;
2073
- const base = t * hiddenSize;
2073
+ const base = t2 * hiddenSize;
2074
2074
  for (let h = 0; h < hiddenSize; h++) out[h] += hidden[base + h] * m;
2075
2075
  }
2076
2076
  const denom = Math.max(maskSum, 1e-9);
@@ -2078,16 +2078,16 @@ function meanPool(hidden, mask, seqLen, hiddenSize) {
2078
2078
  return out;
2079
2079
  }
2080
2080
  function chunkForEmbedding(text) {
2081
- const t = text.trim();
2082
- if (!t) return [];
2083
- 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];
2084
2084
  const chunks = [];
2085
2085
  let start = 0;
2086
- while (start < t.length && chunks.length < MAX_CHUNKS_PER_RECORD) {
2087
- let end = Math.min(start + CHUNK_TARGET_CHARS, t.length);
2088
- 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) {
2089
2089
  const floor = start + Math.floor(CHUNK_TARGET_CHARS / 2);
2090
- const slice = t.slice(start, end);
2090
+ const slice = t2.slice(start, end);
2091
2091
  const para = slice.lastIndexOf("\n\n");
2092
2092
  const sent = Math.max(slice.lastIndexOf(". "), slice.lastIndexOf(".\n"));
2093
2093
  const space = slice.lastIndexOf(" ");
@@ -2099,9 +2099,9 @@ function chunkForEmbedding(text) {
2099
2099
  }
2100
2100
  }
2101
2101
  }
2102
- const chunk = t.slice(start, end).trim();
2102
+ const chunk = t2.slice(start, end).trim();
2103
2103
  if (chunk) chunks.push(chunk);
2104
- if (end >= t.length) break;
2104
+ if (end >= t2.length) break;
2105
2105
  start = Math.max(end - CHUNK_OVERLAP_CHARS, start + 1);
2106
2106
  }
2107
2107
  return chunks;
@@ -2186,17 +2186,17 @@ async function embedBatch(texts) {
2186
2186
  const settings = loadEmbedSettings();
2187
2187
  if (!settings.enabled) return texts.map(() => null);
2188
2188
  const results = [];
2189
- for (const t of texts) {
2190
- if (!t || !t.trim()) {
2189
+ for (const t2 of texts) {
2190
+ if (!t2 || !t2.trim()) {
2191
2191
  results.push(null);
2192
2192
  continue;
2193
2193
  }
2194
2194
  try {
2195
2195
  let vec = null;
2196
2196
  if (settings.endpoint) {
2197
- vec = await embedTier0(t, settings);
2197
+ vec = await embedTier0(t2, settings);
2198
2198
  }
2199
- if (!vec) vec = await embedTier1(t);
2199
+ if (!vec) vec = await embedTier1(t2);
2200
2200
  results.push(vec);
2201
2201
  } catch {
2202
2202
  results.push(null);
@@ -2340,6 +2340,8 @@ __export(memory_db_exports, {
2340
2340
  MEMORY_FILE_TITLE_LIKE: () => MEMORY_FILE_TITLE_LIKE,
2341
2341
  MEMORY_FILE_TITLE_PREFIX: () => MEMORY_FILE_TITLE_PREFIX,
2342
2342
  MEMORY_SCHEMA_VERSION: () => MEMORY_SCHEMA_VERSION,
2343
+ PENDING_SYNC_MAX_AGE_DAYS: () => PENDING_SYNC_MAX_AGE_DAYS,
2344
+ PENDING_SYNC_MAX_ITEMS: () => PENDING_SYNC_MAX_ITEMS,
2343
2345
  TOOL_COST_EVENTS_RETENTION_DAYS: () => TOOL_COST_EVENTS_RETENTION_DAYS,
2344
2346
  USAGE_COUNTER_ARMED_KEY: () => USAGE_COUNTER_ARMED_KEY,
2345
2347
  addConversationTurn: () => addConversationTurn,
@@ -2348,6 +2350,7 @@ __export(memory_db_exports, {
2348
2350
  addSummary: () => addSummary,
2349
2351
  addToolCallDetail: () => addToolCallDetail,
2350
2352
  addUserPrompt: () => addUserPrompt,
2353
+ allPendingSync: () => allPendingSync,
2351
2354
  appendIncidentToFailureClass: () => appendIncidentToFailureClass,
2352
2355
  armUsageCounter: () => armUsageCounter,
2353
2356
  assignImportance: () => assignImportance,
@@ -2355,6 +2358,7 @@ __export(memory_db_exports, {
2355
2358
  createSession: () => createSession,
2356
2359
  deduplicateFailedAttempt: () => deduplicateFailedAttempt,
2357
2360
  dequeuePendingSync: () => dequeuePendingSync,
2361
+ discardPendingSync: () => discardPendingSync,
2358
2362
  embedMissingObservations: () => embedMissingObservations,
2359
2363
  endSession: () => endSession,
2360
2364
  enqueueRulePromotionEvent: () => enqueueRulePromotionEvent,
@@ -2411,14 +2415,14 @@ function sanitizeFts5Query(raw) {
2411
2415
  const trimmed = raw.trim();
2412
2416
  if (!trimmed) return '""';
2413
2417
  const tokens = trimmed.replace(/"/g, "").split(/\s+/).filter(Boolean);
2414
- return tokens.map((t) => `"${t}"`).join(" ");
2418
+ return tokens.map((t2) => `"${t2}"`).join(" ");
2415
2419
  }
2416
2420
  function sanitizeFts5QueryOr(raw) {
2417
2421
  const trimmed = raw.trim();
2418
2422
  if (!trimmed) return '""';
2419
- const tokens = trimmed.replace(/"/g, "").split(/\s+/).filter((t) => t.replace(/[^a-zA-Z0-9]/g, "").length >= 3);
2423
+ const tokens = trimmed.replace(/"/g, "").split(/\s+/).filter((t2) => t2.replace(/[^a-zA-Z0-9]/g, "").length >= 3);
2420
2424
  if (tokens.length === 0) return '""';
2421
- return tokens.map((t) => `"${t}"`).join(" OR ");
2425
+ return tokens.map((t2) => `"${t2}"`).join(" OR ");
2422
2426
  }
2423
2427
  function getMemoryDb() {
2424
2428
  const dbPath = getResolvedPaths().memoryDbPath;
@@ -3337,6 +3341,35 @@ function initMemorySchema(db) {
3337
3341
  }
3338
3342
  function enqueueSyncPayload(db, payload) {
3339
3343
  db.prepare("INSERT INTO pending_sync (payload) VALUES (?)").run(payload);
3344
+ const overflow = db.prepare(
3345
+ `SELECT id, retry_count, last_error, payload FROM pending_sync
3346
+ WHERE created_at < datetime('now', ?)
3347
+ OR id NOT IN (SELECT id FROM pending_sync ORDER BY created_at DESC, id DESC LIMIT ?)
3348
+ LIMIT 10000`
3349
+ ).all(`-${PENDING_SYNC_MAX_AGE_DAYS} days`, PENDING_SYNC_MAX_ITEMS);
3350
+ if (overflow.length > 0) {
3351
+ discardPendingSync(db, overflow, `over the queue limit (${PENDING_SYNC_MAX_ITEMS} items / ${PENDING_SYNC_MAX_AGE_DAYS} days)`);
3352
+ }
3353
+ }
3354
+ function discardPendingSync(db, rows, why) {
3355
+ if (rows.length === 0) return 0;
3356
+ const rescued = rescueLearningFromStalePayloads(db, rows);
3357
+ for (let i = 0; i < rows.length; i += 500) {
3358
+ const ids = rows.slice(i, i + 500).map((r2) => r2.id);
3359
+ db.prepare(`DELETE FROM pending_sync WHERE id IN (${ids.map(() => "?").join(",")})`).run(...ids);
3360
+ }
3361
+ if (rescued > 0) {
3362
+ process.stderr.write(
3363
+ `[massu] RESCUED ${rescued} learned rule(s) from ${rows.length} discarded payload(s) \u2014 they were re-queued for delivery, NOT discarded.
3364
+ `
3365
+ );
3366
+ }
3367
+ process.stderr.write(`[massu] cloud sync: discarded ${rows.length} queued upload(s): ${why}.
3368
+ `);
3369
+ return rows.length;
3370
+ }
3371
+ function allPendingSync(db) {
3372
+ return db.prepare("SELECT id, retry_count, last_error, payload FROM pending_sync").all();
3340
3373
  }
3341
3374
  function getMemoryMeta(db, key) {
3342
3375
  const row = db.prepare("SELECT value FROM memory_meta WHERE key = ?").get(key);
@@ -3364,9 +3397,9 @@ function upsertObservationEmbedding(db, observationId, vec, modelId, dim) {
3364
3397
  ).run(observationId, modelId, dim, float32ToBlob(vec));
3365
3398
  }
3366
3399
  function observationEmbedText(title, detail) {
3367
- const t = (title ?? "").trim();
3400
+ const t2 = (title ?? "").trim();
3368
3401
  const d = (detail ?? "").trim();
3369
- return d ? `${t}. ${d}` : t;
3402
+ return d ? `${t2}. ${d}` : t2;
3370
3403
  }
3371
3404
  async function embedMissingObservations(db, opts = {}) {
3372
3405
  return runEmbedSweep(
@@ -3539,7 +3572,9 @@ function dequeuePendingSync(db, limit = 10) {
3539
3572
  }
3540
3573
  }
3541
3574
  return db.prepare(
3542
- "SELECT id, payload, retry_count FROM pending_sync ORDER BY created_at ASC LIMIT ?"
3575
+ // Least-retried first: oldest-first retried the same 10 items until they were discarded
3576
+ // while every newer item waited at retry 0 (2026-11-09: 43,492 never attempted).
3577
+ "SELECT id, payload, retry_count FROM pending_sync ORDER BY retry_count ASC, created_at ASC LIMIT ?"
3543
3578
  ).all(limit);
3544
3579
  }
3545
3580
  function removePendingSync(db, id) {
@@ -4212,7 +4247,7 @@ function scoreFailureClasses(db, matchText, filePath, promptContext, weights) {
4212
4247
  }
4213
4248
  return bestMatch;
4214
4249
  }
4215
- var MEMORY_SCHEMA_VERSION, TOOL_COST_EVENTS_RETENTION_DAYS, MAX_DRAFT_TEXT_LEN, FUNNEL_EVENT_OUTBOX_CAP, _temporalColCache, CONSOLIDATION_LESSON_EVIDENCE, MEMORY_FILE_TITLE_PREFIX, MEMORY_FILE_TITLE_LIKE, USAGE_COUNTER_ARMED_KEY;
4250
+ var MEMORY_SCHEMA_VERSION, TOOL_COST_EVENTS_RETENTION_DAYS, PENDING_SYNC_MAX_ITEMS, PENDING_SYNC_MAX_AGE_DAYS, MAX_DRAFT_TEXT_LEN, FUNNEL_EVENT_OUTBOX_CAP, _temporalColCache, CONSOLIDATION_LESSON_EVIDENCE, MEMORY_FILE_TITLE_PREFIX, MEMORY_FILE_TITLE_LIKE, USAGE_COUNTER_ARMED_KEY;
4216
4251
  var init_memory_db = __esm({
4217
4252
  "src/memory-db.ts"() {
4218
4253
  "use strict";
@@ -4227,6 +4262,8 @@ var init_memory_db = __esm({
4227
4262
  init_memory_embed_sweep();
4228
4263
  MEMORY_SCHEMA_VERSION = 2;
4229
4264
  TOOL_COST_EVENTS_RETENTION_DAYS = 90;
4265
+ PENDING_SYNC_MAX_ITEMS = 500;
4266
+ PENDING_SYNC_MAX_AGE_DAYS = 30;
4230
4267
  MAX_DRAFT_TEXT_LEN = 16384;
4231
4268
  FUNNEL_EVENT_OUTBOX_CAP = 2e4;
4232
4269
  _temporalColCache = /* @__PURE__ */ new WeakMap();
@@ -4560,6 +4597,21 @@ function classifyVisibility(title, detail) {
4560
4597
  }
4561
4598
 
4562
4599
  // src/cloud-sync.ts
4600
+ var CLOUD_SYNC_REFUSAL_KEY = "cloud_sync_refused";
4601
+ var CLOUD_SYNC_RECHECK_MS = 24 * 60 * 60 * 1e3;
4602
+ function isPermanentSyncStatus(status) {
4603
+ return status >= 400 && status < 500 && status !== 408 && status !== 429;
4604
+ }
4605
+ function readCloudSyncRefusal(db) {
4606
+ try {
4607
+ const raw = getMemoryMeta(db, CLOUD_SYNC_REFUSAL_KEY);
4608
+ if (!raw) return null;
4609
+ const v = JSON.parse(raw);
4610
+ return v && typeof v.status === "number" ? v : null;
4611
+ } catch {
4612
+ return null;
4613
+ }
4614
+ }
4563
4615
  var MAX_RETRIES = 3;
4564
4616
  var RETRY_DELAYS = [1e3, 2e3, 4e3];
4565
4617
  var DEFAULT_CLOUD_REQUEST_TIMEOUT_MS = 15e3;
@@ -4579,6 +4631,15 @@ async function syncToCloud(db, payload, opts = {}) {
4579
4631
  if (!endpoint) {
4580
4632
  return { success: false, synced: { sessions: 0, observations: 0, analytics: 0, audit: 0 }, error: "No sync endpoint configured" };
4581
4633
  }
4634
+ const refusal = readCloudSyncRefusal(db);
4635
+ if (refusal && Date.now() - Date.parse(refusal.at) < CLOUD_SYNC_RECHECK_MS) {
4636
+ return {
4637
+ success: false,
4638
+ paused: true,
4639
+ synced: { sessions: 0, observations: 0, analytics: 0, audit: 0 },
4640
+ error: `cloud sync paused: HTTP ${refusal.status}: ${refusal.reason}`
4641
+ };
4642
+ }
4582
4643
  const filteredPayload = {};
4583
4644
  if (cloud.sync?.memory !== false) {
4584
4645
  filteredPayload.sessions = payload.sessions;
@@ -4648,9 +4709,12 @@ async function syncToCloud(db, payload, opts = {}) {
4648
4709
  }
4649
4710
  }
4650
4711
  let lastError = "";
4712
+ let permanentStatus = null;
4713
+ let permanentReason = "";
4651
4714
  const configuredTimeoutMs = cloud.requestTimeoutMs ?? DEFAULT_CLOUD_REQUEST_TIMEOUT_MS;
4652
4715
  const deadlineAt = Math.min(Date.now() + SYNC_DEADLINE_MS, opts.deadlineAt ?? Infinity);
4653
- for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
4716
+ const maxAttempts = Math.max(1, Math.min(opts.maxAttempts ?? MAX_RETRIES, MAX_RETRIES));
4717
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
4654
4718
  const remainingMs = deadlineAt - Date.now();
4655
4719
  if (remainingMs < MIN_ATTEMPT_BUDGET_MS) {
4656
4720
  lastError = lastError || "sync deadline exceeded";
@@ -4671,16 +4735,25 @@ async function syncToCloud(db, payload, opts = {}) {
4671
4735
  signal: AbortSignal.timeout(requestTimeoutMs)
4672
4736
  });
4673
4737
  if (!response.ok) {
4674
- lastError = `HTTP ${response.status}: ${response.statusText}`;
4675
- if (response.status >= 400 && response.status < 500) {
4738
+ const reason = await readErrorReason(response);
4739
+ lastError = `HTTP ${response.status}: ${reason}`;
4740
+ if (isPermanentSyncStatus(response.status)) {
4741
+ permanentStatus = response.status;
4742
+ permanentReason = reason;
4676
4743
  break;
4677
4744
  }
4678
- if (attempt < MAX_RETRIES - 1) {
4745
+ if (attempt < maxAttempts - 1) {
4679
4746
  await sleep(RETRY_DELAYS[attempt]);
4680
4747
  continue;
4681
4748
  }
4682
4749
  break;
4683
4750
  }
4751
+ if (refusal) {
4752
+ try {
4753
+ setMemoryMeta(db, CLOUD_SYNC_REFUSAL_KEY, "");
4754
+ } catch {
4755
+ }
4756
+ }
4684
4757
  const result = await response.json();
4685
4758
  return {
4686
4759
  success: true,
@@ -4700,12 +4773,33 @@ async function syncToCloud(db, payload, opts = {}) {
4700
4773
  if (err instanceof Error && (err.name === "AbortError" || err.name === "TimeoutError")) {
4701
4774
  break;
4702
4775
  }
4703
- if (attempt < MAX_RETRIES - 1) {
4776
+ if (attempt < maxAttempts - 1) {
4704
4777
  await sleep(RETRY_DELAYS[attempt]);
4705
4778
  continue;
4706
4779
  }
4707
4780
  }
4708
4781
  }
4782
+ if (permanentStatus !== null) {
4783
+ if (permanentStatus === 401 || permanentStatus === 403) {
4784
+ try {
4785
+ setMemoryMeta(db, CLOUD_SYNC_REFUSAL_KEY, JSON.stringify({
4786
+ status: permanentStatus,
4787
+ reason: permanentReason,
4788
+ at: (/* @__PURE__ */ new Date()).toISOString()
4789
+ }));
4790
+ discardPendingSync(db, allPendingSync(db), `the cloud refused this account (HTTP ${permanentStatus}: ${permanentReason})`);
4791
+ } catch (err) {
4792
+ process.stderr.write(`[massu] WARNING: could not record the cloud-sync refusal: ${err instanceof Error ? err.message : String(err)}
4793
+ `);
4794
+ }
4795
+ }
4796
+ return {
4797
+ success: false,
4798
+ permanent: true,
4799
+ synced: { sessions: 0, observations: 0, analytics: 0, audit: 0 },
4800
+ error: lastError
4801
+ };
4802
+ }
4709
4803
  if (opts.enqueueOnFailure !== false) {
4710
4804
  try {
4711
4805
  enqueueSyncPayload(db, JSON.stringify(stripLearningFromPayload(payload)));
@@ -4726,6 +4820,8 @@ async function drainSyncQueue(db, deadlineAt) {
4726
4820
  const out = { attempted: 0, deferred: 0 };
4727
4821
  const config = getConfig();
4728
4822
  if (!config.cloud?.enabled || !config.cloud?.apiKey) return out;
4823
+ const refusal = readCloudSyncRefusal(db);
4824
+ if (refusal && Date.now() - Date.parse(refusal.at) < CLOUD_SYNC_RECHECK_MS) return out;
4729
4825
  const pending = dequeuePendingSync(db, 10);
4730
4826
  for (let i = 0; i < pending.length; i++) {
4731
4827
  const item = pending[i];
@@ -4737,9 +4833,14 @@ async function drainSyncQueue(db, deadlineAt) {
4737
4833
  out.attempted++;
4738
4834
  try {
4739
4835
  const payload = JSON.parse(item.payload);
4740
- const result = await syncToCloud(db, payload, { deadlineAt, enqueueOnFailure: false });
4836
+ const result = await syncToCloud(db, payload, { deadlineAt, enqueueOnFailure: false, maxAttempts: 1 });
4741
4837
  if (result.success) {
4742
4838
  removePendingSync(db, item.id);
4839
+ } else if (result.paused) {
4840
+ break;
4841
+ } else if (result.permanent) {
4842
+ discardPendingSync(db, [{ id: item.id, retry_count: item.retry_count, last_error: result.error ?? null, payload: item.payload }], result.error ?? "permanent refusal");
4843
+ if (readCloudSyncRefusal(db)) break;
4743
4844
  } else {
4744
4845
  incrementRetryCount(db, item.id, result.error ?? "Unknown error");
4745
4846
  }
@@ -4750,7 +4851,21 @@ async function drainSyncQueue(db, deadlineAt) {
4750
4851
  return out;
4751
4852
  }
4752
4853
  function sleep(ms) {
4753
- return new Promise((resolve8) => setTimeout(resolve8, ms));
4854
+ return new Promise((resolve9) => setTimeout(resolve9, ms));
4855
+ }
4856
+ async function readErrorReason(response) {
4857
+ try {
4858
+ const text = (await response.text()).slice(0, 300);
4859
+ try {
4860
+ const j = JSON.parse(text);
4861
+ const r2 = typeof j.error === "string" ? j.error : typeof j.message === "string" ? j.message : "";
4862
+ if (r2) return r2;
4863
+ } catch {
4864
+ }
4865
+ return text.trim() || response.statusText;
4866
+ } catch {
4867
+ return response.statusText;
4868
+ }
4754
4869
  }
4755
4870
 
4756
4871
  // src/team-rule-sync.ts
@@ -5131,6 +5246,13 @@ function assertContainedIn(rootDir, candidate, opts = {}) {
5131
5246
  }
5132
5247
  return abs;
5133
5248
  }
5249
+ function isSameRealPath(a, b) {
5250
+ try {
5251
+ return realpathSync(a) === realpathSync(b);
5252
+ } catch {
5253
+ return false;
5254
+ }
5255
+ }
5134
5256
  function isContainedIn(rootDir, candidate, opts = {}) {
5135
5257
  if (candidate.includes("\0")) return false;
5136
5258
  const root = resolve7(rootDir);
@@ -5953,9 +6075,9 @@ var NOISE_PATTERNS = [
5953
6075
  /system-reminder/i
5954
6076
  ];
5955
6077
  function isSummarizableSignal(text) {
5956
- const t = text.trim();
5957
- if (t.length < 25) return false;
5958
- return !NOISE_PATTERNS.some((re) => re.test(t));
6078
+ const t2 = text.trim();
6079
+ if (t2.length < 25) return false;
6080
+ return !NOISE_PATTERNS.some((re) => re.test(t2));
5959
6081
  }
5960
6082
  var CREDENTIAL_LABELS = ["api[_-]?key", "secret", "password", "token"];
5961
6083
  var LABELLED_CREDENTIAL = new RegExp(
@@ -6400,9 +6522,403 @@ function backfillQualityScores(db) {
6400
6522
 
6401
6523
  // src/cost-reprice.ts
6402
6524
  init_config();
6403
- init_memory_db();
6404
- import { existsSync as existsSync16 } from "fs";
6525
+ import { existsSync as existsSync17 } from "fs";
6526
+ import { dirname as dirname14, join as join12 } from "path";
6527
+
6528
+ // src/db.ts
6529
+ init_db_driver();
6405
6530
  import { dirname as dirname13, join as join11 } from "path";
6531
+ import { existsSync as existsSync16, mkdirSync as mkdirSync14, readdirSync as readdirSync3, statSync as statSync4 } from "fs";
6532
+
6533
+ // src/preflight.ts
6534
+ init_db_driver();
6535
+ init_config();
6536
+
6537
+ // src/db.ts
6538
+ init_config();
6539
+
6540
+ // src/lib/sql-table-names.ts
6541
+ init_config();
6542
+ function t(suffix) {
6543
+ return `${getConfig().toolPrefix}_${suffix}`;
6544
+ }
6545
+
6546
+ // src/db.ts
6547
+ function getDataDb() {
6548
+ const dbPath = getResolvedPaths().dataDbPath;
6549
+ const dir = dirname13(dbPath);
6550
+ if (!existsSync16(dir)) {
6551
+ mkdirSync14(dir, { recursive: true });
6552
+ }
6553
+ const db = openDatabase2(dbPath);
6554
+ db.pragma("journal_mode = WAL");
6555
+ db.pragma("foreign_keys = ON");
6556
+ initDataSchema(db);
6557
+ return db;
6558
+ }
6559
+ function initDataSchema(db) {
6560
+ db.exec(`
6561
+ CREATE TABLE IF NOT EXISTS ${t("imports")} (
6562
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
6563
+ source_file TEXT NOT NULL,
6564
+ target_file TEXT NOT NULL,
6565
+ import_type TEXT NOT NULL CHECK(import_type IN ('named', 'default', 'namespace', 'side_effect', 'dynamic')),
6566
+ imported_names TEXT NOT NULL DEFAULT '[]',
6567
+ line INTEGER NOT NULL DEFAULT 0
6568
+ );
6569
+
6570
+ CREATE INDEX IF NOT EXISTS idx_massu_imports_source ON ${t("imports")}(source_file);
6571
+ CREATE INDEX IF NOT EXISTS idx_massu_imports_target ON ${t("imports")}(target_file);
6572
+
6573
+ CREATE TABLE IF NOT EXISTS ${t("trpc_procedures")} (
6574
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
6575
+ router_file TEXT NOT NULL,
6576
+ router_name TEXT NOT NULL,
6577
+ procedure_name TEXT NOT NULL,
6578
+ procedure_type TEXT NOT NULL CHECK(procedure_type IN ('query', 'mutation')),
6579
+ has_ui_caller INTEGER NOT NULL DEFAULT 0
6580
+ );
6581
+
6582
+ CREATE INDEX IF NOT EXISTS idx_massu_trpc_router ON ${t("trpc_procedures")}(router_name);
6583
+
6584
+ CREATE TABLE IF NOT EXISTS ${t("trpc_call_sites")} (
6585
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
6586
+ procedure_id INTEGER NOT NULL,
6587
+ file TEXT NOT NULL,
6588
+ line INTEGER NOT NULL DEFAULT 0,
6589
+ call_pattern TEXT NOT NULL,
6590
+ FOREIGN KEY (procedure_id) REFERENCES ${t("trpc_procedures")}(id)
6591
+ );
6592
+
6593
+ CREATE INDEX IF NOT EXISTS idx_massu_call_sites_proc ON ${t("trpc_call_sites")}(procedure_id);
6594
+
6595
+ CREATE TABLE IF NOT EXISTS ${t("page_deps")} (
6596
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
6597
+ page_file TEXT NOT NULL,
6598
+ route TEXT NOT NULL,
6599
+ portal TEXT NOT NULL DEFAULT 'unknown',
6600
+ components TEXT NOT NULL DEFAULT '[]',
6601
+ hooks TEXT NOT NULL DEFAULT '[]',
6602
+ routers TEXT NOT NULL DEFAULT '[]',
6603
+ tables_touched TEXT NOT NULL DEFAULT '[]'
6604
+ );
6605
+
6606
+ CREATE INDEX IF NOT EXISTS idx_massu_page_deps_page ON ${t("page_deps")}(page_file);
6607
+
6608
+ CREATE TABLE IF NOT EXISTS ${t("middleware_tree")} (
6609
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
6610
+ file TEXT NOT NULL UNIQUE
6611
+ );
6612
+
6613
+ CREATE TABLE IF NOT EXISTS ${t("meta")} (
6614
+ key TEXT PRIMARY KEY,
6615
+ value TEXT NOT NULL
6616
+ );
6617
+
6618
+ -- ============================================================
6619
+ -- Python Code Intelligence Tables
6620
+ -- ============================================================
6621
+
6622
+ CREATE TABLE IF NOT EXISTS ${t("py_imports")} (
6623
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
6624
+ source_file TEXT NOT NULL,
6625
+ target_file TEXT NOT NULL,
6626
+ import_type TEXT NOT NULL CHECK(import_type IN ('absolute', 'relative', 'from_absolute', 'from_relative')),
6627
+ imported_names TEXT NOT NULL DEFAULT '[]',
6628
+ line INTEGER NOT NULL DEFAULT 0
6629
+ );
6630
+ CREATE INDEX IF NOT EXISTS idx_massu_py_imports_source ON ${t("py_imports")}(source_file);
6631
+ CREATE INDEX IF NOT EXISTS idx_massu_py_imports_target ON ${t("py_imports")}(target_file);
6632
+
6633
+ CREATE TABLE IF NOT EXISTS ${t("py_meta")} (
6634
+ key TEXT PRIMARY KEY,
6635
+ value TEXT NOT NULL
6636
+ );
6637
+
6638
+ CREATE TABLE IF NOT EXISTS ${t("py_routes")} (
6639
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
6640
+ file TEXT NOT NULL,
6641
+ method TEXT NOT NULL,
6642
+ path TEXT NOT NULL,
6643
+ function_name TEXT NOT NULL,
6644
+ dependencies TEXT NOT NULL DEFAULT '[]',
6645
+ request_model TEXT,
6646
+ response_model TEXT,
6647
+ is_authenticated INTEGER NOT NULL DEFAULT 0,
6648
+ line INTEGER NOT NULL DEFAULT 0
6649
+ );
6650
+ CREATE INDEX IF NOT EXISTS idx_massu_py_routes_path ON ${t("py_routes")}(path);
6651
+ CREATE INDEX IF NOT EXISTS idx_massu_py_routes_file ON ${t("py_routes")}(file);
6652
+
6653
+ CREATE TABLE IF NOT EXISTS ${t("py_route_callers")} (
6654
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
6655
+ route_id INTEGER NOT NULL REFERENCES ${t("py_routes")}(id) ON DELETE CASCADE,
6656
+ frontend_file TEXT NOT NULL,
6657
+ line INTEGER NOT NULL DEFAULT 0,
6658
+ call_pattern TEXT NOT NULL
6659
+ );
6660
+ CREATE INDEX IF NOT EXISTS idx_massu_py_route_callers_route ON ${t("py_route_callers")}(route_id);
6661
+
6662
+ CREATE TABLE IF NOT EXISTS ${t("py_models")} (
6663
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
6664
+ class_name TEXT NOT NULL,
6665
+ table_name TEXT,
6666
+ file TEXT NOT NULL,
6667
+ line INTEGER NOT NULL DEFAULT 0,
6668
+ columns TEXT NOT NULL DEFAULT '[]',
6669
+ relationships TEXT NOT NULL DEFAULT '[]',
6670
+ foreign_keys TEXT NOT NULL DEFAULT '[]'
6671
+ );
6672
+ CREATE INDEX IF NOT EXISTS idx_massu_py_models_file ON ${t("py_models")}(file);
6673
+ CREATE INDEX IF NOT EXISTS idx_massu_py_models_table ON ${t("py_models")}(table_name);
6674
+
6675
+ CREATE TABLE IF NOT EXISTS ${t("py_fk_edges")} (
6676
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
6677
+ source_table TEXT NOT NULL,
6678
+ source_column TEXT NOT NULL,
6679
+ target_table TEXT NOT NULL,
6680
+ target_column TEXT NOT NULL
6681
+ );
6682
+ CREATE INDEX IF NOT EXISTS idx_massu_py_fk_source ON ${t("py_fk_edges")}(source_table);
6683
+ CREATE INDEX IF NOT EXISTS idx_massu_py_fk_target ON ${t("py_fk_edges")}(target_table);
6684
+
6685
+ CREATE TABLE IF NOT EXISTS ${t("py_migrations")} (
6686
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
6687
+ revision TEXT NOT NULL UNIQUE,
6688
+ down_revision TEXT,
6689
+ file TEXT NOT NULL,
6690
+ description TEXT,
6691
+ operations TEXT NOT NULL DEFAULT '[]',
6692
+ is_head INTEGER NOT NULL DEFAULT 0
6693
+ );
6694
+ CREATE INDEX IF NOT EXISTS idx_massu_py_migrations_rev ON ${t("py_migrations")}(revision);
6695
+
6696
+ -- ============================================================
6697
+ -- Sentinel: Feature Registry Tables
6698
+ -- ============================================================
6699
+
6700
+ -- Core feature definition
6701
+ CREATE TABLE IF NOT EXISTS ${t("sentinel")} (
6702
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
6703
+ feature_key TEXT UNIQUE NOT NULL,
6704
+ domain TEXT NOT NULL,
6705
+ subdomain TEXT,
6706
+ title TEXT NOT NULL,
6707
+ description TEXT,
6708
+ status TEXT NOT NULL DEFAULT 'active'
6709
+ CHECK(status IN ('planned', 'active', 'deprecated', 'removed')),
6710
+ priority TEXT DEFAULT 'standard'
6711
+ CHECK(priority IN ('critical', 'standard', 'nice-to-have')),
6712
+ portal_scope TEXT DEFAULT '[]',
6713
+ created_at TEXT DEFAULT (datetime('now')),
6714
+ updated_at TEXT DEFAULT (datetime('now')),
6715
+ removed_at TEXT,
6716
+ removed_reason TEXT
6717
+ );
6718
+
6719
+ CREATE INDEX IF NOT EXISTS idx_sentinel_domain ON ${t("sentinel")}(domain);
6720
+ CREATE INDEX IF NOT EXISTS idx_sentinel_status ON ${t("sentinel")}(status);
6721
+ CREATE INDEX IF NOT EXISTS idx_sentinel_key ON ${t("sentinel")}(feature_key);
6722
+
6723
+ -- Feature-to-component mapping (many-to-many)
6724
+ CREATE TABLE IF NOT EXISTS ${t("sentinel_components")} (
6725
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
6726
+ feature_id INTEGER NOT NULL REFERENCES ${t("sentinel")}(id) ON DELETE CASCADE,
6727
+ component_file TEXT NOT NULL,
6728
+ component_name TEXT,
6729
+ role TEXT DEFAULT 'implementation'
6730
+ CHECK(role IN ('implementation', 'ui', 'data', 'utility')),
6731
+ is_primary BOOLEAN DEFAULT 0,
6732
+ UNIQUE(feature_id, component_file, component_name)
6733
+ );
6734
+
6735
+ -- Feature-to-procedure mapping (many-to-many)
6736
+ CREATE TABLE IF NOT EXISTS ${t("sentinel_procedures")} (
6737
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
6738
+ feature_id INTEGER NOT NULL REFERENCES ${t("sentinel")}(id) ON DELETE CASCADE,
6739
+ router_name TEXT NOT NULL,
6740
+ procedure_name TEXT NOT NULL,
6741
+ procedure_type TEXT,
6742
+ UNIQUE(feature_id, router_name, procedure_name)
6743
+ );
6744
+
6745
+ -- Feature-to-page mapping (where feature is accessible)
6746
+ CREATE TABLE IF NOT EXISTS ${t("sentinel_pages")} (
6747
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
6748
+ feature_id INTEGER NOT NULL REFERENCES ${t("sentinel")}(id) ON DELETE CASCADE,
6749
+ page_route TEXT NOT NULL,
6750
+ portal TEXT,
6751
+ UNIQUE(feature_id, page_route, portal)
6752
+ );
6753
+
6754
+ -- Feature dependency graph
6755
+ CREATE TABLE IF NOT EXISTS ${t("sentinel_deps")} (
6756
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
6757
+ feature_id INTEGER NOT NULL REFERENCES ${t("sentinel")}(id) ON DELETE CASCADE,
6758
+ depends_on_feature_id INTEGER NOT NULL REFERENCES ${t("sentinel")}(id) ON DELETE CASCADE,
6759
+ dependency_type TEXT DEFAULT 'requires'
6760
+ CHECK(dependency_type IN ('requires', 'enhances', 'replaces')),
6761
+ UNIQUE(feature_id, depends_on_feature_id)
6762
+ );
6763
+
6764
+ -- Feature change log (audit trail)
6765
+ CREATE TABLE IF NOT EXISTS ${t("sentinel_changelog")} (
6766
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
6767
+ feature_id INTEGER NOT NULL REFERENCES ${t("sentinel")}(id) ON DELETE CASCADE,
6768
+ change_type TEXT NOT NULL,
6769
+ changed_by TEXT,
6770
+ change_detail TEXT,
6771
+ commit_hash TEXT,
6772
+ created_at TEXT DEFAULT (datetime('now'))
6773
+ );
6774
+
6775
+ CREATE INDEX IF NOT EXISTS idx_sentinel_components_file ON ${t("sentinel_components")}(component_file);
6776
+ CREATE INDEX IF NOT EXISTS idx_sentinel_procedures_router ON ${t("sentinel_procedures")}(router_name);
6777
+ CREATE INDEX IF NOT EXISTS idx_sentinel_pages_route ON ${t("sentinel_pages")}(page_route);
6778
+ CREATE INDEX IF NOT EXISTS idx_sentinel_changelog_feature ON ${t("sentinel_changelog")}(feature_id);
6779
+ `);
6780
+ db.exec(`
6781
+ CREATE VIRTUAL TABLE IF NOT EXISTS ${t("sentinel_fts")} USING fts5(
6782
+ feature_key, title, description, domain, subdomain,
6783
+ content=${t("sentinel")}, content_rowid=id
6784
+ );
6785
+ `);
6786
+ db.exec(`
6787
+ CREATE TRIGGER IF NOT EXISTS massu_sentinel_ai AFTER INSERT ON ${t("sentinel")} BEGIN
6788
+ INSERT INTO ${t("sentinel_fts")}(rowid, feature_key, title, description, domain, subdomain)
6789
+ VALUES (new.id, new.feature_key, new.title, new.description, new.domain, new.subdomain);
6790
+ END;
6791
+
6792
+ CREATE TRIGGER IF NOT EXISTS massu_sentinel_ad AFTER DELETE ON ${t("sentinel")} BEGIN
6793
+ INSERT INTO ${t("sentinel_fts")}(${t("sentinel_fts")}, rowid, feature_key, title, description, domain, subdomain)
6794
+ VALUES ('delete', old.id, old.feature_key, old.title, old.description, old.domain, old.subdomain);
6795
+ END;
6796
+
6797
+ CREATE TRIGGER IF NOT EXISTS massu_sentinel_au AFTER UPDATE ON ${t("sentinel")} BEGIN
6798
+ INSERT INTO ${t("sentinel_fts")}(${t("sentinel_fts")}, rowid, feature_key, title, description, domain, subdomain)
6799
+ VALUES ('delete', old.id, old.feature_key, old.title, old.description, old.domain, old.subdomain);
6800
+ INSERT INTO ${t("sentinel_fts")}(rowid, feature_key, title, description, domain, subdomain)
6801
+ VALUES (new.id, new.feature_key, new.title, new.description, new.domain, new.subdomain);
6802
+ END;
6803
+ `);
6804
+ }
6805
+
6806
+ // src/feature-cost-attribution.ts
6807
+ import { isAbsolute as isAbsolute2, relative as relative2, resolve as resolve8 } from "path";
6808
+
6809
+ // src/sentinel-db.ts
6810
+ init_config();
6811
+ init_memory_db();
6812
+ var PROJECT_ROOT = getProjectRoot();
6813
+ function parsePortalScope(raw) {
6814
+ if (!raw) return [];
6815
+ try {
6816
+ return JSON.parse(raw);
6817
+ } catch {
6818
+ return [];
6819
+ }
6820
+ }
6821
+ function toFeature(row) {
6822
+ return {
6823
+ id: row.id,
6824
+ feature_key: row.feature_key,
6825
+ domain: row.domain,
6826
+ subdomain: row.subdomain || null,
6827
+ title: row.title,
6828
+ description: row.description || null,
6829
+ status: row.status,
6830
+ priority: row.priority,
6831
+ portal_scope: parsePortalScope(row.portal_scope),
6832
+ created_at: row.created_at,
6833
+ updated_at: row.updated_at,
6834
+ removed_at: row.removed_at || null,
6835
+ removed_reason: row.removed_reason || null
6836
+ };
6837
+ }
6838
+ function getFeaturesByFile(db, filePath) {
6839
+ const rows = db.prepare(`
6840
+ SELECT DISTINCT s.* FROM ${t("sentinel")} s
6841
+ JOIN ${t("sentinel_components")} c ON c.feature_id = s.id
6842
+ WHERE c.component_file = ?
6843
+ ORDER BY s.feature_key
6844
+ LIMIT 1000
6845
+ `).all(filePath);
6846
+ return rows.map(toFeature);
6847
+ }
6848
+
6849
+ // src/feature-cost-attribution.ts
6850
+ var UNATTRIBUTED_FEATURE = "(no registered feature)";
6851
+ var MAX_SESSION_FILES = 2e4;
6852
+ function sessionFileTouches(memDb, sessionId, projectRoot) {
6853
+ const rows = memDb.prepare(
6854
+ `SELECT j.value AS file, COUNT(*) AS n
6855
+ FROM tool_call_details t, json_each(t.files_involved) j
6856
+ WHERE t.session_id = ? AND json_valid(t.files_involved)
6857
+ AND json_type(t.files_involved) = 'array' AND j.type = 'text' AND j.value <> ''
6858
+ GROUP BY j.value
6859
+ LIMIT ?`
6860
+ ).all(sessionId, MAX_SESSION_FILES + 1);
6861
+ if (rows.length > MAX_SESSION_FILES) {
6862
+ throw new Error(`session ${sessionId} touched more than ${MAX_SESSION_FILES} distinct files; not allocating a truncated list`);
6863
+ }
6864
+ const touches = /* @__PURE__ */ new Map();
6865
+ for (const { file, n } of rows) {
6866
+ const abs = isAbsolute2(file) ? file : resolve8(projectRoot, file);
6867
+ const rel = relative2(projectRoot, abs);
6868
+ const key = rel.startsWith("..") || isAbsolute2(rel) ? abs : rel;
6869
+ touches.set(key, (touches.get(key) ?? 0) + Number(n));
6870
+ }
6871
+ return touches;
6872
+ }
6873
+ function allocateSessionCost(touches, featuresOf, totals) {
6874
+ const units = /* @__PURE__ */ new Map();
6875
+ let all = 0;
6876
+ for (const [file, n] of touches) {
6877
+ all += n;
6878
+ const features = [...new Set(featuresOf(file))];
6879
+ if (features.length === 0) {
6880
+ units.set(UNATTRIBUTED_FEATURE, (units.get(UNATTRIBUTED_FEATURE) ?? 0) + n);
6881
+ } else {
6882
+ for (const k of features) units.set(k, (units.get(k) ?? 0) + n / features.length);
6883
+ }
6884
+ }
6885
+ if (all === 0) return [{ featureKey: UNATTRIBUTED_FEATURE, share: 1, tokens: totals.tokens, costUsd: totals.costUsd }];
6886
+ return [...units].map(([featureKey, u]) => ({
6887
+ featureKey,
6888
+ share: u / all,
6889
+ tokens: Math.round(totals.tokens * u / all),
6890
+ costUsd: totals.costUsd * u / all
6891
+ })).sort((a, b) => b.costUsd - a.costUsd || a.featureKey.localeCompare(b.featureKey));
6892
+ }
6893
+ function attributeSessionCost(memDb, dataDb, sessionId, projectRoot) {
6894
+ const cost = memDb.prepare(
6895
+ `SELECT total_tokens, estimated_cost_usd, cost_status FROM session_costs WHERE session_id = ?`
6896
+ ).get(sessionId);
6897
+ const replace = memDb.transaction((rows2) => {
6898
+ memDb.prepare("DELETE FROM feature_costs WHERE session_id = ?").run(sessionId);
6899
+ const ins = memDb.prepare(
6900
+ "INSERT INTO feature_costs (feature_key, session_id, tokens_used, estimated_cost_usd) VALUES (?, ?, ?, ?)"
6901
+ );
6902
+ for (const r2 of rows2) ins.run(r2.featureKey, sessionId, r2.tokens, r2.costUsd);
6903
+ });
6904
+ if (!cost || cost.cost_status === "unknown") {
6905
+ replace([]);
6906
+ return [];
6907
+ }
6908
+ const touches = sessionFileTouches(memDb, sessionId, projectRoot);
6909
+ const featuresOf = (file) => {
6910
+ const keys = getFeaturesByFile(dataDb, file).map((f) => f.feature_key);
6911
+ if (!isAbsolute2(file)) keys.push(...getFeaturesByFile(dataDb, resolve8(projectRoot, file)).map((f) => f.feature_key));
6912
+ return keys;
6913
+ };
6914
+ const rows = allocateSessionCost(touches, featuresOf, { tokens: cost.total_tokens, costUsd: cost.estimated_cost_usd });
6915
+ replace(rows);
6916
+ return rows;
6917
+ }
6918
+
6919
+ // src/cost-reprice.ts
6920
+ init_hook_failure_signal();
6921
+ init_memory_db();
6406
6922
 
6407
6923
  // src/cost-tracker.ts
6408
6924
  init_config();
@@ -6472,8 +6988,8 @@ function tokenCounts(u) {
6472
6988
  }
6473
6989
  function priceResponse(price, u) {
6474
6990
  if (!price) return null;
6475
- const t = tokenCounts(u);
6476
- const prompt = t.input + t.cacheRead + t.cacheWrite5m + t.cacheWrite1h;
6991
+ const t2 = tokenCounts(u);
6992
+ const prompt = t2.input + t2.cacheRead + t2.cacheWrite5m + t2.cacheWrite1h;
6477
6993
  let rates = price.longPrompt && prompt > price.longPrompt.overTokens ? price.longPrompt.rates : price;
6478
6994
  if (u.speed === "fast" && price.fast) {
6479
6995
  const k = price.fast.input / rates.input;
@@ -6486,7 +7002,7 @@ function priceResponse(price, u) {
6486
7002
  };
6487
7003
  }
6488
7004
  const geo = u.inference_geo === "us" ? price.usGeoMultiplier ?? 1 : 1;
6489
- const dollars = t.input * rates.input + t.output * rates.output + t.cacheRead * rates.cacheRead + t.cacheWrite5m * rates.cacheWrite5m + t.cacheWrite1h * rates.cacheWrite1h;
7005
+ const dollars = t2.input * rates.input + t2.output * rates.output + t2.cacheRead * rates.cacheRead + t2.cacheWrite5m * rates.cacheWrite5m + t2.cacheWrite1h * rates.cacheWrite1h;
6490
7006
  return dollars / 1e6 * geo;
6491
7007
  }
6492
7008
 
@@ -6520,12 +7036,12 @@ function costTranscriptEntries(entries, opts = {}) {
6520
7036
  out.lastMessageId = msg.id;
6521
7037
  }
6522
7038
  const usage = msg.usage;
6523
- const t = tokenCounts(usage);
6524
- if (t.input + t.output + t.cacheRead + t.cacheWrite5m + t.cacheWrite1h === 0) continue;
6525
- out.inputTokens += t.input;
6526
- out.outputTokens += t.output;
6527
- out.cacheReadTokens += t.cacheRead;
6528
- out.cacheWriteTokens += t.cacheWrite5m + t.cacheWrite1h;
7039
+ const t2 = tokenCounts(usage);
7040
+ if (t2.input + t2.output + t2.cacheRead + t2.cacheWrite5m + t2.cacheWrite1h === 0) continue;
7041
+ out.inputTokens += t2.input;
7042
+ out.outputTokens += t2.output;
7043
+ out.cacheReadTokens += t2.cacheRead;
7044
+ out.cacheWriteTokens += t2.cacheWrite5m + t2.cacheWrite1h;
6529
7045
  const dollars = priceResponse(resolveModelPrice(msg.model, overrides), usage);
6530
7046
  if (dollars === null) {
6531
7047
  out.unpricedResponses++;
@@ -6605,7 +7121,7 @@ function markSessionCostUnknown(db, sessionId) {
6605
7121
  var cursorKey = (sid) => `cost_processed_line:${sid}`;
6606
7122
  var lastIdKey = (sid) => `cost_last_message:${sid}`;
6607
7123
  var sessionLeaseKey = (sid) => `session_end_lease:${sid}`;
6608
- async function updateSessionCost(db, sessionId, transcriptPath) {
7124
+ async function updateSessionCost(db, sessionId, transcriptPath, attribute = attributeFeatureCosts) {
6609
7125
  const row = db.prepare("SELECT pricing_version FROM session_costs WHERE session_id = ?").get(sessionId);
6610
7126
  const cursor = Number(getMemoryMeta(db, cursorKey(sessionId)) ?? "0") || 0;
6611
7127
  const incremental = row?.pricing_version === PRICING_VERSION && cursor > 0;
@@ -6617,11 +7133,23 @@ async function updateSessionCost(db, sessionId, transcriptPath) {
6617
7133
  storeSessionCost(db, sessionId, slice, incremental ? "add" : "replace");
6618
7134
  setMemoryMeta(db, cursorKey(sessionId), String(totalLines));
6619
7135
  if (slice.lastMessageId) setMemoryMeta(db, lastIdKey(sessionId), slice.lastMessageId);
7136
+ attribute(db, sessionId);
6620
7137
  }
7138
+ var attributeFeatureCosts = (db, sessionId) => {
7139
+ let dataDb = null;
7140
+ try {
7141
+ dataDb = getDataDb();
7142
+ attributeSessionCost(db, dataDb, sessionId, getProjectRoot());
7143
+ } catch (err) {
7144
+ recordHookFailure("cost:feature-attribution", err, { sessionId });
7145
+ } finally {
7146
+ dataDb?.close();
7147
+ }
7148
+ };
6621
7149
  function defaultTranscriptFor(sessionId) {
6622
7150
  if (!/^[A-Za-z0-9_-]+$/.test(sessionId)) return null;
6623
- const path = join11(dirname13(getResolvedPaths().memoryDir), `${sessionId}.jsonl`);
6624
- return existsSync16(path) ? path : null;
7151
+ const path = join12(dirname14(getResolvedPaths().memoryDir), `${sessionId}.jsonl`);
7152
+ return existsSync17(path) ? path : null;
6625
7153
  }
6626
7154
  async function repriceStoredSessions(db, opts = {}) {
6627
7155
  const deadlineAt = opts.deadlineAt ?? Infinity;
@@ -6645,10 +7173,11 @@ async function repriceStoredSessions(db, opts = {}) {
6645
7173
  try {
6646
7174
  const path = find(sid);
6647
7175
  if (path) {
6648
- await updateSessionCost(db, sid, path);
7176
+ await updateSessionCost(db, sid, path, opts.attribute);
6649
7177
  out.repriced++;
6650
7178
  } else {
6651
7179
  markSessionCostUnknown(db, sid);
7180
+ (opts.attribute ?? attributeFeatureCosts)(db, sid);
6652
7181
  out.markedUnknown++;
6653
7182
  }
6654
7183
  } catch (err) {
@@ -6781,8 +7310,8 @@ init_memory_vector();
6781
7310
  function temporalPredicate(asOf, includeSuperseded, prefix) {
6782
7311
  const c = (name) => `${prefix}${name}`;
6783
7312
  if (asOf != null && Number.isFinite(asOf)) {
6784
- const t = Math.floor(Number(asOf) / 1e3);
6785
- 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})`;
7313
+ const t2 = Math.floor(Number(asOf) / 1e3);
7314
+ 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})`;
6786
7315
  }
6787
7316
  if (!includeSuperseded) {
6788
7317
  return `${c("expired_at_epoch")} IS NULL`;
@@ -6878,7 +7407,7 @@ function collectObservations(memDb, queryText, pool, nowMs, loadVec, modelId, di
6878
7407
  }
6879
7408
  function collectLikeSource(db, source, cfg, queryText, pool, nowMs) {
6880
7409
  const map = /* @__PURE__ */ new Map();
6881
- const tokens = queryText.toLowerCase().replace(/[^a-z0-9\s]/g, " ").split(/\s+/).filter((t) => t.length >= 3).slice(0, 8);
7410
+ const tokens = queryText.toLowerCase().replace(/[^a-z0-9\s]/g, " ").split(/\s+/).filter((t2) => t2.length >= 3).slice(0, 8);
6882
7411
  const ageExpr = (created) => {
6883
7412
  const ms = cfg.createdIsEpoch ? Number(created) * 1e3 : Date.parse(String(created));
6884
7413
  return Number.isFinite(ms) ? ageDaysFrom(ms, nowMs) : 0;
@@ -6886,7 +7415,7 @@ function collectLikeSource(db, source, cfg, queryText, pool, nowMs) {
6886
7415
  if (tokens.length > 0) {
6887
7416
  const likeClause = cfg.searchCols.map((col) => tokens.map(() => `${col} LIKE ?`).join(" OR ")).join(" OR ");
6888
7417
  const params = [];
6889
- for (const _col of cfg.searchCols) for (const t of tokens) params.push(`%${t}%`);
7418
+ for (const _col of cfg.searchCols) for (const t2 of tokens) params.push(`%${t2}%`);
6890
7419
  try {
6891
7420
  const rows = db.prepare(
6892
7421
  `SELECT ${cfg.idCol} AS id, ${cfg.titleExpr} AS title,
@@ -7304,8 +7833,8 @@ ${d.decision}`.trim(),
7304
7833
  // src/memory-consolidate.ts
7305
7834
  init_memory_db();
7306
7835
  import { createHash as createHash6 } from "crypto";
7307
- import { existsSync as existsSync17, mkdirSync as mkdirSync14, writeFileSync as writeFileSync10 } from "fs";
7308
- import { join as join12 } from "path";
7836
+ import { existsSync as existsSync18, mkdirSync as mkdirSync15, writeFileSync as writeFileSync10 } from "fs";
7837
+ import { join as join13 } from "path";
7309
7838
  init_memory_embedder();
7310
7839
  init_memory_vector();
7311
7840
  var LEASE_KEY = "consolidate_lease";
@@ -7466,7 +7995,7 @@ ${r2.detail ?? ""}`.trim());
7466
7995
  }
7467
7996
  clusters.push(cluster);
7468
7997
  }
7469
- const candidateDir = join12(projectRoot, ".massu", "rule-candidates");
7998
+ const candidateDir = join13(projectRoot, ".massu", "rule-candidates");
7470
7999
  let promoted = 0;
7471
8000
  for (const cluster of clusters) {
7472
8001
  const occurrences = cluster.reduce((n, c) => n + (c.recurrence_count || 1), 0);
@@ -7479,13 +8008,13 @@ ${r2.detail ?? ""}`.trim());
7479
8008
  ${representative.detail}` : ""}`;
7480
8009
  const clusterKey = cluster.map((c) => c.title.toLowerCase().replace(/\s+/g, " ").trim()).sort().join("|");
7481
8010
  const promptHash = createHash6("sha256").update(clusterKey).digest("hex").slice(0, 16);
7482
- const candidatePath = join12(candidateDir, `${promptHash}.json`);
7483
- if (existsSync17(candidatePath)) continue;
8011
+ const candidatePath = join13(candidateDir, `${promptHash}.json`);
8012
+ if (existsSync18(candidatePath)) continue;
7484
8013
  if (dryRun) {
7485
8014
  promoted++;
7486
8015
  continue;
7487
8016
  }
7488
- mkdirSync14(candidateDir, { recursive: true });
8017
+ mkdirSync15(candidateDir, { recursive: true });
7489
8018
  writeFileSync10(
7490
8019
  candidatePath,
7491
8020
  JSON.stringify(
@@ -7644,12 +8173,17 @@ async function runConsolidation(db, opts = {}) {
7644
8173
  init_hook_failure_signal();
7645
8174
 
7646
8175
  // src/hooks/lib/is-direct-invocation.ts
7647
- import { pathToFileURL } from "url";
8176
+ import { fileURLToPath as fileURLToPath2, pathToFileURL } from "url";
7648
8177
  function isDirectInvocation(moduleUrl) {
7649
8178
  const entry = process.argv[1];
7650
8179
  if (!entry) return false;
7651
8180
  try {
7652
- return moduleUrl === pathToFileURL(entry).href;
8181
+ if (moduleUrl === pathToFileURL(entry).href) return true;
8182
+ } catch {
8183
+ return false;
8184
+ }
8185
+ try {
8186
+ return isSameRealPath(fileURLToPath2(moduleUrl), entry);
7653
8187
  } catch {
7654
8188
  return false;
7655
8189
  }
@@ -8084,14 +8618,14 @@ function extractFilesFromToolCall(toolName, input) {
8084
8618
  return [];
8085
8619
  }
8086
8620
  function readStdin() {
8087
- return new Promise((resolve8) => {
8621
+ return new Promise((resolve9) => {
8088
8622
  let data = "";
8089
8623
  process.stdin.setEncoding("utf-8");
8090
8624
  process.stdin.on("data", (chunk) => {
8091
8625
  data += chunk;
8092
8626
  });
8093
- process.stdin.on("end", () => resolve8(data));
8094
- setTimeout(() => resolve8(data), 5e3);
8627
+ process.stdin.on("end", () => resolve9(data));
8628
+ setTimeout(() => resolve9(data), 5e3);
8095
8629
  });
8096
8630
  }
8097
8631
  if (isDirectInvocation(import.meta.url)) {