@pushary/agent-hooks 0.92.1 → 0.93.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.
Files changed (70) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/bin/pushary-bell-hook.js +4 -4
  3. package/dist/bin/pushary-bell.js +5 -5
  4. package/dist/bin/pushary-claude.js +41 -25
  5. package/dist/bin/pushary-clean.js +11 -11
  6. package/dist/bin/pushary-codex-bridge.js +10 -10
  7. package/dist/bin/pushary-codex-hook.js +48 -14
  8. package/dist/bin/pushary-codex.js +9 -8
  9. package/dist/bin/pushary-connect.js +7 -7
  10. package/dist/bin/pushary-daemon.js +13 -13
  11. package/dist/bin/pushary-disconnect.js +5 -5
  12. package/dist/bin/pushary-doctor.js +27 -20
  13. package/dist/bin/pushary-elicitation-hook.js +5 -5
  14. package/dist/bin/pushary-gemini-bridge.js +10 -10
  15. package/dist/bin/pushary-gemini-hook.js +10 -9
  16. package/dist/bin/pushary-hook.js +12 -12
  17. package/dist/bin/pushary-login.js +7 -7
  18. package/dist/bin/pushary-logout.js +4 -4
  19. package/dist/bin/pushary-mode.js +3 -3
  20. package/dist/bin/pushary-notification-hook.js +9 -8
  21. package/dist/bin/pushary-opencode-hook.js +10 -9
  22. package/dist/bin/pushary-permission-denied-hook.js +12 -12
  23. package/dist/bin/pushary-permission-hook.js +12 -12
  24. package/dist/bin/pushary-post-hook.js +9 -8
  25. package/dist/bin/pushary-prompt-hook.js +9 -8
  26. package/dist/bin/pushary-session-end-hook.js +9 -8
  27. package/dist/bin/pushary-session-start-hook.js +13 -13
  28. package/dist/bin/pushary-setup.js +21 -21
  29. package/dist/bin/pushary-stats.js +2 -2
  30. package/dist/bin/pushary-status.js +9 -9
  31. package/dist/bin/pushary-stop-hook.js +9 -8
  32. package/dist/bin/pushary-stopfailure-hook.js +9 -8
  33. package/dist/bin/pushary-transcripts.js +4 -4
  34. package/dist/bin/pushary-upgrade.js +12 -12
  35. package/dist/bin/pushary-wait.js +3 -3
  36. package/dist/{chunk-VHWCVD42.js → chunk-2ORRI66J.js} +1 -1
  37. package/dist/{chunk-I3ORVBTP.js → chunk-36LR2LOT.js} +5 -5
  38. package/dist/{chunk-7FE2KABY.js → chunk-46P3VES5.js} +1 -1
  39. package/dist/{chunk-L4E2BIJ2.js → chunk-4PE2SQVM.js} +57 -4
  40. package/dist/{chunk-YXDYKECP.js → chunk-5HGLZGI4.js} +138 -86
  41. package/dist/{chunk-OSALSIUF.js → chunk-6BIMJIRG.js} +1 -1
  42. package/dist/{chunk-UZNKVRLE.js → chunk-6WYL3XIP.js} +1 -1
  43. package/dist/{chunk-CELX75MW.js → chunk-BSEAOCCQ.js} +1 -1
  44. package/dist/{chunk-QZCFVWA7.js → chunk-EQRS3SGM.js} +3 -1
  45. package/dist/{chunk-BSZYIAZL.js → chunk-EYRGBYZG.js} +2 -1
  46. package/dist/{chunk-QCXKQJTG.js → chunk-FYOIW5RV.js} +3 -3
  47. package/dist/{chunk-ZMKKK6DC.js → chunk-HSAVPZ46.js} +3 -3
  48. package/dist/{chunk-2ACC5M7F.js → chunk-NVPYRDCT.js} +1 -1
  49. package/dist/{chunk-CNPAT3HL.js → chunk-OB4RF2TF.js} +2 -2
  50. package/dist/{chunk-JW4KO5AF.js → chunk-ODL52O5V.js} +1 -1
  51. package/dist/{chunk-I463AEG3.js → chunk-OYNPLEXP.js} +1 -1
  52. package/dist/{chunk-2XQSN66Q.js → chunk-QGA2ZB4I.js} +1 -1
  53. package/dist/{chunk-RQPQSJJH.js → chunk-QKNLW7H6.js} +26 -11
  54. package/dist/{chunk-F7OURW3C.js → chunk-RER3FRTD.js} +7 -7
  55. package/dist/{chunk-5NAZ7SFS.js → chunk-RH3TM7TO.js} +1 -1
  56. package/dist/{chunk-PYA4FLKI.js → chunk-ROHFVGXM.js} +2 -2
  57. package/dist/{chunk-I7W3UTDK.js → chunk-RX75MRGC.js} +10 -8
  58. package/dist/{chunk-4TFXV5MW.js → chunk-TDUTNYAQ.js} +1 -1
  59. package/dist/{chunk-BXTJVM7O.js → chunk-UZFVNEEU.js} +6 -6
  60. package/dist/{chunk-Z6RFZKY4.js → chunk-WY4ALDYN.js} +1 -1
  61. package/dist/chunk-Y3UGCQQC.js +53 -0
  62. package/dist/{chunk-CV2YD2JR.js → chunk-Y6XXFXVB.js} +43 -11
  63. package/dist/{chunk-3HS4YGHB.js → chunk-YANV5TGK.js} +3 -3
  64. package/dist/{chunk-VNMKIJZB.js → chunk-Z6CP5ZZD.js} +3 -0
  65. package/dist/{chunk-HDORB7WD.js → chunk-ZO2XG3CX.js} +1 -1
  66. package/dist/{chunk-SQYBP3QQ.js → chunk-ZTJF6ANY.js} +2 -2
  67. package/dist/src/index.d.ts +33 -3
  68. package/dist/src/index.js +13 -13
  69. package/package.json +2 -2
  70. package/dist/chunk-2LAHXHBR.js +0 -23
@@ -5,7 +5,7 @@ import {
5
5
  FILE_TARGET_TOOLS,
6
6
  redactSecrets,
7
7
  redactSecretsDeep
8
- } from "./chunk-VNMKIJZB.js";
8
+ } from "./chunk-Z6CP5ZZD.js";
9
9
 
10
10
  // ../hook-mapping/src/describe.ts
11
11
  import { isAbsolute, relative, resolve } from "path";
@@ -20,8 +20,14 @@ var hookPrefixes = {
20
20
  // Without an entry here the fallback stringifies the whole tool input, and for
21
21
  // ExitPlanMode that input IS the plan: the action line became a truncated dump of
22
22
  // the plan markdown. The plan belongs in the body, not the one-line summary.
23
- ExitPlanMode: () => "finished planning and wants to start coding"
24
- };
23
+ ExitPlanMode: () => "finished planning and wants to start coding",
24
+ // Mirrored on PostToolUse since the matcher was widened. Without entries the
25
+ // fallback prints `Task: done` and `WebSearch: done`, which name the tool and
26
+ // drop the only part a person reads: which subagent, and what it looked for.
27
+ Task: (input) => `subagent: ${describeTaskCall(input)}`,
28
+ WebSearch: (input) => `web search: ${firstString(input.query) ?? "(no query)"}`
29
+ };
30
+ var describeTaskCall = (input) => firstString(input.description) ?? firstString(input.subagent_type) ?? "(unnamed)";
25
31
  var describeTodoProgress = (todos) => {
26
32
  if (!Array.isArray(todos)) return "updated the plan";
27
33
  const items = todos.filter((t) => !!t && typeof t === "object");
@@ -42,7 +48,9 @@ var eventPrefixes = {
42
48
  Edit: (input) => `edited: ${input.file_path ?? "unknown"}`,
43
49
  MultiEdit: (input) => `edited: ${input.file_path ?? "unknown"}`,
44
50
  Read: (input) => `read: ${input.file_path ?? "unknown"}`,
45
- TodoWrite: (input) => describeTodoProgress(input.todos)
51
+ TodoWrite: (input) => describeTodoProgress(input.todos),
52
+ Task: (input) => `ran subagent: ${describeTaskCall(input)}`,
53
+ WebSearch: (input) => `searched: ${firstString(input.query) ?? "the web"}`
46
54
  };
47
55
  var MCP_TOOL_PREFIX = "mcp__";
48
56
  var parseMcpToolName = (toolName) => {
@@ -287,6 +295,7 @@ var AGENT_IDENTITIES = {
287
295
  vscode: { type: "vscode", label: AGENT_LABELS_BY_TYPE.vscode },
288
296
  opencode: { type: "opencode", label: AGENT_LABELS_BY_TYPE.opencode }
289
297
  };
298
+ var TEAMMATE_IDLE_ACTION = "A teammate went idle";
290
299
  var TASK_TITLE_MAX_LENGTH = 120;
291
300
  var STOP_SUMMARY_MAX_LENGTH = 200;
292
301
  var SESSION_END_ACTIONS = {
@@ -333,6 +342,45 @@ var sessionStartAction = (source) => source === "resume" ? "Session resumed" : "
333
342
  var sessionEndAction = (reason) => typeof reason === "string" && SESSION_END_ACTIONS[reason] || "Session ended";
334
343
  var taskTitleFrom = (prompt) => typeof prompt === "string" ? prompt.replace(/\s+/g, " ").trim().slice(0, TASK_TITLE_MAX_LENGTH) || void 0 : void 0;
335
344
  var asString = (value) => typeof value === "string" && value.length > 0 ? value : void 0;
345
+ var NOTIFICATION_ACTION_MAX_LENGTH = 200;
346
+ var STOP_FAILURE_REASON_MAX_LENGTH = 300;
347
+ var notificationTypeOf = (payload) => asString(payload.notification_type) ?? asString(payload.type);
348
+ var notificationAction = (payload) => {
349
+ const text = asString(payload.message) ?? asString(payload.title);
350
+ const collapsed = redactSecrets(text ?? "").replace(/\s+/g, " ").trim();
351
+ return collapsed ? collapsed.slice(0, NOTIFICATION_ACTION_MAX_LENGTH) : "Notification";
352
+ };
353
+ var STOP_FAILURE_REASONS = {
354
+ rate_limit: "Rate limited by the API",
355
+ overloaded: "The API is overloaded right now",
356
+ authentication_failed: "Authentication failed",
357
+ billing_error: "A billing error stopped the turn"
358
+ };
359
+ var STOP_FAILURE_FALLBACK = "The turn ended on an API error (rate limit, billing, or auth).";
360
+ var describeStopFailure = (error) => {
361
+ const fromString = (value) => {
362
+ const trimmed = value.trim();
363
+ if (!trimmed) return void 0;
364
+ return STOP_FAILURE_REASONS[trimmed] ?? redactSecrets(trimmed).slice(0, STOP_FAILURE_REASON_MAX_LENGTH);
365
+ };
366
+ if (typeof error === "string") {
367
+ const described = fromString(error);
368
+ if (described) return described;
369
+ } else if (error && typeof error === "object") {
370
+ for (const key of ["type", "message", "error", "code"]) {
371
+ const value = error[key];
372
+ if (typeof value === "string") {
373
+ const described = fromString(value);
374
+ if (described) return described;
375
+ }
376
+ }
377
+ }
378
+ return STOP_FAILURE_FALLBACK;
379
+ };
380
+ var permissionDeniedAction = (toolName) => {
381
+ const tool = asString(toolName);
382
+ return tool ? `Auto-mode blocked ${tool}` : "Auto-mode blocked a tool call";
383
+ };
336
384
  var sessionIdentityOf = (payload) => {
337
385
  const sessionId = asString(payload.session_id);
338
386
  const agentId = asString(payload.agent_id);
@@ -356,11 +404,16 @@ export {
356
404
  deriveAction,
357
405
  deriveBlocker,
358
406
  AGENT_IDENTITIES,
407
+ TEAMMATE_IDLE_ACTION,
359
408
  summarizeStop,
360
409
  subagentStopSummary,
361
410
  compactionAction,
362
411
  sessionStartAction,
363
412
  sessionEndAction,
364
413
  taskTitleFrom,
414
+ notificationTypeOf,
415
+ notificationAction,
416
+ describeStopFailure,
417
+ permissionDeniedAction,
365
418
  sessionIdentityOf
366
419
  };
@@ -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,16 +34,20 @@ import {
30
34
  summarizeStop,
31
35
  taskTitleFrom,
32
36
  toolResultErrorText
33
- } from "./chunk-L4E2BIJ2.js";
37
+ } from "./chunk-4PE2SQVM.js";
38
+ import {
39
+ NOTIFICATION_PUSH_TYPES,
40
+ POSTTOOLUSE_MIRROR_ONLY_TOOLS
41
+ } from "./chunk-Y3UGCQQC.js";
34
42
  import {
35
43
  getMachineId
36
- } from "./chunk-2ACC5M7F.js";
44
+ } from "./chunk-NVPYRDCT.js";
37
45
  import {
38
46
  withRetry
39
- } from "./chunk-BSZYIAZL.js";
47
+ } from "./chunk-EYRGBYZG.js";
40
48
  import {
41
49
  reportedInstallSource
42
- } from "./chunk-JW4KO5AF.js";
50
+ } from "./chunk-ODL52O5V.js";
43
51
  import {
44
52
  getApiKey,
45
53
  getBaseUrl
@@ -55,7 +63,7 @@ import {
55
63
  pendingCommandEvents,
56
64
  resolveAutoResolveOrigin,
57
65
  resolvePolicy
58
- } from "./chunk-VNMKIJZB.js";
66
+ } from "./chunk-Z6CP5ZZD.js";
59
67
 
60
68
  // src/policy.ts
61
69
  import { createHash } from "crypto";
@@ -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);
@@ -284,6 +292,7 @@ var repoKeyFor = (cwd) => {
284
292
 
285
293
  // src/usage.ts
286
294
  import { closeSync, mkdirSync, openSync, readFileSync as readFileSync3, readSync, statSync as statSync2, writeFileSync as writeFileSync2 } from "fs";
295
+ import { createHash as createHash2 } from "crypto";
287
296
  import { join as join3 } from "path";
288
297
  import { tmpdir as tmpdir2 } from "os";
289
298
  var DEFAULT_PRICES = [
@@ -423,10 +432,12 @@ var readLastUserPrompt = (transcriptPath) => {
423
432
  };
424
433
  var readNewUsage = (transcriptPath, sessionId) => {
425
434
  try {
426
- const size = statSync2(transcriptPath).size;
435
+ const file = statSync2(transcriptPath);
436
+ const size = file.size;
427
437
  const path = stateFile(sessionId);
428
438
  let state = readState(path);
429
439
  if (size < state.offset) state = { ...emptyState(), recentIds: state.recentIds };
440
+ const startOffset = state.offset;
430
441
  let deltaUsd = 0;
431
442
  if (size > state.offset) {
432
443
  const buffer = readRange(transcriptPath, state.offset, size);
@@ -443,6 +454,18 @@ var readNewUsage = (transcriptPath, sessionId) => {
443
454
  writeFileSync2(path, JSON.stringify(state), "utf-8");
444
455
  if (state.tokensIn === 0 && state.tokensOut === 0) return null;
445
456
  return {
457
+ // Identical cursor-range reports share an identity across transport retries.
458
+ // Native message identity distinguishes an in-place rotation of the range.
459
+ // This does not serialize readers of different, potentially overlapping ranges.
460
+ eventId: createHash2("sha256").update(JSON.stringify([
461
+ sessionId,
462
+ file.dev,
463
+ file.ino,
464
+ startOffset,
465
+ state.offset,
466
+ state.recentIds.at(-1)
467
+ ])).digest("hex").slice(0, 32),
468
+ reportedAt: Date.now(),
446
469
  tokensIn: state.tokensIn,
447
470
  tokensOut: state.tokensOut,
448
471
  costUsd: state.costUsd,
@@ -521,10 +544,10 @@ var phoneDrivenEnv = (base) => ({
521
544
  // src/throttle.ts
522
545
  import { join as join4 } from "path";
523
546
  import { tmpdir as tmpdir3 } from "os";
524
- import { createHash as createHash2 } from "crypto";
547
+ import { createHash as createHash3 } from "crypto";
525
548
  import { existsSync as existsSync3, mkdirSync as mkdirSync2, statSync as statSync3, writeFileSync as writeFileSync3 } from "fs";
526
549
  var THROTTLE_DIR = join4(tmpdir3(), "pushary-throttle");
527
- var markerPath = (key) => join4(THROTTLE_DIR, createHash2("sha256").update(key).digest("hex").slice(0, 16));
550
+ var markerPath = (key) => join4(THROTTLE_DIR, createHash3("sha256").update(key).digest("hex").slice(0, 16));
528
551
  var throttlePass = (key, windowMs) => {
529
552
  try {
530
553
  const path = markerPath(key);
@@ -569,20 +592,21 @@ var pendingQuestionsToSettle = (sessionId, toolUseId) => {
569
592
  const settledElsewhere = toolUseId ? listPendingQuestionsForToolUse(sessionId, toolUseId) : [];
570
593
  return [.../* @__PURE__ */ new Set([...settledElsewhere, ...listPendingQuestions(sessionId)])];
571
594
  };
572
- var reconcileQuestions = async (sessionId, files) => {
595
+ var reconcileQuestions = async (sessionId, files, signal) => {
573
596
  const late = [];
574
597
  try {
575
598
  if (files.length === 0) return late;
576
599
  const apiKey = getApiKey();
600
+ if (signal) signal = AbortSignal.any([signal, AbortSignal.timeout(2e3)]);
577
601
  for (const correlationId of files) {
602
+ if (signal?.aborted) break;
578
603
  try {
579
- const answer = await waitForAnswer(apiKey, correlationId, 1e3);
604
+ const answer = await waitForAnswer(apiKey, correlationId, 1e3, signal);
580
605
  if (answer.answered) {
581
606
  late.push({ correlationId, value: answer.value });
582
607
  continue;
583
608
  }
584
- await cancelQuestion(apiKey, correlationId).catch(() => {
585
- });
609
+ await cancelQuestion(apiKey, correlationId, signal);
586
610
  } catch {
587
611
  continue;
588
612
  }
@@ -592,7 +616,7 @@ var reconcileQuestions = async (sessionId, files) => {
592
616
  }
593
617
  return late;
594
618
  };
595
- var reconcilePendingQuestions = (sessionId, toolUseId) => reconcileQuestions(sessionId, pendingQuestionsToSettle(sessionId, toolUseId));
619
+ var reconcilePendingQuestions = (sessionId, toolUseId, signal) => reconcileQuestions(sessionId, pendingQuestionsToSettle(sessionId, toolUseId), signal);
596
620
  var describeLateAnswer = (value) => {
597
621
  const v = (value ?? "").trim();
598
622
  if (v === "yes") return "approved it";
@@ -605,7 +629,7 @@ var buildLateAnswerContext = (late) => {
605
629
  const parts = late.map((l) => `you ${describeLateAnswer(l.value)} on your phone`);
606
630
  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
631
  };
608
- var notifyLateAnswers = async (apiKey, late, agentName, sessionId) => {
632
+ var notifyLateAnswers = async (apiKey, late, agentName, sessionId, signal) => {
609
633
  if (late.length === 0) return;
610
634
  const summary = late.map((l) => describeLateAnswer(l.value)).join(", ");
611
635
  await sendNotification(apiKey, {
@@ -613,7 +637,7 @@ var notifyLateAnswers = async (apiKey, late, agentName, sessionId) => {
613
637
  body: `You ${summary} after the agent had already moved on.`,
614
638
  agentName,
615
639
  sessionId
616
- }, 2500).catch(() => {
640
+ }, 2500, signal).catch(() => {
617
641
  });
618
642
  };
619
643
  var CLAUDE_CODE_AGENT = { type: "claude_code", label: "Claude Code" };
@@ -687,8 +711,10 @@ var consumesActivityContext = (agentType) => {
687
711
  return pendingCommandEvents(source).some((event) => event !== "Stop");
688
712
  };
689
713
  var detectInstallMode = () => (process.argv[1] ?? "").includes("_npx") ? "npx" : "cli";
690
- var acknowledgePendingCommand = async (apiKey, baseUrl, commandId, sessionId, machineId, timeoutMs, state = "delivered") => {
714
+ var acknowledgePendingCommand = async (apiKey, baseUrl, commandId, sessionId, machineId, timeoutMs, state = "delivered", signal) => {
691
715
  try {
716
+ signal?.throwIfAborted();
717
+ if (timeoutMs <= 0) return false;
692
718
  const response = await fetch(`${baseUrl}/api/agent/command/ack`, {
693
719
  method: "POST",
694
720
  headers: {
@@ -702,13 +728,14 @@ var acknowledgePendingCommand = async (apiKey, baseUrl, commandId, sessionId, ma
702
728
  state,
703
729
  ...state === "failed" ? { failureCode: "consumer_rejected" } : {}
704
730
  }),
705
- signal: AbortSignal.timeout(timeoutMs)
731
+ signal: AbortSignal.any([AbortSignal.timeout(timeoutMs), ...signal ? [signal] : []])
706
732
  });
707
733
  return response.ok;
708
734
  } catch {
709
735
  return false;
710
736
  }
711
737
  };
738
+ var ACK_RESERVE_MS = 1500;
712
739
  var reportEvent = async (event, options = {}) => {
713
740
  const apiKey = options.apiKey ?? getApiKey();
714
741
  const baseUrl = getBaseUrl();
@@ -716,7 +743,14 @@ var reportEvent = async (event, options = {}) => {
716
743
  const sessionKey = event.sessionId || DEFAULT_SESSION;
717
744
  const receiptNamespace = `${apiKey}:${machineId}:${event.agentType}`;
718
745
  const clientCanDrain = consumesActivityContext(event.agentType) && options.timeoutMs === void 0;
746
+ const deadlineMs = options.budgetMs === void 0 ? void 0 : Date.now() + options.budgetMs;
747
+ const ackReserveMs = deadlineMs === void 0 ? 0 : Math.min(ACK_RESERVE_MS, Math.floor((options.budgetMs ?? 0) * 0.4));
748
+ const postTimeoutMs = (fallback) => deadlineMs === void 0 ? fallback : Math.max(0, Math.min(fallback, deadlineMs - ackReserveMs - Date.now()));
749
+ const ackTimeoutMs = (fallback) => deadlineMs === void 0 ? fallback : Math.max(0, Math.min(fallback, deadlineMs - Date.now()));
719
750
  return withRetry(async () => {
751
+ options.signal?.throwIfAborted();
752
+ const timeoutMs = postTimeoutMs(options.timeoutMs ?? 1e4);
753
+ if (timeoutMs <= 0) return null;
720
754
  const storedCommand = clientCanDrain ? readPendingCommand(sessionKey, receiptNamespace) : null;
721
755
  const res = await fetch(`${baseUrl}/api/agent/event`, {
722
756
  method: "POST",
@@ -744,11 +778,11 @@ var reportEvent = async (event, options = {}) => {
744
778
  ...driverFromEnv(process.env) ? { driver: driverFromEnv(process.env) } : {},
745
779
  canDrainCommand: clientCanDrain && !storedCommand,
746
780
  ackMode: clientCanDrain && !storedCommand,
747
- skipCommandDrain: storedCommand != null
781
+ skipCommandDrain: options.skipDrain === true || storedCommand != null
748
782
  }),
749
- signal: AbortSignal.timeout(options.timeoutMs ?? 1e4)
783
+ signal: AbortSignal.any([AbortSignal.timeout(timeoutMs), ...options.signal ? [options.signal] : []])
750
784
  });
751
- if (options.requireOk && !res.ok) throw new Error(`/api/agent/event rejected (${res.status})`);
785
+ if ((options.requireOk || event.usage !== void 0) && !res.ok) throw new Error(`/api/agent/event rejected (${res.status})`);
752
786
  try {
753
787
  const result = await res.json();
754
788
  const withoutPending = () => ({ ...result, pendingCommand: void 0, pendingCommandId: void 0 });
@@ -760,8 +794,9 @@ var reportEvent = async (event, options = {}) => {
760
794
  storedCommand.id,
761
795
  event.sessionId,
762
796
  machineId,
763
- options.timeoutMs ?? 1e4,
764
- failed ? "failed" : "delivered"
797
+ ackTimeoutMs(options.timeoutMs ?? 1e4),
798
+ failed ? "failed" : "delivered",
799
+ options.signal
765
800
  );
766
801
  if (!acknowledged) return withoutPending();
767
802
  if (storedCommand.delivered || failed) {
@@ -785,7 +820,9 @@ var reportEvent = async (event, options = {}) => {
785
820
  result.pendingCommandId,
786
821
  event.sessionId,
787
822
  machineId,
788
- options.timeoutMs ?? 1e4
823
+ ackTimeoutMs(options.timeoutMs ?? 1e4),
824
+ "delivered",
825
+ options.signal
789
826
  );
790
827
  if (!acknowledged || !markPendingCommandDelivered(sessionKey, result.pendingCommandId, receiptNamespace)) {
791
828
  return withoutPending();
@@ -797,13 +834,17 @@ var reportEvent = async (event, options = {}) => {
797
834
  }
798
835
  }, { maxAttempts: options.maxAttempts ?? 2, baseDelayMs: 300 });
799
836
  };
800
- var NON_GATED_MIRROR_TOOLS = /* @__PURE__ */ new Set(["TodoWrite"]);
837
+ var NON_GATED_MIRROR_TOOLS = new Set(POSTTOOLUSE_MIRROR_ONLY_TOOLS);
838
+ var HOOK_BUDGET_MS = 9e3;
839
+ var DRAINING_REPORT_BUDGET_MS = 8e3;
801
840
  var settleQuestionsForToolUse = async (sessionId, toolUseId) => {
802
841
  if (!toolUseId) return;
803
842
  const late = await reconcileQuestions(sessionId, listPendingQuestionsForToolUse(sessionId, toolUseId));
804
843
  for (const answer of late) removePendingQuestion(sessionId, answer.correlationId);
805
844
  };
806
845
  var handlePostToolUse = async (input, agent = CLAUDE_CODE_AGENT) => {
846
+ const deadline = Date.now() + HOOK_BUDGET_MS;
847
+ const signal = AbortSignal.timeout(HOOK_BUDGET_MS);
807
848
  try {
808
849
  const projectName = basename2(input.cwd ?? process.cwd());
809
850
  const action = describeToolCall(input.tool_name, input.tool_input, "event");
@@ -812,8 +853,10 @@ var handlePostToolUse = async (input, agent = CLAUDE_CODE_AGENT) => {
812
853
  const isError = isToolResultError(toolResult);
813
854
  const receiptsEnabled = process.env.PUSHARY_RECEIPTS !== "off";
814
855
  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);
856
+ const [liveMode, late] = await Promise.all([
857
+ fetchModeState(getApiKey(), input.session_id, MODE_REUSE_MS, signal),
858
+ reconcilePendingQuestions(sessionKey, input.tool_use_id, signal)
859
+ ]);
817
860
  let additionalContext;
818
861
  if (late.length > 0 && agent.type === CLAUDE_CODE_AGENT.type) {
819
862
  additionalContext = buildLateAnswerContext(late);
@@ -833,7 +876,7 @@ var handlePostToolUse = async (input, agent = CLAUDE_CODE_AGENT) => {
833
876
  ...beacon ? { decisionOrigin: beacon.decisionOrigin, toolName: beacon.toolName, toolTarget: beacon.toolTarget } : {},
834
877
  meta: receiptsEnabled ? deriveReceiptMeta(lookup.tool, lookup.input, toolResult, input.cwd ?? process.cwd()) : void 0,
835
878
  usage: deriveUsage(input.transcript_path, input.session_id)
836
- }, { maxAttempts: 1 });
879
+ }, { maxAttempts: 1, budgetMs: Math.min(DRAINING_REPORT_BUDGET_MS, Math.max(0, deadline - Date.now())), signal });
837
880
  const extraReports = [];
838
881
  if (throttlePass(`hookactive:${sessionKey}`, HOOK_ACTIVE_WINDOW_MS)) {
839
882
  extraReports.push(
@@ -844,7 +887,7 @@ var handlePostToolUse = async (input, agent = CLAUDE_CODE_AGENT) => {
844
887
  agentName: `${agent.label} - ${projectName}`,
845
888
  sessionId: input.session_id
846
889
  },
847
- { maxAttempts: 1, timeoutMs: 800 }
890
+ { maxAttempts: 1, timeoutMs: 800, signal }
848
891
  )
849
892
  );
850
893
  }
@@ -858,13 +901,15 @@ var handlePostToolUse = async (input, agent = CLAUDE_CODE_AGENT) => {
858
901
  }
859
902
  };
860
903
  var handleUserPrompt = async (input, agent = CLAUDE_CODE_AGENT) => {
904
+ const deadline = Date.now() + HOOK_BUDGET_MS;
905
+ const signal = AbortSignal.timeout(HOOK_BUDGET_MS);
861
906
  try {
862
907
  const projectName = basename2(input.cwd ?? process.cwd());
863
908
  const sessionKey = input.session_id || DEFAULT_SESSION;
864
909
  const titlesEnabled = process.env.PUSHARY_TASK_TITLES !== "off";
865
910
  const taskTitle = titlesEnabled ? taskTitleFrom(input.prompt) : void 0;
866
911
  if (input.prompt) saveLastPrompt(sessionKey, input.prompt);
867
- const late = await reconcilePendingQuestions(sessionKey);
912
+ const late = await reconcilePendingQuestions(sessionKey, void 0, signal);
868
913
  let additionalContext;
869
914
  if (late.length > 0 && agent.type === CLAUDE_CODE_AGENT.type) {
870
915
  additionalContext = buildLateAnswerContext(late);
@@ -877,7 +922,7 @@ var handleUserPrompt = async (input, agent = CLAUDE_CODE_AGENT) => {
877
922
  agentName: `${agent.label} - ${projectName}`,
878
923
  sessionId: input.session_id,
879
924
  taskTitle
880
- }, { maxAttempts: 1, timeoutMs: 800 })
925
+ }, { maxAttempts: 1, budgetMs: Math.min(DRAINING_REPORT_BUDGET_MS, Math.max(0, deadline - Date.now())), signal })
881
926
  ]);
882
927
  if (consumesActivityContext(agent.type)) {
883
928
  additionalContext = mergeAdditionalContext(additionalContext, reported);
@@ -888,10 +933,12 @@ var handleUserPrompt = async (input, agent = CLAUDE_CODE_AGENT) => {
888
933
  }
889
934
  };
890
935
  var handleStop = async (input, agent = CLAUDE_CODE_AGENT) => {
936
+ const deadline = Date.now() + HOOK_BUDGET_MS;
937
+ const signal = AbortSignal.timeout(HOOK_BUDGET_MS);
891
938
  try {
892
939
  const projectName = basename2(input.cwd ?? process.cwd());
893
940
  const sessionKey = input.session_id || DEFAULT_SESSION;
894
- const late = await reconcilePendingQuestions(sessionKey);
941
+ const late = await reconcilePendingQuestions(sessionKey, void 0, signal);
895
942
  const tasks = [
896
943
  reportEvent({
897
944
  event: "session_end",
@@ -900,13 +947,13 @@ var handleStop = async (input, agent = CLAUDE_CODE_AGENT) => {
900
947
  action: summarizeStop(input),
901
948
  ...sessionIdentityOf(input),
902
949
  usage: deriveUsage(input.transcript_path, input.session_id)
903
- }, { maxAttempts: 1 })
950
+ }, { maxAttempts: 1, budgetMs: Math.min(DRAINING_REPORT_BUDGET_MS, Math.max(0, deadline - Date.now())), signal })
904
951
  ];
905
952
  if (late.length > 0) {
906
- tasks.push(notifyLateAnswers(getApiKey(), late, `${agent.label} - ${projectName}`, input.session_id));
953
+ tasks.push(notifyLateAnswers(getApiKey(), late, `${agent.label} - ${projectName}`, input.session_id, signal));
907
954
  for (const l of late) removePendingQuestion(sessionKey, l.correlationId);
908
955
  }
909
- if (!isDefaultSession(sessionKey)) removePendingSession(sessionKey);
956
+ if (!isDefaultSession(sessionKey) && listPendingQuestions(sessionKey).length === 0) removePendingSession(sessionKey);
910
957
  const [reported] = await Promise.allSettled(tasks);
911
958
  const pendingCommand = reported.status === "fulfilled" ? reported.value?.pendingCommand : void 0;
912
959
  if (typeof pendingCommand === "string" && pendingCommand.trim().length > 0) {
@@ -931,7 +978,10 @@ var handleSubagentStop = async (input, agent = CLAUDE_CODE_AGENT) => {
931
978
  agentName: `${agent.label} - ${projectName}`,
932
979
  action: subagentStopSummary(input),
933
980
  ...sessionIdentityOf(input)
934
- }, { maxAttempts: 1, timeoutMs: SUBAGENT_STOP_REPORT_TIMEOUT_MS });
981
+ // A subagent's end is a `session_end` like any other as far as the route is
982
+ // concerned, and this caller discards the response, so without this the
983
+ // parent's queued instruction is popped and thrown away.
984
+ }, { maxAttempts: 1, timeoutMs: SUBAGENT_STOP_REPORT_TIMEOUT_MS, skipDrain: true });
935
985
  } catch {
936
986
  }
937
987
  };
@@ -945,7 +995,7 @@ var handleCompaction = async (input, agent = CLAUDE_CODE_AGENT) => {
945
995
  agentType: agent.type,
946
996
  agentName: `${agent.label} - ${projectName}`,
947
997
  action: compactionAction(input.hook_event_name, input.trigger),
948
- sessionId: input.session_id
998
+ ...sessionIdentityOf(input)
949
999
  }, { maxAttempts: 1, timeoutMs: COMPACTION_REPORT_TIMEOUT_MS });
950
1000
  } catch {
951
1001
  }
@@ -988,14 +1038,39 @@ var handleSessionEnd = async (input, agent = CLAUDE_CODE_AGENT, options = {}) =>
988
1038
  } catch {
989
1039
  }
990
1040
  };
991
- var NOTIFICATION_PUSH_TYPES = /* @__PURE__ */ new Set(["idle_prompt", "agent_needs_input"]);
992
1041
  var NOTIFICATION_THROTTLE_MS = 60 * 1e3;
993
1042
  var handleNotification = async (input, agent = CLAUDE_CODE_AGENT) => {
1043
+ const signal = AbortSignal.timeout(HOOK_BUDGET_MS);
994
1044
  try {
995
1045
  const projectName = basename2(input.cwd ?? process.cwd());
996
- const notifType = input.notification_type ?? input.type ?? "";
1046
+ const notifType = notificationTypeOf(input) ?? "";
997
1047
  const sessionKey = input.session_id || DEFAULT_SESSION;
998
- if (NOTIFICATION_PUSH_TYPES.has(notifType)) {
1048
+ const teammateIdle = input.hook_event_name === "TeammateIdle";
1049
+ const action = teammateIdle ? TEAMMATE_IDLE_ACTION : notificationAction(input);
1050
+ const failed = !teammateIdle && notifType === "error";
1051
+ await reportEvent({
1052
+ event: teammateIdle ? "teammate_idle" : failed ? "error" : "notification",
1053
+ agentType: agent.type,
1054
+ agentName: `${agent.label} - ${projectName}`,
1055
+ action,
1056
+ sessionId: input.session_id,
1057
+ error: failed ? action : void 0
1058
+ // `timeoutMs` is not decoration. It is this file's way of saying "side
1059
+ // report", and it sets `clientCanDrain` false. Without it this call
1060
+ // advertises itself as able to consume a phone-queued instruction, and the
1061
+ // stored-command replay inside `reportEvent` hands one back, acknowledges it
1062
+ // as delivered and marks the local receipt delivered, to a caller that
1063
+ // discards the return value. The notification hook prints nothing, so the
1064
+ // instruction is destroyed rather than delayed. Hoisting this report out of
1065
+ // the else-branch put every notification type on that path, not just the
1066
+ // quiet ones.
1067
+ //
1068
+ // 2.5s rather than the 4s a side report usually gets: on the push branch
1069
+ // this is followed by a ~3s `fetchModeState` and a 4s `sendNotification`,
1070
+ // and all three have to fit inside Claude Code's 10s Notification budget.
1071
+ }, { maxAttempts: 1, timeoutMs: 2500, signal }).catch(() => {
1072
+ });
1073
+ if (!teammateIdle && NOTIFICATION_PUSH_TYPES.has(notifType)) {
999
1074
  if (!throttlePass(`notify:${sessionKey}:${notifType}`, NOTIFICATION_THROTTLE_MS)) return;
1000
1075
  let apiKey;
1001
1076
  try {
@@ -1003,61 +1078,24 @@ var handleNotification = async (input, agent = CLAUDE_CODE_AGENT) => {
1003
1078
  } catch {
1004
1079
  return;
1005
1080
  }
1006
- const mode = await fetchModeState(apiKey, input.session_id, MODE_REUSE_MS).catch(() => null);
1081
+ const mode = await fetchModeState(apiKey, input.session_id, MODE_REUSE_MS, signal).catch(() => null);
1007
1082
  if (mode?.kill || mode?.mode === "terminal_only") return;
1008
1083
  await sendNotification(apiKey, {
1009
1084
  title: `${agent.label} is waiting`,
1010
- body: (input.message ?? "Your agent is waiting for you.").slice(0, 200),
1085
+ body: input.message ? action.slice(0, 200) : "Your agent is waiting for you.",
1011
1086
  agentName: `${agent.label} - ${projectName}`,
1012
1087
  sessionId: input.session_id,
1013
1088
  // Makes this an `update` server-side, so resolveSiteMode gates it on
1014
1089
  // the wildcard policy too. `info` because nothing finished.
1015
1090
  context: { type: "info" }
1016
- }, 4e3).catch(() => {
1091
+ }, 4e3, signal).catch(() => {
1017
1092
  });
1018
- return;
1019
1093
  }
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
1094
  } catch {
1030
1095
  }
1031
1096
  };
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
1097
  var handleStopFailure = async (input, agent = CLAUDE_CODE_AGENT) => {
1098
+ const signal = AbortSignal.timeout(HOOK_BUDGET_MS);
1061
1099
  try {
1062
1100
  let apiKey;
1063
1101
  try {
@@ -1066,16 +1104,29 @@ var handleStopFailure = async (input, agent = CLAUDE_CODE_AGENT) => {
1066
1104
  return;
1067
1105
  }
1068
1106
  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
1107
  const reason = describeStopFailure(input.error);
1108
+ await reportEvent({
1109
+ event: "turn_error",
1110
+ agentType: agent.type,
1111
+ agentName: `${agent.label} - ${projectName}`,
1112
+ action: reason,
1113
+ error: reason,
1114
+ sessionId: input.session_id
1115
+ // Same reason as the Notification report above, and the same budget shape:
1116
+ // this is followed by a ~3s fetchModeState and a 4s sendNotification inside
1117
+ // Claude Code's 10s StopFailure budget. The stopfailure hook prints nothing,
1118
+ // so without this the phone's queued instruction is taken and destroyed.
1119
+ }, { maxAttempts: 1, timeoutMs: 2500, signal }).catch(() => {
1120
+ });
1121
+ const mode = await fetchModeState(apiKey, input.session_id, MODE_REUSE_MS, signal).catch(() => null);
1122
+ if (mode?.kill || mode?.mode === "terminal_only") return;
1072
1123
  await sendNotification(apiKey, {
1073
1124
  title: `${agent.label} stopped on an error`,
1074
1125
  body: reason,
1075
1126
  agentName: `${agent.label} - ${projectName}`,
1076
1127
  sessionId: input.session_id,
1077
1128
  context: { type: "error", errorMessage: reason }
1078
- }, 4e3).catch(() => {
1129
+ }, 4e3, signal).catch(() => {
1079
1130
  });
1080
1131
  } catch {
1081
1132
  }
@@ -1101,6 +1152,7 @@ export {
1101
1152
  permissionTimeoutDecision,
1102
1153
  preToolUseTimeoutDecision,
1103
1154
  readLastPrompt,
1155
+ CLAUDE_CODE_AGENT,
1104
1156
  reportEvent,
1105
1157
  settleQuestionsForToolUse,
1106
1158
  handlePostToolUse,
@@ -3,7 +3,7 @@ import {
3
3
  } from "./chunk-BC3VCZ3E.js";
4
4
  import {
5
5
  resolveInstallSourceFor
6
- } from "./chunk-JW4KO5AF.js";
6
+ } from "./chunk-ODL52O5V.js";
7
7
  import {
8
8
  backupFile,
9
9
  readJsonSafe,
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  readTranscriptPreference
3
- } from "./chunk-OSALSIUF.js";
3
+ } from "./chunk-6BIMJIRG.js";
4
4
 
5
5
  // src/transcript-setting.ts
6
6
  var fromEnv = () => {
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  HOOK_BUDGETS,
3
3
  OPENCODE_APPROVAL_EVENT
4
- } from "./chunk-VNMKIJZB.js";
4
+ } from "./chunk-Z6CP5ZZD.js";
5
5
 
6
6
  // src/opencode-plugin.ts
7
7
  var OPENCODE_PLUGIN_MARKER = "pushary-opencode-plugin w1";