practice: the between-problems pause — conclude ends the turn, the learner's go-ahead paces the stream (round 16)

Owner ruling (2026-10-06): every concluded problem opens a pause. The
agent may speak its closing words about the learner's last thinking
after the conclude result, then ENDS ITS TURN — a system-prompt law
deliberately, per owner correction, NOT tool-level terminate. The next
problem is never posed until the learner explicitly moves on; the pause
is the session's teaching space (repairs, clarifications, follow-ups —
still surgical, never re-teaching).

The move-on signal is a first-class input: { type: "next" } appends a
practice-next marker (content = single-sourced NEXT_PROBLEM_TEXT — the
model's unambiguous go-ahead: pose per the selection rules or wrap if
the arc is complete). Honest 400s: practice sessions only, nothing
active (mid-problem input rides the dock), riding after any pending
dismissal synthesis exactly like text.

The fast path (owner approved: composer-band chip + Enter-on-empty):
- Composer.enterAction gains "next" — Enter on an EMPTY composer in
  the pause fires the signal; typing anything makes Enter a send again;
  busy (steer/stop) outranks the pause.
- The Next chip hovers at the composer band's top-right (pick-pill
  chrome register) whenever the pause is live and a problem has ever
  been posed; the placeholder becomes the hint ("Ask about this
  problem — or press Enter for the next one…", exact);
- the transcript renders the signal as a compact centered NEXT PROBLEM
  line (mono-caps chrome); session-core appends it optimistically.

CONCLUDE_ACK_TEXT rewritten to carry the law; doctrine §VIII gains the
pause law (owner-sanctioned edit #3); 00-base's conclude/dismissal
bullets rewritten + the between-problems section added.
This commit is contained in:
amosblomqvist
2026-10-06 03:47:05 +03:00
parent a2db80b72d
commit 573d89ae66
12 changed files with 246 additions and 36 deletions
+22 -1
View File
@@ -32,6 +32,7 @@ import {
concludedByDismissalPair,
danglingQuestionOf,
newAnswerResult,
newNextMessage,
newProblemDismissedMarker,
newThinkingMessage,
newUserMessage,
@@ -48,7 +49,12 @@ export type ChatInput =
* 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; text: string };
| { type: "thinking"; problemCallId: string; text: string }
/** The practice move-on signal (owner round 16): the learner's
* explicit go-ahead during the between-problems pause — the Next
* chip or Enter on an empty composer. Practice sessions only, and
* only with nothing active (mid-problem input rides the dock). */
| { type: "next" };
/** A wake-pipeline steer delivery — same turn composition, server-origin.
* Wave C (2026-09-26): the idle wake carries EVERY queued marker as its
@@ -172,6 +178,8 @@ export function parseChatInput(input: unknown): ChatInput {
throw new ChatInputError("thinking input requires non-empty text");
}
return { type: "thinking", problemCallId: i.problemCallId, text: i.text };
case "next":
return { type: "next" };
default:
throw new ChatInputError(`unknown input type: ${String(i.type)}`);
}
@@ -206,6 +214,19 @@ export function inputAppends(record: { messages: LhMessage[] }, input: ChatInput
}
return [...dismissalConclusion(record), newThinkingMessage(input.problemCallId, input.text)];
}
case "next": {
// Honest 400s, the thinking gate's discipline: the signal is a
// between-problems affordance — practice sessions only, nothing
// active (mid-problem input rides the dock), and the client gates
// make violations unreachable in practice anyway.
if ((record as { kind?: string }).kind !== "practice") {
throw new ChatInputError("next input is a practice-session affordance");
}
if (activeProblemOf(record.messages)) {
throw new ChatInputError("next input requires no active problem");
}
return [...dismissalConclusion(record), newNextMessage()];
}
}
}