@askexenow/exe-os 0.9.33 → 0.9.35

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.
Files changed (76) hide show
  1. package/dist/bin/backfill-conversations.js +228 -12
  2. package/dist/bin/backfill-responses.js +228 -12
  3. package/dist/bin/backfill-vectors.js +31 -4
  4. package/dist/bin/cleanup-stale-review-tasks.js +235 -12
  5. package/dist/bin/cli.js +265 -22
  6. package/dist/bin/exe-assign.js +228 -12
  7. package/dist/bin/exe-boot.js +56 -13
  8. package/dist/bin/exe-cloud.js +3 -3
  9. package/dist/bin/exe-dispatch.js +243 -15
  10. package/dist/bin/exe-doctor.js +34 -9
  11. package/dist/bin/exe-export-behaviors.js +235 -12
  12. package/dist/bin/exe-forget.js +244 -18
  13. package/dist/bin/exe-gateway.js +243 -15
  14. package/dist/bin/exe-heartbeat.js +235 -12
  15. package/dist/bin/exe-kill.js +235 -12
  16. package/dist/bin/exe-launch-agent.js +126 -4
  17. package/dist/bin/exe-link.js +28 -5
  18. package/dist/bin/exe-pending-messages.js +235 -12
  19. package/dist/bin/exe-pending-notifications.js +235 -12
  20. package/dist/bin/exe-pending-reviews.js +235 -12
  21. package/dist/bin/exe-rename.js +26 -2
  22. package/dist/bin/exe-review.js +235 -12
  23. package/dist/bin/exe-search.js +235 -12
  24. package/dist/bin/exe-session-cleanup.js +243 -15
  25. package/dist/bin/exe-settings.js +1 -1
  26. package/dist/bin/exe-start-codex.js +126 -4
  27. package/dist/bin/exe-start-opencode.js +126 -4
  28. package/dist/bin/exe-status.js +235 -12
  29. package/dist/bin/exe-team.js +235 -12
  30. package/dist/bin/git-sweep.js +243 -15
  31. package/dist/bin/graph-backfill.js +110 -4
  32. package/dist/bin/graph-export.js +235 -12
  33. package/dist/bin/intercom-check.js +243 -15
  34. package/dist/bin/scan-tasks.js +243 -15
  35. package/dist/bin/setup.js +32 -9
  36. package/dist/bin/shard-migrate.js +110 -4
  37. package/dist/gateway/index.js +243 -15
  38. package/dist/hooks/bug-report-worker.js +243 -15
  39. package/dist/hooks/codex-stop-task-finalizer.js +238 -14
  40. package/dist/hooks/commit-complete.js +243 -15
  41. package/dist/hooks/error-recall.js +244 -12
  42. package/dist/hooks/exe-heartbeat-hook.js +9 -0
  43. package/dist/hooks/ingest.js +244 -12
  44. package/dist/hooks/instructions-loaded.js +244 -12
  45. package/dist/hooks/notification.js +244 -12
  46. package/dist/hooks/post-compact.js +247 -13
  47. package/dist/hooks/post-tool-combined.js +251 -12
  48. package/dist/hooks/pre-compact.js +255 -16
  49. package/dist/hooks/pre-tool-use.js +247 -13
  50. package/dist/hooks/prompt-submit.js +255 -16
  51. package/dist/hooks/session-end.js +255 -16
  52. package/dist/hooks/session-start.js +251 -12
  53. package/dist/hooks/stop.js +247 -13
  54. package/dist/hooks/subagent-stop.js +247 -13
  55. package/dist/hooks/summary-worker.js +36 -8
  56. package/dist/index.js +243 -15
  57. package/dist/lib/cloud-sync.js +28 -5
  58. package/dist/lib/consolidation.js +3 -1
  59. package/dist/lib/database.js +25 -1
  60. package/dist/lib/db.js +25 -1
  61. package/dist/lib/device-registry.js +26 -2
  62. package/dist/lib/exe-daemon.js +22905 -9125
  63. package/dist/lib/hybrid-search.js +235 -12
  64. package/dist/lib/schedules.js +31 -4
  65. package/dist/lib/store.js +228 -12
  66. package/dist/lib/tasks.js +8 -3
  67. package/dist/lib/tmux-routing.js +8 -3
  68. package/dist/mcp/server.js +411 -164
  69. package/dist/mcp/tools/create-task.js +20 -4
  70. package/dist/mcp/tools/deactivate-behavior.js +9 -0
  71. package/dist/mcp/tools/list-tasks.js +12 -1
  72. package/dist/mcp/tools/send-message.js +12 -1
  73. package/dist/mcp/tools/update-task.js +24 -3
  74. package/dist/runtime/index.js +243 -15
  75. package/dist/tui/App.js +243 -15
  76. package/package.json +1 -1
@@ -1586,6 +1586,7 @@ var init_db_daemon_client = __esm({
1586
1586
  // src/lib/database.ts
1587
1587
  var database_exports = {};
1588
1588
  __export(database_exports, {
1589
+ SOFT_DELETE_RETENTION_DAYS: () => SOFT_DELETE_RETENTION_DAYS,
1589
1590
  disposeDatabase: () => disposeDatabase,
1590
1591
  disposeTurso: () => disposeTurso,
1591
1592
  ensureSchema: () => ensureSchema,
@@ -1742,10 +1743,17 @@ async function ensureSchema() {
1742
1743
  INSERT INTO memories_fts(memories_fts, rowid, raw_text) VALUES('delete', old.rowid, old.raw_text);
1743
1744
  END;
1744
1745
 
1745
- CREATE TRIGGER IF NOT EXISTS memories_fts_au AFTER UPDATE ON memories BEGIN
1746
+ CREATE TRIGGER IF NOT EXISTS memories_fts_au AFTER UPDATE ON memories
1747
+ WHEN new.status IS NULL OR new.status != 'deleted' BEGIN
1746
1748
  INSERT INTO memories_fts(memories_fts, rowid, raw_text) VALUES('delete', old.rowid, old.raw_text);
1747
1749
  INSERT INTO memories_fts(rowid, raw_text) VALUES (new.rowid, new.raw_text);
1748
1750
  END;
1751
+
1752
+ -- Soft-delete trigger: remove from FTS when status changes to 'deleted'
1753
+ CREATE TRIGGER IF NOT EXISTS memories_fts_soft_delete AFTER UPDATE ON memories
1754
+ WHEN new.status = 'deleted' AND (old.status IS NULL OR old.status != 'deleted') BEGIN
1755
+ INSERT INTO memories_fts(memories_fts, rowid, raw_text) VALUES('delete', old.rowid, old.raw_text);
1756
+ END;
1749
1757
  `);
1750
1758
  await client.executeMultiple(`
1751
1759
  CREATE TABLE IF NOT EXISTS sync_meta (
@@ -2148,6 +2156,13 @@ async function ensureSchema() {
2148
2156
  });
2149
2157
  } catch {
2150
2158
  }
2159
+ try {
2160
+ await client.execute({
2161
+ sql: `ALTER TABLE memories ADD COLUMN deleted_at TEXT`,
2162
+ args: []
2163
+ });
2164
+ } catch {
2165
+ }
2151
2166
  try {
2152
2167
  await client.execute({
2153
2168
  sql: `ALTER TABLE memories ADD COLUMN confidence REAL DEFAULT 0.7`,
@@ -2198,6 +2213,14 @@ async function ensureSchema() {
2198
2213
  );
2199
2214
  } catch {
2200
2215
  }
2216
+ try {
2217
+ await client.execute(
2218
+ `CREATE INDEX IF NOT EXISTS idx_memories_scoped_content_hash
2219
+ ON memories(content_hash, agent_id, project_name, memory_type)
2220
+ WHERE content_hash IS NOT NULL`
2221
+ );
2222
+ } catch {
2223
+ }
2201
2224
  await client.executeMultiple(`
2202
2225
  CREATE TABLE IF NOT EXISTS entities (
2203
2226
  id TEXT PRIMARY KEY,
@@ -2684,7 +2707,7 @@ async function disposeDatabase() {
2684
2707
  _resilientClient = null;
2685
2708
  }
2686
2709
  }
2687
- var _client, _resilientClient, _walCheckpointTimer, _daemonClient, _adapterClient, initTurso, disposeTurso;
2710
+ var _client, _resilientClient, _walCheckpointTimer, _daemonClient, _adapterClient, initTurso, SOFT_DELETE_RETENTION_DAYS, disposeTurso;
2688
2711
  var init_database = __esm({
2689
2712
  "src/lib/database.ts"() {
2690
2713
  "use strict";
@@ -2698,6 +2721,7 @@ var init_database = __esm({
2698
2721
  _daemonClient = null;
2699
2722
  _adapterClient = null;
2700
2723
  initTurso = initDatabase;
2724
+ SOFT_DELETE_RETENTION_DAYS = 7;
2701
2725
  disposeTurso = disposeDatabase;
2702
2726
  }
2703
2727
  });
@@ -2866,7 +2890,8 @@ async function ensureShardSchema(client) {
2866
2890
  }
2867
2891
  for (const idx of [
2868
2892
  "CREATE INDEX IF NOT EXISTS idx_memories_tier ON memories(tier)",
2869
- "CREATE INDEX IF NOT EXISTS idx_memories_supersedes ON memories(supersedes_id) WHERE supersedes_id IS NOT NULL"
2893
+ "CREATE INDEX IF NOT EXISTS idx_memories_supersedes ON memories(supersedes_id) WHERE supersedes_id IS NOT NULL",
2894
+ "CREATE INDEX IF NOT EXISTS idx_memories_scoped_content_hash ON memories(content_hash, agent_id, project_name, memory_type) WHERE content_hash IS NOT NULL"
2870
2895
  ]) {
2871
2896
  try {
2872
2897
  await client.execute(idx);
@@ -3285,7 +3310,6 @@ import { parseArgs } from "util";
3285
3310
  // src/lib/store.ts
3286
3311
  init_memory();
3287
3312
  init_database();
3288
- import { createHash } from "crypto";
3289
3313
 
3290
3314
  // src/lib/keychain.ts
3291
3315
  import { readFile as readFile3, writeFile as writeFile3, unlink, mkdir as mkdir3, chmod as chmod2 } from "fs/promises";
@@ -3518,6 +3542,190 @@ var StateBus = class {
3518
3542
  };
3519
3543
  var orgBus = new StateBus();
3520
3544
 
3545
+ // src/lib/memory-write-governor.ts
3546
+ import { createHash } from "crypto";
3547
+ var HIGH_VALUE_SUPERSESSION_TYPES = /* @__PURE__ */ new Set([
3548
+ "decision",
3549
+ "adr",
3550
+ "behavior",
3551
+ "procedure"
3552
+ ]);
3553
+ var NOISE_DROP_PATTERNS = [
3554
+ /^\s*\[📋\s+\d+\s+reviews?\s+pending\b/im,
3555
+ /^\s*<system-reminder>[\s\S]*?<\/system-reminder>\s*$/im,
3556
+ /^\s*The UserPromptSubmit hook checks the DB for new tasks/im,
3557
+ /^\s*Intercom is a speedup, not delivery/im,
3558
+ /^\s*Context bar reads as USAGE not remaining/im
3559
+ ];
3560
+ var SKIP_EMBED_PATTERNS = [
3561
+ /tmux capture-pane\b/i,
3562
+ /docker ps\b/i,
3563
+ /docker images\b/i,
3564
+ /git status\b/i,
3565
+ /grep .*node_modules/i,
3566
+ /npm (install|ci)\b[\s\S]*(added \d+ packages|audited \d+ packages)/i
3567
+ ];
3568
+ function normalizeMemoryText(text) {
3569
+ return text.replace(/\r\n/g, "\n").replace(/[ \t]+$/gm, "").replace(/\n{4,}/g, "\n\n\n").trim();
3570
+ }
3571
+ function classifyMemoryType(input) {
3572
+ if (input.memory_type && input.memory_type.trim()) return input.memory_type.trim();
3573
+ const tool = input.tool_name.toLowerCase();
3574
+ const text = input.raw_text.toLowerCase();
3575
+ if (tool.includes("store_decision") || tool.includes("decision")) return "decision";
3576
+ if (tool.includes("commit") || text.includes("adr-") || text.includes("architectural decision")) return "adr";
3577
+ if (tool.includes("store_behavior") || tool.includes("behavior")) return "behavior";
3578
+ if (tool.includes("global_procedure") || text.includes("organization-wide procedures")) return "procedure";
3579
+ if (tool.includes("send_whatsapp") || tool.includes("conversation")) return "conversation";
3580
+ if (tool === "store_memory" || tool === "manual") return "observation";
3581
+ return "raw";
3582
+ }
3583
+ function shouldDropMemory(text) {
3584
+ const normalized = normalizeMemoryText(text);
3585
+ if (normalized.length < 10) return { drop: true, reason: "too_short" };
3586
+ if (NOISE_DROP_PATTERNS.some((pattern) => pattern.test(normalized))) {
3587
+ return { drop: true, reason: "known_boilerplate_noise" };
3588
+ }
3589
+ return { drop: false };
3590
+ }
3591
+ function shouldSkipEmbedding(input) {
3592
+ const type = classifyMemoryType(input);
3593
+ if (HIGH_VALUE_SUPERSESSION_TYPES.has(type)) return false;
3594
+ if (type === "raw" && input.raw_text.length > 2e4) return true;
3595
+ if (SKIP_EMBED_PATTERNS.some((pattern) => pattern.test(input.raw_text))) return true;
3596
+ return false;
3597
+ }
3598
+ function hashMemoryContent(text) {
3599
+ return createHash("sha256").update(normalizeMemoryText(text)).digest("hex");
3600
+ }
3601
+ function scopedDedupArgs(input) {
3602
+ return [input.contentHash, input.agentId, input.projectName, input.memoryType];
3603
+ }
3604
+ function governMemoryRecord(record) {
3605
+ const normalized = normalizeMemoryText(record.raw_text);
3606
+ const memoryType = classifyMemoryType({
3607
+ raw_text: normalized,
3608
+ agent_id: record.agent_id,
3609
+ project_name: record.project_name,
3610
+ tool_name: record.tool_name,
3611
+ memory_type: record.memory_type
3612
+ });
3613
+ const drop = shouldDropMemory(normalized);
3614
+ const skipEmbedding = shouldSkipEmbedding({
3615
+ raw_text: normalized,
3616
+ agent_id: record.agent_id,
3617
+ project_name: record.project_name,
3618
+ tool_name: record.tool_name,
3619
+ memory_type: memoryType
3620
+ });
3621
+ return {
3622
+ record: {
3623
+ ...record,
3624
+ raw_text: normalized,
3625
+ memory_type: memoryType,
3626
+ vector: skipEmbedding ? null : record.vector
3627
+ },
3628
+ contentHash: hashMemoryContent(normalized),
3629
+ shouldDrop: drop.drop,
3630
+ dropReason: drop.reason,
3631
+ skipEmbedding,
3632
+ hygiene: {
3633
+ dedup: true,
3634
+ supersession: HIGH_VALUE_SUPERSESSION_TYPES.has(memoryType)
3635
+ }
3636
+ };
3637
+ }
3638
+ async function findScopedDuplicate(input) {
3639
+ const { getClient: getClient2 } = await Promise.resolve().then(() => (init_database(), database_exports));
3640
+ const client = getClient2();
3641
+ const args = scopedDedupArgs(input);
3642
+ let sql = `SELECT id FROM memories
3643
+ WHERE content_hash = ?
3644
+ AND agent_id = ?
3645
+ AND project_name = ?
3646
+ AND COALESCE(memory_type, 'raw') = ?
3647
+ AND COALESCE(status, 'active') != 'deleted'`;
3648
+ if (input.excludeId) {
3649
+ sql += " AND id != ?";
3650
+ args.push(input.excludeId);
3651
+ }
3652
+ sql += " ORDER BY timestamp DESC LIMIT 1";
3653
+ const result = await client.execute({ sql, args });
3654
+ return result.rows[0]?.id ? String(result.rows[0].id) : null;
3655
+ }
3656
+ async function runPostWriteMemoryHygiene(memoryId) {
3657
+ try {
3658
+ const { getClient: getClient2 } = await Promise.resolve().then(() => (init_database(), database_exports));
3659
+ const client = getClient2();
3660
+ const current = await client.execute({
3661
+ sql: `SELECT id, agent_id, project_name, memory_type, content_hash, supersedes_id,
3662
+ importance, timestamp
3663
+ FROM memories
3664
+ WHERE id = ?
3665
+ LIMIT 1`,
3666
+ args: [memoryId]
3667
+ });
3668
+ const row = current.rows[0];
3669
+ if (!row) return;
3670
+ const memoryType = String(row.memory_type ?? "raw");
3671
+ const contentHash = row.content_hash ? String(row.content_hash) : null;
3672
+ const agentId = String(row.agent_id);
3673
+ const projectName = String(row.project_name);
3674
+ if (contentHash) {
3675
+ await client.execute({
3676
+ sql: `UPDATE memories
3677
+ SET status = 'deleted',
3678
+ outcome = COALESCE(outcome, 'superseded')
3679
+ WHERE id != ?
3680
+ AND content_hash = ?
3681
+ AND agent_id = ?
3682
+ AND project_name = ?
3683
+ AND COALESCE(memory_type, 'raw') = ?
3684
+ AND COALESCE(status, 'active') = 'active'`,
3685
+ args: [memoryId, contentHash, agentId, projectName, memoryType]
3686
+ });
3687
+ }
3688
+ const supersedesId = row.supersedes_id ? String(row.supersedes_id) : null;
3689
+ if (supersedesId && HIGH_VALUE_SUPERSESSION_TYPES.has(memoryType)) {
3690
+ const old = await client.execute({
3691
+ sql: `SELECT importance FROM memories WHERE id = ? LIMIT 1`,
3692
+ args: [supersedesId]
3693
+ });
3694
+ const oldImportance = Number(old.rows[0]?.importance ?? 0);
3695
+ const newImportance = Number(row.importance ?? 0);
3696
+ await client.batch([
3697
+ {
3698
+ sql: `UPDATE memories
3699
+ SET status = 'archived',
3700
+ outcome = COALESCE(outcome, 'superseded')
3701
+ WHERE id = ?`,
3702
+ args: [supersedesId]
3703
+ },
3704
+ {
3705
+ sql: `UPDATE memories
3706
+ SET importance = MAX(COALESCE(importance, 5), ?),
3707
+ parent_memory_id = COALESCE(parent_memory_id, ?)
3708
+ WHERE id = ?`,
3709
+ args: [Math.max(oldImportance, newImportance), supersedesId, memoryId]
3710
+ }
3711
+ ], "write");
3712
+ }
3713
+ } catch (err) {
3714
+ process.stderr.write(
3715
+ `[memory-governor] post-write hygiene failed for ${memoryId}: ${err instanceof Error ? err.message : String(err)}
3716
+ `
3717
+ );
3718
+ }
3719
+ }
3720
+ function schedulePostWriteMemoryHygiene(memoryIds) {
3721
+ if (memoryIds.length === 0) return;
3722
+ const run = () => {
3723
+ void Promise.all(memoryIds.map((id) => runPostWriteMemoryHygiene(id)));
3724
+ };
3725
+ if (typeof setImmediate === "function") setImmediate(run);
3726
+ else setTimeout(run, 0);
3727
+ }
3728
+
3521
3729
  // src/lib/store.ts
3522
3730
  var INIT_MAX_RETRIES = 3;
3523
3731
  var INIT_RETRY_DELAY_MS = 1e3;
@@ -3640,17 +3848,24 @@ async function writeMemory(record) {
3640
3848
  `Expected ${EMBEDDING_DIM}-dim vector, got ${record.vector.length}`
3641
3849
  );
3642
3850
  }
3643
- const contentHash = createHash("md5").update(record.raw_text).digest("hex");
3644
- if (_pendingRecords.some((r) => r.content_hash === contentHash && r.agent_id === record.agent_id)) {
3851
+ const governed = governMemoryRecord(record);
3852
+ if (governed.shouldDrop) return;
3853
+ record = governed.record;
3854
+ const contentHash = governed.contentHash;
3855
+ const memoryType = record.memory_type ?? "raw";
3856
+ if (_pendingRecords.some(
3857
+ (r) => r.content_hash === contentHash && r.agent_id === record.agent_id && r.project_name === record.project_name && (r.memory_type ?? "raw") === memoryType
3858
+ )) {
3645
3859
  return;
3646
3860
  }
3647
3861
  try {
3648
- const client = getClient();
3649
- const existing = await client.execute({
3650
- sql: "SELECT id FROM memories WHERE content_hash = ? AND agent_id = ? LIMIT 1",
3651
- args: [contentHash, record.agent_id]
3862
+ const existing = await findScopedDuplicate({
3863
+ contentHash,
3864
+ agentId: record.agent_id,
3865
+ projectName: record.project_name,
3866
+ memoryType
3652
3867
  });
3653
- if (existing.rows.length > 0) return;
3868
+ if (existing) return;
3654
3869
  } catch {
3655
3870
  }
3656
3871
  const dbRow = {
@@ -3681,7 +3896,7 @@ async function writeMemory(record) {
3681
3896
  tier: record.tier ?? classifyTier(record),
3682
3897
  supersedes_id: record.supersedes_id ?? null,
3683
3898
  draft: record.draft ? 1 : 0,
3684
- memory_type: record.memory_type ?? "raw",
3899
+ memory_type: memoryType,
3685
3900
  trajectory: record.trajectory ? JSON.stringify(record.trajectory) : null,
3686
3901
  content_hash: contentHash,
3687
3902
  intent: record.intent ?? null,
@@ -3840,6 +4055,7 @@ async function flushBatch() {
3840
4055
  const globalClient = getClient();
3841
4056
  const globalStmts = batch.map(buildStmt);
3842
4057
  await globalClient.batch(globalStmts, "write");
4058
+ schedulePostWriteMemoryHygiene(batch.map((row) => row.id));
3843
4059
  _pendingRecords.splice(0, batch.length);
3844
4060
  try {
3845
4061
  const { isShardingEnabled: isShardingEnabled2, getReadyShardClient: getReadyShardClient2 } = await Promise.resolve().then(() => (init_shard_manager(), shard_manager_exports));
@@ -1586,6 +1586,7 @@ var init_db_daemon_client = __esm({
1586
1586
  // src/lib/database.ts
1587
1587
  var database_exports = {};
1588
1588
  __export(database_exports, {
1589
+ SOFT_DELETE_RETENTION_DAYS: () => SOFT_DELETE_RETENTION_DAYS,
1589
1590
  disposeDatabase: () => disposeDatabase,
1590
1591
  disposeTurso: () => disposeTurso,
1591
1592
  ensureSchema: () => ensureSchema,
@@ -1742,10 +1743,17 @@ async function ensureSchema() {
1742
1743
  INSERT INTO memories_fts(memories_fts, rowid, raw_text) VALUES('delete', old.rowid, old.raw_text);
1743
1744
  END;
1744
1745
 
1745
- CREATE TRIGGER IF NOT EXISTS memories_fts_au AFTER UPDATE ON memories BEGIN
1746
+ CREATE TRIGGER IF NOT EXISTS memories_fts_au AFTER UPDATE ON memories
1747
+ WHEN new.status IS NULL OR new.status != 'deleted' BEGIN
1746
1748
  INSERT INTO memories_fts(memories_fts, rowid, raw_text) VALUES('delete', old.rowid, old.raw_text);
1747
1749
  INSERT INTO memories_fts(rowid, raw_text) VALUES (new.rowid, new.raw_text);
1748
1750
  END;
1751
+
1752
+ -- Soft-delete trigger: remove from FTS when status changes to 'deleted'
1753
+ CREATE TRIGGER IF NOT EXISTS memories_fts_soft_delete AFTER UPDATE ON memories
1754
+ WHEN new.status = 'deleted' AND (old.status IS NULL OR old.status != 'deleted') BEGIN
1755
+ INSERT INTO memories_fts(memories_fts, rowid, raw_text) VALUES('delete', old.rowid, old.raw_text);
1756
+ END;
1749
1757
  `);
1750
1758
  await client.executeMultiple(`
1751
1759
  CREATE TABLE IF NOT EXISTS sync_meta (
@@ -2148,6 +2156,13 @@ async function ensureSchema() {
2148
2156
  });
2149
2157
  } catch {
2150
2158
  }
2159
+ try {
2160
+ await client.execute({
2161
+ sql: `ALTER TABLE memories ADD COLUMN deleted_at TEXT`,
2162
+ args: []
2163
+ });
2164
+ } catch {
2165
+ }
2151
2166
  try {
2152
2167
  await client.execute({
2153
2168
  sql: `ALTER TABLE memories ADD COLUMN confidence REAL DEFAULT 0.7`,
@@ -2198,6 +2213,14 @@ async function ensureSchema() {
2198
2213
  );
2199
2214
  } catch {
2200
2215
  }
2216
+ try {
2217
+ await client.execute(
2218
+ `CREATE INDEX IF NOT EXISTS idx_memories_scoped_content_hash
2219
+ ON memories(content_hash, agent_id, project_name, memory_type)
2220
+ WHERE content_hash IS NOT NULL`
2221
+ );
2222
+ } catch {
2223
+ }
2201
2224
  await client.executeMultiple(`
2202
2225
  CREATE TABLE IF NOT EXISTS entities (
2203
2226
  id TEXT PRIMARY KEY,
@@ -2684,7 +2707,7 @@ async function disposeDatabase() {
2684
2707
  _resilientClient = null;
2685
2708
  }
2686
2709
  }
2687
- var _client, _resilientClient, _walCheckpointTimer, _daemonClient, _adapterClient, initTurso, disposeTurso;
2710
+ var _client, _resilientClient, _walCheckpointTimer, _daemonClient, _adapterClient, initTurso, SOFT_DELETE_RETENTION_DAYS, disposeTurso;
2688
2711
  var init_database = __esm({
2689
2712
  "src/lib/database.ts"() {
2690
2713
  "use strict";
@@ -2698,6 +2721,7 @@ var init_database = __esm({
2698
2721
  _daemonClient = null;
2699
2722
  _adapterClient = null;
2700
2723
  initTurso = initDatabase;
2724
+ SOFT_DELETE_RETENTION_DAYS = 7;
2701
2725
  disposeTurso = disposeDatabase;
2702
2726
  }
2703
2727
  });
@@ -2866,7 +2890,8 @@ async function ensureShardSchema(client) {
2866
2890
  }
2867
2891
  for (const idx of [
2868
2892
  "CREATE INDEX IF NOT EXISTS idx_memories_tier ON memories(tier)",
2869
- "CREATE INDEX IF NOT EXISTS idx_memories_supersedes ON memories(supersedes_id) WHERE supersedes_id IS NOT NULL"
2893
+ "CREATE INDEX IF NOT EXISTS idx_memories_supersedes ON memories(supersedes_id) WHERE supersedes_id IS NOT NULL",
2894
+ "CREATE INDEX IF NOT EXISTS idx_memories_scoped_content_hash ON memories(content_hash, agent_id, project_name, memory_type) WHERE content_hash IS NOT NULL"
2870
2895
  ]) {
2871
2896
  try {
2872
2897
  await client.execute(idx);
@@ -3284,7 +3309,6 @@ import { homedir } from "os";
3284
3309
  // src/lib/store.ts
3285
3310
  init_memory();
3286
3311
  init_database();
3287
- import { createHash } from "crypto";
3288
3312
 
3289
3313
  // src/lib/keychain.ts
3290
3314
  import { readFile as readFile3, writeFile as writeFile3, unlink, mkdir as mkdir3, chmod as chmod2 } from "fs/promises";
@@ -3517,6 +3541,190 @@ var StateBus = class {
3517
3541
  };
3518
3542
  var orgBus = new StateBus();
3519
3543
 
3544
+ // src/lib/memory-write-governor.ts
3545
+ import { createHash } from "crypto";
3546
+ var HIGH_VALUE_SUPERSESSION_TYPES = /* @__PURE__ */ new Set([
3547
+ "decision",
3548
+ "adr",
3549
+ "behavior",
3550
+ "procedure"
3551
+ ]);
3552
+ var NOISE_DROP_PATTERNS = [
3553
+ /^\s*\[📋\s+\d+\s+reviews?\s+pending\b/im,
3554
+ /^\s*<system-reminder>[\s\S]*?<\/system-reminder>\s*$/im,
3555
+ /^\s*The UserPromptSubmit hook checks the DB for new tasks/im,
3556
+ /^\s*Intercom is a speedup, not delivery/im,
3557
+ /^\s*Context bar reads as USAGE not remaining/im
3558
+ ];
3559
+ var SKIP_EMBED_PATTERNS = [
3560
+ /tmux capture-pane\b/i,
3561
+ /docker ps\b/i,
3562
+ /docker images\b/i,
3563
+ /git status\b/i,
3564
+ /grep .*node_modules/i,
3565
+ /npm (install|ci)\b[\s\S]*(added \d+ packages|audited \d+ packages)/i
3566
+ ];
3567
+ function normalizeMemoryText(text) {
3568
+ return text.replace(/\r\n/g, "\n").replace(/[ \t]+$/gm, "").replace(/\n{4,}/g, "\n\n\n").trim();
3569
+ }
3570
+ function classifyMemoryType(input) {
3571
+ if (input.memory_type && input.memory_type.trim()) return input.memory_type.trim();
3572
+ const tool = input.tool_name.toLowerCase();
3573
+ const text = input.raw_text.toLowerCase();
3574
+ if (tool.includes("store_decision") || tool.includes("decision")) return "decision";
3575
+ if (tool.includes("commit") || text.includes("adr-") || text.includes("architectural decision")) return "adr";
3576
+ if (tool.includes("store_behavior") || tool.includes("behavior")) return "behavior";
3577
+ if (tool.includes("global_procedure") || text.includes("organization-wide procedures")) return "procedure";
3578
+ if (tool.includes("send_whatsapp") || tool.includes("conversation")) return "conversation";
3579
+ if (tool === "store_memory" || tool === "manual") return "observation";
3580
+ return "raw";
3581
+ }
3582
+ function shouldDropMemory(text) {
3583
+ const normalized = normalizeMemoryText(text);
3584
+ if (normalized.length < 10) return { drop: true, reason: "too_short" };
3585
+ if (NOISE_DROP_PATTERNS.some((pattern) => pattern.test(normalized))) {
3586
+ return { drop: true, reason: "known_boilerplate_noise" };
3587
+ }
3588
+ return { drop: false };
3589
+ }
3590
+ function shouldSkipEmbedding(input) {
3591
+ const type = classifyMemoryType(input);
3592
+ if (HIGH_VALUE_SUPERSESSION_TYPES.has(type)) return false;
3593
+ if (type === "raw" && input.raw_text.length > 2e4) return true;
3594
+ if (SKIP_EMBED_PATTERNS.some((pattern) => pattern.test(input.raw_text))) return true;
3595
+ return false;
3596
+ }
3597
+ function hashMemoryContent(text) {
3598
+ return createHash("sha256").update(normalizeMemoryText(text)).digest("hex");
3599
+ }
3600
+ function scopedDedupArgs(input) {
3601
+ return [input.contentHash, input.agentId, input.projectName, input.memoryType];
3602
+ }
3603
+ function governMemoryRecord(record) {
3604
+ const normalized = normalizeMemoryText(record.raw_text);
3605
+ const memoryType = classifyMemoryType({
3606
+ raw_text: normalized,
3607
+ agent_id: record.agent_id,
3608
+ project_name: record.project_name,
3609
+ tool_name: record.tool_name,
3610
+ memory_type: record.memory_type
3611
+ });
3612
+ const drop = shouldDropMemory(normalized);
3613
+ const skipEmbedding = shouldSkipEmbedding({
3614
+ raw_text: normalized,
3615
+ agent_id: record.agent_id,
3616
+ project_name: record.project_name,
3617
+ tool_name: record.tool_name,
3618
+ memory_type: memoryType
3619
+ });
3620
+ return {
3621
+ record: {
3622
+ ...record,
3623
+ raw_text: normalized,
3624
+ memory_type: memoryType,
3625
+ vector: skipEmbedding ? null : record.vector
3626
+ },
3627
+ contentHash: hashMemoryContent(normalized),
3628
+ shouldDrop: drop.drop,
3629
+ dropReason: drop.reason,
3630
+ skipEmbedding,
3631
+ hygiene: {
3632
+ dedup: true,
3633
+ supersession: HIGH_VALUE_SUPERSESSION_TYPES.has(memoryType)
3634
+ }
3635
+ };
3636
+ }
3637
+ async function findScopedDuplicate(input) {
3638
+ const { getClient: getClient2 } = await Promise.resolve().then(() => (init_database(), database_exports));
3639
+ const client = getClient2();
3640
+ const args = scopedDedupArgs(input);
3641
+ let sql = `SELECT id FROM memories
3642
+ WHERE content_hash = ?
3643
+ AND agent_id = ?
3644
+ AND project_name = ?
3645
+ AND COALESCE(memory_type, 'raw') = ?
3646
+ AND COALESCE(status, 'active') != 'deleted'`;
3647
+ if (input.excludeId) {
3648
+ sql += " AND id != ?";
3649
+ args.push(input.excludeId);
3650
+ }
3651
+ sql += " ORDER BY timestamp DESC LIMIT 1";
3652
+ const result = await client.execute({ sql, args });
3653
+ return result.rows[0]?.id ? String(result.rows[0].id) : null;
3654
+ }
3655
+ async function runPostWriteMemoryHygiene(memoryId) {
3656
+ try {
3657
+ const { getClient: getClient2 } = await Promise.resolve().then(() => (init_database(), database_exports));
3658
+ const client = getClient2();
3659
+ const current = await client.execute({
3660
+ sql: `SELECT id, agent_id, project_name, memory_type, content_hash, supersedes_id,
3661
+ importance, timestamp
3662
+ FROM memories
3663
+ WHERE id = ?
3664
+ LIMIT 1`,
3665
+ args: [memoryId]
3666
+ });
3667
+ const row = current.rows[0];
3668
+ if (!row) return;
3669
+ const memoryType = String(row.memory_type ?? "raw");
3670
+ const contentHash = row.content_hash ? String(row.content_hash) : null;
3671
+ const agentId = String(row.agent_id);
3672
+ const projectName = String(row.project_name);
3673
+ if (contentHash) {
3674
+ await client.execute({
3675
+ sql: `UPDATE memories
3676
+ SET status = 'deleted',
3677
+ outcome = COALESCE(outcome, 'superseded')
3678
+ WHERE id != ?
3679
+ AND content_hash = ?
3680
+ AND agent_id = ?
3681
+ AND project_name = ?
3682
+ AND COALESCE(memory_type, 'raw') = ?
3683
+ AND COALESCE(status, 'active') = 'active'`,
3684
+ args: [memoryId, contentHash, agentId, projectName, memoryType]
3685
+ });
3686
+ }
3687
+ const supersedesId = row.supersedes_id ? String(row.supersedes_id) : null;
3688
+ if (supersedesId && HIGH_VALUE_SUPERSESSION_TYPES.has(memoryType)) {
3689
+ const old = await client.execute({
3690
+ sql: `SELECT importance FROM memories WHERE id = ? LIMIT 1`,
3691
+ args: [supersedesId]
3692
+ });
3693
+ const oldImportance = Number(old.rows[0]?.importance ?? 0);
3694
+ const newImportance = Number(row.importance ?? 0);
3695
+ await client.batch([
3696
+ {
3697
+ sql: `UPDATE memories
3698
+ SET status = 'archived',
3699
+ outcome = COALESCE(outcome, 'superseded')
3700
+ WHERE id = ?`,
3701
+ args: [supersedesId]
3702
+ },
3703
+ {
3704
+ sql: `UPDATE memories
3705
+ SET importance = MAX(COALESCE(importance, 5), ?),
3706
+ parent_memory_id = COALESCE(parent_memory_id, ?)
3707
+ WHERE id = ?`,
3708
+ args: [Math.max(oldImportance, newImportance), supersedesId, memoryId]
3709
+ }
3710
+ ], "write");
3711
+ }
3712
+ } catch (err) {
3713
+ process.stderr.write(
3714
+ `[memory-governor] post-write hygiene failed for ${memoryId}: ${err instanceof Error ? err.message : String(err)}
3715
+ `
3716
+ );
3717
+ }
3718
+ }
3719
+ function schedulePostWriteMemoryHygiene(memoryIds) {
3720
+ if (memoryIds.length === 0) return;
3721
+ const run = () => {
3722
+ void Promise.all(memoryIds.map((id) => runPostWriteMemoryHygiene(id)));
3723
+ };
3724
+ if (typeof setImmediate === "function") setImmediate(run);
3725
+ else setTimeout(run, 0);
3726
+ }
3727
+
3520
3728
  // src/lib/store.ts
3521
3729
  var INIT_MAX_RETRIES = 3;
3522
3730
  var INIT_RETRY_DELAY_MS = 1e3;
@@ -3639,17 +3847,24 @@ async function writeMemory(record) {
3639
3847
  `Expected ${EMBEDDING_DIM}-dim vector, got ${record.vector.length}`
3640
3848
  );
3641
3849
  }
3642
- const contentHash = createHash("md5").update(record.raw_text).digest("hex");
3643
- if (_pendingRecords.some((r) => r.content_hash === contentHash && r.agent_id === record.agent_id)) {
3850
+ const governed = governMemoryRecord(record);
3851
+ if (governed.shouldDrop) return;
3852
+ record = governed.record;
3853
+ const contentHash = governed.contentHash;
3854
+ const memoryType = record.memory_type ?? "raw";
3855
+ if (_pendingRecords.some(
3856
+ (r) => r.content_hash === contentHash && r.agent_id === record.agent_id && r.project_name === record.project_name && (r.memory_type ?? "raw") === memoryType
3857
+ )) {
3644
3858
  return;
3645
3859
  }
3646
3860
  try {
3647
- const client = getClient();
3648
- const existing = await client.execute({
3649
- sql: "SELECT id FROM memories WHERE content_hash = ? AND agent_id = ? LIMIT 1",
3650
- args: [contentHash, record.agent_id]
3861
+ const existing = await findScopedDuplicate({
3862
+ contentHash,
3863
+ agentId: record.agent_id,
3864
+ projectName: record.project_name,
3865
+ memoryType
3651
3866
  });
3652
- if (existing.rows.length > 0) return;
3867
+ if (existing) return;
3653
3868
  } catch {
3654
3869
  }
3655
3870
  const dbRow = {
@@ -3680,7 +3895,7 @@ async function writeMemory(record) {
3680
3895
  tier: record.tier ?? classifyTier(record),
3681
3896
  supersedes_id: record.supersedes_id ?? null,
3682
3897
  draft: record.draft ? 1 : 0,
3683
- memory_type: record.memory_type ?? "raw",
3898
+ memory_type: memoryType,
3684
3899
  trajectory: record.trajectory ? JSON.stringify(record.trajectory) : null,
3685
3900
  content_hash: contentHash,
3686
3901
  intent: record.intent ?? null,
@@ -3839,6 +4054,7 @@ async function flushBatch() {
3839
4054
  const globalClient = getClient();
3840
4055
  const globalStmts = batch.map(buildStmt);
3841
4056
  await globalClient.batch(globalStmts, "write");
4057
+ schedulePostWriteMemoryHygiene(batch.map((row) => row.id));
3842
4058
  _pendingRecords.splice(0, batch.length);
3843
4059
  try {
3844
4060
  const { isShardingEnabled: isShardingEnabled2, getReadyShardClient: getReadyShardClient2 } = await Promise.resolve().then(() => (init_shard_manager(), shard_manager_exports));