@massu/core 2.5.4 → 2.5.5

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
  }
@@ -1649,8 +1650,8 @@ function nackLearning(db, lease, error) {
1649
1650
  function undeliveredSnapshot(db) {
1650
1651
  const count = (t) => {
1651
1652
  if (!tableExists(db, t)) return 0;
1652
- const r = db.prepare(`SELECT COUNT(*) AS n FROM ${t}`).get();
1653
- return r.n;
1653
+ const r2 = db.prepare(`SELECT COUNT(*) AS n FROM ${t}`).get();
1654
+ return r2.n;
1654
1655
  };
1655
1656
  const promotions = count("team_promotion_outbound");
1656
1657
  const revocations = count("team_revocation_outbound");
@@ -1660,9 +1661,9 @@ function undeliveredSnapshot(db) {
1660
1661
  for (const t of RULE_OUTBOUND_TABLES) {
1661
1662
  if (!tableExists(db, t)) continue;
1662
1663
  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;
1664
+ const r2 = db.prepare(`SELECT MAX(attempts) AS a, MIN(created_at) AS c FROM ${t}`).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,
@@ -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 };
@@ -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
  });
@@ -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)
@@ -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,6 +4747,7 @@ 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
4753
  return new Promise((resolve8) => setTimeout(resolve8, ms));
@@ -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) {
@@ -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,76 +6398,271 @@ function backfillQualityScores(db) {
6354
6398
  return backfilled;
6355
6399
  }
6356
6400
 
6401
+ // src/cost-reprice.ts
6402
+ init_config();
6403
+ init_memory_db();
6404
+ import { existsSync as existsSync16 } from "fs";
6405
+ import { dirname as dirname13, join as join11 } from "path";
6406
+
6357
6407
  // src/cost-tracker.ts
6358
6408
  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 }
6409
+
6410
+ // src/model-pricing.ts
6411
+ var PRICING_VERSION = 1;
6412
+ var r = (input, cacheWrite5m, cacheWrite1h, cacheRead, output) => ({ input, cacheWrite5m, cacheWrite1h, cacheRead, output });
6413
+ var US = 1.1;
6414
+ var BUILT_IN_PRICES = {
6415
+ "claude-fable-5-1": { ...r(10, 12.5, 20, 0.25, 50), usGeoMultiplier: US },
6416
+ "claude-mythos-5-1": { ...r(10, 12.5, 20, 0.25, 50), usGeoMultiplier: US },
6417
+ "claude-fable-5": { ...r(10, 12.5, 20, 1, 50), usGeoMultiplier: US },
6418
+ "claude-mythos-5": { ...r(10, 12.5, 20, 1, 50), usGeoMultiplier: US },
6419
+ "claude-opus-5-5": { ...r(4, 5, 8, 0.2, 20), fast: { input: 8, output: 40 }, usGeoMultiplier: US },
6420
+ "claude-opus-5": { ...r(5, 6.25, 10, 0.5, 25), fast: { input: 10, output: 50 }, usGeoMultiplier: US },
6421
+ "claude-opus-4-8": { ...r(5, 6.25, 10, 0.5, 25), fast: { input: 10, output: 50 }, usGeoMultiplier: US },
6422
+ "claude-opus-4-7": { ...r(5, 6.25, 10, 0.5, 25), usGeoMultiplier: US },
6423
+ "claude-opus-4-6": { ...r(5, 6.25, 10, 0.5, 25), usGeoMultiplier: US },
6424
+ "claude-opus-4-5": r(5, 6.25, 10, 0.5, 25),
6425
+ "claude-opus-4-1": r(15, 18.75, 30, 1.5, 75),
6426
+ "claude-opus-4": r(15, 18.75, 30, 1.5, 75),
6427
+ "claude-sonnet-5-5": { ...r(2, 2.5, 4, 0.1, 10), usGeoMultiplier: US },
6428
+ "claude-sonnet-5": { ...r(2, 2.5, 4, 0.2, 10), usGeoMultiplier: US },
6429
+ "claude-sonnet-4-6": { ...r(3, 3.75, 6, 0.3, 15), usGeoMultiplier: US },
6430
+ "claude-sonnet-4-5": r(3, 3.75, 6, 0.3, 15),
6431
+ "claude-sonnet-4": r(3, 3.75, 6, 0.3, 15),
6432
+ "claude-haiku-5-5": {
6433
+ ...r(0.1, 0.125, 0.2, 0.01, 0.5),
6434
+ longPrompt: { overTokens: 1e5, rates: r(0.5, 0.625, 1, 0.05, 2.5) },
6435
+ usGeoMultiplier: US
6436
+ },
6437
+ "claude-haiku-4-5": r(1, 1.25, 2, 0.1, 5)
6365
6438
  };
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);
6439
+ function normalizeModelId(model) {
6440
+ return model.trim().replace(/-\d{8}$/, "");
6441
+ }
6442
+ function resolveModelPrice(model, overrides = {}) {
6443
+ if (!model) return null;
6444
+ const id = normalizeModelId(model);
6445
+ const builtIn = BUILT_IN_PRICES[id];
6446
+ const o = overrides[model] ?? overrides[id];
6447
+ if (!o) return builtIn ?? null;
6448
+ const input = o.input_per_million;
6449
+ return {
6450
+ ...builtIn,
6451
+ input,
6452
+ output: o.output_per_million,
6453
+ cacheRead: o.cache_read_per_million ?? builtIn?.cacheRead ?? input * 0.1,
6454
+ cacheWrite5m: o.cache_write_per_million ?? builtIn?.cacheWrite5m ?? input * 1.25,
6455
+ cacheWrite1h: o.cache_write_1h_per_million ?? builtIn?.cacheWrite1h ?? input * 2
6456
+ };
6457
+ }
6458
+ function tokenCounts(u) {
6459
+ const n = (v) => typeof v === "number" && Number.isFinite(v) && v > 0 ? v : 0;
6460
+ const total = n(u.cache_creation_input_tokens);
6461
+ const has5m = typeof u.cache_creation?.ephemeral_5m_input_tokens === "number";
6462
+ const has1h = typeof u.cache_creation?.ephemeral_1h_input_tokens === "number";
6463
+ const w1h = has1h ? n(u.cache_creation?.ephemeral_1h_input_tokens) : 0;
6464
+ const w5m = has5m ? n(u.cache_creation?.ephemeral_5m_input_tokens) : total - w1h;
6400
6465
  return {
6401
- totalCost: inputCost + outputCost + cacheReadCost + cacheWriteCost,
6402
- inputCost,
6403
- outputCost,
6404
- cacheReadCost,
6405
- cacheWriteCost,
6406
- currency: getCurrency()
6466
+ input: n(u.input_tokens),
6467
+ output: n(u.output_tokens),
6468
+ cacheRead: n(u.cache_read_input_tokens),
6469
+ cacheWrite5m: Math.max(0, w5m),
6470
+ cacheWrite1h: w1h
6407
6471
  };
6408
6472
  }
6409
- function storeSessionCost(db, sessionId, usage, cost) {
6410
- const totalTokens = usage.inputTokens + usage.outputTokens + usage.cacheReadTokens + usage.cacheWriteTokens;
6473
+ function priceResponse(price, u) {
6474
+ if (!price) return null;
6475
+ const t = tokenCounts(u);
6476
+ const prompt = t.input + t.cacheRead + t.cacheWrite5m + t.cacheWrite1h;
6477
+ let rates = price.longPrompt && prompt > price.longPrompt.overTokens ? price.longPrompt.rates : price;
6478
+ if (u.speed === "fast" && price.fast) {
6479
+ const k = price.fast.input / rates.input;
6480
+ rates = {
6481
+ input: price.fast.input,
6482
+ output: price.fast.output,
6483
+ cacheRead: rates.cacheRead * k,
6484
+ cacheWrite5m: rates.cacheWrite5m * k,
6485
+ cacheWrite1h: rates.cacheWrite1h * k
6486
+ };
6487
+ }
6488
+ 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;
6490
+ return dollars / 1e6 * geo;
6491
+ }
6492
+
6493
+ // src/cost-tracker.ts
6494
+ function configPriceOverrides() {
6495
+ return getConfig().analytics?.cost?.models ?? {};
6496
+ }
6497
+ function costTranscriptEntries(entries, opts = {}) {
6498
+ const overrides = configPriceOverrides();
6499
+ const out = {
6500
+ inputTokens: 0,
6501
+ outputTokens: 0,
6502
+ cacheReadTokens: 0,
6503
+ cacheWriteTokens: 0,
6504
+ costUsd: 0,
6505
+ pricedResponses: 0,
6506
+ unpricedResponses: 0,
6507
+ unpricedModels: [],
6508
+ model: "",
6509
+ lastMessageId: null
6510
+ };
6511
+ const seen = /* @__PURE__ */ new Set();
6512
+ if (opts.skipMessageId) seen.add(opts.skipMessageId);
6513
+ const unpriced = /* @__PURE__ */ new Set();
6514
+ for (const entry of entries) {
6515
+ const msg = entry.message;
6516
+ if (entry.type !== "assistant" || !msg?.usage) continue;
6517
+ if (msg.id) {
6518
+ if (seen.has(msg.id)) continue;
6519
+ seen.add(msg.id);
6520
+ out.lastMessageId = msg.id;
6521
+ }
6522
+ 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;
6529
+ const dollars = priceResponse(resolveModelPrice(msg.model, overrides), usage);
6530
+ if (dollars === null) {
6531
+ out.unpricedResponses++;
6532
+ unpriced.add(msg.model ?? "unknown");
6533
+ } else {
6534
+ out.costUsd += dollars;
6535
+ out.pricedResponses++;
6536
+ }
6537
+ if (msg.model) out.model = msg.model;
6538
+ }
6539
+ out.unpricedModels = [...unpriced].sort();
6540
+ return out;
6541
+ }
6542
+ function costStatusOf(priced, unpriced) {
6543
+ if (unpriced === 0) return "priced";
6544
+ return priced === 0 ? "unknown" : "partial";
6545
+ }
6546
+ function storeSessionCost(db, sessionId, slice, mode = "replace") {
6547
+ const prev = mode === "add" ? db.prepare(
6548
+ `SELECT input_tokens, output_tokens, cache_read_tokens, cache_write_tokens, estimated_cost_usd,
6549
+ priced_responses, unpriced_responses, unpriced_models, model
6550
+ FROM session_costs WHERE session_id = ?`
6551
+ ).get(sessionId) : void 0;
6552
+ const num = (k) => Number(prev?.[k] ?? 0) || 0;
6553
+ const input = num("input_tokens") + slice.inputTokens;
6554
+ const output = num("output_tokens") + slice.outputTokens;
6555
+ const cacheRead = num("cache_read_tokens") + slice.cacheReadTokens;
6556
+ const cacheWrite = num("cache_write_tokens") + slice.cacheWriteTokens;
6557
+ const priced = num("priced_responses") + slice.pricedResponses;
6558
+ const unpricedCount = num("unpriced_responses") + slice.unpricedResponses;
6559
+ const unpricedModels = [.../* @__PURE__ */ new Set([
6560
+ ...String(prev?.unpriced_models ?? "").split(",").filter(Boolean),
6561
+ ...slice.unpricedModels
6562
+ ])].sort().join(",");
6563
+ const model = slice.model || String(prev?.model ?? "") || null;
6411
6564
  db.prepare(`
6412
6565
  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 (?, ?, ?, ?, ?, ?, ?, ?)
6566
+ (session_id, model, input_tokens, output_tokens, cache_read_tokens, cache_write_tokens,
6567
+ total_tokens, estimated_cost_usd, priced_responses, unpriced_responses, unpriced_models,
6568
+ cost_status, pricing_version)
6569
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
6570
+ ON CONFLICT(session_id) DO UPDATE SET
6571
+ model = excluded.model, input_tokens = excluded.input_tokens,
6572
+ output_tokens = excluded.output_tokens, cache_read_tokens = excluded.cache_read_tokens,
6573
+ cache_write_tokens = excluded.cache_write_tokens, total_tokens = excluded.total_tokens,
6574
+ estimated_cost_usd = excluded.estimated_cost_usd,
6575
+ priced_responses = excluded.priced_responses, unpriced_responses = excluded.unpriced_responses,
6576
+ unpriced_models = excluded.unpriced_models, cost_status = excluded.cost_status,
6577
+ pricing_version = excluded.pricing_version
6416
6578
  `).run(
6417
6579
  sessionId,
6418
- usage.model,
6419
- usage.inputTokens,
6420
- usage.outputTokens,
6421
- usage.cacheReadTokens,
6422
- usage.cacheWriteTokens,
6423
- totalTokens,
6424
- cost.totalCost
6580
+ model,
6581
+ input,
6582
+ output,
6583
+ cacheRead,
6584
+ cacheWrite,
6585
+ input + output + cacheRead + cacheWrite,
6586
+ num("estimated_cost_usd") + slice.costUsd,
6587
+ priced,
6588
+ unpricedCount,
6589
+ unpricedModels,
6590
+ costStatusOf(priced, unpricedCount),
6591
+ PRICING_VERSION
6425
6592
  );
6426
6593
  }
6594
+ function markSessionCostUnknown(db, sessionId) {
6595
+ db.prepare(`
6596
+ INSERT INTO session_costs (session_id, cost_status, pricing_version) VALUES (?, 'unknown', ?)
6597
+ ON CONFLICT(session_id) DO UPDATE SET
6598
+ input_tokens = 0, output_tokens = 0, cache_read_tokens = 0, cache_write_tokens = 0,
6599
+ total_tokens = 0, estimated_cost_usd = 0, priced_responses = 0, unpriced_responses = 0,
6600
+ unpriced_models = '', cost_status = 'unknown', pricing_version = excluded.pricing_version
6601
+ `).run(sessionId, PRICING_VERSION);
6602
+ }
6603
+
6604
+ // src/cost-reprice.ts
6605
+ var cursorKey = (sid) => `cost_processed_line:${sid}`;
6606
+ var lastIdKey = (sid) => `cost_last_message:${sid}`;
6607
+ var sessionLeaseKey = (sid) => `session_end_lease:${sid}`;
6608
+ async function updateSessionCost(db, sessionId, transcriptPath) {
6609
+ const row = db.prepare("SELECT pricing_version FROM session_costs WHERE session_id = ?").get(sessionId);
6610
+ const cursor = Number(getMemoryMeta(db, cursorKey(sessionId)) ?? "0") || 0;
6611
+ const incremental = row?.pricing_version === PRICING_VERSION && cursor > 0;
6612
+ const from = incremental ? cursor : 0;
6613
+ const { entries, totalLines } = await parseTranscriptFrom(transcriptPath, from);
6614
+ const slice = costTranscriptEntries(entries, {
6615
+ skipMessageId: incremental ? getMemoryMeta(db, lastIdKey(sessionId)) : null
6616
+ });
6617
+ storeSessionCost(db, sessionId, slice, incremental ? "add" : "replace");
6618
+ setMemoryMeta(db, cursorKey(sessionId), String(totalLines));
6619
+ if (slice.lastMessageId) setMemoryMeta(db, lastIdKey(sessionId), slice.lastMessageId);
6620
+ }
6621
+ function defaultTranscriptFor(sessionId) {
6622
+ if (!/^[A-Za-z0-9_-]+$/.test(sessionId)) return null;
6623
+ const path = join11(dirname13(getResolvedPaths().memoryDir), `${sessionId}.jsonl`);
6624
+ return existsSync16(path) ? path : null;
6625
+ }
6626
+ async function repriceStoredSessions(db, opts = {}) {
6627
+ const deadlineAt = opts.deadlineAt ?? Infinity;
6628
+ const find = opts.findTranscript ?? defaultTranscriptFor;
6629
+ const out = { repriced: 0, markedUnknown: 0, busy: 0, failed: 0, remaining: 0 };
6630
+ const stale = db.prepare(
6631
+ `SELECT session_id FROM session_costs WHERE pricing_version < ? ORDER BY id LIMIT ?`
6632
+ );
6633
+ const ttlSec = 120;
6634
+ const tried = /* @__PURE__ */ new Set();
6635
+ outer: while (Date.now() < deadlineAt) {
6636
+ const ids = stale.all(PRICING_VERSION, opts.batch ?? 50).map((r2) => r2.session_id).filter((sid) => !tried.has(sid));
6637
+ if (ids.length === 0) break;
6638
+ for (const sid of ids) {
6639
+ if (Date.now() >= deadlineAt) break outer;
6640
+ tried.add(sid);
6641
+ if (!tryAcquireMetaLease(db, sessionLeaseKey(sid), ttlSec)) {
6642
+ out.busy++;
6643
+ continue;
6644
+ }
6645
+ try {
6646
+ const path = find(sid);
6647
+ if (path) {
6648
+ await updateSessionCost(db, sid, path);
6649
+ out.repriced++;
6650
+ } else {
6651
+ markSessionCostUnknown(db, sid);
6652
+ out.markedUnknown++;
6653
+ }
6654
+ } catch (err) {
6655
+ out.failed++;
6656
+ process.stderr.write(`[massu] cost reprice: ${sid}: ${err instanceof Error ? err.message : String(err)}
6657
+ `);
6658
+ } finally {
6659
+ releaseMetaLease(db, sessionLeaseKey(sid));
6660
+ }
6661
+ }
6662
+ }
6663
+ out.remaining = db.prepare("SELECT COUNT(*) n FROM session_costs WHERE pricing_version < ?").get(PRICING_VERSION).n;
6664
+ return out;
6665
+ }
6427
6666
 
6428
6667
  // src/prompt-analyzer.ts
6429
6668
  init_config();
@@ -6589,14 +6828,14 @@ function collectObservations(memDb, queryText, pool, nowMs, loadVec, modelId, di
6589
6828
  WHERE observations_fts MATCH ?${bm25Temporal ? ` AND ${bm25Temporal}` : ""}
6590
6829
  ORDER BY rank LIMIT ?`
6591
6830
  ).all(sanitizeFts5QueryOr(queryText), pool);
6592
- rows.forEach((r, i) => {
6593
- map.set(r.id, {
6594
- id: r.id,
6831
+ rows.forEach((r2, i) => {
6832
+ map.set(r2.id, {
6833
+ id: r2.id,
6595
6834
  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),
6835
+ title: r2.title,
6836
+ snippet: snippetOf(r2.detail ?? r2.title),
6837
+ importance: r2.importance,
6838
+ ageDays: ageDaysFrom(r2.created_at_epoch * 1e3, nowMs),
6600
6839
  bm25Order: i,
6601
6840
  vecs: []
6602
6841
  });
@@ -6607,15 +6846,15 @@ function collectObservations(memDb, queryText, pool, nowMs, loadVec, modelId, di
6607
6846
  `SELECT id, title, detail, importance, created_at_epoch
6608
6847
  FROM observations${plainTemporal ? ` WHERE ${plainTemporal}` : ""} ORDER BY created_at_epoch DESC LIMIT ?`
6609
6848
  ).all(pool);
6610
- for (const r of recent) {
6611
- if (!map.has(r.id)) {
6612
- map.set(r.id, {
6613
- id: r.id,
6849
+ for (const r2 of recent) {
6850
+ if (!map.has(r2.id)) {
6851
+ map.set(r2.id, {
6852
+ id: r2.id,
6614
6853
  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),
6854
+ title: r2.title,
6855
+ snippet: snippetOf(r2.detail ?? r2.title),
6856
+ importance: r2.importance,
6857
+ ageDays: ageDaysFrom(r2.created_at_epoch * 1e3, nowMs),
6619
6858
  bm25Order: null,
6620
6859
  vecs: []
6621
6860
  });
@@ -6655,14 +6894,14 @@ function collectLikeSource(db, source, cfg, queryText, pool, nowMs) {
6655
6894
  FROM ${cfg.table} WHERE (${likeClause})${cfg.temporalClause ? ` AND ${cfg.temporalClause}` : ""}
6656
6895
  ORDER BY ${cfg.createdCol} DESC LIMIT ?`
6657
6896
  ).all(...params, pool);
6658
- rows.forEach((r, i) => {
6659
- map.set(r.id, {
6660
- id: r.id,
6897
+ rows.forEach((r2, i) => {
6898
+ map.set(r2.id, {
6899
+ id: r2.id,
6661
6900
  source,
6662
- title: r.title,
6663
- snippet: snippetOf(r.snippet ?? r.title),
6901
+ title: r2.title,
6902
+ snippet: snippetOf(r2.snippet ?? r2.title),
6664
6903
  importance: 3,
6665
- ageDays: ageExpr(r.created),
6904
+ ageDays: ageExpr(r2.created),
6666
6905
  bm25Order: i,
6667
6906
  vecs: []
6668
6907
  });
@@ -6675,15 +6914,15 @@ function collectLikeSource(db, source, cfg, queryText, pool, nowMs) {
6675
6914
  ${cfg.snippetCol} AS snippet, ${cfg.createdCol} AS created
6676
6915
  FROM ${cfg.table}${cfg.temporalClause ? ` WHERE ${cfg.temporalClause}` : ""} ORDER BY ${cfg.createdCol} DESC LIMIT ?`
6677
6916
  ).all(pool);
6678
- for (const r of recent) {
6679
- if (!map.has(r.id)) {
6680
- map.set(r.id, {
6681
- id: r.id,
6917
+ for (const r2 of recent) {
6918
+ if (!map.has(r2.id)) {
6919
+ map.set(r2.id, {
6920
+ id: r2.id,
6682
6921
  source,
6683
- title: r.title,
6684
- snippet: snippetOf(r.snippet ?? r.title),
6922
+ title: r2.title,
6923
+ snippet: snippetOf(r2.snippet ?? r2.title),
6685
6924
  importance: 3,
6686
- ageDays: ageExpr(r.created),
6925
+ ageDays: ageExpr(r2.created),
6687
6926
  bm25Order: null,
6688
6927
  vecs: []
6689
6928
  });
@@ -6703,14 +6942,14 @@ function collectKnowledgeChunks(knowledgeDb, queryText, pool, nowMs, loadVec, mo
6703
6942
  WHERE knowledge_fts MATCH ?
6704
6943
  ORDER BY rank LIMIT ?`
6705
6944
  ).all(sanitizeFts5QueryOr(queryText), pool);
6706
- rows.forEach((r, i) => {
6707
- map.set(r.id, {
6708
- id: r.id,
6945
+ rows.forEach((r2, i) => {
6946
+ map.set(r2.id, {
6947
+ id: r2.id,
6709
6948
  source: "knowledge_chunk",
6710
- title: r.heading || snippetOf(r.content, 60),
6711
- snippet: snippetOf(r.content),
6949
+ title: r2.heading || snippetOf(r2.content, 60),
6950
+ snippet: snippetOf(r2.content),
6712
6951
  importance: 3,
6713
- ageDays: ageDaysFrom((r.epoch ?? 0) * 1e3, nowMs),
6952
+ ageDays: ageDaysFrom((r2.epoch ?? 0) * 1e3, nowMs),
6714
6953
  bm25Order: i,
6715
6954
  vecs: []
6716
6955
  });
@@ -6742,9 +6981,9 @@ function loadEmbeddings(db, table, idCol, ids, modelId, dim, assign) {
6742
6981
  WHERE ${idCol} IN (${placeholders}) AND model_id = ? AND dim = ?
6743
6982
  LIMIT ?`
6744
6983
  ).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);
6984
+ for (const r2 of rows) {
6985
+ const v = blobToFloat32(r2.vec);
6986
+ if (v && v.length === dim) assign(r2.id, v);
6748
6987
  }
6749
6988
  } catch {
6750
6989
  }
@@ -6854,17 +7093,17 @@ function hybridSearch(memDb, knowledgeDb, opts) {
6854
7093
  ageDays: c.ageDays
6855
7094
  };
6856
7095
  });
6857
- const top = ranked.filter((r) => r.score >= minScore).sort((a, b) => b.score - a.score || a.ageDays - b.ageDays).slice(0, limit);
7096
+ const top = ranked.filter((r2) => r2.score >= minScore).sort((a, b) => b.score - a.score || a.ageDays - b.ageDays).slice(0, limit);
6858
7097
  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;
7098
+ for (const r2 of top) {
7099
+ const table = r2.source === "observation" ? "observations" : r2.source === "architecture_decision" ? "architecture_decisions" : null;
6861
7100
  if (!table) continue;
6862
7101
  try {
6863
- const row = memDb.prepare(`SELECT valid_to, superseded_by, expired_at_epoch FROM ${table} WHERE id = ?`).get(r.id);
7102
+ const row = memDb.prepare(`SELECT valid_to, superseded_by, expired_at_epoch FROM ${table} WHERE id = ?`).get(r2.id);
6864
7103
  if (row && row.expired_at_epoch != null) {
6865
7104
  const when = row.valid_to ? row.valid_to.slice(0, 10) : "an earlier date";
6866
7105
  const by = row.superseded_by != null ? ` by #${row.superseded_by}` : "";
6867
- r.snippet = `(superseded on ${when}${by}) ${r.snippet}`;
7106
+ r2.snippet = `(superseded on ${when}${by}) ${r2.snippet}`;
6868
7107
  }
6869
7108
  } catch {
6870
7109
  }
@@ -6986,11 +7225,11 @@ async function supersedeIfContradicted(memDb, knowledgeDb, args) {
6986
7225
  candidatePool: 20
6987
7226
  });
6988
7227
  const scored = [];
6989
- for (const r of results) {
6990
- if (r.id === args.newId) continue;
7228
+ for (const r2 of results) {
7229
+ if (r2.id === args.newId) continue;
6991
7230
  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) });
7231
+ const vec = await candidateVec(memDb, args.source, r2.id);
7232
+ if (vec) scored.push({ id: r2.id, source: args.source, cosine: cosineSim(queryVec, vec) });
6994
7233
  }
6995
7234
  let verdict = null;
6996
7235
  if (cfg.judgeEndpoint && Date.now() - start < cfg.budgetMs) {
@@ -7065,8 +7304,8 @@ ${d.decision}`.trim(),
7065
7304
  // src/memory-consolidate.ts
7066
7305
  init_memory_db();
7067
7306
  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";
7307
+ import { existsSync as existsSync17, mkdirSync as mkdirSync14, writeFileSync as writeFileSync10 } from "fs";
7308
+ import { join as join12 } from "path";
7070
7309
  init_memory_embedder();
7071
7310
  init_memory_vector();
7072
7311
  var LEASE_KEY = "consolidate_lease";
@@ -7089,17 +7328,11 @@ function emptyResult() {
7089
7328
  };
7090
7329
  }
7091
7330
  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;
7331
+ return tryAcquireMetaLease(db, LEASE_KEY, ttlSec, now);
7099
7332
  }
7100
7333
  function releaseLease(db) {
7101
7334
  try {
7102
- setMemoryMeta(db, LEASE_KEY, "");
7335
+ releaseMetaLease(db, LEASE_KEY);
7103
7336
  } catch {
7104
7337
  }
7105
7338
  }
@@ -7118,19 +7351,19 @@ async function stageDedupe(db, cfg, deadline, now) {
7118
7351
  return 0;
7119
7352
  }
7120
7353
  let deduped = 0;
7121
- for (const r of rows) {
7354
+ for (const r2 of rows) {
7122
7355
  if (Date.now() > deadline) break;
7123
- const live = db.prepare(`SELECT expired_at FROM observations WHERE id = ?`).get(r.id);
7356
+ const live = db.prepare(`SELECT expired_at FROM observations WHERE id = ?`).get(r2.id);
7124
7357
  if (!live || live.expired_at != null) continue;
7125
7358
  const res = await supersedeIfContradicted(db, null, {
7126
- text: `${r.title}
7127
- ${r.detail ?? ""}`.trim(),
7359
+ text: `${r2.title}
7360
+ ${r2.detail ?? ""}`.trim(),
7128
7361
  source: "observation",
7129
- newId: r.id,
7362
+ newId: r2.id,
7130
7363
  nowEpochSec: now
7131
7364
  });
7132
7365
  if (res.superseded != null) deduped++;
7133
- setMemoryMeta(db, DEDUPE_CURSOR, String(r.id));
7366
+ setMemoryMeta(db, DEDUPE_CURSOR, String(r2.id));
7134
7367
  }
7135
7368
  return deduped;
7136
7369
  }
@@ -7208,11 +7441,11 @@ async function stagePromote(db, cfg, projectRoot, deadline, now, dryRun) {
7208
7441
  ).all(MAX_ROWS_PER_STAGE);
7209
7442
  if (rows.length === 0) return { promoted: 0, refusedByTier: 0, embedderUnavailable: false };
7210
7443
  const vecs = /* @__PURE__ */ new Map();
7211
- for (const r of rows) {
7444
+ for (const r2 of rows) {
7212
7445
  if (Date.now() > deadline) break;
7213
- const v = await embed(`${r.title}
7214
- ${r.detail ?? ""}`.trim());
7215
- if (v) vecs.set(r.id, v);
7446
+ const v = await embed(`${r2.title}
7447
+ ${r2.detail ?? ""}`.trim());
7448
+ if (v) vecs.set(r2.id, v);
7216
7449
  }
7217
7450
  if (vecs.size === 0) {
7218
7451
  return { promoted: 0, refusedByTier: 0, embedderUnavailable: true };
@@ -7220,20 +7453,20 @@ ${r.detail ?? ""}`.trim());
7220
7453
  const CLUSTER_THRESHOLD = 0.8;
7221
7454
  const used = /* @__PURE__ */ new Set();
7222
7455
  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);
7456
+ for (const r2 of rows) {
7457
+ if (used.has(r2.id) || !vecs.has(r2.id)) continue;
7458
+ const cluster = [r2];
7459
+ used.add(r2.id);
7227
7460
  for (const other of rows) {
7228
7461
  if (used.has(other.id) || !vecs.has(other.id)) continue;
7229
- if (cosineSim(vecs.get(r.id), vecs.get(other.id)) >= CLUSTER_THRESHOLD) {
7462
+ if (cosineSim(vecs.get(r2.id), vecs.get(other.id)) >= CLUSTER_THRESHOLD) {
7230
7463
  cluster.push(other);
7231
7464
  used.add(other.id);
7232
7465
  }
7233
7466
  }
7234
7467
  clusters.push(cluster);
7235
7468
  }
7236
- const candidateDir = join11(projectRoot, ".massu", "rule-candidates");
7469
+ const candidateDir = join12(projectRoot, ".massu", "rule-candidates");
7237
7470
  let promoted = 0;
7238
7471
  for (const cluster of clusters) {
7239
7472
  const occurrences = cluster.reduce((n, c) => n + (c.recurrence_count || 1), 0);
@@ -7246,8 +7479,8 @@ ${r.detail ?? ""}`.trim());
7246
7479
  ${representative.detail}` : ""}`;
7247
7480
  const clusterKey = cluster.map((c) => c.title.toLowerCase().replace(/\s+/g, " ").trim()).sort().join("|");
7248
7481
  const promptHash = createHash6("sha256").update(clusterKey).digest("hex").slice(0, 16);
7249
- const candidatePath = join11(candidateDir, `${promptHash}.json`);
7250
- if (existsSync16(candidatePath)) continue;
7482
+ const candidatePath = join12(candidateDir, `${promptHash}.json`);
7483
+ if (existsSync17(candidatePath)) continue;
7251
7484
  if (dryRun) {
7252
7485
  promoted++;
7253
7486
  continue;
@@ -7325,14 +7558,14 @@ function stageReweight(db, cfg, now) {
7325
7558
  ON CONFLICT(source, record_id) DO UPDATE SET last_reweight_epoch = excluded.last_reweight_epoch`
7326
7559
  );
7327
7560
  const tx = db.transaction(() => {
7328
- for (const r of promote) {
7329
- bump.run(r.id);
7330
- mark.run(r.id, now);
7561
+ for (const r2 of promote) {
7562
+ bump.run(r2.id);
7563
+ mark.run(r2.id, now);
7331
7564
  changed++;
7332
7565
  }
7333
- for (const r of demote) {
7334
- drop2.run(r.id);
7335
- mark.run(r.id, now);
7566
+ for (const r2 of demote) {
7567
+ drop2.run(r2.id);
7568
+ mark.run(r2.id, now);
7336
7569
  changed++;
7337
7570
  }
7338
7571
  });
@@ -7423,105 +7656,184 @@ function isDirectInvocation(moduleUrl) {
7423
7656
  }
7424
7657
 
7425
7658
  // src/hooks/session-end.ts
7659
+ var SESSION_END_BUDGET_ENV = "MASSU_SESSION_END_BUDGET_MS";
7660
+ var DEFAULT_SESSION_END_BUDGET_MS = 2e4;
7661
+ var SESSION_END_WATCHDOG_GRACE_MS = 15e3;
7662
+ var MAX_COALESCED_RUNS = 3;
7663
+ var MIN_PHASE_BUDGET_MS = 500;
7664
+ function resolveSessionEndBudgetMs() {
7665
+ const raw = process.env[SESSION_END_BUDGET_ENV];
7666
+ if (raw === void 0 || raw === "") return DEFAULT_SESSION_END_BUDGET_MS;
7667
+ const n = Number(raw);
7668
+ return Number.isFinite(n) && n > 0 ? Math.floor(n) : DEFAULT_SESSION_END_BUDGET_MS;
7669
+ }
7670
+ var leaseKey = sessionLeaseKey;
7671
+ var rerunKey = (sessionId) => `session_end_rerun:${sessionId}`;
7672
+ async function runCoalesced(db, sessionId, budgetMs, work) {
7673
+ const ttlSec = Math.ceil((budgetMs + SESSION_END_WATCHDOG_GRACE_MS) / 1e3) + 1;
7674
+ let runs = 0;
7675
+ while (runs < MAX_COALESCED_RUNS) {
7676
+ if (!tryAcquireMetaLease(db, leaseKey(sessionId), ttlSec)) {
7677
+ setMemoryMeta(db, rerunKey(sessionId), "1");
7678
+ if (!tryAcquireMetaLease(db, leaseKey(sessionId), ttlSec)) {
7679
+ return runs === 0 ? "handed-off" : "ran";
7680
+ }
7681
+ }
7682
+ runs++;
7683
+ try {
7684
+ setMemoryMeta(db, rerunKey(sessionId), "");
7685
+ await work(Date.now() + budgetMs);
7686
+ } finally {
7687
+ releaseMetaLease(db, leaseKey(sessionId));
7688
+ }
7689
+ if (getMemoryMeta(db, rerunKey(sessionId)) !== "1") return "ran";
7690
+ }
7691
+ return "ran";
7692
+ }
7693
+ function phaseBudget(deadlineAt, phase, cap) {
7694
+ const left = deadlineAt - Date.now();
7695
+ if (left < MIN_PHASE_BUDGET_MS) {
7696
+ process.stderr.write(`[massu] session-end: skipped ${phase} \u2014 ${Math.max(0, left)}ms of budget left
7697
+ `);
7698
+ return null;
7699
+ }
7700
+ return Math.min(cap, left);
7701
+ }
7426
7702
  async function main() {
7427
7703
  try {
7428
7704
  const input = await readStdin();
7429
7705
  const hookInput = JSON.parse(input);
7430
- const { session_id, cwd } = hookInput;
7706
+ const budgetMs = resolveSessionEndBudgetMs();
7707
+ let watchdog;
7431
7708
  const db = getMemoryDb();
7432
7709
  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) {
7710
+ await runCoalesced(db, hookInput.session_id, budgetMs, async (deadlineAt) => {
7711
+ clearTimeout(watchdog);
7712
+ watchdog = setTimeout(() => {
7713
+ recordHookFailure(
7714
+ "session-end:watchdog",
7715
+ new Error(`a run exceeded its ${budgetMs}ms budget by ${SESSION_END_WATCHDOG_GRACE_MS}ms`)
7716
+ );
7717
+ process.exit(0);
7718
+ }, budgetMs + SESSION_END_WATCHDOG_GRACE_MS);
7719
+ watchdog.unref();
7720
+ await runSessionEnd(db, hookInput, deadlineAt);
7721
+ });
7722
+ } finally {
7723
+ clearTimeout(watchdog);
7724
+ db.close();
7725
+ }
7726
+ } catch (err) {
7727
+ recordHookFailure("session-end", err);
7728
+ }
7729
+ process.exit(0);
7730
+ }
7731
+ async function runSessionEnd(db, hookInput, deadlineAt) {
7732
+ const { session_id, cwd } = hookInput;
7733
+ {
7734
+ createSession(db, session_id);
7735
+ const observations = db.prepare(
7736
+ "SELECT * FROM observations WHERE session_id = ? ORDER BY created_at_epoch ASC LIMIT 10000"
7737
+ ).all(session_id);
7738
+ const prompts = db.prepare(
7739
+ "SELECT prompt_text FROM user_prompts WHERE session_id = ? ORDER BY prompt_number ASC LIMIT 10000"
7740
+ ).all(session_id);
7741
+ const summary = buildSummaryFromObservations(observations, prompts);
7742
+ addSummary(db, session_id, summary);
7743
+ try {
7744
+ await captureConversationData(db, session_id, hookInput.transcript_path);
7745
+ } catch (_captureErr) {
7746
+ }
7747
+ try {
7748
+ const { score, breakdown } = calculateQualityScore(db, session_id);
7749
+ if (score !== 50) {
7750
+ storeQualityScore(db, session_id, score, breakdown);
7472
7751
  }
7473
- try {
7474
- const consolidationCfg = resolveConsolidationConfig();
7475
- if (consolidationCfg.enabled && consolidationCfg.sessionSweepEnabled) {
7752
+ backfillQualityScores(db);
7753
+ } catch (_qualityErr) {
7754
+ }
7755
+ try {
7756
+ await updateSessionCost(db, session_id, hookInput.transcript_path);
7757
+ } catch (costErr) {
7758
+ recordHookFailure("session-end:cost", costErr);
7759
+ }
7760
+ try {
7761
+ analyzeSessionPrompts(db, session_id);
7762
+ } catch (_promptErr) {
7763
+ }
7764
+ try {
7765
+ const budget = phaseBudget(deadlineAt, "embed sweep", 3e3);
7766
+ if (budget !== null) await embedMissingObservations(db, { budgetMs: budget });
7767
+ } catch (_embedErr) {
7768
+ }
7769
+ try {
7770
+ const budget = phaseBudget(deadlineAt, "supersede sweep", 4e3);
7771
+ if (budget !== null) await runSessionSupersedeSweep(db, session_id, { budgetMs: budget });
7772
+ } catch (_supersedeErr) {
7773
+ }
7774
+ try {
7775
+ const consolidationCfg = resolveConsolidationConfig();
7776
+ if (consolidationCfg.enabled && consolidationCfg.sessionSweepEnabled) {
7777
+ const budget = phaseBudget(deadlineAt, "consolidation", consolidationCfg.budgetMs);
7778
+ if (budget !== null) {
7476
7779
  await runConsolidation(db, {
7477
7780
  config: consolidationCfg,
7478
- budgetMs: consolidationCfg.budgetMs,
7781
+ budgetMs: budget,
7479
7782
  projectRoot: cwd
7480
7783
  });
7481
7784
  }
7482
- } catch (_consolidateErr) {
7483
7785
  }
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
- }
7786
+ } catch (_consolidateErr) {
7787
+ }
7788
+ try {
7789
+ const budget = phaseBudget(deadlineAt, "cost reprice", 3e3);
7790
+ if (budget !== null) await repriceStoredSessions(db, { deadlineAt: Date.now() + budget });
7791
+ } catch (repriceErr) {
7792
+ recordHookFailure("session-end:cost-reprice", repriceErr);
7793
+ }
7794
+ endSession(db, session_id, "completed");
7795
+ archiveAndRegenerate(db, session_id);
7796
+ let openLease = null;
7797
+ try {
7798
+ const drained = await drainSyncQueue(db, deadlineAt);
7799
+ if (drained.deferred > 0) {
7800
+ process.stderr.write(`[massu] session-end: deferred ${drained.deferred} queued sync item(s) to the next run
7801
+ `);
7802
+ }
7803
+ const cloudCfg = getConfig().cloud;
7804
+ const willTransmit = !!cloudCfg?.enabled && !!cloudCfg?.apiKey && !!cloudCfg?.endpoint && cloudCfg?.sync?.memory !== false;
7805
+ const lease = willTransmit ? leaseLearning(db) : null;
7806
+ openLease = lease;
7807
+ const syncPayload = buildSyncPayload(db, session_id, observations, summary, lease);
7808
+ const result = await syncToCloud(db, syncPayload, { deadlineAt });
7809
+ if (lease) {
7810
+ if (result.transmitted === true) {
7811
+ ackLearning(db, lease);
7812
+ } else {
7813
+ nackLearning(db, lease, result.error ?? "not transmitted (sync disabled or unreachable)");
7501
7814
  }
7502
- openLease = null;
7503
- try {
7815
+ }
7816
+ openLease = null;
7817
+ try {
7818
+ if (phaseBudget(deadlineAt, "team promotion pull", Infinity) !== null) {
7504
7819
  await pullTeamPromotions(db);
7505
- } catch (_pullErr) {
7506
7820
  }
7507
- try {
7821
+ } catch (_pullErr) {
7822
+ }
7823
+ try {
7824
+ if (phaseBudget(deadlineAt, "shared-memory exchange", Infinity) !== null) {
7508
7825
  const shareTransport = new LocalFsTransport();
7509
7826
  await exportSharedMemories(db, shareTransport);
7510
7827
  await importSharedMemories(db, shareTransport);
7511
- } catch (_shareErr) {
7512
7828
  }
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);
7829
+ } catch (_shareErr) {
7517
7830
  }
7518
- } finally {
7519
- db.close();
7831
+ } catch (syncErr) {
7832
+ const msg = syncErr instanceof Error ? syncErr.message : String(syncErr);
7833
+ if (openLease) nackLearning(db, openLease, msg);
7834
+ recordHookFailure("session-end:cloud-sync", syncErr);
7520
7835
  }
7521
- } catch (err) {
7522
- recordHookFailure("session-end", err);
7523
7836
  }
7524
- process.exit(0);
7525
7837
  }
7526
7838
  function buildSyncPayload(db, sessionId, observations, summary, lease) {
7527
7839
  const promotions = lease?.promotions ?? [];
@@ -7785,3 +8097,11 @@ function readStdin() {
7785
8097
  if (isDirectInvocation(import.meta.url)) {
7786
8098
  main();
7787
8099
  }
8100
+ export {
8101
+ DEFAULT_SESSION_END_BUDGET_MS,
8102
+ MAX_COALESCED_RUNS,
8103
+ SESSION_END_BUDGET_ENV,
8104
+ SESSION_END_WATCHDOG_GRACE_MS,
8105
+ resolveSessionEndBudgetMs,
8106
+ runCoalesced
8107
+ };