FN-6594: classify Codex transcript desync as non-continuable
Recognize Codex transcript replay desync failures so completed tasks can recover instead of wedging. - Add Codex transcript-desync matching to non-continuable session error detection. - Cover canonical and symmetric missing function-call-output messages while excluding auth, quota, and ordinary bad-input errors. - Add self-healing coverage for post-done wedges with Codex envelope and log evidence. Files changed: .../post-done-continuation-no-wedge.test.ts | 72 ++++++++++++++++++++++ .../src/__tests__/transient-error-detector.test.ts | 24 +++++++- packages/engine/src/transient-error-detector.ts | 7 ++- 3 files changed, 101 insertions(+), 2 deletions(-) Fusion-Task-Id: FN-6594 Fusion-Task-Lineage: 517f39c8-9bbb-47d2-a4a4-495eaf8c3fca
This commit is contained in:
@@ -301,6 +301,78 @@ describe("FN-5866 reliability interactions: post-done continuation no wedge", ()
|
||||
manager.stop();
|
||||
});
|
||||
|
||||
it("self-heals Codex transcript-desync post-done wedges without swallowing generic 400s", async () => {
|
||||
const codexEnvelope = "Codex error: " + JSON.stringify({
|
||||
type: "error",
|
||||
error: {
|
||||
type: "invalid_request_error",
|
||||
message: "No tool call found for function call output with call_id call_2KewW55MyBgwZoNtMubFNpUb.",
|
||||
param: "input",
|
||||
},
|
||||
status: 400,
|
||||
});
|
||||
const symmetricLogEvidence = "No function call found for function call output with call_id call_2KewW55MyBgwZoNtMubFNpUb.";
|
||||
const envelopeWedged = makeTask({
|
||||
id: "FN-6594-CODEX-ENVELOPE-WEDGED",
|
||||
column: "in-review",
|
||||
status: "failed",
|
||||
error: codexEnvelope,
|
||||
steps: [{ name: "Implement", status: "done" as const }],
|
||||
log: [{ timestamp: new Date(Date.now() - 60_000).toISOString(), action: "Task marked done by agent" } as any],
|
||||
});
|
||||
const logEvidenceWedged = makeTask({
|
||||
id: "FN-6594-CODEX-LOG-WEDGED",
|
||||
column: "in-review",
|
||||
status: "failed",
|
||||
error: "Session failed while replaying transcript",
|
||||
steps: [{ name: "Implement", status: "done" as const }],
|
||||
log: [
|
||||
{ timestamp: new Date(Date.now() - 60_000).toISOString(), action: "Task marked done by agent" } as any,
|
||||
{
|
||||
timestamp: new Date(Date.now() - 30_000).toISOString(),
|
||||
action: "executor post-done continuation failed",
|
||||
outcome: symmetricLogEvidence,
|
||||
} as any,
|
||||
],
|
||||
});
|
||||
const badInputWedged = makeTask({
|
||||
id: "FN-6594-BAD-INPUT-WEDGED",
|
||||
column: "in-review",
|
||||
status: "failed",
|
||||
error: "400 invalid_request_error: invalid temperature",
|
||||
steps: [{ name: "Implement", status: "done" as const }],
|
||||
log: [
|
||||
{ timestamp: new Date(Date.now() - 60_000).toISOString(), action: "Task marked done by agent" } as any,
|
||||
{ timestamp: new Date(Date.now() - 30_000).toISOString(), action: "quota exceeded" } as any,
|
||||
],
|
||||
});
|
||||
const store = createSelfHealingStore([envelopeWedged, logEvidenceWedged, badInputWedged]);
|
||||
const manager = new SelfHealingManager(store, { rootDir: "/tmp/repo" });
|
||||
|
||||
expect(await manager.recoverPostDoneNonContinuableWedge()).toBe(2);
|
||||
|
||||
for (const recovered of [envelopeWedged, logEvidenceWedged]) {
|
||||
expect(recovered.column).toBe("in-review");
|
||||
expect(recovered.status).toBeUndefined();
|
||||
expect(recovered.error).toBeUndefined();
|
||||
expect(recovered.completionHandoffLimboRecoveryCount).toBe(1);
|
||||
expect(
|
||||
(recovered.log ?? []).some((entry: any) => entry.action.includes("Auto-recovered completed-task non-continuable wedge")),
|
||||
).toBe(true);
|
||||
expect(
|
||||
((store as any).__audits as any[]).some(
|
||||
(event: any) => event.mutationType === "task:auto-recover-post-done-noncontinuable-wedge" && event.target === recovered.id,
|
||||
),
|
||||
).toBe(true);
|
||||
}
|
||||
|
||||
expect(badInputWedged.status).toBe("failed");
|
||||
expect(badInputWedged.error).toBe("400 invalid_request_error: invalid temperature");
|
||||
expect(badInputWedged.completionHandoffLimboRecoveryCount).toBeUndefined();
|
||||
expect(((store as any).__audits as any[]).some((event: any) => event.target === badInputWedged.id)).toBe(false);
|
||||
manager.stop();
|
||||
});
|
||||
|
||||
it("falls through to terminal failure after the non-continuable fresh-session retry budget is exhausted", async () => {
|
||||
const task = makeTask({
|
||||
id: "FN-5866-INCOMPLETE-EXHAUSTED",
|
||||
|
||||
@@ -316,13 +316,35 @@ describe("Transient Error Detector", () => {
|
||||
.toBe(true);
|
||||
});
|
||||
|
||||
it("returns false for unrelated and provider role-validation errors", () => {
|
||||
it("returns true for Codex transcript-desync errors", () => {
|
||||
expect(
|
||||
isNonContinuableSessionError(
|
||||
"No tool call found for function call output with call_id call_2KewW55MyBgwZoNtMubFNpUb.",
|
||||
),
|
||||
).toBe(true);
|
||||
expect(
|
||||
isNonContinuableSessionError(
|
||||
'Codex error: {"type":"error","error":{"type":"invalid_request_error","message":"No tool call found for function call output with call_id call_2KewW55MyBgwZoNtMubFNpUb.","param":"input"},"status":400}',
|
||||
),
|
||||
).toBe(true);
|
||||
expect(
|
||||
isNonContinuableSessionError(
|
||||
"No function call found for function call output with call_id call_2KewW55MyBgwZoNtMubFNpUb.",
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("returns false for unrelated, operator-actionable, and ordinary bad-input errors", () => {
|
||||
expect(isNonContinuableSessionError("socket hang up")).toBe(false);
|
||||
expect(
|
||||
isNonContinuableSessionError(
|
||||
"developer is not one of ['system', 'assistant', 'user', 'tool', 'function'] - 'messages.[0].role'",
|
||||
),
|
||||
).toBe(false);
|
||||
expect(isNonContinuableSessionError("invalid api key")).toBe(false);
|
||||
expect(isNonContinuableSessionError("quota exceeded")).toBe(false);
|
||||
expect(isNonContinuableSessionError("billing issue: quota exceeded")).toBe(false);
|
||||
expect(isNonContinuableSessionError("400 invalid_request_error: invalid temperature")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -193,6 +193,11 @@ export function extractMissingModulePath(errorMessage: string): string | null {
|
||||
|
||||
const UNSUPPORTED_MESSAGE_ROLE_PATTERN = /\bmessages\.\[\d+\]\.role\b[\s\S]*\bis not one of\b|\bis not one of\b[\s\S]*\bmessages\.\[\d+\]\.role\b/i;
|
||||
const NON_CONTINUABLE_SESSION_PATTERN = /cannot continue from message role\s*[:=-]?\s*(?:['"`]?)(assistant|tool|function|system|user)(?:['"`]?)\b/i;
|
||||
/*
|
||||
FNXC:Reliability-ErrorClassification 2026-06-17-14:48:
|
||||
FN-6594 treats Codex transcript-desync on post-done session re-entry as non-continuable when a `function_call_output` is replayed without its `function_call`, or the symmetric function-call/output pair is missing. Anchor on the original `No tool call found for function call output with call_id ...` symptom so executor fresh-session retry and self-healing post-done wedge recovery engage without swallowing generic 400/auth/quota errors.
|
||||
*/
|
||||
const CODEX_TRANSCRIPT_DESYNC_NON_CONTINUABLE_PATTERN = /\bno\s+(?:tool\s+call|function\s+call)\s+found\s+for\s+function\s+call\s+output\b/i;
|
||||
const MODEL_AUTH_TIER_INCOMPATIBILITY_PATTERNS: RegExp[] = [
|
||||
// Codex ChatGPT-account auth-tier incompatibility: the model is valid, but
|
||||
// unavailable for the current auth tier.
|
||||
@@ -228,7 +233,7 @@ export function isNonContinuableSessionError(errorMessage: string): boolean {
|
||||
if (!errorMessage || typeof errorMessage !== "string") {
|
||||
return false;
|
||||
}
|
||||
return NON_CONTINUABLE_SESSION_PATTERN.test(errorMessage);
|
||||
return NON_CONTINUABLE_SESSION_PATTERN.test(errorMessage) || CODEX_TRANSCRIPT_DESYNC_NON_CONTINUABLE_PATTERN.test(errorMessage);
|
||||
}
|
||||
|
||||
const OPERATOR_ACTIONABLE_AGENT_ERROR_PATTERNS: RegExp[] = [
|
||||
|
||||
Reference in New Issue
Block a user