@askexenow/exe-os 0.9.7 → 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 +953 -105
  2. package/dist/bin/backfill-responses.js +952 -104
  3. package/dist/bin/backfill-vectors.js +956 -108
  4. package/dist/bin/cleanup-stale-review-tasks.js +802 -58
  5. package/dist/bin/cli.js +2292 -1070
  6. package/dist/bin/exe-agent-config.js +157 -101
  7. package/dist/bin/exe-agent.js +55 -29
  8. package/dist/bin/exe-assign.js +940 -92
  9. package/dist/bin/exe-boot.js +1424 -442
  10. package/dist/bin/exe-call.js +240 -141
  11. package/dist/bin/exe-cloud.js +198 -70
  12. package/dist/bin/exe-dispatch.js +951 -192
  13. package/dist/bin/exe-doctor.js +791 -51
  14. package/dist/bin/exe-export-behaviors.js +790 -42
  15. package/dist/bin/exe-forget.js +771 -31
  16. package/dist/bin/exe-gateway.js +1592 -521
  17. package/dist/bin/exe-heartbeat.js +850 -109
  18. package/dist/bin/exe-kill.js +783 -35
  19. package/dist/bin/exe-launch-agent.js +1030 -107
  20. package/dist/bin/exe-link.js +916 -110
  21. package/dist/bin/exe-new-employee.js +526 -217
  22. package/dist/bin/exe-pending-messages.js +1046 -62
  23. package/dist/bin/exe-pending-notifications.js +1318 -111
  24. package/dist/bin/exe-pending-reviews.js +1040 -72
  25. package/dist/bin/exe-rename.js +772 -59
  26. package/dist/bin/exe-review.js +772 -32
  27. package/dist/bin/exe-search.js +982 -128
  28. package/dist/bin/exe-session-cleanup.js +1180 -306
  29. package/dist/bin/exe-settings.js +185 -105
  30. package/dist/bin/exe-start-codex.js +886 -132
  31. package/dist/bin/exe-start-opencode.js +873 -119
  32. package/dist/bin/exe-status.js +803 -59
  33. package/dist/bin/exe-team.js +772 -32
  34. package/dist/bin/git-sweep.js +1046 -223
  35. package/dist/bin/graph-backfill.js +779 -31
  36. package/dist/bin/graph-export.js +785 -37
  37. package/dist/bin/install.js +632 -200
  38. package/dist/bin/scan-tasks.js +1055 -232
  39. package/dist/bin/setup.js +1419 -320
  40. package/dist/bin/shard-migrate.js +783 -35
  41. package/dist/bin/update.js +138 -49
  42. package/dist/bin/wiki-sync.js +782 -34
  43. package/dist/gateway/index.js +1444 -449
  44. package/dist/hooks/bug-report-worker.js +1141 -269
  45. package/dist/hooks/codex-stop-task-finalizer.js +4678 -0
  46. package/dist/hooks/commit-complete.js +1044 -221
  47. package/dist/hooks/error-recall.js +989 -135
  48. package/dist/hooks/exe-heartbeat-hook.js +99 -75
  49. package/dist/hooks/ingest-worker.js +4176 -3226
  50. package/dist/hooks/ingest.js +920 -168
  51. package/dist/hooks/instructions-loaded.js +874 -70
  52. package/dist/hooks/notification.js +860 -56
  53. package/dist/hooks/post-compact.js +881 -73
  54. package/dist/hooks/pre-compact.js +1050 -227
  55. package/dist/hooks/pre-tool-use.js +1084 -159
  56. package/dist/hooks/prompt-ingest-worker.js +1089 -164
  57. package/dist/hooks/prompt-submit.js +1469 -515
  58. package/dist/hooks/response-ingest-worker.js +1104 -179
  59. package/dist/hooks/session-end.js +1085 -251
  60. package/dist/hooks/session-start.js +1241 -231
  61. package/dist/hooks/stop.js +935 -109
  62. package/dist/hooks/subagent-stop.js +881 -73
  63. package/dist/hooks/summary-worker.js +1323 -307
  64. package/dist/index.js +1449 -452
  65. package/dist/lib/agent-config.js +28 -6
  66. package/dist/lib/cloud-sync.js +909 -115
  67. package/dist/lib/config.js +30 -10
  68. package/dist/lib/consolidation.js +42 -9
  69. package/dist/lib/database.js +739 -33
  70. package/dist/lib/db-daemon-client.js +73 -19
  71. package/dist/lib/db.js +2359 -0
  72. package/dist/lib/device-registry.js +760 -47
  73. package/dist/lib/embedder.js +201 -73
  74. package/dist/lib/employee-templates.js +30 -4
  75. package/dist/lib/employees.js +290 -86
  76. package/dist/lib/exe-daemon-client.js +187 -83
  77. package/dist/lib/exe-daemon.js +1696 -616
  78. package/dist/lib/hybrid-search.js +982 -128
  79. package/dist/lib/identity.js +43 -13
  80. package/dist/lib/license.js +133 -48
  81. package/dist/lib/messaging.js +167 -80
  82. package/dist/lib/reminders.js +35 -5
  83. package/dist/lib/schedules.js +772 -32
  84. package/dist/lib/skill-learning.js +54 -7
  85. package/dist/lib/store.js +779 -31
  86. package/dist/lib/task-router.js +94 -73
  87. package/dist/lib/tasks.js +298 -225
  88. package/dist/lib/tmux-routing.js +246 -172
  89. package/dist/lib/token-spend.js +52 -14
  90. package/dist/mcp/server.js +2893 -850
  91. package/dist/mcp/tools/complete-reminder.js +35 -5
  92. package/dist/mcp/tools/create-reminder.js +35 -5
  93. package/dist/mcp/tools/create-task.js +507 -323
  94. package/dist/mcp/tools/deactivate-behavior.js +40 -10
  95. package/dist/mcp/tools/list-reminders.js +35 -5
  96. package/dist/mcp/tools/list-tasks.js +277 -104
  97. package/dist/mcp/tools/send-message.js +129 -56
  98. package/dist/mcp/tools/update-task.js +1864 -188
  99. package/dist/runtime/index.js +1083 -259
  100. package/dist/tui/App.js +1501 -434
  101. package/package.json +3 -2
@@ -308,9 +308,34 @@ var init_provider_table = __esm({
308
308
  }
309
309
  });
310
310
 
311
+ // src/lib/secure-files.ts
312
+ import { chmodSync, existsSync as existsSync2, mkdirSync as mkdirSync2 } from "fs";
313
+ import { chmod, mkdir } from "fs/promises";
314
+ async function ensurePrivateDir(dirPath) {
315
+ await mkdir(dirPath, { recursive: true, mode: PRIVATE_DIR_MODE });
316
+ try {
317
+ await chmod(dirPath, PRIVATE_DIR_MODE);
318
+ } catch {
319
+ }
320
+ }
321
+ async function enforcePrivateFile(filePath) {
322
+ try {
323
+ await chmod(filePath, PRIVATE_FILE_MODE);
324
+ } catch {
325
+ }
326
+ }
327
+ var PRIVATE_DIR_MODE, PRIVATE_FILE_MODE;
328
+ var init_secure_files = __esm({
329
+ "src/lib/secure-files.ts"() {
330
+ "use strict";
331
+ PRIVATE_DIR_MODE = 448;
332
+ PRIVATE_FILE_MODE = 384;
333
+ }
334
+ });
335
+
311
336
  // src/lib/config.ts
312
- import { readFile, writeFile, mkdir, chmod } from "fs/promises";
313
- import { readFileSync as readFileSync2, existsSync as existsSync2, renameSync } from "fs";
337
+ import { readFile, writeFile } from "fs/promises";
338
+ import { readFileSync as readFileSync2, existsSync as existsSync3, renameSync } from "fs";
314
339
  import path2 from "path";
315
340
  import os2 from "os";
316
341
  function resolveDataDir() {
@@ -318,7 +343,7 @@ function resolveDataDir() {
318
343
  if (process.env.EXE_MEM_DIR) return process.env.EXE_MEM_DIR;
319
344
  const newDir = path2.join(os2.homedir(), ".exe-os");
320
345
  const legacyDir = path2.join(os2.homedir(), ".exe-mem");
321
- if (!existsSync2(newDir) && existsSync2(legacyDir)) {
346
+ if (!existsSync3(newDir) && existsSync3(legacyDir)) {
322
347
  try {
323
348
  renameSync(legacyDir, newDir);
324
349
  process.stderr.write(`[exe-os] Migrated data directory: ~/.exe-mem \u2192 ~/.exe-os
@@ -381,9 +406,9 @@ function normalizeAutoUpdate(raw) {
381
406
  }
382
407
  async function loadConfig() {
383
408
  const dir = process.env.EXE_OS_DIR ?? process.env.EXE_MEM_DIR ?? EXE_AI_DIR;
384
- await mkdir(dir, { recursive: true });
409
+ await ensurePrivateDir(dir);
385
410
  const configPath = path2.join(dir, "config.json");
386
- if (!existsSync2(configPath)) {
411
+ if (!existsSync3(configPath)) {
387
412
  return { ...DEFAULT_CONFIG, dbPath: path2.join(dir, "memories.db") };
388
413
  }
389
414
  const raw = await readFile(configPath, "utf-8");
@@ -396,6 +421,7 @@ async function loadConfig() {
396
421
  `);
397
422
  try {
398
423
  await writeFile(configPath, JSON.stringify(migratedCfg, null, 2) + "\n");
424
+ await enforcePrivateFile(configPath);
399
425
  } catch {
400
426
  }
401
427
  }
@@ -415,6 +441,7 @@ var EXE_AI_DIR, DB_PATH, MODELS_DIR, CONFIG_PATH, LEGACY_LANCE_PATH, CURRENT_CON
415
441
  var init_config = __esm({
416
442
  "src/lib/config.ts"() {
417
443
  "use strict";
444
+ init_secure_files();
418
445
  EXE_AI_DIR = resolveDataDir();
419
446
  DB_PATH = path2.join(EXE_AI_DIR, "memories.db");
420
447
  MODELS_DIR = path2.join(EXE_AI_DIR, "models");
@@ -519,10 +546,10 @@ var init_runtime_table = __esm({
519
546
  });
520
547
 
521
548
  // src/lib/agent-config.ts
522
- import { readFileSync as readFileSync3, writeFileSync as writeFileSync2, existsSync as existsSync3, mkdirSync as mkdirSync2 } from "fs";
549
+ import { readFileSync as readFileSync3, writeFileSync as writeFileSync2, existsSync as existsSync4 } from "fs";
523
550
  import path3 from "path";
524
551
  function loadAgentConfig() {
525
- if (!existsSync3(AGENT_CONFIG_PATH)) return {};
552
+ if (!existsSync4(AGENT_CONFIG_PATH)) return {};
526
553
  try {
527
554
  return JSON.parse(readFileSync3(AGENT_CONFIG_PATH, "utf-8"));
528
555
  } catch {
@@ -543,6 +570,7 @@ var init_agent_config = __esm({
543
570
  "use strict";
544
571
  init_config();
545
572
  init_runtime_table();
573
+ init_secure_files();
546
574
  AGENT_CONFIG_PATH = path3.join(EXE_AI_DIR, "agent-config.json");
547
575
  DEFAULT_MODELS = {
548
576
  claude: "claude-opus-4",
@@ -561,16 +589,16 @@ __export(intercom_queue_exports, {
561
589
  queueIntercom: () => queueIntercom,
562
590
  readQueue: () => readQueue
563
591
  });
564
- import { readFileSync as readFileSync4, writeFileSync as writeFileSync3, renameSync as renameSync2, existsSync as existsSync4, mkdirSync as mkdirSync3 } from "fs";
592
+ import { readFileSync as readFileSync4, writeFileSync as writeFileSync3, renameSync as renameSync2, existsSync as existsSync5, mkdirSync as mkdirSync3 } from "fs";
565
593
  import path4 from "path";
566
594
  import os3 from "os";
567
595
  function ensureDir() {
568
596
  const dir = path4.dirname(QUEUE_PATH);
569
- if (!existsSync4(dir)) mkdirSync3(dir, { recursive: true });
597
+ if (!existsSync5(dir)) mkdirSync3(dir, { recursive: true });
570
598
  }
571
599
  function readQueue() {
572
600
  try {
573
- if (!existsSync4(QUEUE_PATH)) return [];
601
+ if (!existsSync5(QUEUE_PATH)) return [];
574
602
  return JSON.parse(readFileSync4(QUEUE_PATH, "utf8"));
575
603
  } catch {
576
604
  return [];
@@ -735,7 +763,7 @@ var init_db_retry = __esm({
735
763
 
736
764
  // src/lib/employees.ts
737
765
  import { readFile as readFile2, writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
738
- import { existsSync as existsSync5, symlinkSync, readlinkSync, readFileSync as readFileSync5, renameSync as renameSync3, unlinkSync, writeFileSync as writeFileSync4 } from "fs";
766
+ import { existsSync as existsSync6, symlinkSync, readlinkSync, readFileSync as readFileSync5, renameSync as renameSync3, unlinkSync, writeFileSync as writeFileSync4 } from "fs";
739
767
  import { execSync as execSync3 } from "child_process";
740
768
  import path5 from "path";
741
769
  import os4 from "os";
@@ -756,7 +784,7 @@ function isCoordinatorName(agentName, employees = loadEmployeesSync()) {
756
784
  return agentName.toLowerCase() === getCoordinatorName(employees).toLowerCase();
757
785
  }
758
786
  function loadEmployeesSync(employeesPath = EMPLOYEES_PATH) {
759
- if (!existsSync5(employeesPath)) return [];
787
+ if (!existsSync6(employeesPath)) return [];
760
788
  try {
761
789
  return JSON.parse(readFileSync5(employeesPath, "utf-8"));
762
790
  } catch {
@@ -780,7 +808,7 @@ function isMultiInstance(agentName, employees) {
780
808
  if (!emp) return false;
781
809
  return MULTI_INSTANCE_ROLES.has(emp.role.toLowerCase());
782
810
  }
783
- var EMPLOYEES_PATH, DEFAULT_COORDINATOR_TEMPLATE_NAME, COORDINATOR_ROLE, MULTI_INSTANCE_ROLES;
811
+ var EMPLOYEES_PATH, DEFAULT_COORDINATOR_TEMPLATE_NAME, COORDINATOR_ROLE, MULTI_INSTANCE_ROLES, IDENTITY_DIR;
784
812
  var init_employees = __esm({
785
813
  "src/lib/employees.ts"() {
786
814
  "use strict";
@@ -789,12 +817,609 @@ var init_employees = __esm({
789
817
  DEFAULT_COORDINATOR_TEMPLATE_NAME = "exe";
790
818
  COORDINATOR_ROLE = "COO";
791
819
  MULTI_INSTANCE_ROLES = /* @__PURE__ */ new Set(["principal engineer", "content production specialist", "staff code reviewer"]);
820
+ IDENTITY_DIR = path5.join(EXE_AI_DIR, "identity");
821
+ }
822
+ });
823
+
824
+ // src/lib/database-adapter.ts
825
+ import os5 from "os";
826
+ import path6 from "path";
827
+ import { createRequire } from "module";
828
+ import { pathToFileURL } from "url";
829
+ function quotedIdentifier(identifier) {
830
+ return `"${identifier.replace(/"/g, '""')}"`;
831
+ }
832
+ function unqualifiedTableName(name) {
833
+ const raw = name.trim().replace(/^"|"$/g, "");
834
+ const parts = raw.split(".");
835
+ return parts[parts.length - 1].replace(/^"|"$/g, "").toLowerCase();
836
+ }
837
+ function stripTrailingSemicolon(sql) {
838
+ return sql.trim().replace(/;+\s*$/u, "");
839
+ }
840
+ function appendClause(sql, clause) {
841
+ const trimmed = stripTrailingSemicolon(sql);
842
+ const returningMatch = /\sRETURNING\b[\s\S]*$/iu.exec(trimmed);
843
+ if (!returningMatch) {
844
+ return `${trimmed}${clause}`;
845
+ }
846
+ const idx = returningMatch.index;
847
+ return `${trimmed.slice(0, idx)}${clause}${trimmed.slice(idx)}`;
848
+ }
849
+ function normalizeStatement(stmt) {
850
+ if (typeof stmt === "string") {
851
+ return { kind: "positional", sql: stmt, args: [] };
852
+ }
853
+ const sql = stmt.sql;
854
+ if (Array.isArray(stmt.args) || stmt.args === void 0) {
855
+ return { kind: "positional", sql, args: stmt.args ?? [] };
856
+ }
857
+ return { kind: "named", sql, args: stmt.args };
858
+ }
859
+ function rewriteBooleanLiterals(sql) {
860
+ let out = sql;
861
+ for (const column of BOOLEAN_COLUMN_NAMES) {
862
+ const scoped = `((?:\\b[a-z_][a-z0-9_]*\\.)?${column})`;
863
+ out = out.replace(new RegExp(`${scoped}\\s*=\\s*0\\b`, "giu"), "$1 = FALSE");
864
+ out = out.replace(new RegExp(`${scoped}\\s*=\\s*1\\b`, "giu"), "$1 = TRUE");
865
+ out = out.replace(new RegExp(`${scoped}\\s*!=\\s*0\\b`, "giu"), "$1 != FALSE");
866
+ out = out.replace(new RegExp(`${scoped}\\s*!=\\s*1\\b`, "giu"), "$1 != TRUE");
867
+ out = out.replace(new RegExp(`${scoped}\\s*<>\\s*0\\b`, "giu"), "$1 <> FALSE");
868
+ out = out.replace(new RegExp(`${scoped}\\s*<>\\s*1\\b`, "giu"), "$1 <> TRUE");
869
+ }
870
+ return out;
871
+ }
872
+ function rewriteInsertOrIgnore(sql) {
873
+ if (!/^\s*INSERT\s+OR\s+IGNORE\s+INTO\b/iu.test(sql)) {
874
+ return sql;
875
+ }
876
+ const replaced = sql.replace(/^\s*INSERT\s+OR\s+IGNORE\s+INTO\b/iu, "INSERT INTO");
877
+ return /\bON\s+CONFLICT\b/iu.test(replaced) ? replaced : appendClause(replaced, " ON CONFLICT DO NOTHING");
878
+ }
879
+ function rewriteInsertOrReplace(sql) {
880
+ const match = /^\s*INSERT\s+OR\s+REPLACE\s+INTO\s+([A-Za-z0-9_."]+)\s*\(([^)]+)\)([\s\S]*)$/iu.exec(sql);
881
+ if (!match) {
882
+ return sql;
883
+ }
884
+ const rawTable = match[1];
885
+ const rawColumns = match[2];
886
+ const remainder = match[3];
887
+ const tableName = unqualifiedTableName(rawTable);
888
+ const conflictKeys = UPSERT_KEYS[tableName];
889
+ if (!conflictKeys?.length) {
890
+ return sql;
891
+ }
892
+ const columns = rawColumns.split(",").map((col) => col.trim().replace(/^"|"$/g, ""));
893
+ const updateColumns = columns.filter((col) => !conflictKeys.includes(col));
894
+ const conflictTarget = conflictKeys.map(quotedIdentifier).join(", ");
895
+ const updateClause = updateColumns.length === 0 ? " DO NOTHING" : ` DO UPDATE SET ${updateColumns.map((col) => `${quotedIdentifier(col)} = EXCLUDED.${quotedIdentifier(col)}`).join(", ")}`;
896
+ return `INSERT INTO ${rawTable} (${rawColumns})${appendClause(remainder, ` ON CONFLICT (${conflictTarget})${updateClause}`)}`;
897
+ }
898
+ function rewriteSql(sql) {
899
+ let out = sql;
900
+ out = out.replace(/\bdatetime\(\s*['"]now['"]\s*\)/giu, "CURRENT_TIMESTAMP");
901
+ out = out.replace(/\bvector32\s*\(\s*\?\s*\)/giu, "?");
902
+ out = rewriteBooleanLiterals(out);
903
+ out = rewriteInsertOrReplace(out);
904
+ out = rewriteInsertOrIgnore(out);
905
+ return stripTrailingSemicolon(out);
906
+ }
907
+ function toBoolean(value) {
908
+ if (value === null || value === void 0) return value;
909
+ if (typeof value === "boolean") return value;
910
+ if (typeof value === "number") return value !== 0;
911
+ if (typeof value === "bigint") return value !== 0n;
912
+ if (typeof value === "string") {
913
+ const normalized = value.trim().toLowerCase();
914
+ if (normalized === "0" || normalized === "false") return false;
915
+ if (normalized === "1" || normalized === "true") return true;
916
+ }
917
+ return Boolean(value);
918
+ }
919
+ function countQuestionMarks(sql, end) {
920
+ let count = 0;
921
+ let inSingle = false;
922
+ let inDouble = false;
923
+ let inLineComment = false;
924
+ let inBlockComment = false;
925
+ for (let i = 0; i < end; i++) {
926
+ const ch = sql[i];
927
+ const next = sql[i + 1];
928
+ if (inLineComment) {
929
+ if (ch === "\n") inLineComment = false;
930
+ continue;
931
+ }
932
+ if (inBlockComment) {
933
+ if (ch === "*" && next === "/") {
934
+ inBlockComment = false;
935
+ i += 1;
936
+ }
937
+ continue;
938
+ }
939
+ if (!inSingle && !inDouble && ch === "-" && next === "-") {
940
+ inLineComment = true;
941
+ i += 1;
942
+ continue;
943
+ }
944
+ if (!inSingle && !inDouble && ch === "/" && next === "*") {
945
+ inBlockComment = true;
946
+ i += 1;
947
+ continue;
948
+ }
949
+ if (!inDouble && ch === "'" && sql[i - 1] !== "\\") {
950
+ inSingle = !inSingle;
951
+ continue;
952
+ }
953
+ if (!inSingle && ch === '"' && sql[i - 1] !== "\\") {
954
+ inDouble = !inDouble;
955
+ continue;
956
+ }
957
+ if (!inSingle && !inDouble && ch === "?") {
958
+ count += 1;
959
+ }
960
+ }
961
+ return count;
962
+ }
963
+ function findBooleanPlaceholderIndexes(sql) {
964
+ const indexes = /* @__PURE__ */ new Set();
965
+ for (const column of BOOLEAN_COLUMN_NAMES) {
966
+ const pattern = new RegExp(`(?:\\b[a-z_][a-z0-9_]*\\.)?${column}\\s*=\\s*\\?`, "giu");
967
+ for (const match of sql.matchAll(pattern)) {
968
+ const matchText = match[0];
969
+ const qIndex = match.index + matchText.lastIndexOf("?");
970
+ indexes.add(countQuestionMarks(sql, qIndex + 1));
971
+ }
972
+ }
973
+ return indexes;
974
+ }
975
+ function coerceInsertBooleanArgs(sql, args) {
976
+ const match = /^\s*INSERT(?:\s+OR\s+(?:IGNORE|REPLACE))?\s+INTO\s+([A-Za-z0-9_."]+)\s*\(([^)]+)\)/iu.exec(sql);
977
+ if (!match) return;
978
+ const rawTable = match[1];
979
+ const rawColumns = match[2];
980
+ const boolColumns = BOOLEAN_COLUMNS_BY_TABLE[unqualifiedTableName(rawTable)];
981
+ if (!boolColumns?.size) return;
982
+ const columns = rawColumns.split(",").map((col) => col.trim().replace(/^"|"$/g, ""));
983
+ for (const [index, column] of columns.entries()) {
984
+ if (boolColumns.has(column) && index < args.length) {
985
+ args[index] = toBoolean(args[index]);
986
+ }
987
+ }
988
+ }
989
+ function coerceUpdateBooleanArgs(sql, args) {
990
+ const match = /^\s*UPDATE\s+([A-Za-z0-9_."]+)\s+SET\s+([\s\S]+?)(?:\s+WHERE\b|$)/iu.exec(sql);
991
+ if (!match) return;
992
+ const rawTable = match[1];
993
+ const setClause = match[2];
994
+ const boolColumns = BOOLEAN_COLUMNS_BY_TABLE[unqualifiedTableName(rawTable)];
995
+ if (!boolColumns?.size) return;
996
+ const assignments = setClause.split(",");
997
+ let placeholderIndex = 0;
998
+ for (const assignment of assignments) {
999
+ if (!assignment.includes("?")) continue;
1000
+ placeholderIndex += 1;
1001
+ const colMatch = /^\s*(?:[A-Za-z_][A-Za-z0-9_]*\.)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*\?/iu.exec(assignment);
1002
+ if (colMatch && boolColumns.has(colMatch[1])) {
1003
+ args[placeholderIndex - 1] = toBoolean(args[placeholderIndex - 1]);
1004
+ }
1005
+ }
1006
+ }
1007
+ function coerceBooleanArgs(sql, args) {
1008
+ const nextArgs = [...args];
1009
+ coerceInsertBooleanArgs(sql, nextArgs);
1010
+ coerceUpdateBooleanArgs(sql, nextArgs);
1011
+ const placeholderIndexes = findBooleanPlaceholderIndexes(sql);
1012
+ for (const index of placeholderIndexes) {
1013
+ if (index > 0 && index <= nextArgs.length) {
1014
+ nextArgs[index - 1] = toBoolean(nextArgs[index - 1]);
1015
+ }
1016
+ }
1017
+ return nextArgs;
1018
+ }
1019
+ function convertQuestionMarksToDollarParams(sql) {
1020
+ let out = "";
1021
+ let placeholder = 0;
1022
+ let inSingle = false;
1023
+ let inDouble = false;
1024
+ let inLineComment = false;
1025
+ let inBlockComment = false;
1026
+ for (let i = 0; i < sql.length; i++) {
1027
+ const ch = sql[i];
1028
+ const next = sql[i + 1];
1029
+ if (inLineComment) {
1030
+ out += ch;
1031
+ if (ch === "\n") inLineComment = false;
1032
+ continue;
1033
+ }
1034
+ if (inBlockComment) {
1035
+ out += ch;
1036
+ if (ch === "*" && next === "/") {
1037
+ out += next;
1038
+ inBlockComment = false;
1039
+ i += 1;
1040
+ }
1041
+ continue;
1042
+ }
1043
+ if (!inSingle && !inDouble && ch === "-" && next === "-") {
1044
+ out += ch + next;
1045
+ inLineComment = true;
1046
+ i += 1;
1047
+ continue;
1048
+ }
1049
+ if (!inSingle && !inDouble && ch === "/" && next === "*") {
1050
+ out += ch + next;
1051
+ inBlockComment = true;
1052
+ i += 1;
1053
+ continue;
1054
+ }
1055
+ if (!inDouble && ch === "'" && sql[i - 1] !== "\\") {
1056
+ inSingle = !inSingle;
1057
+ out += ch;
1058
+ continue;
1059
+ }
1060
+ if (!inSingle && ch === '"' && sql[i - 1] !== "\\") {
1061
+ inDouble = !inDouble;
1062
+ out += ch;
1063
+ continue;
1064
+ }
1065
+ if (!inSingle && !inDouble && ch === "?") {
1066
+ placeholder += 1;
1067
+ out += `$${placeholder}`;
1068
+ continue;
1069
+ }
1070
+ out += ch;
1071
+ }
1072
+ return out;
1073
+ }
1074
+ function translateStatementForPostgres(stmt) {
1075
+ const normalized = normalizeStatement(stmt);
1076
+ if (normalized.kind === "named") {
1077
+ throw new Error("Named SQL parameters are not supported by the Prisma adapter.");
1078
+ }
1079
+ const rewrittenSql = rewriteSql(normalized.sql);
1080
+ const coercedArgs = coerceBooleanArgs(rewrittenSql, normalized.args);
1081
+ return {
1082
+ sql: convertQuestionMarksToDollarParams(rewrittenSql),
1083
+ args: coercedArgs
1084
+ };
1085
+ }
1086
+ function shouldBypassPostgres(stmt) {
1087
+ const normalized = normalizeStatement(stmt);
1088
+ if (normalized.kind === "named") {
1089
+ return true;
1090
+ }
1091
+ return IMMEDIATE_FALLBACK_PATTERNS.some((pattern) => pattern.test(normalized.sql));
1092
+ }
1093
+ function shouldFallbackOnError(error) {
1094
+ const message = error instanceof Error ? error.message : String(error);
1095
+ return /42P01|42883|42601|does not exist|syntax error|not supported|Named SQL parameters are not supported/iu.test(message);
1096
+ }
1097
+ function isReadQuery(sql) {
1098
+ const trimmed = sql.trimStart();
1099
+ return /^(SELECT|WITH|SHOW|EXPLAIN|VALUES)\b/iu.test(trimmed) || /\bRETURNING\b/iu.test(trimmed);
1100
+ }
1101
+ function buildRow(row, columns) {
1102
+ const values = columns.map((column) => row[column]);
1103
+ return Object.assign(values, row);
1104
+ }
1105
+ function buildResultSet(rows, rowsAffected = 0) {
1106
+ const columns = rows[0] ? Object.keys(rows[0]) : [];
1107
+ const resultRows = rows.map((row) => buildRow(row, columns));
1108
+ return {
1109
+ columns,
1110
+ columnTypes: columns.map(() => ""),
1111
+ rows: resultRows,
1112
+ rowsAffected,
1113
+ lastInsertRowid: void 0,
1114
+ toJSON() {
1115
+ return {
1116
+ columns,
1117
+ columnTypes: columns.map(() => ""),
1118
+ rows,
1119
+ rowsAffected,
1120
+ lastInsertRowid: void 0
1121
+ };
1122
+ }
1123
+ };
1124
+ }
1125
+ async function loadPrismaClient() {
1126
+ if (!prismaClientPromise) {
1127
+ prismaClientPromise = (async () => {
1128
+ const explicitPath = process.env.EXE_OS_PRISMA_CLIENT_PATH;
1129
+ if (explicitPath) {
1130
+ const module2 = await import(pathToFileURL(explicitPath).href);
1131
+ const PrismaClient2 = module2.PrismaClient ?? module2.default?.PrismaClient;
1132
+ if (!PrismaClient2) {
1133
+ throw new Error(`No PrismaClient export found at ${explicitPath}`);
1134
+ }
1135
+ return new PrismaClient2();
1136
+ }
1137
+ const exeDbRoot = process.env.EXE_DB_ROOT ?? path6.join(os5.homedir(), "exe-db");
1138
+ const requireFromExeDb = createRequire(path6.join(exeDbRoot, "package.json"));
1139
+ const prismaEntry = requireFromExeDb.resolve("@prisma/client");
1140
+ const module = await import(pathToFileURL(prismaEntry).href);
1141
+ const PrismaClient = module.PrismaClient ?? module.default?.PrismaClient;
1142
+ if (!PrismaClient) {
1143
+ throw new Error(`No PrismaClient export found in ${prismaEntry}`);
1144
+ }
1145
+ return new PrismaClient();
1146
+ })();
1147
+ }
1148
+ return prismaClientPromise;
1149
+ }
1150
+ async function ensureCompatibilityViews(prisma) {
1151
+ if (!compatibilityBootstrapPromise) {
1152
+ compatibilityBootstrapPromise = (async () => {
1153
+ for (const mapping of VIEW_MAPPINGS) {
1154
+ const relation = mapping.source.replace(/"/g, "");
1155
+ const rows = await prisma.$queryRawUnsafe(
1156
+ "SELECT to_regclass($1) AS regclass",
1157
+ relation
1158
+ );
1159
+ if (!rows[0]?.regclass) {
1160
+ continue;
1161
+ }
1162
+ await prisma.$executeRawUnsafe(
1163
+ `CREATE OR REPLACE VIEW public.${quotedIdentifier(mapping.view)} AS SELECT * FROM ${mapping.source}`
1164
+ );
1165
+ }
1166
+ })();
1167
+ }
1168
+ return compatibilityBootstrapPromise;
1169
+ }
1170
+ async function executeOnPrisma(executor, stmt) {
1171
+ const translated = translateStatementForPostgres(stmt);
1172
+ if (isReadQuery(translated.sql)) {
1173
+ const rows = await executor.$queryRawUnsafe(
1174
+ translated.sql,
1175
+ ...translated.args
1176
+ );
1177
+ return buildResultSet(rows, /\bRETURNING\b/iu.test(translated.sql) ? rows.length : 0);
1178
+ }
1179
+ const rowsAffected = await executor.$executeRawUnsafe(translated.sql, ...translated.args);
1180
+ return buildResultSet([], rowsAffected);
1181
+ }
1182
+ function splitSqlStatements(sql) {
1183
+ const parts = [];
1184
+ let current = "";
1185
+ let inSingle = false;
1186
+ let inDouble = false;
1187
+ let inLineComment = false;
1188
+ let inBlockComment = false;
1189
+ for (let i = 0; i < sql.length; i++) {
1190
+ const ch = sql[i];
1191
+ const next = sql[i + 1];
1192
+ if (inLineComment) {
1193
+ current += ch;
1194
+ if (ch === "\n") inLineComment = false;
1195
+ continue;
1196
+ }
1197
+ if (inBlockComment) {
1198
+ current += ch;
1199
+ if (ch === "*" && next === "/") {
1200
+ current += next;
1201
+ inBlockComment = false;
1202
+ i += 1;
1203
+ }
1204
+ continue;
1205
+ }
1206
+ if (!inSingle && !inDouble && ch === "-" && next === "-") {
1207
+ current += ch + next;
1208
+ inLineComment = true;
1209
+ i += 1;
1210
+ continue;
1211
+ }
1212
+ if (!inSingle && !inDouble && ch === "/" && next === "*") {
1213
+ current += ch + next;
1214
+ inBlockComment = true;
1215
+ i += 1;
1216
+ continue;
1217
+ }
1218
+ if (!inDouble && ch === "'" && sql[i - 1] !== "\\") {
1219
+ inSingle = !inSingle;
1220
+ current += ch;
1221
+ continue;
1222
+ }
1223
+ if (!inSingle && ch === '"' && sql[i - 1] !== "\\") {
1224
+ inDouble = !inDouble;
1225
+ current += ch;
1226
+ continue;
1227
+ }
1228
+ if (!inSingle && !inDouble && ch === ";") {
1229
+ if (current.trim()) {
1230
+ parts.push(current.trim());
1231
+ }
1232
+ current = "";
1233
+ continue;
1234
+ }
1235
+ current += ch;
1236
+ }
1237
+ if (current.trim()) {
1238
+ parts.push(current.trim());
1239
+ }
1240
+ return parts;
1241
+ }
1242
+ async function createPrismaDbAdapter(fallbackClient) {
1243
+ const prisma = await loadPrismaClient();
1244
+ await ensureCompatibilityViews(prisma);
1245
+ let closed = false;
1246
+ let adapter;
1247
+ const fallbackExecute = async (stmt, error) => {
1248
+ if (!fallbackClient) {
1249
+ if (error) throw error;
1250
+ throw new Error("No fallback SQLite client is available for this Prisma-routed query.");
1251
+ }
1252
+ if (error) {
1253
+ process.stderr.write(
1254
+ `[database-adapter] Falling back to SQLite: ${error instanceof Error ? error.message : String(error)}
1255
+ `
1256
+ );
1257
+ }
1258
+ return fallbackClient.execute(stmt);
1259
+ };
1260
+ adapter = {
1261
+ async execute(stmt) {
1262
+ if (shouldBypassPostgres(stmt)) {
1263
+ return fallbackExecute(stmt);
1264
+ }
1265
+ try {
1266
+ return await executeOnPrisma(prisma, stmt);
1267
+ } catch (error) {
1268
+ if (shouldFallbackOnError(error)) {
1269
+ return fallbackExecute(stmt, error);
1270
+ }
1271
+ throw error;
1272
+ }
1273
+ },
1274
+ async batch(stmts, mode) {
1275
+ if (stmts.some((stmt) => shouldBypassPostgres(stmt))) {
1276
+ if (!fallbackClient) {
1277
+ throw new Error("Cannot batch unsupported SQLite-only statements without a fallback client.");
1278
+ }
1279
+ return fallbackClient.batch(stmts, mode);
1280
+ }
1281
+ try {
1282
+ if (prisma.$transaction) {
1283
+ return await prisma.$transaction(async (tx) => {
1284
+ const results2 = [];
1285
+ for (const stmt of stmts) {
1286
+ results2.push(await executeOnPrisma(tx, stmt));
1287
+ }
1288
+ return results2;
1289
+ });
1290
+ }
1291
+ const results = [];
1292
+ for (const stmt of stmts) {
1293
+ results.push(await executeOnPrisma(prisma, stmt));
1294
+ }
1295
+ return results;
1296
+ } catch (error) {
1297
+ if (fallbackClient && shouldFallbackOnError(error)) {
1298
+ process.stderr.write(
1299
+ `[database-adapter] Falling back batch to SQLite: ${error instanceof Error ? error.message : String(error)}
1300
+ `
1301
+ );
1302
+ return fallbackClient.batch(stmts, mode);
1303
+ }
1304
+ throw error;
1305
+ }
1306
+ },
1307
+ async migrate(stmts) {
1308
+ if (fallbackClient) {
1309
+ return fallbackClient.migrate(stmts);
1310
+ }
1311
+ return adapter.batch(stmts, "deferred");
1312
+ },
1313
+ async transaction(mode) {
1314
+ if (!fallbackClient) {
1315
+ throw new Error("Interactive transactions are only supported on the SQLite fallback client.");
1316
+ }
1317
+ return fallbackClient.transaction(mode);
1318
+ },
1319
+ async executeMultiple(sql) {
1320
+ if (fallbackClient && shouldBypassPostgres(sql)) {
1321
+ return fallbackClient.executeMultiple(sql);
1322
+ }
1323
+ for (const statement of splitSqlStatements(sql)) {
1324
+ await adapter.execute(statement);
1325
+ }
1326
+ },
1327
+ async sync() {
1328
+ if (fallbackClient) {
1329
+ return fallbackClient.sync();
1330
+ }
1331
+ return { frame_no: 0, frames_synced: 0 };
1332
+ },
1333
+ close() {
1334
+ closed = true;
1335
+ prismaClientPromise = null;
1336
+ compatibilityBootstrapPromise = null;
1337
+ void prisma.$disconnect?.();
1338
+ },
1339
+ get closed() {
1340
+ return closed;
1341
+ },
1342
+ get protocol() {
1343
+ return "prisma-postgres";
1344
+ }
1345
+ };
1346
+ return adapter;
1347
+ }
1348
+ var VIEW_MAPPINGS, UPSERT_KEYS, BOOLEAN_COLUMNS_BY_TABLE, BOOLEAN_COLUMN_NAMES, IMMEDIATE_FALLBACK_PATTERNS, prismaClientPromise, compatibilityBootstrapPromise;
1349
+ var init_database_adapter = __esm({
1350
+ "src/lib/database-adapter.ts"() {
1351
+ "use strict";
1352
+ VIEW_MAPPINGS = [
1353
+ { view: "memories", source: "memory.memory_records" },
1354
+ { view: "tasks", source: "memory.tasks" },
1355
+ { view: "behaviors", source: "memory.behaviors" },
1356
+ { view: "entities", source: "memory.entities" },
1357
+ { view: "relationships", source: "memory.relationships" },
1358
+ { view: "entity_memories", source: "memory.entity_memories" },
1359
+ { view: "entity_aliases", source: "memory.entity_aliases" },
1360
+ { view: "notifications", source: "memory.notifications" },
1361
+ { view: "messages", source: "memory.messages" },
1362
+ { view: "users", source: "wiki.users" },
1363
+ { view: "workspaces", source: "wiki.workspaces" },
1364
+ { view: "workspace_users", source: "wiki.workspace_users" },
1365
+ { view: "documents", source: "wiki.workspace_documents" },
1366
+ { view: "chats", source: "wiki.workspace_chats" }
1367
+ ];
1368
+ UPSERT_KEYS = {
1369
+ memories: ["id"],
1370
+ tasks: ["id"],
1371
+ behaviors: ["id"],
1372
+ entities: ["id"],
1373
+ relationships: ["id"],
1374
+ entity_aliases: ["alias"],
1375
+ notifications: ["id"],
1376
+ messages: ["id"],
1377
+ users: ["id"],
1378
+ workspaces: ["id"],
1379
+ workspace_users: ["id"],
1380
+ documents: ["id"],
1381
+ chats: ["id"]
1382
+ };
1383
+ BOOLEAN_COLUMNS_BY_TABLE = {
1384
+ memories: /* @__PURE__ */ new Set(["has_error", "draft"]),
1385
+ behaviors: /* @__PURE__ */ new Set(["active"]),
1386
+ notifications: /* @__PURE__ */ new Set(["read"]),
1387
+ users: /* @__PURE__ */ new Set(["has_personal_memory"])
1388
+ };
1389
+ BOOLEAN_COLUMN_NAMES = new Set(
1390
+ Object.values(BOOLEAN_COLUMNS_BY_TABLE).flatMap((cols) => [...cols])
1391
+ );
1392
+ IMMEDIATE_FALLBACK_PATTERNS = [
1393
+ /\bPRAGMA\b/i,
1394
+ /\bsqlite_master\b/i,
1395
+ /(?:^|[.\s])(?:memories|conversations|entities)_fts\b/i,
1396
+ /\bMATCH\b/i,
1397
+ /\bvector_distance_cos\s*\(/i,
1398
+ /\bjson_extract\s*\(/i,
1399
+ /\bjulianday\s*\(/i,
1400
+ /\bstrftime\s*\(/i,
1401
+ /\blast_insert_rowid\s*\(/i
1402
+ ];
1403
+ prismaClientPromise = null;
1404
+ compatibilityBootstrapPromise = null;
792
1405
  }
793
1406
  });
794
1407
 
795
1408
  // src/lib/database.ts
796
1409
  import { createClient } from "@libsql/client";
797
1410
  async function initDatabase(config) {
1411
+ if (_walCheckpointTimer) {
1412
+ clearInterval(_walCheckpointTimer);
1413
+ _walCheckpointTimer = null;
1414
+ }
1415
+ if (_daemonClient) {
1416
+ _daemonClient.close();
1417
+ _daemonClient = null;
1418
+ }
1419
+ if (_adapterClient && _adapterClient !== _resilientClient) {
1420
+ _adapterClient.close();
1421
+ }
1422
+ _adapterClient = null;
798
1423
  if (_client) {
799
1424
  _client.close();
800
1425
  _client = null;
@@ -808,6 +1433,7 @@ async function initDatabase(config) {
808
1433
  }
809
1434
  _client = createClient(opts);
810
1435
  _resilientClient = wrapWithRetry(_client);
1436
+ _adapterClient = _resilientClient;
811
1437
  _client.execute("PRAGMA busy_timeout = 30000").catch(() => {
812
1438
  });
813
1439
  _client.execute("PRAGMA journal_mode = WAL").catch(() => {
@@ -818,11 +1444,17 @@ async function initDatabase(config) {
818
1444
  });
819
1445
  }, 3e4);
820
1446
  _walCheckpointTimer.unref();
1447
+ if (process.env.DATABASE_URL) {
1448
+ _adapterClient = await createPrismaDbAdapter(_resilientClient);
1449
+ }
821
1450
  }
822
1451
  function getClient() {
823
- if (!_resilientClient) {
1452
+ if (!_adapterClient) {
824
1453
  throw new Error("Database client not initialized. Call initDatabase() first.");
825
1454
  }
1455
+ if (process.env.DATABASE_URL) {
1456
+ return _adapterClient;
1457
+ }
826
1458
  if (process.env.EXE_IS_DAEMON === "1") {
827
1459
  return _resilientClient;
828
1460
  }
@@ -1115,6 +1747,7 @@ async function ensureSchema() {
1115
1747
  project TEXT NOT NULL,
1116
1748
  summary TEXT NOT NULL,
1117
1749
  task_file TEXT,
1750
+ session_scope TEXT,
1118
1751
  read INTEGER NOT NULL DEFAULT 0,
1119
1752
  created_at TEXT NOT NULL
1120
1753
  );
@@ -1123,7 +1756,7 @@ async function ensureSchema() {
1123
1756
  ON notifications(read);
1124
1757
 
1125
1758
  CREATE INDEX IF NOT EXISTS idx_notifications_agent
1126
- ON notifications(agent_id);
1759
+ ON notifications(agent_id, session_scope);
1127
1760
 
1128
1761
  CREATE INDEX IF NOT EXISTS idx_notifications_task_file
1129
1762
  ON notifications(task_file);
@@ -1161,6 +1794,7 @@ async function ensureSchema() {
1161
1794
  target_agent TEXT NOT NULL,
1162
1795
  target_project TEXT,
1163
1796
  target_device TEXT NOT NULL DEFAULT 'local',
1797
+ session_scope TEXT,
1164
1798
  content TEXT NOT NULL,
1165
1799
  priority TEXT DEFAULT 'normal',
1166
1800
  status TEXT DEFAULT 'pending',
@@ -1174,10 +1808,31 @@ async function ensureSchema() {
1174
1808
  );
1175
1809
 
1176
1810
  CREATE INDEX IF NOT EXISTS idx_messages_target
1177
- ON messages(target_agent, status);
1811
+ ON messages(target_agent, session_scope, status);
1178
1812
 
1179
1813
  CREATE INDEX IF NOT EXISTS idx_messages_conversation_order
1180
- ON messages(target_agent, from_agent, server_seq);
1814
+ ON messages(target_agent, session_scope, from_agent, server_seq);
1815
+ `);
1816
+ try {
1817
+ await client.execute({
1818
+ sql: `ALTER TABLE notifications ADD COLUMN session_scope TEXT`,
1819
+ args: []
1820
+ });
1821
+ } catch {
1822
+ }
1823
+ try {
1824
+ await client.execute({
1825
+ sql: `ALTER TABLE messages ADD COLUMN session_scope TEXT`,
1826
+ args: []
1827
+ });
1828
+ } catch {
1829
+ }
1830
+ await client.executeMultiple(`
1831
+ CREATE INDEX IF NOT EXISTS idx_notifications_agent_scope_read
1832
+ ON notifications(agent_id, session_scope, read, created_at);
1833
+
1834
+ CREATE INDEX IF NOT EXISTS idx_messages_target_scope_status
1835
+ ON messages(target_agent, session_scope, status, created_at);
1181
1836
  `);
1182
1837
  try {
1183
1838
  await client.execute({
@@ -1761,34 +2416,46 @@ async function ensureSchema() {
1761
2416
  } catch {
1762
2417
  }
1763
2418
  }
2419
+ try {
2420
+ await client.execute({
2421
+ sql: `UPDATE tasks SET status = 'closed' WHERE status = 'done' AND result IS NOT NULL`,
2422
+ args: []
2423
+ });
2424
+ } catch {
2425
+ }
1764
2426
  }
1765
- var _client, _resilientClient, _walCheckpointTimer, _daemonClient, initTurso;
2427
+ var _client, _resilientClient, _walCheckpointTimer, _daemonClient, _adapterClient, initTurso;
1766
2428
  var init_database = __esm({
1767
2429
  "src/lib/database.ts"() {
1768
2430
  "use strict";
1769
2431
  init_db_retry();
1770
2432
  init_employees();
2433
+ init_database_adapter();
1771
2434
  _client = null;
1772
2435
  _resilientClient = null;
1773
2436
  _walCheckpointTimer = null;
1774
2437
  _daemonClient = null;
2438
+ _adapterClient = null;
1775
2439
  initTurso = initDatabase;
1776
2440
  }
1777
2441
  });
1778
2442
 
1779
2443
  // src/lib/license.ts
1780
- import { readFileSync as readFileSync6, writeFileSync as writeFileSync5, existsSync as existsSync6, mkdirSync as mkdirSync4 } from "fs";
2444
+ import { readFileSync as readFileSync6, writeFileSync as writeFileSync5, existsSync as existsSync7, mkdirSync as mkdirSync4 } from "fs";
1781
2445
  import { randomUUID } from "crypto";
1782
- import path6 from "path";
2446
+ import { createRequire as createRequire2 } from "module";
2447
+ import { pathToFileURL as pathToFileURL2 } from "url";
2448
+ import os6 from "os";
2449
+ import path7 from "path";
1783
2450
  import { jwtVerify, importSPKI } from "jose";
1784
2451
  var LICENSE_PATH, CACHE_PATH, DEVICE_ID_PATH, PLAN_LIMITS;
1785
2452
  var init_license = __esm({
1786
2453
  "src/lib/license.ts"() {
1787
2454
  "use strict";
1788
2455
  init_config();
1789
- LICENSE_PATH = path6.join(EXE_AI_DIR, "license.key");
1790
- CACHE_PATH = path6.join(EXE_AI_DIR, "license-cache.json");
1791
- DEVICE_ID_PATH = path6.join(EXE_AI_DIR, "device-id");
2456
+ LICENSE_PATH = path7.join(EXE_AI_DIR, "license.key");
2457
+ CACHE_PATH = path7.join(EXE_AI_DIR, "license-cache.json");
2458
+ DEVICE_ID_PATH = path7.join(EXE_AI_DIR, "device-id");
1792
2459
  PLAN_LIMITS = {
1793
2460
  free: { devices: 1, employees: 1, memories: 5e3 },
1794
2461
  pro: { devices: 3, employees: 5, memories: 1e5 },
@@ -1800,11 +2467,11 @@ var init_license = __esm({
1800
2467
  });
1801
2468
 
1802
2469
  // src/lib/plan-limits.ts
1803
- import { readFileSync as readFileSync7, existsSync as existsSync7 } from "fs";
1804
- import path7 from "path";
2470
+ import { readFileSync as readFileSync7, existsSync as existsSync8 } from "fs";
2471
+ import path8 from "path";
1805
2472
  function getLicenseSync() {
1806
2473
  try {
1807
- if (!existsSync7(CACHE_PATH2)) return freeLicense();
2474
+ if (!existsSync8(CACHE_PATH2)) return freeLicense();
1808
2475
  const raw = JSON.parse(readFileSync7(CACHE_PATH2, "utf8"));
1809
2476
  if (!raw.token || typeof raw.token !== "string") return freeLicense();
1810
2477
  const parts = raw.token.split(".");
@@ -1843,7 +2510,7 @@ function assertEmployeeLimitSync(rosterPath) {
1843
2510
  const filePath = rosterPath ?? EMPLOYEES_PATH;
1844
2511
  let count = 0;
1845
2512
  try {
1846
- if (existsSync7(filePath)) {
2513
+ if (existsSync8(filePath)) {
1847
2514
  const raw = readFileSync7(filePath, "utf8");
1848
2515
  const employees = JSON.parse(raw);
1849
2516
  count = Array.isArray(employees) ? employees.length : 0;
@@ -1873,19 +2540,52 @@ var init_plan_limits = __esm({
1873
2540
  this.name = "PlanLimitError";
1874
2541
  }
1875
2542
  };
1876
- CACHE_PATH2 = path7.join(EXE_AI_DIR, "license-cache.json");
2543
+ CACHE_PATH2 = path8.join(EXE_AI_DIR, "license-cache.json");
2544
+ }
2545
+ });
2546
+
2547
+ // src/lib/task-scope.ts
2548
+ function getCurrentSessionScope() {
2549
+ try {
2550
+ return resolveExeSession();
2551
+ } catch {
2552
+ return null;
2553
+ }
2554
+ }
2555
+ function sessionScopeFilter(sessionScope, tableAlias) {
2556
+ const scope = sessionScope !== void 0 ? sessionScope : getCurrentSessionScope();
2557
+ if (!scope) return { sql: "", args: [] };
2558
+ const col = tableAlias ? `${tableAlias}.session_scope` : "session_scope";
2559
+ return {
2560
+ sql: ` AND (${col} IS NULL OR ${col} = ?)`,
2561
+ args: [scope]
2562
+ };
2563
+ }
2564
+ function strictSessionScopeFilter(sessionScope, tableAlias) {
2565
+ const scope = sessionScope !== void 0 ? sessionScope : getCurrentSessionScope();
2566
+ if (!scope) return { sql: "", args: [] };
2567
+ const col = tableAlias ? `${tableAlias}.session_scope` : "session_scope";
2568
+ return {
2569
+ sql: ` AND ${col} = ?`,
2570
+ args: [scope]
2571
+ };
2572
+ }
2573
+ var init_task_scope = __esm({
2574
+ "src/lib/task-scope.ts"() {
2575
+ "use strict";
2576
+ init_tmux_routing();
1877
2577
  }
1878
2578
  });
1879
2579
 
1880
2580
  // src/lib/notifications.ts
1881
2581
  import crypto from "crypto";
1882
- import path8 from "path";
1883
- import os5 from "os";
2582
+ import path9 from "path";
2583
+ import os7 from "os";
1884
2584
  import {
1885
2585
  readFileSync as readFileSync8,
1886
2586
  readdirSync,
1887
2587
  unlinkSync as unlinkSync2,
1888
- existsSync as existsSync8,
2588
+ existsSync as existsSync9,
1889
2589
  rmdirSync
1890
2590
  } from "fs";
1891
2591
  async function writeNotification(notification) {
@@ -1893,9 +2593,10 @@ async function writeNotification(notification) {
1893
2593
  const client = getClient();
1894
2594
  const id = crypto.randomUUID();
1895
2595
  const now = (/* @__PURE__ */ new Date()).toISOString();
2596
+ const sessionScope = notification.sessionScope === void 0 ? getCurrentSessionScope() : notification.sessionScope;
1896
2597
  await client.execute({
1897
- sql: `INSERT INTO notifications (id, agent_id, agent_role, event, project, summary, task_file, read, created_at)
1898
- VALUES (?, ?, ?, ?, ?, ?, ?, 0, ?)`,
2598
+ sql: `INSERT INTO notifications (id, agent_id, agent_role, event, project, summary, task_file, session_scope, read, created_at)
2599
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, 0, ?)`,
1899
2600
  args: [
1900
2601
  id,
1901
2602
  notification.agentId,
@@ -1904,6 +2605,7 @@ async function writeNotification(notification) {
1904
2605
  notification.project,
1905
2606
  notification.summary,
1906
2607
  notification.taskFile ?? null,
2608
+ sessionScope,
1907
2609
  now
1908
2610
  ]
1909
2611
  });
@@ -1912,12 +2614,14 @@ async function writeNotification(notification) {
1912
2614
  `);
1913
2615
  }
1914
2616
  }
1915
- async function markAsReadByTaskFile(taskFile) {
2617
+ async function markAsReadByTaskFile(taskFile, sessionScope) {
1916
2618
  try {
1917
2619
  const client = getClient();
2620
+ const scope = strictSessionScopeFilter(sessionScope);
1918
2621
  await client.execute({
1919
- sql: "UPDATE notifications SET read = 1 WHERE task_file = ? AND read = 0",
1920
- args: [taskFile]
2622
+ sql: `UPDATE notifications SET read = 1
2623
+ WHERE task_file = ? AND read = 0${scope.sql}`,
2624
+ args: [taskFile, ...scope.args]
1921
2625
  });
1922
2626
  } catch {
1923
2627
  }
@@ -1926,6 +2630,7 @@ var init_notifications = __esm({
1926
2630
  "src/lib/notifications.ts"() {
1927
2631
  "use strict";
1928
2632
  init_database();
2633
+ init_task_scope();
1929
2634
  }
1930
2635
  });
1931
2636
 
@@ -1963,30 +2668,6 @@ var init_session_kill_telemetry = __esm({
1963
2668
  }
1964
2669
  });
1965
2670
 
1966
- // src/lib/task-scope.ts
1967
- function getCurrentSessionScope() {
1968
- try {
1969
- return resolveExeSession();
1970
- } catch {
1971
- return null;
1972
- }
1973
- }
1974
- function sessionScopeFilter(sessionScope, tableAlias) {
1975
- const scope = sessionScope !== void 0 ? sessionScope : getCurrentSessionScope();
1976
- if (!scope) return { sql: "", args: [] };
1977
- const col = tableAlias ? `${tableAlias}.session_scope` : "session_scope";
1978
- return {
1979
- sql: ` AND (${col} IS NULL OR ${col} = ?)`,
1980
- args: [scope]
1981
- };
1982
- }
1983
- var init_task_scope = __esm({
1984
- "src/lib/task-scope.ts"() {
1985
- "use strict";
1986
- init_tmux_routing();
1987
- }
1988
- });
1989
-
1990
2671
  // src/lib/state-bus.ts
1991
2672
  var StateBus, orgBus;
1992
2673
  var init_state_bus = __esm({
@@ -2044,11 +2725,11 @@ var init_state_bus = __esm({
2044
2725
 
2045
2726
  // src/lib/tasks-crud.ts
2046
2727
  import crypto3 from "crypto";
2047
- import path9 from "path";
2048
- import os6 from "os";
2728
+ import path10 from "path";
2729
+ import os8 from "os";
2049
2730
  import { execSync as execSync4 } from "child_process";
2050
2731
  import { mkdir as mkdir3, writeFile as writeFile3, appendFile } from "fs/promises";
2051
- import { existsSync as existsSync9, readFileSync as readFileSync9 } from "fs";
2732
+ import { existsSync as existsSync10, readFileSync as readFileSync9 } from "fs";
2052
2733
  async function writeCheckpoint(input) {
2053
2734
  const client = getClient();
2054
2735
  const row = await resolveTask(client, input.taskId);
@@ -2223,8 +2904,8 @@ ${laneWarning}` : laneWarning;
2223
2904
  }
2224
2905
  if (input.baseDir) {
2225
2906
  try {
2226
- await mkdir3(path9.join(input.baseDir, "exe", "output"), { recursive: true });
2227
- await mkdir3(path9.join(input.baseDir, "exe", "research"), { recursive: true });
2907
+ await mkdir3(path10.join(input.baseDir, "exe", "output"), { recursive: true });
2908
+ await mkdir3(path10.join(input.baseDir, "exe", "research"), { recursive: true });
2228
2909
  await ensureArchitectureDoc(input.baseDir, input.projectName);
2229
2910
  await ensureGitignoreExe(input.baseDir);
2230
2911
  } catch {
@@ -2260,13 +2941,19 @@ ${laneWarning}` : laneWarning;
2260
2941
  });
2261
2942
  if (input.baseDir) {
2262
2943
  try {
2263
- const EXE_OS_DIR = path9.join(os6.homedir(), ".exe-os");
2264
- const mdPath = path9.join(EXE_OS_DIR, taskFile);
2265
- const mdDir = path9.dirname(mdPath);
2266
- if (!existsSync9(mdDir)) await mkdir3(mdDir, { recursive: true });
2944
+ const EXE_OS_DIR = path10.join(os8.homedir(), ".exe-os");
2945
+ const mdPath = path10.join(EXE_OS_DIR, taskFile);
2946
+ const mdDir = path10.dirname(mdPath);
2947
+ if (!existsSync10(mdDir)) await mkdir3(mdDir, { recursive: true });
2267
2948
  const reviewer = input.reviewer ?? input.assignedBy;
2268
2949
  const mdContent = `# ${input.title}
2269
2950
 
2951
+ ## MANDATORY: When done
2952
+
2953
+ You MUST call update_task with status "done" and a result summary when finished.
2954
+ If you skip this, your reviewer will not know you're done and your work won't be reviewed.
2955
+ Do NOT let a failed commit or any error prevent you from calling update_task(done).
2956
+
2270
2957
  **ID:** ${id}
2271
2958
  **Status:** ${initialStatus}
2272
2959
  **Priority:** ${input.priority}
@@ -2280,12 +2967,6 @@ ${laneWarning}` : laneWarning;
2280
2967
  ## Context
2281
2968
 
2282
2969
  ${input.context}
2283
-
2284
- ## MANDATORY: When done
2285
-
2286
- You MUST call update_task with status "done" and a result summary when finished.
2287
- If you skip this, your reviewer will not know you're done and your work won't be reviewed.
2288
- Do NOT let a failed commit or any error prevent you from calling update_task(done).
2289
2970
  `;
2290
2971
  await writeFile3(mdPath, mdContent, "utf-8");
2291
2972
  } catch (err) {
@@ -2534,7 +3215,7 @@ ${input.result}` : `\u26A0\uFE0F ${warning}`;
2534
3215
  await client.execute("PRAGMA wal_checkpoint(PASSIVE)");
2535
3216
  } catch {
2536
3217
  }
2537
- if (input.status === "done" || input.status === "cancelled") {
3218
+ if (input.status === "done" || input.status === "cancelled" || input.status === "closed") {
2538
3219
  try {
2539
3220
  const { clearQueueForAgent: clearQueueForAgent2 } = await Promise.resolve().then(() => (init_intercom_queue(), intercom_queue_exports));
2540
3221
  clearQueueForAgent2(String(row.assigned_to));
@@ -2563,9 +3244,9 @@ async function deleteTaskCore(taskId, _baseDir) {
2563
3244
  return { taskFile, assignedTo, assignedBy, taskSlug };
2564
3245
  }
2565
3246
  async function ensureArchitectureDoc(baseDir, projectName) {
2566
- const archPath = path9.join(baseDir, "exe", "ARCHITECTURE.md");
3247
+ const archPath = path10.join(baseDir, "exe", "ARCHITECTURE.md");
2567
3248
  try {
2568
- if (existsSync9(archPath)) return;
3249
+ if (existsSync10(archPath)) return;
2569
3250
  const template = [
2570
3251
  `# ${projectName} \u2014 System Architecture`,
2571
3252
  "",
@@ -2598,9 +3279,9 @@ async function ensureArchitectureDoc(baseDir, projectName) {
2598
3279
  }
2599
3280
  }
2600
3281
  async function ensureGitignoreExe(baseDir) {
2601
- const gitignorePath = path9.join(baseDir, ".gitignore");
3282
+ const gitignorePath = path10.join(baseDir, ".gitignore");
2602
3283
  try {
2603
- if (existsSync9(gitignorePath)) {
3284
+ if (existsSync10(gitignorePath)) {
2604
3285
  const content = readFileSync9(gitignorePath, "utf-8");
2605
3286
  if (/^\/?exe\/?$/m.test(content)) return;
2606
3287
  await appendFile(gitignorePath, "\n# Employee task assignments (private)\n/exe/\n");
@@ -2632,58 +3313,42 @@ var init_tasks_crud = __esm({
2632
3313
  });
2633
3314
 
2634
3315
  // src/lib/tasks-review.ts
2635
- import path10 from "path";
2636
- import { existsSync as existsSync10, readdirSync as readdirSync2, unlinkSync as unlinkSync3 } from "fs";
3316
+ import path11 from "path";
3317
+ import { existsSync as existsSync11, readdirSync as readdirSync2, unlinkSync as unlinkSync3 } from "fs";
2637
3318
  async function countPendingReviews(sessionScope) {
2638
3319
  const client = getClient();
2639
- if (sessionScope) {
2640
- const result3 = await client.execute({
2641
- sql: "SELECT COUNT(*) as cnt FROM tasks WHERE status = 'needs_review' AND (session_scope = ? OR session_scope IS NULL)",
2642
- args: [sessionScope]
2643
- });
2644
- return Number(result3.rows[0]?.cnt) || 0;
2645
- }
3320
+ const scope = strictSessionScopeFilter(
3321
+ sessionScope === void 0 ? getCurrentSessionScope() : sessionScope
3322
+ );
2646
3323
  const result2 = await client.execute({
2647
- sql: "SELECT COUNT(*) as cnt FROM tasks WHERE status = 'needs_review'",
2648
- args: []
3324
+ sql: `SELECT COUNT(*) as cnt FROM tasks
3325
+ WHERE status = 'needs_review'${scope.sql}`,
3326
+ args: [...scope.args]
2649
3327
  });
2650
3328
  return Number(result2.rows[0]?.cnt) || 0;
2651
3329
  }
2652
3330
  async function countNewPendingReviewsSince(sinceIso, sessionScope) {
2653
3331
  const client = getClient();
2654
- if (sessionScope) {
2655
- const result3 = await client.execute({
2656
- sql: `SELECT COUNT(*) as cnt FROM tasks
2657
- WHERE status = 'needs_review' AND updated_at > ?
2658
- AND session_scope = ?`,
2659
- args: [sinceIso, sessionScope]
2660
- });
2661
- return Number(result3.rows[0]?.cnt) || 0;
2662
- }
3332
+ const scope = strictSessionScopeFilter(
3333
+ sessionScope === void 0 ? getCurrentSessionScope() : sessionScope
3334
+ );
2663
3335
  const result2 = await client.execute({
2664
3336
  sql: `SELECT COUNT(*) as cnt FROM tasks
2665
- WHERE status = 'needs_review' AND updated_at > ?`,
2666
- args: [sinceIso]
3337
+ WHERE status = 'needs_review' AND updated_at > ?${scope.sql}`,
3338
+ args: [sinceIso, ...scope.args]
2667
3339
  });
2668
3340
  return Number(result2.rows[0]?.cnt) || 0;
2669
3341
  }
2670
3342
  async function listPendingReviews(limit, sessionScope) {
2671
3343
  const client = getClient();
2672
- if (sessionScope) {
2673
- const result3 = await client.execute({
2674
- sql: `SELECT title, assigned_to, project_name, updated_at FROM tasks
2675
- WHERE status = 'needs_review'
2676
- AND session_scope = ?
2677
- ORDER BY updated_at ASC LIMIT ?`,
2678
- args: [sessionScope, limit]
2679
- });
2680
- return result3.rows;
2681
- }
3344
+ const scope = strictSessionScopeFilter(
3345
+ sessionScope === void 0 ? getCurrentSessionScope() : sessionScope
3346
+ );
2682
3347
  const result2 = await client.execute({
2683
3348
  sql: `SELECT title, assigned_to, project_name, updated_at FROM tasks
2684
- WHERE status = 'needs_review'
3349
+ WHERE status = 'needs_review'${scope.sql}
2685
3350
  ORDER BY updated_at ASC LIMIT ?`,
2686
- args: [limit]
3351
+ args: [...scope.args, limit]
2687
3352
  });
2688
3353
  return result2.rows;
2689
3354
  }
@@ -2695,7 +3360,7 @@ async function cleanupOrphanedReviews() {
2695
3360
  WHERE status IN ('open', 'needs_review', 'in_progress')
2696
3361
  AND assigned_by = 'system'
2697
3362
  AND title LIKE 'Review:%'
2698
- AND parent_task_id IN (SELECT id FROM tasks WHERE status IN ('done', 'cancelled'))`,
3363
+ AND parent_task_id IN (SELECT id FROM tasks WHERE status IN ('done', 'cancelled', 'closed'))`,
2699
3364
  args: [now]
2700
3365
  });
2701
3366
  const r1b = await client.execute({
@@ -2814,11 +3479,11 @@ async function cleanupReviewFile(row, taskFile, _baseDir) {
2814
3479
  );
2815
3480
  }
2816
3481
  try {
2817
- const cacheDir = path10.join(EXE_AI_DIR, "session-cache");
2818
- if (existsSync10(cacheDir)) {
3482
+ const cacheDir = path11.join(EXE_AI_DIR, "session-cache");
3483
+ if (existsSync11(cacheDir)) {
2819
3484
  for (const f of readdirSync2(cacheDir)) {
2820
3485
  if (f.startsWith("review-notified-")) {
2821
- unlinkSync3(path10.join(cacheDir, f));
3486
+ unlinkSync3(path11.join(cacheDir, f));
2822
3487
  }
2823
3488
  }
2824
3489
  }
@@ -2835,11 +3500,12 @@ var init_tasks_review = __esm({
2835
3500
  init_tmux_routing();
2836
3501
  init_session_key();
2837
3502
  init_state_bus();
3503
+ init_task_scope();
2838
3504
  }
2839
3505
  });
2840
3506
 
2841
3507
  // src/lib/tasks-chain.ts
2842
- import path11 from "path";
3508
+ import path12 from "path";
2843
3509
  import { readFile as readFile3, writeFile as writeFile4 } from "fs/promises";
2844
3510
  async function cascadeUnblock(taskId, baseDir, now) {
2845
3511
  const client = getClient();
@@ -2856,7 +3522,7 @@ async function cascadeUnblock(taskId, baseDir, now) {
2856
3522
  });
2857
3523
  for (const ur of unblockedRows.rows) {
2858
3524
  try {
2859
- const ubFile = path11.join(baseDir, String(ur.task_file));
3525
+ const ubFile = path12.join(baseDir, String(ur.task_file));
2860
3526
  let ubContent = await readFile3(ubFile, "utf-8");
2861
3527
  ubContent = ubContent.replace(/\*\*Status:\*\* blocked/, "**Status:** open");
2862
3528
  ubContent = ubContent.replace(/\n\*\*Blocked by:\*\*.*\n/, "\n");
@@ -2891,7 +3557,7 @@ async function checkSubtaskCompletion(parentTaskId, projectName) {
2891
3557
  const scScope = sessionScopeFilter();
2892
3558
  const remaining = await client.execute({
2893
3559
  sql: `SELECT COUNT(*) as cnt FROM tasks
2894
- WHERE parent_task_id = ? AND status NOT IN ('done', 'cancelled')${scScope.sql}`,
3560
+ WHERE parent_task_id = ? AND status NOT IN ('done', 'cancelled', 'closed')${scScope.sql}`,
2895
3561
  args: [parentTaskId, ...scScope.args]
2896
3562
  });
2897
3563
  const cnt = Number(remaining.rows[0]?.cnt ?? 1);
@@ -2925,7 +3591,7 @@ var init_tasks_chain = __esm({
2925
3591
 
2926
3592
  // src/lib/project-name.ts
2927
3593
  import { execSync as execSync5 } from "child_process";
2928
- import path12 from "path";
3594
+ import path13 from "path";
2929
3595
  function getProjectName(cwd) {
2930
3596
  const dir = cwd ?? process.cwd();
2931
3597
  if (_cached2 && _cachedCwd === dir) return _cached2;
@@ -2938,7 +3604,7 @@ function getProjectName(cwd) {
2938
3604
  timeout: 2e3,
2939
3605
  stdio: ["pipe", "pipe", "pipe"]
2940
3606
  }).trim();
2941
- repoRoot = path12.dirname(gitCommonDir);
3607
+ repoRoot = path13.dirname(gitCommonDir);
2942
3608
  } catch {
2943
3609
  repoRoot = execSync5("git rev-parse --show-toplevel", {
2944
3610
  cwd: dir,
@@ -2947,11 +3613,11 @@ function getProjectName(cwd) {
2947
3613
  stdio: ["pipe", "pipe", "pipe"]
2948
3614
  }).trim();
2949
3615
  }
2950
- _cached2 = path12.basename(repoRoot);
3616
+ _cached2 = path13.basename(repoRoot);
2951
3617
  _cachedCwd = dir;
2952
3618
  return _cached2;
2953
3619
  } catch {
2954
- _cached2 = path12.basename(dir);
3620
+ _cached2 = path13.basename(dir);
2955
3621
  _cachedCwd = dir;
2956
3622
  return _cached2;
2957
3623
  }
@@ -3424,7 +4090,7 @@ __export(tasks_exports, {
3424
4090
  updateTaskStatus: () => updateTaskStatus,
3425
4091
  writeCheckpoint: () => writeCheckpoint
3426
4092
  });
3427
- import path13 from "path";
4093
+ import path14 from "path";
3428
4094
  import { writeFileSync as writeFileSync6, mkdirSync as mkdirSync5, unlinkSync as unlinkSync4 } from "fs";
3429
4095
  async function createTask(input) {
3430
4096
  const result2 = await createTaskCore(input);
@@ -3444,12 +4110,12 @@ async function updateTask(input) {
3444
4110
  const { row, taskFile, now, taskId } = await updateTaskStatus(input);
3445
4111
  try {
3446
4112
  const agent = String(row.assigned_to);
3447
- const cacheDir = path13.join(EXE_AI_DIR, "session-cache");
3448
- const cachePath = path13.join(cacheDir, `current-task-${agent}.json`);
4113
+ const cacheDir = path14.join(EXE_AI_DIR, "session-cache");
4114
+ const cachePath = path14.join(cacheDir, `current-task-${agent}.json`);
3449
4115
  if (input.status === "in_progress") {
3450
4116
  mkdirSync5(cacheDir, { recursive: true });
3451
4117
  writeFileSync6(cachePath, JSON.stringify({ taskId, title: String(row.title) }));
3452
- } else if (input.status === "done" || input.status === "blocked" || input.status === "cancelled") {
4118
+ } else if (input.status === "done" || input.status === "blocked" || input.status === "cancelled" || input.status === "closed") {
3453
4119
  try {
3454
4120
  unlinkSync4(cachePath);
3455
4121
  } catch {
@@ -3457,10 +4123,10 @@ async function updateTask(input) {
3457
4123
  }
3458
4124
  } catch {
3459
4125
  }
3460
- if (input.status === "done") {
4126
+ if (input.status === "done" || input.status === "closed") {
3461
4127
  await cleanupReviewFile(row, taskFile, input.baseDir);
3462
4128
  }
3463
- if (input.status === "done" || input.status === "cancelled") {
4129
+ if (input.status === "done" || input.status === "cancelled" || input.status === "closed") {
3464
4130
  try {
3465
4131
  const client = getClient();
3466
4132
  const taskTitle = String(row.title);
@@ -3476,7 +4142,7 @@ async function updateTask(input) {
3476
4142
  if (!isCoordinatorName(assignedAgent)) {
3477
4143
  try {
3478
4144
  const draftClient = getClient();
3479
- if (input.status === "done") {
4145
+ if (input.status === "done" || input.status === "closed") {
3480
4146
  await draftClient.execute({
3481
4147
  sql: `UPDATE memories SET draft = 0 WHERE agent_id = ? AND draft = 1`,
3482
4148
  args: [assignedAgent]
@@ -3493,7 +4159,7 @@ async function updateTask(input) {
3493
4159
  try {
3494
4160
  const client = getClient();
3495
4161
  const cascaded = await client.execute({
3496
- sql: `UPDATE tasks SET status = 'done', updated_at = ?
4162
+ sql: `UPDATE tasks SET status = 'closed', updated_at = ?
3497
4163
  WHERE parent_task_id = ? AND status = 'needs_review'`,
3498
4164
  args: [now, taskId]
3499
4165
  });
@@ -3506,14 +4172,14 @@ async function updateTask(input) {
3506
4172
  } catch {
3507
4173
  }
3508
4174
  }
3509
- const isTerminal = input.status === "done" || input.status === "needs_review";
4175
+ const isTerminal = input.status === "done" || input.status === "needs_review" || input.status === "closed";
3510
4176
  if (isTerminal) {
3511
4177
  const isCoordinator = isCoordinatorName(String(row.assigned_to));
3512
4178
  if (!isCoordinator) {
3513
4179
  notifyTaskDone();
3514
4180
  }
3515
4181
  await markTaskNotificationsRead(taskFile);
3516
- if (input.status === "done") {
4182
+ if (input.status === "done" || input.status === "closed") {
3517
4183
  try {
3518
4184
  await cascadeUnblock(taskId, input.baseDir, now);
3519
4185
  } catch {
@@ -3533,7 +4199,7 @@ async function updateTask(input) {
3533
4199
  }
3534
4200
  }
3535
4201
  }
3536
- if (input.status === "done" && !isCoordinatorName(String(row.assigned_to)) && !process.env.VITEST) {
4202
+ if ((input.status === "done" || input.status === "closed") && !isCoordinatorName(String(row.assigned_to)) && !process.env.VITEST) {
3537
4203
  Promise.resolve().then(() => (init_skill_learning(), skill_learning_exports)).then(
3538
4204
  ({ captureAndLearn: captureAndLearn2 }) => captureAndLearn2({
3539
4205
  taskId,
@@ -3905,6 +4571,7 @@ __export(tmux_routing_exports, {
3905
4571
  isEmployeeAlive: () => isEmployeeAlive,
3906
4572
  isExeSession: () => isExeSession,
3907
4573
  isSessionBusy: () => isSessionBusy,
4574
+ notifyCoordinatorTaskCompletion: () => notifyCoordinatorTaskCompletion,
3908
4575
  notifyParentExe: () => notifyParentExe,
3909
4576
  parseParentExe: () => parseParentExe,
3910
4577
  registerParentExe: () => registerParentExe,
@@ -3915,13 +4582,13 @@ __export(tmux_routing_exports, {
3915
4582
  verifyPaneAtCapacity: () => verifyPaneAtCapacity
3916
4583
  });
3917
4584
  import { execFileSync as execFileSync2, execSync as execSync6 } from "child_process";
3918
- import { readFileSync as readFileSync10, writeFileSync as writeFileSync7, mkdirSync as mkdirSync6, existsSync as existsSync11, appendFileSync, readdirSync as readdirSync3 } from "fs";
3919
- import path14 from "path";
3920
- import os7 from "os";
4585
+ import { readFileSync as readFileSync10, writeFileSync as writeFileSync7, mkdirSync as mkdirSync6, existsSync as existsSync12, appendFileSync, readdirSync as readdirSync3 } from "fs";
4586
+ import path15 from "path";
4587
+ import os9 from "os";
3921
4588
  import { fileURLToPath } from "url";
3922
4589
  import { unlinkSync as unlinkSync5 } from "fs";
3923
4590
  function spawnLockPath(sessionName) {
3924
- return path14.join(SPAWN_LOCK_DIR, `${sessionName}.lock`);
4591
+ return path15.join(SPAWN_LOCK_DIR, `${sessionName}.lock`);
3925
4592
  }
3926
4593
  function isProcessAlive(pid) {
3927
4594
  try {
@@ -3932,11 +4599,11 @@ function isProcessAlive(pid) {
3932
4599
  }
3933
4600
  }
3934
4601
  function acquireSpawnLock(sessionName) {
3935
- if (!existsSync11(SPAWN_LOCK_DIR)) {
4602
+ if (!existsSync12(SPAWN_LOCK_DIR)) {
3936
4603
  mkdirSync6(SPAWN_LOCK_DIR, { recursive: true });
3937
4604
  }
3938
4605
  const lockFile = spawnLockPath(sessionName);
3939
- if (existsSync11(lockFile)) {
4606
+ if (existsSync12(lockFile)) {
3940
4607
  try {
3941
4608
  const lock = JSON.parse(readFileSync10(lockFile, "utf8"));
3942
4609
  const age = Date.now() - lock.timestamp;
@@ -3958,13 +4625,13 @@ function releaseSpawnLock(sessionName) {
3958
4625
  function resolveBehaviorsExporterScript() {
3959
4626
  try {
3960
4627
  const thisFile = fileURLToPath(import.meta.url);
3961
- const scriptPath = path14.join(
3962
- path14.dirname(thisFile),
4628
+ const scriptPath = path15.join(
4629
+ path15.dirname(thisFile),
3963
4630
  "..",
3964
4631
  "bin",
3965
4632
  "exe-export-behaviors.js"
3966
4633
  );
3967
- return existsSync11(scriptPath) ? scriptPath : null;
4634
+ return existsSync12(scriptPath) ? scriptPath : null;
3968
4635
  } catch {
3969
4636
  return null;
3970
4637
  }
@@ -4030,11 +4697,11 @@ function extractRootExe(name) {
4030
4697
  return parts.length > 0 ? parts[parts.length - 1] : null;
4031
4698
  }
4032
4699
  function registerParentExe(sessionKey, parentExe, dispatchedBy) {
4033
- if (!existsSync11(SESSION_CACHE)) {
4700
+ if (!existsSync12(SESSION_CACHE)) {
4034
4701
  mkdirSync6(SESSION_CACHE, { recursive: true });
4035
4702
  }
4036
4703
  const rootExe = extractRootExe(parentExe) ?? parentExe;
4037
- const filePath = path14.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`);
4704
+ const filePath = path15.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`);
4038
4705
  writeFileSync7(filePath, JSON.stringify({
4039
4706
  parentExe: rootExe,
4040
4707
  dispatchedBy: dispatchedBy || rootExe,
@@ -4043,7 +4710,7 @@ function registerParentExe(sessionKey, parentExe, dispatchedBy) {
4043
4710
  }
4044
4711
  function getParentExe(sessionKey) {
4045
4712
  try {
4046
- const data = JSON.parse(readFileSync10(path14.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`), "utf8"));
4713
+ const data = JSON.parse(readFileSync10(path15.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`), "utf8"));
4047
4714
  return data.parentExe || null;
4048
4715
  } catch {
4049
4716
  return null;
@@ -4052,7 +4719,7 @@ function getParentExe(sessionKey) {
4052
4719
  function getDispatchedBy(sessionKey) {
4053
4720
  try {
4054
4721
  const data = JSON.parse(readFileSync10(
4055
- path14.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`),
4722
+ path15.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`),
4056
4723
  "utf8"
4057
4724
  ));
4058
4725
  return data.dispatchedBy ?? data.parentExe ?? null;
@@ -4122,7 +4789,7 @@ async function verifyPaneAtCapacity(sessionName) {
4122
4789
  }
4123
4790
  function readDebounceState() {
4124
4791
  try {
4125
- if (!existsSync11(DEBOUNCE_FILE)) return {};
4792
+ if (!existsSync12(DEBOUNCE_FILE)) return {};
4126
4793
  const raw = JSON.parse(readFileSync10(DEBOUNCE_FILE, "utf8"));
4127
4794
  const state = {};
4128
4795
  for (const [key, val] of Object.entries(raw)) {
@@ -4139,7 +4806,7 @@ function readDebounceState() {
4139
4806
  }
4140
4807
  function writeDebounceState(state) {
4141
4808
  try {
4142
- if (!existsSync11(SESSION_CACHE)) mkdirSync6(SESSION_CACHE, { recursive: true });
4809
+ if (!existsSync12(SESSION_CACHE)) mkdirSync6(SESSION_CACHE, { recursive: true });
4143
4810
  writeFileSync7(DEBOUNCE_FILE, JSON.stringify(state));
4144
4811
  } catch {
4145
4812
  }
@@ -4238,8 +4905,8 @@ function sendIntercom(targetSession) {
4238
4905
  try {
4239
4906
  const rawAgent = targetSession.split("-")[0] ?? targetSession;
4240
4907
  const agent = baseAgentName(rawAgent);
4241
- const markerPath = path14.join(SESSION_CACHE, `current-task-${agent}.json`);
4242
- if (existsSync11(markerPath)) {
4908
+ const markerPath = path15.join(SESSION_CACHE, `current-task-${agent}.json`);
4909
+ if (existsSync12(markerPath)) {
4243
4910
  logIntercom(`SKIP \u2192 ${targetSession} (has in_progress task marker \u2014 will auto-chain)`);
4244
4911
  return "debounced";
4245
4912
  }
@@ -4248,8 +4915,8 @@ function sendIntercom(targetSession) {
4248
4915
  try {
4249
4916
  const rawAgent = targetSession.split("-")[0] ?? targetSession;
4250
4917
  const agent = baseAgentName(rawAgent);
4251
- const taskDir = path14.join(process.cwd(), "exe", agent);
4252
- if (existsSync11(taskDir)) {
4918
+ const taskDir = path15.join(process.cwd(), "exe", agent);
4919
+ if (existsSync12(taskDir)) {
4253
4920
  const files = readdirSync3(taskDir).filter(
4254
4921
  (f) => f.endsWith(".md") && f !== "DONE.txt"
4255
4922
  );
@@ -4309,6 +4976,21 @@ function notifyParentExe(sessionKey) {
4309
4976
  }
4310
4977
  return true;
4311
4978
  }
4979
+ function notifyCoordinatorTaskCompletion(coordinatorSession, agentName, taskTitle) {
4980
+ const transport = getTransport();
4981
+ try {
4982
+ const sessions = transport.listSessions();
4983
+ if (!sessions.includes(coordinatorSession)) return false;
4984
+ execSync6(
4985
+ `tmux send-keys -t ${JSON.stringify(coordinatorSession)} '/exe-intercom' Enter`,
4986
+ { timeout: 3e3 }
4987
+ );
4988
+ logIntercom(`COMPLETION \u2192 ${coordinatorSession} (${agentName} completed "${taskTitle.slice(0, 50)}")`);
4989
+ return true;
4990
+ } catch {
4991
+ return false;
4992
+ }
4993
+ }
4312
4994
  function ensureEmployee(employeeName2, exeSession2, projectDir2, opts) {
4313
4995
  if (isCoordinatorName(employeeName2)) {
4314
4996
  return { status: "failed", sessionName: "", error: "The COO is not a dispatchable employee" };
@@ -4382,23 +5064,23 @@ function spawnEmployee(employeeName2, exeSession2, projectDir2, opts) {
4382
5064
  const transport = getTransport();
4383
5065
  const sessionName = employeeSessionName(employeeName2, exeSession2, opts?.instance);
4384
5066
  const instanceLabel = opts?.instance != null && opts.instance > 0 ? `${employeeName2}${opts.instance}` : employeeName2;
4385
- const logDir = path14.join(os7.homedir(), ".exe-os", "session-logs");
4386
- const logFile = path14.join(logDir, `${instanceLabel}-${Date.now()}.log`);
4387
- if (!existsSync11(logDir)) {
5067
+ const logDir = path15.join(os9.homedir(), ".exe-os", "session-logs");
5068
+ const logFile = path15.join(logDir, `${instanceLabel}-${Date.now()}.log`);
5069
+ if (!existsSync12(logDir)) {
4388
5070
  mkdirSync6(logDir, { recursive: true });
4389
5071
  }
4390
5072
  transport.kill(sessionName);
4391
5073
  let cleanupSuffix = "";
4392
5074
  try {
4393
5075
  const thisFile = fileURLToPath(import.meta.url);
4394
- const cleanupScript = path14.join(path14.dirname(thisFile), "..", "bin", "exe-session-cleanup.js");
4395
- if (existsSync11(cleanupScript)) {
5076
+ const cleanupScript = path15.join(path15.dirname(thisFile), "..", "bin", "exe-session-cleanup.js");
5077
+ if (existsSync12(cleanupScript)) {
4396
5078
  cleanupSuffix = `; ${process.execPath} "${cleanupScript}" "${employeeName2}" "${exeSession2}"`;
4397
5079
  }
4398
5080
  } catch {
4399
5081
  }
4400
5082
  try {
4401
- const claudeJsonPath = path14.join(os7.homedir(), ".claude.json");
5083
+ const claudeJsonPath = path15.join(os9.homedir(), ".claude.json");
4402
5084
  let claudeJson = {};
4403
5085
  try {
4404
5086
  claudeJson = JSON.parse(readFileSync10(claudeJsonPath, "utf8"));
@@ -4413,10 +5095,10 @@ function spawnEmployee(employeeName2, exeSession2, projectDir2, opts) {
4413
5095
  } catch {
4414
5096
  }
4415
5097
  try {
4416
- const settingsDir = path14.join(os7.homedir(), ".claude", "projects");
5098
+ const settingsDir = path15.join(os9.homedir(), ".claude", "projects");
4417
5099
  const normalizedKey = (opts?.cwd ?? projectDir2).replace(/\//g, "-").replace(/^-/, "");
4418
- const projSettingsDir = path14.join(settingsDir, normalizedKey);
4419
- const settingsPath = path14.join(projSettingsDir, "settings.json");
5100
+ const projSettingsDir = path15.join(settingsDir, normalizedKey);
5101
+ const settingsPath = path15.join(projSettingsDir, "settings.json");
4420
5102
  let settings = {};
4421
5103
  try {
4422
5104
  settings = JSON.parse(readFileSync10(settingsPath, "utf8"));
@@ -4463,8 +5145,8 @@ function spawnEmployee(employeeName2, exeSession2, projectDir2, opts) {
4463
5145
  let behaviorsFlag = "";
4464
5146
  let legacyFallbackWarned = false;
4465
5147
  if (!useExeAgent && !useBinSymlink) {
4466
- const identityPath = path14.join(
4467
- os7.homedir(),
5148
+ const identityPath = path15.join(
5149
+ os9.homedir(),
4468
5150
  ".exe-os",
4469
5151
  "identity",
4470
5152
  `${employeeName2}.md`
@@ -4473,13 +5155,13 @@ function spawnEmployee(employeeName2, exeSession2, projectDir2, opts) {
4473
5155
  const hasAgentFlag = claudeSupportsAgentFlag();
4474
5156
  if (hasAgentFlag) {
4475
5157
  identityFlag = ` --agent ${employeeName2}`;
4476
- } else if (existsSync11(identityPath)) {
5158
+ } else if (existsSync12(identityPath)) {
4477
5159
  identityFlag = ` --append-system-prompt-file ${identityPath}`;
4478
5160
  legacyFallbackWarned = true;
4479
5161
  }
4480
5162
  const behaviorsFile = exportBehaviorsSync(
4481
5163
  employeeName2,
4482
- path14.basename(spawnCwd),
5164
+ path15.basename(spawnCwd),
4483
5165
  sessionName
4484
5166
  );
4485
5167
  if (behaviorsFile) {
@@ -4494,9 +5176,9 @@ function spawnEmployee(employeeName2, exeSession2, projectDir2, opts) {
4494
5176
  }
4495
5177
  let sessionContextFlag = "";
4496
5178
  try {
4497
- const ctxDir = path14.join(os7.homedir(), ".exe-os", "session-cache");
5179
+ const ctxDir = path15.join(os9.homedir(), ".exe-os", "session-cache");
4498
5180
  mkdirSync6(ctxDir, { recursive: true });
4499
- const ctxFile = path14.join(ctxDir, `session-context-${sessionName}.md`);
5181
+ const ctxFile = path15.join(ctxDir, `session-context-${sessionName}.md`);
4500
5182
  const ctxContent = [
4501
5183
  `## Session Context`,
4502
5184
  `You are running in tmux session: ${sessionName}.`,
@@ -4580,7 +5262,7 @@ function spawnEmployee(employeeName2, exeSession2, projectDir2, opts) {
4580
5262
  transport.pipeLog(sessionName, logFile);
4581
5263
  try {
4582
5264
  const mySession = getMySession();
4583
- const dispatchInfo = path14.join(SESSION_CACHE, `dispatch-info-${sessionName}.json`);
5265
+ const dispatchInfo = path15.join(SESSION_CACHE, `dispatch-info-${sessionName}.json`);
4584
5266
  writeFileSync7(dispatchInfo, JSON.stringify({
4585
5267
  dispatchedBy: mySession,
4586
5268
  rootExe: exeSession2,
@@ -4655,15 +5337,15 @@ var init_tmux_routing = __esm({
4655
5337
  init_intercom_queue();
4656
5338
  init_plan_limits();
4657
5339
  init_employees();
4658
- SPAWN_LOCK_DIR = path14.join(os7.homedir(), ".exe-os", "spawn-locks");
4659
- SESSION_CACHE = path14.join(os7.homedir(), ".exe-os", "session-cache");
5340
+ SPAWN_LOCK_DIR = path15.join(os9.homedir(), ".exe-os", "spawn-locks");
5341
+ SESSION_CACHE = path15.join(os9.homedir(), ".exe-os", "session-cache");
4660
5342
  BEHAVIORS_EXPORT_TIMEOUT_MS = 1e4;
4661
5343
  VALID_SESSION_NAME = /^[a-z]+\d*-[a-zA-Z0-9_]+$/;
4662
5344
  VERIFY_PANE_LINES = 200;
4663
5345
  INTERCOM_DEBOUNCE_MS = 3e4;
4664
5346
  CODEX_DEBOUNCE_MS = 12e4;
4665
- INTERCOM_LOG2 = path14.join(os7.homedir(), ".exe-os", "intercom.log");
4666
- DEBOUNCE_FILE = path14.join(SESSION_CACHE, "intercom-debounce.json");
5347
+ INTERCOM_LOG2 = path15.join(os9.homedir(), ".exe-os", "intercom.log");
5348
+ DEBOUNCE_FILE = path15.join(SESSION_CACHE, "intercom-debounce.json");
4667
5349
  DEBOUNCE_CLEANUP_AGE_MS = 5 * 60 * 1e3;
4668
5350
  BUSY_PATTERN = /[✻✽✶✳·].*…|Running…|• Working|• Ran |• Explored|• Called|esc to interrupt/;
4669
5351
  }
@@ -4674,6 +5356,7 @@ var shard_manager_exports = {};
4674
5356
  __export(shard_manager_exports, {
4675
5357
  disposeShards: () => disposeShards,
4676
5358
  ensureShardSchema: () => ensureShardSchema,
5359
+ getOpenShardCount: () => getOpenShardCount,
4677
5360
  getReadyShardClient: () => getReadyShardClient,
4678
5361
  getShardClient: () => getShardClient,
4679
5362
  getShardsDir: () => getShardsDir,
@@ -4682,15 +5365,18 @@ __export(shard_manager_exports, {
4682
5365
  listShards: () => listShards,
4683
5366
  shardExists: () => shardExists
4684
5367
  });
4685
- import path16 from "path";
4686
- import { existsSync as existsSync13, mkdirSync as mkdirSync7, readdirSync as readdirSync4 } from "fs";
5368
+ import path17 from "path";
5369
+ import { existsSync as existsSync14, mkdirSync as mkdirSync7, readdirSync as readdirSync4 } from "fs";
4687
5370
  import { createClient as createClient2 } from "@libsql/client";
4688
5371
  function initShardManager(encryptionKey) {
4689
5372
  _encryptionKey = encryptionKey;
4690
- if (!existsSync13(SHARDS_DIR)) {
5373
+ if (!existsSync14(SHARDS_DIR)) {
4691
5374
  mkdirSync7(SHARDS_DIR, { recursive: true });
4692
5375
  }
4693
5376
  _shardingEnabled = true;
5377
+ if (_evictionTimer) clearInterval(_evictionTimer);
5378
+ _evictionTimer = setInterval(evictIdleShards, EVICTION_INTERVAL_MS);
5379
+ _evictionTimer.unref();
4694
5380
  }
4695
5381
  function isShardingEnabled() {
4696
5382
  return _shardingEnabled;
@@ -4707,21 +5393,28 @@ function getShardClient(projectName) {
4707
5393
  throw new Error(`Invalid project name for shard: "${projectName}"`);
4708
5394
  }
4709
5395
  const cached = _shards.get(safeName);
4710
- if (cached) return cached;
4711
- const dbPath = path16.join(SHARDS_DIR, `${safeName}.db`);
5396
+ if (cached) {
5397
+ _shardLastAccess.set(safeName, Date.now());
5398
+ return cached;
5399
+ }
5400
+ while (_shards.size >= MAX_OPEN_SHARDS) {
5401
+ evictLRU();
5402
+ }
5403
+ const dbPath = path17.join(SHARDS_DIR, `${safeName}.db`);
4712
5404
  const client = createClient2({
4713
5405
  url: `file:${dbPath}`,
4714
5406
  encryptionKey: _encryptionKey
4715
5407
  });
4716
5408
  _shards.set(safeName, client);
5409
+ _shardLastAccess.set(safeName, Date.now());
4717
5410
  return client;
4718
5411
  }
4719
5412
  function shardExists(projectName) {
4720
5413
  const safeName = projectName.replace(/[^a-zA-Z0-9_-]/g, "_");
4721
- return existsSync13(path16.join(SHARDS_DIR, `${safeName}.db`));
5414
+ return existsSync14(path17.join(SHARDS_DIR, `${safeName}.db`));
4722
5415
  }
4723
5416
  function listShards() {
4724
- if (!existsSync13(SHARDS_DIR)) return [];
5417
+ if (!existsSync14(SHARDS_DIR)) return [];
4725
5418
  return readdirSync4(SHARDS_DIR).filter((f) => f.endsWith(".db")).map((f) => f.replace(".db", ""));
4726
5419
  }
4727
5420
  async function ensureShardSchema(client) {
@@ -4773,6 +5466,8 @@ async function ensureShardSchema(client) {
4773
5466
  for (const col of [
4774
5467
  "ALTER TABLE memories ADD COLUMN task_id TEXT",
4775
5468
  "ALTER TABLE memories ADD COLUMN consolidated INTEGER NOT NULL DEFAULT 0",
5469
+ "ALTER TABLE memories ADD COLUMN author_device_id TEXT",
5470
+ "ALTER TABLE memories ADD COLUMN scope TEXT NOT NULL DEFAULT 'business'",
4776
5471
  "ALTER TABLE memories ADD COLUMN importance INTEGER DEFAULT 5",
4777
5472
  "ALTER TABLE memories ADD COLUMN status TEXT DEFAULT 'active'",
4778
5473
  "ALTER TABLE memories ADD COLUMN wiki_synced INTEGER DEFAULT 0",
@@ -4795,7 +5490,23 @@ async function ensureShardSchema(client) {
4795
5490
  // MS-11: draft staging, MS-6a: memory_type, MS-7: trajectory
4796
5491
  "ALTER TABLE memories ADD COLUMN draft INTEGER DEFAULT 0",
4797
5492
  "ALTER TABLE memories ADD COLUMN memory_type TEXT DEFAULT 'raw'",
4798
- "ALTER TABLE memories ADD COLUMN trajectory TEXT"
5493
+ "ALTER TABLE memories ADD COLUMN trajectory TEXT",
5494
+ // Metadata enrichment columns (must match database.ts)
5495
+ "ALTER TABLE memories ADD COLUMN intent TEXT",
5496
+ "ALTER TABLE memories ADD COLUMN outcome TEXT",
5497
+ "ALTER TABLE memories ADD COLUMN domain TEXT",
5498
+ "ALTER TABLE memories ADD COLUMN referenced_entities TEXT",
5499
+ "ALTER TABLE memories ADD COLUMN retrieval_count INTEGER DEFAULT 0",
5500
+ "ALTER TABLE memories ADD COLUMN chain_position TEXT",
5501
+ "ALTER TABLE memories ADD COLUMN review_status TEXT",
5502
+ "ALTER TABLE memories ADD COLUMN context_window_pct INTEGER",
5503
+ "ALTER TABLE memories ADD COLUMN file_paths TEXT",
5504
+ "ALTER TABLE memories ADD COLUMN commit_hash TEXT",
5505
+ "ALTER TABLE memories ADD COLUMN duration_ms INTEGER",
5506
+ "ALTER TABLE memories ADD COLUMN token_cost REAL",
5507
+ "ALTER TABLE memories ADD COLUMN audience TEXT",
5508
+ "ALTER TABLE memories ADD COLUMN language_type TEXT",
5509
+ "ALTER TABLE memories ADD COLUMN parent_memory_id TEXT"
4799
5510
  ]) {
4800
5511
  try {
4801
5512
  await client.execute(col);
@@ -4894,21 +5605,69 @@ async function getReadyShardClient(projectName) {
4894
5605
  await ensureShardSchema(client);
4895
5606
  return client;
4896
5607
  }
5608
+ function evictLRU() {
5609
+ let oldest = null;
5610
+ let oldestTime = Infinity;
5611
+ for (const [name, time] of _shardLastAccess) {
5612
+ if (time < oldestTime) {
5613
+ oldestTime = time;
5614
+ oldest = name;
5615
+ }
5616
+ }
5617
+ if (oldest) {
5618
+ const client = _shards.get(oldest);
5619
+ if (client) {
5620
+ client.close();
5621
+ }
5622
+ _shards.delete(oldest);
5623
+ _shardLastAccess.delete(oldest);
5624
+ }
5625
+ }
5626
+ function evictIdleShards() {
5627
+ const now = Date.now();
5628
+ const toEvict = [];
5629
+ for (const [name, lastAccess] of _shardLastAccess) {
5630
+ if (now - lastAccess > SHARD_IDLE_MS) {
5631
+ toEvict.push(name);
5632
+ }
5633
+ }
5634
+ for (const name of toEvict) {
5635
+ const client = _shards.get(name);
5636
+ if (client) {
5637
+ client.close();
5638
+ }
5639
+ _shards.delete(name);
5640
+ _shardLastAccess.delete(name);
5641
+ }
5642
+ }
5643
+ function getOpenShardCount() {
5644
+ return _shards.size;
5645
+ }
4897
5646
  function disposeShards() {
5647
+ if (_evictionTimer) {
5648
+ clearInterval(_evictionTimer);
5649
+ _evictionTimer = null;
5650
+ }
4898
5651
  for (const [, client] of _shards) {
4899
5652
  client.close();
4900
5653
  }
4901
5654
  _shards.clear();
5655
+ _shardLastAccess.clear();
4902
5656
  _shardingEnabled = false;
4903
5657
  _encryptionKey = null;
4904
5658
  }
4905
- var SHARDS_DIR, _shards, _encryptionKey, _shardingEnabled;
5659
+ var SHARDS_DIR, SHARD_IDLE_MS, MAX_OPEN_SHARDS, EVICTION_INTERVAL_MS, _shards, _shardLastAccess, _evictionTimer, _encryptionKey, _shardingEnabled;
4906
5660
  var init_shard_manager = __esm({
4907
5661
  "src/lib/shard-manager.ts"() {
4908
5662
  "use strict";
4909
5663
  init_config();
4910
- SHARDS_DIR = path16.join(EXE_AI_DIR, "shards");
5664
+ SHARDS_DIR = path17.join(EXE_AI_DIR, "shards");
5665
+ SHARD_IDLE_MS = 5 * 60 * 1e3;
5666
+ MAX_OPEN_SHARDS = 10;
5667
+ EVICTION_INTERVAL_MS = 60 * 1e3;
4911
5668
  _shards = /* @__PURE__ */ new Map();
5669
+ _shardLastAccess = /* @__PURE__ */ new Map();
5670
+ _evictionTimer = null;
4912
5671
  _encryptionKey = null;
4913
5672
  _shardingEnabled = false;
4914
5673
  }
@@ -5111,16 +5870,16 @@ init_database();
5111
5870
 
5112
5871
  // src/lib/keychain.ts
5113
5872
  import { readFile as readFile4, writeFile as writeFile5, unlink, mkdir as mkdir4, chmod as chmod2 } from "fs/promises";
5114
- import { existsSync as existsSync12 } from "fs";
5115
- import path15 from "path";
5116
- import os8 from "os";
5873
+ import { existsSync as existsSync13 } from "fs";
5874
+ import path16 from "path";
5875
+ import os10 from "os";
5117
5876
  var SERVICE = "exe-mem";
5118
5877
  var ACCOUNT = "master-key";
5119
5878
  function getKeyDir() {
5120
- return process.env.EXE_OS_DIR ?? process.env.EXE_MEM_DIR ?? path15.join(os8.homedir(), ".exe-os");
5879
+ return process.env.EXE_OS_DIR ?? process.env.EXE_MEM_DIR ?? path16.join(os10.homedir(), ".exe-os");
5121
5880
  }
5122
5881
  function getKeyPath() {
5123
- return path15.join(getKeyDir(), "master.key");
5882
+ return path16.join(getKeyDir(), "master.key");
5124
5883
  }
5125
5884
  async function tryKeytar() {
5126
5885
  try {
@@ -5141,9 +5900,9 @@ async function getMasterKey() {
5141
5900
  }
5142
5901
  }
5143
5902
  const keyPath = getKeyPath();
5144
- if (!existsSync12(keyPath)) {
5903
+ if (!existsSync13(keyPath)) {
5145
5904
  process.stderr.write(
5146
- `[keychain] Key not found at ${keyPath} (HOME=${os8.homedir()}, EXE_OS_DIR=${process.env.EXE_OS_DIR ?? "unset"})
5905
+ `[keychain] Key not found at ${keyPath} (HOME=${os10.homedir()}, EXE_OS_DIR=${process.env.EXE_OS_DIR ?? "unset"})
5147
5906
  `
5148
5907
  );
5149
5908
  return null;