@joshuaswarren/openclaw-engram 9.0.11 → 9.0.12

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/README.md CHANGED
@@ -138,13 +138,13 @@ openclaw engram policy-status # Lifecycle policy snapshot
138
138
 
139
139
  ## Configuration
140
140
 
141
- All settings live in `openclaw.json` under `plugins.entries.openclaw-engram.config`. Only `openaiApiKey` is required everything else has sensible defaults.
141
+ All settings live in `openclaw.json` under `plugins.entries.openclaw-engram.config`. `openaiApiKey` is optional when local LLM or gateway fallback paths are available.
142
142
 
143
143
  Key settings:
144
144
 
145
145
  | Setting | Default | Description |
146
146
  |---------|---------|-------------|
147
- | `openaiApiKey` | `(env fallback)` | OpenAI API key or `${ENV_VAR}` reference |
147
+ | `openaiApiKey` | `(env fallback)` | Optional OpenAI API key or `${ENV_VAR}` reference for direct-client paths |
148
148
  | `model` | `gpt-5.2` | LLM model for extraction |
149
149
  | `searchBackend` | `"qmd"` | Search engine: `qmd`, `orama`, `lancedb`, `meilisearch`, `remote`, `noop` |
150
150
  | `qmdEnabled` | `true` | Enable QMD hybrid search |
package/dist/index.js CHANGED
@@ -2401,7 +2401,7 @@ var ExtractionEngine = class {
2401
2401
  });
2402
2402
  } else {
2403
2403
  this.client = null;
2404
- log.warn("no OpenAI API key \u2014 extraction/consolidation disabled (retrieval still works)");
2404
+ log.warn("no OpenAI API key \u2014 direct OpenAI client disabled; local and gateway fallback paths remain available");
2405
2405
  }
2406
2406
  this.localLlm = localLlm ?? new LocalLlmClient(config, modelRegistry);
2407
2407
  this.fallbackLlm = new FallbackLlmClient(gatewayConfig);
@@ -2465,6 +2465,52 @@ var ExtractionEngine = class {
2465
2465
  ) : void 0
2466
2466
  };
2467
2467
  }
2468
+ parseJsonObject(content) {
2469
+ const trimmed = content?.trim();
2470
+ if (!trimmed) return null;
2471
+ for (const candidate of extractJsonCandidates(trimmed)) {
2472
+ try {
2473
+ return JSON.parse(candidate);
2474
+ } catch {
2475
+ }
2476
+ }
2477
+ return null;
2478
+ }
2479
+ normalizeContradictionVerificationResult(parsed) {
2480
+ if (!parsed || typeof parsed.isContradiction !== "boolean") return null;
2481
+ const rawWhich = parsed.whichIsNewer ?? parsed.winner;
2482
+ const normalizedWhich = rawWhich === "first" || rawWhich === "existing" ? "first" : rawWhich === "second" || rawWhich === "new" ? "second" : "unclear";
2483
+ return {
2484
+ isContradiction: Boolean(parsed.isContradiction),
2485
+ confidence: typeof parsed.confidence === "number" ? parsed.confidence : 0.5,
2486
+ reasoning: typeof parsed.reasoning === "string" ? parsed.reasoning : typeof parsed.explanation === "string" ? parsed.explanation : "",
2487
+ whichIsNewer: normalizedWhich
2488
+ };
2489
+ }
2490
+ normalizeSuggestedLinksResult(parsed) {
2491
+ if (!parsed || !Array.isArray(parsed.links)) {
2492
+ return null;
2493
+ }
2494
+ const normalizedLinks = parsed.links.map((link) => {
2495
+ const rawLinkType = link?.linkType ?? link?.type;
2496
+ return {
2497
+ targetId: typeof link?.targetId === "string" ? link.targetId : "",
2498
+ linkType: rawLinkType === "follows" || rawLinkType === "references" || rawLinkType === "contradicts" || rawLinkType === "supports" || rawLinkType === "related" ? rawLinkType : "related",
2499
+ strength: typeof link?.strength === "number" ? Math.max(0, Math.min(1, link.strength)) : 0.5,
2500
+ reason: typeof link?.reason === "string" ? link.reason : void 0
2501
+ };
2502
+ }).filter((link) => link.targetId.length > 0);
2503
+ return { links: normalizedLinks };
2504
+ }
2505
+ normalizeMemorySummaryResult(parsed) {
2506
+ if (!parsed) return null;
2507
+ const normalized = {
2508
+ summaryText: typeof parsed.summaryText === "string" ? parsed.summaryText : typeof parsed.summary === "string" ? parsed.summary : "",
2509
+ keyFacts: Array.isArray(parsed.keyFacts) ? parsed.keyFacts.filter((f) => typeof f === "string") : [],
2510
+ keyEntities: Array.isArray(parsed.keyEntities) ? parsed.keyEntities.filter((e) => typeof e === "string") : Array.isArray(parsed.entities) ? parsed.entities.filter((e) => typeof e === "string") : []
2511
+ };
2512
+ return normalized.summaryText.length > 0 ? normalized : null;
2513
+ }
2468
2514
  sanitizeConsolidationResult(result) {
2469
2515
  const items = result.items.map((item) => {
2470
2516
  if (!item.updatedContent) return item;
@@ -3650,10 +3696,6 @@ Respond with valid JSON matching this schema:
3650
3696
  * Called when QMD finds semantically similar memories (Phase 2B).
3651
3697
  */
3652
3698
  async verifyContradiction(newMemory, existingMemory) {
3653
- if (!this.client) {
3654
- log.warn("contradiction verification skipped \u2014 no OpenAI API key");
3655
- return null;
3656
- }
3657
3699
  const input = `Memory 1 (existing, created ${existingMemory.created}):
3658
3700
  Category: ${existingMemory.category}
3659
3701
  Content: ${existingMemory.content}
@@ -3684,6 +3726,26 @@ Respond with valid JSON matching this schema:
3684
3726
  "reasoning": "why they contradict or don't",
3685
3727
  "whichIsNewer": "first"
3686
3728
  }`;
3729
+ if (!this.client) {
3730
+ const fallbackResponse = await this.fallbackLlm.chatCompletion(
3731
+ [
3732
+ { role: "system", content: systemPrompt },
3733
+ { role: "user", content: input }
3734
+ ],
3735
+ { temperature: 0.3, maxTokens: 2048 }
3736
+ );
3737
+ const normalized2 = this.normalizeContradictionVerificationResult(
3738
+ this.parseJsonObject(fallbackResponse?.content)
3739
+ );
3740
+ if (normalized2) {
3741
+ log.debug(
3742
+ `contradiction check via fallback: ${normalized2.isContradiction ? "YES" : "NO"} (confidence: ${normalized2.confidence})`
3743
+ );
3744
+ return normalized2;
3745
+ }
3746
+ log.warn("contradiction verification skipped \u2014 no OpenAI API key and fallback unavailable");
3747
+ return null;
3748
+ }
3687
3749
  const response = await this.client.chat.completions.create({
3688
3750
  model: this.config.model,
3689
3751
  messages: [
@@ -3693,26 +3755,10 @@ Respond with valid JSON matching this schema:
3693
3755
  temperature: 0.3,
3694
3756
  max_tokens: 2048
3695
3757
  });
3696
- const rawContent = response.choices?.[0]?.message?.content?.trim();
3697
- let parsed = null;
3698
- if (rawContent) {
3699
- for (const candidate of extractJsonCandidates(rawContent)) {
3700
- try {
3701
- parsed = JSON.parse(candidate);
3702
- break;
3703
- } catch {
3704
- }
3705
- }
3706
- }
3707
- if (parsed && typeof parsed.isContradiction === "boolean") {
3708
- const rawWhich = parsed.whichIsNewer ?? parsed.winner;
3709
- const normalizedWhich = rawWhich === "first" || rawWhich === "existing" ? "first" : rawWhich === "second" || rawWhich === "new" ? "second" : "unclear";
3710
- const normalized = {
3711
- isContradiction: Boolean(parsed.isContradiction),
3712
- confidence: typeof parsed.confidence === "number" ? parsed.confidence : 0.5,
3713
- reasoning: typeof parsed.reasoning === "string" ? parsed.reasoning : typeof parsed.explanation === "string" ? parsed.explanation : "",
3714
- whichIsNewer: normalizedWhich
3715
- };
3758
+ const normalized = this.normalizeContradictionVerificationResult(
3759
+ this.parseJsonObject(response.choices?.[0]?.message?.content)
3760
+ );
3761
+ if (normalized) {
3716
3762
  log.debug(
3717
3763
  `contradiction check: ${normalized.isContradiction ? "YES" : "NO"} (confidence: ${normalized.confidence})`
3718
3764
  );
@@ -3729,10 +3775,6 @@ Respond with valid JSON matching this schema:
3729
3775
  * Called during extraction to build the knowledge graph.
3730
3776
  */
3731
3777
  async suggestLinks(newMemory, candidateMemories) {
3732
- if (!this.client) {
3733
- log.warn("link suggestion skipped \u2014 no OpenAI API key");
3734
- return null;
3735
- }
3736
3778
  if (candidateMemories.length === 0) {
3737
3779
  return { links: [] };
3738
3780
  }
@@ -3765,6 +3807,22 @@ Respond with valid JSON matching this schema:
3765
3807
  {
3766
3808
  "links": [{"targetId": "memory-id", "linkType": "follows|references|contradicts|supports|related", "strength": 0.8, "reason": "why"}]
3767
3809
  }`;
3810
+ if (!this.client) {
3811
+ const fallbackResponse = await this.fallbackLlm.chatCompletion(
3812
+ [
3813
+ { role: "system", content: systemPrompt },
3814
+ { role: "user", content: input }
3815
+ ],
3816
+ { temperature: 0.3, maxTokens: 2048 }
3817
+ );
3818
+ const normalized2 = this.normalizeSuggestedLinksResult(this.parseJsonObject(fallbackResponse?.content));
3819
+ if (normalized2) {
3820
+ log.debug(`suggested ${normalized2.links.length} links via fallback`);
3821
+ return normalized2;
3822
+ }
3823
+ log.warn("link suggestion skipped \u2014 no OpenAI API key and fallback unavailable");
3824
+ return null;
3825
+ }
3768
3826
  const response = await this.client.chat.completions.create({
3769
3827
  model: this.config.model,
3770
3828
  messages: [
@@ -3774,44 +3832,23 @@ Respond with valid JSON matching this schema:
3774
3832
  temperature: 0.3,
3775
3833
  max_tokens: 2048
3776
3834
  });
3777
- const rawContent = response.choices?.[0]?.message?.content?.trim();
3778
- let parsed = null;
3779
- if (rawContent) {
3780
- for (const candidate of extractJsonCandidates(rawContent)) {
3781
- try {
3782
- parsed = JSON.parse(candidate);
3783
- break;
3784
- } catch {
3785
- }
3786
- }
3787
- }
3788
- if (parsed && Array.isArray(parsed.links)) {
3789
- const normalizedLinks = parsed.links.map((link) => {
3790
- const rawLinkType = link?.linkType ?? link?.type;
3791
- return {
3792
- targetId: typeof link?.targetId === "string" ? link.targetId : "",
3793
- linkType: rawLinkType === "follows" || rawLinkType === "references" || rawLinkType === "contradicts" || rawLinkType === "supports" || rawLinkType === "related" ? rawLinkType : "related",
3794
- strength: typeof link?.strength === "number" ? Math.max(0, Math.min(1, link.strength)) : 0.5,
3795
- reason: typeof link?.reason === "string" ? link.reason : void 0
3796
- };
3797
- }).filter((link) => link.targetId.length > 0);
3798
- log.debug(`suggested ${normalizedLinks.length} links`);
3799
- return { links: normalizedLinks };
3835
+ const normalized = this.normalizeSuggestedLinksResult(
3836
+ this.parseJsonObject(response.choices?.[0]?.message?.content)
3837
+ );
3838
+ if (normalized) {
3839
+ log.debug(`suggested ${normalized.links.length} links`);
3840
+ return normalized;
3800
3841
  }
3801
- return { links: [] };
3842
+ return null;
3802
3843
  } catch (err) {
3803
3844
  log.error("link suggestion failed", err);
3804
- return { links: [] };
3845
+ return null;
3805
3846
  }
3806
3847
  }
3807
3848
  /**
3808
3849
  * Summarize a batch of old memories into a compact summary (Phase 4A).
3809
3850
  */
3810
3851
  async summarizeMemories(memories) {
3811
- if (!this.client) {
3812
- log.warn("summarization skipped \u2014 no OpenAI API key");
3813
- return null;
3814
- }
3815
3852
  if (memories.length === 0) return null;
3816
3853
  const memoryList = memories.map((m) => `[${m.id}] (${m.category}, ${m.created.slice(0, 10)})
3817
3854
  ${m.content}`).join("\n\n");
@@ -3835,6 +3872,24 @@ Respond with valid JSON matching this schema:
3835
3872
  "keyFacts": ["fact 1", "fact 2"],
3836
3873
  "keyEntities": ["entity-1", "entity-2"]
3837
3874
  }`;
3875
+ if (!this.client) {
3876
+ const fallbackResponse = await this.fallbackLlm.chatCompletion(
3877
+ [
3878
+ { role: "system", content: systemPrompt },
3879
+ { role: "user", content: `Summarize these ${memories.length} memories:
3880
+
3881
+ ${memoryList}` }
3882
+ ],
3883
+ { temperature: 0.3, maxTokens: 4096 }
3884
+ );
3885
+ const normalized2 = this.normalizeMemorySummaryResult(this.parseJsonObject(fallbackResponse?.content));
3886
+ if (normalized2) {
3887
+ log.debug(`summarized ${memories.length} memories into ${normalized2.keyFacts.length} key facts via fallback`);
3888
+ return normalized2;
3889
+ }
3890
+ log.warn("summarization skipped \u2014 no OpenAI API key and fallback unavailable");
3891
+ return null;
3892
+ }
3838
3893
  const response = await this.client.chat.completions.create({
3839
3894
  model: this.config.model,
3840
3895
  messages: [
@@ -3846,27 +3901,12 @@ ${memoryList}` }
3846
3901
  temperature: 0.3,
3847
3902
  max_tokens: 4096
3848
3903
  });
3849
- const rawContent = response.choices?.[0]?.message?.content?.trim();
3850
- let parsed = null;
3851
- if (rawContent) {
3852
- for (const candidate of extractJsonCandidates(rawContent)) {
3853
- try {
3854
- parsed = JSON.parse(candidate);
3855
- break;
3856
- } catch {
3857
- }
3858
- }
3859
- }
3860
- if (parsed) {
3861
- const normalized = {
3862
- summaryText: typeof parsed.summaryText === "string" ? parsed.summaryText : typeof parsed.summary === "string" ? parsed.summary : "",
3863
- keyFacts: Array.isArray(parsed.keyFacts) ? parsed.keyFacts.filter((f) => typeof f === "string") : [],
3864
- keyEntities: Array.isArray(parsed.keyEntities) ? parsed.keyEntities.filter((e) => typeof e === "string") : Array.isArray(parsed.entities) ? parsed.entities.filter((e) => typeof e === "string") : []
3865
- };
3866
- if (normalized.summaryText.length > 0) {
3867
- log.debug(`summarized ${memories.length} memories into ${normalized.keyFacts.length} key facts`);
3868
- return normalized;
3869
- }
3904
+ const normalized = this.normalizeMemorySummaryResult(
3905
+ this.parseJsonObject(response.choices?.[0]?.message?.content)
3906
+ );
3907
+ if (normalized) {
3908
+ log.debug(`summarized ${memories.length} memories into ${normalized.keyFacts.length} key facts`);
3909
+ return normalized;
3870
3910
  }
3871
3911
  return null;
3872
3912
  } catch (err) {