Skip to content

Commit b9e1099

Browse files
authored
fix(gateway): log swallowed background-task finalization errors (#92033)
Background-task finalization swallowed all errors. Log via formatForLog at warn so non-transient failures are observable, staying non-blocking.
1 parent 5d6899c commit b9e1099

2 files changed

Lines changed: 59 additions & 2 deletions

File tree

src/gateway/server-methods/agent.test.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4470,6 +4470,54 @@ describe("gateway agent handler", () => {
44704470
});
44714471
});
44724472

4473+
it("logs a swallowed finalize error without blocking the background run", async () => {
4474+
await withTempDir({ prefix: "openclaw-gateway-agent-finalize-throw-" }, async (root) => {
4475+
process.env.OPENCLAW_STATE_DIR = root;
4476+
resetTaskRegistryForTests();
4477+
primeMainAgentRun();
4478+
4479+
const defaultRuntime = getDetachedTaskLifecycleRuntime();
4480+
const finalizeError = new Error("finalize boom");
4481+
const finalizeTaskRunByRunIdSpy = vi.fn(() => {
4482+
throw finalizeError;
4483+
});
4484+
setDetachedTaskLifecycleRuntime({
4485+
...defaultRuntime,
4486+
finalizeTaskRunByRunId: finalizeTaskRunByRunIdSpy,
4487+
});
4488+
4489+
const context = makeContext();
4490+
const respond = vi.fn();
4491+
4492+
await invokeAgent(
4493+
{
4494+
message: "finalize throw seam task",
4495+
sessionKey: "agent:main:main",
4496+
idempotencyKey: "task-registry-finalize-throw",
4497+
},
4498+
{ context, respond, reqId: "task-registry-finalize-throw" },
4499+
);
4500+
4501+
// Finalize threw, but the run must still complete (second res frame with ok status).
4502+
expect(finalizeTaskRunByRunIdSpy).toHaveBeenCalledTimes(1);
4503+
const completed = respond.mock.calls.some(([ok, payload]) => {
4504+
return ok === true && (payload as { status?: string } | undefined)?.status === "ok";
4505+
});
4506+
expect(completed).toBe(true);
4507+
4508+
// The swallowed finalize error stays observable via a warn log.
4509+
const warnMock = context.logGateway.warn as ReturnType<typeof vi.fn>;
4510+
const loggedFinalizeError = warnMock.mock.calls.some(([message]) => {
4511+
return (
4512+
typeof message === "string" &&
4513+
message.includes("failed to finalize tracked agent task") &&
4514+
message.includes("finalize boom")
4515+
);
4516+
});
4517+
expect(loggedFinalizeError).toBe(true);
4518+
});
4519+
});
4520+
44734521
it("routes voice wake trigger to configured session target", async () => {
44744522
mocks.loadVoiceWakeRoutingConfig.mockResolvedValue({
44754523
version: 1,

src/gateway/server-methods/agent.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -682,6 +682,7 @@ function tryFinalizeTrackedAgentTask(params: {
682682
status: GatewayAgentTaskTerminalStatus;
683683
error?: string;
684684
terminalSummary?: string;
685+
log: Pick<GatewayRequestContext["logGateway"], "warn">;
685686
}): void {
686687
try {
687688
finalizeTaskRunByRunId({
@@ -692,8 +693,10 @@ function tryFinalizeTrackedAgentTask(params: {
692693
...(params.error !== undefined ? { error: params.error } : {}),
693694
...(params.terminalSummary !== undefined ? { terminalSummary: params.terminalSummary } : {}),
694695
});
695-
} catch {
696+
} catch (err) {
696697
// Best-effort only: background task tracking must not block agent runs.
698+
// Still surface the swallowed error so non-transient finalize failures stay observable.
699+
params.log.warn(`failed to finalize tracked agent task ${params.runId}: ${formatForLog(err)}`);
697700
}
698701
}
699702

@@ -908,8 +911,12 @@ function dispatchAgentRunFromGateway(params: {
908911
startedAt: Date.now(),
909912
}),
910913
);
911-
} catch {
914+
} catch (err) {
912915
// Best-effort only: background task tracking must not block agent runs.
916+
// Still surface the swallowed error so non-transient tracking failures stay observable.
917+
params.context.logGateway.warn(
918+
`failed to start tracked agent task ${params.runId}: ${formatForLog(err)}`,
919+
);
913920
}
914921
}
915922
void agentCommandFromIngress(params.ingressOpts, defaultRuntime, params.context.deps)
@@ -921,6 +928,7 @@ function dispatchAgentRunFromGateway(params: {
921928
runId: params.runId,
922929
status: aborted ? "timed_out" : "succeeded",
923930
terminalSummary: aborted ? "aborted" : "completed",
931+
log: params.context.logGateway,
924932
});
925933
}
926934
const payload = {
@@ -958,6 +966,7 @@ function dispatchAgentRunFromGateway(params: {
958966
status: aborted ? "timed_out" : resolveFailedTrackedAgentTaskStatus(err),
959967
error: renderedErr,
960968
terminalSummary: renderedErr,
969+
log: params.context.logGateway,
961970
});
962971
}
963972
const error = errorShape(ErrorCodes.UNAVAILABLE, renderedErr);

0 commit comments

Comments
 (0)