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/08/31 14:07:24 UTC

(maka) branch main updated: refactor(runtime): budget and compact effective model history (#4348)

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 36f058c90f refactor(runtime): budget and compact effective model history (#4348)
36f058c90f is described below

commit 36f058c90ffe58b5940d16ea0dd29f908a15d9ee
Author: AstroHan <le...@outlook.com>
AuthorDate: Mon Aug 31 22:07:19 2026 +0800

    refactor(runtime): budget and compact effective model history (#4348)
    
    * refactor(runtime): budget and compact effective model history
    
    Context budgeting, summarization, provider-native compaction, and history
    compact checkpoint source digests each re-derived their own view of a Tool
    Result. All four read the raw `function_response.result` — the execution
    fact — rather than the durable model projection that #4287 commits atomically
    at T2 and that live continuation, next-Turn replay, and restart already
    replay. A bounded or redacted projection was therefore sized, summarized, and
    hashed at the size and content of the raw evidence it replaced.
    
    Move the sizing authority to the reducer: `estimateRuntimeEventChars`,
    `estimateRuntimeEventsTokens`, and `groupEventsByTurn` leave the
    dependency-free scalar helpers and live in `model-history.ts`, where a
    `function_response` is measured through `effectiveToolResultModelValue` —
    one decode of the effective projection shared by sizing and digests. The
    summary size floor rides the same estimator, so it is fixed by construction.
    
    Route the two compaction materializers through the same effective output as
    primary replay: `effectiveReplayToolResultOutput` prefers the durable
    projection and falls back to raw output only for a response that has none
    (legacy or provider-native). This removes the divergent `toolResultOutput`
    call in the LLM summarizer and the Codex compactor.
    
    Derive the checkpoint source digest from effective model history. Rewriting
    raw evidence under an unchanged projection no longer invalidates a valid
    checkpoint, while replacing the projection does — a checkpoint can never
    replay over content it did not cover. That is a source-policy change, so
    `HISTORY_COMPACT_SOURCE_POLICY_VERSION` becomes v2 and a v1 checkpoint fails
    the existing shape check and re-summarizes instead of claiming coverage this
    policy never computed. `HistoryCompactCheckpoint` stays a contiguous-prefix
    replacement; no sparse override is introduced.
    
    Covered by `effective-history-compaction.test.ts` (budget sizes the
    projection, summarization cannot read replaced raw output, the digest ignores
    raw evidence but tracks the projection, a v1-policy checkpoint no longer
    validates) and the runtime workspace suite.
    
    Part of #4283 (PR 2).
    
    Generated-by: Claude Code
    
    * refactor(runtime): anchor the checkpoint digest on persisted fields
    
    Review found that hashing the MATERIALIZED effective value bound a durable
    digest to in-memory representation: the discriminant labels of an internal
    union, the artifact placeholder wording in
    `durableProjectionToToolResultOutput`, and — for legacy responses re-derived
    through the compatibility codec — the Bash/File projectors and the encoder's
    size bounds. Any later refactor of those would silently invalidate every
    persisted checkpoint, with no reason for anyone to bump the source policy.
    
    Select the persisted field that carries the response's model-visible content
    instead: `providerOutput` for a provider-native result, `modelProjection` when
    the response has one, and otherwise the raw `result` its effective content is
    still derived from — which is exactly what the pre-projection digest hashed.
    This is a structural choice over durable data only, so the digest is stable
    under code evolution while keeping the property it was changed for: rewriting
    raw evidence under an unchanged projection does not invalidate a checkpoint,
    and replacing the projection does.
    
    Sizing follows the same simplification, since the digest was its only other
    consumer: `EffectiveToolResultModelValue` and `effectiveToolResultModelValue`
    are gone and `estimateEffectiveToolResultChars` switches on the reducer's own
    `decodeEffectiveToolResultProjection` result. That leaves
    `estimateToolResultOutputChars` with a single caller, so it moves in beside it
    and stops being exported — which also drops the import edge this PR had added
    from the projection codec back into the budgeting helpers.
    
    Measured while re-deriving a legacy response through the whole compatibility
    codec on every call (200 responses x 200 KB: 94 ms, against 4 ms for the raw
    length it replaced), and one request measures the same events several times —
    budget verdicts, the compactable-content filter, and checkpoint prefix
    matching each walk the history. Memoize the per-response count on the content
    object, which is immutable once committed.
    
    Refs #4283.
    
    Generated-by: Claude Code
    
    * fix(runtime): treat a superseded compaction checkpoint as absent, not fatal
    
    Bumping the source policy to v2 was described as a safe degradation because
    compaction fails open and re-summarizes. Review showed that is true of
    `history-compaction`, `ai-sdk-compaction`, and the backend, but NOT of
    conversation copy: `cloneAgentRunEvent` threw both when the shape check
    rejected a checkpoint and when its prefix no longer matched. A run ledger
    keeps every checkpoint it ever recorded, so a session that had compacted
    under the old policy would have failed to branch, edit-and-resend, or start a
    side conversation — permanently, since a newer checkpoint does not remove the
    older record. Nothing in the suite covered it, because every copy fixture
    built its checkpoint under the current policy.
    
    A copy carries the canonical raw RuntimeEvents and can compact again, so drop
    such a record instead, joining the `return null` that opaque provider state
    already used for the same reason.
    
    The same rejection made `execution-inspect` report every pre-upgrade session
    as `compaction_checkpoint_invalid` at severity `error`, which would bury the
    records that really are damaged. Distinguish a well-formed checkpoint minted
    under an older policy with `isSupersededHistoryCompactCheckpoint`, and report
    it as `compaction_checkpoint_superseded` at `info` with a matching
    `superseded` validation state.
    
    Covered by a copy regression test that records a v1-policy checkpoint and
    asserts the copy completes carrying none; it fails against the previous
    throwing branch.
    
    Refs #4283.
    
    Generated-by: Claude Code
---
 packages/core/src/execution-inspect.ts             |   7 +-
 .../runtime/src/__tests__/context-budget.test.ts   |   2 +-
 .../src/__tests__/conversation-copy.test.ts        | 113 ++++++++++++
 .../__tests__/effective-history-compaction.test.ts | 195 +++++++++++++++++++++
 packages/runtime/src/context-budget-helpers.ts     |  61 +------
 packages/runtime/src/context-budget.ts             |  10 +-
 packages/runtime/src/conversation-copy.ts          |  17 +-
 .../runtime/src/durable-tool-result-projection.ts  |  18 ++
 packages/runtime/src/execution-inspect.ts          |  19 +-
 packages/runtime/src/history-compact-checkpoint.ts |  56 +++++-
 packages/runtime/src/history-compact-summarizer.ts |   4 +-
 .../src/history-compact-summary-validation.ts      |   3 +-
 packages/runtime/src/history-compaction.ts         |   7 +-
 packages/runtime/src/model-history.ts              | 116 +++++++++++-
 .../runtime/src/openai-codex-history-compactor.ts  |   4 +-
 packages/runtime/src/session-recap.ts              |   3 +-
 16 files changed, 542 insertions(+), 93 deletions(-)

diff --git a/packages/core/src/execution-inspect.ts b/packages/core/src/execution-inspect.ts
index 1a5303454f..5022e1b23e 100644
--- a/packages/core/src/execution-inspect.ts
+++ b/packages/core/src/execution-inspect.ts
@@ -70,7 +70,8 @@ export interface AgentRunInspectToolSummary {
 
 export interface AgentRunInspectCompactionCheckpoint {
   eventId: string;
-  validation: 'shape_valid' | 'invalid';
+  /** `superseded`: well-formed, but minted under an older source policy. */
+  validation: 'shape_valid' | 'invalid' | 'superseded';
   checkpointId?: string;
   policyVersion?: string;
   sourceCoverage?: ExecutionLogCoverage;
@@ -297,7 +298,9 @@ function isCompactionCheckpoint(value: unknown): boolean {
       ['checkpointId', 'policyVersion', 'sourceCoverage'],
     ) &&
     typeof value.eventId === 'string' &&
-    (value.validation === 'shape_valid' || value.validation === 'invalid') &&
+    (value.validation === 'shape_valid' ||
+      value.validation === 'invalid' ||
+      value.validation === 'superseded') &&
     isOptionalString(value.checkpointId) &&
     isOptionalString(value.policyVersion) &&
     (value.sourceCoverage === undefined || isCoverage(value.sourceCoverage))
diff --git a/packages/runtime/src/__tests__/context-budget.test.ts b/packages/runtime/src/__tests__/context-budget.test.ts
index c360aea827..25389b61dc 100644
--- a/packages/runtime/src/__tests__/context-budget.test.ts
+++ b/packages/runtime/src/__tests__/context-budget.test.ts
@@ -22,7 +22,7 @@ import { createHash } from 'node:crypto';
 import { test } from 'node:test';
 import type { RuntimeEvent } from '@maka/core/runtime-event';
 import { applyRuntimeEventContextBudget } from '../context-budget.js';
-import { estimateRuntimeEventsTokens } from '../context-budget-helpers.js';
+import { estimateRuntimeEventsTokens } from '../model-history.js';
 import { buildHistoryCompactCheckpoint } from '../history-compact-checkpoint.js';
 
 test('estimates only model-visible provider context', () => {
diff --git a/packages/runtime/src/__tests__/conversation-copy.test.ts b/packages/runtime/src/__tests__/conversation-copy.test.ts
index cf7ccbb3a4..e19d1b9a1f 100644
--- a/packages/runtime/src/__tests__/conversation-copy.test.ts
+++ b/packages/runtime/src/__tests__/conversation-copy.test.ts
@@ -2402,6 +2402,119 @@ test('conversation copy rebuilds an inline checkpoint without legacy child event
   }
 });
 
+test('conversation copy drops a checkpoint from a superseded source policy instead of failing', async () => {
+  // A ledger keeps every checkpoint it ever recorded, so a session that
+  // compacted under an older source policy still carries that record forever.
+  // Copy must treat it as absent — the copy carries the canonical raw
+  // RuntimeEvents and can compact again — or those sessions become permanently
+  // unbranchable (apache/maka#4283).
+  const root = await mkdtemp(join(tmpdir(), 'maka-conversation-legacy-policy-copy-'));
+  try {
+    const runStore = createSqliteAgentRunStore(root);
+    const runtimeEventStore = createWorkspaceRuntimeStore(root);
+    const run = agentRunHeader({
+      runId: 'run-source',
+      invocationId: 'invocation-1',
+      turnId: 'turn-1',
+      cwd: root,
+      completedAt: 3,
+    });
+    await runStore.createRun(run);
+    const sourceEvents = [
+      runtimeEvent({
+        id: 'event-user',
+        invocationId: 'invocation-1',
+        runId: 'run-source',
+        turnId: 'turn-1',
+        role: 'user',
+        author: 'user',
+        content: { kind: 'text', text: 'first' },
+      }),
+      runtimeEvent({
+        id: 'event-terminal',
+        invocationId: 'invocation-1',
+        runId: 'run-source',
+        turnId: 'turn-1',
+        ts: 2,
+        role: 'system',
+        author: 'system',
+        status: 'completed',
+      }),
+    ];
+    for (const event of sourceEvents) {
+      await runtimeEventStore.appendRuntimeEvent(event.sessionId, event.runId, event);
+    }
+    const current = buildHistoryCompactCheckpoint({
+      sessionId: 'session-source',
+      coveredRuntimeEvents: sourceEvents.filter(isHistoryCompactContentEvent),
+      summary: 'Everything so far is complete.',
+      summaryFormat: 'legacy_freeform',
+      highWaterSeq: 5,
+    });
+    const legacyPolicyCheckpoint = {
+      ...current,
+      source: {
+        ...current.source,
+        policyVersion: 'maka.compactable_runtime_event_projection.v1',
+      },
+    };
+    await runStore.appendEvent('session-source', 'run-source', {
+      type: 'history_compact_checkpoint_recorded',
+      id: 'checkpoint-legacy-policy',
+      runId: 'run-source',
+      sessionId: 'session-source',
+      turnId: 'turn-1',
+      ts: 2.5,
+      data: {
+        checkpointId: legacyPolicyCheckpoint.checkpointId,
+        highWaterName: legacyPolicyCheckpoint.highWaterName,
+        highWaterSeq: legacyPolicyCheckpoint.highWaterSeq,
+        boundaryKind: 'historyCompact',
+        checkpoint: legacyPolicyCheckpoint,
+      },
+    });
+    const source = await new RuntimeReadModel({
+      runStore,
+      runtimeEventStore,
+    }).getSessionView('session-source');
+    let sequence = 0;
+
+    await cloneConversationRuntimeLedger({
+      plan: await prepareTestCopyPlan(source, source.messages, runStore, runtimeEventStore),
+      copiedMessages: source.messages,
+      referenceMap: {
+        mode: 'exact',
+        linkedChildren: { mode: 'reject' },
+        sourceSessionId: 'session-source',
+        targetSessionId: 'session-target',
+        artifactIds: new Map(),
+        relativePaths: new Map(),
+      },
+      runStore,
+      runtimeEventStore,
+      newId: () => `target-${++sequence}`,
+    });
+
+    const targetRuns = await runStore.listSessionRuns('session-target');
+    assert.ok(targetRuns.length > 0);
+    const targetOperationalEvents = (
+      await Promise.all(targetRuns.map((run) => runStore.readEvents('session-target', run.runId)))
+    ).flat();
+    assert.equal(
+      targetOperationalEvents.some((event) => event.type === 'history_compact_checkpoint_recorded'),
+      false,
+    );
+    const targetEvents = (
+      await Promise.all(
+        targetRuns.map((run) => runtimeEventStore.readRuntimeEvents('session-target', run.runId)),
+      )
+    ).flat();
+    assert.equal(targetEvents.length, sourceEvents.length);
+  } finally {
+    await rm(root, { recursive: true, force: true });
+  }
+});
+
 test('conversation copy rebuilds a resumed child checkpoint over its child run chain', async () => {
   const root = await mkdtemp(join(tmpdir(), 'maka-conversation-child-checkpoint-copy-'));
   try {
diff --git a/packages/runtime/src/__tests__/effective-history-compaction.test.ts b/packages/runtime/src/__tests__/effective-history-compaction.test.ts
new file mode 100644
index 0000000000..3c09d826d1
--- /dev/null
+++ b/packages/runtime/src/__tests__/effective-history-compaction.test.ts
@@ -0,0 +1,195 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+/**
+ * Budgeting, summarization, and checkpoint source digests must all read the
+ * EFFECTIVE model history — the durable Tool Result projection committed at T2
+ * — and never the raw execution fact it replaced (apache/maka#4283, PR 2).
+ */
+
+import assert from 'node:assert/strict';
+import { describe, test } from 'node:test';
+import type { RuntimeEvent } from '@maka/core/runtime-event';
+import type { DurableToolResultProjection } from '@maka/core/durable-tool-result-projection';
+import { estimateRuntimeEventsTokens } from '../model-history.js';
+import {
+  buildHistoryCompactCheckpoint,
+  matchHistoryCompactCheckpointPrefix,
+  validateHistoryCompactCheckpointShape,
+} from '../history-compact-checkpoint.js';
+import {
+  buildLlmHistorySummarizer,
+  type AiSdkGenerateTextLike,
+} from '../history-compact-summarizer.js';
+
+const RAW_SECRET = 'RAW-EXECUTION-EVIDENCE-'.repeat(200);
+const PROJECTED = 'bounded model-visible result';
+
+const STRUCTURED_SUMMARY = [
+  '## Goal',
+  'X',
+  '',
+  '## Progress',
+  '- done',
+  '',
+  '## Next Steps',
+  '1. continue',
+  '',
+  '## Critical Context',
+  '- (none)',
+].join('\n');
+
+describe('effective model history feeds budgeting and compaction', () => {
+  test('budgeting sizes the durable projection, not the raw execution fact', () => {
+    const projected = toolResultEvent('evt-3', RAW_SECRET, textProjection(PROJECTED));
+    const raw = toolResultEvent('evt-3', RAW_SECRET);
+
+    // Same raw result, but only the un-projected legacy event is sized by it.
+    assert.equal(estimateRuntimeEventsTokens([projected], 1), 'Bash'.length + PROJECTED.length);
+    assert.ok(estimateRuntimeEventsTokens([raw], 1) > RAW_SECRET.length);
+  });
+
+  test('summarization cannot read raw output the projection replaced', async () => {
+    let seen: Parameters<AiSdkGenerateTextLike>[0] | undefined;
+    const summarize = buildLlmHistorySummarizer({
+      resolveModel: () => 'fake-model',
+      generateText: async (options) => {
+        seen = options;
+        return { text: STRUCTURED_SUMMARY };
+      },
+    });
+
+    await summarize({
+      sessionId: 'session-1',
+      turnId: 'turn-1',
+      source: {
+        foldedRuntimeEvents: [
+          userEvent('evt-1', 'run it'),
+          toolCallEvent('evt-2'),
+          toolResultEvent('evt-3', RAW_SECRET, textProjection(PROJECTED)),
+        ],
+      },
+    });
+
+    const serialized = JSON.stringify(seen?.messages ?? []);
+    assert.ok(serialized.includes(PROJECTED));
+    assert.equal(serialized.includes('RAW-EXECUTION-EVIDENCE-'), false);
+  });
+
+  test('the source digest ignores raw evidence but tracks the effective projection', () => {
+    const covered = [
+      userEvent('evt-1', 'run it'),
+      toolCallEvent('evt-2'),
+      toolResultEvent('evt-3', RAW_SECRET, textProjection(PROJECTED)),
+    ];
+    const checkpoint = buildHistoryCompactCheckpoint({
+      sessionId: 'session-1',
+      coveredRuntimeEvents: covered,
+      summary: STRUCTURED_SUMMARY,
+      charsPerToken: 1,
+    });
+
+    // Raw evidence rewritten under an unchanged projection: still the same
+    // folded model history, so the checkpoint keeps replaying.
+    const rawRewritten = [
+      ...covered.slice(0, 2),
+      toolResultEvent('evt-3', 'a different raw fact', textProjection(PROJECTED)),
+    ];
+    assert.equal(matchHistoryCompactCheckpointPrefix(checkpoint, rawRewritten).reason, undefined);
+
+    // The projection itself replaced: the folded content is no longer what
+    // this checkpoint covered, so it must not replay over it.
+    const projectionReplaced = [
+      ...covered.slice(0, 2),
+      toolResultEvent('evt-3', RAW_SECRET, textProjection('[archived]')),
+    ];
+    assert.equal(
+      matchHistoryCompactCheckpointPrefix(checkpoint, projectionReplaced).reason,
+      'source_hash_mismatch',
+    );
+  });
+
+  test('a checkpoint minted under the raw-source policy no longer validates', () => {
+    const checkpoint = buildHistoryCompactCheckpoint({
+      sessionId: 'session-1',
+      coveredRuntimeEvents: [userEvent('evt-1', 'run it')],
+      summary: STRUCTURED_SUMMARY,
+      charsPerToken: 1,
+    });
+    const legacy = {
+      ...checkpoint,
+      source: {
+        ...checkpoint.source,
+        policyVersion: 'maka.compactable_runtime_event_projection.v1',
+      },
+    } as unknown as typeof checkpoint;
+
+    assert.equal(validateHistoryCompactCheckpointShape(checkpoint, 'session-1'), true);
+    assert.equal(validateHistoryCompactCheckpointShape(legacy, 'session-1'), false);
+  });
+});
+
+function textProjection(text: string): DurableToolResultProjection {
+  return { version: 1, kind: 'text', text };
+}
+
+function userEvent(id: string, text: string): RuntimeEvent {
+  return {
+    id,
+    invocationId: 'invocation-1',
+    sessionId: 'session-1',
+    runId: 'run-1',
+    turnId: 'turn-1',
+    ts: 1,
+    partial: false,
+    role: 'user',
+    author: 'user',
+    status: 'completed',
+    modelVisibility: 'visible',
+    content: { kind: 'text', text },
+  };
+}
+
+function toolCallEvent(id: string): RuntimeEvent {
+  return {
+    ...userEvent(id, ''),
+    role: 'model',
+    author: 'agent',
+    content: { kind: 'function_call', id: 'tool-call', name: 'Bash', args: {} },
+  };
+}
+
+function toolResultEvent(
+  id: string,
+  result: string,
+  modelProjection?: DurableToolResultProjection,
+): RuntimeEvent {
+  return {
+    ...userEvent(id, ''),
+    role: 'tool',
+    author: 'tool',
+    content: {
+      kind: 'function_response',
+      id: 'tool-call',
+      name: 'Bash',
+      result,
+      ...(modelProjection ? { modelProjection } : {}),
+    },
+  };
+}
diff --git a/packages/runtime/src/context-budget-helpers.ts b/packages/runtime/src/context-budget-helpers.ts
index 920114f6ea..b66e78aa1b 100644
--- a/packages/runtime/src/context-budget-helpers.ts
+++ b/packages/runtime/src/context-budget-helpers.ts
@@ -26,7 +26,10 @@ import type { RuntimeEvent } from '@maka/core/runtime-event';
  * modules can reuse them without a reverse import into `context-budget.ts`.
  *
  * These are intentionally dependency-free (only `node:crypto` and the
- * `RuntimeEvent` type); no domain policy types live here.
+ * `RuntimeEvent` type); no domain policy types live here. Sizing a
+ * RuntimeEvent is NOT such a helper — it must read the effective model
+ * projection — so `estimateRuntimeEventChars`/`estimateRuntimeEventsTokens`/
+ * `groupEventsByTurn` live with the reducer in `model-history.ts`.
  */
 
 export function estimateTokens(chars: number, charsPerToken = 4): number {
@@ -43,66 +46,10 @@ export function stableJsonLength(value: unknown): number {
   }
 }
 
-export function estimateRuntimeEventChars(event: RuntimeEvent): number {
-  let total = 0;
-  const content = event.content;
-  if (content?.kind === 'text' || content?.kind === 'thinking') total += content.text.length;
-  else if (content?.kind === 'function_call')
-    total += content.name.length + stableJsonLength(content.args);
-  else if (content?.kind === 'function_response')
-    total +=
-      content.name.length +
-      stableJsonLength(
-        content.providerExecuted && content.providerOutput !== undefined
-          ? content.providerOutput
-          : content.result,
-      );
-  else if (content?.kind === 'error') total += content.message.length;
-  return total;
-}
-
-export function estimateRuntimeEventsTokens(
-  events: readonly RuntimeEvent[],
-  charsPerToken = 4,
-): number {
-  const chars = events.reduce(
-    (total, event) =>
-      event.modelVisibility === 'hidden' ? total : total + estimateRuntimeEventChars(event),
-    0,
-  );
-  return estimateTokens(chars, charsPerToken);
-}
-
 export function turnKey(event: RuntimeEvent): string {
   return event.turnId || '<unknown-turn>';
 }
 
-export function groupEventsByTurn(
-  events: readonly RuntimeEvent[],
-  charsPerToken: number,
-): Array<{
-  turnId: string;
-  estimatedTokens: number;
-  events: RuntimeEvent[];
-}> {
-  const order: string[] = [];
-  const byTurn = new Map<string, RuntimeEvent[]>();
-  for (const event of events) {
-    const key = turnKey(event);
-    const group = byTurn.get(key);
-    if (group) group.push(event);
-    else {
-      order.push(key);
-      byTurn.set(key, [event]);
-    }
-  }
-  return order.map((turnId) => ({
-    turnId,
-    events: byTurn.get(turnId) ?? [],
-    estimatedTokens: estimateRuntimeEventsTokens(byTurn.get(turnId) ?? [], charsPerToken),
-  }));
-}
-
 export function uniqueSorted(values: readonly string[]): string[] {
   return [...new Set(values.filter((value) => value.length > 0))].sort();
 }
diff --git a/packages/runtime/src/context-budget.ts b/packages/runtime/src/context-budget.ts
index a38ad111c0..2e133ea7f6 100644
--- a/packages/runtime/src/context-budget.ts
+++ b/packages/runtime/src/context-budget.ts
@@ -17,15 +17,13 @@
  * under the License.
  */
 
-import {
-  estimateTokens,
-  estimateRuntimeEventsTokens,
-  stableJsonLength,
-} from './context-budget-helpers.js';
+import { estimateTokens, stableJsonLength } from './context-budget-helpers.js';
+import { estimateRuntimeEventsTokens } from './model-history.js';
 
 // Public re-export surface for @maka/runtime consumers. Explicit list keeps
 // the ./context-budget subpath from leaking leaf-internal collaboration symbols.
-export { estimateRuntimeEventsTokens, estimateTokens } from './context-budget-helpers.js';
+export { estimateTokens } from './context-budget-helpers.js';
+export { estimateRuntimeEventsTokens } from './model-history.js';
 export {
   ARCHIVED_TOOL_RESULT_PLACEHOLDER_KIND,
   ARCHIVED_TOOL_RESULT_REWRITE_VERSION,
diff --git a/packages/runtime/src/conversation-copy.ts b/packages/runtime/src/conversation-copy.ts
index 41d3c72618..cf2ab9d01b 100644
--- a/packages/runtime/src/conversation-copy.ts
+++ b/packages/runtime/src/conversation-copy.ts
@@ -738,17 +738,18 @@ function cloneAgentRunEvent(
     );
   } else if (event.type === 'history_compact_checkpoint_recorded') {
     const sourceCheckpoint = event.data?.checkpoint;
-    if (!validateHistoryCompactCheckpointShape(sourceCheckpoint, event.sessionId)) {
-      throw new Error(`Cannot copy invalid history compact checkpoint ${event.id}`);
-    }
     // Conversation copies carry the canonical raw RuntimeEvents and can create
-    // a fresh checkpoint on demand. Do not export opaque provider state into a
-    // new session or degrade it into user-visible placeholder text.
+    // a fresh checkpoint on demand, so a checkpoint this Runtime can no longer
+    // hold to its own contract is DROPPED, never fatal: the copy is complete
+    // without it. That covers opaque provider state (do not export it into a
+    // new session or degrade it into user-visible placeholder text), a
+    // superseded source policy, and a prefix that no longer matches. A ledger
+    // keeps every checkpoint it ever recorded, so a session that compacted
+    // under an older policy would otherwise be permanently uncopyable.
+    if (!validateHistoryCompactCheckpointShape(sourceCheckpoint, event.sessionId)) return null;
     if (sourceCheckpoint.version === 3) return null;
     const match = matchHistoryCompactCheckpointPrefix(sourceCheckpoint, sourceCompactableEvents);
-    if (match.reason) {
-      throw new Error(`Cannot copy unmatched history compact checkpoint ${event.id}`);
-    }
+    if (match.reason) return null;
     // Copy is an admission seam for the sectioned summary contract: a marked
     // checkpoint whose summary no longer satisfies the COMPLETE predicate —
     // including the size floor, re-runnable here because the matched covered
diff --git a/packages/runtime/src/durable-tool-result-projection.ts b/packages/runtime/src/durable-tool-result-projection.ts
index f81e4d5f82..ad58bbebd1 100644
--- a/packages/runtime/src/durable-tool-result-projection.ts
+++ b/packages/runtime/src/durable-tool-result-projection.ts
@@ -163,6 +163,24 @@ export function durableProjectionToToolResultOutput(
   }
 }
 
+/**
+ * The one pure decision of WHICH source a replayed Tool Result materializes
+ * from: a durable projection wins, and only a response that has none (legacy
+ * or provider-native) falls back to its raw output. Replay, summarization, and
+ * provider-native compaction share it so no second path can re-read what the
+ * projection removed. The image-materializing replay path in the ai-sdk
+ * backend applies the same choice under a request budget.
+ */
+export function effectiveReplayToolResultOutput(item: {
+  modelProjection?: DurableToolResultProjection;
+  output: unknown;
+  isError: boolean;
+}): ToolResultOutput {
+  return item.modelProjection
+    ? durableProjectionToToolResultOutput(item.modelProjection)
+    : toolResultOutput(item.output, item.isError);
+}
+
 export function rewriteDurableToolResultProjectionArtifactRefs(
   projection: DurableToolResultProjection,
   rewrite: (ref: DurableProjectionArtifactRef) => DurableProjectionArtifactRef,
diff --git a/packages/runtime/src/execution-inspect.ts b/packages/runtime/src/execution-inspect.ts
index 63b794928c..5647c313e5 100644
--- a/packages/runtime/src/execution-inspect.ts
+++ b/packages/runtime/src/execution-inspect.ts
@@ -42,6 +42,7 @@ import {
   type SessionAgentRunInspectReader,
 } from './agent-run-inspect.js';
 import {
+  isSupersededHistoryCompactCheckpoint,
   validateHistoryCompactCheckpointShape,
   type HistoryCompactCheckpoint,
 } from './history-compact-checkpoint.js';
@@ -287,16 +288,26 @@ function inspectCompactionCheckpoints(
     if (event.type !== 'history_compact_checkpoint_recorded') continue;
     const checkpoint = event.data?.checkpoint;
     if (!validateHistoryCompactCheckpointShape(checkpoint, header.sessionId)) {
+      // A checkpoint recorded under an older source policy is expected history,
+      // not corruption: the ledger keeps every checkpoint it ever wrote, and
+      // every consumer fails open on it. Reporting it as an error would drown
+      // out the records that really are damaged.
+      const superseded = isSupersededHistoryCompactCheckpoint(checkpoint);
       diagnostics.push(
         diagnostic(
           header,
-          'compaction_checkpoint_invalid',
-          'error',
-          'AgentRun contains an invalid durable Compaction checkpoint record.',
+          superseded ? 'compaction_checkpoint_superseded' : 'compaction_checkpoint_invalid',
+          superseded ? 'info' : 'error',
+          superseded
+            ? 'AgentRun contains a durable Compaction checkpoint from a superseded source policy; it is ignored and re-created on demand.'
+            : 'AgentRun contains an invalid durable Compaction checkpoint record.',
           event.id,
         ),
       );
-      checkpoints.push({ eventId: event.id, validation: 'invalid' });
+      checkpoints.push({
+        eventId: event.id,
+        validation: superseded ? 'superseded' : 'invalid',
+      });
       continue;
     }
     const valid = checkpoint as HistoryCompactCheckpoint;
diff --git a/packages/runtime/src/history-compact-checkpoint.ts b/packages/runtime/src/history-compact-checkpoint.ts
index d47463bfb3..0234f278d0 100644
--- a/packages/runtime/src/history-compact-checkpoint.ts
+++ b/packages/runtime/src/history-compact-checkpoint.ts
@@ -29,8 +29,13 @@ import {
   type SectionedSummaryFormat,
 } from './history-compact-summary-validation.js';
 
+// v2: coverage and source digest are taken over EFFECTIVE model history (the
+// durable Tool Result projection), not raw RuntimeEvent evidence. A v1
+// checkpoint's digest was computed over a different source, so it fails the
+// shape check and its session re-summarizes rather than replaying a
+// coverage claim this policy never made.
 export const HISTORY_COMPACT_SOURCE_POLICY_VERSION =
-  'maka.compactable_runtime_event_projection.v1' as const;
+  'maka.compactable_runtime_event_projection.v2' as const;
 export interface HistoryCompactCheckpointSource {
   schemaVersion: 1;
   kind: 'runtime_event_projection';
@@ -505,6 +510,23 @@ export function validateHistoryCompactCheckpointShape(
 }
 
 /** Accept forward progress, or a compare-and-swap rewrite of the exact same source coverage. */
+/**
+ * A recorded checkpoint that this Runtime no longer holds to its own contract
+ * because it was minted under an older source policy — not a corrupt record.
+ * Every consumer already fails open on it (compaction re-summarizes, copy
+ * drops it); diagnostics use this to say so instead of crying corruption.
+ */
+export function isSupersededHistoryCompactCheckpoint(value: unknown): boolean {
+  if (!value || typeof value !== 'object') return false;
+  const checkpoint = value as Partial<HistoryCompactCheckpoint>;
+  const policyVersion = checkpoint.source?.policyVersion as unknown;
+  return (
+    checkpoint.kind === 'maka.history_compact_checkpoint' &&
+    typeof policyVersion === 'string' &&
+    policyVersion !== HISTORY_COMPACT_SOURCE_POLICY_VERSION
+  );
+}
+
 export function canReplaceHistoryCompactCheckpoint(
   current: HistoryCompactCheckpoint | undefined,
   candidate: HistoryCompactCheckpoint,
@@ -725,10 +747,18 @@ function sameHistoryCompactSourceCoverage(
   );
 }
 
+/**
+ * A checkpoint replaces a contiguous prefix of EFFECTIVE model history, so its
+ * source digest must pin exactly that: raw execution evidence a durable
+ * projection already bounded or redacted is not part of what was folded, and
+ * changing it must not invalidate a valid checkpoint. Conversely, replacing a
+ * response's effective projection does change the digest, so a checkpoint can
+ * never be replayed over content it never covered.
+ */
 function historyCompactSourceDigest(events: readonly RuntimeEvent[]): string {
   const hash = createHash('sha256');
   for (const event of events) {
-    const serialized = stableStringify(event);
+    const serialized = stableStringify(effectiveDigestEvent(event));
     hash.update(String(Buffer.byteLength(serialized, 'utf8')));
     hash.update(':');
     hash.update(serialized);
@@ -737,6 +767,28 @@ function historyCompactSourceDigest(events: readonly RuntimeEvent[]): string {
   return `sha256:${hash.digest('hex')}`;
 }
 
+/**
+ * Selects the persisted field that carries a response's model-visible content,
+ * dropping the others. This is a structural choice over durable data only —
+ * never a code-derived materialization — so the digest of an already-persisted
+ * checkpoint cannot drift when a projector, a codec bound, or a placeholder
+ * string changes. A response with no durable projection (legacy, or
+ * provider-native) keeps the raw field its effective content is still derived
+ * from, which is what the pre-projection digest already hashed.
+ */
+function effectiveDigestEvent(event: RuntimeEvent): unknown {
+  const content = event.content;
+  if (content?.kind !== 'function_response') return event;
+  const { result, providerOutput, modelProjection, ...identity } = content;
+  if (content.providerExecuted && providerOutput !== undefined) {
+    return { ...event, content: { ...identity, providerOutput } };
+  }
+  if (modelProjection !== undefined) {
+    return { ...event, content: { ...identity, modelProjection } };
+  }
+  return { ...event, content: { ...identity, result } };
+}
+
 function sha256(value: string): string {
   return createHash('sha256').update(value).digest('hex');
 }
diff --git a/packages/runtime/src/history-compact-summarizer.ts b/packages/runtime/src/history-compact-summarizer.ts
index f952b600df..bcfad2552c 100644
--- a/packages/runtime/src/history-compact-summarizer.ts
+++ b/packages/runtime/src/history-compact-summarizer.ts
@@ -23,7 +23,7 @@ import {
   findCheckpointSummaryDefect,
   SUMMARY_FORMAT_TEMPLATE,
 } from './history-compact-summary-validation.js';
-import { toolResultOutput } from './tool-result-output.js';
+import { effectiveReplayToolResultOutput } from './durable-tool-result-projection.js';
 import type { HistoryCompactSummaryInput } from './ai-sdk-compaction-contract.js';
 import {
   HistoryCompactSummarizerError,
@@ -297,7 +297,7 @@ export function replayPlanItemsToModelMessages(items: ReplayPlanItems): ModelMes
             type: 'tool-result',
             toolCallId: item.toolCallId,
             toolName: item.toolName,
-            output: toolResultOutput(item.output, item.isError),
+            output: effectiveReplayToolResultOutput(item),
           },
         ],
       };
diff --git a/packages/runtime/src/history-compact-summary-validation.ts b/packages/runtime/src/history-compact-summary-validation.ts
index 6ed2f5da25..eec8172a5a 100644
--- a/packages/runtime/src/history-compact-summary-validation.ts
+++ b/packages/runtime/src/history-compact-summary-validation.ts
@@ -19,7 +19,8 @@
 
 import type { RuntimeEvent } from '@maka/core/runtime-event';
 import type { ContextBudgetExhaustedDetail } from '@maka/core/events';
-import { estimateRuntimeEventsTokens, estimateTokens } from './context-budget-helpers.js';
+import { estimateTokens } from './context-budget-helpers.js';
+import { estimateRuntimeEventsTokens } from './model-history.js';
 
 // The single authority on what a history-compact checkpoint summary must look
 // like (#3029). The summarization prompt is built from the same constants and
diff --git a/packages/runtime/src/history-compaction.ts b/packages/runtime/src/history-compaction.ts
index ee50498ee5..1ee5fc1fb0 100644
--- a/packages/runtime/src/history-compaction.ts
+++ b/packages/runtime/src/history-compaction.ts
@@ -19,11 +19,8 @@
 
 import type { RuntimeEvent } from '@maka/core/runtime-event';
 import type { ContextBudgetDiagnostic } from '@maka/core/usage-stats/types';
-import {
-  estimateRuntimeEventChars,
-  estimateRuntimeEventsTokens,
-  finitePositive,
-} from './context-budget-helpers.js';
+import { finitePositive } from './context-budget-helpers.js';
+import { estimateRuntimeEventChars, estimateRuntimeEventsTokens } from './model-history.js';
 import { compactionDecisionDiagnosticPatch } from './compaction-boundary.js';
 import {
   HistoryCompactSummarizerError,
diff --git a/packages/runtime/src/model-history.ts b/packages/runtime/src/model-history.ts
index 1a74cde6c6..9d1be51abf 100644
--- a/packages/runtime/src/model-history.ts
+++ b/packages/runtime/src/model-history.ts
@@ -63,12 +63,124 @@ import {
 } from '@maka/core/runtime-event';
 import { formatAttachmentResourceRef } from '@maka/core/attachments';
 import type { AttachmentRef, QuoteRef } from '@maka/core/events';
-import type { ModelMessage, UserContent, UserModelMessage } from './model-protocol.js';
-import { decodeEffectiveToolResultProjection } from './durable-tool-result-projection.js';
+import type {
+  ModelMessage,
+  ToolResultOutput,
+  UserContent,
+  UserModelMessage,
+} from './model-protocol.js';
+import {
+  decodeEffectiveToolResultProjection,
+  durableProjectionToToolResultOutput,
+} from './durable-tool-result-projection.js';
+import { estimateTokens, stableJsonLength, turnKey } from './context-budget-helpers.js';
 import type { DurableToolResultProjection } from '@maka/core/durable-tool-result-projection';
 
 export const PROVIDER_REPLAY_PROJECTION_VERSION = 1;
 
+// ============================================================================
+// Effective model-history sizing
+// ============================================================================
+
+/**
+ * Model-visible character count of a `function_response`'s EFFECTIVE value —
+ * the durable projection when the response has one, never the raw execution
+ * fact the projection already bounded or redacted.
+ *
+ * Memoized on the content object because a legacy response (no durable
+ * projection) is re-derived through the whole compatibility codec, and one
+ * request measures the same events several times: budget verdicts, the
+ * compactable-content filter, and checkpoint prefix matching all walk the
+ * history. Content is immutable once committed, so identity is a sound key.
+ */
+const effectiveToolResultChars = new WeakMap<object, number>();
+
+/** Model-visible character count of one materialized Tool Result output. */
+function estimateToolResultOutputChars(output: ToolResultOutput): number {
+  switch (output.type) {
+    case 'text':
+    case 'error-text':
+      return output.value.length;
+    case 'json':
+    case 'error-json':
+      return stableJsonLength(output.value);
+    case 'content':
+      return output.value.reduce(
+        (total, part) => total + (part.type === 'text' ? part.text.length : 0),
+        0,
+      );
+    case 'execution-denied':
+      return output.reason?.length ?? 0;
+  }
+}
+
+export function estimateEffectiveToolResultChars(
+  content: Extract<RuntimeEventContent, { kind: 'function_response' }>,
+  sessionId: string,
+): number {
+  const memoized = effectiveToolResultChars.get(content);
+  if (memoized !== undefined) return memoized;
+  const effective = decodeEffectiveToolResultProjection(content, sessionId);
+  const chars =
+    effective.kind === 'projection'
+      ? estimateToolResultOutputChars(durableProjectionToToolResultOutput(effective.projection))
+      : effective.kind === 'invalid_legacy'
+        ? effective.message.length
+        : stableJsonLength(effective.output);
+  effectiveToolResultChars.set(content, chars);
+  return chars;
+}
+
+export function estimateRuntimeEventChars(event: RuntimeEvent): number {
+  let total = 0;
+  const content = event.content;
+  if (content?.kind === 'text' || content?.kind === 'thinking') total += content.text.length;
+  else if (content?.kind === 'function_call')
+    total += content.name.length + stableJsonLength(content.args);
+  else if (content?.kind === 'function_response')
+    total += content.name.length + estimateEffectiveToolResultChars(content, event.sessionId);
+  else if (content?.kind === 'error') total += content.message.length;
+  return total;
+}
+
+export function estimateRuntimeEventsTokens(
+  events: readonly RuntimeEvent[],
+  charsPerToken = 4,
+): number {
+  const chars = events.reduce(
+    (total, event) =>
+      event.modelVisibility === 'hidden' ? total : total + estimateRuntimeEventChars(event),
+    0,
+  );
+  return estimateTokens(chars, charsPerToken);
+}
+
+export function groupEventsByTurn(
+  events: readonly RuntimeEvent[],
+  charsPerToken: number,
+): Array<{
+  turnId: string;
+  estimatedTokens: number;
+  events: RuntimeEvent[];
+}> {
+  const order: string[] = [];
+  const byTurn = new Map<string, RuntimeEvent[]>();
+  for (const event of events) {
+    const key = turnKey(event);
+    const group = byTurn.get(key);
+    if (group) group.push(event);
+    else {
+      order.push(key);
+      byTurn.set(key, [event]);
+    }
+  }
+  return order.map((turnId) => ({
+    turnId,
+    events: byTurn.get(turnId) ?? [],
+    estimatedTokens: estimateRuntimeEventsTokens(byTurn.get(turnId) ?? [], charsPerToken),
+  }));
+}
+
 // ============================================================================
 // Output type
 // ============================================================================
diff --git a/packages/runtime/src/openai-codex-history-compactor.ts b/packages/runtime/src/openai-codex-history-compactor.ts
index b147a24fb0..9d9e843dec 100644
--- a/packages/runtime/src/openai-codex-history-compactor.ts
+++ b/packages/runtime/src/openai-codex-history-compactor.ts
@@ -35,7 +35,7 @@ import {
   type RuntimeEventModelReplayItem,
 } from './model-history.js';
 import { withProviderStreamTracking } from './provider-request-telemetry.js';
-import { toolResultOutput } from './tool-result-output.js';
+import { effectiveReplayToolResultOutput } from './durable-tool-result-projection.js';
 import { providerFailureDiagnostic } from './provider-error-classification.js';
 
 export { fitHistoryCompactMessages as fitOpenAiCodexCompactionMessages } from './history-compact-input-fit.js';
@@ -232,7 +232,7 @@ export function openAiCodexCompactionMessages(events: readonly RuntimeEvent[]):
           type: 'tool-result',
           toolCallId: result.toolCallId,
           toolName: result.toolName,
-          output: toolResultOutput(result.output, result.isError),
+          output: effectiveReplayToolResultOutput(result),
         },
       ],
     });
diff --git a/packages/runtime/src/session-recap.ts b/packages/runtime/src/session-recap.ts
index 1333fbf159..4c8549cb40 100644
--- a/packages/runtime/src/session-recap.ts
+++ b/packages/runtime/src/session-recap.ts
@@ -20,7 +20,8 @@
 import type { RuntimeEvent } from '@maka/core/runtime-event';
 import type { RuntimeExecutionConnection } from '@maka/core/llm-connections';
 import { resolveSelectedModelContextWindow } from './context-budget-policy.js';
-import { groupEventsByTurn, stableJsonLength } from './context-budget-helpers.js';
+import { stableJsonLength } from './context-budget-helpers.js';
+import { groupEventsByTurn } from './model-history.js';
 import { HistoryCompactSummarizerError } from './history-compact-error.js';
 import { fitHistoryCompactMessages } from './history-compact-input-fit.js';
 import { replayPlanItemsToModelMessages } from './history-compact-summarizer.js';


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org