Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions src/lib/db/responsesContinuationStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 +
Expand Down Expand Up @@ -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 };
}
38 changes: 38 additions & 0 deletions tests/unit/responses-continuation-store.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Loading