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