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
6 changes: 4 additions & 2 deletions src/gateway/gateway.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import type { PressureLevel } from "../memory/types";
// progress messages now flow through the transport via this.transport.progress().
import { isCommand as _isCmd, isCommandWithArgs as _isCmdArgs, resolveAgentThreadId as _resolveThread, getSystemResources as _getSysRes } from "./helpers";
import { saveAttachments, type AttachmentResult } from "./attachments";
import { handleStreaming as _handleStream } from "./streaming";
import { handleStreaming as _handleStream, StreamModelOverflowError } from "./streaming";
import { handleNew, handleRestart, handleUpdate, handleCompact, handleStatus, handleStop, handleVerbose, handleDoctor, handleCrons, type CommandContext } from "./commands";
import { handleModel, handleModelAction, MODEL_ACTION_ID } from "./model-command";
import { handleLater } from "./later-command";
Expand Down Expand Up @@ -515,9 +515,11 @@ export class Gateway {
console.error(`[roundhouse] memory finalize error:`, (err as Error).message);
}
} catch (err) {
// Extract hadVisibleText from StreamModelOverflowError if present
const errorHadVisibleText = err instanceof StreamModelOverflowError ? err.hadVisibleText : streamHadVisibleText;
await recoverFromAgentTurnOverflow(thread, agentThreadId, agent, err, {
turnSource,
hadVisibleText: streamHadVisibleText,
hadVisibleText: errorHadVisibleText,
});
} finally {
if (stopTyping) stopTyping();
Expand Down
7 changes: 5 additions & 2 deletions src/gateway/streaming.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,11 @@ import { isContextOverflowError } from "../agents/shared/error-classifiers";
*/
export class StreamModelOverflowError extends Error {
override readonly name = "StreamModelOverflowError";
constructor(message: string) {
readonly hadVisibleText: boolean;

constructor(message: string, hadVisibleText: boolean = false) {
super(message);
this.hadVisibleText = hadVisibleText;
}
}

Expand Down Expand Up @@ -213,7 +216,7 @@ export async function handleStreaming(
// Non-overflow errors keep today's inline post + continue-loop.
if (isContextOverflowError({ message: event.message })) {
console.warn(`[roundhouse] streamed model_error: context overflow — escalating to gateway recovery`);
throw new StreamModelOverflowError(event.message);
throw new StreamModelOverflowError(event.message, hasVisibleText);
}
modelErrorPosted = true;
const safeMsg = event.message.split("\n")[0].slice(0, 400);
Expand Down
23 changes: 9 additions & 14 deletions test/streaming-overflow.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -244,25 +244,20 @@ describe("streaming-overflow → gateway recovery (F1 end-to-end)", () => {
let caught: unknown;
let hadVisibleText = false;
try {
// We can't read the StreamResult after a throw; the gateway tracks
// hadVisibleText separately via a hoisted local that's updated only
// on the success path. Simulate that by inspecting whether the stream
// produced visible text before the throw.
const r = await handleStreaming(stream, ctxFor(thread));
hadVisibleText = r.hadVisibleText;
} catch (e) { caught = e; }

// In the gateway, `streamHadVisibleText` is set only on the success
// assignment — but the production gateway actually tracks it across the
// throw via the hoisted-local pattern in handleAgentTurn. To validate
// the recovery copy here we set hadVisibleText=true to mirror the
// gateway's behavior after a partial-text emit (see gateway.ts comment).
// The pure-streaming visible-text path is covered by other tests.
void hadVisibleText;
} catch (e) {
caught = e;
// StreamModelOverflowError now carries hadVisibleText from the stream.
// In production gateway.ts, this is extracted and passed to recovery.
if (e instanceof StreamModelOverflowError) {
hadVisibleText = e.hadVisibleText;
}
}

const result = await recoverFromAgentTurnOverflow(thread, tid, agent, caught, {
turnSource: "user",
hadVisibleText: true,
hadVisibleText,
});

expect(result.outcome?.kind).toBe("recovered");
Expand Down
Loading