ccdeck 3.26.4 → 3.26.5
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/assets/{BrowserWatchModal-BUWNIaK8.js → BrowserWatchModal-B6MuDGvq.js} +1 -1
- package/dist/web/assets/{UsageHistoryModal-DlRe0P3F.js → UsageHistoryModal-BJA3mUzs.js} +1 -1
- package/dist/web/assets/index-D4fPrztm.js +297 -0
- package/dist/web/index.html +1 -1
- package/hook/hook.js +61 -20
- package/package.json +1 -1
- package/release-notes.json +2 -1
- package/src/server/installer.mjs +23 -1
- package/src/server/retire-sound-hook.mjs +10 -1
- package/dist/web/assets/index-CPHonXg4.js +0 -297
package/dist/web/index.html
CHANGED
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
stored === "light" || (stored !== "dark" && light) ? "light" : "dark");
|
|
45
45
|
})();
|
|
46
46
|
</script>
|
|
47
|
-
<script type="module" crossorigin src="/assets/index-
|
|
47
|
+
<script type="module" crossorigin src="/assets/index-D4fPrztm.js"></script>
|
|
48
48
|
<link rel="stylesheet" crossorigin href="/assets/index-DlaHgzye.css">
|
|
49
49
|
</head>
|
|
50
50
|
<body>
|
package/hook/hook.js
CHANGED
|
@@ -357,13 +357,15 @@ function sameProof(got, want) {
|
|
|
357
357
|
//
|
|
358
358
|
// They are now separated by a barrier: every target is challenged, then the
|
|
359
359
|
// election is decided, then the payload goes out (#695). The worst case is
|
|
360
|
-
// unchanged — the challenges run in parallel, so it is still one
|
|
361
|
-
// followed by one 1000ms deadline
|
|
362
|
-
//
|
|
363
|
-
//
|
|
364
|
-
//
|
|
365
|
-
//
|
|
366
|
-
//
|
|
360
|
+
// unchanged — the challenges run in parallel, so it is still one challenge's
|
|
361
|
+
// two 400ms attempts followed by one 1000ms deadline, and each 400ms bounds the
|
|
362
|
+
// whole answer rather than a silence in it (see prove). What the barrier does
|
|
363
|
+
// cost is that an honest deck's POST waits for the slowest challenge in the
|
|
364
|
+
// set, which only matters when some OTHER record's port accepts a connection
|
|
365
|
+
// and then does not answer in time. A ghost port with nothing behind it refuses
|
|
366
|
+
// instantly and delays no one; one with something slow or silent behind it
|
|
367
|
+
// costs both attempts once, and its record is then forgotten if no deck has
|
|
368
|
+
// been keeping it — see forgetIfAbandoned.
|
|
367
369
|
const CHALLENGE_TIMEOUT_MS = 400;
|
|
368
370
|
const POST_TIMEOUT_MS = 1000;
|
|
369
371
|
|
|
@@ -423,6 +425,8 @@ const CAP_MS = 1900;
|
|
|
423
425
|
*/
|
|
424
426
|
function prove(d, cb, attempt = 0) {
|
|
425
427
|
let settled = false;
|
|
428
|
+
// Armed once the request is out, and cleared by whichever verdict comes first.
|
|
429
|
+
let deadline = null;
|
|
426
430
|
// A DEADLINE IS NOT AN ANSWER, and the difference is worth one retry.
|
|
427
431
|
//
|
|
428
432
|
// A wrong proof, a refused connection and a 404 are all verdicts: that port
|
|
@@ -444,7 +448,12 @@ function prove(d, cb, attempt = 0) {
|
|
|
444
448
|
settled = true; // this attempt is over; the next owns `cb`
|
|
445
449
|
prove(d, cb, attempt + 1);
|
|
446
450
|
};
|
|
447
|
-
const finish = (ok, why = null) => {
|
|
451
|
+
const finish = (ok, why = null) => {
|
|
452
|
+
if (settled) return;
|
|
453
|
+
settled = true;
|
|
454
|
+
clearTimeout(deadline);
|
|
455
|
+
cb(ok, why);
|
|
456
|
+
};
|
|
448
457
|
|
|
449
458
|
if (!requiresProof(d)) return finish(true);
|
|
450
459
|
|
|
@@ -456,7 +465,6 @@ function prove(d, cb, attempt = 0) {
|
|
|
456
465
|
port: d.port,
|
|
457
466
|
path: `/api/hook-challenge?nonce=${nonce}`,
|
|
458
467
|
method: "GET",
|
|
459
|
-
timeout: CHALLENGE_TIMEOUT_MS,
|
|
460
468
|
}, res => {
|
|
461
469
|
if (res.statusCode !== 200) { res.resume(); return res.on("end", () => finish(false)); }
|
|
462
470
|
let answer = "";
|
|
@@ -476,12 +484,26 @@ function prove(d, cb, attempt = 0) {
|
|
|
476
484
|
finish(sameProof(proof, want));
|
|
477
485
|
});
|
|
478
486
|
});
|
|
479
|
-
|
|
480
|
-
//
|
|
481
|
-
//
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
487
|
+
req.on("error", () => finish(false));
|
|
488
|
+
// THE DEADLINE IS ON THE WHOLE ANSWER, not on the silences in it (#1172).
|
|
489
|
+
//
|
|
490
|
+
// This was http.request's `timeout`, which is an IDLE timeout: it fires only
|
|
491
|
+
// after that long with no bytes either way. A port that answers one byte
|
|
492
|
+
// every 150ms is never idle and never reaches the 4096-byte cap above, so its
|
|
493
|
+
// prove() never settled — and proveTargets waits for every target before
|
|
494
|
+
// anything is posted. Measured with an honest deck beside such a port: the
|
|
495
|
+
// hook ran to CAP_MS at 1938ms and the deck saw its challenge and no event,
|
|
496
|
+
// on every tool call for as long as the record stood, and the record was
|
|
497
|
+
// never forgotten either, because the "deadline" verdict forgetIfAbandoned
|
|
498
|
+
// needs never came.
|
|
499
|
+
//
|
|
500
|
+
// The idle timeout had a second hole, and a port that sends its status line
|
|
501
|
+
// and then goes quiet fell into it: once a response has begun, destroying the
|
|
502
|
+
// request raises no 'error' on it, so the verdict that used to ride on that
|
|
503
|
+
// event never arrived either. So the timer gives the verdict itself, and
|
|
504
|
+
// gives it first: the destroy's own 'error', if it raises one, finds this
|
|
505
|
+
// attempt already over.
|
|
506
|
+
deadline = setTimeout(() => { retryOnTimeout(); req.destroy(); }, CHALLENGE_TIMEOUT_MS);
|
|
485
507
|
req.end();
|
|
486
508
|
}
|
|
487
509
|
|
|
@@ -686,11 +708,30 @@ function post(d, body, persists, done) {
|
|
|
686
708
|
finish(res.statusCode >= 200 && res.statusCode < 300);
|
|
687
709
|
});
|
|
688
710
|
req.on("finish", () => { sent = true; });
|
|
689
|
-
// `destroy()` on a timeout makes 'error' fire with ECONNRESET
|
|
690
|
-
//
|
|
691
|
-
// writer hanging up.
|
|
692
|
-
|
|
693
|
-
|
|
711
|
+
// `destroy()` on a timeout makes 'error' fire with ECONNRESET — the status
|
|
712
|
+
// line has not arrived, or it would have settled this already — so `timedOut`
|
|
713
|
+
// is what tells this process giving up apart from the writer hanging up.
|
|
714
|
+
//
|
|
715
|
+
// AND THE BODY'S STATE IS READ WHEN THE DEADLINE FIRES, NOT WHEN THE ERROR
|
|
716
|
+
// ARRIVES (#1172). `timedOut && sent` was evaluated in the 'error' handler,
|
|
717
|
+
// and by the time that runs the answer is always yes: destroying a request
|
|
718
|
+
// that has been end()ed finalises its writable, which emits 'finish' — so
|
|
719
|
+
// `sent` flips to true on the way out of req.destroy(), one tick before the
|
|
720
|
+
// 'error' it causes. Measured on Node 22 against a listener that takes the
|
|
721
|
+
// headers and then stops reading, with 256MB still queued:
|
|
722
|
+
//
|
|
723
|
+
// TIMEOUT sent=false writableLength=268435608
|
|
724
|
+
// FINISH writableFinished=true pending=0
|
|
725
|
+
// ERROR ECONNRESET sent=true <- verdict "deadline"
|
|
726
|
+
//
|
|
727
|
+
// which made every deadline a "deadline" and left the paragraph above
|
|
728
|
+
// describing a branch that could not be taken: a writer wedged mid-read kept
|
|
729
|
+
// a log it had not received, and the deck behind it — which could have
|
|
730
|
+
// written the line — was never asked. The snapshot is the rule that paragraph
|
|
731
|
+
// states, taken at the only moment it is still true.
|
|
732
|
+
let sentByDeadline = false;
|
|
733
|
+
req.on("error", () => finish(false, timedOut && sentByDeadline ? "deadline" : null));
|
|
734
|
+
req.on("timeout", () => { timedOut = true; sentByDeadline = sent; req.destroy(); });
|
|
694
735
|
req.write(body);
|
|
695
736
|
req.end();
|
|
696
737
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ccdeck",
|
|
3
|
-
"version": "3.26.
|
|
3
|
+
"version": "3.26.5",
|
|
4
4
|
"description": "Live dashboard for Claude Code and Codex — which agent is waiting on you, 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
package/src/server/installer.mjs
CHANGED
|
@@ -403,15 +403,37 @@ async function writeFileAtomic(rawTarget, text) {
|
|
|
403
403
|
* identical content skips the write and the file is not replaced at all.
|
|
404
404
|
*/
|
|
405
405
|
async function installScript(src, dst) {
|
|
406
|
-
|
|
406
|
+
return installText(await readFile(src, "utf8"), dst);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
async function installText(text, dst) {
|
|
407
410
|
const current = await readFile(dst, "utf8").catch(() => null);
|
|
408
411
|
if (current === text) return false;
|
|
409
412
|
await writeFileAtomic(dst, text);
|
|
410
413
|
return true;
|
|
411
414
|
}
|
|
412
415
|
|
|
416
|
+
/**
|
|
417
|
+
* What makes the installed hook.js a CommonJS file wherever the config dir is.
|
|
418
|
+
*
|
|
419
|
+
* Node decides whether a `.js` file is CommonJS or an ES module from the nearest
|
|
420
|
+
* package.json above it, and hook.js is CommonJS. With none in agent-dag/, that
|
|
421
|
+
* nearest one is whatever the user has further up: a `"type": "module"` in
|
|
422
|
+
* ~/package.json, or in the repo a relocated CLAUDE_CONFIG_DIR lives in, loads
|
|
423
|
+
* the hook as an ES module, and it dies on its first `require` before main()
|
|
424
|
+
* has installed a single handler. Exit 1 and `require is not defined in ES
|
|
425
|
+
* module scope` on every tool call, and no deck receives anything (#1172). One
|
|
426
|
+
* file here answers the question before Node looks any further.
|
|
427
|
+
*
|
|
428
|
+
* Written before the hook, so a first install never has a hook.js without it,
|
|
429
|
+
* and through the same atomic write, because a half-written package.json is a
|
|
430
|
+
* load error of its own for every hook that starts while it is being written.
|
|
431
|
+
*/
|
|
432
|
+
const HOOK_PACKAGE_JSON = '{ "type": "commonjs" }\n';
|
|
433
|
+
|
|
413
434
|
async function installHookScript(installDir) {
|
|
414
435
|
await ensureDir(installDir);
|
|
436
|
+
await installText(HOOK_PACKAGE_JSON, join(installDir, "package.json"));
|
|
415
437
|
const src = join(PKG_ROOT, "hook", "hook.js");
|
|
416
438
|
const dst = join(installDir, "hook.js");
|
|
417
439
|
await installScript(src, dst);
|
|
@@ -91,8 +91,17 @@ const EVENT = "Stop";
|
|
|
91
91
|
// writes this any more; retirement reads it once and deletes it.
|
|
92
92
|
const PARKED_PATH = join(homedir(), ".agents-deck", "parked-sound-hooks.json");
|
|
93
93
|
|
|
94
|
+
// `Array.isArray`, not `?? []`, and that is a crash fix rather than a style
|
|
95
|
+
// choice. A group whose `hooks` is not an array — hand-edited settings.json,
|
|
96
|
+
// something else's schema, a half-finished edit — made this `.map` throw
|
|
97
|
+
// `TypeError: … .map is not a function`, out of `retireSoundHookIn`, out of
|
|
98
|
+
// `installHooks`, on the unconditional install every boot runs: the deck would
|
|
99
|
+
// not start at all until the user found and fixed the entry, with nothing but a
|
|
100
|
+
// stack to say which one. installer.mjs's own `isOurEntry` has always read the
|
|
101
|
+
// same field with the same guard, and its answer for a group it cannot read is
|
|
102
|
+
// to leave it exactly where it is.
|
|
94
103
|
const commandsOf = (entry) =>
|
|
95
|
-
(entry?.hooks
|
|
104
|
+
(Array.isArray(entry?.hooks) ? entry.hooks : []).map(h => (typeof h?.command === "string" ? h.command : ""));
|
|
96
105
|
|
|
97
106
|
/**
|
|
98
107
|
* THE TAIL, NOT THE WHOLE PATH — and this is a data-loss fix, not a tidy-up.
|