mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-21 10:16:44 +00:00
fix(codex): do not promote missing_tool_result on abort/interrupt (#104955)
* fix(codex): do not promote missing_tool_result on abort/interrupt Keep synthetic tool.result rows for transcript integrity, but skip promoting the missing-tool invariant to promptError when the turn is already aborted or interrupted so mid-exec cancel paths keep abort classification and assistant text (#104898). * fix(codex): preserve explicit abort outcome --------- Co-authored-by: Altay <altay@hey.com>
This commit is contained in:
@@ -274,6 +274,24 @@ function turnWithStatus(status: string, items: unknown[] = []): ProjectorNotific
|
||||
} as ProjectorNotification;
|
||||
}
|
||||
|
||||
function pendingCommandStarted(id: string): ProjectorNotification {
|
||||
return forCurrentTurn("item/started", {
|
||||
item: {
|
||||
type: "commandExecution",
|
||||
id,
|
||||
command: "/bin/bash -lc 'sleep 600'",
|
||||
cwd: "/workspace",
|
||||
processId: null,
|
||||
source: "agent",
|
||||
status: "inProgress",
|
||||
commandActions: [],
|
||||
aggregatedOutput: null,
|
||||
exitCode: null,
|
||||
durationMs: null,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
describe("CodexAppServerEventProjector", () => {
|
||||
it("projects assistant deltas and usage into embedded attempt results", async () => {
|
||||
const { onAssistantMessageStart, onPartialReply, projector } =
|
||||
@@ -1651,6 +1669,38 @@ describe("CodexAppServerEventProjector", () => {
|
||||
expect(result.assistantTexts).toEqual([]);
|
||||
});
|
||||
|
||||
it("keeps missing tool detail without overriding an explicit abort", async () => {
|
||||
const projector = await createProjector();
|
||||
projector.markAborted();
|
||||
|
||||
await projector.handleNotification(pendingCommandStarted("cmd-aborted"));
|
||||
await projector.handleNotification(turnWithStatus("interrupted"));
|
||||
|
||||
const result = projector.buildResult(buildEmptyToolTelemetry());
|
||||
|
||||
expect(result.aborted).toBe(true);
|
||||
expect(result.promptError).toBeNull();
|
||||
expect(result.promptErrorSource).toBeNull();
|
||||
expect(result.lastToolError).toMatchObject({
|
||||
toolName: "bash",
|
||||
error: expect.stringContaining("without a matching tool.result"),
|
||||
});
|
||||
});
|
||||
|
||||
it("fails closed when interrupted status has no abort marker", async () => {
|
||||
const projector = await createProjector();
|
||||
|
||||
await projector.handleNotification(pendingCommandStarted("cmd-interrupted"));
|
||||
await projector.handleNotification(turnWithStatus("interrupted"));
|
||||
|
||||
const result = projector.buildResult(buildEmptyToolTelemetry());
|
||||
|
||||
expect(result.aborted).toBe(false);
|
||||
expect(result.promptError).toContain("without a matching tool.result");
|
||||
expect(result.promptErrorSource).toBe("prompt");
|
||||
expect(result.lastToolError).toBeUndefined();
|
||||
});
|
||||
|
||||
it("does not fail a completed reply after a retryable app-server error notification", async () => {
|
||||
const projector = await createProjector();
|
||||
|
||||
|
||||
@@ -784,7 +784,10 @@ export class CodexAppServerEventProjector {
|
||||
assistantTexts.some((text) => text.trim().length > 0);
|
||||
this.synthesizeMissingToolResults({
|
||||
synthesize: legacyFailClosed,
|
||||
recordPromptError: legacyFailClosed && !hasDeliverableAssistantOnCompletedTurn,
|
||||
recordPromptError:
|
||||
legacyFailClosed &&
|
||||
!hasDeliverableAssistantOnCompletedTurn &&
|
||||
!this.aborted,
|
||||
});
|
||||
const lastAssistant =
|
||||
assistantTexts.length > 0
|
||||
|
||||
Reference in New Issue
Block a user