@drakon-systems/multi-clawd 1.7.0 → 1.7.2
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/README.md +62 -3
- package/dist/health.js +28 -0
- package/dist/index.js +1 -1
- package/dist/update-core.js +19 -0
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/scripts/doctor.mjs +37 -1
package/README.md
CHANGED
|
@@ -153,7 +153,52 @@ npm i -g @drakon-systems/multi-clawd && multi-clawd update
|
|
|
153
153
|
```
|
|
154
154
|
|
|
155
155
|
`update` runs the registry install with the right flags, offers the gateway
|
|
156
|
-
restart, and finishes with a doctor health check.
|
|
156
|
+
restart, and finishes with a doctor health check.
|
|
157
|
+
|
|
158
|
+
> ### ⚠️ Keeping up to date: there are two halves
|
|
159
|
+
>
|
|
160
|
+
> One package installs as **two separate artifacts**, and they update by
|
|
161
|
+
> different routes:
|
|
162
|
+
>
|
|
163
|
+
> | Half | What it is | How it updates |
|
|
164
|
+
> |---|---|---|
|
|
165
|
+
> | **The plugin** | serves your turns — pooling, rotation, credentials | `multi-clawd update` |
|
|
166
|
+
> | **The CLI** | the `multi-clawd` command — `doctor`, `chain`, `setup`, `explain` | `npm i -g @drakon-systems/multi-clawd@latest` |
|
|
167
|
+
>
|
|
168
|
+
> **`multi-clawd update` upgrades the plugin, not itself.** Since v1.6.0 it
|
|
169
|
+
> notices when the command has fallen behind and offers to update it — but
|
|
170
|
+
> that notice ships *in the command*, so a CLI older than v1.6.0 has no code
|
|
171
|
+
> to warn you with. We found this on three of our own machines, all running
|
|
172
|
+
> current plugins behind commands that were three versions stale.
|
|
173
|
+
>
|
|
174
|
+
> **So run the global install once, by hand, to arm it:**
|
|
175
|
+
>
|
|
176
|
+
> ```bash
|
|
177
|
+
> npm i -g @drakon-systems/multi-clawd@latest
|
|
178
|
+
> ```
|
|
179
|
+
>
|
|
180
|
+
> After that it maintains itself. It matters because `doctor` and `chain` live
|
|
181
|
+
> in the CLI — a stale command reports *stale diagnostics about a current
|
|
182
|
+
> plugin*, which is a confusing way to be told nothing is wrong. `multi-clawd
|
|
183
|
+
> version` shows both halves, and since v1.6.0 says plainly whether the pair
|
|
184
|
+
> is a problem.
|
|
185
|
+
>
|
|
186
|
+
> ### ⚠️ `openclaw plugins update --all` cannot move this plugin
|
|
187
|
+
>
|
|
188
|
+
> It will tell you the plugin is up to date when it is not. We install with
|
|
189
|
+
> `--pin` (OpenClaw's own security audit raises a HIGH finding for unpinned
|
|
190
|
+
> install specs, so pinning is the right side of that trade), and OpenClaw
|
|
191
|
+
> resolves registry metadata *for the pinned spec* — comparing 1.6.0 against
|
|
192
|
+
> 1.6.0 and returning "up to date" while a newer version sits on npm. The
|
|
193
|
+
> honest "pinned to X; registry default resolves to Y" message exists in
|
|
194
|
+
> OpenClaw but is built inside its `--dry-run` branch, so a real update run
|
|
195
|
+
> never prints it.
|
|
196
|
+
>
|
|
197
|
+
> **Use `multi-clawd update`.** Since v1.7.2 `doctor` also checks the registry
|
|
198
|
+
> itself (cached, and silent when offline), so a lagging plugin is reported
|
|
199
|
+
> rather than left to a command that reassures you wrongly.
|
|
200
|
+
|
|
201
|
+
Prefer the raw form?
|
|
157
202
|
|
|
158
203
|
```bash
|
|
159
204
|
openclaw plugins install @drakon-systems/multi-clawd --pin
|
|
@@ -189,7 +234,7 @@ openclaw plugins install (Get-Location).Path
|
|
|
189
234
|
**Or let your agent install it.** Running an OpenClaw assistant or Claude
|
|
190
235
|
Code on the target machine already? Paste it this and go make coffee:
|
|
191
236
|
|
|
192
|
-
> Read https://raw.githubusercontent.com/Drakon-Systems-Ltd/multi-clawd/v1.7.
|
|
237
|
+
> Read https://raw.githubusercontent.com/Drakon-Systems-Ltd/multi-clawd/v1.7.2/SETUP-AGENT.md
|
|
193
238
|
> and follow it to set up multi-clawd on this machine. I own a second
|
|
194
239
|
> Claude account — ask me when you need me to log in.
|
|
195
240
|
|
|
@@ -375,11 +420,25 @@ How it decides, per launch (all data from each account's live
|
|
|
375
420
|
| Account state | Effect |
|
|
376
421
|
|---|---|
|
|
377
422
|
| `rejected` + reset in the future | skipped until `resetsAt` passes |
|
|
423
|
+
| `rejected` with no reset, on a named window | skipped for an hour, then re-probed (v1.7.2) |
|
|
424
|
+
| `rejected` on the `unknown` window | still used — that is where a single-model limit lands, and it must not strand the account |
|
|
378
425
|
| any window utilization ≥ threshold | skipped (nearly maxed — the point of the pool) |
|
|
379
|
-
| `allowed_warning`
|
|
426
|
+
| `allowed_warning` with a number below threshold | still used — trust the number |
|
|
427
|
+
| `allowed_warning` and **no** number, on a short window | skipped (v1.7.2) — see below |
|
|
428
|
+
| `allowed_warning` and no number, on the weekly window | still used — weekly warns from ~30% and would flap |
|
|
380
429
|
| no data / stale data | used — never rotate on missing evidence |
|
|
381
430
|
| whole pool exhausted | home account anyway → real limit error → your chain drops provider |
|
|
382
431
|
|
|
432
|
+
**Why the short-window rule exists (v1.7.2).** The threshold rule needs a
|
|
433
|
+
utilization number, and Anthropic does not send one for the 5-hour session
|
|
434
|
+
window — across every observation we hold from two accounts it arrives as a
|
|
435
|
+
bare status plus a reset time, while the weekly windows carry percentages. A
|
|
436
|
+
rule that waits for a number therefore could never pre-empt the session
|
|
437
|
+
limit: the pool would take the hit and rotate afterwards. So on hour-scoped
|
|
438
|
+
windows the warning counts on its own. It stays narrow deliberately — a
|
|
439
|
+
reported number always wins over the status, and a warning from a window that
|
|
440
|
+
has since reset is void, exactly like a stale percentage.
|
|
441
|
+
|
|
383
442
|
Notes:
|
|
384
443
|
|
|
385
444
|
- Rotation happens at limit boundaries only. A mid-conversation handover
|
package/dist/health.js
CHANGED
|
@@ -3,6 +3,17 @@ const DEFAULT_UTILIZATION_THRESHOLD = 0.85;
|
|
|
3
3
|
const DEFAULT_STALE_AFTER_MS = 6 * 60 * 60 * 1000;
|
|
4
4
|
export const MODEL_REJECTED_TTL_MS = 60 * 60 * 1000;
|
|
5
5
|
const MODEL_WINDOW_PREFIX = "model:";
|
|
6
|
+
const SHORT_WINDOW_PATTERN = /(^|_)hours?(_|$)/;
|
|
7
|
+
export function isShortWindow(window) {
|
|
8
|
+
return SHORT_WINDOW_PATTERN.test(window);
|
|
9
|
+
}
|
|
10
|
+
const PERIOD_WINDOW_PATTERN = /(^|_)(minutes?|hours?|days?|weeks?|months?)(_|$)/;
|
|
11
|
+
export function isPeriodWindow(window) {
|
|
12
|
+
return PERIOD_WINDOW_PATTERN.test(window);
|
|
13
|
+
}
|
|
14
|
+
export function isWarningStatus(status) {
|
|
15
|
+
return /warning/i.test(status);
|
|
16
|
+
}
|
|
6
17
|
export const MAX_RESET_HORIZON_MS = 8 * 24 * 60 * 60 * 1000;
|
|
7
18
|
export function classifyAccountHealth(state, options, nowMs, requestedModel) {
|
|
8
19
|
const staleAfterMs = options.staleAfterMs ?? DEFAULT_STALE_AFTER_MS;
|
|
@@ -58,6 +69,13 @@ export function classifyAccountHealth(state, options, nowMs, requestedModel) {
|
|
|
58
69
|
reason: `${window} rejected until ${new Date(resetMs).toISOString()}`,
|
|
59
70
|
};
|
|
60
71
|
}
|
|
72
|
+
if (w.status === "rejected" && resetMs === undefined && isPeriodWindow(window)) {
|
|
73
|
+
return {
|
|
74
|
+
verdict: "exhausted",
|
|
75
|
+
resumeAt: w.seenAt + MODEL_REJECTED_TTL_MS,
|
|
76
|
+
reason: `${window} rejected ${Math.round((nowMs - w.seenAt) / 60000)}m ago (no reset time; TTL block)`,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
61
79
|
if (worst.verdict === "ok" &&
|
|
62
80
|
typeof w.utilization === "number" &&
|
|
63
81
|
w.utilization >= threshold &&
|
|
@@ -67,6 +85,16 @@ export function classifyAccountHealth(state, options, nowMs, requestedModel) {
|
|
|
67
85
|
reason: `${window} utilization ${w.utilization} >= ${threshold}`,
|
|
68
86
|
};
|
|
69
87
|
}
|
|
88
|
+
if (worst.verdict === "ok" &&
|
|
89
|
+
typeof w.utilization !== "number" &&
|
|
90
|
+
isShortWindow(window) &&
|
|
91
|
+
isWarningStatus(w.status) &&
|
|
92
|
+
(resetBearing || resetMs === undefined)) {
|
|
93
|
+
worst = {
|
|
94
|
+
verdict: "near_limit",
|
|
95
|
+
reason: `${window} reported ${w.status} with no utilization — short window, treated as near-limit`,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
70
98
|
}
|
|
71
99
|
if (worst.verdict === "ok" && !hasLiveEvidence)
|
|
72
100
|
return { verdict: "no_data" };
|
package/dist/index.js
CHANGED
|
@@ -471,7 +471,7 @@ function writeStickyEntry(file, entry, logger) {
|
|
|
471
471
|
logger.warn(`[multi-clawd] sticky state write failed: ${String(err)}`);
|
|
472
472
|
}
|
|
473
473
|
}
|
|
474
|
-
function registerPoolBackend(api, pool, accounts, registeredIds, execMode) {
|
|
474
|
+
export function registerPoolBackend(api, pool, accounts, registeredIds, execMode) {
|
|
475
475
|
const logger = api.logger;
|
|
476
476
|
if (!pool)
|
|
477
477
|
return;
|
package/dist/update-core.js
CHANGED
|
@@ -57,6 +57,25 @@ export function formatCliSkew(opts) {
|
|
|
57
57
|
`the plugin serving your turns is behind. Fix: multi-clawd update`);
|
|
58
58
|
}
|
|
59
59
|
}
|
|
60
|
+
export const REGISTRY_CHECK_TTL_MS = 6 * 60 * 60 * 1000;
|
|
61
|
+
export function registryCacheIsFresh(checkedAt, nowMs, ttlMs = REGISTRY_CHECK_TTL_MS) {
|
|
62
|
+
if (typeof checkedAt !== "number")
|
|
63
|
+
return false;
|
|
64
|
+
return checkedAt <= nowMs && nowMs - checkedAt < ttlMs;
|
|
65
|
+
}
|
|
66
|
+
export function formatRegistryLag(opts) {
|
|
67
|
+
if (!opts.installed || !opts.latest)
|
|
68
|
+
return undefined;
|
|
69
|
+
if (compareVersions(opts.installed, opts.latest) >= 0) {
|
|
70
|
+
return { level: "ok", text: `plugin v${opts.installed} is the latest published version` };
|
|
71
|
+
}
|
|
72
|
+
return {
|
|
73
|
+
level: "warn",
|
|
74
|
+
text: `plugin v${opts.installed} — npm publishes v${opts.latest}. The install is pinned, so ` +
|
|
75
|
+
`\`openclaw plugins update --all\` reports it up to date and will not move it. ` +
|
|
76
|
+
`Fix: multi-clawd update`,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
60
79
|
export function formatUpdateBanner(opts) {
|
|
61
80
|
const action = decideUpdateAction(opts);
|
|
62
81
|
switch (action) {
|
package/openclaw.plugin.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "multi-clawd",
|
|
3
3
|
"name": "multi-clawd",
|
|
4
|
-
"version": "1.7.
|
|
4
|
+
"version": "1.7.2",
|
|
5
5
|
"description": "Register additional Claude Code logins (Max/Pro accounts) as first-class OpenClaw CLI backends for cross-account failover, keeping the full skills/MCP harness on every account.",
|
|
6
6
|
"cliBackends": [
|
|
7
7
|
"claw1",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@drakon-systems/multi-clawd",
|
|
3
|
-
"version": "1.7.
|
|
3
|
+
"version": "1.7.2",
|
|
4
4
|
"description": "Multi-account Claude Code failover for OpenClaw — register additional Claude (Max/Pro) logins as first-class CLI backends and keep the full skills/MCP harness across every account.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
package/scripts/doctor.mjs
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
* Exit code: 0 all good, 1 any ❌.
|
|
21
21
|
*/
|
|
22
22
|
import { execFileSync } from "node:child_process";
|
|
23
|
-
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
23
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
|
|
24
24
|
import { homedir } from "node:os";
|
|
25
25
|
import { dirname, join, resolve } from "node:path";
|
|
26
26
|
import { fileURLToPath } from "node:url";
|
|
@@ -138,6 +138,42 @@ else ok(`installed at ${EXT_DIR}`);
|
|
|
138
138
|
: undefined;
|
|
139
139
|
if (note) warn(note);
|
|
140
140
|
else if (cliVer && pluginVer) ok(`CLI and plugin both v${cliVer}`);
|
|
141
|
+
|
|
142
|
+
// Is the installed plugin behind what npm publishes? Nothing else asks:
|
|
143
|
+
// `openclaw plugins update --all` compares against the PINNED spec and so
|
|
144
|
+
// always reports a pinned plugin up to date (found 1 Aug 2026), and doctor
|
|
145
|
+
// did no registry lookup at all. Cached, short-timeout, and silent when the
|
|
146
|
+
// registry cannot be reached — an offline box has no finding to report.
|
|
147
|
+
if (uc && pluginVer && typeof uc.formatRegistryLag === "function") {
|
|
148
|
+
const cacheFile = join(STATE_DIR, "registry-check.json");
|
|
149
|
+
const cached = readJson(cacheFile);
|
|
150
|
+
let latest = uc.registryCacheIsFresh(cached?.checkedAt, Date.now())
|
|
151
|
+
? cached?.latest
|
|
152
|
+
: undefined;
|
|
153
|
+
if (!latest) {
|
|
154
|
+
try {
|
|
155
|
+
latest = execFileSync("npm", ["view", "@drakon-systems/multi-clawd", "version"], {
|
|
156
|
+
encoding: "utf8",
|
|
157
|
+
timeout: 8000,
|
|
158
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
159
|
+
}).trim();
|
|
160
|
+
try {
|
|
161
|
+
mkdirSync(STATE_DIR, { recursive: true });
|
|
162
|
+
writeFileSync(
|
|
163
|
+
cacheFile,
|
|
164
|
+
JSON.stringify({ latest, checkedAt: Date.now() }, null, 2) + "\n",
|
|
165
|
+
);
|
|
166
|
+
} catch {
|
|
167
|
+
/* cache is an optimisation, never a requirement */
|
|
168
|
+
}
|
|
169
|
+
} catch {
|
|
170
|
+
latest = undefined; // offline / registry down — stay silent
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
const lag = uc.formatRegistryLag({ installed: pluginVer, latest });
|
|
174
|
+
if (lag?.level === "warn") warn(lag.text);
|
|
175
|
+
else if (lag?.level === "ok") ok(lag.text);
|
|
176
|
+
}
|
|
141
177
|
}
|
|
142
178
|
if (!entry) bad("no plugins.entries[\"multi-clawd\"] in openclaw.json");
|
|
143
179
|
else if (entry.enabled !== true) bad("plugin entry present but not enabled");
|