Skip to content

Commit 9aa0b92

Browse files
committed
fix(gateway): cover delayed and owner-side terminal persistence failure logs
Extends the previous chat-abort fix to also log terminal persistence rejections in the remaining two boundaries ClawSweeper identified: - src/gateway/server-runtime-subscriptions.ts: delayed cleanup ordering when persistence is attached after cleanup() was requested. - src/gateway/server-chat.ts: owner-side terminal lifecycle persistence fallback catch. Both boundaries receive the injected SubsystemLogger. The existing server-chat.agent-events test now asserts log.error is called on persistence failure.
1 parent 2e7fdf1 commit 9aa0b92

4 files changed

Lines changed: 32 additions & 4 deletions

File tree

src/gateway/server-chat.agent-events.test.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ describe("agent event handler", () => {
105105
const toolEventRecipients = createToolEventRecipientRegistry();
106106
const sessionEventSubscribers = createSessionEventSubscriberRegistry();
107107
const sessionMessageSubscribers = createSessionMessageSubscriberRegistry();
108+
const log = { error: vi.fn() } as unknown as import("../logging/subsystem.js").SubsystemLogger;
108109

109110
const handler = createAgentEventHandler({
110111
broadcast,
@@ -124,6 +125,7 @@ describe("agent event handler", () => {
124125
markTrackedRunTerminalPersisted: params?.markTrackedRunTerminalPersisted,
125126
trackTrackedRunTerminalPersistence: params?.trackTrackedRunTerminalPersistence,
126127
resolveActiveLifecycleGenerationForRun: params?.resolveActiveLifecycleGenerationForRun,
128+
log,
127129
});
128130

129131
return {
@@ -139,6 +141,7 @@ describe("agent event handler", () => {
139141
sessionEventSubscribers,
140142
sessionMessageSubscribers,
141143
handler,
144+
log,
142145
};
143146
}
144147

@@ -2502,7 +2505,7 @@ describe("agent event handler", () => {
25022505
});
25032506
persistGatewaySessionLifecycleEventMock.mockRejectedValueOnce(new Error("disk full"));
25042507
const markTrackedRunTerminalPersisted = vi.fn();
2505-
const { broadcastToConnIds, handler, sessionEventSubscribers } = createHarness({
2508+
const { broadcastToConnIds, handler, log, sessionEventSubscribers } = createHarness({
25062509
resolveSessionKeyForRun: () => "session-failed-write",
25072510
lifecycleErrorRetryGraceMs: 0,
25082511
markTrackedRunTerminalPersisted,
@@ -2533,6 +2536,13 @@ describe("agent event handler", () => {
25332536
abortedLastRun: false,
25342537
});
25352538
expect(markTrackedRunTerminalPersisted).not.toHaveBeenCalled();
2539+
expect(log.error).toHaveBeenCalledOnce();
2540+
expect(log.error).toHaveBeenCalledWith(
2541+
expect.stringContaining(
2542+
"Failed to persist terminal lifecycle event for run run-failed-write",
2543+
),
2544+
expect.objectContaining({ runId: "run-failed-write" }),
2545+
);
25362546
});
25372547

25382548
it("does not clear a same-id retry when an old restart terminal arrives", () => {

src/gateway/server-chat.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,9 @@ import { DEFAULT_HEARTBEAT_ACK_MAX_CHARS, stripHeartbeatToken } from "../auto-re
77
import { normalizeVerboseLevel } from "../auto-reply/thinking.js";
88
import { getRuntimeConfig } from "../config/io.js";
99
import { type AgentEventPayload, getAgentRunContext } from "../infra/agent-events.js";
10-
import { detectErrorKind, type ErrorKind } from "../infra/errors.js";
10+
import { detectErrorKind, formatErrorMessage, type ErrorKind } from "../infra/errors.js";
1111
import { resolveHeartbeatVisibility } from "../infra/heartbeat-visibility.js";
12+
import type { SubsystemLogger } from "../logging/subsystem.js";
1213
import { isAcpSessionKey, isSubagentSessionKey } from "../sessions/session-key-utils.js";
1314
import { resolveAssistantEventPhase } from "../shared/chat-message-content.js";
1415
import { setSafeTimeout } from "../utils/timer-delay.js";
@@ -290,6 +291,7 @@ export type AgentEventHandlerOptions = {
290291
persistence: Promise<void>;
291292
}) => void;
292293
resolveActiveLifecycleGenerationForRun?: (runId: string) => string | undefined;
294+
log?: SubsystemLogger;
293295
};
294296

295297
function roundedChatSendTimingMs(value: number): number {
@@ -314,6 +316,7 @@ export function createAgentEventHandler({
314316
markTrackedRunTerminalPersisted,
315317
trackTrackedRunTerminalPersistence,
316318
resolveActiveLifecycleGenerationForRun = () => undefined,
319+
log,
317320
}: AgentEventHandlerOptions) {
318321
type TerminalLifecycleOptions = {
319322
skipChatErrorFinal?: boolean;
@@ -716,7 +719,11 @@ export function createAgentEventHandler({
716719
markPersisted();
717720
broadcastSessionChange();
718721
})
719-
.catch(() => {
722+
.catch((err) => {
723+
log?.error(
724+
`Failed to persist terminal lifecycle event for run ${evt.runId}: ${formatErrorMessage(err)}`,
725+
{ error: err, runId: evt.runId },
726+
);
720727
// Persistence recovery remains tracked by the controller entry, but
721728
// subscribers still need a terminal projection instead of hanging.
722729
broadcastSessionChange(evt);

src/gateway/server-runtime-subscriptions.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
// Gateway event subscription wiring for agent, heartbeat, transcript, and lifecycle broadcasts.
22
import { clearAgentRunContext, onAgentEvent } from "../infra/agent-events.js";
3+
import { formatErrorMessage } from "../infra/errors.js";
34
import { onHeartbeatEvent } from "../infra/heartbeat-events.js";
5+
import type { SubsystemLogger } from "../logging/subsystem.js";
46
import { onSessionLifecycleEvent } from "../sessions/session-lifecycle-events.js";
57
import { onInternalSessionTranscriptUpdate } from "../sessions/transcript-events.js";
68
import type { ChatAbortControllerEntry, RestartRecoveryCandidate } from "./chat-abort.js";
@@ -28,6 +30,7 @@ export function startGatewayEventSubscriptions(params: {
2830
sessionMessageSubscribers: SessionMessageSubscriberRegistry;
2931
chatAbortControllers: Map<string, ChatAbortControllerEntry>;
3032
restartRecoveryCandidates: Map<string, RestartRecoveryCandidate>;
33+
log?: SubsystemLogger;
3134
}) {
3235
let agentEventHandlerPromise: Promise<
3336
ReturnType<typeof import("./server-chat.js").createAgentEventHandler>
@@ -49,6 +52,7 @@ export function startGatewayEventSubscriptions(params: {
4952
toolEventRecipients: params.toolEventRecipients,
5053
sessionEventSubscribers: params.sessionEventSubscribers,
5154
sessionMessageSubscribers: params.sessionMessageSubscribers,
55+
log: params.log,
5256
clearTrackedActiveRun: ({ runId, clientRunId }) => {
5357
const candidateRunIds = runId === clientRunId ? [runId] : [runId, clientRunId];
5458
for (const candidateRunId of candidateRunIds) {
@@ -99,7 +103,13 @@ export function startGatewayEventSubscriptions(params: {
99103
entry.projectSessionTerminalPersistence = persistence;
100104
if (entry.registrationCleanupRequested === true) {
101105
void persistence
102-
.catch(() => undefined)
106+
.catch((err) => {
107+
params.log?.error(
108+
`Failed to persist tracked terminal state for run ${candidateRunId}: ${formatErrorMessage(err)}`,
109+
{ error: err, runId: candidateRunId },
110+
);
111+
return undefined;
112+
})
103113
.then(() => {
104114
if (params.chatAbortControllers.get(candidateRunId) === entry) {
105115
params.chatAbortControllers.delete(candidateRunId);

src/gateway/server.impl.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1195,6 +1195,7 @@ export async function startGatewayServer(
11951195
sessionMessageSubscribers,
11961196
chatAbortControllers,
11971197
restartRecoveryCandidates,
1198+
log,
11981199
}),
11991200
);
12001201
Object.assign(runtimeState, runtimeSubscriptions);

0 commit comments

Comments
 (0)