From 4fde6caa4c59ab5a909185da5bd087c1e217a5e1 Mon Sep 17 00:00:00 2001 From: Brooklyn Nicholson Date: Mon, 27 Apr 2026 13:41:50 -0500 Subject: [PATCH] fix(tui): include learning notes in turn completion Carry learning events on the message completion payload so remembered/recalled notes flush deterministically after the assistant response even if standalone event timing is missed. --- tui_gateway/server.py | 6 ++++++ ui-tui/src/app/createGatewayEventHandler.ts | 12 +++++++++++- ui-tui/src/gatewayTypes.ts | 8 +++++++- 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/tui_gateway/server.py b/tui_gateway/server.py index 9d13bafc3b..ba3ed71bee 100644 --- a/tui_gateway/server.py +++ b/tui_gateway/server.py @@ -1108,6 +1108,8 @@ def _on_tool_complete(sid: str, tool_call_id: str, name: str, args: dict, result event = learning_event_from_tool(name, args, result) if event: + if session is not None: + session.setdefault("learning_events", []).append(event) _emit("learning.event", sid, event) except Exception: pass @@ -2340,6 +2342,7 @@ def _(rid, params: dict) -> dict: if session.get("running"): return _err(rid, 4009, "session busy") session["running"] = True + session["learning_events"] = [] history = list(session["history"]) history_version = int(session.get("history_version", 0)) images = list(session.get("attached_images", [])) @@ -2502,6 +2505,9 @@ def _(rid, params: dict) -> dict: payload["reasoning"] = last_reasoning if status_note: payload["warning"] = status_note + learning_events = list(session.get("learning_events") or []) + if learning_events: + payload["learning_events"] = learning_events rendered = render_message(raw, cols) if rendered: payload["rendered"] = rendered diff --git a/ui-tui/src/app/createGatewayEventHandler.ts b/ui-tui/src/app/createGatewayEventHandler.ts index 232a77e240..1a8baa55ed 100644 --- a/ui-tui/src/app/createGatewayEventHandler.ts +++ b/ui-tui/src/app/createGatewayEventHandler.ts @@ -547,11 +547,21 @@ export function createGatewayEventHandler(ctx: GatewayEventHandlerContext): (ev: return case 'message.complete': { const { finalMessages, finalText, wasInterrupted } = turnController.recordMessageComplete(ev.payload ?? {}) + const completedLearning = (ev.payload?.learning_events ?? []) + .map(e => { + const title = String(e?.title ?? '').trim() + const verb = String(e?.verb ?? e?.type ?? 'learned').trim() + + return title ? `${verb}: ${title}` : '' + }) + .filter(Boolean) if (!wasInterrupted) { const msgs: Msg[] = finalMessages.length ? finalMessages : [{ role: 'assistant', text: finalText }] + const learningLines = [...completedLearning, ...pendingLearning].filter((text, i, xs) => xs.indexOf(text) === i) + msgs.forEach(appendMessage) - pendingLearning.forEach(text => appendMessage({ kind: 'learning', role: 'system', text })) + learningLines.forEach(text => appendMessage({ kind: 'learning', role: 'system', text })) pendingLearning = [] if (bellOnComplete && stdout?.isTTY) { diff --git a/ui-tui/src/gatewayTypes.ts b/ui-tui/src/gatewayTypes.ts index d17196f1d2..dd6272c1e8 100644 --- a/ui-tui/src/gatewayTypes.ts +++ b/ui-tui/src/gatewayTypes.ts @@ -425,7 +425,13 @@ export type GatewayEvent = | { payload: SubagentEventPayload; session_id?: string; type: 'subagent.complete' } | { payload: { rendered?: string; text?: string }; session_id?: string; type: 'message.delta' } | { - payload?: { reasoning?: string; rendered?: string; text?: string; usage?: Usage } + payload?: { + learning_events?: { source?: string; summary?: string; title?: string; type?: string; verb?: string; via?: string }[] + reasoning?: string + rendered?: string + text?: string + usage?: Usage + } session_id?: string type: 'message.complete' }