Merge remote-tracking branch 'origin/release/v1.41.0' into v141/d-settings

This commit is contained in:
Marc-André Bombeck committed 2026-10-06 19:44:56 +02:00
commit 656d8b8563
52 files changed
+6048 -850

No files matched your search

+156 -10
View File
@@ -29,6 +29,11 @@ import {
} from "@/lib/insights-layout";
import { COACH_CONVERSATION_TITLE_MAX } from "@/lib/ai/coach/types";
import { COACH_FACT_CATEGORIES } from "@/lib/ai/coach/facts";
import { COACH_FACT_SOURCES } from "@/lib/ai/coach/memory/shared";
import {
coachFactCreateSchema,
coachFactPatchSchema,
} from "@/lib/validations/coach-fact";
import { COACH_PLAN_STATUSES } from "@/lib/ai/coach/plans";
import {
COACH_PLAN_SCOPES,
@@ -273,24 +278,81 @@ const insightsLayoutResult = insightsLayoutSchema
"Resolved Insights layout plus the optimistic-concurrency `updatedAt` token.",
});
// ── Coach facts (v1.11.1) ────────────────────────────────────────────
// Read + delete surface for the durable facts the Coach extracts. Facts
// are server-extracted, not user-authored, so there is no create/update
// shape — only list, bulk-clear, and single-delete responses.
// ── Coach facts (v1.11.1, v1.41) ─────────────────────────────────────
// The Coach's memory list: list, the remember button (create from one of the
// caller's own messages), edit the wording, bulk-clear and single-delete.
const coachFactCategory = z
.enum(COACH_FACT_CATEGORIES)
.describe(
"App-side closed category: preference | goal | context | condition | constraint | medication (v1.41).",
);
const coachFactSource = z
.enum(COACH_FACT_SOURCES)
.describe(
"v1.41 — where the fact came from: `user` (the remember button, or a proposal the caller confirmed), `coach` (saved during a turn), `extracted` (the background extraction; every fact older than v1.41), `pattern` (the deterministic matcher). `proposed` is never listed.",
);
const coachFactItem = z.object({
id: z.string(),
category: z
.enum(COACH_FACT_CATEGORIES)
.describe(
"App-side closed category: preference | condition | goal | constraint | context.",
),
category: coachFactCategory,
text: z.string().describe("Decrypted fact text."),
confidence: z
.number()
.int()
.describe("0..100 server-assigned extraction confidence."),
source: coachFactSource,
sourceConversationId: z
.string()
.nullable()
.describe("The conversation the fact came out of, when known."),
sourceMessageId: z
.string()
.nullable()
.describe("v1.41 — the message the fact came out of, when known."),
lastUsedAt: z.iso
.datetime({ offset: true })
.nullable()
.describe("v1.41 — when the fact last went into a Coach turn."),
createdAt: z.iso.datetime({ offset: true }),
updatedAt: z.iso
.datetime({ offset: true })
.describe("v1.41 — when the fact was last written or edited."),
});
const coachFactCreateRequest = coachFactCreateSchema.meta({
id: "CoachFactCreateRequest",
description:
"v1.41 — the remember button: the id of one of the caller's own Coach messages. Never the text: the server reads the message it stored. Strict: unknown keys 422.",
});
const coachFactPatchRequest = coachFactPatchSchema.meta({
id: "CoachFactPatchRequest",
description:
"v1.41 — the fact's new wording (3..160 characters). Only the text; category and source stay. Strict: unknown keys 422.",
});
const coachFactWrittenItem = z.object({
id: z.string(),
category: coachFactCategory,
text: z.string().describe("The fact as stored."),
source: coachFactSource,
});
const coachFactCreatedResponse = z.object({
fact: coachFactWrittenItem,
created: z
.boolean()
.describe(
"False when the caller's memory already held the same fact (or a proposal for it, now confirmed); the existing fact is returned.",
),
});
const coachFactUpdatedResponse = z.object({
fact: coachFactWrittenItem.extend({
updatedAt: z.iso.datetime({ offset: true }),
}),
});
const coachFactsListResponse = z.object({
@@ -1116,7 +1178,7 @@ export const coachPaths: NonNullable<ZodOpenApiObject["paths"]> = {
tags: ["Insights"],
summary: "List the caller's durable Coach facts",
description:
"v1.11.1 — returns the active facts the Coach has extracted about the caller (highest-confidence then newest first), each decrypted on the fly. The GDPR 'what do you know about me' surface. Never AI-gated: the facts stay readable and erasable while the Coach is unavailable for any reason. Auth via cookie or Bearer; the owner is always narrowed from the session, never the body. Undecryptable rows are omitted rather than failing the read.",
"v1.11.1 — returns the active facts the Coach keeps about the caller (highest-confidence then newest first), each decrypted on the fly. The GDPR 'what do you know about me' surface. v1.41 — each fact carries its source, the message it came from and when it was last used in a turn; a health fact still waiting for the caller's confirmation is not listed. Never AI-gated: the facts stay readable and erasable while the Coach is unavailable for any reason. Auth via cookie or Bearer; the owner is always narrowed from the session, never the body. Undecryptable rows are omitted rather than failing the read.",
responses: {
"200": {
description: "The caller's active facts.",
@@ -1129,6 +1191,51 @@ export const coachPaths: NonNullable<ZodOpenApiObject["paths"]> = {
...stdResponses,
},
},
post: {
tags: ["Insights"],
summary: "Remember one of the caller's own Coach messages",
description:
'v1.41 — the remember button. Saves the named user message as a fact with `source: "user"`: the text is the message the server stored, filed as `medication` or `condition` when it names one, `context` otherwise. The tap is the caller\'s confirmation, so a health message is saved too. A message the caller does not own, or an assistant message, is a 404. Saving the same fact again returns the existing one with `created: false`. Gated on the Coach module, never on AI. Auth via cookie or Bearer.',
requestBody: {
required: true,
content: {
"application/json": { schema: coachFactCreateRequest },
},
},
responses: {
"201": {
description: "The fact was saved.",
content: {
"application/json": {
schema: dataEnvelope(
coachFactCreatedResponse,
"CoachFactCreated",
),
},
},
},
"200": {
description: "The caller's memory already held this fact.",
content: {
"application/json": {
schema: dataEnvelope(
coachFactCreatedResponse,
"CoachFactAlreadyKnown",
),
},
},
},
"403": {
description: MODULE_DISABLED_DESCRIPTION,
content: { "application/json": { schema: errorEnvelope } },
},
"404": {
description: "Message not found or not one of the caller's own.",
content: { "application/json": { schema: errorEnvelope } },
},
...stdResponses,
},
},
delete: {
tags: ["Insights"],
summary: "Forget all of the caller's Coach facts",
@@ -1151,6 +1258,45 @@ export const coachPaths: NonNullable<ZodOpenApiObject["paths"]> = {
},
},
"/api/insights/coach/facts/{id}": {
patch: {
tags: ["Insights"],
summary: "Edit one Coach fact's wording",
description:
"v1.41 — rewrites the text of a fact the caller owns (re-encrypted at rest); category and source stay. A proposal still waiting for the caller's confirmation, an unknown, cross-user or deleted id are all a 404. Never AI-gated. Auth via cookie or Bearer.",
parameters: [
{
name: "id",
in: "path",
required: true,
schema: { type: "string" },
description: "Fact id.",
},
],
requestBody: {
required: true,
content: {
"application/json": { schema: coachFactPatchRequest },
},
},
responses: {
"200": {
description: "The fact after the edit.",
content: {
"application/json": {
schema: dataEnvelope(
coachFactUpdatedResponse,
"CoachFactUpdated",
),
},
},
},
"404": {
description: "Fact not found or not owned by the caller.",
content: { "application/json": { schema: errorEnvelope } },
},
...stdResponses,
},
},
delete: {
tags: ["Insights"],
summary: "Forget one Coach fact",