@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
@@ -2924,6 +2924,42 @@ import { readFileSync as readFileSync11, writeFileSync as writeFileSync4, mkdirS
2924
2924
  import path14 from "path";
2925
2925
  import os5 from "os";
2926
2926
  import { fileURLToPath as fileURLToPath2 } from "url";
2927
+ import { unlinkSync as unlinkSync3 } from "fs";
2928
+ function spawnLockPath(sessionName) {
2929
+ return path14.join(SPAWN_LOCK_DIR, `${sessionName}.lock`);
2930
+ }
2931
+ function isProcessAlive(pid) {
2932
+ try {
2933
+ process.kill(pid, 0);
2934
+ return true;
2935
+ } catch {
2936
+ return false;
2937
+ }
2938
+ }
2939
+ function acquireSpawnLock2(sessionName) {
2940
+ if (!existsSync13(SPAWN_LOCK_DIR)) {
2941
+ mkdirSync5(SPAWN_LOCK_DIR, { recursive: true });
2942
+ }
2943
+ const lockFile = spawnLockPath(sessionName);
2944
+ if (existsSync13(lockFile)) {
2945
+ try {
2946
+ const lock = JSON.parse(readFileSync11(lockFile, "utf8"));
2947
+ const age = Date.now() - lock.timestamp;
2948
+ if (isProcessAlive(lock.pid) && age < 6e4) {
2949
+ return false;
2950
+ }
2951
+ } catch {
2952
+ }
2953
+ }
2954
+ writeFileSync4(lockFile, JSON.stringify({ pid: process.pid, timestamp: Date.now() }));
2955
+ return true;
2956
+ }
2957
+ function releaseSpawnLock2(sessionName) {
2958
+ try {
2959
+ unlinkSync3(spawnLockPath(sessionName));
2960
+ } catch {
2961
+ }
2962
+ }
2927
2963
  function resolveBehaviorsExporterScript() {
2928
2964
  try {
2929
2965
  const thisFile = fileURLToPath2(import.meta.url);
@@ -3004,10 +3040,10 @@ function isEmployeeAlive(sessionName) {
3004
3040
  }
3005
3041
  function findFreeInstance(employeeName, exeSession, maxInstances = 10, isAlive = isEmployeeAlive) {
3006
3042
  const base = employeeSessionName(employeeName, exeSession);
3007
- if (!isAlive(base)) return 0;
3043
+ if (!isAlive(base) && acquireSpawnLock2(base)) return 0;
3008
3044
  for (let i = 2; i <= maxInstances; i++) {
3009
3045
  const candidate = employeeSessionName(employeeName, exeSession, i);
3010
- if (!isAlive(candidate)) return i;
3046
+ if (!isAlive(candidate) && acquireSpawnLock2(candidate)) return i;
3011
3047
  }
3012
3048
  return null;
3013
3049
  }
@@ -3341,6 +3377,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
3341
3377
  command: spawnCommand
3342
3378
  });
3343
3379
  if (spawnResult.error) {
3380
+ releaseSpawnLock2(sessionName);
3344
3381
  return { sessionName, error: `tmux new-session failed: ${spawnResult.error}` };
3345
3382
  }
3346
3383
  transport.pipeLog(sessionName, logFile);
@@ -3378,6 +3415,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
3378
3415
  }
3379
3416
  }
3380
3417
  if (!booted) {
3418
+ releaseSpawnLock2(sessionName);
3381
3419
  return { sessionName, error: `${useExeAgent ? "exe-agent" : "claude"} did not boot within 15s` };
3382
3420
  }
3383
3421
  if (!useExeAgent) {
@@ -3394,9 +3432,10 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
3394
3432
  pid: 0,
3395
3433
  registeredAt: (/* @__PURE__ */ new Date()).toISOString()
3396
3434
  });
3435
+ releaseSpawnLock2(sessionName);
3397
3436
  return { sessionName };
3398
3437
  }
3399
- var SESSION_CACHE, BEHAVIORS_EXPORT_TIMEOUT_MS, INTERCOM_DEBOUNCE_MS, INTERCOM_LOG2, DEBOUNCE_FILE, DEBOUNCE_CLEANUP_AGE_MS, BUSY_PATTERN;
3438
+ var SPAWN_LOCK_DIR, SESSION_CACHE, BEHAVIORS_EXPORT_TIMEOUT_MS, INTERCOM_DEBOUNCE_MS, INTERCOM_LOG2, DEBOUNCE_FILE, DEBOUNCE_CLEANUP_AGE_MS, BUSY_PATTERN;
3400
3439
  var init_tmux_routing = __esm({
3401
3440
  "src/lib/tmux-routing.ts"() {
3402
3441
  "use strict";
@@ -3408,6 +3447,7 @@ var init_tmux_routing = __esm({
3408
3447
  init_provider_table();
3409
3448
  init_intercom_queue();
3410
3449
  init_plan_limits();
3450
+ SPAWN_LOCK_DIR = path14.join(os5.homedir(), ".exe-os", "spawn-locks");
3411
3451
  SESSION_CACHE = path14.join(os5.homedir(), ".exe-os", "session-cache");
3412
3452
  BEHAVIORS_EXPORT_TIMEOUT_MS = 1e4;
3413
3453
  INTERCOM_DEBOUNCE_MS = 3e4;
@@ -3420,7 +3460,7 @@ var init_tmux_routing = __esm({
3420
3460
 
3421
3461
  // src/lib/tasks-review.ts
3422
3462
  import path15 from "path";
3423
- import { existsSync as existsSync14, readdirSync as readdirSync3, unlinkSync as unlinkSync3 } from "fs";
3463
+ import { existsSync as existsSync14, readdirSync as readdirSync3, unlinkSync as unlinkSync4 } from "fs";
3424
3464
  async function countPendingReviews() {
3425
3465
  const client = getClient();
3426
3466
  const result = await client.execute({
@@ -3544,7 +3584,7 @@ async function cleanupReviewFile(row, taskFile, _baseDir) {
3544
3584
  if (existsSync14(cacheDir)) {
3545
3585
  for (const f of readdirSync3(cacheDir)) {
3546
3586
  if (f.startsWith("review-notified-")) {
3547
- unlinkSync3(path15.join(cacheDir, f));
3587
+ unlinkSync4(path15.join(cacheDir, f));
3548
3588
  }
3549
3589
  }
3550
3590
  }
@@ -4103,7 +4143,7 @@ __export(tasks_exports, {
4103
4143
  writeCheckpoint: () => writeCheckpoint
4104
4144
  });
4105
4145
  import path17 from "path";
4106
- import { writeFileSync as writeFileSync5, mkdirSync as mkdirSync6, unlinkSync as unlinkSync4 } from "fs";
4146
+ import { writeFileSync as writeFileSync5, mkdirSync as mkdirSync6, unlinkSync as unlinkSync5 } from "fs";
4107
4147
  async function createTask(input2) {
4108
4148
  const result = await createTaskCore(input2);
4109
4149
  if (!input2.skipDispatch && result.status !== "blocked" && !process.env.VITEST) {
@@ -4129,7 +4169,7 @@ async function updateTask(input2) {
4129
4169
  writeFileSync5(cachePath, JSON.stringify({ taskId, title: String(row.title) }));
4130
4170
  } else if (input2.status === "done" || input2.status === "blocked" || input2.status === "cancelled") {
4131
4171
  try {
4132
- unlinkSync4(cachePath);
4172
+ unlinkSync5(cachePath);
4133
4173
  } catch {
4134
4174
  }
4135
4175
  }
@@ -4464,7 +4504,8 @@ async function writeMemory(record) {
4464
4504
  has_error: record.has_error ? 1 : 0,
4465
4505
  raw_text: record.raw_text,
4466
4506
  vector: record.vector,
4467
- version: _nextVersion++,
4507
+ version: 0,
4508
+ // Placeholder — assigned atomically at flush time
4468
4509
  task_id: record.task_id ?? null,
4469
4510
  importance: record.importance ?? 5,
4470
4511
  status: record.status ?? "active",
@@ -4498,6 +4539,13 @@ async function flushBatch() {
4498
4539
  _flushing = true;
4499
4540
  try {
4500
4541
  const batch = _pendingRecords.slice(0);
4542
+ const client = getClient();
4543
+ const vResult = await client.execute("SELECT MAX(version) as max_v FROM memories");
4544
+ let baseVersion = (Number(vResult.rows[0]?.max_v) || 0) + 1;
4545
+ for (const row of batch) {
4546
+ row.version = baseVersion++;
4547
+ }
4548
+ _nextVersion = baseVersion;
4501
4549
  const buildStmt = (row) => {
4502
4550
  const hasVector = row.vector !== null;
4503
4551
  const taskId = row.task_id ?? null;
@@ -247,7 +247,7 @@ function getActiveAgent() {
247
247
  "tmux display-message -p '#{session_name}' 2>/dev/null",
248
248
  { encoding: "utf8", timeout: 2e3 }
249
249
  ).trim();
250
- const empMatch = sessionName.match(/^(\w+)-exe\d+$/);
250
+ const empMatch = sessionName.match(/^([a-zA-Z]+)\d*-exe\d+$/);
251
251
  if (empMatch && empMatch[1] !== "exe") {
252
252
  return { agentId: empMatch[1], agentRole: "employee" };
253
253
  }
@@ -1489,7 +1489,8 @@ async function writeMemory(record) {
1489
1489
  has_error: record.has_error ? 1 : 0,
1490
1490
  raw_text: record.raw_text,
1491
1491
  vector: record.vector,
1492
- version: _nextVersion++,
1492
+ version: 0,
1493
+ // Placeholder — assigned atomically at flush time
1493
1494
  task_id: record.task_id ?? null,
1494
1495
  importance: record.importance ?? 5,
1495
1496
  status: record.status ?? "active",
@@ -1523,6 +1524,13 @@ async function flushBatch() {
1523
1524
  _flushing = true;
1524
1525
  try {
1525
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;
1526
1534
  const buildStmt = (row) => {
1527
1535
  const hasVector = row.vector !== null;
1528
1536
  const taskId = row.task_id ?? null;
@@ -1921,7 +1929,7 @@ function getActiveAgent() {
1921
1929
  "tmux display-message -p '#{session_name}' 2>/dev/null",
1922
1930
  { encoding: "utf8", timeout: 2e3 }
1923
1931
  ).trim();
1924
- const empMatch = sessionName.match(/^(\w+)-exe\d+$/);
1932
+ const empMatch = sessionName.match(/^([a-zA-Z]+)\d*-exe\d+$/);
1925
1933
  if (empMatch && empMatch[1] !== "exe") {
1926
1934
  return { agentId: empMatch[1], agentRole: "employee" };
1927
1935
  }
@@ -1489,7 +1489,8 @@ async function writeMemory(record) {
1489
1489
  has_error: record.has_error ? 1 : 0,
1490
1490
  raw_text: record.raw_text,
1491
1491
  vector: record.vector,
1492
- version: _nextVersion++,
1492
+ version: 0,
1493
+ // Placeholder — assigned atomically at flush time
1493
1494
  task_id: record.task_id ?? null,
1494
1495
  importance: record.importance ?? 5,
1495
1496
  status: record.status ?? "active",
@@ -1523,6 +1524,13 @@ async function flushBatch() {
1523
1524
  _flushing = true;
1524
1525
  try {
1525
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;
1526
1534
  const buildStmt = (row) => {
1527
1535
  const hasVector = row.vector !== null;
1528
1536
  const taskId = row.task_id ?? null;
@@ -1921,7 +1929,7 @@ function getActiveAgent() {
1921
1929
  "tmux display-message -p '#{session_name}' 2>/dev/null",
1922
1930
  { encoding: "utf8", timeout: 2e3 }
1923
1931
  ).trim();
1924
- const empMatch = sessionName.match(/^(\w+)-exe\d+$/);
1932
+ const empMatch = sessionName.match(/^([a-zA-Z]+)\d*-exe\d+$/);
1925
1933
  if (empMatch && empMatch[1] !== "exe") {
1926
1934
  return { agentId: empMatch[1], agentRole: "employee" };
1927
1935
  }
@@ -1489,7 +1489,8 @@ async function writeMemory(record) {
1489
1489
  has_error: record.has_error ? 1 : 0,
1490
1490
  raw_text: record.raw_text,
1491
1491
  vector: record.vector,
1492
- version: _nextVersion++,
1492
+ version: 0,
1493
+ // Placeholder — assigned atomically at flush time
1493
1494
  task_id: record.task_id ?? null,
1494
1495
  importance: record.importance ?? 5,
1495
1496
  status: record.status ?? "active",
@@ -1523,6 +1524,13 @@ async function flushBatch() {
1523
1524
  _flushing = true;
1524
1525
  try {
1525
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;
1526
1534
  const buildStmt = (row) => {
1527
1535
  const hasVector = row.vector !== null;
1528
1536
  const taskId = row.task_id ?? null;
@@ -1921,7 +1929,7 @@ function getActiveAgent() {
1921
1929
  "tmux display-message -p '#{session_name}' 2>/dev/null",
1922
1930
  { encoding: "utf8", timeout: 2e3 }
1923
1931
  ).trim();
1924
- const empMatch = sessionName.match(/^(\w+)-exe\d+$/);
1932
+ const empMatch = sessionName.match(/^([a-zA-Z]+)\d*-exe\d+$/);
1925
1933
  if (empMatch && empMatch[1] !== "exe") {
1926
1934
  return { agentId: empMatch[1], agentRole: "employee" };
1927
1935
  }
@@ -1489,7 +1489,8 @@ async function writeMemory(record) {
1489
1489
  has_error: record.has_error ? 1 : 0,
1490
1490
  raw_text: record.raw_text,
1491
1491
  vector: record.vector,
1492
- version: _nextVersion++,
1492
+ version: 0,
1493
+ // Placeholder — assigned atomically at flush time
1493
1494
  task_id: record.task_id ?? null,
1494
1495
  importance: record.importance ?? 5,
1495
1496
  status: record.status ?? "active",
@@ -1523,6 +1524,13 @@ async function flushBatch() {
1523
1524
  _flushing = true;
1524
1525
  try {
1525
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;
1526
1534
  const buildStmt = (row) => {
1527
1535
  const hasVector = row.vector !== null;
1528
1536
  const taskId = row.task_id ?? null;
@@ -1921,7 +1929,7 @@ function getActiveAgent() {
1921
1929
  "tmux display-message -p '#{session_name}' 2>/dev/null",
1922
1930
  { encoding: "utf8", timeout: 2e3 }
1923
1931
  ).trim();
1924
- const empMatch = sessionName.match(/^(\w+)-exe\d+$/);
1932
+ const empMatch = sessionName.match(/^([a-zA-Z]+)\d*-exe\d+$/);
1925
1933
  if (empMatch && empMatch[1] !== "exe") {
1926
1934
  return { agentId: empMatch[1], agentRole: "employee" };
1927
1935
  }
@@ -1793,7 +1793,8 @@ async function writeMemory(record) {
1793
1793
  has_error: record.has_error ? 1 : 0,
1794
1794
  raw_text: record.raw_text,
1795
1795
  vector: record.vector,
1796
- version: _nextVersion++,
1796
+ version: 0,
1797
+ // Placeholder — assigned atomically at flush time
1797
1798
  task_id: record.task_id ?? null,
1798
1799
  importance: record.importance ?? 5,
1799
1800
  status: record.status ?? "active",
@@ -1827,6 +1828,13 @@ async function flushBatch() {
1827
1828
  _flushing = true;
1828
1829
  try {
1829
1830
  const batch = _pendingRecords.slice(0);
1831
+ const client = getClient();
1832
+ const vResult = await client.execute("SELECT MAX(version) as max_v FROM memories");
1833
+ let baseVersion = (Number(vResult.rows[0]?.max_v) || 0) + 1;
1834
+ for (const row of batch) {
1835
+ row.version = baseVersion++;
1836
+ }
1837
+ _nextVersion = baseVersion;
1830
1838
  const buildStmt = (row) => {
1831
1839
  const hasVector = row.vector !== null;
1832
1840
  const taskId = row.task_id ?? null;
@@ -2356,7 +2364,7 @@ function getActiveAgent() {
2356
2364
  "tmux display-message -p '#{session_name}' 2>/dev/null",
2357
2365
  { encoding: "utf8", timeout: 2e3 }
2358
2366
  ).trim();
2359
- const empMatch = sessionName.match(/^(\w+)-exe\d+$/);
2367
+ const empMatch = sessionName.match(/^([a-zA-Z]+)\d*-exe\d+$/);
2360
2368
  if (empMatch && empMatch[1] !== "exe") {
2361
2369
  return { agentId: empMatch[1], agentRole: "employee" };
2362
2370
  }
@@ -1946,7 +1946,8 @@ async function writeMemory(record) {
1946
1946
  has_error: record.has_error ? 1 : 0,
1947
1947
  raw_text: record.raw_text,
1948
1948
  vector: record.vector,
1949
- version: _nextVersion++,
1949
+ version: 0,
1950
+ // Placeholder — assigned atomically at flush time
1950
1951
  task_id: record.task_id ?? null,
1951
1952
  importance: record.importance ?? 5,
1952
1953
  status: record.status ?? "active",
@@ -1980,6 +1981,13 @@ async function flushBatch() {
1980
1981
  _flushing = true;
1981
1982
  try {
1982
1983
  const batch = _pendingRecords.slice(0);
1984
+ const client = getClient();
1985
+ const vResult = await client.execute("SELECT MAX(version) as max_v FROM memories");
1986
+ let baseVersion = (Number(vResult.rows[0]?.max_v) || 0) + 1;
1987
+ for (const row of batch) {
1988
+ row.version = baseVersion++;
1989
+ }
1990
+ _nextVersion = baseVersion;
1983
1991
  const buildStmt = (row) => {
1984
1992
  const hasVector = row.vector !== null;
1985
1993
  const taskId = row.task_id ?? null;
@@ -1540,7 +1540,8 @@ async function writeMemory(record) {
1540
1540
  has_error: record.has_error ? 1 : 0,
1541
1541
  raw_text: record.raw_text,
1542
1542
  vector: record.vector,
1543
- version: _nextVersion++,
1543
+ version: 0,
1544
+ // Placeholder — assigned atomically at flush time
1544
1545
  task_id: record.task_id ?? null,
1545
1546
  importance: record.importance ?? 5,
1546
1547
  status: record.status ?? "active",
@@ -1574,6 +1575,13 @@ async function flushBatch() {
1574
1575
  _flushing = true;
1575
1576
  try {
1576
1577
  const batch = _pendingRecords.slice(0);
1578
+ const client = getClient();
1579
+ const vResult = await client.execute("SELECT MAX(version) as max_v FROM memories");
1580
+ let baseVersion = (Number(vResult.rows[0]?.max_v) || 0) + 1;
1581
+ for (const row of batch) {
1582
+ row.version = baseVersion++;
1583
+ }
1584
+ _nextVersion = baseVersion;
1577
1585
  const buildStmt = (row) => {
1578
1586
  const hasVector = row.vector !== null;
1579
1587
  const taskId = row.task_id ?? null;
@@ -3281,6 +3289,42 @@ import { readFileSync as readFileSync10, writeFileSync as writeFileSync5, mkdirS
3281
3289
  import path14 from "path";
3282
3290
  import os4 from "os";
3283
3291
  import { fileURLToPath as fileURLToPath2 } from "url";
3292
+ import { unlinkSync as unlinkSync3 } from "fs";
3293
+ function spawnLockPath(sessionName) {
3294
+ return path14.join(SPAWN_LOCK_DIR, `${sessionName}.lock`);
3295
+ }
3296
+ function isProcessAlive(pid) {
3297
+ try {
3298
+ process.kill(pid, 0);
3299
+ return true;
3300
+ } catch {
3301
+ return false;
3302
+ }
3303
+ }
3304
+ function acquireSpawnLock2(sessionName) {
3305
+ if (!existsSync12(SPAWN_LOCK_DIR)) {
3306
+ mkdirSync6(SPAWN_LOCK_DIR, { recursive: true });
3307
+ }
3308
+ const lockFile = spawnLockPath(sessionName);
3309
+ if (existsSync12(lockFile)) {
3310
+ try {
3311
+ const lock = JSON.parse(readFileSync10(lockFile, "utf8"));
3312
+ const age = Date.now() - lock.timestamp;
3313
+ if (isProcessAlive(lock.pid) && age < 6e4) {
3314
+ return false;
3315
+ }
3316
+ } catch {
3317
+ }
3318
+ }
3319
+ writeFileSync5(lockFile, JSON.stringify({ pid: process.pid, timestamp: Date.now() }));
3320
+ return true;
3321
+ }
3322
+ function releaseSpawnLock2(sessionName) {
3323
+ try {
3324
+ unlinkSync3(spawnLockPath(sessionName));
3325
+ } catch {
3326
+ }
3327
+ }
3284
3328
  function resolveBehaviorsExporterScript() {
3285
3329
  try {
3286
3330
  const thisFile = fileURLToPath2(import.meta.url);
@@ -3350,10 +3394,10 @@ function isEmployeeAlive(sessionName) {
3350
3394
  }
3351
3395
  function findFreeInstance(employeeName, exeSession, maxInstances = 10, isAlive = isEmployeeAlive) {
3352
3396
  const base = employeeSessionName(employeeName, exeSession);
3353
- if (!isAlive(base)) return 0;
3397
+ if (!isAlive(base) && acquireSpawnLock2(base)) return 0;
3354
3398
  for (let i = 2; i <= maxInstances; i++) {
3355
3399
  const candidate = employeeSessionName(employeeName, exeSession, i);
3356
- if (!isAlive(candidate)) return i;
3400
+ if (!isAlive(candidate) && acquireSpawnLock2(candidate)) return i;
3357
3401
  }
3358
3402
  return null;
3359
3403
  }
@@ -3665,6 +3709,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
3665
3709
  command: spawnCommand
3666
3710
  });
3667
3711
  if (spawnResult.error) {
3712
+ releaseSpawnLock2(sessionName);
3668
3713
  return { sessionName, error: `tmux new-session failed: ${spawnResult.error}` };
3669
3714
  }
3670
3715
  transport.pipeLog(sessionName, logFile);
@@ -3702,6 +3747,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
3702
3747
  }
3703
3748
  }
3704
3749
  if (!booted) {
3750
+ releaseSpawnLock2(sessionName);
3705
3751
  return { sessionName, error: `${useExeAgent ? "exe-agent" : "claude"} did not boot within 15s` };
3706
3752
  }
3707
3753
  if (!useExeAgent) {
@@ -3718,9 +3764,10 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
3718
3764
  pid: 0,
3719
3765
  registeredAt: (/* @__PURE__ */ new Date()).toISOString()
3720
3766
  });
3767
+ releaseSpawnLock2(sessionName);
3721
3768
  return { sessionName };
3722
3769
  }
3723
- var SESSION_CACHE, BEHAVIORS_EXPORT_TIMEOUT_MS, INTERCOM_DEBOUNCE_MS, INTERCOM_LOG2, DEBOUNCE_FILE, DEBOUNCE_CLEANUP_AGE_MS, BUSY_PATTERN;
3770
+ var SPAWN_LOCK_DIR, SESSION_CACHE, BEHAVIORS_EXPORT_TIMEOUT_MS, INTERCOM_DEBOUNCE_MS, INTERCOM_LOG2, DEBOUNCE_FILE, DEBOUNCE_CLEANUP_AGE_MS, BUSY_PATTERN;
3724
3771
  var init_tmux_routing = __esm({
3725
3772
  "src/lib/tmux-routing.ts"() {
3726
3773
  "use strict";
@@ -3732,6 +3779,7 @@ var init_tmux_routing = __esm({
3732
3779
  init_provider_table();
3733
3780
  init_intercom_queue();
3734
3781
  init_plan_limits();
3782
+ SPAWN_LOCK_DIR = path14.join(os4.homedir(), ".exe-os", "spawn-locks");
3735
3783
  SESSION_CACHE = path14.join(os4.homedir(), ".exe-os", "session-cache");
3736
3784
  BEHAVIORS_EXPORT_TIMEOUT_MS = 1e4;
3737
3785
  INTERCOM_DEBOUNCE_MS = 3e4;
@@ -3999,7 +4047,7 @@ import os5 from "os";
3999
4047
  import {
4000
4048
  readFileSync as readFileSync11,
4001
4049
  readdirSync as readdirSync3,
4002
- unlinkSync as unlinkSync3,
4050
+ unlinkSync as unlinkSync4,
4003
4051
  existsSync as existsSync13,
4004
4052
  rmdirSync
4005
4053
  } from "fs";
@@ -4256,7 +4304,7 @@ __export(tasks_review_exports, {
4256
4304
  listPendingReviews: () => listPendingReviews
4257
4305
  });
4258
4306
  import path17 from "path";
4259
- import { existsSync as existsSync15, readdirSync as readdirSync4, unlinkSync as unlinkSync4 } from "fs";
4307
+ import { existsSync as existsSync15, readdirSync as readdirSync4, unlinkSync as unlinkSync5 } from "fs";
4260
4308
  async function countPendingReviews() {
4261
4309
  const client = getClient();
4262
4310
  const result = await client.execute({
@@ -4470,7 +4518,7 @@ async function cleanupReviewFile(row, taskFile, _baseDir) {
4470
4518
  if (existsSync15(cacheDir)) {
4471
4519
  for (const f of readdirSync4(cacheDir)) {
4472
4520
  if (f.startsWith("review-notified-")) {
4473
- unlinkSync4(path17.join(cacheDir, f));
4521
+ unlinkSync5(path17.join(cacheDir, f));
4474
4522
  }
4475
4523
  }
4476
4524
  }
@@ -4904,7 +4952,7 @@ function getActiveAgent() {
4904
4952
  "tmux display-message -p '#{session_name}' 2>/dev/null",
4905
4953
  { encoding: "utf8", timeout: 2e3 }
4906
4954
  ).trim();
4907
- const empMatch = sessionName.match(/^(\w+)-exe\d+$/);
4955
+ const empMatch = sessionName.match(/^([a-zA-Z]+)\d*-exe\d+$/);
4908
4956
  if (empMatch && empMatch[1] !== "exe") {
4909
4957
  return { agentId: empMatch[1], agentRole: "employee" };
4910
4958
  }
@@ -2272,7 +2272,8 @@ async function writeMemory(record) {
2272
2272
  has_error: record.has_error ? 1 : 0,
2273
2273
  raw_text: record.raw_text,
2274
2274
  vector: record.vector,
2275
- version: _nextVersion++,
2275
+ version: 0,
2276
+ // Placeholder — assigned atomically at flush time
2276
2277
  task_id: record.task_id ?? null,
2277
2278
  importance: record.importance ?? 5,
2278
2279
  status: record.status ?? "active",
@@ -2306,6 +2307,13 @@ async function flushBatch() {
2306
2307
  _flushing = true;
2307
2308
  try {
2308
2309
  const batch = _pendingRecords.slice(0);
2310
+ const client = getClient();
2311
+ const vResult = await client.execute("SELECT MAX(version) as max_v FROM memories");
2312
+ let baseVersion = (Number(vResult.rows[0]?.max_v) || 0) + 1;
2313
+ for (const row of batch) {
2314
+ row.version = baseVersion++;
2315
+ }
2316
+ _nextVersion = baseVersion;
2309
2317
  const buildStmt = (row) => {
2310
2318
  const hasVector = row.vector !== null;
2311
2319
  const taskId = row.task_id ?? null;
@@ -1489,7 +1489,8 @@ async function writeMemory(record) {
1489
1489
  has_error: record.has_error ? 1 : 0,
1490
1490
  raw_text: record.raw_text,
1491
1491
  vector: record.vector,
1492
- version: _nextVersion++,
1492
+ version: 0,
1493
+ // Placeholder — assigned atomically at flush time
1493
1494
  task_id: record.task_id ?? null,
1494
1495
  importance: record.importance ?? 5,
1495
1496
  status: record.status ?? "active",
@@ -1523,6 +1524,13 @@ async function flushBatch() {
1523
1524
  _flushing = true;
1524
1525
  try {
1525
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;
1526
1534
  const buildStmt = (row) => {
1527
1535
  const hasVector = row.vector !== null;
1528
1536
  const taskId = row.task_id ?? null;
@@ -2162,7 +2170,7 @@ function getActiveAgent() {
2162
2170
  "tmux display-message -p '#{session_name}' 2>/dev/null",
2163
2171
  { encoding: "utf8", timeout: 2e3 }
2164
2172
  ).trim();
2165
- const empMatch = sessionName.match(/^(\w+)-exe\d+$/);
2173
+ const empMatch = sessionName.match(/^([a-zA-Z]+)\d*-exe\d+$/);
2166
2174
  if (empMatch && empMatch[1] !== "exe") {
2167
2175
  return { agentId: empMatch[1], agentRole: "employee" };
2168
2176
  }
@@ -1529,7 +1529,8 @@ async function writeMemory(record) {
1529
1529
  has_error: record.has_error ? 1 : 0,
1530
1530
  raw_text: record.raw_text,
1531
1531
  vector: record.vector,
1532
- version: _nextVersion++,
1532
+ version: 0,
1533
+ // Placeholder — assigned atomically at flush time
1533
1534
  task_id: record.task_id ?? null,
1534
1535
  importance: record.importance ?? 5,
1535
1536
  status: record.status ?? "active",
@@ -1563,6 +1564,13 @@ async function flushBatch() {
1563
1564
  _flushing = true;
1564
1565
  try {
1565
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;
1566
1574
  const buildStmt = (row) => {
1567
1575
  const hasVector = row.vector !== null;
1568
1576
  const taskId = row.task_id ?? null;
@@ -3201,7 +3209,7 @@ function getActiveAgent() {
3201
3209
  "tmux display-message -p '#{session_name}' 2>/dev/null",
3202
3210
  { encoding: "utf8", timeout: 2e3 }
3203
3211
  ).trim();
3204
- const empMatch = sessionName.match(/^(\w+)-exe\d+$/);
3212
+ const empMatch = sessionName.match(/^([a-zA-Z]+)\d*-exe\d+$/);
3205
3213
  if (empMatch && empMatch[1] !== "exe") {
3206
3214
  return { agentId: empMatch[1], agentRole: "employee" };
3207
3215
  }
@@ -1508,7 +1508,8 @@ async function writeMemory(record) {
1508
1508
  has_error: record.has_error ? 1 : 0,
1509
1509
  raw_text: record.raw_text,
1510
1510
  vector: record.vector,
1511
- version: _nextVersion++,
1511
+ version: 0,
1512
+ // Placeholder — assigned atomically at flush time
1512
1513
  task_id: record.task_id ?? null,
1513
1514
  importance: record.importance ?? 5,
1514
1515
  status: record.status ?? "active",
@@ -1542,6 +1543,13 @@ async function flushBatch() {
1542
1543
  _flushing = true;
1543
1544
  try {
1544
1545
  const batch = _pendingRecords.slice(0);
1546
+ const client = getClient();
1547
+ const vResult = await client.execute("SELECT MAX(version) as max_v FROM memories");
1548
+ let baseVersion = (Number(vResult.rows[0]?.max_v) || 0) + 1;
1549
+ for (const row of batch) {
1550
+ row.version = baseVersion++;
1551
+ }
1552
+ _nextVersion = baseVersion;
1545
1553
  const buildStmt = (row) => {
1546
1554
  const hasVector = row.vector !== null;
1547
1555
  const taskId = row.task_id ?? null;
@@ -1948,7 +1956,7 @@ function getActiveAgent() {
1948
1956
  "tmux display-message -p '#{session_name}' 2>/dev/null",
1949
1957
  { encoding: "utf8", timeout: 2e3 }
1950
1958
  ).trim();
1951
- const empMatch = sessionName.match(/^(\w+)-exe\d+$/);
1959
+ const empMatch = sessionName.match(/^([a-zA-Z]+)\d*-exe\d+$/);
1952
1960
  if (empMatch && empMatch[1] !== "exe") {
1953
1961
  return { agentId: empMatch[1], agentRole: "employee" };
1954
1962
  }
@@ -1489,7 +1489,8 @@ async function writeMemory(record) {
1489
1489
  has_error: record.has_error ? 1 : 0,
1490
1490
  raw_text: record.raw_text,
1491
1491
  vector: record.vector,
1492
- version: _nextVersion++,
1492
+ version: 0,
1493
+ // Placeholder — assigned atomically at flush time
1493
1494
  task_id: record.task_id ?? null,
1494
1495
  importance: record.importance ?? 5,
1495
1496
  status: record.status ?? "active",
@@ -1523,6 +1524,13 @@ async function flushBatch() {
1523
1524
  _flushing = true;
1524
1525
  try {
1525
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;
1526
1534
  const buildStmt = (row) => {
1527
1535
  const hasVector = row.vector !== null;
1528
1536
  const taskId = row.task_id ?? null;
@@ -1921,7 +1929,7 @@ function getActiveAgent() {
1921
1929
  "tmux display-message -p '#{session_name}' 2>/dev/null",
1922
1930
  { encoding: "utf8", timeout: 2e3 }
1923
1931
  ).trim();
1924
- const empMatch = sessionName.match(/^(\w+)-exe\d+$/);
1932
+ const empMatch = sessionName.match(/^([a-zA-Z]+)\d*-exe\d+$/);
1925
1933
  if (empMatch && empMatch[1] !== "exe") {
1926
1934
  return { agentId: empMatch[1], agentRole: "employee" };
1927
1935
  }