@remnic/plugin-openclaw 9.7.14 → 9.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -24,7 +24,7 @@ import * as day_summary_star from "@remnic/core/day-summary";
24
24
  // ../../src/index.ts
25
25
  import OpenAI from "openai";
26
26
  import { createRequire } from "module";
27
- import { createHash as createHash3 } from "crypto";
27
+ import { createHash as createHash4 } from "crypto";
28
28
 
29
29
  // ../../src/config.ts
30
30
  var config_exports = {};
@@ -43,7 +43,7 @@ import { detectSdkCapabilities } from "@remnic/core/sdk-compat";
43
43
  import { Orchestrator, sanitizeSessionKeyForFilename, defaultWorkspaceDir } from "@remnic/core/orchestrator";
44
44
 
45
45
  // ../../src/tools.ts
46
- import { createHash } from "crypto";
46
+ import { createHash as createHash2 } from "crypto";
47
47
  import { Type } from "@sinclair/typebox";
48
48
 
49
49
  // ../../src/temporal-index.ts
@@ -74,6 +74,102 @@ import * as temporal_index_star from "@remnic/core/temporal-index";
74
74
  // ../../src/explicit-capture.ts
75
75
  import { hasInlineExplicitCaptureMarkup, parseInlineExplicitCaptureNotes, persistExplicitCapture, queueExplicitCaptureForReview, shouldProcessInlineExplicitCapture, shouldSkipImplicitExtraction, stripInlineExplicitCaptureNotes, validateExplicitCaptureInput } from "@remnic/core/explicit-capture";
76
76
 
77
+ // ../../src/tools.ts
78
+ import { composeSalvagedEnvelope as composeSalvagedEnvelope2 } from "@remnic/core/salvage-envelope";
79
+
80
+ // ../../src/memory-promote.ts
81
+ import { createHash } from "crypto";
82
+ import { composeSalvagedEnvelope } from "@remnic/core/salvage-envelope";
83
+ import { STRUCTURED_ATTRIBUTE_LIMITS, TAG_LIMITS } from "@remnic/core/write-envelope";
84
+ import { stripAttributesSuffix } from "@remnic/core/storage";
85
+ import { displayErrorDetail } from "@remnic/core/runtime/better-sqlite";
86
+ import { isSafeRouteNamespace } from "@remnic/core/routing/engine";
87
+ function buildPromotionOrigin(srcNamespace, memoryId) {
88
+ const memoryIdHash = createHash("sha256").update(memoryId).digest("hex");
89
+ const fullTag = `promotedFrom:${srcNamespace}:${memoryId}`;
90
+ return {
91
+ tag: fullTag.length <= TAG_LIMITS.maxTagLength ? fullTag : `promotedFromHash:${memoryIdHash}`,
92
+ structuredAttributes: {
93
+ promotedFromNamespace: srcNamespace,
94
+ promotedFromMemoryIdHash: memoryIdHash,
95
+ ...memoryId.length <= STRUCTURED_ATTRIBUTE_LIMITS.maxValueLength ? { promotedFromMemoryId: memoryId } : {}
96
+ }
97
+ };
98
+ }
99
+ async function executeMemoryPromote(orchestrator, params) {
100
+ const { memoryId, fromNamespace, toNamespace, note } = params;
101
+ if (typeof memoryId !== "string" || memoryId.length === 0) {
102
+ return `Invalid memoryId: ${JSON.stringify(memoryId)} \u2014 expected a non-empty memory ID.`;
103
+ }
104
+ for (const [name, value] of [
105
+ ["fromNamespace", fromNamespace],
106
+ ["toNamespace", toNamespace]
107
+ ]) {
108
+ if (value !== void 0 && value !== null && (typeof value !== "string" || !isSafeRouteNamespace(value))) {
109
+ return `Invalid ${name}: ${JSON.stringify(value)} \u2014 expected a path-safe namespace name.`;
110
+ }
111
+ }
112
+ const srcNs = fromNamespace ?? orchestrator.config.defaultNamespace;
113
+ const dstNs = toNamespace ?? orchestrator.config.sharedNamespace;
114
+ const src = await orchestrator.getStorage(srcNs);
115
+ const mem = await src.getMemoryById(memoryId);
116
+ if (!mem) {
117
+ return `Memory not found in ${srcNs}: ${memoryId}`;
118
+ }
119
+ const dst = await orchestrator.getStorage(dstNs);
120
+ const promotionOrigin = buildPromotionOrigin(srcNs, memoryId);
121
+ const promoteEnvelope = composeSalvagedEnvelope(
122
+ "promote",
123
+ {
124
+ content: mem.frontmatter.structuredAttributes ? stripAttributesSuffix(mem.content) : mem.content,
125
+ category: mem.frontmatter.category,
126
+ confidence: mem.frontmatter.confidence,
127
+ tags: (() => {
128
+ const promotionTags = [
129
+ "promoted",
130
+ promotionOrigin.tag,
131
+ ...note ? [`note:${note}`] : []
132
+ ];
133
+ const sourceTags = mem.frontmatter.tags ?? [];
134
+ return Array.from(/* @__PURE__ */ new Set([...promotionTags, ...sourceTags])).slice(
135
+ 0,
136
+ TAG_LIMITS.maxTags
137
+ );
138
+ })(),
139
+ structuredAttributes: promotionOrigin.structuredAttributes,
140
+ entityRef: mem.frontmatter.entityRef
141
+ },
142
+ { source: "promote" }
143
+ );
144
+ const { id: newId, tombstoneBlocked } = await dst.writeSealedMemory(
145
+ promoteEnvelope,
146
+ {
147
+ importance: mem.frontmatter.importance,
148
+ supersedes: mem.frontmatter.supersedes,
149
+ links: mem.frontmatter.links
150
+ }
151
+ );
152
+ if (tombstoneBlocked) {
153
+ return `Promotion of ${srcNs}:${memoryId} \u2192 ${dstNs}:${newId} is queued for review (tombstone-blocked): no active promoted copy was created.`;
154
+ }
155
+ try {
156
+ if (orchestrator.config.queryAwareIndexingEnabled && await indexesExistAsync(orchestrator.config.memoryDir)) {
157
+ const promoted = await dst.getMemoryById(newId).catch(() => null);
158
+ if (promoted?.path && promoted.frontmatter?.created) {
159
+ await indexMemoryAsync(
160
+ orchestrator.config.memoryDir,
161
+ promoted.path,
162
+ promoted.frontmatter.created,
163
+ promoted.frontmatter.tags ?? []
164
+ );
165
+ }
166
+ }
167
+ } catch (err) {
168
+ return `Promoted ${srcNs}:${memoryId} \u2192 ${dstNs}:${newId} (index refresh failed \u2014 recall picks it up on the next maintenance pass: ${displayErrorDetail(err)})`;
169
+ }
170
+ return `Promoted ${srcNs}:${memoryId} \u2192 ${dstNs}:${newId}`;
171
+ }
172
+
77
173
  // ../../src/tools.ts
78
174
  import { WorkStorage } from "@remnic/core/work/storage";
79
175
  import { exportWorkBoardMarkdown, exportWorkBoardSnapshot, importWorkBoardSnapshot } from "@remnic/core/work/board";
@@ -310,7 +406,7 @@ function registerTools(api, orchestrator) {
310
406
  const actionTypeSet = new Set(actionTypes);
311
407
  function promptHashForTelemetry(input) {
312
408
  if (typeof input !== "string" || input.trim().length === 0) return void 0;
313
- return createHash("sha256").update(input).digest("hex").slice(0, 16);
409
+ return createHash2("sha256").update(input).digest("hex").slice(0, 16);
314
410
  }
315
411
  function normalizeMemoryCategory(value, fallback) {
316
412
  const normalized = asNonEmptyString(value);
@@ -1503,9 +1599,13 @@ NOTE: You did not provide sessionKey; under concurrency this may not match your
1503
1599
  let queuedForReview = false;
1504
1600
  switch (action) {
1505
1601
  case "store_episode": {
1506
- const { id: createdId, tombstoneBlocked } = await storage.writeMemory(normalizedCategory ?? "fact", contentValue, {
1602
+ const episodeEnvelope = composeSalvagedEnvelope2(
1603
+ "memory-action",
1604
+ { content: contentValue, category: normalizedCategory ?? "fact" },
1605
+ { source: "memory_action_apply" }
1606
+ );
1607
+ const { id: createdId, tombstoneBlocked } = await storage.writeSealedMemory(episodeEnvelope, {
1507
1608
  actor: "tool.memory_action_apply",
1508
- source: "memory_action_apply",
1509
1609
  memoryKind: "episode"
1510
1610
  });
1511
1611
  outputMemoryIds.push(createdId);
@@ -1518,9 +1618,13 @@ NOTE: You did not provide sessionKey; under concurrency this may not match your
1518
1618
  break;
1519
1619
  }
1520
1620
  case "store_note": {
1521
- const { id: createdId, tombstoneBlocked } = await storage.writeMemory(normalizedCategory ?? "fact", contentValue, {
1522
- actor: "tool.memory_action_apply",
1523
- source: "memory_action_apply"
1621
+ const noteEnvelope = composeSalvagedEnvelope2(
1622
+ "memory-action",
1623
+ { content: contentValue, category: normalizedCategory ?? "fact" },
1624
+ { source: "memory_action_apply" }
1625
+ );
1626
+ const { id: createdId, tombstoneBlocked } = await storage.writeSealedMemory(noteEnvelope, {
1627
+ actor: "tool.memory_action_apply"
1524
1628
  });
1525
1629
  outputMemoryIds.push(createdId);
1526
1630
  if (tombstoneBlocked) {
@@ -1572,9 +1676,13 @@ NOTE: You did not provide sessionKey; under concurrency this may not match your
1572
1676
  break;
1573
1677
  }
1574
1678
  case "summarize_node": {
1575
- const { id: createdId, tombstoneBlocked } = await storage.writeMemory(normalizedCategory ?? "fact", contentValue, {
1679
+ const summaryEnvelope = composeSalvagedEnvelope2(
1680
+ "memory-action",
1681
+ { content: contentValue, category: normalizedCategory ?? "fact" },
1682
+ { source: "memory_action_apply" }
1683
+ );
1684
+ const { id: createdId, tombstoneBlocked } = await storage.writeSealedMemory(summaryEnvelope, {
1576
1685
  actor: "tool.memory_action_apply",
1577
- source: "memory_action_apply",
1578
1686
  sourceMemoryId: memoryIdValue
1579
1687
  });
1580
1688
  outputMemoryIds.push(createdId);
@@ -1922,36 +2030,11 @@ Best for:
1922
2030
  "Namespaces are disabled. Enable `namespacesEnabled: true` to use memory promotion."
1923
2031
  );
1924
2032
  }
1925
- const { memoryId, fromNamespace, toNamespace, note } = params;
1926
- const srcNs = fromNamespace && fromNamespace.length > 0 ? fromNamespace : orchestrator.config.defaultNamespace;
1927
- const dstNs = toNamespace && toNamespace.length > 0 ? toNamespace : orchestrator.config.sharedNamespace;
1928
- const src = await orchestrator.getStorage(srcNs);
1929
- const mem = await src.getMemoryById(memoryId);
1930
- if (!mem) {
1931
- return toolResult(`Memory not found in ${srcNs}: ${memoryId}`);
1932
- }
1933
- const dst = await orchestrator.getStorage(dstNs);
1934
- const { id: newId, tombstoneBlocked } = await dst.writeMemory(mem.frontmatter.category, mem.content, {
1935
- confidence: mem.frontmatter.confidence,
1936
- tags: Array.from(/* @__PURE__ */ new Set([...mem.frontmatter.tags ?? [], "promoted", `promotedFrom:${srcNs}:${memoryId}`, ...note ? [`note:${note}`] : []])),
1937
- entityRef: mem.frontmatter.entityRef,
1938
- source: "promote",
1939
- importance: mem.frontmatter.importance,
1940
- supersedes: mem.frontmatter.supersedes,
1941
- links: mem.frontmatter.links
1942
- });
1943
- if (tombstoneBlocked) {
1944
- return toolResult(
1945
- `Promotion of ${srcNs}:${memoryId} \u2192 ${dstNs}:${newId} is queued for review (tombstone-blocked): no active promoted copy was created.`
1946
- );
1947
- }
1948
- if (orchestrator.config.queryAwareIndexingEnabled && await indexesExistAsync(orchestrator.config.memoryDir)) {
1949
- const promoted = await dst.getMemoryById(newId).catch(() => null);
1950
- if (promoted?.path && promoted.frontmatter?.created) {
1951
- await indexMemoryAsync(orchestrator.config.memoryDir, promoted.path, promoted.frontmatter.created, promoted.frontmatter.tags ?? []);
1952
- }
1953
- }
1954
- return toolResult(`Promoted ${srcNs}:${memoryId} \u2192 ${dstNs}:${newId}`);
2033
+ const message = await executeMemoryPromote(
2034
+ orchestrator,
2035
+ params
2036
+ );
2037
+ return toolResult(message);
1955
2038
  }
1956
2039
  },
1957
2040
  { name: "memory_promote" }
@@ -2989,7 +3072,7 @@ import { createOpikExporter, OpikExporter } from "@remnic/core/opik-exporter";
2989
3072
 
2990
3073
  // ../../src/index.ts
2991
3074
  import { readEnvVar, resolveHomeDir } from "@remnic/core/runtime/env";
2992
- import { displayErrorDetail } from "@remnic/core/runtime/better-sqlite";
3075
+ import { displayErrorDetail as displayErrorDetail2 } from "@remnic/core/runtime/better-sqlite";
2993
3076
 
2994
3077
  // ../../src/migrate/from-engram.ts
2995
3078
  import { migrateFromEngram, rollbackFromEngramMigration } from "@remnic/core/migrate/from-engram";
@@ -3275,6 +3358,7 @@ function buildMemorySearchTool(orchestrator, options = {}) {
3275
3358
  }
3276
3359
 
3277
3360
  // src/runtime-surfaces.ts
3361
+ import { composeSalvagedEnvelope as composeSalvagedEnvelope3 } from "@remnic/core/salvage-envelope";
3278
3362
  import { gatewayTaskChainOptions } from "@remnic/core/fallback-llm";
3279
3363
  function resolveDreamNarrativeRoute(config, directClientAvailable) {
3280
3364
  if (config.modelSource === "gateway") {
@@ -3484,13 +3568,14 @@ async function syncDreamSurfaceEntries(params) {
3484
3568
  };
3485
3569
  const existing = findSurfaceMemoryByAttribute(memories, DREAM_ENTRY_ID_KEY, entry.id);
3486
3570
  if (!existing) {
3487
- const { id: memoryId } = await storage.writeMemory("moment", content, {
3488
- confidence: 0.85,
3489
- tags,
3490
- source: "dreams.md",
3491
- memoryKind: "dream",
3492
- structuredAttributes
3493
- });
3571
+ const { id: memoryId } = await storage.writeSealedMemory(
3572
+ composeSalvagedEnvelope3(
3573
+ "dreams-surface",
3574
+ { content, category: "moment", confidence: 0.85, tags, structuredAttributes },
3575
+ { source: "dreams.md" }
3576
+ ),
3577
+ { memoryKind: "dream" }
3578
+ );
3494
3579
  memories.push(
3495
3580
  makeSurfaceMemorySnapshot({
3496
3581
  id: memoryId,
@@ -3543,13 +3628,14 @@ async function syncHeartbeatSurfaceEntries(params) {
3543
3628
  };
3544
3629
  const existing = findSurfaceMemoryByAttribute(memories, HEARTBEAT_ENTRY_ID_KEY, entry.id) ?? findUniqueSurfaceMemoryBySlug(memories, HEARTBEAT_SURFACE_TYPE, entry.slug);
3545
3630
  if (!existing) {
3546
- const { id: memoryId } = await storage.writeMemory("principle", content, {
3547
- confidence: 0.95,
3548
- tags,
3549
- source: "heartbeat.md",
3550
- memoryKind: "procedural",
3551
- structuredAttributes
3552
- });
3631
+ const { id: memoryId } = await storage.writeSealedMemory(
3632
+ composeSalvagedEnvelope3(
3633
+ "heartbeat-surface",
3634
+ { content, category: "principle", confidence: 0.95, tags, structuredAttributes },
3635
+ { source: "heartbeat.md" }
3636
+ ),
3637
+ { memoryKind: "procedural" }
3638
+ );
3553
3639
  memories.push(
3554
3640
  makeSurfaceMemorySnapshot({
3555
3641
  id: memoryId,
@@ -4051,7 +4137,7 @@ function validateSlotSelection(ctx) {
4051
4137
 
4052
4138
  // ../../src/openclaw-flush-plan-lifecycle.ts
4053
4139
  import path2 from "path";
4054
- import { createHash as createHash2, randomUUID } from "crypto";
4140
+ import { createHash as createHash3, randomUUID } from "crypto";
4055
4141
  import * as fsReadModule0 from "fs/promises";
4056
4142
  const lstat2 = fsReadModule0.lstat;
4057
4143
  const mkdir = fsReadModule0.mkdir;
@@ -4111,7 +4197,7 @@ function isPathInsideOrEqual(parentPath, childPath) {
4111
4197
  return relative === "" || !relative.startsWith("..") && !path2.isAbsolute(relative);
4112
4198
  }
4113
4199
  function hashContent(content) {
4114
- return createHash2("sha256").update(content, "utf8").digest("hex");
4200
+ return createHash3("sha256").update(content, "utf8").digest("hex");
4115
4201
  }
4116
4202
  function utf8PrefixByByteLength(content, byteLength) {
4117
4203
  const buffer = Buffer.from(content, "utf8");
@@ -5986,7 +6072,7 @@ function stableOpenClawConfigSignature(value, seen = /* @__PURE__ */ new WeakSet
5986
6072
  return JSON.stringify(String(value));
5987
6073
  }
5988
6074
  function openClawHostEmbeddingConfigSignature(cfg, apiConfig, workspaceDir) {
5989
- return `sha256:${createHash3("sha256").update(JSON.stringify({
6075
+ return `sha256:${createHash4("sha256").update(JSON.stringify({
5990
6076
  enabled: cfg.hostEmbeddingProviderEnabled !== false,
5991
6077
  memoryDir: cfg.memoryDir,
5992
6078
  providerId: cfg.hostEmbeddingProviderId ?? "",
@@ -6346,7 +6432,7 @@ var pluginDefinition = {
6346
6432
  );
6347
6433
  }
6348
6434
  } catch (error) {
6349
- const detail = displayErrorDetail(error) || "unknown error";
6435
+ const detail = displayErrorDetail2(error) || "unknown error";
6350
6436
  logger_exports.log.warn(`OpenClaw flush-plan processing failed: ${detail}`);
6351
6437
  }
6352
6438
  }
@@ -6540,6 +6626,7 @@ Keep the reflection grounded in the evidence below.
6540
6626
  queryMode: cfg.activeRecallQueryMode,
6541
6627
  promptStyle: cfg.activeRecallPromptStyle,
6542
6628
  customInstruction: cfg.activeRecallCustomInstruction,
6629
+ promptReplacement: cfg.activeRecallPromptReplacement,
6543
6630
  promptAppend: cfg.activeRecallPromptAppend,
6544
6631
  maxSummaryChars: cfg.activeRecallMaxSummaryChars,
6545
6632
  recentUserTurns: cfg.activeRecallRecentUserTurns,
@@ -6635,7 +6722,7 @@ Keep the reflection grounded in the evidence below.
6635
6722
  }
6636
6723
  }, buildInlineExplicitCaptureDedupeKey2 = function(messageKey, note) {
6637
6724
  if (!messageKey) return null;
6638
- return `${messageKey}:inline-memory-note:${createHash3("sha256").update(JSON.stringify(note)).digest("hex")}`;
6725
+ return `${messageKey}:inline-memory-note:${createHash4("sha256").update(JSON.stringify(note)).digest("hex")}`;
6639
6726
  }, resolveStoredCodexThreadId2 = function(sessionKey) {
6640
6727
  const threadId = codexThreadBySession.get(sessionKey);
6641
6728
  return typeof threadId === "string" && threadId.length > 0 ? threadId : null;
@@ -9056,7 +9143,7 @@ function buildOpenClawInboundContentFingerprint(content, message, event, ctx, se
9056
9143
  normalizeThreadId(message.threadId) ?? normalizeThreadId(event.threadId) ?? normalizeThreadId(ctx.threadId) ?? "",
9057
9144
  512
9058
9145
  );
9059
- const contentHash = createHash3("sha256").update(normalizedContent).digest("hex");
9146
+ const contentHash = createHash4("sha256").update(normalizedContent).digest("hex");
9060
9147
  return [
9061
9148
  sessionPart,
9062
9149
  "content",
@@ -9070,7 +9157,7 @@ function buildOpenClawSparseInboundContentFingerprint(content, sessionKey) {
9070
9157
  const normalizedContent = content.trim();
9071
9158
  if (!normalizedContent) return null;
9072
9159
  const sessionPart = truncateMetadataValue(sessionKey || "default", 512);
9073
- const contentHash = createHash3("sha256").update(normalizedContent).digest("hex");
9160
+ const contentHash = createHash4("sha256").update(normalizedContent).digest("hex");
9074
9161
  return `${sessionPart}\0sparse-content\0${contentHash}`;
9075
9162
  }
9076
9163
  function withReplyExtractionHint(content, metadata) {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "openclaw-remnic",
3
3
  "name": "Remnic OpenClaw Plugin",
4
- "version": "9.7.14",
4
+ "version": "9.8.0",
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": {
@@ -225,6 +225,14 @@
225
225
  "type": "string",
226
226
  "description": "Optional additional guidance for the active-recall builder."
227
227
  },
228
+ "activeRecallPromptOverride": {
229
+ "type": "string",
230
+ "description": "Legacy optional custom instruction for the active-recall builder."
231
+ },
232
+ "activeRecallPromptReplacement": {
233
+ "type": "string",
234
+ "description": "Optional complete replacement prompt for the active-recall builder."
235
+ },
228
236
  "activeRecallPromptStyle": {
229
237
  "type": "string",
230
238
  "enum": [
@@ -585,6 +593,13 @@
585
593
  "default": 5,
586
594
  "description": "Max turns before forced extraction"
587
595
  },
596
+ "bufferSaveDebounceMs": {
597
+ "type": "integer",
598
+ "default": 3000,
599
+ "minimum": 0,
600
+ "maximum": 2147483647,
601
+ "description": "Debounce window (ms) for persisting the smart buffer to state/buffer.json. Steady-state buffering coalesces the whole-state write onto a trailing-edge timer instead of rewriting every turn; extraction trigger/clear and shutdown force an immediate flush. 0 restores save-every-turn. Capped at 2147483647 (Node's 32-bit setTimeout limit)."
602
+ },
588
603
  "bufferSurpriseK": {
589
604
  "type": "number",
590
605
  "default": 5,
@@ -1496,6 +1511,68 @@
1496
1511
  "default": "off",
1497
1512
  "description": "Passive correction capture (#1581) — detects corrections expressed in conversation during extraction and routes them to the Correction Contract (#1580). \"off\" disables detection (default, rule 48); \"queue\" plans corrections for human review in the review list; \"auto\" applies immediately when all safety guards pass (confidence floor, blast-radius cap, allowed action/classification), otherwise falls back to queue. Invalid values are rejected (rule 51)."
1498
1513
  },
1514
+ "correction": {
1515
+ "type": "object",
1516
+ "description": "Memory-correction contract (issue #1580) — nested form; nested keys win over the flat legacy correction* keys.",
1517
+ "properties": {
1518
+ "enabled": {
1519
+ "type": "boolean",
1520
+ "description": "Enable the correction contract (plan/apply). Default true."
1521
+ },
1522
+ "applyRequiresConfirm": {
1523
+ "type": "boolean",
1524
+ "description": "Require confirm: true on memory_correct_apply. Default true."
1525
+ },
1526
+ "maxAffected": {
1527
+ "type": "integer",
1528
+ "minimum": 1,
1529
+ "description": "Maximum memories one correction plan may touch. Default 10."
1530
+ },
1531
+ "planTtlHours": {
1532
+ "type": "number",
1533
+ "minimum": 1,
1534
+ "description": "Hours a planned correction stays applicable. Default 24."
1535
+ }
1536
+ }
1537
+ },
1538
+ "correctionCapture": {
1539
+ "type": "object",
1540
+ "description": "Implicit correction capture (nested form; nested keys win over the flat legacy correctionCapture* keys).",
1541
+ "properties": {
1542
+ "mode": {
1543
+ "type": "string",
1544
+ "enum": [
1545
+ "off",
1546
+ "queue",
1547
+ "auto"
1548
+ ],
1549
+ "description": "Implicit capture mode. Default off."
1550
+ },
1551
+ "confidenceFloor": {
1552
+ "type": "number",
1553
+ "minimum": 0,
1554
+ "maximum": 1,
1555
+ "description": "Minimum detector confidence to act on. Default 0.85."
1556
+ },
1557
+ "autoApplyMaxAffected": {
1558
+ "type": "integer",
1559
+ "minimum": 1,
1560
+ "description": "Auto-apply ceiling on affected memories. Default 2."
1561
+ }
1562
+ }
1563
+ },
1564
+ "openclawHostEmbeddingProviderEnabled": {
1565
+ "type": "boolean",
1566
+ "description": "Legacy alias for hostEmbeddingProviderEnabled (OpenClaw host embedding provider). Default true."
1567
+ },
1568
+ "openclawHostEmbeddingProviderId": {
1569
+ "type": "string",
1570
+ "description": "Legacy alias for hostEmbeddingProviderId."
1571
+ },
1572
+ "openclawHostEmbeddingProviderModel": {
1573
+ "type": "string",
1574
+ "description": "Legacy alias for hostEmbeddingProviderModel."
1575
+ },
1499
1576
  "correctionEnabled": {
1500
1577
  "type": "boolean",
1501
1578
  "default": true,
@@ -2382,6 +2459,11 @@
2382
2459
  "default": false,
2383
2460
  "description": "In full mode, compute graph assist for comparison telemetry and snapshots but keep recall output baseline-identical."
2384
2461
  },
2462
+ "graphEdgeCacheIncrementalEnabled": {
2463
+ "type": "boolean",
2464
+ "default": true,
2465
+ "description": "Push single-writer graph-edge appends into the warm edge cache in place (size-revalidated for cross-process coherence) instead of nulling and re-reading the edge file on every write (issue #1904). Set false to restore the pre-#1904 null-on-every-write behavior."
2466
+ },
2385
2467
  "graphEdgeDecayCadenceMs": {
2386
2468
  "type": "number",
2387
2469
  "default": 604800000,
@@ -3158,6 +3240,18 @@
3158
3240
  "default": "",
3159
3241
  "description": "Root directory for memory extensions. Empty string derives from memoryDir (go up to Remnic home and append memory_extensions)."
3160
3242
  },
3243
+ "memoryLifecycleLedgerCompactBytes": {
3244
+ "type": "integer",
3245
+ "default": 67108864,
3246
+ "minimum": 0,
3247
+ "description": "Auto-compact state/memory-lifecycle-ledger.jsonl when it exceeds this many bytes, triggered off the debounced maintenance path (issue #1910). Set 0 to disable. Default 64MB."
3248
+ },
3249
+ "memoryLifecycleLedgerCompactMinIntervalMs": {
3250
+ "type": "integer",
3251
+ "default": 21600000,
3252
+ "minimum": 60000,
3253
+ "description": "Minimum interval between auto-compactions of the lifecycle ledger (issue #1910). Default 6 hours."
3254
+ },
3161
3255
  "memoryLinkingEnabled": {
3162
3256
  "type": "boolean",
3163
3257
  "default": false,
@@ -3766,6 +3860,11 @@
3766
3860
  "default": 900,
3767
3861
  "description": "Token budget for each proactive extraction sub-call (0 disables the proactive second pass)."
3768
3862
  },
3863
+ "proactiveExtractionSkipWhenLocalLlmBusy": {
3864
+ "type": "boolean",
3865
+ "default": true,
3866
+ "description": "Skip the optional proactive extraction second pass when the local LLM background lane is already busy, instead of queueing behind an in-flight extraction and losing its shorter deadline (issue #2011). Set false to always attempt the second pass."
3867
+ },
3769
3868
  "proactiveExtractionTimeoutMs": {
3770
3869
  "type": "number",
3771
3870
  "default": 2500,
@@ -4010,6 +4109,12 @@
4010
4109
  "default": true,
4011
4110
  "description": "Enable debounced background QMD maintenance instead of immediate updates on extraction."
4012
4111
  },
4112
+ "qmdEmbeddingBacklogThreshold": {
4113
+ "type": "integer",
4114
+ "minimum": 0,
4115
+ "default": 1000,
4116
+ "description": "Pending-embedding count above which QMD health reports degraded. Set to 0 to disable backlog degradation."
4117
+ },
4013
4118
  "qmdMaxResults": {
4014
4119
  "type": "number",
4015
4120
  "default": 8,
@@ -4192,6 +4297,13 @@
4192
4297
  "type": "number",
4193
4298
  "description": "Hard character cap for total recall context. Defaults to maxMemoryTokens * 4."
4194
4299
  },
4300
+ "recallProfileMaxRatio": {
4301
+ "type": "number",
4302
+ "minimum": 0,
4303
+ "maximum": 1,
4304
+ "default": 0.3,
4305
+ "description": "Maximum fraction of the total recall budget available to the behavioral profile. Set to 1 to disable the share cap."
4306
+ },
4195
4307
  "recallConfidenceGateEnabled": {
4196
4308
  "type": "boolean",
4197
4309
  "default": false,
@@ -4324,6 +4436,25 @@
4324
4436
  "default": 5,
4325
4437
  "description": "Issue #1582 — number of recent per-session recall snapshots searched when resolving a `[m:xxxx]` handle to a memory id. Older-than-N snapshots are not searched; a miss is tagged rather than widening the window."
4326
4438
  },
4439
+ "recallImpressionsRotateBytes": {
4440
+ "type": "integer",
4441
+ "default": 33554432,
4442
+ "minimum": 0,
4443
+ "description": "Rotate state/recall_impressions.jsonl to .1..N when it exceeds this many bytes (issue #1910). Set 0 to disable. Default 32MB."
4444
+ },
4445
+ "recallImpressionsRotateKeep": {
4446
+ "type": "integer",
4447
+ "default": 5,
4448
+ "minimum": 1,
4449
+ "maximum": 1000,
4450
+ "description": "Number of rotated recall-impression archives to keep (.1 .. .N) (issue #1910). Default 5. Max 1000: each retained slot costs one rename under the held impressions lock per rotation, so a larger value is rejected at config-parse time before it can stall recall recording."
4451
+ },
4452
+ "recallMaxConcurrentPerPrincipal": {
4453
+ "type": "integer",
4454
+ "minimum": 0,
4455
+ "default": 4,
4456
+ "description": "Maximum concurrent recalls executed per principal (issue #1906). Recalls beyond this cap queue FIFO; an aborted waiter leaves the queue immediately. 0 = unlimited (no cap). Replaces the former width-1 budget-lock serialization; budget accounting stays correct because peek/record are synchronous. Set 1 to restore exact serialization."
4457
+ },
4327
4458
  "recallMemoryHandles": {
4328
4459
  "type": "boolean",
4329
4460
  "default": false,
@@ -4334,6 +4465,11 @@
4334
4465
  "default": true,
4335
4466
  "description": "When true, recall multiplies candidate scores by the Memory Worth factor (mw_success / mw_fail counters, see issue #560). Memories with a history of failed sessions sink; neutral/uninstrumented memories are untouched. PR 5 bench: +0.60 precision@5 vs baseline on all 50 seeded cases. Operators can opt out with false."
4336
4467
  },
4468
+ "recallTrustStageCorpusFallbackEnabled": {
4469
+ "type": "boolean",
4470
+ "default": true,
4471
+ "description": "When true (default), the Memory-Worth/TrustScore recall rerank stage probes a per-namespace corpus counter/signal map for candidates whose frontmatter was NOT already loaded on the hot path (cold-tier / embedding-fallback / archive rows). The map is version-keyed (invalidated on corpus mutation) and age-bounded for trust signals, so it hits in steady state without the old 30s-TTL permanent-miss. Set false to disable the corpus fallback entirely (only preloaded-candidate + direct-read paths run) — max performance when every candidate is hot, at the cost of neutral treatment for cold candidates."
4472
+ },
4337
4473
  "hotMemoriesCacheEnabled": {
4338
4474
  "type": "boolean",
4339
4475
  "default": true,
@@ -4428,6 +4564,26 @@
4428
4564
  },
4429
4565
  "forceGeneric": {
4430
4566
  "type": "boolean"
4567
+ },
4568
+ "maxHints": {
4569
+ "type": "number",
4570
+ "description": "Per-section cap on injected hint lines."
4571
+ },
4572
+ "maxSupportingFacts": {
4573
+ "type": "number",
4574
+ "description": "Per-section cap on supporting facts."
4575
+ },
4576
+ "maxRelatedEntities": {
4577
+ "type": "number",
4578
+ "description": "Per-section cap on related entities."
4579
+ },
4580
+ "recentTurns": {
4581
+ "type": "number",
4582
+ "description": "Per-section recent-turn scan window."
4583
+ },
4584
+ "maxRubrics": {
4585
+ "type": "number",
4586
+ "description": "Per-section cap on injected rubrics."
4431
4587
  }
4432
4588
  },
4433
4589
  "required": [
@@ -4495,6 +4651,11 @@
4495
4651
  "default": false,
4496
4652
  "description": "Boost stored reasoning_trace memories in recall results when the incoming query reads like a problem-solving ask (e.g. 'how do I...', 'step by step', 'walk me through...'). Default false - opt in after benchmarking (issue #564)."
4497
4653
  },
4654
+ "recallSingleFlightEnabled": {
4655
+ "type": "boolean",
4656
+ "default": true,
4657
+ "description": "When true (default), identical concurrent recalls for the same principal share a single in-flight execution (single-flight coalescing, issue #1906); each caller still receives its own cloned response and records its own budget event. Set false to restore per-request execution."
4658
+ },
4498
4659
  "recallTranscriptRetentionDays": {
4499
4660
  "type": "integer",
4500
4661
  "minimum": 1,
@@ -4636,6 +4797,11 @@
4636
4797
  "default": "state/routing-rules.json",
4637
4798
  "description": "Relative path under memoryDir for persisted routing rules."
4638
4799
  },
4800
+ "scopedCacheInvalidationEnabled": {
4801
+ "type": "boolean",
4802
+ "default": true,
4803
+ "description": "Evict only the cache layers a given memory write can affect so a plain fact create no longer nukes the QMD result caches or the version-keyed entity cache (issue #1904). Set false to restore the pre-#1904 full-clear-on-every-write behavior."
4804
+ },
4639
4805
  "scopeProfiles": {
4640
4806
  "type": "object",
4641
4807
  "default": {},
@@ -6868,6 +7034,11 @@
6868
7034
  "advanced": true,
6869
7035
  "placeholder": "2500"
6870
7036
  },
7037
+ "proactiveExtractionSkipWhenLocalLlmBusy": {
7038
+ "label": "Skip Proactive When Local LLM Busy",
7039
+ "advanced": true,
7040
+ "help": "Skip the optional proactive extraction pass when the local LLM background lane is saturated, rather than queueing behind an in-flight extraction (issue #2011, default on)"
7041
+ },
6871
7042
  "proactiveExtractionMaxTokens": {
6872
7043
  "label": "Proactive Extraction Max Tokens",
6873
7044
  "advanced": true,
@@ -6987,6 +7158,12 @@
6987
7158
  "advanced": true,
6988
7159
  "placeholder": "8000"
6989
7160
  },
7161
+ "recallProfileMaxRatio": {
7162
+ "label": "Recall Profile Budget Share",
7163
+ "advanced": true,
7164
+ "placeholder": "0.3",
7165
+ "help": "Maximum fraction of the total recall budget available to the behavioral profile. Set to 1 to disable the share cap."
7166
+ },
6990
7167
  "recallPipeline": {
6991
7168
  "label": "Recall Pipeline",
6992
7169
  "advanced": true,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remnic/plugin-openclaw",
3
- "version": "9.7.14",
3
+ "version": "9.8.0",
4
4
  "description": "OpenClaw adapter for Remnic memory with bundled @remnic/core runtime",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -72,7 +72,7 @@
72
72
  "dependencies": {
73
73
  "@sinclair/typebox": "^0.34.0",
74
74
  "openai": "^6.0.0",
75
- "@remnic/core": "^9.7.14"
75
+ "@remnic/core": "^9.8.0"
76
76
  },
77
77
  "peerDependencies": {
78
78
  "openclaw": ">=2026.4.1 || 2026.4.7-1 || 2026.4.9-beta.1 || 2026.4.11-beta.1 || 2026.4.12-beta.1 || 2026.4.14-beta.1 || 2026.4.15-beta.1 || 2026.4.15-beta.2 || 2026.4.19-beta.1 || 2026.4.19-beta.2 || 2026.4.20-beta.1 || 2026.4.20-beta.2 || 2026.4.22-beta.1 || 2026.4.23-beta.1 || 2026.4.23-beta.2 || 2026.4.23-beta.3 || 2026.4.23-beta.4 || 2026.4.23-beta.5 || 2026.4.23-beta.6 || 2026.4.24-beta.1 || 2026.4.24-beta.2 || 2026.4.24-beta.3 || 2026.4.24-beta.4 || 2026.4.24-beta.5 || 2026.4.24-beta.6 || 2026.4.25-beta.1 || 2026.4.25-beta.2 || 2026.4.25-beta.3 || 2026.4.25-beta.4 || 2026.4.25-beta.5 || 2026.4.25-beta.6 || 2026.4.25-beta.7 || 2026.4.25-beta.8 || 2026.4.25-beta.9 || 2026.4.25-beta.10 || 2026.4.25-beta.11 || 2026.4.26-beta.1 || 2026.4.27-beta.1 || 2026.4.29-beta.1 || 2026.4.29-beta.2 || 2026.4.29-beta.3 || 2026.4.29-beta.4 || 2026.4.30-beta.1 || 2026.5.2-beta.1 || 2026.5.2-beta.2 || 2026.5.2-beta.3 || 2026.5.3-beta.1 || 2026.5.3-beta.2 || 2026.5.3-beta.3 || 2026.5.3-beta.4 || 2026.5.3-1 || 2026.5.4-beta.1 || 2026.5.4-beta.2 || 2026.5.4-beta.3 || 2026.5.5-beta.1 || 2026.5.5-beta.2 || 2026.5.6-beta.1 || 2026.5.7-beta.1 || 2026.5.9-beta.1 || 2026.5.10-beta.1 || 2026.5.10-beta.2 || 2026.5.10-beta.3 || 2026.5.10-beta.4 || 2026.5.10-beta.5 || 2026.5.10-beta.6 || 2026.5.12-beta.1 || 2026.5.12-beta.2 || 2026.5.12-beta.3 || 2026.5.12-beta.4 || 2026.5.12-beta.5 || 2026.5.12-beta.6 || 2026.5.12-beta.7 || 2026.5.12-beta.8 || 2026.5.14-beta.1 || 2026.5.14-beta.2 || 2026.5.16-beta.1 || 2026.5.16-beta.2 || 2026.5.16-beta.3 || 2026.5.16-beta.4 || 2026.5.16-beta.5 || 2026.5.16-beta.6 || 2026.5.16-beta.7 || 2026.5.18-beta.1 || 2026.5.19-alpha.1 || 2026.5.19-beta.1 || 2026.5.19-beta.2 || 2026.5.20-beta.1 || 2026.5.20-beta.2 || 2026.5.21-alpha.1 || 2026.5.21-beta.1 || 2026.5.22-beta.1 || 2026.5.23-alpha.1 || 2026.5.24-alpha.1 || 2026.5.24-beta.1 || 2026.5.24-beta.2 || 2026.5.25-alpha.1 || 2026.5.25-alpha.2 || 2026.5.25-beta.1 || 2026.5.26-beta.1 || 2026.5.26-beta.2 || 2026.5.27-alpha.1 || 2026.5.27-beta.1 || 2026.5.28-alpha.1 || 2026.5.28-beta.1 || 2026.5.28-beta.2 || 2026.5.28-beta.3 || 2026.5.28-beta.4 || 2026.5.29-alpha.1 || 2026.5.30-beta.1 || 2026.5.30-beta.2 || 2026.5.31-alpha.1 || 2026.5.31-beta.1 || 2026.5.31-beta.2 || 2026.5.31-beta.3 || 2026.5.31-beta.4 || 2026.6.1-alpha.1 || 2026.6.1-alpha.2 || 2026.6.1-alpha.3 || 2026.6.1-beta.1 || 2026.6.1-beta.2 || 2026.6.1-beta.3 || 2026.6.2-alpha.1 || 2026.6.2-alpha.2 || 2026.6.2-beta.1 || 2026.6.3-alpha.1 || 2026.6.4-alpha.1 || 2026.6.5-alpha.1 || 2026.6.5-alpha.2 || 2026.6.5-beta.1 || 2026.6.5-beta.2 || 2026.6.5-beta.3 || 2026.6.5-beta.5 || 2026.6.5-beta.6 || 2026.6.6-alpha.1 || 2026.6.6-beta.2 || 2026.6.6 || 2026.6.7-beta.1 || 2026.6.8-beta.1 || 2026.6.8-beta.2 || 2026.6.9-beta.1 || 2026.6.10-beta.1 || 2026.6.10-beta.2 || 2026.6.11-beta.1"
@@ -82,7 +82,7 @@
82
82
  "acorn": "^8.16.0",
83
83
  "tsup": "^8.5.1",
84
84
  "typescript": "^5.9.3",
85
- "@remnic/core": "^9.7.14"
85
+ "@remnic/core": "^9.8.0"
86
86
  },
87
87
  "license": "MIT",
88
88
  "repository": {