You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maka.apache.org by as...@apache.org on 2026/09/07 13:24:28 UTC
(maka) 01/02: fix(transcript): project bounded turn failure diagnostics
This is an automated email from the ASF dual-hosted git repository.
Astro-Han pushed a commit to branch fix/4502-turn-failure-details
in repository https://gitbox.apache.org/repos/asf/maka.git
commit c260b1f068a83e27287af1b1d12b0157535c5229
Author: AstroHan <le...@outlook.com>
AuthorDate: Mon Sep 7 21:16:34 2026 +0800
fix(transcript): project bounded turn failure diagnostics
Generated-by: Codex
---
packages/core/src/session.ts | 14 ++++++++-
.../src/__tests__/session-transcript-pager.test.ts | 4 +++
.../src/__tests__/session-turns.test.ts | 5 ++++
packages/runtime-host/src/protocol/index.ts | 2 +-
.../runtime-host/src/protocol/session-turns.ts | 11 ++++++++
.../src/__tests__/runtime-event-read-model.test.ts | 33 +++++++++++++++++++++-
packages/runtime/src/runtime-event-read-model.ts | 12 ++++++++
packages/ui/src/materialize.ts | 2 ++
8 files changed, 80 insertions(+), 3 deletions(-)
diff --git a/packages/core/src/session.ts b/packages/core/src/session.ts
index 342ff47eaf..2b6cf566a5 100644
--- a/packages/core/src/session.ts
+++ b/packages/core/src/session.ts
@@ -17,7 +17,11 @@
* under the License.
*/
-import { isModelRetryDecision, type ModelRetryDecision } from './model-failure.js';
+import {
+ MODEL_FAILURE_MESSAGE_MAX_BYTES,
+ isModelRetryDecision,
+ type ModelRetryDecision,
+} from './model-failure.js';
import {
decodeMessageContent,
@@ -937,6 +941,7 @@ export interface TurnStateMessage {
/** Diagnostic source for user/renderer-triggered aborts, e.g. renderer.stop_button. */
abortSource?: string;
errorClass?: string;
+ failureMessage?: string;
retry?: ModelRetryDecision;
/** Legacy retained-output hint; current projections derive this from output contributions. */
partialOutputRetained?: boolean;
@@ -1148,6 +1153,7 @@ export interface TurnRecord {
abortedAt?: number;
abortSource?: string;
errorClass?: string;
+ failureMessage?: string;
retry?: ModelRetryDecision;
partialOutputRetained: boolean;
}
@@ -1268,6 +1274,7 @@ const TURN_STATE_MESSAGE_SHAPE = defineObjectShape<TurnStateMessage>()(
'abortedAt',
'abortSource',
'errorClass',
+ 'failureMessage',
'retry',
],
);
@@ -1562,6 +1569,10 @@ function decodeMessage(
(message.abortedAt === undefined || isFiniteNumber(message.abortedAt)) &&
isOptionalString(message.abortSource) &&
isOptionalString(message.errorClass) &&
+ (message.failureMessage === undefined ||
+ (typeof message.failureMessage === 'string' &&
+ new TextEncoder().encode(message.failureMessage).byteLength <=
+ MODEL_FAILURE_MESSAGE_MAX_BYTES)) &&
(message.retry === undefined || isModelRetryDecision(message.retry))
)
return message as unknown as TurnStateMessage;
@@ -1856,6 +1867,7 @@ export function deriveTurnRecords(messages: readonly StoredMessage[]): TurnRecor
...(latestState.abortedAt !== undefined ? { abortedAt: latestState.abortedAt } : {}),
...(latestState.abortSource ? { abortSource: latestState.abortSource } : {}),
...(latestState.errorClass ? { errorClass: latestState.errorClass } : {}),
+ ...(latestState.failureMessage ? { failureMessage: latestState.failureMessage } : {}),
...(latestState.retry ? { retry: latestState.retry } : {}),
partialOutputRetained: latestState.partialOutputRetained || partialOutputRetained,
};
diff --git a/packages/runtime-host/src/__tests__/session-transcript-pager.test.ts b/packages/runtime-host/src/__tests__/session-transcript-pager.test.ts
index 0c4a63a7d6..95924d6bc6 100644
--- a/packages/runtime-host/src/__tests__/session-transcript-pager.test.ts
+++ b/packages/runtime-host/src/__tests__/session-transcript-pager.test.ts
@@ -88,6 +88,7 @@ test('preserves the canonical retry decision in shared bootstrap and later pages
status: 'failed',
errorClass: 'stream_truncated',
retry: { decision: 'declined', because: 'side_effects' },
+ failureMessage: 'Private provider diagnostic',
},
];
const reader = transcriptReader(durable);
@@ -101,6 +102,7 @@ test('preserves the canonical retry decision in shared bootstrap and later pages
maxBytes: 1024,
projection: 'shared',
});
+ assert.equal(decodeBootstrap(bootstrap.durable)[0]?.failureMessage, undefined);
assert.deepEqual(decodeBootstrap(bootstrap.durable)[0]?.retry, {
decision: 'declined',
because: 'side_effects',
@@ -113,6 +115,7 @@ test('preserves the canonical retry decision in shared bootstrap and later pages
status: 'failed',
errorClass: 'stream_truncated',
retry: { decision: 'exhausted', attempts: 2 },
+ failureMessage: 'Another private diagnostic',
});
updateSubscriberTranscriptHighWater(state, 1);
const page = await readSessionTranscriptPage({
@@ -128,6 +131,7 @@ test('preserves the canonical retry decision in shared bootstrap and later pages
maxBytes: 1024,
},
});
+ assert.equal(decodeBootstrap(page)[0]?.failureMessage, undefined);
assert.deepEqual(decodeBootstrap(page)[0]?.retry, { decision: 'exhausted', attempts: 2 });
});
diff --git a/packages/runtime-host/src/__tests__/session-turns.test.ts b/packages/runtime-host/src/__tests__/session-turns.test.ts
index 5a8798dd42..9d0ebcbd4c 100644
--- a/packages/runtime-host/src/__tests__/session-turns.test.ts
+++ b/packages/runtime-host/src/__tests__/session-turns.test.ts
@@ -18,6 +18,7 @@
*/
import assert from 'node:assert/strict';
+import { MODEL_FAILURE_MESSAGE_MAX_BYTES } from '@maka/core/model-failure';
import test from 'node:test';
import {
decodeSessionTurnsQueryResult,
@@ -86,6 +87,7 @@ test('bounds turn diagnostics before publishing a contribution', () => {
status: 'failed',
partialOutputRetained: false,
errorClass: '失败'.repeat(100_000),
+ failureMessage: '失败'.repeat(100_000),
retry: { decision: 'declined', because: 'side_effects' },
},
},
@@ -110,6 +112,9 @@ test('bounds turn diagnostics before publishing a contribution', () => {
}),
);
const turn = projectSessionTurnContribution(contribution);
+ assert.ok(turn.failureMessage);
+ assert.ok(Buffer.byteLength(turn.failureMessage) <= MODEL_FAILURE_MESSAGE_MAX_BYTES);
+ assert.equal(turn.failureMessage, contribution.latestState!.message.failureMessage);
assert.deepEqual(turn.retry, { decision: 'declined', because: 'side_effects' });
});
diff --git a/packages/runtime-host/src/protocol/index.ts b/packages/runtime-host/src/protocol/index.ts
index 698324a282..f552a3bf0f 100644
--- a/packages/runtime-host/src/protocol/index.ts
+++ b/packages/runtime-host/src/protocol/index.ts
@@ -101,7 +101,7 @@ 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 = 124 as const;
+export const RUNTIME_HOST_COMPATIBILITY_EPOCH = 125 as const;
// 124: PTY delivery is independent of the ordered Session state stream. A
// bounded PTY overflow requests terminal-only snapshot recovery.
// 123: Failed turns carry canonical retry decisions through bounded projections.
diff --git a/packages/runtime-host/src/protocol/session-turns.ts b/packages/runtime-host/src/protocol/session-turns.ts
index e984c16962..dde40c4e1b 100644
--- a/packages/runtime-host/src/protocol/session-turns.ts
+++ b/packages/runtime-host/src/protocol/session-turns.ts
@@ -17,6 +17,7 @@
* under the License.
*/
+import { MODEL_FAILURE_MESSAGE_MAX_BYTES } from '@maka/core/model-failure';
import { decodeCanonicalMessage, type TurnRecord, type TurnStateMessage } from '@maka/core/session';
import { truncateUtf8 } from '@maka/core/diagnostic-log';
import {
@@ -179,6 +180,15 @@ function projectTurnStateMessageForWire(message: TurnStateMessage): TurnStateMes
...(message.errorClass
? { errorClass: truncateUtf8(message.errorClass, SESSION_TURN_DIAGNOSTIC_MAX_BYTES) }
: {}),
+ ...(message.failureMessage
+ ? {
+ failureMessage: truncateUtf8(
+ message.failureMessage,
+ MODEL_FAILURE_MESSAGE_MAX_BYTES,
+ '…',
+ ),
+ }
+ : {}),
...(message.retry ? { retry: message.retry } : {}),
...(message.partialOutputRetained !== undefined
? { partialOutputRetained: message.partialOutputRetained }
@@ -208,6 +218,7 @@ export function projectSessionTurnContribution(contribution: SessionTurnContribu
...(state.abortedAt !== undefined ? { abortedAt: state.abortedAt } : {}),
...(state.abortSource ? { abortSource: state.abortSource } : {}),
...(state.errorClass ? { errorClass: state.errorClass } : {}),
+ ...(state.failureMessage ? { failureMessage: state.failureMessage } : {}),
...(state.retry ? { retry: state.retry } : {}),
partialOutputRetained: state.partialOutputRetained || partialOutputRetained,
};
diff --git a/packages/runtime/src/__tests__/runtime-event-read-model.test.ts b/packages/runtime/src/__tests__/runtime-event-read-model.test.ts
index 97582e1db2..9a9399b8c7 100644
--- a/packages/runtime/src/__tests__/runtime-event-read-model.test.ts
+++ b/packages/runtime/src/__tests__/runtime-event-read-model.test.ts
@@ -23,7 +23,7 @@ import type { RuntimeInvocationRecord } from '@maka/core/runtime-invocation';
import type { CreateSessionInput, SessionListFilter } from '@maka/core/runtime-inputs';
import type { RuntimeEvent, RuntimeEventActions } from '@maka/core/runtime-event';
import type { SessionHeader, SessionSummary, StoredMessage, TurnRecord } from '@maka/core/session';
-import { deriveTurnRecords } from '@maka/core/session';
+import { deriveTurnRecords, decodeCanonicalMessage } from '@maka/core/session';
import {
compareRuntimeReadModelMessages,
isHardRuntimeEventReadModelDiagnostic,
@@ -1593,6 +1593,37 @@ describe('projectRuntimeEventsToStoredMessages', () => {
assert.deepStrictEqual(out.diagnostics, []);
});
+ test('failure diagnostics survive terminal projection and cold decoding', () => {
+ const message =
+ 'Quota exceeded for this account (code=insufficient_quota, status=429, requestId=req-4502)';
+ const out = projectRuntimeEventsToStoredMessages(
+ [
+ ev({
+ id: 'provider-failed',
+ status: 'failed',
+ content: {
+ kind: 'error',
+ message,
+ retry: { decision: 'declined', because: 'side_effects' },
+ },
+ actions: { endInvocation: true, stateDelta: { failureClass: 'rate_limit' } },
+ }),
+ ],
+ { invocations: [endedAs('failed', 'rate_limit')] },
+ );
+ const live = deriveTurnRecords(out.messages)[0];
+ const cold = deriveTurnRecords(
+ JSON.parse(JSON.stringify(out.messages)).map(decodeCanonicalMessage),
+ )[0];
+ assert.equal(live.failureMessage, message);
+ assert.deepEqual(cold, live);
+ assert.equal(live.errorClass, 'rate_limit');
+ assert.throws(() =>
+ decodeCanonicalMessage({ ...out.messages[0], failureMessage: '界'.repeat(2048) }),
+ );
+ assert.deepEqual(live.retry, { decision: 'declined', because: 'side_effects' });
+ });
+
test('a session written with the retired context_budget_exhausted reads back as context_overflow', () => {
// The runtime no longer decides locally that a request cannot be made to
// fit, so that outcome is gone from the live contract. Sessions persisted
diff --git a/packages/runtime/src/runtime-event-read-model.ts b/packages/runtime/src/runtime-event-read-model.ts
index f0478bf0ad..f79dfa543d 100644
--- a/packages/runtime/src/runtime-event-read-model.ts
+++ b/packages/runtime/src/runtime-event-read-model.ts
@@ -17,6 +17,9 @@
* under the License.
*/
+import { MODEL_FAILURE_MESSAGE_MAX_BYTES } from '@maka/core/model-failure';
+import { truncateUtf8 } from '@maka/core/diagnostic-log';
+import { redactSecrets } from '@maka/core/redaction';
import type { RuntimeInvocationRecord } from '@maka/core/runtime-invocation';
import type { AssistantStepContentKind, StoredMessage, TurnStatus } from '@maka/core/session';
import type { RuntimeEvent, RuntimeEventStatus } from '@maka/core/runtime-event';
@@ -1194,6 +1197,15 @@ function projectTerminalTurnState(
...(status === 'aborted' ? { abortedAt: event.ts } : {}),
...(abortSource ? { abortSource } : {}),
...(status === 'failed' ? { errorClass: failureClass ?? 'unknown' } : {}),
+ ...(status === 'failed' && event.content?.kind === 'error' && event.content.message
+ ? {
+ failureMessage: truncateUtf8(
+ redactSecrets(event.content.message),
+ MODEL_FAILURE_MESSAGE_MAX_BYTES,
+ '…',
+ ),
+ }
+ : {}),
...(status === 'failed' && event.content?.kind === 'error' && event.content.retry
? { retry: event.content.retry }
: {}),
diff --git a/packages/ui/src/materialize.ts b/packages/ui/src/materialize.ts
index a1663b77e3..d0de3771af 100644
--- a/packages/ui/src/materialize.ts
+++ b/packages/ui/src/materialize.ts
@@ -415,6 +415,7 @@ export interface TurnViewModel {
abortedAt?: number;
abortSource?: string;
errorClass?: string;
+ failureMessage?: string;
retry?: import('@maka/core/model-failure').ModelRetryDecision;
user?: ChatItem;
tools: ToolActivityItem[];
@@ -740,6 +741,7 @@ export function materializeTurns(
? { abortedAt: record.abortedAt }
: {}),
...(record?.abortSource ? { abortSource: record.abortSource } : {}),
+ ...(record?.failureMessage ? { failureMessage: record.failureMessage } : {}),
...(record?.errorClass ? { errorClass: record.errorClass } : {}),
...(record?.retry ? { retry: record.retry } : {}),
tools: [],
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org