@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
package/dist/index.js CHANGED
@@ -25,6 +25,444 @@ var __copyProps = (to, from, except, desc) => {
25
25
  };
26
26
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
27
27
 
28
+ // src/lib/config.ts
29
+ var config_exports = {};
30
+ __export(config_exports, {
31
+ CONFIG_MIGRATIONS: () => CONFIG_MIGRATIONS,
32
+ CONFIG_PATH: () => CONFIG_PATH,
33
+ CURRENT_CONFIG_VERSION: () => CURRENT_CONFIG_VERSION,
34
+ DB_PATH: () => DB_PATH,
35
+ EXE_AI_DIR: () => EXE_AI_DIR,
36
+ LEGACY_LANCE_PATH: () => LEGACY_LANCE_PATH,
37
+ MODELS_DIR: () => MODELS_DIR,
38
+ loadConfig: () => loadConfig,
39
+ loadConfigFrom: () => loadConfigFrom,
40
+ loadConfigSync: () => loadConfigSync,
41
+ migrateConfig: () => migrateConfig,
42
+ saveConfig: () => saveConfig
43
+ });
44
+ import { readFile, writeFile, mkdir, chmod } from "fs/promises";
45
+ import { readFileSync, existsSync, renameSync } from "fs";
46
+ import path2 from "path";
47
+ import os2 from "os";
48
+ function resolveDataDir() {
49
+ if (process.env.EXE_OS_DIR) return process.env.EXE_OS_DIR;
50
+ if (process.env.EXE_MEM_DIR) return process.env.EXE_MEM_DIR;
51
+ const newDir = path2.join(os2.homedir(), ".exe-os");
52
+ const legacyDir = path2.join(os2.homedir(), ".exe-mem");
53
+ if (!existsSync(newDir) && existsSync(legacyDir)) {
54
+ try {
55
+ renameSync(legacyDir, newDir);
56
+ process.stderr.write(`[exe-os] Migrated data directory: ~/.exe-mem \u2192 ~/.exe-os
57
+ `);
58
+ } catch {
59
+ return legacyDir;
60
+ }
61
+ }
62
+ return newDir;
63
+ }
64
+ function migrateLegacyConfig(raw) {
65
+ if ("r2" in raw) {
66
+ process.stderr.write(
67
+ "[exe-os] Warning: config.json contains deprecated 'r2' field from v1.0. R2 sync has been replaced in v1.1. The 'r2' field will be ignored.\n"
68
+ );
69
+ delete raw.r2;
70
+ }
71
+ if ("syncIntervalMs" in raw) {
72
+ delete raw.syncIntervalMs;
73
+ }
74
+ return raw;
75
+ }
76
+ function migrateConfig(raw) {
77
+ const fromVersion = typeof raw.config_version === "number" ? raw.config_version : 0;
78
+ let currentVersion = fromVersion;
79
+ let migrated = false;
80
+ if (currentVersion > CURRENT_CONFIG_VERSION) {
81
+ return { config: raw, migrated: false, fromVersion };
82
+ }
83
+ for (const migration of CONFIG_MIGRATIONS) {
84
+ if (currentVersion === migration.from && migration.to <= CURRENT_CONFIG_VERSION) {
85
+ raw = migration.migrate(raw);
86
+ currentVersion = migration.to;
87
+ migrated = true;
88
+ }
89
+ }
90
+ return { config: raw, migrated, fromVersion };
91
+ }
92
+ function normalizeScalingRoadmap(raw) {
93
+ const defaultAuto = DEFAULT_CONFIG.scalingRoadmap.rerankerAutoTrigger;
94
+ const userRoadmap = raw.scalingRoadmap ?? {};
95
+ const userAuto = userRoadmap.rerankerAutoTrigger ?? {};
96
+ if (userAuto.enabled === void 0 && raw.rerankerEnabled !== void 0) {
97
+ userAuto.enabled = raw.rerankerEnabled;
98
+ }
99
+ raw.scalingRoadmap = {
100
+ ...userRoadmap,
101
+ rerankerAutoTrigger: { ...defaultAuto, ...userAuto }
102
+ };
103
+ }
104
+ function normalizeSessionLifecycle(raw) {
105
+ const defaultSL = DEFAULT_CONFIG.sessionLifecycle;
106
+ const userSL = raw.sessionLifecycle ?? {};
107
+ raw.sessionLifecycle = { ...defaultSL, ...userSL };
108
+ }
109
+ function normalizeAutoUpdate(raw) {
110
+ const defaultAU = DEFAULT_CONFIG.autoUpdate;
111
+ const userAU = raw.autoUpdate ?? {};
112
+ raw.autoUpdate = { ...defaultAU, ...userAU };
113
+ }
114
+ async function loadConfig() {
115
+ const dir = process.env.EXE_OS_DIR ?? process.env.EXE_MEM_DIR ?? EXE_AI_DIR;
116
+ await mkdir(dir, { recursive: true });
117
+ const configPath = path2.join(dir, "config.json");
118
+ if (!existsSync(configPath)) {
119
+ return { ...DEFAULT_CONFIG, dbPath: path2.join(dir, "memories.db") };
120
+ }
121
+ const raw = await readFile(configPath, "utf-8");
122
+ try {
123
+ let parsed = JSON.parse(raw);
124
+ parsed = migrateLegacyConfig(parsed);
125
+ const { config: migratedCfg, migrated, fromVersion } = migrateConfig(parsed);
126
+ if (migrated) {
127
+ process.stderr.write(`[exe-os] Config migrated from v${fromVersion} to v${migratedCfg.config_version}
128
+ `);
129
+ try {
130
+ await writeFile(configPath, JSON.stringify(migratedCfg, null, 2) + "\n");
131
+ } catch {
132
+ }
133
+ }
134
+ normalizeScalingRoadmap(migratedCfg);
135
+ normalizeSessionLifecycle(migratedCfg);
136
+ normalizeAutoUpdate(migratedCfg);
137
+ const config2 = { ...DEFAULT_CONFIG, dbPath: path2.join(dir, "memories.db"), ...migratedCfg };
138
+ if (config2.dbPath.startsWith("~")) {
139
+ config2.dbPath = config2.dbPath.replace(/^~/, os2.homedir());
140
+ }
141
+ return config2;
142
+ } catch {
143
+ return { ...DEFAULT_CONFIG, dbPath: path2.join(dir, "memories.db") };
144
+ }
145
+ }
146
+ function loadConfigSync() {
147
+ const dir = process.env.EXE_OS_DIR ?? process.env.EXE_MEM_DIR ?? EXE_AI_DIR;
148
+ const configPath = path2.join(dir, "config.json");
149
+ if (!existsSync(configPath)) {
150
+ return { ...DEFAULT_CONFIG, dbPath: path2.join(dir, "memories.db") };
151
+ }
152
+ try {
153
+ const raw = readFileSync(configPath, "utf-8");
154
+ let parsed = JSON.parse(raw);
155
+ parsed = migrateLegacyConfig(parsed);
156
+ const { config: migratedCfg } = migrateConfig(parsed);
157
+ normalizeScalingRoadmap(migratedCfg);
158
+ normalizeSessionLifecycle(migratedCfg);
159
+ normalizeAutoUpdate(migratedCfg);
160
+ return { ...DEFAULT_CONFIG, dbPath: path2.join(dir, "memories.db"), ...migratedCfg };
161
+ } catch {
162
+ return { ...DEFAULT_CONFIG, dbPath: path2.join(dir, "memories.db") };
163
+ }
164
+ }
165
+ async function saveConfig(config2) {
166
+ const dir = process.env.EXE_OS_DIR ?? process.env.EXE_MEM_DIR ?? EXE_AI_DIR;
167
+ await mkdir(dir, { recursive: true });
168
+ const configPath = path2.join(dir, "config.json");
169
+ await writeFile(configPath, JSON.stringify(config2, null, 2) + "\n");
170
+ if (config2.cloud?.apiKey) {
171
+ await chmod(configPath, 384);
172
+ }
173
+ }
174
+ async function loadConfigFrom(configPath) {
175
+ const raw = await readFile(configPath, "utf-8");
176
+ try {
177
+ let parsed = JSON.parse(raw);
178
+ parsed = migrateLegacyConfig(parsed);
179
+ const { config: migratedCfg } = migrateConfig(parsed);
180
+ normalizeScalingRoadmap(migratedCfg);
181
+ normalizeSessionLifecycle(migratedCfg);
182
+ normalizeAutoUpdate(migratedCfg);
183
+ return { ...DEFAULT_CONFIG, ...migratedCfg };
184
+ } catch {
185
+ return { ...DEFAULT_CONFIG };
186
+ }
187
+ }
188
+ var EXE_AI_DIR, DB_PATH, MODELS_DIR, CONFIG_PATH, LEGACY_LANCE_PATH, CURRENT_CONFIG_VERSION, DEFAULT_CONFIG, CONFIG_MIGRATIONS;
189
+ var init_config = __esm({
190
+ "src/lib/config.ts"() {
191
+ "use strict";
192
+ EXE_AI_DIR = resolveDataDir();
193
+ DB_PATH = path2.join(EXE_AI_DIR, "memories.db");
194
+ MODELS_DIR = path2.join(EXE_AI_DIR, "models");
195
+ CONFIG_PATH = path2.join(EXE_AI_DIR, "config.json");
196
+ LEGACY_LANCE_PATH = path2.join(EXE_AI_DIR, "local.lance");
197
+ CURRENT_CONFIG_VERSION = 1;
198
+ DEFAULT_CONFIG = {
199
+ config_version: CURRENT_CONFIG_VERSION,
200
+ dbPath: DB_PATH,
201
+ modelFile: "jina-embeddings-v5-small-q4_k_m.gguf",
202
+ embeddingDim: 1024,
203
+ batchSize: 20,
204
+ flushIntervalMs: 1e4,
205
+ autoIngestion: true,
206
+ autoRetrieval: true,
207
+ searchMode: "hybrid",
208
+ hookSearchMode: "hybrid",
209
+ fileGrepEnabled: true,
210
+ splashEffect: true,
211
+ consolidationEnabled: true,
212
+ consolidationIntervalMs: 6 * 60 * 60 * 1e3,
213
+ consolidationModel: "claude-haiku-4-5-20251001",
214
+ consolidationMaxCallsPerRun: 20,
215
+ selfQueryRouter: true,
216
+ selfQueryModel: "claude-haiku-4-5-20251001",
217
+ rerankerEnabled: true,
218
+ scalingRoadmap: {
219
+ rerankerAutoTrigger: {
220
+ enabled: true,
221
+ broadQueryMinCardinality: 5e4,
222
+ fetchTopK: 150,
223
+ returnTopK: 5
224
+ }
225
+ },
226
+ graphRagEnabled: true,
227
+ wikiEnabled: false,
228
+ wikiUrl: "",
229
+ wikiApiKey: "",
230
+ wikiSyncIntervalMs: 30 * 60 * 1e3,
231
+ wikiWorkspaceMapping: {},
232
+ wikiAutoUpdate: true,
233
+ wikiAutoUpdateThreshold: 0.5,
234
+ wikiAutoUpdateCreateNew: true,
235
+ skillLearning: true,
236
+ skillThreshold: 3,
237
+ skillModel: "claude-haiku-4-5-20251001",
238
+ exeHeartbeat: {
239
+ enabled: true,
240
+ intervalSeconds: 60,
241
+ staleInProgressThresholdHours: 2
242
+ },
243
+ sessionLifecycle: {
244
+ idleKillEnabled: true,
245
+ idleKillTicksRequired: 3,
246
+ idleKillIntercomAckWindowMs: 1e4,
247
+ maxAutoInstances: 10
248
+ },
249
+ autoUpdate: {
250
+ checkOnBoot: true,
251
+ autoInstall: false,
252
+ checkIntervalMs: 24 * 60 * 60 * 1e3
253
+ }
254
+ };
255
+ CONFIG_MIGRATIONS = [
256
+ {
257
+ from: 0,
258
+ to: 1,
259
+ migrate: (cfg) => {
260
+ cfg.config_version = 1;
261
+ return cfg;
262
+ }
263
+ }
264
+ ];
265
+ }
266
+ });
267
+
268
+ // src/lib/employees.ts
269
+ var employees_exports = {};
270
+ __export(employees_exports, {
271
+ COORDINATOR_ROLE: () => COORDINATOR_ROLE,
272
+ DEFAULT_COORDINATOR_TEMPLATE_NAME: () => DEFAULT_COORDINATOR_TEMPLATE_NAME,
273
+ EMPLOYEES_PATH: () => EMPLOYEES_PATH,
274
+ addEmployee: () => addEmployee,
275
+ canCoordinate: () => canCoordinate,
276
+ getCoordinatorEmployee: () => getCoordinatorEmployee,
277
+ getCoordinatorName: () => getCoordinatorName,
278
+ getEmployee: () => getEmployee,
279
+ getEmployeeByRole: () => getEmployeeByRole,
280
+ getEmployeeNamesByRole: () => getEmployeeNamesByRole,
281
+ hasRole: () => hasRole,
282
+ isCoordinatorName: () => isCoordinatorName,
283
+ isCoordinatorRole: () => isCoordinatorRole,
284
+ isMultiInstance: () => isMultiInstance,
285
+ loadEmployees: () => loadEmployees,
286
+ loadEmployeesSync: () => loadEmployeesSync,
287
+ normalizeRole: () => normalizeRole,
288
+ normalizeRosterCase: () => normalizeRosterCase,
289
+ registerBinSymlinks: () => registerBinSymlinks,
290
+ saveEmployees: () => saveEmployees,
291
+ validateEmployeeName: () => validateEmployeeName
292
+ });
293
+ import { readFile as readFile2, writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
294
+ import { existsSync as existsSync2, symlinkSync, readlinkSync, readFileSync as readFileSync2, renameSync as renameSync2, unlinkSync, writeFileSync } from "fs";
295
+ import { execSync } from "child_process";
296
+ import path3 from "path";
297
+ import os3 from "os";
298
+ function normalizeRole(role) {
299
+ return (role ?? "").trim().toLowerCase();
300
+ }
301
+ function isCoordinatorRole(role) {
302
+ return normalizeRole(role) === normalizeRole(COORDINATOR_ROLE);
303
+ }
304
+ function getCoordinatorEmployee(employees) {
305
+ return employees.find((e) => isCoordinatorRole(e.role));
306
+ }
307
+ function getCoordinatorName(employees = loadEmployeesSync()) {
308
+ return getCoordinatorEmployee(employees)?.name ?? DEFAULT_COORDINATOR_TEMPLATE_NAME;
309
+ }
310
+ function isCoordinatorName(agentName, employees = loadEmployeesSync()) {
311
+ if (!agentName) return false;
312
+ return agentName.toLowerCase() === getCoordinatorName(employees).toLowerCase();
313
+ }
314
+ function canCoordinate(agentName, agentRole, employees = loadEmployeesSync()) {
315
+ return agentName === "default" || isCoordinatorRole(agentRole) || isCoordinatorName(agentName, employees);
316
+ }
317
+ function validateEmployeeName(name) {
318
+ if (!name) {
319
+ return { valid: false, error: "Name is required" };
320
+ }
321
+ if (name.length > 32) {
322
+ return { valid: false, error: "Name must be 32 characters or fewer" };
323
+ }
324
+ if (!/^[a-z][a-z0-9]*$/.test(name)) {
325
+ return {
326
+ valid: false,
327
+ error: "Name must start with a letter and contain only lowercase alphanumeric characters"
328
+ };
329
+ }
330
+ return { valid: true };
331
+ }
332
+ async function loadEmployees(employeesPath = EMPLOYEES_PATH) {
333
+ if (!existsSync2(employeesPath)) {
334
+ return [];
335
+ }
336
+ const raw = await readFile2(employeesPath, "utf-8");
337
+ try {
338
+ return JSON.parse(raw);
339
+ } catch {
340
+ return [];
341
+ }
342
+ }
343
+ async function saveEmployees(employees, employeesPath = EMPLOYEES_PATH) {
344
+ await mkdir2(path3.dirname(employeesPath), { recursive: true });
345
+ await writeFile2(employeesPath, JSON.stringify(employees, null, 2) + "\n", "utf-8");
346
+ }
347
+ function loadEmployeesSync(employeesPath = EMPLOYEES_PATH) {
348
+ if (!existsSync2(employeesPath)) return [];
349
+ try {
350
+ return JSON.parse(readFileSync2(employeesPath, "utf-8"));
351
+ } catch {
352
+ return [];
353
+ }
354
+ }
355
+ function getEmployee(employees, name) {
356
+ return employees.find((e) => e.name.toLowerCase() === name.toLowerCase());
357
+ }
358
+ function getEmployeeByRole(employees, role) {
359
+ const lower = role.toLowerCase();
360
+ return employees.find((e) => e.role.toLowerCase() === lower);
361
+ }
362
+ function getEmployeeNamesByRole(employees, role) {
363
+ const lower = role.toLowerCase();
364
+ return employees.filter((e) => e.role.toLowerCase() === lower).map((e) => e.name);
365
+ }
366
+ function hasRole(agentName, role) {
367
+ const employees = loadEmployeesSync();
368
+ const emp = getEmployee(employees, agentName);
369
+ return emp ? emp.role.toLowerCase() === role.toLowerCase() : false;
370
+ }
371
+ function isMultiInstance(agentName, employees) {
372
+ const roster = employees ?? loadEmployeesSync();
373
+ const emp = getEmployee(roster, agentName);
374
+ if (!emp) return false;
375
+ return MULTI_INSTANCE_ROLES.has(emp.role.toLowerCase());
376
+ }
377
+ function addEmployee(employees, employee) {
378
+ const normalized = { ...employee, name: employee.name.toLowerCase() };
379
+ if (employees.some((e) => e.name.toLowerCase() === normalized.name)) {
380
+ throw new Error(`Employee '${normalized.name}' already exists`);
381
+ }
382
+ return [...employees, normalized];
383
+ }
384
+ async function normalizeRosterCase(rosterPath) {
385
+ const employees = await loadEmployees(rosterPath);
386
+ let changed = false;
387
+ for (const emp of employees) {
388
+ if (emp.name !== emp.name.toLowerCase()) {
389
+ const oldName = emp.name;
390
+ emp.name = emp.name.toLowerCase();
391
+ changed = true;
392
+ try {
393
+ const identityDir = path3.join(os3.homedir(), ".exe-os", "identity");
394
+ const oldPath = path3.join(identityDir, `${oldName}.md`);
395
+ const newPath = path3.join(identityDir, `${emp.name}.md`);
396
+ if (existsSync2(oldPath) && !existsSync2(newPath)) {
397
+ renameSync2(oldPath, newPath);
398
+ } else if (existsSync2(oldPath) && oldPath !== newPath) {
399
+ const content = readFileSync2(oldPath, "utf-8");
400
+ writeFileSync(newPath, content, "utf-8");
401
+ if (oldPath.toLowerCase() !== newPath.toLowerCase()) {
402
+ unlinkSync(oldPath);
403
+ }
404
+ }
405
+ } catch {
406
+ }
407
+ }
408
+ }
409
+ if (changed) {
410
+ await saveEmployees(employees, rosterPath);
411
+ }
412
+ return changed;
413
+ }
414
+ function findExeBin() {
415
+ try {
416
+ return execSync(process.platform === "win32" ? "where exe-os" : "which exe-os", { encoding: "utf8" }).trim();
417
+ } catch {
418
+ return null;
419
+ }
420
+ }
421
+ function registerBinSymlinks(name) {
422
+ const created = [];
423
+ const skipped = [];
424
+ const errors = [];
425
+ const exeBinPath = findExeBin();
426
+ if (!exeBinPath) {
427
+ errors.push("Could not find 'exe-os' in PATH");
428
+ return { created, skipped, errors };
429
+ }
430
+ const binDir = path3.dirname(exeBinPath);
431
+ let target;
432
+ try {
433
+ target = readlinkSync(exeBinPath);
434
+ } catch {
435
+ errors.push("Could not read 'exe' symlink");
436
+ return { created, skipped, errors };
437
+ }
438
+ for (const suffix of ["", "-opencode"]) {
439
+ const linkName = `${name}${suffix}`;
440
+ const linkPath = path3.join(binDir, linkName);
441
+ if (existsSync2(linkPath)) {
442
+ skipped.push(linkName);
443
+ continue;
444
+ }
445
+ try {
446
+ symlinkSync(target, linkPath);
447
+ created.push(linkName);
448
+ } catch (err) {
449
+ errors.push(`${linkName}: ${err instanceof Error ? err.message : String(err)}`);
450
+ }
451
+ }
452
+ return { created, skipped, errors };
453
+ }
454
+ var EMPLOYEES_PATH, DEFAULT_COORDINATOR_TEMPLATE_NAME, COORDINATOR_ROLE, MULTI_INSTANCE_ROLES;
455
+ var init_employees = __esm({
456
+ "src/lib/employees.ts"() {
457
+ "use strict";
458
+ init_config();
459
+ EMPLOYEES_PATH = path3.join(EXE_AI_DIR, "exe-employees.json");
460
+ DEFAULT_COORDINATOR_TEMPLATE_NAME = "exe";
461
+ COORDINATOR_ROLE = "COO";
462
+ MULTI_INSTANCE_ROLES = /* @__PURE__ */ new Set(["principal engineer", "content production specialist", "staff code reviewer"]);
463
+ }
464
+ });
465
+
28
466
  // src/lib/session-registry.ts
29
467
  var session_registry_exports = {};
30
468
  __export(session_registry_exports, {
@@ -32,13 +470,13 @@ __export(session_registry_exports, {
32
470
  pruneStaleSessions: () => pruneStaleSessions,
33
471
  registerSession: () => registerSession
34
472
  });
35
- import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
36
- import { execSync } from "child_process";
37
- import path2 from "path";
38
- import os2 from "os";
473
+ import { readFileSync as readFileSync3, writeFileSync as writeFileSync2, mkdirSync, existsSync as existsSync3 } from "fs";
474
+ import { execSync as execSync2 } from "child_process";
475
+ import path4 from "path";
476
+ import os4 from "os";
39
477
  function registerSession(entry) {
40
- const dir = path2.dirname(REGISTRY_PATH);
41
- if (!existsSync(dir)) {
478
+ const dir = path4.dirname(REGISTRY_PATH);
479
+ if (!existsSync3(dir)) {
42
480
  mkdirSync(dir, { recursive: true });
43
481
  }
44
482
  const sessions = listSessions();
@@ -48,11 +486,11 @@ function registerSession(entry) {
48
486
  } else {
49
487
  sessions.push(entry);
50
488
  }
51
- writeFileSync(REGISTRY_PATH, JSON.stringify(sessions, null, 2));
489
+ writeFileSync2(REGISTRY_PATH, JSON.stringify(sessions, null, 2));
52
490
  }
53
491
  function listSessions() {
54
492
  try {
55
- const raw = readFileSync(REGISTRY_PATH, "utf8");
493
+ const raw = readFileSync3(REGISTRY_PATH, "utf8");
56
494
  return JSON.parse(raw);
57
495
  } catch {
58
496
  return [];
@@ -63,7 +501,7 @@ function pruneStaleSessions() {
63
501
  if (sessions.length === 0) return 0;
64
502
  let liveSessions = [];
65
503
  try {
66
- liveSessions = execSync("tmux list-sessions -F '#{session_name}' 2>/dev/null", {
504
+ liveSessions = execSync2("tmux list-sessions -F '#{session_name}' 2>/dev/null", {
67
505
  encoding: "utf8"
68
506
  }).trim().split("\n").filter(Boolean);
69
507
  } catch {
@@ -73,7 +511,7 @@ function pruneStaleSessions() {
73
511
  const alive = sessions.filter((s) => liveSet.has(s.windowName));
74
512
  const pruned = sessions.length - alive.length;
75
513
  if (pruned > 0) {
76
- writeFileSync(REGISTRY_PATH, JSON.stringify(alive, null, 2));
514
+ writeFileSync2(REGISTRY_PATH, JSON.stringify(alive, null, 2));
77
515
  }
78
516
  return pruned;
79
517
  }
@@ -81,18 +519,18 @@ var REGISTRY_PATH;
81
519
  var init_session_registry = __esm({
82
520
  "src/lib/session-registry.ts"() {
83
521
  "use strict";
84
- REGISTRY_PATH = path2.join(os2.homedir(), ".exe-os", "session-registry.json");
522
+ REGISTRY_PATH = path4.join(os4.homedir(), ".exe-os", "session-registry.json");
85
523
  }
86
524
  });
87
525
 
88
526
  // src/lib/session-key.ts
89
- import { execSync as execSync2 } from "child_process";
527
+ import { execSync as execSync3 } from "child_process";
90
528
  function getSessionKey() {
91
529
  if (_cached) return _cached;
92
530
  let pid = process.ppid;
93
531
  for (let i = 0; i < 10; i++) {
94
532
  try {
95
- const info = execSync2(`ps -p ${pid} -o ppid=,comm=`, {
533
+ const info = execSync3(`ps -p ${pid} -o ppid=,comm=`, {
96
534
  encoding: "utf8",
97
535
  timeout: 2e3
98
536
  }).trim();
@@ -228,14 +666,14 @@ var init_transport = __esm({
228
666
  });
229
667
 
230
668
  // src/lib/cc-agent-support.ts
231
- import { execSync as execSync3 } from "child_process";
669
+ import { execSync as execSync4 } from "child_process";
232
670
  function _resetCcAgentSupportCache() {
233
671
  _cachedSupport = null;
234
672
  }
235
673
  function claudeSupportsAgentFlag() {
236
674
  if (_cachedSupport !== null) return _cachedSupport;
237
675
  try {
238
- const helpOutput = execSync3("claude --help 2>&1", {
676
+ const helpOutput = execSync4("claude --help 2>&1", {
239
677
  encoding: "utf-8",
240
678
  timeout: 5e3
241
679
  });
@@ -311,17 +749,17 @@ var init_provider_table = __esm({
311
749
  });
312
750
 
313
751
  // src/lib/intercom-queue.ts
314
- import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, renameSync, existsSync as existsSync2, mkdirSync as mkdirSync2 } from "fs";
315
- import path3 from "path";
316
- import os3 from "os";
752
+ import { readFileSync as readFileSync4, writeFileSync as writeFileSync3, renameSync as renameSync3, existsSync as existsSync4, mkdirSync as mkdirSync2 } from "fs";
753
+ import path5 from "path";
754
+ import os5 from "os";
317
755
  function ensureDir() {
318
- const dir = path3.dirname(QUEUE_PATH);
319
- if (!existsSync2(dir)) mkdirSync2(dir, { recursive: true });
756
+ const dir = path5.dirname(QUEUE_PATH);
757
+ if (!existsSync4(dir)) mkdirSync2(dir, { recursive: true });
320
758
  }
321
759
  function readQueue() {
322
760
  try {
323
- if (!existsSync2(QUEUE_PATH)) return [];
324
- return JSON.parse(readFileSync2(QUEUE_PATH, "utf8"));
761
+ if (!existsSync4(QUEUE_PATH)) return [];
762
+ return JSON.parse(readFileSync4(QUEUE_PATH, "utf8"));
325
763
  } catch {
326
764
  return [];
327
765
  }
@@ -329,8 +767,8 @@ function readQueue() {
329
767
  function writeQueue(queue) {
330
768
  ensureDir();
331
769
  const tmp = `${QUEUE_PATH}.tmp`;
332
- writeFileSync2(tmp, JSON.stringify(queue, null, 2));
333
- renameSync(tmp, QUEUE_PATH);
770
+ writeFileSync3(tmp, JSON.stringify(queue, null, 2));
771
+ renameSync3(tmp, QUEUE_PATH);
334
772
  }
335
773
  function queueIntercom(targetSession, reason) {
336
774
  const queue = readQueue();
@@ -353,9 +791,9 @@ var QUEUE_PATH, TTL_MS, INTERCOM_LOG;
353
791
  var init_intercom_queue = __esm({
354
792
  "src/lib/intercom-queue.ts"() {
355
793
  "use strict";
356
- QUEUE_PATH = path3.join(os3.homedir(), ".exe-os", "intercom-queue.json");
794
+ QUEUE_PATH = path5.join(os5.homedir(), ".exe-os", "intercom-queue.json");
357
795
  TTL_MS = 60 * 60 * 1e3;
358
- INTERCOM_LOG = path3.join(os3.homedir(), ".exe-os", "intercom.log");
796
+ INTERCOM_LOG = path5.join(os5.homedir(), ".exe-os", "intercom.log");
359
797
  }
360
798
  });
361
799
 
@@ -398,7 +836,7 @@ function wrapWithRetry(client) {
398
836
  return (sql) => retryOnBusy(() => target.execute(sql), "execute");
399
837
  }
400
838
  if (prop === "batch") {
401
- return (stmts) => retryOnBusy(() => target.batch(stmts), "batch");
839
+ return (stmts, mode) => retryOnBusy(() => target.batch(stmts, mode), "batch");
402
840
  }
403
841
  return Reflect.get(target, prop, receiver);
404
842
  }
@@ -561,22 +999,24 @@ async function ensureSchema() {
561
999
  ON behaviors(agent_id, active);
562
1000
  `);
563
1001
  try {
1002
+ const coordinatorName = getCoordinatorName();
564
1003
  const existing = await client.execute({
565
- sql: "SELECT COUNT(*) as cnt FROM behaviors WHERE agent_id = 'exe'",
566
- args: []
1004
+ sql: "SELECT COUNT(*) as cnt FROM behaviors WHERE agent_id = ?",
1005
+ args: [coordinatorName]
567
1006
  });
568
1007
  if (Number(existing.rows[0]?.cnt) === 0) {
569
- await client.executeMultiple(`
570
- INSERT INTO behaviors (id, agent_id, project_name, domain, content, active, created_at, updated_at)
571
- VALUES
572
- (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');
573
- INSERT INTO behaviors (id, agent_id, project_name, domain, content, active, created_at, updated_at)
574
- VALUES
575
- (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');
576
- INSERT INTO behaviors (id, agent_id, project_name, domain, content, active, created_at, updated_at)
577
- VALUES
578
- (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');
579
- `);
1008
+ const seededAt = "2026-03-25T00:00:00Z";
1009
+ for (const [domain, content] of [
1010
+ ["workflow", `Don't ask "keep going?" \u2014 just keep executing phases/plans autonomously`],
1011
+ ["tool-use", "Always use create_task MCP tool, never write .md files directly for task creation"],
1012
+ ["workflow", "Auto-start reviewing when idle and reviews are pending \u2014 never ask founder for permission"]
1013
+ ]) {
1014
+ await client.execute({
1015
+ sql: `INSERT INTO behaviors (id, agent_id, project_name, domain, content, active, created_at, updated_at)
1016
+ VALUES (hex(randomblob(16)), ?, NULL, ?, ?, 1, ?, ?)`,
1017
+ args: [coordinatorName, domain, content, seededAt, seededAt]
1018
+ });
1019
+ }
580
1020
  }
581
1021
  } catch {
582
1022
  }
@@ -1268,304 +1708,57 @@ async function ensureSchema() {
1268
1708
  } catch {
1269
1709
  }
1270
1710
  }
1271
- }
1272
- async function disposeDatabase() {
1273
- if (_client) {
1274
- _client.close();
1275
- _client = null;
1276
- _resilientClient = null;
1277
- }
1278
- }
1279
- var _client, _resilientClient, initTurso, disposeTurso;
1280
- var init_database = __esm({
1281
- "src/lib/database.ts"() {
1282
- "use strict";
1283
- init_db_retry();
1284
- _client = null;
1285
- _resilientClient = null;
1286
- initTurso = initDatabase;
1287
- disposeTurso = disposeDatabase;
1288
- }
1289
- });
1290
-
1291
- // src/lib/config.ts
1292
- var config_exports = {};
1293
- __export(config_exports, {
1294
- CONFIG_MIGRATIONS: () => CONFIG_MIGRATIONS,
1295
- CONFIG_PATH: () => CONFIG_PATH,
1296
- COO_AGENT_NAME: () => COO_AGENT_NAME,
1297
- CURRENT_CONFIG_VERSION: () => CURRENT_CONFIG_VERSION,
1298
- DB_PATH: () => DB_PATH,
1299
- EXE_AI_DIR: () => EXE_AI_DIR,
1300
- LEGACY_LANCE_PATH: () => LEGACY_LANCE_PATH,
1301
- MODELS_DIR: () => MODELS_DIR,
1302
- loadConfig: () => loadConfig,
1303
- loadConfigFrom: () => loadConfigFrom,
1304
- loadConfigSync: () => loadConfigSync,
1305
- migrateConfig: () => migrateConfig,
1306
- saveConfig: () => saveConfig
1307
- });
1308
- import { readFile, writeFile, mkdir, chmod } from "fs/promises";
1309
- import { readFileSync as readFileSync3, existsSync as existsSync3, renameSync as renameSync2 } from "fs";
1310
- import path4 from "path";
1311
- import os4 from "os";
1312
- function resolveDataDir() {
1313
- if (process.env.EXE_OS_DIR) return process.env.EXE_OS_DIR;
1314
- if (process.env.EXE_MEM_DIR) return process.env.EXE_MEM_DIR;
1315
- const newDir = path4.join(os4.homedir(), ".exe-os");
1316
- const legacyDir = path4.join(os4.homedir(), ".exe-mem");
1317
- if (!existsSync3(newDir) && existsSync3(legacyDir)) {
1318
- try {
1319
- renameSync2(legacyDir, newDir);
1320
- process.stderr.write(`[exe-os] Migrated data directory: ~/.exe-mem \u2192 ~/.exe-os
1321
- `);
1322
- } catch {
1323
- return legacyDir;
1324
- }
1711
+ try {
1712
+ await client.execute({
1713
+ sql: `ALTER TABLE memories ADD COLUMN draft INTEGER DEFAULT 0`,
1714
+ args: []
1715
+ });
1716
+ } catch {
1325
1717
  }
1326
- return newDir;
1327
- }
1328
- function migrateLegacyConfig(raw) {
1329
- if ("r2" in raw) {
1330
- process.stderr.write(
1331
- "[exe-os] Warning: config.json contains deprecated 'r2' field from v1.0. R2 sync has been replaced in v1.1. The 'r2' field will be ignored.\n"
1718
+ try {
1719
+ await client.execute(
1720
+ `CREATE INDEX IF NOT EXISTS idx_memories_draft ON memories(draft) WHERE draft = 1`
1332
1721
  );
1333
- delete raw.r2;
1334
- }
1335
- if ("syncIntervalMs" in raw) {
1336
- delete raw.syncIntervalMs;
1337
- }
1338
- return raw;
1339
- }
1340
- function migrateConfig(raw) {
1341
- const fromVersion = typeof raw.config_version === "number" ? raw.config_version : 0;
1342
- let currentVersion = fromVersion;
1343
- let migrated = false;
1344
- if (currentVersion > CURRENT_CONFIG_VERSION) {
1345
- return { config: raw, migrated: false, fromVersion };
1346
- }
1347
- for (const migration of CONFIG_MIGRATIONS) {
1348
- if (currentVersion === migration.from && migration.to <= CURRENT_CONFIG_VERSION) {
1349
- raw = migration.migrate(raw);
1350
- currentVersion = migration.to;
1351
- migrated = true;
1352
- }
1353
- }
1354
- return { config: raw, migrated, fromVersion };
1355
- }
1356
- function normalizeScalingRoadmap(raw) {
1357
- const defaultAuto = DEFAULT_CONFIG.scalingRoadmap.rerankerAutoTrigger;
1358
- const userRoadmap = raw.scalingRoadmap ?? {};
1359
- const userAuto = userRoadmap.rerankerAutoTrigger ?? {};
1360
- if (userAuto.enabled === void 0 && raw.rerankerEnabled !== void 0) {
1361
- userAuto.enabled = raw.rerankerEnabled;
1362
- }
1363
- raw.scalingRoadmap = {
1364
- ...userRoadmap,
1365
- rerankerAutoTrigger: { ...defaultAuto, ...userAuto }
1366
- };
1367
- }
1368
- function normalizeSessionLifecycle(raw) {
1369
- const defaultSL = DEFAULT_CONFIG.sessionLifecycle;
1370
- const userSL = raw.sessionLifecycle ?? {};
1371
- raw.sessionLifecycle = { ...defaultSL, ...userSL };
1372
- }
1373
- function normalizeAutoUpdate(raw) {
1374
- const defaultAU = DEFAULT_CONFIG.autoUpdate;
1375
- const userAU = raw.autoUpdate ?? {};
1376
- raw.autoUpdate = { ...defaultAU, ...userAU };
1377
- }
1378
- async function loadConfig() {
1379
- const dir = process.env.EXE_OS_DIR ?? process.env.EXE_MEM_DIR ?? EXE_AI_DIR;
1380
- await mkdir(dir, { recursive: true });
1381
- const configPath = path4.join(dir, "config.json");
1382
- if (!existsSync3(configPath)) {
1383
- return { ...DEFAULT_CONFIG, dbPath: path4.join(dir, "memories.db") };
1722
+ } catch {
1384
1723
  }
1385
- const raw = await readFile(configPath, "utf-8");
1386
1724
  try {
1387
- let parsed = JSON.parse(raw);
1388
- parsed = migrateLegacyConfig(parsed);
1389
- const { config: migratedCfg, migrated, fromVersion } = migrateConfig(parsed);
1390
- if (migrated) {
1391
- process.stderr.write(`[exe-os] Config migrated from v${fromVersion} to v${migratedCfg.config_version}
1392
- `);
1393
- try {
1394
- await writeFile(configPath, JSON.stringify(migratedCfg, null, 2) + "\n");
1395
- } catch {
1396
- }
1397
- }
1398
- normalizeScalingRoadmap(migratedCfg);
1399
- normalizeSessionLifecycle(migratedCfg);
1400
- normalizeAutoUpdate(migratedCfg);
1401
- const config2 = { ...DEFAULT_CONFIG, dbPath: path4.join(dir, "memories.db"), ...migratedCfg };
1402
- if (config2.dbPath.startsWith("~")) {
1403
- config2.dbPath = config2.dbPath.replace(/^~/, os4.homedir());
1404
- }
1405
- return config2;
1725
+ await client.execute({
1726
+ sql: `ALTER TABLE memories ADD COLUMN memory_type TEXT DEFAULT 'raw'`,
1727
+ args: []
1728
+ });
1406
1729
  } catch {
1407
- return { ...DEFAULT_CONFIG, dbPath: path4.join(dir, "memories.db") };
1408
- }
1409
- }
1410
- function loadConfigSync() {
1411
- const dir = process.env.EXE_OS_DIR ?? process.env.EXE_MEM_DIR ?? EXE_AI_DIR;
1412
- const configPath = path4.join(dir, "config.json");
1413
- if (!existsSync3(configPath)) {
1414
- return { ...DEFAULT_CONFIG, dbPath: path4.join(dir, "memories.db") };
1415
1730
  }
1416
1731
  try {
1417
- const raw = readFileSync3(configPath, "utf-8");
1418
- let parsed = JSON.parse(raw);
1419
- parsed = migrateLegacyConfig(parsed);
1420
- const { config: migratedCfg } = migrateConfig(parsed);
1421
- normalizeScalingRoadmap(migratedCfg);
1422
- normalizeSessionLifecycle(migratedCfg);
1423
- normalizeAutoUpdate(migratedCfg);
1424
- return { ...DEFAULT_CONFIG, dbPath: path4.join(dir, "memories.db"), ...migratedCfg };
1732
+ await client.execute(
1733
+ `CREATE INDEX IF NOT EXISTS idx_memories_type ON memories(memory_type)`
1734
+ );
1425
1735
  } catch {
1426
- return { ...DEFAULT_CONFIG, dbPath: path4.join(dir, "memories.db") };
1427
- }
1428
- }
1429
- async function saveConfig(config2) {
1430
- const dir = process.env.EXE_OS_DIR ?? process.env.EXE_MEM_DIR ?? EXE_AI_DIR;
1431
- await mkdir(dir, { recursive: true });
1432
- const configPath = path4.join(dir, "config.json");
1433
- await writeFile(configPath, JSON.stringify(config2, null, 2) + "\n");
1434
- if (config2.cloud?.apiKey) {
1435
- await chmod(configPath, 384);
1436
1736
  }
1437
- }
1438
- async function loadConfigFrom(configPath) {
1439
- const raw = await readFile(configPath, "utf-8");
1440
1737
  try {
1441
- let parsed = JSON.parse(raw);
1442
- parsed = migrateLegacyConfig(parsed);
1443
- const { config: migratedCfg } = migrateConfig(parsed);
1444
- normalizeScalingRoadmap(migratedCfg);
1445
- normalizeSessionLifecycle(migratedCfg);
1446
- normalizeAutoUpdate(migratedCfg);
1447
- return { ...DEFAULT_CONFIG, ...migratedCfg };
1738
+ await client.execute({
1739
+ sql: `ALTER TABLE memories ADD COLUMN trajectory TEXT`,
1740
+ args: []
1741
+ });
1448
1742
  } catch {
1449
- return { ...DEFAULT_CONFIG };
1450
1743
  }
1451
1744
  }
1452
- var EXE_AI_DIR, DB_PATH, MODELS_DIR, CONFIG_PATH, COO_AGENT_NAME, LEGACY_LANCE_PATH, CURRENT_CONFIG_VERSION, DEFAULT_CONFIG, CONFIG_MIGRATIONS;
1453
- var init_config = __esm({
1454
- "src/lib/config.ts"() {
1455
- "use strict";
1456
- EXE_AI_DIR = resolveDataDir();
1457
- DB_PATH = path4.join(EXE_AI_DIR, "memories.db");
1458
- MODELS_DIR = path4.join(EXE_AI_DIR, "models");
1459
- CONFIG_PATH = path4.join(EXE_AI_DIR, "config.json");
1460
- COO_AGENT_NAME = "exe";
1461
- LEGACY_LANCE_PATH = path4.join(EXE_AI_DIR, "local.lance");
1462
- CURRENT_CONFIG_VERSION = 1;
1463
- DEFAULT_CONFIG = {
1464
- config_version: CURRENT_CONFIG_VERSION,
1465
- dbPath: DB_PATH,
1466
- modelFile: "jina-embeddings-v5-small-q4_k_m.gguf",
1467
- embeddingDim: 1024,
1468
- batchSize: 20,
1469
- flushIntervalMs: 1e4,
1470
- autoIngestion: true,
1471
- autoRetrieval: true,
1472
- searchMode: "hybrid",
1473
- hookSearchMode: "hybrid",
1474
- fileGrepEnabled: true,
1475
- splashEffect: true,
1476
- consolidationEnabled: true,
1477
- consolidationIntervalMs: 6 * 60 * 60 * 1e3,
1478
- consolidationModel: "claude-haiku-4-5-20251001",
1479
- consolidationMaxCallsPerRun: 20,
1480
- selfQueryRouter: true,
1481
- selfQueryModel: "claude-haiku-4-5-20251001",
1482
- rerankerEnabled: true,
1483
- scalingRoadmap: {
1484
- rerankerAutoTrigger: {
1485
- enabled: true,
1486
- broadQueryMinCardinality: 5e4,
1487
- fetchTopK: 150,
1488
- returnTopK: 5
1489
- }
1490
- },
1491
- graphRagEnabled: true,
1492
- wikiEnabled: false,
1493
- wikiUrl: "",
1494
- wikiApiKey: "",
1495
- wikiSyncIntervalMs: 30 * 60 * 1e3,
1496
- wikiWorkspaceMapping: {
1497
- exe: "Executive",
1498
- yoshi: "Engineering",
1499
- mari: "Marketing",
1500
- tom: "Engineering",
1501
- sasha: "Production"
1502
- },
1503
- wikiAutoUpdate: true,
1504
- wikiAutoUpdateThreshold: 0.5,
1505
- wikiAutoUpdateCreateNew: true,
1506
- skillLearning: true,
1507
- skillThreshold: 3,
1508
- skillModel: "claude-haiku-4-5-20251001",
1509
- exeHeartbeat: {
1510
- enabled: true,
1511
- intervalSeconds: 60,
1512
- staleInProgressThresholdHours: 2
1513
- },
1514
- sessionLifecycle: {
1515
- idleKillEnabled: true,
1516
- idleKillTicksRequired: 3,
1517
- idleKillIntercomAckWindowMs: 1e4,
1518
- maxAutoInstances: 10
1519
- },
1520
- autoUpdate: {
1521
- checkOnBoot: true,
1522
- autoInstall: false,
1523
- checkIntervalMs: 24 * 60 * 60 * 1e3
1524
- }
1525
- };
1526
- CONFIG_MIGRATIONS = [
1527
- {
1528
- from: 0,
1529
- to: 1,
1530
- migrate: (cfg) => {
1531
- cfg.config_version = 1;
1532
- return cfg;
1533
- }
1534
- }
1535
- ];
1536
- }
1537
- });
1538
-
1539
- // src/lib/employees.ts
1540
- import { readFile as readFile2, writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
1541
- import { existsSync as existsSync4, symlinkSync, readlinkSync, readFileSync as readFileSync4, renameSync as renameSync3, unlinkSync, writeFileSync as writeFileSync3 } from "fs";
1542
- import { execSync as execSync4 } from "child_process";
1543
- import path5 from "path";
1544
- import os5 from "os";
1545
- function loadEmployeesSync(employeesPath = EMPLOYEES_PATH) {
1546
- if (!existsSync4(employeesPath)) return [];
1547
- try {
1548
- return JSON.parse(readFileSync4(employeesPath, "utf-8"));
1549
- } catch {
1550
- return [];
1745
+ async function disposeDatabase() {
1746
+ if (_client) {
1747
+ _client.close();
1748
+ _client = null;
1749
+ _resilientClient = null;
1551
1750
  }
1552
1751
  }
1553
- function getEmployee(employees, name) {
1554
- return employees.find((e) => e.name.toLowerCase() === name.toLowerCase());
1555
- }
1556
- function isMultiInstance(agentName, employees) {
1557
- const roster = employees ?? loadEmployeesSync();
1558
- const emp = getEmployee(roster, agentName);
1559
- if (!emp) return false;
1560
- return MULTI_INSTANCE_ROLES.has(emp.role.toLowerCase());
1561
- }
1562
- var EMPLOYEES_PATH, MULTI_INSTANCE_ROLES;
1563
- var init_employees = __esm({
1564
- "src/lib/employees.ts"() {
1752
+ var _client, _resilientClient, initTurso, disposeTurso;
1753
+ var init_database = __esm({
1754
+ "src/lib/database.ts"() {
1565
1755
  "use strict";
1566
- init_config();
1567
- EMPLOYEES_PATH = path5.join(EXE_AI_DIR, "exe-employees.json");
1568
- MULTI_INSTANCE_ROLES = /* @__PURE__ */ new Set(["principal engineer", "content production specialist", "staff code reviewer"]);
1756
+ init_db_retry();
1757
+ init_employees();
1758
+ _client = null;
1759
+ _resilientClient = null;
1760
+ initTurso = initDatabase;
1761
+ disposeTurso = disposeDatabase;
1569
1762
  }
1570
1763
  });
1571
1764
 
@@ -2085,6 +2278,36 @@ async function listTasks(input) {
2085
2278
  tokensWarnedAt: r.tokens_warned_at !== null ? Number(r.tokens_warned_at) : null
2086
2279
  }));
2087
2280
  }
2281
+ function isTmuxSessionAlive(identifier) {
2282
+ if (!identifier || identifier === "unknown") return true;
2283
+ try {
2284
+ if (identifier.startsWith("%")) {
2285
+ const output = execSync5("tmux list-panes -a -F '#{pane_id}'", {
2286
+ timeout: 2e3,
2287
+ encoding: "utf8",
2288
+ stdio: ["pipe", "pipe", "pipe"]
2289
+ });
2290
+ return output.split("\n").some((l) => l.trim() === identifier);
2291
+ } else {
2292
+ execSync5(`tmux has-session -t ${JSON.stringify(identifier)}`, {
2293
+ timeout: 2e3,
2294
+ stdio: ["pipe", "pipe", "pipe"]
2295
+ });
2296
+ return true;
2297
+ }
2298
+ } catch {
2299
+ if (identifier.startsWith("%")) return true;
2300
+ try {
2301
+ execSync5("tmux list-sessions", {
2302
+ timeout: 2e3,
2303
+ stdio: ["pipe", "pipe", "pipe"]
2304
+ });
2305
+ return false;
2306
+ } catch {
2307
+ return true;
2308
+ }
2309
+ }
2310
+ }
2088
2311
  function checkStaleCompletion(taskContext, taskCreatedAt) {
2089
2312
  if (!taskContext) return null;
2090
2313
  if (!DELEGATION_KEYWORDS.test(taskContext)) return null;
@@ -2147,13 +2370,59 @@ ${input.result}` : `\u26A0\uFE0F ${warning}`;
2147
2370
  });
2148
2371
  if (claim.rowsAffected === 0) {
2149
2372
  const current = await client.execute({
2150
- sql: "SELECT status, assigned_tmux FROM tasks WHERE id = ?",
2373
+ sql: "SELECT status, assigned_tmux, assigned_by FROM tasks WHERE id = ?",
2151
2374
  args: [taskId]
2152
2375
  });
2153
2376
  const cur = current.rows[0];
2154
- const status = cur?.status ?? "unknown";
2155
- const claimedBy = cur?.assigned_tmux ? ` (claimed by ${cur.assigned_tmux})` : "";
2156
- throw new Error(`${TASK_ALREADY_CLAIMED_PREFIX}: task ${taskId} is ${status}${claimedBy}`);
2377
+ const curStatus = cur?.status ?? "unknown";
2378
+ const claimedBySession = cur?.assigned_tmux ?? "";
2379
+ const assignedBy = cur?.assigned_by ?? "";
2380
+ if (curStatus === "in_progress" && claimedBySession && !isTmuxSessionAlive(claimedBySession)) {
2381
+ process.stderr.write(
2382
+ `[tasks] Auto-releasing dead claim on ${taskId} (was ${claimedBySession})
2383
+ `
2384
+ );
2385
+ await client.execute({
2386
+ sql: "UPDATE tasks SET status = 'open', assigned_tmux = NULL, updated_at = ? WHERE id = ?",
2387
+ args: [now, taskId]
2388
+ });
2389
+ const retried = await client.execute({
2390
+ sql: `UPDATE tasks SET status = 'in_progress', assigned_tmux = ?, updated_at = ? WHERE id = ? AND status = 'open'`,
2391
+ args: [tmuxSession, now, taskId]
2392
+ });
2393
+ if (retried.rowsAffected > 0) {
2394
+ try {
2395
+ await writeCheckpoint({
2396
+ taskId,
2397
+ step: "reclaimed_dead_session",
2398
+ contextSummary: `Task reclaimed after dead session ${claimedBySession} released.`
2399
+ });
2400
+ } catch {
2401
+ }
2402
+ return { row, taskFile, now, taskId };
2403
+ }
2404
+ }
2405
+ if (curStatus === "in_progress" && input.callerAgentId && (input.callerAgentId === assignedBy || input.callerAgentId === "exe")) {
2406
+ process.stderr.write(
2407
+ `[tasks] Assigner override: ${input.callerAgentId} reclaiming ${taskId}
2408
+ `
2409
+ );
2410
+ await client.execute({
2411
+ sql: `UPDATE tasks SET status = 'in_progress', assigned_tmux = ?, updated_at = ? WHERE id = ?`,
2412
+ args: [tmuxSession, now, taskId]
2413
+ });
2414
+ try {
2415
+ await writeCheckpoint({
2416
+ taskId,
2417
+ step: "assigner_override",
2418
+ contextSummary: `Task force-reclaimed by assigner ${input.callerAgentId}.`
2419
+ });
2420
+ } catch {
2421
+ }
2422
+ return { row, taskFile, now, taskId };
2423
+ }
2424
+ const claimedBy = claimedBySession ? ` (claimed by ${claimedBySession})` : "";
2425
+ throw new Error(`${TASK_ALREADY_CLAIMED_PREFIX}: task ${taskId} is ${curStatus}${claimedBy}`);
2157
2426
  }
2158
2427
  try {
2159
2428
  await writeCheckpoint({
@@ -2251,7 +2520,7 @@ var init_tasks_crud = __esm({
2251
2520
  "use strict";
2252
2521
  init_database();
2253
2522
  init_task_scope();
2254
- DELEGATION_KEYWORDS = /parallel|delegate|wave|tom\d*-exe/i;
2523
+ DELEGATION_KEYWORDS = /parallel|delegate|wave|worktree|multi-instance/i;
2255
2524
  TASK_ALREADY_CLAIMED_PREFIX = "TASK_ALREADY_CLAIMED";
2256
2525
  }
2257
2526
  });
@@ -2608,7 +2877,7 @@ function findSessionForProject(projectName) {
2608
2877
  const sessions = listSessions();
2609
2878
  for (const s of sessions) {
2610
2879
  const proj = s.projectDir.split("/").filter(Boolean).pop();
2611
- if (proj === projectName && s.agentId === "exe") return s;
2880
+ if (proj === projectName && (s.agentId === "exe" || isCoordinatorName(s.agentId))) return s;
2612
2881
  }
2613
2882
  return null;
2614
2883
  }
@@ -2648,12 +2917,13 @@ var init_session_scope = __esm({
2648
2917
  init_session_registry();
2649
2918
  init_project_name();
2650
2919
  init_tmux_routing();
2920
+ init_employees();
2651
2921
  }
2652
2922
  });
2653
2923
 
2654
2924
  // src/lib/tasks-notify.ts
2655
2925
  async function dispatchTaskToEmployee(input) {
2656
- if (input.assignedTo === "exe") return { dispatched: "skipped" };
2926
+ if (input.assignedTo === "exe" || isCoordinatorName(input.assignedTo)) return { dispatched: "skipped" };
2657
2927
  let crossProject = false;
2658
2928
  if (input.projectName) {
2659
2929
  try {
@@ -3158,6 +3428,24 @@ async function updateTask(input) {
3158
3428
  });
3159
3429
  } catch {
3160
3430
  }
3431
+ const assignedAgent = String(row.assigned_to);
3432
+ if (!isCoordinatorName(assignedAgent)) {
3433
+ try {
3434
+ const draftClient = getClient();
3435
+ if (input.status === "done") {
3436
+ await draftClient.execute({
3437
+ sql: `UPDATE memories SET draft = 0 WHERE agent_id = ? AND draft = 1`,
3438
+ args: [assignedAgent]
3439
+ });
3440
+ } else if (input.status === "cancelled") {
3441
+ await draftClient.execute({
3442
+ sql: `DELETE FROM memories WHERE agent_id = ? AND draft = 1`,
3443
+ args: [assignedAgent]
3444
+ });
3445
+ }
3446
+ } catch {
3447
+ }
3448
+ }
3161
3449
  try {
3162
3450
  const client = getClient();
3163
3451
  const cascaded = await client.execute({
@@ -3176,8 +3464,8 @@ async function updateTask(input) {
3176
3464
  }
3177
3465
  const isTerminal = input.status === "done" || input.status === "needs_review";
3178
3466
  if (isTerminal) {
3179
- const isExe = String(row.assigned_to) === "exe";
3180
- if (!isExe) {
3467
+ const isCoordinator = String(row.assigned_to) === "exe" || isCoordinatorName(String(row.assigned_to));
3468
+ if (!isCoordinator) {
3181
3469
  notifyTaskDone();
3182
3470
  }
3183
3471
  await markTaskNotificationsRead(taskFile);
@@ -3201,7 +3489,7 @@ async function updateTask(input) {
3201
3489
  }
3202
3490
  }
3203
3491
  }
3204
- if (input.status === "done" && String(row.assigned_to) !== "exe" && !process.env.VITEST) {
3492
+ if (input.status === "done" && String(row.assigned_to) !== "exe" && !isCoordinatorName(String(row.assigned_to)) && !process.env.VITEST) {
3205
3493
  Promise.resolve().then(() => (init_skill_learning(), skill_learning_exports)).then(
3206
3494
  ({ captureAndLearn: captureAndLearn2 }) => captureAndLearn2({
3207
3495
  taskId,
@@ -3217,7 +3505,7 @@ async function updateTask(input) {
3217
3505
  });
3218
3506
  }
3219
3507
  let nextTask;
3220
- if (isTerminal && String(row.assigned_to) !== "exe") {
3508
+ if (isTerminal && String(row.assigned_to) !== "exe" && !isCoordinatorName(String(row.assigned_to))) {
3221
3509
  try {
3222
3510
  nextTask = await findNextTask(String(row.assigned_to));
3223
3511
  } catch {
@@ -3244,12 +3532,14 @@ async function updateTask(input) {
3244
3532
  async function deleteTask(taskId, baseDir) {
3245
3533
  const client = getClient();
3246
3534
  const { taskFile, assignedTo, assignedBy, taskSlug } = await deleteTaskCore(taskId, baseDir);
3247
- const reviewer = assignedBy || "exe";
3535
+ const coordinatorName = getCoordinatorName();
3536
+ const reviewer = assignedBy || coordinatorName;
3248
3537
  const reviewSlug = `review-${assignedTo}-${taskSlug}`;
3249
3538
  const reviewFile = `exe/${reviewer}/${reviewSlug}.md`;
3539
+ const legacyReviewFile = `exe/${coordinatorName}/${reviewSlug}.md`;
3250
3540
  await client.execute({
3251
- sql: "DELETE FROM tasks WHERE task_file = ? OR task_file = ?",
3252
- args: [reviewFile, `exe/exe/${reviewSlug}.md`]
3541
+ sql: "DELETE FROM tasks WHERE task_file = ? OR task_file = ? OR task_file = ?",
3542
+ args: [reviewFile, legacyReviewFile, `exe/exe/${reviewSlug}.md`]
3253
3543
  });
3254
3544
  await markAsReadByTaskFile(taskFile);
3255
3545
  await markAsReadByTaskFile(reviewFile);
@@ -3261,6 +3551,7 @@ var init_tasks = __esm({
3261
3551
  init_config();
3262
3552
  init_notifications();
3263
3553
  init_state_bus();
3554
+ init_employees();
3264
3555
  init_tasks_crud();
3265
3556
  init_tasks_review();
3266
3557
  init_tasks_crud();
@@ -3346,7 +3637,7 @@ function _resetLastRelaunchCache() {
3346
3637
  }
3347
3638
  async function lastResumeCreatedAtMs(agentId) {
3348
3639
  const client = getClient();
3349
- const cmScope = sessionScopeFilter();
3640
+ const cmScope = sessionScopeFilter(null);
3350
3641
  const result = await client.execute({
3351
3642
  sql: `SELECT MAX(created_at) AS last_created_at
3352
3643
  FROM tasks
@@ -3371,7 +3662,7 @@ async function createOrRefreshResumeTask(agentId, projectDir, openTasks) {
3371
3662
  const client = getClient();
3372
3663
  const now = (/* @__PURE__ */ new Date()).toISOString();
3373
3664
  const context = buildResumeContext(agentId, openTasks);
3374
- const rdScope = sessionScopeFilter();
3665
+ const rdScope = sessionScopeFilter(null);
3375
3666
  const existing = await client.execute({
3376
3667
  sql: `SELECT id FROM tasks
3377
3668
  WHERE assigned_to = ?
@@ -3405,7 +3696,7 @@ async function pollCapacityDead() {
3405
3696
  const transport = getTransport();
3406
3697
  const relaunched = [];
3407
3698
  const registered = listSessions().filter(
3408
- (s) => s.agentId !== "exe"
3699
+ (s) => s.agentId !== "exe" && !isCoordinatorName(s.agentId)
3409
3700
  );
3410
3701
  if (registered.length === 0) return [];
3411
3702
  let liveSessions;
@@ -3465,7 +3756,7 @@ async function pollCapacityDead() {
3465
3756
  reason: "capacity"
3466
3757
  });
3467
3758
  const client = getClient();
3468
- const rlScope = sessionScopeFilter();
3759
+ const rlScope = sessionScopeFilter(null);
3469
3760
  const openTasks = await client.execute({
3470
3761
  sql: `SELECT id, title, priority, task_file, status
3471
3762
  FROM tasks
@@ -3519,6 +3810,7 @@ var init_capacity_monitor = __esm({
3519
3810
  init_session_kill_telemetry();
3520
3811
  init_tmux_routing();
3521
3812
  init_task_scope();
3813
+ init_employees();
3522
3814
  CAPACITY_PATTERNS = [
3523
3815
  /conversation is too long/i,
3524
3816
  /maximum context length/i,
@@ -3668,7 +3960,7 @@ function employeeSessionName(employee, exeSession, instance) {
3668
3960
  exeSession = root;
3669
3961
  } else {
3670
3962
  throw new Error(
3671
- `Invalid exeSession "${exeSession}" \u2014 contains a dash but no recognizable root session. Pass a root session name (e.g., "exe1", "work", "yoda1")`
3963
+ `Invalid coordinator session "${exeSession}" \u2014 contains a dash but no recognizable root session. Pass a root session name.`
3672
3964
  );
3673
3965
  }
3674
3966
  }
@@ -3688,8 +3980,10 @@ function parseParentExe(sessionName, agentId) {
3688
3980
  return match?.[1] ?? null;
3689
3981
  }
3690
3982
  function extractRootExe(name) {
3691
- const match = name.match(/(exe\d+)$/);
3692
- return match?.[1] ?? null;
3983
+ if (!name) return null;
3984
+ if (!name.includes("-")) return name;
3985
+ const parts = name.split("-").filter(Boolean);
3986
+ return parts.length > 0 ? parts[parts.length - 1] : null;
3693
3987
  }
3694
3988
  function registerParentExe(sessionKey, parentExe, dispatchedBy) {
3695
3989
  if (!existsSync10(SESSION_CACHE)) {
@@ -3834,12 +4128,14 @@ function isSessionBusy(sessionName) {
3834
4128
  return state === "thinking" || state === "tool";
3835
4129
  }
3836
4130
  function isExeSession(sessionName) {
3837
- return /^exe\d*$/.test(sessionName);
4131
+ const matchesBaseWithInstance = (baseName) => sessionName === baseName || sessionName.startsWith(baseName) && /^\d+$/.test(sessionName.slice(baseName.length));
4132
+ const coordinatorName = getCoordinatorName();
4133
+ return matchesBaseWithInstance(coordinatorName) || matchesBaseWithInstance("exe");
3838
4134
  }
3839
4135
  function sendIntercom(targetSession) {
3840
4136
  const transport = getTransport();
3841
4137
  if (isExeSession(targetSession)) {
3842
- logIntercom(`SKIP_EXE \u2192 ${targetSession} (exe sessions use prompt-submit hook)`);
4138
+ logIntercom(`SKIP_COORDINATOR \u2192 ${targetSession} (coordinator sessions use prompt-submit hook)`);
3843
4139
  return "skipped_exe";
3844
4140
  }
3845
4141
  if (isDebounced(targetSession)) {
@@ -3891,7 +4187,7 @@ function notifyParentExe(sessionKey) {
3891
4187
  if (result === "failed") {
3892
4188
  const rootExe = resolveExeSession();
3893
4189
  if (rootExe && rootExe !== target) {
3894
- process.stderr.write(`[intercom] notifyParentExe: dispatcher ${target} dead, falling back to root exe ${rootExe}
4190
+ process.stderr.write(`[intercom] notifyParentExe: dispatcher ${target} dead, falling back to root coordinator session ${rootExe}
3895
4191
  `);
3896
4192
  const fallback = sendIntercom(rootExe);
3897
4193
  return fallback !== "failed";
@@ -3901,8 +4197,8 @@ function notifyParentExe(sessionKey) {
3901
4197
  return true;
3902
4198
  }
3903
4199
  function ensureEmployee(employeeName, exeSession, projectDir, opts) {
3904
- if (employeeName === "exe") {
3905
- return { status: "failed", sessionName: "", error: "exe is the COO, not a dispatchable employee" };
4200
+ if (employeeName === "exe" || isCoordinatorName(employeeName)) {
4201
+ return { status: "failed", sessionName: "", error: "The COO is not a dispatchable employee" };
3906
4202
  }
3907
4203
  try {
3908
4204
  assertEmployeeLimitSync();
@@ -3911,8 +4207,8 @@ function ensureEmployee(employeeName, exeSession, projectDir, opts) {
3911
4207
  return { status: "failed", sessionName: "", error: err.message };
3912
4208
  }
3913
4209
  }
3914
- if (/-exe\d*$/.test(employeeName)) {
3915
- const bare = employeeName.replace(/-exe\d*$/, "").replace(/\d+$/, "");
4210
+ if (employeeName.includes("-")) {
4211
+ const bare = employeeName.split("-")[0].replace(/\d+$/, "");
3916
4212
  return {
3917
4213
  status: "failed",
3918
4214
  sessionName: "",
@@ -3931,7 +4227,7 @@ function ensureEmployee(employeeName, exeSession, projectDir, opts) {
3931
4227
  return {
3932
4228
  status: "failed",
3933
4229
  sessionName: "",
3934
- error: `Invalid exeSession "${exeSession}" \u2014 contains a dash but no recognizable root session. Pass a root session name (e.g., "exe1", "work", "yoda1")`
4230
+ error: `Invalid coordinator session "${exeSession}" \u2014 contains a dash but no recognizable root session. Pass a root session name.`
3935
4231
  };
3936
4232
  }
3937
4233
  }
@@ -4088,8 +4384,8 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
4088
4384
  const ctxContent = [
4089
4385
  `## Session Context`,
4090
4386
  `You are running in tmux session: ${sessionName}.`,
4091
- `Your parent exe session is ${exeSession}.`,
4092
- `Your employees (if any) use the -${exeSession} suffix (e.g., tom-${exeSession}).`
4387
+ `Your parent coordinator session is ${exeSession}.`,
4388
+ `Your employees (if any) use the -${exeSession} suffix.`
4093
4389
  ].join("\n");
4094
4390
  writeFileSync6(ctxFile, ctxContent);
4095
4391
  sessionContextFlag = ` --append-system-prompt-file ${ctxFile}`;
@@ -4193,6 +4489,7 @@ var init_tmux_routing = __esm({
4193
4489
  init_provider_table();
4194
4490
  init_intercom_queue();
4195
4491
  init_plan_limits();
4492
+ init_employees();
4196
4493
  SPAWN_LOCK_DIR = path14.join(os7.homedir(), ".exe-os", "spawn-locks");
4197
4494
  SESSION_CACHE = path14.join(os7.homedir(), ".exe-os", "session-cache");
4198
4495
  BEHAVIORS_EXPORT_TIMEOUT_MS = 1e4;
@@ -4386,7 +4683,11 @@ async function ensureShardSchema(client) {
4386
4683
  "ALTER TABLE memories ADD COLUMN source_path TEXT",
4387
4684
  "ALTER TABLE memories ADD COLUMN source_type TEXT DEFAULT 'text'",
4388
4685
  "ALTER TABLE memories ADD COLUMN tier INTEGER DEFAULT 3",
4389
- "ALTER TABLE memories ADD COLUMN supersedes_id TEXT"
4686
+ "ALTER TABLE memories ADD COLUMN supersedes_id TEXT",
4687
+ // MS-11: draft staging, MS-6a: memory_type, MS-7: trajectory
4688
+ "ALTER TABLE memories ADD COLUMN draft INTEGER DEFAULT 0",
4689
+ "ALTER TABLE memories ADD COLUMN memory_type TEXT DEFAULT 'raw'",
4690
+ "ALTER TABLE memories ADD COLUMN trajectory TEXT"
4390
4691
  ]) {
4391
4692
  try {
4392
4693
  await client.execute(col);
@@ -4516,26 +4817,26 @@ var init_platform_procedures = __esm({
4516
4817
  title: "What is exe-os \u2014 the operating model every agent must understand",
4517
4818
  domain: "architecture",
4518
4819
  priority: "p0",
4519
- 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."
4820
+ 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."
4520
4821
  },
4521
4822
  {
4522
4823
  title: "Mode 1 \u2014 how exe-os runs inside Claude Code",
4523
4824
  domain: "architecture",
4524
4825
  priority: "p0",
4525
- 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."
4826
+ 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."
4526
4827
  },
4527
4828
  {
4528
- title: "Sessions explained \u2014 what exeN means and how projects work",
4829
+ title: "Sessions explained \u2014 coordinator session names and projects",
4529
4830
  domain: "architecture",
4530
4831
  priority: "p0",
4531
- 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."
4832
+ 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."
4532
4833
  },
4533
4834
  // --- Hierarchy and dispatch ---
4534
4835
  {
4535
4836
  title: "Chain of command \u2014 who talks to whom",
4536
4837
  domain: "workflow",
4537
4838
  priority: "p0",
4538
- 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."
4839
+ 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."
4539
4840
  },
4540
4841
  {
4541
4842
  title: "Single dispatch path \u2014 create_task only",
@@ -4545,30 +4846,30 @@ var init_platform_procedures = __esm({
4545
4846
  },
4546
4847
  // --- Session isolation ---
4547
4848
  {
4548
- title: "Session scoping \u2014 stay in your exe boundary",
4849
+ title: "Session scoping \u2014 stay in your coordinator boundary",
4549
4850
  domain: "security",
4550
4851
  priority: "p0",
4551
- 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."
4852
+ 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."
4552
4853
  },
4553
4854
  {
4554
4855
  title: "Session isolation \u2014 never touch another session's work",
4555
4856
  domain: "workflow",
4556
4857
  priority: "p0",
4557
- 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.`
4858
+ 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."
4558
4859
  },
4559
4860
  // --- Engineering: session scoping in code ---
4560
4861
  {
4561
4862
  title: "Three-dimensional scoping \u2014 session, project, role \u2014 enforced in every query",
4562
4863
  domain: "architecture",
4563
4864
  priority: "p0",
4564
- 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."
4865
+ 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."
4565
4866
  },
4566
4867
  // --- Hard constraints ---
4567
4868
  {
4568
4869
  title: "What you CANNOT do in exe-os \u2014 hard constraints",
4569
4870
  domain: "security",
4570
4871
  priority: "p0",
4571
- 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."
4872
+ 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."
4572
4873
  },
4573
4874
  // --- Operations ---
4574
4875
  {
@@ -4808,7 +5109,10 @@ async function writeMemory(record) {
4808
5109
  source_path: record.source_path ?? null,
4809
5110
  source_type: record.source_type ?? null,
4810
5111
  tier: record.tier ?? classifyTier(record),
4811
- supersedes_id: record.supersedes_id ?? null
5112
+ supersedes_id: record.supersedes_id ?? null,
5113
+ draft: record.draft ? 1 : 0,
5114
+ memory_type: record.memory_type ?? "raw",
5115
+ trajectory: record.trajectory ? JSON.stringify(record.trajectory) : null
4812
5116
  };
4813
5117
  _pendingRecords.push(dbRow);
4814
5118
  orgBus.emit({
@@ -4863,6 +5167,9 @@ async function flushBatch() {
4863
5167
  const sourceType = row.source_type ?? null;
4864
5168
  const tier = row.tier ?? 3;
4865
5169
  const supersedesId = row.supersedes_id ?? null;
5170
+ const draft = row.draft ? 1 : 0;
5171
+ const memoryType = row.memory_type ?? "raw";
5172
+ const trajectory = row.trajectory ?? null;
4866
5173
  return {
4867
5174
  sql: hasVector ? `INSERT OR IGNORE INTO memories
4868
5175
  (id, agent_id, agent_role, session_id, timestamp,
@@ -4870,15 +5177,15 @@ async function flushBatch() {
4870
5177
  has_error, raw_text, vector, version, task_id, importance, status,
4871
5178
  confidence, last_accessed,
4872
5179
  workspace_id, document_id, user_id, char_offset, page_number,
4873
- source_path, source_type, tier, supersedes_id)
4874
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, vector32(?), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` : `INSERT OR IGNORE INTO memories
5180
+ source_path, source_type, tier, supersedes_id, draft, memory_type, trajectory)
5181
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, vector32(?), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` : `INSERT OR IGNORE INTO memories
4875
5182
  (id, agent_id, agent_role, session_id, timestamp,
4876
5183
  tool_name, project_name,
4877
5184
  has_error, raw_text, vector, version, task_id, importance, status,
4878
5185
  confidence, last_accessed,
4879
5186
  workspace_id, document_id, user_id, char_offset, page_number,
4880
- source_path, source_type, tier, supersedes_id)
4881
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
5187
+ source_path, source_type, tier, supersedes_id, draft, memory_type, trajectory)
5188
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
4882
5189
  args: hasVector ? [
4883
5190
  row.id,
4884
5191
  row.agent_id,
@@ -4904,7 +5211,10 @@ async function flushBatch() {
4904
5211
  sourcePath,
4905
5212
  sourceType,
4906
5213
  tier,
4907
- supersedesId
5214
+ supersedesId,
5215
+ draft,
5216
+ memoryType,
5217
+ trajectory
4908
5218
  ] : [
4909
5219
  row.id,
4910
5220
  row.agent_id,
@@ -4929,7 +5239,10 @@ async function flushBatch() {
4929
5239
  sourcePath,
4930
5240
  sourceType,
4931
5241
  tier,
4932
- supersedesId
5242
+ supersedesId,
5243
+ draft,
5244
+ memoryType,
5245
+ trajectory
4933
5246
  ]
4934
5247
  };
4935
5248
  };
@@ -4998,6 +5311,8 @@ async function searchMemories(queryVector, agentId, options) {
4998
5311
  const limit = options?.limit ?? 10;
4999
5312
  const statusFilter = options?.includeArchived ? "" : `
5000
5313
  AND COALESCE(status, 'active') = 'active'`;
5314
+ const draftFilter = options?.includeDrafts ? "" : `
5315
+ AND (draft = 0 OR draft IS NULL)`;
5001
5316
  let sql = `SELECT id, agent_id, agent_role, session_id, timestamp,
5002
5317
  tool_name, project_name,
5003
5318
  has_error, raw_text, vector, importance, status,
@@ -5007,7 +5322,7 @@ async function searchMemories(queryVector, agentId, options) {
5007
5322
  source_path, source_type
5008
5323
  FROM memories
5009
5324
  WHERE agent_id = ?
5010
- AND vector IS NOT NULL${statusFilter}
5325
+ AND vector IS NOT NULL${statusFilter}${draftFilter}
5011
5326
  AND COALESCE(confidence, 0.7) >= 0.3`;
5012
5327
  const args = [agentId];
5013
5328
  const scope = buildWikiScopeFilter(options, "");
@@ -5029,6 +5344,10 @@ async function searchMemories(queryVector, agentId, options) {
5029
5344
  sql += ` AND timestamp >= ?`;
5030
5345
  args.push(options.since);
5031
5346
  }
5347
+ if (options?.memoryType) {
5348
+ sql += ` AND memory_type = ?`;
5349
+ args.push(options.memoryType);
5350
+ }
5032
5351
  sql += ` ORDER BY vector_distance_cos(vector, vector32(?))`;
5033
5352
  args.push(vectorToBlob(queryVector));
5034
5353
  sql += ` LIMIT ?`;
@@ -5768,563 +6087,1480 @@ async function pushGatewayEventToCRM(params) {
5768
6087
  console.error("[crm-bridge] Event push failed:", err instanceof Error ? err.message : err);
5769
6088
  }
5770
6089
  }
5771
- var config, disabledLogged;
5772
- var init_crm_bridge = __esm({
5773
- "src/gateway/crm-bridge.ts"() {
5774
- "use strict";
5775
- config = null;
5776
- disabledLogged = false;
6090
+ var config, disabledLogged;
6091
+ var init_crm_bridge = __esm({
6092
+ "src/gateway/crm-bridge.ts"() {
6093
+ "use strict";
6094
+ config = null;
6095
+ disabledLogged = false;
6096
+ }
6097
+ });
6098
+
6099
+ // src/lib/exe-daemon-client.ts
6100
+ import net from "net";
6101
+ import { spawn } from "child_process";
6102
+ import { randomUUID as randomUUID6 } from "crypto";
6103
+ import { existsSync as existsSync13, unlinkSync as unlinkSync6, readFileSync as readFileSync10, openSync, closeSync, statSync } from "fs";
6104
+ import path17 from "path";
6105
+ import { fileURLToPath as fileURLToPath2 } from "url";
6106
+ function handleData(chunk) {
6107
+ _buffer += chunk.toString();
6108
+ if (_buffer.length > MAX_BUFFER) {
6109
+ _buffer = "";
6110
+ return;
6111
+ }
6112
+ let newlineIdx;
6113
+ while ((newlineIdx = _buffer.indexOf("\n")) !== -1) {
6114
+ const line = _buffer.slice(0, newlineIdx).trim();
6115
+ _buffer = _buffer.slice(newlineIdx + 1);
6116
+ if (!line) continue;
6117
+ try {
6118
+ const response = JSON.parse(line);
6119
+ const entry = _pending.get(response.id);
6120
+ if (entry) {
6121
+ clearTimeout(entry.timer);
6122
+ _pending.delete(response.id);
6123
+ entry.resolve(response);
6124
+ }
6125
+ } catch {
6126
+ }
6127
+ }
6128
+ }
6129
+ function cleanupStaleFiles() {
6130
+ if (existsSync13(PID_PATH)) {
6131
+ try {
6132
+ const pid = parseInt(readFileSync10(PID_PATH, "utf8").trim(), 10);
6133
+ if (pid > 0) {
6134
+ try {
6135
+ process.kill(pid, 0);
6136
+ return;
6137
+ } catch {
6138
+ }
6139
+ }
6140
+ } catch {
6141
+ }
6142
+ try {
6143
+ unlinkSync6(PID_PATH);
6144
+ } catch {
6145
+ }
6146
+ try {
6147
+ unlinkSync6(SOCKET_PATH);
6148
+ } catch {
6149
+ }
6150
+ }
6151
+ }
6152
+ function findPackageRoot() {
6153
+ let dir = path17.dirname(fileURLToPath2(import.meta.url));
6154
+ const { root } = path17.parse(dir);
6155
+ while (dir !== root) {
6156
+ if (existsSync13(path17.join(dir, "package.json"))) return dir;
6157
+ dir = path17.dirname(dir);
6158
+ }
6159
+ return null;
6160
+ }
6161
+ function spawnDaemon() {
6162
+ const pkgRoot = findPackageRoot();
6163
+ if (!pkgRoot) {
6164
+ process.stderr.write("[exed-client] WARN: cannot find package root\n");
6165
+ return;
6166
+ }
6167
+ const daemonPath = path17.join(pkgRoot, "dist", "lib", "exe-daemon.js");
6168
+ if (!existsSync13(daemonPath)) {
6169
+ process.stderr.write(`[exed-client] WARN: daemon script not found at ${daemonPath}
6170
+ `);
6171
+ return;
6172
+ }
6173
+ const resolvedPath = daemonPath;
6174
+ process.stderr.write(`[exed-client] Spawning daemon: ${resolvedPath}
6175
+ `);
6176
+ const logPath = path17.join(path17.dirname(SOCKET_PATH), "exed.log");
6177
+ let stderrFd = "ignore";
6178
+ try {
6179
+ stderrFd = openSync(logPath, "a");
6180
+ } catch {
6181
+ }
6182
+ const child = spawn(process.execPath, [resolvedPath], {
6183
+ detached: true,
6184
+ stdio: ["ignore", "ignore", stderrFd],
6185
+ env: {
6186
+ ...process.env,
6187
+ TMUX: void 0,
6188
+ // Daemon is global — must not inherit session scope
6189
+ TMUX_PANE: void 0,
6190
+ // Prevents resolveExeSession() from scoping to one session
6191
+ EXE_DAEMON_SOCK: SOCKET_PATH,
6192
+ EXE_DAEMON_PID: PID_PATH
6193
+ }
6194
+ });
6195
+ child.unref();
6196
+ if (typeof stderrFd === "number") {
6197
+ try {
6198
+ closeSync(stderrFd);
6199
+ } catch {
6200
+ }
6201
+ }
6202
+ }
6203
+ function acquireSpawnLock2() {
6204
+ try {
6205
+ const fd = openSync(SPAWN_LOCK_PATH, "wx");
6206
+ closeSync(fd);
6207
+ return true;
6208
+ } catch {
6209
+ try {
6210
+ const stat = statSync(SPAWN_LOCK_PATH);
6211
+ if (Date.now() - stat.mtimeMs > SPAWN_LOCK_STALE_MS) {
6212
+ try {
6213
+ unlinkSync6(SPAWN_LOCK_PATH);
6214
+ } catch {
6215
+ }
6216
+ try {
6217
+ const fd = openSync(SPAWN_LOCK_PATH, "wx");
6218
+ closeSync(fd);
6219
+ return true;
6220
+ } catch {
6221
+ }
6222
+ }
6223
+ } catch {
6224
+ }
6225
+ return false;
6226
+ }
6227
+ }
6228
+ function releaseSpawnLock2() {
6229
+ try {
6230
+ unlinkSync6(SPAWN_LOCK_PATH);
6231
+ } catch {
6232
+ }
6233
+ }
6234
+ function connectToSocket() {
6235
+ return new Promise((resolve) => {
6236
+ if (_socket && _connected) {
6237
+ resolve(true);
6238
+ return;
6239
+ }
6240
+ const socket = net.createConnection({ path: SOCKET_PATH });
6241
+ const connectTimeout = setTimeout(() => {
6242
+ socket.destroy();
6243
+ resolve(false);
6244
+ }, 2e3);
6245
+ socket.on("connect", () => {
6246
+ clearTimeout(connectTimeout);
6247
+ _socket = socket;
6248
+ _connected = true;
6249
+ _buffer = "";
6250
+ socket.on("data", handleData);
6251
+ socket.on("close", () => {
6252
+ _connected = false;
6253
+ _socket = null;
6254
+ for (const [id, entry] of _pending) {
6255
+ clearTimeout(entry.timer);
6256
+ _pending.delete(id);
6257
+ entry.resolve({ error: "Connection closed" });
6258
+ }
6259
+ });
6260
+ socket.on("error", () => {
6261
+ _connected = false;
6262
+ _socket = null;
6263
+ });
6264
+ resolve(true);
6265
+ });
6266
+ socket.on("error", () => {
6267
+ clearTimeout(connectTimeout);
6268
+ resolve(false);
6269
+ });
6270
+ });
6271
+ }
6272
+ async function connectEmbedDaemon() {
6273
+ if (_socket && _connected) return true;
6274
+ if (await connectToSocket()) return true;
6275
+ if (acquireSpawnLock2()) {
6276
+ try {
6277
+ cleanupStaleFiles();
6278
+ spawnDaemon();
6279
+ } finally {
6280
+ releaseSpawnLock2();
6281
+ }
5777
6282
  }
5778
- });
5779
-
5780
- // src/lib/exe-daemon-client.ts
5781
- import net from "net";
5782
- import { spawn } from "child_process";
5783
- import { randomUUID as randomUUID6 } from "crypto";
5784
- import { existsSync as existsSync13, unlinkSync as unlinkSync6, readFileSync as readFileSync10, openSync, closeSync, statSync } from "fs";
5785
- import path17 from "path";
5786
- import { fileURLToPath as fileURLToPath2 } from "url";
5787
- function handleData(chunk) {
5788
- _buffer += chunk.toString();
5789
- if (_buffer.length > MAX_BUFFER) {
5790
- _buffer = "";
5791
- return;
6283
+ const start = Date.now();
6284
+ let delay2 = 100;
6285
+ while (Date.now() - start < CONNECT_TIMEOUT_MS) {
6286
+ await new Promise((r) => setTimeout(r, delay2));
6287
+ if (await connectToSocket()) return true;
6288
+ delay2 = Math.min(delay2 * 2, 3e3);
5792
6289
  }
5793
- let newlineIdx;
5794
- while ((newlineIdx = _buffer.indexOf("\n")) !== -1) {
5795
- const line = _buffer.slice(0, newlineIdx).trim();
5796
- _buffer = _buffer.slice(newlineIdx + 1);
5797
- if (!line) continue;
6290
+ return false;
6291
+ }
6292
+ function sendRequest(texts, priority) {
6293
+ return new Promise((resolve) => {
6294
+ if (!_socket || !_connected) {
6295
+ resolve({ error: "Not connected" });
6296
+ return;
6297
+ }
6298
+ const id = randomUUID6();
6299
+ const timer = setTimeout(() => {
6300
+ _pending.delete(id);
6301
+ resolve({ error: "Request timeout" });
6302
+ }, REQUEST_TIMEOUT_MS);
6303
+ _pending.set(id, { resolve, timer });
5798
6304
  try {
5799
- const response = JSON.parse(line);
5800
- const entry = _pending.get(response.id);
5801
- if (entry) {
5802
- clearTimeout(entry.timer);
5803
- _pending.delete(response.id);
5804
- entry.resolve(response);
5805
- }
6305
+ _socket.write(JSON.stringify({ id, texts, priority }) + "\n");
6306
+ } catch {
6307
+ clearTimeout(timer);
6308
+ _pending.delete(id);
6309
+ resolve({ error: "Write failed" });
6310
+ }
6311
+ });
6312
+ }
6313
+ async function pingDaemon() {
6314
+ if (!_socket || !_connected) return null;
6315
+ return new Promise((resolve) => {
6316
+ const id = randomUUID6();
6317
+ const timer = setTimeout(() => {
6318
+ _pending.delete(id);
6319
+ resolve(null);
6320
+ }, 5e3);
6321
+ _pending.set(id, {
6322
+ resolve: (data) => {
6323
+ if (data.health) {
6324
+ resolve(data.health);
6325
+ } else {
6326
+ resolve(null);
6327
+ }
6328
+ },
6329
+ timer
6330
+ });
6331
+ try {
6332
+ _socket.write(JSON.stringify({ id, type: "health" }) + "\n");
5806
6333
  } catch {
6334
+ clearTimeout(timer);
6335
+ _pending.delete(id);
6336
+ resolve(null);
5807
6337
  }
5808
- }
6338
+ });
5809
6339
  }
5810
- function cleanupStaleFiles() {
6340
+ function killAndRespawnDaemon() {
6341
+ process.stderr.write("[exed-client] Killing daemon for restart...\n");
5811
6342
  if (existsSync13(PID_PATH)) {
5812
6343
  try {
5813
6344
  const pid = parseInt(readFileSync10(PID_PATH, "utf8").trim(), 10);
5814
6345
  if (pid > 0) {
5815
6346
  try {
5816
- process.kill(pid, 0);
5817
- return;
6347
+ process.kill(pid, "SIGKILL");
5818
6348
  } catch {
5819
6349
  }
5820
6350
  }
5821
6351
  } catch {
5822
6352
  }
5823
- try {
5824
- unlinkSync6(PID_PATH);
5825
- } catch {
6353
+ }
6354
+ if (_socket) {
6355
+ _socket.destroy();
6356
+ _socket = null;
6357
+ }
6358
+ _connected = false;
6359
+ _buffer = "";
6360
+ try {
6361
+ unlinkSync6(PID_PATH);
6362
+ } catch {
6363
+ }
6364
+ try {
6365
+ unlinkSync6(SOCKET_PATH);
6366
+ } catch {
6367
+ }
6368
+ spawnDaemon();
6369
+ }
6370
+ async function embedViaClient(text, priority = "high") {
6371
+ if (!_connected && !await connectEmbedDaemon()) return null;
6372
+ _requestCount++;
6373
+ if (_requestCount % HEALTH_CHECK_INTERVAL === 0) {
6374
+ const health = await pingDaemon();
6375
+ if (!health) {
6376
+ process.stderr.write(`[exed-client] Periodic health check failed at request ${_requestCount} \u2014 restarting daemon
6377
+ `);
6378
+ killAndRespawnDaemon();
6379
+ const start = Date.now();
6380
+ let delay2 = 200;
6381
+ while (Date.now() - start < CONNECT_TIMEOUT_MS) {
6382
+ await new Promise((r) => setTimeout(r, delay2));
6383
+ if (await connectToSocket()) break;
6384
+ delay2 = Math.min(delay2 * 2, 3e3);
6385
+ }
6386
+ if (!_connected) return null;
5826
6387
  }
5827
- try {
5828
- unlinkSync6(SOCKET_PATH);
5829
- } catch {
6388
+ }
6389
+ const result = await sendRequest([text], priority);
6390
+ if (!result.error && result.vectors?.[0]) return result.vectors[0];
6391
+ if (result.error) {
6392
+ process.stderr.write(`[exed-client] Embed failed (${result.error}) \u2014 attempting restart
6393
+ `);
6394
+ killAndRespawnDaemon();
6395
+ const start = Date.now();
6396
+ let delay2 = 200;
6397
+ while (Date.now() - start < CONNECT_TIMEOUT_MS) {
6398
+ await new Promise((r) => setTimeout(r, delay2));
6399
+ if (await connectToSocket()) break;
6400
+ delay2 = Math.min(delay2 * 2, 3e3);
5830
6401
  }
6402
+ if (!_connected) return null;
6403
+ const retry = await sendRequest([text], priority);
6404
+ if (!retry.error && retry.vectors?.[0]) return retry.vectors[0];
6405
+ process.stderr.write(`[exed-client] Embed retry also failed: ${retry.error ?? "no vector"}
6406
+ `);
6407
+ }
6408
+ return null;
6409
+ }
6410
+ function disconnectClient() {
6411
+ if (_socket) {
6412
+ _socket.destroy();
6413
+ _socket = null;
6414
+ }
6415
+ _connected = false;
6416
+ _buffer = "";
6417
+ for (const [id, entry] of _pending) {
6418
+ clearTimeout(entry.timer);
6419
+ _pending.delete(id);
6420
+ entry.resolve({ error: "Client disconnected" });
5831
6421
  }
5832
6422
  }
5833
- function findPackageRoot() {
5834
- let dir = path17.dirname(fileURLToPath2(import.meta.url));
5835
- const { root } = path17.parse(dir);
5836
- while (dir !== root) {
5837
- if (existsSync13(path17.join(dir, "package.json"))) return dir;
5838
- dir = path17.dirname(dir);
6423
+ var SOCKET_PATH, PID_PATH, SPAWN_LOCK_PATH, SPAWN_LOCK_STALE_MS, CONNECT_TIMEOUT_MS, REQUEST_TIMEOUT_MS, _socket, _connected, _buffer, _requestCount, HEALTH_CHECK_INTERVAL, _pending, MAX_BUFFER;
6424
+ var init_exe_daemon_client = __esm({
6425
+ "src/lib/exe-daemon-client.ts"() {
6426
+ "use strict";
6427
+ init_config();
6428
+ SOCKET_PATH = process.env.EXE_DAEMON_SOCK ?? process.env.EXE_EMBED_SOCK ?? path17.join(EXE_AI_DIR, "exed.sock");
6429
+ PID_PATH = process.env.EXE_DAEMON_PID ?? process.env.EXE_EMBED_PID ?? path17.join(EXE_AI_DIR, "exed.pid");
6430
+ SPAWN_LOCK_PATH = path17.join(EXE_AI_DIR, "exed-spawn.lock");
6431
+ SPAWN_LOCK_STALE_MS = 3e4;
6432
+ CONNECT_TIMEOUT_MS = 15e3;
6433
+ REQUEST_TIMEOUT_MS = 3e4;
6434
+ _socket = null;
6435
+ _connected = false;
6436
+ _buffer = "";
6437
+ _requestCount = 0;
6438
+ HEALTH_CHECK_INTERVAL = 100;
6439
+ _pending = /* @__PURE__ */ new Map();
6440
+ MAX_BUFFER = 1e7;
6441
+ }
6442
+ });
6443
+
6444
+ // src/lib/embedder.ts
6445
+ var embedder_exports = {};
6446
+ __export(embedder_exports, {
6447
+ disposeEmbedder: () => disposeEmbedder,
6448
+ embed: () => embed,
6449
+ embedDirect: () => embedDirect,
6450
+ getEmbedder: () => getEmbedder
6451
+ });
6452
+ async function getEmbedder() {
6453
+ const ok = await connectEmbedDaemon();
6454
+ if (!ok) {
6455
+ throw new Error(
6456
+ "Could not connect to embedding daemon. Ensure the model is installed (run /exe-setup)."
6457
+ );
6458
+ }
6459
+ }
6460
+ async function embed(text) {
6461
+ const priority = process.env.EXE_EMBED_PRIORITY === "low" ? "low" : "high";
6462
+ const vector = await embedViaClient(text, priority);
6463
+ if (!vector) {
6464
+ throw new Error(
6465
+ "Embedding failed: daemon unavailable. Run /exe-setup to verify model installation."
6466
+ );
6467
+ }
6468
+ if (vector.length !== EMBEDDING_DIM) {
6469
+ throw new Error(
6470
+ `Embedding dimension mismatch: expected ${EMBEDDING_DIM}, got ${vector.length}. Ensure the correct Jina v5-small Q4_K_M GGUF model is installed.`
6471
+ );
6472
+ }
6473
+ return vector;
6474
+ }
6475
+ async function disposeEmbedder() {
6476
+ disconnectClient();
6477
+ }
6478
+ async function embedDirect(text) {
6479
+ const llamaCpp = await import("node-llama-cpp");
6480
+ const { MODELS_DIR: MODELS_DIR2 } = await Promise.resolve().then(() => (init_config(), config_exports));
6481
+ const { existsSync: existsSync15 } = await import("fs");
6482
+ const path20 = await import("path");
6483
+ const modelPath = path20.join(MODELS_DIR2, "jina-embeddings-v5-small-q4_k_m.gguf");
6484
+ if (!existsSync15(modelPath)) {
6485
+ throw new Error(`Embedding model not found at ${modelPath}. Run '/exe-setup' to download it.`);
6486
+ }
6487
+ const llama = await llamaCpp.getLlama();
6488
+ const model = await llama.loadModel({ modelPath });
6489
+ const context = await model.createEmbeddingContext();
6490
+ try {
6491
+ const embedding = await context.getEmbeddingFor(text);
6492
+ const vector = Array.from(embedding.vector);
6493
+ if (vector.length !== EMBEDDING_DIM) {
6494
+ throw new Error(
6495
+ `Embedding dimension mismatch: expected ${EMBEDDING_DIM}, got ${vector.length}.`
6496
+ );
6497
+ }
6498
+ return vector;
6499
+ } finally {
6500
+ await context.dispose();
6501
+ await model.dispose();
5839
6502
  }
5840
- return null;
5841
6503
  }
5842
- function spawnDaemon() {
5843
- const pkgRoot = findPackageRoot();
5844
- if (!pkgRoot) {
5845
- process.stderr.write("[exed-client] WARN: cannot find package root\n");
5846
- return;
5847
- }
5848
- const daemonPath = path17.join(pkgRoot, "dist", "lib", "exe-daemon.js");
5849
- if (!existsSync13(daemonPath)) {
5850
- process.stderr.write(`[exed-client] WARN: daemon script not found at ${daemonPath}
5851
- `);
5852
- return;
6504
+ var init_embedder = __esm({
6505
+ "src/lib/embedder.ts"() {
6506
+ "use strict";
6507
+ init_memory();
6508
+ init_exe_daemon_client();
5853
6509
  }
5854
- const resolvedPath = daemonPath;
5855
- process.stderr.write(`[exed-client] Spawning daemon: ${resolvedPath}
5856
- `);
5857
- const logPath = path17.join(path17.dirname(SOCKET_PATH), "exed.log");
5858
- let stderrFd = "ignore";
6510
+ });
6511
+
6512
+ // src/lib/wiki-client.ts
6513
+ var wiki_client_exports = {};
6514
+ __export(wiki_client_exports, {
6515
+ chatInWorkspace: () => chatInWorkspace,
6516
+ createWikiClient: () => createWikiClient,
6517
+ getChatHistory: () => getChatHistory,
6518
+ listDocuments: () => listDocuments,
6519
+ listWorkspaces: () => listWorkspaces
6520
+ });
6521
+ async function wikiFetch(config2, path20, method = "GET", body) {
6522
+ const url = `${config2.baseUrl}/api/v1${path20}`;
6523
+ const headers = {
6524
+ Authorization: `Bearer ${config2.apiKey}`,
6525
+ "Content-Type": "application/json"
6526
+ };
6527
+ const controller = new AbortController();
6528
+ const timeout = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS2);
5859
6529
  try {
5860
- stderrFd = openSync(logPath, "a");
5861
- } catch {
5862
- }
5863
- const child = spawn(process.execPath, [resolvedPath], {
5864
- detached: true,
5865
- stdio: ["ignore", "ignore", stderrFd],
5866
- env: {
5867
- ...process.env,
5868
- EXE_DAEMON_SOCK: SOCKET_PATH,
5869
- EXE_DAEMON_PID: PID_PATH
5870
- }
5871
- });
5872
- child.unref();
5873
- if (typeof stderrFd === "number") {
6530
+ let response;
5874
6531
  try {
5875
- closeSync(stderrFd);
6532
+ response = await fetch(url, {
6533
+ method,
6534
+ headers,
6535
+ body: body ? JSON.stringify(body) : void 0,
6536
+ signal: controller.signal
6537
+ });
5876
6538
  } catch {
6539
+ clearTimeout(timeout);
6540
+ const retryController = new AbortController();
6541
+ const retryTimeout = setTimeout(() => retryController.abort(), REQUEST_TIMEOUT_MS2);
6542
+ try {
6543
+ await new Promise((r) => setTimeout(r, 500));
6544
+ response = await fetch(url, {
6545
+ method,
6546
+ headers,
6547
+ body: body ? JSON.stringify(body) : void 0,
6548
+ signal: retryController.signal
6549
+ });
6550
+ } finally {
6551
+ clearTimeout(retryTimeout);
6552
+ }
6553
+ }
6554
+ if (!response.ok) {
6555
+ throw new Error(`Wiki API ${method} ${path20}: ${response.status} ${response.statusText}`);
5877
6556
  }
6557
+ return response.json();
6558
+ } finally {
6559
+ clearTimeout(timeout);
5878
6560
  }
5879
6561
  }
5880
- function acquireSpawnLock2() {
6562
+ async function resolveWikiUrl(configUrl) {
5881
6563
  try {
5882
- const fd = openSync(SPAWN_LOCK_PATH, "wx");
5883
- closeSync(fd);
5884
- return true;
6564
+ const controller = new AbortController();
6565
+ const timeout = setTimeout(() => controller.abort(), 3e3);
6566
+ const res = await fetch(`${LOCAL_WIKI_URL}/api/v1/auth`, { signal: controller.signal });
6567
+ clearTimeout(timeout);
6568
+ if (res.ok || res.status === 401 || res.status === 403) {
6569
+ return LOCAL_WIKI_URL;
6570
+ }
5885
6571
  } catch {
6572
+ }
6573
+ if (configUrl && configUrl !== LOCAL_WIKI_URL) {
5886
6574
  try {
5887
- const stat = statSync(SPAWN_LOCK_PATH);
5888
- if (Date.now() - stat.mtimeMs > SPAWN_LOCK_STALE_MS) {
5889
- try {
5890
- unlinkSync6(SPAWN_LOCK_PATH);
5891
- } catch {
5892
- }
5893
- try {
5894
- const fd = openSync(SPAWN_LOCK_PATH, "wx");
5895
- closeSync(fd);
5896
- return true;
5897
- } catch {
5898
- }
6575
+ const controller = new AbortController();
6576
+ const timeout = setTimeout(() => controller.abort(), 3e3);
6577
+ const res = await fetch(`${configUrl}/api/v1/auth`, { signal: controller.signal });
6578
+ clearTimeout(timeout);
6579
+ if (res.ok || res.status === 401 || res.status === 403) {
6580
+ return configUrl;
5899
6581
  }
5900
6582
  } catch {
5901
6583
  }
5902
- return false;
5903
6584
  }
6585
+ return null;
5904
6586
  }
5905
- function releaseSpawnLock2() {
5906
- try {
5907
- unlinkSync6(SPAWN_LOCK_PATH);
5908
- } catch {
5909
- }
6587
+ async function createWikiClient() {
6588
+ const { loadConfig: loadConfig2 } = await Promise.resolve().then(() => (init_config(), config_exports));
6589
+ const config2 = await loadConfig2();
6590
+ const baseUrl = await resolveWikiUrl(config2.wikiUrl);
6591
+ if (!baseUrl) return null;
6592
+ return {
6593
+ baseUrl,
6594
+ apiKey: config2.wikiApiKey
6595
+ };
5910
6596
  }
5911
- function connectToSocket() {
5912
- return new Promise((resolve) => {
5913
- if (_socket && _connected) {
5914
- resolve(true);
6597
+ async function listWorkspaces(client) {
6598
+ const data = await wikiFetch(client, "/workspaces");
6599
+ return (data.workspaces ?? []).map((w) => ({
6600
+ slug: w.slug,
6601
+ name: w.name,
6602
+ createdAt: w.createdAt ?? "",
6603
+ threads: w.threads ?? []
6604
+ }));
6605
+ }
6606
+ async function listDocuments(client, workspaceSlug) {
6607
+ const data = await wikiFetch(
6608
+ client,
6609
+ `/workspace/${encodeURIComponent(workspaceSlug)}`
6610
+ );
6611
+ const docs = Array.isArray(data.workspace) ? data.workspace[0]?.documents ?? [] : [];
6612
+ return docs.map((d) => ({
6613
+ filename: d.filename ?? "untitled",
6614
+ docpath: d.docpath,
6615
+ workspace: workspaceSlug,
6616
+ pinned: d.pinned ?? false
6617
+ }));
6618
+ }
6619
+ async function chatInWorkspace(client, workspaceSlug, message, mode = "chat") {
6620
+ const data = await wikiFetch(
6621
+ client,
6622
+ `/workspace/${encodeURIComponent(workspaceSlug)}/chat`,
6623
+ "POST",
6624
+ { message, mode }
6625
+ );
6626
+ return {
6627
+ id: data.id ?? "",
6628
+ type: data.type ?? "textResponse",
6629
+ textResponse: data.textResponse ?? "",
6630
+ sources: data.sources ?? [],
6631
+ error: data.error ?? null
6632
+ };
6633
+ }
6634
+ async function getChatHistory(client, workspaceSlug, limit = 50) {
6635
+ const data = await wikiFetch(
6636
+ client,
6637
+ `/workspace/${encodeURIComponent(workspaceSlug)}/chats?limit=${limit}&orderBy=desc`
6638
+ );
6639
+ return (data.history ?? []).reverse().map((h) => ({
6640
+ role: h.role === "user" ? "user" : "assistant",
6641
+ content: h.content,
6642
+ sentAt: h.sentAt ?? 0
6643
+ }));
6644
+ }
6645
+ var LOCAL_WIKI_URL, REQUEST_TIMEOUT_MS2;
6646
+ var init_wiki_client = __esm({
6647
+ "src/lib/wiki-client.ts"() {
6648
+ "use strict";
6649
+ LOCAL_WIKI_URL = "http://localhost:3001";
6650
+ REQUEST_TIMEOUT_MS2 = 8e3;
6651
+ }
6652
+ });
6653
+
6654
+ // src/lib/code-chunker.ts
6655
+ import ts from "typescript";
6656
+ function chunkSourceFile(source, fileName = "file.ts") {
6657
+ const sourceFile = ts.createSourceFile(
6658
+ fileName,
6659
+ source,
6660
+ ts.ScriptTarget.Latest,
6661
+ true,
6662
+ // setParentNodes
6663
+ fileName.endsWith(".tsx") || fileName.endsWith(".jsx") ? ts.ScriptKind.TSX : ts.ScriptKind.TS
6664
+ );
6665
+ const chunks = [];
6666
+ const lines = source.split("\n");
6667
+ const importLines = [];
6668
+ function getLineNumber(pos) {
6669
+ return sourceFile.getLineAndCharacterOfPosition(pos).line + 1;
6670
+ }
6671
+ function getLeadingComment(node) {
6672
+ const fullText = sourceFile.getFullText();
6673
+ const ranges = ts.getLeadingCommentRanges(fullText, node.getFullStart());
6674
+ if (!ranges || ranges.length === 0) return void 0;
6675
+ return ranges.map((r) => fullText.slice(r.pos, r.end)).join("\n");
6676
+ }
6677
+ function getNodeText(node) {
6678
+ return node.getText(sourceFile);
6679
+ }
6680
+ function getName(node) {
6681
+ if (ts.isFunctionDeclaration(node) || ts.isFunctionExpression(node)) {
6682
+ return node.name?.getText(sourceFile) ?? "(anonymous)";
6683
+ }
6684
+ if (ts.isClassDeclaration(node)) {
6685
+ return node.name?.getText(sourceFile) ?? "(anonymous class)";
6686
+ }
6687
+ if (ts.isInterfaceDeclaration(node)) {
6688
+ return node.name.getText(sourceFile);
6689
+ }
6690
+ if (ts.isTypeAliasDeclaration(node)) {
6691
+ return node.name.getText(sourceFile);
6692
+ }
6693
+ if (ts.isEnumDeclaration(node)) {
6694
+ return node.name.getText(sourceFile);
6695
+ }
6696
+ if (ts.isVariableStatement(node)) {
6697
+ const decls = node.declarationList.declarations;
6698
+ return decls.map((d) => d.name.getText(sourceFile)).join(", ");
6699
+ }
6700
+ if (ts.isExportAssignment(node)) {
6701
+ return "default export";
6702
+ }
6703
+ return "(unknown)";
6704
+ }
6705
+ function visitTopLevel(node) {
6706
+ if (ts.isImportDeclaration(node)) {
6707
+ importLines.push({
6708
+ start: getLineNumber(node.getStart(sourceFile)),
6709
+ end: getLineNumber(node.getEnd())
6710
+ });
5915
6711
  return;
5916
6712
  }
5917
- const socket = net.createConnection({ path: SOCKET_PATH });
5918
- const connectTimeout = setTimeout(() => {
5919
- socket.destroy();
5920
- resolve(false);
5921
- }, 2e3);
5922
- socket.on("connect", () => {
5923
- clearTimeout(connectTimeout);
5924
- _socket = socket;
5925
- _connected = true;
5926
- _buffer = "";
5927
- socket.on("data", handleData);
5928
- socket.on("close", () => {
5929
- _connected = false;
5930
- _socket = null;
5931
- for (const [id, entry] of _pending) {
5932
- clearTimeout(entry.timer);
5933
- _pending.delete(id);
5934
- entry.resolve({ error: "Connection closed" });
5935
- }
6713
+ if (ts.isFunctionDeclaration(node)) {
6714
+ chunks.push({
6715
+ kind: "function",
6716
+ name: getName(node),
6717
+ text: getNodeText(node),
6718
+ startLine: getLineNumber(node.getStart(sourceFile)),
6719
+ endLine: getLineNumber(node.getEnd()),
6720
+ comment: getLeadingComment(node)
5936
6721
  });
5937
- socket.on("error", () => {
5938
- _connected = false;
5939
- _socket = null;
6722
+ return;
6723
+ }
6724
+ if (ts.isClassDeclaration(node)) {
6725
+ chunks.push({
6726
+ kind: "class",
6727
+ name: getName(node),
6728
+ text: getNodeText(node),
6729
+ startLine: getLineNumber(node.getStart(sourceFile)),
6730
+ endLine: getLineNumber(node.getEnd()),
6731
+ comment: getLeadingComment(node)
5940
6732
  });
5941
- resolve(true);
5942
- });
5943
- socket.on("error", () => {
5944
- clearTimeout(connectTimeout);
5945
- resolve(false);
5946
- });
5947
- });
5948
- }
5949
- async function connectEmbedDaemon() {
5950
- if (_socket && _connected) return true;
5951
- if (await connectToSocket()) return true;
5952
- if (acquireSpawnLock2()) {
5953
- try {
5954
- cleanupStaleFiles();
5955
- spawnDaemon();
5956
- } finally {
5957
- releaseSpawnLock2();
6733
+ return;
6734
+ }
6735
+ if (ts.isInterfaceDeclaration(node)) {
6736
+ chunks.push({
6737
+ kind: "type",
6738
+ name: getName(node),
6739
+ text: getNodeText(node),
6740
+ startLine: getLineNumber(node.getStart(sourceFile)),
6741
+ endLine: getLineNumber(node.getEnd()),
6742
+ comment: getLeadingComment(node)
6743
+ });
6744
+ return;
6745
+ }
6746
+ if (ts.isTypeAliasDeclaration(node)) {
6747
+ chunks.push({
6748
+ kind: "type",
6749
+ name: getName(node),
6750
+ text: getNodeText(node),
6751
+ startLine: getLineNumber(node.getStart(sourceFile)),
6752
+ endLine: getLineNumber(node.getEnd()),
6753
+ comment: getLeadingComment(node)
6754
+ });
6755
+ return;
6756
+ }
6757
+ if (ts.isEnumDeclaration(node)) {
6758
+ chunks.push({
6759
+ kind: "type",
6760
+ name: getName(node),
6761
+ text: getNodeText(node),
6762
+ startLine: getLineNumber(node.getStart(sourceFile)),
6763
+ endLine: getLineNumber(node.getEnd()),
6764
+ comment: getLeadingComment(node)
6765
+ });
6766
+ return;
5958
6767
  }
5959
- }
5960
- const start = Date.now();
5961
- let delay2 = 100;
5962
- while (Date.now() - start < CONNECT_TIMEOUT_MS) {
5963
- await new Promise((r) => setTimeout(r, delay2));
5964
- if (await connectToSocket()) return true;
5965
- delay2 = Math.min(delay2 * 2, 3e3);
5966
- }
5967
- return false;
5968
- }
5969
- function sendRequest(texts, priority) {
5970
- return new Promise((resolve) => {
5971
- if (!_socket || !_connected) {
5972
- resolve({ error: "Not connected" });
6768
+ if (ts.isVariableStatement(node)) {
6769
+ const decls = node.declarationList.declarations;
6770
+ const isFnLike = decls.some(
6771
+ (d) => d.initializer && (ts.isArrowFunction(d.initializer) || ts.isFunctionExpression(d.initializer))
6772
+ );
6773
+ chunks.push({
6774
+ kind: isFnLike ? "function" : "variable",
6775
+ name: getName(node),
6776
+ text: getNodeText(node),
6777
+ startLine: getLineNumber(node.getStart(sourceFile)),
6778
+ endLine: getLineNumber(node.getEnd()),
6779
+ comment: getLeadingComment(node)
6780
+ });
5973
6781
  return;
5974
6782
  }
5975
- const id = randomUUID6();
5976
- const timer = setTimeout(() => {
5977
- _pending.delete(id);
5978
- resolve({ error: "Request timeout" });
5979
- }, REQUEST_TIMEOUT_MS);
5980
- _pending.set(id, { resolve, timer });
5981
- try {
5982
- _socket.write(JSON.stringify({ id, texts, priority }) + "\n");
5983
- } catch {
5984
- clearTimeout(timer);
5985
- _pending.delete(id);
5986
- resolve({ error: "Write failed" });
6783
+ if (ts.isExportAssignment(node)) {
6784
+ chunks.push({
6785
+ kind: "export",
6786
+ name: "default export",
6787
+ text: getNodeText(node),
6788
+ startLine: getLineNumber(node.getStart(sourceFile)),
6789
+ endLine: getLineNumber(node.getEnd()),
6790
+ comment: getLeadingComment(node)
6791
+ });
6792
+ return;
5987
6793
  }
5988
- });
6794
+ if (ts.isExpressionStatement(node)) {
6795
+ const text = getNodeText(node);
6796
+ if (text.length > 10) {
6797
+ chunks.push({
6798
+ kind: "other",
6799
+ name: text.slice(0, 40).replace(/\n/g, " "),
6800
+ text,
6801
+ startLine: getLineNumber(node.getStart(sourceFile)),
6802
+ endLine: getLineNumber(node.getEnd())
6803
+ });
6804
+ }
6805
+ return;
6806
+ }
6807
+ }
6808
+ sourceFile.statements.forEach(visitTopLevel);
6809
+ if (importLines.length > 0) {
6810
+ const startLine = importLines[0].start;
6811
+ const endLine = importLines[importLines.length - 1].end;
6812
+ const importText = lines.slice(startLine - 1, endLine).join("\n");
6813
+ chunks.unshift({
6814
+ kind: "import",
6815
+ name: `${importLines.length} imports`,
6816
+ text: importText,
6817
+ startLine,
6818
+ endLine
6819
+ });
6820
+ }
6821
+ chunks.sort((a, b) => a.startLine - b.startLine);
6822
+ return chunks;
5989
6823
  }
5990
- async function pingDaemon() {
5991
- if (!_socket || !_connected) return null;
5992
- return new Promise((resolve) => {
5993
- const id = randomUUID6();
5994
- const timer = setTimeout(() => {
5995
- _pending.delete(id);
5996
- resolve(null);
5997
- }, 5e3);
5998
- _pending.set(id, {
5999
- resolve: (data) => {
6000
- if (data.health) {
6001
- resolve(data.health);
6002
- } else {
6003
- resolve(null);
6004
- }
6005
- },
6006
- timer
6824
+ function isChunkable(filePath) {
6825
+ const ext = filePath.split(".").pop()?.toLowerCase();
6826
+ return ext === "ts" || ext === "tsx" || ext === "js" || ext === "jsx";
6827
+ }
6828
+ var init_code_chunker = __esm({
6829
+ "src/lib/code-chunker.ts"() {
6830
+ "use strict";
6831
+ }
6832
+ });
6833
+
6834
+ // src/lib/graph-rag.ts
6835
+ var graph_rag_exports = {};
6836
+ __export(graph_rag_exports, {
6837
+ EXTRACT_TOOL: () => EXTRACT_TOOL,
6838
+ entityId: () => entityId,
6839
+ extractBatch: () => extractBatch,
6840
+ extractFromCode: () => extractFromCode,
6841
+ extractFromMemory: () => extractFromMemory,
6842
+ mergeEntities: () => mergeEntities,
6843
+ normalizeEntityName: () => normalizeEntityName,
6844
+ registerAlias: () => registerAlias,
6845
+ resolveAlias: () => resolveAlias,
6846
+ storeExtraction: () => storeExtraction
6847
+ });
6848
+ import crypto7 from "crypto";
6849
+ function normalizeEntityName(name) {
6850
+ return name.replace(/\s*\([^)]*\)\s*/g, "").trim().toLowerCase();
6851
+ }
6852
+ function entityId(name, type) {
6853
+ const normalized = normalizeEntityName(name);
6854
+ return crypto7.createHash("sha256").update(`${normalized}::${type.toLowerCase()}`).digest("hex").slice(0, 16);
6855
+ }
6856
+ async function resolveAlias(client, name) {
6857
+ const normalized = normalizeEntityName(name);
6858
+ try {
6859
+ const result = await client.execute({
6860
+ sql: "SELECT canonical_entity_id FROM entity_aliases WHERE alias = ?",
6861
+ args: [normalized]
6007
6862
  });
6008
- try {
6009
- _socket.write(JSON.stringify({ id, type: "health" }) + "\n");
6010
- } catch {
6011
- clearTimeout(timer);
6012
- _pending.delete(id);
6013
- resolve(null);
6863
+ if (result.rows.length > 0) {
6864
+ return String(result.rows[0].canonical_entity_id);
6014
6865
  }
6866
+ } catch {
6867
+ }
6868
+ return null;
6869
+ }
6870
+ async function registerAlias(client, alias, canonicalEntityId) {
6871
+ const normalized = normalizeEntityName(alias);
6872
+ await client.execute({
6873
+ sql: `INSERT OR REPLACE INTO entity_aliases (alias, canonical_entity_id) VALUES (?, ?)`,
6874
+ args: [normalized, canonicalEntityId]
6015
6875
  });
6016
6876
  }
6017
- function killAndRespawnDaemon() {
6018
- process.stderr.write("[exed-client] Killing daemon for restart...\n");
6019
- if (existsSync13(PID_PATH)) {
6020
- try {
6021
- const pid = parseInt(readFileSync10(PID_PATH, "utf8").trim(), 10);
6022
- if (pid > 0) {
6023
- try {
6024
- process.kill(pid, "SIGKILL");
6025
- } catch {
6026
- }
6027
- }
6028
- } catch {
6029
- }
6877
+ async function mergeEntities(client, sourceEntityId, targetEntityId) {
6878
+ const sourceResult = await client.execute({
6879
+ sql: "SELECT name FROM entities WHERE id = ?",
6880
+ args: [sourceEntityId]
6881
+ });
6882
+ if (sourceResult.rows.length === 0) {
6883
+ throw new Error(`Source entity ${sourceEntityId} not found`);
6030
6884
  }
6031
- if (_socket) {
6032
- _socket.destroy();
6033
- _socket = null;
6885
+ const targetResult = await client.execute({
6886
+ sql: "SELECT id FROM entities WHERE id = ?",
6887
+ args: [targetEntityId]
6888
+ });
6889
+ if (targetResult.rows.length === 0) {
6890
+ throw new Error(`Target entity ${targetEntityId} not found`);
6891
+ }
6892
+ const sourceName = String(sourceResult.rows[0].name);
6893
+ const relsSource = await client.execute({
6894
+ sql: `UPDATE relationships SET source_entity_id = ?
6895
+ WHERE source_entity_id = ?
6896
+ AND NOT EXISTS (
6897
+ SELECT 1 FROM relationships r2
6898
+ WHERE r2.source_entity_id = ? AND r2.target_entity_id = relationships.target_entity_id AND r2.type = relationships.type
6899
+ )`,
6900
+ args: [targetEntityId, sourceEntityId, targetEntityId]
6901
+ });
6902
+ const relsTarget = await client.execute({
6903
+ sql: `UPDATE relationships SET target_entity_id = ?
6904
+ WHERE target_entity_id = ?
6905
+ AND NOT EXISTS (
6906
+ SELECT 1 FROM relationships r2
6907
+ WHERE r2.target_entity_id = ? AND r2.source_entity_id = relationships.source_entity_id AND r2.type = relationships.type
6908
+ )`,
6909
+ args: [targetEntityId, sourceEntityId, targetEntityId]
6910
+ });
6911
+ await client.execute({
6912
+ sql: "DELETE FROM relationships WHERE source_entity_id = ? OR target_entity_id = ?",
6913
+ args: [sourceEntityId, sourceEntityId]
6914
+ });
6915
+ const mems = await client.execute({
6916
+ sql: `INSERT OR IGNORE INTO entity_memories (entity_id, memory_id)
6917
+ SELECT ?, memory_id FROM entity_memories WHERE entity_id = ?`,
6918
+ args: [targetEntityId, sourceEntityId]
6919
+ });
6920
+ await client.execute({
6921
+ sql: "DELETE FROM entity_memories WHERE entity_id = ?",
6922
+ args: [sourceEntityId]
6923
+ });
6924
+ await registerAlias(client, sourceName, targetEntityId);
6925
+ await client.execute({
6926
+ sql: "DELETE FROM entities WHERE id = ?",
6927
+ args: [sourceEntityId]
6928
+ });
6929
+ return {
6930
+ relationshipsMoved: (relsSource.rowsAffected ?? 0) + (relsTarget.rowsAffected ?? 0),
6931
+ memoriesMoved: mems.rowsAffected ?? 0
6932
+ };
6933
+ }
6934
+ function parseImportPaths(importText) {
6935
+ const paths = [];
6936
+ const fromRegex = /from\s+["']([^"']+)["']/g;
6937
+ let match;
6938
+ while ((match = fromRegex.exec(importText)) !== null) {
6939
+ paths.push(match[1]);
6034
6940
  }
6035
- _connected = false;
6036
- _buffer = "";
6037
- try {
6038
- unlinkSync6(PID_PATH);
6039
- } catch {
6941
+ const bareRegex = /^import\s+["']([^"']+)["']/gm;
6942
+ while ((match = bareRegex.exec(importText)) !== null) {
6943
+ if (!paths.includes(match[1])) paths.push(match[1]);
6040
6944
  }
6945
+ return paths;
6946
+ }
6947
+ function moduleNameFromPath(importPath) {
6948
+ const base = importPath.split("/").pop() ?? importPath;
6949
+ return base.replace(/\.\w+$/, "") || base;
6950
+ }
6951
+ function extractFromCode(source, filePath) {
6952
+ let chunks;
6041
6953
  try {
6042
- unlinkSync6(SOCKET_PATH);
6954
+ chunks = chunkSourceFile(source, filePath);
6043
6955
  } catch {
6044
- }
6045
- spawnDaemon();
6046
- }
6047
- async function embedViaClient(text, priority = "high") {
6048
- if (!_connected && !await connectEmbedDaemon()) return null;
6049
- _requestCount++;
6050
- if (_requestCount % HEALTH_CHECK_INTERVAL === 0) {
6051
- const health = await pingDaemon();
6052
- if (!health) {
6053
- process.stderr.write(`[exed-client] Periodic health check failed at request ${_requestCount} \u2014 restarting daemon
6054
- `);
6055
- killAndRespawnDaemon();
6056
- const start = Date.now();
6057
- let delay2 = 200;
6058
- while (Date.now() - start < CONNECT_TIMEOUT_MS) {
6059
- await new Promise((r) => setTimeout(r, delay2));
6060
- if (await connectToSocket()) break;
6061
- delay2 = Math.min(delay2 * 2, 3e3);
6956
+ return { entities: [], relationships: [], hyperedges: [] };
6957
+ }
6958
+ if (chunks.length === 0) {
6959
+ return { entities: [], relationships: [], hyperedges: [] };
6960
+ }
6961
+ const entities = [];
6962
+ const relationships = [];
6963
+ const fileName = filePath.split("/").pop() ?? filePath;
6964
+ entities.push({ name: fileName, type: "file" });
6965
+ for (const chunk of chunks) {
6966
+ if (chunk.kind === "import") {
6967
+ const importPaths = parseImportPaths(chunk.text);
6968
+ for (const importPath of importPaths) {
6969
+ const moduleName = moduleNameFromPath(importPath);
6970
+ entities.push({ name: moduleName, type: "file" });
6971
+ relationships.push({
6972
+ source: fileName,
6973
+ sourceType: "file",
6974
+ target: moduleName,
6975
+ targetType: "file",
6976
+ relationship: "depends_on",
6977
+ confidence: 1,
6978
+ confidenceLabel: "extracted"
6979
+ });
6062
6980
  }
6063
- if (!_connected) return null;
6064
- }
6065
- }
6066
- const result = await sendRequest([text], priority);
6067
- if (!result.error && result.vectors?.[0]) return result.vectors[0];
6068
- if (result.error) {
6069
- process.stderr.write(`[exed-client] Embed failed (${result.error}) \u2014 attempting restart
6070
- `);
6071
- killAndRespawnDaemon();
6072
- const start = Date.now();
6073
- let delay2 = 200;
6074
- while (Date.now() - start < CONNECT_TIMEOUT_MS) {
6075
- await new Promise((r) => setTimeout(r, delay2));
6076
- if (await connectToSocket()) break;
6077
- delay2 = Math.min(delay2 * 2, 3e3);
6981
+ continue;
6078
6982
  }
6079
- if (!_connected) return null;
6080
- const retry = await sendRequest([text], priority);
6081
- if (!retry.error && retry.vectors?.[0]) return retry.vectors[0];
6082
- process.stderr.write(`[exed-client] Embed retry also failed: ${retry.error ?? "no vector"}
6083
- `);
6983
+ if (chunk.kind === "other" || chunk.name === "(unknown)") continue;
6984
+ entities.push({ name: chunk.name, type: "concept" });
6985
+ relationships.push({
6986
+ source: fileName,
6987
+ sourceType: "file",
6988
+ target: chunk.name,
6989
+ targetType: "concept",
6990
+ relationship: "uses",
6991
+ confidence: 1,
6992
+ confidenceLabel: "extracted"
6993
+ });
6084
6994
  }
6085
- return null;
6995
+ return { entities, relationships, hyperedges: [] };
6086
6996
  }
6087
- function disconnectClient() {
6088
- if (_socket) {
6089
- _socket.destroy();
6090
- _socket = null;
6091
- }
6092
- _connected = false;
6093
- _buffer = "";
6094
- for (const [id, entry] of _pending) {
6095
- clearTimeout(entry.timer);
6096
- _pending.delete(id);
6097
- entry.resolve({ error: "Client disconnected" });
6997
+ async function extractFromMemory(content, agentId, model = "claude-haiku-4-5-20251001", filePath) {
6998
+ if (content.length < 50) {
6999
+ return { entities: [], relationships: [], hyperedges: [] };
6098
7000
  }
6099
- }
6100
- 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;
6101
- var init_exe_daemon_client = __esm({
6102
- "src/lib/exe-daemon-client.ts"() {
6103
- "use strict";
6104
- init_config();
6105
- SOCKET_PATH = process.env.EXE_DAEMON_SOCK ?? process.env.EXE_EMBED_SOCK ?? path17.join(EXE_AI_DIR, "exed.sock");
6106
- PID_PATH = process.env.EXE_DAEMON_PID ?? process.env.EXE_EMBED_PID ?? path17.join(EXE_AI_DIR, "exed.pid");
6107
- SPAWN_LOCK_PATH = path17.join(EXE_AI_DIR, "exed-spawn.lock");
6108
- SPAWN_LOCK_STALE_MS = 3e4;
6109
- CONNECT_TIMEOUT_MS = 15e3;
6110
- REQUEST_TIMEOUT_MS = 3e4;
6111
- _socket = null;
6112
- _connected = false;
6113
- _buffer = "";
6114
- _requestCount = 0;
6115
- HEALTH_CHECK_INTERVAL = 100;
6116
- _pending = /* @__PURE__ */ new Map();
6117
- MAX_BUFFER = 1e7;
7001
+ if (filePath && isChunkable(filePath)) {
7002
+ const astResult = extractFromCode(content, filePath);
7003
+ if (astResult.entities.length > 0) {
7004
+ return astResult;
7005
+ }
6118
7006
  }
6119
- });
6120
-
6121
- // src/lib/embedder.ts
6122
- var embedder_exports = {};
6123
- __export(embedder_exports, {
6124
- disposeEmbedder: () => disposeEmbedder,
6125
- embed: () => embed,
6126
- embedDirect: () => embedDirect,
6127
- getEmbedder: () => getEmbedder
6128
- });
6129
- async function getEmbedder() {
6130
- const ok = await connectEmbedDaemon();
6131
- if (!ok) {
6132
- throw new Error(
6133
- "Could not connect to embedding daemon. Ensure the model is installed (run /exe-setup)."
7007
+ try {
7008
+ const Anthropic4 = (await import("@anthropic-ai/sdk")).default;
7009
+ const client = new Anthropic4();
7010
+ const response = await client.messages.create({
7011
+ model,
7012
+ max_tokens: 512,
7013
+ system: `Extract entities and relationships from this AI agent work memory. Agent: ${agentId}. Focus on: people, tools, projects, decisions, files, rationale \u2014 and for business contexts: products, companies, suppliers, locations, orders, categories. Only extract what's clearly stated \u2014 don't infer. Use the simplest form of names (e.g., "Lenny" not "Lenny (HYGO)"). When content says "we chose X because Y", extract a rationale entity with rationale_for edge. Business examples: "Lenny supplies organic flour to HYGO" \u2192 (Lenny, supplies, HYGO). "Order #1234 shipped to Auckland" \u2192 (Order #1234, ships_to, Auckland).`,
7014
+ messages: [{ role: "user", content: content.slice(0, 1e3) }],
7015
+ tools: [EXTRACT_TOOL],
7016
+ tool_choice: { type: "tool", name: "extract_entities_and_relationships" }
7017
+ });
7018
+ const toolBlock = response.content.find((b) => b.type === "tool_use");
7019
+ if (toolBlock && toolBlock.type === "tool_use") {
7020
+ const input = toolBlock.input;
7021
+ const rawEntities = input.entities ?? [];
7022
+ const rawRels = input.relationships ?? [];
7023
+ const rawHyperedges = input.hyperedges ?? [];
7024
+ return {
7025
+ entities: rawEntities.map((e) => ({
7026
+ name: e.name ?? "",
7027
+ type: e.type ?? "concept"
7028
+ })).filter((e) => e.name.length > 0),
7029
+ relationships: rawRels.map((r) => ({
7030
+ source: r.source ?? "",
7031
+ sourceType: r.source_type ?? "concept",
7032
+ target: r.target ?? "",
7033
+ targetType: r.target_type ?? "concept",
7034
+ relationship: r.relationship ?? "uses",
7035
+ confidence: Number(r.confidence ?? 1),
7036
+ confidenceLabel: String(r.confidence_label ?? "extracted")
7037
+ })).filter((r) => r.source.length > 0 && r.target.length > 0),
7038
+ hyperedges: rawHyperedges.map((h) => ({
7039
+ label: String(h.label ?? ""),
7040
+ relation: String(h.relation ?? ""),
7041
+ entityNames: h.entity_names ?? [],
7042
+ confidence: Number(h.confidence ?? 1)
7043
+ })).filter((h) => h.label.length > 0 && h.entityNames.length >= 3)
7044
+ };
7045
+ }
7046
+ } catch (err) {
7047
+ process.stderr.write(
7048
+ `[graph-rag] Extraction failed: ${err instanceof Error ? err.message : String(err)}
7049
+ `
6134
7050
  );
6135
7051
  }
7052
+ return { entities: [], relationships: [], hyperedges: [] };
6136
7053
  }
6137
- async function embed(text) {
6138
- const priority = process.env.EXE_EMBED_PRIORITY === "low" ? "low" : "high";
6139
- const vector = await embedViaClient(text, priority);
6140
- if (!vector) {
6141
- throw new Error(
6142
- "Embedding failed: daemon unavailable. Run /exe-setup to verify model installation."
6143
- );
7054
+ async function storeExtraction(client, extraction, memoryId, timestamp) {
7055
+ let entitiesStored = 0;
7056
+ let relationshipsStored = 0;
7057
+ for (const e of extraction.entities) {
7058
+ const aliasTarget = await resolveAlias(client, e.name);
7059
+ const id = aliasTarget ?? entityId(e.name, e.type);
7060
+ const normalizedName = normalizeEntityName(e.name);
7061
+ try {
7062
+ const newProps = e.properties ?? {};
7063
+ let mergedPropsJson = JSON.stringify(newProps);
7064
+ if (newProps.provenance) {
7065
+ const existing = await client.execute({
7066
+ sql: "SELECT properties FROM entities WHERE id = ?",
7067
+ args: [aliasTarget ?? id]
7068
+ });
7069
+ const existingProps = existing.rows.length > 0 ? JSON.parse(String(existing.rows[0].properties ?? "{}")) : {};
7070
+ const existingProvenance = Array.isArray(existingProps.provenance) ? existingProps.provenance : existingProps.provenance ? [existingProps.provenance] : [];
7071
+ mergedPropsJson = JSON.stringify({
7072
+ ...existingProps,
7073
+ ...newProps,
7074
+ provenance: [...existingProvenance, newProps.provenance]
7075
+ });
7076
+ }
7077
+ if (aliasTarget) {
7078
+ await client.execute({
7079
+ sql: `UPDATE entities SET last_seen = ?, properties = ? WHERE id = ?`,
7080
+ args: [timestamp, mergedPropsJson, aliasTarget]
7081
+ });
7082
+ } else {
7083
+ await client.execute({
7084
+ sql: `INSERT INTO entities (id, name, type, first_seen, last_seen, properties)
7085
+ VALUES (?, ?, ?, ?, ?, ?)
7086
+ ON CONFLICT(name, type) DO UPDATE SET last_seen = ?, properties = ?`,
7087
+ args: [id, normalizedName, e.type, timestamp, timestamp, mergedPropsJson, timestamp, mergedPropsJson]
7088
+ });
7089
+ }
7090
+ await client.execute({
7091
+ sql: `INSERT OR IGNORE INTO entity_memories (entity_id, memory_id)
7092
+ VALUES (?, ?)`,
7093
+ args: [id, memoryId]
7094
+ });
7095
+ entitiesStored++;
7096
+ } catch {
7097
+ }
6144
7098
  }
6145
- if (vector.length !== EMBEDDING_DIM) {
6146
- throw new Error(
6147
- `Embedding dimension mismatch: expected ${EMBEDDING_DIM}, got ${vector.length}. Ensure the correct Jina v5-small Q4_K_M GGUF model is installed.`
6148
- );
7099
+ for (const r of extraction.relationships) {
7100
+ const sourceAlias = await resolveAlias(client, r.source);
7101
+ const targetAlias = await resolveAlias(client, r.target);
7102
+ const sourceId = sourceAlias ?? entityId(r.source, r.sourceType);
7103
+ const targetId = targetAlias ?? entityId(r.target, r.targetType);
7104
+ const relId = crypto7.randomUUID().slice(0, 16);
7105
+ try {
7106
+ await client.execute({
7107
+ sql: `INSERT OR IGNORE INTO entities (id, name, type, first_seen, last_seen)
7108
+ VALUES (?, ?, ?, ?, ?)`,
7109
+ args: [sourceId, r.source, r.sourceType, timestamp, timestamp]
7110
+ });
7111
+ await client.execute({
7112
+ sql: `INSERT OR IGNORE INTO entities (id, name, type, first_seen, last_seen)
7113
+ VALUES (?, ?, ?, ?, ?)`,
7114
+ args: [targetId, r.target, r.targetType, timestamp, timestamp]
7115
+ });
7116
+ const relProps = r.properties ?? {};
7117
+ let relPropsJson = JSON.stringify(relProps);
7118
+ if (relProps.provenance) {
7119
+ const existingRels = await client.execute({
7120
+ sql: "SELECT properties FROM relationships WHERE source_entity_id = ? AND target_entity_id = ? AND type = ?",
7121
+ args: [sourceId, targetId, r.relationship]
7122
+ });
7123
+ const existingRelProps = existingRels.rows.length > 0 ? JSON.parse(String(existingRels.rows[0].properties ?? "{}")) : {};
7124
+ const existingProvenance = Array.isArray(existingRelProps.provenance) ? existingRelProps.provenance : existingRelProps.provenance ? [existingRelProps.provenance] : [];
7125
+ relPropsJson = JSON.stringify({
7126
+ ...existingRelProps,
7127
+ ...relProps,
7128
+ provenance: [...existingProvenance, relProps.provenance]
7129
+ });
7130
+ }
7131
+ await client.execute({
7132
+ sql: `INSERT INTO relationships (id, source_entity_id, target_entity_id, type, weight, timestamp, confidence, confidence_label, properties)
7133
+ VALUES (?, ?, ?, ?, 1.0, ?, ?, ?, ?)
7134
+ ON CONFLICT(source_entity_id, target_entity_id, type)
7135
+ DO UPDATE SET weight = MIN(weight + 0.1, 2.0), timestamp = ?,
7136
+ confidence = MAX(confidence, ?), confidence_label = ?, properties = ?`,
7137
+ args: [
7138
+ relId,
7139
+ sourceId,
7140
+ targetId,
7141
+ r.relationship,
7142
+ timestamp,
7143
+ r.confidence,
7144
+ r.confidenceLabel,
7145
+ relPropsJson,
7146
+ timestamp,
7147
+ r.confidence,
7148
+ r.confidenceLabel,
7149
+ relPropsJson
7150
+ ]
7151
+ });
7152
+ const existingRel = await client.execute({
7153
+ sql: `SELECT id FROM relationships WHERE source_entity_id = ? AND target_entity_id = ? AND type = ?`,
7154
+ args: [sourceId, targetId, r.relationship]
7155
+ });
7156
+ const actualRelId = existingRel.rows[0]?.id ? String(existingRel.rows[0].id) : relId;
7157
+ await client.execute({
7158
+ sql: `INSERT OR IGNORE INTO relationship_memories (relationship_id, memory_id)
7159
+ VALUES (?, ?)`,
7160
+ args: [actualRelId, memoryId]
7161
+ });
7162
+ relationshipsStored++;
7163
+ } catch {
7164
+ }
6149
7165
  }
6150
- return vector;
6151
- }
6152
- async function disposeEmbedder() {
6153
- disconnectClient();
7166
+ for (const h of extraction.hyperedges) {
7167
+ const hId = crypto7.randomUUID().slice(0, 16);
7168
+ try {
7169
+ await client.execute({
7170
+ sql: `INSERT OR IGNORE INTO hyperedges (id, label, relation, confidence, timestamp)
7171
+ VALUES (?, ?, ?, ?, ?)`,
7172
+ args: [hId, h.label, h.relation, h.confidence, timestamp]
7173
+ });
7174
+ for (const entityName of h.entityNames) {
7175
+ const eType = extraction.entities.find((e) => e.name === entityName)?.type ?? "concept";
7176
+ const eId = entityId(entityName, eType);
7177
+ await client.execute({
7178
+ sql: `INSERT OR IGNORE INTO entities (id, name, type, first_seen, last_seen)
7179
+ VALUES (?, ?, ?, ?, ?)`,
7180
+ args: [eId, entityName, eType, timestamp, timestamp]
7181
+ });
7182
+ await client.execute({
7183
+ sql: `INSERT OR IGNORE INTO hyperedge_nodes (hyperedge_id, entity_id) VALUES (?, ?)`,
7184
+ args: [hId, eId]
7185
+ });
7186
+ }
7187
+ } catch {
7188
+ }
7189
+ }
7190
+ return { entitiesStored, relationshipsStored };
6154
7191
  }
6155
- async function embedDirect(text) {
6156
- const llamaCpp = await import("node-llama-cpp");
6157
- const { MODELS_DIR: MODELS_DIR2 } = await Promise.resolve().then(() => (init_config(), config_exports));
6158
- const { existsSync: existsSync15 } = await import("fs");
6159
- const path20 = await import("path");
6160
- const modelPath = path20.join(MODELS_DIR2, "jina-embeddings-v5-small-q4_k_m.gguf");
6161
- if (!existsSync15(modelPath)) {
6162
- throw new Error(`Embedding model not found at ${modelPath}. Run '/exe-setup' to download it.`);
7192
+ async function extractBatch(client, batchSize = 50, model = "claude-haiku-4-5-20251001") {
7193
+ const result = await client.execute({
7194
+ sql: `SELECT id, agent_id, raw_text, tool_name, timestamp, content_hash, graph_extracted_hash
7195
+ FROM memories
7196
+ WHERE (COALESCE(graph_extracted, 0) = 0
7197
+ OR (content_hash IS NOT NULL AND content_hash != COALESCE(graph_extracted_hash, '')))
7198
+ AND COALESCE(status, 'active') = 'active'
7199
+ AND LENGTH(raw_text) >= 50
7200
+ ORDER BY timestamp DESC
7201
+ LIMIT ?`,
7202
+ args: [batchSize]
7203
+ });
7204
+ if (result.rows.length === 0) {
7205
+ return { processed: 0, entities: 0, relationships: 0 };
6163
7206
  }
6164
- const llama = await llamaCpp.getLlama();
6165
- const model = await llama.loadModel({ modelPath });
6166
- const context = await model.createEmbeddingContext();
6167
- try {
6168
- const embedding = await context.getEmbeddingFor(text);
6169
- const vector = Array.from(embedding.vector);
6170
- if (vector.length !== EMBEDDING_DIM) {
6171
- throw new Error(
6172
- `Embedding dimension mismatch: expected ${EMBEDDING_DIM}, got ${vector.length}.`
7207
+ let totalEntities = 0;
7208
+ let totalRelationships = 0;
7209
+ for (const row of result.rows) {
7210
+ const memoryId = String(row.id);
7211
+ const agentId = String(row.agent_id);
7212
+ const rawContent = String(row.raw_text);
7213
+ const toolName = String(row.tool_name ?? "");
7214
+ const timestamp = String(row.timestamp);
7215
+ let extractionContent = rawContent;
7216
+ let detectedFilePath;
7217
+ if (toolName === "Read") {
7218
+ const pathMatch = rawContent.match(READ_MEMORY_PATH_PATTERN);
7219
+ if (pathMatch) {
7220
+ detectedFilePath = pathMatch[1];
7221
+ extractionContent = rawContent.slice(pathMatch[0].length);
7222
+ }
7223
+ }
7224
+ try {
7225
+ const extraction = await extractFromMemory(extractionContent, agentId, model, detectedFilePath);
7226
+ if (extraction.entities.length > 0 || extraction.relationships.length > 0) {
7227
+ const stored = await storeExtraction(client, extraction, memoryId, timestamp);
7228
+ totalEntities += stored.entitiesStored;
7229
+ totalRelationships += stored.relationshipsStored;
7230
+ }
7231
+ const contentHash = crypto7.createHash("sha256").update(rawContent).digest("hex").slice(0, 32);
7232
+ await client.execute({
7233
+ sql: "UPDATE memories SET graph_extracted = 1, content_hash = ?, graph_extracted_hash = ? WHERE id = ?",
7234
+ args: [contentHash, contentHash, memoryId]
7235
+ });
7236
+ } catch (err) {
7237
+ process.stderr.write(
7238
+ `[graph-rag] Batch extraction failed for ${memoryId}: ${err instanceof Error ? err.message : String(err)}
7239
+ `
6173
7240
  );
7241
+ await client.execute({
7242
+ sql: "UPDATE memories SET graph_extracted = 1 WHERE id = ?",
7243
+ args: [memoryId]
7244
+ });
6174
7245
  }
6175
- return vector;
6176
- } finally {
6177
- await context.dispose();
6178
- await model.dispose();
6179
7246
  }
7247
+ return { processed: result.rows.length, entities: totalEntities, relationships: totalRelationships };
6180
7248
  }
6181
- var init_embedder = __esm({
6182
- "src/lib/embedder.ts"() {
7249
+ var EXTRACT_TOOL, READ_MEMORY_PATH_PATTERN;
7250
+ var init_graph_rag = __esm({
7251
+ "src/lib/graph-rag.ts"() {
6183
7252
  "use strict";
6184
- init_memory();
6185
- init_exe_daemon_client();
7253
+ init_code_chunker();
7254
+ EXTRACT_TOOL = {
7255
+ name: "extract_entities_and_relationships",
7256
+ description: "Extract entities, relationships, and group connections from a memory record. Include confidence scoring.",
7257
+ input_schema: {
7258
+ type: "object",
7259
+ properties: {
7260
+ entities: {
7261
+ type: "array",
7262
+ items: {
7263
+ type: "object",
7264
+ properties: {
7265
+ name: { type: "string", description: "Entity name" },
7266
+ type: { type: "string", enum: ["person", "tool", "project", "decision", "concept", "file", "rationale", "product", "company", "location", "order", "supplier", "category"] }
7267
+ },
7268
+ required: ["name", "type"]
7269
+ }
7270
+ },
7271
+ relationships: {
7272
+ type: "array",
7273
+ items: {
7274
+ type: "object",
7275
+ properties: {
7276
+ source: { type: "string" },
7277
+ source_type: { type: "string" },
7278
+ target: { type: "string" },
7279
+ target_type: { type: "string" },
7280
+ relationship: {
7281
+ type: "string",
7282
+ enum: [
7283
+ // Engineering
7284
+ "implemented",
7285
+ "superseded_by",
7286
+ "depends_on",
7287
+ "blocked_by",
7288
+ "worked_on",
7289
+ "uses",
7290
+ "decided",
7291
+ "created",
7292
+ "fixed",
7293
+ "reviewed",
7294
+ "rationale_for",
7295
+ // Business / organizational
7296
+ "supplies",
7297
+ "ordered_from",
7298
+ "reports_to",
7299
+ "manages",
7300
+ "located_at",
7301
+ "ships_to",
7302
+ "categorized_as",
7303
+ "priced_at",
7304
+ "partnered_with",
7305
+ "contracted_by",
7306
+ // General purpose
7307
+ "related_to",
7308
+ "part_of",
7309
+ "owns",
7310
+ "communicates_with"
7311
+ ]
7312
+ },
7313
+ confidence: { type: "number", description: "0.0-1.0. 1.0=explicitly stated, 0.6-0.9=inferred, 0.1-0.3=ambiguous" },
7314
+ confidence_label: { type: "string", enum: ["extracted", "inferred", "ambiguous"] }
7315
+ },
7316
+ required: ["source", "source_type", "target", "target_type", "relationship", "confidence", "confidence_label"]
7317
+ }
7318
+ },
7319
+ hyperedges: {
7320
+ type: "array",
7321
+ items: {
7322
+ type: "object",
7323
+ properties: {
7324
+ label: { type: "string", description: "Group label (e.g., 'auth flow files', 'v1.3 contributors')" },
7325
+ relation: { type: "string", description: "What connects them (e.g., 'part_of', 'contributed_to')" },
7326
+ entity_names: { type: "array", items: { type: "string" }, description: "3+ entity names in this group" },
7327
+ confidence: { type: "number" }
7328
+ },
7329
+ required: ["label", "relation", "entity_names", "confidence"]
7330
+ },
7331
+ description: "Group relationships connecting 3+ entities"
7332
+ }
7333
+ },
7334
+ required: ["entities", "relationships", "hyperedges"]
7335
+ }
7336
+ };
7337
+ READ_MEMORY_PATH_PATTERN = /^Read\s+(\S+\.(?:ts|tsx|js|jsx))\s*\n/;
6186
7338
  }
6187
7339
  });
6188
7340
 
6189
- // src/lib/wiki-client.ts
6190
- var wiki_client_exports = {};
6191
- __export(wiki_client_exports, {
6192
- chatInWorkspace: () => chatInWorkspace,
6193
- createWikiClient: () => createWikiClient,
6194
- getChatHistory: () => getChatHistory,
6195
- listDocuments: () => listDocuments,
6196
- listWorkspaces: () => listWorkspaces
7341
+ // src/lib/conversation-entity-extractor.ts
7342
+ var conversation_entity_extractor_exports = {};
7343
+ __export(conversation_entity_extractor_exports, {
7344
+ extractFromConversation: () => extractFromConversation
6197
7345
  });
6198
- async function wikiFetch(config2, path20, method = "GET", body) {
6199
- const url = `${config2.baseUrl}/api/v1${path20}`;
6200
- const headers = {
6201
- Authorization: `Bearer ${config2.apiKey}`,
6202
- "Content-Type": "application/json"
6203
- };
6204
- const controller = new AbortController();
6205
- const timeout = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS2);
7346
+ async function extractFromConversation(msg, agentResponse, agentName, model = "claude-haiku-4-5-20251001") {
7347
+ const text = msg.text ?? "";
7348
+ if (text.length < 20) {
7349
+ return { entities: [], relationships: [], hyperedges: [] };
7350
+ }
7351
+ const senderName = msg.senderName ?? msg.senderId;
7352
+ const prompt = [
7353
+ `Extract entities and relationships from this conversation message.`,
7354
+ `Platform: ${msg.platform} | Sender: ${senderName} | Account: ${msg.accountId ?? "unknown"}`,
7355
+ ``,
7356
+ `Focus on: people (from sender name, mentioned names), companies, products, topics discussed, locations mentioned.`,
7357
+ `Only extract what's clearly stated. For business conversations, capture: customer intent, product interest, service requests.`,
7358
+ ``,
7359
+ `MESSAGE:`,
7360
+ text,
7361
+ agentResponse ? `
7362
+ AGENT RESPONSE (${agentName ?? "unknown"}):
7363
+ ${agentResponse}` : ""
7364
+ ].join("\n");
6206
7365
  try {
6207
- let response;
6208
- try {
6209
- response = await fetch(url, {
6210
- method,
6211
- headers,
6212
- body: body ? JSON.stringify(body) : void 0,
6213
- signal: controller.signal
6214
- });
6215
- } catch {
6216
- clearTimeout(timeout);
6217
- const retryController = new AbortController();
6218
- const retryTimeout = setTimeout(() => retryController.abort(), REQUEST_TIMEOUT_MS2);
6219
- try {
6220
- await new Promise((r) => setTimeout(r, 500));
6221
- response = await fetch(url, {
6222
- method,
6223
- headers,
6224
- body: body ? JSON.stringify(body) : void 0,
6225
- signal: retryController.signal
7366
+ const Anthropic4 = (await import("@anthropic-ai/sdk")).default;
7367
+ const client = new Anthropic4();
7368
+ const response = await client.messages.create({
7369
+ model,
7370
+ max_tokens: 512,
7371
+ system: prompt,
7372
+ messages: [{ role: "user", content: "Extract entities and relationships from the message above." }],
7373
+ tools: [EXTRACT_TOOL],
7374
+ tool_choice: { type: "tool", name: "extract_entities_and_relationships" }
7375
+ });
7376
+ const toolBlock = response.content.find((b) => b.type === "tool_use");
7377
+ if (!toolBlock || toolBlock.type !== "tool_use") {
7378
+ return { entities: [], relationships: [], hyperedges: [] };
7379
+ }
7380
+ const input = toolBlock.input;
7381
+ const rawEntities = input.entities ?? [];
7382
+ const rawRels = input.relationships ?? [];
7383
+ const rawHyperedges = input.hyperedges ?? [];
7384
+ const entities = rawEntities.map((e) => ({ name: e.name ?? "", type: e.type ?? "concept" })).filter((e) => e.name.length > 0);
7385
+ const relationships = rawRels.map((r) => ({
7386
+ source: r.source ?? "",
7387
+ sourceType: r.source_type ?? "concept",
7388
+ target: r.target ?? "",
7389
+ targetType: r.target_type ?? "concept",
7390
+ relationship: r.relationship ?? "related_to",
7391
+ confidence: Number(r.confidence ?? 1),
7392
+ confidenceLabel: String(r.confidence_label ?? "extracted")
7393
+ })).filter((r) => r.source.length > 0 && r.target.length > 0);
7394
+ const hyperedges = rawHyperedges.map((h) => ({
7395
+ label: String(h.label ?? ""),
7396
+ relation: String(h.relation ?? ""),
7397
+ entityNames: h.entity_names ?? [],
7398
+ confidence: Number(h.confidence ?? 1)
7399
+ })).filter((h) => h.label.length > 0 && h.entityNames.length >= 3);
7400
+ const senderExists = entities.some(
7401
+ (e) => e.type === "person" && e.name.toLowerCase() === senderName.toLowerCase()
7402
+ );
7403
+ if (!senderExists) {
7404
+ entities.push({ name: senderName, type: "person" });
7405
+ }
7406
+ if (msg.accountId) {
7407
+ const hasCommunicates = relationships.some(
7408
+ (r) => r.relationship === "communicates_with" && r.source.toLowerCase() === senderName.toLowerCase()
7409
+ );
7410
+ if (!hasCommunicates) {
7411
+ relationships.push({
7412
+ source: senderName,
7413
+ sourceType: "person",
7414
+ target: msg.accountId,
7415
+ targetType: "person",
7416
+ relationship: "communicates_with",
7417
+ confidence: 1,
7418
+ confidenceLabel: "extracted"
6226
7419
  });
6227
- } finally {
6228
- clearTimeout(retryTimeout);
6229
7420
  }
6230
7421
  }
6231
- if (!response.ok) {
6232
- throw new Error(`Wiki API ${method} ${path20}: ${response.status} ${response.statusText}`);
7422
+ const provenance = {
7423
+ source: "gateway-conversation",
7424
+ platform: msg.platform,
7425
+ senderId: msg.senderId,
7426
+ senderName: msg.senderName,
7427
+ accountId: msg.accountId,
7428
+ messageId: msg.messageId,
7429
+ timestamp: msg.timestamp,
7430
+ agentName,
7431
+ extractionModel: model,
7432
+ extractedAt: (/* @__PURE__ */ new Date()).toISOString()
7433
+ };
7434
+ for (const entity of entities) {
7435
+ entity.properties = { ...entity.properties ?? {}, provenance };
6233
7436
  }
6234
- return response.json();
6235
- } finally {
6236
- clearTimeout(timeout);
7437
+ for (const rel of relationships) {
7438
+ rel.properties = { ...rel.properties ?? {}, provenance };
7439
+ }
7440
+ return { entities, relationships, hyperedges };
7441
+ } catch (err) {
7442
+ process.stderr.write(
7443
+ `[conversation-entity-extractor] Extraction failed: ${err instanceof Error ? err.message : String(err)}
7444
+ `
7445
+ );
7446
+ return { entities: [], relationships: [], hyperedges: [] };
6237
7447
  }
6238
7448
  }
6239
- async function resolveWikiUrl(configUrl) {
7449
+ var init_conversation_entity_extractor = __esm({
7450
+ "src/lib/conversation-entity-extractor.ts"() {
7451
+ "use strict";
7452
+ init_graph_rag();
7453
+ }
7454
+ });
7455
+
7456
+ // src/lib/conversation-wiki-populator.ts
7457
+ var conversation_wiki_populator_exports = {};
7458
+ __export(conversation_wiki_populator_exports, {
7459
+ populateWikiFromExtraction: () => populateWikiFromExtraction
7460
+ });
7461
+ async function populateWikiFromExtraction(extraction, msg, agentResponse, agentName) {
7462
+ const { createWikiClient: createWikiClient2, chatInWorkspace: chatInWorkspace2, listWorkspaces: listWorkspaces2 } = await Promise.resolve().then(() => (init_wiki_client(), wiki_client_exports));
7463
+ const client = await createWikiClient2();
7464
+ if (!client) {
7465
+ return { created: 0, updated: 0, skipped: extraction.entities.length };
7466
+ }
7467
+ let availableSlugs;
6240
7468
  try {
6241
- const controller = new AbortController();
6242
- const timeout = setTimeout(() => controller.abort(), 3e3);
6243
- const res = await fetch(`${LOCAL_WIKI_URL}/api/v1/auth`, { signal: controller.signal });
6244
- clearTimeout(timeout);
6245
- if (res.ok || res.status === 401 || res.status === 403) {
6246
- return LOCAL_WIKI_URL;
6247
- }
7469
+ const workspaces = await listWorkspaces2(client);
7470
+ availableSlugs = new Set(workspaces.map((w) => w.slug));
6248
7471
  } catch {
6249
- }
6250
- if (configUrl && configUrl !== LOCAL_WIKI_URL) {
7472
+ return { created: 0, updated: 0, skipped: extraction.entities.length };
7473
+ }
7474
+ let created = 0;
7475
+ let updated = 0;
7476
+ let skipped = 0;
7477
+ let apiCalls = 0;
7478
+ const candidates = extraction.entities.filter((e) => {
7479
+ if (!WIKI_ENTITY_TYPES.has(e.type)) {
7480
+ skipped++;
7481
+ return false;
7482
+ }
7483
+ if (e.name.length < MIN_NAME_LENGTH) {
7484
+ skipped++;
7485
+ return false;
7486
+ }
7487
+ if (SKIP_NAMES.has(e.name.toLowerCase())) {
7488
+ skipped++;
7489
+ return false;
7490
+ }
7491
+ return true;
7492
+ });
7493
+ const senderName = msg.senderName ?? msg.senderId;
7494
+ const timestamp = msg.timestamp;
7495
+ const platform = msg.platform;
7496
+ const accountId = msg.accountId ?? "unknown";
7497
+ const messageSummary = (msg.text ?? "").slice(0, 200);
7498
+ const responseSummary = agentResponse ? agentResponse.slice(0, 200) : void 0;
7499
+ for (const entity of candidates) {
7500
+ if (apiCalls >= MAX_WIKI_CALLS) {
7501
+ skipped += candidates.length - (created + updated + (skipped - (extraction.entities.length - candidates.length)));
7502
+ break;
7503
+ }
7504
+ const workspace = WORKSPACE_MAP[entity.type] ?? DEFAULT_WORKSPACE;
7505
+ if (!availableSlugs.has(workspace)) {
7506
+ skipped++;
7507
+ continue;
7508
+ }
6251
7509
  try {
6252
- const controller = new AbortController();
6253
- const timeout = setTimeout(() => controller.abort(), 3e3);
6254
- const res = await fetch(`${configUrl}/api/v1/auth`, { signal: controller.signal });
6255
- clearTimeout(timeout);
6256
- if (res.ok || res.status === 401 || res.status === 403) {
6257
- return configUrl;
6258
- }
6259
- } catch {
7510
+ const content = buildWikiContent(
7511
+ entity.name,
7512
+ entity.type,
7513
+ timestamp,
7514
+ platform,
7515
+ accountId,
7516
+ senderName,
7517
+ messageSummary,
7518
+ agentName,
7519
+ responseSummary
7520
+ );
7521
+ await chatInWorkspace2(client, workspace, content, "chat");
7522
+ apiCalls++;
7523
+ updated++;
7524
+ } catch (err) {
7525
+ process.stderr.write(
7526
+ `[wiki-populator] Failed for entity "${entity.name}": ${err instanceof Error ? err.message : String(err)}
7527
+ `
7528
+ );
7529
+ skipped++;
6260
7530
  }
6261
7531
  }
6262
- return null;
6263
- }
6264
- async function createWikiClient() {
6265
- const { loadConfig: loadConfig2 } = await Promise.resolve().then(() => (init_config(), config_exports));
6266
- const config2 = await loadConfig2();
6267
- const baseUrl = await resolveWikiUrl(config2.wikiUrl);
6268
- if (!baseUrl) return null;
6269
- return {
6270
- baseUrl,
6271
- apiKey: config2.wikiApiKey
6272
- };
6273
- }
6274
- async function listWorkspaces(client) {
6275
- const data = await wikiFetch(client, "/workspaces");
6276
- return (data.workspaces ?? []).map((w) => ({
6277
- slug: w.slug,
6278
- name: w.name,
6279
- createdAt: w.createdAt ?? "",
6280
- threads: w.threads ?? []
6281
- }));
6282
- }
6283
- async function listDocuments(client, workspaceSlug) {
6284
- const data = await wikiFetch(
6285
- client,
6286
- `/workspace/${encodeURIComponent(workspaceSlug)}`
6287
- );
6288
- const docs = Array.isArray(data.workspace) ? data.workspace[0]?.documents ?? [] : [];
6289
- return docs.map((d) => ({
6290
- filename: d.filename ?? "untitled",
6291
- docpath: d.docpath,
6292
- workspace: workspaceSlug,
6293
- pinned: d.pinned ?? false
6294
- }));
6295
- }
6296
- async function chatInWorkspace(client, workspaceSlug, message, mode = "chat") {
6297
- const data = await wikiFetch(
6298
- client,
6299
- `/workspace/${encodeURIComponent(workspaceSlug)}/chat`,
6300
- "POST",
6301
- { message, mode }
6302
- );
6303
- return {
6304
- id: data.id ?? "",
6305
- type: data.type ?? "textResponse",
6306
- textResponse: data.textResponse ?? "",
6307
- sources: data.sources ?? [],
6308
- error: data.error ?? null
6309
- };
7532
+ return { created, updated, skipped };
6310
7533
  }
6311
- async function getChatHistory(client, workspaceSlug, limit = 50) {
6312
- const data = await wikiFetch(
6313
- client,
6314
- `/workspace/${encodeURIComponent(workspaceSlug)}/chats?limit=${limit}&orderBy=desc`
7534
+ function buildWikiContent(entityName, entityType, timestamp, platform, accountId, senderName, messageSummary, agentName, responseSummary) {
7535
+ const lines = [
7536
+ `Update knowledge about ${entityName} (${entityType}):`,
7537
+ ``,
7538
+ `## [${timestamp}] via ${platform}/${accountId}`,
7539
+ `${senderName}: ${messageSummary}`
7540
+ ];
7541
+ if (agentName && responseSummary) {
7542
+ lines.push(`Agent (${agentName}): ${responseSummary}`);
7543
+ }
7544
+ lines.push(
7545
+ ``,
7546
+ `Source: ${platform} | Account: ${accountId} | Timestamp: ${timestamp}`
6315
7547
  );
6316
- return (data.history ?? []).reverse().map((h) => ({
6317
- role: h.role === "user" ? "user" : "assistant",
6318
- content: h.content,
6319
- sentAt: h.sentAt ?? 0
6320
- }));
7548
+ return lines.join("\n");
6321
7549
  }
6322
- var LOCAL_WIKI_URL, REQUEST_TIMEOUT_MS2;
6323
- var init_wiki_client = __esm({
6324
- "src/lib/wiki-client.ts"() {
7550
+ var MAX_WIKI_CALLS, WIKI_ENTITY_TYPES, WORKSPACE_MAP, DEFAULT_WORKSPACE, MIN_NAME_LENGTH, SKIP_NAMES;
7551
+ var init_conversation_wiki_populator = __esm({
7552
+ "src/lib/conversation-wiki-populator.ts"() {
6325
7553
  "use strict";
6326
- LOCAL_WIKI_URL = "http://localhost:3001";
6327
- REQUEST_TIMEOUT_MS2 = 8e3;
7554
+ MAX_WIKI_CALLS = 3;
7555
+ WIKI_ENTITY_TYPES = /* @__PURE__ */ new Set(["person", "company", "product"]);
7556
+ WORKSPACE_MAP = {
7557
+ person: "contacts",
7558
+ company: "companies",
7559
+ product: "products"
7560
+ };
7561
+ DEFAULT_WORKSPACE = "conversations";
7562
+ MIN_NAME_LENGTH = 3;
7563
+ SKIP_NAMES = /* @__PURE__ */ new Set(["unknown", "none", "n/a", "null", "undefined"]);
6328
7564
  }
6329
7565
  });
6330
7566
 
@@ -6399,10 +7635,10 @@ __export(messaging_exports, {
6399
7635
  sendMessage: () => sendMessage,
6400
7636
  setWsClientSend: () => setWsClientSend
6401
7637
  });
6402
- import crypto8 from "crypto";
7638
+ import crypto9 from "crypto";
6403
7639
  function generateUlid() {
6404
7640
  const timestamp = Date.now().toString(36).padStart(10, "0");
6405
- const random = crypto8.randomBytes(10).toString("hex").slice(0, 16);
7641
+ const random = crypto9.randomBytes(10).toString("hex").slice(0, 16);
6406
7642
  return (timestamp + random).toUpperCase();
6407
7643
  }
6408
7644
  function rowToMessage(row) {
@@ -6506,7 +7742,7 @@ async function deliverLocalMessage(messageId) {
6506
7742
  try {
6507
7743
  const exeSession = resolveExeSession();
6508
7744
  if (!exeSession) {
6509
- throw new Error("No exe session found");
7745
+ throw new Error("No coordinator session found");
6510
7746
  }
6511
7747
  const sessionName = employeeSessionName(targetAgent, exeSession);
6512
7748
  if (!isEmployeeAlive(sessionName)) {
@@ -7859,7 +9095,11 @@ function composeHooks(...pipelines) {
7859
9095
  };
7860
9096
  }
7861
9097
 
9098
+ // src/runtime/orchestrator.ts
9099
+ init_employees();
9100
+
7862
9101
  // src/lib/task-router.ts
9102
+ init_employees();
7863
9103
  import { randomUUID } from "crypto";
7864
9104
  var DEFAULT_BLOOM_CONFIG = {
7865
9105
  complexityToTier: {
@@ -7917,7 +9157,7 @@ async function scoreEmployee(taskVector, agentId, searchFn) {
7917
9157
  return { agentId, score: results.length / 5 };
7918
9158
  }
7919
9159
  async function routeTask(taskDescription, employees, embedFn, searchFn, options) {
7920
- let specialists = employees.filter((e) => e.name !== "exe");
9160
+ let specialists = employees.filter((e) => !isCoordinatorRole(e.role));
7921
9161
  if (specialists.length === 0) {
7922
9162
  throw new Error(
7923
9163
  "No specialist employees available. Create one with /exe-new-employee."
@@ -7994,7 +9234,7 @@ ${task.context}`,
7994
9234
  await createTaskCore({
7995
9235
  title: task.title,
7996
9236
  assignedTo: targetEmployee.name,
7997
- assignedBy: "exe",
9237
+ assignedBy: getCoordinatorName(),
7998
9238
  projectName: task.projectName,
7999
9239
  priority: task.priority,
8000
9240
  context: task.context,
@@ -8099,15 +9339,16 @@ ${task.context}`,
8099
9339
  const client = getClient2();
8100
9340
  const autoApproved = [];
8101
9341
  const needsReview = [];
9342
+ const coordinatorName = getCoordinatorName();
8102
9343
  const rScope = sessionScopeFilter();
8103
9344
  const reviews = await client.execute({
8104
9345
  sql: `SELECT id, title, assigned_to, priority, result, task_file FROM tasks
8105
- WHERE assigned_to = 'exe'
9346
+ WHERE (assigned_to = ? OR assigned_to = 'exe')
8106
9347
  AND status IN ('open', 'in_progress')
8107
9348
  AND task_file LIKE '%review-%'${rScope.sql}
8108
9349
  ORDER BY priority ASC
8109
9350
  LIMIT 20`,
8110
- args: [...rScope.args]
9351
+ args: [coordinatorName, ...rScope.args]
8111
9352
  });
8112
9353
  for (const row of reviews.rows) {
8113
9354
  const item = {
@@ -9088,11 +10329,11 @@ init_crm_bridge();
9088
10329
 
9089
10330
  // src/lib/pipeline-router.ts
9090
10331
  init_database();
9091
- import crypto7 from "crypto";
10332
+ import crypto8 from "crypto";
9092
10333
  async function sinkConversationStore(msg, agentResponse, agentName) {
9093
10334
  try {
9094
10335
  const client = getClient();
9095
- const id = crypto7.randomUUID();
10336
+ const id = crypto8.randomUUID();
9096
10337
  const mediaJson = msg.media ? JSON.stringify(msg.media) : null;
9097
10338
  await client.execute({
9098
10339
  sql: `INSERT INTO conversations
@@ -9142,7 +10383,7 @@ async function sinkMemory(msg, agentResponse, agentName) {
9142
10383
  ].filter(Boolean).join("\n");
9143
10384
  const vector = await embed2(rawText);
9144
10385
  await writeMemory2({
9145
- id: crypto7.randomUUID(),
10386
+ id: crypto8.randomUUID(),
9146
10387
  agent_id: agentName ?? "gateway",
9147
10388
  agent_role: "gateway",
9148
10389
  session_id: `gateway-${msg.platform}`,
@@ -9223,9 +10464,37 @@ Agent response: ${agentResponse}` : ""
9223
10464
  return false;
9224
10465
  }
9225
10466
  }
10467
+ async function sinkEntityExtraction(msg, agentResponse, agentName) {
10468
+ try {
10469
+ const { extractFromConversation: extractFromConversation2 } = await Promise.resolve().then(() => (init_conversation_entity_extractor(), conversation_entity_extractor_exports));
10470
+ const { storeExtraction: storeExtraction2 } = await Promise.resolve().then(() => (init_graph_rag(), graph_rag_exports));
10471
+ if ((msg.text?.length ?? 0) < 20) return false;
10472
+ if (msg.isHistorical) return false;
10473
+ const extraction = await extractFromConversation2(msg, agentResponse, agentName);
10474
+ if (extraction.entities.length === 0) return false;
10475
+ const client = getClient();
10476
+ const memoryId = `conv:${msg.messageId}`;
10477
+ await storeExtraction2(client, extraction, memoryId, msg.timestamp);
10478
+ Promise.resolve().then(() => (init_conversation_wiki_populator(), conversation_wiki_populator_exports)).then(
10479
+ ({ populateWikiFromExtraction: populateWikiFromExtraction2 }) => populateWikiFromExtraction2(extraction, msg, agentResponse, agentName)
10480
+ ).catch((err) => {
10481
+ process.stderr.write(
10482
+ `[pipeline] wiki-population error: ${err instanceof Error ? err.message : String(err)}
10483
+ `
10484
+ );
10485
+ });
10486
+ return true;
10487
+ } catch (err) {
10488
+ process.stderr.write(
10489
+ `[pipeline] entity-extraction-sink error: ${err instanceof Error ? err.message : String(err)}
10490
+ `
10491
+ );
10492
+ return false;
10493
+ }
10494
+ }
9226
10495
  async function ingest(msg, agentResponse, agentName) {
9227
10496
  const errors = [];
9228
- const [conversationStored, memorySunk, crmSunk, wikiSunk] = await Promise.all([
10497
+ const [conversationStored, memorySunk, crmSunk, wikiSunk, entityExtracted] = await Promise.all([
9229
10498
  sinkConversationStore(msg, agentResponse, agentName).catch((e) => {
9230
10499
  errors.push(`conversation: ${e}`);
9231
10500
  return false;
@@ -9241,6 +10510,10 @@ async function ingest(msg, agentResponse, agentName) {
9241
10510
  sinkWiki(msg, agentResponse).catch((e) => {
9242
10511
  errors.push(`wiki: ${e}`);
9243
10512
  return false;
10513
+ }),
10514
+ sinkEntityExtraction(msg, agentResponse, agentName).catch((e) => {
10515
+ errors.push(`entity-extraction: ${e}`);
10516
+ return false;
9244
10517
  })
9245
10518
  ]);
9246
10519
  if (errors.length > 0) {
@@ -9249,7 +10522,7 @@ async function ingest(msg, agentResponse, agentName) {
9249
10522
  `
9250
10523
  );
9251
10524
  }
9252
- return { conversationStored, memorySunk, crmSunk, wikiSunk, errors };
10525
+ return { conversationStored, memorySunk, crmSunk, wikiSunk, entityExtracted, errors };
9253
10526
  }
9254
10527
 
9255
10528
  // src/gateway/gateway.ts
@@ -9535,7 +10808,7 @@ Your personality:
9535
10808
  - If you don't know something, say so and suggest who might
9536
10809
 
9537
10810
  Your tools:
9538
- - ask_team_memory: Query any employee's memories (yoshi, tom, mari, exe)
10811
+ - ask_team_memory: Query any employee's memories by their configured name
9539
10812
  - recall_my_memory: Search your own past conversations with the founder
9540
10813
  - list_tasks: See what's in progress across the org
9541
10814
  - get_session_context: Get context from a specific session
@@ -9676,7 +10949,7 @@ function buildExecAssistantTools() {
9676
10949
  properties: {
9677
10950
  team_member: {
9678
10951
  type: "string",
9679
- description: "Name of the team member (e.g., 'yoshi', 'mari', 'exe')"
10952
+ description: "Configured name of the team member"
9680
10953
  },
9681
10954
  query: { type: "string", description: "What to search for" },
9682
10955
  project_name: {
@@ -9855,7 +11128,7 @@ function buildExecAssistantTools() {
9855
11128
  },
9856
11129
  {
9857
11130
  name: "close_task",
9858
- description: "Reviewer-only: finalize a task after review. Only exe/ea can use this.",
11131
+ description: "Reviewer-only: finalize a task after review. Only coordinators/reviewers can use this.",
9859
11132
  input_schema: {
9860
11133
  type: "object",
9861
11134
  properties: {
@@ -10926,7 +12199,7 @@ ${val}` : val;
10926
12199
  }
10927
12200
  if (envelope.receiptMessage) {
10928
12201
  const rcpt = envelope.receiptMessage;
10929
- for (const ts of rcpt.timestamps) {
12202
+ for (const ts2 of rcpt.timestamps) {
10930
12203
  const normalized2 = {
10931
12204
  messageId: randomUUID10(),
10932
12205
  platform: "signal",
@@ -10941,7 +12214,7 @@ ${val}` : val;
10941
12214
  raw: payload,
10942
12215
  dataCategory: "read_receipt",
10943
12216
  readReceipt: {
10944
- messageId: String(ts),
12217
+ messageId: String(ts2),
10945
12218
  status: rcpt.type === "read" ? "read" : "delivered",
10946
12219
  timestamp: new Date(
10947
12220
  envelope.timestamp ?? Date.now()
@@ -12602,7 +13875,9 @@ async function executeSendWhatsapp(params) {
12602
13875
  }
12603
13876
  async function executeSendMessage(params) {
12604
13877
  const { sendMessage: sendMessage2 } = await Promise.resolve().then(() => (init_messaging(), messaging_exports));
12605
- const to = params.to ?? params.recipient;
13878
+ const { getCoordinatorName: getCoordinatorName2 } = await Promise.resolve().then(() => (init_employees(), employees_exports));
13879
+ const rawTo = params.to ?? params.recipient;
13880
+ const to = rawTo === "coordinator" ? getCoordinatorName2() : rawTo;
12606
13881
  const content = params.message ?? params.content ?? params.text;
12607
13882
  if (!to || !content)
12608
13883
  throw new Error("send_message requires 'to' and 'message' params");
@@ -12615,9 +13890,11 @@ async function executeSendMessage(params) {
12615
13890
  }
12616
13891
  async function executeCreateTask(params) {
12617
13892
  const { createTask: createTask2 } = await Promise.resolve().then(() => (init_tasks(), tasks_exports));
13893
+ const { getCoordinatorName: getCoordinatorName2 } = await Promise.resolve().then(() => (init_employees(), employees_exports));
13894
+ const rawAssignedTo = params.assigned_to;
12618
13895
  await createTask2({
12619
13896
  title: params.title ?? "Triggered task",
12620
- assignedTo: params.assigned_to ?? "exe",
13897
+ assignedTo: !rawAssignedTo || rawAssignedTo === "coordinator" ? getCoordinatorName2() : rawAssignedTo,
12621
13898
  assignedBy: "trigger-engine",
12622
13899
  projectName: params.project ?? "exe-os",
12623
13900
  priority: params.priority ?? "p1",
@@ -12693,10 +13970,11 @@ ${content}` : existingContent + "\n\n" + content,
12693
13970
  }
12694
13971
  async function routeToApproval(action, resolvedParams, triggerName) {
12695
13972
  const { createTask: createTask2 } = await Promise.resolve().then(() => (init_tasks(), tasks_exports));
13973
+ const { getCoordinatorName: getCoordinatorName2 } = await Promise.resolve().then(() => (init_employees(), employees_exports));
12696
13974
  const actionSummary = action.type === "send_whatsapp" ? `Send WhatsApp to ${resolvedParams.to ?? resolvedParams.recipient ?? "unknown"}: "${(resolvedParams.message ?? resolvedParams.text ?? "").slice(0, 100)}"` : `${action.type}: ${JSON.stringify(resolvedParams).slice(0, 200)}`;
12697
13975
  await createTask2({
12698
13976
  title: `[Approval Required] ${triggerName}: ${action.type}`,
12699
- assignedTo: "exe",
13977
+ assignedTo: getCoordinatorName2(),
12700
13978
  assignedBy: "trigger-engine",
12701
13979
  projectName: resolvedParams.project ?? "exe-os",
12702
13980
  priority: "p1",