fix(coach): keep a turn's fact and plan only with the answer that carries them

remember_fact and propose_plan wrote their rows during the tool round. An
answer that was then blocked, failed or abandoned left the fact in memory
without its note and Undo, and left the plan listed, activatable and fed
into later turns and the briefing. Both are now staged under an id minted
up front and written in the transaction that stores the assistant message,
only when its provenance carries them. A background proposal counts as
offered only once an answer carrying it is stored.

Plan text is model text: ifCue, thenAction and target pass the outbound
dose and risk screen in the person's locale, with their scheduled doses and
medication names, and the injection screen; a trip declines as "unsafe".

The trail route serves recalled facts and the proposal's fact to the owner
whatever the AI capability (only the model-written entries are gated), and
withholds medication facts while the medications module is off.

Accepting a fact proposal is decided under a per-message advisory lock, so
concurrent taps write one fact. Editing a fact re-reads its wording: text
naming a medication moves to "medication", other health wording moves a
non-health fact to "condition", and a health fact the person edited counts
as confirmed.
This commit is contained in:
Marc-André Bombeck committed 2026-10-06 21:49:27 +02:00
1 parent fac55720a4
commit 5361239ae5
21 files changed
+1176 -157

No files matched your search

+19 -14
View File
@@ -9434,13 +9434,15 @@ paths:
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."
description: "v1.41 — the text of one assistant message's trail: the screened reasoning titles and summaries by trail
entry (model-written), the fact texts the turn recalled, and a pending fact proposal (the person's own data).
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, `entries` is empty and
the `ai` state rides beside it, but `recalled` and `proposal` are still served, since they are the person's
data. While the medications module is off, recalled facts and a proposal that concern a medication are left out.
`trail` is null when the message has no trail or nothing in it may be served. 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
@@ -9456,7 +9458,7 @@ paths:
description: Assistant message id within the conversation.
responses:
"200":
description: The trail text, or null while it may not be served.
description: The trail text, or null when there is none or nothing in it may be served.
content:
application/json:
schema:
@@ -9891,9 +9893,11 @@ paths:
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.
description: "v1.41 — rewrites the text of a fact the caller owns (re-encrypted at rest) and files it by its new
wording: text that names a medication moves to `medication`, so the medications module keeps filtering it; other
health wording moves a non-health category to `condition`; otherwise the category stays. A health fact the
caller edited counts as confirmed (`source: \"user\"`). 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
@@ -27052,8 +27056,9 @@ components:
required:
- fact
additionalProperties: false
description: "v1.41 — the fact's new wording (3..160 characters). Only the text; category and source stay. Strict:
unknown keys 422."
description: "v1.41 — the fact's new wording (3..160 characters). Only the text: the server reads the new wording to
file it (a fact that names a medication moves to `medication`, other health wording moves a non-health fact to
`condition`) and a health fact the caller wrote counts as confirmed. Strict: unknown keys 422."
UpdateEmergencyProfileRequest:
type: object
properties:
@@ -54211,7 +54216,7 @@ components:
- $ref: "#/components/schemas/CoachTrail"
- type: "null"
ai:
description: The `coach` capability. While unavailable `trail` is null.
description: The `coach` capability. While unavailable `trail.entries` is empty.
$ref: "#/components/schemas/AiCapabilityState"
required:
- trail