agent-dag 3.8.2 → 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,18 @@
|
|
|
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
|
+
],
|
|
40
|
+
"3.8.3": [
|
|
41
|
+
{
|
|
42
|
+
"title": "\ud83e\ude9f Windows: two settings files that could refuse to save",
|
|
43
|
+
"body": "On Windows a file cannot be moved into place while anything else has it open, and Defender and the search indexer open a file the instant it is written. The deck has had a retry for that for a long time, and two files written in the last few days were not using it: the notifications preference, and Browser Watch's episode archive.\n\nWhen it bit, the notifications switch silently did not stick, and Browser Watch answered an error and showed an empty panel. Both now retry the way everything else in the deck already did.\n\nNothing changes on macOS or Linux, where the operating system never refused the move in the first place \u2014 which is why this got past a green test suite on both."
|
|
44
|
+
}
|
|
45
|
+
],
|
|
34
46
|
"3.8.2": [
|
|
35
47
|
{
|
|
36
48
|
"title": "\u2328\ufe0f A closed-behind-your-back dialog no longer kills every keyboard shortcut",
|
|
@@ -17,7 +17,10 @@
|
|
|
17
17
|
// `~/.claude/agent-dag`: readLiveDecks() reads every `.json` in that directory
|
|
18
18
|
// and would have to keep skipping this one forever. A subdirectory is not a
|
|
19
19
|
// name it can collide with.
|
|
20
|
-
import { appendFile, mkdir, readFile,
|
|
20
|
+
import { appendFile, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
21
|
+
// The rename, with the Windows retry ladder installer.mjs wrote for exactly
|
|
22
|
+
// this call. See the note over `writeNow` (#786).
|
|
23
|
+
import { renameWithRetry } from "./installer.mjs";
|
|
21
24
|
import { join } from "node:path";
|
|
22
25
|
import { claudeConfigDir } from "./claude-dir.mjs";
|
|
23
26
|
|
|
@@ -258,7 +261,12 @@ export async function writeStore(state, home = claudeConfigDir(), deps = {}) {
|
|
|
258
261
|
async function writeNow(state, home, deps) {
|
|
259
262
|
const mk = deps.mkdir ?? mkdir;
|
|
260
263
|
const write = deps.writeFile ?? writeFile;
|
|
261
|
-
|
|
264
|
+
// `renameWithRetry`, not `rename` (#786). Same Windows rule as deck-prefs,
|
|
265
|
+
// and the stakes are higher here: none of the three writers catches the
|
|
266
|
+
// throw, so a refused rename 500s `GET /api/browser-watch` and the panel goes
|
|
267
|
+
// blank, while the episode archive — the file that exists BECAUSE an intruder
|
|
268
|
+
// can clear the browser's own history — is not written at all.
|
|
269
|
+
const mv = deps.rename ?? renameWithRetry;
|
|
262
270
|
await mk(storeDir(home), { recursive: true });
|
|
263
271
|
const body = JSON.stringify({
|
|
264
272
|
v: STORE_VERSION,
|
|
@@ -26,7 +26,10 @@
|
|
|
26
26
|
// temp file, rename — because the alternative is a truncated JSON document as
|
|
27
27
|
// the only record of what the user chose, and a corrupt file here silently
|
|
28
28
|
// turns the notifications back on.
|
|
29
|
-
import { mkdir, readFile,
|
|
29
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
30
|
+
// The rename, with the Windows retry ladder installer.mjs wrote for exactly
|
|
31
|
+
// this call. See the note over the write below (#786).
|
|
32
|
+
import { renameWithRetry } from "./installer.mjs";
|
|
30
33
|
import { join } from "node:path";
|
|
31
34
|
import { claudeConfigDir } from "./claude-dir.mjs";
|
|
32
35
|
|
|
@@ -87,7 +90,13 @@ export async function writePrefs(patch, home = claudeConfigDir(), deps = {}) {
|
|
|
87
90
|
const job = async () => {
|
|
88
91
|
const mk = deps.mkdir ?? mkdir;
|
|
89
92
|
const write = deps.writeFile ?? writeFile;
|
|
90
|
-
|
|
93
|
+
// `renameWithRetry`, not `rename` (#786). MoveFileExW refuses while any
|
|
94
|
+
// handle without FILE_SHARE_DELETE is open on either side, and Defender and
|
|
95
|
+
// the search indexer open a file the instant it is written — so on Windows
|
|
96
|
+
// a bare rename fails on a perfectly healthy machine, `POST /api/prefs`
|
|
97
|
+
// 500s through `guard`, and the notifications switch silently does not
|
|
98
|
+
// stick. POSIX rename(2) has no such rule, which is why this shipped green.
|
|
99
|
+
const mv = deps.rename ?? renameWithRetry;
|
|
91
100
|
const next = normalise({ ...(await readPrefs(home, deps)), ...patch });
|
|
92
101
|
await mk(prefsDir(home), { recursive: true });
|
|
93
102
|
const tmp = `${prefsPath(home)}.${process.pid}.tmp`;
|
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
|
|