diff --git a/src/app/api/insights/chat/__tests__/__snapshots__/golden-transcripts.test.ts.snap b/src/app/api/insights/chat/__tests__/__snapshots__/golden-transcripts.test.ts.snap index 2399cd5b7..b9570c8ea 100644 --- a/src/app/api/insights/chat/__tests__/__snapshots__/golden-transcripts.test.ts.snap +++ b/src/app/api/insights/chat/__tests__/__snapshots__/golden-transcripts.test.ts.snap @@ -19,6 +19,9 @@ exports[`coach chat golden transcripts > 01a tool mode, two rounds, present data { "call": "storeDeterministicFacts", }, + { + "call": "enqueueCoachMemoryRefresh", + }, { "call": "assertConsentForChain", "chain": [ @@ -50,7 +53,7 @@ exports[`coach chat golden transcripts > 01a tool mode, two rounds, present data "window": "last30days", }, "signal": true, - "system": "d46e2a89426f:3975", + "system": "37f58b1a7608:4923", "temperature": 0.4, "timeoutMs": 180000, "tools": 6, @@ -265,6 +268,9 @@ exports[`coach chat golden transcripts > 01b tool mode, present data, one figure { "call": "storeDeterministicFacts", }, + { + "call": "enqueueCoachMemoryRefresh", + }, { "call": "assertConsentForChain", "chain": [ @@ -296,7 +302,7 @@ exports[`coach chat golden transcripts > 01b tool mode, present data, one figure "window": "last30days", }, "signal": true, - "system": "d46e2a89426f:3975", + "system": "37f58b1a7608:4923", "temperature": 0.4, "timeoutMs": 180000, "tools": 6, @@ -523,6 +529,9 @@ exports[`coach chat golden transcripts > 02 tool mode, every tool missed, fabric { "call": "storeDeterministicFacts", }, + { + "call": "enqueueCoachMemoryRefresh", + }, { "call": "assertConsentForChain", "chain": [ @@ -554,7 +563,7 @@ exports[`coach chat golden transcripts > 02 tool mode, every tool missed, fabric "window": "last30days", }, "signal": true, - "system": "d46e2a89426f:3975", + "system": "37f58b1a7608:4923", "temperature": 0.4, "timeoutMs": 180000, "tools": 6, @@ -889,6 +898,9 @@ exports[`coach chat golden transcripts > 03 no-tools path with the full snapshot { "call": "storeDeterministicFacts", }, + { + "call": "enqueueCoachMemoryRefresh", + }, { "call": "assertConsentForChain", "chain": [ @@ -1339,6 +1351,9 @@ exports[`coach chat golden transcripts > 06 budget exceeded 1`] = ` { "call": "storeDeterministicFacts", }, + { + "call": "enqueueCoachMemoryRefresh", + }, { "call": "assertConsentForChain", "chain": [ @@ -1390,6 +1405,9 @@ exports[`coach chat golden transcripts > 07a all providers failed (plain) 1`] = { "call": "storeDeterministicFacts", }, + { + "call": "enqueueCoachMemoryRefresh", + }, { "call": "assertConsentForChain", "chain": [ @@ -1421,7 +1439,7 @@ exports[`coach chat golden transcripts > 07a all providers failed (plain) 1`] = "window": "last30days", }, "signal": true, - "system": "d46e2a89426f:3975", + "system": "37f58b1a7608:4923", "temperature": 0.4, "timeoutMs": 180000, "tools": 6, @@ -1479,6 +1497,9 @@ exports[`coach chat golden transcripts > 07b all providers failed (every hop rat { "call": "storeDeterministicFacts", }, + { + "call": "enqueueCoachMemoryRefresh", + }, { "call": "assertConsentForChain", "chain": [ @@ -1510,7 +1531,7 @@ exports[`coach chat golden transcripts > 07b all providers failed (every hop rat "window": "last30days", }, "signal": true, - "system": "d46e2a89426f:3975", + "system": "37f58b1a7608:4923", "temperature": 0.4, "timeoutMs": 180000, "tools": 6, @@ -1568,6 +1589,9 @@ exports[`coach chat golden transcripts > 07c all providers failed (primary crede { "call": "storeDeterministicFacts", }, + { + "call": "enqueueCoachMemoryRefresh", + }, { "call": "assertConsentForChain", "chain": [ @@ -1599,7 +1623,7 @@ exports[`coach chat golden transcripts > 07c all providers failed (primary crede "window": "last30days", }, "signal": true, - "system": "d46e2a89426f:3975", + "system": "37f58b1a7608:4923", "temperature": 0.4, "timeoutMs": 180000, "tools": 6, @@ -1657,6 +1681,9 @@ exports[`coach chat golden transcripts > 08 an unwrapped tagged provider error 1 { "call": "storeDeterministicFacts", }, + { + "call": "enqueueCoachMemoryRefresh", + }, { "call": "assertConsentForChain", "chain": [ @@ -1688,7 +1715,7 @@ exports[`coach chat golden transcripts > 08 an unwrapped tagged provider error 1 "window": "last30days", }, "signal": true, - "system": "d46e2a89426f:3975", + "system": "37f58b1a7608:4923", "temperature": 0.4, "timeoutMs": 180000, "tools": 6, @@ -1747,6 +1774,9 @@ exports[`coach chat golden transcripts > 09 an aborted request leaves a cancelle { "call": "storeDeterministicFacts", }, + { + "call": "enqueueCoachMemoryRefresh", + }, { "call": "assertConsentForChain", "chain": [ @@ -1778,7 +1808,7 @@ exports[`coach chat golden transcripts > 09 an aborted request leaves a cancelle "window": "last30days", }, "signal": true, - "system": "d46e2a89426f:3975", + "system": "37f58b1a7608:4923", "temperature": 0.4, "timeoutMs": 180000, "tools": 6, @@ -1843,6 +1873,9 @@ exports[`coach chat golden transcripts > 10 outbound block replaces the reply an { "call": "storeDeterministicFacts", }, + { + "call": "enqueueCoachMemoryRefresh", + }, { "call": "assertConsentForChain", "chain": [ @@ -1874,7 +1907,7 @@ exports[`coach chat golden transcripts > 10 outbound block replaces the reply an "window": "last30days", }, "signal": true, - "system": "d46e2a89426f:3975", + "system": "37f58b1a7608:4923", "temperature": 0.4, "timeoutMs": 180000, "tools": 6, @@ -2219,6 +2252,9 @@ exports[`coach chat golden transcripts > 11a KEYVALUES valid 1`] = ` { "call": "storeDeterministicFacts", }, + { + "call": "enqueueCoachMemoryRefresh", + }, { "call": "assertConsentForChain", "chain": [ @@ -2250,7 +2286,7 @@ exports[`coach chat golden transcripts > 11a KEYVALUES valid 1`] = ` "window": "last30days", }, "signal": true, - "system": "d46e2a89426f:3975", + "system": "37f58b1a7608:4923", "temperature": 0.4, "timeoutMs": 180000, "tools": 6, @@ -2469,6 +2505,9 @@ exports[`coach chat golden transcripts > 11b KEYVALUES partial 1`] = ` { "call": "storeDeterministicFacts", }, + { + "call": "enqueueCoachMemoryRefresh", + }, { "call": "assertConsentForChain", "chain": [ @@ -2500,7 +2539,7 @@ exports[`coach chat golden transcripts > 11b KEYVALUES partial 1`] = ` "window": "last30days", }, "signal": true, - "system": "d46e2a89426f:3975", + "system": "37f58b1a7608:4923", "temperature": 0.4, "timeoutMs": 180000, "tools": 6, @@ -2717,6 +2756,9 @@ exports[`coach chat golden transcripts > 11c KEYVALUES malformed (no closing mar { "call": "storeDeterministicFacts", }, + { + "call": "enqueueCoachMemoryRefresh", + }, { "call": "assertConsentForChain", "chain": [ @@ -2748,7 +2790,7 @@ exports[`coach chat golden transcripts > 11c KEYVALUES malformed (no closing mar "window": "last30days", }, "signal": true, - "system": "d46e2a89426f:3975", + "system": "37f58b1a7608:4923", "temperature": 0.4, "timeoutMs": 180000, "tools": 6, @@ -2949,6 +2991,9 @@ exports[`coach chat golden transcripts > 11d sentinel-only reply is an empty rep { "call": "storeDeterministicFacts", }, + { + "call": "enqueueCoachMemoryRefresh", + }, { "call": "assertConsentForChain", "chain": [ @@ -2980,7 +3025,7 @@ exports[`coach chat golden transcripts > 11d sentinel-only reply is an empty rep "window": "last30days", }, "signal": true, - "system": "d46e2a89426f:3975", + "system": "37f58b1a7608:4923", "temperature": 0.4, "timeoutMs": 180000, "tools": 6, @@ -3049,6 +3094,9 @@ exports[`coach chat golden transcripts > 11e empty provider reply 1`] = ` { "call": "storeDeterministicFacts", }, + { + "call": "enqueueCoachMemoryRefresh", + }, { "call": "assertConsentForChain", "chain": [ @@ -3080,7 +3128,7 @@ exports[`coach chat golden transcripts > 11e empty provider reply 1`] = ` "window": "last30days", }, "signal": true, - "system": "d46e2a89426f:3975", + "system": "37f58b1a7608:4923", "temperature": 0.4, "timeoutMs": 180000, "tools": 6, @@ -3140,6 +3188,9 @@ exports[`coach chat golden transcripts > 12a SUGGEST-REMINDER surfaced 1`] = ` { "call": "storeDeterministicFacts", }, + { + "call": "enqueueCoachMemoryRefresh", + }, { "call": "assertConsentForChain", "chain": [ @@ -3171,7 +3222,7 @@ exports[`coach chat golden transcripts > 12a SUGGEST-REMINDER surfaced 1`] = ` "window": "last30days", }, "signal": true, - "system": "d46e2a89426f:3975", + "system": "37f58b1a7608:4923", "temperature": 0.4, "timeoutMs": 180000, "tools": 6, @@ -3410,6 +3461,9 @@ exports[`coach chat golden transcripts > 12b SUGGEST-REMINDER suppressed 1`] = ` { "call": "storeDeterministicFacts", }, + { + "call": "enqueueCoachMemoryRefresh", + }, { "call": "assertConsentForChain", "chain": [ @@ -3441,7 +3495,7 @@ exports[`coach chat golden transcripts > 12b SUGGEST-REMINDER suppressed 1`] = ` "window": "last30days", }, "signal": true, - "system": "d46e2a89426f:3975", + "system": "37f58b1a7608:4923", "temperature": 0.4, "timeoutMs": 180000, "tools": 6, @@ -3644,6 +3698,9 @@ exports[`coach chat golden transcripts > 13 SUGGEST-ACTION 1`] = ` { "call": "storeDeterministicFacts", }, + { + "call": "enqueueCoachMemoryRefresh", + }, { "call": "assertConsentForChain", "chain": [ @@ -3675,7 +3732,7 @@ exports[`coach chat golden transcripts > 13 SUGGEST-ACTION 1`] = ` "window": "last30days", }, "signal": true, - "system": "d46e2a89426f:3975", + "system": "37f58b1a7608:4923", "temperature": 0.4, "timeoutMs": 180000, "tools": 6, @@ -3918,6 +3975,9 @@ exports[`coach chat golden transcripts > 14a REMEMBER valid 1`] = ` { "call": "storeDeterministicFacts", }, + { + "call": "enqueueCoachMemoryRefresh", + }, { "call": "assertConsentForChain", "chain": [ @@ -3949,7 +4009,7 @@ exports[`coach chat golden transcripts > 14a REMEMBER valid 1`] = ` "window": "last30days", }, "signal": true, - "system": "d46e2a89426f:3975", + "system": "37f58b1a7608:4923", "temperature": 0.4, "timeoutMs": 180000, "tools": 6, @@ -4172,6 +4232,9 @@ exports[`coach chat golden transcripts > 14b REMEMBER malformed 1`] = ` { "call": "storeDeterministicFacts", }, + { + "call": "enqueueCoachMemoryRefresh", + }, { "call": "assertConsentForChain", "chain": [ @@ -4203,7 +4266,7 @@ exports[`coach chat golden transcripts > 14b REMEMBER malformed 1`] = ` "window": "last30days", }, "signal": true, - "system": "d46e2a89426f:3975", + "system": "37f58b1a7608:4923", "temperature": 0.4, "timeoutMs": 180000, "tools": 6, @@ -4421,6 +4484,9 @@ exports[`coach chat golden transcripts > 15 an unknown learn link is scrubbed, a { "call": "storeDeterministicFacts", }, + { + "call": "enqueueCoachMemoryRefresh", + }, { "call": "assertConsentForChain", "chain": [ @@ -4452,7 +4518,7 @@ exports[`coach chat golden transcripts > 15 an unknown learn link is scrubbed, a "window": "last30days", }, "signal": true, - "system": "d46e2a89426f:3975", + "system": "37f58b1a7608:4923", "temperature": 0.4, "timeoutMs": 180000, "tools": 6, @@ -4668,6 +4734,9 @@ exports[`coach chat golden transcripts > 16a coach.provider.none via an empty ch { "call": "storeDeterministicFacts", }, + { + "call": "enqueueCoachMemoryRefresh", + }, { "call": "annotate", "metaKeys": [], @@ -4707,6 +4776,9 @@ exports[`coach chat golden transcripts > 16b an empty chain served by the legacy { "call": "storeDeterministicFacts", }, + { + "call": "enqueueCoachMemoryRefresh", + }, { "call": "assertConsentForChain", "chain": [ @@ -5064,6 +5136,9 @@ exports[`coach chat golden transcripts > 16d coach.provider.none from the capabi { "call": "storeDeterministicFacts", }, + { + "call": "enqueueCoachMemoryRefresh", + }, { "call": "assertConsentForChain", "chain": [ @@ -5109,6 +5184,9 @@ exports[`coach chat golden transcripts > 17a a workout-scoped first turn 1`] = ` { "call": "storeDeterministicFacts", }, + { + "call": "enqueueCoachMemoryRefresh", + }, { "call": "isModuleEnabled", "module": "workouts", @@ -5153,7 +5231,7 @@ exports[`coach chat golden transcripts > 17a a workout-scoped first turn 1`] = ` "window": "last30days", }, "signal": true, - "system": "d46e2a89426f:3975", + "system": "37f58b1a7608:4923", "temperature": 0.4, "timeoutMs": 180000, "tools": 6, @@ -5368,6 +5446,9 @@ exports[`coach chat golden transcripts > 17b a workout-scoped first turn with th { "call": "storeDeterministicFacts", }, + { + "call": "enqueueCoachMemoryRefresh", + }, { "call": "annotate", "metaKeys": [ @@ -5408,7 +5489,7 @@ exports[`coach chat golden transcripts > 17b a workout-scoped first turn with th "window": "last30days", }, "signal": true, - "system": "d46e2a89426f:3975", + "system": "37f58b1a7608:4923", "temperature": 0.4, "timeoutMs": 180000, "tools": 6, @@ -5611,6 +5692,9 @@ exports[`coach chat golden transcripts > 18 a persistence throw after the provid { "call": "storeDeterministicFacts", }, + { + "call": "enqueueCoachMemoryRefresh", + }, { "call": "assertConsentForChain", "chain": [ @@ -5642,7 +5726,7 @@ exports[`coach chat golden transcripts > 18 a persistence throw after the provid "window": "last30days", }, "signal": true, - "system": "d46e2a89426f:3975", + "system": "37f58b1a7608:4923", "temperature": 0.4, "timeoutMs": 180000, "tools": 6, @@ -5757,7 +5841,7 @@ exports[`coach chat golden transcripts > 19 an existing conversation with prior "window": "last30days", }, "signal": true, - "system": "d46e2a89426f:3975", + "system": "37f58b1a7608:4923", "temperature": 0.4, "timeoutMs": 180000, "tools": 6, @@ -5984,6 +6068,9 @@ exports[`coach chat golden transcripts > 20 a guided-question turn with a scope { "call": "storeDeterministicFacts", }, + { + "call": "enqueueCoachMemoryRefresh", + }, { "call": "assertConsentForChain", "chain": [ @@ -6015,7 +6102,7 @@ exports[`coach chat golden transcripts > 20 a guided-question turn with a scope "window": "last30days", }, "signal": true, - "system": "d46e2a89426f:3975", + "system": "37f58b1a7608:4923", "temperature": 0.4, "timeoutMs": 30000, "tools": 6, @@ -6198,6 +6285,9 @@ exports[`coach chat golden transcripts > 21 the German locale with a stored self { "call": "storeDeterministicFacts", }, + { + "call": "enqueueCoachMemoryRefresh", + }, { "call": "assertConsentForChain", "chain": [ @@ -6229,7 +6319,7 @@ exports[`coach chat golden transcripts > 21 the German locale with a stored self "window": "last30days", }, "signal": true, - "system": "ea823e5e510a:4068", + "system": "7c188ee61f98:5016", "temperature": 0.4, "timeoutMs": 180000, "tools": 6, @@ -6448,6 +6538,9 @@ exports[`coach chat golden transcripts > 22 tool mode with live steps: found, em { "call": "storeDeterministicFacts", }, + { + "call": "enqueueCoachMemoryRefresh", + }, { "call": "assertConsentForChain", "chain": [ @@ -6479,7 +6572,7 @@ exports[`coach chat golden transcripts > 22 tool mode with live steps: found, em "window": "last30days", }, "signal": true, - "system": "d46e2a89426f:3975", + "system": "37f58b1a7608:4923", "temperature": 0.4, "timeoutMs": 180000, "tools": 6, @@ -7064,6 +7157,9 @@ exports[`coach chat golden transcripts — refusals before the stream > R6 conse { "call": "storeDeterministicFacts", }, + { + "call": "enqueueCoachMemoryRefresh", + }, ], "contentType": null, "frames": [], diff --git a/src/lib/ai/coach/tools/dialog-tools.ts b/src/lib/ai/coach/tools/dialog-tools.ts index 62c695fcd..47f693e8a 100644 --- a/src/lib/ai/coach/tools/dialog-tools.ts +++ b/src/lib/ai/coach/tools/dialog-tools.ts @@ -31,6 +31,8 @@ import { import { PLAN_REVIEW_DAYS, REMEMBER_FACT_MAX_CHARS, +} from "@/lib/ai/coach/memory/shared"; +import { proposePlanFromTool, rememberFactFromTool, } from "@/lib/ai/coach/memory/contract"; @@ -200,6 +202,8 @@ export interface DialogToolContext { locale: Locale; /** The person's current message: the only text a fact may come from. */ userMessage: string; + /** The stored id of that message, which a remembered fact points back to. */ + userMessageId?: string; /** What the record holds; null on the no-tools path. */ inventory: InventoryEntry[] | null; /** @@ -319,6 +323,7 @@ export async function runDialogTool(args: { userId: ctx.userId, conversationId: ctx.conversationId, userMessage: ctx.userMessage, + ...(ctx.userMessageId ? { userMessageId: ctx.userMessageId } : {}), call: parsed.data, }); if (outcome.kind === "declined") { diff --git a/src/lib/ai/coach/tools/loop.ts b/src/lib/ai/coach/tools/loop.ts index 031886ed1..74ef7e9c6 100644 --- a/src/lib/ai/coach/tools/loop.ts +++ b/src/lib/ai/coach/tools/loop.ts @@ -228,6 +228,12 @@ export async function runCoachToolLoop(args: { locale?: Locale; /** v1.41 — what the dialog tools need. Absent: they are declined. */ dialog?: DialogToolContext; + /** + * v1.41 — a note the answer already carries before any round (a health + * fact the background found, offered once). It is the answer's one note: + * `remember_fact` is declined beside it. + */ + initialMemoryNote?: CoachMemoryNote; /** v1.41 — the figures read so far, for screening a checkpoint. */ checkpointScreen?: { userMessage: string }; }): Promise { @@ -276,7 +282,7 @@ export async function runCoachToolLoop(args: { const toolTrace: CoachToolTrace[] = []; const toolResults: CoachToolResult[] = []; const declinedClarifications: CoachClarification[] = []; - let memoryNote: CoachMemoryNote | undefined; + let memoryNote: CoachMemoryNote | undefined = args.initialMemoryNote; let planProposal: CoachPlanProposal | undefined; let callCount = 0; let stopReason: CoachStopReason | null = null; diff --git a/src/lib/ai/coach/turn/__tests__/continue-turn.test.ts b/src/lib/ai/coach/turn/__tests__/continue-turn.test.ts index f787dda3a..9b177d622 100644 --- a/src/lib/ai/coach/turn/__tests__/continue-turn.test.ts +++ b/src/lib/ai/coach/turn/__tests__/continue-turn.test.ts @@ -131,6 +131,7 @@ function persistedProvenance(): CoachProvenance { beforeEach(() => { for (const fn of Object.values(m)) fn.mockReset(); + m.persistUserTurn.mockResolvedValue({ messageId: "m-user" }); m.resolveTurnConversation.mockResolvedValue({ conversation: { conversationId: "c1", diff --git a/src/lib/ai/coach/turn/__tests__/follow-up-turn.test.ts b/src/lib/ai/coach/turn/__tests__/follow-up-turn.test.ts index 0e5511ef8..6fcfbfd2e 100644 --- a/src/lib/ai/coach/turn/__tests__/follow-up-turn.test.ts +++ b/src/lib/ai/coach/turn/__tests__/follow-up-turn.test.ts @@ -238,6 +238,7 @@ function modelPath(prefs = DEFAULT_COACH_PREFS) { beforeEach(() => { for (const fn of Object.values(m)) fn.mockReset(); + m.persistUserTurn.mockResolvedValue({ messageId: "m-user" }); m.readFollowUpHistory.mockResolvedValue({ today: "2026-09-27", firstDate: { bp: "2024-01-01" }, diff --git a/src/lib/ai/coach/turn/context.ts b/src/lib/ai/coach/turn/context.ts index 3a87eff5f..5eb51c3dd 100644 --- a/src/lib/ai/coach/turn/context.ts +++ b/src/lib/ai/coach/turn/context.ts @@ -177,18 +177,19 @@ export async function assembleTurnContext(args: { // Fact capture must never break the chat turn; the >TURN_CAP LLM // extraction remains as the catch-all on long conversations. }); - if (turnContext.historyElided) { - void enqueueCoachMemoryRefresh({ - conversationId, - userId, - // Coach memory prose is composed in de/en only — it is MODEL-FACING - // context (a rolling conversation summary + extracted durable facts), - // not user-facing prose, so English is the correct target for every - // locale without a reviewed body. The former `=== "en" ? "en" : "de"` - // binary composed and keyed a French account's memory in German. - locale: instructionLocale(locale), - }); - } + // v1.41 — every turn enqueues the refresh; the job waits for the + // conversation to go quiet and runs once per conversation (its debounce + // and singleton key), so a short chat is remembered too. + void enqueueCoachMemoryRefresh({ + conversationId, + userId, + // Coach memory prose is composed in de/en only — it is MODEL-FACING + // context (a rolling conversation summary + extracted durable facts), + // not user-facing prose, so English is the correct target for every + // locale without a reviewed body. The former `=== "en" ? "en" : "de"` + // binary composed and keyed a French account's memory in German. + locale: instructionLocale(locale), + }); // A workout launch is an optional narrowing of Coach, not permission to // bypass the workouts module. Disabled modules contribute no read and no diff --git a/src/lib/ai/coach/turn/conversation.ts b/src/lib/ai/coach/turn/conversation.ts index 54cb9534f..e6bbbd078 100644 --- a/src/lib/ai/coach/turn/conversation.ts +++ b/src/lib/ai/coach/turn/conversation.ts @@ -163,10 +163,12 @@ export async function resolveTurnConversation(args: { export async function persistUserTurn( conversationId: string, message: string, -): Promise { - await appendMessage({ +): Promise<{ messageId: string }> { + const stored = await appendMessage({ conversationId, role: "user", content: message, }); + // v1.41 — the id a remembered fact points back to. + return { messageId: stored.id }; } diff --git a/src/lib/ai/coach/turn/model.ts b/src/lib/ai/coach/turn/model.ts index b9961683f..c032c45b1 100644 --- a/src/lib/ai/coach/turn/model.ts +++ b/src/lib/ai/coach/turn/model.ts @@ -282,6 +282,8 @@ export async function runTurnModel(args: { priorResults?: PriorResultTurn[]; /** v1.41 — the person's message, the only source of a remembered fact. */ message: string; + /** v1.41 — that message's stored id. */ + userMessageId?: string; /** v1.41 — the reasoning the turn asks for, already resolved. */ reasoning?: TurnReasoning; /** v1.41 — who pays: fixes the turn's budget. */ @@ -333,8 +335,13 @@ export async function runTurnModel(args: { userId, conversationId, locale, + // The block re-runs the wire egress check for exactly these. + providerTypes: chain.map((entry) => entry.providerType), }).catch(() => null); if (memory) { + recorder.setRecalled(memory.recalled); + } + if (memory && memory.factIds.length + memory.planIds.length > 0) { const count = memory.factIds.length + memory.planIds.length; const id = activity.start({ phase: "memory", @@ -365,12 +372,8 @@ export async function runTurnModel(args: { reach: ctx.reach, prior: args.priorResults ?? [], }); - const requestWith = (tableRules: boolean) => { - // `memoryBlock` is the memory package's parameter; held in a - // variable so the request builds whether or not it is declared yet. - const request: Parameters[0] & { - memoryBlock?: string; - } = { + const requestWith = (tableRules: boolean) => + buildCoachToolRequest({ systemPrompt: ctx.systemPrompt, toolModeAddendum: appendBlocks(buildToolModeAddendum(locale), [ buildDialogAddenda(locale, { tableRules }), @@ -384,9 +387,7 @@ export async function runTurnModel(args: { transcript: turnContext.transcript, languageName: LANGUAGE_NAMES[locale], ...(memory ? { memoryBlock: memory.text } : {}), - }; - return buildCoachToolRequest(request); - }; + }); // The recheck and follow-up rules are about tables: they ride the // prompt once the conversation holds one, or from the round after this // turn produced its first. @@ -445,11 +446,15 @@ export async function runTurnModel(args: { ...(args.reasoning ? { reasoning: args.reasoning } : {}), activity, locale, + ...(memory?.pendingProposal + ? { initialMemoryNote: memory.pendingProposal } + : {}), dialog: { userId, conversationId, locale, userMessage: args.message, + ...(args.userMessageId ? { userMessageId: args.userMessageId } : {}), inventory: inventory.entries, conversationWindowSet: args.conversationWindowSet, }, @@ -698,7 +703,7 @@ export async function runTurnModel(args: { activity: recorder, toolClarification: null, declinedClarifications: [], - memoryNote: null, + memoryNote: memory?.pendingProposal ?? null, planProposal: null, interimSent: false, }; diff --git a/src/lib/ai/coach/turn/pipeline.ts b/src/lib/ai/coach/turn/pipeline.ts index 3535a45eb..218232f51 100644 --- a/src/lib/ai/coach/turn/pipeline.ts +++ b/src/lib/ai/coach/turn/pipeline.ts @@ -149,7 +149,10 @@ async function runTurn( ...(clarifiedLine ? [clarifiedLine] : []), ]; - await persistUserTurn(workingConversationId, message); + const { messageId: userMessageId } = await persistUserTurn( + workingConversationId, + message, + ); const ctx = await assembleTurnContext({ userId, @@ -219,6 +222,7 @@ async function runTurn( turnHints, priorResults: conversation.priorResults, message, + userMessageId, reasoning, payer, conversationWindowSet: input.scope?.window !== undefined, diff --git a/src/lib/ai/coach/turn/reuse-turn.ts b/src/lib/ai/coach/turn/reuse-turn.ts index 72d46b8ab..993e3988d 100644 --- a/src/lib/ai/coach/turn/reuse-turn.ts +++ b/src/lib/ai/coach/turn/reuse-turn.ts @@ -49,6 +49,8 @@ import { createSseStream } from "@/lib/sse/create-stream"; import { COACH_MEMORY_KEYS, COACH_PLAN_KEYS, +} from "@/lib/ai/coach/memory/shared"; +import { decideFactProposal, decidePlanProposal, } from "@/lib/ai/coach/memory/contract";