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.
pi-ai 1.0's onProviderStreamEvent, at the layer our incident class lives
(GLM arg truncation, DeepSeek id resets) — env-gated (owner choice:
no app surface, off by default; raw events carry full prompts), one
JSONL per chat under data/debug/provider-events/ with a 20MB cap and a
run-start marker for record correlation.
Wired at every agent construction: teacher/prober turns (AgentBuildOptions.
chatId), jobs via LoopContext (researcher, demo-builder through
runSubagent; visual-maker + pdf-reader through their streamOnce), and
the extraction pipeline (extractor + auditor keyed by source id).
practice deliberately unwired (sunk-cost module).
Authoring refinements (quizInputProblems, nodeId checks, ask/explain
refinements, submit-agent pipeline laws) now return invalidInputResult —
same model-facing failure and retry semantics pi minted from the throw,
but details keeps {tool, problems} as data instead of the throw path's
empty {}. ToolInputError deleted; wire shape unchanged (verified against
pi's own thrown-error conversion in agent-loop).
The teaching cycle gains its motivation question. The teach quiz schema
now carries a REQUIRED type field — predict (posed before the teaching,
arrives open, the learner predicts the next step) or check (posed after,
arrives collapsed behind the reveal). The prober's quiz stays role-less.
- core: quizInputObject.type union + normalizeQuizType + the reveal law
as pure quizNeedsReveal; QuestionData.questionType; note → thinking
rename (legacy reads fold back); readingThinking channel on answers;
compactAnswerForModel emits thinking / thinkingWhileReading
- tools: descriptions stripped to mechanics — QUIZ_OPTION_CRAFT and
EXPLAIN_DOCTRINE deleted, doctrine moved to the prompt layer
(reverses the 2026-09-27 placement, owner 2026-10-03); shared
quizDescription/explainDescription builders differ only in deltas
- prompts: shared/20-question-tools.md rewritten (option craft,
transcribed-speech leniency, the two thinking channels); new
teacher/28-predictions.md (cycle, roles, skip rule, resolution law);
20-teaching + 30-question-principles scoped to the new taxonomy
- UI: predictions render open with a Prediction eyebrow, verdict shows
but the explanation tucks behind a toggle; the reading-thinking field
mounts while a check waits (dictation claims it until reveal);
answered card separates thinking vs thinking-while-reading blocks
- requiresReveal gates on pending.tool === 'quiz' (owner: only graded quizzes
arrive collapsed; ask/explain always open)
- ESC-abort chain opens AbortTurnDialog everywhere (owner ruling); from a
pending question abort IS dismissal — dismissedAnswer rides the answer
path, the teacher acknowledges the skip in its next turn; popup-Esc is
claimed via the state gate (two window listeners fire per Esc)
- 25-confidence.md: stripped the slider/JSON/calibration mechanics — pure
reading doctrine remains
- toolQuestionKind named once in core (dock parity), dock switched to it
Owner design round 2026-10-01: every multiple-choice quiz answer now
carries the learner's stated probability that his selected answer is
correct — calibration data for the prober and teacher, the flow itself
unchanged.
Definition (owner ruling, after weighing chance-normalization): RAW
probability. Sub-chance reports are diagnostic gold, not dead space —
'picked an option, believes it wrong' is a real state. The agents get
the sophistication: new shared/25-confidence.md composes for both
agents (chance level, sub-chance = distrusts the pick, correct+low =
fragile, wrong+high = committed misconception, IDK = genuine gap).
- question-core: AnswerRequest/QuestionAnswerData.confidence (quiz
only, optional — old records valid), buildAnswer clamps and drops it
on dontKnow, formatAnswerMessage states the line, CONFIDENCE_STOPS +
DEFAULT_CONFIDENCE_INDEX exported.
- question-dock: snapped slider below 'I don't know', above 'Your
thinking' — native range over the stop INDEX so ←/→ step the uneven
grid cleanly, Tab chain rows → slider → thinking, Enter submits from
the slider too. Rests at 50 and always sends (owner: 50 must mean a
real coin flip, never 'untouched'). 'I don't know' GRAYS it out —
layout holds, no value sent.
Deliberately untouched: explain free-recall card (quiz-only scope),
answered-card rendering of confidence, ask flow.
The partitioner was told to reference chain ids, but no rendered line
ever showed one — chainLine emitted bare 'premises → conclusion', so
the model re-typed whole rendered lines into submit_partition and every
call failed 'unknown chain ids' validation (owner live report,
2026-09-30 evening). The generator's YOUR TARGET block had the same
blindness.
- chainLine now leads with the chain's id: [arg_x] n_a, n_b → n_c;
a bare proposition repeats its own id ([n_y] n_y (no premises)) —
mechanically teaching that its chain id IS its proposition id.
- PARTITIONER_INSTRUCTIONS rewritten to chains-primary vocabulary
(was still 'follows-edges, bijective re-encoding' — pre-chains
framing) with the id-only doctrine and singular propositions
first-class.
- partitionSchema/problemSchema gained field descriptions: ids
VERBATIM, never re-type, never invent.
- Generator OUTPUT bullet + target-block header say the same.
Design ruling en route: partitioner stays an LLM partitioning chains
(deterministic partitioning and propositions-as-targets both
considered and declined by the owner).
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.
Data analysis of the five-source corpus (490 nodes, 244 chains) found
three pathologies the goal-first core let through:
1. Narrative-adjacency chains — premises that occasioned rather than
entailed conclusions (concentrated in the polemical sources).
2. Session-scaffolding minted as knowledge — the source's own axiom
numbering (A1/F2/UT2), arc labels ('the crux') persisted as nodes.
3. Full-discourse volume — glossary and argument-arc material at equal
rank with load-bearing ideas (~200 nodes per source).
Three doctrine lines, same terse register as the core:
- entailment is the only link that earns a chain; historical/narrative
connections keep their pieces as bare propositions or drop them
- load-bearing ideas only (what a reference on the subject would teach)
- propositions must stand alone for a reader who never saw the source;
no leaning on the source's own scaffolding
Substring pins for all three; shared by session + notes extractors by
construction.
- 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)
Rulings: each problem generation is its own visible dock job (the
extraction per-chunk pattern), and practice shares extraction's
semaphore — one process-wide 3-permit pool for ALL background LLM
jobs; total background LLM concurrency bounded at 3.
A. SHARED POOL: new src/agent/subagents/bg-llm-semaphore.ts —
globalThis __lhLlmSlots, PERMIT_COUNT 3 (the ruling), the
fresh-take/handed-off-permit deadlock discipline preserved for
BOTH callers. extraction.ts's withExtractSlot is now a thin
wrapper (job-row signal → withBgLlmSlot); __lhExtractSlots and
__lhPracticeSlots both die. The partitioner now acquires a permit
too (it's a background LLM job — it didn't before).
B. REGISTRY ROWS: the aggregate setId:generation / setId:grading rows
are replaced by setId:partition (phase partitioning, label
'Partitioning — title'), setId:gen:problemId (queued→generating,
'Problem i/N — title'), setId:grade:problemId (queued→grading,
'Grading i/N — title'). Each row carries its OWN abort controller
and its OWN usage (one PracticeStepLog per row — the aggregate
partitioner/generator/grader folding dies). Row key = feed id =
abort key (the dock dup-key law).
C. ABORT SEMANTICS: abortPracticeRow(key) routes by shape — gen row:
THAT problem only (error 'Aborted', siblings continue, set settles
ready with one errored problem); partition row: the whole run;
grade row: that grade drops (answer kept, re-submittable).
abortPractice(setId) composes from row aborts + the synchronous
persisted marks (a set-abort's verdict stands before the settles
re-read it — the settle step keeps an existing error status).
/api/activity/abort kind practice now takes the ROW key (charset
admits ':').
D. FEED + DOCK: ActivityPractice/FeedPractice carry per-row identity
(id = row key, problemIndex/problemTotal); dock blobs render per
row, labels carry the i/N; the aggregate row is gone.
E. Boot sweep unchanged at set grain; the live-row check is now a
key-prefix scan (any live row of the set protects it). The
updatedAt heartbeat covers the new row scheme as-is.
Tests (20 in the pipeline suite): per-problem row shape/uniqueness/
i-N labels (partition row frozen behind a gated partitioner; per-row
usage ISOLATION pin — b's log never shows on c's row); abort-one-gen
(sibling completes, set ready with one errored problem); partition-
row abort; queued grade-row abort (dead-ticket path: answer kept,
re-submittable, sibling lands); shared-pool identity + contention
(practice queues behind extraction-style holders); registry/dock
feed mapping. Test isolation hardened: unique per-store set ids,
afterEach registry/lock/pool reset (a timed-out test can no longer
poison the rest).
architecture.md: jobs-pattern paragraph rewritten (per-problem rows,
shared pool, abort routing).
1149/1149, tsc 0, lint 0, build 0.
Prior state: zero formatting guidance anywhere in the agent module —
models wrote prose-only by default.
GENERATOR_DOCTRINE Part III gains the FORMATTING bullet (after the
THE PARAMETERS bullet, before PREVIOUS PROBLEMS): statement, solution,
and rubric render as full Markdown — math always LaTeX (inline $…$,
display $$…$$), code/pseudocode/formal systems in fenced blocks with
a language tag, identifiers inline as `code`, tables for parallel
structure, lists for enumerations; prose-only renderings of
mathematical or structured content are a defect.
GRADER_INSTRUCTIONS gains the feedback-formatting paragraph (before
the OUTPUT section): full Markdown, LaTeX math, fenced code, lists/
tables where they carry structure — teach the boundary in properly
formatted prose, not plain text.
PARTITIONER gets nothing (machine-shaped output).
Tests: substring pins on the exported doctrine constants — the exact
FORMATTING sentence + ordering (after presentation, before previous
problems), the exact grader paragraph + before-OUTPUT ordering.
Architecture.md §7 one clause (the formatting doctrine in the
generator's prompt inventory).
1144/1144, tsc 0, lint 0, build 0.
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 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.
src/agent/subagents/practice.ts: createPracticeAgents(deps) with the
ExtractionModels mirror (per-stage engine overrides + engineFor +
providerKeyMissing + refuseIfStagesUnready, all three roles strong-tier).
Generator doctrine = the owner's problem-engineering framework verbatim
(minus Sources) + Part III wiring section per the green-lit amendments
(FULL SET input with minimal-use doctrine; degradation never in
meta-commentary). Partitioner: true-partition doctrine, validate via
chainCoverage + exactly-once + unknown-ids, error-back retries through
runSubmitAgent's new validate hook (ToolInputError → error tool result).
Generator: echo law (chains set === target). Grader: verdict union +
nonempty feedback (schema-enforced). renderChains/renderPropositions/
renderParams/renderPreviousProblems. BACKGROUND_MODEL_ROLES extended with
the practice trio (settings override symmetry). runSubmitAgent gains the
validate seam (null = accept). 16-test faux-engine suite incl. retry
paths, refusal gates, neverSubmitted.
runSubmitAgent moved verbatim from extraction.ts to src/agent/subagents/
submit-agent.ts (exported; extraction imports it — zero behavior change,
extraction suites green untouched). New optional prepareArguments seam:
extraction wires its healStringifiedArrays heal; default is identity
pass-through. ModelRole += partitioner | problemGenerator | grader;
ROLE_SLOT pins all three to strong (quality-critical trio, plan default).
Note: BACKGROUND_MODEL_ROLES (per-role settings overrides) deliberately
NOT extended — settings-surface decision, flagged for the planning
session.
Observed live: the auditor looped find for a very long time, never touching
list_sessions/list_propositions. Root cause was the prompt: its only
procedure text was 'For each proposition, search the graph (its key words,
its synonyms, its phrasings)' — a per-proposition find loop with no
starting point, no sense of the space's extent, and no termination rule.
And find returned id+name+statement with no session pointer, so results
never composed with the listing tools (owner diagnosis, 2026-09-29).
- AUDITOR_INSTRUCTIONS now prescribes the process: (1) list_sessions ONCE —
the graph is small and finite, this shows its entire extent; (2) read
plausibly-related sessions with list_propositions (full text) — every
realistic candidate seen in a few calls; (3) find as the targeted sweep;
(4) SUBMIT once related sessions are read — never re-search seen ground
- find hits now carry 'born in: <session id>' — results point at sessions
the auditor can read whole (legacy no-provenance nodes name none)
- identity doctrine untouched: equivalence bias, linkTo mechanics,
verdicts-only deliverable
Diagnosis on tonight's data (4 sources, 173 nodes, 216 edges): supports is
62-83% of edges, topology is hub-and-spoke (34/94 libertarianism nodes are
degree-1 leaves), hub edges read as essay-flow not logic, 33-41% of nodes
are degree<=1 across sources. The old doctrines asked for exactly this:
'every distinct proposition' + 'lonely nodes = failed extraction' +
'COMPLETENESS... omitting is the main way extraction fails'.
Owner rulings, prompt-only (data model untouched, both prompts):
- strict reasoning bar: a proposition earns its node by being a premise,
step, or conclusion of an argument the source actually makes; isolated
facts, name-drops, asides, trivia, example enumerations are dropped
- supports tightened: ONLY explicit grounding acts in the text, rare
exception, genuinely argumentative relations are almost always follows
- completeness inverted: precision over recall, WHEN IN DOUBT OMIT,
never connect merely-related propositions
- MUST-connect clause dropped; corrections of misconceptions stay in
(argumentative by nature)
Identity doctrines untouched (established-terminology names, MERGE YOUR
OWN OUTPUT, aliases) — zero near-duplicate statements proves they work.
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
Every notes chunk and chat coverage batch is its own job — registry row,
usage record, failure domain. Extracts fan out process-wide behind a
3-permit extract semaphore on globalThis (new 'queued' phase); audits and
merges stay serialized behind the existing FIFO gate ('waiting'), so each
auditor explores the graph after every earlier job's merge.
- ExtractionPhase gains 'queued'; RunningExtraction gains part {i,n};
the feed shows one row per job with a '· i/N' suffix (status route)
- abortExtraction is source-wide: every job of a fan-out dies together;
merged jobs keep their coverage checkpoints
- failure isolation at the fan-out (Promise.allSettled + aggregation):
a failed chunk no longer kills its siblings — the run returns what
actually merged; job-level aborts log, pre-fan-out errors still throw
- usage ledgers: one record per job (source-keyed), failed jobs bill
their partial spend
- runJob/aggregateJobs helpers; setPhase/recordStep/extract/audit/
notesExtract key the registry by jobKey (src#i) — notesExtract's
signal lookup was the one real bug the new tests caught (source-id
lookup meant fan-out aborts never reached the model call)
- semaphore is handoff-correct (fresh takes increment; handed-off
permits don't double-count — an inflated counter deadlocks at 3)
- tests: isolation pins rewritten (stage failure now resolves zeros),
single-flight/registry tails updated, per-job usage counts, and a new
describe pinning the fan-out: 3 extracting + 5 queued, parts 1..8,
source-wide abort, per-job usage on failure, extract peak == 3,
audit peak == 1
- docs/architecture.md: serialized-audit paragraph rewritten for
per-chunk jobs; two-front-doors wording aligned
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.
Extraction may run in parallel across sources (extractors are
source-local and graph-blind), but the auditor explores the LIVE
graph through its tools and stamps identity verdicts against what
it finds — two concurrent audits would each link into a graph the
other is about to change, the structural mint of duplicate nodes.
withAuditLock: a FIFO promise-chain mutex on globalThis (the same
module-instance idiom as the run registries — a module-level lock
would be per-route-instance and useless in dev). The check runs
before any LLM is invoked or the graph read; the lock spans audit
through merge inclusive (the merge is the graph mutation) and is
reacquired per batch, so interleaved multi-batch runs each audit
against the latest graph instead of starving each other. Applied at
both front doors — chat batches and notes imports.
New 'waiting' phase (extracting → waiting → auditing → merging):
visible in the status feed so a queued run never looks stalled in
extracting. Abort-aware on both sides of the queue: a run aborted
while waiting leaves without auditing (its chain link still
releases — successors never hang); a run aborted mid-audit releases
in finally.
Tests pin: a second run queues at the gate and its auditor is
invoked only after the first run's merge is in the ledgers (the
post-merge-graph invariant itself); a run aborted while waiting
never audits, checkpoints nothing, and leaves a healthy chain for a
third run. Gates: typecheck, 1061/1061, lint, build.
Owner's first live test of the relinked pipeline surfaced three issues,
diagnosed against the ledgers:
1. Detail-card edge labels were backwards (UI only — data and canvas
arrows were correct). Semantics pinned 2026-09-28: an edge A→B
means A implies B (follows) or A supports B (supports). Outgoing
edges now list under 'What follows from this' / 'What this
supports', incoming under 'Follows from' / 'Supported by'.
Verified against the ledger: Invariance --supports--> Twin
paradox / Relativity of simultaneity; Argumentation ethics
--follows--> Homesteading — arrows correct, only the words were
swapped. docs/architecture.md wording fixed to match.
2. Session-local terminology leaked into node names ('Criterion C',
'Action axiom (F1)'). Both extractor prompts gain a naming
doctrine: names use established, discipline-canonical terminology
(glossary-headword test); never source-local bookkeeping (letters,
numbering, positional labels); local shorthand goes to aliases.
3. Near-duplicate propositions and lonely nodes (~40% of fresh-run
nodes edge-less; 'Argumentation ethics' vs 'A priori of
argumentation' duplicated despite the auditor having linked that
phrase once). Root cause is upstream of the auditor: the extractor
had no self-merge or edge-completeness duty. Both prompts gain:
MERGE YOUR OWN OUTPUT (same claim at different precision = one
proposition, phrasings to aliases; nothing downstream dedupes) and
edge COMPLETENESS (a proposition that arose as evidence for,
consequence of, or prerequisite of another must carry the edge —
a graph of lonely nodes is a failed extraction).
Owner rulings on the open question: keep two edge types (no
'relates to' junk drawer); re-measure loneliness after these fixes.
Gates: typecheck, 1059/1059, lint, build.
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.
Two follow-up rulings on the explain tool:
1. No 'I don't know' row — not needed in free recall. If he doesn't
know, he says so in his own words; his text is the only answer
channel. Dock: text mode is rowless again (the composer IS the
surface); doctrine and the shared prompt chunk updated; the format
branch and card revert to quiz-only dontKnow.
2. explanation + correctAnswer collapse into ONE field — the separate
explanation was quiz remnant (MC needs the why outside its options;
a free-text commitment carries its own reasoning). Schema is now
{question, correctAnswer, details?}; the reveal shows the single
committed answer; no explanation snapshots anywhere on the explain
path.
1047/1047 tests, typecheck/lint/build green.
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 probe intro was redundant with 60-probing-in-brief.md (and its
no-probe fallback described a state the code can't produce — the teacher
only exists post-probe). 'How the map tracks itself' folded into one
tagging paragraph; grain and rewriting compressed; the live-map context
kept to one sentence. The discovery-chain thinking block stays verbatim,
and the stress-test stays. Presenting: ask him to confirm the plan
before teaching begins — medium unprescribed (owner ruling 2026-09-27;
dock-question confirmation leaves the teach divider floated next to the
planning divider when no text reply splits the group — accepted as
cosmetic).
The inferred teach seam sat as a ternary branch in the messages.map
chain (alongside the no-bubble marker branches), so the ENTIRE first
teaching group — thinking, prose, the tagged quiz call — rendered AS
the divider and vanished below it (the Lacan session, 2026-09-27).
The marker branches replace contentless scaffolding; the teach group is
a real message. The divider now prepends inside the group's Fragment.
Also tightens 10-planning.md: the presenting turn asks NOTHING — no
quiz, no ask_user_question, no 'shall we begin?' (the teacher was
substituting a confirmation question for the dead verdict buttons).
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.
Three owner rulings from the gradient-descent session
(chat_muhskq6p_e8c3600b — 7 failed quiz calls, answers leaked in
option descriptions, planning divider floating at the transcript top):
1. RETRYING LINES (general): any tool call that errors back to the
agent (validation failure, malformed args — the retry loop) renders
one quiet "Retrying…" SystemLine instead of silence. Question tools
previously rendered null, so a flailing quiz loop read as a stall.
Aborts are terminal ("Operation aborted") and stay silent. Verified
live: exactly 7 lines render at the 7 failure sites of the session.
2. QUIZ OPTION DESCRIPTIONS: NUKED. The model used them to telegraph
correctness in every quiz of the session ("that moves uphill — it
climbs f instead of descending it") — answerable by elimination.
The field is gone from BOTH quiz schemas (probe + teach); ask keeps
it (ungraded — nothing to leak). The dock additionally suppresses
descriptions for quiz kind, so pre-fix records stop leaking too.
Verified live: the session's pending quiz renders bare labels.
Plus the diagnosed failure-mode fixes: GLM sometimes emits `options`
as a JSON-encoded string (6 of 7 failures). prepareArguments hooks
on all three question tools coerce it BEFORE pi's validation (the
call lands instead of bouncing); the client coerces again in
normalizeOptions (pi persists raw call args). Schema descriptions
hardened ("REQUIRED ... never a JSON-encoded string").
3. DIVIDERS AT THE MARKERS: the probe-handoff SystemLine
("Preparing your plan…") IS NOW the Planning divider; the accepted
verdict IS NOW the Teaching divider ("✓ Plan accepted" line gone) —
one signal per seam, not two. The fold-based divider block is
DELETED: Wave D grouping merged the whole probe into one giant
assistant group, so the inclusive-fold comparison floated the
divider to the transcript top (reproduced against the real record:
6 view messages, phase flipped at the giant group). The markers
render in the right spot by record construction. A DECLINED verdict
is not a seam — it keeps the "↻ Plan revision requested" line.
Verified live: plan divider after the probe concluded, teach
divider after the plan presentation, nothing at the top.
Tests: 1004/1004 — string-coercion pins (normalizeOptions + the
prepareArguments hooks on all three tools, identity fast path),
quiz-schema-has-no-description pin (ask keeps it).
Owner correction (2026-09-26, later): a real lesson is ONE continuous
teach->quiz->teach run - there are no run-would-stop moments mid-lesson,
and the run ends that DO exist are quiz stopper exits where delivery
must not fire. Subagent results must land BETWEEN messages INSIDE the
run, never splitting a toolCall from its toolResult.
Vehicle 1 now hands markers to agent.steer() - pi's STEERING queue:
the loop drains it at TURN BOUNDARIES (after a completed assistant
message AND its tool results - the bonded pair survives injection) and
again at RUN START (a continuation drains before its first LLM call),
then CONTINUES the run with the marker in context.
steeringMode "all" replaces followUpMode.
registerLiveAgent moved BEFORE runPiTurn and now hands pending markers
itself: the start poll drains the queue SYNCHRONOUSLY inside the
runPiTurn call, so a later registration would miss it. A marker
stranded by a quiz-terminated run therefore rides the ANSWER run's
first LLM call - the native replacement for the old continuation
coalescing.
Settlement reconciliation, the idle wake, probe-handoff exclusion,
and the registry-as-truth design are UNCHANGED; stranding is now
exactly the stopper-exit case.
Tests: jobs 45/45 (fakeAgent.steer; new pin - registration steers
pending markers into the new run, parked wake never fires); chat-flow
24/24 (new e2e through the real composition - a gated mid-run turn, a
marker delivered while the turn streams, record shape
[call, result, MARKER, response] with the marker adjacent to the
completed unit, the model SEES the marker in its next prompt, zero
wakes); full suite 999/999, tsc/lint/build green.
Live smoke (PORT=3004): research dispatched in the probe turn parked
behind the dangling quiz; on the quiz answer the marker injected at
the answer run's START poll - record [answer result, marker, teacher
response], job flipped done/delivered at settlement, brief card
rendered with its sources toggle.
Plan doc: docs/PI-NATIVE-MIGRATION-PLAN.md gains the pivot section.
Deferred (owner list): thinking blocks rendering after messages;
vanishing tool-call output tokens.
Delivery now rides pi's OWN follow-up queue when a run is live, and a
carry-everything idle wake otherwise. One registry, two vehicles;
claimSteers and continuation coalescing are deleted (owner rulings,
2026-09-26: follow-ups only; one queue; the wake is the only appender).
THE LIVE RUN (vehicle 1): the chat-handler registers the streaming
turn's agent (registerLiveAgent, next to registerTurn); the pump hands
every eligible queued marker to agent.followUp() - pi polls that queue
when the run would otherwise stop, injects the markers
(message_start/message_end on the live wire), and CONTINUES the same
run. followUpMode "all" joins every handed marker in one continuation
turn. The question stopper runs before the follow-up poll, so an open
quiz is never overridden - a handed marker strands at settlement and
waits (today's pacing). probe-handoffs never take this path (the
handoff must START the teacher's run - a record-level branch).
SETTLEMENT RECONCILIATION: settleSteers(chatId, persisted) at onFinish
(after unregisterLiveAgent) resolves+flips entries the run delivered
and returns stranded ones to pending - the registry stays the truth;
the dead agent's queued copy is abandoned. This is what makes the
handoff safe: pi owns no retry for a stranded follow-up.
THE IDLE WAKE (vehicle 2): waitForIdle (unchanged gates) then ONE turn
carrying EVERY pending marker as its input appends - dequeue-at-fire
(the Wave B regression lesson: the wake run's own follow-up poll must
never see its own cargo; a 409 pushes the cargo back and retries).
CLIENT: applyWireEvent now upserts non-assistant messages by id, so
in-run injected markers render live instead of dropping until settle.
Tests: jobs 44/44 (live handoff, settlement stranding, handoff-waits,
one-wake-carries-all); chat-flow 23/23 (the double-append pin drives
the real pump through the new input); session-core 28/28 (custom-role
wire appends). Full suite 997/997, tsc/lint/build green.
Live smoke (PORT=3004): research job delivered mid-probe - parked
behind the dangling questions exactly per semantics, landed once on
the open gates, teacher folded the brief in and re-dispatched; the
second (good) result rendered the full 10-source brief card and the
lesson continued from it. No duplicate ids anywhere in the record.
Known gap flagged to owner (NOT from this change): the server accepts
malformed answer events via raw API (answers without the answers[]
shape) and AnsweredCard then crashes the page render - surfaced by the
smoke's scripted answers, repaired by hand in the scratch record.
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).