Commit Graph
583 Commits
Author SHA1 Message Date
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
amosblomqvist 4bea0b46df practice agent: problem + conclude tools, doctrine prompt, handler branch
- tools/practice.ts: problem (terminate pose; commitment hidden in args)
  and conclude (non-terminating; outcome/verdict/reveal/rung/missClass/
  retest), gated by activeProblemOf — pose-while-active and conclude-
  with-nothing-active both return invalidInputResult.
- prompts/practice/00-base.md (voice + accuracy + tool mechanics) and
  10-doctrine.md (the owner's doctrine verbatim; one approved edit —
  §I material arrival generalized). Practice composes its own directory
  ALONE: no teaching base, no shared fragments.
- createPracticeAgent (teacher-agent.ts); chat-handler branch — kind
  'practice' builds the practitioner from turn 1 and skips extraction.
- problemResult/concludeResult acks in tool-execute (conclude text
  single-sourced: CONCLUDE_ACK_TEXT).
2026-10-04 21:47:23 +03:00
amosblomqvist eb7454900c practice record mechanics: active-problem fold, thinking channel, deferred conclude
The native shapes beneath the practice tools (owner design 2026-10-04):
- activeProblemOf — the conclude-before-next-problem gate's fold (a
  conclude call OR a dismissal marker deactivates; the pose-time ack
  never does — nudges keep the card up).
- The thinking input: {problemCallId, commit, text} → practice-thinking
  marker; the label (THINKING_HINT/COMMIT_LABEL, single-sourced in
  contracts) is the only register difference the model sees.
- Ask-style problem dismissal: silent route appends the UI-only marker;
  the user's NEXT input prepends the synthesized conclude pair (call
  outcome:'dismissed' + shared ack) so the agent reads the discard in
  its own vocabulary and the slot frees.
- toLlmMessages: dismissal marker filtered; pair passes natively.
2026-10-04 21:41:29 +03:00
amosblomqvist 7dcb006cf3 session kind: ChatRecord.kind teach|practice (absent = teach)
Practice sessions (owner, 2026-10-04): the marker rides the record and
the normalized summary (absent field reads as teach — pre-practice
records never migrate); POST /api/chats accepts {kind: 'practice'}.
The agent branch, tools, and UI land in the following slices.
2026-10-04 21:36:14 +03:00
amosblomqvist 814c2ad412 delete the batch practice-problems module (superseded by practice sessions)
Owner ruling 2026-10-04: the module is a sunk cost, retrievable from
history if ever revisited. Removed: core practice contracts + pipeline
helpers, the PracticeStore port + adapter + data dir path, server
service + API routes (sets, clusters picker, practice aborts), the
practice pages + dialog/exam/list UI, chain-table's Generate handoff,
sidebar nav, activity-dock practice rows, and the partitioner/
problemGenerator/grader model roles (BACKGROUND_MODEL_ROLES 8 → 5).

Survivors, moved to core/chains.ts (pure + browser-safe): ReasoningChain,
chainsForSelection, formatChainLine — the chain table and graph
emission still consume them.
2026-10-04 21:34:24 +03:00
amosblomqvist a2c1287415 provider-events: raw provider stream recorder (LH_PROVIDER_EVENTS=1)
pi-ai 1.0's onProviderStreamEvent, at the layer our incident class lives
(GLM arg truncation, DeepSeek id resets) — env-gated (owner choice:
no app surface, off by default; raw events carry full prompts), one
JSONL per chat under data/debug/provider-events/ with a 20MB cap and a
run-start marker for record correlation.

Wired at every agent construction: teacher/prober turns (AgentBuildOptions.
chatId), jobs via LoopContext (researcher, demo-builder through
runSubagent; visual-maker + pdf-reader through their streamOnce), and
the extraction pipeline (extractor + auditor keyed by source id).
practice deliberately unwired (sunk-cost module).
2026-10-04 20:21:18 +03:00
amosblomqvist d2d159d8ae isError results replace thrown ToolInputError (pi 1.0 convention)
Authoring refinements (quizInputProblems, nodeId checks, ask/explain
refinements, submit-agent pipeline laws) now return invalidInputResult —
same model-facing failure and retry semantics pi minted from the throw,
but details keeps {tool, problems} as data instead of the throw path's
empty {}. ToolInputError deleted; wire shape unchanged (verified against
pi's own thrown-error conversion in agent-loop).
2026-10-04 20:21:10 +03:00
amosblomqvist 02e7840b2f pi 1.0.2: bump @earendil-works/pi-ai + pi-agent-core 0.87.1 → 1.0.2
All imports and Agent seams survive (verified pre-bump). Fallouts, all
catalog drift handled by the documented refresh path:
- sync-pricing.ts: catalog schema v6 keys are type-qualified (chat:id);
  price chat models only, strip the prefix so PRICES keys stay provider:id
- prices.ts regenerated (1537 keys); auth-keys gains the typesafe provider
- test pins: glm-5.3-flash contextWindow 1048576→1048575, cacheRead
  0.05→0.03 (activity-feed, model-catalog, render-activity-dock, usage)

Inherited free: at-capacity retry, unfinished-tool-call error handling,
model samplingParams no longer dropped on direct stream().
2026-10-04 19:50:20 +03:00
amosblomqvist 5653a394ac layered Esc: an open overlay claims Esc before the abort chain (owner 2026-10-04)
Esc over a mermaid focus, the plan popup, or the setup dialog opened the
question-dismiss confirm underneath — KeyboardShortcuts' window listener
registers first, so the overlays' own self-closing Esc listeners could
never outrun the lh:kbd-abort-turn dispatch. The state gate in ChatView
is the defense (the architecture's own pattern from the abort dialog):
escAbortTarget, exported pure, rules priority by stacking order — abort
confirm (claimed by its dialog) > mermaid (paints above plan) > plan >
setup (self-closing) > question > turn. The gate closes the overlay; the
overlays' own listeners stay (idempotent). Pinned in esc-layering.test.
2026-10-04 18:39:34 +03:00
amosblomqvist 17a497a87c tool-call id uniquifier: the Lacan fix (owner-approved 2026-10-04)
DeepSeek via OpenRouter returns tool-call ids that reset every response —
a one-call-per-turn session names every call call_0. Everything keys on
toolCallId (the fold's call↔result join, the pending gate, answer
routing, the option-shuffle seed), so the Lacan session corrupted: the
quiz answer overwrote the ask card, the ask question vanished, and the
order scrambled. Record itself was always intact.

- src/server/pi/call-ids.ts (new): ToolCallIdUniquifier at the wire
  boundary — colliding ids remint once (orig~2, ~3…), the same mints at
  settlement; tables key by message OBJECT IDENTITY (streaming messages
  carry no ids; the message_end object is reference-identical to the
  state object). History never rewrites; providers see paired ids, which
  is protocol-legal (pi synthesizes ids for providers that send none).
- ui-stream: runPiTurn remaps events + settles through the uniquifier
  (spec.usedToolCallIds, seeded by chat-handler from the record).
- view.ts: findToolPart scans newest-first — pre-fix duplicate-id
  records (the existing Lacan session) heal on next render.
- tests: 9 unit pins (call-ids.test.ts) + the two-call_0 e2e (chat-flow):
  ask answered as ask, quiz as quiz, ids unique in the record.
2026-10-04 01:19:54 +03:00
amosblomqvist 360d564a6c two questions per teaching unit: predict → teach → check (owner 2026-10-03)
The teaching cycle gains its motivation question. The teach quiz schema
now carries a REQUIRED type field — predict (posed before the teaching,
arrives open, the learner predicts the next step) or check (posed after,
arrives collapsed behind the reveal). The prober's quiz stays role-less.

- core: quizInputObject.type union + normalizeQuizType + the reveal law
  as pure quizNeedsReveal; QuestionData.questionType; note → thinking
  rename (legacy reads fold back); readingThinking channel on answers;
  compactAnswerForModel emits thinking / thinkingWhileReading
- tools: descriptions stripped to mechanics — QUIZ_OPTION_CRAFT and
  EXPLAIN_DOCTRINE deleted, doctrine moved to the prompt layer
  (reverses the 2026-09-27 placement, owner 2026-10-03); shared
  quizDescription/explainDescription builders differ only in deltas
- prompts: shared/20-question-tools.md rewritten (option craft,
  transcribed-speech leniency, the two thinking channels); new
  teacher/28-predictions.md (cycle, roles, skip rule, resolution law);
  20-teaching + 30-question-principles scoped to the new taxonomy
- UI: predictions render open with a Prediction eyebrow, verdict shows
  but the explanation tucks behind a toggle; the reading-thinking field
  mounts while a check waits (dictation claims it until reveal);
  answered card separates thinking vs thinking-while-reading blocks
2026-10-04 00:53:29 +03:00
amosblomqvist 571cc94053 steering demoable: the Enter branch keys on BUSY (own-stream clears turnInFlight — Enter was swallowed during our own turn), and the button follows the text (owner live report 2026-10-03)
Root cause: doSteerOrSend branched on turnInFlight, but the local dedup
(chat.tsx ~370) clears that flag the moment the own stream takes over —
during our own turn busy was true and turnInFlight false, so Enter fell to
the plain-send path whose busy guard returned silently. Branch on busy:
own stream AND foreign/wake turns both have a live agent registered by the
time the send's POST resolved, so the steer lands (a raced settle 409s and
falls back to the plain send).

The button now follows the text: busy + typed text = Send (click steers —
steering must be clickable, not Enter-only); busy + empty = Stop. ESC keeps
its confirm-popup abort in every state, so stop is never unreachable.
2026-10-03 03:07:49 +03:00
amosblomqvist 5271ca0e4a composer steering: the general steering machinery (owner arc 2026-10-03)
STEERING, NOT FOLLOW-UPS — pi-agent-core's two rails, used deliberately:
agent.steer() injects at the next response boundary INSIDE the live run
(one logical turn, the model changes course); followUp() would only extend
an ended run — not built. The same rail job markers already ride
(steeringMode 'all', the 2026-09-26 doctrine).

- POST /api/chats/[id]/steer: 404/400 honest guards; no live turn → 409
  and the client silently falls back to a plain text send (the settle
  race); live → steerLiveChat hands the message to the live agent's
  steering queue AND parks it in the userSteers registry — the stranding
  safety net. A steer stranded behind a question termination or abort
  stays parked and rides the chat's NEXT run (handOffEligible), the same
  deliver-or-requeue lifecycle as job markers. Reconciliation at settle:
  delivered ids drop (reconcileUserSteers).
- Client: steer(text) queues + POSTs without flipping any turn state;
  the drain's verbatim message events upsert the real user card by the
  client-minted id (steer_<localid>) and the indicator fold drops the
  line. canSend no longer gates on turnInFlight — Enter mid-turn steers;
  Enter with an empty composer still stops; the square button stays Stop.
- The queued-steer indicator (owner spec): quiet one-liners above the
  composer shell — right-aligned to where its corner rounding starts,
  truncated with an ellipsis, fixed max width, ~half opacity, no
  background, no container. Tunable later.
- Verified from pi-agent-core source: steering drains per completed turn
  inside the loop; injected messages emit message_start/end verbatim;
  finishTurn {action:'end'} (our question stopper) exits BEFORE the
  steering poll — hence the registry.
2026-10-03 03:01:51 +03:00
amosblomqvist a3a048f572 the last resolution decides the wake (owner ruling 2026-10-03, round-1 incident)
Live test finding: answer A then dismiss B woke the agent — the continuation
fold treated a mixed [answered, dismissed] trailing batch as 'reaction owed'
(all-dismissed rule only). Dismiss A then answer B and dismiss-both were
already correct.

The law now: the trailing batch's LAST resolution decides. Ending on a
dismissal = the user signaled stop — no wake, the answers' reactions ride the
next plain message. Ending on a real answer = the reaction is owed. One law
with the pump's pause (endsInDismissal reads the same last result), so
continuation, delivery-holding, and the pause can never disagree. The wrong
pin ('a real answer in the batch still demands the reaction') flipped.
2026-10-03 02:27:44 +03:00
amosblomqvist 7a5a55e94a parallel question gate: an answer that leaves a sibling dangling defers its wake (deepseek batch incident fix, owner 2026-10-03)
The incident: deepseek batched quiz + ask_user_question in one assistant
message; the quiz executed first (sequential) and terminated the turn; the
answer to the quiz IMMEDIATELY woke a reaction turn (fork-1 design assumed
one dangling question max), the model complained about the unanswered ask,
and the ask became permanently unanswerable — its toolResult could only
append AFTER the intervening assistant message, a shape DeepSeek rejects.

The gate: for answer inputs, if a question call still dangles after the
append, persist write-ahead + emit record-changed + return the settled
terminal — no turn. The dock advances to the sibling; the LAST answer fires
the one reaction turn with every result adjacent to its calling message.
Nothing else generates while a question dangles (job wakes wait, continuation
folds refuse, the composer gates), so adjacency holds by construction.

Also repaired the stranded session data (ask call stripped from the batch;
backup at /tmp/chat_murkbeq2_backup.json).
2026-10-03 02:10:16 +03:00
amosblomqvist 673a430d6e question dismissal, A2: the silent write + the dismissal pause (owner design 2026-10-03)
- {type:"dismiss"} input event: parse + pure appends with the answer-event
  orphan discipline (dangling-call 400, kind match, answer must be dismissed)
- POST /api/chats/[id]/dismiss: the no-turn composition — write-ahead append,
  409 while a turn runs, emits record-changed (new bus word; idle views sync)
- needsContinuation: an all-dismissed trailing batch demands nothing;
  endsInDismissal = the derived pause predicate (tail-based, no latch)
- wake pipeline: while the record ends in a dismissal the pump holds every
  delivery in the steer queue — the user's next send is the wake (until the
  text-turn steer claim lands, settlement re-pump delivers right after it)
- session-core.dismissQuestion: optimistic skipped card + silent POST;
  chat.tsx confirmAbort question-mode swaps submitAnswer for it; ESC-abort
  popup everywhere (AbortTurnDialog) with popup-Esc claimed via state gate
- parallel question calls dismiss call-by-call (results bind by id)
2026-10-03 01:51:13 +03:00
amosblomqvist 3e5b857c63 quick-fixes round: quiz-only teach-check reveal, ESC-abort confirm popup + question dismissal, confidence prompt diet
- requiresReveal gates on pending.tool === 'quiz' (owner: only graded quizzes
  arrive collapsed; ask/explain always open)
- ESC-abort chain opens AbortTurnDialog everywhere (owner ruling); from a
  pending question abort IS dismissal — dismissedAnswer rides the answer
  path, the teacher acknowledges the skip in its next turn; popup-Esc is
  claimed via the state gate (two window listeners fire per Esc)
- 25-confidence.md: stripped the slider/JSON/calibration mechanics — pure
  reading doctrine remains
- toolQuestionKind named once in core (dock parity), dock switched to it
2026-10-03 01:03:29 +03:00
amosblomqvist 60971ca6fe vision: the three-part learning model + extensibility direction + the 2026-10-02 roadmap (owner frame doc) 2026-10-03 00:56:02 +03:00
amosblomqvist 76387b043b coverage meter one-truth fix + chunk-budget setting UI: fetchGraphRead resolves through the shared core resolver (the stale 4096 fallback filled the ring at half the 8192 truth); Graph section gains the extraction chunk budget row (floor 500, empty = factory) and the master switch composes instead of replacing the section 2026-10-02 21:18:51 +03:00
amosblomqvist 2ef09b0282 fix the duplicate-previous-assistant-message bug: turn-settled for our own turn flags the ready flip to canonize onto the record (push-pure fix B — the tail's c_-id streamed copies were never absorbed, so the next send's adoption window rendered them twice) 2026-10-02 18:53:16 +03:00
amosblomqvist 26131b3599 push architecture slice 7: graph-status + practice surfaces ride the events channel; coverage polls accepted-by-design; stale poll comments + architecture.md observability section rewritten to the push model 2026-10-02 18:03:47 +03:00
amosblomqvist 8795345c82 push architecture slice 6: chat store announces list-shape writes; sidebar subscribes (chats-changed + all-chat turn events, debounced) — the 2.5s sidebar poll dies, the last standing poll 2026-10-02 17:38:48 +03:00
amosblomqvist d6450e260a push architecture slice 5: extraction/practice registries announce activity-changed; dock subscribes (debounced) — the adaptive poll and every chat-view dock nudge retire 2026-10-02 17:31:35 +03:00
amosblomqvist a38c400eb7 push architecture slice 4: job writes announce jobs-changed from the store; chat view fetches on announcement (debounced) — the jobs interval poll and pollMs machinery retire 2026-10-02 17:20:22 +03:00
amosblomqvist 1345599ff7 push architecture slice 3: ServerEventsProvider + chat view attaches to server-initiated turns via the events channel (handoff/jobs-wake/other-tab discovery — the poll lattice retires) 2026-10-02 17:15:17 +03:00
amosblomqvist ef6d15ed93 push architecture slice 2: turn-started/turn-settled emit from the liveness seams (registry-first ordering, no-op paths silent) 2026-10-02 17:07:03 +03:00
amosblomqvist 78c7393909 push architecture slice 1: core event bus + /api/events SSE channel (notify->fetch infra, no consumers yet) 2026-10-02 17:04:22 +03:00
amosblomqvist a85087528e tool-result diet: question answers return only learner-added truth (compact JSON for the model, full record in details); dead-code sweep 2026-10-02 04:04:04 +03:00
amosblomqvist 9a2e434748 spinner size standard: h-3 everywhere — reasoning row matches the placeholder 2026-10-02 03:09:22 +03:00
amosblomqvist a927884ced sidebar turn-state: instant lh:turn-state propagation, poll demoted to backstop 2026-10-02 03:05:48 +03:00
amosblomqvist 2a1bac89ed optimistic round 3: action-gated placeholder, first-applied-event flip, clean-slate replay, empty message renders the thinking row 2026-10-02 03:00:56 +03:00
amosblomqvist e5b06f7fbd placeholder is content-gated: survives agent_start, releases at first visible content, sticky per turn 2026-10-02 02:42:25 +03:00
amosblomqvist 0f99a92fd3 optimistic submit: one Thinking placeholder everywhere — spinner from the instant of submit 2026-10-02 02:29:24 +03:00
amosblomqvist 142f1978f2 reattach round: attach-first handshake, honest Reattaching placeholder 2026-10-02 02:23:20 +03:00
amosblomqvist 0751fec9d8 streaming round: thinking collapses by default, live tool-input cards, one glimmer grammar 2026-10-02 02:09:38 +03:00
amosblomqvist a99cbf3d90 dictation mechanics everywhere: graph view + new-session view
Consistency ruling (owner, 2026-10-01): the finish-first send and the
run-end focus are properties of the COMPOSER, not of one page.

- home (graph view, raw textarea): topicRef mirror; start() finishes
  dictation first (before the setup/key guards, so an intercepted draft
  is the complete one) then reads the mirror; the start square stays
  live while a run holds deferred speech; run end focuses the topic
  input.
- new-session: the shell owns the dictation instance — handleSend wraps
  the parent's send with finish-first, the composer stays sendable
  during a run, and run end focuses it via the Composer's inputRef. The
  parent's send reads a textRef mirror (applyText at every write): the
  flush lands via setState after the shell's await, and the parent's
  closure must not read a stale draft.
2026-10-02 01:42:55 +03:00