fix(evals): retain completion stability window within original handoff deadline

This commit is contained in:
Dotta committed 2026-10-28 11:21:06 -05:00
1 parent c455a206a8
commit 104a23be5f
3 files changed
+17 -5

No files matched your search

@@ -1,4 +1,4 @@
import { chatCatalogNativePhaseDeadline, chatCatalogNativePhaseIsTimely } from "../chat-catalog-contract.js";
import { chatCatalogNativePhaseDeadline, chatCatalogNativePhaseIsTimely, chatCatalogOriginalPhaseDeadline } from "../chat-catalog-contract.js";
import { expect } from "@playwright/test";
import { writeFile } from "node:fs/promises";
import { pollUntil } from "../api.js";
@@ -94,7 +94,7 @@ export const runtimeHandoffCases = [runtimeDefinition("agent-chat.track-delegate
state.queryLinks = links; state.workerStatusAtQueryReply = phase.runs.find(run => run.id === state.workerRunId)!.status;
await writeFile((state.gate as { gate: string }).gate, `The real website brief: use a blue page with concise copy. Save a short task delivery document that includes exactly ${state.marker}, then complete this existing Website task.`, "utf8");
const delivered = await observeCompletionUpdate({ page: ctx.input.page, api: ctx.input.api, sourceId: String(state.sourceId), workerId: (state.task as Entity).id,
marker: String(state.marker), fixtureRequest: String(state.prompt), allRuns: () => ctx.allRuns(), evidence: ctx.input.evidence, capture: ctx.input.capture });
marker: String(state.marker), fixtureRequest: String(state.prompt), originalObservationDeadlineAt: chatCatalogOriginalPhaseDeadline(ctx), allRuns: () => ctx.allRuns(), evidence: ctx.input.evidence, capture: ctx.input.capture });
const delivery = completionDelivery(delivered); state.delivery = delivery; state.completionDelivered = !!delivery.response;
},
async observe(ctx, state) {
@@ -1,6 +1,7 @@
import {afterEach,expect,it,vi} from "vitest";
import type {ChatCatalogContext} from "./chat-catalog-contract.js";
import {chatCatalogNativePhaseDeadline,chatCatalogNativePhaseIsTimely,chatCatalogOriginalPhaseDeadline} from "./chat-catalog-contract.js";
import {completionObservationBounds} from "./completion-update-flow.js";
import {pollUntil} from "./api.js";
void pollUntil;
import {waitForReply} from "./chat-catalog-domains/identity-support.js";
@@ -11,3 +12,5 @@ it.each(ids)("%s consumes the original setup-inclusive clock with no new grace",
it("actual selected identity reply wait survives its artificial150s cap but keeps the original deadline",async()=>{vi.useFakeTimers();vi.setSystemTime(0);const c=ctx("budgets-costs.set-budget");const result=waitForReply(c,chatCatalogOriginalPhaseDeadline(c)).then(()=>"accepted",()=>"expired");await vi.advanceTimersByTimeAsync(0);await vi.advanceTimersByTimeAsync(181000);expect(await result).toBe("accepted");expect(c.observationDeadlineAt).toBe(270000);});
it("selected identity helper rejects a success read after the original clock",async()=>{vi.useFakeTimers();vi.setSystemTime(269999);const c=ctx("access.cli-authorization");c.refreshIssue=async()=>{vi.setSystemTime(270001);};c.allRuns=async()=>[{status:"succeeded"}] as never;const result=waitForReply(c,chatCatalogOriginalPhaseDeadline(c)).then(()=>"accepted",()=>"expired");await vi.advanceTimersByTimeAsync(1000);expect(await result).toBe("expired");});
it("keeps exhausted cases, provenance controls and other provider lanes outside this new clock scope",()=>{vi.useFakeTimers();vi.setSystemTime(200000);for(const id of ["chat-channels.provider-disabled","access.sign-in","agents.hire-denied","agent-chat.chat-disabled","tasks.create-backlog","activity.audit-change","cli-operations.choose-cli-context","companies.rename-company"]){const c=ctx(id);expect(chatCatalogOriginalPhaseDeadline(c)).toBeUndefined();expect(chatCatalogNativePhaseDeadline(c,350000)).toBe(350000);}const c=ctx(ids[0]!);Object.assign(c.input.execution,{id:`chat-features.runner-codex.local.${ids[0]}`});expect(chatCatalogOriginalPhaseDeadline(c)).toBeUndefined();});
it("completion observation retains its full120s stability minimum without restarting or extending the original clock",()=>{expect(completionObservationBounds(10000)).toEqual({observationWindowEndsAt:130000,deadlineAt:135000});expect(completionObservationBounds(10000,570000)).toEqual({observationWindowEndsAt:130000,deadlineAt:570000});expect(completionObservationBounds(200000,570000)).toEqual({observationWindowEndsAt:320000,deadlineAt:570000});expect(completionObservationBounds(560000,570000)).toEqual({observationWindowEndsAt:680000,deadlineAt:570000});for(const d of [NaN,Infinity,10000,9999])expect(()=>completionObservationBounds(10000,d)).toThrow(/exhausted or invalid/);});
+12 -3
View File
@@ -9,23 +9,30 @@ import { prepareChatBrief } from "./chat-stories.js";
import { completionDelivery, completionOutputUsesReleasedBrief, type CompletionObservation } from "./completion-updates.js";
type Row = Record<string, any>;
/** Keep the full stability window while selected catalog callers retain their original outer clock. */
export function completionObservationBounds(startedAtMs: number, originalDeadlineAt?: number) {
if (originalDeadlineAt !== undefined && (!Number.isFinite(originalDeadlineAt) || originalDeadlineAt <= startedAtMs))
throw new Error("Original completion observation budget exhausted or invalid");
return { observationWindowEndsAt: startedAtMs + 120_000, deadlineAt: originalDeadlineAt ?? startedAtMs + 125_000 };
}
export async function observeCompletionUpdate(input: {
page: Page; api: RunnerApi; sourceId: string; workerId: string; marker: string;
fixtureRequest: string;
originalObservationDeadlineAt?: number;
relatedWorkerIds?: string[];
allRuns(): Promise<Row[]>;
evidence(name: string, data: unknown): Promise<void>;
capture(id: string, label: string, file: string): Promise<void>;
}) {
const startedAt = new Date().toISOString();
const observationWindowEndsAt = Date.now() + 120_000;
const { observationWindowEndsAt, deadlineAt } = completionObservationBounds(Date.now(), input.originalObservationDeadlineAt);
let observation: CompletionObservation | undefined;
let failure: unknown;
try {
await pollUntil({
label: "unsolicited source-thread completion reply and result access",
// Keep observing even after an early reply, so a later correction is retained.
deadlineAt: observationWindowEndsAt + 5_000, intervalMs: 1000,
deadlineAt, intervalMs: 1000,
load: async () => {
const worker = await input.api.get<Row>(`/api/issues/${input.workerId}`);
const documents = await input.api.get<Row[]>(`/api/issues/${input.workerId}/documents`);
@@ -49,7 +56,9 @@ export async function observeCompletionUpdate(input: {
}
return completionDelivery(observation);
},
accept: result => Date.now() >= observationWindowEndsAt && result.checks.every(c => c.passed),
accept: result => Date.now() >= observationWindowEndsAt
&& (input.originalObservationDeadlineAt === undefined || Date.now() < input.originalObservationDeadlineAt)
&& result.checks.every(c => c.passed),
reject: () => observation!.runs.length > 12 ? "completion probe exceeded 12 runs" : undefined,
timeoutDetail: result => result?.checks.filter(c => !c.passed).map(c => c.id).join(", "),
});