Commit Graph
718 Commits
Author SHA1 Message Date
amosblomqvist 4eeb05b3c4 practice: the metadata popover — the node-card info-box register, not a drawer (round 17 refinement)
Owner corrections (2026-10-06): no expand/collapse; a quiet METADATA
trigger sits at the card's bottom (no divider — it just sits there);
click opens the info box in the graph-view node card's exact register
(rounded-xl border bg-card p-4 shadow-lg, w-80, floating ABOVE the
trigger so the transcript scroller never clips it); ANY scroll
(capture-phase — every scroller in the tree) or click outside closes
it, plus Esc claimed from the layered chain (usePopoverDismiss, the
popover law). Values match the model solution's font size
(text-[15px], verified — the drawer's text-xs did not); labels keep
the eyebrow register.
2026-10-06 04:26:34 +03:00
amosblomqvist 793638d6ce practice: the agent's data drawer — full transparency on every conclude card (round 17)
Owner ask (2026-10-06): reveal everything the agent filled in when
writing the problem and concluding it, at the very bottom of the
conclude card, collapsible, arriving collapsed.

The fields, both calls:
- problem: mode (recall/discrimination/composition), target
  proposition ids, statement, modelSolution (the last two ARE the
  visible card — the drawer shows the metadata only)
- conclude: outcome, hint rung (R0–R5/null), miss class
  (cue/discrimination/execution/careless/structural/null), retest.

Seams: problemInputForConclude (native-messages) walks the reveal's
walk and folds the WHOLE problem-call input onto the conclude part
(projectView join; same refused-pose law — an unacked pose folds
nothing); ArtifactPartProps grows problemInput. Targets resolve to
proposition NAMES at render (one lazy module-cached /api/graph fetch;
failure degrades to raw ids — observability never breaks the card).
The drawer: divider + chevron 'AGENT'S DATA' toggle (mono-caps
eyebrow register), definition rows with mono values, 'untagged'/
'none' honest empties, absent entirely when the calls filled nothing
(the dismissal line, legacy bare records).
2026-10-06 04:18:26 +03:00
amosblomqvist 0af3514d9b practice: Enter fires next-problem from anywhere, not just composer focus (round 16 refinement 2)
The docks' useEnterSubmits pattern, mounted for the pause in ChatView:
window-level Enter (typing surfaces and buttons excluded, so the
focused composer/button keep their own lane — no double fire) calls
sendNext, which self-guards on nextReady (inert outside the pause;
the dock listeners are unmounted by then — the two never overlap).
Owner's keyboard-first directive: body focus, a scroll, anywhere.
2026-10-06 04:05:38 +03:00
amosblomqvist d1f085550b practice: the next affordance IS the send button — wide green morph in the pause (round 16 refinement)
Owner correction (2026-10-06): the floating chip clipped inside the
composer band. Replaced with the button morph: an EMPTY composer in
the between-problems pause renders a wide green 'Next problem' button
(bg-green-600 — the answered-card correct register, mono-caps label,
arrow glyph); typing anything returns the regular white send; busy
(steer/stop) outranks the morph. Click and Enter share the one
act() lane, so the button also teaches the Enter-on-empty behavior.
The wrapper div + chip are gone from chat.tsx; inputAppends widens
its record param to { messages; kind? } (the practice gate reads it
without a cast).
2026-10-06 03:55:14 +03:00
amosblomqvist 573d89ae66 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.
2026-10-06 03:47:05 +03:00
amosblomqvist a2db80b72d compaction: the usage-anchored trigger + visible session state (round 15)
The relational-databases incident, diagnosed and fixed:

TRIGGER — pi's estimator law, adopted exactly: anchor on the last
assistant message's provider-reported usage (totalTokens || the field
sum) and chars/4-estimate ONLY what was appended after it. The anchor
scan skips aborted/zero-usage assistants and STOPS at the newest
compaction marker (pre-cut usage described context the next request
will not carry — pi's distrust rule). The old estimator
(JSON.stringify of every message /4) tripled the true context
(measured: 84,412 'tokens' estimated vs 38,799 provider-reported on
the incident record) — scaffolding, escaped LaTeX, thinking blocks
and app-side details all counted. estimateMessageTokens now counts
wire text only (text + thinking + toolCall name/args; content text
for user/toolResult/custom). The unanchored fallback runs the plain
heuristic over the CUT-HONORED projection plus the additive prompt
term; the anchored path adds nothing (usage already billed prompt
and tools).

OBSERVABILITY — compaction is a visible session-wide state (owner
spec): the transcript-bottom placeholder register gains COMPACTING
CONTEXT… (BrailleSpinner + Shimmer, the THINKING placeholder's twin,
mutually exclusive by construction — the gate precedes beginTurn),
and the sidebar row shows 'Compacting context · <ago>' with the
spinner (sessionMeta outranks busy; compacting-changed joins
turn-started/turn-settled as refresh triggers). Liveness rides the
API like turn-busy: ChatListItem.compacting + compactionInFlight on
both GET /api/chats and GET /api/chats/[id] (initiallyCompacting
covers the navigate-back-mid-compaction mount — the event already
fired), exported through the server barrel.

Also: the relational-databases record rolled back to pre-compaction
(marker, post-marker turns, tree reset; backup in /tmp) for a clean
retest. The summarizer-usage 'gap' was a misread — steps[] was
populated all along.
2026-10-06 00:38:15 +03:00
amosblomqvist 6dcad55175 compaction: the leading system message is request chrome, never cut (fix the no-tools request)
Owner report (OpenRouter logs, 2026-10-05): the post-compaction request
arrived with NO system prompt and NO tool definitions — everything
before the cut replaced by the compaction message.

Root cause: pi-agent-core's Agent seeds a SystemMessage carrying the
system prompt AND the tool declarations at state.messages[0]
(createInitialSystemMessage — 'messages declare the provider-visible
loadout'). The record never persists it (ui-stream's settleWith filters
the role), but convertToLlm DOES receive it — and the cut projection
sliced from firstKeptMessageId, dropping [0] from the wire: no prompt,
no tools.

Fix (pi's own law — verified against pi-agent-core + pi's coding-agent):
the projection now carries the leading system message through AHEAD of
the compaction head, exactly as pi's prepareRequest re-applies
context.systemPrompt + context.tools around its compaction projection.
The cut slices the RECORD's history; the chrome is never compactable
payload. Loop start skips [0] when chrome is present (no duplicate);
record-path callers are unchanged.

Pinned: chrome-first-through-cut, exactly-once without a cut,
record-path head-first.
2026-10-05 23:52:19 +03:00
amosblomqvist ffc4df1938 compaction: the surface — settings row, two ruled dividers, summary dialog, send gate (round 14 slice 3)
- settings: compaction.thresholdTokens row on the Agent tab (owner pick)
  in a Context group — CommitInput mirroring the graph budget row, hint
  resolved through the SAME core resolver the trigger reads; PUT rejects
  below the 32k floor with the anti-churn rationale; own section
  (compaction) in SETTINGS_SECTIONS + the settings-signal union
- view fold: one marker → two dividers — the minimal MOMENT divider at
  the marker (retrying-message register, no rule lines) and the longer
  CUT divider emitted immediately before the first kept message
  (phase-divider geometry, chip carries '· Nk tok'); both carry the full
  summary payload; full history stays visible to the user
- components/compaction-divider.tsx: the dividers + summary dialog
  (pure exported body for SSR pins, the delete-dialog pattern; Esc +
  Close dismiss, backdrop does not)
- chat.tsx: compaction-changed SSE flips the running state — composer
  canSend, onSend/doSend/doSteerOrSend, and both docks' submit + hint
  ('compacting session…') gate on it (the server's registry mutex is the
  structural gate; this is the affordance); dialog mounts beside the
  abort dialog
- pins: settings-body (4), view fold (2), divider SSR (4)
2026-10-05 22:51:08 +03:00
amosblomqvist dd159cc8fb compaction: the service — settle kick, turn-start gate, registry mutex (round 14 slice 2)
- src/server/compaction.ts: ensureCompacted (the turn-start gate — awaits
  in-flight compaction, compacts synchronously when still over threshold),
  kickCompaction (the settle kick — summary starts inside the learner's
  reading/answering pause; question/problem poses end runs so the pause is
  always a between-runs window), registry mutex on globalThis, anti-churn
  guard (empty-span skip), summary via the zero-tool one-shot sub-agent
  seam on the session's OWN engine (owner ruling), usage to the turns log
  keyed by the marker, record-changed + compaction-changed events
- chat-handler: the gate rides with the BYOK/key gates (before appends,
  beginTurn; record re-read after); the kick at the onFinish tail
  (re-resolves engine fresh; silent no-op when engineless)
- events: compaction-changed {chatId, running} — running=true is the
  client send-gate signal
- settings: compaction.thresholdTokens (factory 150k, floor 32k,
  clamp-never-reject) resolved concrete in EffectiveSettings
- effective threshold = min(setting, contextWindow − 16k) over the live
  catalog index
- overflow recovery needs no special path: the next turn's gate compacts
- 8 service pins (mutex, events order, chained spans, failure re-arm)
2026-10-05 22:17:33 +03:00
amosblomqvist 7e9597d969 compaction: the pure core — cut walk, marker, and the model-facing projection (round 14 slice 1)
- core/compaction.ts: compactionCutIndex (backwards ~20k-token walk, NEVER
  separates a tool result from its call — the cut extends to the call's
  assistant message; a pending pose always survives), estimateTokens
  (chars/4, pi's method), newCompactionMarker (header + summary; details
  carry firstKeptMessageId/tokensBefore/model/at), serializeForSummary
  (labeled text, tool results truncated ~2k), planTreeToText (verbatim
  tree with transcript-derived statuses), buildCompactionPrompt (the four
  ruled sections: learner understanding + evolution, teaching arc, plan
  tree verbatim, current moment)
- toLlmMessages honors the cut: the ACTIVE (newest) marker renders ONCE
  as the head; everything before firstKeptMessageId drops; corrupt cut
  degrades honestly (everything renders, never throw)
- constants: KEEP_RECENT 20k, RESERVE 16k, DEFAULT_THRESHOLD 150k
- 16 test pins
2026-10-05 22:06:14 +03:00
amosblomqvist 0c9f82e027 prompts: Voice + Accuracy become shared fragments — one law across every agent (round 13 part A)
- shared/10-voice.md (agent-neutral) + shared/15-accuracy.md (research-tool
  truth contract) compose for teacher, prober, AND practice
- PRACTICE_SHARED_FRAGMENTS grows beyond 40-formatting.md
- practice 00-base.md drops its stale near-copy ("You have no research tool
  here" was false since round 10); the solve-and-shortcut-hunt line folds
  into the problem paragraph
- teaching system-prompt.md slims (Voice/Accuracy sections removed)
- pins updated: practice-agents composition, prompt-overrides segment mirror
2026-10-05 21:44:34 +03:00
amosblomqvist 4f3bbf61d0 practice: untagged problems — target optional, topic sessions work (owner round 12)
A session whose material is not in the map is a SUPPORTED mode now:
target is optional (absent = untagged problem, poses freely), an
explicit [] heals away at the seam (the record's canonical form is
tagged-or-absent), and when targets ARE present the round-9 law is
unchanged — whole-graph validation, names normalized to ids. The
rejection message teaches the exit ('omit target entirely'), so a
model inventing targets for an off-graph topic converges to omitting
on the retry; omitting always works, no brick is possible. Prompt
carries the clause: material in the map → ids; not → omit, problems
stand on their own.
2026-10-05 20:23:48 +03:00
amosblomqvist 1f7a3516fe ui: drop the filter's bottom margin — the list's top padding is the seam
The filter's mb-2 stacked with the session list's built-in 8px top
padding, doubling that seam to 16px while the others read 8. The
column now runs 12 / 8 / 8 / 8 — the list's own padding carries the
last seam.
2026-10-05 07:13:53 +03:00
amosblomqvist fde858fda3 ui: the drafts go chrome-less; serif titles name sessions, always (owner round 11)
Both /chat/new drafts lose their TopBar — home's register now: drag
mask first in the DOM, floating sidebar-reopen toggle as the only
pointer-events-auto spot, the empty state carrying the orientation.
The chain picker's own bar then sits at the very top of the column,
exactly like the graph view — no stacked bars, no special-casing.
The chain table drops its 'Chains' title with it: the serif title
register is RESERVED — it always names a session; the table names
nothing, so its bar carries counts + search + close only.
2026-10-05 07:09:16 +03:00
amosblomqvist b8c1b9b096 ui: sidebar bands on one 8px rhythm (owner round 11)
The stack read 12/6/4/4/14 — the search→filter seam was the tightest
gap in the column and the filter→list seam 3.5× larger (its mb-1.5
stacked with the list's 8px top padding). One rhythm now: mb-2 on the
create block, the Sessions+search row, and the filter; the list's p-2
top padding IS the rhythm. The 6px inside the create block stays —
intra-group spacing, a different layer.
2026-10-05 07:05:45 +03:00
amosblomqvist d0bb3da7b8 fix: the draft picker fills the content column, not the window
The overlay is absolute inset-0 — it resolves against its nearest
positioned ancestor. Mounted at the NewSession root it spanned the
whole window, over the sidebar and the traffic lights. It now hosts
inside the draft content column's relative container (the overlay
slot on DraftShell) — the same confinement the graph view gives it:
right of the sidebar, under the top bar, inside the session's section.
2026-10-05 06:56:59 +03:00
amosblomqvist 4f0e701d68 fix: the draft pick pill re-enables pointer events (the none-mask law)
The sticky composer wrapper is a pointer-events-none mask; the pill
row sat outside the composer box's re-enable, so it rendered dead —
no hover, no click. One class, the 953b22b pattern the CTA row and
composer box already follow.
2026-10-05 06:53:42 +03:00
amosblomqvist 93fdf71d4d draft: the pick bar comes to the new-practice screen (round 10 slice 2)
The SAME pill the home screen carries — above the composer, dashed
'Pick propositions from the map' when empty, count + Clear when picked
— now lives on the /chat/new?mode=practice draft. The draft has no
graph behind it, so the pill opens the standalone ChainTableOverlay
(select mode, controlled selection, Done bar) over the /api/graph
snapshot, fetched lazily on first open. The first send carries the
picks as graphContext: the created session is born WITH its ids-first
context marker (target validation's truth set) instead of pickless —
the same contract as the home screen's practice send. Teach drafts
render no pill; practice with no picks stays lean on the wire.
2026-10-05 06:51:23 +03:00
amosblomqvist a1596556df practice: the rarely-needed set — files, research, pdf, map readers (round 10 slice 1)
The practice registry widens beyond problem+conclude: read/ls/find/grep
(the learner-filesystem set, always), research + pdf jobs (research
honestly absent when no engine is live; demo/visual stay teach-only),
and the prober's map readers behind the SAME settings gate as the
prober's (graphToolsEnabled at the composition root). Base prompt
carries the conditional doctrine: reach for them only when the
situation genuinely requires — no propositions → look at the map;
learner references a file → read it; load-bearing uncertainty →
research it. Not toggle-filtered: problem/conclude are the session,
these are exceptions.
2026-10-05 06:49:15 +03:00
amosblomqvist 83dad65481 practice: target-validation pins + the id convention in the base prompt (round 9 slice 3)
Four pins on the mastery-trace contract: a name target normalizes to
the canonical id (case-insensitive, whole-graph set), an id passes
through untouched, an unknown target rejects one-line AND does not arm
the gate (honest activation — the next valid pose goes through), and
the legacy node-source-less wiring stays unvalidated. The base prompt
now states the convention: target takes the proposition ids from the
session context — ids are how problems are traced.
2026-10-05 06:31:22 +03:00
amosblomqvist 0dfb1de246 practice: pose-time target validation — names normalize to ids, unknowns rejected (round 9 slice 2)
problemTool takes the whole-graph node source (chat-handler passes
graphStore.readNodes, read fresh per pose): prepareArguments silently
normalizes name targets to canonical ids so the RECORD carries ids
(the mastery trace groups on them); execute rejects unresolvable
targets with a one-line invalidInputResult — no re-listing, the [id]
proposition list already sits in the agent's context. Validation set
is the whole graph, not the picks: pickless sessions and future
graph-tools poses stay valid. Absent node source (tests) = no
validation.
2026-10-05 06:29:02 +03:00
amosblomqvist d47b3564ce practice: resolveTargetId + ids-first context marker (round 9 slice 1)
The mastery-trace contract: problem targets must be traceable to
propositions. resolveTargetId (pure, chains.ts) resolves a target
string to a canonical node id — id exact first, then name
case-insensitive — against the WHOLE graph (pickless sessions and
future graph-tools poses stay valid). The graph-context marker now
introduces each pick as [id] name: statement (ids were never shown to
the model before — names only); chains stay names: comprehension
context, not tags.
2026-10-05 06:26:21 +03:00
amosblomqvist 1cac08117a ui: the kind filter heads the list — moved below the search (owner round 8b)
It scopes what the list shows, so it sits directly above it as a
caption row; search stays the top muscle-memory target. Column now
reads: LH band → New Teaching/Practice (stacked) → Sessions+search →
Teaching · Practice · All → list.
2026-10-05 06:14:54 +03:00
amosblomqvist 450e0f78f0 ui: round 8 — fuzzy session search, stacked doors, All right, teach divider at the step seam
- SESSION SEARCH goes fuzzy on the SAME fuzzyScore the model pickers
  use (de0c286's util — one scoring across every search surface):
  ranked by score instead of raw substring; empty query keeps order.
- SIDEBAR: New Teaching / New Practice STACK (full-width rows); the
  kind filter reads Teaching · Practice · All — the real modes first,
  All as the escape hatch, mirroring the stack above. Home stays in the
  LH band (owner-confirmed).
- TEACH DIVIDER, the stacked-dividers bug: the plan presentation and
  the first tagged quiz call usually share ONE assistant message, so
  the message-level seam drew Planning+Teaching back-to-back. New pure
  teachStepIndex(parts) finds the STEP boundary governing the flip
  call; the divider renders there (replacing that boundary's plain
  hairline — a labeled rule supersedes it), prepending before the whole
  message only when the flip is in its first step. The fold (chip,
  sidebar state) is untouched.
- JOBS SUITE, flake round 3: the sweep scans the SHARED jobs dir, so
  parallel test FILES' running records respawn into this file's
  counters — the assertions go jurisdictional (own-job ids collected;
  foreign respawns are not this test's business). 1331/1331.
2026-10-05 06:08:51 +03:00
amosblomqvist 6b58d87d9e electron+tests: zoom plumbing (construction-only lights), hermeticity fence for the jobs suite
- ZOOM: the ⌘±/⌘0 ladder is owned, not stock roles — applyZoom sets the
  factor and broadcasts 'lh:zoom'; the renderer publishes --lh-zoom; the
  gutter lengths (.lh-lights-gutter / .lh-closed-toggle) divide by it so
  their RENDERED size is a physical constant at every zoom — the graph
  button stays glued next to the lights, nothing slides under them.
  before-input-event catches ⌘=/layout variants the menu's Plus may not
  claim; did-finish-load re-pushes on reload; getZoom covers first paint.
  The lights themselves stay at construction position — Electron 44 has
  NO setTrafficLightPosition (options-only; the first run crashed on
  it). HIG-acceptable: native lights hug the corner; the web chrome
  compensates around them.
- JOBS SUITE FLAKE, actually fixed this time: the seams are
  module-global and the sweep scans every record — a timed-out async
  test left a running orphan + late wake that poisoned its NEIGHBORS
  (respawned 3 !== 0). New afterEach fence: settle every tracked
  running job, unregister aborts, seams reset to inert no-ops — every
  test starts from identical state regardless of upstream flake.
  Verified: two consecutive full-suite runs 1326/1326.
2026-10-05 05:42:27 +03:00
amosblomqvist f4775ebc93 ui: round 6 — Enter submits from anywhere; model dashes ACTUALLY kept; dumbbell right
- ENTER SUBMITS REGARDLESS OF FOCUS (owner): dictation means the dock
  fields are rarely focused. New useEnterSubmits hook — a window-level
  listener inside both docks (mounted only while a problem/question is
  active). Plain Enter anywhere outside a typing surface submits the
  practice thinking / the teaching answer; typing surfaces keep their
  own handlers, buttons keep their click-activation (the collapsed
  Problem bar still expands on Enter), modified Enters untouched.
  Teaching sessions get the same law as practice — one standard.
- MODEL DASHES: the round-5 join("-") edit never landed (the batch
  script aborted on an earlier assert before reaching it) — the source
  still joined with spaces while the commit message claimed otherwise.
  Now actually joined with dashes: 'glm-5.3-flash' everywhere (picker,
  setup dialog, stats bar); pins updated to match.
- SIDEBAR: the practice dumbbell moves to the far right of the session
  row — titles sit flush with teaching rows, cleaner list.
2026-10-05 05:18:26 +03:00
amosblomqvist a2cddb4dca tests: waitFor budget 8s → 30s — the jobs-suite flake under full-suite load
Under npm test's parallel load the event loop starves and the 8s
wall-clock budget expired before healthy conditions were ever observed;
a timed-out delivery then leaked a late wake into the NEXT test (the
respawn hook is module-global), poisoning even synchronous neighbors —
the recurring double-run-guard / recovery-sweep failures. waitFor
returns the instant the condition holds, so the generous budget costs
nothing when healthy. Full suite 1326/1326.
2026-10-05 04:58:00 +03:00
amosblomqvist 3e402d3c35 ui: round 5 — AGENT'S THINKING buys the 3ch rail; the standard chord; dashed model names
- THE 3CH RAIL: 'AGENT’S THINKING' (16ch, flush) − 'YOUR THINKING' (13ch)
  = 3ch of mono — the rail widens to 3ch (border + padding), keeping the
  chevrons aligned while giving the quote room to breathe (the 1ch rail
  read cramped). YOU row follows.
- THE STANDARD CHORD everywhere: Enter submits, Shift+Enter is a
  newline, ⌘/Ctrl+Enter works too — the teaching thinking field drops
  the ⌘-only Gmail convention; the hint reads '↵ submits · ⇧↵ newline'
  in both docks, shown in text mode too (it Enter-submits as well).
- The transcript-end optimistic placeholder joins the register: small
  mono caps THINKING… (was text-sm sentence case).
- Model names keep their dashes: prettyModel renders 'glm-5.3-flash'
  exactly as written (no auto-caps, no dash→space join).
2026-10-05 04:55:59 +03:00
amosblomqvist d533585bd1 ui: AGENT THINKING joins the small mono-caps register (the aborted edit landed)
The round-4 batch threw on an earlier replace and never reached
reasoning.tsx — the trigger row kept text-sm. Now text-xs font-medium
uppercase tracking-wide, matching YOUR THINKING and the card eyebrows;
chevrons align through the 1ch rail. Plus one missed prettyModel pin
(gpt 5.2, no auto-caps).
2026-10-05 04:43:08 +03:00
amosblomqvist 9b75b4f785 ui: consistency sweep — the 1ch rail geometry, Submit thinking, no auto-caps
Owner round 4 (2026-10-05):

- THE RAIL IS THE MISSING CHARACTER: rail + padding = exactly 1ch of
  the mono heading font. 'YOUR THINKING' (13ch) + rail == 'AGENT
  THINKING' (14ch) flush — the chevrons line up vertically, no fixed
  columns. The YOU heading joins the rail (label inside, like the
  thinking rows) — one quote geometry everywhere.
- HEADING REGISTER: YOUR THINKING / AGENT THINKING / YOU all use the
  card eyebrows' small mono caps (text-xs = the 11px rung, font-medium
  uppercase tracking-wide) — the app chrome is JetBrains Mono.
- SUBMIT THINKING: the practice button is now literally the question
  dock's Button (h-9, px-3.5) in one p-1 row with the hint left
  ('↵ submits · ⇧↵ newline') — the keyboard hints are back. Corner
  radii concentric in both docks: shell 16 − 6 = field 10 − 4 = button 6
  (rounded-[10px] fields, rounded-[6px] buttons — the old rounded-xl/12
  was the 'too large' culprit).
- MODEL NAMES: prettyModel no longer auto-capitalizes — names render
  exactly as their providers write them ('glm 5.3 flash').
- Quiz explanations pulled closer (mt-3 → mt-2).
2026-10-05 04:39:47 +03:00
amosblomqvist 05f083748b ui: conclude card — quiz-chip outcomes, verdict-first; YOUR THINKING / AGENT THINKING
Owner round 3 (2026-10-05):

- CONCLUDE CARD: the stacked same-weight headings are gone. Plain
  'Concluded' eyebrow + the outcome in the quiz cards' EXACT chip
  register. Colors per outcome: solved green (the 'correct' twin),
  solvedWithHints amber (succeeded with cost), surrendered red (the
  problem won), movedOn muted (the agent's call, not a verdict against
  the learner). Verdict as plain 50% prose under the heading — the
  'Verdict:' label is gone — then a divider, then the labeled model
  solution.
- ALL-CAPS: 'YOUR THINKING' and 'AGENT THINKING' — display strings only
  (the model-facing marker label stays sentence-case). The two rows are
  equal-length over the shared rail, so the chevrons align vertically.
  Dock field labels match.
2026-10-05 04:14:21 +03:00
amosblomqvist b770834f29 ui: the rail GROUPS — the thinking label sits inside the rail
Owner correction (2026-10-05): the collapsed trigger row carried no rail,
so ownership was only visible once expanded. The rail now wraps the whole
block — label (trigger) and, on expand, the italic 50% text — so the
quote reads as one object and WHICH thinking belongs to whom is clear
from the rail alone, collapsed or expanded. Same shape as the user text
('You') row.
2026-10-05 03:57:35 +03:00
amosblomqvist 0bd3236af0 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.
2026-10-05 03:46:32 +03:00
amosblomqvist f460a90be9 ui: labeled conclusion sections — 'Model solution' hero, 'Verdict' below
Owner round 2 (2026-10-05), Slice G: 'Solved' alone over unlabeled prose
didn't read as the model solution. The hero now carries its own 'Model
solution' section label (the quiz cards' register), and the verdict
section mirrors it — label muted, text at the same 50% register as quiz
explanations.
2026-10-05 03:42:12 +03:00
amosblomqvist e2284e723a ui: the document look — one language for every thinking, no bubbles
Owner round 2 (2026-10-05), Slice F:

- USER TEXT: the right-aligned bubble is gone. User messages are
  full-width document rows — muted 'You' label, muted quote rail,
  full-opacity text. Learner words are content, not a chat-app tell.
- USER THINKING (everywhere): arrives COLLAPSED as a trigger row in the
  exact register of the agent's reasoning rows (muted label + chevron +
  hover affordance); expanding opens the quote block — rail, 50%
  opacity, italics. The inner-voice signature. The ThinkingBlock loses
  its shell/border: pure document material.
- QUIZ CARDS: the answer's thinking is the SAME collapsible row, same
  placement, just inside the container; the explanation drops to 50%
  opacity (upright — italics stays the thinking signature).
- AGENT'S THINKING: the duration bookkeeping is gone (it was live-turn
  state that died to 'a few seconds' on remount anyway) — the settled
  row reads static 'Agent's thinking', pairing with 'Your thinking'.
  Spinner + shimmer stay the working signal while streaming.
- Glance law: user thinking = rail + 50% italic; agent thinking = no
  rail; user text = upright, full opacity, 'You' label.
2026-10-05 03:41:39 +03:00
amosblomqvist a5a554582e ui: intent-gated fold, measured height caps, the Problem eyebrow, fold fade
Owner round (2026-10-05), Slice D — the dock mechanics:

- INTENT-GATED FOLD: a ResizeObserver on the scroller's content stamps
  the existing 400ms mute whenever the transcript GROWS — growth frames
  (a live turn streaming under stick-to-bottom's chase) can never fold
  the dock; and a fold now needs the distance AND stick-to-bottom's
  isAtBottom agreeing the learner left. Fixes the generation churn
  (dock disappearing and reappearing while the agent works). Shrinks
  stamp nothing (the fold's own morph must not self-mute re-expand).
- MEASURED HEIGHT CAPS: the question (55dvh) and problem (40vh) areas
  now cap at the available column (scroller clientHeight − 2rem,
  re-measured on resize) via --dock-max-h — questions stretch to the
  top bar with sensible padding and scroll internally only when
  genuinely huge.
- The practice dock's statement area carries the 'Problem' eyebrow —
  the quiz cards' label row, mirrored (dock and transcript card read
  identically).
- The fold's CONTENT fade: a 160ms WAAPI fade/rise on the bar↔form
  toggle — no remount, so the question form's state survives; the
  shell height morph was already animated.
2026-10-05 02:31:55 +03:00
amosblomqvist 5d253a933c ui: ProblemCard/ConcludeCard redesigned — the solution is the hero
Owner round (2026-10-05), Slice C:

- ProblemCard: ALWAYS OPEN — the answered card's container + eyebrow
  discipline ('Problem' label, lh-prose statement), no chevron, no
  collapsed bar. The dock mirrors it in Slice D.
- ConcludeCard: the reveal is the modelSolution committed at POSE time,
  shown verbatim as the hero — never re-authored (the slim schema's
  law). The verdict rides below in the quiz-explanation register
  (small, muted 'Verdict:' row). Nothing collapsible. Legacy inputs
  with their own solutionReveal fall back to it.
- The reveal join: modelSolutionForConclude (native-messages.ts) — the
  newest cleanly-posed problem before the conclude call; refused poses
  reveal nothing. projectView enriches tool-conclude parts with
  part.reveal at projection; ArtifactPartProps carries the field
  (contracts/chat.ts).

Pinned: always-open problem card; hero-first conclude layout with the
verdict after it; legacy fallback; the join (clean pose, refused pose,
multi-problem records) and the view enrichment.
2026-10-05 02:29:16 +03:00
amosblomqvist d19df86b13 ui: the unified ThinkingBlock — one visual language for the learner's thinking
Owner round (2026-10-05): the same act (a human thinking in text) had two
visual languages — the quiz cards' quote block vs the practice container
with a collapsing chevron and raw pre-wrap text that read oversized.

- components/thinking-block.tsx: the quiz quote-block recipe verbatim
  (rail + uppercase label + lh-prose at foreground/90) carried by a
  faint user-side shell — ml-auto, max-w-[95%] mirroring the user
  bubble's geometry, px-4/py-3 rhythm. Never collapsible. Rails reuse
  the existing vocabulary: primary = committed register, muted =
  working register.
- ThinkingCard becomes the practice adapter: 'Your solution' (primary)
  / 'Your thinking' (muted) — one component, two registers.
- The reading channel LIFTS out of the answered card: chronologically
  prior to the question, it renders above it as its own user-side
  block under the plain 'Your thinking' label; the in-card inset and
  the 'while reading' phrasing are gone.
- lh-card-enter (globals.css): the one-shot 150ms fade+rise mount,
  the app's motion register; honors reduced-motion.
2026-10-05 02:25:13 +03:00
amosblomqvist 09480979c4 practice: slim the tool schemas — the modelSolution IS the reveal
Owner round (2026-10-05): the problem pose only really needs statement,
mode, target, modelSolution; conclude needs no reveal field.

- problemInputObject drops decisionPoints, expectedWrongMoves, strategy,
  rationale. Halves the pose payload (the GLM incident's failing args
  were 3.3KB — a direct weak-model hardening).
- concludeInputObject drops solutionReveal: the reveal is the
  modelSolution committed at pose time, shown verbatim at conclude.
- Doctrine pass (00-base mechanics, 10-doctrine V.4/V.5): the commitment
  is the solution alone; the reveal instruction points at it.

Fixtures slimmed to the new shapes (flow, agents, heal). ConcludeCard's
render of legacy solutionReveal input stays until the card redesign
lands (Slice C).
2026-10-05 02:22:04 +03:00
amosblomqvist e8fe5da6b1 fix: honest activation + turn-scoped practice gates (the BKT stuck-slot incident)
The bkt-practice record showed ONE turn in which the model concluded
(clean ack) and then every pose died on 'a problem is already active'
until it gave up ('the problem slot is stuck — the conclude calls report
success'). Two flaws, both fixed:

1. GATES READ A FROZEN RECORD. problem/conclude gates re-read the stored
   record per execute — but the record freezes at the turn's write-ahead
   (persistTurn lands only at turn END), so a mid-turn conclude → pose
   pair could never see each other. practiceTools now builds ONE
   turn-scoped gate (createPracticeGate) seeded once from the record;
   the executes flip it. Fresh closure per turn (the agent is built per
   request) — no cross-turn leakage.

2. PHANTOM ACTIVATION. activeProblemOf ignored tool RESULTS, so the
   composer mounted from a pose the gate itself had refused (owner
   ruling: 'either it goes through or it doesn't'). Honest activation:
   a problem activates only on its clean ack; a conclude frees only on
   its clean ack; errored and dangling calls are skipped. Retroactively
   heals the BKT record (all its poses errored, all concludes clean →
   no active problem). pendingProblemDismissalOf follows the same law
   via the shared cleanToolCallIds helper.

Pinned: the mid-turn conclude→pose pair works in one toolset; the gate
re-arms after a successful pose; lone errored/dangling poses activate
nothing; a clean pose under a rejection stays active; an errored
conclude frees nothing.
2026-10-05 01:22:25 +03:00
amosblomqvist 9ccb33ea3b fix: the practice live-view law — live is positive evidence; absorb custom markers
The thinking-submission incident (owner report, 2026-10-05): two spinners
sandwiching the submitted thinking card, and the card doubling once tokens
flowed. Records on disk were clean — both bugs were live-view-only.

- view.ts LIVE IS POSITIVE EVIDENCE (owner ruling): a tool call renders
  live (input-streaming) ONLY when its own message is streaming THIS turn
  (streamingIds) — session busyness can never resurrect a settled call.
  The pose turn's problem call kept flipping to a mid-transcript spinner
  because the fallback keyed on live.inFlight. applyResultTo's placeholder
  fork now also MARKS the settled ack (input-available) instead of leaving
  the guessed state standing. Live cards now sit at the bottom by
  construction, never mid-transcript.
- session-core.ts absorbed(): the custom role gets its identity key
  (customType + details deep-equal) — the optimistic thinking marker is
  dropped when the write-ahead's server copy lands, so the +400ms
  adoption window can no longer double the card. Total over roles; types
  that never appear optimistically never reach the branch.

Pinned: settled problem never live mid-turn; this turn's streaming call
stays shimmer; dangling-settled is input-available; absorbed by
customType+details (different details = different submission); model-level
adoption keeps exactly one marker.
2026-10-05 01:02:43 +03:00
amosblomqvist 85022ae143 practice hardening: weak-model argument heal + parked dismissals mid-turn
The GLM incident (2026-10-04): glm-5.3 sent mode as a JSON-string-encoded
enum ('"recall"') on 19 consecutive problem calls; TypeBox's error never
named the expected values, so the model re-emitted the identical payload
until the dev server was killed. And mid-turn the learner was trapped:
Esc's question/problem rungs only dismissed — the dismiss route 409'd
against the turn-owned record and nothing could stop the running turn.

- healEncodedArguments (practice.ts, the prepareArguments seam): a STRING
  where the schema expects a literal union / boolean / null / number /
  array is JSON.parse-d and kept when it satisfies the schema. Valid args
  pass through untouched (same reference); unhealable values stay for
  validation to reject honestly. Wired on BOTH problem and conclude.
- dismissal-park.ts: the dismiss route parks a mid-turn dismissal (202);
  the turn's settle path (onFinish, after persistTurn, before endTurn —
  the same ordering law as the record write itself) applies it against
  the persisted record, only if the dismissed thing really is pending.
- confirmAbort: question/problem discard while busy now ALSO aborts the
  in-flight turn; the dialog line says so (busy prop).

No error-result cap (owner ruling). 16 new pins (heal incl. pi-ai
validateToolArguments end-to-end; park registry, apply paths, the 202
route contract).
2026-10-04 23:57:00 +03:00
amosblomqvist 4640117ba7 fix: the marker-only first turn — dead send + empty bubble
Owner report, 2026-10-04: starting a session with map picks showed ONE
empty user bubble, no agent turn; a follow-up woke the agent with the
right material but the marker never rendered. Two breaks, one root —
the record no longer starts empty and user-side data parts had no
render lane:

- The first-send gate checked initialMessages.length === 0; the
  graph-context marker at message[0] made that false, so the ?prompt
  turn NEVER fired. New pure helper initialPromptOwed (the escAbortTarget
  pattern): the prompt is owed while no user/assistant message exists —
  scaffolding is not conversation. Pinned in tests/graph-context.test.ts.
- The transcript's user branch rendered text parts only; a data-only
  message (graph-context, practice-thinking) fell through as an EMPTY
  bubble — the data-practice-thinking case I anchored to lived in the
  ASSISTANT switch, dead on arrival. Data-only user messages now hoist
  to a bubble-free card lane (the subagent-steer precedent); the two
  dead assistant-switch cases are gone.

Gates: 1296 pass / 0 fail; typecheck, lint, build clean.
2026-10-04 23:15:40 +03:00
amosblomqvist edfac20bf3 prompts: the shared formatting guide composes for practice too
The practice agent wrote wrong LaTeX delimiters — 40-formatting.md (the
\(...\)/\[...\) delimiter law, markdown, mermaid) lived in shared/,
which practice deliberately excluded. Owner ruling: formatting applies
in every setting. Practice now composes its own directory plus a
CURATED shared allowlist (40-formatting.md only — the question/
confidence/file mechanics stay out, practice has no such tools); the
composition test pins the appendix and the order.
2026-10-04 23:07:13 +03:00
amosblomqvist 90893ae236 graph context: a separate marker message before the first user turn
The map picks no longer glue into the opening prompt as seed text
(owner, 2026-10-04 late). POST /api/chats accepts graphContext
{propositions}; the route resolves the picks against the live graph,
derives the reasoning chains among them (chainsForSelection →
formatChainLine), and writes a graph-context custom marker as
message[0] — before the learner's first user message, in either mode.

- Core: newGraphContextMessage (model-facing content = the
  contextualization framing + proposition lines + chain lines;
  details = the structured pick); toLlmMessages flattens it to a user
  message like every marker, so the model reads it for free.
- View: projects to a user-side data-graph-context part.
- UI: GraphContextCard — minimal, muted, dashed, collapsed by default
  (counts only); expand reveals name — statement rows and the chain
  lines. Same compact-expandable pattern as the practice cards.
- Home: sends graphContext with the creation whenever picks exist;
  the prompt param carries the learner's own words only.
2026-10-04 23:07:13 +03:00
amosblomqvist 87f8e1e6f4 proposition pick: persistent lifted selection, live count, honest affordances
Root cause of 'doesn't register': the slice-6 floating-bar edit never
landed (a silent no-match replace) — the Body accepted selectMode but
the bar ignored it, so the picker showed Clear + Delete and nothing
ever confirmed. Found by re-reading; the test suite never pinned the
select-mode bar.

The owner vision instead: the chain table's selection is PERSISTENT
and caller-owned.
- ChainTableOverlay gains controlledSelection/onSelectionChange (every
  check, clear, and delete-prune reports up immediately; uncontrolled
  fallback keeps the delete flow self-contained).
- Select mode's floating bar: Done (closes; the pick already
  registered) — the delete fence never renders while picking.
- GraphView threads the lifted set through, resolving ids → nodes
  against the live snapshot.
- Home: the pick bar above the composer shows 'N propositions
  selected' + Clear in EITHER mode (dashed 'Pick propositions from the
  map' hint only in practice mode with nothing picked); visible hover
  (bg-accent) so the chip reads clickable. Practice sends still seed
  from the pick set.
2026-10-04 22:54:17 +03:00
amosblomqvist 89580d2b93 practice-flow integration test: the whole loop with zero tokens
Drives handleChatRequest with the real practice agent + faux engine:
pose persists its ack and owes no continuation; a commit arrives as the
'Committed answer:' user message and the conclude turn continues to the
reveal; a nudge carries the 'Thinking trace' label and never concludes;
Esc's silent marker deactivates at once and the next input rides the
synthesized dismissed-conclude pair (shared CONCLUDE_ACK_TEXT).
2026-10-04 22:13:44 +03:00
amosblomqvist 9362baf36e practice polish: no phantom wake, Practice header tag, honest meta
- needsContinuation: a persisted problem-pose ack (terminate:true ended
  the turn deliberately) owes no continuation — reloads after a pose no
  longer wake a phantom turn; error results stay owed like any
  interrupted tool.
- Chat header: practice sessions swap the phase chip for a quiet
  Practice tag (no probe→teach arc applies) and hide the coverage ring
  (practice never extracts — an ever-filling ring would lie).
- ChatSummary gains pendingProblem (activeProblemOf at projection); the
  sidebar meta says 'Problem waiting' for a practice row with a posed
  problem. Kind threads through Chat/[id] page.
- Home picker chip gains Clear (picks were otherwise irremovable).
2026-10-04 22:06:22 +03:00
amosblomqvist e76e42f764 practice entry points: sidebar filter, practice draft, graph picker
- Sidebar: the three-way kind filter (Teaching/All/Practice, persisted
  in localStorage) above the search; two new-session doors (Teaching →
  /chat/new, Practice → /chat/new?mode=practice); practice rows carry
  the dumbbell badge and 'Practicing…' busy meta.
- Draft: ?mode=practice swaps the copy (title, empty state, 'What do
  you want to practice?') and startDraftSession posts kind:'practice'.
- Graph entry (home composer): teach|practice switch on the stats row's
  left edge (StatsBar leadingSlot); practice mode shows the propositions
  chip that opens the chain table as a picker (its floating bar becomes
  'Practice these' — no delete fence); the picks seed the opening prompt
  ('Propositions from the map:…') while the title stays the typed text.
2026-10-04 22:01:42 +03:00
amosblomqvist 59bf5bf219 practice UI: the dock, the transcript cards, layered-Esc discard
- PracticeDock (components/practice-dock.tsx): the active problem
  replaces the composer — statement (markdown+LaTeX) above, the 'Your
  thinking' composer-twin below with the two submit lanes (Nudge =
  thinking without a commit; Commit = answer commit; ⌘/Ctrl+Enter
  commits). Dictation claims the thinking field.
- Transcript cards (practice-cards.tsx + the artifact registry): the
  problem card (compact-expandable statement — the commitment NEVER
  renders), the conclude card (outcome + verdict + reveal; the
  dismissal synthesis is one muted line), and the user-side thinking
  register cards (Your thinking / Your solution).
- view.ts: the problem pose joins the placeholder fork (the ack never
  flips the part); practice-thinking markers project as data parts;
  problem-dismissed markers project nothing.
- chat.tsx: activeProblemOf drives the dock branch (question > problem
  > composer), the esc layering gains the problem rung (below question,
  above turn) with its discard confirm + the silent marker route; the
  thinking submissions POST through the session's optimistic appends.
2026-10-04 21:55:54 +03:00