@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
|
@@ -21,8 +21,59 @@
|
|
|
21
21
|
* existing extraction pipeline. Per-file byte-offset is tracked in
|
|
22
22
|
* ~/.totalreclaw/extract-state.json so we never re-process lines.
|
|
23
23
|
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
24
|
+
* RC1 capture strategy (Phase 4, 2026-06-22):
|
|
25
|
+
* RC1 capture = POLLER-PRIMARY, FLUSH-SHADOWED. This poller is the
|
|
26
|
+
* capture workhorse. OpenClaw's host-facing `flushPlanResolver`
|
|
27
|
+
* (wired in native-memory.ts via buildFlushPlan) returns TR's
|
|
28
|
+
* extraction plan so the memory slot is "complete" and the host CAN
|
|
29
|
+
* drive flushes — but TR's encrypt→on-chain capture does NOT depend
|
|
30
|
+
* on the host invoking the flush. The flush-driven capture path
|
|
31
|
+
* (host flush → read scratch file → encrypt → on-chain) is NOT
|
|
32
|
+
* wired on TR's side today; that is H2/RC2-gated. The poller
|
|
33
|
+
* guarantees capture works regardless of host flush cadence: even
|
|
34
|
+
* if the host never flushes, the poller still captures on its own
|
|
35
|
+
* 60 s schedule. This is the graceful-degradation stance —
|
|
36
|
+
* recall-native + capture-poller meets both primary bars even if
|
|
37
|
+
* H2 (host flush cadence) fails at the RC1 QA gate.
|
|
38
|
+
*
|
|
39
|
+
* RC2 retires this poller IF H2 confirms the host invokes
|
|
40
|
+
* flushPlanResolver at useful cadence. If H2 fails, the poller
|
|
41
|
+
* stays as the long-term capture mechanism.
|
|
42
|
+
*
|
|
43
|
+
* Idempotency / offset-dedup:
|
|
44
|
+
* The poller is idempotent across poller-restart and gateway-reload.
|
|
45
|
+
* `extract-state.json` is the single source of truth for "what's
|
|
46
|
+
* been consumed from each trajectory file": loadState() reads it at
|
|
47
|
+
* the start of every poll iteration, parseNewMessages() only reads
|
|
48
|
+
* bytes past the recorded offset, and the file is rewritten (via
|
|
49
|
+
* saveState) only when at least one file's offset advanced. A
|
|
50
|
+
* poller that crashes mid-extraction loses at most the in-flight
|
|
51
|
+
* pass (state was not yet saved); the next poller re-reads from the
|
|
52
|
+
* last persisted offset and re-runs the extraction — duplicate
|
|
53
|
+
* facts are caught downstream by the dedup pass, and re-extraction
|
|
54
|
+
* of the same slice is bounded to one retry (state then advances).
|
|
55
|
+
*
|
|
56
|
+
* `parseNewMessages` caps newOffset at the last full newline so a
|
|
57
|
+
* partially-flushed trailing line is re-read on the next poll
|
|
58
|
+
* rather than dropped or double-counted.
|
|
59
|
+
*
|
|
60
|
+
* Cross-path double-write (NOT a risk today; RC2 work):
|
|
61
|
+
* Today the poller is the only capture path that actually fires.
|
|
62
|
+
* The agent_end / before_compaction / before_reset hook handlers
|
|
63
|
+
* in index.ts still REGISTER storeExtractedFacts callbacks, but on
|
|
64
|
+
* OpenClaw 2026.5.x the host never fires those events for
|
|
65
|
+
* non-bundled plugins (the bug that motivated this poller), so in
|
|
66
|
+
* practice the poller is the sole capture path. If a future
|
|
67
|
+
* OpenClaw release un-blocks the hooks (or when the RC2
|
|
68
|
+
* flush-driven capture path is wired), BOTH the poller AND the
|
|
69
|
+
* parallel path would capture from overlapping state with NO
|
|
70
|
+
* shared dedup — the hooks do NOT consult `extract-state.json`.
|
|
71
|
+
* That is acceptable for RC1 (only the poller fires) but becomes
|
|
72
|
+
* a real double-write risk at RC2. See the TODO(RC2/H2) marker on
|
|
73
|
+
* STATE_FILE below for where the shared last-captured-offset
|
|
74
|
+
* resolver would live. The earlier claim that "both paths can
|
|
75
|
+
* coexist with offset-based dedup" was aspirational; no such
|
|
76
|
+
* shared offset exists today.
|
|
26
77
|
*
|
|
27
78
|
* Module boundary (scanner constraint):
|
|
28
79
|
* This file does disk I/O (fs.read* on trajectory files + state file)
|
|
@@ -39,7 +90,24 @@
|
|
|
39
90
|
import fs from 'node:fs';
|
|
40
91
|
import os from 'node:os';
|
|
41
92
|
import path from 'node:path';
|
|
93
|
+
import { fileURLToPath } from 'node:url';
|
|
42
94
|
const DEFAULT_POLL_INTERVAL_MS = 60_000;
|
|
95
|
+
// Per-file offset tracker (poller's own source of truth for what's been
|
|
96
|
+
// consumed from each trajectory file). Today this is consulted ONLY by the
|
|
97
|
+
// poller — the agent_end / before_compaction / before_reset hooks in
|
|
98
|
+
// index.ts do NOT read or write it, so if any of those hooks ever fire
|
|
99
|
+
// alongside the poller they would double-capture from overlapping state.
|
|
100
|
+
//
|
|
101
|
+
// TODO(RC2/H2): when the flush-driven capture path is wired (host invokes
|
|
102
|
+
// flushPlanResolver → TR reads the scratch file → encrypt → on-chain) OR
|
|
103
|
+
// the upstream agent_end block is lifted, ALL capture paths MUST consult
|
|
104
|
+
// a shared last-captured-offset resolver before encrypting. The simplest
|
|
105
|
+
// shape is to expose loadState()/saveState() (or a `consumed(file)`
|
|
106
|
+
// helper) to the hook handlers and the flush callback so they skip any
|
|
107
|
+
// byte range the poller already consumed, and conversely the poller must
|
|
108
|
+
// skip any range the hooks/flush path recorded. Until then, this file is
|
|
109
|
+
// poller-private and there is no cross-path double-write risk (only the
|
|
110
|
+
// poller fires on OpenClaw 2026.5.x).
|
|
43
111
|
const STATE_FILE = path.join(os.homedir(), '.totalreclaw', 'extract-state.json');
|
|
44
112
|
/**
|
|
45
113
|
* Skip trajectory files older than this. A user who installs
|
|
@@ -50,16 +118,95 @@ const STATE_FILE = path.join(os.homedir(), '.totalreclaw', 'extract-state.json')
|
|
|
50
118
|
* re-scanned, and skip the extraction path entirely.
|
|
51
119
|
*/
|
|
52
120
|
const STALE_TRAJECTORY_AGE_MS = 7 * 24 * 60 * 60 * 1000; // 7 days
|
|
121
|
+
/**
|
|
122
|
+
* Module-global handle to the currently-running poller (rc.20, #402).
|
|
123
|
+
*
|
|
124
|
+
* OpenClaw's SIGUSR1 restarts are IN-PROCESS — the module cache survives, so a
|
|
125
|
+
* same-version re-register hits THIS module instance again. Without a guard,
|
|
126
|
+
* each re-register started a fresh setInterval on top of the previous one and
|
|
127
|
+
* live pollers accumulated (a fresh container boot was observed with 2 from
|
|
128
|
+
* boot double-register). `startTrajectoryPoller` stops the previous poller
|
|
129
|
+
* from this module instance before starting a new one.
|
|
130
|
+
*/
|
|
131
|
+
let activePoller = null;
|
|
132
|
+
/**
|
|
133
|
+
* Path to THIS module's own file, captured once at load. Each poll tick
|
|
134
|
+
* verifies it still exists AND is the same file (same inode + mtime); if the
|
|
135
|
+
* plugin dir was removed, or the file at this path was swapped for a different
|
|
136
|
+
* one (an old version uninstalled then a new version reinstalled at the SAME
|
|
137
|
+
* path within seconds — the gateway restart is an in-process signal, so the
|
|
138
|
+
* stale module instance survives), the poller self-terminates so a zombie
|
|
139
|
+
* module instance from a stale version can't keep submitting UserOps (rc.20,
|
|
140
|
+
* #402). An existence-only check missed the same-path reinstall case (review
|
|
141
|
+
* LOW-2, observed live: OpenClaw recreates dist at the same path in ~45s, so
|
|
142
|
+
* `existsSync` stayed true and the old-version poller ran on). Tests override
|
|
143
|
+
* this path via `opts.sentinelPath`.
|
|
144
|
+
*/
|
|
145
|
+
const MODULE_SENTINEL = fileURLToPath(import.meta.url);
|
|
53
146
|
/**
|
|
54
147
|
* Start the trajectory poller. Runs an initial poll after 5 s, then
|
|
55
148
|
* every `pollIntervalMs` (default 60 s). Returns a handle the caller
|
|
56
149
|
* can use to stop polling and run one-shot polls in tests.
|
|
150
|
+
*
|
|
151
|
+
* Lifecycle guards (rc.20, #402): a previously-started poller from this module
|
|
152
|
+
* instance is stopped first (singleton), and each tick self-terminates if this
|
|
153
|
+
* module's file is gone.
|
|
57
154
|
*/
|
|
58
155
|
export function startTrajectoryPoller(deps, opts = {}) {
|
|
59
156
|
const pollIntervalMs = opts.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
|
|
60
157
|
const stateFile = opts.stateFile ?? STATE_FILE;
|
|
158
|
+
const sentinelPath = opts.sentinelPath ?? MODULE_SENTINEL;
|
|
159
|
+
// Capture the sentinel's file identity (inode + mtime) once at start so each
|
|
160
|
+
// tick can tell "same file still there" from "different file swapped in at
|
|
161
|
+
// the same path" (reinstall/upgrade). If the initial stat throws, fall back
|
|
162
|
+
// to existence-only semantics rather than crashing startup.
|
|
163
|
+
let sentinelIdentity = null;
|
|
164
|
+
try {
|
|
165
|
+
const st = fs.statSync(sentinelPath);
|
|
166
|
+
sentinelIdentity = { ino: st.ino, mtimeMs: st.mtimeMs };
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
sentinelIdentity = null;
|
|
170
|
+
}
|
|
171
|
+
// Singleton guard: stop any poller this module instance started earlier so a
|
|
172
|
+
// same-version re-register (in-process SIGUSR1 restart) does not stack a
|
|
173
|
+
// second live setInterval on top of the first.
|
|
174
|
+
if (activePoller) {
|
|
175
|
+
activePoller.stop();
|
|
176
|
+
deps.logger.info('extractd: previous poller stopped (re-register)');
|
|
177
|
+
}
|
|
178
|
+
let timer;
|
|
179
|
+
let initialTimeout;
|
|
180
|
+
const stop = () => {
|
|
181
|
+
if (timer)
|
|
182
|
+
clearInterval(timer);
|
|
183
|
+
if (initialTimeout)
|
|
184
|
+
clearTimeout(initialTimeout);
|
|
185
|
+
if (activePoller === handle)
|
|
186
|
+
activePoller = null;
|
|
187
|
+
};
|
|
61
188
|
const pollAndExtract = async () => {
|
|
62
189
|
try {
|
|
190
|
+
// Cross-version self-termination: if this module's own file is gone, or a
|
|
191
|
+
// DIFFERENT file was swapped in at the same path (uninstall→reinstall of a
|
|
192
|
+
// newer version), stop — a zombie poller from a stale module instance must
|
|
193
|
+
// not keep capturing.
|
|
194
|
+
let sentinelStat = null;
|
|
195
|
+
try {
|
|
196
|
+
sentinelStat = fs.statSync(sentinelPath);
|
|
197
|
+
}
|
|
198
|
+
catch {
|
|
199
|
+
// File gone (plugin dir removed).
|
|
200
|
+
deps.logger.warn('extractd: poller self-terminated (plugin dir removed)');
|
|
201
|
+
stop();
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
if (sentinelIdentity !== null &&
|
|
205
|
+
(sentinelStat.ino !== sentinelIdentity.ino || sentinelStat.mtimeMs !== sentinelIdentity.mtimeMs)) {
|
|
206
|
+
deps.logger.warn('extractd: poller self-terminated (plugin file replaced — reinstall/upgrade detected)');
|
|
207
|
+
stop();
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
63
210
|
await deps.ensureInitialized();
|
|
64
211
|
if (deps.isPairingPending())
|
|
65
212
|
return;
|
|
@@ -149,24 +296,23 @@ export function startTrajectoryPoller(deps, opts = {}) {
|
|
|
149
296
|
deps.logger.error(`extractd: poll iteration failed: ${msg}`);
|
|
150
297
|
}
|
|
151
298
|
};
|
|
152
|
-
|
|
299
|
+
timer = setInterval(() => {
|
|
153
300
|
void pollAndExtract();
|
|
154
301
|
}, pollIntervalMs);
|
|
155
302
|
if (typeof timer.unref === 'function')
|
|
156
303
|
timer.unref();
|
|
157
|
-
|
|
304
|
+
initialTimeout = setTimeout(() => {
|
|
158
305
|
void pollAndExtract();
|
|
159
306
|
}, 5_000);
|
|
160
307
|
if (typeof initialTimeout.unref === 'function')
|
|
161
308
|
initialTimeout.unref();
|
|
162
309
|
deps.logger.info(`extractd: trajectory poller started (interval=${Math.round(pollIntervalMs / 1000)}s)`);
|
|
163
|
-
|
|
164
|
-
stop
|
|
165
|
-
clearInterval(timer);
|
|
166
|
-
clearTimeout(initialTimeout);
|
|
167
|
-
},
|
|
310
|
+
const handle = {
|
|
311
|
+
stop,
|
|
168
312
|
pollOnce: pollAndExtract,
|
|
169
313
|
};
|
|
314
|
+
activePoller = handle;
|
|
315
|
+
return handle;
|
|
170
316
|
}
|
|
171
317
|
// ---------------------------------------------------------------------------
|
|
172
318
|
// Filesystem scan + trajectory parser
|