@remnic/plugin-openclaw 1.0.36 → 1.0.38

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.
@@ -1,9 +1,9 @@
1
1
  import {
2
2
  exportCapsule,
3
3
  isValidCapsuleSince
4
- } from "./chunk-3IHGISUN.js";
4
+ } from "./chunk-TXOEHSVP.js";
5
5
  import "./chunk-YKV4EFUI.js";
6
- import "./chunk-E4RM7637.js";
6
+ import "./chunk-QCCP4RU5.js";
7
7
  import "./chunk-ZS6VABML.js";
8
8
  import "./chunk-4XDQ3KEC.js";
9
9
  import "./chunk-MXFJXUHC.js";
@@ -1,8 +1,8 @@
1
1
  import {
2
2
  importCapsule
3
- } from "./chunk-NNAN63QK.js";
3
+ } from "./chunk-CEL5ZLKP.js";
4
4
  import "./chunk-YKV4EFUI.js";
5
- import "./chunk-E4RM7637.js";
5
+ import "./chunk-QCCP4RU5.js";
6
6
  import "./chunk-ZS6VABML.js";
7
7
  import "./chunk-4XDQ3KEC.js";
8
8
  import "./chunk-MXFJXUHC.js";
@@ -7,7 +7,7 @@ import {
7
7
  } from "./chunk-YKV4EFUI.js";
8
8
  import {
9
9
  parseExportBundle
10
- } from "./chunk-E4RM7637.js";
10
+ } from "./chunk-QCCP4RU5.js";
11
11
  import "./chunk-4XDQ3KEC.js";
12
12
  import {
13
13
  createVersion
@@ -7,7 +7,7 @@ import {
7
7
  } from "./chunk-YKV4EFUI.js";
8
8
  import {
9
9
  parseExportBundle
10
- } from "./chunk-E4RM7637.js";
10
+ } from "./chunk-QCCP4RU5.js";
11
11
  import {
12
12
  decryptCapsuleFileInMemory,
13
13
  isEncryptedCapsuleFile
@@ -34,6 +34,11 @@ var SemverLikeSchema = external_exports.string().min(1, "capsule.version must no
34
34
  /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/,
35
35
  "capsule.version must be a semver-like string (e.g. 1.0.0)"
36
36
  );
37
+ function isSafePosixRelativePathPrefix(value) {
38
+ if (value.startsWith("/") || value.includes("\\")) return false;
39
+ const segments = value.split("/");
40
+ return segments.every((segment) => segment !== "" && segment !== "." && segment !== "..");
41
+ }
37
42
  var CapsuleRetrievalPolicySchema = external_exports.object({
38
43
  /**
39
44
  * Per-tier weight overrides applied during recall when the capsule is the
@@ -66,8 +71,8 @@ var CapsuleParentSchema = external_exports.object({
66
71
  * capsule's records were placed. Typically `forks/<parent-capsule-id>`.
67
72
  * Must be a non-empty string with no leading slash.
68
73
  */
69
- forkRoot: external_exports.string().min(1, "capsule.parent.forkRoot must not be empty").refine((v) => !v.startsWith("/"), {
70
- message: "capsule.parent.forkRoot must be a relative path (no leading slash)"
74
+ forkRoot: external_exports.string().min(1, "capsule.parent.forkRoot must not be empty").refine(isSafePosixRelativePathPrefix, {
75
+ message: "capsule.parent.forkRoot must be a normalized posix-relative path with no traversal segments"
71
76
  })
72
77
  });
73
78
  var CapsuleBlockSchema = external_exports.object({
@@ -9,7 +9,7 @@ import {
9
9
  CapsuleBlockSchema,
10
10
  ExportBundleV2Schema,
11
11
  ExportManifestV2Schema
12
- } from "./chunk-E4RM7637.js";
12
+ } from "./chunk-QCCP4RU5.js";
13
13
  import {
14
14
  encryptCapsuleFile
15
15
  } from "./chunk-ZS6VABML.js";
package/dist/index.js CHANGED
@@ -1,12 +1,12 @@
1
1
  import "./chunk-UZJ7EERS.js";
2
- import "./chunk-NNAN63QK.js";
2
+ import "./chunk-CEL5ZLKP.js";
3
3
  import {
4
4
  isValidCapsuleSince
5
- } from "./chunk-3IHGISUN.js";
5
+ } from "./chunk-TXOEHSVP.js";
6
6
  import "./chunk-YKV4EFUI.js";
7
7
  import {
8
8
  CAPSULE_ID_PATTERN
9
- } from "./chunk-E4RM7637.js";
9
+ } from "./chunk-QCCP4RU5.js";
10
10
  import "./chunk-ZS6VABML.js";
11
11
  import "./chunk-QQXJODFL.js";
12
12
  import "./chunk-BU5KJVWF.js";
@@ -6098,6 +6098,7 @@ function throwIfAborted(signal, message = "operation aborted") {
6098
6098
  var QMD_UPDATE_BACKOFF_MS = 15 * 60 * 1e3;
6099
6099
  var QMD_EMBED_BACKOFF_MS = 60 * 60 * 1e3;
6100
6100
  var QMD_CLI_WARN_THROTTLE_MS = 15 * 60 * 1e3;
6101
+ var QMD_AUTO_UPGRADE_CHECK_INTERVAL_MS = 24 * 60 * 6e4;
6101
6102
  var QMD_FALLBACK_PATHS = [
6102
6103
  path8.join(os2.homedir(), ".bun", "bin", "qmd"),
6103
6104
  "/usr/local/bin/qmd",
@@ -6694,6 +6695,7 @@ var recallRequestSchema = external_exports.object({
6694
6695
  topK: external_exports.number().int().min(0).max(200).optional(),
6695
6696
  mode: external_exports.enum(["auto", "no_recall", "minimal", "full", "graph_mode"]).optional(),
6696
6697
  includeDebug: external_exports.boolean().optional(),
6698
+ idempotencyKey: idempotencyKeySchema,
6697
6699
  disclosure: recallDisclosureSchema.optional(),
6698
6700
  codingContext: codingContextSchema.optional(),
6699
6701
  /** Working directory for auto git-context resolution (issue #569). */
@@ -7788,17 +7790,13 @@ async function syncHeartbeatSurfaceEntries(params) {
7788
7790
  function escapeRegExp(value) {
7789
7791
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
7790
7792
  }
7791
- function buildDelimitedBoundaryClass(value) {
7792
- const parts = ["a-z0-9"];
7793
- if (value.includes("-")) {
7794
- parts.push("-");
7795
- }
7796
- return parts.join("");
7793
+ function buildDelimitedBoundaryClass() {
7794
+ return "a-z0-9-";
7797
7795
  }
7798
7796
  function compileDelimitedPhrasePattern(value) {
7799
7797
  const normalized = value.trim().toLowerCase();
7800
7798
  if (normalized.length === 0) return null;
7801
- const boundaryClass = buildDelimitedBoundaryClass(normalized);
7799
+ const boundaryClass = buildDelimitedBoundaryClass();
7802
7800
  return new RegExp(
7803
7801
  `(^|[^${boundaryClass}])${escapeRegExp(normalized)}([^${boundaryClass}]|$)`
7804
7802
  );
@@ -11324,7 +11322,8 @@ function detectBridgeMode() {
11324
11322
  }
11325
11323
  const daemonHost = readCompatEnv("REMNIC_HOST", "ENGRAM_HOST") ?? DEFAULT_HOST;
11326
11324
  const daemonPort = readDaemonPort();
11327
- if (isDaemonRunning() || shouldProbeDaemonHealth(daemonHost) && checkDaemonHealthSync(daemonHost, daemonPort)) {
11325
+ const hasDaemonPidHint = isDaemonRunning();
11326
+ if ((hasDaemonPidHint || shouldProbeDaemonHealth(daemonHost)) && checkDaemonHealthSync(daemonHost, daemonPort)) {
11328
11327
  return {
11329
11328
  mode: "delegate",
11330
11329
  daemonHost,
@@ -11,7 +11,7 @@ import {
11
11
  ExportMemoryRecordV1Schema,
12
12
  parseExportBundle,
13
13
  parseExportManifest
14
- } from "./chunk-E4RM7637.js";
14
+ } from "./chunk-QCCP4RU5.js";
15
15
  import "./chunk-4XDQ3KEC.js";
16
16
  import "./chunk-I2KLQ2HA.js";
17
17
  export {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "openclaw-remnic",
3
3
  "name": "Remnic OpenClaw Plugin",
4
- "version": "1.0.36",
4
+ "version": "1.0.38",
5
5
  "kind": "memory",
6
6
  "description": "Local semantic memory for OpenClaw with bundled Remnic core runtime. Requires plugins.slots.memory set to this plugin id for hooks to fire.",
7
7
  "setup": {
@@ -288,6 +288,83 @@
288
288
  "default": false,
289
289
  "description": "Allow automated cold-tier backfill jobs to repair parity gaps"
290
290
  },
291
+ "qmdSupportedVersion": {
292
+ "type": "string",
293
+ "default": "2.5.1",
294
+ "description": "Highest QMD version this Remnic build will auto-install"
295
+ },
296
+ "qmdAutoUpgradeEnabled": {
297
+ "type": "boolean",
298
+ "default": false,
299
+ "description": "Opt-in auto-upgrade for PATH/fallback QMD installs"
300
+ },
301
+ "qmdAutoUpgradeCheckIntervalMs": {
302
+ "type": "number",
303
+ "minimum": 60000,
304
+ "default": 86400000,
305
+ "description": "Minimum interval between QMD auto-upgrade checks"
306
+ },
307
+ "qmdChunkStrategy": {
308
+ "type": "string",
309
+ "enum": [
310
+ "auto",
311
+ "regex"
312
+ ],
313
+ "default": "auto",
314
+ "description": "QMD chunk strategy forwarded when the installed QMD supports it"
315
+ },
316
+ "qmdCandidateLimit": {
317
+ "type": "number",
318
+ "minimum": 1,
319
+ "description": "Optional candidate limit forwarded to supported QMD query paths"
320
+ },
321
+ "qmdQueryRerankEnabled": {
322
+ "type": "boolean",
323
+ "default": true,
324
+ "description": "When false, ask supported QMD versions to skip the built-in rerank step"
325
+ },
326
+ "qmdIndexName": {
327
+ "type": "string",
328
+ "description": "Optional QMD named index forwarded as qmd --index <name> when supported. Leave unset during upgrades unless existing QMD data already lives in that named index."
329
+ },
330
+ "qmdForceCpu": {
331
+ "type": "boolean",
332
+ "default": false,
333
+ "description": "Set QMD_FORCE_CPU=1 for QMD child processes to bypass GPU probing and run predictably on CPU"
334
+ },
335
+ "qmdGpuBackend": {
336
+ "type": [
337
+ "string",
338
+ "boolean"
339
+ ],
340
+ "enum": [
341
+ "auto",
342
+ "metal",
343
+ "cuda",
344
+ "vulkan",
345
+ "false",
346
+ false
347
+ ],
348
+ "description": "Optional QMD_LLAMA_GPU override for QMD child processes"
349
+ },
350
+ "qmdEmbedParallelism": {
351
+ "type": "number",
352
+ "minimum": 1,
353
+ "maximum": 8,
354
+ "description": "Optional QMD_EMBED_PARALLELISM override, clamped to 1-8"
355
+ },
356
+ "qmdEmbedModel": {
357
+ "type": "string",
358
+ "description": "Optional QMD_EMBED_MODEL override"
359
+ },
360
+ "qmdRerankModel": {
361
+ "type": "string",
362
+ "description": "Optional QMD_RERANK_MODEL override"
363
+ },
364
+ "qmdGenerateModel": {
365
+ "type": "string",
366
+ "description": "Optional QMD_GENERATE_MODEL override"
367
+ },
291
368
  "embeddingFallbackEnabled": {
292
369
  "type": "boolean",
293
370
  "default": true,
@@ -303,6 +380,10 @@
303
380
  "default": "auto",
304
381
  "description": "Embedding provider selection for semantic fallback"
305
382
  },
383
+ "embeddingFallbackModel": {
384
+ "type": "string",
385
+ "description": "Optional model identifier for embedding fallback requests. Defaults to localLlmModel for legacy installs."
386
+ },
306
387
  "qmdPath": {
307
388
  "type": "string",
308
389
  "description": "Optional absolute path to qmd binary (bypasses PATH/fallback discovery)"
@@ -4869,6 +4950,66 @@
4869
4950
  "advanced": true,
4870
4951
  "help": "Enable automated cold-tier backfill runs for parity repair"
4871
4952
  },
4953
+ "qmdSupportedVersion": {
4954
+ "label": "QMD Supported Version",
4955
+ "advanced": true,
4956
+ "placeholder": "2.5.1"
4957
+ },
4958
+ "qmdAutoUpgradeEnabled": {
4959
+ "label": "QMD Auto Upgrade",
4960
+ "advanced": true,
4961
+ "help": "Upgrade PATH/fallback QMD installs to the supported version; explicit qmdPath installs are skipped"
4962
+ },
4963
+ "qmdAutoUpgradeCheckIntervalMs": {
4964
+ "label": "QMD Auto Upgrade Interval",
4965
+ "advanced": true,
4966
+ "placeholder": "86400000"
4967
+ },
4968
+ "qmdChunkStrategy": {
4969
+ "label": "QMD Chunk Strategy",
4970
+ "advanced": true,
4971
+ "placeholder": "auto"
4972
+ },
4973
+ "qmdCandidateLimit": {
4974
+ "label": "QMD Candidate Limit",
4975
+ "advanced": true
4976
+ },
4977
+ "qmdQueryRerankEnabled": {
4978
+ "label": "QMD Query Rerank",
4979
+ "advanced": true
4980
+ },
4981
+ "qmdIndexName": {
4982
+ "label": "QMD Index Name",
4983
+ "advanced": true,
4984
+ "help": "Use a named QMD index when QMD 2.5+ supports --index. Changing this selects a different SQLite DB; keep it unset for existing default-index installs."
4985
+ },
4986
+ "qmdForceCpu": {
4987
+ "label": "QMD Force CPU",
4988
+ "advanced": true,
4989
+ "help": "Set QMD_FORCE_CPU=1 for QMD child processes"
4990
+ },
4991
+ "qmdGpuBackend": {
4992
+ "label": "QMD GPU Backend",
4993
+ "advanced": true,
4994
+ "placeholder": "metal"
4995
+ },
4996
+ "qmdEmbedParallelism": {
4997
+ "label": "QMD Embed Parallelism",
4998
+ "advanced": true,
4999
+ "placeholder": "4"
5000
+ },
5001
+ "qmdEmbedModel": {
5002
+ "label": "QMD Embed Model",
5003
+ "advanced": true
5004
+ },
5005
+ "qmdRerankModel": {
5006
+ "label": "QMD Rerank Model",
5007
+ "advanced": true
5008
+ },
5009
+ "qmdGenerateModel": {
5010
+ "label": "QMD Generate Model",
5011
+ "advanced": true
5012
+ },
4872
5013
  "embeddingFallbackEnabled": {
4873
5014
  "label": "Embedding Fallback",
4874
5015
  "help": "Use semantic embeddings when QMD is unavailable or no results are found"
@@ -4878,6 +5019,12 @@
4878
5019
  "advanced": true,
4879
5020
  "placeholder": "auto"
4880
5021
  },
5022
+ "embeddingFallbackModel": {
5023
+ "label": "Embedding Model",
5024
+ "advanced": true,
5025
+ "placeholder": "(defaults to local LLM model)",
5026
+ "help": "Optional embedding model for semantic fallback. Use this when the local chat model and embedding model are different."
5027
+ },
4881
5028
  "qmdPath": {
4882
5029
  "label": "QMD Path",
4883
5030
  "advanced": true,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remnic/plugin-openclaw",
3
- "version": "1.0.36",
3
+ "version": "1.0.38",
4
4
  "description": "OpenClaw adapter for Remnic memory with bundled @remnic/core runtime",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -71,7 +71,7 @@
71
71
  },
72
72
  "dependencies": {
73
73
  "openai": "^6.0.0",
74
- "@remnic/core": "^1.1.12"
74
+ "@remnic/core": "^1.1.14"
75
75
  },
76
76
  "peerDependencies": {
77
77
  "openclaw": ">=2026.5.16-beta.1"
@@ -13,10 +13,12 @@ import os
13
13
  import subprocess
14
14
  import sys
15
15
  import time
16
+ import uuid
16
17
  from pathlib import Path
17
18
  from typing import Any
18
19
 
19
20
  MODEL_CACHE: dict[str, Any] = {}
21
+ LOCK_OWNERS: dict[str, str] = {}
20
22
  HASH_EMBED_DIM = 128
21
23
  LOCK_TIMEOUT_SECONDS = 10.0
22
24
  LOCK_STALE_SECONDS = 120.0
@@ -372,20 +374,67 @@ def merge_rows(existing: list[dict[str, Any]], updates: list[dict[str, Any]]) ->
372
374
  return [by_key[row_key] for row_key in order]
373
375
 
374
376
 
375
- def read_lock_owner_pid(lock_path: Path) -> int | None:
377
+ def lock_owner_key(lock_path: Path) -> str:
378
+ return str(lock_path.resolve())
379
+
380
+
381
+ def make_lock_owner_token() -> str:
382
+ return f"{os.getpid()}:{uuid.uuid4().hex}"
383
+
384
+
385
+ def read_lock_contents(lock_path: Path) -> str | None:
376
386
  try:
377
387
  raw = lock_path.read_text(encoding="utf-8").strip()
378
388
  except Exception:
379
389
  return None
380
390
  if not raw:
381
391
  return None
392
+ return raw
393
+
394
+
395
+ def read_lock_owner_pid(lock_path: Path) -> int | None:
396
+ raw = read_lock_contents(lock_path)
397
+ if raw is None:
398
+ return None
399
+ pid_raw = raw.split(":", 1)[0].strip()
382
400
  try:
383
- pid = int(raw)
401
+ pid = int(pid_raw)
384
402
  except ValueError:
385
403
  return None
386
404
  return pid if pid > 0 else None
387
405
 
388
406
 
407
+ def lock_stat_matches(current_stat: os.stat_result, observed_stat: os.stat_result) -> bool:
408
+ if current_stat.st_size != observed_stat.st_size:
409
+ return False
410
+ if current_stat.st_mtime_ns != observed_stat.st_mtime_ns:
411
+ return False
412
+ if getattr(current_stat, "st_ino", 0) and getattr(observed_stat, "st_ino", 0):
413
+ return current_stat.st_ino == observed_stat.st_ino
414
+ return True
415
+
416
+
417
+ def unlink_lock_if_unchanged(
418
+ lock_path: Path,
419
+ observed_owner_token: str | None,
420
+ observed_stat: os.stat_result,
421
+ ) -> bool:
422
+ try:
423
+ current_stat = lock_path.stat()
424
+ except FileNotFoundError:
425
+ return False
426
+ current_owner_token = read_lock_contents(lock_path)
427
+ if current_owner_token != observed_owner_token:
428
+ return False
429
+ if not lock_stat_matches(current_stat, observed_stat):
430
+ return False
431
+ try:
432
+ lock_path.unlink()
433
+ except FileNotFoundError:
434
+ return False
435
+ return True
436
+
437
+
389
438
  def is_process_alive(pid: int) -> bool:
390
439
  if pid <= 0:
391
440
  return False
@@ -420,6 +469,7 @@ def is_process_alive(pid: int) -> bool:
420
469
 
421
470
  def acquire_lock(index_dir: Path, lock_name: str) -> Path:
422
471
  lock_path = index_dir / lock_name
472
+ lock_owner_token = make_lock_owner_token()
423
473
  lock_label = lock_name.strip(".")
424
474
  if lock_label.endswith(".lock"):
425
475
  lock_label = lock_label[: -len(".lock")]
@@ -430,19 +480,22 @@ def acquire_lock(index_dir: Path, lock_name: str) -> Path:
430
480
  try:
431
481
  fd = os.open(str(lock_path), os.O_CREAT | os.O_EXCL | os.O_WRONLY)
432
482
  with os.fdopen(fd, "w", encoding="utf-8") as handle:
433
- handle.write(str(os.getpid()))
483
+ handle.write(lock_owner_token)
484
+ LOCK_OWNERS[lock_owner_key(lock_path)] = lock_owner_token
434
485
  return lock_path
435
486
  except FileExistsError:
436
487
  try:
437
- age = time.time() - lock_path.stat().st_mtime
488
+ observed_stat = lock_path.stat()
438
489
  except FileNotFoundError:
439
490
  continue
440
491
 
492
+ age = time.time() - observed_stat.st_mtime
493
+ observed_owner_token = read_lock_contents(lock_path)
441
494
  owner_pid = read_lock_owner_pid(lock_path)
442
495
  owner_alive = is_process_alive(owner_pid) if owner_pid is not None else False
443
496
 
444
497
  if age > LOCK_STALE_SECONDS and not owner_alive:
445
- lock_path.unlink(missing_ok=True)
498
+ unlink_lock_if_unchanged(lock_path, observed_owner_token, observed_stat)
446
499
  continue
447
500
 
448
501
  if time.monotonic() >= deadline:
@@ -459,7 +512,14 @@ def acquire_writer_lock(index_dir: Path) -> Path:
459
512
 
460
513
 
461
514
  def release_lock(lock_path: Path) -> None:
462
- lock_path.unlink(missing_ok=True)
515
+ lock_owner_token = LOCK_OWNERS.pop(lock_owner_key(lock_path), None)
516
+ if lock_owner_token is None:
517
+ return
518
+ try:
519
+ observed_stat = lock_path.stat()
520
+ except FileNotFoundError:
521
+ return
522
+ unlink_lock_if_unchanged(lock_path, lock_owner_token, observed_stat)
463
523
 
464
524
 
465
525
  def release_index_lock(lock_path: Path) -> None: