From b4c48b724daf04196823ec62acd1174ef610e85d Mon Sep 17 00:00:00 2001 From: AgentRun Codex Date: Thu, 11 Jun 2026 09:42:36 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E5=9B=9E=E5=A1=AB=20result=20envelope?= =?UTF-8?q?=20failureKind?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/mgr/result.ts | 38 ++++++++++++++++++++++--- src/selftest/cases/10-manager-memory.ts | 35 +++++++++++++++++++++++ src/selftest/cases/30-codex-stdio.ts | 8 ++++++ 3 files changed, 77 insertions(+), 4 deletions(-) diff --git a/src/mgr/result.ts b/src/mgr/result.ts index 1555e7c..d7e2fa0 100644 --- a/src/mgr/result.ts +++ b/src/mgr/result.ts @@ -48,7 +48,7 @@ export async function buildRunResult(store: AgentRunStore, runId: string, comman const commandTerminal = command ? terminalFromCommand(command) : null; const terminalEventStatus = terminalFromEvents(scopedEvents); const preliminaryTerminal = commandTerminal ?? terminalEventStatus ?? run.terminalStatus; - const failureKind = resultFailureKind(run, command, scopedEvents, preliminaryTerminal); + const failureKind = resultFailureKind(run, command, scopedEvents, jobs, preliminaryTerminal); const terminal = resultTerminal(commandTerminal, terminalEventStatus, run.terminalStatus, failureKind); const terminalSource = resultTerminalSource(commandTerminal, terminalEventStatus, run.terminalStatus, failureKind); const failureMessage = resultFailureMessage(run, command, scopedEvents, terminal); @@ -540,10 +540,35 @@ function failureKindFromEvents(events: RunEvent[]): FailureKind | null { return null; } -function resultFailureKind(run: RunRecord, command: CommandRecord | null, events: RunEvent[], terminal: TerminalStatus | null): FailureKind | null { +function resultFailureKind(run: RunRecord, command: CommandRecord | null, events: RunEvent[], jobs: RunnerJobRecord[], terminal: TerminalStatus | null): FailureKind | null { if (terminal === "completed") return null; - if (command) return failureKindFromEvents(events); - return run.failureKind ?? failureKindFromEvents(events); + return failureKindValue(run.failureKind) ?? failureKindFromEvents(events) ?? failureKindFromRunnerJobs(jobs); +} + +function failureKindFromRunnerJobs(jobs: RunnerJobRecord[]): FailureKind | null { + for (const job of [...jobs].reverse()) { + const value = failureKindFromJson(job.result) ?? failureKindFromNestedRecords(job.result, ["result", "run", "command", "summary", "status", "terminal", "runner", "kubernetes"]); + if (value) return value; + } + return null; +} + +function failureKindFromNestedRecords(record: JsonRecord, keys: string[]): FailureKind | null { + for (const key of keys) { + const nested = jsonRecordValue(record[key]); + if (!nested) continue; + const value = failureKindFromJson(nested) ?? failureKindFromNestedRecords(nested, keys); + if (value) return value; + } + return null; +} + +function failureKindFromJson(record: JsonRecord): FailureKind | null { + return failureKindValue(record.failureKind); +} + +function failureKindValue(value: unknown): FailureKind | null { + return isFailureKind(value) ? value : null; } function isFailureKind(value: unknown): value is FailureKind { @@ -557,10 +582,15 @@ function isFailureKind(value: unknown): value is FailureKind { "skill-unavailable", "runner-lease-conflict", "backend-failed", + "backend-spawn-failed", "backend-timeout", "backend-response-invalid", + "backend-json-parse-error", + "backend-protocol-error", "thread-resume-failed", + "session-store-evicted", "provider-auth-failed", + "provider-http-error", "provider-invalid-tool-call", "provider-compact-unsupported", "provider-rate-limited", diff --git a/src/selftest/cases/10-manager-memory.ts b/src/selftest/cases/10-manager-memory.ts index e7c2b9c..4a022d7 100644 --- a/src/selftest/cases/10-manager-memory.ts +++ b/src/selftest/cases/10-manager-memory.ts @@ -58,6 +58,41 @@ async function assertLongResultUsesTerminalAssistant(client: ManagerClient, stor const finalResponse = result.finalResponse as JsonRecord; assert.equal(finalResponse.text, "final terminal assistant"); assert.equal(finalResponse.seq, finalAssistant.seq); + + const failureRun = store.createRun({ + tenantId: "unidesk", + projectId: "pikasTech/agentrun", + workspaceRef: { kind: "host-path", path: "/tmp/agentrun-selftest" }, + providerId: "G14", + backendProfile: "codex", + executionPolicy: { + sandbox: "workspace-write", + approval: "never", + timeoutMs: 1000, + network: "none", + secretScope: { allowCredentialEcho: false, providerCredentials: [] }, + }, + traceSink: null, + }); + const failureCommand = store.createCommand(failureRun.id, { type: "turn", payload: { prompt: "exercise runner job result failureKind" }, idempotencyKey: "manager-result-runner-job-failure" }); + store.saveRunnerJob({ + runId: failureRun.id, + commandId: failureCommand.id, + payloadHash: "runner-job-failure-hash", + attemptId: "attempt_runner_job_failure", + runnerId: "runner_runner_job_failure", + namespace: "agentrun-v01", + jobName: "agentrun-v01-runner-job-failure", + managerUrl: "http://127.0.0.1", + image: "runner-image", + sourceCommit: "self-test", + serviceAccountName: null, + result: { terminalStatus: "failed", failureKind: "provider-http-error", runner: { logPath: "/tmp/runner.log" } }, + }); + const runnerJobFailureResult = await client.get(`/api/v1/runs/${encodeURIComponent(failureRun.id)}/result?commandId=${encodeURIComponent(failureCommand.id)}`) as JsonRecord; + assert.equal(runnerJobFailureResult.failureKind, "provider-http-error"); + const runnerJobFailureCommandResult = await client.get(`/api/v1/runs/${encodeURIComponent(failureRun.id)}/commands/${encodeURIComponent(failureCommand.id)}/result`) as JsonRecord; + assert.equal(runnerJobFailureCommandResult.failureKind, "provider-http-error"); } export default selfTest; diff --git a/src/selftest/cases/30-codex-stdio.ts b/src/selftest/cases/30-codex-stdio.ts index 13f955c..b968f27 100644 --- a/src/selftest/cases/30-codex-stdio.ts +++ b/src/selftest/cases/30-codex-stdio.ts @@ -394,6 +394,10 @@ async function runFailureDoesNotTerminalRunCase(options: { client: ManagerClient assert.equal(["claimed", "running"].includes(String(run.status)), true, "command failure must keep the reusable run/session non-terminal"); assert.equal(run.terminalStatus, null); assert.equal(run.failureKind, null); + const runEnvelope = await options.client.get(`/api/v1/runs/${item.runId}/result?commandId=${item.commandId}`) as JsonRecord; + assert.equal(runEnvelope.failureKind, "provider-http-error", "run result must inherit terminal command failureKind even when reusable run stays non-terminal"); + const commandEnvelope = await options.client.get(`/api/v1/runs/${item.runId}/commands/${item.commandId}/result`) as JsonRecord; + assert.equal(commandEnvelope.failureKind, "provider-http-error", "command result must expose provider HTTP failureKind from terminal command/events"); } async function runFailureCase(options: { client: ManagerClient; managerUrl: string; context: SelfTestContext; mode: string; expectedStatus: TerminalStatus; expectedFailureKind: FailureKind; timeoutMs?: number; expectRetryError?: boolean }): Promise { @@ -509,6 +513,10 @@ async function runSpawnFailureCase(options: { client: ManagerClient; managerUrl: assert.ok(events.items?.some((event) => event.type === "error"), "spawn failure"); const command = await options.client.get(`/api/v1/runs/${item.runId}/commands/${item.commandId}`) as { state?: string }; assert.equal(command.state, "failed", "spawn failure"); + const commandEnvelope = await options.client.get(`/api/v1/runs/${item.runId}/commands/${item.commandId}/result`) as JsonRecord; + assert.equal(commandEnvelope.failureKind, "backend-spawn-failed", "spawn failure command result kind"); + const runEnvelope = await options.client.get(`/api/v1/runs/${item.runId}/result?commandId=${item.commandId}`) as JsonRecord; + assert.equal(runEnvelope.failureKind, "backend-spawn-failed", "spawn failure run result kind"); assertNoSecretLeak(events); }