@joshuaswarren/openclaw-engram 9.0.10 → 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 +2 -2
- package/dist/index.js +172 -79
- package/dist/index.js.map +1 -1
- package/openclaw.plugin.json +10 -0
- package/package.json +1 -1
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`.
|
|
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
|
@@ -474,6 +474,8 @@ function parseConfig(raw) {
|
|
|
474
474
|
recallConfidenceGateEnabled: cfg.recallConfidenceGateEnabled === true,
|
|
475
475
|
recallConfidenceGateThreshold: typeof cfg.recallConfidenceGateThreshold === "number" ? Math.max(0, Math.min(1, cfg.recallConfidenceGateThreshold)) : 0.12,
|
|
476
476
|
causalRuleExtractionEnabled: cfg.causalRuleExtractionEnabled === true,
|
|
477
|
+
memoryReconstructionEnabled: cfg.memoryReconstructionEnabled === true,
|
|
478
|
+
memoryReconstructionMaxExpansions: typeof cfg.memoryReconstructionMaxExpansions === "number" ? Math.max(0, Math.round(cfg.memoryReconstructionMaxExpansions)) : 3,
|
|
477
479
|
graphLateralInhibitionEnabled: cfg.graphLateralInhibitionEnabled !== false,
|
|
478
480
|
graphLateralInhibitionBeta: typeof cfg.graphLateralInhibitionBeta === "number" ? Math.max(0, Math.min(1, cfg.graphLateralInhibitionBeta)) : 0.15,
|
|
479
481
|
graphLateralInhibitionTopM: typeof cfg.graphLateralInhibitionTopM === "number" ? Math.max(0, Math.round(cfg.graphLateralInhibitionTopM)) : 7,
|
|
@@ -2399,7 +2401,7 @@ var ExtractionEngine = class {
|
|
|
2399
2401
|
});
|
|
2400
2402
|
} else {
|
|
2401
2403
|
this.client = null;
|
|
2402
|
-
log.warn("no OpenAI API key \u2014
|
|
2404
|
+
log.warn("no OpenAI API key \u2014 direct OpenAI client disabled; local and gateway fallback paths remain available");
|
|
2403
2405
|
}
|
|
2404
2406
|
this.localLlm = localLlm ?? new LocalLlmClient(config, modelRegistry);
|
|
2405
2407
|
this.fallbackLlm = new FallbackLlmClient(gatewayConfig);
|
|
@@ -2463,6 +2465,52 @@ var ExtractionEngine = class {
|
|
|
2463
2465
|
) : void 0
|
|
2464
2466
|
};
|
|
2465
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
|
+
}
|
|
2466
2514
|
sanitizeConsolidationResult(result) {
|
|
2467
2515
|
const items = result.items.map((item) => {
|
|
2468
2516
|
if (!item.updatedContent) return item;
|
|
@@ -3648,10 +3696,6 @@ Respond with valid JSON matching this schema:
|
|
|
3648
3696
|
* Called when QMD finds semantically similar memories (Phase 2B).
|
|
3649
3697
|
*/
|
|
3650
3698
|
async verifyContradiction(newMemory, existingMemory) {
|
|
3651
|
-
if (!this.client) {
|
|
3652
|
-
log.warn("contradiction verification skipped \u2014 no OpenAI API key");
|
|
3653
|
-
return null;
|
|
3654
|
-
}
|
|
3655
3699
|
const input = `Memory 1 (existing, created ${existingMemory.created}):
|
|
3656
3700
|
Category: ${existingMemory.category}
|
|
3657
3701
|
Content: ${existingMemory.content}
|
|
@@ -3682,6 +3726,26 @@ Respond with valid JSON matching this schema:
|
|
|
3682
3726
|
"reasoning": "why they contradict or don't",
|
|
3683
3727
|
"whichIsNewer": "first"
|
|
3684
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
|
+
}
|
|
3685
3749
|
const response = await this.client.chat.completions.create({
|
|
3686
3750
|
model: this.config.model,
|
|
3687
3751
|
messages: [
|
|
@@ -3691,26 +3755,10 @@ Respond with valid JSON matching this schema:
|
|
|
3691
3755
|
temperature: 0.3,
|
|
3692
3756
|
max_tokens: 2048
|
|
3693
3757
|
});
|
|
3694
|
-
const
|
|
3695
|
-
|
|
3696
|
-
|
|
3697
|
-
|
|
3698
|
-
try {
|
|
3699
|
-
parsed = JSON.parse(candidate);
|
|
3700
|
-
break;
|
|
3701
|
-
} catch {
|
|
3702
|
-
}
|
|
3703
|
-
}
|
|
3704
|
-
}
|
|
3705
|
-
if (parsed && typeof parsed.isContradiction === "boolean") {
|
|
3706
|
-
const rawWhich = parsed.whichIsNewer ?? parsed.winner;
|
|
3707
|
-
const normalizedWhich = rawWhich === "first" || rawWhich === "existing" ? "first" : rawWhich === "second" || rawWhich === "new" ? "second" : "unclear";
|
|
3708
|
-
const normalized = {
|
|
3709
|
-
isContradiction: Boolean(parsed.isContradiction),
|
|
3710
|
-
confidence: typeof parsed.confidence === "number" ? parsed.confidence : 0.5,
|
|
3711
|
-
reasoning: typeof parsed.reasoning === "string" ? parsed.reasoning : typeof parsed.explanation === "string" ? parsed.explanation : "",
|
|
3712
|
-
whichIsNewer: normalizedWhich
|
|
3713
|
-
};
|
|
3758
|
+
const normalized = this.normalizeContradictionVerificationResult(
|
|
3759
|
+
this.parseJsonObject(response.choices?.[0]?.message?.content)
|
|
3760
|
+
);
|
|
3761
|
+
if (normalized) {
|
|
3714
3762
|
log.debug(
|
|
3715
3763
|
`contradiction check: ${normalized.isContradiction ? "YES" : "NO"} (confidence: ${normalized.confidence})`
|
|
3716
3764
|
);
|
|
@@ -3727,10 +3775,6 @@ Respond with valid JSON matching this schema:
|
|
|
3727
3775
|
* Called during extraction to build the knowledge graph.
|
|
3728
3776
|
*/
|
|
3729
3777
|
async suggestLinks(newMemory, candidateMemories) {
|
|
3730
|
-
if (!this.client) {
|
|
3731
|
-
log.warn("link suggestion skipped \u2014 no OpenAI API key");
|
|
3732
|
-
return null;
|
|
3733
|
-
}
|
|
3734
3778
|
if (candidateMemories.length === 0) {
|
|
3735
3779
|
return { links: [] };
|
|
3736
3780
|
}
|
|
@@ -3763,6 +3807,22 @@ Respond with valid JSON matching this schema:
|
|
|
3763
3807
|
{
|
|
3764
3808
|
"links": [{"targetId": "memory-id", "linkType": "follows|references|contradicts|supports|related", "strength": 0.8, "reason": "why"}]
|
|
3765
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
|
+
}
|
|
3766
3826
|
const response = await this.client.chat.completions.create({
|
|
3767
3827
|
model: this.config.model,
|
|
3768
3828
|
messages: [
|
|
@@ -3772,44 +3832,23 @@ Respond with valid JSON matching this schema:
|
|
|
3772
3832
|
temperature: 0.3,
|
|
3773
3833
|
max_tokens: 2048
|
|
3774
3834
|
});
|
|
3775
|
-
const
|
|
3776
|
-
|
|
3777
|
-
|
|
3778
|
-
|
|
3779
|
-
|
|
3780
|
-
|
|
3781
|
-
break;
|
|
3782
|
-
} catch {
|
|
3783
|
-
}
|
|
3784
|
-
}
|
|
3785
|
-
}
|
|
3786
|
-
if (parsed && Array.isArray(parsed.links)) {
|
|
3787
|
-
const normalizedLinks = parsed.links.map((link) => {
|
|
3788
|
-
const rawLinkType = link?.linkType ?? link?.type;
|
|
3789
|
-
return {
|
|
3790
|
-
targetId: typeof link?.targetId === "string" ? link.targetId : "",
|
|
3791
|
-
linkType: rawLinkType === "follows" || rawLinkType === "references" || rawLinkType === "contradicts" || rawLinkType === "supports" || rawLinkType === "related" ? rawLinkType : "related",
|
|
3792
|
-
strength: typeof link?.strength === "number" ? Math.max(0, Math.min(1, link.strength)) : 0.5,
|
|
3793
|
-
reason: typeof link?.reason === "string" ? link.reason : void 0
|
|
3794
|
-
};
|
|
3795
|
-
}).filter((link) => link.targetId.length > 0);
|
|
3796
|
-
log.debug(`suggested ${normalizedLinks.length} links`);
|
|
3797
|
-
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;
|
|
3798
3841
|
}
|
|
3799
|
-
return
|
|
3842
|
+
return null;
|
|
3800
3843
|
} catch (err) {
|
|
3801
3844
|
log.error("link suggestion failed", err);
|
|
3802
|
-
return
|
|
3845
|
+
return null;
|
|
3803
3846
|
}
|
|
3804
3847
|
}
|
|
3805
3848
|
/**
|
|
3806
3849
|
* Summarize a batch of old memories into a compact summary (Phase 4A).
|
|
3807
3850
|
*/
|
|
3808
3851
|
async summarizeMemories(memories) {
|
|
3809
|
-
if (!this.client) {
|
|
3810
|
-
log.warn("summarization skipped \u2014 no OpenAI API key");
|
|
3811
|
-
return null;
|
|
3812
|
-
}
|
|
3813
3852
|
if (memories.length === 0) return null;
|
|
3814
3853
|
const memoryList = memories.map((m) => `[${m.id}] (${m.category}, ${m.created.slice(0, 10)})
|
|
3815
3854
|
${m.content}`).join("\n\n");
|
|
@@ -3833,6 +3872,24 @@ Respond with valid JSON matching this schema:
|
|
|
3833
3872
|
"keyFacts": ["fact 1", "fact 2"],
|
|
3834
3873
|
"keyEntities": ["entity-1", "entity-2"]
|
|
3835
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
|
+
}
|
|
3836
3893
|
const response = await this.client.chat.completions.create({
|
|
3837
3894
|
model: this.config.model,
|
|
3838
3895
|
messages: [
|
|
@@ -3844,27 +3901,12 @@ ${memoryList}` }
|
|
|
3844
3901
|
temperature: 0.3,
|
|
3845
3902
|
max_tokens: 4096
|
|
3846
3903
|
});
|
|
3847
|
-
const
|
|
3848
|
-
|
|
3849
|
-
|
|
3850
|
-
|
|
3851
|
-
|
|
3852
|
-
|
|
3853
|
-
break;
|
|
3854
|
-
} catch {
|
|
3855
|
-
}
|
|
3856
|
-
}
|
|
3857
|
-
}
|
|
3858
|
-
if (parsed) {
|
|
3859
|
-
const normalized = {
|
|
3860
|
-
summaryText: typeof parsed.summaryText === "string" ? parsed.summaryText : typeof parsed.summary === "string" ? parsed.summary : "",
|
|
3861
|
-
keyFacts: Array.isArray(parsed.keyFacts) ? parsed.keyFacts.filter((f) => typeof f === "string") : [],
|
|
3862
|
-
keyEntities: Array.isArray(parsed.keyEntities) ? parsed.keyEntities.filter((e) => typeof e === "string") : Array.isArray(parsed.entities) ? parsed.entities.filter((e) => typeof e === "string") : []
|
|
3863
|
-
};
|
|
3864
|
-
if (normalized.summaryText.length > 0) {
|
|
3865
|
-
log.debug(`summarized ${memories.length} memories into ${normalized.keyFacts.length} key facts`);
|
|
3866
|
-
return normalized;
|
|
3867
|
-
}
|
|
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;
|
|
3868
3910
|
}
|
|
3869
3911
|
return null;
|
|
3870
3912
|
} catch (err) {
|
|
@@ -4179,6 +4221,26 @@ function rescoreMemoryImportance(memory) {
|
|
|
4179
4221
|
return scoreImportance(memory.content, memory.frontmatter.category, memory.frontmatter.tags ?? []);
|
|
4180
4222
|
}
|
|
4181
4223
|
|
|
4224
|
+
// src/reconstruct.ts
|
|
4225
|
+
function findUnresolvedEntityRefs(recalledSnippets, recalledEntityRefs, knownEntities) {
|
|
4226
|
+
const refSet = new Set(recalledEntityRefs.map((r) => r.toLowerCase()));
|
|
4227
|
+
const combinedText = recalledSnippets.join(" ").toLowerCase();
|
|
4228
|
+
const sorted = [...knownEntities].sort((a, b) => b.length - a.length);
|
|
4229
|
+
const matched = /* @__PURE__ */ new Set();
|
|
4230
|
+
const unresolved = [];
|
|
4231
|
+
for (const entity of sorted) {
|
|
4232
|
+
const lower = entity.toLowerCase();
|
|
4233
|
+
if (refSet.has(lower)) continue;
|
|
4234
|
+
const escaped = lower.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
4235
|
+
const pattern = new RegExp(`(?:^|[\\s,;:'"(\\[])${escaped}(?:$|[\\s,;:'".)\\]])`, "i");
|
|
4236
|
+
if (pattern.test(combinedText) && !matched.has(lower)) {
|
|
4237
|
+
unresolved.push(entity);
|
|
4238
|
+
matched.add(lower);
|
|
4239
|
+
}
|
|
4240
|
+
}
|
|
4241
|
+
return unresolved;
|
|
4242
|
+
}
|
|
4243
|
+
|
|
4182
4244
|
// src/search/noop-backend.ts
|
|
4183
4245
|
var NoopSearchBackend = class {
|
|
4184
4246
|
async probe() {
|
|
@@ -17862,6 +17924,37 @@ ${tmtNode.summary}`);
|
|
|
17862
17924
|
confidenceGateRejected = true;
|
|
17863
17925
|
}
|
|
17864
17926
|
memoryResults = memoryResults.slice(0, recallResultLimit);
|
|
17927
|
+
if (this.config.memoryReconstructionEnabled && memoryResults.length > 0) {
|
|
17928
|
+
try {
|
|
17929
|
+
const snippets = memoryResults.map((r) => r.snippet);
|
|
17930
|
+
const coveredRefs = memoryResults.map((r) => r.path).filter((p) => p.startsWith("entities/")).map((p) => p.replace(/^entities\//, "").replace(/\.md$/, ""));
|
|
17931
|
+
const knownEntities = await profileStorage.listEntityNames();
|
|
17932
|
+
const missing = findUnresolvedEntityRefs(snippets, coveredRefs, knownEntities);
|
|
17933
|
+
if (missing.length > 0) {
|
|
17934
|
+
const budget = this.config.memoryReconstructionMaxExpansions;
|
|
17935
|
+
let expanded = 0;
|
|
17936
|
+
for (const entityName of missing) {
|
|
17937
|
+
if (expanded >= budget) break;
|
|
17938
|
+
const raw = await profileStorage.readEntity(entityName);
|
|
17939
|
+
if (raw && raw.length > 0) {
|
|
17940
|
+
const snippet = raw.length > 300 ? raw.slice(0, 300) + "\u2026" : raw;
|
|
17941
|
+
memoryResults.push({
|
|
17942
|
+
docid: `entity:${entityName}`,
|
|
17943
|
+
path: `entities/${entityName}.md`,
|
|
17944
|
+
snippet: `[Entity: ${entityName}] ${snippet}`,
|
|
17945
|
+
score: 0.1
|
|
17946
|
+
});
|
|
17947
|
+
expanded++;
|
|
17948
|
+
}
|
|
17949
|
+
}
|
|
17950
|
+
if (expanded > 0) {
|
|
17951
|
+
log.debug(`recall: reconstructed ${expanded} entity contexts`);
|
|
17952
|
+
}
|
|
17953
|
+
}
|
|
17954
|
+
} catch (err) {
|
|
17955
|
+
log.warn("recall: memory reconstruction failed (non-fatal)", err);
|
|
17956
|
+
}
|
|
17957
|
+
}
|
|
17865
17958
|
if (memoryResults.length > 0) {
|
|
17866
17959
|
recallSource = "hot_qmd";
|
|
17867
17960
|
recalledMemoryCount = memoryResults.length;
|