@askexenow/exe-os 0.9.8 → 0.9.9

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 (101) hide show
  1. package/dist/bin/backfill-conversations.js +222 -49
  2. package/dist/bin/backfill-responses.js +221 -48
  3. package/dist/bin/backfill-vectors.js +225 -52
  4. package/dist/bin/cleanup-stale-review-tasks.js +150 -28
  5. package/dist/bin/cli.js +1295 -856
  6. package/dist/bin/exe-agent-config.js +36 -8
  7. package/dist/bin/exe-agent.js +14 -4
  8. package/dist/bin/exe-assign.js +221 -48
  9. package/dist/bin/exe-boot.js +778 -427
  10. package/dist/bin/exe-call.js +41 -13
  11. package/dist/bin/exe-cloud.js +163 -58
  12. package/dist/bin/exe-dispatch.js +276 -139
  13. package/dist/bin/exe-doctor.js +145 -27
  14. package/dist/bin/exe-export-behaviors.js +141 -23
  15. package/dist/bin/exe-forget.js +137 -19
  16. package/dist/bin/exe-gateway.js +677 -388
  17. package/dist/bin/exe-heartbeat.js +227 -108
  18. package/dist/bin/exe-kill.js +138 -20
  19. package/dist/bin/exe-launch-agent.js +172 -39
  20. package/dist/bin/exe-link.js +291 -100
  21. package/dist/bin/exe-new-employee.js +214 -106
  22. package/dist/bin/exe-pending-messages.js +395 -33
  23. package/dist/bin/exe-pending-notifications.js +684 -99
  24. package/dist/bin/exe-pending-reviews.js +420 -74
  25. package/dist/bin/exe-rename.js +147 -49
  26. package/dist/bin/exe-review.js +138 -20
  27. package/dist/bin/exe-search.js +240 -69
  28. package/dist/bin/exe-session-cleanup.js +440 -250
  29. package/dist/bin/exe-settings.js +61 -17
  30. package/dist/bin/exe-start-codex.js +158 -39
  31. package/dist/bin/exe-start-opencode.js +157 -38
  32. package/dist/bin/exe-status.js +151 -29
  33. package/dist/bin/exe-team.js +138 -20
  34. package/dist/bin/git-sweep.js +404 -212
  35. package/dist/bin/graph-backfill.js +137 -19
  36. package/dist/bin/graph-export.js +140 -22
  37. package/dist/bin/install.js +90 -61
  38. package/dist/bin/scan-tasks.js +412 -220
  39. package/dist/bin/setup.js +564 -293
  40. package/dist/bin/shard-migrate.js +139 -21
  41. package/dist/bin/update.js +138 -49
  42. package/dist/bin/wiki-sync.js +137 -19
  43. package/dist/gateway/index.js +533 -320
  44. package/dist/hooks/bug-report-worker.js +344 -193
  45. package/dist/hooks/codex-stop-task-finalizer.js +4678 -0
  46. package/dist/hooks/commit-complete.js +402 -210
  47. package/dist/hooks/error-recall.js +245 -74
  48. package/dist/hooks/exe-heartbeat-hook.js +16 -6
  49. package/dist/hooks/ingest-worker.js +3423 -3157
  50. package/dist/hooks/ingest.js +832 -97
  51. package/dist/hooks/instructions-loaded.js +227 -54
  52. package/dist/hooks/notification.js +216 -43
  53. package/dist/hooks/post-compact.js +239 -62
  54. package/dist/hooks/pre-compact.js +408 -216
  55. package/dist/hooks/pre-tool-use.js +268 -90
  56. package/dist/hooks/prompt-ingest-worker.js +352 -102
  57. package/dist/hooks/prompt-submit.js +541 -328
  58. package/dist/hooks/response-ingest-worker.js +372 -122
  59. package/dist/hooks/session-end.js +443 -240
  60. package/dist/hooks/session-start.js +313 -127
  61. package/dist/hooks/stop.js +293 -98
  62. package/dist/hooks/subagent-stop.js +239 -62
  63. package/dist/hooks/summary-worker.js +568 -236
  64. package/dist/index.js +538 -324
  65. package/dist/lib/agent-config.js +28 -6
  66. package/dist/lib/cloud-sync.js +284 -105
  67. package/dist/lib/config.js +30 -10
  68. package/dist/lib/consolidation.js +16 -6
  69. package/dist/lib/database.js +123 -25
  70. package/dist/lib/db-daemon-client.js +73 -19
  71. package/dist/lib/db.js +123 -25
  72. package/dist/lib/device-registry.js +133 -35
  73. package/dist/lib/embedder.js +107 -32
  74. package/dist/lib/employee-templates.js +14 -4
  75. package/dist/lib/employees.js +41 -13
  76. package/dist/lib/exe-daemon-client.js +88 -22
  77. package/dist/lib/exe-daemon.js +935 -587
  78. package/dist/lib/hybrid-search.js +240 -69
  79. package/dist/lib/identity.js +18 -8
  80. package/dist/lib/license.js +133 -48
  81. package/dist/lib/messaging.js +116 -56
  82. package/dist/lib/reminders.js +14 -4
  83. package/dist/lib/schedules.js +137 -19
  84. package/dist/lib/skill-learning.js +33 -6
  85. package/dist/lib/store.js +137 -19
  86. package/dist/lib/task-router.js +14 -4
  87. package/dist/lib/tasks.js +280 -234
  88. package/dist/lib/tmux-routing.js +172 -125
  89. package/dist/lib/token-spend.js +26 -8
  90. package/dist/mcp/server.js +1326 -609
  91. package/dist/mcp/tools/complete-reminder.js +14 -4
  92. package/dist/mcp/tools/create-reminder.js +14 -4
  93. package/dist/mcp/tools/create-task.js +306 -248
  94. package/dist/mcp/tools/deactivate-behavior.js +16 -6
  95. package/dist/mcp/tools/list-reminders.js +14 -4
  96. package/dist/mcp/tools/list-tasks.js +123 -107
  97. package/dist/mcp/tools/send-message.js +75 -29
  98. package/dist/mcp/tools/update-task.js +1848 -199
  99. package/dist/runtime/index.js +441 -248
  100. package/dist/tui/App.js +761 -424
  101. package/package.json +1 -1
@@ -307,9 +307,47 @@ var init_provider_table = __esm({
307
307
  }
308
308
  });
309
309
 
310
+ // src/lib/secure-files.ts
311
+ import { chmodSync, existsSync as existsSync2, mkdirSync as mkdirSync2 } from "fs";
312
+ import { chmod, mkdir } from "fs/promises";
313
+ async function ensurePrivateDir(dirPath) {
314
+ await mkdir(dirPath, { recursive: true, mode: PRIVATE_DIR_MODE });
315
+ try {
316
+ await chmod(dirPath, PRIVATE_DIR_MODE);
317
+ } catch {
318
+ }
319
+ }
320
+ function ensurePrivateDirSync(dirPath) {
321
+ mkdirSync2(dirPath, { recursive: true, mode: PRIVATE_DIR_MODE });
322
+ try {
323
+ chmodSync(dirPath, PRIVATE_DIR_MODE);
324
+ } catch {
325
+ }
326
+ }
327
+ async function enforcePrivateFile(filePath) {
328
+ try {
329
+ await chmod(filePath, PRIVATE_FILE_MODE);
330
+ } catch {
331
+ }
332
+ }
333
+ function enforcePrivateFileSync(filePath) {
334
+ try {
335
+ if (existsSync2(filePath)) chmodSync(filePath, PRIVATE_FILE_MODE);
336
+ } catch {
337
+ }
338
+ }
339
+ var PRIVATE_DIR_MODE, PRIVATE_FILE_MODE;
340
+ var init_secure_files = __esm({
341
+ "src/lib/secure-files.ts"() {
342
+ "use strict";
343
+ PRIVATE_DIR_MODE = 448;
344
+ PRIVATE_FILE_MODE = 384;
345
+ }
346
+ });
347
+
310
348
  // src/lib/config.ts
311
- import { readFile, writeFile, mkdir, chmod } from "fs/promises";
312
- import { readFileSync as readFileSync2, existsSync as existsSync2, renameSync } from "fs";
349
+ import { readFile, writeFile } from "fs/promises";
350
+ import { readFileSync as readFileSync2, existsSync as existsSync3, renameSync } from "fs";
313
351
  import path2 from "path";
314
352
  import os2 from "os";
315
353
  function resolveDataDir() {
@@ -317,7 +355,7 @@ function resolveDataDir() {
317
355
  if (process.env.EXE_MEM_DIR) return process.env.EXE_MEM_DIR;
318
356
  const newDir = path2.join(os2.homedir(), ".exe-os");
319
357
  const legacyDir = path2.join(os2.homedir(), ".exe-mem");
320
- if (!existsSync2(newDir) && existsSync2(legacyDir)) {
358
+ if (!existsSync3(newDir) && existsSync3(legacyDir)) {
321
359
  try {
322
360
  renameSync(legacyDir, newDir);
323
361
  process.stderr.write(`[exe-os] Migrated data directory: ~/.exe-mem \u2192 ~/.exe-os
@@ -380,9 +418,9 @@ function normalizeAutoUpdate(raw) {
380
418
  }
381
419
  async function loadConfig() {
382
420
  const dir = process.env.EXE_OS_DIR ?? process.env.EXE_MEM_DIR ?? EXE_AI_DIR;
383
- await mkdir(dir, { recursive: true });
421
+ await ensurePrivateDir(dir);
384
422
  const configPath = path2.join(dir, "config.json");
385
- if (!existsSync2(configPath)) {
423
+ if (!existsSync3(configPath)) {
386
424
  return { ...DEFAULT_CONFIG, dbPath: path2.join(dir, "memories.db") };
387
425
  }
388
426
  const raw = await readFile(configPath, "utf-8");
@@ -395,6 +433,7 @@ async function loadConfig() {
395
433
  `);
396
434
  try {
397
435
  await writeFile(configPath, JSON.stringify(migratedCfg, null, 2) + "\n");
436
+ await enforcePrivateFile(configPath);
398
437
  } catch {
399
438
  }
400
439
  }
@@ -414,6 +453,7 @@ var EXE_AI_DIR, DB_PATH, MODELS_DIR, CONFIG_PATH, LEGACY_LANCE_PATH, CURRENT_CON
414
453
  var init_config = __esm({
415
454
  "src/lib/config.ts"() {
416
455
  "use strict";
456
+ init_secure_files();
417
457
  EXE_AI_DIR = resolveDataDir();
418
458
  DB_PATH = path2.join(EXE_AI_DIR, "memories.db");
419
459
  MODELS_DIR = path2.join(EXE_AI_DIR, "models");
@@ -518,10 +558,10 @@ var init_runtime_table = __esm({
518
558
  });
519
559
 
520
560
  // src/lib/agent-config.ts
521
- import { readFileSync as readFileSync3, writeFileSync as writeFileSync2, existsSync as existsSync3, mkdirSync as mkdirSync2 } from "fs";
561
+ import { readFileSync as readFileSync3, writeFileSync as writeFileSync2, existsSync as existsSync4 } from "fs";
522
562
  import path3 from "path";
523
563
  function loadAgentConfig() {
524
- if (!existsSync3(AGENT_CONFIG_PATH)) return {};
564
+ if (!existsSync4(AGENT_CONFIG_PATH)) return {};
525
565
  try {
526
566
  return JSON.parse(readFileSync3(AGENT_CONFIG_PATH, "utf-8"));
527
567
  } catch {
@@ -542,6 +582,7 @@ var init_agent_config = __esm({
542
582
  "use strict";
543
583
  init_config();
544
584
  init_runtime_table();
585
+ init_secure_files();
545
586
  AGENT_CONFIG_PATH = path3.join(EXE_AI_DIR, "agent-config.json");
546
587
  DEFAULT_MODELS = {
547
588
  claude: "claude-opus-4",
@@ -560,16 +601,16 @@ __export(intercom_queue_exports, {
560
601
  queueIntercom: () => queueIntercom,
561
602
  readQueue: () => readQueue
562
603
  });
563
- import { readFileSync as readFileSync4, writeFileSync as writeFileSync3, renameSync as renameSync2, existsSync as existsSync4, mkdirSync as mkdirSync3 } from "fs";
604
+ import { readFileSync as readFileSync4, writeFileSync as writeFileSync3, renameSync as renameSync2, existsSync as existsSync5, mkdirSync as mkdirSync3 } from "fs";
564
605
  import path4 from "path";
565
606
  import os3 from "os";
566
607
  function ensureDir() {
567
608
  const dir = path4.dirname(QUEUE_PATH);
568
- if (!existsSync4(dir)) mkdirSync3(dir, { recursive: true });
609
+ if (!existsSync5(dir)) mkdirSync3(dir, { recursive: true });
569
610
  }
570
611
  function readQueue() {
571
612
  try {
572
- if (!existsSync4(QUEUE_PATH)) return [];
613
+ if (!existsSync5(QUEUE_PATH)) return [];
573
614
  return JSON.parse(readFileSync4(QUEUE_PATH, "utf8"));
574
615
  } catch {
575
616
  return [];
@@ -734,7 +775,7 @@ var init_db_retry = __esm({
734
775
 
735
776
  // src/lib/employees.ts
736
777
  import { readFile as readFile2, writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
737
- import { existsSync as existsSync5, symlinkSync, readlinkSync, readFileSync as readFileSync5, renameSync as renameSync3, unlinkSync, writeFileSync as writeFileSync4 } from "fs";
778
+ import { existsSync as existsSync6, symlinkSync, readlinkSync, readFileSync as readFileSync5, renameSync as renameSync3, unlinkSync, writeFileSync as writeFileSync4 } from "fs";
738
779
  import { execSync as execSync3 } from "child_process";
739
780
  import path5 from "path";
740
781
  import os4 from "os";
@@ -755,7 +796,7 @@ function isCoordinatorName(agentName, employees = loadEmployeesSync()) {
755
796
  return agentName.toLowerCase() === getCoordinatorName(employees).toLowerCase();
756
797
  }
757
798
  function loadEmployeesSync(employeesPath = EMPLOYEES_PATH) {
758
- if (!existsSync5(employeesPath)) return [];
799
+ if (!existsSync6(employeesPath)) return [];
759
800
  try {
760
801
  return JSON.parse(readFileSync5(employeesPath, "utf-8"));
761
802
  } catch {
@@ -1376,13 +1417,50 @@ var init_database_adapter = __esm({
1376
1417
  }
1377
1418
  });
1378
1419
 
1420
+ // src/lib/daemon-auth.ts
1421
+ import crypto from "crypto";
1422
+ import path7 from "path";
1423
+ import { existsSync as existsSync7, readFileSync as readFileSync6, writeFileSync as writeFileSync5 } from "fs";
1424
+ function normalizeToken(token) {
1425
+ if (!token) return null;
1426
+ const trimmed = token.trim();
1427
+ return trimmed.length > 0 ? trimmed : null;
1428
+ }
1429
+ function readDaemonToken() {
1430
+ try {
1431
+ if (!existsSync7(DAEMON_TOKEN_PATH)) return null;
1432
+ return normalizeToken(readFileSync6(DAEMON_TOKEN_PATH, "utf8"));
1433
+ } catch {
1434
+ return null;
1435
+ }
1436
+ }
1437
+ function ensureDaemonToken(seed) {
1438
+ const existing = readDaemonToken();
1439
+ if (existing) return existing;
1440
+ const token = normalizeToken(seed) ?? crypto.randomBytes(32).toString("hex");
1441
+ ensurePrivateDirSync(EXE_AI_DIR);
1442
+ writeFileSync5(DAEMON_TOKEN_PATH, `${token}
1443
+ `, "utf8");
1444
+ enforcePrivateFileSync(DAEMON_TOKEN_PATH);
1445
+ return token;
1446
+ }
1447
+ var DAEMON_TOKEN_PATH;
1448
+ var init_daemon_auth = __esm({
1449
+ "src/lib/daemon-auth.ts"() {
1450
+ "use strict";
1451
+ init_config();
1452
+ init_secure_files();
1453
+ DAEMON_TOKEN_PATH = path7.join(EXE_AI_DIR, "exed.token");
1454
+ }
1455
+ });
1456
+
1379
1457
  // src/lib/exe-daemon-client.ts
1380
1458
  import net from "net";
1381
1459
  import os6 from "os";
1382
1460
  import { spawn } from "child_process";
1383
1461
  import { randomUUID } from "crypto";
1384
- import { existsSync as existsSync6, unlinkSync as unlinkSync2, readFileSync as readFileSync6, openSync, closeSync, statSync } from "fs";
1385
- import path7 from "path";
1462
+ import { existsSync as existsSync8, unlinkSync as unlinkSync2, readFileSync as readFileSync7, openSync, closeSync, statSync } from "fs";
1463
+ import path8 from "path";
1386
1464
  import { fileURLToPath } from "url";
1387
1465
  function handleData(chunk) {
1388
1466
  _buffer += chunk.toString();
@@ -1410,9 +1488,9 @@ function handleData(chunk) {
1410
1488
  }
1411
1489
  }
1412
1490
  function cleanupStaleFiles() {
1413
- if (existsSync6(PID_PATH)) {
1491
+ if (existsSync8(PID_PATH)) {
1414
1492
  try {
1415
- const pid = parseInt(readFileSync6(PID_PATH, "utf8").trim(), 10);
1493
+ const pid = parseInt(readFileSync7(PID_PATH, "utf8").trim(), 10);
1416
1494
  if (pid > 0) {
1417
1495
  try {
1418
1496
  process.kill(pid, 0);
@@ -1433,11 +1511,11 @@ function cleanupStaleFiles() {
1433
1511
  }
1434
1512
  }
1435
1513
  function findPackageRoot() {
1436
- let dir = path7.dirname(fileURLToPath(import.meta.url));
1437
- const { root } = path7.parse(dir);
1514
+ let dir = path8.dirname(fileURLToPath(import.meta.url));
1515
+ const { root } = path8.parse(dir);
1438
1516
  while (dir !== root) {
1439
- if (existsSync6(path7.join(dir, "package.json"))) return dir;
1440
- dir = path7.dirname(dir);
1517
+ if (existsSync8(path8.join(dir, "package.json"))) return dir;
1518
+ dir = path8.dirname(dir);
1441
1519
  }
1442
1520
  return null;
1443
1521
  }
@@ -1463,16 +1541,17 @@ function spawnDaemon() {
1463
1541
  process.stderr.write("[exed-client] WARN: cannot find package root\n");
1464
1542
  return;
1465
1543
  }
1466
- const daemonPath = path7.join(pkgRoot, "dist", "lib", "exe-daemon.js");
1467
- if (!existsSync6(daemonPath)) {
1544
+ const daemonPath = path8.join(pkgRoot, "dist", "lib", "exe-daemon.js");
1545
+ if (!existsSync8(daemonPath)) {
1468
1546
  process.stderr.write(`[exed-client] WARN: daemon script not found at ${daemonPath}
1469
1547
  `);
1470
1548
  return;
1471
1549
  }
1472
1550
  const resolvedPath = daemonPath;
1551
+ const daemonToken = ensureDaemonToken(process.env[DAEMON_TOKEN_ENV] ?? null);
1473
1552
  process.stderr.write(`[exed-client] Spawning daemon: ${resolvedPath}
1474
1553
  `);
1475
- const logPath = path7.join(path7.dirname(SOCKET_PATH), "exed.log");
1554
+ const logPath = path8.join(path8.dirname(SOCKET_PATH), "exed.log");
1476
1555
  let stderrFd = "ignore";
1477
1556
  try {
1478
1557
  stderrFd = openSync(logPath, "a");
@@ -1490,7 +1569,8 @@ function spawnDaemon() {
1490
1569
  TMUX_PANE: void 0,
1491
1570
  // Prevents resolveExeSession() from scoping to one session
1492
1571
  EXE_DAEMON_SOCK: SOCKET_PATH,
1493
- EXE_DAEMON_PID: PID_PATH
1572
+ EXE_DAEMON_PID: PID_PATH,
1573
+ [DAEMON_TOKEN_ENV]: daemonToken
1494
1574
  }
1495
1575
  });
1496
1576
  child.unref();
@@ -1597,13 +1677,14 @@ function sendDaemonRequest(payload, timeoutMs = REQUEST_TIMEOUT_MS) {
1597
1677
  return;
1598
1678
  }
1599
1679
  const id = randomUUID();
1680
+ const token = process.env[DAEMON_TOKEN_ENV] ?? readDaemonToken();
1600
1681
  const timer = setTimeout(() => {
1601
1682
  _pending.delete(id);
1602
1683
  resolve({ error: "Request timeout" });
1603
1684
  }, timeoutMs);
1604
1685
  _pending.set(id, { resolve, timer });
1605
1686
  try {
1606
- _socket.write(JSON.stringify({ id, ...payload }) + "\n");
1687
+ _socket.write(JSON.stringify({ id, token, ...payload }) + "\n");
1607
1688
  } catch {
1608
1689
  clearTimeout(timer);
1609
1690
  _pending.delete(id);
@@ -1614,17 +1695,19 @@ function sendDaemonRequest(payload, timeoutMs = REQUEST_TIMEOUT_MS) {
1614
1695
  function isClientConnected() {
1615
1696
  return _connected;
1616
1697
  }
1617
- var SOCKET_PATH, PID_PATH, SPAWN_LOCK_PATH, SPAWN_LOCK_STALE_MS, CONNECT_TIMEOUT_MS, REQUEST_TIMEOUT_MS, _socket, _connected, _buffer, _pending, MAX_BUFFER;
1698
+ var SOCKET_PATH, PID_PATH, SPAWN_LOCK_PATH, SPAWN_LOCK_STALE_MS, CONNECT_TIMEOUT_MS, REQUEST_TIMEOUT_MS, DAEMON_TOKEN_ENV, _socket, _connected, _buffer, _pending, MAX_BUFFER;
1618
1699
  var init_exe_daemon_client = __esm({
1619
1700
  "src/lib/exe-daemon-client.ts"() {
1620
1701
  "use strict";
1621
1702
  init_config();
1622
- SOCKET_PATH = process.env.EXE_DAEMON_SOCK ?? process.env.EXE_EMBED_SOCK ?? path7.join(EXE_AI_DIR, "exed.sock");
1623
- PID_PATH = process.env.EXE_DAEMON_PID ?? process.env.EXE_EMBED_PID ?? path7.join(EXE_AI_DIR, "exed.pid");
1624
- SPAWN_LOCK_PATH = path7.join(EXE_AI_DIR, "exed-spawn.lock");
1703
+ init_daemon_auth();
1704
+ SOCKET_PATH = process.env.EXE_DAEMON_SOCK ?? process.env.EXE_EMBED_SOCK ?? path8.join(EXE_AI_DIR, "exed.sock");
1705
+ PID_PATH = process.env.EXE_DAEMON_PID ?? process.env.EXE_EMBED_PID ?? path8.join(EXE_AI_DIR, "exed.pid");
1706
+ SPAWN_LOCK_PATH = path8.join(EXE_AI_DIR, "exed-spawn.lock");
1625
1707
  SPAWN_LOCK_STALE_MS = 3e4;
1626
1708
  CONNECT_TIMEOUT_MS = 15e3;
1627
1709
  REQUEST_TIMEOUT_MS = 3e4;
1710
+ DAEMON_TOKEN_ENV = "EXE_DAEMON_TOKEN";
1628
1711
  _socket = null;
1629
1712
  _connected = false;
1630
1713
  _buffer = "";
@@ -2203,6 +2286,7 @@ async function ensureSchema() {
2203
2286
  project TEXT NOT NULL,
2204
2287
  summary TEXT NOT NULL,
2205
2288
  task_file TEXT,
2289
+ session_scope TEXT,
2206
2290
  read INTEGER NOT NULL DEFAULT 0,
2207
2291
  created_at TEXT NOT NULL
2208
2292
  );
@@ -2211,7 +2295,7 @@ async function ensureSchema() {
2211
2295
  ON notifications(read);
2212
2296
 
2213
2297
  CREATE INDEX IF NOT EXISTS idx_notifications_agent
2214
- ON notifications(agent_id);
2298
+ ON notifications(agent_id, session_scope);
2215
2299
 
2216
2300
  CREATE INDEX IF NOT EXISTS idx_notifications_task_file
2217
2301
  ON notifications(task_file);
@@ -2249,6 +2333,7 @@ async function ensureSchema() {
2249
2333
  target_agent TEXT NOT NULL,
2250
2334
  target_project TEXT,
2251
2335
  target_device TEXT NOT NULL DEFAULT 'local',
2336
+ session_scope TEXT,
2252
2337
  content TEXT NOT NULL,
2253
2338
  priority TEXT DEFAULT 'normal',
2254
2339
  status TEXT DEFAULT 'pending',
@@ -2262,10 +2347,31 @@ async function ensureSchema() {
2262
2347
  );
2263
2348
 
2264
2349
  CREATE INDEX IF NOT EXISTS idx_messages_target
2265
- ON messages(target_agent, status);
2350
+ ON messages(target_agent, session_scope, status);
2266
2351
 
2267
2352
  CREATE INDEX IF NOT EXISTS idx_messages_conversation_order
2268
- ON messages(target_agent, from_agent, server_seq);
2353
+ ON messages(target_agent, session_scope, from_agent, server_seq);
2354
+ `);
2355
+ try {
2356
+ await client.execute({
2357
+ sql: `ALTER TABLE notifications ADD COLUMN session_scope TEXT`,
2358
+ args: []
2359
+ });
2360
+ } catch {
2361
+ }
2362
+ try {
2363
+ await client.execute({
2364
+ sql: `ALTER TABLE messages ADD COLUMN session_scope TEXT`,
2365
+ args: []
2366
+ });
2367
+ } catch {
2368
+ }
2369
+ await client.executeMultiple(`
2370
+ CREATE INDEX IF NOT EXISTS idx_notifications_agent_scope_read
2371
+ ON notifications(agent_id, session_scope, read, created_at);
2372
+
2373
+ CREATE INDEX IF NOT EXISTS idx_messages_target_scope_status
2374
+ ON messages(target_agent, session_scope, status, created_at);
2269
2375
  `);
2270
2376
  try {
2271
2377
  await client.execute({
@@ -2849,6 +2955,13 @@ async function ensureSchema() {
2849
2955
  } catch {
2850
2956
  }
2851
2957
  }
2958
+ try {
2959
+ await client.execute({
2960
+ sql: `UPDATE tasks SET status = 'closed' WHERE status = 'done' AND result IS NOT NULL`,
2961
+ args: []
2962
+ });
2963
+ } catch {
2964
+ }
2852
2965
  }
2853
2966
  async function disposeDatabase() {
2854
2967
  if (_walCheckpointTimer) {
@@ -2887,18 +3000,21 @@ var init_database = __esm({
2887
3000
  });
2888
3001
 
2889
3002
  // src/lib/license.ts
2890
- import { readFileSync as readFileSync7, writeFileSync as writeFileSync5, existsSync as existsSync7, mkdirSync as mkdirSync4 } from "fs";
3003
+ import { readFileSync as readFileSync8, writeFileSync as writeFileSync6, existsSync as existsSync9, mkdirSync as mkdirSync4 } from "fs";
2891
3004
  import { randomUUID as randomUUID2 } from "crypto";
2892
- import path8 from "path";
3005
+ import { createRequire as createRequire2 } from "module";
3006
+ import { pathToFileURL as pathToFileURL2 } from "url";
3007
+ import os7 from "os";
3008
+ import path9 from "path";
2893
3009
  import { jwtVerify, importSPKI } from "jose";
2894
3010
  var LICENSE_PATH, CACHE_PATH, DEVICE_ID_PATH, PLAN_LIMITS;
2895
3011
  var init_license = __esm({
2896
3012
  "src/lib/license.ts"() {
2897
3013
  "use strict";
2898
3014
  init_config();
2899
- LICENSE_PATH = path8.join(EXE_AI_DIR, "license.key");
2900
- CACHE_PATH = path8.join(EXE_AI_DIR, "license-cache.json");
2901
- DEVICE_ID_PATH = path8.join(EXE_AI_DIR, "device-id");
3015
+ LICENSE_PATH = path9.join(EXE_AI_DIR, "license.key");
3016
+ CACHE_PATH = path9.join(EXE_AI_DIR, "license-cache.json");
3017
+ DEVICE_ID_PATH = path9.join(EXE_AI_DIR, "device-id");
2902
3018
  PLAN_LIMITS = {
2903
3019
  free: { devices: 1, employees: 1, memories: 5e3 },
2904
3020
  pro: { devices: 3, employees: 5, memories: 1e5 },
@@ -2910,12 +3026,12 @@ var init_license = __esm({
2910
3026
  });
2911
3027
 
2912
3028
  // src/lib/plan-limits.ts
2913
- import { readFileSync as readFileSync8, existsSync as existsSync8 } from "fs";
2914
- import path9 from "path";
3029
+ import { readFileSync as readFileSync9, existsSync as existsSync10 } from "fs";
3030
+ import path10 from "path";
2915
3031
  function getLicenseSync() {
2916
3032
  try {
2917
- if (!existsSync8(CACHE_PATH2)) return freeLicense();
2918
- const raw = JSON.parse(readFileSync8(CACHE_PATH2, "utf8"));
3033
+ if (!existsSync10(CACHE_PATH2)) return freeLicense();
3034
+ const raw = JSON.parse(readFileSync9(CACHE_PATH2, "utf8"));
2919
3035
  if (!raw.token || typeof raw.token !== "string") return freeLicense();
2920
3036
  const parts = raw.token.split(".");
2921
3037
  if (parts.length !== 3) return freeLicense();
@@ -2953,8 +3069,8 @@ function assertEmployeeLimitSync(rosterPath) {
2953
3069
  const filePath = rosterPath ?? EMPLOYEES_PATH;
2954
3070
  let count = 0;
2955
3071
  try {
2956
- if (existsSync8(filePath)) {
2957
- const raw = readFileSync8(filePath, "utf8");
3072
+ if (existsSync10(filePath)) {
3073
+ const raw = readFileSync9(filePath, "utf8");
2958
3074
  const employees = JSON.parse(raw);
2959
3075
  count = Array.isArray(employees) ? employees.length : 0;
2960
3076
  }
@@ -2983,29 +3099,30 @@ var init_plan_limits = __esm({
2983
3099
  this.name = "PlanLimitError";
2984
3100
  }
2985
3101
  };
2986
- CACHE_PATH2 = path9.join(EXE_AI_DIR, "license-cache.json");
3102
+ CACHE_PATH2 = path10.join(EXE_AI_DIR, "license-cache.json");
2987
3103
  }
2988
3104
  });
2989
3105
 
2990
3106
  // src/lib/notifications.ts
2991
- import crypto from "crypto";
2992
- import path10 from "path";
2993
- import os7 from "os";
3107
+ import crypto2 from "crypto";
3108
+ import path11 from "path";
3109
+ import os8 from "os";
2994
3110
  import {
2995
- readFileSync as readFileSync9,
3111
+ readFileSync as readFileSync10,
2996
3112
  readdirSync,
2997
3113
  unlinkSync as unlinkSync3,
2998
- existsSync as existsSync9,
3114
+ existsSync as existsSync11,
2999
3115
  rmdirSync
3000
3116
  } from "fs";
3001
3117
  async function writeNotification(notification) {
3002
3118
  try {
3003
3119
  const client = getClient();
3004
- const id = crypto.randomUUID();
3120
+ const id = crypto2.randomUUID();
3005
3121
  const now = (/* @__PURE__ */ new Date()).toISOString();
3122
+ const sessionScope = notification.sessionScope === void 0 ? getCurrentSessionScope() : notification.sessionScope;
3006
3123
  await client.execute({
3007
- sql: `INSERT INTO notifications (id, agent_id, agent_role, event, project, summary, task_file, read, created_at)
3008
- VALUES (?, ?, ?, ?, ?, ?, ?, 0, ?)`,
3124
+ sql: `INSERT INTO notifications (id, agent_id, agent_role, event, project, summary, task_file, session_scope, read, created_at)
3125
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, 0, ?)`,
3009
3126
  args: [
3010
3127
  id,
3011
3128
  notification.agentId,
@@ -3014,6 +3131,7 @@ async function writeNotification(notification) {
3014
3131
  notification.project,
3015
3132
  notification.summary,
3016
3133
  notification.taskFile ?? null,
3134
+ sessionScope,
3017
3135
  now
3018
3136
  ]
3019
3137
  });
@@ -3022,12 +3140,14 @@ async function writeNotification(notification) {
3022
3140
  `);
3023
3141
  }
3024
3142
  }
3025
- async function markAsReadByTaskFile(taskFile) {
3143
+ async function markAsReadByTaskFile(taskFile, sessionScope) {
3026
3144
  try {
3027
3145
  const client = getClient();
3146
+ const scope = strictSessionScopeFilter(sessionScope);
3028
3147
  await client.execute({
3029
- sql: "UPDATE notifications SET read = 1 WHERE task_file = ? AND read = 0",
3030
- args: [taskFile]
3148
+ sql: `UPDATE notifications SET read = 1
3149
+ WHERE task_file = ? AND read = 0${scope.sql}`,
3150
+ args: [taskFile, ...scope.args]
3031
3151
  });
3032
3152
  } catch {
3033
3153
  }
@@ -3036,11 +3156,12 @@ var init_notifications = __esm({
3036
3156
  "src/lib/notifications.ts"() {
3037
3157
  "use strict";
3038
3158
  init_database();
3159
+ init_task_scope();
3039
3160
  }
3040
3161
  });
3041
3162
 
3042
3163
  // src/lib/session-kill-telemetry.ts
3043
- import crypto2 from "crypto";
3164
+ import crypto3 from "crypto";
3044
3165
  async function recordSessionKill(input) {
3045
3166
  try {
3046
3167
  const client = getClient();
@@ -3050,7 +3171,7 @@ async function recordSessionKill(input) {
3050
3171
  ticks_idle, estimated_tokens_saved)
3051
3172
  VALUES (?, ?, ?, ?, ?, ?, ?)`,
3052
3173
  args: [
3053
- crypto2.randomUUID(),
3174
+ crypto3.randomUUID(),
3054
3175
  input.sessionName,
3055
3176
  input.agentId,
3056
3177
  (/* @__PURE__ */ new Date()).toISOString(),
@@ -3145,12 +3266,12 @@ __export(tasks_crud_exports, {
3145
3266
  updateTaskStatus: () => updateTaskStatus,
3146
3267
  writeCheckpoint: () => writeCheckpoint
3147
3268
  });
3148
- import crypto3 from "crypto";
3149
- import path11 from "path";
3150
- import os8 from "os";
3269
+ import crypto4 from "crypto";
3270
+ import path12 from "path";
3271
+ import os9 from "os";
3151
3272
  import { execSync as execSync4 } from "child_process";
3152
3273
  import { mkdir as mkdir3, writeFile as writeFile3, appendFile } from "fs/promises";
3153
- import { existsSync as existsSync10, readFileSync as readFileSync10 } from "fs";
3274
+ import { existsSync as existsSync12, readFileSync as readFileSync11 } from "fs";
3154
3275
  async function writeCheckpoint(input) {
3155
3276
  const client = getClient();
3156
3277
  const row = await resolveTask(client, input.taskId);
@@ -3266,7 +3387,7 @@ async function resolveTask(client, identifier, scopeSession) {
3266
3387
  }
3267
3388
  async function createTaskCore(input) {
3268
3389
  const client = getClient();
3269
- const id = crypto3.randomUUID();
3390
+ const id = crypto4.randomUUID();
3270
3391
  const now = (/* @__PURE__ */ new Date()).toISOString();
3271
3392
  const slug = slugify(input.title);
3272
3393
  let earlySessionScope = null;
@@ -3325,8 +3446,8 @@ ${laneWarning}` : laneWarning;
3325
3446
  }
3326
3447
  if (input.baseDir) {
3327
3448
  try {
3328
- await mkdir3(path11.join(input.baseDir, "exe", "output"), { recursive: true });
3329
- await mkdir3(path11.join(input.baseDir, "exe", "research"), { recursive: true });
3449
+ await mkdir3(path12.join(input.baseDir, "exe", "output"), { recursive: true });
3450
+ await mkdir3(path12.join(input.baseDir, "exe", "research"), { recursive: true });
3330
3451
  await ensureArchitectureDoc(input.baseDir, input.projectName);
3331
3452
  await ensureGitignoreExe(input.baseDir);
3332
3453
  } catch {
@@ -3362,13 +3483,19 @@ ${laneWarning}` : laneWarning;
3362
3483
  });
3363
3484
  if (input.baseDir) {
3364
3485
  try {
3365
- const EXE_OS_DIR = path11.join(os8.homedir(), ".exe-os");
3366
- const mdPath = path11.join(EXE_OS_DIR, taskFile);
3367
- const mdDir = path11.dirname(mdPath);
3368
- if (!existsSync10(mdDir)) await mkdir3(mdDir, { recursive: true });
3486
+ const EXE_OS_DIR = path12.join(os9.homedir(), ".exe-os");
3487
+ const mdPath = path12.join(EXE_OS_DIR, taskFile);
3488
+ const mdDir = path12.dirname(mdPath);
3489
+ if (!existsSync12(mdDir)) await mkdir3(mdDir, { recursive: true });
3369
3490
  const reviewer = input.reviewer ?? input.assignedBy;
3370
3491
  const mdContent = `# ${input.title}
3371
3492
 
3493
+ ## MANDATORY: When done
3494
+
3495
+ You MUST call update_task with status "done" and a result summary when finished.
3496
+ If you skip this, your reviewer will not know you're done and your work won't be reviewed.
3497
+ Do NOT let a failed commit or any error prevent you from calling update_task(done).
3498
+
3372
3499
  **ID:** ${id}
3373
3500
  **Status:** ${initialStatus}
3374
3501
  **Priority:** ${input.priority}
@@ -3382,12 +3509,6 @@ ${laneWarning}` : laneWarning;
3382
3509
  ## Context
3383
3510
 
3384
3511
  ${input.context}
3385
-
3386
- ## MANDATORY: When done
3387
-
3388
- You MUST call update_task with status "done" and a result summary when finished.
3389
- If you skip this, your reviewer will not know you're done and your work won't be reviewed.
3390
- Do NOT let a failed commit or any error prevent you from calling update_task(done).
3391
3512
  `;
3392
3513
  await writeFile3(mdPath, mdContent, "utf-8");
3393
3514
  } catch (err) {
@@ -3636,7 +3757,7 @@ ${input.result}` : `\u26A0\uFE0F ${warning}`;
3636
3757
  await client.execute("PRAGMA wal_checkpoint(PASSIVE)");
3637
3758
  } catch {
3638
3759
  }
3639
- if (input.status === "done" || input.status === "cancelled") {
3760
+ if (input.status === "done" || input.status === "cancelled" || input.status === "closed") {
3640
3761
  try {
3641
3762
  const { clearQueueForAgent: clearQueueForAgent2 } = await Promise.resolve().then(() => (init_intercom_queue(), intercom_queue_exports));
3642
3763
  clearQueueForAgent2(String(row.assigned_to));
@@ -3665,9 +3786,9 @@ async function deleteTaskCore(taskId, _baseDir) {
3665
3786
  return { taskFile, assignedTo, assignedBy, taskSlug };
3666
3787
  }
3667
3788
  async function ensureArchitectureDoc(baseDir, projectName) {
3668
- const archPath = path11.join(baseDir, "exe", "ARCHITECTURE.md");
3789
+ const archPath = path12.join(baseDir, "exe", "ARCHITECTURE.md");
3669
3790
  try {
3670
- if (existsSync10(archPath)) return;
3791
+ if (existsSync12(archPath)) return;
3671
3792
  const template = [
3672
3793
  `# ${projectName} \u2014 System Architecture`,
3673
3794
  "",
@@ -3700,10 +3821,10 @@ async function ensureArchitectureDoc(baseDir, projectName) {
3700
3821
  }
3701
3822
  }
3702
3823
  async function ensureGitignoreExe(baseDir) {
3703
- const gitignorePath = path11.join(baseDir, ".gitignore");
3824
+ const gitignorePath = path12.join(baseDir, ".gitignore");
3704
3825
  try {
3705
- if (existsSync10(gitignorePath)) {
3706
- const content = readFileSync10(gitignorePath, "utf-8");
3826
+ if (existsSync12(gitignorePath)) {
3827
+ const content = readFileSync11(gitignorePath, "utf-8");
3707
3828
  if (/^\/?exe\/?$/m.test(content)) return;
3708
3829
  await appendFile(gitignorePath, "\n# Employee task assignments (private)\n/exe/\n");
3709
3830
  } else {
@@ -3734,58 +3855,42 @@ var init_tasks_crud = __esm({
3734
3855
  });
3735
3856
 
3736
3857
  // src/lib/tasks-review.ts
3737
- import path12 from "path";
3738
- import { existsSync as existsSync11, readdirSync as readdirSync2, unlinkSync as unlinkSync4 } from "fs";
3858
+ import path13 from "path";
3859
+ import { existsSync as existsSync13, readdirSync as readdirSync2, unlinkSync as unlinkSync4 } from "fs";
3739
3860
  async function countPendingReviews(sessionScope) {
3740
3861
  const client = getClient();
3741
- if (sessionScope) {
3742
- const result2 = await client.execute({
3743
- sql: "SELECT COUNT(*) as cnt FROM tasks WHERE status = 'needs_review' AND session_scope = ?",
3744
- args: [sessionScope]
3745
- });
3746
- return Number(result2.rows[0]?.cnt) || 0;
3747
- }
3862
+ const scope = strictSessionScopeFilter(
3863
+ sessionScope === void 0 ? getCurrentSessionScope() : sessionScope
3864
+ );
3748
3865
  const result = await client.execute({
3749
- sql: "SELECT COUNT(*) as cnt FROM tasks WHERE status = 'needs_review'",
3750
- args: []
3866
+ sql: `SELECT COUNT(*) as cnt FROM tasks
3867
+ WHERE status = 'needs_review'${scope.sql}`,
3868
+ args: [...scope.args]
3751
3869
  });
3752
3870
  return Number(result.rows[0]?.cnt) || 0;
3753
3871
  }
3754
3872
  async function countNewPendingReviewsSince(sinceIso, sessionScope) {
3755
3873
  const client = getClient();
3756
- if (sessionScope) {
3757
- const result2 = await client.execute({
3758
- sql: `SELECT COUNT(*) as cnt FROM tasks
3759
- WHERE status = 'needs_review' AND updated_at > ?
3760
- AND session_scope = ?`,
3761
- args: [sinceIso, sessionScope]
3762
- });
3763
- return Number(result2.rows[0]?.cnt) || 0;
3764
- }
3874
+ const scope = strictSessionScopeFilter(
3875
+ sessionScope === void 0 ? getCurrentSessionScope() : sessionScope
3876
+ );
3765
3877
  const result = await client.execute({
3766
3878
  sql: `SELECT COUNT(*) as cnt FROM tasks
3767
- WHERE status = 'needs_review' AND updated_at > ?`,
3768
- args: [sinceIso]
3879
+ WHERE status = 'needs_review' AND updated_at > ?${scope.sql}`,
3880
+ args: [sinceIso, ...scope.args]
3769
3881
  });
3770
3882
  return Number(result.rows[0]?.cnt) || 0;
3771
3883
  }
3772
3884
  async function listPendingReviews(limit, sessionScope) {
3773
3885
  const client = getClient();
3774
- if (sessionScope) {
3775
- const result2 = await client.execute({
3776
- sql: `SELECT title, assigned_to, project_name, updated_at FROM tasks
3777
- WHERE status = 'needs_review'
3778
- AND session_scope = ?
3779
- ORDER BY updated_at ASC LIMIT ?`,
3780
- args: [sessionScope, limit]
3781
- });
3782
- return result2.rows;
3783
- }
3886
+ const scope = strictSessionScopeFilter(
3887
+ sessionScope === void 0 ? getCurrentSessionScope() : sessionScope
3888
+ );
3784
3889
  const result = await client.execute({
3785
3890
  sql: `SELECT title, assigned_to, project_name, updated_at FROM tasks
3786
- WHERE status = 'needs_review'
3891
+ WHERE status = 'needs_review'${scope.sql}
3787
3892
  ORDER BY updated_at ASC LIMIT ?`,
3788
- args: [limit]
3893
+ args: [...scope.args, limit]
3789
3894
  });
3790
3895
  return result.rows;
3791
3896
  }
@@ -3797,7 +3902,7 @@ async function cleanupOrphanedReviews() {
3797
3902
  WHERE status IN ('open', 'needs_review', 'in_progress')
3798
3903
  AND assigned_by = 'system'
3799
3904
  AND title LIKE 'Review:%'
3800
- AND parent_task_id IN (SELECT id FROM tasks WHERE status IN ('done', 'cancelled'))`,
3905
+ AND parent_task_id IN (SELECT id FROM tasks WHERE status IN ('done', 'cancelled', 'closed'))`,
3801
3906
  args: [now]
3802
3907
  });
3803
3908
  const r1b = await client.execute({
@@ -3916,11 +4021,11 @@ async function cleanupReviewFile(row, taskFile, _baseDir) {
3916
4021
  );
3917
4022
  }
3918
4023
  try {
3919
- const cacheDir = path12.join(EXE_AI_DIR, "session-cache");
3920
- if (existsSync11(cacheDir)) {
4024
+ const cacheDir = path13.join(EXE_AI_DIR, "session-cache");
4025
+ if (existsSync13(cacheDir)) {
3921
4026
  for (const f of readdirSync2(cacheDir)) {
3922
4027
  if (f.startsWith("review-notified-")) {
3923
- unlinkSync4(path12.join(cacheDir, f));
4028
+ unlinkSync4(path13.join(cacheDir, f));
3924
4029
  }
3925
4030
  }
3926
4031
  }
@@ -3937,11 +4042,12 @@ var init_tasks_review = __esm({
3937
4042
  init_tmux_routing();
3938
4043
  init_session_key();
3939
4044
  init_state_bus();
4045
+ init_task_scope();
3940
4046
  }
3941
4047
  });
3942
4048
 
3943
4049
  // src/lib/tasks-chain.ts
3944
- import path13 from "path";
4050
+ import path14 from "path";
3945
4051
  import { readFile as readFile3, writeFile as writeFile4 } from "fs/promises";
3946
4052
  async function cascadeUnblock(taskId, baseDir, now) {
3947
4053
  const client = getClient();
@@ -3958,7 +4064,7 @@ async function cascadeUnblock(taskId, baseDir, now) {
3958
4064
  });
3959
4065
  for (const ur of unblockedRows.rows) {
3960
4066
  try {
3961
- const ubFile = path13.join(baseDir, String(ur.task_file));
4067
+ const ubFile = path14.join(baseDir, String(ur.task_file));
3962
4068
  let ubContent = await readFile3(ubFile, "utf-8");
3963
4069
  ubContent = ubContent.replace(/\*\*Status:\*\* blocked/, "**Status:** open");
3964
4070
  ubContent = ubContent.replace(/\n\*\*Blocked by:\*\*.*\n/, "\n");
@@ -3993,7 +4099,7 @@ async function checkSubtaskCompletion(parentTaskId, projectName) {
3993
4099
  const scScope = sessionScopeFilter();
3994
4100
  const remaining = await client.execute({
3995
4101
  sql: `SELECT COUNT(*) as cnt FROM tasks
3996
- WHERE parent_task_id = ? AND status NOT IN ('done', 'cancelled')${scScope.sql}`,
4102
+ WHERE parent_task_id = ? AND status NOT IN ('done', 'cancelled', 'closed')${scScope.sql}`,
3997
4103
  args: [parentTaskId, ...scScope.args]
3998
4104
  });
3999
4105
  const cnt = Number(remaining.rows[0]?.cnt ?? 1);
@@ -4027,7 +4133,7 @@ var init_tasks_chain = __esm({
4027
4133
 
4028
4134
  // src/lib/project-name.ts
4029
4135
  import { execSync as execSync5 } from "child_process";
4030
- import path14 from "path";
4136
+ import path15 from "path";
4031
4137
  function getProjectName(cwd) {
4032
4138
  const dir = cwd ?? process.cwd();
4033
4139
  if (_cached2 && _cachedCwd === dir) return _cached2;
@@ -4040,7 +4146,7 @@ function getProjectName(cwd) {
4040
4146
  timeout: 2e3,
4041
4147
  stdio: ["pipe", "pipe", "pipe"]
4042
4148
  }).trim();
4043
- repoRoot = path14.dirname(gitCommonDir);
4149
+ repoRoot = path15.dirname(gitCommonDir);
4044
4150
  } catch {
4045
4151
  repoRoot = execSync5("git rev-parse --show-toplevel", {
4046
4152
  cwd: dir,
@@ -4049,11 +4155,11 @@ function getProjectName(cwd) {
4049
4155
  stdio: ["pipe", "pipe", "pipe"]
4050
4156
  }).trim();
4051
4157
  }
4052
- _cached2 = path14.basename(repoRoot);
4158
+ _cached2 = path15.basename(repoRoot);
4053
4159
  _cachedCwd = dir;
4054
4160
  return _cached2;
4055
4161
  } catch {
4056
- _cached2 = path14.basename(dir);
4162
+ _cached2 = path15.basename(dir);
4057
4163
  _cachedCwd = dir;
4058
4164
  return _cached2;
4059
4165
  }
@@ -4196,10 +4302,10 @@ var init_tasks_notify = __esm({
4196
4302
  });
4197
4303
 
4198
4304
  // src/lib/behaviors.ts
4199
- import crypto4 from "crypto";
4305
+ import crypto5 from "crypto";
4200
4306
  async function storeBehavior(opts) {
4201
4307
  const client = getClient();
4202
- const id = crypto4.randomUUID();
4308
+ const id = crypto5.randomUUID();
4203
4309
  const now = (/* @__PURE__ */ new Date()).toISOString();
4204
4310
  await client.execute({
4205
4311
  sql: `INSERT INTO behaviors (id, agent_id, project_name, domain, priority, content, active, created_at, updated_at)
@@ -4228,7 +4334,7 @@ __export(skill_learning_exports, {
4228
4334
  storeTrajectory: () => storeTrajectory,
4229
4335
  sweepTrajectories: () => sweepTrajectories
4230
4336
  });
4231
- import crypto5 from "crypto";
4337
+ import crypto6 from "crypto";
4232
4338
  async function extractTrajectory(taskId, agentId) {
4233
4339
  const client = getClient();
4234
4340
  const result = await client.execute({
@@ -4257,11 +4363,11 @@ async function extractTrajectory(taskId, agentId) {
4257
4363
  return signature;
4258
4364
  }
4259
4365
  function hashSignature(signature) {
4260
- return crypto5.createHash("sha256").update(signature.join("|")).digest("hex").slice(0, 16);
4366
+ return crypto6.createHash("sha256").update(signature.join("|")).digest("hex").slice(0, 16);
4261
4367
  }
4262
4368
  async function storeTrajectory(opts) {
4263
4369
  const client = getClient();
4264
- const id = crypto5.randomUUID();
4370
+ const id = crypto6.randomUUID();
4265
4371
  const now = (/* @__PURE__ */ new Date()).toISOString();
4266
4372
  const signatureHash = hashSignature(opts.signature);
4267
4373
  await client.execute({
@@ -4526,8 +4632,8 @@ __export(tasks_exports, {
4526
4632
  updateTaskStatus: () => updateTaskStatus,
4527
4633
  writeCheckpoint: () => writeCheckpoint
4528
4634
  });
4529
- import path15 from "path";
4530
- import { writeFileSync as writeFileSync6, mkdirSync as mkdirSync5, unlinkSync as unlinkSync5 } from "fs";
4635
+ import path16 from "path";
4636
+ import { writeFileSync as writeFileSync7, mkdirSync as mkdirSync5, unlinkSync as unlinkSync5 } from "fs";
4531
4637
  async function createTask(input) {
4532
4638
  const result = await createTaskCore(input);
4533
4639
  if (!input.skipDispatch && result.status !== "blocked" && !process.env.VITEST) {
@@ -4546,12 +4652,12 @@ async function updateTask(input) {
4546
4652
  const { row, taskFile, now, taskId } = await updateTaskStatus(input);
4547
4653
  try {
4548
4654
  const agent = String(row.assigned_to);
4549
- const cacheDir = path15.join(EXE_AI_DIR, "session-cache");
4550
- const cachePath = path15.join(cacheDir, `current-task-${agent}.json`);
4655
+ const cacheDir = path16.join(EXE_AI_DIR, "session-cache");
4656
+ const cachePath = path16.join(cacheDir, `current-task-${agent}.json`);
4551
4657
  if (input.status === "in_progress") {
4552
4658
  mkdirSync5(cacheDir, { recursive: true });
4553
- writeFileSync6(cachePath, JSON.stringify({ taskId, title: String(row.title) }));
4554
- } else if (input.status === "done" || input.status === "blocked" || input.status === "cancelled") {
4659
+ writeFileSync7(cachePath, JSON.stringify({ taskId, title: String(row.title) }));
4660
+ } else if (input.status === "done" || input.status === "blocked" || input.status === "cancelled" || input.status === "closed") {
4555
4661
  try {
4556
4662
  unlinkSync5(cachePath);
4557
4663
  } catch {
@@ -4559,10 +4665,10 @@ async function updateTask(input) {
4559
4665
  }
4560
4666
  } catch {
4561
4667
  }
4562
- if (input.status === "done") {
4668
+ if (input.status === "done" || input.status === "closed") {
4563
4669
  await cleanupReviewFile(row, taskFile, input.baseDir);
4564
4670
  }
4565
- if (input.status === "done" || input.status === "cancelled") {
4671
+ if (input.status === "done" || input.status === "cancelled" || input.status === "closed") {
4566
4672
  try {
4567
4673
  const client = getClient();
4568
4674
  const taskTitle = String(row.title);
@@ -4578,7 +4684,7 @@ async function updateTask(input) {
4578
4684
  if (!isCoordinatorName(assignedAgent)) {
4579
4685
  try {
4580
4686
  const draftClient = getClient();
4581
- if (input.status === "done") {
4687
+ if (input.status === "done" || input.status === "closed") {
4582
4688
  await draftClient.execute({
4583
4689
  sql: `UPDATE memories SET draft = 0 WHERE agent_id = ? AND draft = 1`,
4584
4690
  args: [assignedAgent]
@@ -4595,7 +4701,7 @@ async function updateTask(input) {
4595
4701
  try {
4596
4702
  const client = getClient();
4597
4703
  const cascaded = await client.execute({
4598
- sql: `UPDATE tasks SET status = 'done', updated_at = ?
4704
+ sql: `UPDATE tasks SET status = 'closed', updated_at = ?
4599
4705
  WHERE parent_task_id = ? AND status = 'needs_review'`,
4600
4706
  args: [now, taskId]
4601
4707
  });
@@ -4608,14 +4714,14 @@ async function updateTask(input) {
4608
4714
  } catch {
4609
4715
  }
4610
4716
  }
4611
- const isTerminal = input.status === "done" || input.status === "needs_review";
4717
+ const isTerminal = input.status === "done" || input.status === "needs_review" || input.status === "closed";
4612
4718
  if (isTerminal) {
4613
4719
  const isCoordinator = isCoordinatorName(String(row.assigned_to));
4614
4720
  if (!isCoordinator) {
4615
4721
  notifyTaskDone();
4616
4722
  }
4617
4723
  await markTaskNotificationsRead(taskFile);
4618
- if (input.status === "done") {
4724
+ if (input.status === "done" || input.status === "closed") {
4619
4725
  try {
4620
4726
  await cascadeUnblock(taskId, input.baseDir, now);
4621
4727
  } catch {
@@ -4635,7 +4741,7 @@ async function updateTask(input) {
4635
4741
  }
4636
4742
  }
4637
4743
  }
4638
- if (input.status === "done" && !isCoordinatorName(String(row.assigned_to)) && !process.env.VITEST) {
4744
+ if ((input.status === "done" || input.status === "closed") && !isCoordinatorName(String(row.assigned_to)) && !process.env.VITEST) {
4639
4745
  Promise.resolve().then(() => (init_skill_learning(), skill_learning_exports)).then(
4640
4746
  ({ captureAndLearn: captureAndLearn2 }) => captureAndLearn2({
4641
4747
  taskId,
@@ -5007,6 +5113,7 @@ __export(tmux_routing_exports, {
5007
5113
  isEmployeeAlive: () => isEmployeeAlive,
5008
5114
  isExeSession: () => isExeSession,
5009
5115
  isSessionBusy: () => isSessionBusy,
5116
+ notifyCoordinatorTaskCompletion: () => notifyCoordinatorTaskCompletion,
5010
5117
  notifyParentExe: () => notifyParentExe,
5011
5118
  parseParentExe: () => parseParentExe,
5012
5119
  registerParentExe: () => registerParentExe,
@@ -5017,13 +5124,13 @@ __export(tmux_routing_exports, {
5017
5124
  verifyPaneAtCapacity: () => verifyPaneAtCapacity
5018
5125
  });
5019
5126
  import { execFileSync as execFileSync2, execSync as execSync6 } from "child_process";
5020
- import { readFileSync as readFileSync11, writeFileSync as writeFileSync7, mkdirSync as mkdirSync6, existsSync as existsSync12, appendFileSync, readdirSync as readdirSync3 } from "fs";
5021
- import path16 from "path";
5022
- import os9 from "os";
5127
+ import { readFileSync as readFileSync12, writeFileSync as writeFileSync8, mkdirSync as mkdirSync6, existsSync as existsSync14, appendFileSync, readdirSync as readdirSync3 } from "fs";
5128
+ import path17 from "path";
5129
+ import os10 from "os";
5023
5130
  import { fileURLToPath as fileURLToPath2 } from "url";
5024
5131
  import { unlinkSync as unlinkSync6 } from "fs";
5025
5132
  function spawnLockPath(sessionName) {
5026
- return path16.join(SPAWN_LOCK_DIR, `${sessionName}.lock`);
5133
+ return path17.join(SPAWN_LOCK_DIR, `${sessionName}.lock`);
5027
5134
  }
5028
5135
  function isProcessAlive(pid) {
5029
5136
  try {
@@ -5034,13 +5141,13 @@ function isProcessAlive(pid) {
5034
5141
  }
5035
5142
  }
5036
5143
  function acquireSpawnLock2(sessionName) {
5037
- if (!existsSync12(SPAWN_LOCK_DIR)) {
5144
+ if (!existsSync14(SPAWN_LOCK_DIR)) {
5038
5145
  mkdirSync6(SPAWN_LOCK_DIR, { recursive: true });
5039
5146
  }
5040
5147
  const lockFile = spawnLockPath(sessionName);
5041
- if (existsSync12(lockFile)) {
5148
+ if (existsSync14(lockFile)) {
5042
5149
  try {
5043
- const lock = JSON.parse(readFileSync11(lockFile, "utf8"));
5150
+ const lock = JSON.parse(readFileSync12(lockFile, "utf8"));
5044
5151
  const age = Date.now() - lock.timestamp;
5045
5152
  if (isProcessAlive(lock.pid) && age < 6e4) {
5046
5153
  return false;
@@ -5048,7 +5155,7 @@ function acquireSpawnLock2(sessionName) {
5048
5155
  } catch {
5049
5156
  }
5050
5157
  }
5051
- writeFileSync7(lockFile, JSON.stringify({ pid: process.pid, timestamp: Date.now() }));
5158
+ writeFileSync8(lockFile, JSON.stringify({ pid: process.pid, timestamp: Date.now() }));
5052
5159
  return true;
5053
5160
  }
5054
5161
  function releaseSpawnLock2(sessionName) {
@@ -5060,13 +5167,13 @@ function releaseSpawnLock2(sessionName) {
5060
5167
  function resolveBehaviorsExporterScript() {
5061
5168
  try {
5062
5169
  const thisFile = fileURLToPath2(import.meta.url);
5063
- const scriptPath = path16.join(
5064
- path16.dirname(thisFile),
5170
+ const scriptPath = path17.join(
5171
+ path17.dirname(thisFile),
5065
5172
  "..",
5066
5173
  "bin",
5067
5174
  "exe-export-behaviors.js"
5068
5175
  );
5069
- return existsSync12(scriptPath) ? scriptPath : null;
5176
+ return existsSync14(scriptPath) ? scriptPath : null;
5070
5177
  } catch {
5071
5178
  return null;
5072
5179
  }
@@ -5132,12 +5239,12 @@ function extractRootExe(name) {
5132
5239
  return parts.length > 0 ? parts[parts.length - 1] : null;
5133
5240
  }
5134
5241
  function registerParentExe(sessionKey, parentExe, dispatchedBy) {
5135
- if (!existsSync12(SESSION_CACHE)) {
5242
+ if (!existsSync14(SESSION_CACHE)) {
5136
5243
  mkdirSync6(SESSION_CACHE, { recursive: true });
5137
5244
  }
5138
5245
  const rootExe = extractRootExe(parentExe) ?? parentExe;
5139
- const filePath = path16.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`);
5140
- writeFileSync7(filePath, JSON.stringify({
5246
+ const filePath = path17.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`);
5247
+ writeFileSync8(filePath, JSON.stringify({
5141
5248
  parentExe: rootExe,
5142
5249
  dispatchedBy: dispatchedBy || rootExe,
5143
5250
  registeredAt: (/* @__PURE__ */ new Date()).toISOString()
@@ -5145,7 +5252,7 @@ function registerParentExe(sessionKey, parentExe, dispatchedBy) {
5145
5252
  }
5146
5253
  function getParentExe(sessionKey) {
5147
5254
  try {
5148
- const data = JSON.parse(readFileSync11(path16.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`), "utf8"));
5255
+ const data = JSON.parse(readFileSync12(path17.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`), "utf8"));
5149
5256
  return data.parentExe || null;
5150
5257
  } catch {
5151
5258
  return null;
@@ -5153,8 +5260,8 @@ function getParentExe(sessionKey) {
5153
5260
  }
5154
5261
  function getDispatchedBy(sessionKey) {
5155
5262
  try {
5156
- const data = JSON.parse(readFileSync11(
5157
- path16.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`),
5263
+ const data = JSON.parse(readFileSync12(
5264
+ path17.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`),
5158
5265
  "utf8"
5159
5266
  ));
5160
5267
  return data.dispatchedBy ?? data.parentExe ?? null;
@@ -5224,8 +5331,8 @@ async function verifyPaneAtCapacity(sessionName) {
5224
5331
  }
5225
5332
  function readDebounceState() {
5226
5333
  try {
5227
- if (!existsSync12(DEBOUNCE_FILE)) return {};
5228
- const raw = JSON.parse(readFileSync11(DEBOUNCE_FILE, "utf8"));
5334
+ if (!existsSync14(DEBOUNCE_FILE)) return {};
5335
+ const raw = JSON.parse(readFileSync12(DEBOUNCE_FILE, "utf8"));
5229
5336
  const state = {};
5230
5337
  for (const [key, val] of Object.entries(raw)) {
5231
5338
  if (typeof val === "number") {
@@ -5241,8 +5348,8 @@ function readDebounceState() {
5241
5348
  }
5242
5349
  function writeDebounceState(state) {
5243
5350
  try {
5244
- if (!existsSync12(SESSION_CACHE)) mkdirSync6(SESSION_CACHE, { recursive: true });
5245
- writeFileSync7(DEBOUNCE_FILE, JSON.stringify(state));
5351
+ if (!existsSync14(SESSION_CACHE)) mkdirSync6(SESSION_CACHE, { recursive: true });
5352
+ writeFileSync8(DEBOUNCE_FILE, JSON.stringify(state));
5246
5353
  } catch {
5247
5354
  }
5248
5355
  }
@@ -5340,8 +5447,8 @@ function sendIntercom(targetSession) {
5340
5447
  try {
5341
5448
  const rawAgent = targetSession.split("-")[0] ?? targetSession;
5342
5449
  const agent = baseAgentName(rawAgent);
5343
- const markerPath = path16.join(SESSION_CACHE, `current-task-${agent}.json`);
5344
- if (existsSync12(markerPath)) {
5450
+ const markerPath = path17.join(SESSION_CACHE, `current-task-${agent}.json`);
5451
+ if (existsSync14(markerPath)) {
5345
5452
  logIntercom(`SKIP \u2192 ${targetSession} (has in_progress task marker \u2014 will auto-chain)`);
5346
5453
  return "debounced";
5347
5454
  }
@@ -5350,8 +5457,8 @@ function sendIntercom(targetSession) {
5350
5457
  try {
5351
5458
  const rawAgent = targetSession.split("-")[0] ?? targetSession;
5352
5459
  const agent = baseAgentName(rawAgent);
5353
- const taskDir = path16.join(process.cwd(), "exe", agent);
5354
- if (existsSync12(taskDir)) {
5460
+ const taskDir = path17.join(process.cwd(), "exe", agent);
5461
+ if (existsSync14(taskDir)) {
5355
5462
  const files = readdirSync3(taskDir).filter(
5356
5463
  (f) => f.endsWith(".md") && f !== "DONE.txt"
5357
5464
  );
@@ -5411,6 +5518,21 @@ function notifyParentExe(sessionKey) {
5411
5518
  }
5412
5519
  return true;
5413
5520
  }
5521
+ function notifyCoordinatorTaskCompletion(coordinatorSession, agentName, taskTitle) {
5522
+ const transport = getTransport();
5523
+ try {
5524
+ const sessions = transport.listSessions();
5525
+ if (!sessions.includes(coordinatorSession)) return false;
5526
+ execSync6(
5527
+ `tmux send-keys -t ${JSON.stringify(coordinatorSession)} '/exe-intercom' Enter`,
5528
+ { timeout: 3e3 }
5529
+ );
5530
+ logIntercom(`COMPLETION \u2192 ${coordinatorSession} (${agentName} completed "${taskTitle.slice(0, 50)}")`);
5531
+ return true;
5532
+ } catch {
5533
+ return false;
5534
+ }
5535
+ }
5414
5536
  function ensureEmployee(employeeName, exeSession, projectDir, opts) {
5415
5537
  if (isCoordinatorName(employeeName)) {
5416
5538
  return { status: "failed", sessionName: "", error: "The COO is not a dispatchable employee" };
@@ -5484,26 +5606,26 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
5484
5606
  const transport = getTransport();
5485
5607
  const sessionName = employeeSessionName(employeeName, exeSession, opts?.instance);
5486
5608
  const instanceLabel = opts?.instance != null && opts.instance > 0 ? `${employeeName}${opts.instance}` : employeeName;
5487
- const logDir = path16.join(os9.homedir(), ".exe-os", "session-logs");
5488
- const logFile = path16.join(logDir, `${instanceLabel}-${Date.now()}.log`);
5489
- if (!existsSync12(logDir)) {
5609
+ const logDir = path17.join(os10.homedir(), ".exe-os", "session-logs");
5610
+ const logFile = path17.join(logDir, `${instanceLabel}-${Date.now()}.log`);
5611
+ if (!existsSync14(logDir)) {
5490
5612
  mkdirSync6(logDir, { recursive: true });
5491
5613
  }
5492
5614
  transport.kill(sessionName);
5493
5615
  let cleanupSuffix = "";
5494
5616
  try {
5495
5617
  const thisFile = fileURLToPath2(import.meta.url);
5496
- const cleanupScript = path16.join(path16.dirname(thisFile), "..", "bin", "exe-session-cleanup.js");
5497
- if (existsSync12(cleanupScript)) {
5618
+ const cleanupScript = path17.join(path17.dirname(thisFile), "..", "bin", "exe-session-cleanup.js");
5619
+ if (existsSync14(cleanupScript)) {
5498
5620
  cleanupSuffix = `; ${process.execPath} "${cleanupScript}" "${employeeName}" "${exeSession}"`;
5499
5621
  }
5500
5622
  } catch {
5501
5623
  }
5502
5624
  try {
5503
- const claudeJsonPath = path16.join(os9.homedir(), ".claude.json");
5625
+ const claudeJsonPath = path17.join(os10.homedir(), ".claude.json");
5504
5626
  let claudeJson = {};
5505
5627
  try {
5506
- claudeJson = JSON.parse(readFileSync11(claudeJsonPath, "utf8"));
5628
+ claudeJson = JSON.parse(readFileSync12(claudeJsonPath, "utf8"));
5507
5629
  } catch {
5508
5630
  }
5509
5631
  if (!claudeJson.projects) claudeJson.projects = {};
@@ -5511,17 +5633,17 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
5511
5633
  const trustDir = opts?.cwd ?? projectDir;
5512
5634
  if (!projects[trustDir]) projects[trustDir] = {};
5513
5635
  projects[trustDir].hasTrustDialogAccepted = true;
5514
- writeFileSync7(claudeJsonPath, JSON.stringify(claudeJson, null, 2) + "\n");
5636
+ writeFileSync8(claudeJsonPath, JSON.stringify(claudeJson, null, 2) + "\n");
5515
5637
  } catch {
5516
5638
  }
5517
5639
  try {
5518
- const settingsDir = path16.join(os9.homedir(), ".claude", "projects");
5640
+ const settingsDir = path17.join(os10.homedir(), ".claude", "projects");
5519
5641
  const normalizedKey = (opts?.cwd ?? projectDir).replace(/\//g, "-").replace(/^-/, "");
5520
- const projSettingsDir = path16.join(settingsDir, normalizedKey);
5521
- const settingsPath = path16.join(projSettingsDir, "settings.json");
5642
+ const projSettingsDir = path17.join(settingsDir, normalizedKey);
5643
+ const settingsPath = path17.join(projSettingsDir, "settings.json");
5522
5644
  let settings = {};
5523
5645
  try {
5524
- settings = JSON.parse(readFileSync11(settingsPath, "utf8"));
5646
+ settings = JSON.parse(readFileSync12(settingsPath, "utf8"));
5525
5647
  } catch {
5526
5648
  }
5527
5649
  const perms = settings.permissions ?? {};
@@ -5550,7 +5672,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
5550
5672
  perms.allow = allow;
5551
5673
  settings.permissions = perms;
5552
5674
  mkdirSync6(projSettingsDir, { recursive: true });
5553
- writeFileSync7(settingsPath, JSON.stringify(settings, null, 2) + "\n");
5675
+ writeFileSync8(settingsPath, JSON.stringify(settings, null, 2) + "\n");
5554
5676
  }
5555
5677
  } catch {
5556
5678
  }
@@ -5565,8 +5687,8 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
5565
5687
  let behaviorsFlag = "";
5566
5688
  let legacyFallbackWarned = false;
5567
5689
  if (!useExeAgent && !useBinSymlink) {
5568
- const identityPath = path16.join(
5569
- os9.homedir(),
5690
+ const identityPath = path17.join(
5691
+ os10.homedir(),
5570
5692
  ".exe-os",
5571
5693
  "identity",
5572
5694
  `${employeeName}.md`
@@ -5575,13 +5697,13 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
5575
5697
  const hasAgentFlag = claudeSupportsAgentFlag();
5576
5698
  if (hasAgentFlag) {
5577
5699
  identityFlag = ` --agent ${employeeName}`;
5578
- } else if (existsSync12(identityPath)) {
5700
+ } else if (existsSync14(identityPath)) {
5579
5701
  identityFlag = ` --append-system-prompt-file ${identityPath}`;
5580
5702
  legacyFallbackWarned = true;
5581
5703
  }
5582
5704
  const behaviorsFile = exportBehaviorsSync(
5583
5705
  employeeName,
5584
- path16.basename(spawnCwd),
5706
+ path17.basename(spawnCwd),
5585
5707
  sessionName
5586
5708
  );
5587
5709
  if (behaviorsFile) {
@@ -5596,16 +5718,16 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
5596
5718
  }
5597
5719
  let sessionContextFlag = "";
5598
5720
  try {
5599
- const ctxDir = path16.join(os9.homedir(), ".exe-os", "session-cache");
5721
+ const ctxDir = path17.join(os10.homedir(), ".exe-os", "session-cache");
5600
5722
  mkdirSync6(ctxDir, { recursive: true });
5601
- const ctxFile = path16.join(ctxDir, `session-context-${sessionName}.md`);
5723
+ const ctxFile = path17.join(ctxDir, `session-context-${sessionName}.md`);
5602
5724
  const ctxContent = [
5603
5725
  `## Session Context`,
5604
5726
  `You are running in tmux session: ${sessionName}.`,
5605
5727
  `Your parent coordinator session is ${exeSession}.`,
5606
5728
  `Your employees (if any) use the -${exeSession} suffix.`
5607
5729
  ].join("\n");
5608
- writeFileSync7(ctxFile, ctxContent);
5730
+ writeFileSync8(ctxFile, ctxContent);
5609
5731
  sessionContextFlag = ` --append-system-prompt-file ${ctxFile}`;
5610
5732
  } catch {
5611
5733
  }
@@ -5682,8 +5804,8 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
5682
5804
  transport.pipeLog(sessionName, logFile);
5683
5805
  try {
5684
5806
  const mySession = getMySession();
5685
- const dispatchInfo = path16.join(SESSION_CACHE, `dispatch-info-${sessionName}.json`);
5686
- writeFileSync7(dispatchInfo, JSON.stringify({
5807
+ const dispatchInfo = path17.join(SESSION_CACHE, `dispatch-info-${sessionName}.json`);
5808
+ writeFileSync8(dispatchInfo, JSON.stringify({
5687
5809
  dispatchedBy: mySession,
5688
5810
  rootExe: exeSession,
5689
5811
  provider: useBinSymlink ? ccProvider : useExeAgent ? opts.provider : useCodex ? "openai" : useOpencode ? "opencode" : "anthropic",
@@ -5757,15 +5879,15 @@ var init_tmux_routing = __esm({
5757
5879
  init_intercom_queue();
5758
5880
  init_plan_limits();
5759
5881
  init_employees();
5760
- SPAWN_LOCK_DIR = path16.join(os9.homedir(), ".exe-os", "spawn-locks");
5761
- SESSION_CACHE = path16.join(os9.homedir(), ".exe-os", "session-cache");
5882
+ SPAWN_LOCK_DIR = path17.join(os10.homedir(), ".exe-os", "spawn-locks");
5883
+ SESSION_CACHE = path17.join(os10.homedir(), ".exe-os", "session-cache");
5762
5884
  BEHAVIORS_EXPORT_TIMEOUT_MS = 1e4;
5763
5885
  VALID_SESSION_NAME = /^[a-z]+\d*-[a-zA-Z0-9_]+$/;
5764
5886
  VERIFY_PANE_LINES = 200;
5765
5887
  INTERCOM_DEBOUNCE_MS = 3e4;
5766
5888
  CODEX_DEBOUNCE_MS = 12e4;
5767
- INTERCOM_LOG2 = path16.join(os9.homedir(), ".exe-os", "intercom.log");
5768
- DEBOUNCE_FILE = path16.join(SESSION_CACHE, "intercom-debounce.json");
5889
+ INTERCOM_LOG2 = path17.join(os10.homedir(), ".exe-os", "intercom.log");
5890
+ DEBOUNCE_FILE = path17.join(SESSION_CACHE, "intercom-debounce.json");
5769
5891
  DEBOUNCE_CLEANUP_AGE_MS = 5 * 60 * 1e3;
5770
5892
  BUSY_PATTERN = /[✻✽✶✳·].*…|Running…|• Working|• Ran |• Explored|• Called|esc to interrupt/;
5771
5893
  }
@@ -5788,6 +5910,15 @@ function sessionScopeFilter(sessionScope, tableAlias) {
5788
5910
  args: [scope]
5789
5911
  };
5790
5912
  }
5913
+ function strictSessionScopeFilter(sessionScope, tableAlias) {
5914
+ const scope = sessionScope !== void 0 ? sessionScope : getCurrentSessionScope();
5915
+ if (!scope) return { sql: "", args: [] };
5916
+ const col = tableAlias ? `${tableAlias}.session_scope` : "session_scope";
5917
+ return {
5918
+ sql: ` AND ${col} = ?`,
5919
+ args: [scope]
5920
+ };
5921
+ }
5791
5922
  var init_task_scope = __esm({
5792
5923
  "src/lib/task-scope.ts"() {
5793
5924
  "use strict";
@@ -5806,14 +5937,14 @@ var init_memory = __esm({
5806
5937
 
5807
5938
  // src/lib/keychain.ts
5808
5939
  import { readFile as readFile4, writeFile as writeFile5, unlink, mkdir as mkdir4, chmod as chmod2 } from "fs/promises";
5809
- import { existsSync as existsSync13 } from "fs";
5810
- import path17 from "path";
5811
- import os10 from "os";
5940
+ import { existsSync as existsSync15 } from "fs";
5941
+ import path18 from "path";
5942
+ import os11 from "os";
5812
5943
  function getKeyDir() {
5813
- return process.env.EXE_OS_DIR ?? process.env.EXE_MEM_DIR ?? path17.join(os10.homedir(), ".exe-os");
5944
+ return process.env.EXE_OS_DIR ?? process.env.EXE_MEM_DIR ?? path18.join(os11.homedir(), ".exe-os");
5814
5945
  }
5815
5946
  function getKeyPath() {
5816
- return path17.join(getKeyDir(), "master.key");
5947
+ return path18.join(getKeyDir(), "master.key");
5817
5948
  }
5818
5949
  async function tryKeytar() {
5819
5950
  try {
@@ -5834,9 +5965,9 @@ async function getMasterKey() {
5834
5965
  }
5835
5966
  }
5836
5967
  const keyPath = getKeyPath();
5837
- if (!existsSync13(keyPath)) {
5968
+ if (!existsSync15(keyPath)) {
5838
5969
  process.stderr.write(
5839
- `[keychain] Key not found at ${keyPath} (HOME=${os10.homedir()}, EXE_OS_DIR=${process.env.EXE_OS_DIR ?? "unset"})
5970
+ `[keychain] Key not found at ${keyPath} (HOME=${os11.homedir()}, EXE_OS_DIR=${process.env.EXE_OS_DIR ?? "unset"})
5840
5971
  `
5841
5972
  );
5842
5973
  return null;
@@ -5866,6 +5997,7 @@ var shard_manager_exports = {};
5866
5997
  __export(shard_manager_exports, {
5867
5998
  disposeShards: () => disposeShards,
5868
5999
  ensureShardSchema: () => ensureShardSchema,
6000
+ getOpenShardCount: () => getOpenShardCount,
5869
6001
  getReadyShardClient: () => getReadyShardClient,
5870
6002
  getShardClient: () => getShardClient,
5871
6003
  getShardsDir: () => getShardsDir,
@@ -5874,15 +6006,18 @@ __export(shard_manager_exports, {
5874
6006
  listShards: () => listShards,
5875
6007
  shardExists: () => shardExists
5876
6008
  });
5877
- import path18 from "path";
5878
- import { existsSync as existsSync14, mkdirSync as mkdirSync7, readdirSync as readdirSync4 } from "fs";
6009
+ import path19 from "path";
6010
+ import { existsSync as existsSync16, mkdirSync as mkdirSync7, readdirSync as readdirSync4 } from "fs";
5879
6011
  import { createClient as createClient2 } from "@libsql/client";
5880
6012
  function initShardManager(encryptionKey) {
5881
6013
  _encryptionKey = encryptionKey;
5882
- if (!existsSync14(SHARDS_DIR)) {
6014
+ if (!existsSync16(SHARDS_DIR)) {
5883
6015
  mkdirSync7(SHARDS_DIR, { recursive: true });
5884
6016
  }
5885
6017
  _shardingEnabled = true;
6018
+ if (_evictionTimer) clearInterval(_evictionTimer);
6019
+ _evictionTimer = setInterval(evictIdleShards, EVICTION_INTERVAL_MS);
6020
+ _evictionTimer.unref();
5886
6021
  }
5887
6022
  function isShardingEnabled() {
5888
6023
  return _shardingEnabled;
@@ -5899,21 +6034,28 @@ function getShardClient(projectName) {
5899
6034
  throw new Error(`Invalid project name for shard: "${projectName}"`);
5900
6035
  }
5901
6036
  const cached = _shards.get(safeName);
5902
- if (cached) return cached;
5903
- const dbPath = path18.join(SHARDS_DIR, `${safeName}.db`);
6037
+ if (cached) {
6038
+ _shardLastAccess.set(safeName, Date.now());
6039
+ return cached;
6040
+ }
6041
+ while (_shards.size >= MAX_OPEN_SHARDS) {
6042
+ evictLRU();
6043
+ }
6044
+ const dbPath = path19.join(SHARDS_DIR, `${safeName}.db`);
5904
6045
  const client = createClient2({
5905
6046
  url: `file:${dbPath}`,
5906
6047
  encryptionKey: _encryptionKey
5907
6048
  });
5908
6049
  _shards.set(safeName, client);
6050
+ _shardLastAccess.set(safeName, Date.now());
5909
6051
  return client;
5910
6052
  }
5911
6053
  function shardExists(projectName) {
5912
6054
  const safeName = projectName.replace(/[^a-zA-Z0-9_-]/g, "_");
5913
- return existsSync14(path18.join(SHARDS_DIR, `${safeName}.db`));
6055
+ return existsSync16(path19.join(SHARDS_DIR, `${safeName}.db`));
5914
6056
  }
5915
6057
  function listShards() {
5916
- if (!existsSync14(SHARDS_DIR)) return [];
6058
+ if (!existsSync16(SHARDS_DIR)) return [];
5917
6059
  return readdirSync4(SHARDS_DIR).filter((f) => f.endsWith(".db")).map((f) => f.replace(".db", ""));
5918
6060
  }
5919
6061
  async function ensureShardSchema(client) {
@@ -5965,6 +6107,8 @@ async function ensureShardSchema(client) {
5965
6107
  for (const col of [
5966
6108
  "ALTER TABLE memories ADD COLUMN task_id TEXT",
5967
6109
  "ALTER TABLE memories ADD COLUMN consolidated INTEGER NOT NULL DEFAULT 0",
6110
+ "ALTER TABLE memories ADD COLUMN author_device_id TEXT",
6111
+ "ALTER TABLE memories ADD COLUMN scope TEXT NOT NULL DEFAULT 'business'",
5968
6112
  "ALTER TABLE memories ADD COLUMN importance INTEGER DEFAULT 5",
5969
6113
  "ALTER TABLE memories ADD COLUMN status TEXT DEFAULT 'active'",
5970
6114
  "ALTER TABLE memories ADD COLUMN wiki_synced INTEGER DEFAULT 0",
@@ -6102,21 +6246,69 @@ async function getReadyShardClient(projectName) {
6102
6246
  await ensureShardSchema(client);
6103
6247
  return client;
6104
6248
  }
6249
+ function evictLRU() {
6250
+ let oldest = null;
6251
+ let oldestTime = Infinity;
6252
+ for (const [name, time] of _shardLastAccess) {
6253
+ if (time < oldestTime) {
6254
+ oldestTime = time;
6255
+ oldest = name;
6256
+ }
6257
+ }
6258
+ if (oldest) {
6259
+ const client = _shards.get(oldest);
6260
+ if (client) {
6261
+ client.close();
6262
+ }
6263
+ _shards.delete(oldest);
6264
+ _shardLastAccess.delete(oldest);
6265
+ }
6266
+ }
6267
+ function evictIdleShards() {
6268
+ const now = Date.now();
6269
+ const toEvict = [];
6270
+ for (const [name, lastAccess] of _shardLastAccess) {
6271
+ if (now - lastAccess > SHARD_IDLE_MS) {
6272
+ toEvict.push(name);
6273
+ }
6274
+ }
6275
+ for (const name of toEvict) {
6276
+ const client = _shards.get(name);
6277
+ if (client) {
6278
+ client.close();
6279
+ }
6280
+ _shards.delete(name);
6281
+ _shardLastAccess.delete(name);
6282
+ }
6283
+ }
6284
+ function getOpenShardCount() {
6285
+ return _shards.size;
6286
+ }
6105
6287
  function disposeShards() {
6288
+ if (_evictionTimer) {
6289
+ clearInterval(_evictionTimer);
6290
+ _evictionTimer = null;
6291
+ }
6106
6292
  for (const [, client] of _shards) {
6107
6293
  client.close();
6108
6294
  }
6109
6295
  _shards.clear();
6296
+ _shardLastAccess.clear();
6110
6297
  _shardingEnabled = false;
6111
6298
  _encryptionKey = null;
6112
6299
  }
6113
- var SHARDS_DIR, _shards, _encryptionKey, _shardingEnabled;
6300
+ var SHARDS_DIR, SHARD_IDLE_MS, MAX_OPEN_SHARDS, EVICTION_INTERVAL_MS, _shards, _shardLastAccess, _evictionTimer, _encryptionKey, _shardingEnabled;
6114
6301
  var init_shard_manager = __esm({
6115
6302
  "src/lib/shard-manager.ts"() {
6116
6303
  "use strict";
6117
6304
  init_config();
6118
- SHARDS_DIR = path18.join(EXE_AI_DIR, "shards");
6305
+ SHARDS_DIR = path19.join(EXE_AI_DIR, "shards");
6306
+ SHARD_IDLE_MS = 5 * 60 * 1e3;
6307
+ MAX_OPEN_SHARDS = 10;
6308
+ EVICTION_INTERVAL_MS = 60 * 1e3;
6119
6309
  _shards = /* @__PURE__ */ new Map();
6310
+ _shardLastAccess = /* @__PURE__ */ new Map();
6311
+ _evictionTimer = null;
6120
6312
  _encryptionKey = null;
6121
6313
  _shardingEnabled = false;
6122
6314
  }