@askexenow/exe-os 0.8.0 → 0.8.1

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 (90) hide show
  1. package/README.md +178 -79
  2. package/dist/bin/backfill-responses.js +160 -8
  3. package/dist/bin/backfill-vectors.js +130 -1
  4. package/dist/bin/cleanup-stale-review-tasks.js +130 -1
  5. package/dist/bin/cli.js +10111 -7540
  6. package/dist/bin/exe-agent.js +159 -1
  7. package/dist/bin/exe-assign.js +235 -16
  8. package/dist/bin/exe-boot.js +344 -472
  9. package/dist/bin/exe-call.js +145 -1
  10. package/dist/bin/exe-cloud.js +11 -0
  11. package/dist/bin/exe-dispatch.js +37 -24
  12. package/dist/bin/exe-doctor.js +130 -1
  13. package/dist/bin/exe-export-behaviors.js +150 -7
  14. package/dist/bin/exe-forget.js +822 -665
  15. package/dist/bin/exe-gateway.js +470 -62
  16. package/dist/bin/exe-heartbeat.js +133 -2
  17. package/dist/bin/exe-kill.js +150 -7
  18. package/dist/bin/exe-launch-agent.js +150 -7
  19. package/dist/bin/exe-new-employee.js +756 -224
  20. package/dist/bin/exe-pending-messages.js +132 -2
  21. package/dist/bin/exe-pending-notifications.js +130 -1
  22. package/dist/bin/exe-pending-reviews.js +132 -2
  23. package/dist/bin/exe-review.js +160 -8
  24. package/dist/bin/exe-search.js +2473 -2008
  25. package/dist/bin/exe-session-cleanup.js +238 -51
  26. package/dist/bin/exe-settings.js +11 -0
  27. package/dist/bin/exe-status.js +130 -1
  28. package/dist/bin/exe-team.js +130 -1
  29. package/dist/bin/git-sweep.js +272 -16
  30. package/dist/bin/graph-backfill.js +150 -7
  31. package/dist/bin/graph-export.js +150 -7
  32. package/dist/bin/install.js +5 -0
  33. package/dist/bin/scan-tasks.js +238 -19
  34. package/dist/bin/setup.js +1776 -10
  35. package/dist/bin/shard-migrate.js +150 -7
  36. package/dist/bin/update.js +9 -6
  37. package/dist/bin/wiki-sync.js +150 -7
  38. package/dist/gateway/index.js +470 -62
  39. package/dist/hooks/bug-report-worker.js +195 -35
  40. package/dist/hooks/commit-complete.js +272 -16
  41. package/dist/hooks/error-recall.js +2313 -1847
  42. package/dist/hooks/exe-heartbeat-hook.js +5 -0
  43. package/dist/hooks/ingest-worker.js +330 -58
  44. package/dist/hooks/ingest.js +11 -0
  45. package/dist/hooks/instructions-loaded.js +199 -10
  46. package/dist/hooks/notification.js +199 -10
  47. package/dist/hooks/post-compact.js +199 -10
  48. package/dist/hooks/pre-compact.js +199 -10
  49. package/dist/hooks/pre-tool-use.js +199 -10
  50. package/dist/hooks/prompt-ingest-worker.js +179 -14
  51. package/dist/hooks/prompt-submit.js +781 -285
  52. package/dist/hooks/response-ingest-worker.js +1900 -1405
  53. package/dist/hooks/session-end.js +456 -12
  54. package/dist/hooks/session-start.js +2188 -1724
  55. package/dist/hooks/stop.js +200 -10
  56. package/dist/hooks/subagent-stop.js +199 -10
  57. package/dist/hooks/summary-worker.js +604 -334
  58. package/dist/index.js +554 -61
  59. package/dist/lib/cloud-sync.js +5 -0
  60. package/dist/lib/config.js +13 -0
  61. package/dist/lib/consolidation.js +5 -0
  62. package/dist/lib/database.js +104 -0
  63. package/dist/lib/device-registry.js +109 -0
  64. package/dist/lib/embedder.js +13 -0
  65. package/dist/lib/employee-templates.js +53 -26
  66. package/dist/lib/employees.js +5 -0
  67. package/dist/lib/exe-daemon-client.js +5 -0
  68. package/dist/lib/exe-daemon.js +493 -79
  69. package/dist/lib/file-grep.js +20 -4
  70. package/dist/lib/hybrid-search.js +1435 -190
  71. package/dist/lib/identity-templates.js +126 -5
  72. package/dist/lib/identity.js +5 -0
  73. package/dist/lib/license.js +5 -0
  74. package/dist/lib/messaging.js +37 -24
  75. package/dist/lib/schedules.js +130 -1
  76. package/dist/lib/skill-learning.js +11 -0
  77. package/dist/lib/status-brief.js +5 -0
  78. package/dist/lib/store.js +199 -10
  79. package/dist/lib/task-router.js +72 -6
  80. package/dist/lib/tasks.js +179 -50
  81. package/dist/lib/tmux-routing.js +179 -46
  82. package/dist/mcp/server.js +2129 -1855
  83. package/dist/mcp/tools/create-task.js +86 -36
  84. package/dist/mcp/tools/deactivate-behavior.js +5 -0
  85. package/dist/mcp/tools/list-tasks.js +39 -11
  86. package/dist/mcp/tools/send-message.js +37 -24
  87. package/dist/mcp/tools/update-task.js +153 -38
  88. package/dist/runtime/index.js +451 -59
  89. package/dist/tui/App.js +454 -59
  90. package/package.json +1 -1
@@ -106,6 +106,11 @@ function normalizeSessionLifecycle(raw) {
106
106
  const userSL = raw.sessionLifecycle ?? {};
107
107
  raw.sessionLifecycle = { ...defaultSL, ...userSL };
108
108
  }
109
+ function normalizeAutoUpdate(raw) {
110
+ const defaultAU = DEFAULT_CONFIG.autoUpdate;
111
+ const userAU = raw.autoUpdate ?? {};
112
+ raw.autoUpdate = { ...defaultAU, ...userAU };
113
+ }
109
114
  async function loadConfig() {
110
115
  const dir = process.env.EXE_OS_DIR ?? process.env.EXE_MEM_DIR ?? EXE_AI_DIR;
111
116
  await mkdir(dir, { recursive: true });
@@ -128,6 +133,7 @@ async function loadConfig() {
128
133
  }
129
134
  normalizeScalingRoadmap(migratedCfg);
130
135
  normalizeSessionLifecycle(migratedCfg);
136
+ normalizeAutoUpdate(migratedCfg);
131
137
  const config = { ...DEFAULT_CONFIG, dbPath: path.join(dir, "memories.db"), ...migratedCfg };
132
138
  if (config.dbPath.startsWith("~")) {
133
139
  config.dbPath = config.dbPath.replace(/^~/, os.homedir());
@@ -150,6 +156,7 @@ function loadConfigSync() {
150
156
  const { config: migratedCfg } = migrateConfig(parsed);
151
157
  normalizeScalingRoadmap(migratedCfg);
152
158
  normalizeSessionLifecycle(migratedCfg);
159
+ normalizeAutoUpdate(migratedCfg);
153
160
  return { ...DEFAULT_CONFIG, dbPath: path.join(dir, "memories.db"), ...migratedCfg };
154
161
  } catch {
155
162
  return { ...DEFAULT_CONFIG, dbPath: path.join(dir, "memories.db") };
@@ -169,6 +176,7 @@ async function loadConfigFrom(configPath) {
169
176
  const { config: migratedCfg } = migrateConfig(parsed);
170
177
  normalizeScalingRoadmap(migratedCfg);
171
178
  normalizeSessionLifecycle(migratedCfg);
179
+ normalizeAutoUpdate(migratedCfg);
172
180
  return { ...DEFAULT_CONFIG, ...migratedCfg };
173
181
  } catch {
174
182
  return { ...DEFAULT_CONFIG };
@@ -240,6 +248,11 @@ var init_config = __esm({
240
248
  idleKillTicksRequired: 3,
241
249
  idleKillIntercomAckWindowMs: 1e4,
242
250
  maxAutoInstances: 10
251
+ },
252
+ autoUpdate: {
253
+ checkOnBoot: true,
254
+ autoInstall: false,
255
+ checkIntervalMs: 24 * 60 * 60 * 1e3
243
256
  }
244
257
  };
245
258
  CONFIG_MIGRATIONS = [
@@ -485,6 +498,27 @@ async function ensureSchema() {
485
498
  });
486
499
  } catch {
487
500
  }
501
+ try {
502
+ await client.execute({
503
+ sql: `ALTER TABLE tasks ADD COLUMN checkpoint TEXT`,
504
+ args: []
505
+ });
506
+ } catch {
507
+ }
508
+ try {
509
+ await client.execute({
510
+ sql: `ALTER TABLE tasks ADD COLUMN checkpoint_count INTEGER NOT NULL DEFAULT 0`,
511
+ args: []
512
+ });
513
+ } catch {
514
+ }
515
+ try {
516
+ await client.execute({
517
+ sql: `ALTER TABLE tasks ADD COLUMN complexity TEXT NOT NULL DEFAULT 'standard'`,
518
+ args: []
519
+ });
520
+ } catch {
521
+ }
488
522
  try {
489
523
  await client.execute({
490
524
  sql: `ALTER TABLE memories ADD COLUMN task_id TEXT`,
@@ -895,6 +929,15 @@ async function ensureSchema() {
895
929
  } catch {
896
930
  }
897
931
  }
932
+ for (const col of [
933
+ "ALTER TABLE memories ADD COLUMN source_path TEXT",
934
+ "ALTER TABLE memories ADD COLUMN source_type TEXT DEFAULT 'text'"
935
+ ]) {
936
+ try {
937
+ await client.execute(col);
938
+ } catch {
939
+ }
940
+ }
898
941
  await client.executeMultiple(`
899
942
  CREATE INDEX IF NOT EXISTS idx_memories_workspace
900
943
  ON memories(workspace_id);
@@ -959,6 +1002,34 @@ async function ensureSchema() {
959
1002
  CREATE INDEX IF NOT EXISTS idx_conversations_channel
960
1003
  ON conversations(channel_id);
961
1004
  `);
1005
+ try {
1006
+ await client.execute({
1007
+ sql: `ALTER TABLE tasks ADD COLUMN budget_tokens INTEGER`,
1008
+ args: []
1009
+ });
1010
+ } catch {
1011
+ }
1012
+ try {
1013
+ await client.execute({
1014
+ sql: `ALTER TABLE tasks ADD COLUMN budget_fallback_model TEXT`,
1015
+ args: []
1016
+ });
1017
+ } catch {
1018
+ }
1019
+ try {
1020
+ await client.execute({
1021
+ sql: `ALTER TABLE tasks ADD COLUMN tokens_used INTEGER DEFAULT 0`,
1022
+ args: []
1023
+ });
1024
+ } catch {
1025
+ }
1026
+ try {
1027
+ await client.execute({
1028
+ sql: `ALTER TABLE tasks ADD COLUMN tokens_warned_at INTEGER`,
1029
+ args: []
1030
+ });
1031
+ } catch {
1032
+ }
962
1033
  await client.executeMultiple(`
963
1034
  CREATE VIRTUAL TABLE IF NOT EXISTS conversations_fts USING fts5(
964
1035
  content_text,
@@ -985,6 +1056,52 @@ async function ensureSchema() {
985
1056
  VALUES (new.rowid, new.content_text, new.sender_name, new.agent_response);
986
1057
  END;
987
1058
  `);
1059
+ try {
1060
+ await client.execute({
1061
+ sql: `ALTER TABLE memories ADD COLUMN tier INTEGER DEFAULT 3`,
1062
+ args: []
1063
+ });
1064
+ } catch {
1065
+ }
1066
+ try {
1067
+ await client.execute(
1068
+ `CREATE INDEX IF NOT EXISTS idx_memories_tier ON memories(tier)`
1069
+ );
1070
+ } catch {
1071
+ }
1072
+ try {
1073
+ await client.execute({
1074
+ sql: `UPDATE memories SET tier = 1 WHERE tool_name = 'commit_to_long_term_memory' AND importance >= 8 AND tier = 3`,
1075
+ args: []
1076
+ });
1077
+ await client.execute({
1078
+ sql: `UPDATE memories SET tier = 2 WHERE tool_name IN ('store_memory', 'manual') AND importance >= 5 AND tier = 3`,
1079
+ args: []
1080
+ });
1081
+ } catch {
1082
+ }
1083
+ try {
1084
+ await client.execute({
1085
+ sql: `ALTER TABLE memories ADD COLUMN supersedes_id TEXT`,
1086
+ args: []
1087
+ });
1088
+ } catch {
1089
+ }
1090
+ try {
1091
+ await client.execute(
1092
+ `CREATE INDEX IF NOT EXISTS idx_memories_supersedes ON memories(supersedes_id) WHERE supersedes_id IS NOT NULL`
1093
+ );
1094
+ } catch {
1095
+ }
1096
+ for (const col of [
1097
+ "ALTER TABLE tasks ADD COLUMN checkpoint TEXT",
1098
+ "ALTER TABLE tasks ADD COLUMN checkpoint_count INTEGER DEFAULT 0"
1099
+ ]) {
1100
+ try {
1101
+ await client.execute(col);
1102
+ } catch {
1103
+ }
1104
+ }
988
1105
  }
989
1106
  async function disposeDatabase() {
990
1107
  if (_client) {
@@ -1266,13 +1383,27 @@ async function ensureShardSchema(client) {
1266
1383
  "ALTER TABLE memories ADD COLUMN document_id TEXT",
1267
1384
  "ALTER TABLE memories ADD COLUMN user_id TEXT",
1268
1385
  "ALTER TABLE memories ADD COLUMN char_offset INTEGER",
1269
- "ALTER TABLE memories ADD COLUMN page_number INTEGER"
1386
+ "ALTER TABLE memories ADD COLUMN page_number INTEGER",
1387
+ // Source provenance columns (must match database.ts)
1388
+ "ALTER TABLE memories ADD COLUMN source_path TEXT",
1389
+ "ALTER TABLE memories ADD COLUMN source_type TEXT DEFAULT 'text'",
1390
+ "ALTER TABLE memories ADD COLUMN tier INTEGER DEFAULT 3",
1391
+ "ALTER TABLE memories ADD COLUMN supersedes_id TEXT"
1270
1392
  ]) {
1271
1393
  try {
1272
1394
  await client.execute(col);
1273
1395
  } catch {
1274
1396
  }
1275
1397
  }
1398
+ for (const idx of [
1399
+ "CREATE INDEX IF NOT EXISTS idx_memories_tier ON memories(tier)",
1400
+ "CREATE INDEX IF NOT EXISTS idx_memories_supersedes ON memories(supersedes_id) WHERE supersedes_id IS NOT NULL"
1401
+ ]) {
1402
+ try {
1403
+ await client.execute(idx);
1404
+ } catch {
1405
+ }
1406
+ }
1276
1407
  try {
1277
1408
  await client.execute("CREATE INDEX IF NOT EXISTS idx_memories_status ON memories(status)");
1278
1409
  } catch {
@@ -1381,8 +1512,11 @@ var store_exports = {};
1381
1512
  __export(store_exports, {
1382
1513
  attachDocumentMetadata: () => attachDocumentMetadata,
1383
1514
  buildWikiScopeFilter: () => buildWikiScopeFilter,
1515
+ classifyTier: () => classifyTier,
1384
1516
  disposeStore: () => disposeStore,
1385
1517
  flushBatch: () => flushBatch,
1518
+ flushTier3: () => flushTier3,
1519
+ getMemoryCardinality: () => getMemoryCardinality,
1386
1520
  initStore: () => initStore,
1387
1521
  reserveVersions: () => reserveVersions,
1388
1522
  searchMemories: () => searchMemories,
@@ -1428,6 +1562,11 @@ async function initStore(options) {
1428
1562
  const vResult = await client.execute("SELECT MAX(version) as max_v FROM memories");
1429
1563
  _nextVersion = (Number(vResult.rows[0]?.max_v) || 0) + 1;
1430
1564
  }
1565
+ function classifyTier(record) {
1566
+ if (record.tool_name === "commit_to_long_term_memory" && (record.importance ?? 0) >= 8) return 1;
1567
+ if (["store_memory", "manual"].includes(record.tool_name ?? "") && (record.importance ?? 0) >= 5) return 2;
1568
+ return 3;
1569
+ }
1431
1570
  async function writeMemory(record) {
1432
1571
  if (record.vector !== null && record.vector.length !== EMBEDDING_DIM) {
1433
1572
  throw new Error(
@@ -1455,7 +1594,11 @@ async function writeMemory(record) {
1455
1594
  document_id: record.document_id ?? null,
1456
1595
  user_id: record.user_id ?? null,
1457
1596
  char_offset: record.char_offset ?? null,
1458
- page_number: record.page_number ?? null
1597
+ page_number: record.page_number ?? null,
1598
+ source_path: record.source_path ?? null,
1599
+ source_type: record.source_type ?? null,
1600
+ tier: record.tier ?? classifyTier(record),
1601
+ supersedes_id: record.supersedes_id ?? null
1459
1602
  };
1460
1603
  _pendingRecords.push(dbRow);
1461
1604
  if (_flushTimer === null) {
@@ -1487,20 +1630,26 @@ async function flushBatch() {
1487
1630
  const userId = row.user_id ?? null;
1488
1631
  const charOffset = row.char_offset ?? null;
1489
1632
  const pageNumber = row.page_number ?? null;
1633
+ const sourcePath = row.source_path ?? null;
1634
+ const sourceType = row.source_type ?? null;
1635
+ const tier = row.tier ?? 3;
1636
+ const supersedesId = row.supersedes_id ?? null;
1490
1637
  return {
1491
1638
  sql: hasVector ? `INSERT OR IGNORE INTO memories
1492
1639
  (id, agent_id, agent_role, session_id, timestamp,
1493
1640
  tool_name, project_name,
1494
1641
  has_error, raw_text, vector, version, task_id, importance, status,
1495
1642
  confidence, last_accessed,
1496
- workspace_id, document_id, user_id, char_offset, page_number)
1497
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, vector32(?), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` : `INSERT OR IGNORE INTO memories
1643
+ workspace_id, document_id, user_id, char_offset, page_number,
1644
+ source_path, source_type, tier, supersedes_id)
1645
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, vector32(?), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` : `INSERT OR IGNORE INTO memories
1498
1646
  (id, agent_id, agent_role, session_id, timestamp,
1499
1647
  tool_name, project_name,
1500
1648
  has_error, raw_text, vector, version, task_id, importance, status,
1501
1649
  confidence, last_accessed,
1502
- workspace_id, document_id, user_id, char_offset, page_number)
1503
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
1650
+ workspace_id, document_id, user_id, char_offset, page_number,
1651
+ source_path, source_type, tier, supersedes_id)
1652
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
1504
1653
  args: hasVector ? [
1505
1654
  row.id,
1506
1655
  row.agent_id,
@@ -1522,7 +1671,11 @@ async function flushBatch() {
1522
1671
  documentId,
1523
1672
  userId,
1524
1673
  charOffset,
1525
- pageNumber
1674
+ pageNumber,
1675
+ sourcePath,
1676
+ sourceType,
1677
+ tier,
1678
+ supersedesId
1526
1679
  ] : [
1527
1680
  row.id,
1528
1681
  row.agent_id,
@@ -1543,7 +1696,11 @@ async function flushBatch() {
1543
1696
  documentId,
1544
1697
  userId,
1545
1698
  charOffset,
1546
- pageNumber
1699
+ pageNumber,
1700
+ sourcePath,
1701
+ sourceType,
1702
+ tier,
1703
+ supersedesId
1547
1704
  ]
1548
1705
  };
1549
1706
  };
@@ -1617,7 +1774,8 @@ async function searchMemories(queryVector, agentId, options) {
1617
1774
  has_error, raw_text, vector, importance, status,
1618
1775
  confidence, last_accessed,
1619
1776
  workspace_id, document_id, user_id,
1620
- char_offset, page_number
1777
+ char_offset, page_number,
1778
+ source_path, source_type
1621
1779
  FROM memories
1622
1780
  WHERE agent_id = ?
1623
1781
  AND vector IS NOT NULL${statusFilter}
@@ -1666,7 +1824,9 @@ async function searchMemories(queryVector, agentId, options) {
1666
1824
  document_id: row.document_id ?? null,
1667
1825
  user_id: row.user_id ?? null,
1668
1826
  char_offset: row.char_offset ?? null,
1669
- page_number: row.page_number ?? null
1827
+ page_number: row.page_number ?? null,
1828
+ source_path: row.source_path ?? null,
1829
+ source_type: row.source_type ?? null
1670
1830
  }));
1671
1831
  }
1672
1832
  async function attachDocumentMetadata(records) {
@@ -1704,6 +1864,25 @@ async function attachDocumentMetadata(records) {
1704
1864
  }
1705
1865
  return records;
1706
1866
  }
1867
+ async function flushTier3(agentId, options) {
1868
+ const client = getClient();
1869
+ const maxAge = options?.maxAgeHours ?? 72;
1870
+ const cutoff = new Date(Date.now() - maxAge * 36e5).toISOString();
1871
+ if (options?.dryRun) {
1872
+ const result2 = await client.execute({
1873
+ sql: `SELECT COUNT(*) as cnt FROM memories
1874
+ WHERE agent_id = ? AND tier = 3 AND status = 'active' AND timestamp < ?`,
1875
+ args: [agentId, cutoff]
1876
+ });
1877
+ return { archived: Number(result2.rows[0]?.cnt ?? 0) };
1878
+ }
1879
+ const result = await client.execute({
1880
+ sql: `UPDATE memories SET status = 'archived'
1881
+ WHERE agent_id = ? AND tier = 3 AND status = 'active' AND timestamp < ?`,
1882
+ args: [agentId, cutoff]
1883
+ });
1884
+ return { archived: result.rowsAffected };
1885
+ }
1707
1886
  async function disposeStore() {
1708
1887
  if (_flushTimer !== null) {
1709
1888
  clearInterval(_flushTimer);
@@ -1734,6 +1913,18 @@ function reserveVersions(count) {
1734
1913
  }
1735
1914
  return reserved;
1736
1915
  }
1916
+ async function getMemoryCardinality(agentId) {
1917
+ try {
1918
+ const client = getClient();
1919
+ const result = await client.execute({
1920
+ sql: `SELECT COUNT(*) as cnt FROM memories WHERE agent_id = ? AND COALESCE(status, 'active') = 'active'`,
1921
+ args: [agentId]
1922
+ });
1923
+ return Number(result.rows[0]?.cnt) || 0;
1924
+ } catch {
1925
+ return 0;
1926
+ }
1927
+ }
1737
1928
  var _pendingRecords, _batchSize, _flushIntervalMs, _flushTimer, _flushing, _nextVersion;
1738
1929
  var init_store = __esm({
1739
1930
  "src/lib/store.ts"() {
@@ -2087,6 +2278,12 @@ function drainQueue(isSessionBusy2, sendKeys) {
2087
2278
  let drained = 0;
2088
2279
  let failed = 0;
2089
2280
  for (const item of queue) {
2281
+ const age = Date.now() - new Date(item.queuedAt).getTime();
2282
+ if (age > TTL_MS) {
2283
+ logQueue(`EXPIRED \u2192 ${item.targetSession} (${Math.round(age / 6e4)}min old, reason: ${item.reason})`);
2284
+ failed++;
2285
+ continue;
2286
+ }
2090
2287
  try {
2091
2288
  if (!isSessionBusy2(item.targetSession)) {
2092
2289
  const success = sendKeys(item.targetSession);
@@ -2133,12 +2330,13 @@ function logQueue(msg) {
2133
2330
  } catch {
2134
2331
  }
2135
2332
  }
2136
- var QUEUE_PATH, MAX_RETRIES, INTERCOM_LOG;
2333
+ var QUEUE_PATH, MAX_RETRIES, TTL_MS, INTERCOM_LOG;
2137
2334
  var init_intercom_queue = __esm({
2138
2335
  "src/lib/intercom-queue.ts"() {
2139
2336
  "use strict";
2140
2337
  QUEUE_PATH = path5.join(os3.homedir(), ".exe-os", "intercom-queue.json");
2141
2338
  MAX_RETRIES = 5;
2339
+ TTL_MS = 60 * 60 * 1e3;
2142
2340
  INTERCOM_LOG = path5.join(os3.homedir(), ".exe-os", "intercom.log");
2143
2341
  }
2144
2342
  });
@@ -2289,6 +2487,17 @@ function getGitRoot(dir) {
2289
2487
  return null;
2290
2488
  }
2291
2489
  }
2490
+ function getMainRepoRoot(dir) {
2491
+ try {
2492
+ const commonDir = execSync5(
2493
+ "git rev-parse --path-format=absolute --git-common-dir",
2494
+ { cwd: dir, encoding: "utf-8", timeout: GIT_TIMEOUT_MS, stdio: ["pipe", "pipe", "pipe"] }
2495
+ ).trim();
2496
+ return realpath(path9.dirname(commonDir));
2497
+ } catch {
2498
+ return null;
2499
+ }
2500
+ }
2292
2501
  function worktreePath(repoRoot, employeeName, instance) {
2293
2502
  const label = instanceLabel(employeeName, instance);
2294
2503
  return path9.join(repoRoot, ".worktrees", label);
@@ -2550,6 +2759,36 @@ import path11 from "path";
2550
2759
  import { execSync as execSync6 } from "child_process";
2551
2760
  import { mkdir as mkdir4, writeFile as writeFile4, appendFile } from "fs/promises";
2552
2761
  import { existsSync as existsSync11, readFileSync as readFileSync8 } from "fs";
2762
+ async function writeCheckpoint(input) {
2763
+ const client = getClient();
2764
+ const row = await resolveTask(client, input.taskId);
2765
+ const taskId = String(row.id);
2766
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2767
+ const blockedByIds = [];
2768
+ if (row.blocked_by) {
2769
+ blockedByIds.push(String(row.blocked_by));
2770
+ }
2771
+ const checkpoint = {
2772
+ step: input.step,
2773
+ context_summary: input.contextSummary,
2774
+ files_touched: input.filesTouched ?? [],
2775
+ blocked_by_ids: blockedByIds,
2776
+ last_checkpoint_at: now
2777
+ };
2778
+ const result = await client.execute({
2779
+ sql: `UPDATE tasks SET checkpoint = ?, checkpoint_count = checkpoint_count + 1, updated_at = ? WHERE id = ?`,
2780
+ args: [JSON.stringify(checkpoint), now, taskId]
2781
+ });
2782
+ if (result.rowsAffected === 0) {
2783
+ throw new Error(`Checkpoint write failed: task ${taskId} not found`);
2784
+ }
2785
+ const countResult = await client.execute({
2786
+ sql: "SELECT checkpoint_count FROM tasks WHERE id = ?",
2787
+ args: [taskId]
2788
+ });
2789
+ const checkpointCount = Number(countResult.rows[0]?.checkpoint_count ?? 1);
2790
+ return { checkpointCount };
2791
+ }
2553
2792
  function extractParentFromContext(contextBody) {
2554
2793
  if (!contextBody) return null;
2555
2794
  const match = contextBody.match(
@@ -2656,9 +2895,10 @@ async function createTaskCore(input) {
2656
2895
  } catch {
2657
2896
  }
2658
2897
  }
2898
+ const complexity = input.complexity ?? "standard";
2659
2899
  await client.execute({
2660
- sql: `INSERT INTO tasks (id, title, assigned_to, assigned_by, project_name, priority, status, task_file, blocked_by, parent_task_id, reviewer, context, created_at, updated_at)
2661
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
2900
+ 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, created_at, updated_at)
2901
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
2662
2902
  args: [
2663
2903
  id,
2664
2904
  input.title,
@@ -2672,6 +2912,11 @@ async function createTaskCore(input) {
2672
2912
  parentTaskId,
2673
2913
  input.reviewer ?? null,
2674
2914
  input.context,
2915
+ input.budgetTokens ?? null,
2916
+ input.budgetFallbackModel ?? null,
2917
+ 0,
2918
+ null,
2919
+ complexity,
2675
2920
  now,
2676
2921
  now
2677
2922
  ]
@@ -2687,7 +2932,11 @@ async function createTaskCore(input) {
2687
2932
  taskFile,
2688
2933
  createdAt: now,
2689
2934
  updatedAt: now,
2690
- warning
2935
+ warning,
2936
+ budgetTokens: input.budgetTokens ?? null,
2937
+ budgetFallbackModel: input.budgetFallbackModel ?? null,
2938
+ tokensUsed: 0,
2939
+ tokensWarnedAt: null
2691
2940
  };
2692
2941
  }
2693
2942
  async function listTasks(input) {
@@ -2727,7 +2976,12 @@ async function listTasks(input) {
2727
2976
  status: String(r.status),
2728
2977
  taskFile: String(r.task_file),
2729
2978
  createdAt: String(r.created_at),
2730
- updatedAt: String(r.updated_at)
2979
+ updatedAt: String(r.updated_at),
2980
+ checkpointCount: Number(r.checkpoint_count ?? 0),
2981
+ budgetTokens: r.budget_tokens !== null ? Number(r.budget_tokens) : null,
2982
+ budgetFallbackModel: r.budget_fallback_model !== null ? String(r.budget_fallback_model) : null,
2983
+ tokensUsed: Number(r.tokens_used ?? 0),
2984
+ tokensWarnedAt: r.tokens_warned_at !== null ? Number(r.tokens_warned_at) : null
2731
2985
  }));
2732
2986
  }
2733
2987
  function checkStaleCompletion(taskContext, taskCreatedAt) {
@@ -2735,8 +2989,13 @@ function checkStaleCompletion(taskContext, taskCreatedAt) {
2735
2989
  if (!DELEGATION_KEYWORDS.test(taskContext)) return null;
2736
2990
  try {
2737
2991
  const since = new Date(taskCreatedAt).toISOString();
2992
+ const branch = execSync6(
2993
+ "git rev-parse --abbrev-ref HEAD 2>/dev/null",
2994
+ { encoding: "utf8", timeout: 3e3 }
2995
+ ).trim();
2996
+ const branchArg = branch && branch !== "HEAD" ? branch : "";
2738
2997
  const commitCount = execSync6(
2739
- `git log --oneline --since="${since}" 2>/dev/null | wc -l`,
2998
+ `git log --oneline --since="${since}" ${branchArg} 2>/dev/null | wc -l`,
2740
2999
  { encoding: "utf8", timeout: 5e3 }
2741
3000
  ).trim();
2742
3001
  const count = parseInt(commitCount, 10);
@@ -2795,6 +3054,14 @@ ${input.result}` : `\u26A0\uFE0F ${warning}`;
2795
3054
  const claimedBy = cur?.assigned_tmux ? ` (claimed by ${cur.assigned_tmux})` : "";
2796
3055
  throw new Error(`${TASK_ALREADY_CLAIMED_PREFIX}: task ${taskId} is ${status}${claimedBy}`);
2797
3056
  }
3057
+ try {
3058
+ await writeCheckpoint({
3059
+ taskId,
3060
+ step: "claimed",
3061
+ contextSummary: `Task claimed by session. Transitioning open \u2192 in_progress.`
3062
+ });
3063
+ } catch {
3064
+ }
2798
3065
  return { row, taskFile, now, taskId };
2799
3066
  }
2800
3067
  if (input.result) {
@@ -2808,6 +3075,14 @@ ${input.result}` : `\u26A0\uFE0F ${warning}`;
2808
3075
  args: [input.status, now, taskId]
2809
3076
  });
2810
3077
  }
3078
+ try {
3079
+ await writeCheckpoint({
3080
+ taskId,
3081
+ step: `status_transition:${input.status}`,
3082
+ contextSummary: input.result ? `Transitioned to ${input.status}. Result: ${input.result.slice(0, 500)}` : `Transitioned to ${input.status}.`
3083
+ });
3084
+ } catch {
3085
+ }
2811
3086
  return { row, taskFile, now, taskId };
2812
3087
  }
2813
3088
  async function deleteTaskCore(taskId, _baseDir) {
@@ -3012,6 +3287,7 @@ async function createReviewForCompletedTask(row, result, _baseDir, now) {
3012
3287
  "- **Approved:** mark this review task as done",
3013
3288
  "- **Needs work:** re-open the original task with notes, mark this review as done"
3014
3289
  ].join("\n");
3290
+ const originalTaskId = String(row.id);
3015
3291
  const reviewTask = await createTaskCore({
3016
3292
  title: `Review: ${agent} completed "${taskTitle}"`,
3017
3293
  assignedTo: reviewer,
@@ -3020,6 +3296,7 @@ async function createReviewForCompletedTask(row, result, _baseDir, now) {
3020
3296
  priority: "p1",
3021
3297
  context: reviewContext,
3022
3298
  taskFile: reviewFile,
3299
+ parentTaskId: originalTaskId,
3023
3300
  skipDispatch: true
3024
3301
  });
3025
3302
  const reviewId = reviewTask.id;
@@ -3058,23 +3335,38 @@ async function cleanupReviewFile(row, taskFile, _baseDir) {
3058
3335
  if (String(row.assigned_by) !== "system" || !taskFile.includes("review-")) return;
3059
3336
  try {
3060
3337
  const client = getClient();
3061
- const fileName = taskFile.split("/").pop() ?? "";
3062
- const reviewPrefix = fileName.replace(".md", "");
3063
- const parts = reviewPrefix.split("-");
3064
- if (parts.length >= 3 && parts[0] === "review") {
3065
- const agent = parts[1];
3066
- const slug = parts.slice(2).join("-");
3067
- const originalTaskFile = `exe/${agent}/${slug}.md`;
3338
+ const now = (/* @__PURE__ */ new Date()).toISOString();
3339
+ const parentId = row.parent_task_id ? String(row.parent_task_id) : null;
3340
+ if (parentId) {
3068
3341
  const result = await client.execute({
3069
- sql: "UPDATE tasks SET status = 'done', updated_at = ? WHERE task_file = ? AND status = 'needs_review'",
3070
- args: [(/* @__PURE__ */ new Date()).toISOString(), originalTaskFile]
3342
+ sql: "UPDATE tasks SET status = 'done', updated_at = ? WHERE id = ? AND status = 'needs_review'",
3343
+ args: [now, parentId]
3071
3344
  });
3072
3345
  if (result.rowsAffected > 0) {
3073
3346
  process.stderr.write(
3074
- `[review-cleanup] Cascaded original task to done: ${originalTaskFile}
3347
+ `[review-cleanup] Cascaded original task to done via parent_task_id: ${parentId}
3075
3348
  `
3076
3349
  );
3077
3350
  }
3351
+ } else {
3352
+ const fileName = taskFile.split("/").pop() ?? "";
3353
+ const reviewPrefix = fileName.replace(".md", "");
3354
+ const parts = reviewPrefix.split("-");
3355
+ if (parts.length >= 3 && parts[0] === "review") {
3356
+ const agent = parts[1];
3357
+ const slug = parts.slice(2).join("-");
3358
+ const originalTaskFile = `exe/${agent}/${slug}.md`;
3359
+ const result = await client.execute({
3360
+ sql: "UPDATE tasks SET status = 'done', updated_at = ? WHERE task_file = ? AND status = 'needs_review'",
3361
+ args: [now, originalTaskFile]
3362
+ });
3363
+ if (result.rowsAffected > 0) {
3364
+ process.stderr.write(
3365
+ `[review-cleanup] Cascaded original task to done (legacy path): ${originalTaskFile}
3366
+ `
3367
+ );
3368
+ }
3369
+ }
3078
3370
  }
3079
3371
  } catch (err) {
3080
3372
  process.stderr.write(
@@ -3195,12 +3487,23 @@ function getProjectName(cwd) {
3195
3487
  const dir = cwd ?? process.cwd();
3196
3488
  if (_cached2 && _cachedCwd === dir) return _cached2;
3197
3489
  try {
3198
- const repoRoot = execSync7("git rev-parse --show-toplevel", {
3199
- cwd: dir,
3200
- encoding: "utf8",
3201
- timeout: 2e3,
3202
- stdio: ["pipe", "pipe", "pipe"]
3203
- }).trim();
3490
+ let repoRoot;
3491
+ try {
3492
+ const gitCommonDir = execSync7("git rev-parse --path-format=absolute --git-common-dir", {
3493
+ cwd: dir,
3494
+ encoding: "utf8",
3495
+ timeout: 2e3,
3496
+ stdio: ["pipe", "pipe", "pipe"]
3497
+ }).trim();
3498
+ repoRoot = path14.dirname(gitCommonDir);
3499
+ } catch {
3500
+ repoRoot = execSync7("git rev-parse --show-toplevel", {
3501
+ cwd: dir,
3502
+ encoding: "utf8",
3503
+ timeout: 2e3,
3504
+ stdio: ["pipe", "pipe", "pipe"]
3505
+ }).trim();
3506
+ }
3204
3507
  _cached2 = path14.basename(repoRoot);
3205
3508
  _cachedCwd = dir;
3206
3509
  return _cached2;
@@ -3306,7 +3609,9 @@ async function dispatchTaskToEmployee(input) {
3306
3609
  return { dispatched, session: sessionName, crossProject };
3307
3610
  } else {
3308
3611
  const projectDir = input.projectDir ?? process.cwd();
3309
- const result = ensureEmployee(input.assignedTo, exeSession, projectDir);
3612
+ const result = ensureEmployee(input.assignedTo, exeSession, projectDir, {
3613
+ autoInstance: input.assignedTo === "tom" || input.assignedTo === "sasha"
3614
+ });
3310
3615
  if (result.status === "failed") {
3311
3616
  process.stderr.write(
3312
3617
  `[dispatch] Failed to spawn ${input.assignedTo}: ${result.error}
@@ -3670,7 +3975,8 @@ __export(tasks_exports, {
3670
3975
  resolveTask: () => resolveTask,
3671
3976
  slugify: () => slugify,
3672
3977
  updateTask: () => updateTask,
3673
- updateTaskStatus: () => updateTaskStatus
3978
+ updateTaskStatus: () => updateTaskStatus,
3979
+ writeCheckpoint: () => writeCheckpoint
3674
3980
  });
3675
3981
  import path15 from "path";
3676
3982
  import { writeFileSync as writeFileSync4, mkdirSync as mkdirSync6, unlinkSync as unlinkSync3 } from "fs";
@@ -3712,10 +4018,11 @@ async function updateTask(input) {
3712
4018
  try {
3713
4019
  const client = getClient();
3714
4020
  const taskTitle = String(row.title);
4021
+ const escaped = taskTitle.replace(/%/g, "\\%").replace(/_/g, "\\_");
3715
4022
  await client.execute({
3716
4023
  sql: `UPDATE tasks SET status = 'cancelled', updated_at = ?
3717
- WHERE title LIKE ? AND status IN ('open', 'in_progress')`,
3718
- args: [now, `%left%${taskTitle}%in_progress`]
4024
+ WHERE title LIKE ? ESCAPE '\\' AND status IN ('open', 'in_progress')`,
4025
+ args: [now, `%left '${escaped}' as in\\_progress%`]
3719
4026
  });
3720
4027
  } catch {
3721
4028
  }
@@ -3773,6 +4080,10 @@ async function updateTask(input) {
3773
4080
  taskFile,
3774
4081
  createdAt: String(row.created_at),
3775
4082
  updatedAt: now,
4083
+ budgetTokens: row.budget_tokens !== void 0 && row.budget_tokens !== null ? Number(row.budget_tokens) : null,
4084
+ budgetFallbackModel: row.budget_fallback_model !== void 0 && row.budget_fallback_model !== null ? String(row.budget_fallback_model) : null,
4085
+ tokensUsed: Number(row.tokens_used ?? 0),
4086
+ tokensWarnedAt: row.tokens_warned_at !== void 0 && row.tokens_warned_at !== null ? Number(row.tokens_warned_at) : null,
3776
4087
  nextTask
3777
4088
  };
3778
4089
  }
@@ -4286,6 +4597,11 @@ function getSessionState(sessionName) {
4286
4597
  if (!transport.isAlive(sessionName)) return "offline";
4287
4598
  try {
4288
4599
  const pane = transport.capturePane(sessionName, 5);
4600
+ if (!pane.includes("\u276F") && !pane.includes("Claude Code") && !BUSY_PATTERN.test(pane) && !/Running…/.test(pane)) {
4601
+ if (/\$\s*$/.test(pane) || /% $/.test(pane.trimEnd())) {
4602
+ return "no_claude";
4603
+ }
4604
+ }
4289
4605
  if (/Running…/.test(pane)) return "tool";
4290
4606
  if (BUSY_PATTERN.test(pane)) return "thinking";
4291
4607
  return "idle";
@@ -4316,7 +4632,14 @@ function sendIntercom(targetSession) {
4316
4632
  logIntercom(`SKIP \u2192 ${targetSession} (session not found)`);
4317
4633
  return "failed";
4318
4634
  }
4319
- if (isSessionBusy(targetSession)) {
4635
+ const sessionState = getSessionState(targetSession);
4636
+ if (sessionState === "no_claude") {
4637
+ queueIntercom(targetSession, "claude not running in session");
4638
+ recordDebounce(targetSession);
4639
+ logIntercom(`QUEUED \u2192 ${targetSession} (no claude process \u2014 raw shell detected)`);
4640
+ return "queued";
4641
+ }
4642
+ if (sessionState === "thinking" || sessionState === "tool") {
4320
4643
  queueIntercom(targetSession, "session busy at send time");
4321
4644
  recordDebounce(targetSession);
4322
4645
  logIntercom(`QUEUED \u2192 ${targetSession} (session busy, will retry from queue)`);
@@ -4328,18 +4651,7 @@ function sendIntercom(targetSession) {
4328
4651
  }
4329
4652
  transport.sendKeys(targetSession, "/exe-intercom");
4330
4653
  recordDebounce(targetSession);
4331
- for (let i = 0; i < INTERCOM_POLL_MAX_ATTEMPTS; i++) {
4332
- try {
4333
- execSync8(`sleep ${INTERCOM_POLL_INTERVAL_S}`);
4334
- } catch {
4335
- }
4336
- const state = getSessionState(targetSession);
4337
- if (state === "thinking" || state === "tool") {
4338
- logIntercom(`ACKNOWLEDGED \u2192 ${targetSession} (state=${state}, poll=${i + 1})`);
4339
- return "acknowledged";
4340
- }
4341
- }
4342
- logIntercom(`DELIVERED \u2192 ${targetSession} (no state transition after ${INTERCOM_POLL_MAX_ATTEMPTS}s)`);
4654
+ logIntercom(`DELIVERED \u2192 ${targetSession} (fire-and-forget)`);
4343
4655
  return "delivered";
4344
4656
  } catch {
4345
4657
  logIntercom(`FAIL \u2192 ${targetSession}`);
@@ -4356,7 +4668,17 @@ function notifyParentExe(sessionKey) {
4356
4668
  process.stderr.write(`[intercom] notifyParentExe \u2192 ${target}
4357
4669
  `);
4358
4670
  const result = sendIntercom(target);
4359
- return result !== "failed";
4671
+ if (result === "failed") {
4672
+ const rootExe = resolveExeSession();
4673
+ if (rootExe && rootExe !== target) {
4674
+ process.stderr.write(`[intercom] notifyParentExe: dispatcher ${target} dead, falling back to root exe ${rootExe}
4675
+ `);
4676
+ const fallback = sendIntercom(rootExe);
4677
+ return fallback !== "failed";
4678
+ }
4679
+ return false;
4680
+ }
4681
+ return true;
4360
4682
  }
4361
4683
  function ensureEmployee(employeeName, exeSession, projectDir, opts) {
4362
4684
  if (employeeName === "exe") {
@@ -4405,7 +4727,8 @@ function ensureEmployee(employeeName, exeSession, projectDir, opts) {
4405
4727
  return { status: "failed", sessionName, error: "intercom delivery failed" };
4406
4728
  }
4407
4729
  const spawnOpts = { ...opts, instance: effectiveInstance };
4408
- const wtPath = ensureWorktree(projectDir, employeeName, effectiveInstance);
4730
+ const mainRoot = getMainRepoRoot(projectDir) ?? projectDir;
4731
+ const wtPath = ensureWorktree(mainRoot, employeeName, effectiveInstance);
4409
4732
  if (wtPath) {
4410
4733
  spawnOpts.cwd = wtPath;
4411
4734
  }
@@ -4586,7 +4909,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
4586
4909
  let booted = false;
4587
4910
  for (let i = 0; i < 30; i++) {
4588
4911
  try {
4589
- execSync8("sleep 1");
4912
+ execSync8("sleep 0.5");
4590
4913
  } catch {
4591
4914
  }
4592
4915
  try {
@@ -4606,7 +4929,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
4606
4929
  }
4607
4930
  }
4608
4931
  if (!booted) {
4609
- return { sessionName, error: `${useExeAgent ? "exe-agent" : "claude"} did not boot within 30s` };
4932
+ return { sessionName, error: `${useExeAgent ? "exe-agent" : "claude"} did not boot within 15s` };
4610
4933
  }
4611
4934
  if (!useExeAgent) {
4612
4935
  try {
@@ -4624,7 +4947,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
4624
4947
  });
4625
4948
  return { sessionName };
4626
4949
  }
4627
- var SESSION_CACHE, BEHAVIORS_EXPORT_TIMEOUT_MS, VERIFY_PANE_LINES, INTERCOM_DEBOUNCE_MS, INTERCOM_LOG2, DEBOUNCE_FILE, DEBOUNCE_CLEANUP_AGE_MS, BUSY_PATTERN, INTERCOM_POLL_INTERVAL_S, INTERCOM_POLL_MAX_ATTEMPTS;
4950
+ var SESSION_CACHE, BEHAVIORS_EXPORT_TIMEOUT_MS, VERIFY_PANE_LINES, INTERCOM_DEBOUNCE_MS, INTERCOM_LOG2, DEBOUNCE_FILE, DEBOUNCE_CLEANUP_AGE_MS, BUSY_PATTERN;
4628
4951
  var init_tmux_routing = __esm({
4629
4952
  "src/lib/tmux-routing.ts"() {
4630
4953
  "use strict";
@@ -4645,8 +4968,6 @@ var init_tmux_routing = __esm({
4645
4968
  DEBOUNCE_FILE = path16.join(SESSION_CACHE, "intercom-debounce.json");
4646
4969
  DEBOUNCE_CLEANUP_AGE_MS = 5 * 60 * 1e3;
4647
4970
  BUSY_PATTERN = /[✻✽✶✳·].*…|Running…/;
4648
- INTERCOM_POLL_INTERVAL_S = 1;
4649
- INTERCOM_POLL_MAX_ATTEMPTS = 8;
4650
4971
  }
4651
4972
  });
4652
4973
 
@@ -4774,9 +5095,8 @@ function createRealDeps(getClient2) {
4774
5095
  AND r.title LIKE 'Review:%'
4775
5096
  AND NOT EXISTS (
4776
5097
  SELECT 1 FROM tasks t
4777
- WHERE t.assigned_to != 'exe'
5098
+ WHERE t.id = r.parent_task_id
4778
5099
  AND t.status = 'done'
4779
- AND r.title LIKE '%"' || t.title || '"%'
4780
5100
  AND t.updated_at > r.created_at
4781
5101
  )`,
4782
5102
  args: []
@@ -4797,8 +5117,7 @@ function createRealDeps(getClient2) {
4797
5117
  AND t.assigned_to != 'exe'
4798
5118
  AND NOT EXISTS (
4799
5119
  SELECT 1 FROM tasks r
4800
- WHERE r.task_file LIKE '%review-%'
4801
- AND r.title LIKE '%' || t.title || '%'
5120
+ WHERE r.parent_task_id = t.id
4802
5121
  AND r.status IN ('open', 'in_progress', 'done', 'cancelled')
4803
5122
  )`,
4804
5123
  args: []
@@ -6231,8 +6550,8 @@ async function embedDirect(text) {
6231
6550
  const llamaCpp = await import("node-llama-cpp");
6232
6551
  const { MODELS_DIR: MODELS_DIR2 } = await Promise.resolve().then(() => (init_config(), config_exports));
6233
6552
  const { existsSync: existsSync18 } = await import("fs");
6234
- const path21 = await import("path");
6235
- const modelPath = path21.join(MODELS_DIR2, "jina-embeddings-v5-small-q4_k_m.gguf");
6553
+ const path22 = await import("path");
6554
+ const modelPath = path22.join(MODELS_DIR2, "jina-embeddings-v5-small-q4_k_m.gguf");
6236
6555
  if (!existsSync18(modelPath)) {
6237
6556
  throw new Error(`Embedding model not found at ${modelPath}. Run '/exe-setup' to download it.`);
6238
6557
  }
@@ -6922,8 +7241,8 @@ __export(wiki_sync_exports, {
6922
7241
  listWorkspaces: () => listWorkspaces,
6923
7242
  syncMemories: () => syncMemories
6924
7243
  });
6925
- async function wikiRequest(config, path21, method = "GET", body) {
6926
- const url = `${config.wikiUrl}/api/v1${path21}`;
7244
+ async function wikiRequest(config, path22, method = "GET", body) {
7245
+ const url = `${config.wikiUrl}/api/v1${path22}`;
6927
7246
  const headers = {
6928
7247
  "Authorization": `Bearer ${config.wikiApiKey}`,
6929
7248
  "Content-Type": "application/json"
@@ -6934,7 +7253,7 @@ async function wikiRequest(config, path21, method = "GET", body) {
6934
7253
  body: body ? JSON.stringify(body) : void 0
6935
7254
  });
6936
7255
  if (!response.ok) {
6937
- throw new Error(`Wiki API ${method} ${path21}: ${response.status} ${response.statusText}`);
7256
+ throw new Error(`Wiki API ${method} ${path22}: ${response.status} ${response.statusText}`);
6938
7257
  }
6939
7258
  return response.json();
6940
7259
  }
@@ -7038,6 +7357,54 @@ var init_wiki_sync = __esm({
7038
7357
  }
7039
7358
  });
7040
7359
 
7360
+ // src/lib/update-check.ts
7361
+ var update_check_exports = {};
7362
+ __export(update_check_exports, {
7363
+ checkForUpdate: () => checkForUpdate,
7364
+ getLocalVersion: () => getLocalVersion,
7365
+ getRemoteVersion: () => getRemoteVersion
7366
+ });
7367
+ import { execSync as execSync12 } from "child_process";
7368
+ import { readFileSync as readFileSync12 } from "fs";
7369
+ import path19 from "path";
7370
+ function getLocalVersion(packageRoot) {
7371
+ const pkgPath = path19.join(packageRoot, "package.json");
7372
+ const pkg = JSON.parse(readFileSync12(pkgPath, "utf-8"));
7373
+ return pkg.version;
7374
+ }
7375
+ function getRemoteVersion() {
7376
+ try {
7377
+ const output = execSync12("npm view @askexenow/exe-os version", {
7378
+ encoding: "utf-8",
7379
+ timeout: 15e3,
7380
+ stdio: ["pipe", "pipe", "pipe"]
7381
+ });
7382
+ return output.trim();
7383
+ } catch {
7384
+ return null;
7385
+ }
7386
+ }
7387
+ function checkForUpdate(packageRoot) {
7388
+ const localVersion = getLocalVersion(packageRoot);
7389
+ const remoteVersion = getRemoteVersion();
7390
+ if (!remoteVersion) {
7391
+ return {
7392
+ updateAvailable: false,
7393
+ localVersion,
7394
+ error: "Could not reach npm registry or package not published yet"
7395
+ };
7396
+ }
7397
+ if (remoteVersion === localVersion) {
7398
+ return { updateAvailable: false, localVersion, remoteVersion };
7399
+ }
7400
+ return { updateAvailable: true, localVersion, remoteVersion };
7401
+ }
7402
+ var init_update_check = __esm({
7403
+ "src/lib/update-check.ts"() {
7404
+ "use strict";
7405
+ }
7406
+ });
7407
+
7041
7408
  // src/lib/ws-auth.ts
7042
7409
  var ws_auth_exports = {};
7043
7410
  __export(ws_auth_exports, {
@@ -7074,12 +7441,12 @@ __export(device_registry_exports, {
7074
7441
  });
7075
7442
  import crypto9 from "crypto";
7076
7443
  import os7 from "os";
7077
- import { readFileSync as readFileSync12, writeFileSync as writeFileSync6, mkdirSync as mkdirSync8, existsSync as existsSync16 } from "fs";
7078
- import path19 from "path";
7444
+ import { readFileSync as readFileSync13, writeFileSync as writeFileSync6, mkdirSync as mkdirSync8, existsSync as existsSync16 } from "fs";
7445
+ import path20 from "path";
7079
7446
  function getDeviceInfo() {
7080
7447
  if (existsSync16(DEVICE_JSON_PATH)) {
7081
7448
  try {
7082
- const raw = readFileSync12(DEVICE_JSON_PATH, "utf8");
7449
+ const raw = readFileSync13(DEVICE_JSON_PATH, "utf8");
7083
7450
  const data = JSON.parse(raw);
7084
7451
  if (data.deviceId && data.friendlyName && data.hostname) {
7085
7452
  return data;
@@ -7093,7 +7460,7 @@ function getDeviceInfo() {
7093
7460
  friendlyName: hostname.replace(/\./g, "-").toLowerCase(),
7094
7461
  hostname
7095
7462
  };
7096
- mkdirSync8(path19.dirname(DEVICE_JSON_PATH), { recursive: true });
7463
+ mkdirSync8(path20.dirname(DEVICE_JSON_PATH), { recursive: true });
7097
7464
  writeFileSync6(DEVICE_JSON_PATH, JSON.stringify(info, null, 2));
7098
7465
  return info;
7099
7466
  }
@@ -7134,7 +7501,7 @@ var init_device_registry = __esm({
7134
7501
  "src/lib/device-registry.ts"() {
7135
7502
  "use strict";
7136
7503
  init_config();
7137
- DEVICE_JSON_PATH = path19.join(EXE_AI_DIR, "device.json");
7504
+ DEVICE_JSON_PATH = path20.join(EXE_AI_DIR, "device.json");
7138
7505
  }
7139
7506
  });
7140
7507
 
@@ -7574,11 +7941,11 @@ var init_messaging = __esm({
7574
7941
  init_config();
7575
7942
  init_memory();
7576
7943
  import net2 from "net";
7577
- import { writeFileSync as writeFileSync7, unlinkSync as unlinkSync5, mkdirSync as mkdirSync9, existsSync as existsSync17, readFileSync as readFileSync13 } from "fs";
7578
- import path20 from "path";
7944
+ import { writeFileSync as writeFileSync7, unlinkSync as unlinkSync5, mkdirSync as mkdirSync9, existsSync as existsSync17, readFileSync as readFileSync14 } from "fs";
7945
+ import path21 from "path";
7579
7946
  import { getLlama } from "node-llama-cpp";
7580
- var SOCKET_PATH2 = process.env.EXE_DAEMON_SOCK ?? process.env.EXE_EMBED_SOCK ?? path20.join(EXE_AI_DIR, "exed.sock");
7581
- var PID_PATH2 = process.env.EXE_DAEMON_PID ?? process.env.EXE_EMBED_PID ?? path20.join(EXE_AI_DIR, "exed.pid");
7947
+ var SOCKET_PATH2 = process.env.EXE_DAEMON_SOCK ?? process.env.EXE_EMBED_SOCK ?? path21.join(EXE_AI_DIR, "exed.sock");
7948
+ var PID_PATH2 = process.env.EXE_DAEMON_PID ?? process.env.EXE_EMBED_PID ?? path21.join(EXE_AI_DIR, "exed.pid");
7582
7949
  var MODEL_FILE = "jina-embeddings-v5-small-q4_k_m.gguf";
7583
7950
  var IDLE_TIMEOUT_MS = 15 * 60 * 1e3;
7584
7951
  var REVIEW_POLL_INTERVAL_MS = 60 * 1e3;
@@ -7593,7 +7960,7 @@ var _idleTimer = null;
7593
7960
  var _requestsServed = 0;
7594
7961
  var _startedAt = Date.now();
7595
7962
  async function loadModel() {
7596
- const modelPath = path20.join(MODELS_DIR, MODEL_FILE);
7963
+ const modelPath = path21.join(MODELS_DIR, MODEL_FILE);
7597
7964
  if (!existsSync17(modelPath)) {
7598
7965
  process.stderr.write(`[exed] FATAL: model not found at ${modelPath}
7599
7966
  `);
@@ -7716,12 +8083,12 @@ async function handleHealthCheck(socket, requestId) {
7716
8083
  }
7717
8084
  }
7718
8085
  function startServer() {
7719
- mkdirSync9(path20.dirname(SOCKET_PATH2), { recursive: true });
8086
+ mkdirSync9(path21.dirname(SOCKET_PATH2), { recursive: true });
7720
8087
  for (const oldFile of ["embed.sock", "embed.pid"]) {
7721
- const oldPath = path20.join(path20.dirname(SOCKET_PATH2), oldFile);
8088
+ const oldPath = path21.join(path21.dirname(SOCKET_PATH2), oldFile);
7722
8089
  try {
7723
8090
  if (oldFile.endsWith(".pid")) {
7724
- const pid = parseInt(readFileSync13(oldPath, "utf8").trim(), 10);
8091
+ const pid = parseInt(readFileSync14(oldPath, "utf8").trim(), 10);
7725
8092
  if (pid > 0) try {
7726
8093
  process.kill(pid, "SIGKILL");
7727
8094
  } catch {
@@ -8145,7 +8512,7 @@ process.on("SIGTERM", () => void shutdown());
8145
8512
  function checkExistingDaemon() {
8146
8513
  try {
8147
8514
  if (!existsSync17(PID_PATH2)) return false;
8148
- const pid = parseInt(readFileSync13(PID_PATH2, "utf8").trim(), 10);
8515
+ const pid = parseInt(readFileSync14(PID_PATH2, "utf8").trim(), 10);
8149
8516
  if (!pid || isNaN(pid)) return false;
8150
8517
  process.kill(pid, 0);
8151
8518
  process.stderr.write(`[exed] Another daemon is already running (PID ${pid}). Exiting.
@@ -8164,6 +8531,52 @@ function checkExistingDaemon() {
8164
8531
  return false;
8165
8532
  }
8166
8533
  }
8534
+ function startAutoUpdateCheck() {
8535
+ const tick = async () => {
8536
+ try {
8537
+ const { loadConfig: loadConfig2 } = await Promise.resolve().then(() => (init_config(), config_exports));
8538
+ const config = await loadConfig2();
8539
+ const { checkIntervalMs, autoInstall } = config.autoUpdate;
8540
+ if (!autoInstall && !config.autoUpdate.checkOnBoot) return;
8541
+ const { checkForUpdate: checkForUpdate2 } = await Promise.resolve().then(() => (init_update_check(), update_check_exports));
8542
+ const packageRoot = new URL("../..", import.meta.url).pathname;
8543
+ const result = checkForUpdate2(packageRoot);
8544
+ if (!result.updateAvailable) return;
8545
+ process.stderr.write(
8546
+ `[exed] Update available: v${result.localVersion} \u2192 v${result.remoteVersion}
8547
+ `
8548
+ );
8549
+ if (autoInstall) {
8550
+ process.stderr.write("[exed] Auto-installing update...\n");
8551
+ const { execSync: execSync13 } = await import("child_process");
8552
+ execSync13("npm install -g @askexenow/exe-os@latest", {
8553
+ timeout: 12e4,
8554
+ stdio: ["pipe", "pipe", "pipe"]
8555
+ });
8556
+ process.stderr.write(
8557
+ `[exed] Updated to v${result.remoteVersion}. Restart daemon to apply.
8558
+ `
8559
+ );
8560
+ }
8561
+ void checkIntervalMs;
8562
+ } catch (err) {
8563
+ process.stderr.write(
8564
+ `[exed] Auto-update check error: ${err instanceof Error ? err.message : String(err)}
8565
+ `
8566
+ );
8567
+ }
8568
+ };
8569
+ Promise.resolve().then(() => (init_config(), config_exports)).then(({ loadConfig: loadConfig2 }) => loadConfig2()).then((config) => {
8570
+ const intervalMs = config.autoUpdate.checkIntervalMs;
8571
+ const timer = setInterval(() => void tick(), intervalMs);
8572
+ timer.unref();
8573
+ process.stderr.write(
8574
+ `[exed] Auto-update check started (every ${Math.round(intervalMs / 36e5)}h, autoInstall: ${config.autoUpdate.autoInstall})
8575
+ `
8576
+ );
8577
+ }).catch(() => {
8578
+ });
8579
+ }
8167
8580
  if (checkExistingDaemon()) {
8168
8581
  process.exit(0);
8169
8582
  }
@@ -8181,6 +8594,7 @@ try {
8181
8594
  startWikiSync();
8182
8595
  startIntercomQueueDrain();
8183
8596
  startConfidenceDecay();
8597
+ startAutoUpdateCheck();
8184
8598
  try {
8185
8599
  const { loadConfig: loadConfig2 } = await Promise.resolve().then(() => (init_config(), config_exports));
8186
8600
  const config = await loadConfig2();