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/25 15:07:40 UTC

(maka) branch main updated: fix(ui): drop the duplicate choice history row and render AskUserQuestion answers (#5721)

This is an automated email from the ASF dual-hosted git repository.

Astro-Han pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/maka.git


The following commit(s) were added to refs/heads/main by this push:
     new 8d5a3cac35 fix(ui): drop the duplicate choice history row and render AskUserQuestion answers (#5721)
8d5a3cac35 is described below

commit 8d5a3cac3504c36cb1849b060153b58188e2c7b2
Author: AstroHan <le...@outlook.com>
AuthorDate: Fri Sep 25 23:07:34 2026 +0800

    fix(ui): drop the duplicate choice history row and render AskUserQuestion answers (#5721)
    
    Since #5592 every answered AskUserQuestion showed twice: the tool row kept its answer, and a second "You selected" row was appended after the Turn, outside the conversation column and below a Turn still running. That row came from a parallel projection: the Runtime Host turned every settled form or question from the InteractionStore into a form_interaction transcript message, on the premise that settled choices were lost on replay. They are not: the durable AskUserQuestion tool call k [...]
    
    The projection and its machinery are removed: form_interaction with its decoder, reader projection, UI row and styles; the transcript_changed close reason and its CLI/Desktop recovery branches; listTurnInteractions, its index and the transcript sequence stride change. The tool row is the one record, and its expanded AskUserQuestion view now lists each question with its options and marks the chosen one, using a formatter in @maka/core/tool-quiet-preview that the CLI transcript shares.
    
    Compatibility: RUNTIME_HOST_COMPATIBILITY_EPOCH is 189, since the message type and close reason leave the wire. The core execution migration drops core_interaction_requests_by_turn, which databases opened by a build with #5592 carry; without it the target-schema check refuses to open the workspace.
    
    Generated-by: Claude Code
---
 ...runtime-host-session-subscription-owner.test.ts |   4 +-
 .../runtime-host-session-subscription-owner.ts     |   1 -
 docs/astryx-surface-file-inventory.md              |   3 +-
 docs/astryx-surface-file-inventory.paths           |   1 -
 .../runtime-host-prompt-transcript.test.ts         |  37 ----
 .../__tests__/runtime-host-session-driver.test.ts  |  62 +++----
 packages/cli/src/pi-transcript-tools.ts            |  15 +-
 packages/cli/src/runtime-host-session-channel.ts   |  13 +-
 .../core/src/__tests__/tool-quiet-preview.test.ts  |  31 ++++
 packages/core/src/session.ts                       |  43 -----
 packages/core/src/tool-quiet-preview.ts            |  33 ++++
 packages/core/src/transcript-search.ts             |   1 -
 .../session-continuity-coordinator.test.ts         |  45 -----
 .../src/__tests__/session-transcript-pager.test.ts |  21 ---
 .../__tests__/session-transcript-reader.test.ts    | 197 ++-------------------
 .../src/client/session-subscription.ts             |   7 -
 packages/runtime-host/src/protocol/index.ts        |   3 +-
 .../src/protocol/session-continuity.ts             |   5 +-
 .../src/server/session-continuity-coordinator.ts   |  29 +--
 .../src/server/session-transcript-reader.ts        |  81 ++-------
 .../src/server/shared-session-transcript.ts        |   3 -
 packages/runtime/src/runtime-event-backfill.ts     |   1 -
 .../__tests__/sqlite-core-execution-store.test.ts  |   6 +-
 packages/storage/src/interaction-store-contract.ts |   1 -
 packages/storage/src/interaction-store.ts          |  16 --
 .../storage/src/sqlite-core-execution-schema.ts    |   4 +-
 .../src/test-only/memory-execution-control.ts      |  12 --
 .../__tests__/form-interaction-history.test.tsx    |  69 --------
 .../__tests__/tool-activity-presentation.test.ts   |  19 ++
 packages/ui/src/chat-view.tsx                      |  14 +-
 packages/ui/src/form-interaction-history.tsx       |  80 ---------
 packages/ui/src/styles.css                         |  12 --
 packages/ui/src/tool-activity.tsx                  |   8 +
 33 files changed, 189 insertions(+), 688 deletions(-)

diff --git a/apps/desktop/src/main/__tests__/runtime-host-session-subscription-owner.test.ts b/apps/desktop/src/main/__tests__/runtime-host-session-subscription-owner.test.ts
index dc33f4a193..a8f671fee8 100644
--- a/apps/desktop/src/main/__tests__/runtime-host-session-subscription-owner.test.ts
+++ b/apps/desktop/src/main/__tests__/runtime-host-session-subscription-owner.test.ts
@@ -209,7 +209,7 @@ test('reseeds an evicted replica on the same live subscription', async () => {
   await owner.close();
 });
 
-for (const reason of ['slow_consumer', 'transcript_changed'] as const) test(`a reseed superseded by subscription recovery does not displace the new replica (${reason})`, async () => {
+test('a reseed superseded by subscription recovery does not displace the new replica', async () => {
   const firstEvents = new AsyncFrameQueue();
   const secondEvents = new AsyncFrameQueue();
   const reseedFetch = deferred<void>();
@@ -270,7 +270,7 @@ for (const reason of ['slow_consumer', 'transcript_changed'] as const) test(`a r
     hostEpoch: 'host-1',
     subscriptionId: 'subscription-1',
     sequence: 1,
-    reason,
+    reason: 'slow_consumer',
   });
   await pollFor(() => opens === 2);
   reseedFetch.resolve(undefined);
diff --git a/apps/desktop/src/main/runtime-host-session-subscription-owner.ts b/apps/desktop/src/main/runtime-host-session-subscription-owner.ts
index dca2a7ab4d..4cccd55eb8 100644
--- a/apps/desktop/src/main/runtime-host-session-subscription-owner.ts
+++ b/apps/desktop/src/main/runtime-host-session-subscription-owner.ts
@@ -443,7 +443,6 @@ function isRecoverableSubscriptionFailure(error: unknown): boolean {
   if (!(error instanceof RuntimeHostSubscriptionError)) return false;
   return (
     error.reason === 'slow_consumer' ||
-    error.reason === 'transcript_changed' ||
     error.reason === 'sequence_gap' ||
     error.reason === 'projection_revision_invalid'
   );
diff --git a/docs/astryx-surface-file-inventory.md b/docs/astryx-surface-file-inventory.md
index e97630c219..455397fd2a 100644
--- a/docs/astryx-surface-file-inventory.md
+++ b/docs/astryx-surface-file-inventory.md
@@ -6,7 +6,7 @@ Generated against `@astryxdesign/core@0.6.2` (195 component exports).
 
 Wiki bar: Design Conventions · API Use-the-System · Theming · Container Padding.
 
-**Totals:** 299 files — blocker 0, reimplementation 0, polish 4, aligned 295.
+**Totals:** 298 files — blocker 0, reimplementation 0, polish 4, aligned 294.
 
 ## Exclusions (explicit)
 
@@ -274,7 +274,6 @@ Wiki bar: Design Conventions · API Use-the-System · Theming · Container Paddi
 | `packages/ui/src/daily-review-panel.tsx` | module-hub | Banner, Button, Divider, EmptyState, HStack, Heading, List, ListItem, SegmentedControl, SegmentedControlItem, Skeleton, StackItem, Text, Toolbar, VStack | aligned — uses Astryx (Banner, Button, Divider, EmptyState, HStack, Heading, List, ListItem) | aligned |
 | `packages/ui/src/directory-reference-chip.tsx` | ui-composition | Token, Tooltip | aligned — uses Astryx (Token, Tooltip) | aligned |
 | `packages/ui/src/executor-model-picker.tsx` | ui-composition | Button, Popover | aligned — uses Astryx (Button, Popover) | aligned |
-| `packages/ui/src/form-interaction-history.tsx` | ui-composition | none | aligned — no raw controls; no Astryx JSX usage | aligned |
 | `packages/ui/src/form-interaction-prompt.tsx` | ui-composition | Button, CheckboxInput, RadioList, RadioListItem, Selector, Text, TextInput | aligned — uses Astryx (Button, CheckboxInput, RadioList, RadioListItem, Selector, Text, TextInput) | aligned |
 | `packages/ui/src/icons.tsx` | ui-composition | none | aligned — no raw controls; no Astryx JSX usage | aligned |
 | `packages/ui/src/inline-reference.tsx` | ui-composition | ChatTokenizedText | aligned — uses Astryx (ChatTokenizedText) | aligned |
diff --git a/docs/astryx-surface-file-inventory.paths b/docs/astryx-surface-file-inventory.paths
index 8889912e84..03477d1e05 100644
--- a/docs/astryx-surface-file-inventory.paths
+++ b/docs/astryx-surface-file-inventory.paths
@@ -244,7 +244,6 @@ packages/ui/src/composer.tsx
 packages/ui/src/daily-review-panel.tsx
 packages/ui/src/directory-reference-chip.tsx
 packages/ui/src/executor-model-picker.tsx
-packages/ui/src/form-interaction-history.tsx
 packages/ui/src/form-interaction-prompt.tsx
 packages/ui/src/icons.tsx
 packages/ui/src/inline-reference.tsx
diff --git a/packages/cli/src/__tests__/runtime-host-prompt-transcript.test.ts b/packages/cli/src/__tests__/runtime-host-prompt-transcript.test.ts
index be4bad5b37..d1a38c2126 100644
--- a/packages/cli/src/__tests__/runtime-host-prompt-transcript.test.ts
+++ b/packages/cli/src/__tests__/runtime-host-prompt-transcript.test.ts
@@ -23,7 +23,6 @@ import { setImmediate } from 'node:timers/promises';
 import { deferred } from '@maka/core/test-only/async-primitives';
 import type { StoredMessage } from '@maka/core/session';
 import {
-  RuntimeHostOperationError,
   RuntimeHostSubscriptionError,
   type DecodedSessionTranscriptPage,
   type RuntimeHostSessionSubscription,
@@ -318,42 +317,6 @@ test('recovery across root turns rereads the old prompt below the new bootstrap
   await channel.close();
 });
 
-test('prompt transcript recovers when a retired subscription page fails before its close frame', async () => {
-  const first = new TranscriptSubscription('first', 7);
-  const second = new TranscriptSubscription('second', 31);
-  let opens = 0;
-  const channel = await openChannel(async () => (++opens === 1 ? first : second));
-  const transcript = channel.trackPromptTranscript('turn');
-  try {
-    first.advance(31);
-    await setImmediate();
-    // The Host has removed the subscription but its close frame has not arrived.
-    first.readPage = async () => {
-      throw new RuntimeHostOperationError(
-        'session.transcript.page',
-        'not_found',
-        'Session subscription was not found',
-      );
-    };
-    const expected = [result('tool', 'Form completed'), terminal()];
-    second.readPage = async (input) =>
-      second.page(input, [
-        { identity: 16, message: expected[0]! },
-        { identity: 24, message: expected[1]! },
-      ]);
-    const observed: StoredMessage[] = [];
-    await transcript.reconcile(async (messages) => {
-      observed.push(...messages);
-    });
-    assert.equal(opens, 2);
-    assert.deepEqual(observed, expected);
-    assert.equal(second.pages[0]?.anchorSequence, 7);
-  } finally {
-    transcript.dispose();
-    await channel.close();
-  }
-});
-
 test('prompt transcript rejects nonadvancing cursors and propagates consumer failures', async () => {
   for (const failure of ['cursor', 'consumer'] as const) {
     const subscription = new TranscriptSubscription('first', 7);
diff --git a/packages/cli/src/__tests__/runtime-host-session-driver.test.ts b/packages/cli/src/__tests__/runtime-host-session-driver.test.ts
index 538179862b..937e4b66be 100644
--- a/packages/cli/src/__tests__/runtime-host-session-driver.test.ts
+++ b/packages/cli/src/__tests__/runtime-host-session-driver.test.ts
@@ -2461,41 +2461,37 @@ describe('Runtime Host Maka Session driver', () => {
     assert.equal(statuses.at(-1), undefined);
   });
 
-  for (const reason of ['slow_consumer', 'transcript_changed'] as const)
-    test(`reopens a failed Session channel before starting the next turn (${reason})`, async () => {
-      const first = new FakeSubscription(
-        continuitySnapshot({ rootTurn: null }),
-        Promise.resolve([]),
-      );
-      const second = new FakeSubscription(
-        continuitySnapshot({ rootTurn: null }),
-        Promise.resolve([]),
-        'subscription-2',
-      );
-      const connection = new FakeConnection([first, second]);
-      const driver = createRuntimeHostMakaSessionDriver({
-        connection: connection.value,
-        cwd: '/tmp',
-        llmConnectionId: 'connection-1',
-        llmConnectionSlug: 'openai-main',
-        model: 'gpt-5',
-        newId: sequenceIds('turn-2'),
-      });
-      await driver.switchSession('session-1');
-
-      first.push({
-        kind: 'subscription.closed',
-        hostEpoch: 'host-1',
-        subscriptionId: 'subscription-1',
-        sequence: 1,
-        reason,
-      });
-      await new Promise((resolve) => setImmediate(resolve));
+  test('reopens a failed Session channel before starting the next turn', async () => {
+    const first = new FakeSubscription(continuitySnapshot({ rootTurn: null }), Promise.resolve([]));
+    const second = new FakeSubscription(
+      continuitySnapshot({ rootTurn: null }),
+      Promise.resolve([]),
+      'subscription-2',
+    );
+    const connection = new FakeConnection([first, second]);
+    const driver = createRuntimeHostMakaSessionDriver({
+      connection: connection.value,
+      cwd: '/tmp',
+      llmConnectionId: 'connection-1',
+      llmConnectionSlug: 'openai-main',
+      model: 'gpt-5',
+      newId: sequenceIds('turn-2'),
+    });
+    await driver.switchSession('session-1');
 
-      const turn = await driver.preparePrompt('Continue');
-      second.push(deltaFrame(1, 'turn-2', 0, 'Recovered', 'subscription-2', 'run-2'));
-      assert.equal((await nextEvent(turn.events)).text, 'Recovered');
+    first.push({
+      kind: 'subscription.closed',
+      hostEpoch: 'host-1',
+      subscriptionId: 'subscription-1',
+      sequence: 1,
+      reason: 'slow_consumer',
     });
+    await new Promise((resolve) => setImmediate(resolve));
+
+    const turn = await driver.preparePrompt('Continue');
+    second.push(deltaFrame(1, 'turn-2', 0, 'Recovered', 'subscription-2', 'run-2'));
+    assert.equal((await nextEvent(turn.events)).text, 'Recovered');
+  });
 
   test('starts explicit Skills through the Host command and preserves its typed feedback', async () => {
     const subscription = new FakeSubscription(
diff --git a/packages/cli/src/pi-transcript-tools.ts b/packages/cli/src/pi-transcript-tools.ts
index 040a2dbaa4..e283a09758 100644
--- a/packages/cli/src/pi-transcript-tools.ts
+++ b/packages/cli/src/pi-transcript-tools.ts
@@ -18,7 +18,11 @@
  */
 
 import type { ToolOutputStream, ToolResultContent } from '@maka/core/events';
-import { formatQuietJsonValue, formatToolInvocationLine } from '@maka/core/tool-quiet-preview';
+import {
+  formatQuietJsonValue,
+  formatToolInvocationLine,
+  formatUserQuestionResult,
+} from '@maka/core/tool-quiet-preview';
 import { redactSecrets } from '@maka/core/display-redaction';
 import {
   isActiveShellRunStatus,
@@ -600,6 +604,11 @@ function plainResultText(entry: MakaPiToolEntry): string {
   if (result?.kind === 'text') return typeof result.text === 'string' ? result.text : '';
   if (result?.kind === 'json') {
     const value = result.value;
+    const answers =
+      entry.toolName === 'AskUserQuestion'
+        ? formatUserQuestionResult(entry.input, value, 'en')
+        : undefined;
+    if (answers) return answers;
     if (value !== null && typeof value === 'object') {
       const content = (value as { content?: unknown }).content;
       if (typeof content === 'string') return content;
@@ -610,8 +619,8 @@ function plainResultText(entry: MakaPiToolEntry): string {
     // Generic json fallback: use the shared quiet-value formatter instead of
     // dumping a single-line JSON blob. It extracts headline + body from
     // known shapes (lists, text payloads, Write/Edit results, key-value) and
-    // never produces escaped JSON braces (#1065). AskUserQuestion, GoalSet,
-    // ScheduledTask, and any future tool without a custom case render
+    // never produces escaped JSON braces (#1065). GoalSet, ScheduledTask,
+    // and any future tool without a custom case render
     // human-readable text here.
     const preview = formatQuietJsonValue(value, 'en');
     return preview.headline ? `${preview.headline}\n${preview.body}` : preview.body;
diff --git a/packages/cli/src/runtime-host-session-channel.ts b/packages/cli/src/runtime-host-session-channel.ts
index d6f8f76904..4b7ed07112 100644
--- a/packages/cli/src/runtime-host-session-channel.ts
+++ b/packages/cli/src/runtime-host-session-channel.ts
@@ -32,7 +32,6 @@ import {
 } from '@maka/runtime-host/adapter';
 import {
   isRuntimeHostReconnectingConnection,
-  RuntimeHostOperationError,
   RuntimeHostRequestInterruptedError,
   RuntimeHostSubscriptionError,
   type RuntimeHostConnection,
@@ -852,11 +851,6 @@ export class RuntimeHostSessionChannel {
 
   #canRecover(error: unknown): boolean {
     if (!isRuntimeHostReconnectingConnection(this.#connection)) return false;
-    // The Host can retire a transcript subscription before its close frame is
-    // consumed. Recover the invalidated read through the same bounded policy.
-    if (error instanceof RuntimeHostOperationError) {
-      return error.operation === 'session.transcript.page' && error.code === 'not_found';
-    }
     if (error instanceof RuntimeHostRequestInterruptedError) {
       return error.reason === 'connection_lost';
     }
@@ -865,7 +859,6 @@ export class RuntimeHostSessionChannel {
       (error.reason === 'connection_closed' ||
         error.reason === 'sequence_gap' ||
         error.reason === 'projection_revision_invalid' ||
-        error.reason === 'transcript_changed' ||
         error.reason === 'slow_consumer')
     );
   }
@@ -886,10 +879,10 @@ export class RuntimeHostSessionChannel {
       return;
     }
     if (frame.kind === 'subscription.closed') {
-      if (frame.reason === 'slow_consumer' || frame.reason === 'transcript_changed') {
+      if (frame.reason === 'slow_consumer') {
         throw new RuntimeHostSubscriptionError(
-          frame.reason,
-          'Runtime Host Session transcript requires a fresh subscription',
+          'slow_consumer',
+          'Runtime Host Session subscription consumer fell behind',
         );
       }
       this.#fail(new Error(`Runtime Host Session subscription closed: ${frame.reason}`));
diff --git a/packages/core/src/__tests__/tool-quiet-preview.test.ts b/packages/core/src/__tests__/tool-quiet-preview.test.ts
index 847fc19a5d..077ea1a19e 100644
--- a/packages/core/src/__tests__/tool-quiet-preview.test.ts
+++ b/packages/core/src/__tests__/tool-quiet-preview.test.ts
@@ -23,6 +23,7 @@ import {
   formatAsKeyValueLines,
   formatQuietJsonValue,
   formatToolInvocationLine,
+  formatUserQuestionResult,
   projectToolArgsPreview,
 } from '../tool-quiet-preview.js';
 import { projectToolActivityArgs } from '../tool-activity-args.js';
@@ -36,6 +37,36 @@ describe('tool quiet preview', () => {
     assert.doesNotMatch(key, /secret/);
     assert.match(key, /redacted/i);
   });
+
+  it('lists AskUserQuestion answers against the offered options', () => {
+    const args = {
+      questions: [
+        { question: 'Which client?', options: [{ label: 'claude' }, { label: 'maka' }] },
+        { question: 'Which scope?', options: [{ label: 'user' }, { label: 'project' }] },
+        { question: 'Anything else?', options: [{ label: 'no' }, { label: 'yes' }] },
+      ],
+    };
+    const value = {
+      answers: [
+        { question: 'Which client?', answer: 'maka' },
+        { question: 'Which scope?', answer: null },
+        { question: 'Anything else?', answer: 'typed reply' },
+      ],
+    };
+    assert.equal(
+      formatUserQuestionResult(projectToolActivityArgs('AskUserQuestion', args), value, 'zh-CN'),
+      'Which client?\n  claude\n✓ maka\n\nWhich scope?\n  user\n  project\n  未回答\n\nAnything else?\n  no\n  yes\n✓ typed reply',
+    );
+    // Live rows carry only the question-text args preview.
+    assert.equal(
+      formatUserQuestionResult(projectToolArgsPreview('AskUserQuestion', args), value, 'en'),
+      'Which client?\n✓ maka\n\nWhich scope?\n  Not answered\n\nAnything else?\n✓ typed reply',
+    );
+    assert.equal(
+      formatUserQuestionResult(args, { answers: [{ question: 'Which client?' }] }, 'en'),
+      undefined,
+    );
+  });
 });
 
 describe('formatToolInvocationLine', () => {
diff --git a/packages/core/src/session.ts b/packages/core/src/session.ts
index 1f3999b208..9ae0ac39bd 100644
--- a/packages/core/src/session.ts
+++ b/packages/core/src/session.ts
@@ -20,14 +20,6 @@
 import type { ExecutorConfiguration } from './executor-catalog.js';
 
 import { isWorkHubActionReceipt, type WorkHubActionReceipt } from './workhub-action-result.js';
-import {
-  decodeInteractionRequest,
-  decodeInteractionCanonicalOutcome,
-  isInteractionCanonicalOutcomeValidForRequest,
-  type InteractionFormRequest,
-  type InteractionQuestionRequest,
-  type InteractionCanonicalOutcome,
-} from './interaction.js';
 import { isExecutorId } from './executor-id.js';
 import { isThinkingLevel, type ThinkingLevel } from './model-thinking.js';
 
@@ -782,7 +774,6 @@ export type StoredMessage =
   | AssistantMessage
   | ToolCallMessage
   | ToolResultMessage
-  | FormInteractionMessage
   | PermissionDecisionMessage
   | TokenUsageMessage
   | TurnStateMessage
@@ -927,19 +918,6 @@ export interface ToolResultMessage {
   parentOperationId?: string;
 }
 
-/** Read projection of a canonical answered or closed form or question, never model-authored text. */
-export interface FormInteractionMessage {
-  type: 'form_interaction';
-  id: string;
-  turnId: string;
-  ts: number;
-  request: InteractionFormRequest | InteractionQuestionRequest;
-  outcome: Extract<
-    InteractionCanonicalOutcome,
-    { kind: 'form_answer' | 'question_answer' | 'closure' }
-  >;
-}
-
 export interface PermissionDecisionMessage {
   type: 'permission_decision';
   /** Equals PermissionRequestEvent.requestId for audit correlation. */
@@ -1369,10 +1347,6 @@ const TOOL_RESULT_MESSAGE_SHAPE = defineObjectShape<ToolResultMessage>()(
     'parentOperationId',
   ],
 );
-const FORM_INTERACTION_MESSAGE_SHAPE = defineObjectShape<FormInteractionMessage>()(
-  ['type', 'id', 'turnId', 'ts', 'request', 'outcome'],
-  [],
-);
 const PERMISSION_DECISION_MESSAGE_SHAPE = defineObjectShape<PermissionDecisionMessage>()(
   ['type', 'id', 'turnId', 'ts', 'toolUseId', 'toolName', 'decision'],
   ['rememberForTurn', 'reviewer', 'rationale', 'riskLevel', 'hint'],
@@ -1673,23 +1647,6 @@ function decodeMessage(
       )
         return message as unknown as ToolResultMessage;
       break;
-    case 'form_interaction':
-      if (
-        hasMessageEnvelope(message, true) &&
-        hasExactShape(message, FORM_INTERACTION_MESSAGE_SHAPE)
-      ) {
-        const request = decodeInteractionRequest(message.request);
-        const outcome = decodeInteractionCanonicalOutcome(message.outcome);
-        if (
-          (request.kind === 'form' || request.kind === 'question') &&
-          (outcome.kind === 'form_answer' ||
-            outcome.kind === 'question_answer' ||
-            outcome.kind === 'closure') &&
-          isInteractionCanonicalOutcomeValidForRequest(request, outcome)
-        )
-          return { ...message, request, outcome } as unknown as FormInteractionMessage;
-      }
-      break;
     case 'permission_decision':
       if (
         hasExactShape(message, PERMISSION_DECISION_MESSAGE_SHAPE) &&
diff --git a/packages/core/src/tool-quiet-preview.ts b/packages/core/src/tool-quiet-preview.ts
index 697006f21a..b82b8bab84 100644
--- a/packages/core/src/tool-quiet-preview.ts
+++ b/packages/core/src/tool-quiet-preview.ts
@@ -49,6 +49,7 @@ interface QuietPreviewStrings {
   bytes: (n: number) => string;
   /** Suffix for a question list previewed by its first entry, e.g. `等 2 问` / `+1 more`. */
   moreQuestions: (total: number) => string;
+  unanswered: string;
 }
 
 const STRINGS_BY_LOCALE: Record<UiLocale, QuietPreviewStrings> = {
@@ -61,6 +62,7 @@ const STRINGS_BY_LOCALE: Record<UiLocale, QuietPreviewStrings> = {
     written: '已写入',
     bytes: (n) => `共 ${n} 字节`,
     moreQuestions: (total) => (total > 1 ? ` 等 ${total} 问` : ''),
+    unanswered: '未回答',
   },
   'zh-TW': {
     backgroundTerminal: '後臺終端互動',
@@ -71,6 +73,7 @@ const STRINGS_BY_LOCALE: Record<UiLocale, QuietPreviewStrings> = {
     written: '已寫入',
     bytes: (n) => `共 ${n} 位元組`,
     moreQuestions: (total) => (total > 1 ? ` 等 ${total} 問` : ''),
+    unanswered: '未回答',
   },
   en: {
     backgroundTerminal: 'Background terminal interaction',
@@ -81,6 +84,7 @@ const STRINGS_BY_LOCALE: Record<UiLocale, QuietPreviewStrings> = {
     written: 'written',
     bytes: (n) => `${n} bytes`,
     moreQuestions: (total) => (total > 1 ? ` +${total - 1} more` : ''),
+    unanswered: 'Not answered',
   },
 };
 
@@ -503,6 +507,35 @@ export interface QuietPreview {
   body: string;
 }
 
+/**
+ * An AskUserQuestion result listed against its offered options. Live args
+ * previews carry question text only, so options may be absent.
+ */
+export function formatUserQuestionResult(
+  args: unknown,
+  value: unknown,
+  locale: UiLocale,
+): string | undefined {
+  const answers = asRecord(value)?.answers;
+  if (!Array.isArray(answers) || answers.length === 0) return undefined;
+  const questions = asRecord(args)?.questions;
+  const blocks: string[] = [];
+  for (const [index, entry] of answers.entries()) {
+    const question = stringField(asRecord(entry), 'question');
+    const answer = asRecord(entry)?.answer;
+    if (question === undefined || (answer !== null && typeof answer !== 'string')) return undefined;
+    const options = Array.isArray(questions) ? asRecord(questions[index])?.options : undefined;
+    const labels = Array.isArray(options)
+      ? options.flatMap((option) => stringField(asRecord(option), 'label') ?? [])
+      : [];
+    const lines = [question, ...labels.map((label) => `${label === answer ? '✓' : ' '} ${label}`)];
+    if (answer === null) lines.push(`  ${strings(locale).unanswered}`);
+    else if (!labels.includes(answer)) lines.push(`✓ ${answer}`);
+    blocks.push(lines.join('\n'));
+  }
+  return redactSecrets(blocks.join('\n\n'));
+}
+
 /**
  * Format any tool JSON/result payload for the quiet panel.
  * Always returns a body — never `undefined` for object values so callers
diff --git a/packages/core/src/transcript-search.ts b/packages/core/src/transcript-search.ts
index ad6e79a3c8..f98cd0fe52 100644
--- a/packages/core/src/transcript-search.ts
+++ b/packages/core/src/transcript-search.ts
@@ -58,7 +58,6 @@ export function threadSearchMatchKind(message: StoredMessage): ThreadSearchMatch
       return 'tool_intent';
     case 'tool_result':
       return 'tool_result';
-    case 'form_interaction':
     case 'permission_decision':
     case 'token_usage':
     case 'turn_state':
diff --git a/packages/runtime-host/src/__tests__/session-continuity-coordinator.test.ts b/packages/runtime-host/src/__tests__/session-continuity-coordinator.test.ts
index fecccc1549..51a3e0ab4b 100644
--- a/packages/runtime-host/src/__tests__/session-continuity-coordinator.test.ts
+++ b/packages/runtime-host/src/__tests__/session-continuity-coordinator.test.ts
@@ -2483,48 +2483,3 @@ async function waitFor(predicate: () => boolean): Promise<void> {
     message: 'Timed out waiting for continuity state',
   });
 }
-
-test('settled choice invalidates consumed transcript even without a new RuntimeEvent', async () => {
-  let projection = canonical({ interactions: { pending: [pendingInteraction()] } });
-  const durable: StoredMessage[] = [
-    { type: 'assistant', id: 'old', turnId: 'turn-1', ts: 1, text: 'Done', modelId: 'test' },
-  ];
-  const coordinator = new SessionContinuityCoordinator(
-    HOST_EPOCH,
-    async () => projection,
-    new SessionAdmissionGate(),
-    undefined,
-    { ...transcriptReader(durable), readDurableHighWater: async () => 1 },
-  );
-  const sink = new RecordingSink();
-  const connection = attachTestConnection(coordinator, 'late-choice', sink);
-  const opened = await open(coordinator, 'late-choice', { kind: 'tail', maxBytes: 1 << 20 });
-  connection.activate(opened.subscriptionId);
-  const history: StoredMessage = {
-    type: 'form_interaction',
-    id: 'interaction-1',
-    turnId: 'turn-1',
-    ts: 2,
-    request: pendingInteraction().request,
-    outcome: { kind: 'question_answer', answers: ['Yes'], committedAt: 2 },
-  };
-  durable.push(history);
-  projection = canonical();
-  await coordinator.refreshCanonical(SESSION_ID);
-  await delayImmediate();
-  const closed = sink.frames.find((frame) => frame.kind === 'subscription.closed');
-  assert.equal(
-    closed?.kind === 'subscription.closed' ? closed.reason : undefined,
-    'transcript_changed',
-  );
-  const reopened = await open(coordinator, 'late-choice', { kind: 'tail', maxBytes: 1 << 20 });
-  const client = clientSubscription(
-    reopened,
-    async () => undefined,
-    async () => {
-      throw new Error('unexpected pagination');
-    },
-  );
-  assert.deepEqual(await client.loadTranscript((value) => value), durable);
-  coordinator.close();
-});
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 c36231c02a..b46a0f03eb 100644
--- a/packages/runtime-host/src/__tests__/session-transcript-pager.test.ts
+++ b/packages/runtime-host/src/__tests__/session-transcript-pager.test.ts
@@ -672,24 +672,3 @@ test('shared transcript preserves admitted action identity alongside its physica
     message,
   );
 });
-
-test('shared transcript omits private choice prompts and answers', () => {
-  assert.equal(
-    projectSharedSessionTranscriptMessage(
-      {
-        type: 'form_interaction',
-        id: 'choice',
-        turnId: 'turn',
-        ts: 2,
-        request: {
-          kind: 'question',
-          toolUseId: 'tool',
-          questions: [{ question: 'Private?', options: [{ label: 'Yes' }, { label: 'No' }] }],
-        },
-        outcome: { kind: 'question_answer', answers: ['Yes'], committedAt: 2 },
-      },
-      'session',
-    ),
-    null,
-  );
-});
diff --git a/packages/runtime-host/src/__tests__/session-transcript-reader.test.ts b/packages/runtime-host/src/__tests__/session-transcript-reader.test.ts
index cdbd27d91a..e01ee49126 100644
--- a/packages/runtime-host/src/__tests__/session-transcript-reader.test.ts
+++ b/packages/runtime-host/src/__tests__/session-transcript-reader.test.ts
@@ -777,16 +777,16 @@ test('serves every row of a Turn nested inside another', async () => {
     await read.end('outer', 'outer-end');
 
     const throughSequence = (await read.readDurableHighWater(sessionId))!;
-    assert.equal(throughSequence, 10 * 4096 + 4095);
+    assert.equal(throughSequence, 10 * 8 + 7);
     await assertTranscriptRows(read, sessionId, throughSequence, [
-      [2 * 4096, 'outer-before'],
-      [4 * 4096, 'inner-0'],
-      [5 * 4096, 'inner-1'],
-      [6 * 4096, 'inner-2'],
-      [7 * 4096, 'inner-3'],
-      [8 * 4096, 'inner-end'],
-      [9 * 4096, 'outer-after'],
-      [10 * 4096, 'outer-end'],
+      [2 * 8, 'outer-before'],
+      [4 * 8, 'inner-0'],
+      [5 * 8, 'inner-1'],
+      [6 * 8, 'inner-2'],
+      [7 * 8, 'inner-3'],
+      [8 * 8, 'inner-end'],
+      [9 * 8, 'outer-after'],
+      [10 * 8, 'outer-end'],
     ]);
   });
 });
@@ -816,16 +816,16 @@ test('serves a running Turn that encloses two separated Turns', async () => {
 
     // A watermark inside the outer Turn: it is still running as of this read,
     // so it has no ending to be reached through.
-    const throughSequence = 10 * 4096 + 4095;
-    assert.equal(await read.readDurableHighWater(sessionId), 11 * 4096 + 4095);
+    const throughSequence = 10 * 8 + 7;
+    assert.equal(await read.readDurableHighWater(sessionId), 11 * 8 + 7);
     await assertTranscriptRows(read, sessionId, throughSequence, [
-      [2 * 4096, 'outer-a'],
-      [4 * 4096, 'first-a'],
-      [5 * 4096, 'first-end'],
-      [6 * 4096, 'outer-b'],
-      [8 * 4096, 'second-a'],
-      [9 * 4096, 'second-end'],
-      [10 * 4096, 'outer-c'],
+      [2 * 8, 'outer-a'],
+      [4 * 8, 'first-a'],
+      [5 * 8, 'first-end'],
+      [6 * 8, 'outer-b'],
+      [8 * 8, 'second-a'],
+      [9 * 8, 'second-end'],
+      [10 * 8, 'outer-c'],
     ]);
   });
 });
@@ -926,7 +926,7 @@ test('does not end a page where a handoff resumes the same Turn', async () => {
     const lookup = (turnId: string) =>
       read.readDurableTurnLandmarks(sessionId, { maxLandmarks: 1, turnId });
     assert.deepEqual((await lookup('turn-first')).landmarks, [
-      { turnId: 'turn-first', sequence: 1 * 4096, lastSequence: 6 * 4096 + 4095, label: '' },
+      { turnId: 'turn-first', sequence: 1 * 8, lastSequence: 6 * 8 + 7, label: '' },
     ]);
     assert.deepEqual((await lookup('turn-missing')).landmarks, []);
   });
@@ -1264,162 +1264,3 @@ function assertLargeBashResult(
   assert.equal(result.content.output.stdoutTruncated, true);
   assert.equal(result.content.output.stderrTruncated, true);
 }
-
-test('replays Host form choices and cancellations without model-authored form events', async () => {
-  const base = await mkdtemp(join(tmpdir(), 'maka-form-history-'));
-  const capability = await resolveStorageRoot({ path: join(base, 'root'), kind: 'interactive' });
-  const owner = await tryAcquireInteractiveRootOwner(capability);
-  assert.ok(owner);
-  try {
-    const stores = await openInteractiveExecutionStoresForWrite(owner.lease);
-    const session = await stores.sessionStore.create({
-      cwd: base,
-      llmConnectionSlug: 'fake',
-      model: 'fake',
-      permissionMode: 'ask',
-    });
-    await seedInvocation(stores.runtimeEventStore, {
-      sessionId: session.id,
-      runId: 'run-1',
-      turnId: 'turn-1',
-      openedAt: 1,
-    });
-    await stores.runtimeEventStore.appendRuntimeEvent(
-      session.id,
-      'run-1',
-      runtimeEvent(session.id, {
-        id: 'user-1',
-        ts: 2,
-        role: 'user',
-        author: 'user',
-        content: { kind: 'text', text: 'Continue login' },
-      }),
-    );
-    const request = {
-      kind: 'form' as const,
-      toolUseId: 'tool-1',
-      message: 'Which login work?',
-      requester: { name: 'WorkHub' },
-      fields: [
-        {
-          kind: 'single_select' as const,
-          name: 'target',
-          label: 'Work',
-          required: true,
-          options: [
-            { value: 'ui', label: 'Login UI' },
-            { value: 'api', label: 'Login API' },
-          ],
-        },
-      ],
-    };
-    for (const requestId of [
-      'selected',
-      'cancelled',
-      ...Array.from({ length: 8 }, (_, i) => `closed-${i}`),
-    ]) {
-      await stores.interactionStore.establishRequest({
-        sessionId: session.id,
-        turnId: 'turn-1',
-        runId: 'run-1',
-        requestId,
-        createdAt: 3,
-        request,
-      });
-      await stores.interactionStore.commitOutcome(
-        requestId,
-        requestId === 'selected'
-          ? { kind: 'form_answer', action: 'accept', values: { target: 'api' }, committedAt: 4 }
-          : requestId === 'cancelled'
-            ? { kind: 'form_answer', action: 'cancel', committedAt: 5 }
-            : { kind: 'closure', reason: 'turn_terminal', committedAt: 7 },
-      );
-    }
-    await stores.interactionStore.establishRequest({
-      sessionId: session.id,
-      turnId: 'turn-1',
-      runId: 'run-1',
-      requestId: 'question',
-      createdAt: 5,
-      request: {
-        kind: 'question',
-        toolUseId: 'question-tool',
-        questions: [
-          {
-            question: 'Which work should stop?',
-            options: [{ label: 'Both' }, { label: 'Only UI' }],
-          },
-        ],
-      },
-    });
-    await stores.interactionStore.commitOutcome('question', {
-      kind: 'question_answer',
-      answers: ['Only UI'],
-      committedAt: 6,
-    });
-    await stores.runtimeEventStore.appendRuntimeEvent(
-      session.id,
-      'run-1',
-      runtimeEvent(session.id, {
-        id: 'answer-1',
-        ts: 6,
-        role: 'model',
-        author: 'agent',
-        content: { kind: 'text', text: 'Done.' },
-      }),
-    );
-    await seedInvocation(stores.runtimeEventStore, {
-      sessionId: session.id,
-      runId: 'run-2',
-      turnId: 'turn-1',
-      openedAt: 8,
-    });
-    await stores.runtimeEventStore.appendRuntimeEvent(session.id, 'run-2', {
-      ...runtimeEvent(session.id, {
-        id: 'continued',
-        ts: 9,
-        role: 'model',
-        author: 'agent',
-        content: { kind: 'text', text: 'Continued.' },
-      }),
-      runId: 'run-2',
-      invocationId: 'run-2',
-      turnId: 'turn-1',
-    });
-    const read = createSessionTranscriptReader({
-      stores,
-      canonicalPermissionOutcomes: { readPermissionOutcome: async () => undefined },
-    });
-    const page = await read.readDurableRecords(session.id, {
-      direction: 'newer',
-      position: 0,
-      maxStoredBytes: TRANSCRIPT_TURN_MAX_BYTES,
-      maxMessages: 64,
-    });
-    const forms = page.records
-      .map(({ message }) => message)
-      .filter((message) => message.type === 'form_interaction');
-    assert.equal(forms.length, 11);
-    assert.deepEqual(forms.find((form) => form.id === 'question')?.outcome, {
-      kind: 'question_answer',
-      answers: ['Only UI'],
-      committedAt: 6,
-    });
-    assert.deepEqual(forms[0]?.request, request);
-    assert.deepEqual(
-      forms
-        .filter((form) => form.outcome.kind === 'form_answer')
-        .map((form) => form.outcome.kind === 'form_answer' && form.outcome.action),
-      ['accept', 'cancel'],
-    );
-    assert.deepEqual(forms.find((form) => form.id === 'selected')?.outcome, {
-      kind: 'form_answer',
-      action: 'accept',
-      values: { target: 'api' },
-      committedAt: 4,
-    });
-  } finally {
-    await owner.close();
-    await rm(base, { recursive: true, force: true });
-  }
-});
diff --git a/packages/runtime-host/src/client/session-subscription.ts b/packages/runtime-host/src/client/session-subscription.ts
index 226a227e1d..c20f0d04c9 100644
--- a/packages/runtime-host/src/client/session-subscription.ts
+++ b/packages/runtime-host/src/client/session-subscription.ts
@@ -43,7 +43,6 @@ export type RuntimeHostSubscriptionFailureReason =
   | 'correlation_changed'
   | 'projection_revision_invalid'
   | 'slow_consumer'
-  | 'transcript_changed'
   | 'connection_closed';
 
 export class RuntimeHostSubscriptionError extends Error {
@@ -62,12 +61,6 @@ export class SessionRemovedSubscriptionError extends Error {
 }
 
 export function subscriptionClosedError(reason: SubscriptionClosedFrame['reason']): Error {
-  if (reason === 'transcript_changed') {
-    return new RuntimeHostSubscriptionError(
-      'transcript_changed',
-      'Settled choice history requires a fresh transcript',
-    );
-  }
   if (reason === 'session_removed') {
     return new SessionRemovedSubscriptionError(
       'Runtime Host Session was removed while it was observed',
diff --git a/packages/runtime-host/src/protocol/index.ts b/packages/runtime-host/src/protocol/index.ts
index 76069f6ba2..0893d00db7 100644
--- a/packages/runtime-host/src/protocol/index.ts
+++ b/packages/runtime-host/src/protocol/index.ts
@@ -103,7 +103,8 @@ 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 = 188 as const;
+export const RUNTIME_HOST_COMPATIBILITY_EPOCH = 189 as const;
+// 189: Remove form_interaction transcript messages and the transcript_changed close reason.
 // 188: Session capability replacement can require an atomic idle root check;
 // complete MCP configuration identities fence conflicting providers across ACP clients.
 // 187: transcript_changed closes a subscription for automatic bounded-tail reseeding.
diff --git a/packages/runtime-host/src/protocol/session-continuity.ts b/packages/runtime-host/src/protocol/session-continuity.ts
index 98b5473290..f376c04519 100644
--- a/packages/runtime-host/src/protocol/session-continuity.ts
+++ b/packages/runtime-host/src/protocol/session-continuity.ts
@@ -291,7 +291,7 @@ export interface AgentGraphChangedFrame extends SubscriptionEnvelope {
 
 export interface SubscriptionClosedFrame extends SubscriptionEnvelope {
   kind: 'subscription.closed';
-  reason: 'slow_consumer' | 'session_removed' | 'access_revoked' | 'transcript_changed';
+  reason: 'slow_consumer' | 'session_removed' | 'access_revoked';
 }
 
 export type SubscriptionFrame =
@@ -533,8 +533,7 @@ export function decodeSubscriptionFrame(value: unknown): SubscriptionFrame {
     if (
       record.reason !== 'slow_consumer' &&
       record.reason !== 'session_removed' &&
-      record.reason !== 'access_revoked' &&
-      record.reason !== 'transcript_changed'
+      record.reason !== 'access_revoked'
     ) {
       throw invalidProtocolFrame('Invalid subscription close reason');
     }
diff --git a/packages/runtime-host/src/server/session-continuity-coordinator.ts b/packages/runtime-host/src/server/session-continuity-coordinator.ts
index aba4f76ab4..7f93e18227 100644
--- a/packages/runtime-host/src/server/session-continuity-coordinator.ts
+++ b/packages/runtime-host/src/server/session-continuity-coordinator.ts
@@ -665,7 +665,6 @@ export class SessionContinuityCoordinator implements SessionContinuityService {
 
         const nextRevision = state.revision + 1;
         const snapshot = createSessionContinuitySnapshot(canonical, nextRevision);
-        this.#invalidateSettledChoiceTranscript(state, canonical);
         state.canonical = canonical;
         state.revision = nextRevision;
         delete state.terminalPublicationFence;
@@ -1309,10 +1308,7 @@ export class SessionContinuityCoordinator implements SessionContinuityService {
       );
   }
 
-  #closeSubscriber(
-    subscriber: Subscriber,
-    reason: 'slow_consumer' | 'access_revoked' | 'transcript_changed',
-  ): void {
+  #closeSubscriber(subscriber: Subscriber, reason: 'slow_consumer' | 'access_revoked'): void {
     if (subscriber.phase !== 'open') return;
     subscriber.phase = 'closing';
     const inFlight = subscriber.pumping ? subscriber.queue[0] : undefined;
@@ -1702,7 +1698,6 @@ export class SessionContinuityCoordinator implements SessionContinuityService {
       }
       const nextRevision = state.revision + 1;
       const value = createSessionContinuitySnapshot(canonical, nextRevision);
-      this.#invalidateSettledChoiceTranscript(state, canonical);
       state.canonical = canonical;
       state.revision = nextRevision;
       return { changed, state, value };
@@ -1714,32 +1709,10 @@ export class SessionContinuityCoordinator implements SessionContinuityService {
     };
   }
 
-  #invalidateSettledChoiceTranscript(
-    state: SessionProjectionState,
-    next: CanonicalSessionProjection,
-  ): void {
-    const pending = new Set(next.interactions.pending.map((item) => item.interactionId));
-    if (
-      !state.canonical.interactions.pending.some(
-        (item) =>
-          (item.request.kind === 'form' || item.request.kind === 'question') &&
-          !pending.has(item.interactionId),
-      )
-    )
-      return;
-    // Outcomes can settle after their owner's last event. Their historical rows
-    // then lie behind an already-consumed cursor: advancing the event high water
-    // cannot deliver them. Close explicitly so observers rebuild their bounded tail.
-    for (const subscriber of state.subscribers.values()) {
-      if (subscriber.transcript) this.#closeSubscriber(subscriber, 'transcript_changed');
-    }
-  }
-
   #publishCanonical(state: SessionProjectionState, canonical: CanonicalSessionProjection): void {
     if (isDeepStrictEqual(state.canonical, canonical)) return;
     const nextRevision = state.revision + 1;
     const snapshot = createSessionContinuitySnapshot(canonical, nextRevision);
-    this.#invalidateSettledChoiceTranscript(state, canonical);
     state.canonical = immutableClone(canonical);
     state.revision = nextRevision;
     this.#broadcastProjection(state, snapshot);
diff --git a/packages/runtime-host/src/server/session-transcript-reader.ts b/packages/runtime-host/src/server/session-transcript-reader.ts
index dcff483b38..f625f99b72 100644
--- a/packages/runtime-host/src/server/session-transcript-reader.ts
+++ b/packages/runtime-host/src/server/session-transcript-reader.ts
@@ -47,8 +47,7 @@ import type { SessionTurnLandmark } from '../protocol/index.js';
 
 const PERMISSION_OUTCOME_READ_CONCURRENCY = 8;
 /** One event can emit content, a permission, usage, and terminal/notice rows. */
-// A single event can anchor a batch of Host interaction outcomes.
-const EVENT_SEQUENCE_STRIDE = 4_096;
+const EVENT_SEQUENCE_STRIDE = 8;
 const TRANSCRIPT_TURN_MAX_MESSAGES = 4_096;
 export const TRANSCRIPT_TURN_MAX_BYTES = 16 * 1024 * 1024;
 const TRANSCRIPT_SOURCE_MAX_EVENTS = TRANSCRIPT_TURN_MAX_MESSAGES * 2;
@@ -176,43 +175,6 @@ function createDurableLedgerTranscriptReader(input: {
     if (projected.diagnostics.some(isHardRuntimeEventReadModelDiagnostic)) {
       throw new Error('Durable RuntimeEvent transcript projection is incomplete');
     }
-    const interactions = await input.stores.interactionStore.listTurnInteractions(
-      turn.invocation.sessionId,
-      turn.invocation.turnId,
-    );
-    interactions.sort(
-      (a, b) =>
-        (a.outcome?.outcome.committedAt ?? a.request.createdAt) -
-        (b.outcome?.outcome.committedAt ?? b.request.createdAt),
-    );
-    for (const record of interactions) {
-      const { request, outcome } = record;
-      if (
-        request.runId !== turn.invocation.runId ||
-        (request.request.kind !== 'form' && request.request.kind !== 'question') ||
-        !outcome ||
-        (outcome.outcome.kind !== 'form_answer' &&
-          outcome.outcome.kind !== 'question_answer' &&
-          outcome.outcome.kind !== 'closure')
-      )
-        continue;
-      // Anchor after acceptance, so an incremental reader that already saw the
-      // pending request receives the settled history row as well.
-      const source =
-        turn.projection.anchors.find((event) => event.ts >= outcome.outcome.committedAt) ??
-        turn.projection.anchors.at(-1);
-      if (!source) continue;
-      projected.messages.push({
-        type: 'form_interaction',
-        id: request.requestId,
-        turnId: request.turnId,
-        ts: outcome.outcome.committedAt,
-        request: request.request,
-        outcome: outcome.outcome,
-      });
-      projected.sourceEventIds.push(source.id);
-    }
-    assertTurnPresentationBounded(projected.messages);
     const admission =
       turn.invocation.sessionId === WORKHUB_COORDINATION_SESSION_ID
         ? await input.stores.agentRunStore.readRootTurnAdmission(
@@ -226,26 +188,24 @@ function createDurableLedgerTranscriptReader(input: {
         : undefined;
     const ordinals = turn.ordinals;
     const emitted = new Map<number, number>();
-    return projected.messages
-      .map((message, index) => {
-        const ordinal = ordinals.get(projected.sourceEventIds[index]!);
-        if (ordinal === undefined) {
-          throw new Error('Durable transcript message has no source RuntimeEvent');
-        }
-        const offset = emitted.get(ordinal) ?? 0;
-        if (offset >= EVENT_SEQUENCE_STRIDE) {
-          throw new Error('RuntimeEvent exceeds its transcript sequence stride');
-        }
-        emitted.set(ordinal, offset + 1);
-        return {
-          sequence: ordinal * EVENT_SEQUENCE_STRIDE + offset,
-          message:
-            message.type === 'user' && actionId
-              ? { ...message, coordinationActionId: actionId }
-              : message,
-        };
-      })
-      .sort((a, b) => a.sequence - b.sequence);
+    return projected.messages.map((message, index) => {
+      const ordinal = ordinals.get(projected.sourceEventIds[index]!);
+      if (ordinal === undefined) {
+        throw new Error('Durable transcript message has no source RuntimeEvent');
+      }
+      const offset = emitted.get(ordinal) ?? 0;
+      if (offset >= EVENT_SEQUENCE_STRIDE) {
+        throw new Error('RuntimeEvent exceeds its transcript sequence stride');
+      }
+      emitted.set(ordinal, offset + 1);
+      return {
+        sequence: ordinal * EVENT_SEQUENCE_STRIDE + offset,
+        message:
+          message.type === 'user' && actionId
+            ? { ...message, coordinationActionId: actionId }
+            : message,
+      };
+    });
   };
 
   const readRun = async (
@@ -627,7 +587,6 @@ interface PendingTranscriptRun extends RuntimeTranscriptRun {
 /** Keep only presentation state while the storage snapshot visits complete facts. */
 function createTranscriptProjection(invocations: readonly RuntimeInvocationRecord[]) {
   const canonicalPermissionOutcomes = new Map<string, CanonicalPermissionOutcomeRecord>();
-  const anchors: { id: string; ts: number }[] = [];
   let messageCount = 0;
   let messageBytes = 0;
   let eventCount = 0;
@@ -644,9 +603,7 @@ function createTranscriptProjection(invocations: readonly RuntimeInvocationRecor
     },
   });
   return {
-    anchors,
     push(event: RuntimeEvent) {
-      anchors.push({ id: event.id, ts: event.ts });
       eventCount += 1;
       const content = event.content;
       // The durable model projection is never a transcript input. Large Bash
diff --git a/packages/runtime-host/src/server/shared-session-transcript.ts b/packages/runtime-host/src/server/shared-session-transcript.ts
index e316137cab..243512859f 100644
--- a/packages/runtime-host/src/server/shared-session-transcript.ts
+++ b/packages/runtime-host/src/server/shared-session-transcript.ts
@@ -66,9 +66,6 @@ export function projectSharedSessionTranscriptMessage(
           : { coordinationActionId: message.coordinationActionId }),
       };
     }
-    case 'form_interaction':
-      // Choice requests and answers are private decision evidence.
-      return null;
     case 'assistant':
       return {
         type: message.type,
diff --git a/packages/runtime/src/runtime-event-backfill.ts b/packages/runtime/src/runtime-event-backfill.ts
index 0631797500..be32b644d6 100644
--- a/packages/runtime/src/runtime-event-backfill.ts
+++ b/packages/runtime/src/runtime-event-backfill.ts
@@ -362,7 +362,6 @@ export function backfillRuntimeEventsFromStoredMessages(
       // Both are already accounted for elsewhere: the turn's ending becomes the
       // terminal RuntimeEvent below, and a coordination record is the WorkHub's
       // own durable proof, which no run ledger owns a copy of.
-      case 'form_interaction': // InteractionStore owns this read projection.
       case 'turn_state':
       case 'workhub_coordination':
         break;
diff --git a/packages/storage/src/__tests__/sqlite-core-execution-store.test.ts b/packages/storage/src/__tests__/sqlite-core-execution-store.test.ts
index 558b08f7b4..d537b427d6 100644
--- a/packages/storage/src/__tests__/sqlite-core-execution-store.test.ts
+++ b/packages/storage/src/__tests__/sqlite-core-execution-store.test.ts
@@ -403,7 +403,7 @@ describe('SQLite core execution stores', () => {
     });
   });
 
-  test('drops the obsolete AgentRun identity index on upgrade', async () => {
+  test('drops obsolete execution indexes on upgrade', async () => {
     await withRoot(async (root) => {
       createSqliteAgentRunStore(root).close?.();
       const path = join(root, 'runtime.sqlite');
@@ -411,6 +411,8 @@ describe('SQLite core execution stores', () => {
       legacy.exec(`
         CREATE INDEX IF NOT EXISTS core_agent_runs_identity
           ON core_agent_runs(run_id, session_id);
+        CREATE INDEX IF NOT EXISTS core_interaction_requests_by_turn
+          ON core_interaction_requests(session_id, turn_id, created_at, request_id);
         UPDATE operational_schema_migrations SET version = 5 WHERE scope = 'core_execution';
       `);
       legacy.close();
@@ -421,7 +423,7 @@ describe('SQLite core execution stores', () => {
         assert.deepEqual(
           migrated
             .prepare(
-              "SELECT name FROM sqlite_master WHERE type = 'index' AND name = 'core_agent_runs_identity'",
+              "SELECT name FROM sqlite_master WHERE type = 'index' AND name IN ('core_agent_runs_identity', 'core_interaction_requests_by_turn')",
             )
             .all(),
           [],
diff --git a/packages/storage/src/interaction-store-contract.ts b/packages/storage/src/interaction-store-contract.ts
index f78a242935..18308d3158 100644
--- a/packages/storage/src/interaction-store-contract.ts
+++ b/packages/storage/src/interaction-store-contract.ts
@@ -113,7 +113,6 @@ export type CommitInteractionOutcomeResult =
   | InteractionMutationFailureResult;
 
 export interface InteractionStoreReader {
-  listTurnInteractions(sessionId: string, turnId: string): Promise<InteractionRecord[]>;
   readInteraction(requestId: string): Promise<InteractionRecord | undefined>;
   listSessionPending(sessionId: string): Promise<StoredInteractionRequest[]>;
   listPending(filter?: PendingInteractionFilter): Promise<StoredInteractionRequest[]>;
diff --git a/packages/storage/src/interaction-store.ts b/packages/storage/src/interaction-store.ts
index 6127262424..288dd5b1e5 100644
--- a/packages/storage/src/interaction-store.ts
+++ b/packages/storage/src/interaction-store.ts
@@ -122,8 +122,6 @@ export async function openSqliteInteractiveInteractionStoreForRead(
     access: 'read' as const,
     readInteraction: (requestId: string) => run(() => store.readInteraction(requestId)),
     listSessionPending: (sessionId: string) => run(() => store.listSessionPending(sessionId)),
-    listTurnInteractions: (sessionId: string, turnId: string) =>
-      run(() => store.listTurnInteractions(sessionId, turnId)),
     listPending: (filter?: PendingInteractionFilter) => run(() => store.listPending(filter)),
     readClientCapabilitySessionGrant: (key: ClientCapabilitySessionGrantKey) =>
       run(() => store.readClientCapabilitySessionGrant(key)),
@@ -203,8 +201,6 @@ export async function openSqliteInteractiveInteractionStoreForWrite(
       access: 'write' as const,
       readInteraction: (requestId: string) => run(() => store.readInteraction(requestId)),
       listSessionPending: (sessionId: string) => run(() => store.listSessionPending(sessionId)),
-      listTurnInteractions: (sessionId: string, turnId: string) =>
-        run(() => store.listTurnInteractions(sessionId, turnId)),
       listPending: (filter?: PendingInteractionFilter) => run(() => store.listPending(filter)),
       readClientCapabilitySessionGrant: (key: ClientCapabilitySessionGrantKey) =>
         run(() => store.readClientCapabilitySessionGrant(key)),
@@ -443,18 +439,6 @@ class SqliteInteractionStore implements InteractionStoreWriter {
     return readSqliteInteraction(this.#lease, requestId);
   }
 
-  async listTurnInteractions(sessionId: string, turnId: string): Promise<InteractionRecord[]> {
-    assertId(sessionId);
-    assertId(turnId);
-    const rows = this.#lease.database
-      .prepare(`
-      SELECT request_id FROM core_interaction_requests
-      WHERE session_id = ? AND turn_id = ? ORDER BY created_at, request_id
-    `)
-      .all(sessionId, turnId) as { request_id: string }[];
-    return rows.map(({ request_id }) => readSqliteInteraction(this.#lease, request_id)!);
-  }
-
   async listSessionPending(sessionId: string): Promise<StoredInteractionRequest[]> {
     return this.listPending({ sessionId });
   }
diff --git a/packages/storage/src/sqlite-core-execution-schema.ts b/packages/storage/src/sqlite-core-execution-schema.ts
index c2e69c5dc5..332b0ef6aa 100644
--- a/packages/storage/src/sqlite-core-execution-schema.ts
+++ b/packages/storage/src/sqlite-core-execution-schema.ts
@@ -110,9 +110,6 @@ export function migrateSqliteCoreExecutionDatabase(db: DatabaseSync): void {
     CREATE INDEX IF NOT EXISTS core_interaction_pending
       ON core_interaction_requests(session_id, created_at, request_id);
 
-    CREATE INDEX IF NOT EXISTS core_interaction_requests_by_turn
-      ON core_interaction_requests(session_id, turn_id, created_at, request_id);
-
     CREATE TABLE IF NOT EXISTS core_interaction_outcomes (
       request_id TEXT PRIMARY KEY,
       record_json TEXT NOT NULL,
@@ -193,6 +190,7 @@ export function migrateSqliteCoreExecutionDatabase(db: DatabaseSync): void {
       WHERE json_extract(record_json, '$.execution.kind') = 'safe_boundary_continuation';
 
     DROP INDEX IF EXISTS core_agent_runs_identity;
+    DROP INDEX IF EXISTS core_interaction_requests_by_turn;
 
     DROP TABLE IF EXISTS core_message_receipts;
     DROP TABLE IF EXISTS core_message_host_epochs;
diff --git a/packages/storage/src/test-only/memory-execution-control.ts b/packages/storage/src/test-only/memory-execution-control.ts
index f0726f6b53..4d961e74a3 100644
--- a/packages/storage/src/test-only/memory-execution-control.ts
+++ b/packages/storage/src/test-only/memory-execution-control.ts
@@ -122,18 +122,6 @@ export function createMemoryInteractionStore(
         assertId(requestId);
         return interactions(s).get(requestId);
       }),
-    listTurnInteractions: async (sessionId, turnId) =>
-      a.read((s) => {
-        assertId(sessionId);
-        assertId(turnId);
-        return [...interactions(s).values()]
-          .filter((r) => r.request.sessionId === sessionId && r.request.turnId === turnId)
-          .sort(
-            (a, b) =>
-              a.request.createdAt - b.request.createdAt ||
-              a.request.requestId.localeCompare(b.request.requestId),
-          );
-      }),
     listSessionPending: async (sessionId) => store.listPending({ sessionId }),
     listPending: async (filter = {}) =>
       a.read((s) => {
diff --git a/packages/ui/src/__tests__/form-interaction-history.test.tsx b/packages/ui/src/__tests__/form-interaction-history.test.tsx
deleted file mode 100644
index 136478fc1e..0000000000
--- a/packages/ui/src/__tests__/form-interaction-history.test.tsx
+++ /dev/null
@@ -1,69 +0,0 @@
-/*
- * 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 { renderToStaticMarkup } from 'react-dom/server';
-import { parseHTML } from 'linkedom';
-import type { FormInteractionMessage } from '@maka/core/session';
-import { FormInteractionHistory } from '../form-interaction-history.js';
-import { LocaleProvider } from '../locale-context.js';
-
-const message: FormInteractionMessage = {
-  type: 'form_interaction', id: 'choice', turnId: 'turn', ts: 1,
-  request: { kind: 'form', toolUseId: 'tool', message: 'Which login work should continue?', requester: { name: 'WorkHub' },
-    fields: [{ kind: 'single_select', name: 'target', label: 'Work', required: true,
-      options: [{ value: 'ui', label: 'Login UI', description: 'Page work' }, { value: 'api', label: 'Login API', description: 'Server work' }] }] },
-  outcome: { kind: 'form_answer', action: 'accept', values: { target: 'api' }, committedAt: 2 },
-};
-
-test('choice history uses a collapsed native disclosure retaining the complete question and every option', () => {
-  const { document } = parseHTML(renderToStaticMarkup(<LocaleProvider locale="en"><FormInteractionHistory message={message} /></LocaleProvider>));
-  const details = document.querySelector('details')!;
-  assert.ok(details);
-  assert.equal(details.hasAttribute('open'), false);
-  assert.equal(details.querySelector('summary')?.textContent, 'You selected: Login API');
-  assert.match(details.textContent!, /Which login work should continue/);
-  assert.equal(details.querySelectorAll('li').length, 2);
-  assert.match(details.querySelector('li[data-selected="true"]')!.textContent!, /Login API/);
-  assert.match(details.textContent!, /Page work/);
-  assert.match(details.textContent!, /Server work/);
-});
-
-test('cancelled and closed forms never imply that an option was selected', () => {
-  for (const outcome of [{ kind: 'form_answer', action: 'cancel', committedAt: 2 }, { kind: 'closure', reason: 'turn_terminal', committedAt: 2 }] as const) {
-    const html = renderToStaticMarkup(<LocaleProvider locale="en"><FormInteractionHistory message={{ ...message, outcome }} /></LocaleProvider>);
-    assert.doesNotMatch(html, /data-selected="true"/);
-    assert.match(html, /Login UI/);
-    assert.match(html, /Login API/);
-  }
-});
-
-
-test('model question choices preserve the full question, all options and free-text answers', () => {
-  const request = { kind: 'question', toolUseId: 'tool', questions: [{ question: 'Which work should stop?', options: [{ label: 'Both', description: 'Stop both tasks' }, { label: 'Only UI' }] }] } as const;
-  for (const answer of ['Only UI', 'Neither yet']) {
-    const { document } = parseHTML(renderToStaticMarkup(<LocaleProvider locale="en"><FormInteractionHistory message={{ ...message, request, outcome: { kind: 'question_answer', answers: [answer], committedAt: 2 } }} /></LocaleProvider>));
-    assert.equal(document.querySelector('details')?.hasAttribute('open'), false);
-    assert.match(document.querySelector('details')!.textContent!, /Which work should stop/);
-    assert.equal(document.querySelectorAll('li').length, 2);
-    assert.equal(document.querySelector('summary')?.textContent, `You selected: ${answer}`);
-    assert.equal(document.querySelectorAll('li[data-selected="true"]').length, answer === 'Only UI' ? 1 : 0);
-  }
-});
diff --git a/packages/ui/src/__tests__/tool-activity-presentation.test.ts b/packages/ui/src/__tests__/tool-activity-presentation.test.ts
index 531ccab1bb..0cf38b4245 100644
--- a/packages/ui/src/__tests__/tool-activity-presentation.test.ts
+++ b/packages/ui/src/__tests__/tool-activity-presentation.test.ts
@@ -132,6 +132,25 @@ describe('tool activity presentation', () => {
     );
   });
 
+  it('shows AskUserQuestion answers against the offered options', () => {
+    const settled = renderToStaticMarkup(createElement(ToolCallDetail, {
+      item: {
+        toolUseId: 'question',
+        toolName: 'AskUserQuestion',
+        status: 'completed',
+        args: {
+          questions: [{ question: 'Which client?', options: [{ label: 'claude' }, { label: 'maka' }] }],
+        },
+        result: { kind: 'json', value: { answers: [{ question: 'Which client?', answer: 'maka' }] } },
+      },
+    }))
+      .split(/<[^>]+>/)
+      .filter((text) => text.trim() !== '')
+      .join('\n');
+    assert.ok(settled.includes('Which client?\n  claude\n✓ maka'), settled);
+    assert.doesNotMatch(settled, /answers:/);
+  });
+
   it('describes Computer Use proxy calls by action instead of the generic tool name', () => {
     const item: ToolActivityItem = {
       toolUseId: 'computer-observe',
diff --git a/packages/ui/src/chat-view.tsx b/packages/ui/src/chat-view.tsx
index 3c5fe2d6b6..b41180ff3f 100644
--- a/packages/ui/src/chat-view.tsx
+++ b/packages/ui/src/chat-view.tsx
@@ -34,7 +34,6 @@ import {
   ICON_SIZE,
   AlertTriangle,
 } from './icons.js';
-import { FormInteractionHistory } from './form-interaction-history.js';
 import { EmptyChatHero } from './chat-empty-hero.js';
 import type { ChatModelChoice } from './chat-model-helpers.js';
 import {
@@ -485,21 +484,14 @@ export function ChatView(props: {
     (sessionId: string) => onOpenLinkedSessionRef.current?.(sessionId),
     [],
   );
-  const conversationItems = useMemo(() => [
-    ...(props.conversationItems ?? []),
-    ...props.messages.flatMap((message) => message.type === 'form_interaction' ? [{
-      id: `interaction:${message.id}`, afterTurnId: message.turnId, renderWhenAnchorMissing: false,
-      content: <FormInteractionHistory message={message} />,
-    }] : []),
-  ], [props.conversationItems, props.messages]);
   const conversationItemPlacement = useMemo(() => placeChatConversationItems(
-    conversationItems.map((item) => ({
+    (props.conversationItems ?? []).map((item) => ({
       afterTurnId: item.afterTurnId,
       renderWhenAnchorMissing: item.renderWhenAnchorMissing,
       value: { id: item.id, content: item.content },
     })),
     turnIds,
-  ), [conversationItems, turnIds]);
+  ), [props.conversationItems, turnIds]);
   const chatLayout = useChatLayoutContext();
   if (!chatLayout) {
     throw new Error('ChatView must be rendered inside ChatSurfaceLayout');
@@ -602,7 +594,7 @@ export function ChatView(props: {
   );
 
   if (!props.activeSession) {
-
+    const conversationItems = props.conversationItems ?? [];
     // A side conversation forks lazily: its first send arms the optimistic
     // bubble (and, after the rising-edge delay, the running-status line) BEFORE
     // the fork commits, so there is no session yet. Render that optimistic
diff --git a/packages/ui/src/form-interaction-history.tsx b/packages/ui/src/form-interaction-history.tsx
deleted file mode 100644
index fe8a1b47e2..0000000000
--- a/packages/ui/src/form-interaction-history.tsx
+++ /dev/null
@@ -1,80 +0,0 @@
-/*
- * 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 { FormInteractionMessage } from '@maka/core/session';
-import type { UiCatalog } from '@maka/core/ui-locale';
-import { useUiLocale } from './locale-context.js';
-
-const historyCopy: UiCatalog<{ selected: string; cancelled: string; declined: string; closed: string; chosen: string; empty: string }> = {
-  'zh-CN': { selected: '你选择了', cancelled: '你取消了选择', declined: '你拒绝了请求', closed: '选择已关闭', chosen: '已选', empty: '未填写' },
-  'zh-TW': { selected: '你選擇了', cancelled: '你取消了選擇', declined: '你拒絕了請求', closed: '選擇已關閉', chosen: '已選', empty: '未填寫' },
-  en: { selected: 'You selected', cancelled: 'You cancelled the selection', declined: 'You declined the request', closed: 'Selection closed', chosen: 'Selected', empty: 'Not provided' },
-};
-
-export function FormInteractionHistory({ message }: { message: FormInteractionMessage }) {
-  const copy = historyCopy[useUiLocale()];
-  const { request, outcome } = message;
-  if (request.kind === 'question') {
-    const answers = outcome.kind === 'question_answer' ? outcome.answers : [];
-    const summary = outcome.kind === 'closure' ? copy.closed : `${copy.selected}: ${answers.filter(Boolean).join(' · ') || copy.empty}`;
-    return <details className="maka-form-history" data-interaction-id={message.id}>
-      <summary>{summary}</summary>
-      <div className="maka-form-history-content">
-        {request.questions.map((question, index) => <div key={index} className="maka-form-history-field">
-          <p className="maka-form-history-question">{question.question}</p>
-          <ul>{question.options.map((option) => <li key={option.label} data-selected={answers[index] === option.label}>
-            <span>{option.label}</span>{answers[index] === option.label && <strong> — {copy.chosen}</strong>}
-            {option.description && <p>{option.description}</p>}
-          </li>)}</ul>
-          {answers[index] && !question.options.some((option) => option.label === answers[index]) && <p>{copy.selected}: {answers[index]}</p>}
-        </div>)}
-      </div>
-    </details>;
-  }
-  if (outcome.kind === 'question_answer') return null;
-  const values = outcome.kind === 'form_answer' && outcome.action === 'accept' ? outcome.values : undefined;
-  const labels = request.fields.flatMap((field) => {
-    const value = values?.[field.name];
-    if (value === undefined) return [];
-    if (field.kind === 'single_select' || field.kind === 'multi_select')
-      return field.options.filter((option) => Array.isArray(value) ? value.includes(option.value) : value === option.value).map((option) => option.label);
-    return [String(value)];
-  });
-  const summary = outcome.kind === 'closure' ? copy.closed : outcome.action === 'cancel' ? copy.cancelled : outcome.action === 'decline' ? copy.declined : `${copy.selected}: ${labels.join(' · ')}`;
-  return <details className="maka-form-history" data-interaction-id={message.id}>
-    <summary>{summary}</summary>
-    <div className="maka-form-history-content">
-      <p className="maka-form-history-question">{request.message}</p>
-      {request.fields.map((field) => <div key={field.name} className="maka-form-history-field">
-        <p>{field.label}</p>
-        {field.description && <p>{field.description}</p>}
-        {field.kind === 'single_select' || field.kind === 'multi_select' ? <ul>
-          {field.options.map((option) => {
-            const value = values?.[field.name];
-            const selected = Array.isArray(value) ? value.includes(option.value) : value === option.value;
-            return <li key={option.value} data-selected={selected}>
-              <span>{option.label}</span>{selected && <strong> — {copy.chosen}</strong>}
-              {option.description && <p>{option.description}</p>}
-            </li>;
-          })}
-        </ul> : <p>{values?.[field.name] === undefined ? copy.empty : String(values[field.name])}</p>}
-      </div>)}
-    </div>
-  </details>;
-}
diff --git a/packages/ui/src/styles.css b/packages/ui/src/styles.css
index f688fd663b..47677bcb2c 100644
--- a/packages/ui/src/styles.css
+++ b/packages/ui/src/styles.css
@@ -1137,15 +1137,3 @@
 .maka-choice-panel:focus-visible { outline: var(--focus-ring-width) solid var(--focus-ring); outline-offset: var(--focus-ring-offset); }
 
 .maka-choice-hint { margin: 8px 0; }
-
-.maka-form-history { margin: 8px 0; color: var(--foreground); font-size: 13px; }
-.maka-form-history > summary { cursor: pointer; padding: 8px 10px; border-radius: 6px; overflow-wrap: anywhere; }
-.maka-form-history > summary:hover { background: var(--muted); }
-.maka-form-history > summary:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 2px; }
-.maka-form-history-content { margin: 4px 10px 12px; padding-left: 16px; border-left: 2px solid var(--border); overflow-wrap: anywhere; }
-.maka-form-history-question { white-space: pre-wrap; font-weight: 500; }
-.maka-form-history-field { margin-top: 12px; }
-.maka-form-history-field p { white-space: pre-wrap; margin: 4px 0; }
-.maka-form-history-field ul { padding-left: 20px; margin: 6px 0; }
-.maka-form-history-field li { padding: 4px 0; }
-.maka-form-history-field li[data-selected='true'] { font-weight: 500; }
diff --git a/packages/ui/src/tool-activity.tsx b/packages/ui/src/tool-activity.tsx
index 87b79fbdd3..ce5016b79e 100644
--- a/packages/ui/src/tool-activity.tsx
+++ b/packages/ui/src/tool-activity.tsx
@@ -20,6 +20,7 @@
 import { useEffect, useRef, useState } from 'react';
 import { countDiffLineStats } from '@maka/core/unified-diff';
 import { isInFlightToolStatus } from '@maka/core/tool-result-status';
+import { formatUserQuestionResult } from '@maka/core/tool-quiet-preview';
 import { type ToolResultContent } from '@maka/core/events';
 import { type UiLocale } from '@maka/core/ui-locale';
 import {
@@ -286,6 +287,13 @@ function describeToolCall(
     };
   }
 
+  const userQuestionAnswers = !ownsPanel && item.toolName === 'AskUserQuestion' && displayResult?.kind === 'json'
+    ? formatUserQuestionResult(item.args ?? item.argsPreview, displayResult.value, locale)
+    : undefined;
+  if (userQuestionAnswers) {
+    return { decorations, body: { kind: 'quietText', body: userQuestionAnswers } };
+  }
+
   if (!ownsPanel && displayResult?.kind === 'json') {
     const quiet = formatQuietJsonValue(displayResult.value, locale);
     // Keep structured quiet JSON raw: generic permission-text replacement


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