ccdeck 3.43.1 → 3.43.3

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.
@@ -1 +1 @@
1
- import"./app-ByCusSHX.js";(function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const e of document.querySelectorAll('link[rel="modulepreload"]'))i(e);new MutationObserver(e=>{for(const r of e)if(r.type==="childList")for(const o of r.addedNodes)o.tagName==="LINK"&&o.rel==="modulepreload"&&i(o)}).observe(document,{childList:!0,subtree:!0});function s(e){const r={};return e.integrity&&(r.integrity=e.integrity),e.referrerPolicy&&(r.referrerPolicy=e.referrerPolicy),e.crossOrigin==="use-credentials"?r.credentials="include":e.crossOrigin==="anonymous"?r.credentials="omit":r.credentials="same-origin",r}function i(e){if(e.ep)return;e.ep=!0;const r=s(e);fetch(e.href,r)}})();
1
+ import"./app-HpFVXK_g.js";(function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const e of document.querySelectorAll('link[rel="modulepreload"]'))i(e);new MutationObserver(e=>{for(const r of e)if(r.type==="childList")for(const o of r.addedNodes)o.tagName==="LINK"&&o.rel==="modulepreload"&&i(o)}).observe(document,{childList:!0,subtree:!0});function s(e){const r={};return e.integrity&&(r.integrity=e.integrity),e.referrerPolicy&&(r.referrerPolicy=e.referrerPolicy),e.crossOrigin==="use-credentials"?r.credentials="include":e.crossOrigin==="anonymous"?r.credentials="omit":r.credentials="same-origin",r}function i(e){if(e.ep)return;e.ep=!0;const r=s(e);fetch(e.href,r)}})();
@@ -77,8 +77,8 @@
77
77
  document.documentElement.setAttribute("data-density", density === "tightest" || density === "tighter" || density === "compact" || density === "comfortable" ? density : "default");
78
78
  })();
79
79
  </script>
80
- <script type="module" crossorigin src="/assets/index-CzIBjYsB.js"></script>
81
- <link rel="modulepreload" crossorigin href="/assets/app-ByCusSHX.js">
80
+ <script type="module" crossorigin src="/assets/index-DqxnkNs2.js"></script>
81
+ <link rel="modulepreload" crossorigin href="/assets/app-HpFVXK_g.js">
82
82
  <link rel="stylesheet" crossorigin href="/assets/app-DsHSufBt.css">
83
83
  </head>
84
84
  <body>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ccdeck",
3
- "version": "3.43.1",
3
+ "version": "3.43.3",
4
4
  "description": "Local dashboard for Claude Code and Codex CLI sessions: which Claude Code session needs input, every tool call and Claude Code subagent live, token cost and quota, several Claude accounts. Run npx ccdeck.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -77,7 +77,8 @@
77
77
  "3.39.4",
78
78
  "3.40.1",
79
79
  "3.41.3",
80
- "3.43.1"
80
+ "3.43.1",
81
+ "3.43.3"
81
82
  ],
82
83
  "3.43.0": [
83
84
  {
@@ -95,7 +95,7 @@ export function readingCurrent(account, models, now) {
95
95
 
96
96
  /**
97
97
  * Whether Auto-switch may land on this account: signed in, in rotation, not
98
- * the one another paired deck is working on, not one claude-swap has
98
+ * reserved by a local `cswap run` session, not one claude-swap has
99
99
  * quarantined, read recently, and comfortably under the threshold on every
100
100
  * window that counts. An account with no 5-hour or weekly window (an API key)
101
101
  * is never a target here; the engine still decides about those.
@@ -140,11 +140,12 @@ function soonestWeekly(accounts, ctx, before = null) {
140
140
  * somebody just set a home and expects to be taken there.
141
141
  * threshold the engine's, in percent
142
142
  * models countedModels(...) of the engine's model setting
143
- * busy keys of the accounts whose login is live somewhere else: on a
144
- * paired deck, or in a `cswap run` session
143
+ * busy keys of accounts reserved by local `cswap run` sessions;
144
+ * activity reported by other paired devices does not exclude them
145
145
  * quarantined keys of the accounts claude-swap's engine has quarantined
146
146
  * sinceMove ms since the last move, the engine's or this one's
147
- * cooldownMs the engine's cooldown, which every move here keeps too
147
+ * cooldownMs the engine's cooldown for proactive moves; fully exhausted
148
+ * windows can be escaped immediately
148
149
  * optional whether moves below the threshold may be made at all: only
149
150
  * while the engine itself is on `best`
150
151
  *
@@ -154,7 +155,7 @@ export function chooseAccount({ accounts, choice, threshold, models = [], busy =
154
155
  const next = choice?.next === "weekly" ? "weekly" : "room";
155
156
  const homeKey = choice?.home ?? null;
156
157
  if (next === "room" && !homeKey) return null;
157
- if (sinceMove < cooldownMs || !Number.isFinite(threshold)) return null;
158
+ if (!Number.isFinite(threshold)) return null;
158
159
 
159
160
  const active = accounts.find(a => a.active);
160
161
  // The engine reads the account it is on every minute or so; if the store
@@ -162,6 +163,9 @@ export function chooseAccount({ accounts, choice, threshold, models = [], busy =
162
163
  if (!active || !readingCurrent(active, models, now)) return null;
163
164
  const used = usedOf(active, models);
164
165
  if (used == null) return null;
166
+ // Like the engine, an exhausted window must be escapable immediately.
167
+ // Ordinary threshold moves and optional returns still keep the cooldown.
168
+ if (sinceMove < cooldownMs && used < 100) return null;
165
169
 
166
170
  const ctx = { threshold, models, busy, quarantined, now };
167
171
  const home = homeKey ? accounts.find(a => a.key === homeKey) ?? null : null;
@@ -30,7 +30,7 @@ import { backupRoot, fetchClaudeAccounts, slotNumber, switchClaudeAccount } from
30
30
  import { readCswapConfig } from "./cswap-auto-readers.mjs";
31
31
  import { accountKey } from "./lan-copies.mjs";
32
32
  import { isProcessAlive } from "./deck-probe.mjs";
33
- import { chooseAccount, countedModels, NEXT_CHOICES } from "./account-choice.mjs";
33
+ import { chooseAccount, countedModels, readingCurrent, NEXT_CHOICES } from "./account-choice.mjs";
34
34
 
35
35
  /** Where the choice is kept: beside prefs.json, in the deck's own data. */
36
36
  export function choicePath(home = deckDataDir()) {
@@ -146,26 +146,6 @@ async function engineState(accounts) {
146
146
  };
147
147
  }
148
148
 
149
- /** The accounts paired decks have recently said they are working on.
150
- * One login live on two machines races for every renewal (README, "One login
151
- * live in one place"), so a tick never moves onto one of them. The LAN table
152
- * retains old manifests for display after a peer goes quiet; those are not
153
- * current reservations. Use the authenticated manifest's receipt time, not a
154
- * beacon or a failed round, which cannot confirm the account still in use. */
155
- async function accountsOnOtherDecks(now) {
156
- try {
157
- const [{ lanEngine }, { PRESENT_MS }] = await Promise.all([
158
- import("./lan-deck.mjs"), import("./lan-sync.mjs"),
159
- ]);
160
- const peers = lanEngine.status()?.peers ?? [];
161
- return new Set(peers
162
- .filter(p => p?.paired === true && Number.isFinite(p.offers?.at) && p.offers.at > 0 && now - p.offers.at < PRESENT_MS)
163
- .map(p => p.offers.current?.key).filter(k => typeof k === "string"));
164
- } catch {
165
- return new Set();
166
- }
167
- }
168
-
169
149
  /** The name claude-swap gives an account's `cswap run` profile after its
170
150
  * slot number: the email, NFC, with anything but ASCII letters, digits and
171
151
  * `._-` turned into `_` (claude_swap/session.py, slugify_email). */
@@ -219,7 +199,7 @@ const refOf = a => (a ? { number: a.num, email: a.email ?? null } : null);
219
199
  * roster it decided on, which noteEngineTick needs to name where the engine
220
200
  * went, and null when nothing is set.
221
201
  */
222
- export async function chooseBeforeEngine({ now = Date.now() } = {}) {
202
+ export async function chooseBeforeEngine({ now = Date.now(), refreshHome = true } = {}) {
223
203
  const choice = await readChoice();
224
204
  if (choice.next === "room" && !choice.home) return { result: null, accounts: null };
225
205
  const roster = await fetchClaudeAccounts();
@@ -244,26 +224,39 @@ export async function chooseBeforeEngine({ now = Date.now() } = {}) {
244
224
  const threshold = numberOr(cfg["autoswitch.threshold"]?.value, 90);
245
225
  const models = countedModels(cfg["autoswitch.model"]?.value);
246
226
  const engine = await engineState(accounts);
227
+ const lastMoveAt = Math.max(choice.lastMoveAt, engine.lastSwitchAt);
228
+ const cooldownMs = numberOr(cfg["autoswitch.cooldownSeconds"]?.value, 300) * 1000;
229
+ const cooldownUntil = lastMoveAt + cooldownMs;
247
230
  const pick = chooseAccount({
248
231
  accounts,
249
232
  choice: { next: choice.next, home: choice.home?.key ?? null, manual, goHome },
250
233
  threshold,
251
234
  models,
252
- busy: new Set([...await accountsOnOtherDecks(now), ...await accountsInCswapRun(accounts)]),
235
+ busy: await accountsInCswapRun(accounts),
253
236
  quarantined: engine.quarantined,
254
- sinceMove: now - Math.max(choice.lastMoveAt, engine.lastSwitchAt),
255
- cooldownMs: numberOr(cfg["autoswitch.cooldownSeconds"]?.value, 300) * 1000,
237
+ sinceMove: now - lastMoveAt,
238
+ cooldownMs,
256
239
  optional: (cfg["autoswitch.strategy"]?.value ?? "best") === "best",
257
240
  now,
258
241
  });
259
242
  if (!pick) {
260
243
  await updateChoice(keep);
261
- return { result: null, accounts };
244
+ return { result: null, accounts, cooldownUntil };
245
+ }
246
+ // Give Home one collection pass before a fresh weekly candidate can
247
+ // preempt it. The loop retries with refreshHome=false after its dry run:
248
+ // a failed/backed-off collection must not prevent leaving a full account.
249
+ const home = choice.home ? accounts.find(a => a.key === choice.home.key) : null;
250
+ if (refreshHome && pick.trigger !== "home" && home && !home.active &&
251
+ !home.disabled && home.alive !== false && !home.error && !home.reauth &&
252
+ !readingCurrent(home, models, now)) {
253
+ await updateChoice(keep);
254
+ return { result: null, accounts, cooldownUntil };
262
255
  }
263
256
  const moved = await switchClaudeAccount(pick.to.num);
264
257
  if (!moved.ok) {
265
258
  await updateChoice(keep);
266
- return { result: null, accounts };
259
+ return { result: null, accounts, cooldownUntil };
267
260
  }
268
261
  // The move happened, so where it left the user and when are true whatever
269
262
  // was set meanwhile; a home still to be gone to is the current choice's.
@@ -116,8 +116,20 @@ async function runAutoTick() {
116
116
  if (!_enabled) return { skipped: "disabled" };
117
117
  if (await externalAutoRunning()) return { skipped: "external-engine" };
118
118
  invalidateClaudeAccountsCache();
119
- chose = await chooseBeforeEngine().catch(() => ({ result: null, accounts: null }));
119
+ chose = await chooseBeforeEngine({ refreshHome: false }).catch(() => ({ result: null, accounts: null }));
120
120
  if (chose.result) return { chose: chose.result };
121
+ // A deck-managed `cswap switch` does not advance the engine's own
122
+ // lastSwitchAt. Its fallback must keep our cooldown too, or a manual
123
+ // return to the full account can immediately escape the weekly rule
124
+ // through the engine's most-room choice. Still allow emergency exits.
125
+ const decision = summarise(preview.stdout);
126
+ if (Date.now() < chose.cooldownUntil && decision.trigger !== "at-limit" && decision.trigger !== "failover") {
127
+ return { chose: {
128
+ ok: true, ...decision, event: "no-switch", switched: false,
129
+ trigger: null, reason: "cooldown", from: null, to: null,
130
+ detail: "Waiting for the account-switch cooldown",
131
+ } };
132
+ }
121
133
  }
122
134
  const out = await run(await cswapBin(), ["auto", "--once", "--json"], { timeout: TICK_TIMEOUT_MS });
123
135
  return { ...out, accounts: chose.accounts };