Files
HealthLog/src/lib/ai/coach/coach-memory-refresh-worker.ts
T
Marc-André Bombeck dcbac63fdf feat(coach): goal / if-then plan memory + locale-keyed dashboard snapshot
Add a CoachPlan model: durable, user-scoped if-then implementation plans
the Coach proposes and the user confirms. The background memory-refresh
pass extracts plans the user agreed to and writes them as "proposed"; only
the user-facing PATCH on /api/coach/plans/[id] activates one. The free-text
cue / action / target are encrypted at rest with the shared Bytes codec;
metric, status and dates stay plain so the injection picker ranks without a
per-row decrypt. Active plans fold into the Coach snapshot's memory block
and survive the prompt-budget drop alongside facts. List, confirm and
soft-delete routes are Coach-gated and narrow the owner from the session.

Key the dashboard-snapshot query cell by locale so a locale switch reads
freshly localised tile copy on its own cell instead of serving the prior
locale's prose until the slot goes stale. The zero-arg key stays
byte-identical, so every prefix-based invalidation keeps reaching the
locale-keyed cells; the offline-banner and preloader match on the prefix.

Migration 0194 (additive CREATE TABLE). Encryption-rotation registry and
script cover the three new Bytes columns.
2026-06-27 01:25:35 +02:00

76 lines
2.6 KiB
TypeScript

/**
* v1.11.1 — worker pipeline for the combined Coach memory-refresh queue.
*
* Runs both background generators for one conversation, sequentially so they
* share the wake-up but each pays its own budget check inside
* `runStatusCompletion`: the rolling conversation summary first, then durable
* fact extraction. Each step is fault-isolated — a failure or no-provider in
* one never sinks the other or the job. Kept out of the route bundle (the
* route imports only `enqueueCoachMemoryRefresh` from `coach-memory-shared`).
*/
import { annotate } from "@/lib/logging/context";
import type { CoachMemoryRefreshPayload } from "./coach-memory-shared";
import { extractAndStoreFacts } from "./facts";
import { extractAndStorePlanProposals } from "./plans";
import { refreshConversationSummary } from "./conversation-summary";
export async function runCoachMemoryRefresh(
payload: CoachMemoryRefreshPayload,
): Promise<void> {
const { conversationId, userId } = payload;
const locale = payload.locale ?? "de";
let summaryStatus = "error";
try {
const result = await refreshConversationSummary(conversationId, userId, {
locale,
});
summaryStatus = result.status;
} catch (err) {
annotate({
action: { name: "coach.memory.refresh.summary_failed" },
meta: { error: err instanceof Error ? err.message : String(err) },
});
}
let factsStatus = "error";
let factsCount = 0;
try {
const result = await extractAndStoreFacts(conversationId, userId, {
locale,
});
factsStatus = result.status;
factsCount = result.count;
} catch (err) {
annotate({
action: { name: "coach.memory.refresh.facts_failed" },
meta: { error: err instanceof Error ? err.message : String(err) },
});
}
// v1.21.3 (B1) — durable goal / if-then plan proposals. Same worker pass
// (no separate pg-boss queue), fault-isolated like the facts step: a failure
// or no-provider here never sinks the summary / facts work or the job. Plans
// are written as `proposed`; the user confirms them through the PATCH route.
let plansStatus = "error";
let plansCount = 0;
try {
const result = await extractAndStorePlanProposals(conversationId, userId, {
locale,
});
plansStatus = result.status;
plansCount = result.count;
} catch (err) {
annotate({
action: { name: "coach.memory.refresh.plans_failed" },
meta: { error: err instanceof Error ? err.message : String(err) },
});
}
annotate({
action: { name: "coach.memory.refresh.done" },
meta: { summaryStatus, factsStatus, factsCount, plansStatus, plansCount },
});
}