Persistent PTY Sessions
Identity and readiness
Section titled “Identity and readiness”TerminalSessionId is a service-minted branded id. Optional names are owner-local display metadata; authorization compares the exact owning Agent, not a name or guessed id.
TerminalWaitReason says why one send returned. It is independent from TerminalSessionStatus: silence or timeout may return while the top-level shell remains alive, while session_exit means that shell exited rather than an arbitrary foreground child.
/** Why one interactive send returned control to its caller. */type TerminalWaitReason = 'stdin_read' | 'inferred_idle' | 'timeout' | 'session_exit'/** Top-level PTY process status, independent of a send's wait reason. */type TerminalSessionStatus = | { kind: 'running' } | { kind: 'exited'; exitCode: number | null; signal: NodeJS.Signals | null }Backend and live session
Section titled “Backend and live session”A backend owns how one registered type starts and detects readiness. TerminalSessionService publishes the returned session only after setup succeeds, then owns id authorization and cleanup. A backend that cannot clean partial startup resources rejects with TerminalBackendCleanupError, allowing disposal to retain the cleanup failure without replacing the caller’s cancellation reason. A backend session owns terminal state and captured-resource quiescence.
/** Replaceable provider for one PTY session type. */interface TerminalBackend { /** Stable type selected by {@link TerminalSpawnRequest.type}. */ readonly type: string /** Create an unpublished session or reject after cleaning partial resources; cleanup failure uses {@link TerminalBackendCleanupError}. */ spawn(spec: TerminalBackendSpawnSpec): Promise<TerminalBackendSession>}/** Backend-owned live session retained by {@link TerminalSessionService}. */interface TerminalBackendSession { /** Initial bounded terminal output returned from `terminal_open`. */ readonly motd: string /** Top-level process id when one exists. */ readonly pid?: number /** Start one exclusive send operation. */ startSend(request: TerminalSendRequest): TerminalSendOperation /** Read one bounded page from retained scrollback. */ read(request: TerminalReadRequest): TerminalReadResult /** Signal the verified foreground process group. */ signal(signal: TerminalSignal): Promise<TerminalSignalResult> /** Observe top-level process status. */ status(): TerminalSessionStatus /** Idempotently close the captured owned process tree and await quiescence. */ close(reason: string): Promise<void>}Send and retained output
Section titled “Send and retained output”One live session accepts one active send. Its operation exposes a consuming output cursor for generic background jobs and one terminal result for a foreground caller. TerminalReadResult separately pages the bounded session scrollback.
/** Live backend-owned send; exactly one may be active per PTY session. */interface TerminalSendOperation { /** Resolves after readiness, timeout, cancellation, or top-level process exit. */ done: Promise<TerminalSendResult> /** Consume output produced since the prior call. */ readOutput(): TerminalSendRead /** Request `SIGINT`; returns false after the operation settled. */ cancel(): boolean}/** Settled result for one foreground or background send. */interface TerminalSendResult { /** Bounded rendered terminal delta remaining at settlement. */ viewport: string /** Why the wait returned; this does not imply arbitrary child-process exit. */ waitReason: TerminalWaitReason /** Top-level session status observed at settlement. */ sessionStatus: TerminalSessionStatus /** Whether output was dropped from the operation or retained scrollback. */ truncated: boolean}Ownership and durability
Section titled “Ownership and durability”TerminalSessionService attaches one awaited cleanup to the exact owner scope, rejects foreign operations, and keeps sessions alive across backend or tool-plugin reload. PTY state and raw bytes remain process-local. Model input and bounded returned output are durable through the existing tool/call, tool/result, and task-result paths rather than duplicate PTY session events.
Cordis API
Section titled “Cordis API”Generated from source by scripts/gen-cordis-catalog.ts (verified fresh by pnpm run verify-cordis-catalog in doc-sync; regenerate with pnpm run gen-cordis-catalog) — this section is byte-identical in both language sides of the page. Signature blocks use a ts cordis-catalog fence and keep the original source JSDoc; dispatch modes are defined in the primer, and the framework-inherited ctx API lives in cordis-api/inherited.md.
ctx.terminals — TerminalSessionService
Section titled “ctx.terminals — TerminalSessionService”In-process registry for replaceable PTY backends and exact-Agent sessions.
/** * Register one backend type for this effect scope. * @param backend - provider with a non-empty unique type. * @returns disposer that removes exactly this contribution. */registerBackend(backend: TerminalBackend): () => void
/** * List registered backend types in registration order. * @returns fresh backend type names. */listBackends(): string[]
/** * Create and publish one owner-scoped session after backend setup succeeds. * @param owner - exact registered Agent that owns access and cleanup. * @param request - backend type plus optional owner-local name and cwd. * @param signal - cancellation of unpublished setup. * @returns published identity, metadata, status, and MOTD. */async spawn(owner: Agent, request: TerminalSpawnRequest, signal?: AbortSignal): Promise<TerminalSpawnResult>
/** * Test whether an exact owner has a published session or unpublished spawn. * @param owner - exact live owner to inspect. * @returns true across the entire spawn-to-close interval, with no publication gap. */hasOwnerActivity(owner: Agent): boolean
/** * Start one exclusive interactive send. * @param owner - exact session owner. * @param id - target PTY identity. * @param request - explicit text, submit behavior, and cancellation. * @returns live operation handle for foreground await or task registration. */startSend(owner: Agent, id: TerminalSessionId, request: TerminalSendRequest): TerminalSendOperation
/** * Read one bounded scrollback page from an owned session. * @param owner - exact session owner. * @param id - target PTY identity. * @param request - optional newest-relative offset and line count. * @returns bounded retained text and pagination metadata. */read(owner: Agent, id: TerminalSessionId, request: TerminalReadRequest = {}): TerminalReadResult
/** * Deliver an allowed signal through an owned backend session. * @param owner - exact session owner. * @param id - target PTY identity. * @param signal - allowed POSIX signal name. * @returns delivered foreground process-group identity. */signal(owner: Agent, id: TerminalSessionId, signal: TerminalSignal): Promise<TerminalSignalResult>
/** * Close one owned session and remove it only after quiescent backend cleanup. * @param owner - exact session owner. * @param id - target PTY identity. * @param reason - diagnostic cleanup reason. * @returns true for a newly closed session, false when the same close is already in flight. */async kill(owner: Agent, id: TerminalSessionId, reason: string = 'model request'): Promise<boolean>
/** * List fresh snapshots for exactly one owner. * @param owner - exact owner whose sessions are visible. * @returns owner-visible snapshots in publication order. */list(owner: Agent): TerminalSessionSnapshot[]Types: Agent