@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
@@ -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);