@pushary/agent-hooks 0.89.8 → 0.89.9

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/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.89.9
4
+
5
+ ### Setup proves managed control reaches the server
6
+
7
+ The background daemon now records successful heartbeats separately from
8
+ successful control-queue responses. Setup and upgrade wait for both before
9
+ reporting managed control ready, and doctor reports either missing proof.
10
+
11
+ An authenticated, once-per-minute challenge round trip gives the server the
12
+ same readiness evidence without launching an agent or changing queued work.
13
+ Machine status exposes that observation additively so current clients keep
14
+ their existing fields while newer CLIs can verify capabilities end to end.
15
+
3
16
  ## 0.89.8
4
17
 
5
18
  ### Daemon upgrades keep a recoverable service
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  ensureDaemonRunning
4
- } from "../chunk-R2AGKCXL.js";
4
+ } from "../chunk-543KNJKC.js";
5
5
  import {
6
6
  buildTranscriptRecords,
7
7
  decodePublicKey,
@@ -24,7 +24,7 @@ import "../chunk-ATBKJNWS.js";
24
24
  import "../chunk-AIGDBRIJ.js";
25
25
  import {
26
26
  disableDaemonService
27
- } from "../chunk-R2AGKCXL.js";
27
+ } from "../chunk-543KNJKC.js";
28
28
  import "../chunk-XHKBHWLX.js";
29
29
  import {
30
30
  execNpm
@@ -177,7 +177,7 @@ var guardedDaemonRemoval = async () => {
177
177
  wouldNote("stop and uninstall", "Pushary background service");
178
178
  return true;
179
179
  }
180
- return (await disableDaemonService()).kind === "ok";
180
+ return (await disableDaemonService({ requireAbsenceProof: true })).kind === "ok";
181
181
  };
182
182
  var guardedInstructionBlockRemoval = (path) => {
183
183
  if (!dryRun) return removeInstructionBlock(path);
@@ -3,11 +3,11 @@ import {
3
3
  inspectConfiguredManagedProviders
4
4
  } from "../chunk-EA3E74P2.js";
5
5
  import "../chunk-J2JZJZKB.js";
6
+ import "../chunk-BAE45YLP.js";
6
7
  import {
7
8
  HEARTBEAT_INTERVAL_MS,
8
9
  sendHeartbeat
9
- } from "../chunk-ZL7HEF2T.js";
10
- import "../chunk-BAE45YLP.js";
10
+ } from "../chunk-U5TQCQ25.js";
11
11
  import "../chunk-VA7VUNRD.js";
12
12
  import "../chunk-ATBKJNWS.js";
13
13
  import "../chunk-AIGDBRIJ.js";
@@ -23,7 +23,7 @@ import {
23
23
  replaceDaemonService,
24
24
  requestDaemonHandoff,
25
25
  stopDaemonRunning
26
- } from "../chunk-R2AGKCXL.js";
26
+ } from "../chunk-543KNJKC.js";
27
27
  import {
28
28
  detectProviderLiveCapabilities,
29
29
  hasSpawnProvider,
@@ -116,8 +116,15 @@ var STOP_PROTOCOL_VERSION = 2;
116
116
  var MAX_SPAWNS_PER_WINDOW = 5;
117
117
  var SPAWN_WINDOW_MS = 6e4;
118
118
  var VERSION_CHECK_INTERVAL_MS = 2e3;
119
+ var CONTROL_PROBE_INTERVAL_MS = 6e4;
119
120
  var MAX_TRACKED_PROCESSES = 100;
120
121
  var MAX_COMPLETED_STOPS = 20;
122
+ var controlProbeDue = (lastProbeAt, at = Date.now()) => lastProbeAt === 0 || at - lastProbeAt >= CONTROL_PROBE_INTERVAL_MS;
123
+ var parseControlProbe = (value) => {
124
+ if (typeof value !== "object" || value === null) return null;
125
+ const id = value.id;
126
+ return typeof id === "string" && id.length > 0 && id.length <= 128 ? id : null;
127
+ };
121
128
  var daemonSignals = (platform = process.platform) => platform === "win32" ? ["SIGINT", "SIGTERM"] : ["SIGINT", "SIGTERM", "SIGHUP"];
122
129
  var parseSpawnClaim = (value) => {
123
130
  if (typeof value !== "object" || value === null) return null;
@@ -294,6 +301,7 @@ var runSpawnDaemon = async () => {
294
301
  let stopped = false;
295
302
  let idleTicks = 0;
296
303
  let errorStreak = 0;
304
+ let lastControlProbeAt = 0;
297
305
  const spawnTimestamps = [];
298
306
  const launchedProcesses = /* @__PURE__ */ new Map();
299
307
  const completedStops = /* @__PURE__ */ new Map();
@@ -432,18 +440,41 @@ var runSpawnDaemon = async () => {
432
440
  });
433
441
  }
434
442
  };
443
+ const acknowledgeControlProbe = async (probeId) => {
444
+ const response = await fetch(`${baseUrl}${DRAIN_PATH}`, {
445
+ method: "POST",
446
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}` },
447
+ body: JSON.stringify({ machineId, controlProbeAck: probeId }),
448
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
449
+ }).catch(() => null);
450
+ return response?.ok === true;
451
+ };
435
452
  const drain = async (signal) => {
453
+ const controlProbe = controlProbeDue(lastControlProbeAt);
454
+ if (controlProbe) lastControlProbeAt = Date.now();
436
455
  const res = await fetch(`${baseUrl}${DRAIN_PATH}`, {
437
456
  method: "POST",
438
457
  headers: { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}` },
439
- body: JSON.stringify({ machineId, spawnProtocol: SPAWN_PROTOCOL_VERSION, stopProtocol: STOP_PROTOCOL_VERSION }),
458
+ body: JSON.stringify({
459
+ machineId,
460
+ spawnProtocol: SPAWN_PROTOCOL_VERSION,
461
+ stopProtocol: STOP_PROTOCOL_VERSION,
462
+ controlProbe
463
+ }),
440
464
  signal
441
465
  });
442
466
  if (!res.ok) throw new DrainError(res.status);
443
467
  const data = await res.json();
444
468
  const response = typeof data === "object" && data !== null ? data : {};
445
469
  await Promise.all(parseStopRequests(response.stops).map(runStop));
446
- return parseSpawnClaim(response.spawn);
470
+ const controlProbeId = parseControlProbe(response.controlProbe);
471
+ return {
472
+ request: parseSpawnClaim(response.spawn),
473
+ controlProbeId,
474
+ // Released servers ignore the request field and omit the response field.
475
+ // Their successful authenticated drain remains the strongest proof they expose.
476
+ legacyControlProof: controlProbe && !Object.hasOwn(response, "controlProbe")
477
+ };
447
478
  };
448
479
  const acknowledge = async (body) => {
449
480
  if (!body) return;
@@ -507,18 +538,22 @@ var runSpawnDaemon = async () => {
507
538
  inflight = controller;
508
539
  const timeout = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
509
540
  try {
510
- const req = await drain(controller.signal);
541
+ const drained = await drain(controller.signal);
511
542
  if (!daemonLease.isOwner()) {
512
543
  finish();
513
544
  return;
514
545
  }
515
546
  errorStreak = 0;
516
- if (req) {
547
+ if (drained.request) {
517
548
  idleTicks = 0;
518
- await launchClaim(req);
549
+ await launchClaim(drained.request);
519
550
  } else {
520
551
  idleTicks++;
521
552
  }
553
+ const controlProved = drained.legacyControlProof || drained.controlProbeId !== null && await acknowledgeControlProbe(drained.controlProbeId);
554
+ if (controlProved) {
555
+ daemonLease.recordSuccessfulControl();
556
+ }
522
557
  } catch (err) {
523
558
  if (isAuthFailure(err)) {
524
559
  const status = err.status;
@@ -582,7 +617,7 @@ var runSpawnDaemon = async () => {
582
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.
583
618
  ` : `[pushary] daemon online \u2014 no supported Claude, Codex, or Gemini CLI was found, so phone-start is unavailable. Ctrl-C to stop.
584
619
  `);
585
- timer = setTimeout(tick, FAST_POLL_MS);
620
+ timer = setTimeout(tick, 0);
586
621
  const signals = daemonSignals();
587
622
  await new Promise((resolve) => {
588
623
  const onSignal = () => finish();
@@ -8,6 +8,9 @@ import {
8
8
  geminiWired,
9
9
  pusharyHookExecutable
10
10
  } from "../chunk-BAE45YLP.js";
11
+ import {
12
+ readMachineReadiness
13
+ } from "../chunk-U5TQCQ25.js";
11
14
  import {
12
15
  agentProbes,
13
16
  detectAgent,
@@ -31,10 +34,11 @@ import {
31
34
  import "../chunk-ATBKJNWS.js";
32
35
  import "../chunk-AIGDBRIJ.js";
33
36
  import {
37
+ daemonHasRemoteProof,
34
38
  daemonServiceManagerAvailable,
35
39
  hasDaemonServiceDefinition,
36
40
  readDaemonStatus
37
- } from "../chunk-R2AGKCXL.js";
41
+ } from "../chunk-543KNJKC.js";
38
42
  import {
39
43
  detectProviderLiveCapabilities,
40
44
  hasSpawnProvider,
@@ -558,7 +562,8 @@ var main = async () => {
558
562
  ...codexInPlay ? ["codex"] : [],
559
563
  ...geminiInPlay ? ["gemini"] : []
560
564
  ];
561
- const controlSupported = hasSpawnProvider(detectProviderLiveCapabilities(configuredManagedProviders));
565
+ const controlCapabilities = detectProviderLiveCapabilities(configuredManagedProviders);
566
+ const controlSupported = hasSpawnProvider(controlCapabilities);
562
567
  const controlMode = readControlMode();
563
568
  const daemon = readDaemonStatus();
564
569
  const serviceDefined = hasDaemonServiceDefinition();
@@ -571,8 +576,8 @@ var main = async () => {
571
576
  });
572
577
  const controlRequired = controlPolicy === "enabled";
573
578
  const controlUnsupported = controlMode === "on" && controlPolicy !== "enabled";
574
- const daemonOk = controlUnsupported ? false : controlPolicy === "disabled" ? daemon.kind === "absent" && !serviceDefined : controlPolicy === "enabled" ? daemon.kind === "running" : controlPolicy === "legacy" ? false : daemon.kind === "absent" && !serviceDefined;
575
- const daemonFailure = daemonOk ? null : controlUnsupported ? "control is required but no usable provider/service-manager pair is available" : controlPolicy === "disabled" ? `control is disabled but ${serviceDefined ? "the service definition remains" : `local state is ${daemon.kind}`}` : controlPolicy === "legacy" ? "a legacy service exists without a usable provider/service-manager pair" : `local state is ${daemon.kind}`;
579
+ const daemonOk = controlUnsupported ? false : controlPolicy === "disabled" ? daemon.kind === "absent" && !serviceDefined : controlPolicy === "enabled" ? daemonHasRemoteProof(daemon) : controlPolicy === "legacy" ? false : daemon.kind === "absent" && !serviceDefined;
580
+ const daemonFailure = daemonOk ? null : controlUnsupported ? "control is required but no usable provider/service-manager pair is available" : controlPolicy === "enabled" && daemon.kind === "running" ? "the daemon has not completed an authenticated heartbeat and control poll" : controlPolicy === "disabled" ? `control is disabled but ${serviceDefined ? "the service definition remains" : `local state is ${daemon.kind}`}` : controlPolicy === "legacy" ? "a legacy service exists without a usable provider/service-manager pair" : `local state is ${daemon.kind}`;
576
581
  const daemonDetail = daemonFailure ? controlUnsupported ? `${daemonFailure}; re-run setup with \`--control off\`, or restore both requirements` : controlPolicy === "disabled" ? `${daemonFailure}; re-run setup with \`--control off\`` : controlPolicy === "legacy" ? `${daemonFailure}; run setup with \`--control off\`, or restore both requirements` : `${daemonFailure}; run \`pushary daemon install\` to install or repair the background service` : controlPolicy === "disabled" ? "disabled by setup" : controlPolicy === "preserve-absent" ? "legacy absence preserved" : !controlRequired && daemon.kind === "absent" ? "managed control is unsupported on this provider or operating-system session" : null;
577
582
  check(
578
583
  daemonOk,
@@ -584,9 +589,25 @@ var main = async () => {
584
589
  `daemon ${daemon.daemonVersion ?? "none"}`,
585
590
  `installed ${daemon.installedVersion}`,
586
591
  `machine ${daemon.machineId ?? "none"}`,
587
- `heartbeat ${daemon.lastSuccessfulHeartbeatAt ?? "none yet"}`
592
+ `heartbeat ${daemon.lastSuccessfulHeartbeatAt ?? "none yet"}`,
593
+ `control ${daemon.lastSuccessfulControlAt ?? "none yet"}`
588
594
  ].filter((value) => value !== null).join(" \xB7 ")
589
595
  );
596
+ if (controlPolicy === "enabled" && apiKey && daemon.machineId) {
597
+ const serverProof = await readMachineReadiness(apiKey, ["spawn", ...controlCapabilities], {
598
+ machineId: daemon.machineId
599
+ });
600
+ if (serverProof.kind === "unsupported") {
601
+ console.log(` ${warn} Remote-control server proof unavailable ${dim(`(${serverProof.detail})`)}`);
602
+ } else {
603
+ check(
604
+ serverProof.kind === "ready",
605
+ "Remote-control server proof",
606
+ serverProof.kind === "ready" ? "heartbeat \xB7 capabilities \xB7 control queue" : serverProof.detail,
607
+ serverProof.kind === "failed" ? "unreachable" : void 0
608
+ );
609
+ }
610
+ }
590
611
  const cursorPluginDir2 = cursorPluginDir();
591
612
  if (existsSync(cursorPluginDir2)) {
592
613
  checking("Cursor");
@@ -7,6 +7,9 @@ import {
7
7
  codexWired,
8
8
  geminiWired
9
9
  } from "../chunk-BAE45YLP.js";
10
+ import {
11
+ waitForMachineReadiness
12
+ } from "../chunk-U5TQCQ25.js";
10
13
  import {
11
14
  runBrowserLogin
12
15
  } from "../chunk-Z3Z4VK5F.js";
@@ -50,14 +53,16 @@ import {
50
53
  guardedArgv
51
54
  } from "../chunk-AIGDBRIJ.js";
52
55
  import {
56
+ daemonHasRemoteProof,
53
57
  daemonServiceManagerAvailable,
54
58
  disableDaemonService,
55
59
  ensureDaemonRunning,
56
60
  hasDaemonServiceDefinition,
57
61
  readDaemonStatus,
58
62
  replaceDaemonService,
59
- stopDaemonService
60
- } from "../chunk-R2AGKCXL.js";
63
+ stopDaemonService,
64
+ waitForDaemonRemoteProof
65
+ } from "../chunk-543KNJKC.js";
61
66
  import {
62
67
  detectProviderLiveCapabilities,
63
68
  hasSpawnProvider,
@@ -2125,6 +2130,14 @@ var main = async () => {
2125
2130
  return { kind: "unavailable", detail: formatError(err) };
2126
2131
  }
2127
2132
  })() : controlMode === "on" || controlPolicy === "legacy" ? { kind: "unavailable", detail: "Managed control is required but no usable provider/service-manager pair is available" } : null;
2133
+ const localControlProof = daemon?.kind === "running" || daemon?.kind === "started" ? await waitForDaemonRemoteProof() : null;
2134
+ const serverControlProof = localControlProof && daemonHasRemoteProof(localControlProof) ? await waitForMachineReadiness(trimmedKey, ["spawn", ...controlCapabilities]) : null;
2135
+ const daemonReady = daemon === null ? null : localControlProof !== null && daemonHasRemoteProof(localControlProof) && (serverControlProof?.kind === "ready" || serverControlProof?.kind === "unsupported");
2136
+ if (serverControlProof?.kind === "unsupported") {
2137
+ console.log(` ${yellow("!")} This server predates control-readiness reporting; local heartbeat and queue checks passed.`);
2138
+ } else if (serverControlProof && serverControlProof.kind !== "ready") {
2139
+ console.log(` ${yellow("!")} Managed control could not be verified by the server: ${serverControlProof.detail}`);
2140
+ }
2128
2141
  const closing = describeClosing({
2129
2142
  agentsOk,
2130
2143
  configuredNothing,
@@ -2132,7 +2145,7 @@ var main = async () => {
2132
2145
  failed,
2133
2146
  reachChecked,
2134
2147
  reachesYou,
2135
- daemonReady: controlLifecycleFailure ? false : daemon === null ? null : daemon.kind === "running" || daemon.kind === "started",
2148
+ daemonReady: controlLifecycleFailure ? false : daemonReady,
2136
2149
  // null when the check did not run, so it changes nothing. Only an outright
2137
2150
  // `false` is allowed to hold back "Setup complete.".
2138
2151
  roundTripAnswered: roundTrip === null ? null : roundTrip.kind === "answered" ? true : roundTrip.kind === "unanswered" ? false : null
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  describeLastSeen,
4
4
  listMachines
5
- } from "../chunk-ZL7HEF2T.js";
5
+ } from "../chunk-U5TQCQ25.js";
6
6
  import "../chunk-DKDDLF5P.js";
7
7
  import "../chunk-GMI5QZLG.js";
8
8
  import {
@@ -4,18 +4,23 @@ import {
4
4
  } from "../chunk-EA3E74P2.js";
5
5
  import "../chunk-J2JZJZKB.js";
6
6
  import "../chunk-BAE45YLP.js";
7
+ import {
8
+ waitForMachineReadiness
9
+ } from "../chunk-U5TQCQ25.js";
7
10
  import "../chunk-VA7VUNRD.js";
8
11
  import "../chunk-ATBKJNWS.js";
9
12
  import "../chunk-AIGDBRIJ.js";
10
13
  import {
14
+ daemonHasRemoteProof,
11
15
  daemonServiceManagerAvailable,
12
16
  disableDaemonService,
13
17
  ensureDaemonRunning,
14
18
  hasDaemonServiceDefinition,
15
19
  readDaemonStatus,
16
20
  replaceDaemonService,
17
- stopDaemonService
18
- } from "../chunk-R2AGKCXL.js";
21
+ stopDaemonService,
22
+ waitForDaemonRemoteProof
23
+ } from "../chunk-543KNJKC.js";
19
24
  import {
20
25
  detectProviderLiveCapabilities,
21
26
  hasSpawnProvider,
@@ -129,9 +134,10 @@ var finishUpgrade = async () => {
129
134
  const controlMode = readControlMode();
130
135
  const daemonStatus = readDaemonStatus();
131
136
  const serviceWasPresent = daemonStatus.kind !== "absent" || hasDaemonServiceDefinition();
137
+ const controlCapabilities = detectProviderLiveCapabilities(configuredControlProviders);
132
138
  const controlPolicy = resolveControlPolicy({
133
139
  mode: controlMode,
134
- providerAvailable: hasSpawnProvider(detectProviderLiveCapabilities(configuredControlProviders)),
140
+ providerAvailable: hasSpawnProvider(controlCapabilities),
135
141
  managerAvailable: daemonServiceManagerAvailable(),
136
142
  servicePresent: serviceWasPresent
137
143
  });
@@ -150,7 +156,9 @@ var finishUpgrade = async () => {
150
156
  const service = await replaceDaemonService();
151
157
  const daemon = service.kind === "ok" ? await ensureDaemonRunning({ spawnDaemon: () => {
152
158
  } }) : null;
153
- daemonReady = service.kind === "ok" && (daemon?.kind === "running" || daemon?.kind === "started");
159
+ const localProof = daemon?.kind === "running" || daemon?.kind === "started" ? await waitForDaemonRemoteProof() : null;
160
+ const serverProof = localProof && daemonHasRemoteProof(localProof) ? await waitForMachineReadiness(apiKey, ["spawn", ...controlCapabilities]) : null;
161
+ daemonReady = service.kind === "ok" && localProof !== null && daemonHasRemoteProof(localProof) && (serverProof?.kind === "ready" || serverProof?.kind === "unsupported");
154
162
  }
155
163
  } else if (controlMode === "on" && controlPolicy === "unsupported") {
156
164
  daemonReady = false;
@@ -31,6 +31,14 @@ import { createHash, randomUUID } from "crypto";
31
31
  import { homedir } from "os";
32
32
  import { execFileSync } from "child_process";
33
33
  var daemonNeedsHandoff = (status) => status.kind === "version-mismatch" || status.kind === "configuration-mismatch" || status.kind === "unresponsive" && status.processAlive === true;
34
+ var DAEMON_REMOTE_PROOF_MAX_AGE_MS = 18e4;
35
+ var DAEMON_REMOTE_PROOF_CLOCK_SKEW_MS = 3e4;
36
+ var remoteProofIsFresh = (timestamp, now) => {
37
+ if (timestamp === null) return false;
38
+ const age = now - Date.parse(timestamp);
39
+ return Number.isFinite(age) && age >= -DAEMON_REMOTE_PROOF_CLOCK_SKEW_MS && age < DAEMON_REMOTE_PROOF_MAX_AGE_MS;
40
+ };
41
+ var daemonHasRemoteProof = (status, now = Date.now()) => status.kind === "running" && remoteProofIsFresh(status.lastSuccessfulHeartbeatAt, now) && remoteProofIsFresh(status.lastSuccessfulControlAt, now);
34
42
  var runDaemonServiceCommand = (command, args) => execFileSync(command, [...args], {
35
43
  encoding: "utf-8",
36
44
  stdio: ["ignore", "pipe", "ignore"],
@@ -241,7 +249,7 @@ WantedBy=default.target
241
249
  "-NoProfile",
242
250
  "-NonInteractive",
243
251
  "-Command",
244
- "$task = @(Get-ScheduledTask -ErrorAction Stop) | Where-Object { $_.TaskName -eq 'Pushary Daemon' -and $_.TaskPath -eq '\\' } | Select-Object -First 1; if ($null -eq $task) { exit 3 }; if ($task.State -eq 'Disabled') { exit 4 }"
252
+ "$task = @(Get-ScheduledTask -ErrorAction Stop) | Where-Object { $_.TaskName -eq 'Pushary Daemon' -and $_.TaskPath -eq '\\' } | Select-Object -First 1; if ($null -eq $task) { exit 3 }; if ($null -eq $task.Settings -or $null -eq $task.Settings.Enabled) { exit 5 }; if (-not $task.Settings.Enabled) { exit 4 }"
245
253
  ],
246
254
  negativeExitCodes: [3, 4]
247
255
  },
@@ -313,16 +321,10 @@ var applyDaemonService = (action, definition, options = {}) => {
313
321
  const steps = action === "uninstall" ? [...definition.stop, ...definition.uninstall] : definition[action];
314
322
  runDaemonServiceSteps(steps, run);
315
323
  if (action === "uninstall") {
316
- let registered = true;
317
- try {
318
- run(definition.registrationProbe.command, definition.registrationProbe.args);
319
- } catch (err) {
320
- if (!isExpectedProbeFailure(definition.registrationProbe, err)) {
321
- throw new Error(`Could not verify native background service is absent: ${commandFailureOutput(err) || String(err)}`);
322
- }
323
- registered = false;
324
- }
325
- if (registered) throw new Error("Native background service remains registered");
324
+ const state = probeDaemonServiceState(definition, run);
325
+ if (state.kind !== "ok") throw new Error(`Could not verify native background service is absent: ${state.detail}`);
326
+ if (state.registered) throw new Error("Native background service remains registered");
327
+ if (state.running) throw new Error("Native background service remains active");
326
328
  safeUnlink(definition.definitionPath);
327
329
  const reload = definition.reloadAfterUninstall;
328
330
  if (reload) run(reload.command, reload.args);
@@ -412,7 +414,7 @@ var currentConfigurationId = () => {
412
414
  var parseDaemonState = (value) => {
413
415
  if (typeof value !== "object" || value === null) return null;
414
416
  const state = value;
415
- if (!isString(state.instanceId) || !Number.isInteger(state.pid) || state.pid <= 0 || !isString(state.version) || !(state.configurationId === void 0 || state.configurationId === null || isString(state.configurationId)) || !isString(state.machineId) || !isTimestamp(state.startedAt) || !isTimestamp(state.updatedAt) || !(state.lastSuccessfulHeartbeatAt === null || isTimestamp(state.lastSuccessfulHeartbeatAt))) return null;
417
+ if (!isString(state.instanceId) || !Number.isInteger(state.pid) || state.pid <= 0 || !isString(state.version) || !(state.configurationId === void 0 || state.configurationId === null || isString(state.configurationId)) || !isString(state.machineId) || !isTimestamp(state.startedAt) || !isTimestamp(state.updatedAt) || !(state.lastSuccessfulHeartbeatAt === null || isTimestamp(state.lastSuccessfulHeartbeatAt)) || !(state.lastSuccessfulControlAt === void 0 || state.lastSuccessfulControlAt === null || isTimestamp(state.lastSuccessfulControlAt))) return null;
416
418
  return {
417
419
  instanceId: state.instanceId,
418
420
  pid: state.pid,
@@ -421,7 +423,8 @@ var parseDaemonState = (value) => {
421
423
  machineId: state.machineId,
422
424
  startedAt: state.startedAt,
423
425
  updatedAt: state.updatedAt,
424
- lastSuccessfulHeartbeatAt: state.lastSuccessfulHeartbeatAt
426
+ lastSuccessfulHeartbeatAt: state.lastSuccessfulHeartbeatAt,
427
+ lastSuccessfulControlAt: state.lastSuccessfulControlAt ?? null
425
428
  };
426
429
  };
427
430
  var parseLockOwner = (value) => {
@@ -443,7 +446,8 @@ var readDaemonStatus = (options = {}) => {
443
446
  daemonVersion: null,
444
447
  startedAt: null,
445
448
  updatedAt: null,
446
- lastSuccessfulHeartbeatAt: null
449
+ lastSuccessfulHeartbeatAt: null,
450
+ lastSuccessfulControlAt: null
447
451
  };
448
452
  }
449
453
  const value = state.kind === "ok" ? parseDaemonState(state.value) : null;
@@ -460,7 +464,8 @@ var readDaemonStatus = (options = {}) => {
460
464
  daemonVersion: value.version,
461
465
  startedAt: value.startedAt,
462
466
  updatedAt: value.updatedAt,
463
- lastSuccessfulHeartbeatAt: value.lastSuccessfulHeartbeatAt
467
+ lastSuccessfulHeartbeatAt: value.lastSuccessfulHeartbeatAt,
468
+ lastSuccessfulControlAt: value.lastSuccessfulControlAt
464
469
  };
465
470
  }
466
471
  return {
@@ -472,7 +477,8 @@ var readDaemonStatus = (options = {}) => {
472
477
  daemonVersion: null,
473
478
  startedAt: null,
474
479
  updatedAt: null,
475
- lastSuccessfulHeartbeatAt: null
480
+ lastSuccessfulHeartbeatAt: null,
481
+ lastSuccessfulControlAt: null
476
482
  };
477
483
  };
478
484
  var safeUnlink = (path) => {
@@ -540,7 +546,8 @@ var acquireDaemonLease = (options = {}) => {
540
546
  machineId,
541
547
  startedAt,
542
548
  updatedAt: startedAt,
543
- lastSuccessfulHeartbeatAt: null
549
+ lastSuccessfulHeartbeatAt: null,
550
+ lastSuccessfulControlAt: null
544
551
  };
545
552
  try {
546
553
  writeJsonAtomic(statePath(dir), state, 384);
@@ -576,6 +583,14 @@ var acquireDaemonLease = (options = {}) => {
576
583
  lastSuccessfulHeartbeatAt: timestamp
577
584
  });
578
585
  },
586
+ recordSuccessfulControl: (at = now()) => {
587
+ const timestamp = new Date(at).toISOString();
588
+ updateState({
589
+ ...currentState,
590
+ updatedAt: timestamp,
591
+ lastSuccessfulControlAt: timestamp
592
+ });
593
+ },
579
594
  shouldYield: () => {
580
595
  if (!isOwner()) return true;
581
596
  const target = readJsonSafe(targetPath(dir));
@@ -645,7 +660,8 @@ var deactivateDaemonService = async (action, options) => {
645
660
  const platform = options.platform ?? process.platform;
646
661
  const supported = ["darwin", "linux", "win32"].includes(platform);
647
662
  const managerAvailable = supported && daemonServiceManagerAvailable(options);
648
- const service = supported && !managerAvailable ? { kind: "failed", detail: "Native background-service manager is unavailable" } : managerAvailable ? manageDaemonService(action, options) : { kind: "ok" };
663
+ const absenceMustBeProven = options.requireAbsenceProof || hasDaemonServiceDefinition(options);
664
+ const service = supported && !managerAvailable && absenceMustBeProven ? { kind: "failed", detail: "Native background-service manager is unavailable" } : managerAvailable ? manageDaemonService(action, options) : { kind: "ok" };
649
665
  const stopped = await stopDaemonRunning(options);
650
666
  const failures = [
651
667
  service.kind === "ok" ? null : service.detail,
@@ -956,8 +972,21 @@ var ensureDaemonRunning = async (options = {}) => {
956
972
  }
957
973
  return { kind: "unavailable", status, detail: `daemon did not become healthy (${status.kind})` };
958
974
  };
975
+ var waitForDaemonRemoteProof = async (options = {}) => {
976
+ const sleep = options.sleep ?? wait;
977
+ const pollIntervalMs = options.pollIntervalMs ?? 250;
978
+ const attempts = Math.max(1, Math.ceil((options.waitTimeoutMs ?? 5e3) / pollIntervalMs));
979
+ let status = readDaemonStatus(options);
980
+ for (let attempt = 0; attempt < attempts; attempt++) {
981
+ status = readDaemonStatus(options);
982
+ if (daemonHasRemoteProof(status)) return status;
983
+ if (attempt + 1 < attempts) await sleep(pollIntervalMs);
984
+ }
985
+ return status;
986
+ };
959
987
 
960
988
  export {
989
+ daemonHasRemoteProof,
961
990
  daemonServiceManagerAvailable,
962
991
  DAEMON_LEASE_RENEW_INTERVAL_MS,
963
992
  parseDaemonAction,
@@ -970,5 +999,6 @@ export {
970
999
  stopDaemonService,
971
1000
  disableDaemonService,
972
1001
  replaceDaemonService,
973
- ensureDaemonRunning
1002
+ ensureDaemonRunning,
1003
+ waitForDaemonRemoteProof
974
1004
  };
@@ -46,9 +46,38 @@ var parseMachine = (value) => {
46
46
  repoKey: typeof raw.repoKey === "string" ? raw.repoKey : null,
47
47
  capabilities: Array.isArray(raw.capabilities) ? raw.capabilities.filter((c) => typeof c === "string") : [],
48
48
  lastSeenAt: typeof raw.lastSeenAt === "string" ? raw.lastSeenAt : "",
49
+ lastControlAt: typeof raw.lastControlAt === "string" ? raw.lastControlAt : raw.lastControlAt === null ? null : void 0,
49
50
  online: raw.online === true
50
51
  };
51
52
  };
53
+ var machineReadiness = (machines, machineId, expectedCapabilities) => {
54
+ const machine = machines.find((item) => item.machineId === machineId);
55
+ if (!machine) return { kind: "not-ready", detail: "this machine has not heartbeated to the server" };
56
+ if (!machine.online) return { kind: "not-ready", detail: "the server reports this machine offline" };
57
+ if (machine.lastControlAt === void 0) {
58
+ return { kind: "unsupported", detail: "the server does not report control readiness yet" };
59
+ }
60
+ if (machine.lastControlAt === null || !Number.isFinite(Date.parse(machine.lastControlAt))) {
61
+ return { kind: "not-ready", detail: "the server has not observed this machine polling the control queue" };
62
+ }
63
+ const missing = expectedCapabilities.filter((capability) => !machine.capabilities.includes(capability));
64
+ return missing.length === 0 ? { kind: "ready", machine } : { kind: "not-ready", detail: `the server is missing capabilities: ${missing.join(", ")}` };
65
+ };
66
+ var readMachineReadiness = async (apiKey, expectedCapabilities, deps = {}) => {
67
+ const listed = await listMachines(apiKey, deps);
68
+ return listed.kind === "ok" ? machineReadiness(listed.machines, deps.machineId ?? getMachineId(), expectedCapabilities) : listed.kind === "unsupported" ? { kind: "unsupported", detail: "the server does not report machine readiness yet" } : listed;
69
+ };
70
+ var waitForMachineReadiness = async (apiKey, expectedCapabilities, deps = {}) => {
71
+ const attempts = deps.attempts ?? 21;
72
+ const sleep = deps.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
73
+ let result = { kind: "not-ready", detail: "this machine has not heartbeated to the server" };
74
+ for (let attempt = 0; attempt < attempts; attempt++) {
75
+ result = await readMachineReadiness(apiKey, expectedCapabilities, deps);
76
+ if (result.kind === "ready" || result.kind === "unsupported" || result.kind === "failed" && /^HTTP (?:401|403)$/.test(result.detail)) return result;
77
+ if (attempt + 1 < attempts) await sleep(250);
78
+ }
79
+ return result;
80
+ };
52
81
  var listMachines = async (apiKey, deps = {}) => {
53
82
  const fetchImpl = deps.fetchImpl ?? fetch;
54
83
  const baseUrl = deps.baseUrl ?? getBaseUrl();
@@ -81,6 +110,8 @@ var describeLastSeen = (iso, now = Date.now()) => {
81
110
  export {
82
111
  HEARTBEAT_INTERVAL_MS,
83
112
  sendHeartbeat,
113
+ readMachineReadiness,
114
+ waitForMachineReadiness,
84
115
  listMachines,
85
116
  describeLastSeen
86
117
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pushary/agent-hooks",
3
- "version": "0.89.8",
3
+ "version": "0.89.9",
4
4
  "description": "Permission hooks for AI coding agents: route tool approvals through Pushary push notifications",
5
5
  "keywords": [
6
6
  "pushary",