heartbeat: fix backgrounded exec notification path
- resolveHeartbeatReasonKind: add `exec:` prefix match so `exec:<id>:exit` reasons emitted by maybeNotifyOnExit are classified as "exec-event" instead of "other", enabling shouldInspectPendingEvents = true - isExecCompletionEvent: extend to also match "exec completed", "exec failed", and "exec killed" (maybeNotifyOnExit event text), not just "exec finished" (emitExecSystemEvent / gateway path) Without both fixes, backgrounded allowlisted commands triggered a heartbeat that routed to the correct session but fell through to a regular heartbeat prompt instead of the exec-event prompt.
This commit is contained in:
parent
f7d8b28d21
commit
c21582983d
@ -91,3 +91,30 @@ describe("heartbeat event classification", () => {
|
||||
expect(isCronSystemEvent(value)).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe("isExecCompletionEvent", () => {
|
||||
it("matches emitExecSystemEvent (gateway/node approval path) events", () => {
|
||||
expect(isExecCompletionEvent("Exec finished (gateway id=g1, session=s1, code 0)")).toBe(true);
|
||||
expect(isExecCompletionEvent("exec finished (node=n1, code 1)\nsome output")).toBe(true);
|
||||
});
|
||||
|
||||
it("matches maybeNotifyOnExit (backgrounded allowlisted commands) events", () => {
|
||||
expect(isExecCompletionEvent("Exec completed (abc12345, code 0) :: some output")).toBe(true);
|
||||
expect(isExecCompletionEvent("Exec completed (abc12345, code 0)")).toBe(true);
|
||||
expect(isExecCompletionEvent("Exec failed (abc12345, code 1) :: error text")).toBe(true);
|
||||
expect(isExecCompletionEvent("Exec failed (abc12345, signal SIGTERM)")).toBe(true);
|
||||
expect(isExecCompletionEvent("Exec killed (abc12345, signal SIGKILL)")).toBe(true);
|
||||
});
|
||||
|
||||
it("is case-insensitive", () => {
|
||||
expect(isExecCompletionEvent("EXEC COMPLETED (abc12345, code 0)")).toBe(true);
|
||||
expect(isExecCompletionEvent("exec failed (abc12345, code 2)")).toBe(true);
|
||||
});
|
||||
|
||||
it("does not match non-exec events", () => {
|
||||
expect(isExecCompletionEvent("Exec running (gateway id=g1, session=s1, >5s): ls")).toBe(false);
|
||||
expect(isExecCompletionEvent("Exec denied (gateway id=g1, reason): rm -rf /")).toBe(false);
|
||||
expect(isExecCompletionEvent("Heartbeat wake")).toBe(false);
|
||||
expect(isExecCompletionEvent("")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@ -84,7 +84,15 @@ function isHeartbeatNoiseEvent(evt: string): boolean {
|
||||
}
|
||||
|
||||
export function isExecCompletionEvent(evt: string): boolean {
|
||||
return evt.toLowerCase().includes("exec finished");
|
||||
const lower = evt.toLowerCase();
|
||||
// "exec finished" — emitExecSystemEvent (gateway/node approval path)
|
||||
// "exec completed/failed/killed" — maybeNotifyOnExit (backgrounded allowlisted commands)
|
||||
return (
|
||||
lower.includes("exec finished") ||
|
||||
lower.includes("exec completed") ||
|
||||
lower.includes("exec failed") ||
|
||||
lower.includes("exec killed")
|
||||
);
|
||||
}
|
||||
|
||||
// Returns true when a system event should be treated as real cron reminder content.
|
||||
|
||||
@ -20,6 +20,8 @@ describe("heartbeat-reason", () => {
|
||||
{ value: "interval", expected: "interval" },
|
||||
{ value: "manual", expected: "manual" },
|
||||
{ value: "exec-event", expected: "exec-event" },
|
||||
// maybeNotifyOnExit backgrounded exec path: exec:<sessionId>:exit
|
||||
{ value: "exec:abc12345abc12345:exit", expected: "exec-event" },
|
||||
{ value: "wake", expected: "wake" },
|
||||
{ value: "acp:spawn:stream", expected: "wake" },
|
||||
{ value: "acp:spawn:", expected: "wake" },
|
||||
@ -36,6 +38,7 @@ describe("heartbeat-reason", () => {
|
||||
|
||||
it.each([
|
||||
{ value: "exec-event", expected: true },
|
||||
{ value: "exec:abc123:exit", expected: true },
|
||||
{ value: "cron:job-1", expected: true },
|
||||
{ value: "wake", expected: true },
|
||||
{ value: "acp:spawn:stream", expected: true },
|
||||
@ -50,6 +53,7 @@ describe("heartbeat-reason", () => {
|
||||
it.each([
|
||||
{ value: "manual", expected: true },
|
||||
{ value: "exec-event", expected: true },
|
||||
{ value: "exec:abc123:exit", expected: true },
|
||||
{ value: "hook:wake", expected: true },
|
||||
{ value: "interval", expected: false },
|
||||
{ value: "cron:job-1", expected: false },
|
||||
|
||||
@ -31,6 +31,10 @@ export function resolveHeartbeatReasonKind(reason?: string): HeartbeatReasonKind
|
||||
if (trimmed === "exec-event") {
|
||||
return "exec-event";
|
||||
}
|
||||
// exec:<sessionId>:exit — emitted by maybeNotifyOnExit for backgrounded commands
|
||||
if (trimmed.startsWith("exec:")) {
|
||||
return "exec-event";
|
||||
}
|
||||
if (trimmed === "wake") {
|
||||
return "wake";
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user