@askexenow/exe-os 0.8.83 → 0.8.86

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 (103) hide show
  1. package/dist/bin/backfill-conversations.js +746 -595
  2. package/dist/bin/backfill-responses.js +745 -594
  3. package/dist/bin/backfill-vectors.js +312 -226
  4. package/dist/bin/cleanup-stale-review-tasks.js +154 -21
  5. package/dist/bin/cli.js +14678 -12676
  6. package/dist/bin/exe-agent-config.js +242 -0
  7. package/dist/bin/exe-agent.js +100 -91
  8. package/dist/bin/exe-assign.js +1003 -854
  9. package/dist/bin/exe-boot.js +1420 -485
  10. package/dist/bin/exe-call.js +10 -0
  11. package/dist/bin/exe-cloud.js +29 -6
  12. package/dist/bin/exe-dispatch.js +572 -271
  13. package/dist/bin/exe-doctor.js +403 -6
  14. package/dist/bin/exe-export-behaviors.js +175 -72
  15. package/dist/bin/exe-forget.js +102 -3
  16. package/dist/bin/exe-gateway.js +796 -292
  17. package/dist/bin/exe-healthcheck.js +134 -1
  18. package/dist/bin/exe-heartbeat.js +172 -36
  19. package/dist/bin/exe-kill.js +175 -72
  20. package/dist/bin/exe-launch-agent.js +189 -76
  21. package/dist/bin/exe-link.js +927 -82
  22. package/dist/bin/exe-new-employee.js +60 -8
  23. package/dist/bin/exe-pending-messages.js +151 -19
  24. package/dist/bin/exe-pending-notifications.js +97 -2
  25. package/dist/bin/exe-pending-reviews.js +155 -22
  26. package/dist/bin/exe-rename.js +564 -23
  27. package/dist/bin/exe-review.js +231 -73
  28. package/dist/bin/exe-search.js +995 -228
  29. package/dist/bin/exe-session-cleanup.js +4930 -1664
  30. package/dist/bin/exe-settings.js +20 -5
  31. package/dist/bin/exe-start-codex.js +2598 -0
  32. package/dist/bin/exe-start.sh +15 -3
  33. package/dist/bin/exe-status.js +154 -21
  34. package/dist/bin/exe-team.js +97 -2
  35. package/dist/bin/git-sweep.js +1180 -363
  36. package/dist/bin/graph-backfill.js +175 -72
  37. package/dist/bin/graph-export.js +175 -72
  38. package/dist/bin/install.js +60 -7
  39. package/dist/bin/list-providers.js +1 -0
  40. package/dist/bin/scan-tasks.js +1185 -367
  41. package/dist/bin/setup.js +914 -270
  42. package/dist/bin/shard-migrate.js +175 -72
  43. package/dist/bin/update.js +1 -0
  44. package/dist/bin/wiki-sync.js +175 -72
  45. package/dist/gateway/index.js +792 -285
  46. package/dist/hooks/bug-report-worker.js +445 -135
  47. package/dist/hooks/commit-complete.js +1178 -361
  48. package/dist/hooks/error-recall.js +994 -228
  49. package/dist/hooks/ingest-worker.js +1799 -1234
  50. package/dist/hooks/ingest.js +3 -0
  51. package/dist/hooks/instructions-loaded.js +707 -97
  52. package/dist/hooks/notification.js +699 -89
  53. package/dist/hooks/post-compact.js +757 -109
  54. package/dist/hooks/pre-compact.js +1061 -244
  55. package/dist/hooks/pre-tool-use.js +787 -130
  56. package/dist/hooks/prompt-ingest-worker.js +242 -101
  57. package/dist/hooks/prompt-submit.js +1121 -299
  58. package/dist/hooks/response-ingest-worker.js +242 -101
  59. package/dist/hooks/session-end.js +4063 -397
  60. package/dist/hooks/session-start.js +1071 -254
  61. package/dist/hooks/stop.js +768 -120
  62. package/dist/hooks/subagent-stop.js +757 -109
  63. package/dist/hooks/summary-worker.js +1706 -1011
  64. package/dist/index.js +1821 -1098
  65. package/dist/lib/agent-config.js +167 -0
  66. package/dist/lib/cloud-sync.js +932 -88
  67. package/dist/lib/consolidation.js +2 -1
  68. package/dist/lib/database.js +642 -87
  69. package/dist/lib/db-daemon-client.js +503 -0
  70. package/dist/lib/device-registry.js +547 -7
  71. package/dist/lib/embedder.js +14 -28
  72. package/dist/lib/employee-templates.js +84 -74
  73. package/dist/lib/employees.js +9 -0
  74. package/dist/lib/exe-daemon-client.js +16 -29
  75. package/dist/lib/exe-daemon.js +2733 -1575
  76. package/dist/lib/hybrid-search.js +995 -228
  77. package/dist/lib/identity.js +87 -67
  78. package/dist/lib/keychain.js +9 -1
  79. package/dist/lib/messaging.js +103 -40
  80. package/dist/lib/reminders.js +91 -74
  81. package/dist/lib/runtime-table.js +16 -0
  82. package/dist/lib/schedules.js +96 -2
  83. package/dist/lib/session-wrappers.js +22 -0
  84. package/dist/lib/skill-learning.js +103 -85
  85. package/dist/lib/store.js +234 -73
  86. package/dist/lib/tasks.js +348 -134
  87. package/dist/lib/tmux-routing.js +422 -208
  88. package/dist/lib/token-spend.js +273 -0
  89. package/dist/lib/ws-client.js +11 -0
  90. package/dist/mcp/server.js +5742 -696
  91. package/dist/mcp/tools/complete-reminder.js +94 -77
  92. package/dist/mcp/tools/create-reminder.js +94 -77
  93. package/dist/mcp/tools/create-task.js +375 -152
  94. package/dist/mcp/tools/deactivate-behavior.js +95 -77
  95. package/dist/mcp/tools/list-reminders.js +94 -77
  96. package/dist/mcp/tools/list-tasks.js +99 -31
  97. package/dist/mcp/tools/send-message.js +108 -45
  98. package/dist/mcp/tools/update-task.js +162 -77
  99. package/dist/runtime/index.js +1075 -258
  100. package/dist/tui/App.js +1333 -506
  101. package/package.json +6 -1
  102. package/src/commands/exe/agent-config.md +27 -0
  103. package/src/commands/exe/cc-doctor.md +10 -0
package/dist/index.js CHANGED
@@ -272,6 +272,7 @@ __export(employees_exports, {
272
272
  DEFAULT_COORDINATOR_TEMPLATE_NAME: () => DEFAULT_COORDINATOR_TEMPLATE_NAME,
273
273
  EMPLOYEES_PATH: () => EMPLOYEES_PATH,
274
274
  addEmployee: () => addEmployee,
275
+ baseAgentName: () => baseAgentName,
275
276
  canCoordinate: () => canCoordinate,
276
277
  getCoordinatorEmployee: () => getCoordinatorEmployee,
277
278
  getCoordinatorName: () => getCoordinatorName,
@@ -368,6 +369,14 @@ function hasRole(agentName, role) {
368
369
  const emp = getEmployee(employees, agentName);
369
370
  return emp ? emp.role.toLowerCase() === role.toLowerCase() : false;
370
371
  }
372
+ function baseAgentName(name, employees) {
373
+ const match = name.match(/^([a-zA-Z]+)\d+$/);
374
+ if (!match) return name;
375
+ const base = match[1];
376
+ const roster = employees ?? loadEmployeesSync();
377
+ if (getEmployee(roster, base)) return base;
378
+ return name;
379
+ }
371
380
  function isMultiInstance(agentName, employees) {
372
381
  const roster = employees ?? loadEmployeesSync();
373
382
  const emp = getEmployee(roster, agentName);
@@ -748,18 +757,69 @@ var init_provider_table = __esm({
748
757
  }
749
758
  });
750
759
 
751
- // src/lib/intercom-queue.ts
752
- import { readFileSync as readFileSync4, writeFileSync as writeFileSync3, renameSync as renameSync3, existsSync as existsSync4, mkdirSync as mkdirSync2 } from "fs";
760
+ // src/lib/runtime-table.ts
761
+ var RUNTIME_TABLE, DEFAULT_RUNTIME;
762
+ var init_runtime_table = __esm({
763
+ "src/lib/runtime-table.ts"() {
764
+ "use strict";
765
+ RUNTIME_TABLE = {
766
+ codex: {
767
+ binary: "codex",
768
+ launchMode: "exec",
769
+ autoApproveFlag: "--full-auto",
770
+ inlineFlag: "--no-alt-screen",
771
+ apiKeyEnv: "OPENAI_API_KEY",
772
+ defaultModel: "gpt-5.4"
773
+ }
774
+ };
775
+ DEFAULT_RUNTIME = "claude";
776
+ }
777
+ });
778
+
779
+ // src/lib/agent-config.ts
780
+ import { readFileSync as readFileSync4, writeFileSync as writeFileSync3, existsSync as existsSync4, mkdirSync as mkdirSync2 } from "fs";
753
781
  import path5 from "path";
782
+ function loadAgentConfig() {
783
+ if (!existsSync4(AGENT_CONFIG_PATH)) return {};
784
+ try {
785
+ return JSON.parse(readFileSync4(AGENT_CONFIG_PATH, "utf-8"));
786
+ } catch {
787
+ return {};
788
+ }
789
+ }
790
+ function getAgentRuntime(agentId) {
791
+ const config2 = loadAgentConfig();
792
+ const entry = config2[agentId];
793
+ if (entry) return entry;
794
+ return { runtime: DEFAULT_RUNTIME, model: DEFAULT_MODELS[DEFAULT_RUNTIME] };
795
+ }
796
+ var AGENT_CONFIG_PATH, DEFAULT_MODELS;
797
+ var init_agent_config = __esm({
798
+ "src/lib/agent-config.ts"() {
799
+ "use strict";
800
+ init_config();
801
+ init_runtime_table();
802
+ AGENT_CONFIG_PATH = path5.join(EXE_AI_DIR, "agent-config.json");
803
+ DEFAULT_MODELS = {
804
+ claude: "claude-opus-4",
805
+ codex: RUNTIME_TABLE.codex?.defaultModel ?? "gpt-5.4",
806
+ opencode: "minimax-m2.7"
807
+ };
808
+ }
809
+ });
810
+
811
+ // src/lib/intercom-queue.ts
812
+ import { readFileSync as readFileSync5, writeFileSync as writeFileSync4, renameSync as renameSync3, existsSync as existsSync5, mkdirSync as mkdirSync3 } from "fs";
813
+ import path6 from "path";
754
814
  import os5 from "os";
755
815
  function ensureDir() {
756
- const dir = path5.dirname(QUEUE_PATH);
757
- if (!existsSync4(dir)) mkdirSync2(dir, { recursive: true });
816
+ const dir = path6.dirname(QUEUE_PATH);
817
+ if (!existsSync5(dir)) mkdirSync3(dir, { recursive: true });
758
818
  }
759
819
  function readQueue() {
760
820
  try {
761
- if (!existsSync4(QUEUE_PATH)) return [];
762
- return JSON.parse(readFileSync4(QUEUE_PATH, "utf8"));
821
+ if (!existsSync5(QUEUE_PATH)) return [];
822
+ return JSON.parse(readFileSync5(QUEUE_PATH, "utf8"));
763
823
  } catch {
764
824
  return [];
765
825
  }
@@ -767,7 +827,7 @@ function readQueue() {
767
827
  function writeQueue(queue) {
768
828
  ensureDir();
769
829
  const tmp = `${QUEUE_PATH}.tmp`;
770
- writeFileSync3(tmp, JSON.stringify(queue, null, 2));
830
+ writeFileSync4(tmp, JSON.stringify(queue, null, 2));
771
831
  renameSync3(tmp, QUEUE_PATH);
772
832
  }
773
833
  function queueIntercom(targetSession, reason) {
@@ -791,9 +851,9 @@ var QUEUE_PATH, TTL_MS, INTERCOM_LOG;
791
851
  var init_intercom_queue = __esm({
792
852
  "src/lib/intercom-queue.ts"() {
793
853
  "use strict";
794
- QUEUE_PATH = path5.join(os5.homedir(), ".exe-os", "intercom-queue.json");
854
+ QUEUE_PATH = path6.join(os5.homedir(), ".exe-os", "intercom-queue.json");
795
855
  TTL_MS = 60 * 60 * 1e3;
796
- INTERCOM_LOG = path5.join(os5.homedir(), ".exe-os", "intercom.log");
856
+ INTERCOM_LOG = path6.join(os5.homedir(), ".exe-os", "intercom.log");
797
857
  }
798
858
  });
799
859
 
@@ -852,370 +912,923 @@ var init_db_retry = __esm({
852
912
  }
853
913
  });
854
914
 
855
- // src/lib/database.ts
856
- var database_exports = {};
857
- __export(database_exports, {
858
- disposeDatabase: () => disposeDatabase,
859
- disposeTurso: () => disposeTurso,
860
- ensureSchema: () => ensureSchema,
861
- getClient: () => getClient,
862
- getRawClient: () => getRawClient,
863
- initDatabase: () => initDatabase,
864
- initTurso: () => initTurso,
865
- isInitialized: () => isInitialized
866
- });
867
- import { createClient } from "@libsql/client";
868
- async function initDatabase(config2) {
869
- if (_client) {
870
- _client.close();
871
- _client = null;
872
- _resilientClient = null;
915
+ // src/lib/exe-daemon-client.ts
916
+ import net from "net";
917
+ import { spawn } from "child_process";
918
+ import { randomUUID as randomUUID2 } from "crypto";
919
+ import { existsSync as existsSync6, unlinkSync as unlinkSync2, readFileSync as readFileSync6, openSync, closeSync, statSync } from "fs";
920
+ import path7 from "path";
921
+ import { fileURLToPath } from "url";
922
+ function handleData(chunk) {
923
+ _buffer += chunk.toString();
924
+ if (_buffer.length > MAX_BUFFER) {
925
+ _buffer = "";
926
+ return;
873
927
  }
874
- const opts = {
875
- url: `file:${config2.dbPath}`
876
- };
877
- if (config2.encryptionKey) {
878
- opts.encryptionKey = config2.encryptionKey;
928
+ let newlineIdx;
929
+ while ((newlineIdx = _buffer.indexOf("\n")) !== -1) {
930
+ const line = _buffer.slice(0, newlineIdx).trim();
931
+ _buffer = _buffer.slice(newlineIdx + 1);
932
+ if (!line) continue;
933
+ try {
934
+ const response = JSON.parse(line);
935
+ const id = response.id;
936
+ if (!id) continue;
937
+ const entry = _pending.get(id);
938
+ if (entry) {
939
+ clearTimeout(entry.timer);
940
+ _pending.delete(id);
941
+ entry.resolve(response);
942
+ }
943
+ } catch {
944
+ }
879
945
  }
880
- _client = createClient(opts);
881
- _resilientClient = wrapWithRetry(_client);
882
- }
883
- function isInitialized() {
884
- return _client !== null;
885
946
  }
886
- function getClient() {
887
- if (!_resilientClient) {
888
- throw new Error("Database client not initialized. Call initDatabase() first.");
947
+ function cleanupStaleFiles() {
948
+ if (existsSync6(PID_PATH)) {
949
+ try {
950
+ const pid = parseInt(readFileSync6(PID_PATH, "utf8").trim(), 10);
951
+ if (pid > 0) {
952
+ try {
953
+ process.kill(pid, 0);
954
+ return;
955
+ } catch {
956
+ }
957
+ }
958
+ } catch {
959
+ }
960
+ try {
961
+ unlinkSync2(PID_PATH);
962
+ } catch {
963
+ }
964
+ try {
965
+ unlinkSync2(SOCKET_PATH);
966
+ } catch {
967
+ }
889
968
  }
890
- return _resilientClient;
891
969
  }
892
- function getRawClient() {
893
- if (!_client) {
894
- throw new Error("Database client not initialized. Call initDatabase() first.");
970
+ function findPackageRoot() {
971
+ let dir = path7.dirname(fileURLToPath(import.meta.url));
972
+ const { root } = path7.parse(dir);
973
+ while (dir !== root) {
974
+ if (existsSync6(path7.join(dir, "package.json"))) return dir;
975
+ dir = path7.dirname(dir);
895
976
  }
896
- return _client;
977
+ return null;
897
978
  }
898
- async function ensureSchema() {
899
- const client = getRawClient();
900
- await client.execute("PRAGMA journal_mode = WAL");
901
- await client.execute("PRAGMA busy_timeout = 30000");
902
- await client.execute("PRAGMA wal_autocheckpoint = 1000");
903
- try {
904
- await client.execute("PRAGMA libsql_vector_search_ef = 128");
905
- } catch {
906
- }
907
- await client.executeMultiple(`
908
- CREATE TABLE IF NOT EXISTS memories (
909
- id TEXT PRIMARY KEY,
910
- agent_id TEXT NOT NULL,
911
- agent_role TEXT NOT NULL,
912
- session_id TEXT NOT NULL,
913
- timestamp TEXT NOT NULL,
914
- tool_name TEXT NOT NULL,
915
- project_name TEXT NOT NULL,
916
- has_error INTEGER NOT NULL DEFAULT 0,
917
- raw_text TEXT NOT NULL,
918
- vector F32_BLOB(1024),
919
- version INTEGER NOT NULL DEFAULT 0
920
- );
921
-
922
- CREATE INDEX IF NOT EXISTS idx_memories_agent
923
- ON memories(agent_id);
924
-
925
- CREATE INDEX IF NOT EXISTS idx_memories_timestamp
926
- ON memories(timestamp);
927
-
928
- CREATE INDEX IF NOT EXISTS idx_memories_session
929
- ON memories(session_id);
930
-
931
- CREATE INDEX IF NOT EXISTS idx_memories_project
932
- ON memories(project_name);
933
-
934
- CREATE INDEX IF NOT EXISTS idx_memories_tool
935
- ON memories(tool_name);
936
-
937
- CREATE INDEX IF NOT EXISTS idx_memories_version
938
- ON memories(version);
939
-
940
- CREATE INDEX IF NOT EXISTS idx_memories_agent_project
941
- ON memories(agent_id, project_name);
942
- `);
943
- await client.executeMultiple(`
944
- CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts USING fts5(
945
- raw_text,
946
- content='memories',
947
- content_rowid='rowid'
948
- );
949
-
950
- CREATE TRIGGER IF NOT EXISTS memories_fts_ai AFTER INSERT ON memories BEGIN
951
- INSERT INTO memories_fts(rowid, raw_text) VALUES (new.rowid, new.raw_text);
952
- END;
953
-
954
- CREATE TRIGGER IF NOT EXISTS memories_fts_ad AFTER DELETE ON memories BEGIN
955
- INSERT INTO memories_fts(memories_fts, rowid, raw_text) VALUES('delete', old.rowid, old.raw_text);
956
- END;
957
-
958
- CREATE TRIGGER IF NOT EXISTS memories_fts_au AFTER UPDATE ON memories BEGIN
959
- INSERT INTO memories_fts(memories_fts, rowid, raw_text) VALUES('delete', old.rowid, old.raw_text);
960
- INSERT INTO memories_fts(rowid, raw_text) VALUES (new.rowid, new.raw_text);
961
- END;
962
- `);
963
- await client.executeMultiple(`
964
- CREATE TABLE IF NOT EXISTS sync_meta (
965
- key TEXT PRIMARY KEY,
966
- value TEXT NOT NULL
967
- );
968
- `);
969
- await client.executeMultiple(`
970
- CREATE TABLE IF NOT EXISTS tasks (
971
- id TEXT PRIMARY KEY,
972
- title TEXT NOT NULL,
973
- assigned_to TEXT NOT NULL,
974
- assigned_by TEXT NOT NULL,
975
- project_name TEXT NOT NULL,
976
- priority TEXT NOT NULL DEFAULT 'p1',
977
- status TEXT NOT NULL DEFAULT 'open',
978
- task_file TEXT,
979
- created_at TEXT NOT NULL,
980
- updated_at TEXT NOT NULL
981
- );
982
-
983
- CREATE INDEX IF NOT EXISTS idx_tasks_assignee_status
984
- ON tasks(assigned_to, status);
985
- `);
986
- await client.executeMultiple(`
987
- CREATE TABLE IF NOT EXISTS behaviors (
988
- id TEXT PRIMARY KEY,
989
- agent_id TEXT NOT NULL,
990
- project_name TEXT,
991
- domain TEXT,
992
- content TEXT NOT NULL,
993
- active INTEGER NOT NULL DEFAULT 1,
994
- created_at TEXT NOT NULL,
995
- updated_at TEXT NOT NULL
996
- );
997
-
998
- CREATE INDEX IF NOT EXISTS idx_behaviors_agent
999
- ON behaviors(agent_id, active);
1000
- `);
1001
- try {
1002
- const coordinatorName = getCoordinatorName();
1003
- const existing = await client.execute({
1004
- sql: "SELECT COUNT(*) as cnt FROM behaviors WHERE agent_id = ?",
1005
- args: [coordinatorName]
1006
- });
1007
- if (Number(existing.rows[0]?.cnt) === 0) {
1008
- const seededAt = "2026-03-25T00:00:00Z";
1009
- for (const [domain, content] of [
1010
- ["workflow", `Don't ask "keep going?" \u2014 just keep executing phases/plans autonomously`],
1011
- ["tool-use", "Always use create_task MCP tool, never write .md files directly for task creation"],
1012
- ["workflow", "Auto-start reviewing when idle and reviews are pending \u2014 never ask founder for permission"]
1013
- ]) {
1014
- await client.execute({
1015
- sql: `INSERT INTO behaviors (id, agent_id, project_name, domain, content, active, created_at, updated_at)
1016
- VALUES (hex(randomblob(16)), ?, NULL, ?, ?, 1, ?, ?)`,
1017
- args: [coordinatorName, domain, content, seededAt, seededAt]
1018
- });
1019
- }
1020
- }
1021
- } catch {
979
+ function spawnDaemon() {
980
+ const pkgRoot = findPackageRoot();
981
+ if (!pkgRoot) {
982
+ process.stderr.write("[exed-client] WARN: cannot find package root\n");
983
+ return;
1022
984
  }
1023
- try {
1024
- await client.execute({
1025
- sql: `ALTER TABLE behaviors ADD COLUMN priority TEXT DEFAULT 'p1'`,
1026
- args: []
1027
- });
1028
- } catch {
985
+ const daemonPath = path7.join(pkgRoot, "dist", "lib", "exe-daemon.js");
986
+ if (!existsSync6(daemonPath)) {
987
+ process.stderr.write(`[exed-client] WARN: daemon script not found at ${daemonPath}
988
+ `);
989
+ return;
1029
990
  }
991
+ const resolvedPath = daemonPath;
992
+ process.stderr.write(`[exed-client] Spawning daemon: ${resolvedPath}
993
+ `);
994
+ const logPath = path7.join(path7.dirname(SOCKET_PATH), "exed.log");
995
+ let stderrFd = "ignore";
1030
996
  try {
1031
- await client.execute({
1032
- sql: `ALTER TABLE tasks ADD COLUMN blocked_by TEXT`,
1033
- args: []
1034
- });
997
+ stderrFd = openSync(logPath, "a");
1035
998
  } catch {
1036
999
  }
1037
- try {
1038
- await client.execute({
1039
- sql: `ALTER TABLE tasks ADD COLUMN parent_task_id TEXT`,
1040
- args: []
1041
- });
1042
- } catch {
1043
- }
1044
- try {
1045
- await client.execute({
1046
- sql: `CREATE INDEX IF NOT EXISTS idx_tasks_parent_task_id
1047
- ON tasks(parent_task_id)
1048
- WHERE parent_task_id IS NOT NULL`,
1049
- args: []
1050
- });
1051
- } catch {
1000
+ const child = spawn(process.execPath, [resolvedPath], {
1001
+ detached: true,
1002
+ stdio: ["ignore", "ignore", stderrFd],
1003
+ env: {
1004
+ ...process.env,
1005
+ TMUX: void 0,
1006
+ // Daemon is global — must not inherit session scope
1007
+ TMUX_PANE: void 0,
1008
+ // Prevents resolveExeSession() from scoping to one session
1009
+ EXE_DAEMON_SOCK: SOCKET_PATH,
1010
+ EXE_DAEMON_PID: PID_PATH
1011
+ }
1012
+ });
1013
+ child.unref();
1014
+ if (typeof stderrFd === "number") {
1015
+ try {
1016
+ closeSync(stderrFd);
1017
+ } catch {
1018
+ }
1052
1019
  }
1020
+ }
1021
+ function acquireSpawnLock() {
1053
1022
  try {
1054
- await client.execute({
1055
- sql: `UPDATE tasks SET status = 'done' WHERE status = 'completed'`,
1056
- args: []
1057
- });
1023
+ const fd = openSync(SPAWN_LOCK_PATH, "wx");
1024
+ closeSync(fd);
1025
+ return true;
1058
1026
  } catch {
1027
+ try {
1028
+ const stat = statSync(SPAWN_LOCK_PATH);
1029
+ if (Date.now() - stat.mtimeMs > SPAWN_LOCK_STALE_MS) {
1030
+ try {
1031
+ unlinkSync2(SPAWN_LOCK_PATH);
1032
+ } catch {
1033
+ }
1034
+ try {
1035
+ const fd = openSync(SPAWN_LOCK_PATH, "wx");
1036
+ closeSync(fd);
1037
+ return true;
1038
+ } catch {
1039
+ }
1040
+ }
1041
+ } catch {
1042
+ }
1043
+ return false;
1059
1044
  }
1045
+ }
1046
+ function releaseSpawnLock() {
1060
1047
  try {
1061
- await client.execute({
1062
- sql: `ALTER TABLE tasks ADD COLUMN reviewer TEXT`,
1063
- args: []
1064
- });
1048
+ unlinkSync2(SPAWN_LOCK_PATH);
1065
1049
  } catch {
1066
1050
  }
1067
- try {
1068
- await client.execute({
1069
- sql: `ALTER TABLE tasks ADD COLUMN context TEXT`,
1070
- args: []
1051
+ }
1052
+ function connectToSocket() {
1053
+ return new Promise((resolve) => {
1054
+ if (_socket && _connected) {
1055
+ resolve(true);
1056
+ return;
1057
+ }
1058
+ const socket = net.createConnection({ path: SOCKET_PATH });
1059
+ const connectTimeout = setTimeout(() => {
1060
+ socket.destroy();
1061
+ resolve(false);
1062
+ }, 2e3);
1063
+ socket.on("connect", () => {
1064
+ clearTimeout(connectTimeout);
1065
+ _socket = socket;
1066
+ _connected = true;
1067
+ _buffer = "";
1068
+ socket.on("data", handleData);
1069
+ socket.on("close", () => {
1070
+ _connected = false;
1071
+ _socket = null;
1072
+ for (const [id, entry] of _pending) {
1073
+ clearTimeout(entry.timer);
1074
+ _pending.delete(id);
1075
+ entry.resolve({ error: "Connection closed" });
1076
+ }
1077
+ });
1078
+ socket.on("error", () => {
1079
+ _connected = false;
1080
+ _socket = null;
1081
+ });
1082
+ resolve(true);
1071
1083
  });
1072
- } catch {
1073
- }
1074
- try {
1075
- await client.execute({
1076
- sql: `ALTER TABLE tasks ADD COLUMN result TEXT`,
1077
- args: []
1084
+ socket.on("error", () => {
1085
+ clearTimeout(connectTimeout);
1086
+ resolve(false);
1078
1087
  });
1079
- } catch {
1088
+ });
1089
+ }
1090
+ async function connectEmbedDaemon() {
1091
+ if (_socket && _connected) return true;
1092
+ if (await connectToSocket()) return true;
1093
+ if (acquireSpawnLock()) {
1094
+ try {
1095
+ cleanupStaleFiles();
1096
+ spawnDaemon();
1097
+ } finally {
1098
+ releaseSpawnLock();
1099
+ }
1080
1100
  }
1081
- try {
1082
- await client.execute({
1083
- sql: `ALTER TABLE tasks ADD COLUMN assigned_tmux TEXT`,
1084
- args: []
1085
- });
1086
- } catch {
1101
+ const start = Date.now();
1102
+ let delay2 = 100;
1103
+ while (Date.now() - start < CONNECT_TIMEOUT_MS) {
1104
+ await new Promise((r) => setTimeout(r, delay2));
1105
+ if (await connectToSocket()) return true;
1106
+ delay2 = Math.min(delay2 * 2, 3e3);
1087
1107
  }
1088
- try {
1089
- await client.execute({
1090
- sql: `ALTER TABLE tasks ADD COLUMN checkpoint TEXT`,
1091
- args: []
1092
- });
1093
- } catch {
1108
+ return false;
1109
+ }
1110
+ function sendRequest(texts, priority) {
1111
+ return sendDaemonRequest({ texts, priority });
1112
+ }
1113
+ function sendDaemonRequest(payload, timeoutMs = REQUEST_TIMEOUT_MS) {
1114
+ return new Promise((resolve) => {
1115
+ if (!_socket || !_connected) {
1116
+ resolve({ error: "Not connected" });
1117
+ return;
1118
+ }
1119
+ const id = randomUUID2();
1120
+ const timer = setTimeout(() => {
1121
+ _pending.delete(id);
1122
+ resolve({ error: "Request timeout" });
1123
+ }, timeoutMs);
1124
+ _pending.set(id, { resolve, timer });
1125
+ try {
1126
+ _socket.write(JSON.stringify({ id, ...payload }) + "\n");
1127
+ } catch {
1128
+ clearTimeout(timer);
1129
+ _pending.delete(id);
1130
+ resolve({ error: "Write failed" });
1131
+ }
1132
+ });
1133
+ }
1134
+ async function pingDaemon() {
1135
+ if (!_socket || !_connected) return null;
1136
+ const response = await sendDaemonRequest({ type: "health" }, 5e3);
1137
+ if (response.health) {
1138
+ return response.health;
1094
1139
  }
1095
- try {
1096
- await client.execute({
1097
- sql: `ALTER TABLE tasks ADD COLUMN checkpoint_count INTEGER NOT NULL DEFAULT 0`,
1098
- args: []
1099
- });
1100
- } catch {
1140
+ return null;
1141
+ }
1142
+ function killAndRespawnDaemon() {
1143
+ process.stderr.write("[exed-client] Killing daemon for restart...\n");
1144
+ if (existsSync6(PID_PATH)) {
1145
+ try {
1146
+ const pid = parseInt(readFileSync6(PID_PATH, "utf8").trim(), 10);
1147
+ if (pid > 0) {
1148
+ try {
1149
+ process.kill(pid, "SIGKILL");
1150
+ } catch {
1151
+ }
1152
+ }
1153
+ } catch {
1154
+ }
1101
1155
  }
1102
- try {
1103
- await client.execute({
1104
- sql: `ALTER TABLE tasks ADD COLUMN complexity TEXT NOT NULL DEFAULT 'standard'`,
1105
- args: []
1106
- });
1107
- } catch {
1156
+ if (_socket) {
1157
+ _socket.destroy();
1158
+ _socket = null;
1108
1159
  }
1160
+ _connected = false;
1161
+ _buffer = "";
1109
1162
  try {
1110
- await client.execute({
1111
- sql: `ALTER TABLE tasks ADD COLUMN session_scope TEXT`,
1112
- args: []
1113
- });
1163
+ unlinkSync2(PID_PATH);
1114
1164
  } catch {
1115
1165
  }
1116
1166
  try {
1117
- await client.execute({
1118
- sql: `ALTER TABLE memories ADD COLUMN task_id TEXT`,
1119
- args: []
1120
- });
1167
+ unlinkSync2(SOCKET_PATH);
1121
1168
  } catch {
1122
1169
  }
1123
- try {
1124
- await client.execute({
1125
- sql: `ALTER TABLE memories ADD COLUMN consolidated INTEGER NOT NULL DEFAULT 0`,
1126
- args: []
1127
- });
1128
- } catch {
1170
+ spawnDaemon();
1171
+ }
1172
+ async function embedViaClient(text, priority = "high") {
1173
+ if (!_connected && !await connectEmbedDaemon()) return null;
1174
+ _requestCount++;
1175
+ if (_requestCount % HEALTH_CHECK_INTERVAL === 0) {
1176
+ const health = await pingDaemon();
1177
+ if (!health) {
1178
+ process.stderr.write(`[exed-client] Periodic health check failed at request ${_requestCount} \u2014 restarting daemon
1179
+ `);
1180
+ killAndRespawnDaemon();
1181
+ const start = Date.now();
1182
+ let delay2 = 200;
1183
+ while (Date.now() - start < CONNECT_TIMEOUT_MS) {
1184
+ await new Promise((r) => setTimeout(r, delay2));
1185
+ if (await connectToSocket()) break;
1186
+ delay2 = Math.min(delay2 * 2, 3e3);
1187
+ }
1188
+ if (!_connected) return null;
1189
+ }
1129
1190
  }
1130
- try {
1131
- await client.execute({
1132
- sql: `ALTER TABLE memories ADD COLUMN author_device_id TEXT`,
1133
- args: []
1134
- });
1135
- } catch {
1191
+ const result = await sendRequest([text], priority);
1192
+ if (!result.error && result.vectors?.[0]) return result.vectors[0];
1193
+ if (result.error) {
1194
+ process.stderr.write(`[exed-client] Embed failed (${result.error}) \u2014 attempting restart
1195
+ `);
1196
+ killAndRespawnDaemon();
1197
+ const start = Date.now();
1198
+ let delay2 = 200;
1199
+ while (Date.now() - start < CONNECT_TIMEOUT_MS) {
1200
+ await new Promise((r) => setTimeout(r, delay2));
1201
+ if (await connectToSocket()) break;
1202
+ delay2 = Math.min(delay2 * 2, 3e3);
1203
+ }
1204
+ if (!_connected) return null;
1205
+ const retry = await sendRequest([text], priority);
1206
+ if (!retry.error && retry.vectors?.[0]) return retry.vectors[0];
1207
+ process.stderr.write(`[exed-client] Embed retry also failed: ${retry.error ?? "no vector"}
1208
+ `);
1136
1209
  }
1137
- try {
1138
- await client.execute({
1139
- sql: `ALTER TABLE memories ADD COLUMN scope TEXT NOT NULL DEFAULT 'business'`,
1140
- args: []
1141
- });
1142
- } catch {
1210
+ return null;
1211
+ }
1212
+ function disconnectClient() {
1213
+ if (_socket) {
1214
+ _socket.destroy();
1215
+ _socket = null;
1143
1216
  }
1144
- await client.executeMultiple(`
1145
- CREATE TABLE IF NOT EXISTS consolidations (
1146
- id TEXT PRIMARY KEY,
1147
- consolidated_memory_id TEXT NOT NULL,
1148
- source_memory_id TEXT NOT NULL,
1149
- created_at TEXT NOT NULL
1217
+ _connected = false;
1218
+ _buffer = "";
1219
+ for (const [id, entry] of _pending) {
1220
+ clearTimeout(entry.timer);
1221
+ _pending.delete(id);
1222
+ entry.resolve({ error: "Client disconnected" });
1223
+ }
1224
+ }
1225
+ function isClientConnected() {
1226
+ return _connected;
1227
+ }
1228
+ var SOCKET_PATH, PID_PATH, SPAWN_LOCK_PATH, SPAWN_LOCK_STALE_MS, CONNECT_TIMEOUT_MS, REQUEST_TIMEOUT_MS, _socket, _connected, _buffer, _requestCount, HEALTH_CHECK_INTERVAL, _pending, MAX_BUFFER;
1229
+ var init_exe_daemon_client = __esm({
1230
+ "src/lib/exe-daemon-client.ts"() {
1231
+ "use strict";
1232
+ init_config();
1233
+ SOCKET_PATH = process.env.EXE_DAEMON_SOCK ?? process.env.EXE_EMBED_SOCK ?? path7.join(EXE_AI_DIR, "exed.sock");
1234
+ PID_PATH = process.env.EXE_DAEMON_PID ?? process.env.EXE_EMBED_PID ?? path7.join(EXE_AI_DIR, "exed.pid");
1235
+ SPAWN_LOCK_PATH = path7.join(EXE_AI_DIR, "exed-spawn.lock");
1236
+ SPAWN_LOCK_STALE_MS = 3e4;
1237
+ CONNECT_TIMEOUT_MS = 15e3;
1238
+ REQUEST_TIMEOUT_MS = 3e4;
1239
+ _socket = null;
1240
+ _connected = false;
1241
+ _buffer = "";
1242
+ _requestCount = 0;
1243
+ HEALTH_CHECK_INTERVAL = 100;
1244
+ _pending = /* @__PURE__ */ new Map();
1245
+ MAX_BUFFER = 1e7;
1246
+ }
1247
+ });
1248
+
1249
+ // src/lib/daemon-protocol.ts
1250
+ function serializeValue(v) {
1251
+ if (v === null || v === void 0) return null;
1252
+ if (typeof v === "bigint") return Number(v);
1253
+ if (typeof v === "boolean") return v ? 1 : 0;
1254
+ if (v instanceof Uint8Array) {
1255
+ return { __blob: Buffer.from(v).toString("base64") };
1256
+ }
1257
+ if (ArrayBuffer.isView(v)) {
1258
+ return { __blob: Buffer.from(v.buffer, v.byteOffset, v.byteLength).toString("base64") };
1259
+ }
1260
+ if (v instanceof ArrayBuffer) {
1261
+ return { __blob: Buffer.from(v).toString("base64") };
1262
+ }
1263
+ if (typeof v === "string" || typeof v === "number") return v;
1264
+ return String(v);
1265
+ }
1266
+ function deserializeValue(v) {
1267
+ if (v === null) return null;
1268
+ if (typeof v === "object" && v !== null && "__blob" in v) {
1269
+ const buf = Buffer.from(v.__blob, "base64");
1270
+ return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength);
1271
+ }
1272
+ return v;
1273
+ }
1274
+ function deserializeResultSet(srs) {
1275
+ const rows = srs.rows.map((obj) => {
1276
+ const values = srs.columns.map(
1277
+ (col) => deserializeValue(obj[col] ?? null)
1150
1278
  );
1279
+ const row = values;
1280
+ for (let i = 0; i < srs.columns.length; i++) {
1281
+ const col = srs.columns[i];
1282
+ if (col !== void 0) {
1283
+ row[col] = values[i] ?? null;
1284
+ }
1285
+ }
1286
+ Object.defineProperty(row, "length", {
1287
+ value: values.length,
1288
+ enumerable: false
1289
+ });
1290
+ return row;
1291
+ });
1292
+ return {
1293
+ columns: srs.columns,
1294
+ columnTypes: srs.columnTypes ?? [],
1295
+ rows,
1296
+ rowsAffected: srs.rowsAffected,
1297
+ lastInsertRowid: srs.lastInsertRowid != null ? BigInt(srs.lastInsertRowid) : void 0,
1298
+ toJSON: () => ({
1299
+ columns: srs.columns,
1300
+ columnTypes: srs.columnTypes ?? [],
1301
+ rows: srs.rows,
1302
+ rowsAffected: srs.rowsAffected,
1303
+ lastInsertRowid: srs.lastInsertRowid
1304
+ })
1305
+ };
1306
+ }
1307
+ var init_daemon_protocol = __esm({
1308
+ "src/lib/daemon-protocol.ts"() {
1309
+ "use strict";
1310
+ }
1311
+ });
1151
1312
 
1152
- CREATE INDEX IF NOT EXISTS idx_consolidations_source
1153
- ON consolidations(source_memory_id);
1313
+ // src/lib/db-daemon-client.ts
1314
+ var db_daemon_client_exports = {};
1315
+ __export(db_daemon_client_exports, {
1316
+ createDaemonDbClient: () => createDaemonDbClient,
1317
+ initDaemonDbClient: () => initDaemonDbClient
1318
+ });
1319
+ function normalizeStatement(stmt) {
1320
+ if (typeof stmt === "string") {
1321
+ return { sql: stmt, args: [] };
1322
+ }
1323
+ const sql = stmt.sql;
1324
+ let args = [];
1325
+ if (Array.isArray(stmt.args)) {
1326
+ args = stmt.args.map((v) => serializeValue(v));
1327
+ } else if (stmt.args && typeof stmt.args === "object") {
1328
+ const named = {};
1329
+ for (const [key, val] of Object.entries(stmt.args)) {
1330
+ named[key] = serializeValue(val);
1331
+ }
1332
+ return { sql, args: named };
1333
+ }
1334
+ return { sql, args };
1335
+ }
1336
+ function createDaemonDbClient(fallbackClient) {
1337
+ let _useDaemon = false;
1338
+ const client = {
1339
+ async execute(stmt) {
1340
+ if (!_useDaemon || !isClientConnected()) {
1341
+ return fallbackClient.execute(stmt);
1342
+ }
1343
+ const { sql, args } = normalizeStatement(stmt);
1344
+ const response = await sendDaemonRequest({
1345
+ type: "db-execute",
1346
+ sql,
1347
+ args
1348
+ });
1349
+ if (response.error) {
1350
+ const errMsg = String(response.error);
1351
+ if (errMsg === "Not connected" || errMsg === "Request timeout" || errMsg === "Write failed") {
1352
+ process.stderr.write(`[db-daemon] Transport error (${errMsg}), falling back to direct
1353
+ `);
1354
+ return fallbackClient.execute(stmt);
1355
+ }
1356
+ throw new Error(errMsg);
1357
+ }
1358
+ if (response.db) {
1359
+ return deserializeResultSet(response.db);
1360
+ }
1361
+ process.stderr.write("[db-daemon] Unexpected response shape, falling back to direct\n");
1362
+ return fallbackClient.execute(stmt);
1363
+ },
1364
+ async batch(stmts, mode) {
1365
+ if (!_useDaemon || !isClientConnected()) {
1366
+ return fallbackClient.batch(stmts, mode);
1367
+ }
1368
+ const statements = stmts.map(normalizeStatement);
1369
+ const response = await sendDaemonRequest({
1370
+ type: "db-batch",
1371
+ statements,
1372
+ mode: mode ?? "deferred"
1373
+ });
1374
+ if (response.error) {
1375
+ const errMsg = String(response.error);
1376
+ if (errMsg === "Not connected" || errMsg === "Request timeout" || errMsg === "Write failed") {
1377
+ process.stderr.write(`[db-daemon] Batch transport error (${errMsg}), falling back to direct
1378
+ `);
1379
+ return fallbackClient.batch(stmts, mode);
1380
+ }
1381
+ throw new Error(errMsg);
1382
+ }
1383
+ const batchResults = response["db-batch"];
1384
+ if (batchResults) {
1385
+ return batchResults.map(deserializeResultSet);
1386
+ }
1387
+ process.stderr.write("[db-daemon] Unexpected batch response shape, falling back to direct\n");
1388
+ return fallbackClient.batch(stmts, mode);
1389
+ },
1390
+ // Transaction support — delegate to fallback (transactions need direct connection)
1391
+ async transaction(mode) {
1392
+ return fallbackClient.transaction(mode);
1393
+ },
1394
+ // executeMultiple — delegate to fallback (used only for schema migrations)
1395
+ async executeMultiple(sql) {
1396
+ return fallbackClient.executeMultiple(sql);
1397
+ },
1398
+ // migrate — delegate to fallback
1399
+ async migrate(stmts) {
1400
+ return fallbackClient.migrate(stmts);
1401
+ },
1402
+ // Sync mode — delegate to fallback
1403
+ sync() {
1404
+ return fallbackClient.sync();
1405
+ },
1406
+ close() {
1407
+ _useDaemon = false;
1408
+ },
1409
+ get closed() {
1410
+ return fallbackClient.closed;
1411
+ },
1412
+ get protocol() {
1413
+ return fallbackClient.protocol;
1414
+ }
1415
+ };
1416
+ return {
1417
+ ...client,
1418
+ /** Enable daemon routing (call after confirming daemon is connected) */
1419
+ _enableDaemon() {
1420
+ _useDaemon = true;
1421
+ },
1422
+ /** Check if daemon routing is active */
1423
+ _isDaemonActive() {
1424
+ return _useDaemon && isClientConnected();
1425
+ }
1426
+ };
1427
+ }
1428
+ async function initDaemonDbClient(fallbackClient) {
1429
+ if (process.env.EXE_IS_DAEMON === "1") return null;
1430
+ const connected = await connectEmbedDaemon();
1431
+ if (!connected) {
1432
+ process.stderr.write("[db-daemon] Daemon unavailable \u2014 using direct SQLite\n");
1433
+ return null;
1434
+ }
1435
+ const client = createDaemonDbClient(fallbackClient);
1436
+ client._enableDaemon();
1437
+ process.stderr.write("[db-daemon] DB routing through daemon (single-writer)\n");
1438
+ return client;
1439
+ }
1440
+ var init_db_daemon_client = __esm({
1441
+ "src/lib/db-daemon-client.ts"() {
1442
+ "use strict";
1443
+ init_exe_daemon_client();
1444
+ init_daemon_protocol();
1445
+ }
1446
+ });
1154
1447
 
1155
- CREATE INDEX IF NOT EXISTS idx_consolidations_consolidated
1156
- ON consolidations(consolidated_memory_id);
1157
- `);
1448
+ // src/lib/database.ts
1449
+ var database_exports = {};
1450
+ __export(database_exports, {
1451
+ disposeDatabase: () => disposeDatabase,
1452
+ disposeTurso: () => disposeTurso,
1453
+ ensureSchema: () => ensureSchema,
1454
+ getClient: () => getClient,
1455
+ getRawClient: () => getRawClient,
1456
+ initDaemonClient: () => initDaemonClient,
1457
+ initDatabase: () => initDatabase,
1458
+ initTurso: () => initTurso,
1459
+ isInitialized: () => isInitialized
1460
+ });
1461
+ import { createClient } from "@libsql/client";
1462
+ async function initDatabase(config2) {
1463
+ if (_client) {
1464
+ _client.close();
1465
+ _client = null;
1466
+ _resilientClient = null;
1467
+ }
1468
+ const opts = {
1469
+ url: `file:${config2.dbPath}`
1470
+ };
1471
+ if (config2.encryptionKey) {
1472
+ opts.encryptionKey = config2.encryptionKey;
1473
+ }
1474
+ _client = createClient(opts);
1475
+ _resilientClient = wrapWithRetry(_client);
1476
+ }
1477
+ function isInitialized() {
1478
+ return _client !== null;
1479
+ }
1480
+ function getClient() {
1481
+ if (!_resilientClient) {
1482
+ throw new Error("Database client not initialized. Call initDatabase() first.");
1483
+ }
1484
+ if (process.env.EXE_IS_DAEMON === "1") {
1485
+ return _resilientClient;
1486
+ }
1487
+ if (_daemonClient && _daemonClient._isDaemonActive()) {
1488
+ return _daemonClient;
1489
+ }
1490
+ return _resilientClient;
1491
+ }
1492
+ async function initDaemonClient() {
1493
+ if (process.env.EXE_IS_DAEMON === "1") return;
1494
+ if (!_resilientClient) return;
1495
+ try {
1496
+ const { initDaemonDbClient: initDaemonDbClient2 } = await Promise.resolve().then(() => (init_db_daemon_client(), db_daemon_client_exports));
1497
+ _daemonClient = await initDaemonDbClient2(_resilientClient);
1498
+ } catch (err) {
1499
+ process.stderr.write(
1500
+ `[database] Daemon client init failed (non-fatal): ${err instanceof Error ? err.message : String(err)}
1501
+ `
1502
+ );
1503
+ }
1504
+ }
1505
+ function getRawClient() {
1506
+ if (!_client) {
1507
+ throw new Error("Database client not initialized. Call initDatabase() first.");
1508
+ }
1509
+ return _client;
1510
+ }
1511
+ async function ensureSchema() {
1512
+ const client = getRawClient();
1513
+ await client.execute("PRAGMA journal_mode = WAL");
1514
+ await client.execute("PRAGMA busy_timeout = 30000");
1515
+ await client.execute("PRAGMA wal_autocheckpoint = 1000");
1516
+ try {
1517
+ await client.execute("PRAGMA libsql_vector_search_ef = 128");
1518
+ } catch {
1519
+ }
1158
1520
  await client.executeMultiple(`
1159
- CREATE TABLE IF NOT EXISTS reminders (
1521
+ CREATE TABLE IF NOT EXISTS memories (
1160
1522
  id TEXT PRIMARY KEY,
1161
- text TEXT NOT NULL,
1162
- created_at TEXT NOT NULL,
1163
- due_date TEXT,
1164
- completed_at TEXT
1523
+ agent_id TEXT NOT NULL,
1524
+ agent_role TEXT NOT NULL,
1525
+ session_id TEXT NOT NULL,
1526
+ timestamp TEXT NOT NULL,
1527
+ tool_name TEXT NOT NULL,
1528
+ project_name TEXT NOT NULL,
1529
+ has_error INTEGER NOT NULL DEFAULT 0,
1530
+ raw_text TEXT NOT NULL,
1531
+ vector F32_BLOB(1024),
1532
+ version INTEGER NOT NULL DEFAULT 0
1165
1533
  );
1534
+
1535
+ CREATE INDEX IF NOT EXISTS idx_memories_agent
1536
+ ON memories(agent_id);
1537
+
1538
+ CREATE INDEX IF NOT EXISTS idx_memories_timestamp
1539
+ ON memories(timestamp);
1540
+
1541
+ CREATE INDEX IF NOT EXISTS idx_memories_session
1542
+ ON memories(session_id);
1543
+
1544
+ CREATE INDEX IF NOT EXISTS idx_memories_project
1545
+ ON memories(project_name);
1546
+
1547
+ CREATE INDEX IF NOT EXISTS idx_memories_tool
1548
+ ON memories(tool_name);
1549
+
1550
+ CREATE INDEX IF NOT EXISTS idx_memories_version
1551
+ ON memories(version);
1552
+
1553
+ CREATE INDEX IF NOT EXISTS idx_memories_agent_project
1554
+ ON memories(agent_id, project_name);
1166
1555
  `);
1167
1556
  await client.executeMultiple(`
1168
- CREATE TABLE IF NOT EXISTS notifications (
1169
- id TEXT PRIMARY KEY,
1170
- agent_id TEXT NOT NULL,
1171
- agent_role TEXT NOT NULL,
1172
- event TEXT NOT NULL,
1173
- project TEXT NOT NULL,
1174
- summary TEXT NOT NULL,
1175
- task_file TEXT,
1176
- read INTEGER NOT NULL DEFAULT 0,
1177
- created_at TEXT NOT NULL
1557
+ CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts USING fts5(
1558
+ raw_text,
1559
+ content='memories',
1560
+ content_rowid='rowid'
1178
1561
  );
1179
1562
 
1180
- CREATE INDEX IF NOT EXISTS idx_notifications_read
1181
- ON notifications(read);
1563
+ CREATE TRIGGER IF NOT EXISTS memories_fts_ai AFTER INSERT ON memories BEGIN
1564
+ INSERT INTO memories_fts(rowid, raw_text) VALUES (new.rowid, new.raw_text);
1565
+ END;
1182
1566
 
1183
- CREATE INDEX IF NOT EXISTS idx_notifications_agent
1184
- ON notifications(agent_id);
1567
+ CREATE TRIGGER IF NOT EXISTS memories_fts_ad AFTER DELETE ON memories BEGIN
1568
+ INSERT INTO memories_fts(memories_fts, rowid, raw_text) VALUES('delete', old.rowid, old.raw_text);
1569
+ END;
1185
1570
 
1186
- CREATE INDEX IF NOT EXISTS idx_notifications_task_file
1187
- ON notifications(task_file);
1571
+ CREATE TRIGGER IF NOT EXISTS memories_fts_au AFTER UPDATE ON memories BEGIN
1572
+ INSERT INTO memories_fts(memories_fts, rowid, raw_text) VALUES('delete', old.rowid, old.raw_text);
1573
+ INSERT INTO memories_fts(rowid, raw_text) VALUES (new.rowid, new.raw_text);
1574
+ END;
1188
1575
  `);
1189
1576
  await client.executeMultiple(`
1190
- CREATE TABLE IF NOT EXISTS schedules (
1191
- id TEXT PRIMARY KEY,
1192
- cron TEXT NOT NULL,
1193
- description TEXT NOT NULL,
1194
- job_type TEXT NOT NULL DEFAULT 'report',
1195
- prompt TEXT,
1196
- assigned_to TEXT,
1197
- project_name TEXT,
1198
- active INTEGER NOT NULL DEFAULT 1,
1199
- use_crontab INTEGER NOT NULL DEFAULT 0,
1200
- created_at TEXT NOT NULL
1577
+ CREATE TABLE IF NOT EXISTS sync_meta (
1578
+ key TEXT PRIMARY KEY,
1579
+ value TEXT NOT NULL
1201
1580
  );
1202
1581
  `);
1203
1582
  await client.executeMultiple(`
1204
- CREATE TABLE IF NOT EXISTS device_registry (
1205
- device_id TEXT PRIMARY KEY,
1206
- friendly_name TEXT NOT NULL,
1207
- hostname TEXT NOT NULL,
1208
- projects TEXT NOT NULL DEFAULT '[]',
1209
- agents TEXT NOT NULL DEFAULT '[]',
1210
- connected INTEGER DEFAULT 0,
1211
- last_seen TEXT NOT NULL
1583
+ CREATE TABLE IF NOT EXISTS tasks (
1584
+ id TEXT PRIMARY KEY,
1585
+ title TEXT NOT NULL,
1586
+ assigned_to TEXT NOT NULL,
1587
+ assigned_by TEXT NOT NULL,
1588
+ project_name TEXT NOT NULL,
1589
+ priority TEXT NOT NULL DEFAULT 'p1',
1590
+ status TEXT NOT NULL DEFAULT 'open',
1591
+ task_file TEXT,
1592
+ created_at TEXT NOT NULL,
1593
+ updated_at TEXT NOT NULL
1212
1594
  );
1595
+
1596
+ CREATE INDEX IF NOT EXISTS idx_tasks_assignee_status
1597
+ ON tasks(assigned_to, status);
1213
1598
  `);
1214
1599
  await client.executeMultiple(`
1215
- CREATE TABLE IF NOT EXISTS messages (
1216
- id TEXT PRIMARY KEY,
1217
- from_agent TEXT NOT NULL,
1218
- from_device TEXT NOT NULL DEFAULT 'local',
1600
+ CREATE TABLE IF NOT EXISTS behaviors (
1601
+ id TEXT PRIMARY KEY,
1602
+ agent_id TEXT NOT NULL,
1603
+ project_name TEXT,
1604
+ domain TEXT,
1605
+ content TEXT NOT NULL,
1606
+ active INTEGER NOT NULL DEFAULT 1,
1607
+ created_at TEXT NOT NULL,
1608
+ updated_at TEXT NOT NULL
1609
+ );
1610
+
1611
+ CREATE INDEX IF NOT EXISTS idx_behaviors_agent
1612
+ ON behaviors(agent_id, active);
1613
+ `);
1614
+ try {
1615
+ const coordinatorName = getCoordinatorName();
1616
+ const existing = await client.execute({
1617
+ sql: "SELECT COUNT(*) as cnt FROM behaviors WHERE agent_id = ?",
1618
+ args: [coordinatorName]
1619
+ });
1620
+ if (Number(existing.rows[0]?.cnt) === 0) {
1621
+ const seededAt = "2026-03-25T00:00:00Z";
1622
+ for (const [domain, content] of [
1623
+ ["workflow", `Don't ask "keep going?" \u2014 just keep executing phases/plans autonomously`],
1624
+ ["tool-use", "Always use create_task MCP tool, never write .md files directly for task creation"],
1625
+ ["workflow", "Auto-start reviewing when idle and reviews are pending \u2014 never ask founder for permission"]
1626
+ ]) {
1627
+ await client.execute({
1628
+ sql: `INSERT INTO behaviors (id, agent_id, project_name, domain, content, active, created_at, updated_at)
1629
+ VALUES (hex(randomblob(16)), ?, NULL, ?, ?, 1, ?, ?)`,
1630
+ args: [coordinatorName, domain, content, seededAt, seededAt]
1631
+ });
1632
+ }
1633
+ }
1634
+ } catch {
1635
+ }
1636
+ try {
1637
+ await client.execute({
1638
+ sql: `ALTER TABLE behaviors ADD COLUMN priority TEXT DEFAULT 'p1'`,
1639
+ args: []
1640
+ });
1641
+ } catch {
1642
+ }
1643
+ try {
1644
+ await client.execute({
1645
+ sql: `ALTER TABLE tasks ADD COLUMN blocked_by TEXT`,
1646
+ args: []
1647
+ });
1648
+ } catch {
1649
+ }
1650
+ try {
1651
+ await client.execute({
1652
+ sql: `ALTER TABLE tasks ADD COLUMN parent_task_id TEXT`,
1653
+ args: []
1654
+ });
1655
+ } catch {
1656
+ }
1657
+ try {
1658
+ await client.execute({
1659
+ sql: `CREATE INDEX IF NOT EXISTS idx_tasks_parent_task_id
1660
+ ON tasks(parent_task_id)
1661
+ WHERE parent_task_id IS NOT NULL`,
1662
+ args: []
1663
+ });
1664
+ } catch {
1665
+ }
1666
+ try {
1667
+ await client.execute({
1668
+ sql: `UPDATE tasks SET status = 'done' WHERE status = 'completed'`,
1669
+ args: []
1670
+ });
1671
+ } catch {
1672
+ }
1673
+ try {
1674
+ await client.execute({
1675
+ sql: `ALTER TABLE tasks ADD COLUMN reviewer TEXT`,
1676
+ args: []
1677
+ });
1678
+ } catch {
1679
+ }
1680
+ try {
1681
+ await client.execute({
1682
+ sql: `ALTER TABLE tasks ADD COLUMN context TEXT`,
1683
+ args: []
1684
+ });
1685
+ } catch {
1686
+ }
1687
+ try {
1688
+ await client.execute({
1689
+ sql: `ALTER TABLE tasks ADD COLUMN result TEXT`,
1690
+ args: []
1691
+ });
1692
+ } catch {
1693
+ }
1694
+ try {
1695
+ await client.execute({
1696
+ sql: `ALTER TABLE tasks ADD COLUMN assigned_tmux TEXT`,
1697
+ args: []
1698
+ });
1699
+ } catch {
1700
+ }
1701
+ try {
1702
+ await client.execute({
1703
+ sql: `ALTER TABLE tasks ADD COLUMN checkpoint TEXT`,
1704
+ args: []
1705
+ });
1706
+ } catch {
1707
+ }
1708
+ try {
1709
+ await client.execute({
1710
+ sql: `ALTER TABLE tasks ADD COLUMN checkpoint_count INTEGER NOT NULL DEFAULT 0`,
1711
+ args: []
1712
+ });
1713
+ } catch {
1714
+ }
1715
+ try {
1716
+ await client.execute({
1717
+ sql: `ALTER TABLE tasks ADD COLUMN complexity TEXT NOT NULL DEFAULT 'standard'`,
1718
+ args: []
1719
+ });
1720
+ } catch {
1721
+ }
1722
+ try {
1723
+ await client.execute({
1724
+ sql: `ALTER TABLE tasks ADD COLUMN session_scope TEXT`,
1725
+ args: []
1726
+ });
1727
+ } catch {
1728
+ }
1729
+ try {
1730
+ await client.execute({
1731
+ sql: `ALTER TABLE memories ADD COLUMN task_id TEXT`,
1732
+ args: []
1733
+ });
1734
+ } catch {
1735
+ }
1736
+ try {
1737
+ await client.execute({
1738
+ sql: `ALTER TABLE memories ADD COLUMN consolidated INTEGER NOT NULL DEFAULT 0`,
1739
+ args: []
1740
+ });
1741
+ } catch {
1742
+ }
1743
+ try {
1744
+ await client.execute({
1745
+ sql: `ALTER TABLE memories ADD COLUMN author_device_id TEXT`,
1746
+ args: []
1747
+ });
1748
+ } catch {
1749
+ }
1750
+ try {
1751
+ await client.execute({
1752
+ sql: `ALTER TABLE memories ADD COLUMN scope TEXT NOT NULL DEFAULT 'business'`,
1753
+ args: []
1754
+ });
1755
+ } catch {
1756
+ }
1757
+ await client.executeMultiple(`
1758
+ CREATE TABLE IF NOT EXISTS consolidations (
1759
+ id TEXT PRIMARY KEY,
1760
+ consolidated_memory_id TEXT NOT NULL,
1761
+ source_memory_id TEXT NOT NULL,
1762
+ created_at TEXT NOT NULL
1763
+ );
1764
+
1765
+ CREATE INDEX IF NOT EXISTS idx_consolidations_source
1766
+ ON consolidations(source_memory_id);
1767
+
1768
+ CREATE INDEX IF NOT EXISTS idx_consolidations_consolidated
1769
+ ON consolidations(consolidated_memory_id);
1770
+ `);
1771
+ await client.executeMultiple(`
1772
+ CREATE TABLE IF NOT EXISTS reminders (
1773
+ id TEXT PRIMARY KEY,
1774
+ text TEXT NOT NULL,
1775
+ created_at TEXT NOT NULL,
1776
+ due_date TEXT,
1777
+ completed_at TEXT
1778
+ );
1779
+ `);
1780
+ await client.executeMultiple(`
1781
+ CREATE TABLE IF NOT EXISTS notifications (
1782
+ id TEXT PRIMARY KEY,
1783
+ agent_id TEXT NOT NULL,
1784
+ agent_role TEXT NOT NULL,
1785
+ event TEXT NOT NULL,
1786
+ project TEXT NOT NULL,
1787
+ summary TEXT NOT NULL,
1788
+ task_file TEXT,
1789
+ read INTEGER NOT NULL DEFAULT 0,
1790
+ created_at TEXT NOT NULL
1791
+ );
1792
+
1793
+ CREATE INDEX IF NOT EXISTS idx_notifications_read
1794
+ ON notifications(read);
1795
+
1796
+ CREATE INDEX IF NOT EXISTS idx_notifications_agent
1797
+ ON notifications(agent_id);
1798
+
1799
+ CREATE INDEX IF NOT EXISTS idx_notifications_task_file
1800
+ ON notifications(task_file);
1801
+ `);
1802
+ await client.executeMultiple(`
1803
+ CREATE TABLE IF NOT EXISTS schedules (
1804
+ id TEXT PRIMARY KEY,
1805
+ cron TEXT NOT NULL,
1806
+ description TEXT NOT NULL,
1807
+ job_type TEXT NOT NULL DEFAULT 'report',
1808
+ prompt TEXT,
1809
+ assigned_to TEXT,
1810
+ project_name TEXT,
1811
+ active INTEGER NOT NULL DEFAULT 1,
1812
+ use_crontab INTEGER NOT NULL DEFAULT 0,
1813
+ created_at TEXT NOT NULL
1814
+ );
1815
+ `);
1816
+ await client.executeMultiple(`
1817
+ CREATE TABLE IF NOT EXISTS device_registry (
1818
+ device_id TEXT PRIMARY KEY,
1819
+ friendly_name TEXT NOT NULL,
1820
+ hostname TEXT NOT NULL,
1821
+ projects TEXT NOT NULL DEFAULT '[]',
1822
+ agents TEXT NOT NULL DEFAULT '[]',
1823
+ connected INTEGER DEFAULT 0,
1824
+ last_seen TEXT NOT NULL
1825
+ );
1826
+ `);
1827
+ await client.executeMultiple(`
1828
+ CREATE TABLE IF NOT EXISTS messages (
1829
+ id TEXT PRIMARY KEY,
1830
+ from_agent TEXT NOT NULL,
1831
+ from_device TEXT NOT NULL DEFAULT 'local',
1219
1832
  target_agent TEXT NOT NULL,
1220
1833
  target_project TEXT,
1221
1834
  target_device TEXT NOT NULL DEFAULT 'local',
@@ -1375,6 +1988,12 @@ async function ensureSchema() {
1375
1988
  } catch {
1376
1989
  }
1377
1990
  }
1991
+ try {
1992
+ await client.execute(
1993
+ `CREATE INDEX IF NOT EXISTS idx_memories_content_hash ON memories(content_hash, agent_id)`
1994
+ );
1995
+ } catch {
1996
+ }
1378
1997
  await client.executeMultiple(`
1379
1998
  CREATE TABLE IF NOT EXISTS entities (
1380
1999
  id TEXT PRIMARY KEY,
@@ -1427,7 +2046,30 @@ async function ensureSchema() {
1427
2046
  entity_id TEXT NOT NULL,
1428
2047
  PRIMARY KEY (hyperedge_id, entity_id)
1429
2048
  );
2049
+
2050
+ CREATE VIRTUAL TABLE IF NOT EXISTS entities_fts USING fts5(
2051
+ name,
2052
+ content=entities,
2053
+ content_rowid=rowid
2054
+ );
2055
+
2056
+ CREATE TRIGGER IF NOT EXISTS entities_fts_ai AFTER INSERT ON entities BEGIN
2057
+ INSERT INTO entities_fts(rowid, name) VALUES (new.rowid, new.name);
2058
+ END;
2059
+
2060
+ CREATE TRIGGER IF NOT EXISTS entities_fts_ad AFTER DELETE ON entities BEGIN
2061
+ INSERT INTO entities_fts(entities_fts, rowid, name) VALUES('delete', old.rowid, old.name);
2062
+ END;
2063
+
2064
+ CREATE TRIGGER IF NOT EXISTS entities_fts_au AFTER UPDATE ON entities BEGIN
2065
+ INSERT INTO entities_fts(entities_fts, rowid, name) VALUES('delete', old.rowid, old.name);
2066
+ INSERT INTO entities_fts(rowid, name) VALUES (new.rowid, new.name);
2067
+ END;
1430
2068
  `);
2069
+ try {
2070
+ await client.execute("INSERT INTO entities_fts(entities_fts) VALUES('rebuild')");
2071
+ } catch {
2072
+ }
1431
2073
  await client.executeMultiple(`
1432
2074
  CREATE TABLE IF NOT EXISTS entity_aliases (
1433
2075
  alias TEXT NOT NULL PRIMARY KEY,
@@ -1608,10 +2250,37 @@ async function ensureSchema() {
1608
2250
  CREATE INDEX IF NOT EXISTS idx_conversations_channel
1609
2251
  ON conversations(channel_id);
1610
2252
  `);
2253
+ await client.executeMultiple(`
2254
+ CREATE TABLE IF NOT EXISTS session_agent_map (
2255
+ session_uuid TEXT PRIMARY KEY,
2256
+ agent_id TEXT NOT NULL,
2257
+ session_name TEXT,
2258
+ task_id TEXT,
2259
+ project_name TEXT,
2260
+ started_at TEXT NOT NULL
2261
+ );
2262
+
2263
+ CREATE INDEX IF NOT EXISTS idx_session_agent_map_agent
2264
+ ON session_agent_map(agent_id);
2265
+ `);
1611
2266
  try {
1612
- await client.execute({
1613
- sql: `ALTER TABLE tasks ADD COLUMN budget_tokens INTEGER`,
1614
- args: []
2267
+ const mapCount = await client.execute({ sql: `SELECT COUNT(*) as cnt FROM session_agent_map`, args: [] });
2268
+ if (Number(mapCount.rows[0]?.cnt ?? 0) === 0) {
2269
+ await client.execute({
2270
+ sql: `INSERT OR IGNORE INTO session_agent_map (session_uuid, agent_id, session_name, started_at)
2271
+ SELECT session_id, agent_id, '', MIN(timestamp)
2272
+ FROM memories
2273
+ WHERE session_id IS NOT NULL AND session_id != '' AND agent_id IS NOT NULL AND agent_id != ''
2274
+ GROUP BY session_id, agent_id`,
2275
+ args: []
2276
+ });
2277
+ }
2278
+ } catch {
2279
+ }
2280
+ try {
2281
+ await client.execute({
2282
+ sql: `ALTER TABLE tasks ADD COLUMN budget_tokens INTEGER`,
2283
+ args: []
1615
2284
  });
1616
2285
  } catch {
1617
2286
  }
@@ -1741,15 +2410,41 @@ async function ensureSchema() {
1741
2410
  });
1742
2411
  } catch {
1743
2412
  }
2413
+ for (const col of [
2414
+ "ALTER TABLE memories ADD COLUMN intent TEXT",
2415
+ "ALTER TABLE memories ADD COLUMN outcome TEXT",
2416
+ "ALTER TABLE memories ADD COLUMN domain TEXT",
2417
+ "ALTER TABLE memories ADD COLUMN referenced_entities TEXT",
2418
+ "ALTER TABLE memories ADD COLUMN retrieval_count INTEGER DEFAULT 0",
2419
+ "ALTER TABLE memories ADD COLUMN chain_position TEXT",
2420
+ "ALTER TABLE memories ADD COLUMN review_status TEXT",
2421
+ "ALTER TABLE memories ADD COLUMN context_window_pct INTEGER",
2422
+ "ALTER TABLE memories ADD COLUMN file_paths TEXT",
2423
+ "ALTER TABLE memories ADD COLUMN commit_hash TEXT",
2424
+ "ALTER TABLE memories ADD COLUMN duration_ms INTEGER",
2425
+ "ALTER TABLE memories ADD COLUMN token_cost REAL",
2426
+ "ALTER TABLE memories ADD COLUMN audience TEXT",
2427
+ "ALTER TABLE memories ADD COLUMN language_type TEXT",
2428
+ "ALTER TABLE memories ADD COLUMN parent_memory_id TEXT"
2429
+ ]) {
2430
+ try {
2431
+ await client.execute(col);
2432
+ } catch {
2433
+ }
2434
+ }
1744
2435
  }
1745
2436
  async function disposeDatabase() {
2437
+ if (_daemonClient) {
2438
+ _daemonClient.close();
2439
+ _daemonClient = null;
2440
+ }
1746
2441
  if (_client) {
1747
2442
  _client.close();
1748
2443
  _client = null;
1749
2444
  _resilientClient = null;
1750
2445
  }
1751
2446
  }
1752
- var _client, _resilientClient, initTurso, disposeTurso;
2447
+ var _client, _resilientClient, _daemonClient, initTurso, disposeTurso;
1753
2448
  var init_database = __esm({
1754
2449
  "src/lib/database.ts"() {
1755
2450
  "use strict";
@@ -1757,24 +2452,25 @@ var init_database = __esm({
1757
2452
  init_employees();
1758
2453
  _client = null;
1759
2454
  _resilientClient = null;
2455
+ _daemonClient = null;
1760
2456
  initTurso = initDatabase;
1761
2457
  disposeTurso = disposeDatabase;
1762
2458
  }
1763
2459
  });
1764
2460
 
1765
2461
  // src/lib/license.ts
1766
- import { readFileSync as readFileSync5, writeFileSync as writeFileSync4, existsSync as existsSync5, mkdirSync as mkdirSync3 } from "fs";
1767
- import { randomUUID as randomUUID2 } from "crypto";
1768
- import path6 from "path";
2462
+ import { readFileSync as readFileSync7, writeFileSync as writeFileSync5, existsSync as existsSync7, mkdirSync as mkdirSync4 } from "fs";
2463
+ import { randomUUID as randomUUID3 } from "crypto";
2464
+ import path8 from "path";
1769
2465
  import { jwtVerify, importSPKI } from "jose";
1770
2466
  var LICENSE_PATH, CACHE_PATH, DEVICE_ID_PATH, PLAN_LIMITS;
1771
2467
  var init_license = __esm({
1772
2468
  "src/lib/license.ts"() {
1773
2469
  "use strict";
1774
2470
  init_config();
1775
- LICENSE_PATH = path6.join(EXE_AI_DIR, "license.key");
1776
- CACHE_PATH = path6.join(EXE_AI_DIR, "license-cache.json");
1777
- DEVICE_ID_PATH = path6.join(EXE_AI_DIR, "device-id");
2471
+ LICENSE_PATH = path8.join(EXE_AI_DIR, "license.key");
2472
+ CACHE_PATH = path8.join(EXE_AI_DIR, "license-cache.json");
2473
+ DEVICE_ID_PATH = path8.join(EXE_AI_DIR, "device-id");
1778
2474
  PLAN_LIMITS = {
1779
2475
  free: { devices: 1, employees: 1, memories: 5e3 },
1780
2476
  pro: { devices: 3, employees: 5, memories: 1e5 },
@@ -1786,12 +2482,12 @@ var init_license = __esm({
1786
2482
  });
1787
2483
 
1788
2484
  // src/lib/plan-limits.ts
1789
- import { readFileSync as readFileSync6, existsSync as existsSync6 } from "fs";
1790
- import path7 from "path";
2485
+ import { readFileSync as readFileSync8, existsSync as existsSync8 } from "fs";
2486
+ import path9 from "path";
1791
2487
  function getLicenseSync() {
1792
2488
  try {
1793
- if (!existsSync6(CACHE_PATH2)) return freeLicense();
1794
- const raw = JSON.parse(readFileSync6(CACHE_PATH2, "utf8"));
2489
+ if (!existsSync8(CACHE_PATH2)) return freeLicense();
2490
+ const raw = JSON.parse(readFileSync8(CACHE_PATH2, "utf8"));
1795
2491
  if (!raw.token || typeof raw.token !== "string") return freeLicense();
1796
2492
  const parts = raw.token.split(".");
1797
2493
  if (parts.length !== 3) return freeLicense();
@@ -1829,8 +2525,8 @@ function assertEmployeeLimitSync(rosterPath) {
1829
2525
  const filePath = rosterPath ?? EMPLOYEES_PATH;
1830
2526
  let count = 0;
1831
2527
  try {
1832
- if (existsSync6(filePath)) {
1833
- const raw = readFileSync6(filePath, "utf8");
2528
+ if (existsSync8(filePath)) {
2529
+ const raw = readFileSync8(filePath, "utf8");
1834
2530
  const employees = JSON.parse(raw);
1835
2531
  count = Array.isArray(employees) ? employees.length : 0;
1836
2532
  }
@@ -1859,19 +2555,19 @@ var init_plan_limits = __esm({
1859
2555
  this.name = "PlanLimitError";
1860
2556
  }
1861
2557
  };
1862
- CACHE_PATH2 = path7.join(EXE_AI_DIR, "license-cache.json");
2558
+ CACHE_PATH2 = path9.join(EXE_AI_DIR, "license-cache.json");
1863
2559
  }
1864
2560
  });
1865
2561
 
1866
2562
  // src/lib/notifications.ts
1867
2563
  import crypto from "crypto";
1868
- import path8 from "path";
2564
+ import path10 from "path";
1869
2565
  import os6 from "os";
1870
2566
  import {
1871
- readFileSync as readFileSync7,
2567
+ readFileSync as readFileSync9,
1872
2568
  readdirSync,
1873
- unlinkSync as unlinkSync2,
1874
- existsSync as existsSync7,
2569
+ unlinkSync as unlinkSync3,
2570
+ existsSync as existsSync9,
1875
2571
  rmdirSync
1876
2572
  } from "fs";
1877
2573
  async function writeNotification(notification) {
@@ -2035,10 +2731,11 @@ var init_state_bus = __esm({
2035
2731
 
2036
2732
  // src/lib/tasks-crud.ts
2037
2733
  import crypto3 from "crypto";
2038
- import path9 from "path";
2734
+ import path11 from "path";
2735
+ import os7 from "os";
2039
2736
  import { execSync as execSync5 } from "child_process";
2040
2737
  import { mkdir as mkdir3, writeFile as writeFile3, appendFile } from "fs/promises";
2041
- import { existsSync as existsSync8, readFileSync as readFileSync8 } from "fs";
2738
+ import { existsSync as existsSync10, readFileSync as readFileSync10 } from "fs";
2042
2739
  async function writeCheckpoint(input) {
2043
2740
  const client = getClient();
2044
2741
  const row = await resolveTask(client, input.taskId);
@@ -2079,6 +2776,35 @@ function extractParentFromContext(contextBody) {
2079
2776
  function slugify(title) {
2080
2777
  return title.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
2081
2778
  }
2779
+ function buildKeywordIndex() {
2780
+ const idx = /* @__PURE__ */ new Map();
2781
+ for (const [role, keywords] of Object.entries(LANE_KEYWORDS)) {
2782
+ for (const kw of keywords) {
2783
+ const existing = idx.get(kw) ?? [];
2784
+ existing.push(role);
2785
+ idx.set(kw, existing);
2786
+ }
2787
+ }
2788
+ return idx;
2789
+ }
2790
+ function checkLaneAffinity(title, context, assigneeName) {
2791
+ const employees = loadEmployeesSync();
2792
+ const employee = employees.find((e) => e.name === assigneeName);
2793
+ if (!employee) return void 0;
2794
+ const assigneeRole = employee.role;
2795
+ const text = `${title} ${context}`.toLowerCase();
2796
+ const matchedRoles = /* @__PURE__ */ new Set();
2797
+ for (const [keyword, roles] of KEYWORD_INDEX) {
2798
+ if (text.includes(keyword)) {
2799
+ for (const role of roles) matchedRoles.add(role);
2800
+ }
2801
+ }
2802
+ if (matchedRoles.size === 0) return void 0;
2803
+ if (matchedRoles.has(assigneeRole)) return void 0;
2804
+ if (assigneeRole === "COO") return void 0;
2805
+ const expectedRoles = Array.from(matchedRoles).join(" or ");
2806
+ return `\u26A0\uFE0F Lane mismatch: task content suggests ${expectedRoles}, but assigned to ${assigneeName} (${assigneeRole}).`;
2807
+ }
2082
2808
  async function resolveTask(client, identifier, scopeSession) {
2083
2809
  const scope = sessionScopeFilter(scopeSession);
2084
2810
  let result = await client.execute({
@@ -2128,7 +2854,14 @@ async function createTaskCore(input) {
2128
2854
  const id = crypto3.randomUUID();
2129
2855
  const now = (/* @__PURE__ */ new Date()).toISOString();
2130
2856
  const slug = slugify(input.title);
2131
- const taskFile = input.taskFile ?? `exe/${input.assignedTo}/${slug}.md`;
2857
+ let earlySessionScope = null;
2858
+ try {
2859
+ const { resolveExeSession: resolveExeSession2 } = await Promise.resolve().then(() => (init_tmux_routing(), tmux_routing_exports));
2860
+ earlySessionScope = resolveExeSession2();
2861
+ } catch {
2862
+ }
2863
+ const scope = earlySessionScope ?? "default";
2864
+ const taskFile = input.taskFile ?? `tasks/${scope}/${input.assignedTo}/${slug}.md`;
2132
2865
  let blockedById = null;
2133
2866
  const initialStatus = input.blockedBy ? "blocked" : "open";
2134
2867
  if (input.blockedBy) {
@@ -2168,22 +2901,24 @@ async function createTaskCore(input) {
2168
2901
  if (dupCheck.rows.length > 0) {
2169
2902
  warning = `similar active task already exists (${String(dupCheck.rows[0].id)}). Created new task anyway.`;
2170
2903
  }
2904
+ if (!process.env.DISABLE_LANE_AFFINITY) {
2905
+ const laneWarning = checkLaneAffinity(input.title, input.context, input.assignedTo);
2906
+ if (laneWarning) {
2907
+ warning = warning ? `${warning}
2908
+ ${laneWarning}` : laneWarning;
2909
+ }
2910
+ }
2171
2911
  if (input.baseDir) {
2172
2912
  try {
2173
- await mkdir3(path9.join(input.baseDir, "exe", "output"), { recursive: true });
2174
- await mkdir3(path9.join(input.baseDir, "exe", "research"), { recursive: true });
2913
+ await mkdir3(path11.join(input.baseDir, "exe", "output"), { recursive: true });
2914
+ await mkdir3(path11.join(input.baseDir, "exe", "research"), { recursive: true });
2175
2915
  await ensureArchitectureDoc(input.baseDir, input.projectName);
2176
2916
  await ensureGitignoreExe(input.baseDir);
2177
2917
  } catch {
2178
2918
  }
2179
2919
  }
2180
2920
  const complexity = input.complexity ?? "standard";
2181
- let sessionScope = null;
2182
- try {
2183
- const { resolveExeSession: resolveExeSession2 } = await Promise.resolve().then(() => (init_tmux_routing(), tmux_routing_exports));
2184
- sessionScope = resolveExeSession2();
2185
- } catch {
2186
- }
2921
+ const sessionScope = earlySessionScope;
2187
2922
  await client.execute({
2188
2923
  sql: `INSERT INTO tasks (id, title, assigned_to, assigned_by, project_name, priority, status, task_file, blocked_by, parent_task_id, reviewer, context, complexity, budget_tokens, budget_fallback_model, tokens_used, tokens_warned_at, session_scope, created_at, updated_at)
2189
2924
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
@@ -2210,6 +2945,43 @@ async function createTaskCore(input) {
2210
2945
  now
2211
2946
  ]
2212
2947
  });
2948
+ if (input.baseDir) {
2949
+ try {
2950
+ const EXE_OS_DIR = path11.join(os7.homedir(), ".exe-os");
2951
+ const mdPath = path11.join(EXE_OS_DIR, taskFile);
2952
+ const mdDir = path11.dirname(mdPath);
2953
+ if (!existsSync10(mdDir)) await mkdir3(mdDir, { recursive: true });
2954
+ const reviewer = input.reviewer ?? input.assignedBy;
2955
+ const mdContent = `# ${input.title}
2956
+
2957
+ **ID:** ${id}
2958
+ **Status:** ${initialStatus}
2959
+ **Priority:** ${input.priority}
2960
+ **Assigned by:** ${input.assignedBy}
2961
+ **Assigned to:** ${input.assignedTo}
2962
+ **Project:** ${input.projectName}
2963
+ **Created:** ${now.split("T")[0]}${parentTaskId ? `
2964
+ **Parent task:** ${parentTaskId}` : ""}
2965
+ **Reviewer:** ${reviewer}
2966
+
2967
+ ## Context
2968
+
2969
+ ${input.context}
2970
+
2971
+ ## MANDATORY: When done
2972
+
2973
+ You MUST call update_task with status "done" and a result summary when finished.
2974
+ If you skip this, your reviewer will not know you're done and your work won't be reviewed.
2975
+ Do NOT let a failed commit or any error prevent you from calling update_task(done).
2976
+ `;
2977
+ await writeFile3(mdPath, mdContent, "utf-8");
2978
+ } catch (err) {
2979
+ process.stderr.write(
2980
+ `[create-task] WARNING: .md file write failed for ${taskFile}: ${err instanceof Error ? err.message : String(err)}
2981
+ `
2982
+ );
2983
+ }
2984
+ }
2213
2985
  return {
2214
2986
  id,
2215
2987
  title: input.title,
@@ -2402,7 +3174,7 @@ ${input.result}` : `\u26A0\uFE0F ${warning}`;
2402
3174
  return { row, taskFile, now, taskId };
2403
3175
  }
2404
3176
  }
2405
- if (curStatus === "in_progress" && input.callerAgentId && (input.callerAgentId === assignedBy || input.callerAgentId === "exe")) {
3177
+ if (curStatus === "in_progress" && input.callerAgentId && (input.callerAgentId === assignedBy || isCoordinatorName(input.callerAgentId))) {
2406
3178
  process.stderr.write(
2407
3179
  `[tasks] Assigner override: ${input.callerAgentId} reclaiming ${taskId}
2408
3180
  `
@@ -2467,9 +3239,9 @@ async function deleteTaskCore(taskId, _baseDir) {
2467
3239
  return { taskFile, assignedTo, assignedBy, taskSlug };
2468
3240
  }
2469
3241
  async function ensureArchitectureDoc(baseDir, projectName) {
2470
- const archPath = path9.join(baseDir, "exe", "ARCHITECTURE.md");
3242
+ const archPath = path11.join(baseDir, "exe", "ARCHITECTURE.md");
2471
3243
  try {
2472
- if (existsSync8(archPath)) return;
3244
+ if (existsSync10(archPath)) return;
2473
3245
  const template = [
2474
3246
  `# ${projectName} \u2014 System Architecture`,
2475
3247
  "",
@@ -2502,10 +3274,10 @@ async function ensureArchitectureDoc(baseDir, projectName) {
2502
3274
  }
2503
3275
  }
2504
3276
  async function ensureGitignoreExe(baseDir) {
2505
- const gitignorePath = path9.join(baseDir, ".gitignore");
3277
+ const gitignorePath = path11.join(baseDir, ".gitignore");
2506
3278
  try {
2507
- if (existsSync8(gitignorePath)) {
2508
- const content = readFileSync8(gitignorePath, "utf-8");
3279
+ if (existsSync10(gitignorePath)) {
3280
+ const content = readFileSync10(gitignorePath, "utf-8");
2509
3281
  if (/^\/?exe\/?$/m.test(content)) return;
2510
3282
  await appendFile(gitignorePath, "\n# Employee task assignments (private)\n/exe/\n");
2511
3283
  } else {
@@ -2514,20 +3286,30 @@ async function ensureGitignoreExe(baseDir) {
2514
3286
  } catch {
2515
3287
  }
2516
3288
  }
2517
- var DELEGATION_KEYWORDS, TASK_ALREADY_CLAIMED_PREFIX;
3289
+ var LANE_KEYWORDS, KEYWORD_INDEX, DELEGATION_KEYWORDS, TASK_ALREADY_CLAIMED_PREFIX;
2518
3290
  var init_tasks_crud = __esm({
2519
3291
  "src/lib/tasks-crud.ts"() {
2520
3292
  "use strict";
2521
3293
  init_database();
2522
3294
  init_task_scope();
3295
+ init_employees();
3296
+ LANE_KEYWORDS = {
3297
+ CMO: ["sales", "script", "pitch", "offer", "copy", "objection", "brand", "content", "seo", "marketing", "newsletter", "carousel", "social", "campaign"],
3298
+ CTO: ["spec", "architecture", "migration", "schema", "database", "design doc", "adr", "security audit", "tech stack"],
3299
+ "Principal Engineer": ["implement", "build", "fix", "commit", "refactor", "bug", "feature", "wire", "integration"],
3300
+ "Staff Code Reviewer": ["critique", "verdict", "review", "audit", "code quality"],
3301
+ "Content Production Specialist": ["render", "video", "image", "b-roll", "remotion", "animation", "thumbnail"],
3302
+ "AI Product Lead": ["competitive", "analysis", "benchmark", "compare", "scout", "evaluate", "poc"]
3303
+ };
3304
+ KEYWORD_INDEX = buildKeywordIndex();
2523
3305
  DELEGATION_KEYWORDS = /parallel|delegate|wave|worktree|multi-instance/i;
2524
3306
  TASK_ALREADY_CLAIMED_PREFIX = "TASK_ALREADY_CLAIMED";
2525
3307
  }
2526
3308
  });
2527
3309
 
2528
3310
  // src/lib/tasks-review.ts
2529
- import path10 from "path";
2530
- import { existsSync as existsSync9, readdirSync as readdirSync2, unlinkSync as unlinkSync3 } from "fs";
3311
+ import path12 from "path";
3312
+ import { existsSync as existsSync11, readdirSync as readdirSync2, unlinkSync as unlinkSync4 } from "fs";
2531
3313
  async function countPendingReviews(sessionScope) {
2532
3314
  const client = getClient();
2533
3315
  if (sessionScope) {
@@ -2549,7 +3331,7 @@ async function countNewPendingReviewsSince(sinceIso, sessionScope) {
2549
3331
  const result2 = await client.execute({
2550
3332
  sql: `SELECT COUNT(*) as cnt FROM tasks
2551
3333
  WHERE status = 'needs_review' AND updated_at > ?
2552
- AND (session_scope = ? OR session_scope IS NULL)`,
3334
+ AND session_scope = ?`,
2553
3335
  args: [sinceIso, sessionScope]
2554
3336
  });
2555
3337
  return Number(result2.rows[0]?.cnt) || 0;
@@ -2567,7 +3349,7 @@ async function listPendingReviews(limit, sessionScope) {
2567
3349
  const result2 = await client.execute({
2568
3350
  sql: `SELECT title, assigned_to, project_name FROM tasks
2569
3351
  WHERE status = 'needs_review'
2570
- AND (session_scope = ? OR session_scope IS NULL)
3352
+ AND session_scope = ?
2571
3353
  ORDER BY priority ASC, created_at DESC LIMIT ?`,
2572
3354
  args: [sessionScope, limit]
2573
3355
  });
@@ -2688,14 +3470,14 @@ async function cleanupReviewFile(row, taskFile, _baseDir) {
2688
3470
  if (parts.length >= 3 && parts[0] === "review") {
2689
3471
  const agent = parts[1];
2690
3472
  const slug = parts.slice(2).join("-");
2691
- const originalTaskFile = `exe/${agent}/${slug}.md`;
3473
+ const legacyTaskFile = `exe/${agent}/${slug}.md`;
2692
3474
  const result = await client.execute({
2693
- sql: "UPDATE tasks SET status = 'done', updated_at = ? WHERE task_file = ? AND status = 'needs_review'",
2694
- args: [now, originalTaskFile]
3475
+ sql: "UPDATE tasks SET status = 'done', updated_at = ? WHERE (task_file = ? OR task_file LIKE ?) AND status = 'needs_review'",
3476
+ args: [now, legacyTaskFile, `tasks/%/${agent}/${slug}.md`]
2695
3477
  });
2696
3478
  if (result.rowsAffected > 0) {
2697
3479
  process.stderr.write(
2698
- `[review-cleanup] Cascaded original task to done (legacy path): ${originalTaskFile}
3480
+ `[review-cleanup] Cascaded original task to done: ${agent}/${slug}.md
2699
3481
  `
2700
3482
  );
2701
3483
  }
@@ -2708,11 +3490,11 @@ async function cleanupReviewFile(row, taskFile, _baseDir) {
2708
3490
  );
2709
3491
  }
2710
3492
  try {
2711
- const cacheDir = path10.join(EXE_AI_DIR, "session-cache");
2712
- if (existsSync9(cacheDir)) {
3493
+ const cacheDir = path12.join(EXE_AI_DIR, "session-cache");
3494
+ if (existsSync11(cacheDir)) {
2713
3495
  for (const f of readdirSync2(cacheDir)) {
2714
3496
  if (f.startsWith("review-notified-")) {
2715
- unlinkSync3(path10.join(cacheDir, f));
3497
+ unlinkSync4(path12.join(cacheDir, f));
2716
3498
  }
2717
3499
  }
2718
3500
  }
@@ -2733,7 +3515,7 @@ var init_tasks_review = __esm({
2733
3515
  });
2734
3516
 
2735
3517
  // src/lib/tasks-chain.ts
2736
- import path11 from "path";
3518
+ import path13 from "path";
2737
3519
  import { readFile as readFile3, writeFile as writeFile4 } from "fs/promises";
2738
3520
  async function cascadeUnblock(taskId, baseDir, now) {
2739
3521
  const client = getClient();
@@ -2750,7 +3532,7 @@ async function cascadeUnblock(taskId, baseDir, now) {
2750
3532
  });
2751
3533
  for (const ur of unblockedRows.rows) {
2752
3534
  try {
2753
- const ubFile = path11.join(baseDir, String(ur.task_file));
3535
+ const ubFile = path13.join(baseDir, String(ur.task_file));
2754
3536
  let ubContent = await readFile3(ubFile, "utf-8");
2755
3537
  ubContent = ubContent.replace(/\*\*Status:\*\* blocked/, "**Status:** open");
2756
3538
  ubContent = ubContent.replace(/\n\*\*Blocked by:\*\*.*\n/, "\n");
@@ -2819,7 +3601,7 @@ var init_tasks_chain = __esm({
2819
3601
 
2820
3602
  // src/lib/project-name.ts
2821
3603
  import { execSync as execSync6 } from "child_process";
2822
- import path12 from "path";
3604
+ import path14 from "path";
2823
3605
  function getProjectName(cwd) {
2824
3606
  const dir = cwd ?? process.cwd();
2825
3607
  if (_cached2 && _cachedCwd === dir) return _cached2;
@@ -2832,7 +3614,7 @@ function getProjectName(cwd) {
2832
3614
  timeout: 2e3,
2833
3615
  stdio: ["pipe", "pipe", "pipe"]
2834
3616
  }).trim();
2835
- repoRoot = path12.dirname(gitCommonDir);
3617
+ repoRoot = path14.dirname(gitCommonDir);
2836
3618
  } catch {
2837
3619
  repoRoot = execSync6("git rev-parse --show-toplevel", {
2838
3620
  cwd: dir,
@@ -2841,11 +3623,11 @@ function getProjectName(cwd) {
2841
3623
  stdio: ["pipe", "pipe", "pipe"]
2842
3624
  }).trim();
2843
3625
  }
2844
- _cached2 = path12.basename(repoRoot);
3626
+ _cached2 = path14.basename(repoRoot);
2845
3627
  _cachedCwd = dir;
2846
3628
  return _cached2;
2847
3629
  } catch {
2848
- _cached2 = path12.basename(dir);
3630
+ _cached2 = path14.basename(dir);
2849
3631
  _cachedCwd = dir;
2850
3632
  return _cached2;
2851
3633
  }
@@ -2877,7 +3659,7 @@ function findSessionForProject(projectName) {
2877
3659
  const sessions = listSessions();
2878
3660
  for (const s of sessions) {
2879
3661
  const proj = s.projectDir.split("/").filter(Boolean).pop();
2880
- if (proj === projectName && (s.agentId === "exe" || isCoordinatorName(s.agentId))) return s;
3662
+ if (proj === projectName && isCoordinatorName(s.agentId)) return s;
2881
3663
  }
2882
3664
  return null;
2883
3665
  }
@@ -2923,7 +3705,7 @@ var init_session_scope = __esm({
2923
3705
 
2924
3706
  // src/lib/tasks-notify.ts
2925
3707
  async function dispatchTaskToEmployee(input) {
2926
- if (input.assignedTo === "exe" || isCoordinatorName(input.assignedTo)) return { dispatched: "skipped" };
3708
+ if (isCoordinatorName(input.assignedTo)) return { dispatched: "skipped" };
2927
3709
  let crossProject = false;
2928
3710
  if (input.projectName) {
2929
3711
  try {
@@ -3380,8 +4162,8 @@ __export(tasks_exports, {
3380
4162
  updateTaskStatus: () => updateTaskStatus,
3381
4163
  writeCheckpoint: () => writeCheckpoint
3382
4164
  });
3383
- import path13 from "path";
3384
- import { writeFileSync as writeFileSync5, mkdirSync as mkdirSync4, unlinkSync as unlinkSync4 } from "fs";
4165
+ import path15 from "path";
4166
+ import { writeFileSync as writeFileSync6, mkdirSync as mkdirSync5, unlinkSync as unlinkSync5 } from "fs";
3385
4167
  async function createTask(input) {
3386
4168
  const result = await createTaskCore(input);
3387
4169
  if (!input.skipDispatch && result.status !== "blocked" && !process.env.VITEST) {
@@ -3400,14 +4182,14 @@ async function updateTask(input) {
3400
4182
  const { row, taskFile, now, taskId } = await updateTaskStatus(input);
3401
4183
  try {
3402
4184
  const agent = String(row.assigned_to);
3403
- const cacheDir = path13.join(EXE_AI_DIR, "session-cache");
3404
- const cachePath = path13.join(cacheDir, `current-task-${agent}.json`);
4185
+ const cacheDir = path15.join(EXE_AI_DIR, "session-cache");
4186
+ const cachePath = path15.join(cacheDir, `current-task-${agent}.json`);
3405
4187
  if (input.status === "in_progress") {
3406
- mkdirSync4(cacheDir, { recursive: true });
3407
- writeFileSync5(cachePath, JSON.stringify({ taskId, title: String(row.title) }));
4188
+ mkdirSync5(cacheDir, { recursive: true });
4189
+ writeFileSync6(cachePath, JSON.stringify({ taskId, title: String(row.title) }));
3408
4190
  } else if (input.status === "done" || input.status === "blocked" || input.status === "cancelled") {
3409
4191
  try {
3410
- unlinkSync4(cachePath);
4192
+ unlinkSync5(cachePath);
3411
4193
  } catch {
3412
4194
  }
3413
4195
  }
@@ -3464,7 +4246,7 @@ async function updateTask(input) {
3464
4246
  }
3465
4247
  const isTerminal = input.status === "done" || input.status === "needs_review";
3466
4248
  if (isTerminal) {
3467
- const isCoordinator = String(row.assigned_to) === "exe" || isCoordinatorName(String(row.assigned_to));
4249
+ const isCoordinator = isCoordinatorName(String(row.assigned_to));
3468
4250
  if (!isCoordinator) {
3469
4251
  notifyTaskDone();
3470
4252
  }
@@ -3489,7 +4271,7 @@ async function updateTask(input) {
3489
4271
  }
3490
4272
  }
3491
4273
  }
3492
- if (input.status === "done" && String(row.assigned_to) !== "exe" && !isCoordinatorName(String(row.assigned_to)) && !process.env.VITEST) {
4274
+ if (input.status === "done" && !isCoordinatorName(String(row.assigned_to)) && !process.env.VITEST) {
3493
4275
  Promise.resolve().then(() => (init_skill_learning(), skill_learning_exports)).then(
3494
4276
  ({ captureAndLearn: captureAndLearn2 }) => captureAndLearn2({
3495
4277
  taskId,
@@ -3505,7 +4287,7 @@ async function updateTask(input) {
3505
4287
  });
3506
4288
  }
3507
4289
  let nextTask;
3508
- if (isTerminal && String(row.assigned_to) !== "exe" && !isCoordinatorName(String(row.assigned_to))) {
4290
+ if (isTerminal && !isCoordinatorName(String(row.assigned_to))) {
3509
4291
  try {
3510
4292
  nextTask = await findNextTask(String(row.assigned_to));
3511
4293
  } catch {
@@ -3849,7 +4631,7 @@ var init_capacity_monitor = __esm({
3849
4631
  // src/lib/tmux-routing.ts
3850
4632
  var tmux_routing_exports = {};
3851
4633
  __export(tmux_routing_exports, {
3852
- acquireSpawnLock: () => acquireSpawnLock,
4634
+ acquireSpawnLock: () => acquireSpawnLock2,
3853
4635
  employeeSessionName: () => employeeSessionName,
3854
4636
  ensureEmployee: () => ensureEmployee,
3855
4637
  extractRootExe: () => extractRootExe,
@@ -3864,20 +4646,20 @@ __export(tmux_routing_exports, {
3864
4646
  notifyParentExe: () => notifyParentExe,
3865
4647
  parseParentExe: () => parseParentExe,
3866
4648
  registerParentExe: () => registerParentExe,
3867
- releaseSpawnLock: () => releaseSpawnLock,
4649
+ releaseSpawnLock: () => releaseSpawnLock2,
3868
4650
  resolveExeSession: () => resolveExeSession,
3869
4651
  sendIntercom: () => sendIntercom,
3870
4652
  spawnEmployee: () => spawnEmployee,
3871
4653
  verifyPaneAtCapacity: () => verifyPaneAtCapacity
3872
4654
  });
3873
4655
  import { execFileSync as execFileSync2, execSync as execSync7 } from "child_process";
3874
- import { readFileSync as readFileSync9, writeFileSync as writeFileSync6, mkdirSync as mkdirSync5, existsSync as existsSync10, appendFileSync } from "fs";
3875
- import path14 from "path";
3876
- import os7 from "os";
3877
- import { fileURLToPath } from "url";
3878
- import { unlinkSync as unlinkSync5 } from "fs";
4656
+ import { readFileSync as readFileSync11, writeFileSync as writeFileSync7, mkdirSync as mkdirSync6, existsSync as existsSync12, appendFileSync } from "fs";
4657
+ import path16 from "path";
4658
+ import os8 from "os";
4659
+ import { fileURLToPath as fileURLToPath2 } from "url";
4660
+ import { unlinkSync as unlinkSync6 } from "fs";
3879
4661
  function spawnLockPath(sessionName) {
3880
- return path14.join(SPAWN_LOCK_DIR, `${sessionName}.lock`);
4662
+ return path16.join(SPAWN_LOCK_DIR, `${sessionName}.lock`);
3881
4663
  }
3882
4664
  function isProcessAlive(pid) {
3883
4665
  try {
@@ -3887,14 +4669,14 @@ function isProcessAlive(pid) {
3887
4669
  return false;
3888
4670
  }
3889
4671
  }
3890
- function acquireSpawnLock(sessionName) {
3891
- if (!existsSync10(SPAWN_LOCK_DIR)) {
3892
- mkdirSync5(SPAWN_LOCK_DIR, { recursive: true });
4672
+ function acquireSpawnLock2(sessionName) {
4673
+ if (!existsSync12(SPAWN_LOCK_DIR)) {
4674
+ mkdirSync6(SPAWN_LOCK_DIR, { recursive: true });
3893
4675
  }
3894
4676
  const lockFile = spawnLockPath(sessionName);
3895
- if (existsSync10(lockFile)) {
4677
+ if (existsSync12(lockFile)) {
3896
4678
  try {
3897
- const lock = JSON.parse(readFileSync9(lockFile, "utf8"));
4679
+ const lock = JSON.parse(readFileSync11(lockFile, "utf8"));
3898
4680
  const age = Date.now() - lock.timestamp;
3899
4681
  if (isProcessAlive(lock.pid) && age < 6e4) {
3900
4682
  return false;
@@ -3902,25 +4684,25 @@ function acquireSpawnLock(sessionName) {
3902
4684
  } catch {
3903
4685
  }
3904
4686
  }
3905
- writeFileSync6(lockFile, JSON.stringify({ pid: process.pid, timestamp: Date.now() }));
4687
+ writeFileSync7(lockFile, JSON.stringify({ pid: process.pid, timestamp: Date.now() }));
3906
4688
  return true;
3907
4689
  }
3908
- function releaseSpawnLock(sessionName) {
4690
+ function releaseSpawnLock2(sessionName) {
3909
4691
  try {
3910
- unlinkSync5(spawnLockPath(sessionName));
4692
+ unlinkSync6(spawnLockPath(sessionName));
3911
4693
  } catch {
3912
4694
  }
3913
4695
  }
3914
4696
  function resolveBehaviorsExporterScript() {
3915
4697
  try {
3916
- const thisFile = fileURLToPath(import.meta.url);
3917
- const scriptPath = path14.join(
3918
- path14.dirname(thisFile),
4698
+ const thisFile = fileURLToPath2(import.meta.url);
4699
+ const scriptPath = path16.join(
4700
+ path16.dirname(thisFile),
3919
4701
  "..",
3920
4702
  "bin",
3921
4703
  "exe-export-behaviors.js"
3922
4704
  );
3923
- return existsSync10(scriptPath) ? scriptPath : null;
4705
+ return existsSync12(scriptPath) ? scriptPath : null;
3924
4706
  } catch {
3925
4707
  return null;
3926
4708
  }
@@ -3986,12 +4768,12 @@ function extractRootExe(name) {
3986
4768
  return parts.length > 0 ? parts[parts.length - 1] : null;
3987
4769
  }
3988
4770
  function registerParentExe(sessionKey, parentExe, dispatchedBy) {
3989
- if (!existsSync10(SESSION_CACHE)) {
3990
- mkdirSync5(SESSION_CACHE, { recursive: true });
4771
+ if (!existsSync12(SESSION_CACHE)) {
4772
+ mkdirSync6(SESSION_CACHE, { recursive: true });
3991
4773
  }
3992
4774
  const rootExe = extractRootExe(parentExe) ?? parentExe;
3993
- const filePath = path14.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`);
3994
- writeFileSync6(filePath, JSON.stringify({
4775
+ const filePath = path16.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`);
4776
+ writeFileSync7(filePath, JSON.stringify({
3995
4777
  parentExe: rootExe,
3996
4778
  dispatchedBy: dispatchedBy || rootExe,
3997
4779
  registeredAt: (/* @__PURE__ */ new Date()).toISOString()
@@ -3999,7 +4781,7 @@ function registerParentExe(sessionKey, parentExe, dispatchedBy) {
3999
4781
  }
4000
4782
  function getParentExe(sessionKey) {
4001
4783
  try {
4002
- const data = JSON.parse(readFileSync9(path14.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`), "utf8"));
4784
+ const data = JSON.parse(readFileSync11(path16.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`), "utf8"));
4003
4785
  return data.parentExe || null;
4004
4786
  } catch {
4005
4787
  return null;
@@ -4007,8 +4789,8 @@ function getParentExe(sessionKey) {
4007
4789
  }
4008
4790
  function getDispatchedBy(sessionKey) {
4009
4791
  try {
4010
- const data = JSON.parse(readFileSync9(
4011
- path14.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`),
4792
+ const data = JSON.parse(readFileSync11(
4793
+ path16.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`),
4012
4794
  "utf8"
4013
4795
  ));
4014
4796
  return data.dispatchedBy ?? data.parentExe ?? null;
@@ -4034,10 +4816,10 @@ function isEmployeeAlive(sessionName) {
4034
4816
  }
4035
4817
  function findFreeInstance(employeeName, exeSession, maxInstances = 10, isAlive = isEmployeeAlive) {
4036
4818
  const base = employeeSessionName(employeeName, exeSession);
4037
- if (!isAlive(base) && acquireSpawnLock(base)) return 0;
4819
+ if (!isAlive(base) && acquireSpawnLock2(base)) return 0;
4038
4820
  for (let i = 2; i <= maxInstances; i++) {
4039
4821
  const candidate = employeeSessionName(employeeName, exeSession, i);
4040
- if (!isAlive(candidate) && acquireSpawnLock(candidate)) return i;
4822
+ if (!isAlive(candidate) && acquireSpawnLock2(candidate)) return i;
4041
4823
  }
4042
4824
  return null;
4043
4825
  }
@@ -4069,32 +4851,50 @@ async function verifyPaneAtCapacity(sessionName) {
4069
4851
  }
4070
4852
  function readDebounceState() {
4071
4853
  try {
4072
- if (!existsSync10(DEBOUNCE_FILE)) return {};
4073
- return JSON.parse(readFileSync9(DEBOUNCE_FILE, "utf8"));
4854
+ if (!existsSync12(DEBOUNCE_FILE)) return {};
4855
+ const raw = JSON.parse(readFileSync11(DEBOUNCE_FILE, "utf8"));
4856
+ const state = {};
4857
+ for (const [key, val] of Object.entries(raw)) {
4858
+ if (typeof val === "number") {
4859
+ state[key] = { lastSent: val, pending: 0 };
4860
+ } else if (val && typeof val === "object" && "lastSent" in val) {
4861
+ state[key] = val;
4862
+ }
4863
+ }
4864
+ return state;
4074
4865
  } catch {
4075
4866
  return {};
4076
4867
  }
4077
4868
  }
4078
4869
  function writeDebounceState(state) {
4079
4870
  try {
4080
- if (!existsSync10(SESSION_CACHE)) mkdirSync5(SESSION_CACHE, { recursive: true });
4081
- writeFileSync6(DEBOUNCE_FILE, JSON.stringify(state));
4871
+ if (!existsSync12(SESSION_CACHE)) mkdirSync6(SESSION_CACHE, { recursive: true });
4872
+ writeFileSync7(DEBOUNCE_FILE, JSON.stringify(state));
4082
4873
  } catch {
4083
4874
  }
4084
4875
  }
4085
4876
  function isDebounced(targetSession) {
4086
4877
  const state = readDebounceState();
4087
- const lastSent = state[targetSession] ?? 0;
4088
- return Date.now() - lastSent < INTERCOM_DEBOUNCE_MS;
4878
+ const entry = state[targetSession];
4879
+ const lastSent = entry?.lastSent ?? 0;
4880
+ if (Date.now() - lastSent < INTERCOM_DEBOUNCE_MS) {
4881
+ if (!state[targetSession]) state[targetSession] = { lastSent, pending: 0 };
4882
+ state[targetSession].pending++;
4883
+ writeDebounceState(state);
4884
+ return true;
4885
+ }
4886
+ return false;
4089
4887
  }
4090
4888
  function recordDebounce(targetSession) {
4091
4889
  const state = readDebounceState();
4092
- state[targetSession] = Date.now();
4890
+ const batched = state[targetSession]?.pending ?? 0;
4891
+ state[targetSession] = { lastSent: Date.now(), pending: 0 };
4093
4892
  const cutoff = Date.now() - DEBOUNCE_CLEANUP_AGE_MS;
4094
4893
  for (const key of Object.keys(state)) {
4095
- if ((state[key] ?? 0) < cutoff) delete state[key];
4894
+ if ((state[key]?.lastSent ?? 0) < cutoff) delete state[key];
4096
4895
  }
4097
4896
  writeDebounceState(state);
4897
+ return batched;
4098
4898
  }
4099
4899
  function logIntercom(msg) {
4100
4900
  const line = `[${(/* @__PURE__ */ new Date()).toISOString()}] ${msg}
@@ -4139,7 +4939,7 @@ function sendIntercom(targetSession) {
4139
4939
  return "skipped_exe";
4140
4940
  }
4141
4941
  if (isDebounced(targetSession)) {
4142
- logIntercom(`DEBOUNCE \u2192 ${targetSession} (cross-process file debounce)`);
4942
+ logIntercom(`DEBOUNCE \u2192 ${targetSession} (nudge batched, task safe in DB)`);
4143
4943
  return "debounced";
4144
4944
  }
4145
4945
  try {
@@ -4151,14 +4951,14 @@ function sendIntercom(targetSession) {
4151
4951
  const sessionState = getSessionState(targetSession);
4152
4952
  if (sessionState === "no_claude") {
4153
4953
  queueIntercom(targetSession, "claude not running in session");
4154
- recordDebounce(targetSession);
4155
- logIntercom(`QUEUED \u2192 ${targetSession} (no claude process \u2014 raw shell detected)`);
4954
+ const batched2 = recordDebounce(targetSession);
4955
+ logIntercom(`QUEUED \u2192 ${targetSession} (no claude process)${batched2 > 0 ? ` [${batched2} batched]` : ""}`);
4156
4956
  return "queued";
4157
4957
  }
4158
4958
  if (sessionState === "thinking" || sessionState === "tool") {
4159
4959
  queueIntercom(targetSession, "session busy at send time");
4160
- recordDebounce(targetSession);
4161
- logIntercom(`QUEUED \u2192 ${targetSession} (session busy, will retry from queue)`);
4960
+ const batched2 = recordDebounce(targetSession);
4961
+ logIntercom(`QUEUED \u2192 ${targetSession} (session busy)${batched2 > 0 ? ` [${batched2} batched]` : ""}`);
4162
4962
  return "queued";
4163
4963
  }
4164
4964
  if (transport.isPaneInCopyMode(targetSession)) {
@@ -4166,8 +4966,8 @@ function sendIntercom(targetSession) {
4166
4966
  transport.sendKeys(targetSession, "q");
4167
4967
  }
4168
4968
  transport.sendKeys(targetSession, "/exe-intercom");
4169
- recordDebounce(targetSession);
4170
- logIntercom(`DELIVERED \u2192 ${targetSession} (fire-and-forget)`);
4969
+ const batched = recordDebounce(targetSession);
4970
+ logIntercom(`DELIVERED \u2192 ${targetSession}${batched > 0 ? ` [${batched} nudges batched during debounce]` : ""} (fire-and-forget)`);
4171
4971
  return "delivered";
4172
4972
  } catch {
4173
4973
  logIntercom(`FAIL \u2192 ${targetSession}`);
@@ -4197,7 +4997,7 @@ function notifyParentExe(sessionKey) {
4197
4997
  return true;
4198
4998
  }
4199
4999
  function ensureEmployee(employeeName, exeSession, projectDir, opts) {
4200
- if (employeeName === "exe" || isCoordinatorName(employeeName)) {
5000
+ if (isCoordinatorName(employeeName)) {
4201
5001
  return { status: "failed", sessionName: "", error: "The COO is not a dispatchable employee" };
4202
5002
  }
4203
5003
  try {
@@ -4269,26 +5069,26 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
4269
5069
  const transport = getTransport();
4270
5070
  const sessionName = employeeSessionName(employeeName, exeSession, opts?.instance);
4271
5071
  const instanceLabel = opts?.instance != null && opts.instance > 0 ? `${employeeName}${opts.instance}` : employeeName;
4272
- const logDir = path14.join(os7.homedir(), ".exe-os", "session-logs");
4273
- const logFile = path14.join(logDir, `${instanceLabel}-${Date.now()}.log`);
4274
- if (!existsSync10(logDir)) {
4275
- mkdirSync5(logDir, { recursive: true });
5072
+ const logDir = path16.join(os8.homedir(), ".exe-os", "session-logs");
5073
+ const logFile = path16.join(logDir, `${instanceLabel}-${Date.now()}.log`);
5074
+ if (!existsSync12(logDir)) {
5075
+ mkdirSync6(logDir, { recursive: true });
4276
5076
  }
4277
5077
  transport.kill(sessionName);
4278
5078
  let cleanupSuffix = "";
4279
5079
  try {
4280
- const thisFile = fileURLToPath(import.meta.url);
4281
- const cleanupScript = path14.join(path14.dirname(thisFile), "..", "bin", "exe-session-cleanup.js");
4282
- if (existsSync10(cleanupScript)) {
5080
+ const thisFile = fileURLToPath2(import.meta.url);
5081
+ const cleanupScript = path16.join(path16.dirname(thisFile), "..", "bin", "exe-session-cleanup.js");
5082
+ if (existsSync12(cleanupScript)) {
4283
5083
  cleanupSuffix = `; ${process.execPath} "${cleanupScript}" "${employeeName}" "${exeSession}"`;
4284
5084
  }
4285
5085
  } catch {
4286
5086
  }
4287
5087
  try {
4288
- const claudeJsonPath = path14.join(os7.homedir(), ".claude.json");
5088
+ const claudeJsonPath = path16.join(os8.homedir(), ".claude.json");
4289
5089
  let claudeJson = {};
4290
5090
  try {
4291
- claudeJson = JSON.parse(readFileSync9(claudeJsonPath, "utf8"));
5091
+ claudeJson = JSON.parse(readFileSync11(claudeJsonPath, "utf8"));
4292
5092
  } catch {
4293
5093
  }
4294
5094
  if (!claudeJson.projects) claudeJson.projects = {};
@@ -4296,17 +5096,17 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
4296
5096
  const trustDir = opts?.cwd ?? projectDir;
4297
5097
  if (!projects[trustDir]) projects[trustDir] = {};
4298
5098
  projects[trustDir].hasTrustDialogAccepted = true;
4299
- writeFileSync6(claudeJsonPath, JSON.stringify(claudeJson, null, 2) + "\n");
5099
+ writeFileSync7(claudeJsonPath, JSON.stringify(claudeJson, null, 2) + "\n");
4300
5100
  } catch {
4301
5101
  }
4302
5102
  try {
4303
- const settingsDir = path14.join(os7.homedir(), ".claude", "projects");
5103
+ const settingsDir = path16.join(os8.homedir(), ".claude", "projects");
4304
5104
  const normalizedKey = (opts?.cwd ?? projectDir).replace(/\//g, "-").replace(/^-/, "");
4305
- const projSettingsDir = path14.join(settingsDir, normalizedKey);
4306
- const settingsPath = path14.join(projSettingsDir, "settings.json");
5105
+ const projSettingsDir = path16.join(settingsDir, normalizedKey);
5106
+ const settingsPath = path16.join(projSettingsDir, "settings.json");
4307
5107
  let settings = {};
4308
5108
  try {
4309
- settings = JSON.parse(readFileSync9(settingsPath, "utf8"));
5109
+ settings = JSON.parse(readFileSync11(settingsPath, "utf8"));
4310
5110
  } catch {
4311
5111
  }
4312
5112
  const perms = settings.permissions ?? {};
@@ -4334,21 +5134,24 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
4334
5134
  if (changed) {
4335
5135
  perms.allow = allow;
4336
5136
  settings.permissions = perms;
4337
- mkdirSync5(projSettingsDir, { recursive: true });
4338
- writeFileSync6(settingsPath, JSON.stringify(settings, null, 2) + "\n");
5137
+ mkdirSync6(projSettingsDir, { recursive: true });
5138
+ writeFileSync7(settingsPath, JSON.stringify(settings, null, 2) + "\n");
4339
5139
  }
4340
5140
  } catch {
4341
5141
  }
4342
5142
  const spawnCwd = opts?.cwd ?? projectDir;
4343
5143
  const useExeAgent = !!(opts?.model && opts?.provider);
4344
- const ccProvider = useExeAgent ? DEFAULT_PROVIDER : detectActiveProvider();
5144
+ const agentRtConfig = getAgentRuntime(employeeName);
5145
+ const useCodex = !useExeAgent && agentRtConfig.runtime === "codex";
5146
+ const useOpencode = !useExeAgent && !useCodex && agentRtConfig.runtime === "opencode";
5147
+ const ccProvider = useExeAgent || useCodex || useOpencode ? DEFAULT_PROVIDER : detectActiveProvider();
4345
5148
  const useBinSymlink = ccProvider !== DEFAULT_PROVIDER;
4346
5149
  let identityFlag = "";
4347
5150
  let behaviorsFlag = "";
4348
5151
  let legacyFallbackWarned = false;
4349
5152
  if (!useExeAgent && !useBinSymlink) {
4350
- const identityPath = path14.join(
4351
- os7.homedir(),
5153
+ const identityPath = path16.join(
5154
+ os8.homedir(),
4352
5155
  ".exe-os",
4353
5156
  "identity",
4354
5157
  `${employeeName}.md`
@@ -4357,13 +5160,13 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
4357
5160
  const hasAgentFlag = claudeSupportsAgentFlag();
4358
5161
  if (hasAgentFlag) {
4359
5162
  identityFlag = ` --agent ${employeeName}`;
4360
- } else if (existsSync10(identityPath)) {
5163
+ } else if (existsSync12(identityPath)) {
4361
5164
  identityFlag = ` --append-system-prompt-file ${identityPath}`;
4362
5165
  legacyFallbackWarned = true;
4363
5166
  }
4364
5167
  const behaviorsFile = exportBehaviorsSync(
4365
5168
  employeeName,
4366
- path14.basename(spawnCwd),
5169
+ path16.basename(spawnCwd),
4367
5170
  sessionName
4368
5171
  );
4369
5172
  if (behaviorsFile) {
@@ -4378,16 +5181,16 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
4378
5181
  }
4379
5182
  let sessionContextFlag = "";
4380
5183
  try {
4381
- const ctxDir = path14.join(os7.homedir(), ".exe-os", "session-cache");
4382
- mkdirSync5(ctxDir, { recursive: true });
4383
- const ctxFile = path14.join(ctxDir, `session-context-${sessionName}.md`);
5184
+ const ctxDir = path16.join(os8.homedir(), ".exe-os", "session-cache");
5185
+ mkdirSync6(ctxDir, { recursive: true });
5186
+ const ctxFile = path16.join(ctxDir, `session-context-${sessionName}.md`);
4384
5187
  const ctxContent = [
4385
5188
  `## Session Context`,
4386
5189
  `You are running in tmux session: ${sessionName}.`,
4387
5190
  `Your parent coordinator session is ${exeSession}.`,
4388
5191
  `Your employees (if any) use the -${exeSession} suffix.`
4389
5192
  ].join("\n");
4390
- writeFileSync6(ctxFile, ctxContent);
5193
+ writeFileSync7(ctxFile, ctxContent);
4391
5194
  sessionContextFlag = ` --append-system-prompt-file ${ctxFile}`;
4392
5195
  } catch {
4393
5196
  }
@@ -4401,9 +5204,48 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
4401
5204
  }
4402
5205
  }
4403
5206
  }
5207
+ if (useCodex) {
5208
+ const codexCfg = RUNTIME_TABLE.codex;
5209
+ if (codexCfg?.apiKeyEnv) {
5210
+ const keyVal = process.env[codexCfg.apiKeyEnv];
5211
+ if (keyVal) {
5212
+ envPrefix = `${envPrefix} ${codexCfg.apiKeyEnv}=${keyVal}`;
5213
+ }
5214
+ }
5215
+ envPrefix = `${envPrefix} EXE_AGENT_MODEL=${agentRtConfig.model}`;
5216
+ }
5217
+ if (useOpencode) {
5218
+ const ocCfg = PROVIDER_TABLE.opencode;
5219
+ if (ocCfg?.apiKeyEnv) {
5220
+ const keyVal = process.env[ocCfg.apiKeyEnv];
5221
+ if (keyVal) {
5222
+ envPrefix = `${envPrefix} ${ocCfg.apiKeyEnv}=${keyVal}`;
5223
+ }
5224
+ }
5225
+ envPrefix = `${envPrefix} ANTHROPIC_MODEL=${agentRtConfig.model}`;
5226
+ }
5227
+ if (!useExeAgent && !useCodex && !useOpencode && !useBinSymlink) {
5228
+ const defaultClaudeModel = DEFAULT_MODELS.claude;
5229
+ if (agentRtConfig.runtime === "claude" && agentRtConfig.model !== defaultClaudeModel) {
5230
+ envPrefix = `${envPrefix} ANTHROPIC_MODEL=${agentRtConfig.model}`;
5231
+ }
5232
+ }
4404
5233
  let spawnCommand;
4405
5234
  if (useExeAgent) {
4406
5235
  spawnCommand = `${envPrefix} exe-agent --employee ${employeeName} --model ${opts.model} --provider ${opts.provider}${cleanupSuffix}`;
5236
+ } else if (useCodex) {
5237
+ process.stderr.write(
5238
+ `[tmux-routing] agent-config: ${employeeName} \u2192 codex (${agentRtConfig.model})
5239
+ `
5240
+ );
5241
+ spawnCommand = `${envPrefix} exe-start-codex --agent ${employeeName}${cleanupSuffix}`;
5242
+ } else if (useOpencode) {
5243
+ const binName = `${employeeName}-opencode`;
5244
+ process.stderr.write(
5245
+ `[tmux-routing] agent-config: ${employeeName} \u2192 opencode (${agentRtConfig.model})
5246
+ `
5247
+ );
5248
+ spawnCommand = `${envPrefix} ${binName}${cleanupSuffix}`;
4407
5249
  } else if (useBinSymlink) {
4408
5250
  const binName = `${employeeName}-${ccProvider}`;
4409
5251
  process.stderr.write(
@@ -4419,17 +5261,19 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
4419
5261
  command: spawnCommand
4420
5262
  });
4421
5263
  if (spawnResult.error) {
4422
- releaseSpawnLock(sessionName);
5264
+ releaseSpawnLock2(sessionName);
4423
5265
  return { sessionName, error: `tmux new-session failed: ${spawnResult.error}` };
4424
5266
  }
4425
5267
  transport.pipeLog(sessionName, logFile);
4426
5268
  try {
4427
5269
  const mySession = getMySession();
4428
- const dispatchInfo = path14.join(SESSION_CACHE, `dispatch-info-${sessionName}.json`);
4429
- writeFileSync6(dispatchInfo, JSON.stringify({
5270
+ const dispatchInfo = path16.join(SESSION_CACHE, `dispatch-info-${sessionName}.json`);
5271
+ writeFileSync7(dispatchInfo, JSON.stringify({
4430
5272
  dispatchedBy: mySession,
4431
5273
  rootExe: exeSession,
4432
- provider: useBinSymlink ? ccProvider : useExeAgent ? opts.provider : "anthropic",
5274
+ provider: useBinSymlink ? ccProvider : useExeAgent ? opts.provider : useCodex ? "openai" : useOpencode ? "opencode" : "anthropic",
5275
+ runtime: useCodex ? "codex" : useOpencode ? "opencode" : useExeAgent ? "exe-agent" : "claude",
5276
+ model: useCodex ? agentRtConfig.model : useOpencode ? agentRtConfig.model : void 0,
4433
5277
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
4434
5278
  }));
4435
5279
  } catch {
@@ -4447,6 +5291,11 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
4447
5291
  booted = true;
4448
5292
  break;
4449
5293
  }
5294
+ } else if (useCodex) {
5295
+ if (pane.includes("codex") || pane.includes("Codex") || pane.includes("exe-start-codex")) {
5296
+ booted = true;
5297
+ break;
5298
+ }
4450
5299
  } else {
4451
5300
  if (pane.includes("Claude Code") || pane.includes("\u276F")) {
4452
5301
  booted = true;
@@ -4457,10 +5306,11 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
4457
5306
  }
4458
5307
  }
4459
5308
  if (!booted) {
4460
- releaseSpawnLock(sessionName);
4461
- return { sessionName, error: `${useExeAgent ? "exe-agent" : "claude"} did not boot within 15s` };
5309
+ releaseSpawnLock2(sessionName);
5310
+ const runtimeLabel = useExeAgent ? "exe-agent" : useCodex ? "codex" : "claude";
5311
+ return { sessionName, error: `${runtimeLabel} did not boot within 15s` };
4462
5312
  }
4463
- if (!useExeAgent) {
5313
+ if (!useExeAgent && !useCodex) {
4464
5314
  try {
4465
5315
  transport.sendKeys(sessionName, `/exe-call ${employeeName}`);
4466
5316
  } catch {
@@ -4474,7 +5324,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
4474
5324
  pid: 0,
4475
5325
  registeredAt: (/* @__PURE__ */ new Date()).toISOString()
4476
5326
  });
4477
- releaseSpawnLock(sessionName);
5327
+ releaseSpawnLock2(sessionName);
4478
5328
  return { sessionName };
4479
5329
  }
4480
5330
  var SPAWN_LOCK_DIR, SESSION_CACHE, BEHAVIORS_EXPORT_TIMEOUT_MS, VALID_SESSION_NAME, VERIFY_PANE_LINES, INTERCOM_DEBOUNCE_MS, INTERCOM_LOG2, DEBOUNCE_FILE, DEBOUNCE_CLEANUP_AGE_MS, BUSY_PATTERN;
@@ -4487,17 +5337,19 @@ var init_tmux_routing = __esm({
4487
5337
  init_cc_agent_support();
4488
5338
  init_mcp_prefix();
4489
5339
  init_provider_table();
5340
+ init_agent_config();
5341
+ init_runtime_table();
4490
5342
  init_intercom_queue();
4491
5343
  init_plan_limits();
4492
5344
  init_employees();
4493
- SPAWN_LOCK_DIR = path14.join(os7.homedir(), ".exe-os", "spawn-locks");
4494
- SESSION_CACHE = path14.join(os7.homedir(), ".exe-os", "session-cache");
5345
+ SPAWN_LOCK_DIR = path16.join(os8.homedir(), ".exe-os", "spawn-locks");
5346
+ SESSION_CACHE = path16.join(os8.homedir(), ".exe-os", "session-cache");
4495
5347
  BEHAVIORS_EXPORT_TIMEOUT_MS = 1e4;
4496
5348
  VALID_SESSION_NAME = /^[a-z]+\d*-[a-zA-Z0-9_]+$/;
4497
5349
  VERIFY_PANE_LINES = 200;
4498
5350
  INTERCOM_DEBOUNCE_MS = 3e4;
4499
- INTERCOM_LOG2 = path14.join(os7.homedir(), ".exe-os", "intercom.log");
4500
- DEBOUNCE_FILE = path14.join(SESSION_CACHE, "intercom-debounce.json");
5351
+ INTERCOM_LOG2 = path16.join(os8.homedir(), ".exe-os", "intercom.log");
5352
+ DEBOUNCE_FILE = path16.join(SESSION_CACHE, "intercom-debounce.json");
4501
5353
  DEBOUNCE_CLEANUP_AGE_MS = 5 * 60 * 1e3;
4502
5354
  BUSY_PATTERN = /[✻✽✶✳·].*…|Running…/;
4503
5355
  }
@@ -4514,14 +5366,14 @@ var init_memory = __esm({
4514
5366
 
4515
5367
  // src/lib/keychain.ts
4516
5368
  import { readFile as readFile4, writeFile as writeFile5, unlink, mkdir as mkdir4, chmod as chmod2 } from "fs/promises";
4517
- import { existsSync as existsSync11 } from "fs";
4518
- import path15 from "path";
4519
- import os8 from "os";
5369
+ import { existsSync as existsSync13 } from "fs";
5370
+ import path17 from "path";
5371
+ import os9 from "os";
4520
5372
  function getKeyDir() {
4521
- return process.env.EXE_OS_DIR ?? process.env.EXE_MEM_DIR ?? path15.join(os8.homedir(), ".exe-os");
5373
+ return process.env.EXE_OS_DIR ?? process.env.EXE_MEM_DIR ?? path17.join(os9.homedir(), ".exe-os");
4522
5374
  }
4523
5375
  function getKeyPath() {
4524
- return path15.join(getKeyDir(), "master.key");
5376
+ return path17.join(getKeyDir(), "master.key");
4525
5377
  }
4526
5378
  async function tryKeytar() {
4527
5379
  try {
@@ -4542,13 +5394,21 @@ async function getMasterKey() {
4542
5394
  }
4543
5395
  }
4544
5396
  const keyPath = getKeyPath();
4545
- if (!existsSync11(keyPath)) {
5397
+ if (!existsSync13(keyPath)) {
5398
+ process.stderr.write(
5399
+ `[keychain] Key not found at ${keyPath} (HOME=${os9.homedir()}, EXE_OS_DIR=${process.env.EXE_OS_DIR ?? "unset"})
5400
+ `
5401
+ );
4546
5402
  return null;
4547
5403
  }
4548
5404
  try {
4549
5405
  const content = await readFile4(keyPath, "utf-8");
4550
5406
  return Buffer.from(content.trim(), "base64");
4551
- } catch {
5407
+ } catch (err) {
5408
+ process.stderr.write(
5409
+ `[keychain] Key read failed at ${keyPath}: ${err instanceof Error ? err.message : String(err)}
5410
+ `
5411
+ );
4552
5412
  return null;
4553
5413
  }
4554
5414
  }
@@ -4574,13 +5434,13 @@ __export(shard_manager_exports, {
4574
5434
  listShards: () => listShards,
4575
5435
  shardExists: () => shardExists
4576
5436
  });
4577
- import path16 from "path";
4578
- import { existsSync as existsSync12, mkdirSync as mkdirSync6, readdirSync as readdirSync3 } from "fs";
5437
+ import path18 from "path";
5438
+ import { existsSync as existsSync14, mkdirSync as mkdirSync7, readdirSync as readdirSync3 } from "fs";
4579
5439
  import { createClient as createClient2 } from "@libsql/client";
4580
5440
  function initShardManager(encryptionKey) {
4581
5441
  _encryptionKey = encryptionKey;
4582
- if (!existsSync12(SHARDS_DIR)) {
4583
- mkdirSync6(SHARDS_DIR, { recursive: true });
5442
+ if (!existsSync14(SHARDS_DIR)) {
5443
+ mkdirSync7(SHARDS_DIR, { recursive: true });
4584
5444
  }
4585
5445
  _shardingEnabled = true;
4586
5446
  }
@@ -4600,7 +5460,7 @@ function getShardClient(projectName) {
4600
5460
  }
4601
5461
  const cached = _shards.get(safeName);
4602
5462
  if (cached) return cached;
4603
- const dbPath = path16.join(SHARDS_DIR, `${safeName}.db`);
5463
+ const dbPath = path18.join(SHARDS_DIR, `${safeName}.db`);
4604
5464
  const client = createClient2({
4605
5465
  url: `file:${dbPath}`,
4606
5466
  encryptionKey: _encryptionKey
@@ -4610,10 +5470,10 @@ function getShardClient(projectName) {
4610
5470
  }
4611
5471
  function shardExists(projectName) {
4612
5472
  const safeName = projectName.replace(/[^a-zA-Z0-9_-]/g, "_");
4613
- return existsSync12(path16.join(SHARDS_DIR, `${safeName}.db`));
5473
+ return existsSync14(path18.join(SHARDS_DIR, `${safeName}.db`));
4614
5474
  }
4615
5475
  function listShards() {
4616
- if (!existsSync12(SHARDS_DIR)) return [];
5476
+ if (!existsSync14(SHARDS_DIR)) return [];
4617
5477
  return readdirSync3(SHARDS_DIR).filter((f) => f.endsWith(".db")).map((f) => f.replace(".db", ""));
4618
5478
  }
4619
5479
  async function ensureShardSchema(client) {
@@ -4799,7 +5659,7 @@ var init_shard_manager = __esm({
4799
5659
  "src/lib/shard-manager.ts"() {
4800
5660
  "use strict";
4801
5661
  init_config();
4802
- SHARDS_DIR = path16.join(EXE_AI_DIR, "shards");
5662
+ SHARDS_DIR = path18.join(EXE_AI_DIR, "shards");
4803
5663
  _shards = /* @__PURE__ */ new Map();
4804
5664
  _encryptionKey = null;
4805
5665
  _shardingEnabled = false;
@@ -4924,7 +5784,7 @@ __export(global_procedures_exports, {
4924
5784
  loadGlobalProcedures: () => loadGlobalProcedures,
4925
5785
  storeGlobalProcedure: () => storeGlobalProcedure
4926
5786
  });
4927
- import { randomUUID as randomUUID3 } from "crypto";
5787
+ import { randomUUID as randomUUID4 } from "crypto";
4928
5788
  async function loadGlobalProcedures() {
4929
5789
  const client = getClient();
4930
5790
  const result = await client.execute({
@@ -4953,7 +5813,7 @@ ${sections.join("\n\n")}
4953
5813
  `;
4954
5814
  }
4955
5815
  async function storeGlobalProcedure(input) {
4956
- const id = randomUUID3();
5816
+ const id = randomUUID4();
4957
5817
  const now = (/* @__PURE__ */ new Date()).toISOString();
4958
5818
  const client = getClient();
4959
5819
  await client.execute({
@@ -5004,6 +5864,7 @@ __export(store_exports, {
5004
5864
  vectorToBlob: () => vectorToBlob,
5005
5865
  writeMemory: () => writeMemory
5006
5866
  });
5867
+ import { createHash } from "crypto";
5007
5868
  function isBusyError2(err) {
5008
5869
  if (err instanceof Error) {
5009
5870
  const msg = err.message.toLowerCase();
@@ -5077,12 +5938,52 @@ function classifyTier(record) {
5077
5938
  if (["store_memory", "manual"].includes(record.tool_name ?? "") && (record.importance ?? 0) >= 5) return 2;
5078
5939
  return 3;
5079
5940
  }
5941
+ function inferFilePaths(record) {
5942
+ if (!["Read", "Write", "Edit"].includes(record.tool_name)) return null;
5943
+ const firstLine = record.raw_text.split("\n")[0] ?? "";
5944
+ const match = firstLine.match(/(\/[\w./-]+\.\w+)/);
5945
+ return match ? JSON.stringify([match[1]]) : null;
5946
+ }
5947
+ function inferCommitHash(record) {
5948
+ if (record.tool_name !== "Bash") return null;
5949
+ const match = record.raw_text.match(/\b([a-f0-9]{7,40})\b/);
5950
+ return match ? match[1] : null;
5951
+ }
5952
+ function inferLanguageType(record) {
5953
+ const text = record.raw_text;
5954
+ if (!text || text.length < 10) return null;
5955
+ const trimmed = text.trimStart();
5956
+ if (trimmed.startsWith("{") || trimmed.startsWith("[")) return "json";
5957
+ if (/\b(SELECT|INSERT|UPDATE|DELETE|CREATE TABLE|ALTER TABLE)\b/i.test(text)) return "sql";
5958
+ if (/\b(function |const |import |export |class |def |async |=>)\b/.test(text)) return "code";
5959
+ if (trimmed.startsWith("#") || trimmed.startsWith("*")) return "prose";
5960
+ return "mixed";
5961
+ }
5962
+ function inferDomain(record) {
5963
+ const proj = (record.project_name ?? "").toLowerCase();
5964
+ if (proj.includes("marketing") || proj.includes("content")) return "marketing";
5965
+ if (proj.includes("crm") || proj.includes("customer")) return "customer";
5966
+ return null;
5967
+ }
5080
5968
  async function writeMemory(record) {
5081
5969
  if (record.vector !== null && record.vector.length !== EMBEDDING_DIM) {
5082
5970
  throw new Error(
5083
5971
  `Expected ${EMBEDDING_DIM}-dim vector, got ${record.vector.length}`
5084
5972
  );
5085
5973
  }
5974
+ const contentHash = createHash("md5").update(record.raw_text).digest("hex");
5975
+ if (_pendingRecords.some((r) => r.content_hash === contentHash && r.agent_id === record.agent_id)) {
5976
+ return;
5977
+ }
5978
+ try {
5979
+ const client = getClient();
5980
+ const existing = await client.execute({
5981
+ sql: "SELECT id FROM memories WHERE content_hash = ? AND agent_id = ? LIMIT 1",
5982
+ args: [contentHash, record.agent_id]
5983
+ });
5984
+ if (existing.rows.length > 0) return;
5985
+ } catch {
5986
+ }
5086
5987
  const dbRow = {
5087
5988
  id: record.id,
5088
5989
  agent_id: record.agent_id,
@@ -5112,7 +6013,23 @@ async function writeMemory(record) {
5112
6013
  supersedes_id: record.supersedes_id ?? null,
5113
6014
  draft: record.draft ? 1 : 0,
5114
6015
  memory_type: record.memory_type ?? "raw",
5115
- trajectory: record.trajectory ? JSON.stringify(record.trajectory) : null
6016
+ trajectory: record.trajectory ? JSON.stringify(record.trajectory) : null,
6017
+ content_hash: contentHash,
6018
+ intent: record.intent ?? null,
6019
+ outcome: record.outcome ?? null,
6020
+ domain: record.domain ?? inferDomain(record),
6021
+ referenced_entities: record.referenced_entities ?? null,
6022
+ retrieval_count: record.retrieval_count ?? 0,
6023
+ chain_position: record.chain_position ?? null,
6024
+ review_status: record.review_status ?? null,
6025
+ context_window_pct: record.context_window_pct ?? null,
6026
+ file_paths: record.file_paths ?? inferFilePaths(record),
6027
+ commit_hash: record.commit_hash ?? inferCommitHash(record),
6028
+ duration_ms: record.duration_ms ?? null,
6029
+ token_cost: record.token_cost ?? null,
6030
+ audience: record.audience ?? null,
6031
+ language_type: record.language_type ?? inferLanguageType(record),
6032
+ parent_memory_id: record.parent_memory_id ?? null
5116
6033
  };
5117
6034
  _pendingRecords.push(dbRow);
5118
6035
  orgBus.emit({
@@ -5170,80 +6087,85 @@ async function flushBatch() {
5170
6087
  const draft = row.draft ? 1 : 0;
5171
6088
  const memoryType = row.memory_type ?? "raw";
5172
6089
  const trajectory = row.trajectory ?? null;
5173
- return {
5174
- sql: hasVector ? `INSERT OR IGNORE INTO memories
5175
- (id, agent_id, agent_role, session_id, timestamp,
5176
- tool_name, project_name,
5177
- has_error, raw_text, vector, version, task_id, importance, status,
5178
- confidence, last_accessed,
5179
- workspace_id, document_id, user_id, char_offset, page_number,
5180
- source_path, source_type, tier, supersedes_id, draft, memory_type, trajectory)
5181
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, vector32(?), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` : `INSERT OR IGNORE INTO memories
5182
- (id, agent_id, agent_role, session_id, timestamp,
6090
+ const contentHash = row.content_hash ?? null;
6091
+ const intent = row.intent ?? null;
6092
+ const outcome = row.outcome ?? null;
6093
+ const domain = row.domain ?? null;
6094
+ const referencedEntities = row.referenced_entities ?? null;
6095
+ const retrievalCount = row.retrieval_count ?? 0;
6096
+ const chainPosition = row.chain_position ?? null;
6097
+ const reviewStatus = row.review_status ?? null;
6098
+ const contextWindowPct = row.context_window_pct ?? null;
6099
+ const filePaths = row.file_paths ?? null;
6100
+ const commitHash = row.commit_hash ?? null;
6101
+ const durationMs = row.duration_ms ?? null;
6102
+ const tokenCost = row.token_cost ?? null;
6103
+ const audience = row.audience ?? null;
6104
+ const languageType = row.language_type ?? null;
6105
+ const parentMemoryId = row.parent_memory_id ?? null;
6106
+ const cols = `id, agent_id, agent_role, session_id, timestamp,
5183
6107
  tool_name, project_name,
5184
6108
  has_error, raw_text, vector, version, task_id, importance, status,
5185
6109
  confidence, last_accessed,
5186
6110
  workspace_id, document_id, user_id, char_offset, page_number,
5187
- source_path, source_type, tier, supersedes_id, draft, memory_type, trajectory)
5188
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
5189
- args: hasVector ? [
5190
- row.id,
5191
- row.agent_id,
5192
- row.agent_role,
5193
- row.session_id,
5194
- row.timestamp,
5195
- row.tool_name,
5196
- row.project_name,
5197
- row.has_error,
5198
- row.raw_text,
5199
- vectorToBlob(row.vector),
5200
- row.version,
5201
- taskId,
5202
- importance,
5203
- status,
5204
- confidence,
5205
- lastAccessed,
5206
- workspaceId,
5207
- documentId,
5208
- userId,
5209
- charOffset,
5210
- pageNumber,
5211
- sourcePath,
5212
- sourceType,
5213
- tier,
5214
- supersedesId,
5215
- draft,
5216
- memoryType,
5217
- trajectory
5218
- ] : [
5219
- row.id,
5220
- row.agent_id,
5221
- row.agent_role,
5222
- row.session_id,
5223
- row.timestamp,
5224
- row.tool_name,
5225
- row.project_name,
5226
- row.has_error,
5227
- row.raw_text,
5228
- row.version,
5229
- taskId,
5230
- importance,
5231
- status,
5232
- confidence,
5233
- lastAccessed,
5234
- workspaceId,
5235
- documentId,
5236
- userId,
5237
- charOffset,
5238
- pageNumber,
5239
- sourcePath,
5240
- sourceType,
5241
- tier,
5242
- supersedesId,
5243
- draft,
5244
- memoryType,
5245
- trajectory
5246
- ]
6111
+ source_path, source_type, tier, supersedes_id, draft, memory_type, trajectory, content_hash,
6112
+ intent, outcome, domain, referenced_entities, retrieval_count,
6113
+ chain_position, review_status, context_window_pct, file_paths, commit_hash,
6114
+ duration_ms, token_cost, audience, language_type, parent_memory_id`;
6115
+ const metaArgs = [
6116
+ intent,
6117
+ outcome,
6118
+ domain,
6119
+ referencedEntities,
6120
+ retrievalCount,
6121
+ chainPosition,
6122
+ reviewStatus,
6123
+ contextWindowPct,
6124
+ filePaths,
6125
+ commitHash,
6126
+ durationMs,
6127
+ tokenCost,
6128
+ audience,
6129
+ languageType,
6130
+ parentMemoryId
6131
+ ];
6132
+ const baseArgs = [
6133
+ row.id,
6134
+ row.agent_id,
6135
+ row.agent_role,
6136
+ row.session_id,
6137
+ row.timestamp,
6138
+ row.tool_name,
6139
+ row.project_name,
6140
+ row.has_error,
6141
+ row.raw_text
6142
+ ];
6143
+ const sharedArgs = [
6144
+ row.version,
6145
+ taskId,
6146
+ importance,
6147
+ status,
6148
+ confidence,
6149
+ lastAccessed,
6150
+ workspaceId,
6151
+ documentId,
6152
+ userId,
6153
+ charOffset,
6154
+ pageNumber,
6155
+ sourcePath,
6156
+ sourceType,
6157
+ tier,
6158
+ supersedesId,
6159
+ draft,
6160
+ memoryType,
6161
+ trajectory,
6162
+ contentHash
6163
+ ];
6164
+ return {
6165
+ sql: hasVector ? `INSERT OR IGNORE INTO memories (${cols})
6166
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, vector32(?), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` : `INSERT OR IGNORE INTO memories (${cols})
6167
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
6168
+ args: hasVector ? [...baseArgs, vectorToBlob(row.vector), ...sharedArgs, ...metaArgs] : [...baseArgs, ...sharedArgs, ...metaArgs]
5247
6169
  };
5248
6170
  };
5249
6171
  const globalClient = getClient();
@@ -5526,8 +6448,8 @@ function findContainingChunk(filePath, snippet) {
5526
6448
  try {
5527
6449
  const ext = filePath.split(".").pop()?.toLowerCase();
5528
6450
  if (ext !== "ts" && ext !== "tsx" && ext !== "js" && ext !== "jsx") return "";
5529
- const { readFileSync: readFileSync13 } = __require("fs");
5530
- const source = readFileSync13(filePath, "utf8");
6451
+ const { readFileSync: readFileSync14 } = __require("fs");
6452
+ const source = readFileSync14(filePath, "utf8");
5531
6453
  const lines = source.split("\n");
5532
6454
  const lowerSnippet = snippet.toLowerCase().slice(0, 80);
5533
6455
  let matchLine = -1;
@@ -5593,9 +6515,9 @@ function extractBash(input, response) {
5593
6515
  }
5594
6516
  function extractGrep(input, response) {
5595
6517
  const pattern = String(input.pattern ?? "");
5596
- const path20 = input.path ? String(input.path) : "";
6518
+ const path21 = input.path ? String(input.path) : "";
5597
6519
  const output = String(response.text ?? response.content ?? JSON.stringify(response).slice(0, MAX_OUTPUT));
5598
- return `Searched for "${pattern}"${path20 ? ` in ${path20}` : ""}
6520
+ return `Searched for "${pattern}"${path21 ? ` in ${path21}` : ""}
5599
6521
  ${output.slice(0, MAX_OUTPUT)}`;
5600
6522
  }
5601
6523
  function extractGlob(input, response) {
@@ -5953,491 +6875,146 @@ async function createTimelineActivity(personId, params) {
5953
6875
  direction: params.direction,
5954
6876
  accountId: params.accountId || null
5955
6877
  },
5956
- targetPersonId: personId
5957
- };
5958
- const query = `
5959
- mutation CreateTimelineActivity($input: TimelineActivityCreateInput!) {
5960
- createTimelineActivity(data: $input) {
5961
- id
5962
- }
5963
- }
5964
- `;
5965
- const res = await gqlRequest(
5966
- query,
5967
- { input }
5968
- );
5969
- if (res.errors && res.errors.length > 0) {
5970
- console.error(
5971
- "[crm-bridge] createTimelineActivity error:",
5972
- res.errors[0].message
5973
- );
5974
- return false;
5975
- }
5976
- return true;
5977
- }
5978
- async function pushConversationToCRM(params) {
5979
- if (!config) return;
5980
- try {
5981
- let personId = await findPersonByContact(params.platform, params.senderId);
5982
- if (!personId) {
5983
- personId = await createPerson(
5984
- params.platform,
5985
- params.senderId,
5986
- params.senderName
5987
- );
5988
- }
5989
- if (!personId) {
5990
- console.error("[crm-bridge] Failed to find or create person for", params.senderId);
5991
- return;
5992
- }
5993
- const ok = await createTimelineActivity(personId, {
5994
- ...params,
5995
- direction: "conversation"
5996
- });
5997
- if (ok) {
5998
- console.log(
5999
- `[crm-bridge] Pushed ${params.platform}/${params.senderId} \u2192 person ${personId}`
6000
- );
6001
- }
6002
- } catch (err) {
6003
- console.error("[crm-bridge] Push failed:", err instanceof Error ? err.message : err);
6004
- }
6005
- }
6006
- async function pushInboundMessageToCRM(params) {
6007
- if (!config) return;
6008
- try {
6009
- let personId = await findPersonByContact(params.platform, params.senderId);
6010
- if (!personId) {
6011
- personId = await createPerson(
6012
- params.platform,
6013
- params.senderId,
6014
- params.senderName
6015
- );
6016
- }
6017
- if (!personId) {
6018
- console.error("[crm-bridge] Failed to find or create person for", params.senderId);
6019
- return;
6020
- }
6021
- const ok = await createTimelineActivity(personId, {
6022
- ...params,
6023
- direction: "inbound"
6024
- });
6025
- if (ok) {
6026
- console.log(
6027
- `[crm-bridge] Inbound ${params.platform}/${params.senderId} \u2192 person ${personId}`
6028
- );
6029
- }
6030
- } catch (err) {
6031
- console.error("[crm-bridge] Inbound push failed:", err instanceof Error ? err.message : err);
6032
- }
6033
- }
6034
- async function pushGatewayEventToCRM(params) {
6035
- if (!config) return;
6036
- try {
6037
- let personId = await findPersonByContact(params.platform, params.senderId);
6038
- if (!personId) {
6039
- personId = await createPerson(
6040
- params.platform,
6041
- params.senderId,
6042
- params.senderName
6043
- );
6044
- }
6045
- if (!personId) {
6046
- console.error("[crm-bridge] Failed to find or create person for", params.senderId);
6047
- return;
6048
- }
6049
- const channelLabel = params.platform.charAt(0).toUpperCase() + params.platform.slice(1);
6050
- const categoryLabel = params.dataCategory.replace(/_/g, " ");
6051
- const input = {
6052
- name: `${channelLabel} ${categoryLabel} \u2014 ${params.senderName || params.senderId}`,
6053
- happensAt: params.timestamp,
6054
- properties: {
6055
- channel: params.platform,
6056
- dataCategory: params.dataCategory,
6057
- senderId: params.senderId,
6058
- senderName: params.senderName || null,
6059
- accountId: params.accountId || null,
6060
- direction: "inbound",
6061
- ...params.eventData
6062
- },
6063
- targetPersonId: personId
6064
- };
6065
- const query = `
6066
- mutation CreateTimelineActivity($input: TimelineActivityCreateInput!) {
6067
- createTimelineActivity(data: $input) {
6068
- id
6069
- }
6070
- }
6071
- `;
6072
- const res = await gqlRequest(
6073
- query,
6074
- { input }
6075
- );
6076
- if (res.errors && res.errors.length > 0) {
6077
- console.error(
6078
- "[crm-bridge] pushGatewayEvent error:",
6079
- res.errors[0].message
6080
- );
6081
- return;
6082
- }
6083
- console.log(
6084
- `[crm-bridge] Event ${params.dataCategory} ${params.platform}/${params.senderId} \u2192 person ${personId}`
6085
- );
6086
- } catch (err) {
6087
- console.error("[crm-bridge] Event push failed:", err instanceof Error ? err.message : err);
6088
- }
6089
- }
6090
- var config, disabledLogged;
6091
- var init_crm_bridge = __esm({
6092
- "src/gateway/crm-bridge.ts"() {
6093
- "use strict";
6094
- config = null;
6095
- disabledLogged = false;
6096
- }
6097
- });
6098
-
6099
- // src/lib/exe-daemon-client.ts
6100
- import net from "net";
6101
- import { spawn } from "child_process";
6102
- import { randomUUID as randomUUID6 } from "crypto";
6103
- import { existsSync as existsSync13, unlinkSync as unlinkSync6, readFileSync as readFileSync10, openSync, closeSync, statSync } from "fs";
6104
- import path17 from "path";
6105
- import { fileURLToPath as fileURLToPath2 } from "url";
6106
- function handleData(chunk) {
6107
- _buffer += chunk.toString();
6108
- if (_buffer.length > MAX_BUFFER) {
6109
- _buffer = "";
6110
- return;
6111
- }
6112
- let newlineIdx;
6113
- while ((newlineIdx = _buffer.indexOf("\n")) !== -1) {
6114
- const line = _buffer.slice(0, newlineIdx).trim();
6115
- _buffer = _buffer.slice(newlineIdx + 1);
6116
- if (!line) continue;
6117
- try {
6118
- const response = JSON.parse(line);
6119
- const entry = _pending.get(response.id);
6120
- if (entry) {
6121
- clearTimeout(entry.timer);
6122
- _pending.delete(response.id);
6123
- entry.resolve(response);
6124
- }
6125
- } catch {
6126
- }
6127
- }
6128
- }
6129
- function cleanupStaleFiles() {
6130
- if (existsSync13(PID_PATH)) {
6131
- try {
6132
- const pid = parseInt(readFileSync10(PID_PATH, "utf8").trim(), 10);
6133
- if (pid > 0) {
6134
- try {
6135
- process.kill(pid, 0);
6136
- return;
6137
- } catch {
6138
- }
6139
- }
6140
- } catch {
6141
- }
6142
- try {
6143
- unlinkSync6(PID_PATH);
6144
- } catch {
6145
- }
6146
- try {
6147
- unlinkSync6(SOCKET_PATH);
6148
- } catch {
6149
- }
6150
- }
6151
- }
6152
- function findPackageRoot() {
6153
- let dir = path17.dirname(fileURLToPath2(import.meta.url));
6154
- const { root } = path17.parse(dir);
6155
- while (dir !== root) {
6156
- if (existsSync13(path17.join(dir, "package.json"))) return dir;
6157
- dir = path17.dirname(dir);
6158
- }
6159
- return null;
6160
- }
6161
- function spawnDaemon() {
6162
- const pkgRoot = findPackageRoot();
6163
- if (!pkgRoot) {
6164
- process.stderr.write("[exed-client] WARN: cannot find package root\n");
6165
- return;
6166
- }
6167
- const daemonPath = path17.join(pkgRoot, "dist", "lib", "exe-daemon.js");
6168
- if (!existsSync13(daemonPath)) {
6169
- process.stderr.write(`[exed-client] WARN: daemon script not found at ${daemonPath}
6170
- `);
6171
- return;
6172
- }
6173
- const resolvedPath = daemonPath;
6174
- process.stderr.write(`[exed-client] Spawning daemon: ${resolvedPath}
6175
- `);
6176
- const logPath = path17.join(path17.dirname(SOCKET_PATH), "exed.log");
6177
- let stderrFd = "ignore";
6178
- try {
6179
- stderrFd = openSync(logPath, "a");
6180
- } catch {
6181
- }
6182
- const child = spawn(process.execPath, [resolvedPath], {
6183
- detached: true,
6184
- stdio: ["ignore", "ignore", stderrFd],
6185
- env: {
6186
- ...process.env,
6187
- TMUX: void 0,
6188
- // Daemon is global — must not inherit session scope
6189
- TMUX_PANE: void 0,
6190
- // Prevents resolveExeSession() from scoping to one session
6191
- EXE_DAEMON_SOCK: SOCKET_PATH,
6192
- EXE_DAEMON_PID: PID_PATH
6193
- }
6194
- });
6195
- child.unref();
6196
- if (typeof stderrFd === "number") {
6197
- try {
6198
- closeSync(stderrFd);
6199
- } catch {
6200
- }
6201
- }
6202
- }
6203
- function acquireSpawnLock2() {
6204
- try {
6205
- const fd = openSync(SPAWN_LOCK_PATH, "wx");
6206
- closeSync(fd);
6207
- return true;
6208
- } catch {
6209
- try {
6210
- const stat = statSync(SPAWN_LOCK_PATH);
6211
- if (Date.now() - stat.mtimeMs > SPAWN_LOCK_STALE_MS) {
6212
- try {
6213
- unlinkSync6(SPAWN_LOCK_PATH);
6214
- } catch {
6215
- }
6216
- try {
6217
- const fd = openSync(SPAWN_LOCK_PATH, "wx");
6218
- closeSync(fd);
6219
- return true;
6220
- } catch {
6221
- }
6222
- }
6223
- } catch {
6224
- }
6225
- return false;
6226
- }
6227
- }
6228
- function releaseSpawnLock2() {
6229
- try {
6230
- unlinkSync6(SPAWN_LOCK_PATH);
6231
- } catch {
6232
- }
6233
- }
6234
- function connectToSocket() {
6235
- return new Promise((resolve) => {
6236
- if (_socket && _connected) {
6237
- resolve(true);
6238
- return;
6239
- }
6240
- const socket = net.createConnection({ path: SOCKET_PATH });
6241
- const connectTimeout = setTimeout(() => {
6242
- socket.destroy();
6243
- resolve(false);
6244
- }, 2e3);
6245
- socket.on("connect", () => {
6246
- clearTimeout(connectTimeout);
6247
- _socket = socket;
6248
- _connected = true;
6249
- _buffer = "";
6250
- socket.on("data", handleData);
6251
- socket.on("close", () => {
6252
- _connected = false;
6253
- _socket = null;
6254
- for (const [id, entry] of _pending) {
6255
- clearTimeout(entry.timer);
6256
- _pending.delete(id);
6257
- entry.resolve({ error: "Connection closed" });
6258
- }
6259
- });
6260
- socket.on("error", () => {
6261
- _connected = false;
6262
- _socket = null;
6263
- });
6264
- resolve(true);
6265
- });
6266
- socket.on("error", () => {
6267
- clearTimeout(connectTimeout);
6268
- resolve(false);
6269
- });
6270
- });
6271
- }
6272
- async function connectEmbedDaemon() {
6273
- if (_socket && _connected) return true;
6274
- if (await connectToSocket()) return true;
6275
- if (acquireSpawnLock2()) {
6276
- try {
6277
- cleanupStaleFiles();
6278
- spawnDaemon();
6279
- } finally {
6280
- releaseSpawnLock2();
6281
- }
6282
- }
6283
- const start = Date.now();
6284
- let delay2 = 100;
6285
- while (Date.now() - start < CONNECT_TIMEOUT_MS) {
6286
- await new Promise((r) => setTimeout(r, delay2));
6287
- if (await connectToSocket()) return true;
6288
- delay2 = Math.min(delay2 * 2, 3e3);
6289
- }
6290
- return false;
6291
- }
6292
- function sendRequest(texts, priority) {
6293
- return new Promise((resolve) => {
6294
- if (!_socket || !_connected) {
6295
- resolve({ error: "Not connected" });
6296
- return;
6297
- }
6298
- const id = randomUUID6();
6299
- const timer = setTimeout(() => {
6300
- _pending.delete(id);
6301
- resolve({ error: "Request timeout" });
6302
- }, REQUEST_TIMEOUT_MS);
6303
- _pending.set(id, { resolve, timer });
6304
- try {
6305
- _socket.write(JSON.stringify({ id, texts, priority }) + "\n");
6306
- } catch {
6307
- clearTimeout(timer);
6308
- _pending.delete(id);
6309
- resolve({ error: "Write failed" });
6310
- }
6311
- });
6312
- }
6313
- async function pingDaemon() {
6314
- if (!_socket || !_connected) return null;
6315
- return new Promise((resolve) => {
6316
- const id = randomUUID6();
6317
- const timer = setTimeout(() => {
6318
- _pending.delete(id);
6319
- resolve(null);
6320
- }, 5e3);
6321
- _pending.set(id, {
6322
- resolve: (data) => {
6323
- if (data.health) {
6324
- resolve(data.health);
6325
- } else {
6326
- resolve(null);
6327
- }
6328
- },
6329
- timer
6330
- });
6331
- try {
6332
- _socket.write(JSON.stringify({ id, type: "health" }) + "\n");
6333
- } catch {
6334
- clearTimeout(timer);
6335
- _pending.delete(id);
6336
- resolve(null);
6337
- }
6338
- });
6339
- }
6340
- function killAndRespawnDaemon() {
6341
- process.stderr.write("[exed-client] Killing daemon for restart...\n");
6342
- if (existsSync13(PID_PATH)) {
6343
- try {
6344
- const pid = parseInt(readFileSync10(PID_PATH, "utf8").trim(), 10);
6345
- if (pid > 0) {
6346
- try {
6347
- process.kill(pid, "SIGKILL");
6348
- } catch {
6349
- }
6878
+ targetPersonId: personId
6879
+ };
6880
+ const query = `
6881
+ mutation CreateTimelineActivity($input: TimelineActivityCreateInput!) {
6882
+ createTimelineActivity(data: $input) {
6883
+ id
6350
6884
  }
6351
- } catch {
6352
6885
  }
6886
+ `;
6887
+ const res = await gqlRequest(
6888
+ query,
6889
+ { input }
6890
+ );
6891
+ if (res.errors && res.errors.length > 0) {
6892
+ console.error(
6893
+ "[crm-bridge] createTimelineActivity error:",
6894
+ res.errors[0].message
6895
+ );
6896
+ return false;
6353
6897
  }
6354
- if (_socket) {
6355
- _socket.destroy();
6356
- _socket = null;
6357
- }
6358
- _connected = false;
6359
- _buffer = "";
6360
- try {
6361
- unlinkSync6(PID_PATH);
6362
- } catch {
6363
- }
6898
+ return true;
6899
+ }
6900
+ async function pushConversationToCRM(params) {
6901
+ if (!config) return;
6364
6902
  try {
6365
- unlinkSync6(SOCKET_PATH);
6366
- } catch {
6903
+ let personId = await findPersonByContact(params.platform, params.senderId);
6904
+ if (!personId) {
6905
+ personId = await createPerson(
6906
+ params.platform,
6907
+ params.senderId,
6908
+ params.senderName
6909
+ );
6910
+ }
6911
+ if (!personId) {
6912
+ console.error("[crm-bridge] Failed to find or create person for", params.senderId);
6913
+ return;
6914
+ }
6915
+ const ok = await createTimelineActivity(personId, {
6916
+ ...params,
6917
+ direction: "conversation"
6918
+ });
6919
+ if (ok) {
6920
+ console.log(
6921
+ `[crm-bridge] Pushed ${params.platform}/${params.senderId} \u2192 person ${personId}`
6922
+ );
6923
+ }
6924
+ } catch (err) {
6925
+ console.error("[crm-bridge] Push failed:", err instanceof Error ? err.message : err);
6367
6926
  }
6368
- spawnDaemon();
6369
6927
  }
6370
- async function embedViaClient(text, priority = "high") {
6371
- if (!_connected && !await connectEmbedDaemon()) return null;
6372
- _requestCount++;
6373
- if (_requestCount % HEALTH_CHECK_INTERVAL === 0) {
6374
- const health = await pingDaemon();
6375
- if (!health) {
6376
- process.stderr.write(`[exed-client] Periodic health check failed at request ${_requestCount} \u2014 restarting daemon
6377
- `);
6378
- killAndRespawnDaemon();
6379
- const start = Date.now();
6380
- let delay2 = 200;
6381
- while (Date.now() - start < CONNECT_TIMEOUT_MS) {
6382
- await new Promise((r) => setTimeout(r, delay2));
6383
- if (await connectToSocket()) break;
6384
- delay2 = Math.min(delay2 * 2, 3e3);
6385
- }
6386
- if (!_connected) return null;
6928
+ async function pushInboundMessageToCRM(params) {
6929
+ if (!config) return;
6930
+ try {
6931
+ let personId = await findPersonByContact(params.platform, params.senderId);
6932
+ if (!personId) {
6933
+ personId = await createPerson(
6934
+ params.platform,
6935
+ params.senderId,
6936
+ params.senderName
6937
+ );
6387
6938
  }
6388
- }
6389
- const result = await sendRequest([text], priority);
6390
- if (!result.error && result.vectors?.[0]) return result.vectors[0];
6391
- if (result.error) {
6392
- process.stderr.write(`[exed-client] Embed failed (${result.error}) \u2014 attempting restart
6393
- `);
6394
- killAndRespawnDaemon();
6395
- const start = Date.now();
6396
- let delay2 = 200;
6397
- while (Date.now() - start < CONNECT_TIMEOUT_MS) {
6398
- await new Promise((r) => setTimeout(r, delay2));
6399
- if (await connectToSocket()) break;
6400
- delay2 = Math.min(delay2 * 2, 3e3);
6939
+ if (!personId) {
6940
+ console.error("[crm-bridge] Failed to find or create person for", params.senderId);
6941
+ return;
6401
6942
  }
6402
- if (!_connected) return null;
6403
- const retry = await sendRequest([text], priority);
6404
- if (!retry.error && retry.vectors?.[0]) return retry.vectors[0];
6405
- process.stderr.write(`[exed-client] Embed retry also failed: ${retry.error ?? "no vector"}
6406
- `);
6943
+ const ok = await createTimelineActivity(personId, {
6944
+ ...params,
6945
+ direction: "inbound"
6946
+ });
6947
+ if (ok) {
6948
+ console.log(
6949
+ `[crm-bridge] Inbound ${params.platform}/${params.senderId} \u2192 person ${personId}`
6950
+ );
6951
+ }
6952
+ } catch (err) {
6953
+ console.error("[crm-bridge] Inbound push failed:", err instanceof Error ? err.message : err);
6407
6954
  }
6408
- return null;
6409
6955
  }
6410
- function disconnectClient() {
6411
- if (_socket) {
6412
- _socket.destroy();
6413
- _socket = null;
6414
- }
6415
- _connected = false;
6416
- _buffer = "";
6417
- for (const [id, entry] of _pending) {
6418
- clearTimeout(entry.timer);
6419
- _pending.delete(id);
6420
- entry.resolve({ error: "Client disconnected" });
6956
+ async function pushGatewayEventToCRM(params) {
6957
+ if (!config) return;
6958
+ try {
6959
+ let personId = await findPersonByContact(params.platform, params.senderId);
6960
+ if (!personId) {
6961
+ personId = await createPerson(
6962
+ params.platform,
6963
+ params.senderId,
6964
+ params.senderName
6965
+ );
6966
+ }
6967
+ if (!personId) {
6968
+ console.error("[crm-bridge] Failed to find or create person for", params.senderId);
6969
+ return;
6970
+ }
6971
+ const channelLabel = params.platform.charAt(0).toUpperCase() + params.platform.slice(1);
6972
+ const categoryLabel = params.dataCategory.replace(/_/g, " ");
6973
+ const input = {
6974
+ name: `${channelLabel} ${categoryLabel} \u2014 ${params.senderName || params.senderId}`,
6975
+ happensAt: params.timestamp,
6976
+ properties: {
6977
+ channel: params.platform,
6978
+ dataCategory: params.dataCategory,
6979
+ senderId: params.senderId,
6980
+ senderName: params.senderName || null,
6981
+ accountId: params.accountId || null,
6982
+ direction: "inbound",
6983
+ ...params.eventData
6984
+ },
6985
+ targetPersonId: personId
6986
+ };
6987
+ const query = `
6988
+ mutation CreateTimelineActivity($input: TimelineActivityCreateInput!) {
6989
+ createTimelineActivity(data: $input) {
6990
+ id
6991
+ }
6992
+ }
6993
+ `;
6994
+ const res = await gqlRequest(
6995
+ query,
6996
+ { input }
6997
+ );
6998
+ if (res.errors && res.errors.length > 0) {
6999
+ console.error(
7000
+ "[crm-bridge] pushGatewayEvent error:",
7001
+ res.errors[0].message
7002
+ );
7003
+ return;
7004
+ }
7005
+ console.log(
7006
+ `[crm-bridge] Event ${params.dataCategory} ${params.platform}/${params.senderId} \u2192 person ${personId}`
7007
+ );
7008
+ } catch (err) {
7009
+ console.error("[crm-bridge] Event push failed:", err instanceof Error ? err.message : err);
6421
7010
  }
6422
7011
  }
6423
- var SOCKET_PATH, PID_PATH, SPAWN_LOCK_PATH, SPAWN_LOCK_STALE_MS, CONNECT_TIMEOUT_MS, REQUEST_TIMEOUT_MS, _socket, _connected, _buffer, _requestCount, HEALTH_CHECK_INTERVAL, _pending, MAX_BUFFER;
6424
- var init_exe_daemon_client = __esm({
6425
- "src/lib/exe-daemon-client.ts"() {
7012
+ var config, disabledLogged;
7013
+ var init_crm_bridge = __esm({
7014
+ "src/gateway/crm-bridge.ts"() {
6426
7015
  "use strict";
6427
- init_config();
6428
- SOCKET_PATH = process.env.EXE_DAEMON_SOCK ?? process.env.EXE_EMBED_SOCK ?? path17.join(EXE_AI_DIR, "exed.sock");
6429
- PID_PATH = process.env.EXE_DAEMON_PID ?? process.env.EXE_EMBED_PID ?? path17.join(EXE_AI_DIR, "exed.pid");
6430
- SPAWN_LOCK_PATH = path17.join(EXE_AI_DIR, "exed-spawn.lock");
6431
- SPAWN_LOCK_STALE_MS = 3e4;
6432
- CONNECT_TIMEOUT_MS = 15e3;
6433
- REQUEST_TIMEOUT_MS = 3e4;
6434
- _socket = null;
6435
- _connected = false;
6436
- _buffer = "";
6437
- _requestCount = 0;
6438
- HEALTH_CHECK_INTERVAL = 100;
6439
- _pending = /* @__PURE__ */ new Map();
6440
- MAX_BUFFER = 1e7;
7016
+ config = null;
7017
+ disabledLogged = false;
6441
7018
  }
6442
7019
  });
6443
7020
 
@@ -6478,10 +7055,10 @@ async function disposeEmbedder() {
6478
7055
  async function embedDirect(text) {
6479
7056
  const llamaCpp = await import("node-llama-cpp");
6480
7057
  const { MODELS_DIR: MODELS_DIR2 } = await Promise.resolve().then(() => (init_config(), config_exports));
6481
- const { existsSync: existsSync15 } = await import("fs");
6482
- const path20 = await import("path");
6483
- const modelPath = path20.join(MODELS_DIR2, "jina-embeddings-v5-small-q4_k_m.gguf");
6484
- if (!existsSync15(modelPath)) {
7058
+ const { existsSync: existsSync16 } = await import("fs");
7059
+ const path21 = await import("path");
7060
+ const modelPath = path21.join(MODELS_DIR2, "jina-embeddings-v5-small-q4_k_m.gguf");
7061
+ if (!existsSync16(modelPath)) {
6485
7062
  throw new Error(`Embedding model not found at ${modelPath}. Run '/exe-setup' to download it.`);
6486
7063
  }
6487
7064
  const llama = await llamaCpp.getLlama();
@@ -6518,8 +7095,8 @@ __export(wiki_client_exports, {
6518
7095
  listDocuments: () => listDocuments,
6519
7096
  listWorkspaces: () => listWorkspaces
6520
7097
  });
6521
- async function wikiFetch(config2, path20, method = "GET", body) {
6522
- const url = `${config2.baseUrl}/api/v1${path20}`;
7098
+ async function wikiFetch(config2, path21, method = "GET", body) {
7099
+ const url = `${config2.baseUrl}/api/v1${path21}`;
6523
7100
  const headers = {
6524
7101
  Authorization: `Bearer ${config2.apiKey}`,
6525
7102
  "Content-Type": "application/json"
@@ -6552,7 +7129,7 @@ async function wikiFetch(config2, path20, method = "GET", body) {
6552
7129
  }
6553
7130
  }
6554
7131
  if (!response.ok) {
6555
- throw new Error(`Wiki API ${method} ${path20}: ${response.status} ${response.statusText}`);
7132
+ throw new Error(`Wiki API ${method} ${path21}: ${response.status} ${response.statusText}`);
6556
7133
  }
6557
7134
  return response.json();
6558
7135
  } finally {
@@ -6646,7 +7223,7 @@ var LOCAL_WIKI_URL, REQUEST_TIMEOUT_MS2;
6646
7223
  var init_wiki_client = __esm({
6647
7224
  "src/lib/wiki-client.ts"() {
6648
7225
  "use strict";
6649
- LOCAL_WIKI_URL = "http://localhost:3001";
7226
+ LOCAL_WIKI_URL = process.env.EXE_WIKI_URL || "http://localhost:3001";
6650
7227
  REQUEST_TIMEOUT_MS2 = 8e3;
6651
7228
  }
6652
7229
  });
@@ -7573,13 +8150,13 @@ __export(whatsapp_accounts_exports, {
7573
8150
  getDefaultAccount: () => getDefaultAccount,
7574
8151
  loadAccounts: () => loadAccounts
7575
8152
  });
7576
- import { readFileSync as readFileSync11 } from "fs";
8153
+ import { readFileSync as readFileSync12 } from "fs";
7577
8154
  import { join as join2 } from "path";
7578
8155
  import { homedir as homedir2 } from "os";
7579
8156
  function loadAccounts() {
7580
8157
  if (cachedAccounts !== null) return cachedAccounts;
7581
8158
  try {
7582
- const raw = readFileSync11(CONFIG_PATH2, "utf8");
8159
+ const raw = readFileSync12(CONFIG_PATH2, "utf8");
7583
8160
  const parsed = JSON.parse(raw);
7584
8161
  if (!Array.isArray(parsed)) {
7585
8162
  console.warn("[whatsapp] Config is not an array, ignoring");
@@ -9554,11 +10131,11 @@ function createQuietRenderer() {
9554
10131
  init_state_bus();
9555
10132
 
9556
10133
  // src/runtime/session-manager.ts
9557
- import { randomUUID as randomUUID5 } from "crypto";
10134
+ import { randomUUID as randomUUID6 } from "crypto";
9558
10135
  init_state_bus();
9559
10136
 
9560
10137
  // src/runtime/exe-hooks.ts
9561
- import { randomUUID as randomUUID4 } from "crypto";
10138
+ import { randomUUID as randomUUID5 } from "crypto";
9562
10139
  function createExeOSHooks(config2) {
9563
10140
  let sessionRegistered = false;
9564
10141
  return {
@@ -9617,7 +10194,7 @@ function createExeOSHooks(config2) {
9617
10194
  const toolResponse = result.isError ? { error: result.content } : { output: result.content };
9618
10195
  const rawText = extractSemanticText2(toolName, toolInput, toolResponse);
9619
10196
  await writeMemory2({
9620
- id: randomUUID4(),
10197
+ id: randomUUID5(),
9621
10198
  agent_id: config2.agentId,
9622
10199
  agent_role: "employee",
9623
10200
  session_id: `api-${config2.agentId}`,
@@ -9640,7 +10217,7 @@ function createExeOSHooks(config2) {
9640
10217
  const { writeMemory: writeMemory2 } = await Promise.resolve().then(() => (init_store(), store_exports));
9641
10218
  if (summary) {
9642
10219
  await writeMemory2({
9643
- id: randomUUID4(),
10220
+ id: randomUUID5(),
9644
10221
  agent_id: config2.agentId,
9645
10222
  agent_role: "employee",
9646
10223
  session_id: `api-${config2.agentId}`,
@@ -9668,8 +10245,8 @@ function createExeOSHooks(config2) {
9668
10245
  },
9669
10246
  async onError(error, _context) {
9670
10247
  try {
9671
- const { classifyError: classifyError2 } = await Promise.resolve().then(() => (init_error_detector(), error_detector_exports));
9672
- const classification = classifyError2(error.message);
10248
+ const { classifyError: classifyError3 } = await Promise.resolve().then(() => (init_error_detector(), error_detector_exports));
10249
+ const classification = classifyError3(error.message);
9673
10250
  process.stderr.write(
9674
10251
  `[exe-hooks] Error (${classification}): ${error.message.slice(0, 100)}
9675
10252
  `
@@ -9713,7 +10290,7 @@ function createExeOSHooks(config2) {
9713
10290
  await client.execute({
9714
10291
  sql: `INSERT OR IGNORE INTO notifications (id, type, source_agent, message, task_file, created_at, read)
9715
10292
  VALUES (?, 'system', ?, ?, NULL, ?, 0)`,
9716
- args: [randomUUID4(), config2.agentId, message.slice(0, 500), (/* @__PURE__ */ new Date()).toISOString()]
10293
+ args: [randomUUID5(), config2.agentId, message.slice(0, 500), (/* @__PURE__ */ new Date()).toISOString()]
9717
10294
  });
9718
10295
  } catch {
9719
10296
  }
@@ -9729,7 +10306,7 @@ function createExeOSHooks(config2) {
9729
10306
  try {
9730
10307
  const { writeMemory: writeMemory2 } = await Promise.resolve().then(() => (init_store(), store_exports));
9731
10308
  await writeMemory2({
9732
- id: randomUUID4(),
10309
+ id: randomUUID5(),
9733
10310
  agent_id: config2.agentId,
9734
10311
  agent_role: "employee",
9735
10312
  session_id: `api-${config2.agentId}`,
@@ -9751,7 +10328,7 @@ function createExeOSHooks(config2) {
9751
10328
  try {
9752
10329
  const { writeMemory: writeMemory2 } = await Promise.resolve().then(() => (init_store(), store_exports));
9753
10330
  await writeMemory2({
9754
- id: randomUUID4(),
10331
+ id: randomUUID5(),
9755
10332
  agent_id: config2.agentId,
9756
10333
  agent_role: "employee",
9757
10334
  session_id: `api-${config2.agentId}`,
@@ -9790,7 +10367,7 @@ function createExeOSHooks(config2) {
9790
10367
  if (tasks.rows.length > 0) {
9791
10368
  const taskList = tasks.rows.map((r) => `- [${String(r.status)}] ${String(r.title)} (${String(r.task_file)})`).join("\n");
9792
10369
  await writeMemory2({
9793
- id: randomUUID4(),
10370
+ id: randomUUID5(),
9794
10371
  agent_id: config2.agentId,
9795
10372
  agent_role: "employee",
9796
10373
  session_id: `api-${config2.agentId}`,
@@ -9816,7 +10393,7 @@ ${taskList}`,
9816
10393
  try {
9817
10394
  const { writeMemory: writeMemory2 } = await Promise.resolve().then(() => (init_store(), store_exports));
9818
10395
  await writeMemory2({
9819
- id: randomUUID4(),
10396
+ id: randomUUID5(),
9820
10397
  agent_id: config2.agentId,
9821
10398
  agent_role: "employee",
9822
10399
  session_id: `api-${config2.agentId}`,
@@ -9843,7 +10420,7 @@ var SessionManager = class {
9843
10420
  eventHandlers = /* @__PURE__ */ new Set();
9844
10421
  /** Start a new agent session for an employee */
9845
10422
  startSession(employeeId, config2) {
9846
- const sessionId = randomUUID5();
10423
+ const sessionId = randomUUID6();
9847
10424
  const abortController = new AbortController();
9848
10425
  const session = {
9849
10426
  info: {
@@ -10096,13 +10673,16 @@ __export(gateway_exports, {
10096
10673
  FULL_ACCESS: () => FULL_ACCESS,
10097
10674
  FailoverCascade: () => FailoverCascade,
10098
10675
  FailoverExhaustedError: () => FailoverExhaustedError,
10676
+ FatalBotError: () => FatalBotError,
10099
10677
  Gateway: () => Gateway,
10100
10678
  IMessageAdapter: () => IMessageAdapter,
10679
+ MaxStepsError: () => MaxStepsError,
10101
10680
  OllamaProvider: () => OllamaProvider,
10102
10681
  OpenAICompatProvider: () => OpenAICompatProvider,
10103
10682
  READ_ONLY: () => READ_ONLY,
10104
10683
  READ_TOOLS: () => READ_TOOLS,
10105
10684
  RateLimiter: () => RateLimiter,
10685
+ RecoverableBotError: () => RecoverableBotError,
10106
10686
  SessionStore: () => SessionStore,
10107
10687
  SignalAdapter: () => SignalAdapter,
10108
10688
  SlackAdapter: () => SlackAdapter,
@@ -10114,6 +10694,7 @@ __export(gateway_exports, {
10114
10694
  buildExecAssistantTools: () => buildExecAssistantTools,
10115
10695
  buildPermissionContext: () => buildPermissionContext,
10116
10696
  checkToolPermission: () => checkToolPermission,
10697
+ classifyError: () => classifyError2,
10117
10698
  createCRMWebhookHandler: () => createCRMWebhookHandler,
10118
10699
  createPerson: () => createPerson,
10119
10700
  createReceptionist: () => createReceptionist,
@@ -10789,10 +11370,49 @@ function buildPermissionContext(platform, permissions) {
10789
11370
  return `[${platform.toUpperCase()} \u2014 allowed: ${parts.join(", ")}]`;
10790
11371
  }
10791
11372
 
11373
+ // src/gateway/bot-errors.ts
11374
+ var FatalBotError = class extends Error {
11375
+ constructor(message, cause) {
11376
+ super(message);
11377
+ this.cause = cause;
11378
+ this.name = "FatalBotError";
11379
+ }
11380
+ fatal = true;
11381
+ };
11382
+ var RecoverableBotError = class extends Error {
11383
+ constructor(message, toolName, cause) {
11384
+ super(message);
11385
+ this.toolName = toolName;
11386
+ this.cause = cause;
11387
+ this.name = "RecoverableBotError";
11388
+ }
11389
+ recoverable = true;
11390
+ };
11391
+ var MaxStepsError = class extends Error {
11392
+ constructor(stepsTaken, maxSteps) {
11393
+ super(
11394
+ `Reached maximum steps (${stepsTaken}/${maxSteps}). Returning partial result.`
11395
+ );
11396
+ this.stepsTaken = stepsTaken;
11397
+ this.maxSteps = maxSteps;
11398
+ this.name = "MaxStepsError";
11399
+ }
11400
+ };
11401
+ function classifyError2(err, toolName) {
11402
+ if (err instanceof FatalBotError) return err;
11403
+ if (err instanceof RecoverableBotError) return err;
11404
+ const message = err instanceof Error ? err.message : String(err);
11405
+ if (message.includes("401") || message.includes("403") || message.includes("authentication") || message.includes("rate_limit")) {
11406
+ return new FatalBotError(message, err);
11407
+ }
11408
+ return new RecoverableBotError(message, toolName, err);
11409
+ }
11410
+
10792
11411
  // src/gateway/bot-runtime.ts
10793
11412
  var DEFAULT_MODEL = "claude-sonnet-4-20250514";
10794
11413
  var MAX_TURNS = 10;
10795
11414
  var MAX_HISTORY = 50;
11415
+ var DEFAULT_PLANNING_INTERVAL = 3;
10796
11416
  function buildExecAssistantSystemPrompt(platform, permissions) {
10797
11417
  const permContext = buildPermissionContext(platform, permissions);
10798
11418
  return `You are the founder's executive assistant (agent_id: "ea").
@@ -10844,7 +11464,7 @@ var BotRuntime = class {
10844
11464
  async processMessage(msg, permissions) {
10845
11465
  const sessionKey = msg.chatType === "group" ? msg.channelId : msg.senderId;
10846
11466
  const history = this.getHistory(sessionKey);
10847
- history.push({ role: "user", content: msg.text });
11467
+ history.push({ role: "user", content: msg.text, frameType: "task" });
10848
11468
  const systemPrompt = this.config.systemPrompt + "\n\n" + buildPermissionContext(msg.platform, permissions);
10849
11469
  const allowedTools = filterToolsForPermissions(
10850
11470
  this.config.tools,
@@ -10852,29 +11472,54 @@ var BotRuntime = class {
10852
11472
  );
10853
11473
  const model = this.config.model ?? DEFAULT_MODEL;
10854
11474
  const maxTurns = this.config.maxTurns ?? MAX_TURNS;
11475
+ const planningInterval = this.config.planningInterval ?? DEFAULT_PLANNING_INTERVAL;
10855
11476
  let turns = 0;
10856
11477
  while (turns < maxTurns) {
10857
11478
  turns++;
10858
- const response = await this.client.messages.create({
10859
- model,
10860
- max_tokens: 4096,
10861
- system: systemPrompt,
10862
- messages: history.map((m) => ({
10863
- role: m.role,
10864
- content: m.content
10865
- })),
10866
- tools: allowedTools.map((t) => ({
10867
- name: t.name,
10868
- description: t.description,
10869
- input_schema: t.input_schema
10870
- }))
10871
- });
11479
+ if (planningInterval > 0 && turns > 1 && turns % planningInterval === 1 && maxTurns - turns >= 2) {
11480
+ history.push({
11481
+ role: "user",
11482
+ content: "[Planning checkpoint] Review what you know so far. What facts have you gathered? What is your plan for the remaining steps? Be concise.",
11483
+ frameType: "planning"
11484
+ });
11485
+ }
11486
+ let response;
11487
+ try {
11488
+ response = await this.client.messages.create({
11489
+ model,
11490
+ max_tokens: 4096,
11491
+ system: systemPrompt,
11492
+ messages: history.map((m) => ({
11493
+ role: m.role,
11494
+ content: m.content
11495
+ })),
11496
+ tools: allowedTools.map((t) => ({
11497
+ name: t.name,
11498
+ description: t.description,
11499
+ input_schema: t.input_schema
11500
+ }))
11501
+ });
11502
+ } catch (err) {
11503
+ const classified = classifyError2(err);
11504
+ if (classified instanceof FatalBotError) {
11505
+ const errorMsg = `Bot error: ${classified.message}`;
11506
+ history.push({ role: "assistant", content: errorMsg, frameType: "error" });
11507
+ this.trimHistory(sessionKey);
11508
+ return errorMsg;
11509
+ }
11510
+ history.push({
11511
+ role: "assistant",
11512
+ content: `[API error \u2014 retrying] ${classified.message}`,
11513
+ frameType: "error"
11514
+ });
11515
+ continue;
11516
+ }
10872
11517
  const toolUseBlocks = response.content.filter(
10873
11518
  (b) => b.type === "tool_use"
10874
11519
  );
10875
11520
  if (toolUseBlocks.length === 0) {
10876
11521
  const textContent = response.content.filter((b) => b.type === "text").map((b) => b.text).join("\n");
10877
- history.push({ role: "assistant", content: textContent });
11522
+ history.push({ role: "assistant", content: textContent, frameType: "assistant" });
10878
11523
  this.trimHistory(sessionKey);
10879
11524
  return textContent;
10880
11525
  }
@@ -10884,9 +11529,11 @@ var BotRuntime = class {
10884
11529
  );
10885
11530
  history.push({
10886
11531
  role: "assistant",
10887
- content: response.content
11532
+ content: response.content,
11533
+ frameType: "assistant"
10888
11534
  });
10889
11535
  const toolResults = [];
11536
+ let hadFatalError = false;
10890
11537
  for (const block of allowed) {
10891
11538
  try {
10892
11539
  const result = await this.config.toolExecutor(
@@ -10899,12 +11546,23 @@ var BotRuntime = class {
10899
11546
  content: result
10900
11547
  });
10901
11548
  } catch (err) {
10902
- toolResults.push({
10903
- type: "tool_result",
10904
- tool_use_id: block.id,
10905
- content: `Error: ${err instanceof Error ? err.message : String(err)}`,
10906
- is_error: true
10907
- });
11549
+ const classified = classifyError2(err, block.name);
11550
+ if (classified instanceof FatalBotError) {
11551
+ toolResults.push({
11552
+ type: "tool_result",
11553
+ tool_use_id: block.id,
11554
+ content: `Fatal error: ${classified.message}`,
11555
+ is_error: true
11556
+ });
11557
+ hadFatalError = true;
11558
+ } else {
11559
+ toolResults.push({
11560
+ type: "tool_result",
11561
+ tool_use_id: block.id,
11562
+ content: `Error (recoverable): ${classified.message}`,
11563
+ is_error: true
11564
+ });
11565
+ }
10908
11566
  }
10909
11567
  }
10910
11568
  for (const { block, check } of blocked) {
@@ -10917,10 +11575,22 @@ var BotRuntime = class {
10917
11575
  }
10918
11576
  history.push({
10919
11577
  role: "user",
10920
- content: toolResults
11578
+ content: toolResults,
11579
+ frameType: hadFatalError ? "error" : "tool_result"
10921
11580
  });
11581
+ if (hadFatalError) {
11582
+ this.trimHistory(sessionKey);
11583
+ return `A fatal error occurred during tool execution. The bot loop has been stopped.`;
11584
+ }
10922
11585
  }
10923
- return "I reached the maximum number of tool calls for this request. Please try again with a more specific question.";
11586
+ const maxErr = new MaxStepsError(turns, maxTurns);
11587
+ history.push({
11588
+ role: "assistant",
11589
+ content: maxErr.message,
11590
+ frameType: "error"
11591
+ });
11592
+ this.trimHistory(sessionKey);
11593
+ return maxErr.message;
10924
11594
  }
10925
11595
  getHistory(sessionKey) {
10926
11596
  if (!this.conversations.has(sessionKey)) {
@@ -10930,9 +11600,19 @@ var BotRuntime = class {
10930
11600
  }
10931
11601
  trimHistory(sessionKey) {
10932
11602
  const history = this.conversations.get(sessionKey);
10933
- if (history && history.length > MAX_HISTORY) {
10934
- this.conversations.set(sessionKey, history.slice(-MAX_HISTORY));
11603
+ if (!history || history.length <= MAX_HISTORY) return;
11604
+ const firstTaskIdx = history.findIndex((m) => m.frameType === "task");
11605
+ let trimmed = history.filter(
11606
+ (m, i) => m.frameType !== "planning" || i >= history.length - MAX_HISTORY
11607
+ );
11608
+ if (trimmed.length > MAX_HISTORY) {
11609
+ const tail = trimmed.slice(-MAX_HISTORY);
11610
+ if (firstTaskIdx >= 0 && !tail.includes(history[firstTaskIdx])) {
11611
+ tail[0] = history[firstTaskIdx];
11612
+ }
11613
+ trimmed = tail;
10935
11614
  }
11615
+ this.conversations.set(sessionKey, trimmed);
10936
11616
  }
10937
11617
  /** Clear conversation history for a session */
10938
11618
  clearHistory(sessionKey) {
@@ -11637,9 +12317,20 @@ var OllamaProvider = class {
11637
12317
  import { randomUUID as randomUUID9 } from "crypto";
11638
12318
  import { homedir } from "os";
11639
12319
  import { join } from "path";
11640
- import { mkdirSync as mkdirSync7 } from "fs";
11641
- var RECONNECT_DELAY_MS = 5e3;
12320
+ import { mkdirSync as mkdirSync8 } from "fs";
12321
+ var INITIAL_BACKOFF_MS = 1e3;
12322
+ var MAX_BACKOFF_MS = 3e5;
12323
+ var BACKOFF_MULTIPLIER = 2;
12324
+ var JITTER_FACTOR = 0.25;
11642
12325
  var AUTH_DIR = join(homedir(), ".exe-os", "whatsapp-auth");
12326
+ function calculateBackoff(retryCount) {
12327
+ const base = Math.min(
12328
+ INITIAL_BACKOFF_MS * BACKOFF_MULTIPLIER ** retryCount,
12329
+ MAX_BACKOFF_MS
12330
+ );
12331
+ const jitter = base * JITTER_FACTOR * (2 * Math.random() - 1);
12332
+ return Math.max(INITIAL_BACKOFF_MS, Math.round(base + jitter));
12333
+ }
11643
12334
  var WhatsAppAdapter = class {
11644
12335
  platform = "whatsapp";
11645
12336
  sock = null;
@@ -11647,14 +12338,31 @@ var WhatsAppAdapter = class {
11647
12338
  connected = false;
11648
12339
  abortController = null;
11649
12340
  authDir = AUTH_DIR;
12341
+ // Resilience state
12342
+ retryCount = 0;
12343
+ disconnectedAt = 0;
11650
12344
  async connect(config2) {
11651
12345
  this.authDir = config2.credentials.authDir ?? AUTH_DIR;
11652
- mkdirSync7(this.authDir, { recursive: true });
12346
+ mkdirSync8(this.authDir, { recursive: true });
11653
12347
  const baileys = await import("@whiskeysockets/baileys");
11654
12348
  const { makeWASocket, useMultiFileAuthState, fetchLatestBaileysVersion, DisconnectReason, makeCacheableSignalKeyStore } = baileys;
11655
12349
  const { state, saveCreds } = await useMultiFileAuthState(this.authDir);
11656
12350
  const { version } = await fetchLatestBaileysVersion();
11657
12351
  this.abortController = new AbortController();
12352
+ let agent;
12353
+ const socksProxy = config2.credentials.socksProxy;
12354
+ if (socksProxy) {
12355
+ try {
12356
+ const modName = "socks-proxy-agent";
12357
+ const mod = await import(modName);
12358
+ const SocksProxyAgent = mod.SocksProxyAgent ?? mod.default;
12359
+ agent = new SocksProxyAgent(socksProxy);
12360
+ console.log(`[whatsapp] Using SOCKS proxy: ${socksProxy.replace(/\/\/.*@/, "//***@")}`);
12361
+ } catch {
12362
+ console.error("[whatsapp] socks-proxy-agent not installed \u2014 run: npm i socks-proxy-agent");
12363
+ throw new Error("SOCKS proxy configured but socks-proxy-agent package not installed");
12364
+ }
12365
+ }
11658
12366
  const sock = makeWASocket({
11659
12367
  auth: {
11660
12368
  creds: state.creds,
@@ -11664,7 +12372,8 @@ var WhatsAppAdapter = class {
11664
12372
  printQRInTerminal: true,
11665
12373
  browser: ["exe-os", "cli", "1.0"],
11666
12374
  syncFullHistory: false,
11667
- markOnlineOnConnect: false
12375
+ markOnlineOnConnect: false,
12376
+ ...agent ? { agent } : {}
11668
12377
  });
11669
12378
  this.sock = sock;
11670
12379
  sock.ev.on("creds.update", saveCreds);
@@ -11672,18 +12381,32 @@ var WhatsAppAdapter = class {
11672
12381
  const { connection, lastDisconnect } = update;
11673
12382
  if (connection === "close") {
11674
12383
  this.connected = false;
12384
+ if (this.disconnectedAt === 0) this.disconnectedAt = Date.now();
11675
12385
  const statusCode = lastDisconnect?.error?.output?.statusCode;
11676
12386
  const shouldReconnect = statusCode !== DisconnectReason.loggedOut;
11677
12387
  if (shouldReconnect && !this.abortController?.signal.aborted) {
11678
- console.log(`[whatsapp] Connection closed (${statusCode}), reconnecting...`);
11679
- setTimeout(() => void this.connect(config2), RECONNECT_DELAY_MS);
12388
+ const delay2 = calculateBackoff(this.retryCount);
12389
+ this.retryCount++;
12390
+ console.log(
12391
+ `[whatsapp] Connection closed (code=${statusCode}), retry #${this.retryCount} in ${(delay2 / 1e3).toFixed(1)}s` + (socksProxy ? ` (proxy: ${socksProxy.replace(/\/\/.*@/, "//***@")})` : "")
12392
+ );
12393
+ setTimeout(() => void this.connect(config2), delay2);
11680
12394
  } else {
11681
12395
  console.log("[whatsapp] Logged out \u2014 clear auth and re-scan QR");
11682
12396
  }
11683
12397
  }
11684
12398
  if (connection === "open") {
12399
+ if (this.retryCount > 0) {
12400
+ const downtimeSec = this.disconnectedAt > 0 ? ((Date.now() - this.disconnectedAt) / 1e3).toFixed(1) : "?";
12401
+ console.log(
12402
+ `[whatsapp] Reconnected after ${this.retryCount} retries (${downtimeSec}s downtime)`
12403
+ );
12404
+ } else {
12405
+ console.log("[whatsapp] Connected via Baileys (linked device)");
12406
+ }
11685
12407
  this.connected = true;
11686
- console.log("[whatsapp] Connected via Baileys (linked device)");
12408
+ this.retryCount = 0;
12409
+ this.disconnectedAt = 0;
11687
12410
  }
11688
12411
  });
11689
12412
  sock.ev.on("messages.upsert", (upsert) => {
@@ -12922,12 +13645,12 @@ var SlackAdapter = class {
12922
13645
  // src/gateway/adapters/imessage.ts
12923
13646
  import { execFile } from "child_process";
12924
13647
  import { promisify } from "util";
12925
- import os9 from "os";
12926
- import path18 from "path";
13648
+ import os10 from "os";
13649
+ import path19 from "path";
12927
13650
  var execFileAsync = promisify(execFile);
12928
13651
  var POLL_INTERVAL_MS = 5e3;
12929
- var MESSAGES_DB_PATH = path18.join(
12930
- process.env.HOME ?? os9.homedir(),
13652
+ var MESSAGES_DB_PATH = path19.join(
13653
+ process.env.HOME ?? os10.homedir(),
12931
13654
  "Library/Messages/chat.db"
12932
13655
  );
12933
13656
  var IMessageAdapter = class {
@@ -13770,11 +14493,11 @@ async function ensureCRMContact(info) {
13770
14493
  }
13771
14494
 
13772
14495
  // src/automation/trigger-engine.ts
13773
- import { readFileSync as readFileSync12, writeFileSync as writeFileSync7, existsSync as existsSync14, mkdirSync as mkdirSync8 } from "fs";
14496
+ import { readFileSync as readFileSync13, writeFileSync as writeFileSync8, existsSync as existsSync15, mkdirSync as mkdirSync9 } from "fs";
13774
14497
  import { randomUUID as randomUUID15 } from "crypto";
13775
- import path19 from "path";
13776
- import os10 from "os";
13777
- var TRIGGERS_PATH = path19.join(os10.homedir(), ".exe-os", "triggers.json");
14498
+ import path20 from "path";
14499
+ import os11 from "os";
14500
+ var TRIGGERS_PATH = path20.join(os11.homedir(), ".exe-os", "triggers.json");
13778
14501
  var GRAPH_API_VERSION = "v21.0";
13779
14502
  function substituteTemplate(template, record) {
13780
14503
  return template.replace(
@@ -13828,9 +14551,9 @@ function evaluateConditions(conditions, record) {
13828
14551
  return conditions.every((c) => evaluateCondition(c, record));
13829
14552
  }
13830
14553
  function loadTriggers(project) {
13831
- if (!existsSync14(TRIGGERS_PATH)) return [];
14554
+ if (!existsSync15(TRIGGERS_PATH)) return [];
13832
14555
  try {
13833
- const raw = readFileSync12(TRIGGERS_PATH, "utf-8");
14556
+ const raw = readFileSync13(TRIGGERS_PATH, "utf-8");
13834
14557
  const all = JSON.parse(raw);
13835
14558
  if (!Array.isArray(all)) return [];
13836
14559
  if (project) {