@askexenow/exe-os 0.9.113 → 0.9.115

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 (86) hide show
  1. package/dist/bin/agentic-ontology-backfill.js +36 -12
  2. package/dist/bin/agentic-reflection-backfill.js +36 -12
  3. package/dist/bin/agentic-semantic-label.js +36 -12
  4. package/dist/bin/backfill-conversations.js +36 -12
  5. package/dist/bin/backfill-responses.js +36 -12
  6. package/dist/bin/backfill-vectors.js +36 -12
  7. package/dist/bin/bulk-sync-postgres.js +36 -12
  8. package/dist/bin/cleanup-stale-review-tasks.js +470 -113
  9. package/dist/bin/cli.js +413 -62
  10. package/dist/bin/exe-agent.js +27 -0
  11. package/dist/bin/exe-assign.js +36 -12
  12. package/dist/bin/exe-boot.js +246 -54
  13. package/dist/bin/exe-call.js +8 -0
  14. package/dist/bin/exe-cloud.js +47 -12
  15. package/dist/bin/exe-dispatch.js +348 -53
  16. package/dist/bin/exe-doctor.js +51 -13
  17. package/dist/bin/exe-export-behaviors.js +37 -12
  18. package/dist/bin/exe-forget.js +36 -12
  19. package/dist/bin/exe-gateway.js +348 -53
  20. package/dist/bin/exe-heartbeat.js +471 -113
  21. package/dist/bin/exe-kill.js +36 -12
  22. package/dist/bin/exe-launch-agent.js +117 -18
  23. package/dist/bin/exe-new-employee.js +9 -1
  24. package/dist/bin/exe-pending-messages.js +452 -95
  25. package/dist/bin/exe-pending-notifications.js +452 -95
  26. package/dist/bin/exe-pending-reviews.js +452 -95
  27. package/dist/bin/exe-rename.js +36 -12
  28. package/dist/bin/exe-review.js +36 -12
  29. package/dist/bin/exe-search.js +37 -12
  30. package/dist/bin/exe-session-cleanup.js +348 -53
  31. package/dist/bin/exe-settings.js +12 -0
  32. package/dist/bin/exe-start-codex.js +46 -13
  33. package/dist/bin/exe-start-opencode.js +46 -13
  34. package/dist/bin/exe-status.js +460 -114
  35. package/dist/bin/exe-support.js +12 -0
  36. package/dist/bin/exe-team.js +36 -12
  37. package/dist/bin/git-sweep.js +348 -53
  38. package/dist/bin/graph-backfill.js +36 -12
  39. package/dist/bin/graph-export.js +36 -12
  40. package/dist/bin/install.js +9 -1
  41. package/dist/bin/intercom-check.js +255 -53
  42. package/dist/bin/scan-tasks.js +348 -53
  43. package/dist/bin/setup.js +74 -12
  44. package/dist/bin/shard-migrate.js +36 -12
  45. package/dist/gateway/index.js +348 -53
  46. package/dist/hooks/bug-report-worker.js +348 -53
  47. package/dist/hooks/codex-stop-task-finalizer.js +308 -37
  48. package/dist/hooks/commit-complete.js +348 -53
  49. package/dist/hooks/error-recall.js +37 -12
  50. package/dist/hooks/ingest.js +363 -54
  51. package/dist/hooks/instructions-loaded.js +36 -12
  52. package/dist/hooks/notification.js +36 -12
  53. package/dist/hooks/post-compact.js +426 -72
  54. package/dist/hooks/post-tool-combined.js +501 -146
  55. package/dist/hooks/pre-compact.js +348 -53
  56. package/dist/hooks/pre-tool-use.js +92 -13
  57. package/dist/hooks/prompt-submit.js +348 -53
  58. package/dist/hooks/session-end.js +158 -53
  59. package/dist/hooks/session-start.js +66 -13
  60. package/dist/hooks/stop.js +420 -72
  61. package/dist/hooks/subagent-stop.js +419 -72
  62. package/dist/hooks/summary-worker.js +442 -121
  63. package/dist/index.js +375 -53
  64. package/dist/lib/agent-config.js +8 -0
  65. package/dist/lib/cloud-sync.js +35 -12
  66. package/dist/lib/config.js +13 -0
  67. package/dist/lib/consolidation.js +9 -1
  68. package/dist/lib/embedder.js +13 -0
  69. package/dist/lib/employees.js +8 -0
  70. package/dist/lib/exe-daemon.js +524 -60
  71. package/dist/lib/hybrid-search.js +37 -12
  72. package/dist/lib/keychain.js +25 -13
  73. package/dist/lib/messaging.js +395 -74
  74. package/dist/lib/schedules.js +36 -12
  75. package/dist/lib/skill-learning.js +21 -0
  76. package/dist/lib/store.js +36 -12
  77. package/dist/lib/tasks.js +324 -41
  78. package/dist/lib/tmux-routing.js +324 -41
  79. package/dist/mcp/server.js +374 -54
  80. package/dist/mcp/tools/create-task.js +324 -41
  81. package/dist/mcp/tools/list-tasks.js +406 -57
  82. package/dist/mcp/tools/send-message.js +395 -74
  83. package/dist/mcp/tools/update-task.js +324 -41
  84. package/dist/runtime/index.js +375 -53
  85. package/dist/tui/App.js +377 -55
  86. package/package.json +1 -1
@@ -143,6 +143,17 @@ function normalizeOrchestration(raw) {
143
143
  const userOrg = raw.orchestration ?? {};
144
144
  raw.orchestration = { ...defaultOrg, ...userOrg };
145
145
  }
146
+ function normalizeCloudEndpoint(raw) {
147
+ const cloud = raw.cloud;
148
+ if (!cloud?.endpoint) return;
149
+ const ep = String(cloud.endpoint);
150
+ if (ep === "https://askexe.com/cloud" || ep === "https://askexe.com/cloud/") {
151
+ cloud.endpoint = "https://cloud.askexe.com";
152
+ process.stderr.write(
153
+ "[config] Auto-migrated cloud endpoint: askexe.com/cloud \u2192 cloud.askexe.com\n"
154
+ );
155
+ }
156
+ }
146
157
  async function loadConfig() {
147
158
  const dir = process.env.EXE_OS_DIR ?? process.env.EXE_MEM_DIR ?? EXE_AI_DIR;
148
159
  await ensurePrivateDir(dir);
@@ -168,6 +179,7 @@ async function loadConfig() {
168
179
  normalizeSessionLifecycle(migratedCfg);
169
180
  normalizeAutoUpdate(migratedCfg);
170
181
  normalizeOrchestration(migratedCfg);
182
+ normalizeCloudEndpoint(migratedCfg);
171
183
  const config = { ...DEFAULT_CONFIG, dbPath: path.join(dir, "memories.db"), ...migratedCfg };
172
184
  if (config.dbPath.startsWith("~")) {
173
185
  config.dbPath = config.dbPath.replace(/^~/, os.homedir());
@@ -196,6 +208,7 @@ function loadConfigSync() {
196
208
  normalizeSessionLifecycle(migratedCfg);
197
209
  normalizeAutoUpdate(migratedCfg);
198
210
  normalizeOrchestration(migratedCfg);
211
+ normalizeCloudEndpoint(migratedCfg);
199
212
  const config = { ...DEFAULT_CONFIG, dbPath: path.join(dir, "memories.db"), ...migratedCfg };
200
213
  if (config.dbPath.startsWith("~")) {
201
214
  config.dbPath = config.dbPath.replace(/^~/, os.homedir());
@@ -356,6 +369,7 @@ __export(agent_config_exports, {
356
369
  clearAgentRuntime: () => clearAgentRuntime,
357
370
  getAgentRuntime: () => getAgentRuntime,
358
371
  loadAgentConfig: () => loadAgentConfig,
372
+ normalizeCcModelName: () => normalizeCcModelName,
359
373
  saveAgentConfig: () => saveAgentConfig,
360
374
  setAgentMcps: () => setAgentMcps,
361
375
  setAgentRuntime: () => setAgentRuntime
@@ -384,6 +398,13 @@ function getAgentRuntime(agentId) {
384
398
  if (orgDefault) return orgDefault;
385
399
  return { runtime: DEFAULT_RUNTIME, model: DEFAULT_MODELS[DEFAULT_RUNTIME] };
386
400
  }
401
+ function normalizeCcModelName(model) {
402
+ let ccModel = model.replace(/(\d+)\.(\d+)/g, "$1-$2");
403
+ if (/claude-(opus|sonnet)-4-[6-9]/.test(ccModel) && !ccModel.includes("[1m]")) {
404
+ ccModel += "[1m]";
405
+ }
406
+ return ccModel;
407
+ }
387
408
  function setAgentRuntime(agentId, runtime, model, reasoning_effort, mcps) {
388
409
  const knownModels = KNOWN_RUNTIMES[runtime];
389
410
  if (!knownModels) {
package/dist/lib/store.js CHANGED
@@ -192,6 +192,17 @@ function normalizeOrchestration(raw) {
192
192
  const userOrg = raw.orchestration ?? {};
193
193
  raw.orchestration = { ...defaultOrg, ...userOrg };
194
194
  }
195
+ function normalizeCloudEndpoint(raw) {
196
+ const cloud = raw.cloud;
197
+ if (!cloud?.endpoint) return;
198
+ const ep = String(cloud.endpoint);
199
+ if (ep === "https://askexe.com/cloud" || ep === "https://askexe.com/cloud/") {
200
+ cloud.endpoint = "https://cloud.askexe.com";
201
+ process.stderr.write(
202
+ "[config] Auto-migrated cloud endpoint: askexe.com/cloud \u2192 cloud.askexe.com\n"
203
+ );
204
+ }
205
+ }
195
206
  async function loadConfig() {
196
207
  const dir = process.env.EXE_OS_DIR ?? process.env.EXE_MEM_DIR ?? EXE_AI_DIR;
197
208
  await ensurePrivateDir(dir);
@@ -217,6 +228,7 @@ async function loadConfig() {
217
228
  normalizeSessionLifecycle(migratedCfg);
218
229
  normalizeAutoUpdate(migratedCfg);
219
230
  normalizeOrchestration(migratedCfg);
231
+ normalizeCloudEndpoint(migratedCfg);
220
232
  const config = { ...DEFAULT_CONFIG, dbPath: path.join(dir, "memories.db"), ...migratedCfg };
221
233
  if (config.dbPath.startsWith("~")) {
222
234
  config.dbPath = config.dbPath.replace(/^~/, os.homedir());
@@ -4384,7 +4396,7 @@ init_memory();
4384
4396
  init_database();
4385
4397
 
4386
4398
  // src/lib/keychain.ts
4387
- import { readFile as readFile3, writeFile as writeFile3, unlink, mkdir as mkdir3, chmod as chmod2 } from "fs/promises";
4399
+ import { readFile as readFile3, writeFile as writeFile3, unlink, mkdir as mkdir3, chmod as chmod2, rename, copyFile } from "fs/promises";
4388
4400
  import { existsSync as existsSync7, statSync as statSync3 } from "fs";
4389
4401
  import { execSync as execSync3 } from "child_process";
4390
4402
  import path6 from "path";
@@ -4423,12 +4435,14 @@ function linuxSecretAvailable() {
4423
4435
  function isRootOnlyTrustedServerKeyFile(keyPath) {
4424
4436
  if (process.platform !== "linux") return false;
4425
4437
  try {
4426
- const uid = typeof os5.userInfo().uid === "number" ? os5.userInfo().uid : -1;
4427
4438
  const st = statSync3(keyPath);
4428
4439
  if (!st.isFile() || (st.mode & 63) !== 0) return false;
4440
+ const uid = typeof os5.userInfo().uid === "number" ? os5.userInfo().uid : -1;
4429
4441
  if (uid === 0) return true;
4430
4442
  const exeOsDir = process.env.EXE_OS_DIR;
4431
- return Boolean(exeOsDir && path6.resolve(keyPath).startsWith(path6.resolve(exeOsDir) + path6.sep));
4443
+ if (exeOsDir && path6.resolve(keyPath).startsWith(path6.resolve(exeOsDir) + path6.sep)) return true;
4444
+ if (!linuxSecretAvailable()) return true;
4445
+ return false;
4432
4446
  } catch {
4433
4447
  return false;
4434
4448
  }
@@ -4579,15 +4593,25 @@ async function writeMachineBoundFileFallback(b64) {
4579
4593
  await mkdir3(dir, { recursive: true });
4580
4594
  const keyPath = getKeyPath();
4581
4595
  const machineKey = deriveMachineKey();
4582
- if (machineKey) {
4583
- const encrypted = encryptWithMachineKey(b64, machineKey);
4584
- await writeFile3(keyPath, encrypted + "\n", "utf-8");
4585
- await chmod2(keyPath, 384);
4586
- return "encrypted";
4596
+ const content = machineKey ? encryptWithMachineKey(b64, machineKey) + "\n" : b64 + "\n";
4597
+ const result = machineKey ? "encrypted" : "plaintext";
4598
+ const tmpPath = keyPath + ".tmp";
4599
+ try {
4600
+ if (existsSync7(keyPath)) {
4601
+ await copyFile(keyPath, keyPath + ".bak").catch(() => {
4602
+ });
4603
+ }
4604
+ await writeFile3(tmpPath, content, "utf-8");
4605
+ await chmod2(tmpPath, 384);
4606
+ await rename(tmpPath, keyPath);
4607
+ } catch (err) {
4608
+ try {
4609
+ await unlink(tmpPath);
4610
+ } catch {
4611
+ }
4612
+ throw err;
4587
4613
  }
4588
- await writeFile3(keyPath, b64 + "\n", "utf-8");
4589
- await chmod2(keyPath, 384);
4590
- return "plaintext";
4614
+ return result;
4591
4615
  }
4592
4616
  async function getMasterKey() {
4593
4617
  let nativeValue = macKeychainGet() ?? linuxSecretGet();
@@ -4654,7 +4678,7 @@ async function getMasterKey() {
4654
4678
  b64Value = content;
4655
4679
  }
4656
4680
  const key = Buffer.from(b64Value, "base64");
4657
- if (!content.startsWith(ENCRYPTED_PREFIX) && isRootOnlyTrustedServerKeyFile(keyPath)) {
4681
+ if (isRootOnlyTrustedServerKeyFile(keyPath)) {
4658
4682
  return key;
4659
4683
  }
4660
4684
  const migrated = macKeychainSet(b64Value) || linuxSecretSet(b64Value);
package/dist/lib/tasks.js CHANGED
@@ -161,6 +161,17 @@ function normalizeOrchestration(raw) {
161
161
  const userOrg = raw.orchestration ?? {};
162
162
  raw.orchestration = { ...defaultOrg, ...userOrg };
163
163
  }
164
+ function normalizeCloudEndpoint(raw) {
165
+ const cloud = raw.cloud;
166
+ if (!cloud?.endpoint) return;
167
+ const ep = String(cloud.endpoint);
168
+ if (ep === "https://askexe.com/cloud" || ep === "https://askexe.com/cloud/") {
169
+ cloud.endpoint = "https://cloud.askexe.com";
170
+ process.stderr.write(
171
+ "[config] Auto-migrated cloud endpoint: askexe.com/cloud \u2192 cloud.askexe.com\n"
172
+ );
173
+ }
174
+ }
164
175
  async function loadConfig() {
165
176
  const dir = process.env.EXE_OS_DIR ?? process.env.EXE_MEM_DIR ?? EXE_AI_DIR;
166
177
  await ensurePrivateDir(dir);
@@ -186,6 +197,7 @@ async function loadConfig() {
186
197
  normalizeSessionLifecycle(migratedCfg);
187
198
  normalizeAutoUpdate(migratedCfg);
188
199
  normalizeOrchestration(migratedCfg);
200
+ normalizeCloudEndpoint(migratedCfg);
189
201
  const config = { ...DEFAULT_CONFIG, dbPath: path.join(dir, "memories.db"), ...migratedCfg };
190
202
  if (config.dbPath.startsWith("~")) {
191
203
  config.dbPath = config.dbPath.replace(/^~/, os.homedir());
@@ -214,6 +226,7 @@ function loadConfigSync() {
214
226
  normalizeSessionLifecycle(migratedCfg);
215
227
  normalizeAutoUpdate(migratedCfg);
216
228
  normalizeOrchestration(migratedCfg);
229
+ normalizeCloudEndpoint(migratedCfg);
217
230
  const config = { ...DEFAULT_CONFIG, dbPath: path.join(dir, "memories.db"), ...migratedCfg };
218
231
  if (config.dbPath.startsWith("~")) {
219
232
  config.dbPath = config.dbPath.replace(/^~/, os.homedir());
@@ -374,6 +387,7 @@ __export(agent_config_exports, {
374
387
  clearAgentRuntime: () => clearAgentRuntime,
375
388
  getAgentRuntime: () => getAgentRuntime,
376
389
  loadAgentConfig: () => loadAgentConfig,
390
+ normalizeCcModelName: () => normalizeCcModelName,
377
391
  saveAgentConfig: () => saveAgentConfig,
378
392
  setAgentMcps: () => setAgentMcps,
379
393
  setAgentRuntime: () => setAgentRuntime
@@ -402,6 +416,13 @@ function getAgentRuntime(agentId) {
402
416
  if (orgDefault) return orgDefault;
403
417
  return { runtime: DEFAULT_RUNTIME, model: DEFAULT_MODELS[DEFAULT_RUNTIME] };
404
418
  }
419
+ function normalizeCcModelName(model) {
420
+ let ccModel = model.replace(/(\d+)\.(\d+)/g, "$1-$2");
421
+ if (/claude-(opus|sonnet)-4-[6-9]/.test(ccModel) && !ccModel.includes("[1m]")) {
422
+ ccModel += "[1m]";
423
+ }
424
+ return ccModel;
425
+ }
405
426
  function setAgentRuntime(agentId, runtime, model, reasoning_effort, mcps) {
406
427
  const knownModels = KNOWN_RUNTIMES[runtime];
407
428
  if (!knownModels) {
@@ -1103,6 +1124,7 @@ var init_provider_table = __esm({
1103
1124
  // src/lib/intercom-queue.ts
1104
1125
  var intercom_queue_exports = {};
1105
1126
  __export(intercom_queue_exports, {
1127
+ _resetDrainGuard: () => _resetDrainGuard,
1106
1128
  clearQueueForAgent: () => clearQueueForAgent,
1107
1129
  drainForSession: () => drainForSession,
1108
1130
  drainQueue: () => drainQueue,
@@ -1148,38 +1170,47 @@ function queueIntercom(targetSession, reason) {
1148
1170
  writeQueue(queue);
1149
1171
  }
1150
1172
  function drainQueue(isSessionBusy2, sendKeys) {
1173
+ if (_draining) {
1174
+ logQueue("SKIP_DRAIN \u2014 previous drain still running (possible tmux hang)");
1175
+ return { drained: 0, failed: 0 };
1176
+ }
1151
1177
  const queue = readQueue();
1152
1178
  if (queue.length === 0) return { drained: 0, failed: 0 };
1179
+ _draining = true;
1153
1180
  const remaining = [];
1154
1181
  let drained = 0;
1155
1182
  let failed = 0;
1156
- for (const item of queue) {
1157
- const age = Date.now() - new Date(item.queuedAt).getTime();
1158
- if (age > TTL_MS) {
1159
- logQueue(`EXPIRED \u2192 ${item.targetSession} (${Math.round(age / 6e4)}min old, reason: ${item.reason})`);
1160
- failed++;
1161
- continue;
1162
- }
1163
- try {
1164
- if (!isSessionBusy2(item.targetSession)) {
1165
- const success = sendKeys(item.targetSession);
1166
- if (success) {
1167
- logQueue(`DRAINED \u2192 ${item.targetSession} (after ${item.attempts} retries)`);
1168
- drained++;
1169
- continue;
1183
+ try {
1184
+ for (const item of queue) {
1185
+ const age = Date.now() - new Date(item.queuedAt).getTime();
1186
+ if (age > TTL_MS) {
1187
+ logQueue(`EXPIRED \u2192 ${item.targetSession} (${Math.round(age / 6e4)}min old, reason: ${item.reason})`);
1188
+ failed++;
1189
+ continue;
1190
+ }
1191
+ try {
1192
+ if (!isSessionBusy2(item.targetSession)) {
1193
+ const success = sendKeys(item.targetSession);
1194
+ if (success) {
1195
+ logQueue(`DRAINED \u2192 ${item.targetSession} (after ${item.attempts} retries)`);
1196
+ drained++;
1197
+ continue;
1198
+ }
1170
1199
  }
1200
+ } catch {
1171
1201
  }
1172
- } catch {
1173
- }
1174
- item.attempts++;
1175
- if (item.attempts >= MAX_RETRIES) {
1176
- logQueue(`FAILED \u2192 ${item.targetSession} (${MAX_RETRIES} retries exhausted, reason: ${item.reason})`);
1177
- failed++;
1178
- continue;
1202
+ item.attempts++;
1203
+ if (item.attempts >= MAX_RETRIES) {
1204
+ logQueue(`FAILED \u2192 ${item.targetSession} (${MAX_RETRIES} retries exhausted, reason: ${item.reason})`);
1205
+ failed++;
1206
+ continue;
1207
+ }
1208
+ remaining.push(item);
1179
1209
  }
1180
- remaining.push(item);
1210
+ writeQueue(remaining);
1211
+ } finally {
1212
+ _draining = false;
1181
1213
  }
1182
- writeQueue(remaining);
1183
1214
  return { drained, failed };
1184
1215
  }
1185
1216
  function drainForSession(targetSession, sendKeys) {
@@ -1204,6 +1235,9 @@ function clearQueueForAgent(agentName) {
1204
1235
  logQueue(`CLEARED ${before - filtered.length} stale item(s) for ${agentName}`);
1205
1236
  }
1206
1237
  }
1238
+ function _resetDrainGuard() {
1239
+ _draining = false;
1240
+ }
1207
1241
  function logQueue(msg) {
1208
1242
  const line = `[${(/* @__PURE__ */ new Date()).toISOString()}] [queue] ${msg}
1209
1243
  `;
@@ -1215,13 +1249,14 @@ function logQueue(msg) {
1215
1249
  } catch {
1216
1250
  }
1217
1251
  }
1218
- var QUEUE_PATH, MAX_RETRIES, TTL_MS, INTERCOM_LOG;
1252
+ var QUEUE_PATH, MAX_RETRIES, TTL_MS, _draining, INTERCOM_LOG;
1219
1253
  var init_intercom_queue = __esm({
1220
1254
  "src/lib/intercom-queue.ts"() {
1221
1255
  "use strict";
1222
1256
  QUEUE_PATH = path6.join(os5.homedir(), ".exe-os", "intercom-queue.json");
1223
1257
  MAX_RETRIES = 5;
1224
1258
  TTL_MS = 60 * 60 * 1e3;
1259
+ _draining = false;
1225
1260
  INTERCOM_LOG = path6.join(os5.homedir(), ".exe-os", "intercom.log");
1226
1261
  }
1227
1262
  });
@@ -2426,7 +2461,13 @@ async function createReviewForCompletedTask(row, result, _baseDir, now) {
2426
2461
  taskFile
2427
2462
  });
2428
2463
  const originalPriority = String(row.priority).toLowerCase();
2429
- const autoApprove = originalPriority === "p2" && result?.toLowerCase().includes("tests pass");
2464
+ const resultLower = result?.toLowerCase() ?? "";
2465
+ const hasTestEvidence = (
2466
+ // Vitest/Jest output patterns (hard to fake without actually running tests)
2467
+ /\d+\s+pass(ed|ing)/.test(resultLower) || /test files?\s+\d+\s+passed/.test(resultLower) || /tests?\s+\d+\s+passed/.test(resultLower)
2468
+ );
2469
+ const hasNoFailures = !/fail(ed|ure|ing)|error/i.test(resultLower);
2470
+ const autoApprove = originalPriority === "p2" && hasTestEvidence && hasNoFailures;
2430
2471
  if (!autoApprove) {
2431
2472
  try {
2432
2473
  const key = getSessionKey();
@@ -2434,6 +2475,13 @@ async function createReviewForCompletedTask(row, result, _baseDir, now) {
2434
2475
  if (exeSession) {
2435
2476
  sendIntercom(exeSession);
2436
2477
  }
2478
+ if (reviewer && reviewer !== coordinatorName && reviewer !== exeSession) {
2479
+ const { employeeSessionName: employeeSessionName2 } = await Promise.resolve().then(() => (init_tmux_routing(), tmux_routing_exports));
2480
+ if (exeSession) {
2481
+ const reviewerSession = employeeSessionName2(reviewer, exeSession);
2482
+ sendIntercom(reviewerSession);
2483
+ }
2484
+ }
2437
2485
  } catch {
2438
2486
  }
2439
2487
  }
@@ -2564,18 +2612,31 @@ function acquireSpawnLock(sessionName) {
2564
2612
  mkdirSync7(SPAWN_LOCK_DIR, { recursive: true });
2565
2613
  }
2566
2614
  const lockFile = spawnLockPath(sessionName);
2567
- if (existsSync12(lockFile)) {
2568
- try {
2569
- const lock = JSON.parse(readFileSync8(lockFile, "utf8"));
2570
- const age = Date.now() - lock.timestamp;
2571
- if (isProcessAlive(lock.pid) && age < 6e4) {
2572
- return false;
2573
- }
2574
- } catch {
2615
+ const lockData = JSON.stringify({ pid: process.pid, timestamp: Date.now() });
2616
+ const { openSync: openSync3, closeSync: closeSync3, writeSync } = __require("fs");
2617
+ const { constants } = __require("fs");
2618
+ try {
2619
+ const fd = openSync3(lockFile, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL, 420);
2620
+ writeSync(fd, lockData);
2621
+ closeSync3(fd);
2622
+ return true;
2623
+ } catch (err) {
2624
+ if (err?.code !== "EEXIST") {
2625
+ return true;
2575
2626
  }
2576
2627
  }
2577
- writeFileSync6(lockFile, JSON.stringify({ pid: process.pid, timestamp: Date.now() }));
2578
- return true;
2628
+ try {
2629
+ const lock = JSON.parse(readFileSync8(lockFile, "utf8"));
2630
+ const age = Date.now() - lock.timestamp;
2631
+ if (isProcessAlive(lock.pid) && age < 6e4) {
2632
+ return false;
2633
+ }
2634
+ writeFileSync6(lockFile, lockData);
2635
+ return true;
2636
+ } catch {
2637
+ writeFileSync6(lockFile, lockData);
2638
+ return true;
2639
+ }
2579
2640
  }
2580
2641
  function releaseSpawnLock(sessionName) {
2581
2642
  try {
@@ -2654,6 +2715,21 @@ function parseParentExe(sessionName, agentId) {
2654
2715
  function extractRootExe(name) {
2655
2716
  if (!name) return null;
2656
2717
  if (!name.includes("-")) return name;
2718
+ try {
2719
+ const roster = (init_employees(), __toCommonJS(employees_exports)).loadEmployeesSync();
2720
+ if (roster.length > 0) {
2721
+ const sortedNames = roster.map((e) => e.name).sort((a, b) => b.length - a.length);
2722
+ for (const agentName of sortedNames) {
2723
+ const escaped = agentName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2724
+ const regex = new RegExp(`^${escaped}\\d*-(.+)$`);
2725
+ const match = name.match(regex);
2726
+ if (match) {
2727
+ return extractRootExe(match[1]);
2728
+ }
2729
+ }
2730
+ }
2731
+ } catch {
2732
+ }
2657
2733
  const parts = name.split("-").filter(Boolean);
2658
2734
  return parts.length > 0 ? parts[parts.length - 1] : null;
2659
2735
  }
@@ -2672,6 +2748,10 @@ function registerParentExe(sessionKey, parentExe, dispatchedBy) {
2672
2748
  function getParentExe(sessionKey) {
2673
2749
  try {
2674
2750
  const data = JSON.parse(readFileSync8(path11.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`), "utf8"));
2751
+ if (data.registeredAt) {
2752
+ const age = Date.now() - new Date(data.registeredAt).getTime();
2753
+ if (age > PARENT_EXE_CACHE_TTL_MS) return null;
2754
+ }
2675
2755
  return data.parentExe || null;
2676
2756
  } catch {
2677
2757
  return null;
@@ -3220,7 +3300,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
3220
3300
  sessionContextFlag = ` --append-system-prompt-file ${ctxFile}`;
3221
3301
  } catch {
3222
3302
  }
3223
- let envPrefix = `EXE_SESSION=${exeSession} EXE_SESSION_NAME=${sessionName}`;
3303
+ let envPrefix = `EXE_SESSION=${exeSession} EXE_SESSION_NAME=${sessionName} EXE_SESSION_START_ISO=${(/* @__PURE__ */ new Date()).toISOString()}`;
3224
3304
  if (ccProvider !== DEFAULT_PROVIDER) {
3225
3305
  const cfg = PROVIDER_TABLE[ccProvider];
3226
3306
  if (cfg?.apiKeyEnv) {
@@ -3255,10 +3335,8 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
3255
3335
  }
3256
3336
  if (!useExeAgent && !useCodex && !useOpencode && !useBinSymlink) {
3257
3337
  if (agentRtConfig.runtime === "claude" && agentRtConfig.model) {
3258
- let ccModel = agentRtConfig.model.replace(/(\d+)\.(\d+)/g, "$1-$2");
3259
- if (/claude-(opus|sonnet)-4-[6-9]/.test(ccModel) && !ccModel.includes("[1m]")) {
3260
- ccModel += "[1m]";
3261
- }
3338
+ const { normalizeCcModelName: normalizeCcModelName2 } = (init_agent_config(), __toCommonJS(agent_config_exports));
3339
+ const ccModel = normalizeCcModelName2(agentRtConfig.model);
3262
3340
  envPrefix = `${envPrefix} ANTHROPIC_MODEL=${ccModel}`;
3263
3341
  }
3264
3342
  }
@@ -3359,7 +3437,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
3359
3437
  releaseSpawnLock(sessionName);
3360
3438
  return { sessionName };
3361
3439
  }
3362
- var SPAWN_LOCK_DIR, SESSION_CACHE, BEHAVIORS_EXPORT_TIMEOUT_MS, VALID_SESSION_NAME, VERIFY_PANE_LINES, INTERCOM_DEBOUNCE_MS, CODEX_DEBOUNCE_MS, INTERCOM_LOG2, DEBOUNCE_FILE, DEBOUNCE_CLEANUP_AGE_MS, BUSY_PATTERN;
3440
+ var SPAWN_LOCK_DIR, SESSION_CACHE, BEHAVIORS_EXPORT_TIMEOUT_MS, VALID_SESSION_NAME, PARENT_EXE_CACHE_TTL_MS, VERIFY_PANE_LINES, INTERCOM_DEBOUNCE_MS, CODEX_DEBOUNCE_MS, INTERCOM_LOG2, DEBOUNCE_FILE, DEBOUNCE_CLEANUP_AGE_MS, BUSY_PATTERN;
3363
3441
  var init_tmux_routing = __esm({
3364
3442
  "src/lib/tmux-routing.ts"() {
3365
3443
  "use strict";
@@ -3379,6 +3457,7 @@ var init_tmux_routing = __esm({
3379
3457
  SESSION_CACHE = path11.join(os8.homedir(), ".exe-os", "session-cache");
3380
3458
  BEHAVIORS_EXPORT_TIMEOUT_MS = 1e4;
3381
3459
  VALID_SESSION_NAME = /^[a-z]+\d*-[a-zA-Z0-9_]+$/;
3460
+ PARENT_EXE_CACHE_TTL_MS = 4 * 60 * 60 * 1e3;
3382
3461
  VERIFY_PANE_LINES = 200;
3383
3462
  INTERCOM_DEBOUNCE_MS = 3e4;
3384
3463
  CODEX_DEBOUNCE_MS = 12e4;
@@ -3423,6 +3502,17 @@ var init_task_scope = __esm({
3423
3502
  });
3424
3503
 
3425
3504
  // src/lib/notifications.ts
3505
+ var notifications_exports = {};
3506
+ __export(notifications_exports, {
3507
+ cleanupOldNotifications: () => cleanupOldNotifications,
3508
+ formatNotifications: () => formatNotifications,
3509
+ markAsRead: () => markAsRead,
3510
+ markAsReadByTaskFile: () => markAsReadByTaskFile,
3511
+ markDoneTaskNotificationsAsRead: () => markDoneTaskNotificationsAsRead,
3512
+ migrateJsonNotifications: () => migrateJsonNotifications,
3513
+ readUnreadNotifications: () => readUnreadNotifications,
3514
+ writeNotification: () => writeNotification
3515
+ });
3426
3516
  import crypto2 from "crypto";
3427
3517
  import path12 from "path";
3428
3518
  import os9 from "os";
@@ -3459,6 +3549,52 @@ async function writeNotification(notification) {
3459
3549
  `);
3460
3550
  }
3461
3551
  }
3552
+ async function readUnreadNotifications(agentFilter, sessionScope) {
3553
+ try {
3554
+ const client = getClient();
3555
+ const conditions = ["read = 0"];
3556
+ const args = [];
3557
+ const scope = strictSessionScopeFilter(sessionScope);
3558
+ if (agentFilter) {
3559
+ conditions.push("agent_id = ?");
3560
+ args.push(agentFilter);
3561
+ }
3562
+ const result = await client.execute({
3563
+ sql: `SELECT id, agent_id, agent_role, event, project, summary, task_file, session_scope, created_at
3564
+ FROM notifications
3565
+ WHERE ${conditions.join(" AND ")}${scope.sql}
3566
+ ORDER BY created_at ASC`,
3567
+ args: [...args, ...scope.args]
3568
+ });
3569
+ return result.rows.map((r) => ({
3570
+ id: String(r.id),
3571
+ agentId: String(r.agent_id),
3572
+ agentRole: String(r.agent_role),
3573
+ event: String(r.event),
3574
+ project: String(r.project),
3575
+ summary: String(r.summary),
3576
+ taskFile: r.task_file ? String(r.task_file) : void 0,
3577
+ sessionScope: r.session_scope == null ? null : String(r.session_scope),
3578
+ timestamp: String(r.created_at),
3579
+ read: false
3580
+ }));
3581
+ } catch {
3582
+ return [];
3583
+ }
3584
+ }
3585
+ async function markAsRead(ids, sessionScope) {
3586
+ if (ids.length === 0) return;
3587
+ try {
3588
+ const client = getClient();
3589
+ const placeholders = ids.map(() => "?").join(", ");
3590
+ const scope = strictSessionScopeFilter(sessionScope);
3591
+ await client.execute({
3592
+ sql: `UPDATE notifications SET read = 1 WHERE id IN (${placeholders})${scope.sql}`,
3593
+ args: [...ids, ...scope.args]
3594
+ });
3595
+ } catch {
3596
+ }
3597
+ }
3462
3598
  async function markAsReadByTaskFile(taskFile, sessionScope) {
3463
3599
  try {
3464
3600
  const client = getClient();
@@ -3471,11 +3607,144 @@ async function markAsReadByTaskFile(taskFile, sessionScope) {
3471
3607
  } catch {
3472
3608
  }
3473
3609
  }
3610
+ async function cleanupOldNotifications(daysOld = CLEANUP_DAYS, sessionScope) {
3611
+ try {
3612
+ const client = getClient();
3613
+ const cutoff = new Date(
3614
+ Date.now() - daysOld * 24 * 60 * 60 * 1e3
3615
+ ).toISOString();
3616
+ const scope = strictSessionScopeFilter(sessionScope);
3617
+ const result = await client.execute({
3618
+ sql: `DELETE FROM notifications WHERE created_at < ?${scope.sql}`,
3619
+ args: [cutoff, ...scope.args]
3620
+ });
3621
+ return result.rowsAffected;
3622
+ } catch {
3623
+ return 0;
3624
+ }
3625
+ }
3626
+ async function markDoneTaskNotificationsAsRead(sessionScope) {
3627
+ try {
3628
+ const client = getClient();
3629
+ const scope = strictSessionScopeFilter(sessionScope);
3630
+ const result = await client.execute({
3631
+ sql: `UPDATE notifications SET read = 1
3632
+ WHERE read = 0
3633
+ AND task_file IS NOT NULL
3634
+ ${scope.sql}
3635
+ AND task_file IN (
3636
+ SELECT task_file FROM tasks WHERE status = 'done'${scope.sql}
3637
+ )`,
3638
+ args: [...scope.args, ...scope.args]
3639
+ });
3640
+ return result.rowsAffected;
3641
+ } catch {
3642
+ return 0;
3643
+ }
3644
+ }
3645
+ function formatNotifications(notifications) {
3646
+ if (notifications.length === 0) return "";
3647
+ const grouped = /* @__PURE__ */ new Map();
3648
+ for (const n of notifications) {
3649
+ const key = `${n.agentId}|${n.agentRole}`;
3650
+ if (!grouped.has(key)) grouped.set(key, []);
3651
+ grouped.get(key).push(n);
3652
+ }
3653
+ const lines = [];
3654
+ lines.push(`## Notifications (${notifications.length} unread)
3655
+ `);
3656
+ for (const [key, items] of grouped) {
3657
+ const [agentId, agentRole] = key.split("|");
3658
+ lines.push(`**${agentId}** (${agentRole}):`);
3659
+ for (const item of items) {
3660
+ const ago = formatTimeAgo(item.timestamp);
3661
+ const icon = eventIcon(item.event);
3662
+ lines.push(`- ${icon} ${item.summary} (${item.project}) \u2014 ${ago}`);
3663
+ }
3664
+ lines.push("");
3665
+ }
3666
+ return lines.join("\n");
3667
+ }
3668
+ async function migrateJsonNotifications() {
3669
+ const base = process.env.EXE_OS_DIR || process.env.EXE_MEM_DIR || path12.join(os9.homedir(), ".exe-os");
3670
+ const notifDir = path12.join(base, "notifications");
3671
+ if (!existsSync13(notifDir)) return 0;
3672
+ let migrated = 0;
3673
+ try {
3674
+ const files = readdirSync3(notifDir).filter((f) => f.endsWith(".json"));
3675
+ if (files.length === 0) return 0;
3676
+ const client = getClient();
3677
+ for (const file of files) {
3678
+ try {
3679
+ const filePath = path12.join(notifDir, file);
3680
+ const data = JSON.parse(readFileSync9(filePath, "utf8"));
3681
+ await client.execute({
3682
+ sql: `INSERT OR IGNORE INTO notifications (id, agent_id, agent_role, event, project, summary, task_file, session_scope, read, created_at)
3683
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
3684
+ args: [
3685
+ crypto2.randomUUID(),
3686
+ data.agentId ?? "unknown",
3687
+ data.agentRole ?? "unknown",
3688
+ data.event ?? "session_summary",
3689
+ data.project ?? "unknown",
3690
+ data.summary ?? "",
3691
+ data.taskFile ?? null,
3692
+ null,
3693
+ data.read ? 1 : 0,
3694
+ data.timestamp ?? (/* @__PURE__ */ new Date()).toISOString()
3695
+ ]
3696
+ });
3697
+ unlinkSync5(filePath);
3698
+ migrated++;
3699
+ } catch {
3700
+ }
3701
+ }
3702
+ try {
3703
+ const remaining = readdirSync3(notifDir);
3704
+ if (remaining.length === 0) {
3705
+ rmdirSync(notifDir);
3706
+ }
3707
+ } catch {
3708
+ }
3709
+ } catch {
3710
+ }
3711
+ return migrated;
3712
+ }
3713
+ function eventIcon(event) {
3714
+ switch (event) {
3715
+ case "task_complete":
3716
+ return "Completed:";
3717
+ case "task_needs_fix":
3718
+ return "Needs fix:";
3719
+ case "session_summary":
3720
+ return "Session:";
3721
+ case "error_spike":
3722
+ return "Errors:";
3723
+ case "orphan_task":
3724
+ return "Orphan:";
3725
+ case "subtasks_complete":
3726
+ return "Subtasks done:";
3727
+ case "capacity_relaunch":
3728
+ return "Relaunched:";
3729
+ }
3730
+ }
3731
+ function formatTimeAgo(timestamp) {
3732
+ const diffMs = Date.now() - new Date(timestamp).getTime();
3733
+ const mins = Math.floor(diffMs / 6e4);
3734
+ if (mins < 1) return "just now";
3735
+ if (mins < 60) return `${mins}m ago`;
3736
+ const hours = Math.floor(mins / 60);
3737
+ if (hours < 24) return `${hours}h ago`;
3738
+ const days = Math.floor(hours / 24);
3739
+ return `${days}d ago`;
3740
+ }
3741
+ var CLEANUP_DAYS;
3474
3742
  var init_notifications = __esm({
3475
3743
  "src/lib/notifications.ts"() {
3476
3744
  "use strict";
3477
3745
  init_database();
3478
3746
  init_task_scope();
3747
+ CLEANUP_DAYS = 7;
3479
3748
  }
3480
3749
  });
3481
3750
 
@@ -5356,6 +5625,20 @@ async function updateTask(input) {
5356
5625
  notifyTaskDone();
5357
5626
  }
5358
5627
  await markTaskNotificationsRead(taskFile);
5628
+ if (input.status === "needs_review" && !isCoordinator) {
5629
+ try {
5630
+ const { writeNotification: writeNotification2 } = await Promise.resolve().then(() => (init_notifications(), notifications_exports));
5631
+ await writeNotification2({
5632
+ agentId: String(row.assigned_to),
5633
+ agentRole: String(row.assigned_to),
5634
+ event: "task_complete",
5635
+ project: String(row.project_name),
5636
+ summary: `"${String(row.title)}" is ready for review`,
5637
+ taskFile
5638
+ });
5639
+ } catch {
5640
+ }
5641
+ }
5359
5642
  if (input.status === "done" || input.status === "closed") {
5360
5643
  try {
5361
5644
  await cascadeUnblock(taskId, input.baseDir, now);