5c4419eOpen thread| … | … | @@ -0,0 +1,116 @@ | |
| 110 | + | if (text.length > 0) yield* input.onChunk(text); | |
| 111 | + | } | |
| 112 | + | } | |
| 113 | + | if (last) return; | |
🟡 Medium Adapters/ClaudeTaskOutputTail.ts:113
When output ends with an incomplete UTF-8 sequence, tailClaudeTaskOutput silently drops the buffered bytes instead of emitting the documented U+FFFD replacement. The last branch returns without flushing decoder; call decoder.decode() once more and forward its result before returning.
Also found in 1 other location(s)
apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts:5874
Starting
tailClaudeTaskOutputhere exposes a truncation bug for output ending with an incomplete UTF-8 sequence. The tail reads every chunk withTextDecoder.decode(..., { stream: true })but never performs a final non-streamingdecode()when stopped, so buffered terminal bytes are discarded rather than emitted as replacement characters. A command that writes invalid/truncated UTF-8 therefore has a live tail that silently omits its final bytes.
No longer relevant as of ae8b5493b77616abdf5f03f0d8254d018199d9ba
| … | … | @@ -0,0 +1,733 @@ | |
| 521 | + | * cursor, style and any unfinished escape. A viewer that starts from it (or | |
| 522 | + | * resyncs to it) applies later raw chunks exactly as the server does. | |
| 523 | + | */ | |
| 524 | + | export function terminalOutputResumeText(state: TerminalOutputState): string { | |
🟡 Medium src/terminalOutput.ts:524
terminalOutputResumeText produces incorrect resync behavior: after ESC 7/CSI s, a later ESC 8/CSI u is ignored by the viewer, so abc\x1b7def followed by \x1b8X renders abcdefX instead of abcXef. The resume text also computes cursor movement from the full screen.lines even when state.text has been tail-truncated, positioning the cursor on the wrong displayed row after resync. Serialize the saved cursor and calculate the reconstructed cursor position relative to the retained output.
No longer relevant as of ae8b5493b77616abdf5f03f0d8254d018199d9ba
| … | … | @@ -3777,13 +3817,15 @@ export function makeClaudeAdapterV2( | |
| 3784 | 3826 | type: "command_execution", | |
| 3785 | 3827 | input: commandInputFromClaudeTool(input.toolName, input.toolInput), | |
| 3786 | - | ...(outputText.length === 0 ? {} : { output: outputText }), | |
| 3828 | + | ...(commandOutputText.length === 0 ? {} : { output: commandOutputText }), | |
🟡 Medium Adapters/ClaudeAdapterV2.ts:3828
claudeCommandOutputText inserts an extra newline between nonempty stdout and stderr, so output such as { stdout: "ok\n", stderr: "warning\n" } is persisted as "ok\n\nwarning\n" instead of Claude's recorded "ok\nwarning\n". This makes the completed command row differ from the emitted output; concatenate the streams as ${stdout}${stderr} without adding a separator.
No longer relevant as of ae8b5493b77616abdf5f03f0d8254d018199d9ba
(agent reply) Partly agree, fixed differently in ae8b5493b7. Claude's Bash results trim trailing newlines from each stream (the recorded fixtures have e.g. "stdout":"~/apps/web$ tsgo --noEmit cache disabled" with no trailing \n), so plain ${stdout}${stderr} would glue the last stdout line to the first stderr line. It now adds the newline only when stdout doesn't already end in one, so neither case produces a blank or merged line.
— Claude Opus 5.5
| … | … | @@ -0,0 +1,733 @@ | |
| 324 | + | ||
| 325 | + | moveTo(row: number, col: number): void { | |
| 326 | + | this.row = Math.max(0, Math.min(this.lines.length - 1, row)); | |
| 327 | + | this.col = Math.max(0, col); | |
🟠 High src/terminalOutput.ts:327
A large horizontal cursor move such as \x1b[999999999999C makes the next printable character throw RangeError: Invalid string length in writeLine, allowing arbitrary command output to crash the output path. moveTo accepts the unbounded column and line 126 passes it to " ".repeat; clamp cursor columns to a bounded tail size before writing.
No longer relevant as of ae8b5493b77616abdf5f03f0d8254d018199d9ba
| … | … | @@ -0,0 +1,297 @@ | |
| 214 | + | ? observe(stored.event.payload) | |
| 215 | + | : Effect.void, | |
| 216 | + | ), | |
| 217 | + | Effect.catchCause((cause) => | |
🟡 Medium orchestration-v2/CommandOutputHub.ts:217
An eventSink.stream failure is swallowed as a successful, terminated fiber, so if the initial projection is still running, the subscription waits forever in Queue.take and never emits the declared OrchestrationV2CommandOutputError or closes. Propagate the stream failure to the subscription instead of converting it to a warning-only completion; finish does not wake subscribers.
No longer relevant as of ae8b5493b77616abdf5f03f0d8254d018199d9ba
Sorry, I'm unable to act on this request because you do not have permissions within this repository.
| … | … | @@ -1990,6 +1991,17 @@ const makeWsRpcLayer = ( | |
| 1995 | + | observeRpcStream( | |
| 1996 | + | ORCHESTRATION_V2_WS_METHODS.subscribeCommandOutput, | |
| 1997 | + | Stream.unwrap( | |
| 1998 | + | CommandOutputHub.CommandOutputHub.use((hub) => Effect.succeed(hub.subscribe(input))), | |
🟠 High src/ws.ts:1998
subscribeCommandOutput creates an unbounded subscription for arbitrary threadId/itemId values. For a nonexistent or non-command item, CommandOutputHub.subscribe never settles, while the forked eventSink.stream remains subscribed to the global turn-item.updated PubSub before filtering; repeated RPC subscriptions therefore retain every future update once per subscriber and can exhaust server memory. Validate that the item is a command before subscribing and create the event stream with bounded: true.
🤖 Copy this AI Prompt to have your agent fix this:
| 1 | In file @apps/server/src/ws.ts around line 1998: |
| 2 | |
| 3 | `subscribeCommandOutput` creates an unbounded subscription for arbitrary `threadId`/`itemId` values. For a nonexistent or non-command item, `CommandOutputHub.subscribe` never settles, while the forked `eventSink.stream` remains subscribed to the global `turn-item.updated` PubSub before filtering; repeated RPC subscriptions therefore retain every future update once per subscriber and can exhaust server memory. Validate that the item is a command before subscribing and create the event stream with `bounded: true`. |