@askexenow/exe-os 0.8.83 → 0.8.85

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 (95) 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 +97 -2
  5. package/dist/bin/cli.js +14350 -12518
  6. package/dist/bin/exe-agent.js +97 -88
  7. package/dist/bin/exe-assign.js +1003 -854
  8. package/dist/bin/exe-boot.js +1257 -320
  9. package/dist/bin/exe-call.js +10 -0
  10. package/dist/bin/exe-cloud.js +29 -6
  11. package/dist/bin/exe-dispatch.js +210 -34
  12. package/dist/bin/exe-doctor.js +403 -6
  13. package/dist/bin/exe-export-behaviors.js +175 -72
  14. package/dist/bin/exe-forget.js +97 -2
  15. package/dist/bin/exe-gateway.js +550 -171
  16. package/dist/bin/exe-healthcheck.js +1 -0
  17. package/dist/bin/exe-heartbeat.js +100 -5
  18. package/dist/bin/exe-kill.js +175 -72
  19. package/dist/bin/exe-launch-agent.js +189 -76
  20. package/dist/bin/exe-link.js +902 -80
  21. package/dist/bin/exe-new-employee.js +38 -8
  22. package/dist/bin/exe-pending-messages.js +96 -2
  23. package/dist/bin/exe-pending-notifications.js +97 -2
  24. package/dist/bin/exe-pending-reviews.js +98 -3
  25. package/dist/bin/exe-rename.js +564 -23
  26. package/dist/bin/exe-review.js +231 -73
  27. package/dist/bin/exe-search.js +989 -226
  28. package/dist/bin/exe-session-cleanup.js +4806 -1665
  29. package/dist/bin/exe-settings.js +20 -5
  30. package/dist/bin/exe-status.js +97 -2
  31. package/dist/bin/exe-team.js +97 -2
  32. package/dist/bin/git-sweep.js +899 -207
  33. package/dist/bin/graph-backfill.js +175 -72
  34. package/dist/bin/graph-export.js +175 -72
  35. package/dist/bin/install.js +38 -7
  36. package/dist/bin/list-providers.js +1 -0
  37. package/dist/bin/scan-tasks.js +904 -211
  38. package/dist/bin/setup.js +867 -268
  39. package/dist/bin/shard-migrate.js +175 -72
  40. package/dist/bin/update.js +1 -0
  41. package/dist/bin/wiki-sync.js +175 -72
  42. package/dist/gateway/index.js +548 -166
  43. package/dist/hooks/bug-report-worker.js +208 -23
  44. package/dist/hooks/commit-complete.js +897 -205
  45. package/dist/hooks/error-recall.js +988 -226
  46. package/dist/hooks/ingest-worker.js +1638 -1194
  47. package/dist/hooks/ingest.js +3 -0
  48. package/dist/hooks/instructions-loaded.js +707 -97
  49. package/dist/hooks/notification.js +699 -89
  50. package/dist/hooks/post-compact.js +714 -104
  51. package/dist/hooks/pre-compact.js +897 -205
  52. package/dist/hooks/pre-tool-use.js +742 -123
  53. package/dist/hooks/prompt-ingest-worker.js +242 -101
  54. package/dist/hooks/prompt-submit.js +995 -233
  55. package/dist/hooks/response-ingest-worker.js +242 -101
  56. package/dist/hooks/session-end.js +3941 -400
  57. package/dist/hooks/session-start.js +1001 -226
  58. package/dist/hooks/stop.js +725 -115
  59. package/dist/hooks/subagent-stop.js +714 -104
  60. package/dist/hooks/summary-worker.js +1964 -1330
  61. package/dist/index.js +1651 -1053
  62. package/dist/lib/cloud-sync.js +907 -86
  63. package/dist/lib/consolidation.js +2 -1
  64. package/dist/lib/database.js +642 -87
  65. package/dist/lib/db-daemon-client.js +503 -0
  66. package/dist/lib/device-registry.js +547 -7
  67. package/dist/lib/embedder.js +14 -28
  68. package/dist/lib/employee-templates.js +84 -74
  69. package/dist/lib/employees.js +9 -0
  70. package/dist/lib/exe-daemon-client.js +16 -29
  71. package/dist/lib/exe-daemon.js +1955 -922
  72. package/dist/lib/hybrid-search.js +988 -226
  73. package/dist/lib/identity.js +87 -67
  74. package/dist/lib/keychain.js +9 -1
  75. package/dist/lib/messaging.js +8 -1
  76. package/dist/lib/reminders.js +91 -74
  77. package/dist/lib/schedules.js +96 -2
  78. package/dist/lib/skill-learning.js +103 -85
  79. package/dist/lib/store.js +234 -73
  80. package/dist/lib/tasks.js +111 -22
  81. package/dist/lib/tmux-routing.js +120 -31
  82. package/dist/lib/token-spend.js +273 -0
  83. package/dist/lib/ws-client.js +11 -0
  84. package/dist/mcp/server.js +5222 -475
  85. package/dist/mcp/tools/complete-reminder.js +94 -77
  86. package/dist/mcp/tools/create-reminder.js +94 -77
  87. package/dist/mcp/tools/create-task.js +120 -22
  88. package/dist/mcp/tools/deactivate-behavior.js +95 -77
  89. package/dist/mcp/tools/list-reminders.js +94 -77
  90. package/dist/mcp/tools/list-tasks.js +31 -1
  91. package/dist/mcp/tools/send-message.js +8 -1
  92. package/dist/mcp/tools/update-task.js +39 -10
  93. package/dist/runtime/index.js +911 -219
  94. package/dist/tui/App.js +997 -295
  95. package/package.json +6 -1
@@ -183,12 +183,12 @@ __export(config_exports, {
183
183
  import { readFile, writeFile, mkdir, chmod } from "fs/promises";
184
184
  import { readFileSync as readFileSync2, existsSync as existsSync2, renameSync } from "fs";
185
185
  import path3 from "path";
186
- import os from "os";
186
+ import os2 from "os";
187
187
  function resolveDataDir() {
188
188
  if (process.env.EXE_OS_DIR) return process.env.EXE_OS_DIR;
189
189
  if (process.env.EXE_MEM_DIR) return process.env.EXE_MEM_DIR;
190
- const newDir = path3.join(os.homedir(), ".exe-os");
191
- const legacyDir = path3.join(os.homedir(), ".exe-mem");
190
+ const newDir = path3.join(os2.homedir(), ".exe-os");
191
+ const legacyDir = path3.join(os2.homedir(), ".exe-mem");
192
192
  if (!existsSync2(newDir) && existsSync2(legacyDir)) {
193
193
  try {
194
194
  renameSync(legacyDir, newDir);
@@ -275,7 +275,7 @@ async function loadConfig() {
275
275
  normalizeAutoUpdate(migratedCfg);
276
276
  const config = { ...DEFAULT_CONFIG, dbPath: path3.join(dir, "memories.db"), ...migratedCfg };
277
277
  if (config.dbPath.startsWith("~")) {
278
- config.dbPath = config.dbPath.replace(/^~/, os.homedir());
278
+ config.dbPath = config.dbPath.replace(/^~/, os2.homedir());
279
279
  }
280
280
  return config;
281
281
  } catch {
@@ -409,7 +409,7 @@ import { readFile as readFile2, writeFile as writeFile2, mkdir as mkdir2 } from
409
409
  import { existsSync as existsSync3, symlinkSync, readlinkSync, readFileSync as readFileSync3, renameSync as renameSync2, unlinkSync, writeFileSync } from "fs";
410
410
  import { execSync as execSync3 } from "child_process";
411
411
  import path4 from "path";
412
- import os2 from "os";
412
+ import os3 from "os";
413
413
  function normalizeRole(role) {
414
414
  return (role ?? "").trim().toLowerCase();
415
415
  }
@@ -458,389 +458,942 @@ var init_employees = __esm({
458
458
  }
459
459
  });
460
460
 
461
- // src/lib/database.ts
462
- var database_exports = {};
463
- __export(database_exports, {
464
- disposeDatabase: () => disposeDatabase,
465
- disposeTurso: () => disposeTurso,
466
- ensureSchema: () => ensureSchema,
467
- getClient: () => getClient,
468
- getRawClient: () => getRawClient,
469
- initDatabase: () => initDatabase,
470
- initTurso: () => initTurso,
471
- isInitialized: () => isInitialized
472
- });
473
- import { createClient } from "@libsql/client";
474
- async function initDatabase(config) {
475
- if (_client) {
476
- _client.close();
477
- _client = null;
478
- _resilientClient = null;
461
+ // src/lib/exe-daemon-client.ts
462
+ import net from "net";
463
+ import { spawn } from "child_process";
464
+ import { randomUUID } from "crypto";
465
+ import { existsSync as existsSync4, unlinkSync as unlinkSync2, readFileSync as readFileSync4, openSync, closeSync, statSync as statSync2 } from "fs";
466
+ import path5 from "path";
467
+ import { fileURLToPath } from "url";
468
+ function handleData(chunk) {
469
+ _buffer += chunk.toString();
470
+ if (_buffer.length > MAX_BUFFER) {
471
+ _buffer = "";
472
+ return;
479
473
  }
480
- const opts = {
481
- url: `file:${config.dbPath}`
482
- };
483
- if (config.encryptionKey) {
484
- opts.encryptionKey = config.encryptionKey;
474
+ let newlineIdx;
475
+ while ((newlineIdx = _buffer.indexOf("\n")) !== -1) {
476
+ const line = _buffer.slice(0, newlineIdx).trim();
477
+ _buffer = _buffer.slice(newlineIdx + 1);
478
+ if (!line) continue;
479
+ try {
480
+ const response = JSON.parse(line);
481
+ const id = response.id;
482
+ if (!id) continue;
483
+ const entry = _pending.get(id);
484
+ if (entry) {
485
+ clearTimeout(entry.timer);
486
+ _pending.delete(id);
487
+ entry.resolve(response);
488
+ }
489
+ } catch {
490
+ }
485
491
  }
486
- _client = createClient(opts);
487
- _resilientClient = wrapWithRetry(_client);
488
- }
489
- function isInitialized() {
490
- return _client !== null;
491
492
  }
492
- function getClient() {
493
- if (!_resilientClient) {
494
- throw new Error("Database client not initialized. Call initDatabase() first.");
493
+ function cleanupStaleFiles() {
494
+ if (existsSync4(PID_PATH)) {
495
+ try {
496
+ const pid = parseInt(readFileSync4(PID_PATH, "utf8").trim(), 10);
497
+ if (pid > 0) {
498
+ try {
499
+ process.kill(pid, 0);
500
+ return;
501
+ } catch {
502
+ }
503
+ }
504
+ } catch {
505
+ }
506
+ try {
507
+ unlinkSync2(PID_PATH);
508
+ } catch {
509
+ }
510
+ try {
511
+ unlinkSync2(SOCKET_PATH);
512
+ } catch {
513
+ }
495
514
  }
496
- return _resilientClient;
497
515
  }
498
- function getRawClient() {
499
- if (!_client) {
500
- throw new Error("Database client not initialized. Call initDatabase() first.");
516
+ function findPackageRoot() {
517
+ let dir = path5.dirname(fileURLToPath(import.meta.url));
518
+ const { root } = path5.parse(dir);
519
+ while (dir !== root) {
520
+ if (existsSync4(path5.join(dir, "package.json"))) return dir;
521
+ dir = path5.dirname(dir);
501
522
  }
502
- return _client;
523
+ return null;
503
524
  }
504
- async function ensureSchema() {
505
- const client = getRawClient();
506
- await client.execute("PRAGMA journal_mode = WAL");
507
- await client.execute("PRAGMA busy_timeout = 30000");
508
- await client.execute("PRAGMA wal_autocheckpoint = 1000");
509
- try {
510
- await client.execute("PRAGMA libsql_vector_search_ef = 128");
511
- } catch {
525
+ function spawnDaemon() {
526
+ const pkgRoot = findPackageRoot();
527
+ if (!pkgRoot) {
528
+ process.stderr.write("[exed-client] WARN: cannot find package root\n");
529
+ return;
512
530
  }
513
- await client.executeMultiple(`
514
- CREATE TABLE IF NOT EXISTS memories (
515
- id TEXT PRIMARY KEY,
516
- agent_id TEXT NOT NULL,
517
- agent_role TEXT NOT NULL,
518
- session_id TEXT NOT NULL,
519
- timestamp TEXT NOT NULL,
520
- tool_name TEXT NOT NULL,
521
- project_name TEXT NOT NULL,
522
- has_error INTEGER NOT NULL DEFAULT 0,
523
- raw_text TEXT NOT NULL,
524
- vector F32_BLOB(1024),
525
- version INTEGER NOT NULL DEFAULT 0
526
- );
527
-
528
- CREATE INDEX IF NOT EXISTS idx_memories_agent
529
- ON memories(agent_id);
530
-
531
- CREATE INDEX IF NOT EXISTS idx_memories_timestamp
532
- ON memories(timestamp);
533
-
534
- CREATE INDEX IF NOT EXISTS idx_memories_session
535
- ON memories(session_id);
536
-
537
- CREATE INDEX IF NOT EXISTS idx_memories_project
538
- ON memories(project_name);
539
-
540
- CREATE INDEX IF NOT EXISTS idx_memories_tool
541
- ON memories(tool_name);
542
-
543
- CREATE INDEX IF NOT EXISTS idx_memories_version
544
- ON memories(version);
545
-
546
- CREATE INDEX IF NOT EXISTS idx_memories_agent_project
547
- ON memories(agent_id, project_name);
548
- `);
549
- await client.executeMultiple(`
550
- CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts USING fts5(
551
- raw_text,
552
- content='memories',
553
- content_rowid='rowid'
554
- );
555
-
556
- CREATE TRIGGER IF NOT EXISTS memories_fts_ai AFTER INSERT ON memories BEGIN
557
- INSERT INTO memories_fts(rowid, raw_text) VALUES (new.rowid, new.raw_text);
558
- END;
559
-
560
- CREATE TRIGGER IF NOT EXISTS memories_fts_ad AFTER DELETE ON memories BEGIN
561
- INSERT INTO memories_fts(memories_fts, rowid, raw_text) VALUES('delete', old.rowid, old.raw_text);
562
- END;
563
-
564
- CREATE TRIGGER IF NOT EXISTS memories_fts_au AFTER UPDATE ON memories BEGIN
565
- INSERT INTO memories_fts(memories_fts, rowid, raw_text) VALUES('delete', old.rowid, old.raw_text);
566
- INSERT INTO memories_fts(rowid, raw_text) VALUES (new.rowid, new.raw_text);
567
- END;
568
- `);
569
- await client.executeMultiple(`
570
- CREATE TABLE IF NOT EXISTS sync_meta (
571
- key TEXT PRIMARY KEY,
572
- value TEXT NOT NULL
573
- );
574
- `);
575
- await client.executeMultiple(`
576
- CREATE TABLE IF NOT EXISTS tasks (
577
- id TEXT PRIMARY KEY,
578
- title TEXT NOT NULL,
579
- assigned_to TEXT NOT NULL,
580
- assigned_by TEXT NOT NULL,
581
- project_name TEXT NOT NULL,
582
- priority TEXT NOT NULL DEFAULT 'p1',
583
- status TEXT NOT NULL DEFAULT 'open',
584
- task_file TEXT,
585
- created_at TEXT NOT NULL,
586
- updated_at TEXT NOT NULL
587
- );
588
-
589
- CREATE INDEX IF NOT EXISTS idx_tasks_assignee_status
590
- ON tasks(assigned_to, status);
591
- `);
592
- await client.executeMultiple(`
593
- CREATE TABLE IF NOT EXISTS behaviors (
594
- id TEXT PRIMARY KEY,
595
- agent_id TEXT NOT NULL,
596
- project_name TEXT,
597
- domain TEXT,
598
- content TEXT NOT NULL,
599
- active INTEGER NOT NULL DEFAULT 1,
600
- created_at TEXT NOT NULL,
601
- updated_at TEXT NOT NULL
602
- );
603
-
604
- CREATE INDEX IF NOT EXISTS idx_behaviors_agent
605
- ON behaviors(agent_id, active);
606
- `);
607
- try {
608
- const coordinatorName = getCoordinatorName();
609
- const existing = await client.execute({
610
- sql: "SELECT COUNT(*) as cnt FROM behaviors WHERE agent_id = ?",
611
- args: [coordinatorName]
612
- });
613
- if (Number(existing.rows[0]?.cnt) === 0) {
614
- const seededAt = "2026-03-25T00:00:00Z";
615
- for (const [domain, content] of [
616
- ["workflow", `Don't ask "keep going?" \u2014 just keep executing phases/plans autonomously`],
617
- ["tool-use", "Always use create_task MCP tool, never write .md files directly for task creation"],
618
- ["workflow", "Auto-start reviewing when idle and reviews are pending \u2014 never ask founder for permission"]
619
- ]) {
620
- await client.execute({
621
- sql: `INSERT INTO behaviors (id, agent_id, project_name, domain, content, active, created_at, updated_at)
622
- VALUES (hex(randomblob(16)), ?, NULL, ?, ?, 1, ?, ?)`,
623
- args: [coordinatorName, domain, content, seededAt, seededAt]
624
- });
625
- }
626
- }
627
- } catch {
531
+ const daemonPath = path5.join(pkgRoot, "dist", "lib", "exe-daemon.js");
532
+ if (!existsSync4(daemonPath)) {
533
+ process.stderr.write(`[exed-client] WARN: daemon script not found at ${daemonPath}
534
+ `);
535
+ return;
628
536
  }
537
+ const resolvedPath = daemonPath;
538
+ process.stderr.write(`[exed-client] Spawning daemon: ${resolvedPath}
539
+ `);
540
+ const logPath = path5.join(path5.dirname(SOCKET_PATH), "exed.log");
541
+ let stderrFd = "ignore";
629
542
  try {
630
- await client.execute({
631
- sql: `ALTER TABLE behaviors ADD COLUMN priority TEXT DEFAULT 'p1'`,
632
- args: []
633
- });
543
+ stderrFd = openSync(logPath, "a");
634
544
  } catch {
635
545
  }
636
- try {
637
- await client.execute({
638
- sql: `ALTER TABLE tasks ADD COLUMN blocked_by TEXT`,
639
- args: []
640
- });
641
- } catch {
546
+ const child = spawn(process.execPath, [resolvedPath], {
547
+ detached: true,
548
+ stdio: ["ignore", "ignore", stderrFd],
549
+ env: {
550
+ ...process.env,
551
+ TMUX: void 0,
552
+ // Daemon is global — must not inherit session scope
553
+ TMUX_PANE: void 0,
554
+ // Prevents resolveExeSession() from scoping to one session
555
+ EXE_DAEMON_SOCK: SOCKET_PATH,
556
+ EXE_DAEMON_PID: PID_PATH
557
+ }
558
+ });
559
+ child.unref();
560
+ if (typeof stderrFd === "number") {
561
+ try {
562
+ closeSync(stderrFd);
563
+ } catch {
564
+ }
642
565
  }
566
+ }
567
+ function acquireSpawnLock() {
643
568
  try {
644
- await client.execute({
645
- sql: `ALTER TABLE tasks ADD COLUMN parent_task_id TEXT`,
646
- args: []
647
- });
648
- } catch {
649
- }
650
- try {
651
- await client.execute({
652
- sql: `CREATE INDEX IF NOT EXISTS idx_tasks_parent_task_id
653
- ON tasks(parent_task_id)
654
- WHERE parent_task_id IS NOT NULL`,
655
- args: []
656
- });
569
+ const fd = openSync(SPAWN_LOCK_PATH, "wx");
570
+ closeSync(fd);
571
+ return true;
657
572
  } catch {
573
+ try {
574
+ const stat = statSync2(SPAWN_LOCK_PATH);
575
+ if (Date.now() - stat.mtimeMs > SPAWN_LOCK_STALE_MS) {
576
+ try {
577
+ unlinkSync2(SPAWN_LOCK_PATH);
578
+ } catch {
579
+ }
580
+ try {
581
+ const fd = openSync(SPAWN_LOCK_PATH, "wx");
582
+ closeSync(fd);
583
+ return true;
584
+ } catch {
585
+ }
586
+ }
587
+ } catch {
588
+ }
589
+ return false;
658
590
  }
591
+ }
592
+ function releaseSpawnLock() {
659
593
  try {
660
- await client.execute({
661
- sql: `UPDATE tasks SET status = 'done' WHERE status = 'completed'`,
662
- args: []
663
- });
594
+ unlinkSync2(SPAWN_LOCK_PATH);
664
595
  } catch {
665
596
  }
666
- try {
667
- await client.execute({
668
- sql: `ALTER TABLE tasks ADD COLUMN reviewer TEXT`,
669
- args: []
597
+ }
598
+ function connectToSocket() {
599
+ return new Promise((resolve) => {
600
+ if (_socket && _connected) {
601
+ resolve(true);
602
+ return;
603
+ }
604
+ const socket = net.createConnection({ path: SOCKET_PATH });
605
+ const connectTimeout = setTimeout(() => {
606
+ socket.destroy();
607
+ resolve(false);
608
+ }, 2e3);
609
+ socket.on("connect", () => {
610
+ clearTimeout(connectTimeout);
611
+ _socket = socket;
612
+ _connected = true;
613
+ _buffer = "";
614
+ socket.on("data", handleData);
615
+ socket.on("close", () => {
616
+ _connected = false;
617
+ _socket = null;
618
+ for (const [id, entry] of _pending) {
619
+ clearTimeout(entry.timer);
620
+ _pending.delete(id);
621
+ entry.resolve({ error: "Connection closed" });
622
+ }
623
+ });
624
+ socket.on("error", () => {
625
+ _connected = false;
626
+ _socket = null;
627
+ });
628
+ resolve(true);
670
629
  });
671
- } catch {
672
- }
673
- try {
674
- await client.execute({
675
- sql: `ALTER TABLE tasks ADD COLUMN context TEXT`,
676
- args: []
630
+ socket.on("error", () => {
631
+ clearTimeout(connectTimeout);
632
+ resolve(false);
677
633
  });
678
- } catch {
634
+ });
635
+ }
636
+ async function connectEmbedDaemon() {
637
+ if (_socket && _connected) return true;
638
+ if (await connectToSocket()) return true;
639
+ if (acquireSpawnLock()) {
640
+ try {
641
+ cleanupStaleFiles();
642
+ spawnDaemon();
643
+ } finally {
644
+ releaseSpawnLock();
645
+ }
679
646
  }
680
- try {
681
- await client.execute({
682
- sql: `ALTER TABLE tasks ADD COLUMN result TEXT`,
683
- args: []
684
- });
685
- } catch {
647
+ const start = Date.now();
648
+ let delay2 = 100;
649
+ while (Date.now() - start < CONNECT_TIMEOUT_MS) {
650
+ await new Promise((r) => setTimeout(r, delay2));
651
+ if (await connectToSocket()) return true;
652
+ delay2 = Math.min(delay2 * 2, 3e3);
686
653
  }
687
- try {
688
- await client.execute({
689
- sql: `ALTER TABLE tasks ADD COLUMN assigned_tmux TEXT`,
690
- args: []
691
- });
692
- } catch {
654
+ return false;
655
+ }
656
+ function sendRequest(texts, priority) {
657
+ return sendDaemonRequest({ texts, priority });
658
+ }
659
+ function sendDaemonRequest(payload, timeoutMs = REQUEST_TIMEOUT_MS) {
660
+ return new Promise((resolve) => {
661
+ if (!_socket || !_connected) {
662
+ resolve({ error: "Not connected" });
663
+ return;
664
+ }
665
+ const id = randomUUID();
666
+ const timer = setTimeout(() => {
667
+ _pending.delete(id);
668
+ resolve({ error: "Request timeout" });
669
+ }, timeoutMs);
670
+ _pending.set(id, { resolve, timer });
671
+ try {
672
+ _socket.write(JSON.stringify({ id, ...payload }) + "\n");
673
+ } catch {
674
+ clearTimeout(timer);
675
+ _pending.delete(id);
676
+ resolve({ error: "Write failed" });
677
+ }
678
+ });
679
+ }
680
+ async function pingDaemon() {
681
+ if (!_socket || !_connected) return null;
682
+ const response = await sendDaemonRequest({ type: "health" }, 5e3);
683
+ if (response.health) {
684
+ return response.health;
693
685
  }
694
- try {
695
- await client.execute({
696
- sql: `ALTER TABLE tasks ADD COLUMN checkpoint TEXT`,
697
- args: []
698
- });
699
- } catch {
686
+ return null;
687
+ }
688
+ function killAndRespawnDaemon() {
689
+ process.stderr.write("[exed-client] Killing daemon for restart...\n");
690
+ if (existsSync4(PID_PATH)) {
691
+ try {
692
+ const pid = parseInt(readFileSync4(PID_PATH, "utf8").trim(), 10);
693
+ if (pid > 0) {
694
+ try {
695
+ process.kill(pid, "SIGKILL");
696
+ } catch {
697
+ }
698
+ }
699
+ } catch {
700
+ }
700
701
  }
701
- try {
702
- await client.execute({
703
- sql: `ALTER TABLE tasks ADD COLUMN checkpoint_count INTEGER NOT NULL DEFAULT 0`,
704
- args: []
705
- });
706
- } catch {
702
+ if (_socket) {
703
+ _socket.destroy();
704
+ _socket = null;
707
705
  }
706
+ _connected = false;
707
+ _buffer = "";
708
708
  try {
709
- await client.execute({
710
- sql: `ALTER TABLE tasks ADD COLUMN complexity TEXT NOT NULL DEFAULT 'standard'`,
711
- args: []
712
- });
709
+ unlinkSync2(PID_PATH);
713
710
  } catch {
714
711
  }
715
712
  try {
716
- await client.execute({
717
- sql: `ALTER TABLE tasks ADD COLUMN session_scope TEXT`,
718
- args: []
719
- });
713
+ unlinkSync2(SOCKET_PATH);
720
714
  } catch {
721
715
  }
722
- try {
723
- await client.execute({
724
- sql: `ALTER TABLE memories ADD COLUMN task_id TEXT`,
725
- args: []
726
- });
727
- } catch {
716
+ spawnDaemon();
717
+ }
718
+ async function embedViaClient(text, priority = "high") {
719
+ if (!_connected && !await connectEmbedDaemon()) return null;
720
+ _requestCount++;
721
+ if (_requestCount % HEALTH_CHECK_INTERVAL === 0) {
722
+ const health = await pingDaemon();
723
+ if (!health) {
724
+ process.stderr.write(`[exed-client] Periodic health check failed at request ${_requestCount} \u2014 restarting daemon
725
+ `);
726
+ killAndRespawnDaemon();
727
+ const start = Date.now();
728
+ let delay2 = 200;
729
+ while (Date.now() - start < CONNECT_TIMEOUT_MS) {
730
+ await new Promise((r) => setTimeout(r, delay2));
731
+ if (await connectToSocket()) break;
732
+ delay2 = Math.min(delay2 * 2, 3e3);
733
+ }
734
+ if (!_connected) return null;
735
+ }
728
736
  }
729
- try {
730
- await client.execute({
731
- sql: `ALTER TABLE memories ADD COLUMN consolidated INTEGER NOT NULL DEFAULT 0`,
732
- args: []
733
- });
734
- } catch {
737
+ const result = await sendRequest([text], priority);
738
+ if (!result.error && result.vectors?.[0]) return result.vectors[0];
739
+ if (result.error) {
740
+ process.stderr.write(`[exed-client] Embed failed (${result.error}) \u2014 attempting restart
741
+ `);
742
+ killAndRespawnDaemon();
743
+ const start = Date.now();
744
+ let delay2 = 200;
745
+ while (Date.now() - start < CONNECT_TIMEOUT_MS) {
746
+ await new Promise((r) => setTimeout(r, delay2));
747
+ if (await connectToSocket()) break;
748
+ delay2 = Math.min(delay2 * 2, 3e3);
749
+ }
750
+ if (!_connected) return null;
751
+ const retry = await sendRequest([text], priority);
752
+ if (!retry.error && retry.vectors?.[0]) return retry.vectors[0];
753
+ process.stderr.write(`[exed-client] Embed retry also failed: ${retry.error ?? "no vector"}
754
+ `);
735
755
  }
736
- try {
737
- await client.execute({
738
- sql: `ALTER TABLE memories ADD COLUMN author_device_id TEXT`,
739
- args: []
740
- });
741
- } catch {
756
+ return null;
757
+ }
758
+ function disconnectClient() {
759
+ if (_socket) {
760
+ _socket.destroy();
761
+ _socket = null;
742
762
  }
743
- try {
744
- await client.execute({
745
- sql: `ALTER TABLE memories ADD COLUMN scope TEXT NOT NULL DEFAULT 'business'`,
746
- args: []
747
- });
748
- } catch {
763
+ _connected = false;
764
+ _buffer = "";
765
+ for (const [id, entry] of _pending) {
766
+ clearTimeout(entry.timer);
767
+ _pending.delete(id);
768
+ entry.resolve({ error: "Client disconnected" });
769
+ }
770
+ }
771
+ function isClientConnected() {
772
+ return _connected;
773
+ }
774
+ 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;
775
+ var init_exe_daemon_client = __esm({
776
+ "src/lib/exe-daemon-client.ts"() {
777
+ "use strict";
778
+ init_config();
779
+ SOCKET_PATH = process.env.EXE_DAEMON_SOCK ?? process.env.EXE_EMBED_SOCK ?? path5.join(EXE_AI_DIR, "exed.sock");
780
+ PID_PATH = process.env.EXE_DAEMON_PID ?? process.env.EXE_EMBED_PID ?? path5.join(EXE_AI_DIR, "exed.pid");
781
+ SPAWN_LOCK_PATH = path5.join(EXE_AI_DIR, "exed-spawn.lock");
782
+ SPAWN_LOCK_STALE_MS = 3e4;
783
+ CONNECT_TIMEOUT_MS = 15e3;
784
+ REQUEST_TIMEOUT_MS = 3e4;
785
+ _socket = null;
786
+ _connected = false;
787
+ _buffer = "";
788
+ _requestCount = 0;
789
+ HEALTH_CHECK_INTERVAL = 100;
790
+ _pending = /* @__PURE__ */ new Map();
791
+ MAX_BUFFER = 1e7;
792
+ }
793
+ });
794
+
795
+ // src/lib/daemon-protocol.ts
796
+ function serializeValue(v) {
797
+ if (v === null || v === void 0) return null;
798
+ if (typeof v === "bigint") return Number(v);
799
+ if (typeof v === "boolean") return v ? 1 : 0;
800
+ if (v instanceof Uint8Array) {
801
+ return { __blob: Buffer.from(v).toString("base64") };
802
+ }
803
+ if (ArrayBuffer.isView(v)) {
804
+ return { __blob: Buffer.from(v.buffer, v.byteOffset, v.byteLength).toString("base64") };
805
+ }
806
+ if (v instanceof ArrayBuffer) {
807
+ return { __blob: Buffer.from(v).toString("base64") };
808
+ }
809
+ if (typeof v === "string" || typeof v === "number") return v;
810
+ return String(v);
811
+ }
812
+ function deserializeValue(v) {
813
+ if (v === null) return null;
814
+ if (typeof v === "object" && v !== null && "__blob" in v) {
815
+ const buf = Buffer.from(v.__blob, "base64");
816
+ return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength);
817
+ }
818
+ return v;
819
+ }
820
+ function deserializeResultSet(srs) {
821
+ const rows = srs.rows.map((obj) => {
822
+ const values = srs.columns.map(
823
+ (col) => deserializeValue(obj[col] ?? null)
824
+ );
825
+ const row = values;
826
+ for (let i = 0; i < srs.columns.length; i++) {
827
+ const col = srs.columns[i];
828
+ if (col !== void 0) {
829
+ row[col] = values[i] ?? null;
830
+ }
831
+ }
832
+ Object.defineProperty(row, "length", {
833
+ value: values.length,
834
+ enumerable: false
835
+ });
836
+ return row;
837
+ });
838
+ return {
839
+ columns: srs.columns,
840
+ columnTypes: srs.columnTypes ?? [],
841
+ rows,
842
+ rowsAffected: srs.rowsAffected,
843
+ lastInsertRowid: srs.lastInsertRowid != null ? BigInt(srs.lastInsertRowid) : void 0,
844
+ toJSON: () => ({
845
+ columns: srs.columns,
846
+ columnTypes: srs.columnTypes ?? [],
847
+ rows: srs.rows,
848
+ rowsAffected: srs.rowsAffected,
849
+ lastInsertRowid: srs.lastInsertRowid
850
+ })
851
+ };
852
+ }
853
+ var init_daemon_protocol = __esm({
854
+ "src/lib/daemon-protocol.ts"() {
855
+ "use strict";
856
+ }
857
+ });
858
+
859
+ // src/lib/db-daemon-client.ts
860
+ var db_daemon_client_exports = {};
861
+ __export(db_daemon_client_exports, {
862
+ createDaemonDbClient: () => createDaemonDbClient,
863
+ initDaemonDbClient: () => initDaemonDbClient
864
+ });
865
+ function normalizeStatement(stmt) {
866
+ if (typeof stmt === "string") {
867
+ return { sql: stmt, args: [] };
868
+ }
869
+ const sql = stmt.sql;
870
+ let args = [];
871
+ if (Array.isArray(stmt.args)) {
872
+ args = stmt.args.map((v) => serializeValue(v));
873
+ } else if (stmt.args && typeof stmt.args === "object") {
874
+ const named = {};
875
+ for (const [key, val] of Object.entries(stmt.args)) {
876
+ named[key] = serializeValue(val);
877
+ }
878
+ return { sql, args: named };
879
+ }
880
+ return { sql, args };
881
+ }
882
+ function createDaemonDbClient(fallbackClient) {
883
+ let _useDaemon = false;
884
+ const client = {
885
+ async execute(stmt) {
886
+ if (!_useDaemon || !isClientConnected()) {
887
+ return fallbackClient.execute(stmt);
888
+ }
889
+ const { sql, args } = normalizeStatement(stmt);
890
+ const response = await sendDaemonRequest({
891
+ type: "db-execute",
892
+ sql,
893
+ args
894
+ });
895
+ if (response.error) {
896
+ const errMsg = String(response.error);
897
+ if (errMsg === "Not connected" || errMsg === "Request timeout" || errMsg === "Write failed") {
898
+ process.stderr.write(`[db-daemon] Transport error (${errMsg}), falling back to direct
899
+ `);
900
+ return fallbackClient.execute(stmt);
901
+ }
902
+ throw new Error(errMsg);
903
+ }
904
+ if (response.db) {
905
+ return deserializeResultSet(response.db);
906
+ }
907
+ process.stderr.write("[db-daemon] Unexpected response shape, falling back to direct\n");
908
+ return fallbackClient.execute(stmt);
909
+ },
910
+ async batch(stmts, mode) {
911
+ if (!_useDaemon || !isClientConnected()) {
912
+ return fallbackClient.batch(stmts, mode);
913
+ }
914
+ const statements = stmts.map(normalizeStatement);
915
+ const response = await sendDaemonRequest({
916
+ type: "db-batch",
917
+ statements,
918
+ mode: mode ?? "deferred"
919
+ });
920
+ if (response.error) {
921
+ const errMsg = String(response.error);
922
+ if (errMsg === "Not connected" || errMsg === "Request timeout" || errMsg === "Write failed") {
923
+ process.stderr.write(`[db-daemon] Batch transport error (${errMsg}), falling back to direct
924
+ `);
925
+ return fallbackClient.batch(stmts, mode);
926
+ }
927
+ throw new Error(errMsg);
928
+ }
929
+ const batchResults = response["db-batch"];
930
+ if (batchResults) {
931
+ return batchResults.map(deserializeResultSet);
932
+ }
933
+ process.stderr.write("[db-daemon] Unexpected batch response shape, falling back to direct\n");
934
+ return fallbackClient.batch(stmts, mode);
935
+ },
936
+ // Transaction support — delegate to fallback (transactions need direct connection)
937
+ async transaction(mode) {
938
+ return fallbackClient.transaction(mode);
939
+ },
940
+ // executeMultiple — delegate to fallback (used only for schema migrations)
941
+ async executeMultiple(sql) {
942
+ return fallbackClient.executeMultiple(sql);
943
+ },
944
+ // migrate — delegate to fallback
945
+ async migrate(stmts) {
946
+ return fallbackClient.migrate(stmts);
947
+ },
948
+ // Sync mode — delegate to fallback
949
+ sync() {
950
+ return fallbackClient.sync();
951
+ },
952
+ close() {
953
+ _useDaemon = false;
954
+ },
955
+ get closed() {
956
+ return fallbackClient.closed;
957
+ },
958
+ get protocol() {
959
+ return fallbackClient.protocol;
960
+ }
961
+ };
962
+ return {
963
+ ...client,
964
+ /** Enable daemon routing (call after confirming daemon is connected) */
965
+ _enableDaemon() {
966
+ _useDaemon = true;
967
+ },
968
+ /** Check if daemon routing is active */
969
+ _isDaemonActive() {
970
+ return _useDaemon && isClientConnected();
971
+ }
972
+ };
973
+ }
974
+ async function initDaemonDbClient(fallbackClient) {
975
+ if (process.env.EXE_IS_DAEMON === "1") return null;
976
+ const connected = await connectEmbedDaemon();
977
+ if (!connected) {
978
+ process.stderr.write("[db-daemon] Daemon unavailable \u2014 using direct SQLite\n");
979
+ return null;
980
+ }
981
+ const client = createDaemonDbClient(fallbackClient);
982
+ client._enableDaemon();
983
+ process.stderr.write("[db-daemon] DB routing through daemon (single-writer)\n");
984
+ return client;
985
+ }
986
+ var init_db_daemon_client = __esm({
987
+ "src/lib/db-daemon-client.ts"() {
988
+ "use strict";
989
+ init_exe_daemon_client();
990
+ init_daemon_protocol();
991
+ }
992
+ });
993
+
994
+ // src/lib/database.ts
995
+ var database_exports = {};
996
+ __export(database_exports, {
997
+ disposeDatabase: () => disposeDatabase,
998
+ disposeTurso: () => disposeTurso,
999
+ ensureSchema: () => ensureSchema,
1000
+ getClient: () => getClient,
1001
+ getRawClient: () => getRawClient,
1002
+ initDaemonClient: () => initDaemonClient,
1003
+ initDatabase: () => initDatabase,
1004
+ initTurso: () => initTurso,
1005
+ isInitialized: () => isInitialized
1006
+ });
1007
+ import { createClient } from "@libsql/client";
1008
+ async function initDatabase(config) {
1009
+ if (_client) {
1010
+ _client.close();
1011
+ _client = null;
1012
+ _resilientClient = null;
1013
+ }
1014
+ const opts = {
1015
+ url: `file:${config.dbPath}`
1016
+ };
1017
+ if (config.encryptionKey) {
1018
+ opts.encryptionKey = config.encryptionKey;
1019
+ }
1020
+ _client = createClient(opts);
1021
+ _resilientClient = wrapWithRetry(_client);
1022
+ }
1023
+ function isInitialized() {
1024
+ return _client !== null;
1025
+ }
1026
+ function getClient() {
1027
+ if (!_resilientClient) {
1028
+ throw new Error("Database client not initialized. Call initDatabase() first.");
1029
+ }
1030
+ if (process.env.EXE_IS_DAEMON === "1") {
1031
+ return _resilientClient;
1032
+ }
1033
+ if (_daemonClient && _daemonClient._isDaemonActive()) {
1034
+ return _daemonClient;
1035
+ }
1036
+ return _resilientClient;
1037
+ }
1038
+ async function initDaemonClient() {
1039
+ if (process.env.EXE_IS_DAEMON === "1") return;
1040
+ if (!_resilientClient) return;
1041
+ try {
1042
+ const { initDaemonDbClient: initDaemonDbClient2 } = await Promise.resolve().then(() => (init_db_daemon_client(), db_daemon_client_exports));
1043
+ _daemonClient = await initDaemonDbClient2(_resilientClient);
1044
+ } catch (err) {
1045
+ process.stderr.write(
1046
+ `[database] Daemon client init failed (non-fatal): ${err instanceof Error ? err.message : String(err)}
1047
+ `
1048
+ );
1049
+ }
1050
+ }
1051
+ function getRawClient() {
1052
+ if (!_client) {
1053
+ throw new Error("Database client not initialized. Call initDatabase() first.");
1054
+ }
1055
+ return _client;
1056
+ }
1057
+ async function ensureSchema() {
1058
+ const client = getRawClient();
1059
+ await client.execute("PRAGMA journal_mode = WAL");
1060
+ await client.execute("PRAGMA busy_timeout = 30000");
1061
+ await client.execute("PRAGMA wal_autocheckpoint = 1000");
1062
+ try {
1063
+ await client.execute("PRAGMA libsql_vector_search_ef = 128");
1064
+ } catch {
749
1065
  }
750
1066
  await client.executeMultiple(`
751
- CREATE TABLE IF NOT EXISTS consolidations (
752
- id TEXT PRIMARY KEY,
753
- consolidated_memory_id TEXT NOT NULL,
754
- source_memory_id TEXT NOT NULL,
755
- created_at TEXT NOT NULL
756
- );
757
-
758
- CREATE INDEX IF NOT EXISTS idx_consolidations_source
759
- ON consolidations(source_memory_id);
760
-
761
- CREATE INDEX IF NOT EXISTS idx_consolidations_consolidated
762
- ON consolidations(consolidated_memory_id);
763
- `);
764
- await client.executeMultiple(`
765
- CREATE TABLE IF NOT EXISTS reminders (
1067
+ CREATE TABLE IF NOT EXISTS memories (
766
1068
  id TEXT PRIMARY KEY,
767
- text TEXT NOT NULL,
768
- created_at TEXT NOT NULL,
769
- due_date TEXT,
770
- completed_at TEXT
1069
+ agent_id TEXT NOT NULL,
1070
+ agent_role TEXT NOT NULL,
1071
+ session_id TEXT NOT NULL,
1072
+ timestamp TEXT NOT NULL,
1073
+ tool_name TEXT NOT NULL,
1074
+ project_name TEXT NOT NULL,
1075
+ has_error INTEGER NOT NULL DEFAULT 0,
1076
+ raw_text TEXT NOT NULL,
1077
+ vector F32_BLOB(1024),
1078
+ version INTEGER NOT NULL DEFAULT 0
771
1079
  );
1080
+
1081
+ CREATE INDEX IF NOT EXISTS idx_memories_agent
1082
+ ON memories(agent_id);
1083
+
1084
+ CREATE INDEX IF NOT EXISTS idx_memories_timestamp
1085
+ ON memories(timestamp);
1086
+
1087
+ CREATE INDEX IF NOT EXISTS idx_memories_session
1088
+ ON memories(session_id);
1089
+
1090
+ CREATE INDEX IF NOT EXISTS idx_memories_project
1091
+ ON memories(project_name);
1092
+
1093
+ CREATE INDEX IF NOT EXISTS idx_memories_tool
1094
+ ON memories(tool_name);
1095
+
1096
+ CREATE INDEX IF NOT EXISTS idx_memories_version
1097
+ ON memories(version);
1098
+
1099
+ CREATE INDEX IF NOT EXISTS idx_memories_agent_project
1100
+ ON memories(agent_id, project_name);
772
1101
  `);
773
1102
  await client.executeMultiple(`
774
- CREATE TABLE IF NOT EXISTS notifications (
775
- id TEXT PRIMARY KEY,
776
- agent_id TEXT NOT NULL,
777
- agent_role TEXT NOT NULL,
778
- event TEXT NOT NULL,
779
- project TEXT NOT NULL,
780
- summary TEXT NOT NULL,
781
- task_file TEXT,
782
- read INTEGER NOT NULL DEFAULT 0,
783
- created_at TEXT NOT NULL
1103
+ CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts USING fts5(
1104
+ raw_text,
1105
+ content='memories',
1106
+ content_rowid='rowid'
784
1107
  );
785
1108
 
786
- CREATE INDEX IF NOT EXISTS idx_notifications_read
787
- ON notifications(read);
1109
+ CREATE TRIGGER IF NOT EXISTS memories_fts_ai AFTER INSERT ON memories BEGIN
1110
+ INSERT INTO memories_fts(rowid, raw_text) VALUES (new.rowid, new.raw_text);
1111
+ END;
788
1112
 
789
- CREATE INDEX IF NOT EXISTS idx_notifications_agent
790
- ON notifications(agent_id);
1113
+ CREATE TRIGGER IF NOT EXISTS memories_fts_ad AFTER DELETE ON memories BEGIN
1114
+ INSERT INTO memories_fts(memories_fts, rowid, raw_text) VALUES('delete', old.rowid, old.raw_text);
1115
+ END;
791
1116
 
792
- CREATE INDEX IF NOT EXISTS idx_notifications_task_file
793
- ON notifications(task_file);
1117
+ CREATE TRIGGER IF NOT EXISTS memories_fts_au AFTER UPDATE ON memories BEGIN
1118
+ INSERT INTO memories_fts(memories_fts, rowid, raw_text) VALUES('delete', old.rowid, old.raw_text);
1119
+ INSERT INTO memories_fts(rowid, raw_text) VALUES (new.rowid, new.raw_text);
1120
+ END;
794
1121
  `);
795
1122
  await client.executeMultiple(`
796
- CREATE TABLE IF NOT EXISTS schedules (
797
- id TEXT PRIMARY KEY,
798
- cron TEXT NOT NULL,
799
- description TEXT NOT NULL,
800
- job_type TEXT NOT NULL DEFAULT 'report',
801
- prompt TEXT,
802
- assigned_to TEXT,
803
- project_name TEXT,
804
- active INTEGER NOT NULL DEFAULT 1,
805
- use_crontab INTEGER NOT NULL DEFAULT 0,
806
- created_at TEXT NOT NULL
1123
+ CREATE TABLE IF NOT EXISTS sync_meta (
1124
+ key TEXT PRIMARY KEY,
1125
+ value TEXT NOT NULL
807
1126
  );
808
1127
  `);
809
1128
  await client.executeMultiple(`
810
- CREATE TABLE IF NOT EXISTS device_registry (
811
- device_id TEXT PRIMARY KEY,
812
- friendly_name TEXT NOT NULL,
813
- hostname TEXT NOT NULL,
814
- projects TEXT NOT NULL DEFAULT '[]',
815
- agents TEXT NOT NULL DEFAULT '[]',
816
- connected INTEGER DEFAULT 0,
817
- last_seen TEXT NOT NULL
1129
+ CREATE TABLE IF NOT EXISTS tasks (
1130
+ id TEXT PRIMARY KEY,
1131
+ title TEXT NOT NULL,
1132
+ assigned_to TEXT NOT NULL,
1133
+ assigned_by TEXT NOT NULL,
1134
+ project_name TEXT NOT NULL,
1135
+ priority TEXT NOT NULL DEFAULT 'p1',
1136
+ status TEXT NOT NULL DEFAULT 'open',
1137
+ task_file TEXT,
1138
+ created_at TEXT NOT NULL,
1139
+ updated_at TEXT NOT NULL
818
1140
  );
1141
+
1142
+ CREATE INDEX IF NOT EXISTS idx_tasks_assignee_status
1143
+ ON tasks(assigned_to, status);
819
1144
  `);
820
1145
  await client.executeMultiple(`
821
- CREATE TABLE IF NOT EXISTS messages (
822
- id TEXT PRIMARY KEY,
823
- from_agent TEXT NOT NULL,
824
- from_device TEXT NOT NULL DEFAULT 'local',
825
- target_agent TEXT NOT NULL,
826
- target_project TEXT,
827
- target_device TEXT NOT NULL DEFAULT 'local',
828
- content TEXT NOT NULL,
829
- priority TEXT DEFAULT 'normal',
830
- status TEXT DEFAULT 'pending',
831
- server_seq INTEGER,
832
- retry_count INTEGER DEFAULT 0,
833
- created_at TEXT NOT NULL,
834
- delivered_at TEXT,
835
- processed_at TEXT,
836
- failed_at TEXT,
837
- failure_reason TEXT
1146
+ CREATE TABLE IF NOT EXISTS behaviors (
1147
+ id TEXT PRIMARY KEY,
1148
+ agent_id TEXT NOT NULL,
1149
+ project_name TEXT,
1150
+ domain TEXT,
1151
+ content TEXT NOT NULL,
1152
+ active INTEGER NOT NULL DEFAULT 1,
1153
+ created_at TEXT NOT NULL,
1154
+ updated_at TEXT NOT NULL
838
1155
  );
839
1156
 
840
- CREATE INDEX IF NOT EXISTS idx_messages_target
841
- ON messages(target_agent, status);
842
-
843
- CREATE INDEX IF NOT EXISTS idx_messages_conversation_order
1157
+ CREATE INDEX IF NOT EXISTS idx_behaviors_agent
1158
+ ON behaviors(agent_id, active);
1159
+ `);
1160
+ try {
1161
+ const coordinatorName = getCoordinatorName();
1162
+ const existing = await client.execute({
1163
+ sql: "SELECT COUNT(*) as cnt FROM behaviors WHERE agent_id = ?",
1164
+ args: [coordinatorName]
1165
+ });
1166
+ if (Number(existing.rows[0]?.cnt) === 0) {
1167
+ const seededAt = "2026-03-25T00:00:00Z";
1168
+ for (const [domain, content] of [
1169
+ ["workflow", `Don't ask "keep going?" \u2014 just keep executing phases/plans autonomously`],
1170
+ ["tool-use", "Always use create_task MCP tool, never write .md files directly for task creation"],
1171
+ ["workflow", "Auto-start reviewing when idle and reviews are pending \u2014 never ask founder for permission"]
1172
+ ]) {
1173
+ await client.execute({
1174
+ sql: `INSERT INTO behaviors (id, agent_id, project_name, domain, content, active, created_at, updated_at)
1175
+ VALUES (hex(randomblob(16)), ?, NULL, ?, ?, 1, ?, ?)`,
1176
+ args: [coordinatorName, domain, content, seededAt, seededAt]
1177
+ });
1178
+ }
1179
+ }
1180
+ } catch {
1181
+ }
1182
+ try {
1183
+ await client.execute({
1184
+ sql: `ALTER TABLE behaviors ADD COLUMN priority TEXT DEFAULT 'p1'`,
1185
+ args: []
1186
+ });
1187
+ } catch {
1188
+ }
1189
+ try {
1190
+ await client.execute({
1191
+ sql: `ALTER TABLE tasks ADD COLUMN blocked_by TEXT`,
1192
+ args: []
1193
+ });
1194
+ } catch {
1195
+ }
1196
+ try {
1197
+ await client.execute({
1198
+ sql: `ALTER TABLE tasks ADD COLUMN parent_task_id TEXT`,
1199
+ args: []
1200
+ });
1201
+ } catch {
1202
+ }
1203
+ try {
1204
+ await client.execute({
1205
+ sql: `CREATE INDEX IF NOT EXISTS idx_tasks_parent_task_id
1206
+ ON tasks(parent_task_id)
1207
+ WHERE parent_task_id IS NOT NULL`,
1208
+ args: []
1209
+ });
1210
+ } catch {
1211
+ }
1212
+ try {
1213
+ await client.execute({
1214
+ sql: `UPDATE tasks SET status = 'done' WHERE status = 'completed'`,
1215
+ args: []
1216
+ });
1217
+ } catch {
1218
+ }
1219
+ try {
1220
+ await client.execute({
1221
+ sql: `ALTER TABLE tasks ADD COLUMN reviewer TEXT`,
1222
+ args: []
1223
+ });
1224
+ } catch {
1225
+ }
1226
+ try {
1227
+ await client.execute({
1228
+ sql: `ALTER TABLE tasks ADD COLUMN context TEXT`,
1229
+ args: []
1230
+ });
1231
+ } catch {
1232
+ }
1233
+ try {
1234
+ await client.execute({
1235
+ sql: `ALTER TABLE tasks ADD COLUMN result TEXT`,
1236
+ args: []
1237
+ });
1238
+ } catch {
1239
+ }
1240
+ try {
1241
+ await client.execute({
1242
+ sql: `ALTER TABLE tasks ADD COLUMN assigned_tmux TEXT`,
1243
+ args: []
1244
+ });
1245
+ } catch {
1246
+ }
1247
+ try {
1248
+ await client.execute({
1249
+ sql: `ALTER TABLE tasks ADD COLUMN checkpoint TEXT`,
1250
+ args: []
1251
+ });
1252
+ } catch {
1253
+ }
1254
+ try {
1255
+ await client.execute({
1256
+ sql: `ALTER TABLE tasks ADD COLUMN checkpoint_count INTEGER NOT NULL DEFAULT 0`,
1257
+ args: []
1258
+ });
1259
+ } catch {
1260
+ }
1261
+ try {
1262
+ await client.execute({
1263
+ sql: `ALTER TABLE tasks ADD COLUMN complexity TEXT NOT NULL DEFAULT 'standard'`,
1264
+ args: []
1265
+ });
1266
+ } catch {
1267
+ }
1268
+ try {
1269
+ await client.execute({
1270
+ sql: `ALTER TABLE tasks ADD COLUMN session_scope TEXT`,
1271
+ args: []
1272
+ });
1273
+ } catch {
1274
+ }
1275
+ try {
1276
+ await client.execute({
1277
+ sql: `ALTER TABLE memories ADD COLUMN task_id TEXT`,
1278
+ args: []
1279
+ });
1280
+ } catch {
1281
+ }
1282
+ try {
1283
+ await client.execute({
1284
+ sql: `ALTER TABLE memories ADD COLUMN consolidated INTEGER NOT NULL DEFAULT 0`,
1285
+ args: []
1286
+ });
1287
+ } catch {
1288
+ }
1289
+ try {
1290
+ await client.execute({
1291
+ sql: `ALTER TABLE memories ADD COLUMN author_device_id TEXT`,
1292
+ args: []
1293
+ });
1294
+ } catch {
1295
+ }
1296
+ try {
1297
+ await client.execute({
1298
+ sql: `ALTER TABLE memories ADD COLUMN scope TEXT NOT NULL DEFAULT 'business'`,
1299
+ args: []
1300
+ });
1301
+ } catch {
1302
+ }
1303
+ await client.executeMultiple(`
1304
+ CREATE TABLE IF NOT EXISTS consolidations (
1305
+ id TEXT PRIMARY KEY,
1306
+ consolidated_memory_id TEXT NOT NULL,
1307
+ source_memory_id TEXT NOT NULL,
1308
+ created_at TEXT NOT NULL
1309
+ );
1310
+
1311
+ CREATE INDEX IF NOT EXISTS idx_consolidations_source
1312
+ ON consolidations(source_memory_id);
1313
+
1314
+ CREATE INDEX IF NOT EXISTS idx_consolidations_consolidated
1315
+ ON consolidations(consolidated_memory_id);
1316
+ `);
1317
+ await client.executeMultiple(`
1318
+ CREATE TABLE IF NOT EXISTS reminders (
1319
+ id TEXT PRIMARY KEY,
1320
+ text TEXT NOT NULL,
1321
+ created_at TEXT NOT NULL,
1322
+ due_date TEXT,
1323
+ completed_at TEXT
1324
+ );
1325
+ `);
1326
+ await client.executeMultiple(`
1327
+ CREATE TABLE IF NOT EXISTS notifications (
1328
+ id TEXT PRIMARY KEY,
1329
+ agent_id TEXT NOT NULL,
1330
+ agent_role TEXT NOT NULL,
1331
+ event TEXT NOT NULL,
1332
+ project TEXT NOT NULL,
1333
+ summary TEXT NOT NULL,
1334
+ task_file TEXT,
1335
+ read INTEGER NOT NULL DEFAULT 0,
1336
+ created_at TEXT NOT NULL
1337
+ );
1338
+
1339
+ CREATE INDEX IF NOT EXISTS idx_notifications_read
1340
+ ON notifications(read);
1341
+
1342
+ CREATE INDEX IF NOT EXISTS idx_notifications_agent
1343
+ ON notifications(agent_id);
1344
+
1345
+ CREATE INDEX IF NOT EXISTS idx_notifications_task_file
1346
+ ON notifications(task_file);
1347
+ `);
1348
+ await client.executeMultiple(`
1349
+ CREATE TABLE IF NOT EXISTS schedules (
1350
+ id TEXT PRIMARY KEY,
1351
+ cron TEXT NOT NULL,
1352
+ description TEXT NOT NULL,
1353
+ job_type TEXT NOT NULL DEFAULT 'report',
1354
+ prompt TEXT,
1355
+ assigned_to TEXT,
1356
+ project_name TEXT,
1357
+ active INTEGER NOT NULL DEFAULT 1,
1358
+ use_crontab INTEGER NOT NULL DEFAULT 0,
1359
+ created_at TEXT NOT NULL
1360
+ );
1361
+ `);
1362
+ await client.executeMultiple(`
1363
+ CREATE TABLE IF NOT EXISTS device_registry (
1364
+ device_id TEXT PRIMARY KEY,
1365
+ friendly_name TEXT NOT NULL,
1366
+ hostname TEXT NOT NULL,
1367
+ projects TEXT NOT NULL DEFAULT '[]',
1368
+ agents TEXT NOT NULL DEFAULT '[]',
1369
+ connected INTEGER DEFAULT 0,
1370
+ last_seen TEXT NOT NULL
1371
+ );
1372
+ `);
1373
+ await client.executeMultiple(`
1374
+ CREATE TABLE IF NOT EXISTS messages (
1375
+ id TEXT PRIMARY KEY,
1376
+ from_agent TEXT NOT NULL,
1377
+ from_device TEXT NOT NULL DEFAULT 'local',
1378
+ target_agent TEXT NOT NULL,
1379
+ target_project TEXT,
1380
+ target_device TEXT NOT NULL DEFAULT 'local',
1381
+ content TEXT NOT NULL,
1382
+ priority TEXT DEFAULT 'normal',
1383
+ status TEXT DEFAULT 'pending',
1384
+ server_seq INTEGER,
1385
+ retry_count INTEGER DEFAULT 0,
1386
+ created_at TEXT NOT NULL,
1387
+ delivered_at TEXT,
1388
+ processed_at TEXT,
1389
+ failed_at TEXT,
1390
+ failure_reason TEXT
1391
+ );
1392
+
1393
+ CREATE INDEX IF NOT EXISTS idx_messages_target
1394
+ ON messages(target_agent, status);
1395
+
1396
+ CREATE INDEX IF NOT EXISTS idx_messages_conversation_order
844
1397
  ON messages(target_agent, from_agent, server_seq);
845
1398
  `);
846
1399
  try {
@@ -981,6 +1534,12 @@ async function ensureSchema() {
981
1534
  } catch {
982
1535
  }
983
1536
  }
1537
+ try {
1538
+ await client.execute(
1539
+ `CREATE INDEX IF NOT EXISTS idx_memories_content_hash ON memories(content_hash, agent_id)`
1540
+ );
1541
+ } catch {
1542
+ }
984
1543
  await client.executeMultiple(`
985
1544
  CREATE TABLE IF NOT EXISTS entities (
986
1545
  id TEXT PRIMARY KEY,
@@ -1033,7 +1592,30 @@ async function ensureSchema() {
1033
1592
  entity_id TEXT NOT NULL,
1034
1593
  PRIMARY KEY (hyperedge_id, entity_id)
1035
1594
  );
1595
+
1596
+ CREATE VIRTUAL TABLE IF NOT EXISTS entities_fts USING fts5(
1597
+ name,
1598
+ content=entities,
1599
+ content_rowid=rowid
1600
+ );
1601
+
1602
+ CREATE TRIGGER IF NOT EXISTS entities_fts_ai AFTER INSERT ON entities BEGIN
1603
+ INSERT INTO entities_fts(rowid, name) VALUES (new.rowid, new.name);
1604
+ END;
1605
+
1606
+ CREATE TRIGGER IF NOT EXISTS entities_fts_ad AFTER DELETE ON entities BEGIN
1607
+ INSERT INTO entities_fts(entities_fts, rowid, name) VALUES('delete', old.rowid, old.name);
1608
+ END;
1609
+
1610
+ CREATE TRIGGER IF NOT EXISTS entities_fts_au AFTER UPDATE ON entities BEGIN
1611
+ INSERT INTO entities_fts(entities_fts, rowid, name) VALUES('delete', old.rowid, old.name);
1612
+ INSERT INTO entities_fts(rowid, name) VALUES (new.rowid, new.name);
1613
+ END;
1036
1614
  `);
1615
+ try {
1616
+ await client.execute("INSERT INTO entities_fts(entities_fts) VALUES('rebuild')");
1617
+ } catch {
1618
+ }
1037
1619
  await client.executeMultiple(`
1038
1620
  CREATE TABLE IF NOT EXISTS entity_aliases (
1039
1621
  alias TEXT NOT NULL PRIMARY KEY,
@@ -1214,6 +1796,33 @@ async function ensureSchema() {
1214
1796
  CREATE INDEX IF NOT EXISTS idx_conversations_channel
1215
1797
  ON conversations(channel_id);
1216
1798
  `);
1799
+ await client.executeMultiple(`
1800
+ CREATE TABLE IF NOT EXISTS session_agent_map (
1801
+ session_uuid TEXT PRIMARY KEY,
1802
+ agent_id TEXT NOT NULL,
1803
+ session_name TEXT,
1804
+ task_id TEXT,
1805
+ project_name TEXT,
1806
+ started_at TEXT NOT NULL
1807
+ );
1808
+
1809
+ CREATE INDEX IF NOT EXISTS idx_session_agent_map_agent
1810
+ ON session_agent_map(agent_id);
1811
+ `);
1812
+ try {
1813
+ const mapCount = await client.execute({ sql: `SELECT COUNT(*) as cnt FROM session_agent_map`, args: [] });
1814
+ if (Number(mapCount.rows[0]?.cnt ?? 0) === 0) {
1815
+ await client.execute({
1816
+ sql: `INSERT OR IGNORE INTO session_agent_map (session_uuid, agent_id, session_name, started_at)
1817
+ SELECT session_id, agent_id, '', MIN(timestamp)
1818
+ FROM memories
1819
+ WHERE session_id IS NOT NULL AND session_id != '' AND agent_id IS NOT NULL AND agent_id != ''
1820
+ GROUP BY session_id, agent_id`,
1821
+ args: []
1822
+ });
1823
+ }
1824
+ } catch {
1825
+ }
1217
1826
  try {
1218
1827
  await client.execute({
1219
1828
  sql: `ALTER TABLE tasks ADD COLUMN budget_tokens INTEGER`,
@@ -1347,15 +1956,41 @@ async function ensureSchema() {
1347
1956
  });
1348
1957
  } catch {
1349
1958
  }
1959
+ for (const col of [
1960
+ "ALTER TABLE memories ADD COLUMN intent TEXT",
1961
+ "ALTER TABLE memories ADD COLUMN outcome TEXT",
1962
+ "ALTER TABLE memories ADD COLUMN domain TEXT",
1963
+ "ALTER TABLE memories ADD COLUMN referenced_entities TEXT",
1964
+ "ALTER TABLE memories ADD COLUMN retrieval_count INTEGER DEFAULT 0",
1965
+ "ALTER TABLE memories ADD COLUMN chain_position TEXT",
1966
+ "ALTER TABLE memories ADD COLUMN review_status TEXT",
1967
+ "ALTER TABLE memories ADD COLUMN context_window_pct INTEGER",
1968
+ "ALTER TABLE memories ADD COLUMN file_paths TEXT",
1969
+ "ALTER TABLE memories ADD COLUMN commit_hash TEXT",
1970
+ "ALTER TABLE memories ADD COLUMN duration_ms INTEGER",
1971
+ "ALTER TABLE memories ADD COLUMN token_cost REAL",
1972
+ "ALTER TABLE memories ADD COLUMN audience TEXT",
1973
+ "ALTER TABLE memories ADD COLUMN language_type TEXT",
1974
+ "ALTER TABLE memories ADD COLUMN parent_memory_id TEXT"
1975
+ ]) {
1976
+ try {
1977
+ await client.execute(col);
1978
+ } catch {
1979
+ }
1980
+ }
1350
1981
  }
1351
1982
  async function disposeDatabase() {
1983
+ if (_daemonClient) {
1984
+ _daemonClient.close();
1985
+ _daemonClient = null;
1986
+ }
1352
1987
  if (_client) {
1353
1988
  _client.close();
1354
1989
  _client = null;
1355
1990
  _resilientClient = null;
1356
1991
  }
1357
1992
  }
1358
- var _client, _resilientClient, initTurso, disposeTurso;
1993
+ var _client, _resilientClient, _daemonClient, initTurso, disposeTurso;
1359
1994
  var init_database = __esm({
1360
1995
  "src/lib/database.ts"() {
1361
1996
  "use strict";
@@ -1363,6 +1998,7 @@ var init_database = __esm({
1363
1998
  init_employees();
1364
1999
  _client = null;
1365
2000
  _resilientClient = null;
2001
+ _daemonClient = null;
1366
2002
  initTurso = initDatabase;
1367
2003
  disposeTurso = disposeDatabase;
1368
2004
  }
@@ -1436,12 +2072,12 @@ __export(shard_manager_exports, {
1436
2072
  listShards: () => listShards,
1437
2073
  shardExists: () => shardExists
1438
2074
  });
1439
- import path6 from "path";
1440
- import { existsSync as existsSync5, mkdirSync, readdirSync as readdirSync2 } from "fs";
2075
+ import path7 from "path";
2076
+ import { existsSync as existsSync6, mkdirSync, readdirSync as readdirSync2 } from "fs";
1441
2077
  import { createClient as createClient2 } from "@libsql/client";
1442
2078
  function initShardManager(encryptionKey) {
1443
2079
  _encryptionKey = encryptionKey;
1444
- if (!existsSync5(SHARDS_DIR)) {
2080
+ if (!existsSync6(SHARDS_DIR)) {
1445
2081
  mkdirSync(SHARDS_DIR, { recursive: true });
1446
2082
  }
1447
2083
  _shardingEnabled = true;
@@ -1462,7 +2098,7 @@ function getShardClient(projectName) {
1462
2098
  }
1463
2099
  const cached = _shards.get(safeName);
1464
2100
  if (cached) return cached;
1465
- const dbPath = path6.join(SHARDS_DIR, `${safeName}.db`);
2101
+ const dbPath = path7.join(SHARDS_DIR, `${safeName}.db`);
1466
2102
  const client = createClient2({
1467
2103
  url: `file:${dbPath}`,
1468
2104
  encryptionKey: _encryptionKey
@@ -1472,10 +2108,10 @@ function getShardClient(projectName) {
1472
2108
  }
1473
2109
  function shardExists(projectName) {
1474
2110
  const safeName = projectName.replace(/[^a-zA-Z0-9_-]/g, "_");
1475
- return existsSync5(path6.join(SHARDS_DIR, `${safeName}.db`));
2111
+ return existsSync6(path7.join(SHARDS_DIR, `${safeName}.db`));
1476
2112
  }
1477
2113
  function listShards() {
1478
- if (!existsSync5(SHARDS_DIR)) return [];
2114
+ if (!existsSync6(SHARDS_DIR)) return [];
1479
2115
  return readdirSync2(SHARDS_DIR).filter((f) => f.endsWith(".db")).map((f) => f.replace(".db", ""));
1480
2116
  }
1481
2117
  async function ensureShardSchema(client) {
@@ -1661,7 +2297,7 @@ var init_shard_manager = __esm({
1661
2297
  "src/lib/shard-manager.ts"() {
1662
2298
  "use strict";
1663
2299
  init_config();
1664
- SHARDS_DIR = path6.join(EXE_AI_DIR, "shards");
2300
+ SHARDS_DIR = path7.join(EXE_AI_DIR, "shards");
1665
2301
  _shards = /* @__PURE__ */ new Map();
1666
2302
  _encryptionKey = null;
1667
2303
  _shardingEnabled = false;
@@ -1770,808 +2406,463 @@ var init_platform_procedures = __esm({
1770
2406
  domain: "architecture",
1771
2407
  priority: "p0",
1772
2408
  content: "Tasks live in the DB. Intercom (tmux send-keys) is fire-and-forget \u2014 it may fail, get garbled, or arrive mid-work. Never rely on intercom for task delivery. The UserPromptSubmit hook checks the DB for new tasks on every prompt. Your operating procedures step 7 says check for next work. The daemon nudges idle agents as a speedup. If you have no tasks, you found them all."
1773
- }
1774
- ];
1775
- PLATFORM_PROCEDURE_TITLES = new Set(
1776
- PLATFORM_PROCEDURES.map((p) => p.title)
1777
- );
1778
- }
1779
- });
1780
-
1781
- // src/lib/global-procedures.ts
1782
- var global_procedures_exports = {};
1783
- __export(global_procedures_exports, {
1784
- deactivateGlobalProcedure: () => deactivateGlobalProcedure,
1785
- getGlobalProceduresBlock: () => getGlobalProceduresBlock,
1786
- loadGlobalProcedures: () => loadGlobalProcedures,
1787
- storeGlobalProcedure: () => storeGlobalProcedure
1788
- });
1789
- import { randomUUID } from "crypto";
1790
- async function loadGlobalProcedures() {
1791
- const client = getClient();
1792
- const result = await client.execute({
1793
- sql: "SELECT * FROM global_procedures WHERE active = 1 ORDER BY priority ASC, created_at ASC",
1794
- args: []
1795
- });
1796
- const allRows = result.rows;
1797
- const customerOnly = allRows.filter((p) => !PLATFORM_PROCEDURE_TITLES.has(p.title));
1798
- if (customerOnly.length > 0) {
1799
- _customerCache = customerOnly.map((p) => `### ${p.title}
1800
- ${p.content}`).join("\n\n");
1801
- } else {
1802
- _customerCache = "";
1803
- }
1804
- _cacheLoaded = true;
1805
- return customerOnly;
1806
- }
1807
- function getGlobalProceduresBlock() {
1808
- const sections = [];
1809
- if (_platformCache) sections.push(_platformCache);
1810
- if (_cacheLoaded && _customerCache) sections.push(_customerCache);
1811
- if (sections.length === 0) return "";
1812
- return `## Organization-Wide Procedures (MANDATORY \u2014 supersedes all other rules)
1813
-
1814
- ${sections.join("\n\n")}
1815
- `;
1816
- }
1817
- async function storeGlobalProcedure(input2) {
1818
- const id = randomUUID();
1819
- const now = (/* @__PURE__ */ new Date()).toISOString();
1820
- const client = getClient();
1821
- await client.execute({
1822
- sql: `INSERT INTO global_procedures (id, title, content, priority, domain, active, created_at, updated_at)
1823
- VALUES (?, ?, ?, ?, ?, 1, ?, ?)`,
1824
- args: [id, input2.title, input2.content, input2.priority ?? "p0", input2.domain ?? null, now, now]
1825
- });
1826
- await loadGlobalProcedures();
1827
- return id;
1828
- }
1829
- async function deactivateGlobalProcedure(id) {
1830
- const now = (/* @__PURE__ */ new Date()).toISOString();
1831
- const client = getClient();
1832
- const result = await client.execute({
1833
- sql: "UPDATE global_procedures SET active = 0, updated_at = ? WHERE id = ?",
1834
- args: [now, id]
1835
- });
1836
- await loadGlobalProcedures();
1837
- return result.rowsAffected > 0;
1838
- }
1839
- var _customerCache, _cacheLoaded, _platformCache;
1840
- var init_global_procedures = __esm({
1841
- "src/lib/global-procedures.ts"() {
1842
- "use strict";
1843
- init_database();
1844
- init_platform_procedures();
1845
- _customerCache = "";
1846
- _cacheLoaded = false;
1847
- _platformCache = PLATFORM_PROCEDURES.map((p) => `### ${p.title}
1848
- ${p.content}`).join("\n\n");
1849
- }
1850
- });
1851
-
1852
- // src/lib/notifications.ts
1853
- import crypto2 from "crypto";
1854
- import path7 from "path";
1855
- import os4 from "os";
1856
- import {
1857
- readFileSync as readFileSync4,
1858
- readdirSync as readdirSync3,
1859
- unlinkSync as unlinkSync2,
1860
- existsSync as existsSync6,
1861
- rmdirSync
1862
- } from "fs";
1863
- async function writeNotification(notification) {
1864
- try {
1865
- const client = getClient();
1866
- const id = crypto2.randomUUID();
1867
- const now = (/* @__PURE__ */ new Date()).toISOString();
1868
- await client.execute({
1869
- sql: `INSERT INTO notifications (id, agent_id, agent_role, event, project, summary, task_file, read, created_at)
1870
- VALUES (?, ?, ?, ?, ?, ?, ?, 0, ?)`,
1871
- args: [
1872
- id,
1873
- notification.agentId,
1874
- notification.agentRole,
1875
- notification.event,
1876
- notification.project,
1877
- notification.summary,
1878
- notification.taskFile ?? null,
1879
- now
1880
- ]
1881
- });
1882
- } catch (err) {
1883
- process.stderr.write(`[notifications] WRITE FAILED: ${err instanceof Error ? err.message : String(err)}
1884
- `);
1885
- }
1886
- }
1887
- async function markAsReadByTaskFile(taskFile) {
1888
- try {
1889
- const client = getClient();
1890
- await client.execute({
1891
- sql: "UPDATE notifications SET read = 1 WHERE task_file = ? AND read = 0",
1892
- args: [taskFile]
1893
- });
1894
- } catch {
1895
- }
1896
- }
1897
- var init_notifications = __esm({
1898
- "src/lib/notifications.ts"() {
1899
- "use strict";
1900
- init_database();
1901
- }
1902
- });
1903
-
1904
- // src/lib/license.ts
1905
- import { readFileSync as readFileSync5, writeFileSync as writeFileSync2, existsSync as existsSync7, mkdirSync as mkdirSync2 } from "fs";
1906
- import { randomUUID as randomUUID2 } from "crypto";
1907
- import path8 from "path";
1908
- import { jwtVerify, importSPKI } from "jose";
1909
- async function fetchRetry(url, init) {
1910
- try {
1911
- return await fetch(url, init);
1912
- } catch {
1913
- await new Promise((r) => setTimeout(r, RETRY_DELAY_MS));
1914
- return fetch(url, { ...init, signal: AbortSignal.timeout(1e4) });
1915
- }
1916
- }
1917
- function loadDeviceId() {
1918
- const deviceJsonPath = path8.join(EXE_AI_DIR, "device.json");
1919
- try {
1920
- if (existsSync7(deviceJsonPath)) {
1921
- const data = JSON.parse(readFileSync5(deviceJsonPath, "utf8"));
1922
- if (data.deviceId) return data.deviceId;
1923
- }
1924
- } catch {
1925
- }
1926
- try {
1927
- if (existsSync7(DEVICE_ID_PATH)) {
1928
- const id2 = readFileSync5(DEVICE_ID_PATH, "utf8").trim();
1929
- if (id2) return id2;
1930
- }
1931
- } catch {
1932
- }
1933
- const id = randomUUID2();
1934
- mkdirSync2(EXE_AI_DIR, { recursive: true });
1935
- writeFileSync2(DEVICE_ID_PATH, id, "utf8");
1936
- return id;
1937
- }
1938
- function loadLicense() {
1939
- try {
1940
- if (!existsSync7(LICENSE_PATH)) return null;
1941
- return readFileSync5(LICENSE_PATH, "utf8").trim();
1942
- } catch {
1943
- return null;
1944
- }
1945
- }
1946
- function saveLicense(apiKey) {
1947
- mkdirSync2(EXE_AI_DIR, { recursive: true });
1948
- writeFileSync2(LICENSE_PATH, apiKey.trim(), { encoding: "utf8", mode: 384 });
1949
- }
1950
- async function verifyLicenseJwt(token) {
1951
- try {
1952
- const key = await importSPKI(LICENSE_PUBLIC_KEY_PEM, LICENSE_JWT_ALG);
1953
- const { payload } = await jwtVerify(token, key, {
1954
- algorithms: [LICENSE_JWT_ALG]
1955
- });
1956
- const plan = payload.plan ?? "free";
1957
- const email = payload.sub ?? "";
1958
- const limits = PLAN_LIMITS[plan] ?? PLAN_LIMITS.free;
1959
- return {
1960
- valid: true,
1961
- plan,
1962
- email,
1963
- expiresAt: payload.exp ? new Date(payload.exp * 1e3).toISOString() : null,
1964
- deviceLimit: limits.devices,
1965
- employeeLimit: limits.employees,
1966
- memoryLimit: limits.memories
1967
- };
1968
- } catch {
1969
- return null;
1970
- }
1971
- }
1972
- async function getCachedLicense() {
1973
- try {
1974
- if (!existsSync7(CACHE_PATH)) return null;
1975
- const raw = JSON.parse(readFileSync5(CACHE_PATH, "utf8"));
1976
- if (!raw.token || typeof raw.token !== "string") return null;
1977
- return await verifyLicenseJwt(raw.token);
1978
- } catch {
1979
- return null;
1980
- }
1981
- }
1982
- function readCachedToken() {
1983
- try {
1984
- if (!existsSync7(CACHE_PATH)) return null;
1985
- const raw = JSON.parse(readFileSync5(CACHE_PATH, "utf8"));
1986
- return typeof raw.token === "string" ? raw.token : null;
1987
- } catch {
1988
- return null;
1989
- }
1990
- }
1991
- function getRawCachedPlan() {
1992
- try {
1993
- const token = readCachedToken();
1994
- if (!token) return null;
1995
- const parts = token.split(".");
1996
- if (parts.length !== 3) return null;
1997
- const payload = JSON.parse(Buffer.from(parts[1], "base64url").toString());
1998
- const plan = payload.plan ?? "free";
1999
- const limits = PLAN_LIMITS[plan] ?? PLAN_LIMITS.free;
2000
- process.stderr.write(
2001
- `[license] WARN: using unverified cached plan (API unreachable, JWT expired). Plan: ${plan}
2002
- `
2003
- );
2004
- return {
2005
- valid: true,
2006
- plan,
2007
- email: payload.sub ?? "",
2008
- expiresAt: payload.exp ? new Date(payload.exp * 1e3).toISOString() : null,
2009
- deviceLimit: limits.devices,
2010
- employeeLimit: limits.employees,
2011
- memoryLimit: limits.memories
2012
- };
2013
- } catch {
2014
- return null;
2015
- }
2016
- }
2017
- function cacheResponse(token) {
2018
- try {
2019
- writeFileSync2(CACHE_PATH, JSON.stringify({ token }), "utf8");
2020
- } catch {
2021
- }
2022
- }
2023
- async function validateLicense(apiKey, deviceId) {
2024
- const did = deviceId ?? loadDeviceId();
2025
- try {
2026
- const res = await fetchRetry(`${API_BASE}/auth/activate`, {
2027
- method: "POST",
2028
- headers: { "Content-Type": "application/json" },
2029
- body: JSON.stringify({ apiKey, deviceId: did }),
2030
- signal: AbortSignal.timeout(1e4)
2031
- });
2032
- if (res.ok) {
2033
- const data = await res.json();
2034
- if (data.error === "device_limit_exceeded") {
2035
- const cached2 = await getCachedLicense();
2036
- if (cached2) return cached2;
2037
- const raw2 = getRawCachedPlan();
2038
- if (raw2) return { ...raw2, valid: false };
2039
- return { ...FREE_LICENSE, valid: false, plan: "free" };
2040
- }
2041
- if (data.token) {
2042
- cacheResponse(data.token);
2043
- const verified = await verifyLicenseJwt(data.token);
2044
- if (verified) return verified;
2045
- }
2046
- const limits = PLAN_LIMITS[data.plan] ?? PLAN_LIMITS.free;
2047
- return {
2048
- valid: data.valid,
2049
- plan: data.plan,
2050
- email: data.email,
2051
- expiresAt: data.expiresAt,
2052
- deviceLimit: limits.devices,
2053
- employeeLimit: limits.employees,
2054
- memoryLimit: limits.memories
2055
- };
2056
- }
2057
- const cached = await getCachedLicense();
2058
- if (cached) return cached;
2059
- const raw = getRawCachedPlan();
2060
- if (raw) return raw;
2061
- return { ...FREE_LICENSE, valid: false, plan: "free" };
2062
- } catch {
2063
- const cached = await getCachedLicense();
2064
- if (cached) return cached;
2065
- const rawFallback = getRawCachedPlan();
2066
- if (rawFallback) return rawFallback;
2067
- return { ...FREE_LICENSE, valid: false, error: "offline" };
2068
- }
2069
- }
2070
- function getCacheAgeMs() {
2071
- try {
2072
- const { statSync: statSync3 } = __require("fs");
2073
- const s = statSync3(CACHE_PATH);
2074
- return Date.now() - s.mtimeMs;
2075
- } catch {
2076
- return Infinity;
2077
- }
2078
- }
2079
- async function checkLicense() {
2080
- let key = loadLicense();
2081
- if (!key) {
2082
- try {
2083
- const configPath = path8.join(EXE_AI_DIR, "config.json");
2084
- if (existsSync7(configPath)) {
2085
- const raw = JSON.parse(readFileSync5(configPath, "utf8"));
2086
- const cloud = raw.cloud;
2087
- if (cloud?.apiKey) {
2088
- key = cloud.apiKey;
2089
- saveLicense(key);
2090
- }
2091
- }
2092
- } catch {
2093
- }
2094
- }
2095
- if (!key) return FREE_LICENSE;
2096
- const cached = await getCachedLicense();
2097
- if (cached && getCacheAgeMs() < CACHE_MAX_AGE_MS) return cached;
2098
- const deviceId = loadDeviceId();
2099
- return validateLicense(key, deviceId);
2100
- }
2101
- var LICENSE_PATH, CACHE_PATH, DEVICE_ID_PATH, API_BASE, RETRY_DELAY_MS, LICENSE_PUBLIC_KEY_PEM, LICENSE_JWT_ALG, PLAN_LIMITS, FREE_LICENSE, CACHE_MAX_AGE_MS;
2102
- var init_license = __esm({
2103
- "src/lib/license.ts"() {
2104
- "use strict";
2105
- init_config();
2106
- LICENSE_PATH = path8.join(EXE_AI_DIR, "license.key");
2107
- CACHE_PATH = path8.join(EXE_AI_DIR, "license-cache.json");
2108
- DEVICE_ID_PATH = path8.join(EXE_AI_DIR, "device-id");
2109
- API_BASE = "https://askexe.com/cloud";
2110
- RETRY_DELAY_MS = 500;
2111
- LICENSE_PUBLIC_KEY_PEM = `-----BEGIN PUBLIC KEY-----
2112
- MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEeHztAMOpR/ZMh+rWuOASjEZ54CGY
2113
- 4uj+UqeKCcvtgNHKmOK278HJaJcANe9xAeji8AFYu27q3WtzCi04pHudow==
2114
- -----END PUBLIC KEY-----`;
2115
- LICENSE_JWT_ALG = "ES256";
2116
- PLAN_LIMITS = {
2117
- free: { devices: 1, employees: 1, memories: 5e3 },
2118
- pro: { devices: 3, employees: 5, memories: 1e5 },
2119
- team: { devices: 10, employees: 20, memories: 1e6 },
2120
- agency: { devices: 50, employees: 100, memories: 1e7 },
2121
- enterprise: { devices: -1, employees: -1, memories: -1 }
2122
- };
2123
- FREE_LICENSE = {
2124
- valid: true,
2125
- plan: "free",
2126
- email: "",
2127
- expiresAt: null,
2128
- deviceLimit: 1,
2129
- employeeLimit: 1,
2130
- memoryLimit: 5e3
2131
- };
2132
- CACHE_MAX_AGE_MS = 36e5;
2409
+ }
2410
+ ];
2411
+ PLATFORM_PROCEDURE_TITLES = new Set(
2412
+ PLATFORM_PROCEDURES.map((p) => p.title)
2413
+ );
2133
2414
  }
2134
2415
  });
2135
2416
 
2136
- // src/lib/plan-limits.ts
2137
- import { readFileSync as readFileSync6, existsSync as existsSync8 } from "fs";
2138
- import path9 from "path";
2139
- function getLicenseSync() {
2140
- try {
2141
- if (!existsSync8(CACHE_PATH2)) return freeLicense();
2142
- const raw = JSON.parse(readFileSync6(CACHE_PATH2, "utf8"));
2143
- if (!raw.token || typeof raw.token !== "string") return freeLicense();
2144
- const parts = raw.token.split(".");
2145
- if (parts.length !== 3) return freeLicense();
2146
- const payload = JSON.parse(Buffer.from(parts[1], "base64url").toString());
2147
- const plan = payload.plan ?? "free";
2148
- const limits = PLAN_LIMITS[plan] ?? PLAN_LIMITS.free;
2149
- return {
2150
- valid: true,
2151
- plan,
2152
- email: payload.sub ?? "",
2153
- expiresAt: payload.exp ? new Date(payload.exp * 1e3).toISOString() : null,
2154
- deviceLimit: limits.devices,
2155
- employeeLimit: limits.employees,
2156
- memoryLimit: limits.memories
2157
- };
2158
- } catch {
2159
- return freeLicense();
2417
+ // src/lib/global-procedures.ts
2418
+ var global_procedures_exports = {};
2419
+ __export(global_procedures_exports, {
2420
+ deactivateGlobalProcedure: () => deactivateGlobalProcedure,
2421
+ getGlobalProceduresBlock: () => getGlobalProceduresBlock,
2422
+ loadGlobalProcedures: () => loadGlobalProcedures,
2423
+ storeGlobalProcedure: () => storeGlobalProcedure
2424
+ });
2425
+ import { randomUUID as randomUUID2 } from "crypto";
2426
+ async function loadGlobalProcedures() {
2427
+ const client = getClient();
2428
+ const result = await client.execute({
2429
+ sql: "SELECT * FROM global_procedures WHERE active = 1 ORDER BY priority ASC, created_at ASC",
2430
+ args: []
2431
+ });
2432
+ const allRows = result.rows;
2433
+ const customerOnly = allRows.filter((p) => !PLATFORM_PROCEDURE_TITLES.has(p.title));
2434
+ if (customerOnly.length > 0) {
2435
+ _customerCache = customerOnly.map((p) => `### ${p.title}
2436
+ ${p.content}`).join("\n\n");
2437
+ } else {
2438
+ _customerCache = "";
2160
2439
  }
2440
+ _cacheLoaded = true;
2441
+ return customerOnly;
2161
2442
  }
2162
- function freeLicense() {
2163
- const limits = PLAN_LIMITS.free;
2164
- return {
2165
- valid: true,
2166
- plan: "free",
2167
- email: "",
2168
- expiresAt: null,
2169
- deviceLimit: limits.devices,
2170
- employeeLimit: limits.employees,
2171
- memoryLimit: limits.memories
2172
- };
2443
+ function getGlobalProceduresBlock() {
2444
+ const sections = [];
2445
+ if (_platformCache) sections.push(_platformCache);
2446
+ if (_cacheLoaded && _customerCache) sections.push(_customerCache);
2447
+ if (sections.length === 0) return "";
2448
+ return `## Organization-Wide Procedures (MANDATORY \u2014 supersedes all other rules)
2449
+
2450
+ ${sections.join("\n\n")}
2451
+ `;
2173
2452
  }
2174
- async function countActiveMemories() {
2175
- if (!isInitialized()) return 0;
2453
+ async function storeGlobalProcedure(input2) {
2454
+ const id = randomUUID2();
2455
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2176
2456
  const client = getClient();
2177
- const result = await client.execute(
2178
- "SELECT COUNT(*) as cnt FROM memories WHERE status = 'active' OR status IS NULL"
2179
- );
2180
- const row = result.rows[0];
2181
- return Number(row?.cnt ?? 0);
2457
+ await client.execute({
2458
+ sql: `INSERT INTO global_procedures (id, title, content, priority, domain, active, created_at, updated_at)
2459
+ VALUES (?, ?, ?, ?, ?, 1, ?, ?)`,
2460
+ args: [id, input2.title, input2.content, input2.priority ?? "p0", input2.domain ?? null, now, now]
2461
+ });
2462
+ await loadGlobalProcedures();
2463
+ return id;
2182
2464
  }
2183
- async function assertMemoryLimit() {
2184
- const license = await checkLicense();
2185
- if (license.memoryLimit < 0) return;
2186
- const count = await countActiveMemories();
2187
- if (count >= license.memoryLimit) {
2188
- throw new PlanLimitError(
2189
- `Memory limit reached: ${count}/${license.memoryLimit} active memories on the ${license.plan} plan. Upgrade at https://askexe.com to store more.`
2190
- );
2465
+ async function deactivateGlobalProcedure(id) {
2466
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2467
+ const client = getClient();
2468
+ const result = await client.execute({
2469
+ sql: "UPDATE global_procedures SET active = 0, updated_at = ? WHERE id = ?",
2470
+ args: [now, id]
2471
+ });
2472
+ await loadGlobalProcedures();
2473
+ return result.rowsAffected > 0;
2474
+ }
2475
+ var _customerCache, _cacheLoaded, _platformCache;
2476
+ var init_global_procedures = __esm({
2477
+ "src/lib/global-procedures.ts"() {
2478
+ "use strict";
2479
+ init_database();
2480
+ init_platform_procedures();
2481
+ _customerCache = "";
2482
+ _cacheLoaded = false;
2483
+ _platformCache = PLATFORM_PROCEDURES.map((p) => `### ${p.title}
2484
+ ${p.content}`).join("\n\n");
2485
+ }
2486
+ });
2487
+
2488
+ // src/lib/notifications.ts
2489
+ import crypto2 from "crypto";
2490
+ import path8 from "path";
2491
+ import os5 from "os";
2492
+ import {
2493
+ readFileSync as readFileSync5,
2494
+ readdirSync as readdirSync3,
2495
+ unlinkSync as unlinkSync3,
2496
+ existsSync as existsSync7,
2497
+ rmdirSync
2498
+ } from "fs";
2499
+ async function writeNotification(notification) {
2500
+ try {
2501
+ const client = getClient();
2502
+ const id = crypto2.randomUUID();
2503
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2504
+ await client.execute({
2505
+ sql: `INSERT INTO notifications (id, agent_id, agent_role, event, project, summary, task_file, read, created_at)
2506
+ VALUES (?, ?, ?, ?, ?, ?, ?, 0, ?)`,
2507
+ args: [
2508
+ id,
2509
+ notification.agentId,
2510
+ notification.agentRole,
2511
+ notification.event,
2512
+ notification.project,
2513
+ notification.summary,
2514
+ notification.taskFile ?? null,
2515
+ now
2516
+ ]
2517
+ });
2518
+ } catch (err) {
2519
+ process.stderr.write(`[notifications] WRITE FAILED: ${err instanceof Error ? err.message : String(err)}
2520
+ `);
2191
2521
  }
2192
2522
  }
2193
- function assertEmployeeLimitSync(rosterPath) {
2194
- const license = getLicenseSync();
2195
- if (license.employeeLimit < 0) return;
2196
- const filePath = rosterPath ?? EMPLOYEES_PATH;
2197
- let count = 0;
2523
+ async function markAsReadByTaskFile(taskFile) {
2198
2524
  try {
2199
- if (existsSync8(filePath)) {
2200
- const raw = readFileSync6(filePath, "utf8");
2201
- const employees = JSON.parse(raw);
2202
- count = Array.isArray(employees) ? employees.length : 0;
2203
- }
2525
+ const client = getClient();
2526
+ await client.execute({
2527
+ sql: "UPDATE notifications SET read = 1 WHERE task_file = ? AND read = 0",
2528
+ args: [taskFile]
2529
+ });
2204
2530
  } catch {
2205
- throw new PlanLimitError(
2206
- `Cannot verify employee count: roster unreadable at ${filePath}. Refusing to proceed. Check file permissions or upgrade plan.`
2207
- );
2208
- }
2209
- if (count >= license.employeeLimit) {
2210
- throw new PlanLimitError(
2211
- `Employee limit reached: ${count}/${license.employeeLimit} employees on the ${license.plan} plan. Upgrade at https://askexe.com to add more.`
2212
- );
2213
2531
  }
2214
2532
  }
2215
- var PlanLimitError, CACHE_PATH2;
2216
- var init_plan_limits = __esm({
2217
- "src/lib/plan-limits.ts"() {
2533
+ var init_notifications = __esm({
2534
+ "src/lib/notifications.ts"() {
2218
2535
  "use strict";
2219
2536
  init_database();
2220
- init_employees();
2221
- init_license();
2222
- init_config();
2223
- PlanLimitError = class extends Error {
2224
- constructor(message) {
2225
- super(message);
2226
- this.name = "PlanLimitError";
2227
- }
2228
- };
2229
- CACHE_PATH2 = path9.join(EXE_AI_DIR, "license-cache.json");
2230
2537
  }
2231
2538
  });
2232
2539
 
2233
- // src/lib/exe-daemon-client.ts
2234
- import net from "net";
2235
- import { spawn } from "child_process";
2540
+ // src/lib/license.ts
2541
+ import { readFileSync as readFileSync6, writeFileSync as writeFileSync2, existsSync as existsSync8, mkdirSync as mkdirSync2 } from "fs";
2236
2542
  import { randomUUID as randomUUID3 } from "crypto";
2237
- import { existsSync as existsSync9, unlinkSync as unlinkSync3, readFileSync as readFileSync7, openSync, closeSync, statSync as statSync2 } from "fs";
2238
- import path10 from "path";
2239
- import { fileURLToPath } from "url";
2240
- function handleData(chunk) {
2241
- _buffer += chunk.toString();
2242
- if (_buffer.length > MAX_BUFFER) {
2243
- _buffer = "";
2244
- return;
2245
- }
2246
- let newlineIdx;
2247
- while ((newlineIdx = _buffer.indexOf("\n")) !== -1) {
2248
- const line = _buffer.slice(0, newlineIdx).trim();
2249
- _buffer = _buffer.slice(newlineIdx + 1);
2250
- if (!line) continue;
2251
- try {
2252
- const response = JSON.parse(line);
2253
- const entry = _pending.get(response.id);
2254
- if (entry) {
2255
- clearTimeout(entry.timer);
2256
- _pending.delete(response.id);
2257
- entry.resolve(response);
2258
- }
2259
- } catch {
2260
- }
2261
- }
2262
- }
2263
- function cleanupStaleFiles() {
2264
- if (existsSync9(PID_PATH)) {
2265
- try {
2266
- const pid = parseInt(readFileSync7(PID_PATH, "utf8").trim(), 10);
2267
- if (pid > 0) {
2268
- try {
2269
- process.kill(pid, 0);
2270
- return;
2271
- } catch {
2272
- }
2273
- }
2274
- } catch {
2275
- }
2276
- try {
2277
- unlinkSync3(PID_PATH);
2278
- } catch {
2279
- }
2280
- try {
2281
- unlinkSync3(SOCKET_PATH);
2282
- } catch {
2283
- }
2284
- }
2285
- }
2286
- function findPackageRoot() {
2287
- let dir = path10.dirname(fileURLToPath(import.meta.url));
2288
- const { root } = path10.parse(dir);
2289
- while (dir !== root) {
2290
- if (existsSync9(path10.join(dir, "package.json"))) return dir;
2291
- dir = path10.dirname(dir);
2543
+ import path9 from "path";
2544
+ import { jwtVerify, importSPKI } from "jose";
2545
+ async function fetchRetry(url, init) {
2546
+ try {
2547
+ return await fetch(url, init);
2548
+ } catch {
2549
+ await new Promise((r) => setTimeout(r, RETRY_DELAY_MS));
2550
+ return fetch(url, { ...init, signal: AbortSignal.timeout(1e4) });
2292
2551
  }
2293
- return null;
2294
2552
  }
2295
- function spawnDaemon() {
2296
- const pkgRoot = findPackageRoot();
2297
- if (!pkgRoot) {
2298
- process.stderr.write("[exed-client] WARN: cannot find package root\n");
2299
- return;
2300
- }
2301
- const daemonPath = path10.join(pkgRoot, "dist", "lib", "exe-daemon.js");
2302
- if (!existsSync9(daemonPath)) {
2303
- process.stderr.write(`[exed-client] WARN: daemon script not found at ${daemonPath}
2304
- `);
2305
- return;
2306
- }
2307
- const resolvedPath = daemonPath;
2308
- process.stderr.write(`[exed-client] Spawning daemon: ${resolvedPath}
2309
- `);
2310
- const logPath = path10.join(path10.dirname(SOCKET_PATH), "exed.log");
2311
- let stderrFd = "ignore";
2553
+ function loadDeviceId() {
2554
+ const deviceJsonPath = path9.join(EXE_AI_DIR, "device.json");
2312
2555
  try {
2313
- stderrFd = openSync(logPath, "a");
2556
+ if (existsSync8(deviceJsonPath)) {
2557
+ const data = JSON.parse(readFileSync6(deviceJsonPath, "utf8"));
2558
+ if (data.deviceId) return data.deviceId;
2559
+ }
2314
2560
  } catch {
2315
2561
  }
2316
- const child = spawn(process.execPath, [resolvedPath], {
2317
- detached: true,
2318
- stdio: ["ignore", "ignore", stderrFd],
2319
- env: {
2320
- ...process.env,
2321
- TMUX: void 0,
2322
- // Daemon is global — must not inherit session scope
2323
- TMUX_PANE: void 0,
2324
- // Prevents resolveExeSession() from scoping to one session
2325
- EXE_DAEMON_SOCK: SOCKET_PATH,
2326
- EXE_DAEMON_PID: PID_PATH
2327
- }
2328
- });
2329
- child.unref();
2330
- if (typeof stderrFd === "number") {
2331
- try {
2332
- closeSync(stderrFd);
2333
- } catch {
2562
+ try {
2563
+ if (existsSync8(DEVICE_ID_PATH)) {
2564
+ const id2 = readFileSync6(DEVICE_ID_PATH, "utf8").trim();
2565
+ if (id2) return id2;
2334
2566
  }
2567
+ } catch {
2335
2568
  }
2569
+ const id = randomUUID3();
2570
+ mkdirSync2(EXE_AI_DIR, { recursive: true });
2571
+ writeFileSync2(DEVICE_ID_PATH, id, "utf8");
2572
+ return id;
2336
2573
  }
2337
- function acquireSpawnLock() {
2574
+ function loadLicense() {
2338
2575
  try {
2339
- const fd = openSync(SPAWN_LOCK_PATH, "wx");
2340
- closeSync(fd);
2341
- return true;
2576
+ if (!existsSync8(LICENSE_PATH)) return null;
2577
+ return readFileSync6(LICENSE_PATH, "utf8").trim();
2342
2578
  } catch {
2343
- try {
2344
- const stat = statSync2(SPAWN_LOCK_PATH);
2345
- if (Date.now() - stat.mtimeMs > SPAWN_LOCK_STALE_MS) {
2346
- try {
2347
- unlinkSync3(SPAWN_LOCK_PATH);
2348
- } catch {
2349
- }
2350
- try {
2351
- const fd = openSync(SPAWN_LOCK_PATH, "wx");
2352
- closeSync(fd);
2353
- return true;
2354
- } catch {
2355
- }
2356
- }
2357
- } catch {
2358
- }
2359
- return false;
2579
+ return null;
2360
2580
  }
2361
2581
  }
2362
- function releaseSpawnLock() {
2582
+ function saveLicense(apiKey) {
2583
+ mkdirSync2(EXE_AI_DIR, { recursive: true });
2584
+ writeFileSync2(LICENSE_PATH, apiKey.trim(), { encoding: "utf8", mode: 384 });
2585
+ }
2586
+ async function verifyLicenseJwt(token) {
2363
2587
  try {
2364
- unlinkSync3(SPAWN_LOCK_PATH);
2588
+ const key = await importSPKI(LICENSE_PUBLIC_KEY_PEM, LICENSE_JWT_ALG);
2589
+ const { payload } = await jwtVerify(token, key, {
2590
+ algorithms: [LICENSE_JWT_ALG]
2591
+ });
2592
+ const plan = payload.plan ?? "free";
2593
+ const email = payload.sub ?? "";
2594
+ const limits = PLAN_LIMITS[plan] ?? PLAN_LIMITS.free;
2595
+ return {
2596
+ valid: true,
2597
+ plan,
2598
+ email,
2599
+ expiresAt: payload.exp ? new Date(payload.exp * 1e3).toISOString() : null,
2600
+ deviceLimit: limits.devices,
2601
+ employeeLimit: limits.employees,
2602
+ memoryLimit: limits.memories
2603
+ };
2365
2604
  } catch {
2605
+ return null;
2366
2606
  }
2367
2607
  }
2368
- function connectToSocket() {
2369
- return new Promise((resolve) => {
2370
- if (_socket && _connected) {
2371
- resolve(true);
2372
- return;
2373
- }
2374
- const socket = net.createConnection({ path: SOCKET_PATH });
2375
- const connectTimeout = setTimeout(() => {
2376
- socket.destroy();
2377
- resolve(false);
2378
- }, 2e3);
2379
- socket.on("connect", () => {
2380
- clearTimeout(connectTimeout);
2381
- _socket = socket;
2382
- _connected = true;
2383
- _buffer = "";
2384
- socket.on("data", handleData);
2385
- socket.on("close", () => {
2386
- _connected = false;
2387
- _socket = null;
2388
- for (const [id, entry] of _pending) {
2389
- clearTimeout(entry.timer);
2390
- _pending.delete(id);
2391
- entry.resolve({ error: "Connection closed" });
2392
- }
2393
- });
2394
- socket.on("error", () => {
2395
- _connected = false;
2396
- _socket = null;
2397
- });
2398
- resolve(true);
2399
- });
2400
- socket.on("error", () => {
2401
- clearTimeout(connectTimeout);
2402
- resolve(false);
2403
- });
2404
- });
2608
+ async function getCachedLicense() {
2609
+ try {
2610
+ if (!existsSync8(CACHE_PATH)) return null;
2611
+ const raw = JSON.parse(readFileSync6(CACHE_PATH, "utf8"));
2612
+ if (!raw.token || typeof raw.token !== "string") return null;
2613
+ return await verifyLicenseJwt(raw.token);
2614
+ } catch {
2615
+ return null;
2616
+ }
2405
2617
  }
2406
- async function connectEmbedDaemon() {
2407
- if (_socket && _connected) return true;
2408
- if (await connectToSocket()) return true;
2409
- if (acquireSpawnLock()) {
2410
- try {
2411
- cleanupStaleFiles();
2412
- spawnDaemon();
2413
- } finally {
2414
- releaseSpawnLock();
2415
- }
2618
+ function readCachedToken() {
2619
+ try {
2620
+ if (!existsSync8(CACHE_PATH)) return null;
2621
+ const raw = JSON.parse(readFileSync6(CACHE_PATH, "utf8"));
2622
+ return typeof raw.token === "string" ? raw.token : null;
2623
+ } catch {
2624
+ return null;
2416
2625
  }
2417
- const start = Date.now();
2418
- let delay2 = 100;
2419
- while (Date.now() - start < CONNECT_TIMEOUT_MS) {
2420
- await new Promise((r) => setTimeout(r, delay2));
2421
- if (await connectToSocket()) return true;
2422
- delay2 = Math.min(delay2 * 2, 3e3);
2626
+ }
2627
+ function getRawCachedPlan() {
2628
+ try {
2629
+ const token = readCachedToken();
2630
+ if (!token) return null;
2631
+ const parts = token.split(".");
2632
+ if (parts.length !== 3) return null;
2633
+ const payload = JSON.parse(Buffer.from(parts[1], "base64url").toString());
2634
+ const plan = payload.plan ?? "free";
2635
+ const limits = PLAN_LIMITS[plan] ?? PLAN_LIMITS.free;
2636
+ process.stderr.write(
2637
+ `[license] WARN: using unverified cached plan (API unreachable, JWT expired). Plan: ${plan}
2638
+ `
2639
+ );
2640
+ return {
2641
+ valid: true,
2642
+ plan,
2643
+ email: payload.sub ?? "",
2644
+ expiresAt: payload.exp ? new Date(payload.exp * 1e3).toISOString() : null,
2645
+ deviceLimit: limits.devices,
2646
+ employeeLimit: limits.employees,
2647
+ memoryLimit: limits.memories
2648
+ };
2649
+ } catch {
2650
+ return null;
2423
2651
  }
2424
- return false;
2425
2652
  }
2426
- function sendRequest(texts, priority) {
2427
- return new Promise((resolve) => {
2428
- if (!_socket || !_connected) {
2429
- resolve({ error: "Not connected" });
2430
- return;
2431
- }
2432
- const id = randomUUID3();
2433
- const timer = setTimeout(() => {
2434
- _pending.delete(id);
2435
- resolve({ error: "Request timeout" });
2436
- }, REQUEST_TIMEOUT_MS);
2437
- _pending.set(id, { resolve, timer });
2438
- try {
2439
- _socket.write(JSON.stringify({ id, texts, priority }) + "\n");
2440
- } catch {
2441
- clearTimeout(timer);
2442
- _pending.delete(id);
2443
- resolve({ error: "Write failed" });
2444
- }
2445
- });
2653
+ function cacheResponse(token) {
2654
+ try {
2655
+ writeFileSync2(CACHE_PATH, JSON.stringify({ token }), "utf8");
2656
+ } catch {
2657
+ }
2446
2658
  }
2447
- async function pingDaemon() {
2448
- if (!_socket || !_connected) return null;
2449
- return new Promise((resolve) => {
2450
- const id = randomUUID3();
2451
- const timer = setTimeout(() => {
2452
- _pending.delete(id);
2453
- resolve(null);
2454
- }, 5e3);
2455
- _pending.set(id, {
2456
- resolve: (data) => {
2457
- if (data.health) {
2458
- resolve(data.health);
2459
- } else {
2460
- resolve(null);
2461
- }
2462
- },
2463
- timer
2659
+ async function validateLicense(apiKey, deviceId) {
2660
+ const did = deviceId ?? loadDeviceId();
2661
+ try {
2662
+ const res = await fetchRetry(`${API_BASE}/auth/activate`, {
2663
+ method: "POST",
2664
+ headers: { "Content-Type": "application/json" },
2665
+ body: JSON.stringify({ apiKey, deviceId: did }),
2666
+ signal: AbortSignal.timeout(1e4)
2464
2667
  });
2465
- try {
2466
- _socket.write(JSON.stringify({ id, type: "health" }) + "\n");
2467
- } catch {
2468
- clearTimeout(timer);
2469
- _pending.delete(id);
2470
- resolve(null);
2668
+ if (res.ok) {
2669
+ const data = await res.json();
2670
+ if (data.error === "device_limit_exceeded") {
2671
+ const cached2 = await getCachedLicense();
2672
+ if (cached2) return cached2;
2673
+ const raw2 = getRawCachedPlan();
2674
+ if (raw2) return { ...raw2, valid: false };
2675
+ return { ...FREE_LICENSE, valid: false, plan: "free" };
2676
+ }
2677
+ if (data.token) {
2678
+ cacheResponse(data.token);
2679
+ const verified = await verifyLicenseJwt(data.token);
2680
+ if (verified) return verified;
2681
+ }
2682
+ const limits = PLAN_LIMITS[data.plan] ?? PLAN_LIMITS.free;
2683
+ return {
2684
+ valid: data.valid,
2685
+ plan: data.plan,
2686
+ email: data.email,
2687
+ expiresAt: data.expiresAt,
2688
+ deviceLimit: limits.devices,
2689
+ employeeLimit: limits.employees,
2690
+ memoryLimit: limits.memories
2691
+ };
2471
2692
  }
2472
- });
2693
+ const cached = await getCachedLicense();
2694
+ if (cached) return cached;
2695
+ const raw = getRawCachedPlan();
2696
+ if (raw) return raw;
2697
+ return { ...FREE_LICENSE, valid: false, plan: "free" };
2698
+ } catch {
2699
+ const cached = await getCachedLicense();
2700
+ if (cached) return cached;
2701
+ const rawFallback = getRawCachedPlan();
2702
+ if (rawFallback) return rawFallback;
2703
+ return { ...FREE_LICENSE, valid: false, error: "offline" };
2704
+ }
2473
2705
  }
2474
- function killAndRespawnDaemon() {
2475
- process.stderr.write("[exed-client] Killing daemon for restart...\n");
2476
- if (existsSync9(PID_PATH)) {
2706
+ function getCacheAgeMs() {
2707
+ try {
2708
+ const { statSync: statSync3 } = __require("fs");
2709
+ const s = statSync3(CACHE_PATH);
2710
+ return Date.now() - s.mtimeMs;
2711
+ } catch {
2712
+ return Infinity;
2713
+ }
2714
+ }
2715
+ async function checkLicense() {
2716
+ let key = loadLicense();
2717
+ if (!key) {
2477
2718
  try {
2478
- const pid = parseInt(readFileSync7(PID_PATH, "utf8").trim(), 10);
2479
- if (pid > 0) {
2480
- try {
2481
- process.kill(pid, "SIGKILL");
2482
- } catch {
2719
+ const configPath = path9.join(EXE_AI_DIR, "config.json");
2720
+ if (existsSync8(configPath)) {
2721
+ const raw = JSON.parse(readFileSync6(configPath, "utf8"));
2722
+ const cloud = raw.cloud;
2723
+ if (cloud?.apiKey) {
2724
+ key = cloud.apiKey;
2725
+ saveLicense(key);
2483
2726
  }
2484
2727
  }
2485
2728
  } catch {
2486
2729
  }
2487
2730
  }
2488
- if (_socket) {
2489
- _socket.destroy();
2490
- _socket = null;
2491
- }
2492
- _connected = false;
2493
- _buffer = "";
2494
- try {
2495
- unlinkSync3(PID_PATH);
2496
- } catch {
2731
+ if (!key) return FREE_LICENSE;
2732
+ const cached = await getCachedLicense();
2733
+ if (cached && getCacheAgeMs() < CACHE_MAX_AGE_MS) return cached;
2734
+ const deviceId = loadDeviceId();
2735
+ return validateLicense(key, deviceId);
2736
+ }
2737
+ var LICENSE_PATH, CACHE_PATH, DEVICE_ID_PATH, API_BASE, RETRY_DELAY_MS, LICENSE_PUBLIC_KEY_PEM, LICENSE_JWT_ALG, PLAN_LIMITS, FREE_LICENSE, CACHE_MAX_AGE_MS;
2738
+ var init_license = __esm({
2739
+ "src/lib/license.ts"() {
2740
+ "use strict";
2741
+ init_config();
2742
+ LICENSE_PATH = path9.join(EXE_AI_DIR, "license.key");
2743
+ CACHE_PATH = path9.join(EXE_AI_DIR, "license-cache.json");
2744
+ DEVICE_ID_PATH = path9.join(EXE_AI_DIR, "device-id");
2745
+ API_BASE = "https://askexe.com/cloud";
2746
+ RETRY_DELAY_MS = 500;
2747
+ LICENSE_PUBLIC_KEY_PEM = `-----BEGIN PUBLIC KEY-----
2748
+ MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEeHztAMOpR/ZMh+rWuOASjEZ54CGY
2749
+ 4uj+UqeKCcvtgNHKmOK278HJaJcANe9xAeji8AFYu27q3WtzCi04pHudow==
2750
+ -----END PUBLIC KEY-----`;
2751
+ LICENSE_JWT_ALG = "ES256";
2752
+ PLAN_LIMITS = {
2753
+ free: { devices: 1, employees: 1, memories: 5e3 },
2754
+ pro: { devices: 3, employees: 5, memories: 1e5 },
2755
+ team: { devices: 10, employees: 20, memories: 1e6 },
2756
+ agency: { devices: 50, employees: 100, memories: 1e7 },
2757
+ enterprise: { devices: -1, employees: -1, memories: -1 }
2758
+ };
2759
+ FREE_LICENSE = {
2760
+ valid: true,
2761
+ plan: "free",
2762
+ email: "",
2763
+ expiresAt: null,
2764
+ deviceLimit: 1,
2765
+ employeeLimit: 1,
2766
+ memoryLimit: 5e3
2767
+ };
2768
+ CACHE_MAX_AGE_MS = 36e5;
2497
2769
  }
2770
+ });
2771
+
2772
+ // src/lib/plan-limits.ts
2773
+ import { readFileSync as readFileSync7, existsSync as existsSync9 } from "fs";
2774
+ import path10 from "path";
2775
+ function getLicenseSync() {
2498
2776
  try {
2499
- unlinkSync3(SOCKET_PATH);
2777
+ if (!existsSync9(CACHE_PATH2)) return freeLicense();
2778
+ const raw = JSON.parse(readFileSync7(CACHE_PATH2, "utf8"));
2779
+ if (!raw.token || typeof raw.token !== "string") return freeLicense();
2780
+ const parts = raw.token.split(".");
2781
+ if (parts.length !== 3) return freeLicense();
2782
+ const payload = JSON.parse(Buffer.from(parts[1], "base64url").toString());
2783
+ const plan = payload.plan ?? "free";
2784
+ const limits = PLAN_LIMITS[plan] ?? PLAN_LIMITS.free;
2785
+ return {
2786
+ valid: true,
2787
+ plan,
2788
+ email: payload.sub ?? "",
2789
+ expiresAt: payload.exp ? new Date(payload.exp * 1e3).toISOString() : null,
2790
+ deviceLimit: limits.devices,
2791
+ employeeLimit: limits.employees,
2792
+ memoryLimit: limits.memories
2793
+ };
2500
2794
  } catch {
2795
+ return freeLicense();
2501
2796
  }
2502
- spawnDaemon();
2503
2797
  }
2504
- async function embedViaClient(text, priority = "high") {
2505
- if (!_connected && !await connectEmbedDaemon()) return null;
2506
- _requestCount++;
2507
- if (_requestCount % HEALTH_CHECK_INTERVAL === 0) {
2508
- const health = await pingDaemon();
2509
- if (!health) {
2510
- process.stderr.write(`[exed-client] Periodic health check failed at request ${_requestCount} \u2014 restarting daemon
2511
- `);
2512
- killAndRespawnDaemon();
2513
- const start = Date.now();
2514
- let delay2 = 200;
2515
- while (Date.now() - start < CONNECT_TIMEOUT_MS) {
2516
- await new Promise((r) => setTimeout(r, delay2));
2517
- if (await connectToSocket()) break;
2518
- delay2 = Math.min(delay2 * 2, 3e3);
2519
- }
2520
- if (!_connected) return null;
2521
- }
2522
- }
2523
- const result = await sendRequest([text], priority);
2524
- if (!result.error && result.vectors?.[0]) return result.vectors[0];
2525
- if (result.error) {
2526
- process.stderr.write(`[exed-client] Embed failed (${result.error}) \u2014 attempting restart
2527
- `);
2528
- killAndRespawnDaemon();
2529
- const start = Date.now();
2530
- let delay2 = 200;
2531
- while (Date.now() - start < CONNECT_TIMEOUT_MS) {
2532
- await new Promise((r) => setTimeout(r, delay2));
2533
- if (await connectToSocket()) break;
2534
- delay2 = Math.min(delay2 * 2, 3e3);
2535
- }
2536
- if (!_connected) return null;
2537
- const retry = await sendRequest([text], priority);
2538
- if (!retry.error && retry.vectors?.[0]) return retry.vectors[0];
2539
- process.stderr.write(`[exed-client] Embed retry also failed: ${retry.error ?? "no vector"}
2540
- `);
2798
+ function freeLicense() {
2799
+ const limits = PLAN_LIMITS.free;
2800
+ return {
2801
+ valid: true,
2802
+ plan: "free",
2803
+ email: "",
2804
+ expiresAt: null,
2805
+ deviceLimit: limits.devices,
2806
+ employeeLimit: limits.employees,
2807
+ memoryLimit: limits.memories
2808
+ };
2809
+ }
2810
+ async function countActiveMemories() {
2811
+ if (!isInitialized()) return 0;
2812
+ const client = getClient();
2813
+ const result = await client.execute(
2814
+ "SELECT COUNT(*) as cnt FROM memories WHERE status = 'active' OR status IS NULL"
2815
+ );
2816
+ const row = result.rows[0];
2817
+ return Number(row?.cnt ?? 0);
2818
+ }
2819
+ async function assertMemoryLimit() {
2820
+ const license = await checkLicense();
2821
+ if (license.memoryLimit < 0) return;
2822
+ const count = await countActiveMemories();
2823
+ if (count >= license.memoryLimit) {
2824
+ throw new PlanLimitError(
2825
+ `Memory limit reached: ${count}/${license.memoryLimit} active memories on the ${license.plan} plan. Upgrade at https://askexe.com to store more.`
2826
+ );
2541
2827
  }
2542
- return null;
2543
2828
  }
2544
- function disconnectClient() {
2545
- if (_socket) {
2546
- _socket.destroy();
2547
- _socket = null;
2829
+ function assertEmployeeLimitSync(rosterPath) {
2830
+ const license = getLicenseSync();
2831
+ if (license.employeeLimit < 0) return;
2832
+ const filePath = rosterPath ?? EMPLOYEES_PATH;
2833
+ let count = 0;
2834
+ try {
2835
+ if (existsSync9(filePath)) {
2836
+ const raw = readFileSync7(filePath, "utf8");
2837
+ const employees = JSON.parse(raw);
2838
+ count = Array.isArray(employees) ? employees.length : 0;
2839
+ }
2840
+ } catch {
2841
+ throw new PlanLimitError(
2842
+ `Cannot verify employee count: roster unreadable at ${filePath}. Refusing to proceed. Check file permissions or upgrade plan.`
2843
+ );
2548
2844
  }
2549
- _connected = false;
2550
- _buffer = "";
2551
- for (const [id, entry] of _pending) {
2552
- clearTimeout(entry.timer);
2553
- _pending.delete(id);
2554
- entry.resolve({ error: "Client disconnected" });
2845
+ if (count >= license.employeeLimit) {
2846
+ throw new PlanLimitError(
2847
+ `Employee limit reached: ${count}/${license.employeeLimit} employees on the ${license.plan} plan. Upgrade at https://askexe.com to add more.`
2848
+ );
2555
2849
  }
2556
2850
  }
2557
- 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;
2558
- var init_exe_daemon_client = __esm({
2559
- "src/lib/exe-daemon-client.ts"() {
2851
+ var PlanLimitError, CACHE_PATH2;
2852
+ var init_plan_limits = __esm({
2853
+ "src/lib/plan-limits.ts"() {
2560
2854
  "use strict";
2855
+ init_database();
2856
+ init_employees();
2857
+ init_license();
2561
2858
  init_config();
2562
- SOCKET_PATH = process.env.EXE_DAEMON_SOCK ?? process.env.EXE_EMBED_SOCK ?? path10.join(EXE_AI_DIR, "exed.sock");
2563
- PID_PATH = process.env.EXE_DAEMON_PID ?? process.env.EXE_EMBED_PID ?? path10.join(EXE_AI_DIR, "exed.pid");
2564
- SPAWN_LOCK_PATH = path10.join(EXE_AI_DIR, "exed-spawn.lock");
2565
- SPAWN_LOCK_STALE_MS = 3e4;
2566
- CONNECT_TIMEOUT_MS = 15e3;
2567
- REQUEST_TIMEOUT_MS = 3e4;
2568
- _socket = null;
2569
- _connected = false;
2570
- _buffer = "";
2571
- _requestCount = 0;
2572
- HEALTH_CHECK_INTERVAL = 100;
2573
- _pending = /* @__PURE__ */ new Map();
2574
- MAX_BUFFER = 1e7;
2859
+ PlanLimitError = class extends Error {
2860
+ constructor(message) {
2861
+ super(message);
2862
+ this.name = "PlanLimitError";
2863
+ }
2864
+ };
2865
+ CACHE_PATH2 = path10.join(EXE_AI_DIR, "license-cache.json");
2575
2866
  }
2576
2867
  });
2577
2868
 
@@ -2646,7 +2937,7 @@ var init_embedder = __esm({
2646
2937
  // src/lib/session-registry.ts
2647
2938
  import { readFileSync as readFileSync8, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3, existsSync as existsSync10 } from "fs";
2648
2939
  import path11 from "path";
2649
- import os5 from "os";
2940
+ import os6 from "os";
2650
2941
  function registerSession(entry) {
2651
2942
  const dir = path11.dirname(REGISTRY_PATH);
2652
2943
  if (!existsSync10(dir)) {
@@ -2673,7 +2964,7 @@ var REGISTRY_PATH;
2673
2964
  var init_session_registry = __esm({
2674
2965
  "src/lib/session-registry.ts"() {
2675
2966
  "use strict";
2676
- REGISTRY_PATH = path11.join(os5.homedir(), ".exe-os", "session-registry.json");
2967
+ REGISTRY_PATH = path11.join(os6.homedir(), ".exe-os", "session-registry.json");
2677
2968
  }
2678
2969
  });
2679
2970
 
@@ -2872,7 +3163,7 @@ var init_provider_table = __esm({
2872
3163
  // src/lib/intercom-queue.ts
2873
3164
  import { readFileSync as readFileSync9, writeFileSync as writeFileSync4, renameSync as renameSync3, existsSync as existsSync11, mkdirSync as mkdirSync4 } from "fs";
2874
3165
  import path12 from "path";
2875
- import os6 from "os";
3166
+ import os7 from "os";
2876
3167
  function ensureDir() {
2877
3168
  const dir = path12.dirname(QUEUE_PATH);
2878
3169
  if (!existsSync11(dir)) mkdirSync4(dir, { recursive: true });
@@ -2912,9 +3203,9 @@ var QUEUE_PATH, TTL_MS, INTERCOM_LOG;
2912
3203
  var init_intercom_queue = __esm({
2913
3204
  "src/lib/intercom-queue.ts"() {
2914
3205
  "use strict";
2915
- QUEUE_PATH = path12.join(os6.homedir(), ".exe-os", "intercom-queue.json");
3206
+ QUEUE_PATH = path12.join(os7.homedir(), ".exe-os", "intercom-queue.json");
2916
3207
  TTL_MS = 60 * 60 * 1e3;
2917
- INTERCOM_LOG = path12.join(os6.homedir(), ".exe-os", "intercom.log");
3208
+ INTERCOM_LOG = path12.join(os7.homedir(), ".exe-os", "intercom.log");
2918
3209
  }
2919
3210
  });
2920
3211
 
@@ -3264,7 +3555,7 @@ __export(tmux_routing_exports, {
3264
3555
  import { execFileSync as execFileSync2, execSync as execSync6 } from "child_process";
3265
3556
  import { readFileSync as readFileSync10, writeFileSync as writeFileSync5, mkdirSync as mkdirSync5, existsSync as existsSync12, appendFileSync } from "fs";
3266
3557
  import path13 from "path";
3267
- import os7 from "os";
3558
+ import os8 from "os";
3268
3559
  import { fileURLToPath as fileURLToPath2 } from "url";
3269
3560
  import { unlinkSync as unlinkSync4 } from "fs";
3270
3561
  function spawnLockPath(sessionName) {
@@ -3588,7 +3879,7 @@ function notifyParentExe(sessionKey) {
3588
3879
  return true;
3589
3880
  }
3590
3881
  function ensureEmployee(employeeName, exeSession, projectDir, opts) {
3591
- if (employeeName === "exe" || isCoordinatorName(employeeName)) {
3882
+ if (isCoordinatorName(employeeName)) {
3592
3883
  return { status: "failed", sessionName: "", error: "The COO is not a dispatchable employee" };
3593
3884
  }
3594
3885
  try {
@@ -3660,7 +3951,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
3660
3951
  const transport = getTransport();
3661
3952
  const sessionName = employeeSessionName(employeeName, exeSession, opts?.instance);
3662
3953
  const instanceLabel = opts?.instance != null && opts.instance > 0 ? `${employeeName}${opts.instance}` : employeeName;
3663
- const logDir = path13.join(os7.homedir(), ".exe-os", "session-logs");
3954
+ const logDir = path13.join(os8.homedir(), ".exe-os", "session-logs");
3664
3955
  const logFile = path13.join(logDir, `${instanceLabel}-${Date.now()}.log`);
3665
3956
  if (!existsSync12(logDir)) {
3666
3957
  mkdirSync5(logDir, { recursive: true });
@@ -3676,7 +3967,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
3676
3967
  } catch {
3677
3968
  }
3678
3969
  try {
3679
- const claudeJsonPath = path13.join(os7.homedir(), ".claude.json");
3970
+ const claudeJsonPath = path13.join(os8.homedir(), ".claude.json");
3680
3971
  let claudeJson = {};
3681
3972
  try {
3682
3973
  claudeJson = JSON.parse(readFileSync10(claudeJsonPath, "utf8"));
@@ -3691,7 +3982,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
3691
3982
  } catch {
3692
3983
  }
3693
3984
  try {
3694
- const settingsDir = path13.join(os7.homedir(), ".claude", "projects");
3985
+ const settingsDir = path13.join(os8.homedir(), ".claude", "projects");
3695
3986
  const normalizedKey = (opts?.cwd ?? projectDir).replace(/\//g, "-").replace(/^-/, "");
3696
3987
  const projSettingsDir = path13.join(settingsDir, normalizedKey);
3697
3988
  const settingsPath = path13.join(projSettingsDir, "settings.json");
@@ -3739,7 +4030,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
3739
4030
  let legacyFallbackWarned = false;
3740
4031
  if (!useExeAgent && !useBinSymlink) {
3741
4032
  const identityPath = path13.join(
3742
- os7.homedir(),
4033
+ os8.homedir(),
3743
4034
  ".exe-os",
3744
4035
  "identity",
3745
4036
  `${employeeName}.md`
@@ -3769,7 +4060,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
3769
4060
  }
3770
4061
  let sessionContextFlag = "";
3771
4062
  try {
3772
- const ctxDir = path13.join(os7.homedir(), ".exe-os", "session-cache");
4063
+ const ctxDir = path13.join(os8.homedir(), ".exe-os", "session-cache");
3773
4064
  mkdirSync5(ctxDir, { recursive: true });
3774
4065
  const ctxFile = path13.join(ctxDir, `session-context-${sessionName}.md`);
3775
4066
  const ctxContent = [
@@ -3881,13 +4172,13 @@ var init_tmux_routing = __esm({
3881
4172
  init_intercom_queue();
3882
4173
  init_plan_limits();
3883
4174
  init_employees();
3884
- SPAWN_LOCK_DIR = path13.join(os7.homedir(), ".exe-os", "spawn-locks");
3885
- SESSION_CACHE = path13.join(os7.homedir(), ".exe-os", "session-cache");
4175
+ SPAWN_LOCK_DIR = path13.join(os8.homedir(), ".exe-os", "spawn-locks");
4176
+ SESSION_CACHE = path13.join(os8.homedir(), ".exe-os", "session-cache");
3886
4177
  BEHAVIORS_EXPORT_TIMEOUT_MS = 1e4;
3887
4178
  VALID_SESSION_NAME = /^[a-z]+\d*-[a-zA-Z0-9_]+$/;
3888
4179
  VERIFY_PANE_LINES = 200;
3889
4180
  INTERCOM_DEBOUNCE_MS = 3e4;
3890
- INTERCOM_LOG2 = path13.join(os7.homedir(), ".exe-os", "intercom.log");
4181
+ INTERCOM_LOG2 = path13.join(os8.homedir(), ".exe-os", "intercom.log");
3891
4182
  DEBOUNCE_FILE = path13.join(SESSION_CACHE, "intercom-debounce.json");
3892
4183
  DEBOUNCE_CLEANUP_AGE_MS = 5 * 60 * 1e3;
3893
4184
  BUSY_PATTERN = /[✻✽✶✳·].*…|Running…/;
@@ -3921,6 +4212,7 @@ var init_task_scope = __esm({
3921
4212
  // src/lib/tasks-crud.ts
3922
4213
  import crypto4 from "crypto";
3923
4214
  import path14 from "path";
4215
+ import os9 from "os";
3924
4216
  import { execSync as execSync7 } from "child_process";
3925
4217
  import { mkdir as mkdir4, writeFile as writeFile4, appendFile } from "fs/promises";
3926
4218
  import { existsSync as existsSync13, readFileSync as readFileSync11 } from "fs";
@@ -3964,6 +4256,35 @@ function extractParentFromContext(contextBody) {
3964
4256
  function slugify(title) {
3965
4257
  return title.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
3966
4258
  }
4259
+ function buildKeywordIndex() {
4260
+ const idx = /* @__PURE__ */ new Map();
4261
+ for (const [role, keywords] of Object.entries(LANE_KEYWORDS)) {
4262
+ for (const kw of keywords) {
4263
+ const existing = idx.get(kw) ?? [];
4264
+ existing.push(role);
4265
+ idx.set(kw, existing);
4266
+ }
4267
+ }
4268
+ return idx;
4269
+ }
4270
+ function checkLaneAffinity(title, context, assigneeName) {
4271
+ const employees = loadEmployeesSync();
4272
+ const employee = employees.find((e) => e.name === assigneeName);
4273
+ if (!employee) return void 0;
4274
+ const assigneeRole = employee.role;
4275
+ const text = `${title} ${context}`.toLowerCase();
4276
+ const matchedRoles = /* @__PURE__ */ new Set();
4277
+ for (const [keyword, roles] of KEYWORD_INDEX) {
4278
+ if (text.includes(keyword)) {
4279
+ for (const role of roles) matchedRoles.add(role);
4280
+ }
4281
+ }
4282
+ if (matchedRoles.size === 0) return void 0;
4283
+ if (matchedRoles.has(assigneeRole)) return void 0;
4284
+ if (assigneeRole === "COO") return void 0;
4285
+ const expectedRoles = Array.from(matchedRoles).join(" or ");
4286
+ return `\u26A0\uFE0F Lane mismatch: task content suggests ${expectedRoles}, but assigned to ${assigneeName} (${assigneeRole}).`;
4287
+ }
3967
4288
  async function resolveTask(client, identifier, scopeSession) {
3968
4289
  const scope = sessionScopeFilter(scopeSession);
3969
4290
  let result = await client.execute({
@@ -4013,7 +4334,14 @@ async function createTaskCore(input2) {
4013
4334
  const id = crypto4.randomUUID();
4014
4335
  const now = (/* @__PURE__ */ new Date()).toISOString();
4015
4336
  const slug = slugify(input2.title);
4016
- const taskFile = input2.taskFile ?? `exe/${input2.assignedTo}/${slug}.md`;
4337
+ let earlySessionScope = null;
4338
+ try {
4339
+ const { resolveExeSession: resolveExeSession2 } = await Promise.resolve().then(() => (init_tmux_routing(), tmux_routing_exports));
4340
+ earlySessionScope = resolveExeSession2();
4341
+ } catch {
4342
+ }
4343
+ const scope = earlySessionScope ?? "default";
4344
+ const taskFile = input2.taskFile ?? `tasks/${scope}/${input2.assignedTo}/${slug}.md`;
4017
4345
  let blockedById = null;
4018
4346
  const initialStatus = input2.blockedBy ? "blocked" : "open";
4019
4347
  if (input2.blockedBy) {
@@ -4053,6 +4381,13 @@ async function createTaskCore(input2) {
4053
4381
  if (dupCheck.rows.length > 0) {
4054
4382
  warning = `similar active task already exists (${String(dupCheck.rows[0].id)}). Created new task anyway.`;
4055
4383
  }
4384
+ if (!process.env.DISABLE_LANE_AFFINITY) {
4385
+ const laneWarning = checkLaneAffinity(input2.title, input2.context, input2.assignedTo);
4386
+ if (laneWarning) {
4387
+ warning = warning ? `${warning}
4388
+ ${laneWarning}` : laneWarning;
4389
+ }
4390
+ }
4056
4391
  if (input2.baseDir) {
4057
4392
  try {
4058
4393
  await mkdir4(path14.join(input2.baseDir, "exe", "output"), { recursive: true });
@@ -4063,12 +4398,7 @@ async function createTaskCore(input2) {
4063
4398
  }
4064
4399
  }
4065
4400
  const complexity = input2.complexity ?? "standard";
4066
- let sessionScope = null;
4067
- try {
4068
- const { resolveExeSession: resolveExeSession2 } = await Promise.resolve().then(() => (init_tmux_routing(), tmux_routing_exports));
4069
- sessionScope = resolveExeSession2();
4070
- } catch {
4071
- }
4401
+ const sessionScope = earlySessionScope;
4072
4402
  await client.execute({
4073
4403
  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)
4074
4404
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
@@ -4095,6 +4425,39 @@ async function createTaskCore(input2) {
4095
4425
  now
4096
4426
  ]
4097
4427
  });
4428
+ if (input2.baseDir) {
4429
+ try {
4430
+ const EXE_OS_DIR = path14.join(os9.homedir(), ".exe-os");
4431
+ const mdPath = path14.join(EXE_OS_DIR, taskFile);
4432
+ const mdDir = path14.dirname(mdPath);
4433
+ if (!existsSync13(mdDir)) await mkdir4(mdDir, { recursive: true });
4434
+ const reviewer = input2.reviewer ?? input2.assignedBy;
4435
+ const mdContent = `# ${input2.title}
4436
+
4437
+ **ID:** ${id}
4438
+ **Status:** ${initialStatus}
4439
+ **Priority:** ${input2.priority}
4440
+ **Assigned by:** ${input2.assignedBy}
4441
+ **Assigned to:** ${input2.assignedTo}
4442
+ **Project:** ${input2.projectName}
4443
+ **Created:** ${now.split("T")[0]}${parentTaskId ? `
4444
+ **Parent task:** ${parentTaskId}` : ""}
4445
+ **Reviewer:** ${reviewer}
4446
+
4447
+ ## Context
4448
+
4449
+ ${input2.context}
4450
+
4451
+ ## MANDATORY: When done
4452
+
4453
+ You MUST call update_task with status "done" and a result summary when finished.
4454
+ If you skip this, your reviewer will not know you're done and your work won't be reviewed.
4455
+ Do NOT let a failed commit or any error prevent you from calling update_task(done).
4456
+ `;
4457
+ await writeFile4(mdPath, mdContent, "utf-8");
4458
+ } catch {
4459
+ }
4460
+ }
4098
4461
  return {
4099
4462
  id,
4100
4463
  title: input2.title,
@@ -4287,7 +4650,7 @@ ${input2.result}` : `\u26A0\uFE0F ${warning}`;
4287
4650
  return { row, taskFile, now, taskId };
4288
4651
  }
4289
4652
  }
4290
- if (curStatus === "in_progress" && input2.callerAgentId && (input2.callerAgentId === assignedBy || input2.callerAgentId === "exe")) {
4653
+ if (curStatus === "in_progress" && input2.callerAgentId && (input2.callerAgentId === assignedBy || isCoordinatorName(input2.callerAgentId))) {
4291
4654
  process.stderr.write(
4292
4655
  `[tasks] Assigner override: ${input2.callerAgentId} reclaiming ${taskId}
4293
4656
  `
@@ -4399,12 +4762,22 @@ async function ensureGitignoreExe(baseDir) {
4399
4762
  } catch {
4400
4763
  }
4401
4764
  }
4402
- var DELEGATION_KEYWORDS, TASK_ALREADY_CLAIMED_PREFIX;
4765
+ var LANE_KEYWORDS, KEYWORD_INDEX, DELEGATION_KEYWORDS, TASK_ALREADY_CLAIMED_PREFIX;
4403
4766
  var init_tasks_crud = __esm({
4404
4767
  "src/lib/tasks-crud.ts"() {
4405
4768
  "use strict";
4406
4769
  init_database();
4407
4770
  init_task_scope();
4771
+ init_employees();
4772
+ LANE_KEYWORDS = {
4773
+ CMO: ["sales", "script", "pitch", "offer", "copy", "objection", "brand", "content", "seo", "marketing", "newsletter", "carousel", "social", "campaign"],
4774
+ CTO: ["spec", "architecture", "migration", "schema", "database", "design doc", "adr", "security audit", "tech stack"],
4775
+ "Principal Engineer": ["implement", "build", "fix", "commit", "refactor", "bug", "feature", "wire", "integration"],
4776
+ "Staff Code Reviewer": ["critique", "verdict", "review", "audit", "code quality"],
4777
+ "Content Production Specialist": ["render", "video", "image", "b-roll", "remotion", "animation", "thumbnail"],
4778
+ "AI Product Lead": ["competitive", "analysis", "benchmark", "compare", "scout", "evaluate", "poc"]
4779
+ };
4780
+ KEYWORD_INDEX = buildKeywordIndex();
4408
4781
  DELEGATION_KEYWORDS = /parallel|delegate|wave|worktree|multi-instance/i;
4409
4782
  TASK_ALREADY_CLAIMED_PREFIX = "TASK_ALREADY_CLAIMED";
4410
4783
  }
@@ -4434,7 +4807,7 @@ async function countNewPendingReviewsSince(sinceIso, sessionScope) {
4434
4807
  const result2 = await client.execute({
4435
4808
  sql: `SELECT COUNT(*) as cnt FROM tasks
4436
4809
  WHERE status = 'needs_review' AND updated_at > ?
4437
- AND (session_scope = ? OR session_scope IS NULL)`,
4810
+ AND session_scope = ?`,
4438
4811
  args: [sinceIso, sessionScope]
4439
4812
  });
4440
4813
  return Number(result2.rows[0]?.cnt) || 0;
@@ -4452,7 +4825,7 @@ async function listPendingReviews(limit, sessionScope) {
4452
4825
  const result2 = await client.execute({
4453
4826
  sql: `SELECT title, assigned_to, project_name FROM tasks
4454
4827
  WHERE status = 'needs_review'
4455
- AND (session_scope = ? OR session_scope IS NULL)
4828
+ AND session_scope = ?
4456
4829
  ORDER BY priority ASC, created_at DESC LIMIT ?`,
4457
4830
  args: [sessionScope, limit]
4458
4831
  });
@@ -4573,14 +4946,14 @@ async function cleanupReviewFile(row, taskFile, _baseDir) {
4573
4946
  if (parts.length >= 3 && parts[0] === "review") {
4574
4947
  const agent = parts[1];
4575
4948
  const slug = parts.slice(2).join("-");
4576
- const originalTaskFile = `exe/${agent}/${slug}.md`;
4949
+ const legacyTaskFile = `exe/${agent}/${slug}.md`;
4577
4950
  const result = await client.execute({
4578
- sql: "UPDATE tasks SET status = 'done', updated_at = ? WHERE task_file = ? AND status = 'needs_review'",
4579
- args: [now, originalTaskFile]
4951
+ sql: "UPDATE tasks SET status = 'done', updated_at = ? WHERE (task_file = ? OR task_file LIKE ?) AND status = 'needs_review'",
4952
+ args: [now, legacyTaskFile, `tasks/%/${agent}/${slug}.md`]
4580
4953
  });
4581
4954
  if (result.rowsAffected > 0) {
4582
4955
  process.stderr.write(
4583
- `[review-cleanup] Cascaded original task to done (legacy path): ${originalTaskFile}
4956
+ `[review-cleanup] Cascaded original task to done: ${agent}/${slug}.md
4584
4957
  `
4585
4958
  );
4586
4959
  }
@@ -4720,7 +5093,7 @@ function findSessionForProject(projectName) {
4720
5093
  const sessions = listSessions();
4721
5094
  for (const s of sessions) {
4722
5095
  const proj = s.projectDir.split("/").filter(Boolean).pop();
4723
- if (proj === projectName && (s.agentId === "exe" || isCoordinatorName(s.agentId))) return s;
5096
+ if (proj === projectName && isCoordinatorName(s.agentId)) return s;
4724
5097
  }
4725
5098
  return null;
4726
5099
  }
@@ -4766,7 +5139,7 @@ var init_session_scope = __esm({
4766
5139
 
4767
5140
  // src/lib/tasks-notify.ts
4768
5141
  async function dispatchTaskToEmployee(input2) {
4769
- if (input2.assignedTo === "exe" || isCoordinatorName(input2.assignedTo)) return { dispatched: "skipped" };
5142
+ if (isCoordinatorName(input2.assignedTo)) return { dispatched: "skipped" };
4770
5143
  let crossProject = false;
4771
5144
  if (input2.projectName) {
4772
5145
  try {
@@ -5245,7 +5618,7 @@ async function updateTask(input2) {
5245
5618
  }
5246
5619
  const isTerminal = input2.status === "done" || input2.status === "needs_review";
5247
5620
  if (isTerminal) {
5248
- const isCoordinator = String(row.assigned_to) === "exe" || isCoordinatorName(String(row.assigned_to));
5621
+ const isCoordinator = isCoordinatorName(String(row.assigned_to));
5249
5622
  if (!isCoordinator) {
5250
5623
  notifyTaskDone();
5251
5624
  }
@@ -5270,7 +5643,7 @@ async function updateTask(input2) {
5270
5643
  }
5271
5644
  }
5272
5645
  }
5273
- if (input2.status === "done" && String(row.assigned_to) !== "exe" && !isCoordinatorName(String(row.assigned_to)) && !process.env.VITEST) {
5646
+ if (input2.status === "done" && !isCoordinatorName(String(row.assigned_to)) && !process.env.VITEST) {
5274
5647
  Promise.resolve().then(() => (init_skill_learning(), skill_learning_exports)).then(
5275
5648
  ({ captureAndLearn: captureAndLearn2 }) => captureAndLearn2({
5276
5649
  taskId,
@@ -5286,7 +5659,7 @@ async function updateTask(input2) {
5286
5659
  });
5287
5660
  }
5288
5661
  let nextTask;
5289
- if (isTerminal && String(row.assigned_to) !== "exe" && !isCoordinatorName(String(row.assigned_to))) {
5662
+ if (isTerminal && !isCoordinatorName(String(row.assigned_to))) {
5290
5663
  try {
5291
5664
  nextTask = await findNextTask(String(row.assigned_to));
5292
5665
  } catch {
@@ -5556,6 +5929,7 @@ function detectError(data) {
5556
5929
  import { readdirSync, readFileSync, existsSync, statSync } from "fs";
5557
5930
  import { execSync } from "child_process";
5558
5931
  import path from "path";
5932
+ import os from "os";
5559
5933
  var STATUS_RE = /^\*\*Status:\*\*\s*(\w+)/m;
5560
5934
  var TITLE_RE = /^# (.+)/m;
5561
5935
 
@@ -5565,19 +5939,20 @@ init_project_name();
5565
5939
  // src/lib/store.ts
5566
5940
  init_memory();
5567
5941
  init_database();
5942
+ import { createHash } from "crypto";
5568
5943
 
5569
5944
  // src/lib/keychain.ts
5570
5945
  import { readFile as readFile3, writeFile as writeFile3, unlink, mkdir as mkdir3, chmod as chmod2 } from "fs/promises";
5571
- import { existsSync as existsSync4 } from "fs";
5572
- import path5 from "path";
5573
- import os3 from "os";
5946
+ import { existsSync as existsSync5 } from "fs";
5947
+ import path6 from "path";
5948
+ import os4 from "os";
5574
5949
  var SERVICE = "exe-mem";
5575
5950
  var ACCOUNT = "master-key";
5576
5951
  function getKeyDir() {
5577
- return process.env.EXE_OS_DIR ?? process.env.EXE_MEM_DIR ?? path5.join(os3.homedir(), ".exe-os");
5952
+ return process.env.EXE_OS_DIR ?? process.env.EXE_MEM_DIR ?? path6.join(os4.homedir(), ".exe-os");
5578
5953
  }
5579
5954
  function getKeyPath() {
5580
- return path5.join(getKeyDir(), "master.key");
5955
+ return path6.join(getKeyDir(), "master.key");
5581
5956
  }
5582
5957
  async function tryKeytar() {
5583
5958
  try {
@@ -5598,13 +5973,21 @@ async function getMasterKey() {
5598
5973
  }
5599
5974
  }
5600
5975
  const keyPath = getKeyPath();
5601
- if (!existsSync4(keyPath)) {
5976
+ if (!existsSync5(keyPath)) {
5977
+ process.stderr.write(
5978
+ `[keychain] Key not found at ${keyPath} (HOME=${os4.homedir()}, EXE_OS_DIR=${process.env.EXE_OS_DIR ?? "unset"})
5979
+ `
5980
+ );
5602
5981
  return null;
5603
5982
  }
5604
5983
  try {
5605
5984
  const content = await readFile3(keyPath, "utf-8");
5606
5985
  return Buffer.from(content.trim(), "base64");
5607
- } catch {
5986
+ } catch (err) {
5987
+ process.stderr.write(
5988
+ `[keychain] Key read failed at ${keyPath}: ${err instanceof Error ? err.message : String(err)}
5989
+ `
5990
+ );
5608
5991
  return null;
5609
5992
  }
5610
5993
  }
@@ -5693,12 +6076,52 @@ function classifyTier(record) {
5693
6076
  if (["store_memory", "manual"].includes(record.tool_name ?? "") && (record.importance ?? 0) >= 5) return 2;
5694
6077
  return 3;
5695
6078
  }
6079
+ function inferFilePaths(record) {
6080
+ if (!["Read", "Write", "Edit"].includes(record.tool_name)) return null;
6081
+ const firstLine = record.raw_text.split("\n")[0] ?? "";
6082
+ const match = firstLine.match(/(\/[\w./-]+\.\w+)/);
6083
+ return match ? JSON.stringify([match[1]]) : null;
6084
+ }
6085
+ function inferCommitHash(record) {
6086
+ if (record.tool_name !== "Bash") return null;
6087
+ const match = record.raw_text.match(/\b([a-f0-9]{7,40})\b/);
6088
+ return match ? match[1] : null;
6089
+ }
6090
+ function inferLanguageType(record) {
6091
+ const text = record.raw_text;
6092
+ if (!text || text.length < 10) return null;
6093
+ const trimmed = text.trimStart();
6094
+ if (trimmed.startsWith("{") || trimmed.startsWith("[")) return "json";
6095
+ if (/\b(SELECT|INSERT|UPDATE|DELETE|CREATE TABLE|ALTER TABLE)\b/i.test(text)) return "sql";
6096
+ if (/\b(function |const |import |export |class |def |async |=>)\b/.test(text)) return "code";
6097
+ if (trimmed.startsWith("#") || trimmed.startsWith("*")) return "prose";
6098
+ return "mixed";
6099
+ }
6100
+ function inferDomain(record) {
6101
+ const proj = (record.project_name ?? "").toLowerCase();
6102
+ if (proj.includes("marketing") || proj.includes("content")) return "marketing";
6103
+ if (proj.includes("crm") || proj.includes("customer")) return "customer";
6104
+ return null;
6105
+ }
5696
6106
  async function writeMemory(record) {
5697
6107
  if (record.vector !== null && record.vector.length !== EMBEDDING_DIM) {
5698
6108
  throw new Error(
5699
6109
  `Expected ${EMBEDDING_DIM}-dim vector, got ${record.vector.length}`
5700
6110
  );
5701
6111
  }
6112
+ const contentHash = createHash("md5").update(record.raw_text).digest("hex");
6113
+ if (_pendingRecords.some((r) => r.content_hash === contentHash && r.agent_id === record.agent_id)) {
6114
+ return;
6115
+ }
6116
+ try {
6117
+ const client = getClient();
6118
+ const existing = await client.execute({
6119
+ sql: "SELECT id FROM memories WHERE content_hash = ? AND agent_id = ? LIMIT 1",
6120
+ args: [contentHash, record.agent_id]
6121
+ });
6122
+ if (existing.rows.length > 0) return;
6123
+ } catch {
6124
+ }
5702
6125
  const dbRow = {
5703
6126
  id: record.id,
5704
6127
  agent_id: record.agent_id,
@@ -5728,7 +6151,23 @@ async function writeMemory(record) {
5728
6151
  supersedes_id: record.supersedes_id ?? null,
5729
6152
  draft: record.draft ? 1 : 0,
5730
6153
  memory_type: record.memory_type ?? "raw",
5731
- trajectory: record.trajectory ? JSON.stringify(record.trajectory) : null
6154
+ trajectory: record.trajectory ? JSON.stringify(record.trajectory) : null,
6155
+ content_hash: contentHash,
6156
+ intent: record.intent ?? null,
6157
+ outcome: record.outcome ?? null,
6158
+ domain: record.domain ?? inferDomain(record),
6159
+ referenced_entities: record.referenced_entities ?? null,
6160
+ retrieval_count: record.retrieval_count ?? 0,
6161
+ chain_position: record.chain_position ?? null,
6162
+ review_status: record.review_status ?? null,
6163
+ context_window_pct: record.context_window_pct ?? null,
6164
+ file_paths: record.file_paths ?? inferFilePaths(record),
6165
+ commit_hash: record.commit_hash ?? inferCommitHash(record),
6166
+ duration_ms: record.duration_ms ?? null,
6167
+ token_cost: record.token_cost ?? null,
6168
+ audience: record.audience ?? null,
6169
+ language_type: record.language_type ?? inferLanguageType(record),
6170
+ parent_memory_id: record.parent_memory_id ?? null
5732
6171
  };
5733
6172
  _pendingRecords.push(dbRow);
5734
6173
  orgBus.emit({
@@ -5786,80 +6225,85 @@ async function flushBatch() {
5786
6225
  const draft = row.draft ? 1 : 0;
5787
6226
  const memoryType = row.memory_type ?? "raw";
5788
6227
  const trajectory = row.trajectory ?? null;
5789
- return {
5790
- sql: hasVector ? `INSERT OR IGNORE INTO memories
5791
- (id, agent_id, agent_role, session_id, timestamp,
5792
- tool_name, project_name,
5793
- has_error, raw_text, vector, version, task_id, importance, status,
5794
- confidence, last_accessed,
5795
- workspace_id, document_id, user_id, char_offset, page_number,
5796
- source_path, source_type, tier, supersedes_id, draft, memory_type, trajectory)
5797
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, vector32(?), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` : `INSERT OR IGNORE INTO memories
5798
- (id, agent_id, agent_role, session_id, timestamp,
6228
+ const contentHash = row.content_hash ?? null;
6229
+ const intent = row.intent ?? null;
6230
+ const outcome = row.outcome ?? null;
6231
+ const domain = row.domain ?? null;
6232
+ const referencedEntities = row.referenced_entities ?? null;
6233
+ const retrievalCount = row.retrieval_count ?? 0;
6234
+ const chainPosition = row.chain_position ?? null;
6235
+ const reviewStatus = row.review_status ?? null;
6236
+ const contextWindowPct = row.context_window_pct ?? null;
6237
+ const filePaths = row.file_paths ?? null;
6238
+ const commitHash = row.commit_hash ?? null;
6239
+ const durationMs = row.duration_ms ?? null;
6240
+ const tokenCost = row.token_cost ?? null;
6241
+ const audience = row.audience ?? null;
6242
+ const languageType = row.language_type ?? null;
6243
+ const parentMemoryId = row.parent_memory_id ?? null;
6244
+ const cols = `id, agent_id, agent_role, session_id, timestamp,
5799
6245
  tool_name, project_name,
5800
6246
  has_error, raw_text, vector, version, task_id, importance, status,
5801
6247
  confidence, last_accessed,
5802
6248
  workspace_id, document_id, user_id, char_offset, page_number,
5803
- source_path, source_type, tier, supersedes_id, draft, memory_type, trajectory)
5804
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
5805
- args: hasVector ? [
5806
- row.id,
5807
- row.agent_id,
5808
- row.agent_role,
5809
- row.session_id,
5810
- row.timestamp,
5811
- row.tool_name,
5812
- row.project_name,
5813
- row.has_error,
5814
- row.raw_text,
5815
- vectorToBlob(row.vector),
5816
- row.version,
5817
- taskId,
5818
- importance,
5819
- status,
5820
- confidence,
5821
- lastAccessed,
5822
- workspaceId,
5823
- documentId,
5824
- userId,
5825
- charOffset,
5826
- pageNumber,
5827
- sourcePath,
5828
- sourceType,
5829
- tier,
5830
- supersedesId,
5831
- draft,
5832
- memoryType,
5833
- trajectory
5834
- ] : [
5835
- row.id,
5836
- row.agent_id,
5837
- row.agent_role,
5838
- row.session_id,
5839
- row.timestamp,
5840
- row.tool_name,
5841
- row.project_name,
5842
- row.has_error,
5843
- row.raw_text,
5844
- row.version,
5845
- taskId,
5846
- importance,
5847
- status,
5848
- confidence,
5849
- lastAccessed,
5850
- workspaceId,
5851
- documentId,
5852
- userId,
5853
- charOffset,
5854
- pageNumber,
5855
- sourcePath,
5856
- sourceType,
5857
- tier,
5858
- supersedesId,
5859
- draft,
5860
- memoryType,
5861
- trajectory
5862
- ]
6249
+ source_path, source_type, tier, supersedes_id, draft, memory_type, trajectory, content_hash,
6250
+ intent, outcome, domain, referenced_entities, retrieval_count,
6251
+ chain_position, review_status, context_window_pct, file_paths, commit_hash,
6252
+ duration_ms, token_cost, audience, language_type, parent_memory_id`;
6253
+ const metaArgs = [
6254
+ intent,
6255
+ outcome,
6256
+ domain,
6257
+ referencedEntities,
6258
+ retrievalCount,
6259
+ chainPosition,
6260
+ reviewStatus,
6261
+ contextWindowPct,
6262
+ filePaths,
6263
+ commitHash,
6264
+ durationMs,
6265
+ tokenCost,
6266
+ audience,
6267
+ languageType,
6268
+ parentMemoryId
6269
+ ];
6270
+ const baseArgs = [
6271
+ row.id,
6272
+ row.agent_id,
6273
+ row.agent_role,
6274
+ row.session_id,
6275
+ row.timestamp,
6276
+ row.tool_name,
6277
+ row.project_name,
6278
+ row.has_error,
6279
+ row.raw_text
6280
+ ];
6281
+ const sharedArgs = [
6282
+ row.version,
6283
+ taskId,
6284
+ importance,
6285
+ status,
6286
+ confidence,
6287
+ lastAccessed,
6288
+ workspaceId,
6289
+ documentId,
6290
+ userId,
6291
+ charOffset,
6292
+ pageNumber,
6293
+ sourcePath,
6294
+ sourceType,
6295
+ tier,
6296
+ supersedesId,
6297
+ draft,
6298
+ memoryType,
6299
+ trajectory,
6300
+ contentHash
6301
+ ];
6302
+ return {
6303
+ sql: hasVector ? `INSERT OR IGNORE INTO memories (${cols})
6304
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, vector32(?), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` : `INSERT OR IGNORE INTO memories (${cols})
6305
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
6306
+ args: hasVector ? [...baseArgs, vectorToBlob(row.vector), ...sharedArgs, ...metaArgs] : [...baseArgs, ...sharedArgs, ...metaArgs]
5863
6307
  };
5864
6308
  };
5865
6309
  const globalClient = getClient();