@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
@@ -632,6 +632,7 @@ __export(employees_exports, {
632
632
  DEFAULT_COORDINATOR_TEMPLATE_NAME: () => DEFAULT_COORDINATOR_TEMPLATE_NAME,
633
633
  EMPLOYEES_PATH: () => EMPLOYEES_PATH,
634
634
  addEmployee: () => addEmployee,
635
+ baseAgentName: () => baseAgentName,
635
636
  canCoordinate: () => canCoordinate,
636
637
  getCoordinatorEmployee: () => getCoordinatorEmployee,
637
638
  getCoordinatorName: () => getCoordinatorName,
@@ -728,6 +729,14 @@ function hasRole(agentName, role) {
728
729
  const emp = getEmployee(employees, agentName);
729
730
  return emp ? emp.role.toLowerCase() === role.toLowerCase() : false;
730
731
  }
732
+ function baseAgentName(name, employees) {
733
+ const match = name.match(/^([a-zA-Z]+)\d+$/);
734
+ if (!match) return name;
735
+ const base = match[1];
736
+ const roster = employees ?? loadEmployeesSync();
737
+ if (getEmployee(roster, base)) return base;
738
+ return name;
739
+ }
731
740
  function isMultiInstance(agentName, employees) {
732
741
  const roster = employees ?? loadEmployeesSync();
733
742
  const emp = getEmployee(roster, agentName);
@@ -844,6 +853,12 @@ function getClient() {
844
853
  if (!_resilientClient) {
845
854
  throw new Error("Database client not initialized. Call initDatabase() first.");
846
855
  }
856
+ if (process.env.EXE_IS_DAEMON === "1") {
857
+ return _resilientClient;
858
+ }
859
+ if (_daemonClient && _daemonClient._isDaemonActive()) {
860
+ return _daemonClient;
861
+ }
847
862
  return _resilientClient;
848
863
  }
849
864
  function getRawClient() {
@@ -1332,6 +1347,12 @@ async function ensureSchema() {
1332
1347
  } catch {
1333
1348
  }
1334
1349
  }
1350
+ try {
1351
+ await client.execute(
1352
+ `CREATE INDEX IF NOT EXISTS idx_memories_content_hash ON memories(content_hash, agent_id)`
1353
+ );
1354
+ } catch {
1355
+ }
1335
1356
  await client.executeMultiple(`
1336
1357
  CREATE TABLE IF NOT EXISTS entities (
1337
1358
  id TEXT PRIMARY KEY,
@@ -1384,7 +1405,30 @@ async function ensureSchema() {
1384
1405
  entity_id TEXT NOT NULL,
1385
1406
  PRIMARY KEY (hyperedge_id, entity_id)
1386
1407
  );
1408
+
1409
+ CREATE VIRTUAL TABLE IF NOT EXISTS entities_fts USING fts5(
1410
+ name,
1411
+ content=entities,
1412
+ content_rowid=rowid
1413
+ );
1414
+
1415
+ CREATE TRIGGER IF NOT EXISTS entities_fts_ai AFTER INSERT ON entities BEGIN
1416
+ INSERT INTO entities_fts(rowid, name) VALUES (new.rowid, new.name);
1417
+ END;
1418
+
1419
+ CREATE TRIGGER IF NOT EXISTS entities_fts_ad AFTER DELETE ON entities BEGIN
1420
+ INSERT INTO entities_fts(entities_fts, rowid, name) VALUES('delete', old.rowid, old.name);
1421
+ END;
1422
+
1423
+ CREATE TRIGGER IF NOT EXISTS entities_fts_au AFTER UPDATE ON entities BEGIN
1424
+ INSERT INTO entities_fts(entities_fts, rowid, name) VALUES('delete', old.rowid, old.name);
1425
+ INSERT INTO entities_fts(rowid, name) VALUES (new.rowid, new.name);
1426
+ END;
1387
1427
  `);
1428
+ try {
1429
+ await client.execute("INSERT INTO entities_fts(entities_fts) VALUES('rebuild')");
1430
+ } catch {
1431
+ }
1388
1432
  await client.executeMultiple(`
1389
1433
  CREATE TABLE IF NOT EXISTS entity_aliases (
1390
1434
  alias TEXT NOT NULL PRIMARY KEY,
@@ -1565,6 +1609,33 @@ async function ensureSchema() {
1565
1609
  CREATE INDEX IF NOT EXISTS idx_conversations_channel
1566
1610
  ON conversations(channel_id);
1567
1611
  `);
1612
+ await client.executeMultiple(`
1613
+ CREATE TABLE IF NOT EXISTS session_agent_map (
1614
+ session_uuid TEXT PRIMARY KEY,
1615
+ agent_id TEXT NOT NULL,
1616
+ session_name TEXT,
1617
+ task_id TEXT,
1618
+ project_name TEXT,
1619
+ started_at TEXT NOT NULL
1620
+ );
1621
+
1622
+ CREATE INDEX IF NOT EXISTS idx_session_agent_map_agent
1623
+ ON session_agent_map(agent_id);
1624
+ `);
1625
+ try {
1626
+ const mapCount = await client.execute({ sql: `SELECT COUNT(*) as cnt FROM session_agent_map`, args: [] });
1627
+ if (Number(mapCount.rows[0]?.cnt ?? 0) === 0) {
1628
+ await client.execute({
1629
+ sql: `INSERT OR IGNORE INTO session_agent_map (session_uuid, agent_id, session_name, started_at)
1630
+ SELECT session_id, agent_id, '', MIN(timestamp)
1631
+ FROM memories
1632
+ WHERE session_id IS NOT NULL AND session_id != '' AND agent_id IS NOT NULL AND agent_id != ''
1633
+ GROUP BY session_id, agent_id`,
1634
+ args: []
1635
+ });
1636
+ }
1637
+ } catch {
1638
+ }
1568
1639
  try {
1569
1640
  await client.execute({
1570
1641
  sql: `ALTER TABLE tasks ADD COLUMN budget_tokens INTEGER`,
@@ -1698,15 +1769,41 @@ async function ensureSchema() {
1698
1769
  });
1699
1770
  } catch {
1700
1771
  }
1772
+ for (const col of [
1773
+ "ALTER TABLE memories ADD COLUMN intent TEXT",
1774
+ "ALTER TABLE memories ADD COLUMN outcome TEXT",
1775
+ "ALTER TABLE memories ADD COLUMN domain TEXT",
1776
+ "ALTER TABLE memories ADD COLUMN referenced_entities TEXT",
1777
+ "ALTER TABLE memories ADD COLUMN retrieval_count INTEGER DEFAULT 0",
1778
+ "ALTER TABLE memories ADD COLUMN chain_position TEXT",
1779
+ "ALTER TABLE memories ADD COLUMN review_status TEXT",
1780
+ "ALTER TABLE memories ADD COLUMN context_window_pct INTEGER",
1781
+ "ALTER TABLE memories ADD COLUMN file_paths TEXT",
1782
+ "ALTER TABLE memories ADD COLUMN commit_hash TEXT",
1783
+ "ALTER TABLE memories ADD COLUMN duration_ms INTEGER",
1784
+ "ALTER TABLE memories ADD COLUMN token_cost REAL",
1785
+ "ALTER TABLE memories ADD COLUMN audience TEXT",
1786
+ "ALTER TABLE memories ADD COLUMN language_type TEXT",
1787
+ "ALTER TABLE memories ADD COLUMN parent_memory_id TEXT"
1788
+ ]) {
1789
+ try {
1790
+ await client.execute(col);
1791
+ } catch {
1792
+ }
1793
+ }
1701
1794
  }
1702
1795
  async function disposeDatabase() {
1796
+ if (_daemonClient) {
1797
+ _daemonClient.close();
1798
+ _daemonClient = null;
1799
+ }
1703
1800
  if (_client) {
1704
1801
  _client.close();
1705
1802
  _client = null;
1706
1803
  _resilientClient = null;
1707
1804
  }
1708
1805
  }
1709
- var _client, _resilientClient, initTurso, disposeTurso;
1806
+ var _client, _resilientClient, _daemonClient, initTurso, disposeTurso;
1710
1807
  var init_database = __esm({
1711
1808
  "src/lib/database.ts"() {
1712
1809
  "use strict";
@@ -1714,6 +1811,7 @@ var init_database = __esm({
1714
1811
  init_employees();
1715
1812
  _client = null;
1716
1813
  _resilientClient = null;
1814
+ _daemonClient = null;
1717
1815
  initTurso = initDatabase;
1718
1816
  disposeTurso = disposeDatabase;
1719
1817
  }
@@ -1748,10 +1846,12 @@ function handleData(chunk) {
1748
1846
  if (!line) continue;
1749
1847
  try {
1750
1848
  const response = JSON.parse(line);
1751
- const entry = _pending.get(response.id);
1849
+ const id = response.id;
1850
+ if (!id) continue;
1851
+ const entry = _pending.get(id);
1752
1852
  if (entry) {
1753
1853
  clearTimeout(entry.timer);
1754
- _pending.delete(response.id);
1854
+ _pending.delete(id);
1755
1855
  entry.resolve(response);
1756
1856
  }
1757
1857
  } catch {
@@ -1922,6 +2022,9 @@ async function connectEmbedDaemon() {
1922
2022
  return false;
1923
2023
  }
1924
2024
  function sendRequest(texts, priority) {
2025
+ return sendDaemonRequest({ texts, priority });
2026
+ }
2027
+ function sendDaemonRequest(payload, timeoutMs = REQUEST_TIMEOUT_MS) {
1925
2028
  return new Promise((resolve) => {
1926
2029
  if (!_socket || !_connected) {
1927
2030
  resolve({ error: "Not connected" });
@@ -1931,10 +2034,10 @@ function sendRequest(texts, priority) {
1931
2034
  const timer = setTimeout(() => {
1932
2035
  _pending.delete(id);
1933
2036
  resolve({ error: "Request timeout" });
1934
- }, REQUEST_TIMEOUT_MS);
2037
+ }, timeoutMs);
1935
2038
  _pending.set(id, { resolve, timer });
1936
2039
  try {
1937
- _socket.write(JSON.stringify({ id, texts, priority }) + "\n");
2040
+ _socket.write(JSON.stringify({ id, ...payload }) + "\n");
1938
2041
  } catch {
1939
2042
  clearTimeout(timer);
1940
2043
  _pending.delete(id);
@@ -1944,30 +2047,11 @@ function sendRequest(texts, priority) {
1944
2047
  }
1945
2048
  async function pingDaemon() {
1946
2049
  if (!_socket || !_connected) return null;
1947
- return new Promise((resolve) => {
1948
- const id = randomUUID();
1949
- const timer = setTimeout(() => {
1950
- _pending.delete(id);
1951
- resolve(null);
1952
- }, 5e3);
1953
- _pending.set(id, {
1954
- resolve: (data) => {
1955
- if (data.health) {
1956
- resolve(data.health);
1957
- } else {
1958
- resolve(null);
1959
- }
1960
- },
1961
- timer
1962
- });
1963
- try {
1964
- _socket.write(JSON.stringify({ id, type: "health" }) + "\n");
1965
- } catch {
1966
- clearTimeout(timer);
1967
- _pending.delete(id);
1968
- resolve(null);
1969
- }
1970
- });
2050
+ const response = await sendDaemonRequest({ type: "health" }, 5e3);
2051
+ if (response.health) {
2052
+ return response.health;
2053
+ }
2054
+ return null;
1971
2055
  }
1972
2056
  function killAndRespawnDaemon() {
1973
2057
  process.stderr.write("[exed-client] Killing daemon for restart...\n");
@@ -2172,12 +2256,20 @@ async function getMasterKey() {
2172
2256
  }
2173
2257
  const keyPath = getKeyPath();
2174
2258
  if (!existsSync4(keyPath)) {
2259
+ process.stderr.write(
2260
+ `[keychain] Key not found at ${keyPath} (HOME=${os3.homedir()}, EXE_OS_DIR=${process.env.EXE_OS_DIR ?? "unset"})
2261
+ `
2262
+ );
2175
2263
  return null;
2176
2264
  }
2177
2265
  try {
2178
2266
  const content = await readFile3(keyPath, "utf-8");
2179
2267
  return Buffer.from(content.trim(), "base64");
2180
- } catch {
2268
+ } catch (err) {
2269
+ process.stderr.write(
2270
+ `[keychain] Key read failed at ${keyPath}: ${err instanceof Error ? err.message : String(err)}
2271
+ `
2272
+ );
2181
2273
  return null;
2182
2274
  }
2183
2275
  }
@@ -2633,6 +2725,7 @@ __export(store_exports, {
2633
2725
  vectorToBlob: () => vectorToBlob,
2634
2726
  writeMemory: () => writeMemory
2635
2727
  });
2728
+ import { createHash } from "crypto";
2636
2729
  function isBusyError2(err) {
2637
2730
  if (err instanceof Error) {
2638
2731
  const msg = err.message.toLowerCase();
@@ -2706,12 +2799,52 @@ function classifyTier(record) {
2706
2799
  if (["store_memory", "manual"].includes(record.tool_name ?? "") && (record.importance ?? 0) >= 5) return 2;
2707
2800
  return 3;
2708
2801
  }
2802
+ function inferFilePaths(record) {
2803
+ if (!["Read", "Write", "Edit"].includes(record.tool_name)) return null;
2804
+ const firstLine = record.raw_text.split("\n")[0] ?? "";
2805
+ const match = firstLine.match(/(\/[\w./-]+\.\w+)/);
2806
+ return match ? JSON.stringify([match[1]]) : null;
2807
+ }
2808
+ function inferCommitHash(record) {
2809
+ if (record.tool_name !== "Bash") return null;
2810
+ const match = record.raw_text.match(/\b([a-f0-9]{7,40})\b/);
2811
+ return match ? match[1] : null;
2812
+ }
2813
+ function inferLanguageType(record) {
2814
+ const text = record.raw_text;
2815
+ if (!text || text.length < 10) return null;
2816
+ const trimmed = text.trimStart();
2817
+ if (trimmed.startsWith("{") || trimmed.startsWith("[")) return "json";
2818
+ if (/\b(SELECT|INSERT|UPDATE|DELETE|CREATE TABLE|ALTER TABLE)\b/i.test(text)) return "sql";
2819
+ if (/\b(function |const |import |export |class |def |async |=>)\b/.test(text)) return "code";
2820
+ if (trimmed.startsWith("#") || trimmed.startsWith("*")) return "prose";
2821
+ return "mixed";
2822
+ }
2823
+ function inferDomain(record) {
2824
+ const proj = (record.project_name ?? "").toLowerCase();
2825
+ if (proj.includes("marketing") || proj.includes("content")) return "marketing";
2826
+ if (proj.includes("crm") || proj.includes("customer")) return "customer";
2827
+ return null;
2828
+ }
2709
2829
  async function writeMemory(record) {
2710
2830
  if (record.vector !== null && record.vector.length !== EMBEDDING_DIM) {
2711
2831
  throw new Error(
2712
2832
  `Expected ${EMBEDDING_DIM}-dim vector, got ${record.vector.length}`
2713
2833
  );
2714
2834
  }
2835
+ const contentHash = createHash("md5").update(record.raw_text).digest("hex");
2836
+ if (_pendingRecords.some((r) => r.content_hash === contentHash && r.agent_id === record.agent_id)) {
2837
+ return;
2838
+ }
2839
+ try {
2840
+ const client = getClient();
2841
+ const existing = await client.execute({
2842
+ sql: "SELECT id FROM memories WHERE content_hash = ? AND agent_id = ? LIMIT 1",
2843
+ args: [contentHash, record.agent_id]
2844
+ });
2845
+ if (existing.rows.length > 0) return;
2846
+ } catch {
2847
+ }
2715
2848
  const dbRow = {
2716
2849
  id: record.id,
2717
2850
  agent_id: record.agent_id,
@@ -2741,7 +2874,23 @@ async function writeMemory(record) {
2741
2874
  supersedes_id: record.supersedes_id ?? null,
2742
2875
  draft: record.draft ? 1 : 0,
2743
2876
  memory_type: record.memory_type ?? "raw",
2744
- trajectory: record.trajectory ? JSON.stringify(record.trajectory) : null
2877
+ trajectory: record.trajectory ? JSON.stringify(record.trajectory) : null,
2878
+ content_hash: contentHash,
2879
+ intent: record.intent ?? null,
2880
+ outcome: record.outcome ?? null,
2881
+ domain: record.domain ?? inferDomain(record),
2882
+ referenced_entities: record.referenced_entities ?? null,
2883
+ retrieval_count: record.retrieval_count ?? 0,
2884
+ chain_position: record.chain_position ?? null,
2885
+ review_status: record.review_status ?? null,
2886
+ context_window_pct: record.context_window_pct ?? null,
2887
+ file_paths: record.file_paths ?? inferFilePaths(record),
2888
+ commit_hash: record.commit_hash ?? inferCommitHash(record),
2889
+ duration_ms: record.duration_ms ?? null,
2890
+ token_cost: record.token_cost ?? null,
2891
+ audience: record.audience ?? null,
2892
+ language_type: record.language_type ?? inferLanguageType(record),
2893
+ parent_memory_id: record.parent_memory_id ?? null
2745
2894
  };
2746
2895
  _pendingRecords.push(dbRow);
2747
2896
  orgBus.emit({
@@ -2799,80 +2948,85 @@ async function flushBatch() {
2799
2948
  const draft = row.draft ? 1 : 0;
2800
2949
  const memoryType = row.memory_type ?? "raw";
2801
2950
  const trajectory = row.trajectory ?? null;
2802
- return {
2803
- sql: hasVector ? `INSERT OR IGNORE INTO memories
2804
- (id, agent_id, agent_role, session_id, timestamp,
2951
+ const contentHash = row.content_hash ?? null;
2952
+ const intent = row.intent ?? null;
2953
+ const outcome = row.outcome ?? null;
2954
+ const domain = row.domain ?? null;
2955
+ const referencedEntities = row.referenced_entities ?? null;
2956
+ const retrievalCount = row.retrieval_count ?? 0;
2957
+ const chainPosition = row.chain_position ?? null;
2958
+ const reviewStatus = row.review_status ?? null;
2959
+ const contextWindowPct = row.context_window_pct ?? null;
2960
+ const filePaths = row.file_paths ?? null;
2961
+ const commitHash = row.commit_hash ?? null;
2962
+ const durationMs = row.duration_ms ?? null;
2963
+ const tokenCost = row.token_cost ?? null;
2964
+ const audience = row.audience ?? null;
2965
+ const languageType = row.language_type ?? null;
2966
+ const parentMemoryId = row.parent_memory_id ?? null;
2967
+ const cols = `id, agent_id, agent_role, session_id, timestamp,
2805
2968
  tool_name, project_name,
2806
2969
  has_error, raw_text, vector, version, task_id, importance, status,
2807
2970
  confidence, last_accessed,
2808
2971
  workspace_id, document_id, user_id, char_offset, page_number,
2809
- source_path, source_type, tier, supersedes_id, draft, memory_type, trajectory)
2810
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, vector32(?), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` : `INSERT OR IGNORE INTO memories
2811
- (id, agent_id, agent_role, session_id, timestamp,
2812
- tool_name, project_name,
2813
- has_error, raw_text, vector, version, task_id, importance, status,
2814
- confidence, last_accessed,
2815
- workspace_id, document_id, user_id, char_offset, page_number,
2816
- source_path, source_type, tier, supersedes_id, draft, memory_type, trajectory)
2817
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
2818
- args: hasVector ? [
2819
- row.id,
2820
- row.agent_id,
2821
- row.agent_role,
2822
- row.session_id,
2823
- row.timestamp,
2824
- row.tool_name,
2825
- row.project_name,
2826
- row.has_error,
2827
- row.raw_text,
2828
- vectorToBlob(row.vector),
2829
- row.version,
2830
- taskId,
2831
- importance,
2832
- status,
2833
- confidence,
2834
- lastAccessed,
2835
- workspaceId,
2836
- documentId,
2837
- userId,
2838
- charOffset,
2839
- pageNumber,
2840
- sourcePath,
2841
- sourceType,
2842
- tier,
2843
- supersedesId,
2844
- draft,
2845
- memoryType,
2846
- trajectory
2847
- ] : [
2848
- row.id,
2849
- row.agent_id,
2850
- row.agent_role,
2851
- row.session_id,
2852
- row.timestamp,
2853
- row.tool_name,
2854
- row.project_name,
2855
- row.has_error,
2856
- row.raw_text,
2857
- row.version,
2858
- taskId,
2859
- importance,
2860
- status,
2861
- confidence,
2862
- lastAccessed,
2863
- workspaceId,
2864
- documentId,
2865
- userId,
2866
- charOffset,
2867
- pageNumber,
2868
- sourcePath,
2869
- sourceType,
2870
- tier,
2871
- supersedesId,
2872
- draft,
2873
- memoryType,
2874
- trajectory
2875
- ]
2972
+ source_path, source_type, tier, supersedes_id, draft, memory_type, trajectory, content_hash,
2973
+ intent, outcome, domain, referenced_entities, retrieval_count,
2974
+ chain_position, review_status, context_window_pct, file_paths, commit_hash,
2975
+ duration_ms, token_cost, audience, language_type, parent_memory_id`;
2976
+ const metaArgs = [
2977
+ intent,
2978
+ outcome,
2979
+ domain,
2980
+ referencedEntities,
2981
+ retrievalCount,
2982
+ chainPosition,
2983
+ reviewStatus,
2984
+ contextWindowPct,
2985
+ filePaths,
2986
+ commitHash,
2987
+ durationMs,
2988
+ tokenCost,
2989
+ audience,
2990
+ languageType,
2991
+ parentMemoryId
2992
+ ];
2993
+ const baseArgs = [
2994
+ row.id,
2995
+ row.agent_id,
2996
+ row.agent_role,
2997
+ row.session_id,
2998
+ row.timestamp,
2999
+ row.tool_name,
3000
+ row.project_name,
3001
+ row.has_error,
3002
+ row.raw_text
3003
+ ];
3004
+ const sharedArgs = [
3005
+ row.version,
3006
+ taskId,
3007
+ importance,
3008
+ status,
3009
+ confidence,
3010
+ lastAccessed,
3011
+ workspaceId,
3012
+ documentId,
3013
+ userId,
3014
+ charOffset,
3015
+ pageNumber,
3016
+ sourcePath,
3017
+ sourceType,
3018
+ tier,
3019
+ supersedesId,
3020
+ draft,
3021
+ memoryType,
3022
+ trajectory,
3023
+ contentHash
3024
+ ];
3025
+ return {
3026
+ sql: hasVector ? `INSERT OR IGNORE INTO memories (${cols})
3027
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, vector32(?), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` : `INSERT OR IGNORE INTO memories (${cols})
3028
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
3029
+ args: hasVector ? [...baseArgs, vectorToBlob(row.vector), ...sharedArgs, ...metaArgs] : [...baseArgs, ...sharedArgs, ...metaArgs]
2876
3030
  };
2877
3031
  };
2878
3032
  const globalClient = getClient();
@@ -3259,7 +3413,7 @@ var LOCAL_WIKI_URL, REQUEST_TIMEOUT_MS2;
3259
3413
  var init_wiki_client = __esm({
3260
3414
  "src/lib/wiki-client.ts"() {
3261
3415
  "use strict";
3262
- LOCAL_WIKI_URL = "http://localhost:3001";
3416
+ LOCAL_WIKI_URL = process.env.EXE_WIKI_URL || "http://localhost:3001";
3263
3417
  REQUEST_TIMEOUT_MS2 = 8e3;
3264
3418
  }
3265
3419
  });
@@ -4743,6 +4897,7 @@ var init_task_scope = __esm({
4743
4897
  // src/lib/tasks-crud.ts
4744
4898
  import crypto5 from "crypto";
4745
4899
  import path12 from "path";
4900
+ import os8 from "os";
4746
4901
  import { execSync as execSync4 } from "child_process";
4747
4902
  import { mkdir as mkdir4, writeFile as writeFile4, appendFile } from "fs/promises";
4748
4903
  import { existsSync as existsSync11, readFileSync as readFileSync10 } from "fs";
@@ -4786,6 +4941,35 @@ function extractParentFromContext(contextBody) {
4786
4941
  function slugify(title) {
4787
4942
  return title.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
4788
4943
  }
4944
+ function buildKeywordIndex() {
4945
+ const idx = /* @__PURE__ */ new Map();
4946
+ for (const [role, keywords] of Object.entries(LANE_KEYWORDS)) {
4947
+ for (const kw of keywords) {
4948
+ const existing = idx.get(kw) ?? [];
4949
+ existing.push(role);
4950
+ idx.set(kw, existing);
4951
+ }
4952
+ }
4953
+ return idx;
4954
+ }
4955
+ function checkLaneAffinity(title, context, assigneeName) {
4956
+ const employees = loadEmployeesSync();
4957
+ const employee = employees.find((e) => e.name === assigneeName);
4958
+ if (!employee) return void 0;
4959
+ const assigneeRole = employee.role;
4960
+ const text = `${title} ${context}`.toLowerCase();
4961
+ const matchedRoles = /* @__PURE__ */ new Set();
4962
+ for (const [keyword, roles] of KEYWORD_INDEX) {
4963
+ if (text.includes(keyword)) {
4964
+ for (const role of roles) matchedRoles.add(role);
4965
+ }
4966
+ }
4967
+ if (matchedRoles.size === 0) return void 0;
4968
+ if (matchedRoles.has(assigneeRole)) return void 0;
4969
+ if (assigneeRole === "COO") return void 0;
4970
+ const expectedRoles = Array.from(matchedRoles).join(" or ");
4971
+ return `\u26A0\uFE0F Lane mismatch: task content suggests ${expectedRoles}, but assigned to ${assigneeName} (${assigneeRole}).`;
4972
+ }
4789
4973
  async function resolveTask(client, identifier, scopeSession) {
4790
4974
  const scope = sessionScopeFilter(scopeSession);
4791
4975
  let result = await client.execute({
@@ -4835,7 +5019,14 @@ async function createTaskCore(input) {
4835
5019
  const id = crypto5.randomUUID();
4836
5020
  const now = (/* @__PURE__ */ new Date()).toISOString();
4837
5021
  const slug = slugify(input.title);
4838
- const taskFile = input.taskFile ?? `exe/${input.assignedTo}/${slug}.md`;
5022
+ let earlySessionScope = null;
5023
+ try {
5024
+ const { resolveExeSession: resolveExeSession2 } = await Promise.resolve().then(() => (init_tmux_routing(), tmux_routing_exports));
5025
+ earlySessionScope = resolveExeSession2();
5026
+ } catch {
5027
+ }
5028
+ const scope = earlySessionScope ?? "default";
5029
+ const taskFile = input.taskFile ?? `tasks/${scope}/${input.assignedTo}/${slug}.md`;
4839
5030
  let blockedById = null;
4840
5031
  const initialStatus = input.blockedBy ? "blocked" : "open";
4841
5032
  if (input.blockedBy) {
@@ -4875,6 +5066,13 @@ async function createTaskCore(input) {
4875
5066
  if (dupCheck.rows.length > 0) {
4876
5067
  warning = `similar active task already exists (${String(dupCheck.rows[0].id)}). Created new task anyway.`;
4877
5068
  }
5069
+ if (!process.env.DISABLE_LANE_AFFINITY) {
5070
+ const laneWarning = checkLaneAffinity(input.title, input.context, input.assignedTo);
5071
+ if (laneWarning) {
5072
+ warning = warning ? `${warning}
5073
+ ${laneWarning}` : laneWarning;
5074
+ }
5075
+ }
4878
5076
  if (input.baseDir) {
4879
5077
  try {
4880
5078
  await mkdir4(path12.join(input.baseDir, "exe", "output"), { recursive: true });
@@ -4885,12 +5083,7 @@ async function createTaskCore(input) {
4885
5083
  }
4886
5084
  }
4887
5085
  const complexity = input.complexity ?? "standard";
4888
- let sessionScope = null;
4889
- try {
4890
- const { resolveExeSession: resolveExeSession2 } = await Promise.resolve().then(() => (init_tmux_routing(), tmux_routing_exports));
4891
- sessionScope = resolveExeSession2();
4892
- } catch {
4893
- }
5086
+ const sessionScope = earlySessionScope;
4894
5087
  await client.execute({
4895
5088
  sql: `INSERT INTO tasks (id, title, assigned_to, assigned_by, project_name, priority, status, task_file, blocked_by, parent_task_id, reviewer, context, complexity, budget_tokens, budget_fallback_model, tokens_used, tokens_warned_at, session_scope, created_at, updated_at)
4896
5089
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
@@ -4917,6 +5110,39 @@ async function createTaskCore(input) {
4917
5110
  now
4918
5111
  ]
4919
5112
  });
5113
+ if (input.baseDir) {
5114
+ try {
5115
+ const EXE_OS_DIR = path12.join(os8.homedir(), ".exe-os");
5116
+ const mdPath = path12.join(EXE_OS_DIR, taskFile);
5117
+ const mdDir = path12.dirname(mdPath);
5118
+ if (!existsSync11(mdDir)) await mkdir4(mdDir, { recursive: true });
5119
+ const reviewer = input.reviewer ?? input.assignedBy;
5120
+ const mdContent = `# ${input.title}
5121
+
5122
+ **ID:** ${id}
5123
+ **Status:** ${initialStatus}
5124
+ **Priority:** ${input.priority}
5125
+ **Assigned by:** ${input.assignedBy}
5126
+ **Assigned to:** ${input.assignedTo}
5127
+ **Project:** ${input.projectName}
5128
+ **Created:** ${now.split("T")[0]}${parentTaskId ? `
5129
+ **Parent task:** ${parentTaskId}` : ""}
5130
+ **Reviewer:** ${reviewer}
5131
+
5132
+ ## Context
5133
+
5134
+ ${input.context}
5135
+
5136
+ ## MANDATORY: When done
5137
+
5138
+ You MUST call update_task with status "done" and a result summary when finished.
5139
+ If you skip this, your reviewer will not know you're done and your work won't be reviewed.
5140
+ Do NOT let a failed commit or any error prevent you from calling update_task(done).
5141
+ `;
5142
+ await writeFile4(mdPath, mdContent, "utf-8");
5143
+ } catch {
5144
+ }
5145
+ }
4920
5146
  return {
4921
5147
  id,
4922
5148
  title: input.title,
@@ -5109,7 +5335,7 @@ ${input.result}` : `\u26A0\uFE0F ${warning}`;
5109
5335
  return { row, taskFile, now, taskId };
5110
5336
  }
5111
5337
  }
5112
- if (curStatus === "in_progress" && input.callerAgentId && (input.callerAgentId === assignedBy || input.callerAgentId === "exe")) {
5338
+ if (curStatus === "in_progress" && input.callerAgentId && (input.callerAgentId === assignedBy || isCoordinatorName(input.callerAgentId))) {
5113
5339
  process.stderr.write(
5114
5340
  `[tasks] Assigner override: ${input.callerAgentId} reclaiming ${taskId}
5115
5341
  `
@@ -5221,12 +5447,22 @@ async function ensureGitignoreExe(baseDir) {
5221
5447
  } catch {
5222
5448
  }
5223
5449
  }
5224
- var DELEGATION_KEYWORDS, TASK_ALREADY_CLAIMED_PREFIX;
5450
+ var LANE_KEYWORDS, KEYWORD_INDEX, DELEGATION_KEYWORDS, TASK_ALREADY_CLAIMED_PREFIX;
5225
5451
  var init_tasks_crud = __esm({
5226
5452
  "src/lib/tasks-crud.ts"() {
5227
5453
  "use strict";
5228
5454
  init_database();
5229
5455
  init_task_scope();
5456
+ init_employees();
5457
+ LANE_KEYWORDS = {
5458
+ CMO: ["sales", "script", "pitch", "offer", "copy", "objection", "brand", "content", "seo", "marketing", "newsletter", "carousel", "social", "campaign"],
5459
+ CTO: ["spec", "architecture", "migration", "schema", "database", "design doc", "adr", "security audit", "tech stack"],
5460
+ "Principal Engineer": ["implement", "build", "fix", "commit", "refactor", "bug", "feature", "wire", "integration"],
5461
+ "Staff Code Reviewer": ["critique", "verdict", "review", "audit", "code quality"],
5462
+ "Content Production Specialist": ["render", "video", "image", "b-roll", "remotion", "animation", "thumbnail"],
5463
+ "AI Product Lead": ["competitive", "analysis", "benchmark", "compare", "scout", "evaluate", "poc"]
5464
+ };
5465
+ KEYWORD_INDEX = buildKeywordIndex();
5230
5466
  DELEGATION_KEYWORDS = /parallel|delegate|wave|worktree|multi-instance/i;
5231
5467
  TASK_ALREADY_CLAIMED_PREFIX = "TASK_ALREADY_CLAIMED";
5232
5468
  }
@@ -5256,7 +5492,7 @@ async function countNewPendingReviewsSince(sinceIso, sessionScope) {
5256
5492
  const result2 = await client.execute({
5257
5493
  sql: `SELECT COUNT(*) as cnt FROM tasks
5258
5494
  WHERE status = 'needs_review' AND updated_at > ?
5259
- AND (session_scope = ? OR session_scope IS NULL)`,
5495
+ AND session_scope = ?`,
5260
5496
  args: [sinceIso, sessionScope]
5261
5497
  });
5262
5498
  return Number(result2.rows[0]?.cnt) || 0;
@@ -5274,7 +5510,7 @@ async function listPendingReviews(limit, sessionScope) {
5274
5510
  const result2 = await client.execute({
5275
5511
  sql: `SELECT title, assigned_to, project_name FROM tasks
5276
5512
  WHERE status = 'needs_review'
5277
- AND (session_scope = ? OR session_scope IS NULL)
5513
+ AND session_scope = ?
5278
5514
  ORDER BY priority ASC, created_at DESC LIMIT ?`,
5279
5515
  args: [sessionScope, limit]
5280
5516
  });
@@ -5395,14 +5631,14 @@ async function cleanupReviewFile(row, taskFile, _baseDir) {
5395
5631
  if (parts.length >= 3 && parts[0] === "review") {
5396
5632
  const agent = parts[1];
5397
5633
  const slug = parts.slice(2).join("-");
5398
- const originalTaskFile = `exe/${agent}/${slug}.md`;
5634
+ const legacyTaskFile = `exe/${agent}/${slug}.md`;
5399
5635
  const result = await client.execute({
5400
- sql: "UPDATE tasks SET status = 'done', updated_at = ? WHERE task_file = ? AND status = 'needs_review'",
5401
- args: [now, originalTaskFile]
5636
+ sql: "UPDATE tasks SET status = 'done', updated_at = ? WHERE (task_file = ? OR task_file LIKE ?) AND status = 'needs_review'",
5637
+ args: [now, legacyTaskFile, `tasks/%/${agent}/${slug}.md`]
5402
5638
  });
5403
5639
  if (result.rowsAffected > 0) {
5404
5640
  process.stderr.write(
5405
- `[review-cleanup] Cascaded original task to done (legacy path): ${originalTaskFile}
5641
+ `[review-cleanup] Cascaded original task to done: ${agent}/${slug}.md
5406
5642
  `
5407
5643
  );
5408
5644
  }
@@ -5584,7 +5820,7 @@ function findSessionForProject(projectName) {
5584
5820
  const sessions = listSessions();
5585
5821
  for (const s of sessions) {
5586
5822
  const proj = s.projectDir.split("/").filter(Boolean).pop();
5587
- if (proj === projectName && (s.agentId === "exe" || isCoordinatorName(s.agentId))) return s;
5823
+ if (proj === projectName && isCoordinatorName(s.agentId)) return s;
5588
5824
  }
5589
5825
  return null;
5590
5826
  }
@@ -5630,7 +5866,7 @@ var init_session_scope = __esm({
5630
5866
 
5631
5867
  // src/lib/tasks-notify.ts
5632
5868
  async function dispatchTaskToEmployee(input) {
5633
- if (input.assignedTo === "exe" || isCoordinatorName(input.assignedTo)) return { dispatched: "skipped" };
5869
+ if (isCoordinatorName(input.assignedTo)) return { dispatched: "skipped" };
5634
5870
  let crossProject = false;
5635
5871
  if (input.projectName) {
5636
5872
  try {
@@ -6109,7 +6345,7 @@ async function updateTask(input) {
6109
6345
  }
6110
6346
  const isTerminal = input.status === "done" || input.status === "needs_review";
6111
6347
  if (isTerminal) {
6112
- const isCoordinator = String(row.assigned_to) === "exe" || isCoordinatorName(String(row.assigned_to));
6348
+ const isCoordinator = isCoordinatorName(String(row.assigned_to));
6113
6349
  if (!isCoordinator) {
6114
6350
  notifyTaskDone();
6115
6351
  }
@@ -6134,7 +6370,7 @@ async function updateTask(input) {
6134
6370
  }
6135
6371
  }
6136
6372
  }
6137
- if (input.status === "done" && String(row.assigned_to) !== "exe" && !isCoordinatorName(String(row.assigned_to)) && !process.env.VITEST) {
6373
+ if (input.status === "done" && !isCoordinatorName(String(row.assigned_to)) && !process.env.VITEST) {
6138
6374
  Promise.resolve().then(() => (init_skill_learning(), skill_learning_exports)).then(
6139
6375
  ({ captureAndLearn: captureAndLearn2 }) => captureAndLearn2({
6140
6376
  taskId,
@@ -6150,7 +6386,7 @@ async function updateTask(input) {
6150
6386
  });
6151
6387
  }
6152
6388
  let nextTask;
6153
- if (isTerminal && String(row.assigned_to) !== "exe" && !isCoordinatorName(String(row.assigned_to))) {
6389
+ if (isTerminal && !isCoordinatorName(String(row.assigned_to))) {
6154
6390
  try {
6155
6391
  nextTask = await findNextTask(String(row.assigned_to));
6156
6392
  } catch {
@@ -6518,7 +6754,7 @@ __export(tmux_routing_exports, {
6518
6754
  import { execFileSync as execFileSync2, execSync as execSync6 } from "child_process";
6519
6755
  import { readFileSync as readFileSync11, writeFileSync as writeFileSync6, mkdirSync as mkdirSync7, existsSync as existsSync13, appendFileSync } from "fs";
6520
6756
  import path17 from "path";
6521
- import os8 from "os";
6757
+ import os9 from "os";
6522
6758
  import { fileURLToPath as fileURLToPath2 } from "url";
6523
6759
  import { unlinkSync as unlinkSync6 } from "fs";
6524
6760
  function spawnLockPath(sessionName) {
@@ -6842,7 +7078,7 @@ function notifyParentExe(sessionKey) {
6842
7078
  return true;
6843
7079
  }
6844
7080
  function ensureEmployee(employeeName, exeSession, projectDir, opts) {
6845
- if (employeeName === "exe" || isCoordinatorName(employeeName)) {
7081
+ if (isCoordinatorName(employeeName)) {
6846
7082
  return { status: "failed", sessionName: "", error: "The COO is not a dispatchable employee" };
6847
7083
  }
6848
7084
  try {
@@ -6914,7 +7150,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
6914
7150
  const transport = getTransport();
6915
7151
  const sessionName = employeeSessionName(employeeName, exeSession, opts?.instance);
6916
7152
  const instanceLabel = opts?.instance != null && opts.instance > 0 ? `${employeeName}${opts.instance}` : employeeName;
6917
- const logDir = path17.join(os8.homedir(), ".exe-os", "session-logs");
7153
+ const logDir = path17.join(os9.homedir(), ".exe-os", "session-logs");
6918
7154
  const logFile = path17.join(logDir, `${instanceLabel}-${Date.now()}.log`);
6919
7155
  if (!existsSync13(logDir)) {
6920
7156
  mkdirSync7(logDir, { recursive: true });
@@ -6930,7 +7166,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
6930
7166
  } catch {
6931
7167
  }
6932
7168
  try {
6933
- const claudeJsonPath = path17.join(os8.homedir(), ".claude.json");
7169
+ const claudeJsonPath = path17.join(os9.homedir(), ".claude.json");
6934
7170
  let claudeJson = {};
6935
7171
  try {
6936
7172
  claudeJson = JSON.parse(readFileSync11(claudeJsonPath, "utf8"));
@@ -6945,7 +7181,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
6945
7181
  } catch {
6946
7182
  }
6947
7183
  try {
6948
- const settingsDir = path17.join(os8.homedir(), ".claude", "projects");
7184
+ const settingsDir = path17.join(os9.homedir(), ".claude", "projects");
6949
7185
  const normalizedKey = (opts?.cwd ?? projectDir).replace(/\//g, "-").replace(/^-/, "");
6950
7186
  const projSettingsDir = path17.join(settingsDir, normalizedKey);
6951
7187
  const settingsPath = path17.join(projSettingsDir, "settings.json");
@@ -6993,7 +7229,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
6993
7229
  let legacyFallbackWarned = false;
6994
7230
  if (!useExeAgent && !useBinSymlink) {
6995
7231
  const identityPath = path17.join(
6996
- os8.homedir(),
7232
+ os9.homedir(),
6997
7233
  ".exe-os",
6998
7234
  "identity",
6999
7235
  `${employeeName}.md`
@@ -7023,7 +7259,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
7023
7259
  }
7024
7260
  let sessionContextFlag = "";
7025
7261
  try {
7026
- const ctxDir = path17.join(os8.homedir(), ".exe-os", "session-cache");
7262
+ const ctxDir = path17.join(os9.homedir(), ".exe-os", "session-cache");
7027
7263
  mkdirSync7(ctxDir, { recursive: true });
7028
7264
  const ctxFile = path17.join(ctxDir, `session-context-${sessionName}.md`);
7029
7265
  const ctxContent = [
@@ -7135,13 +7371,13 @@ var init_tmux_routing = __esm({
7135
7371
  init_intercom_queue();
7136
7372
  init_plan_limits();
7137
7373
  init_employees();
7138
- SPAWN_LOCK_DIR = path17.join(os8.homedir(), ".exe-os", "spawn-locks");
7139
- SESSION_CACHE = path17.join(os8.homedir(), ".exe-os", "session-cache");
7374
+ SPAWN_LOCK_DIR = path17.join(os9.homedir(), ".exe-os", "spawn-locks");
7375
+ SESSION_CACHE = path17.join(os9.homedir(), ".exe-os", "session-cache");
7140
7376
  BEHAVIORS_EXPORT_TIMEOUT_MS = 1e4;
7141
7377
  VALID_SESSION_NAME = /^[a-z]+\d*-[a-zA-Z0-9_]+$/;
7142
7378
  VERIFY_PANE_LINES = 200;
7143
7379
  INTERCOM_DEBOUNCE_MS = 3e4;
7144
- INTERCOM_LOG2 = path17.join(os8.homedir(), ".exe-os", "intercom.log");
7380
+ INTERCOM_LOG2 = path17.join(os9.homedir(), ".exe-os", "intercom.log");
7145
7381
  DEBOUNCE_FILE = path17.join(SESSION_CACHE, "intercom-debounce.json");
7146
7382
  DEBOUNCE_CLEANUP_AGE_MS = 5 * 60 * 1e3;
7147
7383
  BUSY_PATTERN = /[✻✽✶✳·].*…|Running…/;
@@ -8051,10 +8287,49 @@ function buildPermissionContext(platform, permissions) {
8051
8287
  return `[${platform.toUpperCase()} \u2014 allowed: ${parts.join(", ")}]`;
8052
8288
  }
8053
8289
 
8290
+ // src/gateway/bot-errors.ts
8291
+ var FatalBotError = class extends Error {
8292
+ constructor(message, cause) {
8293
+ super(message);
8294
+ this.cause = cause;
8295
+ this.name = "FatalBotError";
8296
+ }
8297
+ fatal = true;
8298
+ };
8299
+ var RecoverableBotError = class extends Error {
8300
+ constructor(message, toolName, cause) {
8301
+ super(message);
8302
+ this.toolName = toolName;
8303
+ this.cause = cause;
8304
+ this.name = "RecoverableBotError";
8305
+ }
8306
+ recoverable = true;
8307
+ };
8308
+ var MaxStepsError = class extends Error {
8309
+ constructor(stepsTaken, maxSteps) {
8310
+ super(
8311
+ `Reached maximum steps (${stepsTaken}/${maxSteps}). Returning partial result.`
8312
+ );
8313
+ this.stepsTaken = stepsTaken;
8314
+ this.maxSteps = maxSteps;
8315
+ this.name = "MaxStepsError";
8316
+ }
8317
+ };
8318
+ function classifyError(err, toolName) {
8319
+ if (err instanceof FatalBotError) return err;
8320
+ if (err instanceof RecoverableBotError) return err;
8321
+ const message = err instanceof Error ? err.message : String(err);
8322
+ if (message.includes("401") || message.includes("403") || message.includes("authentication") || message.includes("rate_limit")) {
8323
+ return new FatalBotError(message, err);
8324
+ }
8325
+ return new RecoverableBotError(message, toolName, err);
8326
+ }
8327
+
8054
8328
  // src/gateway/bot-runtime.ts
8055
8329
  var DEFAULT_MODEL = "claude-sonnet-4-20250514";
8056
8330
  var MAX_TURNS = 10;
8057
8331
  var MAX_HISTORY = 50;
8332
+ var DEFAULT_PLANNING_INTERVAL = 3;
8058
8333
  function buildExecAssistantSystemPrompt(platform, permissions) {
8059
8334
  const permContext = buildPermissionContext(platform, permissions);
8060
8335
  return `You are the founder's executive assistant (agent_id: "ea").
@@ -8106,7 +8381,7 @@ var BotRuntime = class {
8106
8381
  async processMessage(msg, permissions) {
8107
8382
  const sessionKey = msg.chatType === "group" ? msg.channelId : msg.senderId;
8108
8383
  const history = this.getHistory(sessionKey);
8109
- history.push({ role: "user", content: msg.text });
8384
+ history.push({ role: "user", content: msg.text, frameType: "task" });
8110
8385
  const systemPrompt = this.config.systemPrompt + "\n\n" + buildPermissionContext(msg.platform, permissions);
8111
8386
  const allowedTools = filterToolsForPermissions(
8112
8387
  this.config.tools,
@@ -8114,29 +8389,54 @@ var BotRuntime = class {
8114
8389
  );
8115
8390
  const model = this.config.model ?? DEFAULT_MODEL;
8116
8391
  const maxTurns = this.config.maxTurns ?? MAX_TURNS;
8392
+ const planningInterval = this.config.planningInterval ?? DEFAULT_PLANNING_INTERVAL;
8117
8393
  let turns = 0;
8118
8394
  while (turns < maxTurns) {
8119
8395
  turns++;
8120
- const response = await this.client.messages.create({
8121
- model,
8122
- max_tokens: 4096,
8123
- system: systemPrompt,
8124
- messages: history.map((m) => ({
8125
- role: m.role,
8126
- content: m.content
8127
- })),
8128
- tools: allowedTools.map((t) => ({
8129
- name: t.name,
8130
- description: t.description,
8131
- input_schema: t.input_schema
8132
- }))
8133
- });
8396
+ if (planningInterval > 0 && turns > 1 && turns % planningInterval === 1 && maxTurns - turns >= 2) {
8397
+ history.push({
8398
+ role: "user",
8399
+ content: "[Planning checkpoint] Review what you know so far. What facts have you gathered? What is your plan for the remaining steps? Be concise.",
8400
+ frameType: "planning"
8401
+ });
8402
+ }
8403
+ let response;
8404
+ try {
8405
+ response = await this.client.messages.create({
8406
+ model,
8407
+ max_tokens: 4096,
8408
+ system: systemPrompt,
8409
+ messages: history.map((m) => ({
8410
+ role: m.role,
8411
+ content: m.content
8412
+ })),
8413
+ tools: allowedTools.map((t) => ({
8414
+ name: t.name,
8415
+ description: t.description,
8416
+ input_schema: t.input_schema
8417
+ }))
8418
+ });
8419
+ } catch (err) {
8420
+ const classified = classifyError(err);
8421
+ if (classified instanceof FatalBotError) {
8422
+ const errorMsg = `Bot error: ${classified.message}`;
8423
+ history.push({ role: "assistant", content: errorMsg, frameType: "error" });
8424
+ this.trimHistory(sessionKey);
8425
+ return errorMsg;
8426
+ }
8427
+ history.push({
8428
+ role: "assistant",
8429
+ content: `[API error \u2014 retrying] ${classified.message}`,
8430
+ frameType: "error"
8431
+ });
8432
+ continue;
8433
+ }
8134
8434
  const toolUseBlocks = response.content.filter(
8135
8435
  (b) => b.type === "tool_use"
8136
8436
  );
8137
8437
  if (toolUseBlocks.length === 0) {
8138
8438
  const textContent = response.content.filter((b) => b.type === "text").map((b) => b.text).join("\n");
8139
- history.push({ role: "assistant", content: textContent });
8439
+ history.push({ role: "assistant", content: textContent, frameType: "assistant" });
8140
8440
  this.trimHistory(sessionKey);
8141
8441
  return textContent;
8142
8442
  }
@@ -8146,9 +8446,11 @@ var BotRuntime = class {
8146
8446
  );
8147
8447
  history.push({
8148
8448
  role: "assistant",
8149
- content: response.content
8449
+ content: response.content,
8450
+ frameType: "assistant"
8150
8451
  });
8151
8452
  const toolResults = [];
8453
+ let hadFatalError = false;
8152
8454
  for (const block of allowed) {
8153
8455
  try {
8154
8456
  const result = await this.config.toolExecutor(
@@ -8161,12 +8463,23 @@ var BotRuntime = class {
8161
8463
  content: result
8162
8464
  });
8163
8465
  } catch (err) {
8164
- toolResults.push({
8165
- type: "tool_result",
8166
- tool_use_id: block.id,
8167
- content: `Error: ${err instanceof Error ? err.message : String(err)}`,
8168
- is_error: true
8169
- });
8466
+ const classified = classifyError(err, block.name);
8467
+ if (classified instanceof FatalBotError) {
8468
+ toolResults.push({
8469
+ type: "tool_result",
8470
+ tool_use_id: block.id,
8471
+ content: `Fatal error: ${classified.message}`,
8472
+ is_error: true
8473
+ });
8474
+ hadFatalError = true;
8475
+ } else {
8476
+ toolResults.push({
8477
+ type: "tool_result",
8478
+ tool_use_id: block.id,
8479
+ content: `Error (recoverable): ${classified.message}`,
8480
+ is_error: true
8481
+ });
8482
+ }
8170
8483
  }
8171
8484
  }
8172
8485
  for (const { block, check } of blocked) {
@@ -8179,10 +8492,22 @@ var BotRuntime = class {
8179
8492
  }
8180
8493
  history.push({
8181
8494
  role: "user",
8182
- content: toolResults
8495
+ content: toolResults,
8496
+ frameType: hadFatalError ? "error" : "tool_result"
8183
8497
  });
8498
+ if (hadFatalError) {
8499
+ this.trimHistory(sessionKey);
8500
+ return `A fatal error occurred during tool execution. The bot loop has been stopped.`;
8501
+ }
8184
8502
  }
8185
- return "I reached the maximum number of tool calls for this request. Please try again with a more specific question.";
8503
+ const maxErr = new MaxStepsError(turns, maxTurns);
8504
+ history.push({
8505
+ role: "assistant",
8506
+ content: maxErr.message,
8507
+ frameType: "error"
8508
+ });
8509
+ this.trimHistory(sessionKey);
8510
+ return maxErr.message;
8186
8511
  }
8187
8512
  getHistory(sessionKey) {
8188
8513
  if (!this.conversations.has(sessionKey)) {
@@ -8192,9 +8517,19 @@ var BotRuntime = class {
8192
8517
  }
8193
8518
  trimHistory(sessionKey) {
8194
8519
  const history = this.conversations.get(sessionKey);
8195
- if (history && history.length > MAX_HISTORY) {
8196
- this.conversations.set(sessionKey, history.slice(-MAX_HISTORY));
8520
+ if (!history || history.length <= MAX_HISTORY) return;
8521
+ const firstTaskIdx = history.findIndex((m) => m.frameType === "task");
8522
+ let trimmed = history.filter(
8523
+ (m, i) => m.frameType !== "planning" || i >= history.length - MAX_HISTORY
8524
+ );
8525
+ if (trimmed.length > MAX_HISTORY) {
8526
+ const tail = trimmed.slice(-MAX_HISTORY);
8527
+ if (firstTaskIdx >= 0 && !tail.includes(history[firstTaskIdx])) {
8528
+ tail[0] = history[firstTaskIdx];
8529
+ }
8530
+ trimmed = tail;
8197
8531
  }
8532
+ this.conversations.set(sessionKey, trimmed);
8198
8533
  }
8199
8534
  /** Clear conversation history for a session */
8200
8535
  clearHistory(sessionKey) {
@@ -8900,8 +9235,19 @@ import { randomUUID as randomUUID5 } from "crypto";
8900
9235
  import { homedir } from "os";
8901
9236
  import { join } from "path";
8902
9237
  import { mkdirSync as mkdirSync2 } from "fs";
8903
- var RECONNECT_DELAY_MS = 5e3;
9238
+ var INITIAL_BACKOFF_MS = 1e3;
9239
+ var MAX_BACKOFF_MS = 3e5;
9240
+ var BACKOFF_MULTIPLIER = 2;
9241
+ var JITTER_FACTOR = 0.25;
8904
9242
  var AUTH_DIR = join(homedir(), ".exe-os", "whatsapp-auth");
9243
+ function calculateBackoff(retryCount) {
9244
+ const base = Math.min(
9245
+ INITIAL_BACKOFF_MS * BACKOFF_MULTIPLIER ** retryCount,
9246
+ MAX_BACKOFF_MS
9247
+ );
9248
+ const jitter = base * JITTER_FACTOR * (2 * Math.random() - 1);
9249
+ return Math.max(INITIAL_BACKOFF_MS, Math.round(base + jitter));
9250
+ }
8905
9251
  var WhatsAppAdapter = class {
8906
9252
  platform = "whatsapp";
8907
9253
  sock = null;
@@ -8909,6 +9255,9 @@ var WhatsAppAdapter = class {
8909
9255
  connected = false;
8910
9256
  abortController = null;
8911
9257
  authDir = AUTH_DIR;
9258
+ // Resilience state
9259
+ retryCount = 0;
9260
+ disconnectedAt = 0;
8912
9261
  async connect(config2) {
8913
9262
  this.authDir = config2.credentials.authDir ?? AUTH_DIR;
8914
9263
  mkdirSync2(this.authDir, { recursive: true });
@@ -8917,6 +9266,20 @@ var WhatsAppAdapter = class {
8917
9266
  const { state, saveCreds } = await useMultiFileAuthState(this.authDir);
8918
9267
  const { version } = await fetchLatestBaileysVersion();
8919
9268
  this.abortController = new AbortController();
9269
+ let agent;
9270
+ const socksProxy = config2.credentials.socksProxy;
9271
+ if (socksProxy) {
9272
+ try {
9273
+ const modName = "socks-proxy-agent";
9274
+ const mod = await import(modName);
9275
+ const SocksProxyAgent = mod.SocksProxyAgent ?? mod.default;
9276
+ agent = new SocksProxyAgent(socksProxy);
9277
+ console.log(`[whatsapp] Using SOCKS proxy: ${socksProxy.replace(/\/\/.*@/, "//***@")}`);
9278
+ } catch {
9279
+ console.error("[whatsapp] socks-proxy-agent not installed \u2014 run: npm i socks-proxy-agent");
9280
+ throw new Error("SOCKS proxy configured but socks-proxy-agent package not installed");
9281
+ }
9282
+ }
8920
9283
  const sock = makeWASocket({
8921
9284
  auth: {
8922
9285
  creds: state.creds,
@@ -8926,7 +9289,8 @@ var WhatsAppAdapter = class {
8926
9289
  printQRInTerminal: true,
8927
9290
  browser: ["exe-os", "cli", "1.0"],
8928
9291
  syncFullHistory: false,
8929
- markOnlineOnConnect: false
9292
+ markOnlineOnConnect: false,
9293
+ ...agent ? { agent } : {}
8930
9294
  });
8931
9295
  this.sock = sock;
8932
9296
  sock.ev.on("creds.update", saveCreds);
@@ -8934,18 +9298,32 @@ var WhatsAppAdapter = class {
8934
9298
  const { connection, lastDisconnect } = update;
8935
9299
  if (connection === "close") {
8936
9300
  this.connected = false;
9301
+ if (this.disconnectedAt === 0) this.disconnectedAt = Date.now();
8937
9302
  const statusCode = lastDisconnect?.error?.output?.statusCode;
8938
9303
  const shouldReconnect = statusCode !== DisconnectReason.loggedOut;
8939
9304
  if (shouldReconnect && !this.abortController?.signal.aborted) {
8940
- console.log(`[whatsapp] Connection closed (${statusCode}), reconnecting...`);
8941
- setTimeout(() => void this.connect(config2), RECONNECT_DELAY_MS);
9305
+ const delay2 = calculateBackoff(this.retryCount);
9306
+ this.retryCount++;
9307
+ console.log(
9308
+ `[whatsapp] Connection closed (code=${statusCode}), retry #${this.retryCount} in ${(delay2 / 1e3).toFixed(1)}s` + (socksProxy ? ` (proxy: ${socksProxy.replace(/\/\/.*@/, "//***@")})` : "")
9309
+ );
9310
+ setTimeout(() => void this.connect(config2), delay2);
8942
9311
  } else {
8943
9312
  console.log("[whatsapp] Logged out \u2014 clear auth and re-scan QR");
8944
9313
  }
8945
9314
  }
8946
9315
  if (connection === "open") {
9316
+ if (this.retryCount > 0) {
9317
+ const downtimeSec = this.disconnectedAt > 0 ? ((Date.now() - this.disconnectedAt) / 1e3).toFixed(1) : "?";
9318
+ console.log(
9319
+ `[whatsapp] Reconnected after ${this.retryCount} retries (${downtimeSec}s downtime)`
9320
+ );
9321
+ } else {
9322
+ console.log("[whatsapp] Connected via Baileys (linked device)");
9323
+ }
8947
9324
  this.connected = true;
8948
- console.log("[whatsapp] Connected via Baileys (linked device)");
9325
+ this.retryCount = 0;
9326
+ this.disconnectedAt = 0;
8949
9327
  }
8950
9328
  });
8951
9329
  sock.ev.on("messages.upsert", (upsert) => {
@@ -11035,8 +11413,8 @@ async function ensureCRMContact(info) {
11035
11413
  import { readFileSync as readFileSync12, writeFileSync as writeFileSync7, existsSync as existsSync14, mkdirSync as mkdirSync8 } from "fs";
11036
11414
  import { randomUUID as randomUUID12 } from "crypto";
11037
11415
  import path18 from "path";
11038
- import os9 from "os";
11039
- var TRIGGERS_PATH = path18.join(os9.homedir(), ".exe-os", "triggers.json");
11416
+ import os10 from "os";
11417
+ var TRIGGERS_PATH = path18.join(os10.homedir(), ".exe-os", "triggers.json");
11040
11418
  var GRAPH_API_VERSION = "v21.0";
11041
11419
  function substituteTemplate(template, record) {
11042
11420
  return template.replace(
@@ -11390,13 +11768,16 @@ export {
11390
11768
  FULL_ACCESS,
11391
11769
  FailoverCascade,
11392
11770
  FailoverExhaustedError,
11771
+ FatalBotError,
11393
11772
  Gateway,
11394
11773
  IMessageAdapter,
11774
+ MaxStepsError,
11395
11775
  OllamaProvider,
11396
11776
  OpenAICompatProvider,
11397
11777
  READ_ONLY,
11398
11778
  READ_TOOLS,
11399
11779
  RateLimiter,
11780
+ RecoverableBotError,
11400
11781
  SessionStore,
11401
11782
  SignalAdapter,
11402
11783
  SlackAdapter,
@@ -11408,6 +11789,7 @@ export {
11408
11789
  buildExecAssistantTools,
11409
11790
  buildPermissionContext,
11410
11791
  checkToolPermission,
11792
+ classifyError,
11411
11793
  createCRMWebhookHandler,
11412
11794
  createPerson,
11413
11795
  createReceptionist,