@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
@@ -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;
@@ -2790,10 +3064,10 @@ async function disposeEmbedder() {
2790
3064
  async function embedDirect(text) {
2791
3065
  const llamaCpp = await import("node-llama-cpp");
2792
3066
  const { MODELS_DIR: MODELS_DIR2 } = await Promise.resolve().then(() => (init_config(), config_exports));
2793
- const { existsSync: existsSync15 } = await import("fs");
2794
- const path18 = await import("path");
2795
- const modelPath = path18.join(MODELS_DIR2, "jina-embeddings-v5-small-q4_k_m.gguf");
2796
- if (!existsSync15(modelPath)) {
3067
+ const { existsSync: existsSync16 } = await import("fs");
3068
+ const path19 = await import("path");
3069
+ const modelPath = path19.join(MODELS_DIR2, "jina-embeddings-v5-small-q4_k_m.gguf");
3070
+ if (!existsSync16(modelPath)) {
2797
3071
  throw new Error(`Embedding model not found at ${modelPath}. Run '/exe-setup' to download it.`);
2798
3072
  }
2799
3073
  const llama = await llamaCpp.getLlama();
@@ -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
 
@@ -3393,6 +4130,42 @@ var init_provider_table = __esm({
3393
4130
  }
3394
4131
  });
3395
4132
 
4133
+ // src/lib/runtime-table.ts
4134
+ var RUNTIME_TABLE;
4135
+ var init_runtime_table = __esm({
4136
+ "src/lib/runtime-table.ts"() {
4137
+ "use strict";
4138
+ RUNTIME_TABLE = {
4139
+ codex: {
4140
+ binary: "codex",
4141
+ launchMode: "exec",
4142
+ autoApproveFlag: "--full-auto",
4143
+ inlineFlag: "--no-alt-screen",
4144
+ apiKeyEnv: "OPENAI_API_KEY",
4145
+ defaultModel: "gpt-5.4"
4146
+ }
4147
+ };
4148
+ }
4149
+ });
4150
+
4151
+ // src/lib/agent-config.ts
4152
+ import { readFileSync as readFileSync6, writeFileSync as writeFileSync3, existsSync as existsSync8, mkdirSync as mkdirSync3 } from "fs";
4153
+ import path11 from "path";
4154
+ var AGENT_CONFIG_PATH, DEFAULT_MODELS;
4155
+ var init_agent_config = __esm({
4156
+ "src/lib/agent-config.ts"() {
4157
+ "use strict";
4158
+ init_config();
4159
+ init_runtime_table();
4160
+ AGENT_CONFIG_PATH = path11.join(EXE_AI_DIR, "agent-config.json");
4161
+ DEFAULT_MODELS = {
4162
+ claude: "claude-opus-4",
4163
+ codex: RUNTIME_TABLE.codex?.defaultModel ?? "gpt-5.4",
4164
+ opencode: "minimax-m2.7"
4165
+ };
4166
+ }
4167
+ });
4168
+
3396
4169
  // src/lib/intercom-queue.ts
3397
4170
  var intercom_queue_exports = {};
3398
4171
  __export(intercom_queue_exports, {
@@ -3401,17 +4174,17 @@ __export(intercom_queue_exports, {
3401
4174
  queueIntercom: () => queueIntercom,
3402
4175
  readQueue: () => readQueue
3403
4176
  });
3404
- import { readFileSync as readFileSync6, writeFileSync as writeFileSync3, renameSync as renameSync3, existsSync as existsSync8, mkdirSync as mkdirSync3 } from "fs";
3405
- import path11 from "path";
4177
+ import { readFileSync as readFileSync7, writeFileSync as writeFileSync4, renameSync as renameSync3, existsSync as existsSync9, mkdirSync as mkdirSync4 } from "fs";
4178
+ import path12 from "path";
3406
4179
  import os5 from "os";
3407
4180
  function ensureDir() {
3408
- const dir = path11.dirname(QUEUE_PATH);
3409
- if (!existsSync8(dir)) mkdirSync3(dir, { recursive: true });
4181
+ const dir = path12.dirname(QUEUE_PATH);
4182
+ if (!existsSync9(dir)) mkdirSync4(dir, { recursive: true });
3410
4183
  }
3411
4184
  function readQueue() {
3412
4185
  try {
3413
- if (!existsSync8(QUEUE_PATH)) return [];
3414
- return JSON.parse(readFileSync6(QUEUE_PATH, "utf8"));
4186
+ if (!existsSync9(QUEUE_PATH)) return [];
4187
+ return JSON.parse(readFileSync7(QUEUE_PATH, "utf8"));
3415
4188
  } catch {
3416
4189
  return [];
3417
4190
  }
@@ -3419,7 +4192,7 @@ function readQueue() {
3419
4192
  function writeQueue(queue) {
3420
4193
  ensureDir();
3421
4194
  const tmp = `${QUEUE_PATH}.tmp`;
3422
- writeFileSync3(tmp, JSON.stringify(queue, null, 2));
4195
+ writeFileSync4(tmp, JSON.stringify(queue, null, 2));
3423
4196
  renameSync3(tmp, QUEUE_PATH);
3424
4197
  }
3425
4198
  function queueIntercom(targetSession, reason) {
@@ -3502,32 +4275,32 @@ var QUEUE_PATH, MAX_RETRIES2, TTL_MS, INTERCOM_LOG;
3502
4275
  var init_intercom_queue = __esm({
3503
4276
  "src/lib/intercom-queue.ts"() {
3504
4277
  "use strict";
3505
- QUEUE_PATH = path11.join(os5.homedir(), ".exe-os", "intercom-queue.json");
4278
+ QUEUE_PATH = path12.join(os5.homedir(), ".exe-os", "intercom-queue.json");
3506
4279
  MAX_RETRIES2 = 5;
3507
4280
  TTL_MS = 60 * 60 * 1e3;
3508
- INTERCOM_LOG = path11.join(os5.homedir(), ".exe-os", "intercom.log");
4281
+ INTERCOM_LOG = path12.join(os5.homedir(), ".exe-os", "intercom.log");
3509
4282
  }
3510
4283
  });
3511
4284
 
3512
4285
  // src/lib/license.ts
3513
- import { readFileSync as readFileSync7, writeFileSync as writeFileSync4, existsSync as existsSync9, mkdirSync as mkdirSync4 } from "fs";
4286
+ import { readFileSync as readFileSync8, writeFileSync as writeFileSync5, existsSync as existsSync10, mkdirSync as mkdirSync5 } from "fs";
3514
4287
  import { randomUUID as randomUUID3 } from "crypto";
3515
- import path12 from "path";
4288
+ import path13 from "path";
3516
4289
  import { jwtVerify, importSPKI } from "jose";
3517
4290
  var LICENSE_PATH, CACHE_PATH, DEVICE_ID_PATH;
3518
4291
  var init_license = __esm({
3519
4292
  "src/lib/license.ts"() {
3520
4293
  "use strict";
3521
4294
  init_config();
3522
- LICENSE_PATH = path12.join(EXE_AI_DIR, "license.key");
3523
- CACHE_PATH = path12.join(EXE_AI_DIR, "license-cache.json");
3524
- DEVICE_ID_PATH = path12.join(EXE_AI_DIR, "device-id");
4295
+ LICENSE_PATH = path13.join(EXE_AI_DIR, "license.key");
4296
+ CACHE_PATH = path13.join(EXE_AI_DIR, "license-cache.json");
4297
+ DEVICE_ID_PATH = path13.join(EXE_AI_DIR, "device-id");
3525
4298
  }
3526
4299
  });
3527
4300
 
3528
4301
  // src/lib/plan-limits.ts
3529
- import { readFileSync as readFileSync8, existsSync as existsSync10 } from "fs";
3530
- import path13 from "path";
4302
+ import { readFileSync as readFileSync9, existsSync as existsSync11 } from "fs";
4303
+ import path14 from "path";
3531
4304
  var CACHE_PATH2;
3532
4305
  var init_plan_limits = __esm({
3533
4306
  "src/lib/plan-limits.ts"() {
@@ -3536,13 +4309,13 @@ var init_plan_limits = __esm({
3536
4309
  init_employees();
3537
4310
  init_license();
3538
4311
  init_config();
3539
- CACHE_PATH2 = path13.join(EXE_AI_DIR, "license-cache.json");
4312
+ CACHE_PATH2 = path14.join(EXE_AI_DIR, "license-cache.json");
3540
4313
  }
3541
4314
  });
3542
4315
 
3543
4316
  // src/lib/tmux-routing.ts
3544
- import { readFileSync as readFileSync9, writeFileSync as writeFileSync5, mkdirSync as mkdirSync5, existsSync as existsSync11, appendFileSync } from "fs";
3545
- import path14 from "path";
4317
+ import { readFileSync as readFileSync10, writeFileSync as writeFileSync6, mkdirSync as mkdirSync6, existsSync as existsSync12, appendFileSync } from "fs";
4318
+ import path15 from "path";
3546
4319
  import os6 from "os";
3547
4320
  import { fileURLToPath as fileURLToPath2 } from "url";
3548
4321
  function getMySession() {
@@ -3583,7 +4356,7 @@ function extractRootExe(name) {
3583
4356
  }
3584
4357
  function getParentExe(sessionKey) {
3585
4358
  try {
3586
- const data = JSON.parse(readFileSync9(path14.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`), "utf8"));
4359
+ const data = JSON.parse(readFileSync10(path15.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`), "utf8"));
3587
4360
  return data.parentExe || null;
3588
4361
  } catch {
3589
4362
  return null;
@@ -3607,32 +4380,50 @@ function isEmployeeAlive(sessionName) {
3607
4380
  }
3608
4381
  function readDebounceState() {
3609
4382
  try {
3610
- if (!existsSync11(DEBOUNCE_FILE)) return {};
3611
- return JSON.parse(readFileSync9(DEBOUNCE_FILE, "utf8"));
4383
+ if (!existsSync12(DEBOUNCE_FILE)) return {};
4384
+ const raw = JSON.parse(readFileSync10(DEBOUNCE_FILE, "utf8"));
4385
+ const state = {};
4386
+ for (const [key, val] of Object.entries(raw)) {
4387
+ if (typeof val === "number") {
4388
+ state[key] = { lastSent: val, pending: 0 };
4389
+ } else if (val && typeof val === "object" && "lastSent" in val) {
4390
+ state[key] = val;
4391
+ }
4392
+ }
4393
+ return state;
3612
4394
  } catch {
3613
4395
  return {};
3614
4396
  }
3615
4397
  }
3616
4398
  function writeDebounceState(state) {
3617
4399
  try {
3618
- if (!existsSync11(SESSION_CACHE)) mkdirSync5(SESSION_CACHE, { recursive: true });
3619
- writeFileSync5(DEBOUNCE_FILE, JSON.stringify(state));
4400
+ if (!existsSync12(SESSION_CACHE)) mkdirSync6(SESSION_CACHE, { recursive: true });
4401
+ writeFileSync6(DEBOUNCE_FILE, JSON.stringify(state));
3620
4402
  } catch {
3621
4403
  }
3622
4404
  }
3623
4405
  function isDebounced(targetSession) {
3624
4406
  const state = readDebounceState();
3625
- const lastSent = state[targetSession] ?? 0;
3626
- return Date.now() - lastSent < INTERCOM_DEBOUNCE_MS;
4407
+ const entry = state[targetSession];
4408
+ const lastSent = entry?.lastSent ?? 0;
4409
+ if (Date.now() - lastSent < INTERCOM_DEBOUNCE_MS) {
4410
+ if (!state[targetSession]) state[targetSession] = { lastSent, pending: 0 };
4411
+ state[targetSession].pending++;
4412
+ writeDebounceState(state);
4413
+ return true;
4414
+ }
4415
+ return false;
3627
4416
  }
3628
4417
  function recordDebounce(targetSession) {
3629
4418
  const state = readDebounceState();
3630
- state[targetSession] = Date.now();
4419
+ const batched = state[targetSession]?.pending ?? 0;
4420
+ state[targetSession] = { lastSent: Date.now(), pending: 0 };
3631
4421
  const cutoff = Date.now() - DEBOUNCE_CLEANUP_AGE_MS;
3632
4422
  for (const key of Object.keys(state)) {
3633
- if ((state[key] ?? 0) < cutoff) delete state[key];
4423
+ if ((state[key]?.lastSent ?? 0) < cutoff) delete state[key];
3634
4424
  }
3635
4425
  writeDebounceState(state);
4426
+ return batched;
3636
4427
  }
3637
4428
  function logIntercom(msg) {
3638
4429
  const line = `[${(/* @__PURE__ */ new Date()).toISOString()}] ${msg}
@@ -3673,7 +4464,7 @@ function sendIntercom(targetSession) {
3673
4464
  return "skipped_exe";
3674
4465
  }
3675
4466
  if (isDebounced(targetSession)) {
3676
- logIntercom(`DEBOUNCE \u2192 ${targetSession} (cross-process file debounce)`);
4467
+ logIntercom(`DEBOUNCE \u2192 ${targetSession} (nudge batched, task safe in DB)`);
3677
4468
  return "debounced";
3678
4469
  }
3679
4470
  try {
@@ -3685,14 +4476,14 @@ function sendIntercom(targetSession) {
3685
4476
  const sessionState = getSessionState(targetSession);
3686
4477
  if (sessionState === "no_claude") {
3687
4478
  queueIntercom(targetSession, "claude not running in session");
3688
- recordDebounce(targetSession);
3689
- logIntercom(`QUEUED \u2192 ${targetSession} (no claude process \u2014 raw shell detected)`);
4479
+ const batched2 = recordDebounce(targetSession);
4480
+ logIntercom(`QUEUED \u2192 ${targetSession} (no claude process)${batched2 > 0 ? ` [${batched2} batched]` : ""}`);
3690
4481
  return "queued";
3691
4482
  }
3692
4483
  if (sessionState === "thinking" || sessionState === "tool") {
3693
4484
  queueIntercom(targetSession, "session busy at send time");
3694
- recordDebounce(targetSession);
3695
- logIntercom(`QUEUED \u2192 ${targetSession} (session busy, will retry from queue)`);
4485
+ const batched2 = recordDebounce(targetSession);
4486
+ logIntercom(`QUEUED \u2192 ${targetSession} (session busy)${batched2 > 0 ? ` [${batched2} batched]` : ""}`);
3696
4487
  return "queued";
3697
4488
  }
3698
4489
  if (transport.isPaneInCopyMode(targetSession)) {
@@ -3700,8 +4491,8 @@ function sendIntercom(targetSession) {
3700
4491
  transport.sendKeys(targetSession, "q");
3701
4492
  }
3702
4493
  transport.sendKeys(targetSession, "/exe-intercom");
3703
- recordDebounce(targetSession);
3704
- logIntercom(`DELIVERED \u2192 ${targetSession} (fire-and-forget)`);
4494
+ const batched = recordDebounce(targetSession);
4495
+ logIntercom(`DELIVERED \u2192 ${targetSession}${batched > 0 ? ` [${batched} nudges batched during debounce]` : ""} (fire-and-forget)`);
3705
4496
  return "delivered";
3706
4497
  } catch {
3707
4498
  logIntercom(`FAIL \u2192 ${targetSession}`);
@@ -3718,15 +4509,17 @@ var init_tmux_routing = __esm({
3718
4509
  init_cc_agent_support();
3719
4510
  init_mcp_prefix();
3720
4511
  init_provider_table();
4512
+ init_agent_config();
4513
+ init_runtime_table();
3721
4514
  init_intercom_queue();
3722
4515
  init_plan_limits();
3723
4516
  init_employees();
3724
- SPAWN_LOCK_DIR = path14.join(os6.homedir(), ".exe-os", "spawn-locks");
3725
- SESSION_CACHE = path14.join(os6.homedir(), ".exe-os", "session-cache");
4517
+ SPAWN_LOCK_DIR = path15.join(os6.homedir(), ".exe-os", "spawn-locks");
4518
+ SESSION_CACHE = path15.join(os6.homedir(), ".exe-os", "session-cache");
3726
4519
  VALID_SESSION_NAME = /^[a-z]+\d*-[a-zA-Z0-9_]+$/;
3727
4520
  INTERCOM_DEBOUNCE_MS = 3e4;
3728
- INTERCOM_LOG2 = path14.join(os6.homedir(), ".exe-os", "intercom.log");
3729
- DEBOUNCE_FILE = path14.join(SESSION_CACHE, "intercom-debounce.json");
4521
+ INTERCOM_LOG2 = path15.join(os6.homedir(), ".exe-os", "intercom.log");
4522
+ DEBOUNCE_FILE = path15.join(SESSION_CACHE, "intercom-debounce.json");
3730
4523
  DEBOUNCE_CLEANUP_AGE_MS = 5 * 60 * 1e3;
3731
4524
  BUSY_PATTERN = /[✻✽✶✳·].*…|Running…/;
3732
4525
  }
@@ -3985,13 +4778,13 @@ var init_messaging = __esm({
3985
4778
 
3986
4779
  // src/lib/notifications.ts
3987
4780
  import crypto3 from "crypto";
3988
- import path15 from "path";
4781
+ import path16 from "path";
3989
4782
  import os7 from "os";
3990
4783
  import {
3991
- readFileSync as readFileSync10,
4784
+ readFileSync as readFileSync11,
3992
4785
  readdirSync as readdirSync4,
3993
4786
  unlinkSync as unlinkSync4,
3994
- existsSync as existsSync12,
4787
+ existsSync as existsSync13,
3995
4788
  rmdirSync
3996
4789
  } from "fs";
3997
4790
  async function writeNotification(notification) {
@@ -4036,8 +4829,8 @@ __export(tasks_review_exports, {
4036
4829
  getReviewChecklist: () => getReviewChecklist,
4037
4830
  listPendingReviews: () => listPendingReviews
4038
4831
  });
4039
- import path16 from "path";
4040
- import { existsSync as existsSync13, readdirSync as readdirSync5, unlinkSync as unlinkSync5 } from "fs";
4832
+ import path17 from "path";
4833
+ import { existsSync as existsSync14, readdirSync as readdirSync5, unlinkSync as unlinkSync5 } from "fs";
4041
4834
  async function countPendingReviews(sessionScope) {
4042
4835
  const client = getClient();
4043
4836
  if (sessionScope) {
@@ -4059,7 +4852,7 @@ async function countNewPendingReviewsSince(sinceIso, sessionScope) {
4059
4852
  const result2 = await client.execute({
4060
4853
  sql: `SELECT COUNT(*) as cnt FROM tasks
4061
4854
  WHERE status = 'needs_review' AND updated_at > ?
4062
- AND (session_scope = ? OR session_scope IS NULL)`,
4855
+ AND session_scope = ?`,
4063
4856
  args: [sinceIso, sessionScope]
4064
4857
  });
4065
4858
  return Number(result2.rows[0]?.cnt) || 0;
@@ -4077,7 +4870,7 @@ async function listPendingReviews(limit, sessionScope) {
4077
4870
  const result2 = await client.execute({
4078
4871
  sql: `SELECT title, assigned_to, project_name FROM tasks
4079
4872
  WHERE status = 'needs_review'
4080
- AND (session_scope = ? OR session_scope IS NULL)
4873
+ AND session_scope = ?
4081
4874
  ORDER BY priority ASC, created_at DESC LIMIT ?`,
4082
4875
  args: [sessionScope, limit]
4083
4876
  });
@@ -4178,7 +4971,7 @@ async function createReviewForCompletedTask(row, result, _baseDir, now) {
4178
4971
  const taskFile = String(row.task_file);
4179
4972
  const employees = await loadEmployees();
4180
4973
  const coordinatorName = getCoordinatorName(employees);
4181
- if (String(row.assigned_to) === "exe" || isCoordinatorName(String(row.assigned_to), employees)) return;
4974
+ if (isCoordinatorName(String(row.assigned_to), employees)) return;
4182
4975
  if (String(row.title).startsWith("Review:")) return;
4183
4976
  const fileName = taskFile.split("/").pop() ?? "";
4184
4977
  if (fileName.startsWith("review-") && String(row.assigned_by) === "system") return;
@@ -4287,14 +5080,14 @@ async function cleanupReviewFile(row, taskFile, _baseDir) {
4287
5080
  if (parts.length >= 3 && parts[0] === "review") {
4288
5081
  const agent = parts[1];
4289
5082
  const slug = parts.slice(2).join("-");
4290
- const originalTaskFile = `exe/${agent}/${slug}.md`;
5083
+ const legacyTaskFile = `exe/${agent}/${slug}.md`;
4291
5084
  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]
5085
+ sql: "UPDATE tasks SET status = 'done', updated_at = ? WHERE (task_file = ? OR task_file LIKE ?) AND status = 'needs_review'",
5086
+ args: [now, legacyTaskFile, `tasks/%/${agent}/${slug}.md`]
4294
5087
  });
4295
5088
  if (result.rowsAffected > 0) {
4296
5089
  process.stderr.write(
4297
- `[review-cleanup] Cascaded original task to done (legacy path): ${originalTaskFile}
5090
+ `[review-cleanup] Cascaded original task to done: ${agent}/${slug}.md
4298
5091
  `
4299
5092
  );
4300
5093
  }
@@ -4307,11 +5100,11 @@ async function cleanupReviewFile(row, taskFile, _baseDir) {
4307
5100
  );
4308
5101
  }
4309
5102
  try {
4310
- const cacheDir = path16.join(EXE_AI_DIR, "session-cache");
4311
- if (existsSync13(cacheDir)) {
5103
+ const cacheDir = path17.join(EXE_AI_DIR, "session-cache");
5104
+ if (existsSync14(cacheDir)) {
4312
5105
  for (const f of readdirSync5(cacheDir)) {
4313
5106
  if (f.startsWith("review-notified-")) {
4314
- unlinkSync5(path16.join(cacheDir, f));
5107
+ unlinkSync5(path17.join(cacheDir, f));
4315
5108
  }
4316
5109
  }
4317
5110
  }
@@ -4336,8 +5129,8 @@ init_config();
4336
5129
  init_config();
4337
5130
  init_store();
4338
5131
  import { spawn as spawn2 } from "child_process";
4339
- import { readFileSync as readFileSync11, writeFileSync as writeFileSync6, mkdirSync as mkdirSync6, existsSync as existsSync14, openSync as openSync2, closeSync as closeSync2 } from "fs";
4340
- import path17 from "path";
5132
+ import { readFileSync as readFileSync12, writeFileSync as writeFileSync7, mkdirSync as mkdirSync7, existsSync as existsSync15, openSync as openSync2, closeSync as closeSync2 } from "fs";
5133
+ import path18 from "path";
4341
5134
  import { fileURLToPath as fileURLToPath3 } from "url";
4342
5135
 
4343
5136
  // src/lib/hybrid-search.ts
@@ -4384,8 +5177,14 @@ async function hybridSearch(queryText, agentId, options) {
4384
5177
  `
4385
5178
  );
4386
5179
  }
5180
+ let rerankerAvailable = false;
5181
+ try {
5182
+ const { isRerankerAvailable: isRerankerAvailable2 } = await Promise.resolve().then(() => (init_reranker(), reranker_exports));
5183
+ rerankerAvailable = isRerankerAvailable2();
5184
+ } catch {
5185
+ }
4387
5186
  const broadFetchTopK = config.scalingRoadmap?.rerankerAutoTrigger?.fetchTopK ?? 150;
4388
- const fetchLimit = effectiveIsBroad ? Math.max(limit * 5, broadFetchTopK) : Math.max(limit * 3, 30);
5187
+ const fetchLimit = effectiveIsBroad ? Math.max(limit * 5, broadFetchTopK) : rerankerAvailable ? Math.max(limit * 4, 60) : Math.max(limit * 3, 30);
4389
5188
  const fetchOptions = { ...effectiveOptions, limit: fetchLimit, includeSource: false };
4390
5189
  let queryVector = null;
4391
5190
  try {
@@ -4395,7 +5194,7 @@ async function hybridSearch(queryText, agentId, options) {
4395
5194
  process.stderr.write("[hybrid-search] Embed daemon unavailable \u2014 FTS-only mode\n");
4396
5195
  }
4397
5196
  let grepPromise = Promise.resolve([]);
4398
- if (config.fileGrepEnabled !== false) {
5197
+ if (config.fileGrepEnabled === true) {
4399
5198
  try {
4400
5199
  const { getProjectName: getProjectName2 } = await Promise.resolve().then(() => (init_project_name(), project_name_exports));
4401
5200
  const projectRoot = process.cwd();
@@ -4431,7 +5230,17 @@ async function hybridSearch(queryText, agentId, options) {
4431
5230
  if (lists.length === 0) return [];
4432
5231
  if (lists.length === 1 && !effectiveIsBroad) return lists[0].slice(0, limit);
4433
5232
  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);
5233
+ let merged = lists.length === 1 ? lists[0].slice(0, rrfLimit) : rrfMergeMulti(lists, rrfLimit, RRF_K, weights);
5234
+ let graphContextMap = /* @__PURE__ */ new Map();
5235
+ if (merged.length > 0) {
5236
+ try {
5237
+ const { applyEntityBoost: applyEntityBoost2 } = await Promise.resolve().then(() => (init_entity_boost(), entity_boost_exports));
5238
+ const boosted = await applyEntityBoost2(merged, effectiveQuery, getClient());
5239
+ merged = boosted.results;
5240
+ graphContextMap = boosted.graphContext;
5241
+ } catch {
5242
+ }
5243
+ }
4435
5244
  const auto = config.scalingRoadmap?.rerankerAutoTrigger ?? {
4436
5245
  enabled: config.rerankerEnabled ?? true,
4437
5246
  broadQueryMinCardinality: 5e4,
@@ -4439,20 +5248,29 @@ async function hybridSearch(queryText, agentId, options) {
4439
5248
  returnTopK: 5
4440
5249
  };
4441
5250
  let rerankedAndBlended = null;
4442
- if (effectiveIsBroad && auto.enabled) {
5251
+ if (effectiveIsBroad && auto.enabled && rerankerAvailable) {
4443
5252
  const cardinality2 = await estimateCardinality(agentId, effectiveOptions);
4444
5253
  if (cardinality2 > auto.broadQueryMinCardinality) {
4445
5254
  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
- }
5255
+ let rerankedRecords;
5256
+ if (graphContextMap.size > 0) {
5257
+ const { rerankWithContext: rerankWithContext2 } = await Promise.resolve().then(() => (init_reranker(), reranker_exports));
5258
+ const candidates = merged.map((m) => ({
5259
+ text: m.raw_text,
5260
+ context: graphContextMap.get(m.id)
5261
+ }));
5262
+ const scored = await rerankWithContext2(effectiveQuery, candidates, auto.returnTopK);
5263
+ rerankedRecords = scored.map((s) => merged[s.index]);
5264
+ } else {
5265
+ const { rerank: rerank2 } = await Promise.resolve().then(() => (init_reranker(), reranker_exports));
5266
+ rerankedRecords = await rerank2(effectiveQuery, merged, auto.returnTopK);
5267
+ }
5268
+ if (rerankedRecords.length > 0) {
5269
+ rerankedAndBlended = rrfMergeMulti(
5270
+ [rerankedRecords],
5271
+ auto.returnTopK,
5272
+ RRF_K
5273
+ );
4456
5274
  }
4457
5275
  } catch {
4458
5276
  }
@@ -4472,7 +5290,7 @@ async function hybridSearch(queryText, agentId, options) {
4472
5290
  try {
4473
5291
  const client = getClient();
4474
5292
  void client.execute({
4475
- sql: `UPDATE memories SET last_accessed = ? WHERE id IN (${placeholders})`,
5293
+ sql: `UPDATE memories SET last_accessed = ?, retrieval_count = COALESCE(retrieval_count, 0) + 1 WHERE id IN (${placeholders})`,
4476
5294
  args: [now, ...ids]
4477
5295
  }).catch(() => {
4478
5296
  });
@@ -4642,7 +5460,7 @@ async function ftsQuery(client, matchExpr, agentId, options, limit) {
4642
5460
  source_type: row.source_type ?? null
4643
5461
  }));
4644
5462
  }
4645
- async function recentRecords(agentId, options, limit) {
5463
+ async function recentRecords(agentId, options, limit, textFilter) {
4646
5464
  const client = getClient();
4647
5465
  const statusFilter = options?.includeArchived ? "" : `
4648
5466
  AND COALESCE(status, 'active') = 'active'`;
@@ -4682,6 +5500,10 @@ async function recentRecords(agentId, options, limit) {
4682
5500
  sql += ` AND memory_type = ?`;
4683
5501
  args.push(options.memoryType);
4684
5502
  }
5503
+ if (textFilter) {
5504
+ sql += ` AND raw_text LIKE '%' || ? || '%'`;
5505
+ args.push(textFilter);
5506
+ }
4685
5507
  sql += ` ORDER BY timestamp DESC LIMIT ?`;
4686
5508
  args.push(limit);
4687
5509
  const result = await client.execute({ sql, args });
@@ -4904,7 +5726,7 @@ if (!process.env.AGENT_ID) {
4904
5726
  if (!loadConfigSync().autoRetrieval) {
4905
5727
  process.exit(0);
4906
5728
  }
4907
- var WORKER_LOG_PATH = path17.join(EXE_AI_DIR, "workers.log");
5729
+ var WORKER_LOG_PATH = path18.join(EXE_AI_DIR, "workers.log");
4908
5730
  function openWorkerLog() {
4909
5731
  try {
4910
5732
  return openSync2(WORKER_LOG_PATH, "a");
@@ -4912,10 +5734,10 @@ function openWorkerLog() {
4912
5734
  return "ignore";
4913
5735
  }
4914
5736
  }
4915
- var CACHE_DIR2 = path17.join(EXE_AI_DIR, "session-cache");
5737
+ var CACHE_DIR2 = path18.join(EXE_AI_DIR, "session-cache");
4916
5738
  function loadInjectedIds(sessionId) {
4917
5739
  try {
4918
- const raw = readFileSync11(path17.join(CACHE_DIR2, `${sessionId}.json`), "utf8");
5740
+ const raw = readFileSync12(path18.join(CACHE_DIR2, `${sessionId}.json`), "utf8");
4919
5741
  return new Set(JSON.parse(raw));
4920
5742
  } catch {
4921
5743
  return /* @__PURE__ */ new Set();
@@ -4923,9 +5745,9 @@ function loadInjectedIds(sessionId) {
4923
5745
  }
4924
5746
  function saveInjectedIds(sessionId, ids) {
4925
5747
  try {
4926
- mkdirSync6(CACHE_DIR2, { recursive: true });
4927
- writeFileSync6(
4928
- path17.join(CACHE_DIR2, `${sessionId}.json`),
5748
+ mkdirSync7(CACHE_DIR2, { recursive: true });
5749
+ writeFileSync7(
5750
+ path18.join(CACHE_DIR2, `${sessionId}.json`),
4929
5751
  JSON.stringify([...ids])
4930
5752
  );
4931
5753
  } catch {
@@ -5008,7 +5830,7 @@ ${fresh.map(
5008
5830
  try {
5009
5831
  const { countPendingReviews: countPendingReviews2, countNewPendingReviewsSince: countNewPendingReviewsSince2 } = await Promise.resolve().then(() => (init_tasks_review(), tasks_review_exports));
5010
5832
  const sessionKey = getSessionKey();
5011
- const lastCheckPath = path17.join(CACHE_DIR2, `review-lastcheck-${sessionKey}.json`);
5833
+ const lastCheckPath = path18.join(CACHE_DIR2, `review-lastcheck-${sessionKey}.json`);
5012
5834
  let sessionScope;
5013
5835
  try {
5014
5836
  const { execSync: execSync7 } = await import("child_process");
@@ -5018,7 +5840,7 @@ ${fresh.map(
5018
5840
  }
5019
5841
  let lastCheckedAt = "";
5020
5842
  try {
5021
- lastCheckedAt = readFileSync11(lastCheckPath, "utf8").trim();
5843
+ lastCheckedAt = readFileSync12(lastCheckPath, "utf8").trim();
5022
5844
  } catch {
5023
5845
  }
5024
5846
  const totalCount = await countPendingReviews2(sessionScope);
@@ -5076,11 +5898,11 @@ IMPORTANT: After completing your current task, you MUST address the pending revi
5076
5898
  function spawnPromptWorker(prompt, sessionId, agent) {
5077
5899
  if (!loadConfigSync().autoIngestion) return;
5078
5900
  try {
5079
- const workerPath = path17.resolve(
5080
- path17.dirname(fileURLToPath3(import.meta.url)),
5901
+ const workerPath = path18.resolve(
5902
+ path18.dirname(fileURLToPath3(import.meta.url)),
5081
5903
  "prompt-ingest-worker.js"
5082
5904
  );
5083
- if (!existsSync14(workerPath)) {
5905
+ if (!existsSync15(workerPath)) {
5084
5906
  process.stderr.write(`[prompt-submit] WARN: prompt-ingest-worker not found at ${workerPath}
5085
5907
  `);
5086
5908
  return;