mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-26 09:02:11 +03:00
fix(responses-continuation): fail closed on a log-truncated stored input/output array (#11473)
Validated in a combined 2-PR batch worktree off release/v3.8.51 tip. - Focused test: responses-continuation-store.test.ts — part of batch's 46/46 node:test run - typecheck:core, file-size, changelog-integrity, complexity, cognitive-complexity — all OK - Full-repo lint: 228 pre-existing dashboard react-hooks/* findings, unrelated to this diff Thanks for the live-verified root cause — a truncation sentinel getting forwarded upstream as a real Responses-API item, breaking the turn with a genuine 400, is exactly the kind of defect that's easy to miss without production traffic to reproduce against.
This commit is contained in:
@@ -30,6 +30,23 @@ function isPlainRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
}
|
||||
|
||||
// Both array-bounding implementations that clip a stored artifact's payload
|
||||
// for log-storage size (cloneBoundedChatLogPayload in
|
||||
// open-sse/handlers/chatCore/logTruncation.ts, and cloneBoundedForLog in
|
||||
// open-sse/utils/requestLogger.ts) prepend this sentinel in place of the
|
||||
// items they dropped once an array exceeds their tail-item cap -- so a real,
|
||||
// ordinary-length conversation resolves fine, but any conversation whose
|
||||
// input/output grew past that cap gets this object silently standing in for
|
||||
// real history. Reading it back as a genuine Responses-API item sent a
|
||||
// malformed reconstructed request upstream (translator 400:
|
||||
// "input item type 'missing' cannot be represented..."), which is worse than
|
||||
// the plain cache-miss this function is otherwise designed to fail into.
|
||||
const TRUNCATED_ARRAY_MARKER = "_omniroute_truncated_array";
|
||||
|
||||
function containsTruncatedArrayMarker(items: readonly unknown[]): boolean {
|
||||
return items.some((item) => isPlainRecord(item) && item[TRUNCATED_ARRAY_MARKER] === true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the full input + output a prior Responses API call produced, so
|
||||
* the caller can reconstruct `full_input = stored.input + stored.output +
|
||||
@@ -88,6 +105,7 @@ export function resolvePreviousResponseState(
|
||||
? clientResponse.output
|
||||
: clientResponse?.summary?.output;
|
||||
if (!Array.isArray(input) || !Array.isArray(output)) return null;
|
||||
if (containsTruncatedArrayMarker(input) || containsTruncatedArrayMarker(output)) return null;
|
||||
|
||||
return { input, output };
|
||||
}
|
||||
|
||||
@@ -223,6 +223,44 @@ test("resolvePreviousResponseState resolves input from clientRawRequest when pro
|
||||
});
|
||||
});
|
||||
|
||||
test("resolvePreviousResponseState fails closed when the stored input array was log-truncated", () => {
|
||||
// Real production shape: cloneBoundedChatLogPayload (chatCore/logTruncation.ts)
|
||||
// and cloneBoundedForLog (utils/requestLogger.ts) both prepend an
|
||||
// `_omniroute_truncated_array` sentinel in place of the items they dropped
|
||||
// once a logged array exceeds their tail-item cap (~24 items) -- routine
|
||||
// for any conversation that's been going a while, not an edge case. Reading
|
||||
// that sentinel back as a real Responses-API item and forwarding it upstream
|
||||
// produced a live 400: "input item type 'missing' cannot be represented in
|
||||
// Chat Completions" -- worse than the plain cache-miss this function is
|
||||
// otherwise designed to fail into.
|
||||
insertCallLog({
|
||||
id: "log-7",
|
||||
responseId: "resp_gen-truncated-history",
|
||||
apiKeyId: "key-1",
|
||||
detailState: "ready",
|
||||
artifactRelPath: "2026-01-01/log-7.json",
|
||||
});
|
||||
writeArtifact("2026-01-01/log-7.json", {
|
||||
clientRawRequest: {
|
||||
body: {
|
||||
input: [
|
||||
{ _omniroute_truncated_array: true, originalLength: 26, retainedTailItems: 24 },
|
||||
{ type: "function_call_output", call_id: "call_1", output: "ok" },
|
||||
],
|
||||
},
|
||||
},
|
||||
providerRequest: { body: { input: [] } },
|
||||
clientResponse: {
|
||||
summary: {
|
||||
id: "resp_gen-truncated-history",
|
||||
output: [{ type: "message", role: "assistant", content: "hello" }],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(store.resolvePreviousResponseState("resp_gen-truncated-history", "key-1"), null);
|
||||
});
|
||||
|
||||
test("resolvePreviousResponseState returns null when detail logging was never captured for this row", () => {
|
||||
insertCallLog({
|
||||
id: "log-5",
|
||||
|
||||
Reference in New Issue
Block a user