fix(core): preserve shell output tail
Shell truncation read from cursor zero, so large command output kept the head instead of the useful tail. Read the final capture window and cover the direction with head/tail markers.
This commit is contained in:
parent
49cb73b348
commit
d8b7a0a391
9 changed files with 32 additions and 12 deletions
|
|
@ -139,8 +139,9 @@ export const layer = Layer.effect(
|
|||
const cursor = input?.cursor ?? 0
|
||||
const limit = input?.limit ?? 65536
|
||||
if (cursor >= session.size) return { output: "", cursor: session.size, size: session.size, truncated: false }
|
||||
const start = Math.max(0, cursor)
|
||||
const length = Math.min(limit, session.size - start)
|
||||
const available = session.size - Math.max(0, cursor)
|
||||
const start = input?.keep === "tail" ? Math.max(cursor, session.size - limit) : Math.max(0, cursor)
|
||||
const length = Math.min(limit, available)
|
||||
const buffer = Buffer.alloc(length)
|
||||
const bytesRead = yield* Effect.promise(
|
||||
() =>
|
||||
|
|
|
|||
|
|
@ -201,7 +201,7 @@ export const Plugin = {
|
|||
|
||||
const settleShell = Effect.fn("ShellTool.settleShell")(function* () {
|
||||
const final = yield* shell.wait(info.id)
|
||||
const page = yield* shell.output(info.id, { limit: MAX_CAPTURE_BYTES })
|
||||
const page = yield* shell.output(info.id, { limit: MAX_CAPTURE_BYTES, keep: "tail" })
|
||||
|
||||
if (final.status === "timeout") {
|
||||
return {
|
||||
|
|
@ -213,7 +213,7 @@ export const Plugin = {
|
|||
}
|
||||
}
|
||||
|
||||
const truncated = page.size > page.cursor
|
||||
const truncated = page.size > MAX_CAPTURE_BYTES
|
||||
const body = page.output || "(no output)"
|
||||
const notice = truncated ? `\n\n[output truncated; full output saved to: ${final.file}]` : ""
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -162,10 +162,10 @@ const idleCommand = isWindows ? "Start-Sleep -Seconds 60" : "sleep 60"
|
|||
const bodyExitCommand = isWindows
|
||||
? "[Console]::Out.Write('body'); Start-Sleep -Milliseconds 100; exit 7"
|
||||
: "printf body && exit 7"
|
||||
const overflowCommand = (bytes: number) =>
|
||||
const overflowMarkersCommand = (bytes: number) =>
|
||||
isWindows
|
||||
? `[Console]::Out.Write(('x' * ${bytes})); Start-Sleep -Milliseconds 100`
|
||||
: `head -c ${bytes} /dev/zero | tr '\\0' 'x'`
|
||||
? `[Console]::Out.Write('head-marker'); [Console]::Out.Write(('x' * ${bytes})); [Console]::Out.Write('tail-marker'); Start-Sleep -Milliseconds 100`
|
||||
: `printf head-marker; head -c ${bytes} /dev/zero | tr '\\0' 'x'; printf tail-marker`
|
||||
|
||||
const withSession = <A, E, R>(directory: string, body: (registry: ToolRegistry.Interface) => Effect.Effect<A, E, R>) =>
|
||||
Effect.gen(function* () {
|
||||
|
|
@ -396,15 +396,18 @@ describe("ShellTool", () => {
|
|||
reset()
|
||||
const bytes = ShellTool.MAX_CAPTURE_BYTES + 1024
|
||||
return withSession(tmp.path, (registry) =>
|
||||
settleTool(registry, call({ command: overflowCommand(bytes) }, "call-overflow")),
|
||||
settleTool(registry, call({ command: overflowMarkersCommand(bytes) }, "call-overflow")),
|
||||
).pipe(
|
||||
Effect.andThen((settled) =>
|
||||
Effect.sync(() => {
|
||||
expect(settled.output?.structured).toMatchObject({ exit: 0, truncated: true })
|
||||
expect(settled.output?.content[0]).toMatchObject({
|
||||
type: "text",
|
||||
text: expect.stringContaining("output truncated; full output saved to:"),
|
||||
text: expect.stringMatching(/tail-marker[\s\S]*output truncated; full output saved to:/),
|
||||
})
|
||||
const content = settled.output?.content[0]
|
||||
if (content?.type === "text" && typeof content.text === "string")
|
||||
expect(content.text.includes("head-marker")).toBe(false)
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue