@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
@@ -502,18 +502,69 @@ var init_provider_table = __esm({
502
502
  }
503
503
  });
504
504
 
505
- // src/lib/intercom-queue.ts
506
- import { readFileSync as readFileSync5, writeFileSync as writeFileSync4, renameSync as renameSync3, existsSync as existsSync4, mkdirSync as mkdirSync3 } from "fs";
505
+ // src/lib/runtime-table.ts
506
+ var RUNTIME_TABLE, DEFAULT_RUNTIME;
507
+ var init_runtime_table = __esm({
508
+ "src/lib/runtime-table.ts"() {
509
+ "use strict";
510
+ RUNTIME_TABLE = {
511
+ codex: {
512
+ binary: "codex",
513
+ launchMode: "exec",
514
+ autoApproveFlag: "--full-auto",
515
+ inlineFlag: "--no-alt-screen",
516
+ apiKeyEnv: "OPENAI_API_KEY",
517
+ defaultModel: "gpt-5.4"
518
+ }
519
+ };
520
+ DEFAULT_RUNTIME = "claude";
521
+ }
522
+ });
523
+
524
+ // src/lib/agent-config.ts
525
+ import { readFileSync as readFileSync5, writeFileSync as writeFileSync4, existsSync as existsSync4, mkdirSync as mkdirSync3 } from "fs";
507
526
  import path5 from "path";
527
+ function loadAgentConfig() {
528
+ if (!existsSync4(AGENT_CONFIG_PATH)) return {};
529
+ try {
530
+ return JSON.parse(readFileSync5(AGENT_CONFIG_PATH, "utf-8"));
531
+ } catch {
532
+ return {};
533
+ }
534
+ }
535
+ function getAgentRuntime(agentId) {
536
+ const config = loadAgentConfig();
537
+ const entry = config[agentId];
538
+ if (entry) return entry;
539
+ return { runtime: DEFAULT_RUNTIME, model: DEFAULT_MODELS[DEFAULT_RUNTIME] };
540
+ }
541
+ var AGENT_CONFIG_PATH, DEFAULT_MODELS;
542
+ var init_agent_config = __esm({
543
+ "src/lib/agent-config.ts"() {
544
+ "use strict";
545
+ init_config();
546
+ init_runtime_table();
547
+ AGENT_CONFIG_PATH = path5.join(EXE_AI_DIR, "agent-config.json");
548
+ DEFAULT_MODELS = {
549
+ claude: "claude-opus-4",
550
+ codex: RUNTIME_TABLE.codex?.defaultModel ?? "gpt-5.4",
551
+ opencode: "minimax-m2.7"
552
+ };
553
+ }
554
+ });
555
+
556
+ // src/lib/intercom-queue.ts
557
+ import { readFileSync as readFileSync6, writeFileSync as writeFileSync5, renameSync as renameSync3, existsSync as existsSync5, mkdirSync as mkdirSync4 } from "fs";
558
+ import path6 from "path";
508
559
  import os4 from "os";
509
560
  function ensureDir() {
510
- const dir = path5.dirname(QUEUE_PATH);
511
- if (!existsSync4(dir)) mkdirSync3(dir, { recursive: true });
561
+ const dir = path6.dirname(QUEUE_PATH);
562
+ if (!existsSync5(dir)) mkdirSync4(dir, { recursive: true });
512
563
  }
513
564
  function readQueue() {
514
565
  try {
515
- if (!existsSync4(QUEUE_PATH)) return [];
516
- return JSON.parse(readFileSync5(QUEUE_PATH, "utf8"));
566
+ if (!existsSync5(QUEUE_PATH)) return [];
567
+ return JSON.parse(readFileSync6(QUEUE_PATH, "utf8"));
517
568
  } catch {
518
569
  return [];
519
570
  }
@@ -521,7 +572,7 @@ function readQueue() {
521
572
  function writeQueue(queue) {
522
573
  ensureDir();
523
574
  const tmp = `${QUEUE_PATH}.tmp`;
524
- writeFileSync4(tmp, JSON.stringify(queue, null, 2));
575
+ writeFileSync5(tmp, JSON.stringify(queue, null, 2));
525
576
  renameSync3(tmp, QUEUE_PATH);
526
577
  }
527
578
  function queueIntercom(targetSession, reason) {
@@ -545,9 +596,9 @@ var QUEUE_PATH, TTL_MS, INTERCOM_LOG;
545
596
  var init_intercom_queue = __esm({
546
597
  "src/lib/intercom-queue.ts"() {
547
598
  "use strict";
548
- QUEUE_PATH = path5.join(os4.homedir(), ".exe-os", "intercom-queue.json");
599
+ QUEUE_PATH = path6.join(os4.homedir(), ".exe-os", "intercom-queue.json");
549
600
  TTL_MS = 60 * 60 * 1e3;
550
- INTERCOM_LOG = path5.join(os4.homedir(), ".exe-os", "intercom.log");
601
+ INTERCOM_LOG = path6.join(os4.homedir(), ".exe-os", "intercom.log");
551
602
  }
552
603
  });
553
604
 
@@ -606,6 +657,443 @@ var init_db_retry = __esm({
606
657
  }
607
658
  });
608
659
 
660
+ // src/lib/exe-daemon-client.ts
661
+ import net from "net";
662
+ import { spawn } from "child_process";
663
+ import { randomUUID } from "crypto";
664
+ import { existsSync as existsSync6, unlinkSync as unlinkSync3, readFileSync as readFileSync7, openSync, closeSync, statSync } from "fs";
665
+ import path7 from "path";
666
+ import { fileURLToPath } from "url";
667
+ function handleData(chunk) {
668
+ _buffer += chunk.toString();
669
+ if (_buffer.length > MAX_BUFFER) {
670
+ _buffer = "";
671
+ return;
672
+ }
673
+ let newlineIdx;
674
+ while ((newlineIdx = _buffer.indexOf("\n")) !== -1) {
675
+ const line = _buffer.slice(0, newlineIdx).trim();
676
+ _buffer = _buffer.slice(newlineIdx + 1);
677
+ if (!line) continue;
678
+ try {
679
+ const response = JSON.parse(line);
680
+ const id = response.id;
681
+ if (!id) continue;
682
+ const entry = _pending.get(id);
683
+ if (entry) {
684
+ clearTimeout(entry.timer);
685
+ _pending.delete(id);
686
+ entry.resolve(response);
687
+ }
688
+ } catch {
689
+ }
690
+ }
691
+ }
692
+ function cleanupStaleFiles() {
693
+ if (existsSync6(PID_PATH)) {
694
+ try {
695
+ const pid = parseInt(readFileSync7(PID_PATH, "utf8").trim(), 10);
696
+ if (pid > 0) {
697
+ try {
698
+ process.kill(pid, 0);
699
+ return;
700
+ } catch {
701
+ }
702
+ }
703
+ } catch {
704
+ }
705
+ try {
706
+ unlinkSync3(PID_PATH);
707
+ } catch {
708
+ }
709
+ try {
710
+ unlinkSync3(SOCKET_PATH);
711
+ } catch {
712
+ }
713
+ }
714
+ }
715
+ function findPackageRoot() {
716
+ let dir = path7.dirname(fileURLToPath(import.meta.url));
717
+ const { root } = path7.parse(dir);
718
+ while (dir !== root) {
719
+ if (existsSync6(path7.join(dir, "package.json"))) return dir;
720
+ dir = path7.dirname(dir);
721
+ }
722
+ return null;
723
+ }
724
+ function spawnDaemon() {
725
+ const pkgRoot = findPackageRoot();
726
+ if (!pkgRoot) {
727
+ process.stderr.write("[exed-client] WARN: cannot find package root\n");
728
+ return;
729
+ }
730
+ const daemonPath = path7.join(pkgRoot, "dist", "lib", "exe-daemon.js");
731
+ if (!existsSync6(daemonPath)) {
732
+ process.stderr.write(`[exed-client] WARN: daemon script not found at ${daemonPath}
733
+ `);
734
+ return;
735
+ }
736
+ const resolvedPath = daemonPath;
737
+ process.stderr.write(`[exed-client] Spawning daemon: ${resolvedPath}
738
+ `);
739
+ const logPath = path7.join(path7.dirname(SOCKET_PATH), "exed.log");
740
+ let stderrFd = "ignore";
741
+ try {
742
+ stderrFd = openSync(logPath, "a");
743
+ } catch {
744
+ }
745
+ const child = spawn(process.execPath, [resolvedPath], {
746
+ detached: true,
747
+ stdio: ["ignore", "ignore", stderrFd],
748
+ env: {
749
+ ...process.env,
750
+ TMUX: void 0,
751
+ // Daemon is global — must not inherit session scope
752
+ TMUX_PANE: void 0,
753
+ // Prevents resolveExeSession() from scoping to one session
754
+ EXE_DAEMON_SOCK: SOCKET_PATH,
755
+ EXE_DAEMON_PID: PID_PATH
756
+ }
757
+ });
758
+ child.unref();
759
+ if (typeof stderrFd === "number") {
760
+ try {
761
+ closeSync(stderrFd);
762
+ } catch {
763
+ }
764
+ }
765
+ }
766
+ function acquireSpawnLock() {
767
+ try {
768
+ const fd = openSync(SPAWN_LOCK_PATH, "wx");
769
+ closeSync(fd);
770
+ return true;
771
+ } catch {
772
+ try {
773
+ const stat = statSync(SPAWN_LOCK_PATH);
774
+ if (Date.now() - stat.mtimeMs > SPAWN_LOCK_STALE_MS) {
775
+ try {
776
+ unlinkSync3(SPAWN_LOCK_PATH);
777
+ } catch {
778
+ }
779
+ try {
780
+ const fd = openSync(SPAWN_LOCK_PATH, "wx");
781
+ closeSync(fd);
782
+ return true;
783
+ } catch {
784
+ }
785
+ }
786
+ } catch {
787
+ }
788
+ return false;
789
+ }
790
+ }
791
+ function releaseSpawnLock() {
792
+ try {
793
+ unlinkSync3(SPAWN_LOCK_PATH);
794
+ } catch {
795
+ }
796
+ }
797
+ function connectToSocket() {
798
+ return new Promise((resolve) => {
799
+ if (_socket && _connected) {
800
+ resolve(true);
801
+ return;
802
+ }
803
+ const socket = net.createConnection({ path: SOCKET_PATH });
804
+ const connectTimeout = setTimeout(() => {
805
+ socket.destroy();
806
+ resolve(false);
807
+ }, 2e3);
808
+ socket.on("connect", () => {
809
+ clearTimeout(connectTimeout);
810
+ _socket = socket;
811
+ _connected = true;
812
+ _buffer = "";
813
+ socket.on("data", handleData);
814
+ socket.on("close", () => {
815
+ _connected = false;
816
+ _socket = null;
817
+ for (const [id, entry] of _pending) {
818
+ clearTimeout(entry.timer);
819
+ _pending.delete(id);
820
+ entry.resolve({ error: "Connection closed" });
821
+ }
822
+ });
823
+ socket.on("error", () => {
824
+ _connected = false;
825
+ _socket = null;
826
+ });
827
+ resolve(true);
828
+ });
829
+ socket.on("error", () => {
830
+ clearTimeout(connectTimeout);
831
+ resolve(false);
832
+ });
833
+ });
834
+ }
835
+ async function connectEmbedDaemon() {
836
+ if (_socket && _connected) return true;
837
+ if (await connectToSocket()) return true;
838
+ if (acquireSpawnLock()) {
839
+ try {
840
+ cleanupStaleFiles();
841
+ spawnDaemon();
842
+ } finally {
843
+ releaseSpawnLock();
844
+ }
845
+ }
846
+ const start = Date.now();
847
+ let delay2 = 100;
848
+ while (Date.now() - start < CONNECT_TIMEOUT_MS) {
849
+ await new Promise((r) => setTimeout(r, delay2));
850
+ if (await connectToSocket()) return true;
851
+ delay2 = Math.min(delay2 * 2, 3e3);
852
+ }
853
+ return false;
854
+ }
855
+ function sendDaemonRequest(payload, timeoutMs = REQUEST_TIMEOUT_MS) {
856
+ return new Promise((resolve) => {
857
+ if (!_socket || !_connected) {
858
+ resolve({ error: "Not connected" });
859
+ return;
860
+ }
861
+ const id = randomUUID();
862
+ const timer = setTimeout(() => {
863
+ _pending.delete(id);
864
+ resolve({ error: "Request timeout" });
865
+ }, timeoutMs);
866
+ _pending.set(id, { resolve, timer });
867
+ try {
868
+ _socket.write(JSON.stringify({ id, ...payload }) + "\n");
869
+ } catch {
870
+ clearTimeout(timer);
871
+ _pending.delete(id);
872
+ resolve({ error: "Write failed" });
873
+ }
874
+ });
875
+ }
876
+ function isClientConnected() {
877
+ return _connected;
878
+ }
879
+ var SOCKET_PATH, PID_PATH, SPAWN_LOCK_PATH, SPAWN_LOCK_STALE_MS, CONNECT_TIMEOUT_MS, REQUEST_TIMEOUT_MS, _socket, _connected, _buffer, _pending, MAX_BUFFER;
880
+ var init_exe_daemon_client = __esm({
881
+ "src/lib/exe-daemon-client.ts"() {
882
+ "use strict";
883
+ init_config();
884
+ SOCKET_PATH = process.env.EXE_DAEMON_SOCK ?? process.env.EXE_EMBED_SOCK ?? path7.join(EXE_AI_DIR, "exed.sock");
885
+ PID_PATH = process.env.EXE_DAEMON_PID ?? process.env.EXE_EMBED_PID ?? path7.join(EXE_AI_DIR, "exed.pid");
886
+ SPAWN_LOCK_PATH = path7.join(EXE_AI_DIR, "exed-spawn.lock");
887
+ SPAWN_LOCK_STALE_MS = 3e4;
888
+ CONNECT_TIMEOUT_MS = 15e3;
889
+ REQUEST_TIMEOUT_MS = 3e4;
890
+ _socket = null;
891
+ _connected = false;
892
+ _buffer = "";
893
+ _pending = /* @__PURE__ */ new Map();
894
+ MAX_BUFFER = 1e7;
895
+ }
896
+ });
897
+
898
+ // src/lib/daemon-protocol.ts
899
+ function serializeValue(v) {
900
+ if (v === null || v === void 0) return null;
901
+ if (typeof v === "bigint") return Number(v);
902
+ if (typeof v === "boolean") return v ? 1 : 0;
903
+ if (v instanceof Uint8Array) {
904
+ return { __blob: Buffer.from(v).toString("base64") };
905
+ }
906
+ if (ArrayBuffer.isView(v)) {
907
+ return { __blob: Buffer.from(v.buffer, v.byteOffset, v.byteLength).toString("base64") };
908
+ }
909
+ if (v instanceof ArrayBuffer) {
910
+ return { __blob: Buffer.from(v).toString("base64") };
911
+ }
912
+ if (typeof v === "string" || typeof v === "number") return v;
913
+ return String(v);
914
+ }
915
+ function deserializeValue(v) {
916
+ if (v === null) return null;
917
+ if (typeof v === "object" && v !== null && "__blob" in v) {
918
+ const buf = Buffer.from(v.__blob, "base64");
919
+ return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength);
920
+ }
921
+ return v;
922
+ }
923
+ function deserializeResultSet(srs) {
924
+ const rows = srs.rows.map((obj) => {
925
+ const values = srs.columns.map(
926
+ (col) => deserializeValue(obj[col] ?? null)
927
+ );
928
+ const row = values;
929
+ for (let i = 0; i < srs.columns.length; i++) {
930
+ const col = srs.columns[i];
931
+ if (col !== void 0) {
932
+ row[col] = values[i] ?? null;
933
+ }
934
+ }
935
+ Object.defineProperty(row, "length", {
936
+ value: values.length,
937
+ enumerable: false
938
+ });
939
+ return row;
940
+ });
941
+ return {
942
+ columns: srs.columns,
943
+ columnTypes: srs.columnTypes ?? [],
944
+ rows,
945
+ rowsAffected: srs.rowsAffected,
946
+ lastInsertRowid: srs.lastInsertRowid != null ? BigInt(srs.lastInsertRowid) : void 0,
947
+ toJSON: () => ({
948
+ columns: srs.columns,
949
+ columnTypes: srs.columnTypes ?? [],
950
+ rows: srs.rows,
951
+ rowsAffected: srs.rowsAffected,
952
+ lastInsertRowid: srs.lastInsertRowid
953
+ })
954
+ };
955
+ }
956
+ var init_daemon_protocol = __esm({
957
+ "src/lib/daemon-protocol.ts"() {
958
+ "use strict";
959
+ }
960
+ });
961
+
962
+ // src/lib/db-daemon-client.ts
963
+ var db_daemon_client_exports = {};
964
+ __export(db_daemon_client_exports, {
965
+ createDaemonDbClient: () => createDaemonDbClient,
966
+ initDaemonDbClient: () => initDaemonDbClient
967
+ });
968
+ function normalizeStatement(stmt) {
969
+ if (typeof stmt === "string") {
970
+ return { sql: stmt, args: [] };
971
+ }
972
+ const sql = stmt.sql;
973
+ let args = [];
974
+ if (Array.isArray(stmt.args)) {
975
+ args = stmt.args.map((v) => serializeValue(v));
976
+ } else if (stmt.args && typeof stmt.args === "object") {
977
+ const named = {};
978
+ for (const [key, val] of Object.entries(stmt.args)) {
979
+ named[key] = serializeValue(val);
980
+ }
981
+ return { sql, args: named };
982
+ }
983
+ return { sql, args };
984
+ }
985
+ function createDaemonDbClient(fallbackClient) {
986
+ let _useDaemon = false;
987
+ const client = {
988
+ async execute(stmt) {
989
+ if (!_useDaemon || !isClientConnected()) {
990
+ return fallbackClient.execute(stmt);
991
+ }
992
+ const { sql, args } = normalizeStatement(stmt);
993
+ const response = await sendDaemonRequest({
994
+ type: "db-execute",
995
+ sql,
996
+ args
997
+ });
998
+ if (response.error) {
999
+ const errMsg = String(response.error);
1000
+ if (errMsg === "Not connected" || errMsg === "Request timeout" || errMsg === "Write failed") {
1001
+ process.stderr.write(`[db-daemon] Transport error (${errMsg}), falling back to direct
1002
+ `);
1003
+ return fallbackClient.execute(stmt);
1004
+ }
1005
+ throw new Error(errMsg);
1006
+ }
1007
+ if (response.db) {
1008
+ return deserializeResultSet(response.db);
1009
+ }
1010
+ process.stderr.write("[db-daemon] Unexpected response shape, falling back to direct\n");
1011
+ return fallbackClient.execute(stmt);
1012
+ },
1013
+ async batch(stmts, mode) {
1014
+ if (!_useDaemon || !isClientConnected()) {
1015
+ return fallbackClient.batch(stmts, mode);
1016
+ }
1017
+ const statements = stmts.map(normalizeStatement);
1018
+ const response = await sendDaemonRequest({
1019
+ type: "db-batch",
1020
+ statements,
1021
+ mode: mode ?? "deferred"
1022
+ });
1023
+ if (response.error) {
1024
+ const errMsg = String(response.error);
1025
+ if (errMsg === "Not connected" || errMsg === "Request timeout" || errMsg === "Write failed") {
1026
+ process.stderr.write(`[db-daemon] Batch transport error (${errMsg}), falling back to direct
1027
+ `);
1028
+ return fallbackClient.batch(stmts, mode);
1029
+ }
1030
+ throw new Error(errMsg);
1031
+ }
1032
+ const batchResults = response["db-batch"];
1033
+ if (batchResults) {
1034
+ return batchResults.map(deserializeResultSet);
1035
+ }
1036
+ process.stderr.write("[db-daemon] Unexpected batch response shape, falling back to direct\n");
1037
+ return fallbackClient.batch(stmts, mode);
1038
+ },
1039
+ // Transaction support — delegate to fallback (transactions need direct connection)
1040
+ async transaction(mode) {
1041
+ return fallbackClient.transaction(mode);
1042
+ },
1043
+ // executeMultiple — delegate to fallback (used only for schema migrations)
1044
+ async executeMultiple(sql) {
1045
+ return fallbackClient.executeMultiple(sql);
1046
+ },
1047
+ // migrate — delegate to fallback
1048
+ async migrate(stmts) {
1049
+ return fallbackClient.migrate(stmts);
1050
+ },
1051
+ // Sync mode — delegate to fallback
1052
+ sync() {
1053
+ return fallbackClient.sync();
1054
+ },
1055
+ close() {
1056
+ _useDaemon = false;
1057
+ },
1058
+ get closed() {
1059
+ return fallbackClient.closed;
1060
+ },
1061
+ get protocol() {
1062
+ return fallbackClient.protocol;
1063
+ }
1064
+ };
1065
+ return {
1066
+ ...client,
1067
+ /** Enable daemon routing (call after confirming daemon is connected) */
1068
+ _enableDaemon() {
1069
+ _useDaemon = true;
1070
+ },
1071
+ /** Check if daemon routing is active */
1072
+ _isDaemonActive() {
1073
+ return _useDaemon && isClientConnected();
1074
+ }
1075
+ };
1076
+ }
1077
+ async function initDaemonDbClient(fallbackClient) {
1078
+ if (process.env.EXE_IS_DAEMON === "1") return null;
1079
+ const connected = await connectEmbedDaemon();
1080
+ if (!connected) {
1081
+ process.stderr.write("[db-daemon] Daemon unavailable \u2014 using direct SQLite\n");
1082
+ return null;
1083
+ }
1084
+ const client = createDaemonDbClient(fallbackClient);
1085
+ client._enableDaemon();
1086
+ process.stderr.write("[db-daemon] DB routing through daemon (single-writer)\n");
1087
+ return client;
1088
+ }
1089
+ var init_db_daemon_client = __esm({
1090
+ "src/lib/db-daemon-client.ts"() {
1091
+ "use strict";
1092
+ init_exe_daemon_client();
1093
+ init_daemon_protocol();
1094
+ }
1095
+ });
1096
+
609
1097
  // src/lib/database.ts
610
1098
  var database_exports = {};
611
1099
  __export(database_exports, {
@@ -614,6 +1102,7 @@ __export(database_exports, {
614
1102
  ensureSchema: () => ensureSchema,
615
1103
  getClient: () => getClient,
616
1104
  getRawClient: () => getRawClient,
1105
+ initDaemonClient: () => initDaemonClient,
617
1106
  initDatabase: () => initDatabase,
618
1107
  initTurso: () => initTurso,
619
1108
  isInitialized: () => isInitialized
@@ -641,8 +1130,27 @@ function getClient() {
641
1130
  if (!_resilientClient) {
642
1131
  throw new Error("Database client not initialized. Call initDatabase() first.");
643
1132
  }
1133
+ if (process.env.EXE_IS_DAEMON === "1") {
1134
+ return _resilientClient;
1135
+ }
1136
+ if (_daemonClient && _daemonClient._isDaemonActive()) {
1137
+ return _daemonClient;
1138
+ }
644
1139
  return _resilientClient;
645
1140
  }
1141
+ async function initDaemonClient() {
1142
+ if (process.env.EXE_IS_DAEMON === "1") return;
1143
+ if (!_resilientClient) return;
1144
+ try {
1145
+ const { initDaemonDbClient: initDaemonDbClient2 } = await Promise.resolve().then(() => (init_db_daemon_client(), db_daemon_client_exports));
1146
+ _daemonClient = await initDaemonDbClient2(_resilientClient);
1147
+ } catch (err) {
1148
+ process.stderr.write(
1149
+ `[database] Daemon client init failed (non-fatal): ${err instanceof Error ? err.message : String(err)}
1150
+ `
1151
+ );
1152
+ }
1153
+ }
646
1154
  function getRawClient() {
647
1155
  if (!_client) {
648
1156
  throw new Error("Database client not initialized. Call initDatabase() first.");
@@ -1129,6 +1637,12 @@ async function ensureSchema() {
1129
1637
  } catch {
1130
1638
  }
1131
1639
  }
1640
+ try {
1641
+ await client.execute(
1642
+ `CREATE INDEX IF NOT EXISTS idx_memories_content_hash ON memories(content_hash, agent_id)`
1643
+ );
1644
+ } catch {
1645
+ }
1132
1646
  await client.executeMultiple(`
1133
1647
  CREATE TABLE IF NOT EXISTS entities (
1134
1648
  id TEXT PRIMARY KEY,
@@ -1181,7 +1695,30 @@ async function ensureSchema() {
1181
1695
  entity_id TEXT NOT NULL,
1182
1696
  PRIMARY KEY (hyperedge_id, entity_id)
1183
1697
  );
1698
+
1699
+ CREATE VIRTUAL TABLE IF NOT EXISTS entities_fts USING fts5(
1700
+ name,
1701
+ content=entities,
1702
+ content_rowid=rowid
1703
+ );
1704
+
1705
+ CREATE TRIGGER IF NOT EXISTS entities_fts_ai AFTER INSERT ON entities BEGIN
1706
+ INSERT INTO entities_fts(rowid, name) VALUES (new.rowid, new.name);
1707
+ END;
1708
+
1709
+ CREATE TRIGGER IF NOT EXISTS entities_fts_ad AFTER DELETE ON entities BEGIN
1710
+ INSERT INTO entities_fts(entities_fts, rowid, name) VALUES('delete', old.rowid, old.name);
1711
+ END;
1712
+
1713
+ CREATE TRIGGER IF NOT EXISTS entities_fts_au AFTER UPDATE ON entities BEGIN
1714
+ INSERT INTO entities_fts(entities_fts, rowid, name) VALUES('delete', old.rowid, old.name);
1715
+ INSERT INTO entities_fts(rowid, name) VALUES (new.rowid, new.name);
1716
+ END;
1184
1717
  `);
1718
+ try {
1719
+ await client.execute("INSERT INTO entities_fts(entities_fts) VALUES('rebuild')");
1720
+ } catch {
1721
+ }
1185
1722
  await client.executeMultiple(`
1186
1723
  CREATE TABLE IF NOT EXISTS entity_aliases (
1187
1724
  alias TEXT NOT NULL PRIMARY KEY,
@@ -1362,6 +1899,33 @@ async function ensureSchema() {
1362
1899
  CREATE INDEX IF NOT EXISTS idx_conversations_channel
1363
1900
  ON conversations(channel_id);
1364
1901
  `);
1902
+ await client.executeMultiple(`
1903
+ CREATE TABLE IF NOT EXISTS session_agent_map (
1904
+ session_uuid TEXT PRIMARY KEY,
1905
+ agent_id TEXT NOT NULL,
1906
+ session_name TEXT,
1907
+ task_id TEXT,
1908
+ project_name TEXT,
1909
+ started_at TEXT NOT NULL
1910
+ );
1911
+
1912
+ CREATE INDEX IF NOT EXISTS idx_session_agent_map_agent
1913
+ ON session_agent_map(agent_id);
1914
+ `);
1915
+ try {
1916
+ const mapCount = await client.execute({ sql: `SELECT COUNT(*) as cnt FROM session_agent_map`, args: [] });
1917
+ if (Number(mapCount.rows[0]?.cnt ?? 0) === 0) {
1918
+ await client.execute({
1919
+ sql: `INSERT OR IGNORE INTO session_agent_map (session_uuid, agent_id, session_name, started_at)
1920
+ SELECT session_id, agent_id, '', MIN(timestamp)
1921
+ FROM memories
1922
+ WHERE session_id IS NOT NULL AND session_id != '' AND agent_id IS NOT NULL AND agent_id != ''
1923
+ GROUP BY session_id, agent_id`,
1924
+ args: []
1925
+ });
1926
+ }
1927
+ } catch {
1928
+ }
1365
1929
  try {
1366
1930
  await client.execute({
1367
1931
  sql: `ALTER TABLE tasks ADD COLUMN budget_tokens INTEGER`,
@@ -1495,15 +2059,41 @@ async function ensureSchema() {
1495
2059
  });
1496
2060
  } catch {
1497
2061
  }
2062
+ for (const col of [
2063
+ "ALTER TABLE memories ADD COLUMN intent TEXT",
2064
+ "ALTER TABLE memories ADD COLUMN outcome TEXT",
2065
+ "ALTER TABLE memories ADD COLUMN domain TEXT",
2066
+ "ALTER TABLE memories ADD COLUMN referenced_entities TEXT",
2067
+ "ALTER TABLE memories ADD COLUMN retrieval_count INTEGER DEFAULT 0",
2068
+ "ALTER TABLE memories ADD COLUMN chain_position TEXT",
2069
+ "ALTER TABLE memories ADD COLUMN review_status TEXT",
2070
+ "ALTER TABLE memories ADD COLUMN context_window_pct INTEGER",
2071
+ "ALTER TABLE memories ADD COLUMN file_paths TEXT",
2072
+ "ALTER TABLE memories ADD COLUMN commit_hash TEXT",
2073
+ "ALTER TABLE memories ADD COLUMN duration_ms INTEGER",
2074
+ "ALTER TABLE memories ADD COLUMN token_cost REAL",
2075
+ "ALTER TABLE memories ADD COLUMN audience TEXT",
2076
+ "ALTER TABLE memories ADD COLUMN language_type TEXT",
2077
+ "ALTER TABLE memories ADD COLUMN parent_memory_id TEXT"
2078
+ ]) {
2079
+ try {
2080
+ await client.execute(col);
2081
+ } catch {
2082
+ }
2083
+ }
1498
2084
  }
1499
2085
  async function disposeDatabase() {
2086
+ if (_daemonClient) {
2087
+ _daemonClient.close();
2088
+ _daemonClient = null;
2089
+ }
1500
2090
  if (_client) {
1501
2091
  _client.close();
1502
2092
  _client = null;
1503
2093
  _resilientClient = null;
1504
2094
  }
1505
2095
  }
1506
- var _client, _resilientClient, initTurso, disposeTurso;
2096
+ var _client, _resilientClient, _daemonClient, initTurso, disposeTurso;
1507
2097
  var init_database = __esm({
1508
2098
  "src/lib/database.ts"() {
1509
2099
  "use strict";
@@ -1511,24 +2101,25 @@ var init_database = __esm({
1511
2101
  init_employees();
1512
2102
  _client = null;
1513
2103
  _resilientClient = null;
2104
+ _daemonClient = null;
1514
2105
  initTurso = initDatabase;
1515
2106
  disposeTurso = disposeDatabase;
1516
2107
  }
1517
2108
  });
1518
2109
 
1519
2110
  // src/lib/license.ts
1520
- import { readFileSync as readFileSync6, writeFileSync as writeFileSync5, existsSync as existsSync5, mkdirSync as mkdirSync4 } from "fs";
1521
- import { randomUUID } from "crypto";
1522
- import path6 from "path";
2111
+ import { readFileSync as readFileSync8, writeFileSync as writeFileSync6, existsSync as existsSync7, mkdirSync as mkdirSync5 } from "fs";
2112
+ import { randomUUID as randomUUID2 } from "crypto";
2113
+ import path8 from "path";
1523
2114
  import { jwtVerify, importSPKI } from "jose";
1524
2115
  var LICENSE_PATH, CACHE_PATH, DEVICE_ID_PATH, PLAN_LIMITS;
1525
2116
  var init_license = __esm({
1526
2117
  "src/lib/license.ts"() {
1527
2118
  "use strict";
1528
2119
  init_config();
1529
- LICENSE_PATH = path6.join(EXE_AI_DIR, "license.key");
1530
- CACHE_PATH = path6.join(EXE_AI_DIR, "license-cache.json");
1531
- DEVICE_ID_PATH = path6.join(EXE_AI_DIR, "device-id");
2120
+ LICENSE_PATH = path8.join(EXE_AI_DIR, "license.key");
2121
+ CACHE_PATH = path8.join(EXE_AI_DIR, "license-cache.json");
2122
+ DEVICE_ID_PATH = path8.join(EXE_AI_DIR, "device-id");
1532
2123
  PLAN_LIMITS = {
1533
2124
  free: { devices: 1, employees: 1, memories: 5e3 },
1534
2125
  pro: { devices: 3, employees: 5, memories: 1e5 },
@@ -1540,12 +2131,12 @@ var init_license = __esm({
1540
2131
  });
1541
2132
 
1542
2133
  // src/lib/plan-limits.ts
1543
- import { readFileSync as readFileSync7, existsSync as existsSync6 } from "fs";
1544
- import path7 from "path";
2134
+ import { readFileSync as readFileSync9, existsSync as existsSync8 } from "fs";
2135
+ import path9 from "path";
1545
2136
  function getLicenseSync() {
1546
2137
  try {
1547
- if (!existsSync6(CACHE_PATH2)) return freeLicense();
1548
- const raw = JSON.parse(readFileSync7(CACHE_PATH2, "utf8"));
2138
+ if (!existsSync8(CACHE_PATH2)) return freeLicense();
2139
+ const raw = JSON.parse(readFileSync9(CACHE_PATH2, "utf8"));
1549
2140
  if (!raw.token || typeof raw.token !== "string") return freeLicense();
1550
2141
  const parts = raw.token.split(".");
1551
2142
  if (parts.length !== 3) return freeLicense();
@@ -1583,8 +2174,8 @@ function assertEmployeeLimitSync(rosterPath) {
1583
2174
  const filePath = rosterPath ?? EMPLOYEES_PATH;
1584
2175
  let count = 0;
1585
2176
  try {
1586
- if (existsSync6(filePath)) {
1587
- const raw = readFileSync7(filePath, "utf8");
2177
+ if (existsSync8(filePath)) {
2178
+ const raw = readFileSync9(filePath, "utf8");
1588
2179
  const employees = JSON.parse(raw);
1589
2180
  count = Array.isArray(employees) ? employees.length : 0;
1590
2181
  }
@@ -1613,19 +2204,19 @@ var init_plan_limits = __esm({
1613
2204
  this.name = "PlanLimitError";
1614
2205
  }
1615
2206
  };
1616
- CACHE_PATH2 = path7.join(EXE_AI_DIR, "license-cache.json");
2207
+ CACHE_PATH2 = path9.join(EXE_AI_DIR, "license-cache.json");
1617
2208
  }
1618
2209
  });
1619
2210
 
1620
2211
  // src/lib/notifications.ts
1621
2212
  import crypto from "crypto";
1622
- import path8 from "path";
2213
+ import path10 from "path";
1623
2214
  import os5 from "os";
1624
2215
  import {
1625
- readFileSync as readFileSync8,
2216
+ readFileSync as readFileSync10,
1626
2217
  readdirSync as readdirSync2,
1627
- unlinkSync as unlinkSync3,
1628
- existsSync as existsSync7,
2218
+ unlinkSync as unlinkSync4,
2219
+ existsSync as existsSync9,
1629
2220
  rmdirSync
1630
2221
  } from "fs";
1631
2222
  async function writeNotification(notification) {
@@ -1760,10 +2351,11 @@ var init_state_bus = __esm({
1760
2351
 
1761
2352
  // src/lib/tasks-crud.ts
1762
2353
  import crypto3 from "crypto";
1763
- import path9 from "path";
2354
+ import path11 from "path";
2355
+ import os6 from "os";
1764
2356
  import { execSync as execSync5 } from "child_process";
1765
2357
  import { mkdir as mkdir3, writeFile as writeFile3, appendFile } from "fs/promises";
1766
- import { existsSync as existsSync8, readFileSync as readFileSync9 } from "fs";
2358
+ import { existsSync as existsSync10, readFileSync as readFileSync11 } from "fs";
1767
2359
  async function writeCheckpoint(input2) {
1768
2360
  const client = getClient();
1769
2361
  const row = await resolveTask(client, input2.taskId);
@@ -1804,6 +2396,35 @@ function extractParentFromContext(contextBody) {
1804
2396
  function slugify(title) {
1805
2397
  return title.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
1806
2398
  }
2399
+ function buildKeywordIndex() {
2400
+ const idx = /* @__PURE__ */ new Map();
2401
+ for (const [role, keywords] of Object.entries(LANE_KEYWORDS)) {
2402
+ for (const kw of keywords) {
2403
+ const existing = idx.get(kw) ?? [];
2404
+ existing.push(role);
2405
+ idx.set(kw, existing);
2406
+ }
2407
+ }
2408
+ return idx;
2409
+ }
2410
+ function checkLaneAffinity(title, context, assigneeName) {
2411
+ const employees = loadEmployeesSync();
2412
+ const employee = employees.find((e) => e.name === assigneeName);
2413
+ if (!employee) return void 0;
2414
+ const assigneeRole = employee.role;
2415
+ const text = `${title} ${context}`.toLowerCase();
2416
+ const matchedRoles = /* @__PURE__ */ new Set();
2417
+ for (const [keyword, roles] of KEYWORD_INDEX) {
2418
+ if (text.includes(keyword)) {
2419
+ for (const role of roles) matchedRoles.add(role);
2420
+ }
2421
+ }
2422
+ if (matchedRoles.size === 0) return void 0;
2423
+ if (matchedRoles.has(assigneeRole)) return void 0;
2424
+ if (assigneeRole === "COO") return void 0;
2425
+ const expectedRoles = Array.from(matchedRoles).join(" or ");
2426
+ return `\u26A0\uFE0F Lane mismatch: task content suggests ${expectedRoles}, but assigned to ${assigneeName} (${assigneeRole}).`;
2427
+ }
1807
2428
  async function resolveTask(client, identifier, scopeSession) {
1808
2429
  const scope = sessionScopeFilter(scopeSession);
1809
2430
  let result = await client.execute({
@@ -1853,7 +2474,14 @@ async function createTaskCore(input2) {
1853
2474
  const id = crypto3.randomUUID();
1854
2475
  const now = (/* @__PURE__ */ new Date()).toISOString();
1855
2476
  const slug = slugify(input2.title);
1856
- const taskFile = input2.taskFile ?? `exe/${input2.assignedTo}/${slug}.md`;
2477
+ let earlySessionScope = null;
2478
+ try {
2479
+ const { resolveExeSession: resolveExeSession2 } = await Promise.resolve().then(() => (init_tmux_routing(), tmux_routing_exports));
2480
+ earlySessionScope = resolveExeSession2();
2481
+ } catch {
2482
+ }
2483
+ const scope = earlySessionScope ?? "default";
2484
+ const taskFile = input2.taskFile ?? `tasks/${scope}/${input2.assignedTo}/${slug}.md`;
1857
2485
  let blockedById = null;
1858
2486
  const initialStatus = input2.blockedBy ? "blocked" : "open";
1859
2487
  if (input2.blockedBy) {
@@ -1893,22 +2521,24 @@ async function createTaskCore(input2) {
1893
2521
  if (dupCheck.rows.length > 0) {
1894
2522
  warning = `similar active task already exists (${String(dupCheck.rows[0].id)}). Created new task anyway.`;
1895
2523
  }
2524
+ if (!process.env.DISABLE_LANE_AFFINITY) {
2525
+ const laneWarning = checkLaneAffinity(input2.title, input2.context, input2.assignedTo);
2526
+ if (laneWarning) {
2527
+ warning = warning ? `${warning}
2528
+ ${laneWarning}` : laneWarning;
2529
+ }
2530
+ }
1896
2531
  if (input2.baseDir) {
1897
2532
  try {
1898
- await mkdir3(path9.join(input2.baseDir, "exe", "output"), { recursive: true });
1899
- await mkdir3(path9.join(input2.baseDir, "exe", "research"), { recursive: true });
2533
+ await mkdir3(path11.join(input2.baseDir, "exe", "output"), { recursive: true });
2534
+ await mkdir3(path11.join(input2.baseDir, "exe", "research"), { recursive: true });
1900
2535
  await ensureArchitectureDoc(input2.baseDir, input2.projectName);
1901
2536
  await ensureGitignoreExe(input2.baseDir);
1902
2537
  } catch {
1903
2538
  }
1904
2539
  }
1905
2540
  const complexity = input2.complexity ?? "standard";
1906
- let sessionScope = null;
1907
- try {
1908
- const { resolveExeSession: resolveExeSession2 } = await Promise.resolve().then(() => (init_tmux_routing(), tmux_routing_exports));
1909
- sessionScope = resolveExeSession2();
1910
- } catch {
1911
- }
2541
+ const sessionScope = earlySessionScope;
1912
2542
  await client.execute({
1913
2543
  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)
1914
2544
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
@@ -1935,6 +2565,43 @@ async function createTaskCore(input2) {
1935
2565
  now
1936
2566
  ]
1937
2567
  });
2568
+ if (input2.baseDir) {
2569
+ try {
2570
+ const EXE_OS_DIR = path11.join(os6.homedir(), ".exe-os");
2571
+ const mdPath = path11.join(EXE_OS_DIR, taskFile);
2572
+ const mdDir = path11.dirname(mdPath);
2573
+ if (!existsSync10(mdDir)) await mkdir3(mdDir, { recursive: true });
2574
+ const reviewer = input2.reviewer ?? input2.assignedBy;
2575
+ const mdContent = `# ${input2.title}
2576
+
2577
+ **ID:** ${id}
2578
+ **Status:** ${initialStatus}
2579
+ **Priority:** ${input2.priority}
2580
+ **Assigned by:** ${input2.assignedBy}
2581
+ **Assigned to:** ${input2.assignedTo}
2582
+ **Project:** ${input2.projectName}
2583
+ **Created:** ${now.split("T")[0]}${parentTaskId ? `
2584
+ **Parent task:** ${parentTaskId}` : ""}
2585
+ **Reviewer:** ${reviewer}
2586
+
2587
+ ## Context
2588
+
2589
+ ${input2.context}
2590
+
2591
+ ## MANDATORY: When done
2592
+
2593
+ You MUST call update_task with status "done" and a result summary when finished.
2594
+ If you skip this, your reviewer will not know you're done and your work won't be reviewed.
2595
+ Do NOT let a failed commit or any error prevent you from calling update_task(done).
2596
+ `;
2597
+ await writeFile3(mdPath, mdContent, "utf-8");
2598
+ } catch (err) {
2599
+ process.stderr.write(
2600
+ `[create-task] WARNING: .md file write failed for ${taskFile}: ${err instanceof Error ? err.message : String(err)}
2601
+ `
2602
+ );
2603
+ }
2604
+ }
1938
2605
  return {
1939
2606
  id,
1940
2607
  title: input2.title,
@@ -2127,7 +2794,7 @@ ${input2.result}` : `\u26A0\uFE0F ${warning}`;
2127
2794
  return { row, taskFile, now, taskId };
2128
2795
  }
2129
2796
  }
2130
- if (curStatus === "in_progress" && input2.callerAgentId && (input2.callerAgentId === assignedBy || input2.callerAgentId === "exe")) {
2797
+ if (curStatus === "in_progress" && input2.callerAgentId && (input2.callerAgentId === assignedBy || isCoordinatorName(input2.callerAgentId))) {
2131
2798
  process.stderr.write(
2132
2799
  `[tasks] Assigner override: ${input2.callerAgentId} reclaiming ${taskId}
2133
2800
  `
@@ -2192,9 +2859,9 @@ async function deleteTaskCore(taskId, _baseDir) {
2192
2859
  return { taskFile, assignedTo, assignedBy, taskSlug };
2193
2860
  }
2194
2861
  async function ensureArchitectureDoc(baseDir, projectName) {
2195
- const archPath = path9.join(baseDir, "exe", "ARCHITECTURE.md");
2862
+ const archPath = path11.join(baseDir, "exe", "ARCHITECTURE.md");
2196
2863
  try {
2197
- if (existsSync8(archPath)) return;
2864
+ if (existsSync10(archPath)) return;
2198
2865
  const template = [
2199
2866
  `# ${projectName} \u2014 System Architecture`,
2200
2867
  "",
@@ -2227,10 +2894,10 @@ async function ensureArchitectureDoc(baseDir, projectName) {
2227
2894
  }
2228
2895
  }
2229
2896
  async function ensureGitignoreExe(baseDir) {
2230
- const gitignorePath = path9.join(baseDir, ".gitignore");
2897
+ const gitignorePath = path11.join(baseDir, ".gitignore");
2231
2898
  try {
2232
- if (existsSync8(gitignorePath)) {
2233
- const content = readFileSync9(gitignorePath, "utf-8");
2899
+ if (existsSync10(gitignorePath)) {
2900
+ const content = readFileSync11(gitignorePath, "utf-8");
2234
2901
  if (/^\/?exe\/?$/m.test(content)) return;
2235
2902
  await appendFile(gitignorePath, "\n# Employee task assignments (private)\n/exe/\n");
2236
2903
  } else {
@@ -2239,20 +2906,30 @@ async function ensureGitignoreExe(baseDir) {
2239
2906
  } catch {
2240
2907
  }
2241
2908
  }
2242
- var DELEGATION_KEYWORDS, TASK_ALREADY_CLAIMED_PREFIX;
2909
+ var LANE_KEYWORDS, KEYWORD_INDEX, DELEGATION_KEYWORDS, TASK_ALREADY_CLAIMED_PREFIX;
2243
2910
  var init_tasks_crud = __esm({
2244
2911
  "src/lib/tasks-crud.ts"() {
2245
2912
  "use strict";
2246
2913
  init_database();
2247
2914
  init_task_scope();
2915
+ init_employees();
2916
+ LANE_KEYWORDS = {
2917
+ CMO: ["sales", "script", "pitch", "offer", "copy", "objection", "brand", "content", "seo", "marketing", "newsletter", "carousel", "social", "campaign"],
2918
+ CTO: ["spec", "architecture", "migration", "schema", "database", "design doc", "adr", "security audit", "tech stack"],
2919
+ "Principal Engineer": ["implement", "build", "fix", "commit", "refactor", "bug", "feature", "wire", "integration"],
2920
+ "Staff Code Reviewer": ["critique", "verdict", "review", "audit", "code quality"],
2921
+ "Content Production Specialist": ["render", "video", "image", "b-roll", "remotion", "animation", "thumbnail"],
2922
+ "AI Product Lead": ["competitive", "analysis", "benchmark", "compare", "scout", "evaluate", "poc"]
2923
+ };
2924
+ KEYWORD_INDEX = buildKeywordIndex();
2248
2925
  DELEGATION_KEYWORDS = /parallel|delegate|wave|worktree|multi-instance/i;
2249
2926
  TASK_ALREADY_CLAIMED_PREFIX = "TASK_ALREADY_CLAIMED";
2250
2927
  }
2251
2928
  });
2252
2929
 
2253
2930
  // src/lib/tasks-review.ts
2254
- import path10 from "path";
2255
- import { existsSync as existsSync9, readdirSync as readdirSync3, unlinkSync as unlinkSync4 } from "fs";
2931
+ import path12 from "path";
2932
+ import { existsSync as existsSync11, readdirSync as readdirSync3, unlinkSync as unlinkSync5 } from "fs";
2256
2933
  async function countPendingReviews(sessionScope) {
2257
2934
  const client = getClient();
2258
2935
  if (sessionScope) {
@@ -2274,7 +2951,7 @@ async function countNewPendingReviewsSince(sinceIso, sessionScope) {
2274
2951
  const result2 = await client.execute({
2275
2952
  sql: `SELECT COUNT(*) as cnt FROM tasks
2276
2953
  WHERE status = 'needs_review' AND updated_at > ?
2277
- AND (session_scope = ? OR session_scope IS NULL)`,
2954
+ AND session_scope = ?`,
2278
2955
  args: [sinceIso, sessionScope]
2279
2956
  });
2280
2957
  return Number(result2.rows[0]?.cnt) || 0;
@@ -2292,7 +2969,7 @@ async function listPendingReviews(limit, sessionScope) {
2292
2969
  const result2 = await client.execute({
2293
2970
  sql: `SELECT title, assigned_to, project_name FROM tasks
2294
2971
  WHERE status = 'needs_review'
2295
- AND (session_scope = ? OR session_scope IS NULL)
2972
+ AND session_scope = ?
2296
2973
  ORDER BY priority ASC, created_at DESC LIMIT ?`,
2297
2974
  args: [sessionScope, limit]
2298
2975
  });
@@ -2413,14 +3090,14 @@ async function cleanupReviewFile(row, taskFile, _baseDir) {
2413
3090
  if (parts.length >= 3 && parts[0] === "review") {
2414
3091
  const agent = parts[1];
2415
3092
  const slug = parts.slice(2).join("-");
2416
- const originalTaskFile = `exe/${agent}/${slug}.md`;
3093
+ const legacyTaskFile = `exe/${agent}/${slug}.md`;
2417
3094
  const result = await client.execute({
2418
- sql: "UPDATE tasks SET status = 'done', updated_at = ? WHERE task_file = ? AND status = 'needs_review'",
2419
- args: [now, originalTaskFile]
3095
+ sql: "UPDATE tasks SET status = 'done', updated_at = ? WHERE (task_file = ? OR task_file LIKE ?) AND status = 'needs_review'",
3096
+ args: [now, legacyTaskFile, `tasks/%/${agent}/${slug}.md`]
2420
3097
  });
2421
3098
  if (result.rowsAffected > 0) {
2422
3099
  process.stderr.write(
2423
- `[review-cleanup] Cascaded original task to done (legacy path): ${originalTaskFile}
3100
+ `[review-cleanup] Cascaded original task to done: ${agent}/${slug}.md
2424
3101
  `
2425
3102
  );
2426
3103
  }
@@ -2433,11 +3110,11 @@ async function cleanupReviewFile(row, taskFile, _baseDir) {
2433
3110
  );
2434
3111
  }
2435
3112
  try {
2436
- const cacheDir = path10.join(EXE_AI_DIR, "session-cache");
2437
- if (existsSync9(cacheDir)) {
3113
+ const cacheDir = path12.join(EXE_AI_DIR, "session-cache");
3114
+ if (existsSync11(cacheDir)) {
2438
3115
  for (const f of readdirSync3(cacheDir)) {
2439
3116
  if (f.startsWith("review-notified-")) {
2440
- unlinkSync4(path10.join(cacheDir, f));
3117
+ unlinkSync5(path12.join(cacheDir, f));
2441
3118
  }
2442
3119
  }
2443
3120
  }
@@ -2458,7 +3135,7 @@ var init_tasks_review = __esm({
2458
3135
  });
2459
3136
 
2460
3137
  // src/lib/tasks-chain.ts
2461
- import path11 from "path";
3138
+ import path13 from "path";
2462
3139
  import { readFile as readFile3, writeFile as writeFile4 } from "fs/promises";
2463
3140
  async function cascadeUnblock(taskId, baseDir, now) {
2464
3141
  const client = getClient();
@@ -2475,7 +3152,7 @@ async function cascadeUnblock(taskId, baseDir, now) {
2475
3152
  });
2476
3153
  for (const ur of unblockedRows.rows) {
2477
3154
  try {
2478
- const ubFile = path11.join(baseDir, String(ur.task_file));
3155
+ const ubFile = path13.join(baseDir, String(ur.task_file));
2479
3156
  let ubContent = await readFile3(ubFile, "utf-8");
2480
3157
  ubContent = ubContent.replace(/\*\*Status:\*\* blocked/, "**Status:** open");
2481
3158
  ubContent = ubContent.replace(/\n\*\*Blocked by:\*\*.*\n/, "\n");
@@ -2544,7 +3221,7 @@ var init_tasks_chain = __esm({
2544
3221
 
2545
3222
  // src/lib/project-name.ts
2546
3223
  import { execSync as execSync6 } from "child_process";
2547
- import path12 from "path";
3224
+ import path14 from "path";
2548
3225
  function getProjectName(cwd) {
2549
3226
  const dir = cwd ?? process.cwd();
2550
3227
  if (_cached2 && _cachedCwd === dir) return _cached2;
@@ -2557,7 +3234,7 @@ function getProjectName(cwd) {
2557
3234
  timeout: 2e3,
2558
3235
  stdio: ["pipe", "pipe", "pipe"]
2559
3236
  }).trim();
2560
- repoRoot = path12.dirname(gitCommonDir);
3237
+ repoRoot = path14.dirname(gitCommonDir);
2561
3238
  } catch {
2562
3239
  repoRoot = execSync6("git rev-parse --show-toplevel", {
2563
3240
  cwd: dir,
@@ -2566,11 +3243,11 @@ function getProjectName(cwd) {
2566
3243
  stdio: ["pipe", "pipe", "pipe"]
2567
3244
  }).trim();
2568
3245
  }
2569
- _cached2 = path12.basename(repoRoot);
3246
+ _cached2 = path14.basename(repoRoot);
2570
3247
  _cachedCwd = dir;
2571
3248
  return _cached2;
2572
3249
  } catch {
2573
- _cached2 = path12.basename(dir);
3250
+ _cached2 = path14.basename(dir);
2574
3251
  _cachedCwd = dir;
2575
3252
  return _cached2;
2576
3253
  }
@@ -2602,7 +3279,7 @@ function findSessionForProject(projectName) {
2602
3279
  const sessions = listSessions();
2603
3280
  for (const s of sessions) {
2604
3281
  const proj = s.projectDir.split("/").filter(Boolean).pop();
2605
- if (proj === projectName && (s.agentId === "exe" || isCoordinatorName(s.agentId))) return s;
3282
+ if (proj === projectName && isCoordinatorName(s.agentId)) return s;
2606
3283
  }
2607
3284
  return null;
2608
3285
  }
@@ -2648,7 +3325,7 @@ var init_session_scope = __esm({
2648
3325
 
2649
3326
  // src/lib/tasks-notify.ts
2650
3327
  async function dispatchTaskToEmployee(input2) {
2651
- if (input2.assignedTo === "exe" || isCoordinatorName(input2.assignedTo)) return { dispatched: "skipped" };
3328
+ if (isCoordinatorName(input2.assignedTo)) return { dispatched: "skipped" };
2652
3329
  let crossProject = false;
2653
3330
  if (input2.projectName) {
2654
3331
  try {
@@ -3043,8 +3720,8 @@ __export(tasks_exports, {
3043
3720
  updateTaskStatus: () => updateTaskStatus,
3044
3721
  writeCheckpoint: () => writeCheckpoint
3045
3722
  });
3046
- import path13 from "path";
3047
- import { writeFileSync as writeFileSync6, mkdirSync as mkdirSync5, unlinkSync as unlinkSync5 } from "fs";
3723
+ import path15 from "path";
3724
+ import { writeFileSync as writeFileSync7, mkdirSync as mkdirSync6, unlinkSync as unlinkSync6 } from "fs";
3048
3725
  async function createTask(input2) {
3049
3726
  const result = await createTaskCore(input2);
3050
3727
  if (!input2.skipDispatch && result.status !== "blocked" && !process.env.VITEST) {
@@ -3063,14 +3740,14 @@ async function updateTask(input2) {
3063
3740
  const { row, taskFile, now, taskId } = await updateTaskStatus(input2);
3064
3741
  try {
3065
3742
  const agent = String(row.assigned_to);
3066
- const cacheDir = path13.join(EXE_AI_DIR, "session-cache");
3067
- const cachePath = path13.join(cacheDir, `current-task-${agent}.json`);
3743
+ const cacheDir = path15.join(EXE_AI_DIR, "session-cache");
3744
+ const cachePath = path15.join(cacheDir, `current-task-${agent}.json`);
3068
3745
  if (input2.status === "in_progress") {
3069
- mkdirSync5(cacheDir, { recursive: true });
3070
- writeFileSync6(cachePath, JSON.stringify({ taskId, title: String(row.title) }));
3746
+ mkdirSync6(cacheDir, { recursive: true });
3747
+ writeFileSync7(cachePath, JSON.stringify({ taskId, title: String(row.title) }));
3071
3748
  } else if (input2.status === "done" || input2.status === "blocked" || input2.status === "cancelled") {
3072
3749
  try {
3073
- unlinkSync5(cachePath);
3750
+ unlinkSync6(cachePath);
3074
3751
  } catch {
3075
3752
  }
3076
3753
  }
@@ -3127,7 +3804,7 @@ async function updateTask(input2) {
3127
3804
  }
3128
3805
  const isTerminal = input2.status === "done" || input2.status === "needs_review";
3129
3806
  if (isTerminal) {
3130
- const isCoordinator = String(row.assigned_to) === "exe" || isCoordinatorName(String(row.assigned_to));
3807
+ const isCoordinator = isCoordinatorName(String(row.assigned_to));
3131
3808
  if (!isCoordinator) {
3132
3809
  notifyTaskDone();
3133
3810
  }
@@ -3152,7 +3829,7 @@ async function updateTask(input2) {
3152
3829
  }
3153
3830
  }
3154
3831
  }
3155
- if (input2.status === "done" && String(row.assigned_to) !== "exe" && !isCoordinatorName(String(row.assigned_to)) && !process.env.VITEST) {
3832
+ if (input2.status === "done" && !isCoordinatorName(String(row.assigned_to)) && !process.env.VITEST) {
3156
3833
  Promise.resolve().then(() => (init_skill_learning(), skill_learning_exports)).then(
3157
3834
  ({ captureAndLearn: captureAndLearn2 }) => captureAndLearn2({
3158
3835
  taskId,
@@ -3168,7 +3845,7 @@ async function updateTask(input2) {
3168
3845
  });
3169
3846
  }
3170
3847
  let nextTask;
3171
- if (isTerminal && String(row.assigned_to) !== "exe" && !isCoordinatorName(String(row.assigned_to))) {
3848
+ if (isTerminal && !isCoordinatorName(String(row.assigned_to))) {
3172
3849
  try {
3173
3850
  nextTask = await findNextTask(String(row.assigned_to));
3174
3851
  } catch {
@@ -3512,7 +4189,7 @@ var init_capacity_monitor = __esm({
3512
4189
  // src/lib/tmux-routing.ts
3513
4190
  var tmux_routing_exports = {};
3514
4191
  __export(tmux_routing_exports, {
3515
- acquireSpawnLock: () => acquireSpawnLock,
4192
+ acquireSpawnLock: () => acquireSpawnLock2,
3516
4193
  employeeSessionName: () => employeeSessionName,
3517
4194
  ensureEmployee: () => ensureEmployee,
3518
4195
  extractRootExe: () => extractRootExe,
@@ -3527,20 +4204,20 @@ __export(tmux_routing_exports, {
3527
4204
  notifyParentExe: () => notifyParentExe,
3528
4205
  parseParentExe: () => parseParentExe,
3529
4206
  registerParentExe: () => registerParentExe,
3530
- releaseSpawnLock: () => releaseSpawnLock,
4207
+ releaseSpawnLock: () => releaseSpawnLock2,
3531
4208
  resolveExeSession: () => resolveExeSession,
3532
4209
  sendIntercom: () => sendIntercom,
3533
4210
  spawnEmployee: () => spawnEmployee,
3534
4211
  verifyPaneAtCapacity: () => verifyPaneAtCapacity
3535
4212
  });
3536
4213
  import { execFileSync as execFileSync2, execSync as execSync7 } from "child_process";
3537
- import { readFileSync as readFileSync10, writeFileSync as writeFileSync7, mkdirSync as mkdirSync6, existsSync as existsSync10, appendFileSync } from "fs";
3538
- import path14 from "path";
3539
- import os6 from "os";
3540
- import { fileURLToPath } from "url";
3541
- import { unlinkSync as unlinkSync6 } from "fs";
4214
+ import { readFileSync as readFileSync12, writeFileSync as writeFileSync8, mkdirSync as mkdirSync7, existsSync as existsSync12, appendFileSync } from "fs";
4215
+ import path16 from "path";
4216
+ import os7 from "os";
4217
+ import { fileURLToPath as fileURLToPath2 } from "url";
4218
+ import { unlinkSync as unlinkSync7 } from "fs";
3542
4219
  function spawnLockPath(sessionName) {
3543
- return path14.join(SPAWN_LOCK_DIR, `${sessionName}.lock`);
4220
+ return path16.join(SPAWN_LOCK_DIR, `${sessionName}.lock`);
3544
4221
  }
3545
4222
  function isProcessAlive(pid) {
3546
4223
  try {
@@ -3550,14 +4227,14 @@ function isProcessAlive(pid) {
3550
4227
  return false;
3551
4228
  }
3552
4229
  }
3553
- function acquireSpawnLock(sessionName) {
3554
- if (!existsSync10(SPAWN_LOCK_DIR)) {
3555
- mkdirSync6(SPAWN_LOCK_DIR, { recursive: true });
4230
+ function acquireSpawnLock2(sessionName) {
4231
+ if (!existsSync12(SPAWN_LOCK_DIR)) {
4232
+ mkdirSync7(SPAWN_LOCK_DIR, { recursive: true });
3556
4233
  }
3557
4234
  const lockFile = spawnLockPath(sessionName);
3558
- if (existsSync10(lockFile)) {
4235
+ if (existsSync12(lockFile)) {
3559
4236
  try {
3560
- const lock = JSON.parse(readFileSync10(lockFile, "utf8"));
4237
+ const lock = JSON.parse(readFileSync12(lockFile, "utf8"));
3561
4238
  const age = Date.now() - lock.timestamp;
3562
4239
  if (isProcessAlive(lock.pid) && age < 6e4) {
3563
4240
  return false;
@@ -3565,25 +4242,25 @@ function acquireSpawnLock(sessionName) {
3565
4242
  } catch {
3566
4243
  }
3567
4244
  }
3568
- writeFileSync7(lockFile, JSON.stringify({ pid: process.pid, timestamp: Date.now() }));
4245
+ writeFileSync8(lockFile, JSON.stringify({ pid: process.pid, timestamp: Date.now() }));
3569
4246
  return true;
3570
4247
  }
3571
- function releaseSpawnLock(sessionName) {
4248
+ function releaseSpawnLock2(sessionName) {
3572
4249
  try {
3573
- unlinkSync6(spawnLockPath(sessionName));
4250
+ unlinkSync7(spawnLockPath(sessionName));
3574
4251
  } catch {
3575
4252
  }
3576
4253
  }
3577
4254
  function resolveBehaviorsExporterScript() {
3578
4255
  try {
3579
- const thisFile = fileURLToPath(import.meta.url);
3580
- const scriptPath = path14.join(
3581
- path14.dirname(thisFile),
4256
+ const thisFile = fileURLToPath2(import.meta.url);
4257
+ const scriptPath = path16.join(
4258
+ path16.dirname(thisFile),
3582
4259
  "..",
3583
4260
  "bin",
3584
4261
  "exe-export-behaviors.js"
3585
4262
  );
3586
- return existsSync10(scriptPath) ? scriptPath : null;
4263
+ return existsSync12(scriptPath) ? scriptPath : null;
3587
4264
  } catch {
3588
4265
  return null;
3589
4266
  }
@@ -3649,12 +4326,12 @@ function extractRootExe(name) {
3649
4326
  return parts.length > 0 ? parts[parts.length - 1] : null;
3650
4327
  }
3651
4328
  function registerParentExe(sessionKey, parentExe, dispatchedBy) {
3652
- if (!existsSync10(SESSION_CACHE)) {
3653
- mkdirSync6(SESSION_CACHE, { recursive: true });
4329
+ if (!existsSync12(SESSION_CACHE)) {
4330
+ mkdirSync7(SESSION_CACHE, { recursive: true });
3654
4331
  }
3655
4332
  const rootExe = extractRootExe(parentExe) ?? parentExe;
3656
- const filePath = path14.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`);
3657
- writeFileSync7(filePath, JSON.stringify({
4333
+ const filePath = path16.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`);
4334
+ writeFileSync8(filePath, JSON.stringify({
3658
4335
  parentExe: rootExe,
3659
4336
  dispatchedBy: dispatchedBy || rootExe,
3660
4337
  registeredAt: (/* @__PURE__ */ new Date()).toISOString()
@@ -3662,7 +4339,7 @@ function registerParentExe(sessionKey, parentExe, dispatchedBy) {
3662
4339
  }
3663
4340
  function getParentExe(sessionKey) {
3664
4341
  try {
3665
- const data = JSON.parse(readFileSync10(path14.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`), "utf8"));
4342
+ const data = JSON.parse(readFileSync12(path16.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`), "utf8"));
3666
4343
  return data.parentExe || null;
3667
4344
  } catch {
3668
4345
  return null;
@@ -3670,8 +4347,8 @@ function getParentExe(sessionKey) {
3670
4347
  }
3671
4348
  function getDispatchedBy(sessionKey) {
3672
4349
  try {
3673
- const data = JSON.parse(readFileSync10(
3674
- path14.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`),
4350
+ const data = JSON.parse(readFileSync12(
4351
+ path16.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`),
3675
4352
  "utf8"
3676
4353
  ));
3677
4354
  return data.dispatchedBy ?? data.parentExe ?? null;
@@ -3697,10 +4374,10 @@ function isEmployeeAlive(sessionName) {
3697
4374
  }
3698
4375
  function findFreeInstance(employeeName, exeSession, maxInstances = 10, isAlive = isEmployeeAlive) {
3699
4376
  const base = employeeSessionName(employeeName, exeSession);
3700
- if (!isAlive(base) && acquireSpawnLock(base)) return 0;
4377
+ if (!isAlive(base) && acquireSpawnLock2(base)) return 0;
3701
4378
  for (let i = 2; i <= maxInstances; i++) {
3702
4379
  const candidate = employeeSessionName(employeeName, exeSession, i);
3703
- if (!isAlive(candidate) && acquireSpawnLock(candidate)) return i;
4380
+ if (!isAlive(candidate) && acquireSpawnLock2(candidate)) return i;
3704
4381
  }
3705
4382
  return null;
3706
4383
  }
@@ -3732,32 +4409,50 @@ async function verifyPaneAtCapacity(sessionName) {
3732
4409
  }
3733
4410
  function readDebounceState() {
3734
4411
  try {
3735
- if (!existsSync10(DEBOUNCE_FILE)) return {};
3736
- return JSON.parse(readFileSync10(DEBOUNCE_FILE, "utf8"));
4412
+ if (!existsSync12(DEBOUNCE_FILE)) return {};
4413
+ const raw = JSON.parse(readFileSync12(DEBOUNCE_FILE, "utf8"));
4414
+ const state = {};
4415
+ for (const [key, val] of Object.entries(raw)) {
4416
+ if (typeof val === "number") {
4417
+ state[key] = { lastSent: val, pending: 0 };
4418
+ } else if (val && typeof val === "object" && "lastSent" in val) {
4419
+ state[key] = val;
4420
+ }
4421
+ }
4422
+ return state;
3737
4423
  } catch {
3738
4424
  return {};
3739
4425
  }
3740
4426
  }
3741
4427
  function writeDebounceState(state) {
3742
4428
  try {
3743
- if (!existsSync10(SESSION_CACHE)) mkdirSync6(SESSION_CACHE, { recursive: true });
3744
- writeFileSync7(DEBOUNCE_FILE, JSON.stringify(state));
4429
+ if (!existsSync12(SESSION_CACHE)) mkdirSync7(SESSION_CACHE, { recursive: true });
4430
+ writeFileSync8(DEBOUNCE_FILE, JSON.stringify(state));
3745
4431
  } catch {
3746
4432
  }
3747
4433
  }
3748
4434
  function isDebounced(targetSession) {
3749
4435
  const state = readDebounceState();
3750
- const lastSent = state[targetSession] ?? 0;
3751
- return Date.now() - lastSent < INTERCOM_DEBOUNCE_MS;
4436
+ const entry = state[targetSession];
4437
+ const lastSent = entry?.lastSent ?? 0;
4438
+ if (Date.now() - lastSent < INTERCOM_DEBOUNCE_MS) {
4439
+ if (!state[targetSession]) state[targetSession] = { lastSent, pending: 0 };
4440
+ state[targetSession].pending++;
4441
+ writeDebounceState(state);
4442
+ return true;
4443
+ }
4444
+ return false;
3752
4445
  }
3753
4446
  function recordDebounce(targetSession) {
3754
4447
  const state = readDebounceState();
3755
- state[targetSession] = Date.now();
4448
+ const batched = state[targetSession]?.pending ?? 0;
4449
+ state[targetSession] = { lastSent: Date.now(), pending: 0 };
3756
4450
  const cutoff = Date.now() - DEBOUNCE_CLEANUP_AGE_MS;
3757
4451
  for (const key of Object.keys(state)) {
3758
- if ((state[key] ?? 0) < cutoff) delete state[key];
4452
+ if ((state[key]?.lastSent ?? 0) < cutoff) delete state[key];
3759
4453
  }
3760
4454
  writeDebounceState(state);
4455
+ return batched;
3761
4456
  }
3762
4457
  function logIntercom(msg) {
3763
4458
  const line = `[${(/* @__PURE__ */ new Date()).toISOString()}] ${msg}
@@ -3802,7 +4497,7 @@ function sendIntercom(targetSession) {
3802
4497
  return "skipped_exe";
3803
4498
  }
3804
4499
  if (isDebounced(targetSession)) {
3805
- logIntercom(`DEBOUNCE \u2192 ${targetSession} (cross-process file debounce)`);
4500
+ logIntercom(`DEBOUNCE \u2192 ${targetSession} (nudge batched, task safe in DB)`);
3806
4501
  return "debounced";
3807
4502
  }
3808
4503
  try {
@@ -3814,14 +4509,14 @@ function sendIntercom(targetSession) {
3814
4509
  const sessionState = getSessionState(targetSession);
3815
4510
  if (sessionState === "no_claude") {
3816
4511
  queueIntercom(targetSession, "claude not running in session");
3817
- recordDebounce(targetSession);
3818
- logIntercom(`QUEUED \u2192 ${targetSession} (no claude process \u2014 raw shell detected)`);
4512
+ const batched2 = recordDebounce(targetSession);
4513
+ logIntercom(`QUEUED \u2192 ${targetSession} (no claude process)${batched2 > 0 ? ` [${batched2} batched]` : ""}`);
3819
4514
  return "queued";
3820
4515
  }
3821
4516
  if (sessionState === "thinking" || sessionState === "tool") {
3822
4517
  queueIntercom(targetSession, "session busy at send time");
3823
- recordDebounce(targetSession);
3824
- logIntercom(`QUEUED \u2192 ${targetSession} (session busy, will retry from queue)`);
4518
+ const batched2 = recordDebounce(targetSession);
4519
+ logIntercom(`QUEUED \u2192 ${targetSession} (session busy)${batched2 > 0 ? ` [${batched2} batched]` : ""}`);
3825
4520
  return "queued";
3826
4521
  }
3827
4522
  if (transport.isPaneInCopyMode(targetSession)) {
@@ -3829,8 +4524,8 @@ function sendIntercom(targetSession) {
3829
4524
  transport.sendKeys(targetSession, "q");
3830
4525
  }
3831
4526
  transport.sendKeys(targetSession, "/exe-intercom");
3832
- recordDebounce(targetSession);
3833
- logIntercom(`DELIVERED \u2192 ${targetSession} (fire-and-forget)`);
4527
+ const batched = recordDebounce(targetSession);
4528
+ logIntercom(`DELIVERED \u2192 ${targetSession}${batched > 0 ? ` [${batched} nudges batched during debounce]` : ""} (fire-and-forget)`);
3834
4529
  return "delivered";
3835
4530
  } catch {
3836
4531
  logIntercom(`FAIL \u2192 ${targetSession}`);
@@ -3860,7 +4555,7 @@ function notifyParentExe(sessionKey) {
3860
4555
  return true;
3861
4556
  }
3862
4557
  function ensureEmployee(employeeName, exeSession, projectDir, opts) {
3863
- if (employeeName === "exe" || isCoordinatorName(employeeName)) {
4558
+ if (isCoordinatorName(employeeName)) {
3864
4559
  return { status: "failed", sessionName: "", error: "The COO is not a dispatchable employee" };
3865
4560
  }
3866
4561
  try {
@@ -3932,26 +4627,26 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
3932
4627
  const transport = getTransport();
3933
4628
  const sessionName = employeeSessionName(employeeName, exeSession, opts?.instance);
3934
4629
  const instanceLabel = opts?.instance != null && opts.instance > 0 ? `${employeeName}${opts.instance}` : employeeName;
3935
- const logDir = path14.join(os6.homedir(), ".exe-os", "session-logs");
3936
- const logFile = path14.join(logDir, `${instanceLabel}-${Date.now()}.log`);
3937
- if (!existsSync10(logDir)) {
3938
- mkdirSync6(logDir, { recursive: true });
4630
+ const logDir = path16.join(os7.homedir(), ".exe-os", "session-logs");
4631
+ const logFile = path16.join(logDir, `${instanceLabel}-${Date.now()}.log`);
4632
+ if (!existsSync12(logDir)) {
4633
+ mkdirSync7(logDir, { recursive: true });
3939
4634
  }
3940
4635
  transport.kill(sessionName);
3941
4636
  let cleanupSuffix = "";
3942
4637
  try {
3943
- const thisFile = fileURLToPath(import.meta.url);
3944
- const cleanupScript = path14.join(path14.dirname(thisFile), "..", "bin", "exe-session-cleanup.js");
3945
- if (existsSync10(cleanupScript)) {
4638
+ const thisFile = fileURLToPath2(import.meta.url);
4639
+ const cleanupScript = path16.join(path16.dirname(thisFile), "..", "bin", "exe-session-cleanup.js");
4640
+ if (existsSync12(cleanupScript)) {
3946
4641
  cleanupSuffix = `; ${process.execPath} "${cleanupScript}" "${employeeName}" "${exeSession}"`;
3947
4642
  }
3948
4643
  } catch {
3949
4644
  }
3950
4645
  try {
3951
- const claudeJsonPath = path14.join(os6.homedir(), ".claude.json");
4646
+ const claudeJsonPath = path16.join(os7.homedir(), ".claude.json");
3952
4647
  let claudeJson = {};
3953
4648
  try {
3954
- claudeJson = JSON.parse(readFileSync10(claudeJsonPath, "utf8"));
4649
+ claudeJson = JSON.parse(readFileSync12(claudeJsonPath, "utf8"));
3955
4650
  } catch {
3956
4651
  }
3957
4652
  if (!claudeJson.projects) claudeJson.projects = {};
@@ -3959,17 +4654,17 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
3959
4654
  const trustDir = opts?.cwd ?? projectDir;
3960
4655
  if (!projects[trustDir]) projects[trustDir] = {};
3961
4656
  projects[trustDir].hasTrustDialogAccepted = true;
3962
- writeFileSync7(claudeJsonPath, JSON.stringify(claudeJson, null, 2) + "\n");
4657
+ writeFileSync8(claudeJsonPath, JSON.stringify(claudeJson, null, 2) + "\n");
3963
4658
  } catch {
3964
4659
  }
3965
4660
  try {
3966
- const settingsDir = path14.join(os6.homedir(), ".claude", "projects");
4661
+ const settingsDir = path16.join(os7.homedir(), ".claude", "projects");
3967
4662
  const normalizedKey = (opts?.cwd ?? projectDir).replace(/\//g, "-").replace(/^-/, "");
3968
- const projSettingsDir = path14.join(settingsDir, normalizedKey);
3969
- const settingsPath = path14.join(projSettingsDir, "settings.json");
4663
+ const projSettingsDir = path16.join(settingsDir, normalizedKey);
4664
+ const settingsPath = path16.join(projSettingsDir, "settings.json");
3970
4665
  let settings = {};
3971
4666
  try {
3972
- settings = JSON.parse(readFileSync10(settingsPath, "utf8"));
4667
+ settings = JSON.parse(readFileSync12(settingsPath, "utf8"));
3973
4668
  } catch {
3974
4669
  }
3975
4670
  const perms = settings.permissions ?? {};
@@ -3997,21 +4692,24 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
3997
4692
  if (changed) {
3998
4693
  perms.allow = allow;
3999
4694
  settings.permissions = perms;
4000
- mkdirSync6(projSettingsDir, { recursive: true });
4001
- writeFileSync7(settingsPath, JSON.stringify(settings, null, 2) + "\n");
4695
+ mkdirSync7(projSettingsDir, { recursive: true });
4696
+ writeFileSync8(settingsPath, JSON.stringify(settings, null, 2) + "\n");
4002
4697
  }
4003
4698
  } catch {
4004
4699
  }
4005
4700
  const spawnCwd = opts?.cwd ?? projectDir;
4006
4701
  const useExeAgent = !!(opts?.model && opts?.provider);
4007
- const ccProvider = useExeAgent ? DEFAULT_PROVIDER : detectActiveProvider();
4702
+ const agentRtConfig = getAgentRuntime(employeeName);
4703
+ const useCodex = !useExeAgent && agentRtConfig.runtime === "codex";
4704
+ const useOpencode = !useExeAgent && !useCodex && agentRtConfig.runtime === "opencode";
4705
+ const ccProvider = useExeAgent || useCodex || useOpencode ? DEFAULT_PROVIDER : detectActiveProvider();
4008
4706
  const useBinSymlink = ccProvider !== DEFAULT_PROVIDER;
4009
4707
  let identityFlag = "";
4010
4708
  let behaviorsFlag = "";
4011
4709
  let legacyFallbackWarned = false;
4012
4710
  if (!useExeAgent && !useBinSymlink) {
4013
- const identityPath = path14.join(
4014
- os6.homedir(),
4711
+ const identityPath = path16.join(
4712
+ os7.homedir(),
4015
4713
  ".exe-os",
4016
4714
  "identity",
4017
4715
  `${employeeName}.md`
@@ -4020,13 +4718,13 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
4020
4718
  const hasAgentFlag = claudeSupportsAgentFlag();
4021
4719
  if (hasAgentFlag) {
4022
4720
  identityFlag = ` --agent ${employeeName}`;
4023
- } else if (existsSync10(identityPath)) {
4721
+ } else if (existsSync12(identityPath)) {
4024
4722
  identityFlag = ` --append-system-prompt-file ${identityPath}`;
4025
4723
  legacyFallbackWarned = true;
4026
4724
  }
4027
4725
  const behaviorsFile = exportBehaviorsSync(
4028
4726
  employeeName,
4029
- path14.basename(spawnCwd),
4727
+ path16.basename(spawnCwd),
4030
4728
  sessionName
4031
4729
  );
4032
4730
  if (behaviorsFile) {
@@ -4041,16 +4739,16 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
4041
4739
  }
4042
4740
  let sessionContextFlag = "";
4043
4741
  try {
4044
- const ctxDir = path14.join(os6.homedir(), ".exe-os", "session-cache");
4045
- mkdirSync6(ctxDir, { recursive: true });
4046
- const ctxFile = path14.join(ctxDir, `session-context-${sessionName}.md`);
4742
+ const ctxDir = path16.join(os7.homedir(), ".exe-os", "session-cache");
4743
+ mkdirSync7(ctxDir, { recursive: true });
4744
+ const ctxFile = path16.join(ctxDir, `session-context-${sessionName}.md`);
4047
4745
  const ctxContent = [
4048
4746
  `## Session Context`,
4049
4747
  `You are running in tmux session: ${sessionName}.`,
4050
4748
  `Your parent coordinator session is ${exeSession}.`,
4051
4749
  `Your employees (if any) use the -${exeSession} suffix.`
4052
4750
  ].join("\n");
4053
- writeFileSync7(ctxFile, ctxContent);
4751
+ writeFileSync8(ctxFile, ctxContent);
4054
4752
  sessionContextFlag = ` --append-system-prompt-file ${ctxFile}`;
4055
4753
  } catch {
4056
4754
  }
@@ -4064,9 +4762,48 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
4064
4762
  }
4065
4763
  }
4066
4764
  }
4765
+ if (useCodex) {
4766
+ const codexCfg = RUNTIME_TABLE.codex;
4767
+ if (codexCfg?.apiKeyEnv) {
4768
+ const keyVal = process.env[codexCfg.apiKeyEnv];
4769
+ if (keyVal) {
4770
+ envPrefix = `${envPrefix} ${codexCfg.apiKeyEnv}=${keyVal}`;
4771
+ }
4772
+ }
4773
+ envPrefix = `${envPrefix} EXE_AGENT_MODEL=${agentRtConfig.model}`;
4774
+ }
4775
+ if (useOpencode) {
4776
+ const ocCfg = PROVIDER_TABLE.opencode;
4777
+ if (ocCfg?.apiKeyEnv) {
4778
+ const keyVal = process.env[ocCfg.apiKeyEnv];
4779
+ if (keyVal) {
4780
+ envPrefix = `${envPrefix} ${ocCfg.apiKeyEnv}=${keyVal}`;
4781
+ }
4782
+ }
4783
+ envPrefix = `${envPrefix} ANTHROPIC_MODEL=${agentRtConfig.model}`;
4784
+ }
4785
+ if (!useExeAgent && !useCodex && !useOpencode && !useBinSymlink) {
4786
+ const defaultClaudeModel = DEFAULT_MODELS.claude;
4787
+ if (agentRtConfig.runtime === "claude" && agentRtConfig.model !== defaultClaudeModel) {
4788
+ envPrefix = `${envPrefix} ANTHROPIC_MODEL=${agentRtConfig.model}`;
4789
+ }
4790
+ }
4067
4791
  let spawnCommand;
4068
4792
  if (useExeAgent) {
4069
4793
  spawnCommand = `${envPrefix} exe-agent --employee ${employeeName} --model ${opts.model} --provider ${opts.provider}${cleanupSuffix}`;
4794
+ } else if (useCodex) {
4795
+ process.stderr.write(
4796
+ `[tmux-routing] agent-config: ${employeeName} \u2192 codex (${agentRtConfig.model})
4797
+ `
4798
+ );
4799
+ spawnCommand = `${envPrefix} exe-start-codex --agent ${employeeName}${cleanupSuffix}`;
4800
+ } else if (useOpencode) {
4801
+ const binName = `${employeeName}-opencode`;
4802
+ process.stderr.write(
4803
+ `[tmux-routing] agent-config: ${employeeName} \u2192 opencode (${agentRtConfig.model})
4804
+ `
4805
+ );
4806
+ spawnCommand = `${envPrefix} ${binName}${cleanupSuffix}`;
4070
4807
  } else if (useBinSymlink) {
4071
4808
  const binName = `${employeeName}-${ccProvider}`;
4072
4809
  process.stderr.write(
@@ -4082,17 +4819,19 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
4082
4819
  command: spawnCommand
4083
4820
  });
4084
4821
  if (spawnResult.error) {
4085
- releaseSpawnLock(sessionName);
4822
+ releaseSpawnLock2(sessionName);
4086
4823
  return { sessionName, error: `tmux new-session failed: ${spawnResult.error}` };
4087
4824
  }
4088
4825
  transport.pipeLog(sessionName, logFile);
4089
4826
  try {
4090
4827
  const mySession = getMySession();
4091
- const dispatchInfo = path14.join(SESSION_CACHE, `dispatch-info-${sessionName}.json`);
4092
- writeFileSync7(dispatchInfo, JSON.stringify({
4828
+ const dispatchInfo = path16.join(SESSION_CACHE, `dispatch-info-${sessionName}.json`);
4829
+ writeFileSync8(dispatchInfo, JSON.stringify({
4093
4830
  dispatchedBy: mySession,
4094
4831
  rootExe: exeSession,
4095
- provider: useBinSymlink ? ccProvider : useExeAgent ? opts.provider : "anthropic",
4832
+ provider: useBinSymlink ? ccProvider : useExeAgent ? opts.provider : useCodex ? "openai" : useOpencode ? "opencode" : "anthropic",
4833
+ runtime: useCodex ? "codex" : useOpencode ? "opencode" : useExeAgent ? "exe-agent" : "claude",
4834
+ model: useCodex ? agentRtConfig.model : useOpencode ? agentRtConfig.model : void 0,
4096
4835
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
4097
4836
  }));
4098
4837
  } catch {
@@ -4110,6 +4849,11 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
4110
4849
  booted = true;
4111
4850
  break;
4112
4851
  }
4852
+ } else if (useCodex) {
4853
+ if (pane.includes("codex") || pane.includes("Codex") || pane.includes("exe-start-codex")) {
4854
+ booted = true;
4855
+ break;
4856
+ }
4113
4857
  } else {
4114
4858
  if (pane.includes("Claude Code") || pane.includes("\u276F")) {
4115
4859
  booted = true;
@@ -4120,10 +4864,11 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
4120
4864
  }
4121
4865
  }
4122
4866
  if (!booted) {
4123
- releaseSpawnLock(sessionName);
4124
- return { sessionName, error: `${useExeAgent ? "exe-agent" : "claude"} did not boot within 15s` };
4867
+ releaseSpawnLock2(sessionName);
4868
+ const runtimeLabel = useExeAgent ? "exe-agent" : useCodex ? "codex" : "claude";
4869
+ return { sessionName, error: `${runtimeLabel} did not boot within 15s` };
4125
4870
  }
4126
- if (!useExeAgent) {
4871
+ if (!useExeAgent && !useCodex) {
4127
4872
  try {
4128
4873
  transport.sendKeys(sessionName, `/exe-call ${employeeName}`);
4129
4874
  } catch {
@@ -4137,7 +4882,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
4137
4882
  pid: 0,
4138
4883
  registeredAt: (/* @__PURE__ */ new Date()).toISOString()
4139
4884
  });
4140
- releaseSpawnLock(sessionName);
4885
+ releaseSpawnLock2(sessionName);
4141
4886
  return { sessionName };
4142
4887
  }
4143
4888
  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;
@@ -4150,17 +4895,19 @@ var init_tmux_routing = __esm({
4150
4895
  init_cc_agent_support();
4151
4896
  init_mcp_prefix();
4152
4897
  init_provider_table();
4898
+ init_agent_config();
4899
+ init_runtime_table();
4153
4900
  init_intercom_queue();
4154
4901
  init_plan_limits();
4155
4902
  init_employees();
4156
- SPAWN_LOCK_DIR = path14.join(os6.homedir(), ".exe-os", "spawn-locks");
4157
- SESSION_CACHE = path14.join(os6.homedir(), ".exe-os", "session-cache");
4903
+ SPAWN_LOCK_DIR = path16.join(os7.homedir(), ".exe-os", "spawn-locks");
4904
+ SESSION_CACHE = path16.join(os7.homedir(), ".exe-os", "session-cache");
4158
4905
  BEHAVIORS_EXPORT_TIMEOUT_MS = 1e4;
4159
4906
  VALID_SESSION_NAME = /^[a-z]+\d*-[a-zA-Z0-9_]+$/;
4160
4907
  VERIFY_PANE_LINES = 200;
4161
4908
  INTERCOM_DEBOUNCE_MS = 3e4;
4162
- INTERCOM_LOG2 = path14.join(os6.homedir(), ".exe-os", "intercom.log");
4163
- DEBOUNCE_FILE = path14.join(SESSION_CACHE, "intercom-debounce.json");
4909
+ INTERCOM_LOG2 = path16.join(os7.homedir(), ".exe-os", "intercom.log");
4910
+ DEBOUNCE_FILE = path16.join(SESSION_CACHE, "intercom-debounce.json");
4164
4911
  DEBOUNCE_CLEANUP_AGE_MS = 5 * 60 * 1e3;
4165
4912
  BUSY_PATTERN = /[✻✽✶✳·].*…|Running…/;
4166
4913
  }
@@ -4201,14 +4948,14 @@ var init_memory = __esm({
4201
4948
 
4202
4949
  // src/lib/keychain.ts
4203
4950
  import { readFile as readFile4, writeFile as writeFile5, unlink, mkdir as mkdir4, chmod as chmod2 } from "fs/promises";
4204
- import { existsSync as existsSync11 } from "fs";
4205
- import path15 from "path";
4206
- import os7 from "os";
4951
+ import { existsSync as existsSync13 } from "fs";
4952
+ import path17 from "path";
4953
+ import os8 from "os";
4207
4954
  function getKeyDir() {
4208
- return process.env.EXE_OS_DIR ?? process.env.EXE_MEM_DIR ?? path15.join(os7.homedir(), ".exe-os");
4955
+ return process.env.EXE_OS_DIR ?? process.env.EXE_MEM_DIR ?? path17.join(os8.homedir(), ".exe-os");
4209
4956
  }
4210
4957
  function getKeyPath() {
4211
- return path15.join(getKeyDir(), "master.key");
4958
+ return path17.join(getKeyDir(), "master.key");
4212
4959
  }
4213
4960
  async function tryKeytar() {
4214
4961
  try {
@@ -4229,13 +4976,21 @@ async function getMasterKey() {
4229
4976
  }
4230
4977
  }
4231
4978
  const keyPath = getKeyPath();
4232
- if (!existsSync11(keyPath)) {
4979
+ if (!existsSync13(keyPath)) {
4980
+ process.stderr.write(
4981
+ `[keychain] Key not found at ${keyPath} (HOME=${os8.homedir()}, EXE_OS_DIR=${process.env.EXE_OS_DIR ?? "unset"})
4982
+ `
4983
+ );
4233
4984
  return null;
4234
4985
  }
4235
4986
  try {
4236
4987
  const content = await readFile4(keyPath, "utf-8");
4237
4988
  return Buffer.from(content.trim(), "base64");
4238
- } catch {
4989
+ } catch (err) {
4990
+ process.stderr.write(
4991
+ `[keychain] Key read failed at ${keyPath}: ${err instanceof Error ? err.message : String(err)}
4992
+ `
4993
+ );
4239
4994
  return null;
4240
4995
  }
4241
4996
  }
@@ -4261,13 +5016,13 @@ __export(shard_manager_exports, {
4261
5016
  listShards: () => listShards,
4262
5017
  shardExists: () => shardExists
4263
5018
  });
4264
- import path16 from "path";
4265
- import { existsSync as existsSync12, mkdirSync as mkdirSync7, readdirSync as readdirSync4 } from "fs";
5019
+ import path18 from "path";
5020
+ import { existsSync as existsSync14, mkdirSync as mkdirSync8, readdirSync as readdirSync4 } from "fs";
4266
5021
  import { createClient as createClient2 } from "@libsql/client";
4267
5022
  function initShardManager(encryptionKey) {
4268
5023
  _encryptionKey = encryptionKey;
4269
- if (!existsSync12(SHARDS_DIR)) {
4270
- mkdirSync7(SHARDS_DIR, { recursive: true });
5024
+ if (!existsSync14(SHARDS_DIR)) {
5025
+ mkdirSync8(SHARDS_DIR, { recursive: true });
4271
5026
  }
4272
5027
  _shardingEnabled = true;
4273
5028
  }
@@ -4287,7 +5042,7 @@ function getShardClient(projectName) {
4287
5042
  }
4288
5043
  const cached = _shards.get(safeName);
4289
5044
  if (cached) return cached;
4290
- const dbPath = path16.join(SHARDS_DIR, `${safeName}.db`);
5045
+ const dbPath = path18.join(SHARDS_DIR, `${safeName}.db`);
4291
5046
  const client = createClient2({
4292
5047
  url: `file:${dbPath}`,
4293
5048
  encryptionKey: _encryptionKey
@@ -4297,10 +5052,10 @@ function getShardClient(projectName) {
4297
5052
  }
4298
5053
  function shardExists(projectName) {
4299
5054
  const safeName = projectName.replace(/[^a-zA-Z0-9_-]/g, "_");
4300
- return existsSync12(path16.join(SHARDS_DIR, `${safeName}.db`));
5055
+ return existsSync14(path18.join(SHARDS_DIR, `${safeName}.db`));
4301
5056
  }
4302
5057
  function listShards() {
4303
- if (!existsSync12(SHARDS_DIR)) return [];
5058
+ if (!existsSync14(SHARDS_DIR)) return [];
4304
5059
  return readdirSync4(SHARDS_DIR).filter((f) => f.endsWith(".db")).map((f) => f.replace(".db", ""));
4305
5060
  }
4306
5061
  async function ensureShardSchema(client) {
@@ -4486,7 +5241,7 @@ var init_shard_manager = __esm({
4486
5241
  "src/lib/shard-manager.ts"() {
4487
5242
  "use strict";
4488
5243
  init_config();
4489
- SHARDS_DIR = path16.join(EXE_AI_DIR, "shards");
5244
+ SHARDS_DIR = path18.join(EXE_AI_DIR, "shards");
4490
5245
  _shards = /* @__PURE__ */ new Map();
4491
5246
  _encryptionKey = null;
4492
5247
  _shardingEnabled = false;
@@ -4611,7 +5366,7 @@ __export(global_procedures_exports, {
4611
5366
  loadGlobalProcedures: () => loadGlobalProcedures,
4612
5367
  storeGlobalProcedure: () => storeGlobalProcedure
4613
5368
  });
4614
- import { randomUUID as randomUUID2 } from "crypto";
5369
+ import { randomUUID as randomUUID3 } from "crypto";
4615
5370
  async function loadGlobalProcedures() {
4616
5371
  const client = getClient();
4617
5372
  const result = await client.execute({
@@ -4640,7 +5395,7 @@ ${sections.join("\n\n")}
4640
5395
  `;
4641
5396
  }
4642
5397
  async function storeGlobalProcedure(input2) {
4643
- const id = randomUUID2();
5398
+ const id = randomUUID3();
4644
5399
  const now = (/* @__PURE__ */ new Date()).toISOString();
4645
5400
  const client = getClient();
4646
5401
  await client.execute({
@@ -4691,6 +5446,7 @@ __export(store_exports, {
4691
5446
  vectorToBlob: () => vectorToBlob,
4692
5447
  writeMemory: () => writeMemory
4693
5448
  });
5449
+ import { createHash } from "crypto";
4694
5450
  function isBusyError2(err) {
4695
5451
  if (err instanceof Error) {
4696
5452
  const msg = err.message.toLowerCase();
@@ -4764,12 +5520,52 @@ function classifyTier(record) {
4764
5520
  if (["store_memory", "manual"].includes(record.tool_name ?? "") && (record.importance ?? 0) >= 5) return 2;
4765
5521
  return 3;
4766
5522
  }
5523
+ function inferFilePaths(record) {
5524
+ if (!["Read", "Write", "Edit"].includes(record.tool_name)) return null;
5525
+ const firstLine = record.raw_text.split("\n")[0] ?? "";
5526
+ const match = firstLine.match(/(\/[\w./-]+\.\w+)/);
5527
+ return match ? JSON.stringify([match[1]]) : null;
5528
+ }
5529
+ function inferCommitHash(record) {
5530
+ if (record.tool_name !== "Bash") return null;
5531
+ const match = record.raw_text.match(/\b([a-f0-9]{7,40})\b/);
5532
+ return match ? match[1] : null;
5533
+ }
5534
+ function inferLanguageType(record) {
5535
+ const text = record.raw_text;
5536
+ if (!text || text.length < 10) return null;
5537
+ const trimmed = text.trimStart();
5538
+ if (trimmed.startsWith("{") || trimmed.startsWith("[")) return "json";
5539
+ if (/\b(SELECT|INSERT|UPDATE|DELETE|CREATE TABLE|ALTER TABLE)\b/i.test(text)) return "sql";
5540
+ if (/\b(function |const |import |export |class |def |async |=>)\b/.test(text)) return "code";
5541
+ if (trimmed.startsWith("#") || trimmed.startsWith("*")) return "prose";
5542
+ return "mixed";
5543
+ }
5544
+ function inferDomain(record) {
5545
+ const proj = (record.project_name ?? "").toLowerCase();
5546
+ if (proj.includes("marketing") || proj.includes("content")) return "marketing";
5547
+ if (proj.includes("crm") || proj.includes("customer")) return "customer";
5548
+ return null;
5549
+ }
4767
5550
  async function writeMemory(record) {
4768
5551
  if (record.vector !== null && record.vector.length !== EMBEDDING_DIM) {
4769
5552
  throw new Error(
4770
5553
  `Expected ${EMBEDDING_DIM}-dim vector, got ${record.vector.length}`
4771
5554
  );
4772
5555
  }
5556
+ const contentHash = createHash("md5").update(record.raw_text).digest("hex");
5557
+ if (_pendingRecords.some((r) => r.content_hash === contentHash && r.agent_id === record.agent_id)) {
5558
+ return;
5559
+ }
5560
+ try {
5561
+ const client = getClient();
5562
+ const existing = await client.execute({
5563
+ sql: "SELECT id FROM memories WHERE content_hash = ? AND agent_id = ? LIMIT 1",
5564
+ args: [contentHash, record.agent_id]
5565
+ });
5566
+ if (existing.rows.length > 0) return;
5567
+ } catch {
5568
+ }
4773
5569
  const dbRow = {
4774
5570
  id: record.id,
4775
5571
  agent_id: record.agent_id,
@@ -4799,7 +5595,23 @@ async function writeMemory(record) {
4799
5595
  supersedes_id: record.supersedes_id ?? null,
4800
5596
  draft: record.draft ? 1 : 0,
4801
5597
  memory_type: record.memory_type ?? "raw",
4802
- trajectory: record.trajectory ? JSON.stringify(record.trajectory) : null
5598
+ trajectory: record.trajectory ? JSON.stringify(record.trajectory) : null,
5599
+ content_hash: contentHash,
5600
+ intent: record.intent ?? null,
5601
+ outcome: record.outcome ?? null,
5602
+ domain: record.domain ?? inferDomain(record),
5603
+ referenced_entities: record.referenced_entities ?? null,
5604
+ retrieval_count: record.retrieval_count ?? 0,
5605
+ chain_position: record.chain_position ?? null,
5606
+ review_status: record.review_status ?? null,
5607
+ context_window_pct: record.context_window_pct ?? null,
5608
+ file_paths: record.file_paths ?? inferFilePaths(record),
5609
+ commit_hash: record.commit_hash ?? inferCommitHash(record),
5610
+ duration_ms: record.duration_ms ?? null,
5611
+ token_cost: record.token_cost ?? null,
5612
+ audience: record.audience ?? null,
5613
+ language_type: record.language_type ?? inferLanguageType(record),
5614
+ parent_memory_id: record.parent_memory_id ?? null
4803
5615
  };
4804
5616
  _pendingRecords.push(dbRow);
4805
5617
  orgBus.emit({
@@ -4857,80 +5669,85 @@ async function flushBatch() {
4857
5669
  const draft = row.draft ? 1 : 0;
4858
5670
  const memoryType = row.memory_type ?? "raw";
4859
5671
  const trajectory = row.trajectory ?? null;
4860
- return {
4861
- sql: hasVector ? `INSERT OR IGNORE INTO memories
4862
- (id, agent_id, agent_role, session_id, timestamp,
5672
+ const contentHash = row.content_hash ?? null;
5673
+ const intent = row.intent ?? null;
5674
+ const outcome = row.outcome ?? null;
5675
+ const domain = row.domain ?? null;
5676
+ const referencedEntities = row.referenced_entities ?? null;
5677
+ const retrievalCount = row.retrieval_count ?? 0;
5678
+ const chainPosition = row.chain_position ?? null;
5679
+ const reviewStatus = row.review_status ?? null;
5680
+ const contextWindowPct = row.context_window_pct ?? null;
5681
+ const filePaths = row.file_paths ?? null;
5682
+ const commitHash = row.commit_hash ?? null;
5683
+ const durationMs = row.duration_ms ?? null;
5684
+ const tokenCost = row.token_cost ?? null;
5685
+ const audience = row.audience ?? null;
5686
+ const languageType = row.language_type ?? null;
5687
+ const parentMemoryId = row.parent_memory_id ?? null;
5688
+ const cols = `id, agent_id, agent_role, session_id, timestamp,
4863
5689
  tool_name, project_name,
4864
5690
  has_error, raw_text, vector, version, task_id, importance, status,
4865
5691
  confidence, last_accessed,
4866
5692
  workspace_id, document_id, user_id, char_offset, page_number,
4867
- source_path, source_type, tier, supersedes_id, draft, memory_type, trajectory)
4868
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, vector32(?), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` : `INSERT OR IGNORE INTO memories
4869
- (id, agent_id, agent_role, session_id, timestamp,
4870
- tool_name, project_name,
4871
- has_error, raw_text, vector, version, task_id, importance, status,
4872
- confidence, last_accessed,
4873
- workspace_id, document_id, user_id, char_offset, page_number,
4874
- source_path, source_type, tier, supersedes_id, draft, memory_type, trajectory)
4875
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
4876
- args: hasVector ? [
4877
- row.id,
4878
- row.agent_id,
4879
- row.agent_role,
4880
- row.session_id,
4881
- row.timestamp,
4882
- row.tool_name,
4883
- row.project_name,
4884
- row.has_error,
4885
- row.raw_text,
4886
- vectorToBlob(row.vector),
4887
- row.version,
4888
- taskId,
4889
- importance,
4890
- status,
4891
- confidence,
4892
- lastAccessed,
4893
- workspaceId,
4894
- documentId,
4895
- userId,
4896
- charOffset,
4897
- pageNumber,
4898
- sourcePath,
4899
- sourceType,
4900
- tier,
4901
- supersedesId,
4902
- draft,
4903
- memoryType,
4904
- trajectory
4905
- ] : [
4906
- row.id,
4907
- row.agent_id,
4908
- row.agent_role,
4909
- row.session_id,
4910
- row.timestamp,
4911
- row.tool_name,
4912
- row.project_name,
4913
- row.has_error,
4914
- row.raw_text,
4915
- row.version,
4916
- taskId,
4917
- importance,
4918
- status,
4919
- confidence,
4920
- lastAccessed,
4921
- workspaceId,
4922
- documentId,
4923
- userId,
4924
- charOffset,
4925
- pageNumber,
4926
- sourcePath,
4927
- sourceType,
4928
- tier,
4929
- supersedesId,
4930
- draft,
4931
- memoryType,
4932
- trajectory
4933
- ]
5693
+ source_path, source_type, tier, supersedes_id, draft, memory_type, trajectory, content_hash,
5694
+ intent, outcome, domain, referenced_entities, retrieval_count,
5695
+ chain_position, review_status, context_window_pct, file_paths, commit_hash,
5696
+ duration_ms, token_cost, audience, language_type, parent_memory_id`;
5697
+ const metaArgs = [
5698
+ intent,
5699
+ outcome,
5700
+ domain,
5701
+ referencedEntities,
5702
+ retrievalCount,
5703
+ chainPosition,
5704
+ reviewStatus,
5705
+ contextWindowPct,
5706
+ filePaths,
5707
+ commitHash,
5708
+ durationMs,
5709
+ tokenCost,
5710
+ audience,
5711
+ languageType,
5712
+ parentMemoryId
5713
+ ];
5714
+ const baseArgs = [
5715
+ row.id,
5716
+ row.agent_id,
5717
+ row.agent_role,
5718
+ row.session_id,
5719
+ row.timestamp,
5720
+ row.tool_name,
5721
+ row.project_name,
5722
+ row.has_error,
5723
+ row.raw_text
5724
+ ];
5725
+ const sharedArgs = [
5726
+ row.version,
5727
+ taskId,
5728
+ importance,
5729
+ status,
5730
+ confidence,
5731
+ lastAccessed,
5732
+ workspaceId,
5733
+ documentId,
5734
+ userId,
5735
+ charOffset,
5736
+ pageNumber,
5737
+ sourcePath,
5738
+ sourceType,
5739
+ tier,
5740
+ supersedesId,
5741
+ draft,
5742
+ memoryType,
5743
+ trajectory,
5744
+ contentHash
5745
+ ];
5746
+ return {
5747
+ sql: hasVector ? `INSERT OR IGNORE INTO memories (${cols})
5748
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, vector32(?), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` : `INSERT OR IGNORE INTO memories (${cols})
5749
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
5750
+ args: hasVector ? [...baseArgs, vectorToBlob(row.vector), ...sharedArgs, ...metaArgs] : [...baseArgs, ...sharedArgs, ...metaArgs]
4934
5751
  };
4935
5752
  };
4936
5753
  const globalClient = getClient();