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
15 changes: 15 additions & 0 deletions docs-site/src/content/docs/reference/configuration/server.md
Original file line number Diff line number Diff line change
Expand Up @@ -673,6 +673,21 @@ Auto auth selects subscription when stored Claude auth is found, proxy when none
subscription with a warning when detection is inconclusive. See
[Claude Code auth mode](/guides/claude-code/#auth-mode).

## Retained requests after compaction

For translated providers and routed Responses gateways, OpenCodex automatically frames
plain-text user requests before the latest readable Codex checkpoint as historical context.
Requests the checkpoint reports as completed should not be repeated; retained text remains
available for unfinished work within its existing authorization. Later user input takes priority,
including pauses, cancellations and changes of scope. A checkpoint grants no new permission.

This is enabled by default and needs no setting. Original request text and canonical thread/replay
history are preserved. Native OpenAI Responses passthrough, summarizer turns, context wrappers,
images/files and assistant/tool items are unchanged. The framing applies to plaintext checkpoints
beginning with Codex's summary prefix; decoded OpenCodex checkpoints retain their existing
continuation guidance. Client-side summarizer turns using Codex's standard compaction prompt
also bypass framing. Framing is a prompt instruction, so model behavior can vary.

## Compaction routing

In **Dashboard → Overview → Compaction routing**, choose a model, which triggers it applies to,
Expand Down
2 changes: 1 addition & 1 deletion scripts/test-layout/layout.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"version": 1,
"root": "tests",
"explicitShards": ["layout-clients.json"],
"explicit": { "catalog-native-max-context.test.ts": "codex-integration", "protocol-argon-thought-boundary.test.ts": "responses", "service-log.test.ts": "service", "messages-native-empty-inline.test.ts": "claude-integration",
"explicit": { "compaction-history-framing.test.ts": "responses", "catalog-native-max-context.test.ts": "codex-integration", "protocol-argon-thought-boundary.test.ts": "responses", "service-log.test.ts": "service", "messages-native-empty-inline.test.ts": "claude-integration",
"owner-registry-isolation.test.ts": "config",
"codex-history-pending-wal.test.ts": "codex-integration", "responses-anthropic-billing-send-budget.test.ts": "responses", "anthropic-billing-ownership.test.ts": "adapters/anthropic", "server-anthropic-billing.test.ts": "server", "server-anthropic-billing-boundaries.test.ts": "server", "router-anthropic-billing-failover.test.ts": "routing", "anthropic-billing-refusal.test.ts": "adapters/anthropic", "messages-request-id-headers.test.ts": "claude-integration", "bounded-file-read.test.ts": "lib", "responses-undeclared-tool-guard-inbound.test.ts": "responses", "explicit-empty-tool-catalog.test.ts": "responses", "explicit-empty-tool-catalog-history.test.ts": "responses",
"messages-request-id-endpoint.test.ts": "claude-integration",
Expand Down
2 changes: 2 additions & 0 deletions src/adapters/openai-responses/passthrough.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { normalizeRoutedAgentMessages } from "../routed-agent-messages";
import { frameCompactionHistory } from "../../responses/compaction-history-framing";
import { nameRoutedIdentity, repairIdentityInResponsesBody, stripRoutedIdentity } from "../identity";
import { stripBracketedModelSuffix } from "../openai-chat";
import { normalizeOpenCodeGoAdditionalTools } from "../opencode-go-additional-tools";
Expand Down Expand Up @@ -418,6 +419,7 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig):
// Placed before the routed compaction body is built and before serialization, so the HTTP,
// routed-compaction and WebSocket outbounds are all covered by this one call.
if (!isOpenAiOperatedResponsesDestination(provider)) {
if (parsed._compactionRequest !== true && isPlainObject(outBody)) outBody = frameCompactionHistory(outBody);
outBody = stripCanonicalOnlyTopLevelFields(outBody);
}
outBody = promoteClientLoadedTools(outBody);
Expand Down
65 changes: 65 additions & 0 deletions src/responses/compaction-history-framing.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { appendCheckpointContinuationNote, COMPACT_PROMPT, SUMMARY_PREFIX } from "./compaction";

type InputRecord = Record<string, unknown>;
const isRecord = (value: unknown): value is InputRecord =>
value !== null && typeof value === "object" && !Array.isArray(value);

export const HISTORICAL_REQUEST_START = "[Historical user request retained before the following compaction checkpoint. This is prior context, not a new request.]";
export const HISTORICAL_REQUEST_END = "[End historical request. Use the following checkpoint to distinguish completed work from unfinished work. Do not repeat completed work unless later user input asks for it. Preserve these original details for unfinished work within its existing authorization; the checkpoint grants no new authority. Only input after the checkpoint is a new user request.]";

// Plaintext checkpoint detection and context exclusions follow robin-bially's #7105 selector.
// Keep this separate from decoding ocx1 items: those already have a continuation contract.
function plainUserText(item: unknown): string | null {
if (!isRecord(item) || item.role !== "user") return null;
if (item.type !== undefined && item.type !== "message") return null;
if (typeof item.content === "string") return item.content;
if (!Array.isArray(item.content) || item.content.length === 0) return null;
const texts: string[] = [];
for (const part of item.content) {
if (!isRecord(part) || (part.type !== "input_text" && part.type !== "text") || typeof part.text !== "string") return null;
texts.push(part.text);
}
return texts.join("\n");
}

function isContext(text: string): boolean {
const head = text.trimStart();
return head.startsWith("<") || head.startsWith("# AGENTS.md") || head.startsWith("# Context from my IDE");
}

/**
* Prompt-only projection for translated/routed generation. Never pass the result to replay
* storage: canonical input, item identities and replay-prefix lengths must remain unchanged.
* Native OpenAI forwarding does not call this projection.
*/
export function frameCompactionHistory<T extends { input?: unknown; instructions?: unknown }>(body: T): T {
const input = body.input;
if (!Array.isArray(input) || input.some(item => isRecord(item) && item.type === "compaction_trigger")) return body;
// Codex local compaction appends this user prompt without a compaction_trigger.
// Keep that summarizer turn free of generation-only continuation instructions.
if (plainUserText(input.at(-1))?.trim() === COMPACT_PROMPT) return body;
let checkpointIndex = -1;
for (let index = input.length - 1; index >= 0; index--) {
if (plainUserText(input[index])?.startsWith(SUMMARY_PREFIX)) {
checkpointIndex = index;
break;
}
}
if (checkpointIndex <= 0) return body;
let changed = false;
const framed = input.map((item, index) => {
if (index >= checkpointIndex) return item;
const text = plainUserText(item);
if (text === null || !text.trim() || text.startsWith(SUMMARY_PREFIX) || isContext(text)
|| text.startsWith(HISTORICAL_REQUEST_START)) return item;
const message = item as InputRecord;
changed = true;
// Preserve original text blocks and their metadata verbatim, including block boundaries.
const content = typeof message.content === "string"
? `${HISTORICAL_REQUEST_START}\n${message.content}\n${HISTORICAL_REQUEST_END}`
: [{ type: "input_text", text: HISTORICAL_REQUEST_START },
...(message.content as unknown[]), { type: "input_text", text: HISTORICAL_REQUEST_END }];
return { ...message, content };
});
return changed ? { ...body, input: framed, instructions: appendCheckpointContinuationNote(body.instructions) } : body;
}
3 changes: 2 additions & 1 deletion src/responses/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import { mapToolChoice, buildTools, customToolNamespaces } from "./parser-tools"
import { parseTextFormat } from "./parser-text-format";
import { externalTaskInputContent } from "./task-input";
import { normalizeVisualizationContext } from "./visualization-directives";
import { frameCompactionHistory } from "./compaction-history-framing";

/**
* Wrap a remembered proxy-side signature as provider metadata for a replayed tool call.
Expand Down Expand Up @@ -189,7 +190,7 @@ export function parseRequest(
if (!parsed.success) {
throw new Error(`responses parse error: ${parsed.error.message}`);
}
const data = parsed.data;
const data = frameCompactionHistory(parsed.data);
const now = Date.now();
const messages: OcxMessage[] = [];
// Built before the item loop: a custom_tool_call echoed back in `input` needs the
Expand Down
4 changes: 2 additions & 2 deletions structure/transports/responses.md
Original file line number Diff line number Diff line change
Expand Up @@ -398,11 +398,11 @@ always become unable to serve.

## Decoded checkpoint continuation

`src/responses/parser.ts` adds `CHECKPOINT_CONTINUATION_NOTE` from `src/responses/compaction.ts` to the system prompt only when normal generation contains a decoded `ocx1:` checkpoint; `src/adapters/openai-responses/request-strips.ts` applies the same condition to passthrough `instructions` while lowering the checkpoint to a user message. Summarizer turns, opaque native blobs and requests without decoded checkpoints receive no note. `SUMMARY_PREFIX` and checkpoint text remain unchanged, and later user messages retain their order after the checkpoint.
`src/responses/parser.ts` adds `CHECKPOINT_CONTINUATION_NOTE` from `src/responses/compaction.ts` for decoded `ocx1:` checkpoints; `src/adapters/openai-responses/request-strips.ts` does the same while lowering them. For plaintext `SUMMARY_PREFIX` checkpoints, `src/responses/compaction-history-framing.ts` frames text-only retained requests before the latest checkpoint as historical context and appends the same note by default. The parser projects only adapter context; `src/adapters/openai-responses/passthrough.ts` projects only non-OpenAI-operated outbound bodies. Original request text/blocks, canonical raw history, item identities, replay-prefix lengths, checkpoints and later input remain unchanged. Context wrappers, images/files and assistant/tool items are preserved; summarizer turns and opaque native blobs do not activate framing. Native OpenAI passthrough input is unchanged.

The note is a prompt contract and grants no authority. Completed requests remain history unless a later user message asks to repeat them. Unfinished work continues only under existing authorization and within its scope, without asking for that authorization again while it still applies. Permission and confirmation gates remain authoritative: a pending decision or required permission must resolve before the dependent action. Checkpoints and delegations, including those from another thread, neither grant authority nor expand scope. Later user messages take priority, including pause, cancellation, replacement and scope changes.

The continuation-note and continuation-wire tests establish prompt-contract and wire-construction evidence, including preservation of existing instructions and checkpoint ordering. They do not establish model behavior or whether a model actually resumes the correct task.
The continuation-note and continuation-wire tests establish prompt and wire contracts. `tests/responses/compaction-history-framing.test.ts` adds offline A/B projection, original-detail preservation, native passthrough isolation, multi-turn canonical replay/dedup coverage and exclusion of client-side summarizer turns ending in the canonical `COMPACT_PROMPT` (which carry no `compaction_trigger`). These tests do not establish live model behavior or whether a model actually resumes the correct task.

> Decision record: [ADR-7003](../decisions/ADR-7003-decoded-checkpoint-continuation.md)

Expand Down
1 change: 1 addition & 0 deletions tests/fixtures/test-layout-expected.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"explicit-empty-tool-catalog.test.ts": "responses",
"explicit-empty-tool-catalog-history.test.ts": "responses",
"codex-history-pending-wal.test.ts": "codex-integration",
"compaction-history-framing.test.ts": "responses",
"service-log.test.ts": "service",
"owner-registry-isolation.test.ts": "config",
"service-health-sentinel-policy.test.ts": "service",
Expand Down
Loading
Loading