mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-11 22:02:16 +00:00
fix(insights): give the briefing warm every call of its generation
The out-of-band warm the Insights page relies on bounded one comprehensive generation at the account's response timeout plus 30 s. A generation is up to five model calls in a row: the generation, the JSON retry, the grounding retry, the outbound-screen repair, and the one repeat a thinking model gets when it answered with reasoning alone. Each call honoured the setting, but a local model needing 300 s per answer under a 600 s setting met the 630 s bound in the middle of its first corrective retry, and the briefing was dropped. The bound is now the per-call timeout times COMPREHENSIVE_MODEL_CALLS plus the headroom, for the nightly pass and the forced warm alike. A test drives the real warm, generator, chain runner, Local client, safeFetch and pinned dispatcher against a loopback server that answers after 300 s; under the old bound it reports a timeout on the third answer. The response-timeout guard now names every outer bound around model work and holds both briefing generators' stages to the call counts the outer waits multiply by. Refs #1274
This commit is contained in:
1 parent
810a949d2b
commit
73e98c6ef5
8 files changed
+562
-96
No files matched your search
@@ -19,7 +19,14 @@
|
||||
* 3. a client is constructed outside `provider.ts`, where no resolver binds
|
||||
* it (the offline evaluation harness is the one named exception);
|
||||
* 4. a surface opts out with `timeoutPolicy: "surface-ceiling"` without
|
||||
* being named below with its reason.
|
||||
* being named below with its reason;
|
||||
* 5. an outer bound waits on several model calls in a row but is sized as
|
||||
* one (#1274: the briefing warm gave a five-call generation one call
|
||||
* plus 30 s, so a local model answering in 300 s under a 600 s setting
|
||||
* lost its briefing on the first corrective retry). Every `withTimeout`
|
||||
* around model work is named below with what it must allow, and the
|
||||
* briefing generators' stages are held to the call counts the outer
|
||||
* waits multiply by.
|
||||
*
|
||||
* Its limit: it reads source text. A provider object built by hand in
|
||||
* production code that dials the network itself would slip all four matchers,
|
||||
@@ -29,9 +36,10 @@
|
||||
*
|
||||
* Mutation check: put `params.timeoutMs ?? 60_000` back into any client, drop
|
||||
* `bindResponseTimeout` from `resolveProviderChain`, construct a
|
||||
* `LocalOpenAICompatibleClient` in a route, or add
|
||||
* `timeoutPolicy: "surface-ceiling"` to the document summary: each one goes
|
||||
* red here.
|
||||
* `LocalOpenAICompatibleClient` in a route, add
|
||||
* `timeoutPolicy: "surface-ceiling"` to the document summary, wrap a model
|
||||
* call in a new `withTimeout`, or add a sixth `runBriefingCompletion` stage to
|
||||
* the comprehensive generator: each one goes red here.
|
||||
*/
|
||||
import { readFileSync, readdirSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
@@ -187,3 +195,92 @@ describe("surface ceilings are frozen", () => {
|
||||
expect(sites.sort()).toEqual(Object.keys(SURFACE_CEILING_SITES).sort());
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Every `withTimeout` around model work. It cannot stop the calls it waits
|
||||
* for, so it is a backstop against a wedged run and has to sit above the
|
||||
* longest run those calls allow: the per-call timeout times the calls made
|
||||
* one after another. `bound` is what each call site's budget must read.
|
||||
*/
|
||||
const OUTER_BOUND_SITES: Record<
|
||||
string,
|
||||
{ sites: number; bound: RegExp; sizedBy?: RegExp; why: string }
|
||||
> = {
|
||||
"lib/jobs/insight-pregenerate.ts": {
|
||||
sites: 2,
|
||||
bound: /\bwarmBudgetMs\b/,
|
||||
sizedBy: /\bwarmBudgetMs\s*=\s*comprehensiveWarmBudgetMs\s*\(/g,
|
||||
why: "The comprehensive warm, nightly and forced: the per-call timeout times COMPREHENSIVE_MODEL_CALLS, plus headroom.",
|
||||
},
|
||||
"lib/insights/status-provider.ts": {
|
||||
sites: 1,
|
||||
bound: /\beffectiveTimeoutMs\b/,
|
||||
why: "One short status note per chain run, bounded at the per-call timeout on purpose: the card has deterministic fallback copy and the next read retries.",
|
||||
},
|
||||
};
|
||||
|
||||
describe("outer bounds around model work allow every call they wait on", () => {
|
||||
const files = walkSourceFiles(SRC, { floor: 3000 })
|
||||
.filter((p) => !p.startsWith("generated/"))
|
||||
.filter((p) => !p.includes("__tests__"))
|
||||
.filter((p) => !p.endsWith(".test.ts") && !p.endsWith(".test.tsx"))
|
||||
.filter((p) => p !== "lib/insights/with-timeout.ts");
|
||||
const callsIn = (rel: string): string[] => {
|
||||
const source = code(rel);
|
||||
return [...source.matchAll(/\bwithTimeout\s*\(/g)].map((m) =>
|
||||
source.slice(m.index, m.index + 900),
|
||||
);
|
||||
};
|
||||
const sites = files.filter((rel) => callsIn(rel).length > 0);
|
||||
|
||||
it("names every file that bounds model work from outside", () => {
|
||||
expect(sites.length).toBeGreaterThan(0);
|
||||
expect(sites.sort()).toEqual(Object.keys(OUTER_BOUND_SITES).sort());
|
||||
});
|
||||
|
||||
it.each(Object.entries(OUTER_BOUND_SITES))(
|
||||
"%s sizes each bound from the setting",
|
||||
(rel, { sites: count, bound, sizedBy }) => {
|
||||
const calls = callsIn(rel);
|
||||
expect(calls).toHaveLength(count);
|
||||
for (const call of calls) expect(call).toMatch(bound);
|
||||
if (sizedBy) {
|
||||
expect(code(rel).match(sizedBy) ?? []).toHaveLength(count);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
/** The `stage` of every briefing completion a file starts. */
|
||||
function stagesIn(rel: string): string[] {
|
||||
return [...code(rel).matchAll(/\bstage:\s*"([a-z-]+)",/g)].map((m) => m[1]);
|
||||
}
|
||||
|
||||
it("the comprehensive generator makes no more calls in a row than the warm waits for", () => {
|
||||
// `reroll` replaces `generate` on an unchanged snapshot; the other four
|
||||
// run one after another, and the Local and gateway clients may ask the
|
||||
// first again once. A new stage means a longer generation: recount
|
||||
// COMPREHENSIVE_MODEL_CALLS before adding it here.
|
||||
expect(stagesIn("lib/insights/comprehensive-generate.ts").sort()).toEqual([
|
||||
"generate",
|
||||
"grounding-retry",
|
||||
"json-retry",
|
||||
"reroll",
|
||||
"screen-retry",
|
||||
]);
|
||||
expect(code("lib/ai/ai-budgets.ts")).toMatch(
|
||||
/\bCOMPREHENSIVE_MODEL_CALLS\s*=\s*5\b/,
|
||||
);
|
||||
});
|
||||
|
||||
it("the on-demand regenerate makes no more calls in a row than the browser waits for", () => {
|
||||
// The generation and its grounding retry, and the one repeat inside the
|
||||
// client: FORCE_ADVISOR_MODEL_CALLS in the advisor hook.
|
||||
expect([
|
||||
...stagesIn("lib/insights/briefing-request/completion.ts"),
|
||||
...stagesIn("lib/insights/briefing-request/grounding.ts"),
|
||||
]).toEqual(["generate", "grounding-retry"]);
|
||||
expect(code("components/insights/use-insights-advisor.ts")).toMatch(
|
||||
/\bFORCE_ADVISOR_MODEL_CALLS\s*=\s*3\b/,
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$comment": "Production files over 800 lines and their length. Maintained by src/__tests__/source-size-ratchet.test.ts.",
|
||||
"count": 89,
|
||||
"total": 99593,
|
||||
"total": 99565,
|
||||
"entries": {
|
||||
"app/medications/page-client.tsx": 839,
|
||||
"components/charts/mood-chart.tsx": 1166,
|
||||
@@ -68,7 +68,7 @@
|
||||
"lib/insights/status-cache.ts": 812,
|
||||
"lib/integrations/status.ts": 1445,
|
||||
"lib/jobs/coach-nudge.ts": 1084,
|
||||
"lib/jobs/insight-pregenerate.ts": 1259,
|
||||
"lib/jobs/insight-pregenerate.ts": 1231,
|
||||
"lib/jobs/medication-intake-import.ts": 819,
|
||||
"lib/jobs/offhost-backup.ts": 1147,
|
||||
"lib/jobs/reaction-line.ts": 824,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$comment": "Comment lines that open with a release version, per file under src/. Maintained by src/__tests__/version-stamp-ratchet.test.ts.",
|
||||
"count": 2897,
|
||||
"total": 9285,
|
||||
"total": 9283,
|
||||
"entries": {
|
||||
"__tests__/account-payload-consumer-guard.test.ts": 2,
|
||||
"__tests__/acting-account-boundary-guard.test.ts": 7,
|
||||
@@ -2324,7 +2324,7 @@
|
||||
"lib/jobs/google-health-oauth-state-cleanup.ts": 1,
|
||||
"lib/jobs/google-health-sleep-repair.ts": 1,
|
||||
"lib/jobs/insight-pregenerate-shared.ts": 1,
|
||||
"lib/jobs/insight-pregenerate.ts": 39,
|
||||
"lib/jobs/insight-pregenerate.ts": 37,
|
||||
"lib/jobs/insight-status-generate-shared.ts": 3,
|
||||
"lib/jobs/insight-status-generate.ts": 3,
|
||||
"lib/jobs/intake-auto-skip.ts": 8,
|
||||
|
||||
Reference in new issue
Block a user