@pushary/agent-hooks 0.92.1 → 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.
- package/CHANGELOG.md +10 -0
- package/dist/bin/pushary-bell.js +1 -1
- package/dist/bin/pushary-claude.js +36 -20
- package/dist/bin/pushary-clean.js +9 -9
- package/dist/bin/pushary-codex-bridge.js +5 -5
- package/dist/bin/pushary-codex-hook.js +44 -10
- package/dist/bin/pushary-codex.js +7 -6
- package/dist/bin/pushary-connect.js +2 -2
- package/dist/bin/pushary-daemon.js +10 -10
- package/dist/bin/pushary-disconnect.js +3 -3
- package/dist/bin/pushary-doctor.js +21 -14
- package/dist/bin/pushary-elicitation-hook.js +4 -4
- package/dist/bin/pushary-gemini-bridge.js +5 -5
- package/dist/bin/pushary-gemini-hook.js +7 -6
- package/dist/bin/pushary-hook.js +8 -8
- package/dist/bin/pushary-login.js +3 -3
- package/dist/bin/pushary-logout.js +1 -1
- package/dist/bin/pushary-notification-hook.js +7 -6
- package/dist/bin/pushary-opencode-hook.js +7 -6
- package/dist/bin/pushary-permission-denied-hook.js +8 -8
- package/dist/bin/pushary-permission-hook.js +8 -8
- package/dist/bin/pushary-post-hook.js +7 -6
- package/dist/bin/pushary-prompt-hook.js +7 -6
- package/dist/bin/pushary-session-end-hook.js +7 -6
- package/dist/bin/pushary-session-start-hook.js +11 -11
- package/dist/bin/pushary-setup.js +15 -15
- package/dist/bin/pushary-status.js +5 -5
- package/dist/bin/pushary-stop-hook.js +7 -6
- package/dist/bin/pushary-stopfailure-hook.js +7 -6
- package/dist/bin/pushary-upgrade.js +9 -9
- package/dist/{chunk-ZMKKK6DC.js → chunk-6Y4IKMQT.js} +2 -2
- package/dist/{chunk-2ACC5M7F.js → chunk-72FWXLW2.js} +1 -1
- package/dist/{chunk-QZCFVWA7.js → chunk-EQRS3SGM.js} +3 -1
- package/dist/{chunk-BSZYIAZL.js → chunk-EYRGBYZG.js} +2 -1
- package/dist/{chunk-YXDYKECP.js → chunk-G5L3WVFV.js} +117 -80
- package/dist/{chunk-4TFXV5MW.js → chunk-GKMQMSTB.js} +1 -1
- package/dist/{chunk-RQPQSJJH.js → chunk-HCLSTQ3N.js} +23 -8
- package/dist/{chunk-CNPAT3HL.js → chunk-KHTFHUL3.js} +1 -1
- package/dist/{chunk-CV2YD2JR.js → chunk-N3TF3RQ2.js} +42 -10
- package/dist/{chunk-3HS4YGHB.js → chunk-NXZYC3B3.js} +1 -1
- package/dist/{chunk-QCXKQJTG.js → chunk-NZQTIDXN.js} +2 -2
- package/dist/chunk-QQSQWLFR.js +53 -0
- package/dist/{chunk-F7OURW3C.js → chunk-RER3FRTD.js} +7 -7
- package/dist/{chunk-2XQSN66Q.js → chunk-T2PKGGDM.js} +1 -1
- package/dist/{chunk-I3ORVBTP.js → chunk-TFN7ZCTI.js} +4 -4
- package/dist/{chunk-SQYBP3QQ.js → chunk-TWVZHZM3.js} +2 -2
- package/dist/{chunk-L4E2BIJ2.js → chunk-UAFQC35P.js} +56 -3
- package/dist/{chunk-PYA4FLKI.js → chunk-UPCPR7QW.js} +1 -1
- package/dist/{chunk-HDORB7WD.js → chunk-WEDYCMHU.js} +1 -1
- package/dist/{chunk-I7W3UTDK.js → chunk-YUZYMAL7.js} +9 -7
- package/dist/src/index.d.ts +31 -3
- package/dist/src/index.js +9 -9
- package/package.json +2 -2
- package/dist/chunk-2LAHXHBR.js +0 -23
|
@@ -13,14 +13,18 @@ import {
|
|
|
13
13
|
savePendingCommand,
|
|
14
14
|
sendNotification,
|
|
15
15
|
waitForAnswer
|
|
16
|
-
} from "./chunk-
|
|
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-
|
|
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-
|
|
44
|
+
} from "./chunk-72FWXLW2.js";
|
|
37
45
|
import {
|
|
38
46
|
withRetry
|
|
39
|
-
} from "./chunk-
|
|
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)
|
|
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.
|
|
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 =
|
|
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
|
|
816
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1031
|
+
const notifType = notificationTypeOf(input) ?? "";
|
|
997
1032
|
const sessionKey = input.session_id || DEFAULT_SESSION;
|
|
998
|
-
|
|
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:
|
|
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,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-
|
|
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-
|
|
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-
|
|
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-
|
|
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 {
|
|
@@ -34,11 +34,23 @@ var CODEX_HOOK_EVENTS = [
|
|
|
34
34
|
{ event: "PostToolUse", matcher: "Bash|apply_patch", delivery: "telemetry", timeout: 10 },
|
|
35
35
|
{ event: "UserPromptSubmit", delivery: "telemetry", timeout: 10 },
|
|
36
36
|
{ event: "Stop", delivery: "telemetry", timeout: 10 },
|
|
37
|
-
|
|
37
|
+
// `clear` alongside startup and resume, matching what we already do on Claude
|
|
38
|
+
// Code. Codex names all three in its own SessionStart sources, and without it a
|
|
39
|
+
// session the person started by clearing is one we never see begin.
|
|
40
|
+
{ event: "SessionStart", matcher: "startup|resume|clear", delivery: "telemetry", timeout: 10 },
|
|
38
41
|
// Codex caps SessionEnd at 3s and defaults it to 1s, unlike every other event
|
|
39
42
|
// here. It also runs synchronously whatever `async` says, so the handler must
|
|
40
43
|
// fit the window rather than assume the 10s Claude Code allows.
|
|
41
|
-
{ event: "SessionEnd", delivery: "telemetry", timeout: 3 }
|
|
44
|
+
{ event: "SessionEnd", delivery: "telemetry", timeout: 3 },
|
|
45
|
+
// Observability only, and the four Codex names we were not registering at all.
|
|
46
|
+
// Every one is in codex-cli 0.151.0's own `HookEventName` enum
|
|
47
|
+
// (subagent_start, subagent_stop, pre_compact, post_compact), and we already
|
|
48
|
+
// carry all four on Claude Code, so the island showed subagent and compaction
|
|
49
|
+
// shape for one agent and not the other. None of them decides anything.
|
|
50
|
+
{ event: "SubagentStart", delivery: "telemetry", timeout: 5 },
|
|
51
|
+
{ event: "SubagentStop", delivery: "telemetry", timeout: 5 },
|
|
52
|
+
{ event: "PreCompact", matcher: "manual|auto", delivery: "telemetry", timeout: 5 },
|
|
53
|
+
{ event: "PostCompact", matcher: "manual|auto", delivery: "telemetry", timeout: 5 }
|
|
42
54
|
];
|
|
43
55
|
var CODEX_EVENT_KEY = {
|
|
44
56
|
PermissionRequest: "permission_request",
|
|
@@ -47,7 +59,11 @@ var CODEX_EVENT_KEY = {
|
|
|
47
59
|
UserPromptSubmit: "user_prompt_submit",
|
|
48
60
|
Stop: "stop",
|
|
49
61
|
SessionStart: "session_start",
|
|
50
|
-
SessionEnd: "session_end"
|
|
62
|
+
SessionEnd: "session_end",
|
|
63
|
+
SubagentStart: "subagent_start",
|
|
64
|
+
SubagentStop: "subagent_stop",
|
|
65
|
+
PreCompact: "pre_compact",
|
|
66
|
+
PostCompact: "post_compact"
|
|
51
67
|
};
|
|
52
68
|
var codexEventKeyOf = (event) => event.replace(/([a-z0-9])([A-Z])/g, "$1_$2").toLowerCase();
|
|
53
69
|
var asRecord = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
@@ -209,14 +225,30 @@ var restoreCodexHookTrust = (config, hooksJsonPath, positions, snapshot) => {
|
|
|
209
225
|
}
|
|
210
226
|
return true;
|
|
211
227
|
};
|
|
212
|
-
var
|
|
228
|
+
var codexTrustGaps = (config, hooksConfig, hooksJsonPath, command, positions) => {
|
|
213
229
|
const state = asRecord(asRecord(config.hooks)?.state);
|
|
214
|
-
|
|
230
|
+
const hooks = asRecord(hooksConfig?.hooks);
|
|
231
|
+
const gaps = [];
|
|
232
|
+
for (const definition of CODEX_HOOK_EVENTS) {
|
|
215
233
|
const position = positions[definition.event];
|
|
216
|
-
if (!position)
|
|
217
|
-
const
|
|
218
|
-
|
|
219
|
-
|
|
234
|
+
if (!position) continue;
|
|
235
|
+
const stored = asRecord(state?.[codexHookStateKey(hooksJsonPath, definition.event, position)])?.trusted_hash;
|
|
236
|
+
const eventKey = CODEX_EVENT_KEY[definition.event] ?? definition.event;
|
|
237
|
+
const onDisk = asRecord(
|
|
238
|
+
asRecord(hooks?.[definition.event]) ?? (Array.isArray(hooks?.[definition.event]) ? hooks[definition.event][position[0]] : void 0)
|
|
239
|
+
);
|
|
240
|
+
const diskHash = onDisk ? codexEntryTrustHash(eventKey, onDisk) : void 0;
|
|
241
|
+
if (stored !== void 0 && diskHash !== void 0 && stored === diskHash) {
|
|
242
|
+
if (diskHash !== codexHookTrustHash(definition, command)) {
|
|
243
|
+
gaps.push({ event: definition.event, kind: "stale" });
|
|
244
|
+
}
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
if (stored !== (diskHash ?? codexHookTrustHash(definition, command))) {
|
|
248
|
+
gaps.push({ event: definition.event, kind: "untrusted" });
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
return gaps;
|
|
220
252
|
};
|
|
221
253
|
var removeCodexMcpServer = (config) => {
|
|
222
254
|
const servers = asRecord(config.mcp_servers);
|
|
@@ -318,7 +350,7 @@ export {
|
|
|
318
350
|
codexHookPositions,
|
|
319
351
|
captureCodexHookTrust,
|
|
320
352
|
restoreCodexHookTrust,
|
|
321
|
-
|
|
353
|
+
codexTrustGaps,
|
|
322
354
|
removeCodexHookTrust,
|
|
323
355
|
removeCodexSettings,
|
|
324
356
|
addCodexHookTrust,
|