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.
This commit is contained in:
amosblomqvist
2026-09-15 07:54:50 +03:00
parent ecb293a77d
commit 1d45d77303
25 changed files with 1443 additions and 28 deletions
+35
View File
@@ -84,6 +84,41 @@ describe("DraftShell — the sessionless surface", () => {
assert.ok(/rounded-2xl[^>]*pointer-events-auto|pointer-events-auto[^>]*rounded-2xl/.test(html));
});
it("renders the knowledge-context slider at the decided default (absent settings = on, 1024)", () => {
const html = renderToStaticMarkup(
<DraftShell
busy={false}
text=""
onChange={() => {}}
onSend={() => {}}
settings={undefined}
onSettingsSaved={() => {}}
/>,
);
assert.ok(html.includes('data-testid="graph-context-control"'));
// The slider's default position: 1024 of 4096, step 128 (decided).
assert.ok(html.includes('min="0"') && html.includes('max="4096"') && html.includes('step="128"'));
assert.ok(html.includes("1k tokens"), "the readout shows the default budget, not 0 — default is ON");
assert.ok(html.includes("Knowledge context"), "the control is labeled");
// Absent settings render the default — never 'off'.
assert.ok(!html.includes(">off<"));
});
it("an explicit 0 renders as off — the learner turned the graph off for this session", () => {
const html = renderToStaticMarkup(
<DraftShell
busy={false}
text=""
onChange={() => {}}
onSend={() => {}}
settings={{ graphContextTokens: 0 }}
onSettingsSaved={() => {}}
/>,
);
assert.ok(html.includes('data-testid="graph-context-value"'));
assert.ok(html.includes(">off<"));
});
it("no chat id anywhere — nothing in the markup references a session", () => {
const html = renderToStaticMarkup(
<DraftShell