agent-dag 1.33.77 → 1.33.79

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