@remnic/cli 9.43.0 → 9.45.0

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 (2) hide show
  1. package/dist/index.js +97 -11
  2. package/package.json +29 -29
package/dist/index.js CHANGED
@@ -259,6 +259,10 @@ import {
259
259
  readConvergeCursor,
260
260
  writeConvergeCursor
261
261
  } from "@remnic/core/reconcile/cursor.js";
262
+ import {
263
+ buildReconcileManifest,
264
+ collapseActiveFactDuplicates
265
+ } from "@remnic/core/reconcile/manifest.js";
262
266
 
263
267
  // src/offline-storage-io.ts
264
268
  import { mkdtemp, readdir, lstat, rm } from "fs/promises";
@@ -680,12 +684,18 @@ async function fetchPeerFileContent(peerUrl, namespace, filePath, token, fetchIm
680
684
  }
681
685
  return null;
682
686
  }
687
+ function withoutTrailingSlashes(value) {
688
+ let end = value.length;
689
+ while (end > 0 && value.charCodeAt(end - 1) === 47) end -= 1;
690
+ return value.slice(0, end);
691
+ }
683
692
  async function postPeerFileContent(peerUrl, namespace, filePath, content, metadata, token, fetchImpl = globalThis.fetch) {
684
- const base = peerUrl.replace(/\/+$/, "");
693
+ const base = withoutTrailingSlashes(peerUrl);
685
694
  const routes = [
686
695
  `/remnic/v1/offline-sync/apply-file-content?namespace=${encodeURIComponent(namespace)}`,
687
696
  `/engram/v1/offline-sync/apply-file-content?namespace=${encodeURIComponent(namespace)}`
688
697
  ];
698
+ let previousAttemptFailed = false;
689
699
  for (const route of routes) {
690
700
  try {
691
701
  let offset = 0;
@@ -717,7 +727,9 @@ async function postPeerFileContent(peerUrl, namespace, filePath, content, metada
717
727
  return false;
718
728
  }
719
729
  if (result.done) {
720
- return result.skipped || result.applied && offset + chunk.length === content.length;
730
+ if (result.skipped) return previousAttemptFailed ? "applied" : "skipped";
731
+ if (result.applied && offset + chunk.length === content.length) return "applied";
732
+ return false;
721
733
  }
722
734
  if (result.applied || result.skipped || chunk.length === 0) {
723
735
  return false;
@@ -726,6 +738,30 @@ async function postPeerFileContent(peerUrl, namespace, filePath, content, metada
726
738
  } while (offset < content.length);
727
739
  return false;
728
740
  } catch {
741
+ previousAttemptFailed = true;
742
+ }
743
+ }
744
+ return false;
745
+ }
746
+ async function postPeerConvergenceComplete(peerUrl, namespaces, token, fetchImpl = globalThis.fetch) {
747
+ const base = withoutTrailingSlashes(peerUrl);
748
+ const query = namespaces.map((namespace) => `namespace=${encodeURIComponent(namespace)}`).join("&");
749
+ const routes = [
750
+ "/remnic/v1/offline-sync/convergence-complete",
751
+ "/engram/v1/offline-sync/convergence-complete"
752
+ ];
753
+ for (const route of routes) {
754
+ const response = await fetchImpl(`${base}${route}?${query}`, {
755
+ method: "POST",
756
+ headers: {
757
+ "x-remnic-source-id": encodeURIComponent("remnic-converge"),
758
+ ...token ? { authorization: `Bearer ${token}` } : {}
759
+ }
760
+ }).catch(() => null);
761
+ if (!response?.ok) continue;
762
+ const result = await response.json().catch(() => null);
763
+ if (result && typeof result === "object" && "namespaces" in result && Array.isArray(result.namespaces) && result.namespaces.length === namespaces.length && result.namespaces.every((namespace, index) => namespace === namespaces[index]) && "refreshed" in result && result.refreshed === true) {
764
+ return true;
729
765
  }
730
766
  }
731
767
  return false;
@@ -737,6 +773,8 @@ async function computeConvergePlan(options = {}) {
737
773
  const localTombstones = /* @__PURE__ */ new Map();
738
774
  const peerMap = /* @__PURE__ */ new Map();
739
775
  const peerTombstones = /* @__PURE__ */ new Map();
776
+ const localManifests = /* @__PURE__ */ new Map();
777
+ const peerManifests = /* @__PURE__ */ new Map();
740
778
  if (options.baseFilesByNamespace) {
741
779
  for (const [ns, files] of options.baseFilesByNamespace) {
742
780
  namespacesToPlan.add(ns);
@@ -794,6 +832,26 @@ async function computeConvergePlan(options = {}) {
794
832
  bytes: record.bytes
795
833
  }));
796
834
  localMap.set(ns, files);
835
+ try {
836
+ const io = await createOfflineStorageIo(rootInfo.rootDir);
837
+ localManifests.set(
838
+ ns,
839
+ await buildReconcileManifest({
840
+ files,
841
+ readFile: async (file) => {
842
+ const readFile2 = io.readFile;
843
+ if (!readFile2) throw new Error("offline storage cannot read reconciliation manifest files");
844
+ return await readFile2({
845
+ root: rootInfo.rootDir,
846
+ path: file.path,
847
+ filePath: path2.join(rootInfo.rootDir, file.path)
848
+ });
849
+ }
850
+ })
851
+ );
852
+ } catch {
853
+ localManifests.delete(ns);
854
+ }
797
855
  const tombstones = await readLocalTombstones(rootInfo.rootDir);
798
856
  localTombstones.set(ns, tombstones);
799
857
  } catch {
@@ -801,7 +859,8 @@ async function computeConvergePlan(options = {}) {
801
859
  }
802
860
  }
803
861
  }
804
- if (!options.peerFilesByNamespace && options.peerUrl) {
862
+ const peerUrl = options.peerUrl;
863
+ if (!options.peerFilesByNamespace && peerUrl) {
805
864
  let resolvedToken;
806
865
  if (options.peerToken) {
807
866
  try {
@@ -814,9 +873,23 @@ async function computeConvergePlan(options = {}) {
814
873
  }
815
874
  const fetchFn = options.fetchImpl ?? globalThis.fetch;
816
875
  for (const ns of namespacesToPlan) {
817
- const peerData = await fetchPeerSnapshot(options.peerUrl, ns, resolvedToken, fetchFn);
876
+ const peerData = await fetchPeerSnapshot(peerUrl, ns, resolvedToken, fetchFn);
818
877
  peerMap.set(ns, peerData.files);
819
878
  peerTombstones.set(ns, peerData.tombstones);
879
+ peerManifests.set(
880
+ ns,
881
+ await buildReconcileManifest({
882
+ files: peerData.files,
883
+ cachedFiles: localManifests.get(ns)?.files,
884
+ readFile: async (file) => {
885
+ const remote = await fetchPeerFileContent(peerUrl, ns, file.path, resolvedToken, fetchFn);
886
+ if (!remote || remote.sha256 !== file.sha256) {
887
+ throw new Error(`failed to read peer reconciliation manifest file: ${file.path}`);
888
+ }
889
+ return remote.content;
890
+ }
891
+ })
892
+ );
820
893
  }
821
894
  }
822
895
  const memoryDir = options.cursorDir ?? config?.memoryDir;
@@ -840,7 +913,8 @@ async function computeConvergePlan(options = {}) {
840
913
  peerTombstonedFileSha256: peerTombstones.get(ns) ?? []
841
914
  });
842
915
  }
843
- return planReconciliation(inputs, { conflictPolicy: options.conflictPolicy });
916
+ const plan = planReconciliation(inputs, { conflictPolicy: options.conflictPolicy });
917
+ return collapseActiveFactDuplicates(plan, localManifests, peerManifests);
844
918
  }
845
919
  async function executeConvergeApply(options = {}) {
846
920
  const conflictPolicy = options.conflictPolicy ?? "manual";
@@ -894,6 +968,7 @@ async function executeConvergeApply(options = {}) {
894
968
  suppressed: 0,
895
969
  failed: 0
896
970
  };
971
+ const peerMutatedNamespaces = /* @__PURE__ */ new Set();
897
972
  let resolvedToken;
898
973
  if (options.peerToken) {
899
974
  try {
@@ -1057,7 +1132,7 @@ async function executeConvergeApply(options = {}) {
1057
1132
  else actualTransfers.pushed += 1;
1058
1133
  } else if (options.peerUrl && entry.localSha256) {
1059
1134
  const expectedPeerSha256 = entry.action === "conflict" ? entry.peerSha256 : entry.baseSha256;
1060
- const ok = await postPeerFileContent(
1135
+ const applied = await postPeerFileContent(
1061
1136
  options.peerUrl,
1062
1137
  entry.namespace,
1063
1138
  entry.path,
@@ -1070,7 +1145,8 @@ async function executeConvergeApply(options = {}) {
1070
1145
  resolvedToken,
1071
1146
  fetchFn
1072
1147
  );
1073
- if (ok) {
1148
+ if (applied) {
1149
+ if (applied === "applied") peerMutatedNamespaces.add(entry.namespace);
1074
1150
  if (entry.action === "conflict") actualTransfers.conflictsResolved += 1;
1075
1151
  else actualTransfers.pushed += 1;
1076
1152
  } else {
@@ -1086,6 +1162,17 @@ async function executeConvergeApply(options = {}) {
1086
1162
  actualTransfers.suppressed += 1;
1087
1163
  }
1088
1164
  }
1165
+ if (options.peerUrl && peerMutatedNamespaces.size > 0) {
1166
+ const namespaces = [...peerMutatedNamespaces].sort();
1167
+ if (!await postPeerConvergenceComplete(
1168
+ options.peerUrl,
1169
+ namespaces,
1170
+ resolvedToken,
1171
+ fetchFn
1172
+ )) {
1173
+ actualTransfers.failed += 1;
1174
+ }
1175
+ }
1089
1176
  let cursorUpdated = false;
1090
1177
  if (actualTransfers.failed === 0) {
1091
1178
  await updateCursorsForPlan(plan, options);
@@ -1111,10 +1198,9 @@ async function updateCursorsForPlan(plan, options) {
1111
1198
  const namespaces = new Set(plan.byNamespace.map((n) => n.namespace));
1112
1199
  for (const ns of namespaces) {
1113
1200
  const cursorPath = defaultConvergeCursorPath(memoryDir, peerUrl, ns);
1114
- const nsEntries = plan.entries.filter((e) => e.namespace === ns);
1115
- const baseFiles = nsEntries.map((e) => ({
1116
- path: e.path,
1117
- sha256: e.localSha256 ?? e.peerSha256 ?? "unknown"
1201
+ const baseFiles = plan.entries.filter((entry) => entry.namespace === ns && entry.reason !== "semantic_duplicate").map((entry) => ({
1202
+ path: entry.path,
1203
+ sha256: entry.localSha256 ?? entry.peerSha256 ?? "unknown"
1118
1204
  }));
1119
1205
  const cursorState = {
1120
1206
  version: 1,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remnic/cli",
3
- "version": "9.43.0",
3
+ "version": "9.45.0",
4
4
  "description": "CLI for Remnic memory — init, query, doctor, daemon management",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -26,24 +26,24 @@
26
26
  },
27
27
  "dependencies": {
28
28
  "yaml": "^2.4.2",
29
- "@remnic/server": "^9.43.0",
30
- "@remnic/plugin-pi": "^9.43.0",
31
- "@remnic/core": "^9.43.0"
29
+ "@remnic/plugin-pi": "^9.45.0",
30
+ "@remnic/core": "^9.45.0",
31
+ "@remnic/server": "^9.45.0"
32
32
  },
33
33
  "peerDependencies": {
34
- "@remnic/bench": "^9.43.0",
35
- "@remnic/export-weclone": "^9.43.0",
36
- "@remnic/import-weclone": "^9.43.0",
37
- "@remnic/import-chatgpt": "^9.43.0",
38
- "@remnic/import-claude": "^9.43.0",
39
- "@remnic/import-gemini": "^9.43.0",
40
- "@remnic/import-lossless-claw": "^9.43.0",
41
- "@remnic/import-mem0": "^9.43.0",
42
- "@remnic/import-supermemory": "^9.43.0",
43
- "@remnic/connector-limitless": "^9.43.0",
44
- "@remnic/connector-bee": "^9.43.0",
45
- "@remnic/connector-omi": "^9.43.0",
46
- "@remnic/capture-audio": "^9.43.0"
34
+ "@remnic/bench": "^9.45.0",
35
+ "@remnic/export-weclone": "^9.45.0",
36
+ "@remnic/import-weclone": "^9.45.0",
37
+ "@remnic/import-chatgpt": "^9.45.0",
38
+ "@remnic/import-claude": "^9.45.0",
39
+ "@remnic/import-gemini": "^9.45.0",
40
+ "@remnic/import-lossless-claw": "^9.45.0",
41
+ "@remnic/import-mem0": "^9.45.0",
42
+ "@remnic/import-supermemory": "^9.45.0",
43
+ "@remnic/connector-limitless": "^9.45.0",
44
+ "@remnic/connector-bee": "^9.45.0",
45
+ "@remnic/connector-omi": "^9.45.0",
46
+ "@remnic/capture-audio": "^9.45.0"
47
47
  },
48
48
  "peerDependenciesMeta": {
49
49
  "@remnic/bench": {
@@ -89,18 +89,18 @@
89
89
  "devDependencies": {
90
90
  "tsup": "^8.5.1",
91
91
  "typescript": "^5.9.3",
92
- "@remnic/bench": "9.43.0",
93
- "@remnic/export-weclone": "9.43.0",
94
- "@remnic/import-weclone": "9.43.0",
95
- "@remnic/import-chatgpt": "9.43.0",
96
- "@remnic/import-claude": "9.43.0",
97
- "@remnic/import-lossless-claw": "9.43.0",
98
- "@remnic/import-mem0": "9.43.0",
99
- "@remnic/import-gemini": "9.43.0",
100
- "@remnic/import-supermemory": "9.43.0",
101
- "@remnic/connector-limitless": "9.43.0",
102
- "@remnic/connector-bee": "9.43.0",
103
- "@remnic/connector-omi": "9.43.0"
92
+ "@remnic/bench": "9.45.0",
93
+ "@remnic/export-weclone": "9.45.0",
94
+ "@remnic/import-weclone": "9.45.0",
95
+ "@remnic/import-chatgpt": "9.45.0",
96
+ "@remnic/import-claude": "9.45.0",
97
+ "@remnic/import-gemini": "9.45.0",
98
+ "@remnic/import-lossless-claw": "9.45.0",
99
+ "@remnic/import-mem0": "9.45.0",
100
+ "@remnic/connector-limitless": "9.45.0",
101
+ "@remnic/import-supermemory": "9.45.0",
102
+ "@remnic/connector-omi": "9.45.0",
103
+ "@remnic/connector-bee": "9.45.0"
104
104
  },
105
105
  "license": "MIT",
106
106
  "repository": {