@pushary/agent-hooks 0.92.0 → 0.92.1

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,28 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.92.1
4
+
5
+ ### A rejected key is an answer, not an empty poll
6
+
7
+ `pollAgent` returned `{ command: null, mode: null }` for every non-OK response,
8
+ so a 401 was indistinguishable from a healthy poll with nothing queued. That
9
+ landed on the success branch of `startCommandPoller`, which resets `errorStreak`
10
+ to zero, so the backoff never engaged and the wrapper polled a dead key at the
11
+ idle cadence forever. Because `mode` was null, `onModeState` never fired, and
12
+ `onModeState` is the only place `dualMode` sets `killed` and stops the active
13
+ leg. A wrapper whose key the server rejects therefore reported `kill=false` to
14
+ itself indefinitely: remote stop could not reach it, phone messages never
15
+ landed, and nothing was printed.
16
+
17
+ `PollResult` now carries `rejected`, set only for 401 and 403, which is the same
18
+ line `fetchModeState` already drew between an answer and a blip. A rejected poll
19
+ takes the error path, so the existing exponential backoff applies, and the
20
+ wrapper prints once that remote stop and phone messages are not active. A 500 or
21
+ a dropped connection is still a blip and still retried at the normal cadence.
22
+
23
+ A rejected response is also no longer trusted for its contents: neither the
24
+ command nor the mode state it carries is applied.
25
+
3
26
  ## 0.92.0
4
27
 
5
28
  ### A Claude Code upgrade registers the events it just gained
@@ -744,6 +744,8 @@ var FAST_DECAY_TICKS = 3;
744
744
  var MAX_ERROR_BACKOFF_MS = 12e4;
745
745
  var MAX_ERROR_STREAK = 6;
746
746
  var REQUEST_TIMEOUT_MS = 1e4;
747
+ var PollRejectedError = class extends Error {
748
+ };
747
749
  var startCommandPoller = (opts) => {
748
750
  const drain = opts.drain ?? drainPendingCommand;
749
751
  const slow = opts.slowPollMs ?? SLOW_POLL_MS;
@@ -754,6 +756,16 @@ var startCommandPoller = (opts) => {
754
756
  let inflight;
755
757
  let idleTicks = 0;
756
758
  let errorStreak = 0;
759
+ let authRejectionReported = false;
760
+ const reportAuthRejection = () => {
761
+ if (authRejectionReported) return;
762
+ authRejectionReported = true;
763
+ try {
764
+ opts.onAuthRejected?.();
765
+ } catch {
766
+ return;
767
+ }
768
+ };
757
769
  const jitter = (ms) => {
758
770
  const r = opts.jitter ? opts.jitter() : Math.random();
759
771
  return Math.max(1, Math.round(ms * (0.85 + r * 0.3)));
@@ -789,6 +801,10 @@ var startCommandPoller = (opts) => {
789
801
  let command;
790
802
  if (opts.poll) {
791
803
  const result = await opts.poll(opts.apiKey, opts.getSessionId?.(), wantCommands, controller.signal);
804
+ if (result.rejected) {
805
+ reportAuthRejection();
806
+ throw new PollRejectedError();
807
+ }
792
808
  command = result.command;
793
809
  if (result.mode && opts.onModeState) {
794
810
  try {
@@ -845,6 +861,7 @@ var startCommandPoller = (opts) => {
845
861
 
846
862
  // src/wrapper/pollClient.ts
847
863
  var APPROVAL_MODES = /* @__PURE__ */ new Set(["push_only", "terminal_only", "push_first", "notify_only"]);
864
+ var isRejection = (status) => status === 401 || status === 403;
848
865
  var pollAgent = async (apiKey, sessionId, wantCommand, signal, baseUrl = getBaseUrl(), machineId) => {
849
866
  try {
850
867
  const sessionKey = sessionId || DEFAULT_SESSION;
@@ -894,7 +911,7 @@ var pollAgent = async (apiKey, sessionId, wantCommand, signal, baseUrl = getBase
894
911
  }),
895
912
  signal
896
913
  });
897
- if (!response.ok) return { command: null, mode: null };
914
+ if (!response.ok) return { command: null, mode: null, rejected: isRejection(response.status) };
898
915
  const data = await response.json();
899
916
  const claimedCommand = (id, text) => ({
900
917
  id,
@@ -923,9 +940,9 @@ var pollAgent = async (apiKey, sessionId, wantCommand, signal, baseUrl = getBase
923
940
  policyVersion: typeof data.policyVersion === "string" && data.policyVersion.length > 0 ? data.policyVersion : null,
924
941
  relayUrl: typeof data.relayUrl === "string" && data.relayUrl.length > 0 ? data.relayUrl : null
925
942
  };
926
- return { command, mode };
943
+ return { command, mode, rejected: false };
927
944
  } catch {
928
- return { command: null, mode: null };
945
+ return { command: null, mode: null, rejected: false };
929
946
  }
930
947
  };
931
948
 
@@ -1734,6 +1751,11 @@ var runDualMode = async (binary, args2, startingMode) => {
1734
1751
  // Relay-primary: idle the safety-net poll at 60s (see RELAY_FALLBACK_POLL_MS).
1735
1752
  // No relay advertised -> the poller IS the command path, so keep the default.
1736
1753
  slowPollMs: relayAdvertised ? RELAY_FALLBACK_POLL_MS : void 0,
1754
+ onAuthRejected: () => {
1755
+ process.stderr.write(
1756
+ "[pushary] this key was rejected \u2014 remote stop and phone messages are NOT active; run `pushary login` to reconnect\n"
1757
+ );
1758
+ },
1737
1759
  onModeState: (state) => {
1738
1760
  if (state.kill && !killed) {
1739
1761
  killed = true;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pushary/agent-hooks",
3
- "version": "0.92.0",
3
+ "version": "0.92.1",
4
4
  "description": "Permission hooks for AI coding agents: route tool approvals through Pushary push notifications",
5
5
  "keywords": [
6
6
  "pushary",