fix: quiet Retrying lines, quiz option descriptions nuked, dividers at the markers

Three owner rulings from the gradient-descent session
(chat_muhskq6p_e8c3600b — 7 failed quiz calls, answers leaked in
option descriptions, planning divider floating at the transcript top):

1. RETRYING LINES (general): any tool call that errors back to the
   agent (validation failure, malformed args — the retry loop) renders
   one quiet "Retrying…" SystemLine instead of silence. Question tools
   previously rendered null, so a flailing quiz loop read as a stall.
   Aborts are terminal ("Operation aborted") and stay silent. Verified
   live: exactly 7 lines render at the 7 failure sites of the session.

2. QUIZ OPTION DESCRIPTIONS: NUKED. The model used them to telegraph
   correctness in every quiz of the session ("that moves uphill — it
   climbs f instead of descending it") — answerable by elimination.
   The field is gone from BOTH quiz schemas (probe + teach); ask keeps
   it (ungraded — nothing to leak). The dock additionally suppresses
   descriptions for quiz kind, so pre-fix records stop leaking too.
   Verified live: the session's pending quiz renders bare labels.

   Plus the diagnosed failure-mode fixes: GLM sometimes emits `options`
   as a JSON-encoded string (6 of 7 failures). prepareArguments hooks
   on all three question tools coerce it BEFORE pi's validation (the
   call lands instead of bouncing); the client coerces again in
   normalizeOptions (pi persists raw call args). Schema descriptions
   hardened ("REQUIRED ... never a JSON-encoded string").

3. DIVIDERS AT THE MARKERS: the probe-handoff SystemLine
   ("Preparing your plan…") IS NOW the Planning divider; the accepted
   verdict IS NOW the Teaching divider ("✓ Plan accepted" line gone) —
   one signal per seam, not two. The fold-based divider block is
   DELETED: Wave D grouping merged the whole probe into one giant
   assistant group, so the inclusive-fold comparison floated the
   divider to the transcript top (reproduced against the real record:
   6 view messages, phase flipped at the giant group). The markers
   render in the right spot by record construction. A DECLINED verdict
   is not a seam — it keeps the "↻ Plan revision requested" line.
   Verified live: plan divider after the probe concluded, teach
   divider after the plan presentation, nothing at the top.

Tests: 1004/1004 — string-coercion pins (normalizeOptions + the
prepareArguments hooks on all three tools, identity fast path),
quiz-schema-has-no-description pin (ask keeps it).
This commit is contained in:
amosblomqvist
2026-09-26 19:08:39 +03:00
parent b84ab1cdc0
commit 9853075331
6 changed files with 186 additions and 44 deletions
+41 -33
View File
@@ -34,7 +34,7 @@ import { QuestionDock } from "@/components/question-dock";
import { SidebarToggle } from "@/components/sidebar";
import type { ArtifactPartProps, JobRecord, Phase3, QuestionAnswerData, TurnUsage } from "@/src/core";
import type { LhViewMessage } from "@/src/core";
import { PhaseChip, PhaseDivider, planNodeCount } from "@/components/phase-divider";
import { PhaseChip, PhaseDivider } from "@/components/phase-divider";
import { CoverageRing } from "@/components/extraction-coverage";
import { findPendingToolCall, phaseAt3, phaseTimeline, streamingToolActivity } from "@/src/core";
import { slotsConfigured, type ModelsSettings } from "@/src/core";
@@ -1067,41 +1067,31 @@ export function Chat({
// can still hold empty ids — the index keeps keys unique for them.
<SubagentSteerCard key={m.id || `m-${mi}`} message={m} />
) : m.role === "user" && m.parts.some((p) => p.type === "data-probe-handoff") ? (
// The probe→teach handoff wake (custom-message scheme): quiet
// system line, never a user bubble — the wake is scaffolding,
// the "Planning" divider marks the seam, and the model-facing
// instruction lives in the adapter.
<SystemLine key={m.id || `m-${mi}`} testId="probe-handoff">
Preparing your plan…
</SystemLine>
// The probe→teach handoff wake (custom-message scheme): the
// PLANNING DIVIDER IS the seam (owner ruling 2026-09-26 — it
// replaced the "Preparing your plan…" system line; the marker
// renders in the right spot by record construction, so the old
// fold-based divider — which floated to the transcript top once
// Wave D grouping merged the whole probe into one giant
// assistant group — is gone). Never a user bubble: the wake is
// scaffolding; the model-facing instruction lives in the adapter.
<PhaseDivider key={m.id || `m-${mi}`} phase="plan" />
) : m.role === "user" && m.parts.some((p) => p.type === "data-verdict") ? (
// The learner's plan verdict (custom-message scheme): its own
// message, the quiet marker — the accept is the phase seam, a
// decline asks for revision. Never a user bubble: the model-
// facing wording is the adapter's, and this is the learner's
// ACT, not their words (a typed note, if any, rendered above
// as its own real message).
<SystemLine
key={m.id || `m-${mi}`}
testId={
(m.parts.find((p) => p.type === "data-verdict") as
| { data?: { accepted?: boolean } }
| undefined)?.data?.accepted
? "verdict-accepted"
: "verdict-declined"
}
>
{(m.parts.find((p) => p.type === "data-verdict") as
| { data?: { accepted?: boolean } }
| undefined)?.data?.accepted
? "✓ Plan accepted"
: "↻ Plan revision requested"}
</SystemLine>
// The learner's plan verdict (custom-message scheme): the
// accept IS the phase seam — it renders the TEACHING DIVIDER
// (owner ruling 2026-09-26: replaced the "✓ Plan accepted"
// line; same spot, one signal, not two). A DECLINE is not a
// seam — it stays the quiet revision line. Never a user
// bubble: the model-facing wording is the adapter's, and this
// is the learner's ACT, not their words (a typed note, if
// any, renders above as its own real message).
((m.parts.find((p) => p.type === "data-verdict") as
| { data?: { accepted?: boolean } }
| undefined)?.data?.accepted)
? <PhaseDivider key={m.id || `m-${mi}`} phase="teach" />
: <SystemLine key={m.id || `m-${mi}`} testId="verdict-declined">↻ Plan revision requested</SystemLine>
) : (
<Fragment key={m.id || `m-${mi}`}>
{mi > 0 && phases[mi] !== phases[mi - 1] && (
<PhaseDivider phase={phases[mi]!} nodeCount={planNodeCount(m)} />
)}
<Message from={m.role} className={m.role === "assistant" ? "max-w-full" : undefined}>
<MessageContent className={m.role === "assistant" ? "w-full" : undefined}>
{m.role === "user" ? (
@@ -1113,6 +1103,24 @@ export function Chat({
) : (
m.parts.map((part, pi) => {
const isLastPart = mi === messages.length - 1 && pi === m.parts.length - 1;
// A tool call that errored BACK TO THE AGENT (validation
// failure, malformed args) is invisible work — the agent
// reads the error and retries. Owner ruling 2026-09-26:
// surface the retry loop as one quiet line, any tool,
// instead of silence (question tools used to render null,
// so a flailing quiz loop read as a stall). Aborts are
// terminal — nothing retries — so they stay silent.
if (
part.state === "output-error" &&
part.type.startsWith("tool-") &&
part.errorText !== "Operation aborted"
) {
return (
<SystemLine key={`retry-${pi}`} testId="tool-retry">
Retrying…
</SystemLine>
);
}
switch (part.type) {
case "text":
return <MessageResponse key={pi}>{part.text}</MessageResponse>;