@massu/core 1.15.7 → 1.16.0

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.
@@ -956,6 +956,106 @@ var init_db_backup = __esm({
956
956
  }
957
957
  });
958
958
 
959
+ // src/rule-delivery.ts
960
+ function tableExists(db, table) {
961
+ const row = db.prepare("SELECT 1 AS ok FROM sqlite_master WHERE type='table' AND name=?").get(table);
962
+ return row !== void 0;
963
+ }
964
+ function columns(db, table) {
965
+ const rows = db.prepare(`PRAGMA table_info(${table})`).all();
966
+ return new Set(rows.map((r) => r.name));
967
+ }
968
+ function migrateRuleDelivery(db) {
969
+ const ADDITIONS = [
970
+ { name: "attempts", decl: "INTEGER NOT NULL DEFAULT 0" },
971
+ { name: "lease_token", decl: "TEXT" },
972
+ { name: "leased_at_ms", decl: "INTEGER" },
973
+ { name: "last_error", decl: "TEXT" }
974
+ ];
975
+ for (const table of ALL_OUTBOUND_TABLES) {
976
+ if (!tableExists(db, table)) continue;
977
+ const cols = columns(db, table);
978
+ for (const add of ADDITIONS) {
979
+ if (cols.has(add.name)) continue;
980
+ db.exec(`ALTER TABLE ${table} ADD COLUMN ${add.name} ${add.decl}`);
981
+ }
982
+ }
983
+ }
984
+ function capTelemetry(db, cap = TELEMETRY_OUTBOX_CAP) {
985
+ if (!tableExists(db, TELEMETRY_OUTBOUND_TABLE)) return 0;
986
+ const before = db.prepare(`SELECT COUNT(*) AS n FROM ${TELEMETRY_OUTBOUND_TABLE}`).get();
987
+ if (before.n <= cap) return 0;
988
+ const info = db.prepare(
989
+ `DELETE FROM ${TELEMETRY_OUTBOUND_TABLE}
990
+ WHERE id NOT IN (SELECT id FROM ${TELEMETRY_OUTBOUND_TABLE} ORDER BY id DESC LIMIT ?)`
991
+ ).run(cap);
992
+ const dropped = info.changes;
993
+ if (dropped > 0) {
994
+ process.stderr.write(
995
+ `[massu] NOTE: dropped ${dropped} oldest promotion-funnel telemetry row(s) at the ${cap}-row cap. No learned rules were affected (rules are never dropped).
996
+ `
997
+ );
998
+ recordAnalytics(db, EVENT_TELEMETRY_CAPPED, { dropped, cap });
999
+ }
1000
+ return dropped;
1001
+ }
1002
+ function recordAnalytics(db, eventType, data) {
1003
+ try {
1004
+ db.prepare(
1005
+ `INSERT INTO analytics_events (event_type, event_data, created_at)
1006
+ VALUES (?, ?, datetime('now'))`
1007
+ ).run(eventType, JSON.stringify(data));
1008
+ } catch (err) {
1009
+ process.stderr.write(
1010
+ `[massu] WARNING: could not record '${eventType}' telemetry: ${err instanceof Error ? err.message : String(err)}
1011
+ `
1012
+ );
1013
+ }
1014
+ }
1015
+ var RULE_OUTBOUND_TABLES, TELEMETRY_OUTBOUND_TABLE, ALL_OUTBOUND_TABLES, TELEMETRY_OUTBOX_CAP, LEASE_TTL_MS, EVENT_TELEMETRY_CAPPED;
1016
+ var init_rule_delivery = __esm({
1017
+ "src/rule-delivery.ts"() {
1018
+ "use strict";
1019
+ RULE_OUTBOUND_TABLES = [
1020
+ "team_promotion_outbound",
1021
+ "team_revocation_outbound"
1022
+ ];
1023
+ TELEMETRY_OUTBOUND_TABLE = "rule_promotion_events_outbound";
1024
+ ALL_OUTBOUND_TABLES = [
1025
+ ...RULE_OUTBOUND_TABLES,
1026
+ TELEMETRY_OUTBOUND_TABLE
1027
+ ];
1028
+ TELEMETRY_OUTBOX_CAP = 2e4;
1029
+ LEASE_TTL_MS = 15 * 60 * 1e3;
1030
+ EVENT_TELEMETRY_CAPPED = "rule_telemetry_capped";
1031
+ }
1032
+ });
1033
+
1034
+ // src/rule-candidate-store.ts
1035
+ function ensureRuleCandidatesTable(db) {
1036
+ db.exec(`
1037
+ CREATE TABLE IF NOT EXISTS rule_candidates (
1038
+ prompt_hash TEXT PRIMARY KEY,
1039
+ status TEXT NOT NULL DEFAULT 'proposed'
1040
+ CHECK (status IN ('proposed','shown','promoted','dismissed')),
1041
+ origin TEXT NOT NULL DEFAULT 'local'
1042
+ CHECK (origin IN ('local','team','pack')),
1043
+ score REAL,
1044
+ destination TEXT,
1045
+ payload_json TEXT NOT NULL DEFAULT '{}',
1046
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
1047
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
1048
+ );
1049
+ CREATE INDEX IF NOT EXISTS idx_rule_candidates_status ON rule_candidates(status);
1050
+ CREATE INDEX IF NOT EXISTS idx_rule_candidates_created ON rule_candidates(created_at);
1051
+ `);
1052
+ }
1053
+ var init_rule_candidate_store = __esm({
1054
+ "src/rule-candidate-store.ts"() {
1055
+ "use strict";
1056
+ }
1057
+ });
1058
+
959
1059
  // src/hooks/lib/hook-failure-signal.ts
960
1060
  import { appendFileSync, mkdirSync as mkdirSync3, existsSync as existsSync4 } from "fs";
961
1061
  import { join as join2, dirname as dirname3 } from "path";
@@ -1509,9 +1609,6 @@ __export(memory_db_exports, {
1509
1609
  createSession: () => createSession,
1510
1610
  deduplicateFailedAttempt: () => deduplicateFailedAttempt,
1511
1611
  dequeuePendingSync: () => dequeuePendingSync,
1512
- drainRulePromotionEvents: () => drainRulePromotionEvents,
1513
- drainTeamPromotions: () => drainTeamPromotions,
1514
- drainTeamRevocations: () => drainTeamRevocations,
1515
1612
  embedMissingObservations: () => embedMissingObservations,
1516
1613
  endSession: () => endSession,
1517
1614
  enqueueRulePromotionEvent: () => enqueueRulePromotionEvent,
@@ -2418,6 +2515,8 @@ function initMemorySchema(db) {
2418
2515
  `);
2419
2516
  migrateObservationEmbeddingChunks(db);
2420
2517
  migrateMemoryFilesFor4B(db);
2518
+ ensureRuleCandidatesTable(db);
2519
+ migrateRuleDelivery(db);
2421
2520
  }
2422
2521
  function enqueueSyncPayload(db, payload) {
2423
2522
  db.prepare("INSERT INTO pending_sync (payload) VALUES (?)").run(payload);
@@ -2492,38 +2591,12 @@ function enqueueTeamPromotion(db, promo) {
2492
2591
  promo.review_attestation !== void 0 ? JSON.stringify(promo.review_attestation) : null
2493
2592
  );
2494
2593
  }
2495
- function drainTeamPromotions(db) {
2496
- const rows = db.prepare(`
2497
- SELECT prompt_hash, destination, draft_text, score, signals_json, content_hash, hardened, review_attestation_json
2498
- FROM team_promotion_outbound ORDER BY created_at ASC LIMIT 1000
2499
- `).all();
2500
- if (rows.length === 0) return [];
2501
- db.prepare("DELETE FROM team_promotion_outbound").run();
2502
- return rows.map((r) => ({
2503
- prompt_hash: r.prompt_hash,
2504
- destination: r.destination,
2505
- draft_text: r.draft_text,
2506
- score: r.score ?? void 0,
2507
- signals: safeJsonArray(r.signals_json),
2508
- content_hash: r.content_hash,
2509
- hardened: r.hardened === 1,
2510
- review_attestation: r.review_attestation_json ? safeJsonParse(r.review_attestation_json) : void 0
2511
- }));
2512
- }
2513
2594
  function enqueueTeamRevocation(db, promptHash) {
2514
2595
  db.prepare(`
2515
2596
  INSERT OR REPLACE INTO team_revocation_outbound (prompt_hash, created_at)
2516
2597
  VALUES (?, datetime('now'))
2517
2598
  `).run(promptHash);
2518
2599
  }
2519
- function drainTeamRevocations(db) {
2520
- const rows = db.prepare(
2521
- `SELECT prompt_hash FROM team_revocation_outbound ORDER BY created_at ASC LIMIT 1000`
2522
- ).all();
2523
- if (rows.length === 0) return [];
2524
- db.prepare("DELETE FROM team_revocation_outbound").run();
2525
- return rows.map((r) => r.prompt_hash);
2526
- }
2527
2600
  function getRecurrenceCountForPromptHash(db, promptHash) {
2528
2601
  try {
2529
2602
  const row = db.prepare(`
@@ -2551,30 +2624,14 @@ function enqueueRulePromotionEvent(db, ev) {
2551
2624
  JSON.stringify(ev.metadata ?? {}),
2552
2625
  ev.created_at
2553
2626
  );
2554
- db.prepare(`
2555
- DELETE FROM rule_promotion_events_outbound
2556
- WHERE id NOT IN (
2557
- SELECT id FROM rule_promotion_events_outbound ORDER BY id DESC LIMIT ?
2558
- )
2559
- `).run(FUNNEL_EVENT_OUTBOX_CAP);
2560
- } catch {
2627
+ capTelemetry(db, FUNNEL_EVENT_OUTBOX_CAP);
2628
+ } catch (err) {
2629
+ process.stderr.write(
2630
+ `[massu] WARNING: failed to record promotion-funnel event (${ev.event_type} / ${ev.prompt_hash}): ${err instanceof Error ? err.message : String(err)}
2631
+ `
2632
+ );
2561
2633
  }
2562
2634
  }
2563
- function drainRulePromotionEvents(db) {
2564
- const rows = db.prepare(`
2565
- SELECT id, prompt_hash, event_type, metadata_json, created_at
2566
- FROM rule_promotion_events_outbound ORDER BY id ASC LIMIT ?
2567
- `).all(FUNNEL_EVENT_DRAIN_LIMIT);
2568
- if (rows.length === 0) return [];
2569
- const maxId = rows[rows.length - 1].id;
2570
- db.prepare("DELETE FROM rule_promotion_events_outbound WHERE id <= ?").run(maxId);
2571
- return rows.map((r) => ({
2572
- prompt_hash: r.prompt_hash,
2573
- event_type: r.event_type,
2574
- created_at: r.created_at,
2575
- metadata: safeJsonParse(r.metadata_json) ?? {}
2576
- }));
2577
- }
2578
2635
  function recordTelemetry(db, eventType, data) {
2579
2636
  try {
2580
2637
  db.prepare(`
@@ -2584,28 +2641,55 @@ function recordTelemetry(db, eventType, data) {
2584
2641
  } catch {
2585
2642
  }
2586
2643
  }
2587
- function safeJsonArray(json) {
2588
- try {
2589
- const v = JSON.parse(json);
2590
- return Array.isArray(v) ? v : [];
2591
- } catch {
2592
- return [];
2593
- }
2594
- }
2595
- function safeJsonParse(json) {
2596
- try {
2597
- return JSON.parse(json);
2598
- } catch {
2599
- return void 0;
2644
+ function rescueLearningFromStalePayloads(db, stale) {
2645
+ let rescued = 0;
2646
+ for (const item of stale) {
2647
+ let payload;
2648
+ try {
2649
+ payload = JSON.parse(item.payload);
2650
+ } catch {
2651
+ continue;
2652
+ }
2653
+ try {
2654
+ const promotions = Array.isArray(payload.rule_promotions) ? payload.rule_promotions : [];
2655
+ for (const p of promotions) {
2656
+ enqueueTeamPromotion(db, p);
2657
+ rescued++;
2658
+ }
2659
+ const revocations = Array.isArray(payload.rule_revocations) ? payload.rule_revocations : [];
2660
+ for (const r of revocations) {
2661
+ if (typeof r?.prompt_hash === "string") {
2662
+ enqueueTeamRevocation(db, r.prompt_hash);
2663
+ rescued++;
2664
+ }
2665
+ }
2666
+ const events = Array.isArray(payload.rule_promotion_events) ? payload.rule_promotion_events : [];
2667
+ for (const e of events) {
2668
+ enqueueRulePromotionEvent(db, e);
2669
+ }
2670
+ } catch (err) {
2671
+ process.stderr.write(
2672
+ `[massu] WARNING: failed to rescue learned rules from give-up payload ${item.id}: ${err instanceof Error ? err.message : String(err)}
2673
+ `
2674
+ );
2675
+ }
2600
2676
  }
2677
+ return rescued;
2601
2678
  }
2602
2679
  function dequeuePendingSync(db, limit = 10) {
2603
2680
  const stale = db.prepare(
2604
- "SELECT id, retry_count, last_error FROM pending_sync WHERE retry_count >= 10 LIMIT 10000"
2681
+ "SELECT id, retry_count, last_error, payload FROM pending_sync WHERE retry_count >= 10 LIMIT 10000"
2605
2682
  ).all();
2606
2683
  if (stale.length > 0) {
2684
+ const rescued = rescueLearningFromStalePayloads(db, stale);
2607
2685
  const ids = stale.map((s) => s.id);
2608
2686
  db.prepare(`DELETE FROM pending_sync WHERE id IN (${ids.map(() => "?").join(",")})`).run(...ids);
2687
+ if (rescued > 0) {
2688
+ process.stderr.write(
2689
+ `[massu] RESCUED ${rescued} learned rule(s) from ${stale.length} give-up payload(s) \u2014 they were re-queued for delivery, NOT discarded.
2690
+ `
2691
+ );
2692
+ }
2609
2693
  const lastErrors = [...new Set(stale.map((s) => s.last_error).filter(Boolean))];
2610
2694
  process.stderr.write(
2611
2695
  `[massu] WARNING: ${stale.length} cloud-sync queue item(s) discarded after 10+ retries. Likely cause: invalid API key or unreachable endpoint. Recent errors: ${lastErrors.slice(0, 3).join("; ") || "(none recorded)"}
@@ -3297,19 +3381,20 @@ function scoreFailureClasses(db, matchText, filePath, promptContext, weights) {
3297
3381
  }
3298
3382
  return bestMatch;
3299
3383
  }
3300
- var MEMORY_SCHEMA_VERSION, TOOL_COST_EVENTS_RETENTION_DAYS, MAX_DRAFT_TEXT_LEN, FUNNEL_EVENT_DRAIN_LIMIT, FUNNEL_EVENT_OUTBOX_CAP, _temporalColCache, CONSOLIDATION_LESSON_EVIDENCE, MEMORY_FILE_TITLE_PREFIX, MEMORY_FILE_TITLE_LIKE, USAGE_COUNTER_ARMED_KEY;
3384
+ 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;
3301
3385
  var init_memory_db = __esm({
3302
3386
  "src/memory-db.ts"() {
3303
3387
  "use strict";
3304
3388
  init_config();
3305
3389
  init_memory_vector();
3306
3390
  init_db_backup();
3391
+ init_rule_delivery();
3392
+ init_rule_candidate_store();
3307
3393
  init_hook_failure_signal();
3308
3394
  init_memory_embed_sweep();
3309
3395
  MEMORY_SCHEMA_VERSION = 1;
3310
3396
  TOOL_COST_EVENTS_RETENTION_DAYS = 90;
3311
3397
  MAX_DRAFT_TEXT_LEN = 16384;
3312
- FUNNEL_EVENT_DRAIN_LIMIT = 5e3;
3313
3398
  FUNNEL_EVENT_OUTBOX_CAP = 2e4;
3314
3399
  _temporalColCache = /* @__PURE__ */ new WeakMap();
3315
3400
  CONSOLIDATION_LESSON_EVIDENCE = "consolidation:session-summary";
@@ -950,6 +950,106 @@ var init_db_backup = __esm({
950
950
  }
951
951
  });
952
952
 
953
+ // src/rule-delivery.ts
954
+ function tableExists(db, table) {
955
+ const row = db.prepare("SELECT 1 AS ok FROM sqlite_master WHERE type='table' AND name=?").get(table);
956
+ return row !== void 0;
957
+ }
958
+ function columns(db, table) {
959
+ const rows = db.prepare(`PRAGMA table_info(${table})`).all();
960
+ return new Set(rows.map((r) => r.name));
961
+ }
962
+ function migrateRuleDelivery(db) {
963
+ const ADDITIONS = [
964
+ { name: "attempts", decl: "INTEGER NOT NULL DEFAULT 0" },
965
+ { name: "lease_token", decl: "TEXT" },
966
+ { name: "leased_at_ms", decl: "INTEGER" },
967
+ { name: "last_error", decl: "TEXT" }
968
+ ];
969
+ for (const table of ALL_OUTBOUND_TABLES) {
970
+ if (!tableExists(db, table)) continue;
971
+ const cols = columns(db, table);
972
+ for (const add of ADDITIONS) {
973
+ if (cols.has(add.name)) continue;
974
+ db.exec(`ALTER TABLE ${table} ADD COLUMN ${add.name} ${add.decl}`);
975
+ }
976
+ }
977
+ }
978
+ function capTelemetry(db, cap = TELEMETRY_OUTBOX_CAP) {
979
+ if (!tableExists(db, TELEMETRY_OUTBOUND_TABLE)) return 0;
980
+ const before = db.prepare(`SELECT COUNT(*) AS n FROM ${TELEMETRY_OUTBOUND_TABLE}`).get();
981
+ if (before.n <= cap) return 0;
982
+ const info = db.prepare(
983
+ `DELETE FROM ${TELEMETRY_OUTBOUND_TABLE}
984
+ WHERE id NOT IN (SELECT id FROM ${TELEMETRY_OUTBOUND_TABLE} ORDER BY id DESC LIMIT ?)`
985
+ ).run(cap);
986
+ const dropped = info.changes;
987
+ if (dropped > 0) {
988
+ process.stderr.write(
989
+ `[massu] NOTE: dropped ${dropped} oldest promotion-funnel telemetry row(s) at the ${cap}-row cap. No learned rules were affected (rules are never dropped).
990
+ `
991
+ );
992
+ recordAnalytics(db, EVENT_TELEMETRY_CAPPED, { dropped, cap });
993
+ }
994
+ return dropped;
995
+ }
996
+ function recordAnalytics(db, eventType, data) {
997
+ try {
998
+ db.prepare(
999
+ `INSERT INTO analytics_events (event_type, event_data, created_at)
1000
+ VALUES (?, ?, datetime('now'))`
1001
+ ).run(eventType, JSON.stringify(data));
1002
+ } catch (err) {
1003
+ process.stderr.write(
1004
+ `[massu] WARNING: could not record '${eventType}' telemetry: ${err instanceof Error ? err.message : String(err)}
1005
+ `
1006
+ );
1007
+ }
1008
+ }
1009
+ var RULE_OUTBOUND_TABLES, TELEMETRY_OUTBOUND_TABLE, ALL_OUTBOUND_TABLES, TELEMETRY_OUTBOX_CAP, LEASE_TTL_MS, EVENT_TELEMETRY_CAPPED;
1010
+ var init_rule_delivery = __esm({
1011
+ "src/rule-delivery.ts"() {
1012
+ "use strict";
1013
+ RULE_OUTBOUND_TABLES = [
1014
+ "team_promotion_outbound",
1015
+ "team_revocation_outbound"
1016
+ ];
1017
+ TELEMETRY_OUTBOUND_TABLE = "rule_promotion_events_outbound";
1018
+ ALL_OUTBOUND_TABLES = [
1019
+ ...RULE_OUTBOUND_TABLES,
1020
+ TELEMETRY_OUTBOUND_TABLE
1021
+ ];
1022
+ TELEMETRY_OUTBOX_CAP = 2e4;
1023
+ LEASE_TTL_MS = 15 * 60 * 1e3;
1024
+ EVENT_TELEMETRY_CAPPED = "rule_telemetry_capped";
1025
+ }
1026
+ });
1027
+
1028
+ // src/rule-candidate-store.ts
1029
+ function ensureRuleCandidatesTable(db) {
1030
+ db.exec(`
1031
+ CREATE TABLE IF NOT EXISTS rule_candidates (
1032
+ prompt_hash TEXT PRIMARY KEY,
1033
+ status TEXT NOT NULL DEFAULT 'proposed'
1034
+ CHECK (status IN ('proposed','shown','promoted','dismissed')),
1035
+ origin TEXT NOT NULL DEFAULT 'local'
1036
+ CHECK (origin IN ('local','team','pack')),
1037
+ score REAL,
1038
+ destination TEXT,
1039
+ payload_json TEXT NOT NULL DEFAULT '{}',
1040
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
1041
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
1042
+ );
1043
+ CREATE INDEX IF NOT EXISTS idx_rule_candidates_status ON rule_candidates(status);
1044
+ CREATE INDEX IF NOT EXISTS idx_rule_candidates_created ON rule_candidates(created_at);
1045
+ `);
1046
+ }
1047
+ var init_rule_candidate_store = __esm({
1048
+ "src/rule-candidate-store.ts"() {
1049
+ "use strict";
1050
+ }
1051
+ });
1052
+
953
1053
  // src/hooks/lib/hook-failure-signal.ts
954
1054
  import { appendFileSync, mkdirSync as mkdirSync3, existsSync as existsSync4 } from "fs";
955
1055
  import { join as join2, dirname as dirname3 } from "path";
@@ -1503,9 +1603,6 @@ __export(memory_db_exports, {
1503
1603
  createSession: () => createSession,
1504
1604
  deduplicateFailedAttempt: () => deduplicateFailedAttempt,
1505
1605
  dequeuePendingSync: () => dequeuePendingSync,
1506
- drainRulePromotionEvents: () => drainRulePromotionEvents,
1507
- drainTeamPromotions: () => drainTeamPromotions,
1508
- drainTeamRevocations: () => drainTeamRevocations,
1509
1606
  embedMissingObservations: () => embedMissingObservations,
1510
1607
  endSession: () => endSession,
1511
1608
  enqueueRulePromotionEvent: () => enqueueRulePromotionEvent,
@@ -2412,6 +2509,8 @@ function initMemorySchema(db) {
2412
2509
  `);
2413
2510
  migrateObservationEmbeddingChunks(db);
2414
2511
  migrateMemoryFilesFor4B(db);
2512
+ ensureRuleCandidatesTable(db);
2513
+ migrateRuleDelivery(db);
2415
2514
  }
2416
2515
  function enqueueSyncPayload(db, payload) {
2417
2516
  db.prepare("INSERT INTO pending_sync (payload) VALUES (?)").run(payload);
@@ -2486,38 +2585,12 @@ function enqueueTeamPromotion(db, promo) {
2486
2585
  promo.review_attestation !== void 0 ? JSON.stringify(promo.review_attestation) : null
2487
2586
  );
2488
2587
  }
2489
- function drainTeamPromotions(db) {
2490
- const rows = db.prepare(`
2491
- SELECT prompt_hash, destination, draft_text, score, signals_json, content_hash, hardened, review_attestation_json
2492
- FROM team_promotion_outbound ORDER BY created_at ASC LIMIT 1000
2493
- `).all();
2494
- if (rows.length === 0) return [];
2495
- db.prepare("DELETE FROM team_promotion_outbound").run();
2496
- return rows.map((r) => ({
2497
- prompt_hash: r.prompt_hash,
2498
- destination: r.destination,
2499
- draft_text: r.draft_text,
2500
- score: r.score ?? void 0,
2501
- signals: safeJsonArray(r.signals_json),
2502
- content_hash: r.content_hash,
2503
- hardened: r.hardened === 1,
2504
- review_attestation: r.review_attestation_json ? safeJsonParse(r.review_attestation_json) : void 0
2505
- }));
2506
- }
2507
2588
  function enqueueTeamRevocation(db, promptHash) {
2508
2589
  db.prepare(`
2509
2590
  INSERT OR REPLACE INTO team_revocation_outbound (prompt_hash, created_at)
2510
2591
  VALUES (?, datetime('now'))
2511
2592
  `).run(promptHash);
2512
2593
  }
2513
- function drainTeamRevocations(db) {
2514
- const rows = db.prepare(
2515
- `SELECT prompt_hash FROM team_revocation_outbound ORDER BY created_at ASC LIMIT 1000`
2516
- ).all();
2517
- if (rows.length === 0) return [];
2518
- db.prepare("DELETE FROM team_revocation_outbound").run();
2519
- return rows.map((r) => r.prompt_hash);
2520
- }
2521
2594
  function getRecurrenceCountForPromptHash(db, promptHash) {
2522
2595
  try {
2523
2596
  const row = db.prepare(`
@@ -2545,30 +2618,14 @@ function enqueueRulePromotionEvent(db, ev) {
2545
2618
  JSON.stringify(ev.metadata ?? {}),
2546
2619
  ev.created_at
2547
2620
  );
2548
- db.prepare(`
2549
- DELETE FROM rule_promotion_events_outbound
2550
- WHERE id NOT IN (
2551
- SELECT id FROM rule_promotion_events_outbound ORDER BY id DESC LIMIT ?
2552
- )
2553
- `).run(FUNNEL_EVENT_OUTBOX_CAP);
2554
- } catch {
2621
+ capTelemetry(db, FUNNEL_EVENT_OUTBOX_CAP);
2622
+ } catch (err) {
2623
+ process.stderr.write(
2624
+ `[massu] WARNING: failed to record promotion-funnel event (${ev.event_type} / ${ev.prompt_hash}): ${err instanceof Error ? err.message : String(err)}
2625
+ `
2626
+ );
2555
2627
  }
2556
2628
  }
2557
- function drainRulePromotionEvents(db) {
2558
- const rows = db.prepare(`
2559
- SELECT id, prompt_hash, event_type, metadata_json, created_at
2560
- FROM rule_promotion_events_outbound ORDER BY id ASC LIMIT ?
2561
- `).all(FUNNEL_EVENT_DRAIN_LIMIT);
2562
- if (rows.length === 0) return [];
2563
- const maxId = rows[rows.length - 1].id;
2564
- db.prepare("DELETE FROM rule_promotion_events_outbound WHERE id <= ?").run(maxId);
2565
- return rows.map((r) => ({
2566
- prompt_hash: r.prompt_hash,
2567
- event_type: r.event_type,
2568
- created_at: r.created_at,
2569
- metadata: safeJsonParse(r.metadata_json) ?? {}
2570
- }));
2571
- }
2572
2629
  function recordTelemetry(db, eventType, data) {
2573
2630
  try {
2574
2631
  db.prepare(`
@@ -2578,28 +2635,55 @@ function recordTelemetry(db, eventType, data) {
2578
2635
  } catch {
2579
2636
  }
2580
2637
  }
2581
- function safeJsonArray(json) {
2582
- try {
2583
- const v = JSON.parse(json);
2584
- return Array.isArray(v) ? v : [];
2585
- } catch {
2586
- return [];
2587
- }
2588
- }
2589
- function safeJsonParse(json) {
2590
- try {
2591
- return JSON.parse(json);
2592
- } catch {
2593
- return void 0;
2638
+ function rescueLearningFromStalePayloads(db, stale) {
2639
+ let rescued = 0;
2640
+ for (const item of stale) {
2641
+ let payload;
2642
+ try {
2643
+ payload = JSON.parse(item.payload);
2644
+ } catch {
2645
+ continue;
2646
+ }
2647
+ try {
2648
+ const promotions = Array.isArray(payload.rule_promotions) ? payload.rule_promotions : [];
2649
+ for (const p of promotions) {
2650
+ enqueueTeamPromotion(db, p);
2651
+ rescued++;
2652
+ }
2653
+ const revocations = Array.isArray(payload.rule_revocations) ? payload.rule_revocations : [];
2654
+ for (const r of revocations) {
2655
+ if (typeof r?.prompt_hash === "string") {
2656
+ enqueueTeamRevocation(db, r.prompt_hash);
2657
+ rescued++;
2658
+ }
2659
+ }
2660
+ const events = Array.isArray(payload.rule_promotion_events) ? payload.rule_promotion_events : [];
2661
+ for (const e of events) {
2662
+ enqueueRulePromotionEvent(db, e);
2663
+ }
2664
+ } catch (err) {
2665
+ process.stderr.write(
2666
+ `[massu] WARNING: failed to rescue learned rules from give-up payload ${item.id}: ${err instanceof Error ? err.message : String(err)}
2667
+ `
2668
+ );
2669
+ }
2594
2670
  }
2671
+ return rescued;
2595
2672
  }
2596
2673
  function dequeuePendingSync(db, limit = 10) {
2597
2674
  const stale = db.prepare(
2598
- "SELECT id, retry_count, last_error FROM pending_sync WHERE retry_count >= 10 LIMIT 10000"
2675
+ "SELECT id, retry_count, last_error, payload FROM pending_sync WHERE retry_count >= 10 LIMIT 10000"
2599
2676
  ).all();
2600
2677
  if (stale.length > 0) {
2678
+ const rescued = rescueLearningFromStalePayloads(db, stale);
2601
2679
  const ids = stale.map((s) => s.id);
2602
2680
  db.prepare(`DELETE FROM pending_sync WHERE id IN (${ids.map(() => "?").join(",")})`).run(...ids);
2681
+ if (rescued > 0) {
2682
+ process.stderr.write(
2683
+ `[massu] RESCUED ${rescued} learned rule(s) from ${stale.length} give-up payload(s) \u2014 they were re-queued for delivery, NOT discarded.
2684
+ `
2685
+ );
2686
+ }
2603
2687
  const lastErrors = [...new Set(stale.map((s) => s.last_error).filter(Boolean))];
2604
2688
  process.stderr.write(
2605
2689
  `[massu] WARNING: ${stale.length} cloud-sync queue item(s) discarded after 10+ retries. Likely cause: invalid API key or unreachable endpoint. Recent errors: ${lastErrors.slice(0, 3).join("; ") || "(none recorded)"}
@@ -3291,19 +3375,20 @@ function scoreFailureClasses(db, matchText, filePath, promptContext, weights) {
3291
3375
  }
3292
3376
  return bestMatch;
3293
3377
  }
3294
- var MEMORY_SCHEMA_VERSION, TOOL_COST_EVENTS_RETENTION_DAYS, MAX_DRAFT_TEXT_LEN, FUNNEL_EVENT_DRAIN_LIMIT, FUNNEL_EVENT_OUTBOX_CAP, _temporalColCache, CONSOLIDATION_LESSON_EVIDENCE, MEMORY_FILE_TITLE_PREFIX, MEMORY_FILE_TITLE_LIKE, USAGE_COUNTER_ARMED_KEY;
3378
+ 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;
3295
3379
  var init_memory_db = __esm({
3296
3380
  "src/memory-db.ts"() {
3297
3381
  "use strict";
3298
3382
  init_config();
3299
3383
  init_memory_vector();
3300
3384
  init_db_backup();
3385
+ init_rule_delivery();
3386
+ init_rule_candidate_store();
3301
3387
  init_hook_failure_signal();
3302
3388
  init_memory_embed_sweep();
3303
3389
  MEMORY_SCHEMA_VERSION = 1;
3304
3390
  TOOL_COST_EVENTS_RETENTION_DAYS = 90;
3305
3391
  MAX_DRAFT_TEXT_LEN = 16384;
3306
- FUNNEL_EVENT_DRAIN_LIMIT = 5e3;
3307
3392
  FUNNEL_EVENT_OUTBOX_CAP = 2e4;
3308
3393
  _temporalColCache = /* @__PURE__ */ new WeakMap();
3309
3394
  CONSOLIDATION_LESSON_EVIDENCE = "consolidation:session-summary";