8355fbabdb950da5de7fd389c4bb7907dece2764
100
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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 |
||
|
|
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. |
||
|
|
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. |
||
|
|
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 |
||
|
|
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 |
||
|
|
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
|
||
|
|
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. |
||
|
|
1f5e21a674 | teacher-prose-only extraction view: coverage, fingerprints, tokens, and batches derive from the teacher's text blocks alone (owner 2026-09-28) | ||
|
|
c17405a3d7 | chunk notes like chats; both submit tools require edges/links (owner 2026-09-28) | ||
|
|
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. |
||
|
|
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.
|
||
|
|
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. |
||
|
|
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. |
||
|
|
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.
|
||
|
|
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. |
||
|
|
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. |
||
|
|
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.
|
||
|
|
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. |
||
|
|
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 | ||
|
|
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.)
|
||
|
|
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 | ||
|
|
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 | ||
|
|
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 | ||
|
|
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). |
||
|
|
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). |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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
|
||
|
|
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).
|
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
7f3d8aadcc | docs: Wave C planning — pi-native follow-up delivery (forks resolved) | ||
|
|
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. |
||
|
|
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. |
||
|
|
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 ) |
||
|
|
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). |
||
|
|
7c0e8d9a50 | docs: Wave D planning — native wire + client rewrite (forks resolved) | ||
|
|
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. |
||
|
|
2e8bf92408 |
fix(answer): stash the answer input event before addToolOutput
The onSubmitAnswer path was the one Wave B send site missing its stash: addToolOutput triggers the SDK's automatic continuation send, the transport found nothing stashed, and threw — the answer never POSTed, so the question stayed open. One line (plus the contract comment). Caught live by the owner; the suite drives answer events directly at the handler, so this client seam stays untested until Wave D's client work. |
||
|
|
7ab47d4fcb |
Wave B: the native message record — input events, projection, adapter death
The chat record is now pi-native agent history (LhMessage[]), the AI SDK's
UIMessage transcript contract is gone from the server, and every client turn
arrives as ONE explicit input event.
Core (new):
- src/core/native-messages.ts — the LhMessage taxonomy (user/assistant/
toolResult + lhMarker customs: verdict, probe-handoff, subagent-steer,
subagent-abort, plan-block), constructors, and toLlmMessages — the single
model-facing seam (convertToLlm): strips ids, flattens markers to user
text, drops UI-only plan-blocks, synthesizes INTERRUPTED results for
dangling aborted calls, never throws.
- src/core/projection.ts — projectNative: the one-way native → UIMessage
projection (consecutive assistant+toolResult runs group into one
assistant view message; toolResults attach backward by toolCallId;
markers project to data parts; plan-blocks splice after the last
successful plan_init). Every existing consumer reads through it.
- src/core/input-events.ts — the ChatInput union (text/answer/verdict/
continue), parseChatInput validation, and inputAppends (answer targets
the dangling question call or 400s; verdict = optional note user
message + native verdict marker).
Server:
- chat-handler.ts — the turn root takes {chatId, input}; write-ahead
persists input appends + claimed steers before the turn; claimSteers
coalesces the whole queue with the handler's steerDelivered filter as
the dedupe seam; teacher-vs-prober branch on hasProbeDone.
- turn-service.ts — persistTurn stamps ids, drops clean question
placeholder toolResults (the record ends at the unanswered call),
emits plan-block markers idempotently (plan-blocks.ts rewrite), and
persists abort partials.
- ui-stream.ts — PiTurnSpec takes the native base; the wire dialect is
unchanged. jobs.ts steers are native markers (steer_<jobId> dedup ids);
waitForIdle reads danglingQuestionOf. teacher-agent.ts wires
convertToLlm: toLlmMessages. record-adapter.ts DELETED wholesale.
- GET /api/chats/[id] serves messages: projectNative(record.messages).
Client:
- chat-transport.ts — the 409 retry is IDENTITY (re-POST the same event
body after the turn settles — the event was never persisted
pre-acceptance; the merge logic is gone). The transport consumes the
stashed input event via prepareSendMessagesRequest and throws loudly on
a missing stash.
- chat.tsx — every send call site stashes exactly one ChatInput event
(text, answer, verdict, continue) before invoking the SDK send.
Tests: full native migration (chat-flow rewritten to input events and
native record pins; jobs/extraction/demos/graph/usage/plan-blocks/
probe-done/reattach-transport converted; record-adapter.test.ts deleted;
chat-transport tests pin the identity retry). 982/982, typecheck, lint,
build all green.
|
||
|
|
b87a6925af |
Wave A: pi-native tool layer — delete LhTool/toPiTool, TypeBox everywhere, terminate on questions
- src/agent/lh-tool.ts deleted: the zod-wrapped LhTool + toPiTool bridge (z.toJSONSchema per call, clientQuestion flag, hand-rolled result mapping) is gone. Tools are pi-agent-core native AgentTools now. - New src/agent/tool-execute.ts owns what the wrapper added: the abort guard (runWithAbortGuard: run signal + 30s per-call timeout + 250ms grace, 'Operation aborted' / timeout wording preserved), ToolInputError (the retryable '- problem at path' format), result mapping (string verbatim / object JSON / image parts, details rides the record's tool part output), and questionResult() — placeholder + terminate:true (owner Option A: belt of terminate + slim makeQuestionStopper with the clean-question rule; maxTurns deleted from the teaching turn entirely per owner). - All 11 tool files + tools.ts registries converted to TypeBox parameters (typebox@1.3.7, pinned to pi-agent-core's copy). zod refinements (quizInputProblems, askOptionProblems, live-nodeId) moved from schemas into execute — the model sees the same retryable error shape. - question-core + contracts/probe TypeBox-authored (single source shared with the UI); tool-parts parse seam runs the boundary's full chain (Convert → Check → Clean → refinement), fixing the live-smoke lockout: pi's boundary Value.Converts correctAnswer: 17 → '17' but the record keeps raw args, so a parse without Convert rejected a posed quiz (invisible pending question, turn ended). Pinned with the real smoke emission shape. - Subagents converted: demo-builder/researcher AgentTool-native, visual-maker/pdf-reader declaration-only pi-ai Tools with local execute fns, extraction submit schemas re-authored in TypeBox (owner option 2: zod defaults → Type.Optional + fill at the submit seam). - Accepted delta, flagged: TypeBox-native schemas skip pi's JSON-Schema primitive coercion fast path... (boundary still Value.Converts), and prober-quiz unknown keys ride through (Value.Clean strips at display). - Tests: 988 passing (was 967 at the recon baseline); tool-abort and read-tool rewritten against the native seams; question-core/question- tools/tool-parts/demos/web-search/graph-tools/plan-tree/probe-done/ tool-toggles converted to Value.Check/execute pins. Live smoke (PORT=3004, real model): native ls turn streams end-to-end; a real quiz pose (first attempt missing correctAnswer → tool-input-error → model retry → posed) terminates the turn cleanly and persists pending. |
||
|
|
b07cb313f0 |
delete the getApiKey hook — pi-ai resolves keys natively per provider (owner, 2026-09-26)
baseAgentOptions (teacher-agent.ts) and runSubagent (subagents/pi-run.ts) passed getApiKey: () => openRouterApiKey(). pi-agent-core resolves (config.getApiKey ? await config.getApiKey(provider) : undefined) || config.apiKey (agent-loop.js:191) — a provided hook OVERRIDES pi-ai's native per-provider env resolution, and this one ignored its provider arg: every non-OpenRouter provider was sent the OpenRouter key and 401'd (owner repro: openai and anthropic direct). Deletion restores the architecture's stated intent — the keys seam reflects file-saved keys into env and pi-ai reads them per request. openRouterApiKey() export kept (now consumer-less; visual-maker/ pdf-reader read process.env.OPENROUTER_API_KEY directly for their own OpenRouter-specific fetch paths — correct, untouched). Pinned by tests/api-key-hook.test.ts: built teacher + prober expose NO getApiKey hook. Gates: typecheck 0, 987/987, lint clean, build green. |
||
|
|
e89d4c2e23 |
serial questioning + maximal formatting (owner ruling, 2026-09-26)
The prober was planning perfect 7-question batches instead of probing. Three
surfaces now say the same thing — one question, adapt, next question:
- prober/10 rewritten to ~2.6k (from 5.6k): serial discipline leads (ask,
wait, choose the next question from the answer); 'How your questions
arrive' deleted (owner: does nothing); 'You cannot get there by thinking'
deleted (owner: didn't reduce thinking); 'never plan a batch' and the
first-message-length line dropped per owner — no overcorrection, no
strong-instruction inflation. Binary search compressed to four moves.
- tool descriptions: the async-era fossil 'Ask exactly one question per
tool call; after calling, end your turn and wait' — which permitted
multi-call batches — swapped for 'ONE question per turn: ... his answer
determines the next' in all four places (prober quiz + ask descriptions,
teacher quiz description, both zod question-field describes).
- shared/40-formatting: markdown/LaTeX coverage now explicitly names quiz
questions, options, and explanations ('in prose and in quiz questions
alike') — the prober wasn't using LaTeX in questions.
Prober composed prompt ~13.2k -> ~10.0k chars. Pins preserved verbatim
('measurement should be efficient', 'the read tool can inspect it directly').
Gates: typecheck 0, 986/986, lint clean, build green.
|
||
|
|
ac1c1a2818 |
Font picker fix round: one search field, honest empty-enumeration fallback, tooltip only on the closed trigger
- FontMenuList: the separate custom-family input is gone; the search field absorbed its job — a no-match query renders a 'Use <query>' action row (customFamilyFor, pure, pinned), Enter picks it - openFontMenu: an empty-but-successful queryLocalFonts() now falls back to the curated list (Electron 44 grants local-fonts and still enumerates zero fonts — reproduced with a hidden-Window probe); the footer split states WHY: 'could not be read' vs 'needs the desktop app' - The Hint tooltip wraps the trigger button only and unmounts while the menu is open — it no longer pops over the dropdown |
||
|
|
ad61301324 |
Appearance wave: searchable reading-font dropdown + emphasis inks default to text color
Two owner rulings (2026-09-26):
READING FONT — the free-text CommitInput is retired; a searchable
dropdown of the user's INSTALLED fonts over model-menu's exact
fuzzy + arrow-walk + Enter machine (FontMenuList in model-menu.tsx,
fed by window.queryLocalFonts in the desktop shell):
- The DEFAULT row (Source Sans 3) is a fuzzy CANDIDATE, not a ghost:
fontMenuResults (pure, pinned) matches it on empty/source/sans/
default queries only, so an unfiltered list opens with the default
first and a searched list drops it honestly — Enter can never pick
a row that isn't on screen. Picking it writes fontFamily null
(empty = Source Sans 3 semantics preserved).
- The enumeration is CLICK-bound (queryLocalFonts requires user
activation — it cannot be a mount effect), deduped to unique
families, the factory family excluded (the DEFAULT row is it),
cached for the section's life; a failed enumeration falls to the
curated list rather than an empty picker.
- Dev-browser fallback is honest, never fabricated: queryLocalFonts
absent → a small curated list (the app's own knowns + ubiquitous
macOS families) with the footer 'Common families — your installed
list needs the desktop app'. The manual escape lives inside the
menu: a custom-family input + Set commits any family name the
enumeration didn't surface (the old free-text input's heir).
- composeFontFamily (pure, pinned): null/blank → null, else verbatim.
- The trigger gets a Hint (tooltip wave convention); the row's
empty-state hint keeps reading 'Empty = Source Sans 3'.
⚠ electron/main.cjs — one line, FLAGGED FOR PLATFORM: 'local-fonts'
joined the allowedPermissions set (the Local Font Access grant the
picker needs in the desktop shell; the loopback-origin discipline is
untouched).
EMPHASIS INKS — the factory #ee928c/#6fa9d6 are gone: both globals.css
theme blocks now default --lh-ink-strong/--lh-ink-italic to
var(--foreground) — unset emphasis renders the regular text color.
Explicit hex overrides ride unchanged (the appearance seam was already
null-honest); the rows' empty-state labels now say 'same as text'.
Tests (+16, 983 total): fontMenuResults default-row candidacy +
filtering, composeFontFamily null/blank/verbatim, the curated roster
(no factory-family duplicate), FontMenuList SSR shape (default row,
families, effective highlight, custom escape, loading state), the
trigger-replaces-input pin, and the globals.css factory pin (no old
hexes in the declarations; var(--foreground) ×2 in both blocks).
Live-verified on :3001 (dev browser — the curated-fallback path):
dropdown opens with the default row + curated list + honest footer;
search filters ('serif' → CMU Serif only); picking Georgia lands
record + --font-prose live; the default row unsets (var cleared);
the custom input commits Baskerville verbatim; Esc closes the menu
WITHOUT closing the dialog. Ink: strong/em computed = rgb(230,230,230)
(the text color) at factory; #ff0000 through the dialog renders red
and clears back cleanly. The desktop queryLocalFonts path cannot run
in this environment — the seam is click-bound + cached with the
failure fallback, and needs one desktop-shell smoke.
|
||
|
|
3dfc331d7b |
Prompt editor affordances: version-in-use hint, dirty-gated Save, auto-on
Owner-confirmed semantics (verified in composeInstructions first: switch ON = the user's override text is what the agent reads; OFF = the default composes, the saved file stays on disk unused). Four refinements: 1. The small text under a file name states WHICH VERSION IS IN USE: 'Your version is in use' (PROMPT_IN_USE_HINT, pinned) when the switch is on; NOTHING when the default composes — even with a saved override sitting on disk. The old presence-based 'Your version is saved' (shown whenever a file existed, regardless of the switch) dies; the switch's meaning becomes legible through the line it visibly controls. 2. Save is clickable only when there's something to save: grayed and inert while the draft equals what's on disk (including the untouched editor just opened), full-contrast the moment the text differs. The always-active same-color button was the 'doesn't feel like it does anything' half of the report. 3. Saving auto-turns the switch ON — writing an override and putting it to work collapse into one action (a save with the switch off was the did-something-changed-nothing trap). Fires whenever the record genuinely has the path off, so a re-save after toggling off re-on's too. Order is the safe one (file first, then the switch the validation requires it). 4. A quiet inline 'Saved' / 'Deleted' confirmation rides next to the buttons (the old status line echoed the file path at the section's bottom — production-copy violation and easy to miss). Plus the mirror-symmetry the auto-on needs: deleting an override while its switch is on now composes the switch OFF in the same action — not just redundant with Platform's 643a693 server-side clear: the dialog's record mirror is what composes the next section write, so a stale ON would silently defeat the auto-on after a save-following-delete (the mirror would think the switch is already on and skip the write the server record needs). Live-verified on :3001: untouched editor grays Save; typing activates it; reverting re-grays it; save-with-switch-off lands toggle ON + hint + 'Saved' + Save re-grayed in one click; toggle-off drops the hint with the file retained on disk; re-save while off re-on's; delete while on clears switch + file + hint with record left clean and a subsequent agent-section write landing 200. |
||
|
|
c25ee44bfd | formatting chunk: from restraint to maximal use — documents, not chat messages (owner ruling, 2026-09-26) | ||
|
|
e79f85ebe4 |
Prompt-override orphan trap: delete clears the switch, writes heal orphans
Contract-fix interlude (2026-09-26), two bugs from UI's prompts-overhaul (6edc220): (a) ORPHAN-400 TRAP: an ON switch whose override file dies orphaned the record; every agent-section write then 400'd (ON-must-have-file) while the disabled toggle couldn't reach the OFF escape hatch — a user stuck unable to write settings. Verified live on 3004 before the fix. Two-layer fix: - DELETE /api/settings/prompts/[...segment] now clears the record's switch in the same operation (switch-first order: a crash between the two leaves the valid off+file state, never an orphan). Response gains switchCleared (additive). Overrules the 2026-09-19 switch-survives- delete note — the delete route and the record agree by construction. - PUT /api/settings/[section] agent writes HEAL an orphaned ON to OFF (key dropped) inside the write — never a 400 (defense in depth: disk wipes outside the app can still orphan). A valid ON with its file stays valid; the invariant: a user can never get stuck unable to write settings, and the record never contradicts file reality for long. Heal logs at info. (b) TEST WIPES REAL DATA: tests/prompt-overrides.test.ts wiped data/prompts/ (REAL user override files) in after() and its empty-list case — every npm test killed actual overrides and orphaned their ON switches (feeding trap a). Isolated via the established LH_DATA_DIR seam (mkdtemp + env set before the first config.ts-loading import; all app-module imports dynamic). after() now snapshots the REAL repo data/prompts before/after and fails the suite if it changed; the seam is pinned (writes land under the temp dir). Pins: delete-clears-switch (incl. idempotent no-op), the exact repro (disk-level wipe → mirror-the-orphan + tool toggle → 200, healed key, rest of the write landed), vanished-fragment ON heals, valid ON stays. Live: the 400-forever write now lands 200, heal info-logged, record clean. Gates: 967/967 (+5), typecheck, lint, build green. |
||
|
|
178995c987 |
Agent prompts section: re-read on write, dedupe, owner-ruled layout
The prompts area's five owner-reported symptoms, root causes found in code and verified live before fixing: 1. STATE ECHO — the toggle's truth (SegmentInfo.enabled) derives from the settings record's agent switches, but the segment view was read ONCE per section mount: the switch write landed (PUT /api/settings/agent) yet the toggle painted its old state until a remount re-fetched. Root-cause fix in the settings-saved family: the section now re-reads /api/settings/prompts when the 'lh:settings-saved' signal lands for the agent section — the reader the signal law was missing. Toggle flips verify at ~40ms, no navigation. 2. DUPLICATION — the server contract returns the shared segments in BOTH per-agent lists; the UI rendered both lists back to back, so every shared chunk appeared twice (18 rows, 5 dupes). sectionedSegments (pure, exported, pinned) dedupes: the agents' own chunks plus the shared chunks ONCE. 3. ORDERING — Teacher first, then Prober, then Shared last (owner-ruled), each section headed, chunk order within a section preserved from assembly order (the server lists ARE assembly order: base, shared sorted, own numeric-prefix sorted). 4. GRAYING — the whole-row dim died; only the TOGGLE dims now when there is no override file (a wrapper span on the control; the file name is always full-contrast). 5. DESCRIPTION LOGIC — the 'No changes yet' line died. No description when there's no override (the empty state is clean: toggle + name); 'Your version is saved' (PROMPT_SAVED_HINT, pinned) appears ONLY when an override exists. The redundant 'Shared base — appears in both agents' editor note died too — the Shared section header carries 'Used by both agents'. Also: saveSegment/deleteSegment reuse the readPrompts callback (keeps last-good on a failed re-read; the mount effect owns the loading state). Live-verified on :3001: sections in the ruled order with 13 unique rows, clean-row and overridden-row states per the rulings, toggle flip instant (view + record agree), save/delete round-trips, and a shared-segment save + toggle through the single Shared control. Open finding (NOT fixed here — needs a ruling): an ON switch whose override file is later deleted orphans the record, and the agent-section write validation 400s every subsequent write (an ON switch must have an existing file) while the UI's disabled toggle cannot reach the OFF escape hatch the validation anticipates. Repro: Save + switch on, Delete the override, flip any tool. Server validation + row semantics are Platform's / owner's call. |
||
|
|
95bc8d2261 |
launch message: reasoning before the plan (owner ruling, 2026-09-26)
The probe→teach handoff wake read as a dispatch order ("Author the session's
plan map now") and the teacher skipped the deliberation the planning fragment
asks for. The wake now paces it: take your time, read the assessment and the
probing transcript, reason out the optimal way to teach this person this
thing — floors, ceilings, misconceptions, the minimal tree so every step
lands as obviously true — and only then call plan_init. Machinery honest:
plan_init is still the first tool, the plan map still the first artifact, and
the tail now says to wait for his verdict (which is what presenting actually
does — the old "then begin teaching" tail skipped the verdict seam).
|
||
|
|
18c84b8a93 |
prober rescoped to a tight five-step process; method moves out of the shared base; logistics chunks compressed
- base keeps intro + the four philosophy paragraphs + accuracy (9.0k -> 3.0k); the click/arc/hedge paragraphs and Principles i/ii move verbatim to teacher/05-method.md (teacher-only — the prober never teaches) - prober/10 rewritten as one process doc: read map -> make the goal concrete (questions, not orientation) -> reason out strands -> bracket with quiz -> probe_done; teaching-in-brief compressed into the opening, probe-done folded into step 5; prober/20 and prober/30 deleted - shared logistics chunks compressed per owner ruling 2026-09-26: question-tools 5.1k -> 2.3k (option-construction procedure kept whole), learner-files 771 -> 374, formatting 1.8k -> 549 - prober composed prompt 25.9k -> 13.2k chars (~6.5k -> ~3.3k tok, -49%); teacher 36.6k -> 32.8k (method content identical, order shifted) |
||
|
|
79e7c8648b |
Stream reattachment client half: resume via same rendering path
Reattach to a turn that outlived its view through the SDK's native
resumeStream() so replay burst + live deltas flow through the exact
same processUIMessageStream chunk pipeline as first-subscriber
streaming — no second rendering dialect.
chat-transport.ts: ReattachingChatTransport subclasses
DefaultChatTransport and repoints reconnectToStream at GET
/api/chats/[id]/stream (the server's reattach endpoint, not the SDK
default /api/chat/{id}/stream). It peeks the first SSE event:
- turn-settled terminal: cancel body, fire onSettled (refetch), return
null — the SDK treats null as no active stream, status stays ready.
- start chunk whose messageId equals the seeded last assistant message
id (continuation case): rewritten to a fresh id so the resumed run
mounts beside the seeded message instead of replacing its parts —
settle-time record sync merges to the server's assembled message.
chat.tsx: mount effect resumes when initiallyInFlight (no once-ref:
React StrictMode's dev double-mount aborts the first attempt at
dispose — the retry must run). Mid-session discovery sites (jobs
poll, handoff watch) funnel into attachToRunningTurn().
turnInFlight is demoted to fallback liveness: the 1500ms record poll
self-heals a failed attach and clears a stale error banner on settle.
A status watcher clears the flag once the local stream represents
the turn. Stop/Esc unified in abortInFlightTurn: local stop() when a
stream runs here, always POST /api/activity/abort {kind:'turn'} with
409 not_running treated as a benign raced settle. Composer busy now
covers reattached turns (Stop square shows, send gated).
Live-verified on :3001 with real model turns: navigate away mid-turn
and back (replay + live deltas in-view, fallback poll disengaged),
Esc-abort of a reattached turn (partial persisted server-side,
composer restored), two concurrent sessions (raced-settle handled
via turn-settled + record refetch; live settle adopts the final
record), and no leaked stream readers after unmount.
|
||
|
|
7cfd8b5bc7 |
Background turn runner: every turn is a server-side process, clients are views
Owner ruling 2026-09-25. runPiTurn executes a turn decoupled from any HTTP
response: an unbounded memory-only event log (finite by construction; the
30-min liveness TTL bounds the pathological case) plus a subscriber set.
The POST /api/chat stream and the new GET /api/chats/[id]/stream are both
subscribers — identical SSE dialect; late subscribers get FULL replay
(starting at the start chunk) then live until finish + [DONE]. A view
cancelling (reload, navigate) now detaches ONLY — never aborts the turn.
The remote kill is the abort channel: POST /api/activity/abort gains
kind: "turn" (id = chatId; not_running 409 like jobs); handle.abort sets
sawAbort + agent.abort(), every subscriber closes cleanly, the partial
persists through the usual settle (onFinish runs once, then endTurn,
then unregisterTurn LAST so a view that finds nothing finds the record
current). No running turn on reattach: the cheap terminal — one
{"type":"turn-settled"} event + [DONE] (settledTurnResponse).
The live smoke caught what the unit suite had masked: the agent_end
close() had been left behind in the restructure — ReferenceError every
turn, steered into the error path; fixed and pinned (clean turns must
never emit an error chunk; finishReason stop present).
|
||
|
|
369fe497ae | question-tool semantics: the async-era 'end turn and wait' doctrine dies — tools are ordinary tools now (owner ruling, 2026-09-26) | ||
|
|
5f5fa32482 |
Teach questions arrive collapsed in their first paint — no phantom scroll
Owner report (2026-09-25): in the teaching phase a new question arrives collapsed behind the reveal button, but the transcript scrolled up as if it had OPENED — the room it made was the expanded form's height, never shown. Root cause: the expanded state is stale-true from the previous question (nothing resets it when the dock returns to the composer), so the new question's FIRST paint rendered open; the dock measured itself tall and offered that height to the spacer, and the collapse one tick later could never give it back (fold immunity refuses drops while a question pends). Collapsed bar, tall reserve — scroll as-if-opened. Fix (owner-shaped, deliberately minimal): apply the arrival state DURING RENDER, before the first paint (the render-time state adjustment pattern) — revealArrival(wasQuestion) decides how a teach question lands: after the composer → collapsed (the learner reads the teaching first); after ANOTHER question (a queued parallel check) → OPEN — the reading already happened, and the height it already has is the honest height. The paint measures the truth, so the reserve never learns a height that isn't on screen. Probe arrivals are untouched — instant-open at the bottom stays. The old [pendingId] effect keeps only its probe branch. Live-verified on :3001 with a fabricated teach-phase session driven through the real settle/adoption path (zero model calls): composer→teach arrival lands as the 59px bar with the spacer growing 206→211 (+5) and the transcript scrolling only 77px (the new message itself) — the previous behavior jumped the reserve +339px and scrolled 660px; teach-check reveal click still expands (425px) and scrolls to bottom; a queued parallel question arrives open with the reserve unchanged (577→577) and +40px scroll. Pins: tests/reveal-arrival.test.ts — revealArrival(false) = collapsed, revealArrival(true) = open (the owner ruling). 946/946. |
||
|
|
d1542cdcbf | quiz argument order: explanation before correctAnswer — reasoning precedes the pick (owner ruling, 2026-09-25) | ||
|
|
b3c07f89f4 |
Fix ⌘B: dispatch/listen event-name mismatch; thinking pill → "Thinking (⇧⇥)"
Root cause (interlude, owner 2026-09-25): keyboard-shortcuts dispatches
registry actions on the lh:kbd-<action> convention, but the sidebar's
listener sat on the pre-convention name "lh:toggle-sidebar" — a name
nothing ever dispatches, so ⌘B silently did nothing while its tooltip
and cheat-sheet row kept claiming the binding. A sweep of every lh:
listener confirmed the sidebar was the lone stray (chat-renamed and
graph-revision are deliberate non-kbd events).
Fix, structural: kbdEventForAction(action) exported from
src/core/keyboard.ts — the ONE spelling — and both sides compose
through it (dispatcher's default branch, sidebar's toggle listener);
keyboard-shortcuts re-exports the helper so the contract pin can assert
no second spelling exists. The mismatch is now impossible by
construction.
Thinking pill: label trimmed to "Thinking" with its real shortcut via
the registry-composition path — "Thinking (⇧⇥)" (cycle-thinking,
rebind-live like every Hint suffix).
Pins (tests/keyboard-toggle-sidebar.test.ts): the full chain — mod+b →
actionForBinding → toggle-sidebar → kbdEventForAction →
lh:kbd-toggle-sidebar; formatBinding("shift+tab") → ⇧⇥; the
dispatcher-imports-the-same-helper contract.
Live-verified on :3001: ⌘B visually toggles on home, /chat/[id], and
/chat/new (sidebar 0↔288px, hamburger slot 0↔36); with the rebind
lh:keymap {"toggle-sidebar":"mod+j"}: ⌘J toggles and ⌘B is inert
(rebind-live); thinking pill renders "Thinking (⇧⇥)"; keymap restored,
sidebar left open.
|
||
|
|
6b83d3248b |
Tooltip subtlety round: 500ms rest delay, quiet chip, parens suffixes, drops + trims
Owner round 2026-09-25, over 09c0f6e. The wave's tooltips read as distracting; this round makes them quiet: - Delay: TOOLTIP_DELAY_MS = 500 in the primitive — tooltips appear when you pause on a control, never in passing (Radix focus-opens stay instant by design; keyboard users don't wait). - Vanish on leave: disableHoverableContent — the chip is un-hoverable, so the instant the cursor leaves the trigger it's gone, even mid-move onto the chip's own pixels. - Look: bg-accent (#2a2a2a, the hover-chip tone) instead of the white bg-foreground slab; 11px; px-2 py-1; arrow culled (nothing purely decorative); text-foreground/85. - Suffix: parentheses, not em-dash — "Dictate (⌘D)"; the registry source unchanged (still override-aware). - Drops (owner-selected): send square, start square, stats summary — the icons/numbers describe themselves. - Home logo kept but to the RIGHT with its real shortcut "Home (⌘G)" (jump-graph routes to /, the same destination). - Trims: "Model selector (⌘K)", "Thinking level", "Graph costs — click for details", "How much of this session isn't mapped yet". - Added: Import Notes gets "Import notes into the graph" — the one button whose destination wasn't obvious. Pins: TOOLTIP_DELAY_MS=500; hintText parens expectations updated. Live-verified on :3001 — 508ms hover delay measured, close within 10ms of pointerout, chip styles probed (rgb(42,42,42), 11px, 4px/8px), arrow gone, all new labels + sides confirmed, drops carry no tooltip and no aria-describedby. |
||
|
|
b19e7e7eae |
Production tooltips: styled tooltips everywhere, shortcuts surfaced, dev-speak culled
Owner-approved full pass (tooltip wave, 2026-09-25). Mechanism: icon/pill controls ride the styled Radix tooltip via a new Hint composition (components/ui/hint.tsx) — the shared primitive adopted as-is, no restyling; the shortcut appends as an em-dash suffix off the LIVE registry (a rebind in Settings → Keyboard changes the suffix): "Dictate — ⌘D". Surface-owned keys (Esc closes, Enter submits) ride a raw binding prop. Native title= stays only for truncation-reveals (dock spans, the rename field) and menu-interior abbreviations (MenuShell untouched). One em-dash pattern; per-state truth on the send square (busy = "Stop the reply — Esc", idle = "Send — ↵"), Enter on the plan verdict's accept, "Stop dictation (Esc)" kept verbatim per the owner's flag. Copy: the dev-speak cull — no pipeline/prober/tier/wave vocabulary anywhere a user reads. New pins in tests/tooltip-mechanism.test.tsx (hintText composition, asChild no-wrapper-DOM, the auto-update hint constant); dock truncation titles pinned native; graph-chrome + settings pins updated to the new copy. Live-verified on :3001 — 12.6ms hover appearance (no native 1s lag); glyphs render on every suffixed surface (⌘D, ⌘K, ⌘⇧F, ⌘,, ⌘B, F2, ↵, Esc); per-page probes confirm Settings entry, Home logo, session search, Start session, Close sidebar, model + thinking pills, stats summary, rename title, plan chip, coverage ring, send square, cost button, node search (⌘F), graph gear, settings close (Close — Esc), keyboard section "Fixed — part of this screen", auto-update hint, light-mode "Coming soon", the three slot rosters, graph-tools hint. Esc still closes the settings dialog. |
||
|
|
81be55f35c |
Empty-graph chrome: Import Notes stays, search + settings wait for nodes
Owner ruling on the empty graph (honesty principle — dead controls are lies): the top-chrome actions row now gates per control. - Import Notes renders ALWAYS (graph enabled) — the one action that works on an empty graph, the entry point itself. It used to hide with everything else, leaving the empty state a dead end with no entry. - Search + settings render only when the snapshot has nodes, and their appear-once mount carries a deliberate 200ms fade (graph-chrome-fade-in, globals.css — the settings-pop register, reduced-motion appears instantly). Computed animationName verified live. - The row's body is now the exported pure GraphChromeRow (the DeleteNodeBody pattern) — the no-match note's truth (visibleIds + snapshot) stays upstream, computed by the wrapper. - The empty-state placeholder copy is LEFT AS-IS (SCOPE rule: it already has copy — icon, title, and the session-mapping paragraph). Pins (tests/render-graph-chrome.test.tsx): empty → Import Notes present, search/settings absent, no fade carriers; populated → all three, exactly two fade carriers; the no-match note follows its upstream truth. SSR of GraphChromeRow both ways. Live-verified on :3001 with scratch data: empty graph → Import Notes only + placeholder intact; a real import through the button (file input via DataTransfer) → nodes land, search + settings fade in (computed chrome-fade-in on both carriers); wipe → reload → back to Import Notes only (the reverse path). Scratch graph restored byte-identical. 932/932, tsc/lint/build green. |
||
|
|
66090ed769 |
Pre-send key guards: no orphan error-shell chats on first send
The owner's asymmetry find (interlude over the zero-keys wave): the models-unconfigured case guards BEFORE send — no request, no chat created, setup opens — but the key cases only reacted to the 503 after POST /api/chats had already created the record, leaving orphan error-shell chats in the sidebar. The guards now twin the models mechanism on BOTH creation paths (home start(), /chat/new send()): - Zero keys (useZeroKeys reuse): no provider key present -> skip creation and send entirely, open SetupDialog (stage 1 first-key screen). - Strong-provider key missing (owner-approved extension): keys exist generally but the strong slot's pinned provider has none — the original deleted-key scenario. New pure strongKeyless(catalog) in setup-cta.tsx: slots.strong resolved pin + providers[] (present-key only, the catalog's own federation surface), false when unconfigured (the models guard owns that case) or catalog not loaded (the server 503 gate remains the belt). useStrongKeyless rides useModelCatalog, re-reading on the 'keys' AND 'models' signals (a deleted key or a re-pinned slot flips it live). Both guards run after the configured check, same skip-everything-and-open-setup treatment; 'force' (setup's onDone replay) bypasses both — completing setup implies a keyed provider. /chat/[id] sessions keep the 503 revert-draft interception unchanged: those chats legitimately exist. Pins: strongKeyless fires on a keyless strong pin (keys elsewhere or none), never fires with the pin's key present (incl. the keyPresent=false belt), false on unset pin / unloaded catalog. Normal-path pins ride the existing startDraftSession contract suite. Live-verified on :3001 under LH_IGNORE_ENV_KEYS=1 with a file-key anthropic + openrouter-pinned strong: both guards on both pages (setup opens stage-1 / stage-2 respectively, draft preserved, 154 chats unchanged, zero sidebar entries), normal path with a keyed strong provider still creates and lands the prompt redirect. 929/929, tsc/lint/build green. |
||
|
|
1f777f770c |
Zero-keys wave: provider-key gate renders honestly everywhere
Over Platform's 66dbea9 key-presence seam, the five owner-locked tasks: 1. Zero-keys pill — components/setup-cta.tsx: pure hasProviderKey (classifyKeys reuse — tool keys like Deepgram never count), useZeroKeys (GET /api/keys, re-read on the new lh:settings-saved 'keys' signal), ZeroKeysPill (testid setup-cta-keys) taking PRECEDENCE over the models CTA on chat/home/new-session. SettingsSection gains 'keys'; keys-section dispatches after save AND delete. 2. Setup stage 1 is self-contained: provider picker (MenuShell + ProviderMenuList) + key paste + Save via PUT /api/keys/:id — the dialog STAYS MOUNTED; its own save dispatches the keys signal, the fetch effect re-runs on it, and the stage flips to the pickers on its own. onOpenKeys + setup-open-keys die. SetupKeysBody exported for SSR pins. 3. parseProviderKeyRefusal (chat.tsx, envelope-verbatim twin of parseModelsRefusal) joins the revert-draft-and-open-setup interception; activity-dock reads 'needs a provider key'; dispatch-card shows 'Add a key' — both deep-link setup, which lands on stage 1 by itself. 4. Dictation trio: the token route's only 503 (missing Deepgram key) becomes the client-authored DICTATION_KEYLESS short line; the errored idle click clears the error and deep-links Settings -> Keys -> DEEPGRAM_API_KEY via the new open(section, row) seam (dialog landOnRow -> SettingsDialogBody initialRow -> KeysSection openRow follow-effect). Live-verified on :3001 under LH_IGNORE_ENV_KEYS=1 (the fresh-user switch): pill on all three surfaces + precedence over the CTA, stage-1 save -> auto-advance with dialog mounted, real 503 send refusal reverting the draft and opening setup, dictation 503 -> short line -> Settings lands on the unfolded Deepgram row with the error cleared, keys-section save and delete both flipping the pill live, and a fabricated refused research job rendering the 'Add a key' dispatch card. 926/926, tsc/lint/build green. |
||
|
|
9f0ecccc7a | Merge branch 'feat/platform' | ||
|
|
e4049e4bf8 |
Key-presence refusal at every gate: a pinned model is not enough
Owner-diagnosed: the turn gate checked only model coordinates, so a pinned slot whose provider key was deleted passed the gate and died mid-stream masked to 'An error occurred.' The key is now gated where the model is — by what would RUN (env-first keyValue truth, so an env-shadowed key passes). - core/settings.ts: the provider_key_missing envelope family (code, provider, slots, human line naming the provider from AUTH_PROVIDER_KEYS labels) + ProviderKeyMissingError + isProviderKeyRefusal matcher. - chat-handler turn gate: refuses 503 before beginTurn when the resolved conversation's provider key is gone (slots: [strong]). - Job runner: same check via an injected dep (agent layer cannot read the key store) — a keyless engine refuses the spawn through the existing failure-shape convention, no phantom run. - Extraction pipeline: refuseIfStagesUnready refuses BEFORE any batch (first offender: extractor before auditor); the trigger skips info- level, /api/graph/import surfaces the 503 envelope per file. - Tests: 8 new pins (turn gate both ways, runner refusal, pipeline first-offender + pass-through, envelope factory, matcher); the three turn-driving suites seed OPENROUTER_API_KEY like any real setup. - Doc fix: stale EXTRACTION.chunkTokens comment in the manual-extract route (left over from eed4b4e). Known residual (pre-existing, flagged for master): the manual extract route logs BOTH config refusals as console.error (fire-and-forget path); the coverage ledger keeps everything uncovered either way. |
||
|
|
90b74206cc | app icon: liquid-glass LearnHarness icon (Icon Composer PNG exports packed via iconutil) | ||
|
|
085fdc1a98 |
Payload slimming, pre-signing half: the asar stops shipping the world
The measured problem (330MB DMG / 1066MB app): electron-builder's
dependency collector packed the whole production dep tree into
app.asar (502MB) regardless of the files allow-list, because
normalizeFiles turns the array into one {filter} fileSet whose
entries ALL feed the per-dep copier (its implicit '**/*' passing
everything), and unpacked the native rungs into a 150MB
app.asar.unpacked. The launcher needs nothing but electron + node
builtins — the server ships via extraResources.
The fix (electron-builder.yml files): gitignore-style negation
ordering — '!**/*' first excludes everything, then only
electron/** and package.json re-include. The same list reaches the
per-dep filter, where app-builder-lib's destination-path quirk
(stat.moduleFullFilePath in util/filter.js) means no dep file can
ever re-include: all 566 modules collect zero files (DEBUG-mode
verified). Locale trim rides along (mac.electronLanguages: en).
Result on a fresh desktop:dist: app.asar 502MB -> 16KB (shell only:
electron/main.cjs, preload.cjs, package.json), app.asar.unpacked
gone, Frameworks 288MB -> 239MB, app 1066MB -> 367MB, DMG 330MB ->
144MB. Resources/server curation untouched (127MB, sharp native +
pdfjs traced and verified live).
Live verification against the packaged app: server spawn via
ELECTRON_RUN_AS_NODE on its kernel-assigned port, model slots via
PUT /api/settings/models, chat round-trip streaming an exact echo,
read tool on a PDF through the pdfjs path (exact text extracted),
image attach through the vision slot, GET /api/graph snapshot and
render-settings.
Docs full-accuracy pass (owner-approved scope): README +
docs/architecture.md swept of the dead embeddings / graph-context
injection / cluster-namer / semantic-clusters story (wave-7
casualties), MODELS/config roster rewritten to the BYOK slot
chain, EXTRACTION.chunkTokens -> graph.extractionChunkTokens,
route count 16 -> 25, steer_ctx_ dropped, seam table re-pointed at
the prober graph tools; docs/research/stt.md:50's "we already ship
transformers.js" premise corrected; config.ts orphaned EXTRACTION
doc block (left over from eed4b4e) deleted.
Note for master: npm prune still owed in the master tree (~690MB
stale @huggingface/onnxruntime trees absent from the lockfile);
worktree tree is clean and verified.
|
||
|
|
6896c23cc2 |
Session deletion dialog matches the node deletion dialog: red title, mono name below, checkbox flow
Owner polish round (2026-09-24): the two destructive-confirmation
surfaces must read as ONE design. The node dialog was the good one —
the session dialog now wears its exact look and flow.
components/delete-session-dialog.tsx (full rework):
- Red serif "Delete session?" (was neutral "Delete "title"?") — the
node dialog's destructive title, text-destructive on font-title 2xl.
- The session name below the title in JetBrains Mono at text-lg,
NO code-block chrome — the font already says code. (The node
dialog's name got the same treatment: text-xs bg-muted code block
dies, mono text-lg line lives. Owner chose below-the-title over
parens-in-the-title: long titles wrap safely below, never inside
the 25px headline.)
- TWO OPTION BUTTONS -> CHECKBOX FLOW: the locked two-button design
(preserve-graph / delete-nodes) collapses to ONE unchecked-by-default
checkbox "Also remove its graph traces" (accent-primary, the settings
panel's native-checkbox idiom) carrying the graph-touching option's
consequence copy verbatim. Unchecked = preserve-graph; checked =
delete-nodes. The unchecked promise ("everything it added to the
graph stays unless you check below") lives in the intro line now.
- Bottom row = the node dialog's exact pair: quiet Cancel + destructive
red Delete (border + text, fill on hover), "Deleting…" on the Delete
button while busy. Try-again re-derives the mode from the checkbox
(retry IS Delete pressed again; the checkbox is disabled while busy).
- DeleteSessionBody exported pure (the settings/node dialog body
pattern) for SSR pins; TRACES_COPY exported for the copy pin.
DELETE_OPTIONS dies with the two-button design.
Tests: session-context.test.tsx's two-option pins rewritten for the
node-dialog shape (red title, mono name no-chrome, unchecked checkbox
default, red Delete pair, old testids absent); delete-node-dialog
pins the chrome-less text-lg name. 905/905. Live-verified on :3001:
both dialogs render 25px red serif titles, 16px mono names, matching
red Delete buttons; Esc closes both, nothing deleted in verification.
|
||
|
|
73027f49bf |
Cost channel cull: the cluster-naming fold dies — two stages, two channels
Off UI's d651f93 flag (displayCostTotals already isolates the display to extraction + audit; the naming stage row and naming runs were filtered out client-side). This culls the server side of the same ruling. THE STANDING RULING, REVERSED BY ITS OWN PREMISE: NamingRunUsage / readNamingRuns / appendNamingUsage were kept at the wave-7 namer kill ONLY because cost-summary's HISTORICAL naming stage read them. With the fold culled, that premise dies — so the whole ledger read path goes too, following the wave-7 embeddings precedent chain: consumer-less ledger seams are deleted, and the on-disk naming-runs.jsonl stays INERT (real trees keep their historical spend bytes; they are simply not displayed anywhere — the same treatment as historical stages.embedding spend). - core/cost-summary.ts: two channels in (chat + notes front doors), two stages out; RecentRun.kind collapses to "extraction" (labels field gone); CostSummaryInput.naming gone; totalDollars = extraction + audit. - ports/store/contracts/config: appendNamingUsage + readNamingRuns ports, store impls + guards, the NamingRunUsage interface, and LEDGER_FILES.namingRuns die. graphRevision no longer counts the file (a smaller revision value is harmless — reads never compare). - components/graph-view.tsx (mechanical, flagged): the defensive .filter(run.kind !== "naming") guards a kind that no longer exists. - Tests: cost-summary's both-channels fold pins; the naming fixtures and the appendNamingUsage store pin die with the seams. |
||
|
|
2cba283009 |
Graph cost data drops the dead cluster-naming channel from the UI
The cluster namer is dead (owner refinement round, 2026-09-24): cluster labels are just session titles / file names now, coined without an LLM, so there is no reason to keep tracking naming spend in the cost UI. components/graph-view.tsx (UI-only; the core fold is untouched): - NEW pure export displayCostTotals(costs) = extraction + audit dollars and unpriced counts. Both the cost pill and the panel headline read THIS — never GraphCostSummary.totalDollars, which still includes the historical naming channel — so the two can never disagree. - GraphCostPanel's stage rows lose the "Cluster naming" line; the headline and unpriced counts come from displayCostTotals. - The recent-runs list filters out kind === "naming" runs entirely (the "N labels" tail died with them). - Mount comment updated: the fold covers chat + notes extraction runs. Note for master/Platform: src/core/cost-summary.ts still folds naming-runs.jsonl into stages.naming / totalDollars (Platform's lane). The UI no longer reads those fields; Platform can cull the seam and the dead ledger whenever convenient — displayCostTotals already isolates the display from whatever the core returns. Pinned in tests/graph-view-filters.test.ts: displayCostTotals with a naming stage of $9.99 + 2 unpriced proves the display sees extraction + audit only ($2.00, 1 unpriced). Live-verified on :3001 — the panel renders Extraction and Audit rows, no naming row, no naming runs in the recent list. 905/905. |
||
|
|
8c50b50749 |
New-session page reads like a pre-session: no phase chip, outlined bar, opening-question placeholder
Three owner fixes for /chat/new (refinement round 4, 2026-09-24) —
the page must stop looking like a session that's already running:
1. NO PHASE CHIP: the header's <PhaseChip phase="probe" /> dies —
no session has started, so no phase exists yet. Active sessions
keep their chip (chat.tsx's derives from the live timeline).
2. TOP BAR STRUCTURE MATCHS SESSIONS: the header gains the same
border-b border-border as the chat header (h-15, lh-drag,
60px). The floating-bar look is gone; /chat/new now reads as
the same shell with empty content.
3. COMPOSER PLACEHOLDER = THE OPENING QUESTION: active sessions
keep "Ask, answer, or steer the lesson…" (now the DEFAULT of a
new Composer `placeholder` prop), but the fresh page asks what
the graph view's new-session input asks — "What do you want to
learn?" No lesson is in flight, so the composer must not read
like one is.
The ConversationEmptyState intro card ("The teacher will open by
probing…") stays, per owner confirmation.
Live-verified on :3001 /chat/new: no phase-chip node, computed
1px header border rgb(67,67,67) at h-15/60px, placeholder reads
"What do you want to learn?"; the empty-state card intact. Unit
test pins the default session placeholder AND the override.
903/903.
|
||
|
|
4936b776ab |
Scroll-down disc rides the dock: anchored above the composer, never over it
The scroll-to-bottom disc (ConversationScrollButton) was the scroller's last child — DOM order painted it ABOVE the dock overlay, so it floated at the scroller's bottom edge, partly covering the composer (owner refinement round 3, 2026-09-24: it must ALWAYS sit above the composer, never over any element, and stay positioned with respect to the composer — which grows a lot taller when a question owns the band). The fix is a DOM move, not a coordinate: the button now renders INSIDE the DockOverlay, as a sibling of the DOCK container within the dock block — `absolute bottom-full mb-2`, anchored to the BLOCK's top edge. Because the anchor is the block itself (not the scroller's bottom): - it sits above the composer shell AND the stats row, in the gradient's transparent band; - it rides every height change LIVE: typing growth, a pending question expanding the band (verified: the disc moves with the 393px question form, 9px clear), the 180ms fold morph to the slim bar (8px clear of the folded bar — no overlap mid-morph either, height animations reflow per frame); - pointer-events-auto (the overlay is none by default); the isAtBottom/scrollToBottom context still reaches it — DockOverlay is a Conversation child. The use-stick-to-bottom pattern comment updated; the shared ConversationScrollButton component itself is untouched (positioning arrives via className, twMerge swaps bottom-4 → bottom-full). Live-verified on :3001 with a tall transcript, all three states: chat mode (15px clear of the composer textarea), expanded question (9px clear of the form top), folded slim bar (8px clear). 902/902. |
||
|
|
401a21e83f | teaching methodology: theory doc + step-unit teaching loop + minimal-tree planning + click-as-collapse philosophy (owner theory, 2026-09-24) | ||
|
|
f3c075fd2c | Merge branch 'feat/ui-polish' | ||
|
|
7163544134 |
Ask-dock keyboard round: Esc escapes Other typing, Space deselects, bigger multiselect check
Two owner fixes (refinement round 2, 2026-09-24) in the question dock's ask questions (quiz untouched): 1. ESC ESCAPES THE OTHER TYPING: the ask's "Type your own answer…" row auto-focuses its input on selection, and once in it there was no way out — arrows moved the caret (the typing-surface guard), so keyboard navigation was stranded. Esc in that input now blurs it (focus falls to the page) and pins kbdIdx to the Other row, so: the row STAYS selected (deselection is a deliberate act, never a side effect), the typed text survives, the arrow walk resumes immediately, and the row stays the walked row. stopPropagation keeps the window chain from reading this Esc as abort-turn. 2. SPACE DESELECTS A SELECTED OTHER: the walk's Space on the Other row selected it; now Space on an already-selected Other CLEARS the pick — focus never drops back into the typing field (the core's toggleOther has no single-select off-path; the off-path lives in kbdAct, keeping `selected` intact in multi). Re-selecting — walking back and Space, or a click — is the way back into the field. Clicking the selected row keeps its old meaning (choose Other); only the walk's Space gained the off-path, per spec. 3. MULTISELECT CHECK GLYPH BIGGER: the ✓ inside the multiselect checkboxes drew at the 11px text-xs rung — small next to the 16px box. Now text-base (14px on this app's ladder) + leading-none. The single-select dot and the quiz's rows are unchanged. (Pinned live: 14px computed in the 16px box; the span only mounts on a checked row, so no static pin — noted in the render test.) Live-verified on :3001 (fabricated ask chats, keyboard-only flows): select via Space focuses the input; type; Esc → focus BODY, row still selected, text kept; Space → deselected, focus stays out; re-select → input focused again; listed option displaces in single-select. Multi: Other coexists with checked options through the whole sequence. 901/901. |
||
|
|
9f7dd1a7e9 |
Refinement round: ring popover down, menu order, tok unit, list indent, lowercase levels
Five small fixes (owner refinement round, 2026-09-24):
1. RING POPOVER OPENS DOWN: the ring lives in the chat top bar —
the window's top edge is right above it, so the upward popover was
clipped. top-full mt-1.5 now: it drops below the ring.
2. SESSION MENU ORDER FLIPPED: Rename/Delete first, hairline, then
the extraction coverage block + Extract at the bottom — the info
reads after the actions (was the reverse, the block on top).
3. UNIT TYPED OUT: the sidebar block reads "2k tok uncovered" now —
the tokens unit is explicit (owner: "tok").
4. MARKDOWN LIST INDENT: streamdown ships list-inside with no
container padding — top-level markers sat ON the text edge (only
nested lists got their pl-6 via [li_&]). .lh-prose :where(ul, ol)
{ padding-left: 1.5rem } indents both list kinds as blocks, 24px —
the same measure as the nested-case pl-6. Unlayered CSS beats the
utility layer; :where keeps specificity zero.
5. THINKING LEVELS LOWERCASE EVERYWHERE: the composer pill and setup
dialog rendered THINKING_LABEL (Off/Minimal/…/Max) while every
menu body renders the raw lowercase level — the map is now
lowercase, the level string IS the display form, everywhere.
Live-verified on :3001: list content inset 24px vs paragraphs
(padding-left computed 24px, nested fine); the composer pill reads
"low" with options low/high/max; menu order Rename → Delete… →
hairline → block. 901/901.
|
||
|
|
3327309e8b |
The all-sessions coverage GET dies: the per-session block is the coverage surface
Extraction-UX close-out interlude (master dispatch 2026-09-24, off UI's ff1ec7a confirmation): nothing client-side consumes GET /api/graph/coverage — the sidebar block and the chat-bar ring both read GET /api/chats/[id]'s coverage block. The dead route (app/api/graph/coverage/route.ts) is deleted; the manual-extract front door (POST /api/graph/coverage/extract) stays by design. No server helper was orphaned — the route was composition-only over shared seams (chatStore, extractionStatuses, stores, coverageOf). The sessionCoverage doc + the chats/[id] route comment now point at the per-session block as THE coverage surface. Pin (tests/coverage.test.ts): the route file's absence (prompt-overrides' structural-pin precedent) + the Extract POST's survival — a merge cannot resurrect the dead surface silently. |
||
|
|
bc1f668e2a |
Extraction surfaces: sidebar coverage block, chat-bar ring, phase un-pill, panel death
The owner-locked extraction-UX redesign (dispatch 2026-09-24), UI chunk over Platform's data layer (eed4b4e — GET /api/chats/[id] carries `coverage`, the threshold is Graph ▸ extractionChunkTokens): 1. SIDEBAR BLOCK (components/extraction-coverage.tsx + sidebar's session menu): the coverage info block at the TOP of the context menu — uncovered tokens, uncovered messages, running state — then the Extract affordance (optimistic "extracting… / Running" until the server says running; 409 = benign, the trigger raced us), fetched ON MENU OPEN (the block mounts with the menu — on-demand, never eagerly), polled at 2s while a run is in flight. Visually separated from Rename/Delete by a hairline; those stay the two session-management actions. Failed read → nothing (never fake zeros). 2. RING METER (CoverageRing, chat top bar, before the PhaseChip): a 16px donut filling uncoveredTokens / extractionChunkTokens — "how close this session is to its next extraction run". Click → the exact numbers (covered/uncovered tokens, messages, the threshold, lastFoldedAt via timeAgo). Refresh chains: mount, the turn-settle bump (chat.tsx coverageTick), the settings signal (threshold/kill-switch re-read live), a 2s drain-poll while a run is in flight, and the ACTIVITY FEED — a run for this session appearing or settling in the dock's feed re-reads coverage (runs fire from other surfaces — the sidebar menu, the turn pipeline — and no turn-settle bump announces them; the runs-empty-transition the graph view rides). Vanishes entirely when the graph is off or before the first honest read. Running = the whole ring PULSES (implementer's visual call: motion is the working-state signal in this app's quiet register). 3. PHASECHIP UN-PILL: the phase label is a flat tag — no border, no rounding, no padding. The Plan chip is the only button-shaped thing in the top bar. Drag-surface (pointer-events-none) and aria semantics unchanged; the transcript PhaseDivider keeps its look. 4. PANEL DEATH: components/graph-coverage.tsx, its graph-view mount, and tests/render-graph-coverage.test.tsx die whole. Confirmed: after this, NOTHING client-side consumes GET /api/graph/coverage — the only remaining client use of the coverage endpoints is the Extract POST (kept, by design); the routes stay (Platform's call). Pure seams exported for tests: ringFill (clamp math), CoverageMenuBody, CoverageRingGlyph, CoverageRingDetail. 901/901. Live-verified on :3001: ring fill arcs match the API byte-for-byte (483/4096 = 4.45/37.70; 1991/4096 = 18.32/37.70); the sidebar block reads "2k uncovered / 4 messages unextracted"; a real Extract run went optimistic → dock run → menu settled "fully extracted" AND the ring drained to the empty arc (the feed chain); graph off → the ring vanishes; the flat phase label renders chrome-free beside the Plan pill. |
||
|
|
2e9fd01c0c |
Extraction-UX data layer: the chunk budget becomes a graph setting; the chat view gets a per-session coverage read
Owner-locked extraction-UX redesign (dispatch 2026-09-24), part 1 (data
layer; the UI chunk follows).
THRESHOLD -> SETTING
- config.ts's EXTRACTION.chunkTokens dies; the default moves into
FACTORY_SETTINGS.graph.extractionChunkTokens (4096, the old constant's
value). GraphSettings gains the optional field, Required at resolution
via the standard ?? FACTORY pattern — pre-redesign records resolve the
default cleanly.
- FLOOR 500 (the implementer's call, documented on the field): every run
pays the graph-carrying prompt overhead out of the write channel's
money — a smaller budget fires penny-batches that cost more per
extracted token than the content is worth. The sanitizer CLAMPS up
(bd68486 clamp-never-reject discipline: fractions floor, junk drops);
the settings PUT REJECTS below-floor with the rationale in the message.
No ceiling: a large budget just delays extraction — the user's stated
cost/thoroughness tradeoff, not a misconfiguration to police.
- The trigger (turn-service.triggerExtraction) takes the budget as a
parameter — chat-handler passes effective.graph.extractionChunkTokens,
the fresh per-turn read, same as every other gate.
- The pipeline's BATCH side threads through the same number:
ExtractionPipelineDeps gains an OPTIONAL getChunkTokens getter with the
factory default (Teacher's 24 test fixtures stay untouched; the two
call-site lines inside extraction.ts are the only cross-band edits,
flagged). chat-handler + backfill/import-notes wire the live
effectiveSettings — trigger and batches share one budget, always.
PER-SESSION COVERAGE READ
- sessionCoverage(chatId) (server/chats.ts): the chat view's ring-meter
block — totalMessages/coveredCount/uncoveredCount/coveredTokens/
uncoveredTokens (the same coverageOf derivation /api/graph/coverage
serves, scoped to one chat), running (chat-kind extraction registered
for this id), and lastFoldedAt (the chat's newest coverage row's fold
— a cheap single pass; null before any fold). GET /api/chats/[id]
carries it as coverage: {...}; unknown chat -> null (the route 404s
first).
- /api/graph/coverage (the all-sessions list) compiles untouched — the
per-session read is a scoping of it, not a supersession; the UI's
sidebar/ring chunk decides which surface it consumes.
Tests: factory/sanitizer pins (clamp-up, fraction-floor, junk-drop),
resolution pins (absent -> 4096, stored 2000 wins), settings-body pins
(valid/absent/fraction/below-floor 400/junk 400), sessionCoverage pins
(derived split, newest-fold scoping, cross-session isolation, running via
the registry, null on unknown chat). 893/893.
|
||
|
|
b9397fa783 | formatting chunk: renderer-awareness opener — full markdown set available, content determines presentation (LaTeX/mermaid verbatim) | ||
|
|
81709afd22 |
Stats-bar catalog join + dock numerator consistency (context-meter close-out)
Platform's context-meter pair (04e336d, 18cc493) left two client seams in my band; this closes the arc: 1. STATS-BAR CATALOG JOIN: contextWindow's denominator resolves ONLY from pi's live catalog — stats-bar now builds the provider:id -> contextWindow index client-side (catalogLimits in model-picker.tsx, the client twin of the server's catalogContextLimits join) over the picker's existing /api/models fetch/cache (useModelCatalog exported; module-level dedupe, one fetch feeds both ModelControl and the meter). Catalog pending or failed -> undefined index -> limit null -> USAGE-ONLY (Platform's interim render IS the null path; the honest-render precedent, never a fake window). 2. DOCK NUMERATOR CONSISTENCY: toBlobs' context fold was still last-step input+output only — now the same cache-inclusive footprint the session meter uses (all four directions; provider "input" EXCLUDES cacheRead/cacheWrite, so the old fold read wildly low on cache-heavy jobs). Same metric everywhere. Review of Platform's interim edits in this band: verified as-shipped — stats-bar's usage-only render (summary + popover), FeedJob.contextLimit with the line-hides null treatment, and the render-test rewrites all fit. Only fix needed: none beyond the above. Tests: catalogLimits pinned pure (index shape, provider:id keying, missing-model absence); toBlobs pinned on the cache-inclusive numerator (10k+0.5k+120k+30k = 160.5k against a 200k feed limit). 889/889. Live-verified on :3001: seedplan's bar reads "18.7k / 1M" (footprint 18725, catalog 1048576); a nope/unknown-model chat renders "12.8k" usage-only with cost "-"; the popover detail matches; the dock blob's context line renders "946 / 1M" from a server-joined feed (an unintentionally real runner-adopted job — the full chain, live). |
||
|
|
4ded547cb5 |
Context-meter amendment: CONTEXT_LIMITS dies — pi's live catalog is the only limit source
Owner ruling (2026-09-23, amendment to 04e336d): no hardcoded fallback exists anywhere. contextWindow(record, limits) resolves the denominator ONLY from the live index (provider:id -> contextWindow straight from pi's catalog); a model missing from it (historical session, package update, failed fetch) reads limit null and the meter renders usage-only — the honest-render precedent, never a fake window. - src/core/prices.ts: the 1354-entry CONTEXT_LIMITS table (65KB) and its generation apparatus deleted; the fold returns limit: number | null. PRICES untouched (regen verified byte-identical via the reworked sync-pricing script). - scripts/sync-pricing.ts: generates PRICES only; contextWindow leaves the priced map, the guard, and the template. - The dock meter's owner-selected treatment: the activity feed carries the limit server-side. catalogContextLimits() (server/model-catalog.ts) builds the index over the same keyed providers the picker federates; activityFeed() joins it onto every running job (ActivityJob.contextLimit, null when the model is missing — the dock reads it off the feed, no client-side table, no regression while the UI's catalog-join chunk lands). - components/activity-dock.tsx: toBlobs reads job.contextLimit (import of CONTEXT_LIMITS gone); stats-bar renders usage-only on null limit (mechanical interim — UI stream's join chunk follows). - tests: usage.test.ts pins the no-fallback contract (index hit / absent / empty / missing-model -> null); activity-feed.test.ts pins the server join (catalog model -> 1048576, unknown -> null, no-usage -> null); render pins updated to the interim usage-only render. |
||
|
|
b68a16fdb4 |
Context-meter fold: cache-inclusive numerator, live-limits denominator, zeroed-step fallback
The owner's context-meter audit (2026-09-23), three fixes in the pure contextWindow() fold (core/prices.ts): - NUMERATOR: cache tokens count. Provider "input" excludes cacheRead/cacheWrite, so input+output read wildly low on cache-heavy sessions (40k shown vs ~190k real). The fold is now pi's own totalTokens semantics: input + output + cacheRead + cacheWrite. The fields were already persisted in every CallUsage (chat-handler's capture seam); non-caching providers are unchanged. - DENOMINATOR: the live catalog wins. contextWindow() accepts an optional live-limits index (provider:id -> contextWindow — the shape the client already holds from GET /api/models, where toModelDto projects pi's Model.contextWindow); CONTEXT_LIMITS, the generated duplicate, is demoted to fallback for models the live catalog doesn't carry. The UI's stats-bar join passes its catalog index; the call is unchanged when omitted. - RIDE-ALONG (honesty guard): a zeroed/aborted last step falls back to the previous step's footprint — an abort artifact is not a reading. All-zero runs return null (no meter, never a bogus tiny number). Tests pin all three: the cache-inclusive fold (and the plain no-cache fold unchanged), live-index-beats-table + table fallback + unknown-to- both null, and the zeroed-step fallback (previous step wins; all-zero is null). 885/885 tests, typecheck/lint/build green. |
||
|
|
f7a04f6c02 |
Default bold ink #c96b4a -> #ee928c
Owner default change (2026-09-23): the factory strong-color token moves to the softer coral. The factory lives in the stylesheet token (both theme blocks in globals.css) - settings' boldColor:null means "factory" by design, so no record/schema change. The settings row hint + placeholder follow, plus the placeholder test and two comments that cited the old literal. Live-verified: unset boldColor renders rgb(238,146,140) (#ee928c) on the real <strong>; dialog hint reads "Empty = #ee928c". |
||
|
|
fb72b4fee3 |
Bold renders the real element: the Bold color setting applies
The owner's repro: the appearance Bold color never applied anywhere. Root cause: streamdown's default `strong` component renders bold as <span class="font-semibold" data-streamdown="strong"> - NOT a <strong> element - while the app's ink plumbing (.lh-prose :where(strong, b) in globals.css, zero specificity by design) matches strong/b ELEMENTS. So bold text never took --lh-ink-strong: not the factory #c96b4a, not any settings boldColor (it rendered plain foreground at weight 600). Italic always worked because streamdown leaves `em` native - hence the asymmetry the owner noticed. Fix: a `strong` override in mdComponents (the stable module-level set, so Streamdown's memoized tree keeps identity) renders the semantic element: <strong data-streamdown="strong">. Screen readers announce emphasis again, the designed token rule applies unmodified, and the weight reads 700 (the .lh-prose rule) instead of the span's 600. Both Streamdown surfaces (MessageResponse, reasoning) pass mdComponents, so every markdown surface is covered. Verified live: bold in seedplan now renders #c96b4a at weight 700 in a real <strong>; after PUTting boldColor #7dd3fc the live strong flips to rgb(125,211,252) with no reload; italic unchanged (#6fa9d6 native em). Scratch record restored to unset. |
||
|
|
10be308ab6 |
writePositionMap: tmp + rename — the same crash-safety rewriteLedgers uses
Flag routed from the UI stream's rename-shuffle fix (a9339b3): the
positions ledger was the one remaining plain writeFileSync among the
rewritable ledgers — a torn write (crash mid-write) could replace it
with a half-written file, and readPositionMap parses a failure to {}
(honest empty, but the resting layout would be silently lost). The
tmp+rename pattern from rewriteLedgers, ~4 lines up the file: write a
uniquely-named temp, rename over the ledger — a crash leaves either the
old ledger intact or the new one complete, plus an orphan .tmp-*.
879/879 tests, typecheck/lint/build green; live-verified via the node
deletion's positions prune (no temp residue, valid JSON).
|
||
|
|
e826088cc8 |
Rename-shuffle fix: label-only channel + three hygiene items
The live-rename interlude routed label changes through a fresh snapshot object, which flowed setData -> geometry rebuild - and the rebuild treats the last-fetched position ledger as authoritative and cancels any pending drag commit, so a rename visibly rewound the whole graph whenever display had drifted (recent drag, mid-anneal, physics re-anneal). - Renderer gains setClusterLabels (a setPhysicsTuning sibling): pure applyClusterLabels patches the data-side cluster records and the built region meta (re-wrapping the changed entries with the same measure the build uses), markDirty only. Never touches nodes, the sim, positions, or the pending commit. The hull-silence guard travels inside it: a null-label cluster is a gone record - no rename can reach it. - The lh:chat-renamed listener stops building snapshot objects: renameSessionsPatch writes the sessions entry in place (pure display state - the detail card's provenance line), the cluster label goes through the new channel, and a tick bump re-renders the view. Identity preserved everywhere -> the [snapshot]-keyed setData feed cannot fire. - Hygiene (a): a fresh sim starts at alpha 1 and never decays while stopped - the first post-rebuild drag re-annealed the whole graph at full strength. buildSim now initializes alpha at the settle floor, so first-drag matches steady-state drag. setPhysicsTuning still rehearses alpha(1) when a full re-anneal is the point. - Hygiene (b): nodes missing from the position ledger used to dump at (0,0) via the ?? 0 fallback. seedMissingNodePositions seeds them on a deterministic ring around the known positions' centroid (FNV-1a hashed angle, golden-angle spiral offset). - Hygiene (c) is store-band (writePositionMap's writeFileSync -> tmp+rename): flagged for Platform, not crossed. Tests: applyClusterLabels suite (patch + re-wrap, hull silence, no-op, explicit null, unknown ids), renameSessionsPatch identity pins (sessions map and clusters array identity preserved - the contract that keeps setData from firing), seedMissingNodePositions determinism + ring math. Live-verified: mid-anneal rename shows a 4-world-unit max disc jump (no teleport), the commit PUT survives the rename, rest renames leave the disc build byte-identical, and a held first drag displaces discs ~14 units instead of re-forming the graph. |
||
|
|
976be50a81 |
Polish round: node-delete chrome + rename alignment
Owner polish dispatch (2026-09-23): - Right-click no longer pins a node or opens the detail card: the canvas pointerdown/up handlers now gate on the primary button, so the context menu arrives alone. - Node context menu: label is just "Delete" and the shell hugs its content (w-44 -> w-fit); the menu body keeps focus for arrow cycling but never shows the browser's default focus ring (outline-none on the MenuShell body - applies to every menu). - Delete confirm: "Delete node?" title in CMU Serif, red (text-destructive); the node name sits below in JetBrains Mono inline-code; the Delete button carries the destructive treatment (border + tint, fill deepens on hover). Rest of the dialog stays neutral. - Sidebar rename row: text and metadata stay at the idle row's x - the status spacer and the input's border/padding are gone; the underline is the only editing cue. |
||
|
|
63b2756465 | Merge branch 'feat/sub-agents' | ||
|
|
d75867246d | prompt rechunk: neutral base (philosophy + principles + accuracy), shared voice/mechanics/files/formatting fragments, phase-owned question methodology, teacher 30-60 renumbering | ||
|
|
92943864c4 |
Node deletion affordance (node interlude): right-click menu + full-death confirm
Platform's 50bfcba engine, surfaced. Three pieces: 1. THE RIGHT-CLICK (graph-canvas.ts): a contextmenu listener — always preventDefault (the native menu on a full-bleed custom canvas is never useful) — picks the node under the pointer and hands the canvas-relative position to a new onContext callback (null on empty stage). pickAt already respected the visibility filters: a hidden node cannot be right-clicked, same as it cannot be clicked. The view opens a MenuShell anchored at a zero-size point-div positioned at the pointer — MenuShell's placement machine measures it like any element, so the menu gets the full discipline: placement, focus on open, arrow cycling, honest-fit flip. Delete… is its one action; the dismissal contract is the sidebar's (wave 6): outside-click / Esc (claimed) close, and a camera move closes it too — a menu floating over a moved graph is anchored to nothing. 2. THE CONFIRM (delete-node-dialog.tsx, portaled z-50 like the session-delete dialog whose tone it follows): the pre-flight impact read (GET /api/graph/nodes/[id]) renders the plain facts — link count, quiz-answer count, session-note count (zeros stay visible — hiding a zero fact would understate the blast), and the referencing sessions with display names joined CLIENT-SIDE from the snapshot (chat titles; imported-notes file paths; unknown ids honestly themselves). "This cannot be undone" says the permanence. The 409 extraction_running renders the dock phrasing (EXTRACTION_RETRY_LINE — the server names the extraction, the ACTIVITY DOCK is the surface the user actually watches drain) with a Try-again; every other failure shows the server's line verbatim. The honest 404 (deleted elsewhere meanwhile) states it plainly and its Close rides the same refresh. The pure body is exported separately (the settings-dialog pattern) — SSR pins everything the live dialog shows. 3. SUCCESS: onDeleted dispatches lh:graph-revision with the returned graphRevision — the wave-6 chain refetches snapshot + costs. The detail card closes if it pointed at the dying node (the refetch would close it anyway; the same frame should not show a card for a node the user just killed). Clusters shrink / unrender below the minimum on the server's read — the client just renders the refreshed snapshot. Tests: 867/867 (+8 — the join helper, honest plurals, the 409 phrasing pin, the dialog body at loading/gone/impact/failure states). Live-verified on :3001 against the real engine, scratch data: impact facts match the API, the intercepted 409 shows the dock line and Try-again recovers into a real delete, the three-member cluster died below MIN_CLUSTER_SIZE server-side, the labeled seedplan cluster + its canvas label died with it (cold load draws nothing stale; the in-session refresh refetches and the dying node's label stops drawing), other nodes survived, and the full menu contract holds (empty stage → no menu; Esc, outside-click, camera move all close). |
||
|
|
2d133f1133 |
Single-node deletion: full death by node id (node interlude, owner-locked)
The remedy for undeletable nodes (preserve-graph orphans from session deletion; notes-import nodes with no chat behind them): DELETE /api/graph/nodes/[id], full-death semantics — the user pointing at the node IS the intent; references from other sessions do NOT preserve it. - core/graph-deletion.ts deleteNodeTraces: the node's every line, every edge touching it, every quiz/observation event naming it, its mapping lines, and its resting position — one synchronous atomic rewriteLedgers call; only the ledgers that changed rewrite (mtimes stay honest). Blast radius is exactly the rows naming the node id; notes source records stay (deleting a proposition is not deleting the file). Derived data self-heals on read: the birth cluster shrinks below MIN_CLUSTER_SIZE and stops rendering. - nodeDeletionImpact (GET /api/graph/nodes/[id]): the confirm dialog's pre-flight facts — edge count, quiz/observation history, referencing sessions. The snapshot's edges + nodeSessions cover part of this, but the raw event history was nowhere on the client — hence the read. - Guards (server/graph.ts deleteGraphNode), decided + documented: NO turn/jobs gates (they never write graph ledgers; the orchestration is synchronous end to end, nothing interleaves). THE EXTRACTION GATE: 409 while ANY pipeline run is registered — a mid-flight run holds pre-deletion findings and could re-reference the dying node. The run is never aborted (its spend belongs to another session — outside this deletion's blast radius; the UI retries once the dock drains. A run started after the deletion is legitimate re-learning: fresh ledgers, fresh node id, and mergeFindings' existingIds guard ignores linkTo ids that no longer exist — no dangling edges, no resurrection. Live-verified on dev data: impact read, full-death stats, honest 404 on repeat, and the file cluster fell 3→2 and unrendered itself. 859/859 tests, typecheck/lint/build green. EOF ) |
||
|
|
7479dce8ef |
Wave 7 interlude: prober tool-call chrome + live rename cluster labels
Graph tool-call visibility — the prober's list_sessions /
list_propositions rendered like nothing happened in the transcript
(the registry default is silence for unregistered tools). They now join
the EXACT read/ls/find/grep discipline: artifactRenderers maps both to
the ToolCallCard — collapsed one-liner, expandable Input/Output card.
The results ride the card's string path verbatim (the tools' own
compact name/level/date line format), exactly how read results render.
- list_propositions gets a headline arg: the session id is the call's
subject — the collapsed line reads "list_propositions chat_…".
Shown as given, never resolved (the same no-resolution rule as
read's path). list_sessions carries no arg (empty input — the
ls-with-no-path discipline).
- No bespoke row-layout card: the outputs are pre-formatted strings
(the model-facing format IS the learner-facing format here), and
the dispatch's "SAME look" directive settles the shape question.
Live rename labels — renaming a session anywhere (sidebar inline row,
chat header) already announces lh:chat-renamed {id, title}; the graph
view now listens. The birth-session partition means the title IS the
cluster label, so the patch is targeted and in-place, no refetch: the
snapshot's sessions map (the detail card's provenance line) and the
one matching cluster. The new snapshot object flows the existing
setData → geoVersion → rebuild chain, so the wrapped label (wave 7's
wrapRegionLabel) re-wraps for the new text. Unlabeled clusters keep
their hull-only silence — a null label is a GONE record, the guard
holds the line.
Tests: registry pins for both tools + the prober-card render (collapsed
headline, verbatim rows, no-arg discipline). 855/855; live-verified on
:3001 with fillText instrumentation: both cards render collapsed and
expandable in the transcript, and a sidebar rename flipped the canvas
cluster label old→new with the old label's draws stopping cold.
|
||
|
|
7a17230f26 |
Wave 7 close-out: the graphTools surface, wrapped cluster labels, the orphan sweep
Part 1 — the surface:
- Settings ▸ Agent ▸ Tools gains the prober GRAPH-TOOLS row as its FIRST
row (owner ruling: in the Tools section, not the tool-toggles list —
its subject is the map, its mechanism is agent capability). Writes
agent.graphTools {enabled} (absent = on, the toggle discipline) via
the section PUT; composeGraphTools pure seam. Its own key in the agent
contract — never in the tools map.
- The row carries the search-row hint discipline: "Connects prior
sessions' knowledge into new ones — the prober reads the knowledge
map" (owner wording). The honest interplay composes both: when
graph.enabled is off the row dims + disables with a "Graph is
disabled" hint (the effective gate itself is server-side, per-turn
fresh). AgentSection reads the graph section (record.graph, passed by
the dialog) — read-only.
- Cluster labels wrap: wave 7 made labels session names and FILE PATHS,
which the old single-line fillText could not bound. wrapRegionLabel
(pure, exported): word-wrap first, char-split long paths (the
node-label discipline), capped at 3 lines, overflow folded into an
ellipsized last line (owner: wrap, cap 2–3, ellipsize). Pre-wrapped
once per geometry build at the SCREEN-fixed size (viewer-relative
labels — the wrap never goes stale mid-camera-move), drawn centered
line-by-line at the centroid. Deleted-session clusters keep their
hull-only honest silence (label null → no lines).
- The orphan sweep: StatsBar's leftSlot prop is dead (its one consumer,
the knowledge-context slider, died in c690181) — removed, and pre-chat
views now render NOTHING on the left (owner ruling this round: a
"— · —" summary over an empty popover is pure noise) — the dictation
middle just widens. Stale knowledge-context comments swept in
chat.tsx (adoption-window + activity-nudge docs), activity-dock.tsx
(useActivity ground rules), dictation.tsx (×2 adaptive-meter docs),
home.tsx (pre-chat dock comment).
Part 2 — Teacher's c690181 review: clean. Imports, KIND_ICON/KIND_LABEL
entries, render branches and event seams all excised whole; the probe-
handoff branch re-indented intact and merge-adopted; tests updated in
lockstep (merge-adopted, record-adapter, render-new-session retired).
Platform's 4b60e0f roster-string fixes (labeler dropped from both
SLOT_META blocks) reviewed — consistent across agent-section +
setup-dialog.
Gates: tsc clean, 854/854 (8 new: wrapRegionLabel cases, graphTools
SSR rows ×3; stats-bar + ladder fixtures updated to the new rulings),
lint 0, build ok. Live on :3001: graphTools row first-in-group with
hint, off→{enabled:false}→on round-trip, graph-off dimming + hint +
disabled toggle, pre-chat rows summary-free, chat view keeps its
summary, long-title cluster label wrapped without console errors
(visual read unavailable this session — pure wrap is unit-pinned).
|
||
|
|
d1b46132b9 |
Wave 7 close-out: the embedding ledger seam dies; notes birth via mapping lines
Part 1 — the ledger + remnants interlude (master-dispatched, owner ruling 2026-09-23: nothing configures the graph beyond chat models): - The embedding ledger surface is gone: readEmbeddings/writeEmbeddings ports, the EmbeddingsLedger contract, EMBEDDINGS_FILE (config, store impls, graphRevision source list), and graph-deletion's vector-pruning step. On-disk data files stay untouched (inert history). - The extraction embedder seam dies wholesale: ExtractionPipelineDeps drops the embedder dep (chat-handler + backfill/import-notes stubs go with it), the 'embedding' phase leaves ExtractionPhase, the usage capture/persist helpers die, and stages.embedding leaves ExtractionRunUsage (historical records keep their shape — reads are lenient). Extraction is two stages again, like the local-embedder era. - Consumer-less remnants: latestQuizOutcomes + QUIZ_EVENT_KINDS, clusterContentId, matchClusterLabelings + Community + ClusterLabeling, the readClusterLabels/appendClusterLabel port seams + store impls + LEDGER_FILES.clusterLabels. NamingRunUsage/readNamingRuns STAY (standing ruling — the cost pill renders historical naming spend); clusterHue + MIN_CLUSTER_SIZE stay (live). - The 'labeler' role dies: BACKGROUND_MODEL_ROLES, ROLE_SLOT, ModelRole, the catalog pin projections, and the UI roster strings. RolePins keys shrink with the union. The notes birth fallback (owner-approved 2026-09-23, surfaced by the interlude's live smoke: production notes imports create provenance-EMPTY nodes (importNotes passes messages: null — the file's anchor is the mapping line), so the provenance-only birth rule left every imported file unclustered and invisible to the prober's session list, against the locked file-birth ruling. A provenance-empty node now births under its FIRST mapping line's chatId (ledger order); only notes-born nodes ever hit the fallback (chat nodes always carry provenance; a re-encountered node's link-append re-opens the provenance rule). Deletion-safe: mapping lines of deleted chats are stripped alongside their provenance. Live-verified on port 3004: a fresh notes import clusters under its src_ id with the file path as the label, end to end with no embedder. 845/845 tests, typecheck/lint/build green. EOF ) |
||
|
|
1c3a5e121c | fix(tsconfig): exclude dist/ and .next/ build artifacts from typecheck — stale DMG residue in master-only dist/ reddened gates after wave 7's graphContextTokens removal | ||
|
|
6b13583e96 |
wave 7: the prober reads its map — graph-context injection dies whole
The owner's zero-configuration ruling (2026-09-23): nothing configures the graph beyond chat models. The prior-knowledge injection — its budget dial, its runner, its marker part, its dock/dispatch entries — is removed across all bands; the prober now reads the learner's persistent knowledge map on demand through two gated tools instead: - list_sessions: every birth session, names + kinds + counts + dates (graphSessionIndex, Platform's core seam) - list_propositions(session): what was born there + the session's own latest understanding level + that evaluation's date (graphSessionPropositions) — names/levels/dating ONLY, no statement text (owner ruling: the map's organization is the prober's business, its prose is not) Registered via createProberTools when chat-handler passes graphStore (the graphToolsEnabled gate decided server-side, per-turn fresh); the prober doctrine gains the map-review nudge at session start. Also: runner's JobKind special-casing collapses (KIND_ROLE/KIND_RUNNERS are plain records again), semantic-clusters.ts dies with its test, the embedder dep's doc tells the wave-7 stub story, and ports.ts's readClusterLabels/readNamingRuns surface stays untouched for Platform's ledger-port interlude. 848/848 tests, typecheck/lint/build green. |
||
|
|
a72753b059 |
Merge branch 'feat/platform'
# Conflicts: # scripts/import-notes.ts |
||
|
|
c86430e8bf |
Wave 7 chunk B: remove the embeddings machinery — the ladder collapses
The owner's wave-7 ruling: embeddings die entirely (git history is the
stash); clustering is birth-session derived (chunk A, 950ddfd), labels
are session names, and the prober's graph tools replace injection.
- settings: GraphSettings collapses to { enabled } — embedding,
embeddingModel, clustering, labeling all die. Migration story: stale
record keys are UNREAD and dropped by the sanitizer's reconstruction;
the next graph-section PUT (replace-whole) persists the collapsed
shape. The settings-panel GraphSection renders one master row.
- deleted: src/server/embeddings.ts (adapters, resolveEmbeddingPortFor,
embedNewNodes, embeddingPort, embeddingModelCatalog), the
GET /api/models/embeddings route, scripts/embed-graph.ts, tests for
all three, and the /api/models/embeddings picker surfaces in the UI.
- prices: EMBEDDING_PRICES overlay + callCost fallback die. cost-summary:
the embedding stage fold dies — back to three stages (extraction/
audit/naming) per the owner ruling; historical stages.embedding spend
in old run records is simply not displayed.
- ports: EmbeddingPort/EmbeddingResult/EmbeddingTask die. The embeddings
LEDGER surface (readEmbeddings/writeEmbeddings + EmbeddingsLedger +
EMBEDDINGS_FILE + graph-deletion's vector pruning) STAYS — inert data,
kept only so graph-context.ts compiles until Teacher's pass removes
it; the seam dies in the follow-up interlude.
- chat-handler (approved cross-lane unwiring, wave-6 precedent): the
extraction pipeline's embedder becomes a documented no-op (nothing
embedded, nothing spent, ledger untouched) until Teacher's pass
removes the phase; the graph-context injection dispatch gate, the
graphContext deps wiring, the setContextRespawn seam, and
turn-service's triggerGraphContext are gone — injection is dead
code pending Teacher's file deletions.
- scripts: backfill-graph/import-notes pass the same no-op embedder;
replay-layout derives clusters from birthSessionPartition (the same
partition the real settle uses) instead of the embeddings ledger.
- settings-body: parseGraphSettingsBody reads the master switch only —
old-shape keys in a body are ignored, not rejected; bad shapes 400.
- next.config: the @huggingface tracing-exclusion guards die with the
last reference.
Cross-lane flags: extraction.ts/graph-context.ts keep stale embed
comments + the embed phase until Teacher's full pass (their parallel
interlude owns those files — zero overlap kept by design);
semantic-clusters.ts + its test stay one more round (extraction.test.ts,
Teacher's file, still imports it — they delete it with the namer cases);
ChatSettings.graphContextTokens + the graph-context-control UI stay
until Teacher+UI.
Gates: tsc clean, eslint clean, 885/885 tests, build compiles.
Live-verified on the port-3004 dev tree: PUT /api/settings/graph with
dead keys collapses on write, bad shapes 400, GET reads {enabled: true},
/api/models/embeddings 404s.
|