Chat records gain version (CURRENT_CHAT_VERSION), stamped by a single
writeChat choke point in the store; absence reads as v1 — existing files
migrate on their next write, no mass rewrite. The graph family (four JSONL
ledgers, sources.json, render-settings.json) shares one lazy sidecar,
graph/format.json; positions stays unstamped (rebuildable) and jobs stay
unstamped (transient).
Downgrade scenario ruled honest-refusal: records stamped newer than the
running build read as absent (chats, logged) or throw (graph family) — a
read-modify-write store never rewrites what it doesn't understand.
Golden fixtures stay the unstamped legacy shape on purpose (lazy v1 is
current-valid by design); the stamped write path is pinned by
tests/schema-stamps.test.ts. Reads never write — the golden byte-identical
pin holds.
The tools_fails_2 diagnosis (glm-5.3 dropping trailing required fields):
- probe-phase quiz/explain renamed to probe/recall — a shared name carried
the probe call shape into teaching (the first teach quiz dropped nodeId);
display seams accept the old names forever
- teach quiz+explain schemas lead with the required pair (type, nodeId);
explain gains the predict/check type and joins the reveal law (checks
collapse, predictions arrive open; no-type records render check-style)
- plan_init/probe_done results go context-slim (owner, context diet):
content = validated nodeIds + binding-id reminder / fixed ack; details
keeps the tree for the popup and {ok:true} — serializer-verified that
details never reaches the model; failure echoes dropped
- the cards carry the tool's own name (Probe/Recall) and the quiz type
reads on the label (Prediction/Check) in composer and transcript
- bridge measures the BODY (scrollHeight + rect + margins) instead of the
viewport-clamped documentElement — documentElement.scrollHeight latches
at the parent's own height, so demos could grow but never shrink on
window resize (owner-reported); smoke: 1036 → 2386 → 1036 px
- invert now PERSISTS as details.ui.inverted on the steer record, written
by the silent artifact-ui route (dismissal pattern: no wake, no SSE) and
seeded through the projection (SubagentArtifactData.ui); survives
navigation + reload, never enters teacher context or exports
- invert restyled as the house Toggle switch (settings controls) on both
cards; demo header is the caption grammar: 'INTERACTIVE DEMO — title'
left (title normal-case, same size/tracking), switch + download far
right; figure header: label left, switch/download/open far right
1439 tests green (route + pure-seam pins added); live smoke passed
Owner round (2026-10-06, release polish #3): first messages are always
long, and the auto-title's hard 72-char cut landed mid-word. The cut now
snaps to the last word boundary at or under TITLE_MAX_CHARS (72, exported
and tunable); whitespace collapses first so boundaries are single-space.
The one special case (owner-specified): a single word longer than the cap
— rare but real: identifiers, URLs — splits at the cap. A boundary landing
exactly at the cap is a clean cut. Seam unchanged: titleFromPrompt
(core/contracts/graph.ts), applied once server-side at chat creation.
Pinned in tests/title-from-prompt.test.ts (6).
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).
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.
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.
- 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
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.
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.
- 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).
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.
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.
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.
Owner live-test verdict on the spine (2026-09-30): no perceptible gain —
the real problems (restatement nodes, shallow re-derivations, same
conclusion from many angles) are mint-time issues a post-hoc chain
minter cannot fix. This arc moves assembly INTO the extractor and adds
the deterministic dedupe the owner specified:
- Digest-aware extraction: each stretch's extractor prompt carries THE
GRAPH SO FAR (its own source's merged propositions + chains, ids
verbatim); build on existing ids instead of restating, lean on
established conclusions as premises. Mixed-namespace chain endpoints
(fresh p-ids OR existing n_-ids) validated at the submit seam.
- Serialized per source: a source's batches/chunks run strictly in
order (each extract reads every earlier merge); cross-source runs
stay parallel; source-wide abort stops the loop. The spine stage,
role, phase, usage stage, cost row, and setup roster are gone.
- Core-ideas prompt: goal reframed as uncovering core ideas and showing
how everything derives from the foundations; direction upward, prefer
lean derivations, never re-derive what the graph already holds.
- Chain simplification (merger maintenance): simplifyChains drops
provably redundant chains at every merge — prefer-fewest-premises +
ancestry-cover ([A,B]⟹X with [X,C]⟹Y covers [A,B,C]⟹Y, transitive);
same-key provenance lines drop together; rewriteLedgers under the
audit lock; droppedChains counted separately in stats.
Chunk budget stays 8192 (owner: keep for now, maybe lower later).
Per-batch pipeline is now extract → spine → audit → merge (owner order,
2026-09-30), the spine inside the serialized audit section so it reads
the graph exactly as the following audit will. Measured motive: 0 of 133
chains in the live corpus crossed a stretch boundary — no stage minted
cross-stretch chains.
- src/agent/subagents/spine.ts: SESSION/NOTES spine preambles + shared
SPINE_CORE (span doctrine; extraction doctrines carried over), the
submit_spine TypeBox schema, validateSpine (mechanical span law: every
chain touches >=1 fresh endpoint AND >=1 existing digest endpoint;
unknown ids, self-reference, and id collisions are loud), the digest
prompt builder, and the runner (strong tier, retry-only).
- extraction.ts: 'assembling' phase, spine stage in the per-need gate,
usage/billing stage, assemble() inside withAuditLock for both front
doors; first stretch of a source skips the call (empty digest, no
spend); the audit sees COMBINED propositions (chunk + connectives),
never chains; merger receives the combined findings.
- core/graph.ts mergeFindings: chain endpoints resolve through the
job-local map OR pass through verbatim as existing n_ ids (the spine's
mixed namespace).
- EXTRACTION_CORE: steps-of-reasoning selection bar (examples, notation
glosses, and restatements that no argument needs are not steps).
- Factory chunk budget 4096 -> 8192 (fewer seams, more local context).
- healStringifiedArrays moved to the shared submit seam (submit-agent.ts).
- Cost panel: Assembly row; usage records carry a spine stage (optional
on legacy records).
- Tests +5: prompt pins, span-law matrix, digest rendering, seeded
end-to-end flow (spanning chain + connective, verbatim n_ endpoint,
spine billed as its own stage), non-spanning refused + retried.
Owner ruling (2026-09-30, live-testing loop — supersedes 02a58db's
exemption): implication nodes ARE depth-anchored after all. Their anchor
is the average of the HIGHEST premise row and the conclusion's row —
depth(imp) = (max(premise depths) + depth(conclusion)) / 2 — so the
junction always sits strictly above every premise and strictly below
the conclusion (a conclusion is >= highest premise + 1 in the logical
graph). Uniform across modes: a half-row in vertical, a ring midway in
radial (owner confirmed both).
Implementation:
- layout.ts: new ImpLink type; SimOptions/incrementalLayout gain
impLinks (the derived chain linkage). The rank map is now computed
once alongside the orphan mask: proposition ranks via Tarjan over the
logical edges, then each supplied link adds its midpoint entry. An
imp node WITH an anchor is structured (leaves the orphan mask —
gravity projections treat it like any ranked node); WITHOUT supplied
links, arg_ ids keep the legacy 02a58db exemption (honest fallback
for callers that predate the field). The depth force reuses the
precomputed map — one pass, shared.
- deriveView (core/graph.ts) emits impLinks (one per multi-premise
live chain — single-premise chains mint no imp node); GraphSnapshot
carries them; graph-view maps them into CanvasData; settleGraphLayout
and replay-layout forward them (replay activates a junction's link
exactly when its chain is delivered).
Tests (+3, 1 rewritten): the 02a58db exemption pin became the MIDPOINT
pin (premises row 0, conclusion row -500, junction pinned to -250);
mixed-depth pin (premises at 0 and 1, conclusion 2, junction at the 1.5
half-row — highest premise, not average-of-premises); legacy fallback
pin (no impLinks = never depth-anchored); deriveView impLinks emission
pin (multi-premise only). The horizontal-pole and ORPHAN RULE pins from
5554c3a survive unchanged — proposition orphans are untouched.
Owner-approved brief (orchestrator, physics arc): two laws.
1. IMP-NODE CLUSTER ASSIGNMENT. Imp nodes were unclustered (the partition
runs over stored propositions only) — no centroid pull, outside the
painted hulls, bundles sitting on the periphery pointing outward.
Now: a derived imp node's cluster is a function of its derivation —
MAJORITY among its PREMISES' birth sessions (owner: premises, NOT the
conclusion), ties to the chain's OWN birth (its provenance's first
point — the asserting session; deterministic, coincides with the
unanimous common case). Imp nodes are always re-derived, never
birthed, so the assignment must be exactly this.
Seam chosen: core/session-clusters.ts exports impClusterKey + a
shared attachImpToCommunities that mutates a communities list's
members IN PLACE. graphSnapshot attaches AFTER the hull filter (a
2-proposition community + its bundle never resurrects a hull — the
filter counts propositions); settleGraphLayout attaches to the
unfiltered partition (settle pulls every community; hulls are a
canvas-only concern). Both cluster-membership consumers — the sim's
centroid pull and buildRegions' rasterization — read it through the
members lists that went through the one helper, so the resting
ledger and the live sim agree on where bundles belong.
2. DEPTHS OVER THE LOGICAL GRAPH. inferenceDepths ran Tarjan over the
DRAWN edges (premise→imp→conclusion), inflating every multi-premise
conclusion one hop (depth(imp)+1 instead of depth(premises)+1) and
ranking the geometry point itself. Now:
- deriveView additionally emits logicalEdges — every chain contracted
to direct premise→conclusion arrows over PROPOSITION ids only
(per-premise, deduped — a conclusion's rank is max over premises,
which a single arbitrary premise edge could not express).
- GraphSnapshot carries logicalEdges; createGraphSimulation /
incrementalLayout accept a logicalEdges option: the depth
machinery (orphan detection AND the rank pass) reads it, springs
keep the drawn edges. Absent = edges (legacy behavior).
- Imp nodes join the ORPHAN-EXEMPTION mask explicitly — never pinned
to a level, keep the full regular forces (gravity, cluster pull,
springs), settle naturally between premise and conclusion levels;
the rank map is computed over propositions only, so arg_ ids are
absent from it by construction.
Wiring: settle + canvas (CanvasData.logicalEdges → buildSim) +
replay-layout all pass the logical set.
EXTRA FIX (in-slice discovery, one line, graph-server domain):
server/graph.ts graphClusters — the practice picker's /api/graph/
clusters — partitioned snap.NODES (the canvas image, imp nodes riding
it since chains-primary), leaking arg_ ids into nodeIds: pickable ids
that silently vanish inside chainsForSelection while nodeCount lied.
Now partitions snap.propositions — same purity law as the chain
table's 767a559 fix.
Tests (+13): impClusterKey pins (unanimous; cross-session majority; tie
→ chain's own birth, not the conclusion's); attach pins (members gain
the imp id, sorted; single-premise attaches nothing — no node exists;
dangling mints nothing; home absent from the list stays unattached);
snapshot pins (imp id rides cluster members to both consumers; the hull
filter counts propositions); logical-depth pins (multi-premise
conclusion at premises'+1 NOT +2; imp absent from the rank map);
depth-force exemption pin (rows pin crisply, arg_x floats unpinned —
old behavior stamped it onto the conclusion's row); deriveView logical
arrows (per-premise, deduped, proposition ids only).
The owner's live test surfaced it: after importing notes, the chains view
listed implication nodes as bogus singleton "chains" — and each carried
the same arg_ id as the real stored chain, tripping React's duplicate-key
error (chain-table.tsx:372).
Root cause: graph-view passed snapshot.nodes — the derived canvas view
(propositions + implication nodes) — into ChainTableOverlay, whose
chainsForSelection turns every unchained node into a singleton chain.
An implication node is never a chain member (members are proposition
ids), so every imp node minted a bogus singleton with a colliding id;
its row was even selectable, leaking arg_ ids into practice input.
Fix (owner-approved shape): GraphSnapshot gains `propositions` — the
stored-truth node list riding beside `chains` for exactly this class of
consumer. The chain table now receives snapshot.propositions; the canvas
keeps snapshot.nodes (imp nodes are meant to be drawn). Practice pipeline
unaffected (it already read getGraph, which is propositions-only).
Test: the snapshot split pinned as an invariant in graph.test.ts — image
= truth + one imp node per fully-live stored chain; truth never carries
an arg_ id.
- architecture.md section 6 rewritten: stored truth = propositions + chains
(chains.jsonl); deriveView as the lossy image (implication nodes +
untyped arrows, rebuilt per read); the bijection framing (independent
arguments for one conclusion stay distinct — the corruption the edge
store invited); new deletion-semantics paragraph (proposition death
cascades to every referencing chain; arg_ death = one argument);
supports retirement stated plainly; auditor's propositions-only scope;
clusters stated as birth-session partition truth (no Louvain claim)
- section 7 practice chain model: stored-chains inclusion law replaces
follows-edge grouping; chainsToGraph bijection claim deleted
- README: graph clauses (stored chains, derived view, chain dedupe),
practice clause, ledger inventory, and two long-stale line-5 claims
(ledger events, semantic top-K) corrected
- contracts/graph.ts GraphSnapshot comment: Louvain claim replaced with
birth-session truth; extraction.ts comments no longer narrate the
dead expandFindings/chainsToEdges seam
Owner rulings 2026-09-30: reasoning chains are the stored truth; the graph
canvas renders a lossy derived image. No migration, no backcompat — old
graph data was wiped.
- contracts: GraphChain (arg_-minted id, premises, conclusion) replaces
typed edges; GraphEdge survives only as the untyped derived arrow;
GraphSnapshot carries stored chains; supports edges retired entirely
- store: chains.jsonl ledger (edges.jsonl purged at load alongside legacy
events.jsonl); readChains/appendChain seam; merge dedupes by
(sorted premises, conclusion), re-assertions append under the same arg_ id
- core: deriveView mints implication nodes (name ⟹, statement via
formatChainLine) plus premise→imp→conclusion arrows; clusters stay
proposition-only (birthSessionPartition); chainsForSelection now groups
STORED chains (chain rides iff every member selected) — independent
arguments for one conclusion never union; chainsToGraph/chainsToEdges die
- extraction: extractor submits chains natively (expandFindings deleted);
auditor receives propositions only; supports fan-out gone
- deletion: full death of a proposition kills every referencing chain;
arg_-prefixed node deletes kill that chain; chainsRemoved stats
- UI: every arrow renders the ⟹ double shaft; supports toggle + thickness
slider and detail-card edge sections deleted (card keeps name +
proposition); chain table + practice pipeline read stored chains
- tests: 1188 green across the new seams
- dialog: optional 'Anything else?' textarea (typing-only, 2000-char mirror)
- API: body gains note? — trimmed, whitespace-only omitted, hard 400 over 2000
- pipeline: note persisted on the set and threaded to both agent stages
- agents: THE LEARNER'S NOTE prompt block (present only when set) + the
weighed-never-owed stance pinned in both doctrines
- fix en route: doubled THE PARAMETERS heading in both prompts since slice 3
(renderParams owns the heading; the literal blocks removed)
Owner report: created a set, 'Interrupted (server restarted)' showed
on list + set views, generation proceeded and overwrote it. The
sweep's 'generating + no registry row' test had a false-positive
mode: it ran at EVERY pipeline instantiation, and an instance could
evaluate against a registry whose fresh row it couldn't observe.
Three guards, per ruling:
1. HEARTBEAT: ProblemSet.updatedAt? (contract + doc) — bumped on
every store write; createSet stamps it = createdAt; saveSet
re-stamps (validator tolerates absence for legacy rows).
2. ONCE PER PROCESS: the sweep guards on globalThis
.__lhPracticeSweepDone — no instance re-evaluates after the
process's first sweep.
3. GRACE WINDOW: only sets whose heartbeat (updatedAt ?? createdAt)
is older than 120s are stamped. A live run saves at every phase/
problem transition, so it is structurally unreachable; a true
orphan is minutes stale at next boot. One [practice <id>] log
line per stamped set (the extraction/jobs console.error idiom).
Tests: stale orphan stamped; FRESH generating set untouched (grace);
once-guard (second factory instantiation never re-sweeps); live-row
protection re-armed via resetSweep() test seam; store pins for
updatedAt birth-stamp + re-stamp + legacy tolerance. Architecture.md
sweep sentence updated (once-per-process + grace semantics).
1142/1142, tsc 0, lint 0, build 0.
Contracts: LadderRung 0-4, CueDistance 0-2, Presentation theory|
application; PracticeParams carries the three named levels. Doc
comments hold the three definition tables verbatim (rung names +
definitions from the ladder doctrine; cue levels; presentation
levels).
Core: LADDER_RUNGS/CUE_LEVELS/PRESENTATIONS tables in src/core/
practice.ts (definitions verbatim) — ONE source spoken by the agents'
prompts, the dialog's stepped controls, and the exam header; core
barrel re-exports practice.ts (pure domain, browser-safe).
Agents: Part I's ladder intro now 'five rungs, numbered; the request
arrives as a rung' with bullets numbered 0-4 (definitions untouched);
Part III's parameters block replaced with the named-level doctrine
(rung = exact class, no continuum; cue counts lesson examples AND
previous problems' situations; presentation dresses, never changes);
partitioner's RESPECT THE PARAMETERS replaced (low rungs = many small
assignments, high = larger; far cue makes confusables-together MORE
important; presentation does not shape the partition). renderParams
outputs the exact named-level block. Anchor/0..1 language swept dead.
Service + store: strict union validation — floats between levels,
out-of-range, wrong strings, legacy dial shapes all 400 at the API
and fail isProblemSet (legacy sets skipped from lists; no migration —
data/practice is disposable scratch, owner wipes before testing).
UI: dialog = 5-detent ladder slider + 3-detent cue slider (each shows
the level NAME + one-line definition) + Theory|Application segmented
toggle; defaults rung 2 / cue 1 / theory; Create posts the new shape.
Exam header gains the levels line ('Direct exercise · Adjacent
context · Theory').
Tests: renderParams exact-block pin; strict-validation pins (new
tests/practice-service.test.ts — bad unions 400, legacy shape 400);
store pins (legacy floats + 2.5 rung fail); render pins (detents +
names + definitions, defaults, detent-swap, toggle active segment,
levels line); every params fixture moved off floats. 1138/1138, tsc
0, lint 0, build 0.
Docs: architecture.md — discretization paragraph (compact tables,
named-level invocation, defaults, one-table source), UI mentions;
README axes sentence.
Owner escalation on deviation 1: practice availability must not
inherit a visual-rendering threshold. graphClusters() now runs
birthSessionPartition(snap.nodes) directly (same size-desc/id-asc
ordering) instead of reading the snapshot's MIN_CLUSTER_SIZE-filtered
clusters array — MIN_CLUSTER_SIZE stays a hull-drawing rule only.
Label join mirrors the snapshot's (chat title / notes path, null when
the naming record is gone). Contract doc + architecture.md sentence
corrected to match; empty-picker copy reworded ('once a session births
its first proposition' — no 3+ mechanics). Render pins: new empty line
+ a 1- and 2-proposition group listing pin. 1130/1130, tsc 0, lint 0,
build 0.
Owner ruling: 'problem sets always take in nodes — graph objects' —
practice is graph-domain, not chat-domain. No session-created sets.
API: POST /api/practice/sets takes {nodeIds, params, title?} ONLY —
the chatId branch, UnknownChatError, and the any-provenance filter are
deleted; the selector is nodeIds (readNodes filtered to the selection,
unknown ids drop, empty live intersection → 400 empty_selection);
title optional from the UI (the picker's cluster label), dated default
stays. chatStore leaves the pipeline deps entirely (labels resolve
store-side in the clusters service).
GET /api/graph/clusters (new): the birth-session groups the graph view
renders — graphSnapshot's clusters verbatim (same ordering, same
MIN_CLUSTER_SIZE presentation filter), mapped to {key, kind
chat|notes, label (null when the naming record is gone — honest,
never guessed), nodeIds, nodeCount}; corrupt/empty graph → empty
list. GraphClusterGroup contract in core/contracts/graph.ts (client-
safe — the dialog imports it).
practice-dialog: session picker → cluster picker (rows show label +
proposition count, notes clusters marked); Create posts the cluster's
nodeIds + label as title + params. 400 empty-selection copy reworded
for the vanished-nodes edge ('Nothing practicable is left in that
cluster — the graph changed.'); 503 deep link unchanged. Preselected
mode gone — the dialog is the only creation entry for now (the graph-
view cluster entry comes later with free-form selection).
Sidebar: session right-click 'Practice this session's propositions'
removed (item + state + dialog wiring); the /practice nav row stays.
Tests: pipeline — chatId/any-provenance tests die, nodeIds selection
pins added (unknown-id filtering, dated-default title, title hint
rides, empty/vanished → EmptySelectionError); render — cluster picker
pins (label+count rows, notes kind, unlabeled cluster muted, empty
graph line), preselected pins removed; 16 render + 13 pipeline.
Docs: architecture.md §7 API+UI paragraph rewritten (graph-domain
ruling, /api/graph/clusters, cluster picker as the only entry, no
404-unknown-chat); README creation-flow line updated.
1130/1130, tsc 0, lint 0, build 0.
The persisted (chatId, sourceId)→node table was a shared namespace across
independent chunk/batch jobs: each chunk's extractor mints local ids fresh
(p1..pN), so later chunks collided with earlier ones — propositions silently
skipped as 'already resolved' (coverage checkpointed anyway: permanent silent
loss) and edges resolved onto the wrong nodes (the 'everything links to
everything' mush). Owner ruling 2026-09-29: mergeFindings resolves THIS job's
ids in memory, nothing persists between jobs; cross-job identity is the
auditor's linkTo verdicts alone.
- contracts: NodeMapping dies; NotesSource.contentFp — sha256 of the text at
the last run, the re-import guard (unchanged re-import = TRUE no-op, no
model calls, no usage; changed text re-runs; legacy unstamped sources
re-run once)
- merger: job-local resolution map, in-submit duplicate ids skip once,
edges resolve extractor-local ids only
- ports/store: appendMapping/readMappings, LEDGER_FILES.mappings,
rewriteLedgers.mappings gone (stale data/graph/mappings.jsonl just stops
being read)
- session-clusters: birthSessionOf is provenance-only — the legacy
mapping-line fallback for provenance-empty notes nodes dies
- deletion: no mapping rows to strip; stats lose mappingsRemoved
- replay-layout batches nodes by first provenance point
- graph.test pins the new independence semantics (re-merge re-creates;
duplicate ids within one submit resolve once); extraction tests pin
per-chunk merges + the fingerprint guard
Hypothesis under test by the owner: coherent extractions turn to mush
after merging because the auditor minted too many bridging edges of its
own — inference, not identity, and out of the auditor's scope. The graph
now connects across sources solely through identified equivalences;
relations between propositions come from the extractor alone.
- auditSchema drops 'edges' (submit_review = links only); the submit-seam
required-field doctrine reworded per tool
- AUDITOR_INSTRUCTIONS: the BRIDGING EDGES section dies; deliverable is
the identity verdicts only
- AuditVerdicts drops edges; ExtractedEdge endpoints are extractor-local
ids only; mergeFindings stops consuming verdict edges and its resolve()
drops the raw-node-id branch (dead — the extractor never sees graph ids)
- extractor untouched: submit_findings keeps propositions + follows/
supports edges, required as before
- tests: 17 submit fixtures + 9 typed verdict literals slimmed to links
- docs: architecture.md division-of-trust + pipeline diagram updated
Owner ruling 2026-09-28: the auditor has no control over the
extractor's output and never alters it, so re-emitting the full
findings per submit was pure payload cost — the retry collapse the
schema slim attacked, halved again. submit_review is now links
(local proposition id -> existing graph node; EQUIVALENCE BIAS
carried in the linkTo description) + bridging edges ONLY; the
merger joins extractor findings + verdicts.
mergeFindings(chatId, findings, verdicts, ...) — propositions come
from findings verbatim, links join by id, edges emit extractor's
then auditor's through one dedup-preserving emitter. The old echo
form hid an accidental drop channel: an auditor that omitted a
proposition from its echo silently dropped it from the merge.
Verdicts-only removes it structurally — extractor output always
fully merges (tests updated to full-merge expectations).
AuditResult/AuditedProposition -> IdentityLink/AuditVerdicts;
audit() logs links/bridges; batch view renders anchorless like
transcriptView; auditor prompt rewritten to the verdicts-only
deliverable; architecture.md division-of-trust updated.
OpenRouter log diagnosis (owner, 2026-09-28): extraction time was
dominated by tool-validation retries. The failing call had the whole
propositions array emitted as ONE JSON-STRING ('propositions.0: must
be object'), a truncated placeholder artifact inside it, and
word-salad statements late in the array — payload-size collapse.
Field audit: 'evidence' was never persisted anywhere and its
faithfulness-checking consumer died with the fully-trusted ruling;
'messages' fed messageId granularity that NOTHING downstream reads,
and the model sent [] anyway (chat-level provenance in practice).
'aliases' stays (optional now): the auditor's find tool globs them.
Schema: propositions = {id, name, statement, aliases?}; edges =
{from, to, type}. Prompts lose the evidence/messages doctrine and
[id]-anchor guidance, gain a compact faithfulness line;
transcriptView renders anchorless. The auditor echo instruction
drops the evidence/messages mention, so its payload halves too.
Heal: healStringifiedArrays on the submit tool's prepareArguments
(pi-agent-core's sanctioned pre-TypeBox shim) parses a stringified
propositions/edges back into arrays — the LaTeX-restore doctrine:
heal weak-model wire behavior at the seam. Test drives the real
agent-loop seam with a stringified submit.
The graph becomes raw reasoning chains only. Quiz outcomes and
understanding observations are no longer formalized; assessment
moves to the future practice module.
Events ledger removed end-to-end: LedgerEvent/LedgerEventKind/
UnderstandingLevel/ExtractionAttribution/AuditResult.dropped and
snapshot nodeObservations deleted from contracts; quizEvent builder
and the events step of mergeFindings gone; appendEvent/readEvents
dropped from the port and store; LEDGER_FILES loses events.jsonl
with a one-time purge of a stale file next to the nodes ledger;
graph-deletion and the delete dialog lose event purge + quiz-answer
impact counts; GraphSessionProposition becomes a pure birth list.
Whole-session markdown source view: new core/session-markdown.ts is
the shared formatting engine (id-anchored speaker blocks, quiz/
recall/question blocks, tool one-liners) — the extractor's prompt
view and the future user-facing export render from one engine.
Coverage fingerprints deliberately stay on the old line format
(content-hash semantics; re-formatting never re-opens covered
history). The whole session goes to the extractor — no phase cut.
Auditor refocused as the identity linker: never sees the source;
explores the graph via new auditor-specific tools in
agent/tools/graph.ts (find — glob over name/statement/aliases;
list_sessions; list_propositions with full statement text; no
timestamps) instead of an injected full-graph dump; equivalence
bias — paraphrases merge, only genuinely distinct propositions stay
apart; no drop channel, the extractor is fully trusted; may draw
bridging edges (typed follows/supports) between existing nodes and
the batch, resolved by the merger with chat-scoped message-less
provenance. Agentic loop, maxTurns 16.
UI: the node detail card gains an edges section above the sessions
list (follows-from / supported-by / what-follows / what-it-supports,
rendered only when present, click-to-select neighbors); the
understanding-note chips are gone.
Prober list_propositions reworded to what a session covered —
names only, no levels. docs/architecture.md + README updated to
the as-built pipeline. Gates: typecheck, 1059/1059, lint, build.
Sycophancy mitigation as a structural commitment: the model writes its
full correctAnswer (plus explanation, in that order) BEFORE the learner
sees the question; the commitment rides the persisted call args, is
revealed on submit, and grades his free text in the model's next reply —
the client never grades free recall.
- tools.explain setting (segmented 'Quizzing — Multiple choice | Free
recall' in Agent settings' Tools group): free arms explain and drops
quiz for BOTH agents (prober variant carries no nodeId, same pattern
as the probe quiz); mechanics keys never pass filterEnabledTools —
quiz stays unwritable (the 2026-09-19 wedge ruling stands)
- doctrine lives pi-natively in the tool descriptions (owner ruling —
no conditional system-prompt chunks): explain carries the commitment
rule; the MC option-building craft moved out of the prompt into the
quiz description; shared/20-question-tools.md slims to the
quiz-agnostic core
- display seams: parseExplainInput (LaTeX-debris restoration included),
dock text-mode with an 'I don't know' row (kbd row 0), AnsweredCard
reveal (The answer / Why, no verdict chips), [Explain answer] format
- phases/plan seams recognize both quizzing tools' nodeId tags; explain
answers deliberately feed no ledger evidence (no client grading to
key on — a future ruling can parse the model's grade)
1049/1049 tests, typecheck/lint/build green.
The owner's capture-vs-composition check confirmed the wire sends exactly
what composes (byte-identical save the nodeId paragraph ebfe058 had since
changed). This wave removes the implementation meta that leaked into the
prompts and reorders so the method carries weight:
- base: owner's new opener verbatim (install understanding; the
understanding definition — which absorbs the old 'Understanding is
structure' paragraph); Voice moved in from shared/10 (deleted); the
philosophy keeps edge-dependence + logistics ('standard learning'
dropped per owner); intro's two-agents/fragments sentence gone.
- teacher/05-method opens with the owner's methodology bridge ('No other
teaching methods come close') — paragraph 3 of the opener, placed here
so the shared base never points the prober at methodology it doesn't
carry.
- deleted: teacher/60-probing-in-brief (self-evident from shared history;
its mid-lesson mini-probe move salvaged into 20-teaching as one
sentence), the 'Session shape — the durable artifact' section
(extraction-pipeline meta), the fragments-meta lines in shared/20 and
05-method, 'the transcript's seam' phrasing, prober's 'the teacher
that takes over' / 'the handoff is automatic'.
- composition reorder (listSourceSegments): base -> the agent's own
fragments -> shared/ appendix. Teacher method now starts at 12% in
(was 62%); prober doctrine at 34%. shared/20 opens with the owner's
decision rule (quiz only for known-correct-answer, ask for everything
else).
- HANDOFF_TEXT: the owner's user-message version — first-person, paces
the planning (unconditional truths, motivated derivations), ends at
plan_init + accept-or-changes.
Teacher 31.4k -> 28.7k chars; prober 10.0k -> 9.6k. Gates: typecheck 0,
1018/1018, lint clean, build green. (Suite runs 16s; the earlier 10-min
timeout was stale node_modules after master's df1328e 0.87.1 sync —
npm install fixed it.)
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.
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).
The chat record is now pi-native agent history (LhMessage[]), the AI SDK's
UIMessage transcript contract is gone from the server, and every client turn
arrives as ONE explicit input event.
Core (new):
- src/core/native-messages.ts — the LhMessage taxonomy (user/assistant/
toolResult + lhMarker customs: verdict, probe-handoff, subagent-steer,
subagent-abort, plan-block), constructors, and toLlmMessages — the single
model-facing seam (convertToLlm): strips ids, flattens markers to user
text, drops UI-only plan-blocks, synthesizes INTERRUPTED results for
dangling aborted calls, never throws.
- src/core/projection.ts — projectNative: the one-way native → UIMessage
projection (consecutive assistant+toolResult runs group into one
assistant view message; toolResults attach backward by toolCallId;
markers project to data parts; plan-blocks splice after the last
successful plan_init). Every existing consumer reads through it.
- src/core/input-events.ts — the ChatInput union (text/answer/verdict/
continue), parseChatInput validation, and inputAppends (answer targets
the dangling question call or 400s; verdict = optional note user
message + native verdict marker).
Server:
- chat-handler.ts — the turn root takes {chatId, input}; write-ahead
persists input appends + claimed steers before the turn; claimSteers
coalesces the whole queue with the handler's steerDelivered filter as
the dedupe seam; teacher-vs-prober branch on hasProbeDone.
- turn-service.ts — persistTurn stamps ids, drops clean question
placeholder toolResults (the record ends at the unanswered call),
emits plan-block markers idempotently (plan-blocks.ts rewrite), and
persists abort partials.
- ui-stream.ts — PiTurnSpec takes the native base; the wire dialect is
unchanged. jobs.ts steers are native markers (steer_<jobId> dedup ids);
waitForIdle reads danglingQuestionOf. teacher-agent.ts wires
convertToLlm: toLlmMessages. record-adapter.ts DELETED wholesale.
- GET /api/chats/[id] serves messages: projectNative(record.messages).
Client:
- chat-transport.ts — the 409 retry is IDENTITY (re-POST the same event
body after the turn settles — the event was never persisted
pre-acceptance; the merge logic is gone). The transport consumes the
stashed input event via prepareSendMessagesRequest and throws loudly on
a missing stash.
- chat.tsx — every send call site stashes exactly one ChatInput event
(text, answer, verdict, continue) before invoking the SDK send.
Tests: full native migration (chat-flow rewritten to input events and
native record pins; jobs/extraction/demos/graph/usage/plan-blocks/
probe-done/reattach-transport converted; record-adapter.test.ts deleted;
chat-transport tests pin the identity retry). 982/982, typecheck, lint,
build all green.
- src/agent/lh-tool.ts deleted: the zod-wrapped LhTool + toPiTool bridge
(z.toJSONSchema per call, clientQuestion flag, hand-rolled result mapping)
is gone. Tools are pi-agent-core native AgentTools now.
- New src/agent/tool-execute.ts owns what the wrapper added: the abort guard
(runWithAbortGuard: run signal + 30s per-call timeout + 250ms grace,
'Operation aborted' / timeout wording preserved), ToolInputError (the
retryable '- problem at path' format), result mapping (string verbatim /
object JSON / image parts, details rides the record's tool part output),
and questionResult() — placeholder + terminate:true (owner Option A:
belt of terminate + slim makeQuestionStopper with the clean-question
rule; maxTurns deleted from the teaching turn entirely per owner).
- All 11 tool files + tools.ts registries converted to TypeBox parameters
(typebox@1.3.7, pinned to pi-agent-core's copy). zod refinements
(quizInputProblems, askOptionProblems, live-nodeId) moved from schemas
into execute — the model sees the same retryable error shape.
- question-core + contracts/probe TypeBox-authored (single source shared
with the UI); tool-parts parse seam runs the boundary's full chain
(Convert → Check → Clean → refinement), fixing the live-smoke lockout:
pi's boundary Value.Converts correctAnswer: 17 → '17' but the record
keeps raw args, so a parse without Convert rejected a posed quiz
(invisible pending question, turn ended). Pinned with the real smoke
emission shape.
- Subagents converted: demo-builder/researcher AgentTool-native,
visual-maker/pdf-reader declaration-only pi-ai Tools with local execute
fns, extraction submit schemas re-authored in TypeBox (owner option 2:
zod defaults → Type.Optional + fill at the submit seam).
- Accepted delta, flagged: TypeBox-native schemas skip pi's JSON-Schema
primitive coercion fast path... (boundary still Value.Converts), and
prober-quiz unknown keys ride through (Value.Clean strips at display).
- Tests: 988 passing (was 967 at the recon baseline); tool-abort and
read-tool rewritten against the native seams; question-core/question-
tools/tool-parts/demos/web-search/graph-tools/plan-tree/probe-done/
tool-toggles converted to Value.Check/execute pins.
Live smoke (PORT=3004, real model): native ls turn streams end-to-end; a
real quiz pose (first attempt missing correctAnswer → tool-input-error →
model retry → posed) terminates the turn cleanly and persists pending.
The probe→teach handoff wake read as a dispatch order ("Author the session's
plan map now") and the teacher skipped the deliberation the planning fragment
asks for. The wake now paces it: take your time, read the assessment and the
probing transcript, reason out the optimal way to teach this person this
thing — floors, ceilings, misconceptions, the minimal tree so every step
lands as obviously true — and only then call plan_init. Machinery honest:
plan_init is still the first tool, the plan map still the first artifact, and
the tail now says to wait for his verdict (which is what presenting actually
does — the old "then begin teaching" tail skipped the verdict seam).
Off UI's d651f93 flag (displayCostTotals already isolates the display to
extraction + audit; the naming stage row and naming runs were filtered
out client-side). This culls the server side of the same ruling.
THE STANDING RULING, REVERSED BY ITS OWN PREMISE: NamingRunUsage /
readNamingRuns / appendNamingUsage were kept at the wave-7 namer kill
ONLY because cost-summary's HISTORICAL naming stage read them. With the
fold culled, that premise dies — so the whole ledger read path goes too,
following the wave-7 embeddings precedent chain: consumer-less ledger
seams are deleted, and the on-disk naming-runs.jsonl stays INERT (real
trees keep their historical spend bytes; they are simply not displayed
anywhere — the same treatment as historical stages.embedding spend).
- core/cost-summary.ts: two channels in (chat + notes front doors), two
stages out; RecentRun.kind collapses to "extraction" (labels field
gone); CostSummaryInput.naming gone; totalDollars = extraction + audit.
- ports/store/contracts/config: appendNamingUsage + readNamingRuns ports,
store impls + guards, the NamingRunUsage interface, and
LEDGER_FILES.namingRuns die. graphRevision no longer counts the file
(a smaller revision value is harmless — reads never compare).
- components/graph-view.tsx (mechanical, flagged): the defensive
.filter(run.kind !== "naming") guards a kind that no longer exists.
- Tests: cost-summary's both-channels fold pins; the naming fixtures and
the appendNamingUsage store pin die with the seams.
Owner ruling (2026-09-23, amendment to 04e336d): no hardcoded fallback
exists anywhere. contextWindow(record, limits) resolves the denominator
ONLY from the live index (provider:id -> contextWindow straight from
pi's catalog); a model missing from it (historical session, package
update, failed fetch) reads limit null and the meter renders
usage-only — the honest-render precedent, never a fake window.
- src/core/prices.ts: the 1354-entry CONTEXT_LIMITS table (65KB) and its
generation apparatus deleted; the fold returns limit: number | null.
PRICES untouched (regen verified byte-identical via the reworked
sync-pricing script).
- scripts/sync-pricing.ts: generates PRICES only; contextWindow leaves
the priced map, the guard, and the template.
- The dock meter's owner-selected treatment: the activity feed carries
the limit server-side. catalogContextLimits() (server/model-catalog.ts)
builds the index over the same keyed providers the picker federates;
activityFeed() joins it onto every running job (ActivityJob.contextLimit,
null when the model is missing — the dock reads it off the feed, no
client-side table, no regression while the UI's catalog-join chunk lands).
- components/activity-dock.tsx: toBlobs reads job.contextLimit (import
of CONTEXT_LIMITS gone); stats-bar renders usage-only on null limit
(mechanical interim — UI stream's join chunk follows).
- tests: usage.test.ts pins the no-fallback contract (index hit / absent
/ empty / missing-model -> null); activity-feed.test.ts pins the
server join (catalog model -> 1048576, unknown -> null, no-usage ->
null); render pins updated to the interim usage-only render.
Part 1 — the ledger + remnants interlude (master-dispatched, owner ruling
2026-09-23: nothing configures the graph beyond chat models):
- The embedding ledger surface is gone: readEmbeddings/writeEmbeddings
ports, the EmbeddingsLedger contract, EMBEDDINGS_FILE (config, store
impls, graphRevision source list), and graph-deletion's vector-pruning
step. On-disk data files stay untouched (inert history).
- The extraction embedder seam dies wholesale: ExtractionPipelineDeps
drops the embedder dep (chat-handler + backfill/import-notes stubs go
with it), the 'embedding' phase leaves ExtractionPhase, the usage
capture/persist helpers die, and stages.embedding leaves
ExtractionRunUsage (historical records keep their shape — reads are
lenient). Extraction is two stages again, like the local-embedder era.
- Consumer-less remnants: latestQuizOutcomes + QUIZ_EVENT_KINDS,
clusterContentId, matchClusterLabelings + Community + ClusterLabeling,
the readClusterLabels/appendClusterLabel port seams + store impls +
LEDGER_FILES.clusterLabels. NamingRunUsage/readNamingRuns STAY
(standing ruling — the cost pill renders historical naming spend);
clusterHue + MIN_CLUSTER_SIZE stay (live).
- The 'labeler' role dies: BACKGROUND_MODEL_ROLES, ROLE_SLOT, ModelRole,
the catalog pin projections, and the UI roster strings. RolePins keys
shrink with the union.
The notes birth fallback (owner-approved 2026-09-23, surfaced by the
interlude's live smoke: production notes imports create provenance-EMPTY
nodes (importNotes passes messages: null — the file's anchor is the
mapping line), so the provenance-only birth rule left every imported
file unclustered and invisible to the prober's session list, against the
locked file-birth ruling. A provenance-empty node now births under its
FIRST mapping line's chatId (ledger order); only notes-born nodes ever
hit the fallback (chat nodes always carry provenance; a re-encountered
node's link-append re-opens the provenance rule). Deletion-safe: mapping
lines of deleted chats are stripped alongside their provenance.
Live-verified on port 3004: a fresh notes import clusters under its
src_ id with the file path as the label, end to end with no embedder.
845/845 tests, typecheck/lint/build green.
EOF
)
The owner's zero-configuration ruling (2026-09-23): nothing configures
the graph beyond chat models. The prior-knowledge injection — its budget
dial, its runner, its marker part, its dock/dispatch entries — is
removed across all bands; the prober now reads the learner's persistent
knowledge map on demand through two gated tools instead:
- list_sessions: every birth session, names + kinds + counts + dates
(graphSessionIndex, Platform's core seam)
- list_propositions(session): what was born there + the session's own
latest understanding level + that evaluation's date
(graphSessionPropositions) — names/levels/dating ONLY, no statement
text (owner ruling: the map's organization is the prober's business,
its prose is not)
Registered via createProberTools when chat-handler passes graphStore
(the graphToolsEnabled gate decided server-side, per-turn fresh); the
prober doctrine gains the map-review nudge at session start.
Also: runner's JobKind special-casing collapses (KIND_ROLE/KIND_RUNNERS
are plain records again), semantic-clusters.ts dies with its test, the
embedder dep's doc tells the wave-7 stub story, and ports.ts's
readClusterLabels/readNamingRuns surface stays untouched for Platform's
ledger-port interlude.
848/848 tests, typecheck/lint/build green.