@askexenow/exe-os 0.8.83 → 0.8.86

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 (103) hide show
  1. package/dist/bin/backfill-conversations.js +746 -595
  2. package/dist/bin/backfill-responses.js +745 -594
  3. package/dist/bin/backfill-vectors.js +312 -226
  4. package/dist/bin/cleanup-stale-review-tasks.js +154 -21
  5. package/dist/bin/cli.js +14678 -12676
  6. package/dist/bin/exe-agent-config.js +242 -0
  7. package/dist/bin/exe-agent.js +100 -91
  8. package/dist/bin/exe-assign.js +1003 -854
  9. package/dist/bin/exe-boot.js +1420 -485
  10. package/dist/bin/exe-call.js +10 -0
  11. package/dist/bin/exe-cloud.js +29 -6
  12. package/dist/bin/exe-dispatch.js +572 -271
  13. package/dist/bin/exe-doctor.js +403 -6
  14. package/dist/bin/exe-export-behaviors.js +175 -72
  15. package/dist/bin/exe-forget.js +102 -3
  16. package/dist/bin/exe-gateway.js +796 -292
  17. package/dist/bin/exe-healthcheck.js +134 -1
  18. package/dist/bin/exe-heartbeat.js +172 -36
  19. package/dist/bin/exe-kill.js +175 -72
  20. package/dist/bin/exe-launch-agent.js +189 -76
  21. package/dist/bin/exe-link.js +927 -82
  22. package/dist/bin/exe-new-employee.js +60 -8
  23. package/dist/bin/exe-pending-messages.js +151 -19
  24. package/dist/bin/exe-pending-notifications.js +97 -2
  25. package/dist/bin/exe-pending-reviews.js +155 -22
  26. package/dist/bin/exe-rename.js +564 -23
  27. package/dist/bin/exe-review.js +231 -73
  28. package/dist/bin/exe-search.js +995 -228
  29. package/dist/bin/exe-session-cleanup.js +4930 -1664
  30. package/dist/bin/exe-settings.js +20 -5
  31. package/dist/bin/exe-start-codex.js +2598 -0
  32. package/dist/bin/exe-start.sh +15 -3
  33. package/dist/bin/exe-status.js +154 -21
  34. package/dist/bin/exe-team.js +97 -2
  35. package/dist/bin/git-sweep.js +1180 -363
  36. package/dist/bin/graph-backfill.js +175 -72
  37. package/dist/bin/graph-export.js +175 -72
  38. package/dist/bin/install.js +60 -7
  39. package/dist/bin/list-providers.js +1 -0
  40. package/dist/bin/scan-tasks.js +1185 -367
  41. package/dist/bin/setup.js +914 -270
  42. package/dist/bin/shard-migrate.js +175 -72
  43. package/dist/bin/update.js +1 -0
  44. package/dist/bin/wiki-sync.js +175 -72
  45. package/dist/gateway/index.js +792 -285
  46. package/dist/hooks/bug-report-worker.js +445 -135
  47. package/dist/hooks/commit-complete.js +1178 -361
  48. package/dist/hooks/error-recall.js +994 -228
  49. package/dist/hooks/ingest-worker.js +1799 -1234
  50. package/dist/hooks/ingest.js +3 -0
  51. package/dist/hooks/instructions-loaded.js +707 -97
  52. package/dist/hooks/notification.js +699 -89
  53. package/dist/hooks/post-compact.js +757 -109
  54. package/dist/hooks/pre-compact.js +1061 -244
  55. package/dist/hooks/pre-tool-use.js +787 -130
  56. package/dist/hooks/prompt-ingest-worker.js +242 -101
  57. package/dist/hooks/prompt-submit.js +1121 -299
  58. package/dist/hooks/response-ingest-worker.js +242 -101
  59. package/dist/hooks/session-end.js +4063 -397
  60. package/dist/hooks/session-start.js +1071 -254
  61. package/dist/hooks/stop.js +768 -120
  62. package/dist/hooks/subagent-stop.js +757 -109
  63. package/dist/hooks/summary-worker.js +1706 -1011
  64. package/dist/index.js +1821 -1098
  65. package/dist/lib/agent-config.js +167 -0
  66. package/dist/lib/cloud-sync.js +932 -88
  67. package/dist/lib/consolidation.js +2 -1
  68. package/dist/lib/database.js +642 -87
  69. package/dist/lib/db-daemon-client.js +503 -0
  70. package/dist/lib/device-registry.js +547 -7
  71. package/dist/lib/embedder.js +14 -28
  72. package/dist/lib/employee-templates.js +84 -74
  73. package/dist/lib/employees.js +9 -0
  74. package/dist/lib/exe-daemon-client.js +16 -29
  75. package/dist/lib/exe-daemon.js +2733 -1575
  76. package/dist/lib/hybrid-search.js +995 -228
  77. package/dist/lib/identity.js +87 -67
  78. package/dist/lib/keychain.js +9 -1
  79. package/dist/lib/messaging.js +103 -40
  80. package/dist/lib/reminders.js +91 -74
  81. package/dist/lib/runtime-table.js +16 -0
  82. package/dist/lib/schedules.js +96 -2
  83. package/dist/lib/session-wrappers.js +22 -0
  84. package/dist/lib/skill-learning.js +103 -85
  85. package/dist/lib/store.js +234 -73
  86. package/dist/lib/tasks.js +348 -134
  87. package/dist/lib/tmux-routing.js +422 -208
  88. package/dist/lib/token-spend.js +273 -0
  89. package/dist/lib/ws-client.js +11 -0
  90. package/dist/mcp/server.js +5742 -696
  91. package/dist/mcp/tools/complete-reminder.js +94 -77
  92. package/dist/mcp/tools/create-reminder.js +94 -77
  93. package/dist/mcp/tools/create-task.js +375 -152
  94. package/dist/mcp/tools/deactivate-behavior.js +95 -77
  95. package/dist/mcp/tools/list-reminders.js +94 -77
  96. package/dist/mcp/tools/list-tasks.js +99 -31
  97. package/dist/mcp/tools/send-message.js +108 -45
  98. package/dist/mcp/tools/update-task.js +162 -77
  99. package/dist/runtime/index.js +1075 -258
  100. package/dist/tui/App.js +1333 -506
  101. package/package.json +6 -1
  102. package/src/commands/exe/agent-config.md +27 -0
  103. package/src/commands/exe/cc-doctor.md +10 -0
@@ -371,6 +371,12 @@ function getClient() {
371
371
  if (!_resilientClient) {
372
372
  throw new Error("Database client not initialized. Call initDatabase() first.");
373
373
  }
374
+ if (process.env.EXE_IS_DAEMON === "1") {
375
+ return _resilientClient;
376
+ }
377
+ if (_daemonClient && _daemonClient._isDaemonActive()) {
378
+ return _daemonClient;
379
+ }
374
380
  return _resilientClient;
375
381
  }
376
382
  function getRawClient() {
@@ -859,6 +865,12 @@ async function ensureSchema() {
859
865
  } catch {
860
866
  }
861
867
  }
868
+ try {
869
+ await client.execute(
870
+ `CREATE INDEX IF NOT EXISTS idx_memories_content_hash ON memories(content_hash, agent_id)`
871
+ );
872
+ } catch {
873
+ }
862
874
  await client.executeMultiple(`
863
875
  CREATE TABLE IF NOT EXISTS entities (
864
876
  id TEXT PRIMARY KEY,
@@ -911,7 +923,30 @@ async function ensureSchema() {
911
923
  entity_id TEXT NOT NULL,
912
924
  PRIMARY KEY (hyperedge_id, entity_id)
913
925
  );
926
+
927
+ CREATE VIRTUAL TABLE IF NOT EXISTS entities_fts USING fts5(
928
+ name,
929
+ content=entities,
930
+ content_rowid=rowid
931
+ );
932
+
933
+ CREATE TRIGGER IF NOT EXISTS entities_fts_ai AFTER INSERT ON entities BEGIN
934
+ INSERT INTO entities_fts(rowid, name) VALUES (new.rowid, new.name);
935
+ END;
936
+
937
+ CREATE TRIGGER IF NOT EXISTS entities_fts_ad AFTER DELETE ON entities BEGIN
938
+ INSERT INTO entities_fts(entities_fts, rowid, name) VALUES('delete', old.rowid, old.name);
939
+ END;
940
+
941
+ CREATE TRIGGER IF NOT EXISTS entities_fts_au AFTER UPDATE ON entities BEGIN
942
+ INSERT INTO entities_fts(entities_fts, rowid, name) VALUES('delete', old.rowid, old.name);
943
+ INSERT INTO entities_fts(rowid, name) VALUES (new.rowid, new.name);
944
+ END;
914
945
  `);
946
+ try {
947
+ await client.execute("INSERT INTO entities_fts(entities_fts) VALUES('rebuild')");
948
+ } catch {
949
+ }
915
950
  await client.executeMultiple(`
916
951
  CREATE TABLE IF NOT EXISTS entity_aliases (
917
952
  alias TEXT NOT NULL PRIMARY KEY,
@@ -1092,6 +1127,33 @@ async function ensureSchema() {
1092
1127
  CREATE INDEX IF NOT EXISTS idx_conversations_channel
1093
1128
  ON conversations(channel_id);
1094
1129
  `);
1130
+ await client.executeMultiple(`
1131
+ CREATE TABLE IF NOT EXISTS session_agent_map (
1132
+ session_uuid TEXT PRIMARY KEY,
1133
+ agent_id TEXT NOT NULL,
1134
+ session_name TEXT,
1135
+ task_id TEXT,
1136
+ project_name TEXT,
1137
+ started_at TEXT NOT NULL
1138
+ );
1139
+
1140
+ CREATE INDEX IF NOT EXISTS idx_session_agent_map_agent
1141
+ ON session_agent_map(agent_id);
1142
+ `);
1143
+ try {
1144
+ const mapCount = await client.execute({ sql: `SELECT COUNT(*) as cnt FROM session_agent_map`, args: [] });
1145
+ if (Number(mapCount.rows[0]?.cnt ?? 0) === 0) {
1146
+ await client.execute({
1147
+ sql: `INSERT OR IGNORE INTO session_agent_map (session_uuid, agent_id, session_name, started_at)
1148
+ SELECT session_id, agent_id, '', MIN(timestamp)
1149
+ FROM memories
1150
+ WHERE session_id IS NOT NULL AND session_id != '' AND agent_id IS NOT NULL AND agent_id != ''
1151
+ GROUP BY session_id, agent_id`,
1152
+ args: []
1153
+ });
1154
+ }
1155
+ } catch {
1156
+ }
1095
1157
  try {
1096
1158
  await client.execute({
1097
1159
  sql: `ALTER TABLE tasks ADD COLUMN budget_tokens INTEGER`,
@@ -1225,15 +1287,41 @@ async function ensureSchema() {
1225
1287
  });
1226
1288
  } catch {
1227
1289
  }
1290
+ for (const col of [
1291
+ "ALTER TABLE memories ADD COLUMN intent TEXT",
1292
+ "ALTER TABLE memories ADD COLUMN outcome TEXT",
1293
+ "ALTER TABLE memories ADD COLUMN domain TEXT",
1294
+ "ALTER TABLE memories ADD COLUMN referenced_entities TEXT",
1295
+ "ALTER TABLE memories ADD COLUMN retrieval_count INTEGER DEFAULT 0",
1296
+ "ALTER TABLE memories ADD COLUMN chain_position TEXT",
1297
+ "ALTER TABLE memories ADD COLUMN review_status TEXT",
1298
+ "ALTER TABLE memories ADD COLUMN context_window_pct INTEGER",
1299
+ "ALTER TABLE memories ADD COLUMN file_paths TEXT",
1300
+ "ALTER TABLE memories ADD COLUMN commit_hash TEXT",
1301
+ "ALTER TABLE memories ADD COLUMN duration_ms INTEGER",
1302
+ "ALTER TABLE memories ADD COLUMN token_cost REAL",
1303
+ "ALTER TABLE memories ADD COLUMN audience TEXT",
1304
+ "ALTER TABLE memories ADD COLUMN language_type TEXT",
1305
+ "ALTER TABLE memories ADD COLUMN parent_memory_id TEXT"
1306
+ ]) {
1307
+ try {
1308
+ await client.execute(col);
1309
+ } catch {
1310
+ }
1311
+ }
1228
1312
  }
1229
1313
  async function disposeDatabase() {
1314
+ if (_daemonClient) {
1315
+ _daemonClient.close();
1316
+ _daemonClient = null;
1317
+ }
1230
1318
  if (_client) {
1231
1319
  _client.close();
1232
1320
  _client = null;
1233
1321
  _resilientClient = null;
1234
1322
  }
1235
1323
  }
1236
- var _client, _resilientClient, initTurso, disposeTurso;
1324
+ var _client, _resilientClient, _daemonClient, initTurso, disposeTurso;
1237
1325
  var init_database = __esm({
1238
1326
  "src/lib/database.ts"() {
1239
1327
  "use strict";
@@ -1241,6 +1329,7 @@ var init_database = __esm({
1241
1329
  init_employees();
1242
1330
  _client = null;
1243
1331
  _resilientClient = null;
1332
+ _daemonClient = null;
1244
1333
  initTurso = initDatabase;
1245
1334
  disposeTurso = disposeDatabase;
1246
1335
  }
@@ -1277,12 +1366,20 @@ async function getMasterKey() {
1277
1366
  }
1278
1367
  const keyPath = getKeyPath();
1279
1368
  if (!existsSync3(keyPath)) {
1369
+ process.stderr.write(
1370
+ `[keychain] Key not found at ${keyPath} (HOME=${os3.homedir()}, EXE_OS_DIR=${process.env.EXE_OS_DIR ?? "unset"})
1371
+ `
1372
+ );
1280
1373
  return null;
1281
1374
  }
1282
1375
  try {
1283
1376
  const content = await readFile3(keyPath, "utf-8");
1284
1377
  return Buffer.from(content.trim(), "base64");
1285
- } catch {
1378
+ } catch (err) {
1379
+ process.stderr.write(
1380
+ `[keychain] Key read failed at ${keyPath}: ${err instanceof Error ? err.message : String(err)}
1381
+ `
1382
+ );
1286
1383
  return null;
1287
1384
  }
1288
1385
  }
@@ -1793,6 +1890,7 @@ __export(store_exports, {
1793
1890
  vectorToBlob: () => vectorToBlob,
1794
1891
  writeMemory: () => writeMemory
1795
1892
  });
1893
+ import { createHash } from "crypto";
1796
1894
  function isBusyError2(err) {
1797
1895
  if (err instanceof Error) {
1798
1896
  const msg = err.message.toLowerCase();
@@ -1866,12 +1964,52 @@ function classifyTier(record) {
1866
1964
  if (["store_memory", "manual"].includes(record.tool_name ?? "") && (record.importance ?? 0) >= 5) return 2;
1867
1965
  return 3;
1868
1966
  }
1967
+ function inferFilePaths(record) {
1968
+ if (!["Read", "Write", "Edit"].includes(record.tool_name)) return null;
1969
+ const firstLine = record.raw_text.split("\n")[0] ?? "";
1970
+ const match = firstLine.match(/(\/[\w./-]+\.\w+)/);
1971
+ return match ? JSON.stringify([match[1]]) : null;
1972
+ }
1973
+ function inferCommitHash(record) {
1974
+ if (record.tool_name !== "Bash") return null;
1975
+ const match = record.raw_text.match(/\b([a-f0-9]{7,40})\b/);
1976
+ return match ? match[1] : null;
1977
+ }
1978
+ function inferLanguageType(record) {
1979
+ const text = record.raw_text;
1980
+ if (!text || text.length < 10) return null;
1981
+ const trimmed = text.trimStart();
1982
+ if (trimmed.startsWith("{") || trimmed.startsWith("[")) return "json";
1983
+ if (/\b(SELECT|INSERT|UPDATE|DELETE|CREATE TABLE|ALTER TABLE)\b/i.test(text)) return "sql";
1984
+ if (/\b(function |const |import |export |class |def |async |=>)\b/.test(text)) return "code";
1985
+ if (trimmed.startsWith("#") || trimmed.startsWith("*")) return "prose";
1986
+ return "mixed";
1987
+ }
1988
+ function inferDomain(record) {
1989
+ const proj = (record.project_name ?? "").toLowerCase();
1990
+ if (proj.includes("marketing") || proj.includes("content")) return "marketing";
1991
+ if (proj.includes("crm") || proj.includes("customer")) return "customer";
1992
+ return null;
1993
+ }
1869
1994
  async function writeMemory(record) {
1870
1995
  if (record.vector !== null && record.vector.length !== EMBEDDING_DIM) {
1871
1996
  throw new Error(
1872
1997
  `Expected ${EMBEDDING_DIM}-dim vector, got ${record.vector.length}`
1873
1998
  );
1874
1999
  }
2000
+ const contentHash = createHash("md5").update(record.raw_text).digest("hex");
2001
+ if (_pendingRecords.some((r) => r.content_hash === contentHash && r.agent_id === record.agent_id)) {
2002
+ return;
2003
+ }
2004
+ try {
2005
+ const client = getClient();
2006
+ const existing = await client.execute({
2007
+ sql: "SELECT id FROM memories WHERE content_hash = ? AND agent_id = ? LIMIT 1",
2008
+ args: [contentHash, record.agent_id]
2009
+ });
2010
+ if (existing.rows.length > 0) return;
2011
+ } catch {
2012
+ }
1875
2013
  const dbRow = {
1876
2014
  id: record.id,
1877
2015
  agent_id: record.agent_id,
@@ -1901,7 +2039,23 @@ async function writeMemory(record) {
1901
2039
  supersedes_id: record.supersedes_id ?? null,
1902
2040
  draft: record.draft ? 1 : 0,
1903
2041
  memory_type: record.memory_type ?? "raw",
1904
- trajectory: record.trajectory ? JSON.stringify(record.trajectory) : null
2042
+ trajectory: record.trajectory ? JSON.stringify(record.trajectory) : null,
2043
+ content_hash: contentHash,
2044
+ intent: record.intent ?? null,
2045
+ outcome: record.outcome ?? null,
2046
+ domain: record.domain ?? inferDomain(record),
2047
+ referenced_entities: record.referenced_entities ?? null,
2048
+ retrieval_count: record.retrieval_count ?? 0,
2049
+ chain_position: record.chain_position ?? null,
2050
+ review_status: record.review_status ?? null,
2051
+ context_window_pct: record.context_window_pct ?? null,
2052
+ file_paths: record.file_paths ?? inferFilePaths(record),
2053
+ commit_hash: record.commit_hash ?? inferCommitHash(record),
2054
+ duration_ms: record.duration_ms ?? null,
2055
+ token_cost: record.token_cost ?? null,
2056
+ audience: record.audience ?? null,
2057
+ language_type: record.language_type ?? inferLanguageType(record),
2058
+ parent_memory_id: record.parent_memory_id ?? null
1905
2059
  };
1906
2060
  _pendingRecords.push(dbRow);
1907
2061
  orgBus.emit({
@@ -1959,80 +2113,85 @@ async function flushBatch() {
1959
2113
  const draft = row.draft ? 1 : 0;
1960
2114
  const memoryType = row.memory_type ?? "raw";
1961
2115
  const trajectory = row.trajectory ?? null;
1962
- return {
1963
- sql: hasVector ? `INSERT OR IGNORE INTO memories
1964
- (id, agent_id, agent_role, session_id, timestamp,
1965
- tool_name, project_name,
1966
- has_error, raw_text, vector, version, task_id, importance, status,
1967
- confidence, last_accessed,
1968
- workspace_id, document_id, user_id, char_offset, page_number,
1969
- source_path, source_type, tier, supersedes_id, draft, memory_type, trajectory)
1970
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, vector32(?), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` : `INSERT OR IGNORE INTO memories
1971
- (id, agent_id, agent_role, session_id, timestamp,
2116
+ const contentHash = row.content_hash ?? null;
2117
+ const intent = row.intent ?? null;
2118
+ const outcome = row.outcome ?? null;
2119
+ const domain = row.domain ?? null;
2120
+ const referencedEntities = row.referenced_entities ?? null;
2121
+ const retrievalCount = row.retrieval_count ?? 0;
2122
+ const chainPosition = row.chain_position ?? null;
2123
+ const reviewStatus = row.review_status ?? null;
2124
+ const contextWindowPct = row.context_window_pct ?? null;
2125
+ const filePaths = row.file_paths ?? null;
2126
+ const commitHash = row.commit_hash ?? null;
2127
+ const durationMs = row.duration_ms ?? null;
2128
+ const tokenCost = row.token_cost ?? null;
2129
+ const audience = row.audience ?? null;
2130
+ const languageType = row.language_type ?? null;
2131
+ const parentMemoryId = row.parent_memory_id ?? null;
2132
+ const cols = `id, agent_id, agent_role, session_id, timestamp,
1972
2133
  tool_name, project_name,
1973
2134
  has_error, raw_text, vector, version, task_id, importance, status,
1974
2135
  confidence, last_accessed,
1975
2136
  workspace_id, document_id, user_id, char_offset, page_number,
1976
- source_path, source_type, tier, supersedes_id, draft, memory_type, trajectory)
1977
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
1978
- args: hasVector ? [
1979
- row.id,
1980
- row.agent_id,
1981
- row.agent_role,
1982
- row.session_id,
1983
- row.timestamp,
1984
- row.tool_name,
1985
- row.project_name,
1986
- row.has_error,
1987
- row.raw_text,
1988
- vectorToBlob(row.vector),
1989
- row.version,
1990
- taskId,
1991
- importance,
1992
- status,
1993
- confidence,
1994
- lastAccessed,
1995
- workspaceId,
1996
- documentId,
1997
- userId,
1998
- charOffset,
1999
- pageNumber,
2000
- sourcePath,
2001
- sourceType,
2002
- tier,
2003
- supersedesId,
2004
- draft,
2005
- memoryType,
2006
- trajectory
2007
- ] : [
2008
- row.id,
2009
- row.agent_id,
2010
- row.agent_role,
2011
- row.session_id,
2012
- row.timestamp,
2013
- row.tool_name,
2014
- row.project_name,
2015
- row.has_error,
2016
- row.raw_text,
2017
- row.version,
2018
- taskId,
2019
- importance,
2020
- status,
2021
- confidence,
2022
- lastAccessed,
2023
- workspaceId,
2024
- documentId,
2025
- userId,
2026
- charOffset,
2027
- pageNumber,
2028
- sourcePath,
2029
- sourceType,
2030
- tier,
2031
- supersedesId,
2032
- draft,
2033
- memoryType,
2034
- trajectory
2035
- ]
2137
+ source_path, source_type, tier, supersedes_id, draft, memory_type, trajectory, content_hash,
2138
+ intent, outcome, domain, referenced_entities, retrieval_count,
2139
+ chain_position, review_status, context_window_pct, file_paths, commit_hash,
2140
+ duration_ms, token_cost, audience, language_type, parent_memory_id`;
2141
+ const metaArgs = [
2142
+ intent,
2143
+ outcome,
2144
+ domain,
2145
+ referencedEntities,
2146
+ retrievalCount,
2147
+ chainPosition,
2148
+ reviewStatus,
2149
+ contextWindowPct,
2150
+ filePaths,
2151
+ commitHash,
2152
+ durationMs,
2153
+ tokenCost,
2154
+ audience,
2155
+ languageType,
2156
+ parentMemoryId
2157
+ ];
2158
+ const baseArgs = [
2159
+ row.id,
2160
+ row.agent_id,
2161
+ row.agent_role,
2162
+ row.session_id,
2163
+ row.timestamp,
2164
+ row.tool_name,
2165
+ row.project_name,
2166
+ row.has_error,
2167
+ row.raw_text
2168
+ ];
2169
+ const sharedArgs = [
2170
+ row.version,
2171
+ taskId,
2172
+ importance,
2173
+ status,
2174
+ confidence,
2175
+ lastAccessed,
2176
+ workspaceId,
2177
+ documentId,
2178
+ userId,
2179
+ charOffset,
2180
+ pageNumber,
2181
+ sourcePath,
2182
+ sourceType,
2183
+ tier,
2184
+ supersedesId,
2185
+ draft,
2186
+ memoryType,
2187
+ trajectory,
2188
+ contentHash
2189
+ ];
2190
+ return {
2191
+ sql: hasVector ? `INSERT OR IGNORE INTO memories (${cols})
2192
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, vector32(?), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` : `INSERT OR IGNORE INTO memories (${cols})
2193
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
2194
+ args: hasVector ? [...baseArgs, vectorToBlob(row.vector), ...sharedArgs, ...metaArgs] : [...baseArgs, ...sharedArgs, ...metaArgs]
2036
2195
  };
2037
2196
  };
2038
2197
  const globalClient = getClient();
@@ -2371,12 +2530,141 @@ var init_self_query_router = __esm({
2371
2530
  }
2372
2531
  });
2373
2532
 
2533
+ // src/lib/reranker.ts
2534
+ var reranker_exports = {};
2535
+ __export(reranker_exports, {
2536
+ disposeReranker: () => disposeReranker,
2537
+ getRerankerModelPath: () => getRerankerModelPath,
2538
+ isRerankerAvailable: () => isRerankerAvailable,
2539
+ rerank: () => rerank,
2540
+ rerankWithContext: () => rerankWithContext,
2541
+ rerankWithScores: () => rerankWithScores
2542
+ });
2543
+ import path5 from "path";
2544
+ import { existsSync as existsSync5 } from "fs";
2545
+ function resetIdleTimer() {
2546
+ if (_idleTimer) clearTimeout(_idleTimer);
2547
+ _idleTimer = setTimeout(() => {
2548
+ void disposeReranker();
2549
+ }, IDLE_TIMEOUT_MS);
2550
+ if (_idleTimer && typeof _idleTimer === "object" && "unref" in _idleTimer) {
2551
+ _idleTimer.unref();
2552
+ }
2553
+ }
2554
+ function isRerankerAvailable() {
2555
+ return existsSync5(path5.join(MODELS_DIR, RERANKER_MODEL_FILE));
2556
+ }
2557
+ function getRerankerModelPath() {
2558
+ return path5.join(MODELS_DIR, RERANKER_MODEL_FILE);
2559
+ }
2560
+ async function ensureLoaded() {
2561
+ if (_rerankerContext) {
2562
+ resetIdleTimer();
2563
+ return;
2564
+ }
2565
+ const modelPath = path5.join(MODELS_DIR, RERANKER_MODEL_FILE);
2566
+ if (!existsSync5(modelPath)) {
2567
+ throw new Error(
2568
+ `Reranker model not found at ${modelPath}. Run /exe-setup to download it.`
2569
+ );
2570
+ }
2571
+ process.stderr.write("[reranker] Loading Jina Reranker v3...\n");
2572
+ const { getLlama } = await import("node-llama-cpp");
2573
+ const llama = await getLlama();
2574
+ _rerankerModel = await llama.loadModel({ modelPath });
2575
+ _rerankerContext = await _rerankerModel.createEmbeddingContext();
2576
+ process.stderr.write("[reranker] Jina Reranker v3 loaded.\n");
2577
+ resetIdleTimer();
2578
+ }
2579
+ async function disposeReranker() {
2580
+ if (_idleTimer) {
2581
+ clearTimeout(_idleTimer);
2582
+ _idleTimer = null;
2583
+ }
2584
+ if (_rerankerContext) {
2585
+ try {
2586
+ await _rerankerContext.dispose();
2587
+ } catch {
2588
+ }
2589
+ _rerankerContext = null;
2590
+ }
2591
+ if (_rerankerModel) {
2592
+ try {
2593
+ await _rerankerModel.dispose();
2594
+ } catch {
2595
+ }
2596
+ _rerankerModel = null;
2597
+ }
2598
+ process.stderr.write("[reranker] Unloaded (idle timeout).\n");
2599
+ }
2600
+ async function rerankWithScores(query, texts, topK) {
2601
+ if (texts.length === 0) return [];
2602
+ await ensureLoaded();
2603
+ const ctx = _rerankerContext;
2604
+ const scored = [];
2605
+ for (let i = 0; i < texts.length; i++) {
2606
+ const text = texts[i] ?? "";
2607
+ try {
2608
+ const input = `query: ${query} document: ${text.slice(0, 512)}`;
2609
+ const embedding = await ctx.getEmbeddingFor(input);
2610
+ const score = embedding.vector[0] ?? 0;
2611
+ scored.push({ text, score, index: i });
2612
+ } catch {
2613
+ scored.push({ text, score: -1, index: i });
2614
+ }
2615
+ }
2616
+ scored.sort((a, b) => b.score - a.score);
2617
+ return typeof topK === "number" ? scored.slice(0, topK) : scored;
2618
+ }
2619
+ async function rerank(query, candidates, topK = 5) {
2620
+ if (candidates.length === 0) return [];
2621
+ if (candidates.length <= topK) return candidates;
2622
+ const scored = await rerankWithScores(
2623
+ query,
2624
+ candidates.map((c) => c.raw_text),
2625
+ topK
2626
+ );
2627
+ return scored.map((s) => candidates[s.index]);
2628
+ }
2629
+ async function rerankWithContext(query, candidates, topK) {
2630
+ if (candidates.length === 0) return [];
2631
+ await ensureLoaded();
2632
+ const ctx = _rerankerContext;
2633
+ const scored = [];
2634
+ for (let i = 0; i < candidates.length; i++) {
2635
+ const candidate = candidates[i];
2636
+ try {
2637
+ const docText = candidate.context ? `[${candidate.context}] ${candidate.text.slice(0, 460)}` : candidate.text.slice(0, 512);
2638
+ const input = `query: ${query} document: ${docText}`;
2639
+ const embedding = await ctx.getEmbeddingFor(input);
2640
+ const score = embedding.vector[0] ?? 0;
2641
+ scored.push({ text: candidate.text, score, index: i });
2642
+ } catch {
2643
+ scored.push({ text: candidate.text, score: -1, index: i });
2644
+ }
2645
+ }
2646
+ scored.sort((a, b) => b.score - a.score);
2647
+ return typeof topK === "number" ? scored.slice(0, topK) : scored;
2648
+ }
2649
+ var RERANKER_MODEL_FILE, IDLE_TIMEOUT_MS, _rerankerContext, _rerankerModel, _idleTimer;
2650
+ var init_reranker = __esm({
2651
+ "src/lib/reranker.ts"() {
2652
+ "use strict";
2653
+ init_config();
2654
+ RERANKER_MODEL_FILE = "jina-reranker-v3-q4_k_m.gguf";
2655
+ IDLE_TIMEOUT_MS = 6e4;
2656
+ _rerankerContext = null;
2657
+ _rerankerModel = null;
2658
+ _idleTimer = null;
2659
+ }
2660
+ });
2661
+
2374
2662
  // src/lib/exe-daemon-client.ts
2375
2663
  import net from "net";
2376
2664
  import { spawn } from "child_process";
2377
2665
  import { randomUUID as randomUUID2 } from "crypto";
2378
- import { existsSync as existsSync5, unlinkSync as unlinkSync2, readFileSync as readFileSync3, openSync, closeSync, statSync } from "fs";
2379
- import path5 from "path";
2666
+ import { existsSync as existsSync6, unlinkSync as unlinkSync2, readFileSync as readFileSync3, openSync, closeSync, statSync } from "fs";
2667
+ import path6 from "path";
2380
2668
  import { fileURLToPath } from "url";
2381
2669
  function handleData(chunk) {
2382
2670
  _buffer += chunk.toString();
@@ -2391,10 +2679,12 @@ function handleData(chunk) {
2391
2679
  if (!line) continue;
2392
2680
  try {
2393
2681
  const response = JSON.parse(line);
2394
- const entry = _pending.get(response.id);
2682
+ const id = response.id;
2683
+ if (!id) continue;
2684
+ const entry = _pending.get(id);
2395
2685
  if (entry) {
2396
2686
  clearTimeout(entry.timer);
2397
- _pending.delete(response.id);
2687
+ _pending.delete(id);
2398
2688
  entry.resolve(response);
2399
2689
  }
2400
2690
  } catch {
@@ -2402,7 +2692,7 @@ function handleData(chunk) {
2402
2692
  }
2403
2693
  }
2404
2694
  function cleanupStaleFiles() {
2405
- if (existsSync5(PID_PATH)) {
2695
+ if (existsSync6(PID_PATH)) {
2406
2696
  try {
2407
2697
  const pid = parseInt(readFileSync3(PID_PATH, "utf8").trim(), 10);
2408
2698
  if (pid > 0) {
@@ -2425,11 +2715,11 @@ function cleanupStaleFiles() {
2425
2715
  }
2426
2716
  }
2427
2717
  function findPackageRoot() {
2428
- let dir = path5.dirname(fileURLToPath(import.meta.url));
2429
- const { root } = path5.parse(dir);
2718
+ let dir = path6.dirname(fileURLToPath(import.meta.url));
2719
+ const { root } = path6.parse(dir);
2430
2720
  while (dir !== root) {
2431
- if (existsSync5(path5.join(dir, "package.json"))) return dir;
2432
- dir = path5.dirname(dir);
2721
+ if (existsSync6(path6.join(dir, "package.json"))) return dir;
2722
+ dir = path6.dirname(dir);
2433
2723
  }
2434
2724
  return null;
2435
2725
  }
@@ -2439,8 +2729,8 @@ function spawnDaemon() {
2439
2729
  process.stderr.write("[exed-client] WARN: cannot find package root\n");
2440
2730
  return;
2441
2731
  }
2442
- const daemonPath = path5.join(pkgRoot, "dist", "lib", "exe-daemon.js");
2443
- if (!existsSync5(daemonPath)) {
2732
+ const daemonPath = path6.join(pkgRoot, "dist", "lib", "exe-daemon.js");
2733
+ if (!existsSync6(daemonPath)) {
2444
2734
  process.stderr.write(`[exed-client] WARN: daemon script not found at ${daemonPath}
2445
2735
  `);
2446
2736
  return;
@@ -2448,7 +2738,7 @@ function spawnDaemon() {
2448
2738
  const resolvedPath = daemonPath;
2449
2739
  process.stderr.write(`[exed-client] Spawning daemon: ${resolvedPath}
2450
2740
  `);
2451
- const logPath = path5.join(path5.dirname(SOCKET_PATH), "exed.log");
2741
+ const logPath = path6.join(path6.dirname(SOCKET_PATH), "exed.log");
2452
2742
  let stderrFd = "ignore";
2453
2743
  try {
2454
2744
  stderrFd = openSync(logPath, "a");
@@ -2565,6 +2855,9 @@ async function connectEmbedDaemon() {
2565
2855
  return false;
2566
2856
  }
2567
2857
  function sendRequest(texts, priority) {
2858
+ return sendDaemonRequest({ texts, priority });
2859
+ }
2860
+ function sendDaemonRequest(payload, timeoutMs = REQUEST_TIMEOUT_MS) {
2568
2861
  return new Promise((resolve) => {
2569
2862
  if (!_socket || !_connected) {
2570
2863
  resolve({ error: "Not connected" });
@@ -2574,10 +2867,10 @@ function sendRequest(texts, priority) {
2574
2867
  const timer = setTimeout(() => {
2575
2868
  _pending.delete(id);
2576
2869
  resolve({ error: "Request timeout" });
2577
- }, REQUEST_TIMEOUT_MS);
2870
+ }, timeoutMs);
2578
2871
  _pending.set(id, { resolve, timer });
2579
2872
  try {
2580
- _socket.write(JSON.stringify({ id, texts, priority }) + "\n");
2873
+ _socket.write(JSON.stringify({ id, ...payload }) + "\n");
2581
2874
  } catch {
2582
2875
  clearTimeout(timer);
2583
2876
  _pending.delete(id);
@@ -2587,34 +2880,15 @@ function sendRequest(texts, priority) {
2587
2880
  }
2588
2881
  async function pingDaemon() {
2589
2882
  if (!_socket || !_connected) return null;
2590
- return new Promise((resolve) => {
2591
- const id = randomUUID2();
2592
- const timer = setTimeout(() => {
2593
- _pending.delete(id);
2594
- resolve(null);
2595
- }, 5e3);
2596
- _pending.set(id, {
2597
- resolve: (data) => {
2598
- if (data.health) {
2599
- resolve(data.health);
2600
- } else {
2601
- resolve(null);
2602
- }
2603
- },
2604
- timer
2605
- });
2606
- try {
2607
- _socket.write(JSON.stringify({ id, type: "health" }) + "\n");
2608
- } catch {
2609
- clearTimeout(timer);
2610
- _pending.delete(id);
2611
- resolve(null);
2612
- }
2613
- });
2883
+ const response = await sendDaemonRequest({ type: "health" }, 5e3);
2884
+ if (response.health) {
2885
+ return response.health;
2886
+ }
2887
+ return null;
2614
2888
  }
2615
2889
  function killAndRespawnDaemon() {
2616
2890
  process.stderr.write("[exed-client] Killing daemon for restart...\n");
2617
- if (existsSync5(PID_PATH)) {
2891
+ if (existsSync6(PID_PATH)) {
2618
2892
  try {
2619
2893
  const pid = parseInt(readFileSync3(PID_PATH, "utf8").trim(), 10);
2620
2894
  if (pid > 0) {
@@ -2700,9 +2974,9 @@ var init_exe_daemon_client = __esm({
2700
2974
  "src/lib/exe-daemon-client.ts"() {
2701
2975
  "use strict";
2702
2976
  init_config();
2703
- SOCKET_PATH = process.env.EXE_DAEMON_SOCK ?? process.env.EXE_EMBED_SOCK ?? path5.join(EXE_AI_DIR, "exed.sock");
2704
- PID_PATH = process.env.EXE_DAEMON_PID ?? process.env.EXE_EMBED_PID ?? path5.join(EXE_AI_DIR, "exed.pid");
2705
- SPAWN_LOCK_PATH = path5.join(EXE_AI_DIR, "exed-spawn.lock");
2977
+ SOCKET_PATH = process.env.EXE_DAEMON_SOCK ?? process.env.EXE_EMBED_SOCK ?? path6.join(EXE_AI_DIR, "exed.sock");
2978
+ PID_PATH = process.env.EXE_DAEMON_PID ?? process.env.EXE_EMBED_PID ?? path6.join(EXE_AI_DIR, "exed.pid");
2979
+ SPAWN_LOCK_PATH = path6.join(EXE_AI_DIR, "exed-spawn.lock");
2706
2980
  SPAWN_LOCK_STALE_MS = 3e4;
2707
2981
  CONNECT_TIMEOUT_MS = 15e3;
2708
2982
  REQUEST_TIMEOUT_MS = 3e4;
@@ -2791,7 +3065,7 @@ __export(project_name_exports, {
2791
3065
  getProjectName: () => getProjectName
2792
3066
  });
2793
3067
  import { execSync as execSync2 } from "child_process";
2794
- import path6 from "path";
3068
+ import path7 from "path";
2795
3069
  function getProjectName(cwd) {
2796
3070
  const dir = cwd ?? process.cwd();
2797
3071
  if (_cached && _cachedCwd === dir) return _cached;
@@ -2804,7 +3078,7 @@ function getProjectName(cwd) {
2804
3078
  timeout: 2e3,
2805
3079
  stdio: ["pipe", "pipe", "pipe"]
2806
3080
  }).trim();
2807
- repoRoot = path6.dirname(gitCommonDir);
3081
+ repoRoot = path7.dirname(gitCommonDir);
2808
3082
  } catch {
2809
3083
  repoRoot = execSync2("git rev-parse --show-toplevel", {
2810
3084
  cwd: dir,
@@ -2813,11 +3087,11 @@ function getProjectName(cwd) {
2813
3087
  stdio: ["pipe", "pipe", "pipe"]
2814
3088
  }).trim();
2815
3089
  }
2816
- _cached = path6.basename(repoRoot);
3090
+ _cached = path7.basename(repoRoot);
2817
3091
  _cachedCwd = dir;
2818
3092
  return _cached;
2819
3093
  } catch {
2820
- _cached = path6.basename(dir);
3094
+ _cached = path7.basename(dir);
2821
3095
  _cachedCwd = dir;
2822
3096
  return _cached;
2823
3097
  }
@@ -2841,8 +3115,8 @@ __export(file_grep_exports, {
2841
3115
  grepProjectFiles: () => grepProjectFiles
2842
3116
  });
2843
3117
  import { execSync as execSync3 } from "child_process";
2844
- import { readFileSync as readFileSync4, readdirSync as readdirSync2, statSync as statSync2, existsSync as existsSync6 } from "fs";
2845
- import path7 from "path";
3118
+ import { readFileSync as readFileSync4, readdirSync as readdirSync2, statSync as statSync2, existsSync as existsSync7 } from "fs";
3119
+ import path8 from "path";
2846
3120
  import crypto from "crypto";
2847
3121
  function hasRipgrep() {
2848
3122
  if (_hasRg === null) {
@@ -2882,7 +3156,7 @@ async function grepProjectFiles(query, projectRoot, options) {
2882
3156
  session_id: "file-grep",
2883
3157
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2884
3158
  tool_name: "file_grep",
2885
- project_name: path7.basename(projectRoot),
3159
+ project_name: path8.basename(projectRoot),
2886
3160
  has_error: false,
2887
3161
  raw_text: `${prefix} ${buildSnippet(hit, projectRoot)}`,
2888
3162
  vector: null,
@@ -2956,7 +3230,7 @@ function grepWithNodeFs(pattern, projectRoot, patterns) {
2956
3230
  const files = collectFiles(projectRoot, patterns ?? DEFAULT_PATTERNS);
2957
3231
  const hits = [];
2958
3232
  for (const filePath of files.slice(0, MAX_FILES)) {
2959
- const absPath = path7.join(projectRoot, filePath);
3233
+ const absPath = path8.join(projectRoot, filePath);
2960
3234
  try {
2961
3235
  const stat = statSync2(absPath);
2962
3236
  if (stat.size > MAX_FILE_SIZE) continue;
@@ -2983,15 +3257,15 @@ function collectFiles(root, patterns) {
2983
3257
  const files = [];
2984
3258
  function walk(dir, relative) {
2985
3259
  if (files.length >= MAX_FILES) return;
2986
- const basename = path7.basename(dir);
3260
+ const basename = path8.basename(dir);
2987
3261
  if (EXCLUDE_DIRS.includes(basename)) return;
2988
3262
  try {
2989
3263
  const entries = readdirSync2(dir, { withFileTypes: true });
2990
3264
  for (const entry of entries) {
2991
3265
  if (files.length >= MAX_FILES) return;
2992
- const rel = path7.join(relative, entry.name);
3266
+ const rel = path8.join(relative, entry.name);
2993
3267
  if (entry.isDirectory()) {
2994
- walk(path7.join(dir, entry.name), rel);
3268
+ walk(path8.join(dir, entry.name), rel);
2995
3269
  } else if (entry.isFile()) {
2996
3270
  for (const pat of patterns) {
2997
3271
  if (matchGlob(rel, pat)) {
@@ -3023,7 +3297,7 @@ function matchGlob(filePath, pattern) {
3023
3297
  if (slashIdx !== -1) {
3024
3298
  const dir = pattern.slice(0, slashIdx);
3025
3299
  const ext2 = pattern.slice(slashIdx + 1).replace("*", "");
3026
- const fileDir = path7.dirname(filePath);
3300
+ const fileDir = path8.dirname(filePath);
3027
3301
  return fileDir === dir && filePath.endsWith(ext2);
3028
3302
  }
3029
3303
  const ext = pattern.replace("*", "");
@@ -3031,8 +3305,8 @@ function matchGlob(filePath, pattern) {
3031
3305
  }
3032
3306
  function buildSnippet(hit, projectRoot) {
3033
3307
  try {
3034
- const absPath = path7.join(projectRoot, hit.filePath);
3035
- if (!existsSync6(absPath)) return hit.matchLine;
3308
+ const absPath = path8.join(projectRoot, hit.filePath);
3309
+ if (!existsSync7(absPath)) return hit.matchLine;
3036
3310
  const lines = readFileSync4(absPath, "utf8").split("\n");
3037
3311
  const start = Math.max(0, hit.lineNumber - 3);
3038
3312
  const end = Math.min(lines.length, hit.lineNumber + 2);
@@ -3058,111 +3332,574 @@ var init_file_grep = __esm({
3058
3332
  }
3059
3333
  });
3060
3334
 
3061
- // src/lib/reranker.ts
3062
- var reranker_exports = {};
3063
- __export(reranker_exports, {
3064
- disposeReranker: () => disposeReranker,
3065
- getRerankerModelPath: () => getRerankerModelPath,
3066
- isRerankerAvailable: () => isRerankerAvailable,
3067
- rerank: () => rerank,
3068
- rerankWithScores: () => rerankWithScores
3335
+ // src/lib/graph-query.ts
3336
+ var graph_query_exports = {};
3337
+ __export(graph_query_exports, {
3338
+ getConversationPartners: () => getConversationPartners,
3339
+ getEntityByName: () => getEntityByName,
3340
+ getEntityNeighbors: () => getEntityNeighbors,
3341
+ getEntityTimeline: () => getEntityTimeline,
3342
+ getGraphStats: () => getGraphStats,
3343
+ getHotEntities: () => getHotEntities,
3344
+ getRelationshipFrequency: () => getRelationshipFrequency,
3345
+ getRelationships: () => getRelationships,
3346
+ searchEntities: () => searchEntities,
3347
+ traverseChain: () => traverseChain
3069
3348
  });
3070
- import path8 from "path";
3071
- import { existsSync as existsSync7 } from "fs";
3072
- function resetIdleTimer() {
3073
- if (_idleTimer) clearTimeout(_idleTimer);
3074
- _idleTimer = setTimeout(() => {
3075
- void disposeReranker();
3076
- }, IDLE_TIMEOUT_MS);
3077
- if (_idleTimer && typeof _idleTimer === "object" && "unref" in _idleTimer) {
3078
- _idleTimer.unref();
3349
+ async function getEntityByName(client, name, type) {
3350
+ const sql = type ? `SELECT * FROM entities WHERE LOWER(name) = LOWER(?) AND type = ? LIMIT 1` : `SELECT * FROM entities WHERE LOWER(name) = LOWER(?) LIMIT 1`;
3351
+ const args = type ? [name, type] : [name];
3352
+ const result = await client.execute({ sql, args });
3353
+ if (result.rows.length === 0) return null;
3354
+ const row = result.rows[0];
3355
+ return {
3356
+ id: String(row.id),
3357
+ name: String(row.name),
3358
+ type: String(row.type),
3359
+ firstSeen: String(row.first_seen),
3360
+ lastSeen: String(row.last_seen),
3361
+ properties: JSON.parse(String(row.properties ?? "{}"))
3362
+ };
3363
+ }
3364
+ async function searchEntities(client, query, limit = 10) {
3365
+ const result = await client.execute({
3366
+ sql: `SELECT * FROM entities WHERE LOWER(name) LIKE ? ORDER BY last_seen DESC LIMIT ?`,
3367
+ args: [`%${query.toLowerCase()}%`, limit]
3368
+ });
3369
+ return result.rows.map((row) => ({
3370
+ id: String(row.id),
3371
+ name: String(row.name),
3372
+ type: String(row.type),
3373
+ firstSeen: String(row.first_seen),
3374
+ lastSeen: String(row.last_seen),
3375
+ properties: JSON.parse(String(row.properties ?? "{}"))
3376
+ }));
3377
+ }
3378
+ async function getRelationships(client, entityId, options) {
3379
+ const direction = options?.direction ?? "both";
3380
+ let sql;
3381
+ const args = [];
3382
+ if (direction === "outgoing") {
3383
+ sql = `SELECT r.*, s.name as source_name, t.name as target_name
3384
+ FROM relationships r
3385
+ JOIN entities s ON r.source_entity_id = s.id
3386
+ JOIN entities t ON r.target_entity_id = t.id
3387
+ WHERE r.source_entity_id = ?`;
3388
+ args.push(entityId);
3389
+ } else if (direction === "incoming") {
3390
+ sql = `SELECT r.*, s.name as source_name, t.name as target_name
3391
+ FROM relationships r
3392
+ JOIN entities s ON r.source_entity_id = s.id
3393
+ JOIN entities t ON r.target_entity_id = t.id
3394
+ WHERE r.target_entity_id = ?`;
3395
+ args.push(entityId);
3396
+ } else {
3397
+ sql = `SELECT r.*, s.name as source_name, t.name as target_name
3398
+ FROM relationships r
3399
+ JOIN entities s ON r.source_entity_id = s.id
3400
+ JOIN entities t ON r.target_entity_id = t.id
3401
+ WHERE r.source_entity_id = ? OR r.target_entity_id = ?`;
3402
+ args.push(entityId, entityId);
3403
+ }
3404
+ if (options?.type) {
3405
+ sql += ` AND r.type = ?`;
3406
+ args.push(options.type);
3407
+ }
3408
+ sql += ` ORDER BY r.weight DESC, r.timestamp DESC`;
3409
+ const result = await client.execute({ sql, args });
3410
+ return result.rows.map((row) => ({
3411
+ id: String(row.id),
3412
+ sourceEntityId: String(row.source_entity_id),
3413
+ targetEntityId: String(row.target_entity_id),
3414
+ type: String(row.type),
3415
+ weight: Number(row.weight),
3416
+ timestamp: String(row.timestamp),
3417
+ properties: JSON.parse(String(row.properties ?? "{}")),
3418
+ confidence: Number(row.confidence ?? 1),
3419
+ confidenceLabel: String(row.confidence_label ?? "extracted"),
3420
+ sourceName: String(row.source_name),
3421
+ targetName: String(row.target_name)
3422
+ }));
3423
+ }
3424
+ async function traverseChain(client, startEntityId, relationshipType, maxDepth = 3) {
3425
+ const result = await client.execute({
3426
+ sql: `WITH RECURSIVE chain(entity_id, depth, rel_type) AS (
3427
+ SELECT ?, 0, ''
3428
+ UNION ALL
3429
+ SELECT r.target_entity_id, c.depth + 1, r.type
3430
+ FROM chain c
3431
+ JOIN relationships r ON r.source_entity_id = c.entity_id
3432
+ WHERE r.type = ? AND c.depth < ?
3433
+ )
3434
+ SELECT DISTINCT e.*, chain.depth, chain.rel_type
3435
+ FROM chain
3436
+ JOIN entities e ON e.id = chain.entity_id
3437
+ ORDER BY chain.depth ASC`,
3438
+ args: [startEntityId, relationshipType, maxDepth]
3439
+ });
3440
+ return result.rows.map((row) => ({
3441
+ entity: {
3442
+ id: String(row.id),
3443
+ name: String(row.name),
3444
+ type: String(row.type),
3445
+ firstSeen: String(row.first_seen),
3446
+ lastSeen: String(row.last_seen),
3447
+ properties: JSON.parse(String(row.properties ?? "{}"))
3448
+ },
3449
+ depth: Number(row.depth),
3450
+ relationship: String(row.rel_type)
3451
+ }));
3452
+ }
3453
+ async function getEntityNeighbors(client, entityId, maxHops = 2) {
3454
+ const result = await client.execute({
3455
+ sql: `WITH RECURSIVE neighborhood(entity_id, depth, rel_type, rel_confidence) AS (
3456
+ SELECT ?, 0, '', 1.0
3457
+ UNION ALL
3458
+ SELECT CASE
3459
+ WHEN r.source_entity_id = n.entity_id THEN r.target_entity_id
3460
+ ELSE r.source_entity_id
3461
+ END, n.depth + 1, r.type, COALESCE(r.confidence, 1.0)
3462
+ FROM neighborhood n
3463
+ JOIN relationships r ON r.source_entity_id = n.entity_id
3464
+ OR r.target_entity_id = n.entity_id
3465
+ WHERE n.depth < ?
3466
+ )
3467
+ SELECT DISTINCT e.*, MIN(neighborhood.depth) as depth,
3468
+ neighborhood.rel_type, neighborhood.rel_confidence
3469
+ FROM neighborhood
3470
+ JOIN entities e ON e.id = neighborhood.entity_id
3471
+ GROUP BY e.id
3472
+ ORDER BY depth ASC, e.last_seen DESC
3473
+ LIMIT 50`,
3474
+ args: [entityId, maxHops]
3475
+ });
3476
+ return result.rows.map((row) => ({
3477
+ entity: {
3478
+ id: String(row.id),
3479
+ name: String(row.name),
3480
+ type: String(row.type),
3481
+ firstSeen: String(row.first_seen),
3482
+ lastSeen: String(row.last_seen),
3483
+ properties: JSON.parse(String(row.properties ?? "{}"))
3484
+ },
3485
+ depth: Number(row.depth),
3486
+ relType: String(row.rel_type ?? ""),
3487
+ relConfidence: Number(row.rel_confidence ?? 1)
3488
+ }));
3489
+ }
3490
+ async function getGraphStats(client) {
3491
+ const entityCount = await client.execute("SELECT COUNT(*) as cnt FROM entities");
3492
+ const relCount = await client.execute("SELECT COUNT(*) as cnt FROM relationships");
3493
+ const typeResult = await client.execute(
3494
+ "SELECT type, COUNT(*) as cnt FROM entities GROUP BY type ORDER BY cnt DESC"
3495
+ );
3496
+ const types = {};
3497
+ for (const row of typeResult.rows) {
3498
+ types[String(row.type)] = Number(row.cnt);
3079
3499
  }
3500
+ return {
3501
+ entities: Number(entityCount.rows[0]?.cnt ?? 0),
3502
+ relationships: Number(relCount.rows[0]?.cnt ?? 0),
3503
+ types
3504
+ };
3080
3505
  }
3081
- function isRerankerAvailable() {
3082
- return existsSync7(path8.join(MODELS_DIR, RERANKER_MODEL_FILE));
3506
+ async function getEntityTimeline(client, entityId, since) {
3507
+ const args = [entityId, entityId];
3508
+ let sinceClause = "";
3509
+ if (since) {
3510
+ sinceClause = " AND r.timestamp >= ?";
3511
+ args.push(since.toISOString());
3512
+ }
3513
+ const result = await client.execute({
3514
+ sql: `SELECT r.*, s.name as source_name, t.name as target_name
3515
+ FROM relationships r
3516
+ JOIN entities s ON r.source_entity_id = s.id
3517
+ JOIN entities t ON r.target_entity_id = t.id
3518
+ WHERE (r.source_entity_id = ? OR r.target_entity_id = ?)${sinceClause}
3519
+ ORDER BY r.timestamp DESC`,
3520
+ args
3521
+ });
3522
+ return result.rows.map((row) => ({
3523
+ relationship: {
3524
+ id: String(row.id),
3525
+ sourceEntityId: String(row.source_entity_id),
3526
+ targetEntityId: String(row.target_entity_id),
3527
+ type: String(row.type),
3528
+ weight: Number(row.weight),
3529
+ timestamp: String(row.timestamp),
3530
+ properties: JSON.parse(String(row.properties ?? "{}")),
3531
+ sourceName: String(row.source_name),
3532
+ targetName: String(row.target_name)
3533
+ },
3534
+ timestamp: String(row.timestamp)
3535
+ }));
3083
3536
  }
3084
- function getRerankerModelPath() {
3085
- return path8.join(MODELS_DIR, RERANKER_MODEL_FILE);
3537
+ async function getRelationshipFrequency(client, entityId, options) {
3538
+ const granularity = options?.granularity ?? "day";
3539
+ const args = [entityId, entityId];
3540
+ let typeClause = "";
3541
+ if (options?.type) {
3542
+ typeClause = " AND r.type = ?";
3543
+ args.push(options.type);
3544
+ }
3545
+ let bucketExpr;
3546
+ switch (granularity) {
3547
+ case "week":
3548
+ bucketExpr = `strftime('%Y', r.timestamp) || '-W' || strftime('%W', r.timestamp)`;
3549
+ break;
3550
+ case "month":
3551
+ bucketExpr = `strftime('%Y-%m', r.timestamp)`;
3552
+ break;
3553
+ default:
3554
+ bucketExpr = `strftime('%Y-%m-%d', r.timestamp)`;
3555
+ }
3556
+ const result = await client.execute({
3557
+ sql: `SELECT ${bucketExpr} as bucket, COUNT(*) as cnt, r.type as rel_type
3558
+ FROM relationships r
3559
+ WHERE (r.source_entity_id = ? OR r.target_entity_id = ?)${typeClause}
3560
+ GROUP BY bucket${options?.type ? "" : ", r.type"}
3561
+ ORDER BY bucket DESC`,
3562
+ args
3563
+ });
3564
+ return result.rows.map((row) => ({
3565
+ bucket: String(row.bucket),
3566
+ count: Number(row.cnt),
3567
+ relationshipType: options?.type ? options.type : String(row.rel_type)
3568
+ }));
3086
3569
  }
3087
- async function ensureLoaded() {
3088
- if (_rerankerContext) {
3089
- resetIdleTimer();
3090
- return;
3570
+ async function getConversationPartners(client, contactEntityId, since) {
3571
+ const args = [contactEntityId, contactEntityId];
3572
+ let sinceClause = "";
3573
+ if (since) {
3574
+ sinceClause = " AND r.timestamp >= ?";
3575
+ args.push(since.toISOString());
3091
3576
  }
3092
- const modelPath = path8.join(MODELS_DIR, RERANKER_MODEL_FILE);
3093
- if (!existsSync7(modelPath)) {
3094
- throw new Error(
3095
- `Reranker model not found at ${modelPath}. Run /exe-setup to download it.`
3096
- );
3577
+ const result = await client.execute({
3578
+ sql: `SELECT
3579
+ CASE
3580
+ WHEN r.source_entity_id = ? THEN r.target_entity_id
3581
+ ELSE r.source_entity_id
3582
+ END as partner_id,
3583
+ COUNT(*) as interaction_count,
3584
+ MAX(r.timestamp) as last_interaction
3585
+ FROM relationships r
3586
+ WHERE (r.source_entity_id = ? OR r.target_entity_id = ?)${sinceClause}
3587
+ GROUP BY partner_id
3588
+ ORDER BY interaction_count DESC`,
3589
+ args: [contactEntityId, ...args]
3590
+ });
3591
+ const partners = [];
3592
+ for (const row of result.rows) {
3593
+ const partnerId = String(row.partner_id);
3594
+ const entityResult = await client.execute({
3595
+ sql: "SELECT * FROM entities WHERE id = ?",
3596
+ args: [partnerId]
3597
+ });
3598
+ if (entityResult.rows.length === 0) continue;
3599
+ const e = entityResult.rows[0];
3600
+ partners.push({
3601
+ agentEntity: {
3602
+ id: String(e.id),
3603
+ name: String(e.name),
3604
+ type: String(e.type),
3605
+ firstSeen: String(e.first_seen),
3606
+ lastSeen: String(e.last_seen),
3607
+ properties: JSON.parse(String(e.properties ?? "{}"))
3608
+ },
3609
+ interactionCount: Number(row.interaction_count),
3610
+ lastInteraction: String(row.last_interaction)
3611
+ });
3097
3612
  }
3098
- process.stderr.write("[reranker] Loading Jina Reranker v3...\n");
3099
- const { getLlama } = await import("node-llama-cpp");
3100
- const llama = await getLlama();
3101
- _rerankerModel = await llama.loadModel({ modelPath });
3102
- _rerankerContext = await _rerankerModel.createEmbeddingContext();
3103
- process.stderr.write("[reranker] Jina Reranker v3 loaded.\n");
3104
- resetIdleTimer();
3613
+ return partners;
3105
3614
  }
3106
- async function disposeReranker() {
3107
- if (_idleTimer) {
3108
- clearTimeout(_idleTimer);
3109
- _idleTimer = null;
3615
+ async function getHotEntities(client, since, limit = 10) {
3616
+ const sinceISO = since.toISOString();
3617
+ const result = await client.execute({
3618
+ sql: `SELECT entity_id, COUNT(*) as rel_count
3619
+ FROM (
3620
+ SELECT source_entity_id as entity_id FROM relationships WHERE timestamp >= ?
3621
+ UNION ALL
3622
+ SELECT target_entity_id as entity_id FROM relationships WHERE timestamp >= ?
3623
+ )
3624
+ GROUP BY entity_id
3625
+ ORDER BY rel_count DESC
3626
+ LIMIT ?`,
3627
+ args: [sinceISO, sinceISO, limit]
3628
+ });
3629
+ const hotEntities = [];
3630
+ for (const row of result.rows) {
3631
+ const eid = String(row.entity_id);
3632
+ const entityResult = await client.execute({
3633
+ sql: "SELECT * FROM entities WHERE id = ?",
3634
+ args: [eid]
3635
+ });
3636
+ if (entityResult.rows.length === 0) continue;
3637
+ const e = entityResult.rows[0];
3638
+ hotEntities.push({
3639
+ entity: {
3640
+ id: String(e.id),
3641
+ name: String(e.name),
3642
+ type: String(e.type),
3643
+ firstSeen: String(e.first_seen),
3644
+ lastSeen: String(e.last_seen),
3645
+ properties: JSON.parse(String(e.properties ?? "{}"))
3646
+ },
3647
+ newRelationships: Number(row.rel_count)
3648
+ });
3110
3649
  }
3111
- if (_rerankerContext) {
3112
- try {
3113
- await _rerankerContext.dispose();
3114
- } catch {
3650
+ return hotEntities;
3651
+ }
3652
+ var init_graph_query = __esm({
3653
+ "src/lib/graph-query.ts"() {
3654
+ "use strict";
3655
+ }
3656
+ });
3657
+
3658
+ // src/lib/entity-boost.ts
3659
+ var entity_boost_exports = {};
3660
+ __export(entity_boost_exports, {
3661
+ applyEntityBoost: () => applyEntityBoost
3662
+ });
3663
+ function getRelTypeWeights() {
3664
+ const override = process.env.RELATIONSHIP_TYPE_WEIGHTS;
3665
+ if (!override) return DEFAULT_RELATIONSHIP_WEIGHTS;
3666
+ try {
3667
+ return { ...DEFAULT_RELATIONSHIP_WEIGHTS, ...JSON.parse(override) };
3668
+ } catch {
3669
+ return DEFAULT_RELATIONSHIP_WEIGHTS;
3670
+ }
3671
+ }
3672
+ async function matchEntities(query, client) {
3673
+ const words = query.toLowerCase().split(/\s+/).filter((w) => w.length >= MIN_WORD_LENGTH).map((w) => w.replace(/[^a-z0-9_-]/g, "")).filter((w) => w.length >= MIN_WORD_LENGTH);
3674
+ if (words.length === 0) return [];
3675
+ try {
3676
+ const matchExpr = words.map((w) => `${w}*`).join(" OR ");
3677
+ const result = await client.execute({
3678
+ sql: `SELECT e.id, e.name FROM entities e
3679
+ JOIN entities_fts fts ON e.rowid = fts.rowid
3680
+ WHERE entities_fts MATCH ?
3681
+ ORDER BY rank
3682
+ LIMIT ?`,
3683
+ args: [matchExpr, MAX_ENTITY_MATCHES]
3684
+ });
3685
+ if (result.rows.length > 0) {
3686
+ return result.rows.map((row) => ({
3687
+ entityId: String(row.id),
3688
+ name: String(row.name)
3689
+ }));
3115
3690
  }
3116
- _rerankerContext = null;
3691
+ } catch {
3117
3692
  }
3118
- if (_rerankerModel) {
3119
- try {
3120
- await _rerankerModel.dispose();
3121
- } catch {
3693
+ const conditions = words.map(() => `LOWER(name) LIKE ?`);
3694
+ const args = words.map((w) => `%${w}%`);
3695
+ try {
3696
+ const result = await client.execute({
3697
+ sql: `SELECT id, name FROM entities
3698
+ WHERE ${conditions.join(" OR ")}
3699
+ LIMIT ?`,
3700
+ args: [...args, MAX_ENTITY_MATCHES]
3701
+ });
3702
+ return result.rows.map((row) => ({
3703
+ entityId: String(row.id),
3704
+ name: String(row.name)
3705
+ }));
3706
+ } catch {
3707
+ return [];
3708
+ }
3709
+ }
3710
+ async function getLinkedMemories(entityIds, client) {
3711
+ const linked = /* @__PURE__ */ new Map();
3712
+ if (entityIds.length === 0) return linked;
3713
+ const placeholders = entityIds.map(() => "?").join(",");
3714
+ try {
3715
+ const result = await client.execute({
3716
+ sql: `SELECT entity_id, memory_id FROM entity_memories
3717
+ WHERE entity_id IN (${placeholders})`,
3718
+ args: entityIds
3719
+ });
3720
+ for (const row of result.rows) {
3721
+ const entityId = String(row.entity_id);
3722
+ const memoryId = String(row.memory_id);
3723
+ const entry = linked.get(entityId) ?? {
3724
+ entityId,
3725
+ memoryIds: /* @__PURE__ */ new Set(),
3726
+ count: 0
3727
+ };
3728
+ entry.memoryIds.add(memoryId);
3729
+ entry.count = entry.memoryIds.size;
3730
+ linked.set(entityId, entry);
3122
3731
  }
3123
- _rerankerModel = null;
3732
+ } catch {
3124
3733
  }
3125
- process.stderr.write("[reranker] Unloaded (idle timeout).\n");
3734
+ return linked;
3126
3735
  }
3127
- async function rerankWithScores(query, texts, topK) {
3128
- if (texts.length === 0) return [];
3129
- await ensureLoaded();
3130
- const ctx = _rerankerContext;
3131
- const scored = [];
3132
- for (let i = 0; i < texts.length; i++) {
3133
- const text = texts[i] ?? "";
3736
+ function spreadAttenuation(numLinked) {
3737
+ return 1 / (1 + 1e-3 * (numLinked - 1) ** 2);
3738
+ }
3739
+ async function traverseAndScore(entities, client, boostMap, resultIds, graphContextMap) {
3740
+ const topEntities = entities.slice(0, TOP_ENTITIES_TO_TRAVERSE);
3741
+ if (topEntities.length === 0) return;
3742
+ const { getEntityNeighbors: getEntityNeighbors2 } = await Promise.resolve().then(() => (init_graph_query(), graph_query_exports));
3743
+ const relWeights = getRelTypeWeights();
3744
+ const neighborEntityIds = [];
3745
+ const neighborMeta = /* @__PURE__ */ new Map();
3746
+ for (const entity of topEntities) {
3134
3747
  try {
3135
- const input = `query: ${query} document: ${text.slice(0, 512)}`;
3136
- const embedding = await ctx.getEmbeddingFor(input);
3137
- const score = embedding.vector[0] ?? 0;
3138
- scored.push({ text, score, index: i });
3748
+ const neighbors = await getEntityNeighbors2(
3749
+ client,
3750
+ entity.entityId,
3751
+ GRAPH_TRAVERSAL_MAX_HOPS
3752
+ );
3753
+ for (const n of neighbors) {
3754
+ if (n.depth === 0) continue;
3755
+ if (neighborMeta.has(n.entity.id)) continue;
3756
+ neighborEntityIds.push(n.entity.id);
3757
+ neighborMeta.set(n.entity.id, {
3758
+ depth: n.depth,
3759
+ relType: n.relType,
3760
+ relConfidence: n.relConfidence,
3761
+ sourceName: entity.name
3762
+ });
3763
+ }
3139
3764
  } catch {
3140
- scored.push({ text, score: -1, index: i });
3141
3765
  }
3142
3766
  }
3143
- scored.sort((a, b) => b.score - a.score);
3144
- return typeof topK === "number" ? scored.slice(0, topK) : scored;
3767
+ if (neighborEntityIds.length === 0) return;
3768
+ const neighborLinked = await getLinkedMemories(neighborEntityIds, client);
3769
+ for (const [entityId, entry] of neighborLinked) {
3770
+ const meta = neighborMeta.get(entityId);
3771
+ if (!meta) continue;
3772
+ const relWeight = relWeights[meta.relType] ?? DEFAULT_REL_WEIGHT;
3773
+ const depthDecay = 1 / (1 + meta.depth);
3774
+ const neighborBoost = ENTITY_BOOST_WEIGHT * meta.relConfidence * depthDecay * relWeight;
3775
+ const attenuation = spreadAttenuation(entry.count);
3776
+ const finalBoost = neighborBoost * attenuation;
3777
+ for (const memoryId of entry.memoryIds) {
3778
+ if (resultIds.has(memoryId)) {
3779
+ boostMap.set(memoryId, (boostMap.get(memoryId) ?? 0) + finalBoost);
3780
+ const contextPath = `${meta.sourceName} ${meta.relType} ${entityId}`;
3781
+ if (!graphContextMap.has(memoryId)) {
3782
+ graphContextMap.set(memoryId, contextPath);
3783
+ }
3784
+ }
3785
+ }
3786
+ }
3145
3787
  }
3146
- async function rerank(query, candidates, topK = 5) {
3147
- if (candidates.length === 0) return [];
3148
- if (candidates.length <= topK) return candidates;
3149
- const scored = await rerankWithScores(
3150
- query,
3151
- candidates.map((c) => c.raw_text),
3152
- topK
3788
+ async function applyHyperedgeBoost(entities, client, boostMap, resultIds) {
3789
+ if (entities.length < 2) return;
3790
+ const entityIds = entities.map((e) => e.entityId);
3791
+ const placeholders = entityIds.map(() => "?").join(",");
3792
+ try {
3793
+ const result = await client.execute({
3794
+ sql: `SELECT hyperedge_id, entity_id FROM hyperedge_nodes
3795
+ WHERE entity_id IN (${placeholders})`,
3796
+ args: entityIds
3797
+ });
3798
+ const hyperedgeEntities = /* @__PURE__ */ new Map();
3799
+ for (const row of result.rows) {
3800
+ const hid = String(row.hyperedge_id);
3801
+ const eid = String(row.entity_id);
3802
+ const set = hyperedgeEntities.get(hid) ?? /* @__PURE__ */ new Set();
3803
+ set.add(eid);
3804
+ hyperedgeEntities.set(hid, set);
3805
+ }
3806
+ const sharedHyperedgeIds = [];
3807
+ for (const [hid, eids] of hyperedgeEntities) {
3808
+ if (eids.size >= 2) {
3809
+ sharedHyperedgeIds.push(hid);
3810
+ }
3811
+ }
3812
+ if (sharedHyperedgeIds.length === 0) return;
3813
+ const hPlaceholders = sharedHyperedgeIds.map(() => "?").join(",");
3814
+ const allNodesResult = await client.execute({
3815
+ sql: `SELECT DISTINCT entity_id FROM hyperedge_nodes
3816
+ WHERE hyperedge_id IN (${hPlaceholders})`,
3817
+ args: sharedHyperedgeIds
3818
+ });
3819
+ const allEntityIds = allNodesResult.rows.map((r) => String(r.entity_id));
3820
+ if (allEntityIds.length === 0) return;
3821
+ const linked = await getLinkedMemories(allEntityIds, client);
3822
+ for (const [, entry] of linked) {
3823
+ const attenuation = spreadAttenuation(entry.count);
3824
+ const boost = ENTITY_BOOST_WEIGHT * attenuation;
3825
+ for (const memoryId of entry.memoryIds) {
3826
+ if (resultIds.has(memoryId)) {
3827
+ boostMap.set(memoryId, (boostMap.get(memoryId) ?? 0) + boost);
3828
+ }
3829
+ }
3830
+ }
3831
+ } catch {
3832
+ }
3833
+ }
3834
+ async function applyEntityBoost(results, query, client) {
3835
+ const emptyResult = { results, graphContext: /* @__PURE__ */ new Map() };
3836
+ if (ENTITY_BOOST_WEIGHT === 0 || results.length === 0) {
3837
+ return emptyResult;
3838
+ }
3839
+ console.time("entity-boost");
3840
+ const entities = await matchEntities(query, client);
3841
+ if (entities.length === 0) {
3842
+ console.timeEnd("entity-boost");
3843
+ return emptyResult;
3844
+ }
3845
+ const boostMap = /* @__PURE__ */ new Map();
3846
+ const resultIds = new Set(results.map((r) => r.id));
3847
+ const graphContextMap = /* @__PURE__ */ new Map();
3848
+ const directLinked = await getLinkedMemories(
3849
+ entities.map((e) => e.entityId),
3850
+ client
3153
3851
  );
3154
- return scored.map((s) => candidates[s.index]);
3852
+ for (const [, entry] of directLinked) {
3853
+ const attenuation = spreadAttenuation(entry.count);
3854
+ const boost = ENTITY_BOOST_WEIGHT * attenuation;
3855
+ for (const memoryId of entry.memoryIds) {
3856
+ if (resultIds.has(memoryId)) {
3857
+ boostMap.set(memoryId, (boostMap.get(memoryId) ?? 0) + boost);
3858
+ }
3859
+ }
3860
+ }
3861
+ await traverseAndScore(entities, client, boostMap, resultIds, graphContextMap);
3862
+ await applyHyperedgeBoost(entities, client, boostMap, resultIds);
3863
+ if (boostMap.size === 0) {
3864
+ console.timeEnd("entity-boost");
3865
+ return emptyResult;
3866
+ }
3867
+ const scored = results.map((r, i) => ({
3868
+ record: r,
3869
+ baseScore: 1 / (1 + i * 0.1),
3870
+ entityBoost: boostMap.get(r.id) ?? 0
3871
+ }));
3872
+ scored.sort(
3873
+ (a, b) => b.baseScore + b.entityBoost - (a.baseScore + a.entityBoost)
3874
+ );
3875
+ console.timeEnd("entity-boost");
3876
+ return {
3877
+ results: scored.map((s) => s.record),
3878
+ graphContext: graphContextMap
3879
+ };
3155
3880
  }
3156
- var RERANKER_MODEL_FILE, IDLE_TIMEOUT_MS, _rerankerContext, _rerankerModel, _idleTimer;
3157
- var init_reranker = __esm({
3158
- "src/lib/reranker.ts"() {
3881
+ var ENTITY_BOOST_WEIGHT, MIN_WORD_LENGTH, MAX_ENTITY_MATCHES, GRAPH_TRAVERSAL_MAX_HOPS, TOP_ENTITIES_TO_TRAVERSE, DEFAULT_RELATIONSHIP_WEIGHTS, DEFAULT_REL_WEIGHT;
3882
+ var init_entity_boost = __esm({
3883
+ "src/lib/entity-boost.ts"() {
3159
3884
  "use strict";
3160
- init_config();
3161
- RERANKER_MODEL_FILE = "jina-reranker-v3-q4_k_m.gguf";
3162
- IDLE_TIMEOUT_MS = 6e4;
3163
- _rerankerContext = null;
3164
- _rerankerModel = null;
3165
- _idleTimer = null;
3885
+ ENTITY_BOOST_WEIGHT = parseFloat(
3886
+ process.env.ENTITY_BOOST_WEIGHT ?? "0.5"
3887
+ );
3888
+ MIN_WORD_LENGTH = 3;
3889
+ MAX_ENTITY_MATCHES = 20;
3890
+ GRAPH_TRAVERSAL_MAX_HOPS = Math.min(
3891
+ parseInt(process.env.GRAPH_TRAVERSAL_MAX_HOPS ?? "2", 10),
3892
+ 3
3893
+ );
3894
+ TOP_ENTITIES_TO_TRAVERSE = 5;
3895
+ DEFAULT_RELATIONSHIP_WEIGHTS = {
3896
+ decided: 1,
3897
+ depends_on: 0.8,
3898
+ part_of: 0.7,
3899
+ related_to: 0.5,
3900
+ mentioned_in: 0.3
3901
+ };
3902
+ DEFAULT_REL_WEIGHT = 0.4;
3166
3903
  }
3167
3904
  });
3168
3905
 
@@ -3213,8 +3950,14 @@ async function hybridSearch(queryText, agentId, options) {
3213
3950
  `
3214
3951
  );
3215
3952
  }
3953
+ let rerankerAvailable = false;
3954
+ try {
3955
+ const { isRerankerAvailable: isRerankerAvailable2 } = await Promise.resolve().then(() => (init_reranker(), reranker_exports));
3956
+ rerankerAvailable = isRerankerAvailable2();
3957
+ } catch {
3958
+ }
3216
3959
  const broadFetchTopK = config.scalingRoadmap?.rerankerAutoTrigger?.fetchTopK ?? 150;
3217
- const fetchLimit = effectiveIsBroad ? Math.max(limit * 5, broadFetchTopK) : Math.max(limit * 3, 30);
3960
+ const fetchLimit = effectiveIsBroad ? Math.max(limit * 5, broadFetchTopK) : rerankerAvailable ? Math.max(limit * 4, 60) : Math.max(limit * 3, 30);
3218
3961
  const fetchOptions = { ...effectiveOptions, limit: fetchLimit, includeSource: false };
3219
3962
  let queryVector = null;
3220
3963
  try {
@@ -3224,7 +3967,7 @@ async function hybridSearch(queryText, agentId, options) {
3224
3967
  process.stderr.write("[hybrid-search] Embed daemon unavailable \u2014 FTS-only mode\n");
3225
3968
  }
3226
3969
  let grepPromise = Promise.resolve([]);
3227
- if (config.fileGrepEnabled !== false) {
3970
+ if (config.fileGrepEnabled === true) {
3228
3971
  try {
3229
3972
  const { getProjectName: getProjectName2 } = await Promise.resolve().then(() => (init_project_name(), project_name_exports));
3230
3973
  const projectRoot = process.cwd();
@@ -3260,7 +4003,17 @@ async function hybridSearch(queryText, agentId, options) {
3260
4003
  if (lists.length === 0) return [];
3261
4004
  if (lists.length === 1 && !effectiveIsBroad) return lists[0].slice(0, limit);
3262
4005
  const rrfLimit = effectiveIsBroad ? Math.max(limit * 5, 150) : limit;
3263
- const merged = lists.length === 1 ? lists[0].slice(0, rrfLimit) : rrfMergeMulti(lists, rrfLimit, RRF_K, weights);
4006
+ let merged = lists.length === 1 ? lists[0].slice(0, rrfLimit) : rrfMergeMulti(lists, rrfLimit, RRF_K, weights);
4007
+ let graphContextMap = /* @__PURE__ */ new Map();
4008
+ if (merged.length > 0) {
4009
+ try {
4010
+ const { applyEntityBoost: applyEntityBoost2 } = await Promise.resolve().then(() => (init_entity_boost(), entity_boost_exports));
4011
+ const boosted = await applyEntityBoost2(merged, effectiveQuery, getClient());
4012
+ merged = boosted.results;
4013
+ graphContextMap = boosted.graphContext;
4014
+ } catch {
4015
+ }
4016
+ }
3264
4017
  const auto = config.scalingRoadmap?.rerankerAutoTrigger ?? {
3265
4018
  enabled: config.rerankerEnabled ?? true,
3266
4019
  broadQueryMinCardinality: 5e4,
@@ -3268,20 +4021,29 @@ async function hybridSearch(queryText, agentId, options) {
3268
4021
  returnTopK: 5
3269
4022
  };
3270
4023
  let rerankedAndBlended = null;
3271
- if (effectiveIsBroad && auto.enabled) {
4024
+ if (effectiveIsBroad && auto.enabled && rerankerAvailable) {
3272
4025
  const cardinality2 = await estimateCardinality(agentId, effectiveOptions);
3273
4026
  if (cardinality2 > auto.broadQueryMinCardinality) {
3274
4027
  try {
3275
- const { isRerankerAvailable: isRerankerAvailable2, rerank: rerank2 } = await Promise.resolve().then(() => (init_reranker(), reranker_exports));
3276
- if (isRerankerAvailable2()) {
3277
- const reranked = await rerank2(effectiveQuery, merged, auto.returnTopK);
3278
- if (reranked.length > 0) {
3279
- rerankedAndBlended = rrfMergeMulti(
3280
- [reranked],
3281
- auto.returnTopK,
3282
- RRF_K
3283
- );
3284
- }
4028
+ let rerankedRecords;
4029
+ if (graphContextMap.size > 0) {
4030
+ const { rerankWithContext: rerankWithContext2 } = await Promise.resolve().then(() => (init_reranker(), reranker_exports));
4031
+ const candidates = merged.map((m) => ({
4032
+ text: m.raw_text,
4033
+ context: graphContextMap.get(m.id)
4034
+ }));
4035
+ const scored = await rerankWithContext2(effectiveQuery, candidates, auto.returnTopK);
4036
+ rerankedRecords = scored.map((s) => merged[s.index]);
4037
+ } else {
4038
+ const { rerank: rerank2 } = await Promise.resolve().then(() => (init_reranker(), reranker_exports));
4039
+ rerankedRecords = await rerank2(effectiveQuery, merged, auto.returnTopK);
4040
+ }
4041
+ if (rerankedRecords.length > 0) {
4042
+ rerankedAndBlended = rrfMergeMulti(
4043
+ [rerankedRecords],
4044
+ auto.returnTopK,
4045
+ RRF_K
4046
+ );
3285
4047
  }
3286
4048
  } catch {
3287
4049
  }
@@ -3301,7 +4063,7 @@ async function hybridSearch(queryText, agentId, options) {
3301
4063
  try {
3302
4064
  const client = getClient();
3303
4065
  void client.execute({
3304
- sql: `UPDATE memories SET last_accessed = ? WHERE id IN (${placeholders})`,
4066
+ sql: `UPDATE memories SET last_accessed = ?, retrieval_count = COALESCE(retrieval_count, 0) + 1 WHERE id IN (${placeholders})`,
3305
4067
  args: [now, ...ids]
3306
4068
  }).catch(() => {
3307
4069
  });
@@ -3471,7 +4233,7 @@ async function ftsQuery(client, matchExpr, agentId, options, limit) {
3471
4233
  source_type: row.source_type ?? null
3472
4234
  }));
3473
4235
  }
3474
- async function recentRecords(agentId, options, limit) {
4236
+ async function recentRecords(agentId, options, limit, textFilter) {
3475
4237
  const client = getClient();
3476
4238
  const statusFilter = options?.includeArchived ? "" : `
3477
4239
  AND COALESCE(status, 'active') = 'active'`;
@@ -3511,6 +4273,10 @@ async function recentRecords(agentId, options, limit) {
3511
4273
  sql += ` AND memory_type = ?`;
3512
4274
  args.push(options.memoryType);
3513
4275
  }
4276
+ if (textFilter) {
4277
+ sql += ` AND raw_text LIKE '%' || ? || '%'`;
4278
+ args.push(textFilter);
4279
+ }
3514
4280
  sql += ` ORDER BY timestamp DESC LIMIT ?`;
3515
4281
  args.push(limit);
3516
4282
  const result = await client.execute({ sql, args });
@@ -3630,6 +4396,7 @@ import { realpathSync } from "fs";
3630
4396
  import { fileURLToPath as fileURLToPath2 } from "url";
3631
4397
  function isMainModule(importMetaUrl) {
3632
4398
  if (process.argv[1] == null) return false;
4399
+ if (process.argv[1].includes("mcp/server")) return false;
3633
4400
  try {
3634
4401
  const scriptPath = realpathSync(process.argv[1]);
3635
4402
  const modulePath = realpathSync(fileURLToPath2(importMetaUrl));