practice: ONE send lane — the register is read from the text

Owner round 2 (2026-10-05), Slice H: the Nudge/Commit two-lane buttons
are gone. One Send; Enter sends, Shift+Enter is a newline. Whether a
submission claims an answer is apparent from the thinking itself — the
agent reads it (hint ladder for traces, moves+verdict for claims).

- The thinking input loses the commit field end-to-end: ChatInput,
  parseChatInput, newThinkingMessage (details and the model-facing label
  both collapse to the single 'Your thinking'), the optimistic marker.
- Legacy records' commit flags are deliberately unread.
- Doctrine: the two labeled channels become one read instruction; 'answer
  commits' section retitled 'Claimed answers' — a claim is not a
  progress signal.
- ThinkingCard: one label, collapsed arrival (the unified row).
- The dock placeholder no longer explains lanes that no longer exist.
This commit is contained in:
amosblomqvist
2026-10-05 03:46:32 +03:00
parent f460a90be9
commit 0bd3236af0
13 changed files with 101 additions and 130 deletions
+4 -2
View File
@@ -186,8 +186,10 @@ export const HANDOFF_TEXT =
/** The model-facing register of a thinking submission (newThinkingMessage
* prefixes the learner's text with this label). Hints and commits ride
* one channel — the label is the ONLY difference the model sees. */
export const THINKING_HINT_LABEL = "Thinking trace (working, not committed)";
export const THINKING_COMMIT_LABEL = "Committed answer";
/** The single submission label (owner round, 2026-10-05): one send
* lane — whether a submission claims an answer is read from the text,
* not declared by a button. */
export const THINKING_LABEL = "Your thinking";
/** The conclude tool's ack — shared by the executed call (agent layer) and
* the dismissal-synthesized pair (input-events), so a concluded problem
+5 -8
View File
@@ -45,10 +45,10 @@ export type ChatInput =
| { type: "answer"; toolCallId: string; answer: QuestionAnswerData }
| { type: "continue" }
/** The practice dock's two buttons (owner, 2026-10-04): the learner's
* thinking on the ACTIVE problem — commit=false is the hint request
* (working, not committed), commit=true the answer commit. One
* thinking on the ACTIVE problem — one lane (owner 2026-10-05):
* whether it claims an answer is read from the text. One
* problem may carry many submissions; each is its own turn. */
| { type: "thinking"; problemCallId: string; commit: boolean; text: string };
| { type: "thinking"; problemCallId: string; text: string };
/** A wake-pipeline steer delivery — same turn composition, server-origin.
* Wave C (2026-09-26): the idle wake carries EVERY queued marker as its
@@ -168,13 +168,10 @@ export function parseChatInput(input: unknown): ChatInput {
if (typeof i.problemCallId !== "string" || i.problemCallId.length === 0) {
throw new ChatInputError("thinking input requires problemCallId");
}
if (typeof i.commit !== "boolean") {
throw new ChatInputError("thinking input requires commit (boolean)");
}
if (typeof i.text !== "string" || i.text.trim().length === 0) {
throw new ChatInputError("thinking input requires non-empty text");
}
return { type: "thinking", problemCallId: i.problemCallId, commit: i.commit, text: i.text };
return { type: "thinking", problemCallId: i.problemCallId, text: i.text };
default:
throw new ChatInputError(`unknown input type: ${String(i.type)}`);
}
@@ -207,7 +204,7 @@ export function inputAppends(record: { messages: LhMessage[] }, input: ChatInput
if (!active || active.toolCallId !== input.problemCallId) {
throw new ChatInputError("thinking input targets no active problem");
}
return [...dismissalConclusion(record), newThinkingMessage(input.problemCallId, input.commit, input.text)];
return [...dismissalConclusion(record), newThinkingMessage(input.problemCallId, input.text)];
}
}
}
+9 -10
View File
@@ -39,8 +39,7 @@ import { QUESTION_TOOLS } from "./pending.ts";
import {
CONCLUDE_ACK_TEXT,
HANDOFF_TEXT,
THINKING_COMMIT_LABEL,
THINKING_HINT_LABEL,
THINKING_LABEL,
} from "./contracts/chat.ts";
import { compactAnswerForModel, type QuestionAnswerData } from "./question-core.ts";
@@ -224,20 +223,20 @@ export function newGraphContextMessage(picks: readonly GraphContextPick[], chain
};
}
/** The learner's thinking submission (the practice dock's two buttons):
* a user-register message the model reads WITH its label (hint vs
* commit — the register is the whole difference), while `details` keeps
* the raw payload for the transcript card. One problem may carry many
/** The learner's thinking submission (ONE send lane, owner round
* 2026-10-05): the register is no longer declared by a button — the
* model READS the text to tell working traces from claimed answers. The
* content label is the single "Your thinking"; `details` keeps the raw
* payload for the transcript card. One problem may carry many
* submissions. */
export function newThinkingMessage(problemCallId: string, commit: boolean, text: string): LhMarkerMessage {
const label = commit ? THINKING_COMMIT_LABEL : THINKING_HINT_LABEL;
export function newThinkingMessage(problemCallId: string, text: string): LhMarkerMessage {
return {
id: newMessageId(),
role: "custom",
customType: "practice-thinking",
content: `${label}:\n${text}`,
content: `${THINKING_LABEL}:\n${text}`,
display: true,
details: { problemCallId, commit, text },
details: { problemCallId, text },
timestamp: Date.now(),
};
}