@drakon-systems/multi-clawd 2.2.0 → 2.3.0
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 +12 -2
- package/dist/direct-report.js +13 -6
- package/dist/direct-route.js +6 -2
- package/dist/direct-sync.js +15 -7
- package/dist/explain-core.js +46 -5
- package/dist/health.js +23 -3
- package/dist/index.js +7 -1
- package/dist/sticky.js +11 -1
- package/openclaw.plugin.json +7 -2
- package/package.json +1 -1
- package/scripts/cli.mjs +16 -3
- package/scripts/doctor.mjs +9 -6
package/README.md
CHANGED
|
@@ -386,7 +386,7 @@ openclaw plugins install (Get-Location).Path
|
|
|
386
386
|
**Or let your agent install it.** Running an OpenClaw assistant or Claude
|
|
387
387
|
Code on the target machine already? Paste it this and go make coffee:
|
|
388
388
|
|
|
389
|
-
> Read https://raw.githubusercontent.com/Drakon-Systems-Ltd/multi-clawd/v2.
|
|
389
|
+
> Read https://raw.githubusercontent.com/Drakon-Systems-Ltd/multi-clawd/v2.3.0/SETUP-AGENT.md
|
|
390
390
|
> and follow it to set up multi-clawd on this machine. I own a second
|
|
391
391
|
> Claude account — ask me when you need me to log in.
|
|
392
392
|
|
|
@@ -553,7 +553,8 @@ error.
|
|
|
553
553
|
],
|
|
554
554
|
"pool": {
|
|
555
555
|
"id": "clawd",
|
|
556
|
-
"accounts": ["claw1", "claw2"], // preference order; first = home
|
|
556
|
+
"accounts": ["claw1", "claw2"], // preference order; first = home for ordered strategy
|
|
557
|
+
"strategy": "ordered", // optional: "reset-aware" drains healthy weekly quota that resets soonest
|
|
557
558
|
"utilizationThreshold": 0.85, // default hand-over; minute/hour and overage windows use this
|
|
558
559
|
"weeklyUtilizationThreshold": 0.97, // optional: day/week/month quota windows at 97%
|
|
559
560
|
"minDwellMs": 600000, // v0.3: anti-flap dwell before returning home
|
|
@@ -577,6 +578,15 @@ error.
|
|
|
577
578
|
} }
|
|
578
579
|
```
|
|
579
580
|
|
|
581
|
+
`pool.strategy` defaults to `"ordered"`, which retains the configured account
|
|
582
|
+
order. Set it to `"reset-aware"` to use healthy weekly quota before its
|
|
583
|
+
`seven_day` window resets: among `ok` accounts with live seven-day reset data,
|
|
584
|
+
the soonest reset wins. Accounts without that data follow in pool order;
|
|
585
|
+
near-limit or exhausted accounts never outrank a healthy one. Equal reset
|
|
586
|
+
times keep pool order. The existing `minDwellMs` delay still holds an account
|
|
587
|
+
that remains usable. Both CLI launches and the direct Anthropic route use the
|
|
588
|
+
same preference. Other values are rejected by the plugin config schema.
|
|
589
|
+
|
|
580
590
|
How it decides, per launch (all data from each account's live
|
|
581
591
|
`rate_limit_event` stream, captured by the shim):
|
|
582
592
|
|
package/dist/direct-report.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { classifyAccountHealth } from "./health.js";
|
|
1
|
+
import { classifyAccountHealth, sevenDayReset } from "./health.js";
|
|
2
2
|
import { DIRECT_PROVIDER, collectDirectMembers, } from "./direct-route.js";
|
|
3
3
|
import { parseUnusableProfiles, planAgentOrder, readDirectRouteSnapshot, safeCliError, } from "./direct-sync.js";
|
|
4
4
|
export function describeDirectSource(member) {
|
|
@@ -19,11 +19,17 @@ export async function gatherDirectStatus(params) {
|
|
|
19
19
|
if (members.length === 0 && problems.length === 0)
|
|
20
20
|
return undefined;
|
|
21
21
|
const errors = [];
|
|
22
|
-
const verdicts = members.map((m) =>
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
22
|
+
const verdicts = members.map((m) => {
|
|
23
|
+
const state = params.readHealth(m.accountId);
|
|
24
|
+
return {
|
|
25
|
+
accountId: m.accountId,
|
|
26
|
+
profileId: m.profileId,
|
|
27
|
+
verdict: classifyAccountHealth(state, params.healthOptions, params.nowMs).verdict,
|
|
28
|
+
sevenDayResetsAt: params.strategy === "reset-aware"
|
|
29
|
+
? sevenDayReset(state, params.healthOptions, params.nowMs)
|
|
30
|
+
: undefined,
|
|
31
|
+
};
|
|
32
|
+
});
|
|
27
33
|
const read = members.length > 0 ? await readDirectRouteSnapshot(params.runner, params.agentId) : {};
|
|
28
34
|
if ("error" in read && read.error)
|
|
29
35
|
errors.push(read.error);
|
|
@@ -46,6 +52,7 @@ export async function gatherDirectStatus(params) {
|
|
|
46
52
|
sticky: params.sticky,
|
|
47
53
|
nowMs: params.nowMs,
|
|
48
54
|
minDwellMs: params.minDwellMs,
|
|
55
|
+
strategy: params.strategy,
|
|
49
56
|
}).order;
|
|
50
57
|
}
|
|
51
58
|
const byProfile = new Map(unusable.map((u) => [u.profileId, u]));
|
package/dist/direct-route.js
CHANGED
|
@@ -132,16 +132,20 @@ export function planDirectOrder(params) {
|
|
|
132
132
|
if (members.length === 0)
|
|
133
133
|
return undefined;
|
|
134
134
|
const decision = decideStickySelection({
|
|
135
|
-
verdicts: members.map((m) => ({ id: m.accountId, verdict: m.verdict })),
|
|
135
|
+
verdicts: members.map((m) => ({ id: m.accountId, verdict: m.verdict, sevenDayResetsAt: m.sevenDayResetsAt })),
|
|
136
136
|
sticky: params.sticky,
|
|
137
137
|
nowMs: params.nowMs,
|
|
138
138
|
minDwellMs: params.minDwellMs,
|
|
139
|
+
strategy: params.strategy,
|
|
139
140
|
});
|
|
140
141
|
const first = members.find((m) => m.accountId === decision.account) ?? members[0];
|
|
141
142
|
const rest = members
|
|
142
143
|
.filter((m) => m !== first)
|
|
143
144
|
.map((m, index) => ({ m, index }))
|
|
144
|
-
.sort((a, b) => VERDICT_RANK[a.m.verdict] - VERDICT_RANK[b.m.verdict] ||
|
|
145
|
+
.sort((a, b) => VERDICT_RANK[a.m.verdict] - VERDICT_RANK[b.m.verdict] ||
|
|
146
|
+
(params.strategy === "reset-aware" && a.m.verdict === "ok"
|
|
147
|
+
? (a.m.sevenDayResetsAt ?? Infinity) - (b.m.sevenDayResetsAt ?? Infinity)
|
|
148
|
+
: 0) || a.index - b.index)
|
|
145
149
|
.map(({ m }) => m.profileId);
|
|
146
150
|
const managed = new Set(members.map((m) => m.profileId));
|
|
147
151
|
const unmanaged = (params.currentOrder ?? []).filter((id, i, all) => typeof id === "string" && !managed.has(id) && all.indexOf(id) === i);
|
package/dist/direct-sync.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { classifyAccountHealth } from "./health.js";
|
|
1
|
+
import { classifyAccountHealth, sevenDayReset } from "./health.js";
|
|
2
2
|
import { DIRECT_PROVIDER, planDirectOrder } from "./direct-route.js";
|
|
3
3
|
import { parseClaudeSetupToken } from "./hermes-core.js";
|
|
4
4
|
export function safeCliError(result) {
|
|
@@ -224,6 +224,7 @@ export function planAgentOrder(params) {
|
|
|
224
224
|
sticky: params.sticky,
|
|
225
225
|
nowMs: params.nowMs,
|
|
226
226
|
minDwellMs: params.minDwellMs,
|
|
227
|
+
strategy: params.strategy,
|
|
227
228
|
});
|
|
228
229
|
if (!plan)
|
|
229
230
|
return { missing };
|
|
@@ -244,13 +245,19 @@ export function createDirectOrderController(deps) {
|
|
|
244
245
|
return { managedOrder: [], agents: [] };
|
|
245
246
|
running = true;
|
|
246
247
|
try {
|
|
247
|
-
const verdicts = deps.members.map((m) =>
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
248
|
+
const verdicts = deps.members.map((m) => {
|
|
249
|
+
const state = deps.readHealth(m.accountId);
|
|
250
|
+
return {
|
|
251
|
+
accountId: m.accountId,
|
|
252
|
+
profileId: m.profileId,
|
|
253
|
+
verdict: classifyAccountHealth(state, deps.healthOptions, nowMs).verdict,
|
|
254
|
+
sevenDayResetsAt: deps.strategy === "reset-aware"
|
|
255
|
+
? sevenDayReset(state, deps.healthOptions, nowMs)
|
|
256
|
+
: undefined,
|
|
257
|
+
};
|
|
258
|
+
});
|
|
252
259
|
const sticky = deps.readSticky();
|
|
253
|
-
const managed = planDirectOrder({ members: verdicts, sticky, nowMs, minDwellMs: deps.minDwellMs });
|
|
260
|
+
const managed = planDirectOrder({ members: verdicts, sticky, nowMs, minDwellMs: deps.minDwellMs, strategy: deps.strategy });
|
|
254
261
|
if (!managed)
|
|
255
262
|
return { managedOrder: [], agents: [] };
|
|
256
263
|
if (JSON.stringify(managed.sticky) !== JSON.stringify(sticky))
|
|
@@ -285,6 +292,7 @@ export function createDirectOrderController(deps) {
|
|
|
285
292
|
sticky,
|
|
286
293
|
nowMs,
|
|
287
294
|
minDwellMs: deps.minDwellMs,
|
|
295
|
+
strategy: deps.strategy,
|
|
288
296
|
});
|
|
289
297
|
for (const id of plan.missing) {
|
|
290
298
|
const k = `${agentId}:${id}`;
|
package/dist/explain-core.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { DEFAULT_UTILIZATION_THRESHOLD } from "./health.js";
|
|
1
|
+
import { DEFAULT_UTILIZATION_THRESHOLD, choosePoolAccount, poolStrategy } from "./health.js";
|
|
2
|
+
import { decideStickySelection } from "./sticky.js";
|
|
2
3
|
const WINDOW_LABELS = {
|
|
3
4
|
five_hour: "5-hour",
|
|
4
5
|
seven_day: "weekly",
|
|
@@ -88,9 +89,15 @@ export function renderExplanation(model) {
|
|
|
88
89
|
const generalThreshold = model.pool.utilizationThreshold ?? DEFAULT_UTILIZATION_THRESHOLD;
|
|
89
90
|
const weeklyThreshold = model.pool.weeklyUtilizationThreshold ?? generalThreshold;
|
|
90
91
|
lines.push(`POOL ${model.pool.id} (${model.pool.accounts.join(" → ")})`);
|
|
91
|
-
lines.push(`
|
|
92
|
-
|
|
93
|
-
|
|
92
|
+
lines.push(` Strategy: ${poolStrategy(model.pool.strategy)}.`);
|
|
93
|
+
if (poolStrategy(model.pool.strategy) === "reset-aware") {
|
|
94
|
+
lines.push(" Healthy accounts with a live seven-day reset are preferred by soonest reset.");
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
lines.push(" Every Claude launch runs on the first account below its threshold;");
|
|
98
|
+
lines.push(" the home account reclaims work");
|
|
99
|
+
lines.push(" automatically once its window resets.");
|
|
100
|
+
}
|
|
94
101
|
lines.push(` Thresholds: general ${Math.round(generalThreshold * 100)}%, weekly ${Math.round(weeklyThreshold * 100)}%${model.pool.weeklyUtilizationThreshold === undefined ? " (follows general)" : ""}.`);
|
|
95
102
|
lines.push(` The weekly threshold applies to day/week/month quota windows; overage uses the general threshold.`);
|
|
96
103
|
const ladder = model.pool.degrade?.ladder ?? [];
|
|
@@ -130,8 +137,42 @@ export function renderExplanation(model) {
|
|
|
130
137
|
lines.push(` usage: ${h.usage?.length ? renderUsageLine(h.usage, nowMs) : "no live telemetry"}`);
|
|
131
138
|
}
|
|
132
139
|
if (model.pool) {
|
|
140
|
+
const strategy = poolStrategy(model.pool.strategy);
|
|
141
|
+
const verdicts = model.pool.accounts.map((id) => {
|
|
142
|
+
const h = model.health.find((row) => row.id === id);
|
|
143
|
+
return {
|
|
144
|
+
id,
|
|
145
|
+
verdict: h?.verdict ?? "no_data",
|
|
146
|
+
sevenDayResetsAt: (h?.usage?.find((u) => u.window === "usage:seven_day" && u.resetsAt !== undefined && u.resetsAt > nowMs)
|
|
147
|
+
?? h?.usage?.find((u) => u.window === "seven_day" && u.resetsAt !== undefined && u.resetsAt > nowMs))?.resetsAt,
|
|
148
|
+
};
|
|
149
|
+
});
|
|
150
|
+
if (verdicts.length) {
|
|
151
|
+
const preferred = choosePoolAccount(verdicts, strategy);
|
|
152
|
+
const decision = decideStickySelection({ verdicts, sticky: model.stickyEntry, nowMs, minDwellMs: model.pool.minDwellMs, strategy });
|
|
153
|
+
const picked = verdicts.find((v) => v.id === decision.account);
|
|
154
|
+
let reason;
|
|
155
|
+
if (preferred && decision.account !== preferred)
|
|
156
|
+
reason = "sticky dwell keeps this usable account";
|
|
157
|
+
else if (strategy === "ordered")
|
|
158
|
+
reason = picked?.verdict === "near_limit" ? "first near-limit member in pool order" : "first healthy member in pool order";
|
|
159
|
+
else if (picked?.verdict === "ok" && picked.sevenDayResetsAt !== undefined)
|
|
160
|
+
reason = `seven-day window resets soonest (in ${relativeUntil(picked.sevenDayResetsAt, nowMs)})`;
|
|
161
|
+
else if (picked?.verdict === "ok")
|
|
162
|
+
reason = "first healthy member without live seven-day reset data";
|
|
163
|
+
else if (picked?.verdict === "no_data")
|
|
164
|
+
reason = "first member without live health data";
|
|
165
|
+
else
|
|
166
|
+
reason = "no healthy member; using the existing fallback";
|
|
167
|
+
lines.push(` strategy: ${strategy} — picked ${decision.account}: ${reason}`);
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
lines.push(` strategy: ${strategy}`);
|
|
171
|
+
}
|
|
133
172
|
lines.push(model.stickyAccount
|
|
134
|
-
?
|
|
173
|
+
? strategy === "reset-aware"
|
|
174
|
+
? ` pool is on ${model.stickyAccount} (dwell applies while it remains usable)`
|
|
175
|
+
: ` pool is rotated onto ${model.stickyAccount} (returns home when the home window resets)`
|
|
135
176
|
: ` pool is on its home account (no rotation active)`);
|
|
136
177
|
}
|
|
137
178
|
}
|
package/dist/health.js
CHANGED
|
@@ -222,7 +222,27 @@ export function summarizeWindowUsage(state, options, nowMs) {
|
|
|
222
222
|
}
|
|
223
223
|
return usage.sort((a, b) => (b.resetsAt ?? 0) - (a.resetsAt ?? 0));
|
|
224
224
|
}
|
|
225
|
-
export function
|
|
225
|
+
export function poolStrategy(value) {
|
|
226
|
+
return value === "reset-aware" ? "reset-aware" : "ordered";
|
|
227
|
+
}
|
|
228
|
+
export function sevenDayReset(state, options, nowMs) {
|
|
229
|
+
const usage = summarizeWindowUsage(state, options, nowMs);
|
|
230
|
+
return (usage.find((w) => w.window === "usage:seven_day" && w.resetsAt !== undefined)
|
|
231
|
+
?? usage.find((w) => w.window === "seven_day" && w.resetsAt !== undefined))?.resetsAt;
|
|
232
|
+
}
|
|
233
|
+
export function choosePoolAccount(pool, strategy = "ordered") {
|
|
234
|
+
if (strategy === "reset-aware") {
|
|
235
|
+
let best;
|
|
236
|
+
for (const member of pool) {
|
|
237
|
+
if (member.verdict !== "ok")
|
|
238
|
+
continue;
|
|
239
|
+
if (!best || (member.sevenDayResetsAt !== undefined &&
|
|
240
|
+
(best.sevenDayResetsAt === undefined || member.sevenDayResetsAt < best.sevenDayResetsAt)))
|
|
241
|
+
best = member;
|
|
242
|
+
}
|
|
243
|
+
if (best?.sevenDayResetsAt !== undefined)
|
|
244
|
+
return best.id;
|
|
245
|
+
}
|
|
226
246
|
const usable = pool.find((a) => a.verdict === "ok" || a.verdict === "no_data");
|
|
227
247
|
if (usable)
|
|
228
248
|
return usable.id;
|
|
@@ -252,6 +272,6 @@ export function fallbackPoolAccount(pool, nowMs) {
|
|
|
252
272
|
return retest.id;
|
|
253
273
|
return (pool.find((a) => a.verdict !== "credential_failed") ?? pool[0]).id;
|
|
254
274
|
}
|
|
255
|
-
export function pickPoolAccountForLaunch(pool, nowMs) {
|
|
256
|
-
return choosePoolAccount(pool) ?? fallbackPoolAccount(pool, nowMs);
|
|
275
|
+
export function pickPoolAccountForLaunch(pool, nowMs, strategy = "ordered") {
|
|
276
|
+
return choosePoolAccount(pool, strategy) ?? fallbackPoolAccount(pool, nowMs);
|
|
257
277
|
}
|
package/dist/index.js
CHANGED
|
@@ -7,7 +7,7 @@ import { MODEL_ALIASES, buildCatalogEntries, canonicalModelId, isModernClaudeMod
|
|
|
7
7
|
import { decideDegradation, matchesPin } from "./degrade.js";
|
|
8
8
|
import { resolveExecMode, permissionModeArgs } from "./exec-policy.js";
|
|
9
9
|
import { resolveBaseModelIds } from "./catalog-source.js";
|
|
10
|
-
import { allCredentialFailed, classifyAccountHealth, credentialFailureFor, lastAuthSuccessAt, overdueProbeAccount, } from "./health.js";
|
|
10
|
+
import { allCredentialFailed, classifyAccountHealth, credentialFailureFor, lastAuthSuccessAt, overdueProbeAccount, sevenDayReset, } from "./health.js";
|
|
11
11
|
import { decideStickySelection } from "./sticky.js";
|
|
12
12
|
import { mergeHealthStates, parseStoredState, recordCredentialFailure, } from "./shim-core.js";
|
|
13
13
|
import { createTokenRefResolver, isSecretRefShape, } from "./token-resolution.js";
|
|
@@ -843,12 +843,16 @@ export function registerPoolBackend(api, pool, accounts, registeredIds, execMode
|
|
|
843
843
|
id: v.id,
|
|
844
844
|
verdict: v.health.verdict,
|
|
845
845
|
observedAt: v.health.observedAt,
|
|
846
|
+
sevenDayResetsAt: pool.strategy === "reset-aware"
|
|
847
|
+
? sevenDayReset(states.find((s) => s.id === v.id)?.state, options, now)
|
|
848
|
+
: undefined,
|
|
846
849
|
}));
|
|
847
850
|
const decision = decideStickySelection({
|
|
848
851
|
verdicts: poolVerdicts,
|
|
849
852
|
sticky: previousSticky,
|
|
850
853
|
nowMs: now,
|
|
851
854
|
minDwellMs,
|
|
855
|
+
strategy: pool.strategy,
|
|
852
856
|
});
|
|
853
857
|
const chosen = members.find((a) => a.id === decision.account) ?? members[0];
|
|
854
858
|
const previousAccount = previousSticky?.account ?? members[0].id;
|
|
@@ -1051,6 +1055,7 @@ export function startDirectOrderSync(params) {
|
|
|
1051
1055
|
intervalMs,
|
|
1052
1056
|
healthOptions,
|
|
1053
1057
|
minDwellMs: params.pool?.minDwellMs,
|
|
1058
|
+
strategy: params.pool?.strategy,
|
|
1054
1059
|
command: params.directRoute?.openclawCommand,
|
|
1055
1060
|
});
|
|
1056
1061
|
if (directController?.signature === signature && directTimer) {
|
|
@@ -1065,6 +1070,7 @@ export function startDirectOrderSync(params) {
|
|
|
1065
1070
|
readHealth: (id) => readHealthState(id),
|
|
1066
1071
|
healthOptions,
|
|
1067
1072
|
minDwellMs: params.pool?.minDwellMs,
|
|
1073
|
+
strategy: params.pool?.strategy,
|
|
1068
1074
|
configOrder: params.configOrder,
|
|
1069
1075
|
readSticky: () => readStickyEntry(stickyFile),
|
|
1070
1076
|
writeSticky: (entry) => writeStickyEntry(stickyFile, entry, logger),
|
package/dist/sticky.js
CHANGED
|
@@ -4,9 +4,19 @@ export function decideStickySelection(params) {
|
|
|
4
4
|
const { verdicts, sticky, nowMs } = params;
|
|
5
5
|
const minDwellMs = params.minDwellMs ?? DEFAULT_MIN_DWELL_MS;
|
|
6
6
|
const home = verdicts[0];
|
|
7
|
-
const healthChoice = choosePoolAccount(verdicts);
|
|
7
|
+
const healthChoice = choosePoolAccount(verdicts, params.strategy);
|
|
8
8
|
if (!healthChoice)
|
|
9
9
|
return { account: fallbackPoolAccount(verdicts, nowMs) };
|
|
10
|
+
if (params.strategy === "reset-aware") {
|
|
11
|
+
const current = sticky && verdicts.find((v) => v.id === sticky.account);
|
|
12
|
+
const stillUsable = current?.verdict === "ok" || current?.verdict === "no_data";
|
|
13
|
+
if (sticky && stillUsable && nowMs - sticky.since < minDwellMs) {
|
|
14
|
+
return { account: sticky.account, sticky };
|
|
15
|
+
}
|
|
16
|
+
if (sticky?.account === healthChoice)
|
|
17
|
+
return { account: healthChoice, sticky };
|
|
18
|
+
return { account: healthChoice, sticky: { account: healthChoice, since: nowMs } };
|
|
19
|
+
}
|
|
10
20
|
const stickyVerdict = sticky
|
|
11
21
|
? verdicts.find((v) => v.id === sticky.account)?.verdict
|
|
12
22
|
: undefined;
|
package/openclaw.plugin.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "multi-clawd",
|
|
3
3
|
"name": "multi-clawd",
|
|
4
|
-
"version": "2.
|
|
4
|
+
"version": "2.3.0",
|
|
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",
|
|
@@ -41,7 +41,12 @@
|
|
|
41
41
|
"items": {
|
|
42
42
|
"type": "string"
|
|
43
43
|
},
|
|
44
|
-
"description": "Account ids (from accounts[]) in preference order; the first is the home account
|
|
44
|
+
"description": "Account ids (from accounts[]) in preference order; the first is the home account in the default ordered strategy."
|
|
45
|
+
},
|
|
46
|
+
"strategy": {
|
|
47
|
+
"type": "string",
|
|
48
|
+
"enum": ["ordered", "reset-aware"],
|
|
49
|
+
"description": "Healthy account selection: ordered (default) keeps pool order; reset-aware prefers the healthy account whose live seven-day quota window resets soonest."
|
|
45
50
|
},
|
|
46
51
|
"utilizationThreshold": {
|
|
47
52
|
"type": "number",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@drakon-systems/multi-clawd",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.3.0",
|
|
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. Also imports those accounts' setup tokens into Hermes Agent's Anthropic credential pool.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
package/scripts/cli.mjs
CHANGED
|
@@ -215,6 +215,7 @@ async function loadDirect(config) {
|
|
|
215
215
|
configOrder,
|
|
216
216
|
sticky,
|
|
217
217
|
minDwellMs: pool?.minDwellMs,
|
|
218
|
+
strategy: pool?.strategy,
|
|
218
219
|
nowMs: Date.now(),
|
|
219
220
|
...opts,
|
|
220
221
|
});
|
|
@@ -692,9 +693,11 @@ async function explain() {
|
|
|
692
693
|
return { id: a.id, verdict: h.verdict, detail, usage };
|
|
693
694
|
});
|
|
694
695
|
let stickyAccount;
|
|
696
|
+
let stickyEntry;
|
|
695
697
|
if (pool) {
|
|
696
698
|
try {
|
|
697
699
|
const sticky = JSON.parse(rf(join(stateDir, `pool-${pool.id}.sticky.json`), "utf8"));
|
|
700
|
+
stickyEntry = sticky;
|
|
698
701
|
if (sticky?.account && sticky.account !== pool.accounts[0]) stickyAccount = sticky.account;
|
|
699
702
|
} catch {
|
|
700
703
|
/* no sticky state */
|
|
@@ -710,7 +713,7 @@ async function explain() {
|
|
|
710
713
|
}
|
|
711
714
|
console.log(`\n${BOLD}🦞 multi-clawd — your setup, in plain English${RESET}\n`);
|
|
712
715
|
console.log(
|
|
713
|
-
ec.renderExplanation({ accounts, pool, chain, health: healthRows, stickyAccount, nowMs: now, direct }),
|
|
716
|
+
ec.renderExplanation({ accounts, pool, chain, health: healthRows, stickyAccount, stickyEntry, nowMs: now, direct }),
|
|
714
717
|
);
|
|
715
718
|
console.log(`\n${DIM}(health checks: multi-clawd doctor · change things: multi-clawd setup)${RESET}`);
|
|
716
719
|
}
|
|
@@ -755,6 +758,7 @@ async function usageCommand(args) {
|
|
|
755
758
|
const stateDir = join(homedir(), ".openclaw", "state", "multi-clawd");
|
|
756
759
|
const now = Date.now();
|
|
757
760
|
const rows = [];
|
|
761
|
+
const resetById = new Map();
|
|
758
762
|
for (const a of accounts) {
|
|
759
763
|
const row = { id: a.id, label: a.label };
|
|
760
764
|
if (a.oauthTokenFile || a.oauthTokenRef) {
|
|
@@ -785,12 +789,21 @@ async function usageCommand(args) {
|
|
|
785
789
|
const h = health.classifyAccountHealth(state, healthOptions, now);
|
|
786
790
|
row.verdict = h.verdict;
|
|
787
791
|
row.reason = h.reason;
|
|
792
|
+
if (pool?.strategy === "reset-aware") resetById.set(a.id, health.sevenDayReset(state, healthOptions, now));
|
|
788
793
|
rows.push(row);
|
|
789
794
|
}
|
|
790
795
|
let next;
|
|
791
796
|
if (pool) {
|
|
792
|
-
const verdicts = pool.accounts.map((id) => ({ id, verdict: rows.find((r) => r.id === id)?.verdict ?? "no_data"
|
|
793
|
-
|
|
797
|
+
const verdicts = pool.accounts.map((id) => ({ id, verdict: rows.find((r) => r.id === id)?.verdict ?? "no_data",
|
|
798
|
+
sevenDayResetsAt: resetById.get(id) }));
|
|
799
|
+
if (pool.strategy === "reset-aware") {
|
|
800
|
+
const stickySelection = await import(resolve(__dirname, "..", "dist", "sticky.js"));
|
|
801
|
+
let sticky;
|
|
802
|
+
try { sticky = JSON.parse(rf(join(stateDir, `pool-${pool.id}.sticky.json`), "utf8")); } catch { /* none */ }
|
|
803
|
+
next = stickySelection.decideStickySelection({ verdicts, sticky, nowMs: now, minDwellMs: pool.minDwellMs, strategy: pool.strategy }).account;
|
|
804
|
+
} else {
|
|
805
|
+
next = health.choosePoolAccount(verdicts);
|
|
806
|
+
}
|
|
794
807
|
}
|
|
795
808
|
if (args.includes("--json")) {
|
|
796
809
|
console.log(JSON.stringify({ at: new Date(now).toISOString(), rotateAt, warnThreshold, pool: pool?.id, nextLaunch: next, accounts: rows }, null, 2));
|
package/scripts/doctor.mjs
CHANGED
|
@@ -294,12 +294,13 @@ const { createSystemCredentialIo, findCredentialCopies, describeSplitStore } = a
|
|
|
294
294
|
["createSystemCredentialIo", "findCredentialCopies", "describeSplitStore"],
|
|
295
295
|
"cli",
|
|
296
296
|
);
|
|
297
|
-
const { summarizeWindowUsage, classifyAccountHealth, overdueProbeAccount, lastAuthSuccessAt } =
|
|
297
|
+
const { summarizeWindowUsage, classifyAccountHealth, overdueProbeAccount, lastAuthSuccessAt, sevenDayReset } =
|
|
298
298
|
await importDist("health.js", [
|
|
299
299
|
"summarizeWindowUsage",
|
|
300
300
|
"classifyAccountHealth",
|
|
301
301
|
"overdueProbeAccount",
|
|
302
302
|
"lastAuthSuccessAt",
|
|
303
|
+
"sevenDayReset",
|
|
303
304
|
]);
|
|
304
305
|
const { DEFAULT_UTILIZATION_THRESHOLD } = await import(join(REPO_DIR, "dist", "health.js"));
|
|
305
306
|
// "cli", like the chain audits: resolving WHICH login backs an account is
|
|
@@ -536,16 +537,17 @@ else {
|
|
|
536
537
|
weeklyUtilizationThreshold: pool.weeklyUtilizationThreshold,
|
|
537
538
|
rotateOnOverage: pool.rotateOnOverage,
|
|
538
539
|
};
|
|
539
|
-
const verdicts = members.map((id) =>
|
|
540
|
-
id
|
|
541
|
-
verdict: classifyAccountHealth(
|
|
542
|
-
.
|
|
543
|
-
})
|
|
540
|
+
const verdicts = members.map((id) => {
|
|
541
|
+
const state = readJson(join(STATE_DIR, `${id}.json`));
|
|
542
|
+
return { id, verdict: classifyAccountHealth(state, healthOptions, now).verdict,
|
|
543
|
+
sevenDayResetsAt: pool.strategy === "reset-aware" ? sevenDayReset(state, healthOptions, now) : undefined };
|
|
544
|
+
});
|
|
544
545
|
const decision = decideStickySelection({
|
|
545
546
|
verdicts,
|
|
546
547
|
sticky,
|
|
547
548
|
nowMs: now,
|
|
548
549
|
minDwellMs: pool.minDwellMs,
|
|
550
|
+
strategy: pool.strategy,
|
|
549
551
|
});
|
|
550
552
|
const identity = identities.find((i) => i.accountId === decision.account);
|
|
551
553
|
const who =
|
|
@@ -888,6 +890,7 @@ if (accounts.some((a) => a && a.direct !== undefined && a.direct !== false)) {
|
|
|
888
890
|
configOrder: Array.isArray(config?.auth?.order?.anthropic) ? config.auth.order.anthropic : undefined,
|
|
889
891
|
sticky: readJson(join(STATE_DIR, "direct-order.sticky.json")),
|
|
890
892
|
minDwellMs: pool?.minDwellMs,
|
|
893
|
+
strategy: pool?.strategy,
|
|
891
894
|
nowMs: Date.now(),
|
|
892
895
|
});
|
|
893
896
|
if (!status) continue;
|