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.
This commit is contained in:
Brooklyn Nicholson
2026-04-27 13:41:50 -05:00
parent 51e28eabf7
commit 4fde6caa4c
3 changed files with 24 additions and 2 deletions

View File

@@ -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

View File

@@ -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) {

View File

@@ -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'
}