Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 14 additions & 3 deletions src/core/stream.ts
Original file line number Diff line number Diff line change
Expand Up @@ -368,13 +368,24 @@ function numOrUndef(v: unknown): number | undefined {
function strOrUndef(v: unknown): string | undefined {
return v == null ? undefined : String(v);
}

const LEGACY_PUBLIC_STREAM_ERRORS = new Set([
"The media studio agent hit an error.",
"generation registry did not reach a terminal state",
]);

function streamErrorMessage(obj: Record<string, unknown>): string {
// Some server paths send `error` instead of `msg`. Keep the internal `reason`
// field out of user-facing frames, even when all message fields are blank.
for (const key of ["msg", "message", "error"]) {
// Only msg/message are a public message contract. Legacy media frames use
// fixed public `error` strings, but other `error` values may be provider
// exception text. Never map the internal `reason` field into a frame.
for (const key of ["msg", "message"]) {
const value = obj[key];
if (typeof value === "string" && value.trim()) return value;
}
const legacyError = obj["error"];
if (typeof legacyError === "string" && LEGACY_PUBLIC_STREAM_ERRORS.has(legacyError)) {
return legacyError;
}
return "";
}
function parseStrArray(v: unknown): string[] | undefined {
Expand Down
19 changes: 14 additions & 5 deletions test/stream.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,16 +31,25 @@ test("normalizeFrame error uses contract keys msg/error_code/ref_id", () => {
assert.deepEqual(f, { type: "error", msg: "boom", errorCode: "E42", refId: "r1" });
});

test("normalizeFrame takes the first nonblank error text and never exposes reason", () => {
test("normalizeFrame accepts canonical and fixed legacy public error text only", () => {
const base = { type: "error", reason: "internal detail" };
assert.deepEqual(normalizeFrame({ ...base, msg: "primary", error: "fallback" }), {
assert.deepEqual(normalizeFrame({ ...base, msg: "primary", error: "private provider exception" }), {
type: "error", msg: "primary", errorCode: undefined, refId: undefined,
});
assert.deepEqual(normalizeFrame({ ...base, msg: " ", message: "alternate", error: "fallback" }), {
assert.deepEqual(normalizeFrame({ ...base, msg: " ", message: "alternate", error: "private provider exception" }), {
type: "error", msg: "alternate", errorCode: undefined, refId: undefined,
});
assert.deepEqual(normalizeFrame({ ...base, msg: "", error: "server failure" }), {
type: "error", msg: "server failure", errorCode: undefined, refId: undefined,
assert.deepEqual(normalizeFrame({ ...base, msg: "", error: "The media studio agent hit an error." }), {
type: "error", msg: "The media studio agent hit an error.", errorCode: undefined, refId: undefined,
});
assert.deepEqual(normalizeFrame({ ...base, error: "generation registry did not reach a terminal state" }), {
type: "error", msg: "generation registry did not reach a terminal state", errorCode: undefined, refId: undefined,
});
assert.deepEqual(normalizeFrame({ ...base, error: "private provider exception: token=SECRET" }), {
type: "error", msg: "", errorCode: undefined, refId: undefined,
});
assert.deepEqual(normalizeFrame({ ...base, error: "The media studio agent hit an error. " }), {
type: "error", msg: "", errorCode: undefined, refId: undefined,
});
assert.deepEqual(normalizeFrame({ ...base, error: { secret: true } }), {
type: "error", msg: "", errorCode: undefined, refId: undefined,
Expand Down
26 changes: 22 additions & 4 deletions test/turn_lifecycle.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -224,29 +224,47 @@ test("streamed 402 after a partial delta preserves text and adds actionable sani
}
});

test("one-shot error-key frame is a visible failed turn without exposing reason or trailing done", async () => {
test("one-shot fixed legacy media error is visible without exposing reason or trailing done", async () => {
resetRegistry();
const realFetch = globalThis.fetch;
let calls = 0;
globalThis.fetch = (async () => {
calls += 1;
return sseResponse([
{ type: "error", error: "The worker hit an error.\u001b]52;c;payload\u0007", reason: "internal detail" },
{ type: "error", error: "The media studio agent hit an error.", reason: "internal detail" },
{ type: "done", uvt: 0, cents: 0 },
]);
}) as typeof globalThis.fetch;
try {
const result = await captureWrites(() => cmdChat(cloudContext(), "ship this"));
assert.equal(result.value, 1);
assert.equal(calls, 1, "a terminal error must not retry or switch transports");
assert.match(result.stderr, /The worker hit an error\./);
assert.doesNotMatch(result.stderr, /internal detail|\u001b\]52|0 UVT/i);
assert.match(result.stderr, /The media studio agent hit an error\./);
assert.doesNotMatch(result.stderr, /internal detail|0 UVT/i);
assert.equal(result.stdout, "", "an error must not fabricate model output");
} finally {
globalThis.fetch = realFetch;
}
});

test("one-shot arbitrary error-key text is hidden while the turn still fails", async () => {
resetRegistry();
const realFetch = globalThis.fetch;
globalThis.fetch = (async () => sseResponse([
{ type: "error", error: "provider token=SECRET\u001b]52;c;payload\u0007", reason: "internal detail" },
{ type: "done", uvt: 0, cents: 0 },
])) as typeof globalThis.fetch;
try {
const result = await captureWrites(() => cmdChat(cloudContext(), "ship this"));
assert.equal(result.value, 1);
assert.match(result.stderr, /turn failed/i);
assert.doesNotMatch(result.stderr, /SECRET|internal detail|\u001b\]52|0 UVT/i);
assert.equal(result.stdout, "");
} finally {
globalThis.fetch = realFetch;
}
});

test("one-shot blank error frame remains a failed turn with a fallback message", async () => {
resetRegistry();
const realFetch = globalThis.fetch;
Expand Down
Loading