@totalreclaw/totalreclaw 3.3.11-rc.4 → 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 CHANGED
@@ -4,6 +4,27 @@ 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
+
7
28
  ## [3.3.11-rc.4] — 2026-05-07
8
29
 
9
30
  Fix #4 added to `patchOpenClawConfig()`: the **populated-allowlist plugin-skip bug** that broke every realistic install path.
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
4
+ version: 3.3.11-rc.5
5
5
  author: TotalReclaw Team
6
6
  license: MIT
7
7
  homepage: https://totalreclaw.xyz
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.4';
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 && messages.length >= 2;
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@totalreclaw/totalreclaw",
3
- "version": "3.3.11-rc.4",
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.4",
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.4';
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`);
@@ -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 = turnsAccum >= extractInterval && messages.length >= 2;
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
  );