@remnic/plugin-openclaw 1.0.21 → 1.0.22

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
@@ -667,9 +667,12 @@ function parseConfig(raw) {
667
667
  } else {
668
668
  cfg = baseCfg;
669
669
  }
670
+ const modelSource = cfg.modelSource === "gateway" ? "gateway" : "plugin";
670
671
  let apiKey;
671
672
  if (typeof cfg.openaiApiKey === "string" && cfg.openaiApiKey.length > 0) {
672
673
  apiKey = resolveEnvVars(cfg.openaiApiKey);
674
+ } else if (modelSource === "gateway") {
675
+ apiKey = void 0;
673
676
  } else {
674
677
  apiKey = readEnvVar("OPENAI_API_KEY");
675
678
  }
@@ -1594,7 +1597,7 @@ function parseConfig(raw) {
1594
1597
  // Gateway config (passed from index.ts for fallback AI)
1595
1598
  gatewayConfig: cfg.gatewayConfig,
1596
1599
  // Gateway model source (v9.2) — route LLM calls through gateway agent model chain
1597
- modelSource: cfg.modelSource === "gateway" ? "gateway" : "plugin",
1600
+ modelSource,
1598
1601
  gatewayAgentId: typeof cfg.gatewayAgentId === "string" && cfg.gatewayAgentId.length > 0 ? cfg.gatewayAgentId : "",
1599
1602
  fastGatewayAgentId: typeof cfg.fastGatewayAgentId === "string" && cfg.fastGatewayAgentId.length > 0 ? cfg.fastGatewayAgentId : "",
1600
1603
  // v3.0 namespaces (default off)
@@ -9070,6 +9073,9 @@ var NoopSearchBackend = class {
9070
9073
  }
9071
9074
  async updateCollection(_collection, _execution) {
9072
9075
  }
9076
+ updatesAllCollections() {
9077
+ return false;
9078
+ }
9073
9079
  async embed() {
9074
9080
  }
9075
9081
  async embedCollection(_collection) {
@@ -11359,6 +11365,9 @@ ${stderr}`.split("\n").map((s) => s.trim()).filter((s) => s.length > 0);
11359
11365
  execution?.signal
11360
11366
  );
11361
11367
  }
11368
+ updatesAllCollections() {
11369
+ return true;
11370
+ }
11362
11371
  async runUpdateForCollection(collection, options, signal) {
11363
11372
  if (this.available === false) {
11364
11373
  if (options.strict) {
@@ -23198,6 +23207,9 @@ var TURN_REFERENCE_WINDOW_RADIUS = 0;
23198
23207
  var LEXICAL_CUE_WINDOW_RADIUS = 1;
23199
23208
  var LEXICAL_CUE_SEARCH_LIMIT = 3;
23200
23209
  var LEXICAL_CUE_MAX_TOKENS = 400;
23210
+ var CONTENT_LABEL_SEARCH_LIMIT = 64;
23211
+ var CONTENT_LABEL_MAX_TOKENS = 2e3;
23212
+ var CONTENT_LABEL_MAX_PAIRED_WINDOWS_PER_REFERENCE = 1;
23201
23213
  var LATEST_STATE_CUES = /* @__PURE__ */ new Set([
23202
23214
  "as of",
23203
23215
  "currently",
@@ -23419,6 +23431,13 @@ async function collectTurnReferenceEvidence(options) {
23419
23431
  if (references.length === 0) {
23420
23432
  return;
23421
23433
  }
23434
+ await collectContentLabelReferenceEvidence({
23435
+ engine: options.engine,
23436
+ sessionId: options.sessionId,
23437
+ references,
23438
+ evidenceItems: options.evidenceItems,
23439
+ seenTurns: options.seenTurns
23440
+ });
23422
23441
  const windows = /* @__PURE__ */ new Map();
23423
23442
  for (const reference of references) {
23424
23443
  for (const center of candidateTurnIndexesForReference(reference)) {
@@ -23447,6 +23466,144 @@ async function collectTurnReferenceEvidence(options) {
23447
23466
  );
23448
23467
  }
23449
23468
  }
23469
+ async function collectContentLabelReferenceEvidence(options) {
23470
+ const resolved = /* @__PURE__ */ new Set();
23471
+ for (const reference of options.references) {
23472
+ if (reference.includeDirectTurn) {
23473
+ continue;
23474
+ }
23475
+ const hits = await searchReferenceContentLabels(
23476
+ options.engine,
23477
+ reference.number,
23478
+ options.sessionId
23479
+ );
23480
+ if (hits.length === 0) {
23481
+ continue;
23482
+ }
23483
+ resolved.add(reference.number);
23484
+ let appendedWindows = 0;
23485
+ for (const hit of hits) {
23486
+ if (appendedWindows >= CONTENT_LABEL_MAX_PAIRED_WINDOWS_PER_REFERENCE) {
23487
+ break;
23488
+ }
23489
+ const { fromTurn, toTurn } = contentLabelEvidenceWindow(hit);
23490
+ const expanded = await options.engine.expandContext(
23491
+ hit.session_id,
23492
+ fromTurn,
23493
+ toTurn,
23494
+ CONTENT_LABEL_MAX_TOKENS
23495
+ );
23496
+ if (!expandedHasPairedTrajectoryLabels(expanded, reference.number)) {
23497
+ continue;
23498
+ }
23499
+ if (expanded.length === 0) {
23500
+ appendEvidenceItem(options.evidenceItems, options.seenTurns, {
23501
+ id: `${hit.session_id}:${hit.turn_index}`,
23502
+ sessionId: hit.session_id,
23503
+ turnIndex: hit.turn_index,
23504
+ role: hit.role,
23505
+ content: hit.content
23506
+ });
23507
+ continue;
23508
+ }
23509
+ appendExpandedEvidence(
23510
+ options.evidenceItems,
23511
+ options.seenTurns,
23512
+ hit.session_id,
23513
+ expanded
23514
+ );
23515
+ appendedWindows += 1;
23516
+ }
23517
+ }
23518
+ return resolved;
23519
+ }
23520
+ async function searchReferenceContentLabels(engine, referenceNumber, sessionId) {
23521
+ const hits = /* @__PURE__ */ new Map();
23522
+ for (const labelKind of ["action", "observation"]) {
23523
+ const label = labelKind === "action" ? "Action" : "Observation";
23524
+ for (const query of [`[${label} ${referenceNumber}]`, `${label} ${referenceNumber}`]) {
23525
+ const results = await engine.searchContextFull(
23526
+ query,
23527
+ CONTENT_LABEL_SEARCH_LIMIT,
23528
+ sessionId
23529
+ );
23530
+ for (const result of results) {
23531
+ if (!isReferenceLabelRole(result.role, labelKind) || !contentHasReferenceLabel(result.content, labelKind, referenceNumber)) {
23532
+ continue;
23533
+ }
23534
+ hits.set(`${result.session_id}:${result.turn_index}:${labelKind}`, {
23535
+ turn_index: result.turn_index,
23536
+ role: result.role,
23537
+ content: result.content,
23538
+ session_id: result.session_id,
23539
+ labelKind
23540
+ });
23541
+ }
23542
+ }
23543
+ }
23544
+ const numericCandidates = candidateTurnIndexesForReference({
23545
+ number: referenceNumber,
23546
+ includeDirectTurn: false
23547
+ });
23548
+ return [...hits.values()].sort((left, right) => {
23549
+ const sessionOrder = left.session_id.localeCompare(right.session_id);
23550
+ if (sessionOrder !== 0) {
23551
+ return sessionOrder;
23552
+ }
23553
+ const leftDistance = nearestTurnDistance(left.turn_index, numericCandidates);
23554
+ const rightDistance = nearestTurnDistance(right.turn_index, numericCandidates);
23555
+ if (leftDistance !== rightDistance) {
23556
+ return leftDistance - rightDistance;
23557
+ }
23558
+ return left.turn_index - right.turn_index || left.labelKind.localeCompare(right.labelKind);
23559
+ });
23560
+ }
23561
+ function nearestTurnDistance(turnIndex, candidates) {
23562
+ let nearest = Number.POSITIVE_INFINITY;
23563
+ for (const candidate of candidates) {
23564
+ nearest = Math.min(nearest, Math.abs(turnIndex - candidate));
23565
+ }
23566
+ return nearest;
23567
+ }
23568
+ function contentLabelEvidenceWindow(hit) {
23569
+ if (hit.labelKind === "action") {
23570
+ return {
23571
+ fromTurn: Math.max(0, hit.turn_index - 1),
23572
+ toTurn: hit.turn_index + 1
23573
+ };
23574
+ }
23575
+ return {
23576
+ fromTurn: Math.max(0, hit.turn_index - 1),
23577
+ toTurn: hit.turn_index
23578
+ };
23579
+ }
23580
+ function contentHasReferenceLabel(content, labelKind, referenceNumber) {
23581
+ const label = labelKind === "action" ? "Action" : "Observation";
23582
+ const escapedNumber = String(referenceNumber).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
23583
+ return new RegExp(
23584
+ `^\\s*\\[\\s*${label}\\s+${escapedNumber}\\s*\\]\\s*(?::\\s*)?`,
23585
+ "i"
23586
+ ).test(content);
23587
+ }
23588
+ function isReferenceLabelRole(role, labelKind) {
23589
+ if (labelKind === "action") {
23590
+ return role === "user";
23591
+ }
23592
+ return role === "assistant";
23593
+ }
23594
+ function expandedHasPairedTrajectoryLabels(expanded, referenceNumber) {
23595
+ let hasAction = false;
23596
+ let hasObservation = false;
23597
+ for (const message of expanded) {
23598
+ if (isReferenceLabelRole(message.role, "action") && contentHasReferenceLabel(message.content, "action", referenceNumber)) {
23599
+ hasAction = true;
23600
+ }
23601
+ if (isReferenceLabelRole(message.role, "observation") && contentHasReferenceLabel(message.content, "observation", referenceNumber)) {
23602
+ hasObservation = true;
23603
+ }
23604
+ }
23605
+ return hasAction && hasObservation;
23606
+ }
23450
23607
  async function collectLexicalCueEvidence(options) {
23451
23608
  const cues = collectLexicalCues(options.query, {
23452
23609
  includeBenchmarkAnchorCues: options.includeBenchmarkAnchorCues,
@@ -32474,15 +32631,19 @@ var NamespaceSearchRouter = class {
32474
32631
  */
32475
32632
  async updateNamespaces(namespaces, execution) {
32476
32633
  const unique = Array.from(new Set(namespaces.map((value) => value.trim()).filter(Boolean)));
32477
- const results = await Promise.all(
32634
+ const eligible = (await Promise.all(
32478
32635
  unique.map(async (namespace) => {
32479
32636
  const record = await this.backendRecordFor(namespace);
32480
- if (!record.available || record.collectionState === "missing") return 0;
32481
- await record.backend.update(execution);
32482
- return 1;
32637
+ return record.available && record.collectionState !== "missing" ? record : null;
32483
32638
  })
32484
- );
32485
- return results.reduce((sum, v) => sum + v, 0);
32639
+ )).filter((record) => record !== null);
32640
+ const globalRecord = eligible.find((record) => record.backend.updatesAllCollections?.() === true);
32641
+ const scopedRecords = globalRecord ? eligible.filter((record) => record.backend.updatesAllCollections?.() !== true) : eligible;
32642
+ await Promise.all([
32643
+ globalRecord ? globalRecord.backend.update(execution) : Promise.resolve(),
32644
+ ...scopedRecords.map((record) => record.backend.update(execution))
32645
+ ]);
32646
+ return (globalRecord ? 1 : 0) + scopedRecords.length;
32486
32647
  }
32487
32648
  async embedNamespaces(namespaces) {
32488
32649
  const unique = Array.from(new Set(namespaces.map((value) => value.trim()).filter(Boolean)));
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "openclaw-remnic",
3
3
  "name": "Remnic OpenClaw Plugin",
4
- "version": "1.0.21",
4
+ "version": "1.0.22",
5
5
  "kind": "memory",
6
6
  "description": "Local semantic memory for OpenClaw. Requires plugins.slots.memory set to this plugin id for hooks to fire.",
7
7
  "setup": {
@@ -28,14 +28,14 @@
28
28
  "provider": "openai",
29
29
  "method": "api-key",
30
30
  "choiceId": "remnic-openai-api-key",
31
- "choiceLabel": "OpenAI API key for Remnic memory extraction",
32
- "choiceHint": "Remnic sends memory extraction, consolidation, and embedding requests to OpenAI or the configured OpenAI-compatible endpoint unless you route those tasks through OpenClaw gateway/local LLM settings.",
31
+ "choiceLabel": "Optional OpenAI API key for Remnic plugin-mode extraction",
32
+ "choiceHint": "Not needed when Remnic uses the OpenClaw gateway model source. Set only if you intentionally use plugin mode with OpenAI or an OpenAI-compatible endpoint.",
33
33
  "groupId": "remnic-memory",
34
34
  "groupLabel": "Remnic memory",
35
35
  "optionKey": "openaiApiKey",
36
36
  "cliFlag": "--openai-api-key",
37
37
  "cliOption": "--openai-api-key <key>",
38
- "cliDescription": "OpenAI API key used by Remnic memory extraction, consolidation, and embedding flows.",
38
+ "cliDescription": "Optional OpenAI API key used by Remnic plugin-mode extraction, consolidation, and embedding flows.",
39
39
  "onboardingScopes": [
40
40
  "text-inference"
41
41
  ]
@@ -107,7 +107,7 @@
107
107
  "properties": {
108
108
  "openaiApiKey": {
109
109
  "type": "string",
110
- "description": "OpenAI API key (or set OPENAI_API_KEY env var). Remnic may send conversation and memory content to OpenAI or the configured OpenAI-compatible endpoint for extraction, consolidation, summarization, and embeddings."
110
+ "description": "Optional OpenAI API key for plugin mode (or set OPENAI_API_KEY env var). Ignored by default gateway-mode installs; in plugin mode, Remnic may send conversation and memory content to OpenAI or the configured OpenAI-compatible endpoint for extraction, consolidation, summarization, and embeddings."
111
111
  },
112
112
  "openaiBaseUrl": {
113
113
  "type": "string",
@@ -2853,12 +2853,12 @@
2853
2853
  },
2854
2854
  "modelSource": {
2855
2855
  "type": "string",
2856
- "enum": [
2857
- "plugin",
2858
- "gateway"
2859
- ],
2860
- "default": "plugin",
2861
- "description": "LLM source: 'plugin' uses Engram's own openai/localLlm config; 'gateway' delegates to a gateway agent's model chain (agents.list[])."
2856
+ "enum": [
2857
+ "plugin",
2858
+ "gateway"
2859
+ ],
2860
+ "default": "gateway",
2861
+ "description": "LLM source: 'gateway' delegates to a gateway agent's model chain (agents.list[]); 'plugin' uses Engram's own openai/localLlm config."
2862
2862
  },
2863
2863
  "gatewayAgentId": {
2864
2864
  "type": "string",
@@ -4593,7 +4593,7 @@
4593
4593
  "label": "OpenAI API Key",
4594
4594
  "sensitive": true,
4595
4595
  "placeholder": "sk-...",
4596
- "help": "API key for OpenAI (or use ${OPENAI_API_KEY})"
4596
+ "help": "Optional API key for plugin mode only. Not needed when Model Source is gateway."
4597
4597
  },
4598
4598
  "openaiBaseUrl": {
4599
4599
  "label": "OpenAI Base URL",
@@ -5041,7 +5041,7 @@
5041
5041
  },
5042
5042
  "modelSource": {
5043
5043
  "label": "Model Source",
5044
- "help": "Route LLM calls through the gateway's agent model chain instead of Engram's own config. When set to 'gateway', localLlm and openai settings are ignored."
5044
+ "help": "Route LLM calls through the gateway's agent model chain instead of Engram's own config. Default for OpenClaw installs. When set to 'gateway', localLlm and openai settings are ignored."
5045
5045
  },
5046
5046
  "gatewayAgentId": {
5047
5047
  "label": "Gateway Agent ID",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remnic/plugin-openclaw",
3
- "version": "1.0.21",
3
+ "version": "1.0.22",
4
4
  "description": "OpenClaw adapter for Remnic memory — thin wrapper delegating to @remnic/core",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",