Commit Graph
811 Commits
Author SHA1 Message Date
amosblomqvist 827647c2f4 backfill script: optional single-chat arg (used to retry the Nietzsche extraction after an auditor non-submission) 2026-09-08 22:41:57 +03:00
amosblomqvist 56ed1fea40 Sync main (graph redesign b07c907): extraction observability ported onto the redesigned pipeline
Conflict resolution: extraction.ts takes main's redesign wholesale and
re-applies 072671f's observability deltas (stage durations, pipeline
failure line) — log shape only, no behavior change. graph-view.tsx's
legend/filters (094bafa) rekeyed from node.name (removed) to
statement/description, matching the new GraphNode shape.
2026-09-08 22:23:07 +03:00
amosblomqvist e73e6048ef Sync main (graph redesign b07c907) 2026-09-08 22:20:16 +03:00
amosblomqvist fe37854479 Sync main (graph redesign b07c907) 2026-09-08 22:20:16 +03:00
amosblomqvist b07c907ffd Graph redesign: true propositions, follows/supports, auditor-owned identity (vision §1 minimal implementation)
The minimal implementation of the 2026-09-02 graph vision (docs/vision.md §1):

- contracts/graph.ts: every node is a TRUE proposition (statement is both
  rendering label and identity key; required description; no kind, no name).
  Edges are follows (derivable) / supports (grounds without entailing) with
  message-granular provenance points {chatId, messageId}. Events reduce to
  correct/miss/dont-know plus observation (understanding notes); no taught
  events, no misconception kind.
- extraction.ts: extractor is TREE-BLIND (transcript only, no plan tree) and
  free-form; auditor sees findings + the current graph, owns faithfulness and
  identity (link-or-create verdict per proposition, recorded as linkTo);
  transcriptView tags every line with its message id (provenance anchors).
- core/graph.ts merger: applies verdicts deterministically — linkTo appends
  a link line (phrasings join aliases, provenance unions; reads merge by id),
  absent creates; NO string matching anywhere. Quiz events rebuilt from tool
  parts with attribution-only nodeId resolution; understanding notes become
  observation events (obs:<sourceId>); mapping lines are the verdict audit
  trail and make re-merges idempotent.
- deriveNodeStates: transitional — latest quiz event wins (known/missed/
  dont-know, else candidate); taught unreachable until the state-model
  ideation lands.
- graph-context.ts + 50-graph-context.md: follows chains, hotspot grouping,
  state vocabulary updated for the new event kinds.
- graph-view.tsx: node label is the statement; detail shows statement +
  description.
- tests: merger verdicts (link, no-string-match, cross-chat assertions),
  tree-blind pipeline end-to-end, follows vocabulary in context tests.
  276/276 green.
2026-09-08 22:19:37 +03:00
amosblomqvist e322d775e4 Vision §1 redesign: node theory, follows/supports, free-form extraction
Records the 2026-09-02 graph philosophy decisions, replacing the
2026-08-22 live-emission construction model (second reversal — back to
post-hoc, now deliberately tree-blind):

- Node theory: every node is a TRUE proposition (no kind field);
  statement (minimal, context-relative) + description (required) +
  aliases (verbatim phrasings); contradictions coexist — the
  surrounding subgraph is the context; misconceptions enter as true
  corrections, wrongness lives in the event ledger.
- Edges: follows (derivable) | supports (grounds without entailing);
  no scalars, no encoded strength — derived at read time.
- Provenance: message-granular (chat, message) points on nodes and
  edges, deep-linkable.
- Construction: free-form extraction fully decoupled from the
  session's formalism. Extractor sees ONLY the session transcript
  (tree-blind, message ids); auditor sees extractor output + the
  global graph (faithfulness net + identity judgment, link-or-create,
  aliases as phrasings); merger stays deterministic append-only.
- Event ledger: quiz outcomes deterministic from tool parts; NO
  taught events; extractor records per-proposition understanding
  notes (named-word scale, no numbers) — treatment TBD.
- Understanding: inferred, never encoded; state model (session-
  independent, computed from dependency depth/strength) is pending
  ideation — current five-class derivation is transitional.

Also updates: §2 plan tree (session scaffolding only), graph-as-context
(follows-edge subtree, event-derived hotspots), home page (message
provenance, transitional states), roadmap item 5, open questions
#2/#3 resolved, amendments table (+6 rows).
2026-09-08 22:05:09 +03:00
amosblomqvist 4131cb6b24 Vision §1 redesign: node theory, follows/supports, free-form extraction
Records the 2026-09-02 graph philosophy decisions, replacing the
2026-08-22 live-emission construction model (second reversal — back to
post-hoc, now deliberately tree-blind):

- Node theory: every node is a TRUE proposition (no kind field);
  statement (minimal, context-relative) + description (required) +
  aliases (verbatim phrasings); contradictions coexist — the
  surrounding subgraph is the context; misconceptions enter as true
  corrections, wrongness lives in the event ledger.
- Edges: follows (derivable) | supports (grounds without entailing);
  no scalars, no encoded strength — derived at read time.
- Provenance: message-granular (chat, message) points on nodes and
  edges, deep-linkable.
- Construction: free-form extraction fully decoupled from the
  session's formalism. Extractor sees ONLY the session transcript
  (tree-blind, message ids); auditor sees extractor output + the
  global graph (faithfulness net + identity judgment, link-or-create,
  aliases as phrasings); merger stays deterministic append-only.
- Event ledger: quiz outcomes deterministic from tool parts; NO
  taught events; extractor records per-proposition understanding
  notes (named-word scale, no numbers) — treatment TBD.
- Understanding: inferred, never encoded; state model (session-
  independent, computed from dependency depth/strength) is pending
  ideation — current five-class derivation is transitional.

Also updates: §2 plan tree (session scaffolding only), graph-as-context
(follows-edge subtree, event-derived hotspots), home page (message
provenance, transitional states), roadmap item 5, open questions
#2/#3 resolved, amendments table (+6 rows).
2026-09-08 22:04:10 +03:00
amosblomqvist afba6261a2 Research steers: the wave-1 'view full research' affordance — collapsed <details> carries the full brief
The Research-complete one-liner is now a native <details> disclosure:
collapsed it reads exactly as before (Research · complete · N sources);
one click expands the full persisted brief + source links via
ResearchBriefCard. No fetch, no state, no protocol change — the
data-subagent-artifact part in the record is the single source of truth,
which is what made this affordance 'feasible later' back in wave 1.
The brief still never clutters the transcript by default; the teacher's
wake turn remains the primary channel for weaving it into the lesson.

Test updated: the brief + sources now assert PRESENT inside the details
(and collapsed — no open attribute) instead of absent from the DOM.

Gates: typecheck, 279/279 tests, lint, build — all green.
2026-09-05 21:38:22 +03:00
amosblomqvist 072671f6da Extraction observability: stage durations + pipeline failure line
The §5 baseline had per-step lines, starts, drops, kept ratios, and
merge stats — but no durations (a background pipeline's cost profile
was invisible) and no failure line: a mid-pipeline throw left a
hanging 'pipeline start' in the server log with nothing recording
what failed or when. Now the extractor and auditor each log their
elapsed ms on completion, the merged line carries the pipeline total,
and runExtraction logs a failure line (with elapsed) before
rethrowing. Log shape only — no behavior, contract, or control-flow
change.
2026-09-05 21:36:40 +03:00
amosblomqvist f005713aab Merge branch 'main' into feat/ui-polish 2026-09-05 21:36:04 +03:00
amosblomqvist c014d22c9f Wire the 409 merge-retry transport into the chat (the one-line seam)
chat.tsx now uses createChatTransport(chatId): a send racing the wake
pipeline waits for the in-flight turn, merges onto the fresh server
record, and re-POSTs instead of surfacing the 409 banner. Also fixes the
transport's import to the core barrel (shell import law).

The optional "server-ahead heal" line in the jobs poller was considered
and skipped: the racing turn IS the wake turn, whose completion flips the
job to done within a poll tick or two — the existing settle-triggered
syncFromServer already heals the post-merge gap within ~1-2s. The extra
record fetch on every poll tick would double poll traffic for a path the
settle sync already covers.
2026-09-05 21:36:04 +03:00
amosblomqvist 6792b185b0 Merge branch 'main' into feat/graph-rendering 2026-09-05 21:36:04 +03:00
amosblomqvist 094bafaf2d Legend/filters panel: search + per-state visibility toggles
The top-right overlay doubles as the state legend and the filter
surface: a search box (matches name or statement, case-insensitive)
plus one row per node state in reading order (taught, known, missed,
don't know, candidate) with its color dot, label, and total count.
Clicking a row toggles that state's visibility.

Filtering flows through sigma's official nodeReducer/edgeReducer
settings: hidden nodes drop out of rendering, hover, picking, AND
labels; edges drop when either endpoint is hidden; an open detail card
closes if its node is filtered out. Pure predicate (isNodeVisible) is
exported and covered by 7 tests; the reducers just consume its
verdict. Verified live: search 'Nietzsche' 5457->1687->5457 label px
(exact restore), known toggle drops 13 discs+labels and restores,
nonsense query shows the 'No matching nodes.' hint with zero labels,
counts 0/13/0/0/10 match the ledger.
2026-09-05 21:35:07 +03:00
amosblomqvist b30192ff77 Merge feat/sub-agents wave 2: 409 merge-retry transport, dead pendingJob deletion, evidence-based timing, prompt-size guard
The 409 fix is a merge-retry, not a bare retry: a 409 means the server
record is AHEAD (wake turn persisted the steer), and the write-ahead
persist makes the request body canonical — a naive re-POST would clobber
steer+wake messages and steerDelivered makes the loss permanent. The
transport waits for the racing turn (500ms poll, 30s cap), refetches the
record, appends the client's unsent messages by id, and re-POSTs. The
integration seam (one line in chat.tsx) follows in the next commit.
2026-09-05 21:34:55 +03:00
amosblomqvist 2f8cf71f52 Wave 2: 409 merge-retry transport, dead pendingJob code, evidence-based timing, prompt-size guard
- components/chat-transport.ts (NEW): createChatTransport(chatId) — the
  chat.tsx seam is now a one-line swap. Its fetch turns a 409 on POST
  /api/chat (a learner send racing the wake pipeline's ~1s adoption window)
  into: wait out the in-flight turn (poll turnInFlight, 30s cap), refetch
  the record, MERGE the outgoing messages onto the fresh server list, re-POST.
  A bare retry would clobber the steer + wake messages via the write-ahead
  persist (steerDelivered makes that loss permanent) — merging is the whole
  point. Unit-tested: pass-through, wait+merge+reorder, dedupe by id,
  repeated-409 loop, bounded give-up.
- src/core/pending.ts: findPendingJobCall deleted (dead since wave 1 —
  job tools resolve server-side with an ack, nothing pends client-side).
  JOB_TOOLS kept (runner.ts still uses it). Header doc rewritten.
- src/server/jobs.ts: GRACE_MS 1500 → 750, with the evidence in the
  comment: the race window is only the learner's answer→POST flight
  (sendAutomaticallyWhen dispatches synchronously; ~10ms localhost,
  <300ms realistic RTT — 750 is 2.5× worst case) and misses self-heal
  via the pump's gate recheck, the 409 backoff ramp, and the new
  transport merge-retry. POLL_MS=400 kept (sampling granularity only).
- tests/jobs.test.ts: prompt-size guard — every fragment ≤8k chars,
  composed prompt ≤60k — the README-leak class (815 bytes ≈ 200 tokens
  per teacher turn before wave 1's fix) can't silently recur.

Gates: typecheck, 279/279 tests, lint, build — all green.
2026-09-05 21:33:13 +03:00
amosblomqvist 1a1b39aac5 Plan graph: fit scales small graphs UP to use the panel (wave-2 item 1)
The fit zoom was capped at 1×, so any graph smaller than the panel
rendered at natural size, centered, with big empty margins above and
below — the common case for early-session chains in the 384px panel.
The fit now scales up to fill (FIT_ZOOM_MAX 1.5 — a lone node doesn't
become a billboard), floor-clamped as before. Math extracted into
fitZoomFor() and unit-tested (scale-up, cap, width/height-bound,
floor, unmeasured viewport).
2026-09-05 21:29:17 +03:00
amosblomqvist e6409036a9 AGENTS.md workflow: domain-owned contracts rule + one-way data-copy allowance 2026-09-05 21:23:16 +03:00
amosblomqvist 8b145e226d next-env.d.ts: machine-generated churn (build variant) 2026-09-05 21:20:46 +03:00
amosblomqvist 8a9497bf59 Split contracts.ts by domain: plan / graph / chat / jobs
Purely mechanical: src/core/contracts/{plan,graph,chat,jobs}.ts carry the
sections verbatim (graph = nodes + ledgers + extraction findings; jobs =
the async ack-and-steer protocol + tool results + demo bridge), and
contracts.ts becomes a re-exporting barrel so every existing import path
(@/src/core and ./contracts.ts) is unchanged.

Wave-2 lane ownership follows the split: the graph stream owns
contracts/graph.ts, the teacher stream owns contracts/jobs.ts — same-file
contract collisions between streams are structurally impossible now;
cross-domain schema changes go through master.
2026-09-05 21:20:38 +03:00
amosblomqvist d8324ca9bb Visual maker model: qwen3.8-max (vision-capable, cheaper) 2026-09-02 18:42:25 +03:00
amosblomqvist 3271000ea4 Fleet panel gains a collapsed rail view in lockstep with the plan
The right sidebar's mode (rail | list | graph) is now shared state: a
new useTreePanelMode() hook lets every sidebar section collapse and
expand together. With sub-agents running in rail mode the fleet shows a
quiet strip — bot icon (click expands the sidebar) + vertical 'N running'
count — mirroring the plan rail's node-count strip below it. The sidebar
now collapses all the way regardless of running jobs.

End-to-end verified through the real flow (dispatch part in the
transcript + running job record, the 1s jobs poll rendering the panel):
rail column exactly w-10 with both rails stacked and aligned (40px), the
fleet rail's expand click transitions to list mode and persists, fleet +
plan panels equal widths in list (256/256) and graph (384/384) modes,
9-node graph intact, console clean.
2026-09-02 17:32:19 +03:00
amosblomqvist 6db4465ff3 Right sidebar reads as ONE panel: fleet section stretches to the panel width
The sub-agents fleet panel was a w-64 fixed aside with no left edge —
aligned with the plan panel only by coincidence in list mode, floating
detached in rail (w-10) and graph (w-96) modes. It now stretches to the
sidebar column's current width and shares the panel's border-l, so the
sidebar is one continuous panel with SUB-AGENTS above PLAN (thin border-b
divider). Verified headlessly in list and graph mode: identical edges and
widths (256/256 and 384/384), perfect vertical stack (fleet 0..99.5, tree
99.5..800), no overlap, 9-node graph intact.

The rail-mode-while-jobs-run question (fleet cards would cram into w-10)
is logged as wave-2 UX design work for the UI stream.
2026-09-02 17:25:05 +03:00
amosblomqvist 9fac2a0471 Fix merged layout: right sidebar is a full-height sibling; plan graph renders
The weave had nested the right sidebar inside the transcript column
(below the header), so the phase chip sat above the panel instead of left
of it and the panel was bounded by the scroller area. The column is now
a true sibling of the middle column under flex h-dvh, per the UI stream's
original design; TreePanel's aside gets flex-1 min-h-0 so it shares the
column with the fleet panel instead of overflowing it. The plan-graph
viewport (ResizeObserver-measured) now has real size, so the mermaid
view draws.

Headless-verified at 1280x800: panel spans 0..800 top-to-bottom in both
modes, chip right edge < panel left edge in both modes, graph SVG = 9
boxes + 8 beziers, console clean.
2026-09-02 17:09:10 +03:00
amosblomqvist df8862dfa7 next-env.d.ts: machine-generated churn (dev/build variant) 2026-09-02 16:41:44 +03:00
amosblomqvist 51a05d6c56 Merge feat/graph-rendering: graph labels + hover affordance
Node names word-wrapped as muted labels below nodes (global zoom gate),
constant-radius static glow in the node's own state color with
enter/leave crossfades, node-anchored detail card, and padded node
picking (+10px pickable radius via a picking-mode-only shader — the
maintainer-endorsed fix for sigma v3's half-resolution hitbox reads;
visible rendering pixel-identical).
2026-09-02 16:40:58 +03:00
amosblomqvist 25df0edd5b Merge feat/ui-polish: three-phase session model, plan graph, layout restructure
chat.tsx woven by hand: UI's session-surface layout (transcript column +
right sidebar as full-height siblings, h-10 header with PhaseChip) wrapped
in sub-agents' JobsContext.Provider; the message map carries both the
steer ternary (steers bypass the Message wrapper — the model-facing glue
text never renders) and UI's PhaseDivider emission (steers are verified
phase-boundary no-ops). Right sidebar = sub-agents' fleet panel column
(SubAgentsPanel above TreePanel, xl-only) carried onto UI's skeleton.
contracts.ts auto-merged (ChatSummary.phase/pendingQuestion + ChatListItem,
additive above the async-protocol section).

Smoke-verified on the merged tree: steer renders bare as its artifact
badge with no learner bubble, no rendered glue text, and no spurious
phase divider at the steer boundary.
2026-09-02 16:40:47 +03:00
amosblomqvist 4c7e5a7ba6 Docs: the async sub-agent protocol is ack-and-steer (README + architecture)
README mermaid + layout + protocol paragraph now describe the merged
protocol: server-executed job tools ack mid-turn, runners steer the
teacher later via the wake pipeline (server/jobs.ts), client adopts
wake turns via turnInFlight and renders steers as artifacts.
architecture.md turn-composition seam 3 rewritten; runner dependency
signature updated (deliver seam).
2026-09-02 16:35:44 +03:00
amosblomqvist e5e20a9233 Merge feat/sub-agents: pi-style ack-and-steer sub-agent protocol
Server-executed job tools resolve mid-turn with an immediate ack; the
runner steers the teacher later via a server-side wake turn (per-chat
serialized queue on globalThis, turn-free + quiz-aware firing, 409
retry, idempotent per jobId, stale-job sweep). Live observability client:
central /api/jobs poll, dispatch cards, steer artifacts (research/visual/
demo renderers), sub-agents sidebar panel, wake-turn adoption via the
existing turnInFlight machinery. persistTurn fills empty message ids
(withMessageIds). prompt composition loads numeric-prefix fragments only
(README.md had been leaking into the system prompt).
2026-09-02 16:34:32 +03:00
amosblomqvist 28ccc76594 fix: README.md leaked into the teacher's system prompt
composeInstructions loaded EVERY .md in src/agent/prompts — including
README.md, the directory's human documentation, which rode into the
system prompt as its own section (visible in OpenRouter logs, old POC
paths and all).

The loader now enforces the documented convention: only numeric-prefix
fragments (\d+-*.md) compose. README.md stays in the directory as docs
and can never reach the model again. README's rule text updated to
match; composeInstructions exported for tests; regression test asserts
the README never leaks and every numeric fragment composes verbatim.
2026-09-02 03:19:03 +03:00
amosblomqvist 8f8659c8b4 fix: scale rendered SVGs to the display area's full width
Figures now always fill the card's width (h-auto keeps the aspect
ratio): narrower svgs scale up instead of leaving a white strip on the
right when they carry their own background color; oversized ones still
shrink to fit. bg-white stays as a neutral backing for svgs without
their own background.
2026-09-02 03:12:22 +03:00
amosblomqvist 8f5276040c Padded node picking: +10px pickable radius via a picking-mode-only shader
Fixes the hover flicker the maintainers' way (jacomyal/sigma.js#1493):
sigma v3 picks nodes by reading a single pixel from a picking
framebuffer rendered at HALF the CSS resolution, and has no setting
for hit padding. Small nodes get tiny, ragged hit edges, and our glow
halo (9px beyond the disc) was NOT pickable — moving the cursor from
the disc onto its own halo de-hovered it, an oscillation loop.

NodeCircleProgram subclass (registered via nodeProgramClasses)
replaces only VERTEX_SHADER_SOURCE: a verbatim copy of sigma 3.0.3's
circle vertex shader that adds PICKING_PAD_PX (10) of radius under
#ifdef PICKING_MODE alone. The visible pass never takes that branch,
so rendering is pixel-identical (verified headless); the pickable
disc is disc + 10px, covering the whole hover halo, so the halo is
now hoverable and small candidate nodes are comfortable to hit.
sigma/rendering is imported dynamically (its ESM build touches
WebGL globals at module scope; SSR-safe like the sigma import).

This replaces the reverted prototype-patch approach (f12480e).
sigma v4 ships this officially as the nodePickingPadding setting —
noted for a future upgrade.
2026-09-02 03:10:34 +03:00
amosblomqvist 671fb0aa25 fix: backfill empty message ids in legacy transcripts + defensive key fallback
The persistTurn id hygiene (2d7b004) only covers NEW writes — records
persisted before it still carry messages with empty SDK ids, and two of
those in one transcript re-trigger the duplicate-React-key error.

- data/chats: backfilled all empty ids (6 messages across 5 records,
  msg_<ts>_<hex> scheme matching withMessageIds).
- chat.tsx: key={m.id || `m-${mi}`} on both message branches — even an
  unmigrated legacy record can never collide keys again.

NOTE for master at merge: the master tree's data/ needs the same
one-time backfill (any chat saved before the withMessageIds commit).
2026-09-02 03:02:23 +03:00
amosblomqvist 5a7e2748d7 fix: steer delivery renders bare in the transcript — no bubble, no model glue
Three refinements from live testing:

- Steer messages no longer sit inside the user Message container (the
  bubble background + right alignment it gave artifact cards). chat.tsx
  renders SubagentSteerCard directly in the transcript column — full
  width, centered, same register as the teacher's artifacts.
- Research steers render a quiet "Research complete · N sources" badge
  (the dispatch-settled idiom), not the full brief: briefs are long, the
  wake turn weaves them into the lesson anyway, and the full text stays
  persisted in the artifact part — a "view full research" affordance
  can be added later without any protocol change. Failures keep the
  failure card (the brief explains what went wrong).
- The sub-agents' verification notes (visual/demo builder notes) are
  model-facing glue and no longer render on success; failure notes
  remain the error explanation.

SSR-verified against a synthetic record: badge + SVG bare in the
transcript (no is-user wrapper), notes/brief confined to the RSC
payload. 229/229, typecheck, lint, build green.
2026-09-02 02:58:37 +03:00
amosblomqvist f1ce776008 Revert "Fix hover flicker: padded smooth hit-testing + leave-debounce"
This reverts commit f12480e040.
2026-09-02 02:54:51 +03:00
amosblomqvist fe88fa87ec Tree panel mode lives in the root layout: no open-then-close on session open
The persisted mode (lh:tree-panel-mode) was read inside TreePanel — a
per-page component that remounts on every session navigation. First
paint rendered the SSR-safe "list" default, then the mount effect
applied the stored mode: with the animated frame, opening a session
visibly opened the panel and then closed it. The mode now lives in a
root-layout provider (the SidebarProvider pattern) and survives
navigations untouched — the panel mounts already in the right mode.
The default stays "list" so SSR and first client render agree; the
stored preference applies once, on provider mount.
2026-09-02 02:43:28 +03:00
amosblomqvist f12480e040 Fix hover flicker: padded smooth hit-testing + leave-debounce
Root cause: sigma's getNodeAtPosition reads the picking framebuffer,
which is rendered at HALF the device resolution
(pickingDownSizingRatio = 2 * pixelRatio). Small nodes' hit circles
are a couple of device pixels across in that buffer with ragged,
quantized edges — near a node boundary, tiny cursor movements flip
the hover state on/off, and each flip makes sigma clear the hover
layer immediately. That state churn is the flicker; it survived every
animation change because it was never in the animation.

Mitigations:
- patchNodePicking() replaces Sigma.prototype.getNodeAtPosition with
  a smooth distance test (rendered radius + HIT_PAD_PX = 6px) built on
  public renderer APIs only (getGraph, getNodeDisplayData,
  framedGraphToViewport, scaleSize). No ragged edges, and small
  candidate nodes get roughly double the hoverable area.
- Leave-debounce (90ms): when hover drops, hold repainting the
  captured hover paint at the current fade (sigma would otherwise
  clear the layer the instant hoveredNode flips to null), and only
  fade out if the cursor hasn't bounced back within the window. A
  re-enter cancels the fade, so boundary oscillation never visibly
  dims anything.
2026-09-02 02:43:13 +03:00
amosblomqvist 8dda21ce1b Tree panel: animated width toggle, no reflow flash; steady chat header
- The three panel modes were three separate fixed-width asides, so a
  mode switch jumped the whole transcript column width in one frame:
  scrollbar flash, overshoot, re-correct. Now ONE aside animates its
  width exactly like the left sidebar (reusing the sidebar-motion
  hook, so it inherits the provider's mount-gated transition), with
  a FIXED-width inner layer clipped by overflow-hidden — content
  never reflows mid-transition.
- The chat header was auto-height: the sidebar toggle (a size-9 icon
  button) renders only while the sidebar is closed, so opening it
  collapsed the row and slid the title + phase chip up. The header is
  now a constant h-10.
2026-09-02 02:39:49 +03:00
amosblomqvist 2d9c649f0b Hover text: cross-fade on the same factor as the glow
The instant muted->bright color swap felt abrupt next to the fading
halo. The hover name is again drawn at partial alpha over the muted
resting label, using the SAME transition factor as the halo — text
and glow fade in/out in sync. Because every enter/leave interpolates
from the current fade value, rapid hover toggles at a node's hit
boundary glide instead of snapping (the flicker-damping fix).
2026-09-02 02:34:08 +03:00
amosblomqvist 2d7b004fda fix: live sub-agent observability — dispatch cards, steer artifacts, sidebar panel
The pi-style ack protocol (previous commit) left the client describing the
old world: artifact cards read the ack string as a result ("could not be
displayed"), the only job poller rendered on input-available parts that no
longer exist (so wake turns landed invisibly until re-navigation), steers
rendered as learner bubbles with no SVG anywhere, and server-persisted
assistant messages carried empty SDK ids (duplicate React keys).

- persistTurn (turn-service): fill empty message ids at the canonical write
  — server-only messages (last turn's, wake turns') now key uniquely;
  client-authored ids untouched.
- chat.tsx: ONE central /api/jobs poll (fast while jobs run, slow discovery
  cadence, off for job-less sessions) drives every job surface, re-syncs
  once when a job settles (the record already holds the steer + wake
  response at flip time), and adopts server-side wake turns via the same
  turnInFlight machinery as returning mid-turn. Steers branch to the
  artifact card — never a bubble. Composer no longer gates on jobs.
- /api/jobs now reports turnInFlight alongside the job list.
- dispatch-card: the tool-call box — live activity by toolCallId while
  running, quiet "delivered"/"failed" one-liner after; the ack is visible
  observability, the artifact lives in the steer.
- steer-card: renders the delivered artifact by kind (research brief /
  sanitized SVG / sandboxed demo iframe) from the data-subagent-artifact
  part; result renderers extracted and exported from the artifact cards.
- sub-agents-panel: right-sidebar section above the plan tree listing
  running sub-agents (kind, elapsed, live activity) — vanishes when idle.
- tests: dispatch/steer/panel render tests re-target the new protocol;
  persistTurn id-hygiene suite. 229/229 green, build green.

Out-of-lane touches for master review: chat.tsx + components/** (cleared
this wave), app/api/jobs/route.ts, src/server/turn-service.ts, contracts
protocol doc §2. Live-verified on :3003 (full loop, SSR DOM checked).
2026-09-02 02:34:02 +03:00
amosblomqvist 96da582d35 Hover polish: constant-radius glow (opacity fade only), instant text swap
- The halo no longer grows during the fade: radius is constant at
  size + 9px; only its opacity fades in/out over ~180ms.
- The hover name is drawn opaque in the bright color instead of
  cross-fading over the muted resting label. Partial-alpha text
  oscillated whenever hover toggles rapidly at a small node's hit
  boundary (sigma fires enterNode/leaveNode on every state change) —
  an un-animated color can't flicker.
2026-09-02 02:31:08 +03:00
amosblomqvist 0a8c360546 Hover glow: static (no pulse), fade-in/out transition, flush with disc edge
- The glow no longer pulses: it's a static halo (9px beyond the disc,
  0.45 alpha at the edge fading outward) in the node's state color.
- Hover enter/leave now cross-fades over ~180ms instead of appearing
  instantly: entering schedules sigma renders so drawHover repaints at
  the interpolated fade; leaving draws through a repainter captured on
  the last hover paint (sigma stops calling drawHover once unhovered).
  The text cross-fades too — the brighter hover name is drawn at
  partial alpha over the muted resting label on the layer beneath.
- The halo starts flush at the node disc edge (was size+1, leaving a
  dark gap ring that read as a gray border between node and glow).
- Both fade directions cancel each other and stop on completion, so
  there is no continuous loop — idle CPU stays near zero.
2026-09-02 02:22:18 +03:00
Amos aa14351474 Hover affordance: pulsing node glow + brighter name, drop the dark pill
- defaultDrawNodeHover no longer paints a background box: the hovered
  node now pulses a soft radial-gradient halo ring in its own state
  color (evenodd punch-out keeps the node disc untouched), phase driven
  by performance.now() inside the drawer.
- The hovered label text brightens (muted-foreground -> foreground) at
  identical geometry — drawName is shared by both drawers so nothing
  shifts when hover begins/ends.
- The pulse is animated by a rAF loop that only runs while a node is
  hovered (enterNode starts, leaveNode/leaveStage stop, cleanup
  cancels) — idle CPU stays near zero, preserving run-then-freeze.
- roundedRect removed with the pill (its only remaining consumer).
2026-09-02 01:57:25 +03:00
Amos 4a5bc40243 Session picker: one phase-colored dot, state via fill + motion
Redesign per review feedback: the colored phase strips were too busy —
removed. One small dot now carries two channels:

- COLOR = phase (sky/violet/emerald, same system as chip + dividers)
- FILL + MOTION = what's owed:
  - pulsing  = teacher is working (the only state that deserves motion)
  - solid    = a question awaits the learner's answer — the dominant
               resting state of real sessions, so deliberately calm
  - hollow   = idle, nothing owed

The amber attention channel is gone entirely (it would read as a
phantom fourth phase). The meta line now leads with the useful fact:
"Waiting for your answer · 5m ago" / "Teacher is working…" — the
message count only shows when idle. Ordering stays pure recency.
2026-09-01 19:49:47 +03:00
Amos 99fa3f0847 Pi-style ack-and-steer sub-agent protocol: dispatch mid-turn, steer via server wake turn
The job tools (research/demo/visual) switch from client-side turn-breaking
calls to pi's model: execute resolves IMMEDIATELY with a fire-and-forget
ack, the teacher's turn continues, and the result arrives later as an
injected user-role steer message that wakes the teacher in a new
server-side turn.

- contracts.ts: THE ASYNC PROTOCOL rewritten (ack now, steer later); steer
  types (SubagentSteerMessage, metadata.subagentResult + data-subagent-
  artifact part), pure buildSubagentSteer/steerDelivered/isSubagentFailure
- tools/jobs.ts: jobTools(ctx) factory — server-side execute creates the
  job (deduped by toolCallId), fires spawn with the dispatch-time input
  (the part isn't persisted yet at execute — a transcript scan would race)
- runner.ts: runJob(jobId, input?) builds the steer and hands it to the
  injected deliver seam; pipeline owns the done/error flip (a polling
  client re-syncs once, at the end); failures still steer
- server/jobs.ts (new pipeline): per-chat serialized queues on globalThis,
  idle gates (turn liveness + pending question, 1.5s grace), 409 retry
  with backoff, idempotent per jobId, stale-job recovery sweep (re-deliver
  or re-spawn); wake turns run through the same handleChatRequest
  composition via setTurnRunner injection
- chat-handler.ts: wires runner+deliver+tools ctx; drops spawnTurnJobs
  from onFinish; teacher default registry = question tools only
- plan.ts: PlanToolContext split out (plan tools need no job wiring)
- visual-maker: edit_svg added (pi parity — exact-match single
  replacement, fuzzy context on a miss); researcher: pi's kept/dropped
  source accounting restored
- prompts 20-jobs/40-demos: async discipline rewritten (announce and keep
  teaching, no waiting/polling/fabricating, parallel dispatch, teach with
  the artifact when it arrives)
- tests: jobs.test.ts rewritten (dispatch ack mid-turn, runner steers,
  wake pipeline: deferral/409/idempotency/failure), demos.test.tsx runner
  tests ported to runJob + fake deliver; 222 green

FLAGGED surgical touches outside the stream's owned files, for master
review at merge: src/core/contracts.ts (protocol doc + steer types),
src/core/ui-message.ts (job tool + data-part types), src/server/
chat-handler.ts (wiring), src/server/turn-service.ts (spawnTurnJobs
retirement), src/agent/teacher-agent.ts (default tools), src/agent/tools/
plan.ts (context type split).

Verified live on PORT=3003 with real models: teacher dispatched research
mid-turn, researcher ran real searches/fetches, steer injected on quiz
answer, wake turn wove the brief in citing its sources, job flipped done;
a client/wake race correctly 409'd.
2026-09-01 19:15:43 +03:00
Amos 475726a976 Layout + graph interaction fixes
- Chat page: the tree panel is now a full-height sibling of the whole
  transcript column (header + scroller), exactly like the left sidebar —
  the session header (name + phase chip) spans from the left sidebar's
  edge to the panel's edge instead of riding over the panel.
- Plan graph: the zoom controls sit inside the pan surface, so their
  pointerdown started a capture drag and the click never reached the
  button (felt dead). startDrag now ignores presses on buttons.
- Fully zoomed out IS the fit view: zooming to the floor snaps to the
  centered fit (pure zoomedGraphView), pan is locked there (no drag
  begins, cursor-default instead of grab), and sitting at the floor
  re-arms auto-fit so later tree edits keep the graph fitting.
- Fit padding 48px → 16px: the default view fits more graph.
2026-09-01 19:02:18 +03:00
Amos 08e417134b Fix hover pill geometry; fade labels in/out over a zoom span
- roundedRect typo: the first arcTo's second control point used y+width
  as the x-coordinate (y + width instead of x + width). Since arcTo
  tangent math is in absolute coordinates, the pill outline sprouted a
  stray wedge/notch whose size scaled with the pill's on-screen
  position (diagnosed live by the tester subagent: ~1000px left spike
  on a top-right node, right-side bites near the left edge).
- Labels now fade instead of popping: fully visible at fit zoom or
  closer, then the labels canvas layer's opacity is interpolated across
  a 0.5 camera-ratio zoom span before renderLabels switches off, with
  the setSetting call guarded so pans don't spam refreshes.
2026-09-01 19:01:25 +03:00
Amos 826de071dc Plan graph: the expanded mermaid-style view of the plan tree
The nested list stays as the default/collapsed view; the new expand
toggle (list header) opens the graph — a hand-built SVG diagram
(mermaid STYLE, no mermaid renderer: full control of the styling so it
speaks this app's design language):

- Top-down layered layout (topoLayout depth = rows, authoring order
  within a row, rows centered on the widest) — vertical space is the
  cheap axis; boxes + vertical bezier arrows with arrowheads.
- Status coloring matches the list marks: done nodes are colored in
  (teach-phase green tint — the graph fills in as the session
  progresses), current = the highlighted position, next = unlocked
  ring, upcoming = quiet.
- Drag pan + wheel zoom (to the cursor, non-passive), +/- and
  fit-to-panel buttons. The zoom FLOOR is fit: fully zoomed out, the
  whole diagram always fits the panel — it can never disappear into
  nothing. Auto-fits on measure/layout change until the learner takes
  manual control; fit re-enables it.
- Panel widens to w-96 in graph mode; mode persists
  (lh:tree-panel-mode: rail|list|graph, legacy key migrated).
- wrapTitle + graphLayout pure, unit-tested; TreeGraph SSR-render
  tested (statuses, arrows, wrapping, data-next).
2026-09-01 18:34:58 +03:00
Amos fbe4f16305 In-session phase presentation: header chip + dotted boundary dividers
- components/phase-divider.tsx: PhaseChip (header, next to the session
  title: phase-colored dot + Probing/Planning/Teaching) and PhaseDivider
  (the transcript boundary: color-coded dotted rules + a small chip —
  'Plan · N nodes' at the plan seam, 'Teaching' at the teach seam; probe
  never renders, phases only advance). planNodeCount reads the seam
  message's first plan tree.
- chat.tsx: dividers render at MESSAGE boundaries (phaseTimeline, one
  linear fold) — the seam is the transition to the message's phase, so
  the seam message's prose sits below its divider. requiresReveal now
  uses phaseAt3 teach (an occupied node), so probe- AND plan-phase
  questions auto-open.
- Retired the part-level plan-divider (artifact registry): the boundary
  is message-level, not part-level. Its tests replaced by
  render-phase-divider tests; core/phases gains tested phaseTimeline.
2026-09-01 18:31:01 +03:00
Amos 05359301e2 Session picker: phase strips, status dots, live polling
- SessionRow (extracted, exported for tests): leftmost phase strip
  (bookmark tick, per-phase color: probe/plan/teach) + status dot left of
  the title — pulsing amber attention dot when an answer is owed, quiet
  breathing dot while the teacher works, nothing when it's the learner's
  turn. A pending answer outranks 'working'.
- globals.css: phase + attention color tokens (one system shared with the
  upcoming header chip and dividers) and the two dot animations, both
  disabled under prefers-reduced-motion.
- SessionsSidebar polls /api/chats every 2.5s while open (payload is tiny
  now that busy/phase/pendingQuestion come from the list) and pauses while
  the tab is hidden; fetch-on-navigation preserved.
2026-09-01 18:28:35 +03:00
Amos 42c112f8c5 Labels appear all at once: one global zoom gate, no per-node size gating
labelRenderedSizeThreshold gated each node's label by its own rendered
size, so big (taught) nodes showed names several zoom stops before
small (candidate) ones. Zero the per-node threshold and instead toggle
renderLabels from the camera ratio: every name appears at fit zoom or
closer and they all vanish together when zooming further out. Hover
still reveals any node's name regardless of zoom.
2026-09-01 18:25:04 +03:00