@totalreclaw/totalreclaw 3.3.1-rc.21 → 3.3.1-rc.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/CHANGELOG.md +2 -1
- package/SKILL.md +1 -1
- package/claims-helper.ts +47 -1
- package/config.ts +22 -1
- package/confirm-indexed.ts +191 -0
- package/dist/claims-helper.js +19 -1
- package/dist/config.js +18 -1
- package/dist/confirm-indexed.js +127 -0
- package/dist/embedder-cache.js +185 -0
- package/dist/embedder-loader.js +121 -0
- package/dist/embedder-network.js +301 -0
- package/dist/embedding.js +88 -33
- package/dist/fs-helpers.js +162 -0
- package/dist/index.js +75 -35
- package/dist/llm-client.js +4 -3
- package/dist/pin.js +15 -1
- package/dist/reranker.js +19 -52
- package/dist/retype-setscope.js +25 -5
- package/embedder-cache.ts +230 -0
- package/embedder-loader.ts +189 -0
- package/embedder-network.ts +350 -0
- package/embedding.ts +119 -51
- package/fs-helpers.ts +185 -0
- package/index.ts +86 -47
- package/llm-client.ts +4 -3
- package/package.json +10 -5
- package/pin.ts +31 -0
- package/reranker.ts +19 -52
- package/retype-setscope.ts +57 -8
- package/skill.json +1 -1
package/dist/embedding.js
CHANGED
|
@@ -1,26 +1,38 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* TotalReclaw Plugin - Local Embedding via
|
|
2
|
+
* TotalReclaw Plugin - Local Embedding via lazy GitHub-Releases bundle
|
|
3
3
|
*
|
|
4
|
-
* Generates text embeddings locally using an ONNX model.
|
|
5
|
-
*
|
|
4
|
+
* Generates text embeddings locally using an ONNX model. Preserves the
|
|
5
|
+
* E2EE guarantee — embeddings are computed on the user's machine and
|
|
6
|
+
* never leave it. The model itself, plus the heavy native dependencies
|
|
7
|
+
* (`@huggingface/transformers`, `onnxruntime-node`), is fetched on
|
|
8
|
+
* first use from a versioned GitHub Release tarball rather than shipped
|
|
9
|
+
* inside the npm/ClawHub plugin tarball.
|
|
6
10
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
11
|
+
* Why lazy retrieval (rc.22):
|
|
12
|
+
* rc.21 OOM-killed the OpenClaw gateway during `openclaw plugins install`
|
|
13
|
+
* on a 3.7 GB Hetzner VPS — the heavy native deps required ~700 MB+
|
|
14
|
+
* peak install RAM, and a partial install left orphaned
|
|
15
|
+
* `~/.openclaw/extensions/.openclaw-install-stage-*` directories that
|
|
16
|
+
* the loader then auto-discovered on every boot, crashing the CLI.
|
|
17
|
+
* rc.22 splits the heavy bits out of the install path: the plugin
|
|
18
|
+
* tarball stays ~5-10 MB (ClawHub-friendly), the model + native deps
|
|
19
|
+
* are downloaded lazily when the user actually invokes a memory tool,
|
|
20
|
+
* and per-turn OOM is recoverable in a way install-time OOM is not.
|
|
10
21
|
*
|
|
11
|
-
*
|
|
22
|
+
* Locked to Harrier-OSS-v1-270M (640d, q4, ~344MB, pre-pooled). Changing
|
|
23
|
+
* the embedding model breaks search across an existing vault, so the
|
|
24
|
+
* `TOTALRECLAW_EMBEDDING_MODEL` user-facing env var was removed in v1.
|
|
12
25
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* 60s keep-alive, 3-attempt exponential-backoff retry, loud actionable
|
|
17
|
-
* failure. Slow-bandwidth hosts no longer see a silent freeze.
|
|
26
|
+
* Forward-compat (rc.22): every claim is tagged with `embedding_model_id`
|
|
27
|
+
* (see `getEmbeddingModelId()`) so a future distillation can be detected
|
|
28
|
+
* and rescoped per claim without breaking the active vault.
|
|
18
29
|
*/
|
|
19
|
-
|
|
20
|
-
import
|
|
21
|
-
import {
|
|
30
|
+
import os from 'node:os';
|
|
31
|
+
import path from 'node:path';
|
|
32
|
+
import { loadEmbedder } from './embedder-loader.js';
|
|
22
33
|
const HARRIER_MODEL = {
|
|
23
|
-
|
|
34
|
+
semanticId: 'harrier-oss-270m-q4',
|
|
35
|
+
hfId: 'onnx-community/harrier-oss-v1-270m-ONNX',
|
|
24
36
|
dims: 640,
|
|
25
37
|
pooling: 'sentence_embedding',
|
|
26
38
|
size: '~344MB',
|
|
@@ -29,7 +41,24 @@ const HARRIER_MODEL = {
|
|
|
29
41
|
function getModelConfig() {
|
|
30
42
|
return HARRIER_MODEL;
|
|
31
43
|
}
|
|
32
|
-
|
|
44
|
+
let runtimeConfig = null;
|
|
45
|
+
export function configureEmbedder(cfg) {
|
|
46
|
+
runtimeConfig = cfg;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Default cache root. Used when `configureEmbedder()` was not called —
|
|
50
|
+
* production code always calls it from index.ts; tests may rely on this
|
|
51
|
+
* default.
|
|
52
|
+
*/
|
|
53
|
+
function defaultCacheRoot() {
|
|
54
|
+
return path.join(os.homedir(), '.totalreclaw', 'embedder');
|
|
55
|
+
}
|
|
56
|
+
function activeRuntimeConfig() {
|
|
57
|
+
if (runtimeConfig)
|
|
58
|
+
return runtimeConfig;
|
|
59
|
+
return { cacheRoot: defaultCacheRoot(), rcTag: '0.0.0-dev' };
|
|
60
|
+
}
|
|
61
|
+
/** Lazily initialized state. */
|
|
33
62
|
let pipelineExtractor = null;
|
|
34
63
|
let autoTokenizer = null;
|
|
35
64
|
let autoModel = null;
|
|
@@ -37,39 +66,53 @@ let activeModel = null;
|
|
|
37
66
|
/**
|
|
38
67
|
* Generate an embedding vector for the given text.
|
|
39
68
|
*
|
|
40
|
-
* On first call, downloads
|
|
41
|
-
*
|
|
69
|
+
* On first call, downloads the embedder bundle (transformers + onnxruntime
|
|
70
|
+
* + the q4 ONNX model) from the pinned GitHub Release, verifies the
|
|
71
|
+
* tarball SHA-256 against the manifest, extracts to
|
|
72
|
+
* `~/.totalreclaw/embedder/v1/`, then loads the model into memory.
|
|
73
|
+
* Subsequent calls reuse the loaded model and run in ~100 ms.
|
|
42
74
|
*/
|
|
43
75
|
export async function generateEmbedding(text, options) {
|
|
44
76
|
if (!activeModel) {
|
|
45
77
|
activeModel = getModelConfig();
|
|
46
|
-
const
|
|
47
|
-
console.error(`[TotalReclaw]
|
|
48
|
-
|
|
78
|
+
const cfg = activeRuntimeConfig();
|
|
79
|
+
console.error(`[TotalReclaw] Embedding model first-call: fetching bundle ${activeModel.size} from GitHub Releases for v${cfg.rcTag} (cached at ${cfg.cacheRoot}).`);
|
|
80
|
+
const loaded = await loadEmbedder({
|
|
81
|
+
cacheRoot: cfg.cacheRoot,
|
|
82
|
+
rcTag: cfg.rcTag,
|
|
83
|
+
});
|
|
84
|
+
if (loaded.manifest.dimension !== activeModel.dims) {
|
|
85
|
+
throw new Error(`embedder bundle dimension ${loaded.manifest.dimension} does not match plugin-expected ${activeModel.dims}. ` +
|
|
86
|
+
`Refusing to use mismatched embedder — vector space drift would corrupt cosine search.`);
|
|
87
|
+
}
|
|
88
|
+
if (loaded.manifest.model_id !== activeModel.semanticId) {
|
|
89
|
+
console.error(`[TotalReclaw] WARNING: bundled model_id "${loaded.manifest.model_id}" != plugin-expected "${activeModel.semanticId}". Continuing — distillation forward-compat path.`);
|
|
90
|
+
}
|
|
91
|
+
// Resolve the transformers entrypoint via the cache-bound require.
|
|
92
|
+
// The bundled package was generated by `scripts/build-embedder-bundle.mjs`
|
|
93
|
+
// and lives at `<cache>/v1/node_modules/@huggingface/transformers`.
|
|
94
|
+
const transformers = loaded.cacheRequire('@huggingface/transformers');
|
|
95
|
+
const { AutoTokenizer, AutoModel, pipeline } = transformers;
|
|
49
96
|
if (activeModel.pooling === 'sentence_embedding') {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
autoModel = await downloadWithUX('embedding model', () => AutoModel.from_pretrained(activeModel.id, {
|
|
97
|
+
autoTokenizer = await AutoTokenizer.from_pretrained(activeModel.hfId);
|
|
98
|
+
autoModel = await AutoModel.from_pretrained(activeModel.hfId, {
|
|
53
99
|
dtype: activeModel.dtype,
|
|
54
|
-
})
|
|
100
|
+
});
|
|
55
101
|
}
|
|
56
102
|
else {
|
|
57
|
-
|
|
58
|
-
pipelineExtractor = await downloadWithUX('embedding pipeline', () => pipeline('feature-extraction', activeModel.id, {
|
|
103
|
+
pipelineExtractor = await pipeline('feature-extraction', activeModel.hfId, {
|
|
59
104
|
dtype: activeModel.dtype,
|
|
60
|
-
})
|
|
105
|
+
});
|
|
61
106
|
}
|
|
62
|
-
console.error('[TotalReclaw] Embedding model ready. Future
|
|
107
|
+
console.error('[TotalReclaw] Embedding model ready. Future calls are in-memory.');
|
|
63
108
|
}
|
|
64
109
|
const model = activeModel;
|
|
65
110
|
if (model.pooling === 'sentence_embedding') {
|
|
66
|
-
// Harrier: pre-pooled, pre-normalized output
|
|
67
111
|
const inputs = await autoTokenizer(text, { return_tensors: 'pt', padding: true });
|
|
68
112
|
const output = await autoModel(inputs);
|
|
69
113
|
return Array.from(output.sentence_embedding.data);
|
|
70
114
|
}
|
|
71
115
|
else {
|
|
72
|
-
// Pipeline models: use pooling option
|
|
73
116
|
const input = model.pooling === 'mean' && options?.isQuery
|
|
74
117
|
? `query: ${text}`
|
|
75
118
|
: text;
|
|
@@ -79,8 +122,20 @@ export async function generateEmbedding(text, options) {
|
|
|
79
122
|
}
|
|
80
123
|
/**
|
|
81
124
|
* Get the embedding vector dimensionality.
|
|
82
|
-
* Returns 640
|
|
125
|
+
* Returns 640 for Harrier-OSS-270M-q4.
|
|
83
126
|
*/
|
|
84
127
|
export function getEmbeddingDims() {
|
|
85
128
|
return getModelConfig().dims;
|
|
86
129
|
}
|
|
130
|
+
/**
|
|
131
|
+
* Get the semantic embedding-model id stamped on each new claim (rc.22+).
|
|
132
|
+
*
|
|
133
|
+
* Forward-compat marker: if a future plugin version distills to a smaller
|
|
134
|
+
* model, claims tagged with the prior id can be re-embedded selectively
|
|
135
|
+
* instead of forcing a vault-wide rebuild. Defaults to the v1 Harrier id —
|
|
136
|
+
* plugin code always tags new claims via this constant, never trusts the
|
|
137
|
+
* model id from a downloaded bundle for write-time tagging.
|
|
138
|
+
*/
|
|
139
|
+
export function getEmbeddingModelId() {
|
|
140
|
+
return getModelConfig().semanticId;
|
|
141
|
+
}
|
package/dist/fs-helpers.js
CHANGED
|
@@ -295,6 +295,168 @@ export function cleanupInstallStagingDirs(pluginDir, _now = Date.now) {
|
|
|
295
295
|
return removed;
|
|
296
296
|
}
|
|
297
297
|
// ---------------------------------------------------------------------------
|
|
298
|
+
// Partial-install detection (rc.22 finding #5)
|
|
299
|
+
// ---------------------------------------------------------------------------
|
|
300
|
+
/**
|
|
301
|
+
* Marker filename written into the plugin directory at register-time. Its
|
|
302
|
+
* presence means a prior install was interrupted before the plugin successfully
|
|
303
|
+
* loaded — a confirmed-broken half-state that the next `openclaw plugins
|
|
304
|
+
* install` retry can detect and clean.
|
|
305
|
+
*
|
|
306
|
+
* Conceptually the marker is dropped BEFORE npm install completes (the
|
|
307
|
+
* complementary npm script removes it on success) and additionally
|
|
308
|
+
* re-asserted at register-time as a second-line check. If you see this file
|
|
309
|
+
* in `<extensionsDir>/totalreclaw/`, the install never reached register()
|
|
310
|
+
* AND the marker drop wasn't undone.
|
|
311
|
+
*
|
|
312
|
+
* Constants are exported so the npm preinstall/cleanup scripts in
|
|
313
|
+
* `package.json` use the same name as the runtime detector.
|
|
314
|
+
*/
|
|
315
|
+
export const PARTIAL_INSTALL_MARKER = '.tr-partial-install';
|
|
316
|
+
/** Package name we own — used to confirm a directory is OUR plugin, not a stray. */
|
|
317
|
+
export const PLUGIN_PACKAGE_NAME = '@totalreclaw/totalreclaw';
|
|
318
|
+
/**
|
|
319
|
+
* Inspect a plugin install directory to decide whether it is fully installed,
|
|
320
|
+
* a corrupted half-state from an interrupted install, or someone else's
|
|
321
|
+
* plugin. Pure filesystem inspection; never deletes anything.
|
|
322
|
+
*
|
|
323
|
+
* Background — rc.22 finding #5
|
|
324
|
+
* ------------------------------
|
|
325
|
+
* After a partial `openclaw plugins install @totalreclaw/totalreclaw` (e.g.
|
|
326
|
+
* the auto-gateway-restart kills npm mid-build), `extensions/totalreclaw/`
|
|
327
|
+
* survives with a populated package.json but a missing or empty `dist/`. The
|
|
328
|
+
* agent's recovery retry then fires another install; OpenClaw's plugin
|
|
329
|
+
* loader scans `extensions/` and tries to register the half-state as a "hook
|
|
330
|
+
* pack", failing with the cryptic "package.json missing openclaw.hooks". The
|
|
331
|
+
* fix: detect the partial state up-front so the retry can wipe + reinstall
|
|
332
|
+
* instead of cargo-culting a confused error.
|
|
333
|
+
*
|
|
334
|
+
* Decision rules
|
|
335
|
+
* --------------
|
|
336
|
+
* 1. `pluginRootDir` does not exist → `'absent'`.
|
|
337
|
+
* 2. package.json missing or unparsable → `'foreign'` (don't touch).
|
|
338
|
+
* 3. package.json `name !== '@totalreclaw/totalreclaw'` → `'foreign'`.
|
|
339
|
+
* 4. `<root>/.tr-partial-install` exists → `'partial'` (the canonical signal).
|
|
340
|
+
* 5. `<root>/dist/index.js` missing → `'partial'` (build never finished).
|
|
341
|
+
* 6. otherwise → `'clean'`.
|
|
342
|
+
*
|
|
343
|
+
* The function is intentionally conservative: it returns `'foreign'` on any
|
|
344
|
+
* ambiguous read. Callers should NEVER auto-wipe a `'foreign'` directory.
|
|
345
|
+
*
|
|
346
|
+
* @param pluginRootDir Absolute path to the suspect plugin dir, e.g.
|
|
347
|
+
* `~/.openclaw/extensions/totalreclaw`.
|
|
348
|
+
*/
|
|
349
|
+
export function detectPartialInstall(pluginRootDir) {
|
|
350
|
+
const reasons = [];
|
|
351
|
+
// Rule 1 — absent dir.
|
|
352
|
+
let rootStat;
|
|
353
|
+
try {
|
|
354
|
+
rootStat = fs.statSync(pluginRootDir);
|
|
355
|
+
}
|
|
356
|
+
catch {
|
|
357
|
+
return { status: 'absent', reasons: ['directory does not exist'] };
|
|
358
|
+
}
|
|
359
|
+
if (!rootStat.isDirectory()) {
|
|
360
|
+
return { status: 'foreign', reasons: ['path exists but is not a directory'] };
|
|
361
|
+
}
|
|
362
|
+
// Rules 2-3 — package.json must claim our name.
|
|
363
|
+
const pkgJsonPath = path.join(pluginRootDir, 'package.json');
|
|
364
|
+
let pkgRaw;
|
|
365
|
+
try {
|
|
366
|
+
pkgRaw = fs.readFileSync(pkgJsonPath, 'utf-8');
|
|
367
|
+
}
|
|
368
|
+
catch {
|
|
369
|
+
return { status: 'foreign', reasons: ['package.json missing or unreadable'] };
|
|
370
|
+
}
|
|
371
|
+
let parsed;
|
|
372
|
+
try {
|
|
373
|
+
parsed = JSON.parse(pkgRaw);
|
|
374
|
+
}
|
|
375
|
+
catch {
|
|
376
|
+
return { status: 'foreign', reasons: ['package.json is not valid JSON'] };
|
|
377
|
+
}
|
|
378
|
+
if (parsed.name !== PLUGIN_PACKAGE_NAME) {
|
|
379
|
+
return {
|
|
380
|
+
status: 'foreign',
|
|
381
|
+
reasons: [`package.json declares "${String(parsed.name)}" not "${PLUGIN_PACKAGE_NAME}"`],
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
// Rule 4 — explicit partial marker wins.
|
|
385
|
+
const markerPath = path.join(pluginRootDir, PARTIAL_INSTALL_MARKER);
|
|
386
|
+
if (fs.existsSync(markerPath)) {
|
|
387
|
+
reasons.push(`${PARTIAL_INSTALL_MARKER} marker present (preinstall fired, postinstall did not)`);
|
|
388
|
+
}
|
|
389
|
+
// Rule 5 — dist/index.js must exist for the loader to register.
|
|
390
|
+
const distIndex = path.join(pluginRootDir, 'dist', 'index.js');
|
|
391
|
+
if (!fs.existsSync(distIndex)) {
|
|
392
|
+
reasons.push('dist/index.js missing (build artifact absent)');
|
|
393
|
+
}
|
|
394
|
+
if (reasons.length > 0) {
|
|
395
|
+
return { status: 'partial', reasons };
|
|
396
|
+
}
|
|
397
|
+
return { status: 'clean', reasons: [] };
|
|
398
|
+
}
|
|
399
|
+
/**
|
|
400
|
+
* Wipe a partial-install directory so the next `openclaw plugins install`
|
|
401
|
+
* starts from a blank slate. Only acts when `detectPartialInstall(...)`
|
|
402
|
+
* returns `'partial'` — `'foreign'` and `'clean'` are no-ops by design.
|
|
403
|
+
*
|
|
404
|
+
* Returns `true` if the directory was wiped, `false` otherwise.
|
|
405
|
+
*
|
|
406
|
+
* SAFETY: this helper is the only place that recursively deletes a plugin
|
|
407
|
+
* dir. It refuses to act on `'foreign'` and `'clean'` results so a
|
|
408
|
+
* misconfigured caller can never wipe a healthy install or someone else's
|
|
409
|
+
* plugin.
|
|
410
|
+
*/
|
|
411
|
+
export function wipePartialInstall(pluginRootDir) {
|
|
412
|
+
const detection = detectPartialInstall(pluginRootDir);
|
|
413
|
+
if (detection.status !== 'partial')
|
|
414
|
+
return false;
|
|
415
|
+
try {
|
|
416
|
+
fs.rmSync(pluginRootDir, { recursive: true, force: true });
|
|
417
|
+
return true;
|
|
418
|
+
}
|
|
419
|
+
catch {
|
|
420
|
+
return false;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
/**
|
|
424
|
+
* Drop the `.tr-partial-install` marker into `pluginRootDir`. Idempotent
|
|
425
|
+
* (overwrites any existing marker) and best-effort — returns `true` on
|
|
426
|
+
* success, `false` if the dir doesn't exist or write fails. Used by the
|
|
427
|
+
* `preinstall` npm script and (defensively) by the runtime if the npm
|
|
428
|
+
* preinstall/cleanup script pair did not fire.
|
|
429
|
+
*/
|
|
430
|
+
export function writePartialInstallMarker(pluginRootDir) {
|
|
431
|
+
try {
|
|
432
|
+
if (!fs.existsSync(pluginRootDir))
|
|
433
|
+
return false;
|
|
434
|
+
fs.writeFileSync(path.join(pluginRootDir, PARTIAL_INSTALL_MARKER), '');
|
|
435
|
+
return true;
|
|
436
|
+
}
|
|
437
|
+
catch {
|
|
438
|
+
return false;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
/**
|
|
442
|
+
* Remove the partial-install marker. Called by the `postinstall` script and
|
|
443
|
+
* (defensively) at register-time once we've confirmed the load succeeded.
|
|
444
|
+
* Returns `true` if a marker was removed, `false` if there was nothing to
|
|
445
|
+
* remove.
|
|
446
|
+
*/
|
|
447
|
+
export function clearPartialInstallMarker(pluginRootDir) {
|
|
448
|
+
try {
|
|
449
|
+
const markerPath = path.join(pluginRootDir, PARTIAL_INSTALL_MARKER);
|
|
450
|
+
if (!fs.existsSync(markerPath))
|
|
451
|
+
return false;
|
|
452
|
+
fs.unlinkSync(markerPath);
|
|
453
|
+
return true;
|
|
454
|
+
}
|
|
455
|
+
catch {
|
|
456
|
+
return false;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
// ---------------------------------------------------------------------------
|
|
298
460
|
// Auto-bootstrap of credentials.json (3.1.0 first-run UX)
|
|
299
461
|
// ---------------------------------------------------------------------------
|
|
300
462
|
/**
|
package/dist/index.js
CHANGED
|
@@ -48,13 +48,14 @@
|
|
|
48
48
|
import { deriveKeys, deriveLshSeed, computeAuthKeyHash, encrypt, decrypt, generateBlindIndices, generateContentFingerprint, } from './crypto.js';
|
|
49
49
|
import { createApiClient } from './api-client.js';
|
|
50
50
|
import { extractFacts, extractDebrief, isValidMemoryType, parseEntity, VALID_MEMORY_TYPES, LEGACY_V0_MEMORY_TYPES, VALID_MEMORY_SOURCES, VALID_MEMORY_SCOPES, EXTRACTION_SYSTEM_PROMPT, extractFactsForCompaction, } from './extractor.js';
|
|
51
|
-
import { initLLMClient, resolveLLMConfig, chatCompletion, generateEmbedding, getEmbeddingDims } from './llm-client.js';
|
|
51
|
+
import { initLLMClient, resolveLLMConfig, chatCompletion, generateEmbedding, getEmbeddingDims, getEmbeddingModelId, configureEmbedder, } from './llm-client.js';
|
|
52
52
|
import { defaultAuthProfilesRoot, readAllProfileKeys, dedupeByProvider, } from './llm-profile-reader.js';
|
|
53
53
|
import { LSHHasher } from './lsh.js';
|
|
54
|
-
import { rerank, cosineSimilarity, detectQueryIntent, INTENT_WEIGHTS
|
|
54
|
+
import { rerank, cosineSimilarity, detectQueryIntent, INTENT_WEIGHTS } from './reranker.js';
|
|
55
55
|
import { deduplicateBatch } from './semantic-dedup.js';
|
|
56
56
|
import { findNearDuplicate, shouldSupersede, clusterFacts, getStoreDedupThreshold, getConsolidationThreshold, STORE_DEDUP_MAX_CANDIDATES, } from './consolidation.js';
|
|
57
57
|
import { isSubgraphMode, getSubgraphConfig, encodeFactProtobuf, submitFactBatchOnChain, deriveSmartAccountAddress, PROTOBUF_VERSION_V4 } from './subgraph-store.js';
|
|
58
|
+
import { confirmIndexed } from './confirm-indexed.js';
|
|
58
59
|
import { DIGEST_TRAPDOOR, buildCanonicalClaim, computeEntityTrapdoor, computeEntityTrapdoors, isDigestBlob, normalizeToV1Type, readClaimFromBlob, resolveDigestMode, } from './claims-helper.js';
|
|
59
60
|
import { maybeInjectDigest, recompileDigest, fetchAllActiveClaims, isRecompileInProgress, tryBeginRecompile, endRecompile, } from './digest-sync.js';
|
|
60
61
|
import { detectAndResolveContradictions, runWeightTuningLoop, } from './contradiction-sync.js';
|
|
@@ -65,7 +66,7 @@ import { PluginHotCache } from './hot-cache-wrapper.js';
|
|
|
65
66
|
import { CONFIG, setRecoveryPhraseOverride } from './config.js';
|
|
66
67
|
import { buildRelayHeaders } from './relay-headers.js';
|
|
67
68
|
import { readBillingCache, writeBillingCache, BILLING_CACHE_PATH, } from './billing-cache.js';
|
|
68
|
-
import { ensureMemoryHeaderFile, loadCredentialsJson, writeCredentialsJson, deleteCredentialsFile, isRunningInDocker, deleteFileIfExists, resolveOnboardingState, writeOnboardingState, readPluginVersion, cleanupInstallStagingDirs, } from './fs-helpers.js';
|
|
69
|
+
import { ensureMemoryHeaderFile, loadCredentialsJson, writeCredentialsJson, deleteCredentialsFile, isRunningInDocker, deleteFileIfExists, resolveOnboardingState, writeOnboardingState, readPluginVersion, cleanupInstallStagingDirs, clearPartialInstallMarker, } from './fs-helpers.js';
|
|
69
70
|
import { isRcBuild } from './qa-bug-report.js';
|
|
70
71
|
import { decideToolGate, isGatedToolName } from './tool-gating.js';
|
|
71
72
|
import { detectFirstRun, buildWelcomePrepend } from './first-run.js';
|
|
@@ -1548,6 +1549,11 @@ async function storeExtractedFacts(facts, logger, sourceOverride) {
|
|
|
1548
1549
|
fact: factForBlob,
|
|
1549
1550
|
importance: effectiveImportance,
|
|
1550
1551
|
sourceAgent: factSource,
|
|
1552
|
+
// 3.3.1-rc.22 — tag every new claim with the active embedder id
|
|
1553
|
+
// so future distillation can rescore selectively. Plugin-only
|
|
1554
|
+
// field; survives the core validator strip via re-attach in
|
|
1555
|
+
// `buildCanonicalClaimV1`.
|
|
1556
|
+
embeddingModelId: getEmbeddingModelId(),
|
|
1551
1557
|
});
|
|
1552
1558
|
const factId = crypto.randomUUID();
|
|
1553
1559
|
// Phase 2 Slice 2d: contradiction detection + auto-resolution.
|
|
@@ -2342,6 +2348,47 @@ const plugin = {
|
|
|
2342
2348
|
// Best-effort — already swallowed inside the helper, but keep this
|
|
2343
2349
|
// outer try as belt-and-braces against future helper changes.
|
|
2344
2350
|
}
|
|
2351
|
+
// 3.3.1-rc.22 — wire the lazy-embedder runtime config so the first
|
|
2352
|
+
// `generateEmbedding()` call knows where to cache the bundle and
|
|
2353
|
+
// which RC's GitHub Release to fetch from. `pluginVersion` may be
|
|
2354
|
+
// `null` if package.json is unreadable; the embedder defaults to
|
|
2355
|
+
// a "0.0.0-dev" tag in that case.
|
|
2356
|
+
try {
|
|
2357
|
+
configureEmbedder({
|
|
2358
|
+
cacheRoot: CONFIG.embedderCachePath,
|
|
2359
|
+
rcTag: pluginVersion ?? '0.0.0-dev',
|
|
2360
|
+
});
|
|
2361
|
+
}
|
|
2362
|
+
catch (err) {
|
|
2363
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2364
|
+
api.logger.warn(`TotalReclaw: configureEmbedder failed (will use defaults): ${msg}`);
|
|
2365
|
+
}
|
|
2366
|
+
// 3.3.1-rc.22 (rc.21 finding #5): self-heal partial-install marker.
|
|
2367
|
+
// The `preinstall` npm script writes `.tr-partial-install`; the
|
|
2368
|
+
// `postinstall` script removes it on a successful install. If we
|
|
2369
|
+
// have gotten this far the loader did register us — meaning the
|
|
2370
|
+
// install succeeded enough to be useful — so any lingering marker
|
|
2371
|
+
// (e.g. npm ran preinstall but postinstall misfired) is stale.
|
|
2372
|
+
// Clear it so the next retry's detector does not see a false positive.
|
|
2373
|
+
//
|
|
2374
|
+
// 3.3.1-rc.22 (rc.21 finding #6) — gateway/reload upstream caveat:
|
|
2375
|
+
// OpenClaw's config-watcher fires `gateway/reload` when
|
|
2376
|
+
// `plugins.entries.totalreclaw` mutates (e.g. mid-install). In-flight
|
|
2377
|
+
// CLI clients see `1006 abnormal closure` and start a 600-second wait.
|
|
2378
|
+
// Proper fix is upstream OpenClaw FR. Plugin-side mitigation = these
|
|
2379
|
+
// helper calls MUST be idempotent under repeated register() calls
|
|
2380
|
+
// triggered by reload chatter. Asserted by
|
|
2381
|
+
// `install-reload-idempotency.test.ts`.
|
|
2382
|
+
try {
|
|
2383
|
+
const pluginRoot = nodePath.resolve(pluginDir, '..');
|
|
2384
|
+
const cleared = clearPartialInstallMarker(pluginRoot);
|
|
2385
|
+
if (cleared) {
|
|
2386
|
+
api.logger.info(`TotalReclaw: cleared stale .tr-partial-install marker (rc.22 finding #5)`);
|
|
2387
|
+
}
|
|
2388
|
+
}
|
|
2389
|
+
catch {
|
|
2390
|
+
// Best-effort. Helper logs internally and never throws.
|
|
2391
|
+
}
|
|
2345
2392
|
}
|
|
2346
2393
|
catch {
|
|
2347
2394
|
rcMode = false;
|
|
@@ -2994,21 +3041,10 @@ const plugin = {
|
|
|
2994
3041
|
details: { count: 0, memories: [] },
|
|
2995
3042
|
};
|
|
2996
3043
|
}
|
|
2997
|
-
// 6b. Relevance gate
|
|
2998
|
-
//
|
|
2999
|
-
//
|
|
3000
|
-
//
|
|
3001
|
-
// "favorite color" produce embeddings with low cosine sim
|
|
3002
|
-
// against the local Harrier-OSS-270m model even when the
|
|
3003
|
-
// stored fact text contains every query token.
|
|
3004
|
-
if (!passesRelevanceGate(params.query, reranked, COSINE_THRESHOLD)) {
|
|
3005
|
-
const maxCosine = Math.max(...reranked.map((r) => r.cosineSimilarity ?? 0));
|
|
3006
|
-
api.logger.info(`Recall: relevance gate filtered results (max cosine=${maxCosine.toFixed(3)}, threshold=${COSINE_THRESHOLD}, no lexical override)`);
|
|
3007
|
-
return {
|
|
3008
|
-
content: [{ type: 'text', text: 'No relevant memories found for this query.' }],
|
|
3009
|
-
details: { count: 0, memories: [] },
|
|
3010
|
-
};
|
|
3011
|
-
}
|
|
3044
|
+
// 6b. Relevance gate removed in rc.22 -- core's intent-weighted
|
|
3045
|
+
// RRF + Tier 1 source weighting handles short queries via the
|
|
3046
|
+
// BM25 component, making the rc.18 cosine + lexical-override
|
|
3047
|
+
// band-aid (issue #116) redundant.
|
|
3012
3048
|
// 7. Format results.
|
|
3013
3049
|
const lines = reranked.map((m, i) => {
|
|
3014
3050
|
const meta = metaMap.get(m.id);
|
|
@@ -3132,13 +3168,29 @@ const plugin = {
|
|
|
3132
3168
|
throw new Error(`On-chain tombstone failed (tx=${result.txHash?.slice(0, 10) || 'none'}…)`);
|
|
3133
3169
|
}
|
|
3134
3170
|
api.logger.info(`Tombstone written for ${factId}: tx=${result.txHash}`);
|
|
3171
|
+
// Read-after-write: poll the subgraph until the original fact id
|
|
3172
|
+
// is no longer active (forget flips isActive=false). On timeout
|
|
3173
|
+
// surface `partial: true` so the agent can explain the chain
|
|
3174
|
+
// write succeeded but the subgraph is still propagating.
|
|
3175
|
+
const confirm = await confirmIndexed(factId, {
|
|
3176
|
+
expect: 'inactive',
|
|
3177
|
+
authKeyHex: authKeyHex,
|
|
3178
|
+
});
|
|
3135
3179
|
return {
|
|
3136
3180
|
content: [{
|
|
3137
3181
|
type: 'text',
|
|
3138
|
-
text:
|
|
3139
|
-
|
|
3182
|
+
text: confirm.indexed
|
|
3183
|
+
? `Memory ${factId} deleted on-chain and confirmed by the subgraph (tx: ${result.txHash}).`
|
|
3184
|
+
: `Memory ${factId} deleted on-chain (tx: ${result.txHash}). ` +
|
|
3185
|
+
'The subgraph indexer is still propagating the change — ' +
|
|
3186
|
+
'recall/export may briefly show the memory as still active.',
|
|
3140
3187
|
}],
|
|
3141
|
-
details: {
|
|
3188
|
+
details: {
|
|
3189
|
+
deleted: true,
|
|
3190
|
+
txHash: result.txHash,
|
|
3191
|
+
factId,
|
|
3192
|
+
...(confirm.indexed ? {} : { partial: true }),
|
|
3193
|
+
},
|
|
3142
3194
|
};
|
|
3143
3195
|
}
|
|
3144
3196
|
else {
|
|
@@ -5019,13 +5071,7 @@ const plugin = {
|
|
|
5019
5071
|
lastQueryEmbedding = queryEmbedding;
|
|
5020
5072
|
if (reranked.length === 0)
|
|
5021
5073
|
return undefined;
|
|
5022
|
-
//
|
|
5023
|
-
// lexical-override rule (issue #116).
|
|
5024
|
-
if (!passesRelevanceGate(evt.prompt, reranked, COSINE_THRESHOLD)) {
|
|
5025
|
-
const hookMaxCosine = Math.max(...reranked.map((r) => r.cosineSimilarity ?? 0));
|
|
5026
|
-
api.logger.info(`Hook: relevance gate filtered results (max cosine=${hookMaxCosine.toFixed(3)}, threshold=${COSINE_THRESHOLD}, no lexical override)`);
|
|
5027
|
-
return undefined;
|
|
5028
|
-
}
|
|
5074
|
+
// Relevance gate removed in rc.22 (see recall tool comment).
|
|
5029
5075
|
// 7. Build context string.
|
|
5030
5076
|
const lines = reranked.map((m, i) => {
|
|
5031
5077
|
const meta = hookMetaMap.get(m.id);
|
|
@@ -5108,13 +5154,7 @@ const plugin = {
|
|
|
5108
5154
|
/* applySourceWeights (Retrieval v2 Tier 1) */ true);
|
|
5109
5155
|
if (reranked.length === 0)
|
|
5110
5156
|
return undefined;
|
|
5111
|
-
// Relevance gate
|
|
5112
|
-
// rule (issue #116).
|
|
5113
|
-
if (!passesRelevanceGate(evt.prompt, reranked, COSINE_THRESHOLD)) {
|
|
5114
|
-
const srvMaxCosine = Math.max(...reranked.map((r) => r.cosineSimilarity ?? 0));
|
|
5115
|
-
api.logger.info(`Hook: relevance gate filtered results (max cosine=${srvMaxCosine.toFixed(3)}, threshold=${COSINE_THRESHOLD}, no lexical override)`);
|
|
5116
|
-
return undefined;
|
|
5117
|
-
}
|
|
5157
|
+
// Relevance gate removed in rc.22 (see recall tool comment).
|
|
5118
5158
|
// 7. Build context string.
|
|
5119
5159
|
const lines = reranked.map((m, i) => {
|
|
5120
5160
|
const meta = hookMetaMap.get(m.id);
|
package/dist/llm-client.js
CHANGED
|
@@ -681,6 +681,7 @@ async function chatCompletionAnthropic(config, messages, maxTokens, temperature,
|
|
|
681
681
|
// Embedding (re-exported from local ONNX module)
|
|
682
682
|
// ---------------------------------------------------------------------------
|
|
683
683
|
// Embeddings are now generated locally via @huggingface/transformers
|
|
684
|
-
// (Harrier-OSS-v1-270M ONNX model). No API key needed.
|
|
685
|
-
//
|
|
686
|
-
|
|
684
|
+
// (Harrier-OSS-v1-270M ONNX model). No API key needed. The native deps +
|
|
685
|
+
// model are lazy-fetched from a pinned GitHub Release on first call —
|
|
686
|
+
// see embedding.ts + embedder-loader.ts.
|
|
687
|
+
export { generateEmbedding, getEmbeddingDims, getEmbeddingModelId, configureEmbedder } from './embedding.js';
|
package/dist/pin.js
CHANGED
|
@@ -14,6 +14,7 @@ import { buildV1ClaimBlob, mapTypeToCategory, readV1Blob, } from './claims-helpe
|
|
|
14
14
|
import { findLoserClaimInDecisionLog, maybeWriteFeedbackForPin, } from './contradiction-sync.js';
|
|
15
15
|
import { isValidMemoryType, V0_TO_V1_TYPE } from './extractor.js';
|
|
16
16
|
import { PROTOBUF_VERSION_V4 } from './subgraph-store.js';
|
|
17
|
+
import { confirmIndexed } from './confirm-indexed.js';
|
|
17
18
|
// Lazy-load WASM core (mirrors claims-helper.ts pattern — plays nicely under
|
|
18
19
|
// both the OpenClaw runtime (CJS-ish tsx) and bare Node ESM used by tests).
|
|
19
20
|
const requireWasm = createRequire(import.meta.url);
|
|
@@ -103,6 +104,7 @@ export function parseBlobForPin(decrypted) {
|
|
|
103
104
|
expiresAt: v1.expiresAt,
|
|
104
105
|
id: v1.id,
|
|
105
106
|
pinStatus: v1.pinStatus,
|
|
107
|
+
embeddingModelId: v1.embeddingModelId,
|
|
106
108
|
},
|
|
107
109
|
claim: shortProjection,
|
|
108
110
|
currentStatus: human,
|
|
@@ -228,6 +230,7 @@ function projectToV1(src, defaultSourceAgent) {
|
|
|
228
230
|
entities: src.entities,
|
|
229
231
|
importance: src.importance,
|
|
230
232
|
confidence: src.confidence,
|
|
233
|
+
embeddingModelId: src.embeddingModelId,
|
|
231
234
|
};
|
|
232
235
|
}
|
|
233
236
|
// v0 path — upgrade short-key claim to v1.
|
|
@@ -308,7 +311,7 @@ function projectToV1(src, defaultSourceAgent) {
|
|
|
308
311
|
* chain. Matches MCP's `executePinOperation` byte-for-byte on the supersession
|
|
309
312
|
* semantics (short keys, idempotent no-op, decayScore=1.0, trapdoor regen).
|
|
310
313
|
*/
|
|
311
|
-
export async function executePinOperation(factId, targetStatus, deps, reason) {
|
|
314
|
+
export async function executePinOperation(factId, targetStatus, deps, reason, confirmOpts) {
|
|
312
315
|
// 1. Fetch the existing fact
|
|
313
316
|
const existing = await deps.fetchFactById(factId);
|
|
314
317
|
if (!existing) {
|
|
@@ -408,6 +411,11 @@ export async function executePinOperation(factId, targetStatus, deps, reason) {
|
|
|
408
411
|
createdAt: new Date().toISOString(),
|
|
409
412
|
supersededBy: factId,
|
|
410
413
|
pinStatus,
|
|
414
|
+
// 3.3.1-rc.22 — preserve the source claim's embedder tag through
|
|
415
|
+
// pin mutation. The new fact reuses the same encrypted embedding
|
|
416
|
+
// as the original (re-indexed via deps.regenerateBlindIndices),
|
|
417
|
+
// so the embedder identity must round-trip too.
|
|
418
|
+
embeddingModelId: v1View.embeddingModelId,
|
|
411
419
|
});
|
|
412
420
|
}
|
|
413
421
|
catch (err) {
|
|
@@ -502,6 +510,11 @@ export async function executePinOperation(factId, targetStatus, deps, reason) {
|
|
|
502
510
|
tx_hash: txHash,
|
|
503
511
|
};
|
|
504
512
|
}
|
|
513
|
+
// Read-after-write: poll the subgraph until the new (pinned/unpinned)
|
|
514
|
+
// fact id is indexed and active. On timeout, surface `partial: true`
|
|
515
|
+
// so a follow-up recall/export that races against indexer lag can
|
|
516
|
+
// surface a clear "still propagating" hint rather than apparent staleness.
|
|
517
|
+
const confirm = await confirmIndexed(newFactId, confirmOpts);
|
|
505
518
|
return {
|
|
506
519
|
success: true,
|
|
507
520
|
fact_id: factId,
|
|
@@ -510,6 +523,7 @@ export async function executePinOperation(factId, targetStatus, deps, reason) {
|
|
|
510
523
|
new_status: targetStatus,
|
|
511
524
|
tx_hash: txHash,
|
|
512
525
|
reason,
|
|
526
|
+
...(confirm.indexed ? {} : { partial: true }),
|
|
513
527
|
};
|
|
514
528
|
}
|
|
515
529
|
catch (err) {
|