@askexenow/exe-os 0.8.38 → 0.8.39

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 (91) hide show
  1. package/README.md +17 -8
  2. package/dist/bin/backfill-conversations.js +46 -10
  3. package/dist/bin/backfill-responses.js +46 -10
  4. package/dist/bin/backfill-vectors.js +42 -8
  5. package/dist/bin/cleanup-stale-review-tasks.js +37 -8
  6. package/dist/bin/cli.js +281 -154
  7. package/dist/bin/exe-agent.js +19 -4
  8. package/dist/bin/exe-assign.js +39 -5
  9. package/dist/bin/exe-boot.js +237 -111
  10. package/dist/bin/exe-call.js +11 -6
  11. package/dist/bin/exe-cloud.js +99 -28
  12. package/dist/bin/exe-dispatch.js +1 -1
  13. package/dist/bin/exe-doctor.js +37 -8
  14. package/dist/bin/exe-export-behaviors.js +39 -10
  15. package/dist/bin/exe-forget.js +38 -9
  16. package/dist/bin/exe-gateway.js +109 -42
  17. package/dist/bin/exe-heartbeat.js +49 -20
  18. package/dist/bin/exe-kill.js +39 -10
  19. package/dist/bin/exe-launch-agent.js +58 -22
  20. package/dist/bin/exe-link.js +184 -85
  21. package/dist/bin/exe-new-employee.js +21 -7
  22. package/dist/bin/exe-pending-messages.js +46 -17
  23. package/dist/bin/exe-pending-notifications.js +37 -8
  24. package/dist/bin/exe-pending-reviews.js +47 -18
  25. package/dist/bin/exe-rename.js +21 -7
  26. package/dist/bin/exe-review.js +34 -5
  27. package/dist/bin/exe-search.js +47 -10
  28. package/dist/bin/exe-session-cleanup.js +56 -19
  29. package/dist/bin/exe-settings.js +63 -2
  30. package/dist/bin/exe-status.js +34 -5
  31. package/dist/bin/exe-team.js +34 -5
  32. package/dist/bin/git-sweep.js +38 -9
  33. package/dist/bin/graph-backfill.js +37 -8
  34. package/dist/bin/graph-export.js +37 -8
  35. package/dist/bin/install.js +1 -1
  36. package/dist/bin/scan-tasks.js +40 -11
  37. package/dist/bin/setup.js +58 -24
  38. package/dist/bin/shard-migrate.js +37 -8
  39. package/dist/bin/wiki-sync.js +39 -9
  40. package/dist/gateway/index.js +102 -37
  41. package/dist/hooks/bug-report-worker.js +62 -28
  42. package/dist/hooks/commit-complete.js +38 -9
  43. package/dist/hooks/error-recall.js +49 -8
  44. package/dist/hooks/exe-heartbeat-hook.js +3 -2
  45. package/dist/hooks/ingest-worker.js +151 -37
  46. package/dist/hooks/ingest.js +74 -28
  47. package/dist/hooks/instructions-loaded.js +39 -9
  48. package/dist/hooks/notification.js +37 -7
  49. package/dist/hooks/post-compact.js +37 -7
  50. package/dist/hooks/pre-compact.js +35 -6
  51. package/dist/hooks/pre-tool-use.js +52 -14
  52. package/dist/hooks/prompt-ingest-worker.js +56 -10
  53. package/dist/hooks/prompt-submit.js +61 -23
  54. package/dist/hooks/response-ingest-worker.js +57 -11
  55. package/dist/hooks/session-end.js +43 -10
  56. package/dist/hooks/session-start.js +46 -8
  57. package/dist/hooks/stop.js +37 -7
  58. package/dist/hooks/subagent-stop.js +37 -7
  59. package/dist/hooks/summary-worker.js +317 -99
  60. package/dist/index.js +87 -22
  61. package/dist/lib/cloud-sync.js +172 -78
  62. package/dist/lib/config.js +4 -1
  63. package/dist/lib/consolidation.js +5 -4
  64. package/dist/lib/database.js +1 -0
  65. package/dist/lib/device-registry.js +2 -1
  66. package/dist/lib/embedder.js +9 -1
  67. package/dist/lib/employees.js +11 -6
  68. package/dist/lib/exe-daemon-client.js +6 -1
  69. package/dist/lib/exe-daemon.js +71 -28
  70. package/dist/lib/hybrid-search.js +47 -10
  71. package/dist/lib/identity.js +1 -1
  72. package/dist/lib/keychain.js +2 -1
  73. package/dist/lib/license.js +13 -4
  74. package/dist/lib/messaging.js +1 -1
  75. package/dist/lib/reminders.js +2 -2
  76. package/dist/lib/schedules.js +37 -8
  77. package/dist/lib/skill-learning.js +1 -1
  78. package/dist/lib/store.js +37 -8
  79. package/dist/lib/tasks.js +1 -1
  80. package/dist/lib/tmux-routing.js +1 -1
  81. package/dist/mcp/server.js +97 -43
  82. package/dist/mcp/tools/complete-reminder.js +1 -1
  83. package/dist/mcp/tools/create-task.js +14 -6
  84. package/dist/mcp/tools/deactivate-behavior.js +2 -2
  85. package/dist/mcp/tools/list-reminders.js +1 -1
  86. package/dist/mcp/tools/list-tasks.js +1 -1
  87. package/dist/mcp/tools/send-message.js +1 -1
  88. package/dist/mcp/tools/update-task.js +1 -1
  89. package/dist/runtime/index.js +35 -6
  90. package/dist/tui/App.js +177 -95
  91. package/package.json +3 -3
@@ -79,6 +79,17 @@ var init_db_retry = __esm({
79
79
  });
80
80
 
81
81
  // src/lib/database.ts
82
+ var database_exports = {};
83
+ __export(database_exports, {
84
+ disposeDatabase: () => disposeDatabase,
85
+ disposeTurso: () => disposeTurso,
86
+ ensureSchema: () => ensureSchema,
87
+ getClient: () => getClient,
88
+ getRawClient: () => getRawClient,
89
+ initDatabase: () => initDatabase,
90
+ initTurso: () => initTurso,
91
+ isInitialized: () => isInitialized
92
+ });
82
93
  import { createClient } from "@libsql/client";
83
94
  async function initDatabase(config) {
84
95
  if (_client) {
@@ -114,6 +125,7 @@ async function ensureSchema() {
114
125
  const client = getRawClient();
115
126
  await client.execute("PRAGMA journal_mode = WAL");
116
127
  await client.execute("PRAGMA busy_timeout = 30000");
128
+ await client.execute("PRAGMA wal_autocheckpoint = 1000");
117
129
  try {
118
130
  await client.execute("PRAGMA libsql_vector_search_ef = 128");
119
131
  } catch {
@@ -902,7 +914,14 @@ async function ensureSchema() {
902
914
  }
903
915
  }
904
916
  }
905
- var _client, _resilientClient, initTurso;
917
+ async function disposeDatabase() {
918
+ if (_client) {
919
+ _client.close();
920
+ _client = null;
921
+ _resilientClient = null;
922
+ }
923
+ }
924
+ var _client, _resilientClient, initTurso, disposeTurso;
906
925
  var init_database = __esm({
907
926
  "src/lib/database.ts"() {
908
927
  "use strict";
@@ -910,6 +929,7 @@ var init_database = __esm({
910
929
  _client = null;
911
930
  _resilientClient = null;
912
931
  initTurso = initDatabase;
932
+ disposeTurso = disposeDatabase;
913
933
  }
914
934
  });
915
935
 
@@ -925,9 +945,10 @@ __export(keychain_exports, {
925
945
  import { readFile, writeFile, unlink, mkdir, chmod } from "fs/promises";
926
946
  import { existsSync } from "fs";
927
947
  import path from "path";
948
+ import os from "os";
928
949
  import crypto from "crypto";
929
950
  function getKeyDir() {
930
- return process.env.EXE_OS_DIR ?? process.env.EXE_MEM_DIR ?? path.join(process.env.HOME ?? "/tmp", ".exe-os");
951
+ return process.env.EXE_OS_DIR ?? process.env.EXE_MEM_DIR ?? path.join(os.homedir(), ".exe-os");
931
952
  }
932
953
  function getKeyPath() {
933
954
  return path.join(getKeyDir(), "master.key");
@@ -1075,15 +1096,15 @@ __export(config_exports, {
1075
1096
  migrateConfig: () => migrateConfig,
1076
1097
  saveConfig: () => saveConfig
1077
1098
  });
1078
- import { readFile as readFile2, writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
1099
+ import { readFile as readFile2, writeFile as writeFile2, mkdir as mkdir2, chmod as chmod2 } from "fs/promises";
1079
1100
  import { readFileSync, existsSync as existsSync2, renameSync } from "fs";
1080
1101
  import path2 from "path";
1081
- import os from "os";
1102
+ import os2 from "os";
1082
1103
  function resolveDataDir() {
1083
1104
  if (process.env.EXE_OS_DIR) return process.env.EXE_OS_DIR;
1084
1105
  if (process.env.EXE_MEM_DIR) return process.env.EXE_MEM_DIR;
1085
- const newDir = path2.join(os.homedir(), ".exe-os");
1086
- const legacyDir = path2.join(os.homedir(), ".exe-mem");
1106
+ const newDir = path2.join(os2.homedir(), ".exe-os");
1107
+ const legacyDir = path2.join(os2.homedir(), ".exe-mem");
1087
1108
  if (!existsSync2(newDir) && existsSync2(legacyDir)) {
1088
1109
  try {
1089
1110
  renameSync(legacyDir, newDir);
@@ -1170,7 +1191,7 @@ async function loadConfig() {
1170
1191
  normalizeAutoUpdate(migratedCfg);
1171
1192
  const config = { ...DEFAULT_CONFIG, dbPath: path2.join(dir, "memories.db"), ...migratedCfg };
1172
1193
  if (config.dbPath.startsWith("~")) {
1173
- config.dbPath = config.dbPath.replace(/^~/, os.homedir());
1194
+ config.dbPath = config.dbPath.replace(/^~/, os2.homedir());
1174
1195
  }
1175
1196
  return config;
1176
1197
  } catch {
@@ -1201,6 +1222,9 @@ async function saveConfig(config) {
1201
1222
  await mkdir2(dir, { recursive: true });
1202
1223
  const configPath = path2.join(dir, "config.json");
1203
1224
  await writeFile2(configPath, JSON.stringify(config, null, 2) + "\n");
1225
+ if (config.cloud?.apiKey) {
1226
+ await chmod2(configPath, 384);
1227
+ }
1204
1228
  }
1205
1229
  async function loadConfigFrom(configPath) {
1206
1230
  const raw = await readFile2(configPath, "utf-8");
@@ -1563,15 +1587,20 @@ async function saveEmployees(employees, employeesPath = EMPLOYEES_PATH) {
1563
1587
  await mkdir3(path5.dirname(employeesPath), { recursive: true });
1564
1588
  await writeFile3(employeesPath, JSON.stringify(employees, null, 2) + "\n", "utf-8");
1565
1589
  }
1590
+ function findExeBin() {
1591
+ try {
1592
+ return execSync(process.platform === "win32" ? "where exe-os" : "which exe-os", { encoding: "utf8" }).trim();
1593
+ } catch {
1594
+ return null;
1595
+ }
1596
+ }
1566
1597
  function registerBinSymlinks(name) {
1567
1598
  const created = [];
1568
1599
  const skipped = [];
1569
1600
  const errors = [];
1570
- let exeBinPath;
1571
- try {
1572
- exeBinPath = execSync("which exe", { encoding: "utf-8" }).trim();
1573
- } catch {
1574
- errors.push("Could not find 'exe' in PATH");
1601
+ const exeBinPath = findExeBin();
1602
+ if (!exeBinPath) {
1603
+ errors.push("Could not find 'exe-os' in PATH");
1575
1604
  return { created, skipped, errors };
1576
1605
  }
1577
1606
  const binDir = path5.dirname(exeBinPath);
@@ -1612,6 +1641,14 @@ import { readFileSync as readFileSync4, writeFileSync, existsSync as existsSync6
1612
1641
  import { randomUUID } from "crypto";
1613
1642
  import path6 from "path";
1614
1643
  import { jwtVerify, importSPKI } from "jose";
1644
+ async function fetchRetry(url, init) {
1645
+ try {
1646
+ return await fetch(url, init);
1647
+ } catch {
1648
+ await new Promise((r) => setTimeout(r, RETRY_DELAY_MS));
1649
+ return fetch(url, { ...init, signal: AbortSignal.timeout(1e4) });
1650
+ }
1651
+ }
1615
1652
  function loadDeviceId() {
1616
1653
  const deviceJsonPath = path6.join(EXE_AI_DIR, "device.json");
1617
1654
  try {
@@ -1682,7 +1719,7 @@ function cacheResponse(token) {
1682
1719
  async function validateLicense(apiKey, deviceId) {
1683
1720
  const did = deviceId ?? loadDeviceId();
1684
1721
  try {
1685
- const res = await fetch(`${API_BASE}/auth/activate`, {
1722
+ const res = await fetchRetry(`${API_BASE}/auth/activate`, {
1686
1723
  method: "POST",
1687
1724
  headers: { "Content-Type": "application/json" },
1688
1725
  body: JSON.stringify({ apiKey, deviceId: did }),
@@ -1747,7 +1784,7 @@ function isFeatureAllowed(license, feature) {
1747
1784
  return license.plan === "team" || license.plan === "agency" || license.plan === "enterprise";
1748
1785
  }
1749
1786
  }
1750
- var LICENSE_PATH, CACHE_PATH, DEVICE_ID_PATH, API_BASE, LICENSE_PUBLIC_KEY_PEM, LICENSE_JWT_ALG, PLAN_LIMITS, FREE_LICENSE, CACHE_MAX_AGE_MS;
1787
+ var LICENSE_PATH, CACHE_PATH, DEVICE_ID_PATH, API_BASE, RETRY_DELAY_MS, LICENSE_PUBLIC_KEY_PEM, LICENSE_JWT_ALG, PLAN_LIMITS, FREE_LICENSE, CACHE_MAX_AGE_MS;
1751
1788
  var init_license = __esm({
1752
1789
  "src/lib/license.ts"() {
1753
1790
  "use strict";
@@ -1756,6 +1793,7 @@ var init_license = __esm({
1756
1793
  CACHE_PATH = path6.join(EXE_AI_DIR, "license-cache.json");
1757
1794
  DEVICE_ID_PATH = path6.join(EXE_AI_DIR, "device-id");
1758
1795
  API_BASE = "https://askexe.com/cloud";
1796
+ RETRY_DELAY_MS = 500;
1759
1797
  LICENSE_PUBLIC_KEY_PEM = `-----BEGIN PUBLIC KEY-----
1760
1798
  MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEeHztAMOpR/ZMh+rWuOASjEZ54CGY
1761
1799
  4uj+UqeKCcvtgNHKmOK278HJaJcANe9xAeji8AFYu27q3WtzCi04pHudow==
@@ -1915,6 +1953,10 @@ import path8 from "path";
1915
1953
  import { fileURLToPath } from "url";
1916
1954
  function handleData(chunk) {
1917
1955
  _buffer += chunk.toString();
1956
+ if (_buffer.length > MAX_BUFFER) {
1957
+ _buffer = "";
1958
+ return;
1959
+ }
1918
1960
  let newlineIdx;
1919
1961
  while ((newlineIdx = _buffer.indexOf("\n")) !== -1) {
1920
1962
  const line = _buffer.slice(0, newlineIdx).trim();
@@ -2222,7 +2264,7 @@ function disconnectClient() {
2222
2264
  entry.resolve({ error: "Client disconnected" });
2223
2265
  }
2224
2266
  }
2225
- 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;
2267
+ 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;
2226
2268
  var init_exe_daemon_client = __esm({
2227
2269
  "src/lib/exe-daemon-client.ts"() {
2228
2270
  "use strict";
@@ -2239,6 +2281,7 @@ var init_exe_daemon_client = __esm({
2239
2281
  _requestCount = 0;
2240
2282
  HEALTH_CHECK_INTERVAL = 100;
2241
2283
  _pending = /* @__PURE__ */ new Map();
2284
+ MAX_BUFFER = 1e7;
2242
2285
  }
2243
2286
  });
2244
2287
 
@@ -2280,8 +2323,8 @@ async function embedDirect(text) {
2280
2323
  const llamaCpp = await import("node-llama-cpp");
2281
2324
  const { MODELS_DIR: MODELS_DIR2 } = await Promise.resolve().then(() => (init_config(), config_exports));
2282
2325
  const { existsSync: existsSync11 } = await import("fs");
2283
- const path11 = await import("path");
2284
- const modelPath = path11.join(MODELS_DIR2, "jina-embeddings-v5-small-q4_k_m.gguf");
2326
+ const path12 = await import("path");
2327
+ const modelPath = path12.join(MODELS_DIR2, "jina-embeddings-v5-small-q4_k_m.gguf");
2285
2328
  if (!existsSync11(modelPath)) {
2286
2329
  throw new Error(`Embedding model not found at ${modelPath}. Run '/exe-setup' to download it.`);
2287
2330
  }
@@ -2392,6 +2435,7 @@ var init_compress = __esm({
2392
2435
  // src/lib/cloud-sync.ts
2393
2436
  var cloud_sync_exports = {};
2394
2437
  __export(cloud_sync_exports, {
2438
+ assertSecureEndpoint: () => assertSecureEndpoint,
2395
2439
  buildRosterBlob: () => buildRosterBlob,
2396
2440
  cloudPull: () => cloudPull,
2397
2441
  cloudPullBehaviors: () => cloudPullBehaviors,
@@ -2411,9 +2455,10 @@ __export(cloud_sync_exports, {
2411
2455
  cloudPushTasks: () => cloudPushTasks,
2412
2456
  cloudSync: () => cloudSync,
2413
2457
  mergeConfig: () => mergeConfig,
2414
- mergeRosterFromRemote: () => mergeRosterFromRemote
2458
+ mergeRosterFromRemote: () => mergeRosterFromRemote,
2459
+ recordRosterDeletion: () => recordRosterDeletion
2415
2460
  });
2416
- import { readFileSync as readFileSync7, writeFileSync as writeFileSync2, existsSync as existsSync9, readdirSync as readdirSync3, mkdirSync as mkdirSync3, appendFileSync } from "fs";
2461
+ import { readFileSync as readFileSync7, writeFileSync as writeFileSync2, existsSync as existsSync9, readdirSync as readdirSync3, mkdirSync as mkdirSync3, appendFileSync, unlinkSync as unlinkSync3 } from "fs";
2417
2462
  import path9 from "path";
2418
2463
  import { homedir } from "os";
2419
2464
  function logError(msg) {
@@ -2424,16 +2469,47 @@ function logError(msg) {
2424
2469
  } catch {
2425
2470
  }
2426
2471
  }
2472
+ async function withRosterLock(fn) {
2473
+ if (existsSync9(ROSTER_LOCK_PATH)) {
2474
+ try {
2475
+ const ts = parseInt(readFileSync7(ROSTER_LOCK_PATH, "utf-8"), 10);
2476
+ if (Date.now() - ts < LOCK_STALE_MS) {
2477
+ throw new Error("Roster merge already in progress \u2014 another sync is running");
2478
+ }
2479
+ } catch (err) {
2480
+ if (err instanceof Error && err.message.includes("already in progress")) throw err;
2481
+ }
2482
+ }
2483
+ writeFileSync2(ROSTER_LOCK_PATH, String(Date.now()));
2484
+ try {
2485
+ return await fn();
2486
+ } finally {
2487
+ try {
2488
+ unlinkSync3(ROSTER_LOCK_PATH);
2489
+ } catch {
2490
+ }
2491
+ }
2492
+ }
2427
2493
  async function fetchWithRetry(url, init) {
2428
- const attempt = async () => {
2429
- const signal = AbortSignal.timeout(FETCH_TIMEOUT_MS);
2430
- return fetch(url, { ...init, signal });
2431
- };
2432
- const resp = await attempt();
2433
- if (resp.status >= 500) {
2434
- return attempt();
2494
+ const MAX_RETRIES2 = 3;
2495
+ const BASE_DELAY_MS2 = 200;
2496
+ let lastError;
2497
+ for (let attempt = 0; attempt <= MAX_RETRIES2; attempt++) {
2498
+ try {
2499
+ const signal = AbortSignal.timeout(FETCH_TIMEOUT_MS);
2500
+ const resp = await fetch(url, { ...init, signal });
2501
+ if (resp.status >= 500 && attempt < MAX_RETRIES2) {
2502
+ await new Promise((r) => setTimeout(r, BASE_DELAY_MS2 * Math.pow(2, attempt)));
2503
+ continue;
2504
+ }
2505
+ return resp;
2506
+ } catch (err) {
2507
+ lastError = err;
2508
+ if (attempt === MAX_RETRIES2) throw err;
2509
+ await new Promise((r) => setTimeout(r, BASE_DELAY_MS2 * Math.pow(2, attempt)));
2510
+ }
2435
2511
  }
2436
- return resp;
2512
+ throw lastError;
2437
2513
  }
2438
2514
  function assertSecureEndpoint(endpoint) {
2439
2515
  if (endpoint.startsWith("https://")) return;
@@ -2461,10 +2537,15 @@ async function cloudPush(records, maxVersion, config) {
2461
2537
  headers: {
2462
2538
  Authorization: `Bearer ${config.apiKey}`,
2463
2539
  "Content-Type": "application/json",
2464
- "X-Device-Id": loadDeviceId()
2540
+ "X-Device-Id": loadDeviceId(),
2541
+ "X-Expected-Version": String(maxVersion)
2465
2542
  },
2466
2543
  body: JSON.stringify({ version: maxVersion, blob })
2467
2544
  });
2545
+ if (resp.status === 409) {
2546
+ logError("[cloud-sync] PUSH VERSION CONFLICT \u2014 re-pull required before next push");
2547
+ return false;
2548
+ }
2468
2549
  return resp.ok;
2469
2550
  } catch (err) {
2470
2551
  logError(`[cloud-sync] PUSH FAILED: ${err instanceof Error ? err.message : String(err)}`);
@@ -2551,18 +2632,21 @@ async function cloudSync(config) {
2551
2632
  "SELECT value FROM sync_meta WHERE key = 'last_cloud_push_version'"
2552
2633
  );
2553
2634
  const lastPushVersion = pushMeta.rows.length > 0 ? Number(pushMeta.rows[0].value) : 0;
2554
- const recordsResult = await client.execute({
2555
- sql: `SELECT id, agent_id, agent_role, session_id, timestamp,
2556
- tool_name, project_name, has_error, raw_text, version,
2557
- author_device_id, scope
2558
- FROM memories
2559
- WHERE version > ?
2560
- AND (scope IS NULL OR scope != 'personal')
2561
- ORDER BY version ASC`,
2562
- args: [lastPushVersion]
2563
- });
2564
2635
  let pushed = 0;
2565
- if (recordsResult.rows.length > 0) {
2636
+ let batchCursor = lastPushVersion;
2637
+ while (true) {
2638
+ const recordsResult = await client.execute({
2639
+ sql: `SELECT id, agent_id, agent_role, session_id, timestamp,
2640
+ tool_name, project_name, has_error, raw_text, version,
2641
+ author_device_id, scope
2642
+ FROM memories
2643
+ WHERE version > ?
2644
+ AND (scope IS NULL OR scope != 'personal')
2645
+ ORDER BY version ASC
2646
+ LIMIT ?`,
2647
+ args: [batchCursor, PUSH_BATCH_SIZE]
2648
+ });
2649
+ if (recordsResult.rows.length === 0) break;
2566
2650
  const records = recordsResult.rows.map((row) => ({
2567
2651
  id: row.id,
2568
2652
  agent_id: row.agent_id,
@@ -2579,13 +2663,14 @@ async function cloudSync(config) {
2579
2663
  }));
2580
2664
  const maxVersion = Number(records[records.length - 1].version);
2581
2665
  const pushOk = await cloudPush(records, maxVersion, config);
2582
- if (pushOk) {
2583
- await client.execute({
2584
- sql: "INSERT OR REPLACE INTO sync_meta (key, value) VALUES ('last_cloud_push_version', ?)",
2585
- args: [String(maxVersion)]
2586
- });
2587
- pushed = records.length;
2588
- }
2666
+ if (!pushOk) break;
2667
+ await client.execute({
2668
+ sql: "INSERT OR REPLACE INTO sync_meta (key, value) VALUES ('last_cloud_push_version', ?)",
2669
+ args: [String(maxVersion)]
2670
+ });
2671
+ pushed += records.length;
2672
+ batchCursor = maxVersion;
2673
+ if (recordsResult.rows.length < PUSH_BATCH_SIZE) break;
2589
2674
  }
2590
2675
  try {
2591
2676
  await cloudPushRoster(config);
@@ -2667,6 +2752,27 @@ async function cloudSync(config) {
2667
2752
  documents: documentsResult
2668
2753
  };
2669
2754
  }
2755
+ function recordRosterDeletion(name) {
2756
+ let deletions = [];
2757
+ try {
2758
+ if (existsSync9(ROSTER_DELETIONS_PATH)) {
2759
+ deletions = JSON.parse(readFileSync7(ROSTER_DELETIONS_PATH, "utf-8"));
2760
+ }
2761
+ } catch {
2762
+ }
2763
+ if (!deletions.includes(name)) deletions.push(name);
2764
+ writeFileSync2(ROSTER_DELETIONS_PATH, JSON.stringify(deletions));
2765
+ }
2766
+ function consumeRosterDeletions() {
2767
+ try {
2768
+ if (!existsSync9(ROSTER_DELETIONS_PATH)) return [];
2769
+ const deletions = JSON.parse(readFileSync7(ROSTER_DELETIONS_PATH, "utf-8"));
2770
+ writeFileSync2(ROSTER_DELETIONS_PATH, "[]");
2771
+ return deletions;
2772
+ } catch {
2773
+ return [];
2774
+ }
2775
+ }
2670
2776
  function buildRosterBlob(paths) {
2671
2777
  const rosterPath = paths?.rosterPath ?? path9.join(EXE_AI_DIR, "exe-employees.json");
2672
2778
  const identityDir = paths?.identityDir ?? path9.join(EXE_AI_DIR, "identity");
@@ -2694,9 +2800,10 @@ function buildRosterBlob(paths) {
2694
2800
  } catch {
2695
2801
  }
2696
2802
  }
2697
- const content = JSON.stringify({ roster, identities, config });
2803
+ const deletedNames = consumeRosterDeletions();
2804
+ const content = JSON.stringify({ roster, identities, config, deletedNames });
2698
2805
  const hash = Buffer.from(content).length;
2699
- return { roster, identities, config, version: hash };
2806
+ return { roster, identities, config, deletedNames, version: hash };
2700
2807
  }
2701
2808
  async function cloudPushRoster(config) {
2702
2809
  assertSecureEndpoint(config.endpoint);
@@ -2779,38 +2886,50 @@ function mergeConfig(remoteConfig, configPath) {
2779
2886
  writeFileSync2(cfgPath, JSON.stringify(merged, null, 2), "utf-8");
2780
2887
  }
2781
2888
  async function mergeRosterFromRemote(remote, paths) {
2782
- const rosterPath = paths?.rosterPath ?? void 0;
2783
- const identityDir = paths?.identityDir ?? path9.join(EXE_AI_DIR, "identity");
2784
- const localEmployees = await loadEmployees(rosterPath);
2785
- const localNames = new Set(localEmployees.map((e) => e.name));
2786
- let added = 0;
2787
- for (const remoteEmp of remote.roster) {
2788
- if (localNames.has(remoteEmp.name)) continue;
2789
- localEmployees.push(remoteEmp);
2790
- localNames.add(remoteEmp.name);
2791
- added++;
2792
- if (remote.identities[`${remoteEmp.name}.md`]) {
2793
- if (!existsSync9(identityDir)) mkdirSync3(identityDir, { recursive: true });
2794
- const idPath = path9.join(identityDir, `${remoteEmp.name}.md`);
2795
- if (!existsSync9(idPath)) {
2796
- writeFileSync2(idPath, remote.identities[`${remoteEmp.name}.md`], "utf-8");
2889
+ return withRosterLock(async () => {
2890
+ const rosterPath = paths?.rosterPath ?? void 0;
2891
+ const identityDir = paths?.identityDir ?? path9.join(EXE_AI_DIR, "identity");
2892
+ const localEmployees = await loadEmployees(rosterPath);
2893
+ const localNames = new Set(localEmployees.map((e) => e.name));
2894
+ let added = 0;
2895
+ for (const remoteEmp of remote.roster) {
2896
+ if (localNames.has(remoteEmp.name)) continue;
2897
+ localEmployees.push(remoteEmp);
2898
+ localNames.add(remoteEmp.name);
2899
+ added++;
2900
+ if (remote.identities[`${remoteEmp.name}.md`]) {
2901
+ if (!existsSync9(identityDir)) mkdirSync3(identityDir, { recursive: true });
2902
+ const idPath = path9.join(identityDir, `${remoteEmp.name}.md`);
2903
+ if (!existsSync9(idPath)) {
2904
+ writeFileSync2(idPath, remote.identities[`${remoteEmp.name}.md`], "utf-8");
2905
+ }
2906
+ }
2907
+ try {
2908
+ registerBinSymlinks(remoteEmp.name);
2909
+ } catch {
2797
2910
  }
2798
2911
  }
2799
- try {
2800
- registerBinSymlinks(remoteEmp.name);
2801
- } catch {
2912
+ let removed = 0;
2913
+ if (remote.deletedNames && remote.deletedNames.length > 0) {
2914
+ const toRemove = new Set(remote.deletedNames);
2915
+ const filtered = localEmployees.filter((e) => !toRemove.has(e.name));
2916
+ removed = localEmployees.length - filtered.length;
2917
+ if (removed > 0) {
2918
+ localEmployees.length = 0;
2919
+ localEmployees.push(...filtered);
2920
+ }
2802
2921
  }
2803
- }
2804
- if (added > 0) {
2805
- await saveEmployees(localEmployees, rosterPath);
2806
- }
2807
- if (remote.config && Object.keys(remote.config).length > 0) {
2808
- try {
2809
- mergeConfig(remote.config, paths?.configPath);
2810
- } catch {
2922
+ if (added > 0 || removed > 0) {
2923
+ await saveEmployees(localEmployees, rosterPath);
2811
2924
  }
2812
- }
2813
- return { added };
2925
+ if (remote.config && Object.keys(remote.config).length > 0) {
2926
+ try {
2927
+ mergeConfig(remote.config, paths?.configPath);
2928
+ } catch {
2929
+ }
2930
+ }
2931
+ return { added };
2932
+ });
2814
2933
  }
2815
2934
  async function cloudPushBlob(route, data, metaKey, config) {
2816
2935
  if (data.length === 0) return { ok: true };
@@ -2878,7 +2997,7 @@ async function cloudPullBlob(route, config) {
2878
2997
  }
2879
2998
  async function cloudPushBehaviors(config) {
2880
2999
  const client = getClient();
2881
- const result = await client.execute("SELECT * FROM behaviors");
3000
+ const result = await client.execute("SELECT * FROM behaviors LIMIT 10000");
2882
3001
  const rows = result.rows;
2883
3002
  const { ok } = await cloudPushBlob(
2884
3003
  "/sync/push-behaviors",
@@ -2926,13 +3045,13 @@ async function cloudPullBehaviors(config) {
2926
3045
  async function cloudPushGraphRAG(config) {
2927
3046
  const client = getClient();
2928
3047
  const [entities, relationships, aliases, entityMems, relMems, hyperedges, hyperedgeNodes] = await Promise.all([
2929
- client.execute("SELECT * FROM entities"),
2930
- client.execute("SELECT * FROM relationships"),
2931
- client.execute("SELECT * FROM entity_aliases"),
2932
- client.execute("SELECT * FROM entity_memories"),
2933
- client.execute("SELECT * FROM relationship_memories"),
2934
- client.execute("SELECT * FROM hyperedges"),
2935
- client.execute("SELECT * FROM hyperedge_nodes")
3048
+ client.execute("SELECT * FROM entities LIMIT 50000"),
3049
+ client.execute("SELECT * FROM relationships LIMIT 50000"),
3050
+ client.execute("SELECT * FROM entity_aliases LIMIT 50000"),
3051
+ client.execute("SELECT * FROM entity_memories LIMIT 50000"),
3052
+ client.execute("SELECT * FROM relationship_memories LIMIT 50000"),
3053
+ client.execute("SELECT * FROM hyperedges LIMIT 50000"),
3054
+ client.execute("SELECT * FROM hyperedge_nodes LIMIT 50000")
2936
3055
  ]);
2937
3056
  const blob = {
2938
3057
  entities: entities.rows,
@@ -3034,7 +3153,7 @@ async function cloudPullGraphRAG(config) {
3034
3153
  }
3035
3154
  async function cloudPushTasks(config) {
3036
3155
  const client = getClient();
3037
- const result = await client.execute("SELECT * FROM tasks");
3156
+ const result = await client.execute("SELECT * FROM tasks LIMIT 10000");
3038
3157
  const rows = result.rows;
3039
3158
  const { ok } = await cloudPushBlob(
3040
3159
  "/sync/push-tasks",
@@ -3080,7 +3199,7 @@ async function cloudPullTasks(config) {
3080
3199
  }
3081
3200
  async function cloudPushConversations(config) {
3082
3201
  const client = getClient();
3083
- const result = await client.execute("SELECT * FROM conversations");
3202
+ const result = await client.execute("SELECT * FROM conversations LIMIT 50000");
3084
3203
  const rows = result.rows;
3085
3204
  const { ok } = await cloudPushBlob(
3086
3205
  "/sync/push-conversations",
@@ -3130,8 +3249,8 @@ async function cloudPullConversations(config) {
3130
3249
  async function cloudPushDocuments(config) {
3131
3250
  const client = getClient();
3132
3251
  const [workspaces, documents] = await Promise.all([
3133
- client.execute("SELECT * FROM workspaces"),
3134
- client.execute("SELECT * FROM documents")
3252
+ client.execute("SELECT * FROM workspaces LIMIT 1000"),
3253
+ client.execute("SELECT * FROM documents LIMIT 10000")
3135
3254
  ]);
3136
3255
  const blob = {
3137
3256
  workspaces: workspaces.rows,
@@ -3184,7 +3303,7 @@ async function cloudPullDocuments(config) {
3184
3303
  }
3185
3304
  return { pulled };
3186
3305
  }
3187
- var LOCALHOST_PATTERNS, FETCH_TIMEOUT_MS;
3306
+ var LOCALHOST_PATTERNS, FETCH_TIMEOUT_MS, PUSH_BATCH_SIZE, ROSTER_LOCK_PATH, LOCK_STALE_MS, ROSTER_DELETIONS_PATH;
3188
3307
  var init_cloud_sync = __esm({
3189
3308
  "src/lib/cloud-sync.ts"() {
3190
3309
  "use strict";
@@ -3197,6 +3316,68 @@ var init_cloud_sync = __esm({
3197
3316
  init_employees();
3198
3317
  LOCALHOST_PATTERNS = /^(localhost|127\.0\.0\.1|\[::1\])$/i;
3199
3318
  FETCH_TIMEOUT_MS = 3e4;
3319
+ PUSH_BATCH_SIZE = 5e3;
3320
+ ROSTER_LOCK_PATH = path9.join(EXE_AI_DIR, "roster-merge.lock");
3321
+ LOCK_STALE_MS = 3e4;
3322
+ ROSTER_DELETIONS_PATH = path9.join(EXE_AI_DIR, "roster-deletions.json");
3323
+ }
3324
+ });
3325
+
3326
+ // src/lib/worker-gate.ts
3327
+ var worker_gate_exports = {};
3328
+ __export(worker_gate_exports, {
3329
+ MAX_CONCURRENT_WORKERS: () => MAX_CONCURRENT_WORKERS,
3330
+ cleanupWorkerPid: () => cleanupWorkerPid,
3331
+ registerWorkerPid: () => registerWorkerPid,
3332
+ tryAcquireWorkerSlot: () => tryAcquireWorkerSlot
3333
+ });
3334
+ import { readdirSync as readdirSync4, writeFileSync as writeFileSync3, unlinkSync as unlinkSync4, mkdirSync as mkdirSync4 } from "fs";
3335
+ import path10 from "path";
3336
+ function tryAcquireWorkerSlot() {
3337
+ try {
3338
+ mkdirSync4(WORKER_PID_DIR, { recursive: true });
3339
+ const files = readdirSync4(WORKER_PID_DIR);
3340
+ let alive = 0;
3341
+ for (const f of files) {
3342
+ if (!f.endsWith(".pid")) continue;
3343
+ const dashIdx = f.lastIndexOf("-");
3344
+ const pid = parseInt(f.slice(dashIdx + 1).replace(".pid", ""), 10);
3345
+ if (isNaN(pid)) continue;
3346
+ try {
3347
+ process.kill(pid, 0);
3348
+ alive++;
3349
+ } catch {
3350
+ try {
3351
+ unlinkSync4(path10.join(WORKER_PID_DIR, f));
3352
+ } catch {
3353
+ }
3354
+ }
3355
+ }
3356
+ return alive < MAX_CONCURRENT_WORKERS;
3357
+ } catch {
3358
+ return true;
3359
+ }
3360
+ }
3361
+ function registerWorkerPid(pid) {
3362
+ try {
3363
+ mkdirSync4(WORKER_PID_DIR, { recursive: true });
3364
+ writeFileSync3(path10.join(WORKER_PID_DIR, `worker-${pid}.pid`), String(pid));
3365
+ } catch {
3366
+ }
3367
+ }
3368
+ function cleanupWorkerPid() {
3369
+ try {
3370
+ unlinkSync4(path10.join(WORKER_PID_DIR, `worker-${process.pid}.pid`));
3371
+ } catch {
3372
+ }
3373
+ }
3374
+ var WORKER_PID_DIR, MAX_CONCURRENT_WORKERS;
3375
+ var init_worker_gate = __esm({
3376
+ "src/lib/worker-gate.ts"() {
3377
+ "use strict";
3378
+ init_config();
3379
+ WORKER_PID_DIR = path10.join(EXE_AI_DIR, "worker-pids");
3380
+ MAX_CONCURRENT_WORKERS = 3;
3200
3381
  }
3201
3382
  });
3202
3383
 
@@ -3205,6 +3386,30 @@ init_memory();
3205
3386
  init_database();
3206
3387
  init_keychain();
3207
3388
  init_config();
3389
+ var INIT_MAX_RETRIES = 3;
3390
+ var INIT_RETRY_DELAY_MS = 1e3;
3391
+ function isBusyError2(err) {
3392
+ if (err instanceof Error) {
3393
+ const msg = err.message.toLowerCase();
3394
+ return msg.includes("sqlite_busy") || msg.includes("database is locked");
3395
+ }
3396
+ return false;
3397
+ }
3398
+ async function retryOnBusy2(fn, label) {
3399
+ for (let attempt = 0; attempt <= INIT_MAX_RETRIES; attempt++) {
3400
+ try {
3401
+ return await fn();
3402
+ } catch (err) {
3403
+ if (!isBusyError2(err) || attempt === INIT_MAX_RETRIES) throw err;
3404
+ process.stderr.write(
3405
+ `[store] SQLITE_BUSY during ${label}, retry ${attempt + 1}/${INIT_MAX_RETRIES}
3406
+ `
3407
+ );
3408
+ await new Promise((r) => setTimeout(r, INIT_RETRY_DELAY_MS * (attempt + 1)));
3409
+ }
3410
+ }
3411
+ throw new Error("unreachable");
3412
+ }
3208
3413
  var _pendingRecords = [];
3209
3414
  var _batchSize = 20;
3210
3415
  var _flushIntervalMs = 1e4;
@@ -3239,14 +3444,17 @@ async function initStore(options) {
3239
3444
  dbPath,
3240
3445
  encryptionKey: hexKey
3241
3446
  });
3242
- await ensureSchema();
3447
+ await retryOnBusy2(() => ensureSchema(), "ensureSchema");
3243
3448
  try {
3244
3449
  const { initShardManager: initShardManager2 } = await Promise.resolve().then(() => (init_shard_manager(), shard_manager_exports));
3245
3450
  initShardManager2(hexKey);
3246
3451
  } catch {
3247
3452
  }
3248
3453
  const client = getClient();
3249
- const vResult = await client.execute("SELECT MAX(version) as max_v FROM memories");
3454
+ const vResult = await retryOnBusy2(
3455
+ () => client.execute("SELECT MAX(version) as max_v FROM memories"),
3456
+ "version-query"
3457
+ );
3250
3458
  _nextVersion = (Number(vResult.rows[0]?.max_v) || 0) + 1;
3251
3459
  }
3252
3460
  function classifyTier(record) {
@@ -3451,7 +3659,7 @@ import crypto4 from "crypto";
3451
3659
  init_database();
3452
3660
  import crypto2 from "crypto";
3453
3661
  import path4 from "path";
3454
- import os2 from "os";
3662
+ import os3 from "os";
3455
3663
  import {
3456
3664
  readFileSync as readFileSync2,
3457
3665
  readdirSync as readdirSync2,
@@ -3486,8 +3694,8 @@ async function writeNotification(notification) {
3486
3694
 
3487
3695
  // src/adapters/claude/hooks/summary-worker.ts
3488
3696
  import { execSync as execSync2 } from "child_process";
3489
- import { existsSync as existsSync10, mkdirSync as mkdirSync4, openSync as openSync2, closeSync as closeSync2 } from "fs";
3490
- import path10 from "path";
3697
+ import { existsSync as existsSync10, mkdirSync as mkdirSync5, openSync as openSync2, closeSync as closeSync2 } from "fs";
3698
+ import path11 from "path";
3491
3699
  async function main() {
3492
3700
  const agentId = process.env.AGENT_ID ?? "default";
3493
3701
  const agentRole = process.env.AGENT_ROLE ?? "employee";
@@ -3611,16 +3819,16 @@ async function main() {
3611
3819
  }
3612
3820
  try {
3613
3821
  const { EXE_AI_DIR: EXE_AI_DIR2 } = await Promise.resolve().then(() => (init_config(), config_exports));
3614
- const flagPath = path10.join(EXE_AI_DIR2, "session-cache", "needs-backfill");
3822
+ const flagPath = path11.join(EXE_AI_DIR2, "session-cache", "needs-backfill");
3615
3823
  if (existsSync10(flagPath)) {
3616
3824
  const { spawn: spawn2 } = await import("child_process");
3617
3825
  const { fileURLToPath: fileURLToPath2 } = await import("url");
3618
3826
  const thisFile = fileURLToPath2(import.meta.url);
3619
- const backfillPath = path10.resolve(path10.dirname(thisFile), "backfill-vectors.js");
3827
+ const backfillPath = path11.resolve(path11.dirname(thisFile), "backfill-vectors.js");
3620
3828
  if (existsSync10(backfillPath)) {
3621
3829
  const { EXE_AI_DIR: exeDir2 } = await Promise.resolve().then(() => (init_config(), config_exports));
3622
- const bLogPath = path10.join(exeDir2, "workers.log");
3623
- mkdirSync4(path10.dirname(bLogPath), { recursive: true });
3830
+ const bLogPath = path11.join(exeDir2, "workers.log");
3831
+ mkdirSync5(path11.dirname(bLogPath), { recursive: true });
3624
3832
  const bLogFd = openSync2(bLogPath, "a");
3625
3833
  const child = spawn2(process.execPath, [backfillPath], {
3626
3834
  detached: true,
@@ -3693,9 +3901,19 @@ async function main() {
3693
3901
  process.stderr.write("[summary-worker] orphan scan failed: " + (err instanceof Error ? err.message : String(err)) + "\n");
3694
3902
  }
3695
3903
  }
3904
+ try {
3905
+ const { getRawClient: getRawClient2 } = await Promise.resolve().then(() => (init_database(), database_exports));
3906
+ await getRawClient2().execute("PRAGMA wal_checkpoint(PASSIVE)");
3907
+ } catch {
3908
+ }
3696
3909
  }
3697
3910
  main().catch((err) => {
3698
3911
  process.stderr.write("[summary-worker] FATAL: " + (err instanceof Error ? err.message : String(err)) + "\n");
3699
- }).finally(() => {
3912
+ }).finally(async () => {
3913
+ try {
3914
+ const { cleanupWorkerPid: cleanupWorkerPid2 } = await Promise.resolve().then(() => (init_worker_gate(), worker_gate_exports));
3915
+ cleanupWorkerPid2();
3916
+ } catch {
3917
+ }
3700
3918
  process.exit(0);
3701
3919
  });