@totalreclaw/totalreclaw 3.3.11-rc.3 → 3.3.11-rc.5
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 +49 -0
- package/SKILL.md +1 -1
- package/dist/fs-helpers.js +40 -0
- package/dist/index.js +1 -1
- package/dist/tr-cli.js +1 -1
- package/dist/trajectory-poller.js +46 -1
- package/fs-helpers.ts +41 -0
- package/index.ts +1 -1
- package/package.json +1 -1
- package/skill.json +1 -1
- package/tr-cli.ts +1 -1
- package/trajectory-poller.ts +47 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,55 @@ All notable changes to `@totalreclaw/totalreclaw` (the OpenClaw plugin) are docu
|
|
|
4
4
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## [3.3.11-rc.5] — 2026-05-07
|
|
8
|
+
|
|
9
|
+
Trajectory poller hardening: cap extractions per poll iteration + skip stale trajectory files. Pedro's 2026-05-07 zai 429 cascade was caused by ~5 old session files all crossing the extract threshold in the same poll → 5 back-to-back LLM calls in seconds → daily quota tripped. Today's chat memories were lost because every extraction call returned 0 facts (LLM rejected with rate-limit).
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- **Cap = 1 extraction per poll iteration.** When multiple session files cross the extract-interval threshold in the same 60 s poll, only the first fires the LLM call. The rest defer to subsequent polls. Their `turnsAccum` and `offset` state is preserved, so they don't lose progress — they just stagger. With the default 60 s poll interval, 5 backlogged files take 5 minutes to drain instead of 5 seconds. Free-tier LLM rate limits don't trip.
|
|
14
|
+
- **Stale-trajectory skip (>7 days mtime).** A user installing TotalReclaw on a host with months of OpenClaw session log history won't get a retroactive extraction backlog. Files with mtime older than 7 days are baseline-snapshotted (offset captured) but skip the extraction path entirely. If the user later resumes an old session, the offset is current and only net-new content extracts.
|
|
15
|
+
|
|
16
|
+
### Implementation notes
|
|
17
|
+
|
|
18
|
+
- `MAX_EXTRACTIONS_PER_POLL = 1` constant in `trajectory-poller.ts`. Sequential `for` loop already guarantees serialization within a poll; the cap just stops issuing additional LLM calls once the first one completes.
|
|
19
|
+
- `STALE_TRAJECTORY_AGE_MS = 7 * 24 * 60 * 60 * 1000`. Stale files get a one-time offset record (`state[file] = {offset: stat.size, turnsAccum: 0}`) so they're never re-stat'd repeatedly. If the user re-engages an old session (writes new content → mtime refreshes), the stale-skip check fails and normal extraction resumes from the recorded offset.
|
|
20
|
+
- 4 new test cases in `trajectory-poller.test.ts`: cap=1 multi-session deferral, second-poll-no-op-after-deferred-cap, stale-file skip (>7 days), recent-file (<7 days) NOT skipped. 44/44 green.
|
|
21
|
+
- 92/92 fs-helpers + 21/21 register-command-name + 37/37 skill-md + 21/21 tr-cli-json + 44/44 trajectory-poller + 10/10 manifest-shape. check-scanner: 129 files, 0 flags.
|
|
22
|
+
|
|
23
|
+
### Won't fix here
|
|
24
|
+
|
|
25
|
+
- Pop-os specific delayed-load anomaly (~58 min from rc.4 install to plugin first-load) — couldn't reproduce in fresh container; pop-os has accumulated state from many install/wipe cycles. Auto-QA confirmed clean fresh-install path.
|
|
26
|
+
- The retroactive-backlog scenario itself remains: if a user has ACTIVE sessions (mtime < 7 days) with high turn counts, they'll still drain over multiple polls. With cap=1 and 60 s interval, 5 active-but-backlogged files = 5 minutes to drain. Acceptable.
|
|
27
|
+
|
|
28
|
+
## [3.3.11-rc.4] — 2026-05-07
|
|
29
|
+
|
|
30
|
+
Fix #4 added to `patchOpenClawConfig()`: the **populated-allowlist plugin-skip bug** that broke every realistic install path.
|
|
31
|
+
|
|
32
|
+
### Fixed
|
|
33
|
+
|
|
34
|
+
- **`plugins.bundledDiscovery: "compat"` now auto-set when `plugins.allow` is populated.** OpenClaw 2026.5.x switches the plugin loader into strict-allowlist mode whenever `plugins.allow` is a non-empty array. In that mode, **non-bundled plugins like totalreclaw are silently rejected even when listed in the allow array** unless `plugins.bundledDiscovery: "compat"` is set. Without the compat flag, the gateway boots with only the bundled providers (e.g. `[device-pair, telegram]`), the totalreclaw plugin is dropped on the floor, and `extractd:` log lines never appear because the plugin's `register()` never runs.
|
|
35
|
+
|
|
36
|
+
This bug surfaces on every real-world install path:
|
|
37
|
+
- User installs telegram + zai/openai/anthropic before installing TotalReclaw → `plugins.allow` populated → loader rejects totalreclaw on every gateway restart.
|
|
38
|
+
- Pedro's pop-os QA host hit this on rc.3 because the host had `['device-pair', 'google', 'telegram', 'totalreclaw', 'zai']` in `plugins.allow` from prior install cycles.
|
|
39
|
+
- `openclaw doctor --fix` was the manual cure (sets `bundledDiscovery: "compat"`), but users shouldn't need to run doctor for the plugin they just installed.
|
|
40
|
+
|
|
41
|
+
Fix: `patchOpenClawConfig()` now sets `plugins.bundledDiscovery = "compat"` when `plugins.allow` is a non-empty array AND `bundledDiscovery` is unset. Power-users who explicitly chose `"allowlist"` (the stricter mode) keep their setting — only first-run defaults are touched. `plugins.allow=null` (auto-discover mode) is left alone.
|
|
42
|
+
|
|
43
|
+
### Auto-QA gap (acknowledged)
|
|
44
|
+
|
|
45
|
+
The 3.3.11-rc.1 auto-QA harness ran on a fresh canonical OpenClaw 2026.5.4 container with `plugins.allow=null` (auto-discover mode). It hit the looser code path where the plugin loaded fine. The strict-allowlist code path was untested. As a result, rc.3 shipped with this bug, Pedro hit it on pop-os, and I had to chase it down post-publish.
|
|
46
|
+
|
|
47
|
+
This RC adds:
|
|
48
|
+
- 5 new fs-helpers test cases covering Fix #4: empty-allow no-op, populated-allow + missing-bundledDiscovery patches, empty-array allow no-op, explicit `"allowlist"` preserved, full pop-os-style scenario.
|
|
49
|
+
- TODO for next iteration: expand the live-container auto-QA harness to run a populated-allowlist scenario (mimicking a real install with telegram + zai pre-configured) alongside the existing fresh-allow=null scenario.
|
|
50
|
+
|
|
51
|
+
### Implementation notes
|
|
52
|
+
|
|
53
|
+
- 92/92 fs-helpers tests green. 21/21 register-command-name + 37/37 skill-md + 21/21 tr-cli-json + 40/40 trajectory-poller + 10/10 manifest-shape. check-scanner: 129 files, 0 flags.
|
|
54
|
+
- The patchOpenClawConfig restart-required warn now mentions all four keys (slots.memory + allowConversationAccess + telegram.streaming.mode + bundledDiscovery).
|
|
55
|
+
|
|
7
56
|
## [3.3.11-rc.3] — 2026-05-06
|
|
8
57
|
|
|
9
58
|
Two real-bug fixes flagged by Pedro's pop-os QA on rc.11-rc.1/rc.2: credentials.json shipped with only `{mnemonic}` (no userId, no salt), and the subgraph "Invalid character 'q' at position 0" Bytes-decode error.
|
package/SKILL.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: totalreclaw
|
|
3
3
|
description: "Install + set up TotalReclaw encrypted memory for OpenClaw, then use tr CLI for remember / recall. Trigger on 'install TotalReclaw', 'set up TotalReclaw', 'restore my recovery phrase', or any remember / recall request."
|
|
4
|
-
version: 3.3.11-rc.
|
|
4
|
+
version: 3.3.11-rc.5
|
|
5
5
|
author: TotalReclaw Team
|
|
6
6
|
license: MIT
|
|
7
7
|
homepage: https://totalreclaw.xyz
|
package/dist/fs-helpers.js
CHANGED
|
@@ -954,6 +954,16 @@ export function resolveOnboardingState(credentialsPath, statePath) {
|
|
|
954
954
|
* this to "off" on first run for a clean UX. Existing explicit values
|
|
955
955
|
* ("partial", "block", "progress") are preserved.
|
|
956
956
|
*
|
|
957
|
+
* 4. `plugins.bundledDiscovery = "compat"` (only if unset, and only when
|
|
958
|
+
* `plugins.allow` is populated). When `plugins.allow` is a non-empty
|
|
959
|
+
* array, OpenClaw 2026.5.x switches the loader into strict-allowlist
|
|
960
|
+
* mode and silently rejects non-bundled plugins like totalreclaw —
|
|
961
|
+
* EVEN IF they are listed in the allow array. Setting
|
|
962
|
+
* `bundledDiscovery: "compat"` restores the looser behavior so allow-
|
|
963
|
+
* listed non-bundled plugins load. Without this fix, users with
|
|
964
|
+
* telegram or any model provider configured before TR install (which
|
|
965
|
+
* populates allow) get a silent plugin-skip on every gateway boot.
|
|
966
|
+
*
|
|
957
967
|
* Design constraints
|
|
958
968
|
* ------------------
|
|
959
969
|
* - SYNCHRONOUS — called during register() which must be sync.
|
|
@@ -1076,6 +1086,36 @@ export function patchOpenClawConfig(configPath) {
|
|
|
1076
1086
|
}
|
|
1077
1087
|
}
|
|
1078
1088
|
}
|
|
1089
|
+
// --- Fix #4: plugins.bundledDiscovery = "compat" (3.3.11-rc.4) ---
|
|
1090
|
+
//
|
|
1091
|
+
// OpenClaw 2026.5.x: when `plugins.allow` is populated (any non-empty
|
|
1092
|
+
// array), the gateway's plugin loader switches into strict-allowlist
|
|
1093
|
+
// mode. In strict mode, NON-BUNDLED plugins like totalreclaw are
|
|
1094
|
+
// silently rejected even when listed in `plugins.allow`, unless
|
|
1095
|
+
// `plugins.bundledDiscovery = "compat"` is explicitly set. Pedro's
|
|
1096
|
+
// 2026-05-07 QA on pop-os surfaced this — the gateway booted with only
|
|
1097
|
+
// the bundled providers (telegram, device-pair) and skipped totalreclaw
|
|
1098
|
+
// despite it being in the allow list. `openclaw doctor --fix` cures it
|
|
1099
|
+
// by setting `bundledDiscovery: "compat"`, but users shouldn't need
|
|
1100
|
+
// to run doctor manually for the plugin to load.
|
|
1101
|
+
//
|
|
1102
|
+
// Fix: when `plugins.allow` is a non-empty array AND
|
|
1103
|
+
// `plugins.bundledDiscovery` is unset, set it to "compat". If the user
|
|
1104
|
+
// explicitly chose "allowlist" (the stricter mode), preserve their
|
|
1105
|
+
// choice — only first-run defaults are touched.
|
|
1106
|
+
//
|
|
1107
|
+
// This bug was missed by the auto-QA harness because the harness ran
|
|
1108
|
+
// on a fresh canonical container with `plugins.allow=null`, hitting
|
|
1109
|
+
// the auto-discover code path. Real users with telegram + a model
|
|
1110
|
+
// provider configured before TR install have a populated allow list,
|
|
1111
|
+
// hitting the strict-mode path. The auto-QA harness in 3.3.11-rc.4
|
|
1112
|
+
// adds a populated-allow scenario to catch future regressions.
|
|
1113
|
+
if (Array.isArray(cfg.plugins.allow) && cfg.plugins.allow.length > 0) {
|
|
1114
|
+
if (cfg.plugins.bundledDiscovery === undefined) {
|
|
1115
|
+
cfg.plugins.bundledDiscovery = 'compat';
|
|
1116
|
+
mutated = true;
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1079
1119
|
if (!mutated)
|
|
1080
1120
|
return 'unchanged';
|
|
1081
1121
|
// Write back with 2-space indent to match OpenClaw's own write style.
|
package/dist/index.js
CHANGED
|
@@ -2574,7 +2574,7 @@ const plugin = {
|
|
|
2574
2574
|
if (patchResult === 'patched') {
|
|
2575
2575
|
api.logger.warn('TotalReclaw: updated openclaw.json with required 2026.5.x keys ' +
|
|
2576
2576
|
'(plugins.slots.memory + hooks.allowConversationAccess + ' +
|
|
2577
|
-
'channels.telegram.streaming.mode). ' +
|
|
2577
|
+
'channels.telegram.streaming.mode + plugins.bundledDiscovery). ' +
|
|
2578
2578
|
'Gateway restart required for the changes to take effect. ' +
|
|
2579
2579
|
'Run `/totalreclaw-restart` or restart the gateway manually.');
|
|
2580
2580
|
}
|
package/dist/tr-cli.js
CHANGED
|
@@ -41,7 +41,7 @@ const STATE_PATH = CONFIG.onboardingStatePath;
|
|
|
41
41
|
// Auto-synced by skill/scripts/sync-version.mjs from skill/plugin/package.json::version.
|
|
42
42
|
// Do not edit by hand — running tests will catch drift but the publish workflow
|
|
43
43
|
// rewrites this constant at the start of every npm/ClawHub publish.
|
|
44
|
-
const PLUGIN_VERSION = '3.3.11-rc.
|
|
44
|
+
const PLUGIN_VERSION = '3.3.11-rc.5';
|
|
45
45
|
function die(msg, code = 1) {
|
|
46
46
|
process.stderr.write(`tr: ${msg}\n`);
|
|
47
47
|
process.exit(code);
|
|
@@ -41,6 +41,15 @@ import os from 'node:os';
|
|
|
41
41
|
import path from 'node:path';
|
|
42
42
|
const DEFAULT_POLL_INTERVAL_MS = 60_000;
|
|
43
43
|
const STATE_FILE = path.join(os.homedir(), '.totalreclaw', 'extract-state.json');
|
|
44
|
+
/**
|
|
45
|
+
* Skip trajectory files older than this. A user who installs
|
|
46
|
+
* TotalReclaw on a host with months of OpenClaw session log history
|
|
47
|
+
* shouldn't get a retroactive extraction backlog — we only care about
|
|
48
|
+
* ongoing chat from now forward (3.3.11-rc.5). Files with mtime older
|
|
49
|
+
* than this threshold get a one-time offset snapshot so they're never
|
|
50
|
+
* re-scanned, and skip the extraction path entirely.
|
|
51
|
+
*/
|
|
52
|
+
const STALE_TRAJECTORY_AGE_MS = 7 * 24 * 60 * 60 * 1000; // 7 days
|
|
44
53
|
/**
|
|
45
54
|
* Start the trajectory poller. Runs an initial poll after 5 s, then
|
|
46
55
|
* every `pollIntervalMs` (default 60 s). Returns a handle the caller
|
|
@@ -62,15 +71,51 @@ export function startTrajectoryPoller(deps, opts = {}) {
|
|
|
62
71
|
return;
|
|
63
72
|
const extractInterval = deps.getExtractInterval();
|
|
64
73
|
let stateChanged = false;
|
|
74
|
+
// 3.3.11-rc.5: cap extractions per poll iteration. Pedro's
|
|
75
|
+
// 2026-05-07 zai 429 cascade was caused by N session files all
|
|
76
|
+
// crossing the extract threshold in the same poll → N back-to-back
|
|
77
|
+
// LLM calls trip the rate-limiter (especially on free tiers).
|
|
78
|
+
// With cap=1, extra files defer to the next poll iteration
|
|
79
|
+
// (60 s later by default). Their turnsAccum is preserved across
|
|
80
|
+
// polls so they don't lose progress.
|
|
81
|
+
let extractionsThisPoll = 0;
|
|
82
|
+
const MAX_EXTRACTIONS_PER_POLL = 1;
|
|
65
83
|
for (const file of files) {
|
|
84
|
+
// Stale-file skip: trajectory files untouched for STALE_TRAJECTORY_AGE_MS
|
|
85
|
+
// are likely abandoned sessions (old test runs, dead chats). Skip them
|
|
86
|
+
// entirely — don't burn LLM budget on extraction from stale content
|
|
87
|
+
// that the user has effectively forgotten about.
|
|
88
|
+
let mtimeMs = 0;
|
|
89
|
+
try {
|
|
90
|
+
mtimeMs = fs.statSync(file).mtimeMs;
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
if (Date.now() - mtimeMs > STALE_TRAJECTORY_AGE_MS) {
|
|
96
|
+
// Lazy-record offset so we don't repeatedly re-scan stale files —
|
|
97
|
+
// if the user later resumes this session, the offset is already
|
|
98
|
+
// current and we'll only extract net-new content.
|
|
99
|
+
if (!state[file]) {
|
|
100
|
+
try {
|
|
101
|
+
state[file] = { offset: fs.statSync(file).size, turnsAccum: 0 };
|
|
102
|
+
stateChanged = true;
|
|
103
|
+
}
|
|
104
|
+
catch { /* ignore */ }
|
|
105
|
+
}
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
66
108
|
const lastEntry = state[file] ?? { offset: 0, turnsAccum: 0 };
|
|
67
109
|
const { messages, newOffset } = parseNewMessages(file, lastEntry.offset);
|
|
68
110
|
if (newOffset === lastEntry.offset)
|
|
69
111
|
continue; // nothing new
|
|
70
112
|
const turnsAdded = countTurns(messages);
|
|
71
113
|
const turnsAccum = lastEntry.turnsAccum + turnsAdded;
|
|
72
|
-
const shouldExtract = turnsAccum >= extractInterval &&
|
|
114
|
+
const shouldExtract = turnsAccum >= extractInterval &&
|
|
115
|
+
messages.length >= 2 &&
|
|
116
|
+
extractionsThisPoll < MAX_EXTRACTIONS_PER_POLL;
|
|
73
117
|
if (shouldExtract) {
|
|
118
|
+
extractionsThisPoll++;
|
|
74
119
|
deps.logger.info(`extractd: ${path.basename(file)} -> ${turnsAccum}/${extractInterval} turns; running extraction (${messages.length} messages)`);
|
|
75
120
|
const existing = deps.isDedupEnabled() ? await deps.getDedupCandidates(20, messages) : [];
|
|
76
121
|
const rawFacts = await deps.runExtraction(messages, 'turn', existing, undefined);
|
package/fs-helpers.ts
CHANGED
|
@@ -1170,6 +1170,16 @@ export type OpenClawConfigPatchResult = 'patched' | 'unchanged' | 'skipped' | 'e
|
|
|
1170
1170
|
* this to "off" on first run for a clean UX. Existing explicit values
|
|
1171
1171
|
* ("partial", "block", "progress") are preserved.
|
|
1172
1172
|
*
|
|
1173
|
+
* 4. `plugins.bundledDiscovery = "compat"` (only if unset, and only when
|
|
1174
|
+
* `plugins.allow` is populated). When `plugins.allow` is a non-empty
|
|
1175
|
+
* array, OpenClaw 2026.5.x switches the loader into strict-allowlist
|
|
1176
|
+
* mode and silently rejects non-bundled plugins like totalreclaw —
|
|
1177
|
+
* EVEN IF they are listed in the allow array. Setting
|
|
1178
|
+
* `bundledDiscovery: "compat"` restores the looser behavior so allow-
|
|
1179
|
+
* listed non-bundled plugins load. Without this fix, users with
|
|
1180
|
+
* telegram or any model provider configured before TR install (which
|
|
1181
|
+
* populates allow) get a silent plugin-skip on every gateway boot.
|
|
1182
|
+
*
|
|
1173
1183
|
* Design constraints
|
|
1174
1184
|
* ------------------
|
|
1175
1185
|
* - SYNCHRONOUS — called during register() which must be sync.
|
|
@@ -1301,6 +1311,37 @@ export function patchOpenClawConfig(
|
|
|
1301
1311
|
}
|
|
1302
1312
|
}
|
|
1303
1313
|
|
|
1314
|
+
// --- Fix #4: plugins.bundledDiscovery = "compat" (3.3.11-rc.4) ---
|
|
1315
|
+
//
|
|
1316
|
+
// OpenClaw 2026.5.x: when `plugins.allow` is populated (any non-empty
|
|
1317
|
+
// array), the gateway's plugin loader switches into strict-allowlist
|
|
1318
|
+
// mode. In strict mode, NON-BUNDLED plugins like totalreclaw are
|
|
1319
|
+
// silently rejected even when listed in `plugins.allow`, unless
|
|
1320
|
+
// `plugins.bundledDiscovery = "compat"` is explicitly set. Pedro's
|
|
1321
|
+
// 2026-05-07 QA on pop-os surfaced this — the gateway booted with only
|
|
1322
|
+
// the bundled providers (telegram, device-pair) and skipped totalreclaw
|
|
1323
|
+
// despite it being in the allow list. `openclaw doctor --fix` cures it
|
|
1324
|
+
// by setting `bundledDiscovery: "compat"`, but users shouldn't need
|
|
1325
|
+
// to run doctor manually for the plugin to load.
|
|
1326
|
+
//
|
|
1327
|
+
// Fix: when `plugins.allow` is a non-empty array AND
|
|
1328
|
+
// `plugins.bundledDiscovery` is unset, set it to "compat". If the user
|
|
1329
|
+
// explicitly chose "allowlist" (the stricter mode), preserve their
|
|
1330
|
+
// choice — only first-run defaults are touched.
|
|
1331
|
+
//
|
|
1332
|
+
// This bug was missed by the auto-QA harness because the harness ran
|
|
1333
|
+
// on a fresh canonical container with `plugins.allow=null`, hitting
|
|
1334
|
+
// the auto-discover code path. Real users with telegram + a model
|
|
1335
|
+
// provider configured before TR install have a populated allow list,
|
|
1336
|
+
// hitting the strict-mode path. The auto-QA harness in 3.3.11-rc.4
|
|
1337
|
+
// adds a populated-allow scenario to catch future regressions.
|
|
1338
|
+
if (Array.isArray(cfg.plugins.allow) && cfg.plugins.allow.length > 0) {
|
|
1339
|
+
if (cfg.plugins.bundledDiscovery === undefined) {
|
|
1340
|
+
cfg.plugins.bundledDiscovery = 'compat';
|
|
1341
|
+
mutated = true;
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1304
1345
|
if (!mutated) return 'unchanged';
|
|
1305
1346
|
|
|
1306
1347
|
// Write back with 2-space indent to match OpenClaw's own write style.
|
package/index.ts
CHANGED
|
@@ -3156,7 +3156,7 @@ const plugin = {
|
|
|
3156
3156
|
api.logger.warn(
|
|
3157
3157
|
'TotalReclaw: updated openclaw.json with required 2026.5.x keys ' +
|
|
3158
3158
|
'(plugins.slots.memory + hooks.allowConversationAccess + ' +
|
|
3159
|
-
'channels.telegram.streaming.mode). ' +
|
|
3159
|
+
'channels.telegram.streaming.mode + plugins.bundledDiscovery). ' +
|
|
3160
3160
|
'Gateway restart required for the changes to take effect. ' +
|
|
3161
3161
|
'Run `/totalreclaw-restart` or restart the gateway manually.',
|
|
3162
3162
|
);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@totalreclaw/totalreclaw",
|
|
3
|
-
"version": "3.3.11-rc.
|
|
3
|
+
"version": "3.3.11-rc.5",
|
|
4
4
|
"description": "End-to-end encrypted, agent-portable memory for OpenClaw and any LLM-agent runtime. XChaCha20-Poly1305 with protobuf v4 + on-chain Memory Taxonomy v1 (claim / preference / directive / commitment / episode / summary).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"keywords": [
|
package/skill.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "totalreclaw",
|
|
3
|
-
"version": "3.3.11-rc.
|
|
3
|
+
"version": "3.3.11-rc.5",
|
|
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/tr-cli.ts
CHANGED
|
@@ -52,7 +52,7 @@ const STATE_PATH = CONFIG.onboardingStatePath;
|
|
|
52
52
|
// Auto-synced by skill/scripts/sync-version.mjs from skill/plugin/package.json::version.
|
|
53
53
|
// Do not edit by hand — running tests will catch drift but the publish workflow
|
|
54
54
|
// rewrites this constant at the start of every npm/ClawHub publish.
|
|
55
|
-
const PLUGIN_VERSION = '3.3.11-rc.
|
|
55
|
+
const PLUGIN_VERSION = '3.3.11-rc.5';
|
|
56
56
|
|
|
57
57
|
function die(msg: string, code = 1): never {
|
|
58
58
|
process.stderr.write(`tr: ${msg}\n`);
|
package/trajectory-poller.ts
CHANGED
|
@@ -127,6 +127,15 @@ export interface TrajectoryPollerHandle {
|
|
|
127
127
|
|
|
128
128
|
const DEFAULT_POLL_INTERVAL_MS = 60_000;
|
|
129
129
|
const STATE_FILE = path.join(os.homedir(), '.totalreclaw', 'extract-state.json');
|
|
130
|
+
/**
|
|
131
|
+
* Skip trajectory files older than this. A user who installs
|
|
132
|
+
* TotalReclaw on a host with months of OpenClaw session log history
|
|
133
|
+
* shouldn't get a retroactive extraction backlog — we only care about
|
|
134
|
+
* ongoing chat from now forward (3.3.11-rc.5). Files with mtime older
|
|
135
|
+
* than this threshold get a one-time offset snapshot so they're never
|
|
136
|
+
* re-scanned, and skip the extraction path entirely.
|
|
137
|
+
*/
|
|
138
|
+
const STALE_TRAJECTORY_AGE_MS = 7 * 24 * 60 * 60 * 1000; // 7 days
|
|
130
139
|
|
|
131
140
|
/**
|
|
132
141
|
* Start the trajectory poller. Runs an initial poll after 5 s, then
|
|
@@ -152,17 +161,54 @@ export function startTrajectoryPoller(
|
|
|
152
161
|
|
|
153
162
|
const extractInterval = deps.getExtractInterval();
|
|
154
163
|
let stateChanged = false;
|
|
164
|
+
// 3.3.11-rc.5: cap extractions per poll iteration. Pedro's
|
|
165
|
+
// 2026-05-07 zai 429 cascade was caused by N session files all
|
|
166
|
+
// crossing the extract threshold in the same poll → N back-to-back
|
|
167
|
+
// LLM calls trip the rate-limiter (especially on free tiers).
|
|
168
|
+
// With cap=1, extra files defer to the next poll iteration
|
|
169
|
+
// (60 s later by default). Their turnsAccum is preserved across
|
|
170
|
+
// polls so they don't lose progress.
|
|
171
|
+
let extractionsThisPoll = 0;
|
|
172
|
+
const MAX_EXTRACTIONS_PER_POLL = 1;
|
|
155
173
|
|
|
156
174
|
for (const file of files) {
|
|
175
|
+
// Stale-file skip: trajectory files untouched for STALE_TRAJECTORY_AGE_MS
|
|
176
|
+
// are likely abandoned sessions (old test runs, dead chats). Skip them
|
|
177
|
+
// entirely — don't burn LLM budget on extraction from stale content
|
|
178
|
+
// that the user has effectively forgotten about.
|
|
179
|
+
let mtimeMs = 0;
|
|
180
|
+
try {
|
|
181
|
+
mtimeMs = fs.statSync(file).mtimeMs;
|
|
182
|
+
} catch {
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
if (Date.now() - mtimeMs > STALE_TRAJECTORY_AGE_MS) {
|
|
186
|
+
// Lazy-record offset so we don't repeatedly re-scan stale files —
|
|
187
|
+
// if the user later resumes this session, the offset is already
|
|
188
|
+
// current and we'll only extract net-new content.
|
|
189
|
+
if (!state[file]) {
|
|
190
|
+
try {
|
|
191
|
+
state[file] = { offset: fs.statSync(file).size, turnsAccum: 0 };
|
|
192
|
+
stateChanged = true;
|
|
193
|
+
} catch { /* ignore */ }
|
|
194
|
+
}
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
|
|
157
198
|
const lastEntry = state[file] ?? { offset: 0, turnsAccum: 0 };
|
|
158
199
|
const { messages, newOffset } = parseNewMessages(file, lastEntry.offset);
|
|
159
200
|
if (newOffset === lastEntry.offset) continue; // nothing new
|
|
160
201
|
|
|
161
202
|
const turnsAdded = countTurns(messages);
|
|
162
203
|
const turnsAccum = lastEntry.turnsAccum + turnsAdded;
|
|
163
|
-
const shouldExtract =
|
|
204
|
+
const shouldExtract =
|
|
205
|
+
turnsAccum >= extractInterval &&
|
|
206
|
+
messages.length >= 2 &&
|
|
207
|
+
extractionsThisPoll < MAX_EXTRACTIONS_PER_POLL;
|
|
164
208
|
|
|
165
209
|
if (shouldExtract) {
|
|
210
|
+
extractionsThisPoll++;
|
|
211
|
+
|
|
166
212
|
deps.logger.info(
|
|
167
213
|
`extractd: ${path.basename(file)} -> ${turnsAccum}/${extractInterval} turns; running extraction (${messages.length} messages)`,
|
|
168
214
|
);
|