diff --git a/packages/core/src/session/session.ts b/packages/core/src/session/session.ts index 55a90efd2c82..88f38ca1be30 100644 --- a/packages/core/src/session/session.ts +++ b/packages/core/src/session/session.ts @@ -9,6 +9,7 @@ import { Location } from "../location.js" import { PluginSupervisor } from "../plugin/supervisor-service.js" import { Shell } from "../shell.js" import { ShellResult } from "../shell/result.js" +import { ToolOutput } from "../tool-output.js" import { Skill } from "../skill.js" import { BusyError, @@ -446,4 +447,4 @@ function isUnfinishedTool(content: SessionMessage.AssistantContent) { } // Mirrors the shell tool's in-memory preview safety limit. -const SHELL_MAX_CAPTURE_BYTES = 1024 * 1024 +const SHELL_MAX_CAPTURE_BYTES = ToolOutput.MAX_BYTES diff --git a/packages/core/test/session-create.test.ts b/packages/core/test/session-create.test.ts index d4e319293f55..76e9f5799c88 100644 --- a/packages/core/test/session-create.test.ts +++ b/packages/core/test/session-create.test.ts @@ -23,6 +23,7 @@ import { Provider } from "@opencode-ai/core/provider" import { AbsolutePath, RelativePath } from "@opencode-ai/core/schema" import { Session } from "@opencode-ai/core/session" import { SessionMessage } from "@opencode-ai/core/session/message" +import { toLLMMessages } from "@opencode-ai/core/session/runner/to-llm-message" import { SessionProjector } from "@opencode-ai/core/session/projector" import { SessionExecution } from "@opencode-ai/core/session/execution" import { SessionInbox } from "@opencode-ai/core/session/inbox" @@ -31,6 +32,7 @@ import { SessionEvent } from "@opencode-ai/core/session/event" import { SessionTable } from "@opencode-ai/core/session/sql" import { SessionStore } from "@opencode-ai/core/session/store" import { SessionTransfer } from "@opencode-ai/core/session/transfer" +import { ToolOutput } from "@opencode-ai/core/tool-output" import { Workspace } from "@opencode-ai/core/workspace" import { Expected } from "./lib/session-message" import { testEffect } from "./lib/effect" @@ -1071,6 +1073,47 @@ describe("Session.create", () => { ), ) + it.live("bounds large session shell output before model replay", () => + withTmp((directory) => + Effect.gen(function* () { + const session = yield* Session.Service + const created = yield* session.create({ + location: Location.Ref.make({ directory: AbsolutePath.make(directory) }), + }) + const bytes = ToolOutput.MAX_BYTES + 1024 + const command = + process.platform === "win32" + ? `[Console]::Out.Write('x' * ${bytes})` + : `head -c ${bytes} /dev/zero | tr '\\0' 'x'` + + yield* session.shell({ sessionID: created.id, command }) + + const messages = yield* session.messages({ sessionID: created.id, order: "asc" }) + const shell = messages.find((message): message is SessionMessage.Shell => message.type === "shell") + expect(shell).toBeDefined() + if (!shell?.output) throw new Error("Expected shell output") + + expect(Buffer.byteLength(shell.output.output, "utf8")).toBeLessThanOrEqual(ToolOutput.MAX_BYTES) + expect(shell.output.size).toBeGreaterThan(ToolOutput.MAX_BYTES) + expect(shell.output.cursor).toBeLessThan(shell.output.size) + + const model = Model.Ref.make({ + id: Model.ID.make("session-shell-limit"), + providerID: Provider.ID.make("test"), + }) + const content = toLLMMessages([shell], model)[0]?.content[0] + if (!content || content.type !== "text") throw new Error("Expected shell text content") + + const marker = "\n\nOutput:\n" + const outputStart = content.text.indexOf(marker) + expect(outputStart).toBeGreaterThanOrEqual(0) + expect(Buffer.byteLength(content.text.slice(outputStart + marker.length), "utf8")).toBeLessThanOrEqual( + ToolOutput.MAX_BYTES, + ) + }), + ), + ) + it.effect("switches the selected agent through the durable Session event", () => Effect.gen(function* () { const session = yield* Session.Service