@remnic/cli 9.69.45 → 9.69.47
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 +183 -80
- package/package.json +32 -32
package/dist/index.js
CHANGED
|
@@ -2084,7 +2084,7 @@ import { buildReconcileManifest } from "@remnic/core/reconcile/manifest.js";
|
|
|
2084
2084
|
import { convergeIdentityCachePath } from "@remnic/core/reconcile/cursor.js";
|
|
2085
2085
|
|
|
2086
2086
|
// src/converge-plan-cache.ts
|
|
2087
|
-
import { createHash } from "crypto";
|
|
2087
|
+
import { createHash, randomUUID } from "crypto";
|
|
2088
2088
|
import { readFileSync } from "fs";
|
|
2089
2089
|
import * as fs16 from "fs/promises";
|
|
2090
2090
|
import * as path2 from "path";
|
|
@@ -2099,6 +2099,15 @@ var CONVERGE_PLAN_CACHE_MAX_ENTRIES = 128;
|
|
|
2099
2099
|
var CONVERGE_PLAN_CACHE_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
2100
2100
|
var CONVERGE_PLAN_CACHE_MAX_SCOPE_DIRS = 8;
|
|
2101
2101
|
var SHA256_HEX = /^[0-9a-f]{64}$/i;
|
|
2102
|
+
var MEMORY_STATUSES = [
|
|
2103
|
+
"active",
|
|
2104
|
+
"pending_review",
|
|
2105
|
+
"rejected",
|
|
2106
|
+
"quarantined",
|
|
2107
|
+
"superseded",
|
|
2108
|
+
"archived",
|
|
2109
|
+
"forgotten"
|
|
2110
|
+
];
|
|
2102
2111
|
var ConvergePlanCacheBusyError = class extends Error {
|
|
2103
2112
|
constructor(holderPid) {
|
|
2104
2113
|
super(
|
|
@@ -2155,7 +2164,7 @@ function normalizeEntryFile(raw) {
|
|
|
2155
2164
|
if (typeof candidate.id !== "string" || candidate.id.length === 0) return null;
|
|
2156
2165
|
if (typeof candidate.contentHash !== "string" || !SHA256_HEX.test(candidate.contentHash)) return null;
|
|
2157
2166
|
if (typeof candidate.category !== "string") return null;
|
|
2158
|
-
if (
|
|
2167
|
+
if (!MEMORY_STATUSES.includes(candidate.status)) return null;
|
|
2159
2168
|
const normalizerVersion = typeof candidate.normalizerVersion === "number" ? candidate.normalizerVersion : void 0;
|
|
2160
2169
|
const identityResolutionVersion = typeof candidate.identityResolutionVersion === "number" ? candidate.identityResolutionVersion : void 0;
|
|
2161
2170
|
const contentHashAliases = Array.isArray(candidate.contentHashAliases) && candidate.contentHashAliases.every((alias) => typeof alias === "string") ? candidate.contentHashAliases : void 0;
|
|
@@ -2331,57 +2340,76 @@ async function pruneSiblingScopes(root, activeScope) {
|
|
|
2331
2340
|
});
|
|
2332
2341
|
}
|
|
2333
2342
|
}
|
|
2343
|
+
var heldLockPaths = /* @__PURE__ */ new Set();
|
|
2344
|
+
var failedReleaseNonces = /* @__PURE__ */ new Map();
|
|
2334
2345
|
var ConvergePlanCache = class _ConvergePlanCache {
|
|
2335
2346
|
scope;
|
|
2336
2347
|
scopeDir;
|
|
2337
2348
|
lockPath;
|
|
2349
|
+
nonce = randomUUID();
|
|
2338
2350
|
closed = false;
|
|
2339
2351
|
constructor(root, scope) {
|
|
2340
2352
|
this.scope = scope;
|
|
2341
2353
|
this.scopeDir = path2.join(root, scope);
|
|
2342
2354
|
this.lockPath = path2.join(root, "lock.json");
|
|
2343
2355
|
}
|
|
2356
|
+
lockPayload() {
|
|
2357
|
+
return `${JSON.stringify({
|
|
2358
|
+
pid: process.pid,
|
|
2359
|
+
startTicks: PROCESS_START_TICKS,
|
|
2360
|
+
savedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2361
|
+
nonce: this.nonce
|
|
2362
|
+
})}
|
|
2363
|
+
`;
|
|
2364
|
+
}
|
|
2344
2365
|
/**
|
|
2345
2366
|
* Open (and lock) the cache for one planning run. The lock is
|
|
2346
2367
|
* cross-process: a second live planner fails fast with
|
|
2347
2368
|
* {@link ConvergePlanCacheBusyError} instead of racing checkpoint writes.
|
|
2348
|
-
* A lock left by a dead process
|
|
2349
|
-
*
|
|
2350
|
-
*
|
|
2351
|
-
* written via rename, so
|
|
2369
|
+
* A lock left by a dead process — or by this process's own failed
|
|
2370
|
+
* release — is stolen; a lock file is only ever replaced atomically, so
|
|
2371
|
+
* a steal racing the true owner at worst causes redundant recompute —
|
|
2372
|
+
* entries themselves are content-addressed and written via rename, so
|
|
2373
|
+
* concurrent identical writes cannot interleave.
|
|
2352
2374
|
*/
|
|
2353
2375
|
static async open(memoryDir, scope) {
|
|
2354
2376
|
const root = convergePlanCacheRoot(memoryDir);
|
|
2355
2377
|
const scopeDir = path2.join(root, scope);
|
|
2356
2378
|
await ensureSafePlanCacheTree(memoryDir, scopeDir);
|
|
2357
2379
|
const lockPath = path2.join(root, "lock.json");
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
savedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2362
|
-
})}
|
|
2363
|
-
`;
|
|
2380
|
+
if (heldLockPaths.has(lockPath)) throw new ConvergePlanCacheBusyError(process.pid);
|
|
2381
|
+
heldLockPaths.add(lockPath);
|
|
2382
|
+
const cache = new _ConvergePlanCache(root, scope);
|
|
2364
2383
|
try {
|
|
2365
|
-
|
|
2366
|
-
} catch (error) {
|
|
2367
|
-
if (error.code !== "EEXIST") throw error;
|
|
2368
|
-
let held = {};
|
|
2384
|
+
const payload = cache.lockPayload();
|
|
2369
2385
|
try {
|
|
2370
|
-
|
|
2386
|
+
await fs16.writeFile(lockPath, payload, { flag: "wx" });
|
|
2387
|
+
} catch (error) {
|
|
2388
|
+
if (error.code !== "EEXIST") throw error;
|
|
2389
|
+
let held = {};
|
|
2390
|
+
try {
|
|
2391
|
+
held = JSON.parse(await fs16.readFile(lockPath, "utf8"));
|
|
2392
|
+
} catch {
|
|
2393
|
+
held = {};
|
|
2394
|
+
}
|
|
2395
|
+
const sameOwner = held.pid === process.pid && (typeof held.nonce === "string" && failedReleaseNonces.get(lockPath) === held.nonce || PROCESS_START_TICKS !== null && held.startTicks === PROCESS_START_TICKS);
|
|
2396
|
+
if (!sameOwner) {
|
|
2397
|
+
const owner = lockOwnerLive(held);
|
|
2398
|
+
if (owner.live) throw new ConvergePlanCacheBusyError(owner.pid);
|
|
2399
|
+
}
|
|
2400
|
+
const tmp = `${lockPath}.${process.pid}.tmp`;
|
|
2401
|
+
await fs16.writeFile(tmp, payload);
|
|
2402
|
+
await fs16.rename(tmp, lockPath);
|
|
2403
|
+
}
|
|
2404
|
+
failedReleaseNonces.delete(lockPath);
|
|
2405
|
+
try {
|
|
2406
|
+
await pruneSiblingScopes(root, scope);
|
|
2407
|
+
await pruneScopeDir(cache.scopeDir);
|
|
2371
2408
|
} catch {
|
|
2372
|
-
held = {};
|
|
2373
2409
|
}
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
await fs16.writeFile(tmp, payload);
|
|
2378
|
-
await fs16.rename(tmp, lockPath);
|
|
2379
|
-
}
|
|
2380
|
-
const cache = new _ConvergePlanCache(root, scope);
|
|
2381
|
-
try {
|
|
2382
|
-
await pruneSiblingScopes(root, scope);
|
|
2383
|
-
await pruneScopeDir(cache.scopeDir);
|
|
2384
|
-
} catch {
|
|
2410
|
+
} catch (error) {
|
|
2411
|
+
heldLockPaths.delete(lockPath);
|
|
2412
|
+
throw error;
|
|
2385
2413
|
}
|
|
2386
2414
|
return cache;
|
|
2387
2415
|
}
|
|
@@ -2417,6 +2445,7 @@ var ConvergePlanCache = class _ConvergePlanCache {
|
|
|
2417
2445
|
*/
|
|
2418
2446
|
async writeEntry(entry) {
|
|
2419
2447
|
if (this.closed) return;
|
|
2448
|
+
await this.renewLease();
|
|
2420
2449
|
const fileName = entryFileName(entry.side, entry.namespace, entry.watermark);
|
|
2421
2450
|
const finalPath = path2.join(this.scopeDir, fileName);
|
|
2422
2451
|
const tmpPath = `${finalPath}.${process.pid}.${Date.now()}.tmp`;
|
|
@@ -2439,11 +2468,49 @@ var ConvergePlanCache = class _ConvergePlanCache {
|
|
|
2439
2468
|
} catch {
|
|
2440
2469
|
}
|
|
2441
2470
|
}
|
|
2442
|
-
/**
|
|
2471
|
+
/**
|
|
2472
|
+
* Refresh savedAt while the cache stays open (#2965): on hosts without
|
|
2473
|
+
* readable /proc start ticks the 24h lease is the only steal guard, and a
|
|
2474
|
+
* plan legitimately longer than that must not have its lock stolen
|
|
2475
|
+
* mid-run. Renewal rides the per-namespace checkpoint cadence; it skips
|
|
2476
|
+
* a lock that was stolen from us (nonce mismatch).
|
|
2477
|
+
*/
|
|
2478
|
+
async renewLease() {
|
|
2479
|
+
if (this.closed) return;
|
|
2480
|
+
try {
|
|
2481
|
+
const held = JSON.parse(await fs16.readFile(this.lockPath, "utf8"));
|
|
2482
|
+
if (typeof held.nonce !== "string" || held.nonce !== this.nonce) return;
|
|
2483
|
+
const tmp = `${this.lockPath}.${process.pid}.renew.tmp`;
|
|
2484
|
+
await fs16.writeFile(tmp, this.lockPayload());
|
|
2485
|
+
await fs16.rename(tmp, this.lockPath);
|
|
2486
|
+
} catch {
|
|
2487
|
+
}
|
|
2488
|
+
}
|
|
2489
|
+
/**
|
|
2490
|
+
* Release the cross-process lock. Safe to call more than once. Only the
|
|
2491
|
+
* lock instance this cache owns is unlinked (#2965): a lock stolen from
|
|
2492
|
+
* us carries another owner's nonce and must survive this close. When our
|
|
2493
|
+
* own unlink fails, the nonce is retained so a later open in this
|
|
2494
|
+
* process reclaims the orphan instead of busy-failing on it.
|
|
2495
|
+
*/
|
|
2443
2496
|
async close() {
|
|
2444
2497
|
if (this.closed) return;
|
|
2445
2498
|
this.closed = true;
|
|
2446
|
-
|
|
2499
|
+
heldLockPaths.delete(this.lockPath);
|
|
2500
|
+
let owned = false;
|
|
2501
|
+
try {
|
|
2502
|
+
const held = JSON.parse(await fs16.readFile(this.lockPath, "utf8"));
|
|
2503
|
+
owned = typeof held.nonce === "string" && held.nonce === this.nonce;
|
|
2504
|
+
} catch {
|
|
2505
|
+
}
|
|
2506
|
+
if (!owned) return;
|
|
2507
|
+
try {
|
|
2508
|
+
await fs16.unlink(this.lockPath);
|
|
2509
|
+
} catch (error) {
|
|
2510
|
+
if (error.code !== "ENOENT") {
|
|
2511
|
+
failedReleaseNonces.set(this.lockPath, this.nonce);
|
|
2512
|
+
}
|
|
2513
|
+
}
|
|
2447
2514
|
}
|
|
2448
2515
|
};
|
|
2449
2516
|
|
|
@@ -2938,6 +3005,7 @@ async function planLocalNamespaceCensus(args) {
|
|
|
2938
3005
|
const evidence = await readLocalTombstoneEvidence(rootDir);
|
|
2939
3006
|
let manifestReadFailed = false;
|
|
2940
3007
|
const manifest = await buildReconcileManifest({
|
|
3008
|
+
signal: args.signal,
|
|
2941
3009
|
files,
|
|
2942
3010
|
parseMemory: parseFrontmatter,
|
|
2943
3011
|
citationTemplate: args.citationTemplate,
|
|
@@ -2970,20 +3038,20 @@ async function planLocalNamespaceCensus(args) {
|
|
|
2970
3038
|
identityCache,
|
|
2971
3039
|
classificationUpdates
|
|
2972
3040
|
);
|
|
3041
|
+
const priorByPath = new Map((priorFiles ?? []).map((file) => [file.path, file.sha256.toLowerCase()]));
|
|
3042
|
+
let reused = 0;
|
|
3043
|
+
for (const file of files) {
|
|
3044
|
+
if (priorByPath.get(file.path) === file.sha256.toLowerCase()) reused += 1;
|
|
3045
|
+
}
|
|
3046
|
+
args.onProgress?.({
|
|
3047
|
+
side: "local",
|
|
3048
|
+
namespace: ns,
|
|
3049
|
+
index: args.index,
|
|
3050
|
+
total: args.total,
|
|
3051
|
+
reused,
|
|
3052
|
+
computed: files.length - reused
|
|
3053
|
+
});
|
|
2973
3054
|
if (args.cache) {
|
|
2974
|
-
const priorByPath = new Map((priorFiles ?? []).map((file) => [file.path, file.sha256.toLowerCase()]));
|
|
2975
|
-
let reused = 0;
|
|
2976
|
-
for (const file of files) {
|
|
2977
|
-
if (priorByPath.get(file.path) === file.sha256.toLowerCase()) reused += 1;
|
|
2978
|
-
}
|
|
2979
|
-
args.onProgress?.({
|
|
2980
|
-
side: "local",
|
|
2981
|
-
namespace: ns,
|
|
2982
|
-
index: args.index,
|
|
2983
|
-
total: args.total,
|
|
2984
|
-
reused,
|
|
2985
|
-
computed: files.length - reused
|
|
2986
|
-
});
|
|
2987
3055
|
await args.cache.writeEntry({
|
|
2988
3056
|
version: 1,
|
|
2989
3057
|
scope: args.cache.scope,
|
|
@@ -3025,7 +3093,7 @@ import {
|
|
|
3025
3093
|
RECONCILE_MANIFEST_SCHEMA_VERSION as RECONCILE_MANIFEST_SCHEMA_VERSION2
|
|
3026
3094
|
} from "@remnic/core/reconcile/manifest.js";
|
|
3027
3095
|
var SHA256_PATTERN = /^[a-f0-9]{64}$/i;
|
|
3028
|
-
var
|
|
3096
|
+
var MEMORY_STATUSES2 = /* @__PURE__ */ new Set([
|
|
3029
3097
|
"active",
|
|
3030
3098
|
"pending_review",
|
|
3031
3099
|
"rejected",
|
|
@@ -3062,7 +3130,7 @@ function parseMemory(value) {
|
|
|
3062
3130
|
if (typeof memory.contentHash !== "string" || !SHA256_PATTERN.test(memory.contentHash)) {
|
|
3063
3131
|
throw new Error("peer manifest memory metadata had invalid contentHash");
|
|
3064
3132
|
}
|
|
3065
|
-
if (typeof memory.status !== "string" || !
|
|
3133
|
+
if (typeof memory.status !== "string" || !MEMORY_STATUSES2.has(memory.status)) {
|
|
3066
3134
|
throw new Error("peer manifest memory metadata had invalid status");
|
|
3067
3135
|
}
|
|
3068
3136
|
const contentHash = memory.contentHash.toLowerCase();
|
|
@@ -3221,7 +3289,7 @@ async function fetchPeerSyncCapabilities(peerUrl, token, fetchImpl, timeoutMs) {
|
|
|
3221
3289
|
}
|
|
3222
3290
|
return null;
|
|
3223
3291
|
}
|
|
3224
|
-
async function fetchPeerManifestStream(peerUrl, namespace, token, fetchImpl = globalThis.fetch, timeoutMs = DEFAULT_PEER_REQUEST_TIMEOUT_MS) {
|
|
3292
|
+
async function fetchPeerManifestStream(peerUrl, namespace, token, fetchImpl = globalThis.fetch, timeoutMs = DEFAULT_PEER_REQUEST_TIMEOUT_MS, signal) {
|
|
3225
3293
|
const base = normalizePeerBaseUrl(peerUrl);
|
|
3226
3294
|
const headers = token ? { authorization: `Bearer ${token}` } : {};
|
|
3227
3295
|
const routes = [
|
|
@@ -3229,7 +3297,12 @@ async function fetchPeerManifestStream(peerUrl, namespace, token, fetchImpl = gl
|
|
|
3229
3297
|
`/engram/v1/offline-sync/manifest-stream?namespace=${encodeURIComponent(namespace)}&include_transcripts=false`
|
|
3230
3298
|
];
|
|
3231
3299
|
for (const route of routes) {
|
|
3232
|
-
const response = await fetchPeerRequest(
|
|
3300
|
+
const response = await fetchPeerRequest(
|
|
3301
|
+
fetchImpl,
|
|
3302
|
+
`${base}${route}`,
|
|
3303
|
+
{ headers, ...signal ? { signal } : {} },
|
|
3304
|
+
timeoutMs
|
|
3305
|
+
);
|
|
3233
3306
|
if (response.status === 404 || response.status === 405) continue;
|
|
3234
3307
|
if (response.status === 401 || response.status === 403) {
|
|
3235
3308
|
throw new Error(`peer manifest authentication failed: HTTP ${response.status}`);
|
|
@@ -3239,7 +3312,7 @@ async function fetchPeerManifestStream(peerUrl, namespace, token, fetchImpl = gl
|
|
|
3239
3312
|
}
|
|
3240
3313
|
return null;
|
|
3241
3314
|
}
|
|
3242
|
-
async function fetchPeerSnapshot(peerUrl, namespace, token, fetchImpl = globalThis.fetch, timeoutMs = DEFAULT_PEER_REQUEST_TIMEOUT_MS) {
|
|
3315
|
+
async function fetchPeerSnapshot(peerUrl, namespace, token, fetchImpl = globalThis.fetch, timeoutMs = DEFAULT_PEER_REQUEST_TIMEOUT_MS, signal) {
|
|
3243
3316
|
const base = normalizePeerBaseUrl(peerUrl);
|
|
3244
3317
|
const routes = [
|
|
3245
3318
|
// The snapshot validates the cached peer manifest, whose file set
|
|
@@ -3255,8 +3328,14 @@ async function fetchPeerSnapshot(peerUrl, namespace, token, fetchImpl = globalTh
|
|
|
3255
3328
|
for (const route of routes) {
|
|
3256
3329
|
let response;
|
|
3257
3330
|
try {
|
|
3258
|
-
response = await fetchPeerRequest(
|
|
3331
|
+
response = await fetchPeerRequest(
|
|
3332
|
+
fetchImpl,
|
|
3333
|
+
`${base}${route}`,
|
|
3334
|
+
{ headers, ...signal ? { signal } : {} },
|
|
3335
|
+
timeoutMs
|
|
3336
|
+
);
|
|
3259
3337
|
} catch (error) {
|
|
3338
|
+
if (signal?.aborted) throw error;
|
|
3260
3339
|
lastFailure = error instanceof Error ? error.message : String(error);
|
|
3261
3340
|
continue;
|
|
3262
3341
|
}
|
|
@@ -3552,7 +3631,14 @@ async function postPeerFileDeletion(peerUrl, namespace, filePath, baseSha256, to
|
|
|
3552
3631
|
async function planPeerNamespaceCensus(args) {
|
|
3553
3632
|
const { peerUrl, ns } = { peerUrl: args.peerUrl, ns: args.namespace };
|
|
3554
3633
|
args.signal?.throwIfAborted();
|
|
3555
|
-
const peerData = await fetchPeerSnapshot(
|
|
3634
|
+
const peerData = await fetchPeerSnapshot(
|
|
3635
|
+
peerUrl,
|
|
3636
|
+
ns,
|
|
3637
|
+
args.resolvedToken,
|
|
3638
|
+
args.fetchFn,
|
|
3639
|
+
args.timeoutMs,
|
|
3640
|
+
args.signal
|
|
3641
|
+
);
|
|
3556
3642
|
const priorPeerEntry = args.cache ? await args.cache.readEntry("peer", ns) : null;
|
|
3557
3643
|
const revisionTrusted = args.peerManifestRevision !== void 0 && priorPeerEntry?.peerManifestRevision === args.peerManifestRevision;
|
|
3558
3644
|
const clientBuiltPrior = priorPeerEntry?.clientBuilt === true;
|
|
@@ -3561,7 +3647,11 @@ async function planPeerNamespaceCensus(args) {
|
|
|
3561
3647
|
const watermark = censusWatermark(peerData.files);
|
|
3562
3648
|
let peerManifest = null;
|
|
3563
3649
|
let clientBuilt = clientBuiltPrior;
|
|
3564
|
-
if (reusableEntry && reusableEntry.watermark === watermark && reusableEntry.fileCount === peerData.files.length
|
|
3650
|
+
if (reusableEntry && reusableEntry.watermark === watermark && reusableEntry.fileCount === peerData.files.length && // A peer upgraded to manifest streaming must not keep serving this
|
|
3651
|
+
// client's older parser semantics on an unchanged watermark (#2965):
|
|
3652
|
+
// client-built rows stay a warm base only; the stream runs once and
|
|
3653
|
+
// the checkpoint becomes a streamed entry.
|
|
3654
|
+
!(args.manifestStream && clientBuiltPrior)) {
|
|
3565
3655
|
const freshByPath = new Map(peerData.files.map((file) => [file.path, file]));
|
|
3566
3656
|
peerManifest = {
|
|
3567
3657
|
format: RECONCILE_MANIFEST_FORMAT2,
|
|
@@ -3572,7 +3662,14 @@ async function planPeerNamespaceCensus(args) {
|
|
|
3572
3662
|
})
|
|
3573
3663
|
};
|
|
3574
3664
|
} else {
|
|
3575
|
-
const streamedManifest = args.manifestStream ? await fetchPeerManifestStream(
|
|
3665
|
+
const streamedManifest = args.manifestStream ? await fetchPeerManifestStream(
|
|
3666
|
+
peerUrl,
|
|
3667
|
+
ns,
|
|
3668
|
+
args.resolvedToken,
|
|
3669
|
+
args.fetchFn,
|
|
3670
|
+
args.timeoutMs,
|
|
3671
|
+
args.signal
|
|
3672
|
+
) : null;
|
|
3576
3673
|
peerManifest = streamedManifest;
|
|
3577
3674
|
if (peerManifest) {
|
|
3578
3675
|
clientBuilt = false;
|
|
@@ -3583,6 +3680,7 @@ async function planPeerNamespaceCensus(args) {
|
|
|
3583
3680
|
files: peerData.files,
|
|
3584
3681
|
parseMemory: parseFrontmatter2,
|
|
3585
3682
|
citationTemplate: args.citationTemplate,
|
|
3683
|
+
signal: args.signal,
|
|
3586
3684
|
// Older peers need one content request per memory file; prior cache
|
|
3587
3685
|
// rows (sha-keyed) skip the ones already fetched.
|
|
3588
3686
|
cachedFiles: priorPeerFiles ?? args.localManifestFiles,
|
|
@@ -3595,7 +3693,8 @@ async function planPeerNamespaceCensus(args) {
|
|
|
3595
3693
|
file.path,
|
|
3596
3694
|
args.resolvedToken,
|
|
3597
3695
|
args.fetchFn,
|
|
3598
|
-
args.timeoutMs
|
|
3696
|
+
args.timeoutMs,
|
|
3697
|
+
args.signal
|
|
3599
3698
|
);
|
|
3600
3699
|
} catch (error) {
|
|
3601
3700
|
readFailure = error instanceof Error ? error : new Error(String(error));
|
|
@@ -3622,7 +3721,8 @@ async function planPeerNamespaceCensus(args) {
|
|
|
3622
3721
|
tombstonePath,
|
|
3623
3722
|
args.resolvedToken,
|
|
3624
3723
|
args.fetchFn,
|
|
3625
|
-
args.timeoutMs
|
|
3724
|
+
args.timeoutMs,
|
|
3725
|
+
args.signal
|
|
3626
3726
|
);
|
|
3627
3727
|
if (!remote) {
|
|
3628
3728
|
throw new Error(`failed to read peer tombstone evidence: ${tombstonePath}`);
|
|
@@ -3640,33 +3740,35 @@ async function planPeerNamespaceCensus(args) {
|
|
|
3640
3740
|
}
|
|
3641
3741
|
const mapped = tombstonedFileDigests(evidence, peerManifests);
|
|
3642
3742
|
for (const digest of peerData.tombstones) mapped.add(digest);
|
|
3743
|
+
const priorByPath = new Map((priorPeerFiles ?? []).map((file) => [file.path, file.sha256.toLowerCase()]));
|
|
3744
|
+
let reused = 0;
|
|
3745
|
+
for (const file of peerManifests.files) {
|
|
3746
|
+
if (priorByPath.get(file.path) === file.sha256.toLowerCase()) reused += 1;
|
|
3747
|
+
}
|
|
3748
|
+
args.onProgress?.({
|
|
3749
|
+
side: "peer",
|
|
3750
|
+
namespace: ns,
|
|
3751
|
+
index: args.index,
|
|
3752
|
+
total: args.total,
|
|
3753
|
+
reused,
|
|
3754
|
+
computed: peerManifests.files.length - reused
|
|
3755
|
+
});
|
|
3643
3756
|
if (args.cache) {
|
|
3644
|
-
|
|
3645
|
-
|
|
3646
|
-
|
|
3647
|
-
|
|
3757
|
+
if (censusWatermark(peerManifests.files) === watermark) {
|
|
3758
|
+
await args.cache.writeEntry({
|
|
3759
|
+
version: 1,
|
|
3760
|
+
scope: args.cache.scope,
|
|
3761
|
+
side: "peer",
|
|
3762
|
+
namespace: ns,
|
|
3763
|
+
watermark,
|
|
3764
|
+
fileCount: peerManifests.files.length,
|
|
3765
|
+
capturedAtMs: Date.now(),
|
|
3766
|
+
savedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3767
|
+
...args.peerManifestRevision !== void 0 ? { peerManifestRevision: args.peerManifestRevision } : {},
|
|
3768
|
+
...clientBuilt ? { clientBuilt: true } : {},
|
|
3769
|
+
files: peerManifests.files
|
|
3770
|
+
});
|
|
3648
3771
|
}
|
|
3649
|
-
args.onProgress?.({
|
|
3650
|
-
side: "peer",
|
|
3651
|
-
namespace: ns,
|
|
3652
|
-
index: args.index,
|
|
3653
|
-
total: args.total,
|
|
3654
|
-
reused,
|
|
3655
|
-
computed: peerManifests.files.length - reused
|
|
3656
|
-
});
|
|
3657
|
-
await args.cache.writeEntry({
|
|
3658
|
-
version: 1,
|
|
3659
|
-
scope: args.cache.scope,
|
|
3660
|
-
side: "peer",
|
|
3661
|
-
namespace: ns,
|
|
3662
|
-
watermark,
|
|
3663
|
-
fileCount: peerManifests.files.length,
|
|
3664
|
-
capturedAtMs: Date.now(),
|
|
3665
|
-
savedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3666
|
-
...args.peerManifestRevision !== void 0 ? { peerManifestRevision: args.peerManifestRevision } : {},
|
|
3667
|
-
...clientBuilt ? { clientBuilt: true } : {},
|
|
3668
|
-
files: peerManifests.files
|
|
3669
|
-
});
|
|
3670
3772
|
}
|
|
3671
3773
|
return { manifest: peerManifests, tombstones: mapped };
|
|
3672
3774
|
}
|
|
@@ -4560,6 +4662,7 @@ async function updateCursorsForPlan(plan, options) {
|
|
|
4560
4662
|
if (!memoryDir) return;
|
|
4561
4663
|
const namespaces = new Set(plan.byNamespace.map((n) => n.namespace));
|
|
4562
4664
|
for (const ns of namespaces) {
|
|
4665
|
+
options.signal?.throwIfAborted();
|
|
4563
4666
|
const cursorPath = defaultConvergeCursorPath(memoryDir, peerUrl, ns);
|
|
4564
4667
|
const priorSemanticAgreements = options.semanticAgreementsByNamespace?.get(ns) ?? (await readConvergeCursor(cursorPath))?.semanticAgreements ?? [];
|
|
4565
4668
|
const { baseFiles, semanticAgreements } = deriveConvergeCursorBase(plan.entries, ns, priorSemanticAgreements);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@remnic/cli",
|
|
3
|
-
"version": "9.69.
|
|
3
|
+
"version": "9.69.47",
|
|
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": "^9.69.
|
|
30
|
-
"@remnic/server": "^9.69.
|
|
31
|
-
"@remnic/core": "^9.69.
|
|
29
|
+
"@remnic/plugin-pi": "^9.69.47",
|
|
30
|
+
"@remnic/server": "^9.69.47",
|
|
31
|
+
"@remnic/core": "^9.69.47"
|
|
32
32
|
},
|
|
33
33
|
"peerDependencies": {
|
|
34
|
-
"@remnic/bench": "^9.69.
|
|
35
|
-
"@remnic/plugin-openclaw": "^9.69.
|
|
36
|
-
"@remnic/export-weclone": "^9.69.
|
|
37
|
-
"@remnic/import-weclone": "^9.69.
|
|
38
|
-
"@remnic/import-chatgpt": "^9.69.
|
|
39
|
-
"@remnic/import-claude": "^9.69.
|
|
40
|
-
"@remnic/import-gemini": "^9.69.
|
|
41
|
-
"@remnic/import-lossless-claw": "^9.69.
|
|
42
|
-
"@remnic/import-mem0": "^9.69.
|
|
43
|
-
"@remnic/import-supermemory": "^9.69.
|
|
44
|
-
"@remnic/import-okf": "^9.69.
|
|
45
|
-
"@remnic/connector-limitless": "^9.69.
|
|
46
|
-
"@remnic/connector-bee": "^9.69.
|
|
47
|
-
"@remnic/connector-omi": "^9.69.
|
|
48
|
-
"@remnic/capture-audio": "^9.69.
|
|
34
|
+
"@remnic/bench": "^9.69.47",
|
|
35
|
+
"@remnic/plugin-openclaw": "^9.69.47",
|
|
36
|
+
"@remnic/export-weclone": "^9.69.47",
|
|
37
|
+
"@remnic/import-weclone": "^9.69.47",
|
|
38
|
+
"@remnic/import-chatgpt": "^9.69.47",
|
|
39
|
+
"@remnic/import-claude": "^9.69.47",
|
|
40
|
+
"@remnic/import-gemini": "^9.69.47",
|
|
41
|
+
"@remnic/import-lossless-claw": "^9.69.47",
|
|
42
|
+
"@remnic/import-mem0": "^9.69.47",
|
|
43
|
+
"@remnic/import-supermemory": "^9.69.47",
|
|
44
|
+
"@remnic/import-okf": "^9.69.47",
|
|
45
|
+
"@remnic/connector-limitless": "^9.69.47",
|
|
46
|
+
"@remnic/connector-bee": "^9.69.47",
|
|
47
|
+
"@remnic/connector-omi": "^9.69.47",
|
|
48
|
+
"@remnic/capture-audio": "^9.69.47"
|
|
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-claude": "9.69.
|
|
105
|
-
"@remnic/import-chatgpt": "9.69.
|
|
106
|
-
"@remnic/import-gemini": "9.69.
|
|
107
|
-
"@remnic/import-
|
|
108
|
-
"@remnic/import-mem0": "9.69.
|
|
109
|
-
"@remnic/connector-limitless": "9.69.
|
|
110
|
-
"@remnic/connector-bee": "9.69.
|
|
111
|
-
"@remnic/
|
|
112
|
-
"@remnic/
|
|
100
|
+
"@remnic/bench": "9.69.47",
|
|
101
|
+
"@remnic/plugin-openclaw": "9.69.47",
|
|
102
|
+
"@remnic/export-weclone": "9.69.47",
|
|
103
|
+
"@remnic/import-weclone": "9.69.47",
|
|
104
|
+
"@remnic/import-claude": "9.69.47",
|
|
105
|
+
"@remnic/import-chatgpt": "9.69.47",
|
|
106
|
+
"@remnic/import-gemini": "9.69.47",
|
|
107
|
+
"@remnic/import-lossless-claw": "9.69.47",
|
|
108
|
+
"@remnic/import-mem0": "9.69.47",
|
|
109
|
+
"@remnic/connector-limitless": "9.69.47",
|
|
110
|
+
"@remnic/connector-bee": "9.69.47",
|
|
111
|
+
"@remnic/import-supermemory": "9.69.47",
|
|
112
|
+
"@remnic/connector-omi": "9.69.47"
|
|
113
113
|
},
|
|
114
114
|
"license": "MIT",
|
|
115
115
|
"repository": {
|