@pushary/agent-hooks 0.89.12 → 0.90.0

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.
Files changed (82) hide show
  1. package/CHANGELOG.md +77 -0
  2. package/README.md +1 -0
  3. package/data/SKILL.md +1 -1
  4. package/data/cursor-plugin/skills/pushary/SKILL.md +1 -1
  5. package/data/vscode-plugin/skills/pushary/SKILL.md +1 -1
  6. package/dist/bin/pushary-bell-hook.js +4 -4
  7. package/dist/bin/pushary-bell.js +8 -8
  8. package/dist/bin/pushary-claude.js +194 -38
  9. package/dist/bin/pushary-clean.js +15 -32
  10. package/dist/bin/pushary-codex-bridge.js +37 -12
  11. package/dist/bin/pushary-codex-hook.js +45 -12
  12. package/dist/bin/pushary-codex.js +7 -7
  13. package/dist/bin/pushary-connect.js +11 -10
  14. package/dist/bin/pushary-daemon.js +186 -62
  15. package/dist/bin/pushary-disconnect.js +8 -8
  16. package/dist/bin/pushary-doctor.js +108 -25
  17. package/dist/bin/pushary-elicitation-hook.js +4 -4
  18. package/dist/bin/pushary-gemini-bridge.js +33 -11
  19. package/dist/bin/pushary-gemini-hook.js +21 -10
  20. package/dist/bin/pushary-hook.js +13 -13
  21. package/dist/bin/pushary-login.js +18 -7
  22. package/dist/bin/pushary-logout.js +6 -6
  23. package/dist/bin/pushary-mode.js +28 -21
  24. package/dist/bin/pushary-notification-hook.js +7 -7
  25. package/dist/bin/pushary-opencode-hook.js +8 -8
  26. package/dist/bin/pushary-permission-denied-hook.js +11 -11
  27. package/dist/bin/pushary-permission-hook.js +11 -11
  28. package/dist/bin/pushary-post-hook.js +14 -8
  29. package/dist/bin/pushary-prompt-hook.js +7 -7
  30. package/dist/bin/pushary-session-end-hook.js +7 -7
  31. package/dist/bin/pushary-session-start-hook.js +7 -7
  32. package/dist/bin/pushary-setup.js +168 -45
  33. package/dist/bin/pushary-stats.js +4 -4
  34. package/dist/bin/pushary-status.js +11 -10
  35. package/dist/bin/pushary-stop-hook.js +14 -8
  36. package/dist/bin/pushary-stopfailure-hook.js +7 -7
  37. package/dist/bin/pushary-suggestions.js +2 -2
  38. package/dist/bin/pushary-transcripts.d.ts +1 -0
  39. package/dist/bin/pushary-transcripts.js +79 -0
  40. package/dist/bin/pushary-upgrade.js +15 -15
  41. package/dist/bin/pushary-wait.js +5 -5
  42. package/dist/bin/pushary.js +3 -2
  43. package/dist/{chunk-KVHP5ZME.js → chunk-2LAHXHBR.js} +1 -1
  44. package/dist/{chunk-RLQWNEPM.js → chunk-43GMYSS4.js} +15 -0
  45. package/dist/{chunk-5NDB3YOE.js → chunk-54ELC7D7.js} +1 -1
  46. package/dist/{chunk-VPLQFXCH.js → chunk-5NAZ7SFS.js} +1 -1
  47. package/dist/chunk-7FE2KABY.js +156 -0
  48. package/dist/{chunk-DHR4F2CR.js → chunk-7TPQEWUZ.js} +1 -1
  49. package/dist/{chunk-Y4WVZIEI.js → chunk-AJYV3VGU.js} +1 -1
  50. package/dist/{chunk-QFJZHVVL.js → chunk-AZPIQG4G.js} +35 -1
  51. package/dist/{chunk-OCMOTJTW.js → chunk-B7UZHEBL.js} +29 -22
  52. package/dist/{chunk-7HYJPOGG.js → chunk-BQBEH6TV.js} +5 -5
  53. package/dist/{chunk-OTPJ4FUR.js → chunk-BVBEURWJ.js} +1 -1
  54. package/dist/{chunk-7DK3ZH3N.js → chunk-CELX75MW.js} +1 -1
  55. package/dist/{chunk-6VXNA5JB.js → chunk-CNPAT3HL.js} +3 -2
  56. package/dist/{chunk-327YMKP5.js → chunk-DYP6SMOR.js} +1 -1
  57. package/dist/chunk-DZDI62LY.js +145 -0
  58. package/dist/{chunk-VF3RTVWY.js → chunk-EZIEXHYM.js} +15 -7
  59. package/dist/{chunk-PS7DQ2SU.js → chunk-F2ICEWZL.js} +101 -19
  60. package/dist/{chunk-3XSP5SML.js → chunk-FQL5E6NG.js} +2 -2
  61. package/dist/{chunk-JHJHCMAF.js → chunk-FXEVRLHC.js} +20 -2
  62. package/dist/{chunk-6V7ZDH3A.js → chunk-GL3FFM7A.js} +7 -4
  63. package/dist/{chunk-NBRM3FMD.js → chunk-H2QZRQR2.js} +1 -1
  64. package/dist/{chunk-GKPURU7P.js → chunk-I463AEG3.js} +1 -1
  65. package/dist/{chunk-XREW7RQ3.js → chunk-JW4KO5AF.js} +1 -1
  66. package/dist/{chunk-OVEGEQSB.js → chunk-KUZWXPUS.js} +3 -2
  67. package/dist/{chunk-JVZX5YRJ.js → chunk-L4E2BIJ2.js} +80 -7
  68. package/dist/{chunk-RNKPLUWC.js → chunk-OSALSIUF.js} +1 -1
  69. package/dist/{chunk-MFNQ5IMW.js → chunk-PYA4FLKI.js} +32 -21
  70. package/dist/chunk-QCGNGEGE.js +15 -0
  71. package/dist/chunk-S6LYP7QA.js +127 -0
  72. package/dist/{chunk-OADGBOP7.js → chunk-TDZFPMSZ.js} +70 -11
  73. package/dist/{chunk-JJV5Q4KG.js → chunk-UZNKVRLE.js} +1 -1
  74. package/dist/{chunk-FCUUVHTS.js → chunk-VHWCVD42.js} +1 -1
  75. package/dist/{chunk-3MCEES6Z.js → chunk-VNMKIJZB.js} +15 -2
  76. package/dist/{chunk-6GU55AQQ.js → chunk-Y6BWPERV.js} +90 -7
  77. package/dist/chunk-YKLCWWEI.js +76 -0
  78. package/dist/src/index.d.ts +3 -0
  79. package/dist/src/index.js +11 -11
  80. package/package.json +3 -2
  81. package/dist/chunk-NESKLV6R.js +0 -72
  82. package/dist/chunk-WDVQ6O4C.js +0 -84
@@ -1,38 +1,46 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  inspectConfiguredManagedProviders
4
- } from "../chunk-7HYJPOGG.js";
5
- import "../chunk-NBRM3FMD.js";
4
+ } from "../chunk-BQBEH6TV.js";
5
+ import "../chunk-H2QZRQR2.js";
6
6
  import {
7
7
  HEARTBEAT_INTERVAL_MS,
8
8
  sendHeartbeat
9
- } from "../chunk-DHR4F2CR.js";
10
- import "../chunk-NESKLV6R.js";
11
- import "../chunk-6V7ZDH3A.js";
12
- import "../chunk-KVHP5ZME.js";
13
- import "../chunk-VF3RTVWY.js";
9
+ } from "../chunk-7TPQEWUZ.js";
10
+ import "../chunk-DZDI62LY.js";
11
+ import "../chunk-GL3FFM7A.js";
12
+ import "../chunk-2LAHXHBR.js";
13
+ import "../chunk-EZIEXHYM.js";
14
14
  import {
15
15
  DAEMON_LEASE_RENEW_INTERVAL_MS,
16
16
  acquireDaemonLease,
17
+ clearDaemonNeedsLogin,
17
18
  daemonServiceManagerAvailable,
19
+ describeDaemonLoginBlock,
18
20
  ensureDaemonRunning,
19
21
  hasDaemonServiceDefinition,
20
22
  manageDaemonService,
21
23
  parseDaemonAction,
24
+ readDaemonNeedsLogin,
22
25
  readDaemonStatus,
23
26
  replaceDaemonService,
24
27
  requestDaemonHandoff,
25
- stopDaemonRunning
26
- } from "../chunk-PS7DQ2SU.js";
28
+ stopDaemonRunning,
29
+ writeDaemonNeedsLogin
30
+ } from "../chunk-F2ICEWZL.js";
27
31
  import {
28
32
  detectProviderLiveCapabilities,
29
33
  hasSpawnProvider,
30
34
  resolveControlPolicy
31
35
  } from "../chunk-3OXHYPZG.js";
32
36
  import {
37
+ SESSION_REAP_SILENCE_MS,
38
+ describeIdleWindow,
33
39
  findLiveProcessBySpawnId,
34
- ownsLiveProcess
35
- } from "../chunk-6GU55AQQ.js";
40
+ listProcessProofs,
41
+ ownsLiveProcess,
42
+ silentSpawnedProofs
43
+ } from "../chunk-Y6BWPERV.js";
36
44
  import {
37
45
  spawnClaude
38
46
  } from "../chunk-XHKBHWLX.js";
@@ -41,27 +49,30 @@ import {
41
49
  } from "../chunk-L4ZCFMWW.js";
42
50
  import {
43
51
  getMachineId
44
- } from "../chunk-5NDB3YOE.js";
45
- import "../chunk-OVEGEQSB.js";
52
+ } from "../chunk-54ELC7D7.js";
53
+ import "../chunk-KUZWXPUS.js";
54
+ import {
55
+ writeHuman
56
+ } from "../chunk-QCGNGEGE.js";
46
57
  import {
47
58
  exitOnHelpFlag
48
- } from "../chunk-OTPJ4FUR.js";
49
- import "../chunk-RLQWNEPM.js";
59
+ } from "../chunk-BVBEURWJ.js";
60
+ import "../chunk-43GMYSS4.js";
50
61
  import "../chunk-DINDL2CF.js";
51
62
  import {
52
63
  readControlMode
53
- } from "../chunk-RNKPLUWC.js";
64
+ } from "../chunk-OSALSIUF.js";
54
65
  import "../chunk-BC3VCZ3E.js";
55
66
  import {
56
67
  EXIT
57
68
  } from "../chunk-KZERVKTD.js";
58
- import "../chunk-XREW7RQ3.js";
69
+ import "../chunk-JW4KO5AF.js";
59
70
  import "../chunk-4LL6LG5X.js";
60
71
  import {
61
72
  getApiKey,
62
73
  getBaseUrl
63
74
  } from "../chunk-2UMNXADU.js";
64
- import "../chunk-3MCEES6Z.js";
75
+ import "../chunk-VNMKIJZB.js";
65
76
 
66
77
  // src/spawn-daemon.ts
67
78
  import { execFileSync, spawn } from "child_process";
@@ -119,6 +130,7 @@ var VERSION_CHECK_INTERVAL_MS = 2e3;
119
130
  var CONTROL_PROBE_INTERVAL_MS = 6e4;
120
131
  var MAX_TRACKED_PROCESSES = 100;
121
132
  var MAX_COMPLETED_STOPS = 20;
133
+ var SESSION_REAP_INTERVAL_MS = 6e4;
122
134
  var controlProbeDue = (lastProbeAt, at = Date.now()) => lastProbeAt === 0 || at - lastProbeAt >= CONTROL_PROBE_INTERVAL_MS;
123
135
  var parseControlProbe = (value) => {
124
136
  if (typeof value !== "object" || value === null) return null;
@@ -256,6 +268,28 @@ var spawnRateAllows = (timestamps, now, maxPerWindow = MAX_SPAWNS_PER_WINDOW, wi
256
268
  var stderr = (msg) => {
257
269
  process.stderr.write(msg);
258
270
  };
271
+ var log = (message) => {
272
+ writeHuman(`${(/* @__PURE__ */ new Date()).toISOString()} ${message}
273
+ `);
274
+ };
275
+ var errMsg = (err) => err instanceof Error ? err.message : String(err);
276
+ var trackFailureStreak = (name, sinks) => {
277
+ let failing = false;
278
+ let lastDetail = "";
279
+ return {
280
+ fail(detail) {
281
+ sinks.recordError(`${name}: ${detail}`);
282
+ if (!failing || detail !== lastDetail) sinks.log(`${name} failing: ${detail}`);
283
+ failing = true;
284
+ lastDetail = detail;
285
+ },
286
+ recover() {
287
+ if (failing) sinks.log(`${name} recovered`);
288
+ failing = false;
289
+ lastDetail = "";
290
+ }
291
+ };
292
+ };
259
293
  var runSpawnDaemon = async () => {
260
294
  let apiKey;
261
295
  try {
@@ -264,6 +298,13 @@ var runSpawnDaemon = async () => {
264
298
  stderr("[pushary] daemon needs an API key. Run `npx @pushary/agent-hooks setup`.\n");
265
299
  return 1;
266
300
  }
301
+ const loginBlock = readDaemonNeedsLogin();
302
+ if (loginBlock.kind === "blocked") {
303
+ stderr(`[pushary] daemon not starting: ${describeDaemonLoginBlock(loginBlock.record)}
304
+ `);
305
+ return 0;
306
+ }
307
+ if (loginBlock.kind === "stale") clearDaemonNeedsLogin();
267
308
  const machineId = getMachineId();
268
309
  const baseUrl = getBaseUrl();
269
310
  const pusharyBin = resolveBinary("pushary") ?? "pushary";
@@ -298,6 +339,9 @@ var runSpawnDaemon = async () => {
298
339
  return 1;
299
340
  }
300
341
  const daemonLease = lifecycle.lease;
342
+ const sinks = { log, recordError: (message) => daemonLease.recordError(message) };
343
+ const drainHealth = trackFailureStreak("drain", sinks);
344
+ const heartbeatHealth = trackFailureStreak("heartbeat", sinks);
301
345
  let stopped = false;
302
346
  let idleTicks = 0;
303
347
  let errorStreak = 0;
@@ -307,12 +351,14 @@ var runSpawnDaemon = async () => {
307
351
  const completedStops = /* @__PURE__ */ new Map();
308
352
  let timer;
309
353
  let inflight;
310
- let fatalCode = null;
311
354
  let finish = () => {
312
355
  };
313
356
  let heartbeat;
314
357
  let versionWatch;
315
358
  let leaseRenewal;
359
+ let sessionReaper;
360
+ let reaping = false;
361
+ let rateCapped = false;
316
362
  const launch = (req) => {
317
363
  if (launchedProcesses.size >= MAX_TRACKED_PROCESSES) {
318
364
  return Promise.reject(new Error("This daemon is already tracking 100 active sessions"));
@@ -364,8 +410,10 @@ var runSpawnDaemon = async () => {
364
410
  let started = false;
365
411
  child.once("error", (err) => {
366
412
  if (!started) reject(err);
367
- else stderr(`[pushary] launched session reported an error: ${err.message}
368
- `);
413
+ else {
414
+ log(`spawn ${req.id}: launched session reported an error: ${err.message}`);
415
+ daemonLease.recordError(`spawn ${req.id}: ${err.message}`);
416
+ }
369
417
  });
370
418
  child.once("spawn", () => {
371
419
  started = true;
@@ -374,8 +422,7 @@ var runSpawnDaemon = async () => {
374
422
  child.once("close", () => launchedProcesses.delete(child.pid));
375
423
  }
376
424
  child.unref();
377
- stderr(`[pushary] launched a phone-requested ${plan.agent} session in ${basename(cwd)}
378
- `);
425
+ log(`spawn ${req.id}: launched a phone-requested ${plan.agent} session in ${basename(cwd)} (pid ${child.pid ?? "unknown"})`);
379
426
  resolve(typeof child.pid === "number" ? child.pid : null);
380
427
  });
381
428
  } catch (err) {
@@ -383,10 +430,45 @@ var runSpawnDaemon = async () => {
383
430
  }
384
431
  });
385
432
  };
433
+ const kill = process.platform === "win32" ? (pid) => {
434
+ execFileSync("taskkill", windowsTaskkillArgs(pid), { stdio: "ignore" });
435
+ } : process.kill.bind(process);
436
+ const stopDeps = {
437
+ kill,
438
+ ownsProcess: (request) => {
439
+ const launched = request.spawnId ? launchedProcesses.get(request.pid) === request.spawnId : !request.id && launchedProcesses.has(request.pid);
440
+ return launched || ownsLiveProcess(request);
441
+ },
442
+ selfPid: process.pid,
443
+ sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
444
+ isAlive: (pid) => {
445
+ try {
446
+ process.kill(pid, 0);
447
+ return true;
448
+ } catch (err) {
449
+ return err.code === "EPERM";
450
+ }
451
+ }
452
+ };
453
+ const reapSilentSessions = async () => {
454
+ if (stopped || reaping) return;
455
+ reaping = true;
456
+ try {
457
+ const now = Date.now();
458
+ const silent = silentSpawnedProofs(listProcessProofs({ reapDead: true }), now, SESSION_REAP_SILENCE_MS);
459
+ for (const proof of silent) {
460
+ if (stopped) return;
461
+ const outcome = await applyStopAndVerify(
462
+ { sessionId: proof.sessionId, pid: proof.pid, spawnId: proof.spawnId },
463
+ stopDeps
464
+ );
465
+ log(`session ${proof.sessionId} (pid ${proof.pid}) was silent for ${describeIdleWindow(now - proof.lastSeenAt)}: ${outcome}`);
466
+ }
467
+ } finally {
468
+ reaping = false;
469
+ }
470
+ };
386
471
  const runStop = async (stop) => {
387
- const kill = process.platform === "win32" ? (pid) => {
388
- execFileSync("taskkill", windowsTaskkillArgs(pid), { stdio: "ignore" });
389
- } : process.kill.bind(process);
390
472
  const outcome = stop.id ? completedStops.get(stop.id) ?? await apply() : await apply();
391
473
  if (stop.id && !completedStops.has(stop.id)) {
392
474
  if (completedStops.size === MAX_COMPLETED_STOPS) {
@@ -402,8 +484,7 @@ var runSpawnDaemon = async () => {
402
484
  refused: `could not stop session ${stop.sessionId}, the system refused the signal`,
403
485
  skipped_self: `refused to stop session ${stop.sessionId}: that pid is this daemon`
404
486
  };
405
- stderr(`[pushary] ${said[outcome]}
406
- `);
487
+ log(said[outcome]);
407
488
  const acknowledgement = stopAcknowledgement(stop, machineId, outcome);
408
489
  if (acknowledgement) {
409
490
  for (let attempt = 0; attempt < 3 && !stopped; attempt++) {
@@ -421,23 +502,7 @@ var runSpawnDaemon = async () => {
421
502
  }
422
503
  }
423
504
  async function apply() {
424
- return applyStopAndVerify(stop, {
425
- kill,
426
- ownsProcess: (request) => {
427
- const launched = request.spawnId ? launchedProcesses.get(request.pid) === request.spawnId : !request.id && launchedProcesses.has(request.pid);
428
- return launched || ownsLiveProcess(request);
429
- },
430
- selfPid: process.pid,
431
- sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
432
- isAlive: (pid) => {
433
- try {
434
- process.kill(pid, 0);
435
- return true;
436
- } catch (err) {
437
- return err.code === "EPERM";
438
- }
439
- }
440
- });
505
+ return applyStopAndVerify(stop, stopDeps);
441
506
  }
442
507
  };
443
508
  const acknowledgeControlProbe = async (probeId) => {
@@ -491,20 +556,20 @@ var runSpawnDaemon = async () => {
491
556
  signal: controller.signal
492
557
  });
493
558
  if (response.ok) return;
494
- const error = new Error(`spawn acknowledgement failed (${response.status})`);
559
+ const error = new Error(`spawn ${body.spawnId} acknowledgement failed (${response.status})`);
495
560
  if (response.status < 500) {
496
561
  lastError = error;
497
562
  break;
498
563
  }
499
564
  lastError = error;
500
565
  } catch (err) {
501
- lastError = err;
566
+ lastError = new Error(`spawn ${body.spawnId} acknowledgement failed: ${errMsg(err)}`);
502
567
  } finally {
503
568
  clearTimeout(timeout);
504
569
  if (inflight === controller) inflight = void 0;
505
570
  }
506
571
  }
507
- throw lastError ?? new Error("spawn acknowledgement stopped");
572
+ throw lastError ?? new Error(`spawn ${body.spawnId} acknowledgement stopped`);
508
573
  };
509
574
  const launchClaim = (request) => launchSpawnClaim(request, {
510
575
  machineId,
@@ -516,8 +581,10 @@ var runSpawnDaemon = async () => {
516
581
  },
517
582
  launch,
518
583
  acknowledge,
519
- reportLaunchError: (message) => stderr(`[pushary] could not launch session: ${message}
520
- `)
584
+ reportLaunchError: (message) => {
585
+ log(`spawn ${request.id}: could not launch session: ${message}`);
586
+ daemonLease.recordError(`spawn ${request.id}: ${message}`);
587
+ }
521
588
  });
522
589
  const nextDelay = () => {
523
590
  if (errorStreak > 0) return Math.min(SLOW_POLL_MS * 2 ** (errorStreak - 1), MAX_ERROR_BACKOFF_MS);
@@ -531,9 +598,12 @@ var runSpawnDaemon = async () => {
531
598
  return;
532
599
  }
533
600
  if (!spawnRateAllows(spawnTimestamps, Date.now())) {
601
+ if (!rateCapped) log(`spawn rate cap reached (${MAX_SPAWNS_PER_WINDOW} in ${SPAWN_WINDOW_MS / 1e3}s); leaving the queue until the window frees`);
602
+ rateCapped = true;
534
603
  timer = setTimeout(tick, jitter(nextDelay()));
535
604
  return;
536
605
  }
606
+ rateCapped = false;
537
607
  const controller = new AbortController();
538
608
  inflight = controller;
539
609
  const timeout = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
@@ -544,6 +614,7 @@ var runSpawnDaemon = async () => {
544
614
  return;
545
615
  }
546
616
  errorStreak = 0;
617
+ drainHealth.recover();
547
618
  if (drained.request) {
548
619
  idleTicks = 0;
549
620
  await launchClaim(drained.request);
@@ -557,14 +628,15 @@ var runSpawnDaemon = async () => {
557
628
  } catch (err) {
558
629
  if (isAuthFailure(err)) {
559
630
  const status = err.status;
560
- fatalCode = EXIT.UNAUTHENTICATED;
561
- stderr(
562
- status === 401 ? "[pushary] daemon stopping: this machine's key was rejected. Run `pushary login` to sign in again.\n" : "[pushary] daemon stopping: this workspace has no active plan, so the queue is closed. Pick one at pushary.com/dashboard/settings?tab=billing\n"
631
+ writeDaemonNeedsLogin(status === 401 ? "key_rejected" : "plan_inactive");
632
+ log(
633
+ status === 401 ? "daemon stopping: this machine's key was rejected. Run `pushary login` to sign in again." : "daemon stopping: this workspace has no active plan, so the queue is closed. Pick one at pushary.com/dashboard/settings?tab=billing"
563
634
  );
564
635
  finish();
565
636
  return;
566
637
  }
567
638
  errorStreak = Math.min(errorStreak + 1, MAX_ERROR_STREAK);
639
+ drainHealth.fail(errMsg(err));
568
640
  } finally {
569
641
  clearTimeout(timeout);
570
642
  inflight = void 0;
@@ -578,12 +650,12 @@ var runSpawnDaemon = async () => {
578
650
  if (heartbeat) clearInterval(heartbeat);
579
651
  if (versionWatch) clearInterval(versionWatch);
580
652
  if (leaseRenewal) clearInterval(leaseRenewal);
653
+ if (sessionReaper) clearInterval(sessionReaper);
581
654
  inflight?.abort();
582
655
  try {
583
656
  daemonLease.release();
584
657
  } catch (err) {
585
- stderr(`[pushary] daemon could not release its local lease: ${err instanceof Error ? err.message : String(err)}
586
- `);
658
+ log(`daemon could not release its local lease: ${errMsg(err)}`);
587
659
  }
588
660
  };
589
661
  const beat = async () => {
@@ -601,10 +673,14 @@ var runSpawnDaemon = async () => {
601
673
  ...providerCapabilities
602
674
  ]
603
675
  });
604
- if (result.kind === "ok") daemonLease.recordSuccessfulHeartbeat();
676
+ if (result.kind === "ok") {
677
+ daemonLease.recordSuccessfulHeartbeat();
678
+ heartbeatHealth.recover();
679
+ }
680
+ if (result.kind === "failed") heartbeatHealth.fail(result.detail);
605
681
  if (result.kind === "unauthorized" && !stopped) {
606
- fatalCode = EXIT.UNAUTHENTICATED;
607
- stderr("[pushary] daemon stopping: this machine's key was rejected. Run `pushary login` to sign in again.\n");
682
+ writeDaemonNeedsLogin("key_rejected");
683
+ log("daemon stopping: this machine's key was rejected. Run `pushary login` to sign in again.");
608
684
  finish();
609
685
  }
610
686
  };
@@ -614,9 +690,8 @@ var runSpawnDaemon = async () => {
614
690
  versionWatch = setInterval(() => {
615
691
  if (daemonLease.shouldYield()) finish();
616
692
  }, VERSION_CHECK_INTERVAL_MS);
617
- stderr(hasSpawnProvider(providerCapabilities) ? `[pushary] daemon online \u2014 this machine (${machineId}) can now be sent a new session from your phone. Ctrl-C to stop.
618
- ` : `[pushary] daemon online \u2014 no supported Claude, Codex, or Gemini CLI was found, so phone-start is unavailable. Ctrl-C to stop.
619
- `);
693
+ sessionReaper = setInterval(() => void reapSilentSessions(), SESSION_REAP_INTERVAL_MS);
694
+ log(hasSpawnProvider(providerCapabilities) ? `daemon online (pid ${process.pid}): this machine (${machineId}) can now be sent a new session from your phone. Ctrl-C to stop.` : `daemon online (pid ${process.pid}): no supported Claude, Codex, or Gemini CLI was found, so phone-start is unavailable. Ctrl-C to stop.`);
620
695
  timer = setTimeout(tick, 0);
621
696
  const signals = daemonSignals();
622
697
  await new Promise((resolve) => {
@@ -628,7 +703,51 @@ var runSpawnDaemon = async () => {
628
703
  resolve();
629
704
  };
630
705
  });
631
- return fatalCode ?? 0;
706
+ return 0;
707
+ };
708
+
709
+ // src/daemon-sessions.ts
710
+ import { execFileSync as execFileSync2 } from "child_process";
711
+ var readResidentSetKb = (pid) => {
712
+ if (process.platform === "win32") return null;
713
+ try {
714
+ const output = execFileSync2("ps", ["-o", "rss=", "-p", String(pid)], {
715
+ encoding: "utf8",
716
+ stdio: ["ignore", "pipe", "ignore"]
717
+ });
718
+ const kb = Number.parseInt(output.trim(), 10);
719
+ return Number.isFinite(kb) ? kb : null;
720
+ } catch {
721
+ return null;
722
+ }
723
+ };
724
+ var formatAge = (ms) => {
725
+ const totalSeconds = Math.max(0, Math.floor(ms / 1e3));
726
+ const hours = Math.floor(totalSeconds / 3600);
727
+ const minutes = Math.floor(totalSeconds % 3600 / 60);
728
+ if (hours > 0) return `${hours}h${String(minutes).padStart(2, "0")}m`;
729
+ if (minutes > 0) return `${minutes}m`;
730
+ return `${totalSeconds}s`;
731
+ };
732
+ var formatMemory = (kb) => kb === null ? "n/a" : kb >= 1024 ? `${Math.round(kb / 1024)} MB` : `${kb} KB`;
733
+ var describeSessionProof = (entry, now, rssKb) => [
734
+ entry.sessionId,
735
+ `pid ${entry.pid}`,
736
+ entry.alive ? "alive" : "dead, reaped",
737
+ `age ${formatAge(now - entry.startedAt)}`,
738
+ `seen ${formatAge(now - entry.lastSeenAt)} ago`,
739
+ `rss ${formatMemory(rssKb)}`,
740
+ ...entry.spawnId ? [`spawn ${entry.spawnId}`] : []
741
+ ].join(" ");
742
+ var describeSessionProofs = (entries, now, residentSet) => {
743
+ const live = entries.filter((entry) => entry.alive).length;
744
+ const reaped = entries.length - live;
745
+ if (entries.length === 0) return ["Sessions: none"];
746
+ const header = `Sessions: ${live} live${reaped > 0 ? `, ${reaped} dead proof${reaped === 1 ? "" : "s"} reaped` : ""}`;
747
+ return [
748
+ header,
749
+ ...entries.map((entry) => ` ${describeSessionProof(entry, now, entry.alive ? residentSet(entry.pid) : null)}`)
750
+ ];
632
751
  };
633
752
 
634
753
  // bin/pushary-daemon.ts
@@ -650,6 +769,11 @@ var main = async () => {
650
769
  if (action === "status") {
651
770
  const status = readDaemonStatus();
652
771
  process.stdout.write(`Pushary daemon: ${status.kind}${status.pid ? ` (pid ${status.pid})` : ""}
772
+ `);
773
+ if (status.lastError) process.stdout.write(`Last error: ${status.lastError.message} at ${status.lastError.at}
774
+ `);
775
+ const sessions = describeSessionProofs(listProcessProofs({ reapDead: true }), Date.now(), readResidentSetKb);
776
+ process.stdout.write(`${sessions.join("\n")}
653
777
  `);
654
778
  return status.kind === "running" ? EXIT.OK : EXIT.UNREACHABLE;
655
779
  }
@@ -8,16 +8,16 @@ import {
8
8
  openCodePluginDir,
9
9
  openCodePluginFile,
10
10
  removeOpenCodeMcpServer
11
- } from "../chunk-7DK3ZH3N.js";
11
+ } from "../chunk-CELX75MW.js";
12
12
  import {
13
13
  isClaudeMcpSnapshot,
14
14
  removeClaudeMcpServers,
15
15
  removeGeminiSettings,
16
16
  removePusharySettings,
17
17
  restoreClaudeMcpServer
18
- } from "../chunk-6V7ZDH3A.js";
19
- import "../chunk-KVHP5ZME.js";
20
- import "../chunk-VF3RTVWY.js";
18
+ } from "../chunk-GL3FFM7A.js";
19
+ import "../chunk-2LAHXHBR.js";
20
+ import "../chunk-EZIEXHYM.js";
21
21
  import "../chunk-L4ZCFMWW.js";
22
22
  import {
23
23
  claudeJson,
@@ -32,14 +32,14 @@ import {
32
32
  geminiSettings,
33
33
  removeCodexHooks,
34
34
  removeCodexSettings
35
- } from "../chunk-OVEGEQSB.js";
35
+ } from "../chunk-KUZWXPUS.js";
36
36
  import {
37
37
  exitOnHelpFlag
38
- } from "../chunk-OTPJ4FUR.js";
39
- import "../chunk-RLQWNEPM.js";
38
+ } from "../chunk-BVBEURWJ.js";
39
+ import "../chunk-43GMYSS4.js";
40
40
  import "../chunk-DINDL2CF.js";
41
41
  import "../chunk-KZERVKTD.js";
42
- import "../chunk-3MCEES6Z.js";
42
+ import "../chunk-VNMKIJZB.js";
43
43
 
44
44
  // bin/pushary-disconnect.ts
45
45
  import { readdirSync, readFileSync, rmSync, writeFileSync } from "fs";