Decode only Retry's HTTP 400 error field for the existing banner. Preserve raw model/key refusal envelopes and other submission-error handling.
Verified on the accepted-main integration: 2424 full-suite tests, typecheck, full ESLint, production build and isolated synthetic production-browser Retry/Submit smoke. No real provider calls.
Keep failed attempts in the saved transcript and omit only trailing failures from Retry's temporary agent history. Use the matching temporary prefix length when persisting genuinely new output.
Preserve the error notice across plan/compaction markers, refuse Retry after later compaction, and avoid synthesizing results for tool calls inside failed generations while retaining Pi's native filtering.
Verified: 173 focused Retry/session/compaction-projection/practice-record tests, typecheck, changed-file lint, and diff --check. No app launch or real provider calls.
Titles become text-sm font-semibold in destructive red — JetBrains Mono
via the UI default, matching the abort-turn/dismiss-question dialogs the
owner named as the ideal look. Node and session names below the title
drop the sized-up text-lg for normal text-sm, still white mono, no
code-block chrome. Settings and Setup keep their serif titles by owner
ruling; abort-turn already renders this way. 1610/1610 both configs;
verified live on the running dev server.
The mount-time jump never fired on real graph-link arrivals: the
transcript scroller's ref is attached by a later sibling, and the queued
rAF was cancelled outright whenever use-stick-to-bottom re-created its
context while the initial scroll settled — every link landed at the
bottom (owner report 2026-10-13).
Rewrite the mount path as a synchronous 64ms retry watcher that keeps
running past its first success (hydration/adoption can replace the
jumped-to node), with a ~20s watchdog for cold-compile SSR streams
(observed 6.8s). jump() now reads the stick-to-bottom context through a
ref so effect identities stay stable, and the unmount cleanup no longer
strips a live flash. Verified live on real data: full load, SPA link
navigation, and in-session hashchange all land the anchor 16px from the
scroller top with the 2s highlight.
Also gives the node card's session/file rows matching horizontal and
vertical padding (px-2 py-1.5).
- switch the math authoring/rendering contract from \(...\)/\[...\] to the
standard $...$ inline / $$...$$ display: prompts (20-question-tools,
40-formatting), singleDollarTextMath on the renderer, legacy reader kept,
and escape-debris recovery extended to dollar spans (mirroring micromark
delimiter-run rules)
- exact answer membership now precedes debris cleanup in
coerceCorrectAnswer (calculus incident: an exact sentence-final-period
label was stripped as model debris, then rejected); option keys are
passed at the boundary/grading seams, the dock no longer pre-coerces
- a complete, phase-correct JSON-escaping example rides the probe/quiz
tool descriptions (serialized from one canonical body; quiz leads with
type+nodeId), plus the one-sentence JSON-escaping rule in 40-formatting
- ask option descriptions render markdown/LaTeX in the pending dock;
settled cards keep the details line and option descriptions
retroactively, read from the persisted call arguments (chat passes
part.input to AnsweredCard)
1566/1566 tests on both navigator configs; typecheck, lint, production
build, standalone packaging, and Electron-runtime smoke all green.
electron-updater in the shell (packaged builds only; dev never updates).
Checks at startup + every 24h, downloads silently, and the renderer shows
a Restart banner once the update is staged (it installs on quit either
way). Manual "Check for Updates..." under the app menu answers via dialog.
Every updater event routes through the shell log and is caught - the
updater can never take the app down (verified live: a feed failure logs
three lines and the app runs on).
LH_UPDATE_FEED overrides the baked GitHub feed with a generic local
server - the pre-release drill: serve latest-mac.yml + zip + blockmap
from localhost and a real installed copy updates itself, no release, no
public repo.
The asar allow-list re-includes electron-updater + its 16-package runtime
tree explicitly (gitignore last-match-wins; asar verified at 2.1 MB).
The banner rides the preload bridge (onUpdateStatus/installUpdate) and
renders nothing in the browser. window.lh typing centralized in
sidebar.tsx's declaration.
THE START BUG (owner report, reproduced live both kinds): new sessions
(?prompt flow — practice and teach alike) sat dead until a second
message, which then persisted WITHOUT the first. Root cause, proven
both directions: the submit POST rode the view's consumeAbort
controller; React StrictMode's simulated remount fires Diet A's
unmount cleanup (model.stop()) ~8ms in and killed the POST mid-flight;
the page's sentInitial guard had already flipped, so it never retried.
Teach was equally broken — only unnoticed, because every teach chat in
use predates the diet and practice starts are always fresh. Fix: the
submit law — POSTs are not abortable by view teardown (they are ~100ms
acks since Diet B and cannot starve the pool); after the ack, a
consumeAbort identity check guards the stream attach. Pinned with the
stop-mid-submit regression test. Live: StrictMode on, practice draft
POST 200/67ms, stream attached, message persisted, problem posed.
PRACTICE PROMPT (00-base): the opening rule — material arrives two
ways, both first-class (picked propositions primary; free-form —
topics, course material, files/PDFs — fully supported, never refused);
as soon as material exists the reply poses, at most one short line
before the problem call, no warm-up interview. Plus the hard law: no
problem is ever conversation — every problem through the tool (no
thinking field/no reveal/no verdict attaches to prose).
GRAPH-CONTEXT PREAMBLE: custom markers flatten to user-role on the
wire, so the old "Contextualization — the learner picked…" read like a
system broadcast in the learner's voice channel. Now the bracket
convention: "[Session context] — picked from the learner's knowledge
map…".
SIDEBAR: busy rows unify to "Working…" for both kinds (the spinner's
default label included); "Practicing…"/"Teacher is working…" retired.
Gates: 1490/1490, typecheck, lint. doctrine's stale doc pointer fixed.
Plan canvas (owner report: arrows clipped at the viewBox edge in dist —
measured 14px/20px cuts in the owner's real plans): blockLayout now
unions node boxes with every edge path's control-point hull (a bezier
never leaves it; the layout calls edgePathD with the renderer's exact
inputs). Dagre's node extent stays the floor, so ordinary plans render
byte-identical; the viewBox origin shifts only when ink bulges past
zero. Verified on both real chats; pins use the owner's real
linear-algebra tree (golden-real-data).
Keyboard-first (owner's law: Enter always submits, arrows always cycle;
text fields the only exception):
- Enter guard split (enter-submits): a focused BUTTON keeps native
Enter only when keyboard-walked (:focus-visible) or inside an open
overlay (menus/dialogs); pointer-clicked transcript buttons submit,
preventDefault suppressing the re-activation underneath.
- Focus hygiene (focus-hygiene): pointer clicks on transient controls
(agent/user thinking toggles, copy, mermaid expand, plan chip) blur
to body — the state clicking empty space produces.
- Session search owns the walk: Up/Down wrap-cycle the filtered rows,
Enter opens the walked row or the first match (quick-open), then
blurs and keeps the query. The Alt-arrow session cycle is RETIRED.
- Registry: model-menu walk/pick entries removed (obvious in-field
navigation, not settings material); question-dock keys stay
displayOnly per owner ruling.
Gates: 1489/1489, typecheck, lint. Verified live end-to-end.