Files
HealthLog/src/lib/ai/coach/coach-memory-shared.ts
T
Marc-André BombeckandClaude Opus 4.8 7e29595a71 feat(coach): wire the combined memory-refresh worker queue
Add the coach-memory-refresh pg-boss queue: a fire-and-forget enqueue
contract (coach-memory-shared) and a worker pipeline that runs the
conversation-summary refresh then the fact extraction for one long
conversation, each fault-isolated and budget-gated. Register the queue
in allQueues + a boss.work handler in the worker, with a source-grep
inventory guard so the queue can never ship unregistered.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 10:18:52 +02:00

65 lines
2.3 KiB
TypeScript

/**
* v1.11.1 — generator-free contract for the Coach long-term-memory refresh
* queue. The chat route enqueues a single-conversation refresh here without
* importing the concrete generators (which would pull the provider chain into
* the route bundle). The worker handler (`runCoachMemoryRefresh`) calls the
* summary + fact generators; it re-uses the queue name from here so there is
* one source of truth.
*
* One combined queue does BOTH conversation-summary refresh and durable-fact
* extraction in a single worker job: when a long conversation crosses the
* history-window cap, both are usually due together, so one job + one
* singleton window avoids a second queue and a redundant wake-up.
*
* Mirrors `period-narrative-shared.ts`: queue name + payload type + enqueue
* helper here, the concrete dispatch in the worker.
*/
import { getGlobalBoss } from "@/lib/jobs/boss-instance";
import { annotate } from "@/lib/logging/context";
export const COACH_MEMORY_REFRESH_QUEUE = "coach-memory-refresh";
export interface CoachMemoryRefreshPayload {
conversationId: string;
userId: string;
/** Locale to compose the summary / facts prose in; defaults to "de". */
locale?: "de" | "en";
}
/**
* Fire-and-forget enqueue from the chat turn once a conversation has grown past
* the history-window cap. A `singletonKey` per conversation collapses repeated
* turns within a short window into one queued job. No-ops cleanly when the
* global boss is unavailable (a web process without an embedded worker) — the
* memory simply stays as-is until the next eligible turn.
*/
export async function enqueueCoachMemoryRefresh(payload: {
conversationId: string;
userId: string;
locale: "de" | "en";
}): Promise<void> {
const boss = getGlobalBoss();
if (!boss) return;
try {
await boss.send(
COACH_MEMORY_REFRESH_QUEUE,
{
conversationId: payload.conversationId,
userId: payload.userId,
locale: payload.locale,
} satisfies CoachMemoryRefreshPayload,
{
singletonKey: `refresh:${payload.conversationId}`,
singletonSeconds: 120,
},
);
annotate({
action: { name: "coach.memory.refresh.enqueued" },
meta: { locale: payload.locale },
});
} catch {
// Best-effort — a failure just leaves the Coach memory unchanged until the
// next eligible turn re-enqueues.
}
}