@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
@@ -1651,7 +1651,8 @@ async function writeMemory(record) {
1651
1651
  has_error: record.has_error ? 1 : 0,
1652
1652
  raw_text: record.raw_text,
1653
1653
  vector: record.vector,
1654
- version: _nextVersion++,
1654
+ version: 0,
1655
+ // Placeholder — assigned atomically at flush time
1655
1656
  task_id: record.task_id ?? null,
1656
1657
  importance: record.importance ?? 5,
1657
1658
  status: record.status ?? "active",
@@ -1685,6 +1686,13 @@ async function flushBatch() {
1685
1686
  _flushing = true;
1686
1687
  try {
1687
1688
  const batch = _pendingRecords.slice(0);
1689
+ const client = getClient();
1690
+ const vResult = await client.execute("SELECT MAX(version) as max_v FROM memories");
1691
+ let baseVersion = (Number(vResult.rows[0]?.max_v) || 0) + 1;
1692
+ for (const row of batch) {
1693
+ row.version = baseVersion++;
1694
+ }
1695
+ _nextVersion = baseVersion;
1688
1696
  const buildStmt = (row) => {
1689
1697
  const hasVector = row.vector !== null;
1690
1698
  const taskId = row.task_id ?? null;
@@ -4354,6 +4362,7 @@ var init_capacity_monitor = __esm({
4354
4362
  // src/lib/tmux-routing.ts
4355
4363
  var tmux_routing_exports = {};
4356
4364
  __export(tmux_routing_exports, {
4365
+ acquireSpawnLock: () => acquireSpawnLock,
4357
4366
  employeeSessionName: () => employeeSessionName,
4358
4367
  ensureEmployee: () => ensureEmployee,
4359
4368
  extractRootExe: () => extractRootExe,
@@ -4368,6 +4377,7 @@ __export(tmux_routing_exports, {
4368
4377
  notifyParentExe: () => notifyParentExe,
4369
4378
  parseParentExe: () => parseParentExe,
4370
4379
  registerParentExe: () => registerParentExe,
4380
+ releaseSpawnLock: () => releaseSpawnLock,
4371
4381
  resolveExeSession: () => resolveExeSession,
4372
4382
  sendIntercom: () => sendIntercom,
4373
4383
  spawnEmployee: () => spawnEmployee,
@@ -4378,6 +4388,42 @@ import { readFileSync as readFileSync9, writeFileSync as writeFileSync5, mkdirSy
4378
4388
  import path15 from "path";
4379
4389
  import os5 from "os";
4380
4390
  import { fileURLToPath } from "url";
4391
+ import { unlinkSync as unlinkSync4 } from "fs";
4392
+ function spawnLockPath(sessionName) {
4393
+ return path15.join(SPAWN_LOCK_DIR, `${sessionName}.lock`);
4394
+ }
4395
+ function isProcessAlive(pid) {
4396
+ try {
4397
+ process.kill(pid, 0);
4398
+ return true;
4399
+ } catch {
4400
+ return false;
4401
+ }
4402
+ }
4403
+ function acquireSpawnLock(sessionName) {
4404
+ if (!existsSync12(SPAWN_LOCK_DIR)) {
4405
+ mkdirSync6(SPAWN_LOCK_DIR, { recursive: true });
4406
+ }
4407
+ const lockFile = spawnLockPath(sessionName);
4408
+ if (existsSync12(lockFile)) {
4409
+ try {
4410
+ const lock = JSON.parse(readFileSync9(lockFile, "utf8"));
4411
+ const age = Date.now() - lock.timestamp;
4412
+ if (isProcessAlive(lock.pid) && age < 6e4) {
4413
+ return false;
4414
+ }
4415
+ } catch {
4416
+ }
4417
+ }
4418
+ writeFileSync5(lockFile, JSON.stringify({ pid: process.pid, timestamp: Date.now() }));
4419
+ return true;
4420
+ }
4421
+ function releaseSpawnLock(sessionName) {
4422
+ try {
4423
+ unlinkSync4(spawnLockPath(sessionName));
4424
+ } catch {
4425
+ }
4426
+ }
4381
4427
  function resolveBehaviorsExporterScript() {
4382
4428
  try {
4383
4429
  const thisFile = fileURLToPath(import.meta.url);
@@ -4476,10 +4522,10 @@ function isEmployeeAlive(sessionName) {
4476
4522
  }
4477
4523
  function findFreeInstance(employeeName, exeSession, maxInstances = 10, isAlive = isEmployeeAlive) {
4478
4524
  const base = employeeSessionName(employeeName, exeSession);
4479
- if (!isAlive(base)) return 0;
4525
+ if (!isAlive(base) && acquireSpawnLock(base)) return 0;
4480
4526
  for (let i = 2; i <= maxInstances; i++) {
4481
4527
  const candidate = employeeSessionName(employeeName, exeSession, i);
4482
- if (!isAlive(candidate)) return i;
4528
+ if (!isAlive(candidate) && acquireSpawnLock(candidate)) return i;
4483
4529
  }
4484
4530
  return null;
4485
4531
  }
@@ -4843,6 +4889,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
4843
4889
  command: spawnCommand
4844
4890
  });
4845
4891
  if (spawnResult.error) {
4892
+ releaseSpawnLock(sessionName);
4846
4893
  return { sessionName, error: `tmux new-session failed: ${spawnResult.error}` };
4847
4894
  }
4848
4895
  transport.pipeLog(sessionName, logFile);
@@ -4880,6 +4927,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
4880
4927
  }
4881
4928
  }
4882
4929
  if (!booted) {
4930
+ releaseSpawnLock(sessionName);
4883
4931
  return { sessionName, error: `${useExeAgent ? "exe-agent" : "claude"} did not boot within 15s` };
4884
4932
  }
4885
4933
  if (!useExeAgent) {
@@ -4896,9 +4944,10 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
4896
4944
  pid: 0,
4897
4945
  registeredAt: (/* @__PURE__ */ new Date()).toISOString()
4898
4946
  });
4947
+ releaseSpawnLock(sessionName);
4899
4948
  return { sessionName };
4900
4949
  }
4901
- var SESSION_CACHE, BEHAVIORS_EXPORT_TIMEOUT_MS, VERIFY_PANE_LINES, INTERCOM_DEBOUNCE_MS, INTERCOM_LOG2, DEBOUNCE_FILE, DEBOUNCE_CLEANUP_AGE_MS, BUSY_PATTERN;
4950
+ 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;
4902
4951
  var init_tmux_routing = __esm({
4903
4952
  "src/lib/tmux-routing.ts"() {
4904
4953
  "use strict";
@@ -4910,6 +4959,7 @@ var init_tmux_routing = __esm({
4910
4959
  init_provider_table();
4911
4960
  init_intercom_queue();
4912
4961
  init_plan_limits();
4962
+ SPAWN_LOCK_DIR = path15.join(os5.homedir(), ".exe-os", "spawn-locks");
4913
4963
  SESSION_CACHE = path15.join(os5.homedir(), ".exe-os", "session-cache");
4914
4964
  BEHAVIORS_EXPORT_TIMEOUT_MS = 1e4;
4915
4965
  VERIFY_PANE_LINES = 200;
@@ -6130,7 +6180,7 @@ var init_consolidation = __esm({
6130
6180
  import net from "net";
6131
6181
  import { spawn } from "child_process";
6132
6182
  import { randomUUID as randomUUID3 } from "crypto";
6133
- import { existsSync as existsSync14, unlinkSync as unlinkSync4, readFileSync as readFileSync11, openSync, closeSync, statSync } from "fs";
6183
+ import { existsSync as existsSync14, unlinkSync as unlinkSync5, readFileSync as readFileSync11, openSync, closeSync, statSync } from "fs";
6134
6184
  import path17 from "path";
6135
6185
  import { fileURLToPath as fileURLToPath2 } from "url";
6136
6186
  function handleData(chunk) {
@@ -6166,11 +6216,11 @@ function cleanupStaleFiles() {
6166
6216
  } catch {
6167
6217
  }
6168
6218
  try {
6169
- unlinkSync4(PID_PATH);
6219
+ unlinkSync5(PID_PATH);
6170
6220
  } catch {
6171
6221
  }
6172
6222
  try {
6173
- unlinkSync4(SOCKET_PATH);
6223
+ unlinkSync5(SOCKET_PATH);
6174
6224
  } catch {
6175
6225
  }
6176
6226
  }
@@ -6222,7 +6272,7 @@ function spawnDaemon() {
6222
6272
  }
6223
6273
  }
6224
6274
  }
6225
- function acquireSpawnLock() {
6275
+ function acquireSpawnLock2() {
6226
6276
  try {
6227
6277
  const fd = openSync(SPAWN_LOCK_PATH, "wx");
6228
6278
  closeSync(fd);
@@ -6232,7 +6282,7 @@ function acquireSpawnLock() {
6232
6282
  const stat = statSync(SPAWN_LOCK_PATH);
6233
6283
  if (Date.now() - stat.mtimeMs > SPAWN_LOCK_STALE_MS) {
6234
6284
  try {
6235
- unlinkSync4(SPAWN_LOCK_PATH);
6285
+ unlinkSync5(SPAWN_LOCK_PATH);
6236
6286
  } catch {
6237
6287
  }
6238
6288
  try {
@@ -6247,9 +6297,9 @@ function acquireSpawnLock() {
6247
6297
  return false;
6248
6298
  }
6249
6299
  }
6250
- function releaseSpawnLock() {
6300
+ function releaseSpawnLock2() {
6251
6301
  try {
6252
- unlinkSync4(SPAWN_LOCK_PATH);
6302
+ unlinkSync5(SPAWN_LOCK_PATH);
6253
6303
  } catch {
6254
6304
  }
6255
6305
  }
@@ -6294,12 +6344,12 @@ function connectToSocket() {
6294
6344
  async function connectEmbedDaemon() {
6295
6345
  if (_socket && _connected) return true;
6296
6346
  if (await connectToSocket()) return true;
6297
- if (acquireSpawnLock()) {
6347
+ if (acquireSpawnLock2()) {
6298
6348
  try {
6299
6349
  cleanupStaleFiles();
6300
6350
  spawnDaemon();
6301
6351
  } finally {
6302
- releaseSpawnLock();
6352
+ releaseSpawnLock2();
6303
6353
  }
6304
6354
  }
6305
6355
  const start = Date.now();
@@ -6380,11 +6430,11 @@ function killAndRespawnDaemon() {
6380
6430
  _connected = false;
6381
6431
  _buffer = "";
6382
6432
  try {
6383
- unlinkSync4(PID_PATH);
6433
+ unlinkSync5(PID_PATH);
6384
6434
  } catch {
6385
6435
  }
6386
6436
  try {
6387
- unlinkSync4(SOCKET_PATH);
6437
+ unlinkSync5(SOCKET_PATH);
6388
6438
  } catch {
6389
6439
  }
6390
6440
  spawnDaemon();
@@ -7891,7 +7941,7 @@ var init_messaging = __esm({
7891
7941
  init_config();
7892
7942
  init_memory();
7893
7943
  import net2 from "net";
7894
- import { writeFileSync as writeFileSync7, unlinkSync as unlinkSync5, mkdirSync as mkdirSync8, existsSync as existsSync16, readFileSync as readFileSync14 } from "fs";
7944
+ import { writeFileSync as writeFileSync7, unlinkSync as unlinkSync6, mkdirSync as mkdirSync8, existsSync as existsSync16, readFileSync as readFileSync14 } from "fs";
7895
7945
  import path20 from "path";
7896
7946
  import { getLlama } from "node-llama-cpp";
7897
7947
  var SOCKET_PATH2 = process.env.EXE_DAEMON_SOCK ?? process.env.EXE_EMBED_SOCK ?? path20.join(EXE_AI_DIR, "exed.sock");
@@ -7996,11 +8046,11 @@ async function shutdown() {
7996
8046
  }
7997
8047
  _llama = null;
7998
8048
  try {
7999
- unlinkSync5(SOCKET_PATH2);
8049
+ unlinkSync6(SOCKET_PATH2);
8000
8050
  } catch {
8001
8051
  }
8002
8052
  try {
8003
- unlinkSync5(PID_PATH2);
8053
+ unlinkSync6(PID_PATH2);
8004
8054
  } catch {
8005
8055
  }
8006
8056
  process.stderr.write("[exed] Shutdown complete.\n");
@@ -8044,12 +8094,12 @@ function startServer() {
8044
8094
  } catch {
8045
8095
  }
8046
8096
  }
8047
- unlinkSync5(oldPath);
8097
+ unlinkSync6(oldPath);
8048
8098
  } catch {
8049
8099
  }
8050
8100
  }
8051
8101
  try {
8052
- unlinkSync5(SOCKET_PATH2);
8102
+ unlinkSync6(SOCKET_PATH2);
8053
8103
  } catch {
8054
8104
  }
8055
8105
  const server = net2.createServer((socket) => {
@@ -8475,7 +8525,7 @@ function checkExistingDaemon() {
8475
8525
  return true;
8476
8526
  }
8477
8527
  try {
8478
- unlinkSync5(PID_PATH2);
8528
+ unlinkSync6(PID_PATH2);
8479
8529
  } catch {
8480
8530
  }
8481
8531
  return false;
@@ -8638,11 +8688,11 @@ try {
8638
8688
  process.stderr.write(`[exed] FATAL: ${err instanceof Error ? err.message : String(err)}
8639
8689
  `);
8640
8690
  try {
8641
- unlinkSync5(SOCKET_PATH2);
8691
+ unlinkSync6(SOCKET_PATH2);
8642
8692
  } catch {
8643
8693
  }
8644
8694
  try {
8645
- unlinkSync5(PID_PATH2);
8695
+ unlinkSync6(PID_PATH2);
8646
8696
  } catch {
8647
8697
  }
8648
8698
  process.exit(1);
@@ -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;
@@ -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;
package/dist/lib/store.js CHANGED
@@ -1443,7 +1443,8 @@ async function writeMemory(record) {
1443
1443
  has_error: record.has_error ? 1 : 0,
1444
1444
  raw_text: record.raw_text,
1445
1445
  vector: record.vector,
1446
- version: _nextVersion++,
1446
+ version: 0,
1447
+ // Placeholder — assigned atomically at flush time
1447
1448
  task_id: record.task_id ?? null,
1448
1449
  importance: record.importance ?? 5,
1449
1450
  status: record.status ?? "active",
@@ -1477,6 +1478,13 @@ async function flushBatch() {
1477
1478
  _flushing = true;
1478
1479
  try {
1479
1480
  const batch = _pendingRecords.slice(0);
1481
+ const client = getClient();
1482
+ const vResult = await client.execute("SELECT MAX(version) as max_v FROM memories");
1483
+ let baseVersion = (Number(vResult.rows[0]?.max_v) || 0) + 1;
1484
+ for (const row of batch) {
1485
+ row.version = baseVersion++;
1486
+ }
1487
+ _nextVersion = baseVersion;
1480
1488
  const buildStmt = (row) => {
1481
1489
  const hasVector = row.vector !== null;
1482
1490
  const taskId = row.task_id ?? null;
package/dist/lib/tasks.js CHANGED
@@ -1122,6 +1122,42 @@ import { readFileSync as readFileSync9, writeFileSync as writeFileSync4, mkdirSy
1122
1122
  import path9 from "path";
1123
1123
  import os5 from "os";
1124
1124
  import { fileURLToPath } from "url";
1125
+ import { unlinkSync as unlinkSync2 } from "fs";
1126
+ function spawnLockPath(sessionName) {
1127
+ return path9.join(SPAWN_LOCK_DIR, `${sessionName}.lock`);
1128
+ }
1129
+ function isProcessAlive(pid) {
1130
+ try {
1131
+ process.kill(pid, 0);
1132
+ return true;
1133
+ } catch {
1134
+ return false;
1135
+ }
1136
+ }
1137
+ function acquireSpawnLock(sessionName) {
1138
+ if (!existsSync9(SPAWN_LOCK_DIR)) {
1139
+ mkdirSync4(SPAWN_LOCK_DIR, { recursive: true });
1140
+ }
1141
+ const lockFile = spawnLockPath(sessionName);
1142
+ if (existsSync9(lockFile)) {
1143
+ try {
1144
+ const lock = JSON.parse(readFileSync9(lockFile, "utf8"));
1145
+ const age = Date.now() - lock.timestamp;
1146
+ if (isProcessAlive(lock.pid) && age < 6e4) {
1147
+ return false;
1148
+ }
1149
+ } catch {
1150
+ }
1151
+ }
1152
+ writeFileSync4(lockFile, JSON.stringify({ pid: process.pid, timestamp: Date.now() }));
1153
+ return true;
1154
+ }
1155
+ function releaseSpawnLock(sessionName) {
1156
+ try {
1157
+ unlinkSync2(spawnLockPath(sessionName));
1158
+ } catch {
1159
+ }
1160
+ }
1125
1161
  function resolveBehaviorsExporterScript() {
1126
1162
  try {
1127
1163
  const thisFile = fileURLToPath(import.meta.url);
@@ -1202,10 +1238,10 @@ function isEmployeeAlive(sessionName) {
1202
1238
  }
1203
1239
  function findFreeInstance(employeeName, exeSession, maxInstances = 10, isAlive = isEmployeeAlive) {
1204
1240
  const base = employeeSessionName(employeeName, exeSession);
1205
- if (!isAlive(base)) return 0;
1241
+ if (!isAlive(base) && acquireSpawnLock(base)) return 0;
1206
1242
  for (let i = 2; i <= maxInstances; i++) {
1207
1243
  const candidate = employeeSessionName(employeeName, exeSession, i);
1208
- if (!isAlive(candidate)) return i;
1244
+ if (!isAlive(candidate) && acquireSpawnLock(candidate)) return i;
1209
1245
  }
1210
1246
  return null;
1211
1247
  }
@@ -1539,6 +1575,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
1539
1575
  command: spawnCommand
1540
1576
  });
1541
1577
  if (spawnResult.error) {
1578
+ releaseSpawnLock(sessionName);
1542
1579
  return { sessionName, error: `tmux new-session failed: ${spawnResult.error}` };
1543
1580
  }
1544
1581
  transport.pipeLog(sessionName, logFile);
@@ -1576,6 +1613,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
1576
1613
  }
1577
1614
  }
1578
1615
  if (!booted) {
1616
+ releaseSpawnLock(sessionName);
1579
1617
  return { sessionName, error: `${useExeAgent ? "exe-agent" : "claude"} did not boot within 15s` };
1580
1618
  }
1581
1619
  if (!useExeAgent) {
@@ -1592,9 +1630,10 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
1592
1630
  pid: 0,
1593
1631
  registeredAt: (/* @__PURE__ */ new Date()).toISOString()
1594
1632
  });
1633
+ releaseSpawnLock(sessionName);
1595
1634
  return { sessionName };
1596
1635
  }
1597
- var SESSION_CACHE, BEHAVIORS_EXPORT_TIMEOUT_MS, INTERCOM_DEBOUNCE_MS, INTERCOM_LOG2, DEBOUNCE_FILE, DEBOUNCE_CLEANUP_AGE_MS, BUSY_PATTERN;
1636
+ var SPAWN_LOCK_DIR, SESSION_CACHE, BEHAVIORS_EXPORT_TIMEOUT_MS, INTERCOM_DEBOUNCE_MS, INTERCOM_LOG2, DEBOUNCE_FILE, DEBOUNCE_CLEANUP_AGE_MS, BUSY_PATTERN;
1598
1637
  var init_tmux_routing = __esm({
1599
1638
  "src/lib/tmux-routing.ts"() {
1600
1639
  "use strict";
@@ -1606,6 +1645,7 @@ var init_tmux_routing = __esm({
1606
1645
  init_provider_table();
1607
1646
  init_intercom_queue();
1608
1647
  init_plan_limits();
1648
+ SPAWN_LOCK_DIR = path9.join(os5.homedir(), ".exe-os", "spawn-locks");
1609
1649
  SESSION_CACHE = path9.join(os5.homedir(), ".exe-os", "session-cache");
1610
1650
  BEHAVIORS_EXPORT_TIMEOUT_MS = 1e4;
1611
1651
  INTERCOM_DEBOUNCE_MS = 3e4;
@@ -1618,7 +1658,7 @@ var init_tmux_routing = __esm({
1618
1658
 
1619
1659
  // src/lib/tasks-review.ts
1620
1660
  import path10 from "path";
1621
- import { existsSync as existsSync10, readdirSync as readdirSync2, unlinkSync as unlinkSync2 } from "fs";
1661
+ import { existsSync as existsSync10, readdirSync as readdirSync2, unlinkSync as unlinkSync3 } from "fs";
1622
1662
  async function countPendingReviews() {
1623
1663
  const client = getClient();
1624
1664
  const result = await client.execute({
@@ -1742,7 +1782,7 @@ async function cleanupReviewFile(row, taskFile, _baseDir) {
1742
1782
  if (existsSync10(cacheDir)) {
1743
1783
  for (const f of readdirSync2(cacheDir)) {
1744
1784
  if (f.startsWith("review-notified-")) {
1745
- unlinkSync2(path10.join(cacheDir, f));
1785
+ unlinkSync3(path10.join(cacheDir, f));
1746
1786
  }
1747
1787
  }
1748
1788
  }
@@ -2324,7 +2364,7 @@ var init_skill_learning = __esm({
2324
2364
 
2325
2365
  // src/lib/tasks.ts
2326
2366
  import path13 from "path";
2327
- import { writeFileSync as writeFileSync5, mkdirSync as mkdirSync5, unlinkSync as unlinkSync3 } from "fs";
2367
+ import { writeFileSync as writeFileSync5, mkdirSync as mkdirSync5, unlinkSync as unlinkSync4 } from "fs";
2328
2368
  async function createTask(input) {
2329
2369
  const result = await createTaskCore(input);
2330
2370
  if (!input.skipDispatch && result.status !== "blocked" && !process.env.VITEST) {
@@ -2350,7 +2390,7 @@ async function updateTask(input) {
2350
2390
  writeFileSync5(cachePath, JSON.stringify({ taskId, title: String(row.title) }));
2351
2391
  } else if (input.status === "done" || input.status === "blocked" || input.status === "cancelled") {
2352
2392
  try {
2353
- unlinkSync3(cachePath);
2393
+ unlinkSync4(cachePath);
2354
2394
  } catch {
2355
2395
  }
2356
2396
  }
@@ -2299,6 +2299,42 @@ import { readFileSync as readFileSync9, writeFileSync as writeFileSync5, mkdirSy
2299
2299
  import path13 from "path";
2300
2300
  import os5 from "os";
2301
2301
  import { fileURLToPath } from "url";
2302
+ import { unlinkSync as unlinkSync4 } from "fs";
2303
+ function spawnLockPath(sessionName) {
2304
+ return path13.join(SPAWN_LOCK_DIR, `${sessionName}.lock`);
2305
+ }
2306
+ function isProcessAlive(pid) {
2307
+ try {
2308
+ process.kill(pid, 0);
2309
+ return true;
2310
+ } catch {
2311
+ return false;
2312
+ }
2313
+ }
2314
+ function acquireSpawnLock(sessionName) {
2315
+ if (!existsSync10(SPAWN_LOCK_DIR)) {
2316
+ mkdirSync5(SPAWN_LOCK_DIR, { recursive: true });
2317
+ }
2318
+ const lockFile = spawnLockPath(sessionName);
2319
+ if (existsSync10(lockFile)) {
2320
+ try {
2321
+ const lock = JSON.parse(readFileSync9(lockFile, "utf8"));
2322
+ const age = Date.now() - lock.timestamp;
2323
+ if (isProcessAlive(lock.pid) && age < 6e4) {
2324
+ return false;
2325
+ }
2326
+ } catch {
2327
+ }
2328
+ }
2329
+ writeFileSync5(lockFile, JSON.stringify({ pid: process.pid, timestamp: Date.now() }));
2330
+ return true;
2331
+ }
2332
+ function releaseSpawnLock(sessionName) {
2333
+ try {
2334
+ unlinkSync4(spawnLockPath(sessionName));
2335
+ } catch {
2336
+ }
2337
+ }
2302
2338
  function resolveBehaviorsExporterScript() {
2303
2339
  try {
2304
2340
  const thisFile = fileURLToPath(import.meta.url);
@@ -2397,10 +2433,10 @@ function isEmployeeAlive(sessionName) {
2397
2433
  }
2398
2434
  function findFreeInstance(employeeName, exeSession, maxInstances = 10, isAlive = isEmployeeAlive) {
2399
2435
  const base = employeeSessionName(employeeName, exeSession);
2400
- if (!isAlive(base)) return 0;
2436
+ if (!isAlive(base) && acquireSpawnLock(base)) return 0;
2401
2437
  for (let i = 2; i <= maxInstances; i++) {
2402
2438
  const candidate = employeeSessionName(employeeName, exeSession, i);
2403
- if (!isAlive(candidate)) return i;
2439
+ if (!isAlive(candidate) && acquireSpawnLock(candidate)) return i;
2404
2440
  }
2405
2441
  return null;
2406
2442
  }
@@ -2764,6 +2800,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
2764
2800
  command: spawnCommand
2765
2801
  });
2766
2802
  if (spawnResult.error) {
2803
+ releaseSpawnLock(sessionName);
2767
2804
  return { sessionName, error: `tmux new-session failed: ${spawnResult.error}` };
2768
2805
  }
2769
2806
  transport.pipeLog(sessionName, logFile);
@@ -2801,6 +2838,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
2801
2838
  }
2802
2839
  }
2803
2840
  if (!booted) {
2841
+ releaseSpawnLock(sessionName);
2804
2842
  return { sessionName, error: `${useExeAgent ? "exe-agent" : "claude"} did not boot within 15s` };
2805
2843
  }
2806
2844
  if (!useExeAgent) {
@@ -2817,9 +2855,10 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
2817
2855
  pid: 0,
2818
2856
  registeredAt: (/* @__PURE__ */ new Date()).toISOString()
2819
2857
  });
2858
+ releaseSpawnLock(sessionName);
2820
2859
  return { sessionName };
2821
2860
  }
2822
- var SESSION_CACHE, BEHAVIORS_EXPORT_TIMEOUT_MS, VERIFY_PANE_LINES, INTERCOM_DEBOUNCE_MS, INTERCOM_LOG2, DEBOUNCE_FILE, DEBOUNCE_CLEANUP_AGE_MS, BUSY_PATTERN;
2861
+ 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;
2823
2862
  var init_tmux_routing = __esm({
2824
2863
  "src/lib/tmux-routing.ts"() {
2825
2864
  init_session_registry();
@@ -2830,6 +2869,7 @@ var init_tmux_routing = __esm({
2830
2869
  init_provider_table();
2831
2870
  init_intercom_queue();
2832
2871
  init_plan_limits();
2872
+ SPAWN_LOCK_DIR = path13.join(os5.homedir(), ".exe-os", "spawn-locks");
2833
2873
  SESSION_CACHE = path13.join(os5.homedir(), ".exe-os", "session-cache");
2834
2874
  BEHAVIORS_EXPORT_TIMEOUT_MS = 1e4;
2835
2875
  VERIFY_PANE_LINES = 200;
@@ -2842,6 +2882,7 @@ var init_tmux_routing = __esm({
2842
2882
  });
2843
2883
  init_tmux_routing();
2844
2884
  export {
2885
+ acquireSpawnLock,
2845
2886
  employeeSessionName,
2846
2887
  ensureEmployee,
2847
2888
  extractRootExe,
@@ -2856,6 +2897,7 @@ export {
2856
2897
  notifyParentExe,
2857
2898
  parseParentExe,
2858
2899
  registerParentExe,
2900
+ releaseSpawnLock,
2859
2901
  resolveExeSession,
2860
2902
  sendIntercom,
2861
2903
  spawnEmployee,