Commit Graph
11 Commits
Author SHA1 Message Date
amosblomqvist 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.
2026-09-23 15:24:48 +03:00
amosblomqvist 7a3e19e95f feat(sources v1): the agent locates files itself — ls/find/grep + tool-call cards
User pivot (2026-09-19): no attachments UI at all. Pasting paths is native
for technical users and alien for everyone else - so the agent gets the
tools to find what the learner merely NAMES. The learner communicates in
messages; the agent locates and reads. Zero cross-stream seams: paths
already live in tool parts, so the sources "list" is the transcript.

- ls/find/grep: pi-verbatim schemas, descriptions, and output formats
  (fd's glob semantics, rg's path:line: match format, pi's truncation
  notices), ported to pure Node - no binaries, no shell-out, read-only by
  construction. Deltas from pi: home-anchored resolution (the harness has
  no cwd), .gitignore replaced by hard noise exclusions
  (node_modules/.git/Library/.Trash), 10s walk timeout with partial results.
- read: description swapped to pi-verbatim (doctrine moved to the prompt);
  relative paths now resolve against the learner's home like the rest.
- prompts/shared/05-learner-files.md: one minimal doctrine fragment
  composed into BOTH agents (composeInstructions grew a shared/ dir,
  before the agent's own fragments). teacher/30-jobs.md keeps only the
  pdf-dispatch addendum.
- ToolCallCard: the generic quiet tool-call card the user specced -
  collapsed one-liner (tool name + headline argument), expandable to full
  input/output, errors red, image payloads never render. Registered for
  read/ls/find/grep.
- ui-message.ts: TeacherUITools entries for the four tools (mechanical).
- tests: fs-tools suite (glob semantics, pi formats, exclusions, limits),
  registry/card renders, shared-fragment composition pins, tool-registry
  key updates, read re-anchor.
2026-09-19 16:50:20 +03:00
amosblomqvist dfe1a21989 fix(pdf): render the pdf dispatch card (live browser-smoke gap)
The browser e2e check on the live dispatch session found the tool-pdf
part rendered NO card: the artifact renderer registry had no pdf entry
(the doc comment literally says "add your renderer here"), and the typed
TeacherUITools union + the chat.tsx job-part enumerations (poll cadence,
dispatch nudge, settle-sync) all missed the new tool kind.

- PdfJobCard in job-card.tsx: the research JobCard idiom, kind pdf —
  registered in artifacts/index.ts.
- ui-message.ts: pdf entry in TeacherUITools (fire-and-forget ack output).
- chat.tsx: two mechanical "tool-pdf" additions to the research/demo/visual
  enumerations — UI-stream file, flagged for review at merge (same
  treatment as the activity-dock type-forced entries).
- demos.test.tsx: live/settled dispatch-card renders + registry pin.
2026-09-19 16:27:11 +03:00
amosblomqvist 31978f3fc3 fix: background-delivery ground rules — the dock is the source of truth, delivered content shows the moment it persists
The owner's three-part test of the context-injection flow surfaced
timing/visibility bugs, fixed as standing ground rules for ALL background
processes (owner rules, 2026-09-15):

1. THE JOB STARTS AT SEND. triggerGraphContext moves from onFinish to
   right after the write-ahead persist in handleChatRequest: its only
   input is the first typed message (the topic), just made durable — so
   embedding + cosine scan run CONCURRENT with the prober's turn and
   the dock blob appears the moment the session starts, not after turn
   one finishes. Extraction stays at onFinish (it needs answers
   accumulated DURING the turn).

2. DELIVERED CONTENT SHOWS MID-TURN. Steers inserted server-side
   (claimed at turn start, persisted write-ahead) used to wait for the
   next turn's completion to appear — the full record sync mid-stream
   is forbidden (it breaks the SDK's in-place continuation merge), so
   adoption was deferred to idle. New append-only seam: core
   mergeAdopted() inserts the record's new message ids in canonical
   order (in-flight streaming tail kept last) and never rewrites a
   local message; any other shape bails to the full-sync-at-idle net.
   Wired at three points, no new standing polls:
   - turn-start window: three bounded record fetches (400ms/1s/2s)
     after a send catch the claimed steer while the turn streams;
     self-terminating, survives the submitted->streaming flip.
   - jobs-poll settle: mid-turn settles adopt append-only NOW, the
     owed full sync still runs at idle.
   - wake-adoption poll: each tick adopts persisted wake content, so
     steers show at the wake turn's START, not its finish.
   The dock stays honest with zero gap: jobs hold "running" while
   queued behind a pending question ("delivering context"), then flip
   done at claim/delivery — visible from start to delivery, and the
   turn-start nudge (useActivity().refresh) clears it within ~150ms.

3. THE MARKER IS CLICKABLE (observability). The knowledge-context
   SystemLine becomes GraphContextMarker: quiet line, popover with the
   delivered propositions IN DELIVERY ORDER (most similar first — the
   order the budget filled), each with its folded understanding level;
   Esc / outside pointerdown close. The part data was already the audit
   trail — now it's readable at the seam where it landed.

Tests: mergeAdopted unit pins (6), marker render pins (3) — 468/468,
tsc, eslint, build all green; live-smoked end-to-end: blob at send,
marker at t+500ms mid-turn, popover order + levels, instant dock clear.
2026-09-15 16:34:25 +03:00
amosblomqvist 1d45d77303 feat: graph context injection — the session's prior knowledge, steered in at session start (vision-context-injection.md)
One shot per chat, fired at the first turn's onFinish: the session's
topic (its only typed message) is embedded as an EmbeddingGemma retrieval
query; the semantically closest graph propositions are selected under the
session's token budget (pure budget, no similarity threshold — the
decided doctrine) and delivered as a marker-only steer through the wake
pipeline, queuing behind pending questions like every sub-agent result.

- contracts: ChatSettings.graphContextTokens (default 1024, max 4096,
  step 128, 0 = off); GraphContextData + graphContextText; the
  graph-context JobKind + buildGraphContextSteer (id steer_ctx_<chatId>)
- core/graph-context.ts: brute-force cosine top-K, greedy chars/4 fill
  under budget, most-recent UnderstandingLevel fold, count cap 64,
  parse-don't-cast at the marker seam
- server: embedQuery (role-asymmetric query format, same warm singleton
  — vector-space parity by construction); the runner (startGraphContext-
  Injection one-shot gates; runGraphContextJob validates ledger parity);
  wake branches (chatIdOf/jobIdOf, finalizeJob done, recoverStaleJobs
  re-deliver or contextRespawn); abortJob kills context jobs SILENTLY
  (nothing was promised in the lesson); turn-service trigger seam
- adapter: data-graph-context flattens to the canonical block (malformed
  parts have no model face)
- UI: the budget slider (home dock + /chat/new draft, the session-data
  strip); the quiet SystemLine "~537 · 10 propositions"; dock kind
  "Knowledge context"; model-picker PUT now spreads settings (a model
  pick can no longer clear the sibling override)

Live-smoked end to end on the real embedder + real GLM: seeded ledger
(10 propositions), session on "modal logic: possible worlds and the
necessity box" — job in the dock, queued behind the prober's question,
delivered on the answer, SystemLine in the transcript, wake turn reads
the block and continues the probe. Gates: tsc, lint, 459/459, build.
2026-09-15 07:54:50 +03:00
amosblomqvist 740293c361 feat: custom message types for system turns — wake and verdict messages
The custom-message scheme (user-decided 2026-09-12): system-authored
user-role messages are NOT human messages — they carry marker data parts
only, the adapter owns their model-facing wording, and the UI renders
them under control. Nothing fake is ever persisted as human speech.

The inventory (all migrated):
- probe→teacher handoff wake: was a text-bearing user message rendering
  as a fake user bubble; now data-probe-handoff marker-only — the
  adapter flattens it to HANDOFF_TEXT (core/contracts/chat.ts), the UI
  renders a quiet 'Preparing your plan…' system line.
- plan accept/decline: was boilerplate text ('The plan is accepted…',
  'I'd like the plan revised…') masquerading as learner speech — the
  decline wasn't even marked; now data-verdict {accepted, at} on its own
  message, adapter flattens to verdictText(), UI renders the quiet
  verdict marker ('✓ Plan accepted' / '↻ Plan revision requested').
  sendVerdict sends TWO REAL MESSAGES when the learner typed a note
  (the note as a genuine user message, then the verdict) in ONE turn
  via setMessages + sendMessage() — no 409 dance, no double teacher turn.
- sub-agent steers: the abort steer gains its data-subagent-aborted
  marker part; the UI keys steer rendering off PARTS (artifact/aborted),
  not metadata — metadata stays as pump mechanics only.
- record-adapter: systemTextOf() flattens marker-only messages; steers
  keep their text (payload — briefs are content, not boilerplate).

phases.ts: plan→teach advances on data-verdict{accepted:true} only;
learner text alone can never sanction teaching (pinned). data-plan-
accepted retired, no back-compat (user-decided: wiped-data doctrine).

Pinned: record-adapter.test.ts (verdict/handoff flattening, note+verdict
order, steer payload kept, UI-only parts skipped), phases.test.ts
(verdict marker semantics, out-of-order, text-never-fakes), jobs.test.ts
(abort marker part), demos.test.tsx (badge renders off the part, not
metadata).

Live GLM smoke (disposable driver, deleted): probe→conclude→wake→plan→
decline-with-note (revised, still plan)→accept-with-note (teach) — ALL
CHECKS PASS, including marker-only handoff/verdict on the wire and zero
fake human speech in the transcript.

Gates: tsc, lint, build, 335/335.
2026-09-12 05:14:40 +03:00
amosblomqvist b2c89b8c91 feat: the plan verdict gate — acceptance is the plan→teach seam
The accept-plan UI lands (docs/probe-teach-split.md §4, formerly the
interim rule): presenting the plan no longer moves the phase — the
learner's ACCEPT does. While the presented plan awaits its verdict, the
composer's send square becomes two stacked buttons (green Accept plan /
red Decline); typed text rides the verdict as his note, Enter keeps
sending plain side questions (phase unmoved), decline asks for a
revision and the buttons stay.

The seam: accept sends a user message carrying a data-plan-accepted
part (PlanAcceptedData in contracts/chat.ts, TeacherUIDataParts
'plan-accepted'); phaseAt3 reads it as the plan→teach event (plan_init
demoted to presentation-only); convertToModelMessages drops data parts,
so the teacher sees only the text. Mid-lesson rewrites need no fresh
verdict — acceptance happens once, at the first presentation.

The teacher's planning prompt now ends the turn at presentation and
waits for the verdict (never teaches into an unaccepted plan).

Gates: tsc clean, 304/304 (phases tests rewritten to the acceptance
seam incl. decline/side-question/failed-init cases; composer verdict
render tests), lint, build. Live GLM smoke ALL PASS: phase pinned at
'plan' after plan_init, presentation turn carried NO check, accept
click flipped the summary to 'teach', then tagged checks moved the map
(current=rates, visited=composite); zero console errors.
2026-09-11 21:26:29 +03:00
amosblomqvist 5653c77a29 feat: probe/teach agent split — explicit phase markers, position-only plan map
Diagnosis (2026-09-11 user testing, minimal-effort GLM): sessions stuck on
'plan' forever. Two causes: (1) the three-phase view read the stored tree's
currentNodeId, which nothing ever writes post author-once (2026-09-03) — a
structural dead seam; (2) the teacher stochastically left quiz.nodeId
untagged, and done-by-quiz-result semantics would have falsely completed
nodes anyway.

The redesign (docs/probe-teach-split.md) abolishes derived occupancy in
favor of explicit events:

- Two agents over one transcript: createProberAgent (quiz without a
  nodeId field, ask, probe_done) and createTeacherAgent (full toolset,
  plan_init onward). The chat-handler construction branch keys on a
  probe_done part in the transcript. MODELS.prober added.
- probe_done: server-side seam tool — carries the learner assessment
  (strands + free-form edge read), refuses while a question is open or
  the probe is already concluded, and queues a handoff steer through the
  generalized wake pipeline (steers now dedupe by id, not jobId; job
  finalization only for job steers). The teacher auto-wakes and authors
  the plan; the learner sees the assessment via the probe-done card.
- phases: probe ends at a concluded probe_done, plan ends at a
  successful plan_init — strictly sequential, no skipping, no legacy
  compatibility (data wiped at the pivot per doctrine).
- Plan map = position, not completion: derivePlanStatuses reads only
  tagged quiz CALLS (state/outcome feed nothing); current = latest
  tagged node with consecutive duplicates collapsed, visited = earlier
  distinct stays (a revisit stays in both — current wins at render).
  PlanTree is structure-only; currentNodeId/doneNodeIds and the dead
  mutation helpers are gone. No fallback current.
- Prompts split per agent (prompts/prober/, prompts/teacher/ over the
  trimmed shared base); graph-context injection removed entirely
  (shelved, docs/probe-teach-split.md §5); knowledge-graph context no
  longer reaches either agent.
- Plan blocks are full-only snapshots at plan_init (neighborhood
  machinery retired; the persistent DAG window is the future UI wave);
  usage capture unchanged.

Gates: tsc clean, 294/294 tests (phases/plan-tree/plan-blocks/question-
tools rewritten to the new semantics; probe-done + handoff-wake tests
added), lint, build. Live GLM smoke PASS: prober concluded with an honest
assessment, teacher auto-woke, authored a 10-node plan with the block
attached, then tagged checks on slope-instinct → secant derived current/
visited exactly as designed; grading honest both ways (driver off-by-one
proved a wrong answer grades false).
2026-09-11 06:44:20 +03:00
amosblomqvist bb369a077b Plan blocks in chat: full DAG borderless in the flow, fragment + glow, live popup (wave-4 item 3)
- components/plan-block.tsx: hand-built SVG (user decision) — deterministic
  layered layout (Kahn topo + longest-path depth, rows centered, authoring
  tie-break), bezier edges, the retired panel's status color language.
- PlanBlock renders data-plan-block parts AS CARRIED (frozen snapshots,
  never re-derived): kind full = borderless, viewBox = layout extent so
  nodes hug the column edges, natural height; kind neighborhood = own
  small labeled 'Plan fragment' container with the current node glowing
  (pulsing primary halo).
- PlanPopup: full-screen overlay with the LIVE plan — latest tree from the
  transcript, statuses derived at click time via derivePlanStatuses
  (contracts/plan.ts). Esc / backdrop / X close; drag pan, wheel
  zoom-to-cursor, zoom buttons; fit-floor semantics harvested from the
  retired tree panel (zoom floor = the fit, pan locks at it).
- chat.tsx: data-plan-block case in the part switch + popup mount.
- src/core/ui-message.ts: TeacherUIDataParts gains "plan-block": PlanBlockData
  (additive, type-only — lane touch flagged to master).
- tests/render-plan-block.test.tsx: pure layout (layering, centering,
  dangling-edge tolerance), wrap, frozen-snapshot render, live derivation.
2026-09-11 03:26:56 +03:00
Amos 99fa3f0847 Pi-style ack-and-steer sub-agent protocol: dispatch mid-turn, steer via server wake turn
The job tools (research/demo/visual) switch from client-side turn-breaking
calls to pi's model: execute resolves IMMEDIATELY with a fire-and-forget
ack, the teacher's turn continues, and the result arrives later as an
injected user-role steer message that wakes the teacher in a new
server-side turn.

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

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

Verified live on PORT=3003 with real models: teacher dispatched research
mid-turn, researcher ran real searches/fetches, steer injected on quiz
answer, wake turn wove the brief in citing its sources, job flipped done;
a client/wake race correctly 409'd.
2026-09-01 19:15:43 +03:00
Amos e92140f993 Shell/core import law: public API barrels + ESLint enforcement (stage 2)
The shell now imports through exactly two barrels, enforced by
import/no-restricted-paths (npm run lint):

  components/**  ->  "@/src/core"      (pure core API, browser-safe)
  app/**         ->  "@/src/core" + "@/src/server" (server entries)

Zone violations are lint errors with architecture-pointer messages; the
pure-core modules (contracts, question-core, plan-tree, phases, pending,
math-delims, ui-message) may never import agent/server/store.

Supporting moves (direction: IO layers -> core):
- core/index.ts: the core public API barrel
- core/ui-message.ts: moved from src/agent (it was shell-facing all along)
- quiz/ask input zod schemas moved agent/tools -> core/question-core
  (the question contract, alongside the grading logic)
- ChatRecord/ChatSummary moved store/chats -> core/contracts (single schema)
- GraphSnapshot/NodeStateClass moved core/graph -> core/contracts (the
  /api/graph payload IS a contract; the pure renderer needs only these)
- src/server/{chats,graph,jobs}.ts + index.ts: thin server entries for the
  transport layer; GET /api/graph's fold side-effect now lives behind
  graphView() instead of inline in the route

Gates: tsc clean, 190/190 tests, eslint clean (law verified to fire on
store/agent/subpath violations), production build clean.

(Repo repair en route: a git object (app/chat/[id] tree) was missing from
the object store — reconstructed byte-exactly via mktree from its blob;
the broken node_modules that stalled the build was cleanly reinstalled.)
2026-08-31 19:51:17 +03:00