Files
deepseek-harness/docs/subsystems/pty.md
Tianyi Cui f7323354bb docs: generate each subsystem's cordis surface into its own page; delete the flat catalogs
Rebuild of the region machinery (PR3) on the post-#904 Typert projection:
renderPageRegion/renderInheritedPage live in dsh-typert-generator beside the
projection; scripts/gen-cordis-catalog.ts owns the curated SERVICE_PAGE /
EVENT_SCOPE_PAGE / SERVICE_WALK_EXEMPTIONS / LINK_MAP partition (fail-loud in
both directions, with the independent Context-merge scan backstopping the
projection's blind spot), spliceRegion, and the guarded pair auto-record.
docs/cordis-catalog/ is deleted: the flat events/services catalogs dissolve
into per-page regions and docs/cordis-catalog/core moves to docs/cordis-api/
with the inherited tier as its own generated page. The partition absorbs the
post-regrouping surface: ctx.typert → invariants.md, ctx.directoryPicker →
workspace.md, skills/* events → skills.md, and the four launcher-provided tui
accessor values join the named exemptions.
2026-08-09 01:31:57 +08:00

7.9 KiB

Persistent PTY Sessions

English | 中文

Types shared by PTY backends, ctx.pty, and the model-facing consumer. The persistent PTY Agent Note owns the rationale; this page records the cross-package vocabulary from packages/pty/pty/src/types.ts.

Identity and readiness

PtySessionId 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.

PtyWaitReason says why one send returned. It is independent from PtySessionStatus: 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 PtyWaitReason = 'stdin_read' | 'inferred_idle' | 'timeout' | 'session_exit'
/** Top-level PTY process status, independent of a send's wait reason. */
type PtySessionStatus =
  | { kind: 'running' }
  | { kind: 'exited'; exitCode: number | null; signal: NodeJS.Signals | null }

Backend and live session

A backend owns how one registered type starts and detects readiness. PtyService publishes the returned session only after setup succeeds, then owns id authorization and cleanup. A backend that cannot clean partial startup resources rejects with PtyBackendCleanupError, 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 PtyBackend {
  /** Stable type selected by {@link PtySpawnRequest.type}. */
  readonly type: string
  /** Create an unpublished session or reject after cleaning partial resources; cleanup failure uses {@link PtyBackendCleanupError}. */
  spawn(spec: PtyBackendSpawnSpec): Promise<PtyBackendSession>
}
/** Backend-owned live session retained by {@link PtyService}. */
interface PtyBackendSession {
  /** 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: PtySendRequest): PtySendOperation
  /** Read one bounded page from retained scrollback. */
  read(request: PtyReadRequest): PtyReadResult
  /** Signal the verified foreground process group. */
  signal(signal: PtySignal): Promise<PtySignalResult>
  /** Observe top-level process status. */
  status(): PtySessionStatus
  /** Idempotently close the captured owned process tree and await quiescence. */
  close(reason: string): Promise<void>
}

Send and retained output

One live session accepts one active send. Its operation exposes a consuming output cursor for generic background tasks and one terminal result for a foreground caller. PtyReadResult separately pages the bounded session scrollback.

/** Live backend-owned send; exactly one may be active per PTY session. */
interface PtySendOperation {
  /** Resolves after readiness, timeout, cancellation, or top-level process exit. */
  done: Promise<PtySendResult>
  /** Consume output produced since the prior call. */
  readOutput(): PtySendRead
  /** Request `SIGINT`; returns false after the operation settled. */
  cancel(): boolean
}
/** Settled result for one foreground or background send. */
interface PtySendResult {
  /** Bounded rendered terminal delta remaining at settlement. */
  viewport: string
  /** Why the wait returned; this does not imply arbitrary child-process exit. */
  waitReason: PtyWaitReason
  /** Top-level session status observed at settlement. */
  sessionStatus: PtySessionStatus
  /** Whether output was dropped from the operation or retained scrollback. */
  truncated: boolean
}

Ownership and durability

PtyService 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 surface

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 surface lives in cordis-api/inherited.md.

ctx.ptyPtyService

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: PtyBackend): () => 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: PtySpawnRequest, signal?: AbortSignal): Promise<PtySpawnResult>

/**
 * 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: PtySessionId, request: PtySendRequest): PtySendOperation

/**
 * 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: PtySessionId, request: PtyReadRequest = {}): PtyReadResult

/**
 * 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: PtySessionId, signal: PtySignal): Promise<PtySignalResult>

/**
 * 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: PtySessionId, 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): PtySessionSnapshot[]

Types: Agent

Source: packages/pty/pty/src/index.ts:105