@drakon-systems/multi-clawd 1.8.5 → 1.8.6

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 CHANGED
@@ -367,7 +367,7 @@ openclaw plugins install (Get-Location).Path
367
367
  **Or let your agent install it.** Running an OpenClaw assistant or Claude
368
368
  Code on the target machine already? Paste it this and go make coffee:
369
369
 
370
- > Read https://raw.githubusercontent.com/Drakon-Systems-Ltd/multi-clawd/v1.8.5/SETUP-AGENT.md
370
+ > Read https://raw.githubusercontent.com/Drakon-Systems-Ltd/multi-clawd/v1.8.6/SETUP-AGENT.md
371
371
  > and follow it to set up multi-clawd on this machine. I own a second
372
372
  > Claude account — ask me when you need me to log in.
373
373
 
@@ -0,0 +1,125 @@
1
+ import { existsSync, readFileSync, readdirSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ export const SQLITE_STORE_RELPATH = join("agent", "openclaw-agent.sqlite");
4
+ export const JSON_STORE_RELPATH = join("sessions", "sessions.json");
5
+ export function locateSessionStore(agentDir, exists = existsSync, list = readdirSync) {
6
+ const sqlitePath = join(agentDir, SQLITE_STORE_RELPATH);
7
+ if (exists(sqlitePath))
8
+ return { kind: "sqlite", path: sqlitePath };
9
+ const jsonPath = join(agentDir, JSON_STORE_RELPATH);
10
+ if (exists(jsonPath))
11
+ return { kind: "json", path: jsonPath };
12
+ const sessionsDir = join(agentDir, "sessions");
13
+ if (exists(sessionsDir) && holdsSessionArtifacts(sessionsDir, list))
14
+ return { kind: "missing", path: jsonPath };
15
+ return null;
16
+ }
17
+ function holdsSessionArtifacts(sessionsDir, list) {
18
+ let names;
19
+ try {
20
+ names = list(sessionsDir);
21
+ }
22
+ catch {
23
+ return false;
24
+ }
25
+ return names.some((n) => n.endsWith(".json") || n.endsWith(".jsonl") || !n.includes("."));
26
+ }
27
+ async function defaultOpener() {
28
+ const { DatabaseSync } = await importSqliteQuietly();
29
+ return (path) => new DatabaseSync(path, { readOnly: true });
30
+ }
31
+ async function importSqliteQuietly() {
32
+ const original = process.emitWarning;
33
+ const quiet = (warning, ...rest) => {
34
+ const text = typeof warning === "string" ? warning : warning.message;
35
+ if (text.includes("SQLite is an experimental feature"))
36
+ return;
37
+ return original.call(process, warning, ...rest);
38
+ };
39
+ process.emitWarning = quiet;
40
+ try {
41
+ return await import("node:sqlite");
42
+ }
43
+ finally {
44
+ process.emitWarning = original;
45
+ }
46
+ }
47
+ export async function readSqliteSessionStore(path, open) {
48
+ let opener;
49
+ try {
50
+ opener = open ?? (await defaultOpener());
51
+ }
52
+ catch (err) {
53
+ return {
54
+ error: `node:sqlite unavailable on this runtime (${describe(err)}) — Node ≥ 22.13 required to audit a SQLite session store`,
55
+ skippedRows: 0,
56
+ };
57
+ }
58
+ let db;
59
+ try {
60
+ db = opener(path);
61
+ const rows = db.prepare("SELECT session_key, entry_json FROM session_nodes").all();
62
+ const entries = {};
63
+ let skippedRows = 0;
64
+ for (const row of rows) {
65
+ const parsed = parseRow(row);
66
+ if (parsed === undefined) {
67
+ skippedRows++;
68
+ continue;
69
+ }
70
+ entries[parsed.key] = parsed.entry;
71
+ }
72
+ return { entries, skippedRows };
73
+ }
74
+ catch (err) {
75
+ return { error: describe(err), skippedRows: 0 };
76
+ }
77
+ finally {
78
+ try {
79
+ db?.close();
80
+ }
81
+ catch {
82
+ }
83
+ }
84
+ }
85
+ export function readJsonSessionStore(path) {
86
+ try {
87
+ const parsed = JSON.parse(readFileSync(path, "utf8"));
88
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
89
+ return { error: "store is not a JSON object", skippedRows: 0 };
90
+ }
91
+ return { entries: parsed, skippedRows: 0 };
92
+ }
93
+ catch (err) {
94
+ return { error: describe(err), skippedRows: 0 };
95
+ }
96
+ }
97
+ export async function readSessionStore(location, open) {
98
+ switch (location.kind) {
99
+ case "sqlite":
100
+ return readSqliteSessionStore(location.path, open);
101
+ case "json":
102
+ return readJsonSessionStore(location.path);
103
+ case "missing":
104
+ return { error: "no session store found (neither SQLite database nor sessions.json)", skippedRows: 0 };
105
+ }
106
+ }
107
+ function parseRow(row) {
108
+ if (!row || typeof row !== "object")
109
+ return undefined;
110
+ const { session_key: key, entry_json: json } = row;
111
+ if (typeof key !== "string" || typeof json !== "string")
112
+ return undefined;
113
+ try {
114
+ const entry = JSON.parse(json);
115
+ if (!entry || typeof entry !== "object" || Array.isArray(entry))
116
+ return undefined;
117
+ return { key, entry: entry };
118
+ }
119
+ catch {
120
+ return undefined;
121
+ }
122
+ }
123
+ function describe(err) {
124
+ return err instanceof Error ? err.message : String(err);
125
+ }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "multi-clawd",
3
3
  "name": "multi-clawd",
4
- "version": "1.8.5",
4
+ "version": "1.8.6",
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.5",
3
+ "version": "1.8.6",
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
@@ -222,21 +222,24 @@ async function chain(args = []) {
222
222
  );
223
223
  }
224
224
 
225
- // Session pins, across every agent's session store.
225
+ // Session pins, across every agent's session store (2026.8.x SQLite database
226
+ // or the older sessions.json — src/session-store.ts owns the where/how).
226
227
  const sessionFindings = [];
227
228
  const agentsDir = join(homedir(), ".openclaw", "agents");
228
229
  try {
230
+ const store = await import(resolve(__dirname, "..", "dist", "session-store.js"));
229
231
  for (const agent of readdirSync(agentsDir)) {
230
- const p = join(agentsDir, agent, "sessions", "sessions.json");
231
- if (!existsSync(p)) continue;
232
- try {
233
- sessionFindings.push(...ca.auditSessionOverrides(JSON.parse(rf(p, "utf8")), Boolean(poolId)));
234
- } catch {
235
- /* unreadable store — skip, doctor reports install health */
232
+ const location = store.locateSessionStore(join(agentsDir, agent));
233
+ if (!location) continue;
234
+ const read = await store.readSessionStore(location);
235
+ if (!read.entries) {
236
+ console.log(`${DIM} (session store ${location.path} not audited: ${read.error} — see doctor)${RESET}`);
237
+ continue;
236
238
  }
239
+ sessionFindings.push(...ca.auditSessionOverrides(read.entries, Boolean(poolId)));
237
240
  }
238
241
  } catch {
239
- /* no agents dir */
242
+ /* no agents dir, or dist not built — doctor reports install health */
240
243
  }
241
244
  section("SESSION PINS", sessionFindings, (f) =>
242
245
  raw ? f.surface : f.surface.replace(/^session (.*)$/, (_, k) => `session ${ca.maskSessionKey(k)}`),
@@ -393,16 +393,20 @@ if (!pool) {
393
393
  if (warns.length === 0) ok(`effective chain: all live Claude tiers route through the ${poolId} pool`);
394
394
 
395
395
  // ── case 2: session-level /model overrides ─────────────────────────────────
396
- // Enumerate agents/*/sessions/sessions.json. A store that is expected (its
397
- // agent has a sessions/ dir) but absent/unparseable gets a LOUD skip — never
398
- // a silent pass; a missed off-pool pin is worse than an extra line.
396
+ // Enumerate every agent's session store — the 2026.8.x per-agent SQLite
397
+ // database, or the older sessions.json map (src/session-store.ts owns the
398
+ // where and the how). A store that is expected but absent/unreadable gets a
399
+ // LOUD skip — never a silent pass; a missed off-pool pin is worse than an
400
+ // extra line.
401
+ const { locateSessionStore, readSessionStore } = await import(
402
+ join(EXT_DIR, "dist", "session-store.js")
403
+ ).catch(() => import(join(REPO_DIR, "dist", "session-store.js")));
399
404
  const AGENTS_DIR = join(HOME, ".openclaw", "agents");
400
405
  const stores = [];
401
406
  try {
402
407
  for (const agent of readdirSync(AGENTS_DIR)) {
403
- const sessionsDir = join(AGENTS_DIR, agent, "sessions");
404
- if (!existsSync(sessionsDir)) continue; // not an agent-with-sessions
405
- stores.push(join(sessionsDir, "sessions.json"));
408
+ const location = locateSessionStore(join(AGENTS_DIR, agent));
409
+ if (location) stores.push(location);
406
410
  }
407
411
  } catch {
408
412
  /* no agents dir at all */
@@ -412,14 +416,17 @@ if (!pool) {
412
416
  } else {
413
417
  let sessionWarns = 0;
414
418
  let readable = 0;
415
- for (const storePath of stores) {
416
- const parsed = readJson(storePath);
417
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
418
- warn(`session-override check SKIPPED for ${storePath} (store unreadable)`);
419
+ for (const location of stores) {
420
+ const read = await readSessionStore(location);
421
+ if (!read.entries) {
422
+ warn(`session-override check SKIPPED for ${location.path} (${read.error})`);
419
423
  continue;
420
424
  }
421
425
  readable++;
422
- const sessionFindings = auditSessionOverrides(parsed, true);
426
+ if (read.skippedRows > 0) {
427
+ warn(`${location.path}: ${read.skippedRows} session row(s) unparseable — those sessions were not audited`);
428
+ }
429
+ const sessionFindings = auditSessionOverrides(read.entries, true);
423
430
  for (const f of sessionFindings) {
424
431
  warn(`${renderSurface(f.surface)}: ${f.ref} ${f.reason}`);
425
432
  sessionWarns++;