fix(coach): hold restored dialog ids to the shapes the server mints

Table refs, step ids, chip ids, a chip's anchor ref, clarification choice
ids and a reused table's source are now validated against their exact
server-minted patterns when a stored reply is read back. Some of them are
written into the model's context on a later turn (the earlier-tables list,
the "keep looking" fetched list), so a stored row edited by hand can no
longer carry text into a prompt through them.
This commit is contained in:
Marc-André Bombeck committed 2026-09-27 13:22:09 +02:00
1 parent d6ac4acad2
commit 6a458ac5b0
2 files changed
+36 -9

No files matched your search

+12
View File
@@ -50492,6 +50492,7 @@ components:
properties:
id:
type: string
pattern: ^s[1-9]\d{0,2}$
description: "`s1`..`s12`, unique within a turn."
tool:
type: string
@@ -50560,6 +50561,7 @@ components:
resultRef:
description: The table this step produced (`r1`..), when it produced one.
type: string
pattern: ^r[1-6]$
required:
- id
- tool
@@ -50685,6 +50687,7 @@ components:
properties:
ref:
type: string
pattern: ^r[1-6]$
description: "`r1`..`r6`, unique within a message."
source:
type: object
@@ -50764,8 +50767,10 @@ components:
properties:
messageId:
type: string
pattern: ^[A-Za-z0-9_-]{1,64}$
ref:
type: string
pattern: ^r[1-6]$
required:
- messageId
- ref
@@ -50787,6 +50792,7 @@ components:
properties:
id:
type: string
pattern: ^f[1-3]$
description: "`f1`..`f3`."
kind:
type: string
@@ -50808,6 +50814,7 @@ components:
properties:
ref:
type: string
pattern: ^(?:r[1-6])?$
domain:
$ref: "#/components/schemas/CoachStepDomain"
window:
@@ -50868,6 +50875,7 @@ components:
properties:
id:
type: string
pattern: ^c[1-4]$
labelKey:
type: string
label:
@@ -50978,6 +50986,7 @@ components:
properties:
ref:
type: string
pattern: ^r[1-6]$
description: "`r1`..`r6`, unique within a message."
source:
type: object
@@ -51057,8 +51066,10 @@ components:
properties:
messageId:
type: string
pattern: ^[A-Za-z0-9_-]{1,64}$
ref:
type: string
pattern: ^r[1-6]$
required:
- messageId
- ref
@@ -51231,6 +51242,7 @@ components:
properties:
ref:
type: string
pattern: ^r[1-6]$
withheld:
type: string
enum:
+24 -9
View File
@@ -51,11 +51,23 @@ export const coachStepDomainSchema = z
const coachResultGranularitySchema = z.enum(["day", "week", "month"]);
const coachResultPeriodSchema = z.enum(["current", "previous", "yearAgo"]);
// The ids and names below are restored from stored provenance and some of
// them are written back into the model's context (`m<k>.r<n>` lines, the
// "keep looking" fetched list). Each is held to its exact server-minted
// shape, so a stored row edited by hand cannot carry text into a prompt.
/** A table of a message: `r1`..`r6`. */
const resultRefSchema = z.string().regex(/^r[1-6]$/);
/** A server-minted row id (cuid). */
const messageIdSchema = z.string().regex(/^[A-Za-z0-9_-]{1,64}$/);
// ── Steps ─────────────────────────────────────────────────────────────────
export const coachStepSchema = z
.object({
id: z.string().describe("`s1`..`s12`, unique within a turn."),
id: z
.string()
.regex(/^s[1-9]\d{0,2}$/)
.describe("`s1`..`s12`, unique within a turn."),
tool: z.enum([...COACH_TOOL_NAMES, "show_result", "snapshot"]),
labelKey: z
.string()
@@ -84,8 +96,7 @@ export const coachStepSchema = z
"invalid_arguments",
])
.optional(),
resultRef: z
.string()
resultRef: resultRefSchema
.optional()
.describe("The table this step produced (`r1`..), when it produced one."),
})
@@ -140,7 +151,7 @@ const coachResultSourceSchema = z.object({
});
const coachResultMetaShape = {
ref: z.string().describe("`r1`..`r6`, unique within a message."),
ref: resultRefSchema.describe("`r1`..`r6`, unique within a message."),
source: coachResultSourceSchema,
shape: z.enum(["timeSeries", "categoryCounts", "distribution", "single"]),
titleKey: z.string(),
@@ -153,7 +164,7 @@ const coachResultMetaShape = {
"True when the answer referenced the table (shown expanded); false when it sits under the data-used disclosure.",
),
reusedFrom: z
.object({ messageId: z.string(), ref: z.string() })
.object({ messageId: messageIdSchema, ref: resultRefSchema })
.optional()
.describe("Set when the table was copied from an earlier message."),
};
@@ -185,7 +196,7 @@ export const coachResultTableSchema = z
});
const coachResultWithheldSchema = z.object({
ref: z.string(),
ref: resultRefSchema,
withheld: z
.enum(["module_disabled", "unavailable"])
.describe(
@@ -232,7 +243,10 @@ export const coachMethodSchema = z
export const coachFollowUpSchema = z
.object({
id: z.string().describe("`f1`..`f3`."),
id: z
.string()
.regex(/^f[1-3]$/)
.describe("`f1`..`f3`."),
kind: z.enum([
"widen_window",
"previous_period",
@@ -248,7 +262,8 @@ export const coachFollowUpSchema = z
.describe("Rendered on the server from a catalog; never model text."),
anchor: z
.object({
ref: z.string(),
// Empty for a related metric read without a table of its own.
ref: z.string().regex(/^(?:r[1-6])?$/),
domain: coachStepDomainSchema,
window: coachWindowEnum.optional(),
granularity: coachResultGranularitySchema.optional(),
@@ -276,7 +291,7 @@ export const coachClarificationSchema = z
choices: z
.array(
z.object({
id: z.string(),
id: z.string().regex(/^c[1-4]$/),
labelKey: z.string(),
label: z.string(),
value: z.object({