@askexenow/exe-os 0.8.33 → 0.8.37

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 (89) hide show
  1. package/dist/bin/backfill-conversations.js +341 -349
  2. package/dist/bin/backfill-responses.js +81 -13
  3. package/dist/bin/backfill-vectors.js +72 -12
  4. package/dist/bin/cleanup-stale-review-tasks.js +63 -3
  5. package/dist/bin/cli.js +1737 -1117
  6. package/dist/bin/exe-assign.js +89 -19
  7. package/dist/bin/exe-boot.js +951 -101
  8. package/dist/bin/exe-call.js +61 -2
  9. package/dist/bin/exe-dispatch.js +61 -13
  10. package/dist/bin/exe-doctor.js +63 -3
  11. package/dist/bin/exe-export-behaviors.js +71 -3
  12. package/dist/bin/exe-forget.js +69 -4
  13. package/dist/bin/exe-gateway.js +178 -45
  14. package/dist/bin/exe-heartbeat.js +79 -14
  15. package/dist/bin/exe-kill.js +71 -3
  16. package/dist/bin/exe-launch-agent.js +148 -14
  17. package/dist/bin/exe-link.js +1437 -0
  18. package/dist/bin/exe-new-employee.js +98 -13
  19. package/dist/bin/exe-pending-messages.js +74 -8
  20. package/dist/bin/exe-pending-notifications.js +63 -3
  21. package/dist/bin/exe-pending-reviews.js +77 -11
  22. package/dist/bin/exe-rename.js +1287 -0
  23. package/dist/bin/exe-review.js +73 -5
  24. package/dist/bin/exe-search.js +88 -14
  25. package/dist/bin/exe-session-cleanup.js +102 -28
  26. package/dist/bin/exe-status.js +64 -4
  27. package/dist/bin/exe-team.js +64 -4
  28. package/dist/bin/git-sweep.js +80 -5
  29. package/dist/bin/graph-backfill.js +71 -3
  30. package/dist/bin/graph-export.js +71 -3
  31. package/dist/bin/install.js +38 -8
  32. package/dist/bin/scan-tasks.js +80 -5
  33. package/dist/bin/setup.js +128 -10
  34. package/dist/bin/shard-migrate.js +71 -3
  35. package/dist/bin/wiki-sync.js +71 -3
  36. package/dist/gateway/index.js +179 -46
  37. package/dist/hooks/bug-report-worker.js +254 -28
  38. package/dist/hooks/commit-complete.js +80 -5
  39. package/dist/hooks/error-recall.js +89 -15
  40. package/dist/hooks/exe-heartbeat-hook.js +1 -1
  41. package/dist/hooks/ingest-worker.js +185 -51
  42. package/dist/hooks/ingest.js +1 -1
  43. package/dist/hooks/instructions-loaded.js +81 -6
  44. package/dist/hooks/notification.js +81 -6
  45. package/dist/hooks/post-compact.js +81 -6
  46. package/dist/hooks/pre-compact.js +81 -6
  47. package/dist/hooks/pre-tool-use.js +423 -196
  48. package/dist/hooks/prompt-ingest-worker.js +91 -23
  49. package/dist/hooks/prompt-submit.js +159 -45
  50. package/dist/hooks/response-ingest-worker.js +96 -23
  51. package/dist/hooks/session-end.js +81 -6
  52. package/dist/hooks/session-start.js +89 -15
  53. package/dist/hooks/stop.js +81 -6
  54. package/dist/hooks/subagent-stop.js +81 -6
  55. package/dist/hooks/summary-worker.js +807 -55
  56. package/dist/index.js +198 -60
  57. package/dist/lib/cloud-sync.js +703 -18
  58. package/dist/lib/consolidation.js +4 -4
  59. package/dist/lib/database.js +64 -2
  60. package/dist/lib/device-registry.js +70 -3
  61. package/dist/lib/employee-templates.js +26 -0
  62. package/dist/lib/employees.js +34 -1
  63. package/dist/lib/exe-daemon.js +207 -74
  64. package/dist/lib/hybrid-search.js +88 -14
  65. package/dist/lib/identity-templates.js +51 -0
  66. package/dist/lib/identity.js +3 -3
  67. package/dist/lib/messaging.js +65 -17
  68. package/dist/lib/reminders.js +3 -3
  69. package/dist/lib/schedules.js +63 -3
  70. package/dist/lib/skill-learning.js +3 -3
  71. package/dist/lib/status-brief.js +63 -5
  72. package/dist/lib/store.js +73 -4
  73. package/dist/lib/task-router.js +4 -2
  74. package/dist/lib/tasks.js +95 -28
  75. package/dist/lib/tmux-routing.js +92 -23
  76. package/dist/mcp/server.js +800 -74
  77. package/dist/mcp/tools/complete-reminder.js +3 -3
  78. package/dist/mcp/tools/create-reminder.js +3 -3
  79. package/dist/mcp/tools/create-task.js +198 -31
  80. package/dist/mcp/tools/deactivate-behavior.js +4 -4
  81. package/dist/mcp/tools/list-reminders.js +3 -3
  82. package/dist/mcp/tools/list-tasks.js +19 -9
  83. package/dist/mcp/tools/send-message.js +69 -21
  84. package/dist/mcp/tools/update-task.js +28 -18
  85. package/dist/runtime/index.js +166 -28
  86. package/dist/tui/App.js +193 -40
  87. package/package.json +7 -3
  88. package/src/commands/exe/afk.md +116 -0
  89. package/src/commands/exe/rename.md +12 -0
@@ -25,12 +25,68 @@ var __copyProps = (to, from, except, desc) => {
25
25
  };
26
26
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
27
27
 
28
+ // src/lib/db-retry.ts
29
+ function isBusyError(err) {
30
+ if (err instanceof Error) {
31
+ const msg = err.message.toLowerCase();
32
+ return msg.includes("sqlite_busy") || msg.includes("database is locked");
33
+ }
34
+ return false;
35
+ }
36
+ function delay(ms) {
37
+ return new Promise((resolve) => setTimeout(resolve, ms));
38
+ }
39
+ async function retryOnBusy(fn, label) {
40
+ let lastError;
41
+ for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
42
+ try {
43
+ return await fn();
44
+ } catch (err) {
45
+ lastError = err;
46
+ if (!isBusyError(err) || attempt === MAX_RETRIES) {
47
+ throw err;
48
+ }
49
+ const backoff = BASE_DELAY_MS * Math.pow(2, attempt);
50
+ const jitter = Math.floor(Math.random() * MAX_JITTER_MS);
51
+ process.stderr.write(
52
+ `[exe-os] SQLITE_BUSY ${label} retry ${attempt + 1}/${MAX_RETRIES} \u2014 waiting ${backoff + jitter}ms
53
+ `
54
+ );
55
+ await delay(backoff + jitter);
56
+ }
57
+ }
58
+ throw lastError;
59
+ }
60
+ function wrapWithRetry(client) {
61
+ return new Proxy(client, {
62
+ get(target, prop, receiver) {
63
+ if (prop === "execute") {
64
+ return (sql) => retryOnBusy(() => target.execute(sql), "execute");
65
+ }
66
+ if (prop === "batch") {
67
+ return (stmts) => retryOnBusy(() => target.batch(stmts), "batch");
68
+ }
69
+ return Reflect.get(target, prop, receiver);
70
+ }
71
+ });
72
+ }
73
+ var MAX_RETRIES, BASE_DELAY_MS, MAX_JITTER_MS;
74
+ var init_db_retry = __esm({
75
+ "src/lib/db-retry.ts"() {
76
+ "use strict";
77
+ MAX_RETRIES = 3;
78
+ BASE_DELAY_MS = 200;
79
+ MAX_JITTER_MS = 300;
80
+ }
81
+ });
82
+
28
83
  // src/lib/database.ts
29
84
  import { createClient } from "@libsql/client";
30
85
  async function initDatabase(config) {
31
86
  if (_client) {
32
87
  _client.close();
33
88
  _client = null;
89
+ _resilientClient = null;
34
90
  }
35
91
  const opts = {
36
92
  url: `file:${config.dbPath}`
@@ -39,17 +95,24 @@ async function initDatabase(config) {
39
95
  opts.encryptionKey = config.encryptionKey;
40
96
  }
41
97
  _client = createClient(opts);
98
+ _resilientClient = wrapWithRetry(_client);
42
99
  }
43
100
  function getClient() {
101
+ if (!_resilientClient) {
102
+ throw new Error("Database client not initialized. Call initDatabase() first.");
103
+ }
104
+ return _resilientClient;
105
+ }
106
+ function getRawClient() {
44
107
  if (!_client) {
45
108
  throw new Error("Database client not initialized. Call initDatabase() first.");
46
109
  }
47
110
  return _client;
48
111
  }
49
112
  async function ensureSchema() {
50
- const client = getClient();
113
+ const client = getRawClient();
51
114
  await client.execute("PRAGMA journal_mode = WAL");
52
- await client.execute("PRAGMA busy_timeout = 5000");
115
+ await client.execute("PRAGMA busy_timeout = 30000");
53
116
  try {
54
117
  await client.execute("PRAGMA libsql_vector_search_ef = 128");
55
118
  } catch {
@@ -838,11 +901,13 @@ async function ensureSchema() {
838
901
  }
839
902
  }
840
903
  }
841
- var _client, initTurso;
904
+ var _client, _resilientClient, initTurso;
842
905
  var init_database = __esm({
843
906
  "src/lib/database.ts"() {
844
907
  "use strict";
908
+ init_db_retry();
845
909
  _client = null;
910
+ _resilientClient = null;
846
911
  initTurso = initDatabase;
847
912
  }
848
913
  });
@@ -1094,7 +1159,7 @@ function listShards() {
1094
1159
  }
1095
1160
  async function ensureShardSchema(client) {
1096
1161
  await client.execute("PRAGMA journal_mode = WAL");
1097
- await client.execute("PRAGMA busy_timeout = 5000");
1162
+ await client.execute("PRAGMA busy_timeout = 30000");
1098
1163
  try {
1099
1164
  await client.execute("PRAGMA libsql_vector_search_ef = 128");
1100
1165
  } catch {
@@ -1508,21 +1573,139 @@ var init_tasks_crud = __esm({
1508
1573
  });
1509
1574
 
1510
1575
  // src/lib/employees.ts
1576
+ var employees_exports = {};
1577
+ __export(employees_exports, {
1578
+ EMPLOYEES_PATH: () => EMPLOYEES_PATH,
1579
+ addEmployee: () => addEmployee,
1580
+ getEmployee: () => getEmployee,
1581
+ getEmployeeByRole: () => getEmployeeByRole,
1582
+ getEmployeeNamesByRole: () => getEmployeeNamesByRole,
1583
+ hasRole: () => hasRole,
1584
+ isMultiInstance: () => isMultiInstance,
1585
+ loadEmployees: () => loadEmployees,
1586
+ loadEmployeesSync: () => loadEmployeesSync,
1587
+ registerBinSymlinks: () => registerBinSymlinks,
1588
+ saveEmployees: () => saveEmployees,
1589
+ validateEmployeeName: () => validateEmployeeName
1590
+ });
1511
1591
  import { readFile as readFile3, writeFile as writeFile4, mkdir as mkdir4 } from "fs/promises";
1512
- import { existsSync as existsSync6, symlinkSync, readlinkSync } from "fs";
1592
+ import { existsSync as existsSync6, symlinkSync, readlinkSync, readFileSync as readFileSync4 } from "fs";
1513
1593
  import { execSync as execSync2 } from "child_process";
1514
1594
  import path6 from "path";
1515
- var EMPLOYEES_PATH;
1595
+ function validateEmployeeName(name) {
1596
+ if (!name) {
1597
+ return { valid: false, error: "Name is required" };
1598
+ }
1599
+ if (name.length > 32) {
1600
+ return { valid: false, error: "Name must be 32 characters or fewer" };
1601
+ }
1602
+ if (!/^[a-z][a-z0-9]*$/.test(name)) {
1603
+ return {
1604
+ valid: false,
1605
+ error: "Name must start with a letter and contain only lowercase alphanumeric characters"
1606
+ };
1607
+ }
1608
+ return { valid: true };
1609
+ }
1610
+ async function loadEmployees(employeesPath = EMPLOYEES_PATH) {
1611
+ if (!existsSync6(employeesPath)) {
1612
+ return [];
1613
+ }
1614
+ const raw = await readFile3(employeesPath, "utf-8");
1615
+ try {
1616
+ return JSON.parse(raw);
1617
+ } catch {
1618
+ return [];
1619
+ }
1620
+ }
1621
+ async function saveEmployees(employees, employeesPath = EMPLOYEES_PATH) {
1622
+ await mkdir4(path6.dirname(employeesPath), { recursive: true });
1623
+ await writeFile4(employeesPath, JSON.stringify(employees, null, 2) + "\n", "utf-8");
1624
+ }
1625
+ function loadEmployeesSync(employeesPath = EMPLOYEES_PATH) {
1626
+ if (!existsSync6(employeesPath)) return [];
1627
+ try {
1628
+ return JSON.parse(readFileSync4(employeesPath, "utf-8"));
1629
+ } catch {
1630
+ return [];
1631
+ }
1632
+ }
1633
+ function getEmployee(employees, name) {
1634
+ return employees.find((e) => e.name.toLowerCase() === name.toLowerCase());
1635
+ }
1636
+ function getEmployeeByRole(employees, role) {
1637
+ const lower = role.toLowerCase();
1638
+ return employees.find((e) => e.role.toLowerCase() === lower);
1639
+ }
1640
+ function getEmployeeNamesByRole(employees, role) {
1641
+ const lower = role.toLowerCase();
1642
+ return employees.filter((e) => e.role.toLowerCase() === lower).map((e) => e.name);
1643
+ }
1644
+ function hasRole(agentName, role) {
1645
+ const employees = loadEmployeesSync();
1646
+ const emp = getEmployee(employees, agentName);
1647
+ return emp ? emp.role.toLowerCase() === role.toLowerCase() : false;
1648
+ }
1649
+ function isMultiInstance(agentName, employees) {
1650
+ const roster = employees ?? loadEmployeesSync();
1651
+ const emp = getEmployee(roster, agentName);
1652
+ if (!emp) return false;
1653
+ return MULTI_INSTANCE_ROLES.has(emp.role.toLowerCase());
1654
+ }
1655
+ function addEmployee(employees, employee) {
1656
+ const normalized = { ...employee, name: employee.name.toLowerCase() };
1657
+ if (employees.some((e) => e.name.toLowerCase() === normalized.name)) {
1658
+ throw new Error(`Employee '${normalized.name}' already exists`);
1659
+ }
1660
+ return [...employees, normalized];
1661
+ }
1662
+ function registerBinSymlinks(name) {
1663
+ const created = [];
1664
+ const skipped = [];
1665
+ const errors = [];
1666
+ let exeBinPath;
1667
+ try {
1668
+ exeBinPath = execSync2("which exe", { encoding: "utf-8" }).trim();
1669
+ } catch {
1670
+ errors.push("Could not find 'exe' in PATH");
1671
+ return { created, skipped, errors };
1672
+ }
1673
+ const binDir = path6.dirname(exeBinPath);
1674
+ let target;
1675
+ try {
1676
+ target = readlinkSync(exeBinPath);
1677
+ } catch {
1678
+ errors.push("Could not read 'exe' symlink");
1679
+ return { created, skipped, errors };
1680
+ }
1681
+ for (const suffix of ["", "-opencode"]) {
1682
+ const linkName = `${name}${suffix}`;
1683
+ const linkPath = path6.join(binDir, linkName);
1684
+ if (existsSync6(linkPath)) {
1685
+ skipped.push(linkName);
1686
+ continue;
1687
+ }
1688
+ try {
1689
+ symlinkSync(target, linkPath);
1690
+ created.push(linkName);
1691
+ } catch (err) {
1692
+ errors.push(`${linkName}: ${err instanceof Error ? err.message : String(err)}`);
1693
+ }
1694
+ }
1695
+ return { created, skipped, errors };
1696
+ }
1697
+ var EMPLOYEES_PATH, MULTI_INSTANCE_ROLES;
1516
1698
  var init_employees = __esm({
1517
1699
  "src/lib/employees.ts"() {
1518
1700
  "use strict";
1519
1701
  init_config();
1520
1702
  EMPLOYEES_PATH = path6.join(EXE_AI_DIR, "exe-employees.json");
1703
+ MULTI_INSTANCE_ROLES = /* @__PURE__ */ new Set(["principal engineer", "content production specialist", "staff code reviewer"]);
1521
1704
  }
1522
1705
  });
1523
1706
 
1524
1707
  // src/lib/session-registry.ts
1525
- import { readFileSync as readFileSync4, writeFileSync, mkdirSync as mkdirSync2, existsSync as existsSync7 } from "fs";
1708
+ import { readFileSync as readFileSync5, writeFileSync, mkdirSync as mkdirSync2, existsSync as existsSync7 } from "fs";
1526
1709
  import path7 from "path";
1527
1710
  import os3 from "os";
1528
1711
  function registerSession(entry) {
@@ -1541,7 +1724,7 @@ function registerSession(entry) {
1541
1724
  }
1542
1725
  function listSessions() {
1543
1726
  try {
1544
- const raw = readFileSync4(REGISTRY_PATH, "utf8");
1727
+ const raw = readFileSync5(REGISTRY_PATH, "utf8");
1545
1728
  return JSON.parse(raw);
1546
1729
  } catch {
1547
1730
  return [];
@@ -1771,7 +1954,7 @@ var init_provider_table = __esm({
1771
1954
  });
1772
1955
 
1773
1956
  // src/lib/intercom-queue.ts
1774
- import { readFileSync as readFileSync5, writeFileSync as writeFileSync2, renameSync as renameSync2, existsSync as existsSync8, mkdirSync as mkdirSync3 } from "fs";
1957
+ import { readFileSync as readFileSync6, writeFileSync as writeFileSync2, renameSync as renameSync2, existsSync as existsSync8, mkdirSync as mkdirSync3 } from "fs";
1775
1958
  import path8 from "path";
1776
1959
  import os4 from "os";
1777
1960
  function ensureDir() {
@@ -1781,7 +1964,7 @@ function ensureDir() {
1781
1964
  function readQueue() {
1782
1965
  try {
1783
1966
  if (!existsSync8(QUEUE_PATH)) return [];
1784
- return JSON.parse(readFileSync5(QUEUE_PATH, "utf8"));
1967
+ return JSON.parse(readFileSync6(QUEUE_PATH, "utf8"));
1785
1968
  } catch {
1786
1969
  return [];
1787
1970
  }
@@ -1820,7 +2003,7 @@ var init_intercom_queue = __esm({
1820
2003
  });
1821
2004
 
1822
2005
  // src/lib/license.ts
1823
- import { readFileSync as readFileSync6, writeFileSync as writeFileSync3, existsSync as existsSync9, mkdirSync as mkdirSync4 } from "fs";
2006
+ import { readFileSync as readFileSync7, writeFileSync as writeFileSync3, existsSync as existsSync9, mkdirSync as mkdirSync4 } from "fs";
1824
2007
  import { randomUUID } from "crypto";
1825
2008
  import path9 from "path";
1826
2009
  import { jwtVerify, importSPKI } from "jose";
@@ -1843,12 +2026,12 @@ var init_license = __esm({
1843
2026
  });
1844
2027
 
1845
2028
  // src/lib/plan-limits.ts
1846
- import { readFileSync as readFileSync7, existsSync as existsSync10 } from "fs";
2029
+ import { readFileSync as readFileSync8, existsSync as existsSync10 } from "fs";
1847
2030
  import path10 from "path";
1848
2031
  function getLicenseSync() {
1849
2032
  try {
1850
2033
  if (!existsSync10(CACHE_PATH2)) return freeLicense();
1851
- const raw = JSON.parse(readFileSync7(CACHE_PATH2, "utf8"));
2034
+ const raw = JSON.parse(readFileSync8(CACHE_PATH2, "utf8"));
1852
2035
  if (!raw.token || typeof raw.token !== "string") return freeLicense();
1853
2036
  const parts = raw.token.split(".");
1854
2037
  if (parts.length !== 3) return freeLicense();
@@ -1887,7 +2070,7 @@ function assertEmployeeLimitSync(rosterPath) {
1887
2070
  let count = 0;
1888
2071
  try {
1889
2072
  if (existsSync10(filePath)) {
1890
- const raw = readFileSync7(filePath, "utf8");
2073
+ const raw = readFileSync8(filePath, "utf8");
1891
2074
  const employees = JSON.parse(raw);
1892
2075
  count = Array.isArray(employees) ? employees.length : 0;
1893
2076
  }
@@ -1922,10 +2105,46 @@ var init_plan_limits = __esm({
1922
2105
 
1923
2106
  // src/lib/tmux-routing.ts
1924
2107
  import { execFileSync as execFileSync2, execSync as execSync5 } from "child_process";
1925
- import { readFileSync as readFileSync8, writeFileSync as writeFileSync4, mkdirSync as mkdirSync5, existsSync as existsSync11, appendFileSync } from "fs";
2108
+ import { readFileSync as readFileSync9, writeFileSync as writeFileSync4, mkdirSync as mkdirSync5, existsSync as existsSync11, appendFileSync } from "fs";
1926
2109
  import path11 from "path";
1927
2110
  import os5 from "os";
1928
2111
  import { fileURLToPath } from "url";
2112
+ import { unlinkSync as unlinkSync2 } from "fs";
2113
+ function spawnLockPath(sessionName) {
2114
+ return path11.join(SPAWN_LOCK_DIR, `${sessionName}.lock`);
2115
+ }
2116
+ function isProcessAlive(pid) {
2117
+ try {
2118
+ process.kill(pid, 0);
2119
+ return true;
2120
+ } catch {
2121
+ return false;
2122
+ }
2123
+ }
2124
+ function acquireSpawnLock(sessionName) {
2125
+ if (!existsSync11(SPAWN_LOCK_DIR)) {
2126
+ mkdirSync5(SPAWN_LOCK_DIR, { recursive: true });
2127
+ }
2128
+ const lockFile = spawnLockPath(sessionName);
2129
+ if (existsSync11(lockFile)) {
2130
+ try {
2131
+ const lock = JSON.parse(readFileSync9(lockFile, "utf8"));
2132
+ const age = Date.now() - lock.timestamp;
2133
+ if (isProcessAlive(lock.pid) && age < 6e4) {
2134
+ return false;
2135
+ }
2136
+ } catch {
2137
+ }
2138
+ }
2139
+ writeFileSync4(lockFile, JSON.stringify({ pid: process.pid, timestamp: Date.now() }));
2140
+ return true;
2141
+ }
2142
+ function releaseSpawnLock(sessionName) {
2143
+ try {
2144
+ unlinkSync2(spawnLockPath(sessionName));
2145
+ } catch {
2146
+ }
2147
+ }
1929
2148
  function resolveBehaviorsExporterScript() {
1930
2149
  try {
1931
2150
  const thisFile = fileURLToPath(import.meta.url);
@@ -1971,7 +2190,7 @@ function extractRootExe(name) {
1971
2190
  }
1972
2191
  function getParentExe(sessionKey) {
1973
2192
  try {
1974
- const data = JSON.parse(readFileSync8(path11.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`), "utf8"));
2193
+ const data = JSON.parse(readFileSync9(path11.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`), "utf8"));
1975
2194
  return data.parentExe || null;
1976
2195
  } catch {
1977
2196
  return null;
@@ -1995,17 +2214,17 @@ function isEmployeeAlive(sessionName) {
1995
2214
  }
1996
2215
  function findFreeInstance(employeeName, exeSession, maxInstances = 10, isAlive = isEmployeeAlive) {
1997
2216
  const base = employeeSessionName(employeeName, exeSession);
1998
- if (!isAlive(base)) return 0;
2217
+ if (!isAlive(base) && acquireSpawnLock(base)) return 0;
1999
2218
  for (let i = 2; i <= maxInstances; i++) {
2000
2219
  const candidate = employeeSessionName(employeeName, exeSession, i);
2001
- if (!isAlive(candidate)) return i;
2220
+ if (!isAlive(candidate) && acquireSpawnLock(candidate)) return i;
2002
2221
  }
2003
2222
  return null;
2004
2223
  }
2005
2224
  function readDebounceState() {
2006
2225
  try {
2007
2226
  if (!existsSync11(DEBOUNCE_FILE)) return {};
2008
- return JSON.parse(readFileSync8(DEBOUNCE_FILE, "utf8"));
2227
+ return JSON.parse(readFileSync9(DEBOUNCE_FILE, "utf8"));
2009
2228
  } catch {
2010
2229
  return {};
2011
2230
  }
@@ -2179,7 +2398,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
2179
2398
  const claudeJsonPath = path11.join(os5.homedir(), ".claude.json");
2180
2399
  let claudeJson = {};
2181
2400
  try {
2182
- claudeJson = JSON.parse(readFileSync8(claudeJsonPath, "utf8"));
2401
+ claudeJson = JSON.parse(readFileSync9(claudeJsonPath, "utf8"));
2183
2402
  } catch {
2184
2403
  }
2185
2404
  if (!claudeJson.projects) claudeJson.projects = {};
@@ -2197,7 +2416,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
2197
2416
  const settingsPath = path11.join(projSettingsDir, "settings.json");
2198
2417
  let settings = {};
2199
2418
  try {
2200
- settings = JSON.parse(readFileSync8(settingsPath, "utf8"));
2419
+ settings = JSON.parse(readFileSync9(settingsPath, "utf8"));
2201
2420
  } catch {
2202
2421
  }
2203
2422
  const perms = settings.permissions ?? {};
@@ -2310,6 +2529,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
2310
2529
  command: spawnCommand
2311
2530
  });
2312
2531
  if (spawnResult.error) {
2532
+ releaseSpawnLock(sessionName);
2313
2533
  return { sessionName, error: `tmux new-session failed: ${spawnResult.error}` };
2314
2534
  }
2315
2535
  transport.pipeLog(sessionName, logFile);
@@ -2347,6 +2567,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
2347
2567
  }
2348
2568
  }
2349
2569
  if (!booted) {
2570
+ releaseSpawnLock(sessionName);
2350
2571
  return { sessionName, error: `${useExeAgent ? "exe-agent" : "claude"} did not boot within 15s` };
2351
2572
  }
2352
2573
  if (!useExeAgent) {
@@ -2363,9 +2584,10 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
2363
2584
  pid: 0,
2364
2585
  registeredAt: (/* @__PURE__ */ new Date()).toISOString()
2365
2586
  });
2587
+ releaseSpawnLock(sessionName);
2366
2588
  return { sessionName };
2367
2589
  }
2368
- var SESSION_CACHE, BEHAVIORS_EXPORT_TIMEOUT_MS, INTERCOM_DEBOUNCE_MS, INTERCOM_LOG2, DEBOUNCE_FILE, DEBOUNCE_CLEANUP_AGE_MS, BUSY_PATTERN;
2590
+ var SPAWN_LOCK_DIR, SESSION_CACHE, BEHAVIORS_EXPORT_TIMEOUT_MS, INTERCOM_DEBOUNCE_MS, INTERCOM_LOG2, DEBOUNCE_FILE, DEBOUNCE_CLEANUP_AGE_MS, BUSY_PATTERN;
2369
2591
  var init_tmux_routing = __esm({
2370
2592
  "src/lib/tmux-routing.ts"() {
2371
2593
  "use strict";
@@ -2377,6 +2599,7 @@ var init_tmux_routing = __esm({
2377
2599
  init_provider_table();
2378
2600
  init_intercom_queue();
2379
2601
  init_plan_limits();
2602
+ SPAWN_LOCK_DIR = path11.join(os5.homedir(), ".exe-os", "spawn-locks");
2380
2603
  SESSION_CACHE = path11.join(os5.homedir(), ".exe-os", "session-cache");
2381
2604
  BEHAVIORS_EXPORT_TIMEOUT_MS = 1e4;
2382
2605
  INTERCOM_DEBOUNCE_MS = 3e4;
@@ -2389,7 +2612,7 @@ var init_tmux_routing = __esm({
2389
2612
 
2390
2613
  // src/lib/tasks-review.ts
2391
2614
  import path12 from "path";
2392
- import { existsSync as existsSync12, readdirSync as readdirSync2, unlinkSync as unlinkSync2 } from "fs";
2615
+ import { existsSync as existsSync12, readdirSync as readdirSync2, unlinkSync as unlinkSync3 } from "fs";
2393
2616
  var init_tasks_review = __esm({
2394
2617
  "src/lib/tasks-review.ts"() {
2395
2618
  "use strict";
@@ -2544,7 +2767,7 @@ async function dispatchTaskToEmployee(input) {
2544
2767
  } else {
2545
2768
  const projectDir = input.projectDir ?? process.cwd();
2546
2769
  const result = ensureEmployee(input.assignedTo, exeSession, projectDir, {
2547
- autoInstance: input.assignedTo === "tom" || input.assignedTo === "sasha"
2770
+ autoInstance: isMultiInstance(input.assignedTo)
2548
2771
  });
2549
2772
  if (result.status === "failed") {
2550
2773
  process.stderr.write(
@@ -2566,12 +2789,13 @@ var init_tasks_notify = __esm({
2566
2789
  init_session_key();
2567
2790
  init_notifications();
2568
2791
  init_transport();
2792
+ init_employees();
2569
2793
  }
2570
2794
  });
2571
2795
 
2572
2796
  // src/lib/tasks.ts
2573
2797
  import path15 from "path";
2574
- import { writeFileSync as writeFileSync5, mkdirSync as mkdirSync6, unlinkSync as unlinkSync3 } from "fs";
2798
+ import { writeFileSync as writeFileSync5, mkdirSync as mkdirSync6, unlinkSync as unlinkSync4 } from "fs";
2575
2799
  async function createTask(input) {
2576
2800
  const result = await createTaskCore(input);
2577
2801
  if (!input.skipDispatch && result.status !== "blocked" && !process.env.VITEST) {
@@ -2708,14 +2932,16 @@ async function main() {
2708
2932
  await initStore();
2709
2933
  const fpPrefix = fingerprint.slice(0, 8);
2710
2934
  const client = getClient();
2935
+ const { loadEmployeesSync: loadEmployeesSync2, getEmployeeByRole: getEmployeeByRole2 } = await Promise.resolve().then(() => (init_employees(), employees_exports));
2936
+ const ctoName = getEmployeeByRole2(loadEmployeesSync2(), "CTO")?.name ?? "yoshi";
2711
2937
  const existing = await client.execute({
2712
2938
  sql: `SELECT id FROM tasks
2713
- WHERE assigned_to = 'yoshi'
2939
+ WHERE assigned_to = ?
2714
2940
  AND status IN ('open', 'in_progress')
2715
2941
  AND title LIKE '[auto-bug]%'
2716
2942
  AND task_file LIKE ?
2717
2943
  LIMIT 1`,
2718
- args: [`%${fpPrefix}%`]
2944
+ args: [ctoName, `%${fpPrefix}%`]
2719
2945
  });
2720
2946
  if (existing.rows.length > 0) {
2721
2947
  process.stderr.write(`[bug-report-worker] Duplicate found for fingerprint ${fingerprint}, skipping
@@ -2751,7 +2977,7 @@ async function main() {
2751
2977
  ].join("\n");
2752
2978
  await createTask({
2753
2979
  title: `[auto-bug] ${toolName}: ${errorSummary}`,
2754
- assignedTo: "yoshi",
2980
+ assignedTo: ctoName,
2755
2981
  assignedBy: "system",
2756
2982
  projectName,
2757
2983
  priority: "p1",
@@ -23,6 +23,61 @@ var init_memory = __esm({
23
23
  }
24
24
  });
25
25
 
26
+ // src/lib/db-retry.ts
27
+ function isBusyError(err) {
28
+ if (err instanceof Error) {
29
+ const msg = err.message.toLowerCase();
30
+ return msg.includes("sqlite_busy") || msg.includes("database is locked");
31
+ }
32
+ return false;
33
+ }
34
+ function delay(ms) {
35
+ return new Promise((resolve) => setTimeout(resolve, ms));
36
+ }
37
+ async function retryOnBusy(fn, label) {
38
+ let lastError;
39
+ for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
40
+ try {
41
+ return await fn();
42
+ } catch (err) {
43
+ lastError = err;
44
+ if (!isBusyError(err) || attempt === MAX_RETRIES) {
45
+ throw err;
46
+ }
47
+ const backoff = BASE_DELAY_MS * Math.pow(2, attempt);
48
+ const jitter = Math.floor(Math.random() * MAX_JITTER_MS);
49
+ process.stderr.write(
50
+ `[exe-os] SQLITE_BUSY ${label} retry ${attempt + 1}/${MAX_RETRIES} \u2014 waiting ${backoff + jitter}ms
51
+ `
52
+ );
53
+ await delay(backoff + jitter);
54
+ }
55
+ }
56
+ throw lastError;
57
+ }
58
+ function wrapWithRetry(client) {
59
+ return new Proxy(client, {
60
+ get(target, prop, receiver) {
61
+ if (prop === "execute") {
62
+ return (sql) => retryOnBusy(() => target.execute(sql), "execute");
63
+ }
64
+ if (prop === "batch") {
65
+ return (stmts) => retryOnBusy(() => target.batch(stmts), "batch");
66
+ }
67
+ return Reflect.get(target, prop, receiver);
68
+ }
69
+ });
70
+ }
71
+ var MAX_RETRIES, BASE_DELAY_MS, MAX_JITTER_MS;
72
+ var init_db_retry = __esm({
73
+ "src/lib/db-retry.ts"() {
74
+ "use strict";
75
+ MAX_RETRIES = 3;
76
+ BASE_DELAY_MS = 200;
77
+ MAX_JITTER_MS = 300;
78
+ }
79
+ });
80
+
26
81
  // src/lib/database.ts
27
82
  var database_exports = {};
28
83
  __export(database_exports, {
@@ -30,6 +85,7 @@ __export(database_exports, {
30
85
  disposeTurso: () => disposeTurso,
31
86
  ensureSchema: () => ensureSchema,
32
87
  getClient: () => getClient,
88
+ getRawClient: () => getRawClient,
33
89
  initDatabase: () => initDatabase,
34
90
  initTurso: () => initTurso,
35
91
  isInitialized: () => isInitialized
@@ -39,6 +95,7 @@ async function initDatabase(config) {
39
95
  if (_client) {
40
96
  _client.close();
41
97
  _client = null;
98
+ _resilientClient = null;
42
99
  }
43
100
  const opts = {
44
101
  url: `file:${config.dbPath}`
@@ -47,20 +104,27 @@ async function initDatabase(config) {
47
104
  opts.encryptionKey = config.encryptionKey;
48
105
  }
49
106
  _client = createClient(opts);
107
+ _resilientClient = wrapWithRetry(_client);
50
108
  }
51
109
  function isInitialized() {
52
110
  return _client !== null;
53
111
  }
54
112
  function getClient() {
113
+ if (!_resilientClient) {
114
+ throw new Error("Database client not initialized. Call initDatabase() first.");
115
+ }
116
+ return _resilientClient;
117
+ }
118
+ function getRawClient() {
55
119
  if (!_client) {
56
120
  throw new Error("Database client not initialized. Call initDatabase() first.");
57
121
  }
58
122
  return _client;
59
123
  }
60
124
  async function ensureSchema() {
61
- const client = getClient();
125
+ const client = getRawClient();
62
126
  await client.execute("PRAGMA journal_mode = WAL");
63
- await client.execute("PRAGMA busy_timeout = 5000");
127
+ await client.execute("PRAGMA busy_timeout = 30000");
64
128
  try {
65
129
  await client.execute("PRAGMA libsql_vector_search_ef = 128");
66
130
  } catch {
@@ -853,13 +917,16 @@ async function disposeDatabase() {
853
917
  if (_client) {
854
918
  _client.close();
855
919
  _client = null;
920
+ _resilientClient = null;
856
921
  }
857
922
  }
858
- var _client, initTurso, disposeTurso;
923
+ var _client, _resilientClient, initTurso, disposeTurso;
859
924
  var init_database = __esm({
860
925
  "src/lib/database.ts"() {
861
926
  "use strict";
927
+ init_db_retry();
862
928
  _client = null;
929
+ _resilientClient = null;
863
930
  initTurso = initDatabase;
864
931
  disposeTurso = disposeDatabase;
865
932
  }
@@ -1161,7 +1228,7 @@ function listShards() {
1161
1228
  }
1162
1229
  async function ensureShardSchema(client) {
1163
1230
  await client.execute("PRAGMA journal_mode = WAL");
1164
- await client.execute("PRAGMA busy_timeout = 5000");
1231
+ await client.execute("PRAGMA busy_timeout = 30000");
1165
1232
  try {
1166
1233
  await client.execute("PRAGMA libsql_vector_search_ef = 128");
1167
1234
  } catch {
@@ -1422,7 +1489,8 @@ async function writeMemory(record) {
1422
1489
  has_error: record.has_error ? 1 : 0,
1423
1490
  raw_text: record.raw_text,
1424
1491
  vector: record.vector,
1425
- version: _nextVersion++,
1492
+ version: 0,
1493
+ // Placeholder — assigned atomically at flush time
1426
1494
  task_id: record.task_id ?? null,
1427
1495
  importance: record.importance ?? 5,
1428
1496
  status: record.status ?? "active",
@@ -1456,6 +1524,13 @@ async function flushBatch() {
1456
1524
  _flushing = true;
1457
1525
  try {
1458
1526
  const batch = _pendingRecords.slice(0);
1527
+ const client = getClient();
1528
+ const vResult = await client.execute("SELECT MAX(version) as max_v FROM memories");
1529
+ let baseVersion = (Number(vResult.rows[0]?.max_v) || 0) + 1;
1530
+ for (const row of batch) {
1531
+ row.version = baseVersion++;
1532
+ }
1533
+ _nextVersion = baseVersion;
1459
1534
  const buildStmt = (row) => {
1460
1535
  const hasVector = row.vector !== null;
1461
1536
  const taskId = row.task_id ?? null;