From 3d72d21dbcde1730827481189a1c12a8b9b58306 Mon Sep 17 00:00:00 2001 From: AstroHan Date: Tue, 1 Sep 2026 01:31:18 +0800 Subject: [PATCH 01/19] feat(runtime): compare semantic request prefixes Generated-by: Codex --- .../semantic-prefix-continuity.test.ts | 91 ++++++++++++++ .../runtime/src/semantic-prefix-continuity.ts | 114 ++++++++++++++++++ 2 files changed, 205 insertions(+) create mode 100644 packages/runtime/src/__tests__/semantic-prefix-continuity.test.ts create mode 100644 packages/runtime/src/semantic-prefix-continuity.ts diff --git a/packages/runtime/src/__tests__/semantic-prefix-continuity.test.ts b/packages/runtime/src/__tests__/semantic-prefix-continuity.test.ts new file mode 100644 index 0000000000..965f3bbd15 --- /dev/null +++ b/packages/runtime/src/__tests__/semantic-prefix-continuity.test.ts @@ -0,0 +1,91 @@ +/* + * 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. + */ + +import assert from 'node:assert/strict'; +import { test } from 'node:test'; +import type { + PreparedRequestObservation, + PreparedRequestObservationSegment, +} from '@maka/core/model-call-attempt'; +import { deriveSemanticPrefixContinuity } from '../semantic-prefix-continuity.js'; + +test('keeps every earlier cacheable segment when the current request only appends', () => { + const previous = observation([message(0, 'system'), message(1, 'user-1')]); + const current = observation([ + message(0, 'system'), + message(1, 'user-1'), + message(2, 'assistant-1'), + ]); + + assert.deepEqual(deriveSemanticPrefixContinuity(current, previous), { + status: 'preserved', + previousSegmentCount: 2, + preservedSegmentCount: 2, + }); +}); + +test('does not treat opaque digests as evidence of divergence', () => { + const previous = observation([{ ...message(0, 'redacted-a'), comparison: 'opaque' }]); + const current = observation([{ ...message(0, 'redacted-b'), comparison: 'opaque' }]); + + assert.deepEqual(deriveSemanticPrefixContinuity(current, previous), { + status: 'unknown', + previousSegmentCount: 1, + preservedSegmentCount: 0, + }); +}); + +test('reports the first changed earlier segment', () => { + const previous = observation([message(0, 'system'), message(1, 'user-1')]); + const current = observation([message(0, 'system'), message(1, 'edited-user-1')]); + + assert.deepEqual(deriveSemanticPrefixContinuity(current, previous), { + status: 'diverged', + previousSegmentCount: 2, + preservedSegmentCount: 1, + firstDivergentSegment: { kind: 'message', index: 1 }, + }); +}); + +test('reports the first removed earlier segment', () => { + const previous = observation([message(0, 'system'), message(1, 'user-1')]); + const current = observation([message(0, 'system')]); + + assert.deepEqual(deriveSemanticPrefixContinuity(current, previous), { + status: 'diverged', + previousSegmentCount: 2, + preservedSegmentCount: 1, + firstDivergentSegment: { kind: 'message', index: 1 }, + }); +}); + +function observation(segments: PreparedRequestObservationSegment[]): PreparedRequestObservation { + return { schemaVersion: 1, digest: 'request', bytes: 1, segments }; +} + +function message(index: number, digest: string): PreparedRequestObservationSegment { + return { + kind: 'message', + index, + cacheable: true, + comparison: 'exact', + digest, + bytes: 1, + }; +} diff --git a/packages/runtime/src/semantic-prefix-continuity.ts b/packages/runtime/src/semantic-prefix-continuity.ts new file mode 100644 index 0000000000..f30248aef1 --- /dev/null +++ b/packages/runtime/src/semantic-prefix-continuity.ts @@ -0,0 +1,114 @@ +/* + * 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. + */ + +import type { + PreparedRequestObservation, + PreparedRequestObservationSegment, +} from '@maka/core/model-call-attempt'; + +export type SemanticPrefixContinuity = + | { + status: 'no_predecessor' | 'unavailable'; + previousSegmentCount: 0; + preservedSegmentCount: 0; + } + | { + status: 'preserved' | 'unknown'; + previousSegmentCount: number; + preservedSegmentCount: number; + } + | { + status: 'diverged'; + previousSegmentCount: number; + preservedSegmentCount: number; + firstDivergentSegment: SemanticPrefixSegmentRef; + }; + +export type SemanticPrefixSegmentRef = Pick< + PreparedRequestObservationSegment, + 'kind' | 'index' | 'role' | 'label' +>; + +export function deriveSemanticPrefixContinuity( + currentObservation: PreparedRequestObservation, + previousObservation: PreparedRequestObservation, +): SemanticPrefixContinuity { + const current = currentObservation.segments.filter((segment) => segment.cacheable); + const previous = previousObservation.segments.filter((segment) => segment.cacheable); + const previousSegmentCount = representedCount(previous); + + for (let index = 0; index < previous.length; index += 1) { + const before = previous[index]!; + const after = current[index]; + if (!after || !sameIdentity(before, after)) { + return { + status: 'diverged', + previousSegmentCount, + preservedSegmentCount: representedCount(previous.slice(0, index)), + firstDivergentSegment: segmentRef(after ?? before), + }; + } + if (before.comparison === 'opaque' || after.comparison === 'opaque') { + return { + status: 'unknown', + previousSegmentCount, + preservedSegmentCount: representedCount(previous.slice(0, index)), + }; + } + if (before.digest !== after.digest) { + return { + status: 'diverged', + previousSegmentCount, + preservedSegmentCount: representedCount(previous.slice(0, index)), + firstDivergentSegment: segmentRef(after), + }; + } + } + + return { + status: 'preserved', + previousSegmentCount, + preservedSegmentCount: previousSegmentCount, + }; +} + +function sameIdentity( + left: PreparedRequestObservationSegment, + right: PreparedRequestObservationSegment, +): boolean { + return ( + left.kind === right.kind && + left.index === right.index && + left.role === right.role && + left.label === right.label + ); +} + +function representedCount(segments: readonly PreparedRequestObservationSegment[]): number { + return segments.reduce((count, segment) => count + (segment.representedSegments ?? 1), 0); +} + +function segmentRef(segment: PreparedRequestObservationSegment): SemanticPrefixSegmentRef { + return { + kind: segment.kind, + index: segment.index, + ...(segment.role === undefined ? {} : { role: segment.role }), + ...(segment.label === undefined ? {} : { label: segment.label }), + }; +} From 50ccacb349a7026964bc0eac16dcfc5d547d1f7b Mon Sep 17 00:00:00 2001 From: AstroHan Date: Tue, 1 Sep 2026 01:40:32 +0800 Subject: [PATCH 02/19] feat(runtime): seal semantic prefix continuity Generated-by: Codex --- packages/core/src/agent-run.ts | 5 + .../__tests__/latest-context-commit.test.ts | 115 +++++++--- packages/runtime/src/agent-run.ts | 27 ++- packages/runtime/src/context-diagnostics.ts | 4 + .../runtime/src/latest-context-snapshot.ts | 18 +- .../runtime/src/semantic-prefix-continuity.ts | 207 ++++++++++++++++++ 6 files changed, 341 insertions(+), 35 deletions(-) diff --git a/packages/core/src/agent-run.ts b/packages/core/src/agent-run.ts index ac0fcedb24..ccf1506df4 100644 --- a/packages/core/src/agent-run.ts +++ b/packages/core/src/agent-run.ts @@ -795,6 +795,11 @@ export interface AgentRunStore { options?: AgentRunAppendOptions, ): Promise; readEvents(sessionId: string, runId: string): Promise; + /** Durable causal predecessor for a root Turn, when this store owns root admission. */ + readRootTurnAdmission?( + sessionId: string, + turnId: string, + ): Promise<{ runId: string; previousRootTurnId?: string | null } | undefined>; /** * `undefined` means uninitialized; `null` is an initialized empty projection. * diff --git a/packages/runtime/src/__tests__/latest-context-commit.test.ts b/packages/runtime/src/__tests__/latest-context-commit.test.ts index 5075d6cbb9..71c851a8c2 100644 --- a/packages/runtime/src/__tests__/latest-context-commit.test.ts +++ b/packages/runtime/src/__tests__/latest-context-commit.test.ts @@ -48,7 +48,7 @@ import { BackendRegistry, SessionManager } from '../session-manager.js'; import { readLatestContextDiagnostics } from '../context-diagnostics.js'; import { createTestAiSdkBackend } from './execution-boundary-test-helpers.js'; -test('a real send seals its observation into SQLite and reconstructs it after restart', async () => { +test('real consecutive sends seal a preserved prefix verdict that survives restart', async () => { // Tracker → backend → the kernel seam a backend is actually built with → // AgentRun → the storage transaction. Every layer in that list once had a // signature that compiled while dropping the row, and no test crossed all of @@ -64,30 +64,32 @@ test('a real send seals its observation into SQLite and reconstructs it after re let clock = 1_000; const now = () => (clock += 1); - backends.register('ai-sdk', (ctx) => - createTestAiSdkBackend({ - sessionId: ctx.sessionId, - header: ctx.header, - appendMessage: async () => {}, - connection: { - slug: 'mock-main', - providerType: 'anthropic', - defaultModel: 'mock-model-id', - models: [{ id: 'mock-model-id', contextWindow: 200_000 }], - }, - apiKey: 'sk-test', - modelId: 'mock-model-id', - modelFactory: () => answeringModel(), - tools: [], - // The seams the kernel hands a real backend, forwarded exactly as the - // production composition forwards them — this is the hop that broke. - ...(ctx.recordModelCallAttempt - ? { recordModelCallAttempt: ctx.recordModelCallAttempt } - : {}), - newId, - now, + backends.register('ai-sdk', { + prepare: async () => ({ + providerStateIdentity: `sha256:${'1'.repeat(64)}`, + build: (ctx) => + createTestAiSdkBackend({ + sessionId: ctx.sessionId, + header: ctx.header, + appendMessage: ctx.appendMessage ?? (async () => {}), + connection: { + slug: 'mock-main', + providerType: 'anthropic', + defaultModel: 'mock-model-id', + models: [{ id: 'mock-model-id', contextWindow: 200_000 }], + }, + apiKey: 'sk-test', + modelId: 'mock-model-id', + modelFactory: () => answeringModel(), + tools: [], + ...(ctx.recordModelCallAttempt + ? { recordModelCallAttempt: ctx.recordModelCallAttempt } + : {}), + newId, + now, + }), }), - ); + }); const manager = new SessionManager({ store: sessionStore, @@ -102,10 +104,12 @@ test('a real send seals its observation into SQLite and reconstructs it after re llmConnectionSlug: 'mock-main', permissionMode: 'bypass', }); - for await (const _event of manager.sendMessage(session.id, { - turnId: 'turn-1', - text: 'what is my context made of?', - })) { + await admitTurn(runStore, session.id, 'turn-1', 'run-1', null, now()); + for await (const _event of manager.sendMessage( + session.id, + { turnId: 'turn-1', text: 'what is my context made of?' }, + { runId: 'run-1', userMessageId: 'message-turn-1', durability: 'required' }, + )) { // Drain the turn so its run reaches the durable ledger. } @@ -133,8 +137,30 @@ test('a real send seals its observation into SQLite and reconstructs it after re diagnostics.composition?.segments.some((segment) => segment.kind === 'messages'), 'and the request describes what it was made of', ); + assert.deepEqual(diagnostics.requestPrefix, { + status: 'no_predecessor', + previousSegmentCount: 0, + preservedSegmentCount: 0, + }); assert.equal(scanned, 0, 'the row was committed by the send, not rebuilt by the read'); + await admitTurn(runStore, session.id, 'turn-2', 'run-2', 'turn-1', now()); + for await (const _event of manager.sendMessage( + session.id, + { turnId: 'turn-2', text: 'what changed?' }, + { runId: 'run-2', userMessageId: 'message-turn-2', durability: 'required' }, + )) { + // Drain the successor through the same public Runtime boundary. + } + const successor = await readLatestContextDiagnostics(runStore, session.id); + assert.equal(successor.status, 'available'); + if (successor.status !== 'available') return; + assert.equal(successor.requestPrefix.status, 'preserved'); + assert.equal( + successor.requestPrefix.preservedSegmentCount, + successor.requestPrefix.previousSegmentCount, + ); + await manager.stopSession(session.id, { source: 'stop_button' }); runStore.close?.(); @@ -151,13 +177,18 @@ test('a real send seals its observation into SQLite and reconstructs it after re }), ) ).flat(); - assert.equal(canonicalAttempts.length, 1); - const observation = canonicalAttempts[0]?.requestObservation; + assert.equal(canonicalAttempts.length, 2); + const observation = canonicalAttempts[1]?.requestObservation; assert.ok(observation); assert.ok(observation.segments.length <= PREPARED_REQUEST_OBSERVATION_MAX_SEGMENTS); assert.ok(observation.segments.length > 0); assert.ok(observation.segments.every((segment) => segment.comparison === 'exact')); + const restarted = await readLatestContextDiagnostics(reopened, session.id); + assert.equal(restarted.status, 'available'); + if (restarted.status !== 'available') return; + assert.deepEqual(restarted.requestPrefix, successor.requestPrefix); + let coldScans = 0; const cold = await readLatestContextDiagnostics( { @@ -175,7 +206,8 @@ test('a real send seals its observation into SQLite and reconstructs it after re assert.ok(coldScans > 0, 'omitting the projection reader forces a restart-safe ledger fold'); assert.equal(cold.status, 'available'); if (cold.status !== 'available') return; - assert.deepEqual(cold.composition, diagnostics.composition); + assert.deepEqual(cold.composition, successor.composition); + assert.equal(cold.requestPrefix, undefined); } finally { reopened.close?.(); } @@ -293,3 +325,24 @@ function answeringModel(): MockLanguageModelV4 { }), }); } + +async function admitTurn( + runStore: ReturnType, + sessionId: string, + turnId: string, + runId: string, + previousRootTurnId: string | null, + admittedAt: number, +): Promise { + await runStore.admitRootTurn({ + sessionId, + turnId, + proposedRunId: runId, + proposedUserMessageId: `message-${turnId}`, + execution: { kind: 'external_message' }, + previousRootTurnId, + normalizedInput: { text: turnId }, + sourceMessages: [], + admittedAt, + }); +} diff --git a/packages/runtime/src/agent-run.ts b/packages/runtime/src/agent-run.ts index cff8aaed16..6edc658ffc 100644 --- a/packages/runtime/src/agent-run.ts +++ b/packages/runtime/src/agent-run.ts @@ -91,6 +91,7 @@ import { import { DEFAULT_TOOL_MODE, isToolMode, type ToolMode } from '@maka/core/tool-mode'; import { materializeRuntimeEventTranscriptProjection } from './runtime-ledger-repair.js'; import { cloneAndFreezeRuntimeSnapshot } from './runtime-snapshot.js'; +import { deriveAttemptSemanticPrefixContinuity } from './semantic-prefix-continuity.js'; export interface AgentRunActiveSession { sessionId: string; @@ -474,7 +475,29 @@ export class AgentRun { const { attempt, latestContext } = commit; if (!this.input.runStore) return Promise.resolve(); return this.enqueueRequiredRunStoreWrite('append model call attempt', async () => { - await this.input.runStore?.appendEvent( + const runStore = this.input.runStore; + if (!runStore) return; + let projected = latestContext; + if (projected && attempt.callKind === 'main') { + const requestPrefix = await deriveAttemptSemanticPrefixContinuity({ + current: attempt, + currentProviderStateIdentity: this.providerStateIdentity, + lineage: { + ...this.lineage, + ...(this.header.conversationCopy ? { conversationCopy: true } : {}), + }, + store: runStore, + }).catch(() => ({ + status: 'unavailable' as const, + previousSegmentCount: 0 as const, + preservedSegmentCount: 0 as const, + })); + projected = { + ...projected, + snapshot: { ...projected.snapshot, requestPrefix }, + }; + } + await runStore.appendEvent( this.sessionId, this.runId, { @@ -489,7 +512,7 @@ export class AgentRun { // The latest-context projection rides this durable append rather than // racing it: one commit for the request, and derived state that cannot // survive a metering write that failed (#2323). - { durable: true, ...(latestContext ? { latestContext } : {}) }, + { durable: true, ...(projected ? { latestContext: projected } : {}) }, ); }); } diff --git a/packages/runtime/src/context-diagnostics.ts b/packages/runtime/src/context-diagnostics.ts index bc45466ab0..6023fde4c2 100644 --- a/packages/runtime/src/context-diagnostics.ts +++ b/packages/runtime/src/context-diagnostics.ts @@ -39,6 +39,7 @@ import { validateHistoryCompactCheckpointShape, type HistoryCompactCheckpoint, } from './history-compact-checkpoint.js'; +import type { SemanticPrefixContinuity } from './semantic-prefix-continuity.js'; export type ContextDiagnosticsUnavailableReason = 'no_completed_request' | 'trace_unavailable'; @@ -98,6 +99,8 @@ export type ContextDiagnostics = */ composition?: ContextDiagnosticsComposition; compaction?: ContextDiagnosticsCompaction; + /** Absent only for historical projections written before this diagnostic existed. */ + requestPrefix?: SemanticPrefixContinuity; }; export interface ContextDiagnosticsComposition { @@ -357,6 +360,7 @@ function availableFrom(snapshot: LatestContextSnapshot): ContextDiagnostics { ...(snapshot.contextWindow !== undefined ? { contextWindow: snapshot.contextWindow } : {}), ...(snapshot.composition ? { composition: snapshot.composition } : {}), ...(snapshot.compaction ? { compaction: snapshot.compaction } : {}), + ...(snapshot.requestPrefix ? { requestPrefix: snapshot.requestPrefix } : {}), }; } diff --git a/packages/runtime/src/latest-context-snapshot.ts b/packages/runtime/src/latest-context-snapshot.ts index d2f53652d6..e363cbdb80 100644 --- a/packages/runtime/src/latest-context-snapshot.ts +++ b/packages/runtime/src/latest-context-snapshot.ts @@ -29,6 +29,10 @@ import type { ContextDiagnosticsComposition, } from './context-diagnostics.js'; import { foldPromptComposition, type SizedRequestSegment } from './prompt-composition.js'; +import { + isSemanticPrefixContinuity, + type SemanticPrefixContinuity, +} from './semantic-prefix-continuity.js'; /** * One request's context, frozen by the transaction that committed it (#2323). @@ -67,6 +71,8 @@ export interface LatestContextSnapshot { composition?: ContextDiagnosticsComposition; /** The boundary that applied when this request was built, if any. */ compaction?: ContextDiagnosticsCompaction; + /** Runtime-owned conclusion; consumers must not select or compare observations. */ + requestPrefix?: SemanticPrefixContinuity; } /** @@ -129,7 +135,14 @@ export function readLatestContextSnapshot( const record = shapedRecord( event.data, ['schemaVersion', 'attemptId', 'providerId', 'modelId', 'completedAt'], - ['inputTokens', 'cacheReadInputTokens', 'contextWindow', 'composition', 'compaction'], + [ + 'inputTokens', + 'cacheReadInputTokens', + 'contextWindow', + 'composition', + 'compaction', + 'requestPrefix', + ], ); if (!record) return undefined; if ( @@ -143,7 +156,8 @@ export function readLatestContextSnapshot( (record.contextWindow !== undefined && (!isCount(record.contextWindow) || record.contextWindow === 0)) || (record.composition !== undefined && !isContextDiagnosticsComposition(record.composition)) || - (record.compaction !== undefined && !isContextDiagnosticsCompaction(record.compaction)) + (record.compaction !== undefined && !isContextDiagnosticsCompaction(record.compaction)) || + (record.requestPrefix !== undefined && !isSemanticPrefixContinuity(record.requestPrefix)) ) { return undefined; } diff --git a/packages/runtime/src/semantic-prefix-continuity.ts b/packages/runtime/src/semantic-prefix-continuity.ts index f30248aef1..1dd546b89b 100644 --- a/packages/runtime/src/semantic-prefix-continuity.ts +++ b/packages/runtime/src/semantic-prefix-continuity.ts @@ -18,9 +18,12 @@ */ import type { + ModelCallAttempt, PreparedRequestObservation, PreparedRequestObservationSegment, } from '@maka/core/model-call-attempt'; +import { decodeModelCallAttempt } from '@maka/core/model-call-attempt'; +import { isSessionInlineRun, type AgentRunHeader, type AgentRunStore } from '@maka/core/agent-run'; export type SemanticPrefixContinuity = | { @@ -45,6 +48,36 @@ export type SemanticPrefixSegmentRef = Pick< 'kind' | 'index' | 'role' | 'label' >; +type PrefixStore = Pick; + +export async function deriveAttemptSemanticPrefixContinuity(input: { + current: ModelCallAttempt; + currentProviderStateIdentity?: `sha256:${string}`; + lineage: { + conversationCopy?: boolean; + parentRunId?: string; + parentTurnId?: string; + retriedFromTurnId?: string; + regeneratedFromTurnId?: string; + branchOfTurnId?: string; + parentSessionId?: string; + }; + store: PrefixStore; +}): Promise { + const { current } = input; + if (current.callKind !== 'main' || !current.requestObservation) return unavailable(); + + const predecessor = await predecessorAttempt(input); + if (predecessor === null) { + return { status: 'no_predecessor', previousSegmentCount: 0, preservedSegmentCount: 0 }; + } + if (!predecessor || !sameDomain(input, predecessor)) return unavailable(); + return deriveSemanticPrefixContinuity( + current.requestObservation, + predecessor.attempt.requestObservation!, + ); +} + export function deriveSemanticPrefixContinuity( currentObservation: PreparedRequestObservation, previousObservation: PreparedRequestObservation, @@ -112,3 +145,177 @@ function segmentRef(segment: PreparedRequestObservationSegment): SemanticPrefixS ...(segment.label === undefined ? {} : { label: segment.label }), }; } + +async function predecessorAttempt(input: { + current: ModelCallAttempt; + currentProviderStateIdentity?: `sha256:${string}`; + lineage: { + conversationCopy?: boolean; + parentRunId?: string; + parentTurnId?: string; + retriedFromTurnId?: string; + regeneratedFromTurnId?: string; + branchOfTurnId?: string; + parentSessionId?: string; + }; + store: PrefixStore; +}): Promise<{ attempt: ModelCallAttempt; run: AgentRunHeader } | null | undefined> { + const { current, lineage, store } = input; + const runs = await store.listSessionRuns(current.sessionId); + const currentRun = runs.find((run) => run.runId === current.runId); + if (!currentRun) return undefined; + + if (current.attempt > 0) { + return uniqueAttempt( + currentRun, + await attemptsFor(store, currentRun), + (candidate) => + candidate.logicalCallId === current.logicalCallId && + candidate.attempt === current.attempt - 1, + ); + } + if (current.step > 0) { + return latestAttempt( + currentRun, + (await attemptsFor(store, currentRun)).filter( + (candidate) => candidate.turnId === current.turnId && candidate.step === current.step - 1, + ), + ); + } + if (lineage.parentRunId) { + const parent = runs.find((run) => run.runId === lineage.parentRunId); + return parent ? latestAttempt(parent, await attemptsFor(store, parent)) : undefined; + } + if ( + lineage.conversationCopy || + lineage.parentSessionId || + lineage.parentTurnId || + lineage.retriedFromTurnId || + lineage.regeneratedFromTurnId || + lineage.branchOfTurnId + ) { + return undefined; + } + + const admission = await store.readRootTurnAdmission?.(current.sessionId, current.turnId); + if ( + !admission || + admission.runId !== current.runId || + admission.previousRootTurnId === undefined + ) { + return undefined; + } + if (admission.previousRootTurnId === null) return null; + const previousRuns = runs.filter( + (run) => run.turnId === admission.previousRootTurnId && isSessionInlineRun(run), + ); + if (previousRuns.length !== 1) return undefined; + return latestAttempt(previousRuns[0]!, await attemptsFor(store, previousRuns[0]!)); +} + +async function attemptsFor(store: PrefixStore, run: AgentRunHeader): Promise { + const attempts: ModelCallAttempt[] = []; + for (const event of await store.readEvents(run.sessionId, run.runId)) { + if (event.type !== 'model_call_attempt_recorded') continue; + try { + const attempt = decodeModelCallAttempt(event.data); + if ( + attempt.callKind === 'main' && + attempt.sessionId === run.sessionId && + attempt.runId === run.runId && + attempt.attemptId === event.id + ) { + attempts.push(attempt); + } + } catch { + // An unreadable attempt cannot become a guessed baseline. + } + } + return attempts; +} + +function latestAttempt( + run: AgentRunHeader, + attempts: readonly ModelCallAttempt[], +): { attempt: ModelCallAttempt; run: AgentRunHeader } | undefined { + if (attempts.length === 0) return undefined; + const step = Math.max(...attempts.map((attempt) => attempt.step)); + const onStep = attempts.filter((attempt) => attempt.step === step); + const physical = Math.max(...onStep.map((attempt) => attempt.attempt)); + return uniqueAttempt(run, onStep, (attempt) => attempt.attempt === physical); +} + +function uniqueAttempt( + run: AgentRunHeader, + attempts: readonly ModelCallAttempt[], + predicate: (attempt: ModelCallAttempt) => boolean, +): { attempt: ModelCallAttempt; run: AgentRunHeader } | undefined { + const matches = attempts.filter(predicate); + return matches.length === 1 ? { attempt: matches[0]!, run } : undefined; +} + +function sameDomain( + input: { + current: ModelCallAttempt; + currentProviderStateIdentity?: `sha256:${string}`; + }, + previous: { attempt: ModelCallAttempt; run: AgentRunHeader }, +): boolean { + const current = input.current; + const before = previous.attempt; + if (!before.requestObservation) return false; + const currentPartition = exactProviderPartition(current.requestObservation!); + const previousPartition = exactProviderPartition(before.requestObservation); + return ( + input.currentProviderStateIdentity !== undefined && + input.currentProviderStateIdentity === previous.run.providerStateIdentity && + current.sessionId === before.sessionId && + current.connectionSlug === before.connectionSlug && + current.providerId === before.providerId && + current.modelId === before.modelId && + currentPartition !== undefined && + currentPartition === previousPartition + ); +} + +function exactProviderPartition(observation: PreparedRequestObservation): string | undefined { + const segments = observation.segments.filter((segment) => segment.kind === 'provider_options'); + if (segments.some((segment) => segment.comparison === 'opaque')) return undefined; + return segments.map((segment) => `${segment.index}:${segment.digest}`).join('|'); +} + +function unavailable(): SemanticPrefixContinuity { + return { status: 'unavailable', previousSegmentCount: 0, preservedSegmentCount: 0 }; +} + +export function isSemanticPrefixContinuity(value: unknown): value is SemanticPrefixContinuity { + if (!value || typeof value !== 'object') return false; + const candidate = value as Partial; + if ( + !Number.isSafeInteger(candidate.previousSegmentCount) || + !Number.isSafeInteger(candidate.preservedSegmentCount) || + candidate.previousSegmentCount! < 0 || + candidate.preservedSegmentCount! < 0 || + candidate.preservedSegmentCount! > candidate.previousSegmentCount! + ) { + return false; + } + if (candidate.status === 'no_predecessor' || candidate.status === 'unavailable') { + return candidate.previousSegmentCount === 0 && candidate.preservedSegmentCount === 0; + } + if (candidate.status === 'preserved') { + return candidate.preservedSegmentCount === candidate.previousSegmentCount; + } + if (candidate.status === 'unknown') return true; + if (candidate.status !== 'diverged') return false; + const segment = candidate.firstDivergentSegment; + return Boolean( + segment && + (segment.kind === 'tool_schema' || + segment.kind === 'system_prompt' || + segment.kind === 'message' || + segment.kind === 'provider_options') && + Number.isSafeInteger(segment.index) && + segment.index >= 0, + ); +} From 72d3ea1d817b5a59b352d4ba04b93961fc66b1d5 Mon Sep 17 00:00:00 2001 From: AstroHan Date: Tue, 1 Sep 2026 01:43:44 +0800 Subject: [PATCH 03/19] refactor(runtime): keep root admission capability narrow Generated-by: Codex --- packages/core/src/agent-run.ts | 5 ----- packages/runtime/src/semantic-prefix-continuity.ts | 14 ++++++++++++-- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/packages/core/src/agent-run.ts b/packages/core/src/agent-run.ts index ccf1506df4..ac0fcedb24 100644 --- a/packages/core/src/agent-run.ts +++ b/packages/core/src/agent-run.ts @@ -795,11 +795,6 @@ export interface AgentRunStore { options?: AgentRunAppendOptions, ): Promise; readEvents(sessionId: string, runId: string): Promise; - /** Durable causal predecessor for a root Turn, when this store owns root admission. */ - readRootTurnAdmission?( - sessionId: string, - turnId: string, - ): Promise<{ runId: string; previousRootTurnId?: string | null } | undefined>; /** * `undefined` means uninitialized; `null` is an initialized empty projection. * diff --git a/packages/runtime/src/semantic-prefix-continuity.ts b/packages/runtime/src/semantic-prefix-continuity.ts index 1dd546b89b..2f67fa054b 100644 --- a/packages/runtime/src/semantic-prefix-continuity.ts +++ b/packages/runtime/src/semantic-prefix-continuity.ts @@ -48,7 +48,14 @@ export type SemanticPrefixSegmentRef = Pick< 'kind' | 'index' | 'role' | 'label' >; -type PrefixStore = Pick; +type PrefixStore = Pick; + +interface RootAdmissionReader { + readRootTurnAdmission?( + sessionId: string, + turnId: string, + ): Promise<{ runId: string; previousRootTurnId?: string | null } | undefined>; +} export async function deriveAttemptSemanticPrefixContinuity(input: { current: ModelCallAttempt; @@ -197,7 +204,10 @@ async function predecessorAttempt(input: { return undefined; } - const admission = await store.readRootTurnAdmission?.(current.sessionId, current.turnId); + const admission = await (store as PrefixStore & RootAdmissionReader).readRootTurnAdmission?.( + current.sessionId, + current.turnId, + ); if ( !admission || admission.runId !== current.runId || From 7674a15fea9913972f5b7c1d74ad225791ce0033 Mon Sep 17 00:00:00 2001 From: AstroHan Date: Tue, 1 Sep 2026 01:43:44 +0800 Subject: [PATCH 04/19] feat(runtime-host): carry request prefix continuity Generated-by: Codex --- .../src/__tests__/context-protocol.test.ts | 12 ++ packages/runtime-host/src/protocol/context.ts | 103 +++++++++++++++++- packages/runtime-host/src/protocol/index.ts | 4 +- 3 files changed, 117 insertions(+), 2 deletions(-) diff --git a/packages/runtime-host/src/__tests__/context-protocol.test.ts b/packages/runtime-host/src/__tests__/context-protocol.test.ts index 2e555ff508..30fa716882 100644 --- a/packages/runtime-host/src/__tests__/context-protocol.test.ts +++ b/packages/runtime-host/src/__tests__/context-protocol.test.ts @@ -71,6 +71,12 @@ test('context operations preserve bounded exact wire values', () => { turnCount: 2, estimatedTokens: 12, }, + requestPrefix: { + status: 'diverged', + previousSegmentCount: 8, + preservedSegmentCount: 2, + firstDivergentSegment: { kind: 'message', index: 2, role: 'user' }, + }, }, }), { @@ -96,6 +102,12 @@ test('context operations preserve bounded exact wire values', () => { turnCount: 2, estimatedTokens: 12, }, + requestPrefix: { + status: 'diverged', + previousSegmentCount: 8, + preservedSegmentCount: 2, + firstDivergentSegment: { kind: 'message', index: 2, role: 'user' }, + }, }, }, ); diff --git a/packages/runtime-host/src/protocol/context.ts b/packages/runtime-host/src/protocol/context.ts index 6d0e18c56c..003a7ca95d 100644 --- a/packages/runtime-host/src/protocol/context.ts +++ b/packages/runtime-host/src/protocol/context.ts @@ -71,6 +71,29 @@ export interface ContextDiagnosticsComposition { readonly unlabelledToolBytes?: number; } +export type ContextDiagnosticsRequestPrefix = + | { + readonly status: 'no_predecessor' | 'unavailable'; + readonly previousSegmentCount: 0; + readonly preservedSegmentCount: 0; + } + | { + readonly status: 'preserved' | 'unknown'; + readonly previousSegmentCount: number; + readonly preservedSegmentCount: number; + } + | { + readonly status: 'diverged'; + readonly previousSegmentCount: number; + readonly preservedSegmentCount: number; + readonly firstDivergentSegment: { + readonly kind: 'tool_schema' | 'system_prompt' | 'message' | 'provider_options'; + readonly index: number; + readonly role?: string; + readonly label?: string; + }; + }; + export type ContextDiagnosticsResult = | { readonly status: 'unavailable'; @@ -98,6 +121,8 @@ export type ContextDiagnosticsResult = readonly turnCount: number; readonly estimatedTokens: number; }; + /** Runtime-owned verdict; Host and Desktop must not recompute it. */ + readonly requestPrefix?: ContextDiagnosticsRequestPrefix; }; const QUERY_ERRORS = [ @@ -174,6 +199,7 @@ function decodeContextDiagnosticsResult(value: unknown): ContextDiagnosticsResul 'contextWindow', 'composition', 'compaction', + 'requestPrefix', ], ); if (record.status === 'unavailable') { @@ -196,7 +222,14 @@ function decodeContextDiagnosticsResult(value: unknown): ContextDiagnosticsResul record, 'Available context diagnostics', ['status', 'providerId', 'modelId', 'completedAt'], - ['inputTokens', 'cacheReadInputTokens', 'contextWindow', 'composition', 'compaction'], + [ + 'inputTokens', + 'cacheReadInputTokens', + 'contextWindow', + 'composition', + 'compaction', + 'requestPrefix', + ], ); return { status: 'available', @@ -223,6 +256,74 @@ function decodeContextDiagnosticsResult(value: unknown): ContextDiagnosticsResul ...(available.compaction === undefined ? {} : { compaction: decodeContextDiagnosticsCompaction(available.compaction) }), + ...(available.requestPrefix === undefined + ? {} + : { requestPrefix: decodeContextDiagnosticsRequestPrefix(available.requestPrefix) }), + }; +} + +function decodeContextDiagnosticsRequestPrefix(value: unknown): ContextDiagnosticsRequestPrefix { + const prefix = requireShapedRecord( + value, + 'Context diagnostics request prefix', + ['status', 'previousSegmentCount', 'preservedSegmentCount'], + ['firstDivergentSegment'], + ); + const previousSegmentCount = requireCount(prefix.previousSegmentCount, 'previousSegmentCount'); + const preservedSegmentCount = requireCount(prefix.preservedSegmentCount, 'preservedSegmentCount'); + if (preservedSegmentCount > previousSegmentCount) { + throw invalidProtocolFrame('Invalid request prefix segment counts'); + } + if (prefix.status === 'no_predecessor' || prefix.status === 'unavailable') { + if ( + prefix.firstDivergentSegment !== undefined || + previousSegmentCount !== 0 || + preservedSegmentCount !== 0 + ) { + throw invalidProtocolFrame('Invalid unavailable request prefix'); + } + return { status: prefix.status, previousSegmentCount: 0, preservedSegmentCount: 0 }; + } + if (prefix.status === 'preserved' || prefix.status === 'unknown') { + if ( + prefix.firstDivergentSegment !== undefined || + (prefix.status === 'preserved' && preservedSegmentCount !== previousSegmentCount) + ) { + throw invalidProtocolFrame('Invalid request prefix result'); + } + return { status: prefix.status, previousSegmentCount, preservedSegmentCount }; + } + if (prefix.status !== 'diverged' || prefix.firstDivergentSegment === undefined) { + throw invalidProtocolFrame('Invalid request prefix status'); + } + const segment = requireShapedRecord( + prefix.firstDivergentSegment, + 'Request prefix divergent segment', + ['kind', 'index'], + ['role', 'label'], + ); + if ( + segment.kind !== 'tool_schema' && + segment.kind !== 'system_prompt' && + segment.kind !== 'message' && + segment.kind !== 'provider_options' + ) { + throw invalidProtocolFrame('Invalid request prefix segment kind'); + } + return { + status: 'diverged', + previousSegmentCount, + preservedSegmentCount, + firstDivergentSegment: { + kind: segment.kind, + index: requireCount(segment.index, 'requestPrefixSegmentIndex'), + ...(segment.role === undefined + ? {} + : { role: requireString(segment.role, 'requestPrefixSegmentRole', 256) }), + ...(segment.label === undefined + ? {} + : { label: requireString(segment.label, 'requestPrefixSegmentLabel', 256) }), + }, }; } diff --git a/packages/runtime-host/src/protocol/index.ts b/packages/runtime-host/src/protocol/index.ts index 8aff63a16a..4ffe415b9a 100644 --- a/packages/runtime-host/src/protocol/index.ts +++ b/packages/runtime-host/src/protocol/index.ts @@ -95,7 +95,9 @@ export const RUNTIME_HOST_REGISTRATION_SCHEMA_VERSION = 1 as const; export const RUNTIME_HOST_PROTOCOL_VERSION = 0 as const; // Increment when the same protocol version no longer guarantees safe Client-Host // interoperability. Mismatches are rejected before domain commands are admitted. -export const RUNTIME_HOST_COMPATIBILITY_EPOCH = 87 as const; +export const RUNTIME_HOST_COMPATIBILITY_EPOCH = 88 as const; +// 88: Context diagnostics may carry Runtime-owned request-preservation results. +// Older peers reject this optional field on the closed diagnostics shape. // 87: The connection catalog projects each model as the Host resolved it — // a `catalog_entry` item per model, counted by the connection header. Clients // render those entries instead of merging the stored row against their own From 7f2cfb192ceb18161e4af717e0f0807fef939b71 Mon Sep 17 00:00:00 2001 From: AstroHan Date: Tue, 1 Sep 2026 01:46:32 +0800 Subject: [PATCH 05/19] feat(desktop): show request prefix continuity Generated-by: Codex --- .../session-inspector-composition.test.ts | 22 ++++++++- .../session-inspector-panel-model.test.ts | 19 ++++++++ .../src/renderer/features/workbar/testing.ts | 6 ++- .../session-inspector-overview-model.ts | 5 +++ .../inspector/session-inspector-panel.tsx | 45 ++++++++++++++++++- .../src/renderer/locales/conversation-copy.ts | 32 +++++++++++++ 6 files changed, 126 insertions(+), 3 deletions(-) diff --git a/apps/desktop/src/main/__tests__/session-inspector-composition.test.ts b/apps/desktop/src/main/__tests__/session-inspector-composition.test.ts index 03adbe1432..570bd5ead6 100644 --- a/apps/desktop/src/main/__tests__/session-inspector-composition.test.ts +++ b/apps/desktop/src/main/__tests__/session-inspector-composition.test.ts @@ -21,7 +21,10 @@ import assert from 'node:assert/strict'; import { test } from 'node:test'; import { createElement } from 'react'; import { renderToStaticMarkup } from 'react-dom/server'; -import { InspectorCompositionSection } from '../../renderer/features/workbar/testing.js'; +import { + InspectorCompositionSection, + InspectorRequestPrefixBadge, +} from '../../renderer/features/workbar/testing.js'; import { getDesktopConversationCopy } from '../../renderer/locales/conversation-copy.js'; test('maps each request-composition category to the same colour in the chart and legend', () => { @@ -60,3 +63,20 @@ test('maps each request-composition category to the same colour in the chart and ); } }); + +test('renders the Runtime divergence location as a compact badge', () => { + const markup = renderToStaticMarkup( + createElement(InspectorRequestPrefixBadge, { + copy: getDesktopConversationCopy('en').inspector, + requestPrefix: { + status: 'diverged', + previousSegmentCount: 8, + preservedSegmentCount: 2, + firstDivergentSegment: { kind: 'message', index: 2 }, + }, + }), + ); + + assert.match(markup, /Request prefix diverged at message 3/); + assert.match(markup, /data-maka-contract="request-prefix-continuity"/); +}); diff --git a/apps/desktop/src/main/__tests__/session-inspector-panel-model.test.ts b/apps/desktop/src/main/__tests__/session-inspector-panel-model.test.ts index e301fd4ec7..61adc3e6d7 100644 --- a/apps/desktop/src/main/__tests__/session-inspector-panel-model.test.ts +++ b/apps/desktop/src/main/__tests__/session-inspector-panel-model.test.ts @@ -138,6 +138,25 @@ test('does not estimate a cache-hit ratio from partial usage', () => { assert.equal(overview.cacheHitRate, undefined); }); + +test('passes the Runtime request-prefix verdict through without recomputing it', () => { + const requestPrefix = { + status: 'diverged' as const, + previousSegmentCount: 8, + preservedSegmentCount: 2, + firstDivergentSegment: { kind: 'message' as const, index: 2, role: 'user' }, + }; + const overview = deriveInspectorOverviewModel({ + status: 'available', + providerId: 'anthropic', + modelId: 'claude', + completedAt: 10, + requestPrefix, + }); + + assert.equal(overview.requestPrefix, requestPrefix); +}); + test('derives per-turn cost only from priced model-call step totals', () => { const cases: readonly { name: string; diff --git a/apps/desktop/src/renderer/features/workbar/testing.ts b/apps/desktop/src/renderer/features/workbar/testing.ts index 8bf5c6bb0a..ddc51e71f5 100644 --- a/apps/desktop/src/renderer/features/workbar/testing.ts +++ b/apps/desktop/src/renderer/features/workbar/testing.ts @@ -32,7 +32,11 @@ export * from './model/workbar-tool-definitions.js'; export * from './tools/artifacts/artifact-list-keyboard.js'; export * from './tools/artifacts/artifact-visibility.js'; export * from './tools/inspector/session-inspector-panel-model.js'; -export { compactNumberFormatter, InspectorCompositionSection } from './tools/inspector/session-inspector-panel.js'; +export { + compactNumberFormatter, + InspectorCompositionSection, + InspectorRequestPrefixBadge, +} from './tools/inspector/session-inspector-panel.js'; export * from './tools/inspector/session-inspector-overview-model.js'; export * from './tools/side-chat/quote-companion-panel-state.js'; export * from './tools/side-chat/quote-companion-core.js'; diff --git a/apps/desktop/src/renderer/features/workbar/tools/inspector/session-inspector-overview-model.ts b/apps/desktop/src/renderer/features/workbar/tools/inspector/session-inspector-overview-model.ts index 36e835d81f..17302c8e02 100644 --- a/apps/desktop/src/renderer/features/workbar/tools/inspector/session-inspector-overview-model.ts +++ b/apps/desktop/src/renderer/features/workbar/tools/inspector/session-inspector-overview-model.ts @@ -140,6 +140,8 @@ export interface InspectorOverviewModel { * the run ledger; three statements of the same tokens is two too many. */ cacheHitRate?: number; + /** Runtime-owned semantic prefix verdict; Desktop only presents it. */ + requestPrefix?: Extract['requestPrefix']; } export function estimatedSessionCost( @@ -186,6 +188,9 @@ export function deriveInspectorOverviewModel( ...(context ? { context } : {}), ...(composition ? { composition } : {}), ...(cacheHitRate !== undefined ? { cacheHitRate } : {}), + ...(diagnostics?.status === 'available' && diagnostics.requestPrefix + ? { requestPrefix: diagnostics.requestPrefix } + : {}), }; } diff --git a/apps/desktop/src/renderer/features/workbar/tools/inspector/session-inspector-panel.tsx b/apps/desktop/src/renderer/features/workbar/tools/inspector/session-inspector-panel.tsx index 86749b04dc..498cdadbbc 100644 --- a/apps/desktop/src/renderer/features/workbar/tools/inspector/session-inspector-panel.tsx +++ b/apps/desktop/src/renderer/features/workbar/tools/inspector/session-inspector-panel.tsx @@ -19,6 +19,7 @@ import { type ReactNode, useMemo } from 'react'; import { Banner } from '@astryxdesign/core/Banner'; +import { Badge } from '@astryxdesign/core/Badge'; import { Button } from '@astryxdesign/core/Button'; import { EmptyState } from '@astryxdesign/core/EmptyState'; import { Heading } from '@astryxdesign/core/Heading'; @@ -148,7 +149,7 @@ export function SessionInspectorPanel(props: { sessionId: string; active: boolea {copy.summaryUnavailable} )} - {(snapshot.summary || overview.context || overview.composition) && ( + {(snapshot.summary || overview.context || overview.composition || overview.requestPrefix) && ( + {overview.requestPrefix && ( + + )} {props.showTotals && ( ['requestPrefix']>; +}) { + const { requestPrefix, copy } = props; + if (requestPrefix.status === 'no_predecessor') return null; + if (requestPrefix.status === 'preserved') { + return ( + + ); + } + if (requestPrefix.status === 'diverged') { + return ( + + ); + } + return ( + + ); +} + /** * One overview total on the same title/readout rhythm as the sections below. * These figures answer parallel questions, so changing typography between the diff --git a/apps/desktop/src/renderer/locales/conversation-copy.ts b/apps/desktop/src/renderer/locales/conversation-copy.ts index a41bc754a7..a9a301e0b8 100644 --- a/apps/desktop/src/renderer/locales/conversation-copy.ts +++ b/apps/desktop/src/renderer/locales/conversation-copy.ts @@ -262,6 +262,15 @@ export interface DesktopConversationCopy { }; /** The three figures a reader opens this tab for, as headline stats. */ cacheHit: string; + requestPrefix: { + preserved: (preserved: number, previous: number) => string; + diverged: (segment: string, index: number) => string; + cannotJudge: string; + segment: Record< + 'tool_schema' | 'system_prompt' | 'message' | 'provider_options', + string + >; + }; /** Heading over the causal record. */ timelineTab: string; /** @@ -596,6 +605,17 @@ const COPY = { free: '剩余', }, cacheHit: '缓存命中率', + requestPrefix: { + preserved: (preserved, previous) => `请求前缀 ${preserved}/${previous} 保持`, + diverged: (segment, index) => `请求前缀在${segment} ${index}处分叉`, + cannotJudge: '请求前缀无法判断', + segment: { + tool_schema: '工具', + system_prompt: '系统提示', + message: '消息', + provider_options: '模型选项', + }, + }, timelineTab: '时间轴', composition: { title: '构成估算', @@ -831,6 +851,18 @@ const COPY = { free: 'Remaining', }, cacheHit: 'Cache hit rate', + requestPrefix: { + preserved: (preserved, previous) => + `Request prefix ${preserved}/${previous} preserved`, + diverged: (segment, index) => `Request prefix diverged at ${segment} ${index}`, + cannotJudge: 'Request prefix unavailable', + segment: { + tool_schema: 'tool', + system_prompt: 'system prompt', + message: 'message', + provider_options: 'model option', + }, + }, timelineTab: 'Timeline', composition: { title: 'Estimated composition', From 50a9c9cc3b7a58f53dcaf507ea5e643481b417f4 Mon Sep 17 00:00:00 2001 From: AstroHan Date: Tue, 1 Sep 2026 01:50:26 +0800 Subject: [PATCH 06/19] test(runtime): cover prefix continuity boundaries Generated-by: Codex --- .../__tests__/latest-context-commit.test.ts | 1 + .../semantic-prefix-continuity.test.ts | 162 +++++++++++++++++- 2 files changed, 160 insertions(+), 3 deletions(-) diff --git a/packages/runtime/src/__tests__/latest-context-commit.test.ts b/packages/runtime/src/__tests__/latest-context-commit.test.ts index 71c851a8c2..d56e1e8312 100644 --- a/packages/runtime/src/__tests__/latest-context-commit.test.ts +++ b/packages/runtime/src/__tests__/latest-context-commit.test.ts @@ -155,6 +155,7 @@ test('real consecutive sends seal a preserved prefix verdict that survives resta const successor = await readLatestContextDiagnostics(runStore, session.id); assert.equal(successor.status, 'available'); if (successor.status !== 'available') return; + assert.ok(successor.requestPrefix); assert.equal(successor.requestPrefix.status, 'preserved'); assert.equal( successor.requestPrefix.preservedSegmentCount, diff --git a/packages/runtime/src/__tests__/semantic-prefix-continuity.test.ts b/packages/runtime/src/__tests__/semantic-prefix-continuity.test.ts index 965f3bbd15..987387829f 100644 --- a/packages/runtime/src/__tests__/semantic-prefix-continuity.test.ts +++ b/packages/runtime/src/__tests__/semantic-prefix-continuity.test.ts @@ -18,12 +18,18 @@ */ import assert from 'node:assert/strict'; +import { createHash } from 'node:crypto'; import { test } from 'node:test'; import type { + ModelCallAttempt, PreparedRequestObservation, PreparedRequestObservationSegment, } from '@maka/core/model-call-attempt'; -import { deriveSemanticPrefixContinuity } from '../semantic-prefix-continuity.js'; +import type { AgentRunEvent, AgentRunHeader } from '@maka/core/agent-run'; +import { + deriveAttemptSemanticPrefixContinuity, + deriveSemanticPrefixContinuity, +} from '../semantic-prefix-continuity.js'; test('keeps every earlier cacheable segment when the current request only appends', () => { const previous = observation([message(0, 'system'), message(1, 'user-1')]); @@ -75,8 +81,79 @@ test('reports the first removed earlier segment', () => { }); }); +test('uses the preceding physical retry as its durable baseline', async () => { + const previous = attempt({ attemptId: 'attempt-0', attempt: 0 }); + const current = attempt({ attemptId: 'attempt-1', attempt: 1 }); + + assert.equal( + ( + await deriveAttemptSemanticPrefixContinuity({ + current, + currentProviderStateIdentity: PROVIDER_STATE, + lineage: {}, + store: prefixStore([run('run-1', 'turn-1', PROVIDER_STATE)], [previous]), + }) + ).status, + 'preserved', + ); +}); + +test('does not compare across provider execution identities', async () => { + const previous = attempt({ attemptId: 'previous', runId: 'run-1', turnId: 'turn-1' }); + const current = attempt({ attemptId: 'current', runId: 'run-2', turnId: 'turn-2' }); + + assert.equal( + ( + await deriveAttemptSemanticPrefixContinuity({ + current, + currentProviderStateIdentity: PROVIDER_STATE, + lineage: {}, + store: prefixStore( + [run('run-1', 'turn-1', OTHER_PROVIDER_STATE), run('run-2', 'turn-2', PROVIDER_STATE)], + [previous], + { runId: 'run-2', previousRootTurnId: 'turn-1' }, + ), + }) + ).status, + 'unavailable', + ); +}); + +test('does not choose between overlapping durable predecessor runs', async () => { + const current = attempt({ attemptId: 'current', runId: 'run-3', turnId: 'turn-2' }); + const headers = [ + run('run-1', 'turn-1', PROVIDER_STATE), + run('run-2', 'turn-1', PROVIDER_STATE), + run('run-3', 'turn-2', PROVIDER_STATE), + ]; + + assert.equal( + ( + await deriveAttemptSemanticPrefixContinuity({ + current, + currentProviderStateIdentity: PROVIDER_STATE, + lineage: {}, + store: prefixStore( + headers, + [ + attempt({ attemptId: 'previous-1', runId: 'run-1', turnId: 'turn-1' }), + attempt({ attemptId: 'previous-2', runId: 'run-2', turnId: 'turn-1' }), + ], + { runId: 'run-3', previousRootTurnId: 'turn-1' }, + ), + }) + ).status, + 'unavailable', + ); +}); + function observation(segments: PreparedRequestObservationSegment[]): PreparedRequestObservation { - return { schemaVersion: 1, digest: 'request', bytes: 1, segments }; + return { + schemaVersion: 1, + digest: digestFor(segments.map((segment) => segment.digest).join(':')), + bytes: 1, + segments, + }; } function message(index: number, digest: string): PreparedRequestObservationSegment { @@ -85,7 +162,86 @@ function message(index: number, digest: string): PreparedRequestObservationSegme index, cacheable: true, comparison: 'exact', - digest, + digest: digestFor(digest), bytes: 1, }; } + +function digestFor(value: string): string { + return `sha256:${createHash('sha256').update(value).digest('hex')}`; +} + +const PROVIDER_STATE = `sha256:${'1'.repeat(64)}` as const; +const OTHER_PROVIDER_STATE = `sha256:${'2'.repeat(64)}` as const; + +function attempt(overrides: Partial): ModelCallAttempt { + return { + schemaVersion: 1, + logicalCallId: 'logical-1', + attemptId: 'attempt-0', + traceId: 'trace-1', + sessionId: 'session-1', + runId: 'run-1', + turnId: 'turn-1', + step: 0, + attempt: 0, + callKind: 'main', + connectionSlug: 'connection', + providerId: 'anthropic', + modelId: 'model', + requestObservation: observation([message(0, 'same')]), + startedAt: 1, + completedAt: 2, + latencyMs: 1, + status: 'completed', + usageBasis: 'missing', + costBasis: 'unpriced', + ...overrides, + }; +} + +function run( + runId: string, + turnId: string, + providerStateIdentity: `sha256:${string}`, +): AgentRunHeader { + return { + runId, + sessionId: 'session-1', + turnId, + status: 'completed', + backendKind: 'ai-sdk', + providerStateIdentity, + llmConnectionSlug: 'connection', + modelId: 'model', + cwd: '/repo', + permissionMode: 'ask', + createdAt: 1, + updatedAt: 2, + }; +} + +function prefixStore( + runs: AgentRunHeader[], + attempts: ModelCallAttempt[], + admission?: { runId: string; previousRootTurnId: string | null }, +) { + return { + listSessionRuns: async () => runs, + readEvents: async (_sessionId: string, runId: string) => + attempts.filter((item) => item.runId === runId).map(attemptEvent), + readRootTurnAdmission: async () => admission, + }; +} + +function attemptEvent(attempt: ModelCallAttempt): AgentRunEvent { + return { + type: 'model_call_attempt_recorded', + id: attempt.attemptId, + runId: attempt.runId, + sessionId: attempt.sessionId, + turnId: attempt.turnId, + ts: attempt.completedAt, + data: { ...attempt }, + }; +} From 0dbea4f3890191484c622f38d63351a8b33e6faa Mon Sep 17 00:00:00 2001 From: AstroHan Date: Tue, 1 Sep 2026 01:59:57 +0800 Subject: [PATCH 07/19] fix(runtime): exclude child runs from prefix continuity Generated-by: Codex --- .../semantic-prefix-continuity.test.ts | 20 +++++++++++++ packages/runtime/src/agent-run.ts | 1 + .../runtime/src/semantic-prefix-continuity.ts | 30 ++++++++----------- 3 files changed, 34 insertions(+), 17 deletions(-) diff --git a/packages/runtime/src/__tests__/semantic-prefix-continuity.test.ts b/packages/runtime/src/__tests__/semantic-prefix-continuity.test.ts index 987387829f..fadefe49e6 100644 --- a/packages/runtime/src/__tests__/semantic-prefix-continuity.test.ts +++ b/packages/runtime/src/__tests__/semantic-prefix-continuity.test.ts @@ -90,6 +90,7 @@ test('uses the preceding physical retry as its durable baseline', async () => { await deriveAttemptSemanticPrefixContinuity({ current, currentProviderStateIdentity: PROVIDER_STATE, + currentSessionInline: true, lineage: {}, store: prefixStore([run('run-1', 'turn-1', PROVIDER_STATE)], [previous]), }) @@ -107,6 +108,7 @@ test('does not compare across provider execution identities', async () => { await deriveAttemptSemanticPrefixContinuity({ current, currentProviderStateIdentity: PROVIDER_STATE, + currentSessionInline: true, lineage: {}, store: prefixStore( [run('run-1', 'turn-1', OTHER_PROVIDER_STATE), run('run-2', 'turn-2', PROVIDER_STATE)], @@ -132,6 +134,7 @@ test('does not choose between overlapping durable predecessor runs', async () => await deriveAttemptSemanticPrefixContinuity({ current, currentProviderStateIdentity: PROVIDER_STATE, + currentSessionInline: true, lineage: {}, store: prefixStore( headers, @@ -147,6 +150,23 @@ test('does not choose between overlapping durable predecessor runs', async () => ); }); +test('does not compare a child run against its parent session run', async () => { + const previous = attempt({ attemptId: 'parent-attempt', runId: 'run-parent' }); + const current = attempt({ attemptId: 'child-attempt', runId: 'run-child' }); + const input = { + current, + currentProviderStateIdentity: PROVIDER_STATE, + currentSessionInline: false, + lineage: { parentRunId: 'run-parent' }, + store: prefixStore( + [run('run-parent', 'turn-1', PROVIDER_STATE), run('run-child', 'turn-1', PROVIDER_STATE)], + [previous], + ), + }; + + assert.equal((await deriveAttemptSemanticPrefixContinuity(input)).status, 'unavailable'); +}); + function observation(segments: PreparedRequestObservationSegment[]): PreparedRequestObservation { return { schemaVersion: 1, diff --git a/packages/runtime/src/agent-run.ts b/packages/runtime/src/agent-run.ts index 6edc658ffc..033bbf56b0 100644 --- a/packages/runtime/src/agent-run.ts +++ b/packages/runtime/src/agent-run.ts @@ -482,6 +482,7 @@ export class AgentRun { const requestPrefix = await deriveAttemptSemanticPrefixContinuity({ current: attempt, currentProviderStateIdentity: this.providerStateIdentity, + currentSessionInline: this.isSessionInline(), lineage: { ...this.lineage, ...(this.header.conversationCopy ? { conversationCopy: true } : {}), diff --git a/packages/runtime/src/semantic-prefix-continuity.ts b/packages/runtime/src/semantic-prefix-continuity.ts index 2f67fa054b..a0d9213e02 100644 --- a/packages/runtime/src/semantic-prefix-continuity.ts +++ b/packages/runtime/src/semantic-prefix-continuity.ts @@ -57,9 +57,10 @@ interface RootAdmissionReader { ): Promise<{ runId: string; previousRootTurnId?: string | null } | undefined>; } -export async function deriveAttemptSemanticPrefixContinuity(input: { +interface AttemptContinuityInput { current: ModelCallAttempt; currentProviderStateIdentity?: `sha256:${string}`; + currentSessionInline: boolean; lineage: { conversationCopy?: boolean; parentRunId?: string; @@ -70,9 +71,15 @@ export async function deriveAttemptSemanticPrefixContinuity(input: { parentSessionId?: string; }; store: PrefixStore; -}): Promise { +} + +export async function deriveAttemptSemanticPrefixContinuity( + input: AttemptContinuityInput, +): Promise { const { current } = input; - if (current.callKind !== 'main' || !current.requestObservation) return unavailable(); + if (!input.currentSessionInline || current.callKind !== 'main' || !current.requestObservation) { + return unavailable(); + } const predecessor = await predecessorAttempt(input); if (predecessor === null) { @@ -153,20 +160,9 @@ function segmentRef(segment: PreparedRequestObservationSegment): SemanticPrefixS }; } -async function predecessorAttempt(input: { - current: ModelCallAttempt; - currentProviderStateIdentity?: `sha256:${string}`; - lineage: { - conversationCopy?: boolean; - parentRunId?: string; - parentTurnId?: string; - retriedFromTurnId?: string; - regeneratedFromTurnId?: string; - branchOfTurnId?: string; - parentSessionId?: string; - }; - store: PrefixStore; -}): Promise<{ attempt: ModelCallAttempt; run: AgentRunHeader } | null | undefined> { +async function predecessorAttempt( + input: AttemptContinuityInput, +): Promise<{ attempt: ModelCallAttempt; run: AgentRunHeader } | null | undefined> { const { current, lineage, store } = input; const runs = await store.listSessionRuns(current.sessionId); const currentRun = runs.find((run) => run.runId === current.runId); From 2d8f7b7529142d8c94c40e9b1177591a97cb3a2a Mon Sep 17 00:00:00 2001 From: AstroHan Date: Tue, 1 Sep 2026 02:08:23 +0800 Subject: [PATCH 08/19] fix(runtime): rebuild continuity from durable lineage Generated-by: Codex --- .../src/__tests__/context-diagnostics.test.ts | 18 ++++ .../__tests__/latest-context-commit.test.ts | 4 +- .../semantic-prefix-continuity.test.ts | 92 +++++++++++++++++++ packages/runtime/src/agent-run.ts | 5 +- packages/runtime/src/context-diagnostics.ts | 32 ++++++- .../runtime/src/semantic-prefix-continuity.ts | 85 ++++++++++++++--- 6 files changed, 214 insertions(+), 22 deletions(-) diff --git a/packages/runtime/src/__tests__/context-diagnostics.test.ts b/packages/runtime/src/__tests__/context-diagnostics.test.ts index 9354d762a3..5926df6abe 100644 --- a/packages/runtime/src/__tests__/context-diagnostics.test.ts +++ b/packages/runtime/src/__tests__/context-diagnostics.test.ts @@ -68,6 +68,24 @@ test('rejects v2 snapshots that the canonical writer cannot produce', () => { estimatedTokens: 10, }, }, + { + ...base, + requestPrefix: { + status: 'diverged', + previousSegmentCount: 1, + preservedSegmentCount: 0, + firstDivergentSegment: { kind: 'message', index: 0, role: {} }, + }, + }, + { + ...base, + requestPrefix: { + status: 'preserved', + previousSegmentCount: 1, + preservedSegmentCount: 1, + unexpected: true, + }, + }, ]; for (const snapshot of impossible) { diff --git a/packages/runtime/src/__tests__/latest-context-commit.test.ts b/packages/runtime/src/__tests__/latest-context-commit.test.ts index d56e1e8312..5344412cca 100644 --- a/packages/runtime/src/__tests__/latest-context-commit.test.ts +++ b/packages/runtime/src/__tests__/latest-context-commit.test.ts @@ -200,6 +200,8 @@ test('real consecutive sends seal a preserved prefix verdict that survives resta }, repairEventProjection: (sessionId, type, event, options) => reopened.repairEventProjection(sessionId, type, event, options), + readRootTurnAdmission: (sessionId, turnId) => + reopened.readRootTurnAdmission(sessionId, turnId), }, session.id, ); @@ -208,7 +210,7 @@ test('real consecutive sends seal a preserved prefix verdict that survives resta assert.equal(cold.status, 'available'); if (cold.status !== 'available') return; assert.deepEqual(cold.composition, successor.composition); - assert.equal(cold.requestPrefix, undefined); + assert.deepEqual(cold.requestPrefix, successor.requestPrefix); } finally { reopened.close?.(); } diff --git a/packages/runtime/src/__tests__/semantic-prefix-continuity.test.ts b/packages/runtime/src/__tests__/semantic-prefix-continuity.test.ts index fadefe49e6..60fc9172ba 100644 --- a/packages/runtime/src/__tests__/semantic-prefix-continuity.test.ts +++ b/packages/runtime/src/__tests__/semantic-prefix-continuity.test.ts @@ -121,6 +121,34 @@ test('does not compare across provider execution identities', async () => { ); }); +test('does not compare attempts whose connection identity is missing', async () => { + const previous = attempt({ attemptId: 'previous', runId: 'run-1', turnId: 'turn-1' }); + const current = attempt({ + attemptId: 'current', + runId: 'run-2', + turnId: 'turn-2', + connectionSlug: undefined, + }); + previous.connectionSlug = undefined; + + assert.equal( + ( + await deriveAttemptSemanticPrefixContinuity({ + current, + currentProviderStateIdentity: PROVIDER_STATE, + currentSessionInline: true, + lineage: {}, + store: prefixStore( + [run('run-1', 'turn-1', PROVIDER_STATE), run('run-2', 'turn-2', PROVIDER_STATE)], + [previous], + { runId: 'run-2', previousRootTurnId: 'turn-1' }, + ), + }) + ).status, + 'unavailable', + ); +}); + test('does not choose between overlapping durable predecessor runs', async () => { const current = attempt({ attemptId: 'current', runId: 'run-3', turnId: 'turn-2' }); const headers = [ @@ -150,6 +178,57 @@ test('does not choose between overlapping durable predecessor runs', async () => ); }); +test('uses the unique continuation tip for the previous root turn', async () => { + const previous = attempt({ attemptId: 'continued', runId: 'run-2', turnId: 'turn-1' }); + const current = attempt({ attemptId: 'current', runId: 'run-3', turnId: 'turn-2' }); + + assert.equal( + ( + await deriveAttemptSemanticPrefixContinuity({ + current, + currentProviderStateIdentity: PROVIDER_STATE, + currentSessionInline: true, + lineage: {}, + store: prefixStore( + [ + run('run-1', 'turn-1', PROVIDER_STATE), + run('run-2', 'turn-1', PROVIDER_STATE, { + parentRunId: 'run-1', + continuationSource: continuationSource('run-1'), + }), + run('run-3', 'turn-2', PROVIDER_STATE), + ], + [previous], + { runId: 'run-3', previousRootTurnId: 'turn-1' }, + ), + }) + ).status, + 'preserved', + ); +}); + +test('compares later local turns without inheriting a copied session baseline', async () => { + const previous = attempt({ attemptId: 'local-1', runId: 'run-1', turnId: 'turn-1' }); + const current = attempt({ attemptId: 'local-2', runId: 'run-2', turnId: 'turn-2' }); + + assert.equal( + ( + await deriveAttemptSemanticPrefixContinuity({ + current, + currentProviderStateIdentity: PROVIDER_STATE, + currentSessionInline: true, + lineage: {}, + store: prefixStore( + [run('run-1', 'turn-1', PROVIDER_STATE), run('run-2', 'turn-2', PROVIDER_STATE)], + [previous], + { runId: 'run-2', previousRootTurnId: 'turn-1' }, + ), + }) + ).status, + 'preserved', + ); +}); + test('does not compare a child run against its parent session run', async () => { const previous = attempt({ attemptId: 'parent-attempt', runId: 'run-parent' }); const current = attempt({ attemptId: 'child-attempt', runId: 'run-child' }); @@ -224,6 +303,7 @@ function run( runId: string, turnId: string, providerStateIdentity: `sha256:${string}`, + overrides: Partial = {}, ): AgentRunHeader { return { runId, @@ -238,6 +318,18 @@ function run( permissionMode: 'ask', createdAt: 1, updatedAt: 2, + ...overrides, + }; +} + +function continuationSource( + sourceRunId: string, +): NonNullable { + return { + sourceInvocationId: sourceRunId, + sourceRunId, + sourceTurnId: 'turn-1', + sourceRuntimeEventHighWater: 1, }; } diff --git a/packages/runtime/src/agent-run.ts b/packages/runtime/src/agent-run.ts index 033bbf56b0..14347f9c71 100644 --- a/packages/runtime/src/agent-run.ts +++ b/packages/runtime/src/agent-run.ts @@ -483,10 +483,7 @@ export class AgentRun { current: attempt, currentProviderStateIdentity: this.providerStateIdentity, currentSessionInline: this.isSessionInline(), - lineage: { - ...this.lineage, - ...(this.header.conversationCopy ? { conversationCopy: true } : {}), - }, + lineage: this.lineage, store: runStore, }).catch(() => ({ status: 'unavailable' as const, diff --git a/packages/runtime/src/context-diagnostics.ts b/packages/runtime/src/context-diagnostics.ts index 6023fde4c2..d57137b19f 100644 --- a/packages/runtime/src/context-diagnostics.ts +++ b/packages/runtime/src/context-diagnostics.ts @@ -21,6 +21,7 @@ import { isSessionInlineRun, supersedesLatestContext, type AgentRunEvent, + type AgentRunHeader, type AgentRunStore, } from '@maka/core/agent-run'; import { decodeModelCallAttempt, type ModelCallAttempt } from '@maka/core/model-call-attempt'; @@ -39,7 +40,10 @@ import { validateHistoryCompactCheckpointShape, type HistoryCompactCheckpoint, } from './history-compact-checkpoint.js'; -import type { SemanticPrefixContinuity } from './semantic-prefix-continuity.js'; +import { + deriveAttemptSemanticPrefixContinuity, + type SemanticPrefixContinuity, +} from './semantic-prefix-continuity.js'; export type ContextDiagnosticsUnavailableReason = 'no_completed_request' | 'trace_unavailable'; @@ -120,7 +124,12 @@ type ContextRunStore = Pick< | 'readEventProjection' | 'readEventLedgerRevision' | 'repairEventProjection' ->; +> & { + readRootTurnAdmission?( + sessionId: string, + turnId: string, + ): Promise<{ runId: string; previousRootTurnId?: string | null } | undefined>; +}; /** * What the session's context is made of right now (#1580, reshaped for #2323). @@ -214,7 +223,7 @@ async function rebuildContextFromLedger( for (const event of await runStore.readEvents(sessionId, run.runId)) { if (event.type === METERING_EVENT_TYPE) { sawCanonicalRecord = true; - const candidate = meteringAnchor(event); + const candidate = meteringAnchor(event, run); if (candidate && supersedesLatestContext(candidate, anchor)) anchor = candidate; continue; } @@ -250,6 +259,16 @@ async function rebuildContextFromLedger( (anchor && !anchor.hasRequestObservation ? exactHistoricalComposition(anchor, historicalAttempts) : undefined); + const requestPrefix = + anchor?.attempt && anchor.run + ? await deriveAttemptSemanticPrefixContinuity({ + current: anchor.attempt, + currentProviderStateIdentity: anchor.run.providerStateIdentity, + currentSessionInline: true, + lineage: anchor.run, + store: runStore, + }) + : undefined; const snapshot: LatestContextSnapshot = { schemaVersion: LATEST_CONTEXT_SNAPSHOT_SCHEMA_VERSION, attemptId: resolved.attemptId, @@ -263,6 +282,7 @@ async function rebuildContextFromLedger( ...(resolved.contextWindow !== undefined ? { contextWindow: resolved.contextWindow } : {}), ...(composition ? { composition } : {}), ...(boundary ? { compaction: contextDiagnosticsCompactionOf(boundary.checkpoint) } : {}), + ...(requestPrefix ? { requestPrefix } : {}), }; // Repair on the way out, so this scan happens once per session rather than // on every panel refresh. Best-effort: the caller already has its answer, @@ -379,6 +399,8 @@ interface MeteringAnchor { contextWindow?: number; composition?: ContextDiagnosticsComposition; hasRequestObservation: boolean; + attempt?: ModelCallAttempt; + run?: AgentRunHeader; } interface CheckpointCandidate { @@ -388,7 +410,7 @@ interface CheckpointCandidate { } /** Only a completed MAIN call describes the conversation's own context. */ -function meteringAnchor(event: AgentRunEvent): MeteringAnchor | undefined { +function meteringAnchor(event: AgentRunEvent, run: AgentRunHeader): MeteringAnchor | undefined { let attempt: ModelCallAttempt; try { attempt = decodeModelCallAttempt(event.data); @@ -407,6 +429,8 @@ function meteringAnchor(event: AgentRunEvent): MeteringAnchor | undefined { startedAt: attempt.startedAt, completedAt: attempt.completedAt, hasRequestObservation: attempt.requestObservation !== undefined, + attempt, + run, ...(attempt.inputTokens !== undefined ? { inputTokens: attempt.inputTokens } : {}), ...(attempt.cacheReadInputTokens !== undefined ? { cacheReadInputTokens: attempt.cacheReadInputTokens } diff --git a/packages/runtime/src/semantic-prefix-continuity.ts b/packages/runtime/src/semantic-prefix-continuity.ts index a0d9213e02..c3bea06708 100644 --- a/packages/runtime/src/semantic-prefix-continuity.ts +++ b/packages/runtime/src/semantic-prefix-continuity.ts @@ -62,7 +62,6 @@ interface AttemptContinuityInput { currentProviderStateIdentity?: `sha256:${string}`; currentSessionInline: boolean; lineage: { - conversationCopy?: boolean; parentRunId?: string; parentTurnId?: string; retriedFromTurnId?: string; @@ -190,7 +189,6 @@ async function predecessorAttempt( return parent ? latestAttempt(parent, await attemptsFor(store, parent)) : undefined; } if ( - lineage.conversationCopy || lineage.parentSessionId || lineage.parentTurnId || lineage.retriedFromTurnId || @@ -215,8 +213,30 @@ async function predecessorAttempt( const previousRuns = runs.filter( (run) => run.turnId === admission.previousRootTurnId && isSessionInlineRun(run), ); - if (previousRuns.length !== 1) return undefined; - return latestAttempt(previousRuns[0]!, await attemptsFor(store, previousRuns[0]!)); + const previous = uniqueDurableRunTip(previousRuns); + return previous ? latestAttempt(previous, await attemptsFor(store, previous)) : undefined; +} + +function uniqueDurableRunTip(runs: readonly AgentRunHeader[]): AgentRunHeader | undefined { + const byId = new Map(runs.map((run) => [run.runId, run])); + if (byId.size !== runs.length) return undefined; + const childByParent = new Map(); + for (const run of runs) { + if (!run.parentRunId || !byId.has(run.parentRunId)) continue; + if (childByParent.has(run.parentRunId)) return undefined; + childByParent.set(run.parentRunId, run.runId); + } + const tips = runs.filter((run) => !childByParent.has(run.runId)); + if (tips.length !== 1) return undefined; + + const visited = new Set(); + let cursor: AgentRunHeader | undefined = tips[0]; + while (cursor) { + if (visited.has(cursor.runId)) return undefined; + visited.add(cursor.runId); + cursor = cursor.parentRunId ? byId.get(cursor.parentRunId) : undefined; + } + return visited.size === runs.length ? tips[0] : undefined; } async function attemptsFor(store: PrefixStore, run: AgentRunHeader): Promise { @@ -276,6 +296,7 @@ function sameDomain( input.currentProviderStateIdentity !== undefined && input.currentProviderStateIdentity === previous.run.providerStateIdentity && current.sessionId === before.sessionId && + current.connectionSlug !== undefined && current.connectionSlug === before.connectionSlug && current.providerId === before.providerId && current.modelId === before.modelId && @@ -296,32 +317,70 @@ function unavailable(): SemanticPrefixContinuity { export function isSemanticPrefixContinuity(value: unknown): value is SemanticPrefixContinuity { if (!value || typeof value !== 'object') return false; - const candidate = value as Partial; + const candidate = value as { + status?: unknown; + previousSegmentCount?: unknown; + preservedSegmentCount?: unknown; + firstDivergentSegment?: unknown; + }; + if ( + !hasOnlyKeys(candidate, [ + 'status', + 'previousSegmentCount', + 'preservedSegmentCount', + 'firstDivergentSegment', + ]) + ) { + return false; + } if ( + typeof candidate.previousSegmentCount !== 'number' || + typeof candidate.preservedSegmentCount !== 'number' || !Number.isSafeInteger(candidate.previousSegmentCount) || !Number.isSafeInteger(candidate.preservedSegmentCount) || - candidate.previousSegmentCount! < 0 || - candidate.preservedSegmentCount! < 0 || - candidate.preservedSegmentCount! > candidate.previousSegmentCount! + candidate.previousSegmentCount < 0 || + candidate.preservedSegmentCount < 0 || + candidate.preservedSegmentCount > candidate.previousSegmentCount ) { return false; } if (candidate.status === 'no_predecessor' || candidate.status === 'unavailable') { - return candidate.previousSegmentCount === 0 && candidate.preservedSegmentCount === 0; + return ( + candidate.firstDivergentSegment === undefined && + candidate.previousSegmentCount === 0 && + candidate.preservedSegmentCount === 0 + ); } if (candidate.status === 'preserved') { - return candidate.preservedSegmentCount === candidate.previousSegmentCount; + return ( + candidate.firstDivergentSegment === undefined && + candidate.preservedSegmentCount === candidate.previousSegmentCount + ); } - if (candidate.status === 'unknown') return true; + if (candidate.status === 'unknown') return candidate.firstDivergentSegment === undefined; if (candidate.status !== 'diverged') return false; - const segment = candidate.firstDivergentSegment; + const segment = candidate.firstDivergentSegment as + | { kind?: unknown; index?: unknown; role?: unknown; label?: unknown } + | undefined; return Boolean( segment && + hasOnlyKeys(segment, ['kind', 'index', 'role', 'label']) && (segment.kind === 'tool_schema' || segment.kind === 'system_prompt' || segment.kind === 'message' || segment.kind === 'provider_options') && + typeof segment.index === 'number' && Number.isSafeInteger(segment.index) && - segment.index >= 0, + segment.index >= 0 && + (segment.role === undefined || isBoundedText(segment.role)) && + (segment.label === undefined || isBoundedText(segment.label)), ); } + +function hasOnlyKeys(value: object, allowed: readonly string[]): boolean { + return Object.keys(value).every((key) => allowed.includes(key)); +} + +function isBoundedText(value: unknown): value is string { + return typeof value === 'string' && value.length <= 256; +} From 3a91e9655aed5f52c2d3f749021b4c43979bb1ad Mon Sep 17 00:00:00 2001 From: AstroHan Date: Tue, 1 Sep 2026 02:08:23 +0800 Subject: [PATCH 09/19] fix(runtime-host): qualify connection configuration changes Generated-by: Codex --- .../execution-model-composition.test.ts | 32 +++++++++++++++++-- .../src/server/execution-model-authority.ts | 10 ++++-- 2 files changed, 38 insertions(+), 4 deletions(-) diff --git a/packages/runtime-host/src/__tests__/execution-model-composition.test.ts b/packages/runtime-host/src/__tests__/execution-model-composition.test.ts index a4fe2d3c7a..1a8e7578c2 100644 --- a/packages/runtime-host/src/__tests__/execution-model-composition.test.ts +++ b/packages/runtime-host/src/__tests__/execution-model-composition.test.ts @@ -45,7 +45,7 @@ import type { BackendCompactHistoryInput } from '@maka/core/backend-types'; import { decodeCanonicalToolResultContent } from '@maka/core/tool-result-record-schema'; import { type ModelCallAttempt, type ModelCallKind } from '@maka/core/model-call-attempt'; import { type RuntimeEvent } from '@maka/core/runtime-event'; -import { createDefaultRuntimePolicy } from '@maka/core/runtime-policy'; +import { createDefaultRuntimePolicy, type RuntimePolicy } from '@maka/core/runtime-policy'; import type { PlanSessionState, PlanStore } from '@maka/core/plan'; import type { SessionTodoToolStore } from '@maka/runtime/session-todo-tools'; import { @@ -184,6 +184,31 @@ test('prepared backend activation builds from its admitted provider snapshot', a await backend.dispose(); }); +test('connection and proxy changes qualify the admitted provider identity', async () => { + const prepare = async (revision: number, networkProxy?: RuntimePolicy['networkProxy']) => { + const input = backendCreationFixture({ + abortSignal: new AbortController().signal, + resolveExecutionConnection: async () => + readyExecutionConnection(undefined, { revision, networkProxy }), + readPricing: async () => ({ revision: 0, overrides: [] }), + }); + const { context, ...dependencies } = input; + return prepareHostAiSdkBackend({ context, ...dependencies }); + }; + + const original = (await prepare(1)).providerStateIdentity; + assert.notEqual(original, (await prepare(2)).providerStateIdentity); + assert.notEqual( + original, + ( + await prepare(1, { + ...createDefaultRuntimePolicy().networkProxy, + enabled: true, + }) + ).providerStateIdentity, + ); +}); + test('backend creation aborts a stalled canonical connection read', async () => { const abort = new AbortController(); const creating = createHostAiSdkBackend( @@ -4047,12 +4072,15 @@ function readyExecutionConnection( customization: { readonly requestHeaders?: Readonly>; readonly requestBodyOverlay?: Readonly>; + readonly networkProxy?: RuntimePolicy['networkProxy']; + readonly revision?: number; readonly vision?: boolean; } = {}, ) { return { kind: 'ready', connection: { + revision: customization.revision ?? 1, slug: 'backend-creation-connection', providerType: 'moonshot', ...(baseUrl ? { baseUrl } : {}), @@ -4073,7 +4101,7 @@ function readyExecutionConnection( }, ], }, - networkProxy: { enabled: false }, + networkProxy: customization.networkProxy ?? { enabled: false }, secretMaterial: { connection: { secret: API_KEY }, ...(customization.requestHeaders diff --git a/packages/runtime-host/src/server/execution-model-authority.ts b/packages/runtime-host/src/server/execution-model-authority.ts index 72e71a03e1..214c30563d 100644 --- a/packages/runtime-host/src/server/execution-model-authority.ts +++ b/packages/runtime-host/src/server/execution-model-authority.ts @@ -763,15 +763,21 @@ function providerStateIdentityForResolvedExecution( { kind: 'ready' } >, ): `sha256:${string}` { - const credentialBasis = (material: typeof resolved.secretMaterial.connection) => - material ? { credentialId: material.credentialId, revision: material.revision } : null; + const credentialBasis = ( + material: { readonly credentialId?: string; readonly revision?: number } | null | undefined, + ) => (material ? { credentialId: material.credentialId, revision: material.revision } : null); + const proxy = resolved.networkProxy.enabled ? resolved.networkProxy : null; return stableHash({ protocol: 'provider_state_identity_v1', connectionId: resolved.connection.connectionId, + connectionRevision: resolved.connection.revision, providerType: resolved.connection.providerType, endpoint: new URL(effectiveBaseUrl(resolved.connection)).toString(), credential: credentialBasis(resolved.secretMaterial.connection), requestHeaders: credentialBasis(resolved.secretMaterial.requestHeaders), + proxy, + proxyCredential: + proxy?.authEnabled === true ? credentialBasis(resolved.secretMaterial.networkProxy) : null, }); } From 3dd36d1e0f6c44bf6028cac34e185498b639692e Mon Sep 17 00:00:00 2001 From: AstroHan Date: Tue, 1 Sep 2026 02:11:59 +0800 Subject: [PATCH 10/19] fix(desktop): hide stale prefix diagnostics Generated-by: Codex --- .../session-inspector-panel-model.test.ts | 16 +++++++ .../main/__tests__/use-session-trace.test.ts | 47 +++++++++++++++++++ .../session-inspector-overview-model.ts | 4 +- .../tools/inspector/use-session-trace.ts | 12 +++-- 4 files changed, 73 insertions(+), 6 deletions(-) diff --git a/apps/desktop/src/main/__tests__/session-inspector-panel-model.test.ts b/apps/desktop/src/main/__tests__/session-inspector-panel-model.test.ts index 61adc3e6d7..1176849d0e 100644 --- a/apps/desktop/src/main/__tests__/session-inspector-panel-model.test.ts +++ b/apps/desktop/src/main/__tests__/session-inspector-panel-model.test.ts @@ -157,6 +157,22 @@ test('passes the Runtime request-prefix verdict through without recomputing it', assert.equal(overview.requestPrefix, requestPrefix); }); +test('does not create an empty overview for a request without a predecessor', () => { + const overview = deriveInspectorOverviewModel({ + status: 'available', + providerId: 'anthropic', + modelId: 'claude', + completedAt: 10, + requestPrefix: { + status: 'no_predecessor', + previousSegmentCount: 0, + preservedSegmentCount: 0, + }, + }); + + assert.equal(overview.requestPrefix, undefined); +}); + test('derives per-turn cost only from priced model-call step totals', () => { const cases: readonly { name: string; diff --git a/apps/desktop/src/main/__tests__/use-session-trace.test.ts b/apps/desktop/src/main/__tests__/use-session-trace.test.ts index 06d0b5eed1..3a32cc6d8c 100644 --- a/apps/desktop/src/main/__tests__/use-session-trace.test.ts +++ b/apps/desktop/src/main/__tests__/use-session-trace.test.ts @@ -120,6 +120,10 @@ function createTraceHarness( sessionId: string, readIndex: number, ) => Promise>; + context?: ( + sessionId: string, + readIndex: number, + ) => ReturnType; } = {}, ): TraceHarness { const handlers = new Set<(event: SessionEvent) => void>(); @@ -168,6 +172,7 @@ function createTraceHarness( // TRACE is re-read, and an enrichment read must not move them. context: async (sessionId: string) => { harness.contextReads.push(sessionId); + if (options.context) return options.context(sessionId, harness.contextReads.length); return { ok: true as const, data: { @@ -312,6 +317,48 @@ describe('useSessionTrace', () => { assert.equal(harness.reads.length, 2, 'a closing burst is one re-read, not three'); }); + it('does not keep an earlier request-prefix verdict while its refresh fails', async () => { + const { root } = installReactRenderer(); + const harness = createTraceHarness({ + context: async (_sessionId, readIndex) => { + if (readIndex > 1) throw new Error('context unavailable'); + return { + ok: true, + data: { + status: 'available', + providerId: 'anthropic', + modelId: 'model', + completedAt: 1, + requestPrefix: { + status: 'preserved', + previousSegmentCount: 1, + preservedSegmentCount: 1, + }, + }, + }; + }, + }); + let snapshot: ReturnType | undefined; + await act(async () => { + root.render( + createElement(Probe, { + services: harness.services, + sessionId: 'session-1', + active: true, + onHookSnapshot: (value) => { + snapshot = value; + }, + }), + ); + }); + assert.equal(snapshot?.context?.status, 'available'); + + await act(async () => harness.emit(event('complete'))); + await flushRefresh(); + + assert.equal(snapshot?.context, undefined); + }); + it('refreshes Session usage only from the Usage authority signal', async () => { const { root } = installReactRenderer(); const harness = createTraceHarness(); diff --git a/apps/desktop/src/renderer/features/workbar/tools/inspector/session-inspector-overview-model.ts b/apps/desktop/src/renderer/features/workbar/tools/inspector/session-inspector-overview-model.ts index 17302c8e02..ea9063f0cd 100644 --- a/apps/desktop/src/renderer/features/workbar/tools/inspector/session-inspector-overview-model.ts +++ b/apps/desktop/src/renderer/features/workbar/tools/inspector/session-inspector-overview-model.ts @@ -188,7 +188,9 @@ export function deriveInspectorOverviewModel( ...(context ? { context } : {}), ...(composition ? { composition } : {}), ...(cacheHitRate !== undefined ? { cacheHitRate } : {}), - ...(diagnostics?.status === 'available' && diagnostics.requestPrefix + ...(diagnostics?.status === 'available' && + diagnostics.requestPrefix && + diagnostics.requestPrefix.status !== 'no_predecessor' ? { requestPrefix: diagnostics.requestPrefix } : {}), }; diff --git a/apps/desktop/src/renderer/features/workbar/tools/inspector/use-session-trace.ts b/apps/desktop/src/renderer/features/workbar/tools/inspector/use-session-trace.ts index 5632085f8f..a77e824e33 100644 --- a/apps/desktop/src/renderer/features/workbar/tools/inspector/use-session-trace.ts +++ b/apps/desktop/src/renderer/features/workbar/tools/inspector/use-session-trace.ts @@ -255,8 +255,11 @@ export function useSessionTrace( }, [inspector]); const readContext = useCallback((targetSessionId: string) => { - const contextRevision = ++contextRevisionRef.current; - // Enrichment, and read as such: the context snapshot has its own owner + const contextRevision = ++contextRevisionRef.current; + setState((current) => + current.sessionId === targetSessionId ? { ...current, context: undefined } : current, + ); + // Enrichment, and read as such: the context snapshot has its own owner // and its own failure modes, so it lands when it lands and its absence // costs the composition block, never the trace. void inspector.context(targetSessionId).then( @@ -269,9 +272,8 @@ export function useSessionTrace( ); }, () => { - // A refresh that could not reach the snapshot leaves the last one - // standing: it is still the newest answer anyone has, and blanking it - // would report "no composition" for a read that simply failed. + // The old snapshot was cleared when this read began. Keeping it would + // present the previous request's verdict as the current request's. }, ); }, [inspector]); From c265819a6aeac001b34a32b39d2568f6c3c7fea2 Mon Sep 17 00:00:00 2001 From: AstroHan Date: Tue, 1 Sep 2026 02:17:31 +0800 Subject: [PATCH 11/19] fix(runtime): reject ambiguous prefix baselines Generated-by: Codex --- .../semantic-prefix-continuity.test.ts | 49 +++++++++++++++++-- .../runtime/src/semantic-prefix-continuity.ts | 8 ++- 2 files changed, 51 insertions(+), 6 deletions(-) diff --git a/packages/runtime/src/__tests__/semantic-prefix-continuity.test.ts b/packages/runtime/src/__tests__/semantic-prefix-continuity.test.ts index 60fc9172ba..8f8a05bca8 100644 --- a/packages/runtime/src/__tests__/semantic-prefix-continuity.test.ts +++ b/packages/runtime/src/__tests__/semantic-prefix-continuity.test.ts @@ -149,6 +149,28 @@ test('does not compare attempts whose connection identity is missing', async () ); }); +test('does not use an attempt whose durable turn disagrees with its run', async () => { + const previous = attempt({ attemptId: 'previous', runId: 'run-1', turnId: 'wrong-turn' }); + const current = attempt({ attemptId: 'current', runId: 'run-2', turnId: 'turn-2' }); + + assert.equal( + ( + await deriveAttemptSemanticPrefixContinuity({ + current, + currentProviderStateIdentity: PROVIDER_STATE, + currentSessionInline: true, + lineage: {}, + store: prefixStore( + [run('run-1', 'turn-1', PROVIDER_STATE), run('run-2', 'turn-2', PROVIDER_STATE)], + [previous], + { runId: 'run-2', previousRootTurnId: 'turn-1' }, + ), + }) + ).status, + 'unavailable', + ); +}); + test('does not choose between overlapping durable predecessor runs', async () => { const current = attempt({ attemptId: 'current', runId: 'run-3', turnId: 'turn-2' }); const headers = [ @@ -210,6 +232,24 @@ test('uses the unique continuation tip for the previous root turn', async () => test('compares later local turns without inheriting a copied session baseline', async () => { const previous = attempt({ attemptId: 'local-1', runId: 'run-1', turnId: 'turn-1' }); const current = attempt({ attemptId: 'local-2', runId: 'run-2', turnId: 'turn-2' }); + const runs = [ + run('copied-run', 'copied-turn', PROVIDER_STATE), + run('run-1', 'turn-1', PROVIDER_STATE), + run('run-2', 'turn-2', PROVIDER_STATE), + ]; + + assert.equal( + ( + await deriveAttemptSemanticPrefixContinuity({ + current: previous, + currentProviderStateIdentity: PROVIDER_STATE, + currentSessionInline: true, + lineage: {}, + store: prefixStore(runs, [], { runId: 'run-1', previousRootTurnId: null }), + }) + ).status, + 'unavailable', + ); assert.equal( ( @@ -218,11 +258,10 @@ test('compares later local turns without inheriting a copied session baseline', currentProviderStateIdentity: PROVIDER_STATE, currentSessionInline: true, lineage: {}, - store: prefixStore( - [run('run-1', 'turn-1', PROVIDER_STATE), run('run-2', 'turn-2', PROVIDER_STATE)], - [previous], - { runId: 'run-2', previousRootTurnId: 'turn-1' }, - ), + store: prefixStore(runs, [previous], { + runId: 'run-2', + previousRootTurnId: 'turn-1', + }), }) ).status, 'preserved', diff --git a/packages/runtime/src/semantic-prefix-continuity.ts b/packages/runtime/src/semantic-prefix-continuity.ts index c3bea06708..92a9d3fd4a 100644 --- a/packages/runtime/src/semantic-prefix-continuity.ts +++ b/packages/runtime/src/semantic-prefix-continuity.ts @@ -209,7 +209,11 @@ async function predecessorAttempt( ) { return undefined; } - if (admission.previousRootTurnId === null) return null; + if (admission.previousRootTurnId === null) { + return runs.some((run) => run.runId !== current.runId && isSessionInlineRun(run)) + ? undefined + : null; + } const previousRuns = runs.filter( (run) => run.turnId === admission.previousRootTurnId && isSessionInlineRun(run), ); @@ -249,6 +253,8 @@ async function attemptsFor(store: PrefixStore, run: AgentRunHeader): Promise Date: Tue, 1 Sep 2026 02:17:31 +0800 Subject: [PATCH 12/19] fix(runtime-host): align continuity wire contract Generated-by: Codex --- .../src/__tests__/context-protocol.test.ts | 4 +-- packages/runtime-host/src/protocol/context.ts | 34 ++++++------------- packages/runtime/src/context-diagnostics.ts | 2 ++ 3 files changed, 14 insertions(+), 26 deletions(-) diff --git a/packages/runtime-host/src/__tests__/context-protocol.test.ts b/packages/runtime-host/src/__tests__/context-protocol.test.ts index 30fa716882..88d7ead1b5 100644 --- a/packages/runtime-host/src/__tests__/context-protocol.test.ts +++ b/packages/runtime-host/src/__tests__/context-protocol.test.ts @@ -75,7 +75,7 @@ test('context operations preserve bounded exact wire values', () => { status: 'diverged', previousSegmentCount: 8, preservedSegmentCount: 2, - firstDivergentSegment: { kind: 'message', index: 2, role: 'user' }, + firstDivergentSegment: { kind: 'message', index: 2, role: '' }, }, }, }), @@ -106,7 +106,7 @@ test('context operations preserve bounded exact wire values', () => { status: 'diverged', previousSegmentCount: 8, preservedSegmentCount: 2, - firstDivergentSegment: { kind: 'message', index: 2, role: 'user' }, + firstDivergentSegment: { kind: 'message', index: 2, role: '' }, }, }, }, diff --git a/packages/runtime-host/src/protocol/context.ts b/packages/runtime-host/src/protocol/context.ts index 003a7ca95d..8ae912f8f2 100644 --- a/packages/runtime-host/src/protocol/context.ts +++ b/packages/runtime-host/src/protocol/context.ts @@ -28,6 +28,7 @@ import { invalidProtocolFrame } from './errors.js'; import { defineOperation } from './operation-spec.js'; import { decodeContextCompactionOutcome, decodeTurnSnapshot, type TurnSnapshot } from './turn.js'; import type { ContextCompactionOutcome } from '@maka/core/events'; +import type { SemanticPrefixContinuity } from '@maka/runtime/context-diagnostics'; export interface ContextDiagnosticsQueryInput { readonly sessionId: string; @@ -71,28 +72,7 @@ export interface ContextDiagnosticsComposition { readonly unlabelledToolBytes?: number; } -export type ContextDiagnosticsRequestPrefix = - | { - readonly status: 'no_predecessor' | 'unavailable'; - readonly previousSegmentCount: 0; - readonly preservedSegmentCount: 0; - } - | { - readonly status: 'preserved' | 'unknown'; - readonly previousSegmentCount: number; - readonly preservedSegmentCount: number; - } - | { - readonly status: 'diverged'; - readonly previousSegmentCount: number; - readonly preservedSegmentCount: number; - readonly firstDivergentSegment: { - readonly kind: 'tool_schema' | 'system_prompt' | 'message' | 'provider_options'; - readonly index: number; - readonly role?: string; - readonly label?: string; - }; - }; +export type ContextDiagnosticsRequestPrefix = SemanticPrefixContinuity; export type ContextDiagnosticsResult = | { @@ -319,14 +299,20 @@ function decodeContextDiagnosticsRequestPrefix(value: unknown): ContextDiagnosti index: requireCount(segment.index, 'requestPrefixSegmentIndex'), ...(segment.role === undefined ? {} - : { role: requireString(segment.role, 'requestPrefixSegmentRole', 256) }), + : { role: requireOptionalSegmentText(segment.role, 'requestPrefixSegmentRole') }), ...(segment.label === undefined ? {} - : { label: requireString(segment.label, 'requestPrefixSegmentLabel', 256) }), + : { label: requireOptionalSegmentText(segment.label, 'requestPrefixSegmentLabel') }), }, }; } +function requireOptionalSegmentText(value: unknown, label: string): string { + if (typeof value !== 'string' || value.length > 256) + throw invalidProtocolFrame(`Invalid ${label}`); + return value; +} + /** * The tool list is bounded on the wire for the same reason the evidence reads * are: a Host answer is built in memory, and a registry that grew without limit diff --git a/packages/runtime/src/context-diagnostics.ts b/packages/runtime/src/context-diagnostics.ts index d57137b19f..eca590ceda 100644 --- a/packages/runtime/src/context-diagnostics.ts +++ b/packages/runtime/src/context-diagnostics.ts @@ -45,6 +45,8 @@ import { type SemanticPrefixContinuity, } from './semantic-prefix-continuity.js'; +export type { SemanticPrefixContinuity } from './semantic-prefix-continuity.js'; + export type ContextDiagnosticsUnavailableReason = 'no_completed_request' | 'trace_unavailable'; export type ContextDiagnosticsSegmentKind = From 829553cc746ad09aa499165453c7736be0c95e76 Mon Sep 17 00:00:00 2001 From: AstroHan Date: Tue, 1 Sep 2026 02:19:38 +0800 Subject: [PATCH 13/19] refactor(test): simplify prefix continuity fixtures Generated-by: Codex --- .../semantic-prefix-continuity.test.ts | 188 +++++++----------- 1 file changed, 73 insertions(+), 115 deletions(-) diff --git a/packages/runtime/src/__tests__/semantic-prefix-continuity.test.ts b/packages/runtime/src/__tests__/semantic-prefix-continuity.test.ts index 8f8a05bca8..9b04a339b0 100644 --- a/packages/runtime/src/__tests__/semantic-prefix-continuity.test.ts +++ b/packages/runtime/src/__tests__/semantic-prefix-continuity.test.ts @@ -84,17 +84,8 @@ test('reports the first removed earlier segment', () => { test('uses the preceding physical retry as its durable baseline', async () => { const previous = attempt({ attemptId: 'attempt-0', attempt: 0 }); const current = attempt({ attemptId: 'attempt-1', attempt: 1 }); - assert.equal( - ( - await deriveAttemptSemanticPrefixContinuity({ - current, - currentProviderStateIdentity: PROVIDER_STATE, - currentSessionInline: true, - lineage: {}, - store: prefixStore([run('run-1', 'turn-1', PROVIDER_STATE)], [previous]), - }) - ).status, + await statusOf(current, [run('run-1', 'turn-1', PROVIDER_STATE)], [previous]), 'preserved', ); }); @@ -104,19 +95,12 @@ test('does not compare across provider execution identities', async () => { const current = attempt({ attemptId: 'current', runId: 'run-2', turnId: 'turn-2' }); assert.equal( - ( - await deriveAttemptSemanticPrefixContinuity({ - current, - currentProviderStateIdentity: PROVIDER_STATE, - currentSessionInline: true, - lineage: {}, - store: prefixStore( - [run('run-1', 'turn-1', OTHER_PROVIDER_STATE), run('run-2', 'turn-2', PROVIDER_STATE)], - [previous], - { runId: 'run-2', previousRootTurnId: 'turn-1' }, - ), - }) - ).status, + await statusOf( + current, + [run('run-1', 'turn-1', OTHER_PROVIDER_STATE), run('run-2', 'turn-2', PROVIDER_STATE)], + [previous], + { runId: 'run-2', previousRootTurnId: 'turn-1' }, + ), 'unavailable', ); }); @@ -132,19 +116,12 @@ test('does not compare attempts whose connection identity is missing', async () previous.connectionSlug = undefined; assert.equal( - ( - await deriveAttemptSemanticPrefixContinuity({ - current, - currentProviderStateIdentity: PROVIDER_STATE, - currentSessionInline: true, - lineage: {}, - store: prefixStore( - [run('run-1', 'turn-1', PROVIDER_STATE), run('run-2', 'turn-2', PROVIDER_STATE)], - [previous], - { runId: 'run-2', previousRootTurnId: 'turn-1' }, - ), - }) - ).status, + await statusOf( + current, + [run('run-1', 'turn-1', PROVIDER_STATE), run('run-2', 'turn-2', PROVIDER_STATE)], + [previous], + { runId: 'run-2', previousRootTurnId: 'turn-1' }, + ), 'unavailable', ); }); @@ -154,19 +131,12 @@ test('does not use an attempt whose durable turn disagrees with its run', async const current = attempt({ attemptId: 'current', runId: 'run-2', turnId: 'turn-2' }); assert.equal( - ( - await deriveAttemptSemanticPrefixContinuity({ - current, - currentProviderStateIdentity: PROVIDER_STATE, - currentSessionInline: true, - lineage: {}, - store: prefixStore( - [run('run-1', 'turn-1', PROVIDER_STATE), run('run-2', 'turn-2', PROVIDER_STATE)], - [previous], - { runId: 'run-2', previousRootTurnId: 'turn-1' }, - ), - }) - ).status, + await statusOf( + current, + [run('run-1', 'turn-1', PROVIDER_STATE), run('run-2', 'turn-2', PROVIDER_STATE)], + [previous], + { runId: 'run-2', previousRootTurnId: 'turn-1' }, + ), 'unavailable', ); }); @@ -180,22 +150,15 @@ test('does not choose between overlapping durable predecessor runs', async () => ]; assert.equal( - ( - await deriveAttemptSemanticPrefixContinuity({ - current, - currentProviderStateIdentity: PROVIDER_STATE, - currentSessionInline: true, - lineage: {}, - store: prefixStore( - headers, - [ - attempt({ attemptId: 'previous-1', runId: 'run-1', turnId: 'turn-1' }), - attempt({ attemptId: 'previous-2', runId: 'run-2', turnId: 'turn-1' }), - ], - { runId: 'run-3', previousRootTurnId: 'turn-1' }, - ), - }) - ).status, + await statusOf( + current, + headers, + [ + attempt({ attemptId: 'previous-1', runId: 'run-1', turnId: 'turn-1' }), + attempt({ attemptId: 'previous-2', runId: 'run-2', turnId: 'turn-1' }), + ], + { runId: 'run-3', previousRootTurnId: 'turn-1' }, + ), 'unavailable', ); }); @@ -205,26 +168,19 @@ test('uses the unique continuation tip for the previous root turn', async () => const current = attempt({ attemptId: 'current', runId: 'run-3', turnId: 'turn-2' }); assert.equal( - ( - await deriveAttemptSemanticPrefixContinuity({ - current, - currentProviderStateIdentity: PROVIDER_STATE, - currentSessionInline: true, - lineage: {}, - store: prefixStore( - [ - run('run-1', 'turn-1', PROVIDER_STATE), - run('run-2', 'turn-1', PROVIDER_STATE, { - parentRunId: 'run-1', - continuationSource: continuationSource('run-1'), - }), - run('run-3', 'turn-2', PROVIDER_STATE), - ], - [previous], - { runId: 'run-3', previousRootTurnId: 'turn-1' }, - ), - }) - ).status, + await statusOf( + current, + [ + run('run-1', 'turn-1', PROVIDER_STATE), + run('run-2', 'turn-1', PROVIDER_STATE, { + parentRunId: 'run-1', + continuationSource: continuationSource('run-1'), + }), + run('run-3', 'turn-2', PROVIDER_STATE), + ], + [previous], + { runId: 'run-3', previousRootTurnId: 'turn-1' }, + ), 'preserved', ); }); @@ -239,31 +195,15 @@ test('compares later local turns without inheriting a copied session baseline', ]; assert.equal( - ( - await deriveAttemptSemanticPrefixContinuity({ - current: previous, - currentProviderStateIdentity: PROVIDER_STATE, - currentSessionInline: true, - lineage: {}, - store: prefixStore(runs, [], { runId: 'run-1', previousRootTurnId: null }), - }) - ).status, + await statusOf(previous, runs, [], { runId: 'run-1', previousRootTurnId: null }), 'unavailable', ); assert.equal( - ( - await deriveAttemptSemanticPrefixContinuity({ - current, - currentProviderStateIdentity: PROVIDER_STATE, - currentSessionInline: true, - lineage: {}, - store: prefixStore(runs, [previous], { - runId: 'run-2', - previousRootTurnId: 'turn-1', - }), - }) - ).status, + await statusOf(current, runs, [previous], { + runId: 'run-2', + previousRootTurnId: 'turn-1', + }), 'preserved', ); }); @@ -271,20 +211,38 @@ test('compares later local turns without inheriting a copied session baseline', test('does not compare a child run against its parent session run', async () => { const previous = attempt({ attemptId: 'parent-attempt', runId: 'run-parent' }); const current = attempt({ attemptId: 'child-attempt', runId: 'run-child' }); - const input = { - current, - currentProviderStateIdentity: PROVIDER_STATE, - currentSessionInline: false, - lineage: { parentRunId: 'run-parent' }, - store: prefixStore( + assert.equal( + await statusOf( + current, [run('run-parent', 'turn-1', PROVIDER_STATE), run('run-child', 'turn-1', PROVIDER_STATE)], [previous], + undefined, + false, + { parentRunId: 'run-parent' }, ), - }; - - assert.equal((await deriveAttemptSemanticPrefixContinuity(input)).status, 'unavailable'); + 'unavailable', + ); }); +async function statusOf( + current: ModelCallAttempt, + runs: AgentRunHeader[], + attempts: ModelCallAttempt[], + admission?: { runId: string; previousRootTurnId: string | null }, + currentSessionInline = true, + lineage: { parentRunId?: string } = {}, +) { + return ( + await deriveAttemptSemanticPrefixContinuity({ + current, + currentProviderStateIdentity: PROVIDER_STATE, + currentSessionInline, + lineage, + store: prefixStore(runs, attempts, admission), + }) + ).status; +} + function observation(segments: PreparedRequestObservationSegment[]): PreparedRequestObservation { return { schemaVersion: 1, From 24d2a1b1650268a76458c0fb434289969c0582da Mon Sep 17 00:00:00 2001 From: AstroHan Date: Tue, 1 Sep 2026 02:27:18 +0800 Subject: [PATCH 14/19] feat(desktop): show provider cache usage Generated-by: Codex --- .../session-inspector-panel-model.test.ts | 36 +++++++++++++++++++ .../session-inspector-overview-model.ts | 21 +++++++++++ .../inspector/session-inspector-panel.tsx | 6 ++++ .../src/renderer/locales/conversation-copy.ts | 3 ++ 4 files changed, 66 insertions(+) diff --git a/apps/desktop/src/main/__tests__/session-inspector-panel-model.test.ts b/apps/desktop/src/main/__tests__/session-inspector-panel-model.test.ts index 1176849d0e..f7e2b407c8 100644 --- a/apps/desktop/src/main/__tests__/session-inspector-panel-model.test.ts +++ b/apps/desktop/src/main/__tests__/session-inspector-panel-model.test.ts @@ -139,6 +139,42 @@ test('does not estimate a cache-hit ratio from partial usage', () => { assert.equal(overview.cacheHitRate, undefined); }); +test('keeps provider cache read and write usage separate from prefix continuity', () => { + const overview = deriveInspectorOverviewModel(undefined, { + range: { from: 0, to: 1 }, + totalRequests: 1, + totalCostUsd: 0, + totalTokens: { + input: 10, + output: 0, + cacheMiss: 3, + cacheRead: 7, + cacheWrite: 4, + reasoning: 0, + total: 10, + }, + cacheHitRequests: 1, + cacheCreateRequests: 1, + errorRequests: 0, + provenance: { + coverage: { + attempts: 1, + pricedAttempts: 1, + unpricedAttempts: 0, + usageReportedAttempts: 1, + usagePartialAttempts: 0, + usageMissingAttempts: 0, + }, + legacyRecords: 0, + unreadableRecords: 0, + pendingRepairs: 0, + }, + }); + + assert.deepEqual(overview.providerCacheUsage, { readTokens: 7, writeTokens: 4 }); + assert.equal(overview.requestPrefix, undefined); +}); + test('passes the Runtime request-prefix verdict through without recomputing it', () => { const requestPrefix = { status: 'diverged' as const, diff --git a/apps/desktop/src/renderer/features/workbar/tools/inspector/session-inspector-overview-model.ts b/apps/desktop/src/renderer/features/workbar/tools/inspector/session-inspector-overview-model.ts index ea9063f0cd..ef00f4919f 100644 --- a/apps/desktop/src/renderer/features/workbar/tools/inspector/session-inspector-overview-model.ts +++ b/apps/desktop/src/renderer/features/workbar/tools/inspector/session-inspector-overview-model.ts @@ -140,6 +140,8 @@ export interface InspectorOverviewModel { * the run ledger; three statements of the same tokens is two too many. */ cacheHitRate?: number; + /** Provider-reported cache usage; independent of semantic continuity. */ + providerCacheUsage?: { readTokens: number; writeTokens: number }; /** Runtime-owned semantic prefix verdict; Desktop only presents it. */ requestPrefix?: Extract['requestPrefix']; } @@ -184,10 +186,12 @@ export function deriveInspectorOverviewModel( const composition = compositionState(diagnostics); const context = contextBudget(diagnostics); const cacheHitRate = usageCacheHitRate(usage); + const providerCacheUsage = usageProviderCache(usage); return { ...(context ? { context } : {}), ...(composition ? { composition } : {}), ...(cacheHitRate !== undefined ? { cacheHitRate } : {}), + ...(providerCacheUsage ? { providerCacheUsage } : {}), ...(diagnostics?.status === 'available' && diagnostics.requestPrefix && diagnostics.requestPrefix.status !== 'no_predecessor' @@ -196,6 +200,23 @@ export function deriveInspectorOverviewModel( }; } +function usageProviderCache( + usage: SessionUsageSummary | undefined, +): InspectorOverviewModel['providerCacheUsage'] { + if ( + !usage || + usage.provenance?.coverage.usagePartialAttempts || + usage.provenance?.coverage.usageMissingAttempts || + (usage.totalTokens.cacheRead === 0 && usage.totalTokens.cacheWrite === 0) + ) { + return undefined; + } + return { + readTokens: usage.totalTokens.cacheRead, + writeTokens: usage.totalTokens.cacheWrite, + }; +} + function usageCacheHitRate(usage: SessionUsageSummary | undefined): number | undefined { if (!usage || usage.totalTokens.input === 0) return undefined; if ( diff --git a/apps/desktop/src/renderer/features/workbar/tools/inspector/session-inspector-panel.tsx b/apps/desktop/src/renderer/features/workbar/tools/inspector/session-inspector-panel.tsx index 498cdadbbc..4e1acbaaa2 100644 --- a/apps/desktop/src/renderer/features/workbar/tools/inspector/session-inspector-panel.tsx +++ b/apps/desktop/src/renderer/features/workbar/tools/inspector/session-inspector-panel.tsx @@ -288,6 +288,12 @@ function InspectorOverview(props: { value={formatPercent(overview.cacheHitRate)} /> )} + {overview.providerCacheUsage && ( + + )} {copy.costEstimateHelp} diff --git a/apps/desktop/src/renderer/locales/conversation-copy.ts b/apps/desktop/src/renderer/locales/conversation-copy.ts index a9a301e0b8..23d49803ac 100644 --- a/apps/desktop/src/renderer/locales/conversation-copy.ts +++ b/apps/desktop/src/renderer/locales/conversation-copy.ts @@ -262,6 +262,7 @@ export interface DesktopConversationCopy { }; /** The three figures a reader opens this tab for, as headline stats. */ cacheHit: string; + providerCache: string; requestPrefix: { preserved: (preserved: number, previous: number) => string; diverged: (segment: string, index: number) => string; @@ -605,6 +606,7 @@ const COPY = { free: '剩余', }, cacheHit: '缓存命中率', + providerCache: 'Provider 缓存读取 / 写入', requestPrefix: { preserved: (preserved, previous) => `请求前缀 ${preserved}/${previous} 保持`, diverged: (segment, index) => `请求前缀在${segment} ${index}处分叉`, @@ -851,6 +853,7 @@ const COPY = { free: 'Remaining', }, cacheHit: 'Cache hit rate', + providerCache: 'Provider cache read / write', requestPrefix: { preserved: (preserved, previous) => `Request prefix ${preserved}/${previous} preserved`, From 69d64457f6fa30dd085a7e8287f15d2d6731ea7b Mon Sep 17 00:00:00 2001 From: AstroHan Date: Tue, 1 Sep 2026 02:34:31 +0800 Subject: [PATCH 15/19] fix(runtime): validate canonical prefix anchors Generated-by: Codex --- .../src/__tests__/context-diagnostics.test.ts | 30 ++++++++++- .../semantic-prefix-continuity.test.ts | 52 +++++++++++++++++++ packages/runtime/src/context-diagnostics.ts | 9 +++- .../runtime/src/semantic-prefix-continuity.ts | 32 ++++++++---- 4 files changed, 112 insertions(+), 11 deletions(-) diff --git a/packages/runtime/src/__tests__/context-diagnostics.test.ts b/packages/runtime/src/__tests__/context-diagnostics.test.ts index 5926df6abe..c04168c275 100644 --- a/packages/runtime/src/__tests__/context-diagnostics.test.ts +++ b/packages/runtime/src/__tests__/context-diagnostics.test.ts @@ -247,6 +247,34 @@ test('a failed call does not replace the last good snapshot', async () => { } }); +test('an identity-mismatched canonical event cannot become the latest request', async () => { + const root = await mkdtemp(join(tmpdir(), 'maka-context-diagnostics-')); + try { + const writer = createSqliteAgentRunStore(root); + await writer.createRun(runHeader('run-1', 1)); + await writer.appendEvent( + 'session-1', + 'run-1', + meteringEvent('run-1', 'attempt-1', 10, 'model-valid', 40, 200), + ); + await writer.appendEvent( + 'session-1', + 'run-1', + meteringEvent('run-1', 'attempt-2', 20, 'model-mismatched', 40, 200, { + runId: 'another-run', + }), + ); + + const diagnostics = await readLatestContextDiagnostics(writer, 'session-1'); + + assert.equal(diagnostics.status, 'available'); + if (diagnostics.status !== 'available') return; + assert.equal(diagnostics.modelId, 'model-valid'); + } finally { + await rm(root, { recursive: true, force: true }); + } +}); + test("a subagent's run never becomes the session's context", async () => { const root = await mkdtemp(join(tmpdir(), 'maka-context-diagnostics-')); try { @@ -1138,7 +1166,7 @@ function meteringEvent( const turnId = `turn-${runId}`; return { type: 'model_call_attempt_recorded', - id: `metering-${attemptId}`, + id: attemptId, runId, sessionId: 'session-1', turnId, diff --git a/packages/runtime/src/__tests__/semantic-prefix-continuity.test.ts b/packages/runtime/src/__tests__/semantic-prefix-continuity.test.ts index 9b04a339b0..99145b5ddd 100644 --- a/packages/runtime/src/__tests__/semantic-prefix-continuity.test.ts +++ b/packages/runtime/src/__tests__/semantic-prefix-continuity.test.ts @@ -81,6 +81,58 @@ test('reports the first removed earlier segment', () => { }); }); +test('reports the current segment at the first middle deletion', () => { + const previous = observation([ + message(0, 'system'), + message(1, 'user-1'), + message(2, 'assistant-1'), + ]); + const current = observation([message(0, 'system'), message(2, 'assistant-1')]); + + assert.deepEqual(deriveSemanticPrefixContinuity(current, previous), { + status: 'diverged', + previousSegmentCount: 3, + preservedSegmentCount: 1, + firstDivergentSegment: { kind: 'message', index: 2 }, + }); +}); + +test('reports an inserted segment as the first divergence', () => { + const previous = observation([message(0, 'system'), message(2, 'assistant-1')]); + const current = observation([ + message(0, 'system'), + message(1, 'inserted-user'), + message(2, 'assistant-1'), + ]); + + assert.deepEqual(deriveSemanticPrefixContinuity(current, previous), { + status: 'diverged', + previousSegmentCount: 2, + preservedSegmentCount: 1, + firstDivergentSegment: { kind: 'message', index: 1 }, + }); +}); + +test('reports the first moved segment after a reorder', () => { + const previous = observation([ + message(0, 'system'), + message(1, 'user-1'), + message(2, 'assistant-1'), + ]); + const current = observation([ + message(0, 'system'), + message(2, 'assistant-1'), + message(1, 'user-1'), + ]); + + assert.deepEqual(deriveSemanticPrefixContinuity(current, previous), { + status: 'diverged', + previousSegmentCount: 3, + preservedSegmentCount: 1, + firstDivergentSegment: { kind: 'message', index: 2 }, + }); +}); + test('uses the preceding physical retry as its durable baseline', async () => { const previous = attempt({ attemptId: 'attempt-0', attempt: 0 }); const current = attempt({ attemptId: 'attempt-1', attempt: 1 }); diff --git a/packages/runtime/src/context-diagnostics.ts b/packages/runtime/src/context-diagnostics.ts index eca590ceda..0d77f87192 100644 --- a/packages/runtime/src/context-diagnostics.ts +++ b/packages/runtime/src/context-diagnostics.ts @@ -42,6 +42,7 @@ import { } from './history-compact-checkpoint.js'; import { deriveAttemptSemanticPrefixContinuity, + isCanonicalAttemptForRun, type SemanticPrefixContinuity, } from './semantic-prefix-continuity.js'; @@ -419,7 +420,13 @@ function meteringAnchor(event: AgentRunEvent, run: AgentRunHeader): MeteringAnch } catch { return undefined; } - if (attempt.callKind !== 'main' || attempt.status !== 'completed') return undefined; + if ( + attempt.callKind !== 'main' || + attempt.status !== 'completed' || + !isCanonicalAttemptForRun(event, attempt, run) + ) { + return undefined; + } const composition = attempt.requestObservation ? foldPromptComposition(attempt.requestObservation.segments) : undefined; diff --git a/packages/runtime/src/semantic-prefix-continuity.ts b/packages/runtime/src/semantic-prefix-continuity.ts index 92a9d3fd4a..7edbe1aace 100644 --- a/packages/runtime/src/semantic-prefix-continuity.ts +++ b/packages/runtime/src/semantic-prefix-continuity.ts @@ -23,7 +23,12 @@ import type { PreparedRequestObservationSegment, } from '@maka/core/model-call-attempt'; import { decodeModelCallAttempt } from '@maka/core/model-call-attempt'; -import { isSessionInlineRun, type AgentRunHeader, type AgentRunStore } from '@maka/core/agent-run'; +import { + isSessionInlineRun, + type AgentRunEvent, + type AgentRunHeader, + type AgentRunStore, +} from '@maka/core/agent-run'; export type SemanticPrefixContinuity = | { @@ -249,14 +254,7 @@ async function attemptsFor(store: PrefixStore, run: AgentRunHeader): Promise Date: Tue, 1 Sep 2026 09:50:20 +0800 Subject: [PATCH 16/19] chore(desktop): refresh Astryx surface inventory Generated-by: Codex --- docs/astryx-surface-file-inventory.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/astryx-surface-file-inventory.md b/docs/astryx-surface-file-inventory.md index f6ed5c7566..8f82bff7aa 100644 --- a/docs/astryx-surface-file-inventory.md +++ b/docs/astryx-surface-file-inventory.md @@ -63,7 +63,7 @@ Wiki bar: Design Conventions · API Use-the-System · Theming · Container Paddi | `apps/desktop/src/renderer/features/workbar/tools/artifacts/artifact-preview-registry-shell.tsx` | shell-chrome-or-panel | Banner, Button, Spinner | aligned — uses Astryx (Banner, Button, Spinner) | aligned | | `apps/desktop/src/renderer/features/workbar/tools/artifacts/artifact-preview.tsx` | shell-chrome-or-panel | Banner, Button, CodeBlock, Spinner | aligned — uses Astryx (Banner, Button, CodeBlock, Spinner) | aligned | | `apps/desktop/src/renderer/features/workbar/tools/browser/browser-panel.tsx` | shell-chrome-or-panel | EmptyState, IconButton, TextInput, Toolbar, Tooltip | aligned — uses Astryx (EmptyState, IconButton, TextInput, Toolbar, Tooltip) | aligned | -| `apps/desktop/src/renderer/features/workbar/tools/inspector/session-inspector-panel.tsx` | shell-chrome-or-panel | Banner, Button, EmptyState, Heading, Section, Text, VStack | aligned — uses Astryx (Banner, Button, EmptyState, Heading, Section, Text, VStack) | aligned | +| `apps/desktop/src/renderer/features/workbar/tools/inspector/session-inspector-panel.tsx` | shell-chrome-or-panel | Badge, Banner, Button, EmptyState, Heading, Section, Text, VStack | aligned — uses Astryx (Badge, Banner, Button, EmptyState, Heading, Section, Text, VStack) | aligned | | `apps/desktop/src/renderer/features/workbar/tools/review/session-review-panel.tsx` | shell-chrome-or-panel | Banner, Button, Collapsible, CollapsibleGroup, EmptyState, HStack, Section, Skeleton, Text, VStack | aligned — uses Astryx (Banner, Button, Collapsible, CollapsibleGroup, EmptyState, HStack, Section, Skeleton) | aligned | | `apps/desktop/src/renderer/features/workbar/tools/side-chat/quote-companion-panel.tsx` | shell-chrome-or-panel | Banner, Spinner | aligned — uses Astryx (Banner, Spinner) | aligned | | `apps/desktop/src/renderer/features/workbar/tools/terminal/session-terminal-panel.tsx` | shell-chrome-or-panel | Banner, EmptyState | aligned — uses Astryx (Banner, EmptyState) | aligned | From 55b6335d1bbc85dd663905c496507edf4a0d85da Mon Sep 17 00:00:00 2001 From: AstroHan Date: Tue, 1 Sep 2026 09:55:57 +0800 Subject: [PATCH 17/19] refactor(runtime): clarify request preservation Generated-by: Codex --- .../session-inspector-composition.test.ts | 10 +- .../session-inspector-panel-model.test.ts | 16 +-- .../main/__tests__/use-session-trace.test.ts | 4 +- .../src/renderer/features/workbar/testing.ts | 2 +- .../session-inspector-overview-model.ts | 10 +- .../inspector/session-inspector-panel.tsx | 40 ++++---- .../src/renderer/locales/conversation-copy.ts | 18 ++-- .../src/__tests__/context-protocol.test.ts | 4 +- packages/runtime-host/src/protocol/context.ts | 90 +++-------------- .../src/__tests__/context-diagnostics.test.ts | 4 +- .../__tests__/latest-context-commit.test.ts | 16 +-- ...y.test.ts => request-preservation.test.ts} | 39 ++++---- packages/runtime/src/agent-run.ts | 9 +- packages/runtime/src/context-diagnostics.ts | 21 ++-- .../runtime/src/latest-context-snapshot.ts | 11 +-- ...-continuity.ts => request-preservation.ts} | 98 +++++++++---------- 16 files changed, 155 insertions(+), 237 deletions(-) rename packages/runtime/src/__tests__/{semantic-prefix-continuity.test.ts => request-preservation.test.ts} (91%) rename packages/runtime/src/{semantic-prefix-continuity.ts => request-preservation.ts} (84%) diff --git a/apps/desktop/src/main/__tests__/session-inspector-composition.test.ts b/apps/desktop/src/main/__tests__/session-inspector-composition.test.ts index 570bd5ead6..31fed020b6 100644 --- a/apps/desktop/src/main/__tests__/session-inspector-composition.test.ts +++ b/apps/desktop/src/main/__tests__/session-inspector-composition.test.ts @@ -23,7 +23,7 @@ import { createElement } from 'react'; import { renderToStaticMarkup } from 'react-dom/server'; import { InspectorCompositionSection, - InspectorRequestPrefixBadge, + InspectorRequestPreservationBadge, } from '../../renderer/features/workbar/testing.js'; import { getDesktopConversationCopy } from '../../renderer/locales/conversation-copy.js'; @@ -66,9 +66,9 @@ test('maps each request-composition category to the same colour in the chart and test('renders the Runtime divergence location as a compact badge', () => { const markup = renderToStaticMarkup( - createElement(InspectorRequestPrefixBadge, { + createElement(InspectorRequestPreservationBadge, { copy: getDesktopConversationCopy('en').inspector, - requestPrefix: { + requestPreservation: { status: 'diverged', previousSegmentCount: 8, preservedSegmentCount: 2, @@ -77,6 +77,6 @@ test('renders the Runtime divergence location as a compact badge', () => { }), ); - assert.match(markup, /Request prefix diverged at message 3/); - assert.match(markup, /data-maka-contract="request-prefix-continuity"/); + assert.match(markup, /Previous request changed at message 3/); + assert.match(markup, /data-maka-contract="request-preservation"/); }); diff --git a/apps/desktop/src/main/__tests__/session-inspector-panel-model.test.ts b/apps/desktop/src/main/__tests__/session-inspector-panel-model.test.ts index f7e2b407c8..8235b60da3 100644 --- a/apps/desktop/src/main/__tests__/session-inspector-panel-model.test.ts +++ b/apps/desktop/src/main/__tests__/session-inspector-panel-model.test.ts @@ -139,7 +139,7 @@ test('does not estimate a cache-hit ratio from partial usage', () => { assert.equal(overview.cacheHitRate, undefined); }); -test('keeps provider cache read and write usage separate from prefix continuity', () => { +test('keeps provider cache read and write usage separate from request preservation', () => { const overview = deriveInspectorOverviewModel(undefined, { range: { from: 0, to: 1 }, totalRequests: 1, @@ -172,11 +172,11 @@ test('keeps provider cache read and write usage separate from prefix continuity' }); assert.deepEqual(overview.providerCacheUsage, { readTokens: 7, writeTokens: 4 }); - assert.equal(overview.requestPrefix, undefined); + assert.equal(overview.requestPreservation, undefined); }); -test('passes the Runtime request-prefix verdict through without recomputing it', () => { - const requestPrefix = { +test('passes the Runtime request-preservation verdict through without recomputing it', () => { + const requestPreservation = { status: 'diverged' as const, previousSegmentCount: 8, preservedSegmentCount: 2, @@ -187,10 +187,10 @@ test('passes the Runtime request-prefix verdict through without recomputing it', providerId: 'anthropic', modelId: 'claude', completedAt: 10, - requestPrefix, + requestPreservation, }); - assert.equal(overview.requestPrefix, requestPrefix); + assert.equal(overview.requestPreservation, requestPreservation); }); test('does not create an empty overview for a request without a predecessor', () => { @@ -199,14 +199,14 @@ test('does not create an empty overview for a request without a predecessor', () providerId: 'anthropic', modelId: 'claude', completedAt: 10, - requestPrefix: { + requestPreservation: { status: 'no_predecessor', previousSegmentCount: 0, preservedSegmentCount: 0, }, }); - assert.equal(overview.requestPrefix, undefined); + assert.equal(overview.requestPreservation, undefined); }); test('derives per-turn cost only from priced model-call step totals', () => { diff --git a/apps/desktop/src/main/__tests__/use-session-trace.test.ts b/apps/desktop/src/main/__tests__/use-session-trace.test.ts index 3a32cc6d8c..320c191ce8 100644 --- a/apps/desktop/src/main/__tests__/use-session-trace.test.ts +++ b/apps/desktop/src/main/__tests__/use-session-trace.test.ts @@ -317,7 +317,7 @@ describe('useSessionTrace', () => { assert.equal(harness.reads.length, 2, 'a closing burst is one re-read, not three'); }); - it('does not keep an earlier request-prefix verdict while its refresh fails', async () => { + it('does not keep an earlier request-preservation verdict while its refresh fails', async () => { const { root } = installReactRenderer(); const harness = createTraceHarness({ context: async (_sessionId, readIndex) => { @@ -329,7 +329,7 @@ describe('useSessionTrace', () => { providerId: 'anthropic', modelId: 'model', completedAt: 1, - requestPrefix: { + requestPreservation: { status: 'preserved', previousSegmentCount: 1, preservedSegmentCount: 1, diff --git a/apps/desktop/src/renderer/features/workbar/testing.ts b/apps/desktop/src/renderer/features/workbar/testing.ts index ddc51e71f5..d35daaf983 100644 --- a/apps/desktop/src/renderer/features/workbar/testing.ts +++ b/apps/desktop/src/renderer/features/workbar/testing.ts @@ -35,7 +35,7 @@ export * from './tools/inspector/session-inspector-panel-model.js'; export { compactNumberFormatter, InspectorCompositionSection, - InspectorRequestPrefixBadge, + InspectorRequestPreservationBadge, } from './tools/inspector/session-inspector-panel.js'; export * from './tools/inspector/session-inspector-overview-model.js'; export * from './tools/side-chat/quote-companion-panel-state.js'; diff --git a/apps/desktop/src/renderer/features/workbar/tools/inspector/session-inspector-overview-model.ts b/apps/desktop/src/renderer/features/workbar/tools/inspector/session-inspector-overview-model.ts index ef00f4919f..d6e4839f86 100644 --- a/apps/desktop/src/renderer/features/workbar/tools/inspector/session-inspector-overview-model.ts +++ b/apps/desktop/src/renderer/features/workbar/tools/inspector/session-inspector-overview-model.ts @@ -142,8 +142,8 @@ export interface InspectorOverviewModel { cacheHitRate?: number; /** Provider-reported cache usage; independent of semantic continuity. */ providerCacheUsage?: { readTokens: number; writeTokens: number }; - /** Runtime-owned semantic prefix verdict; Desktop only presents it. */ - requestPrefix?: Extract['requestPrefix']; + /** Runtime-owned request-preservation verdict; Desktop only presents it. */ + requestPreservation?: Extract['requestPreservation']; } export function estimatedSessionCost( @@ -193,9 +193,9 @@ export function deriveInspectorOverviewModel( ...(cacheHitRate !== undefined ? { cacheHitRate } : {}), ...(providerCacheUsage ? { providerCacheUsage } : {}), ...(diagnostics?.status === 'available' && - diagnostics.requestPrefix && - diagnostics.requestPrefix.status !== 'no_predecessor' - ? { requestPrefix: diagnostics.requestPrefix } + diagnostics.requestPreservation && + diagnostics.requestPreservation.status !== 'no_predecessor' + ? { requestPreservation: diagnostics.requestPreservation } : {}), }; } diff --git a/apps/desktop/src/renderer/features/workbar/tools/inspector/session-inspector-panel.tsx b/apps/desktop/src/renderer/features/workbar/tools/inspector/session-inspector-panel.tsx index 4e1acbaaa2..c0f7a7c6ac 100644 --- a/apps/desktop/src/renderer/features/workbar/tools/inspector/session-inspector-panel.tsx +++ b/apps/desktop/src/renderer/features/workbar/tools/inspector/session-inspector-panel.tsx @@ -149,7 +149,7 @@ export function SessionInspectorPanel(props: { sessionId: string; active: boolea {copy.summaryUnavailable} )} - {(snapshot.summary || overview.context || overview.composition || overview.requestPrefix) && ( + {(snapshot.summary || overview.context || overview.composition || overview.requestPreservation) && ( - {overview.requestPrefix && ( - + {overview.requestPreservation && ( + )} {props.showTotals && ( @@ -320,41 +320,41 @@ function InspectorOverview(props: { ); } -export function InspectorRequestPrefixBadge(props: { +export function InspectorRequestPreservationBadge(props: { copy: InspectorCopy; - requestPrefix: NonNullable['requestPrefix']>; + requestPreservation: NonNullable['requestPreservation']>; }) { - const { requestPrefix, copy } = props; - if (requestPrefix.status === 'no_predecessor') return null; - if (requestPrefix.status === 'preserved') { + const { requestPreservation, copy } = props; + if (requestPreservation.status === 'no_predecessor') return null; + if (requestPreservation.status === 'preserved') { return ( ); } - if (requestPrefix.status === 'diverged') { + if (requestPreservation.status === 'diverged') { return ( ); } return ( ); } diff --git a/apps/desktop/src/renderer/locales/conversation-copy.ts b/apps/desktop/src/renderer/locales/conversation-copy.ts index 23d49803ac..18f316620c 100644 --- a/apps/desktop/src/renderer/locales/conversation-copy.ts +++ b/apps/desktop/src/renderer/locales/conversation-copy.ts @@ -263,7 +263,7 @@ export interface DesktopConversationCopy { /** The three figures a reader opens this tab for, as headline stats. */ cacheHit: string; providerCache: string; - requestPrefix: { + requestPreservation: { preserved: (preserved: number, previous: number) => string; diverged: (segment: string, index: number) => string; cannotJudge: string; @@ -607,10 +607,10 @@ const COPY = { }, cacheHit: '缓存命中率', providerCache: 'Provider 缓存读取 / 写入', - requestPrefix: { - preserved: (preserved, previous) => `请求前缀 ${preserved}/${previous} 保持`, - diverged: (segment, index) => `请求前缀在${segment} ${index}处分叉`, - cannotJudge: '请求前缀无法判断', + requestPreservation: { + preserved: (preserved, previous) => `上次请求内容已保留 ${preserved}/${previous}`, + diverged: (segment, index) => `上次请求从${segment} ${index}起有变化`, + cannotJudge: '无法比较上次请求', segment: { tool_schema: '工具', system_prompt: '系统提示', @@ -854,11 +854,11 @@ const COPY = { }, cacheHit: 'Cache hit rate', providerCache: 'Provider cache read / write', - requestPrefix: { + requestPreservation: { preserved: (preserved, previous) => - `Request prefix ${preserved}/${previous} preserved`, - diverged: (segment, index) => `Request prefix diverged at ${segment} ${index}`, - cannotJudge: 'Request prefix unavailable', + `Previous request ${preserved}/${previous} preserved`, + diverged: (segment, index) => `Previous request changed at ${segment} ${index}`, + cannotJudge: 'Previous request could not be compared', segment: { tool_schema: 'tool', system_prompt: 'system prompt', diff --git a/packages/runtime-host/src/__tests__/context-protocol.test.ts b/packages/runtime-host/src/__tests__/context-protocol.test.ts index 88d7ead1b5..d557db1917 100644 --- a/packages/runtime-host/src/__tests__/context-protocol.test.ts +++ b/packages/runtime-host/src/__tests__/context-protocol.test.ts @@ -71,7 +71,7 @@ test('context operations preserve bounded exact wire values', () => { turnCount: 2, estimatedTokens: 12, }, - requestPrefix: { + requestPreservation: { status: 'diverged', previousSegmentCount: 8, preservedSegmentCount: 2, @@ -102,7 +102,7 @@ test('context operations preserve bounded exact wire values', () => { turnCount: 2, estimatedTokens: 12, }, - requestPrefix: { + requestPreservation: { status: 'diverged', previousSegmentCount: 8, preservedSegmentCount: 2, diff --git a/packages/runtime-host/src/protocol/context.ts b/packages/runtime-host/src/protocol/context.ts index 8ae912f8f2..456d2cb2b0 100644 --- a/packages/runtime-host/src/protocol/context.ts +++ b/packages/runtime-host/src/protocol/context.ts @@ -28,7 +28,7 @@ import { invalidProtocolFrame } from './errors.js'; import { defineOperation } from './operation-spec.js'; import { decodeContextCompactionOutcome, decodeTurnSnapshot, type TurnSnapshot } from './turn.js'; import type { ContextCompactionOutcome } from '@maka/core/events'; -import type { SemanticPrefixContinuity } from '@maka/runtime/context-diagnostics'; +import { isRequestPreservation, type RequestPreservation } from '@maka/runtime/context-diagnostics'; export interface ContextDiagnosticsQueryInput { readonly sessionId: string; @@ -72,7 +72,7 @@ export interface ContextDiagnosticsComposition { readonly unlabelledToolBytes?: number; } -export type ContextDiagnosticsRequestPrefix = SemanticPrefixContinuity; +export type ContextDiagnosticsRequestPreservation = RequestPreservation; export type ContextDiagnosticsResult = | { @@ -102,7 +102,7 @@ export type ContextDiagnosticsResult = readonly estimatedTokens: number; }; /** Runtime-owned verdict; Host and Desktop must not recompute it. */ - readonly requestPrefix?: ContextDiagnosticsRequestPrefix; + readonly requestPreservation?: ContextDiagnosticsRequestPreservation; }; const QUERY_ERRORS = [ @@ -179,7 +179,7 @@ function decodeContextDiagnosticsResult(value: unknown): ContextDiagnosticsResul 'contextWindow', 'composition', 'compaction', - 'requestPrefix', + 'requestPreservation', ], ); if (record.status === 'unavailable') { @@ -208,7 +208,7 @@ function decodeContextDiagnosticsResult(value: unknown): ContextDiagnosticsResul 'contextWindow', 'composition', 'compaction', - 'requestPrefix', + 'requestPreservation', ], ); return { @@ -236,80 +236,22 @@ function decodeContextDiagnosticsResult(value: unknown): ContextDiagnosticsResul ...(available.compaction === undefined ? {} : { compaction: decodeContextDiagnosticsCompaction(available.compaction) }), - ...(available.requestPrefix === undefined + ...(available.requestPreservation === undefined ? {} - : { requestPrefix: decodeContextDiagnosticsRequestPrefix(available.requestPrefix) }), + : { + requestPreservation: decodeContextDiagnosticsRequestPreservation( + available.requestPreservation, + ), + }), }; } -function decodeContextDiagnosticsRequestPrefix(value: unknown): ContextDiagnosticsRequestPrefix { - const prefix = requireShapedRecord( - value, - 'Context diagnostics request prefix', - ['status', 'previousSegmentCount', 'preservedSegmentCount'], - ['firstDivergentSegment'], - ); - const previousSegmentCount = requireCount(prefix.previousSegmentCount, 'previousSegmentCount'); - const preservedSegmentCount = requireCount(prefix.preservedSegmentCount, 'preservedSegmentCount'); - if (preservedSegmentCount > previousSegmentCount) { - throw invalidProtocolFrame('Invalid request prefix segment counts'); - } - if (prefix.status === 'no_predecessor' || prefix.status === 'unavailable') { - if ( - prefix.firstDivergentSegment !== undefined || - previousSegmentCount !== 0 || - preservedSegmentCount !== 0 - ) { - throw invalidProtocolFrame('Invalid unavailable request prefix'); - } - return { status: prefix.status, previousSegmentCount: 0, preservedSegmentCount: 0 }; - } - if (prefix.status === 'preserved' || prefix.status === 'unknown') { - if ( - prefix.firstDivergentSegment !== undefined || - (prefix.status === 'preserved' && preservedSegmentCount !== previousSegmentCount) - ) { - throw invalidProtocolFrame('Invalid request prefix result'); - } - return { status: prefix.status, previousSegmentCount, preservedSegmentCount }; - } - if (prefix.status !== 'diverged' || prefix.firstDivergentSegment === undefined) { - throw invalidProtocolFrame('Invalid request prefix status'); - } - const segment = requireShapedRecord( - prefix.firstDivergentSegment, - 'Request prefix divergent segment', - ['kind', 'index'], - ['role', 'label'], - ); - if ( - segment.kind !== 'tool_schema' && - segment.kind !== 'system_prompt' && - segment.kind !== 'message' && - segment.kind !== 'provider_options' - ) { - throw invalidProtocolFrame('Invalid request prefix segment kind'); +function decodeContextDiagnosticsRequestPreservation( + value: unknown, +): ContextDiagnosticsRequestPreservation { + if (!isRequestPreservation(value)) { + throw invalidProtocolFrame('Invalid context diagnostics request preservation'); } - return { - status: 'diverged', - previousSegmentCount, - preservedSegmentCount, - firstDivergentSegment: { - kind: segment.kind, - index: requireCount(segment.index, 'requestPrefixSegmentIndex'), - ...(segment.role === undefined - ? {} - : { role: requireOptionalSegmentText(segment.role, 'requestPrefixSegmentRole') }), - ...(segment.label === undefined - ? {} - : { label: requireOptionalSegmentText(segment.label, 'requestPrefixSegmentLabel') }), - }, - }; -} - -function requireOptionalSegmentText(value: unknown, label: string): string { - if (typeof value !== 'string' || value.length > 256) - throw invalidProtocolFrame(`Invalid ${label}`); return value; } diff --git a/packages/runtime/src/__tests__/context-diagnostics.test.ts b/packages/runtime/src/__tests__/context-diagnostics.test.ts index c04168c275..02e565446b 100644 --- a/packages/runtime/src/__tests__/context-diagnostics.test.ts +++ b/packages/runtime/src/__tests__/context-diagnostics.test.ts @@ -70,7 +70,7 @@ test('rejects v2 snapshots that the canonical writer cannot produce', () => { }, { ...base, - requestPrefix: { + requestPreservation: { status: 'diverged', previousSegmentCount: 1, preservedSegmentCount: 0, @@ -79,7 +79,7 @@ test('rejects v2 snapshots that the canonical writer cannot produce', () => { }, { ...base, - requestPrefix: { + requestPreservation: { status: 'preserved', previousSegmentCount: 1, preservedSegmentCount: 1, diff --git a/packages/runtime/src/__tests__/latest-context-commit.test.ts b/packages/runtime/src/__tests__/latest-context-commit.test.ts index 5344412cca..5666a5b875 100644 --- a/packages/runtime/src/__tests__/latest-context-commit.test.ts +++ b/packages/runtime/src/__tests__/latest-context-commit.test.ts @@ -48,7 +48,7 @@ import { BackendRegistry, SessionManager } from '../session-manager.js'; import { readLatestContextDiagnostics } from '../context-diagnostics.js'; import { createTestAiSdkBackend } from './execution-boundary-test-helpers.js'; -test('real consecutive sends seal a preserved prefix verdict that survives restart', async () => { +test('real consecutive sends preserve the previous request across restart', async () => { // Tracker → backend → the kernel seam a backend is actually built with → // AgentRun → the storage transaction. Every layer in that list once had a // signature that compiled while dropping the row, and no test crossed all of @@ -137,7 +137,7 @@ test('real consecutive sends seal a preserved prefix verdict that survives resta diagnostics.composition?.segments.some((segment) => segment.kind === 'messages'), 'and the request describes what it was made of', ); - assert.deepEqual(diagnostics.requestPrefix, { + assert.deepEqual(diagnostics.requestPreservation, { status: 'no_predecessor', previousSegmentCount: 0, preservedSegmentCount: 0, @@ -155,11 +155,11 @@ test('real consecutive sends seal a preserved prefix verdict that survives resta const successor = await readLatestContextDiagnostics(runStore, session.id); assert.equal(successor.status, 'available'); if (successor.status !== 'available') return; - assert.ok(successor.requestPrefix); - assert.equal(successor.requestPrefix.status, 'preserved'); + assert.ok(successor.requestPreservation); + assert.equal(successor.requestPreservation.status, 'preserved'); assert.equal( - successor.requestPrefix.preservedSegmentCount, - successor.requestPrefix.previousSegmentCount, + successor.requestPreservation.preservedSegmentCount, + successor.requestPreservation.previousSegmentCount, ); await manager.stopSession(session.id, { source: 'stop_button' }); @@ -188,7 +188,7 @@ test('real consecutive sends seal a preserved prefix verdict that survives resta const restarted = await readLatestContextDiagnostics(reopened, session.id); assert.equal(restarted.status, 'available'); if (restarted.status !== 'available') return; - assert.deepEqual(restarted.requestPrefix, successor.requestPrefix); + assert.deepEqual(restarted.requestPreservation, successor.requestPreservation); let coldScans = 0; const cold = await readLatestContextDiagnostics( @@ -210,7 +210,7 @@ test('real consecutive sends seal a preserved prefix verdict that survives resta assert.equal(cold.status, 'available'); if (cold.status !== 'available') return; assert.deepEqual(cold.composition, successor.composition); - assert.deepEqual(cold.requestPrefix, successor.requestPrefix); + assert.deepEqual(cold.requestPreservation, successor.requestPreservation); } finally { reopened.close?.(); } diff --git a/packages/runtime/src/__tests__/semantic-prefix-continuity.test.ts b/packages/runtime/src/__tests__/request-preservation.test.ts similarity index 91% rename from packages/runtime/src/__tests__/semantic-prefix-continuity.test.ts rename to packages/runtime/src/__tests__/request-preservation.test.ts index 99145b5ddd..a77da048b1 100644 --- a/packages/runtime/src/__tests__/semantic-prefix-continuity.test.ts +++ b/packages/runtime/src/__tests__/request-preservation.test.ts @@ -27,9 +27,9 @@ import type { } from '@maka/core/model-call-attempt'; import type { AgentRunEvent, AgentRunHeader } from '@maka/core/agent-run'; import { - deriveAttemptSemanticPrefixContinuity, - deriveSemanticPrefixContinuity, -} from '../semantic-prefix-continuity.js'; + deriveAttemptRequestPreservation, + deriveRequestPreservation, +} from '../request-preservation.js'; test('keeps every earlier cacheable segment when the current request only appends', () => { const previous = observation([message(0, 'system'), message(1, 'user-1')]); @@ -39,7 +39,7 @@ test('keeps every earlier cacheable segment when the current request only append message(2, 'assistant-1'), ]); - assert.deepEqual(deriveSemanticPrefixContinuity(current, previous), { + assert.deepEqual(deriveRequestPreservation(current, previous), { status: 'preserved', previousSegmentCount: 2, preservedSegmentCount: 2, @@ -50,7 +50,7 @@ test('does not treat opaque digests as evidence of divergence', () => { const previous = observation([{ ...message(0, 'redacted-a'), comparison: 'opaque' }]); const current = observation([{ ...message(0, 'redacted-b'), comparison: 'opaque' }]); - assert.deepEqual(deriveSemanticPrefixContinuity(current, previous), { + assert.deepEqual(deriveRequestPreservation(current, previous), { status: 'unknown', previousSegmentCount: 1, preservedSegmentCount: 0, @@ -61,7 +61,7 @@ test('reports the first changed earlier segment', () => { const previous = observation([message(0, 'system'), message(1, 'user-1')]); const current = observation([message(0, 'system'), message(1, 'edited-user-1')]); - assert.deepEqual(deriveSemanticPrefixContinuity(current, previous), { + assert.deepEqual(deriveRequestPreservation(current, previous), { status: 'diverged', previousSegmentCount: 2, preservedSegmentCount: 1, @@ -73,7 +73,7 @@ test('reports the first removed earlier segment', () => { const previous = observation([message(0, 'system'), message(1, 'user-1')]); const current = observation([message(0, 'system')]); - assert.deepEqual(deriveSemanticPrefixContinuity(current, previous), { + assert.deepEqual(deriveRequestPreservation(current, previous), { status: 'diverged', previousSegmentCount: 2, preservedSegmentCount: 1, @@ -89,7 +89,7 @@ test('reports the current segment at the first middle deletion', () => { ]); const current = observation([message(0, 'system'), message(2, 'assistant-1')]); - assert.deepEqual(deriveSemanticPrefixContinuity(current, previous), { + assert.deepEqual(deriveRequestPreservation(current, previous), { status: 'diverged', previousSegmentCount: 3, preservedSegmentCount: 1, @@ -105,7 +105,7 @@ test('reports an inserted segment as the first divergence', () => { message(2, 'assistant-1'), ]); - assert.deepEqual(deriveSemanticPrefixContinuity(current, previous), { + assert.deepEqual(deriveRequestPreservation(current, previous), { status: 'diverged', previousSegmentCount: 2, preservedSegmentCount: 1, @@ -125,7 +125,7 @@ test('reports the first moved segment after a reorder', () => { message(1, 'user-1'), ]); - assert.deepEqual(deriveSemanticPrefixContinuity(current, previous), { + assert.deepEqual(deriveRequestPreservation(current, previous), { status: 'diverged', previousSegmentCount: 3, preservedSegmentCount: 1, @@ -266,11 +266,11 @@ test('does not compare a child run against its parent session run', async () => assert.equal( await statusOf( current, - [run('run-parent', 'turn-1', PROVIDER_STATE), run('run-child', 'turn-1', PROVIDER_STATE)], + [ + run('run-parent', 'turn-1', PROVIDER_STATE), + run('run-child', 'turn-1', PROVIDER_STATE, { parentRunId: 'run-parent' }), + ], [previous], - undefined, - false, - { parentRunId: 'run-parent' }, ), 'unavailable', ); @@ -281,16 +281,11 @@ async function statusOf( runs: AgentRunHeader[], attempts: ModelCallAttempt[], admission?: { runId: string; previousRootTurnId: string | null }, - currentSessionInline = true, - lineage: { parentRunId?: string } = {}, ) { return ( - await deriveAttemptSemanticPrefixContinuity({ + await deriveAttemptRequestPreservation({ current, - currentProviderStateIdentity: PROVIDER_STATE, - currentSessionInline, - lineage, - store: prefixStore(runs, attempts, admission), + store: preservationStore(runs, attempts, admission), }) ).status; } @@ -382,7 +377,7 @@ function continuationSource( }; } -function prefixStore( +function preservationStore( runs: AgentRunHeader[], attempts: ModelCallAttempt[], admission?: { runId: string; previousRootTurnId: string | null }, diff --git a/packages/runtime/src/agent-run.ts b/packages/runtime/src/agent-run.ts index 14347f9c71..74b08d8e5d 100644 --- a/packages/runtime/src/agent-run.ts +++ b/packages/runtime/src/agent-run.ts @@ -91,7 +91,7 @@ import { import { DEFAULT_TOOL_MODE, isToolMode, type ToolMode } from '@maka/core/tool-mode'; import { materializeRuntimeEventTranscriptProjection } from './runtime-ledger-repair.js'; import { cloneAndFreezeRuntimeSnapshot } from './runtime-snapshot.js'; -import { deriveAttemptSemanticPrefixContinuity } from './semantic-prefix-continuity.js'; +import { deriveAttemptRequestPreservation } from './request-preservation.js'; export interface AgentRunActiveSession { sessionId: string; @@ -479,11 +479,8 @@ export class AgentRun { if (!runStore) return; let projected = latestContext; if (projected && attempt.callKind === 'main') { - const requestPrefix = await deriveAttemptSemanticPrefixContinuity({ + const requestPreservation = await deriveAttemptRequestPreservation({ current: attempt, - currentProviderStateIdentity: this.providerStateIdentity, - currentSessionInline: this.isSessionInline(), - lineage: this.lineage, store: runStore, }).catch(() => ({ status: 'unavailable' as const, @@ -492,7 +489,7 @@ export class AgentRun { })); projected = { ...projected, - snapshot: { ...projected.snapshot, requestPrefix }, + snapshot: { ...projected.snapshot, requestPreservation }, }; } await runStore.appendEvent( diff --git a/packages/runtime/src/context-diagnostics.ts b/packages/runtime/src/context-diagnostics.ts index 0d77f87192..d3aff6ce69 100644 --- a/packages/runtime/src/context-diagnostics.ts +++ b/packages/runtime/src/context-diagnostics.ts @@ -41,12 +41,12 @@ import { type HistoryCompactCheckpoint, } from './history-compact-checkpoint.js'; import { - deriveAttemptSemanticPrefixContinuity, + deriveAttemptRequestPreservation, isCanonicalAttemptForRun, - type SemanticPrefixContinuity, -} from './semantic-prefix-continuity.js'; + type RequestPreservation, +} from './request-preservation.js'; -export type { SemanticPrefixContinuity } from './semantic-prefix-continuity.js'; +export { isRequestPreservation, type RequestPreservation } from './request-preservation.js'; export type ContextDiagnosticsUnavailableReason = 'no_completed_request' | 'trace_unavailable'; @@ -107,7 +107,7 @@ export type ContextDiagnostics = composition?: ContextDiagnosticsComposition; compaction?: ContextDiagnosticsCompaction; /** Absent only for historical projections written before this diagnostic existed. */ - requestPrefix?: SemanticPrefixContinuity; + requestPreservation?: RequestPreservation; }; export interface ContextDiagnosticsComposition { @@ -262,13 +262,10 @@ async function rebuildContextFromLedger( (anchor && !anchor.hasRequestObservation ? exactHistoricalComposition(anchor, historicalAttempts) : undefined); - const requestPrefix = + const requestPreservation = anchor?.attempt && anchor.run - ? await deriveAttemptSemanticPrefixContinuity({ + ? await deriveAttemptRequestPreservation({ current: anchor.attempt, - currentProviderStateIdentity: anchor.run.providerStateIdentity, - currentSessionInline: true, - lineage: anchor.run, store: runStore, }) : undefined; @@ -285,7 +282,7 @@ async function rebuildContextFromLedger( ...(resolved.contextWindow !== undefined ? { contextWindow: resolved.contextWindow } : {}), ...(composition ? { composition } : {}), ...(boundary ? { compaction: contextDiagnosticsCompactionOf(boundary.checkpoint) } : {}), - ...(requestPrefix ? { requestPrefix } : {}), + ...(requestPreservation ? { requestPreservation } : {}), }; // Repair on the way out, so this scan happens once per session rather than // on every panel refresh. Best-effort: the caller already has its answer, @@ -383,7 +380,7 @@ function availableFrom(snapshot: LatestContextSnapshot): ContextDiagnostics { ...(snapshot.contextWindow !== undefined ? { contextWindow: snapshot.contextWindow } : {}), ...(snapshot.composition ? { composition: snapshot.composition } : {}), ...(snapshot.compaction ? { compaction: snapshot.compaction } : {}), - ...(snapshot.requestPrefix ? { requestPrefix: snapshot.requestPrefix } : {}), + ...(snapshot.requestPreservation ? { requestPreservation: snapshot.requestPreservation } : {}), }; } diff --git a/packages/runtime/src/latest-context-snapshot.ts b/packages/runtime/src/latest-context-snapshot.ts index e363cbdb80..89b632c6cb 100644 --- a/packages/runtime/src/latest-context-snapshot.ts +++ b/packages/runtime/src/latest-context-snapshot.ts @@ -29,10 +29,7 @@ import type { ContextDiagnosticsComposition, } from './context-diagnostics.js'; import { foldPromptComposition, type SizedRequestSegment } from './prompt-composition.js'; -import { - isSemanticPrefixContinuity, - type SemanticPrefixContinuity, -} from './semantic-prefix-continuity.js'; +import { isRequestPreservation, type RequestPreservation } from './request-preservation.js'; /** * One request's context, frozen by the transaction that committed it (#2323). @@ -72,7 +69,7 @@ export interface LatestContextSnapshot { /** The boundary that applied when this request was built, if any. */ compaction?: ContextDiagnosticsCompaction; /** Runtime-owned conclusion; consumers must not select or compare observations. */ - requestPrefix?: SemanticPrefixContinuity; + requestPreservation?: RequestPreservation; } /** @@ -141,7 +138,7 @@ export function readLatestContextSnapshot( 'contextWindow', 'composition', 'compaction', - 'requestPrefix', + 'requestPreservation', ], ); if (!record) return undefined; @@ -157,7 +154,7 @@ export function readLatestContextSnapshot( (!isCount(record.contextWindow) || record.contextWindow === 0)) || (record.composition !== undefined && !isContextDiagnosticsComposition(record.composition)) || (record.compaction !== undefined && !isContextDiagnosticsCompaction(record.compaction)) || - (record.requestPrefix !== undefined && !isSemanticPrefixContinuity(record.requestPrefix)) + (record.requestPreservation !== undefined && !isRequestPreservation(record.requestPreservation)) ) { return undefined; } diff --git a/packages/runtime/src/semantic-prefix-continuity.ts b/packages/runtime/src/request-preservation.ts similarity index 84% rename from packages/runtime/src/semantic-prefix-continuity.ts rename to packages/runtime/src/request-preservation.ts index 7edbe1aace..cb393af7e1 100644 --- a/packages/runtime/src/semantic-prefix-continuity.ts +++ b/packages/runtime/src/request-preservation.ts @@ -30,7 +30,7 @@ import { type AgentRunStore, } from '@maka/core/agent-run'; -export type SemanticPrefixContinuity = +export type RequestPreservation = | { status: 'no_predecessor' | 'unavailable'; previousSegmentCount: 0; @@ -45,15 +45,15 @@ export type SemanticPrefixContinuity = status: 'diverged'; previousSegmentCount: number; preservedSegmentCount: number; - firstDivergentSegment: SemanticPrefixSegmentRef; + firstDivergentSegment: RequestPreservationSegmentRef; }; -export type SemanticPrefixSegmentRef = Pick< +export type RequestPreservationSegmentRef = Pick< PreparedRequestObservationSegment, 'kind' | 'index' | 'role' | 'label' >; -type PrefixStore = Pick; +type PreservationStore = Pick; interface RootAdmissionReader { readRootTurnAdmission?( @@ -62,44 +62,37 @@ interface RootAdmissionReader { ): Promise<{ runId: string; previousRootTurnId?: string | null } | undefined>; } -interface AttemptContinuityInput { +interface AttemptPreservationInput { current: ModelCallAttempt; - currentProviderStateIdentity?: `sha256:${string}`; - currentSessionInline: boolean; - lineage: { - parentRunId?: string; - parentTurnId?: string; - retriedFromTurnId?: string; - regeneratedFromTurnId?: string; - branchOfTurnId?: string; - parentSessionId?: string; - }; - store: PrefixStore; + store: PreservationStore; } -export async function deriveAttemptSemanticPrefixContinuity( - input: AttemptContinuityInput, -): Promise { - const { current } = input; - if (!input.currentSessionInline || current.callKind !== 'main' || !current.requestObservation) { +export async function deriveAttemptRequestPreservation( + input: AttemptPreservationInput, +): Promise { + const { current, store } = input; + if (current.callKind !== 'main' || !current.requestObservation) { return unavailable(); } + const runs = await store.listSessionRuns(current.sessionId); + const currentRun = runs.find((run) => run.runId === current.runId); + if (!currentRun || !isSessionInlineRun(currentRun)) return unavailable(); - const predecessor = await predecessorAttempt(input); + const predecessor = await predecessorAttempt(current, currentRun, runs, store); if (predecessor === null) { return { status: 'no_predecessor', previousSegmentCount: 0, preservedSegmentCount: 0 }; } - if (!predecessor || !sameDomain(input, predecessor)) return unavailable(); - return deriveSemanticPrefixContinuity( + if (!predecessor || !sameDomain(current, currentRun, predecessor)) return unavailable(); + return deriveRequestPreservation( current.requestObservation, predecessor.attempt.requestObservation!, ); } -export function deriveSemanticPrefixContinuity( +export function deriveRequestPreservation( currentObservation: PreparedRequestObservation, previousObservation: PreparedRequestObservation, -): SemanticPrefixContinuity { +): RequestPreservation { const current = currentObservation.segments.filter((segment) => segment.cacheable); const previous = previousObservation.segments.filter((segment) => segment.cacheable); const previousSegmentCount = representedCount(previous); @@ -155,7 +148,7 @@ function representedCount(segments: readonly PreparedRequestObservationSegment[] return segments.reduce((count, segment) => count + (segment.representedSegments ?? 1), 0); } -function segmentRef(segment: PreparedRequestObservationSegment): SemanticPrefixSegmentRef { +function segmentRef(segment: PreparedRequestObservationSegment): RequestPreservationSegmentRef { return { kind: segment.kind, index: segment.index, @@ -165,13 +158,11 @@ function segmentRef(segment: PreparedRequestObservationSegment): SemanticPrefixS } async function predecessorAttempt( - input: AttemptContinuityInput, + current: ModelCallAttempt, + currentRun: AgentRunHeader, + runs: readonly AgentRunHeader[], + store: PreservationStore, ): Promise<{ attempt: ModelCallAttempt; run: AgentRunHeader } | null | undefined> { - const { current, lineage, store } = input; - const runs = await store.listSessionRuns(current.sessionId); - const currentRun = runs.find((run) => run.runId === current.runId); - if (!currentRun) return undefined; - if (current.attempt > 0) { return uniqueAttempt( currentRun, @@ -189,24 +180,23 @@ async function predecessorAttempt( ), ); } - if (lineage.parentRunId) { - const parent = runs.find((run) => run.runId === lineage.parentRunId); + if (currentRun.parentRunId) { + const parent = runs.find((run) => run.runId === currentRun.parentRunId); return parent ? latestAttempt(parent, await attemptsFor(store, parent)) : undefined; } if ( - lineage.parentSessionId || - lineage.parentTurnId || - lineage.retriedFromTurnId || - lineage.regeneratedFromTurnId || - lineage.branchOfTurnId + currentRun.parentSessionId || + currentRun.parentTurnId || + currentRun.retriedFromTurnId || + currentRun.regeneratedFromTurnId || + currentRun.branchOfTurnId ) { return undefined; } - const admission = await (store as PrefixStore & RootAdmissionReader).readRootTurnAdmission?.( - current.sessionId, - current.turnId, - ); + const admission = await ( + store as PreservationStore & RootAdmissionReader + ).readRootTurnAdmission?.(current.sessionId, current.turnId); if ( !admission || admission.runId !== current.runId || @@ -248,7 +238,10 @@ function uniqueDurableRunTip(runs: readonly AgentRunHeader[]): AgentRunHeader | return visited.size === runs.length ? tips[0] : undefined; } -async function attemptsFor(store: PrefixStore, run: AgentRunHeader): Promise { +async function attemptsFor( + store: PreservationStore, + run: AgentRunHeader, +): Promise { const attempts: ModelCallAttempt[] = []; for (const event of await store.readEvents(run.sessionId, run.runId)) { if (event.type !== 'model_call_attempt_recorded') continue; @@ -301,20 +294,17 @@ function uniqueAttempt( } function sameDomain( - input: { - current: ModelCallAttempt; - currentProviderStateIdentity?: `sha256:${string}`; - }, + current: ModelCallAttempt, + currentRun: AgentRunHeader, previous: { attempt: ModelCallAttempt; run: AgentRunHeader }, ): boolean { - const current = input.current; const before = previous.attempt; if (!before.requestObservation) return false; const currentPartition = exactProviderPartition(current.requestObservation!); const previousPartition = exactProviderPartition(before.requestObservation); return ( - input.currentProviderStateIdentity !== undefined && - input.currentProviderStateIdentity === previous.run.providerStateIdentity && + currentRun.providerStateIdentity !== undefined && + currentRun.providerStateIdentity === previous.run.providerStateIdentity && current.sessionId === before.sessionId && current.connectionSlug !== undefined && current.connectionSlug === before.connectionSlug && @@ -331,11 +321,11 @@ function exactProviderPartition(observation: PreparedRequestObservation): string return segments.map((segment) => `${segment.index}:${segment.digest}`).join('|'); } -function unavailable(): SemanticPrefixContinuity { +function unavailable(): RequestPreservation { return { status: 'unavailable', previousSegmentCount: 0, preservedSegmentCount: 0 }; } -export function isSemanticPrefixContinuity(value: unknown): value is SemanticPrefixContinuity { +export function isRequestPreservation(value: unknown): value is RequestPreservation { if (!value || typeof value !== 'object') return false; const candidate = value as { status?: unknown; From a09f5ccd3f564ab7280d3a927cd6fb566d3a9b51 Mon Sep 17 00:00:00 2001 From: AstroHan Date: Tue, 1 Sep 2026 10:28:07 +0800 Subject: [PATCH 18/19] refactor(desktop): remove request preservation UI Generated-by: Codex --- .../session-inspector-composition.test.ts | 22 +----- .../session-inspector-panel-model.test.ts | 71 ------------------- .../main/__tests__/use-session-trace.test.ts | 47 ------------ .../src/renderer/features/workbar/testing.ts | 6 +- .../session-inspector-overview-model.ts | 28 -------- .../inspector/session-inspector-panel.tsx | 51 +------------ .../tools/inspector/use-session-trace.ts | 12 ++-- .../src/renderer/locales/conversation-copy.ts | 35 --------- docs/astryx-surface-file-inventory.md | 2 +- 9 files changed, 9 insertions(+), 265 deletions(-) diff --git a/apps/desktop/src/main/__tests__/session-inspector-composition.test.ts b/apps/desktop/src/main/__tests__/session-inspector-composition.test.ts index 31fed020b6..03adbe1432 100644 --- a/apps/desktop/src/main/__tests__/session-inspector-composition.test.ts +++ b/apps/desktop/src/main/__tests__/session-inspector-composition.test.ts @@ -21,10 +21,7 @@ import assert from 'node:assert/strict'; import { test } from 'node:test'; import { createElement } from 'react'; import { renderToStaticMarkup } from 'react-dom/server'; -import { - InspectorCompositionSection, - InspectorRequestPreservationBadge, -} from '../../renderer/features/workbar/testing.js'; +import { InspectorCompositionSection } from '../../renderer/features/workbar/testing.js'; import { getDesktopConversationCopy } from '../../renderer/locales/conversation-copy.js'; test('maps each request-composition category to the same colour in the chart and legend', () => { @@ -63,20 +60,3 @@ test('maps each request-composition category to the same colour in the chart and ); } }); - -test('renders the Runtime divergence location as a compact badge', () => { - const markup = renderToStaticMarkup( - createElement(InspectorRequestPreservationBadge, { - copy: getDesktopConversationCopy('en').inspector, - requestPreservation: { - status: 'diverged', - previousSegmentCount: 8, - preservedSegmentCount: 2, - firstDivergentSegment: { kind: 'message', index: 2 }, - }, - }), - ); - - assert.match(markup, /Previous request changed at message 3/); - assert.match(markup, /data-maka-contract="request-preservation"/); -}); diff --git a/apps/desktop/src/main/__tests__/session-inspector-panel-model.test.ts b/apps/desktop/src/main/__tests__/session-inspector-panel-model.test.ts index 8235b60da3..e301fd4ec7 100644 --- a/apps/desktop/src/main/__tests__/session-inspector-panel-model.test.ts +++ b/apps/desktop/src/main/__tests__/session-inspector-panel-model.test.ts @@ -138,77 +138,6 @@ test('does not estimate a cache-hit ratio from partial usage', () => { assert.equal(overview.cacheHitRate, undefined); }); - -test('keeps provider cache read and write usage separate from request preservation', () => { - const overview = deriveInspectorOverviewModel(undefined, { - range: { from: 0, to: 1 }, - totalRequests: 1, - totalCostUsd: 0, - totalTokens: { - input: 10, - output: 0, - cacheMiss: 3, - cacheRead: 7, - cacheWrite: 4, - reasoning: 0, - total: 10, - }, - cacheHitRequests: 1, - cacheCreateRequests: 1, - errorRequests: 0, - provenance: { - coverage: { - attempts: 1, - pricedAttempts: 1, - unpricedAttempts: 0, - usageReportedAttempts: 1, - usagePartialAttempts: 0, - usageMissingAttempts: 0, - }, - legacyRecords: 0, - unreadableRecords: 0, - pendingRepairs: 0, - }, - }); - - assert.deepEqual(overview.providerCacheUsage, { readTokens: 7, writeTokens: 4 }); - assert.equal(overview.requestPreservation, undefined); -}); - -test('passes the Runtime request-preservation verdict through without recomputing it', () => { - const requestPreservation = { - status: 'diverged' as const, - previousSegmentCount: 8, - preservedSegmentCount: 2, - firstDivergentSegment: { kind: 'message' as const, index: 2, role: 'user' }, - }; - const overview = deriveInspectorOverviewModel({ - status: 'available', - providerId: 'anthropic', - modelId: 'claude', - completedAt: 10, - requestPreservation, - }); - - assert.equal(overview.requestPreservation, requestPreservation); -}); - -test('does not create an empty overview for a request without a predecessor', () => { - const overview = deriveInspectorOverviewModel({ - status: 'available', - providerId: 'anthropic', - modelId: 'claude', - completedAt: 10, - requestPreservation: { - status: 'no_predecessor', - previousSegmentCount: 0, - preservedSegmentCount: 0, - }, - }); - - assert.equal(overview.requestPreservation, undefined); -}); - test('derives per-turn cost only from priced model-call step totals', () => { const cases: readonly { name: string; diff --git a/apps/desktop/src/main/__tests__/use-session-trace.test.ts b/apps/desktop/src/main/__tests__/use-session-trace.test.ts index 320c191ce8..06d0b5eed1 100644 --- a/apps/desktop/src/main/__tests__/use-session-trace.test.ts +++ b/apps/desktop/src/main/__tests__/use-session-trace.test.ts @@ -120,10 +120,6 @@ function createTraceHarness( sessionId: string, readIndex: number, ) => Promise>; - context?: ( - sessionId: string, - readIndex: number, - ) => ReturnType; } = {}, ): TraceHarness { const handlers = new Set<(event: SessionEvent) => void>(); @@ -172,7 +168,6 @@ function createTraceHarness( // TRACE is re-read, and an enrichment read must not move them. context: async (sessionId: string) => { harness.contextReads.push(sessionId); - if (options.context) return options.context(sessionId, harness.contextReads.length); return { ok: true as const, data: { @@ -317,48 +312,6 @@ describe('useSessionTrace', () => { assert.equal(harness.reads.length, 2, 'a closing burst is one re-read, not three'); }); - it('does not keep an earlier request-preservation verdict while its refresh fails', async () => { - const { root } = installReactRenderer(); - const harness = createTraceHarness({ - context: async (_sessionId, readIndex) => { - if (readIndex > 1) throw new Error('context unavailable'); - return { - ok: true, - data: { - status: 'available', - providerId: 'anthropic', - modelId: 'model', - completedAt: 1, - requestPreservation: { - status: 'preserved', - previousSegmentCount: 1, - preservedSegmentCount: 1, - }, - }, - }; - }, - }); - let snapshot: ReturnType | undefined; - await act(async () => { - root.render( - createElement(Probe, { - services: harness.services, - sessionId: 'session-1', - active: true, - onHookSnapshot: (value) => { - snapshot = value; - }, - }), - ); - }); - assert.equal(snapshot?.context?.status, 'available'); - - await act(async () => harness.emit(event('complete'))); - await flushRefresh(); - - assert.equal(snapshot?.context, undefined); - }); - it('refreshes Session usage only from the Usage authority signal', async () => { const { root } = installReactRenderer(); const harness = createTraceHarness(); diff --git a/apps/desktop/src/renderer/features/workbar/testing.ts b/apps/desktop/src/renderer/features/workbar/testing.ts index d35daaf983..8bf5c6bb0a 100644 --- a/apps/desktop/src/renderer/features/workbar/testing.ts +++ b/apps/desktop/src/renderer/features/workbar/testing.ts @@ -32,11 +32,7 @@ export * from './model/workbar-tool-definitions.js'; export * from './tools/artifacts/artifact-list-keyboard.js'; export * from './tools/artifacts/artifact-visibility.js'; export * from './tools/inspector/session-inspector-panel-model.js'; -export { - compactNumberFormatter, - InspectorCompositionSection, - InspectorRequestPreservationBadge, -} from './tools/inspector/session-inspector-panel.js'; +export { compactNumberFormatter, InspectorCompositionSection } from './tools/inspector/session-inspector-panel.js'; export * from './tools/inspector/session-inspector-overview-model.js'; export * from './tools/side-chat/quote-companion-panel-state.js'; export * from './tools/side-chat/quote-companion-core.js'; diff --git a/apps/desktop/src/renderer/features/workbar/tools/inspector/session-inspector-overview-model.ts b/apps/desktop/src/renderer/features/workbar/tools/inspector/session-inspector-overview-model.ts index d6e4839f86..36e835d81f 100644 --- a/apps/desktop/src/renderer/features/workbar/tools/inspector/session-inspector-overview-model.ts +++ b/apps/desktop/src/renderer/features/workbar/tools/inspector/session-inspector-overview-model.ts @@ -140,10 +140,6 @@ export interface InspectorOverviewModel { * the run ledger; three statements of the same tokens is two too many. */ cacheHitRate?: number; - /** Provider-reported cache usage; independent of semantic continuity. */ - providerCacheUsage?: { readTokens: number; writeTokens: number }; - /** Runtime-owned request-preservation verdict; Desktop only presents it. */ - requestPreservation?: Extract['requestPreservation']; } export function estimatedSessionCost( @@ -186,34 +182,10 @@ export function deriveInspectorOverviewModel( const composition = compositionState(diagnostics); const context = contextBudget(diagnostics); const cacheHitRate = usageCacheHitRate(usage); - const providerCacheUsage = usageProviderCache(usage); return { ...(context ? { context } : {}), ...(composition ? { composition } : {}), ...(cacheHitRate !== undefined ? { cacheHitRate } : {}), - ...(providerCacheUsage ? { providerCacheUsage } : {}), - ...(diagnostics?.status === 'available' && - diagnostics.requestPreservation && - diagnostics.requestPreservation.status !== 'no_predecessor' - ? { requestPreservation: diagnostics.requestPreservation } - : {}), - }; -} - -function usageProviderCache( - usage: SessionUsageSummary | undefined, -): InspectorOverviewModel['providerCacheUsage'] { - if ( - !usage || - usage.provenance?.coverage.usagePartialAttempts || - usage.provenance?.coverage.usageMissingAttempts || - (usage.totalTokens.cacheRead === 0 && usage.totalTokens.cacheWrite === 0) - ) { - return undefined; - } - return { - readTokens: usage.totalTokens.cacheRead, - writeTokens: usage.totalTokens.cacheWrite, }; } diff --git a/apps/desktop/src/renderer/features/workbar/tools/inspector/session-inspector-panel.tsx b/apps/desktop/src/renderer/features/workbar/tools/inspector/session-inspector-panel.tsx index c0f7a7c6ac..86749b04dc 100644 --- a/apps/desktop/src/renderer/features/workbar/tools/inspector/session-inspector-panel.tsx +++ b/apps/desktop/src/renderer/features/workbar/tools/inspector/session-inspector-panel.tsx @@ -19,7 +19,6 @@ import { type ReactNode, useMemo } from 'react'; import { Banner } from '@astryxdesign/core/Banner'; -import { Badge } from '@astryxdesign/core/Badge'; import { Button } from '@astryxdesign/core/Button'; import { EmptyState } from '@astryxdesign/core/EmptyState'; import { Heading } from '@astryxdesign/core/Heading'; @@ -149,7 +148,7 @@ export function SessionInspectorPanel(props: { sessionId: string; active: boolea {copy.summaryUnavailable} )} - {(snapshot.summary || overview.context || overview.composition || overview.requestPreservation) && ( + {(snapshot.summary || overview.context || overview.composition) && ( - {overview.requestPreservation && ( - - )} {props.showTotals && ( )} - {overview.providerCacheUsage && ( - - )} {copy.costEstimateHelp} @@ -320,45 +310,6 @@ function InspectorOverview(props: { ); } -export function InspectorRequestPreservationBadge(props: { - copy: InspectorCopy; - requestPreservation: NonNullable['requestPreservation']>; -}) { - const { requestPreservation, copy } = props; - if (requestPreservation.status === 'no_predecessor') return null; - if (requestPreservation.status === 'preserved') { - return ( - - ); - } - if (requestPreservation.status === 'diverged') { - return ( - - ); - } - return ( - - ); -} - /** * One overview total on the same title/readout rhythm as the sections below. * These figures answer parallel questions, so changing typography between the diff --git a/apps/desktop/src/renderer/features/workbar/tools/inspector/use-session-trace.ts b/apps/desktop/src/renderer/features/workbar/tools/inspector/use-session-trace.ts index a77e824e33..5632085f8f 100644 --- a/apps/desktop/src/renderer/features/workbar/tools/inspector/use-session-trace.ts +++ b/apps/desktop/src/renderer/features/workbar/tools/inspector/use-session-trace.ts @@ -255,11 +255,8 @@ export function useSessionTrace( }, [inspector]); const readContext = useCallback((targetSessionId: string) => { - const contextRevision = ++contextRevisionRef.current; - setState((current) => - current.sessionId === targetSessionId ? { ...current, context: undefined } : current, - ); - // Enrichment, and read as such: the context snapshot has its own owner + const contextRevision = ++contextRevisionRef.current; + // Enrichment, and read as such: the context snapshot has its own owner // and its own failure modes, so it lands when it lands and its absence // costs the composition block, never the trace. void inspector.context(targetSessionId).then( @@ -272,8 +269,9 @@ export function useSessionTrace( ); }, () => { - // The old snapshot was cleared when this read began. Keeping it would - // present the previous request's verdict as the current request's. + // A refresh that could not reach the snapshot leaves the last one + // standing: it is still the newest answer anyone has, and blanking it + // would report "no composition" for a read that simply failed. }, ); }, [inspector]); diff --git a/apps/desktop/src/renderer/locales/conversation-copy.ts b/apps/desktop/src/renderer/locales/conversation-copy.ts index 18f316620c..a41bc754a7 100644 --- a/apps/desktop/src/renderer/locales/conversation-copy.ts +++ b/apps/desktop/src/renderer/locales/conversation-copy.ts @@ -262,16 +262,6 @@ export interface DesktopConversationCopy { }; /** The three figures a reader opens this tab for, as headline stats. */ cacheHit: string; - providerCache: string; - requestPreservation: { - preserved: (preserved: number, previous: number) => string; - diverged: (segment: string, index: number) => string; - cannotJudge: string; - segment: Record< - 'tool_schema' | 'system_prompt' | 'message' | 'provider_options', - string - >; - }; /** Heading over the causal record. */ timelineTab: string; /** @@ -606,18 +596,6 @@ const COPY = { free: '剩余', }, cacheHit: '缓存命中率', - providerCache: 'Provider 缓存读取 / 写入', - requestPreservation: { - preserved: (preserved, previous) => `上次请求内容已保留 ${preserved}/${previous}`, - diverged: (segment, index) => `上次请求从${segment} ${index}起有变化`, - cannotJudge: '无法比较上次请求', - segment: { - tool_schema: '工具', - system_prompt: '系统提示', - message: '消息', - provider_options: '模型选项', - }, - }, timelineTab: '时间轴', composition: { title: '构成估算', @@ -853,19 +831,6 @@ const COPY = { free: 'Remaining', }, cacheHit: 'Cache hit rate', - providerCache: 'Provider cache read / write', - requestPreservation: { - preserved: (preserved, previous) => - `Previous request ${preserved}/${previous} preserved`, - diverged: (segment, index) => `Previous request changed at ${segment} ${index}`, - cannotJudge: 'Previous request could not be compared', - segment: { - tool_schema: 'tool', - system_prompt: 'system prompt', - message: 'message', - provider_options: 'model option', - }, - }, timelineTab: 'Timeline', composition: { title: 'Estimated composition', diff --git a/docs/astryx-surface-file-inventory.md b/docs/astryx-surface-file-inventory.md index 8f82bff7aa..f6ed5c7566 100644 --- a/docs/astryx-surface-file-inventory.md +++ b/docs/astryx-surface-file-inventory.md @@ -63,7 +63,7 @@ Wiki bar: Design Conventions · API Use-the-System · Theming · Container Paddi | `apps/desktop/src/renderer/features/workbar/tools/artifacts/artifact-preview-registry-shell.tsx` | shell-chrome-or-panel | Banner, Button, Spinner | aligned — uses Astryx (Banner, Button, Spinner) | aligned | | `apps/desktop/src/renderer/features/workbar/tools/artifacts/artifact-preview.tsx` | shell-chrome-or-panel | Banner, Button, CodeBlock, Spinner | aligned — uses Astryx (Banner, Button, CodeBlock, Spinner) | aligned | | `apps/desktop/src/renderer/features/workbar/tools/browser/browser-panel.tsx` | shell-chrome-or-panel | EmptyState, IconButton, TextInput, Toolbar, Tooltip | aligned — uses Astryx (EmptyState, IconButton, TextInput, Toolbar, Tooltip) | aligned | -| `apps/desktop/src/renderer/features/workbar/tools/inspector/session-inspector-panel.tsx` | shell-chrome-or-panel | Badge, Banner, Button, EmptyState, Heading, Section, Text, VStack | aligned — uses Astryx (Badge, Banner, Button, EmptyState, Heading, Section, Text, VStack) | aligned | +| `apps/desktop/src/renderer/features/workbar/tools/inspector/session-inspector-panel.tsx` | shell-chrome-or-panel | Banner, Button, EmptyState, Heading, Section, Text, VStack | aligned — uses Astryx (Banner, Button, EmptyState, Heading, Section, Text, VStack) | aligned | | `apps/desktop/src/renderer/features/workbar/tools/review/session-review-panel.tsx` | shell-chrome-or-panel | Banner, Button, Collapsible, CollapsibleGroup, EmptyState, HStack, Section, Skeleton, Text, VStack | aligned — uses Astryx (Banner, Button, Collapsible, CollapsibleGroup, EmptyState, HStack, Section, Skeleton) | aligned | | `apps/desktop/src/renderer/features/workbar/tools/side-chat/quote-companion-panel.tsx` | shell-chrome-or-panel | Banner, Spinner | aligned — uses Astryx (Banner, Spinner) | aligned | | `apps/desktop/src/renderer/features/workbar/tools/terminal/session-terminal-panel.tsx` | shell-chrome-or-panel | Banner, EmptyState | aligned — uses Astryx (Banner, EmptyState) | aligned | From a6a4aa1a36a3b8997df5edc389184b65bd3109dc Mon Sep 17 00:00:00 2001 From: AstroHan Date: Tue, 1 Sep 2026 11:34:04 +0800 Subject: [PATCH 19/19] fix(runtime): retry unresolved request preservation Generated-by: Codex --- .../src/__tests__/context-diagnostics.test.ts | 154 ++++++++++++++++++ .../__tests__/request-preservation.test.ts | 41 ++++- packages/runtime/src/agent-run.ts | 13 +- packages/runtime/src/context-diagnostics.ts | 19 ++- .../runtime/src/latest-context-snapshot.ts | 9 +- packages/runtime/src/request-preservation.ts | 75 +++++++-- 6 files changed, 290 insertions(+), 21 deletions(-) diff --git a/packages/runtime/src/__tests__/context-diagnostics.test.ts b/packages/runtime/src/__tests__/context-diagnostics.test.ts index 02e565446b..86d9744f11 100644 --- a/packages/runtime/src/__tests__/context-diagnostics.test.ts +++ b/packages/runtime/src/__tests__/context-diagnostics.test.ts @@ -122,6 +122,139 @@ test('serves the sealed snapshot without reading a single run', async () => { } }); +test('repairs a pending preservation result after its admitted predecessor becomes durable', async () => { + const root = await mkdtemp(join(tmpdir(), 'maka-context-diagnostics-')); + try { + const store = createSqliteAgentRunStore(root); + const previousRun = { + ...runHeader('run-1', 1), + status: 'running' as const, + providerStateIdentity: `sha256:${'1'.repeat(64)}` as const, + llmConnectionSlug: 'connection', + }; + const currentRun = { + ...runHeader('run-2', 2), + providerStateIdentity: `sha256:${'1'.repeat(64)}` as const, + llmConnectionSlug: 'connection', + }; + await store.createRun(previousRun); + await store.createRun(currentRun); + await admitTurn(store, 'turn-run-1', 'run-1', null, 1); + await admitTurn(store, 'turn-run-2', 'run-2', 'turn-run-1', 2); + + const observation = requestObservation([ + { + kind: 'message', + index: 0, + cacheable: true, + comparison: 'exact', + digest: `sha256:${'a'.repeat(64)}`, + bytes: 10, + }, + ]); + const currentEvent = meteringEvent('run-2', 'current', 20, 'model', 40, 200, { + logicalCallId: 'current-call', + connectionSlug: 'connection', + requestObservation: observation, + }); + const currentProjection = latestContext('current', 20); + Object.assign(currentProjection.snapshot, { requestPreservationPending: true }); + await store.appendEvent('session-1', 'run-2', currentEvent, { + durable: true, + latestContext: currentProjection, + }); + + const before = await readLatestContextDiagnostics(store, 'session-1'); + assert.equal(before.status, 'available'); + if (before.status !== 'available') return; + assert.deepEqual(before.requestPreservation, { + status: 'unavailable', + previousSegmentCount: 0, + preservedSegmentCount: 0, + }); + + const previousEvent = meteringEvent('run-1', 'previous', 10, 'model', 40, 200, { + logicalCallId: 'previous-call', + connectionSlug: 'connection', + requestObservation: observation, + }); + await store.appendEvent('session-1', 'run-1', previousEvent, { + durable: true, + latestContext: latestContext('previous', 10), + }); + + const repaired = await readLatestContextDiagnostics(store, 'session-1'); + assert.equal(repaired.status, 'available'); + if (repaired.status !== 'available') return; + assert.equal(repaired.modelId, 'model'); + assert.deepEqual(repaired.requestPreservation, { + status: 'preserved', + previousSegmentCount: 1, + preservedSegmentCount: 1, + }); + } finally { + await rm(root, { recursive: true, force: true }); + } +}); + +test('settles unavailable when a terminal predecessor has no canonical attempt', async () => { + const root = await mkdtemp(join(tmpdir(), 'maka-context-diagnostics-')); + try { + const store = createSqliteAgentRunStore(root); + const providerStateIdentity = `sha256:${'1'.repeat(64)}` as const; + await store.createRun({ + ...runHeader('run-1', 1), + providerStateIdentity, + llmConnectionSlug: 'connection', + }); + await store.createRun({ + ...runHeader('run-2', 2), + providerStateIdentity, + llmConnectionSlug: 'connection', + }); + await admitTurn(store, 'turn-run-1', 'run-1', null, 1); + await admitTurn(store, 'turn-run-2', 'run-2', 'turn-run-1', 2); + + const currentProjection = latestContext('current', 20); + Object.assign(currentProjection.snapshot, { requestPreservationPending: true }); + await store.appendEvent( + 'session-1', + 'run-2', + meteringEvent('run-2', 'current', 20, 'model', 40, 200, { + connectionSlug: 'connection', + requestObservation: requestObservation([ + { + kind: 'message', + index: 0, + cacheable: true, + comparison: 'exact', + digest: `sha256:${'a'.repeat(64)}`, + bytes: 10, + }, + ]), + }), + { durable: true, latestContext: currentProjection }, + ); + + let scanned = 0; + const counted = countingStore(store, () => { + scanned += 1; + }); + const first = await readLatestContextDiagnostics(counted, 'session-1'); + assert.equal(first.status, 'available'); + if (first.status !== 'available') return; + assert.equal(first.requestPreservation?.status, 'unavailable'); + assert.ok(scanned > 0, 'the pending projection is rebuilt once'); + + scanned = 0; + const second = await readLatestContextDiagnostics(counted, 'session-1'); + assert.equal(second.status, 'available'); + assert.equal(scanned, 0, 'terminal unavailability becomes a settled warm projection'); + } finally { + await rm(root, { recursive: true, force: true }); + } +}); + test('does not trust a pre-observation projection over its canonical attempt', async () => { const root = await mkdtemp(join(tmpdir(), 'maka-context-diagnostics-')); try { @@ -1056,9 +1189,30 @@ function countingStore( readEventLedgerRevision: (sessionId) => reader.readEventLedgerRevision(sessionId), repairEventProjection: (sessionId, type, event, options) => reader.repairEventProjection(sessionId, type, event, options), + readRootTurnAdmission: (sessionId, turnId) => reader.readRootTurnAdmission(sessionId, turnId), }; } +async function admitTurn( + store: ReturnType, + turnId: string, + runId: string, + previousRootTurnId: string | null, + admittedAt: number, +): Promise { + await store.admitRootTurn({ + sessionId: 'session-1', + turnId, + proposedRunId: runId, + proposedUserMessageId: `message-${turnId}`, + execution: { kind: 'external_message' }, + previousRootTurnId, + normalizedInput: { text: turnId }, + sourceMessages: [], + admittedAt, + }); +} + /** * The derived row as the canonical append commits it — the same shape the * store writes inside that transaction, never a separate ledger record. diff --git a/packages/runtime/src/__tests__/request-preservation.test.ts b/packages/runtime/src/__tests__/request-preservation.test.ts index a77da048b1..3df09e56ad 100644 --- a/packages/runtime/src/__tests__/request-preservation.test.ts +++ b/packages/runtime/src/__tests__/request-preservation.test.ts @@ -237,6 +237,45 @@ test('uses the unique continuation tip for the previous root turn', async () => ); }); +test('leaves an admitted predecessor unresolved until its canonical attempt is durable', async () => { + const current = attempt({ attemptId: 'current', runId: 'run-2', turnId: 'turn-2' }); + + assert.equal( + await deriveAttemptRequestPreservation({ + current, + store: preservationStore( + [ + run('run-1', 'turn-1', PROVIDER_STATE, { status: 'running' }), + run('run-2', 'turn-2', PROVIDER_STATE), + ], + [], + { runId: 'run-2', previousRootTurnId: 'turn-1' }, + ), + }), + undefined, + ); +}); + +test('leaves durable lineage unresolved while an expected run header is not readable', async () => { + const current = attempt({ attemptId: 'current', runId: 'run-2', turnId: 'turn-2' }); + const admission = { runId: 'run-2', previousRootTurnId: 'turn-1' }; + + assert.equal( + await deriveAttemptRequestPreservation({ + current, + store: preservationStore([run('run-2', 'turn-2', PROVIDER_STATE)], [], admission), + }), + undefined, + ); + assert.equal( + await deriveAttemptRequestPreservation({ + current, + store: preservationStore([run('run-1', 'turn-1', PROVIDER_STATE)], [], admission), + }), + undefined, + ); +}); + test('compares later local turns without inheriting a copied session baseline', async () => { const previous = attempt({ attemptId: 'local-1', runId: 'run-1', turnId: 'turn-1' }); const current = attempt({ attemptId: 'local-2', runId: 'run-2', turnId: 'turn-2' }); @@ -287,7 +326,7 @@ async function statusOf( current, store: preservationStore(runs, attempts, admission), }) - ).status; + )?.status; } function observation(segments: PreparedRequestObservationSegment[]): PreparedRequestObservation { diff --git a/packages/runtime/src/agent-run.ts b/packages/runtime/src/agent-run.ts index 74b08d8e5d..d40602f177 100644 --- a/packages/runtime/src/agent-run.ts +++ b/packages/runtime/src/agent-run.ts @@ -482,14 +482,15 @@ export class AgentRun { const requestPreservation = await deriveAttemptRequestPreservation({ current: attempt, store: runStore, - }).catch(() => ({ - status: 'unavailable' as const, - previousSegmentCount: 0 as const, - preservedSegmentCount: 0 as const, - })); + }).catch(() => undefined); projected = { ...projected, - snapshot: { ...projected.snapshot, requestPreservation }, + snapshot: { + ...projected.snapshot, + ...(requestPreservation + ? { requestPreservation } + : { requestPreservationPending: true }), + }, }; } await runStore.appendEvent( diff --git a/packages/runtime/src/context-diagnostics.ts b/packages/runtime/src/context-diagnostics.ts index d3aff6ce69..756f107332 100644 --- a/packages/runtime/src/context-diagnostics.ts +++ b/packages/runtime/src/context-diagnostics.ts @@ -173,7 +173,7 @@ export async function readLatestContextDiagnostics( .catch(() => undefined); if (projected === null) return { status: 'unavailable', reason: 'no_completed_request' }; const snapshot = readLatestContextSnapshot(projected ?? undefined); - if (snapshot) return availableFrom(snapshot); + if (snapshot && !snapshot.requestPreservationPending) return availableFrom(snapshot); if (projected) replaceProjectionId = projected.id; } const ledgerRevision = @@ -282,7 +282,11 @@ async function rebuildContextFromLedger( ...(resolved.contextWindow !== undefined ? { contextWindow: resolved.contextWindow } : {}), ...(composition ? { composition } : {}), ...(boundary ? { compaction: contextDiagnosticsCompactionOf(boundary.checkpoint) } : {}), - ...(requestPreservation ? { requestPreservation } : {}), + ...(requestPreservation + ? { requestPreservation } + : anchor?.attempt + ? { requestPreservationPending: true as const } + : {}), }; // Repair on the way out, so this scan happens once per session rather than // on every panel refresh. Best-effort: the caller already has its answer, @@ -368,6 +372,15 @@ function legacyProviderAnchor(event: AgentRunEvent): MeteringAnchor | undefined type LegacyProviderAnchor = MeteringAnchor; function availableFrom(snapshot: LatestContextSnapshot): ContextDiagnostics { + const requestPreservation = + snapshot.requestPreservation ?? + (snapshot.requestPreservationPending + ? { + status: 'unavailable' as const, + previousSegmentCount: 0 as const, + preservedSegmentCount: 0 as const, + } + : undefined); return { status: 'available', providerId: snapshot.providerId, @@ -380,7 +393,7 @@ function availableFrom(snapshot: LatestContextSnapshot): ContextDiagnostics { ...(snapshot.contextWindow !== undefined ? { contextWindow: snapshot.contextWindow } : {}), ...(snapshot.composition ? { composition: snapshot.composition } : {}), ...(snapshot.compaction ? { compaction: snapshot.compaction } : {}), - ...(snapshot.requestPreservation ? { requestPreservation: snapshot.requestPreservation } : {}), + ...(requestPreservation ? { requestPreservation } : {}), }; } diff --git a/packages/runtime/src/latest-context-snapshot.ts b/packages/runtime/src/latest-context-snapshot.ts index 89b632c6cb..080d0a8e8c 100644 --- a/packages/runtime/src/latest-context-snapshot.ts +++ b/packages/runtime/src/latest-context-snapshot.ts @@ -70,6 +70,8 @@ export interface LatestContextSnapshot { compaction?: ContextDiagnosticsCompaction; /** Runtime-owned conclusion; consumers must not select or compare observations. */ requestPreservation?: RequestPreservation; + /** Internal retry marker; never projected through Runtime Host. */ + requestPreservationPending?: true; } /** @@ -139,6 +141,7 @@ export function readLatestContextSnapshot( 'composition', 'compaction', 'requestPreservation', + 'requestPreservationPending', ], ); if (!record) return undefined; @@ -154,7 +157,11 @@ export function readLatestContextSnapshot( (!isCount(record.contextWindow) || record.contextWindow === 0)) || (record.composition !== undefined && !isContextDiagnosticsComposition(record.composition)) || (record.compaction !== undefined && !isContextDiagnosticsCompaction(record.compaction)) || - (record.requestPreservation !== undefined && !isRequestPreservation(record.requestPreservation)) + (record.requestPreservation !== undefined && + !isRequestPreservation(record.requestPreservation)) || + (record.requestPreservationPending !== undefined && + record.requestPreservationPending !== true) || + (record.requestPreservation !== undefined && record.requestPreservationPending === true) ) { return undefined; } diff --git a/packages/runtime/src/request-preservation.ts b/packages/runtime/src/request-preservation.ts index cb393af7e1..07daa2f96b 100644 --- a/packages/runtime/src/request-preservation.ts +++ b/packages/runtime/src/request-preservation.ts @@ -67,18 +67,28 @@ interface AttemptPreservationInput { store: PreservationStore; } +const PREDECESSOR_PENDING = Symbol('predecessor_pending'); +type Predecessor = { attempt: ModelCallAttempt; run: AgentRunHeader }; +type PredecessorResolution = Predecessor | null | undefined | typeof PREDECESSOR_PENDING; +interface AttemptCandidates { + attempts: ModelCallAttempt[]; + sawInvalidAttemptEvent: boolean; +} + export async function deriveAttemptRequestPreservation( input: AttemptPreservationInput, -): Promise { +): Promise { const { current, store } = input; if (current.callKind !== 'main' || !current.requestObservation) { return unavailable(); } const runs = await store.listSessionRuns(current.sessionId); const currentRun = runs.find((run) => run.runId === current.runId); - if (!currentRun || !isSessionInlineRun(currentRun)) return unavailable(); + if (!currentRun) return undefined; + if (!isSessionInlineRun(currentRun)) return unavailable(); const predecessor = await predecessorAttempt(current, currentRun, runs, store); + if (predecessor === PREDECESSOR_PENDING) return undefined; if (predecessor === null) { return { status: 'no_predecessor', previousSegmentCount: 0, preservedSegmentCount: 0 }; } @@ -162,9 +172,9 @@ async function predecessorAttempt( currentRun: AgentRunHeader, runs: readonly AgentRunHeader[], store: PreservationStore, -): Promise<{ attempt: ModelCallAttempt; run: AgentRunHeader } | null | undefined> { +): Promise { if (current.attempt > 0) { - return uniqueAttempt( + return expectedAttempt( currentRun, await attemptsFor(store, currentRun), (candidate) => @@ -173,16 +183,19 @@ async function predecessorAttempt( ); } if (current.step > 0) { - return latestAttempt( + return expectedLatestAttempt( currentRun, - (await attemptsFor(store, currentRun)).filter( + filterAttempts( + await attemptsFor(store, currentRun), (candidate) => candidate.turnId === current.turnId && candidate.step === current.step - 1, ), ); } if (currentRun.parentRunId) { const parent = runs.find((run) => run.runId === currentRun.parentRunId); - return parent ? latestAttempt(parent, await attemptsFor(store, parent)) : undefined; + return parent + ? expectedLatestAttempt(parent, await attemptsFor(store, parent)) + : PREDECESSOR_PENDING; } if ( currentRun.parentSessionId || @@ -212,8 +225,9 @@ async function predecessorAttempt( const previousRuns = runs.filter( (run) => run.turnId === admission.previousRootTurnId && isSessionInlineRun(run), ); + if (previousRuns.length === 0) return PREDECESSOR_PENDING; const previous = uniqueDurableRunTip(previousRuns); - return previous ? latestAttempt(previous, await attemptsFor(store, previous)) : undefined; + return previous ? expectedLatestAttempt(previous, await attemptsFor(store, previous)) : undefined; } function uniqueDurableRunTip(runs: readonly AgentRunHeader[]): AgentRunHeader | undefined { @@ -241,20 +255,31 @@ function uniqueDurableRunTip(runs: readonly AgentRunHeader[]): AgentRunHeader | async function attemptsFor( store: PreservationStore, run: AgentRunHeader, -): Promise { +): Promise { const attempts: ModelCallAttempt[] = []; + let sawInvalidAttemptEvent = false; for (const event of await store.readEvents(run.sessionId, run.runId)) { if (event.type !== 'model_call_attempt_recorded') continue; try { const attempt = decodeModelCallAttempt(event.data); if (attempt.callKind === 'main' && isCanonicalAttemptForRun(event, attempt, run)) { attempts.push(attempt); + } else { + sawInvalidAttemptEvent = true; } } catch { // An unreadable attempt cannot become a guessed baseline. + sawInvalidAttemptEvent = true; } } - return attempts; + return { attempts, sawInvalidAttemptEvent }; +} + +function filterAttempts( + candidates: AttemptCandidates, + predicate: (attempt: ModelCallAttempt) => boolean, +): AttemptCandidates { + return { ...candidates, attempts: candidates.attempts.filter(predicate) }; } export function isCanonicalAttemptForRun( @@ -284,6 +309,36 @@ function latestAttempt( return uniqueAttempt(run, onStep, (attempt) => attempt.attempt === physical); } +function expectedLatestAttempt( + run: AgentRunHeader, + candidates: AttemptCandidates, +): Predecessor | undefined | typeof PREDECESSOR_PENDING { + if (candidates.attempts.length === 0) { + return candidates.sawInvalidAttemptEvent || isTerminalRun(run) + ? undefined + : PREDECESSOR_PENDING; + } + return latestAttempt(run, candidates.attempts); +} + +function expectedAttempt( + run: AgentRunHeader, + candidates: AttemptCandidates, + predicate: (attempt: ModelCallAttempt) => boolean, +): Predecessor | undefined | typeof PREDECESSOR_PENDING { + const matches = candidates.attempts.filter(predicate); + if (matches.length === 0) { + return candidates.sawInvalidAttemptEvent || isTerminalRun(run) + ? undefined + : PREDECESSOR_PENDING; + } + return matches.length === 1 ? { attempt: matches[0]!, run } : undefined; +} + +function isTerminalRun(run: AgentRunHeader): boolean { + return run.status === 'completed' || run.status === 'failed' || run.status === 'cancelled'; +} + function uniqueAttempt( run: AgentRunHeader, attempts: readonly ModelCallAttempt[],