From 41555af9ae082ee105633b98bffa73074d98184c Mon Sep 17 00:00:00 2001 From: Lyon <88232613+pikasTech@users.noreply.github.com> Date: Thu, 25 Jun 2026 08:06:33 +0800 Subject: [PATCH] fix: preserve sealed AgentRun cancel result (#2095) --- internal/cloud/code-agent-agentrun-adapter.ts | 16 +++++++ .../cloud/code-agent-agentrun-cancel.test.ts | 43 ++++++++++++++++++- 2 files changed, 58 insertions(+), 1 deletion(-) diff --git a/internal/cloud/code-agent-agentrun-adapter.ts b/internal/cloud/code-agent-agentrun-adapter.ts index aeacb6dc..04ee5b67 100644 --- a/internal/cloud/code-agent-agentrun-adapter.ts +++ b/internal/cloud/code-agent-agentrun-adapter.ts @@ -706,7 +706,15 @@ function isAgentRunCommandAlreadyClaimed(error) { export async function syncAgentRunChatResult({ traceId, currentResult = null, options = {}, traceStore = defaultCodeAgentTraceStore, appendResultEvent = true, refreshEvents = true, forceResultSync = false }) { const initial = currentResult ?? await loadPersistedAgentRunResult(traceId, options); + if (isAgentRunUserCancelSealed(initial)) { + const runnerTrace = initial.runnerTrace ?? traceStore.snapshot(traceId); + return { result: initial, runnerTrace, found: true, eventsRefreshed: false, resultSynced: false, terminalRefreshSkipped: true, localCancelSealed: true }; + } const mapped = initial ? await resolveAgentRunTraceCommandMapping({ traceId, mapped: initial, options }) : initial; + if (isAgentRunUserCancelSealed(mapped)) { + const runnerTrace = mapped.runnerTrace ?? traceStore.snapshot(traceId); + return { result: mapped, runnerTrace, found: true, eventsRefreshed: false, resultSynced: false, terminalRefreshSkipped: true, localCancelSealed: true }; + } const performanceStore = options.backendPerformanceStore ?? currentBackendPerformanceContext(); if (!forceResultSync && options.skipAgentRunTerminalRefreshIfComplete === true && agentRunTerminalResultRefreshSatisfied(mapped, traceId)) { const runnerTrace = mapped.runnerTrace ?? traceStore.snapshot(traceId); @@ -882,6 +890,14 @@ function normalizeAgentRunStatus(value) { return status === "cancelled" ? "canceled" : status; } +function isAgentRunUserCancelSealed(payload = null) { + if (!payload || typeof payload !== "object") return false; + const status = normalizeAgentRunStatus(payload.status); + const cancelStatus = normalizeAgentRunStatus(payload.cancelStatus ?? payload.cancelDisposition?.status); + const terminalStatus = normalizeAgentRunStatus(payload.agentRun?.terminalStatus ?? payload.agentRun?.commandState ?? payload.agentRun?.status); + return payload.canceled === true || status === "canceled" || cancelStatus === "canceled" || terminalStatus === "canceled"; +} + async function resolveAgentRunTraceCommandMapping({ traceId, mapped, options = {} }) { const safeId = safeTraceId(traceId); const agentRun = mapped?.agentRun && typeof mapped.agentRun === "object" ? mapped.agentRun : null; diff --git a/internal/cloud/code-agent-agentrun-cancel.test.ts b/internal/cloud/code-agent-agentrun-cancel.test.ts index 6a55cb14..9796756c 100644 --- a/internal/cloud/code-agent-agentrun-cancel.test.ts +++ b/internal/cloud/code-agent-agentrun-cancel.test.ts @@ -4,7 +4,7 @@ import assert from "node:assert/strict"; import { test } from "bun:test"; -import { cancelAgentRunChatTurn } from "./code-agent-agentrun-adapter.ts"; +import { cancelAgentRunChatTurn, syncAgentRunChatResult } from "./code-agent-agentrun-adapter.ts"; import { createCodeAgentTraceStore } from "./code-agent-trace-store.ts"; test("AgentRun cancel downstream failure returns visible blocked disposition instead of throwing", async () => { @@ -54,3 +54,44 @@ test("AgentRun cancel downstream failure returns visible blocked disposition ins assert.equal(codeAgentChatResults.get(traceId)?.cancelDisposition?.code, "command_not_cancelable"); assert.ok(traceStore.snapshot(traceId).events.some((event) => event.label === "agentrun:cancel:blocked" && event.status === "blocked")); }); + +test("AgentRun result sync preserves a user-canceled payload instead of accepting late completed result", async () => { + const traceId = "trc_cancel_sealed_result"; + const traceStore = createCodeAgentTraceStore(); + const currentResult = { + traceId, + status: "canceled", + canceled: true, + cancelStatus: "canceled", + cancelDisposition: { status: "canceled", forwarded: true, terminal: true, traceId, valuesPrinted: false }, + finalResponse: { text: "hwlab-user-cancel", status: "canceled", traceId, valuesPrinted: false }, + agentRun: { + runId: "run_cancel_sealed_result", + commandId: "cmd_cancel_sealed_result", + managerUrl: "http://127.0.0.1:65535", + status: "cancelled", + commandState: "cancelled", + terminalStatus: "cancelled", + traceId, + valuesPrinted: false + }, + valuesPrinted: false + }; + + const synced = await syncAgentRunChatResult({ + traceId, + currentResult, + traceStore, + options: { + env: { HWLAB_CODE_AGENT_AGENTRUN_ALLOW_NON_K3S_URL: "1" }, + fetchImpl() { + throw new Error("late AgentRun result must not be fetched after HWLAB user cancel is sealed"); + } + } + }); + + assert.equal(synced.localCancelSealed, true); + assert.equal(synced.resultSynced, false); + assert.equal(synced.result.status, "canceled"); + assert.equal(synced.result.finalResponse.text, "hwlab-user-cancel"); +});