diff --git a/components/graph-view.tsx b/components/graph-view.tsx
index b11145a..bb4e349 100644
--- a/components/graph-view.tsx
+++ b/components/graph-view.tsx
@@ -625,15 +625,26 @@ export function GraphView() {
{selected.chatIds.length > 0 ? (
Sessions
- {selected.chatIds.map((chatId) => (
-
- {snapshot?.sessions[chatId] ?? chatId}
-
- ))}
+ {selected.chatIds.map((chatId) => {
+ // That session's understanding observation — the
+ // extractor's named-word note (confidence history).
+ const note = snapshot?.nodeObservations?.[selected.node.id]?.[chatId];
+ return (
+
+
+ {snapshot?.sessions[chatId] ?? chatId}
+
+ {note && (
+
+ {note}
+
+ )}
+
+ );
+ })}
) : (
No sessions recorded.
diff --git a/src/core/contracts/graph.ts b/src/core/contracts/graph.ts
index 6a2ad44..158b320 100644
--- a/src/core/contracts/graph.ts
+++ b/src/core/contracts/graph.ts
@@ -229,6 +229,11 @@ export interface GraphSnapshot {
sessions: Record;
/** nodeId → chatIds that touched the node (click-through provenance). */
nodeSessions: Record;
+ /** nodeId → chatId → that session's understanding observation (the
+ * extractor's per-session named-word note; session-relative data,
+ * shown next to the session list — the confidence history). Multiple
+ * notes for one (node, chat) collapse to the latest. */
+ nodeObservations: Record>;
}
/** Derive a short chat title from the learner's first message (pure).
diff --git a/src/core/graph.ts b/src/core/graph.ts
index b2d2281..f40d684 100644
--- a/src/core/graph.ts
+++ b/src/core/graph.ts
@@ -447,5 +447,26 @@ export function graphSnapshot(deps: GraphDeps): GraphSnapshot {
if (ids.length > 0) nodeSessions[nodeId] = ids;
}
- return { nodes, edges, sessions, nodeSessions };
+ // Understanding observations per (node, chat) — the extractor's
+ // session-relative note (latest wins within a chat). Read-time join for
+ // the detail card: the confidence history next to the session list.
+ const nodeObservations: Record> = {};
+ const latestObs = new Map();
+ events.forEach((event, index) => {
+ if (event.kind !== "observation" || event.nodeId === null) return;
+ if (!event.note) return;
+ const key = `${event.nodeId}\u0000${event.chatId}`;
+ const current = latestObs.get(key);
+ if (!current || event.at > current.at || (event.at === current.at && index > current.index)) {
+ latestObs.set(key, { at: event.at, note: event.note, index });
+ }
+ });
+ for (const [key, { note }] of latestObs) {
+ const sep = key.indexOf("\u0000");
+ const nodeId = key.slice(0, sep);
+ const chatId = key.slice(sep + 1);
+ (nodeObservations[nodeId] ??= {})[chatId] = note;
+ }
+
+ return { nodes, edges, sessions, nodeSessions, nodeObservations };
}
diff --git a/tests/graph.test.ts b/tests/graph.test.ts
index ccc2e18..f440ab6 100644
--- a/tests/graph.test.ts
+++ b/tests/graph.test.ts
@@ -363,7 +363,7 @@ describe("the merger (mergeFindings)", () => {
assert.deepEqual(m, { chatId: chat2.id, sourceId: "q1", graphNodeId: alphaId });
});
- it("derives node states and sessions in graphSnapshot", () => {
+ it("derives sessions and per-session understanding observations in graphSnapshot", () => {
const snap = graphSnapshot(graphDeps);
const idByStatement = new Map(snap.nodes.map((n) => [n.statement, n.id]));
// No node-state classes anymore (2026-09-08): the snapshot carries
@@ -378,6 +378,12 @@ describe("the merger (mergeFindings)", () => {
);
// Gamma was only ever touched by chat1.
assert.deepEqual(snap.nodeSessions[idByStatement.get("Gamma application")!], [chat1.id]);
+
+ // Understanding observations: (node → chat → the extractor's note).
+ // Alpha carries chat1's "solid after the check"; chat2's q1 has no note.
+ const alphaObs = snap.nodeObservations[idByStatement.get("Alpha intuition")!] ?? {};
+ assert.equal(alphaObs[chat1.id], "solid after the check");
+ assert.equal(alphaObs[chat2Id], undefined);
});
it("never string-matches without a verdict — an identical statement without linkTo is a NEW node", () => {