@drakon-systems/multi-clawd 1.9.2 → 1.9.4
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 +2 -25
- package/dist/account-identity.js +41 -26
- package/dist/chain-audit.js +2 -0
- package/dist/index.js +0 -52
- package/openclaw.plugin.json +1 -5
- package/package.json +1 -1
- package/scripts/doctor.mjs +12 -7
package/README.md
CHANGED
|
@@ -375,7 +375,7 @@ openclaw plugins install (Get-Location).Path
|
|
|
375
375
|
**Or let your agent install it.** Running an OpenClaw assistant or Claude
|
|
376
376
|
Code on the target machine already? Paste it this and go make coffee:
|
|
377
377
|
|
|
378
|
-
> Read https://raw.githubusercontent.com/Drakon-Systems-Ltd/multi-clawd/v1.9.
|
|
378
|
+
> Read https://raw.githubusercontent.com/Drakon-Systems-Ltd/multi-clawd/v1.9.4/SETUP-AGENT.md
|
|
379
379
|
> and follow it to set up multi-clawd on this machine. I own a second
|
|
380
380
|
> Claude account — ask me when you need me to log in.
|
|
381
381
|
|
|
@@ -666,29 +666,9 @@ What to know:
|
|
|
666
666
|
any cooldown, and the live order. `explain` shows the same, and `doctor
|
|
667
667
|
--probe` makes one tiny live call per profile.
|
|
668
668
|
|
|
669
|
-
## History identity for fresh Claude sessions
|
|
670
|
-
|
|
671
|
-
OpenClaw only replays a conversation into a **fresh** Claude session when it
|
|
672
|
-
knows which account owns the history. It learns that from an auth profile in
|
|
673
|
-
its own store, and Claude logins aren't OpenClaw profiles, so it used to log
|
|
674
|
-
`cli session history refused across auth boundary: reason=auth-unknown` on
|
|
675
|
-
every turn and start fresh sessions blank.
|
|
676
|
-
|
|
677
|
-
multi-clawd now stores one identity-only profile per backend
|
|
678
|
-
(`claw1:multi-clawd-identity`, `clawd:multi-clawd-identity`, …) and points the
|
|
679
|
-
backend at it. The profile is a **name, not a credential**: no token, and
|
|
680
|
-
OpenClaw never uses it to log in — `openclaw models status` shows it as
|
|
681
|
-
`missing_credential`, which is correct. Which account runs is decided exactly
|
|
682
|
-
as before. Right after upgrading, each existing conversation resets its Claude
|
|
683
|
-
session once (`cli session reset … reason=auth-profile` in the log). OpenClaw
|
|
684
|
-
binds the owner only when a conversation is born, so conversations that
|
|
685
|
-
already have history keep logging the refusal line until you start a new one
|
|
686
|
-
(`/new`); every session started after the upgrade is covered from its first
|
|
687
|
-
turn. `"historyIdentity": false` turns the feature off.
|
|
688
|
-
|
|
689
669
|
## How it works
|
|
690
670
|
|
|
691
|
-
|
|
671
|
+
Three moves, all through the official plugin SDK (details in
|
|
692
672
|
[`DESIGN.md`](./DESIGN.md)):
|
|
693
673
|
|
|
694
674
|
1. **`registerCliBackend`** mirrors the bundled `claude-cli` backend — same
|
|
@@ -701,9 +681,6 @@ Four moves, all through the official plugin SDK (details in
|
|
|
701
681
|
3. **`prepareExecution`** injects that account's own login
|
|
702
682
|
(`CLAUDE_CONFIG_DIR` + `CLAUDE_CODE_OAUTH_TOKEN`) into the child process
|
|
703
683
|
env, after the host's ambient Claude credentials are stripped.
|
|
704
|
-
4. **`defaultAuthProfileId`** names an identity-only auth profile per backend,
|
|
705
|
-
so OpenClaw's CLI history boundary binds and fresh sessions get the
|
|
706
|
-
conversation back (see "History identity" above).
|
|
707
684
|
|
|
708
685
|
## Legacy issue: idle backend eviction on OpenClaw ≤ 2026.7.1
|
|
709
686
|
|
package/dist/account-identity.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
export function
|
|
2
|
-
const dir = !account.native && account.configDir ? io.expandHome(account.configDir) : io.defaultConfigDir;
|
|
3
|
-
if (!dir)
|
|
4
|
-
return undefined;
|
|
1
|
+
export function identityFilePaths(account, io) {
|
|
5
2
|
if (account.oauthTokenFile || account.oauthTokenRef)
|
|
6
|
-
return
|
|
7
|
-
|
|
3
|
+
return [];
|
|
4
|
+
const file = (dir) => `${dir.replace(/\/+$/, "")}/.claude.json`;
|
|
5
|
+
if (!account.native && account.configDir)
|
|
6
|
+
return [file(io.expandHome(account.configDir))];
|
|
7
|
+
const candidates = [io.homeDir ? file(io.homeDir) : undefined, io.defaultConfigDir ? file(io.defaultConfigDir) : undefined];
|
|
8
|
+
return [...new Set(candidates.filter((c) => Boolean(c)))];
|
|
8
9
|
}
|
|
9
10
|
export function formatPlan(oauthAccount) {
|
|
10
11
|
const tier = typeof oauthAccount.organizationRateLimitTier === "string"
|
|
@@ -47,14 +48,15 @@ export function parseIdentityFile(text) {
|
|
|
47
48
|
email: str("emailAddress"),
|
|
48
49
|
organizationName: str("organizationName"),
|
|
49
50
|
plan: formatPlan(account),
|
|
51
|
+
profileFetchedAt: typeof account.profileFetchedAt === "number" ? account.profileFetchedAt : undefined,
|
|
50
52
|
};
|
|
51
53
|
if (!identity.accountUuid && !identity.email)
|
|
52
54
|
return undefined;
|
|
53
55
|
return identity;
|
|
54
56
|
}
|
|
55
57
|
export function resolveAccountIdentity(account, io) {
|
|
56
|
-
const
|
|
57
|
-
if (
|
|
58
|
+
const paths = identityFilePaths(account, io);
|
|
59
|
+
if (paths.length === 0) {
|
|
58
60
|
return {
|
|
59
61
|
accountId: account.id,
|
|
60
62
|
status: "unknown",
|
|
@@ -63,28 +65,41 @@ export function resolveAccountIdentity(account, io) {
|
|
|
63
65
|
: "no config dir to read",
|
|
64
66
|
};
|
|
65
67
|
}
|
|
66
|
-
let
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
68
|
+
let best;
|
|
69
|
+
const problems = [];
|
|
70
|
+
for (const path of paths) {
|
|
71
|
+
let text;
|
|
72
|
+
try {
|
|
73
|
+
text = io.readFile(path);
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
problems.push(`no readable ${path}`);
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
const identity = parseIdentityFile(text);
|
|
80
|
+
if (!identity) {
|
|
81
|
+
problems.push(`${path} carries no oauthAccount record`);
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
const candidate = {
|
|
81
85
|
accountId: account.id,
|
|
82
|
-
status: "
|
|
83
|
-
reason: `${path} carries no oauthAccount record`,
|
|
86
|
+
status: "resolved",
|
|
84
87
|
source: path,
|
|
88
|
+
...identity,
|
|
85
89
|
};
|
|
90
|
+
if (!best || (candidate.profileFetchedAt ?? 0) > (best.profileFetchedAt ?? 0))
|
|
91
|
+
best = candidate;
|
|
86
92
|
}
|
|
87
|
-
|
|
93
|
+
if (best)
|
|
94
|
+
return best;
|
|
95
|
+
return {
|
|
96
|
+
accountId: account.id,
|
|
97
|
+
status: "unknown",
|
|
98
|
+
reason: problems.length === 1
|
|
99
|
+
? `${problems[0]} — this account has never completed a login here`
|
|
100
|
+
: `${problems.join("; ")} — this account has never completed a login here`,
|
|
101
|
+
source: paths[0],
|
|
102
|
+
};
|
|
88
103
|
}
|
|
89
104
|
export function maskEmail(email) {
|
|
90
105
|
const at = email.lastIndexOf("@");
|
package/dist/chain-audit.js
CHANGED
|
@@ -271,6 +271,8 @@ export function auditSessionOverrides(sessions, poolConfigured, opts = {}) {
|
|
|
271
271
|
const source = entry.modelOverrideSource;
|
|
272
272
|
if (typeof source !== "string" || source === "auto")
|
|
273
273
|
continue;
|
|
274
|
+
if (source === "default" && !entry.modelOverride)
|
|
275
|
+
continue;
|
|
274
276
|
const provider = entry.providerOverride ?? entry.modelProvider;
|
|
275
277
|
const model = entry.modelOverride;
|
|
276
278
|
if (!provider || !model) {
|
package/dist/index.js
CHANGED
|
@@ -25,7 +25,6 @@ import { execFileSync } from "node:child_process";
|
|
|
25
25
|
import { collectDirectMembers } from "./direct-route.js";
|
|
26
26
|
import { createDirectOrderController, isValidAgentId } from "./direct-sync.js";
|
|
27
27
|
import { createOpenclawRunner } from "./openclaw-runner.js";
|
|
28
|
-
import { HISTORY_IDENTITY_CONFIG_KEY, describeEnsureResult, ensureIdentityProfiles, identityProfileId, loadProviderAuthSdk, } from "./history-identity.js";
|
|
29
28
|
const CLI_FRESH_WATCHDOG_DEFAULTS = {
|
|
30
29
|
noOutputTimeoutRatio: 0.8,
|
|
31
30
|
minMs: 180_000,
|
|
@@ -401,7 +400,6 @@ export function buildBackend(account, execMode) {
|
|
|
401
400
|
resolveExecutionArgs: resolvePoolExecutionArgs,
|
|
402
401
|
sideQuestionToolMode: "disabled",
|
|
403
402
|
ownsNativeCompaction: true,
|
|
404
|
-
defaultAuthProfileId: identityProfileId(account.id),
|
|
405
403
|
config: {
|
|
406
404
|
command: process.execPath,
|
|
407
405
|
args: [SHIM_PATH, ...BASE_ARGS, ...permissionModeArgs(execMode)],
|
|
@@ -554,12 +552,6 @@ export default definePluginEntry({
|
|
|
554
552
|
api.registerProvider(buildCatalogProvider(normalized));
|
|
555
553
|
}
|
|
556
554
|
registerPoolBackend(api, cfg.pool, accounts, seen, execMode);
|
|
557
|
-
startHistoryIdentityEnsure({
|
|
558
|
-
backendIds: [...seen],
|
|
559
|
-
enabled: cfg[HISTORY_IDENTITY_CONFIG_KEY] !== false,
|
|
560
|
-
registrationMode: api.registrationMode,
|
|
561
|
-
logger: api.logger,
|
|
562
|
-
});
|
|
563
555
|
{
|
|
564
556
|
const logger = api.logger;
|
|
565
557
|
try {
|
|
@@ -588,50 +580,6 @@ export default definePluginEntry({
|
|
|
588
580
|
api.logger.info(`[multi-clawd] registered ${seen.size} backend(s)+provider(s): ${[...seen].join(", ")}`);
|
|
589
581
|
},
|
|
590
582
|
});
|
|
591
|
-
let historyIdentitySdkLoader = loadProviderAuthSdk;
|
|
592
|
-
let historyIdentityPending;
|
|
593
|
-
export function setProviderAuthSdkLoaderForTests(loader) {
|
|
594
|
-
historyIdentitySdkLoader = loader ?? loadProviderAuthSdk;
|
|
595
|
-
historyIdentityPending = undefined;
|
|
596
|
-
}
|
|
597
|
-
export function awaitHistoryIdentityEnsure() {
|
|
598
|
-
return historyIdentityPending ?? Promise.resolve(undefined);
|
|
599
|
-
}
|
|
600
|
-
function startHistoryIdentityEnsure(params) {
|
|
601
|
-
const { logger } = params;
|
|
602
|
-
if (params.registrationMode !== undefined && params.registrationMode !== "full")
|
|
603
|
-
return;
|
|
604
|
-
if (!params.enabled) {
|
|
605
|
-
logger.info("[multi-clawd] history identity: disabled by config (historyIdentity: false)");
|
|
606
|
-
return;
|
|
607
|
-
}
|
|
608
|
-
if (params.backendIds.length === 0 || historyIdentityPending)
|
|
609
|
-
return;
|
|
610
|
-
const ids = [...params.backendIds];
|
|
611
|
-
historyIdentityPending = (async () => {
|
|
612
|
-
try {
|
|
613
|
-
const sdk = await historyIdentitySdkLoader();
|
|
614
|
-
if (!sdk) {
|
|
615
|
-
logger.warn("[multi-clawd] history identity: this OpenClaw does not expose openclaw/plugin-sdk/provider-auth store writers — skipped (CLI history reseed stays auth-unknown)");
|
|
616
|
-
return undefined;
|
|
617
|
-
}
|
|
618
|
-
const result = await ensureIdentityProfiles(ids, sdk);
|
|
619
|
-
const line = `[multi-clawd] history identity: ${describeEnsureResult(result)}`;
|
|
620
|
-
if (result.failed.length > 0)
|
|
621
|
-
logger.warn(line);
|
|
622
|
-
else
|
|
623
|
-
logger.info(line);
|
|
624
|
-
return result;
|
|
625
|
-
}
|
|
626
|
-
catch (err) {
|
|
627
|
-
logger.warn(`[multi-clawd] history identity: ${String(err)}`);
|
|
628
|
-
return undefined;
|
|
629
|
-
}
|
|
630
|
-
finally {
|
|
631
|
-
historyIdentityPending = undefined;
|
|
632
|
-
}
|
|
633
|
-
})();
|
|
634
|
-
}
|
|
635
583
|
function readHealthState(accountId) {
|
|
636
584
|
try {
|
|
637
585
|
return JSON.parse(readFileSync(healthStateFile(accountId), "utf8"));
|
package/openclaw.plugin.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "multi-clawd",
|
|
3
3
|
"name": "multi-clawd",
|
|
4
|
-
"version": "1.9.
|
|
4
|
+
"version": "1.9.4",
|
|
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",
|
|
@@ -205,10 +205,6 @@
|
|
|
205
205
|
}
|
|
206
206
|
}
|
|
207
207
|
},
|
|
208
|
-
"historyIdentity": {
|
|
209
|
-
"type": "boolean",
|
|
210
|
-
"description": "Store one identity-only OpenClaw auth profile per registered backend id (<id>:multi-clawd-identity — a name, no token) and point the backend's defaultAuthProfileId at it, so OpenClaw's CLI history boundary can bind and a fresh Claude session is reseeded with prior context instead of `cli session history refused across auth boundary: reason=auth-unknown`. Default true. false = no auth-store writes; the reseed refusal stays."
|
|
211
|
-
},
|
|
212
208
|
"directRoute": {
|
|
213
209
|
"type": "object",
|
|
214
210
|
"additionalProperties": false,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@drakon-systems/multi-clawd",
|
|
3
|
-
"version": "1.9.
|
|
3
|
+
"version": "1.9.4",
|
|
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/doctor.mjs
CHANGED
|
@@ -288,13 +288,17 @@ const { summarizeWindowUsage, classifyAccountHealth } = await importDist("health
|
|
|
288
288
|
"summarizeWindowUsage",
|
|
289
289
|
"classifyAccountHealth",
|
|
290
290
|
]);
|
|
291
|
+
// "cli", like the chain audits: resolving WHICH login backs an account is
|
|
292
|
+
// doctor's own diagnostic, not a description of the running plugin's
|
|
293
|
+
// behaviour. Asking the installed copy meant a fix to the resolver could not
|
|
294
|
+
// take effect until the plugin itself was reinstalled — so the CLI reported a
|
|
295
|
+
// bug it had already been taught not to make.
|
|
291
296
|
const { resolveAccountIdentity, describeIdentity, findDuplicateLogins, maskEmail } =
|
|
292
|
-
await importDist(
|
|
293
|
-
"
|
|
294
|
-
"describeIdentity",
|
|
295
|
-
"
|
|
296
|
-
|
|
297
|
-
]);
|
|
297
|
+
await importDist(
|
|
298
|
+
"account-identity.js",
|
|
299
|
+
["resolveAccountIdentity", "describeIdentity", "findDuplicateLogins", "maskEmail"],
|
|
300
|
+
"cli",
|
|
301
|
+
);
|
|
298
302
|
const { decideStickySelection } = await importDist("sticky.js", ["decideStickySelection"]);
|
|
299
303
|
const io = {
|
|
300
304
|
readFile: (p) => readFileSync(expandHome(p), "utf8"),
|
|
@@ -322,6 +326,7 @@ const identityIo = {
|
|
|
322
326
|
defaultConfigDir: process.env.CLAUDE_CONFIG_DIR
|
|
323
327
|
? expandHome(process.env.CLAUDE_CONFIG_DIR)
|
|
324
328
|
: join(HOME, ".claude"),
|
|
329
|
+
homeDir: HOME,
|
|
325
330
|
};
|
|
326
331
|
const accounts = pluginConfig.accounts ?? [];
|
|
327
332
|
const identities = [];
|
|
@@ -334,7 +339,7 @@ for (const account of accounts) {
|
|
|
334
339
|
const identity = resolveAccountIdentity(account, identityIo);
|
|
335
340
|
identities.push(identity);
|
|
336
341
|
const source = account.native
|
|
337
|
-
? `native login, ${identityIo.defaultConfigDir}`
|
|
342
|
+
? `native login, ${identity.source ?? identityIo.defaultConfigDir}`
|
|
338
343
|
: account.configDir
|
|
339
344
|
? `config dir ${account.configDir}`
|
|
340
345
|
: account.oauthTokenFile
|