@drakon-systems/multi-clawd 1.9.1 → 1.9.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 +25 -2
- package/dist/history-identity.js +91 -0
- package/dist/index.js +52 -0
- package/openclaw.plugin.json +5 -1
- package/package.json +1 -1
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.2/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,9 +666,29 @@ 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
|
+
|
|
669
689
|
## How it works
|
|
670
690
|
|
|
671
|
-
|
|
691
|
+
Four moves, all through the official plugin SDK (details in
|
|
672
692
|
[`DESIGN.md`](./DESIGN.md)):
|
|
673
693
|
|
|
674
694
|
1. **`registerCliBackend`** mirrors the bundled `claude-cli` backend — same
|
|
@@ -681,6 +701,9 @@ Three moves, all through the official plugin SDK (details in
|
|
|
681
701
|
3. **`prepareExecution`** injects that account's own login
|
|
682
702
|
(`CLAUDE_CONFIG_DIR` + `CLAUDE_CODE_OAUTH_TOKEN`) into the child process
|
|
683
703
|
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).
|
|
684
707
|
|
|
685
708
|
## Legacy issue: idle backend eviction on OpenClaw ≤ 2026.7.1
|
|
686
709
|
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
export const IDENTITY_PROFILE_SUFFIX = "multi-clawd-identity";
|
|
2
|
+
export const HISTORY_IDENTITY_CONFIG_KEY = "historyIdentity";
|
|
3
|
+
export function identityProfileId(backendId) {
|
|
4
|
+
return `${backendId}:${IDENTITY_PROFILE_SUFFIX}`;
|
|
5
|
+
}
|
|
6
|
+
export function identityAccountId(backendId) {
|
|
7
|
+
return `multi-clawd:${backendId}`;
|
|
8
|
+
}
|
|
9
|
+
export function buildIdentityCredential(backendId) {
|
|
10
|
+
return {
|
|
11
|
+
type: "oauth",
|
|
12
|
+
provider: backendId,
|
|
13
|
+
accountId: identityAccountId(backendId),
|
|
14
|
+
displayName: `multi-clawd history identity for ${backendId} (a name, not a credential)`,
|
|
15
|
+
access: "",
|
|
16
|
+
refresh: "",
|
|
17
|
+
expires: 1,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
export function hasIdentity(existing, wanted) {
|
|
21
|
+
if (!existing || typeof existing !== "object")
|
|
22
|
+
return false;
|
|
23
|
+
const cred = existing;
|
|
24
|
+
return (cred.type === "oauth" &&
|
|
25
|
+
cred.provider === wanted.provider &&
|
|
26
|
+
cred.accountId === wanted.accountId &&
|
|
27
|
+
cred.email === undefined &&
|
|
28
|
+
cred.clientId === undefined &&
|
|
29
|
+
cred.enterpriseUrl === undefined &&
|
|
30
|
+
cred.projectId === undefined);
|
|
31
|
+
}
|
|
32
|
+
export async function ensureIdentityProfiles(backendIds, sdk) {
|
|
33
|
+
const result = { written: [], kept: [], failed: [] };
|
|
34
|
+
let profiles;
|
|
35
|
+
try {
|
|
36
|
+
profiles = sdk.ensureAuthProfileStore(undefined, { readOnly: true }).profiles;
|
|
37
|
+
}
|
|
38
|
+
catch (err) {
|
|
39
|
+
for (const id of backendIds) {
|
|
40
|
+
result.failed.push({ profileId: identityProfileId(id), reason: `store read failed: ${errorName(err)}` });
|
|
41
|
+
}
|
|
42
|
+
return result;
|
|
43
|
+
}
|
|
44
|
+
for (const id of backendIds) {
|
|
45
|
+
const profileId = identityProfileId(id);
|
|
46
|
+
const wanted = buildIdentityCredential(id);
|
|
47
|
+
if (hasIdentity(profiles[profileId], wanted)) {
|
|
48
|
+
result.kept.push(profileId);
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
try {
|
|
52
|
+
await sdk.upsertAuthProfileWithLock({ profileId, credential: wanted });
|
|
53
|
+
result.written.push(profileId);
|
|
54
|
+
}
|
|
55
|
+
catch (err) {
|
|
56
|
+
result.failed.push({ profileId, reason: `write failed: ${errorName(err)}` });
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return result;
|
|
60
|
+
}
|
|
61
|
+
export function describeEnsureResult(result) {
|
|
62
|
+
const parts = [];
|
|
63
|
+
if (result.written.length)
|
|
64
|
+
parts.push(`stored ${result.written.join(", ")}`);
|
|
65
|
+
if (result.kept.length)
|
|
66
|
+
parts.push(`kept ${result.kept.join(", ")}`);
|
|
67
|
+
for (const f of result.failed)
|
|
68
|
+
parts.push(`${f.profileId}: ${f.reason}`);
|
|
69
|
+
return parts.join("; ") || "nothing to do";
|
|
70
|
+
}
|
|
71
|
+
function errorName(err) {
|
|
72
|
+
if (err instanceof Error)
|
|
73
|
+
return `${err.name}: ${err.message}`.slice(0, 200);
|
|
74
|
+
return String(err).slice(0, 200);
|
|
75
|
+
}
|
|
76
|
+
export async function loadProviderAuthSdk() {
|
|
77
|
+
try {
|
|
78
|
+
const mod = (await import("openclaw/plugin-sdk/provider-auth"));
|
|
79
|
+
if (typeof mod.ensureAuthProfileStore !== "function" ||
|
|
80
|
+
typeof mod.upsertAuthProfileWithLock !== "function") {
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
ensureAuthProfileStore: mod.ensureAuthProfileStore,
|
|
85
|
+
upsertAuthProfileWithLock: mod.upsertAuthProfileWithLock,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
return undefined;
|
|
90
|
+
}
|
|
91
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -25,6 +25,7 @@ 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";
|
|
28
29
|
const CLI_FRESH_WATCHDOG_DEFAULTS = {
|
|
29
30
|
noOutputTimeoutRatio: 0.8,
|
|
30
31
|
minMs: 180_000,
|
|
@@ -400,6 +401,7 @@ export function buildBackend(account, execMode) {
|
|
|
400
401
|
resolveExecutionArgs: resolvePoolExecutionArgs,
|
|
401
402
|
sideQuestionToolMode: "disabled",
|
|
402
403
|
ownsNativeCompaction: true,
|
|
404
|
+
defaultAuthProfileId: identityProfileId(account.id),
|
|
403
405
|
config: {
|
|
404
406
|
command: process.execPath,
|
|
405
407
|
args: [SHIM_PATH, ...BASE_ARGS, ...permissionModeArgs(execMode)],
|
|
@@ -552,6 +554,12 @@ export default definePluginEntry({
|
|
|
552
554
|
api.registerProvider(buildCatalogProvider(normalized));
|
|
553
555
|
}
|
|
554
556
|
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
|
+
});
|
|
555
563
|
{
|
|
556
564
|
const logger = api.logger;
|
|
557
565
|
try {
|
|
@@ -580,6 +588,50 @@ export default definePluginEntry({
|
|
|
580
588
|
api.logger.info(`[multi-clawd] registered ${seen.size} backend(s)+provider(s): ${[...seen].join(", ")}`);
|
|
581
589
|
},
|
|
582
590
|
});
|
|
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
|
+
}
|
|
583
635
|
function readHealthState(accountId) {
|
|
584
636
|
try {
|
|
585
637
|
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.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",
|
|
@@ -205,6 +205,10 @@
|
|
|
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
|
+
},
|
|
208
212
|
"directRoute": {
|
|
209
213
|
"type": "object",
|
|
210
214
|
"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.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. Also imports those accounts' setup tokens into Hermes Agent's Anthropic credential pool.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|