@askexenow/exe-os 0.8.83 → 0.8.86

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. package/dist/bin/backfill-conversations.js +746 -595
  2. package/dist/bin/backfill-responses.js +745 -594
  3. package/dist/bin/backfill-vectors.js +312 -226
  4. package/dist/bin/cleanup-stale-review-tasks.js +154 -21
  5. package/dist/bin/cli.js +14678 -12676
  6. package/dist/bin/exe-agent-config.js +242 -0
  7. package/dist/bin/exe-agent.js +100 -91
  8. package/dist/bin/exe-assign.js +1003 -854
  9. package/dist/bin/exe-boot.js +1420 -485
  10. package/dist/bin/exe-call.js +10 -0
  11. package/dist/bin/exe-cloud.js +29 -6
  12. package/dist/bin/exe-dispatch.js +572 -271
  13. package/dist/bin/exe-doctor.js +403 -6
  14. package/dist/bin/exe-export-behaviors.js +175 -72
  15. package/dist/bin/exe-forget.js +102 -3
  16. package/dist/bin/exe-gateway.js +796 -292
  17. package/dist/bin/exe-healthcheck.js +134 -1
  18. package/dist/bin/exe-heartbeat.js +172 -36
  19. package/dist/bin/exe-kill.js +175 -72
  20. package/dist/bin/exe-launch-agent.js +189 -76
  21. package/dist/bin/exe-link.js +927 -82
  22. package/dist/bin/exe-new-employee.js +60 -8
  23. package/dist/bin/exe-pending-messages.js +151 -19
  24. package/dist/bin/exe-pending-notifications.js +97 -2
  25. package/dist/bin/exe-pending-reviews.js +155 -22
  26. package/dist/bin/exe-rename.js +564 -23
  27. package/dist/bin/exe-review.js +231 -73
  28. package/dist/bin/exe-search.js +995 -228
  29. package/dist/bin/exe-session-cleanup.js +4930 -1664
  30. package/dist/bin/exe-settings.js +20 -5
  31. package/dist/bin/exe-start-codex.js +2598 -0
  32. package/dist/bin/exe-start.sh +15 -3
  33. package/dist/bin/exe-status.js +154 -21
  34. package/dist/bin/exe-team.js +97 -2
  35. package/dist/bin/git-sweep.js +1180 -363
  36. package/dist/bin/graph-backfill.js +175 -72
  37. package/dist/bin/graph-export.js +175 -72
  38. package/dist/bin/install.js +60 -7
  39. package/dist/bin/list-providers.js +1 -0
  40. package/dist/bin/scan-tasks.js +1185 -367
  41. package/dist/bin/setup.js +914 -270
  42. package/dist/bin/shard-migrate.js +175 -72
  43. package/dist/bin/update.js +1 -0
  44. package/dist/bin/wiki-sync.js +175 -72
  45. package/dist/gateway/index.js +792 -285
  46. package/dist/hooks/bug-report-worker.js +445 -135
  47. package/dist/hooks/commit-complete.js +1178 -361
  48. package/dist/hooks/error-recall.js +994 -228
  49. package/dist/hooks/ingest-worker.js +1799 -1234
  50. package/dist/hooks/ingest.js +3 -0
  51. package/dist/hooks/instructions-loaded.js +707 -97
  52. package/dist/hooks/notification.js +699 -89
  53. package/dist/hooks/post-compact.js +757 -109
  54. package/dist/hooks/pre-compact.js +1061 -244
  55. package/dist/hooks/pre-tool-use.js +787 -130
  56. package/dist/hooks/prompt-ingest-worker.js +242 -101
  57. package/dist/hooks/prompt-submit.js +1121 -299
  58. package/dist/hooks/response-ingest-worker.js +242 -101
  59. package/dist/hooks/session-end.js +4063 -397
  60. package/dist/hooks/session-start.js +1071 -254
  61. package/dist/hooks/stop.js +768 -120
  62. package/dist/hooks/subagent-stop.js +757 -109
  63. package/dist/hooks/summary-worker.js +1706 -1011
  64. package/dist/index.js +1821 -1098
  65. package/dist/lib/agent-config.js +167 -0
  66. package/dist/lib/cloud-sync.js +932 -88
  67. package/dist/lib/consolidation.js +2 -1
  68. package/dist/lib/database.js +642 -87
  69. package/dist/lib/db-daemon-client.js +503 -0
  70. package/dist/lib/device-registry.js +547 -7
  71. package/dist/lib/embedder.js +14 -28
  72. package/dist/lib/employee-templates.js +84 -74
  73. package/dist/lib/employees.js +9 -0
  74. package/dist/lib/exe-daemon-client.js +16 -29
  75. package/dist/lib/exe-daemon.js +2733 -1575
  76. package/dist/lib/hybrid-search.js +995 -228
  77. package/dist/lib/identity.js +87 -67
  78. package/dist/lib/keychain.js +9 -1
  79. package/dist/lib/messaging.js +103 -40
  80. package/dist/lib/reminders.js +91 -74
  81. package/dist/lib/runtime-table.js +16 -0
  82. package/dist/lib/schedules.js +96 -2
  83. package/dist/lib/session-wrappers.js +22 -0
  84. package/dist/lib/skill-learning.js +103 -85
  85. package/dist/lib/store.js +234 -73
  86. package/dist/lib/tasks.js +348 -134
  87. package/dist/lib/tmux-routing.js +422 -208
  88. package/dist/lib/token-spend.js +273 -0
  89. package/dist/lib/ws-client.js +11 -0
  90. package/dist/mcp/server.js +5742 -696
  91. package/dist/mcp/tools/complete-reminder.js +94 -77
  92. package/dist/mcp/tools/create-reminder.js +94 -77
  93. package/dist/mcp/tools/create-task.js +375 -152
  94. package/dist/mcp/tools/deactivate-behavior.js +95 -77
  95. package/dist/mcp/tools/list-reminders.js +94 -77
  96. package/dist/mcp/tools/list-tasks.js +99 -31
  97. package/dist/mcp/tools/send-message.js +108 -45
  98. package/dist/mcp/tools/update-task.js +162 -77
  99. package/dist/runtime/index.js +1075 -258
  100. package/dist/tui/App.js +1333 -506
  101. package/package.json +6 -1
  102. package/src/commands/exe/agent-config.md +27 -0
  103. package/src/commands/exe/cc-doctor.md +10 -0
@@ -59,34 +59,34 @@ var init_mcp_prefix = __esm({
59
59
  });
60
60
 
61
61
  // src/lib/project-name.ts
62
- import { execSync as execSync2 } from "child_process";
63
- import path2 from "path";
62
+ import { execSync } from "child_process";
63
+ import path from "path";
64
64
  function getProjectName(cwd) {
65
65
  const dir = cwd ?? process.cwd();
66
66
  if (_cached && _cachedCwd === dir) return _cached;
67
67
  try {
68
68
  let repoRoot;
69
69
  try {
70
- const gitCommonDir = execSync2("git rev-parse --path-format=absolute --git-common-dir", {
70
+ const gitCommonDir = execSync("git rev-parse --path-format=absolute --git-common-dir", {
71
71
  cwd: dir,
72
72
  encoding: "utf8",
73
73
  timeout: 2e3,
74
74
  stdio: ["pipe", "pipe", "pipe"]
75
75
  }).trim();
76
- repoRoot = path2.dirname(gitCommonDir);
76
+ repoRoot = path.dirname(gitCommonDir);
77
77
  } catch {
78
- repoRoot = execSync2("git rev-parse --show-toplevel", {
78
+ repoRoot = execSync("git rev-parse --show-toplevel", {
79
79
  cwd: dir,
80
80
  encoding: "utf8",
81
81
  timeout: 2e3,
82
82
  stdio: ["pipe", "pipe", "pipe"]
83
83
  }).trim();
84
84
  }
85
- _cached = path2.basename(repoRoot);
85
+ _cached = path.basename(repoRoot);
86
86
  _cachedCwd = dir;
87
87
  return _cached;
88
88
  } catch {
89
- _cached = path2.basename(dir);
89
+ _cached = path.basename(dir);
90
90
  _cachedCwd = dir;
91
91
  return _cached;
92
92
  }
@@ -181,15 +181,15 @@ __export(config_exports, {
181
181
  saveConfig: () => saveConfig
182
182
  });
183
183
  import { readFile, writeFile, mkdir, chmod } from "fs/promises";
184
- import { readFileSync as readFileSync2, existsSync as existsSync2, renameSync } from "fs";
185
- import path3 from "path";
184
+ import { readFileSync, existsSync, renameSync } from "fs";
185
+ import path2 from "path";
186
186
  import os 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");
192
- if (!existsSync2(newDir) && existsSync2(legacyDir)) {
190
+ const newDir = path2.join(os.homedir(), ".exe-os");
191
+ const legacyDir = path2.join(os.homedir(), ".exe-mem");
192
+ if (!existsSync(newDir) && existsSync(legacyDir)) {
193
193
  try {
194
194
  renameSync(legacyDir, newDir);
195
195
  process.stderr.write(`[exe-os] Migrated data directory: ~/.exe-mem \u2192 ~/.exe-os
@@ -253,9 +253,9 @@ function normalizeAutoUpdate(raw) {
253
253
  async function loadConfig() {
254
254
  const dir = process.env.EXE_OS_DIR ?? process.env.EXE_MEM_DIR ?? EXE_AI_DIR;
255
255
  await mkdir(dir, { recursive: true });
256
- const configPath = path3.join(dir, "config.json");
257
- if (!existsSync2(configPath)) {
258
- return { ...DEFAULT_CONFIG, dbPath: path3.join(dir, "memories.db") };
256
+ const configPath = path2.join(dir, "config.json");
257
+ if (!existsSync(configPath)) {
258
+ return { ...DEFAULT_CONFIG, dbPath: path2.join(dir, "memories.db") };
259
259
  }
260
260
  const raw = await readFile(configPath, "utf-8");
261
261
  try {
@@ -273,38 +273,38 @@ async function loadConfig() {
273
273
  normalizeScalingRoadmap(migratedCfg);
274
274
  normalizeSessionLifecycle(migratedCfg);
275
275
  normalizeAutoUpdate(migratedCfg);
276
- const config = { ...DEFAULT_CONFIG, dbPath: path3.join(dir, "memories.db"), ...migratedCfg };
276
+ const config = { ...DEFAULT_CONFIG, dbPath: path2.join(dir, "memories.db"), ...migratedCfg };
277
277
  if (config.dbPath.startsWith("~")) {
278
278
  config.dbPath = config.dbPath.replace(/^~/, os.homedir());
279
279
  }
280
280
  return config;
281
281
  } catch {
282
- return { ...DEFAULT_CONFIG, dbPath: path3.join(dir, "memories.db") };
282
+ return { ...DEFAULT_CONFIG, dbPath: path2.join(dir, "memories.db") };
283
283
  }
284
284
  }
285
285
  function loadConfigSync() {
286
286
  const dir = process.env.EXE_OS_DIR ?? process.env.EXE_MEM_DIR ?? EXE_AI_DIR;
287
- const configPath = path3.join(dir, "config.json");
288
- if (!existsSync2(configPath)) {
289
- return { ...DEFAULT_CONFIG, dbPath: path3.join(dir, "memories.db") };
287
+ const configPath = path2.join(dir, "config.json");
288
+ if (!existsSync(configPath)) {
289
+ return { ...DEFAULT_CONFIG, dbPath: path2.join(dir, "memories.db") };
290
290
  }
291
291
  try {
292
- const raw = readFileSync2(configPath, "utf-8");
292
+ const raw = readFileSync(configPath, "utf-8");
293
293
  let parsed = JSON.parse(raw);
294
294
  parsed = migrateLegacyConfig(parsed);
295
295
  const { config: migratedCfg } = migrateConfig(parsed);
296
296
  normalizeScalingRoadmap(migratedCfg);
297
297
  normalizeSessionLifecycle(migratedCfg);
298
298
  normalizeAutoUpdate(migratedCfg);
299
- return { ...DEFAULT_CONFIG, dbPath: path3.join(dir, "memories.db"), ...migratedCfg };
299
+ return { ...DEFAULT_CONFIG, dbPath: path2.join(dir, "memories.db"), ...migratedCfg };
300
300
  } catch {
301
- return { ...DEFAULT_CONFIG, dbPath: path3.join(dir, "memories.db") };
301
+ return { ...DEFAULT_CONFIG, dbPath: path2.join(dir, "memories.db") };
302
302
  }
303
303
  }
304
304
  async function saveConfig(config) {
305
305
  const dir = process.env.EXE_OS_DIR ?? process.env.EXE_MEM_DIR ?? EXE_AI_DIR;
306
306
  await mkdir(dir, { recursive: true });
307
- const configPath = path3.join(dir, "config.json");
307
+ const configPath = path2.join(dir, "config.json");
308
308
  await writeFile(configPath, JSON.stringify(config, null, 2) + "\n");
309
309
  if (config.cloud?.apiKey) {
310
310
  await chmod(configPath, 384);
@@ -329,10 +329,10 @@ var init_config = __esm({
329
329
  "src/lib/config.ts"() {
330
330
  "use strict";
331
331
  EXE_AI_DIR = resolveDataDir();
332
- DB_PATH = path3.join(EXE_AI_DIR, "memories.db");
333
- MODELS_DIR = path3.join(EXE_AI_DIR, "models");
334
- CONFIG_PATH = path3.join(EXE_AI_DIR, "config.json");
335
- LEGACY_LANCE_PATH = path3.join(EXE_AI_DIR, "local.lance");
332
+ DB_PATH = path2.join(EXE_AI_DIR, "memories.db");
333
+ MODELS_DIR = path2.join(EXE_AI_DIR, "models");
334
+ CONFIG_PATH = path2.join(EXE_AI_DIR, "config.json");
335
+ LEGACY_LANCE_PATH = path2.join(EXE_AI_DIR, "local.lance");
336
336
  CURRENT_CONFIG_VERSION = 1;
337
337
  DEFAULT_CONFIG = {
338
338
  config_version: CURRENT_CONFIG_VERSION,
@@ -406,9 +406,9 @@ var init_config = __esm({
406
406
 
407
407
  // src/lib/employees.ts
408
408
  import { readFile as readFile2, writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
409
- import { existsSync as existsSync3, symlinkSync, readlinkSync, readFileSync as readFileSync3, renameSync as renameSync2, unlinkSync, writeFileSync } from "fs";
410
- import { execSync as execSync3 } from "child_process";
411
- import path4 from "path";
409
+ import { existsSync as existsSync2, symlinkSync, readlinkSync, readFileSync as readFileSync2, renameSync as renameSync2, unlinkSync, writeFileSync } from "fs";
410
+ import { execSync as execSync2 } from "child_process";
411
+ import path3 from "path";
412
412
  import os2 from "os";
413
413
  function normalizeRole(role) {
414
414
  return (role ?? "").trim().toLowerCase();
@@ -430,9 +430,9 @@ function canCoordinate(agentName, agentRole, employees = loadEmployeesSync()) {
430
430
  return agentName === "default" || isCoordinatorRole(agentRole) || isCoordinatorName(agentName, employees);
431
431
  }
432
432
  function loadEmployeesSync(employeesPath = EMPLOYEES_PATH) {
433
- if (!existsSync3(employeesPath)) return [];
433
+ if (!existsSync2(employeesPath)) return [];
434
434
  try {
435
- return JSON.parse(readFileSync3(employeesPath, "utf-8"));
435
+ return JSON.parse(readFileSync2(employeesPath, "utf-8"));
436
436
  } catch {
437
437
  return [];
438
438
  }
@@ -451,395 +451,948 @@ var init_employees = __esm({
451
451
  "src/lib/employees.ts"() {
452
452
  "use strict";
453
453
  init_config();
454
- EMPLOYEES_PATH = path4.join(EXE_AI_DIR, "exe-employees.json");
454
+ EMPLOYEES_PATH = path3.join(EXE_AI_DIR, "exe-employees.json");
455
455
  DEFAULT_COORDINATOR_TEMPLATE_NAME = "exe";
456
456
  COORDINATOR_ROLE = "COO";
457
457
  MULTI_INSTANCE_ROLES = /* @__PURE__ */ new Set(["principal engineer", "content production specialist", "staff code reviewer"]);
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 existsSync3, unlinkSync as unlinkSync2, readFileSync as readFileSync3, openSync, closeSync, statSync } from "fs";
466
+ import path4 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 (existsSync3(PID_PATH)) {
495
+ try {
496
+ const pid = parseInt(readFileSync3(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 = path4.dirname(fileURLToPath(import.meta.url));
518
+ const { root } = path4.parse(dir);
519
+ while (dir !== root) {
520
+ if (existsSync3(path4.join(dir, "package.json"))) return dir;
521
+ dir = path4.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");
525
+ function spawnDaemon() {
526
+ const pkgRoot = findPackageRoot();
527
+ if (!pkgRoot) {
528
+ process.stderr.write("[exed-client] WARN: cannot find package root\n");
529
+ return;
530
+ }
531
+ const daemonPath = path4.join(pkgRoot, "dist", "lib", "exe-daemon.js");
532
+ if (!existsSync3(daemonPath)) {
533
+ process.stderr.write(`[exed-client] WARN: daemon script not found at ${daemonPath}
534
+ `);
535
+ return;
536
+ }
537
+ const resolvedPath = daemonPath;
538
+ process.stderr.write(`[exed-client] Spawning daemon: ${resolvedPath}
539
+ `);
540
+ const logPath = path4.join(path4.dirname(SOCKET_PATH), "exed.log");
541
+ let stderrFd = "ignore";
509
542
  try {
510
- await client.execute("PRAGMA libsql_vector_search_ef = 128");
543
+ stderrFd = openSync(logPath, "a");
511
544
  } catch {
512
545
  }
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
- }
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 {
626
564
  }
627
- } catch {
628
565
  }
566
+ }
567
+ function acquireSpawnLock() {
629
568
  try {
630
- await client.execute({
631
- sql: `ALTER TABLE behaviors ADD COLUMN priority TEXT DEFAULT 'p1'`,
632
- args: []
633
- });
634
- } catch {
635
- }
636
- try {
637
- await client.execute({
638
- sql: `ALTER TABLE tasks ADD COLUMN blocked_by TEXT`,
639
- args: []
640
- });
641
- } catch {
642
- }
643
- 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 = statSync(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 (existsSync3(PID_PATH)) {
691
+ try {
692
+ const pid = parseInt(readFileSync3(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" });
749
769
  }
750
- 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
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 ?? path4.join(EXE_AI_DIR, "exed.sock");
780
+ PID_PATH = process.env.EXE_DAEMON_PID ?? process.env.EXE_EMBED_PID ?? path4.join(EXE_AI_DIR, "exed.pid");
781
+ SPAWN_LOCK_PATH = path4.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)
756
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
+ });
757
858
 
758
- CREATE INDEX IF NOT EXISTS idx_consolidations_source
759
- ON consolidations(source_memory_id);
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
+ });
760
993
 
761
- CREATE INDEX IF NOT EXISTS idx_consolidations_consolidated
762
- ON consolidations(consolidated_memory_id);
763
- `);
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 {
1065
+ }
764
1066
  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
-
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
+
843
1396
  CREATE INDEX IF NOT EXISTS idx_messages_conversation_order
844
1397
  ON messages(target_agent, from_agent, server_seq);
845
1398
  `);
@@ -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,7 +1796,34 @@ async function ensureSchema() {
1214
1796
  CREATE INDEX IF NOT EXISTS idx_conversations_channel
1215
1797
  ON conversations(channel_id);
1216
1798
  `);
1217
- try {
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
+ }
1826
+ try {
1218
1827
  await client.execute({
1219
1828
  sql: `ALTER TABLE tasks ADD COLUMN budget_tokens INTEGER`,
1220
1829
  args: []
@@ -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
  }
@@ -1437,7 +2073,7 @@ __export(shard_manager_exports, {
1437
2073
  shardExists: () => shardExists
1438
2074
  });
1439
2075
  import path6 from "path";
1440
- import { existsSync as existsSync5, mkdirSync, readdirSync as readdirSync2 } from "fs";
2076
+ import { existsSync as existsSync5, mkdirSync, readdirSync } from "fs";
1441
2077
  import { createClient as createClient2 } from "@libsql/client";
1442
2078
  function initShardManager(encryptionKey) {
1443
2079
  _encryptionKey = encryptionKey;
@@ -1476,7 +2112,7 @@ function shardExists(projectName) {
1476
2112
  }
1477
2113
  function listShards() {
1478
2114
  if (!existsSync5(SHARDS_DIR)) return [];
1479
- return readdirSync2(SHARDS_DIR).filter((f) => f.endsWith(".db")).map((f) => f.replace(".db", ""));
2115
+ return readdirSync(SHARDS_DIR).filter((f) => f.endsWith(".db")).map((f) => f.replace(".db", ""));
1480
2116
  }
1481
2117
  async function ensureShardSchema(client) {
1482
2118
  await client.execute("PRAGMA journal_mode = WAL");
@@ -1786,7 +2422,7 @@ __export(global_procedures_exports, {
1786
2422
  loadGlobalProcedures: () => loadGlobalProcedures,
1787
2423
  storeGlobalProcedure: () => storeGlobalProcedure
1788
2424
  });
1789
- import { randomUUID } from "crypto";
2425
+ import { randomUUID as randomUUID2 } from "crypto";
1790
2426
  async function loadGlobalProcedures() {
1791
2427
  const client = getClient();
1792
2428
  const result = await client.execute({
@@ -1810,768 +2446,423 @@ function getGlobalProceduresBlock() {
1810
2446
  if (_cacheLoaded && _customerCache) sections.push(_customerCache);
1811
2447
  if (sections.length === 0) return "";
1812
2448
  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;
2133
- }
2134
- });
2135
-
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();
2160
- }
2161
- }
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
- };
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 path7 from "path";
2491
+ import os4 from "os";
2492
+ import {
2493
+ readFileSync as readFileSync4,
2494
+ readdirSync as readdirSync2,
2495
+ unlinkSync as unlinkSync3,
2496
+ existsSync as existsSync6,
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 readFileSync5, writeFileSync as writeFileSync2, existsSync as existsSync7, 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
- }
2543
+ import path8 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) });
2261
2551
  }
2262
2552
  }
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 {
2553
+ function loadDeviceId() {
2554
+ const deviceJsonPath = path8.join(EXE_AI_DIR, "device.json");
2555
+ try {
2556
+ if (existsSync7(deviceJsonPath)) {
2557
+ const data = JSON.parse(readFileSync5(deviceJsonPath, "utf8"));
2558
+ if (data.deviceId) return data.deviceId;
2279
2559
  }
2280
- try {
2281
- unlinkSync3(SOCKET_PATH);
2282
- } catch {
2560
+ } catch {
2561
+ }
2562
+ try {
2563
+ if (existsSync7(DEVICE_ID_PATH)) {
2564
+ const id2 = readFileSync5(DEVICE_ID_PATH, "utf8").trim();
2565
+ if (id2) return id2;
2283
2566
  }
2567
+ } catch {
2284
2568
  }
2569
+ const id = randomUUID3();
2570
+ mkdirSync2(EXE_AI_DIR, { recursive: true });
2571
+ writeFileSync2(DEVICE_ID_PATH, id, "utf8");
2572
+ return id;
2285
2573
  }
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);
2574
+ function loadLicense() {
2575
+ try {
2576
+ if (!existsSync7(LICENSE_PATH)) return null;
2577
+ return readFileSync5(LICENSE_PATH, "utf8").trim();
2578
+ } catch {
2579
+ return null;
2292
2580
  }
2293
- return null;
2294
2581
  }
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";
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) {
2312
2587
  try {
2313
- stderrFd = openSync(logPath, "a");
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
+ };
2314
2604
  } catch {
2605
+ return null;
2315
2606
  }
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 {
2334
- }
2607
+ }
2608
+ async function getCachedLicense() {
2609
+ try {
2610
+ if (!existsSync7(CACHE_PATH)) return null;
2611
+ const raw = JSON.parse(readFileSync5(CACHE_PATH, "utf8"));
2612
+ if (!raw.token || typeof raw.token !== "string") return null;
2613
+ return await verifyLicenseJwt(raw.token);
2614
+ } catch {
2615
+ return null;
2335
2616
  }
2336
2617
  }
2337
- function acquireSpawnLock() {
2618
+ function readCachedToken() {
2338
2619
  try {
2339
- const fd = openSync(SPAWN_LOCK_PATH, "wx");
2340
- closeSync(fd);
2341
- return true;
2620
+ if (!existsSync7(CACHE_PATH)) return null;
2621
+ const raw = JSON.parse(readFileSync5(CACHE_PATH, "utf8"));
2622
+ return typeof raw.token === "string" ? raw.token : null;
2342
2623
  } 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;
2624
+ return null;
2360
2625
  }
2361
2626
  }
2362
- function releaseSpawnLock() {
2627
+ function getRawCachedPlan() {
2363
2628
  try {
2364
- unlinkSync3(SPAWN_LOCK_PATH);
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
+ };
2365
2649
  } catch {
2650
+ return null;
2366
2651
  }
2367
2652
  }
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
- });
2405
- }
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
- }
2416
- }
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);
2653
+ function cacheResponse(token) {
2654
+ try {
2655
+ writeFileSync2(CACHE_PATH, JSON.stringify({ token }), "utf8");
2656
+ } catch {
2423
2657
  }
2424
- return false;
2425
- }
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
- });
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: statSync2 } = __require("fs");
2709
+ const s = statSync2(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 = path8.join(EXE_AI_DIR, "config.json");
2720
+ if (existsSync7(configPath)) {
2721
+ const raw = JSON.parse(readFileSync5(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 = path8.join(EXE_AI_DIR, "license.key");
2743
+ CACHE_PATH = path8.join(EXE_AI_DIR, "license-cache.json");
2744
+ DEVICE_ID_PATH = path8.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 readFileSync6, existsSync as existsSync8 } from "fs";
2774
+ import path9 from "path";
2775
+ function getLicenseSync() {
2498
2776
  try {
2499
- unlinkSync3(SOCKET_PATH);
2777
+ if (!existsSync8(CACHE_PATH2)) return freeLicense();
2778
+ const raw = JSON.parse(readFileSync6(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 (existsSync8(filePath)) {
2836
+ const raw = readFileSync6(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 = path9.join(EXE_AI_DIR, "license-cache.json");
2575
2866
  }
2576
2867
  });
2577
2868
 
@@ -2644,12 +2935,12 @@ var init_embedder = __esm({
2644
2935
  });
2645
2936
 
2646
2937
  // src/lib/session-registry.ts
2647
- import { readFileSync as readFileSync8, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3, existsSync as existsSync10 } from "fs";
2648
- import path11 from "path";
2938
+ import { readFileSync as readFileSync7, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3, existsSync as existsSync9 } from "fs";
2939
+ import path10 from "path";
2649
2940
  import os5 from "os";
2650
2941
  function registerSession(entry) {
2651
- const dir = path11.dirname(REGISTRY_PATH);
2652
- if (!existsSync10(dir)) {
2942
+ const dir = path10.dirname(REGISTRY_PATH);
2943
+ if (!existsSync9(dir)) {
2653
2944
  mkdirSync3(dir, { recursive: true });
2654
2945
  }
2655
2946
  const sessions = listSessions();
@@ -2663,7 +2954,7 @@ function registerSession(entry) {
2663
2954
  }
2664
2955
  function listSessions() {
2665
2956
  try {
2666
- const raw = readFileSync8(REGISTRY_PATH, "utf8");
2957
+ const raw = readFileSync7(REGISTRY_PATH, "utf8");
2667
2958
  return JSON.parse(raw);
2668
2959
  } catch {
2669
2960
  return [];
@@ -2673,18 +2964,18 @@ 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 = path10.join(os5.homedir(), ".exe-os", "session-registry.json");
2677
2968
  }
2678
2969
  });
2679
2970
 
2680
2971
  // src/lib/session-key.ts
2681
- import { execSync as execSync4 } from "child_process";
2972
+ import { execSync as execSync3 } from "child_process";
2682
2973
  function getSessionKey() {
2683
2974
  if (_cached2) return _cached2;
2684
2975
  let pid = process.ppid;
2685
2976
  for (let i = 0; i < 10; i++) {
2686
2977
  try {
2687
- const info = execSync4(`ps -p ${pid} -o ppid=,comm=`, {
2978
+ const info = execSync3(`ps -p ${pid} -o ppid=,comm=`, {
2688
2979
  encoding: "utf8",
2689
2980
  timeout: 2e3
2690
2981
  }).trim();
@@ -2820,14 +3111,14 @@ var init_transport = __esm({
2820
3111
  });
2821
3112
 
2822
3113
  // src/lib/cc-agent-support.ts
2823
- import { execSync as execSync5 } from "child_process";
3114
+ import { execSync as execSync4 } from "child_process";
2824
3115
  function _resetCcAgentSupportCache() {
2825
3116
  _cachedSupport = null;
2826
3117
  }
2827
3118
  function claudeSupportsAgentFlag() {
2828
3119
  if (_cachedSupport !== null) return _cachedSupport;
2829
3120
  try {
2830
- const helpOutput = execSync5("claude --help 2>&1", {
3121
+ const helpOutput = execSync4("claude --help 2>&1", {
2831
3122
  encoding: "utf-8",
2832
3123
  timeout: 5e3
2833
3124
  });
@@ -2869,13 +3160,64 @@ var init_provider_table = __esm({
2869
3160
  }
2870
3161
  });
2871
3162
 
3163
+ // src/lib/runtime-table.ts
3164
+ var RUNTIME_TABLE, DEFAULT_RUNTIME;
3165
+ var init_runtime_table = __esm({
3166
+ "src/lib/runtime-table.ts"() {
3167
+ "use strict";
3168
+ RUNTIME_TABLE = {
3169
+ codex: {
3170
+ binary: "codex",
3171
+ launchMode: "exec",
3172
+ autoApproveFlag: "--full-auto",
3173
+ inlineFlag: "--no-alt-screen",
3174
+ apiKeyEnv: "OPENAI_API_KEY",
3175
+ defaultModel: "gpt-5.4"
3176
+ }
3177
+ };
3178
+ DEFAULT_RUNTIME = "claude";
3179
+ }
3180
+ });
3181
+
3182
+ // src/lib/agent-config.ts
3183
+ import { readFileSync as readFileSync8, writeFileSync as writeFileSync4, existsSync as existsSync10, mkdirSync as mkdirSync4 } from "fs";
3184
+ import path11 from "path";
3185
+ function loadAgentConfig() {
3186
+ if (!existsSync10(AGENT_CONFIG_PATH)) return {};
3187
+ try {
3188
+ return JSON.parse(readFileSync8(AGENT_CONFIG_PATH, "utf-8"));
3189
+ } catch {
3190
+ return {};
3191
+ }
3192
+ }
3193
+ function getAgentRuntime(agentId) {
3194
+ const config = loadAgentConfig();
3195
+ const entry = config[agentId];
3196
+ if (entry) return entry;
3197
+ return { runtime: DEFAULT_RUNTIME, model: DEFAULT_MODELS[DEFAULT_RUNTIME] };
3198
+ }
3199
+ var AGENT_CONFIG_PATH, DEFAULT_MODELS;
3200
+ var init_agent_config = __esm({
3201
+ "src/lib/agent-config.ts"() {
3202
+ "use strict";
3203
+ init_config();
3204
+ init_runtime_table();
3205
+ AGENT_CONFIG_PATH = path11.join(EXE_AI_DIR, "agent-config.json");
3206
+ DEFAULT_MODELS = {
3207
+ claude: "claude-opus-4",
3208
+ codex: RUNTIME_TABLE.codex?.defaultModel ?? "gpt-5.4",
3209
+ opencode: "minimax-m2.7"
3210
+ };
3211
+ }
3212
+ });
3213
+
2872
3214
  // src/lib/intercom-queue.ts
2873
- import { readFileSync as readFileSync9, writeFileSync as writeFileSync4, renameSync as renameSync3, existsSync as existsSync11, mkdirSync as mkdirSync4 } from "fs";
3215
+ import { readFileSync as readFileSync9, writeFileSync as writeFileSync5, renameSync as renameSync3, existsSync as existsSync11, mkdirSync as mkdirSync5 } from "fs";
2874
3216
  import path12 from "path";
2875
3217
  import os6 from "os";
2876
3218
  function ensureDir() {
2877
3219
  const dir = path12.dirname(QUEUE_PATH);
2878
- if (!existsSync11(dir)) mkdirSync4(dir, { recursive: true });
3220
+ if (!existsSync11(dir)) mkdirSync5(dir, { recursive: true });
2879
3221
  }
2880
3222
  function readQueue() {
2881
3223
  try {
@@ -2888,7 +3230,7 @@ function readQueue() {
2888
3230
  function writeQueue(queue) {
2889
3231
  ensureDir();
2890
3232
  const tmp = `${QUEUE_PATH}.tmp`;
2891
- writeFileSync4(tmp, JSON.stringify(queue, null, 2));
3233
+ writeFileSync5(tmp, JSON.stringify(queue, null, 2));
2892
3234
  renameSync3(tmp, QUEUE_PATH);
2893
3235
  }
2894
3236
  function queueIntercom(targetSession, reason) {
@@ -3261,8 +3603,8 @@ __export(tmux_routing_exports, {
3261
3603
  spawnEmployee: () => spawnEmployee,
3262
3604
  verifyPaneAtCapacity: () => verifyPaneAtCapacity
3263
3605
  });
3264
- import { execFileSync as execFileSync2, execSync as execSync6 } from "child_process";
3265
- import { readFileSync as readFileSync10, writeFileSync as writeFileSync5, mkdirSync as mkdirSync5, existsSync as existsSync12, appendFileSync } from "fs";
3606
+ import { execFileSync as execFileSync2, execSync as execSync5 } from "child_process";
3607
+ import { readFileSync as readFileSync10, writeFileSync as writeFileSync6, mkdirSync as mkdirSync6, existsSync as existsSync12, appendFileSync } from "fs";
3266
3608
  import path13 from "path";
3267
3609
  import os7 from "os";
3268
3610
  import { fileURLToPath as fileURLToPath2 } from "url";
@@ -3280,7 +3622,7 @@ function isProcessAlive(pid) {
3280
3622
  }
3281
3623
  function acquireSpawnLock2(sessionName) {
3282
3624
  if (!existsSync12(SPAWN_LOCK_DIR)) {
3283
- mkdirSync5(SPAWN_LOCK_DIR, { recursive: true });
3625
+ mkdirSync6(SPAWN_LOCK_DIR, { recursive: true });
3284
3626
  }
3285
3627
  const lockFile = spawnLockPath(sessionName);
3286
3628
  if (existsSync12(lockFile)) {
@@ -3293,7 +3635,7 @@ function acquireSpawnLock2(sessionName) {
3293
3635
  } catch {
3294
3636
  }
3295
3637
  }
3296
- writeFileSync5(lockFile, JSON.stringify({ pid: process.pid, timestamp: Date.now() }));
3638
+ writeFileSync6(lockFile, JSON.stringify({ pid: process.pid, timestamp: Date.now() }));
3297
3639
  return true;
3298
3640
  }
3299
3641
  function releaseSpawnLock2(sessionName) {
@@ -3378,11 +3720,11 @@ function extractRootExe(name) {
3378
3720
  }
3379
3721
  function registerParentExe(sessionKey, parentExe, dispatchedBy) {
3380
3722
  if (!existsSync12(SESSION_CACHE)) {
3381
- mkdirSync5(SESSION_CACHE, { recursive: true });
3723
+ mkdirSync6(SESSION_CACHE, { recursive: true });
3382
3724
  }
3383
3725
  const rootExe = extractRootExe(parentExe) ?? parentExe;
3384
3726
  const filePath = path13.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`);
3385
- writeFileSync5(filePath, JSON.stringify({
3727
+ writeFileSync6(filePath, JSON.stringify({
3386
3728
  parentExe: rootExe,
3387
3729
  dispatchedBy: dispatchedBy || rootExe,
3388
3730
  registeredAt: (/* @__PURE__ */ new Date()).toISOString()
@@ -3461,31 +3803,49 @@ async function verifyPaneAtCapacity(sessionName) {
3461
3803
  function readDebounceState() {
3462
3804
  try {
3463
3805
  if (!existsSync12(DEBOUNCE_FILE)) return {};
3464
- return JSON.parse(readFileSync10(DEBOUNCE_FILE, "utf8"));
3806
+ const raw = JSON.parse(readFileSync10(DEBOUNCE_FILE, "utf8"));
3807
+ const state = {};
3808
+ for (const [key, val] of Object.entries(raw)) {
3809
+ if (typeof val === "number") {
3810
+ state[key] = { lastSent: val, pending: 0 };
3811
+ } else if (val && typeof val === "object" && "lastSent" in val) {
3812
+ state[key] = val;
3813
+ }
3814
+ }
3815
+ return state;
3465
3816
  } catch {
3466
3817
  return {};
3467
3818
  }
3468
3819
  }
3469
3820
  function writeDebounceState(state) {
3470
3821
  try {
3471
- if (!existsSync12(SESSION_CACHE)) mkdirSync5(SESSION_CACHE, { recursive: true });
3472
- writeFileSync5(DEBOUNCE_FILE, JSON.stringify(state));
3822
+ if (!existsSync12(SESSION_CACHE)) mkdirSync6(SESSION_CACHE, { recursive: true });
3823
+ writeFileSync6(DEBOUNCE_FILE, JSON.stringify(state));
3473
3824
  } catch {
3474
3825
  }
3475
3826
  }
3476
3827
  function isDebounced(targetSession) {
3477
3828
  const state = readDebounceState();
3478
- const lastSent = state[targetSession] ?? 0;
3479
- return Date.now() - lastSent < INTERCOM_DEBOUNCE_MS;
3829
+ const entry = state[targetSession];
3830
+ const lastSent = entry?.lastSent ?? 0;
3831
+ if (Date.now() - lastSent < INTERCOM_DEBOUNCE_MS) {
3832
+ if (!state[targetSession]) state[targetSession] = { lastSent, pending: 0 };
3833
+ state[targetSession].pending++;
3834
+ writeDebounceState(state);
3835
+ return true;
3836
+ }
3837
+ return false;
3480
3838
  }
3481
3839
  function recordDebounce(targetSession) {
3482
3840
  const state = readDebounceState();
3483
- state[targetSession] = Date.now();
3841
+ const batched = state[targetSession]?.pending ?? 0;
3842
+ state[targetSession] = { lastSent: Date.now(), pending: 0 };
3484
3843
  const cutoff = Date.now() - DEBOUNCE_CLEANUP_AGE_MS;
3485
3844
  for (const key of Object.keys(state)) {
3486
- if ((state[key] ?? 0) < cutoff) delete state[key];
3845
+ if ((state[key]?.lastSent ?? 0) < cutoff) delete state[key];
3487
3846
  }
3488
3847
  writeDebounceState(state);
3848
+ return batched;
3489
3849
  }
3490
3850
  function logIntercom(msg) {
3491
3851
  const line = `[${(/* @__PURE__ */ new Date()).toISOString()}] ${msg}
@@ -3530,7 +3890,7 @@ function sendIntercom(targetSession) {
3530
3890
  return "skipped_exe";
3531
3891
  }
3532
3892
  if (isDebounced(targetSession)) {
3533
- logIntercom(`DEBOUNCE \u2192 ${targetSession} (cross-process file debounce)`);
3893
+ logIntercom(`DEBOUNCE \u2192 ${targetSession} (nudge batched, task safe in DB)`);
3534
3894
  return "debounced";
3535
3895
  }
3536
3896
  try {
@@ -3542,14 +3902,14 @@ function sendIntercom(targetSession) {
3542
3902
  const sessionState = getSessionState(targetSession);
3543
3903
  if (sessionState === "no_claude") {
3544
3904
  queueIntercom(targetSession, "claude not running in session");
3545
- recordDebounce(targetSession);
3546
- logIntercom(`QUEUED \u2192 ${targetSession} (no claude process \u2014 raw shell detected)`);
3905
+ const batched2 = recordDebounce(targetSession);
3906
+ logIntercom(`QUEUED \u2192 ${targetSession} (no claude process)${batched2 > 0 ? ` [${batched2} batched]` : ""}`);
3547
3907
  return "queued";
3548
3908
  }
3549
3909
  if (sessionState === "thinking" || sessionState === "tool") {
3550
3910
  queueIntercom(targetSession, "session busy at send time");
3551
- recordDebounce(targetSession);
3552
- logIntercom(`QUEUED \u2192 ${targetSession} (session busy, will retry from queue)`);
3911
+ const batched2 = recordDebounce(targetSession);
3912
+ logIntercom(`QUEUED \u2192 ${targetSession} (session busy)${batched2 > 0 ? ` [${batched2} batched]` : ""}`);
3553
3913
  return "queued";
3554
3914
  }
3555
3915
  if (transport.isPaneInCopyMode(targetSession)) {
@@ -3557,8 +3917,8 @@ function sendIntercom(targetSession) {
3557
3917
  transport.sendKeys(targetSession, "q");
3558
3918
  }
3559
3919
  transport.sendKeys(targetSession, "/exe-intercom");
3560
- recordDebounce(targetSession);
3561
- logIntercom(`DELIVERED \u2192 ${targetSession} (fire-and-forget)`);
3920
+ const batched = recordDebounce(targetSession);
3921
+ logIntercom(`DELIVERED \u2192 ${targetSession}${batched > 0 ? ` [${batched} nudges batched during debounce]` : ""} (fire-and-forget)`);
3562
3922
  return "delivered";
3563
3923
  } catch {
3564
3924
  logIntercom(`FAIL \u2192 ${targetSession}`);
@@ -3588,7 +3948,7 @@ function notifyParentExe(sessionKey) {
3588
3948
  return true;
3589
3949
  }
3590
3950
  function ensureEmployee(employeeName, exeSession, projectDir, opts) {
3591
- if (employeeName === "exe" || isCoordinatorName(employeeName)) {
3951
+ if (isCoordinatorName(employeeName)) {
3592
3952
  return { status: "failed", sessionName: "", error: "The COO is not a dispatchable employee" };
3593
3953
  }
3594
3954
  try {
@@ -3663,7 +4023,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
3663
4023
  const logDir = path13.join(os7.homedir(), ".exe-os", "session-logs");
3664
4024
  const logFile = path13.join(logDir, `${instanceLabel}-${Date.now()}.log`);
3665
4025
  if (!existsSync12(logDir)) {
3666
- mkdirSync5(logDir, { recursive: true });
4026
+ mkdirSync6(logDir, { recursive: true });
3667
4027
  }
3668
4028
  transport.kill(sessionName);
3669
4029
  let cleanupSuffix = "";
@@ -3687,7 +4047,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
3687
4047
  const trustDir = opts?.cwd ?? projectDir;
3688
4048
  if (!projects[trustDir]) projects[trustDir] = {};
3689
4049
  projects[trustDir].hasTrustDialogAccepted = true;
3690
- writeFileSync5(claudeJsonPath, JSON.stringify(claudeJson, null, 2) + "\n");
4050
+ writeFileSync6(claudeJsonPath, JSON.stringify(claudeJson, null, 2) + "\n");
3691
4051
  } catch {
3692
4052
  }
3693
4053
  try {
@@ -3725,14 +4085,17 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
3725
4085
  if (changed) {
3726
4086
  perms.allow = allow;
3727
4087
  settings.permissions = perms;
3728
- mkdirSync5(projSettingsDir, { recursive: true });
3729
- writeFileSync5(settingsPath, JSON.stringify(settings, null, 2) + "\n");
4088
+ mkdirSync6(projSettingsDir, { recursive: true });
4089
+ writeFileSync6(settingsPath, JSON.stringify(settings, null, 2) + "\n");
3730
4090
  }
3731
4091
  } catch {
3732
4092
  }
3733
4093
  const spawnCwd = opts?.cwd ?? projectDir;
3734
4094
  const useExeAgent = !!(opts?.model && opts?.provider);
3735
- const ccProvider = useExeAgent ? DEFAULT_PROVIDER : detectActiveProvider();
4095
+ const agentRtConfig = getAgentRuntime(employeeName);
4096
+ const useCodex = !useExeAgent && agentRtConfig.runtime === "codex";
4097
+ const useOpencode = !useExeAgent && !useCodex && agentRtConfig.runtime === "opencode";
4098
+ const ccProvider = useExeAgent || useCodex || useOpencode ? DEFAULT_PROVIDER : detectActiveProvider();
3736
4099
  const useBinSymlink = ccProvider !== DEFAULT_PROVIDER;
3737
4100
  let identityFlag = "";
3738
4101
  let behaviorsFlag = "";
@@ -3770,7 +4133,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
3770
4133
  let sessionContextFlag = "";
3771
4134
  try {
3772
4135
  const ctxDir = path13.join(os7.homedir(), ".exe-os", "session-cache");
3773
- mkdirSync5(ctxDir, { recursive: true });
4136
+ mkdirSync6(ctxDir, { recursive: true });
3774
4137
  const ctxFile = path13.join(ctxDir, `session-context-${sessionName}.md`);
3775
4138
  const ctxContent = [
3776
4139
  `## Session Context`,
@@ -3778,7 +4141,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
3778
4141
  `Your parent coordinator session is ${exeSession}.`,
3779
4142
  `Your employees (if any) use the -${exeSession} suffix.`
3780
4143
  ].join("\n");
3781
- writeFileSync5(ctxFile, ctxContent);
4144
+ writeFileSync6(ctxFile, ctxContent);
3782
4145
  sessionContextFlag = ` --append-system-prompt-file ${ctxFile}`;
3783
4146
  } catch {
3784
4147
  }
@@ -3792,9 +4155,48 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
3792
4155
  }
3793
4156
  }
3794
4157
  }
4158
+ if (useCodex) {
4159
+ const codexCfg = RUNTIME_TABLE.codex;
4160
+ if (codexCfg?.apiKeyEnv) {
4161
+ const keyVal = process.env[codexCfg.apiKeyEnv];
4162
+ if (keyVal) {
4163
+ envPrefix = `${envPrefix} ${codexCfg.apiKeyEnv}=${keyVal}`;
4164
+ }
4165
+ }
4166
+ envPrefix = `${envPrefix} EXE_AGENT_MODEL=${agentRtConfig.model}`;
4167
+ }
4168
+ if (useOpencode) {
4169
+ const ocCfg = PROVIDER_TABLE.opencode;
4170
+ if (ocCfg?.apiKeyEnv) {
4171
+ const keyVal = process.env[ocCfg.apiKeyEnv];
4172
+ if (keyVal) {
4173
+ envPrefix = `${envPrefix} ${ocCfg.apiKeyEnv}=${keyVal}`;
4174
+ }
4175
+ }
4176
+ envPrefix = `${envPrefix} ANTHROPIC_MODEL=${agentRtConfig.model}`;
4177
+ }
4178
+ if (!useExeAgent && !useCodex && !useOpencode && !useBinSymlink) {
4179
+ const defaultClaudeModel = DEFAULT_MODELS.claude;
4180
+ if (agentRtConfig.runtime === "claude" && agentRtConfig.model !== defaultClaudeModel) {
4181
+ envPrefix = `${envPrefix} ANTHROPIC_MODEL=${agentRtConfig.model}`;
4182
+ }
4183
+ }
3795
4184
  let spawnCommand;
3796
4185
  if (useExeAgent) {
3797
4186
  spawnCommand = `${envPrefix} exe-agent --employee ${employeeName} --model ${opts.model} --provider ${opts.provider}${cleanupSuffix}`;
4187
+ } else if (useCodex) {
4188
+ process.stderr.write(
4189
+ `[tmux-routing] agent-config: ${employeeName} \u2192 codex (${agentRtConfig.model})
4190
+ `
4191
+ );
4192
+ spawnCommand = `${envPrefix} exe-start-codex --agent ${employeeName}${cleanupSuffix}`;
4193
+ } else if (useOpencode) {
4194
+ const binName = `${employeeName}-opencode`;
4195
+ process.stderr.write(
4196
+ `[tmux-routing] agent-config: ${employeeName} \u2192 opencode (${agentRtConfig.model})
4197
+ `
4198
+ );
4199
+ spawnCommand = `${envPrefix} ${binName}${cleanupSuffix}`;
3798
4200
  } else if (useBinSymlink) {
3799
4201
  const binName = `${employeeName}-${ccProvider}`;
3800
4202
  process.stderr.write(
@@ -3817,10 +4219,12 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
3817
4219
  try {
3818
4220
  const mySession = getMySession();
3819
4221
  const dispatchInfo = path13.join(SESSION_CACHE, `dispatch-info-${sessionName}.json`);
3820
- writeFileSync5(dispatchInfo, JSON.stringify({
4222
+ writeFileSync6(dispatchInfo, JSON.stringify({
3821
4223
  dispatchedBy: mySession,
3822
4224
  rootExe: exeSession,
3823
- provider: useBinSymlink ? ccProvider : useExeAgent ? opts.provider : "anthropic",
4225
+ provider: useBinSymlink ? ccProvider : useExeAgent ? opts.provider : useCodex ? "openai" : useOpencode ? "opencode" : "anthropic",
4226
+ runtime: useCodex ? "codex" : useOpencode ? "opencode" : useExeAgent ? "exe-agent" : "claude",
4227
+ model: useCodex ? agentRtConfig.model : useOpencode ? agentRtConfig.model : void 0,
3824
4228
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
3825
4229
  }));
3826
4230
  } catch {
@@ -3828,7 +4232,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
3828
4232
  let booted = false;
3829
4233
  for (let i = 0; i < 30; i++) {
3830
4234
  try {
3831
- execSync6("sleep 0.5");
4235
+ execSync5("sleep 0.5");
3832
4236
  } catch {
3833
4237
  }
3834
4238
  try {
@@ -3838,6 +4242,11 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
3838
4242
  booted = true;
3839
4243
  break;
3840
4244
  }
4245
+ } else if (useCodex) {
4246
+ if (pane.includes("codex") || pane.includes("Codex") || pane.includes("exe-start-codex")) {
4247
+ booted = true;
4248
+ break;
4249
+ }
3841
4250
  } else {
3842
4251
  if (pane.includes("Claude Code") || pane.includes("\u276F")) {
3843
4252
  booted = true;
@@ -3849,9 +4258,10 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
3849
4258
  }
3850
4259
  if (!booted) {
3851
4260
  releaseSpawnLock2(sessionName);
3852
- return { sessionName, error: `${useExeAgent ? "exe-agent" : "claude"} did not boot within 15s` };
4261
+ const runtimeLabel = useExeAgent ? "exe-agent" : useCodex ? "codex" : "claude";
4262
+ return { sessionName, error: `${runtimeLabel} did not boot within 15s` };
3853
4263
  }
3854
- if (!useExeAgent) {
4264
+ if (!useExeAgent && !useCodex) {
3855
4265
  try {
3856
4266
  transport.sendKeys(sessionName, `/exe-call ${employeeName}`);
3857
4267
  } catch {
@@ -3878,6 +4288,8 @@ var init_tmux_routing = __esm({
3878
4288
  init_cc_agent_support();
3879
4289
  init_mcp_prefix();
3880
4290
  init_provider_table();
4291
+ init_agent_config();
4292
+ init_runtime_table();
3881
4293
  init_intercom_queue();
3882
4294
  init_plan_limits();
3883
4295
  init_employees();
@@ -3921,7 +4333,8 @@ var init_task_scope = __esm({
3921
4333
  // src/lib/tasks-crud.ts
3922
4334
  import crypto4 from "crypto";
3923
4335
  import path14 from "path";
3924
- import { execSync as execSync7 } from "child_process";
4336
+ import os8 from "os";
4337
+ import { execSync as execSync6 } from "child_process";
3925
4338
  import { mkdir as mkdir4, writeFile as writeFile4, appendFile } from "fs/promises";
3926
4339
  import { existsSync as existsSync13, readFileSync as readFileSync11 } from "fs";
3927
4340
  async function writeCheckpoint(input2) {
@@ -3964,6 +4377,35 @@ function extractParentFromContext(contextBody) {
3964
4377
  function slugify(title) {
3965
4378
  return title.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
3966
4379
  }
4380
+ function buildKeywordIndex() {
4381
+ const idx = /* @__PURE__ */ new Map();
4382
+ for (const [role, keywords] of Object.entries(LANE_KEYWORDS)) {
4383
+ for (const kw of keywords) {
4384
+ const existing = idx.get(kw) ?? [];
4385
+ existing.push(role);
4386
+ idx.set(kw, existing);
4387
+ }
4388
+ }
4389
+ return idx;
4390
+ }
4391
+ function checkLaneAffinity(title, context, assigneeName) {
4392
+ const employees = loadEmployeesSync();
4393
+ const employee = employees.find((e) => e.name === assigneeName);
4394
+ if (!employee) return void 0;
4395
+ const assigneeRole = employee.role;
4396
+ const text = `${title} ${context}`.toLowerCase();
4397
+ const matchedRoles = /* @__PURE__ */ new Set();
4398
+ for (const [keyword, roles] of KEYWORD_INDEX) {
4399
+ if (text.includes(keyword)) {
4400
+ for (const role of roles) matchedRoles.add(role);
4401
+ }
4402
+ }
4403
+ if (matchedRoles.size === 0) return void 0;
4404
+ if (matchedRoles.has(assigneeRole)) return void 0;
4405
+ if (assigneeRole === "COO") return void 0;
4406
+ const expectedRoles = Array.from(matchedRoles).join(" or ");
4407
+ return `\u26A0\uFE0F Lane mismatch: task content suggests ${expectedRoles}, but assigned to ${assigneeName} (${assigneeRole}).`;
4408
+ }
3967
4409
  async function resolveTask(client, identifier, scopeSession) {
3968
4410
  const scope = sessionScopeFilter(scopeSession);
3969
4411
  let result = await client.execute({
@@ -4013,7 +4455,14 @@ async function createTaskCore(input2) {
4013
4455
  const id = crypto4.randomUUID();
4014
4456
  const now = (/* @__PURE__ */ new Date()).toISOString();
4015
4457
  const slug = slugify(input2.title);
4016
- const taskFile = input2.taskFile ?? `exe/${input2.assignedTo}/${slug}.md`;
4458
+ let earlySessionScope = null;
4459
+ try {
4460
+ const { resolveExeSession: resolveExeSession2 } = await Promise.resolve().then(() => (init_tmux_routing(), tmux_routing_exports));
4461
+ earlySessionScope = resolveExeSession2();
4462
+ } catch {
4463
+ }
4464
+ const scope = earlySessionScope ?? "default";
4465
+ const taskFile = input2.taskFile ?? `tasks/${scope}/${input2.assignedTo}/${slug}.md`;
4017
4466
  let blockedById = null;
4018
4467
  const initialStatus = input2.blockedBy ? "blocked" : "open";
4019
4468
  if (input2.blockedBy) {
@@ -4053,6 +4502,13 @@ async function createTaskCore(input2) {
4053
4502
  if (dupCheck.rows.length > 0) {
4054
4503
  warning = `similar active task already exists (${String(dupCheck.rows[0].id)}). Created new task anyway.`;
4055
4504
  }
4505
+ if (!process.env.DISABLE_LANE_AFFINITY) {
4506
+ const laneWarning = checkLaneAffinity(input2.title, input2.context, input2.assignedTo);
4507
+ if (laneWarning) {
4508
+ warning = warning ? `${warning}
4509
+ ${laneWarning}` : laneWarning;
4510
+ }
4511
+ }
4056
4512
  if (input2.baseDir) {
4057
4513
  try {
4058
4514
  await mkdir4(path14.join(input2.baseDir, "exe", "output"), { recursive: true });
@@ -4063,12 +4519,7 @@ async function createTaskCore(input2) {
4063
4519
  }
4064
4520
  }
4065
4521
  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
- }
4522
+ const sessionScope = earlySessionScope;
4072
4523
  await client.execute({
4073
4524
  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
4525
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
@@ -4095,6 +4546,43 @@ async function createTaskCore(input2) {
4095
4546
  now
4096
4547
  ]
4097
4548
  });
4549
+ if (input2.baseDir) {
4550
+ try {
4551
+ const EXE_OS_DIR = path14.join(os8.homedir(), ".exe-os");
4552
+ const mdPath = path14.join(EXE_OS_DIR, taskFile);
4553
+ const mdDir = path14.dirname(mdPath);
4554
+ if (!existsSync13(mdDir)) await mkdir4(mdDir, { recursive: true });
4555
+ const reviewer = input2.reviewer ?? input2.assignedBy;
4556
+ const mdContent = `# ${input2.title}
4557
+
4558
+ **ID:** ${id}
4559
+ **Status:** ${initialStatus}
4560
+ **Priority:** ${input2.priority}
4561
+ **Assigned by:** ${input2.assignedBy}
4562
+ **Assigned to:** ${input2.assignedTo}
4563
+ **Project:** ${input2.projectName}
4564
+ **Created:** ${now.split("T")[0]}${parentTaskId ? `
4565
+ **Parent task:** ${parentTaskId}` : ""}
4566
+ **Reviewer:** ${reviewer}
4567
+
4568
+ ## Context
4569
+
4570
+ ${input2.context}
4571
+
4572
+ ## MANDATORY: When done
4573
+
4574
+ You MUST call update_task with status "done" and a result summary when finished.
4575
+ If you skip this, your reviewer will not know you're done and your work won't be reviewed.
4576
+ Do NOT let a failed commit or any error prevent you from calling update_task(done).
4577
+ `;
4578
+ await writeFile4(mdPath, mdContent, "utf-8");
4579
+ } catch (err) {
4580
+ process.stderr.write(
4581
+ `[create-task] WARNING: .md file write failed for ${taskFile}: ${err instanceof Error ? err.message : String(err)}
4582
+ `
4583
+ );
4584
+ }
4585
+ }
4098
4586
  return {
4099
4587
  id,
4100
4588
  title: input2.title,
@@ -4167,14 +4655,14 @@ function isTmuxSessionAlive(identifier) {
4167
4655
  if (!identifier || identifier === "unknown") return true;
4168
4656
  try {
4169
4657
  if (identifier.startsWith("%")) {
4170
- const output = execSync7("tmux list-panes -a -F '#{pane_id}'", {
4658
+ const output = execSync6("tmux list-panes -a -F '#{pane_id}'", {
4171
4659
  timeout: 2e3,
4172
4660
  encoding: "utf8",
4173
4661
  stdio: ["pipe", "pipe", "pipe"]
4174
4662
  });
4175
4663
  return output.split("\n").some((l) => l.trim() === identifier);
4176
4664
  } else {
4177
- execSync7(`tmux has-session -t ${JSON.stringify(identifier)}`, {
4665
+ execSync6(`tmux has-session -t ${JSON.stringify(identifier)}`, {
4178
4666
  timeout: 2e3,
4179
4667
  stdio: ["pipe", "pipe", "pipe"]
4180
4668
  });
@@ -4183,7 +4671,7 @@ function isTmuxSessionAlive(identifier) {
4183
4671
  } catch {
4184
4672
  if (identifier.startsWith("%")) return true;
4185
4673
  try {
4186
- execSync7("tmux list-sessions", {
4674
+ execSync6("tmux list-sessions", {
4187
4675
  timeout: 2e3,
4188
4676
  stdio: ["pipe", "pipe", "pipe"]
4189
4677
  });
@@ -4198,12 +4686,12 @@ function checkStaleCompletion(taskContext, taskCreatedAt) {
4198
4686
  if (!DELEGATION_KEYWORDS.test(taskContext)) return null;
4199
4687
  try {
4200
4688
  const since = new Date(taskCreatedAt).toISOString();
4201
- const branch = execSync7(
4689
+ const branch = execSync6(
4202
4690
  "git rev-parse --abbrev-ref HEAD 2>/dev/null",
4203
4691
  { encoding: "utf8", timeout: 3e3 }
4204
4692
  ).trim();
4205
4693
  const branchArg = branch && branch !== "HEAD" ? branch : "";
4206
- const commitCount = execSync7(
4694
+ const commitCount = execSync6(
4207
4695
  `git log --oneline --since="${since}" ${branchArg} 2>/dev/null | wc -l`,
4208
4696
  { encoding: "utf8", timeout: 5e3 }
4209
4697
  ).trim();
@@ -4287,7 +4775,7 @@ ${input2.result}` : `\u26A0\uFE0F ${warning}`;
4287
4775
  return { row, taskFile, now, taskId };
4288
4776
  }
4289
4777
  }
4290
- if (curStatus === "in_progress" && input2.callerAgentId && (input2.callerAgentId === assignedBy || input2.callerAgentId === "exe")) {
4778
+ if (curStatus === "in_progress" && input2.callerAgentId && (input2.callerAgentId === assignedBy || isCoordinatorName(input2.callerAgentId))) {
4291
4779
  process.stderr.write(
4292
4780
  `[tasks] Assigner override: ${input2.callerAgentId} reclaiming ${taskId}
4293
4781
  `
@@ -4399,12 +4887,22 @@ async function ensureGitignoreExe(baseDir) {
4399
4887
  } catch {
4400
4888
  }
4401
4889
  }
4402
- var DELEGATION_KEYWORDS, TASK_ALREADY_CLAIMED_PREFIX;
4890
+ var LANE_KEYWORDS, KEYWORD_INDEX, DELEGATION_KEYWORDS, TASK_ALREADY_CLAIMED_PREFIX;
4403
4891
  var init_tasks_crud = __esm({
4404
4892
  "src/lib/tasks-crud.ts"() {
4405
4893
  "use strict";
4406
4894
  init_database();
4407
4895
  init_task_scope();
4896
+ init_employees();
4897
+ LANE_KEYWORDS = {
4898
+ CMO: ["sales", "script", "pitch", "offer", "copy", "objection", "brand", "content", "seo", "marketing", "newsletter", "carousel", "social", "campaign"],
4899
+ CTO: ["spec", "architecture", "migration", "schema", "database", "design doc", "adr", "security audit", "tech stack"],
4900
+ "Principal Engineer": ["implement", "build", "fix", "commit", "refactor", "bug", "feature", "wire", "integration"],
4901
+ "Staff Code Reviewer": ["critique", "verdict", "review", "audit", "code quality"],
4902
+ "Content Production Specialist": ["render", "video", "image", "b-roll", "remotion", "animation", "thumbnail"],
4903
+ "AI Product Lead": ["competitive", "analysis", "benchmark", "compare", "scout", "evaluate", "poc"]
4904
+ };
4905
+ KEYWORD_INDEX = buildKeywordIndex();
4408
4906
  DELEGATION_KEYWORDS = /parallel|delegate|wave|worktree|multi-instance/i;
4409
4907
  TASK_ALREADY_CLAIMED_PREFIX = "TASK_ALREADY_CLAIMED";
4410
4908
  }
@@ -4412,7 +4910,7 @@ var init_tasks_crud = __esm({
4412
4910
 
4413
4911
  // src/lib/tasks-review.ts
4414
4912
  import path15 from "path";
4415
- import { existsSync as existsSync14, readdirSync as readdirSync4, unlinkSync as unlinkSync5 } from "fs";
4913
+ import { existsSync as existsSync14, readdirSync as readdirSync3, unlinkSync as unlinkSync5 } from "fs";
4416
4914
  async function countPendingReviews(sessionScope) {
4417
4915
  const client = getClient();
4418
4916
  if (sessionScope) {
@@ -4434,7 +4932,7 @@ async function countNewPendingReviewsSince(sinceIso, sessionScope) {
4434
4932
  const result2 = await client.execute({
4435
4933
  sql: `SELECT COUNT(*) as cnt FROM tasks
4436
4934
  WHERE status = 'needs_review' AND updated_at > ?
4437
- AND (session_scope = ? OR session_scope IS NULL)`,
4935
+ AND session_scope = ?`,
4438
4936
  args: [sinceIso, sessionScope]
4439
4937
  });
4440
4938
  return Number(result2.rows[0]?.cnt) || 0;
@@ -4452,7 +4950,7 @@ async function listPendingReviews(limit, sessionScope) {
4452
4950
  const result2 = await client.execute({
4453
4951
  sql: `SELECT title, assigned_to, project_name FROM tasks
4454
4952
  WHERE status = 'needs_review'
4455
- AND (session_scope = ? OR session_scope IS NULL)
4953
+ AND session_scope = ?
4456
4954
  ORDER BY priority ASC, created_at DESC LIMIT ?`,
4457
4955
  args: [sessionScope, limit]
4458
4956
  });
@@ -4573,14 +5071,14 @@ async function cleanupReviewFile(row, taskFile, _baseDir) {
4573
5071
  if (parts.length >= 3 && parts[0] === "review") {
4574
5072
  const agent = parts[1];
4575
5073
  const slug = parts.slice(2).join("-");
4576
- const originalTaskFile = `exe/${agent}/${slug}.md`;
5074
+ const legacyTaskFile = `exe/${agent}/${slug}.md`;
4577
5075
  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]
5076
+ sql: "UPDATE tasks SET status = 'done', updated_at = ? WHERE (task_file = ? OR task_file LIKE ?) AND status = 'needs_review'",
5077
+ args: [now, legacyTaskFile, `tasks/%/${agent}/${slug}.md`]
4580
5078
  });
4581
5079
  if (result.rowsAffected > 0) {
4582
5080
  process.stderr.write(
4583
- `[review-cleanup] Cascaded original task to done (legacy path): ${originalTaskFile}
5081
+ `[review-cleanup] Cascaded original task to done: ${agent}/${slug}.md
4584
5082
  `
4585
5083
  );
4586
5084
  }
@@ -4595,7 +5093,7 @@ async function cleanupReviewFile(row, taskFile, _baseDir) {
4595
5093
  try {
4596
5094
  const cacheDir = path15.join(EXE_AI_DIR, "session-cache");
4597
5095
  if (existsSync14(cacheDir)) {
4598
- for (const f of readdirSync4(cacheDir)) {
5096
+ for (const f of readdirSync3(cacheDir)) {
4599
5097
  if (f.startsWith("review-notified-")) {
4600
5098
  unlinkSync5(path15.join(cacheDir, f));
4601
5099
  }
@@ -4720,7 +5218,7 @@ function findSessionForProject(projectName) {
4720
5218
  const sessions = listSessions();
4721
5219
  for (const s of sessions) {
4722
5220
  const proj = s.projectDir.split("/").filter(Boolean).pop();
4723
- if (proj === projectName && (s.agentId === "exe" || isCoordinatorName(s.agentId))) return s;
5221
+ if (proj === projectName && isCoordinatorName(s.agentId)) return s;
4724
5222
  }
4725
5223
  return null;
4726
5224
  }
@@ -4766,7 +5264,7 @@ var init_session_scope = __esm({
4766
5264
 
4767
5265
  // src/lib/tasks-notify.ts
4768
5266
  async function dispatchTaskToEmployee(input2) {
4769
- if (input2.assignedTo === "exe" || isCoordinatorName(input2.assignedTo)) return { dispatched: "skipped" };
5267
+ if (isCoordinatorName(input2.assignedTo)) return { dispatched: "skipped" };
4770
5268
  let crossProject = false;
4771
5269
  if (input2.projectName) {
4772
5270
  try {
@@ -5162,7 +5660,7 @@ __export(tasks_exports, {
5162
5660
  writeCheckpoint: () => writeCheckpoint
5163
5661
  });
5164
5662
  import path17 from "path";
5165
- import { writeFileSync as writeFileSync6, mkdirSync as mkdirSync6, unlinkSync as unlinkSync6 } from "fs";
5663
+ import { writeFileSync as writeFileSync7, mkdirSync as mkdirSync7, unlinkSync as unlinkSync6 } from "fs";
5166
5664
  async function createTask(input2) {
5167
5665
  const result = await createTaskCore(input2);
5168
5666
  if (!input2.skipDispatch && result.status !== "blocked" && !process.env.VITEST) {
@@ -5184,8 +5682,8 @@ async function updateTask(input2) {
5184
5682
  const cacheDir = path17.join(EXE_AI_DIR, "session-cache");
5185
5683
  const cachePath = path17.join(cacheDir, `current-task-${agent}.json`);
5186
5684
  if (input2.status === "in_progress") {
5187
- mkdirSync6(cacheDir, { recursive: true });
5188
- writeFileSync6(cachePath, JSON.stringify({ taskId, title: String(row.title) }));
5685
+ mkdirSync7(cacheDir, { recursive: true });
5686
+ writeFileSync7(cachePath, JSON.stringify({ taskId, title: String(row.title) }));
5189
5687
  } else if (input2.status === "done" || input2.status === "blocked" || input2.status === "cancelled") {
5190
5688
  try {
5191
5689
  unlinkSync6(cachePath);
@@ -5245,7 +5743,7 @@ async function updateTask(input2) {
5245
5743
  }
5246
5744
  const isTerminal = input2.status === "done" || input2.status === "needs_review";
5247
5745
  if (isTerminal) {
5248
- const isCoordinator = String(row.assigned_to) === "exe" || isCoordinatorName(String(row.assigned_to));
5746
+ const isCoordinator = isCoordinatorName(String(row.assigned_to));
5249
5747
  if (!isCoordinator) {
5250
5748
  notifyTaskDone();
5251
5749
  }
@@ -5270,7 +5768,7 @@ async function updateTask(input2) {
5270
5768
  }
5271
5769
  }
5272
5770
  }
5273
- if (input2.status === "done" && String(row.assigned_to) !== "exe" && !isCoordinatorName(String(row.assigned_to)) && !process.env.VITEST) {
5771
+ if (input2.status === "done" && !isCoordinatorName(String(row.assigned_to)) && !process.env.VITEST) {
5274
5772
  Promise.resolve().then(() => (init_skill_learning(), skill_learning_exports)).then(
5275
5773
  ({ captureAndLearn: captureAndLearn2 }) => captureAndLearn2({
5276
5774
  taskId,
@@ -5286,7 +5784,7 @@ async function updateTask(input2) {
5286
5784
  });
5287
5785
  }
5288
5786
  let nextTask;
5289
- if (isTerminal && String(row.assigned_to) !== "exe" && !isCoordinatorName(String(row.assigned_to))) {
5787
+ if (isTerminal && !isCoordinatorName(String(row.assigned_to))) {
5290
5788
  try {
5291
5789
  nextTask = await findNextTask(String(row.assigned_to));
5292
5790
  } catch {
@@ -5352,15 +5850,15 @@ __export(worker_gate_exports, {
5352
5850
  tryAcquireBackfillLock: () => tryAcquireBackfillLock,
5353
5851
  tryAcquireWorkerSlot: () => tryAcquireWorkerSlot
5354
5852
  });
5355
- import { readdirSync as readdirSync5, writeFileSync as writeFileSync7, unlinkSync as unlinkSync7, mkdirSync as mkdirSync7, existsSync as existsSync15 } from "fs";
5853
+ import { readdirSync as readdirSync4, writeFileSync as writeFileSync8, unlinkSync as unlinkSync7, mkdirSync as mkdirSync8, existsSync as existsSync15 } from "fs";
5356
5854
  import path18 from "path";
5357
5855
  function tryAcquireWorkerSlot() {
5358
5856
  try {
5359
- mkdirSync7(WORKER_PID_DIR, { recursive: true });
5857
+ mkdirSync8(WORKER_PID_DIR, { recursive: true });
5360
5858
  const reservationId = `res-${process.pid}-${Date.now()}`;
5361
5859
  const reservationPath = path18.join(WORKER_PID_DIR, `${reservationId}.pid`);
5362
- writeFileSync7(reservationPath, String(process.pid));
5363
- const files = readdirSync5(WORKER_PID_DIR);
5860
+ writeFileSync8(reservationPath, String(process.pid));
5861
+ const files = readdirSync4(WORKER_PID_DIR);
5364
5862
  let alive = 0;
5365
5863
  for (const f of files) {
5366
5864
  if (!f.endsWith(".pid")) continue;
@@ -5399,8 +5897,8 @@ function tryAcquireWorkerSlot() {
5399
5897
  }
5400
5898
  function registerWorkerPid(pid) {
5401
5899
  try {
5402
- mkdirSync7(WORKER_PID_DIR, { recursive: true });
5403
- writeFileSync7(path18.join(WORKER_PID_DIR, `worker-${pid}.pid`), String(pid));
5900
+ mkdirSync8(WORKER_PID_DIR, { recursive: true });
5901
+ writeFileSync8(path18.join(WORKER_PID_DIR, `worker-${pid}.pid`), String(pid));
5404
5902
  } catch {
5405
5903
  }
5406
5904
  }
@@ -5412,7 +5910,7 @@ function cleanupWorkerPid() {
5412
5910
  }
5413
5911
  function tryAcquireBackfillLock() {
5414
5912
  try {
5415
- mkdirSync7(WORKER_PID_DIR, { recursive: true });
5913
+ mkdirSync8(WORKER_PID_DIR, { recursive: true });
5416
5914
  if (existsSync15(BACKFILL_LOCK)) {
5417
5915
  try {
5418
5916
  const pid = parseInt(
@@ -5429,7 +5927,7 @@ function tryAcquireBackfillLock() {
5429
5927
  } catch {
5430
5928
  }
5431
5929
  }
5432
- writeFileSync7(BACKFILL_LOCK, String(process.pid));
5930
+ writeFileSync8(BACKFILL_LOCK, String(process.pid));
5433
5931
  return true;
5434
5932
  } catch {
5435
5933
  return true;
@@ -5454,8 +5952,8 @@ var init_worker_gate = __esm({
5454
5952
 
5455
5953
  // src/adapters/claude/hooks/ingest-worker.ts
5456
5954
  import crypto7 from "crypto";
5457
- import { execSync as execSync8 } from "child_process";
5458
- import { mkdirSync as mkdirSync8, writeFileSync as writeFileSync8 } from "fs";
5955
+ import { execSync as execSync7 } from "child_process";
5956
+ import { mkdirSync as mkdirSync9, writeFileSync as writeFileSync9 } from "fs";
5459
5957
  import path19 from "path";
5460
5958
 
5461
5959
  // src/lib/error-detector.ts
@@ -5553,9 +6051,6 @@ function detectError(data) {
5553
6051
  }
5554
6052
 
5555
6053
  // src/lib/task-scanner.ts
5556
- import { readdirSync, readFileSync, existsSync, statSync } from "fs";
5557
- import { execSync } from "child_process";
5558
- import path from "path";
5559
6054
  var STATUS_RE = /^\*\*Status:\*\*\s*(\w+)/m;
5560
6055
  var TITLE_RE = /^# (.+)/m;
5561
6056
 
@@ -5565,6 +6060,7 @@ init_project_name();
5565
6060
  // src/lib/store.ts
5566
6061
  init_memory();
5567
6062
  init_database();
6063
+ import { createHash } from "crypto";
5568
6064
 
5569
6065
  // src/lib/keychain.ts
5570
6066
  import { readFile as readFile3, writeFile as writeFile3, unlink, mkdir as mkdir3, chmod as chmod2 } from "fs/promises";
@@ -5599,12 +6095,20 @@ async function getMasterKey() {
5599
6095
  }
5600
6096
  const keyPath = getKeyPath();
5601
6097
  if (!existsSync4(keyPath)) {
6098
+ process.stderr.write(
6099
+ `[keychain] Key not found at ${keyPath} (HOME=${os3.homedir()}, EXE_OS_DIR=${process.env.EXE_OS_DIR ?? "unset"})
6100
+ `
6101
+ );
5602
6102
  return null;
5603
6103
  }
5604
6104
  try {
5605
6105
  const content = await readFile3(keyPath, "utf-8");
5606
6106
  return Buffer.from(content.trim(), "base64");
5607
- } catch {
6107
+ } catch (err) {
6108
+ process.stderr.write(
6109
+ `[keychain] Key read failed at ${keyPath}: ${err instanceof Error ? err.message : String(err)}
6110
+ `
6111
+ );
5608
6112
  return null;
5609
6113
  }
5610
6114
  }
@@ -5693,12 +6197,52 @@ function classifyTier(record) {
5693
6197
  if (["store_memory", "manual"].includes(record.tool_name ?? "") && (record.importance ?? 0) >= 5) return 2;
5694
6198
  return 3;
5695
6199
  }
6200
+ function inferFilePaths(record) {
6201
+ if (!["Read", "Write", "Edit"].includes(record.tool_name)) return null;
6202
+ const firstLine = record.raw_text.split("\n")[0] ?? "";
6203
+ const match = firstLine.match(/(\/[\w./-]+\.\w+)/);
6204
+ return match ? JSON.stringify([match[1]]) : null;
6205
+ }
6206
+ function inferCommitHash(record) {
6207
+ if (record.tool_name !== "Bash") return null;
6208
+ const match = record.raw_text.match(/\b([a-f0-9]{7,40})\b/);
6209
+ return match ? match[1] : null;
6210
+ }
6211
+ function inferLanguageType(record) {
6212
+ const text = record.raw_text;
6213
+ if (!text || text.length < 10) return null;
6214
+ const trimmed = text.trimStart();
6215
+ if (trimmed.startsWith("{") || trimmed.startsWith("[")) return "json";
6216
+ if (/\b(SELECT|INSERT|UPDATE|DELETE|CREATE TABLE|ALTER TABLE)\b/i.test(text)) return "sql";
6217
+ if (/\b(function |const |import |export |class |def |async |=>)\b/.test(text)) return "code";
6218
+ if (trimmed.startsWith("#") || trimmed.startsWith("*")) return "prose";
6219
+ return "mixed";
6220
+ }
6221
+ function inferDomain(record) {
6222
+ const proj = (record.project_name ?? "").toLowerCase();
6223
+ if (proj.includes("marketing") || proj.includes("content")) return "marketing";
6224
+ if (proj.includes("crm") || proj.includes("customer")) return "customer";
6225
+ return null;
6226
+ }
5696
6227
  async function writeMemory(record) {
5697
6228
  if (record.vector !== null && record.vector.length !== EMBEDDING_DIM) {
5698
6229
  throw new Error(
5699
6230
  `Expected ${EMBEDDING_DIM}-dim vector, got ${record.vector.length}`
5700
6231
  );
5701
6232
  }
6233
+ const contentHash = createHash("md5").update(record.raw_text).digest("hex");
6234
+ if (_pendingRecords.some((r) => r.content_hash === contentHash && r.agent_id === record.agent_id)) {
6235
+ return;
6236
+ }
6237
+ try {
6238
+ const client = getClient();
6239
+ const existing = await client.execute({
6240
+ sql: "SELECT id FROM memories WHERE content_hash = ? AND agent_id = ? LIMIT 1",
6241
+ args: [contentHash, record.agent_id]
6242
+ });
6243
+ if (existing.rows.length > 0) return;
6244
+ } catch {
6245
+ }
5702
6246
  const dbRow = {
5703
6247
  id: record.id,
5704
6248
  agent_id: record.agent_id,
@@ -5728,7 +6272,23 @@ async function writeMemory(record) {
5728
6272
  supersedes_id: record.supersedes_id ?? null,
5729
6273
  draft: record.draft ? 1 : 0,
5730
6274
  memory_type: record.memory_type ?? "raw",
5731
- trajectory: record.trajectory ? JSON.stringify(record.trajectory) : null
6275
+ trajectory: record.trajectory ? JSON.stringify(record.trajectory) : null,
6276
+ content_hash: contentHash,
6277
+ intent: record.intent ?? null,
6278
+ outcome: record.outcome ?? null,
6279
+ domain: record.domain ?? inferDomain(record),
6280
+ referenced_entities: record.referenced_entities ?? null,
6281
+ retrieval_count: record.retrieval_count ?? 0,
6282
+ chain_position: record.chain_position ?? null,
6283
+ review_status: record.review_status ?? null,
6284
+ context_window_pct: record.context_window_pct ?? null,
6285
+ file_paths: record.file_paths ?? inferFilePaths(record),
6286
+ commit_hash: record.commit_hash ?? inferCommitHash(record),
6287
+ duration_ms: record.duration_ms ?? null,
6288
+ token_cost: record.token_cost ?? null,
6289
+ audience: record.audience ?? null,
6290
+ language_type: record.language_type ?? inferLanguageType(record),
6291
+ parent_memory_id: record.parent_memory_id ?? null
5732
6292
  };
5733
6293
  _pendingRecords.push(dbRow);
5734
6294
  orgBus.emit({
@@ -5786,80 +6346,85 @@ async function flushBatch() {
5786
6346
  const draft = row.draft ? 1 : 0;
5787
6347
  const memoryType = row.memory_type ?? "raw";
5788
6348
  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,
6349
+ const contentHash = row.content_hash ?? null;
6350
+ const intent = row.intent ?? null;
6351
+ const outcome = row.outcome ?? null;
6352
+ const domain = row.domain ?? null;
6353
+ const referencedEntities = row.referenced_entities ?? null;
6354
+ const retrievalCount = row.retrieval_count ?? 0;
6355
+ const chainPosition = row.chain_position ?? null;
6356
+ const reviewStatus = row.review_status ?? null;
6357
+ const contextWindowPct = row.context_window_pct ?? null;
6358
+ const filePaths = row.file_paths ?? null;
6359
+ const commitHash = row.commit_hash ?? null;
6360
+ const durationMs = row.duration_ms ?? null;
6361
+ const tokenCost = row.token_cost ?? null;
6362
+ const audience = row.audience ?? null;
6363
+ const languageType = row.language_type ?? null;
6364
+ const parentMemoryId = row.parent_memory_id ?? null;
6365
+ const cols = `id, agent_id, agent_role, session_id, timestamp,
5799
6366
  tool_name, project_name,
5800
6367
  has_error, raw_text, vector, version, task_id, importance, status,
5801
6368
  confidence, last_accessed,
5802
6369
  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
- ]
6370
+ source_path, source_type, tier, supersedes_id, draft, memory_type, trajectory, content_hash,
6371
+ intent, outcome, domain, referenced_entities, retrieval_count,
6372
+ chain_position, review_status, context_window_pct, file_paths, commit_hash,
6373
+ duration_ms, token_cost, audience, language_type, parent_memory_id`;
6374
+ const metaArgs = [
6375
+ intent,
6376
+ outcome,
6377
+ domain,
6378
+ referencedEntities,
6379
+ retrievalCount,
6380
+ chainPosition,
6381
+ reviewStatus,
6382
+ contextWindowPct,
6383
+ filePaths,
6384
+ commitHash,
6385
+ durationMs,
6386
+ tokenCost,
6387
+ audience,
6388
+ languageType,
6389
+ parentMemoryId
6390
+ ];
6391
+ const baseArgs = [
6392
+ row.id,
6393
+ row.agent_id,
6394
+ row.agent_role,
6395
+ row.session_id,
6396
+ row.timestamp,
6397
+ row.tool_name,
6398
+ row.project_name,
6399
+ row.has_error,
6400
+ row.raw_text
6401
+ ];
6402
+ const sharedArgs = [
6403
+ row.version,
6404
+ taskId,
6405
+ importance,
6406
+ status,
6407
+ confidence,
6408
+ lastAccessed,
6409
+ workspaceId,
6410
+ documentId,
6411
+ userId,
6412
+ charOffset,
6413
+ pageNumber,
6414
+ sourcePath,
6415
+ sourceType,
6416
+ tier,
6417
+ supersedesId,
6418
+ draft,
6419
+ memoryType,
6420
+ trajectory,
6421
+ contentHash
6422
+ ];
6423
+ return {
6424
+ sql: hasVector ? `INSERT OR IGNORE INTO memories (${cols})
6425
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, vector32(?), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` : `INSERT OR IGNORE INTO memories (${cols})
6426
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
6427
+ args: hasVector ? [...baseArgs, vectorToBlob(row.vector), ...sharedArgs, ...metaArgs] : [...baseArgs, ...sharedArgs, ...metaArgs]
5863
6428
  };
5864
6429
  };
5865
6430
  const globalClient = getClient();
@@ -6187,7 +6752,7 @@ process.stdin.on("end", async () => {
6187
6752
  try {
6188
6753
  const { EXE_AI_DIR: exeDir } = await Promise.resolve().then(() => (init_config(), config_exports));
6189
6754
  const flagPath = path19.join(exeDir, "session-cache", "needs-backfill");
6190
- writeFileSync8(flagPath, "1");
6755
+ writeFileSync9(flagPath, "1");
6191
6756
  } catch (err) {
6192
6757
  process.stderr.write(`[ingest-worker] backfill flag write failed: ${err instanceof Error ? err.message : String(err)}
6193
6758
  `);
@@ -6215,9 +6780,9 @@ process.stdin.on("end", async () => {
6215
6780
  if (status === "done") {
6216
6781
  const cwd = data.cwd ?? process.cwd();
6217
6782
  try {
6218
- execSync8("git add -u", { cwd, timeout: 1e4, stdio: "ignore" });
6783
+ execSync7("git add -u", { cwd, timeout: 1e4, stdio: "ignore" });
6219
6784
  const msg = `task(${agentId}): ${title}`;
6220
- execSync8(`git commit --no-verify -m ${JSON.stringify(msg)}`, { cwd, timeout: 3e4, stdio: "ignore" });
6785
+ execSync7(`git commit --no-verify -m ${JSON.stringify(msg)}`, { cwd, timeout: 3e4, stdio: "ignore" });
6221
6786
  } catch (err) {
6222
6787
  process.stderr.write(`[ingest-worker] auto-commit failed: ${err instanceof Error ? err.message : String(err)}
6223
6788
  `);
@@ -6298,8 +6863,8 @@ process.stdin.on("end", async () => {
6298
6863
  }
6299
6864
  const cwd = data.cwd ?? process.cwd();
6300
6865
  try {
6301
- mkdirSync8(path19.join(cwd, "exe/output"), { recursive: true });
6302
- mkdirSync8(path19.join(cwd, "exe/research"), { recursive: true });
6866
+ mkdirSync9(path19.join(cwd, "exe/output"), { recursive: true });
6867
+ mkdirSync9(path19.join(cwd, "exe/research"), { recursive: true });
6303
6868
  const { ensureGitignoreExe: ensureGitignoreExe2 } = await Promise.resolve().then(() => (init_tasks(), tasks_exports));
6304
6869
  await ensureGitignoreExe2(cwd);
6305
6870
  } catch (err) {