@totalreclaw/totalreclaw 3.3.0-rc.3 → 3.3.0-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 +207 -0
- package/index.ts +61 -45
- package/package.json +1 -1
- package/pair-session-store.ts +32 -2
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,213 @@ 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.0-rc.5] — 2026-04-20
|
|
8
|
+
|
|
9
|
+
Fifth release candidate for 3.3.0. Single ship-stopper fix for rc.4's
|
|
10
|
+
QR-pairing flow, root-caused by the auto-QA run against rc.4 artifacts
|
|
11
|
+
(report: `docs/notes/QA-plugin-3.3.0-rc.4-20260420-1517.md` in
|
|
12
|
+
`totalreclaw-internal`, thread at `totalreclaw-internal#21` comment
|
|
13
|
+
4281568050). rc.2 (scanner), rc.3 (auth literal path), and rc.4 (auth
|
|
14
|
+
`'plugin'` literal + `ensureSessionsFileDir` mkdir before lock) fixes are
|
|
15
|
+
all preserved. No protocol / on-chain changes vs 3.3.0.
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
|
|
19
|
+
- **`skill/plugin/index.ts` — register pair HTTP routes synchronously
|
|
20
|
+
(remove async IIFE)**. rc.2–rc.4 wrapped the 4 `api.registerHttpRoute`
|
|
21
|
+
calls in a fire-and-forget `(async () => { ... })()` block whose three
|
|
22
|
+
`await import(...)` calls (`./pair-http.js`, `@scure/bip39`, and
|
|
23
|
+
`@scure/bip39/wordlists/english.js`) settled one microtask AFTER the
|
|
24
|
+
SDK loader had already called `register()` and frozen the plugin's
|
|
25
|
+
HTTP-route registry. The 4 post-activation pushes landed on the
|
|
26
|
+
dispatcher's "inactive" copy and never reached the live router;
|
|
27
|
+
`openclaw plugins inspect totalreclaw --json | jq .httpRouteCount`
|
|
28
|
+
returned `0` on rc.4 despite both the `auth: 'plugin'` literal (rc.4)
|
|
29
|
+
and the `ensureSessionsFileDir` mkdir (rc.4) being correct. rc.5:
|
|
30
|
+
|
|
31
|
+
1. `buildPairRoutes`, `validateMnemonic`, and `wordlist` are now
|
|
32
|
+
**static top-of-file imports** (alongside the existing
|
|
33
|
+
`onboarding-cli.ts` / `generate-mnemonic.ts` static imports of the
|
|
34
|
+
same modules — no new deps, no circular-dep risk).
|
|
35
|
+
2. `writeOnboardingState` is added to the existing static
|
|
36
|
+
`./fs-helpers.js` import (it was the only dynamic import inside
|
|
37
|
+
the `completePairing` callback).
|
|
38
|
+
3. The async IIFE is deleted. `buildPairRoutes(...)` and the 4
|
|
39
|
+
`api.registerHttpRoute({...})` calls are now in the synchronous
|
|
40
|
+
body of `register(api)`, inside the existing
|
|
41
|
+
`if (typeof api.registerHttpRoute === 'function')` guard. The
|
|
42
|
+
`else` branch and warning are unchanged. The post-registration
|
|
43
|
+
info log now reads `'registered 4 QR-pairing HTTP routes
|
|
44
|
+
synchronously'` for clearer debug output.
|
|
45
|
+
4. `completePairing` remains `async` (it does disk I/O) — that is
|
|
46
|
+
fine because `registerHttpRoute` accepts async handlers. Only the
|
|
47
|
+
REGISTRATION had to be synchronous; the handler itself can
|
|
48
|
+
defer-to-microtask freely at runtime.
|
|
49
|
+
|
|
50
|
+
Scanner: static imports don't trigger any rule that dynamic imports
|
|
51
|
+
don't already trigger (verified via `node skill/scripts/check-scanner.mjs`,
|
|
52
|
+
0 flags, 72 files scanned).
|
|
53
|
+
|
|
54
|
+
**Before (rc.4):**
|
|
55
|
+
```ts
|
|
56
|
+
if (typeof api.registerHttpRoute === 'function') {
|
|
57
|
+
(async () => {
|
|
58
|
+
try {
|
|
59
|
+
const { buildPairRoutes } = await import('./pair-http.js');
|
|
60
|
+
const { validateMnemonic } = await import('@scure/bip39');
|
|
61
|
+
const { wordlist } = await import('@scure/bip39/wordlists/english.js');
|
|
62
|
+
const bundle = buildPairRoutes({ /* ... */ });
|
|
63
|
+
api.registerHttpRoute!({ path: bundle.finishPath, /*...*/, auth: 'plugin' });
|
|
64
|
+
api.registerHttpRoute!({ path: bundle.startPath, /*...*/, auth: 'plugin' });
|
|
65
|
+
api.registerHttpRoute!({ path: bundle.respondPath,/*...*/, auth: 'plugin' });
|
|
66
|
+
api.registerHttpRoute!({ path: bundle.statusPath, /*...*/, auth: 'plugin' });
|
|
67
|
+
// ^^ these 4 pushes happen AFTER register() has returned + the
|
|
68
|
+
// SDK loader has already activated the (empty) route registry.
|
|
69
|
+
} catch (err) { /* ... */ }
|
|
70
|
+
})();
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
**After (rc.5):**
|
|
75
|
+
```ts
|
|
76
|
+
// top of file
|
|
77
|
+
import { buildPairRoutes } from './pair-http.js';
|
|
78
|
+
import { validateMnemonic } from '@scure/bip39';
|
|
79
|
+
import { wordlist } from '@scure/bip39/wordlists/english.js';
|
|
80
|
+
// ... fs-helpers import now also includes writeOnboardingState
|
|
81
|
+
|
|
82
|
+
// inside register(api)
|
|
83
|
+
if (typeof api.registerHttpRoute === 'function') {
|
|
84
|
+
const bundle = buildPairRoutes({ /* ... */ });
|
|
85
|
+
api.registerHttpRoute!({ path: bundle.finishPath, /*...*/, auth: 'plugin' });
|
|
86
|
+
api.registerHttpRoute!({ path: bundle.startPath, /*...*/, auth: 'plugin' });
|
|
87
|
+
api.registerHttpRoute!({ path: bundle.respondPath, /*...*/, auth: 'plugin' });
|
|
88
|
+
api.registerHttpRoute!({ path: bundle.statusPath, /*...*/, auth: 'plugin' });
|
|
89
|
+
// ^^ these 4 pushes happen synchronously BEFORE register() returns,
|
|
90
|
+
// i.e. BEFORE the SDK loader activates the registry.
|
|
91
|
+
api.logger.info('TotalReclaw: registered 4 QR-pairing HTTP routes synchronously');
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
- **`skill/plugin/pair-http-route-registration.test.ts` — rc.5 regression
|
|
96
|
+
guard**. The existing SIMULATION suite (27 assertions covering the 4
|
|
97
|
+
routes' `auth` literal, path shape, handler type) is preserved. Added
|
|
98
|
+
a new SYNCHRONY suite (14 assertions) that invokes `plugin.register(mockApi)`
|
|
99
|
+
with a minimal mocked OpenClaw API and asserts `mockApi.registerHttpRoute`
|
|
100
|
+
has been called 4 times IMMEDIATELY after `register()` returns — no
|
|
101
|
+
`await`, no tick wait. This assertion would fail under the rc.4 async-IIFE
|
|
102
|
+
implementation and guards against any future refactor that re-introduces
|
|
103
|
+
an async boundary at the registration site. Total: 41/41 passing.
|
|
104
|
+
|
|
105
|
+
## [3.3.0-rc.4] — 2026-04-20
|
|
106
|
+
|
|
107
|
+
Fourth release candidate for 3.3.0. Two independent ship-stopper fixes for
|
|
108
|
+
rc.3's QR-pairing flow, both surfaced by the auto-QA run against rc.3
|
|
109
|
+
artifacts (report: `docs/notes/QA-plugin-3.3.0-rc.3-20260420-1440.md` in
|
|
110
|
+
`totalreclaw-internal`, thread at `totalreclaw-internal#21`). No protocol /
|
|
111
|
+
on-chain changes vs 3.3.0. Bundled into a single RC because shipping them
|
|
112
|
+
separately would require two more QA loops for what are, individually,
|
|
113
|
+
one-line fixes.
|
|
114
|
+
|
|
115
|
+
### Fixed
|
|
116
|
+
|
|
117
|
+
- **`skill/plugin/index.ts` — pair HTTP routes must use `auth: 'plugin'`, not
|
|
118
|
+
`'gateway'`** (lines 2750–2753, now 2760–2763 after added comment). rc.3
|
|
119
|
+
added `auth: 'gateway'` to the 4 `api.registerHttpRoute` calls, which the
|
|
120
|
+
SDK loader accepted as a legal value but whose runtime semantics are
|
|
121
|
+
"requires gateway bearer token" (see
|
|
122
|
+
`matchedPluginRoutesRequireGatewayAuth` at
|
|
123
|
+
`gateway-cli-CWpalJNJ.js:23186`). For the 4 pair routes — reached from a
|
|
124
|
+
phone/laptop browser with no bearer token — that means `/pair/*` is 401'd
|
|
125
|
+
at the plugin-auth stage before the handler ever runs. The second valid
|
|
126
|
+
literal, `auth: 'plugin'` (verified as the only other accepted value at
|
|
127
|
+
`loader-BkOjign1.js:662`), lets the plugin's handler run directly and
|
|
128
|
+
authenticate itself via the in-session sid + 6-digit secondaryCode +
|
|
129
|
+
single-use consumption + ECDH AEAD payload, which is the correct model
|
|
130
|
+
for QR-pair. QA observed `httpRouteCount: 0` in rc.3 via `plugins inspect`
|
|
131
|
+
and confirmed all 4 `/plugin/totalreclaw/pair/*` paths returned 404 / SPA
|
|
132
|
+
fallthrough. rc.4 switches all 4 to `auth: 'plugin'`.
|
|
133
|
+
|
|
134
|
+
**Before (rc.3):**
|
|
135
|
+
```ts
|
|
136
|
+
api.registerHttpRoute!({ path: bundle.finishPath, handler: bundle.handlers.finish, auth: 'gateway' });
|
|
137
|
+
api.registerHttpRoute!({ path: bundle.startPath, handler: bundle.handlers.start, auth: 'gateway' });
|
|
138
|
+
api.registerHttpRoute!({ path: bundle.respondPath, handler: bundle.handlers.respond, auth: 'gateway' });
|
|
139
|
+
api.registerHttpRoute!({ path: bundle.statusPath, handler: bundle.handlers.status, auth: 'gateway' });
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
**After (rc.4):**
|
|
143
|
+
```ts
|
|
144
|
+
api.registerHttpRoute!({ path: bundle.finishPath, handler: bundle.handlers.finish, auth: 'plugin' });
|
|
145
|
+
api.registerHttpRoute!({ path: bundle.startPath, handler: bundle.handlers.start, auth: 'plugin' });
|
|
146
|
+
api.registerHttpRoute!({ path: bundle.respondPath, handler: bundle.handlers.respond, auth: 'plugin' });
|
|
147
|
+
api.registerHttpRoute!({ path: bundle.statusPath, handler: bundle.handlers.status, auth: 'plugin' });
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
- **`skill/plugin/pair-session-store.ts::acquireSessionsFileLock` — mkdir
|
|
151
|
+
parent before `openSync(wx)`**. On a fresh install with no
|
|
152
|
+
`~/.totalreclaw/` directory, the lock's `openSync(path, 'wx')` returned
|
|
153
|
+
`ENOENT (No such file or directory)` and the retry loop misinterpreted
|
|
154
|
+
that as "lock already held", spinning at 50 ms intervals for the full
|
|
155
|
+
10 s `LOCK_WAIT_MS` before throwing `could not acquire lock`. The CLI
|
|
156
|
+
surfaced this as a hung `openclaw totalreclaw pair generate` with no QR,
|
|
157
|
+
URL, or secondary code ever rendered. `writePairSessionsFileSync`
|
|
158
|
+
already had a mkdir, but it was never reached because the lock never
|
|
159
|
+
acquired. rc.4 extracts a shared `ensureSessionsFileDir(sessionsPath)`
|
|
160
|
+
helper (mkdir `-p` with mode 0700) and calls it at the TOP of both
|
|
161
|
+
`acquireSessionsFileLock` AND `writePairSessionsFileSync` so the two
|
|
162
|
+
code paths can't drift. QA strace evidence in
|
|
163
|
+
`totalreclaw-internal#21`.
|
|
164
|
+
|
|
165
|
+
**Before (rc.3):**
|
|
166
|
+
```ts
|
|
167
|
+
async function acquireSessionsFileLock(sessionsPath) {
|
|
168
|
+
const lockPath = `${sessionsPath}.lock`;
|
|
169
|
+
// ...
|
|
170
|
+
while (true) {
|
|
171
|
+
try {
|
|
172
|
+
const fd = fs.openSync(lockPath, 'wx'); // ENOENT here on fresh install
|
|
173
|
+
// ...
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
**After (rc.4):**
|
|
177
|
+
```ts
|
|
178
|
+
function ensureSessionsFileDir(sessionsPath) {
|
|
179
|
+
const dir = path.dirname(sessionsPath);
|
|
180
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async function acquireSessionsFileLock(sessionsPath) {
|
|
184
|
+
ensureSessionsFileDir(sessionsPath); // NEW — guarantees parent dir
|
|
185
|
+
const lockPath = `${sessionsPath}.lock`;
|
|
186
|
+
// ...
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
### Added
|
|
190
|
+
|
|
191
|
+
- `skill/plugin/pair-session-store.test.ts` — two new blocks (§17, §18)
|
|
192
|
+
covering the fresh-install regression: `createPairSession` against a
|
|
193
|
+
path whose parent directory does NOT exist completes in < 2 s (was
|
|
194
|
+
10 s hang), materializes the missing dir with the correct mode, writes
|
|
195
|
+
the sessions file at 0600, and leaves no lock sentinel. Plus read-path
|
|
196
|
+
defensive tests: `getPairSession` / `listActivePairSessions` against
|
|
197
|
+
a missing dir return null / `[]` without throwing (previously would
|
|
198
|
+
have hit the same ENOENT hang).
|
|
199
|
+
- `skill/plugin/pair-http-route-registration.test.ts` — assertions
|
|
200
|
+
updated from `'gateway'` to `'plugin'`, plus a per-call regression
|
|
201
|
+
guard asserting `auth !== 'gateway'` so rc.3's value cannot sneak back
|
|
202
|
+
in. Test count: 23 → 27 assertions.
|
|
203
|
+
|
|
204
|
+
### Unchanged
|
|
205
|
+
|
|
206
|
+
No changes to: scanner-sim rules (still 0 flags), tarball contents (same
|
|
207
|
+
44 files; diff is content of 3 `.ts` files + `package.json` bump +
|
|
208
|
+
`CHANGELOG.md`), UX copy, terminology (`recovery phrase` throughout),
|
|
209
|
+
protobuf schema, Memory Taxonomy v1, on-chain contract surface, MCP
|
|
210
|
+
wiring, client integration, Hermes / NanoClaw / core (plugin-only RC).
|
|
211
|
+
|
|
212
|
+
---
|
|
213
|
+
|
|
7
214
|
## [3.3.0-rc.3] — 2026-04-20
|
|
8
215
|
|
|
9
216
|
Third release candidate for 3.3.0. Sole change vs rc.2: adds the mandatory
|
package/index.ts
CHANGED
|
@@ -133,10 +133,14 @@ import {
|
|
|
133
133
|
isRunningInDocker,
|
|
134
134
|
deleteFileIfExists,
|
|
135
135
|
resolveOnboardingState,
|
|
136
|
+
writeOnboardingState,
|
|
136
137
|
type OnboardingState,
|
|
137
138
|
} from './fs-helpers.js';
|
|
138
139
|
import { decideToolGate, isGatedToolName } from './tool-gating.js';
|
|
139
140
|
import { detectFirstRun, buildWelcomePrepend, type GatewayMode } from './first-run.js';
|
|
141
|
+
import { buildPairRoutes } from './pair-http.js';
|
|
142
|
+
import { validateMnemonic } from '@scure/bip39';
|
|
143
|
+
import { wordlist } from '@scure/bip39/wordlists/english.js';
|
|
140
144
|
import crypto from 'node:crypto';
|
|
141
145
|
|
|
142
146
|
// ---------------------------------------------------------------------------
|
|
@@ -2711,52 +2715,64 @@ const plugin = {
|
|
|
2711
2715
|
// encrypted mnemonic payload, and expose a status polled by the
|
|
2712
2716
|
// CLI. See pair-http.ts and the 2026-04-20 design doc.
|
|
2713
2717
|
if (typeof api.registerHttpRoute === 'function') {
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
2718
|
+
// rc.5 — the 4 `registerHttpRoute` calls MUST happen synchronously inside
|
|
2719
|
+
// `register(api)` because the SDK loader freezes the plugin's HTTP-route
|
|
2720
|
+
// registry as soon as `register()` returns. In rc.2–rc.4 this block was
|
|
2721
|
+
// wrapped in a fire-and-forget async IIFE whose `await import(...)`
|
|
2722
|
+
// settled one microtask AFTER the loader had already activated the
|
|
2723
|
+
// (empty) route list — the post-activation pushes landed on the
|
|
2724
|
+
// dispatcher's "inactive" copy and `openclaw plugins inspect
|
|
2725
|
+
// totalreclaw --json | jq .httpRouteCount` returned 0. See
|
|
2726
|
+
// `docs/notes/QA-plugin-3.3.0-rc.4-20260420-1517.md` (internal#21).
|
|
2727
|
+
// Moving `buildPairRoutes`, `@scure/bip39`, and `fs-helpers`
|
|
2728
|
+
// `writeOnboardingState` to static top-of-file imports keeps the
|
|
2729
|
+
// registration site synchronous and makes the call order deterministic.
|
|
2730
|
+
// `completePairing` remains async (it does disk I/O) — that is fine,
|
|
2731
|
+
// since `registerHttpRoute` accepts async handlers; only the
|
|
2732
|
+
// REGISTRATION must be synchronous.
|
|
2733
|
+
const bundle = buildPairRoutes({
|
|
2734
|
+
sessionsPath: CONFIG.pairSessionsPath,
|
|
2735
|
+
apiBase: '/plugin/totalreclaw/pair',
|
|
2736
|
+
logger: api.logger,
|
|
2737
|
+
validateMnemonic: (p) => validateMnemonic(p, wordlist),
|
|
2738
|
+
completePairing: async ({ mnemonic }) => {
|
|
2739
|
+
// Write credentials.json + flip state to 'active' via
|
|
2740
|
+
// fs-helpers. This centralizes disk I/O off the
|
|
2741
|
+
// pair-http surface (scanner isolation).
|
|
2742
|
+
const creds = loadCredentialsJson(CREDENTIALS_PATH) ?? {};
|
|
2743
|
+
const next = { ...creds, mnemonic };
|
|
2744
|
+
if (!writeCredentialsJson(CREDENTIALS_PATH, next)) {
|
|
2745
|
+
return { state: 'error', error: 'credentials_write_failed' };
|
|
2746
|
+
}
|
|
2747
|
+
// Hot-reload: update the runtime override so existing
|
|
2748
|
+
// in-memory state picks up the new phrase without a
|
|
2749
|
+
// process restart.
|
|
2750
|
+
setRecoveryPhraseOverride(mnemonic);
|
|
2751
|
+
// Flip onboarding state.
|
|
2752
|
+
writeOnboardingState(CONFIG.onboardingStatePath, {
|
|
2753
|
+
onboardingState: 'active',
|
|
2754
|
+
createdBy: 'generate',
|
|
2755
|
+
credentialsCreatedAt: new Date().toISOString(),
|
|
2756
|
+
version: '3.3.0',
|
|
2749
2757
|
});
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2758
|
+
return { state: 'active' };
|
|
2759
|
+
},
|
|
2760
|
+
});
|
|
2761
|
+
// auth: 'plugin' — the 4 pair routes are reached from the operator's
|
|
2762
|
+
// phone/laptop browser, which has no gateway bearer token. The plugin
|
|
2763
|
+
// authenticates each request itself via (a) the in-memory pair session
|
|
2764
|
+
// (sid + secondaryCode + single-use consumption), (b) ECDH + AEAD for
|
|
2765
|
+
// the encrypted mnemonic payload. See gateway-cli dist
|
|
2766
|
+
// `matchedPluginRoutesRequireGatewayAuth` / `enforcePluginRouteGatewayAuth`
|
|
2767
|
+
// — routes with `auth: 'gateway'` require a bearer token and 401 any
|
|
2768
|
+
// browser caller, which is the wrong semantic for QR-pair. rc.3
|
|
2769
|
+
// shipped `auth: 'gateway'` and the QA agent confirmed the routes
|
|
2770
|
+
// were unreachable from a browser (QA-plugin-3.3.0-rc.3 report).
|
|
2771
|
+
api.registerHttpRoute!({ path: bundle.finishPath, handler: bundle.handlers.finish, auth: 'plugin' });
|
|
2772
|
+
api.registerHttpRoute!({ path: bundle.startPath, handler: bundle.handlers.start, auth: 'plugin' });
|
|
2773
|
+
api.registerHttpRoute!({ path: bundle.respondPath, handler: bundle.handlers.respond, auth: 'plugin' });
|
|
2774
|
+
api.registerHttpRoute!({ path: bundle.statusPath, handler: bundle.handlers.status, auth: 'plugin' });
|
|
2775
|
+
api.logger.info('TotalReclaw: registered 4 QR-pairing HTTP routes synchronously');
|
|
2760
2776
|
} else {
|
|
2761
2777
|
api.logger.warn(
|
|
2762
2778
|
'api.registerHttpRoute is unavailable on this OpenClaw version — /totalreclaw pair will not work. ' +
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@totalreclaw/totalreclaw",
|
|
3
|
-
"version": "3.3.0-rc.
|
|
3
|
+
"version": "3.3.0-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/pair-session-store.ts
CHANGED
|
@@ -238,6 +238,25 @@ export const LOCK_WAIT_MS = 10_000;
|
|
|
238
238
|
/** Between-retry sleep. Short enough to feel responsive, long enough not to spin. */
|
|
239
239
|
export const LOCK_RETRY_MS = 50;
|
|
240
240
|
|
|
241
|
+
/**
|
|
242
|
+
* Ensure the parent directory of `sessionsPath` exists, creating it
|
|
243
|
+
* (and any missing intermediates) with 0700 mode. This is called from
|
|
244
|
+
* BOTH the lock-acquisition and the write paths — if we only create
|
|
245
|
+
* the dir on write, a fresh install hits ENOENT on the lock's
|
|
246
|
+
* `openSync(path, 'wx')` and spins the retry loop until deadline (rc.3
|
|
247
|
+
* regression: QA-plugin-3.3.0-rc.3 report).
|
|
248
|
+
*
|
|
249
|
+
* Best-effort: a mkdir failure is re-thrown to the caller, which will
|
|
250
|
+
* surface it via the lock-acquisition error path (for lock) or the
|
|
251
|
+
* try/catch in `writePairSessionsFileSync` (for write). 0700 mode
|
|
252
|
+
* matches the privacy posture of the sessions file (0600) — if the
|
|
253
|
+
* user can read the directory they can already read the file.
|
|
254
|
+
*/
|
|
255
|
+
function ensureSessionsFileDir(sessionsPath: string): void {
|
|
256
|
+
const dir = path.dirname(sessionsPath);
|
|
257
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
258
|
+
}
|
|
259
|
+
|
|
241
260
|
/**
|
|
242
261
|
* Acquire an exclusive lock on the given sessions-file path by
|
|
243
262
|
* atomically creating `<path>.lock` with `wx` mode. Retries up to
|
|
@@ -251,6 +270,16 @@ export const LOCK_RETRY_MS = 50;
|
|
|
251
270
|
* and self-contained is safer than fighting the scanner.
|
|
252
271
|
*/
|
|
253
272
|
async function acquireSessionsFileLock(sessionsPath: string): Promise<() => void> {
|
|
273
|
+
// Guarantee the parent directory exists BEFORE the first openSync(wx).
|
|
274
|
+
// Without this, a fresh install where `~/.totalreclaw/` doesn't yet
|
|
275
|
+
// exist gets ENOENT on every attempt, tight-loops until deadline, and
|
|
276
|
+
// throws "could not acquire lock" — which the CLI surfaces as a hung
|
|
277
|
+
// pair command with no QR / code / URL ever rendered (rc.3 blocker;
|
|
278
|
+
// QA-plugin-3.3.0-rc.3 strace evidence in totalreclaw-internal#21).
|
|
279
|
+
// `writePairSessionsFileSync` already creates the dir, but that path
|
|
280
|
+
// is never reached because the lock never acquires.
|
|
281
|
+
ensureSessionsFileDir(sessionsPath);
|
|
282
|
+
|
|
254
283
|
const lockPath = `${sessionsPath}.lock`;
|
|
255
284
|
const deadline = Date.now() + LOCK_WAIT_MS;
|
|
256
285
|
|
|
@@ -349,8 +378,9 @@ export function writePairSessionsFileSync(
|
|
|
349
378
|
file: PairSessionFile,
|
|
350
379
|
): boolean {
|
|
351
380
|
try {
|
|
352
|
-
|
|
353
|
-
|
|
381
|
+
// Same ensureSessionsFileDir used by acquireSessionsFileLock so the
|
|
382
|
+
// two paths can't drift.
|
|
383
|
+
ensureSessionsFileDir(sessionsPath);
|
|
354
384
|
const tmp = `${sessionsPath}.tmp-${process.pid}-${Date.now()}`;
|
|
355
385
|
fs.writeFileSync(tmp, JSON.stringify(file), { mode: 0o600 });
|
|
356
386
|
fs.renameSync(tmp, sessionsPath);
|