@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
@@ -209,6 +209,17 @@ function normalizeOrchestration(raw) {
209
209
  const userOrg = raw.orchestration ?? {};
210
210
  raw.orchestration = { ...defaultOrg, ...userOrg };
211
211
  }
212
+ function normalizeCloudEndpoint(raw) {
213
+ const cloud = raw.cloud;
214
+ if (!cloud?.endpoint) return;
215
+ const ep = String(cloud.endpoint);
216
+ if (ep === "https://askexe.com/cloud" || ep === "https://askexe.com/cloud/") {
217
+ cloud.endpoint = "https://cloud.askexe.com";
218
+ process.stderr.write(
219
+ "[config] Auto-migrated cloud endpoint: askexe.com/cloud \u2192 cloud.askexe.com\n"
220
+ );
221
+ }
222
+ }
212
223
  async function loadConfig() {
213
224
  const dir = process.env.EXE_OS_DIR ?? process.env.EXE_MEM_DIR ?? EXE_AI_DIR;
214
225
  await ensurePrivateDir(dir);
@@ -234,6 +245,7 @@ async function loadConfig() {
234
245
  normalizeSessionLifecycle(migratedCfg);
235
246
  normalizeAutoUpdate(migratedCfg);
236
247
  normalizeOrchestration(migratedCfg);
248
+ normalizeCloudEndpoint(migratedCfg);
237
249
  const config = { ...DEFAULT_CONFIG, dbPath: path.join(dir, "memories.db"), ...migratedCfg };
238
250
  if (config.dbPath.startsWith("~")) {
239
251
  config.dbPath = config.dbPath.replace(/^~/, os.homedir());
@@ -262,6 +274,7 @@ function loadConfigSync() {
262
274
  normalizeSessionLifecycle(migratedCfg);
263
275
  normalizeAutoUpdate(migratedCfg);
264
276
  normalizeOrchestration(migratedCfg);
277
+ normalizeCloudEndpoint(migratedCfg);
265
278
  const config = { ...DEFAULT_CONFIG, dbPath: path.join(dir, "memories.db"), ...migratedCfg };
266
279
  if (config.dbPath.startsWith("~")) {
267
280
  config.dbPath = config.dbPath.replace(/^~/, os.homedir());
@@ -422,6 +435,7 @@ __export(agent_config_exports, {
422
435
  clearAgentRuntime: () => clearAgentRuntime,
423
436
  getAgentRuntime: () => getAgentRuntime,
424
437
  loadAgentConfig: () => loadAgentConfig,
438
+ normalizeCcModelName: () => normalizeCcModelName,
425
439
  saveAgentConfig: () => saveAgentConfig,
426
440
  setAgentMcps: () => setAgentMcps,
427
441
  setAgentRuntime: () => setAgentRuntime
@@ -450,6 +464,13 @@ function getAgentRuntime(agentId) {
450
464
  if (orgDefault) return orgDefault;
451
465
  return { runtime: DEFAULT_RUNTIME, model: DEFAULT_MODELS[DEFAULT_RUNTIME] };
452
466
  }
467
+ function normalizeCcModelName(model) {
468
+ let ccModel = model.replace(/(\d+)\.(\d+)/g, "$1-$2");
469
+ if (/claude-(opus|sonnet)-4-[6-9]/.test(ccModel) && !ccModel.includes("[1m]")) {
470
+ ccModel += "[1m]";
471
+ }
472
+ return ccModel;
473
+ }
453
474
  function setAgentRuntime(agentId, runtime, model, reasoning_effort, mcps) {
454
475
  const knownModels = KNOWN_RUNTIMES[runtime];
455
476
  if (!knownModels) {
@@ -3741,7 +3762,7 @@ var init_database = __esm({
3741
3762
  });
3742
3763
 
3743
3764
  // src/lib/keychain.ts
3744
- import { readFile as readFile3, writeFile as writeFile3, unlink, mkdir as mkdir3, chmod as chmod2 } from "fs/promises";
3765
+ import { readFile as readFile3, writeFile as writeFile3, unlink, mkdir as mkdir3, chmod as chmod2, rename, copyFile } from "fs/promises";
3745
3766
  import { existsSync as existsSync8, statSync as statSync3 } from "fs";
3746
3767
  import { execSync as execSync3 } from "child_process";
3747
3768
  import path7 from "path";
@@ -3776,12 +3797,14 @@ function linuxSecretAvailable() {
3776
3797
  function isRootOnlyTrustedServerKeyFile(keyPath) {
3777
3798
  if (process.platform !== "linux") return false;
3778
3799
  try {
3779
- const uid = typeof os5.userInfo().uid === "number" ? os5.userInfo().uid : -1;
3780
3800
  const st = statSync3(keyPath);
3781
3801
  if (!st.isFile() || (st.mode & 63) !== 0) return false;
3802
+ const uid = typeof os5.userInfo().uid === "number" ? os5.userInfo().uid : -1;
3782
3803
  if (uid === 0) return true;
3783
3804
  const exeOsDir = process.env.EXE_OS_DIR;
3784
- return Boolean(exeOsDir && path7.resolve(keyPath).startsWith(path7.resolve(exeOsDir) + path7.sep));
3805
+ if (exeOsDir && path7.resolve(keyPath).startsWith(path7.resolve(exeOsDir) + path7.sep)) return true;
3806
+ if (!linuxSecretAvailable()) return true;
3807
+ return false;
3785
3808
  } catch {
3786
3809
  return false;
3787
3810
  }
@@ -3931,15 +3954,25 @@ async function writeMachineBoundFileFallback(b64) {
3931
3954
  await mkdir3(dir, { recursive: true });
3932
3955
  const keyPath = getKeyPath();
3933
3956
  const machineKey = deriveMachineKey();
3934
- if (machineKey) {
3935
- const encrypted = encryptWithMachineKey(b64, machineKey);
3936
- await writeFile3(keyPath, encrypted + "\n", "utf-8");
3937
- await chmod2(keyPath, 384);
3938
- return "encrypted";
3957
+ const content = machineKey ? encryptWithMachineKey(b64, machineKey) + "\n" : b64 + "\n";
3958
+ const result = machineKey ? "encrypted" : "plaintext";
3959
+ const tmpPath = keyPath + ".tmp";
3960
+ try {
3961
+ if (existsSync8(keyPath)) {
3962
+ await copyFile(keyPath, keyPath + ".bak").catch(() => {
3963
+ });
3964
+ }
3965
+ await writeFile3(tmpPath, content, "utf-8");
3966
+ await chmod2(tmpPath, 384);
3967
+ await rename(tmpPath, keyPath);
3968
+ } catch (err) {
3969
+ try {
3970
+ await unlink(tmpPath);
3971
+ } catch {
3972
+ }
3973
+ throw err;
3939
3974
  }
3940
- await writeFile3(keyPath, b64 + "\n", "utf-8");
3941
- await chmod2(keyPath, 384);
3942
- return "plaintext";
3975
+ return result;
3943
3976
  }
3944
3977
  async function getMasterKey() {
3945
3978
  let nativeValue = macKeychainGet() ?? linuxSecretGet();
@@ -4006,7 +4039,7 @@ async function getMasterKey() {
4006
4039
  b64Value = content;
4007
4040
  }
4008
4041
  const key = Buffer.from(b64Value, "base64");
4009
- if (!content.startsWith(ENCRYPTED_PREFIX) && isRootOnlyTrustedServerKeyFile(keyPath)) {
4042
+ if (isRootOnlyTrustedServerKeyFile(keyPath)) {
4010
4043
  return key;
4011
4044
  }
4012
4045
  const migrated = macKeychainSet(b64Value) || linuxSecretSet(b64Value);
@@ -6535,6 +6568,7 @@ var init_provider_table = __esm({
6535
6568
  // src/lib/intercom-queue.ts
6536
6569
  var intercom_queue_exports = {};
6537
6570
  __export(intercom_queue_exports, {
6571
+ _resetDrainGuard: () => _resetDrainGuard,
6538
6572
  clearQueueForAgent: () => clearQueueForAgent,
6539
6573
  drainForSession: () => drainForSession,
6540
6574
  drainQueue: () => drainQueue,
@@ -6580,38 +6614,47 @@ function queueIntercom(targetSession, reason) {
6580
6614
  writeQueue(queue);
6581
6615
  }
6582
6616
  function drainQueue(isSessionBusy2, sendKeys) {
6617
+ if (_draining) {
6618
+ logQueue("SKIP_DRAIN \u2014 previous drain still running (possible tmux hang)");
6619
+ return { drained: 0, failed: 0 };
6620
+ }
6583
6621
  const queue = readQueue();
6584
6622
  if (queue.length === 0) return { drained: 0, failed: 0 };
6623
+ _draining = true;
6585
6624
  const remaining = [];
6586
6625
  let drained = 0;
6587
6626
  let failed = 0;
6588
- for (const item of queue) {
6589
- const age = Date.now() - new Date(item.queuedAt).getTime();
6590
- if (age > TTL_MS) {
6591
- logQueue(`EXPIRED \u2192 ${item.targetSession} (${Math.round(age / 6e4)}min old, reason: ${item.reason})`);
6592
- failed++;
6593
- continue;
6594
- }
6595
- try {
6596
- if (!isSessionBusy2(item.targetSession)) {
6597
- const success = sendKeys(item.targetSession);
6598
- if (success) {
6599
- logQueue(`DRAINED \u2192 ${item.targetSession} (after ${item.attempts} retries)`);
6600
- drained++;
6601
- continue;
6627
+ try {
6628
+ for (const item of queue) {
6629
+ const age = Date.now() - new Date(item.queuedAt).getTime();
6630
+ if (age > TTL_MS) {
6631
+ logQueue(`EXPIRED \u2192 ${item.targetSession} (${Math.round(age / 6e4)}min old, reason: ${item.reason})`);
6632
+ failed++;
6633
+ continue;
6634
+ }
6635
+ try {
6636
+ if (!isSessionBusy2(item.targetSession)) {
6637
+ const success = sendKeys(item.targetSession);
6638
+ if (success) {
6639
+ logQueue(`DRAINED \u2192 ${item.targetSession} (after ${item.attempts} retries)`);
6640
+ drained++;
6641
+ continue;
6642
+ }
6602
6643
  }
6644
+ } catch {
6603
6645
  }
6604
- } catch {
6605
- }
6606
- item.attempts++;
6607
- if (item.attempts >= MAX_RETRIES2) {
6608
- logQueue(`FAILED \u2192 ${item.targetSession} (${MAX_RETRIES2} retries exhausted, reason: ${item.reason})`);
6609
- failed++;
6610
- continue;
6646
+ item.attempts++;
6647
+ if (item.attempts >= MAX_RETRIES2) {
6648
+ logQueue(`FAILED \u2192 ${item.targetSession} (${MAX_RETRIES2} retries exhausted, reason: ${item.reason})`);
6649
+ failed++;
6650
+ continue;
6651
+ }
6652
+ remaining.push(item);
6611
6653
  }
6612
- remaining.push(item);
6654
+ writeQueue(remaining);
6655
+ } finally {
6656
+ _draining = false;
6613
6657
  }
6614
- writeQueue(remaining);
6615
6658
  return { drained, failed };
6616
6659
  }
6617
6660
  function drainForSession(targetSession, sendKeys) {
@@ -6636,6 +6679,9 @@ function clearQueueForAgent(agentName) {
6636
6679
  logQueue(`CLEARED ${before - filtered.length} stale item(s) for ${agentName}`);
6637
6680
  }
6638
6681
  }
6682
+ function _resetDrainGuard() {
6683
+ _draining = false;
6684
+ }
6639
6685
  function logQueue(msg) {
6640
6686
  const line = `[${(/* @__PURE__ */ new Date()).toISOString()}] [queue] ${msg}
6641
6687
  `;
@@ -6647,13 +6693,14 @@ function logQueue(msg) {
6647
6693
  } catch {
6648
6694
  }
6649
6695
  }
6650
- var QUEUE_PATH, MAX_RETRIES2, TTL_MS, INTERCOM_LOG;
6696
+ var QUEUE_PATH, MAX_RETRIES2, TTL_MS, _draining, INTERCOM_LOG;
6651
6697
  var init_intercom_queue = __esm({
6652
6698
  "src/lib/intercom-queue.ts"() {
6653
6699
  "use strict";
6654
6700
  QUEUE_PATH = path10.join(os7.homedir(), ".exe-os", "intercom-queue.json");
6655
6701
  MAX_RETRIES2 = 5;
6656
6702
  TTL_MS = 60 * 60 * 1e3;
6703
+ _draining = false;
6657
6704
  INTERCOM_LOG = path10.join(os7.homedir(), ".exe-os", "intercom.log");
6658
6705
  }
6659
6706
  });
@@ -7803,7 +7850,13 @@ async function createReviewForCompletedTask(row, result, _baseDir, now) {
7803
7850
  taskFile
7804
7851
  });
7805
7852
  const originalPriority = String(row.priority).toLowerCase();
7806
- const autoApprove = originalPriority === "p2" && result?.toLowerCase().includes("tests pass");
7853
+ const resultLower = result?.toLowerCase() ?? "";
7854
+ const hasTestEvidence = (
7855
+ // Vitest/Jest output patterns (hard to fake without actually running tests)
7856
+ /\d+\s+pass(ed|ing)/.test(resultLower) || /test files?\s+\d+\s+passed/.test(resultLower) || /tests?\s+\d+\s+passed/.test(resultLower)
7857
+ );
7858
+ const hasNoFailures = !/fail(ed|ure|ing)|error/i.test(resultLower);
7859
+ const autoApprove = originalPriority === "p2" && hasTestEvidence && hasNoFailures;
7807
7860
  if (!autoApprove) {
7808
7861
  try {
7809
7862
  const key = getSessionKey();
@@ -7811,6 +7864,13 @@ async function createReviewForCompletedTask(row, result, _baseDir, now) {
7811
7864
  if (exeSession) {
7812
7865
  sendIntercom(exeSession);
7813
7866
  }
7867
+ if (reviewer && reviewer !== coordinatorName && reviewer !== exeSession) {
7868
+ const { employeeSessionName: employeeSessionName2 } = await Promise.resolve().then(() => (init_tmux_routing(), tmux_routing_exports));
7869
+ if (exeSession) {
7870
+ const reviewerSession = employeeSessionName2(reviewer, exeSession);
7871
+ sendIntercom(reviewerSession);
7872
+ }
7873
+ }
7814
7874
  } catch {
7815
7875
  }
7816
7876
  }
@@ -7941,18 +8001,31 @@ function acquireSpawnLock2(sessionName) {
7941
8001
  mkdirSync8(SPAWN_LOCK_DIR, { recursive: true });
7942
8002
  }
7943
8003
  const lockFile = spawnLockPath(sessionName);
7944
- if (existsSync16(lockFile)) {
7945
- try {
7946
- const lock = JSON.parse(readFileSync10(lockFile, "utf8"));
7947
- const age = Date.now() - lock.timestamp;
7948
- if (isProcessAlive(lock.pid) && age < 6e4) {
7949
- return false;
7950
- }
7951
- } catch {
8004
+ const lockData = JSON.stringify({ pid: process.pid, timestamp: Date.now() });
8005
+ const { openSync: openSync3, closeSync: closeSync3, writeSync } = __require("fs");
8006
+ const { constants } = __require("fs");
8007
+ try {
8008
+ const fd = openSync3(lockFile, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL, 420);
8009
+ writeSync(fd, lockData);
8010
+ closeSync3(fd);
8011
+ return true;
8012
+ } catch (err) {
8013
+ if (err?.code !== "EEXIST") {
8014
+ return true;
7952
8015
  }
7953
8016
  }
7954
- writeFileSync7(lockFile, JSON.stringify({ pid: process.pid, timestamp: Date.now() }));
7955
- return true;
8017
+ try {
8018
+ const lock = JSON.parse(readFileSync10(lockFile, "utf8"));
8019
+ const age = Date.now() - lock.timestamp;
8020
+ if (isProcessAlive(lock.pid) && age < 6e4) {
8021
+ return false;
8022
+ }
8023
+ writeFileSync7(lockFile, lockData);
8024
+ return true;
8025
+ } catch {
8026
+ writeFileSync7(lockFile, lockData);
8027
+ return true;
8028
+ }
7956
8029
  }
7957
8030
  function releaseSpawnLock2(sessionName) {
7958
8031
  try {
@@ -8031,6 +8104,21 @@ function parseParentExe(sessionName, agentId) {
8031
8104
  function extractRootExe(name) {
8032
8105
  if (!name) return null;
8033
8106
  if (!name.includes("-")) return name;
8107
+ try {
8108
+ const roster = (init_employees(), __toCommonJS(employees_exports)).loadEmployeesSync();
8109
+ if (roster.length > 0) {
8110
+ const sortedNames = roster.map((e) => e.name).sort((a, b) => b.length - a.length);
8111
+ for (const agentName of sortedNames) {
8112
+ const escaped = agentName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
8113
+ const regex = new RegExp(`^${escaped}\\d*-(.+)$`);
8114
+ const match = name.match(regex);
8115
+ if (match) {
8116
+ return extractRootExe(match[1]);
8117
+ }
8118
+ }
8119
+ }
8120
+ } catch {
8121
+ }
8034
8122
  const parts = name.split("-").filter(Boolean);
8035
8123
  return parts.length > 0 ? parts[parts.length - 1] : null;
8036
8124
  }
@@ -8049,6 +8137,10 @@ function registerParentExe(sessionKey, parentExe, dispatchedBy) {
8049
8137
  function getParentExe(sessionKey) {
8050
8138
  try {
8051
8139
  const data = JSON.parse(readFileSync10(path15.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`), "utf8"));
8140
+ if (data.registeredAt) {
8141
+ const age = Date.now() - new Date(data.registeredAt).getTime();
8142
+ if (age > PARENT_EXE_CACHE_TTL_MS) return null;
8143
+ }
8052
8144
  return data.parentExe || null;
8053
8145
  } catch {
8054
8146
  return null;
@@ -8597,7 +8689,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
8597
8689
  sessionContextFlag = ` --append-system-prompt-file ${ctxFile}`;
8598
8690
  } catch {
8599
8691
  }
8600
- let envPrefix = `EXE_SESSION=${exeSession} EXE_SESSION_NAME=${sessionName}`;
8692
+ let envPrefix = `EXE_SESSION=${exeSession} EXE_SESSION_NAME=${sessionName} EXE_SESSION_START_ISO=${(/* @__PURE__ */ new Date()).toISOString()}`;
8601
8693
  if (ccProvider !== DEFAULT_PROVIDER) {
8602
8694
  const cfg = PROVIDER_TABLE[ccProvider];
8603
8695
  if (cfg?.apiKeyEnv) {
@@ -8632,10 +8724,8 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
8632
8724
  }
8633
8725
  if (!useExeAgent && !useCodex && !useOpencode && !useBinSymlink) {
8634
8726
  if (agentRtConfig.runtime === "claude" && agentRtConfig.model) {
8635
- let ccModel = agentRtConfig.model.replace(/(\d+)\.(\d+)/g, "$1-$2");
8636
- if (/claude-(opus|sonnet)-4-[6-9]/.test(ccModel) && !ccModel.includes("[1m]")) {
8637
- ccModel += "[1m]";
8638
- }
8727
+ const { normalizeCcModelName: normalizeCcModelName2 } = (init_agent_config(), __toCommonJS(agent_config_exports));
8728
+ const ccModel = normalizeCcModelName2(agentRtConfig.model);
8639
8729
  envPrefix = `${envPrefix} ANTHROPIC_MODEL=${ccModel}`;
8640
8730
  }
8641
8731
  }
@@ -8736,7 +8826,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
8736
8826
  releaseSpawnLock2(sessionName);
8737
8827
  return { sessionName };
8738
8828
  }
8739
- 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;
8829
+ 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;
8740
8830
  var init_tmux_routing = __esm({
8741
8831
  "src/lib/tmux-routing.ts"() {
8742
8832
  "use strict";
@@ -8756,6 +8846,7 @@ var init_tmux_routing = __esm({
8756
8846
  SESSION_CACHE = path15.join(os10.homedir(), ".exe-os", "session-cache");
8757
8847
  BEHAVIORS_EXPORT_TIMEOUT_MS = 1e4;
8758
8848
  VALID_SESSION_NAME = /^[a-z]+\d*-[a-zA-Z0-9_]+$/;
8849
+ PARENT_EXE_CACHE_TTL_MS = 4 * 60 * 60 * 1e3;
8759
8850
  VERIFY_PANE_LINES = 200;
8760
8851
  INTERCOM_DEBOUNCE_MS = 3e4;
8761
8852
  CODEX_DEBOUNCE_MS = 12e4;
@@ -8800,6 +8891,17 @@ var init_task_scope = __esm({
8800
8891
  });
8801
8892
 
8802
8893
  // src/lib/notifications.ts
8894
+ var notifications_exports = {};
8895
+ __export(notifications_exports, {
8896
+ cleanupOldNotifications: () => cleanupOldNotifications,
8897
+ formatNotifications: () => formatNotifications,
8898
+ markAsRead: () => markAsRead,
8899
+ markAsReadByTaskFile: () => markAsReadByTaskFile,
8900
+ markDoneTaskNotificationsAsRead: () => markDoneTaskNotificationsAsRead,
8901
+ migrateJsonNotifications: () => migrateJsonNotifications,
8902
+ readUnreadNotifications: () => readUnreadNotifications,
8903
+ writeNotification: () => writeNotification
8904
+ });
8803
8905
  import crypto3 from "crypto";
8804
8906
  import path16 from "path";
8805
8907
  import os11 from "os";
@@ -8836,6 +8938,52 @@ async function writeNotification(notification) {
8836
8938
  `);
8837
8939
  }
8838
8940
  }
8941
+ async function readUnreadNotifications(agentFilter, sessionScope) {
8942
+ try {
8943
+ const client = getClient();
8944
+ const conditions = ["read = 0"];
8945
+ const args = [];
8946
+ const scope = strictSessionScopeFilter(sessionScope);
8947
+ if (agentFilter) {
8948
+ conditions.push("agent_id = ?");
8949
+ args.push(agentFilter);
8950
+ }
8951
+ const result = await client.execute({
8952
+ sql: `SELECT id, agent_id, agent_role, event, project, summary, task_file, session_scope, created_at
8953
+ FROM notifications
8954
+ WHERE ${conditions.join(" AND ")}${scope.sql}
8955
+ ORDER BY created_at ASC`,
8956
+ args: [...args, ...scope.args]
8957
+ });
8958
+ return result.rows.map((r) => ({
8959
+ id: String(r.id),
8960
+ agentId: String(r.agent_id),
8961
+ agentRole: String(r.agent_role),
8962
+ event: String(r.event),
8963
+ project: String(r.project),
8964
+ summary: String(r.summary),
8965
+ taskFile: r.task_file ? String(r.task_file) : void 0,
8966
+ sessionScope: r.session_scope == null ? null : String(r.session_scope),
8967
+ timestamp: String(r.created_at),
8968
+ read: false
8969
+ }));
8970
+ } catch {
8971
+ return [];
8972
+ }
8973
+ }
8974
+ async function markAsRead(ids, sessionScope) {
8975
+ if (ids.length === 0) return;
8976
+ try {
8977
+ const client = getClient();
8978
+ const placeholders = ids.map(() => "?").join(", ");
8979
+ const scope = strictSessionScopeFilter(sessionScope);
8980
+ await client.execute({
8981
+ sql: `UPDATE notifications SET read = 1 WHERE id IN (${placeholders})${scope.sql}`,
8982
+ args: [...ids, ...scope.args]
8983
+ });
8984
+ } catch {
8985
+ }
8986
+ }
8839
8987
  async function markAsReadByTaskFile(taskFile, sessionScope) {
8840
8988
  try {
8841
8989
  const client = getClient();
@@ -8848,11 +8996,144 @@ async function markAsReadByTaskFile(taskFile, sessionScope) {
8848
8996
  } catch {
8849
8997
  }
8850
8998
  }
8999
+ async function cleanupOldNotifications(daysOld = CLEANUP_DAYS, sessionScope) {
9000
+ try {
9001
+ const client = getClient();
9002
+ const cutoff = new Date(
9003
+ Date.now() - daysOld * 24 * 60 * 60 * 1e3
9004
+ ).toISOString();
9005
+ const scope = strictSessionScopeFilter(sessionScope);
9006
+ const result = await client.execute({
9007
+ sql: `DELETE FROM notifications WHERE created_at < ?${scope.sql}`,
9008
+ args: [cutoff, ...scope.args]
9009
+ });
9010
+ return result.rowsAffected;
9011
+ } catch {
9012
+ return 0;
9013
+ }
9014
+ }
9015
+ async function markDoneTaskNotificationsAsRead(sessionScope) {
9016
+ try {
9017
+ const client = getClient();
9018
+ const scope = strictSessionScopeFilter(sessionScope);
9019
+ const result = await client.execute({
9020
+ sql: `UPDATE notifications SET read = 1
9021
+ WHERE read = 0
9022
+ AND task_file IS NOT NULL
9023
+ ${scope.sql}
9024
+ AND task_file IN (
9025
+ SELECT task_file FROM tasks WHERE status = 'done'${scope.sql}
9026
+ )`,
9027
+ args: [...scope.args, ...scope.args]
9028
+ });
9029
+ return result.rowsAffected;
9030
+ } catch {
9031
+ return 0;
9032
+ }
9033
+ }
9034
+ function formatNotifications(notifications) {
9035
+ if (notifications.length === 0) return "";
9036
+ const grouped = /* @__PURE__ */ new Map();
9037
+ for (const n of notifications) {
9038
+ const key = `${n.agentId}|${n.agentRole}`;
9039
+ if (!grouped.has(key)) grouped.set(key, []);
9040
+ grouped.get(key).push(n);
9041
+ }
9042
+ const lines = [];
9043
+ lines.push(`## Notifications (${notifications.length} unread)
9044
+ `);
9045
+ for (const [key, items] of grouped) {
9046
+ const [agentId, agentRole] = key.split("|");
9047
+ lines.push(`**${agentId}** (${agentRole}):`);
9048
+ for (const item of items) {
9049
+ const ago = formatTimeAgo(item.timestamp);
9050
+ const icon = eventIcon(item.event);
9051
+ lines.push(`- ${icon} ${item.summary} (${item.project}) \u2014 ${ago}`);
9052
+ }
9053
+ lines.push("");
9054
+ }
9055
+ return lines.join("\n");
9056
+ }
9057
+ async function migrateJsonNotifications() {
9058
+ const base = process.env.EXE_OS_DIR || process.env.EXE_MEM_DIR || path16.join(os11.homedir(), ".exe-os");
9059
+ const notifDir = path16.join(base, "notifications");
9060
+ if (!existsSync17(notifDir)) return 0;
9061
+ let migrated = 0;
9062
+ try {
9063
+ const files = readdirSync4(notifDir).filter((f) => f.endsWith(".json"));
9064
+ if (files.length === 0) return 0;
9065
+ const client = getClient();
9066
+ for (const file of files) {
9067
+ try {
9068
+ const filePath = path16.join(notifDir, file);
9069
+ const data = JSON.parse(readFileSync11(filePath, "utf8"));
9070
+ await client.execute({
9071
+ sql: `INSERT OR IGNORE INTO notifications (id, agent_id, agent_role, event, project, summary, task_file, session_scope, read, created_at)
9072
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
9073
+ args: [
9074
+ crypto3.randomUUID(),
9075
+ data.agentId ?? "unknown",
9076
+ data.agentRole ?? "unknown",
9077
+ data.event ?? "session_summary",
9078
+ data.project ?? "unknown",
9079
+ data.summary ?? "",
9080
+ data.taskFile ?? null,
9081
+ null,
9082
+ data.read ? 1 : 0,
9083
+ data.timestamp ?? (/* @__PURE__ */ new Date()).toISOString()
9084
+ ]
9085
+ });
9086
+ unlinkSync6(filePath);
9087
+ migrated++;
9088
+ } catch {
9089
+ }
9090
+ }
9091
+ try {
9092
+ const remaining = readdirSync4(notifDir);
9093
+ if (remaining.length === 0) {
9094
+ rmdirSync(notifDir);
9095
+ }
9096
+ } catch {
9097
+ }
9098
+ } catch {
9099
+ }
9100
+ return migrated;
9101
+ }
9102
+ function eventIcon(event) {
9103
+ switch (event) {
9104
+ case "task_complete":
9105
+ return "Completed:";
9106
+ case "task_needs_fix":
9107
+ return "Needs fix:";
9108
+ case "session_summary":
9109
+ return "Session:";
9110
+ case "error_spike":
9111
+ return "Errors:";
9112
+ case "orphan_task":
9113
+ return "Orphan:";
9114
+ case "subtasks_complete":
9115
+ return "Subtasks done:";
9116
+ case "capacity_relaunch":
9117
+ return "Relaunched:";
9118
+ }
9119
+ }
9120
+ function formatTimeAgo(timestamp) {
9121
+ const diffMs = Date.now() - new Date(timestamp).getTime();
9122
+ const mins = Math.floor(diffMs / 6e4);
9123
+ if (mins < 1) return "just now";
9124
+ if (mins < 60) return `${mins}m ago`;
9125
+ const hours = Math.floor(mins / 60);
9126
+ if (hours < 24) return `${hours}h ago`;
9127
+ const days = Math.floor(hours / 24);
9128
+ return `${days}d ago`;
9129
+ }
9130
+ var CLEANUP_DAYS;
8851
9131
  var init_notifications = __esm({
8852
9132
  "src/lib/notifications.ts"() {
8853
9133
  "use strict";
8854
9134
  init_database();
8855
9135
  init_task_scope();
9136
+ CLEANUP_DAYS = 7;
8856
9137
  }
8857
9138
  });
8858
9139
 
@@ -10284,6 +10565,20 @@ async function updateTask(input) {
10284
10565
  notifyTaskDone();
10285
10566
  }
10286
10567
  await markTaskNotificationsRead(taskFile);
10568
+ if (input.status === "needs_review" && !isCoordinator) {
10569
+ try {
10570
+ const { writeNotification: writeNotification2 } = await Promise.resolve().then(() => (init_notifications(), notifications_exports));
10571
+ await writeNotification2({
10572
+ agentId: String(row.assigned_to),
10573
+ agentRole: String(row.assigned_to),
10574
+ event: "task_complete",
10575
+ project: String(row.project_name),
10576
+ summary: `"${String(row.title)}" is ready for review`,
10577
+ taskFile
10578
+ });
10579
+ } catch {
10580
+ }
10581
+ }
10287
10582
  if (input.status === "done" || input.status === "closed") {
10288
10583
  try {
10289
10584
  await cascadeUnblock(taskId, input.baseDir, now);