chore(coach): publish the trail route, list it among the mixed reads, and ship its strings in every locale

This commit is contained in:
Marc-André Bombeck committed 2026-10-06 19:53:43 +02:00
1 parent 2f5877cf97
commit 8e7fd0b873
12 files changed
+248 -61

No files matched your search

+123
View File
@@ -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:
-46
View File
@@ -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."
}
}
}
}
}
+10 -2
View File
@@ -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": {
+10 -2
View File
@@ -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": {
+10 -2
View File
@@ -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": {
+10 -2
View File
@@ -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": {
+10 -2
View File
@@ -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": {
+10 -2
View File
@@ -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": {
+10 -2
View File
@@ -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": {
@@ -102,6 +102,11 @@ export const AI_ROUTES: Readonly<Record<string, AiRouteEntry>> = {
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"],
+1 -1
View File
@@ -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,
+49
View File
@@ -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<ZodOpenApiObject["paths"]> = {
},
},
},
"/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"],