@askexenow/exe-os 0.8.36 → 0.8.37

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 (62) hide show
  1. package/dist/bin/backfill-conversations.js +9 -1
  2. package/dist/bin/backfill-responses.js +9 -1
  3. package/dist/bin/cli.js +255 -31
  4. package/dist/bin/exe-assign.js +9 -1
  5. package/dist/bin/exe-boot.js +554 -23
  6. package/dist/bin/exe-dispatch.js +43 -3
  7. package/dist/bin/exe-export-behaviors.js +7 -0
  8. package/dist/bin/exe-gateway.js +57 -9
  9. package/dist/bin/exe-heartbeat.js +2 -1
  10. package/dist/bin/exe-kill.js +7 -0
  11. package/dist/bin/exe-launch-agent.js +8 -1
  12. package/dist/bin/exe-link.js +503 -12
  13. package/dist/bin/exe-pending-messages.js +2 -1
  14. package/dist/bin/exe-pending-reviews.js +2 -1
  15. package/dist/bin/exe-review.js +9 -1
  16. package/dist/bin/exe-search.js +9 -1
  17. package/dist/bin/exe-session-cleanup.js +11 -2
  18. package/dist/bin/git-sweep.js +9 -1
  19. package/dist/bin/graph-backfill.js +7 -0
  20. package/dist/bin/graph-export.js +7 -0
  21. package/dist/bin/install.js +35 -5
  22. package/dist/bin/scan-tasks.js +9 -1
  23. package/dist/bin/shard-migrate.js +7 -0
  24. package/dist/bin/wiki-sync.js +7 -0
  25. package/dist/gateway/index.js +57 -9
  26. package/dist/hooks/bug-report-worker.js +45 -5
  27. package/dist/hooks/commit-complete.js +9 -1
  28. package/dist/hooks/error-recall.js +10 -2
  29. package/dist/hooks/exe-heartbeat-hook.js +1 -1
  30. package/dist/hooks/ingest-worker.js +56 -8
  31. package/dist/hooks/ingest.js +1 -1
  32. package/dist/hooks/instructions-loaded.js +10 -2
  33. package/dist/hooks/notification.js +10 -2
  34. package/dist/hooks/post-compact.js +10 -2
  35. package/dist/hooks/pre-compact.js +10 -2
  36. package/dist/hooks/pre-tool-use.js +10 -2
  37. package/dist/hooks/prompt-ingest-worker.js +9 -1
  38. package/dist/hooks/prompt-submit.js +56 -8
  39. package/dist/hooks/response-ingest-worker.js +9 -1
  40. package/dist/hooks/session-end.js +10 -2
  41. package/dist/hooks/session-start.js +10 -2
  42. package/dist/hooks/stop.js +10 -2
  43. package/dist/hooks/subagent-stop.js +10 -2
  44. package/dist/hooks/summary-worker.js +512 -13
  45. package/dist/index.js +65 -15
  46. package/dist/lib/cloud-sync.js +502 -11
  47. package/dist/lib/exe-daemon.js +73 -23
  48. package/dist/lib/hybrid-search.js +9 -1
  49. package/dist/lib/messaging.js +43 -3
  50. package/dist/lib/store.js +9 -1
  51. package/dist/lib/tasks.js +47 -7
  52. package/dist/lib/tmux-routing.js +45 -3
  53. package/dist/mcp/server.js +73 -16
  54. package/dist/mcp/tools/create-task.js +48 -8
  55. package/dist/mcp/tools/deactivate-behavior.js +1 -1
  56. package/dist/mcp/tools/list-tasks.js +2 -1
  57. package/dist/mcp/tools/send-message.js +46 -6
  58. package/dist/mcp/tools/update-task.js +3 -2
  59. package/dist/runtime/index.js +54 -4
  60. package/dist/tui/App.js +54 -4
  61. package/package.json +2 -2
  62. package/src/commands/exe/afk.md +116 -0
@@ -623,10 +623,23 @@ var cloud_sync_exports = {};
623
623
  __export(cloud_sync_exports, {
624
624
  buildRosterBlob: () => buildRosterBlob,
625
625
  cloudPull: () => cloudPull,
626
+ cloudPullBehaviors: () => cloudPullBehaviors,
627
+ cloudPullBlob: () => cloudPullBlob,
628
+ cloudPullConversations: () => cloudPullConversations,
629
+ cloudPullDocuments: () => cloudPullDocuments,
630
+ cloudPullGraphRAG: () => cloudPullGraphRAG,
626
631
  cloudPullRoster: () => cloudPullRoster,
632
+ cloudPullTasks: () => cloudPullTasks,
627
633
  cloudPush: () => cloudPush,
634
+ cloudPushBehaviors: () => cloudPushBehaviors,
635
+ cloudPushBlob: () => cloudPushBlob,
636
+ cloudPushConversations: () => cloudPushConversations,
637
+ cloudPushDocuments: () => cloudPushDocuments,
638
+ cloudPushGraphRAG: () => cloudPushGraphRAG,
628
639
  cloudPushRoster: () => cloudPushRoster,
640
+ cloudPushTasks: () => cloudPushTasks,
629
641
  cloudSync: () => cloudSync,
642
+ mergeConfig: () => mergeConfig,
630
643
  mergeRosterFromRemote: () => mergeRosterFromRemote
631
644
  });
632
645
  import { readFileSync as readFileSync5, writeFileSync as writeFileSync2, existsSync as existsSync6, readdirSync, mkdirSync as mkdirSync2, appendFileSync } from "fs";
@@ -640,6 +653,17 @@ function logError(msg) {
640
653
  } catch {
641
654
  }
642
655
  }
656
+ async function fetchWithRetry(url, init) {
657
+ const attempt = async () => {
658
+ const signal = AbortSignal.timeout(FETCH_TIMEOUT_MS);
659
+ return fetch(url, { ...init, signal });
660
+ };
661
+ const resp = await attempt();
662
+ if (resp.status >= 500) {
663
+ return attempt();
664
+ }
665
+ return resp;
666
+ }
643
667
  function assertSecureEndpoint(endpoint) {
644
668
  if (endpoint.startsWith("https://")) return;
645
669
  if (endpoint.startsWith("http://")) {
@@ -661,7 +685,7 @@ async function cloudPush(records, maxVersion, config) {
661
685
  const json = JSON.stringify(records);
662
686
  const compressed = compress(Buffer.from(json, "utf8"));
663
687
  const blob = encryptSyncBlob(compressed);
664
- const resp = await fetch(`${config.endpoint}/sync/push`, {
688
+ const resp = await fetchWithRetry(`${config.endpoint}/sync/push`, {
665
689
  method: "POST",
666
690
  headers: {
667
691
  Authorization: `Bearer ${config.apiKey}`,
@@ -679,7 +703,7 @@ async function cloudPush(records, maxVersion, config) {
679
703
  async function cloudPull(sinceVersion, config) {
680
704
  assertSecureEndpoint(config.endpoint);
681
705
  try {
682
- const response = await fetch(`${config.endpoint}/sync/pull`, {
706
+ const response = await fetchWithRetry(`${config.endpoint}/sync/pull`, {
683
707
  method: "POST",
684
708
  headers: {
685
709
  Authorization: `Bearer ${config.apiKey}`,
@@ -795,20 +819,87 @@ async function cloudSync(config) {
795
819
  try {
796
820
  await cloudPushRoster(config);
797
821
  } catch (err) {
798
- process.stderr.write(`[cloud-sync] Roster push warning: ${err instanceof Error ? err.message : String(err)}
799
- `);
822
+ logError(`[cloud-sync] Roster push: ${err instanceof Error ? err.message : String(err)}`);
800
823
  }
801
824
  try {
802
825
  await cloudPullRoster(config);
803
826
  } catch (err) {
804
- process.stderr.write(`[cloud-sync] Roster pull warning: ${err instanceof Error ? err.message : String(err)}
805
- `);
827
+ logError(`[cloud-sync] Roster pull: ${err instanceof Error ? err.message : String(err)}`);
828
+ }
829
+ let behaviorsResult = { pushed: false, pulled: 0 };
830
+ try {
831
+ behaviorsResult.pushed = await cloudPushBehaviors(config);
832
+ } catch (err) {
833
+ logError(`[cloud-sync] Behaviors push: ${err instanceof Error ? err.message : String(err)}`);
834
+ }
835
+ try {
836
+ const pullResult2 = await cloudPullBehaviors(config);
837
+ behaviorsResult.pulled = pullResult2.pulled;
838
+ } catch (err) {
839
+ logError(`[cloud-sync] Behaviors pull: ${err instanceof Error ? err.message : String(err)}`);
840
+ }
841
+ let graphragResult = { pushed: false, pulled: 0 };
842
+ try {
843
+ graphragResult.pushed = await cloudPushGraphRAG(config);
844
+ } catch (err) {
845
+ logError(`[cloud-sync] GraphRAG push: ${err instanceof Error ? err.message : String(err)}`);
846
+ }
847
+ try {
848
+ const pullResult2 = await cloudPullGraphRAG(config);
849
+ graphragResult.pulled = pullResult2.pulled;
850
+ } catch (err) {
851
+ logError(`[cloud-sync] GraphRAG pull: ${err instanceof Error ? err.message : String(err)}`);
852
+ }
853
+ let tasksResult = { pushed: false, pulled: 0 };
854
+ try {
855
+ tasksResult.pushed = await cloudPushTasks(config);
856
+ } catch (err) {
857
+ logError(`[cloud-sync] Tasks push: ${err instanceof Error ? err.message : String(err)}`);
806
858
  }
807
- return { pushed, pulled };
859
+ try {
860
+ const pullResult2 = await cloudPullTasks(config);
861
+ tasksResult.pulled = pullResult2.pulled;
862
+ } catch (err) {
863
+ logError(`[cloud-sync] Tasks pull: ${err instanceof Error ? err.message : String(err)}`);
864
+ }
865
+ let conversationsResult = { pushed: false, pulled: 0 };
866
+ try {
867
+ conversationsResult.pushed = await cloudPushConversations(config);
868
+ } catch (err) {
869
+ logError(`[cloud-sync] Conversations push: ${err instanceof Error ? err.message : String(err)}`);
870
+ }
871
+ try {
872
+ const pullResult2 = await cloudPullConversations(config);
873
+ conversationsResult.pulled = pullResult2.pulled;
874
+ } catch (err) {
875
+ logError(`[cloud-sync] Conversations pull: ${err instanceof Error ? err.message : String(err)}`);
876
+ }
877
+ let documentsResult = { pushed: false, pulled: 0 };
878
+ try {
879
+ documentsResult.pushed = await cloudPushDocuments(config);
880
+ } catch (err) {
881
+ logError(`[cloud-sync] Documents push: ${err instanceof Error ? err.message : String(err)}`);
882
+ }
883
+ try {
884
+ const pullResult2 = await cloudPullDocuments(config);
885
+ documentsResult.pulled = pullResult2.pulled;
886
+ } catch (err) {
887
+ logError(`[cloud-sync] Documents pull: ${err instanceof Error ? err.message : String(err)}`);
888
+ }
889
+ return {
890
+ pushed,
891
+ pulled,
892
+ behaviors: behaviorsResult,
893
+ graphrag: graphragResult,
894
+ tasks: tasksResult,
895
+ conversations: conversationsResult,
896
+ documents: documentsResult
897
+ };
808
898
  }
809
899
  function buildRosterBlob(paths) {
810
900
  const rosterPath = paths?.rosterPath ?? path6.join(EXE_AI_DIR, "exe-employees.json");
811
901
  const identityDir = paths?.identityDir ?? path6.join(EXE_AI_DIR, "identity");
902
+ const configPath = paths?.configPath ?? path6.join(EXE_AI_DIR, "config.json");
812
903
  let roster = [];
813
904
  if (existsSync6(rosterPath)) {
814
905
  try {
@@ -825,9 +916,16 @@ function buildRosterBlob(paths) {
825
916
  }
826
917
  }
827
918
  }
828
- const content = JSON.stringify({ roster, identities });
919
+ let config;
920
+ if (existsSync6(configPath)) {
921
+ try {
922
+ config = JSON.parse(readFileSync5(configPath, "utf-8"));
923
+ } catch {
924
+ }
925
+ }
926
+ const content = JSON.stringify({ roster, identities, config });
829
927
  const hash = Buffer.from(content).length;
830
- return { roster, identities, version: hash };
928
+ return { roster, identities, config, version: hash };
831
929
  }
832
930
  async function cloudPushRoster(config) {
833
931
  assertSecureEndpoint(config.endpoint);
@@ -846,7 +944,7 @@ async function cloudPushRoster(config) {
846
944
  const json = JSON.stringify(blob);
847
945
  const compressed = compress(Buffer.from(json, "utf8"));
848
946
  const encrypted = encryptSyncBlob(compressed);
849
- const resp = await fetch(`${config.endpoint}/sync/push-roster`, {
947
+ const resp = await fetchWithRetry(`${config.endpoint}/sync/push-roster`, {
850
948
  method: "POST",
851
949
  headers: {
852
950
  Authorization: `Bearer ${config.apiKey}`,
@@ -875,7 +973,7 @@ async function cloudPushRoster(config) {
875
973
  async function cloudPullRoster(config) {
876
974
  assertSecureEndpoint(config.endpoint);
877
975
  try {
878
- const resp = await fetch(`${config.endpoint}/sync/pull-roster`, {
976
+ const resp = await fetchWithRetry(`${config.endpoint}/sync/pull-roster`, {
879
977
  method: "GET",
880
978
  headers: {
881
979
  Authorization: `Bearer ${config.apiKey}`,
@@ -895,6 +993,20 @@ async function cloudPullRoster(config) {
895
993
  return { added: 0 };
896
994
  }
897
995
  }
996
+ function mergeConfig(remoteConfig, configPath) {
997
+ const cfgPath = configPath ?? path6.join(EXE_AI_DIR, "config.json");
998
+ let local = {};
999
+ if (existsSync6(cfgPath)) {
1000
+ try {
1001
+ local = JSON.parse(readFileSync5(cfgPath, "utf-8"));
1002
+ } catch {
1003
+ }
1004
+ }
1005
+ const merged = { ...remoteConfig, ...local };
1006
+ const dir = path6.dirname(cfgPath);
1007
+ if (!existsSync6(dir)) mkdirSync2(dir, { recursive: true });
1008
+ writeFileSync2(cfgPath, JSON.stringify(merged, null, 2), "utf-8");
1009
+ }
898
1010
  async function mergeRosterFromRemote(remote, paths) {
899
1011
  const rosterPath = paths?.rosterPath ?? void 0;
900
1012
  const identityDir = paths?.identityDir ?? path6.join(EXE_AI_DIR, "identity");
@@ -921,9 +1033,387 @@ async function mergeRosterFromRemote(remote, paths) {
921
1033
  if (added > 0) {
922
1034
  await saveEmployees(localEmployees, rosterPath);
923
1035
  }
1036
+ if (remote.config && Object.keys(remote.config).length > 0) {
1037
+ try {
1038
+ mergeConfig(remote.config, paths?.configPath);
1039
+ } catch {
1040
+ }
1041
+ }
924
1042
  return { added };
925
1043
  }
926
- var LOCALHOST_PATTERNS;
1044
+ async function cloudPushBlob(route, data, metaKey, config) {
1045
+ if (data.length === 0) return { ok: true };
1046
+ assertSecureEndpoint(config.endpoint);
1047
+ const json = JSON.stringify(data);
1048
+ const version = Buffer.from(json).length;
1049
+ try {
1050
+ const client = getClient();
1051
+ const meta = await client.execute({
1052
+ sql: "SELECT value FROM sync_meta WHERE key = ?",
1053
+ args: [metaKey]
1054
+ });
1055
+ const lastVersion = meta.rows.length > 0 ? Number(meta.rows[0].value) : 0;
1056
+ if (version === lastVersion) return { ok: true };
1057
+ } catch {
1058
+ }
1059
+ try {
1060
+ const compressed = compress(Buffer.from(json, "utf8"));
1061
+ const encrypted = encryptSyncBlob(compressed);
1062
+ const resp = await fetchWithRetry(`${config.endpoint}${route}`, {
1063
+ method: "POST",
1064
+ headers: {
1065
+ Authorization: `Bearer ${config.apiKey}`,
1066
+ "Content-Type": "application/json",
1067
+ "X-Device-Id": loadDeviceId()
1068
+ },
1069
+ body: JSON.stringify({ blob: encrypted })
1070
+ });
1071
+ if (resp.ok) {
1072
+ try {
1073
+ const client = getClient();
1074
+ await client.execute({
1075
+ sql: "INSERT OR REPLACE INTO sync_meta (key, value) VALUES (?, ?)",
1076
+ args: [metaKey, String(version)]
1077
+ });
1078
+ } catch {
1079
+ }
1080
+ }
1081
+ return { ok: resp.ok };
1082
+ } catch (err) {
1083
+ logError(`[cloud-sync] PUSH ${route}: ${err instanceof Error ? err.message : String(err)}`);
1084
+ return { ok: false };
1085
+ }
1086
+ }
1087
+ async function cloudPullBlob(route, config) {
1088
+ assertSecureEndpoint(config.endpoint);
1089
+ try {
1090
+ const resp = await fetchWithRetry(`${config.endpoint}${route}`, {
1091
+ method: "GET",
1092
+ headers: {
1093
+ Authorization: `Bearer ${config.apiKey}`,
1094
+ "X-Device-Id": loadDeviceId()
1095
+ }
1096
+ });
1097
+ if (!resp.ok) return null;
1098
+ const data = await resp.json();
1099
+ if (!data.blob) return null;
1100
+ const compressed = decryptSyncBlob(data.blob);
1101
+ const json = decompress(compressed).toString("utf8");
1102
+ return JSON.parse(json);
1103
+ } catch (err) {
1104
+ logError(`[cloud-sync] PULL ${route}: ${err instanceof Error ? err.message : String(err)}`);
1105
+ return null;
1106
+ }
1107
+ }
1108
+ async function cloudPushBehaviors(config) {
1109
+ const client = getClient();
1110
+ const result = await client.execute("SELECT * FROM behaviors");
1111
+ const rows = result.rows;
1112
+ const { ok } = await cloudPushBlob(
1113
+ "/sync/push-behaviors",
1114
+ rows,
1115
+ "last_behaviors_push_version",
1116
+ config
1117
+ );
1118
+ return ok;
1119
+ }
1120
+ async function cloudPullBehaviors(config) {
1121
+ const remoteBehaviors = await cloudPullBlob(
1122
+ "/sync/pull-behaviors",
1123
+ config
1124
+ );
1125
+ if (!remoteBehaviors || remoteBehaviors.length === 0) return { pulled: 0 };
1126
+ const client = getClient();
1127
+ let pulled = 0;
1128
+ for (const behavior of remoteBehaviors) {
1129
+ const existing = await client.execute({
1130
+ sql: `SELECT COUNT(*) as cnt FROM behaviors
1131
+ WHERE agent_id = ? AND content = ?`,
1132
+ args: [behavior.agent_id, behavior.content]
1133
+ });
1134
+ if (Number(existing.rows[0]?.cnt) > 0) continue;
1135
+ await client.execute({
1136
+ sql: `INSERT OR IGNORE INTO behaviors
1137
+ (id, agent_id, project_name, domain, content, active, priority, created_at, updated_at)
1138
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
1139
+ args: [
1140
+ behavior.id,
1141
+ behavior.agent_id,
1142
+ behavior.project_name ?? null,
1143
+ behavior.domain ?? null,
1144
+ behavior.content,
1145
+ behavior.active,
1146
+ behavior.priority ?? "p1",
1147
+ behavior.created_at,
1148
+ behavior.updated_at
1149
+ ]
1150
+ });
1151
+ pulled++;
1152
+ }
1153
+ return { pulled };
1154
+ }
1155
+ async function cloudPushGraphRAG(config) {
1156
+ const client = getClient();
1157
+ const [entities, relationships, aliases, entityMems, relMems, hyperedges, hyperedgeNodes] = await Promise.all([
1158
+ client.execute("SELECT * FROM entities"),
1159
+ client.execute("SELECT * FROM relationships"),
1160
+ client.execute("SELECT * FROM entity_aliases"),
1161
+ client.execute("SELECT * FROM entity_memories"),
1162
+ client.execute("SELECT * FROM relationship_memories"),
1163
+ client.execute("SELECT * FROM hyperedges"),
1164
+ client.execute("SELECT * FROM hyperedge_nodes")
1165
+ ]);
1166
+ const blob = {
1167
+ entities: entities.rows,
1168
+ relationships: relationships.rows,
1169
+ entity_aliases: aliases.rows,
1170
+ entity_memories: entityMems.rows,
1171
+ relationship_memories: relMems.rows,
1172
+ hyperedges: hyperedges.rows,
1173
+ hyperedge_nodes: hyperedgeNodes.rows
1174
+ };
1175
+ const { ok } = await cloudPushBlob(
1176
+ "/sync/push-graphrag",
1177
+ [blob],
1178
+ "last_graphrag_push_version",
1179
+ config
1180
+ );
1181
+ return ok;
1182
+ }
1183
+ async function cloudPullGraphRAG(config) {
1184
+ const data = await cloudPullBlob(
1185
+ "/sync/pull-graphrag",
1186
+ config
1187
+ );
1188
+ if (!data || data.length === 0) return { pulled: 0 };
1189
+ const blob = data[0];
1190
+ const client = getClient();
1191
+ let pulled = 0;
1192
+ if (blob.entities.length > 0) {
1193
+ const stmts = blob.entities.map((e) => ({
1194
+ sql: `INSERT OR IGNORE INTO entities (id, name, type, first_seen, last_seen, properties)
1195
+ VALUES (?, ?, ?, ?, ?, ?)`,
1196
+ args: [e.id, e.name, e.type, e.first_seen, e.last_seen, e.properties ?? "{}"]
1197
+ }));
1198
+ await client.batch(stmts, "write");
1199
+ pulled += stmts.length;
1200
+ }
1201
+ if (blob.relationships.length > 0) {
1202
+ const stmts = blob.relationships.map((r) => ({
1203
+ sql: `INSERT OR IGNORE INTO relationships
1204
+ (id, source_entity_id, target_entity_id, type, weight, timestamp, properties, confidence, confidence_label)
1205
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
1206
+ args: [
1207
+ r.id,
1208
+ r.source_entity_id,
1209
+ r.target_entity_id,
1210
+ r.type,
1211
+ r.weight ?? 1,
1212
+ r.timestamp,
1213
+ r.properties ?? "{}",
1214
+ r.confidence ?? 1,
1215
+ r.confidence_label ?? "extracted"
1216
+ ]
1217
+ }));
1218
+ await client.batch(stmts, "write");
1219
+ pulled += stmts.length;
1220
+ }
1221
+ if (blob.entity_aliases.length > 0) {
1222
+ const stmts = blob.entity_aliases.map((a) => ({
1223
+ sql: `INSERT OR IGNORE INTO entity_aliases (alias, canonical_entity_id) VALUES (?, ?)`,
1224
+ args: [a.alias, a.canonical_entity_id]
1225
+ }));
1226
+ await client.batch(stmts, "write");
1227
+ pulled += stmts.length;
1228
+ }
1229
+ if (blob.entity_memories.length > 0) {
1230
+ const stmts = blob.entity_memories.map((em) => ({
1231
+ sql: `INSERT OR IGNORE INTO entity_memories (entity_id, memory_id) VALUES (?, ?)`,
1232
+ args: [em.entity_id, em.memory_id]
1233
+ }));
1234
+ await client.batch(stmts, "write");
1235
+ pulled += stmts.length;
1236
+ }
1237
+ if (blob.relationship_memories.length > 0) {
1238
+ const stmts = blob.relationship_memories.map((rm) => ({
1239
+ sql: `INSERT OR IGNORE INTO relationship_memories (relationship_id, memory_id) VALUES (?, ?)`,
1240
+ args: [rm.relationship_id, rm.memory_id]
1241
+ }));
1242
+ await client.batch(stmts, "write");
1243
+ pulled += stmts.length;
1244
+ }
1245
+ if (blob.hyperedges.length > 0) {
1246
+ const stmts = blob.hyperedges.map((h) => ({
1247
+ sql: `INSERT OR IGNORE INTO hyperedges (id, label, relation, confidence, timestamp)
1248
+ VALUES (?, ?, ?, ?, ?)`,
1249
+ args: [h.id, h.label, h.relation, h.confidence ?? 1, h.timestamp]
1250
+ }));
1251
+ await client.batch(stmts, "write");
1252
+ pulled += stmts.length;
1253
+ }
1254
+ if (blob.hyperedge_nodes.length > 0) {
1255
+ const stmts = blob.hyperedge_nodes.map((hn) => ({
1256
+ sql: `INSERT OR IGNORE INTO hyperedge_nodes (hyperedge_id, entity_id) VALUES (?, ?)`,
1257
+ args: [hn.hyperedge_id, hn.entity_id]
1258
+ }));
1259
+ await client.batch(stmts, "write");
1260
+ pulled += stmts.length;
1261
+ }
1262
+ return { pulled };
1263
+ }
1264
+ async function cloudPushTasks(config) {
1265
+ const client = getClient();
1266
+ const result = await client.execute("SELECT * FROM tasks");
1267
+ const rows = result.rows;
1268
+ const { ok } = await cloudPushBlob(
1269
+ "/sync/push-tasks",
1270
+ rows,
1271
+ "last_tasks_push_version",
1272
+ config
1273
+ );
1274
+ return ok;
1275
+ }
1276
+ async function cloudPullTasks(config) {
1277
+ const remoteTasks = await cloudPullBlob(
1278
+ "/sync/pull-tasks",
1279
+ config
1280
+ );
1281
+ if (!remoteTasks || remoteTasks.length === 0) return { pulled: 0 };
1282
+ const client = getClient();
1283
+ const stmts = remoteTasks.map((t) => ({
1284
+ sql: `INSERT OR IGNORE INTO tasks
1285
+ (id, title, assigned_to, assigned_by, project_name, priority, status, task_file, created_at, updated_at,
1286
+ blocked_by, parent_task_id, budget_tokens, budget_fallback_model, tokens_used, tokens_warned_at)
1287
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
1288
+ args: [
1289
+ t.id,
1290
+ t.title,
1291
+ t.assigned_to,
1292
+ t.assigned_by,
1293
+ t.project_name,
1294
+ t.priority ?? "p1",
1295
+ t.status ?? "open",
1296
+ t.task_file ?? null,
1297
+ t.created_at,
1298
+ t.updated_at,
1299
+ t.blocked_by ?? null,
1300
+ t.parent_task_id ?? null,
1301
+ t.budget_tokens ?? null,
1302
+ t.budget_fallback_model ?? null,
1303
+ t.tokens_used ?? 0,
1304
+ t.tokens_warned_at ?? null
1305
+ ]
1306
+ }));
1307
+ await client.batch(stmts, "write");
1308
+ return { pulled: remoteTasks.length };
1309
+ }
1310
+ async function cloudPushConversations(config) {
1311
+ const client = getClient();
1312
+ const result = await client.execute("SELECT * FROM conversations");
1313
+ const rows = result.rows;
1314
+ const { ok } = await cloudPushBlob(
1315
+ "/sync/push-conversations",
1316
+ rows,
1317
+ "last_conversations_push_version",
1318
+ config
1319
+ );
1320
+ return ok;
1321
+ }
1322
+ async function cloudPullConversations(config) {
1323
+ const remoteConvos = await cloudPullBlob(
1324
+ "/sync/pull-conversations",
1325
+ config
1326
+ );
1327
+ if (!remoteConvos || remoteConvos.length === 0) return { pulled: 0 };
1328
+ const client = getClient();
1329
+ const stmts = remoteConvos.map((c) => ({
1330
+ sql: `INSERT OR IGNORE INTO conversations
1331
+ (id, platform, external_id, sender_id, sender_name, sender_phone, sender_email,
1332
+ recipient_id, channel_id, thread_id, reply_to_id, content_text, content_media,
1333
+ content_metadata, agent_response, agent_name, timestamp, ingested_at)
1334
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
1335
+ args: [
1336
+ c.id,
1337
+ c.platform,
1338
+ c.external_id ?? null,
1339
+ c.sender_id,
1340
+ c.sender_name ?? null,
1341
+ c.sender_phone ?? null,
1342
+ c.sender_email ?? null,
1343
+ c.recipient_id ?? null,
1344
+ c.channel_id,
1345
+ c.thread_id ?? null,
1346
+ c.reply_to_id ?? null,
1347
+ c.content_text ?? null,
1348
+ c.content_media ?? null,
1349
+ c.content_metadata ?? null,
1350
+ c.agent_response ?? null,
1351
+ c.agent_name ?? null,
1352
+ c.timestamp,
1353
+ c.ingested_at
1354
+ ]
1355
+ }));
1356
+ await client.batch(stmts, "write");
1357
+ return { pulled: remoteConvos.length };
1358
+ }
1359
+ async function cloudPushDocuments(config) {
1360
+ const client = getClient();
1361
+ const [workspaces, documents] = await Promise.all([
1362
+ client.execute("SELECT * FROM workspaces"),
1363
+ client.execute("SELECT * FROM documents")
1364
+ ]);
1365
+ const blob = {
1366
+ workspaces: workspaces.rows,
1367
+ documents: documents.rows
1368
+ };
1369
+ const { ok } = await cloudPushBlob(
1370
+ "/sync/push-documents",
1371
+ [blob],
1372
+ "last_documents_push_version",
1373
+ config
1374
+ );
1375
+ return ok;
1376
+ }
1377
+ async function cloudPullDocuments(config) {
1378
+ const data = await cloudPullBlob(
1379
+ "/sync/pull-documents",
1380
+ config
1381
+ );
1382
+ if (!data || data.length === 0) return { pulled: 0 };
1383
+ const blob = data[0];
1384
+ const client = getClient();
1385
+ let pulled = 0;
1386
+ if (blob.workspaces.length > 0) {
1387
+ const stmts = blob.workspaces.map((w) => ({
1388
+ sql: `INSERT OR IGNORE INTO workspaces (id, slug, name, owner_agent_id, created_at, metadata)
1389
+ VALUES (?, ?, ?, ?, ?, ?)`,
1390
+ args: [w.id, w.slug, w.name, w.owner_agent_id ?? null, w.created_at, w.metadata ?? null]
1391
+ }));
1392
+ await client.batch(stmts, "write");
1393
+ pulled += stmts.length;
1394
+ }
1395
+ if (blob.documents.length > 0) {
1396
+ const stmts = blob.documents.map((d) => ({
1397
+ sql: `INSERT OR IGNORE INTO documents
1398
+ (id, workspace_id, filename, mime, source_type, user_id, uploaded_at, metadata)
1399
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
1400
+ args: [
1401
+ d.id,
1402
+ d.workspace_id,
1403
+ d.filename,
1404
+ d.mime ?? null,
1405
+ d.source_type ?? null,
1406
+ d.user_id ?? null,
1407
+ d.uploaded_at,
1408
+ d.metadata ?? null
1409
+ ]
1410
+ }));
1411
+ await client.batch(stmts, "write");
1412
+ pulled += stmts.length;
1413
+ }
1414
+ return { pulled };
1415
+ }
1416
+ var LOCALHOST_PATTERNS, FETCH_TIMEOUT_MS;
927
1417
  var init_cloud_sync = __esm({
928
1418
  "src/lib/cloud-sync.ts"() {
929
1419
  "use strict";
@@ -935,6 +1425,7 @@ var init_cloud_sync = __esm({
935
1425
  init_config();
936
1426
  init_employees();
937
1427
  LOCALHOST_PATTERNS = /^(localhost|127\.0\.0\.1|\[::1\])$/i;
1428
+ FETCH_TIMEOUT_MS = 3e4;
938
1429
  }
939
1430
  });
940
1431
 
@@ -1450,7 +1450,7 @@ var init_plan_limits = __esm({
1450
1450
  import path9 from "path";
1451
1451
  import os4 from "os";
1452
1452
  import { fileURLToPath } from "url";
1453
- var SESSION_CACHE, INTERCOM_LOG2, DEBOUNCE_FILE, DEBOUNCE_CLEANUP_AGE_MS;
1453
+ var SPAWN_LOCK_DIR, SESSION_CACHE, INTERCOM_LOG2, DEBOUNCE_FILE, DEBOUNCE_CLEANUP_AGE_MS;
1454
1454
  var init_tmux_routing = __esm({
1455
1455
  "src/lib/tmux-routing.ts"() {
1456
1456
  "use strict";
@@ -1462,6 +1462,7 @@ var init_tmux_routing = __esm({
1462
1462
  init_provider_table();
1463
1463
  init_intercom_queue();
1464
1464
  init_plan_limits();
1465
+ SPAWN_LOCK_DIR = path9.join(os4.homedir(), ".exe-os", "spawn-locks");
1465
1466
  SESSION_CACHE = path9.join(os4.homedir(), ".exe-os", "session-cache");
1466
1467
  INTERCOM_LOG2 = path9.join(os4.homedir(), ".exe-os", "intercom.log");
1467
1468
  DEBOUNCE_FILE = path9.join(SESSION_CACHE, "intercom-debounce.json");
@@ -1482,7 +1482,7 @@ var init_plan_limits = __esm({
1482
1482
  import path11 from "path";
1483
1483
  import os5 from "os";
1484
1484
  import { fileURLToPath as fileURLToPath2 } from "url";
1485
- var SESSION_CACHE, INTERCOM_LOG2, DEBOUNCE_FILE, DEBOUNCE_CLEANUP_AGE_MS;
1485
+ var SPAWN_LOCK_DIR, SESSION_CACHE, INTERCOM_LOG2, DEBOUNCE_FILE, DEBOUNCE_CLEANUP_AGE_MS;
1486
1486
  var init_tmux_routing = __esm({
1487
1487
  "src/lib/tmux-routing.ts"() {
1488
1488
  "use strict";
@@ -1494,6 +1494,7 @@ var init_tmux_routing = __esm({
1494
1494
  init_provider_table();
1495
1495
  init_intercom_queue();
1496
1496
  init_plan_limits();
1497
+ SPAWN_LOCK_DIR = path11.join(os5.homedir(), ".exe-os", "spawn-locks");
1497
1498
  SESSION_CACHE = path11.join(os5.homedir(), ".exe-os", "session-cache");
1498
1499
  INTERCOM_LOG2 = path11.join(os5.homedir(), ".exe-os", "intercom.log");
1499
1500
  DEBOUNCE_FILE = path11.join(SESSION_CACHE, "intercom-debounce.json");
@@ -1461,7 +1461,8 @@ async function writeMemory(record) {
1461
1461
  has_error: record.has_error ? 1 : 0,
1462
1462
  raw_text: record.raw_text,
1463
1463
  vector: record.vector,
1464
- version: _nextVersion++,
1464
+ version: 0,
1465
+ // Placeholder — assigned atomically at flush time
1465
1466
  task_id: record.task_id ?? null,
1466
1467
  importance: record.importance ?? 5,
1467
1468
  status: record.status ?? "active",
@@ -1495,6 +1496,13 @@ async function flushBatch() {
1495
1496
  _flushing = true;
1496
1497
  try {
1497
1498
  const batch = _pendingRecords.slice(0);
1499
+ const client = getClient();
1500
+ const vResult = await client.execute("SELECT MAX(version) as max_v FROM memories");
1501
+ let baseVersion = (Number(vResult.rows[0]?.max_v) || 0) + 1;
1502
+ for (const row of batch) {
1503
+ row.version = baseVersion++;
1504
+ }
1505
+ _nextVersion = baseVersion;
1498
1506
  const buildStmt = (row) => {
1499
1507
  const hasVector = row.vector !== null;
1500
1508
  const taskId = row.task_id ?? null;
@@ -1530,7 +1530,8 @@ async function writeMemory(record) {
1530
1530
  has_error: record.has_error ? 1 : 0,
1531
1531
  raw_text: record.raw_text,
1532
1532
  vector: record.vector,
1533
- version: _nextVersion++,
1533
+ version: 0,
1534
+ // Placeholder — assigned atomically at flush time
1534
1535
  task_id: record.task_id ?? null,
1535
1536
  importance: record.importance ?? 5,
1536
1537
  status: record.status ?? "active",
@@ -1564,6 +1565,13 @@ async function flushBatch() {
1564
1565
  _flushing = true;
1565
1566
  try {
1566
1567
  const batch = _pendingRecords.slice(0);
1568
+ const client = getClient();
1569
+ const vResult = await client.execute("SELECT MAX(version) as max_v FROM memories");
1570
+ let baseVersion = (Number(vResult.rows[0]?.max_v) || 0) + 1;
1571
+ for (const row of batch) {
1572
+ row.version = baseVersion++;
1573
+ }
1574
+ _nextVersion = baseVersion;
1567
1575
  const buildStmt = (row) => {
1568
1576
  const hasVector = row.vector !== null;
1569
1577
  const taskId = row.task_id ?? null;