mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 21:10:59 +00:00
refactor(coach): keep the table storage budget in the pure refs module
The backup validation now reads the per-message table ceiling from results/refs, which imports nothing but types. Reading it from results/project pulled the tool executor and the message store into the snapshot builder's import graph, and the free-text sanitiser guard then walked files that never build a prompt.
This commit is contained in:
1 parent
874d02294e
commit
eeda21c7a1
7 files changed
+59
-38
No files matched your search
@@ -23,7 +23,7 @@ import {
|
||||
coachStepSchema,
|
||||
} from "./stream-events";
|
||||
import { COACH_CONVERSATION_TITLE_MAX } from "./types";
|
||||
import { RESULTS_MAX_BYTES, fitResultsToStorage } from "./results/project";
|
||||
import { RESULTS_MAX_BYTES, fitResultsToStorage } from "./results/refs";
|
||||
import {
|
||||
isCheckupIntervalId,
|
||||
isSuggestedActionType,
|
||||
@@ -609,7 +609,20 @@ export async function recordProactiveNudge(
|
||||
// newest messages up to this cap cover the rendered window without the
|
||||
// unbounded per-open AES-decrypt cost. The response shape is unchanged —
|
||||
// the messages array still arrives oldest->newest (see the reverse below).
|
||||
const CONVERSATION_MESSAGE_DETAIL_CAP = 200;
|
||||
export const CONVERSATION_MESSAGE_DETAIL_CAP = 200;
|
||||
|
||||
/**
|
||||
* The assistant messages of a conversation written before `before`. The
|
||||
* caller has already read the conversation owner-narrowed; this only counts.
|
||||
*/
|
||||
export function countAssistantMessagesBefore(
|
||||
conversationId: string,
|
||||
before: Date,
|
||||
): Promise<number> {
|
||||
return prisma.coachMessage.count({
|
||||
where: { conversationId, role: "assistant", createdAt: { lt: before } },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch one conversation + its messages, decrypting each body on
|
||||
|
||||
@@ -287,7 +287,7 @@ describe("deriveChartSpec: time series", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("leaves the reading count off the chart", () => {
|
||||
it("leaves the reading count off the chart", () => {
|
||||
const spec = deriveChartSpec(daySeries("pulse", [60, 62], "bpm"));
|
||||
expect(spec && "series" in spec ? spec.series : []).not.toContain(
|
||||
"readings",
|
||||
|
||||
@@ -22,34 +22,6 @@ import { MAX_RESULTS_PER_TURN, isTurnResultRef } from "./refs";
|
||||
/** At most this many rows reach a table; a year of days fits. */
|
||||
export const RESULT_TABLE_MAX_ROWS = 400;
|
||||
|
||||
/**
|
||||
* The at-rest ceiling for one message's tables, as JSON before encryption.
|
||||
* Tables arrive trimmed to 400 rows; a message whose tables still exceed
|
||||
* this keeps the leading tables that fit.
|
||||
*/
|
||||
export const RESULTS_MAX_BYTES = 128 * 1024;
|
||||
|
||||
/**
|
||||
* The tables a message keeps: the leading ones whose JSON fits
|
||||
* `RESULTS_MAX_BYTES`, whole tables dropped from the end. Applied once,
|
||||
* before the turn streams them, so the tables the person sees live are the
|
||||
* tables a reload reads back, and the metadata never names a table the
|
||||
* ciphertext does not hold.
|
||||
*/
|
||||
export function fitResultsToStorage(
|
||||
results: readonly CoachResultTable[],
|
||||
): CoachResultTable[] {
|
||||
const kept = results.slice(0, MAX_RESULTS_PER_TURN);
|
||||
while (
|
||||
kept.length > 0 &&
|
||||
new TextEncoder().encode(JSON.stringify(kept)).byteLength >
|
||||
RESULTS_MAX_BYTES
|
||||
) {
|
||||
kept.pop();
|
||||
}
|
||||
return kept;
|
||||
}
|
||||
|
||||
/** One tool call of this turn, as the loop settled it. */
|
||||
export interface SettledToolCall {
|
||||
name: string;
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
* in the tool result it cites from.
|
||||
* - `m<k>.r<n>` — table `r<n>` of an earlier assistant message of the same
|
||||
* conversation, `k` being that message's 1-based position among the
|
||||
* assistant messages the turn loaded. The names are drawn once per turn
|
||||
* conversation's assistant messages (stable however many are loaded). The names are drawn once per turn
|
||||
* from the conversation the turn already read (owner-narrowed), and
|
||||
* `show_result` resolves only against that same list, so a name can never
|
||||
* reach a table of another conversation or another account.
|
||||
@@ -17,7 +17,7 @@
|
||||
*
|
||||
* Pure: no database, no provider. The reply guards import this module.
|
||||
*/
|
||||
import type { CoachResultMeta } from "@/lib/ai/coach/types";
|
||||
import type { CoachResultMeta, CoachResultTable } from "@/lib/ai/coach/types";
|
||||
|
||||
/** At most this many tables per message (`r1`..`r6`). */
|
||||
export const MAX_RESULTS_PER_TURN = 6;
|
||||
@@ -52,7 +52,7 @@ export function createResultRefAllocator(): ResultRefAllocator {
|
||||
/** One earlier assistant message that holds tables. */
|
||||
export interface PriorResultTurn {
|
||||
messageId: string;
|
||||
/** 1-based position among the conversation's loaded assistant messages. */
|
||||
/** 1-based position among the conversation's assistant messages. */
|
||||
turnIndex: number;
|
||||
results: CoachResultMeta[];
|
||||
}
|
||||
@@ -70,9 +70,15 @@ interface LoadedMessage {
|
||||
*/
|
||||
export function collectPriorResults(
|
||||
messages: ReadonlyArray<LoadedMessage>,
|
||||
/**
|
||||
* Assistant messages written before the first loaded one, so an index is
|
||||
* the message's position in the whole conversation, not in the window the
|
||||
* turn happened to load.
|
||||
*/
|
||||
earlierAssistantMessages = 0,
|
||||
): PriorResultTurn[] {
|
||||
const out: PriorResultTurn[] = [];
|
||||
let turnIndex = 0;
|
||||
let turnIndex = earlierAssistantMessages;
|
||||
for (const message of messages) {
|
||||
if (message.role !== "assistant") continue;
|
||||
turnIndex += 1;
|
||||
@@ -166,3 +172,33 @@ export function stripResultRefs(prose: string): {
|
||||
referenced,
|
||||
};
|
||||
}
|
||||
|
||||
// ── What a message keeps ──────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* The at-rest ceiling for one message's tables, as JSON before encryption.
|
||||
* Tables arrive trimmed to 400 rows; a message whose tables still exceed
|
||||
* this keeps the leading tables that fit.
|
||||
*/
|
||||
export const RESULTS_MAX_BYTES = 128 * 1024;
|
||||
|
||||
/**
|
||||
* The tables a message keeps: the leading ones whose JSON fits
|
||||
* `RESULTS_MAX_BYTES`, whole tables dropped from the end. Applied once,
|
||||
* before the turn streams them, so the tables the person sees live are the
|
||||
* tables a reload reads back, and the metadata never names a table the
|
||||
* ciphertext does not hold.
|
||||
*/
|
||||
export function fitResultsToStorage(
|
||||
results: readonly CoachResultTable[],
|
||||
): CoachResultTable[] {
|
||||
const kept = results.slice(0, MAX_RESULTS_PER_TURN);
|
||||
while (
|
||||
kept.length > 0 &&
|
||||
new TextEncoder().encode(JSON.stringify(kept)).byteLength >
|
||||
RESULTS_MAX_BYTES
|
||||
) {
|
||||
kept.pop();
|
||||
}
|
||||
return kept;
|
||||
}
|
||||
@@ -21,7 +21,7 @@ import { DEFAULT_COACH_PREFS } from "@/lib/validations/coach-prefs";
|
||||
|
||||
import { emitReply } from "../sse";
|
||||
import { assembleTurnDialog, buildTurnProvenance } from "../provenance";
|
||||
import { RESULTS_MAX_BYTES } from "@/lib/ai/coach/results/project";
|
||||
import { RESULTS_MAX_BYTES } from "@/lib/ai/coach/results/refs";
|
||||
import type { ModelOutcome } from "../model";
|
||||
import type { GuardedReply } from "../reply-guards";
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ import {
|
||||
import { numberFollowUps } from "@/lib/ai/coach/follow-ups/catalog";
|
||||
import { buildContinueFollowUp } from "@/lib/ai/coach/follow-ups/continue";
|
||||
import type { CoachPrefs } from "@/lib/validations/coach-prefs";
|
||||
import { fitResultsToStorage } from "@/lib/ai/coach/results/project";
|
||||
import { fitResultsToStorage } from "@/lib/ai/coach/results/refs";
|
||||
|
||||
import type { ModelOutcome } from "./model";
|
||||
import type { GuardedReply } from "./reply-guards";
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
* a useful summary for them.
|
||||
*/
|
||||
import { z } from "zod/v4";
|
||||
import { RESULTS_MAX_BYTES } from "@/lib/ai/coach/results/project";
|
||||
import { RESULTS_MAX_BYTES } from "@/lib/ai/coach/results/refs";
|
||||
import {
|
||||
AllergyCategory,
|
||||
AllergySeverity,
|
||||
|
||||
Reference in new issue
Block a user