@askexenow/exe-os 0.8.36 → 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 (62) hide show
  1. package/dist/bin/backfill-conversations.js +9 -1
  2. package/dist/bin/backfill-responses.js +9 -1
  3. package/dist/bin/cli.js +255 -31
  4. package/dist/bin/exe-assign.js +9 -1
  5. package/dist/bin/exe-boot.js +554 -23
  6. package/dist/bin/exe-dispatch.js +43 -3
  7. package/dist/bin/exe-export-behaviors.js +7 -0
  8. package/dist/bin/exe-gateway.js +57 -9
  9. package/dist/bin/exe-heartbeat.js +2 -1
  10. package/dist/bin/exe-kill.js +7 -0
  11. package/dist/bin/exe-launch-agent.js +8 -1
  12. package/dist/bin/exe-link.js +503 -12
  13. package/dist/bin/exe-pending-messages.js +2 -1
  14. package/dist/bin/exe-pending-reviews.js +2 -1
  15. package/dist/bin/exe-review.js +9 -1
  16. package/dist/bin/exe-search.js +9 -1
  17. package/dist/bin/exe-session-cleanup.js +11 -2
  18. package/dist/bin/git-sweep.js +9 -1
  19. package/dist/bin/graph-backfill.js +7 -0
  20. package/dist/bin/graph-export.js +7 -0
  21. package/dist/bin/install.js +35 -5
  22. package/dist/bin/scan-tasks.js +9 -1
  23. package/dist/bin/shard-migrate.js +7 -0
  24. package/dist/bin/wiki-sync.js +7 -0
  25. package/dist/gateway/index.js +57 -9
  26. package/dist/hooks/bug-report-worker.js +45 -5
  27. package/dist/hooks/commit-complete.js +9 -1
  28. package/dist/hooks/error-recall.js +10 -2
  29. package/dist/hooks/exe-heartbeat-hook.js +1 -1
  30. package/dist/hooks/ingest-worker.js +56 -8
  31. package/dist/hooks/ingest.js +1 -1
  32. package/dist/hooks/instructions-loaded.js +10 -2
  33. package/dist/hooks/notification.js +10 -2
  34. package/dist/hooks/post-compact.js +10 -2
  35. package/dist/hooks/pre-compact.js +10 -2
  36. package/dist/hooks/pre-tool-use.js +10 -2
  37. package/dist/hooks/prompt-ingest-worker.js +9 -1
  38. package/dist/hooks/prompt-submit.js +56 -8
  39. package/dist/hooks/response-ingest-worker.js +9 -1
  40. package/dist/hooks/session-end.js +10 -2
  41. package/dist/hooks/session-start.js +10 -2
  42. package/dist/hooks/stop.js +10 -2
  43. package/dist/hooks/subagent-stop.js +10 -2
  44. package/dist/hooks/summary-worker.js +512 -13
  45. package/dist/index.js +65 -15
  46. package/dist/lib/cloud-sync.js +502 -11
  47. package/dist/lib/exe-daemon.js +73 -23
  48. package/dist/lib/hybrid-search.js +9 -1
  49. package/dist/lib/messaging.js +43 -3
  50. package/dist/lib/store.js +9 -1
  51. package/dist/lib/tasks.js +47 -7
  52. package/dist/lib/tmux-routing.js +45 -3
  53. package/dist/mcp/server.js +73 -16
  54. package/dist/mcp/tools/create-task.js +48 -8
  55. package/dist/mcp/tools/deactivate-behavior.js +1 -1
  56. package/dist/mcp/tools/list-tasks.js +2 -1
  57. package/dist/mcp/tools/send-message.js +46 -6
  58. package/dist/mcp/tools/update-task.js +3 -2
  59. package/dist/runtime/index.js +54 -4
  60. package/dist/tui/App.js +54 -4
  61. package/package.json +2 -2
  62. package/src/commands/exe/afk.md +116 -0
@@ -1947,7 +1947,8 @@ async function writeMemory(record) {
1947
1947
  has_error: record.has_error ? 1 : 0,
1948
1948
  raw_text: record.raw_text,
1949
1949
  vector: record.vector,
1950
- version: _nextVersion++,
1950
+ version: 0,
1951
+ // Placeholder — assigned atomically at flush time
1951
1952
  task_id: record.task_id ?? null,
1952
1953
  importance: record.importance ?? 5,
1953
1954
  status: record.status ?? "active",
@@ -1981,6 +1982,13 @@ async function flushBatch() {
1981
1982
  _flushing = true;
1982
1983
  try {
1983
1984
  const batch = _pendingRecords.slice(0);
1985
+ const client = getClient();
1986
+ const vResult = await client.execute("SELECT MAX(version) as max_v FROM memories");
1987
+ let baseVersion = (Number(vResult.rows[0]?.max_v) || 0) + 1;
1988
+ for (const row of batch) {
1989
+ row.version = baseVersion++;
1990
+ }
1991
+ _nextVersion = baseVersion;
1984
1992
  const buildStmt = (row) => {
1985
1993
  const hasVector = row.vector !== null;
1986
1994
  const taskId = row.task_id ?? null;
@@ -2883,7 +2891,7 @@ function getActiveAgent() {
2883
2891
  "tmux display-message -p '#{session_name}' 2>/dev/null",
2884
2892
  { encoding: "utf8", timeout: 2e3 }
2885
2893
  ).trim();
2886
- const empMatch = sessionName.match(/^(\w+)-exe\d+$/);
2894
+ const empMatch = sessionName.match(/^([a-zA-Z]+)\d*-exe\d+$/);
2887
2895
  if (empMatch && empMatch[1] !== "exe") {
2888
2896
  return { agentId: empMatch[1], agentRole: "employee" };
2889
2897
  }
@@ -4232,6 +4240,42 @@ import { readFileSync as readFileSync12, writeFileSync as writeFileSync7, mkdirS
4232
4240
  import path18 from "path";
4233
4241
  import os5 from "os";
4234
4242
  import { fileURLToPath as fileURLToPath2 } from "url";
4243
+ import { unlinkSync as unlinkSync4 } from "fs";
4244
+ function spawnLockPath(sessionName) {
4245
+ return path18.join(SPAWN_LOCK_DIR, `${sessionName}.lock`);
4246
+ }
4247
+ function isProcessAlive(pid) {
4248
+ try {
4249
+ process.kill(pid, 0);
4250
+ return true;
4251
+ } catch {
4252
+ return false;
4253
+ }
4254
+ }
4255
+ function acquireSpawnLock2(sessionName) {
4256
+ if (!existsSync14(SPAWN_LOCK_DIR)) {
4257
+ mkdirSync6(SPAWN_LOCK_DIR, { recursive: true });
4258
+ }
4259
+ const lockFile = spawnLockPath(sessionName);
4260
+ if (existsSync14(lockFile)) {
4261
+ try {
4262
+ const lock = JSON.parse(readFileSync12(lockFile, "utf8"));
4263
+ const age = Date.now() - lock.timestamp;
4264
+ if (isProcessAlive(lock.pid) && age < 6e4) {
4265
+ return false;
4266
+ }
4267
+ } catch {
4268
+ }
4269
+ }
4270
+ writeFileSync7(lockFile, JSON.stringify({ pid: process.pid, timestamp: Date.now() }));
4271
+ return true;
4272
+ }
4273
+ function releaseSpawnLock2(sessionName) {
4274
+ try {
4275
+ unlinkSync4(spawnLockPath(sessionName));
4276
+ } catch {
4277
+ }
4278
+ }
4235
4279
  function resolveBehaviorsExporterScript() {
4236
4280
  try {
4237
4281
  const thisFile = fileURLToPath2(import.meta.url);
@@ -4312,10 +4356,10 @@ function isEmployeeAlive(sessionName) {
4312
4356
  }
4313
4357
  function findFreeInstance(employeeName, exeSession, maxInstances = 10, isAlive = isEmployeeAlive) {
4314
4358
  const base = employeeSessionName(employeeName, exeSession);
4315
- if (!isAlive(base)) return 0;
4359
+ if (!isAlive(base) && acquireSpawnLock2(base)) return 0;
4316
4360
  for (let i = 2; i <= maxInstances; i++) {
4317
4361
  const candidate = employeeSessionName(employeeName, exeSession, i);
4318
- if (!isAlive(candidate)) return i;
4362
+ if (!isAlive(candidate) && acquireSpawnLock2(candidate)) return i;
4319
4363
  }
4320
4364
  return null;
4321
4365
  }
@@ -4649,6 +4693,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
4649
4693
  command: spawnCommand
4650
4694
  });
4651
4695
  if (spawnResult.error) {
4696
+ releaseSpawnLock2(sessionName);
4652
4697
  return { sessionName, error: `tmux new-session failed: ${spawnResult.error}` };
4653
4698
  }
4654
4699
  transport.pipeLog(sessionName, logFile);
@@ -4686,6 +4731,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
4686
4731
  }
4687
4732
  }
4688
4733
  if (!booted) {
4734
+ releaseSpawnLock2(sessionName);
4689
4735
  return { sessionName, error: `${useExeAgent ? "exe-agent" : "claude"} did not boot within 15s` };
4690
4736
  }
4691
4737
  if (!useExeAgent) {
@@ -4702,9 +4748,10 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
4702
4748
  pid: 0,
4703
4749
  registeredAt: (/* @__PURE__ */ new Date()).toISOString()
4704
4750
  });
4751
+ releaseSpawnLock2(sessionName);
4705
4752
  return { sessionName };
4706
4753
  }
4707
- var SESSION_CACHE, BEHAVIORS_EXPORT_TIMEOUT_MS, INTERCOM_DEBOUNCE_MS, INTERCOM_LOG2, DEBOUNCE_FILE, DEBOUNCE_CLEANUP_AGE_MS, BUSY_PATTERN;
4754
+ var SPAWN_LOCK_DIR, SESSION_CACHE, BEHAVIORS_EXPORT_TIMEOUT_MS, INTERCOM_DEBOUNCE_MS, INTERCOM_LOG2, DEBOUNCE_FILE, DEBOUNCE_CLEANUP_AGE_MS, BUSY_PATTERN;
4708
4755
  var init_tmux_routing = __esm({
4709
4756
  "src/lib/tmux-routing.ts"() {
4710
4757
  "use strict";
@@ -4716,6 +4763,7 @@ var init_tmux_routing = __esm({
4716
4763
  init_provider_table();
4717
4764
  init_intercom_queue();
4718
4765
  init_plan_limits();
4766
+ SPAWN_LOCK_DIR = path18.join(os5.homedir(), ".exe-os", "spawn-locks");
4719
4767
  SESSION_CACHE = path18.join(os5.homedir(), ".exe-os", "session-cache");
4720
4768
  BEHAVIORS_EXPORT_TIMEOUT_MS = 1e4;
4721
4769
  INTERCOM_DEBOUNCE_MS = 3e4;
@@ -4728,7 +4776,7 @@ var init_tmux_routing = __esm({
4728
4776
 
4729
4777
  // src/lib/tasks-review.ts
4730
4778
  import path19 from "path";
4731
- import { existsSync as existsSync15, readdirSync as readdirSync4, unlinkSync as unlinkSync4 } from "fs";
4779
+ import { existsSync as existsSync15, readdirSync as readdirSync4, unlinkSync as unlinkSync5 } from "fs";
4732
4780
  async function countPendingReviews() {
4733
4781
  const client = getClient();
4734
4782
  const result = await client.execute({
@@ -4852,7 +4900,7 @@ async function cleanupReviewFile(row, taskFile, _baseDir) {
4852
4900
  if (existsSync15(cacheDir)) {
4853
4901
  for (const f of readdirSync4(cacheDir)) {
4854
4902
  if (f.startsWith("review-notified-")) {
4855
- unlinkSync4(path19.join(cacheDir, f));
4903
+ unlinkSync5(path19.join(cacheDir, f));
4856
4904
  }
4857
4905
  }
4858
4906
  }
@@ -5439,7 +5487,7 @@ __export(tasks_exports, {
5439
5487
  writeCheckpoint: () => writeCheckpoint
5440
5488
  });
5441
5489
  import path21 from "path";
5442
- import { writeFileSync as writeFileSync8, mkdirSync as mkdirSync7, unlinkSync as unlinkSync5 } from "fs";
5490
+ import { writeFileSync as writeFileSync8, mkdirSync as mkdirSync7, unlinkSync as unlinkSync6 } from "fs";
5443
5491
  async function createTask(input) {
5444
5492
  const result = await createTaskCore(input);
5445
5493
  if (!input.skipDispatch && result.status !== "blocked" && !process.env.VITEST) {
@@ -5465,7 +5513,7 @@ async function updateTask(input) {
5465
5513
  writeFileSync8(cachePath, JSON.stringify({ taskId, title: String(row.title) }));
5466
5514
  } else if (input.status === "done" || input.status === "blocked" || input.status === "cancelled") {
5467
5515
  try {
5468
- unlinkSync5(cachePath);
5516
+ unlinkSync6(cachePath);
5469
5517
  } catch {
5470
5518
  }
5471
5519
  }
@@ -6762,7 +6810,8 @@ function registerGetSessionContext(server2) {
6762
6810
  has_error, raw_text, vector, task_id
6763
6811
  FROM memories
6764
6812
  WHERE session_id = ?
6765
- ORDER BY timestamp ASC`,
6813
+ ORDER BY timestamp ASC
6814
+ LIMIT 500`,
6766
6815
  args: [session_id]
6767
6816
  });
6768
6817
  if (result.rows.length === 0) {
@@ -8465,12 +8514,20 @@ async function resolveWorkspace(slug_or_id, name) {
8465
8514
  };
8466
8515
  }
8467
8516
  async function embedTexts(texts) {
8468
- const settled = await Promise.allSettled(
8469
- texts.map((text) => embed(text))
8470
- );
8471
- return settled.map(
8472
- (result) => result.status === "fulfilled" ? result.value : null
8473
- );
8517
+ const BATCH_SIZE = 50;
8518
+ const results = [];
8519
+ for (let i = 0; i < texts.length; i += BATCH_SIZE) {
8520
+ const batch = texts.slice(i, i + BATCH_SIZE);
8521
+ const settled = await Promise.allSettled(
8522
+ batch.map((text) => embed(text))
8523
+ );
8524
+ results.push(
8525
+ ...settled.map(
8526
+ (result) => result.status === "fulfilled" ? result.value : null
8527
+ )
8528
+ );
8529
+ }
8530
+ return results;
8474
8531
  }
8475
8532
  function buildChunkStatement(input) {
8476
8533
  const hasVector = input.vector !== null;
@@ -1052,6 +1052,42 @@ import { readFileSync as readFileSync9, writeFileSync as writeFileSync4, mkdirSy
1052
1052
  import path9 from "path";
1053
1053
  import os5 from "os";
1054
1054
  import { fileURLToPath } from "url";
1055
+ import { unlinkSync as unlinkSync2 } from "fs";
1056
+ function spawnLockPath(sessionName) {
1057
+ return path9.join(SPAWN_LOCK_DIR, `${sessionName}.lock`);
1058
+ }
1059
+ function isProcessAlive(pid) {
1060
+ try {
1061
+ process.kill(pid, 0);
1062
+ return true;
1063
+ } catch {
1064
+ return false;
1065
+ }
1066
+ }
1067
+ function acquireSpawnLock(sessionName) {
1068
+ if (!existsSync9(SPAWN_LOCK_DIR)) {
1069
+ mkdirSync4(SPAWN_LOCK_DIR, { recursive: true });
1070
+ }
1071
+ const lockFile = spawnLockPath(sessionName);
1072
+ if (existsSync9(lockFile)) {
1073
+ try {
1074
+ const lock = JSON.parse(readFileSync9(lockFile, "utf8"));
1075
+ const age = Date.now() - lock.timestamp;
1076
+ if (isProcessAlive(lock.pid) && age < 6e4) {
1077
+ return false;
1078
+ }
1079
+ } catch {
1080
+ }
1081
+ }
1082
+ writeFileSync4(lockFile, JSON.stringify({ pid: process.pid, timestamp: Date.now() }));
1083
+ return true;
1084
+ }
1085
+ function releaseSpawnLock(sessionName) {
1086
+ try {
1087
+ unlinkSync2(spawnLockPath(sessionName));
1088
+ } catch {
1089
+ }
1090
+ }
1055
1091
  function resolveBehaviorsExporterScript() {
1056
1092
  try {
1057
1093
  const thisFile = fileURLToPath(import.meta.url);
@@ -1121,10 +1157,10 @@ function isEmployeeAlive(sessionName) {
1121
1157
  }
1122
1158
  function findFreeInstance(employeeName, exeSession, maxInstances = 10, isAlive = isEmployeeAlive) {
1123
1159
  const base = employeeSessionName(employeeName, exeSession);
1124
- if (!isAlive(base)) return 0;
1160
+ if (!isAlive(base) && acquireSpawnLock(base)) return 0;
1125
1161
  for (let i = 2; i <= maxInstances; i++) {
1126
1162
  const candidate = employeeSessionName(employeeName, exeSession, i);
1127
- if (!isAlive(candidate)) return i;
1163
+ if (!isAlive(candidate) && acquireSpawnLock(candidate)) return i;
1128
1164
  }
1129
1165
  return null;
1130
1166
  }
@@ -1436,6 +1472,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
1436
1472
  command: spawnCommand
1437
1473
  });
1438
1474
  if (spawnResult.error) {
1475
+ releaseSpawnLock(sessionName);
1439
1476
  return { sessionName, error: `tmux new-session failed: ${spawnResult.error}` };
1440
1477
  }
1441
1478
  transport.pipeLog(sessionName, logFile);
@@ -1473,6 +1510,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
1473
1510
  }
1474
1511
  }
1475
1512
  if (!booted) {
1513
+ releaseSpawnLock(sessionName);
1476
1514
  return { sessionName, error: `${useExeAgent ? "exe-agent" : "claude"} did not boot within 15s` };
1477
1515
  }
1478
1516
  if (!useExeAgent) {
@@ -1489,9 +1527,10 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
1489
1527
  pid: 0,
1490
1528
  registeredAt: (/* @__PURE__ */ new Date()).toISOString()
1491
1529
  });
1530
+ releaseSpawnLock(sessionName);
1492
1531
  return { sessionName };
1493
1532
  }
1494
- var SESSION_CACHE, BEHAVIORS_EXPORT_TIMEOUT_MS, INTERCOM_DEBOUNCE_MS, INTERCOM_LOG2, DEBOUNCE_FILE, DEBOUNCE_CLEANUP_AGE_MS, BUSY_PATTERN;
1533
+ var SPAWN_LOCK_DIR, SESSION_CACHE, BEHAVIORS_EXPORT_TIMEOUT_MS, INTERCOM_DEBOUNCE_MS, INTERCOM_LOG2, DEBOUNCE_FILE, DEBOUNCE_CLEANUP_AGE_MS, BUSY_PATTERN;
1495
1534
  var init_tmux_routing = __esm({
1496
1535
  "src/lib/tmux-routing.ts"() {
1497
1536
  "use strict";
@@ -1503,6 +1542,7 @@ var init_tmux_routing = __esm({
1503
1542
  init_provider_table();
1504
1543
  init_intercom_queue();
1505
1544
  init_plan_limits();
1545
+ SPAWN_LOCK_DIR = path9.join(os5.homedir(), ".exe-os", "spawn-locks");
1506
1546
  SESSION_CACHE = path9.join(os5.homedir(), ".exe-os", "session-cache");
1507
1547
  BEHAVIORS_EXPORT_TIMEOUT_MS = 1e4;
1508
1548
  INTERCOM_DEBOUNCE_MS = 3e4;
@@ -1515,7 +1555,7 @@ var init_tmux_routing = __esm({
1515
1555
 
1516
1556
  // src/lib/tasks-review.ts
1517
1557
  import path10 from "path";
1518
- import { existsSync as existsSync10, readdirSync as readdirSync2, unlinkSync as unlinkSync2 } from "fs";
1558
+ import { existsSync as existsSync10, readdirSync as readdirSync2, unlinkSync as unlinkSync3 } from "fs";
1519
1559
  var init_tasks_review = __esm({
1520
1560
  "src/lib/tasks-review.ts"() {
1521
1561
  "use strict";
@@ -1698,7 +1738,7 @@ var init_tasks_notify = __esm({
1698
1738
 
1699
1739
  // src/lib/tasks.ts
1700
1740
  import path13 from "path";
1701
- import { writeFileSync as writeFileSync5, mkdirSync as mkdirSync5, unlinkSync as unlinkSync3 } from "fs";
1741
+ import { writeFileSync as writeFileSync5, mkdirSync as mkdirSync5, unlinkSync as unlinkSync4 } from "fs";
1702
1742
  async function createTask(input) {
1703
1743
  const result = await createTaskCore(input);
1704
1744
  if (!input.skipDispatch && result.status !== "blocked" && !process.env.VITEST) {
@@ -1734,7 +1774,7 @@ import { z } from "zod";
1734
1774
 
1735
1775
  // src/adapters/claude/active-agent.ts
1736
1776
  init_config();
1737
- import { readFileSync as readFileSync10, writeFileSync as writeFileSync6, mkdirSync as mkdirSync6, unlinkSync as unlinkSync4, readdirSync as readdirSync3 } from "fs";
1777
+ import { readFileSync as readFileSync10, writeFileSync as writeFileSync6, mkdirSync as mkdirSync6, unlinkSync as unlinkSync5, readdirSync as readdirSync3 } from "fs";
1738
1778
  import { execSync as execSync7 } from "child_process";
1739
1779
  import path14 from "path";
1740
1780
 
@@ -1757,7 +1797,7 @@ function getActiveAgent() {
1757
1797
  const age = Date.now() - new Date(data.startedAt).getTime();
1758
1798
  if (age > STALE_MS) {
1759
1799
  try {
1760
- unlinkSync4(markerPath);
1800
+ unlinkSync5(markerPath);
1761
1801
  } catch {
1762
1802
  }
1763
1803
  } else {
@@ -1780,7 +1820,7 @@ function getActiveAgent() {
1780
1820
  "tmux display-message -p '#{session_name}' 2>/dev/null",
1781
1821
  { encoding: "utf8", timeout: 2e3 }
1782
1822
  ).trim();
1783
- const empMatch = sessionName.match(/^(\w+)-exe\d+$/);
1823
+ const empMatch = sessionName.match(/^([a-zA-Z]+)\d*-exe\d+$/);
1784
1824
  if (empMatch && empMatch[1] !== "exe") {
1785
1825
  return { agentId: empMatch[1], agentRole: "employee" };
1786
1826
  }
@@ -188,7 +188,7 @@ function getActiveAgent() {
188
188
  "tmux display-message -p '#{session_name}' 2>/dev/null",
189
189
  { encoding: "utf8", timeout: 2e3 }
190
190
  ).trim();
191
- const empMatch = sessionName.match(/^(\w+)-exe\d+$/);
191
+ const empMatch = sessionName.match(/^([a-zA-Z]+)\d*-exe\d+$/);
192
192
  if (empMatch && empMatch[1] !== "exe") {
193
193
  return { agentId: empMatch[1], agentRole: "employee" };
194
194
  }
@@ -318,7 +318,7 @@ var init_plan_limits = __esm({
318
318
  import path9 from "path";
319
319
  import os5 from "os";
320
320
  import { fileURLToPath } from "url";
321
- var SESSION_CACHE, INTERCOM_LOG2, DEBOUNCE_FILE, DEBOUNCE_CLEANUP_AGE_MS;
321
+ var SPAWN_LOCK_DIR, SESSION_CACHE, INTERCOM_LOG2, DEBOUNCE_FILE, DEBOUNCE_CLEANUP_AGE_MS;
322
322
  var init_tmux_routing = __esm({
323
323
  "src/lib/tmux-routing.ts"() {
324
324
  "use strict";
@@ -330,6 +330,7 @@ var init_tmux_routing = __esm({
330
330
  init_provider_table();
331
331
  init_intercom_queue();
332
332
  init_plan_limits();
333
+ SPAWN_LOCK_DIR = path9.join(os5.homedir(), ".exe-os", "spawn-locks");
333
334
  SESSION_CACHE = path9.join(os5.homedir(), ".exe-os", "session-cache");
334
335
  INTERCOM_LOG2 = path9.join(os5.homedir(), ".exe-os", "intercom.log");
335
336
  DEBOUNCE_FILE = path9.join(SESSION_CACHE, "intercom-debounce.json");
@@ -559,6 +559,42 @@ import { readFileSync as readFileSync7, writeFileSync as writeFileSync4, mkdirSy
559
559
  import path7 from "path";
560
560
  import os4 from "os";
561
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
+ }
562
598
  function resolveBehaviorsExporterScript() {
563
599
  try {
564
600
  const thisFile = fileURLToPath(import.meta.url);
@@ -628,10 +664,10 @@ function isEmployeeAlive(sessionName) {
628
664
  }
629
665
  function findFreeInstance(employeeName, exeSession, maxInstances = 10, isAlive = isEmployeeAlive) {
630
666
  const base = employeeSessionName(employeeName, exeSession);
631
- if (!isAlive(base)) return 0;
667
+ if (!isAlive(base) && acquireSpawnLock(base)) return 0;
632
668
  for (let i = 2; i <= maxInstances; i++) {
633
669
  const candidate = employeeSessionName(employeeName, exeSession, i);
634
- if (!isAlive(candidate)) return i;
670
+ if (!isAlive(candidate) && acquireSpawnLock(candidate)) return i;
635
671
  }
636
672
  return null;
637
673
  }
@@ -943,6 +979,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
943
979
  command: spawnCommand
944
980
  });
945
981
  if (spawnResult.error) {
982
+ releaseSpawnLock(sessionName);
946
983
  return { sessionName, error: `tmux new-session failed: ${spawnResult.error}` };
947
984
  }
948
985
  transport.pipeLog(sessionName, logFile);
@@ -980,6 +1017,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
980
1017
  }
981
1018
  }
982
1019
  if (!booted) {
1020
+ releaseSpawnLock(sessionName);
983
1021
  return { sessionName, error: `${useExeAgent ? "exe-agent" : "claude"} did not boot within 15s` };
984
1022
  }
985
1023
  if (!useExeAgent) {
@@ -996,9 +1034,10 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
996
1034
  pid: 0,
997
1035
  registeredAt: (/* @__PURE__ */ new Date()).toISOString()
998
1036
  });
1037
+ releaseSpawnLock(sessionName);
999
1038
  return { sessionName };
1000
1039
  }
1001
- 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;
1002
1041
  var init_tmux_routing = __esm({
1003
1042
  "src/lib/tmux-routing.ts"() {
1004
1043
  "use strict";
@@ -1010,6 +1049,7 @@ var init_tmux_routing = __esm({
1010
1049
  init_provider_table();
1011
1050
  init_intercom_queue();
1012
1051
  init_plan_limits();
1052
+ SPAWN_LOCK_DIR = path7.join(os4.homedir(), ".exe-os", "spawn-locks");
1013
1053
  SESSION_CACHE = path7.join(os4.homedir(), ".exe-os", "session-cache");
1014
1054
  BEHAVIORS_EXPORT_TIMEOUT_MS = 1e4;
1015
1055
  INTERCOM_DEBOUNCE_MS = 3e4;
@@ -1166,7 +1206,7 @@ async function markFailed(messageId, reason) {
1166
1206
 
1167
1207
  // src/adapters/claude/active-agent.ts
1168
1208
  init_config();
1169
- import { readFileSync as readFileSync8, writeFileSync as writeFileSync5, mkdirSync as mkdirSync5, unlinkSync, readdirSync } from "fs";
1209
+ import { readFileSync as readFileSync8, writeFileSync as writeFileSync5, mkdirSync as mkdirSync5, unlinkSync as unlinkSync2, readdirSync } from "fs";
1170
1210
  import { execSync as execSync5 } from "child_process";
1171
1211
  import path8 from "path";
1172
1212
 
@@ -1189,7 +1229,7 @@ function getActiveAgent() {
1189
1229
  const age = Date.now() - new Date(data.startedAt).getTime();
1190
1230
  if (age > STALE_MS) {
1191
1231
  try {
1192
- unlinkSync(markerPath);
1232
+ unlinkSync2(markerPath);
1193
1233
  } catch {
1194
1234
  }
1195
1235
  } else {
@@ -1212,7 +1252,7 @@ function getActiveAgent() {
1212
1252
  "tmux display-message -p '#{session_name}' 2>/dev/null",
1213
1253
  { encoding: "utf8", timeout: 2e3 }
1214
1254
  ).trim();
1215
- const empMatch = sessionName.match(/^(\w+)-exe\d+$/);
1255
+ const empMatch = sessionName.match(/^([a-zA-Z]+)\d*-exe\d+$/);
1216
1256
  if (empMatch && empMatch[1] !== "exe") {
1217
1257
  return { agentId: empMatch[1], agentRole: "employee" };
1218
1258
  }
@@ -917,7 +917,7 @@ function notifyParentExe(sessionKey) {
917
917
  }
918
918
  return true;
919
919
  }
920
- var SESSION_CACHE, INTERCOM_DEBOUNCE_MS, INTERCOM_LOG2, DEBOUNCE_FILE, DEBOUNCE_CLEANUP_AGE_MS, BUSY_PATTERN;
920
+ var SPAWN_LOCK_DIR, SESSION_CACHE, INTERCOM_DEBOUNCE_MS, INTERCOM_LOG2, DEBOUNCE_FILE, DEBOUNCE_CLEANUP_AGE_MS, BUSY_PATTERN;
921
921
  var init_tmux_routing = __esm({
922
922
  "src/lib/tmux-routing.ts"() {
923
923
  "use strict";
@@ -929,6 +929,7 @@ var init_tmux_routing = __esm({
929
929
  init_provider_table();
930
930
  init_intercom_queue();
931
931
  init_plan_limits();
932
+ SPAWN_LOCK_DIR = path9.join(os5.homedir(), ".exe-os", "spawn-locks");
932
933
  SESSION_CACHE = path9.join(os5.homedir(), ".exe-os", "session-cache");
933
934
  INTERCOM_DEBOUNCE_MS = 3e4;
934
935
  INTERCOM_LOG2 = path9.join(os5.homedir(), ".exe-os", "intercom.log");
@@ -1608,7 +1609,7 @@ function getActiveAgent() {
1608
1609
  "tmux display-message -p '#{session_name}' 2>/dev/null",
1609
1610
  { encoding: "utf8", timeout: 2e3 }
1610
1611
  ).trim();
1611
- const empMatch = sessionName.match(/^(\w+)-exe\d+$/);
1612
+ const empMatch = sessionName.match(/^([a-zA-Z]+)\d*-exe\d+$/);
1612
1613
  if (empMatch && empMatch[1] !== "exe") {
1613
1614
  return { agentId: empMatch[1], agentRole: "employee" };
1614
1615
  }
@@ -3285,6 +3285,7 @@ var init_capacity_monitor = __esm({
3285
3285
  // src/lib/tmux-routing.ts
3286
3286
  var tmux_routing_exports = {};
3287
3287
  __export(tmux_routing_exports, {
3288
+ acquireSpawnLock: () => acquireSpawnLock,
3288
3289
  employeeSessionName: () => employeeSessionName,
3289
3290
  ensureEmployee: () => ensureEmployee,
3290
3291
  extractRootExe: () => extractRootExe,
@@ -3299,6 +3300,7 @@ __export(tmux_routing_exports, {
3299
3300
  notifyParentExe: () => notifyParentExe,
3300
3301
  parseParentExe: () => parseParentExe,
3301
3302
  registerParentExe: () => registerParentExe,
3303
+ releaseSpawnLock: () => releaseSpawnLock,
3302
3304
  resolveExeSession: () => resolveExeSession,
3303
3305
  sendIntercom: () => sendIntercom,
3304
3306
  spawnEmployee: () => spawnEmployee,
@@ -3309,6 +3311,42 @@ import { readFileSync as readFileSync9, writeFileSync as writeFileSync5, mkdirSy
3309
3311
  import path14 from "path";
3310
3312
  import os6 from "os";
3311
3313
  import { fileURLToPath } from "url";
3314
+ import { unlinkSync as unlinkSync4 } from "fs";
3315
+ function spawnLockPath(sessionName) {
3316
+ return path14.join(SPAWN_LOCK_DIR, `${sessionName}.lock`);
3317
+ }
3318
+ function isProcessAlive(pid) {
3319
+ try {
3320
+ process.kill(pid, 0);
3321
+ return true;
3322
+ } catch {
3323
+ return false;
3324
+ }
3325
+ }
3326
+ function acquireSpawnLock(sessionName) {
3327
+ if (!existsSync10(SPAWN_LOCK_DIR)) {
3328
+ mkdirSync5(SPAWN_LOCK_DIR, { recursive: true });
3329
+ }
3330
+ const lockFile = spawnLockPath(sessionName);
3331
+ if (existsSync10(lockFile)) {
3332
+ try {
3333
+ const lock = JSON.parse(readFileSync9(lockFile, "utf8"));
3334
+ const age = Date.now() - lock.timestamp;
3335
+ if (isProcessAlive(lock.pid) && age < 6e4) {
3336
+ return false;
3337
+ }
3338
+ } catch {
3339
+ }
3340
+ }
3341
+ writeFileSync5(lockFile, JSON.stringify({ pid: process.pid, timestamp: Date.now() }));
3342
+ return true;
3343
+ }
3344
+ function releaseSpawnLock(sessionName) {
3345
+ try {
3346
+ unlinkSync4(spawnLockPath(sessionName));
3347
+ } catch {
3348
+ }
3349
+ }
3312
3350
  function resolveBehaviorsExporterScript() {
3313
3351
  try {
3314
3352
  const thisFile = fileURLToPath(import.meta.url);
@@ -3407,10 +3445,10 @@ function isEmployeeAlive(sessionName) {
3407
3445
  }
3408
3446
  function findFreeInstance(employeeName, exeSession, maxInstances = 10, isAlive = isEmployeeAlive) {
3409
3447
  const base = employeeSessionName(employeeName, exeSession);
3410
- if (!isAlive(base)) return 0;
3448
+ if (!isAlive(base) && acquireSpawnLock(base)) return 0;
3411
3449
  for (let i = 2; i <= maxInstances; i++) {
3412
3450
  const candidate = employeeSessionName(employeeName, exeSession, i);
3413
- if (!isAlive(candidate)) return i;
3451
+ if (!isAlive(candidate) && acquireSpawnLock(candidate)) return i;
3414
3452
  }
3415
3453
  return null;
3416
3454
  }
@@ -3774,6 +3812,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
3774
3812
  command: spawnCommand
3775
3813
  });
3776
3814
  if (spawnResult.error) {
3815
+ releaseSpawnLock(sessionName);
3777
3816
  return { sessionName, error: `tmux new-session failed: ${spawnResult.error}` };
3778
3817
  }
3779
3818
  transport.pipeLog(sessionName, logFile);
@@ -3811,6 +3850,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
3811
3850
  }
3812
3851
  }
3813
3852
  if (!booted) {
3853
+ releaseSpawnLock(sessionName);
3814
3854
  return { sessionName, error: `${useExeAgent ? "exe-agent" : "claude"} did not boot within 15s` };
3815
3855
  }
3816
3856
  if (!useExeAgent) {
@@ -3827,9 +3867,10 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
3827
3867
  pid: 0,
3828
3868
  registeredAt: (/* @__PURE__ */ new Date()).toISOString()
3829
3869
  });
3870
+ releaseSpawnLock(sessionName);
3830
3871
  return { sessionName };
3831
3872
  }
3832
- var SESSION_CACHE, BEHAVIORS_EXPORT_TIMEOUT_MS, VERIFY_PANE_LINES, INTERCOM_DEBOUNCE_MS, INTERCOM_LOG2, DEBOUNCE_FILE, DEBOUNCE_CLEANUP_AGE_MS, BUSY_PATTERN;
3873
+ var SPAWN_LOCK_DIR, SESSION_CACHE, BEHAVIORS_EXPORT_TIMEOUT_MS, VERIFY_PANE_LINES, INTERCOM_DEBOUNCE_MS, INTERCOM_LOG2, DEBOUNCE_FILE, DEBOUNCE_CLEANUP_AGE_MS, BUSY_PATTERN;
3833
3874
  var init_tmux_routing = __esm({
3834
3875
  "src/lib/tmux-routing.ts"() {
3835
3876
  "use strict";
@@ -3841,6 +3882,7 @@ var init_tmux_routing = __esm({
3841
3882
  init_provider_table();
3842
3883
  init_intercom_queue();
3843
3884
  init_plan_limits();
3885
+ SPAWN_LOCK_DIR = path14.join(os6.homedir(), ".exe-os", "spawn-locks");
3844
3886
  SESSION_CACHE = path14.join(os6.homedir(), ".exe-os", "session-cache");
3845
3887
  BEHAVIORS_EXPORT_TIMEOUT_MS = 1e4;
3846
3888
  VERIFY_PANE_LINES = 200;
@@ -4229,7 +4271,8 @@ async function writeMemory(record) {
4229
4271
  has_error: record.has_error ? 1 : 0,
4230
4272
  raw_text: record.raw_text,
4231
4273
  vector: record.vector,
4232
- version: _nextVersion++,
4274
+ version: 0,
4275
+ // Placeholder — assigned atomically at flush time
4233
4276
  task_id: record.task_id ?? null,
4234
4277
  importance: record.importance ?? 5,
4235
4278
  status: record.status ?? "active",
@@ -4263,6 +4306,13 @@ async function flushBatch() {
4263
4306
  _flushing = true;
4264
4307
  try {
4265
4308
  const batch = _pendingRecords.slice(0);
4309
+ const client = getClient();
4310
+ const vResult = await client.execute("SELECT MAX(version) as max_v FROM memories");
4311
+ let baseVersion = (Number(vResult.rows[0]?.max_v) || 0) + 1;
4312
+ for (const row of batch) {
4313
+ row.version = baseVersion++;
4314
+ }
4315
+ _nextVersion = baseVersion;
4266
4316
  const buildStmt = (row) => {
4267
4317
  const hasVector = row.vector !== null;
4268
4318
  const taskId = row.task_id ?? null;