diff --git a/docs/api/openapi.yaml b/docs/api/openapi.yaml index 307c2be02..13c0148df 100644 --- a/docs/api/openapi.yaml +++ b/docs/api/openapi.yaml @@ -9429,6 +9429,47 @@ paths: $ref: "#/components/schemas/ErrorEnvelope" "422": *a4 "429": *a5 + /api/insights/chat/{id}/messages/{messageId}/trail: + get: + tags: + - Insights + summary: Read the trail text of one Coach message + description: "v1.41 — the model-written text of one assistant message's trail: the screened reasoning titles and + summaries by trail entry, the fact texts the turn recalled, and a pending fact proposal. The trail's structure + (phases, statuses, counts) is plaintext on the message's `metricSource.activity`; this route serves only the + text, decrypted for the account that owns the conversation. Fetch lazily when the person opens the trail. A + mixed read: while the Coach's text may not be shown for the record, the answer is 200 with `trail: null` and the + `ai` state beside it. A foreign or unknown conversation or message id maps to 404 (never 403). Auth via cookie + or a full-access Bearer token." + parameters: + - name: id + in: path + required: true + schema: + type: string + description: Conversation id. + - name: messageId + in: path + required: true + schema: + type: string + description: Assistant message id within the conversation. + responses: + "200": + description: The trail text, or null while it may not be served. + content: + application/json: + schema: + $ref: "#/components/schemas/CoachMessageTrailResponse" + "401": *a1 + "404": + description: Conversation or message not found, or not owned by the caller. + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorEnvelope" + "422": *a4 + "429": *a5 /api/insights/chat/{id}: get: tags: @@ -54159,6 +54200,88 @@ components: - withheld additionalProperties: false description: "One stored table, or the reason it is not served. Absence is explicit: a withheld table names why." + CoachMessageTrailResponse: + type: object + properties: + data: + type: object + properties: + trail: + anyOf: + - $ref: "#/components/schemas/CoachTrail" + - type: "null" + ai: + description: The `coach` capability. While unavailable `trail` is null. + $ref: "#/components/schemas/AiCapabilityState" + required: + - trail + - ai + additionalProperties: false + error: + type: "null" + meta: + type: object + properties: + requestId: + type: string + additionalProperties: false + required: + - data + - error + additionalProperties: false + CoachTrail: + type: object + properties: + entries: + type: array + items: + type: object + properties: + id: + type: string + pattern: ^a[1-9]\d?$ + title: + type: string + maxLength: 80 + text: + type: string + maxLength: 400 + required: + - id + additionalProperties: false + recalled: + type: array + items: + type: string + maxLength: 160 + proposal: + type: object + properties: + proposalId: + type: string + pattern: ^[A-Za-z0-9_-]{1,64}$ + category: + type: string + enum: + - preference + - goal + - context + - condition + - constraint + - medication + fact: + type: string + maxLength: 160 + required: + - proposalId + - category + - fact + additionalProperties: false + required: + - entries + additionalProperties: false + description: v1.41 — the model-written text of a turn's trail and the fact texts it touched. Encrypted at rest and + served only to the account that owns the conversation. CoachConversationDetailResponse: type: object properties: diff --git a/messages/_pending/b.json b/messages/_pending/b.json deleted file mode 100644 index 29c930ff8..000000000 --- a/messages/_pending/b.json +++ /dev/null @@ -1,46 +0,0 @@ -{ - "en": { - "coach": { - "clarify": { - "comparison": { - "baseline90d": "Compare with my usual 90 days" - }, - "anchor": { - "illness": "Since the illness of {date}" - } - } - }, - "insights": { - "coach": { - "memory": { - "declined": "Okay, I won't." - }, - "plan": { - "declined": "Okay, not now." - } - } - } - }, - "de": { - "coach": { - "clarify": { - "comparison": { - "baseline90d": "Mit meinen üblichen 90 Tagen vergleichen" - }, - "anchor": { - "illness": "Seit der Erkrankung vom {date}" - } - } - }, - "insights": { - "coach": { - "memory": { - "declined": "Alles klar, dann nicht." - }, - "plan": { - "declined": "Alles klar, dann nicht jetzt." - } - } - } - } -} diff --git a/messages/de.json b/messages/de.json index d1bfa9bd9..856bbd8e1 100644 --- a/messages/de.json +++ b/messages/de.json @@ -3185,12 +3185,14 @@ "undo": "Rückgängig", "accept": "Ja, merken", "decline": "Nein", - "confirmed": "Gemerkt." + "confirmed": "Gemerkt.", + "declined": "Alles klar, dann nicht." }, "plan": { "accept": "Plan übernehmen", "decline": "Nicht jetzt", - "confirmed": "Ist notiert. Ich schaue in {days} Tagen mit dir drauf." + "confirmed": "Ist notiert. Ich schaue in {days} Tagen mit dir drauf.", + "declined": "Alles klar, dann nicht jetzt." }, "assumption": { "line": "Angenommen: {value}", @@ -10033,6 +10035,12 @@ "last90days": "Letzte 90 Tage", "lastYear": "Letzte 12 Monate", "allTime": "Gesamter Zeitraum" + }, + "comparison": { + "baseline90d": "Mit meinen üblichen 90 Tagen vergleichen" + }, + "anchor": { + "illness": "Seit der Erkrankung vom {date}" } }, "reuse": { diff --git a/messages/en.json b/messages/en.json index d193ae5ec..758b965b5 100644 --- a/messages/en.json +++ b/messages/en.json @@ -3185,12 +3185,14 @@ "undo": "Undo", "accept": "Yes, remember it", "decline": "No", - "confirmed": "Got it, I'll remember." + "confirmed": "Got it, I'll remember.", + "declined": "Okay, I won't." }, "plan": { "accept": "Take on this plan", "decline": "Not now", - "confirmed": "Noted. I'll check in with you in {days} days." + "confirmed": "Noted. I'll check in with you in {days} days.", + "declined": "Okay, not now." }, "assumption": { "line": "Assumed: {value}", @@ -10033,6 +10035,12 @@ "last90days": "Last 90 days", "lastYear": "Last 12 months", "allTime": "All time" + }, + "comparison": { + "baseline90d": "Compare with my usual 90 days" + }, + "anchor": { + "illness": "Since the illness of {date}" } }, "reuse": { diff --git a/messages/es.json b/messages/es.json index 7892f3e77..c1793bf8c 100644 --- a/messages/es.json +++ b/messages/es.json @@ -3185,12 +3185,14 @@ "undo": "Deshacer", "accept": "Sí, recuérdalo", "decline": "No", - "confirmed": "Entendido, lo recordaré." + "confirmed": "Entendido, lo recordaré.", + "declined": "De acuerdo, no lo haré." }, "plan": { "accept": "Adoptar este plan", "decline": "Ahora no", - "confirmed": "Anotado. Lo revisaremos juntos dentro de {days} días." + "confirmed": "Anotado. Lo revisaremos juntos dentro de {days} días.", + "declined": "De acuerdo, ahora no." }, "assumption": { "line": "Supuesto: {value}", @@ -10033,6 +10035,12 @@ "last90days": "Últimos 90 días", "lastYear": "Últimos 12 meses", "allTime": "Todo el periodo" + }, + "comparison": { + "baseline90d": "Comparar con mis 90 días habituales" + }, + "anchor": { + "illness": "Desde la enfermedad del {date}" } }, "reuse": { diff --git a/messages/fr.json b/messages/fr.json index 1d67565f3..31e54cfe0 100644 --- a/messages/fr.json +++ b/messages/fr.json @@ -3185,12 +3185,14 @@ "undo": "Annuler", "accept": "Oui, retiens-le", "decline": "Non", - "confirmed": "C'est noté, je m'en souviendrai." + "confirmed": "C'est noté, je m'en souviendrai.", + "declined": "D'accord, je ne le ferai pas." }, "plan": { "accept": "Adopter ce plan", "decline": "Pas maintenant", - "confirmed": "C'est noté. On fait le point ensemble dans {days} jours." + "confirmed": "C'est noté. On fait le point ensemble dans {days} jours.", + "declined": "D'accord, pas maintenant." }, "assumption": { "line": "Hypothèse : {value}", @@ -10033,6 +10035,12 @@ "last90days": "90 derniers jours", "lastYear": "12 derniers mois", "allTime": "Toute la période" + }, + "comparison": { + "baseline90d": "Comparer avec mes 90 jours habituels" + }, + "anchor": { + "illness": "Depuis la maladie du {date}" } }, "reuse": { diff --git a/messages/it.json b/messages/it.json index b39391e9c..d1c2c9047 100644 --- a/messages/it.json +++ b/messages/it.json @@ -3185,12 +3185,14 @@ "undo": "Annulla", "accept": "Sì, ricordalo", "decline": "No", - "confirmed": "Ricevuto, me lo ricorderò." + "confirmed": "Ricevuto, me lo ricorderò.", + "declined": "D'accordo, non lo faccio." }, "plan": { "accept": "Adotta questo piano", "decline": "Non ora", - "confirmed": "Annotato. Ne riparliamo insieme tra {days} giorni." + "confirmed": "Annotato. Ne riparliamo insieme tra {days} giorni.", + "declined": "D'accordo, non ora." }, "assumption": { "line": "Ipotesi: {value}", @@ -10033,6 +10035,12 @@ "last90days": "Ultimi 90 giorni", "lastYear": "Ultimi 12 mesi", "allTime": "Intero periodo" + }, + "comparison": { + "baseline90d": "Confronta con i miei soliti 90 giorni" + }, + "anchor": { + "illness": "Dalla malattia del {date}" } }, "reuse": { diff --git a/messages/ko.json b/messages/ko.json index 3365b5585..c792625dd 100644 --- a/messages/ko.json +++ b/messages/ko.json @@ -3185,12 +3185,14 @@ "undo": "실행 취소", "accept": "네, 기억해 줘", "decline": "아니요", - "confirmed": "알겠어요, 기억해 둘게요." + "confirmed": "알겠어요, 기억해 둘게요.", + "declined": "알겠어요, 저장하지 않을게요." }, "plan": { "accept": "이 계획 시작하기", "decline": "나중에", - "confirmed": "기록했어요. {days}일 뒤에 함께 살펴볼게요." + "confirmed": "기록했어요. {days}일 뒤에 함께 살펴볼게요.", + "declined": "알겠어요, 지금은 하지 않을게요." }, "assumption": { "line": "가정: {value}", @@ -10033,6 +10035,12 @@ "last90days": "최근 90일", "lastYear": "최근 12개월", "allTime": "전체 기간" + }, + "comparison": { + "baseline90d": "평소 90일과 비교하기" + }, + "anchor": { + "illness": "{date} 질환 이후부터" } }, "reuse": { diff --git a/messages/pl.json b/messages/pl.json index 9602d2454..7bdc35f33 100644 --- a/messages/pl.json +++ b/messages/pl.json @@ -3185,12 +3185,14 @@ "undo": "Cofnij", "accept": "Tak, zapamiętaj", "decline": "Nie", - "confirmed": "Jasne, zapamiętam." + "confirmed": "Jasne, zapamiętam.", + "declined": "Dobrze, nie zapiszę tego." }, "plan": { "accept": "Przyjmij ten plan", "decline": "Nie teraz", - "confirmed": "Zanotowane. Wrócimy do tego razem za {days} dni." + "confirmed": "Zanotowane. Wrócimy do tego razem za {days} dni.", + "declined": "Dobrze, nie teraz." }, "assumption": { "line": "Założenie: {value}", @@ -10033,6 +10035,12 @@ "last90days": "Ostatnie 90 dni", "lastYear": "Ostatnie 12 miesięcy", "allTime": "Cały okres" + }, + "comparison": { + "baseline90d": "Porównaj z moimi zwykłymi 90 dniami" + }, + "anchor": { + "illness": "Od choroby z {date}" } }, "reuse": { diff --git a/src/app/api/__tests__/ai-route-inventory.ts b/src/app/api/__tests__/ai-route-inventory.ts index 1160e4ee8..3e314e53d 100644 --- a/src/app/api/__tests__/ai-route-inventory.ts +++ b/src/app/api/__tests__/ai-route-inventory.ts @@ -102,6 +102,11 @@ export const AI_ROUTES: Readonly> = { capabilities: ["coach"], why: "POST streams a Coach reply from a model. The history GET in the same file asks nothing.", }, + "src/app/api/insights/chat/[id]/messages/[messageId]/trail/route.ts": { + kind: "mixed", + capabilities: ["coach"], + why: "The trail's reasoning text is model-written and served only while the Coach's text may be shown; otherwise 200 with trail null and the ai state.", + }, "src/app/api/insights/generate/route.ts": { kind: "action", capabilities: ["briefing"], diff --git a/src/lib/ai/coach/stream-events.ts b/src/lib/ai/coach/stream-events.ts index dd8749ccc..5c54094bb 100644 --- a/src/lib/ai/coach/stream-events.ts +++ b/src/lib/ai/coach/stream-events.ts @@ -21,7 +21,7 @@ import { import { PLAN_REVIEW_DAYS, REMEMBER_FACT_MAX_CHARS, -} from "@/lib/ai/coach/memory/contract"; +} from "@/lib/ai/coach/memory/shared"; import { SUGGESTED_ACTION_TYPES, type CheckupIntervalId, diff --git a/src/lib/openapi/routes/coach.ts b/src/lib/openapi/routes/coach.ts index 94b9dbbf3..157e517b5 100644 --- a/src/lib/openapi/routes/coach.ts +++ b/src/lib/openapi/routes/coach.ts @@ -44,6 +44,7 @@ import { coachProvenanceSchema, coachResultEntrySchema, coachStreamEventSchema, + coachTrailSchema, } from "@/lib/ai/coach/stream-events"; import { coachAttachmentCreateSchema, @@ -870,6 +871,54 @@ export const coachPaths: NonNullable = { }, }, }, + "/api/insights/chat/{id}/messages/{messageId}/trail": { + get: { + tags: ["Insights"], + summary: "Read the trail text of one Coach message", + description: + "v1.41 — the model-written text of one assistant message's trail: the screened reasoning titles and summaries by trail entry, the fact texts the turn recalled, and a pending fact proposal. The trail's structure (phases, statuses, counts) is plaintext on the message's `metricSource.activity`; this route serves only the text, decrypted for the account that owns the conversation. Fetch lazily when the person opens the trail. A mixed read: while the Coach's text may not be shown for the record, the answer is 200 with `trail: null` and the `ai` state beside it. A foreign or unknown conversation or message id maps to 404 (never 403). Auth via cookie or a full-access Bearer token.", + parameters: [ + { + name: "id", + in: "path", + required: true, + schema: { type: "string" }, + description: "Conversation id.", + }, + { + name: "messageId", + in: "path", + required: true, + schema: { type: "string" }, + description: "Assistant message id within the conversation.", + }, + ], + responses: { + "200": { + description: "The trail text, or null while it may not be served.", + content: { + "application/json": { + schema: dataEnvelope( + z.object({ + trail: coachTrailSchema.nullable(), + ai: aiCapabilityState.describe( + "The `coach` capability. While unavailable `trail` is null.", + ), + }), + "CoachMessageTrailResponse", + ), + }, + }, + }, + "404": { + description: + "Conversation or message not found, or not owned by the caller.", + content: { "application/json": { schema: errorEnvelope } }, + }, + ...stdResponses, + }, + }, + }, "/api/insights/chat/{id}": { get: { tags: ["Insights"],