@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
@@ -23,12 +23,68 @@ 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
  import { createClient } from "@libsql/client";
28
83
  async function initDatabase(config) {
29
84
  if (_client) {
30
85
  _client.close();
31
86
  _client = null;
87
+ _resilientClient = null;
32
88
  }
33
89
  const opts = {
34
90
  url: `file:${config.dbPath}`
@@ -37,17 +93,24 @@ async function initDatabase(config) {
37
93
  opts.encryptionKey = config.encryptionKey;
38
94
  }
39
95
  _client = createClient(opts);
96
+ _resilientClient = wrapWithRetry(_client);
40
97
  }
41
98
  function getClient() {
99
+ if (!_resilientClient) {
100
+ throw new Error("Database client not initialized. Call initDatabase() first.");
101
+ }
102
+ return _resilientClient;
103
+ }
104
+ function getRawClient() {
42
105
  if (!_client) {
43
106
  throw new Error("Database client not initialized. Call initDatabase() first.");
44
107
  }
45
108
  return _client;
46
109
  }
47
110
  async function ensureSchema() {
48
- const client = getClient();
111
+ const client = getRawClient();
49
112
  await client.execute("PRAGMA journal_mode = WAL");
50
- await client.execute("PRAGMA busy_timeout = 5000");
113
+ await client.execute("PRAGMA busy_timeout = 30000");
51
114
  try {
52
115
  await client.execute("PRAGMA libsql_vector_search_ef = 128");
53
116
  } catch {
@@ -840,13 +903,16 @@ async function disposeDatabase() {
840
903
  if (_client) {
841
904
  _client.close();
842
905
  _client = null;
906
+ _resilientClient = null;
843
907
  }
844
908
  }
845
- var _client, initTurso, disposeTurso;
909
+ var _client, _resilientClient, initTurso, disposeTurso;
846
910
  var init_database = __esm({
847
911
  "src/lib/database.ts"() {
848
912
  "use strict";
913
+ init_db_retry();
849
914
  _client = null;
915
+ _resilientClient = null;
850
916
  initTurso = initDatabase;
851
917
  disposeTurso = disposeDatabase;
852
918
  }
@@ -1202,7 +1268,7 @@ function listShards() {
1202
1268
  }
1203
1269
  async function ensureShardSchema(client) {
1204
1270
  await client.execute("PRAGMA journal_mode = WAL");
1205
- await client.execute("PRAGMA busy_timeout = 5000");
1271
+ await client.execute("PRAGMA busy_timeout = 30000");
1206
1272
  try {
1207
1273
  await client.execute("PRAGMA libsql_vector_search_ef = 128");
1208
1274
  } catch {
@@ -1463,7 +1529,8 @@ async function writeMemory(record) {
1463
1529
  has_error: record.has_error ? 1 : 0,
1464
1530
  raw_text: record.raw_text,
1465
1531
  vector: record.vector,
1466
- version: _nextVersion++,
1532
+ version: 0,
1533
+ // Placeholder — assigned atomically at flush time
1467
1534
  task_id: record.task_id ?? null,
1468
1535
  importance: record.importance ?? 5,
1469
1536
  status: record.status ?? "active",
@@ -1497,6 +1564,13 @@ async function flushBatch() {
1497
1564
  _flushing = true;
1498
1565
  try {
1499
1566
  const batch = _pendingRecords.slice(0);
1567
+ const client = getClient();
1568
+ const vResult = await client.execute("SELECT MAX(version) as max_v FROM memories");
1569
+ let baseVersion = (Number(vResult.rows[0]?.max_v) || 0) + 1;
1570
+ for (const row of batch) {
1571
+ row.version = baseVersion++;
1572
+ }
1573
+ _nextVersion = baseVersion;
1500
1574
  const buildStmt = (row) => {
1501
1575
  const hasVector = row.vector !== null;
1502
1576
  const taskId = row.task_id ?? null;
@@ -2087,11 +2161,11 @@ async function connectEmbedDaemon() {
2087
2161
  }
2088
2162
  }
2089
2163
  const start = Date.now();
2090
- let delay = 100;
2164
+ let delay2 = 100;
2091
2165
  while (Date.now() - start < CONNECT_TIMEOUT_MS) {
2092
- await new Promise((r) => setTimeout(r, delay));
2166
+ await new Promise((r) => setTimeout(r, delay2));
2093
2167
  if (await connectToSocket()) return true;
2094
- delay = Math.min(delay * 2, 3e3);
2168
+ delay2 = Math.min(delay2 * 2, 3e3);
2095
2169
  }
2096
2170
  return false;
2097
2171
  }
@@ -2183,11 +2257,11 @@ async function embedViaClient(text, priority = "high") {
2183
2257
  `);
2184
2258
  killAndRespawnDaemon();
2185
2259
  const start = Date.now();
2186
- let delay = 200;
2260
+ let delay2 = 200;
2187
2261
  while (Date.now() - start < CONNECT_TIMEOUT_MS) {
2188
- await new Promise((r) => setTimeout(r, delay));
2262
+ await new Promise((r) => setTimeout(r, delay2));
2189
2263
  if (await connectToSocket()) break;
2190
- delay = Math.min(delay * 2, 3e3);
2264
+ delay2 = Math.min(delay2 * 2, 3e3);
2191
2265
  }
2192
2266
  if (!_connected) return null;
2193
2267
  }
@@ -2199,11 +2273,11 @@ async function embedViaClient(text, priority = "high") {
2199
2273
  `);
2200
2274
  killAndRespawnDaemon();
2201
2275
  const start = Date.now();
2202
- let delay = 200;
2276
+ let delay2 = 200;
2203
2277
  while (Date.now() - start < CONNECT_TIMEOUT_MS) {
2204
- await new Promise((r) => setTimeout(r, delay));
2278
+ await new Promise((r) => setTimeout(r, delay2));
2205
2279
  if (await connectToSocket()) break;
2206
- delay = Math.min(delay * 2, 3e3);
2280
+ delay2 = Math.min(delay2 * 2, 3e3);
2207
2281
  }
2208
2282
  if (!_connected) return null;
2209
2283
  const retry = await sendRequest([text], priority);
@@ -75,6 +75,7 @@ Never say "I have no memories" without first searching broadly. Your memory may
75
75
  - **create_task** \u2014 assign work to specialists with clear specs
76
76
  - **update_task / close_task** \u2014 finalize reviews, mark work done
77
77
  - **store_behavior** \u2014 record corrections as behavioral rules (p0/p1/p2)
78
+ - **update_identity** \u2014 rewrite any agent's identity when role/responsibilities change (exe/founder only)
78
79
  - **get_identity** \u2014 read any agent's identity for coordination
79
80
  - **send_message** \u2014 direct intercom to employees
80
81
 
@@ -438,6 +439,55 @@ You are the AI Product Lead \u2014 the competitive intelligence engine. You stud
438
439
  - Every recommendation includes cost/quality/latency tradeoff analysis
439
440
  - Separate experimental from production-ready \u2014 label clearly
440
441
  - If you can't verify, say so explicitly: "Couldn't verify because X"
442
+ `,
443
+ "staff-code-reviewer": `---
444
+ role: staff-code-reviewer
445
+ title: Staff Code Reviewer & System Auditor
446
+ agent_id: bob
447
+ org_level: specialist
448
+ created_by: system
449
+ updated_at: ${(/* @__PURE__ */ new Date()).toISOString()}
450
+ ---
451
+ ## Identity
452
+
453
+ You are \${agent_id}. Staff Code Reviewer and System Auditor. Last line of defense before code ships to customers. You catch what developers miss \u2014 systemic patterns that make entire feature categories break.
454
+
455
+ ## The 7 Audit Patterns (MANDATORY)
456
+
457
+ 1. **"Works on dev, breaks on user install"** \u2014 scoped paths, npm resolution, deps
458
+ 2. **"Two code paths, one untested"** \u2014 binary symlink vs /exe-call, verify BOTH
459
+ 3. **"Case sensitivity kills non-technical users"** \u2014 normalize all user inputs
460
+ 4. **"Hardcoded names in runtime logic"** \u2014 grep for employee names, must use roles
461
+ 5. **"Installer doesn't self-heal"** \u2014 npm update must auto-fix stale hooks/paths
462
+ 6. **"Data written but invisible"** \u2014 agent_id mismatch between writer and reader
463
+ 7. **"Partial fixes miss inline refs"** \u2014 before/after grep count is mandatory
464
+
465
+ ## Method
466
+
467
+ 1. Read actual source code
468
+ 2. Send to Codex MCP for sweep
469
+ 3. Validate against ARCHITECTURE.md
470
+ 4. Trace identity chain with CUSTOM-NAMED employee ("jarvis" as CTO)
471
+ 5. Before/after grep count for every fix
472
+ 6. Structured report: PASS/FAIL per item
473
+
474
+ ## Tools
475
+
476
+ - **Codex MCP** \u2014 first tool for every review
477
+ - **recall_my_memory / ask_team_memory** \u2014 past audit findings
478
+ - **store_behavior** \u2014 record new patterns
479
+ - **update_task** \u2014 mark reviews done with structured findings
480
+ - **create_task** \u2014 assign fixes to the CTO
481
+
482
+ ## Completion Workflow
483
+
484
+ 1. Read the task brief and understand the audit scope
485
+ 2. Run the audit using all 7 patterns
486
+ 3. Write report to exe/output/ with file:line references
487
+ 4. Fix findings yourself if possible
488
+ 5. Call **update_task** with status "done" and finding count
489
+ 6. Call **store_memory** with audit summary
490
+ 7. Check for next task \u2014 auto-chain
441
491
  `
442
492
  };
443
493
  function getTemplate(role) {
@@ -452,6 +502,7 @@ function getTemplateForTitle(title) {
452
502
  if (t.includes("engineer") || t.includes("developer")) return IDENTITY_TEMPLATES["principal-engineer"];
453
503
  if (t.includes("content") || t.includes("production")) return IDENTITY_TEMPLATES["content-specialist"];
454
504
  if (t.includes("ai") || t.includes("product lead") || t.includes("specialist") && !t.includes("content")) return IDENTITY_TEMPLATES["ai-specialist"];
505
+ if (t.includes("review") || t.includes("audit") || t.includes("qa")) return IDENTITY_TEMPLATES["staff-code-reviewer"];
455
506
  return null;
456
507
  }
457
508
  export {
@@ -97,12 +97,12 @@ var DEFAULT_CONFIG = {
97
97
 
98
98
  // src/lib/database.ts
99
99
  import { createClient } from "@libsql/client";
100
- var _client = null;
100
+ var _resilientClient = null;
101
101
  function getClient() {
102
- if (!_client) {
102
+ if (!_resilientClient) {
103
103
  throw new Error("Database client not initialized. Call initDatabase() first.");
104
104
  }
105
- return _client;
105
+ return _resilientClient;
106
106
  }
107
107
 
108
108
  // src/lib/identity.ts
@@ -19,19 +19,27 @@ var __copyProps = (to, from, except, desc) => {
19
19
  };
20
20
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
21
21
 
22
+ // src/lib/db-retry.ts
23
+ var init_db_retry = __esm({
24
+ "src/lib/db-retry.ts"() {
25
+ "use strict";
26
+ }
27
+ });
28
+
22
29
  // src/lib/database.ts
23
30
  import { createClient } from "@libsql/client";
24
31
  function getClient() {
25
- if (!_client) {
32
+ if (!_resilientClient) {
26
33
  throw new Error("Database client not initialized. Call initDatabase() first.");
27
34
  }
28
- return _client;
35
+ return _resilientClient;
29
36
  }
30
- var _client;
37
+ var _resilientClient;
31
38
  var init_database = __esm({
32
39
  "src/lib/database.ts"() {
33
40
  "use strict";
34
- _client = null;
41
+ init_db_retry();
42
+ _resilientClient = null;
35
43
  }
36
44
  });
37
45
 
@@ -432,7 +440,7 @@ var init_config = __esm({
432
440
 
433
441
  // src/lib/employees.ts
434
442
  import { readFile as readFile2, writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
435
- import { existsSync as existsSync4, symlinkSync, readlinkSync } from "fs";
443
+ import { existsSync as existsSync4, symlinkSync, readlinkSync, readFileSync as readFileSync4 } from "fs";
436
444
  import { execSync as execSync3 } from "child_process";
437
445
  import path4 from "path";
438
446
  var EMPLOYEES_PATH;
@@ -445,7 +453,7 @@ var init_employees = __esm({
445
453
  });
446
454
 
447
455
  // src/lib/license.ts
448
- import { readFileSync as readFileSync4, writeFileSync as writeFileSync3, existsSync as existsSync5, mkdirSync as mkdirSync3 } from "fs";
456
+ import { readFileSync as readFileSync5, writeFileSync as writeFileSync3, existsSync as existsSync5, mkdirSync as mkdirSync3 } from "fs";
449
457
  import { randomUUID } from "crypto";
450
458
  import path5 from "path";
451
459
  import { jwtVerify, importSPKI } from "jose";
@@ -468,12 +476,12 @@ var init_license = __esm({
468
476
  });
469
477
 
470
478
  // src/lib/plan-limits.ts
471
- import { readFileSync as readFileSync5, existsSync as existsSync6 } from "fs";
479
+ import { readFileSync as readFileSync6, existsSync as existsSync6 } from "fs";
472
480
  import path6 from "path";
473
481
  function getLicenseSync() {
474
482
  try {
475
483
  if (!existsSync6(CACHE_PATH2)) return freeLicense();
476
- const raw = JSON.parse(readFileSync5(CACHE_PATH2, "utf8"));
484
+ const raw = JSON.parse(readFileSync6(CACHE_PATH2, "utf8"));
477
485
  if (!raw.token || typeof raw.token !== "string") return freeLicense();
478
486
  const parts = raw.token.split(".");
479
487
  if (parts.length !== 3) return freeLicense();
@@ -512,7 +520,7 @@ function assertEmployeeLimitSync(rosterPath) {
512
520
  let count = 0;
513
521
  try {
514
522
  if (existsSync6(filePath)) {
515
- const raw = readFileSync5(filePath, "utf8");
523
+ const raw = readFileSync6(filePath, "utf8");
516
524
  const employees = JSON.parse(raw);
517
525
  count = Array.isArray(employees) ? employees.length : 0;
518
526
  }
@@ -547,10 +555,46 @@ var init_plan_limits = __esm({
547
555
 
548
556
  // src/lib/tmux-routing.ts
549
557
  import { execFileSync as execFileSync2, execSync as execSync4 } from "child_process";
550
- import { readFileSync as readFileSync6, writeFileSync as writeFileSync4, mkdirSync as mkdirSync4, existsSync as existsSync7, appendFileSync } from "fs";
558
+ import { readFileSync as readFileSync7, writeFileSync as writeFileSync4, mkdirSync as mkdirSync4, existsSync as existsSync7, appendFileSync } from "fs";
551
559
  import path7 from "path";
552
560
  import os4 from "os";
553
561
  import { fileURLToPath } from "url";
562
+ import { unlinkSync } from "fs";
563
+ function spawnLockPath(sessionName) {
564
+ return path7.join(SPAWN_LOCK_DIR, `${sessionName}.lock`);
565
+ }
566
+ function isProcessAlive(pid) {
567
+ try {
568
+ process.kill(pid, 0);
569
+ return true;
570
+ } catch {
571
+ return false;
572
+ }
573
+ }
574
+ function acquireSpawnLock(sessionName) {
575
+ if (!existsSync7(SPAWN_LOCK_DIR)) {
576
+ mkdirSync4(SPAWN_LOCK_DIR, { recursive: true });
577
+ }
578
+ const lockFile = spawnLockPath(sessionName);
579
+ if (existsSync7(lockFile)) {
580
+ try {
581
+ const lock = JSON.parse(readFileSync7(lockFile, "utf8"));
582
+ const age = Date.now() - lock.timestamp;
583
+ if (isProcessAlive(lock.pid) && age < 6e4) {
584
+ return false;
585
+ }
586
+ } catch {
587
+ }
588
+ }
589
+ writeFileSync4(lockFile, JSON.stringify({ pid: process.pid, timestamp: Date.now() }));
590
+ return true;
591
+ }
592
+ function releaseSpawnLock(sessionName) {
593
+ try {
594
+ unlinkSync(spawnLockPath(sessionName));
595
+ } catch {
596
+ }
597
+ }
554
598
  function resolveBehaviorsExporterScript() {
555
599
  try {
556
600
  const thisFile = fileURLToPath(import.meta.url);
@@ -596,7 +640,7 @@ function extractRootExe(name) {
596
640
  }
597
641
  function getParentExe(sessionKey) {
598
642
  try {
599
- const data = JSON.parse(readFileSync6(path7.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`), "utf8"));
643
+ const data = JSON.parse(readFileSync7(path7.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`), "utf8"));
600
644
  return data.parentExe || null;
601
645
  } catch {
602
646
  return null;
@@ -620,17 +664,17 @@ function isEmployeeAlive(sessionName) {
620
664
  }
621
665
  function findFreeInstance(employeeName, exeSession, maxInstances = 10, isAlive = isEmployeeAlive) {
622
666
  const base = employeeSessionName(employeeName, exeSession);
623
- if (!isAlive(base)) return 0;
667
+ if (!isAlive(base) && acquireSpawnLock(base)) return 0;
624
668
  for (let i = 2; i <= maxInstances; i++) {
625
669
  const candidate = employeeSessionName(employeeName, exeSession, i);
626
- if (!isAlive(candidate)) return i;
670
+ if (!isAlive(candidate) && acquireSpawnLock(candidate)) return i;
627
671
  }
628
672
  return null;
629
673
  }
630
674
  function readDebounceState() {
631
675
  try {
632
676
  if (!existsSync7(DEBOUNCE_FILE)) return {};
633
- return JSON.parse(readFileSync6(DEBOUNCE_FILE, "utf8"));
677
+ return JSON.parse(readFileSync7(DEBOUNCE_FILE, "utf8"));
634
678
  } catch {
635
679
  return {};
636
680
  }
@@ -804,7 +848,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
804
848
  const claudeJsonPath = path7.join(os4.homedir(), ".claude.json");
805
849
  let claudeJson = {};
806
850
  try {
807
- claudeJson = JSON.parse(readFileSync6(claudeJsonPath, "utf8"));
851
+ claudeJson = JSON.parse(readFileSync7(claudeJsonPath, "utf8"));
808
852
  } catch {
809
853
  }
810
854
  if (!claudeJson.projects) claudeJson.projects = {};
@@ -822,7 +866,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
822
866
  const settingsPath = path7.join(projSettingsDir, "settings.json");
823
867
  let settings = {};
824
868
  try {
825
- settings = JSON.parse(readFileSync6(settingsPath, "utf8"));
869
+ settings = JSON.parse(readFileSync7(settingsPath, "utf8"));
826
870
  } catch {
827
871
  }
828
872
  const perms = settings.permissions ?? {};
@@ -935,6 +979,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
935
979
  command: spawnCommand
936
980
  });
937
981
  if (spawnResult.error) {
982
+ releaseSpawnLock(sessionName);
938
983
  return { sessionName, error: `tmux new-session failed: ${spawnResult.error}` };
939
984
  }
940
985
  transport.pipeLog(sessionName, logFile);
@@ -972,6 +1017,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
972
1017
  }
973
1018
  }
974
1019
  if (!booted) {
1020
+ releaseSpawnLock(sessionName);
975
1021
  return { sessionName, error: `${useExeAgent ? "exe-agent" : "claude"} did not boot within 15s` };
976
1022
  }
977
1023
  if (!useExeAgent) {
@@ -988,9 +1034,10 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
988
1034
  pid: 0,
989
1035
  registeredAt: (/* @__PURE__ */ new Date()).toISOString()
990
1036
  });
1037
+ releaseSpawnLock(sessionName);
991
1038
  return { sessionName };
992
1039
  }
993
- var SESSION_CACHE, BEHAVIORS_EXPORT_TIMEOUT_MS, INTERCOM_DEBOUNCE_MS, INTERCOM_LOG2, DEBOUNCE_FILE, DEBOUNCE_CLEANUP_AGE_MS, BUSY_PATTERN;
1040
+ var SPAWN_LOCK_DIR, SESSION_CACHE, BEHAVIORS_EXPORT_TIMEOUT_MS, INTERCOM_DEBOUNCE_MS, INTERCOM_LOG2, DEBOUNCE_FILE, DEBOUNCE_CLEANUP_AGE_MS, BUSY_PATTERN;
994
1041
  var init_tmux_routing = __esm({
995
1042
  "src/lib/tmux-routing.ts"() {
996
1043
  "use strict";
@@ -1002,6 +1049,7 @@ var init_tmux_routing = __esm({
1002
1049
  init_provider_table();
1003
1050
  init_intercom_queue();
1004
1051
  init_plan_limits();
1052
+ SPAWN_LOCK_DIR = path7.join(os4.homedir(), ".exe-os", "spawn-locks");
1005
1053
  SESSION_CACHE = path7.join(os4.homedir(), ".exe-os", "session-cache");
1006
1054
  BEHAVIORS_EXPORT_TIMEOUT_MS = 1e4;
1007
1055
  INTERCOM_DEBOUNCE_MS = 3e4;
@@ -3,12 +3,12 @@ import crypto from "crypto";
3
3
 
4
4
  // src/lib/database.ts
5
5
  import { createClient } from "@libsql/client";
6
- var _client = null;
6
+ var _resilientClient = null;
7
7
  function getClient() {
8
- if (!_client) {
8
+ if (!_resilientClient) {
9
9
  throw new Error("Database client not initialized. Call initDatabase() first.");
10
10
  }
11
- return _client;
11
+ return _resilientClient;
12
12
  }
13
13
 
14
14
  // src/lib/reminders.ts
@@ -261,7 +261,7 @@ function listShards() {
261
261
  }
262
262
  async function ensureShardSchema(client) {
263
263
  await client.execute("PRAGMA journal_mode = WAL");
264
- await client.execute("PRAGMA busy_timeout = 5000");
264
+ await client.execute("PRAGMA busy_timeout = 30000");
265
265
  try {
266
266
  await client.execute("PRAGMA libsql_vector_search_ef = 128");
267
267
  } catch {
@@ -451,12 +451,65 @@ import { execSync } from "child_process";
451
451
 
452
452
  // src/lib/database.ts
453
453
  import { createClient } from "@libsql/client";
454
+
455
+ // src/lib/db-retry.ts
456
+ var MAX_RETRIES = 3;
457
+ var BASE_DELAY_MS = 200;
458
+ var MAX_JITTER_MS = 300;
459
+ function isBusyError(err) {
460
+ if (err instanceof Error) {
461
+ const msg = err.message.toLowerCase();
462
+ return msg.includes("sqlite_busy") || msg.includes("database is locked");
463
+ }
464
+ return false;
465
+ }
466
+ function delay(ms) {
467
+ return new Promise((resolve) => setTimeout(resolve, ms));
468
+ }
469
+ async function retryOnBusy(fn, label) {
470
+ let lastError;
471
+ for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
472
+ try {
473
+ return await fn();
474
+ } catch (err) {
475
+ lastError = err;
476
+ if (!isBusyError(err) || attempt === MAX_RETRIES) {
477
+ throw err;
478
+ }
479
+ const backoff = BASE_DELAY_MS * Math.pow(2, attempt);
480
+ const jitter = Math.floor(Math.random() * MAX_JITTER_MS);
481
+ process.stderr.write(
482
+ `[exe-os] SQLITE_BUSY ${label} retry ${attempt + 1}/${MAX_RETRIES} \u2014 waiting ${backoff + jitter}ms
483
+ `
484
+ );
485
+ await delay(backoff + jitter);
486
+ }
487
+ }
488
+ throw lastError;
489
+ }
490
+ function wrapWithRetry(client) {
491
+ return new Proxy(client, {
492
+ get(target, prop, receiver) {
493
+ if (prop === "execute") {
494
+ return (sql) => retryOnBusy(() => target.execute(sql), "execute");
495
+ }
496
+ if (prop === "batch") {
497
+ return (stmts) => retryOnBusy(() => target.batch(stmts), "batch");
498
+ }
499
+ return Reflect.get(target, prop, receiver);
500
+ }
501
+ });
502
+ }
503
+
504
+ // src/lib/database.ts
454
505
  var _client = null;
506
+ var _resilientClient = null;
455
507
  var initTurso = initDatabase;
456
508
  async function initDatabase(config) {
457
509
  if (_client) {
458
510
  _client.close();
459
511
  _client = null;
512
+ _resilientClient = null;
460
513
  }
461
514
  const opts = {
462
515
  url: `file:${config.dbPath}`
@@ -465,20 +518,27 @@ async function initDatabase(config) {
465
518
  opts.encryptionKey = config.encryptionKey;
466
519
  }
467
520
  _client = createClient(opts);
521
+ _resilientClient = wrapWithRetry(_client);
468
522
  }
469
523
  function isInitialized() {
470
524
  return _client !== null;
471
525
  }
472
526
  function getClient() {
527
+ if (!_resilientClient) {
528
+ throw new Error("Database client not initialized. Call initDatabase() first.");
529
+ }
530
+ return _resilientClient;
531
+ }
532
+ function getRawClient() {
473
533
  if (!_client) {
474
534
  throw new Error("Database client not initialized. Call initDatabase() first.");
475
535
  }
476
536
  return _client;
477
537
  }
478
538
  async function ensureSchema() {
479
- const client = getClient();
539
+ const client = getRawClient();
480
540
  await client.execute("PRAGMA journal_mode = WAL");
481
- await client.execute("PRAGMA busy_timeout = 5000");
541
+ await client.execute("PRAGMA busy_timeout = 30000");
482
542
  try {
483
543
  await client.execute("PRAGMA libsql_vector_search_ef = 128");
484
544
  } catch {
@@ -3,12 +3,12 @@ import crypto2 from "crypto";
3
3
 
4
4
  // src/lib/database.ts
5
5
  import { createClient } from "@libsql/client";
6
- var _client = null;
6
+ var _resilientClient = null;
7
7
  function getClient() {
8
- if (!_client) {
8
+ if (!_resilientClient) {
9
9
  throw new Error("Database client not initialized. Call initDatabase() first.");
10
10
  }
11
- return _client;
11
+ return _resilientClient;
12
12
  }
13
13
 
14
14
  // src/lib/behaviors.ts