@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
@@ -552,6 +552,42 @@ import { readFileSync as readFileSync7, writeFileSync as writeFileSync4, mkdirSy
552
552
  import path7 from "path";
553
553
  import os4 from "os";
554
554
  import { fileURLToPath } from "url";
555
+ import { unlinkSync } from "fs";
556
+ function spawnLockPath(sessionName) {
557
+ return path7.join(SPAWN_LOCK_DIR, `${sessionName}.lock`);
558
+ }
559
+ function isProcessAlive(pid) {
560
+ try {
561
+ process.kill(pid, 0);
562
+ return true;
563
+ } catch {
564
+ return false;
565
+ }
566
+ }
567
+ function acquireSpawnLock(sessionName) {
568
+ if (!existsSync7(SPAWN_LOCK_DIR)) {
569
+ mkdirSync4(SPAWN_LOCK_DIR, { recursive: true });
570
+ }
571
+ const lockFile = spawnLockPath(sessionName);
572
+ if (existsSync7(lockFile)) {
573
+ try {
574
+ const lock = JSON.parse(readFileSync7(lockFile, "utf8"));
575
+ const age = Date.now() - lock.timestamp;
576
+ if (isProcessAlive(lock.pid) && age < 6e4) {
577
+ return false;
578
+ }
579
+ } catch {
580
+ }
581
+ }
582
+ writeFileSync4(lockFile, JSON.stringify({ pid: process.pid, timestamp: Date.now() }));
583
+ return true;
584
+ }
585
+ function releaseSpawnLock(sessionName) {
586
+ try {
587
+ unlinkSync(spawnLockPath(sessionName));
588
+ } catch {
589
+ }
590
+ }
555
591
  function resolveBehaviorsExporterScript() {
556
592
  try {
557
593
  const thisFile = fileURLToPath(import.meta.url);
@@ -621,10 +657,10 @@ function isEmployeeAlive(sessionName) {
621
657
  }
622
658
  function findFreeInstance(employeeName2, exeSession2, maxInstances = 10, isAlive = isEmployeeAlive) {
623
659
  const base = employeeSessionName(employeeName2, exeSession2);
624
- if (!isAlive(base)) return 0;
660
+ if (!isAlive(base) && acquireSpawnLock(base)) return 0;
625
661
  for (let i = 2; i <= maxInstances; i++) {
626
662
  const candidate = employeeSessionName(employeeName2, exeSession2, i);
627
- if (!isAlive(candidate)) return i;
663
+ if (!isAlive(candidate) && acquireSpawnLock(candidate)) return i;
628
664
  }
629
665
  return null;
630
666
  }
@@ -936,6 +972,7 @@ function spawnEmployee(employeeName2, exeSession2, projectDir2, opts) {
936
972
  command: spawnCommand
937
973
  });
938
974
  if (spawnResult.error) {
975
+ releaseSpawnLock(sessionName);
939
976
  return { sessionName, error: `tmux new-session failed: ${spawnResult.error}` };
940
977
  }
941
978
  transport.pipeLog(sessionName, logFile);
@@ -973,6 +1010,7 @@ function spawnEmployee(employeeName2, exeSession2, projectDir2, opts) {
973
1010
  }
974
1011
  }
975
1012
  if (!booted) {
1013
+ releaseSpawnLock(sessionName);
976
1014
  return { sessionName, error: `${useExeAgent ? "exe-agent" : "claude"} did not boot within 15s` };
977
1015
  }
978
1016
  if (!useExeAgent) {
@@ -989,9 +1027,10 @@ function spawnEmployee(employeeName2, exeSession2, projectDir2, opts) {
989
1027
  pid: 0,
990
1028
  registeredAt: (/* @__PURE__ */ new Date()).toISOString()
991
1029
  });
1030
+ releaseSpawnLock(sessionName);
992
1031
  return { sessionName };
993
1032
  }
994
- var SESSION_CACHE, BEHAVIORS_EXPORT_TIMEOUT_MS, INTERCOM_DEBOUNCE_MS, INTERCOM_LOG2, DEBOUNCE_FILE, DEBOUNCE_CLEANUP_AGE_MS, BUSY_PATTERN;
1033
+ var SPAWN_LOCK_DIR, SESSION_CACHE, BEHAVIORS_EXPORT_TIMEOUT_MS, INTERCOM_DEBOUNCE_MS, INTERCOM_LOG2, DEBOUNCE_FILE, DEBOUNCE_CLEANUP_AGE_MS, BUSY_PATTERN;
995
1034
  var init_tmux_routing = __esm({
996
1035
  "src/lib/tmux-routing.ts"() {
997
1036
  "use strict";
@@ -1003,6 +1042,7 @@ var init_tmux_routing = __esm({
1003
1042
  init_provider_table();
1004
1043
  init_intercom_queue();
1005
1044
  init_plan_limits();
1045
+ SPAWN_LOCK_DIR = path7.join(os4.homedir(), ".exe-os", "spawn-locks");
1006
1046
  SESSION_CACHE = path7.join(os4.homedir(), ".exe-os", "session-cache");
1007
1047
  BEHAVIORS_EXPORT_TIMEOUT_MS = 1e4;
1008
1048
  INTERCOM_DEBOUNCE_MS = 3e4;
@@ -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;
@@ -2200,7 +2200,8 @@ async function writeMemory(record) {
2200
2200
  has_error: record.has_error ? 1 : 0,
2201
2201
  raw_text: record.raw_text,
2202
2202
  vector: record.vector,
2203
- version: _nextVersion++,
2203
+ version: 0,
2204
+ // Placeholder — assigned atomically at flush time
2204
2205
  task_id: record.task_id ?? null,
2205
2206
  importance: record.importance ?? 5,
2206
2207
  status: record.status ?? "active",
@@ -2234,6 +2235,13 @@ async function flushBatch() {
2234
2235
  _flushing = true;
2235
2236
  try {
2236
2237
  const batch = _pendingRecords.slice(0);
2238
+ const client = getClient();
2239
+ const vResult = await client.execute("SELECT MAX(version) as max_v FROM memories");
2240
+ let baseVersion = (Number(vResult.rows[0]?.max_v) || 0) + 1;
2241
+ for (const row of batch) {
2242
+ row.version = baseVersion++;
2243
+ }
2244
+ _nextVersion = baseVersion;
2237
2245
  const buildStmt = (row) => {
2238
2246
  const hasVector = row.vector !== null;
2239
2247
  const taskId = row.task_id ?? null;
@@ -4690,6 +4698,42 @@ import { readFileSync as readFileSync9, writeFileSync as writeFileSync4, mkdirSy
4690
4698
  import path11 from "path";
4691
4699
  import os4 from "os";
4692
4700
  import { fileURLToPath as fileURLToPath2 } from "url";
4701
+ import { unlinkSync as unlinkSync2 } from "fs";
4702
+ function spawnLockPath(sessionName) {
4703
+ return path11.join(SPAWN_LOCK_DIR, `${sessionName}.lock`);
4704
+ }
4705
+ function isProcessAlive(pid) {
4706
+ try {
4707
+ process.kill(pid, 0);
4708
+ return true;
4709
+ } catch {
4710
+ return false;
4711
+ }
4712
+ }
4713
+ function acquireSpawnLock2(sessionName) {
4714
+ if (!existsSync10(SPAWN_LOCK_DIR)) {
4715
+ mkdirSync6(SPAWN_LOCK_DIR, { recursive: true });
4716
+ }
4717
+ const lockFile = spawnLockPath(sessionName);
4718
+ if (existsSync10(lockFile)) {
4719
+ try {
4720
+ const lock = JSON.parse(readFileSync9(lockFile, "utf8"));
4721
+ const age = Date.now() - lock.timestamp;
4722
+ if (isProcessAlive(lock.pid) && age < 6e4) {
4723
+ return false;
4724
+ }
4725
+ } catch {
4726
+ }
4727
+ }
4728
+ writeFileSync4(lockFile, JSON.stringify({ pid: process.pid, timestamp: Date.now() }));
4729
+ return true;
4730
+ }
4731
+ function releaseSpawnLock2(sessionName) {
4732
+ try {
4733
+ unlinkSync2(spawnLockPath(sessionName));
4734
+ } catch {
4735
+ }
4736
+ }
4693
4737
  function resolveBehaviorsExporterScript() {
4694
4738
  try {
4695
4739
  const thisFile = fileURLToPath2(import.meta.url);
@@ -4770,10 +4814,10 @@ function isEmployeeAlive(sessionName) {
4770
4814
  }
4771
4815
  function findFreeInstance(employeeName, exeSession, maxInstances = 10, isAlive = isEmployeeAlive) {
4772
4816
  const base = employeeSessionName(employeeName, exeSession);
4773
- if (!isAlive(base)) return 0;
4817
+ if (!isAlive(base) && acquireSpawnLock2(base)) return 0;
4774
4818
  for (let i = 2; i <= maxInstances; i++) {
4775
4819
  const candidate = employeeSessionName(employeeName, exeSession, i);
4776
- if (!isAlive(candidate)) return i;
4820
+ if (!isAlive(candidate) && acquireSpawnLock2(candidate)) return i;
4777
4821
  }
4778
4822
  return null;
4779
4823
  }
@@ -5107,6 +5151,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
5107
5151
  command: spawnCommand
5108
5152
  });
5109
5153
  if (spawnResult.error) {
5154
+ releaseSpawnLock2(sessionName);
5110
5155
  return { sessionName, error: `tmux new-session failed: ${spawnResult.error}` };
5111
5156
  }
5112
5157
  transport.pipeLog(sessionName, logFile);
@@ -5144,6 +5189,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
5144
5189
  }
5145
5190
  }
5146
5191
  if (!booted) {
5192
+ releaseSpawnLock2(sessionName);
5147
5193
  return { sessionName, error: `${useExeAgent ? "exe-agent" : "claude"} did not boot within 15s` };
5148
5194
  }
5149
5195
  if (!useExeAgent) {
@@ -5160,9 +5206,10 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
5160
5206
  pid: 0,
5161
5207
  registeredAt: (/* @__PURE__ */ new Date()).toISOString()
5162
5208
  });
5209
+ releaseSpawnLock2(sessionName);
5163
5210
  return { sessionName };
5164
5211
  }
5165
- var SESSION_CACHE, BEHAVIORS_EXPORT_TIMEOUT_MS, INTERCOM_DEBOUNCE_MS, INTERCOM_LOG2, DEBOUNCE_FILE, DEBOUNCE_CLEANUP_AGE_MS, BUSY_PATTERN;
5212
+ var SPAWN_LOCK_DIR, SESSION_CACHE, BEHAVIORS_EXPORT_TIMEOUT_MS, INTERCOM_DEBOUNCE_MS, INTERCOM_LOG2, DEBOUNCE_FILE, DEBOUNCE_CLEANUP_AGE_MS, BUSY_PATTERN;
5166
5213
  var init_tmux_routing = __esm({
5167
5214
  "src/lib/tmux-routing.ts"() {
5168
5215
  "use strict";
@@ -5174,6 +5221,7 @@ var init_tmux_routing = __esm({
5174
5221
  init_provider_table();
5175
5222
  init_intercom_queue();
5176
5223
  init_plan_limits();
5224
+ SPAWN_LOCK_DIR = path11.join(os4.homedir(), ".exe-os", "spawn-locks");
5177
5225
  SESSION_CACHE = path11.join(os4.homedir(), ".exe-os", "session-cache");
5178
5226
  BEHAVIORS_EXPORT_TIMEOUT_MS = 1e4;
5179
5227
  INTERCOM_DEBOUNCE_MS = 3e4;
@@ -5441,7 +5489,7 @@ import os5 from "os";
5441
5489
  import {
5442
5490
  readFileSync as readFileSync10,
5443
5491
  readdirSync,
5444
- unlinkSync as unlinkSync2,
5492
+ unlinkSync as unlinkSync3,
5445
5493
  existsSync as existsSync11,
5446
5494
  rmdirSync
5447
5495
  } from "fs";
@@ -5889,7 +5937,7 @@ var init_tasks_crud = __esm({
5889
5937
 
5890
5938
  // src/lib/tasks-review.ts
5891
5939
  import path14 from "path";
5892
- import { existsSync as existsSync13, readdirSync as readdirSync2, unlinkSync as unlinkSync3 } from "fs";
5940
+ import { existsSync as existsSync13, readdirSync as readdirSync2, unlinkSync as unlinkSync4 } from "fs";
5893
5941
  async function countPendingReviews() {
5894
5942
  const client = getClient();
5895
5943
  const result = await client.execute({
@@ -6013,7 +6061,7 @@ async function cleanupReviewFile(row, taskFile, _baseDir) {
6013
6061
  if (existsSync13(cacheDir)) {
6014
6062
  for (const f of readdirSync2(cacheDir)) {
6015
6063
  if (f.startsWith("review-notified-")) {
6016
- unlinkSync3(path14.join(cacheDir, f));
6064
+ unlinkSync4(path14.join(cacheDir, f));
6017
6065
  }
6018
6066
  }
6019
6067
  }
@@ -6614,7 +6662,7 @@ __export(tasks_exports, {
6614
6662
  writeCheckpoint: () => writeCheckpoint
6615
6663
  });
6616
6664
  import path17 from "path";
6617
- import { writeFileSync as writeFileSync5, mkdirSync as mkdirSync7, unlinkSync as unlinkSync4 } from "fs";
6665
+ import { writeFileSync as writeFileSync5, mkdirSync as mkdirSync7, unlinkSync as unlinkSync5 } from "fs";
6618
6666
  async function createTask(input) {
6619
6667
  const result = await createTaskCore(input);
6620
6668
  if (!input.skipDispatch && result.status !== "blocked" && !process.env.VITEST) {
@@ -6640,7 +6688,7 @@ async function updateTask(input) {
6640
6688
  writeFileSync5(cachePath, JSON.stringify({ taskId, title: String(row.title) }));
6641
6689
  } else if (input.status === "done" || input.status === "blocked" || input.status === "cancelled") {
6642
6690
  try {
6643
- unlinkSync4(cachePath);
6691
+ unlinkSync5(cachePath);
6644
6692
  } catch {
6645
6693
  }
6646
6694
  }
@@ -1501,7 +1501,7 @@ var init_plan_limits = __esm({
1501
1501
  import path11 from "path";
1502
1502
  import os5 from "os";
1503
1503
  import { fileURLToPath as fileURLToPath2 } from "url";
1504
- var SESSION_CACHE, INTERCOM_LOG2, DEBOUNCE_FILE, DEBOUNCE_CLEANUP_AGE_MS;
1504
+ var SPAWN_LOCK_DIR, SESSION_CACHE, INTERCOM_LOG2, DEBOUNCE_FILE, DEBOUNCE_CLEANUP_AGE_MS;
1505
1505
  var init_tmux_routing = __esm({
1506
1506
  "src/lib/tmux-routing.ts"() {
1507
1507
  "use strict";
@@ -1513,6 +1513,7 @@ var init_tmux_routing = __esm({
1513
1513
  init_provider_table();
1514
1514
  init_intercom_queue();
1515
1515
  init_plan_limits();
1516
+ SPAWN_LOCK_DIR = path11.join(os5.homedir(), ".exe-os", "spawn-locks");
1516
1517
  SESSION_CACHE = path11.join(os5.homedir(), ".exe-os", "session-cache");
1517
1518
  INTERCOM_LOG2 = path11.join(os5.homedir(), ".exe-os", "intercom.log");
1518
1519
  DEBOUNCE_FILE = path11.join(SESSION_CACHE, "intercom-debounce.json");
@@ -1427,6 +1427,13 @@ async function flushBatch() {
1427
1427
  _flushing = true;
1428
1428
  try {
1429
1429
  const batch = _pendingRecords.slice(0);
1430
+ const client = getClient();
1431
+ const vResult = await client.execute("SELECT MAX(version) as max_v FROM memories");
1432
+ let baseVersion = (Number(vResult.rows[0]?.max_v) || 0) + 1;
1433
+ for (const row of batch) {
1434
+ row.version = baseVersion++;
1435
+ }
1436
+ _nextVersion = baseVersion;
1430
1437
  const buildStmt = (row) => {
1431
1438
  const hasVector = row.vector !== null;
1432
1439
  const taskId = row.task_id ?? null;
@@ -1295,7 +1295,7 @@ function getActiveAgent() {
1295
1295
  "tmux display-message -p '#{session_name}' 2>/dev/null",
1296
1296
  { encoding: "utf8", timeout: 2e3 }
1297
1297
  ).trim();
1298
- const empMatch = sessionName.match(/^(\w+)-exe\d+$/);
1298
+ const empMatch = sessionName.match(/^([a-zA-Z]+)\d*-exe\d+$/);
1299
1299
  if (empMatch && empMatch[1] !== "exe") {
1300
1300
  return { agentId: empMatch[1], agentRole: "employee" };
1301
1301
  }
@@ -2351,6 +2351,13 @@ async function flushBatch() {
2351
2351
  _flushing = true;
2352
2352
  try {
2353
2353
  const batch = _pendingRecords.slice(0);
2354
+ const client = getClient();
2355
+ const vResult = await client.execute("SELECT MAX(version) as max_v FROM memories");
2356
+ let baseVersion = (Number(vResult.rows[0]?.max_v) || 0) + 1;
2357
+ for (const row of batch) {
2358
+ row.version = baseVersion++;
2359
+ }
2360
+ _nextVersion = baseVersion;
2354
2361
  const buildStmt = (row) => {
2355
2362
  const hasVector = row.vector !== null;
2356
2363
  const taskId = row.task_id ?? null;