@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
package/dist/index.js CHANGED
@@ -461,6 +461,7 @@ async function ensureSchema() {
461
461
  const client = getRawClient();
462
462
  await client.execute("PRAGMA journal_mode = WAL");
463
463
  await client.execute("PRAGMA busy_timeout = 30000");
464
+ await client.execute("PRAGMA wal_autocheckpoint = 1000");
464
465
  try {
465
466
  await client.execute("PRAGMA libsql_vector_search_ef = 128");
466
467
  } catch {
@@ -1284,7 +1285,7 @@ __export(config_exports, {
1284
1285
  migrateConfig: () => migrateConfig,
1285
1286
  saveConfig: () => saveConfig
1286
1287
  });
1287
- import { readFile, writeFile, mkdir } from "fs/promises";
1288
+ import { readFile, writeFile, mkdir, chmod } from "fs/promises";
1288
1289
  import { readFileSync as readFileSync3, existsSync as existsSync3, renameSync as renameSync2 } from "fs";
1289
1290
  import path4 from "path";
1290
1291
  import os4 from "os";
@@ -1410,6 +1411,9 @@ async function saveConfig(config2) {
1410
1411
  await mkdir(dir, { recursive: true });
1411
1412
  const configPath = path4.join(dir, "config.json");
1412
1413
  await writeFile(configPath, JSON.stringify(config2, null, 2) + "\n");
1414
+ if (config2.cloud?.apiKey) {
1415
+ await chmod(configPath, 384);
1416
+ }
1413
1417
  }
1414
1418
  async function loadConfigFrom(configPath) {
1415
1419
  const raw = await readFile(configPath, "utf-8");
@@ -3958,12 +3962,13 @@ var init_memory = __esm({
3958
3962
  });
3959
3963
 
3960
3964
  // src/lib/keychain.ts
3961
- import { readFile as readFile4, writeFile as writeFile5, unlink, mkdir as mkdir4, chmod } from "fs/promises";
3965
+ import { readFile as readFile4, writeFile as writeFile5, unlink, mkdir as mkdir4, chmod as chmod2 } from "fs/promises";
3962
3966
  import { existsSync as existsSync11 } from "fs";
3963
3967
  import path15 from "path";
3968
+ import os7 from "os";
3964
3969
  import crypto6 from "crypto";
3965
3970
  function getKeyDir() {
3966
- return process.env.EXE_OS_DIR ?? process.env.EXE_MEM_DIR ?? path15.join(process.env.HOME ?? "/tmp", ".exe-os");
3971
+ return process.env.EXE_OS_DIR ?? process.env.EXE_MEM_DIR ?? path15.join(os7.homedir(), ".exe-os");
3967
3972
  }
3968
3973
  function getKeyPath() {
3969
3974
  return path15.join(getKeyDir(), "master.key");
@@ -4264,6 +4269,28 @@ __export(store_exports, {
4264
4269
  vectorToBlob: () => vectorToBlob,
4265
4270
  writeMemory: () => writeMemory
4266
4271
  });
4272
+ function isBusyError2(err) {
4273
+ if (err instanceof Error) {
4274
+ const msg = err.message.toLowerCase();
4275
+ return msg.includes("sqlite_busy") || msg.includes("database is locked");
4276
+ }
4277
+ return false;
4278
+ }
4279
+ async function retryOnBusy2(fn, label) {
4280
+ for (let attempt = 0; attempt <= INIT_MAX_RETRIES; attempt++) {
4281
+ try {
4282
+ return await fn();
4283
+ } catch (err) {
4284
+ if (!isBusyError2(err) || attempt === INIT_MAX_RETRIES) throw err;
4285
+ process.stderr.write(
4286
+ `[store] SQLITE_BUSY during ${label}, retry ${attempt + 1}/${INIT_MAX_RETRIES}
4287
+ `
4288
+ );
4289
+ await new Promise((r) => setTimeout(r, INIT_RETRY_DELAY_MS * (attempt + 1)));
4290
+ }
4291
+ }
4292
+ throw new Error("unreachable");
4293
+ }
4267
4294
  async function initStore(options) {
4268
4295
  if (_flushTimer !== null) {
4269
4296
  clearInterval(_flushTimer);
@@ -4292,14 +4319,17 @@ async function initStore(options) {
4292
4319
  dbPath,
4293
4320
  encryptionKey: hexKey
4294
4321
  });
4295
- await ensureSchema();
4322
+ await retryOnBusy2(() => ensureSchema(), "ensureSchema");
4296
4323
  try {
4297
4324
  const { initShardManager: initShardManager2 } = await Promise.resolve().then(() => (init_shard_manager(), shard_manager_exports));
4298
4325
  initShardManager2(hexKey);
4299
4326
  } catch {
4300
4327
  }
4301
4328
  const client = getClient();
4302
- const vResult = await client.execute("SELECT MAX(version) as max_v FROM memories");
4329
+ const vResult = await retryOnBusy2(
4330
+ () => client.execute("SELECT MAX(version) as max_v FROM memories"),
4331
+ "version-query"
4332
+ );
4303
4333
  _nextVersion = (Number(vResult.rows[0]?.max_v) || 0) + 1;
4304
4334
  }
4305
4335
  function classifyTier(record) {
@@ -4679,7 +4709,7 @@ async function getMemoryCardinality(agentId) {
4679
4709
  return 0;
4680
4710
  }
4681
4711
  }
4682
- var _pendingRecords, _batchSize, _flushIntervalMs, _flushTimer, _flushing, _nextVersion;
4712
+ var INIT_MAX_RETRIES, INIT_RETRY_DELAY_MS, _pendingRecords, _batchSize, _flushIntervalMs, _flushTimer, _flushing, _nextVersion;
4683
4713
  var init_store = __esm({
4684
4714
  "src/lib/store.ts"() {
4685
4715
  "use strict";
@@ -4687,6 +4717,8 @@ var init_store = __esm({
4687
4717
  init_database();
4688
4718
  init_keychain();
4689
4719
  init_config();
4720
+ INIT_MAX_RETRIES = 3;
4721
+ INIT_RETRY_DELAY_MS = 1e3;
4690
4722
  _pendingRecords = [];
4691
4723
  _batchSize = 20;
4692
4724
  _flushIntervalMs = 1e4;
@@ -5079,7 +5111,8 @@ async function gqlRequest(query, variables) {
5079
5111
  "Content-Type": "application/json",
5080
5112
  Authorization: `Bearer ${config.apiToken}`
5081
5113
  },
5082
- body: JSON.stringify({ query, variables })
5114
+ body: JSON.stringify({ query, variables }),
5115
+ signal: AbortSignal.timeout(3e4)
5083
5116
  });
5084
5117
  if (!res.ok) {
5085
5118
  throw new Error(`CRM GraphQL request failed: ${res.status} ${res.statusText}`);
@@ -5307,6 +5340,10 @@ import path17 from "path";
5307
5340
  import { fileURLToPath as fileURLToPath2 } from "url";
5308
5341
  function handleData(chunk) {
5309
5342
  _buffer += chunk.toString();
5343
+ if (_buffer.length > MAX_BUFFER) {
5344
+ _buffer = "";
5345
+ return;
5346
+ }
5310
5347
  let newlineIdx;
5311
5348
  while ((newlineIdx = _buffer.indexOf("\n")) !== -1) {
5312
5349
  const line = _buffer.slice(0, newlineIdx).trim();
@@ -5614,7 +5651,7 @@ function disconnectClient() {
5614
5651
  entry.resolve({ error: "Client disconnected" });
5615
5652
  }
5616
5653
  }
5617
- 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;
5654
+ 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;
5618
5655
  var init_exe_daemon_client = __esm({
5619
5656
  "src/lib/exe-daemon-client.ts"() {
5620
5657
  "use strict";
@@ -5631,6 +5668,7 @@ var init_exe_daemon_client = __esm({
5631
5668
  _requestCount = 0;
5632
5669
  HEALTH_CHECK_INTERVAL = 100;
5633
5670
  _pending = /* @__PURE__ */ new Map();
5671
+ MAX_BUFFER = 1e7;
5634
5672
  }
5635
5673
  });
5636
5674
 
@@ -5720,12 +5758,30 @@ async function wikiFetch(config2, path20, method = "GET", body) {
5720
5758
  const controller = new AbortController();
5721
5759
  const timeout = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS2);
5722
5760
  try {
5723
- const response = await fetch(url, {
5724
- method,
5725
- headers,
5726
- body: body ? JSON.stringify(body) : void 0,
5727
- signal: controller.signal
5728
- });
5761
+ let response;
5762
+ try {
5763
+ response = await fetch(url, {
5764
+ method,
5765
+ headers,
5766
+ body: body ? JSON.stringify(body) : void 0,
5767
+ signal: controller.signal
5768
+ });
5769
+ } catch {
5770
+ clearTimeout(timeout);
5771
+ const retryController = new AbortController();
5772
+ const retryTimeout = setTimeout(() => retryController.abort(), REQUEST_TIMEOUT_MS2);
5773
+ try {
5774
+ await new Promise((r) => setTimeout(r, 500));
5775
+ response = await fetch(url, {
5776
+ method,
5777
+ headers,
5778
+ body: body ? JSON.stringify(body) : void 0,
5779
+ signal: retryController.signal
5780
+ });
5781
+ } finally {
5782
+ clearTimeout(retryTimeout);
5783
+ }
5784
+ }
5729
5785
  if (!response.ok) {
5730
5786
  throw new Error(`Wiki API ${method} ${path20}: ${response.status} ${response.statusText}`);
5731
5787
  }
@@ -9814,7 +9870,8 @@ var OllamaProvider = class {
9814
9870
  const res = await fetch(`${this.host}/api/chat`, {
9815
9871
  method: "POST",
9816
9872
  headers: { "Content-Type": "application/json" },
9817
- body: JSON.stringify(body)
9873
+ body: JSON.stringify(body),
9874
+ signal: AbortSignal.timeout(3e4)
9818
9875
  });
9819
9876
  if (!res.ok) {
9820
9877
  throw new Error(`Ollama API error: ${res.status} ${await res.text()}`);
@@ -10242,7 +10299,7 @@ var SignalAdapter = class {
10242
10299
  if (/^https?:\/\//i.test(trimmed)) {
10243
10300
  return trimmed.replace(/\/+$/, "");
10244
10301
  }
10245
- return `http://${trimmed}`.replace(/\/+$/, "");
10302
+ return `https://${trimmed}`.replace(/\/+$/, "");
10246
10303
  }
10247
10304
  async connect(config2) {
10248
10305
  this.baseUrl = this.normalizeBaseUrl(
@@ -10664,9 +10721,15 @@ var WebChatAdapter = class {
10664
10721
  res.end(JSON.stringify({ error: "Not found" }));
10665
10722
  }
10666
10723
  async handleChatRequest(req, res) {
10724
+ const MAX_BODY_SIZE = 1048576;
10667
10725
  let body = "";
10668
10726
  for await (const chunk of req) {
10669
10727
  body += chunk;
10728
+ if (body.length > MAX_BODY_SIZE) {
10729
+ res.writeHead(413, { "Content-Type": "application/json" });
10730
+ res.end(JSON.stringify({ error: "Request body too large" }));
10731
+ return;
10732
+ }
10670
10733
  }
10671
10734
  let parsed;
10672
10735
  try {
@@ -11146,12 +11209,12 @@ var SlackAdapter = class {
11146
11209
  // src/gateway/adapters/imessage.ts
11147
11210
  import { execFile } from "child_process";
11148
11211
  import { promisify } from "util";
11149
- import os7 from "os";
11212
+ import os8 from "os";
11150
11213
  import path18 from "path";
11151
11214
  var execFileAsync = promisify(execFile);
11152
11215
  var POLL_INTERVAL_MS = 5e3;
11153
11216
  var MESSAGES_DB_PATH = path18.join(
11154
- process.env.HOME ?? os7.homedir(),
11217
+ process.env.HOME ?? os8.homedir(),
11155
11218
  "Library/Messages/chat.db"
11156
11219
  );
11157
11220
  var IMessageAdapter = class {
@@ -11997,8 +12060,9 @@ async function ensureCRMContact(info) {
11997
12060
  import { readFileSync as readFileSync12, writeFileSync as writeFileSync6, existsSync as existsSync14, mkdirSync as mkdirSync8 } from "fs";
11998
12061
  import { randomUUID as randomUUID14 } from "crypto";
11999
12062
  import path19 from "path";
12000
- import os8 from "os";
12001
- var TRIGGERS_PATH = path19.join(os8.homedir(), ".exe-os", "triggers.json");
12063
+ import os9 from "os";
12064
+ var TRIGGERS_PATH = path19.join(os9.homedir(), ".exe-os", "triggers.json");
12065
+ var GRAPH_API_VERSION = "v21.0";
12002
12066
  function substituteTemplate(template, record) {
12003
12067
  return template.replace(
12004
12068
  /\{\{(\w+(?:\.\w+)*)\}\}/g,
@@ -12076,7 +12140,7 @@ async function executeSendWhatsapp(params) {
12076
12140
  const message = params.message ?? params.text;
12077
12141
  if (!to || !message)
12078
12142
  throw new Error("send_whatsapp requires 'to' and 'message' params");
12079
- const url = `https://graph.facebook.com/v21.0/${account.phoneNumberId}/messages`;
12143
+ const url = `https://graph.facebook.com/${GRAPH_API_VERSION}/${account.phoneNumberId}/messages`;
12080
12144
  const res = await fetch(url, {
12081
12145
  method: "POST",
12082
12146
  headers: {
@@ -12088,7 +12152,8 @@ async function executeSendWhatsapp(params) {
12088
12152
  to,
12089
12153
  type: "text",
12090
12154
  text: { body: message }
12091
- })
12155
+ }),
12156
+ signal: AbortSignal.timeout(3e4)
12092
12157
  });
12093
12158
  if (!res.ok) {
12094
12159
  const errBody = await res.text();
@@ -6,7 +6,7 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
6
6
  });
7
7
 
8
8
  // src/lib/cloud-sync.ts
9
- import { readFileSync as readFileSync5, writeFileSync as writeFileSync2, existsSync as existsSync5, readdirSync, mkdirSync as mkdirSync2, appendFileSync } from "fs";
9
+ import { readFileSync as readFileSync5, writeFileSync as writeFileSync2, existsSync as existsSync5, readdirSync, mkdirSync as mkdirSync2, appendFileSync, unlinkSync } from "fs";
10
10
  import path5 from "path";
11
11
  import { homedir } from "os";
12
12
 
@@ -80,7 +80,7 @@ import { execSync } from "child_process";
80
80
  import path2 from "path";
81
81
 
82
82
  // src/lib/config.ts
83
- import { readFile, writeFile, mkdir } from "fs/promises";
83
+ import { readFile, writeFile, mkdir, chmod } from "fs/promises";
84
84
  import { readFileSync, existsSync, renameSync } from "fs";
85
85
  import path from "path";
86
86
  import os from "os";
@@ -187,15 +187,20 @@ async function saveEmployees(employees, employeesPath = EMPLOYEES_PATH) {
187
187
  await mkdir2(path2.dirname(employeesPath), { recursive: true });
188
188
  await writeFile2(employeesPath, JSON.stringify(employees, null, 2) + "\n", "utf-8");
189
189
  }
190
+ function findExeBin() {
191
+ try {
192
+ return execSync(process.platform === "win32" ? "where exe-os" : "which exe-os", { encoding: "utf8" }).trim();
193
+ } catch {
194
+ return null;
195
+ }
196
+ }
190
197
  function registerBinSymlinks(name) {
191
198
  const created = [];
192
199
  const skipped = [];
193
200
  const errors = [];
194
- let exeBinPath;
195
- try {
196
- exeBinPath = execSync("which exe", { encoding: "utf-8" }).trim();
197
- } catch {
198
- errors.push("Could not find 'exe' in PATH");
201
+ const exeBinPath = findExeBin();
202
+ if (!exeBinPath) {
203
+ errors.push("Could not find 'exe-os' in PATH");
199
204
  return { created, skipped, errors };
200
205
  }
201
206
  const binDir = path2.dirname(exeBinPath);
@@ -232,6 +237,15 @@ var LICENSE_PATH = path3.join(EXE_AI_DIR, "license.key");
232
237
  var CACHE_PATH = path3.join(EXE_AI_DIR, "license-cache.json");
233
238
  var DEVICE_ID_PATH = path3.join(EXE_AI_DIR, "device-id");
234
239
  var API_BASE = "https://askexe.com/cloud";
240
+ var RETRY_DELAY_MS = 500;
241
+ async function fetchRetry(url, init) {
242
+ try {
243
+ return await fetch(url, init);
244
+ } catch {
245
+ await new Promise((r) => setTimeout(r, RETRY_DELAY_MS));
246
+ return fetch(url, { ...init, signal: AbortSignal.timeout(1e4) });
247
+ }
248
+ }
235
249
  var LICENSE_PUBLIC_KEY_PEM = `-----BEGIN PUBLIC KEY-----
236
250
  MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEeHztAMOpR/ZMh+rWuOASjEZ54CGY
237
251
  4uj+UqeKCcvtgNHKmOK278HJaJcANe9xAeji8AFYu27q3WtzCi04pHudow==
@@ -323,7 +337,7 @@ function cacheResponse(token) {
323
337
  async function validateLicense(apiKey, deviceId) {
324
338
  const did = deviceId ?? loadDeviceId();
325
339
  try {
326
- const res = await fetch(`${API_BASE}/auth/activate`, {
340
+ const res = await fetchRetry(`${API_BASE}/auth/activate`, {
327
341
  method: "POST",
328
342
  headers: { "Content-Type": "application/json" },
329
343
  body: JSON.stringify({ apiKey, deviceId: did }),
@@ -418,16 +432,50 @@ function logError(msg) {
418
432
  }
419
433
  var LOCALHOST_PATTERNS = /^(localhost|127\.0\.0\.1|\[::1\])$/i;
420
434
  var FETCH_TIMEOUT_MS = 3e4;
435
+ var PUSH_BATCH_SIZE = 5e3;
436
+ var ROSTER_LOCK_PATH = path5.join(EXE_AI_DIR, "roster-merge.lock");
437
+ var LOCK_STALE_MS = 3e4;
438
+ async function withRosterLock(fn) {
439
+ if (existsSync5(ROSTER_LOCK_PATH)) {
440
+ try {
441
+ const ts = parseInt(readFileSync5(ROSTER_LOCK_PATH, "utf-8"), 10);
442
+ if (Date.now() - ts < LOCK_STALE_MS) {
443
+ throw new Error("Roster merge already in progress \u2014 another sync is running");
444
+ }
445
+ } catch (err) {
446
+ if (err instanceof Error && err.message.includes("already in progress")) throw err;
447
+ }
448
+ }
449
+ writeFileSync2(ROSTER_LOCK_PATH, String(Date.now()));
450
+ try {
451
+ return await fn();
452
+ } finally {
453
+ try {
454
+ unlinkSync(ROSTER_LOCK_PATH);
455
+ } catch {
456
+ }
457
+ }
458
+ }
421
459
  async function fetchWithRetry(url, init) {
422
- const attempt = async () => {
423
- const signal = AbortSignal.timeout(FETCH_TIMEOUT_MS);
424
- return fetch(url, { ...init, signal });
425
- };
426
- const resp = await attempt();
427
- if (resp.status >= 500) {
428
- return attempt();
460
+ const MAX_RETRIES = 3;
461
+ const BASE_DELAY_MS = 200;
462
+ let lastError;
463
+ for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
464
+ try {
465
+ const signal = AbortSignal.timeout(FETCH_TIMEOUT_MS);
466
+ const resp = await fetch(url, { ...init, signal });
467
+ if (resp.status >= 500 && attempt < MAX_RETRIES) {
468
+ await new Promise((r) => setTimeout(r, BASE_DELAY_MS * Math.pow(2, attempt)));
469
+ continue;
470
+ }
471
+ return resp;
472
+ } catch (err) {
473
+ lastError = err;
474
+ if (attempt === MAX_RETRIES) throw err;
475
+ await new Promise((r) => setTimeout(r, BASE_DELAY_MS * Math.pow(2, attempt)));
476
+ }
429
477
  }
430
- return resp;
478
+ throw lastError;
431
479
  }
432
480
  function assertSecureEndpoint(endpoint) {
433
481
  if (endpoint.startsWith("https://")) return;
@@ -455,10 +503,15 @@ async function cloudPush(records, maxVersion, config) {
455
503
  headers: {
456
504
  Authorization: `Bearer ${config.apiKey}`,
457
505
  "Content-Type": "application/json",
458
- "X-Device-Id": loadDeviceId()
506
+ "X-Device-Id": loadDeviceId(),
507
+ "X-Expected-Version": String(maxVersion)
459
508
  },
460
509
  body: JSON.stringify({ version: maxVersion, blob })
461
510
  });
511
+ if (resp.status === 409) {
512
+ logError("[cloud-sync] PUSH VERSION CONFLICT \u2014 re-pull required before next push");
513
+ return false;
514
+ }
462
515
  return resp.ok;
463
516
  } catch (err) {
464
517
  logError(`[cloud-sync] PUSH FAILED: ${err instanceof Error ? err.message : String(err)}`);
@@ -545,18 +598,21 @@ async function cloudSync(config) {
545
598
  "SELECT value FROM sync_meta WHERE key = 'last_cloud_push_version'"
546
599
  );
547
600
  const lastPushVersion = pushMeta.rows.length > 0 ? Number(pushMeta.rows[0].value) : 0;
548
- const recordsResult = await client.execute({
549
- sql: `SELECT id, agent_id, agent_role, session_id, timestamp,
550
- tool_name, project_name, has_error, raw_text, version,
551
- author_device_id, scope
552
- FROM memories
553
- WHERE version > ?
554
- AND (scope IS NULL OR scope != 'personal')
555
- ORDER BY version ASC`,
556
- args: [lastPushVersion]
557
- });
558
601
  let pushed = 0;
559
- if (recordsResult.rows.length > 0) {
602
+ let batchCursor = lastPushVersion;
603
+ while (true) {
604
+ const recordsResult = await client.execute({
605
+ sql: `SELECT id, agent_id, agent_role, session_id, timestamp,
606
+ tool_name, project_name, has_error, raw_text, version,
607
+ author_device_id, scope
608
+ FROM memories
609
+ WHERE version > ?
610
+ AND (scope IS NULL OR scope != 'personal')
611
+ ORDER BY version ASC
612
+ LIMIT ?`,
613
+ args: [batchCursor, PUSH_BATCH_SIZE]
614
+ });
615
+ if (recordsResult.rows.length === 0) break;
560
616
  const records = recordsResult.rows.map((row) => ({
561
617
  id: row.id,
562
618
  agent_id: row.agent_id,
@@ -573,13 +629,14 @@ async function cloudSync(config) {
573
629
  }));
574
630
  const maxVersion = Number(records[records.length - 1].version);
575
631
  const pushOk = await cloudPush(records, maxVersion, config);
576
- if (pushOk) {
577
- await client.execute({
578
- sql: "INSERT OR REPLACE INTO sync_meta (key, value) VALUES ('last_cloud_push_version', ?)",
579
- args: [String(maxVersion)]
580
- });
581
- pushed = records.length;
582
- }
632
+ if (!pushOk) break;
633
+ await client.execute({
634
+ sql: "INSERT OR REPLACE INTO sync_meta (key, value) VALUES ('last_cloud_push_version', ?)",
635
+ args: [String(maxVersion)]
636
+ });
637
+ pushed += records.length;
638
+ batchCursor = maxVersion;
639
+ if (recordsResult.rows.length < PUSH_BATCH_SIZE) break;
583
640
  }
584
641
  try {
585
642
  await cloudPushRoster(config);
@@ -661,6 +718,28 @@ async function cloudSync(config) {
661
718
  documents: documentsResult
662
719
  };
663
720
  }
721
+ var ROSTER_DELETIONS_PATH = path5.join(EXE_AI_DIR, "roster-deletions.json");
722
+ function recordRosterDeletion(name) {
723
+ let deletions = [];
724
+ try {
725
+ if (existsSync5(ROSTER_DELETIONS_PATH)) {
726
+ deletions = JSON.parse(readFileSync5(ROSTER_DELETIONS_PATH, "utf-8"));
727
+ }
728
+ } catch {
729
+ }
730
+ if (!deletions.includes(name)) deletions.push(name);
731
+ writeFileSync2(ROSTER_DELETIONS_PATH, JSON.stringify(deletions));
732
+ }
733
+ function consumeRosterDeletions() {
734
+ try {
735
+ if (!existsSync5(ROSTER_DELETIONS_PATH)) return [];
736
+ const deletions = JSON.parse(readFileSync5(ROSTER_DELETIONS_PATH, "utf-8"));
737
+ writeFileSync2(ROSTER_DELETIONS_PATH, "[]");
738
+ return deletions;
739
+ } catch {
740
+ return [];
741
+ }
742
+ }
664
743
  function buildRosterBlob(paths) {
665
744
  const rosterPath = paths?.rosterPath ?? path5.join(EXE_AI_DIR, "exe-employees.json");
666
745
  const identityDir = paths?.identityDir ?? path5.join(EXE_AI_DIR, "identity");
@@ -688,9 +767,10 @@ function buildRosterBlob(paths) {
688
767
  } catch {
689
768
  }
690
769
  }
691
- const content = JSON.stringify({ roster, identities, config });
770
+ const deletedNames = consumeRosterDeletions();
771
+ const content = JSON.stringify({ roster, identities, config, deletedNames });
692
772
  const hash = Buffer.from(content).length;
693
- return { roster, identities, config, version: hash };
773
+ return { roster, identities, config, deletedNames, version: hash };
694
774
  }
695
775
  async function cloudPushRoster(config) {
696
776
  assertSecureEndpoint(config.endpoint);
@@ -773,38 +853,50 @@ function mergeConfig(remoteConfig, configPath) {
773
853
  writeFileSync2(cfgPath, JSON.stringify(merged, null, 2), "utf-8");
774
854
  }
775
855
  async function mergeRosterFromRemote(remote, paths) {
776
- const rosterPath = paths?.rosterPath ?? void 0;
777
- const identityDir = paths?.identityDir ?? path5.join(EXE_AI_DIR, "identity");
778
- const localEmployees = await loadEmployees(rosterPath);
779
- const localNames = new Set(localEmployees.map((e) => e.name));
780
- let added = 0;
781
- for (const remoteEmp of remote.roster) {
782
- if (localNames.has(remoteEmp.name)) continue;
783
- localEmployees.push(remoteEmp);
784
- localNames.add(remoteEmp.name);
785
- added++;
786
- if (remote.identities[`${remoteEmp.name}.md`]) {
787
- if (!existsSync5(identityDir)) mkdirSync2(identityDir, { recursive: true });
788
- const idPath = path5.join(identityDir, `${remoteEmp.name}.md`);
789
- if (!existsSync5(idPath)) {
790
- writeFileSync2(idPath, remote.identities[`${remoteEmp.name}.md`], "utf-8");
856
+ return withRosterLock(async () => {
857
+ const rosterPath = paths?.rosterPath ?? void 0;
858
+ const identityDir = paths?.identityDir ?? path5.join(EXE_AI_DIR, "identity");
859
+ const localEmployees = await loadEmployees(rosterPath);
860
+ const localNames = new Set(localEmployees.map((e) => e.name));
861
+ let added = 0;
862
+ for (const remoteEmp of remote.roster) {
863
+ if (localNames.has(remoteEmp.name)) continue;
864
+ localEmployees.push(remoteEmp);
865
+ localNames.add(remoteEmp.name);
866
+ added++;
867
+ if (remote.identities[`${remoteEmp.name}.md`]) {
868
+ if (!existsSync5(identityDir)) mkdirSync2(identityDir, { recursive: true });
869
+ const idPath = path5.join(identityDir, `${remoteEmp.name}.md`);
870
+ if (!existsSync5(idPath)) {
871
+ writeFileSync2(idPath, remote.identities[`${remoteEmp.name}.md`], "utf-8");
872
+ }
873
+ }
874
+ try {
875
+ registerBinSymlinks(remoteEmp.name);
876
+ } catch {
791
877
  }
792
878
  }
793
- try {
794
- registerBinSymlinks(remoteEmp.name);
795
- } catch {
879
+ let removed = 0;
880
+ if (remote.deletedNames && remote.deletedNames.length > 0) {
881
+ const toRemove = new Set(remote.deletedNames);
882
+ const filtered = localEmployees.filter((e) => !toRemove.has(e.name));
883
+ removed = localEmployees.length - filtered.length;
884
+ if (removed > 0) {
885
+ localEmployees.length = 0;
886
+ localEmployees.push(...filtered);
887
+ }
796
888
  }
797
- }
798
- if (added > 0) {
799
- await saveEmployees(localEmployees, rosterPath);
800
- }
801
- if (remote.config && Object.keys(remote.config).length > 0) {
802
- try {
803
- mergeConfig(remote.config, paths?.configPath);
804
- } catch {
889
+ if (added > 0 || removed > 0) {
890
+ await saveEmployees(localEmployees, rosterPath);
805
891
  }
806
- }
807
- return { added };
892
+ if (remote.config && Object.keys(remote.config).length > 0) {
893
+ try {
894
+ mergeConfig(remote.config, paths?.configPath);
895
+ } catch {
896
+ }
897
+ }
898
+ return { added };
899
+ });
808
900
  }
809
901
  async function cloudPushBlob(route, data, metaKey, config) {
810
902
  if (data.length === 0) return { ok: true };
@@ -872,7 +964,7 @@ async function cloudPullBlob(route, config) {
872
964
  }
873
965
  async function cloudPushBehaviors(config) {
874
966
  const client = getClient();
875
- const result = await client.execute("SELECT * FROM behaviors");
967
+ const result = await client.execute("SELECT * FROM behaviors LIMIT 10000");
876
968
  const rows = result.rows;
877
969
  const { ok } = await cloudPushBlob(
878
970
  "/sync/push-behaviors",
@@ -920,13 +1012,13 @@ async function cloudPullBehaviors(config) {
920
1012
  async function cloudPushGraphRAG(config) {
921
1013
  const client = getClient();
922
1014
  const [entities, relationships, aliases, entityMems, relMems, hyperedges, hyperedgeNodes] = await Promise.all([
923
- client.execute("SELECT * FROM entities"),
924
- client.execute("SELECT * FROM relationships"),
925
- client.execute("SELECT * FROM entity_aliases"),
926
- client.execute("SELECT * FROM entity_memories"),
927
- client.execute("SELECT * FROM relationship_memories"),
928
- client.execute("SELECT * FROM hyperedges"),
929
- client.execute("SELECT * FROM hyperedge_nodes")
1015
+ client.execute("SELECT * FROM entities LIMIT 50000"),
1016
+ client.execute("SELECT * FROM relationships LIMIT 50000"),
1017
+ client.execute("SELECT * FROM entity_aliases LIMIT 50000"),
1018
+ client.execute("SELECT * FROM entity_memories LIMIT 50000"),
1019
+ client.execute("SELECT * FROM relationship_memories LIMIT 50000"),
1020
+ client.execute("SELECT * FROM hyperedges LIMIT 50000"),
1021
+ client.execute("SELECT * FROM hyperedge_nodes LIMIT 50000")
930
1022
  ]);
931
1023
  const blob = {
932
1024
  entities: entities.rows,
@@ -1028,7 +1120,7 @@ async function cloudPullGraphRAG(config) {
1028
1120
  }
1029
1121
  async function cloudPushTasks(config) {
1030
1122
  const client = getClient();
1031
- const result = await client.execute("SELECT * FROM tasks");
1123
+ const result = await client.execute("SELECT * FROM tasks LIMIT 10000");
1032
1124
  const rows = result.rows;
1033
1125
  const { ok } = await cloudPushBlob(
1034
1126
  "/sync/push-tasks",
@@ -1074,7 +1166,7 @@ async function cloudPullTasks(config) {
1074
1166
  }
1075
1167
  async function cloudPushConversations(config) {
1076
1168
  const client = getClient();
1077
- const result = await client.execute("SELECT * FROM conversations");
1169
+ const result = await client.execute("SELECT * FROM conversations LIMIT 50000");
1078
1170
  const rows = result.rows;
1079
1171
  const { ok } = await cloudPushBlob(
1080
1172
  "/sync/push-conversations",
@@ -1124,8 +1216,8 @@ async function cloudPullConversations(config) {
1124
1216
  async function cloudPushDocuments(config) {
1125
1217
  const client = getClient();
1126
1218
  const [workspaces, documents] = await Promise.all([
1127
- client.execute("SELECT * FROM workspaces"),
1128
- client.execute("SELECT * FROM documents")
1219
+ client.execute("SELECT * FROM workspaces LIMIT 1000"),
1220
+ client.execute("SELECT * FROM documents LIMIT 10000")
1129
1221
  ]);
1130
1222
  const blob = {
1131
1223
  workspaces: workspaces.rows,
@@ -1179,6 +1271,7 @@ async function cloudPullDocuments(config) {
1179
1271
  return { pulled };
1180
1272
  }
1181
1273
  export {
1274
+ assertSecureEndpoint,
1182
1275
  buildRosterBlob,
1183
1276
  cloudPull,
1184
1277
  cloudPullBehaviors,
@@ -1198,5 +1291,6 @@ export {
1198
1291
  cloudPushTasks,
1199
1292
  cloudSync,
1200
1293
  mergeConfig,
1201
- mergeRosterFromRemote
1294
+ mergeRosterFromRemote,
1295
+ recordRosterDeletion
1202
1296
  };
@@ -1,5 +1,5 @@
1
1
  // src/lib/config.ts
2
- import { readFile, writeFile, mkdir } from "fs/promises";
2
+ import { readFile, writeFile, mkdir, chmod } from "fs/promises";
3
3
  import { readFileSync, existsSync, renameSync } from "fs";
4
4
  import path from "path";
5
5
  import os from "os";
@@ -204,6 +204,9 @@ async function saveConfig(config) {
204
204
  await mkdir(dir, { recursive: true });
205
205
  const configPath = path.join(dir, "config.json");
206
206
  await writeFile(configPath, JSON.stringify(config, null, 2) + "\n");
207
+ if (config.cloud?.apiKey) {
208
+ await chmod(configPath, 384);
209
+ }
207
210
  }
208
211
  async function loadConfigFrom(configPath) {
209
212
  const raw = await readFile(configPath, "utf-8");