agent-dag 1.33.28 → 1.33.30

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.
@@ -5,7 +5,7 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1" />
6
6
  <title>agents-deck</title>
7
7
  <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Ctext y='84' font-size='84'%3E%E2%97%89%3C/text%3E%3C/svg%3E" />
8
- <script type="module" crossorigin src="/assets/index-Bl3jQiYb.js"></script>
8
+ <script type="module" crossorigin src="/assets/index-DmDKN8d2.js"></script>
9
9
  <link rel="stylesheet" crossorigin href="/assets/index-Cpi89XJ8.css">
10
10
  </head>
11
11
  <body>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-dag",
3
- "version": "1.33.28",
3
+ "version": "1.33.30",
4
4
  "description": "Live deck of Claude Code and Codex agents — watch parallel subagents fork, call tools, and return on one calm canvas. Also available as npx ccdeck and npx agent-dag.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -16,6 +16,7 @@
16
16
  // concurrent adds pick the same number and the second write silently drops the
17
17
  // first account's record. Nothing upstream prevents it, so every mutation here
18
18
  // goes through one mutex.
19
+ import { AsyncLocalStorage } from "node:async_hooks";
19
20
  import { readFile } from "node:fs/promises";
20
21
  import { homedir } from "node:os";
21
22
  import { join } from "node:path";
@@ -49,15 +50,28 @@ function backupRoot() {
49
50
 
50
51
  let _chain = Promise.resolve();
51
52
 
53
+ // Whether the code running right now is itself the mutation holding the lock.
54
+ // Async context rather than a plain boolean, which could not tell that apart
55
+ // from another request that merely arrived while the lock was held — and would
56
+ // wave that one through, which is the opposite of a mutex.
57
+ const _holder = new AsyncLocalStorage();
58
+
52
59
  /**
53
60
  * One store mutation at a time.
54
61
  *
55
62
  * Not defence against another process — that would need claude-swap's own file
56
63
  * lock, which `add` does not take either. This is defence against ourselves:
57
64
  * two browser tabs, or a double-click, are enough to race a slot assignment.
65
+ *
66
+ * Re-entrant, because a mutation that reaches for the lock from inside one
67
+ * would otherwise wait for itself forever: the chain cannot advance past the
68
+ * outer link until it settles, and the outer link is blocked on this call. It
69
+ * already has exclusive access, so it simply runs.
58
70
  */
59
71
  export function withStoreLock(fn) {
60
- const next = _chain.then(fn, fn);
72
+ if (_holder.getStore()) return Promise.resolve().then(fn);
73
+ const held = () => _holder.run(true, fn);
74
+ const next = _chain.then(held, held);
61
75
  // Keep the chain alive even when a link rejects, or every later mutation
62
76
  // inherits the failure.
63
77
  _chain = next.then(() => {}, () => {});
@@ -329,13 +343,26 @@ export async function submitLoginCode(code) {
329
343
  export async function cancelLogin() {
330
344
  const flow = _login;
331
345
  if (!flow) return { ok: true, ...loginState() };
346
+ // Stopping the child touches no file, so it happens straight away: a cancel
347
+ // that waited its turn first would leave `claude auth login` sitting on the
348
+ // user's next attempt for the rest of its five minutes.
332
349
  try { flow.child?.kill(); } catch { /* already gone */ }
333
- // The login may have completed before the cancel arrived, in which case the
334
- // live credentials already moved and putting them back is the point.
335
- await restoreActive(flow.previousActive);
336
- invalidateClaudeAccountsCache();
337
- _login = null;
338
- return { ok: true, ...loginState() };
350
+ // Switching back does touch the store, so it is queued like every other
351
+ // mutation. Cancel is reachable while submitLoginCode holds the lock — the
352
+ // dialog fires it on Escape, mid-registration — and running `cswap switch`
353
+ // beside an in-flight `cswap add` is precisely the unlocked read-modify-write
354
+ // of sequence.json the mutex exists to prevent: claude-swap takes no file lock
355
+ // around `add`, so whichever write lands second drops the other's record.
356
+ return withStoreLock(async () => {
357
+ // The login may have completed before the cancel arrived, in which case the
358
+ // live credentials already moved and putting them back is the point.
359
+ await restoreActive(flow.previousActive);
360
+ invalidateClaudeAccountsCache();
361
+ // A newer sign-in can have started while this waited its turn; clearing the
362
+ // slot then would drop a live flow's handle instead of this dead one's.
363
+ if (flow === _login) _login = null;
364
+ return { ok: true, ...loginState() };
365
+ });
339
366
  }
340
367
 
341
368
  /** Put the account that was active before the login back in front. */
@@ -238,13 +238,15 @@ function stripAnsi(s) {
238
238
  */
239
239
  // Parse "Jun 18, 4:09pm" (local time, no tz) into unix seconds.
240
240
  // Claude shows times in the user's local timezone, so parsing as local is correct.
241
- function parseResetToSec(resetStr) {
241
+ export function parseResetToSec(resetStr) {
242
242
  if (!resetStr) return null;
243
243
  try {
244
244
  const year = new Date().getFullYear();
245
- // "4:09pm" → "4:09 PM" so Date.parse handles it
245
+ // "4:09pm" → "4:09 PM" so Date.parse handles it. Minutes are optional in
246
+ // the CLI's output ("9am"); Date.parse rejects "9 AM", so supply ":00".
246
247
  const norm = resetStr
247
- .replace(/(\d{1,2}:\d{2})(am|pm)/i, "$1 $2")
248
+ .replace(/(\d{1,2})(?::(\d{2}))?\s*(am|pm)/i,
249
+ (_all, h, mm, ampm) => `${h}:${mm ?? "00"} ${ampm}`)
248
250
  .trim();
249
251
  const d = new Date(`${norm} ${year}`);
250
252
  return isNaN(d.getTime()) ? null : Math.floor(d.getTime() / 1000);