@askexenow/exe-os 0.8.83 → 0.8.85

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 (95) 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 +97 -2
  5. package/dist/bin/cli.js +14350 -12518
  6. package/dist/bin/exe-agent.js +97 -88
  7. package/dist/bin/exe-assign.js +1003 -854
  8. package/dist/bin/exe-boot.js +1257 -320
  9. package/dist/bin/exe-call.js +10 -0
  10. package/dist/bin/exe-cloud.js +29 -6
  11. package/dist/bin/exe-dispatch.js +210 -34
  12. package/dist/bin/exe-doctor.js +403 -6
  13. package/dist/bin/exe-export-behaviors.js +175 -72
  14. package/dist/bin/exe-forget.js +97 -2
  15. package/dist/bin/exe-gateway.js +550 -171
  16. package/dist/bin/exe-healthcheck.js +1 -0
  17. package/dist/bin/exe-heartbeat.js +100 -5
  18. package/dist/bin/exe-kill.js +175 -72
  19. package/dist/bin/exe-launch-agent.js +189 -76
  20. package/dist/bin/exe-link.js +902 -80
  21. package/dist/bin/exe-new-employee.js +38 -8
  22. package/dist/bin/exe-pending-messages.js +96 -2
  23. package/dist/bin/exe-pending-notifications.js +97 -2
  24. package/dist/bin/exe-pending-reviews.js +98 -3
  25. package/dist/bin/exe-rename.js +564 -23
  26. package/dist/bin/exe-review.js +231 -73
  27. package/dist/bin/exe-search.js +989 -226
  28. package/dist/bin/exe-session-cleanup.js +4806 -1665
  29. package/dist/bin/exe-settings.js +20 -5
  30. package/dist/bin/exe-status.js +97 -2
  31. package/dist/bin/exe-team.js +97 -2
  32. package/dist/bin/git-sweep.js +899 -207
  33. package/dist/bin/graph-backfill.js +175 -72
  34. package/dist/bin/graph-export.js +175 -72
  35. package/dist/bin/install.js +38 -7
  36. package/dist/bin/list-providers.js +1 -0
  37. package/dist/bin/scan-tasks.js +904 -211
  38. package/dist/bin/setup.js +867 -268
  39. package/dist/bin/shard-migrate.js +175 -72
  40. package/dist/bin/update.js +1 -0
  41. package/dist/bin/wiki-sync.js +175 -72
  42. package/dist/gateway/index.js +548 -166
  43. package/dist/hooks/bug-report-worker.js +208 -23
  44. package/dist/hooks/commit-complete.js +897 -205
  45. package/dist/hooks/error-recall.js +988 -226
  46. package/dist/hooks/ingest-worker.js +1638 -1194
  47. package/dist/hooks/ingest.js +3 -0
  48. package/dist/hooks/instructions-loaded.js +707 -97
  49. package/dist/hooks/notification.js +699 -89
  50. package/dist/hooks/post-compact.js +714 -104
  51. package/dist/hooks/pre-compact.js +897 -205
  52. package/dist/hooks/pre-tool-use.js +742 -123
  53. package/dist/hooks/prompt-ingest-worker.js +242 -101
  54. package/dist/hooks/prompt-submit.js +995 -233
  55. package/dist/hooks/response-ingest-worker.js +242 -101
  56. package/dist/hooks/session-end.js +3941 -400
  57. package/dist/hooks/session-start.js +1001 -226
  58. package/dist/hooks/stop.js +725 -115
  59. package/dist/hooks/subagent-stop.js +714 -104
  60. package/dist/hooks/summary-worker.js +1964 -1330
  61. package/dist/index.js +1651 -1053
  62. package/dist/lib/cloud-sync.js +907 -86
  63. package/dist/lib/consolidation.js +2 -1
  64. package/dist/lib/database.js +642 -87
  65. package/dist/lib/db-daemon-client.js +503 -0
  66. package/dist/lib/device-registry.js +547 -7
  67. package/dist/lib/embedder.js +14 -28
  68. package/dist/lib/employee-templates.js +84 -74
  69. package/dist/lib/employees.js +9 -0
  70. package/dist/lib/exe-daemon-client.js +16 -29
  71. package/dist/lib/exe-daemon.js +1955 -922
  72. package/dist/lib/hybrid-search.js +988 -226
  73. package/dist/lib/identity.js +87 -67
  74. package/dist/lib/keychain.js +9 -1
  75. package/dist/lib/messaging.js +8 -1
  76. package/dist/lib/reminders.js +91 -74
  77. package/dist/lib/schedules.js +96 -2
  78. package/dist/lib/skill-learning.js +103 -85
  79. package/dist/lib/store.js +234 -73
  80. package/dist/lib/tasks.js +111 -22
  81. package/dist/lib/tmux-routing.js +120 -31
  82. package/dist/lib/token-spend.js +273 -0
  83. package/dist/lib/ws-client.js +11 -0
  84. package/dist/mcp/server.js +5222 -475
  85. package/dist/mcp/tools/complete-reminder.js +94 -77
  86. package/dist/mcp/tools/create-reminder.js +94 -77
  87. package/dist/mcp/tools/create-task.js +120 -22
  88. package/dist/mcp/tools/deactivate-behavior.js +95 -77
  89. package/dist/mcp/tools/list-reminders.js +94 -77
  90. package/dist/mcp/tools/list-tasks.js +31 -1
  91. package/dist/mcp/tools/send-message.js +8 -1
  92. package/dist/mcp/tools/update-task.js +39 -10
  93. package/dist/runtime/index.js +911 -219
  94. package/dist/tui/App.js +997 -295
  95. package/package.json +6 -1
@@ -408,6 +408,12 @@ function getClient() {
408
408
  if (!_resilientClient) {
409
409
  throw new Error("Database client not initialized. Call initDatabase() first.");
410
410
  }
411
+ if (process.env.EXE_IS_DAEMON === "1") {
412
+ return _resilientClient;
413
+ }
414
+ if (_daemonClient && _daemonClient._isDaemonActive()) {
415
+ return _daemonClient;
416
+ }
411
417
  return _resilientClient;
412
418
  }
413
419
  function getRawClient() {
@@ -896,6 +902,12 @@ async function ensureSchema() {
896
902
  } catch {
897
903
  }
898
904
  }
905
+ try {
906
+ await client.execute(
907
+ `CREATE INDEX IF NOT EXISTS idx_memories_content_hash ON memories(content_hash, agent_id)`
908
+ );
909
+ } catch {
910
+ }
899
911
  await client.executeMultiple(`
900
912
  CREATE TABLE IF NOT EXISTS entities (
901
913
  id TEXT PRIMARY KEY,
@@ -948,7 +960,30 @@ async function ensureSchema() {
948
960
  entity_id TEXT NOT NULL,
949
961
  PRIMARY KEY (hyperedge_id, entity_id)
950
962
  );
963
+
964
+ CREATE VIRTUAL TABLE IF NOT EXISTS entities_fts USING fts5(
965
+ name,
966
+ content=entities,
967
+ content_rowid=rowid
968
+ );
969
+
970
+ CREATE TRIGGER IF NOT EXISTS entities_fts_ai AFTER INSERT ON entities BEGIN
971
+ INSERT INTO entities_fts(rowid, name) VALUES (new.rowid, new.name);
972
+ END;
973
+
974
+ CREATE TRIGGER IF NOT EXISTS entities_fts_ad AFTER DELETE ON entities BEGIN
975
+ INSERT INTO entities_fts(entities_fts, rowid, name) VALUES('delete', old.rowid, old.name);
976
+ END;
977
+
978
+ CREATE TRIGGER IF NOT EXISTS entities_fts_au AFTER UPDATE ON entities BEGIN
979
+ INSERT INTO entities_fts(entities_fts, rowid, name) VALUES('delete', old.rowid, old.name);
980
+ INSERT INTO entities_fts(rowid, name) VALUES (new.rowid, new.name);
981
+ END;
951
982
  `);
983
+ try {
984
+ await client.execute("INSERT INTO entities_fts(entities_fts) VALUES('rebuild')");
985
+ } catch {
986
+ }
952
987
  await client.executeMultiple(`
953
988
  CREATE TABLE IF NOT EXISTS entity_aliases (
954
989
  alias TEXT NOT NULL PRIMARY KEY,
@@ -1129,6 +1164,33 @@ async function ensureSchema() {
1129
1164
  CREATE INDEX IF NOT EXISTS idx_conversations_channel
1130
1165
  ON conversations(channel_id);
1131
1166
  `);
1167
+ await client.executeMultiple(`
1168
+ CREATE TABLE IF NOT EXISTS session_agent_map (
1169
+ session_uuid TEXT PRIMARY KEY,
1170
+ agent_id TEXT NOT NULL,
1171
+ session_name TEXT,
1172
+ task_id TEXT,
1173
+ project_name TEXT,
1174
+ started_at TEXT NOT NULL
1175
+ );
1176
+
1177
+ CREATE INDEX IF NOT EXISTS idx_session_agent_map_agent
1178
+ ON session_agent_map(agent_id);
1179
+ `);
1180
+ try {
1181
+ const mapCount = await client.execute({ sql: `SELECT COUNT(*) as cnt FROM session_agent_map`, args: [] });
1182
+ if (Number(mapCount.rows[0]?.cnt ?? 0) === 0) {
1183
+ await client.execute({
1184
+ sql: `INSERT OR IGNORE INTO session_agent_map (session_uuid, agent_id, session_name, started_at)
1185
+ SELECT session_id, agent_id, '', MIN(timestamp)
1186
+ FROM memories
1187
+ WHERE session_id IS NOT NULL AND session_id != '' AND agent_id IS NOT NULL AND agent_id != ''
1188
+ GROUP BY session_id, agent_id`,
1189
+ args: []
1190
+ });
1191
+ }
1192
+ } catch {
1193
+ }
1132
1194
  try {
1133
1195
  await client.execute({
1134
1196
  sql: `ALTER TABLE tasks ADD COLUMN budget_tokens INTEGER`,
@@ -1262,15 +1324,41 @@ async function ensureSchema() {
1262
1324
  });
1263
1325
  } catch {
1264
1326
  }
1327
+ for (const col of [
1328
+ "ALTER TABLE memories ADD COLUMN intent TEXT",
1329
+ "ALTER TABLE memories ADD COLUMN outcome TEXT",
1330
+ "ALTER TABLE memories ADD COLUMN domain TEXT",
1331
+ "ALTER TABLE memories ADD COLUMN referenced_entities TEXT",
1332
+ "ALTER TABLE memories ADD COLUMN retrieval_count INTEGER DEFAULT 0",
1333
+ "ALTER TABLE memories ADD COLUMN chain_position TEXT",
1334
+ "ALTER TABLE memories ADD COLUMN review_status TEXT",
1335
+ "ALTER TABLE memories ADD COLUMN context_window_pct INTEGER",
1336
+ "ALTER TABLE memories ADD COLUMN file_paths TEXT",
1337
+ "ALTER TABLE memories ADD COLUMN commit_hash TEXT",
1338
+ "ALTER TABLE memories ADD COLUMN duration_ms INTEGER",
1339
+ "ALTER TABLE memories ADD COLUMN token_cost REAL",
1340
+ "ALTER TABLE memories ADD COLUMN audience TEXT",
1341
+ "ALTER TABLE memories ADD COLUMN language_type TEXT",
1342
+ "ALTER TABLE memories ADD COLUMN parent_memory_id TEXT"
1343
+ ]) {
1344
+ try {
1345
+ await client.execute(col);
1346
+ } catch {
1347
+ }
1348
+ }
1265
1349
  }
1266
1350
  async function disposeDatabase() {
1351
+ if (_daemonClient) {
1352
+ _daemonClient.close();
1353
+ _daemonClient = null;
1354
+ }
1267
1355
  if (_client) {
1268
1356
  _client.close();
1269
1357
  _client = null;
1270
1358
  _resilientClient = null;
1271
1359
  }
1272
1360
  }
1273
- var _client, _resilientClient, initTurso, disposeTurso;
1361
+ var _client, _resilientClient, _daemonClient, initTurso, disposeTurso;
1274
1362
  var init_database = __esm({
1275
1363
  "src/lib/database.ts"() {
1276
1364
  "use strict";
@@ -1278,6 +1366,7 @@ var init_database = __esm({
1278
1366
  init_employees();
1279
1367
  _client = null;
1280
1368
  _resilientClient = null;
1369
+ _daemonClient = null;
1281
1370
  initTurso = initDatabase;
1282
1371
  disposeTurso = disposeDatabase;
1283
1372
  }
@@ -1314,12 +1403,20 @@ async function getMasterKey() {
1314
1403
  }
1315
1404
  const keyPath = getKeyPath();
1316
1405
  if (!existsSync3(keyPath)) {
1406
+ process.stderr.write(
1407
+ `[keychain] Key not found at ${keyPath} (HOME=${os3.homedir()}, EXE_OS_DIR=${process.env.EXE_OS_DIR ?? "unset"})
1408
+ `
1409
+ );
1317
1410
  return null;
1318
1411
  }
1319
1412
  try {
1320
1413
  const content = await readFile3(keyPath, "utf-8");
1321
1414
  return Buffer.from(content.trim(), "base64");
1322
- } catch {
1415
+ } catch (err) {
1416
+ process.stderr.write(
1417
+ `[keychain] Key read failed at ${keyPath}: ${err instanceof Error ? err.message : String(err)}
1418
+ `
1419
+ );
1323
1420
  return null;
1324
1421
  }
1325
1422
  }
@@ -1830,6 +1927,7 @@ __export(store_exports, {
1830
1927
  vectorToBlob: () => vectorToBlob,
1831
1928
  writeMemory: () => writeMemory
1832
1929
  });
1930
+ import { createHash } from "crypto";
1833
1931
  function isBusyError2(err) {
1834
1932
  if (err instanceof Error) {
1835
1933
  const msg = err.message.toLowerCase();
@@ -1903,12 +2001,52 @@ function classifyTier(record) {
1903
2001
  if (["store_memory", "manual"].includes(record.tool_name ?? "") && (record.importance ?? 0) >= 5) return 2;
1904
2002
  return 3;
1905
2003
  }
2004
+ function inferFilePaths(record) {
2005
+ if (!["Read", "Write", "Edit"].includes(record.tool_name)) return null;
2006
+ const firstLine = record.raw_text.split("\n")[0] ?? "";
2007
+ const match = firstLine.match(/(\/[\w./-]+\.\w+)/);
2008
+ return match ? JSON.stringify([match[1]]) : null;
2009
+ }
2010
+ function inferCommitHash(record) {
2011
+ if (record.tool_name !== "Bash") return null;
2012
+ const match = record.raw_text.match(/\b([a-f0-9]{7,40})\b/);
2013
+ return match ? match[1] : null;
2014
+ }
2015
+ function inferLanguageType(record) {
2016
+ const text = record.raw_text;
2017
+ if (!text || text.length < 10) return null;
2018
+ const trimmed = text.trimStart();
2019
+ if (trimmed.startsWith("{") || trimmed.startsWith("[")) return "json";
2020
+ if (/\b(SELECT|INSERT|UPDATE|DELETE|CREATE TABLE|ALTER TABLE)\b/i.test(text)) return "sql";
2021
+ if (/\b(function |const |import |export |class |def |async |=>)\b/.test(text)) return "code";
2022
+ if (trimmed.startsWith("#") || trimmed.startsWith("*")) return "prose";
2023
+ return "mixed";
2024
+ }
2025
+ function inferDomain(record) {
2026
+ const proj = (record.project_name ?? "").toLowerCase();
2027
+ if (proj.includes("marketing") || proj.includes("content")) return "marketing";
2028
+ if (proj.includes("crm") || proj.includes("customer")) return "customer";
2029
+ return null;
2030
+ }
1906
2031
  async function writeMemory(record) {
1907
2032
  if (record.vector !== null && record.vector.length !== EMBEDDING_DIM) {
1908
2033
  throw new Error(
1909
2034
  `Expected ${EMBEDDING_DIM}-dim vector, got ${record.vector.length}`
1910
2035
  );
1911
2036
  }
2037
+ const contentHash = createHash("md5").update(record.raw_text).digest("hex");
2038
+ if (_pendingRecords.some((r) => r.content_hash === contentHash && r.agent_id === record.agent_id)) {
2039
+ return;
2040
+ }
2041
+ try {
2042
+ const client = getClient();
2043
+ const existing = await client.execute({
2044
+ sql: "SELECT id FROM memories WHERE content_hash = ? AND agent_id = ? LIMIT 1",
2045
+ args: [contentHash, record.agent_id]
2046
+ });
2047
+ if (existing.rows.length > 0) return;
2048
+ } catch {
2049
+ }
1912
2050
  const dbRow = {
1913
2051
  id: record.id,
1914
2052
  agent_id: record.agent_id,
@@ -1938,7 +2076,23 @@ async function writeMemory(record) {
1938
2076
  supersedes_id: record.supersedes_id ?? null,
1939
2077
  draft: record.draft ? 1 : 0,
1940
2078
  memory_type: record.memory_type ?? "raw",
1941
- trajectory: record.trajectory ? JSON.stringify(record.trajectory) : null
2079
+ trajectory: record.trajectory ? JSON.stringify(record.trajectory) : null,
2080
+ content_hash: contentHash,
2081
+ intent: record.intent ?? null,
2082
+ outcome: record.outcome ?? null,
2083
+ domain: record.domain ?? inferDomain(record),
2084
+ referenced_entities: record.referenced_entities ?? null,
2085
+ retrieval_count: record.retrieval_count ?? 0,
2086
+ chain_position: record.chain_position ?? null,
2087
+ review_status: record.review_status ?? null,
2088
+ context_window_pct: record.context_window_pct ?? null,
2089
+ file_paths: record.file_paths ?? inferFilePaths(record),
2090
+ commit_hash: record.commit_hash ?? inferCommitHash(record),
2091
+ duration_ms: record.duration_ms ?? null,
2092
+ token_cost: record.token_cost ?? null,
2093
+ audience: record.audience ?? null,
2094
+ language_type: record.language_type ?? inferLanguageType(record),
2095
+ parent_memory_id: record.parent_memory_id ?? null
1942
2096
  };
1943
2097
  _pendingRecords.push(dbRow);
1944
2098
  orgBus.emit({
@@ -1996,80 +2150,85 @@ async function flushBatch() {
1996
2150
  const draft = row.draft ? 1 : 0;
1997
2151
  const memoryType = row.memory_type ?? "raw";
1998
2152
  const trajectory = row.trajectory ?? null;
1999
- return {
2000
- sql: hasVector ? `INSERT OR IGNORE INTO memories
2001
- (id, agent_id, agent_role, session_id, timestamp,
2153
+ const contentHash = row.content_hash ?? null;
2154
+ const intent = row.intent ?? null;
2155
+ const outcome = row.outcome ?? null;
2156
+ const domain = row.domain ?? null;
2157
+ const referencedEntities = row.referenced_entities ?? null;
2158
+ const retrievalCount = row.retrieval_count ?? 0;
2159
+ const chainPosition = row.chain_position ?? null;
2160
+ const reviewStatus = row.review_status ?? null;
2161
+ const contextWindowPct = row.context_window_pct ?? null;
2162
+ const filePaths = row.file_paths ?? null;
2163
+ const commitHash = row.commit_hash ?? null;
2164
+ const durationMs = row.duration_ms ?? null;
2165
+ const tokenCost = row.token_cost ?? null;
2166
+ const audience = row.audience ?? null;
2167
+ const languageType = row.language_type ?? null;
2168
+ const parentMemoryId = row.parent_memory_id ?? null;
2169
+ const cols = `id, agent_id, agent_role, session_id, timestamp,
2002
2170
  tool_name, project_name,
2003
2171
  has_error, raw_text, vector, version, task_id, importance, status,
2004
2172
  confidence, last_accessed,
2005
2173
  workspace_id, document_id, user_id, char_offset, page_number,
2006
- source_path, source_type, tier, supersedes_id, draft, memory_type, trajectory)
2007
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, vector32(?), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` : `INSERT OR IGNORE INTO memories
2008
- (id, agent_id, agent_role, session_id, timestamp,
2009
- tool_name, project_name,
2010
- has_error, raw_text, vector, version, task_id, importance, status,
2011
- confidence, last_accessed,
2012
- workspace_id, document_id, user_id, char_offset, page_number,
2013
- source_path, source_type, tier, supersedes_id, draft, memory_type, trajectory)
2014
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
2015
- args: hasVector ? [
2016
- row.id,
2017
- row.agent_id,
2018
- row.agent_role,
2019
- row.session_id,
2020
- row.timestamp,
2021
- row.tool_name,
2022
- row.project_name,
2023
- row.has_error,
2024
- row.raw_text,
2025
- vectorToBlob(row.vector),
2026
- row.version,
2027
- taskId,
2028
- importance,
2029
- status,
2030
- confidence,
2031
- lastAccessed,
2032
- workspaceId,
2033
- documentId,
2034
- userId,
2035
- charOffset,
2036
- pageNumber,
2037
- sourcePath,
2038
- sourceType,
2039
- tier,
2040
- supersedesId,
2041
- draft,
2042
- memoryType,
2043
- trajectory
2044
- ] : [
2045
- row.id,
2046
- row.agent_id,
2047
- row.agent_role,
2048
- row.session_id,
2049
- row.timestamp,
2050
- row.tool_name,
2051
- row.project_name,
2052
- row.has_error,
2053
- row.raw_text,
2054
- row.version,
2055
- taskId,
2056
- importance,
2057
- status,
2058
- confidence,
2059
- lastAccessed,
2060
- workspaceId,
2061
- documentId,
2062
- userId,
2063
- charOffset,
2064
- pageNumber,
2065
- sourcePath,
2066
- sourceType,
2067
- tier,
2068
- supersedesId,
2069
- draft,
2070
- memoryType,
2071
- trajectory
2072
- ]
2174
+ source_path, source_type, tier, supersedes_id, draft, memory_type, trajectory, content_hash,
2175
+ intent, outcome, domain, referenced_entities, retrieval_count,
2176
+ chain_position, review_status, context_window_pct, file_paths, commit_hash,
2177
+ duration_ms, token_cost, audience, language_type, parent_memory_id`;
2178
+ const metaArgs = [
2179
+ intent,
2180
+ outcome,
2181
+ domain,
2182
+ referencedEntities,
2183
+ retrievalCount,
2184
+ chainPosition,
2185
+ reviewStatus,
2186
+ contextWindowPct,
2187
+ filePaths,
2188
+ commitHash,
2189
+ durationMs,
2190
+ tokenCost,
2191
+ audience,
2192
+ languageType,
2193
+ parentMemoryId
2194
+ ];
2195
+ const baseArgs = [
2196
+ row.id,
2197
+ row.agent_id,
2198
+ row.agent_role,
2199
+ row.session_id,
2200
+ row.timestamp,
2201
+ row.tool_name,
2202
+ row.project_name,
2203
+ row.has_error,
2204
+ row.raw_text
2205
+ ];
2206
+ const sharedArgs = [
2207
+ row.version,
2208
+ taskId,
2209
+ importance,
2210
+ status,
2211
+ confidence,
2212
+ lastAccessed,
2213
+ workspaceId,
2214
+ documentId,
2215
+ userId,
2216
+ charOffset,
2217
+ pageNumber,
2218
+ sourcePath,
2219
+ sourceType,
2220
+ tier,
2221
+ supersedesId,
2222
+ draft,
2223
+ memoryType,
2224
+ trajectory,
2225
+ contentHash
2226
+ ];
2227
+ return {
2228
+ sql: hasVector ? `INSERT OR IGNORE INTO memories (${cols})
2229
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, vector32(?), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` : `INSERT OR IGNORE INTO memories (${cols})
2230
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
2231
+ args: hasVector ? [...baseArgs, vectorToBlob(row.vector), ...sharedArgs, ...metaArgs] : [...baseArgs, ...sharedArgs, ...metaArgs]
2073
2232
  };
2074
2233
  };
2075
2234
  const globalClient = getClient();
@@ -2408,12 +2567,141 @@ var init_self_query_router = __esm({
2408
2567
  }
2409
2568
  });
2410
2569
 
2570
+ // src/lib/reranker.ts
2571
+ var reranker_exports = {};
2572
+ __export(reranker_exports, {
2573
+ disposeReranker: () => disposeReranker,
2574
+ getRerankerModelPath: () => getRerankerModelPath,
2575
+ isRerankerAvailable: () => isRerankerAvailable,
2576
+ rerank: () => rerank,
2577
+ rerankWithContext: () => rerankWithContext,
2578
+ rerankWithScores: () => rerankWithScores
2579
+ });
2580
+ import path5 from "path";
2581
+ import { existsSync as existsSync5 } from "fs";
2582
+ function resetIdleTimer() {
2583
+ if (_idleTimer) clearTimeout(_idleTimer);
2584
+ _idleTimer = setTimeout(() => {
2585
+ void disposeReranker();
2586
+ }, IDLE_TIMEOUT_MS);
2587
+ if (_idleTimer && typeof _idleTimer === "object" && "unref" in _idleTimer) {
2588
+ _idleTimer.unref();
2589
+ }
2590
+ }
2591
+ function isRerankerAvailable() {
2592
+ return existsSync5(path5.join(MODELS_DIR, RERANKER_MODEL_FILE));
2593
+ }
2594
+ function getRerankerModelPath() {
2595
+ return path5.join(MODELS_DIR, RERANKER_MODEL_FILE);
2596
+ }
2597
+ async function ensureLoaded() {
2598
+ if (_rerankerContext) {
2599
+ resetIdleTimer();
2600
+ return;
2601
+ }
2602
+ const modelPath = path5.join(MODELS_DIR, RERANKER_MODEL_FILE);
2603
+ if (!existsSync5(modelPath)) {
2604
+ throw new Error(
2605
+ `Reranker model not found at ${modelPath}. Run /exe-setup to download it.`
2606
+ );
2607
+ }
2608
+ process.stderr.write("[reranker] Loading Jina Reranker v3...\n");
2609
+ const { getLlama } = await import("node-llama-cpp");
2610
+ const llama = await getLlama();
2611
+ _rerankerModel = await llama.loadModel({ modelPath });
2612
+ _rerankerContext = await _rerankerModel.createEmbeddingContext();
2613
+ process.stderr.write("[reranker] Jina Reranker v3 loaded.\n");
2614
+ resetIdleTimer();
2615
+ }
2616
+ async function disposeReranker() {
2617
+ if (_idleTimer) {
2618
+ clearTimeout(_idleTimer);
2619
+ _idleTimer = null;
2620
+ }
2621
+ if (_rerankerContext) {
2622
+ try {
2623
+ await _rerankerContext.dispose();
2624
+ } catch {
2625
+ }
2626
+ _rerankerContext = null;
2627
+ }
2628
+ if (_rerankerModel) {
2629
+ try {
2630
+ await _rerankerModel.dispose();
2631
+ } catch {
2632
+ }
2633
+ _rerankerModel = null;
2634
+ }
2635
+ process.stderr.write("[reranker] Unloaded (idle timeout).\n");
2636
+ }
2637
+ async function rerankWithScores(query, texts, topK) {
2638
+ if (texts.length === 0) return [];
2639
+ await ensureLoaded();
2640
+ const ctx = _rerankerContext;
2641
+ const scored = [];
2642
+ for (let i = 0; i < texts.length; i++) {
2643
+ const text = texts[i] ?? "";
2644
+ try {
2645
+ const input2 = `query: ${query} document: ${text.slice(0, 512)}`;
2646
+ const embedding = await ctx.getEmbeddingFor(input2);
2647
+ const score = embedding.vector[0] ?? 0;
2648
+ scored.push({ text, score, index: i });
2649
+ } catch {
2650
+ scored.push({ text, score: -1, index: i });
2651
+ }
2652
+ }
2653
+ scored.sort((a, b) => b.score - a.score);
2654
+ return typeof topK === "number" ? scored.slice(0, topK) : scored;
2655
+ }
2656
+ async function rerank(query, candidates, topK = 5) {
2657
+ if (candidates.length === 0) return [];
2658
+ if (candidates.length <= topK) return candidates;
2659
+ const scored = await rerankWithScores(
2660
+ query,
2661
+ candidates.map((c) => c.raw_text),
2662
+ topK
2663
+ );
2664
+ return scored.map((s) => candidates[s.index]);
2665
+ }
2666
+ async function rerankWithContext(query, candidates, topK) {
2667
+ if (candidates.length === 0) return [];
2668
+ await ensureLoaded();
2669
+ const ctx = _rerankerContext;
2670
+ const scored = [];
2671
+ for (let i = 0; i < candidates.length; i++) {
2672
+ const candidate = candidates[i];
2673
+ try {
2674
+ const docText = candidate.context ? `[${candidate.context}] ${candidate.text.slice(0, 460)}` : candidate.text.slice(0, 512);
2675
+ const input2 = `query: ${query} document: ${docText}`;
2676
+ const embedding = await ctx.getEmbeddingFor(input2);
2677
+ const score = embedding.vector[0] ?? 0;
2678
+ scored.push({ text: candidate.text, score, index: i });
2679
+ } catch {
2680
+ scored.push({ text: candidate.text, score: -1, index: i });
2681
+ }
2682
+ }
2683
+ scored.sort((a, b) => b.score - a.score);
2684
+ return typeof topK === "number" ? scored.slice(0, topK) : scored;
2685
+ }
2686
+ var RERANKER_MODEL_FILE, IDLE_TIMEOUT_MS, _rerankerContext, _rerankerModel, _idleTimer;
2687
+ var init_reranker = __esm({
2688
+ "src/lib/reranker.ts"() {
2689
+ "use strict";
2690
+ init_config();
2691
+ RERANKER_MODEL_FILE = "jina-reranker-v3-q4_k_m.gguf";
2692
+ IDLE_TIMEOUT_MS = 6e4;
2693
+ _rerankerContext = null;
2694
+ _rerankerModel = null;
2695
+ _idleTimer = null;
2696
+ }
2697
+ });
2698
+
2411
2699
  // src/lib/exe-daemon-client.ts
2412
2700
  import net from "net";
2413
2701
  import { spawn } from "child_process";
2414
2702
  import { randomUUID as randomUUID2 } from "crypto";
2415
- import { existsSync as existsSync5, unlinkSync as unlinkSync2, readFileSync as readFileSync3, openSync, closeSync, statSync } from "fs";
2416
- import path5 from "path";
2703
+ import { existsSync as existsSync6, unlinkSync as unlinkSync2, readFileSync as readFileSync3, openSync, closeSync, statSync } from "fs";
2704
+ import path6 from "path";
2417
2705
  import { fileURLToPath } from "url";
2418
2706
  function handleData(chunk) {
2419
2707
  _buffer += chunk.toString();
@@ -2428,10 +2716,12 @@ function handleData(chunk) {
2428
2716
  if (!line) continue;
2429
2717
  try {
2430
2718
  const response = JSON.parse(line);
2431
- const entry = _pending.get(response.id);
2719
+ const id = response.id;
2720
+ if (!id) continue;
2721
+ const entry = _pending.get(id);
2432
2722
  if (entry) {
2433
2723
  clearTimeout(entry.timer);
2434
- _pending.delete(response.id);
2724
+ _pending.delete(id);
2435
2725
  entry.resolve(response);
2436
2726
  }
2437
2727
  } catch {
@@ -2439,7 +2729,7 @@ function handleData(chunk) {
2439
2729
  }
2440
2730
  }
2441
2731
  function cleanupStaleFiles() {
2442
- if (existsSync5(PID_PATH)) {
2732
+ if (existsSync6(PID_PATH)) {
2443
2733
  try {
2444
2734
  const pid = parseInt(readFileSync3(PID_PATH, "utf8").trim(), 10);
2445
2735
  if (pid > 0) {
@@ -2462,11 +2752,11 @@ function cleanupStaleFiles() {
2462
2752
  }
2463
2753
  }
2464
2754
  function findPackageRoot() {
2465
- let dir = path5.dirname(fileURLToPath(import.meta.url));
2466
- const { root } = path5.parse(dir);
2755
+ let dir = path6.dirname(fileURLToPath(import.meta.url));
2756
+ const { root } = path6.parse(dir);
2467
2757
  while (dir !== root) {
2468
- if (existsSync5(path5.join(dir, "package.json"))) return dir;
2469
- dir = path5.dirname(dir);
2758
+ if (existsSync6(path6.join(dir, "package.json"))) return dir;
2759
+ dir = path6.dirname(dir);
2470
2760
  }
2471
2761
  return null;
2472
2762
  }
@@ -2476,8 +2766,8 @@ function spawnDaemon() {
2476
2766
  process.stderr.write("[exed-client] WARN: cannot find package root\n");
2477
2767
  return;
2478
2768
  }
2479
- const daemonPath = path5.join(pkgRoot, "dist", "lib", "exe-daemon.js");
2480
- if (!existsSync5(daemonPath)) {
2769
+ const daemonPath = path6.join(pkgRoot, "dist", "lib", "exe-daemon.js");
2770
+ if (!existsSync6(daemonPath)) {
2481
2771
  process.stderr.write(`[exed-client] WARN: daemon script not found at ${daemonPath}
2482
2772
  `);
2483
2773
  return;
@@ -2485,7 +2775,7 @@ function spawnDaemon() {
2485
2775
  const resolvedPath = daemonPath;
2486
2776
  process.stderr.write(`[exed-client] Spawning daemon: ${resolvedPath}
2487
2777
  `);
2488
- const logPath = path5.join(path5.dirname(SOCKET_PATH), "exed.log");
2778
+ const logPath = path6.join(path6.dirname(SOCKET_PATH), "exed.log");
2489
2779
  let stderrFd = "ignore";
2490
2780
  try {
2491
2781
  stderrFd = openSync(logPath, "a");
@@ -2602,6 +2892,9 @@ async function connectEmbedDaemon() {
2602
2892
  return false;
2603
2893
  }
2604
2894
  function sendRequest(texts, priority) {
2895
+ return sendDaemonRequest({ texts, priority });
2896
+ }
2897
+ function sendDaemonRequest(payload, timeoutMs = REQUEST_TIMEOUT_MS) {
2605
2898
  return new Promise((resolve) => {
2606
2899
  if (!_socket || !_connected) {
2607
2900
  resolve({ error: "Not connected" });
@@ -2611,10 +2904,10 @@ function sendRequest(texts, priority) {
2611
2904
  const timer = setTimeout(() => {
2612
2905
  _pending.delete(id);
2613
2906
  resolve({ error: "Request timeout" });
2614
- }, REQUEST_TIMEOUT_MS);
2907
+ }, timeoutMs);
2615
2908
  _pending.set(id, { resolve, timer });
2616
2909
  try {
2617
- _socket.write(JSON.stringify({ id, texts, priority }) + "\n");
2910
+ _socket.write(JSON.stringify({ id, ...payload }) + "\n");
2618
2911
  } catch {
2619
2912
  clearTimeout(timer);
2620
2913
  _pending.delete(id);
@@ -2624,34 +2917,15 @@ function sendRequest(texts, priority) {
2624
2917
  }
2625
2918
  async function pingDaemon() {
2626
2919
  if (!_socket || !_connected) return null;
2627
- return new Promise((resolve) => {
2628
- const id = randomUUID2();
2629
- const timer = setTimeout(() => {
2630
- _pending.delete(id);
2631
- resolve(null);
2632
- }, 5e3);
2633
- _pending.set(id, {
2634
- resolve: (data) => {
2635
- if (data.health) {
2636
- resolve(data.health);
2637
- } else {
2638
- resolve(null);
2639
- }
2640
- },
2641
- timer
2642
- });
2643
- try {
2644
- _socket.write(JSON.stringify({ id, type: "health" }) + "\n");
2645
- } catch {
2646
- clearTimeout(timer);
2647
- _pending.delete(id);
2648
- resolve(null);
2649
- }
2650
- });
2920
+ const response = await sendDaemonRequest({ type: "health" }, 5e3);
2921
+ if (response.health) {
2922
+ return response.health;
2923
+ }
2924
+ return null;
2651
2925
  }
2652
2926
  function killAndRespawnDaemon() {
2653
2927
  process.stderr.write("[exed-client] Killing daemon for restart...\n");
2654
- if (existsSync5(PID_PATH)) {
2928
+ if (existsSync6(PID_PATH)) {
2655
2929
  try {
2656
2930
  const pid = parseInt(readFileSync3(PID_PATH, "utf8").trim(), 10);
2657
2931
  if (pid > 0) {
@@ -2737,9 +3011,9 @@ var init_exe_daemon_client = __esm({
2737
3011
  "src/lib/exe-daemon-client.ts"() {
2738
3012
  "use strict";
2739
3013
  init_config();
2740
- SOCKET_PATH = process.env.EXE_DAEMON_SOCK ?? process.env.EXE_EMBED_SOCK ?? path5.join(EXE_AI_DIR, "exed.sock");
2741
- PID_PATH = process.env.EXE_DAEMON_PID ?? process.env.EXE_EMBED_PID ?? path5.join(EXE_AI_DIR, "exed.pid");
2742
- SPAWN_LOCK_PATH = path5.join(EXE_AI_DIR, "exed-spawn.lock");
3014
+ SOCKET_PATH = process.env.EXE_DAEMON_SOCK ?? process.env.EXE_EMBED_SOCK ?? path6.join(EXE_AI_DIR, "exed.sock");
3015
+ PID_PATH = process.env.EXE_DAEMON_PID ?? process.env.EXE_EMBED_PID ?? path6.join(EXE_AI_DIR, "exed.pid");
3016
+ SPAWN_LOCK_PATH = path6.join(EXE_AI_DIR, "exed-spawn.lock");
2743
3017
  SPAWN_LOCK_STALE_MS = 3e4;
2744
3018
  CONNECT_TIMEOUT_MS = 15e3;
2745
3019
  REQUEST_TIMEOUT_MS = 3e4;
@@ -2828,7 +3102,7 @@ __export(project_name_exports, {
2828
3102
  getProjectName: () => getProjectName
2829
3103
  });
2830
3104
  import { execSync as execSync2 } from "child_process";
2831
- import path6 from "path";
3105
+ import path7 from "path";
2832
3106
  function getProjectName(cwd) {
2833
3107
  const dir = cwd ?? process.cwd();
2834
3108
  if (_cached && _cachedCwd === dir) return _cached;
@@ -2841,7 +3115,7 @@ function getProjectName(cwd) {
2841
3115
  timeout: 2e3,
2842
3116
  stdio: ["pipe", "pipe", "pipe"]
2843
3117
  }).trim();
2844
- repoRoot = path6.dirname(gitCommonDir);
3118
+ repoRoot = path7.dirname(gitCommonDir);
2845
3119
  } catch {
2846
3120
  repoRoot = execSync2("git rev-parse --show-toplevel", {
2847
3121
  cwd: dir,
@@ -2850,11 +3124,11 @@ function getProjectName(cwd) {
2850
3124
  stdio: ["pipe", "pipe", "pipe"]
2851
3125
  }).trim();
2852
3126
  }
2853
- _cached = path6.basename(repoRoot);
3127
+ _cached = path7.basename(repoRoot);
2854
3128
  _cachedCwd = dir;
2855
3129
  return _cached;
2856
3130
  } catch {
2857
- _cached = path6.basename(dir);
3131
+ _cached = path7.basename(dir);
2858
3132
  _cachedCwd = dir;
2859
3133
  return _cached;
2860
3134
  }
@@ -2878,8 +3152,8 @@ __export(file_grep_exports, {
2878
3152
  grepProjectFiles: () => grepProjectFiles
2879
3153
  });
2880
3154
  import { execSync as execSync3 } from "child_process";
2881
- import { readFileSync as readFileSync4, readdirSync as readdirSync2, statSync as statSync2, existsSync as existsSync6 } from "fs";
2882
- import path7 from "path";
3155
+ import { readFileSync as readFileSync4, readdirSync as readdirSync2, statSync as statSync2, existsSync as existsSync7 } from "fs";
3156
+ import path8 from "path";
2883
3157
  import crypto from "crypto";
2884
3158
  function hasRipgrep() {
2885
3159
  if (_hasRg === null) {
@@ -2919,7 +3193,7 @@ async function grepProjectFiles(query, projectRoot, options) {
2919
3193
  session_id: "file-grep",
2920
3194
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2921
3195
  tool_name: "file_grep",
2922
- project_name: path7.basename(projectRoot),
3196
+ project_name: path8.basename(projectRoot),
2923
3197
  has_error: false,
2924
3198
  raw_text: `${prefix} ${buildSnippet(hit, projectRoot)}`,
2925
3199
  vector: null,
@@ -2993,7 +3267,7 @@ function grepWithNodeFs(pattern, projectRoot, patterns) {
2993
3267
  const files = collectFiles(projectRoot, patterns ?? DEFAULT_PATTERNS);
2994
3268
  const hits = [];
2995
3269
  for (const filePath of files.slice(0, MAX_FILES)) {
2996
- const absPath = path7.join(projectRoot, filePath);
3270
+ const absPath = path8.join(projectRoot, filePath);
2997
3271
  try {
2998
3272
  const stat = statSync2(absPath);
2999
3273
  if (stat.size > MAX_FILE_SIZE) continue;
@@ -3020,15 +3294,15 @@ function collectFiles(root, patterns) {
3020
3294
  const files = [];
3021
3295
  function walk(dir, relative) {
3022
3296
  if (files.length >= MAX_FILES) return;
3023
- const basename = path7.basename(dir);
3297
+ const basename = path8.basename(dir);
3024
3298
  if (EXCLUDE_DIRS.includes(basename)) return;
3025
3299
  try {
3026
3300
  const entries = readdirSync2(dir, { withFileTypes: true });
3027
3301
  for (const entry of entries) {
3028
3302
  if (files.length >= MAX_FILES) return;
3029
- const rel = path7.join(relative, entry.name);
3303
+ const rel = path8.join(relative, entry.name);
3030
3304
  if (entry.isDirectory()) {
3031
- walk(path7.join(dir, entry.name), rel);
3305
+ walk(path8.join(dir, entry.name), rel);
3032
3306
  } else if (entry.isFile()) {
3033
3307
  for (const pat of patterns) {
3034
3308
  if (matchGlob(rel, pat)) {
@@ -3060,7 +3334,7 @@ function matchGlob(filePath, pattern) {
3060
3334
  if (slashIdx !== -1) {
3061
3335
  const dir = pattern.slice(0, slashIdx);
3062
3336
  const ext2 = pattern.slice(slashIdx + 1).replace("*", "");
3063
- const fileDir = path7.dirname(filePath);
3337
+ const fileDir = path8.dirname(filePath);
3064
3338
  return fileDir === dir && filePath.endsWith(ext2);
3065
3339
  }
3066
3340
  const ext = pattern.replace("*", "");
@@ -3068,8 +3342,8 @@ function matchGlob(filePath, pattern) {
3068
3342
  }
3069
3343
  function buildSnippet(hit, projectRoot) {
3070
3344
  try {
3071
- const absPath = path7.join(projectRoot, hit.filePath);
3072
- if (!existsSync6(absPath)) return hit.matchLine;
3345
+ const absPath = path8.join(projectRoot, hit.filePath);
3346
+ if (!existsSync7(absPath)) return hit.matchLine;
3073
3347
  const lines = readFileSync4(absPath, "utf8").split("\n");
3074
3348
  const start = Math.max(0, hit.lineNumber - 3);
3075
3349
  const end = Math.min(lines.length, hit.lineNumber + 2);
@@ -3095,111 +3369,574 @@ var init_file_grep = __esm({
3095
3369
  }
3096
3370
  });
3097
3371
 
3098
- // src/lib/reranker.ts
3099
- var reranker_exports = {};
3100
- __export(reranker_exports, {
3101
- disposeReranker: () => disposeReranker,
3102
- getRerankerModelPath: () => getRerankerModelPath,
3103
- isRerankerAvailable: () => isRerankerAvailable,
3104
- rerank: () => rerank,
3105
- rerankWithScores: () => rerankWithScores
3372
+ // src/lib/graph-query.ts
3373
+ var graph_query_exports = {};
3374
+ __export(graph_query_exports, {
3375
+ getConversationPartners: () => getConversationPartners,
3376
+ getEntityByName: () => getEntityByName,
3377
+ getEntityNeighbors: () => getEntityNeighbors,
3378
+ getEntityTimeline: () => getEntityTimeline,
3379
+ getGraphStats: () => getGraphStats,
3380
+ getHotEntities: () => getHotEntities,
3381
+ getRelationshipFrequency: () => getRelationshipFrequency,
3382
+ getRelationships: () => getRelationships,
3383
+ searchEntities: () => searchEntities,
3384
+ traverseChain: () => traverseChain
3106
3385
  });
3107
- import path8 from "path";
3108
- import { existsSync as existsSync7 } from "fs";
3109
- function resetIdleTimer() {
3110
- if (_idleTimer) clearTimeout(_idleTimer);
3111
- _idleTimer = setTimeout(() => {
3112
- void disposeReranker();
3113
- }, IDLE_TIMEOUT_MS);
3114
- if (_idleTimer && typeof _idleTimer === "object" && "unref" in _idleTimer) {
3115
- _idleTimer.unref();
3386
+ async function getEntityByName(client, name, type) {
3387
+ const sql = type ? `SELECT * FROM entities WHERE LOWER(name) = LOWER(?) AND type = ? LIMIT 1` : `SELECT * FROM entities WHERE LOWER(name) = LOWER(?) LIMIT 1`;
3388
+ const args = type ? [name, type] : [name];
3389
+ const result = await client.execute({ sql, args });
3390
+ if (result.rows.length === 0) return null;
3391
+ const row = result.rows[0];
3392
+ return {
3393
+ id: String(row.id),
3394
+ name: String(row.name),
3395
+ type: String(row.type),
3396
+ firstSeen: String(row.first_seen),
3397
+ lastSeen: String(row.last_seen),
3398
+ properties: JSON.parse(String(row.properties ?? "{}"))
3399
+ };
3400
+ }
3401
+ async function searchEntities(client, query, limit = 10) {
3402
+ const result = await client.execute({
3403
+ sql: `SELECT * FROM entities WHERE LOWER(name) LIKE ? ORDER BY last_seen DESC LIMIT ?`,
3404
+ args: [`%${query.toLowerCase()}%`, limit]
3405
+ });
3406
+ return result.rows.map((row) => ({
3407
+ id: String(row.id),
3408
+ name: String(row.name),
3409
+ type: String(row.type),
3410
+ firstSeen: String(row.first_seen),
3411
+ lastSeen: String(row.last_seen),
3412
+ properties: JSON.parse(String(row.properties ?? "{}"))
3413
+ }));
3414
+ }
3415
+ async function getRelationships(client, entityId, options) {
3416
+ const direction = options?.direction ?? "both";
3417
+ let sql;
3418
+ const args = [];
3419
+ if (direction === "outgoing") {
3420
+ sql = `SELECT r.*, s.name as source_name, t.name as target_name
3421
+ FROM relationships r
3422
+ JOIN entities s ON r.source_entity_id = s.id
3423
+ JOIN entities t ON r.target_entity_id = t.id
3424
+ WHERE r.source_entity_id = ?`;
3425
+ args.push(entityId);
3426
+ } else if (direction === "incoming") {
3427
+ sql = `SELECT r.*, s.name as source_name, t.name as target_name
3428
+ FROM relationships r
3429
+ JOIN entities s ON r.source_entity_id = s.id
3430
+ JOIN entities t ON r.target_entity_id = t.id
3431
+ WHERE r.target_entity_id = ?`;
3432
+ args.push(entityId);
3433
+ } else {
3434
+ sql = `SELECT r.*, s.name as source_name, t.name as target_name
3435
+ FROM relationships r
3436
+ JOIN entities s ON r.source_entity_id = s.id
3437
+ JOIN entities t ON r.target_entity_id = t.id
3438
+ WHERE r.source_entity_id = ? OR r.target_entity_id = ?`;
3439
+ args.push(entityId, entityId);
3440
+ }
3441
+ if (options?.type) {
3442
+ sql += ` AND r.type = ?`;
3443
+ args.push(options.type);
3444
+ }
3445
+ sql += ` ORDER BY r.weight DESC, r.timestamp DESC`;
3446
+ const result = await client.execute({ sql, args });
3447
+ return result.rows.map((row) => ({
3448
+ id: String(row.id),
3449
+ sourceEntityId: String(row.source_entity_id),
3450
+ targetEntityId: String(row.target_entity_id),
3451
+ type: String(row.type),
3452
+ weight: Number(row.weight),
3453
+ timestamp: String(row.timestamp),
3454
+ properties: JSON.parse(String(row.properties ?? "{}")),
3455
+ confidence: Number(row.confidence ?? 1),
3456
+ confidenceLabel: String(row.confidence_label ?? "extracted"),
3457
+ sourceName: String(row.source_name),
3458
+ targetName: String(row.target_name)
3459
+ }));
3460
+ }
3461
+ async function traverseChain(client, startEntityId, relationshipType, maxDepth = 3) {
3462
+ const result = await client.execute({
3463
+ sql: `WITH RECURSIVE chain(entity_id, depth, rel_type) AS (
3464
+ SELECT ?, 0, ''
3465
+ UNION ALL
3466
+ SELECT r.target_entity_id, c.depth + 1, r.type
3467
+ FROM chain c
3468
+ JOIN relationships r ON r.source_entity_id = c.entity_id
3469
+ WHERE r.type = ? AND c.depth < ?
3470
+ )
3471
+ SELECT DISTINCT e.*, chain.depth, chain.rel_type
3472
+ FROM chain
3473
+ JOIN entities e ON e.id = chain.entity_id
3474
+ ORDER BY chain.depth ASC`,
3475
+ args: [startEntityId, relationshipType, maxDepth]
3476
+ });
3477
+ return result.rows.map((row) => ({
3478
+ entity: {
3479
+ id: String(row.id),
3480
+ name: String(row.name),
3481
+ type: String(row.type),
3482
+ firstSeen: String(row.first_seen),
3483
+ lastSeen: String(row.last_seen),
3484
+ properties: JSON.parse(String(row.properties ?? "{}"))
3485
+ },
3486
+ depth: Number(row.depth),
3487
+ relationship: String(row.rel_type)
3488
+ }));
3489
+ }
3490
+ async function getEntityNeighbors(client, entityId, maxHops = 2) {
3491
+ const result = await client.execute({
3492
+ sql: `WITH RECURSIVE neighborhood(entity_id, depth, rel_type, rel_confidence) AS (
3493
+ SELECT ?, 0, '', 1.0
3494
+ UNION ALL
3495
+ SELECT CASE
3496
+ WHEN r.source_entity_id = n.entity_id THEN r.target_entity_id
3497
+ ELSE r.source_entity_id
3498
+ END, n.depth + 1, r.type, COALESCE(r.confidence, 1.0)
3499
+ FROM neighborhood n
3500
+ JOIN relationships r ON r.source_entity_id = n.entity_id
3501
+ OR r.target_entity_id = n.entity_id
3502
+ WHERE n.depth < ?
3503
+ )
3504
+ SELECT DISTINCT e.*, MIN(neighborhood.depth) as depth,
3505
+ neighborhood.rel_type, neighborhood.rel_confidence
3506
+ FROM neighborhood
3507
+ JOIN entities e ON e.id = neighborhood.entity_id
3508
+ GROUP BY e.id
3509
+ ORDER BY depth ASC, e.last_seen DESC
3510
+ LIMIT 50`,
3511
+ args: [entityId, maxHops]
3512
+ });
3513
+ return result.rows.map((row) => ({
3514
+ entity: {
3515
+ id: String(row.id),
3516
+ name: String(row.name),
3517
+ type: String(row.type),
3518
+ firstSeen: String(row.first_seen),
3519
+ lastSeen: String(row.last_seen),
3520
+ properties: JSON.parse(String(row.properties ?? "{}"))
3521
+ },
3522
+ depth: Number(row.depth),
3523
+ relType: String(row.rel_type ?? ""),
3524
+ relConfidence: Number(row.rel_confidence ?? 1)
3525
+ }));
3526
+ }
3527
+ async function getGraphStats(client) {
3528
+ const entityCount = await client.execute("SELECT COUNT(*) as cnt FROM entities");
3529
+ const relCount = await client.execute("SELECT COUNT(*) as cnt FROM relationships");
3530
+ const typeResult = await client.execute(
3531
+ "SELECT type, COUNT(*) as cnt FROM entities GROUP BY type ORDER BY cnt DESC"
3532
+ );
3533
+ const types = {};
3534
+ for (const row of typeResult.rows) {
3535
+ types[String(row.type)] = Number(row.cnt);
3116
3536
  }
3537
+ return {
3538
+ entities: Number(entityCount.rows[0]?.cnt ?? 0),
3539
+ relationships: Number(relCount.rows[0]?.cnt ?? 0),
3540
+ types
3541
+ };
3117
3542
  }
3118
- function isRerankerAvailable() {
3119
- return existsSync7(path8.join(MODELS_DIR, RERANKER_MODEL_FILE));
3543
+ async function getEntityTimeline(client, entityId, since) {
3544
+ const args = [entityId, entityId];
3545
+ let sinceClause = "";
3546
+ if (since) {
3547
+ sinceClause = " AND r.timestamp >= ?";
3548
+ args.push(since.toISOString());
3549
+ }
3550
+ const result = await client.execute({
3551
+ sql: `SELECT r.*, s.name as source_name, t.name as target_name
3552
+ FROM relationships r
3553
+ JOIN entities s ON r.source_entity_id = s.id
3554
+ JOIN entities t ON r.target_entity_id = t.id
3555
+ WHERE (r.source_entity_id = ? OR r.target_entity_id = ?)${sinceClause}
3556
+ ORDER BY r.timestamp DESC`,
3557
+ args
3558
+ });
3559
+ return result.rows.map((row) => ({
3560
+ relationship: {
3561
+ id: String(row.id),
3562
+ sourceEntityId: String(row.source_entity_id),
3563
+ targetEntityId: String(row.target_entity_id),
3564
+ type: String(row.type),
3565
+ weight: Number(row.weight),
3566
+ timestamp: String(row.timestamp),
3567
+ properties: JSON.parse(String(row.properties ?? "{}")),
3568
+ sourceName: String(row.source_name),
3569
+ targetName: String(row.target_name)
3570
+ },
3571
+ timestamp: String(row.timestamp)
3572
+ }));
3120
3573
  }
3121
- function getRerankerModelPath() {
3122
- return path8.join(MODELS_DIR, RERANKER_MODEL_FILE);
3574
+ async function getRelationshipFrequency(client, entityId, options) {
3575
+ const granularity = options?.granularity ?? "day";
3576
+ const args = [entityId, entityId];
3577
+ let typeClause = "";
3578
+ if (options?.type) {
3579
+ typeClause = " AND r.type = ?";
3580
+ args.push(options.type);
3581
+ }
3582
+ let bucketExpr;
3583
+ switch (granularity) {
3584
+ case "week":
3585
+ bucketExpr = `strftime('%Y', r.timestamp) || '-W' || strftime('%W', r.timestamp)`;
3586
+ break;
3587
+ case "month":
3588
+ bucketExpr = `strftime('%Y-%m', r.timestamp)`;
3589
+ break;
3590
+ default:
3591
+ bucketExpr = `strftime('%Y-%m-%d', r.timestamp)`;
3592
+ }
3593
+ const result = await client.execute({
3594
+ sql: `SELECT ${bucketExpr} as bucket, COUNT(*) as cnt, r.type as rel_type
3595
+ FROM relationships r
3596
+ WHERE (r.source_entity_id = ? OR r.target_entity_id = ?)${typeClause}
3597
+ GROUP BY bucket${options?.type ? "" : ", r.type"}
3598
+ ORDER BY bucket DESC`,
3599
+ args
3600
+ });
3601
+ return result.rows.map((row) => ({
3602
+ bucket: String(row.bucket),
3603
+ count: Number(row.cnt),
3604
+ relationshipType: options?.type ? options.type : String(row.rel_type)
3605
+ }));
3123
3606
  }
3124
- async function ensureLoaded() {
3125
- if (_rerankerContext) {
3126
- resetIdleTimer();
3127
- return;
3607
+ async function getConversationPartners(client, contactEntityId, since) {
3608
+ const args = [contactEntityId, contactEntityId];
3609
+ let sinceClause = "";
3610
+ if (since) {
3611
+ sinceClause = " AND r.timestamp >= ?";
3612
+ args.push(since.toISOString());
3128
3613
  }
3129
- const modelPath = path8.join(MODELS_DIR, RERANKER_MODEL_FILE);
3130
- if (!existsSync7(modelPath)) {
3131
- throw new Error(
3132
- `Reranker model not found at ${modelPath}. Run /exe-setup to download it.`
3133
- );
3614
+ const result = await client.execute({
3615
+ sql: `SELECT
3616
+ CASE
3617
+ WHEN r.source_entity_id = ? THEN r.target_entity_id
3618
+ ELSE r.source_entity_id
3619
+ END as partner_id,
3620
+ COUNT(*) as interaction_count,
3621
+ MAX(r.timestamp) as last_interaction
3622
+ FROM relationships r
3623
+ WHERE (r.source_entity_id = ? OR r.target_entity_id = ?)${sinceClause}
3624
+ GROUP BY partner_id
3625
+ ORDER BY interaction_count DESC`,
3626
+ args: [contactEntityId, ...args]
3627
+ });
3628
+ const partners = [];
3629
+ for (const row of result.rows) {
3630
+ const partnerId = String(row.partner_id);
3631
+ const entityResult = await client.execute({
3632
+ sql: "SELECT * FROM entities WHERE id = ?",
3633
+ args: [partnerId]
3634
+ });
3635
+ if (entityResult.rows.length === 0) continue;
3636
+ const e = entityResult.rows[0];
3637
+ partners.push({
3638
+ agentEntity: {
3639
+ id: String(e.id),
3640
+ name: String(e.name),
3641
+ type: String(e.type),
3642
+ firstSeen: String(e.first_seen),
3643
+ lastSeen: String(e.last_seen),
3644
+ properties: JSON.parse(String(e.properties ?? "{}"))
3645
+ },
3646
+ interactionCount: Number(row.interaction_count),
3647
+ lastInteraction: String(row.last_interaction)
3648
+ });
3134
3649
  }
3135
- process.stderr.write("[reranker] Loading Jina Reranker v3...\n");
3136
- const { getLlama } = await import("node-llama-cpp");
3137
- const llama = await getLlama();
3138
- _rerankerModel = await llama.loadModel({ modelPath });
3139
- _rerankerContext = await _rerankerModel.createEmbeddingContext();
3140
- process.stderr.write("[reranker] Jina Reranker v3 loaded.\n");
3141
- resetIdleTimer();
3650
+ return partners;
3142
3651
  }
3143
- async function disposeReranker() {
3144
- if (_idleTimer) {
3145
- clearTimeout(_idleTimer);
3146
- _idleTimer = null;
3652
+ async function getHotEntities(client, since, limit = 10) {
3653
+ const sinceISO = since.toISOString();
3654
+ const result = await client.execute({
3655
+ sql: `SELECT entity_id, COUNT(*) as rel_count
3656
+ FROM (
3657
+ SELECT source_entity_id as entity_id FROM relationships WHERE timestamp >= ?
3658
+ UNION ALL
3659
+ SELECT target_entity_id as entity_id FROM relationships WHERE timestamp >= ?
3660
+ )
3661
+ GROUP BY entity_id
3662
+ ORDER BY rel_count DESC
3663
+ LIMIT ?`,
3664
+ args: [sinceISO, sinceISO, limit]
3665
+ });
3666
+ const hotEntities = [];
3667
+ for (const row of result.rows) {
3668
+ const eid = String(row.entity_id);
3669
+ const entityResult = await client.execute({
3670
+ sql: "SELECT * FROM entities WHERE id = ?",
3671
+ args: [eid]
3672
+ });
3673
+ if (entityResult.rows.length === 0) continue;
3674
+ const e = entityResult.rows[0];
3675
+ hotEntities.push({
3676
+ entity: {
3677
+ id: String(e.id),
3678
+ name: String(e.name),
3679
+ type: String(e.type),
3680
+ firstSeen: String(e.first_seen),
3681
+ lastSeen: String(e.last_seen),
3682
+ properties: JSON.parse(String(e.properties ?? "{}"))
3683
+ },
3684
+ newRelationships: Number(row.rel_count)
3685
+ });
3147
3686
  }
3148
- if (_rerankerContext) {
3149
- try {
3150
- await _rerankerContext.dispose();
3151
- } catch {
3687
+ return hotEntities;
3688
+ }
3689
+ var init_graph_query = __esm({
3690
+ "src/lib/graph-query.ts"() {
3691
+ "use strict";
3692
+ }
3693
+ });
3694
+
3695
+ // src/lib/entity-boost.ts
3696
+ var entity_boost_exports = {};
3697
+ __export(entity_boost_exports, {
3698
+ applyEntityBoost: () => applyEntityBoost
3699
+ });
3700
+ function getRelTypeWeights() {
3701
+ const override = process.env.RELATIONSHIP_TYPE_WEIGHTS;
3702
+ if (!override) return DEFAULT_RELATIONSHIP_WEIGHTS;
3703
+ try {
3704
+ return { ...DEFAULT_RELATIONSHIP_WEIGHTS, ...JSON.parse(override) };
3705
+ } catch {
3706
+ return DEFAULT_RELATIONSHIP_WEIGHTS;
3707
+ }
3708
+ }
3709
+ async function matchEntities(query, client) {
3710
+ 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);
3711
+ if (words.length === 0) return [];
3712
+ try {
3713
+ const matchExpr = words.map((w) => `${w}*`).join(" OR ");
3714
+ const result = await client.execute({
3715
+ sql: `SELECT e.id, e.name FROM entities e
3716
+ JOIN entities_fts fts ON e.rowid = fts.rowid
3717
+ WHERE entities_fts MATCH ?
3718
+ ORDER BY rank
3719
+ LIMIT ?`,
3720
+ args: [matchExpr, MAX_ENTITY_MATCHES]
3721
+ });
3722
+ if (result.rows.length > 0) {
3723
+ return result.rows.map((row) => ({
3724
+ entityId: String(row.id),
3725
+ name: String(row.name)
3726
+ }));
3152
3727
  }
3153
- _rerankerContext = null;
3728
+ } catch {
3154
3729
  }
3155
- if (_rerankerModel) {
3156
- try {
3157
- await _rerankerModel.dispose();
3158
- } catch {
3730
+ const conditions = words.map(() => `LOWER(name) LIKE ?`);
3731
+ const args = words.map((w) => `%${w}%`);
3732
+ try {
3733
+ const result = await client.execute({
3734
+ sql: `SELECT id, name FROM entities
3735
+ WHERE ${conditions.join(" OR ")}
3736
+ LIMIT ?`,
3737
+ args: [...args, MAX_ENTITY_MATCHES]
3738
+ });
3739
+ return result.rows.map((row) => ({
3740
+ entityId: String(row.id),
3741
+ name: String(row.name)
3742
+ }));
3743
+ } catch {
3744
+ return [];
3745
+ }
3746
+ }
3747
+ async function getLinkedMemories(entityIds, client) {
3748
+ const linked = /* @__PURE__ */ new Map();
3749
+ if (entityIds.length === 0) return linked;
3750
+ const placeholders = entityIds.map(() => "?").join(",");
3751
+ try {
3752
+ const result = await client.execute({
3753
+ sql: `SELECT entity_id, memory_id FROM entity_memories
3754
+ WHERE entity_id IN (${placeholders})`,
3755
+ args: entityIds
3756
+ });
3757
+ for (const row of result.rows) {
3758
+ const entityId = String(row.entity_id);
3759
+ const memoryId = String(row.memory_id);
3760
+ const entry = linked.get(entityId) ?? {
3761
+ entityId,
3762
+ memoryIds: /* @__PURE__ */ new Set(),
3763
+ count: 0
3764
+ };
3765
+ entry.memoryIds.add(memoryId);
3766
+ entry.count = entry.memoryIds.size;
3767
+ linked.set(entityId, entry);
3159
3768
  }
3160
- _rerankerModel = null;
3769
+ } catch {
3161
3770
  }
3162
- process.stderr.write("[reranker] Unloaded (idle timeout).\n");
3771
+ return linked;
3163
3772
  }
3164
- async function rerankWithScores(query, texts, topK) {
3165
- if (texts.length === 0) return [];
3166
- await ensureLoaded();
3167
- const ctx = _rerankerContext;
3168
- const scored = [];
3169
- for (let i = 0; i < texts.length; i++) {
3170
- const text = texts[i] ?? "";
3773
+ function spreadAttenuation(numLinked) {
3774
+ return 1 / (1 + 1e-3 * (numLinked - 1) ** 2);
3775
+ }
3776
+ async function traverseAndScore(entities, client, boostMap, resultIds, graphContextMap) {
3777
+ const topEntities = entities.slice(0, TOP_ENTITIES_TO_TRAVERSE);
3778
+ if (topEntities.length === 0) return;
3779
+ const { getEntityNeighbors: getEntityNeighbors2 } = await Promise.resolve().then(() => (init_graph_query(), graph_query_exports));
3780
+ const relWeights = getRelTypeWeights();
3781
+ const neighborEntityIds = [];
3782
+ const neighborMeta = /* @__PURE__ */ new Map();
3783
+ for (const entity of topEntities) {
3171
3784
  try {
3172
- const input2 = `query: ${query} document: ${text.slice(0, 512)}`;
3173
- const embedding = await ctx.getEmbeddingFor(input2);
3174
- const score = embedding.vector[0] ?? 0;
3175
- scored.push({ text, score, index: i });
3785
+ const neighbors = await getEntityNeighbors2(
3786
+ client,
3787
+ entity.entityId,
3788
+ GRAPH_TRAVERSAL_MAX_HOPS
3789
+ );
3790
+ for (const n of neighbors) {
3791
+ if (n.depth === 0) continue;
3792
+ if (neighborMeta.has(n.entity.id)) continue;
3793
+ neighborEntityIds.push(n.entity.id);
3794
+ neighborMeta.set(n.entity.id, {
3795
+ depth: n.depth,
3796
+ relType: n.relType,
3797
+ relConfidence: n.relConfidence,
3798
+ sourceName: entity.name
3799
+ });
3800
+ }
3176
3801
  } catch {
3177
- scored.push({ text, score: -1, index: i });
3178
3802
  }
3179
3803
  }
3180
- scored.sort((a, b) => b.score - a.score);
3181
- return typeof topK === "number" ? scored.slice(0, topK) : scored;
3804
+ if (neighborEntityIds.length === 0) return;
3805
+ const neighborLinked = await getLinkedMemories(neighborEntityIds, client);
3806
+ for (const [entityId, entry] of neighborLinked) {
3807
+ const meta = neighborMeta.get(entityId);
3808
+ if (!meta) continue;
3809
+ const relWeight = relWeights[meta.relType] ?? DEFAULT_REL_WEIGHT;
3810
+ const depthDecay = 1 / (1 + meta.depth);
3811
+ const neighborBoost = ENTITY_BOOST_WEIGHT * meta.relConfidence * depthDecay * relWeight;
3812
+ const attenuation = spreadAttenuation(entry.count);
3813
+ const finalBoost = neighborBoost * attenuation;
3814
+ for (const memoryId of entry.memoryIds) {
3815
+ if (resultIds.has(memoryId)) {
3816
+ boostMap.set(memoryId, (boostMap.get(memoryId) ?? 0) + finalBoost);
3817
+ const contextPath = `${meta.sourceName} ${meta.relType} ${entityId}`;
3818
+ if (!graphContextMap.has(memoryId)) {
3819
+ graphContextMap.set(memoryId, contextPath);
3820
+ }
3821
+ }
3822
+ }
3823
+ }
3182
3824
  }
3183
- async function rerank(query, candidates, topK = 5) {
3184
- if (candidates.length === 0) return [];
3185
- if (candidates.length <= topK) return candidates;
3186
- const scored = await rerankWithScores(
3187
- query,
3188
- candidates.map((c) => c.raw_text),
3189
- topK
3825
+ async function applyHyperedgeBoost(entities, client, boostMap, resultIds) {
3826
+ if (entities.length < 2) return;
3827
+ const entityIds = entities.map((e) => e.entityId);
3828
+ const placeholders = entityIds.map(() => "?").join(",");
3829
+ try {
3830
+ const result = await client.execute({
3831
+ sql: `SELECT hyperedge_id, entity_id FROM hyperedge_nodes
3832
+ WHERE entity_id IN (${placeholders})`,
3833
+ args: entityIds
3834
+ });
3835
+ const hyperedgeEntities = /* @__PURE__ */ new Map();
3836
+ for (const row of result.rows) {
3837
+ const hid = String(row.hyperedge_id);
3838
+ const eid = String(row.entity_id);
3839
+ const set = hyperedgeEntities.get(hid) ?? /* @__PURE__ */ new Set();
3840
+ set.add(eid);
3841
+ hyperedgeEntities.set(hid, set);
3842
+ }
3843
+ const sharedHyperedgeIds = [];
3844
+ for (const [hid, eids] of hyperedgeEntities) {
3845
+ if (eids.size >= 2) {
3846
+ sharedHyperedgeIds.push(hid);
3847
+ }
3848
+ }
3849
+ if (sharedHyperedgeIds.length === 0) return;
3850
+ const hPlaceholders = sharedHyperedgeIds.map(() => "?").join(",");
3851
+ const allNodesResult = await client.execute({
3852
+ sql: `SELECT DISTINCT entity_id FROM hyperedge_nodes
3853
+ WHERE hyperedge_id IN (${hPlaceholders})`,
3854
+ args: sharedHyperedgeIds
3855
+ });
3856
+ const allEntityIds = allNodesResult.rows.map((r) => String(r.entity_id));
3857
+ if (allEntityIds.length === 0) return;
3858
+ const linked = await getLinkedMemories(allEntityIds, client);
3859
+ for (const [, entry] of linked) {
3860
+ const attenuation = spreadAttenuation(entry.count);
3861
+ const boost = ENTITY_BOOST_WEIGHT * attenuation;
3862
+ for (const memoryId of entry.memoryIds) {
3863
+ if (resultIds.has(memoryId)) {
3864
+ boostMap.set(memoryId, (boostMap.get(memoryId) ?? 0) + boost);
3865
+ }
3866
+ }
3867
+ }
3868
+ } catch {
3869
+ }
3870
+ }
3871
+ async function applyEntityBoost(results, query, client) {
3872
+ const emptyResult = { results, graphContext: /* @__PURE__ */ new Map() };
3873
+ if (ENTITY_BOOST_WEIGHT === 0 || results.length === 0) {
3874
+ return emptyResult;
3875
+ }
3876
+ console.time("entity-boost");
3877
+ const entities = await matchEntities(query, client);
3878
+ if (entities.length === 0) {
3879
+ console.timeEnd("entity-boost");
3880
+ return emptyResult;
3881
+ }
3882
+ const boostMap = /* @__PURE__ */ new Map();
3883
+ const resultIds = new Set(results.map((r) => r.id));
3884
+ const graphContextMap = /* @__PURE__ */ new Map();
3885
+ const directLinked = await getLinkedMemories(
3886
+ entities.map((e) => e.entityId),
3887
+ client
3190
3888
  );
3191
- return scored.map((s) => candidates[s.index]);
3889
+ for (const [, entry] of directLinked) {
3890
+ const attenuation = spreadAttenuation(entry.count);
3891
+ const boost = ENTITY_BOOST_WEIGHT * attenuation;
3892
+ for (const memoryId of entry.memoryIds) {
3893
+ if (resultIds.has(memoryId)) {
3894
+ boostMap.set(memoryId, (boostMap.get(memoryId) ?? 0) + boost);
3895
+ }
3896
+ }
3897
+ }
3898
+ await traverseAndScore(entities, client, boostMap, resultIds, graphContextMap);
3899
+ await applyHyperedgeBoost(entities, client, boostMap, resultIds);
3900
+ if (boostMap.size === 0) {
3901
+ console.timeEnd("entity-boost");
3902
+ return emptyResult;
3903
+ }
3904
+ const scored = results.map((r, i) => ({
3905
+ record: r,
3906
+ baseScore: 1 / (1 + i * 0.1),
3907
+ entityBoost: boostMap.get(r.id) ?? 0
3908
+ }));
3909
+ scored.sort(
3910
+ (a, b) => b.baseScore + b.entityBoost - (a.baseScore + a.entityBoost)
3911
+ );
3912
+ console.timeEnd("entity-boost");
3913
+ return {
3914
+ results: scored.map((s) => s.record),
3915
+ graphContext: graphContextMap
3916
+ };
3192
3917
  }
3193
- var RERANKER_MODEL_FILE, IDLE_TIMEOUT_MS, _rerankerContext, _rerankerModel, _idleTimer;
3194
- var init_reranker = __esm({
3195
- "src/lib/reranker.ts"() {
3918
+ var ENTITY_BOOST_WEIGHT, MIN_WORD_LENGTH, MAX_ENTITY_MATCHES, GRAPH_TRAVERSAL_MAX_HOPS, TOP_ENTITIES_TO_TRAVERSE, DEFAULT_RELATIONSHIP_WEIGHTS, DEFAULT_REL_WEIGHT;
3919
+ var init_entity_boost = __esm({
3920
+ "src/lib/entity-boost.ts"() {
3196
3921
  "use strict";
3197
- init_config();
3198
- RERANKER_MODEL_FILE = "jina-reranker-v3-q4_k_m.gguf";
3199
- IDLE_TIMEOUT_MS = 6e4;
3200
- _rerankerContext = null;
3201
- _rerankerModel = null;
3202
- _idleTimer = null;
3922
+ ENTITY_BOOST_WEIGHT = parseFloat(
3923
+ process.env.ENTITY_BOOST_WEIGHT ?? "0.5"
3924
+ );
3925
+ MIN_WORD_LENGTH = 3;
3926
+ MAX_ENTITY_MATCHES = 20;
3927
+ GRAPH_TRAVERSAL_MAX_HOPS = Math.min(
3928
+ parseInt(process.env.GRAPH_TRAVERSAL_MAX_HOPS ?? "2", 10),
3929
+ 3
3930
+ );
3931
+ TOP_ENTITIES_TO_TRAVERSE = 5;
3932
+ DEFAULT_RELATIONSHIP_WEIGHTS = {
3933
+ decided: 1,
3934
+ depends_on: 0.8,
3935
+ part_of: 0.7,
3936
+ related_to: 0.5,
3937
+ mentioned_in: 0.3
3938
+ };
3939
+ DEFAULT_REL_WEIGHT = 0.4;
3203
3940
  }
3204
3941
  });
3205
3942
 
@@ -4059,7 +4796,7 @@ async function countNewPendingReviewsSince(sinceIso, sessionScope) {
4059
4796
  const result2 = await client.execute({
4060
4797
  sql: `SELECT COUNT(*) as cnt FROM tasks
4061
4798
  WHERE status = 'needs_review' AND updated_at > ?
4062
- AND (session_scope = ? OR session_scope IS NULL)`,
4799
+ AND session_scope = ?`,
4063
4800
  args: [sinceIso, sessionScope]
4064
4801
  });
4065
4802
  return Number(result2.rows[0]?.cnt) || 0;
@@ -4077,7 +4814,7 @@ async function listPendingReviews(limit, sessionScope) {
4077
4814
  const result2 = await client.execute({
4078
4815
  sql: `SELECT title, assigned_to, project_name FROM tasks
4079
4816
  WHERE status = 'needs_review'
4080
- AND (session_scope = ? OR session_scope IS NULL)
4817
+ AND session_scope = ?
4081
4818
  ORDER BY priority ASC, created_at DESC LIMIT ?`,
4082
4819
  args: [sessionScope, limit]
4083
4820
  });
@@ -4178,7 +4915,7 @@ async function createReviewForCompletedTask(row, result, _baseDir, now) {
4178
4915
  const taskFile = String(row.task_file);
4179
4916
  const employees = await loadEmployees();
4180
4917
  const coordinatorName = getCoordinatorName(employees);
4181
- if (String(row.assigned_to) === "exe" || isCoordinatorName(String(row.assigned_to), employees)) return;
4918
+ if (isCoordinatorName(String(row.assigned_to), employees)) return;
4182
4919
  if (String(row.title).startsWith("Review:")) return;
4183
4920
  const fileName = taskFile.split("/").pop() ?? "";
4184
4921
  if (fileName.startsWith("review-") && String(row.assigned_by) === "system") return;
@@ -4287,14 +5024,14 @@ async function cleanupReviewFile(row, taskFile, _baseDir) {
4287
5024
  if (parts.length >= 3 && parts[0] === "review") {
4288
5025
  const agent = parts[1];
4289
5026
  const slug = parts.slice(2).join("-");
4290
- const originalTaskFile = `exe/${agent}/${slug}.md`;
5027
+ const legacyTaskFile = `exe/${agent}/${slug}.md`;
4291
5028
  const result = await client.execute({
4292
- sql: "UPDATE tasks SET status = 'done', updated_at = ? WHERE task_file = ? AND status = 'needs_review'",
4293
- args: [now, originalTaskFile]
5029
+ sql: "UPDATE tasks SET status = 'done', updated_at = ? WHERE (task_file = ? OR task_file LIKE ?) AND status = 'needs_review'",
5030
+ args: [now, legacyTaskFile, `tasks/%/${agent}/${slug}.md`]
4294
5031
  });
4295
5032
  if (result.rowsAffected > 0) {
4296
5033
  process.stderr.write(
4297
- `[review-cleanup] Cascaded original task to done (legacy path): ${originalTaskFile}
5034
+ `[review-cleanup] Cascaded original task to done: ${agent}/${slug}.md
4298
5035
  `
4299
5036
  );
4300
5037
  }
@@ -4384,8 +5121,14 @@ async function hybridSearch(queryText, agentId, options) {
4384
5121
  `
4385
5122
  );
4386
5123
  }
5124
+ let rerankerAvailable = false;
5125
+ try {
5126
+ const { isRerankerAvailable: isRerankerAvailable2 } = await Promise.resolve().then(() => (init_reranker(), reranker_exports));
5127
+ rerankerAvailable = isRerankerAvailable2();
5128
+ } catch {
5129
+ }
4387
5130
  const broadFetchTopK = config.scalingRoadmap?.rerankerAutoTrigger?.fetchTopK ?? 150;
4388
- const fetchLimit = effectiveIsBroad ? Math.max(limit * 5, broadFetchTopK) : Math.max(limit * 3, 30);
5131
+ const fetchLimit = effectiveIsBroad ? Math.max(limit * 5, broadFetchTopK) : rerankerAvailable ? Math.max(limit * 4, 60) : Math.max(limit * 3, 30);
4389
5132
  const fetchOptions = { ...effectiveOptions, limit: fetchLimit, includeSource: false };
4390
5133
  let queryVector = null;
4391
5134
  try {
@@ -4431,7 +5174,17 @@ async function hybridSearch(queryText, agentId, options) {
4431
5174
  if (lists.length === 0) return [];
4432
5175
  if (lists.length === 1 && !effectiveIsBroad) return lists[0].slice(0, limit);
4433
5176
  const rrfLimit = effectiveIsBroad ? Math.max(limit * 5, 150) : limit;
4434
- const merged = lists.length === 1 ? lists[0].slice(0, rrfLimit) : rrfMergeMulti(lists, rrfLimit, RRF_K, weights);
5177
+ let merged = lists.length === 1 ? lists[0].slice(0, rrfLimit) : rrfMergeMulti(lists, rrfLimit, RRF_K, weights);
5178
+ let graphContextMap = /* @__PURE__ */ new Map();
5179
+ if (merged.length > 0) {
5180
+ try {
5181
+ const { applyEntityBoost: applyEntityBoost2 } = await Promise.resolve().then(() => (init_entity_boost(), entity_boost_exports));
5182
+ const boosted = await applyEntityBoost2(merged, effectiveQuery, getClient());
5183
+ merged = boosted.results;
5184
+ graphContextMap = boosted.graphContext;
5185
+ } catch {
5186
+ }
5187
+ }
4435
5188
  const auto = config.scalingRoadmap?.rerankerAutoTrigger ?? {
4436
5189
  enabled: config.rerankerEnabled ?? true,
4437
5190
  broadQueryMinCardinality: 5e4,
@@ -4439,20 +5192,29 @@ async function hybridSearch(queryText, agentId, options) {
4439
5192
  returnTopK: 5
4440
5193
  };
4441
5194
  let rerankedAndBlended = null;
4442
- if (effectiveIsBroad && auto.enabled) {
5195
+ if (effectiveIsBroad && auto.enabled && rerankerAvailable) {
4443
5196
  const cardinality2 = await estimateCardinality(agentId, effectiveOptions);
4444
5197
  if (cardinality2 > auto.broadQueryMinCardinality) {
4445
5198
  try {
4446
- const { isRerankerAvailable: isRerankerAvailable2, rerank: rerank2 } = await Promise.resolve().then(() => (init_reranker(), reranker_exports));
4447
- if (isRerankerAvailable2()) {
4448
- const reranked = await rerank2(effectiveQuery, merged, auto.returnTopK);
4449
- if (reranked.length > 0) {
4450
- rerankedAndBlended = rrfMergeMulti(
4451
- [reranked],
4452
- auto.returnTopK,
4453
- RRF_K
4454
- );
4455
- }
5199
+ let rerankedRecords;
5200
+ if (graphContextMap.size > 0) {
5201
+ const { rerankWithContext: rerankWithContext2 } = await Promise.resolve().then(() => (init_reranker(), reranker_exports));
5202
+ const candidates = merged.map((m) => ({
5203
+ text: m.raw_text,
5204
+ context: graphContextMap.get(m.id)
5205
+ }));
5206
+ const scored = await rerankWithContext2(effectiveQuery, candidates, auto.returnTopK);
5207
+ rerankedRecords = scored.map((s) => merged[s.index]);
5208
+ } else {
5209
+ const { rerank: rerank2 } = await Promise.resolve().then(() => (init_reranker(), reranker_exports));
5210
+ rerankedRecords = await rerank2(effectiveQuery, merged, auto.returnTopK);
5211
+ }
5212
+ if (rerankedRecords.length > 0) {
5213
+ rerankedAndBlended = rrfMergeMulti(
5214
+ [rerankedRecords],
5215
+ auto.returnTopK,
5216
+ RRF_K
5217
+ );
4456
5218
  }
4457
5219
  } catch {
4458
5220
  }
@@ -4472,7 +5234,7 @@ async function hybridSearch(queryText, agentId, options) {
4472
5234
  try {
4473
5235
  const client = getClient();
4474
5236
  void client.execute({
4475
- sql: `UPDATE memories SET last_accessed = ? WHERE id IN (${placeholders})`,
5237
+ sql: `UPDATE memories SET last_accessed = ?, retrieval_count = COALESCE(retrieval_count, 0) + 1 WHERE id IN (${placeholders})`,
4476
5238
  args: [now, ...ids]
4477
5239
  }).catch(() => {
4478
5240
  });