@totalreclaw/totalreclaw 3.3.12-rc.2 → 3.3.12-rc.21
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/CHANGELOG.md +26 -0
- package/SKILL.md +34 -249
- package/api-client.ts +17 -9
- package/batch-gate.ts +42 -0
- package/billing-cache.ts +25 -20
- package/claims-helper.ts +7 -1
- package/config.ts +54 -12
- package/consolidation.ts +6 -13
- package/contradiction-sync.ts +19 -14
- package/credential-provider.ts +184 -0
- package/crypto.ts +27 -160
- package/dist/api-client.js +17 -9
- package/dist/batch-gate.js +40 -0
- package/dist/billing-cache.js +19 -21
- package/dist/claims-helper.js +7 -1
- package/dist/config.js +54 -12
- package/dist/consolidation.js +6 -15
- package/dist/contradiction-sync.js +15 -15
- package/dist/credential-provider.js +145 -0
- package/dist/crypto.js +17 -137
- package/dist/download-ux.js +11 -7
- package/dist/embedder-loader.js +266 -0
- package/dist/embedding.js +36 -3
- package/dist/entry.js +123 -0
- package/dist/extractor.js +134 -0
- package/dist/fs-helpers.js +116 -241
- package/dist/import-adapters/chatgpt-adapter.js +14 -0
- package/dist/import-adapters/claude-adapter.js +14 -0
- package/dist/import-adapters/gemini-adapter.js +43 -159
- package/dist/import-adapters/mcp-memory-adapter.js +14 -0
- package/dist/import-state-manager.js +100 -0
- package/dist/index.js +1130 -2520
- package/dist/llm-client.js +69 -1
- package/dist/memory-runtime.js +459 -0
- package/dist/native-memory.js +123 -0
- package/dist/onboarding-cli.js +3 -2
- package/dist/pair-cli.js +1 -1
- package/dist/pair-crypto.js +16 -358
- package/dist/pair-http.js +147 -4
- package/dist/relay.js +140 -0
- package/dist/reranker.js +13 -8
- package/dist/semantic-dedup.js +5 -7
- package/dist/skill-register.js +97 -0
- package/dist/subgraph-search.js +3 -1
- package/dist/subgraph-store.js +315 -282
- package/dist/tool-gating.js +39 -26
- package/dist/tools.js +367 -0
- package/dist/tr-cli-export-helper.js +103 -0
- package/dist/tr-cli.js +220 -127
- package/dist/trajectory-poller.js +155 -9
- package/dist/vault-crypto.js +551 -0
- package/download-ux.ts +12 -6
- package/embedder-loader.ts +293 -1
- package/embedding.ts +43 -3
- package/entry.ts +132 -0
- package/extractor.ts +167 -0
- package/fs-helpers.ts +166 -292
- package/import-adapters/chatgpt-adapter.ts +18 -0
- package/import-adapters/claude-adapter.ts +18 -0
- package/import-adapters/gemini-adapter.ts +56 -183
- package/import-adapters/mcp-memory-adapter.ts +18 -0
- package/import-adapters/types.ts +1 -1
- package/import-state-manager.ts +139 -0
- package/index.ts +1432 -3002
- package/llm-client.ts +74 -1
- package/memory-runtime.ts +723 -0
- package/native-memory.ts +196 -0
- package/onboarding-cli.ts +3 -2
- package/openclaw.plugin.json +5 -17
- package/package.json +7 -4
- package/pair-cli.ts +1 -1
- package/pair-crypto.ts +41 -483
- package/pair-http.ts +194 -5
- package/postinstall.mjs +138 -0
- package/relay.ts +172 -0
- package/reranker.ts +13 -8
- package/semantic-dedup.ts +5 -6
- package/skill-register.ts +146 -0
- package/skill.json +1 -1
- package/subgraph-search.ts +3 -1
- package/subgraph-store.ts +334 -299
- package/tool-gating.ts +39 -26
- package/tools.ts +499 -0
- package/tr-cli-export-helper.ts +138 -0
- package/tr-cli.ts +263 -133
- package/trajectory-poller.ts +162 -10
- package/vault-crypto.ts +705 -0
package/dist/llm-client.js
CHANGED
|
@@ -598,6 +598,47 @@ export function isRetryable(errorMessage) {
|
|
|
598
598
|
// ---------------------------------------------------------------------------
|
|
599
599
|
// OpenAI-compatible chat completion
|
|
600
600
|
// ---------------------------------------------------------------------------
|
|
601
|
+
/**
|
|
602
|
+
* Provider base-URL hints for OpenAI-compatible endpoints that honour the
|
|
603
|
+
* `response_format: {"type": "json_object"}` body field. Sending the field
|
|
604
|
+
* to a provider that supports it makes JSON output deterministic; sending
|
|
605
|
+
* it to a provider that does NOT recognise the field is a 400.
|
|
606
|
+
*
|
|
607
|
+
* Why this exists (3.3.12-rc.6, 2026-05-09):
|
|
608
|
+
* z.ai's GLM family (4.5-flash, 5-turbo, 5.1) silently returns EMPTY
|
|
609
|
+
* `message.content` for the merged-extraction prompt unless this hint
|
|
610
|
+
* is set. No error, no warning — the LLM just emits "" instead of the
|
|
611
|
+
* expected `{"topics": [], "facts": []}` JSON. Plugin's parse step
|
|
612
|
+
* then logs `0 raw facts` from a successful-but-empty branch.
|
|
613
|
+
*
|
|
614
|
+
* This bug was found and fixed on the Python (Hermes) side in
|
|
615
|
+
* 2.3.1-rc.23 (see `python/src/totalreclaw/agent/llm_client.py`
|
|
616
|
+
* `_supports_json_object_response_format`) but the plugin TS port did
|
|
617
|
+
* not carry the fix — observed in plugin 3.3.12-rc.5 auto-QA on
|
|
618
|
+
* 2026-05-09: hook + poller both fired correctly but extraction
|
|
619
|
+
* returned 0 facts on every batch despite trajectories containing
|
|
620
|
+
* explicit "I prefer X" / "I work at Y" statements.
|
|
621
|
+
*
|
|
622
|
+
* Mirror of Python's `_supports_json_object_response_format`. Match by
|
|
623
|
+
* substring on a lowercased baseUrl so cosmetic prefix differences
|
|
624
|
+
* (https://, /v1, etc.) don't matter.
|
|
625
|
+
*/
|
|
626
|
+
const JSON_OBJECT_PROVIDER_HINTS = [
|
|
627
|
+
'z.ai',
|
|
628
|
+
'api.openai.com',
|
|
629
|
+
'groq.com',
|
|
630
|
+
'openrouter.ai',
|
|
631
|
+
'deepseek.com',
|
|
632
|
+
'mistral.ai',
|
|
633
|
+
'x.ai',
|
|
634
|
+
'together.xyz',
|
|
635
|
+
];
|
|
636
|
+
export function supportsJsonObjectResponseFormat(baseUrl) {
|
|
637
|
+
if (!baseUrl)
|
|
638
|
+
return false;
|
|
639
|
+
const lower = baseUrl.toLowerCase();
|
|
640
|
+
return JSON_OBJECT_PROVIDER_HINTS.some((h) => lower.includes(h));
|
|
641
|
+
}
|
|
601
642
|
async function chatCompletionOpenAI(config, messages, maxTokens, temperature, timeoutMs) {
|
|
602
643
|
const url = `${config.baseUrl}/chat/completions`;
|
|
603
644
|
const body = {
|
|
@@ -606,6 +647,12 @@ async function chatCompletionOpenAI(config, messages, maxTokens, temperature, ti
|
|
|
606
647
|
temperature,
|
|
607
648
|
max_completion_tokens: maxTokens,
|
|
608
649
|
};
|
|
650
|
+
// 3.3.12-rc.6: hint the provider to return strict JSON. Critical for
|
|
651
|
+
// z.ai/GLM (silent-empty without it). See JSON_OBJECT_PROVIDER_HINTS
|
|
652
|
+
// doc above.
|
|
653
|
+
if (supportsJsonObjectResponseFormat(config.baseUrl)) {
|
|
654
|
+
body.response_format = { type: 'json_object' };
|
|
655
|
+
}
|
|
609
656
|
try {
|
|
610
657
|
const res = await fetch(url, {
|
|
611
658
|
method: 'POST',
|
|
@@ -621,7 +668,28 @@ async function chatCompletionOpenAI(config, messages, maxTokens, temperature, ti
|
|
|
621
668
|
throw new Error(`LLM API ${res.status}: ${text.slice(0, 200)}`);
|
|
622
669
|
}
|
|
623
670
|
const json = (await res.json());
|
|
624
|
-
|
|
671
|
+
const content = json.choices?.[0]?.message?.content ?? null;
|
|
672
|
+
// 3.3.12-rc.6: loud-on-empty. If the provider returned a 200 with
|
|
673
|
+
// empty content, this almost always means a missing response_format
|
|
674
|
+
// hint or a content-filter. Without this log the silent-empty
|
|
675
|
+
// failure mode (Python rc.23 / plugin rc.5) is invisible to ops.
|
|
676
|
+
if (content === '' || content === null) {
|
|
677
|
+
// Lazy import to avoid circular dep with the registered logger.
|
|
678
|
+
// Fall back to console.warn if logger unavailable.
|
|
679
|
+
const warn = (msg) => {
|
|
680
|
+
try {
|
|
681
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
682
|
+
globalThis?.console?.warn?.(msg);
|
|
683
|
+
}
|
|
684
|
+
catch {
|
|
685
|
+
/* noop */
|
|
686
|
+
}
|
|
687
|
+
};
|
|
688
|
+
warn(`[totalreclaw][llm-client] provider=${config.baseUrl} model=${config.model} ` +
|
|
689
|
+
`returned empty content (status=200). ` +
|
|
690
|
+
`If using z.ai/GLM/OpenAI-compat, check response_format hint is being sent.`);
|
|
691
|
+
}
|
|
692
|
+
return content;
|
|
625
693
|
}
|
|
626
694
|
catch (err) {
|
|
627
695
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -0,0 +1,459 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* memory-runtime — adapter that bridges OpenClaw's FILE-ORIENTED memory
|
|
3
|
+
* result shapes to TR's ENCRYPTED-FACT + ON-CHAIN vault.
|
|
4
|
+
*
|
|
5
|
+
* Phase 2 (Task 2.1) of the OpenClaw native integration plan
|
|
6
|
+
* (docs/plans/2026-06-21-openclaw-native-integration-plan.md, 2026-06-21).
|
|
7
|
+
*
|
|
8
|
+
* WHY THIS FILE EXISTS — the load-bearing discovery:
|
|
9
|
+
* OpenClaw 2026.6.8's memory subsystem calls
|
|
10
|
+
* `runtime.getMemorySearchManager(...)` to get a MemorySearchManager,
|
|
11
|
+
* then `.search(query)` / a file-read method on it. Its result shapes
|
|
12
|
+
* are FILE-ORIENTED:
|
|
13
|
+
* search() -> MemorySearchResult[] where each hit =
|
|
14
|
+
* { path, startLine, endLine, score, snippet, source, citation? }
|
|
15
|
+
* read-by-rel -> { text, path, truncated?, from?, lines?, nextFrom? }
|
|
16
|
+
*
|
|
17
|
+
* TR's vault is ENCRYPTED-FACT + ON-CHAIN: facts have an id, encrypted
|
|
18
|
+
* blob, blind index, plaintext (after decrypt), scope, pinned flag.
|
|
19
|
+
* So this adapter SYNTHESIZES file-shaped results from decrypted facts:
|
|
20
|
+
* path = FACT_PATH_PREFIX + factId (a synthetic URI)
|
|
21
|
+
* startLine = 1, endLine = line-count of plaintext (synthetic)
|
|
22
|
+
* snippet = decrypted plaintext (truncated to 500 chars)
|
|
23
|
+
* source = 'memory', citation = factId
|
|
24
|
+
* The read-by-rel path reverses relPath -> id -> decrypt.
|
|
25
|
+
*
|
|
26
|
+
* This is THE thing that makes TR's on-chain vault look like a memory
|
|
27
|
+
* corpus to OpenClaw's `active-memory` sub-agent and the
|
|
28
|
+
* `memory_search` / `memory_get` tools.
|
|
29
|
+
*
|
|
30
|
+
* SCANNER-CLEAN HARD CONTRACT (env=N net=N):
|
|
31
|
+
* This file is pure orchestration. It touches NO environment state and
|
|
32
|
+
* performs NO outbound network I/O. All subgraph + decrypt work lives
|
|
33
|
+
* in the injected `recall` / `getById` closures (wired to the real
|
|
34
|
+
* pipeline in Task 2.3: subgraph-search + vault-crypto.decrypt +
|
|
35
|
+
* reranker). Keeping all I/O in those closures is what keeps this file
|
|
36
|
+
* clean under OpenClaw's per-file scanner rules — neither the
|
|
37
|
+
* env-harvesting pair nor the disk-exfil pair can ever co-occur here.
|
|
38
|
+
* `npm run check-scanner` must remain 0 flags; this docstring itself
|
|
39
|
+
* avoids the literal trigger tokens for that reason.
|
|
40
|
+
*
|
|
41
|
+
* PHASE 2 STATUS:
|
|
42
|
+
* - Task 2.1: `createTrMemorySearchManager` (shipped).
|
|
43
|
+
* - Task 2.3: `createTrMemoryPluginRuntime` (shipped) — the
|
|
44
|
+
* MemoryPluginRuntime wrapper that owns the wiring surface OpenClaw's
|
|
45
|
+
* memory subsystem calls. The actual binding of recall/getById to the
|
|
46
|
+
* real subgraph-search + vault-crypto.decrypt + reranker pipeline
|
|
47
|
+
* happens in Task 2.7's `buildRecallDeps` inside register().
|
|
48
|
+
* - Task 2.4: `buildPromptSection` (shipped) — recall guidance +
|
|
49
|
+
* quota warning + pinned facts. Mirrors memory-core's branching on
|
|
50
|
+
* memory_search/memory_get availability, adapted to TR's encrypted
|
|
51
|
+
* vault, plus the Hermes-grade extras (quota + pinned). The real
|
|
52
|
+
* quota/pinned binding happens in Task 2.7's `buildRecallDeps`.
|
|
53
|
+
* - Task 2.5: `buildFlushPlan` (shipped) — the `flushPlanResolver` that
|
|
54
|
+
* returns the memory flush PLAN (thresholds + extraction prompt) so
|
|
55
|
+
* OpenClaw's host can decide WHEN/HOW to flush the trajectory to TR's
|
|
56
|
+
* extract→encrypt→on-chain pipeline. Does NOT perform capture itself;
|
|
57
|
+
* the actual encrypt→on-chain path is Task 4.2 / H2 QA, and RC1 keeps
|
|
58
|
+
* the trajectory poller as the capture fallback so capture works
|
|
59
|
+
* regardless of flush cadence.
|
|
60
|
+
*/
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
// Imports — kept scoped: only the canonical extraction system prompt is
|
|
63
|
+
// pulled from extractor.ts. memory-runtime.ts otherwise stays self-contained
|
|
64
|
+
// (no OpenClaw type import) so the plugin compiles without depending on
|
|
65
|
+
// OpenClaw's type package.
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
import { EXTRACTION_SYSTEM_PROMPT } from './extractor.js';
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
// Constants
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
/**
|
|
72
|
+
* Synthetic URI prefix encoding a fact id as a memory path. Reversible by
|
|
73
|
+
* readFile() so the active-memory sub-agent can dereference any hit.
|
|
74
|
+
*/
|
|
75
|
+
export const FACT_PATH_PREFIX = 'totalreclaw://facts/';
|
|
76
|
+
/** Maximum snippet length surfaced in search() hits. Keeps tool payloads small. */
|
|
77
|
+
const SNIPPET_MAX = 500;
|
|
78
|
+
/** Default search cap when the caller doesn't pass maxResults. */
|
|
79
|
+
const DEFAULT_MAX_RESULTS = 8;
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
// Helpers
|
|
82
|
+
// ---------------------------------------------------------------------------
|
|
83
|
+
function toLineCount(s) {
|
|
84
|
+
// '' -> 1 (a single empty line), 'a\nb' -> 2. Used purely for synthetic
|
|
85
|
+
// startLine/endLine; OpenClaw treats these as display hints, not offsets.
|
|
86
|
+
return s.split('\n').length;
|
|
87
|
+
}
|
|
88
|
+
// ---------------------------------------------------------------------------
|
|
89
|
+
// createTrMemorySearchManager — the adapter factory
|
|
90
|
+
// ---------------------------------------------------------------------------
|
|
91
|
+
export function createTrMemorySearchManager(deps) {
|
|
92
|
+
/**
|
|
93
|
+
* search(): run recall, sort defensively by score, filter by minScore if
|
|
94
|
+
* requested, and synthesize file-shaped MemorySearchResult hits. We sort
|
|
95
|
+
* defensively here rather than relying on recall()'s ordering — the
|
|
96
|
+
* `score` field exists for exactly this, and n <= maxResults so it's
|
|
97
|
+
* effectively free. signal + sessionKey are forwarded so an aborted
|
|
98
|
+
* active-memory search actually cancels the in-flight recall.
|
|
99
|
+
*/
|
|
100
|
+
async function search(query, opts) {
|
|
101
|
+
const max = opts?.maxResults ?? DEFAULT_MAX_RESULTS;
|
|
102
|
+
const minScore = opts?.minScore;
|
|
103
|
+
const facts = await deps.recall(query, {
|
|
104
|
+
maxResults: max,
|
|
105
|
+
signal: opts?.signal,
|
|
106
|
+
sessionKey: opts?.sessionKey,
|
|
107
|
+
});
|
|
108
|
+
facts.sort((a, b) => b.score - a.score);
|
|
109
|
+
const filtered = minScore === undefined ? facts : facts.filter((f) => f.score >= minScore);
|
|
110
|
+
return filtered.slice(0, max).map((f) => ({
|
|
111
|
+
path: `${FACT_PATH_PREFIX}${f.id}`,
|
|
112
|
+
startLine: 1,
|
|
113
|
+
endLine: toLineCount(f.plaintext),
|
|
114
|
+
score: f.score,
|
|
115
|
+
snippet: f.plaintext.slice(0, SNIPPET_MAX),
|
|
116
|
+
source: 'memory',
|
|
117
|
+
citation: f.id,
|
|
118
|
+
}));
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* readFile(): reverse relPath -> id -> decrypt. Supports optional
|
|
122
|
+
* `from` / `lines` pagination for large facts (1-indexed line ranges,
|
|
123
|
+
* matching OpenClaw's convention). Returns nextFrom when more lines
|
|
124
|
+
* remain so the caller can page.
|
|
125
|
+
*/
|
|
126
|
+
async function readFile(params) {
|
|
127
|
+
const id = params.relPath.startsWith(FACT_PATH_PREFIX)
|
|
128
|
+
? params.relPath.slice(FACT_PATH_PREFIX.length)
|
|
129
|
+
: params.relPath;
|
|
130
|
+
const f = await deps.getById(id);
|
|
131
|
+
if (!f)
|
|
132
|
+
throw new Error(`fact not found: ${id}`);
|
|
133
|
+
const from = params.from && params.from > 0 ? params.from : 1;
|
|
134
|
+
const want = params.lines && params.lines > 0 ? params.lines : undefined;
|
|
135
|
+
const allLines = f.plaintext.split('\n');
|
|
136
|
+
const totalLines = allLines.length;
|
|
137
|
+
const sliceEnd = want === undefined ? totalLines : Math.min(from + want - 1, totalLines);
|
|
138
|
+
const text = allLines.slice(from - 1, sliceEnd).join('\n');
|
|
139
|
+
const truncated = want !== undefined && from + want - 1 < totalLines;
|
|
140
|
+
const nextFrom = truncated ? from + want : undefined;
|
|
141
|
+
return {
|
|
142
|
+
text,
|
|
143
|
+
path: `${FACT_PATH_PREFIX}${id}`,
|
|
144
|
+
truncated,
|
|
145
|
+
from,
|
|
146
|
+
// Clamp to non-negative: when `from` exceeds totalLines (e.g. reading
|
|
147
|
+
// past the end of a 1-line fact), sliceEnd - from + 1 goes negative.
|
|
148
|
+
// A bridge must never surface a negative line count.
|
|
149
|
+
lines: Math.max(0, sliceEnd - from + 1),
|
|
150
|
+
nextFrom,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
function status() {
|
|
154
|
+
// `backend: 'builtin'` mirrors OpenClaw's non-qmd providers. The
|
|
155
|
+
// provider string is what the active-memory sub-agent logs against.
|
|
156
|
+
return { backend: 'builtin', provider: 'totalreclaw' };
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* probeEmbeddingAvailability / probeVectorAvailability: optimistic OK
|
|
160
|
+
* here. The real availability depends on the injected pipeline (Task
|
|
161
|
+
* 2.3 wires the embedder + vector store); this adapter doesn't own
|
|
162
|
+
* that state, so the probes report ok until 2.3 gives them real hooks.
|
|
163
|
+
*/
|
|
164
|
+
async function probeEmbeddingAvailability() {
|
|
165
|
+
// TODO(task 2.3): replace with real embedder probe.
|
|
166
|
+
return { ok: true };
|
|
167
|
+
}
|
|
168
|
+
async function probeVectorAvailability() {
|
|
169
|
+
// TODO(task 2.3): replace with real vector-store probe.
|
|
170
|
+
return true;
|
|
171
|
+
}
|
|
172
|
+
async function close() { }
|
|
173
|
+
return {
|
|
174
|
+
search,
|
|
175
|
+
readFile,
|
|
176
|
+
status,
|
|
177
|
+
probeEmbeddingAvailability,
|
|
178
|
+
probeVectorAvailability,
|
|
179
|
+
close,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
// ---------------------------------------------------------------------------
|
|
183
|
+
// createTrMemoryPluginRuntime — the MemoryPluginRuntime wrapper (Task 2.3)
|
|
184
|
+
// ---------------------------------------------------------------------------
|
|
185
|
+
//
|
|
186
|
+
// WHY THIS WRAPPER EXISTS:
|
|
187
|
+
// OpenClaw 2026.6.8's memory subsystem does NOT call search/get directly.
|
|
188
|
+
// It calls `runtime.getMemorySearchManager(...)` to obtain a
|
|
189
|
+
// MemorySearchManager, then invokes `.search()` / readFile on it. It also
|
|
190
|
+
// calls `resolveMemoryBackendConfig` to decide between the built-in
|
|
191
|
+
// provider and an external `qmd` process, and `close*` on shutdown.
|
|
192
|
+
//
|
|
193
|
+
// So this wrapper is the seam OpenClaw actually talks to. It returns a
|
|
194
|
+
// fresh TrMemorySearchManager bound to the injected recall/getById
|
|
195
|
+
// pipeline on each getMemorySearchManager call. TR is its own backend
|
|
196
|
+
// (not qmd), so resolveMemoryBackendConfig reports `builtin`.
|
|
197
|
+
//
|
|
198
|
+
// The real pipeline binding — recall/getById wired to subgraph-search +
|
|
199
|
+
// vault-crypto.decrypt + reranker, parameterized by the paired account —
|
|
200
|
+
// is Task 2.7's `buildRecallDeps` in register(). This wrapper just carries
|
|
201
|
+
// whatever deps it's given.
|
|
202
|
+
//
|
|
203
|
+
// SCANNER-CLEAN HARD CONTRACT (env=N net=N):
|
|
204
|
+
// This function is pure orchestration. It touches NO environment state and
|
|
205
|
+
// performs NO outbound network I/O. The injected deps own all I/O. The
|
|
206
|
+
// `cfg` parameter is held as opaque (`unknown`) on purpose — the plugin
|
|
207
|
+
// does not import OpenClaw's config type, and getMemorySearchManager does
|
|
208
|
+
// not read anything off cfg (the paired-account context arrives via deps
|
|
209
|
+
// bound in 2.7, not via cfg here).
|
|
210
|
+
//
|
|
211
|
+
// ERROR CONTRACT:
|
|
212
|
+
// getMemorySearchManager MUST NEVER throw out of its async boundary — a
|
|
213
|
+
// failure to construct the adapter surfaces as `{ manager: null, error }`.
|
|
214
|
+
// Today construction is a closure capture and cannot realistically fail,
|
|
215
|
+
// but the try/catch is the durable guarantee for the day 2.7 adds
|
|
216
|
+
// paired-account resolution at construction time.
|
|
217
|
+
export function createTrMemoryPluginRuntime(deps) {
|
|
218
|
+
return {
|
|
219
|
+
async getMemorySearchManager(_params) {
|
|
220
|
+
try {
|
|
221
|
+
return {
|
|
222
|
+
manager: createTrMemorySearchManager(deps),
|
|
223
|
+
error: undefined,
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
catch (e) {
|
|
227
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
228
|
+
return { manager: null, error: msg };
|
|
229
|
+
}
|
|
230
|
+
},
|
|
231
|
+
resolveMemoryBackendConfig(_params) {
|
|
232
|
+
// TR is its own backend — never the external qmd process path.
|
|
233
|
+
return { backend: 'builtin' };
|
|
234
|
+
},
|
|
235
|
+
async closeMemorySearchManager(_params) {
|
|
236
|
+
// No per-manager resources to release today: the adapter holds only the
|
|
237
|
+
// injected closures; the closures' lifetimes are owned by register().
|
|
238
|
+
// Task 2.7 may add connection-pool / embedder teardown here.
|
|
239
|
+
},
|
|
240
|
+
async closeAllMemorySearchManagers() {
|
|
241
|
+
// See closeMemorySearchManager — no-op until 2.7 binds pool resources.
|
|
242
|
+
},
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
// ---------------------------------------------------------------------------
|
|
246
|
+
// buildPromptSection — recall guidance + quota warning + pinned facts
|
|
247
|
+
// (Task 2.4)
|
|
248
|
+
// ---------------------------------------------------------------------------
|
|
249
|
+
//
|
|
250
|
+
// WHY THIS EXISTS:
|
|
251
|
+
// OpenClaw's memory subsystem calls the registered `promptBuilder` to
|
|
252
|
+
// inject memory guidance into the agent's system prompt. The bundled
|
|
253
|
+
// memory-core's reference branches on which memory tools are available:
|
|
254
|
+
// search + get -> search first, then pull only the needed lines.
|
|
255
|
+
// search only -> search and answer from the matching results.
|
|
256
|
+
// get only -> pull only the needed lines.
|
|
257
|
+
// neither -> no guidance.
|
|
258
|
+
// TR's promptBuilder mirrors that branching BUT adapts the wording to
|
|
259
|
+
// TR's encrypted-vault model (the agent doesn't see files; it calls
|
|
260
|
+
// memory_search/memory_get which decrypt on the fly), AND adds two
|
|
261
|
+
// Hermes-grade extras via injected deps:
|
|
262
|
+
//
|
|
263
|
+
// 1. QUOTA WARNING — when the vault is near quota (>80% used) OR the
|
|
264
|
+
// last capture hit a 403, prepend a one-line warning so the agent
|
|
265
|
+
// can tell the user new memories may not be saved. Mirrors Hermes
|
|
266
|
+
// `on_session_start`'s billing-cache >0.8 + 403 path.
|
|
267
|
+
//
|
|
268
|
+
// 2. PINNED FACTS — always surface pinned facts as a `Pinned memories:`
|
|
269
|
+
// block, regardless of the query. These are always-relevant (user
|
|
270
|
+
// preferences, core commitments) and mirror Hermes surfacing
|
|
271
|
+
// pinned facts at session start.
|
|
272
|
+
//
|
|
273
|
+
// The quota + pinned data arrive via the injected `deps` object so this
|
|
274
|
+
// function stays environment/network clean — the caller in Task 2.7's
|
|
275
|
+
// `buildRecallDeps` binds real quota/pinned from the paired account.
|
|
276
|
+
// `citationsMode` is accepted for shape compatibility with the
|
|
277
|
+
// MemoryPluginCapability contract; it does not alter the guidance today
|
|
278
|
+
// (memory-core itself does not branch on it either, as of 2026.6.8).
|
|
279
|
+
//
|
|
280
|
+
// SCANNER-CLEAN HARD CONTRACT (env=N net=N):
|
|
281
|
+
// This function is pure string rendering. It touches NO host environment
|
|
282
|
+
// state and performs NO outbound network I/O. All quota + pinned data
|
|
283
|
+
// arrives via the deps parameter; the host environment and network
|
|
284
|
+
// primitives are never referenced. Neither the env-harvesting pair nor
|
|
285
|
+
// the disk-exfil pair can ever co-occur here — this docstring itself
|
|
286
|
+
// avoids the literal trigger tokens for that reason.
|
|
287
|
+
/** Quota threshold above which the warning fires. Matches Hermes >0.8. */
|
|
288
|
+
const QUOTA_WARN_THRESHOLD_PCT = 80;
|
|
289
|
+
/**
|
|
290
|
+
* Build the memory-prompt guidance section. Returns an array of lines
|
|
291
|
+
* (OpenClaw concatenates them into the system prompt).
|
|
292
|
+
*
|
|
293
|
+
* Ordering when all three are present:
|
|
294
|
+
* 1. (optional) quota warning — prepended so it's the first thing the
|
|
295
|
+
* agent sees; a near-full vault affects every capture decision.
|
|
296
|
+
* 2. recall guidance — the memory-core branching block.
|
|
297
|
+
* 3. (optional) pinned block — appended so always-relevant facts sit
|
|
298
|
+
* after the recall instructions the agent must follow.
|
|
299
|
+
*/
|
|
300
|
+
export function buildPromptSection(params, deps = {}) {
|
|
301
|
+
const out = [];
|
|
302
|
+
// (1) Quota warning — prepend when >80% used OR on 403/denied.
|
|
303
|
+
if (deps.quota !== undefined) {
|
|
304
|
+
const q = deps.quota;
|
|
305
|
+
const isOver = 'denied' in q ? q.denied === true : typeof q.usedPct === 'number' && q.usedPct > QUOTA_WARN_THRESHOLD_PCT;
|
|
306
|
+
if (isOver) {
|
|
307
|
+
// Wording mirrors Hermes: tells the agent new memories may not be
|
|
308
|
+
// saved so it can surface the state to the user when relevant.
|
|
309
|
+
out.push('⚠️ TotalReclaw memory near quota — some new memories may not be saved. ' +
|
|
310
|
+
'Let the user know if they ask why something was not remembered.');
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
// (2) Recall guidance — branch on memory tool availability, same shape
|
|
314
|
+
// as memory-core but adapted to TR's encrypted-vault model. The agent
|
|
315
|
+
// never sees files; memory_search / memory_get decrypt on demand.
|
|
316
|
+
const hasSearch = params.availableTools.has('memory_search');
|
|
317
|
+
const hasGet = params.availableTools.has('memory_get');
|
|
318
|
+
if (hasSearch && hasGet) {
|
|
319
|
+
out.push('Before answering anything about prior work, decisions, dates, people, ' +
|
|
320
|
+
'preferences, or todos: run memory_search against the user’s encrypted ' +
|
|
321
|
+
'TotalReclaw memory vault, then use memory_get to pull only the needed ' +
|
|
322
|
+
'facts in full. If your confidence is low after searching, say you ' +
|
|
323
|
+
'checked memory and could not find it.');
|
|
324
|
+
}
|
|
325
|
+
else if (hasSearch) {
|
|
326
|
+
out.push('Before answering anything about prior work, decisions, dates, people, ' +
|
|
327
|
+
'preferences, or todos: run memory_search against the user’s encrypted ' +
|
|
328
|
+
'TotalReclaw memory vault and answer from the matching results. If your ' +
|
|
329
|
+
'confidence is low after searching, say you checked memory and could ' +
|
|
330
|
+
'not find it.');
|
|
331
|
+
}
|
|
332
|
+
else if (hasGet) {
|
|
333
|
+
out.push('When you need a specific prior fact, use memory_get to pull it in full ' +
|
|
334
|
+
'from the user’s encrypted TotalReclaw memory vault. Do not speculate ' +
|
|
335
|
+
'about prior work, decisions, dates, people, preferences, or todos ' +
|
|
336
|
+
'without checking memory first.');
|
|
337
|
+
}
|
|
338
|
+
// If neither tool is available, no recall guidance is emitted (matches
|
|
339
|
+
// memory-core's no-guidance path). Pinned facts below still surface.
|
|
340
|
+
// (3) Pinned facts — always surface, even with no memory tools, because
|
|
341
|
+
// pinned facts are always-relevant context the agent should know
|
|
342
|
+
// regardless of whether it can also search/get.
|
|
343
|
+
const pinned = deps.pinned;
|
|
344
|
+
if (pinned !== undefined && pinned.length > 0) {
|
|
345
|
+
out.push('Pinned memories (always relevant):');
|
|
346
|
+
for (const p of pinned) {
|
|
347
|
+
// Each pinned fact on its own line, prefixed so the agent can tell
|
|
348
|
+
// the block apart from the recall guidance above. The plaintext is
|
|
349
|
+
// already decrypted by the caller (Task 2.7 binds this to the real
|
|
350
|
+
// pinned-fact lookup + decrypt pipeline).
|
|
351
|
+
out.push(`- ${p.plaintext}`);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
return out;
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
357
|
+
* Default flush thresholds. Cribbed from memory-core's
|
|
358
|
+
* `buildMemoryFlushPlan` defaults (OpenClaw 2026.6.8):
|
|
359
|
+
* - softThresholdTokens = 4000 (flush as context nears 4k tokens)
|
|
360
|
+
* - forceFlushTranscriptBytes = 2 MiB (hard flush on raw transcript size)
|
|
361
|
+
* - reserveTokensFloor = 20000 (headroom kept after a flush)
|
|
362
|
+
* These are documented defaults to be tuned at the H2 QA gate; Task 2.7
|
|
363
|
+
* may override them from cfg.agents.defaults.compaction.memoryFlush.
|
|
364
|
+
*/
|
|
365
|
+
const DEFAULT_SOFT_THRESHOLD_TOKENS = 4000;
|
|
366
|
+
const DEFAULT_FORCE_FLUSH_TRANSCRIPT_BYTES = 2 * 1024 * 1024; // 2 MiB
|
|
367
|
+
const DEFAULT_RESERVE_TOKENS_FLOOR = 20000;
|
|
368
|
+
/**
|
|
369
|
+
* TR-canonical user-prompt TEMPLATE for flush-driven extraction. Mirrors
|
|
370
|
+
* the turn-extraction user prompt built inline in extractor.ts's
|
|
371
|
+
* `extractFacts()` (see `extractor.ts:1596`): the host appends the
|
|
372
|
+
* trajectory slice (and optionally the dedup context) after this prefix.
|
|
373
|
+
* Kept here rather than in extractor.ts because extractor.ts builds the
|
|
374
|
+
* user prompt dynamically per-call (it concatenates conversationText +
|
|
375
|
+
* existing-memory dedup context), so there's no single constant to
|
|
376
|
+
* import. This template captures the TR wording so the host's flush-
|
|
377
|
+
* driven extraction produces facts in the same shape as turn extraction.
|
|
378
|
+
*/
|
|
379
|
+
const EXTRACTION_USER_PROMPT = 'Extract important facts from these recent conversation turns:\n\n';
|
|
380
|
+
/**
|
|
381
|
+
* Scratch path prefix for TR flush output. The host writes the extraction
|
|
382
|
+
* result here before handing it to the plugin for encrypt→on-chain.
|
|
383
|
+
* Namespaced so it cannot collide with memory-core's `memory/*.md` paths.
|
|
384
|
+
*/
|
|
385
|
+
const TR_FLUSH_DIR = '.totalreclaw/flush';
|
|
386
|
+
/**
|
|
387
|
+
* Format a UTC date stamp (YYYY-MM-DD) from a epoch-ms value. Pure
|
|
388
|
+
* function of the input — no host TZ dependence, no Intl nuance.
|
|
389
|
+
*/
|
|
390
|
+
function formatUtcDateStamp(nowMs) {
|
|
391
|
+
// ISO 8601 UTC: slice the YYYY-MM-DD prefix off the date portion. This
|
|
392
|
+
// mirrors memory-core's date-stamp derivation (which uses the host's
|
|
393
|
+
// configured TZ); TR deliberately uses UTC so the path is invariant
|
|
394
|
+
// across hosts and timezones — the path is a function of nowMs alone.
|
|
395
|
+
return new Date(nowMs).toISOString().slice(0, 10);
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* Build the memory flush plan returned to OpenClaw's host.
|
|
399
|
+
*
|
|
400
|
+
* @param params.cfg OpenClaw config (loose; today only the compaction
|
|
401
|
+
* overrides are consulted, and only `enabled:false`
|
|
402
|
+
* forces null. Task 2.7 may wire more overrides.)
|
|
403
|
+
* @param params.nowMs epoch-ms used to derive the date-stamped relativePath.
|
|
404
|
+
* Defaults to Date.now() — a pure clock read.
|
|
405
|
+
* @returns the MemoryFlushPlan, or null only if capture is explicitly
|
|
406
|
+
* disabled via cfg. Today capture is always on, so this never
|
|
407
|
+
* returns null in practice.
|
|
408
|
+
*/
|
|
409
|
+
export function buildFlushPlan(params = {}) {
|
|
410
|
+
const cfg = params.cfg;
|
|
411
|
+
const flushCfg = cfg?.agents?.defaults?.compaction?.memoryFlush;
|
|
412
|
+
// Capture is opt-OUT: only an explicit `enabled: false` returns null.
|
|
413
|
+
// This mirrors memory-core's contract. TR has no reason to disable
|
|
414
|
+
// capture (the poller is always-on), so default is to ship the plan.
|
|
415
|
+
if (flushCfg?.enabled === false)
|
|
416
|
+
return null;
|
|
417
|
+
// Thresholds: cribbed defaults, optionally overridden by cfg. The byte
|
|
418
|
+
// size accepts a number or a human string (e.g. "2MiB") — we only honor
|
|
419
|
+
// numeric overrides today; string parsing is left to Task 2.7.
|
|
420
|
+
const softThresholdTokens = typeof flushCfg?.softThresholdTokens === 'number' && flushCfg.softThresholdTokens >= 0
|
|
421
|
+
? flushCfg.softThresholdTokens
|
|
422
|
+
: DEFAULT_SOFT_THRESHOLD_TOKENS;
|
|
423
|
+
const forceFlushTranscriptBytes = typeof flushCfg?.forceFlushTranscriptBytes === 'number' && flushCfg.forceFlushTranscriptBytes >= 0
|
|
424
|
+
? flushCfg.forceFlushTranscriptBytes
|
|
425
|
+
: DEFAULT_FORCE_FLUSH_TRANSCRIPT_BYTES;
|
|
426
|
+
const reserveTokensFloor = typeof cfg?.agents?.defaults?.compaction?.reserveTokensFloor === 'number' &&
|
|
427
|
+
cfg.agents.defaults.compaction.reserveTokensFloor >= 0
|
|
428
|
+
? cfg.agents.defaults.compaction.reserveTokensFloor
|
|
429
|
+
: DEFAULT_RESERVE_TOKENS_FLOOR;
|
|
430
|
+
// Extraction prompt: TR's canonical v1 taxonomy prompt. Imported from
|
|
431
|
+
// extractor.ts at module load. The host hands this to the LLM at flush
|
|
432
|
+
// time; the resulting facts are then encrypt→on-chain captured by the
|
|
433
|
+
// poller (today) or the flush-driven capture path (Task 4.2).
|
|
434
|
+
//
|
|
435
|
+
// cfg overrides are honored if provided (string, trimmed) — same shape
|
|
436
|
+
// as memory-core.
|
|
437
|
+
const prompt = typeof flushCfg?.prompt === 'string' && flushCfg.prompt.trim().length > 0
|
|
438
|
+
? flushCfg.prompt.trim()
|
|
439
|
+
: EXTRACTION_USER_PROMPT;
|
|
440
|
+
const systemPrompt = typeof flushCfg?.systemPrompt === 'string' && flushCfg.systemPrompt.trim().length > 0
|
|
441
|
+
? flushCfg.systemPrompt.trim()
|
|
442
|
+
: EXTRACTION_SYSTEM_PROMPT;
|
|
443
|
+
const model = typeof flushCfg?.model === 'string' && flushCfg.model.trim().length > 0
|
|
444
|
+
? flushCfg.model.trim()
|
|
445
|
+
: undefined;
|
|
446
|
+
// relativePath: TR-namespaced scratch path, date-stamped from nowMs.
|
|
447
|
+
const nowMs = typeof params.nowMs === 'number' ? params.nowMs : Date.now();
|
|
448
|
+
const dateStamp = formatUtcDateStamp(nowMs);
|
|
449
|
+
const relativePath = `${TR_FLUSH_DIR}/${dateStamp}.jsonl`;
|
|
450
|
+
return {
|
|
451
|
+
softThresholdTokens,
|
|
452
|
+
forceFlushTranscriptBytes,
|
|
453
|
+
reserveTokensFloor,
|
|
454
|
+
model,
|
|
455
|
+
prompt,
|
|
456
|
+
systemPrompt,
|
|
457
|
+
relativePath,
|
|
458
|
+
};
|
|
459
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* native-memory — the OpenClaw-native MemoryPluginCapability wiring helper
|
|
3
|
+
* for the TR plugin (Task 2.7 of the OpenClaw native integration plan,
|
|
4
|
+
* docs/plans/2026-06-21-openclaw-native-integration-plan.md, 2026-06-21).
|
|
5
|
+
*
|
|
6
|
+
* WHY THIS FILE EXISTS:
|
|
7
|
+
* This is THE integration point. OpenClaw 2026.6.8 exposes a
|
|
8
|
+
* `api.registerMemoryCapability({ promptBuilder, flushPlanResolver, runtime })`
|
|
9
|
+
* host surface plus the conventional `api.registerTool(...)` calls for the
|
|
10
|
+
* `memory_search` / `memory_get` tools the active-memory sub-agent already
|
|
11
|
+
* knows how to drive. For TR to BE the memory backend (not just a tool
|
|
12
|
+
* plugin), it must register all four against TR's own pipeline.
|
|
13
|
+
*
|
|
14
|
+
* This helper takes a single `deps` object (built by `buildRecallDeps` in
|
|
15
|
+
* index.ts) carrying:
|
|
16
|
+
* - recall / getById (the real subgraph-search + decrypt + reranker
|
|
17
|
+
* pipeline, parameterized by the paired account — wired in index.ts
|
|
18
|
+
* because that is where the unexported pipeline helpers + the
|
|
19
|
+
* module-level auth/encryption/owner state live)
|
|
20
|
+
* - quota / pinned (prompt-builder inputs; default to "no warning" /
|
|
21
|
+
* "no pinned" when the caller does not supply them — see TODO markers
|
|
22
|
+
* in index.ts's buildRecallDeps for the H1 QA gate)
|
|
23
|
+
*
|
|
24
|
+
* and performs the canonical four-call registration in the order memory-core
|
|
25
|
+
* itself registers them (verified at
|
|
26
|
+
* /tmp/tr-openclaw-probe/node_modules/openclaw/dist/extensions/memory-core/
|
|
27
|
+
* index.js, 2026.6.8):
|
|
28
|
+
* 1. api.registerMemoryCapability({ promptBuilder, flushPlanResolver, runtime })
|
|
29
|
+
* 2. api.registerTool(() => createMemorySearchTool(runtime), { names: ['memory_search'] })
|
|
30
|
+
* 3. api.registerTool(() => createMemoryGetTool(runtime), { names: ['memory_get'] })
|
|
31
|
+
*
|
|
32
|
+
* The SAME `runtime` instance is handed to the capability AND both tool
|
|
33
|
+
* factories. This is load-bearing: the host calls
|
|
34
|
+
* `runtime.getMemorySearchManager(...)` to obtain a manager, and the
|
|
35
|
+
* tools obtain a manager via the same surface. A distinct runtime per
|
|
36
|
+
* registration would still work today (each construction is a pure
|
|
37
|
+
* closure capture) but would violate the memory-core invariant and
|
|
38
|
+
* break the day runtime owns real per-manager resources (embedder pool,
|
|
39
|
+
* connection cache). register-native.test.ts asserts the identity.
|
|
40
|
+
*
|
|
41
|
+
* DESIGN: WHY THE DEPS OBJECT IS PRE-BUILT, NOT BUILT HERE.
|
|
42
|
+
* `buildRecallDeps` lives in index.ts (not here) on purpose:
|
|
43
|
+
* 1. The real recall/getById closures capture unexported index.ts helpers
|
|
44
|
+
* (ensureInitialized, generateBlindIndices, generateEmbedding,
|
|
45
|
+
* getLSHHasher, computeCandidatePool, isDigestBlob, readClaimFromBlob,
|
|
46
|
+
* searchSubgraph, fetchFactById) AND module-level state
|
|
47
|
+
* (authKeyHex / encryptionKey / userId / subgraphOwner). Moving them
|
|
48
|
+
* here would force either (a) exporting all of those from index.ts
|
|
49
|
+
* (high blast-radius refactor with scanner-trap risk) or (b)
|
|
50
|
+
* re-plumbing them through this file's signature. Neither is in scope
|
|
51
|
+
* for Task 2.7.
|
|
52
|
+
* 2. The paired-account context is resolved LAZILY by `ensureInitialized()`
|
|
53
|
+
* on the first tool/hook call, NOT synchronously at register() time.
|
|
54
|
+
* So the closures must call `ensureInitialized(logger)` internally —
|
|
55
|
+
* they cannot be resolved at register() time even if we wanted to.
|
|
56
|
+
* 3. Keeping this file scanner-trivial: it touches NO host environment
|
|
57
|
+
* state and performs NO outbound network I/O. The closures live in
|
|
58
|
+
* index.ts alongside the rest of the plugin's network surface.
|
|
59
|
+
*
|
|
60
|
+
* SCANNER-CLEAN HARD CONTRACT (env=N net=N):
|
|
61
|
+
* This file is pure orchestration. It contains no environment-variable
|
|
62
|
+
* read token and no outbound network primitive, so neither the
|
|
63
|
+
* env-harvesting pair nor the disk-exfiltration pair can ever co-occur
|
|
64
|
+
* here. It also contains no dynamic-code-evaluation primitive (no
|
|
65
|
+
* runtime `eval` call, no `new Function` constructor). `npm run
|
|
66
|
+
* check-scanner` MUST remain 0 flags; this docstring itself avoids the
|
|
67
|
+
* literal trigger tokens for that reason.
|
|
68
|
+
*
|
|
69
|
+
* `npm run build` uses `--noCheck`, so the loose typing (the api object
|
|
70
|
+
* is typed against a minimal local interface) is safe — the plugin does
|
|
71
|
+
* not import OpenClaw's type package.
|
|
72
|
+
*/
|
|
73
|
+
import { createTrMemoryPluginRuntime, buildPromptSection, buildFlushPlan, } from './memory-runtime.js';
|
|
74
|
+
import { createMemorySearchTool, createMemoryGetTool } from './tools.js';
|
|
75
|
+
// ---------------------------------------------------------------------------
|
|
76
|
+
// registerNativeMemory — the canonical four-call wiring
|
|
77
|
+
// ---------------------------------------------------------------------------
|
|
78
|
+
/**
|
|
79
|
+
* Wire TR's native MemoryPluginCapability + the two memory tools into the
|
|
80
|
+
* OpenClaw host. Performs the canonical registration in the order memory-core
|
|
81
|
+
* itself registers them (capability first, then both tools).
|
|
82
|
+
*
|
|
83
|
+
* The SAME `runtime` instance is passed to:
|
|
84
|
+
* - api.registerMemoryCapability({ ..., runtime }) (host surface)
|
|
85
|
+
* - createMemorySearchTool(runtime) (memory_search factory)
|
|
86
|
+
* - createMemoryGetTool(runtime) (memory_get factory)
|
|
87
|
+
*
|
|
88
|
+
* Identity is load-bearing — see file header. register-native.test.ts
|
|
89
|
+
* asserts the same `runtime` reaches all three.
|
|
90
|
+
*
|
|
91
|
+
* @param api the OpenClaw plugin api (registerMemoryCapability + registerTool)
|
|
92
|
+
* @param deps the combined deps object from buildRecallDeps in index.ts
|
|
93
|
+
* @returns the runtime that was registered (for callers that want to
|
|
94
|
+
* hold a reference, e.g. for close on plugin stop)
|
|
95
|
+
*/
|
|
96
|
+
export function registerNativeMemory(api, deps) {
|
|
97
|
+
// Build the runtime ONCE. This is the single object that flows into the
|
|
98
|
+
// capability AND both tool factories below — identity is asserted by
|
|
99
|
+
// register-native.test.ts.
|
|
100
|
+
const runtime = createTrMemoryPluginRuntime({
|
|
101
|
+
recall: deps.recall,
|
|
102
|
+
getById: deps.getById,
|
|
103
|
+
});
|
|
104
|
+
// (1) Register the capability. The prompt builder closes over deps.quota
|
|
105
|
+
// and deps.pinned directly (it needs no runtime state — it's pure string
|
|
106
|
+
// rendering, see memory-runtime.ts buildPromptSection). flushPlanResolver
|
|
107
|
+
// is stateless (it returns TR's canonical extraction plan; capture is not
|
|
108
|
+
// required, we hand the function reference verbatim).
|
|
109
|
+
api.registerMemoryCapability({
|
|
110
|
+
promptBuilder: (params) => buildPromptSection(params, { quota: deps.quota, pinned: deps.pinned }),
|
|
111
|
+
flushPlanResolver: buildFlushPlan,
|
|
112
|
+
runtime,
|
|
113
|
+
});
|
|
114
|
+
// (2) + (3) Register the two tools. The factories capture the SAME runtime
|
|
115
|
+
// (the captured-runtime design — see tools.ts header). The conventional
|
|
116
|
+
// tool names survive the tool-policy strip in OC 2026.5.x (issue #223):
|
|
117
|
+
// they are passed via the `names` opts so the SDK's name bookkeeping sees
|
|
118
|
+
// them even though the tool object's own `name` field is the visible
|
|
119
|
+
// registration name.
|
|
120
|
+
api.registerTool(() => createMemorySearchTool(runtime), { names: ['memory_search'] });
|
|
121
|
+
api.registerTool(() => createMemoryGetTool(runtime), { names: ['memory_get'] });
|
|
122
|
+
return runtime;
|
|
123
|
+
}
|