agent-dag 1.33.76 → 1.33.78

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/bin/deck.js CHANGED
@@ -77,7 +77,7 @@ const persist = flags.noPersist
77
77
  ? null
78
78
  : (flags.history ?? join(claudeConfigDir(), "agent-dag", "events.jsonl"));
79
79
 
80
- const { installHooks, writeDiscovery, removeDiscovery, hasCodexInstalled } =
80
+ const { installHooks, keepDiscovery, removeDiscovery, hasCodexInstalled } =
81
81
  await import(pathToFileURL(join(PKG_ROOT, "src/server/installer.mjs")).href);
82
82
  const { startServer, hookToken } =
83
83
  await import(pathToFileURL(join(PKG_ROOT, "src/server/index.mjs")).href);
@@ -328,9 +328,31 @@ if (RESPAWN) {
328
328
  else process.stdout.write("\n");
329
329
  }
330
330
 
331
+ // The discovery file is the whole of how a hook finds this deck: hook.js
332
+ // enumerates that directory and nothing else. Writing it once at boot meant
333
+ // anything that later took it away left a deck that listened, served, and
334
+ // received not one event — with nothing on screen to say so. So it is checked
335
+ // on a timer, put back when it goes missing, and its absence is stated out loud
336
+ // rather than left to look like an idle afternoon.
337
+ //
331
338
  // The token goes in with the port: it is what lets a hook tell this deck from
332
339
  // whatever else may later be listening on the same number. See hookToken().
333
- const discoveryFile = await writeDiscovery({ port: realPort, workspace, token: hookToken() });
340
+ let registered = null;
341
+ const discovery = keepDiscovery({
342
+ port: realPort,
343
+ workspace,
344
+ token: hookToken(),
345
+ onState: (state) => {
346
+ const first = registered === null;
347
+ registered = state.ok;
348
+ if (!state.ok) reportUnregistered(state);
349
+ else if (!first) reportReregistered(state);
350
+ },
351
+ });
352
+ const discoveryFile = discovery.file;
353
+ // Now, not in five seconds: nothing should reach the pulse line below without
354
+ // the deck knowing whether the hooks can see it.
355
+ await discovery.check();
334
356
 
335
357
  // Never on a respawn: the tab that asked for the restart is still open and
336
358
  // reconnecting on its own. A second one would be the deck talking over itself.
@@ -344,9 +366,19 @@ if (openBrowser && !RESPAWN) {
344
366
  // ── Pulse indicator ───────────────────────────────────────────────────────────
345
367
  if (tty) {
346
368
  const pulseFrames = [`${C.green}●${C.reset}`, `${C.dim}●${C.reset}`];
369
+ const LISTENING = "listening — Ctrl+C to stop";
370
+ // A deck no hook can find is not listening in any sense the user cares
371
+ // about, and the pulse is the one line that stays on screen for hours — so
372
+ // it is where this has to be said.
373
+ const UNREGISTERED = "listening, but not registered — hooks cannot find this deck";
374
+ // Padded on the plain text, before any colour: the line is redrawn over
375
+ // itself with \r, so the shorter message has to cover the longer one.
376
+ const width = UNREGISTERED.length + 3;
347
377
  let pi = 0;
348
378
  setInterval(() => {
349
- process.stdout.write(`\r ${pulseFrames[pi++ % 2]} ${C.dim}listening Ctrl+C to stop${C.reset} `);
379
+ const text = (registered ? LISTENING : UNREGISTERED).padEnd(width);
380
+ const colour = registered ? C.dim : C.yellow;
381
+ process.stdout.write(`\r ${pulseFrames[pi++ % 2]} ${colour}${text}${C.reset}`);
350
382
  }, 800).unref();
351
383
  }
352
384
 
@@ -358,6 +390,10 @@ const shutdown = async (code = 0) => {
358
390
  if (tty && code !== RESTART_CODE && code !== UPGRADE_CODE) {
359
391
  process.stdout.write(`\n\n ${C.yellow}◉ shutting down…${C.reset}\n`);
360
392
  }
393
+ // Stopped first, always: a tick landing after the unlink would re-register a
394
+ // deck that is on its way out, and leave the file behind for the hooks to
395
+ // find once nothing is listening.
396
+ discovery.stop();
361
397
  await removeDiscovery(discoveryFile);
362
398
  server.close(() => process.exit(code));
363
399
  // SSE connections never end by themselves, so close() alone would sit out the
@@ -369,10 +405,25 @@ const shutdown = async (code = 0) => {
369
405
  };
370
406
  process.on("SIGINT", () => shutdown(0));
371
407
  process.on("SIGTERM", () => shutdown(0));
372
- process.on("beforeExit", () => removeDiscovery(discoveryFile));
408
+ process.on("beforeExit", () => { discovery.stop(); removeDiscovery(discoveryFile); });
373
409
 
374
410
  // ── helpers ───────────────────────────────────────────────────────────────────
375
411
 
412
+ // The deck is up and the hooks cannot see it. Said in full — the path, the
413
+ // reason — because the alternative is what this replaced: an ordinary-looking
414
+ // deck that simply never shows a session.
415
+ function reportUnregistered({ file, error }) {
416
+ const why = error?.message ? ` — ${error.message}` : "";
417
+ process.stdout.write(
418
+ `\n ${C.yellow}⚠${C.reset} ${C.bold}not registered${C.reset}${C.dim}${why}${C.reset}\n` +
419
+ ` ${C.dim}hooks find this deck through ${file}, so until that file exists no events arrive.${C.reset}\n`,
420
+ );
421
+ }
422
+
423
+ function reportReregistered({ file }) {
424
+ process.stdout.write(`\n ${C.green}✓${C.reset} ${C.dim}registered again → ${file}${C.reset}\n`);
425
+ }
426
+
376
427
  function parseArgs(args) {
377
428
  const out = {};
378
429
  for (let i = 0; i < args.length; i++) {