@askexenow/exe-os 0.8.80 → 0.8.82

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 (110) hide show
  1. package/dist/bin/backfill-conversations.js +359 -267
  2. package/dist/bin/backfill-responses.js +357 -265
  3. package/dist/bin/backfill-vectors.js +339 -264
  4. package/dist/bin/cleanup-stale-review-tasks.js +315 -256
  5. package/dist/bin/cli.js +494 -240
  6. package/dist/bin/exe-agent.js +141 -46
  7. package/dist/bin/exe-assign.js +151 -63
  8. package/dist/bin/exe-boot.js +294 -115
  9. package/dist/bin/exe-call.js +76 -51
  10. package/dist/bin/exe-cloud.js +58 -45
  11. package/dist/bin/exe-dispatch.js +434 -277
  12. package/dist/bin/exe-doctor.js +317 -246
  13. package/dist/bin/exe-export-behaviors.js +328 -248
  14. package/dist/bin/exe-forget.js +314 -231
  15. package/dist/bin/exe-gateway.js +2676 -1402
  16. package/dist/bin/exe-heartbeat.js +329 -264
  17. package/dist/bin/exe-kill.js +324 -244
  18. package/dist/bin/exe-launch-agent.js +574 -463
  19. package/dist/bin/exe-link.js +1055 -95
  20. package/dist/bin/exe-new-employee.js +49 -54
  21. package/dist/bin/exe-pending-messages.js +310 -253
  22. package/dist/bin/exe-pending-notifications.js +299 -228
  23. package/dist/bin/exe-pending-reviews.js +314 -245
  24. package/dist/bin/exe-rename.js +259 -195
  25. package/dist/bin/exe-review.js +140 -64
  26. package/dist/bin/exe-search.js +543 -356
  27. package/dist/bin/exe-session-cleanup.js +463 -382
  28. package/dist/bin/exe-settings.js +129 -99
  29. package/dist/bin/exe-start.sh +6 -6
  30. package/dist/bin/exe-status.js +95 -36
  31. package/dist/bin/exe-team.js +116 -51
  32. package/dist/bin/git-sweep.js +482 -307
  33. package/dist/bin/graph-backfill.js +357 -245
  34. package/dist/bin/graph-export.js +324 -244
  35. package/dist/bin/install.js +33 -10
  36. package/dist/bin/scan-tasks.js +481 -307
  37. package/dist/bin/setup.js +1147 -140
  38. package/dist/bin/shard-migrate.js +321 -241
  39. package/dist/bin/update.js +1 -7
  40. package/dist/bin/wiki-sync.js +318 -238
  41. package/dist/gateway/index.js +2656 -1383
  42. package/dist/hooks/bug-report-worker.js +641 -472
  43. package/dist/hooks/commit-complete.js +482 -307
  44. package/dist/hooks/error-recall.js +363 -135
  45. package/dist/hooks/exe-heartbeat-hook.js +97 -27
  46. package/dist/hooks/ingest-worker.js +584 -397
  47. package/dist/hooks/ingest.js +123 -58
  48. package/dist/hooks/instructions-loaded.js +212 -82
  49. package/dist/hooks/notification.js +200 -70
  50. package/dist/hooks/post-compact.js +199 -81
  51. package/dist/hooks/pre-compact.js +352 -140
  52. package/dist/hooks/pre-tool-use.js +416 -278
  53. package/dist/hooks/prompt-ingest-worker.js +376 -299
  54. package/dist/hooks/prompt-submit.js +414 -188
  55. package/dist/hooks/response-ingest-worker.js +408 -338
  56. package/dist/hooks/session-end.js +209 -83
  57. package/dist/hooks/session-start.js +382 -158
  58. package/dist/hooks/stop.js +209 -83
  59. package/dist/hooks/subagent-stop.js +209 -85
  60. package/dist/hooks/summary-worker.js +606 -510
  61. package/dist/index.js +2133 -855
  62. package/dist/lib/cloud-sync.js +1175 -184
  63. package/dist/lib/config.js +1 -9
  64. package/dist/lib/consolidation.js +71 -34
  65. package/dist/lib/database.js +166 -14
  66. package/dist/lib/device-registry.js +189 -117
  67. package/dist/lib/embedder.js +6 -10
  68. package/dist/lib/employee-templates.js +134 -39
  69. package/dist/lib/employees.js +30 -7
  70. package/dist/lib/exe-daemon-client.js +5 -7
  71. package/dist/lib/exe-daemon.js +514 -152
  72. package/dist/lib/hybrid-search.js +543 -356
  73. package/dist/lib/identity-templates.js +15 -15
  74. package/dist/lib/identity.js +19 -15
  75. package/dist/lib/license.js +1 -7
  76. package/dist/lib/messaging.js +157 -135
  77. package/dist/lib/reminders.js +97 -0
  78. package/dist/lib/schedules.js +302 -231
  79. package/dist/lib/skill-learning.js +33 -27
  80. package/dist/lib/status-brief.js +11 -14
  81. package/dist/lib/store.js +326 -237
  82. package/dist/lib/task-router.js +105 -1
  83. package/dist/lib/tasks.js +233 -116
  84. package/dist/lib/tmux-routing.js +173 -56
  85. package/dist/lib/ws-client.js +13 -3
  86. package/dist/mcp/server.js +2009 -1015
  87. package/dist/mcp/tools/complete-reminder.js +97 -0
  88. package/dist/mcp/tools/create-reminder.js +97 -0
  89. package/dist/mcp/tools/create-task.js +426 -262
  90. package/dist/mcp/tools/deactivate-behavior.js +119 -44
  91. package/dist/mcp/tools/list-reminders.js +97 -0
  92. package/dist/mcp/tools/list-tasks.js +56 -57
  93. package/dist/mcp/tools/send-message.js +206 -143
  94. package/dist/mcp/tools/update-task.js +259 -85
  95. package/dist/runtime/index.js +495 -316
  96. package/dist/tui/App.js +1128 -919
  97. package/package.json +2 -10
  98. package/src/commands/exe/afk.md +8 -8
  99. package/src/commands/exe/assign.md +1 -1
  100. package/src/commands/exe/build-adv.md +1 -1
  101. package/src/commands/exe/call.md +10 -10
  102. package/src/commands/exe/employee-heartbeat.md +9 -6
  103. package/src/commands/exe/heartbeat.md +5 -5
  104. package/src/commands/exe/intercom.md +26 -15
  105. package/src/commands/exe/launch.md +2 -2
  106. package/src/commands/exe/new-employee.md +1 -1
  107. package/src/commands/exe/review.md +2 -2
  108. package/src/commands/exe/schedule.md +1 -1
  109. package/src/commands/exe/sessions.md +2 -2
  110. package/src/commands/exe.md +22 -20
@@ -30,7 +30,6 @@ var config_exports = {};
30
30
  __export(config_exports, {
31
31
  CONFIG_MIGRATIONS: () => CONFIG_MIGRATIONS,
32
32
  CONFIG_PATH: () => CONFIG_PATH,
33
- COO_AGENT_NAME: () => COO_AGENT_NAME,
34
33
  CURRENT_CONFIG_VERSION: () => CURRENT_CONFIG_VERSION,
35
34
  DB_PATH: () => DB_PATH,
36
35
  EXE_AI_DIR: () => EXE_AI_DIR,
@@ -186,7 +185,7 @@ async function loadConfigFrom(configPath) {
186
185
  return { ...DEFAULT_CONFIG };
187
186
  }
188
187
  }
189
- var EXE_AI_DIR, DB_PATH, MODELS_DIR, CONFIG_PATH, COO_AGENT_NAME, LEGACY_LANCE_PATH, CURRENT_CONFIG_VERSION, DEFAULT_CONFIG, CONFIG_MIGRATIONS;
188
+ var EXE_AI_DIR, DB_PATH, MODELS_DIR, CONFIG_PATH, LEGACY_LANCE_PATH, CURRENT_CONFIG_VERSION, DEFAULT_CONFIG, CONFIG_MIGRATIONS;
190
189
  var init_config = __esm({
191
190
  "src/lib/config.ts"() {
192
191
  "use strict";
@@ -194,7 +193,6 @@ var init_config = __esm({
194
193
  DB_PATH = path.join(EXE_AI_DIR, "memories.db");
195
194
  MODELS_DIR = path.join(EXE_AI_DIR, "models");
196
195
  CONFIG_PATH = path.join(EXE_AI_DIR, "config.json");
197
- COO_AGENT_NAME = "exe";
198
196
  LEGACY_LANCE_PATH = path.join(EXE_AI_DIR, "local.lance");
199
197
  CURRENT_CONFIG_VERSION = 1;
200
198
  DEFAULT_CONFIG = {
@@ -230,13 +228,7 @@ var init_config = __esm({
230
228
  wikiUrl: "",
231
229
  wikiApiKey: "",
232
230
  wikiSyncIntervalMs: 30 * 60 * 1e3,
233
- wikiWorkspaceMapping: {
234
- exe: "Executive",
235
- yoshi: "Engineering",
236
- mari: "Marketing",
237
- tom: "Engineering",
238
- sasha: "Production"
239
- },
231
+ wikiWorkspaceMapping: {},
240
232
  wikiAutoUpdate: true,
241
233
  wikiAutoUpdateThreshold: 0.5,
242
234
  wikiAutoUpdateCreateNew: true,
@@ -720,7 +712,7 @@ function wrapWithRetry(client) {
720
712
  return (sql) => retryOnBusy(() => target.execute(sql), "execute");
721
713
  }
722
714
  if (prop === "batch") {
723
- return (stmts) => retryOnBusy(() => target.batch(stmts), "batch");
715
+ return (stmts, mode) => retryOnBusy(() => target.batch(stmts, mode), "batch");
724
716
  }
725
717
  return Reflect.get(target, prop, receiver);
726
718
  }
@@ -736,6 +728,68 @@ var init_db_retry = __esm({
736
728
  }
737
729
  });
738
730
 
731
+ // src/lib/employees.ts
732
+ import { readFile as readFile2, writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
733
+ import { existsSync as existsSync4, symlinkSync, readlinkSync, readFileSync as readFileSync4, renameSync as renameSync3, unlinkSync, writeFileSync as writeFileSync3 } from "fs";
734
+ import { execSync as execSync4 } from "child_process";
735
+ import path4 from "path";
736
+ import os4 from "os";
737
+ function normalizeRole(role) {
738
+ return (role ?? "").trim().toLowerCase();
739
+ }
740
+ function isCoordinatorRole(role) {
741
+ return normalizeRole(role) === normalizeRole(COORDINATOR_ROLE);
742
+ }
743
+ function getCoordinatorEmployee(employees) {
744
+ return employees.find((e) => isCoordinatorRole(e.role));
745
+ }
746
+ function getCoordinatorName(employees = loadEmployeesSync()) {
747
+ return getCoordinatorEmployee(employees)?.name ?? DEFAULT_COORDINATOR_TEMPLATE_NAME;
748
+ }
749
+ function isCoordinatorName(agentName, employees = loadEmployeesSync()) {
750
+ if (!agentName) return false;
751
+ return agentName.toLowerCase() === getCoordinatorName(employees).toLowerCase();
752
+ }
753
+ async function loadEmployees(employeesPath = EMPLOYEES_PATH) {
754
+ if (!existsSync4(employeesPath)) {
755
+ return [];
756
+ }
757
+ const raw = await readFile2(employeesPath, "utf-8");
758
+ try {
759
+ return JSON.parse(raw);
760
+ } catch {
761
+ return [];
762
+ }
763
+ }
764
+ function loadEmployeesSync(employeesPath = EMPLOYEES_PATH) {
765
+ if (!existsSync4(employeesPath)) return [];
766
+ try {
767
+ return JSON.parse(readFileSync4(employeesPath, "utf-8"));
768
+ } catch {
769
+ return [];
770
+ }
771
+ }
772
+ function getEmployee(employees, name) {
773
+ return employees.find((e) => e.name.toLowerCase() === name.toLowerCase());
774
+ }
775
+ function isMultiInstance(agentName, employees) {
776
+ const roster = employees ?? loadEmployeesSync();
777
+ const emp = getEmployee(roster, agentName);
778
+ if (!emp) return false;
779
+ return MULTI_INSTANCE_ROLES.has(emp.role.toLowerCase());
780
+ }
781
+ var EMPLOYEES_PATH, DEFAULT_COORDINATOR_TEMPLATE_NAME, COORDINATOR_ROLE, MULTI_INSTANCE_ROLES;
782
+ var init_employees = __esm({
783
+ "src/lib/employees.ts"() {
784
+ "use strict";
785
+ init_config();
786
+ EMPLOYEES_PATH = path4.join(EXE_AI_DIR, "exe-employees.json");
787
+ DEFAULT_COORDINATOR_TEMPLATE_NAME = "exe";
788
+ COORDINATOR_ROLE = "COO";
789
+ MULTI_INSTANCE_ROLES = /* @__PURE__ */ new Set(["principal engineer", "content production specialist", "staff code reviewer"]);
790
+ }
791
+ });
792
+
739
793
  // src/lib/database.ts
740
794
  var database_exports = {};
741
795
  __export(database_exports, {
@@ -883,22 +937,24 @@ async function ensureSchema() {
883
937
  ON behaviors(agent_id, active);
884
938
  `);
885
939
  try {
940
+ const coordinatorName = getCoordinatorName();
886
941
  const existing = await client.execute({
887
- sql: "SELECT COUNT(*) as cnt FROM behaviors WHERE agent_id = 'exe'",
888
- args: []
942
+ sql: "SELECT COUNT(*) as cnt FROM behaviors WHERE agent_id = ?",
943
+ args: [coordinatorName]
889
944
  });
890
945
  if (Number(existing.rows[0]?.cnt) === 0) {
891
- await client.executeMultiple(`
892
- INSERT INTO behaviors (id, agent_id, project_name, domain, content, active, created_at, updated_at)
893
- VALUES
894
- (hex(randomblob(16)), 'exe', NULL, 'workflow', 'Don''t ask "keep going?" \u2014 just keep executing phases/plans autonomously', 1, '2026-03-25T00:00:00Z', '2026-03-25T00:00:00Z');
895
- INSERT INTO behaviors (id, agent_id, project_name, domain, content, active, created_at, updated_at)
896
- VALUES
897
- (hex(randomblob(16)), 'exe', NULL, 'tool-use', 'Always use create_task MCP tool, never write .md files directly for task creation', 1, '2026-03-25T00:00:00Z', '2026-03-25T00:00:00Z');
898
- INSERT INTO behaviors (id, agent_id, project_name, domain, content, active, created_at, updated_at)
899
- VALUES
900
- (hex(randomblob(16)), 'exe', NULL, 'workflow', 'Auto-start reviewing when idle and reviews are pending \u2014 never ask founder for permission', 1, '2026-03-25T00:00:00Z', '2026-03-25T00:00:00Z');
901
- `);
946
+ const seededAt = "2026-03-25T00:00:00Z";
947
+ for (const [domain, content] of [
948
+ ["workflow", `Don't ask "keep going?" \u2014 just keep executing phases/plans autonomously`],
949
+ ["tool-use", "Always use create_task MCP tool, never write .md files directly for task creation"],
950
+ ["workflow", "Auto-start reviewing when idle and reviews are pending \u2014 never ask founder for permission"]
951
+ ]) {
952
+ await client.execute({
953
+ sql: `INSERT INTO behaviors (id, agent_id, project_name, domain, content, active, created_at, updated_at)
954
+ VALUES (hex(randomblob(16)), ?, NULL, ?, ?, 1, ?, ?)`,
955
+ args: [coordinatorName, domain, content, seededAt, seededAt]
956
+ });
957
+ }
902
958
  }
903
959
  } catch {
904
960
  }
@@ -1590,6 +1646,39 @@ async function ensureSchema() {
1590
1646
  } catch {
1591
1647
  }
1592
1648
  }
1649
+ try {
1650
+ await client.execute({
1651
+ sql: `ALTER TABLE memories ADD COLUMN draft INTEGER DEFAULT 0`,
1652
+ args: []
1653
+ });
1654
+ } catch {
1655
+ }
1656
+ try {
1657
+ await client.execute(
1658
+ `CREATE INDEX IF NOT EXISTS idx_memories_draft ON memories(draft) WHERE draft = 1`
1659
+ );
1660
+ } catch {
1661
+ }
1662
+ try {
1663
+ await client.execute({
1664
+ sql: `ALTER TABLE memories ADD COLUMN memory_type TEXT DEFAULT 'raw'`,
1665
+ args: []
1666
+ });
1667
+ } catch {
1668
+ }
1669
+ try {
1670
+ await client.execute(
1671
+ `CREATE INDEX IF NOT EXISTS idx_memories_type ON memories(memory_type)`
1672
+ );
1673
+ } catch {
1674
+ }
1675
+ try {
1676
+ await client.execute({
1677
+ sql: `ALTER TABLE memories ADD COLUMN trajectory TEXT`,
1678
+ args: []
1679
+ });
1680
+ } catch {
1681
+ }
1593
1682
  }
1594
1683
  async function disposeDatabase() {
1595
1684
  if (_client) {
@@ -1603,6 +1692,7 @@ var init_database = __esm({
1603
1692
  "src/lib/database.ts"() {
1604
1693
  "use strict";
1605
1694
  init_db_retry();
1695
+ init_employees();
1606
1696
  _client = null;
1607
1697
  _resilientClient = null;
1608
1698
  initTurso = initDatabase;
@@ -1610,50 +1700,6 @@ var init_database = __esm({
1610
1700
  }
1611
1701
  });
1612
1702
 
1613
- // src/lib/employees.ts
1614
- import { readFile as readFile2, writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
1615
- import { existsSync as existsSync4, symlinkSync, readlinkSync, readFileSync as readFileSync4, renameSync as renameSync3, unlinkSync, writeFileSync as writeFileSync3 } from "fs";
1616
- import { execSync as execSync4 } from "child_process";
1617
- import path4 from "path";
1618
- import os4 from "os";
1619
- async function loadEmployees(employeesPath = EMPLOYEES_PATH) {
1620
- if (!existsSync4(employeesPath)) {
1621
- return [];
1622
- }
1623
- const raw = await readFile2(employeesPath, "utf-8");
1624
- try {
1625
- return JSON.parse(raw);
1626
- } catch {
1627
- return [];
1628
- }
1629
- }
1630
- function loadEmployeesSync(employeesPath = EMPLOYEES_PATH) {
1631
- if (!existsSync4(employeesPath)) return [];
1632
- try {
1633
- return JSON.parse(readFileSync4(employeesPath, "utf-8"));
1634
- } catch {
1635
- return [];
1636
- }
1637
- }
1638
- function getEmployee(employees, name) {
1639
- return employees.find((e) => e.name.toLowerCase() === name.toLowerCase());
1640
- }
1641
- function isMultiInstance(agentName, employees) {
1642
- const roster = employees ?? loadEmployeesSync();
1643
- const emp = getEmployee(roster, agentName);
1644
- if (!emp) return false;
1645
- return MULTI_INSTANCE_ROLES.has(emp.role.toLowerCase());
1646
- }
1647
- var EMPLOYEES_PATH, MULTI_INSTANCE_ROLES;
1648
- var init_employees = __esm({
1649
- "src/lib/employees.ts"() {
1650
- "use strict";
1651
- init_config();
1652
- EMPLOYEES_PATH = path4.join(EXE_AI_DIR, "exe-employees.json");
1653
- MULTI_INSTANCE_ROLES = /* @__PURE__ */ new Set(["principal engineer", "content production specialist", "staff code reviewer"]);
1654
- }
1655
- });
1656
-
1657
1703
  // src/lib/license.ts
1658
1704
  import { readFileSync as readFileSync5, writeFileSync as writeFileSync4, existsSync as existsSync5, mkdirSync as mkdirSync3 } from "fs";
1659
1705
  import { randomUUID } from "crypto";
@@ -2235,6 +2281,36 @@ async function listTasks(input) {
2235
2281
  tokensWarnedAt: r.tokens_warned_at !== null ? Number(r.tokens_warned_at) : null
2236
2282
  }));
2237
2283
  }
2284
+ function isTmuxSessionAlive(identifier) {
2285
+ if (!identifier || identifier === "unknown") return true;
2286
+ try {
2287
+ if (identifier.startsWith("%")) {
2288
+ const output = execSync5("tmux list-panes -a -F '#{pane_id}'", {
2289
+ timeout: 2e3,
2290
+ encoding: "utf8",
2291
+ stdio: ["pipe", "pipe", "pipe"]
2292
+ });
2293
+ return output.split("\n").some((l) => l.trim() === identifier);
2294
+ } else {
2295
+ execSync5(`tmux has-session -t ${JSON.stringify(identifier)}`, {
2296
+ timeout: 2e3,
2297
+ stdio: ["pipe", "pipe", "pipe"]
2298
+ });
2299
+ return true;
2300
+ }
2301
+ } catch {
2302
+ if (identifier.startsWith("%")) return true;
2303
+ try {
2304
+ execSync5("tmux list-sessions", {
2305
+ timeout: 2e3,
2306
+ stdio: ["pipe", "pipe", "pipe"]
2307
+ });
2308
+ return false;
2309
+ } catch {
2310
+ return true;
2311
+ }
2312
+ }
2313
+ }
2238
2314
  function checkStaleCompletion(taskContext, taskCreatedAt) {
2239
2315
  if (!taskContext) return null;
2240
2316
  if (!DELEGATION_KEYWORDS.test(taskContext)) return null;
@@ -2297,13 +2373,59 @@ ${input.result}` : `\u26A0\uFE0F ${warning}`;
2297
2373
  });
2298
2374
  if (claim.rowsAffected === 0) {
2299
2375
  const current = await client.execute({
2300
- sql: "SELECT status, assigned_tmux FROM tasks WHERE id = ?",
2376
+ sql: "SELECT status, assigned_tmux, assigned_by FROM tasks WHERE id = ?",
2301
2377
  args: [taskId]
2302
2378
  });
2303
2379
  const cur = current.rows[0];
2304
- const status = cur?.status ?? "unknown";
2305
- const claimedBy = cur?.assigned_tmux ? ` (claimed by ${cur.assigned_tmux})` : "";
2306
- throw new Error(`${TASK_ALREADY_CLAIMED_PREFIX}: task ${taskId} is ${status}${claimedBy}`);
2380
+ const curStatus = cur?.status ?? "unknown";
2381
+ const claimedBySession = cur?.assigned_tmux ?? "";
2382
+ const assignedBy = cur?.assigned_by ?? "";
2383
+ if (curStatus === "in_progress" && claimedBySession && !isTmuxSessionAlive(claimedBySession)) {
2384
+ process.stderr.write(
2385
+ `[tasks] Auto-releasing dead claim on ${taskId} (was ${claimedBySession})
2386
+ `
2387
+ );
2388
+ await client.execute({
2389
+ sql: "UPDATE tasks SET status = 'open', assigned_tmux = NULL, updated_at = ? WHERE id = ?",
2390
+ args: [now, taskId]
2391
+ });
2392
+ const retried = await client.execute({
2393
+ sql: `UPDATE tasks SET status = 'in_progress', assigned_tmux = ?, updated_at = ? WHERE id = ? AND status = 'open'`,
2394
+ args: [tmuxSession, now, taskId]
2395
+ });
2396
+ if (retried.rowsAffected > 0) {
2397
+ try {
2398
+ await writeCheckpoint({
2399
+ taskId,
2400
+ step: "reclaimed_dead_session",
2401
+ contextSummary: `Task reclaimed after dead session ${claimedBySession} released.`
2402
+ });
2403
+ } catch {
2404
+ }
2405
+ return { row, taskFile, now, taskId };
2406
+ }
2407
+ }
2408
+ if (curStatus === "in_progress" && input.callerAgentId && (input.callerAgentId === assignedBy || input.callerAgentId === "exe")) {
2409
+ process.stderr.write(
2410
+ `[tasks] Assigner override: ${input.callerAgentId} reclaiming ${taskId}
2411
+ `
2412
+ );
2413
+ await client.execute({
2414
+ sql: `UPDATE tasks SET status = 'in_progress', assigned_tmux = ?, updated_at = ? WHERE id = ?`,
2415
+ args: [tmuxSession, now, taskId]
2416
+ });
2417
+ try {
2418
+ await writeCheckpoint({
2419
+ taskId,
2420
+ step: "assigner_override",
2421
+ contextSummary: `Task force-reclaimed by assigner ${input.callerAgentId}.`
2422
+ });
2423
+ } catch {
2424
+ }
2425
+ return { row, taskFile, now, taskId };
2426
+ }
2427
+ const claimedBy = claimedBySession ? ` (claimed by ${claimedBySession})` : "";
2428
+ throw new Error(`${TASK_ALREADY_CLAIMED_PREFIX}: task ${taskId} is ${curStatus}${claimedBy}`);
2307
2429
  }
2308
2430
  try {
2309
2431
  await writeCheckpoint({
@@ -2401,7 +2523,7 @@ var init_tasks_crud = __esm({
2401
2523
  "use strict";
2402
2524
  init_database();
2403
2525
  init_task_scope();
2404
- DELEGATION_KEYWORDS = /parallel|delegate|wave|tom\d*-exe/i;
2526
+ DELEGATION_KEYWORDS = /parallel|delegate|wave|worktree|multi-instance/i;
2405
2527
  TASK_ALREADY_CLAIMED_PREFIX = "TASK_ALREADY_CLAIMED";
2406
2528
  }
2407
2529
  });
@@ -2557,21 +2679,23 @@ function getReviewChecklist(role, agent, taskSlug) {
2557
2679
  }
2558
2680
  async function createReviewForCompletedTask(row, result, _baseDir, now) {
2559
2681
  const taskFile = String(row.task_file);
2560
- if (String(row.assigned_to) === "exe") return;
2682
+ const employees = await loadEmployees();
2683
+ const coordinatorName = getCoordinatorName(employees);
2684
+ if (String(row.assigned_to) === "exe" || isCoordinatorName(String(row.assigned_to), employees)) return;
2561
2685
  if (String(row.title).startsWith("Review:")) return;
2562
2686
  const fileName = taskFile.split("/").pop() ?? "";
2563
2687
  if (fileName.startsWith("review-") && String(row.assigned_by) === "system") return;
2564
2688
  if (fileName.startsWith("review-") && String(row.assigned_to) === "system") return;
2565
2689
  const client = getClient();
2566
2690
  const agent = String(row.assigned_to);
2567
- const reviewer = String(row.reviewer || row.assigned_by) || "exe";
2691
+ const rawReviewer = row.reviewer || row.assigned_by;
2692
+ const reviewer = rawReviewer ? String(rawReviewer) : coordinatorName;
2568
2693
  const currentStatus = String(row.status ?? "");
2569
2694
  if (currentStatus === "done") return;
2570
2695
  const existingResult = String(row.result ?? "");
2571
2696
  if (existingResult.includes("## Review notes")) return;
2572
2697
  let reviewerRole = "unknown";
2573
2698
  try {
2574
- const employees = await loadEmployees();
2575
2699
  const emp = getEmployee(employees, reviewer);
2576
2700
  if (emp) reviewerRole = emp.role;
2577
2701
  } catch {
@@ -2616,7 +2740,7 @@ async function createReviewForCompletedTask(row, result, _baseDir, now) {
2616
2740
  agentRole: String(row.assigned_to),
2617
2741
  event: "task_complete",
2618
2742
  project: String(row.project_name),
2619
- summary: `completed "${taskTitle}" \u2014 review task created`,
2743
+ summary: `completed "${taskTitle}" \u2014 ready for review`,
2620
2744
  taskFile
2621
2745
  });
2622
2746
  const originalPriority = String(row.priority).toLowerCase();
@@ -2855,7 +2979,7 @@ function findSessionForProject(projectName) {
2855
2979
  const sessions = listSessions();
2856
2980
  for (const s of sessions) {
2857
2981
  const proj = s.projectDir.split("/").filter(Boolean).pop();
2858
- if (proj === projectName && s.agentId === "exe") return s;
2982
+ if (proj === projectName && (s.agentId === "exe" || isCoordinatorName(s.agentId))) return s;
2859
2983
  }
2860
2984
  return null;
2861
2985
  }
@@ -2895,12 +3019,13 @@ var init_session_scope = __esm({
2895
3019
  init_session_registry();
2896
3020
  init_project_name();
2897
3021
  init_tmux_routing();
3022
+ init_employees();
2898
3023
  }
2899
3024
  });
2900
3025
 
2901
3026
  // src/lib/tasks-notify.ts
2902
3027
  async function dispatchTaskToEmployee(input) {
2903
- if (input.assignedTo === "exe") return { dispatched: "skipped" };
3028
+ if (input.assignedTo === "exe" || isCoordinatorName(input.assignedTo)) return { dispatched: "skipped" };
2904
3029
  let crossProject = false;
2905
3030
  if (input.projectName) {
2906
3031
  try {
@@ -3343,6 +3468,24 @@ async function updateTask(input) {
3343
3468
  });
3344
3469
  } catch {
3345
3470
  }
3471
+ const assignedAgent = String(row.assigned_to);
3472
+ if (!isCoordinatorName(assignedAgent)) {
3473
+ try {
3474
+ const draftClient = getClient();
3475
+ if (input.status === "done") {
3476
+ await draftClient.execute({
3477
+ sql: `UPDATE memories SET draft = 0 WHERE agent_id = ? AND draft = 1`,
3478
+ args: [assignedAgent]
3479
+ });
3480
+ } else if (input.status === "cancelled") {
3481
+ await draftClient.execute({
3482
+ sql: `DELETE FROM memories WHERE agent_id = ? AND draft = 1`,
3483
+ args: [assignedAgent]
3484
+ });
3485
+ }
3486
+ } catch {
3487
+ }
3488
+ }
3346
3489
  try {
3347
3490
  const client = getClient();
3348
3491
  const cascaded = await client.execute({
@@ -3361,8 +3504,8 @@ async function updateTask(input) {
3361
3504
  }
3362
3505
  const isTerminal = input.status === "done" || input.status === "needs_review";
3363
3506
  if (isTerminal) {
3364
- const isExe = String(row.assigned_to) === "exe";
3365
- if (!isExe) {
3507
+ const isCoordinator = String(row.assigned_to) === "exe" || isCoordinatorName(String(row.assigned_to));
3508
+ if (!isCoordinator) {
3366
3509
  notifyTaskDone();
3367
3510
  }
3368
3511
  await markTaskNotificationsRead(taskFile);
@@ -3386,7 +3529,7 @@ async function updateTask(input) {
3386
3529
  }
3387
3530
  }
3388
3531
  }
3389
- if (input.status === "done" && String(row.assigned_to) !== "exe" && !process.env.VITEST) {
3532
+ if (input.status === "done" && String(row.assigned_to) !== "exe" && !isCoordinatorName(String(row.assigned_to)) && !process.env.VITEST) {
3390
3533
  Promise.resolve().then(() => (init_skill_learning(), skill_learning_exports)).then(
3391
3534
  ({ captureAndLearn: captureAndLearn2 }) => captureAndLearn2({
3392
3535
  taskId,
@@ -3402,7 +3545,7 @@ async function updateTask(input) {
3402
3545
  });
3403
3546
  }
3404
3547
  let nextTask;
3405
- if (isTerminal && String(row.assigned_to) !== "exe") {
3548
+ if (isTerminal && String(row.assigned_to) !== "exe" && !isCoordinatorName(String(row.assigned_to))) {
3406
3549
  try {
3407
3550
  nextTask = await findNextTask(String(row.assigned_to));
3408
3551
  } catch {
@@ -3429,12 +3572,14 @@ async function updateTask(input) {
3429
3572
  async function deleteTask(taskId, baseDir) {
3430
3573
  const client = getClient();
3431
3574
  const { taskFile, assignedTo, assignedBy, taskSlug } = await deleteTaskCore(taskId, baseDir);
3432
- const reviewer = assignedBy || "exe";
3575
+ const coordinatorName = getCoordinatorName();
3576
+ const reviewer = assignedBy || coordinatorName;
3433
3577
  const reviewSlug = `review-${assignedTo}-${taskSlug}`;
3434
3578
  const reviewFile = `exe/${reviewer}/${reviewSlug}.md`;
3579
+ const legacyReviewFile = `exe/${coordinatorName}/${reviewSlug}.md`;
3435
3580
  await client.execute({
3436
- sql: "DELETE FROM tasks WHERE task_file = ? OR task_file = ?",
3437
- args: [reviewFile, `exe/exe/${reviewSlug}.md`]
3581
+ sql: "DELETE FROM tasks WHERE task_file = ? OR task_file = ? OR task_file = ?",
3582
+ args: [reviewFile, legacyReviewFile, `exe/exe/${reviewSlug}.md`]
3438
3583
  });
3439
3584
  await markAsReadByTaskFile(taskFile);
3440
3585
  await markAsReadByTaskFile(reviewFile);
@@ -3446,6 +3591,7 @@ var init_tasks = __esm({
3446
3591
  init_config();
3447
3592
  init_notifications();
3448
3593
  init_state_bus();
3594
+ init_employees();
3449
3595
  init_tasks_crud();
3450
3596
  init_tasks_review();
3451
3597
  init_tasks_crud();
@@ -3531,7 +3677,7 @@ function _resetLastRelaunchCache() {
3531
3677
  }
3532
3678
  async function lastResumeCreatedAtMs(agentId) {
3533
3679
  const client = getClient();
3534
- const cmScope = sessionScopeFilter();
3680
+ const cmScope = sessionScopeFilter(null);
3535
3681
  const result = await client.execute({
3536
3682
  sql: `SELECT MAX(created_at) AS last_created_at
3537
3683
  FROM tasks
@@ -3556,7 +3702,7 @@ async function createOrRefreshResumeTask(agentId, projectDir, openTasks) {
3556
3702
  const client = getClient();
3557
3703
  const now = (/* @__PURE__ */ new Date()).toISOString();
3558
3704
  const context = buildResumeContext(agentId, openTasks);
3559
- const rdScope = sessionScopeFilter();
3705
+ const rdScope = sessionScopeFilter(null);
3560
3706
  const existing = await client.execute({
3561
3707
  sql: `SELECT id FROM tasks
3562
3708
  WHERE assigned_to = ?
@@ -3590,7 +3736,7 @@ async function pollCapacityDead() {
3590
3736
  const transport = getTransport();
3591
3737
  const relaunched = [];
3592
3738
  const registered = listSessions().filter(
3593
- (s) => s.agentId !== "exe"
3739
+ (s) => s.agentId !== "exe" && !isCoordinatorName(s.agentId)
3594
3740
  );
3595
3741
  if (registered.length === 0) return [];
3596
3742
  let liveSessions;
@@ -3650,7 +3796,7 @@ async function pollCapacityDead() {
3650
3796
  reason: "capacity"
3651
3797
  });
3652
3798
  const client = getClient();
3653
- const rlScope = sessionScopeFilter();
3799
+ const rlScope = sessionScopeFilter(null);
3654
3800
  const openTasks = await client.execute({
3655
3801
  sql: `SELECT id, title, priority, task_file, status
3656
3802
  FROM tasks
@@ -3704,6 +3850,7 @@ var init_capacity_monitor = __esm({
3704
3850
  init_session_kill_telemetry();
3705
3851
  init_tmux_routing();
3706
3852
  init_task_scope();
3853
+ init_employees();
3707
3854
  CAPACITY_PATTERNS = [
3708
3855
  /conversation is too long/i,
3709
3856
  /maximum context length/i,
@@ -3853,7 +4000,7 @@ function employeeSessionName(employee, exeSession, instance) {
3853
4000
  exeSession = root;
3854
4001
  } else {
3855
4002
  throw new Error(
3856
- `Invalid exeSession "${exeSession}" \u2014 contains a dash but no recognizable root session. Pass a root session name (e.g., "exe1", "work", "yoda1")`
4003
+ `Invalid coordinator session "${exeSession}" \u2014 contains a dash but no recognizable root session. Pass a root session name.`
3857
4004
  );
3858
4005
  }
3859
4006
  }
@@ -3873,8 +4020,10 @@ function parseParentExe(sessionName, agentId) {
3873
4020
  return match?.[1] ?? null;
3874
4021
  }
3875
4022
  function extractRootExe(name) {
3876
- const match = name.match(/(exe\d+)$/);
3877
- return match?.[1] ?? null;
4023
+ if (!name) return null;
4024
+ if (!name.includes("-")) return name;
4025
+ const parts = name.split("-").filter(Boolean);
4026
+ return parts.length > 0 ? parts[parts.length - 1] : null;
3878
4027
  }
3879
4028
  function registerParentExe(sessionKey, parentExe, dispatchedBy) {
3880
4029
  if (!existsSync10(SESSION_CACHE)) {
@@ -4019,12 +4168,14 @@ function isSessionBusy(sessionName) {
4019
4168
  return state === "thinking" || state === "tool";
4020
4169
  }
4021
4170
  function isExeSession(sessionName) {
4022
- return /^exe\d*$/.test(sessionName);
4171
+ const matchesBaseWithInstance = (baseName) => sessionName === baseName || sessionName.startsWith(baseName) && /^\d+$/.test(sessionName.slice(baseName.length));
4172
+ const coordinatorName = getCoordinatorName();
4173
+ return matchesBaseWithInstance(coordinatorName) || matchesBaseWithInstance("exe");
4023
4174
  }
4024
4175
  function sendIntercom(targetSession) {
4025
4176
  const transport = getTransport();
4026
4177
  if (isExeSession(targetSession)) {
4027
- logIntercom(`SKIP_EXE \u2192 ${targetSession} (exe sessions use prompt-submit hook)`);
4178
+ logIntercom(`SKIP_COORDINATOR \u2192 ${targetSession} (coordinator sessions use prompt-submit hook)`);
4028
4179
  return "skipped_exe";
4029
4180
  }
4030
4181
  if (isDebounced(targetSession)) {
@@ -4076,7 +4227,7 @@ function notifyParentExe(sessionKey) {
4076
4227
  if (result === "failed") {
4077
4228
  const rootExe = resolveExeSession();
4078
4229
  if (rootExe && rootExe !== target) {
4079
- process.stderr.write(`[intercom] notifyParentExe: dispatcher ${target} dead, falling back to root exe ${rootExe}
4230
+ process.stderr.write(`[intercom] notifyParentExe: dispatcher ${target} dead, falling back to root coordinator session ${rootExe}
4080
4231
  `);
4081
4232
  const fallback = sendIntercom(rootExe);
4082
4233
  return fallback !== "failed";
@@ -4086,8 +4237,8 @@ function notifyParentExe(sessionKey) {
4086
4237
  return true;
4087
4238
  }
4088
4239
  function ensureEmployee(employeeName, exeSession, projectDir, opts) {
4089
- if (employeeName === "exe") {
4090
- return { status: "failed", sessionName: "", error: "exe is the COO, not a dispatchable employee" };
4240
+ if (employeeName === "exe" || isCoordinatorName(employeeName)) {
4241
+ return { status: "failed", sessionName: "", error: "The COO is not a dispatchable employee" };
4091
4242
  }
4092
4243
  try {
4093
4244
  assertEmployeeLimitSync();
@@ -4096,8 +4247,8 @@ function ensureEmployee(employeeName, exeSession, projectDir, opts) {
4096
4247
  return { status: "failed", sessionName: "", error: err.message };
4097
4248
  }
4098
4249
  }
4099
- if (/-exe\d*$/.test(employeeName)) {
4100
- const bare = employeeName.replace(/-exe\d*$/, "").replace(/\d+$/, "");
4250
+ if (employeeName.includes("-")) {
4251
+ const bare = employeeName.split("-")[0].replace(/\d+$/, "");
4101
4252
  return {
4102
4253
  status: "failed",
4103
4254
  sessionName: "",
@@ -4116,7 +4267,7 @@ function ensureEmployee(employeeName, exeSession, projectDir, opts) {
4116
4267
  return {
4117
4268
  status: "failed",
4118
4269
  sessionName: "",
4119
- error: `Invalid exeSession "${exeSession}" \u2014 contains a dash but no recognizable root session. Pass a root session name (e.g., "exe1", "work", "yoda1")`
4270
+ error: `Invalid coordinator session "${exeSession}" \u2014 contains a dash but no recognizable root session. Pass a root session name.`
4120
4271
  };
4121
4272
  }
4122
4273
  }
@@ -4273,8 +4424,8 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
4273
4424
  const ctxContent = [
4274
4425
  `## Session Context`,
4275
4426
  `You are running in tmux session: ${sessionName}.`,
4276
- `Your parent exe session is ${exeSession}.`,
4277
- `Your employees (if any) use the -${exeSession} suffix (e.g., tom-${exeSession}).`
4427
+ `Your parent coordinator session is ${exeSession}.`,
4428
+ `Your employees (if any) use the -${exeSession} suffix.`
4278
4429
  ].join("\n");
4279
4430
  writeFileSync6(ctxFile, ctxContent);
4280
4431
  sessionContextFlag = ` --append-system-prompt-file ${ctxFile}`;
@@ -4378,6 +4529,7 @@ var init_tmux_routing = __esm({
4378
4529
  init_provider_table();
4379
4530
  init_intercom_queue();
4380
4531
  init_plan_limits();
4532
+ init_employees();
4381
4533
  SPAWN_LOCK_DIR = path13.join(os6.homedir(), ".exe-os", "spawn-locks");
4382
4534
  SESSION_CACHE = path13.join(os6.homedir(), ".exe-os", "session-cache");
4383
4535
  BEHAVIORS_EXPORT_TIMEOUT_MS = 1e4;
@@ -4674,7 +4826,7 @@ import os7 from "os";
4674
4826
  import path14 from "path";
4675
4827
  async function hasOpenTasks(client, agentId) {
4676
4828
  try {
4677
- const scope = sessionScopeFilter();
4829
+ const scope = sessionScopeFilter(null);
4678
4830
  const result = await client.execute({
4679
4831
  sql: `SELECT 1 FROM tasks
4680
4832
  WHERE assigned_to = ? AND status IN ('open', 'in_progress')${scope.sql}
@@ -4688,7 +4840,7 @@ async function hasOpenTasks(client, agentId) {
4688
4840
  }
4689
4841
  async function hasNeedsReview(client, agentId) {
4690
4842
  try {
4691
- const scope = sessionScopeFilter();
4843
+ const scope = sessionScopeFilter(null);
4692
4844
  const result = await client.execute({
4693
4845
  sql: `SELECT 1 FROM tasks
4694
4846
  WHERE assigned_to = ? AND status = 'needs_review'${scope.sql}
@@ -4760,6 +4912,8 @@ var daemon_orchestration_exports = {};
4760
4912
  __export(daemon_orchestration_exports, {
4761
4913
  IDLE_KILL_INTERCOM_ACK_WINDOW_MS: () => IDLE_KILL_INTERCOM_ACK_WINDOW_MS,
4762
4914
  IDLE_NUDGE_DEDUP_MS: () => IDLE_NUDGE_DEDUP_MS,
4915
+ ORPHAN_PATTERNS: () => ORPHAN_PATTERNS,
4916
+ ORPHAN_SIGKILL_DELAY_MS: () => ORPHAN_SIGKILL_DELAY_MS,
4763
4917
  REVIEW_NUDGE_COOLDOWN_MS: () => REVIEW_NUDGE_COOLDOWN_MS,
4764
4918
  SESSION_CONTEXT_THRESHOLD_PCT: () => SESSION_CONTEXT_THRESHOLD_PCT,
4765
4919
  SESSION_TTL_HOURS: () => SESSION_TTL_HOURS,
@@ -4767,12 +4921,14 @@ __export(daemon_orchestration_exports, {
4767
4921
  classifyTtlKillReason: () => classifyTtlKillReason,
4768
4922
  createIdleKillRealDeps: () => createIdleKillRealDeps,
4769
4923
  createIdleNudgeRealDeps: () => createIdleNudgeRealDeps,
4924
+ createOrphanReaperRealDeps: () => createOrphanReaperRealDeps,
4770
4925
  createReviewNudgeRealDeps: () => createReviewNudgeRealDeps,
4771
4926
  createSessionTTLRealDeps: () => createSessionTTLRealDeps,
4772
4927
  loadNudgeState: () => loadNudgeState,
4773
4928
  pollIdleEmployees: () => pollIdleEmployees,
4774
4929
  pollIdleKill: () => pollIdleKill,
4775
4930
  pollReviewNudge: () => pollReviewNudge,
4931
+ reapOrphanedMcpProcesses: () => reapOrphanedMcpProcesses,
4776
4932
  saveNudgeState: () => saveNudgeState,
4777
4933
  shouldKillIdleSession: () => shouldKillIdleSession,
4778
4934
  shouldKillSession: () => shouldKillSession,
@@ -4808,7 +4964,9 @@ function shouldKillIdleSession(input) {
4808
4964
  return true;
4809
4965
  }
4810
4966
  async function pollIdleEmployees(deps, lastNudge) {
4811
- const registered = deps.listRegisteredSessions().filter((s) => s.agentId !== "exe");
4967
+ const registered = deps.listRegisteredSessions().filter(
4968
+ (s) => s.agentId !== "exe" && !isCoordinatorName(s.agentId)
4969
+ );
4812
4970
  if (registered.length === 0) return [];
4813
4971
  let liveSessions;
4814
4972
  try {
@@ -4835,7 +4993,9 @@ async function pollIdleEmployees(deps, lastNudge) {
4835
4993
  return nudged;
4836
4994
  }
4837
4995
  function checkSessionTTL(deps) {
4838
- const registered = deps.listRegisteredSessions().filter((s) => s.agentId !== "exe");
4996
+ const registered = deps.listRegisteredSessions().filter(
4997
+ (s) => s.agentId !== "exe" && !isCoordinatorName(s.agentId)
4998
+ );
4839
4999
  if (registered.length === 0) return [];
4840
5000
  let liveSessions;
4841
5001
  try {
@@ -4870,7 +5030,7 @@ function checkSessionTTL(deps) {
4870
5030
  async function pollIdleKill(deps, idleTickCounts, opts) {
4871
5031
  if (!opts.enabled) return [];
4872
5032
  const registered = deps.listRegisteredSessions().filter(
4873
- (s) => s.agentId !== "exe" && !isExeSession(s.windowName)
5033
+ (s) => s.agentId !== "exe" && !isCoordinatorName(s.agentId) && !isExeSession(s.windowName)
4874
5034
  );
4875
5035
  if (registered.length === 0) return [];
4876
5036
  let liveSessions;
@@ -4928,7 +5088,7 @@ async function pollIdleKill(deps, idleTickCounts, opts) {
4928
5088
  async function pollReviewNudge(deps, state) {
4929
5089
  let sessions;
4930
5090
  try {
4931
- sessions = deps.listTmuxSessions().filter((s) => /^exe\d+$/.test(s));
5091
+ sessions = deps.listTmuxSessions().filter((s) => isExeSession(s));
4932
5092
  } catch {
4933
5093
  return [];
4934
5094
  }
@@ -5026,7 +5186,7 @@ function createIdleNudgeRealDeps(getClient2) {
5026
5186
  },
5027
5187
  queryOpenTask: async (agentId) => {
5028
5188
  const client = getClient2();
5029
- const doScope = sessionScopeFilter();
5189
+ const doScope = sessionScopeFilter(null);
5030
5190
  const result = await client.execute({
5031
5191
  sql: `SELECT id, title, priority FROM tasks
5032
5192
  WHERE assigned_to = ? AND status IN ('open', 'in_progress', 'needs_review')${doScope.sql}
@@ -5108,18 +5268,76 @@ function createIdleKillRealDeps(getClient2, intercomAckWindowMs) {
5108
5268
  }
5109
5269
  };
5110
5270
  }
5111
- var IDLE_NUDGE_DEDUP_MS, SESSION_TTL_HOURS, SESSION_CONTEXT_THRESHOLD_PCT, IDLE_KILL_INTERCOM_ACK_WINDOW_MS, REVIEW_NUDGE_COOLDOWN_MS, NUDGE_STATE_PATH;
5271
+ function reapOrphanedMcpProcesses(deps) {
5272
+ const lines = deps.listProcesses();
5273
+ const reaped = [];
5274
+ for (const line of lines) {
5275
+ const trimmed = line.trim();
5276
+ const match = trimmed.match(/^(\d+)\s+1\s+(.+)$/);
5277
+ if (!match) continue;
5278
+ const pid = parseInt(match[1], 10);
5279
+ const args = match[2];
5280
+ if (pid === deps.selfPid) continue;
5281
+ if (!ORPHAN_PATTERNS.some((pat) => args.includes(pat))) continue;
5282
+ try {
5283
+ deps.killProcess(pid, "SIGTERM");
5284
+ } catch {
5285
+ continue;
5286
+ }
5287
+ const desc = `PID ${pid} (${args.slice(0, 100)})`;
5288
+ reaped.push(desc);
5289
+ deps.scheduleKill(pid, ORPHAN_SIGKILL_DELAY_MS, () => {
5290
+ try {
5291
+ deps.killProcess(pid, "SIGKILL");
5292
+ } catch {
5293
+ }
5294
+ });
5295
+ }
5296
+ return reaped;
5297
+ }
5298
+ function createOrphanReaperRealDeps() {
5299
+ return {
5300
+ listProcesses: () => {
5301
+ const output = execSync9("ps -eo pid,ppid,args", {
5302
+ encoding: "utf8",
5303
+ timeout: 5e3
5304
+ });
5305
+ return output.split("\n");
5306
+ },
5307
+ killProcess: (pid, signal) => {
5308
+ process.kill(pid, signal);
5309
+ },
5310
+ scheduleKill: (_pid, delayMs, cb) => {
5311
+ setTimeout(() => {
5312
+ try {
5313
+ cb();
5314
+ } catch {
5315
+ }
5316
+ }, delayMs).unref();
5317
+ },
5318
+ selfPid: process.pid
5319
+ };
5320
+ }
5321
+ var IDLE_NUDGE_DEDUP_MS, SESSION_TTL_HOURS, SESSION_CONTEXT_THRESHOLD_PCT, IDLE_KILL_INTERCOM_ACK_WINDOW_MS, REVIEW_NUDGE_COOLDOWN_MS, NUDGE_STATE_PATH, ORPHAN_SIGKILL_DELAY_MS, ORPHAN_PATTERNS;
5112
5322
  var init_daemon_orchestration = __esm({
5113
5323
  "src/lib/daemon-orchestration.ts"() {
5114
5324
  "use strict";
5115
5325
  init_tmux_routing();
5116
5326
  init_task_scope();
5327
+ init_employees();
5117
5328
  IDLE_NUDGE_DEDUP_MS = 6e4;
5118
5329
  SESSION_TTL_HOURS = 4;
5119
5330
  SESSION_CONTEXT_THRESHOLD_PCT = 50;
5120
5331
  IDLE_KILL_INTERCOM_ACK_WINDOW_MS = 1e4;
5121
5332
  REVIEW_NUDGE_COOLDOWN_MS = 3e5;
5122
5333
  NUDGE_STATE_PATH = join(homedir(), ".exe-os", "review-nudge-state.json");
5334
+ ORPHAN_SIGKILL_DELAY_MS = 5e3;
5335
+ ORPHAN_PATTERNS = [
5336
+ "exe-os/dist/mcp/server.js",
5337
+ "exe-mem/dist/mcp/server.js",
5338
+ "exe-os/dist/hooks/ingest-worker.js",
5339
+ "exe-mem/dist/hooks/ingest-worker.js"
5340
+ ];
5123
5341
  }
5124
5342
  });
5125
5343
 
@@ -5360,7 +5578,11 @@ async function ensureShardSchema(client) {
5360
5578
  "ALTER TABLE memories ADD COLUMN source_path TEXT",
5361
5579
  "ALTER TABLE memories ADD COLUMN source_type TEXT DEFAULT 'text'",
5362
5580
  "ALTER TABLE memories ADD COLUMN tier INTEGER DEFAULT 3",
5363
- "ALTER TABLE memories ADD COLUMN supersedes_id TEXT"
5581
+ "ALTER TABLE memories ADD COLUMN supersedes_id TEXT",
5582
+ // MS-11: draft staging, MS-6a: memory_type, MS-7: trajectory
5583
+ "ALTER TABLE memories ADD COLUMN draft INTEGER DEFAULT 0",
5584
+ "ALTER TABLE memories ADD COLUMN memory_type TEXT DEFAULT 'raw'",
5585
+ "ALTER TABLE memories ADD COLUMN trajectory TEXT"
5364
5586
  ]) {
5365
5587
  try {
5366
5588
  await client.execute(col);
@@ -5490,26 +5712,26 @@ var init_platform_procedures = __esm({
5490
5712
  title: "What is exe-os \u2014 the operating model every agent must understand",
5491
5713
  domain: "architecture",
5492
5714
  priority: "p0",
5493
- content: "Exe OS is an AI employee operating system. A founder runs 5-10 AI agents as a real org: COO (exe), CTO (yoshi), CMO (mari), engineers (tom), content (sasha). Each agent has identity, expertise, and experience layers \u2014 persistent memory that makes them better over time. All data is local-first, E2EE, owned by the user. The MCP server is the ONLY data interface \u2014 never access the DB directly."
5715
+ content: "Exe OS is an AI employee operating system. A founder runs 5-10 AI agents as a real org: COO, CTO, CMO, engineers, and content production specialists. Each agent has identity, expertise, and experience layers \u2014 persistent memory that makes them better over time. All data is local-first, E2EE, owned by the user. The MCP server is the ONLY data interface \u2014 never access the DB directly."
5494
5716
  },
5495
5717
  {
5496
5718
  title: "Mode 1 \u2014 how exe-os runs inside Claude Code",
5497
5719
  domain: "architecture",
5498
5720
  priority: "p0",
5499
- content: "Mode 1: exe-os runs AS hooks + MCP + skills inside Claude Code. The founder opens CC, runs /exe to boot the COO. exe manages employees in tmux sessions. Each exeN is a separate CC window/project. Employees (yoshi, tom, mari) run in their own tmux panes via create_task auto-spawn. The founder talks to exe; exe orchestrates the team. CC is the shell, exe-os is the brain."
5721
+ content: "Mode 1: exe-os runs AS hooks + MCP + skills inside Claude Code. The founder opens CC and boots the COO. The COO manages employees in tmux sessions. Each coordinator session is a separate CC window/project. Employees run in their own tmux panes via create_task auto-spawn. The founder talks to the COO; the COO orchestrates the team. CC is the shell, exe-os is the brain."
5500
5722
  },
5501
5723
  {
5502
- title: "Sessions explained \u2014 what exeN means and how projects work",
5724
+ title: "Sessions explained \u2014 coordinator session names and projects",
5503
5725
  domain: "architecture",
5504
5726
  priority: "p0",
5505
- content: "Each exeN (exe1, exe2, exe3) is an isolated project session. exe1 might be exe-os development, exe2 might be exe-wiki. Each session spawns its own employees: exe1\u2192yoshi-exe1\u2192tom-exe1. Sessions share the same memory DB but tasks are scoped to the session that created them. A founder can run multiple projects simultaneously. Sessions never interfere with each other."
5727
+ content: "Each coordinator session is an isolated project session. One might be exe-os development, another might be exe-wiki. Each session spawns its own employees using {employee}-{coordinatorSession}. Sessions share the same memory DB but tasks are scoped to the session that created them. A founder can run multiple projects simultaneously. Sessions never interfere with each other."
5506
5728
  },
5507
5729
  // --- Hierarchy and dispatch ---
5508
5730
  {
5509
5731
  title: "Chain of command \u2014 who talks to whom",
5510
5732
  domain: "workflow",
5511
5733
  priority: "p0",
5512
- content: "Founder \u2192 exe (COO) \u2192 yoshi (CTO) / mari (CMO). Yoshi \u2192 tom (engineer). Mari \u2192 sasha (content). Never skip levels: exe never assigns directly to tom. Tom never reports directly to exe. If you need cross-team info, use ask_team_memory \u2014 don't read other agents' task folders. Each level owns dispatch downward and review upward."
5734
+ content: "Founder -> COO -> CTO/CMO. CTO -> engineers. CMO -> content production. Never skip levels: the COO does not bypass managers for specialist work. Specialists report to their manager. If you need cross-team info, use ask_team_memory \u2014 don't read other agents' task folders. Each level owns dispatch downward and review upward."
5513
5735
  },
5514
5736
  {
5515
5737
  title: "Single dispatch path \u2014 create_task only",
@@ -5519,30 +5741,30 @@ var init_platform_procedures = __esm({
5519
5741
  },
5520
5742
  // --- Session isolation ---
5521
5743
  {
5522
- title: "Session scoping \u2014 stay in your exe boundary",
5744
+ title: "Session scoping \u2014 stay in your coordinator boundary",
5523
5745
  domain: "security",
5524
5746
  priority: "p0",
5525
- content: "Session scoping is mandatory. Managers dispatch to workers within their own exe session ONLY. exe1\u2192yoshi-exe1\u2192tom-exe1. exe2\u2192yoshi-exe2\u2192tom2-exe2. Cross-session dispatch is blocked by the system. Verify session names before dispatch. Tasks are scoped to the creating exe session."
5747
+ content: "Session scoping is mandatory. Managers dispatch to workers within their own coordinator session ONLY. Employee sessions use {employee}-{coordinatorSession}. Cross-session dispatch is blocked by the system. Verify session names before dispatch. Tasks are scoped to the creating coordinator session."
5526
5748
  },
5527
5749
  {
5528
5750
  title: "Session isolation \u2014 never touch another session's work",
5529
5751
  domain: "workflow",
5530
5752
  priority: "p0",
5531
- content: `Sessions are isolated. exeN owns ONLY tasks it dispatched. (1) Never close/update/cancel tasks from another exe session. (2) Never review work from a different session \u2014 report "belongs to exeN" and skip. (3) Ignore other sessions' items in list_tasks results. (4) Employees inherit session: yoshi-exe1 works ONLY on exe1 tasks. Cross-session work is a system violation.`
5753
+ content: "Sessions are isolated. A coordinator session owns ONLY tasks it dispatched. (1) Never close/update/cancel tasks from another coordinator session. (2) Never review work from a different session \u2014 report that it belongs to another session and skip. (3) Ignore other sessions' items in list_tasks results. (4) Employees inherit session: employee sessions work ONLY on their parent coordinator session's tasks. Cross-session work is a system violation."
5532
5754
  },
5533
5755
  // --- Engineering: session scoping in code ---
5534
5756
  {
5535
5757
  title: "Three-dimensional scoping \u2014 session, project, role \u2014 enforced in every query",
5536
5758
  domain: "architecture",
5537
5759
  priority: "p0",
5538
- content: "Every DB query, notification, review count, and task operation MUST be scoped on 3 dimensions: (1) Session \u2014 filter by session_scope matching current exeN. (2) Project \u2014 filter by project_name. (3) Role \u2014 agents only see data at their hierarchy level. When writing ANY function that touches tasks, reviews, messages, or notifications: always accept a sessionScope parameter and pass it to the SQL WHERE clause. Unscoped queries are bugs. Test by running 2+ exe sessions simultaneously."
5760
+ content: "Every DB query, notification, review count, and task operation MUST be scoped on 3 dimensions: (1) Session \u2014 filter by session_scope matching the current coordinator session. (2) Project \u2014 filter by project_name. (3) Role \u2014 agents only see data at their hierarchy level. When writing ANY function that touches tasks, reviews, messages, or notifications: always accept a sessionScope parameter and pass it to the SQL WHERE clause. Unscoped queries are bugs. Test by running 2+ coordinator sessions simultaneously."
5539
5761
  },
5540
5762
  // --- Hard constraints ---
5541
5763
  {
5542
5764
  title: "What you CANNOT do in exe-os \u2014 hard constraints",
5543
5765
  domain: "security",
5544
5766
  priority: "p0",
5545
- content: "NEVER: (1) Access the database directly \u2014 it's SQLCipher encrypted, always fails. Use MCP tools only. (2) Manually spawn tmux sessions \u2014 create_task handles it. (3) Run git checkout main \u2014 agents work in worktrees. (4) Modify another agent's in-progress task. (5) Push to remote \u2014 exe reviews and pushes. (6) Skip update_task(done) \u2014 it's the ONLY way your work gets reviewed. (7) Run git init."
5767
+ content: "NEVER: (1) Access the database directly \u2014 it's SQLCipher encrypted, always fails. Use MCP tools only. (2) Manually spawn tmux sessions \u2014 create_task handles it. (3) Run git checkout main \u2014 agents work in worktrees. (4) Modify another agent's in-progress task. (5) Push to remote \u2014 the COO reviews and pushes. (6) Skip update_task(done) \u2014 it's the ONLY way your work gets reviewed. (7) Run git init."
5546
5768
  },
5547
5769
  // --- Operations ---
5548
5770
  {
@@ -5782,7 +6004,10 @@ async function writeMemory(record) {
5782
6004
  source_path: record.source_path ?? null,
5783
6005
  source_type: record.source_type ?? null,
5784
6006
  tier: record.tier ?? classifyTier(record),
5785
- supersedes_id: record.supersedes_id ?? null
6007
+ supersedes_id: record.supersedes_id ?? null,
6008
+ draft: record.draft ? 1 : 0,
6009
+ memory_type: record.memory_type ?? "raw",
6010
+ trajectory: record.trajectory ? JSON.stringify(record.trajectory) : null
5786
6011
  };
5787
6012
  _pendingRecords.push(dbRow);
5788
6013
  orgBus.emit({
@@ -5837,6 +6062,9 @@ async function flushBatch() {
5837
6062
  const sourceType = row.source_type ?? null;
5838
6063
  const tier = row.tier ?? 3;
5839
6064
  const supersedesId = row.supersedes_id ?? null;
6065
+ const draft = row.draft ? 1 : 0;
6066
+ const memoryType = row.memory_type ?? "raw";
6067
+ const trajectory = row.trajectory ?? null;
5840
6068
  return {
5841
6069
  sql: hasVector ? `INSERT OR IGNORE INTO memories
5842
6070
  (id, agent_id, agent_role, session_id, timestamp,
@@ -5844,15 +6072,15 @@ async function flushBatch() {
5844
6072
  has_error, raw_text, vector, version, task_id, importance, status,
5845
6073
  confidence, last_accessed,
5846
6074
  workspace_id, document_id, user_id, char_offset, page_number,
5847
- source_path, source_type, tier, supersedes_id)
5848
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, vector32(?), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` : `INSERT OR IGNORE INTO memories
6075
+ source_path, source_type, tier, supersedes_id, draft, memory_type, trajectory)
6076
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, vector32(?), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` : `INSERT OR IGNORE INTO memories
5849
6077
  (id, agent_id, agent_role, session_id, timestamp,
5850
6078
  tool_name, project_name,
5851
6079
  has_error, raw_text, vector, version, task_id, importance, status,
5852
6080
  confidence, last_accessed,
5853
6081
  workspace_id, document_id, user_id, char_offset, page_number,
5854
- source_path, source_type, tier, supersedes_id)
5855
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
6082
+ source_path, source_type, tier, supersedes_id, draft, memory_type, trajectory)
6083
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
5856
6084
  args: hasVector ? [
5857
6085
  row.id,
5858
6086
  row.agent_id,
@@ -5878,7 +6106,10 @@ async function flushBatch() {
5878
6106
  sourcePath,
5879
6107
  sourceType,
5880
6108
  tier,
5881
- supersedesId
6109
+ supersedesId,
6110
+ draft,
6111
+ memoryType,
6112
+ trajectory
5882
6113
  ] : [
5883
6114
  row.id,
5884
6115
  row.agent_id,
@@ -5903,7 +6134,10 @@ async function flushBatch() {
5903
6134
  sourcePath,
5904
6135
  sourceType,
5905
6136
  tier,
5906
- supersedesId
6137
+ supersedesId,
6138
+ draft,
6139
+ memoryType,
6140
+ trajectory
5907
6141
  ]
5908
6142
  };
5909
6143
  };
@@ -5972,6 +6206,8 @@ async function searchMemories(queryVector, agentId, options) {
5972
6206
  const limit = options?.limit ?? 10;
5973
6207
  const statusFilter = options?.includeArchived ? "" : `
5974
6208
  AND COALESCE(status, 'active') = 'active'`;
6209
+ const draftFilter = options?.includeDrafts ? "" : `
6210
+ AND (draft = 0 OR draft IS NULL)`;
5975
6211
  let sql = `SELECT id, agent_id, agent_role, session_id, timestamp,
5976
6212
  tool_name, project_name,
5977
6213
  has_error, raw_text, vector, importance, status,
@@ -5981,7 +6217,7 @@ async function searchMemories(queryVector, agentId, options) {
5981
6217
  source_path, source_type
5982
6218
  FROM memories
5983
6219
  WHERE agent_id = ?
5984
- AND vector IS NOT NULL${statusFilter}
6220
+ AND vector IS NOT NULL${statusFilter}${draftFilter}
5985
6221
  AND COALESCE(confidence, 0.7) >= 0.3`;
5986
6222
  const args = [agentId];
5987
6223
  const scope = buildWikiScopeFilter(options, "");
@@ -6003,6 +6239,10 @@ async function searchMemories(queryVector, agentId, options) {
6003
6239
  sql += ` AND timestamp >= ?`;
6004
6240
  args.push(options.since);
6005
6241
  }
6242
+ if (options?.memoryType) {
6243
+ sql += ` AND memory_type = ?`;
6244
+ args.push(options.memoryType);
6245
+ }
6006
6246
  sql += ` ORDER BY vector_distance_cos(vector, vector32(?))`;
6007
6247
  args.push(vectorToBlob(queryVector));
6008
6248
  sql += ` LIMIT ?`;
@@ -6158,7 +6398,7 @@ import { execSync as execSync10 } from "child_process";
6158
6398
  async function pollPendingReviews(deps, state) {
6159
6399
  let sessions;
6160
6400
  try {
6161
- sessions = deps.listTmuxSessions().filter((s) => /^exe\d+$/.test(s));
6401
+ sessions = deps.listTmuxSessions().filter((s) => isExeSession(s));
6162
6402
  } catch {
6163
6403
  return [];
6164
6404
  }
@@ -6265,10 +6505,11 @@ function createRealDeps(getClient2) {
6265
6505
  },
6266
6506
  countPendingReviews: async () => {
6267
6507
  const client = getClient2();
6508
+ const coordinatorName = getCoordinatorName();
6268
6509
  const rpScope = sessionScopeFilter(void 0, "r");
6269
6510
  const result = await client.execute({
6270
6511
  sql: `SELECT COUNT(*) as count FROM tasks r
6271
- WHERE r.assigned_to = 'exe'
6512
+ WHERE (r.assigned_to = ? OR r.assigned_to = 'exe')
6272
6513
  AND r.status IN ('open', 'in_progress')
6273
6514
  AND r.title LIKE 'Review:%'${rpScope.sql}
6274
6515
  AND NOT EXISTS (
@@ -6277,7 +6518,7 @@ function createRealDeps(getClient2) {
6277
6518
  AND t.status = 'done'
6278
6519
  AND t.updated_at > r.created_at
6279
6520
  )`,
6280
- args: [...rpScope.args]
6521
+ args: [coordinatorName, ...rpScope.args]
6281
6522
  });
6282
6523
  return Number(result.rows[0]?.count ?? 0);
6283
6524
  },
@@ -6287,15 +6528,17 @@ function createRealDeps(getClient2) {
6287
6528
  },
6288
6529
  findOrphanedDoneTasks: async () => {
6289
6530
  const client = getClient2();
6531
+ const coordinatorName = getCoordinatorName();
6290
6532
  const odScope = sessionScopeFilter(void 0, "t");
6291
6533
  const result = await client.execute({
6292
6534
  sql: `SELECT t.id, t.title, t.assigned_to, t.assigned_by,
6293
6535
  t.project_name, t.task_file, t.result, t.status
6294
6536
  FROM tasks t
6295
6537
  WHERE t.status = 'needs_review'
6538
+ AND t.assigned_to != ?
6296
6539
  AND t.assigned_to != 'exe'${odScope.sql}
6297
6540
  AND (t.result IS NULL OR t.result NOT LIKE '%## Review notes%')`,
6298
- args: [...odScope.args]
6541
+ args: [coordinatorName, ...odScope.args]
6299
6542
  });
6300
6543
  return result.rows;
6301
6544
  },
@@ -6327,18 +6570,20 @@ function createRealDeps(getClient2) {
6327
6570
  },
6328
6571
  findUnstartedTasks: async () => {
6329
6572
  const client = getClient2();
6573
+ const coordinatorName = getCoordinatorName();
6330
6574
  const usScope = sessionScopeFilter();
6331
6575
  const result = await client.execute({
6332
6576
  sql: `SELECT id, title, assigned_to, created_at
6333
6577
  FROM tasks
6334
6578
  WHERE status = 'open'
6579
+ AND assigned_to != ?
6335
6580
  AND assigned_to != 'exe'
6336
6581
  AND created_at <= datetime('now', '-5 minutes')${usScope.sql}
6337
6582
  ORDER BY
6338
6583
  CASE priority WHEN 'p0' THEN 0 WHEN 'p1' THEN 1 ELSE 2 END,
6339
6584
  created_at ASC
6340
6585
  LIMIT 20`,
6341
- args: [...usScope.args]
6586
+ args: [coordinatorName, ...usScope.args]
6342
6587
  });
6343
6588
  return result.rows;
6344
6589
  }
@@ -6348,6 +6593,8 @@ var init_review_polling = __esm({
6348
6593
  "src/lib/review-polling.ts"() {
6349
6594
  "use strict";
6350
6595
  init_task_scope();
6596
+ init_tmux_routing();
6597
+ init_employees();
6351
6598
  }
6352
6599
  });
6353
6600
 
@@ -6600,8 +6847,8 @@ async function runConsolidation(client, options) {
6600
6847
  if (clustersProcessed >= options.maxCalls) break;
6601
6848
  if (cluster.memories.length < 3) continue;
6602
6849
  try {
6603
- const isExe = cluster.agentId === "exe";
6604
- if (isExe) {
6850
+ const isCoordinator = cluster.agentId === "exe" || isCoordinatorName(cluster.agentId);
6851
+ if (isCoordinator) {
6605
6852
  const synthesis = await consolidateCluster(cluster, options.model);
6606
6853
  if (!synthesis.trim()) continue;
6607
6854
  const result = await storeConsolidation(client, cluster, synthesis, options.embedFn);
@@ -6630,7 +6877,7 @@ async function runConsolidation(client, options) {
6630
6877
  if (dedupCount === 0) continue;
6631
6878
  }
6632
6879
  clustersProcessed++;
6633
- memoriesConsolidated += isExe ? cluster.memories.length : 0;
6880
+ memoriesConsolidated += isCoordinator ? cluster.memories.length : 0;
6634
6881
  } catch (err) {
6635
6882
  process.stderr.write(
6636
6883
  `[consolidation] Cluster failed (${cluster.projectName}/${cluster.dateRange}): ${err instanceof Error ? err.message : String(err)}
@@ -6717,6 +6964,7 @@ var init_consolidation = __esm({
6717
6964
  "src/lib/consolidation.ts"() {
6718
6965
  "use strict";
6719
6966
  init_store();
6967
+ init_employees();
6720
6968
  WIKI_FETCH_TIMEOUT_MS = 1e4;
6721
6969
  }
6722
6970
  });
@@ -6809,6 +7057,10 @@ function spawnDaemon() {
6809
7057
  stdio: ["ignore", "ignore", stderrFd],
6810
7058
  env: {
6811
7059
  ...process.env,
7060
+ TMUX: void 0,
7061
+ // Daemon is global — must not inherit session scope
7062
+ TMUX_PANE: void 0,
7063
+ // Prevents resolveExeSession() from scoping to one session
6812
7064
  EXE_DAEMON_SOCK: SOCKET_PATH,
6813
7065
  EXE_DAEMON_PID: PID_PATH
6814
7066
  }
@@ -7313,6 +7565,7 @@ var init_code_chunker = __esm({
7313
7565
  // src/lib/graph-rag.ts
7314
7566
  var graph_rag_exports = {};
7315
7567
  __export(graph_rag_exports, {
7568
+ EXTRACT_TOOL: () => EXTRACT_TOOL,
7316
7569
  entityId: () => entityId,
7317
7570
  extractBatch: () => extractBatch,
7318
7571
  extractFromCode: () => extractFromCode,
@@ -7537,17 +7790,32 @@ async function storeExtraction(client, extraction, memoryId, timestamp) {
7537
7790
  const id = aliasTarget ?? entityId(e.name, e.type);
7538
7791
  const normalizedName = normalizeEntityName(e.name);
7539
7792
  try {
7793
+ const newProps = e.properties ?? {};
7794
+ let mergedPropsJson = JSON.stringify(newProps);
7795
+ if (newProps.provenance) {
7796
+ const existing = await client.execute({
7797
+ sql: "SELECT properties FROM entities WHERE id = ?",
7798
+ args: [aliasTarget ?? id]
7799
+ });
7800
+ const existingProps = existing.rows.length > 0 ? JSON.parse(String(existing.rows[0].properties ?? "{}")) : {};
7801
+ const existingProvenance = Array.isArray(existingProps.provenance) ? existingProps.provenance : existingProps.provenance ? [existingProps.provenance] : [];
7802
+ mergedPropsJson = JSON.stringify({
7803
+ ...existingProps,
7804
+ ...newProps,
7805
+ provenance: [...existingProvenance, newProps.provenance]
7806
+ });
7807
+ }
7540
7808
  if (aliasTarget) {
7541
7809
  await client.execute({
7542
- sql: `UPDATE entities SET last_seen = ? WHERE id = ?`,
7543
- args: [timestamp, aliasTarget]
7810
+ sql: `UPDATE entities SET last_seen = ?, properties = ? WHERE id = ?`,
7811
+ args: [timestamp, mergedPropsJson, aliasTarget]
7544
7812
  });
7545
7813
  } else {
7546
7814
  await client.execute({
7547
- sql: `INSERT INTO entities (id, name, type, first_seen, last_seen)
7548
- VALUES (?, ?, ?, ?, ?)
7549
- ON CONFLICT(name, type) DO UPDATE SET last_seen = ?`,
7550
- args: [id, normalizedName, e.type, timestamp, timestamp, timestamp]
7815
+ sql: `INSERT INTO entities (id, name, type, first_seen, last_seen, properties)
7816
+ VALUES (?, ?, ?, ?, ?, ?)
7817
+ ON CONFLICT(name, type) DO UPDATE SET last_seen = ?, properties = ?`,
7818
+ args: [id, normalizedName, e.type, timestamp, timestamp, mergedPropsJson, timestamp, mergedPropsJson]
7551
7819
  });
7552
7820
  }
7553
7821
  await client.execute({
@@ -7576,12 +7844,27 @@ async function storeExtraction(client, extraction, memoryId, timestamp) {
7576
7844
  VALUES (?, ?, ?, ?, ?)`,
7577
7845
  args: [targetId, r.target, r.targetType, timestamp, timestamp]
7578
7846
  });
7847
+ const relProps = r.properties ?? {};
7848
+ let relPropsJson = JSON.stringify(relProps);
7849
+ if (relProps.provenance) {
7850
+ const existingRels = await client.execute({
7851
+ sql: "SELECT properties FROM relationships WHERE source_entity_id = ? AND target_entity_id = ? AND type = ?",
7852
+ args: [sourceId, targetId, r.relationship]
7853
+ });
7854
+ const existingRelProps = existingRels.rows.length > 0 ? JSON.parse(String(existingRels.rows[0].properties ?? "{}")) : {};
7855
+ const existingProvenance = Array.isArray(existingRelProps.provenance) ? existingRelProps.provenance : existingRelProps.provenance ? [existingRelProps.provenance] : [];
7856
+ relPropsJson = JSON.stringify({
7857
+ ...existingRelProps,
7858
+ ...relProps,
7859
+ provenance: [...existingProvenance, relProps.provenance]
7860
+ });
7861
+ }
7579
7862
  await client.execute({
7580
- sql: `INSERT INTO relationships (id, source_entity_id, target_entity_id, type, weight, timestamp, confidence, confidence_label)
7581
- VALUES (?, ?, ?, ?, 1.0, ?, ?, ?)
7863
+ sql: `INSERT INTO relationships (id, source_entity_id, target_entity_id, type, weight, timestamp, confidence, confidence_label, properties)
7864
+ VALUES (?, ?, ?, ?, 1.0, ?, ?, ?, ?)
7582
7865
  ON CONFLICT(source_entity_id, target_entity_id, type)
7583
7866
  DO UPDATE SET weight = MIN(weight + 0.1, 2.0), timestamp = ?,
7584
- confidence = MAX(confidence, ?), confidence_label = ?`,
7867
+ confidence = MAX(confidence, ?), confidence_label = ?, properties = ?`,
7585
7868
  args: [
7586
7869
  relId,
7587
7870
  sourceId,
@@ -7590,9 +7873,11 @@ async function storeExtraction(client, extraction, memoryId, timestamp) {
7590
7873
  timestamp,
7591
7874
  r.confidence,
7592
7875
  r.confidenceLabel,
7876
+ relPropsJson,
7593
7877
  timestamp,
7594
7878
  r.confidence,
7595
- r.confidenceLabel
7879
+ r.confidenceLabel,
7880
+ relPropsJson
7596
7881
  ]
7597
7882
  });
7598
7883
  const existingRel = await client.execute({
@@ -8150,16 +8435,25 @@ function createWsClient(config, handlers) {
8150
8435
  } catch {
8151
8436
  }
8152
8437
  };
8153
- ws.onclose = () => {
8438
+ ws.onclose = (event) => {
8154
8439
  connected = false;
8155
8440
  if (heartbeatTimer) {
8156
8441
  clearInterval(heartbeatTimer);
8157
8442
  heartbeatTimer = null;
8158
8443
  }
8444
+ const code = event.code ?? "unknown";
8445
+ const reason = event.reason || "(none)";
8446
+ process.stderr.write(
8447
+ `[ws-client] Disconnected: code=${code} reason=${reason}
8448
+ `
8449
+ );
8159
8450
  handlers.onDisconnect();
8160
8451
  scheduleReconnect();
8161
8452
  };
8162
- ws.onerror = () => {
8453
+ ws.onerror = (event) => {
8454
+ const errMsg = event.message ?? "unknown error";
8455
+ process.stderr.write(`[ws-client] Error: ${errMsg}
8456
+ `);
8163
8457
  };
8164
8458
  } catch {
8165
8459
  connected = false;
@@ -8176,11 +8470,12 @@ function createWsClient(config, handlers) {
8176
8470
  }
8177
8471
  function scheduleReconnect() {
8178
8472
  if (closed || reconnectTimer) return;
8473
+ const jitter = reconnectDelay * (0.75 + Math.random() * 0.5);
8179
8474
  reconnectTimer = setTimeout(() => {
8180
8475
  reconnectTimer = null;
8181
8476
  reconnectDelay = Math.min(reconnectDelay * 2, MAX_RECONNECT_MS);
8182
8477
  connect();
8183
- }, reconnectDelay);
8478
+ }, jitter);
8184
8479
  }
8185
8480
  connect();
8186
8481
  return {
@@ -8362,7 +8657,7 @@ async function deliverLocalMessage(messageId) {
8362
8657
  try {
8363
8658
  const exeSession = resolveExeSession();
8364
8659
  if (!exeSession) {
8365
- throw new Error("No exe session found");
8660
+ throw new Error("No coordinator session found");
8366
8661
  }
8367
8662
  const sessionName = employeeSessionName(targetAgent, exeSession);
8368
8663
  if (!isEmployeeAlive(sessionName)) {
@@ -9017,6 +9312,49 @@ function startWikiSync() {
9017
9312
  }).catch(() => {
9018
9313
  });
9019
9314
  }
9315
+ var AGENT_STATS_INTERVAL_MS = 60 * 1e3;
9316
+ var AGENT_STATS_PATH = path20.join(EXE_AI_DIR, "agent-stats.json");
9317
+ async function writeAgentStats() {
9318
+ if (!await ensureStoreForPolling()) return;
9319
+ try {
9320
+ const { getClient: getClient2 } = await Promise.resolve().then(() => (init_database(), database_exports));
9321
+ const client = getClient2();
9322
+ const result = await client.execute({
9323
+ sql: `SELECT agent_id,
9324
+ COUNT(*) as total,
9325
+ SUM(CASE WHEN timestamp > datetime('now', '-7 days') THEN 1 ELSE 0 END) as growth_7d
9326
+ FROM memories
9327
+ WHERE agent_id != 'default'
9328
+ GROUP BY agent_id
9329
+ ORDER BY total DESC`,
9330
+ args: []
9331
+ });
9332
+ const agents = result.rows.map((row) => ({
9333
+ id: row.agent_id,
9334
+ total: Number(row.total),
9335
+ growth7d: Number(row.growth_7d)
9336
+ }));
9337
+ const stats = {
9338
+ generated: (/* @__PURE__ */ new Date()).toISOString(),
9339
+ agents,
9340
+ daemon: {
9341
+ uptime: Math.floor((Date.now() - _startedAt) / 1e3),
9342
+ pid: process.pid
9343
+ }
9344
+ };
9345
+ writeFileSync9(AGENT_STATS_PATH, JSON.stringify(stats, null, 2), "utf8");
9346
+ } catch (err) {
9347
+ process.stderr.write(`[exed] Agent stats error: ${err instanceof Error ? err.message : String(err)}
9348
+ `);
9349
+ }
9350
+ }
9351
+ function startAgentStats() {
9352
+ void writeAgentStats();
9353
+ const timer = setInterval(() => void writeAgentStats(), AGENT_STATS_INTERVAL_MS);
9354
+ timer.unref();
9355
+ process.stderr.write(`[exed] Agent stats started (every ${AGENT_STATS_INTERVAL_MS / 1e3}s)
9356
+ `);
9357
+ }
9020
9358
  function startCapacityMonitoring() {
9021
9359
  const tick = async () => {
9022
9360
  if (!await ensureStoreForPolling()) return;
@@ -9099,6 +9437,28 @@ function startIntercomQueueDrain() {
9099
9437
  process.stderr.write(`[exed] Intercom queue drain started (every ${QUEUE_DRAIN_INTERVAL_MS / 1e3}s)
9100
9438
  `);
9101
9439
  }
9440
+ var ORPHAN_REAP_INTERVAL_MS = 5 * 60 * 1e3;
9441
+ function startOrphanReaper() {
9442
+ const tick = async () => {
9443
+ try {
9444
+ const { reapOrphanedMcpProcesses: reapOrphanedMcpProcesses2, createOrphanReaperRealDeps: createOrphanReaperRealDeps2 } = await Promise.resolve().then(() => (init_daemon_orchestration(), daemon_orchestration_exports));
9445
+ const deps = createOrphanReaperRealDeps2();
9446
+ const reaped = await reapOrphanedMcpProcesses2(deps);
9447
+ for (const entry of reaped) {
9448
+ process.stderr.write(`[exed] Orphan reaper: killed ${entry}
9449
+ `);
9450
+ }
9451
+ } catch (err) {
9452
+ process.stderr.write(`[exed] Orphan reaper error: ${err instanceof Error ? err.message : String(err)}
9453
+ `);
9454
+ }
9455
+ };
9456
+ void tick();
9457
+ const timer = setInterval(() => void tick(), ORPHAN_REAP_INTERVAL_MS);
9458
+ timer.unref();
9459
+ process.stderr.write(`[exed] Orphan reaper started (every ${ORPHAN_REAP_INTERVAL_MS / 6e4}m)
9460
+ `);
9461
+ }
9102
9462
  process.on("SIGINT", () => void shutdown());
9103
9463
  process.on("SIGTERM", () => void shutdown());
9104
9464
  function checkExistingDaemon() {
@@ -9182,6 +9542,8 @@ try {
9182
9542
  startIdleKill();
9183
9543
  startConsolidation();
9184
9544
  startSkillSweep();
9545
+ startOrphanReaper();
9546
+ startAgentStats();
9185
9547
  startCapacityMonitoring();
9186
9548
  startGraphExtraction();
9187
9549
  startWikiSync();