agent-dag 3.8.3 → 3.8.4
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/dist/web/index.html
CHANGED
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
document.documentElement.setAttribute("data-theme", stored === "light" ? "light" : "dark");
|
|
41
41
|
})();
|
|
42
42
|
</script>
|
|
43
|
-
<script type="module" crossorigin src="/assets/index-
|
|
43
|
+
<script type="module" crossorigin src="/assets/index-DkJt3T9U.js"></script>
|
|
44
44
|
<link rel="stylesheet" crossorigin href="/assets/index-ByAgTqB8.css">
|
|
45
45
|
</head>
|
|
46
46
|
<body>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-dag",
|
|
3
|
-
"version": "3.8.
|
|
3
|
+
"version": "3.8.4",
|
|
4
4
|
"description": "Live deck of Claude Code and Codex agents — watch tool calls, token spend and every Claude Code subagent on one calm canvas. Run it with npx ccdeck.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/release-notes.json
CHANGED
|
@@ -31,6 +31,12 @@
|
|
|
31
31
|
"about a defect in the type, and the suite refuses both it and no space at",
|
|
32
32
|
"all."
|
|
33
33
|
],
|
|
34
|
+
"3.8.4": [
|
|
35
|
+
{
|
|
36
|
+
"title": "\ud83e\uddf7 Your own hooks can no longer be eaten by two decks starting together",
|
|
37
|
+
"body": "If you keep hooks of your own in `settings.json`, the deck parks them while it installs and puts them back afterwards. Two decks starting at the same moment could interleave inside that, and the loser wrote a version of the file computed before the restore \u2014 so your hook was gone from `settings.json` and gone from the park, which was its only other copy.\n\nThere was already a check for this, and it had a hole: if the last-moment re-read of the file failed, it read that as \"nothing changed\" and wrote anyway. A failed read is the one moment that check was most needed, because the usual reason a file cannot be read for a few milliseconds is that something else has just written it.\n\n`ccdeck --uninstall` had no such check at all and could overwrite a deck that was starting, then exit 0 saying it had succeeded. It has one now, and when it declines it says so and tells you to run it again rather than reporting that there was nothing to remove."
|
|
38
|
+
}
|
|
39
|
+
],
|
|
34
40
|
"3.8.3": [
|
|
35
41
|
{
|
|
36
42
|
"title": "\ud83e\ude9f Windows: two settings files that could refuse to save",
|
package/src/server/installer.mjs
CHANGED
|
@@ -528,8 +528,27 @@ export async function installHooks({ provider = "claude", beforeWrite = null } =
|
|
|
528
528
|
// real fs work, so a test that raced it by wall clock would pass or fail by
|
|
529
529
|
// how fast the machine is. Production passes nothing.
|
|
530
530
|
if (beforeWrite) await beforeWrite();
|
|
531
|
-
|
|
532
|
-
|
|
531
|
+
// A READ THE GUARD COULD NOT PERFORM IS NOT PROOF NOTHING CHANGED (#788).
|
|
532
|
+
// This used to be `.catch(() => ({ raw: before }))`, which substituted the
|
|
533
|
+
// snapshot and so answered "unchanged" for every failed re-read. ENOENT is
|
|
534
|
+
// the one case that substitution would be right for, and it does not reach
|
|
535
|
+
// here at all — readSettingsForWrite returns `{ raw: null }` for it without
|
|
536
|
+
// throwing. What does reach here is EACCES/EBUSY on a file written
|
|
537
|
+
// microseconds ago, which is the condition this module's own header names:
|
|
538
|
+
// "a virus scanner or the search indexer opens files the instant they are
|
|
539
|
+
// written, so the target is briefly untouchable on a perfectly healthy
|
|
540
|
+
// machine". Precisely when another deck has just written it.
|
|
541
|
+
//
|
|
542
|
+
// So an unreadable re-read declines, like a changed one. Declining is safe
|
|
543
|
+
// for the reason above: every boot reinstalls and the next pass converges.
|
|
544
|
+
// Being wrong the other way is not — it is the lost update this guard
|
|
545
|
+
// exists to prevent, with the user's own sound hook gone from settings.json
|
|
546
|
+
// and from the park that was its only other copy.
|
|
547
|
+
let onDisk;
|
|
548
|
+
let unreadable = false;
|
|
549
|
+
try { ({ raw: onDisk } = await readSettingsForWrite(cfg.settingsPath)); }
|
|
550
|
+
catch { unreadable = true; }
|
|
551
|
+
if (unreadable || onDisk !== before) {
|
|
533
552
|
return {
|
|
534
553
|
settingsPath: cfg.settingsPath, hookPath, events: cfg.events, provider,
|
|
535
554
|
changed: false, raced: true, retire: { ...retire, pending: false },
|
|
@@ -572,12 +591,13 @@ export async function installHooks({ provider = "claude", beforeWrite = null } =
|
|
|
572
591
|
* quietly do nothing. Only ENOENT is genuinely empty, and readSettingsForWrite
|
|
573
592
|
* already answers that with `{}`, which falls through to `changed: false`.
|
|
574
593
|
*/
|
|
575
|
-
export async function uninstallHooks({ provider = "claude" } = {}) {
|
|
594
|
+
export async function uninstallHooks({ provider = "claude", beforeWrite = null } = {}) {
|
|
576
595
|
const cfg = PROVIDERS[provider];
|
|
577
596
|
if (!cfg) throw new Error(`unknown provider: ${provider}`);
|
|
578
597
|
let current;
|
|
598
|
+
let before;
|
|
579
599
|
try {
|
|
580
|
-
({ settings: current } = await readSettingsForWrite(cfg.settingsPath));
|
|
600
|
+
({ settings: current, raw: before } = await readSettingsForWrite(cfg.settingsPath));
|
|
581
601
|
} catch (err) {
|
|
582
602
|
if (err?.code !== "SETTINGS_UNREADABLE") throw err;
|
|
583
603
|
// Same shape retireSoundHook answers with, so bin/deck.js reports both
|
|
@@ -601,7 +621,43 @@ export async function uninstallHooks({ provider = "claude" } = {}) {
|
|
|
601
621
|
if (cleaned.length === 0) delete current.hooks[evt];
|
|
602
622
|
else current.hooks[evt] = cleaned;
|
|
603
623
|
}
|
|
604
|
-
if (changed)
|
|
624
|
+
if (changed) {
|
|
625
|
+
// THE SAME GUARD ITS SIBLING HAS, for the same file (#788). `installHooks`
|
|
626
|
+
// grew a compare-against-the-file check at the last moment and this
|
|
627
|
+
// function — which rewrites the same settings.json from a snapshot read
|
|
628
|
+
// just as long ago — never got one.
|
|
629
|
+
//
|
|
630
|
+
// The race is `ccdeck --uninstall` against a deck that is starting.
|
|
631
|
+
// Uninstall reads settings.json with the user's own hooks still parked. In
|
|
632
|
+
// the window before its write — resolveWriteTarget, temp create, write,
|
|
633
|
+
// fsync, stat, rename — the booting deck restores those hooks and deletes
|
|
634
|
+
// the park. Uninstall then writes its stale object over the restore,
|
|
635
|
+
// bin/deck.js re-reads the clobbered file, readParked hits ENOENT, and it
|
|
636
|
+
// reports `restored: 0`. The hooks are gone from both copies and the CLI
|
|
637
|
+
// exits 0 saying the uninstall succeeded.
|
|
638
|
+
//
|
|
639
|
+
// Declining here is not as cheap as declining an install — nothing retries
|
|
640
|
+
// an uninstall — so it says so in the result rather than answering `ok`
|
|
641
|
+
// with `changed: false`, which would read as "there was nothing to remove".
|
|
642
|
+
// The seam the suite needs, and `installHooks` states the argument for it:
|
|
643
|
+
// the window this guard covers is filled with real fs work, so a test that
|
|
644
|
+
// raced it by wall clock would pass or fail by how fast the machine is.
|
|
645
|
+
// Production passes nothing.
|
|
646
|
+
if (beforeWrite) await beforeWrite();
|
|
647
|
+
let onDisk;
|
|
648
|
+
let unreadable = false;
|
|
649
|
+
try { ({ raw: onDisk } = await readSettingsForWrite(cfg.settingsPath)); }
|
|
650
|
+
catch { unreadable = true; }
|
|
651
|
+
if (unreadable || onDisk !== before) {
|
|
652
|
+
return {
|
|
653
|
+
ok: false, reason: "raced", changed: false, provider,
|
|
654
|
+
settingsPath: cfg.settingsPath,
|
|
655
|
+
why: "another writer changed settings.json while the uninstall was running",
|
|
656
|
+
message: `${cfg.settingsPath} changed while uninstalling — nothing was removed. Run --uninstall again.`,
|
|
657
|
+
};
|
|
658
|
+
}
|
|
659
|
+
await writeFileAtomic(cfg.settingsPath, JSON.stringify(current, null, 2) + "\n");
|
|
660
|
+
}
|
|
605
661
|
return { ok: true, changed, provider, settingsPath: cfg.settingsPath };
|
|
606
662
|
}
|
|
607
663
|
|