fix(telegram): preserve visible draft recovery (#120626)

* fix(telegram): preserve visible draft recovery

* fix(telegram): await visible draft send

* test(telegram): use const in draft recovery test

* test(telegram): add live partial failure proof

* test(telegram): register live recovery coverage

* ci(qa): support mock Telegram proof scenarios

* test(telegram): isolate live recovery fallback

* test(telegram): assert live recovery behavior

* fix(agents): join partial reply delivery

* test(telegram): keep settlement proof at core boundary
This commit is contained in:
Peter Steinberger
2026-08-09 02:54:38 -07:00
committed by GitHub
parent 917fd92686
commit 0df1a89e3a
16 changed files with 551 additions and 36 deletions
@@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest";
import type { StreamEvent } from "./mock-openai-contracts.js";
import {
buildAssistantEvents,
buildPartialFailureEvents,
buildAssistantThenToolCallEvents,
buildFailedResponseEvents,
buildReasoningAndAssistantEvents,
@@ -31,13 +32,35 @@ function readOutputItemSlots(events: StreamEvent[]) {
describe("mock OpenAI Responses output item slots", () => {
it("emits the provider no-details failure used by repeated-request recovery QA", () => {
expect(buildFailedResponseEvents()).toEqual([
const events = buildFailedResponseEvents();
expect(events).toEqual([
expect.objectContaining({ type: "response.created" }),
expect.objectContaining({
type: "response.failed",
response: expect.not.objectContaining({ error: expect.anything() }),
}),
]);
expect(events.some((event) => event.type === "response.output_text.delta")).toBe(false);
});
it("emits an unfinished assistant delta before the failed response", () => {
const marker = "TELEGRAM-VISIBLE-PARTIAL-BEFORE-FAILURE";
const events = buildPartialFailureEvents(marker);
expect(events.map((event) => event.type)).toEqual([
"response.created",
"response.output_item.added",
"response.output_text.delta",
"response.failed",
]);
expect(events[1]).toMatchObject({
item: { type: "message", role: "assistant", status: "in_progress" },
});
expect(events[2]).toMatchObject({
type: "response.output_text.delta",
delta: marker,
});
expect(events.some((event) => event.type === "response.output_item.done")).toBe(false);
});
it("indexes preview deltas and the final answer on the same assistant slot", () => {
@@ -21,6 +21,40 @@ export function buildFailedResponseEvents(): StreamEvent[] {
];
}
export function buildPartialFailureEvents(partialText: string): StreamEvent[] {
const responseId = "resp_qa_partial_failed_1";
const itemId = "msg_qa_partial_failed_1";
return [
{ type: "response.created", response: { id: responseId } },
{
type: "response.output_item.added",
output_index: 0,
item: {
type: "message",
id: itemId,
role: "assistant",
phase: "final_answer",
content: [],
status: "in_progress",
},
},
{
type: "response.output_text.delta",
item_id: itemId,
output_index: 0,
content_index: 0,
delta: partialText,
},
{
type: "response.failed",
response: {
id: responseId,
status: "failed",
},
},
];
}
export function buildToolCallEvents(prompt: string): StreamEvent[] {
const targetPath = readTargetFromPrompt(prompt);
return buildToolCallEventsWithArgs("read", { path: targetPath });
@@ -219,6 +219,13 @@ function expectOpenAiStreamingResponsesText(server: MockServer, body: Record<str
return expectStreamingResponsesText(server, { model: "gpt-5.6-luna", ...body });
}
function parseStreamingResponseEvents(body: string): StreamEvent[] {
return body
.split("\n")
.filter((line) => line.startsWith("data: {") && line.endsWith("}"))
.map((line) => JSON.parse(line.slice("data: ".length)) as StreamEvent);
}
const requireRecord = createRequireRecord("record", "expected-label-capitalized");
function requireArray(value: unknown, label: string): unknown[] {
@@ -913,6 +920,36 @@ describe("qa mock openai server", () => {
expect(blockContinuationBody).not.toContain('"item_id":"msg_mock_block_1"');
});
it("serves Telegram visible and unsent failure directives", async () => {
const server = await startMockServer();
const visibleEvents = parseStreamingResponseEvents(
await expectOpenAiStreamingResponsesText(server, {
input: [makeUserInput("Telegram visible partial failure QA check")],
}),
);
const unsentEvents = parseStreamingResponseEvents(
await expectOpenAiStreamingResponsesText(server, {
input: [makeUserInput("Telegram unsent failure QA check")],
}),
);
expect(visibleEvents.map((event) => event.type)).toEqual([
"response.created",
"response.output_item.added",
"response.output_text.delta",
"response.failed",
]);
expect(visibleEvents[2]).toMatchObject({
type: "response.output_text.delta",
delta: "TELEGRAM-VISIBLE-PARTIAL-BEFORE-FAILURE",
});
expect(unsentEvents.map((event) => event.type)).toEqual([
"response.created",
"response.failed",
]);
expect(unsentEvents.some((event) => event.type === "response.output_text.delta")).toBe(false);
});
it("plans deterministic tool-progress reads from prompt paths", async () => {
const server = await startMockServer();
@@ -144,6 +144,7 @@ import {
buildQaLongFinalText,
buildAssistantThenToolCallEvents,
buildAssistantEvents,
buildPartialFailureEvents,
buildReasoningOnlyEvents,
buildReasoningAndAssistantEvents,
buildFailedResponseEvents,
@@ -288,6 +289,9 @@ const QA_STREAMING_TOOL_PROGRESS_CONTINUATION_RE =
/^Continue with (?:the current Matrix QA scenario|the QA scenario plan and report worked, failed, and blocked items)\.$/i;
const QA_CODE_MODE_TARGET_MARKER = "qa-code-mode-target:";
const QA_FAILED_TOOL_TERMINAL_RECOVERY_PROMPT_RE = /failed tool terminal recovery qa check/i;
const QA_TELEGRAM_VISIBLE_PARTIAL_FAILURE_PROMPT_RE = /telegram visible partial failure qa check/i;
const QA_TELEGRAM_UNSENT_FAILURE_PROMPT_RE = /telegram unsent failure qa check/i;
const QA_TELEGRAM_VISIBLE_PARTIAL_FAILURE_MARKER = "TELEGRAM-VISIBLE-PARTIAL-BEFORE-FAILURE";
// Keep each real provider request active long enough for retries to span the
// unchanged five-minute recovery bound while remaining below first-byte timeout.
const QA_REPEATED_REQUEST_RESPONSE_PAUSE_MS = 110_000;
@@ -868,6 +872,12 @@ async function buildResponsesPayload(
if (QA_REPEATED_REQUEST_QUEUED_REPLY_PROMPT_RE.test(prompt)) {
return buildAssistantEvents(QA_REPEATED_REQUEST_QUEUED_REPLY_MARKER);
}
if (QA_TELEGRAM_VISIBLE_PARTIAL_FAILURE_PROMPT_RE.test(prompt)) {
return buildPartialFailureEvents(QA_TELEGRAM_VISIBLE_PARTIAL_FAILURE_MARKER);
}
if (QA_TELEGRAM_UNSENT_FAILURE_PROMPT_RE.test(prompt)) {
return buildFailedResponseEvents();
}
if (QA_REPEATED_REQUEST_RECOVERY_PROMPT_RE.test(allInputText)) {
return buildFailedResponseEvents();
}