@pushary/agent-hooks 0.92.0 → 0.93.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 (54) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/dist/bin/pushary-bell.js +1 -1
  3. package/dist/bin/pushary-claude.js +59 -21
  4. package/dist/bin/pushary-clean.js +9 -9
  5. package/dist/bin/pushary-codex-bridge.js +5 -5
  6. package/dist/bin/pushary-codex-hook.js +44 -10
  7. package/dist/bin/pushary-codex.js +7 -6
  8. package/dist/bin/pushary-connect.js +2 -2
  9. package/dist/bin/pushary-daemon.js +10 -10
  10. package/dist/bin/pushary-disconnect.js +3 -3
  11. package/dist/bin/pushary-doctor.js +21 -14
  12. package/dist/bin/pushary-elicitation-hook.js +4 -4
  13. package/dist/bin/pushary-gemini-bridge.js +5 -5
  14. package/dist/bin/pushary-gemini-hook.js +7 -6
  15. package/dist/bin/pushary-hook.js +8 -8
  16. package/dist/bin/pushary-login.js +3 -3
  17. package/dist/bin/pushary-logout.js +1 -1
  18. package/dist/bin/pushary-notification-hook.js +7 -6
  19. package/dist/bin/pushary-opencode-hook.js +7 -6
  20. package/dist/bin/pushary-permission-denied-hook.js +8 -8
  21. package/dist/bin/pushary-permission-hook.js +8 -8
  22. package/dist/bin/pushary-post-hook.js +7 -6
  23. package/dist/bin/pushary-prompt-hook.js +7 -6
  24. package/dist/bin/pushary-session-end-hook.js +7 -6
  25. package/dist/bin/pushary-session-start-hook.js +11 -11
  26. package/dist/bin/pushary-setup.js +15 -15
  27. package/dist/bin/pushary-status.js +5 -5
  28. package/dist/bin/pushary-stop-hook.js +7 -6
  29. package/dist/bin/pushary-stopfailure-hook.js +7 -6
  30. package/dist/bin/pushary-upgrade.js +9 -9
  31. package/dist/{chunk-ZMKKK6DC.js → chunk-6Y4IKMQT.js} +2 -2
  32. package/dist/{chunk-2ACC5M7F.js → chunk-72FWXLW2.js} +1 -1
  33. package/dist/{chunk-QZCFVWA7.js → chunk-EQRS3SGM.js} +3 -1
  34. package/dist/{chunk-BSZYIAZL.js → chunk-EYRGBYZG.js} +2 -1
  35. package/dist/{chunk-YXDYKECP.js → chunk-G5L3WVFV.js} +117 -80
  36. package/dist/{chunk-4TFXV5MW.js → chunk-GKMQMSTB.js} +1 -1
  37. package/dist/{chunk-RQPQSJJH.js → chunk-HCLSTQ3N.js} +23 -8
  38. package/dist/{chunk-CNPAT3HL.js → chunk-KHTFHUL3.js} +1 -1
  39. package/dist/{chunk-CV2YD2JR.js → chunk-N3TF3RQ2.js} +42 -10
  40. package/dist/{chunk-3HS4YGHB.js → chunk-NXZYC3B3.js} +1 -1
  41. package/dist/{chunk-QCXKQJTG.js → chunk-NZQTIDXN.js} +2 -2
  42. package/dist/chunk-QQSQWLFR.js +53 -0
  43. package/dist/{chunk-F7OURW3C.js → chunk-RER3FRTD.js} +7 -7
  44. package/dist/{chunk-2XQSN66Q.js → chunk-T2PKGGDM.js} +1 -1
  45. package/dist/{chunk-I3ORVBTP.js → chunk-TFN7ZCTI.js} +4 -4
  46. package/dist/{chunk-SQYBP3QQ.js → chunk-TWVZHZM3.js} +2 -2
  47. package/dist/{chunk-L4E2BIJ2.js → chunk-UAFQC35P.js} +56 -3
  48. package/dist/{chunk-PYA4FLKI.js → chunk-UPCPR7QW.js} +1 -1
  49. package/dist/{chunk-HDORB7WD.js → chunk-WEDYCMHU.js} +1 -1
  50. package/dist/{chunk-I7W3UTDK.js → chunk-YUZYMAL7.js} +9 -7
  51. package/dist/src/index.d.ts +31 -3
  52. package/dist/src/index.js +9 -9
  53. package/package.json +2 -2
  54. package/dist/chunk-2LAHXHBR.js +0 -23
@@ -1,14 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  reapplyAllAgents
4
- } from "../chunk-I3ORVBTP.js";
5
- import "../chunk-HDORB7WD.js";
4
+ } from "../chunk-TFN7ZCTI.js";
5
+ import "../chunk-WEDYCMHU.js";
6
6
  import {
7
7
  waitForMachineReadiness
8
- } from "../chunk-2XQSN66Q.js";
9
- import "../chunk-ZMKKK6DC.js";
10
- import "../chunk-I7W3UTDK.js";
11
- import "../chunk-2LAHXHBR.js";
8
+ } from "../chunk-T2PKGGDM.js";
9
+ import "../chunk-6Y4IKMQT.js";
10
+ import "../chunk-YUZYMAL7.js";
12
11
  import "../chunk-EZIEXHYM.js";
13
12
  import {
14
13
  daemonHasRemoteProof,
@@ -20,7 +19,7 @@ import {
20
19
  replaceDaemonService,
21
20
  stopDaemonService,
22
21
  waitForDaemonRemoteProof
23
- } from "../chunk-SQYBP3QQ.js";
22
+ } from "../chunk-TWVZHZM3.js";
24
23
  import {
25
24
  detectProviderLiveCapabilities,
26
25
  hasSpawnProvider,
@@ -34,14 +33,15 @@ import {
34
33
  npmErrorMessage,
35
34
  resolveGlobalBinary
36
35
  } from "../chunk-L4ZCFMWW.js";
37
- import "../chunk-2ACC5M7F.js";
36
+ import "../chunk-QQSQWLFR.js";
37
+ import "../chunk-72FWXLW2.js";
38
38
  import {
39
39
  claudeJson,
40
40
  codexConfigToml,
41
41
  cursorPluginDir,
42
42
  geminiSettings,
43
43
  vscodePluginDir
44
- } from "../chunk-CV2YD2JR.js";
44
+ } from "../chunk-N3TF3RQ2.js";
45
45
  import {
46
46
  exitOnHelpFlag
47
47
  } from "../chunk-BVBEURWJ.js";
@@ -2,11 +2,11 @@ import {
2
2
  CLAUDE_HOOK_EVENTS,
3
3
  GEMINI_HOOK_EVENTS,
4
4
  hasGeminiHooks
5
- } from "./chunk-I7W3UTDK.js";
5
+ } from "./chunk-YUZYMAL7.js";
6
6
  import {
7
7
  CODEX_HOOK_EVENTS,
8
8
  hasCodexHooks
9
- } from "./chunk-CV2YD2JR.js";
9
+ } from "./chunk-N3TF3RQ2.js";
10
10
  import {
11
11
  CURSOR_GATE_EVENTS,
12
12
  HOOK_LOCATOR_BINARY
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  pusharyDir
3
- } from "./chunk-CV2YD2JR.js";
3
+ } from "./chunk-N3TF3RQ2.js";
4
4
  import {
5
5
  writeFileAtomic
6
6
  } from "./chunk-4LL6LG5X.js";
@@ -38,16 +38,18 @@ var uploadTranscriptRecords = async (apiKey, params, baseUrl = getBaseUrl()) =>
38
38
  const body = await res.json().catch(() => null);
39
39
  const keyRegistered = body?.keyRegistered === true;
40
40
  const keyConflict = body?.keyConflict === true;
41
+ const keyUnavailable = res.status === 503;
41
42
  const ids = (value) => Array.isArray(value) ? value.filter((v) => typeof v === "string") : [];
42
43
  return {
43
44
  ok: res.ok && !keyConflict && (params.dataEncryptionKey === void 0 || keyRegistered),
44
45
  keyRegistered,
45
46
  keyConflict,
47
+ keyUnavailable,
46
48
  settledIds: Array.isArray(body?.settledIds) ? ids(body.settledIds) : null,
47
49
  rejectedIds: ids(body?.rejectedIds)
48
50
  };
49
51
  } catch {
50
- return { ok: false, keyRegistered: false, keyConflict: false, settledIds: null, rejectedIds: [] };
52
+ return { ok: false, keyRegistered: false, keyConflict: false, keyUnavailable: false, settledIds: null, rejectedIds: [] };
51
53
  }
52
54
  };
53
55
 
@@ -57,6 +57,7 @@ var parseMcpResponse = (body, contentType) => {
57
57
  };
58
58
  var sendMcpRequest = async (apiKey, message, options = {}) => {
59
59
  return withRetry(async () => {
60
+ options.signal?.throwIfAborted();
60
61
  const baseUrl = options.baseUrl ?? getBaseUrl();
61
62
  const fetchFn = options.fetchFn ?? fetch;
62
63
  const headers = {
@@ -71,7 +72,7 @@ var sendMcpRequest = async (apiKey, message, options = {}) => {
71
72
  method: "POST",
72
73
  headers,
73
74
  body: JSON.stringify(message),
74
- signal: options.timeoutMs ? AbortSignal.timeout(options.timeoutMs) : void 0
75
+ signal: options.timeoutMs ? AbortSignal.any([AbortSignal.timeout(options.timeoutMs), ...options.signal ? [options.signal] : []]) : options.signal
75
76
  });
76
77
  const body = await response.text();
77
78
  const contentType = response.headers.get("content-type");
@@ -13,14 +13,18 @@ import {
13
13
  savePendingCommand,
14
14
  sendNotification,
15
15
  waitForAnswer
16
- } from "./chunk-F7OURW3C.js";
16
+ } from "./chunk-RER3FRTD.js";
17
17
  import {
18
18
  AGENT_IDENTITIES,
19
+ TEAMMATE_IDLE_ACTION,
19
20
  compactionAction,
20
21
  deriveReceiptMeta,
21
22
  deriveToolTarget,
23
+ describeStopFailure,
22
24
  describeToolCall,
23
25
  isToolResultError,
26
+ notificationAction,
27
+ notificationTypeOf,
24
28
  parseApplyPatchFiles,
25
29
  parseApplyPatchOps,
26
30
  sessionEndAction,
@@ -30,13 +34,17 @@ import {
30
34
  summarizeStop,
31
35
  taskTitleFrom,
32
36
  toolResultErrorText
33
- } from "./chunk-L4E2BIJ2.js";
37
+ } from "./chunk-UAFQC35P.js";
38
+ import {
39
+ NOTIFICATION_PUSH_TYPES,
40
+ POSTTOOLUSE_MIRROR_ONLY_TOOLS
41
+ } from "./chunk-QQSQWLFR.js";
34
42
  import {
35
43
  getMachineId
36
- } from "./chunk-2ACC5M7F.js";
44
+ } from "./chunk-72FWXLW2.js";
37
45
  import {
38
46
  withRetry
39
- } from "./chunk-BSZYIAZL.js";
47
+ } from "./chunk-EYRGBYZG.js";
40
48
  import {
41
49
  reportedInstallSource
42
50
  } from "./chunk-JW4KO5AF.js";
@@ -174,7 +182,7 @@ var readReusableMode = (apiKey, sessionId, maxAgeMs) => {
174
182
  if (Date.now() - last.at >= maxAgeMs) return null;
175
183
  return { ...last.state, degraded: false };
176
184
  };
177
- var fetchModeState = async (apiKey, sessionId, reuseWithinMs = 0) => {
185
+ var fetchModeState = async (apiKey, sessionId, reuseWithinMs = 0, signal) => {
178
186
  if (reuseWithinMs > 0) {
179
187
  const reusable = readReusableMode(apiKey, sessionId, reuseWithinMs);
180
188
  if (reusable) return reusable;
@@ -184,7 +192,7 @@ var fetchModeState = async (apiKey, sessionId, reuseWithinMs = 0) => {
184
192
  const url = sessionId ? `${baseUrl}/api/mcp/mode?session=${encodeURIComponent(sessionId)}` : `${baseUrl}/api/mcp/mode`;
185
193
  const response = await fetch(url, {
186
194
  headers: { "Authorization": `Bearer ${apiKey}` },
187
- signal: AbortSignal.timeout(3e3)
195
+ signal: AbortSignal.any([AbortSignal.timeout(3e3), ...signal ? [signal] : []])
188
196
  });
189
197
  if (!response.ok) {
190
198
  return degradedFallback(apiKey, response.status === 401 || response.status === 403);
@@ -569,20 +577,21 @@ var pendingQuestionsToSettle = (sessionId, toolUseId) => {
569
577
  const settledElsewhere = toolUseId ? listPendingQuestionsForToolUse(sessionId, toolUseId) : [];
570
578
  return [.../* @__PURE__ */ new Set([...settledElsewhere, ...listPendingQuestions(sessionId)])];
571
579
  };
572
- var reconcileQuestions = async (sessionId, files) => {
580
+ var reconcileQuestions = async (sessionId, files, signal) => {
573
581
  const late = [];
574
582
  try {
575
583
  if (files.length === 0) return late;
576
584
  const apiKey = getApiKey();
585
+ if (signal) signal = AbortSignal.any([signal, AbortSignal.timeout(2e3)]);
577
586
  for (const correlationId of files) {
587
+ if (signal?.aborted) break;
578
588
  try {
579
- const answer = await waitForAnswer(apiKey, correlationId, 1e3);
589
+ const answer = await waitForAnswer(apiKey, correlationId, 1e3, signal);
580
590
  if (answer.answered) {
581
591
  late.push({ correlationId, value: answer.value });
582
592
  continue;
583
593
  }
584
- await cancelQuestion(apiKey, correlationId).catch(() => {
585
- });
594
+ await cancelQuestion(apiKey, correlationId, signal);
586
595
  } catch {
587
596
  continue;
588
597
  }
@@ -592,7 +601,7 @@ var reconcileQuestions = async (sessionId, files) => {
592
601
  }
593
602
  return late;
594
603
  };
595
- var reconcilePendingQuestions = (sessionId, toolUseId) => reconcileQuestions(sessionId, pendingQuestionsToSettle(sessionId, toolUseId));
604
+ var reconcilePendingQuestions = (sessionId, toolUseId, signal) => reconcileQuestions(sessionId, pendingQuestionsToSettle(sessionId, toolUseId), signal);
596
605
  var describeLateAnswer = (value) => {
597
606
  const v = (value ?? "").trim();
598
607
  if (v === "yes") return "approved it";
@@ -605,7 +614,7 @@ var buildLateAnswerContext = (late) => {
605
614
  const parts = late.map((l) => `you ${describeLateAnswer(l.value)} on your phone`);
606
615
  return `Pushary: a permission prompt you were sent was answered late \u2014 ${parts.join("; ")} \u2014 after the terminal already took over. Treat this as the user's latest intent; do not assume the earlier terminal decision matched it.`;
607
616
  };
608
- var notifyLateAnswers = async (apiKey, late, agentName, sessionId) => {
617
+ var notifyLateAnswers = async (apiKey, late, agentName, sessionId, signal) => {
609
618
  if (late.length === 0) return;
610
619
  const summary = late.map((l) => describeLateAnswer(l.value)).join(", ");
611
620
  await sendNotification(apiKey, {
@@ -613,7 +622,7 @@ var notifyLateAnswers = async (apiKey, late, agentName, sessionId) => {
613
622
  body: `You ${summary} after the agent had already moved on.`,
614
623
  agentName,
615
624
  sessionId
616
- }, 2500).catch(() => {
625
+ }, 2500, signal).catch(() => {
617
626
  });
618
627
  };
619
628
  var CLAUDE_CODE_AGENT = { type: "claude_code", label: "Claude Code" };
@@ -687,8 +696,10 @@ var consumesActivityContext = (agentType) => {
687
696
  return pendingCommandEvents(source).some((event) => event !== "Stop");
688
697
  };
689
698
  var detectInstallMode = () => (process.argv[1] ?? "").includes("_npx") ? "npx" : "cli";
690
- var acknowledgePendingCommand = async (apiKey, baseUrl, commandId, sessionId, machineId, timeoutMs, state = "delivered") => {
699
+ var acknowledgePendingCommand = async (apiKey, baseUrl, commandId, sessionId, machineId, timeoutMs, state = "delivered", signal) => {
691
700
  try {
701
+ signal?.throwIfAborted();
702
+ if (timeoutMs <= 0) return false;
692
703
  const response = await fetch(`${baseUrl}/api/agent/command/ack`, {
693
704
  method: "POST",
694
705
  headers: {
@@ -702,13 +713,14 @@ var acknowledgePendingCommand = async (apiKey, baseUrl, commandId, sessionId, ma
702
713
  state,
703
714
  ...state === "failed" ? { failureCode: "consumer_rejected" } : {}
704
715
  }),
705
- signal: AbortSignal.timeout(timeoutMs)
716
+ signal: AbortSignal.any([AbortSignal.timeout(timeoutMs), ...signal ? [signal] : []])
706
717
  });
707
718
  return response.ok;
708
719
  } catch {
709
720
  return false;
710
721
  }
711
722
  };
723
+ var ACK_RESERVE_MS = 1500;
712
724
  var reportEvent = async (event, options = {}) => {
713
725
  const apiKey = options.apiKey ?? getApiKey();
714
726
  const baseUrl = getBaseUrl();
@@ -716,7 +728,14 @@ var reportEvent = async (event, options = {}) => {
716
728
  const sessionKey = event.sessionId || DEFAULT_SESSION;
717
729
  const receiptNamespace = `${apiKey}:${machineId}:${event.agentType}`;
718
730
  const clientCanDrain = consumesActivityContext(event.agentType) && options.timeoutMs === void 0;
731
+ const deadlineMs = options.budgetMs === void 0 ? void 0 : Date.now() + options.budgetMs;
732
+ const ackReserveMs = deadlineMs === void 0 ? 0 : Math.min(ACK_RESERVE_MS, Math.floor((options.budgetMs ?? 0) * 0.4));
733
+ const postTimeoutMs = (fallback) => deadlineMs === void 0 ? fallback : Math.max(0, Math.min(fallback, deadlineMs - ackReserveMs - Date.now()));
734
+ const ackTimeoutMs = (fallback) => deadlineMs === void 0 ? fallback : Math.max(0, Math.min(fallback, deadlineMs - Date.now()));
719
735
  return withRetry(async () => {
736
+ options.signal?.throwIfAborted();
737
+ const timeoutMs = postTimeoutMs(options.timeoutMs ?? 1e4);
738
+ if (timeoutMs <= 0) return null;
720
739
  const storedCommand = clientCanDrain ? readPendingCommand(sessionKey, receiptNamespace) : null;
721
740
  const res = await fetch(`${baseUrl}/api/agent/event`, {
722
741
  method: "POST",
@@ -744,9 +763,9 @@ var reportEvent = async (event, options = {}) => {
744
763
  ...driverFromEnv(process.env) ? { driver: driverFromEnv(process.env) } : {},
745
764
  canDrainCommand: clientCanDrain && !storedCommand,
746
765
  ackMode: clientCanDrain && !storedCommand,
747
- skipCommandDrain: storedCommand != null
766
+ skipCommandDrain: options.skipDrain === true || storedCommand != null
748
767
  }),
749
- signal: AbortSignal.timeout(options.timeoutMs ?? 1e4)
768
+ signal: AbortSignal.any([AbortSignal.timeout(timeoutMs), ...options.signal ? [options.signal] : []])
750
769
  });
751
770
  if (options.requireOk && !res.ok) throw new Error(`/api/agent/event rejected (${res.status})`);
752
771
  try {
@@ -760,8 +779,9 @@ var reportEvent = async (event, options = {}) => {
760
779
  storedCommand.id,
761
780
  event.sessionId,
762
781
  machineId,
763
- options.timeoutMs ?? 1e4,
764
- failed ? "failed" : "delivered"
782
+ ackTimeoutMs(options.timeoutMs ?? 1e4),
783
+ failed ? "failed" : "delivered",
784
+ options.signal
765
785
  );
766
786
  if (!acknowledged) return withoutPending();
767
787
  if (storedCommand.delivered || failed) {
@@ -785,7 +805,9 @@ var reportEvent = async (event, options = {}) => {
785
805
  result.pendingCommandId,
786
806
  event.sessionId,
787
807
  machineId,
788
- options.timeoutMs ?? 1e4
808
+ ackTimeoutMs(options.timeoutMs ?? 1e4),
809
+ "delivered",
810
+ options.signal
789
811
  );
790
812
  if (!acknowledged || !markPendingCommandDelivered(sessionKey, result.pendingCommandId, receiptNamespace)) {
791
813
  return withoutPending();
@@ -797,13 +819,17 @@ var reportEvent = async (event, options = {}) => {
797
819
  }
798
820
  }, { maxAttempts: options.maxAttempts ?? 2, baseDelayMs: 300 });
799
821
  };
800
- var NON_GATED_MIRROR_TOOLS = /* @__PURE__ */ new Set(["TodoWrite"]);
822
+ var NON_GATED_MIRROR_TOOLS = new Set(POSTTOOLUSE_MIRROR_ONLY_TOOLS);
823
+ var HOOK_BUDGET_MS = 9e3;
824
+ var DRAINING_REPORT_BUDGET_MS = 8e3;
801
825
  var settleQuestionsForToolUse = async (sessionId, toolUseId) => {
802
826
  if (!toolUseId) return;
803
827
  const late = await reconcileQuestions(sessionId, listPendingQuestionsForToolUse(sessionId, toolUseId));
804
828
  for (const answer of late) removePendingQuestion(sessionId, answer.correlationId);
805
829
  };
806
830
  var handlePostToolUse = async (input, agent = CLAUDE_CODE_AGENT) => {
831
+ const deadline = Date.now() + HOOK_BUDGET_MS;
832
+ const signal = AbortSignal.timeout(HOOK_BUDGET_MS);
807
833
  try {
808
834
  const projectName = basename2(input.cwd ?? process.cwd());
809
835
  const action = describeToolCall(input.tool_name, input.tool_input, "event");
@@ -812,8 +838,10 @@ var handlePostToolUse = async (input, agent = CLAUDE_CODE_AGENT) => {
812
838
  const isError = isToolResultError(toolResult);
813
839
  const receiptsEnabled = process.env.PUSHARY_RECEIPTS !== "off";
814
840
  const sessionKey = input.session_id || DEFAULT_SESSION;
815
- const liveMode = await fetchModeState(getApiKey(), input.session_id, MODE_REUSE_MS);
816
- const late = await reconcilePendingQuestions(sessionKey, input.tool_use_id);
841
+ const [liveMode, late] = await Promise.all([
842
+ fetchModeState(getApiKey(), input.session_id, MODE_REUSE_MS, signal),
843
+ reconcilePendingQuestions(sessionKey, input.tool_use_id, signal)
844
+ ]);
817
845
  let additionalContext;
818
846
  if (late.length > 0 && agent.type === CLAUDE_CODE_AGENT.type) {
819
847
  additionalContext = buildLateAnswerContext(late);
@@ -833,7 +861,7 @@ var handlePostToolUse = async (input, agent = CLAUDE_CODE_AGENT) => {
833
861
  ...beacon ? { decisionOrigin: beacon.decisionOrigin, toolName: beacon.toolName, toolTarget: beacon.toolTarget } : {},
834
862
  meta: receiptsEnabled ? deriveReceiptMeta(lookup.tool, lookup.input, toolResult, input.cwd ?? process.cwd()) : void 0,
835
863
  usage: deriveUsage(input.transcript_path, input.session_id)
836
- }, { maxAttempts: 1 });
864
+ }, { maxAttempts: 1, budgetMs: Math.min(DRAINING_REPORT_BUDGET_MS, Math.max(0, deadline - Date.now())), signal });
837
865
  const extraReports = [];
838
866
  if (throttlePass(`hookactive:${sessionKey}`, HOOK_ACTIVE_WINDOW_MS)) {
839
867
  extraReports.push(
@@ -844,7 +872,7 @@ var handlePostToolUse = async (input, agent = CLAUDE_CODE_AGENT) => {
844
872
  agentName: `${agent.label} - ${projectName}`,
845
873
  sessionId: input.session_id
846
874
  },
847
- { maxAttempts: 1, timeoutMs: 800 }
875
+ { maxAttempts: 1, timeoutMs: 800, signal }
848
876
  )
849
877
  );
850
878
  }
@@ -858,13 +886,15 @@ var handlePostToolUse = async (input, agent = CLAUDE_CODE_AGENT) => {
858
886
  }
859
887
  };
860
888
  var handleUserPrompt = async (input, agent = CLAUDE_CODE_AGENT) => {
889
+ const deadline = Date.now() + HOOK_BUDGET_MS;
890
+ const signal = AbortSignal.timeout(HOOK_BUDGET_MS);
861
891
  try {
862
892
  const projectName = basename2(input.cwd ?? process.cwd());
863
893
  const sessionKey = input.session_id || DEFAULT_SESSION;
864
894
  const titlesEnabled = process.env.PUSHARY_TASK_TITLES !== "off";
865
895
  const taskTitle = titlesEnabled ? taskTitleFrom(input.prompt) : void 0;
866
896
  if (input.prompt) saveLastPrompt(sessionKey, input.prompt);
867
- const late = await reconcilePendingQuestions(sessionKey);
897
+ const late = await reconcilePendingQuestions(sessionKey, void 0, signal);
868
898
  let additionalContext;
869
899
  if (late.length > 0 && agent.type === CLAUDE_CODE_AGENT.type) {
870
900
  additionalContext = buildLateAnswerContext(late);
@@ -877,7 +907,7 @@ var handleUserPrompt = async (input, agent = CLAUDE_CODE_AGENT) => {
877
907
  agentName: `${agent.label} - ${projectName}`,
878
908
  sessionId: input.session_id,
879
909
  taskTitle
880
- }, { maxAttempts: 1, timeoutMs: 800 })
910
+ }, { maxAttempts: 1, budgetMs: Math.min(DRAINING_REPORT_BUDGET_MS, Math.max(0, deadline - Date.now())), signal })
881
911
  ]);
882
912
  if (consumesActivityContext(agent.type)) {
883
913
  additionalContext = mergeAdditionalContext(additionalContext, reported);
@@ -888,10 +918,12 @@ var handleUserPrompt = async (input, agent = CLAUDE_CODE_AGENT) => {
888
918
  }
889
919
  };
890
920
  var handleStop = async (input, agent = CLAUDE_CODE_AGENT) => {
921
+ const deadline = Date.now() + HOOK_BUDGET_MS;
922
+ const signal = AbortSignal.timeout(HOOK_BUDGET_MS);
891
923
  try {
892
924
  const projectName = basename2(input.cwd ?? process.cwd());
893
925
  const sessionKey = input.session_id || DEFAULT_SESSION;
894
- const late = await reconcilePendingQuestions(sessionKey);
926
+ const late = await reconcilePendingQuestions(sessionKey, void 0, signal);
895
927
  const tasks = [
896
928
  reportEvent({
897
929
  event: "session_end",
@@ -900,13 +932,13 @@ var handleStop = async (input, agent = CLAUDE_CODE_AGENT) => {
900
932
  action: summarizeStop(input),
901
933
  ...sessionIdentityOf(input),
902
934
  usage: deriveUsage(input.transcript_path, input.session_id)
903
- }, { maxAttempts: 1 })
935
+ }, { maxAttempts: 1, budgetMs: Math.min(DRAINING_REPORT_BUDGET_MS, Math.max(0, deadline - Date.now())), signal })
904
936
  ];
905
937
  if (late.length > 0) {
906
- tasks.push(notifyLateAnswers(getApiKey(), late, `${agent.label} - ${projectName}`, input.session_id));
938
+ tasks.push(notifyLateAnswers(getApiKey(), late, `${agent.label} - ${projectName}`, input.session_id, signal));
907
939
  for (const l of late) removePendingQuestion(sessionKey, l.correlationId);
908
940
  }
909
- if (!isDefaultSession(sessionKey)) removePendingSession(sessionKey);
941
+ if (!isDefaultSession(sessionKey) && listPendingQuestions(sessionKey).length === 0) removePendingSession(sessionKey);
910
942
  const [reported] = await Promise.allSettled(tasks);
911
943
  const pendingCommand = reported.status === "fulfilled" ? reported.value?.pendingCommand : void 0;
912
944
  if (typeof pendingCommand === "string" && pendingCommand.trim().length > 0) {
@@ -931,7 +963,10 @@ var handleSubagentStop = async (input, agent = CLAUDE_CODE_AGENT) => {
931
963
  agentName: `${agent.label} - ${projectName}`,
932
964
  action: subagentStopSummary(input),
933
965
  ...sessionIdentityOf(input)
934
- }, { maxAttempts: 1, timeoutMs: SUBAGENT_STOP_REPORT_TIMEOUT_MS });
966
+ // A subagent's end is a `session_end` like any other as far as the route is
967
+ // concerned, and this caller discards the response, so without this the
968
+ // parent's queued instruction is popped and thrown away.
969
+ }, { maxAttempts: 1, timeoutMs: SUBAGENT_STOP_REPORT_TIMEOUT_MS, skipDrain: true });
935
970
  } catch {
936
971
  }
937
972
  };
@@ -945,7 +980,7 @@ var handleCompaction = async (input, agent = CLAUDE_CODE_AGENT) => {
945
980
  agentType: agent.type,
946
981
  agentName: `${agent.label} - ${projectName}`,
947
982
  action: compactionAction(input.hook_event_name, input.trigger),
948
- sessionId: input.session_id
983
+ ...sessionIdentityOf(input)
949
984
  }, { maxAttempts: 1, timeoutMs: COMPACTION_REPORT_TIMEOUT_MS });
950
985
  } catch {
951
986
  }
@@ -988,14 +1023,39 @@ var handleSessionEnd = async (input, agent = CLAUDE_CODE_AGENT, options = {}) =>
988
1023
  } catch {
989
1024
  }
990
1025
  };
991
- var NOTIFICATION_PUSH_TYPES = /* @__PURE__ */ new Set(["idle_prompt", "agent_needs_input"]);
992
1026
  var NOTIFICATION_THROTTLE_MS = 60 * 1e3;
993
1027
  var handleNotification = async (input, agent = CLAUDE_CODE_AGENT) => {
1028
+ const signal = AbortSignal.timeout(HOOK_BUDGET_MS);
994
1029
  try {
995
1030
  const projectName = basename2(input.cwd ?? process.cwd());
996
- const notifType = input.notification_type ?? input.type ?? "";
1031
+ const notifType = notificationTypeOf(input) ?? "";
997
1032
  const sessionKey = input.session_id || DEFAULT_SESSION;
998
- if (NOTIFICATION_PUSH_TYPES.has(notifType)) {
1033
+ const teammateIdle = input.hook_event_name === "TeammateIdle";
1034
+ const action = teammateIdle ? TEAMMATE_IDLE_ACTION : notificationAction(input);
1035
+ const failed = !teammateIdle && notifType === "error";
1036
+ await reportEvent({
1037
+ event: teammateIdle ? "teammate_idle" : failed ? "error" : "notification",
1038
+ agentType: agent.type,
1039
+ agentName: `${agent.label} - ${projectName}`,
1040
+ action,
1041
+ sessionId: input.session_id,
1042
+ error: failed ? action : void 0
1043
+ // `timeoutMs` is not decoration. It is this file's way of saying "side
1044
+ // report", and it sets `clientCanDrain` false. Without it this call
1045
+ // advertises itself as able to consume a phone-queued instruction, and the
1046
+ // stored-command replay inside `reportEvent` hands one back, acknowledges it
1047
+ // as delivered and marks the local receipt delivered, to a caller that
1048
+ // discards the return value. The notification hook prints nothing, so the
1049
+ // instruction is destroyed rather than delayed. Hoisting this report out of
1050
+ // the else-branch put every notification type on that path, not just the
1051
+ // quiet ones.
1052
+ //
1053
+ // 2.5s rather than the 4s a side report usually gets: on the push branch
1054
+ // this is followed by a ~3s `fetchModeState` and a 4s `sendNotification`,
1055
+ // and all three have to fit inside Claude Code's 10s Notification budget.
1056
+ }, { maxAttempts: 1, timeoutMs: 2500, signal }).catch(() => {
1057
+ });
1058
+ if (!teammateIdle && NOTIFICATION_PUSH_TYPES.has(notifType)) {
999
1059
  if (!throttlePass(`notify:${sessionKey}:${notifType}`, NOTIFICATION_THROTTLE_MS)) return;
1000
1060
  let apiKey;
1001
1061
  try {
@@ -1003,61 +1063,24 @@ var handleNotification = async (input, agent = CLAUDE_CODE_AGENT) => {
1003
1063
  } catch {
1004
1064
  return;
1005
1065
  }
1006
- const mode = await fetchModeState(apiKey, input.session_id, MODE_REUSE_MS).catch(() => null);
1066
+ const mode = await fetchModeState(apiKey, input.session_id, MODE_REUSE_MS, signal).catch(() => null);
1007
1067
  if (mode?.kill || mode?.mode === "terminal_only") return;
1008
1068
  await sendNotification(apiKey, {
1009
1069
  title: `${agent.label} is waiting`,
1010
- body: (input.message ?? "Your agent is waiting for you.").slice(0, 200),
1070
+ body: input.message ? action.slice(0, 200) : "Your agent is waiting for you.",
1011
1071
  agentName: `${agent.label} - ${projectName}`,
1012
1072
  sessionId: input.session_id,
1013
1073
  // Makes this an `update` server-side, so resolveSiteMode gates it on
1014
1074
  // the wildcard policy too. `info` because nothing finished.
1015
1075
  context: { type: "info" }
1016
- }, 4e3).catch(() => {
1076
+ }, 4e3, signal).catch(() => {
1017
1077
  });
1018
- return;
1019
1078
  }
1020
- await reportEvent({
1021
- event: notifType === "error" ? "error" : "notification",
1022
- agentType: agent.type,
1023
- agentName: `${agent.label} - ${projectName}`,
1024
- action: input.message ?? input.title ?? "Notification",
1025
- sessionId: input.session_id,
1026
- error: notifType === "error" ? input.message ?? input.title : void 0
1027
- }).catch(() => {
1028
- });
1029
1079
  } catch {
1030
1080
  }
1031
1081
  };
1032
- var STOPFAILURE_MAX_ERROR_LENGTH = 300;
1033
- var STOPFAILURE_REASONS = {
1034
- rate_limit: "Rate limited by the API",
1035
- overloaded: "The API is overloaded right now",
1036
- authentication_failed: "Authentication failed",
1037
- billing_error: "A billing error stopped the turn"
1038
- };
1039
- var describeStopFailure = (error) => {
1040
- const fromString = (s) => {
1041
- const v = s.trim();
1042
- if (!v) return void 0;
1043
- return STOPFAILURE_REASONS[v] ?? v.slice(0, STOPFAILURE_MAX_ERROR_LENGTH);
1044
- };
1045
- if (typeof error === "string") {
1046
- const described = fromString(error);
1047
- if (described) return described;
1048
- } else if (error && typeof error === "object") {
1049
- const e = error;
1050
- for (const key of ["type", "message", "error", "code"]) {
1051
- const val = e[key];
1052
- if (typeof val === "string") {
1053
- const described = fromString(val);
1054
- if (described) return described;
1055
- }
1056
- }
1057
- }
1058
- return "The turn ended on an API error (rate limit, billing, or auth).";
1059
- };
1060
1082
  var handleStopFailure = async (input, agent = CLAUDE_CODE_AGENT) => {
1083
+ const signal = AbortSignal.timeout(HOOK_BUDGET_MS);
1061
1084
  try {
1062
1085
  let apiKey;
1063
1086
  try {
@@ -1066,16 +1089,29 @@ var handleStopFailure = async (input, agent = CLAUDE_CODE_AGENT) => {
1066
1089
  return;
1067
1090
  }
1068
1091
  const projectName = basename2(input.cwd ?? process.cwd());
1069
- const mode = await fetchModeState(apiKey, input.session_id, MODE_REUSE_MS).catch(() => null);
1070
- if (mode?.kill || mode?.mode === "terminal_only") return;
1071
1092
  const reason = describeStopFailure(input.error);
1093
+ await reportEvent({
1094
+ event: "turn_error",
1095
+ agentType: agent.type,
1096
+ agentName: `${agent.label} - ${projectName}`,
1097
+ action: reason,
1098
+ error: reason,
1099
+ sessionId: input.session_id
1100
+ // Same reason as the Notification report above, and the same budget shape:
1101
+ // this is followed by a ~3s fetchModeState and a 4s sendNotification inside
1102
+ // Claude Code's 10s StopFailure budget. The stopfailure hook prints nothing,
1103
+ // so without this the phone's queued instruction is taken and destroyed.
1104
+ }, { maxAttempts: 1, timeoutMs: 2500, signal }).catch(() => {
1105
+ });
1106
+ const mode = await fetchModeState(apiKey, input.session_id, MODE_REUSE_MS, signal).catch(() => null);
1107
+ if (mode?.kill || mode?.mode === "terminal_only") return;
1072
1108
  await sendNotification(apiKey, {
1073
1109
  title: `${agent.label} stopped on an error`,
1074
1110
  body: reason,
1075
1111
  agentName: `${agent.label} - ${projectName}`,
1076
1112
  sessionId: input.session_id,
1077
1113
  context: { type: "error", errorMessage: reason }
1078
- }, 4e3).catch(() => {
1114
+ }, 4e3, signal).catch(() => {
1079
1115
  });
1080
1116
  } catch {
1081
1117
  }
@@ -1101,6 +1137,7 @@ export {
1101
1137
  permissionTimeoutDecision,
1102
1138
  preToolUseTimeoutDecision,
1103
1139
  readLastPrompt,
1140
+ CLAUDE_CODE_AGENT,
1104
1141
  reportEvent,
1105
1142
  settleQuestionsForToolUse,
1106
1143
  handlePostToolUse,
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  pusharyDir
3
- } from "./chunk-CV2YD2JR.js";
3
+ } from "./chunk-N3TF3RQ2.js";
4
4
  import {
5
5
  readJsonSafe
6
6
  } from "./chunk-4LL6LG5X.js";
@@ -1,7 +1,3 @@
1
- import {
2
- PRETOOLUSE_HANDLED_TOOLS,
3
- PRETOOLUSE_PHONE_ANSWERED_TOOLS
4
- } from "./chunk-2LAHXHBR.js";
5
1
  import {
6
2
  defersToNativeMode
7
3
  } from "./chunk-I463AEG3.js";
@@ -14,14 +10,16 @@ import {
14
10
  isDeferAnswer
15
11
  } from "./chunk-YHG74UFF.js";
16
12
  import {
13
+ CLAUDE_CODE_AGENT,
17
14
  fetchModeState,
18
15
  getPolicy,
19
16
  readLastPrompt,
20
17
  readLastUserPrompt,
21
18
  repoKeyFor,
19
+ reportEvent,
22
20
  settleQuestionsForToolUse,
23
21
  throttlePass
24
- } from "./chunk-YXDYKECP.js";
22
+ } from "./chunk-G5L3WVFV.js";
25
23
  import {
26
24
  DEFAULT_SESSION,
27
25
  askUser,
@@ -29,7 +27,7 @@ import {
29
27
  pollForAnswer,
30
28
  savePendingQuestion,
31
29
  sendNotification
32
- } from "./chunk-F7OURW3C.js";
30
+ } from "./chunk-RER3FRTD.js";
33
31
  import {
34
32
  UNNAMED_ACTION,
35
33
  deriveAction,
@@ -37,11 +35,16 @@ import {
37
35
  deriveBlocker,
38
36
  deriveToolTarget,
39
37
  describeToolCall,
38
+ permissionDeniedAction,
40
39
  scopePathFor
41
- } from "./chunk-L4E2BIJ2.js";
40
+ } from "./chunk-UAFQC35P.js";
41
+ import {
42
+ PRETOOLUSE_HANDLED_TOOLS,
43
+ PRETOOLUSE_PHONE_ANSWERED_TOOLS
44
+ } from "./chunk-QQSQWLFR.js";
42
45
  import {
43
46
  getMachineId
44
- } from "./chunk-2ACC5M7F.js";
47
+ } from "./chunk-72FWXLW2.js";
45
48
  import {
46
49
  getApiKey,
47
50
  getBaseUrl
@@ -582,7 +585,19 @@ var automodeRetryEnabled = () => {
582
585
  const flag = process.env.PUSHARY_AUTOMODE_RETRY;
583
586
  return flag === "1" || flag === "true";
584
587
  };
588
+ var reportPermissionDenied = (input) => reportEvent({
589
+ event: "permission_denied",
590
+ agentType: CLAUDE_CODE_AGENT.type,
591
+ agentName: `${CLAUDE_CODE_AGENT.label} - ${basename(input.cwd ?? process.cwd())}`,
592
+ action: permissionDeniedAction(input.tool_name),
593
+ sessionId: input.session_id
594
+ // Side report, not a command carrier. Without timeoutMs this takes the phone's
595
+ // queued instruction and acknowledges it delivered, and this hook can print
596
+ // only {retry}. 4s rather than the Notification path's 2.5s because
597
+ // PermissionDenied is given a 60s budget, not 10s.
598
+ }, { timeoutMs: 4e3 }).catch(() => void 0);
585
599
  var handlePermissionDenied = async (input) => {
600
+ await reportPermissionDenied(input);
586
601
  if (isPusharyOwnTool(input.tool_name)) return void 0;
587
602
  let apiKey;
588
603
  try {
@@ -7,7 +7,7 @@ import {
7
7
  import {
8
8
  fetchTranscriptRecipients,
9
9
  uploadTranscriptRecords
10
- } from "./chunk-QZCFVWA7.js";
10
+ } from "./chunk-EQRS3SGM.js";
11
11
 
12
12
  // src/live-session.ts
13
13
  var MAX_TRANSCRIPT_QUEUE = 100;