@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
@@ -639,6 +639,7 @@ __export(employees_exports, {
639
639
  DEFAULT_COORDINATOR_TEMPLATE_NAME: () => DEFAULT_COORDINATOR_TEMPLATE_NAME,
640
640
  EMPLOYEES_PATH: () => EMPLOYEES_PATH,
641
641
  addEmployee: () => addEmployee,
642
+ baseAgentName: () => baseAgentName,
642
643
  canCoordinate: () => canCoordinate,
643
644
  getCoordinatorEmployee: () => getCoordinatorEmployee,
644
645
  getCoordinatorName: () => getCoordinatorName,
@@ -735,6 +736,14 @@ function hasRole(agentName, role) {
735
736
  const emp = getEmployee(employees, agentName);
736
737
  return emp ? emp.role.toLowerCase() === role.toLowerCase() : false;
737
738
  }
739
+ function baseAgentName(name, employees) {
740
+ const match = name.match(/^([a-zA-Z]+)\d+$/);
741
+ if (!match) return name;
742
+ const base = match[1];
743
+ const roster = employees ?? loadEmployeesSync();
744
+ if (getEmployee(roster, base)) return base;
745
+ return name;
746
+ }
738
747
  function isMultiInstance(agentName, employees) {
739
748
  const roster = employees ?? loadEmployeesSync();
740
749
  const emp = getEmployee(roster, agentName);
@@ -851,6 +860,12 @@ function getClient() {
851
860
  if (!_resilientClient) {
852
861
  throw new Error("Database client not initialized. Call initDatabase() first.");
853
862
  }
863
+ if (process.env.EXE_IS_DAEMON === "1") {
864
+ return _resilientClient;
865
+ }
866
+ if (_daemonClient && _daemonClient._isDaemonActive()) {
867
+ return _daemonClient;
868
+ }
854
869
  return _resilientClient;
855
870
  }
856
871
  function getRawClient() {
@@ -1339,6 +1354,12 @@ async function ensureSchema() {
1339
1354
  } catch {
1340
1355
  }
1341
1356
  }
1357
+ try {
1358
+ await client.execute(
1359
+ `CREATE INDEX IF NOT EXISTS idx_memories_content_hash ON memories(content_hash, agent_id)`
1360
+ );
1361
+ } catch {
1362
+ }
1342
1363
  await client.executeMultiple(`
1343
1364
  CREATE TABLE IF NOT EXISTS entities (
1344
1365
  id TEXT PRIMARY KEY,
@@ -1391,7 +1412,30 @@ async function ensureSchema() {
1391
1412
  entity_id TEXT NOT NULL,
1392
1413
  PRIMARY KEY (hyperedge_id, entity_id)
1393
1414
  );
1415
+
1416
+ CREATE VIRTUAL TABLE IF NOT EXISTS entities_fts USING fts5(
1417
+ name,
1418
+ content=entities,
1419
+ content_rowid=rowid
1420
+ );
1421
+
1422
+ CREATE TRIGGER IF NOT EXISTS entities_fts_ai AFTER INSERT ON entities BEGIN
1423
+ INSERT INTO entities_fts(rowid, name) VALUES (new.rowid, new.name);
1424
+ END;
1425
+
1426
+ CREATE TRIGGER IF NOT EXISTS entities_fts_ad AFTER DELETE ON entities BEGIN
1427
+ INSERT INTO entities_fts(entities_fts, rowid, name) VALUES('delete', old.rowid, old.name);
1428
+ END;
1429
+
1430
+ CREATE TRIGGER IF NOT EXISTS entities_fts_au AFTER UPDATE ON entities BEGIN
1431
+ INSERT INTO entities_fts(entities_fts, rowid, name) VALUES('delete', old.rowid, old.name);
1432
+ INSERT INTO entities_fts(rowid, name) VALUES (new.rowid, new.name);
1433
+ END;
1394
1434
  `);
1435
+ try {
1436
+ await client.execute("INSERT INTO entities_fts(entities_fts) VALUES('rebuild')");
1437
+ } catch {
1438
+ }
1395
1439
  await client.executeMultiple(`
1396
1440
  CREATE TABLE IF NOT EXISTS entity_aliases (
1397
1441
  alias TEXT NOT NULL PRIMARY KEY,
@@ -1572,6 +1616,33 @@ async function ensureSchema() {
1572
1616
  CREATE INDEX IF NOT EXISTS idx_conversations_channel
1573
1617
  ON conversations(channel_id);
1574
1618
  `);
1619
+ await client.executeMultiple(`
1620
+ CREATE TABLE IF NOT EXISTS session_agent_map (
1621
+ session_uuid TEXT PRIMARY KEY,
1622
+ agent_id TEXT NOT NULL,
1623
+ session_name TEXT,
1624
+ task_id TEXT,
1625
+ project_name TEXT,
1626
+ started_at TEXT NOT NULL
1627
+ );
1628
+
1629
+ CREATE INDEX IF NOT EXISTS idx_session_agent_map_agent
1630
+ ON session_agent_map(agent_id);
1631
+ `);
1632
+ try {
1633
+ const mapCount = await client.execute({ sql: `SELECT COUNT(*) as cnt FROM session_agent_map`, args: [] });
1634
+ if (Number(mapCount.rows[0]?.cnt ?? 0) === 0) {
1635
+ await client.execute({
1636
+ sql: `INSERT OR IGNORE INTO session_agent_map (session_uuid, agent_id, session_name, started_at)
1637
+ SELECT session_id, agent_id, '', MIN(timestamp)
1638
+ FROM memories
1639
+ WHERE session_id IS NOT NULL AND session_id != '' AND agent_id IS NOT NULL AND agent_id != ''
1640
+ GROUP BY session_id, agent_id`,
1641
+ args: []
1642
+ });
1643
+ }
1644
+ } catch {
1645
+ }
1575
1646
  try {
1576
1647
  await client.execute({
1577
1648
  sql: `ALTER TABLE tasks ADD COLUMN budget_tokens INTEGER`,
@@ -1705,15 +1776,41 @@ async function ensureSchema() {
1705
1776
  });
1706
1777
  } catch {
1707
1778
  }
1779
+ for (const col of [
1780
+ "ALTER TABLE memories ADD COLUMN intent TEXT",
1781
+ "ALTER TABLE memories ADD COLUMN outcome TEXT",
1782
+ "ALTER TABLE memories ADD COLUMN domain TEXT",
1783
+ "ALTER TABLE memories ADD COLUMN referenced_entities TEXT",
1784
+ "ALTER TABLE memories ADD COLUMN retrieval_count INTEGER DEFAULT 0",
1785
+ "ALTER TABLE memories ADD COLUMN chain_position TEXT",
1786
+ "ALTER TABLE memories ADD COLUMN review_status TEXT",
1787
+ "ALTER TABLE memories ADD COLUMN context_window_pct INTEGER",
1788
+ "ALTER TABLE memories ADD COLUMN file_paths TEXT",
1789
+ "ALTER TABLE memories ADD COLUMN commit_hash TEXT",
1790
+ "ALTER TABLE memories ADD COLUMN duration_ms INTEGER",
1791
+ "ALTER TABLE memories ADD COLUMN token_cost REAL",
1792
+ "ALTER TABLE memories ADD COLUMN audience TEXT",
1793
+ "ALTER TABLE memories ADD COLUMN language_type TEXT",
1794
+ "ALTER TABLE memories ADD COLUMN parent_memory_id TEXT"
1795
+ ]) {
1796
+ try {
1797
+ await client.execute(col);
1798
+ } catch {
1799
+ }
1800
+ }
1708
1801
  }
1709
1802
  async function disposeDatabase() {
1803
+ if (_daemonClient) {
1804
+ _daemonClient.close();
1805
+ _daemonClient = null;
1806
+ }
1710
1807
  if (_client) {
1711
1808
  _client.close();
1712
1809
  _client = null;
1713
1810
  _resilientClient = null;
1714
1811
  }
1715
1812
  }
1716
- var _client, _resilientClient, initTurso, disposeTurso;
1813
+ var _client, _resilientClient, _daemonClient, initTurso, disposeTurso;
1717
1814
  var init_database = __esm({
1718
1815
  "src/lib/database.ts"() {
1719
1816
  "use strict";
@@ -1721,6 +1818,7 @@ var init_database = __esm({
1721
1818
  init_employees();
1722
1819
  _client = null;
1723
1820
  _resilientClient = null;
1821
+ _daemonClient = null;
1724
1822
  initTurso = initDatabase;
1725
1823
  disposeTurso = disposeDatabase;
1726
1824
  }
@@ -1755,10 +1853,12 @@ function handleData(chunk) {
1755
1853
  if (!line) continue;
1756
1854
  try {
1757
1855
  const response = JSON.parse(line);
1758
- const entry = _pending.get(response.id);
1856
+ const id = response.id;
1857
+ if (!id) continue;
1858
+ const entry = _pending.get(id);
1759
1859
  if (entry) {
1760
1860
  clearTimeout(entry.timer);
1761
- _pending.delete(response.id);
1861
+ _pending.delete(id);
1762
1862
  entry.resolve(response);
1763
1863
  }
1764
1864
  } catch {
@@ -1929,6 +2029,9 @@ async function connectEmbedDaemon() {
1929
2029
  return false;
1930
2030
  }
1931
2031
  function sendRequest(texts, priority) {
2032
+ return sendDaemonRequest({ texts, priority });
2033
+ }
2034
+ function sendDaemonRequest(payload, timeoutMs = REQUEST_TIMEOUT_MS) {
1932
2035
  return new Promise((resolve) => {
1933
2036
  if (!_socket || !_connected) {
1934
2037
  resolve({ error: "Not connected" });
@@ -1938,10 +2041,10 @@ function sendRequest(texts, priority) {
1938
2041
  const timer = setTimeout(() => {
1939
2042
  _pending.delete(id);
1940
2043
  resolve({ error: "Request timeout" });
1941
- }, REQUEST_TIMEOUT_MS);
2044
+ }, timeoutMs);
1942
2045
  _pending.set(id, { resolve, timer });
1943
2046
  try {
1944
- _socket.write(JSON.stringify({ id, texts, priority }) + "\n");
2047
+ _socket.write(JSON.stringify({ id, ...payload }) + "\n");
1945
2048
  } catch {
1946
2049
  clearTimeout(timer);
1947
2050
  _pending.delete(id);
@@ -1951,30 +2054,11 @@ function sendRequest(texts, priority) {
1951
2054
  }
1952
2055
  async function pingDaemon() {
1953
2056
  if (!_socket || !_connected) return null;
1954
- return new Promise((resolve) => {
1955
- const id = randomUUID();
1956
- const timer = setTimeout(() => {
1957
- _pending.delete(id);
1958
- resolve(null);
1959
- }, 5e3);
1960
- _pending.set(id, {
1961
- resolve: (data) => {
1962
- if (data.health) {
1963
- resolve(data.health);
1964
- } else {
1965
- resolve(null);
1966
- }
1967
- },
1968
- timer
1969
- });
1970
- try {
1971
- _socket.write(JSON.stringify({ id, type: "health" }) + "\n");
1972
- } catch {
1973
- clearTimeout(timer);
1974
- _pending.delete(id);
1975
- resolve(null);
1976
- }
1977
- });
2057
+ const response = await sendDaemonRequest({ type: "health" }, 5e3);
2058
+ if (response.health) {
2059
+ return response.health;
2060
+ }
2061
+ return null;
1978
2062
  }
1979
2063
  function killAndRespawnDaemon() {
1980
2064
  process.stderr.write("[exed-client] Killing daemon for restart...\n");
@@ -2179,12 +2263,20 @@ async function getMasterKey() {
2179
2263
  }
2180
2264
  const keyPath = getKeyPath();
2181
2265
  if (!existsSync4(keyPath)) {
2266
+ process.stderr.write(
2267
+ `[keychain] Key not found at ${keyPath} (HOME=${os3.homedir()}, EXE_OS_DIR=${process.env.EXE_OS_DIR ?? "unset"})
2268
+ `
2269
+ );
2182
2270
  return null;
2183
2271
  }
2184
2272
  try {
2185
2273
  const content = await readFile3(keyPath, "utf-8");
2186
2274
  return Buffer.from(content.trim(), "base64");
2187
- } catch {
2275
+ } catch (err) {
2276
+ process.stderr.write(
2277
+ `[keychain] Key read failed at ${keyPath}: ${err instanceof Error ? err.message : String(err)}
2278
+ `
2279
+ );
2188
2280
  return null;
2189
2281
  }
2190
2282
  }
@@ -2640,6 +2732,7 @@ __export(store_exports, {
2640
2732
  vectorToBlob: () => vectorToBlob,
2641
2733
  writeMemory: () => writeMemory
2642
2734
  });
2735
+ import { createHash } from "crypto";
2643
2736
  function isBusyError2(err) {
2644
2737
  if (err instanceof Error) {
2645
2738
  const msg = err.message.toLowerCase();
@@ -2713,12 +2806,52 @@ function classifyTier(record) {
2713
2806
  if (["store_memory", "manual"].includes(record.tool_name ?? "") && (record.importance ?? 0) >= 5) return 2;
2714
2807
  return 3;
2715
2808
  }
2809
+ function inferFilePaths(record) {
2810
+ if (!["Read", "Write", "Edit"].includes(record.tool_name)) return null;
2811
+ const firstLine = record.raw_text.split("\n")[0] ?? "";
2812
+ const match = firstLine.match(/(\/[\w./-]+\.\w+)/);
2813
+ return match ? JSON.stringify([match[1]]) : null;
2814
+ }
2815
+ function inferCommitHash(record) {
2816
+ if (record.tool_name !== "Bash") return null;
2817
+ const match = record.raw_text.match(/\b([a-f0-9]{7,40})\b/);
2818
+ return match ? match[1] : null;
2819
+ }
2820
+ function inferLanguageType(record) {
2821
+ const text = record.raw_text;
2822
+ if (!text || text.length < 10) return null;
2823
+ const trimmed = text.trimStart();
2824
+ if (trimmed.startsWith("{") || trimmed.startsWith("[")) return "json";
2825
+ if (/\b(SELECT|INSERT|UPDATE|DELETE|CREATE TABLE|ALTER TABLE)\b/i.test(text)) return "sql";
2826
+ if (/\b(function |const |import |export |class |def |async |=>)\b/.test(text)) return "code";
2827
+ if (trimmed.startsWith("#") || trimmed.startsWith("*")) return "prose";
2828
+ return "mixed";
2829
+ }
2830
+ function inferDomain(record) {
2831
+ const proj = (record.project_name ?? "").toLowerCase();
2832
+ if (proj.includes("marketing") || proj.includes("content")) return "marketing";
2833
+ if (proj.includes("crm") || proj.includes("customer")) return "customer";
2834
+ return null;
2835
+ }
2716
2836
  async function writeMemory(record) {
2717
2837
  if (record.vector !== null && record.vector.length !== EMBEDDING_DIM) {
2718
2838
  throw new Error(
2719
2839
  `Expected ${EMBEDDING_DIM}-dim vector, got ${record.vector.length}`
2720
2840
  );
2721
2841
  }
2842
+ const contentHash = createHash("md5").update(record.raw_text).digest("hex");
2843
+ if (_pendingRecords.some((r) => r.content_hash === contentHash && r.agent_id === record.agent_id)) {
2844
+ return;
2845
+ }
2846
+ try {
2847
+ const client = getClient();
2848
+ const existing = await client.execute({
2849
+ sql: "SELECT id FROM memories WHERE content_hash = ? AND agent_id = ? LIMIT 1",
2850
+ args: [contentHash, record.agent_id]
2851
+ });
2852
+ if (existing.rows.length > 0) return;
2853
+ } catch {
2854
+ }
2722
2855
  const dbRow = {
2723
2856
  id: record.id,
2724
2857
  agent_id: record.agent_id,
@@ -2748,7 +2881,23 @@ async function writeMemory(record) {
2748
2881
  supersedes_id: record.supersedes_id ?? null,
2749
2882
  draft: record.draft ? 1 : 0,
2750
2883
  memory_type: record.memory_type ?? "raw",
2751
- trajectory: record.trajectory ? JSON.stringify(record.trajectory) : null
2884
+ trajectory: record.trajectory ? JSON.stringify(record.trajectory) : null,
2885
+ content_hash: contentHash,
2886
+ intent: record.intent ?? null,
2887
+ outcome: record.outcome ?? null,
2888
+ domain: record.domain ?? inferDomain(record),
2889
+ referenced_entities: record.referenced_entities ?? null,
2890
+ retrieval_count: record.retrieval_count ?? 0,
2891
+ chain_position: record.chain_position ?? null,
2892
+ review_status: record.review_status ?? null,
2893
+ context_window_pct: record.context_window_pct ?? null,
2894
+ file_paths: record.file_paths ?? inferFilePaths(record),
2895
+ commit_hash: record.commit_hash ?? inferCommitHash(record),
2896
+ duration_ms: record.duration_ms ?? null,
2897
+ token_cost: record.token_cost ?? null,
2898
+ audience: record.audience ?? null,
2899
+ language_type: record.language_type ?? inferLanguageType(record),
2900
+ parent_memory_id: record.parent_memory_id ?? null
2752
2901
  };
2753
2902
  _pendingRecords.push(dbRow);
2754
2903
  orgBus.emit({
@@ -2806,80 +2955,85 @@ async function flushBatch() {
2806
2955
  const draft = row.draft ? 1 : 0;
2807
2956
  const memoryType = row.memory_type ?? "raw";
2808
2957
  const trajectory = row.trajectory ?? null;
2809
- return {
2810
- sql: hasVector ? `INSERT OR IGNORE INTO memories
2811
- (id, agent_id, agent_role, session_id, timestamp,
2958
+ const contentHash = row.content_hash ?? null;
2959
+ const intent = row.intent ?? null;
2960
+ const outcome = row.outcome ?? null;
2961
+ const domain = row.domain ?? null;
2962
+ const referencedEntities = row.referenced_entities ?? null;
2963
+ const retrievalCount = row.retrieval_count ?? 0;
2964
+ const chainPosition = row.chain_position ?? null;
2965
+ const reviewStatus = row.review_status ?? null;
2966
+ const contextWindowPct = row.context_window_pct ?? null;
2967
+ const filePaths = row.file_paths ?? null;
2968
+ const commitHash = row.commit_hash ?? null;
2969
+ const durationMs = row.duration_ms ?? null;
2970
+ const tokenCost = row.token_cost ?? null;
2971
+ const audience = row.audience ?? null;
2972
+ const languageType = row.language_type ?? null;
2973
+ const parentMemoryId = row.parent_memory_id ?? null;
2974
+ const cols = `id, agent_id, agent_role, session_id, timestamp,
2812
2975
  tool_name, project_name,
2813
2976
  has_error, raw_text, vector, version, task_id, importance, status,
2814
2977
  confidence, last_accessed,
2815
2978
  workspace_id, document_id, user_id, char_offset, page_number,
2816
- source_path, source_type, tier, supersedes_id, draft, memory_type, trajectory)
2817
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, vector32(?), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` : `INSERT OR IGNORE INTO memories
2818
- (id, agent_id, agent_role, session_id, timestamp,
2819
- tool_name, project_name,
2820
- has_error, raw_text, vector, version, task_id, importance, status,
2821
- confidence, last_accessed,
2822
- workspace_id, document_id, user_id, char_offset, page_number,
2823
- source_path, source_type, tier, supersedes_id, draft, memory_type, trajectory)
2824
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
2825
- args: hasVector ? [
2826
- row.id,
2827
- row.agent_id,
2828
- row.agent_role,
2829
- row.session_id,
2830
- row.timestamp,
2831
- row.tool_name,
2832
- row.project_name,
2833
- row.has_error,
2834
- row.raw_text,
2835
- vectorToBlob(row.vector),
2836
- row.version,
2837
- taskId,
2838
- importance,
2839
- status,
2840
- confidence,
2841
- lastAccessed,
2842
- workspaceId,
2843
- documentId,
2844
- userId,
2845
- charOffset,
2846
- pageNumber,
2847
- sourcePath,
2848
- sourceType,
2849
- tier,
2850
- supersedesId,
2851
- draft,
2852
- memoryType,
2853
- trajectory
2854
- ] : [
2855
- row.id,
2856
- row.agent_id,
2857
- row.agent_role,
2858
- row.session_id,
2859
- row.timestamp,
2860
- row.tool_name,
2861
- row.project_name,
2862
- row.has_error,
2863
- row.raw_text,
2864
- row.version,
2865
- taskId,
2866
- importance,
2867
- status,
2868
- confidence,
2869
- lastAccessed,
2870
- workspaceId,
2871
- documentId,
2872
- userId,
2873
- charOffset,
2874
- pageNumber,
2875
- sourcePath,
2876
- sourceType,
2877
- tier,
2878
- supersedesId,
2879
- draft,
2880
- memoryType,
2881
- trajectory
2882
- ]
2979
+ source_path, source_type, tier, supersedes_id, draft, memory_type, trajectory, content_hash,
2980
+ intent, outcome, domain, referenced_entities, retrieval_count,
2981
+ chain_position, review_status, context_window_pct, file_paths, commit_hash,
2982
+ duration_ms, token_cost, audience, language_type, parent_memory_id`;
2983
+ const metaArgs = [
2984
+ intent,
2985
+ outcome,
2986
+ domain,
2987
+ referencedEntities,
2988
+ retrievalCount,
2989
+ chainPosition,
2990
+ reviewStatus,
2991
+ contextWindowPct,
2992
+ filePaths,
2993
+ commitHash,
2994
+ durationMs,
2995
+ tokenCost,
2996
+ audience,
2997
+ languageType,
2998
+ parentMemoryId
2999
+ ];
3000
+ const baseArgs = [
3001
+ row.id,
3002
+ row.agent_id,
3003
+ row.agent_role,
3004
+ row.session_id,
3005
+ row.timestamp,
3006
+ row.tool_name,
3007
+ row.project_name,
3008
+ row.has_error,
3009
+ row.raw_text
3010
+ ];
3011
+ const sharedArgs = [
3012
+ row.version,
3013
+ taskId,
3014
+ importance,
3015
+ status,
3016
+ confidence,
3017
+ lastAccessed,
3018
+ workspaceId,
3019
+ documentId,
3020
+ userId,
3021
+ charOffset,
3022
+ pageNumber,
3023
+ sourcePath,
3024
+ sourceType,
3025
+ tier,
3026
+ supersedesId,
3027
+ draft,
3028
+ memoryType,
3029
+ trajectory,
3030
+ contentHash
3031
+ ];
3032
+ return {
3033
+ sql: hasVector ? `INSERT OR IGNORE INTO memories (${cols})
3034
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, vector32(?), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` : `INSERT OR IGNORE INTO memories (${cols})
3035
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
3036
+ args: hasVector ? [...baseArgs, vectorToBlob(row.vector), ...sharedArgs, ...metaArgs] : [...baseArgs, ...sharedArgs, ...metaArgs]
2883
3037
  };
2884
3038
  };
2885
3039
  const globalClient = getClient();
@@ -3266,7 +3420,7 @@ var LOCAL_WIKI_URL, REQUEST_TIMEOUT_MS2;
3266
3420
  var init_wiki_client = __esm({
3267
3421
  "src/lib/wiki-client.ts"() {
3268
3422
  "use strict";
3269
- LOCAL_WIKI_URL = "http://localhost:3001";
3423
+ LOCAL_WIKI_URL = process.env.EXE_WIKI_URL || "http://localhost:3001";
3270
3424
  REQUEST_TIMEOUT_MS2 = 8e3;
3271
3425
  }
3272
3426
  });
@@ -4574,17 +4728,29 @@ MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEeHztAMOpR/ZMh+rWuOASjEZ54CGY
4574
4728
  // src/gateway/adapters/whatsapp.ts
4575
4729
  var whatsapp_exports = {};
4576
4730
  __export(whatsapp_exports, {
4577
- WhatsAppAdapter: () => WhatsAppAdapter
4731
+ WhatsAppAdapter: () => WhatsAppAdapter,
4732
+ calculateBackoff: () => calculateBackoff
4578
4733
  });
4579
4734
  import { randomUUID as randomUUID4 } from "crypto";
4580
4735
  import { homedir } from "os";
4581
4736
  import { join } from "path";
4582
4737
  import { mkdirSync as mkdirSync3 } from "fs";
4583
- var RECONNECT_DELAY_MS, AUTH_DIR, WhatsAppAdapter;
4738
+ function calculateBackoff(retryCount) {
4739
+ const base = Math.min(
4740
+ INITIAL_BACKOFF_MS * BACKOFF_MULTIPLIER ** retryCount,
4741
+ MAX_BACKOFF_MS
4742
+ );
4743
+ const jitter = base * JITTER_FACTOR * (2 * Math.random() - 1);
4744
+ return Math.max(INITIAL_BACKOFF_MS, Math.round(base + jitter));
4745
+ }
4746
+ var INITIAL_BACKOFF_MS, MAX_BACKOFF_MS, BACKOFF_MULTIPLIER, JITTER_FACTOR, AUTH_DIR, WhatsAppAdapter;
4584
4747
  var init_whatsapp = __esm({
4585
4748
  "src/gateway/adapters/whatsapp.ts"() {
4586
4749
  "use strict";
4587
- RECONNECT_DELAY_MS = 5e3;
4750
+ INITIAL_BACKOFF_MS = 1e3;
4751
+ MAX_BACKOFF_MS = 3e5;
4752
+ BACKOFF_MULTIPLIER = 2;
4753
+ JITTER_FACTOR = 0.25;
4588
4754
  AUTH_DIR = join(homedir(), ".exe-os", "whatsapp-auth");
4589
4755
  WhatsAppAdapter = class {
4590
4756
  platform = "whatsapp";
@@ -4593,6 +4759,9 @@ var init_whatsapp = __esm({
4593
4759
  connected = false;
4594
4760
  abortController = null;
4595
4761
  authDir = AUTH_DIR;
4762
+ // Resilience state
4763
+ retryCount = 0;
4764
+ disconnectedAt = 0;
4596
4765
  async connect(config2) {
4597
4766
  this.authDir = config2.credentials.authDir ?? AUTH_DIR;
4598
4767
  mkdirSync3(this.authDir, { recursive: true });
@@ -4601,6 +4770,20 @@ var init_whatsapp = __esm({
4601
4770
  const { state, saveCreds } = await useMultiFileAuthState(this.authDir);
4602
4771
  const { version } = await fetchLatestBaileysVersion();
4603
4772
  this.abortController = new AbortController();
4773
+ let agent;
4774
+ const socksProxy = config2.credentials.socksProxy;
4775
+ if (socksProxy) {
4776
+ try {
4777
+ const modName = "socks-proxy-agent";
4778
+ const mod = await import(modName);
4779
+ const SocksProxyAgent = mod.SocksProxyAgent ?? mod.default;
4780
+ agent = new SocksProxyAgent(socksProxy);
4781
+ console.log(`[whatsapp] Using SOCKS proxy: ${socksProxy.replace(/\/\/.*@/, "//***@")}`);
4782
+ } catch {
4783
+ console.error("[whatsapp] socks-proxy-agent not installed \u2014 run: npm i socks-proxy-agent");
4784
+ throw new Error("SOCKS proxy configured but socks-proxy-agent package not installed");
4785
+ }
4786
+ }
4604
4787
  const sock = makeWASocket({
4605
4788
  auth: {
4606
4789
  creds: state.creds,
@@ -4610,7 +4793,8 @@ var init_whatsapp = __esm({
4610
4793
  printQRInTerminal: true,
4611
4794
  browser: ["exe-os", "cli", "1.0"],
4612
4795
  syncFullHistory: false,
4613
- markOnlineOnConnect: false
4796
+ markOnlineOnConnect: false,
4797
+ ...agent ? { agent } : {}
4614
4798
  });
4615
4799
  this.sock = sock;
4616
4800
  sock.ev.on("creds.update", saveCreds);
@@ -4618,18 +4802,32 @@ var init_whatsapp = __esm({
4618
4802
  const { connection, lastDisconnect } = update;
4619
4803
  if (connection === "close") {
4620
4804
  this.connected = false;
4805
+ if (this.disconnectedAt === 0) this.disconnectedAt = Date.now();
4621
4806
  const statusCode = lastDisconnect?.error?.output?.statusCode;
4622
4807
  const shouldReconnect = statusCode !== DisconnectReason.loggedOut;
4623
4808
  if (shouldReconnect && !this.abortController?.signal.aborted) {
4624
- console.log(`[whatsapp] Connection closed (${statusCode}), reconnecting...`);
4625
- setTimeout(() => void this.connect(config2), RECONNECT_DELAY_MS);
4809
+ const delay2 = calculateBackoff(this.retryCount);
4810
+ this.retryCount++;
4811
+ console.log(
4812
+ `[whatsapp] Connection closed (code=${statusCode}), retry #${this.retryCount} in ${(delay2 / 1e3).toFixed(1)}s` + (socksProxy ? ` (proxy: ${socksProxy.replace(/\/\/.*@/, "//***@")})` : "")
4813
+ );
4814
+ setTimeout(() => void this.connect(config2), delay2);
4626
4815
  } else {
4627
4816
  console.log("[whatsapp] Logged out \u2014 clear auth and re-scan QR");
4628
4817
  }
4629
4818
  }
4630
4819
  if (connection === "open") {
4820
+ if (this.retryCount > 0) {
4821
+ const downtimeSec = this.disconnectedAt > 0 ? ((Date.now() - this.disconnectedAt) / 1e3).toFixed(1) : "?";
4822
+ console.log(
4823
+ `[whatsapp] Reconnected after ${this.retryCount} retries (${downtimeSec}s downtime)`
4824
+ );
4825
+ } else {
4826
+ console.log("[whatsapp] Connected via Baileys (linked device)");
4827
+ }
4631
4828
  this.connected = true;
4632
- console.log("[whatsapp] Connected via Baileys (linked device)");
4829
+ this.retryCount = 0;
4830
+ this.disconnectedAt = 0;
4633
4831
  }
4634
4832
  });
4635
4833
  sock.ev.on("messages.upsert", (upsert) => {
@@ -6357,6 +6555,7 @@ var init_task_scope = __esm({
6357
6555
  // src/lib/tasks-crud.ts
6358
6556
  import crypto5 from "crypto";
6359
6557
  import path12 from "path";
6558
+ import os8 from "os";
6360
6559
  import { execSync as execSync4 } from "child_process";
6361
6560
  import { mkdir as mkdir4, writeFile as writeFile4, appendFile } from "fs/promises";
6362
6561
  import { existsSync as existsSync11, readFileSync as readFileSync10 } from "fs";
@@ -6400,6 +6599,35 @@ function extractParentFromContext(contextBody) {
6400
6599
  function slugify(title) {
6401
6600
  return title.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
6402
6601
  }
6602
+ function buildKeywordIndex() {
6603
+ const idx = /* @__PURE__ */ new Map();
6604
+ for (const [role, keywords] of Object.entries(LANE_KEYWORDS)) {
6605
+ for (const kw of keywords) {
6606
+ const existing = idx.get(kw) ?? [];
6607
+ existing.push(role);
6608
+ idx.set(kw, existing);
6609
+ }
6610
+ }
6611
+ return idx;
6612
+ }
6613
+ function checkLaneAffinity(title, context, assigneeName) {
6614
+ const employees = loadEmployeesSync();
6615
+ const employee = employees.find((e) => e.name === assigneeName);
6616
+ if (!employee) return void 0;
6617
+ const assigneeRole = employee.role;
6618
+ const text = `${title} ${context}`.toLowerCase();
6619
+ const matchedRoles = /* @__PURE__ */ new Set();
6620
+ for (const [keyword, roles] of KEYWORD_INDEX) {
6621
+ if (text.includes(keyword)) {
6622
+ for (const role of roles) matchedRoles.add(role);
6623
+ }
6624
+ }
6625
+ if (matchedRoles.size === 0) return void 0;
6626
+ if (matchedRoles.has(assigneeRole)) return void 0;
6627
+ if (assigneeRole === "COO") return void 0;
6628
+ const expectedRoles = Array.from(matchedRoles).join(" or ");
6629
+ return `\u26A0\uFE0F Lane mismatch: task content suggests ${expectedRoles}, but assigned to ${assigneeName} (${assigneeRole}).`;
6630
+ }
6403
6631
  async function resolveTask(client, identifier, scopeSession) {
6404
6632
  const scope = sessionScopeFilter(scopeSession);
6405
6633
  let result = await client.execute({
@@ -6449,7 +6677,14 @@ async function createTaskCore(input) {
6449
6677
  const id = crypto5.randomUUID();
6450
6678
  const now = (/* @__PURE__ */ new Date()).toISOString();
6451
6679
  const slug = slugify(input.title);
6452
- const taskFile = input.taskFile ?? `exe/${input.assignedTo}/${slug}.md`;
6680
+ let earlySessionScope = null;
6681
+ try {
6682
+ const { resolveExeSession: resolveExeSession2 } = await Promise.resolve().then(() => (init_tmux_routing(), tmux_routing_exports));
6683
+ earlySessionScope = resolveExeSession2();
6684
+ } catch {
6685
+ }
6686
+ const scope = earlySessionScope ?? "default";
6687
+ const taskFile = input.taskFile ?? `tasks/${scope}/${input.assignedTo}/${slug}.md`;
6453
6688
  let blockedById = null;
6454
6689
  const initialStatus = input.blockedBy ? "blocked" : "open";
6455
6690
  if (input.blockedBy) {
@@ -6489,6 +6724,13 @@ async function createTaskCore(input) {
6489
6724
  if (dupCheck.rows.length > 0) {
6490
6725
  warning = `similar active task already exists (${String(dupCheck.rows[0].id)}). Created new task anyway.`;
6491
6726
  }
6727
+ if (!process.env.DISABLE_LANE_AFFINITY) {
6728
+ const laneWarning = checkLaneAffinity(input.title, input.context, input.assignedTo);
6729
+ if (laneWarning) {
6730
+ warning = warning ? `${warning}
6731
+ ${laneWarning}` : laneWarning;
6732
+ }
6733
+ }
6492
6734
  if (input.baseDir) {
6493
6735
  try {
6494
6736
  await mkdir4(path12.join(input.baseDir, "exe", "output"), { recursive: true });
@@ -6499,12 +6741,7 @@ async function createTaskCore(input) {
6499
6741
  }
6500
6742
  }
6501
6743
  const complexity = input.complexity ?? "standard";
6502
- let sessionScope = null;
6503
- try {
6504
- const { resolveExeSession: resolveExeSession2 } = await Promise.resolve().then(() => (init_tmux_routing(), tmux_routing_exports));
6505
- sessionScope = resolveExeSession2();
6506
- } catch {
6507
- }
6744
+ const sessionScope = earlySessionScope;
6508
6745
  await client.execute({
6509
6746
  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)
6510
6747
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
@@ -6531,6 +6768,39 @@ async function createTaskCore(input) {
6531
6768
  now
6532
6769
  ]
6533
6770
  });
6771
+ if (input.baseDir) {
6772
+ try {
6773
+ const EXE_OS_DIR = path12.join(os8.homedir(), ".exe-os");
6774
+ const mdPath = path12.join(EXE_OS_DIR, taskFile);
6775
+ const mdDir = path12.dirname(mdPath);
6776
+ if (!existsSync11(mdDir)) await mkdir4(mdDir, { recursive: true });
6777
+ const reviewer = input.reviewer ?? input.assignedBy;
6778
+ const mdContent = `# ${input.title}
6779
+
6780
+ **ID:** ${id}
6781
+ **Status:** ${initialStatus}
6782
+ **Priority:** ${input.priority}
6783
+ **Assigned by:** ${input.assignedBy}
6784
+ **Assigned to:** ${input.assignedTo}
6785
+ **Project:** ${input.projectName}
6786
+ **Created:** ${now.split("T")[0]}${parentTaskId ? `
6787
+ **Parent task:** ${parentTaskId}` : ""}
6788
+ **Reviewer:** ${reviewer}
6789
+
6790
+ ## Context
6791
+
6792
+ ${input.context}
6793
+
6794
+ ## MANDATORY: When done
6795
+
6796
+ You MUST call update_task with status "done" and a result summary when finished.
6797
+ If you skip this, your reviewer will not know you're done and your work won't be reviewed.
6798
+ Do NOT let a failed commit or any error prevent you from calling update_task(done).
6799
+ `;
6800
+ await writeFile4(mdPath, mdContent, "utf-8");
6801
+ } catch {
6802
+ }
6803
+ }
6534
6804
  return {
6535
6805
  id,
6536
6806
  title: input.title,
@@ -6723,7 +6993,7 @@ ${input.result}` : `\u26A0\uFE0F ${warning}`;
6723
6993
  return { row, taskFile, now, taskId };
6724
6994
  }
6725
6995
  }
6726
- if (curStatus === "in_progress" && input.callerAgentId && (input.callerAgentId === assignedBy || input.callerAgentId === "exe")) {
6996
+ if (curStatus === "in_progress" && input.callerAgentId && (input.callerAgentId === assignedBy || isCoordinatorName(input.callerAgentId))) {
6727
6997
  process.stderr.write(
6728
6998
  `[tasks] Assigner override: ${input.callerAgentId} reclaiming ${taskId}
6729
6999
  `
@@ -6835,12 +7105,22 @@ async function ensureGitignoreExe(baseDir) {
6835
7105
  } catch {
6836
7106
  }
6837
7107
  }
6838
- var DELEGATION_KEYWORDS, TASK_ALREADY_CLAIMED_PREFIX;
7108
+ var LANE_KEYWORDS, KEYWORD_INDEX, DELEGATION_KEYWORDS, TASK_ALREADY_CLAIMED_PREFIX;
6839
7109
  var init_tasks_crud = __esm({
6840
7110
  "src/lib/tasks-crud.ts"() {
6841
7111
  "use strict";
6842
7112
  init_database();
6843
7113
  init_task_scope();
7114
+ init_employees();
7115
+ LANE_KEYWORDS = {
7116
+ CMO: ["sales", "script", "pitch", "offer", "copy", "objection", "brand", "content", "seo", "marketing", "newsletter", "carousel", "social", "campaign"],
7117
+ CTO: ["spec", "architecture", "migration", "schema", "database", "design doc", "adr", "security audit", "tech stack"],
7118
+ "Principal Engineer": ["implement", "build", "fix", "commit", "refactor", "bug", "feature", "wire", "integration"],
7119
+ "Staff Code Reviewer": ["critique", "verdict", "review", "audit", "code quality"],
7120
+ "Content Production Specialist": ["render", "video", "image", "b-roll", "remotion", "animation", "thumbnail"],
7121
+ "AI Product Lead": ["competitive", "analysis", "benchmark", "compare", "scout", "evaluate", "poc"]
7122
+ };
7123
+ KEYWORD_INDEX = buildKeywordIndex();
6844
7124
  DELEGATION_KEYWORDS = /parallel|delegate|wave|worktree|multi-instance/i;
6845
7125
  TASK_ALREADY_CLAIMED_PREFIX = "TASK_ALREADY_CLAIMED";
6846
7126
  }
@@ -6870,7 +7150,7 @@ async function countNewPendingReviewsSince(sinceIso, sessionScope) {
6870
7150
  const result2 = await client.execute({
6871
7151
  sql: `SELECT COUNT(*) as cnt FROM tasks
6872
7152
  WHERE status = 'needs_review' AND updated_at > ?
6873
- AND (session_scope = ? OR session_scope IS NULL)`,
7153
+ AND session_scope = ?`,
6874
7154
  args: [sinceIso, sessionScope]
6875
7155
  });
6876
7156
  return Number(result2.rows[0]?.cnt) || 0;
@@ -6888,7 +7168,7 @@ async function listPendingReviews(limit, sessionScope) {
6888
7168
  const result2 = await client.execute({
6889
7169
  sql: `SELECT title, assigned_to, project_name FROM tasks
6890
7170
  WHERE status = 'needs_review'
6891
- AND (session_scope = ? OR session_scope IS NULL)
7171
+ AND session_scope = ?
6892
7172
  ORDER BY priority ASC, created_at DESC LIMIT ?`,
6893
7173
  args: [sessionScope, limit]
6894
7174
  });
@@ -7009,14 +7289,14 @@ async function cleanupReviewFile(row, taskFile, _baseDir) {
7009
7289
  if (parts.length >= 3 && parts[0] === "review") {
7010
7290
  const agent = parts[1];
7011
7291
  const slug = parts.slice(2).join("-");
7012
- const originalTaskFile = `exe/${agent}/${slug}.md`;
7292
+ const legacyTaskFile = `exe/${agent}/${slug}.md`;
7013
7293
  const result = await client.execute({
7014
- sql: "UPDATE tasks SET status = 'done', updated_at = ? WHERE task_file = ? AND status = 'needs_review'",
7015
- args: [now, originalTaskFile]
7294
+ sql: "UPDATE tasks SET status = 'done', updated_at = ? WHERE (task_file = ? OR task_file LIKE ?) AND status = 'needs_review'",
7295
+ args: [now, legacyTaskFile, `tasks/%/${agent}/${slug}.md`]
7016
7296
  });
7017
7297
  if (result.rowsAffected > 0) {
7018
7298
  process.stderr.write(
7019
- `[review-cleanup] Cascaded original task to done (legacy path): ${originalTaskFile}
7299
+ `[review-cleanup] Cascaded original task to done: ${agent}/${slug}.md
7020
7300
  `
7021
7301
  );
7022
7302
  }
@@ -7198,7 +7478,7 @@ function findSessionForProject(projectName) {
7198
7478
  const sessions = listSessions();
7199
7479
  for (const s of sessions) {
7200
7480
  const proj = s.projectDir.split("/").filter(Boolean).pop();
7201
- if (proj === projectName && (s.agentId === "exe" || isCoordinatorName(s.agentId))) return s;
7481
+ if (proj === projectName && isCoordinatorName(s.agentId)) return s;
7202
7482
  }
7203
7483
  return null;
7204
7484
  }
@@ -7244,7 +7524,7 @@ var init_session_scope = __esm({
7244
7524
 
7245
7525
  // src/lib/tasks-notify.ts
7246
7526
  async function dispatchTaskToEmployee(input) {
7247
- if (input.assignedTo === "exe" || isCoordinatorName(input.assignedTo)) return { dispatched: "skipped" };
7527
+ if (isCoordinatorName(input.assignedTo)) return { dispatched: "skipped" };
7248
7528
  let crossProject = false;
7249
7529
  if (input.projectName) {
7250
7530
  try {
@@ -7723,7 +8003,7 @@ async function updateTask(input) {
7723
8003
  }
7724
8004
  const isTerminal = input.status === "done" || input.status === "needs_review";
7725
8005
  if (isTerminal) {
7726
- const isCoordinator = String(row.assigned_to) === "exe" || isCoordinatorName(String(row.assigned_to));
8006
+ const isCoordinator = isCoordinatorName(String(row.assigned_to));
7727
8007
  if (!isCoordinator) {
7728
8008
  notifyTaskDone();
7729
8009
  }
@@ -7748,7 +8028,7 @@ async function updateTask(input) {
7748
8028
  }
7749
8029
  }
7750
8030
  }
7751
- if (input.status === "done" && String(row.assigned_to) !== "exe" && !isCoordinatorName(String(row.assigned_to)) && !process.env.VITEST) {
8031
+ if (input.status === "done" && !isCoordinatorName(String(row.assigned_to)) && !process.env.VITEST) {
7752
8032
  Promise.resolve().then(() => (init_skill_learning(), skill_learning_exports)).then(
7753
8033
  ({ captureAndLearn: captureAndLearn2 }) => captureAndLearn2({
7754
8034
  taskId,
@@ -7764,7 +8044,7 @@ async function updateTask(input) {
7764
8044
  });
7765
8045
  }
7766
8046
  let nextTask;
7767
- if (isTerminal && String(row.assigned_to) !== "exe" && !isCoordinatorName(String(row.assigned_to))) {
8047
+ if (isTerminal && !isCoordinatorName(String(row.assigned_to))) {
7768
8048
  try {
7769
8049
  nextTask = await findNextTask(String(row.assigned_to));
7770
8050
  } catch {
@@ -8132,7 +8412,7 @@ __export(tmux_routing_exports, {
8132
8412
  import { execFileSync as execFileSync2, execSync as execSync6 } from "child_process";
8133
8413
  import { readFileSync as readFileSync11, writeFileSync as writeFileSync6, mkdirSync as mkdirSync7, existsSync as existsSync13, appendFileSync } from "fs";
8134
8414
  import path17 from "path";
8135
- import os8 from "os";
8415
+ import os9 from "os";
8136
8416
  import { fileURLToPath as fileURLToPath2 } from "url";
8137
8417
  import { unlinkSync as unlinkSync6 } from "fs";
8138
8418
  function spawnLockPath(sessionName) {
@@ -8456,7 +8736,7 @@ function notifyParentExe(sessionKey) {
8456
8736
  return true;
8457
8737
  }
8458
8738
  function ensureEmployee(employeeName, exeSession, projectDir, opts) {
8459
- if (employeeName === "exe" || isCoordinatorName(employeeName)) {
8739
+ if (isCoordinatorName(employeeName)) {
8460
8740
  return { status: "failed", sessionName: "", error: "The COO is not a dispatchable employee" };
8461
8741
  }
8462
8742
  try {
@@ -8528,7 +8808,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
8528
8808
  const transport = getTransport();
8529
8809
  const sessionName = employeeSessionName(employeeName, exeSession, opts?.instance);
8530
8810
  const instanceLabel = opts?.instance != null && opts.instance > 0 ? `${employeeName}${opts.instance}` : employeeName;
8531
- const logDir = path17.join(os8.homedir(), ".exe-os", "session-logs");
8811
+ const logDir = path17.join(os9.homedir(), ".exe-os", "session-logs");
8532
8812
  const logFile = path17.join(logDir, `${instanceLabel}-${Date.now()}.log`);
8533
8813
  if (!existsSync13(logDir)) {
8534
8814
  mkdirSync7(logDir, { recursive: true });
@@ -8544,7 +8824,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
8544
8824
  } catch {
8545
8825
  }
8546
8826
  try {
8547
- const claudeJsonPath = path17.join(os8.homedir(), ".claude.json");
8827
+ const claudeJsonPath = path17.join(os9.homedir(), ".claude.json");
8548
8828
  let claudeJson = {};
8549
8829
  try {
8550
8830
  claudeJson = JSON.parse(readFileSync11(claudeJsonPath, "utf8"));
@@ -8559,7 +8839,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
8559
8839
  } catch {
8560
8840
  }
8561
8841
  try {
8562
- const settingsDir = path17.join(os8.homedir(), ".claude", "projects");
8842
+ const settingsDir = path17.join(os9.homedir(), ".claude", "projects");
8563
8843
  const normalizedKey = (opts?.cwd ?? projectDir).replace(/\//g, "-").replace(/^-/, "");
8564
8844
  const projSettingsDir = path17.join(settingsDir, normalizedKey);
8565
8845
  const settingsPath = path17.join(projSettingsDir, "settings.json");
@@ -8607,7 +8887,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
8607
8887
  let legacyFallbackWarned = false;
8608
8888
  if (!useExeAgent && !useBinSymlink) {
8609
8889
  const identityPath = path17.join(
8610
- os8.homedir(),
8890
+ os9.homedir(),
8611
8891
  ".exe-os",
8612
8892
  "identity",
8613
8893
  `${employeeName}.md`
@@ -8637,7 +8917,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
8637
8917
  }
8638
8918
  let sessionContextFlag = "";
8639
8919
  try {
8640
- const ctxDir = path17.join(os8.homedir(), ".exe-os", "session-cache");
8920
+ const ctxDir = path17.join(os9.homedir(), ".exe-os", "session-cache");
8641
8921
  mkdirSync7(ctxDir, { recursive: true });
8642
8922
  const ctxFile = path17.join(ctxDir, `session-context-${sessionName}.md`);
8643
8923
  const ctxContent = [
@@ -8749,13 +9029,13 @@ var init_tmux_routing = __esm({
8749
9029
  init_intercom_queue();
8750
9030
  init_plan_limits();
8751
9031
  init_employees();
8752
- SPAWN_LOCK_DIR = path17.join(os8.homedir(), ".exe-os", "spawn-locks");
8753
- SESSION_CACHE = path17.join(os8.homedir(), ".exe-os", "session-cache");
9032
+ SPAWN_LOCK_DIR = path17.join(os9.homedir(), ".exe-os", "spawn-locks");
9033
+ SESSION_CACHE = path17.join(os9.homedir(), ".exe-os", "session-cache");
8754
9034
  BEHAVIORS_EXPORT_TIMEOUT_MS = 1e4;
8755
9035
  VALID_SESSION_NAME = /^[a-z]+\d*-[a-zA-Z0-9_]+$/;
8756
9036
  VERIFY_PANE_LINES = 200;
8757
9037
  INTERCOM_DEBOUNCE_MS = 3e4;
8758
- INTERCOM_LOG2 = path17.join(os8.homedir(), ".exe-os", "intercom.log");
9038
+ INTERCOM_LOG2 = path17.join(os9.homedir(), ".exe-os", "intercom.log");
8759
9039
  DEBOUNCE_FILE = path17.join(SESSION_CACHE, "intercom-debounce.json");
8760
9040
  DEBOUNCE_CLEANUP_AGE_MS = 5 * 60 * 1e3;
8761
9041
  BUSY_PATTERN = /[✻✽✶✳·].*…|Running…/;
@@ -9017,7 +9297,7 @@ var init_messaging = __esm({
9017
9297
  import { readFileSync as readFileSync12, writeFileSync as writeFileSync7, existsSync as existsSync14, mkdirSync as mkdirSync8 } from "fs";
9018
9298
  import { randomUUID as randomUUID8 } from "crypto";
9019
9299
  import path18 from "path";
9020
- import os9 from "os";
9300
+ import os10 from "os";
9021
9301
  function substituteTemplate(template, record) {
9022
9302
  return template.replace(
9023
9303
  /\{\{(\w+(?:\.\w+)*)\}\}/g,
@@ -9312,7 +9592,7 @@ var TRIGGERS_PATH, GRAPH_API_VERSION;
9312
9592
  var init_trigger_engine = __esm({
9313
9593
  "src/automation/trigger-engine.ts"() {
9314
9594
  "use strict";
9315
- TRIGGERS_PATH = path18.join(os9.homedir(), ".exe-os", "triggers.json");
9595
+ TRIGGERS_PATH = path18.join(os10.homedir(), ".exe-os", "triggers.json");
9316
9596
  GRAPH_API_VERSION = "v21.0";
9317
9597
  }
9318
9598
  });
@@ -9377,13 +9657,13 @@ var init_crm_webhook = __esm({
9377
9657
  // src/bin/exe-gateway.ts
9378
9658
  import { existsSync as existsSync15, readFileSync as readFileSync13 } from "fs";
9379
9659
  import path19 from "path";
9380
- import os10 from "os";
9660
+ import os11 from "os";
9381
9661
 
9382
9662
  // src/gateway/webhook-server.ts
9383
9663
  import {
9384
9664
  createServer
9385
9665
  } from "http";
9386
- var DEFAULT_HOST = "127.0.0.1";
9666
+ var DEFAULT_HOST = process.env.EXE_WEBHOOK_HOST || "127.0.0.1";
9387
9667
  var BODY_SIZE_LIMIT = 1048576;
9388
9668
  var WebhookServer = class {
9389
9669
  constructor(config2) {
@@ -10069,10 +10349,49 @@ function buildPermissionContext(platform, permissions) {
10069
10349
  return `[${platform.toUpperCase()} \u2014 allowed: ${parts.join(", ")}]`;
10070
10350
  }
10071
10351
 
10352
+ // src/gateway/bot-errors.ts
10353
+ var FatalBotError = class extends Error {
10354
+ constructor(message, cause) {
10355
+ super(message);
10356
+ this.cause = cause;
10357
+ this.name = "FatalBotError";
10358
+ }
10359
+ fatal = true;
10360
+ };
10361
+ var RecoverableBotError = class extends Error {
10362
+ constructor(message, toolName, cause) {
10363
+ super(message);
10364
+ this.toolName = toolName;
10365
+ this.cause = cause;
10366
+ this.name = "RecoverableBotError";
10367
+ }
10368
+ recoverable = true;
10369
+ };
10370
+ var MaxStepsError = class extends Error {
10371
+ constructor(stepsTaken, maxSteps) {
10372
+ super(
10373
+ `Reached maximum steps (${stepsTaken}/${maxSteps}). Returning partial result.`
10374
+ );
10375
+ this.stepsTaken = stepsTaken;
10376
+ this.maxSteps = maxSteps;
10377
+ this.name = "MaxStepsError";
10378
+ }
10379
+ };
10380
+ function classifyError(err, toolName) {
10381
+ if (err instanceof FatalBotError) return err;
10382
+ if (err instanceof RecoverableBotError) return err;
10383
+ const message = err instanceof Error ? err.message : String(err);
10384
+ if (message.includes("401") || message.includes("403") || message.includes("authentication") || message.includes("rate_limit")) {
10385
+ return new FatalBotError(message, err);
10386
+ }
10387
+ return new RecoverableBotError(message, toolName, err);
10388
+ }
10389
+
10072
10390
  // src/gateway/bot-runtime.ts
10073
10391
  var DEFAULT_MODEL = "claude-sonnet-4-20250514";
10074
10392
  var MAX_TURNS = 10;
10075
10393
  var MAX_HISTORY = 50;
10394
+ var DEFAULT_PLANNING_INTERVAL = 3;
10076
10395
  function filterToolsForPermissions(tools, permissions) {
10077
10396
  const allAllowed = /* @__PURE__ */ new Set();
10078
10397
  if (permissions.canRead) {
@@ -10101,7 +10420,7 @@ var BotRuntime = class {
10101
10420
  async processMessage(msg, permissions) {
10102
10421
  const sessionKey = msg.chatType === "group" ? msg.channelId : msg.senderId;
10103
10422
  const history = this.getHistory(sessionKey);
10104
- history.push({ role: "user", content: msg.text });
10423
+ history.push({ role: "user", content: msg.text, frameType: "task" });
10105
10424
  const systemPrompt = this.config.systemPrompt + "\n\n" + buildPermissionContext(msg.platform, permissions);
10106
10425
  const allowedTools = filterToolsForPermissions(
10107
10426
  this.config.tools,
@@ -10109,29 +10428,54 @@ var BotRuntime = class {
10109
10428
  );
10110
10429
  const model = this.config.model ?? DEFAULT_MODEL;
10111
10430
  const maxTurns = this.config.maxTurns ?? MAX_TURNS;
10431
+ const planningInterval = this.config.planningInterval ?? DEFAULT_PLANNING_INTERVAL;
10112
10432
  let turns = 0;
10113
10433
  while (turns < maxTurns) {
10114
10434
  turns++;
10115
- const response = await this.client.messages.create({
10116
- model,
10117
- max_tokens: 4096,
10118
- system: systemPrompt,
10119
- messages: history.map((m) => ({
10120
- role: m.role,
10121
- content: m.content
10122
- })),
10123
- tools: allowedTools.map((t) => ({
10124
- name: t.name,
10125
- description: t.description,
10126
- input_schema: t.input_schema
10127
- }))
10128
- });
10435
+ if (planningInterval > 0 && turns > 1 && turns % planningInterval === 1 && maxTurns - turns >= 2) {
10436
+ history.push({
10437
+ role: "user",
10438
+ content: "[Planning checkpoint] Review what you know so far. What facts have you gathered? What is your plan for the remaining steps? Be concise.",
10439
+ frameType: "planning"
10440
+ });
10441
+ }
10442
+ let response;
10443
+ try {
10444
+ response = await this.client.messages.create({
10445
+ model,
10446
+ max_tokens: 4096,
10447
+ system: systemPrompt,
10448
+ messages: history.map((m) => ({
10449
+ role: m.role,
10450
+ content: m.content
10451
+ })),
10452
+ tools: allowedTools.map((t) => ({
10453
+ name: t.name,
10454
+ description: t.description,
10455
+ input_schema: t.input_schema
10456
+ }))
10457
+ });
10458
+ } catch (err) {
10459
+ const classified = classifyError(err);
10460
+ if (classified instanceof FatalBotError) {
10461
+ const errorMsg = `Bot error: ${classified.message}`;
10462
+ history.push({ role: "assistant", content: errorMsg, frameType: "error" });
10463
+ this.trimHistory(sessionKey);
10464
+ return errorMsg;
10465
+ }
10466
+ history.push({
10467
+ role: "assistant",
10468
+ content: `[API error \u2014 retrying] ${classified.message}`,
10469
+ frameType: "error"
10470
+ });
10471
+ continue;
10472
+ }
10129
10473
  const toolUseBlocks = response.content.filter(
10130
10474
  (b) => b.type === "tool_use"
10131
10475
  );
10132
10476
  if (toolUseBlocks.length === 0) {
10133
10477
  const textContent = response.content.filter((b) => b.type === "text").map((b) => b.text).join("\n");
10134
- history.push({ role: "assistant", content: textContent });
10478
+ history.push({ role: "assistant", content: textContent, frameType: "assistant" });
10135
10479
  this.trimHistory(sessionKey);
10136
10480
  return textContent;
10137
10481
  }
@@ -10141,9 +10485,11 @@ var BotRuntime = class {
10141
10485
  );
10142
10486
  history.push({
10143
10487
  role: "assistant",
10144
- content: response.content
10488
+ content: response.content,
10489
+ frameType: "assistant"
10145
10490
  });
10146
10491
  const toolResults = [];
10492
+ let hadFatalError = false;
10147
10493
  for (const block of allowed) {
10148
10494
  try {
10149
10495
  const result = await this.config.toolExecutor(
@@ -10156,12 +10502,23 @@ var BotRuntime = class {
10156
10502
  content: result
10157
10503
  });
10158
10504
  } catch (err) {
10159
- toolResults.push({
10160
- type: "tool_result",
10161
- tool_use_id: block.id,
10162
- content: `Error: ${err instanceof Error ? err.message : String(err)}`,
10163
- is_error: true
10164
- });
10505
+ const classified = classifyError(err, block.name);
10506
+ if (classified instanceof FatalBotError) {
10507
+ toolResults.push({
10508
+ type: "tool_result",
10509
+ tool_use_id: block.id,
10510
+ content: `Fatal error: ${classified.message}`,
10511
+ is_error: true
10512
+ });
10513
+ hadFatalError = true;
10514
+ } else {
10515
+ toolResults.push({
10516
+ type: "tool_result",
10517
+ tool_use_id: block.id,
10518
+ content: `Error (recoverable): ${classified.message}`,
10519
+ is_error: true
10520
+ });
10521
+ }
10165
10522
  }
10166
10523
  }
10167
10524
  for (const { block, check } of blocked) {
@@ -10174,10 +10531,22 @@ var BotRuntime = class {
10174
10531
  }
10175
10532
  history.push({
10176
10533
  role: "user",
10177
- content: toolResults
10534
+ content: toolResults,
10535
+ frameType: hadFatalError ? "error" : "tool_result"
10178
10536
  });
10537
+ if (hadFatalError) {
10538
+ this.trimHistory(sessionKey);
10539
+ return `A fatal error occurred during tool execution. The bot loop has been stopped.`;
10540
+ }
10179
10541
  }
10180
- return "I reached the maximum number of tool calls for this request. Please try again with a more specific question.";
10542
+ const maxErr = new MaxStepsError(turns, maxTurns);
10543
+ history.push({
10544
+ role: "assistant",
10545
+ content: maxErr.message,
10546
+ frameType: "error"
10547
+ });
10548
+ this.trimHistory(sessionKey);
10549
+ return maxErr.message;
10181
10550
  }
10182
10551
  getHistory(sessionKey) {
10183
10552
  if (!this.conversations.has(sessionKey)) {
@@ -10187,9 +10556,19 @@ var BotRuntime = class {
10187
10556
  }
10188
10557
  trimHistory(sessionKey) {
10189
10558
  const history = this.conversations.get(sessionKey);
10190
- if (history && history.length > MAX_HISTORY) {
10191
- this.conversations.set(sessionKey, history.slice(-MAX_HISTORY));
10559
+ if (!history || history.length <= MAX_HISTORY) return;
10560
+ const firstTaskIdx = history.findIndex((m) => m.frameType === "task");
10561
+ let trimmed = history.filter(
10562
+ (m, i) => m.frameType !== "planning" || i >= history.length - MAX_HISTORY
10563
+ );
10564
+ if (trimmed.length > MAX_HISTORY) {
10565
+ const tail = trimmed.slice(-MAX_HISTORY);
10566
+ if (firstTaskIdx >= 0 && !tail.includes(history[firstTaskIdx])) {
10567
+ tail[0] = history[firstTaskIdx];
10568
+ }
10569
+ trimmed = tail;
10192
10570
  }
10571
+ this.conversations.set(sessionKey, trimmed);
10193
10572
  }
10194
10573
  /** Clear conversation history for a session */
10195
10574
  clearHistory(sessionKey) {
@@ -10244,7 +10623,7 @@ var BotRegistry = class {
10244
10623
 
10245
10624
  // src/bin/exe-gateway.ts
10246
10625
  init_employees();
10247
- var CONFIG_DIR = path19.join(os10.homedir(), ".exe-os");
10626
+ var CONFIG_DIR = path19.join(os11.homedir(), ".exe-os");
10248
10627
  var CONFIG_PATH3 = path19.join(CONFIG_DIR, "gateway.json");
10249
10628
  var DEFAULT_PORT = 3100;
10250
10629
  function loadConfig2() {