feat(worker): S3 3b härda UPDATE_USER_MEMORY — budget, kostnad, anti-påhitt, GDPR-regression

This commit is contained in:
Sven (AAMOS AI)
2026-08-11 03:23:09 +07:00
parent 33b0a8267b
commit 05bba93b90
11 changed files with 678 additions and 62 deletions
+52 -16
View File
@@ -1,4 +1,4 @@
import type { AamosClient } from "@app/ai-contracts";
import { TASK_CONTRACTS, type AamosClient } from "@app/ai-contracts";
import type { MemoryItem, MemoryKind, SignalOrigin } from "@app/shared-types";
/**
@@ -21,12 +21,14 @@ export interface MemoryUpdateProposal {
origin: Exclude<SignalOrigin, "user_stated">;
confidence: number;
expiresAt?: string | null;
/** Händelse-ID:n som stödjer förslaget (R1: grundat, aldrig påhittat). */
sourceEventIds: string[];
}
export interface MemorySyncInput {
scope: "user" | "household";
scopeId: string;
events: Array<{ type: string; occurredAt: string; payload: unknown }>;
events: Array<{ id: string; type: string; occurredAt: string; payload: unknown }>;
existingMemoryKeys: string[];
consentFlags: {
personalization: boolean;
@@ -38,15 +40,23 @@ export interface MemorySyncInput {
}
/**
* Kör minnesuppdatering via AAMOS. Returnerar förslag anroparen (workern)
* persisterar dem i memory_items och publicerar MEMORY_UPDATED-event.
* Kör minnesuppdatering via AAMOS. Returnerar granskade förslag + faktisk
* token/kostnadsanvändning så att workern kan bokföra i ai_usage_counters.
* Utan personaliseringssamtycke körs ingenting.
*
* R1 (grundat, aldrig påhittat): AAMOS får explicit instruktion om att aldrig
* fabricera minnen; svaret valideras mot Zod-kontraktet.
*/
export async function deriveMemoryUpdates(
aamos: AamosClient,
input: MemorySyncInput,
): Promise<MemoryUpdateProposal[]> {
if (!input.consentFlags.personalization) return [];
): Promise<{
proposals: MemoryUpdateProposal[];
usage: { costUsd: number; inputTokens: number; outputTokens: number } | null;
}> {
if (!input.consentFlags.personalization) {
return { proposals: [], usage: null };
}
const result = await aamos.runTask(
"UPDATE_USER_MEMORY",
@@ -59,21 +69,47 @@ export async function deriveMemoryUpdates(
{
subjectRef: pseudonymize(input.scopeId),
consentFlags: input.consentFlags,
systemInstruction:
"Du får ENDAST sammanfatta faktiska events. Hitta ALDRIG på minnen, preferenser eller mönster som inte har explicit stöd i events. Varje förslag måste referera till ett eller flera events via sourceEventIds. ai_inferred-förslag ska ha låg konfidens.",
...(input.localeContext ? { localeContext: input.localeContext } : {}),
...(input.correlationId ? { correlationId: input.correlationId } : {}),
},
);
if (result.status !== "ok" || !result.output) return [];
return result.output.memoryUpdates.map((u) => ({
key: u.key,
kind: u.kind,
summarySv: u.summarySv,
value: u.value,
origin: u.origin,
confidence: u.confidence,
expiresAt: u.expiresAt,
}));
const usage =
result.status === "ok" && result.output
? {
costUsd: result.costUsd ?? 0,
inputTokens: result.inputTokens ?? 0,
outputTokens: result.outputTokens ?? 0,
}
: null;
if (result.status !== "ok" || !result.output) {
return { proposals: [], usage };
}
// Extra Zod-validering: ett brutet kontrakt ska aldrig nå användardata.
const parsed = TASK_CONTRACTS["UPDATE_USER_MEMORY"].output.safeParse(result.output);
if (!parsed.success) {
// eslint-disable-next-line no-console
console.error("UPDATE_USER_MEMORY output bröt mot kontraktet:", parsed.error.message);
return { proposals: [], usage };
}
return {
proposals: parsed.data.memoryUpdates.map((u) => ({
key: u.key,
kind: u.kind,
summarySv: u.summarySv,
value: u.value,
origin: u.origin,
confidence: u.confidence,
expiresAt: u.expiresAt,
sourceEventIds: u.sourceEventIds,
})),
usage,
};
}
export interface MemoryOverviewItem extends MemoryItem {