@drakon-systems/multi-clawd 1.8.9 → 1.8.11
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 +6 -1
- package/dist/index.js +29 -0
- package/dist/models.js +2 -0
- package/dist/retry-plan.js +70 -0
- package/dist/shim.js +189 -13
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -241,6 +241,11 @@ claude-cli/claude-fable-5 # main login
|
|
|
241
241
|
- 🧲 **Sticky rotation (v0.3)** — after handing over, the pool dwells on the
|
|
242
242
|
spare account (default 10 min) before returning home, so turns never flap
|
|
243
243
|
across the threshold. Health always overrides stickiness.
|
|
244
|
+
- 🔁 **In-turn retry on a reactive limit** — when an account hits a model cap
|
|
245
|
+
*during* a launch, the pool no longer loses that turn to the next provider in
|
|
246
|
+
your chain: the refusal is swallowed before anything reaches you and the turn
|
|
247
|
+
is re-spawned on a healthy account. Fresh launches, secret-free sibling
|
|
248
|
+
accounts, one retry.
|
|
244
249
|
- 📟 **Operator alerts (v0.3)** — dead logins (probed every 15 min without
|
|
245
250
|
spending quota), pool rotations, whole-pool exhaustion, and watchdog
|
|
246
251
|
restarts surface through your agent's next heartbeat (e.g. straight into
|
|
@@ -369,7 +374,7 @@ openclaw plugins install (Get-Location).Path
|
|
|
369
374
|
**Or let your agent install it.** Running an OpenClaw assistant or Claude
|
|
370
375
|
Code on the target machine already? Paste it this and go make coffee:
|
|
371
376
|
|
|
372
|
-
> Read https://raw.githubusercontent.com/Drakon-Systems-Ltd/multi-clawd/v1.8.
|
|
377
|
+
> Read https://raw.githubusercontent.com/Drakon-Systems-Ltd/multi-clawd/v1.8.11/SETUP-AGENT.md
|
|
373
378
|
> and follow it to set up multi-clawd on this machine. I own a second
|
|
374
379
|
> Claude account — ask me when you need me to log in.
|
|
375
380
|
|
package/dist/index.js
CHANGED
|
@@ -13,6 +13,7 @@ import { mergeHealthStates, parseStoredState, recordCredentialFailure, } from ".
|
|
|
13
13
|
import { createTokenRefResolver, isSecretRefShape, } from "./token-resolution.js";
|
|
14
14
|
import { resolveSecretRefValues } from "openclaw/plugin-sdk/secret-ref-runtime";
|
|
15
15
|
import { resolvePoolExecutionArgs } from "./tool-cap.js";
|
|
16
|
+
import { RETRY_ROSTER_ENV } from "./retry-plan.js";
|
|
16
17
|
import { addAlert, alertKeysWithPrefix, clearAlert, pendingAlertText, } from "./alerts.js";
|
|
17
18
|
import { healthStateFile, clearAccountCredentialFailure } from "./credential-state.js";
|
|
18
19
|
export { healthStateFile, clearAccountCredentialFailure };
|
|
@@ -433,10 +434,29 @@ export function buildBackend(account, execMode) {
|
|
|
433
434
|
Object.assign(backend.config, { sessionArg: "--session-id" });
|
|
434
435
|
return backend;
|
|
435
436
|
}
|
|
437
|
+
export function buildRetryRoster(members, launchedId) {
|
|
438
|
+
const roster = [];
|
|
439
|
+
for (const member of members) {
|
|
440
|
+
if (member.id === launchedId)
|
|
441
|
+
continue;
|
|
442
|
+
if (member.oauthTokenFile || member.oauthTokenRef)
|
|
443
|
+
continue;
|
|
444
|
+
const stateFile = healthStateFile(member.id);
|
|
445
|
+
const env = buildAccountChildEnv(member, undefined, stateFile);
|
|
446
|
+
delete env.MULTI_CLAWD_ACCOUNT_ID;
|
|
447
|
+
delete env.MULTI_CLAWD_STATE_FILE;
|
|
448
|
+
roster.push({ id: member.id, stateFile, env });
|
|
449
|
+
}
|
|
450
|
+
return roster;
|
|
451
|
+
}
|
|
436
452
|
async function buildAccountEnv(account, resolver) {
|
|
437
453
|
const token = await resolveTokenAsync(account, resolver);
|
|
438
454
|
return buildAccountChildEnv(account, token, healthStateFile(account.id));
|
|
439
455
|
}
|
|
456
|
+
export function isRuntimelessRegistration(api) {
|
|
457
|
+
const mode = api?.registrationMode;
|
|
458
|
+
return mode === "cli-metadata" || mode === "setup-only";
|
|
459
|
+
}
|
|
440
460
|
function buildCatalogProvider(account) {
|
|
441
461
|
return {
|
|
442
462
|
id: account.id,
|
|
@@ -466,6 +486,8 @@ export default definePluginEntry({
|
|
|
466
486
|
name: "multi-clawd",
|
|
467
487
|
description: "Register additional Claude Code logins as first-class OpenClaw CLI backends for cross-account failover.",
|
|
468
488
|
register(api) {
|
|
489
|
+
if (isRuntimelessRegistration(api))
|
|
490
|
+
return;
|
|
469
491
|
const runtimeConfigLoader = api.runtime?.config?.current
|
|
470
492
|
? () => api.runtime.config.current()
|
|
471
493
|
: undefined;
|
|
@@ -691,10 +713,12 @@ export function registerPoolBackend(api, pool, accounts, registeredIds, execMode
|
|
|
691
713
|
writeStickyEntry(stickyFile, decision.sticky, logger);
|
|
692
714
|
const order = [chosen, ...members.filter((m) => m.id !== chosen.id)];
|
|
693
715
|
let env;
|
|
716
|
+
let launched;
|
|
694
717
|
const unresolved = [];
|
|
695
718
|
for (const candidate of order) {
|
|
696
719
|
try {
|
|
697
720
|
env = await buildAccountEnv(candidate, deps?.resolver);
|
|
721
|
+
launched = candidate;
|
|
698
722
|
if (candidate.id !== chosen.id) {
|
|
699
723
|
logger.warn(`[multi-clawd] pool ${poolId}: ${chosen.id}'s credential did not resolve — ` +
|
|
700
724
|
`launching on ${candidate.id} instead (account not benched; secret provider may be down)`);
|
|
@@ -713,6 +737,11 @@ export function registerPoolBackend(api, pool, accounts, registeredIds, execMode
|
|
|
713
737
|
throw new Error(`[multi-clawd] ${text}`);
|
|
714
738
|
}
|
|
715
739
|
alertState = clearAlert(alertState, `pool-unresolvable:${poolId}`);
|
|
740
|
+
{
|
|
741
|
+
const roster = buildRetryRoster(members, (launched ?? chosen).id);
|
|
742
|
+
if (roster.length > 0)
|
|
743
|
+
env[RETRY_ROSTER_ENV] = JSON.stringify(roster);
|
|
744
|
+
}
|
|
716
745
|
if (ladder.length > 0) {
|
|
717
746
|
const pinned = matchesPin(pins, {
|
|
718
747
|
agentDir: ctx.agentDir ?? "",
|
package/dist/models.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export const MODEL_ALIASES = {
|
|
2
2
|
opus: "opus",
|
|
3
|
+
"opus-5.5": "claude-opus-5-5",
|
|
3
4
|
"opus-5": "claude-opus-5",
|
|
4
5
|
"opus-4.8": "claude-opus-4-8",
|
|
5
6
|
"opus-4.7": "claude-opus-4-7",
|
|
@@ -9,6 +10,7 @@ export const MODEL_ALIASES = {
|
|
|
9
10
|
haiku: "haiku",
|
|
10
11
|
};
|
|
11
12
|
const KNOWN_SPECS = {
|
|
13
|
+
"claude-opus-5-5": { name: "Claude Opus 5.5", contextWindow: 1000000, maxTokens: 128000 },
|
|
12
14
|
"claude-opus-5": { name: "Claude Opus 5", contextWindow: 1000000, maxTokens: 128000 },
|
|
13
15
|
"claude-opus-4-8": { name: "Claude Opus 4.8", contextWindow: 1048576, maxTokens: 128000 },
|
|
14
16
|
"claude-opus-4-7": { name: "Claude Opus 4.7", contextWindow: 1048576, maxTokens: 64000 },
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { classifyAccountHealth } from "./health.js";
|
|
2
|
+
export const RETRY_ROSTER_ENV = "MULTI_CLAWD_RETRY_ACCOUNTS";
|
|
3
|
+
const CREDENTIAL_ENV_KEYS = ["CLAUDE_CONFIG_DIR", "CLAUDE_CODE_OAUTH_TOKEN"];
|
|
4
|
+
export function parseRetryRoster(raw) {
|
|
5
|
+
if (!raw)
|
|
6
|
+
return [];
|
|
7
|
+
let parsed;
|
|
8
|
+
try {
|
|
9
|
+
parsed = JSON.parse(raw);
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
return [];
|
|
13
|
+
}
|
|
14
|
+
if (!Array.isArray(parsed))
|
|
15
|
+
return [];
|
|
16
|
+
const out = [];
|
|
17
|
+
for (const entry of parsed) {
|
|
18
|
+
if (!entry || typeof entry !== "object")
|
|
19
|
+
continue;
|
|
20
|
+
const e = entry;
|
|
21
|
+
if (typeof e.id !== "string" || !e.id)
|
|
22
|
+
continue;
|
|
23
|
+
if (typeof e.stateFile !== "string" || !e.stateFile)
|
|
24
|
+
continue;
|
|
25
|
+
const env = {};
|
|
26
|
+
if (e.env && typeof e.env === "object") {
|
|
27
|
+
for (const [k, v] of Object.entries(e.env)) {
|
|
28
|
+
if (typeof v === "string")
|
|
29
|
+
env[k] = v;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
out.push({ id: e.id, stateFile: e.stateFile, env });
|
|
33
|
+
}
|
|
34
|
+
return out;
|
|
35
|
+
}
|
|
36
|
+
export function retryArming(argv, roster) {
|
|
37
|
+
if (roster.length === 0) {
|
|
38
|
+
return { armed: false, reason: "no secret-free sibling account to retry onto" };
|
|
39
|
+
}
|
|
40
|
+
if (argv.includes("--resume")) {
|
|
41
|
+
return {
|
|
42
|
+
armed: false,
|
|
43
|
+
reason: "resumed session — its Claude session lives in this account's config dir",
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
if (!argv.some((a) => a === "stream-json" || a === "--output-format=stream-json")) {
|
|
47
|
+
return { armed: false, reason: "not a stream-json launch" };
|
|
48
|
+
}
|
|
49
|
+
return { armed: true };
|
|
50
|
+
}
|
|
51
|
+
export function chooseRetryAccount(params) {
|
|
52
|
+
for (const account of params.roster) {
|
|
53
|
+
const state = params.readState(account.stateFile);
|
|
54
|
+
const verdict = classifyAccountHealth(state, params.options ?? {}, params.nowMs, params.modelId).verdict;
|
|
55
|
+
if (verdict === "ok" || verdict === "no_data")
|
|
56
|
+
return account;
|
|
57
|
+
}
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
export function buildRetryEnv(base, account) {
|
|
61
|
+
const env = { ...base };
|
|
62
|
+
for (const key of CREDENTIAL_ENV_KEYS)
|
|
63
|
+
delete env[key];
|
|
64
|
+
for (const [k, v] of Object.entries(account.env))
|
|
65
|
+
env[k] = v;
|
|
66
|
+
env.MULTI_CLAWD_ACCOUNT_ID = account.id;
|
|
67
|
+
env.MULTI_CLAWD_STATE_FILE = account.stateFile;
|
|
68
|
+
env[RETRY_ROSTER_ENV] = "";
|
|
69
|
+
return env;
|
|
70
|
+
}
|
package/dist/shim.js
CHANGED
|
@@ -5,6 +5,7 @@ import { classifyStateReadFailure, clearCredentialFailure, createLineScanner, me
|
|
|
5
5
|
import { rewriteModelArg } from "./degrade.js";
|
|
6
6
|
import { parseModelLimitError, recordModelLimit } from "./shim-core.js";
|
|
7
7
|
import { canonicalModelId } from "./models.js";
|
|
8
|
+
import { buildRetryEnv, chooseRetryAccount, parseRetryRoster, retryArming, RETRY_ROSTER_ENV, } from "./retry-plan.js";
|
|
8
9
|
function resolveClaudeCommand() {
|
|
9
10
|
const override = process.env.MULTI_CLAWD_CLAUDE_BIN;
|
|
10
11
|
if (override) {
|
|
@@ -20,8 +21,8 @@ function resolveClaudeCommand() {
|
|
|
20
21
|
}
|
|
21
22
|
return { command: "claude", prependArgs: [] };
|
|
22
23
|
}
|
|
23
|
-
|
|
24
|
-
|
|
24
|
+
let stateFile = process.env.MULTI_CLAWD_STATE_FILE;
|
|
25
|
+
let accountId = process.env.MULTI_CLAWD_ACCOUNT_ID ?? "unknown";
|
|
25
26
|
let state = { accountId, windows: {} };
|
|
26
27
|
function preserveCorruptState(raw) {
|
|
27
28
|
if (!stateFile)
|
|
@@ -87,11 +88,43 @@ if (modelOverride) {
|
|
|
87
88
|
process.stderr.write(`[multi-clawd shim] degrading model for this launch → ${modelOverride}\n`);
|
|
88
89
|
}
|
|
89
90
|
}
|
|
90
|
-
const
|
|
91
|
+
const retryRoster = parseRetryRoster(process.env[RETRY_ROSTER_ENV]);
|
|
92
|
+
const arming = retryArming(childArgs, retryRoster);
|
|
93
|
+
let retryArmed = arming.armed;
|
|
94
|
+
if (!retryArmed && retryRoster.length > 0 && arming.reason) {
|
|
95
|
+
process.stderr.write(`[multi-clawd shim] in-turn retry unavailable: ${arming.reason}\n`);
|
|
96
|
+
}
|
|
97
|
+
let child = spawn(command, childArgs, {
|
|
91
98
|
stdio: ["pipe", "pipe", "inherit"],
|
|
92
99
|
env: process.env,
|
|
93
100
|
});
|
|
94
|
-
|
|
101
|
+
const STDIN_REPLAY_CAP_BYTES = 8 * 1024 * 1024;
|
|
102
|
+
let stdinReplay = [];
|
|
103
|
+
let stdinReplayBytes = 0;
|
|
104
|
+
let stdinEnded = false;
|
|
105
|
+
if (retryArmed) {
|
|
106
|
+
process.stdin.on("data", (chunk) => {
|
|
107
|
+
if (retryArmed) {
|
|
108
|
+
stdinReplayBytes += chunk.length;
|
|
109
|
+
if (stdinReplayBytes > STDIN_REPLAY_CAP_BYTES) {
|
|
110
|
+
retryArmed = false;
|
|
111
|
+
stdinReplay = [];
|
|
112
|
+
releaseHeldOutput();
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
stdinReplay.push(chunk);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
child.stdin.write(chunk);
|
|
119
|
+
});
|
|
120
|
+
process.stdin.on("end", () => {
|
|
121
|
+
stdinEnded = true;
|
|
122
|
+
child.stdin.end();
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
process.stdin.pipe(child.stdin);
|
|
127
|
+
}
|
|
95
128
|
function effectiveModelId() {
|
|
96
129
|
const idx = childArgs.indexOf("--model");
|
|
97
130
|
if (idx < 0 || idx + 1 >= childArgs.length)
|
|
@@ -109,7 +142,7 @@ function guessLimitResetsAt() {
|
|
|
109
142
|
return undefined;
|
|
110
143
|
}
|
|
111
144
|
let sawAuthFailure = false;
|
|
112
|
-
|
|
145
|
+
function observeLine(line) {
|
|
113
146
|
try {
|
|
114
147
|
const event = parseRateLimitEvent(line);
|
|
115
148
|
if (event) {
|
|
@@ -135,12 +168,154 @@ const scanner = createLineScanner((line) => {
|
|
|
135
168
|
}
|
|
136
169
|
catch {
|
|
137
170
|
}
|
|
138
|
-
}
|
|
139
|
-
|
|
171
|
+
}
|
|
172
|
+
const scanner = createLineScanner(observeLine);
|
|
173
|
+
const HOLD_CAP_BYTES = 256 * 1024;
|
|
174
|
+
const HOLD_MAX_MS = Number(process.env.MULTI_CLAWD_HOLD_MAX_MS ?? 10_000);
|
|
175
|
+
let held = [];
|
|
176
|
+
let heldBytes = 0;
|
|
177
|
+
let holdTail = "";
|
|
178
|
+
let retryUsed = false;
|
|
179
|
+
function isPreambleRecord(line) {
|
|
180
|
+
if (line.length === 0)
|
|
181
|
+
return true;
|
|
182
|
+
try {
|
|
183
|
+
const record = JSON.parse(line);
|
|
184
|
+
if (record.type === "system" || record.type === "rate_limit_event")
|
|
185
|
+
return true;
|
|
186
|
+
if (record.type === "assistant" && record.message?.model === "<synthetic>")
|
|
187
|
+
return true;
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
catch {
|
|
191
|
+
return false;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
function releaseHeldOutput() {
|
|
195
|
+
if (held.length > 0) {
|
|
196
|
+
process.stdout.write(held.join(""));
|
|
197
|
+
held = [];
|
|
198
|
+
heldBytes = 0;
|
|
199
|
+
}
|
|
200
|
+
if (holdTail.length > 0) {
|
|
201
|
+
process.stdout.write(holdTail);
|
|
202
|
+
scanner.push(holdTail);
|
|
203
|
+
holdTail = "";
|
|
204
|
+
}
|
|
205
|
+
retryArmed = false;
|
|
206
|
+
stdinReplay = [];
|
|
207
|
+
}
|
|
208
|
+
function readSiblingState(file) {
|
|
209
|
+
try {
|
|
210
|
+
return parseStoredState(readFileSync(file, "utf8"));
|
|
211
|
+
}
|
|
212
|
+
catch {
|
|
213
|
+
return undefined;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
function attemptRetry() {
|
|
217
|
+
if (!retryArmed || retryUsed)
|
|
218
|
+
return false;
|
|
219
|
+
const model = effectiveModelId();
|
|
220
|
+
const target = chooseRetryAccount({
|
|
221
|
+
roster: retryRoster,
|
|
222
|
+
readState: readSiblingState,
|
|
223
|
+
modelId: model,
|
|
224
|
+
nowMs: Date.now(),
|
|
225
|
+
});
|
|
226
|
+
if (!target) {
|
|
227
|
+
process.stderr.write(`[multi-clawd shim] model limit on ${accountId} with no healthy sibling to retry onto — ` +
|
|
228
|
+
`passing the failure through to the host's chain\n`);
|
|
229
|
+
releaseHeldOutput();
|
|
230
|
+
return false;
|
|
231
|
+
}
|
|
232
|
+
retryUsed = true;
|
|
233
|
+
retryArmed = false;
|
|
234
|
+
process.stderr.write(`[multi-clawd shim] model limit on ${accountId}${model ? ` for ${model}` : ""} — ` +
|
|
235
|
+
`retrying this turn on ${target.id} (nothing forwarded downstream yet)\n`);
|
|
236
|
+
held = [];
|
|
237
|
+
heldBytes = 0;
|
|
238
|
+
holdTail = "";
|
|
239
|
+
const previous = child;
|
|
240
|
+
previous.stdout.removeAllListeners();
|
|
241
|
+
previous.removeAllListeners("close");
|
|
242
|
+
previous.removeAllListeners("error");
|
|
243
|
+
try {
|
|
244
|
+
previous.kill("SIGTERM");
|
|
245
|
+
}
|
|
246
|
+
catch {
|
|
247
|
+
}
|
|
248
|
+
accountId = target.id;
|
|
249
|
+
stateFile = target.stateFile;
|
|
250
|
+
state = { accountId, windows: {} };
|
|
251
|
+
sawAuthFailure = false;
|
|
252
|
+
child = spawn(command, childArgs, {
|
|
253
|
+
stdio: ["pipe", "pipe", "inherit"],
|
|
254
|
+
env: buildRetryEnv(process.env, target),
|
|
255
|
+
});
|
|
256
|
+
attachChildHandlers();
|
|
257
|
+
for (const chunk of stdinReplay)
|
|
258
|
+
child.stdin.write(chunk);
|
|
259
|
+
if (stdinEnded)
|
|
260
|
+
child.stdin.end();
|
|
261
|
+
stdinReplay = [];
|
|
262
|
+
return true;
|
|
263
|
+
}
|
|
264
|
+
function onArmedChunk(chunk) {
|
|
265
|
+
holdTail += chunk.toString("utf8");
|
|
266
|
+
while (retryArmed) {
|
|
267
|
+
const idx = holdTail.indexOf("\n");
|
|
268
|
+
if (idx < 0)
|
|
269
|
+
break;
|
|
270
|
+
const raw = holdTail.slice(0, idx + 1);
|
|
271
|
+
holdTail = holdTail.slice(idx + 1);
|
|
272
|
+
const line = raw.trim();
|
|
273
|
+
observeLine(line);
|
|
274
|
+
if (parseModelLimitError(line) && attemptRetry())
|
|
275
|
+
return;
|
|
276
|
+
held.push(raw);
|
|
277
|
+
heldBytes += raw.length;
|
|
278
|
+
if (!isPreambleRecord(line) || heldBytes > HOLD_CAP_BYTES) {
|
|
279
|
+
releaseHeldOutput();
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
if (!retryArmed && holdTail.length > 0) {
|
|
284
|
+
const rest = holdTail;
|
|
285
|
+
holdTail = "";
|
|
286
|
+
process.stdout.write(rest);
|
|
287
|
+
scanner.push(rest);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
if (retryArmed) {
|
|
291
|
+
const holdTimer = setTimeout(() => {
|
|
292
|
+
if (!retryArmed)
|
|
293
|
+
return;
|
|
294
|
+
process.stderr.write(`[multi-clawd shim] in-turn retry window closed after ${HOLD_MAX_MS}ms — releasing the stream\n`);
|
|
295
|
+
releaseHeldOutput();
|
|
296
|
+
}, HOLD_MAX_MS);
|
|
297
|
+
holdTimer.unref?.();
|
|
298
|
+
}
|
|
299
|
+
function onPassthroughChunk(chunk) {
|
|
140
300
|
process.stdout.write(chunk);
|
|
141
301
|
scanner.push(chunk.toString("utf8"));
|
|
142
|
-
}
|
|
143
|
-
|
|
302
|
+
}
|
|
303
|
+
function attachChildHandlers() {
|
|
304
|
+
const armedForThisChild = retryArmed;
|
|
305
|
+
child.stdout.on("data", (chunk) => {
|
|
306
|
+
if (armedForThisChild && retryArmed)
|
|
307
|
+
onArmedChunk(chunk);
|
|
308
|
+
else
|
|
309
|
+
onPassthroughChunk(chunk);
|
|
310
|
+
});
|
|
311
|
+
child.stdout.on("end", () => {
|
|
312
|
+
if (retryArmed)
|
|
313
|
+
releaseHeldOutput();
|
|
314
|
+
scanner.flush();
|
|
315
|
+
});
|
|
316
|
+
child.on("close", onChildClose);
|
|
317
|
+
child.on("error", onChildError);
|
|
318
|
+
}
|
|
144
319
|
for (const signal of ["SIGINT", "SIGTERM", "SIGHUP"]) {
|
|
145
320
|
process.on(signal, () => {
|
|
146
321
|
child.kill(signal);
|
|
@@ -159,7 +334,7 @@ function clearRecordedAuthFailureOnSuccess() {
|
|
|
159
334
|
catch {
|
|
160
335
|
}
|
|
161
336
|
}
|
|
162
|
-
|
|
337
|
+
function onChildClose(code, signal) {
|
|
163
338
|
if (signal) {
|
|
164
339
|
process.kill(process.pid, signal);
|
|
165
340
|
return;
|
|
@@ -167,8 +342,9 @@ child.on("close", (code, signal) => {
|
|
|
167
342
|
if ((code ?? 0) === 0)
|
|
168
343
|
clearRecordedAuthFailureOnSuccess();
|
|
169
344
|
process.exit(code ?? 0);
|
|
170
|
-
}
|
|
171
|
-
|
|
345
|
+
}
|
|
346
|
+
function onChildError(err) {
|
|
172
347
|
process.stderr.write(`[multi-clawd shim] failed to spawn claude: ${String(err)}\n`);
|
|
173
348
|
process.exit(127);
|
|
174
|
-
}
|
|
349
|
+
}
|
|
350
|
+
attachChildHandlers();
|
package/openclaw.plugin.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "multi-clawd",
|
|
3
3
|
"name": "multi-clawd",
|
|
4
|
-
"version": "1.8.
|
|
4
|
+
"version": "1.8.11",
|
|
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.8.
|
|
3
|
+
"version": "1.8.11",
|
|
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",
|