You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maka.apache.org by as...@apache.org on 2026/09/04 10:36:23 UTC
(maka) branch main updated: fix(runtime): preserve Codex compaction chronology (#4754)
This is an automated email from the ASF dual-hosted git repository.
Astro-Han pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/maka.git
The following commit(s) were added to refs/heads/main by this push:
new 4ed6255f29 fix(runtime): preserve Codex compaction chronology (#4754)
4ed6255f29 is described below
commit 4ed6255f2998eff93414491cb14305f3bca0884b
Author: AstroHan <le...@outlook.com>
AuthorDate: Fri Sep 4 18:36:19 2026 +0800
fix(runtime): preserve Codex compaction chronology (#4754)
#4750 repaired chronology in the ordinary replay materializer, but the OpenAI Codex
native history compaction projection carried its own implementation and was left
wrong. It kept one global `Step` per `stepId` and emitted the accumulated step at
that step's first timeline position, so a durable sequence of `text A -> text B ->
late tool call/result A` reached the provider as `text A + call A -> result A ->
text B`. The provider then sealed that incorrect order into an opaque durable
checkpoint, which nothing downstream can correct.
The authoritative fix is to stop keying step data globally: each compaction timeline
entry owns its own step segment, and a step item joins the preceding segment only
when that segment carries the same identity. Ordering therefore follows the durable
sequence instead of a first-seen index.
A regression observes the actual compaction request through the public summarizer
boundary; ablation confirms that removing the production repair while keeping the
test restores the wrong chronology. Behaviour change is limited to the order of
entries handed to Codex compaction — no schema, storage or API migration.
Generated-by: OpenAI Codex
---
.../openai-codex-history-compactor.test.ts | 129 +++++++++++++++++++++
.../runtime/src/openai-codex-history-compactor.ts | 15 +--
2 files changed, 135 insertions(+), 9 deletions(-)
diff --git a/packages/runtime/src/__tests__/openai-codex-history-compactor.test.ts b/packages/runtime/src/__tests__/openai-codex-history-compactor.test.ts
index b707be8eb4..5d93f87be2 100644
--- a/packages/runtime/src/__tests__/openai-codex-history-compactor.test.ts
+++ b/packages/runtime/src/__tests__/openai-codex-history-compactor.test.ts
@@ -19,7 +19,11 @@
import assert from 'node:assert/strict';
import { describe, test } from 'node:test';
+import type { LanguageModelV4StreamPart } from '@ai-sdk/provider';
+import type { RuntimeEvent } from '@maka/core/runtime-event';
+import { MockLanguageModelV4, simulateReadableStream } from 'ai/test';
import {
+ buildOpenAiCodexHistoryCompactor,
extractOpenAiCodexCompactionState,
shouldFallbackFromOpenAiCodexHistoryCompaction,
withOpenAiCodexHistoryCompactionFallback,
@@ -129,3 +133,128 @@ describe('OpenAI Codex compaction output', () => {
);
});
});
+
+describe('OpenAI Codex compaction input', () => {
+ test('keeps an interrupted late tool step after the intervening assistant step', async () => {
+ const model = compactionModel();
+ const summarize = buildOpenAiCodexHistoryCompactor({
+ resolveModel: () => model,
+ connectionId: 'codex-subscription',
+ modelId: 'gpt-5.3-codex',
+ });
+
+ await summarize({
+ sessionId: 'session-1',
+ turnId: 'turn-current',
+ source: {
+ foldedRuntimeEvents: [
+ assistantTextEvent('text-a', 'shared-step', 'Text A'),
+ assistantTextEvent('text-b', 'intervening-step', 'Text B'),
+ toolCallEvent(),
+ toolResultEvent(),
+ ],
+ },
+ });
+
+ assert.deepEqual(
+ model.doStreamCalls[0]?.prompt.map((message) => ({
+ role: message.role,
+ parts: Array.isArray(message.content)
+ ? message.content.map((part) => (part.type === 'text' ? `text:${part.text}` : part.type))
+ : [`text:${message.content}`],
+ })),
+ [
+ { role: 'assistant', parts: ['text:Text A'] },
+ { role: 'assistant', parts: ['text:Text B'] },
+ { role: 'assistant', parts: ['tool-call'] },
+ { role: 'tool', parts: ['tool-result'] },
+ ],
+ );
+ });
+});
+
+function assistantTextEvent(id: string, stepId: string, text: string): RuntimeEvent {
+ return runtimeEvent({
+ id,
+ role: 'model',
+ author: 'agent',
+ refs: { providerEventId: stepId },
+ content: { kind: 'text', text },
+ });
+}
+
+function toolCallEvent(): RuntimeEvent {
+ return runtimeEvent({
+ id: 'tool-call',
+ role: 'model',
+ author: 'agent',
+ refs: { stepId: 'shared-step' },
+ content: {
+ kind: 'function_call',
+ id: 'read-1',
+ name: 'Read',
+ args: { path: 'notes.md' },
+ },
+ });
+}
+
+function toolResultEvent(): RuntimeEvent {
+ return runtimeEvent({
+ id: 'tool-result',
+ role: 'tool',
+ author: 'tool',
+ content: {
+ kind: 'function_response',
+ id: 'read-1',
+ name: 'Read',
+ result: { kind: 'text', text: 'contents' },
+ isError: false,
+ },
+ });
+}
+
+function runtimeEvent(
+ input: Pick<RuntimeEvent, 'id' | 'role' | 'author'> & Pick<RuntimeEvent, 'content' | 'refs'>,
+): RuntimeEvent {
+ return {
+ id: input.id,
+ invocationId: 'invocation-1',
+ runId: 'run-1',
+ sessionId: 'session-1',
+ turnId: 'turn-previous',
+ ts: 1,
+ partial: false,
+ role: input.role,
+ author: input.author,
+ content: input.content,
+ refs: input.refs,
+ };
+}
+
+function compactionModel(): MockLanguageModelV4 {
+ const chunks: LanguageModelV4StreamPart[] = [
+ { type: 'stream-start', warnings: [] },
+ {
+ type: 'custom',
+ kind: 'openai.compaction',
+ providerMetadata: { openai: { itemId: 'cmp-1', encryptedContent: 'encrypted-1' } },
+ },
+ {
+ type: 'finish',
+ finishReason: { unified: 'stop', raw: 'stop' },
+ usage: {
+ inputTokens: { total: 1, noCache: 1, cacheRead: 0, cacheWrite: 0 },
+ outputTokens: { total: 1, text: 0, reasoning: 0 },
+ },
+ },
+ ];
+ return new MockLanguageModelV4({
+ doStream: {
+ stream: simulateReadableStream({
+ chunks,
+ initialDelayInMs: null,
+ chunkDelayInMs: null,
+ }),
+ },
+ });
+}
diff --git a/packages/runtime/src/openai-codex-history-compactor.ts b/packages/runtime/src/openai-codex-history-compactor.ts
index c9018cbdce..11c705ab87 100644
--- a/packages/runtime/src/openai-codex-history-compactor.ts
+++ b/packages/runtime/src/openai-codex-history-compactor.ts
@@ -204,7 +204,7 @@ function openAiCodexCompactionMessages(
text?: Text;
};
type TimelineEntry =
- | { kind: 'step'; stepId: string }
+ | { kind: 'step'; stepId: string; value: Step }
| { kind: 'legacy_call'; call: ToolCall }
| { kind: 'text'; item: Text }
| { kind: 'thinking'; item: Thinking };
@@ -218,15 +218,12 @@ function openAiCodexCompactionMessages(
if (item.kind === 'tool_result') results.set(item.toolCallId, item);
}
- const steps = new Map<string, Step>();
const timeline: TimelineEntry[] = [];
const step = (stepId: string): Step => {
- let value = steps.get(stepId);
- if (!value) {
- value = { calls: [], reasoning: [] };
- steps.set(stepId, value);
- timeline.push({ kind: 'step', stepId });
- }
+ const last = timeline.at(-1);
+ if (last?.kind === 'step' && last.stepId === stepId) return last.value;
+ const value = { calls: [], reasoning: [] };
+ timeline.push({ kind: 'step', stepId, value });
return value;
};
for (const item of items) {
@@ -322,7 +319,7 @@ function openAiCodexCompactionMessages(
for (const entry of timeline) {
if (entry.kind === 'step') {
- pushStep(steps.get(entry.stepId)!);
+ pushStep(entry.value);
} else if (entry.kind === 'legacy_call') {
pushStep({ calls: [entry.call], reasoning: [] });
} else if (entry.kind === 'thinking') {
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org