@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 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.2.0/SETUP-AGENT.md
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
 
@@ -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
- accountId: m.accountId,
24
- profileId: m.profileId,
25
- verdict: classifyAccountHealth(params.readHealth(m.accountId), params.healthOptions, params.nowMs).verdict,
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]));
@@ -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] || a.index - b.index)
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);
@@ -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
- accountId: m.accountId,
249
- profileId: m.profileId,
250
- verdict: classifyAccountHealth(deps.readHealth(m.accountId), deps.healthOptions, nowMs).verdict,
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}`;
@@ -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(` Every Claude launch runs on the first account below its threshold;`);
92
- lines.push(` the home account reclaims work`);
93
- lines.push(` automatically once its window resets.`);
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
- ? ` pool is rotated onto ${model.stickyAccount} (returns home when the home window resets)`
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 choosePoolAccount(pool) {
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;
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "multi-clawd",
3
3
  "name": "multi-clawd",
4
- "version": "2.2.0",
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 and reclaims the pool when its usage window resets."
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.2.0",
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
- next = health.choosePoolAccount(verdicts);
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));
@@ -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(readJson(join(STATE_DIR, `${id}.json`)), healthOptions, now)
542
- .verdict,
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;