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 10:39:44 UTC

(maka) branch fix/ui-askuserquestion-choice-history created (now 4e1599b602)

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

Astro-Han pushed a change to branch fix/ui-askuserquestion-choice-history
in repository https://gitbox.apache.org/repos/asf/maka.git


      at 4e1599b602 feat(ui): show AskUserQuestion answers against their options

This branch includes the following new commits:

     new e14cf9bdcd fix(ui): remove the duplicate settled-choice history row
     new 4e1599b602 feat(ui): show AskUserQuestion answers against their options

The 2 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.



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


(maka) 02/02: feat(ui): show AskUserQuestion answers against their options

Posted by as...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

Astro-Han pushed a commit to branch fix/ui-askuserquestion-choice-history
in repository https://gitbox.apache.org/repos/asf/maka.git

commit 4e1599b60283a2aef3b8725f5b777da5ba376af2
Author: AstroHan <le...@outlook.com>
AuthorDate: Fri Sep 25 18:39:37 2026 +0800

    feat(ui): show AskUserQuestion answers against their options
    
    The tool row is the single record of an AskUserQuestion choice, but its
    expanded result rendered the raw result shape: English keys, a repeated
    question, and a literal null for an unanswered question. Render each
    question with its offered options, mark the chosen one, and label an
    unanswered question. Live rows only have the question-text args preview
    until the durable transcript supplies full args, so they show the
    question and the chosen answer.
    
    Generated-by: Claude Code
---
 .../__tests__/tool-activity-presentation.test.ts   | 41 ++++++++++++++++++++++
 packages/ui/src/tool-activity.tsx                  | 29 +++++++++++++++
 packages/ui/src/tool-activity/copy.ts              |  4 +++
 3 files changed, 74 insertions(+)

diff --git a/packages/ui/src/__tests__/tool-activity-presentation.test.ts b/packages/ui/src/__tests__/tool-activity-presentation.test.ts
index 531ccab1bb..10e0e96667 100644
--- a/packages/ui/src/__tests__/tool-activity-presentation.test.ts
+++ b/packages/ui/src/__tests__/tool-activity-presentation.test.ts
@@ -132,6 +132,47 @@ describe('tool activity presentation', () => {
     );
   });
 
+  it('shows AskUserQuestion answers against the offered options', () => {
+    const detailText = (item: ToolActivityItem, locale?: UiLocale) =>
+      renderToStaticMarkup(createElement(ToolCallDetail, { item }), locale)
+        .split(/<[^>]+>/)
+        .filter((text) => text.trim().replace('​', '') !== '')
+        .join('\n');
+    const result = {
+      kind: 'json' as const,
+      value: {
+        answers: [
+          { question: 'Which client?', answer: 'maka' },
+          { question: 'Which scope?', answer: null },
+        ],
+      },
+    };
+    const settled = detailText({
+      toolUseId: 'question',
+      toolName: 'AskUserQuestion',
+      status: 'completed',
+      args: {
+        questions: [
+          { question: 'Which client?', options: [{ label: 'claude' }, { label: 'maka' }] },
+          { question: 'Which scope?', options: [{ label: 'user' }, { label: 'project' }] },
+        ],
+      },
+      result,
+    });
+    assert.ok(settled.includes('Which client?\n  claude\n✓ maka\nWhich scope?\n  user\n  project\n  未回答'), settled);
+    assert.doesNotMatch(settled, /answers:|null/);
+
+    const live = detailText({
+      toolUseId: 'question',
+      toolName: 'AskUserQuestion',
+      status: 'completed',
+      args: undefined,
+      argsPreview: { questions: [{ question: 'Which client?' }, { question: 'Which scope?' }] },
+      result,
+    }, 'en');
+    assert.ok(live.includes('Which client?\n✓ maka\nWhich scope?\n  Not answered'), live);
+  });
+
   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/tool-activity.tsx b/packages/ui/src/tool-activity.tsx
index bb61560067..712160a7a5 100644
--- a/packages/ui/src/tool-activity.tsx
+++ b/packages/ui/src/tool-activity.tsx
@@ -204,6 +204,28 @@ type DetailDecision = {
     | { kind: 'none' };
 };
 
+// Live frames carry only a question-text args preview, so options may be absent.
+function formatUserQuestionAnswers(args: unknown, value: unknown, locale: UiLocale): string | undefined {
+  const answers = (value as { answers?: unknown } | null)?.answers;
+  if (!Array.isArray(answers) || answers.length === 0) return undefined;
+  const questions = (args as { questions?: unknown } | null)?.questions;
+  const unanswered = getToolActivityCopy(locale).result.unanswered;
+  const blocks: string[] = [];
+  for (const [index, entry] of answers.entries()) {
+    const { question, answer } = (entry ?? {}) as { question?: unknown; answer?: unknown };
+    if (typeof question !== 'string' || (answer !== null && typeof answer !== 'string')) return undefined;
+    const options = Array.isArray(questions) ? (questions[index] as { options?: unknown } | undefined)?.options : undefined;
+    const labels = Array.isArray(options)
+      ? options.flatMap((option) => typeof option?.label === 'string' ? [option.label as string] : [])
+      : [];
+    const lines = [question, ...labels.map((label) => `${label === answer ? '✓' : ' '} ${label}`)];
+    if (answer === null) lines.push(`  ${unanswered}`);
+    else if (!labels.includes(answer)) lines.push(`✓ ${answer}`);
+    blocks.push(lines.join('\n'));
+  }
+  return redactSecrets(blocks.join('\n\n'));
+}
+
 function describeToolCall(
   item: ToolActivityItem,
   locale: UiLocale,
@@ -286,6 +308,13 @@ function describeToolCall(
     };
   }
 
+  const userQuestionAnswers = !ownsPanel && item.toolName === 'AskUserQuestion' && displayResult?.kind === 'json'
+    ? formatUserQuestionAnswers(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
diff --git a/packages/ui/src/tool-activity/copy.ts b/packages/ui/src/tool-activity/copy.ts
index 8b042f8a71..4b476ed295 100644
--- a/packages/ui/src/tool-activity/copy.ts
+++ b/packages/ui/src/tool-activity/copy.ts
@@ -165,6 +165,7 @@ export interface ToolActivityCopy {
     workflowCompleted: string;
     workflowFailed: string;
     fileWritten: (bytes: number, path: string) => string;
+    unanswered: string;
     webNoResults: string;
     webResults: (count: number) => string;
     credentialSource: Record<WebCredentialCopyKey, string>;
@@ -284,6 +285,7 @@ const TOOL_ACTIVITY_COPY = {
       workflowCompleted: 'Rive 工作流已完成',
       workflowFailed: 'Rive 工作流执行失败',
       fileWritten: (bytes, path) => `已向 ${path} 写入 ${bytes} 字节`,
+      unanswered: '未回答',
     },
     agent: {
       subagentStatus: { completed: '已完成', failed: '失败', cancelled: '已取消', running: '运行中', waiting_for_user: '等待用户输入' },
@@ -392,6 +394,7 @@ const TOOL_ACTIVITY_COPY = {
       workflowCompleted: 'Rive 工作流已完成',
       workflowFailed: 'Rive 工作流執行失敗',
       fileWritten: (bytes, path) => `已向 ${path} 寫入 ${bytes} 位元組`,
+      unanswered: '未回答',
     },
     agent: {
       subagentStatus: { completed: '已完成', failed: '失敗', cancelled: '已取消', running: '執行中', waiting_for_user: '等待使用者輸入' },
@@ -497,6 +500,7 @@ const TOOL_ACTIVITY_COPY = {
       workflowCompleted: 'Rive workflow completed',
       workflowFailed: 'Rive workflow failed',
       fileWritten: (bytes, path) => `Wrote ${bytes} bytes to ${path}`,
+      unanswered: 'Not answered',
     },
     agent: {
       subagentStatus: { completed: 'Completed', failed: 'Failed', cancelled: 'Cancelled', running: 'Running', waiting_for_user: 'Waiting for user input' },


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


(maka) 01/02: fix(ui): remove the duplicate settled-choice history row

Posted by as...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

Astro-Han pushed a commit to branch fix/ui-askuserquestion-choice-history
in repository https://gitbox.apache.org/repos/asf/maka.git

commit e14cf9bdcd1424e5a744c1e0a3993c336a0c70ff
Author: AstroHan <le...@outlook.com>
AuthorDate: Fri Sep 25 18:39:31 2026 +0800

    fix(ui): remove the duplicate settled-choice history row
    
    #5592 projected every settled form and question interaction into the
    transcript as a separate form_interaction message and rendered it after
    the owning Turn. For AskUserQuestion, the tool row already holds the
    full question, options and answer in its durable args and result, so
    every answered question appeared twice, outside the conversation column
    and below a still-running Turn, in all sessions rather than only WorkHub.
    The WorkHub target selector's choice is likewise already recorded in the
    tasks tool result.
    
    Keeping that second projection live also required closing every
    transcript subscription whenever a question settled (transcript_changed),
    a CLI recovery path for the resulting page race, a 512x wider transcript
    sequence stride, a per-Turn interaction query with a new index, and extra
    branches in search, backfill and shared transcripts. Remove all of it and
    bump the compatibility epoch to 188 because form_interaction messages and
    the transcript_changed close reason leave the wire.
    
    The WorkHub stop/status changes from #5592 are unchanged.
    
    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/runtime-host-session-channel.ts   |  13 +-
 packages/core/src/session.ts                       |  43 -----
 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 -
 packages/storage/src/interaction-store-contract.ts |   1 -
 packages/storage/src/interaction-store.ts          |  16 --
 .../storage/src/sqlite-core-execution-schema.ts    |   3 -
 .../src/test-only/memory-execution-control.ts      |  12 --
 .../__tests__/form-interaction-history.test.tsx    |  69 --------
 packages/ui/src/chat-view.tsx                      |  14 +-
 packages/ui/src/form-interaction-history.tsx       |  80 ---------
 packages/ui/src/styles.css                         |  12 --
 27 files changed, 81 insertions(+), 683 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/runtime-host-session-channel.ts b/packages/cli/src/runtime-host-session-channel.ts
index 8736814dff..14576bfdce 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,
@@ -801,11 +800,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';
     }
@@ -814,7 +808,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')
     );
   }
@@ -835,10 +828,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/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/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 b1b4f8c020..ec84c8a69c 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 = 187 as const;
+export const RUNTIME_HOST_COMPATIBILITY_EPOCH = 188 as const;
+// 188: Remove form_interaction transcript messages and the transcript_changed close reason.
 // 187: transcript_changed closes a subscription for automatic bounded-tail reseeding.
 // 186: Settled form/question history adds form_interaction transcript messages.
 // 185: Add the bounded next-prompt suggestion operation.
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/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..efc3252f13 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,
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/chat-view.tsx b/packages/ui/src/chat-view.tsx
index 7337c379d3..42d10d0b87 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 {
@@ -504,21 +503,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');
@@ -621,7 +613,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 36015f19d4..633f8957f4 100644
--- a/packages/ui/src/styles.css
+++ b/packages/ui/src/styles.css
@@ -1146,15 +1146,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; }


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