@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
@@ -154,6 +154,17 @@ function normalizeOrchestration(raw) {
154
154
  const userOrg = raw.orchestration ?? {};
155
155
  raw.orchestration = { ...defaultOrg, ...userOrg };
156
156
  }
157
+ function normalizeCloudEndpoint(raw) {
158
+ const cloud = raw.cloud;
159
+ if (!cloud?.endpoint) return;
160
+ const ep = String(cloud.endpoint);
161
+ if (ep === "https://askexe.com/cloud" || ep === "https://askexe.com/cloud/") {
162
+ cloud.endpoint = "https://cloud.askexe.com";
163
+ process.stderr.write(
164
+ "[config] Auto-migrated cloud endpoint: askexe.com/cloud \u2192 cloud.askexe.com\n"
165
+ );
166
+ }
167
+ }
157
168
  async function loadConfig() {
158
169
  const dir = process.env.EXE_OS_DIR ?? process.env.EXE_MEM_DIR ?? EXE_AI_DIR;
159
170
  await ensurePrivateDir(dir);
@@ -179,6 +190,7 @@ async function loadConfig() {
179
190
  normalizeSessionLifecycle(migratedCfg);
180
191
  normalizeAutoUpdate(migratedCfg);
181
192
  normalizeOrchestration(migratedCfg);
193
+ normalizeCloudEndpoint(migratedCfg);
182
194
  const config = { ...DEFAULT_CONFIG, dbPath: path.join(dir, "memories.db"), ...migratedCfg };
183
195
  if (config.dbPath.startsWith("~")) {
184
196
  config.dbPath = config.dbPath.replace(/^~/, os.homedir());
@@ -207,6 +219,7 @@ function loadConfigSync() {
207
219
  normalizeSessionLifecycle(migratedCfg);
208
220
  normalizeAutoUpdate(migratedCfg);
209
221
  normalizeOrchestration(migratedCfg);
222
+ normalizeCloudEndpoint(migratedCfg);
210
223
  const config = { ...DEFAULT_CONFIG, dbPath: path.join(dir, "memories.db"), ...migratedCfg };
211
224
  if (config.dbPath.startsWith("~")) {
212
225
  config.dbPath = config.dbPath.replace(/^~/, os.homedir());
@@ -367,6 +380,7 @@ __export(agent_config_exports, {
367
380
  clearAgentRuntime: () => clearAgentRuntime,
368
381
  getAgentRuntime: () => getAgentRuntime,
369
382
  loadAgentConfig: () => loadAgentConfig,
383
+ normalizeCcModelName: () => normalizeCcModelName,
370
384
  saveAgentConfig: () => saveAgentConfig,
371
385
  setAgentMcps: () => setAgentMcps,
372
386
  setAgentRuntime: () => setAgentRuntime
@@ -395,6 +409,13 @@ function getAgentRuntime(agentId) {
395
409
  if (orgDefault) return orgDefault;
396
410
  return { runtime: DEFAULT_RUNTIME, model: DEFAULT_MODELS[DEFAULT_RUNTIME] };
397
411
  }
412
+ function normalizeCcModelName(model) {
413
+ let ccModel = model.replace(/(\d+)\.(\d+)/g, "$1-$2");
414
+ if (/claude-(opus|sonnet)-4-[6-9]/.test(ccModel) && !ccModel.includes("[1m]")) {
415
+ ccModel += "[1m]";
416
+ }
417
+ return ccModel;
418
+ }
398
419
  function setAgentRuntime(agentId, runtime, model, reasoning_effort, mcps) {
399
420
  const knownModels = KNOWN_RUNTIMES[runtime];
400
421
  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);
@@ -6468,6 +6501,7 @@ var init_provider_table = __esm({
6468
6501
  // src/lib/intercom-queue.ts
6469
6502
  var intercom_queue_exports = {};
6470
6503
  __export(intercom_queue_exports, {
6504
+ _resetDrainGuard: () => _resetDrainGuard,
6471
6505
  clearQueueForAgent: () => clearQueueForAgent,
6472
6506
  drainForSession: () => drainForSession,
6473
6507
  drainQueue: () => drainQueue,
@@ -6513,38 +6547,47 @@ function queueIntercom(targetSession, reason) {
6513
6547
  writeQueue(queue);
6514
6548
  }
6515
6549
  function drainQueue(isSessionBusy, sendKeys) {
6550
+ if (_draining) {
6551
+ logQueue("SKIP_DRAIN \u2014 previous drain still running (possible tmux hang)");
6552
+ return { drained: 0, failed: 0 };
6553
+ }
6516
6554
  const queue = readQueue();
6517
6555
  if (queue.length === 0) return { drained: 0, failed: 0 };
6556
+ _draining = true;
6518
6557
  const remaining = [];
6519
6558
  let drained = 0;
6520
6559
  let failed = 0;
6521
- for (const item of queue) {
6522
- const age = Date.now() - new Date(item.queuedAt).getTime();
6523
- if (age > TTL_MS) {
6524
- logQueue(`EXPIRED \u2192 ${item.targetSession} (${Math.round(age / 6e4)}min old, reason: ${item.reason})`);
6525
- failed++;
6526
- continue;
6527
- }
6528
- try {
6529
- if (!isSessionBusy(item.targetSession)) {
6530
- const success = sendKeys(item.targetSession);
6531
- if (success) {
6532
- logQueue(`DRAINED \u2192 ${item.targetSession} (after ${item.attempts} retries)`);
6533
- drained++;
6534
- continue;
6560
+ try {
6561
+ for (const item of queue) {
6562
+ const age = Date.now() - new Date(item.queuedAt).getTime();
6563
+ if (age > TTL_MS) {
6564
+ logQueue(`EXPIRED \u2192 ${item.targetSession} (${Math.round(age / 6e4)}min old, reason: ${item.reason})`);
6565
+ failed++;
6566
+ continue;
6567
+ }
6568
+ try {
6569
+ if (!isSessionBusy(item.targetSession)) {
6570
+ const success = sendKeys(item.targetSession);
6571
+ if (success) {
6572
+ logQueue(`DRAINED \u2192 ${item.targetSession} (after ${item.attempts} retries)`);
6573
+ drained++;
6574
+ continue;
6575
+ }
6535
6576
  }
6577
+ } catch {
6536
6578
  }
6537
- } catch {
6538
- }
6539
- item.attempts++;
6540
- if (item.attempts >= MAX_RETRIES2) {
6541
- logQueue(`FAILED \u2192 ${item.targetSession} (${MAX_RETRIES2} retries exhausted, reason: ${item.reason})`);
6542
- failed++;
6543
- continue;
6579
+ item.attempts++;
6580
+ if (item.attempts >= MAX_RETRIES2) {
6581
+ logQueue(`FAILED \u2192 ${item.targetSession} (${MAX_RETRIES2} retries exhausted, reason: ${item.reason})`);
6582
+ failed++;
6583
+ continue;
6584
+ }
6585
+ remaining.push(item);
6544
6586
  }
6545
- remaining.push(item);
6587
+ writeQueue(remaining);
6588
+ } finally {
6589
+ _draining = false;
6546
6590
  }
6547
- writeQueue(remaining);
6548
6591
  return { drained, failed };
6549
6592
  }
6550
6593
  function drainForSession(targetSession, sendKeys) {
@@ -6569,6 +6612,9 @@ function clearQueueForAgent(agentName) {
6569
6612
  logQueue(`CLEARED ${before - filtered.length} stale item(s) for ${agentName}`);
6570
6613
  }
6571
6614
  }
6615
+ function _resetDrainGuard() {
6616
+ _draining = false;
6617
+ }
6572
6618
  function logQueue(msg) {
6573
6619
  const line = `[${(/* @__PURE__ */ new Date()).toISOString()}] [queue] ${msg}
6574
6620
  `;
@@ -6580,13 +6626,14 @@ function logQueue(msg) {
6580
6626
  } catch {
6581
6627
  }
6582
6628
  }
6583
- var QUEUE_PATH, MAX_RETRIES2, TTL_MS, INTERCOM_LOG;
6629
+ var QUEUE_PATH, MAX_RETRIES2, TTL_MS, _draining, INTERCOM_LOG;
6584
6630
  var init_intercom_queue = __esm({
6585
6631
  "src/lib/intercom-queue.ts"() {
6586
6632
  "use strict";
6587
6633
  QUEUE_PATH = path10.join(os7.homedir(), ".exe-os", "intercom-queue.json");
6588
6634
  MAX_RETRIES2 = 5;
6589
6635
  TTL_MS = 60 * 60 * 1e3;
6636
+ _draining = false;
6590
6637
  INTERCOM_LOG = path10.join(os7.homedir(), ".exe-os", "intercom.log");
6591
6638
  }
6592
6639
  });
@@ -7104,6 +7151,21 @@ function isRootSession(name) {
7104
7151
  function extractRootExe(name) {
7105
7152
  if (!name) return null;
7106
7153
  if (!name.includes("-")) return name;
7154
+ try {
7155
+ const roster = (init_employees(), __toCommonJS(employees_exports)).loadEmployeesSync();
7156
+ if (roster.length > 0) {
7157
+ const sortedNames = roster.map((e) => e.name).sort((a, b) => b.length - a.length);
7158
+ for (const agentName of sortedNames) {
7159
+ const escaped = agentName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
7160
+ const regex = new RegExp(`^${escaped}\\d*-(.+)$`);
7161
+ const match = name.match(regex);
7162
+ if (match) {
7163
+ return extractRootExe(match[1]);
7164
+ }
7165
+ }
7166
+ }
7167
+ } catch {
7168
+ }
7107
7169
  const parts = name.split("-").filter(Boolean);
7108
7170
  return parts.length > 0 ? parts[parts.length - 1] : null;
7109
7171
  }
@@ -7122,6 +7184,10 @@ function registerParentExe(sessionKey, parentExe, dispatchedBy) {
7122
7184
  function getParentExe(sessionKey) {
7123
7185
  try {
7124
7186
  const data = JSON.parse(readFileSync9(path14.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`), "utf8"));
7187
+ if (data.registeredAt) {
7188
+ const age = Date.now() - new Date(data.registeredAt).getTime();
7189
+ if (age > PARENT_EXE_CACHE_TTL_MS) return null;
7190
+ }
7125
7191
  return data.parentExe || null;
7126
7192
  } catch {
7127
7193
  return null;
@@ -7393,7 +7459,7 @@ function notifyParentExe(sessionKey) {
7393
7459
  }
7394
7460
  return true;
7395
7461
  }
7396
- var SPAWN_LOCK_DIR, SESSION_CACHE, INTERCOM_DEBOUNCE_MS, CODEX_DEBOUNCE_MS, INTERCOM_LOG2, DEBOUNCE_FILE, DEBOUNCE_CLEANUP_AGE_MS, BUSY_PATTERN;
7462
+ var SPAWN_LOCK_DIR, SESSION_CACHE, PARENT_EXE_CACHE_TTL_MS, INTERCOM_DEBOUNCE_MS, CODEX_DEBOUNCE_MS, INTERCOM_LOG2, DEBOUNCE_FILE, DEBOUNCE_CLEANUP_AGE_MS, BUSY_PATTERN;
7397
7463
  var init_tmux_routing = __esm({
7398
7464
  "src/lib/tmux-routing.ts"() {
7399
7465
  "use strict";
@@ -7411,6 +7477,7 @@ var init_tmux_routing = __esm({
7411
7477
  init_agent_symlinks();
7412
7478
  SPAWN_LOCK_DIR = path14.join(os10.homedir(), ".exe-os", "spawn-locks");
7413
7479
  SESSION_CACHE = path14.join(os10.homedir(), ".exe-os", "session-cache");
7480
+ PARENT_EXE_CACHE_TTL_MS = 4 * 60 * 60 * 1e3;
7414
7481
  INTERCOM_DEBOUNCE_MS = 3e4;
7415
7482
  CODEX_DEBOUNCE_MS = 12e4;
7416
7483
  INTERCOM_LOG2 = path14.join(os10.homedir(), ".exe-os", "intercom.log");
@@ -7454,6 +7521,17 @@ var init_task_scope = __esm({
7454
7521
  });
7455
7522
 
7456
7523
  // src/lib/notifications.ts
7524
+ var notifications_exports = {};
7525
+ __export(notifications_exports, {
7526
+ cleanupOldNotifications: () => cleanupOldNotifications,
7527
+ formatNotifications: () => formatNotifications,
7528
+ markAsRead: () => markAsRead,
7529
+ markAsReadByTaskFile: () => markAsReadByTaskFile,
7530
+ markDoneTaskNotificationsAsRead: () => markDoneTaskNotificationsAsRead,
7531
+ migrateJsonNotifications: () => migrateJsonNotifications,
7532
+ readUnreadNotifications: () => readUnreadNotifications,
7533
+ writeNotification: () => writeNotification
7534
+ });
7457
7535
  import crypto2 from "crypto";
7458
7536
  import path15 from "path";
7459
7537
  import os11 from "os";
@@ -7490,6 +7568,52 @@ async function writeNotification(notification) {
7490
7568
  `);
7491
7569
  }
7492
7570
  }
7571
+ async function readUnreadNotifications(agentFilter, sessionScope) {
7572
+ try {
7573
+ const client = getClient();
7574
+ const conditions = ["read = 0"];
7575
+ const args = [];
7576
+ const scope = strictSessionScopeFilter(sessionScope);
7577
+ if (agentFilter) {
7578
+ conditions.push("agent_id = ?");
7579
+ args.push(agentFilter);
7580
+ }
7581
+ const result = await client.execute({
7582
+ sql: `SELECT id, agent_id, agent_role, event, project, summary, task_file, session_scope, created_at
7583
+ FROM notifications
7584
+ WHERE ${conditions.join(" AND ")}${scope.sql}
7585
+ ORDER BY created_at ASC`,
7586
+ args: [...args, ...scope.args]
7587
+ });
7588
+ return result.rows.map((r) => ({
7589
+ id: String(r.id),
7590
+ agentId: String(r.agent_id),
7591
+ agentRole: String(r.agent_role),
7592
+ event: String(r.event),
7593
+ project: String(r.project),
7594
+ summary: String(r.summary),
7595
+ taskFile: r.task_file ? String(r.task_file) : void 0,
7596
+ sessionScope: r.session_scope == null ? null : String(r.session_scope),
7597
+ timestamp: String(r.created_at),
7598
+ read: false
7599
+ }));
7600
+ } catch {
7601
+ return [];
7602
+ }
7603
+ }
7604
+ async function markAsRead(ids, sessionScope) {
7605
+ if (ids.length === 0) return;
7606
+ try {
7607
+ const client = getClient();
7608
+ const placeholders = ids.map(() => "?").join(", ");
7609
+ const scope = strictSessionScopeFilter(sessionScope);
7610
+ await client.execute({
7611
+ sql: `UPDATE notifications SET read = 1 WHERE id IN (${placeholders})${scope.sql}`,
7612
+ args: [...ids, ...scope.args]
7613
+ });
7614
+ } catch {
7615
+ }
7616
+ }
7493
7617
  async function markAsReadByTaskFile(taskFile, sessionScope) {
7494
7618
  try {
7495
7619
  const client = getClient();
@@ -7502,11 +7626,144 @@ async function markAsReadByTaskFile(taskFile, sessionScope) {
7502
7626
  } catch {
7503
7627
  }
7504
7628
  }
7629
+ async function cleanupOldNotifications(daysOld = CLEANUP_DAYS, sessionScope) {
7630
+ try {
7631
+ const client = getClient();
7632
+ const cutoff = new Date(
7633
+ Date.now() - daysOld * 24 * 60 * 60 * 1e3
7634
+ ).toISOString();
7635
+ const scope = strictSessionScopeFilter(sessionScope);
7636
+ const result = await client.execute({
7637
+ sql: `DELETE FROM notifications WHERE created_at < ?${scope.sql}`,
7638
+ args: [cutoff, ...scope.args]
7639
+ });
7640
+ return result.rowsAffected;
7641
+ } catch {
7642
+ return 0;
7643
+ }
7644
+ }
7645
+ async function markDoneTaskNotificationsAsRead(sessionScope) {
7646
+ try {
7647
+ const client = getClient();
7648
+ const scope = strictSessionScopeFilter(sessionScope);
7649
+ const result = await client.execute({
7650
+ sql: `UPDATE notifications SET read = 1
7651
+ WHERE read = 0
7652
+ AND task_file IS NOT NULL
7653
+ ${scope.sql}
7654
+ AND task_file IN (
7655
+ SELECT task_file FROM tasks WHERE status = 'done'${scope.sql}
7656
+ )`,
7657
+ args: [...scope.args, ...scope.args]
7658
+ });
7659
+ return result.rowsAffected;
7660
+ } catch {
7661
+ return 0;
7662
+ }
7663
+ }
7664
+ function formatNotifications(notifications) {
7665
+ if (notifications.length === 0) return "";
7666
+ const grouped = /* @__PURE__ */ new Map();
7667
+ for (const n of notifications) {
7668
+ const key = `${n.agentId}|${n.agentRole}`;
7669
+ if (!grouped.has(key)) grouped.set(key, []);
7670
+ grouped.get(key).push(n);
7671
+ }
7672
+ const lines = [];
7673
+ lines.push(`## Notifications (${notifications.length} unread)
7674
+ `);
7675
+ for (const [key, items] of grouped) {
7676
+ const [agentId, agentRole] = key.split("|");
7677
+ lines.push(`**${agentId}** (${agentRole}):`);
7678
+ for (const item of items) {
7679
+ const ago = formatTimeAgo(item.timestamp);
7680
+ const icon = eventIcon(item.event);
7681
+ lines.push(`- ${icon} ${item.summary} (${item.project}) \u2014 ${ago}`);
7682
+ }
7683
+ lines.push("");
7684
+ }
7685
+ return lines.join("\n");
7686
+ }
7687
+ async function migrateJsonNotifications() {
7688
+ const base = process.env.EXE_OS_DIR || process.env.EXE_MEM_DIR || path15.join(os11.homedir(), ".exe-os");
7689
+ const notifDir = path15.join(base, "notifications");
7690
+ if (!existsSync15(notifDir)) return 0;
7691
+ let migrated = 0;
7692
+ try {
7693
+ const files = readdirSync3(notifDir).filter((f) => f.endsWith(".json"));
7694
+ if (files.length === 0) return 0;
7695
+ const client = getClient();
7696
+ for (const file of files) {
7697
+ try {
7698
+ const filePath = path15.join(notifDir, file);
7699
+ const data = JSON.parse(readFileSync10(filePath, "utf8"));
7700
+ await client.execute({
7701
+ sql: `INSERT OR IGNORE INTO notifications (id, agent_id, agent_role, event, project, summary, task_file, session_scope, read, created_at)
7702
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
7703
+ args: [
7704
+ crypto2.randomUUID(),
7705
+ data.agentId ?? "unknown",
7706
+ data.agentRole ?? "unknown",
7707
+ data.event ?? "session_summary",
7708
+ data.project ?? "unknown",
7709
+ data.summary ?? "",
7710
+ data.taskFile ?? null,
7711
+ null,
7712
+ data.read ? 1 : 0,
7713
+ data.timestamp ?? (/* @__PURE__ */ new Date()).toISOString()
7714
+ ]
7715
+ });
7716
+ unlinkSync4(filePath);
7717
+ migrated++;
7718
+ } catch {
7719
+ }
7720
+ }
7721
+ try {
7722
+ const remaining = readdirSync3(notifDir);
7723
+ if (remaining.length === 0) {
7724
+ rmdirSync(notifDir);
7725
+ }
7726
+ } catch {
7727
+ }
7728
+ } catch {
7729
+ }
7730
+ return migrated;
7731
+ }
7732
+ function eventIcon(event) {
7733
+ switch (event) {
7734
+ case "task_complete":
7735
+ return "Completed:";
7736
+ case "task_needs_fix":
7737
+ return "Needs fix:";
7738
+ case "session_summary":
7739
+ return "Session:";
7740
+ case "error_spike":
7741
+ return "Errors:";
7742
+ case "orphan_task":
7743
+ return "Orphan:";
7744
+ case "subtasks_complete":
7745
+ return "Subtasks done:";
7746
+ case "capacity_relaunch":
7747
+ return "Relaunched:";
7748
+ }
7749
+ }
7750
+ function formatTimeAgo(timestamp) {
7751
+ const diffMs = Date.now() - new Date(timestamp).getTime();
7752
+ const mins = Math.floor(diffMs / 6e4);
7753
+ if (mins < 1) return "just now";
7754
+ if (mins < 60) return `${mins}m ago`;
7755
+ const hours = Math.floor(mins / 60);
7756
+ if (hours < 24) return `${hours}h ago`;
7757
+ const days = Math.floor(hours / 24);
7758
+ return `${days}d ago`;
7759
+ }
7760
+ var CLEANUP_DAYS;
7505
7761
  var init_notifications = __esm({
7506
7762
  "src/lib/notifications.ts"() {
7507
7763
  "use strict";
7508
7764
  init_database();
7509
7765
  init_task_scope();
7766
+ CLEANUP_DAYS = 7;
7510
7767
  }
7511
7768
  });
7512
7769
 
@@ -8557,6 +8814,20 @@ async function updateTask(input) {
8557
8814
  notifyTaskDone();
8558
8815
  }
8559
8816
  await markTaskNotificationsRead(taskFile);
8817
+ if (input.status === "needs_review" && !isCoordinator) {
8818
+ try {
8819
+ const { writeNotification: writeNotification2 } = await Promise.resolve().then(() => (init_notifications(), notifications_exports));
8820
+ await writeNotification2({
8821
+ agentId: String(row.assigned_to),
8822
+ agentRole: String(row.assigned_to),
8823
+ event: "task_complete",
8824
+ project: String(row.project_name),
8825
+ summary: `"${String(row.title)}" is ready for review`,
8826
+ taskFile
8827
+ });
8828
+ } catch {
8829
+ }
8830
+ }
8560
8831
  if (input.status === "done" || input.status === "closed") {
8561
8832
  try {
8562
8833
  await cascadeUnblock(taskId, input.baseDir, now);