@askexenow/exe-os 0.8.33 → 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 (89) hide show
  1. package/dist/bin/backfill-conversations.js +341 -349
  2. package/dist/bin/backfill-responses.js +81 -13
  3. package/dist/bin/backfill-vectors.js +72 -12
  4. package/dist/bin/cleanup-stale-review-tasks.js +63 -3
  5. package/dist/bin/cli.js +1737 -1117
  6. package/dist/bin/exe-assign.js +89 -19
  7. package/dist/bin/exe-boot.js +951 -101
  8. package/dist/bin/exe-call.js +61 -2
  9. package/dist/bin/exe-dispatch.js +61 -13
  10. package/dist/bin/exe-doctor.js +63 -3
  11. package/dist/bin/exe-export-behaviors.js +71 -3
  12. package/dist/bin/exe-forget.js +69 -4
  13. package/dist/bin/exe-gateway.js +178 -45
  14. package/dist/bin/exe-heartbeat.js +79 -14
  15. package/dist/bin/exe-kill.js +71 -3
  16. package/dist/bin/exe-launch-agent.js +148 -14
  17. package/dist/bin/exe-link.js +1437 -0
  18. package/dist/bin/exe-new-employee.js +98 -13
  19. package/dist/bin/exe-pending-messages.js +74 -8
  20. package/dist/bin/exe-pending-notifications.js +63 -3
  21. package/dist/bin/exe-pending-reviews.js +77 -11
  22. package/dist/bin/exe-rename.js +1287 -0
  23. package/dist/bin/exe-review.js +73 -5
  24. package/dist/bin/exe-search.js +88 -14
  25. package/dist/bin/exe-session-cleanup.js +102 -28
  26. package/dist/bin/exe-status.js +64 -4
  27. package/dist/bin/exe-team.js +64 -4
  28. package/dist/bin/git-sweep.js +80 -5
  29. package/dist/bin/graph-backfill.js +71 -3
  30. package/dist/bin/graph-export.js +71 -3
  31. package/dist/bin/install.js +38 -8
  32. package/dist/bin/scan-tasks.js +80 -5
  33. package/dist/bin/setup.js +128 -10
  34. package/dist/bin/shard-migrate.js +71 -3
  35. package/dist/bin/wiki-sync.js +71 -3
  36. package/dist/gateway/index.js +179 -46
  37. package/dist/hooks/bug-report-worker.js +254 -28
  38. package/dist/hooks/commit-complete.js +80 -5
  39. package/dist/hooks/error-recall.js +89 -15
  40. package/dist/hooks/exe-heartbeat-hook.js +1 -1
  41. package/dist/hooks/ingest-worker.js +185 -51
  42. package/dist/hooks/ingest.js +1 -1
  43. package/dist/hooks/instructions-loaded.js +81 -6
  44. package/dist/hooks/notification.js +81 -6
  45. package/dist/hooks/post-compact.js +81 -6
  46. package/dist/hooks/pre-compact.js +81 -6
  47. package/dist/hooks/pre-tool-use.js +423 -196
  48. package/dist/hooks/prompt-ingest-worker.js +91 -23
  49. package/dist/hooks/prompt-submit.js +159 -45
  50. package/dist/hooks/response-ingest-worker.js +96 -23
  51. package/dist/hooks/session-end.js +81 -6
  52. package/dist/hooks/session-start.js +89 -15
  53. package/dist/hooks/stop.js +81 -6
  54. package/dist/hooks/subagent-stop.js +81 -6
  55. package/dist/hooks/summary-worker.js +807 -55
  56. package/dist/index.js +198 -60
  57. package/dist/lib/cloud-sync.js +703 -18
  58. package/dist/lib/consolidation.js +4 -4
  59. package/dist/lib/database.js +64 -2
  60. package/dist/lib/device-registry.js +70 -3
  61. package/dist/lib/employee-templates.js +26 -0
  62. package/dist/lib/employees.js +34 -1
  63. package/dist/lib/exe-daemon.js +207 -74
  64. package/dist/lib/hybrid-search.js +88 -14
  65. package/dist/lib/identity-templates.js +51 -0
  66. package/dist/lib/identity.js +3 -3
  67. package/dist/lib/messaging.js +65 -17
  68. package/dist/lib/reminders.js +3 -3
  69. package/dist/lib/schedules.js +63 -3
  70. package/dist/lib/skill-learning.js +3 -3
  71. package/dist/lib/status-brief.js +63 -5
  72. package/dist/lib/store.js +73 -4
  73. package/dist/lib/task-router.js +4 -2
  74. package/dist/lib/tasks.js +95 -28
  75. package/dist/lib/tmux-routing.js +92 -23
  76. package/dist/mcp/server.js +800 -74
  77. package/dist/mcp/tools/complete-reminder.js +3 -3
  78. package/dist/mcp/tools/create-reminder.js +3 -3
  79. package/dist/mcp/tools/create-task.js +198 -31
  80. package/dist/mcp/tools/deactivate-behavior.js +4 -4
  81. package/dist/mcp/tools/list-reminders.js +3 -3
  82. package/dist/mcp/tools/list-tasks.js +19 -9
  83. package/dist/mcp/tools/send-message.js +69 -21
  84. package/dist/mcp/tools/update-task.js +28 -18
  85. package/dist/runtime/index.js +166 -28
  86. package/dist/tui/App.js +193 -40
  87. package/package.json +7 -3
  88. package/src/commands/exe/afk.md +116 -0
  89. package/src/commands/exe/rename.md +12 -0
package/dist/tui/App.js CHANGED
@@ -39,6 +39,61 @@ var init_devtools = __esm({
39
39
  }
40
40
  });
41
41
 
42
+ // src/lib/db-retry.ts
43
+ function isBusyError(err) {
44
+ if (err instanceof Error) {
45
+ const msg = err.message.toLowerCase();
46
+ return msg.includes("sqlite_busy") || msg.includes("database is locked");
47
+ }
48
+ return false;
49
+ }
50
+ function delay(ms) {
51
+ return new Promise((resolve) => setTimeout(resolve, ms));
52
+ }
53
+ async function retryOnBusy(fn, label) {
54
+ let lastError;
55
+ for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
56
+ try {
57
+ return await fn();
58
+ } catch (err) {
59
+ lastError = err;
60
+ if (!isBusyError(err) || attempt === MAX_RETRIES) {
61
+ throw err;
62
+ }
63
+ const backoff = BASE_DELAY_MS * Math.pow(2, attempt);
64
+ const jitter = Math.floor(Math.random() * MAX_JITTER_MS);
65
+ process.stderr.write(
66
+ `[exe-os] SQLITE_BUSY ${label} retry ${attempt + 1}/${MAX_RETRIES} \u2014 waiting ${backoff + jitter}ms
67
+ `
68
+ );
69
+ await delay(backoff + jitter);
70
+ }
71
+ }
72
+ throw lastError;
73
+ }
74
+ function wrapWithRetry(client) {
75
+ return new Proxy(client, {
76
+ get(target, prop, receiver) {
77
+ if (prop === "execute") {
78
+ return (sql) => retryOnBusy(() => target.execute(sql), "execute");
79
+ }
80
+ if (prop === "batch") {
81
+ return (stmts) => retryOnBusy(() => target.batch(stmts), "batch");
82
+ }
83
+ return Reflect.get(target, prop, receiver);
84
+ }
85
+ });
86
+ }
87
+ var MAX_RETRIES, BASE_DELAY_MS, MAX_JITTER_MS;
88
+ var init_db_retry = __esm({
89
+ "src/lib/db-retry.ts"() {
90
+ "use strict";
91
+ MAX_RETRIES = 3;
92
+ BASE_DELAY_MS = 200;
93
+ MAX_JITTER_MS = 300;
94
+ }
95
+ });
96
+
42
97
  // src/lib/database.ts
43
98
  var database_exports = {};
44
99
  __export(database_exports, {
@@ -46,6 +101,7 @@ __export(database_exports, {
46
101
  disposeTurso: () => disposeTurso,
47
102
  ensureSchema: () => ensureSchema,
48
103
  getClient: () => getClient,
104
+ getRawClient: () => getRawClient,
49
105
  initDatabase: () => initDatabase,
50
106
  initTurso: () => initTurso,
51
107
  isInitialized: () => isInitialized
@@ -55,6 +111,7 @@ async function initDatabase(config) {
55
111
  if (_client) {
56
112
  _client.close();
57
113
  _client = null;
114
+ _resilientClient = null;
58
115
  }
59
116
  const opts = {
60
117
  url: `file:${config.dbPath}`
@@ -63,20 +120,27 @@ async function initDatabase(config) {
63
120
  opts.encryptionKey = config.encryptionKey;
64
121
  }
65
122
  _client = createClient(opts);
123
+ _resilientClient = wrapWithRetry(_client);
66
124
  }
67
125
  function isInitialized() {
68
126
  return _client !== null;
69
127
  }
70
128
  function getClient() {
129
+ if (!_resilientClient) {
130
+ throw new Error("Database client not initialized. Call initDatabase() first.");
131
+ }
132
+ return _resilientClient;
133
+ }
134
+ function getRawClient() {
71
135
  if (!_client) {
72
136
  throw new Error("Database client not initialized. Call initDatabase() first.");
73
137
  }
74
138
  return _client;
75
139
  }
76
140
  async function ensureSchema() {
77
- const client = getClient();
141
+ const client = getRawClient();
78
142
  await client.execute("PRAGMA journal_mode = WAL");
79
- await client.execute("PRAGMA busy_timeout = 5000");
143
+ await client.execute("PRAGMA busy_timeout = 30000");
80
144
  try {
81
145
  await client.execute("PRAGMA libsql_vector_search_ef = 128");
82
146
  } catch {
@@ -869,13 +933,16 @@ async function disposeDatabase() {
869
933
  if (_client) {
870
934
  _client.close();
871
935
  _client = null;
936
+ _resilientClient = null;
872
937
  }
873
938
  }
874
- var _client, initTurso, disposeTurso;
939
+ var _client, _resilientClient, initTurso, disposeTurso;
875
940
  var init_database = __esm({
876
941
  "src/lib/database.ts"() {
877
942
  "use strict";
943
+ init_db_retry();
878
944
  _client = null;
945
+ _resilientClient = null;
879
946
  initTurso = initDatabase;
880
947
  disposeTurso = disposeDatabase;
881
948
  }
@@ -3755,13 +3822,18 @@ __export(employees_exports, {
3755
3822
  EMPLOYEES_PATH: () => EMPLOYEES_PATH,
3756
3823
  addEmployee: () => addEmployee,
3757
3824
  getEmployee: () => getEmployee,
3825
+ getEmployeeByRole: () => getEmployeeByRole,
3826
+ getEmployeeNamesByRole: () => getEmployeeNamesByRole,
3827
+ hasRole: () => hasRole,
3828
+ isMultiInstance: () => isMultiInstance,
3758
3829
  loadEmployees: () => loadEmployees,
3830
+ loadEmployeesSync: () => loadEmployeesSync,
3759
3831
  registerBinSymlinks: () => registerBinSymlinks,
3760
3832
  saveEmployees: () => saveEmployees,
3761
3833
  validateEmployeeName: () => validateEmployeeName
3762
3834
  });
3763
3835
  import { readFile as readFile2, writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
3764
- import { existsSync as existsSync5, symlinkSync, readlinkSync } from "fs";
3836
+ import { existsSync as existsSync5, symlinkSync, readlinkSync, readFileSync as readFileSync5 } from "fs";
3765
3837
  import { execSync as execSync3 } from "child_process";
3766
3838
  import path10 from "path";
3767
3839
  function validateEmployeeName(name) {
@@ -3794,9 +3866,36 @@ async function saveEmployees(employees, employeesPath = EMPLOYEES_PATH) {
3794
3866
  await mkdir2(path10.dirname(employeesPath), { recursive: true });
3795
3867
  await writeFile2(employeesPath, JSON.stringify(employees, null, 2) + "\n", "utf-8");
3796
3868
  }
3869
+ function loadEmployeesSync(employeesPath = EMPLOYEES_PATH) {
3870
+ if (!existsSync5(employeesPath)) return [];
3871
+ try {
3872
+ return JSON.parse(readFileSync5(employeesPath, "utf-8"));
3873
+ } catch {
3874
+ return [];
3875
+ }
3876
+ }
3797
3877
  function getEmployee(employees, name) {
3798
3878
  return employees.find((e) => e.name.toLowerCase() === name.toLowerCase());
3799
3879
  }
3880
+ function getEmployeeByRole(employees, role) {
3881
+ const lower = role.toLowerCase();
3882
+ return employees.find((e) => e.role.toLowerCase() === lower);
3883
+ }
3884
+ function getEmployeeNamesByRole(employees, role) {
3885
+ const lower = role.toLowerCase();
3886
+ return employees.filter((e) => e.role.toLowerCase() === lower).map((e) => e.name);
3887
+ }
3888
+ function hasRole(agentName, role) {
3889
+ const employees = loadEmployeesSync();
3890
+ const emp = getEmployee(employees, agentName);
3891
+ return emp ? emp.role.toLowerCase() === role.toLowerCase() : false;
3892
+ }
3893
+ function isMultiInstance(agentName, employees) {
3894
+ const roster = employees ?? loadEmployeesSync();
3895
+ const emp = getEmployee(roster, agentName);
3896
+ if (!emp) return false;
3897
+ return MULTI_INSTANCE_ROLES.has(emp.role.toLowerCase());
3898
+ }
3800
3899
  function addEmployee(employees, employee) {
3801
3900
  const normalized = { ...employee, name: employee.name.toLowerCase() };
3802
3901
  if (employees.some((e) => e.name.toLowerCase() === normalized.name)) {
@@ -3839,12 +3938,13 @@ function registerBinSymlinks(name) {
3839
3938
  }
3840
3939
  return { created, skipped, errors };
3841
3940
  }
3842
- var EMPLOYEES_PATH;
3941
+ var EMPLOYEES_PATH, MULTI_INSTANCE_ROLES;
3843
3942
  var init_employees = __esm({
3844
3943
  "src/lib/employees.ts"() {
3845
3944
  "use strict";
3846
3945
  init_config();
3847
3946
  EMPLOYEES_PATH = path10.join(EXE_AI_DIR, "exe-employees.json");
3947
+ MULTI_INSTANCE_ROLES = /* @__PURE__ */ new Set(["principal engineer", "content production specialist", "staff code reviewer"]);
3848
3948
  }
3849
3949
  });
3850
3950
 
@@ -3918,12 +4018,14 @@ var init_task_router = __esm({
3918
4018
  },
3919
4019
  tierRules: {
3920
4020
  junior: {
3921
- eligible: ["tom"],
4021
+ eligible: [],
4022
+ // resolved dynamically from roster (Principal Engineer role)
3922
4023
  reviewRequired: false,
3923
4024
  manualOnly: false
3924
4025
  },
3925
4026
  standard: {
3926
- eligible: ["tom"],
4027
+ eligible: [],
4028
+ // resolved dynamically from roster (Principal Engineer role)
3927
4029
  reviewRequired: false,
3928
4030
  manualOnly: false
3929
4031
  },
@@ -4167,7 +4269,7 @@ var init_provider_table = __esm({
4167
4269
  });
4168
4270
 
4169
4271
  // src/lib/intercom-queue.ts
4170
- import { readFileSync as readFileSync5, writeFileSync as writeFileSync3, renameSync as renameSync2, existsSync as existsSync6, mkdirSync as mkdirSync3 } from "fs";
4272
+ import { readFileSync as readFileSync6, writeFileSync as writeFileSync3, renameSync as renameSync2, existsSync as existsSync6, mkdirSync as mkdirSync3 } from "fs";
4171
4273
  import path11 from "path";
4172
4274
  import os4 from "os";
4173
4275
  function ensureDir() {
@@ -4177,7 +4279,7 @@ function ensureDir() {
4177
4279
  function readQueue() {
4178
4280
  try {
4179
4281
  if (!existsSync6(QUEUE_PATH)) return [];
4180
- return JSON.parse(readFileSync5(QUEUE_PATH, "utf8"));
4282
+ return JSON.parse(readFileSync6(QUEUE_PATH, "utf8"));
4181
4283
  } catch {
4182
4284
  return [];
4183
4285
  }
@@ -4216,12 +4318,12 @@ var init_intercom_queue = __esm({
4216
4318
  });
4217
4319
 
4218
4320
  // src/lib/plan-limits.ts
4219
- import { readFileSync as readFileSync6, existsSync as existsSync7 } from "fs";
4321
+ import { readFileSync as readFileSync7, existsSync as existsSync7 } from "fs";
4220
4322
  import path12 from "path";
4221
4323
  function getLicenseSync() {
4222
4324
  try {
4223
4325
  if (!existsSync7(CACHE_PATH2)) return freeLicense();
4224
- const raw = JSON.parse(readFileSync6(CACHE_PATH2, "utf8"));
4326
+ const raw = JSON.parse(readFileSync7(CACHE_PATH2, "utf8"));
4225
4327
  if (!raw.token || typeof raw.token !== "string") return freeLicense();
4226
4328
  const parts = raw.token.split(".");
4227
4329
  if (parts.length !== 3) return freeLicense();
@@ -4260,7 +4362,7 @@ function assertEmployeeLimitSync(rosterPath) {
4260
4362
  let count = 0;
4261
4363
  try {
4262
4364
  if (existsSync7(filePath)) {
4263
- const raw = readFileSync6(filePath, "utf8");
4365
+ const raw = readFileSync7(filePath, "utf8");
4264
4366
  const employees = JSON.parse(raw);
4265
4367
  count = Array.isArray(employees) ? employees.length : 0;
4266
4368
  }
@@ -4298,7 +4400,7 @@ import crypto from "crypto";
4298
4400
  import path13 from "path";
4299
4401
  import os5 from "os";
4300
4402
  import {
4301
- readFileSync as readFileSync7,
4403
+ readFileSync as readFileSync8,
4302
4404
  readdirSync,
4303
4405
  unlinkSync,
4304
4406
  existsSync as existsSync8,
@@ -4384,7 +4486,7 @@ import crypto3 from "crypto";
4384
4486
  import path14 from "path";
4385
4487
  import { execSync as execSync6 } from "child_process";
4386
4488
  import { mkdir as mkdir3, writeFile as writeFile3, appendFile } from "fs/promises";
4387
- import { existsSync as existsSync9, readFileSync as readFileSync8 } from "fs";
4489
+ import { existsSync as existsSync9, readFileSync as readFileSync9 } from "fs";
4388
4490
  async function writeCheckpoint(input) {
4389
4491
  const client = getClient();
4390
4492
  const row = await resolveTask(client, input.taskId);
@@ -4761,7 +4863,7 @@ async function ensureGitignoreExe(baseDir) {
4761
4863
  const gitignorePath = path14.join(baseDir, ".gitignore");
4762
4864
  try {
4763
4865
  if (existsSync9(gitignorePath)) {
4764
- const content = readFileSync8(gitignorePath, "utf-8");
4866
+ const content = readFileSync9(gitignorePath, "utf-8");
4765
4867
  if (/^\/?exe\/?$/m.test(content)) return;
4766
4868
  await appendFile(gitignorePath, "\n# Employee task assignments (private)\n/exe/\n");
4767
4869
  } else {
@@ -5137,7 +5239,7 @@ async function dispatchTaskToEmployee(input) {
5137
5239
  } else {
5138
5240
  const projectDir = input.projectDir ?? process.cwd();
5139
5241
  const result = ensureEmployee(input.assignedTo, exeSession, projectDir, {
5140
- autoInstance: input.assignedTo === "tom" || input.assignedTo === "sasha"
5242
+ autoInstance: isMultiInstance(input.assignedTo)
5141
5243
  });
5142
5244
  if (result.status === "failed") {
5143
5245
  process.stderr.write(
@@ -5172,6 +5274,7 @@ var init_tasks_notify = __esm({
5172
5274
  init_session_key();
5173
5275
  init_notifications();
5174
5276
  init_transport();
5277
+ init_employees();
5175
5278
  }
5176
5279
  });
5177
5280
 
@@ -5925,6 +6028,7 @@ var init_capacity_monitor = __esm({
5925
6028
  // src/lib/tmux-routing.ts
5926
6029
  var tmux_routing_exports = {};
5927
6030
  __export(tmux_routing_exports, {
6031
+ acquireSpawnLock: () => acquireSpawnLock,
5928
6032
  employeeSessionName: () => employeeSessionName,
5929
6033
  ensureEmployee: () => ensureEmployee,
5930
6034
  extractRootExe: () => extractRootExe,
@@ -5939,16 +6043,53 @@ __export(tmux_routing_exports, {
5939
6043
  notifyParentExe: () => notifyParentExe,
5940
6044
  parseParentExe: () => parseParentExe,
5941
6045
  registerParentExe: () => registerParentExe,
6046
+ releaseSpawnLock: () => releaseSpawnLock,
5942
6047
  resolveExeSession: () => resolveExeSession,
5943
6048
  sendIntercom: () => sendIntercom,
5944
6049
  spawnEmployee: () => spawnEmployee,
5945
6050
  verifyPaneAtCapacity: () => verifyPaneAtCapacity
5946
6051
  });
5947
6052
  import { execFileSync as execFileSync3, execSync as execSync8 } from "child_process";
5948
- import { readFileSync as readFileSync9, writeFileSync as writeFileSync5, mkdirSync as mkdirSync5, existsSync as existsSync11, appendFileSync } from "fs";
6053
+ import { readFileSync as readFileSync10, writeFileSync as writeFileSync5, mkdirSync as mkdirSync5, existsSync as existsSync11, appendFileSync } from "fs";
5949
6054
  import path19 from "path";
5950
6055
  import os6 from "os";
5951
6056
  import { fileURLToPath } from "url";
6057
+ import { unlinkSync as unlinkSync4 } from "fs";
6058
+ function spawnLockPath(sessionName) {
6059
+ return path19.join(SPAWN_LOCK_DIR, `${sessionName}.lock`);
6060
+ }
6061
+ function isProcessAlive(pid) {
6062
+ try {
6063
+ process.kill(pid, 0);
6064
+ return true;
6065
+ } catch {
6066
+ return false;
6067
+ }
6068
+ }
6069
+ function acquireSpawnLock(sessionName) {
6070
+ if (!existsSync11(SPAWN_LOCK_DIR)) {
6071
+ mkdirSync5(SPAWN_LOCK_DIR, { recursive: true });
6072
+ }
6073
+ const lockFile = spawnLockPath(sessionName);
6074
+ if (existsSync11(lockFile)) {
6075
+ try {
6076
+ const lock = JSON.parse(readFileSync10(lockFile, "utf8"));
6077
+ const age = Date.now() - lock.timestamp;
6078
+ if (isProcessAlive(lock.pid) && age < 6e4) {
6079
+ return false;
6080
+ }
6081
+ } catch {
6082
+ }
6083
+ }
6084
+ writeFileSync5(lockFile, JSON.stringify({ pid: process.pid, timestamp: Date.now() }));
6085
+ return true;
6086
+ }
6087
+ function releaseSpawnLock(sessionName) {
6088
+ try {
6089
+ unlinkSync4(spawnLockPath(sessionName));
6090
+ } catch {
6091
+ }
6092
+ }
5952
6093
  function resolveBehaviorsExporterScript() {
5953
6094
  try {
5954
6095
  const thisFile = fileURLToPath(import.meta.url);
@@ -6012,7 +6153,7 @@ function registerParentExe(sessionKey, parentExe, dispatchedBy) {
6012
6153
  }
6013
6154
  function getParentExe(sessionKey) {
6014
6155
  try {
6015
- const data = JSON.parse(readFileSync9(path19.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`), "utf8"));
6156
+ const data = JSON.parse(readFileSync10(path19.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`), "utf8"));
6016
6157
  return data.parentExe || null;
6017
6158
  } catch {
6018
6159
  return null;
@@ -6020,7 +6161,7 @@ function getParentExe(sessionKey) {
6020
6161
  }
6021
6162
  function getDispatchedBy(sessionKey) {
6022
6163
  try {
6023
- const data = JSON.parse(readFileSync9(
6164
+ const data = JSON.parse(readFileSync10(
6024
6165
  path19.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`),
6025
6166
  "utf8"
6026
6167
  ));
@@ -6047,10 +6188,10 @@ function isEmployeeAlive(sessionName) {
6047
6188
  }
6048
6189
  function findFreeInstance(employeeName, exeSession, maxInstances = 10, isAlive = isEmployeeAlive) {
6049
6190
  const base = employeeSessionName(employeeName, exeSession);
6050
- if (!isAlive(base)) return 0;
6191
+ if (!isAlive(base) && acquireSpawnLock(base)) return 0;
6051
6192
  for (let i = 2; i <= maxInstances; i++) {
6052
6193
  const candidate = employeeSessionName(employeeName, exeSession, i);
6053
- if (!isAlive(candidate)) return i;
6194
+ if (!isAlive(candidate) && acquireSpawnLock(candidate)) return i;
6054
6195
  }
6055
6196
  return null;
6056
6197
  }
@@ -6083,7 +6224,7 @@ async function verifyPaneAtCapacity(sessionName) {
6083
6224
  function readDebounceState() {
6084
6225
  try {
6085
6226
  if (!existsSync11(DEBOUNCE_FILE)) return {};
6086
- return JSON.parse(readFileSync9(DEBOUNCE_FILE, "utf8"));
6227
+ return JSON.parse(readFileSync10(DEBOUNCE_FILE, "utf8"));
6087
6228
  } catch {
6088
6229
  return {};
6089
6230
  }
@@ -6283,7 +6424,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
6283
6424
  const claudeJsonPath = path19.join(os6.homedir(), ".claude.json");
6284
6425
  let claudeJson = {};
6285
6426
  try {
6286
- claudeJson = JSON.parse(readFileSync9(claudeJsonPath, "utf8"));
6427
+ claudeJson = JSON.parse(readFileSync10(claudeJsonPath, "utf8"));
6287
6428
  } catch {
6288
6429
  }
6289
6430
  if (!claudeJson.projects) claudeJson.projects = {};
@@ -6301,7 +6442,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
6301
6442
  const settingsPath = path19.join(projSettingsDir, "settings.json");
6302
6443
  let settings = {};
6303
6444
  try {
6304
- settings = JSON.parse(readFileSync9(settingsPath, "utf8"));
6445
+ settings = JSON.parse(readFileSync10(settingsPath, "utf8"));
6305
6446
  } catch {
6306
6447
  }
6307
6448
  const perms = settings.permissions ?? {};
@@ -6414,6 +6555,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
6414
6555
  command: spawnCommand
6415
6556
  });
6416
6557
  if (spawnResult.error) {
6558
+ releaseSpawnLock(sessionName);
6417
6559
  return { sessionName, error: `tmux new-session failed: ${spawnResult.error}` };
6418
6560
  }
6419
6561
  transport.pipeLog(sessionName, logFile);
@@ -6451,6 +6593,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
6451
6593
  }
6452
6594
  }
6453
6595
  if (!booted) {
6596
+ releaseSpawnLock(sessionName);
6454
6597
  return { sessionName, error: `${useExeAgent ? "exe-agent" : "claude"} did not boot within 15s` };
6455
6598
  }
6456
6599
  if (!useExeAgent) {
@@ -6467,9 +6610,10 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
6467
6610
  pid: 0,
6468
6611
  registeredAt: (/* @__PURE__ */ new Date()).toISOString()
6469
6612
  });
6613
+ releaseSpawnLock(sessionName);
6470
6614
  return { sessionName };
6471
6615
  }
6472
- var SESSION_CACHE, BEHAVIORS_EXPORT_TIMEOUT_MS, VERIFY_PANE_LINES, INTERCOM_DEBOUNCE_MS, INTERCOM_LOG2, DEBOUNCE_FILE, DEBOUNCE_CLEANUP_AGE_MS, BUSY_PATTERN;
6616
+ 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;
6473
6617
  var init_tmux_routing = __esm({
6474
6618
  "src/lib/tmux-routing.ts"() {
6475
6619
  "use strict";
@@ -6481,6 +6625,7 @@ var init_tmux_routing = __esm({
6481
6625
  init_provider_table();
6482
6626
  init_intercom_queue();
6483
6627
  init_plan_limits();
6628
+ SPAWN_LOCK_DIR = path19.join(os6.homedir(), ".exe-os", "spawn-locks");
6484
6629
  SESSION_CACHE = path19.join(os6.homedir(), ".exe-os", "session-cache");
6485
6630
  BEHAVIORS_EXPORT_TIMEOUT_MS = 1e4;
6486
6631
  VERIFY_PANE_LINES = 200;
@@ -7370,7 +7515,7 @@ function listShards() {
7370
7515
  }
7371
7516
  async function ensureShardSchema(client) {
7372
7517
  await client.execute("PRAGMA journal_mode = WAL");
7373
- await client.execute("PRAGMA busy_timeout = 5000");
7518
+ await client.execute("PRAGMA busy_timeout = 30000");
7374
7519
  try {
7375
7520
  await client.execute("PRAGMA libsql_vector_search_ef = 128");
7376
7521
  } catch {
@@ -7631,7 +7776,8 @@ async function writeMemory(record) {
7631
7776
  has_error: record.has_error ? 1 : 0,
7632
7777
  raw_text: record.raw_text,
7633
7778
  vector: record.vector,
7634
- version: _nextVersion++,
7779
+ version: 0,
7780
+ // Placeholder — assigned atomically at flush time
7635
7781
  task_id: record.task_id ?? null,
7636
7782
  importance: record.importance ?? 5,
7637
7783
  status: record.status ?? "active",
@@ -7665,6 +7811,13 @@ async function flushBatch() {
7665
7811
  _flushing = true;
7666
7812
  try {
7667
7813
  const batch = _pendingRecords.slice(0);
7814
+ const client = getClient();
7815
+ const vResult = await client.execute("SELECT MAX(version) as max_v FROM memories");
7816
+ let baseVersion = (Number(vResult.rows[0]?.max_v) || 0) + 1;
7817
+ for (const row of batch) {
7818
+ row.version = baseVersion++;
7819
+ }
7820
+ _nextVersion = baseVersion;
7668
7821
  const buildStmt = (row) => {
7669
7822
  const hasVector = row.vector !== null;
7670
7823
  const taskId = row.task_id ?? null;
@@ -14635,7 +14788,7 @@ function CommandCenterView({
14635
14788
  const { createPermissionsFromPreset: createPermissionsFromPreset2, EMPLOYEE_PERMISSIONS: EMPLOYEE_PERMISSIONS2 } = await Promise.resolve().then(() => (init_permissions(), permissions_exports));
14636
14789
  const { getPresetByRole: getPresetByRole2 } = await Promise.resolve().then(() => (init_permission_presets(), permission_presets_exports));
14637
14790
  const { createDefaultHooks: createDefaultHooks2 } = await Promise.resolve().then(() => (init_hooks(), hooks_exports));
14638
- const { readFileSync: readFileSync10, existsSync: existsSync14 } = await import("fs");
14791
+ const { readFileSync: readFileSync11, existsSync: existsSync14 } = await import("fs");
14639
14792
  const { join } = await import("path");
14640
14793
  const { homedir } = await import("os");
14641
14794
  const configPath = join(homedir(), ".exe-os", "config.json");
@@ -14643,7 +14796,7 @@ function CommandCenterView({
14643
14796
  let providerConfigs = {};
14644
14797
  if (existsSync14(configPath)) {
14645
14798
  try {
14646
- const raw = JSON.parse(readFileSync10(configPath, "utf8"));
14799
+ const raw = JSON.parse(readFileSync11(configPath, "utf8"));
14647
14800
  if (Array.isArray(raw.failoverChain)) failoverChain = raw.failoverChain;
14648
14801
  if (raw.providers && typeof raw.providers === "object") {
14649
14802
  providerConfigs = raw.providers;
@@ -14704,7 +14857,7 @@ function CommandCenterView({
14704
14857
  const markerDir = join(homedir(), ".exe-os", "session-cache");
14705
14858
  const agentFiles = (await import("fs")).readdirSync(markerDir).filter((f) => f.startsWith("active-agent-"));
14706
14859
  for (const f of agentFiles) {
14707
- const data = JSON.parse(readFileSync10(join(markerDir, f), "utf8"));
14860
+ const data = JSON.parse(readFileSync11(join(markerDir, f), "utf8"));
14708
14861
  if (data.agentRole) {
14709
14862
  agentRole = data.agentRole;
14710
14863
  break;
@@ -16397,12 +16550,12 @@ async function loadGatewayConfig() {
16397
16550
  state.running = false;
16398
16551
  }
16399
16552
  try {
16400
- const { existsSync: existsSync14, readFileSync: readFileSync10 } = await import("fs");
16553
+ const { existsSync: existsSync14, readFileSync: readFileSync11 } = await import("fs");
16401
16554
  const { join } = await import("path");
16402
16555
  const home = process.env.HOME ?? "";
16403
16556
  const configPath = join(home, ".exe-os", "gateway.json");
16404
16557
  if (existsSync14(configPath)) {
16405
- const raw = JSON.parse(readFileSync10(configPath, "utf8"));
16558
+ const raw = JSON.parse(readFileSync11(configPath, "utf8"));
16406
16559
  state.port = raw.port ?? 3100;
16407
16560
  state.gatewayUrl = raw.gatewayUrl ?? "";
16408
16561
  if (raw.adapters) {
@@ -16872,12 +17025,12 @@ function TeamView({ onBack }) {
16872
17025
  setMembers(teamData);
16873
17026
  setDbError(false);
16874
17027
  try {
16875
- const { existsSync: existsSync14, readFileSync: readFileSync10 } = await import("fs");
17028
+ const { existsSync: existsSync14, readFileSync: readFileSync11 } = await import("fs");
16876
17029
  const { join } = await import("path");
16877
17030
  const home = process.env.HOME ?? "";
16878
17031
  const gatewayConfig = join(home, ".exe-os", "gateway.json");
16879
17032
  if (existsSync14(gatewayConfig)) {
16880
- const raw = JSON.parse(readFileSync10(gatewayConfig, "utf8"));
17033
+ const raw = JSON.parse(readFileSync11(gatewayConfig, "utf8"));
16881
17034
  if (raw.agents && raw.agents.length > 0) {
16882
17035
  setExternals(raw.agents.map((a) => ({
16883
17036
  name: a.name,
@@ -17413,14 +17566,14 @@ function SettingsView({ onBack }) {
17413
17566
  try {
17414
17567
  const { loadEmployees: loadEmployees2 } = await Promise.resolve().then(() => (init_employees(), employees_exports));
17415
17568
  const roster = await loadEmployees2();
17416
- const { existsSync: existsSync14, readFileSync: readFileSync10 } = await import("fs");
17569
+ const { existsSync: existsSync14, readFileSync: readFileSync11 } = await import("fs");
17417
17570
  const { join } = await import("path");
17418
17571
  const home = process.env.HOME ?? "";
17419
17572
  const configPath = join(home, ".exe-os", "config.json");
17420
17573
  let empConfig = {};
17421
17574
  if (existsSync14(configPath)) {
17422
17575
  try {
17423
- const raw = JSON.parse(readFileSync10(configPath, "utf8"));
17576
+ const raw = JSON.parse(readFileSync11(configPath, "utf8"));
17424
17577
  if (raw.employees && typeof raw.employees === "object") {
17425
17578
  empConfig = raw.employees;
17426
17579
  }
@@ -17435,7 +17588,7 @@ function SettingsView({ onBack }) {
17435
17588
  } catch {
17436
17589
  }
17437
17590
  try {
17438
- const { existsSync: existsSync14, readFileSync: readFileSync10 } = await import("fs");
17591
+ const { existsSync: existsSync14, readFileSync: readFileSync11 } = await import("fs");
17439
17592
  const { join } = await import("path");
17440
17593
  const home = process.env.HOME ?? "";
17441
17594
  const ccSettingsPath = join(home, ".claude", "settings.json");
@@ -17443,7 +17596,7 @@ function SettingsView({ onBack }) {
17443
17596
  let hooksWired = false;
17444
17597
  if (installed) {
17445
17598
  try {
17446
- const settings = JSON.parse(readFileSync10(ccSettingsPath, "utf8"));
17599
+ const settings = JSON.parse(readFileSync11(ccSettingsPath, "utf8"));
17447
17600
  const hooks = settings.hooks;
17448
17601
  if (hooks) {
17449
17602
  hooksWired = Object.values(hooks).flat().some((h) => h.command?.includes("exe-os") || h.command?.includes("exe-mem"));
@@ -17455,13 +17608,13 @@ function SettingsView({ onBack }) {
17455
17608
  } catch {
17456
17609
  }
17457
17610
  try {
17458
- const { existsSync: existsSync14, readFileSync: readFileSync10 } = await import("fs");
17611
+ const { existsSync: existsSync14, readFileSync: readFileSync11 } = await import("fs");
17459
17612
  const { join } = await import("path");
17460
17613
  const home = process.env.HOME ?? "";
17461
17614
  const licensePath = join(home, ".exe-os", "license.json");
17462
17615
  if (existsSync14(licensePath)) {
17463
17616
  try {
17464
- const lic = JSON.parse(readFileSync10(licensePath, "utf8"));
17617
+ const lic = JSON.parse(readFileSync11(licensePath, "utf8"));
17465
17618
  setLicense({ valid: lic.valid !== false, detail: lic.plan ?? "licensed" });
17466
17619
  } catch {
17467
17620
  setLicense({ valid: false, detail: "invalid license file" });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@askexenow/exe-os",
3
- "version": "0.8.33",
3
+ "version": "0.8.37",
4
4
  "description": "AI employee operating system — persistent memory, task management, and multi-agent coordination for Claude Code.",
5
5
  "license": "CC-BY-NC-4.0",
6
6
  "type": "module",
@@ -27,6 +27,7 @@
27
27
  "exe-os-install": "./dist/bin/install.js",
28
28
  "exe-agent": "./dist/bin/exe-agent.js",
29
29
  "exe-os-backfill-responses": "./dist/bin/backfill-responses.js",
30
+ "exe-os-backfill-conversations": "./dist/bin/backfill-conversations.js",
30
31
  "exe-os-repo-drift": "./dist/bin/exe-repo-drift.js",
31
32
  "exe-kill": "./dist/bin/exe-kill.js",
32
33
  "exe-os-list-providers": "./dist/bin/list-providers.js",
@@ -58,14 +59,14 @@
58
59
  "./mcp/server": "./dist/mcp/server.js"
59
60
  },
60
61
  "engines": {
61
- "node": ">=18.0.0"
62
+ "node": ">=20.0.0"
62
63
  },
63
64
  "scripts": {
64
65
  "test": "vitest run",
65
66
  "test:watch": "vitest",
66
67
  "typecheck": "tsc --noEmit",
67
68
  "build": "tsup",
68
- "deploy": "tsup && npm install -g . && node dist/bin/install.js --global",
69
+ "deploy": "node dist/bin/pre-build-guard.js 2>/dev/null; tsup && npm install -g . && node dist/bin/install.js --global && echo '[exe-os] Deploy complete. MCP servers will auto-reconnect on next tool call.'",
69
70
  "postinstall": "node dist/bin/install.js --global 2>/dev/null || true",
70
71
  "prepublishOnly": "npm run typecheck && npm run build && npx vitest run --exclude 'tests/tui/**' --exclude 'tests/lib/tmux-routing.test.ts' --exclude 'tests/lib/intercom-routing.test.ts' --exclude 'tests/gateway/**' --exclude 'tests/installer/setup-wizard.test.ts' --exclude 'tests/mcp/ingest-document.test.ts' --exclude 'tests/lib/hybrid-search.test.ts'"
71
72
  },
@@ -74,6 +75,9 @@
74
75
  "@discordjs/voice": "^0.19.2",
75
76
  "@libsql/client": "^0.14.0",
76
77
  "@modelcontextprotocol/sdk": "^1.27.1",
78
+ "@opentelemetry/api": "^1.9.1",
79
+ "@opentelemetry/sdk-node": "^0.215.0",
80
+ "@opentelemetry/sdk-trace-base": "^2.7.0",
77
81
  "@slack/bolt": "^4.7.0",
78
82
  "@slack/web-api": "^7.15.1",
79
83
  "@types/react": "^19.2.14",