@totalreclaw/totalreclaw 3.3.1-rc.9 → 3.3.1
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 +249 -1
- package/SKILL.md +29 -23
- package/api-client.ts +18 -11
- package/claims-helper.ts +47 -1
- package/config.ts +108 -4
- package/confirm-indexed.ts +191 -0
- package/crypto.ts +10 -2
- package/dist/api-client.js +226 -0
- package/dist/billing-cache.js +100 -0
- package/dist/claims-helper.js +624 -0
- package/dist/config.js +297 -0
- package/dist/confirm-indexed.js +127 -0
- package/dist/consolidation.js +258 -0
- package/dist/contradiction-sync.js +1034 -0
- package/dist/crypto.js +138 -0
- package/dist/digest-sync.js +361 -0
- package/dist/download-ux.js +63 -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 +141 -0
- package/dist/extractor.js +1225 -0
- package/dist/first-run.js +103 -0
- package/dist/fs-helpers.js +725 -0
- package/dist/gateway-url.js +197 -0
- package/dist/generate-mnemonic.js +13 -0
- package/dist/hot-cache-wrapper.js +101 -0
- package/dist/import-adapters/base-adapter.js +64 -0
- package/dist/import-adapters/chatgpt-adapter.js +238 -0
- package/dist/import-adapters/claude-adapter.js +114 -0
- package/dist/import-adapters/gemini-adapter.js +201 -0
- package/dist/import-adapters/index.js +26 -0
- package/dist/import-adapters/mcp-memory-adapter.js +219 -0
- package/dist/import-adapters/mem0-adapter.js +158 -0
- package/dist/import-adapters/types.js +1 -0
- package/dist/index.js +5388 -0
- package/dist/llm-client.js +687 -0
- package/dist/llm-profile-reader.js +346 -0
- package/dist/lsh.js +62 -0
- package/dist/onboarding-cli.js +750 -0
- package/dist/pair-cli.js +344 -0
- package/dist/pair-crypto.js +359 -0
- package/dist/pair-http.js +404 -0
- package/dist/pair-page.js +826 -0
- package/dist/pair-qr.js +107 -0
- package/dist/pair-remote-client.js +410 -0
- package/dist/pair-session-store.js +566 -0
- package/dist/pin.js +556 -0
- package/dist/qa-bug-report.js +301 -0
- package/dist/relay-headers.js +44 -0
- package/dist/reranker.js +409 -0
- package/dist/retype-setscope.js +368 -0
- package/dist/semantic-dedup.js +75 -0
- package/dist/subgraph-search.js +289 -0
- package/dist/subgraph-store.js +694 -0
- package/dist/tool-gating.js +58 -0
- package/download-ux.ts +91 -0
- package/embedder-cache.ts +230 -0
- package/embedder-loader.ts +189 -0
- package/embedder-network.ts +350 -0
- package/embedding.ts +118 -27
- package/fs-helpers.ts +277 -0
- package/gateway-url.ts +57 -9
- package/index.ts +469 -250
- package/llm-client.ts +4 -3
- package/lsh.ts +7 -2
- package/onboarding-cli.ts +114 -1
- package/package.json +24 -5
- package/pair-cli.ts +76 -8
- package/pair-crypto.ts +34 -24
- package/pair-page.ts +28 -17
- package/pair-qr.ts +152 -0
- package/pair-remote-client.ts +540 -0
- package/pin.ts +31 -0
- package/qa-bug-report.ts +84 -2
- package/relay-headers.ts +50 -0
- package/reranker.ts +40 -0
- package/retype-setscope.ts +69 -8
- package/skill.json +1 -1
- package/subgraph-search.ts +4 -3
- package/subgraph-store.ts +15 -10
package/pin.ts
CHANGED
|
@@ -26,6 +26,7 @@ import { isValidMemoryType, V0_TO_V1_TYPE } from './extractor.js';
|
|
|
26
26
|
import type { MemoryType, MemorySource, MemoryScope, MemoryVolatility } from './extractor.js';
|
|
27
27
|
import { PROTOBUF_VERSION_V4 } from './subgraph-store.js';
|
|
28
28
|
import type { SubgraphSearchFact } from './subgraph-search.js';
|
|
29
|
+
import { confirmIndexed, type ConfirmIndexedOptions } from './confirm-indexed.js';
|
|
29
30
|
|
|
30
31
|
// Lazy-load WASM core (mirrors claims-helper.ts pattern — plays nicely under
|
|
31
32
|
// both the OpenClaw runtime (CJS-ish tsx) and bare Node ESM used by tests).
|
|
@@ -114,6 +115,12 @@ export interface V1PinBlob {
|
|
|
114
115
|
id?: string;
|
|
115
116
|
/** Previously-stored pin_status on the blob (v1.1). */
|
|
116
117
|
pinStatus?: PinStatus;
|
|
118
|
+
/**
|
|
119
|
+
* 3.3.1-rc.22 — preserved when round-tripping a v1 blob through pin
|
|
120
|
+
* mutation. We keep the SOURCE blob's tag so distillation backfill
|
|
121
|
+
* never loses track of which embedder produced the original vector.
|
|
122
|
+
*/
|
|
123
|
+
embeddingModelId?: string;
|
|
117
124
|
}
|
|
118
125
|
|
|
119
126
|
/** Shape of a v0 (short-key) blob or a legacy {text, metadata} blob. */
|
|
@@ -186,6 +193,7 @@ export function parseBlobForPin(decrypted: string): ParsedBlob {
|
|
|
186
193
|
expiresAt: v1.expiresAt,
|
|
187
194
|
id: v1.id,
|
|
188
195
|
pinStatus: v1.pinStatus,
|
|
196
|
+
embeddingModelId: v1.embeddingModelId,
|
|
189
197
|
},
|
|
190
198
|
claim: shortProjection,
|
|
191
199
|
currentStatus: human,
|
|
@@ -314,6 +322,8 @@ interface V1Projection {
|
|
|
314
322
|
entities?: Array<{ name: string; type: string; role?: string }>;
|
|
315
323
|
importance: number;
|
|
316
324
|
confidence: number;
|
|
325
|
+
/** 3.3.1-rc.22 — carried through pin/retype rewrites for forward-compat. */
|
|
326
|
+
embeddingModelId?: string;
|
|
317
327
|
}
|
|
318
328
|
|
|
319
329
|
/**
|
|
@@ -336,6 +346,7 @@ function projectToV1(src: V1PinBlob | V0PinBlob, defaultSourceAgent: string): V1
|
|
|
336
346
|
entities: src.entities,
|
|
337
347
|
importance: src.importance,
|
|
338
348
|
confidence: src.confidence,
|
|
349
|
+
embeddingModelId: src.embeddingModelId,
|
|
339
350
|
};
|
|
340
351
|
}
|
|
341
352
|
|
|
@@ -445,6 +456,14 @@ export interface PinOpResult {
|
|
|
445
456
|
tx_hash?: string;
|
|
446
457
|
reason?: string;
|
|
447
458
|
error?: string;
|
|
459
|
+
/**
|
|
460
|
+
* On-chain batch submitted but subgraph indexer did not confirm the new
|
|
461
|
+
* fact id within the timeout window (default 30s). The pin/unpin IS
|
|
462
|
+
* on-chain — `tx_hash` is observable on the explorer — but a follow-up
|
|
463
|
+
* `recall`/`export` may briefly surface stale state. Resolves once the
|
|
464
|
+
* indexer catches up. See `confirm-indexed.ts`.
|
|
465
|
+
*/
|
|
466
|
+
partial?: boolean;
|
|
448
467
|
}
|
|
449
468
|
|
|
450
469
|
/**
|
|
@@ -461,6 +480,7 @@ export async function executePinOperation(
|
|
|
461
480
|
targetStatus: 'pinned' | 'active',
|
|
462
481
|
deps: PinOpDeps,
|
|
463
482
|
reason?: string,
|
|
483
|
+
confirmOpts?: ConfirmIndexedOptions,
|
|
464
484
|
): Promise<PinOpResult> {
|
|
465
485
|
// 1. Fetch the existing fact
|
|
466
486
|
const existing = await deps.fetchFactById(factId);
|
|
@@ -570,6 +590,11 @@ export async function executePinOperation(
|
|
|
570
590
|
createdAt: new Date().toISOString(),
|
|
571
591
|
supersededBy: factId,
|
|
572
592
|
pinStatus,
|
|
593
|
+
// 3.3.1-rc.22 — preserve the source claim's embedder tag through
|
|
594
|
+
// pin mutation. The new fact reuses the same encrypted embedding
|
|
595
|
+
// as the original (re-indexed via deps.regenerateBlindIndices),
|
|
596
|
+
// so the embedder identity must round-trip too.
|
|
597
|
+
embeddingModelId: v1View.embeddingModelId,
|
|
573
598
|
});
|
|
574
599
|
} catch (err) {
|
|
575
600
|
return {
|
|
@@ -672,6 +697,11 @@ export async function executePinOperation(
|
|
|
672
697
|
tx_hash: txHash,
|
|
673
698
|
};
|
|
674
699
|
}
|
|
700
|
+
// Read-after-write: poll the subgraph until the new (pinned/unpinned)
|
|
701
|
+
// fact id is indexed and active. On timeout, surface `partial: true`
|
|
702
|
+
// so a follow-up recall/export that races against indexer lag can
|
|
703
|
+
// surface a clear "still propagating" hint rather than apparent staleness.
|
|
704
|
+
const confirm = await confirmIndexed(newFactId, confirmOpts);
|
|
675
705
|
return {
|
|
676
706
|
success: true,
|
|
677
707
|
fact_id: factId,
|
|
@@ -680,6 +710,7 @@ export async function executePinOperation(
|
|
|
680
710
|
new_status: targetStatus,
|
|
681
711
|
tx_hash: txHash,
|
|
682
712
|
reason,
|
|
713
|
+
...(confirm.indexed ? {} : { partial: true }),
|
|
683
714
|
};
|
|
684
715
|
} catch (err) {
|
|
685
716
|
return {
|
package/qa-bug-report.ts
CHANGED
|
@@ -18,6 +18,13 @@
|
|
|
18
18
|
* POST. BIP-39 phrases, API keys, Telegram bot tokens, and bearer tokens
|
|
19
19
|
* in headers all become `<REDACTED>` in the posted issue. Refer to
|
|
20
20
|
* `redactSecrets()` for the exact rule set.
|
|
21
|
+
*
|
|
22
|
+
* Target repo safety: the default target is `p-diogo/totalreclaw-internal`.
|
|
23
|
+
* Operators can override via the `TOTALRECLAW_QA_REPO` env var, but only
|
|
24
|
+
* to another slug ending in `-internal`. Any other slug — including the
|
|
25
|
+
* public `p-diogo/totalreclaw` — is rejected with a loud error. rc.13 QA
|
|
26
|
+
* surfaced a repo-slug drift where QA findings leaked to the public
|
|
27
|
+
* tracker; rc.14 adds this fail-loud guard.
|
|
21
28
|
*/
|
|
22
29
|
|
|
23
30
|
// ---------------------------------------------------------------------------
|
|
@@ -148,7 +155,12 @@ export interface QaBugArgs {
|
|
|
148
155
|
export interface QaBugDeps {
|
|
149
156
|
/** GitHub personal-access token with `repo` scope. */
|
|
150
157
|
githubToken: string;
|
|
151
|
-
/**
|
|
158
|
+
/**
|
|
159
|
+
* Repo to post to. Defaults to `resolveQaRepo(null)` → reads
|
|
160
|
+
* `TOTALRECLAW_QA_REPO` env var and falls back to
|
|
161
|
+
* `p-diogo/totalreclaw-internal`. Pass a slug (tests only) to
|
|
162
|
+
* bypass env-var lookup.
|
|
163
|
+
*/
|
|
152
164
|
repo?: string;
|
|
153
165
|
/**
|
|
154
166
|
* Abstract fetch for testing — defaults to global `fetch`. Intentionally
|
|
@@ -160,6 +172,76 @@ export interface QaBugDeps {
|
|
|
160
172
|
logger?: { info: (msg: string) => void; warn: (msg: string) => void };
|
|
161
173
|
}
|
|
162
174
|
|
|
175
|
+
// ---------------------------------------------------------------------------
|
|
176
|
+
// Target repo guard — fail-loud on any repo that isn't the internal tracker.
|
|
177
|
+
// ---------------------------------------------------------------------------
|
|
178
|
+
|
|
179
|
+
export const DEFAULT_QA_REPO = 'p-diogo/totalreclaw-internal';
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Known-public repo slugs that must never receive QA bug reports. The
|
|
183
|
+
* structural rule (`endsWith('-internal')`) below should already block
|
|
184
|
+
* these, but the explicit denylist is a belt-and-braces safety against
|
|
185
|
+
* a future rename that accidentally drops the `-internal` suffix.
|
|
186
|
+
*/
|
|
187
|
+
export const PUBLIC_REPOS_DENYLIST: ReadonlySet<string> = new Set([
|
|
188
|
+
'p-diogo/totalreclaw',
|
|
189
|
+
'p-diogo/totalreclaw-website',
|
|
190
|
+
'p-diogo/totalreclaw-relay',
|
|
191
|
+
'p-diogo/totalreclaw-plugin',
|
|
192
|
+
'p-diogo/totalreclaw-hermes',
|
|
193
|
+
]);
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Resolve the target repo for a QA bug filing.
|
|
197
|
+
*
|
|
198
|
+
* Precedence: explicit override → `TOTALRECLAW_QA_REPO` env → default.
|
|
199
|
+
* Throws if the slug is on the public denylist or does not end in
|
|
200
|
+
* `-internal`. rc.13 QA found agent-filed bug reports leaking to the
|
|
201
|
+
* public repo; this guard makes any such drift fail loudly rather than
|
|
202
|
+
* silently leak RC ship-stopper detail.
|
|
203
|
+
*
|
|
204
|
+
* `TOTALRECLAW_QA_REPO` is the documented override var. The env-var
|
|
205
|
+
* read lives in `config.ts` (CONFIG.qaRepoOverride) so this module
|
|
206
|
+
* never touches process environment directly — keeps the plugin
|
|
207
|
+
* scanner-sim clean because this file also performs a GitHub HTTPS
|
|
208
|
+
* request (env + network in the same file would trip OpenClaw's
|
|
209
|
+
* env-harvesting heuristic).
|
|
210
|
+
*
|
|
211
|
+
* Pass the env-resolved slug (or `null`/empty for default) as
|
|
212
|
+
* `override`. Tests can inject via the second arg.
|
|
213
|
+
*/
|
|
214
|
+
export function resolveQaRepo(
|
|
215
|
+
override?: string | null,
|
|
216
|
+
env?: Record<string, string | undefined>,
|
|
217
|
+
): string {
|
|
218
|
+
// `env` is only for test injection — production callers should
|
|
219
|
+
// pre-resolve the env value via CONFIG.qaRepoOverride and pass it as
|
|
220
|
+
// `override`. The env lookup is a last-resort fallback that works in
|
|
221
|
+
// Node but is NEVER the primary path in production.
|
|
222
|
+
const envOverride = env ? env.TOTALRECLAW_QA_REPO : undefined;
|
|
223
|
+
const raw = (override || envOverride || DEFAULT_QA_REPO).trim();
|
|
224
|
+
if (!raw || !raw.includes('/')) {
|
|
225
|
+
throw new Error(`invalid QA repo slug '${raw}': expected 'owner/name' format`);
|
|
226
|
+
}
|
|
227
|
+
if (PUBLIC_REPOS_DENYLIST.has(raw)) {
|
|
228
|
+
throw new Error(
|
|
229
|
+
`refusing to file QA bug to PUBLIC repo '${raw}'. ` +
|
|
230
|
+
'QA bug reports contain RC ship-stopper detail that must not ' +
|
|
231
|
+
"leak to public. Set TOTALRECLAW_QA_REPO to a repo ending in " +
|
|
232
|
+
"'-internal' (e.g. p-diogo/totalreclaw-internal).",
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
if (!raw.endsWith('-internal')) {
|
|
236
|
+
throw new Error(
|
|
237
|
+
`refusing to file QA bug to repo '${raw}': slug must end in ` +
|
|
238
|
+
"'-internal' (structural safety rule). Override via " +
|
|
239
|
+
'TOTALRECLAW_QA_REPO only to another internal fork.',
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
return raw;
|
|
243
|
+
}
|
|
244
|
+
|
|
163
245
|
const VALID_INTEGRATIONS = new Set([
|
|
164
246
|
'plugin',
|
|
165
247
|
'hermes',
|
|
@@ -260,7 +342,7 @@ export async function postQaBugIssue(
|
|
|
260
342
|
if ('error' in validation) throw new Error(`invalid args: ${validation.error}`);
|
|
261
343
|
if (!deps.githubToken) throw new Error('githubToken is required');
|
|
262
344
|
|
|
263
|
-
const repo = deps.repo ??
|
|
345
|
+
const repo = resolveQaRepo(deps.repo ?? null);
|
|
264
346
|
const url = `https://api.github.com/repos/${repo}/issues`;
|
|
265
347
|
|
|
266
348
|
const title = `[qa-bug] ${redactSecrets(args.title)}`;
|
package/relay-headers.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared outbound-header helper for relay calls.
|
|
3
|
+
*
|
|
4
|
+
* Centralizes the common `X-TotalReclaw-*` headers so every fetch site
|
|
5
|
+
* consistently tags requests with:
|
|
6
|
+
* - `X-TotalReclaw-Client` — caller identity (defaults to `openclaw-plugin`).
|
|
7
|
+
* - `X-TotalReclaw-Session` — optional QA / observability tag from
|
|
8
|
+
* `TOTALRECLAW_SESSION_ID`. Used by Axiom log filters and the
|
|
9
|
+
* `qa-totalreclaw` skill to scope log searches per QA run.
|
|
10
|
+
*
|
|
11
|
+
* Pure function — no I/O, no network. Reads `getSessionId()` (which reads the
|
|
12
|
+
* env var via getter so harnesses that flip the env between calls pick up
|
|
13
|
+
* the new value).
|
|
14
|
+
*
|
|
15
|
+
* The session-id env var was accidentally placed in the v1 REMOVED_ENV_VARS
|
|
16
|
+
* list and silently warned-and-dropped, breaking Axiom traceability for QA
|
|
17
|
+
* runs (see internal#127). This helper is the canonical re-entry point for
|
|
18
|
+
* the variable.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { getSessionId } from './config.js';
|
|
22
|
+
|
|
23
|
+
/** Default `X-TotalReclaw-Client` value. */
|
|
24
|
+
export const DEFAULT_CLIENT_ID = 'openclaw-plugin';
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Build the standard outbound header set.
|
|
28
|
+
*
|
|
29
|
+
* @param overrides - merge-in additional headers (`Authorization`,
|
|
30
|
+
* `Content-Type`, etc.); these win over the defaults.
|
|
31
|
+
* @param clientId - override the `X-TotalReclaw-Client` value.
|
|
32
|
+
*
|
|
33
|
+
* Always includes `X-TotalReclaw-Client`. Includes `X-TotalReclaw-Session`
|
|
34
|
+
* only when `TOTALRECLAW_SESSION_ID` is set + non-empty.
|
|
35
|
+
*/
|
|
36
|
+
export function buildRelayHeaders(
|
|
37
|
+
overrides: Record<string, string> = {},
|
|
38
|
+
clientId: string = DEFAULT_CLIENT_ID,
|
|
39
|
+
): Record<string, string> {
|
|
40
|
+
const headers: Record<string, string> = {
|
|
41
|
+
'X-TotalReclaw-Client': clientId,
|
|
42
|
+
};
|
|
43
|
+
const sessionId = getSessionId();
|
|
44
|
+
if (sessionId) {
|
|
45
|
+
headers['X-TotalReclaw-Session'] = sessionId;
|
|
46
|
+
}
|
|
47
|
+
// Caller-supplied headers (Authorization, Content-Type, Accept, etc.) take
|
|
48
|
+
// precedence over the defaults but should generally not stomp the X-* tags.
|
|
49
|
+
return { ...headers, ...overrides };
|
|
50
|
+
}
|
package/reranker.ts
CHANGED
|
@@ -507,3 +507,43 @@ export function rerank(
|
|
|
507
507
|
// Preserve rrfScore and cosineSimilarity through MMR
|
|
508
508
|
return mmrResults as RerankerResult[];
|
|
509
509
|
}
|
|
510
|
+
|
|
511
|
+
// ---------------------------------------------------------------------------
|
|
512
|
+
// Relevance gate
|
|
513
|
+
// ---------------------------------------------------------------------------
|
|
514
|
+
|
|
515
|
+
/**
|
|
516
|
+
* Decide whether reranked results clear the relevance gate for surfacing to
|
|
517
|
+
* the user (recall tool) or auto-injecting into agent context (hooks).
|
|
518
|
+
*
|
|
519
|
+
* Plain cosine cut-off: at least one reranked result has cosine similarity
|
|
520
|
+
* with the query embedding >= `cosineThreshold`.
|
|
521
|
+
*
|
|
522
|
+
* History (rc.18 → rc.22):
|
|
523
|
+
* rc.18 issue #116 surfaced as a recall miss for short queries against
|
|
524
|
+
* the local Harrier-OSS-270m model — cosine alone produced false-negatives
|
|
525
|
+
* even when every query token literally appeared in the candidate. rc.18
|
|
526
|
+
* shipped a defensive "lexical-override" fallback (every meaningful query
|
|
527
|
+
* token had to appear as a 4-char-prefix substring in the top result).
|
|
528
|
+
* The override was always intended as a band-aid until the
|
|
529
|
+
* source-weighted reranker hoisted from `totalreclaw-core` produced
|
|
530
|
+
* honest cosine signals for short queries. rc.22 hoists that reranker
|
|
531
|
+
* and drops the band-aid: the gate is now back to a single-signal cosine
|
|
532
|
+
* cut-off, matching Hermes (Python client) and the canonical reranker
|
|
533
|
+
* spec.
|
|
534
|
+
*
|
|
535
|
+
* @param query - the user's search query (raw string) — accepted for ABI
|
|
536
|
+
* stability, no longer consulted post rc.22.
|
|
537
|
+
* @param reranked - reranked results (top first)
|
|
538
|
+
* @param cosineThreshold - the configured cosine cutoff (typically 0.15)
|
|
539
|
+
* @returns true if results should be surfaced; false to suppress
|
|
540
|
+
*/
|
|
541
|
+
export function passesRelevanceGate(
|
|
542
|
+
_query: string,
|
|
543
|
+
reranked: RerankerResult[],
|
|
544
|
+
cosineThreshold: number,
|
|
545
|
+
): boolean {
|
|
546
|
+
if (reranked.length === 0) return false;
|
|
547
|
+
const maxCosine = Math.max(...reranked.map((r) => r.cosineSimilarity ?? 0));
|
|
548
|
+
return maxCosine >= cosineThreshold;
|
|
549
|
+
}
|
package/retype-setscope.ts
CHANGED
|
@@ -34,6 +34,7 @@ import {
|
|
|
34
34
|
buildV1ClaimBlob,
|
|
35
35
|
mapTypeToCategory,
|
|
36
36
|
readV1Blob,
|
|
37
|
+
type PinStatus,
|
|
37
38
|
} from './claims-helper.js';
|
|
38
39
|
import {
|
|
39
40
|
isValidMemoryType,
|
|
@@ -48,6 +49,7 @@ import type {
|
|
|
48
49
|
} from './extractor.js';
|
|
49
50
|
import { PROTOBUF_VERSION_V4 } from './subgraph-store.js';
|
|
50
51
|
import type { SubgraphSearchFact } from './subgraph-search.js';
|
|
52
|
+
import { confirmIndexed, type ConfirmIndexedOptions } from './confirm-indexed.js';
|
|
51
53
|
|
|
52
54
|
// Lazy-load WASM core — mirrors pin.ts pattern.
|
|
53
55
|
const requireWasm = createRequire(import.meta.url);
|
|
@@ -116,6 +118,15 @@ export interface RetypeSetScopeResult {
|
|
|
116
118
|
new_scope?: MemoryScope;
|
|
117
119
|
tx_hash?: string;
|
|
118
120
|
error?: string;
|
|
121
|
+
/**
|
|
122
|
+
* Set to `true` when the on-chain batch was submitted successfully but the
|
|
123
|
+
* subgraph indexer did not confirm the new fact id within the timeout
|
|
124
|
+
* window (default 30s). The mutation IS on-chain — clients can rely on
|
|
125
|
+
* `tx_hash` for observability — but a follow-up `recall`/`export` may
|
|
126
|
+
* surface stale state for another few seconds. Resolves once the indexer
|
|
127
|
+
* catches up. See `confirm-indexed.ts` for the polling implementation.
|
|
128
|
+
*/
|
|
129
|
+
partial?: boolean;
|
|
119
130
|
}
|
|
120
131
|
|
|
121
132
|
// ---------------------------------------------------------------------------
|
|
@@ -135,6 +146,20 @@ interface NormalizedFact {
|
|
|
135
146
|
confidence: number;
|
|
136
147
|
createdAt: string;
|
|
137
148
|
expiresAt?: string;
|
|
149
|
+
/**
|
|
150
|
+
* v1.1 pin state. Preserved across retype / set_scope so that pinned facts
|
|
151
|
+
* remain pinned after a metadata edit. Issue #117 surfaced an adjacent gap
|
|
152
|
+
* where this field was silently dropped on the rewrite path.
|
|
153
|
+
*/
|
|
154
|
+
pinStatus?: PinStatus;
|
|
155
|
+
/**
|
|
156
|
+
* 3.3.1-rc.22 — embedder identity tag. Preserved across retype/set_scope
|
|
157
|
+
* so the source claim's embedder isn't lost on metadata mutation. The
|
|
158
|
+
* stored encrypted_embedding is regenerated by `deps.generateIndices`
|
|
159
|
+
* downstream; the on-claim tag survives the mutation as a forward-compat
|
|
160
|
+
* marker.
|
|
161
|
+
*/
|
|
162
|
+
embeddingModelId?: string;
|
|
138
163
|
}
|
|
139
164
|
|
|
140
165
|
function projectFromDecrypted(decrypted: string): NormalizedFact | null {
|
|
@@ -166,6 +191,8 @@ function projectFromDecrypted(decrypted: string): NormalizedFact | null {
|
|
|
166
191
|
confidence: v1.confidence,
|
|
167
192
|
createdAt: v1.createdAt,
|
|
168
193
|
expiresAt: v1.expiresAt,
|
|
194
|
+
pinStatus: v1.pinStatus,
|
|
195
|
+
embeddingModelId: v1.embeddingModelId,
|
|
169
196
|
};
|
|
170
197
|
}
|
|
171
198
|
}
|
|
@@ -221,6 +248,7 @@ async function rewriteWithMutation(
|
|
|
221
248
|
factId: string,
|
|
222
249
|
deps: RetypeSetScopeDeps,
|
|
223
250
|
mutate: (existing: NormalizedFact) => NormalizedFact,
|
|
251
|
+
confirmOpts?: ConfirmIndexedOptions,
|
|
224
252
|
): Promise<RetypeSetScopeResult> {
|
|
225
253
|
const existing = await deps.fetchFactById(factId);
|
|
226
254
|
if (!existing) {
|
|
@@ -267,6 +295,13 @@ async function rewriteWithMutation(
|
|
|
267
295
|
confidence: next.confidence,
|
|
268
296
|
createdAt: new Date().toISOString(),
|
|
269
297
|
supersededBy: factId,
|
|
298
|
+
// Issue #117 follow-up: preserve pin_status so that retype / set_scope
|
|
299
|
+
// on a pinned fact does NOT silently un-pin it. Without this, a pinned
|
|
300
|
+
// fact loses its immunity to auto-supersede after any metadata edit.
|
|
301
|
+
pinStatus: next.pinStatus,
|
|
302
|
+
// 3.3.1-rc.22 — preserve the source claim's embedder tag through
|
|
303
|
+
// retype/set_scope rewrites. Distillation forward-compat.
|
|
304
|
+
embeddingModelId: next.embeddingModelId,
|
|
270
305
|
});
|
|
271
306
|
} catch (err) {
|
|
272
307
|
return {
|
|
@@ -337,6 +372,19 @@ async function rewriteWithMutation(
|
|
|
337
372
|
tx_hash: txHash,
|
|
338
373
|
};
|
|
339
374
|
}
|
|
375
|
+
// Read-after-write: poll the subgraph until the new fact id is indexed
|
|
376
|
+
// and active, OR the timeout (default 30s) elapses. On timeout (or if
|
|
377
|
+
// the WASM bindings are unavailable / subgraph unreachable), surface
|
|
378
|
+
// `partial: true` so the caller knows the chain write succeeded but the
|
|
379
|
+
// indexer has not yet caught up. The confirm step is observational —
|
|
380
|
+
// never fail the whole operation just because confirm step couldn't run.
|
|
381
|
+
let indexed = false;
|
|
382
|
+
try {
|
|
383
|
+
const confirm = await confirmIndexed(newFactId, confirmOpts);
|
|
384
|
+
indexed = confirm.indexed;
|
|
385
|
+
} catch {
|
|
386
|
+
indexed = false;
|
|
387
|
+
}
|
|
340
388
|
return {
|
|
341
389
|
success: true,
|
|
342
390
|
fact_id: factId,
|
|
@@ -346,6 +394,7 @@ async function rewriteWithMutation(
|
|
|
346
394
|
previous_scope: current.scope,
|
|
347
395
|
new_scope: next.scope,
|
|
348
396
|
tx_hash: txHash,
|
|
397
|
+
...(indexed ? {} : { partial: true }),
|
|
349
398
|
};
|
|
350
399
|
} catch (err) {
|
|
351
400
|
return {
|
|
@@ -369,6 +418,7 @@ export async function executeRetype(
|
|
|
369
418
|
factId: string,
|
|
370
419
|
newType: MemoryType,
|
|
371
420
|
deps: RetypeSetScopeDeps,
|
|
421
|
+
confirmOpts?: ConfirmIndexedOptions,
|
|
372
422
|
): Promise<RetypeSetScopeResult> {
|
|
373
423
|
if (!isValidMemoryType(newType)) {
|
|
374
424
|
return {
|
|
@@ -377,10 +427,15 @@ export async function executeRetype(
|
|
|
377
427
|
error: `Invalid new type "${newType}". Must be one of: claim, preference, directive, commitment, episode, summary.`,
|
|
378
428
|
};
|
|
379
429
|
}
|
|
380
|
-
return rewriteWithMutation(
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
430
|
+
return rewriteWithMutation(
|
|
431
|
+
factId,
|
|
432
|
+
deps,
|
|
433
|
+
(current) => ({
|
|
434
|
+
...current,
|
|
435
|
+
type: newType,
|
|
436
|
+
}),
|
|
437
|
+
confirmOpts,
|
|
438
|
+
);
|
|
384
439
|
}
|
|
385
440
|
|
|
386
441
|
/**
|
|
@@ -391,6 +446,7 @@ export async function executeSetScope(
|
|
|
391
446
|
factId: string,
|
|
392
447
|
newScope: MemoryScope,
|
|
393
448
|
deps: RetypeSetScopeDeps,
|
|
449
|
+
confirmOpts?: ConfirmIndexedOptions,
|
|
394
450
|
): Promise<RetypeSetScopeResult> {
|
|
395
451
|
if (!(VALID_MEMORY_SCOPES as readonly string[]).includes(newScope)) {
|
|
396
452
|
return {
|
|
@@ -399,10 +455,15 @@ export async function executeSetScope(
|
|
|
399
455
|
error: `Invalid new scope "${newScope}". Must be one of: ${VALID_MEMORY_SCOPES.join(', ')}.`,
|
|
400
456
|
};
|
|
401
457
|
}
|
|
402
|
-
return rewriteWithMutation(
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
458
|
+
return rewriteWithMutation(
|
|
459
|
+
factId,
|
|
460
|
+
deps,
|
|
461
|
+
(current) => ({
|
|
462
|
+
...current,
|
|
463
|
+
scope: newScope,
|
|
464
|
+
}),
|
|
465
|
+
confirmOpts,
|
|
466
|
+
);
|
|
406
467
|
}
|
|
407
468
|
|
|
408
469
|
// ---------------------------------------------------------------------------
|
package/skill.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "totalreclaw",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "3.3.1-rc.22",
|
|
4
4
|
"description": "End-to-end encrypted memory for AI agents — portable, yours forever. XChaCha20-Poly1305 E2EE: server never sees plaintext.",
|
|
5
5
|
"author": "TotalReclaw Team",
|
|
6
6
|
"license": "MIT",
|
package/subgraph-search.ts
CHANGED
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
|
|
23
23
|
import { getSubgraphConfig } from './subgraph-store.js';
|
|
24
24
|
import { CONFIG } from './config.js';
|
|
25
|
+
import { buildRelayHeaders } from './relay-headers.js';
|
|
25
26
|
|
|
26
27
|
export interface SubgraphSearchFact {
|
|
27
28
|
id: string;
|
|
@@ -48,13 +49,13 @@ async function gqlQuery<T>(
|
|
|
48
49
|
authKeyHex?: string,
|
|
49
50
|
): Promise<T | null> {
|
|
50
51
|
try {
|
|
51
|
-
const
|
|
52
|
+
const overrides: Record<string, string> = {
|
|
52
53
|
'Content-Type': 'application/json',
|
|
53
|
-
'X-TotalReclaw-Client': 'openclaw-plugin',
|
|
54
54
|
};
|
|
55
55
|
if (authKeyHex) {
|
|
56
|
-
|
|
56
|
+
overrides['Authorization'] = `Bearer ${authKeyHex}`;
|
|
57
57
|
}
|
|
58
|
+
const headers = buildRelayHeaders(overrides);
|
|
58
59
|
const response = await fetch(endpoint, {
|
|
59
60
|
method: 'POST',
|
|
60
61
|
headers,
|
package/subgraph-store.ts
CHANGED
|
@@ -10,13 +10,18 @@
|
|
|
10
10
|
* and chain RPCs. No viem, no permissionless.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
// Lazy-load WASM
|
|
13
|
+
// Lazy-load WASM via createRequire — the shipped bundle is ESM-only and
|
|
14
|
+
// the bare `require` global is undefined there (issue #124). Same pattern
|
|
15
|
+
// as crypto / lsh / claims-helper / consolidation / digest-sync.
|
|
16
|
+
import { createRequire } from 'node:module';
|
|
17
|
+
const requireWasm = createRequire(import.meta.url);
|
|
14
18
|
let _wasm: typeof import('@totalreclaw/core') | null = null;
|
|
15
19
|
function getWasm() {
|
|
16
|
-
if (!_wasm) _wasm =
|
|
20
|
+
if (!_wasm) _wasm = requireWasm('@totalreclaw/core');
|
|
17
21
|
return _wasm;
|
|
18
22
|
}
|
|
19
23
|
import { CONFIG } from './config.js';
|
|
24
|
+
import { buildRelayHeaders } from './relay-headers.js';
|
|
20
25
|
|
|
21
26
|
// ---------------------------------------------------------------------------
|
|
22
27
|
// Pimlico 429 retry helper
|
|
@@ -383,12 +388,12 @@ async function submitFactOnChainLocked(
|
|
|
383
388
|
sender: string,
|
|
384
389
|
): Promise<{ txHash: string; userOpHash: string; success: boolean }> {
|
|
385
390
|
const bundlerUrl = `${config.relayUrl}/v1/bundler`;
|
|
386
|
-
const
|
|
391
|
+
const overrides: Record<string, string> = {
|
|
387
392
|
'Content-Type': 'application/json',
|
|
388
|
-
'X-TotalReclaw-Client': 'openclaw-plugin',
|
|
389
393
|
};
|
|
390
|
-
if (config.authKeyHex)
|
|
391
|
-
if (config.walletAddress)
|
|
394
|
+
if (config.authKeyHex) overrides['Authorization'] = `Bearer ${config.authKeyHex}`;
|
|
395
|
+
if (config.walletAddress) overrides['X-Wallet-Address'] = config.walletAddress;
|
|
396
|
+
const headers = buildRelayHeaders(overrides);
|
|
392
397
|
|
|
393
398
|
// Helper for JSON-RPC calls to relay bundler (with 429 retry)
|
|
394
399
|
async function rpc(method: string, params: unknown[]): Promise<any> {
|
|
@@ -600,12 +605,12 @@ async function submitFactBatchOnChainLocked(
|
|
|
600
605
|
sender: string,
|
|
601
606
|
): Promise<{ txHash: string; userOpHash: string; success: boolean; batchSize: number }> {
|
|
602
607
|
const bundlerUrl = `${config.relayUrl}/v1/bundler`;
|
|
603
|
-
const
|
|
608
|
+
const overrides: Record<string, string> = {
|
|
604
609
|
'Content-Type': 'application/json',
|
|
605
|
-
'X-TotalReclaw-Client': 'openclaw-plugin',
|
|
606
610
|
};
|
|
607
|
-
if (config.authKeyHex)
|
|
608
|
-
if (config.walletAddress)
|
|
611
|
+
if (config.authKeyHex) overrides['Authorization'] = `Bearer ${config.authKeyHex}`;
|
|
612
|
+
if (config.walletAddress) overrides['X-Wallet-Address'] = config.walletAddress;
|
|
613
|
+
const headers = buildRelayHeaders(overrides);
|
|
609
614
|
|
|
610
615
|
// Helper for JSON-RPC calls to relay bundler (with 429 retry)
|
|
611
616
|
async function rpc(method: string, params: unknown[]): Promise<any> {
|