Split from #2351, which now covers attached background shells only.
Problem
Detached shells (mode: "async", detach: true) are the ones meant to keep running after the agent returns control to the user: dev servers, file watchers, watch-mode builds. They don't keep the session from reaching session.idle, so clients stop showing them as in-progress work. That is exactly when the user still wants to see what the process is printing (port, compile errors, crashes) and be able to stop it.
Today the public SDK only gives snapshots for detached shells:
// session.rpc.tasks.list()
export interface TaskShellInfo {
type: "shell";
id: string;
status: TaskStatus;
command: string;
attachmentMode: "attached" | "detached";
/** Path to the detached shell log, when available */
logPath?: string;
pid?: number;
// ...
}
// session.rpc.tasks.getProgress({ id })
export interface TaskShellProgress {
type: "shell";
/** Recent stdout/stderr lines from the running shell command */
recentOutput: string;
pid?: number;
}
plus session.background_tasks_changed (empty payload) and a system.notification with shell_detached_completed when the process exits.
So a client that wants a live view has to either poll tasks.getProgress(), which only returns a short tail, or tail logPath itself, which means guessing the log format, encoding, lifetime, and which filesystem namespace the path resolves in.
Ask
A supported way to stream a detached shell's output by task id, for example:
const sub = await session.rpc.tasks.subscribeOutput({ id, fromOffset });
// or an event: "session.task_output" { taskId, data, offset }
The exact shape is open. The important part is that detached shell output is structured SDK data the client can subscribe to, rather than a file path the client has to tail.
Desired semantics
- Keyed by task/shell id (
TaskShellInfo.id), not by the tool call that started it.
- Works across turns, since detached shells outlive the turn that started them.
- A client that subscribes late or reconnects can get the backlog from an offset without duplicates.
- Documents what happens to the output and the subscription when the session is closed or resumed while the process keeps running.
- Ends with the exit code on the same stream, or points to the existing
shell_detached_completed notification.
Acceptance criteria
- Public SDK API/events for subscribing to detached shell output, generated consistently across supported SDK languages.
- Documented backlog/offset, lifetime, and encoding semantics.
- VS Code Agent Host can render live output for a detached shell (e.g. a dev server) without polling
tasks.getProgress() or reading logPath directly.
Related:
Split from #2351, which now covers attached background shells only.
Problem
Detached shells (
mode: "async",detach: true) are the ones meant to keep running after the agent returns control to the user: dev servers, file watchers, watch-mode builds. They don't keep the session from reachingsession.idle, so clients stop showing them as in-progress work. That is exactly when the user still wants to see what the process is printing (port, compile errors, crashes) and be able to stop it.Today the public SDK only gives snapshots for detached shells:
plus
session.background_tasks_changed(empty payload) and asystem.notificationwithshell_detached_completedwhen the process exits.So a client that wants a live view has to either poll
tasks.getProgress(), which only returns a short tail, or taillogPathitself, which means guessing the log format, encoding, lifetime, and which filesystem namespace the path resolves in.Ask
A supported way to stream a detached shell's output by task id, for example:
The exact shape is open. The important part is that detached shell output is structured SDK data the client can subscribe to, rather than a file path the client has to tail.
Desired semantics
TaskShellInfo.id), not by the tool call that started it.shell_detached_completednotification.Acceptance criteria
tasks.getProgress()or readinglogPathdirectly.Related: