@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
@@ -2510,7 +2510,7 @@ function sendIntercom(targetSession) {
2510
2510
  return "failed";
2511
2511
  }
2512
2512
  }
2513
- var SESSION_CACHE, INTERCOM_DEBOUNCE_MS, INTERCOM_LOG2, DEBOUNCE_FILE, DEBOUNCE_CLEANUP_AGE_MS, BUSY_PATTERN;
2513
+ var SPAWN_LOCK_DIR, SESSION_CACHE, INTERCOM_DEBOUNCE_MS, INTERCOM_LOG2, DEBOUNCE_FILE, DEBOUNCE_CLEANUP_AGE_MS, BUSY_PATTERN;
2514
2514
  var init_tmux_routing = __esm({
2515
2515
  "src/lib/tmux-routing.ts"() {
2516
2516
  "use strict";
@@ -2522,6 +2522,7 @@ var init_tmux_routing = __esm({
2522
2522
  init_provider_table();
2523
2523
  init_intercom_queue();
2524
2524
  init_plan_limits();
2525
+ SPAWN_LOCK_DIR = path13.join(os5.homedir(), ".exe-os", "spawn-locks");
2525
2526
  SESSION_CACHE = path13.join(os5.homedir(), ".exe-os", "session-cache");
2526
2527
  INTERCOM_DEBOUNCE_MS = 3e4;
2527
2528
  INTERCOM_LOG2 = path13.join(os5.homedir(), ".exe-os", "intercom.log");
@@ -3106,7 +3107,8 @@ async function writeMemory(record) {
3106
3107
  has_error: record.has_error ? 1 : 0,
3107
3108
  raw_text: record.raw_text,
3108
3109
  vector: record.vector,
3109
- version: _nextVersion++,
3110
+ version: 0,
3111
+ // Placeholder — assigned atomically at flush time
3110
3112
  task_id: record.task_id ?? null,
3111
3113
  importance: record.importance ?? 5,
3112
3114
  status: record.status ?? "active",
@@ -3140,6 +3142,13 @@ async function flushBatch() {
3140
3142
  _flushing = true;
3141
3143
  try {
3142
3144
  const batch = _pendingRecords.slice(0);
3145
+ const client = getClient();
3146
+ const vResult = await client.execute("SELECT MAX(version) as max_v FROM memories");
3147
+ let baseVersion = (Number(vResult.rows[0]?.max_v) || 0) + 1;
3148
+ for (const row of batch) {
3149
+ row.version = baseVersion++;
3150
+ }
3151
+ _nextVersion = baseVersion;
3143
3152
  const buildStmt = (row) => {
3144
3153
  const hasVector = row.vector !== null;
3145
3154
  const taskId = row.task_id ?? null;
@@ -1490,7 +1490,8 @@ async function writeMemory(record) {
1490
1490
  has_error: record.has_error ? 1 : 0,
1491
1491
  raw_text: record.raw_text,
1492
1492
  vector: record.vector,
1493
- version: _nextVersion++,
1493
+ version: 0,
1494
+ // Placeholder — assigned atomically at flush time
1494
1495
  task_id: record.task_id ?? null,
1495
1496
  importance: record.importance ?? 5,
1496
1497
  status: record.status ?? "active",
@@ -1524,6 +1525,13 @@ async function flushBatch() {
1524
1525
  _flushing = true;
1525
1526
  try {
1526
1527
  const batch = _pendingRecords.slice(0);
1528
+ const client = getClient();
1529
+ const vResult = await client.execute("SELECT MAX(version) as max_v FROM memories");
1530
+ let baseVersion = (Number(vResult.rows[0]?.max_v) || 0) + 1;
1531
+ for (const row of batch) {
1532
+ row.version = baseVersion++;
1533
+ }
1534
+ _nextVersion = baseVersion;
1527
1535
  const buildStmt = (row) => {
1528
1536
  const hasVector = row.vector !== null;
1529
1537
  const taskId = row.task_id ?? null;
@@ -1424,6 +1424,13 @@ async function flushBatch() {
1424
1424
  _flushing = true;
1425
1425
  try {
1426
1426
  const batch = _pendingRecords.slice(0);
1427
+ const client = getClient();
1428
+ const vResult = await client.execute("SELECT MAX(version) as max_v FROM memories");
1429
+ let baseVersion = (Number(vResult.rows[0]?.max_v) || 0) + 1;
1430
+ for (const row of batch) {
1431
+ row.version = baseVersion++;
1432
+ }
1433
+ _nextVersion = baseVersion;
1427
1434
  const buildStmt = (row) => {
1428
1435
  const hasVector = row.vector !== null;
1429
1436
  const taskId = row.task_id ?? null;
@@ -1428,6 +1428,13 @@ async function flushBatch() {
1428
1428
  _flushing = true;
1429
1429
  try {
1430
1430
  const batch = _pendingRecords.slice(0);
1431
+ const client = getClient();
1432
+ const vResult = await client.execute("SELECT MAX(version) as max_v FROM memories");
1433
+ let baseVersion = (Number(vResult.rows[0]?.max_v) || 0) + 1;
1434
+ for (const row of batch) {
1435
+ row.version = baseVersion++;
1436
+ }
1437
+ _nextVersion = baseVersion;
1431
1438
  const buildStmt = (row) => {
1432
1439
  const hasVector = row.vector !== null;
1433
1440
  const taskId = row.task_id ?? null;
@@ -501,26 +501,56 @@ async function mergeHooks(packageRoot, homeDir = os3.homedir()) {
501
501
  permissions.allow = [];
502
502
  }
503
503
  const toolNames = [
504
+ // Core memory
504
505
  "store_memory",
505
506
  "recall_my_memory",
507
+ "commit_to_long_term_memory",
508
+ "consolidate_memories",
509
+ "ask_team_memory",
510
+ "get_session_context",
511
+ // Tasks
506
512
  "create_task",
507
513
  "list_tasks",
508
514
  "get_task",
509
515
  "update_task",
510
516
  "close_task",
517
+ "checkpoint_task",
518
+ // Behaviors
511
519
  "store_behavior",
512
520
  "deactivate_behavior",
513
- "send_message",
521
+ "list_behaviors",
522
+ // Identity
514
523
  "get_identity",
515
524
  "update_identity",
516
- "ask_team_memory",
517
- "get_session_context",
525
+ // Messaging
526
+ "send_message",
527
+ "acknowledge_messages",
528
+ "send_whatsapp",
529
+ "query_conversations",
530
+ // Reminders + triggers
518
531
  "create_reminder",
519
532
  "complete_reminder",
520
533
  "list_reminders",
521
- "list_behaviors",
534
+ "create_trigger",
535
+ "list_triggers",
536
+ // GraphRAG
522
537
  "query_relationships",
523
- "commit_to_long_term_memory"
538
+ "merge_entities",
539
+ // Documents + wiki
540
+ "ingest_document",
541
+ "list_documents",
542
+ "purge_document",
543
+ "rerank_documents",
544
+ "set_document_importance",
545
+ "create_wiki_page",
546
+ "update_wiki_page",
547
+ "get_wiki_page",
548
+ "list_wiki_pages",
549
+ // System
550
+ "load_skill",
551
+ "apply_starter_pack",
552
+ "resume_employee",
553
+ "deploy_client"
524
554
  ];
525
555
  const allowList = permissions.allow;
526
556
  for (const tool of expandDualPrefixTools(toolNames)) {
@@ -1490,7 +1490,8 @@ async function writeMemory(record) {
1490
1490
  has_error: record.has_error ? 1 : 0,
1491
1491
  raw_text: record.raw_text,
1492
1492
  vector: record.vector,
1493
- version: _nextVersion++,
1493
+ version: 0,
1494
+ // Placeholder — assigned atomically at flush time
1494
1495
  task_id: record.task_id ?? null,
1495
1496
  importance: record.importance ?? 5,
1496
1497
  status: record.status ?? "active",
@@ -1524,6 +1525,13 @@ async function flushBatch() {
1524
1525
  _flushing = true;
1525
1526
  try {
1526
1527
  const batch = _pendingRecords.slice(0);
1528
+ const client = getClient();
1529
+ const vResult = await client.execute("SELECT MAX(version) as max_v FROM memories");
1530
+ let baseVersion = (Number(vResult.rows[0]?.max_v) || 0) + 1;
1531
+ for (const row of batch) {
1532
+ row.version = baseVersion++;
1533
+ }
1534
+ _nextVersion = baseVersion;
1527
1535
  const buildStmt = (row) => {
1528
1536
  const hasVector = row.vector !== null;
1529
1537
  const taskId = row.task_id ?? null;
@@ -1424,6 +1424,13 @@ async function flushBatch() {
1424
1424
  _flushing = true;
1425
1425
  try {
1426
1426
  const batch = _pendingRecords.slice(0);
1427
+ const client = getClient();
1428
+ const vResult = await client.execute("SELECT MAX(version) as max_v FROM memories");
1429
+ let baseVersion = (Number(vResult.rows[0]?.max_v) || 0) + 1;
1430
+ for (const row of batch) {
1431
+ row.version = baseVersion++;
1432
+ }
1433
+ _nextVersion = baseVersion;
1427
1434
  const buildStmt = (row) => {
1428
1435
  const hasVector = row.vector !== null;
1429
1436
  const taskId = row.task_id ?? null;
@@ -1424,6 +1424,13 @@ async function flushBatch() {
1424
1424
  _flushing = true;
1425
1425
  try {
1426
1426
  const batch = _pendingRecords.slice(0);
1427
+ const client = getClient();
1428
+ const vResult = await client.execute("SELECT MAX(version) as max_v FROM memories");
1429
+ let baseVersion = (Number(vResult.rows[0]?.max_v) || 0) + 1;
1430
+ for (const row of batch) {
1431
+ row.version = baseVersion++;
1432
+ }
1433
+ _nextVersion = baseVersion;
1427
1434
  const buildStmt = (row) => {
1428
1435
  const hasVector = row.vector !== null;
1429
1436
  const taskId = row.task_id ?? null;
@@ -2199,7 +2199,8 @@ async function writeMemory(record) {
2199
2199
  has_error: record.has_error ? 1 : 0,
2200
2200
  raw_text: record.raw_text,
2201
2201
  vector: record.vector,
2202
- version: _nextVersion++,
2202
+ version: 0,
2203
+ // Placeholder — assigned atomically at flush time
2203
2204
  task_id: record.task_id ?? null,
2204
2205
  importance: record.importance ?? 5,
2205
2206
  status: record.status ?? "active",
@@ -2233,6 +2234,13 @@ async function flushBatch() {
2233
2234
  _flushing = true;
2234
2235
  try {
2235
2236
  const batch = _pendingRecords.slice(0);
2237
+ const client = getClient();
2238
+ const vResult = await client.execute("SELECT MAX(version) as max_v FROM memories");
2239
+ let baseVersion = (Number(vResult.rows[0]?.max_v) || 0) + 1;
2240
+ for (const row of batch) {
2241
+ row.version = baseVersion++;
2242
+ }
2243
+ _nextVersion = baseVersion;
2236
2244
  const buildStmt = (row) => {
2237
2245
  const hasVector = row.vector !== null;
2238
2246
  const taskId = row.task_id ?? null;
@@ -3172,6 +3180,42 @@ import { readFileSync as readFileSync9, writeFileSync as writeFileSync4, mkdirSy
3172
3180
  import path11 from "path";
3173
3181
  import os4 from "os";
3174
3182
  import { fileURLToPath as fileURLToPath2 } from "url";
3183
+ import { unlinkSync as unlinkSync2 } from "fs";
3184
+ function spawnLockPath(sessionName) {
3185
+ return path11.join(SPAWN_LOCK_DIR, `${sessionName}.lock`);
3186
+ }
3187
+ function isProcessAlive(pid) {
3188
+ try {
3189
+ process.kill(pid, 0);
3190
+ return true;
3191
+ } catch {
3192
+ return false;
3193
+ }
3194
+ }
3195
+ function acquireSpawnLock2(sessionName) {
3196
+ if (!existsSync10(SPAWN_LOCK_DIR)) {
3197
+ mkdirSync6(SPAWN_LOCK_DIR, { recursive: true });
3198
+ }
3199
+ const lockFile = spawnLockPath(sessionName);
3200
+ if (existsSync10(lockFile)) {
3201
+ try {
3202
+ const lock = JSON.parse(readFileSync9(lockFile, "utf8"));
3203
+ const age = Date.now() - lock.timestamp;
3204
+ if (isProcessAlive(lock.pid) && age < 6e4) {
3205
+ return false;
3206
+ }
3207
+ } catch {
3208
+ }
3209
+ }
3210
+ writeFileSync4(lockFile, JSON.stringify({ pid: process.pid, timestamp: Date.now() }));
3211
+ return true;
3212
+ }
3213
+ function releaseSpawnLock2(sessionName) {
3214
+ try {
3215
+ unlinkSync2(spawnLockPath(sessionName));
3216
+ } catch {
3217
+ }
3218
+ }
3175
3219
  function resolveBehaviorsExporterScript() {
3176
3220
  try {
3177
3221
  const thisFile = fileURLToPath2(import.meta.url);
@@ -3252,10 +3296,10 @@ function isEmployeeAlive(sessionName) {
3252
3296
  }
3253
3297
  function findFreeInstance(employeeName, exeSession, maxInstances = 10, isAlive = isEmployeeAlive) {
3254
3298
  const base = employeeSessionName(employeeName, exeSession);
3255
- if (!isAlive(base)) return 0;
3299
+ if (!isAlive(base) && acquireSpawnLock2(base)) return 0;
3256
3300
  for (let i = 2; i <= maxInstances; i++) {
3257
3301
  const candidate = employeeSessionName(employeeName, exeSession, i);
3258
- if (!isAlive(candidate)) return i;
3302
+ if (!isAlive(candidate) && acquireSpawnLock2(candidate)) return i;
3259
3303
  }
3260
3304
  return null;
3261
3305
  }
@@ -3589,6 +3633,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
3589
3633
  command: spawnCommand
3590
3634
  });
3591
3635
  if (spawnResult.error) {
3636
+ releaseSpawnLock2(sessionName);
3592
3637
  return { sessionName, error: `tmux new-session failed: ${spawnResult.error}` };
3593
3638
  }
3594
3639
  transport.pipeLog(sessionName, logFile);
@@ -3626,6 +3671,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
3626
3671
  }
3627
3672
  }
3628
3673
  if (!booted) {
3674
+ releaseSpawnLock2(sessionName);
3629
3675
  return { sessionName, error: `${useExeAgent ? "exe-agent" : "claude"} did not boot within 15s` };
3630
3676
  }
3631
3677
  if (!useExeAgent) {
@@ -3642,9 +3688,10 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
3642
3688
  pid: 0,
3643
3689
  registeredAt: (/* @__PURE__ */ new Date()).toISOString()
3644
3690
  });
3691
+ releaseSpawnLock2(sessionName);
3645
3692
  return { sessionName };
3646
3693
  }
3647
- var SESSION_CACHE, BEHAVIORS_EXPORT_TIMEOUT_MS, INTERCOM_DEBOUNCE_MS, INTERCOM_LOG2, DEBOUNCE_FILE, DEBOUNCE_CLEANUP_AGE_MS, BUSY_PATTERN;
3694
+ var SPAWN_LOCK_DIR, SESSION_CACHE, BEHAVIORS_EXPORT_TIMEOUT_MS, INTERCOM_DEBOUNCE_MS, INTERCOM_LOG2, DEBOUNCE_FILE, DEBOUNCE_CLEANUP_AGE_MS, BUSY_PATTERN;
3648
3695
  var init_tmux_routing = __esm({
3649
3696
  "src/lib/tmux-routing.ts"() {
3650
3697
  "use strict";
@@ -3656,6 +3703,7 @@ var init_tmux_routing = __esm({
3656
3703
  init_provider_table();
3657
3704
  init_intercom_queue();
3658
3705
  init_plan_limits();
3706
+ SPAWN_LOCK_DIR = path11.join(os4.homedir(), ".exe-os", "spawn-locks");
3659
3707
  SESSION_CACHE = path11.join(os4.homedir(), ".exe-os", "session-cache");
3660
3708
  BEHAVIORS_EXPORT_TIMEOUT_MS = 1e4;
3661
3709
  INTERCOM_DEBOUNCE_MS = 3e4;
@@ -3923,7 +3971,7 @@ import os5 from "os";
3923
3971
  import {
3924
3972
  readFileSync as readFileSync10,
3925
3973
  readdirSync,
3926
- unlinkSync as unlinkSync2,
3974
+ unlinkSync as unlinkSync3,
3927
3975
  existsSync as existsSync11,
3928
3976
  rmdirSync
3929
3977
  } from "fs";
@@ -4371,7 +4419,7 @@ var init_tasks_crud = __esm({
4371
4419
 
4372
4420
  // src/lib/tasks-review.ts
4373
4421
  import path14 from "path";
4374
- import { existsSync as existsSync13, readdirSync as readdirSync2, unlinkSync as unlinkSync3 } from "fs";
4422
+ import { existsSync as existsSync13, readdirSync as readdirSync2, unlinkSync as unlinkSync4 } from "fs";
4375
4423
  async function countPendingReviews() {
4376
4424
  const client = getClient();
4377
4425
  const result = await client.execute({
@@ -4495,7 +4543,7 @@ async function cleanupReviewFile(row, taskFile, _baseDir) {
4495
4543
  if (existsSync13(cacheDir)) {
4496
4544
  for (const f of readdirSync2(cacheDir)) {
4497
4545
  if (f.startsWith("review-notified-")) {
4498
- unlinkSync3(path14.join(cacheDir, f));
4546
+ unlinkSync4(path14.join(cacheDir, f));
4499
4547
  }
4500
4548
  }
4501
4549
  }
@@ -5096,7 +5144,7 @@ __export(tasks_exports, {
5096
5144
  writeCheckpoint: () => writeCheckpoint
5097
5145
  });
5098
5146
  import path17 from "path";
5099
- import { writeFileSync as writeFileSync5, mkdirSync as mkdirSync7, unlinkSync as unlinkSync4 } from "fs";
5147
+ import { writeFileSync as writeFileSync5, mkdirSync as mkdirSync7, unlinkSync as unlinkSync5 } from "fs";
5100
5148
  async function createTask(input) {
5101
5149
  const result = await createTaskCore(input);
5102
5150
  if (!input.skipDispatch && result.status !== "blocked" && !process.env.VITEST) {
@@ -5122,7 +5170,7 @@ async function updateTask(input) {
5122
5170
  writeFileSync5(cachePath, JSON.stringify({ taskId, title: String(row.title) }));
5123
5171
  } else if (input.status === "done" || input.status === "blocked" || input.status === "cancelled") {
5124
5172
  try {
5125
- unlinkSync4(cachePath);
5173
+ unlinkSync5(cachePath);
5126
5174
  } catch {
5127
5175
  }
5128
5176
  }
@@ -2109,6 +2109,42 @@ import { readFileSync as readFileSync9, writeFileSync as writeFileSync4, mkdirSy
2109
2109
  import path11 from "path";
2110
2110
  import os5 from "os";
2111
2111
  import { fileURLToPath } from "url";
2112
+ import { unlinkSync as unlinkSync2 } from "fs";
2113
+ function spawnLockPath(sessionName) {
2114
+ return path11.join(SPAWN_LOCK_DIR, `${sessionName}.lock`);
2115
+ }
2116
+ function isProcessAlive(pid) {
2117
+ try {
2118
+ process.kill(pid, 0);
2119
+ return true;
2120
+ } catch {
2121
+ return false;
2122
+ }
2123
+ }
2124
+ function acquireSpawnLock(sessionName) {
2125
+ if (!existsSync11(SPAWN_LOCK_DIR)) {
2126
+ mkdirSync5(SPAWN_LOCK_DIR, { recursive: true });
2127
+ }
2128
+ const lockFile = spawnLockPath(sessionName);
2129
+ if (existsSync11(lockFile)) {
2130
+ try {
2131
+ const lock = JSON.parse(readFileSync9(lockFile, "utf8"));
2132
+ const age = Date.now() - lock.timestamp;
2133
+ if (isProcessAlive(lock.pid) && age < 6e4) {
2134
+ return false;
2135
+ }
2136
+ } catch {
2137
+ }
2138
+ }
2139
+ writeFileSync4(lockFile, JSON.stringify({ pid: process.pid, timestamp: Date.now() }));
2140
+ return true;
2141
+ }
2142
+ function releaseSpawnLock(sessionName) {
2143
+ try {
2144
+ unlinkSync2(spawnLockPath(sessionName));
2145
+ } catch {
2146
+ }
2147
+ }
2112
2148
  function resolveBehaviorsExporterScript() {
2113
2149
  try {
2114
2150
  const thisFile = fileURLToPath(import.meta.url);
@@ -2178,10 +2214,10 @@ function isEmployeeAlive(sessionName) {
2178
2214
  }
2179
2215
  function findFreeInstance(employeeName, exeSession, maxInstances = 10, isAlive = isEmployeeAlive) {
2180
2216
  const base = employeeSessionName(employeeName, exeSession);
2181
- if (!isAlive(base)) return 0;
2217
+ if (!isAlive(base) && acquireSpawnLock(base)) return 0;
2182
2218
  for (let i = 2; i <= maxInstances; i++) {
2183
2219
  const candidate = employeeSessionName(employeeName, exeSession, i);
2184
- if (!isAlive(candidate)) return i;
2220
+ if (!isAlive(candidate) && acquireSpawnLock(candidate)) return i;
2185
2221
  }
2186
2222
  return null;
2187
2223
  }
@@ -2493,6 +2529,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
2493
2529
  command: spawnCommand
2494
2530
  });
2495
2531
  if (spawnResult.error) {
2532
+ releaseSpawnLock(sessionName);
2496
2533
  return { sessionName, error: `tmux new-session failed: ${spawnResult.error}` };
2497
2534
  }
2498
2535
  transport.pipeLog(sessionName, logFile);
@@ -2530,6 +2567,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
2530
2567
  }
2531
2568
  }
2532
2569
  if (!booted) {
2570
+ releaseSpawnLock(sessionName);
2533
2571
  return { sessionName, error: `${useExeAgent ? "exe-agent" : "claude"} did not boot within 15s` };
2534
2572
  }
2535
2573
  if (!useExeAgent) {
@@ -2546,9 +2584,10 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
2546
2584
  pid: 0,
2547
2585
  registeredAt: (/* @__PURE__ */ new Date()).toISOString()
2548
2586
  });
2587
+ releaseSpawnLock(sessionName);
2549
2588
  return { sessionName };
2550
2589
  }
2551
- var SESSION_CACHE, BEHAVIORS_EXPORT_TIMEOUT_MS, INTERCOM_DEBOUNCE_MS, INTERCOM_LOG2, DEBOUNCE_FILE, DEBOUNCE_CLEANUP_AGE_MS, BUSY_PATTERN;
2590
+ var SPAWN_LOCK_DIR, SESSION_CACHE, BEHAVIORS_EXPORT_TIMEOUT_MS, INTERCOM_DEBOUNCE_MS, INTERCOM_LOG2, DEBOUNCE_FILE, DEBOUNCE_CLEANUP_AGE_MS, BUSY_PATTERN;
2552
2591
  var init_tmux_routing = __esm({
2553
2592
  "src/lib/tmux-routing.ts"() {
2554
2593
  "use strict";
@@ -2560,6 +2599,7 @@ var init_tmux_routing = __esm({
2560
2599
  init_provider_table();
2561
2600
  init_intercom_queue();
2562
2601
  init_plan_limits();
2602
+ SPAWN_LOCK_DIR = path11.join(os5.homedir(), ".exe-os", "spawn-locks");
2563
2603
  SESSION_CACHE = path11.join(os5.homedir(), ".exe-os", "session-cache");
2564
2604
  BEHAVIORS_EXPORT_TIMEOUT_MS = 1e4;
2565
2605
  INTERCOM_DEBOUNCE_MS = 3e4;
@@ -2572,7 +2612,7 @@ var init_tmux_routing = __esm({
2572
2612
 
2573
2613
  // src/lib/tasks-review.ts
2574
2614
  import path12 from "path";
2575
- import { existsSync as existsSync12, readdirSync as readdirSync2, unlinkSync as unlinkSync2 } from "fs";
2615
+ import { existsSync as existsSync12, readdirSync as readdirSync2, unlinkSync as unlinkSync3 } from "fs";
2576
2616
  var init_tasks_review = __esm({
2577
2617
  "src/lib/tasks-review.ts"() {
2578
2618
  "use strict";
@@ -2755,7 +2795,7 @@ var init_tasks_notify = __esm({
2755
2795
 
2756
2796
  // src/lib/tasks.ts
2757
2797
  import path15 from "path";
2758
- import { writeFileSync as writeFileSync5, mkdirSync as mkdirSync6, unlinkSync as unlinkSync3 } from "fs";
2798
+ import { writeFileSync as writeFileSync5, mkdirSync as mkdirSync6, unlinkSync as unlinkSync4 } from "fs";
2759
2799
  async function createTask(input) {
2760
2800
  const result = await createTaskCore(input);
2761
2801
  if (!input.skipDispatch && result.status !== "blocked" && !process.env.VITEST) {
@@ -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;
@@ -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;
@@ -3306,7 +3314,7 @@ function getActiveAgent() {
3306
3314
  "tmux display-message -p '#{session_name}' 2>/dev/null",
3307
3315
  { encoding: "utf8", timeout: 2e3 }
3308
3316
  ).trim();
3309
- const empMatch = sessionName.match(/^(\w+)-exe\d+$/);
3317
+ const empMatch = sessionName.match(/^([a-zA-Z]+)\d*-exe\d+$/);
3310
3318
  if (empMatch && empMatch[1] !== "exe") {
3311
3319
  return { agentId: empMatch[1], agentRole: "employee" };
3312
3320
  }
@@ -167,7 +167,7 @@ function getActiveAgent() {
167
167
  "tmux display-message -p '#{session_name}' 2>/dev/null",
168
168
  { encoding: "utf8", timeout: 2e3 }
169
169
  ).trim();
170
- const empMatch = sessionName.match(/^(\w+)-exe\d+$/);
170
+ const empMatch = sessionName.match(/^([a-zA-Z]+)\d*-exe\d+$/);
171
171
  if (empMatch && empMatch[1] !== "exe") {
172
172
  return { agentId: empMatch[1], agentRole: "employee" };
173
173
  }