@remnic/cli 10.0.8 → 10.0.10
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.
- package/dist/index.js +453 -148
- package/package.json +32 -32
package/dist/index.js
CHANGED
|
@@ -2365,9 +2365,7 @@ import {
|
|
|
2365
2365
|
readConvergeCursor,
|
|
2366
2366
|
writeConvergeCursor
|
|
2367
2367
|
} from "@remnic/core/reconcile/cursor.js";
|
|
2368
|
-
import {
|
|
2369
|
-
collapseActiveFactDuplicates
|
|
2370
|
-
} from "@remnic/core/reconcile/manifest.js";
|
|
2368
|
+
import { collapseActiveFactDuplicates } from "@remnic/core/reconcile/manifest.js";
|
|
2371
2369
|
import {
|
|
2372
2370
|
planReconciliation
|
|
2373
2371
|
} from "@remnic/core/reconcile/plan.js";
|
|
@@ -3364,6 +3362,133 @@ async function planLocalNamespaceCensus(args) {
|
|
|
3364
3362
|
return { files, manifest, tombstones: tombstonedFileDigests(evidence, manifest) };
|
|
3365
3363
|
}
|
|
3366
3364
|
|
|
3365
|
+
// src/converge-namespace-exclusions.ts
|
|
3366
|
+
var TRANSPORT_MARKERS = [
|
|
3367
|
+
"aborted due to timeout",
|
|
3368
|
+
"operation was aborted",
|
|
3369
|
+
"fetch failed",
|
|
3370
|
+
"econnrefused",
|
|
3371
|
+
"econnreset",
|
|
3372
|
+
"enotfound",
|
|
3373
|
+
"etimedout",
|
|
3374
|
+
"eai_again",
|
|
3375
|
+
"socket hang up",
|
|
3376
|
+
"und_err_"
|
|
3377
|
+
];
|
|
3378
|
+
function hasHttpStatus(message) {
|
|
3379
|
+
let from = 0;
|
|
3380
|
+
while (from < message.length) {
|
|
3381
|
+
const at = message.indexOf("HTTP ", from);
|
|
3382
|
+
if (at < 0) return false;
|
|
3383
|
+
const start = at + 5;
|
|
3384
|
+
if (start + 2 < message.length && message.charCodeAt(start) >= 48 && message.charCodeAt(start) <= 57 && message.charCodeAt(start + 1) >= 48 && message.charCodeAt(start + 1) <= 57 && message.charCodeAt(start + 2) >= 48 && message.charCodeAt(start + 2) <= 57) {
|
|
3385
|
+
return true;
|
|
3386
|
+
}
|
|
3387
|
+
from = start;
|
|
3388
|
+
}
|
|
3389
|
+
return false;
|
|
3390
|
+
}
|
|
3391
|
+
function hasTransportMarker(message) {
|
|
3392
|
+
const lower = message.toLowerCase();
|
|
3393
|
+
for (const marker of TRANSPORT_MARKERS) {
|
|
3394
|
+
if (lower.includes(marker)) return true;
|
|
3395
|
+
}
|
|
3396
|
+
return false;
|
|
3397
|
+
}
|
|
3398
|
+
var PEER_PATH_ERROR_PREFIXES = [
|
|
3399
|
+
"failed to read peer reconciliation manifest file:",
|
|
3400
|
+
"failed to read peer tombstone evidence:",
|
|
3401
|
+
"offline file content response changed during transfer:"
|
|
3402
|
+
];
|
|
3403
|
+
function errorEmbedsPeerPath(message) {
|
|
3404
|
+
for (const prefix of PEER_PATH_ERROR_PREFIXES) {
|
|
3405
|
+
if (message.startsWith(prefix)) return true;
|
|
3406
|
+
}
|
|
3407
|
+
return false;
|
|
3408
|
+
}
|
|
3409
|
+
function isPeerTransportCensusFailure(error) {
|
|
3410
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3411
|
+
let current = error;
|
|
3412
|
+
while (current !== void 0 && current !== null && !seen.has(current)) {
|
|
3413
|
+
seen.add(current);
|
|
3414
|
+
if (!(current instanceof Error)) return false;
|
|
3415
|
+
if (hasHttpStatus(current.message)) return false;
|
|
3416
|
+
if (current.name === "TimeoutError") return true;
|
|
3417
|
+
if (!errorEmbedsPeerPath(current.message) && hasTransportMarker(current.message)) return true;
|
|
3418
|
+
current = "cause" in current ? current.cause : void 0;
|
|
3419
|
+
}
|
|
3420
|
+
return false;
|
|
3421
|
+
}
|
|
3422
|
+
function recordNamespaceCensusFailure(excluded, namespace, side, error, signal) {
|
|
3423
|
+
if (signal?.aborted) throw error;
|
|
3424
|
+
if (side === "peer" && isPeerTransportCensusFailure(error)) throw error;
|
|
3425
|
+
const message = error instanceof Error && error.message.length > 0 ? error.message : String(error);
|
|
3426
|
+
excluded.set(namespace, { namespace, side, error: message, cause: error });
|
|
3427
|
+
}
|
|
3428
|
+
function excludeNamespaceGroup(excluded, namespaces, side, error, signal, drop) {
|
|
3429
|
+
for (const namespace of namespaces) {
|
|
3430
|
+
recordNamespaceCensusFailure(excluded, namespace, side, error, signal);
|
|
3431
|
+
drop(namespace);
|
|
3432
|
+
}
|
|
3433
|
+
}
|
|
3434
|
+
function singleLine(value) {
|
|
3435
|
+
let out = "";
|
|
3436
|
+
let pendingSpace = false;
|
|
3437
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
3438
|
+
const char = value[index] ?? "";
|
|
3439
|
+
const code = char.charCodeAt(0);
|
|
3440
|
+
const isSpace = code === 32 || code === 9 || code === 10 || code === 13;
|
|
3441
|
+
if (isSpace) {
|
|
3442
|
+
pendingSpace = out.length > 0;
|
|
3443
|
+
continue;
|
|
3444
|
+
}
|
|
3445
|
+
if (code < 32 || code === 127 || code >= 128 && code <= 159) continue;
|
|
3446
|
+
if (pendingSpace) {
|
|
3447
|
+
out += " ";
|
|
3448
|
+
pendingSpace = false;
|
|
3449
|
+
}
|
|
3450
|
+
out += char;
|
|
3451
|
+
}
|
|
3452
|
+
return out;
|
|
3453
|
+
}
|
|
3454
|
+
function loggedCensusFailure(failure) {
|
|
3455
|
+
const message = failure.error.length > 0 ? failure.error : String(failure.cause);
|
|
3456
|
+
const cleaned = singleLine(message);
|
|
3457
|
+
return `${singleLine(failure.namespace)} (${failure.side}): ${cleaned.length > 0 ? cleaned : "namespace census failed"}`;
|
|
3458
|
+
}
|
|
3459
|
+
function compareNamespaceSide(left, right) {
|
|
3460
|
+
if (left.namespace < right.namespace) return -1;
|
|
3461
|
+
if (left.namespace > right.namespace) return 1;
|
|
3462
|
+
if (left.side < right.side) return -1;
|
|
3463
|
+
if (left.side > right.side) return 1;
|
|
3464
|
+
return 0;
|
|
3465
|
+
}
|
|
3466
|
+
function listNamespaceExclusions(excluded) {
|
|
3467
|
+
return [...excluded.values()].map(({ namespace, side, error }) => ({ namespace, side, error })).sort(compareNamespaceSide);
|
|
3468
|
+
}
|
|
3469
|
+
function finishConvergeApplyResult(result, excludedNamespaces) {
|
|
3470
|
+
if (excludedNamespaces.length === 0) return result;
|
|
3471
|
+
const failedTransfers = result.status === "applied" && result.transfers.failed > 0;
|
|
3472
|
+
const keepStatus = result.status === "stopped_unresolved_conflicts" || result.status === "dry_run" || failedTransfers;
|
|
3473
|
+
return {
|
|
3474
|
+
...result,
|
|
3475
|
+
excludedNamespaces,
|
|
3476
|
+
converged: false,
|
|
3477
|
+
status: keepStatus ? result.status : "partial"
|
|
3478
|
+
};
|
|
3479
|
+
}
|
|
3480
|
+
function throwIfEveryNamespaceCensusFailed(plannedNamespaces, excluded) {
|
|
3481
|
+
if (plannedNamespaces > 0 || excluded.size === 0) return;
|
|
3482
|
+
const failures = [...excluded.values()].sort(compareNamespaceSide);
|
|
3483
|
+
const only = failures[0];
|
|
3484
|
+
if (failures.length === 1 && only) {
|
|
3485
|
+
const message = only.error.length > 0 ? only.error : String(only.cause);
|
|
3486
|
+
const cleaned = singleLine(message);
|
|
3487
|
+
throw new Error(cleaned.length > 0 ? cleaned : "namespace census failed", { cause: only.cause });
|
|
3488
|
+
}
|
|
3489
|
+
throw new Error(`converge: every namespace census failed: ${failures.map(loggedCensusFailure).join("; ")}`);
|
|
3490
|
+
}
|
|
3491
|
+
|
|
3367
3492
|
// src/converge-peer-census.ts
|
|
3368
3493
|
import { createHash as createHash4 } from "crypto";
|
|
3369
3494
|
import { parseFrontmatter as parseFrontmatter2 } from "@remnic/core/storage.js";
|
|
@@ -3376,10 +3501,10 @@ import {
|
|
|
3376
3501
|
// src/converge-peer-transport.ts
|
|
3377
3502
|
import { createHash as createHash3 } from "crypto";
|
|
3378
3503
|
import {
|
|
3379
|
-
isInternalRemnicStatePath as isInternalRemnicStatePath3,
|
|
3380
3504
|
OFFLINE_SYNC_CHANGESET_FORMAT,
|
|
3381
3505
|
OFFLINE_SYNC_FILE_CONTENT_MAX_CHUNK_BYTES,
|
|
3382
|
-
OFFLINE_SYNC_FILE_CONTENT_TRANSFER_CHUNK_BYTES as OFFLINE_SYNC_FILE_CONTENT_TRANSFER_CHUNK_BYTES2
|
|
3506
|
+
OFFLINE_SYNC_FILE_CONTENT_TRANSFER_CHUNK_BYTES as OFFLINE_SYNC_FILE_CONTENT_TRANSFER_CHUNK_BYTES2,
|
|
3507
|
+
isInternalRemnicStatePath as isInternalRemnicStatePath3
|
|
3383
3508
|
} from "@remnic/core";
|
|
3384
3509
|
import { normalizeConvergePeerUrl as normalizeConvergePeerUrl2 } from "@remnic/core/reconcile/cursor.js";
|
|
3385
3510
|
|
|
@@ -3609,6 +3734,73 @@ async function fetchPeerManifestStream(peerUrl, namespace, token, fetchImpl = gl
|
|
|
3609
3734
|
}
|
|
3610
3735
|
return null;
|
|
3611
3736
|
}
|
|
3737
|
+
var SNAPSHOT_ERROR_BODY_CAP_BYTES = 512;
|
|
3738
|
+
function collapseWhitespace(value, max) {
|
|
3739
|
+
let out = "";
|
|
3740
|
+
let pendingSpace = false;
|
|
3741
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
3742
|
+
const char = value[index] ?? "";
|
|
3743
|
+
const code = char.charCodeAt(0);
|
|
3744
|
+
const isSpace = code === 32 || code === 9 || code === 10 || code === 13;
|
|
3745
|
+
if (isSpace) {
|
|
3746
|
+
pendingSpace = out.length > 0;
|
|
3747
|
+
continue;
|
|
3748
|
+
}
|
|
3749
|
+
if (code < 32 || code === 127 || code >= 128 && code <= 159) continue;
|
|
3750
|
+
if (pendingSpace) {
|
|
3751
|
+
if (out.length + 1 >= max) break;
|
|
3752
|
+
out += " ";
|
|
3753
|
+
pendingSpace = false;
|
|
3754
|
+
}
|
|
3755
|
+
if (out.length >= max) break;
|
|
3756
|
+
out += char;
|
|
3757
|
+
}
|
|
3758
|
+
return out;
|
|
3759
|
+
}
|
|
3760
|
+
async function readBoundedBody(response, maxBytes) {
|
|
3761
|
+
const body = response.body;
|
|
3762
|
+
if (!body) return "";
|
|
3763
|
+
const reader = body.getReader();
|
|
3764
|
+
const chunks = [];
|
|
3765
|
+
let received = 0;
|
|
3766
|
+
try {
|
|
3767
|
+
while (received < maxBytes) {
|
|
3768
|
+
const { done, value } = await reader.read();
|
|
3769
|
+
if (done || !value || value.byteLength === 0) break;
|
|
3770
|
+
const remaining = maxBytes - received;
|
|
3771
|
+
if (value.byteLength > remaining) {
|
|
3772
|
+
chunks.push(value.subarray(0, remaining));
|
|
3773
|
+
received += remaining;
|
|
3774
|
+
break;
|
|
3775
|
+
}
|
|
3776
|
+
chunks.push(value);
|
|
3777
|
+
received += value.byteLength;
|
|
3778
|
+
}
|
|
3779
|
+
} catch (error) {
|
|
3780
|
+
if (isPeerTransportCensusFailure(error)) throw error;
|
|
3781
|
+
return "";
|
|
3782
|
+
} finally {
|
|
3783
|
+
await reader.cancel().catch(() => void 0);
|
|
3784
|
+
}
|
|
3785
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
3786
|
+
}
|
|
3787
|
+
async function readSnapshotErrorDetail(response) {
|
|
3788
|
+
const raw = await readBoundedBody(response, SNAPSHOT_ERROR_BODY_CAP_BYTES);
|
|
3789
|
+
const trimmed = raw.trim();
|
|
3790
|
+
if (trimmed.length === 0) return "";
|
|
3791
|
+
let detail = trimmed;
|
|
3792
|
+
if (trimmed.startsWith("{")) {
|
|
3793
|
+
try {
|
|
3794
|
+
const parsed = JSON.parse(trimmed);
|
|
3795
|
+
if (parsed !== null && typeof parsed === "object" && "error" in parsed) {
|
|
3796
|
+
const error = parsed.error;
|
|
3797
|
+
if (typeof error === "string" && error.trim().length > 0) detail = error.trim();
|
|
3798
|
+
}
|
|
3799
|
+
} catch {
|
|
3800
|
+
}
|
|
3801
|
+
}
|
|
3802
|
+
return collapseWhitespace(detail, 160);
|
|
3803
|
+
}
|
|
3612
3804
|
async function fetchPeerSnapshot(peerUrl, namespace, token, fetchImpl = globalThis.fetch, timeoutMs = DEFAULT_PEER_REQUEST_TIMEOUT_MS, signal) {
|
|
3613
3805
|
const base = normalizePeerBaseUrl(peerUrl);
|
|
3614
3806
|
const routes = [
|
|
@@ -3622,6 +3814,15 @@ async function fetchPeerSnapshot(peerUrl, namespace, token, fetchImpl = globalTh
|
|
|
3622
3814
|
];
|
|
3623
3815
|
const headers = token ? { authorization: `Bearer ${token}` } : {};
|
|
3624
3816
|
let lastFailure = "no snapshot route responded";
|
|
3817
|
+
let transportFailure;
|
|
3818
|
+
const noteTransportFailure = (error) => {
|
|
3819
|
+
if (transportFailure === void 0 && isPeerTransportCensusFailure(error)) transportFailure = error;
|
|
3820
|
+
lastFailure = error instanceof Error && error.message.length > 0 ? error.message : String(error);
|
|
3821
|
+
};
|
|
3822
|
+
const invalidSnapshot = (detail) => {
|
|
3823
|
+
if (transportFailure) throw transportFailure;
|
|
3824
|
+
return new Error(`invalid peer snapshot for namespace ${namespace}: ${detail}`);
|
|
3825
|
+
};
|
|
3625
3826
|
for (const route of routes) {
|
|
3626
3827
|
let response;
|
|
3627
3828
|
try {
|
|
@@ -3633,25 +3834,38 @@ async function fetchPeerSnapshot(peerUrl, namespace, token, fetchImpl = globalTh
|
|
|
3633
3834
|
);
|
|
3634
3835
|
} catch (error) {
|
|
3635
3836
|
if (signal?.aborted) throw error;
|
|
3636
|
-
|
|
3837
|
+
noteTransportFailure(error);
|
|
3637
3838
|
continue;
|
|
3638
3839
|
}
|
|
3639
3840
|
if (!response.ok) {
|
|
3640
|
-
|
|
3841
|
+
let detail = "";
|
|
3842
|
+
try {
|
|
3843
|
+
detail = await readSnapshotErrorDetail(response);
|
|
3844
|
+
} catch (error) {
|
|
3845
|
+
if (signal?.aborted) throw error;
|
|
3846
|
+
noteTransportFailure(error);
|
|
3847
|
+
continue;
|
|
3848
|
+
}
|
|
3849
|
+
lastFailure = detail.length > 0 ? `HTTP ${response.status} ${detail}` : `HTTP ${response.status}`;
|
|
3641
3850
|
continue;
|
|
3642
3851
|
}
|
|
3643
3852
|
let data;
|
|
3644
3853
|
try {
|
|
3645
3854
|
data = await response.json();
|
|
3646
|
-
} catch {
|
|
3647
|
-
|
|
3855
|
+
} catch (error) {
|
|
3856
|
+
if (signal?.aborted) throw error;
|
|
3857
|
+
if (isPeerTransportCensusFailure(error)) {
|
|
3858
|
+
noteTransportFailure(error);
|
|
3859
|
+
continue;
|
|
3860
|
+
}
|
|
3861
|
+
throw invalidSnapshot("response was not JSON");
|
|
3648
3862
|
}
|
|
3649
3863
|
if (!data || typeof data !== "object" || !("files" in data) || !Array.isArray(data.files)) {
|
|
3650
|
-
throw
|
|
3864
|
+
throw invalidSnapshot("files must be an array");
|
|
3651
3865
|
}
|
|
3652
3866
|
const files = data.files.map((item, index) => {
|
|
3653
3867
|
if (!item || typeof item !== "object" || !("path" in item) || typeof item.path !== "string" || !("sha256" in item) || typeof item.sha256 !== "string") {
|
|
3654
|
-
throw
|
|
3868
|
+
throw invalidSnapshot(`malformed file at index ${index}`);
|
|
3655
3869
|
}
|
|
3656
3870
|
return {
|
|
3657
3871
|
path: item.path,
|
|
@@ -3662,17 +3876,18 @@ async function fetchPeerSnapshot(peerUrl, namespace, token, fetchImpl = globalTh
|
|
|
3662
3876
|
}).filter((file) => !isInternalRemnicStatePath3(file.path));
|
|
3663
3877
|
const rawTombstones = "tombstones" in data ? data.tombstones : void 0;
|
|
3664
3878
|
if (rawTombstones !== void 0 && !Array.isArray(rawTombstones)) {
|
|
3665
|
-
throw
|
|
3879
|
+
throw invalidSnapshot("tombstones must be an array");
|
|
3666
3880
|
}
|
|
3667
3881
|
const tombstones = /* @__PURE__ */ new Set();
|
|
3668
3882
|
for (const tombstone of rawTombstones ?? []) {
|
|
3669
3883
|
if (typeof tombstone !== "string" || !/^[0-9a-f]{64}$/i.test(tombstone)) {
|
|
3670
|
-
throw
|
|
3884
|
+
throw invalidSnapshot("malformed tombstone");
|
|
3671
3885
|
}
|
|
3672
3886
|
tombstones.add(tombstone.toLowerCase());
|
|
3673
3887
|
}
|
|
3674
3888
|
return { files, tombstones };
|
|
3675
3889
|
}
|
|
3890
|
+
if (transportFailure) throw transportFailure;
|
|
3676
3891
|
throw new Error(`failed to fetch peer snapshot for namespace ${namespace}: ${lastFailure}`);
|
|
3677
3892
|
}
|
|
3678
3893
|
function requiredResponseNumber(response, name) {
|
|
@@ -3683,7 +3898,7 @@ function requiredResponseNumber(response, name) {
|
|
|
3683
3898
|
}
|
|
3684
3899
|
return value;
|
|
3685
3900
|
}
|
|
3686
|
-
async function streamPeerFileContent(peerUrl, namespace, filePath, onChunk, token, fetchImpl = globalThis.fetch, timeoutMs = DEFAULT_PEER_REQUEST_TIMEOUT_MS, signal) {
|
|
3901
|
+
async function streamPeerFileContent(peerUrl, namespace, filePath, onChunk, token, fetchImpl = globalThis.fetch, timeoutMs = DEFAULT_PEER_REQUEST_TIMEOUT_MS, signal, propagateTransportErrors = false) {
|
|
3687
3902
|
assertTransferablePeerPath(filePath);
|
|
3688
3903
|
const base = normalizePeerBaseUrl(peerUrl);
|
|
3689
3904
|
const routes = ["/remnic/v1/offline-sync/file-content", "/engram/v1/offline-sync/file-content"];
|
|
@@ -3696,6 +3911,7 @@ async function streamPeerFileContent(peerUrl, namespace, filePath, onChunk, toke
|
|
|
3696
3911
|
let expectedBytes;
|
|
3697
3912
|
let expectedSha256;
|
|
3698
3913
|
let mtimeMs;
|
|
3914
|
+
let transportFailure;
|
|
3699
3915
|
for (const route of routes) {
|
|
3700
3916
|
let routeFailed = false;
|
|
3701
3917
|
do {
|
|
@@ -3719,7 +3935,9 @@ async function streamPeerFileContent(peerUrl, namespace, filePath, onChunk, toke
|
|
|
3719
3935
|
timeoutMs
|
|
3720
3936
|
);
|
|
3721
3937
|
if (!response.ok) throw new Error(`offline file content request failed: ${response.status}`);
|
|
3722
|
-
} catch {
|
|
3938
|
+
} catch (error) {
|
|
3939
|
+
if (signal?.aborted) throw error;
|
|
3940
|
+
if (propagateTransportErrors && isPeerTransportCensusFailure(error)) transportFailure = error;
|
|
3723
3941
|
routeFailed = true;
|
|
3724
3942
|
break;
|
|
3725
3943
|
}
|
|
@@ -3738,7 +3956,9 @@ async function streamPeerFileContent(peerUrl, namespace, filePath, onChunk, toke
|
|
|
3738
3956
|
if (!sha256 || chunkOffset !== offset || chunkBytes !== content.length || encodedPath !== null && decodeURIComponent(encodedPath) !== filePath || expectedBytes !== void 0 && expectedBytes !== totalBytes || expectedSha256 !== void 0 && expectedSha256 !== sha256 || content.length === 0 && offset < totalBytes) {
|
|
3739
3957
|
throw new Error(`offline file content response changed during transfer: ${filePath}`);
|
|
3740
3958
|
}
|
|
3741
|
-
} catch {
|
|
3959
|
+
} catch (error) {
|
|
3960
|
+
if (signal?.aborted) throw error;
|
|
3961
|
+
if (propagateTransportErrors && isPeerTransportCensusFailure(error)) transportFailure = error;
|
|
3742
3962
|
routeFailed = true;
|
|
3743
3963
|
break;
|
|
3744
3964
|
}
|
|
@@ -3757,12 +3977,13 @@ async function streamPeerFileContent(peerUrl, namespace, filePath, onChunk, toke
|
|
|
3757
3977
|
} while (expectedBytes === void 0 || offset < expectedBytes);
|
|
3758
3978
|
if (!routeFailed && expectedBytes !== void 0 && offset === expectedBytes) break;
|
|
3759
3979
|
}
|
|
3760
|
-
if (expectedBytes
|
|
3761
|
-
return
|
|
3980
|
+
if (expectedBytes !== void 0 && expectedSha256 !== void 0 && mtimeMs !== void 0 && offset === expectedBytes && hash.digest("hex") === expectedSha256) {
|
|
3981
|
+
return { sha256: expectedSha256, bytes: expectedBytes, mtimeMs };
|
|
3762
3982
|
}
|
|
3763
|
-
|
|
3983
|
+
if (transportFailure) throw transportFailure;
|
|
3984
|
+
return null;
|
|
3764
3985
|
}
|
|
3765
|
-
async function fetchPeerFileContent(peerUrl, namespace, filePath, token, fetchImpl = globalThis.fetch, timeoutMs = DEFAULT_PEER_REQUEST_TIMEOUT_MS, signal) {
|
|
3986
|
+
async function fetchPeerFileContent(peerUrl, namespace, filePath, token, fetchImpl = globalThis.fetch, timeoutMs = DEFAULT_PEER_REQUEST_TIMEOUT_MS, signal, propagateTransportErrors = false) {
|
|
3766
3987
|
const chunks = [];
|
|
3767
3988
|
const metadata = await streamPeerFileContent(
|
|
3768
3989
|
peerUrl,
|
|
@@ -3774,7 +3995,8 @@ async function fetchPeerFileContent(peerUrl, namespace, filePath, token, fetchIm
|
|
|
3774
3995
|
token,
|
|
3775
3996
|
fetchImpl,
|
|
3776
3997
|
timeoutMs,
|
|
3777
|
-
signal
|
|
3998
|
+
signal,
|
|
3999
|
+
propagateTransportErrors
|
|
3778
4000
|
);
|
|
3779
4001
|
if (!metadata) return null;
|
|
3780
4002
|
return {
|
|
@@ -3973,6 +4195,7 @@ async function planPeerNamespaceCensus(args) {
|
|
|
3973
4195
|
} else {
|
|
3974
4196
|
clientBuilt = true;
|
|
3975
4197
|
let readFailure;
|
|
4198
|
+
let transportFailure;
|
|
3976
4199
|
peerManifest = await buildReconcileManifest2({
|
|
3977
4200
|
files: peerData.files,
|
|
3978
4201
|
parseMemory: parseFrontmatter2,
|
|
@@ -3982,6 +4205,7 @@ async function planPeerNamespaceCensus(args) {
|
|
|
3982
4205
|
// rows (sha-keyed) skip the ones already fetched.
|
|
3983
4206
|
cachedFiles: priorPeerFiles ?? args.localManifestFiles,
|
|
3984
4207
|
readFile: async (file) => {
|
|
4208
|
+
if (transportFailure) throw transportFailure;
|
|
3985
4209
|
let remote;
|
|
3986
4210
|
try {
|
|
3987
4211
|
remote = await fetchPeerFileContent(
|
|
@@ -3991,19 +4215,24 @@ async function planPeerNamespaceCensus(args) {
|
|
|
3991
4215
|
args.resolvedToken,
|
|
3992
4216
|
args.fetchFn,
|
|
3993
4217
|
args.timeoutMs,
|
|
3994
|
-
args.signal
|
|
4218
|
+
args.signal,
|
|
4219
|
+
true
|
|
3995
4220
|
);
|
|
3996
4221
|
} catch (error) {
|
|
3997
|
-
|
|
3998
|
-
|
|
4222
|
+
const failure = error instanceof Error ? error : new Error(String(error));
|
|
4223
|
+
if (isPeerTransportCensusFailure(error)) transportFailure = transportFailure ?? error;
|
|
4224
|
+
if (!transportFailure) readFailure = failure;
|
|
4225
|
+
throw failure;
|
|
3999
4226
|
}
|
|
4000
4227
|
if (!remote || remote.sha256 !== file.sha256) {
|
|
4001
|
-
|
|
4002
|
-
|
|
4228
|
+
const failure = new Error(`failed to read peer reconciliation manifest file: ${file.path}`);
|
|
4229
|
+
if (!transportFailure) readFailure = failure;
|
|
4230
|
+
throw failure;
|
|
4003
4231
|
}
|
|
4004
4232
|
return remote.content;
|
|
4005
4233
|
}
|
|
4006
4234
|
});
|
|
4235
|
+
if (transportFailure) throw transportFailure;
|
|
4007
4236
|
if (readFailure) throw readFailure;
|
|
4008
4237
|
}
|
|
4009
4238
|
}
|
|
@@ -4012,15 +4241,21 @@ async function planPeerNamespaceCensus(args) {
|
|
|
4012
4241
|
for (const tombstonePath of TOMBSTONE_PATHS) {
|
|
4013
4242
|
const state = peerData.files.find((file) => file.path === tombstonePath);
|
|
4014
4243
|
if (!state) continue;
|
|
4015
|
-
|
|
4016
|
-
|
|
4017
|
-
|
|
4018
|
-
|
|
4019
|
-
|
|
4020
|
-
|
|
4021
|
-
|
|
4022
|
-
|
|
4023
|
-
|
|
4244
|
+
let remote;
|
|
4245
|
+
try {
|
|
4246
|
+
remote = await fetchPeerFileContent(
|
|
4247
|
+
peerUrl,
|
|
4248
|
+
ns,
|
|
4249
|
+
tombstonePath,
|
|
4250
|
+
args.resolvedToken,
|
|
4251
|
+
args.fetchFn,
|
|
4252
|
+
args.timeoutMs,
|
|
4253
|
+
args.signal,
|
|
4254
|
+
true
|
|
4255
|
+
);
|
|
4256
|
+
} catch (error) {
|
|
4257
|
+
throw new Error(`failed to read peer tombstone evidence: ${tombstonePath}`, { cause: error });
|
|
4258
|
+
}
|
|
4024
4259
|
if (!remote) {
|
|
4025
4260
|
throw new Error(`failed to read peer tombstone evidence: ${tombstonePath}`);
|
|
4026
4261
|
}
|
|
@@ -4072,8 +4307,10 @@ async function planPeerNamespaceCensus(args) {
|
|
|
4072
4307
|
|
|
4073
4308
|
// src/converge-report.ts
|
|
4074
4309
|
function formatConvergeReport(plan) {
|
|
4310
|
+
const excluded = plan.excludedNamespaces ?? [];
|
|
4075
4311
|
const lines = [];
|
|
4076
|
-
|
|
4312
|
+
const status = excluded.length > 0 ? "PARTIAL" : plan.converged ? "CONVERGED" : "DIVERGED";
|
|
4313
|
+
lines.push(`Convergence Status: ${status}`);
|
|
4077
4314
|
lines.push("");
|
|
4078
4315
|
lines.push("Per-Namespace Summary:");
|
|
4079
4316
|
if (plan.byNamespace.length === 0) {
|
|
@@ -4089,8 +4326,24 @@ function formatConvergeReport(plan) {
|
|
|
4089
4326
|
lines.push(` unresolved: ${report.unresolved}`);
|
|
4090
4327
|
}
|
|
4091
4328
|
}
|
|
4329
|
+
if (excluded.length > 0) {
|
|
4330
|
+
lines.push("");
|
|
4331
|
+
lines.push("Excluded namespaces (census failed; no pushes, no deletes):");
|
|
4332
|
+
for (const exclusion of excluded) {
|
|
4333
|
+
lines.push(` [${singleLine(exclusion.namespace)}] side=${exclusion.side} ${singleLine(exclusion.error)}`);
|
|
4334
|
+
}
|
|
4335
|
+
}
|
|
4092
4336
|
return lines.join("\n");
|
|
4093
4337
|
}
|
|
4338
|
+
function formatConvergeWatchCycleLine(cycle, result) {
|
|
4339
|
+
const transfers = result.transfers;
|
|
4340
|
+
const excluded = result.excludedNamespaces?.length ?? 0;
|
|
4341
|
+
return `converge watch: cycle ${cycle} status=${result.status} pulled=${transfers.pulled} pushed=${transfers.pushed} conflicts=${transfers.conflictsResolved} failed=${transfers.failed} excluded=${excluded}`;
|
|
4342
|
+
}
|
|
4343
|
+
function formatConvergeExclusionLine(cycle, exclusion) {
|
|
4344
|
+
const prefix = cycle === void 0 ? "converge:" : `converge watch: cycle ${cycle}`;
|
|
4345
|
+
return `${prefix} excluded namespace=${singleLine(exclusion.namespace)} side=${exclusion.side}: ${singleLine(exclusion.error)}`;
|
|
4346
|
+
}
|
|
4094
4347
|
function formatConvergeApplyReport(result) {
|
|
4095
4348
|
const lines = [];
|
|
4096
4349
|
lines.push(`Convergence Execution Status: ${result.status.toUpperCase()}`);
|
|
@@ -4107,6 +4360,68 @@ function formatConvergeApplyReport(result) {
|
|
|
4107
4360
|
return lines.join("\n");
|
|
4108
4361
|
}
|
|
4109
4362
|
|
|
4363
|
+
// src/converge-watch.ts
|
|
4364
|
+
var CONVERGE_WATCH_MIN_INTERVAL_MS = 1e3;
|
|
4365
|
+
var CONVERGE_WATCH_DEFAULT_INTERVAL_MS = 3e5;
|
|
4366
|
+
var CONVERGE_WATCH_MAX_INTERVAL_MS = 2147483647;
|
|
4367
|
+
function sleepAborted(ms, signal) {
|
|
4368
|
+
return new Promise((resolve3) => {
|
|
4369
|
+
const timer = setTimeout(() => {
|
|
4370
|
+
signal.removeEventListener("abort", onAbort);
|
|
4371
|
+
resolve3(true);
|
|
4372
|
+
}, ms);
|
|
4373
|
+
const onAbort = () => {
|
|
4374
|
+
clearTimeout(timer);
|
|
4375
|
+
resolve3(false);
|
|
4376
|
+
};
|
|
4377
|
+
if (signal.aborted) {
|
|
4378
|
+
clearTimeout(timer);
|
|
4379
|
+
resolve3(false);
|
|
4380
|
+
return;
|
|
4381
|
+
}
|
|
4382
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
4383
|
+
});
|
|
4384
|
+
}
|
|
4385
|
+
async function convergeWatch(options) {
|
|
4386
|
+
const intervalMs = Math.min(
|
|
4387
|
+
CONVERGE_WATCH_MAX_INTERVAL_MS,
|
|
4388
|
+
Math.max(CONVERGE_WATCH_MIN_INTERVAL_MS, options.intervalMs ?? CONVERGE_WATCH_DEFAULT_INTERVAL_MS)
|
|
4389
|
+
);
|
|
4390
|
+
const { apply, intervalMs: _intervalMs, maxCycles, onCycle, signal, ...applyOptions } = options;
|
|
4391
|
+
const outcome = {
|
|
4392
|
+
cycles: 0,
|
|
4393
|
+
convergedCycles: 0,
|
|
4394
|
+
appliedCycles: 0,
|
|
4395
|
+
failedCycles: 0,
|
|
4396
|
+
lastStatus: "aborted",
|
|
4397
|
+
lastExcludedNamespaces: []
|
|
4398
|
+
};
|
|
4399
|
+
while (maxCycles === void 0 || outcome.cycles < maxCycles) {
|
|
4400
|
+
if (signal?.aborted) break;
|
|
4401
|
+
try {
|
|
4402
|
+
const result = await apply(applyOptions);
|
|
4403
|
+
outcome.cycles += 1;
|
|
4404
|
+
if (result.status === "converged") outcome.convergedCycles += 1;
|
|
4405
|
+
else if (result.status === "stopped_unresolved_conflicts" || result.status === "applied" && result.transfers.failed > 0) {
|
|
4406
|
+
outcome.failedCycles += 1;
|
|
4407
|
+
} else outcome.appliedCycles += 1;
|
|
4408
|
+
outcome.lastStatus = result.status;
|
|
4409
|
+
outcome.lastExcludedNamespaces = result.excludedNamespaces ? [...result.excludedNamespaces] : [];
|
|
4410
|
+
onCycle?.(outcome.cycles, { result });
|
|
4411
|
+
} catch (err) {
|
|
4412
|
+
outcome.cycles += 1;
|
|
4413
|
+
outcome.failedCycles += 1;
|
|
4414
|
+
outcome.lastStatus = "error";
|
|
4415
|
+
outcome.lastExcludedNamespaces = [];
|
|
4416
|
+
onCycle?.(outcome.cycles, { error: err });
|
|
4417
|
+
}
|
|
4418
|
+
if (maxCycles !== void 0 && outcome.cycles >= maxCycles) break;
|
|
4419
|
+
const slept = await sleepAborted(intervalMs, signal ?? new AbortController().signal);
|
|
4420
|
+
if (!slept) break;
|
|
4421
|
+
}
|
|
4422
|
+
return outcome;
|
|
4423
|
+
}
|
|
4424
|
+
|
|
4110
4425
|
// src/credential-channel.ts
|
|
4111
4426
|
import * as fs20 from "fs";
|
|
4112
4427
|
function parseTokenFileFlag(raw) {
|
|
@@ -4203,65 +4518,6 @@ function resolveCredentialChannel(input, env, hooks) {
|
|
|
4203
4518
|
return { ok: true, token: void 0, tokenFromArgv: false };
|
|
4204
4519
|
}
|
|
4205
4520
|
|
|
4206
|
-
// src/converge-watch.ts
|
|
4207
|
-
var CONVERGE_WATCH_MIN_INTERVAL_MS = 1e3;
|
|
4208
|
-
var CONVERGE_WATCH_DEFAULT_INTERVAL_MS = 3e5;
|
|
4209
|
-
var CONVERGE_WATCH_MAX_INTERVAL_MS = 2147483647;
|
|
4210
|
-
function sleepAborted(ms, signal) {
|
|
4211
|
-
return new Promise((resolve3) => {
|
|
4212
|
-
const timer = setTimeout(() => {
|
|
4213
|
-
signal.removeEventListener("abort", onAbort);
|
|
4214
|
-
resolve3(true);
|
|
4215
|
-
}, ms);
|
|
4216
|
-
const onAbort = () => {
|
|
4217
|
-
clearTimeout(timer);
|
|
4218
|
-
resolve3(false);
|
|
4219
|
-
};
|
|
4220
|
-
if (signal.aborted) {
|
|
4221
|
-
clearTimeout(timer);
|
|
4222
|
-
resolve3(false);
|
|
4223
|
-
return;
|
|
4224
|
-
}
|
|
4225
|
-
signal.addEventListener("abort", onAbort, { once: true });
|
|
4226
|
-
});
|
|
4227
|
-
}
|
|
4228
|
-
async function convergeWatch(options) {
|
|
4229
|
-
const intervalMs = Math.min(
|
|
4230
|
-
CONVERGE_WATCH_MAX_INTERVAL_MS,
|
|
4231
|
-
Math.max(CONVERGE_WATCH_MIN_INTERVAL_MS, options.intervalMs ?? CONVERGE_WATCH_DEFAULT_INTERVAL_MS)
|
|
4232
|
-
);
|
|
4233
|
-
const { apply, intervalMs: _intervalMs, maxCycles, onCycle, signal, ...applyOptions } = options;
|
|
4234
|
-
const outcome = {
|
|
4235
|
-
cycles: 0,
|
|
4236
|
-
convergedCycles: 0,
|
|
4237
|
-
appliedCycles: 0,
|
|
4238
|
-
failedCycles: 0,
|
|
4239
|
-
lastStatus: "aborted"
|
|
4240
|
-
};
|
|
4241
|
-
while (maxCycles === void 0 || outcome.cycles < maxCycles) {
|
|
4242
|
-
if (signal?.aborted) break;
|
|
4243
|
-
try {
|
|
4244
|
-
const result = await apply(applyOptions);
|
|
4245
|
-
outcome.cycles += 1;
|
|
4246
|
-
if (result.status === "converged") outcome.convergedCycles += 1;
|
|
4247
|
-
else if (result.status === "stopped_unresolved_conflicts" || result.status === "applied" && result.transfers.failed > 0) {
|
|
4248
|
-
outcome.failedCycles += 1;
|
|
4249
|
-
} else outcome.appliedCycles += 1;
|
|
4250
|
-
outcome.lastStatus = result.status;
|
|
4251
|
-
onCycle?.(outcome.cycles, { result });
|
|
4252
|
-
} catch (err) {
|
|
4253
|
-
outcome.cycles += 1;
|
|
4254
|
-
outcome.failedCycles += 1;
|
|
4255
|
-
outcome.lastStatus = "error";
|
|
4256
|
-
onCycle?.(outcome.cycles, { error: err });
|
|
4257
|
-
}
|
|
4258
|
-
if (maxCycles !== void 0 && outcome.cycles >= maxCycles) break;
|
|
4259
|
-
const slept = await sleepAborted(intervalMs, signal ?? new AbortController().signal);
|
|
4260
|
-
if (!slept) break;
|
|
4261
|
-
}
|
|
4262
|
-
return outcome;
|
|
4263
|
-
}
|
|
4264
|
-
|
|
4265
4521
|
// src/converge.ts
|
|
4266
4522
|
async function discoverCursorNamespaces(memoryDir, peerUrl) {
|
|
4267
4523
|
const cursorDir = path7.join(path7.resolve(memoryDir), ".remnic", "state", "converge-cursors");
|
|
@@ -4302,7 +4558,22 @@ async function computeConvergePlan(options = {}) {
|
|
|
4302
4558
|
const peerDeletionMtimeMs = /* @__PURE__ */ new Map();
|
|
4303
4559
|
const localManifests = /* @__PURE__ */ new Map();
|
|
4304
4560
|
const peerManifests = /* @__PURE__ */ new Map();
|
|
4561
|
+
const excluded = /* @__PURE__ */ new Map();
|
|
4562
|
+
const sharedRootAliases = /* @__PURE__ */ new Map();
|
|
4305
4563
|
let peerPlatform;
|
|
4564
|
+
const dropPlannedNamespace = (namespace) => {
|
|
4565
|
+
namespacesToPlan.delete(namespace);
|
|
4566
|
+
baseMap.delete(namespace);
|
|
4567
|
+
semanticAgreementMap.delete(namespace);
|
|
4568
|
+
localMap.delete(namespace);
|
|
4569
|
+
localTombstones.delete(namespace);
|
|
4570
|
+
localDeletionMtimeMs.delete(namespace);
|
|
4571
|
+
localManifests.delete(namespace);
|
|
4572
|
+
peerMap.delete(namespace);
|
|
4573
|
+
peerTombstones.delete(namespace);
|
|
4574
|
+
peerDeletionMtimeMs.delete(namespace);
|
|
4575
|
+
peerManifests.delete(namespace);
|
|
4576
|
+
};
|
|
4306
4577
|
if (options.baseFilesByNamespace) {
|
|
4307
4578
|
for (const [ns, files] of options.baseFilesByNamespace) {
|
|
4308
4579
|
namespacesToPlan.add(ns);
|
|
@@ -4382,27 +4653,32 @@ async function computeConvergePlan(options = {}) {
|
|
|
4382
4653
|
}
|
|
4383
4654
|
for (const [rootIndex, rootInfo] of roots.entries()) {
|
|
4384
4655
|
namespacesToPlan.add(rootInfo.namespace);
|
|
4385
|
-
const
|
|
4386
|
-
|
|
4387
|
-
|
|
4388
|
-
|
|
4389
|
-
|
|
4390
|
-
|
|
4391
|
-
|
|
4392
|
-
|
|
4393
|
-
|
|
4394
|
-
|
|
4395
|
-
|
|
4396
|
-
|
|
4397
|
-
|
|
4398
|
-
|
|
4399
|
-
|
|
4400
|
-
|
|
4656
|
+
for (const alias of rootInfo.namespaces) sharedRootAliases.set(alias, rootInfo.namespaces);
|
|
4657
|
+
try {
|
|
4658
|
+
const census = await planLocalNamespaceCensus({
|
|
4659
|
+
rootDir: rootInfo.rootDir,
|
|
4660
|
+
namespace: rootInfo.namespace,
|
|
4661
|
+
index: rootIndex + 1,
|
|
4662
|
+
total: roots.length,
|
|
4663
|
+
citationTemplate: config.inlineSourceAttributionFormat,
|
|
4664
|
+
cache,
|
|
4665
|
+
signal: options.signal,
|
|
4666
|
+
onProgress: options.onProgress,
|
|
4667
|
+
memoryDir,
|
|
4668
|
+
peerUrl: options.peerUrl,
|
|
4669
|
+
userExcludeRegexps
|
|
4670
|
+
});
|
|
4671
|
+
localMap.set(rootInfo.namespace, census.files);
|
|
4672
|
+
localManifests.set(rootInfo.namespace, census.manifest);
|
|
4673
|
+
localTombstones.set(rootInfo.namespace, census.tombstones);
|
|
4674
|
+
} catch (error) {
|
|
4675
|
+
excludeNamespaceGroup(excluded, rootInfo.namespaces, "local", error, options.signal, dropPlannedNamespace);
|
|
4676
|
+
}
|
|
4401
4677
|
}
|
|
4402
4678
|
}
|
|
4403
4679
|
if (memoryDir && peerUrl) {
|
|
4404
4680
|
for (const namespace of await discoverCursorNamespaces(memoryDir, peerUrl)) {
|
|
4405
|
-
namespacesToPlan.add(namespace);
|
|
4681
|
+
if (!excluded.has(namespace)) namespacesToPlan.add(namespace);
|
|
4406
4682
|
}
|
|
4407
4683
|
}
|
|
4408
4684
|
if (!options.peerFilesByNamespace && peerUrl) {
|
|
@@ -4422,27 +4698,34 @@ async function computeConvergePlan(options = {}) {
|
|
|
4422
4698
|
peerPlatform = capabilities?.platform;
|
|
4423
4699
|
const sortedNamespaces = [...namespacesToPlan].sort();
|
|
4424
4700
|
for (const [nsIndex, ns] of sortedNamespaces.entries()) {
|
|
4425
|
-
|
|
4426
|
-
|
|
4427
|
-
|
|
4428
|
-
|
|
4429
|
-
|
|
4430
|
-
|
|
4431
|
-
|
|
4432
|
-
|
|
4433
|
-
|
|
4434
|
-
|
|
4435
|
-
|
|
4436
|
-
|
|
4437
|
-
|
|
4438
|
-
|
|
4439
|
-
|
|
4440
|
-
|
|
4441
|
-
|
|
4442
|
-
|
|
4443
|
-
|
|
4701
|
+
if (excluded.has(ns)) continue;
|
|
4702
|
+
try {
|
|
4703
|
+
const census = await planPeerNamespaceCensus({
|
|
4704
|
+
peerUrl,
|
|
4705
|
+
namespace: ns,
|
|
4706
|
+
index: nsIndex + 1,
|
|
4707
|
+
total: sortedNamespaces.length,
|
|
4708
|
+
resolvedToken,
|
|
4709
|
+
fetchFn,
|
|
4710
|
+
timeoutMs,
|
|
4711
|
+
manifestStream: capabilities?.manifestStream === true,
|
|
4712
|
+
peerManifestRevision: capabilities?.manifestRevision,
|
|
4713
|
+
citationTemplate: config?.inlineSourceAttributionFormat,
|
|
4714
|
+
localManifestFiles: localManifests.get(ns)?.files,
|
|
4715
|
+
cache,
|
|
4716
|
+
signal: options.signal,
|
|
4717
|
+
onProgress: options.onProgress
|
|
4718
|
+
});
|
|
4719
|
+
peerMap.set(ns, census.manifest.files);
|
|
4720
|
+
peerManifests.set(ns, census.manifest);
|
|
4721
|
+
peerTombstones.set(ns, census.tombstones);
|
|
4722
|
+
} catch (error) {
|
|
4723
|
+
const aliases = sharedRootAliases.get(ns) ?? [ns];
|
|
4724
|
+
excludeNamespaceGroup(excluded, aliases, "peer", error, options.signal, dropPlannedNamespace);
|
|
4725
|
+
}
|
|
4444
4726
|
}
|
|
4445
4727
|
}
|
|
4728
|
+
throwIfEveryNamespaceCensusFailed(namespacesToPlan.size, excluded);
|
|
4446
4729
|
if (memoryDir && options.peerUrl && (!options.baseFilesByNamespace || !options.semanticAgreementsByNamespace)) {
|
|
4447
4730
|
for (const ns of namespacesToPlan) {
|
|
4448
4731
|
const cursorPath = defaultConvergeCursorPath(memoryDir, options.peerUrl, ns);
|
|
@@ -4465,6 +4748,7 @@ async function computeConvergePlan(options = {}) {
|
|
|
4465
4748
|
}
|
|
4466
4749
|
const inputs = [];
|
|
4467
4750
|
for (const ns of [...namespacesToPlan].sort()) {
|
|
4751
|
+
if (excluded.has(ns)) continue;
|
|
4468
4752
|
const baseFiles = baseMap.get(ns);
|
|
4469
4753
|
const localFiles = localMap.get(ns) ?? [];
|
|
4470
4754
|
const peerFiles = peerMap.get(ns) ?? [];
|
|
@@ -4482,8 +4766,11 @@ async function computeConvergePlan(options = {}) {
|
|
|
4482
4766
|
});
|
|
4483
4767
|
}
|
|
4484
4768
|
const conflictPolicy = options.conflictPolicy ?? config?.converge.conflictPolicy ?? DEFAULT_CONVERGE_CONFLICT_POLICY;
|
|
4485
|
-
const
|
|
4486
|
-
|
|
4769
|
+
const planned = planReconciliation(inputs, { conflictPolicy, peerPlatform });
|
|
4770
|
+
const plan = collapseActiveFactDuplicates(planned, localManifests, peerManifests, semanticAgreementMap);
|
|
4771
|
+
const excludedNamespaces = listNamespaceExclusions(excluded);
|
|
4772
|
+
if (excludedNamespaces.length === 0) return plan;
|
|
4773
|
+
return { ...plan, excludedNamespaces, converged: false };
|
|
4487
4774
|
} finally {
|
|
4488
4775
|
await cache?.close();
|
|
4489
4776
|
}
|
|
@@ -4495,25 +4782,27 @@ async function executeConvergeApply(options = {}) {
|
|
|
4495
4782
|
}
|
|
4496
4783
|
const conflictPolicy = options.conflictPolicy ?? options.config?.converge.conflictPolicy ?? DEFAULT_CONVERGE_CONFLICT_POLICY;
|
|
4497
4784
|
const plan = await computeConvergePlan({ ...options, conflictPolicy });
|
|
4785
|
+
const excludedNamespaces = plan.excludedNamespaces ?? [];
|
|
4786
|
+
const finish = (result) => finishConvergeApplyResult(result, excludedNamespaces);
|
|
4498
4787
|
if (plan.converged && !options.dryRun) {
|
|
4499
4788
|
await updateCursorsForPlan(plan, options);
|
|
4500
|
-
return {
|
|
4789
|
+
return finish({
|
|
4501
4790
|
converged: true,
|
|
4502
4791
|
status: "converged",
|
|
4503
4792
|
plan,
|
|
4504
4793
|
transfers: { pulled: 0, pushed: 0, conflictsResolved: 0, suppressed: 0, failed: 0 },
|
|
4505
4794
|
cursorUpdated: true
|
|
4506
|
-
};
|
|
4795
|
+
});
|
|
4507
4796
|
}
|
|
4508
4797
|
const unresolvedCount = plan.byNamespace.reduce((acc, report) => acc + report.unresolved, 0);
|
|
4509
4798
|
if (unresolvedCount > 0) {
|
|
4510
|
-
return {
|
|
4799
|
+
return finish({
|
|
4511
4800
|
converged: false,
|
|
4512
4801
|
status: "stopped_unresolved_conflicts",
|
|
4513
4802
|
plan,
|
|
4514
4803
|
transfers: { pulled: 0, pushed: 0, conflictsResolved: 0, suppressed: 0, failed: 0 },
|
|
4515
4804
|
cursorUpdated: false
|
|
4516
|
-
};
|
|
4805
|
+
});
|
|
4517
4806
|
}
|
|
4518
4807
|
const plannedTransfers = {
|
|
4519
4808
|
pulled: 0,
|
|
@@ -4529,13 +4818,13 @@ async function executeConvergeApply(options = {}) {
|
|
|
4529
4818
|
else if (entry.action === "suppress") plannedTransfers.suppressed += 1;
|
|
4530
4819
|
}
|
|
4531
4820
|
if (options.dryRun) {
|
|
4532
|
-
return {
|
|
4821
|
+
return finish({
|
|
4533
4822
|
converged: plan.converged,
|
|
4534
4823
|
status: "dry_run",
|
|
4535
4824
|
plan,
|
|
4536
4825
|
transfers: plannedTransfers,
|
|
4537
4826
|
cursorUpdated: false
|
|
4538
|
-
};
|
|
4827
|
+
});
|
|
4539
4828
|
}
|
|
4540
4829
|
const actualTransfers = {
|
|
4541
4830
|
pulled: 0,
|
|
@@ -4930,7 +5219,14 @@ async function executeConvergeApply(options = {}) {
|
|
|
4930
5219
|
options.signal?.throwIfAborted();
|
|
4931
5220
|
if (options.peerUrl && peerMutatedNamespaces.size > 0) {
|
|
4932
5221
|
const namespaces = [...peerMutatedNamespaces].sort();
|
|
4933
|
-
if (!await postPeerConvergenceComplete(
|
|
5222
|
+
if (!await postPeerConvergenceComplete(
|
|
5223
|
+
options.peerUrl,
|
|
5224
|
+
namespaces,
|
|
5225
|
+
resolvedToken,
|
|
5226
|
+
fetchFn,
|
|
5227
|
+
timeoutMs,
|
|
5228
|
+
options.signal
|
|
5229
|
+
)) {
|
|
4934
5230
|
actualTransfers.failed += 1;
|
|
4935
5231
|
}
|
|
4936
5232
|
}
|
|
@@ -4940,13 +5236,13 @@ async function executeConvergeApply(options = {}) {
|
|
|
4940
5236
|
await updateCursorsForPlan(plan, options);
|
|
4941
5237
|
cursorUpdated = true;
|
|
4942
5238
|
}
|
|
4943
|
-
return {
|
|
5239
|
+
return finish({
|
|
4944
5240
|
converged: actualTransfers.failed === 0,
|
|
4945
5241
|
status: "applied",
|
|
4946
5242
|
plan,
|
|
4947
5243
|
transfers: actualTransfers,
|
|
4948
5244
|
cursorUpdated
|
|
4949
|
-
};
|
|
5245
|
+
});
|
|
4950
5246
|
}
|
|
4951
5247
|
async function updateCursorsForPlan(plan, options) {
|
|
4952
5248
|
const peerUrl = normalizeConvergePeerUrl3(options.peerUrl ?? "local");
|
|
@@ -5095,24 +5391,25 @@ Subcommands:
|
|
|
5095
5391
|
intervalMs: intervalSeconds !== void 0 ? intervalSeconds * 1e3 : void 0,
|
|
5096
5392
|
...timeoutMsFlag !== void 0 ? { peerRequestTimeoutMs: timeoutMsFlag } : {},
|
|
5097
5393
|
signal: controller2.signal,
|
|
5098
|
-
onCycle:
|
|
5394
|
+
onCycle: (cycle, event) => {
|
|
5099
5395
|
if (event.error !== void 0) {
|
|
5100
5396
|
console.error(`converge watch: cycle ${cycle} failed: ${String(event.error)}`);
|
|
5101
5397
|
return;
|
|
5102
5398
|
}
|
|
5103
5399
|
const result = event.result;
|
|
5104
5400
|
if (!result) return;
|
|
5105
|
-
const
|
|
5106
|
-
|
|
5107
|
-
|
|
5108
|
-
);
|
|
5401
|
+
for (const exclusion of result.excludedNamespaces ?? []) {
|
|
5402
|
+
console.error(formatConvergeExclusionLine(cycle, exclusion));
|
|
5403
|
+
}
|
|
5404
|
+
if (json) return;
|
|
5405
|
+
console.log(formatConvergeWatchCycleLine(cycle, result));
|
|
5109
5406
|
}
|
|
5110
5407
|
});
|
|
5111
5408
|
if (json) {
|
|
5112
5409
|
console.log(JSON.stringify(outcome, null, 2));
|
|
5113
5410
|
} else {
|
|
5114
5411
|
console.log(
|
|
5115
|
-
`converge watch stopped after ${outcome.cycles} cycle(s): ${outcome.convergedCycles} converged, ${outcome.appliedCycles} applied, ${outcome.failedCycles} failed (last: ${outcome.lastStatus}).`
|
|
5412
|
+
`converge watch stopped after ${outcome.cycles} cycle(s): ${outcome.convergedCycles} converged, ${outcome.appliedCycles} applied, ${outcome.failedCycles} failed (last: ${outcome.lastStatus}, excluded: ${outcome.lastExcludedNamespaces.length}).`
|
|
5116
5413
|
);
|
|
5117
5414
|
}
|
|
5118
5415
|
} finally {
|
|
@@ -5145,6 +5442,10 @@ Subcommands:
|
|
|
5145
5442
|
signal: controller.signal,
|
|
5146
5443
|
onProgress
|
|
5147
5444
|
});
|
|
5445
|
+
for (const exclusion of plan.excludedNamespaces ?? []) {
|
|
5446
|
+
process.stderr.write(`${formatConvergeExclusionLine(void 0, exclusion)}
|
|
5447
|
+
`);
|
|
5448
|
+
}
|
|
5148
5449
|
if (json) {
|
|
5149
5450
|
console.log(JSON.stringify(plan, null, 2));
|
|
5150
5451
|
} else {
|
|
@@ -5162,6 +5463,10 @@ Subcommands:
|
|
|
5162
5463
|
signal: controller.signal,
|
|
5163
5464
|
onProgress
|
|
5164
5465
|
});
|
|
5466
|
+
for (const exclusion of result.excludedNamespaces ?? []) {
|
|
5467
|
+
process.stderr.write(`${formatConvergeExclusionLine(void 0, exclusion)}
|
|
5468
|
+
`);
|
|
5469
|
+
}
|
|
5165
5470
|
if (json) {
|
|
5166
5471
|
console.log(JSON.stringify(result, null, 2));
|
|
5167
5472
|
} else {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@remnic/cli",
|
|
3
|
-
"version": "10.0.
|
|
3
|
+
"version": "10.0.10",
|
|
4
4
|
"description": "CLI for Remnic memory — init, query, doctor, daemon management",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -26,26 +26,26 @@
|
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
28
|
"yaml": "^2.4.2",
|
|
29
|
-
"@remnic/plugin-pi": "^10.0.
|
|
30
|
-
"@remnic/
|
|
31
|
-
"@remnic/
|
|
29
|
+
"@remnic/plugin-pi": "^10.0.10",
|
|
30
|
+
"@remnic/server": "^10.0.10",
|
|
31
|
+
"@remnic/core": "^10.0.10"
|
|
32
32
|
},
|
|
33
33
|
"peerDependencies": {
|
|
34
|
-
"@remnic/bench": "^10.0.
|
|
35
|
-
"@remnic/plugin-openclaw": "^10.0.
|
|
36
|
-
"@remnic/export-weclone": "^10.0.
|
|
37
|
-
"@remnic/import-weclone": "^10.0.
|
|
38
|
-
"@remnic/import-chatgpt": "^10.0.
|
|
39
|
-
"@remnic/import-claude": "^10.0.
|
|
40
|
-
"@remnic/import-gemini": "^10.0.
|
|
41
|
-
"@remnic/import-lossless-claw": "^10.0.
|
|
42
|
-
"@remnic/import-mem0": "^10.0.
|
|
43
|
-
"@remnic/import-supermemory": "^10.0.
|
|
44
|
-
"@remnic/import-okf": "^10.0.
|
|
45
|
-
"@remnic/connector-limitless": "^10.0.
|
|
46
|
-
"@remnic/connector-bee": "^10.0.
|
|
47
|
-
"@remnic/connector-omi": "^10.0.
|
|
48
|
-
"@remnic/capture-audio": "^10.0.
|
|
34
|
+
"@remnic/bench": "^10.0.10",
|
|
35
|
+
"@remnic/plugin-openclaw": "^10.0.10",
|
|
36
|
+
"@remnic/export-weclone": "^10.0.10",
|
|
37
|
+
"@remnic/import-weclone": "^10.0.10",
|
|
38
|
+
"@remnic/import-chatgpt": "^10.0.10",
|
|
39
|
+
"@remnic/import-claude": "^10.0.10",
|
|
40
|
+
"@remnic/import-gemini": "^10.0.10",
|
|
41
|
+
"@remnic/import-lossless-claw": "^10.0.10",
|
|
42
|
+
"@remnic/import-mem0": "^10.0.10",
|
|
43
|
+
"@remnic/import-supermemory": "^10.0.10",
|
|
44
|
+
"@remnic/import-okf": "^10.0.10",
|
|
45
|
+
"@remnic/connector-limitless": "^10.0.10",
|
|
46
|
+
"@remnic/connector-bee": "^10.0.10",
|
|
47
|
+
"@remnic/connector-omi": "^10.0.10",
|
|
48
|
+
"@remnic/capture-audio": "^10.0.10"
|
|
49
49
|
},
|
|
50
50
|
"peerDependenciesMeta": {
|
|
51
51
|
"@remnic/bench": {
|
|
@@ -97,19 +97,19 @@
|
|
|
97
97
|
"devDependencies": {
|
|
98
98
|
"tsup": "^8.5.1",
|
|
99
99
|
"typescript": "^5.9.3",
|
|
100
|
-
"@remnic/
|
|
101
|
-
"@remnic/
|
|
102
|
-
"@remnic/
|
|
103
|
-
"@remnic/
|
|
104
|
-
"@remnic/import-chatgpt": "10.0.
|
|
105
|
-
"@remnic/import-claude": "10.0.
|
|
106
|
-
"@remnic/import-gemini": "10.0.
|
|
107
|
-
"@remnic/import-lossless-claw": "10.0.
|
|
108
|
-
"@remnic/import-mem0": "10.0.
|
|
109
|
-
"@remnic/import-supermemory": "10.0.
|
|
110
|
-
"@remnic/connector-
|
|
111
|
-
"@remnic/connector-
|
|
112
|
-
"@remnic/connector-omi": "10.0.
|
|
100
|
+
"@remnic/bench": "10.0.10",
|
|
101
|
+
"@remnic/plugin-openclaw": "10.0.10",
|
|
102
|
+
"@remnic/export-weclone": "10.0.10",
|
|
103
|
+
"@remnic/import-weclone": "10.0.10",
|
|
104
|
+
"@remnic/import-chatgpt": "10.0.10",
|
|
105
|
+
"@remnic/import-claude": "10.0.10",
|
|
106
|
+
"@remnic/import-gemini": "10.0.10",
|
|
107
|
+
"@remnic/import-lossless-claw": "10.0.10",
|
|
108
|
+
"@remnic/import-mem0": "10.0.10",
|
|
109
|
+
"@remnic/import-supermemory": "10.0.10",
|
|
110
|
+
"@remnic/connector-limitless": "10.0.10",
|
|
111
|
+
"@remnic/connector-bee": "10.0.10",
|
|
112
|
+
"@remnic/connector-omi": "10.0.10"
|
|
113
113
|
},
|
|
114
114
|
"license": "MIT",
|
|
115
115
|
"repository": {
|