Commit Graph
9 Commits
Author SHA1 Message Date
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 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 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 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 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 145b470752 fix: verdict death — plan→teach seam inferred from first nodeId-tagged quiz
The Accept/Decline verdict gate is gone (owner ruling 2026-09-26):
presenting the plan ends the teacher's turn; the learner's next message
is a natural reply. Teaching begins at the first quiz call tagged with a
nodeId (probe quizzes carry none) — both folds (view phaseAt3, native
phaseAt3Lh) advance on the call itself, and the teach divider renders
before that whole assistant block. No backwards compat: verdict markers,
the verdict input event, optimistic appends, composer buttons, and
projection all deleted; prompts and docs rewritten to the natural flow.
2026-09-27 00:48:46 +03:00
amosblomqvist b84ab1cdc0 fix: reasoning always renders above its message; dock drops delivered subagents at delivery
TWO owner-reported bugs from the live red-circle session
(chat_muhpyoll_27305e23), both investigated to root cause first:

1. THINKING BLOCKS UNDER THE MESSAGE. GLM emits trailing reasoning
   tokens, so the persisted content lands [text, thinking] (4 of the 6
   thinking-bearing messages from [21] on in that session) - and the
   view canon presented arrival order verbatim, rendering the reasoning
   UNDER its own message. Fix: projectView stable-partitions each
   assistant message - reasoning parts first (their own order kept),
   then text/tool parts. Owner ruling 2026-09-26: the reasoning belongs
   above, always. Covers live partials and settled records alike (one
   projection canon). NOT addressed (owner ruling): the plan_tools
   "missing" claim in the same session - wire-verified 11 tools incl.
   plan_init; GLM confabulation, no mitigation wanted.

2. DOCK CARDS LINGERING AFTER DELIVERY. /api/activity lists jobs with
   status "running", and the status flipped only in settleSteers at the
   DELIVERING TURN'S onFinish - so a subagent whose card was already
   visible in the transcript (marker injected mid-run) stayed in the
   background dock until the teacher finished teaching. Fix: flip the
   job at the moment the result reaches the learner - noteSteerVisible
   (jobs.ts) called from BOTH visibility seams: the marker's message_end
   wire event (live steer injection, ui-stream) and the write-ahead
   persist (the wake's input appends, chat-handler). finalizeJob is
   idempotent; settleSteers/runner-200 re-run it as a no-op, and the
   registry's delivery promise still settles exactly as before.

Tests: 1001/1001 - view-canon hoist pin (trailing [text, think, call]
projects reasoning-first; think-first unchanged), noteSteerVisible unit
pin (flip at the seam, idempotent, handoff no-op), and the mid-run
steering e2e now holds the turn open past the injection and pins the
job flipping done at the marker's message_end WHILE the turn still
streams. Live-verified read-only on the owner's server: the original
session reloads with every reasoning trigger above its message.
2026-09-26 05:46:33 +03:00
amosblomqvist 0803fbd88d fix: the empty-new-session bug — three live client defects
Live-driven via the owner's dev server (browser tools), reproduced on
/chat/new: send fires, optimistic appends, POST never happens, and the
page shows the frozen empty state. Three compounding defects:

1. session-core: default fetch stored UNBOUND — the browser's native
   fetch requires this === Window, so every call threw 'Illegal
   invocation' and the POST /api/chat never fired. Node's undici has no
   receiver discipline, which is why 994/994 tests passed while the app
   was dead. Now defaulted through a receiver-preserving wrapper; pinned
   by a strict-this regression test (the browser's semantics simulated.

2. session.ts: state memo keyed on [model, forceRender] — the reducer
   dispatch is STABLE by contract, so the memo computed once per mount:
   no optimistic bubble, no stream events, no error banner. The view was
   frozen at first render. Rekeyed on a version counter bumped by the
   model's onChange.

3. view.ts: turn_end carries the clean 'Question posed' placeholder as
   a toolResult message (details: null) — applyResult flipped the quiz
   part to output-available with a null output and AnsweredCard crashed
   on answer.dismissed (killed the whole page render). Fork 2 now holds
   on the message-attachment path too: clean question results with null
   details are ignored; the AnsweredCard renders only the learner's real
   answer. Regression pin extended in session-core.test.ts.

Verified live end to end: new session → text send → probe questions in
the dock → ask answer → graded ✓ quiz card with explanation → reload
renders the full transcript from the native record with the pending
question intact.
EOF
)
2026-09-26 02:13:18 +03:00
amosblomqvist f3003d357a Wave D: native AgentEvent wire + client session rewrite (SDK purged)
Server: ui-stream.ts is verbatim pi AgentEvent passthrough (SSE data
lines + [DONE]); only app-level events are the loop-level error and
turn-settled terminal; onError carries raw provider text (owner ruling).

Client: useChat + the AI-SDK transport replaced by components/session-core.ts
(SessionModel: 409 identity-retry, optimistic c_ appends, absorbed()
adoption, needsContinuation, shared consume for POST + reattach) and the
session.ts hook; the golden-master grouping canon moved client-side into
core/view.ts (projectView); scanners read the native record (phaseAt3Lh,
danglingQuestionOf, graph toolParts, plan-blocks, runner, extraction).

Coverage: the native message is the unit (fork 3) with the call+result
inseparability bond rule; view-line-less messages (markers, error
results) are invisible to coverage and never checkpointed.

Deletes: chat-transport, projection, ui-message, their tests (ported to
tests/session-core.test.ts), and the ai + @ai-sdk/react dependencies —
the Vercel SDK is fully out of the tree. Wire pins in chat-flow rewritten
onto the AgentEvent dialect (eventsOf parser).

Gates: tsc clean, 994/994 tests, eslint clean, build green; live smoke
on PORT=3004 (verbatim wire → native record).
2026-09-26 01:52:47 +03:00