Commit Graph
490 Commits
Author SHA1 Message Date
amosblomqvist da0cd558d0 practice slice 3B — the practice agents: partitioner, generator, grader
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.
2026-09-29 18:14:30 +03:00
amosblomqvist 00b3c23e06 practice slice 3A — runSubmitAgent extracted to shared module; practice model roles
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.
2026-09-29 17:57:53 +03:00
amosblomqvist b7a19609cb practice slice 2 — store, paths, self-contained sets
Contract amendment (planning ruling): ProblemSet gains propositions
(nodeId → {name, statement}) and chains (the frozen derivation) — sets
are documents of what WAS selected; grading/dedup/coverage never touch
the live graph. config.ts PATHS.practice → data/practice/. PracticeStore
port (createSet assigns ps_<ts36>_<rand>, getSet, listSets createdAt-desc,
saveSet replace-whole, deleteSet). src/store/practice.ts: one JSON per
set, hand-written isProblemSet read guard (types-only; corrupt/missing →
null/skipped, never thrown; saveSet refuses invalid records). Wired in
src/server/stores.ts. 10-test suite: round-trip, corrupt-tolerance,
missing-dir, validator garbage-reject + optional-tolerate + verdict
union, ps_ id shape.
2026-09-29 17:53:36 +03:00
amosblomqvist 05dcaafc68 practice slice 1 — contracts, the chain bijection, coverage
src/core/contracts/practice.ts (new domain): ReasoningChain (group by
conclusion, id = conclusion node id), PracticeParams (three 0..1 dials),
ProblemSet/Problem lifecycle, feedback-only ProblemGrade. src/core/
practice.ts: chainsForSelection (follows-only, both-ends-in-selection,
group-by-conclusion, singletons for edge-less nodes), chainsToGraph
(the exact inverse — bijection pinned), chainCoverage (the completeness
law: union of problems' chains = the set's chains; duplicated allowed).
13-test suite: diamond, premise-only, singleton, supports-invisible,
outside-endpoint, dedupe, determinism, round-trips, coverage cases.
2026-09-29 17:48:20 +03:00
amosblomqvist f12489fe49 price drift-guard — the gates fail when a pi-ai bump isn't followed by a sync
PRICES must stay a generated static table (core is client-safe; the
renderer folds costs and reading the catalog JSON needs fs), so the
forgetting-the-sync failure mode is guarded instead of eliminated.

- sync-pricing.ts: buildPrices() extracted (the pure catalog walk),
  exported for the test; main() now guarded by an entry-point check so
  importing the module runs nothing
- tests/prices-sync.test.ts: asserts PRICES equals EXACTLY what the
  script would generate now — unpriced new models, drifted prices, and
  keys gone from the catalog all fail with 'run npx tsx
  scripts/sync-pricing.ts'. Verified it fires by deleting the
  deepseek-v4.1-flash entry (1 unpriced, actionable message)
- sync script's embedded header template corrected to match prices.ts
  (no bare-slug legacy claim) + drift-guard note, so the next
  regeneration keeps the honest docs
2026-09-29 04:39:01 +03:00
amosblomqvist 82503446c8 price-table re-sync — deepseek-v4.1-flash and 173 price corrections from pi-ai 0.87.1
Costs were rendering '—' for tonight's extraction runs. Root cause: the
pi family bump (df1328e, 0.85.1 -> 0.87.1) added deepseek-v4.1-flash to
the shipped catalogs, but scripts/sync-pricing.ts was never re-run after
it — src/core/prices.ts was still the 2026-09-25 generation. The design
itself is sound and matches the owner's expectation: nothing stores
dollars; records keep tokens + model string; callCost infers at read
time from PRICES[model]. So the re-sync retroactively re-prices every
run the moment the entry lands.

- npx tsx scripts/sync-pricing.ts: 1495 keys (+314/-173 lines);
  openrouter:deepseek/deepseek-v4.1-flash = 0.15/0.6/0.003 (matches the
  catalog the picker serves)
- glm-5.3-flash price drift (0.075/0.25 -> 0.15/0.5, cacheRead 0.015 ->
  0.05): four pinned-cost tests re-derived (usage, cost-summary via
  callCost, activity-dock, stats-bar)
- prices.ts header: dropped the false bare-slug-legacy claim (the table
  has zero bare keys; sync emits provider-qualified only) and noted the
  forgetting-the-sync failure mode
2026-09-29 04:30:15 +03:00
amosblomqvist 5707672a09 auditor grounding — a prescribed, bounded exploration procedure + born-in find pointers
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
2026-09-29 04:10:38 +03:00
amosblomqvist 86ba2804d1 extractor prompt rewrite — reasoning skeleton, not lossless archive (owner, 2026-09-29)
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.
2026-09-29 03:47:07 +03:00
amosblomqvist f5a9ed59c7 supports-thickness slider — relative shaft width for supports edges
A LINEAR 0–1 multiplier of Edge width (default 0.5, dead-center at step
0.1 — 11 stops like the ladder rows but ratio-valued), grayed out by the
supports-edges toggle (the Dilation dependency pattern). Renderer splits
supports shafts into their own Path2D and strokes a second pass at
edgeWidth × supportsWidth; arrowheads stay full-geometry in the one shared
path (owner pick: shaft only). A true 0-width stroke is undefined on
canvas — the 0 end floors to a 0.5px on-screen hairline; hiding remains
the toggle's job. Sanitizer: real numbers clamp to [0,1], everything else
resets to 0.5; clamps pinned in render-settings.test.
2026-09-29 03:00:57 +03:00
amosblomqvist 6543d4f886 supports-edges draw toggle — last row of the graph settings' first section
Off hides supports edges from the canvas: the renderer skips them when
building the shaft/head paths (draw-only — the physics sim still feels
them and detail cards still list them; settings never shift the graph).
Server-persisted with the rest (render-settings.json); sanitizer takes
real booleans only, anything else (missing key, legacy file) loads ON —
today's look. Toggle row styled as the Background-colors row; sanitiser
pinned in render-settings.test.
2026-09-29 02:40:07 +03:00
amosblomqvist 8355fbabdb delete the mappings ledger — per-job in-memory resolution + notes fingerprint guard
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
2026-09-29 02:25:32 +03:00
amosblomqvist 92146248d2 Per-chunk reveal: refetch the graph when a JOB row leaves the feed, not when the source drains
The reveal rule (c902bc6: a run leaving the feed means its merge is on
disk, so refetch immediately) was tracking chatId sets — fine when one
row meant one source, but per-chunk jobs made it re-batch the reveal:
chunks finish one by one, each row leaves, but the source's chatId stays
present until its LAST chunk drains, so a big import showed nothing
until it fully completed. The exact batching bug the rule exists to
prevent.

Track by row id (the registry job key) in both the snapshot-reveal poll
and the per-run costs refresh — a chunk row disappearing IS that
chunk's merge landing.
2026-09-29 01:32:53 +03:00
amosblomqvist 4001c3465b Graph view: unique pill keys for chunk jobs (second duplicate-key consumer)
The graph page's pipeline pills keyed rows by run.chatId — the source id,
shared by every chunk job of a fan-out (the dock got the same fix in
8f66f47; this consumer was missed). /api/graph/status rows now carry an
id (the registry job key: source id, or 'src#i'), and the pills key by
it. chatId stays the SOURCE id — the reveal refetch and per-run cost
refresh track chatId SETS, which dedupe naturally and fire when the
source's LAST row leaves the feed.
2026-09-29 01:26:23 +03:00
amosblomqvist 3d9d20794c Auditor submits links ONLY — the edges argument is gone (owner, 2026-09-28 evening)
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
2026-09-29 01:16:32 +03:00
amosblomqvist f72a3f3ada Activity dock: unique row ids for per-chunk extraction jobs (fix duplicate React keys)
The live import surfaced it: a multi-chunk notes source renders N feed
rows, and the dock keyed every one by run.chatId (the SOURCE id) —
React's duplicate-key error. The activity feed also dropped run.part
entirely, so all chunk rows showed identical titles.

- ActivityExtraction/FeedExtraction gain a unique row id (source id, or
  'src#i' for chunk jobs) and part; the feed title carries the '· i/N'
  suffix like the graph status route
- toBlobs keys extraction blobs by the row id; chatId stays the SOURCE
  id (identity for links)
- abort sends the SOURCE id for extractions — aborting any chunk row
  kills the source's whole fan-out (the new source-wide abort), and the
  abort route's id charset rejects '#' anyway
- new pin: one source, two chunk rows → two blobs, distinct ids, same
  chatId; dock fixtures updated
2026-09-29 01:04:40 +03:00
amosblomqvist d047e7b2dc Per-chunk jobs: parallel extracts under a 3-slot semaphore, serialized audits (owner 2026-09-28)
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
2026-09-29 00:34:21 +03:00
amosblomqvist 3e9a85d60f per-session models: every session owns its model + thinking level from birth
The reported bug (2026-09-28): every chat ran glm-5.3-flash regardless of
any pick, while composer and settings UIs agreed with each other and lied
about the run. Root cause: a fossil default-settings record (settings.json
chat section, frozen pre-slots-wave) was stamped verbatim into every new
chat at birth by the new-session flow, and resolveConversationSpec prefers
the per-chat record over the strong slot forever.

Owner redesign — models are per session, always; the strong slot is the
new-session default:

- POST /api/chats stamps the newborn verbatim from the strong slot
  (model + thinking level); no client settings PUT on create anymore
- the composer control is dual-bound: in-session it reads/writes the
  session's own record (GET /api/chats/:id, PUT .../settings); on the
  draft surfaces it binds the strong slot (picking there = changing the
  default)
- thinking levels follow the identical mechanics (confirmed by owner)
- the zombie default-settings system dies whole: useDefaultSettings,
  /api/settings/defaults, src/store/default-settings.ts, the chat
  section of UserSettings (sanitizer strips fossil copies), the legacy
  default-settings.json fold, and DEFAULT_SETTINGS_FILE
- catalogDefaultsFor collapses to the strong slot alone
- thinking display for a legacy unstamped session resolves exactly like
  the engine (session ?? strong ?? model ladder)

Tests: new chat-birth-stamp suite pins the stamp, the unconfigured boot,
the unset level, and the copy-not-live-view regression; defaults pins
rewritten strong-only; legacy-migration suite deleted.
2026-09-28 23:30:11 +03:00
amosblomqvist 1f5e21a674 teacher-prose-only extraction view: coverage, fingerprints, tokens, and batches derive from the teacher's text blocks alone (owner 2026-09-28) 2026-09-28 22:37:18 +03:00
amosblomqvist c17405a3d7 chunk notes like chats; both submit tools require edges/links (owner 2026-09-28) 2026-09-28 21:28:39 +03:00
amosblomqvist f11b1664dc verdicts-only auditor: submit_review is {links, bridging edges} — the findings echo dies
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.
2026-09-28 19:22:58 +03:00
amosblomqvist bd9694b0e4 slim the submit schemas: evidence and messages die; stringified-payload heal at the pre-validation seam
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.
2026-09-28 19:04:39 +03:00
amosblomqvist 14a7f0de1d per-run graph reveal: refetch the snapshot when a run leaves the status feed, not when the feed empties
The old refresh trigger was the some→none transition of the
/api/graph/status poll — with parallel runs under the serialized
audit, run A's merge landed while B was still queued, but the feed
still held B, so nothing refreshed until B finished too: both runs'
nodes appeared in one batched reveal (owner's live-test finding,
2026-09-28). The ledgers were correct the whole time (each merge is
a synchronous append inside the gate; the serialization test pins
B's auditor seeing A's node) — only the reveal was batched.

Now: refetch the snapshot whenever a run DISAPPEARS from the feed
(finished / aborted / failed). A run leaves the registry only after
its merge + settle + usage persist complete, so disappearance
implies the graph change is already on disk; each run surfaces
within poll latency (~2s). Redundant fetches on aborted runs are
free — the deletion-refresh doctrine. The costs pill follows the
same disappearance rule (usage persists per run). Confirmed the
status pills already render the raw phase, so the 'waiting' stage
displays as Waiting with no change.
2026-09-28 18:21:20 +03:00
amosblomqvist 46e128d46c serialized audit: the audit+merge critical section is process-wide, one run at a time (owner 2026-09-28)
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.
2026-09-28 18:00:56 +03:00
amosblomqvist fc8d91a043 first live-extraction findings: fix backwards edge-section labels; extractor naming/self-merge/completeness doctrines
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.
2026-09-28 17:48:51 +03:00
amosblomqvist 1b50013551 extraction cleanup: events ledger dies, markdown source view, auditor-as-linker (2026-09-27 rulings)
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.
2026-09-28 16:54:44 +03:00
amosblomqvist cd09c12b56 vision docs: practice module, side chats, branching, parked persistent agent (2026-09-27 ideation round)
Four living documents capturing the post-explain ideation:

- vision-practice.md (priority 1): exams over chosen node sets; assessment
  record model (extractor observation = data point zero, practice appends,
  labels-for-extractor/numbers-for-everyone-else, aggregation TBD-Bayesian);
  problem-engineer agent flow with self-verifiable coverage metadata and
  commit-before-pose grading via a dedicated write-seam tool; formats
  configurable, theory pending from owner.
- vision-side-chat.md (priority 2): per-block hover-revealed mini threads,
  invisible to the teaching record; v1 = block-only context, escalation
  ladder documented; fork idea set aside (role confusion).
- vision-branching.md (unscheduled): tree-shaped records, plan-overlay UI;
  pi SDK has no native branching (verified) — our own record-model work.
- vision-persistent-agent.md (PARKED): markdown session records as the
  pull-based substrate, graph-as-truth over user.md, graph composer as
  interface, session kinds/three-surfaces note.
2026-09-27 23:12:12 +03:00
amosblomqvist 3160829eb6 explain refinements: no opt-out row, one committed field (owner 2026-09-27)
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.
2026-09-27 09:13:34 +03:00
amosblomqvist 2f39766528 free-recall explain tool: the quizzing-mechanic switch (owner 2026-09-27)
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.
2026-09-27 08:55:13 +03:00
amosblomqvist 07d2c84461 dictation start-latency mitigations A+B+C+D (owner 2026-09-27): queue-don't-drop (frames before socket-open buffer capped and flush in order), parallel mic-gated handshake, app-lifetime AudioContext + memoized worklet module, warm-token cache (mount + post-run pre-mint, TTL 3min, reconnect mints fresh); optimistic meter stays per owner 2026-09-27 04:36:00 +03:00
amosblomqvist 07b25c4ffd de-meta the system prompts; method leads the composition (owner, 2026-09-27)
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.)
2026-09-27 04:14:42 +03:00
amosblomqvist 921cacbb8f display-math newline preservation (Option A ruling 2026-09-27): inline spans restore all six debris chars, display spans keep real newlines — multi-line equations never corrupted; \right/\rho CR-debris still restores; sloppy \neq-in-display accepted as the rarest slice 2026-09-27 02:24:10 +03:00
amosblomqvist 8faf97edc7 LaTeX-escape restoration at the display seam (owner ruling 2026-09-26): JSON-escape debris (FF ate \forall's f, VT \varphi's v, TAB \to's t) restored inside balanced math spans — one seam (tool-parts parse) heals new turns and poisoned records; outside spans untouched 2026-09-27 02:19:27 +03:00
amosblomqvist d6d7c09e2e quiz nodeId required (owner ruling 2026-09-26): no untagged teach quizzes — boundary rejects, model retries tagged; off-map checks route to ask_user_question; anti-rush doctrine in schema+prompt; docs+comments updated 2026-09-27 02:05:51 +03:00
amosblomqvist 8c0f0f7fbf docs: planning prompt cut from 76 to 41 lines — thinking kept, machinery cut
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).
2026-09-27 01:19:27 +03:00
amosblomqvist d680008f25 fix: teach divider prepended, not a replacement branch — first lesson block renders
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).
2026-09-27 00:48:46 +03:00
amosblomqvist 145b470752 fix: verdict death — plan→teach seam inferred from first nodeId-tagged quiz
The Accept/Decline verdict gate is gone (owner ruling 2026-09-26):
presenting the plan ends the teacher's turn; the learner's next message
is a natural reply. Teaching begins at the first quiz call tagged with a
nodeId (probe quizzes carry none) — both folds (view phaseAt3, native
phaseAt3Lh) advance on the call itself, and the teach divider renders
before that whole assistant block. No backwards compat: verdict markers,
the verdict input event, optimistic appends, composer buttons, and
projection all deleted; prompts and docs rewritten to the natural flow.
2026-09-27 00:48:46 +03:00
amosblomqvist dcb839232c fix: plan graph visuals — reading font, 1px lines, both-ends arrow color
Owner's three rendering refinements (plan-graph round, 2026-09-26):

1. Reading font: the diagram root sets font-family var(--font-prose) —
   node titles render in the reading font instead of the inherited mono
   UI font. Deliberately NOT the font-prose utility: @theme inline bakes
   the literal into utilities (the --font-title trap), which would freeze
   the graph at Source Sans 3 and ignore the Appearance reading-font
   override. The runtime var rides it (live-verified: Georgia override
   propagates, null resets to Source Sans 3).

2. Thinner lines: edges + box outlines 1.5 → 1 (the app's standard border
   weight). The arrowheads scale by the same 2/3 (HEAD_H 16 → 10.67; the
   width, the notch overhang, and the DOCK_GAP all derive from the head
   scale), so the head:shaft LaTeX proportion is preserved exactly.

3. Arrow color: white only when BOTH ends are white — an edge with an
   upcoming origin OR upcoming terminal stays gray (line + head).
   Supersedes round 6's target-only rule: arrows leaving a gray box no
   longer paint white into covered territory.

Live-verified on :3001 (native-format fixture: plan_init + answered
quizzes; popup): font default + override, stroke-width 1 on all rects and
edges, 5-edge graph with exactly one both-white edge white and both
upcoming→visited cross edges gray.
2026-09-27 00:47:48 +03:00
amosblomqvist e9b9f52a7e fix: question dock flicker — arrival state at render, growth-stamped fold mute
Two owner-reported symptoms, one mechanism (verified frame-by-frame on a
zero-model-call fixture): the position machine read its OWN growth-driven
scroll lag as the learner leaving.

1. Probe arrival at the bottom: the form opened, the reserve grew, the
   smooth catch-up lagged (d peaked ~405px > COLLAPSE_DIST 96) → phantom
   fold → bar shrink morph → re-expand at bottom. FORM→BAR→FORM, ~600ms.
   Fix: the at-bottom arrival now runs under the SAME fold suppression
   the teach reveal uses (suppressFoldDuringReveal, extracted) — the
   arrival's open and its scroll-to-bottom are the same motion class.

2. Probe arrival scrolled up: one stale-open frame painted before the
   after-paint effect collapsed it. Fix: probeArrivalExpanded — the
   open/closed state decided DURING RENDER (open iff at bottom), like
   the teach seam; the effect keeps only the reveal scroll.

3. Dictation stop flush into the pending question: the field grows on
   its own render (capped 192px), the reserve lands on the NEXT
   DockOverlay render (~1s later live), and the spacer's growth spiked
   d≈152 → fold → bar → re-expand. Fix: the offer stamps the geometry
   mute when the reserve GROWS while pending — the spacer is the
   phantom-scroll source, so the mute covers exactly its scroll events.
   Shrinks never stamp (the resize round's stuck-collapsed regression).

Live-verified on :3001 (job-settle fixture, abort-channel settle, no
model calls): A at-bottom arrival FORM-only; B scrolled-up arrival
straight to BAR with zero FORM frames and no scroll-down; C dictation
flush FORM-only through the +152px reserve growth.
2026-09-26 23:53:43 +03:00
amosblomqvist 876185eb6e chore: pi family 0.85.1 -> 0.87.1 (finishTurn, transcript contexts, typebox 1.3.27)
Mechanical fallout only; semantic deltas flagged in the wave report:
- shouldStopAfterTurn -> finishTurn ({action:'end'} wrap; loop order verified
  equivalent for Wave C stranding)
- transcript rework: seeded leading SystemMessage filtered at both
  state.messages read seams (ui-stream settleWith, pi-run slice); prompt
  and tool declarations now ride the transcript
- streamOnce sites normalizeContext() (Context -> TranscriptContext)
- AgentToolResult details / ToolCall.arguments JsonValue narrowing (casts)
- AUTH_PROVIDER_KEYS regenerated (meta added); glm-5.3-flash price pin
  follows the 0.87.1 catalog (0.15/0.5/0.05)
- vanish probe: 0/12 vs 2/6 baseline; live smoke green on PORT=3004
2026-09-26 21:45:51 +03:00
amosblomqvist 9853075331 fix: quiet Retrying lines, quiz option descriptions nuked, dividers at the markers
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).
2026-09-26 19:08:39 +03:00
amosblomqvist b84ab1cdc0 fix: reasoning always renders above its message; dock drops delivered subagents at delivery
TWO owner-reported bugs from the live red-circle session
(chat_muhpyoll_27305e23), both investigated to root cause first:

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

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

Tests: 1001/1001 - view-canon hoist pin (trailing [text, think, call]
projects reasoning-first; think-first unchanged), noteSteerVisible unit
pin (flip at the seam, idempotent, handoff no-op), and the mid-run
steering e2e now holds the turn open past the injection and pins the
job flipping done at the marker's message_end WHILE the turn still
streams. Live-verified read-only on the owner's server: the original
session reloads with every reasoning trigger above its message.
2026-09-26 05:46:33 +03:00
amosblomqvist ad0a6e5201 feat: Wave C pivot - steer markers, not follow-ups
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.
2026-09-26 04:31:07 +03:00
amosblomqvist 7f51259fc6 feat: Wave C - pi-native steer/follow-up delivery
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.
2026-09-26 03:46:09 +03:00
amosblomqvist 7f3d8aadcc docs: Wave C planning — pi-native follow-up delivery (forks resolved) 2026-09-26 03:15:14 +03:00
amosblomqvist 43ef004ab0 fix: pump wake turns double-appended their steer (Wave B regression)
A pump-fired wake turn carries its steer in the input appends, but the
pump only shifts its queue entry AFTER the turn registers - so the
handler's claimSteers popped that very steer too. The record-side
steerDelivered filter checks the PRE-write-ahead snapshot and cannot
see the carried copy: the marker persisted twice with the same
steer_<jobId> id, two view messages shared one React key, and the
client crashed on delivery (live: steer_job_muhlokbf_84e9373c).

This exact bug was known and fixed 2026-09-18 (the old claimSteers
received the incoming body messages and finalized-without-appending
anything the request already carried). Wave B's turn-root rewrite kept
only the record-side filter and lost the carried-side half.

The fix restores the carried filter: a steer whose id the input already
carries is claimed-and-finalized but never appended a second time.

Regression pin in chat-flow.test.ts drives the REAL pump end to end
(deliverJobResult with a real job record -> waitForIdle -> scripted
wake turn) and asserts exactly one copy + the wake response. Two test
lessons baked into comments: chatIdOf resolves a job steer's chat
through the JOB STORE (a synthetic jobId with no record silently
no-ops the pump), and deliverJobResult resolves at CLAIM time, not at
turn completion (the record assertion waits for the wake response).

The live-corrupted record (chat_muhlcltz) was deduped by hand.
2026-09-26 03:14:40 +03:00
amosblomqvist 8e097752da fix: streaming appended a message per chunk - track the in-flight slot
pi streams AssistantMessages with NO id (verified against a captured
live stream and pi-ai's AssistantMessage type): the id exists only once
the server's persistTurn stamps the record. applyWireEvent minted a
FRESH c_ id per event, so message_start appended, every message_update
found no match and APPENDED another full copy, and message_end appended
again - the tail grew one message per chunk. projectView's step
separators + thinking blocks then filled the transcript with dividers
and thinking indicators mid-stream (with fresh React keys remounting
the whole subtree per chunk), cleaned up only by the settle sync's
setMessages(record).

The fix: the in-flight assistant message is tracked by SLOT -
message_start mints and remembers one client id; message_update and
message_end REPLACE that same tail entry (pi guarantees exactly one
streaming message at a time via its strict start-updates-end ordering;
the reattach replay replays the same sequence). An explicit
message.id on the wire still wins; the slot clears at message_end,
stop(), and setMessages().

Tests: all streaming fixtures rewritten to pi's real shape (no
message.id) - the old ones encoded the wrong assumption - plus a
regression pin asserting N id-less updates leave exactly ONE assistant
message in the tail with a stable slot id.

Verified live mid-stream (DOM sampled every 500ms during a teaching
turn): 0 separators throughout, thinking blocks 1-2 (one per step, not
per chunk), text growing smoothly.
2026-09-26 02:25:04 +03:00
amosblomqvist 0803fbd88d fix: the empty-new-session bug — three live client defects
Live-driven via the owner's dev server (browser tools), reproduced on
/chat/new: send fires, optimistic appends, POST never happens, and the
page shows the frozen empty state. Three compounding defects:

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

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

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

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

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

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

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

Gates: tsc clean, 994/994 tests, eslint clean, build green; live smoke
on PORT=3004 (verbatim wire → native record).
2026-09-26 01:52:47 +03:00
amosblomqvist 7c0e8d9a50 docs: Wave D planning — native wire + client rewrite (forks resolved) 2026-09-26 01:15:25 +03:00
amosblomqvist b3ebbad040 docs: track the pi-native migration plan (recon + Waves A/B records)
Owner-approved home: docs/ instead of the repo root. Wave C (stateful
agents, seam 5) stays optional and deferred; Wave D (SSE dialect +
client rewrite) is next.
2026-09-26 00:57:24 +03:00