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