refactor(tui): centralize gateway chat history limits (#117773)

Co-authored-by: Peter Steinberger <steipete@macos.shared>
This commit is contained in:
Peter Steinberger
2026-08-01 20:34:49 -07:00
committed by GitHub
parent 2bbc2f5bc4
commit d669e9eddd
7 changed files with 56 additions and 4 deletions
@@ -0,0 +1,2 @@
/** Largest history page accepted by the Gateway wire contract. */
export const CHAT_HISTORY_MAX_ENTRIES = 1000;
@@ -1,7 +1,12 @@
// Gateway Protocol tests cover typed chat stream events.
import { Value } from "typebox/value";
import { describe, expect, it } from "vitest";
import { ChatEventSchema, ChatSendParamsSchema, ChatStatusEventSchema } from "./logs-chat.js";
import {
ChatEventSchema,
ChatHistoryParamsSchema,
ChatSendParamsSchema,
ChatStatusEventSchema,
} from "./logs-chat.js";
const statusEvent = {
runId: "run-1",
@@ -11,6 +16,15 @@ const statusEvent = {
phase: "preparing_context",
} as const;
describe("ChatHistoryParamsSchema", () => {
it("accepts the history boundary and rejects larger requests", () => {
const request = { sessionKey: "agent:main:main" };
expect(Value.Check(ChatHistoryParamsSchema, { ...request, limit: 1000 })).toBe(true);
expect(Value.Check(ChatHistoryParamsSchema, { ...request, limit: 1001 })).toBe(false);
});
});
describe("ChatStatusEventSchema", () => {
it("accepts closed startup phases through the chat event union", () => {
expect(Value.Check(ChatStatusEventSchema, statusEvent)).toBe(true);
@@ -1,6 +1,7 @@
// Gateway Protocol schema module defines protocol validation shapes.
import type { Static } from "typebox";
import { Type } from "typebox";
import { CHAT_HISTORY_MAX_ENTRIES } from "./chat-history-constants.js";
import { closedObject } from "./closed-object.js";
import { ChatSendSessionKeyString, InputProvenanceSchema, NonEmptyString } from "./primitives.js";
@@ -25,7 +26,7 @@ export const LogsTailResultSchema = closedObject({
export const ChatHistoryParamsSchema = closedObject({
sessionKey: NonEmptyString,
agentId: Type.Optional(NonEmptyString),
limit: Type.Optional(Type.Integer({ minimum: 1, maximum: 1000 })),
limit: Type.Optional(Type.Integer({ minimum: 1, maximum: CHAT_HISTORY_MAX_ENTRIES })),
offset: Type.Optional(Type.Integer({ minimum: 0 })),
messageId: Type.Optional(NonEmptyString),
sessionId: Type.Optional(NonEmptyString),
+26
View File
@@ -58,9 +58,11 @@ describe("cli program (smoke)", () => {
await runProgram(["tui", "--timeout-ms", "45000"]);
const options = firstMockArg(runTui) as {
timeoutMs?: number;
historyLimit?: number;
forceProcessExitOnReturn?: boolean;
};
expect(options?.timeoutMs).toBe(45000);
expect(options?.historyLimit).toBe(200);
expect(options?.forceProcessExitOnReturn).toBe(true);
});
@@ -92,6 +94,30 @@ describe("cli program (smoke)", () => {
expect(runTui).not.toHaveBeenCalled();
});
it("accepts the maximum Gateway tui history limit", async () => {
await runProgram(["tui", "--history-limit", "1000"]);
expect(firstMockArg(runTui)).toMatchObject({ local: false, historyLimit: 1000 });
});
it.each([
{ entryPoint: "tui --local", args: ["tui", "--local"] },
{ entryPoint: "terminal", args: ["terminal"] },
{ entryPoint: "chat", args: ["chat"] },
])("preserves oversized history limits for local $entryPoint", async ({ args }) => {
await runProgram([...args, "--history-limit", "1001"]);
expect(firstMockArg(runTui)).toMatchObject({ local: true, historyLimit: 1001 });
expect(runtime.error).not.toHaveBeenCalled();
});
it("rejects tui history limits above the Gateway maximum", async () => {
await expect(runProgram(["tui", "--history-limit", "1001"])).rejects.toThrow("exit");
expect(runtime.error).toHaveBeenCalledWith("Error: --history-limit must be at most 1000.");
expect(runTui).not.toHaveBeenCalled();
});
it("runs setup wizard when wizard flags are present", async () => {
await runProgram(["setup", "--remote-url", "ws://example"]);
+4
View File
@@ -1,5 +1,6 @@
// Registers the terminal UI subcommand and normalizes its local-vs-gateway options.
import type { Command } from "commander";
import { CHAT_HISTORY_MAX_ENTRIES } from "../../packages/gateway-protocol/src/schema/chat-history-constants.js";
import { formatDocsLink } from "../../packages/terminal-core/src/links.js";
import { theme } from "../../packages/terminal-core/src/theme.js";
import { parseStrictPositiveInteger } from "../infra/parse-finite-number.js";
@@ -51,6 +52,9 @@ export function registerTuiCli(program: Command) {
if (historyLimit === undefined) {
throw new Error("--history-limit must be a positive integer.");
}
if (!isLocal && historyLimit > CHAT_HISTORY_MAX_ENTRIES) {
throw new Error(`--history-limit must be at most ${CHAT_HISTORY_MAX_ENTRIES}.`);
}
const { runTui } = await import("../tui/tui.js");
await runTui({
local: isLocal,
@@ -9,6 +9,7 @@ import {
validateChatHistoryParams,
validateChatMetadataParams,
} from "../../../packages/gateway-protocol/src/index.js";
import { CHAT_HISTORY_MAX_ENTRIES } from "../../../packages/gateway-protocol/src/schema/chat-history-constants.js";
import {
listAgentIds,
resolveDefaultAgentId,
@@ -385,7 +386,7 @@ async function handleChatHistoryRequest({
requestedSessionId && requestedSessionId !== entry?.sessionId ? undefined : entry;
const resolvedSessionModel = resolveSessionModelRef(cfg, entry, sessionAgentId);
const requested = typeof limit === "number" ? limit : 200;
const max = Math.min(1000, requested);
const max = Math.min(CHAT_HISTORY_MAX_ENTRIES, requested);
const maxHistoryBytes = getMaxChatHistoryMessagesBytes();
const effectiveMaxChars = resolveEffectiveChatHistoryMaxChars(cfg, maxChars);
let historyPage: Awaited<ReturnType<typeof readChatHistoryPage>>;
+5 -1
View File
@@ -1,6 +1,7 @@
// Implements the embedded backend used by local TUI sessions.
import { randomUUID } from "node:crypto";
import type { SessionsPatchResult } from "../../packages/gateway-protocol/src/index.js";
import { CHAT_HISTORY_MAX_ENTRIES } from "../../packages/gateway-protocol/src/schema/chat-history-constants.js";
import { agentCommandFromIngress } from "../agents/agent-command.js";
import { isAgentLifecycleYieldedWaiting } from "../agents/agent-lifecycle-parent-state.js";
import {
@@ -621,7 +622,10 @@ export class EmbeddedTuiBackend implements TuiBackend {
this.runtimePluginRegistry =
runtimePluginsPrewarm.status === "warmed" ? runtimePluginsPrewarm.registry : undefined;
const resolvedSessionModel = resolveSessionModelRef(cfg, entry, sessionAgentId);
const max = Math.min(1000, typeof opts.limit === "number" ? opts.limit : 200);
const max = Math.min(
CHAT_HISTORY_MAX_ENTRIES,
typeof opts.limit === "number" ? opts.limit : 200,
);
const maxHistoryBytes = getMaxChatHistoryMessagesBytes();
const effectiveMaxChars = resolveEffectiveChatHistoryMaxChars(cfg);
const historyPage = await readChatHistoryPage({