@remnic/cli 9.69.45 → 9.69.46

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 +146 -72
  2. 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 (typeof candidate.status !== "string") return null;
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 is stolen; a lock file is only ever
2349
- * replaced atomically, so a steal racing the true owner at worst causes
2350
- * redundant recompute entries themselves are content-addressed and
2351
- * written via rename, so concurrent identical writes cannot interleave.
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
- const payload = `${JSON.stringify({
2359
- pid: process.pid,
2360
- startTicks: PROCESS_START_TICKS,
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
- await fs16.writeFile(lockPath, payload, { flag: "wx" });
2366
- } catch (error) {
2367
- if (error.code !== "EEXIST") throw error;
2368
- let held = {};
2384
+ const payload = cache.lockPayload();
2369
2385
  try {
2370
- held = JSON.parse(await fs16.readFile(lockPath, "utf8"));
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
- const owner = lockOwnerLive(held);
2375
- if (owner.live) throw new ConvergePlanCacheBusyError(owner.pid);
2376
- const tmp = `${lockPath}.${process.pid}.tmp`;
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
- /** Release the cross-process lock. Safe to call more than once. */
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
- await unlinkQuiet(this.lockPath);
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
 
@@ -2970,20 +3037,20 @@ async function planLocalNamespaceCensus(args) {
2970
3037
  identityCache,
2971
3038
  classificationUpdates
2972
3039
  );
3040
+ const priorByPath = new Map((priorFiles ?? []).map((file) => [file.path, file.sha256.toLowerCase()]));
3041
+ let reused = 0;
3042
+ for (const file of files) {
3043
+ if (priorByPath.get(file.path) === file.sha256.toLowerCase()) reused += 1;
3044
+ }
3045
+ args.onProgress?.({
3046
+ side: "local",
3047
+ namespace: ns,
3048
+ index: args.index,
3049
+ total: args.total,
3050
+ reused,
3051
+ computed: files.length - reused
3052
+ });
2973
3053
  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
3054
  await args.cache.writeEntry({
2988
3055
  version: 1,
2989
3056
  scope: args.cache.scope,
@@ -3025,7 +3092,7 @@ import {
3025
3092
  RECONCILE_MANIFEST_SCHEMA_VERSION as RECONCILE_MANIFEST_SCHEMA_VERSION2
3026
3093
  } from "@remnic/core/reconcile/manifest.js";
3027
3094
  var SHA256_PATTERN = /^[a-f0-9]{64}$/i;
3028
- var MEMORY_STATUSES = /* @__PURE__ */ new Set([
3095
+ var MEMORY_STATUSES2 = /* @__PURE__ */ new Set([
3029
3096
  "active",
3030
3097
  "pending_review",
3031
3098
  "rejected",
@@ -3062,7 +3129,7 @@ function parseMemory(value) {
3062
3129
  if (typeof memory.contentHash !== "string" || !SHA256_PATTERN.test(memory.contentHash)) {
3063
3130
  throw new Error("peer manifest memory metadata had invalid contentHash");
3064
3131
  }
3065
- if (typeof memory.status !== "string" || !MEMORY_STATUSES.has(memory.status)) {
3132
+ if (typeof memory.status !== "string" || !MEMORY_STATUSES2.has(memory.status)) {
3066
3133
  throw new Error("peer manifest memory metadata had invalid status");
3067
3134
  }
3068
3135
  const contentHash = memory.contentHash.toLowerCase();
@@ -3561,7 +3628,11 @@ async function planPeerNamespaceCensus(args) {
3561
3628
  const watermark = censusWatermark(peerData.files);
3562
3629
  let peerManifest = null;
3563
3630
  let clientBuilt = clientBuiltPrior;
3564
- if (reusableEntry && reusableEntry.watermark === watermark && reusableEntry.fileCount === peerData.files.length) {
3631
+ if (reusableEntry && reusableEntry.watermark === watermark && reusableEntry.fileCount === peerData.files.length && // A peer upgraded to manifest streaming must not keep serving this
3632
+ // client's older parser semantics on an unchanged watermark (#2965):
3633
+ // client-built rows stay a warm base only; the stream runs once and
3634
+ // the checkpoint becomes a streamed entry.
3635
+ !(args.manifestStream && clientBuiltPrior)) {
3565
3636
  const freshByPath = new Map(peerData.files.map((file) => [file.path, file]));
3566
3637
  peerManifest = {
3567
3638
  format: RECONCILE_MANIFEST_FORMAT2,
@@ -3640,33 +3711,35 @@ async function planPeerNamespaceCensus(args) {
3640
3711
  }
3641
3712
  const mapped = tombstonedFileDigests(evidence, peerManifests);
3642
3713
  for (const digest of peerData.tombstones) mapped.add(digest);
3714
+ const priorByPath = new Map((priorPeerFiles ?? []).map((file) => [file.path, file.sha256.toLowerCase()]));
3715
+ let reused = 0;
3716
+ for (const file of peerManifests.files) {
3717
+ if (priorByPath.get(file.path) === file.sha256.toLowerCase()) reused += 1;
3718
+ }
3719
+ args.onProgress?.({
3720
+ side: "peer",
3721
+ namespace: ns,
3722
+ index: args.index,
3723
+ total: args.total,
3724
+ reused,
3725
+ computed: peerManifests.files.length - reused
3726
+ });
3643
3727
  if (args.cache) {
3644
- const priorByPath = new Map((priorPeerFiles ?? []).map((file) => [file.path, file.sha256.toLowerCase()]));
3645
- let reused = 0;
3646
- for (const file of peerManifests.files) {
3647
- if (priorByPath.get(file.path) === file.sha256.toLowerCase()) reused += 1;
3728
+ if (censusWatermark(peerManifests.files) === watermark) {
3729
+ await args.cache.writeEntry({
3730
+ version: 1,
3731
+ scope: args.cache.scope,
3732
+ side: "peer",
3733
+ namespace: ns,
3734
+ watermark,
3735
+ fileCount: peerManifests.files.length,
3736
+ capturedAtMs: Date.now(),
3737
+ savedAt: (/* @__PURE__ */ new Date()).toISOString(),
3738
+ ...args.peerManifestRevision !== void 0 ? { peerManifestRevision: args.peerManifestRevision } : {},
3739
+ ...clientBuilt ? { clientBuilt: true } : {},
3740
+ files: peerManifests.files
3741
+ });
3648
3742
  }
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
3743
  }
3671
3744
  return { manifest: peerManifests, tombstones: mapped };
3672
3745
  }
@@ -4560,6 +4633,7 @@ async function updateCursorsForPlan(plan, options) {
4560
4633
  if (!memoryDir) return;
4561
4634
  const namespaces = new Set(plan.byNamespace.map((n) => n.namespace));
4562
4635
  for (const ns of namespaces) {
4636
+ options.signal?.throwIfAborted();
4563
4637
  const cursorPath = defaultConvergeCursorPath(memoryDir, peerUrl, ns);
4564
4638
  const priorSemanticAgreements = options.semanticAgreementsByNamespace?.get(ns) ?? (await readConvergeCursor(cursorPath))?.semanticAgreements ?? [];
4565
4639
  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.45",
3
+ "version": "9.69.46",
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.45",
30
- "@remnic/server": "^9.69.45",
31
- "@remnic/core": "^9.69.45"
29
+ "@remnic/plugin-pi": "^9.69.46",
30
+ "@remnic/server": "^9.69.46",
31
+ "@remnic/core": "^9.69.46"
32
32
  },
33
33
  "peerDependencies": {
34
- "@remnic/bench": "^9.69.45",
35
- "@remnic/plugin-openclaw": "^9.69.45",
36
- "@remnic/export-weclone": "^9.69.45",
37
- "@remnic/import-weclone": "^9.69.45",
38
- "@remnic/import-chatgpt": "^9.69.45",
39
- "@remnic/import-claude": "^9.69.45",
40
- "@remnic/import-gemini": "^9.69.45",
41
- "@remnic/import-lossless-claw": "^9.69.45",
42
- "@remnic/import-mem0": "^9.69.45",
43
- "@remnic/import-supermemory": "^9.69.45",
44
- "@remnic/import-okf": "^9.69.45",
45
- "@remnic/connector-limitless": "^9.69.45",
46
- "@remnic/connector-bee": "^9.69.45",
47
- "@remnic/connector-omi": "^9.69.45",
48
- "@remnic/capture-audio": "^9.69.45"
34
+ "@remnic/bench": "^9.69.46",
35
+ "@remnic/plugin-openclaw": "^9.69.46",
36
+ "@remnic/export-weclone": "^9.69.46",
37
+ "@remnic/import-weclone": "^9.69.46",
38
+ "@remnic/import-chatgpt": "^9.69.46",
39
+ "@remnic/import-claude": "^9.69.46",
40
+ "@remnic/import-gemini": "^9.69.46",
41
+ "@remnic/import-lossless-claw": "^9.69.46",
42
+ "@remnic/import-mem0": "^9.69.46",
43
+ "@remnic/import-supermemory": "^9.69.46",
44
+ "@remnic/import-okf": "^9.69.46",
45
+ "@remnic/connector-limitless": "^9.69.46",
46
+ "@remnic/connector-bee": "^9.69.46",
47
+ "@remnic/connector-omi": "^9.69.46",
48
+ "@remnic/capture-audio": "^9.69.46"
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/plugin-openclaw": "9.69.45",
101
- "@remnic/bench": "9.69.45",
102
- "@remnic/import-weclone": "9.69.45",
103
- "@remnic/export-weclone": "9.69.45",
104
- "@remnic/import-claude": "9.69.45",
105
- "@remnic/import-chatgpt": "9.69.45",
106
- "@remnic/import-gemini": "9.69.45",
107
- "@remnic/import-supermemory": "9.69.45",
108
- "@remnic/import-mem0": "9.69.45",
109
- "@remnic/connector-limitless": "9.69.45",
110
- "@remnic/connector-bee": "9.69.45",
111
- "@remnic/connector-omi": "9.69.45",
112
- "@remnic/import-lossless-claw": "9.69.45"
100
+ "@remnic/bench": "9.69.46",
101
+ "@remnic/plugin-openclaw": "9.69.46",
102
+ "@remnic/export-weclone": "9.69.46",
103
+ "@remnic/import-weclone": "9.69.46",
104
+ "@remnic/import-chatgpt": "9.69.46",
105
+ "@remnic/import-claude": "9.69.46",
106
+ "@remnic/import-gemini": "9.69.46",
107
+ "@remnic/import-lossless-claw": "9.69.46",
108
+ "@remnic/import-mem0": "9.69.46",
109
+ "@remnic/connector-limitless": "9.69.46",
110
+ "@remnic/import-supermemory": "9.69.46",
111
+ "@remnic/connector-omi": "9.69.46",
112
+ "@remnic/connector-bee": "9.69.46"
113
113
  },
114
114
  "license": "MIT",
115
115
  "repository": {