AGENTS.md@packages/coding-agent/src/core/extensions/builtin/terminal · git:20260904.d08209a · 2026-09-04 · sha256 5af5b4c9c1039db3

AGENTS.md@packages/coding-agent/src/core/extensions/builtin/terminal git:20260904.d08209aA

Immutable. This exact content is served forever at /api/v1/blob/5af5b4c9c1039db3.

# builtin/terminal

Builtin extension #18. Replaces one-shot bash with a **PTY-backed persistent session** model: `bash` plus companion tools `bash_output`, `bash_input`, `bash_resize`, `kill_bash`, and `monitor`. Registered after `bash-timeout` (so the resolved default timeout reaches PTY bash) and after `anthropic-bash` (so a native Anthropic bash tool makes terminal step aside).

## FILES

```
terminal/
├── index.ts             # Barrel: extension + settings + tool-name constants + shared key/regex helpers
├── extension.ts         # Registration entry — registers all six tools, wires lifecycle + reload bundles
├── manager.ts           # TerminalManager: session map ownership
├── runtime-session.ts   # TerminalRuntimeSession: one live PTY
├── session-bundle.ts    # TerminalSessionBundle: reload parking/claiming across extension generations
├── monitor-registry.ts  # MonitorRegistry: registered watches over session output
├── monitor-notify.ts    # Event → notification delivery (283 LOC, largest non-tool file)
├── monitor-status*.ts   # Footer status text + 1s unref'd ticker
├── notify.ts            # TerminalNotifier
├── output-format.ts     # Output shaping/sanitization
├── settings.ts          # loadTerminalSettings / resolveTerminalSettings
├── shared.ts            # Defaults: 120x40, 10000 scrollback, 32 sessions, 1,000,000 output chars;
│                         durability caps (MAX_DURABLE_MONITORS 5, DURABLE_MONITOR_EXPIRY_MS 7d)
├── terminal-manifest.ts # Durable per-session record + TerminalManifestWriter (transition writes,
│                         debounced checkpoints, durableCount admission, adoptRestored)
├── restore.ts           # Manifest read side: strict fail-closed parse + restoreTerminalState
│                         (classify → per-class handler → ONE digest) + reapplyPersistedMute
├── manifest-lease.ts    # Per-session pid lease: only one live process restores/records a session
├── durable-file.ts      # `checkpointed-file` handler: compare saved checkpoint once, report
│                         at most one detached created/replaced/modified line
├── durable-command.ts   # `restartable-command` handler: re-spawn the saved command exactly once,
│                         no deadline, same mon_ id, no replay of pre-restart output
├── prompt.ts            # Tool prompt guidance
└── tools/               # bash.ts (420 LOC), bash-output/input/resize, kill-bash, monitor,
                         # spawn, render, context, foreground-detach/window, sleep-wait
```

## WHERE TO LOOK

| Task | File |
|---|---|
| Change PTY spawn/exec behavior | `tools/bash.ts`, `tools/spawn.ts` |
| Change auto-detach window (~60s foreground) | `tools/foreground-detach.ts`, `tools/foreground-window.ts` |
| Add/change a monitor condition | `tools/monitor.ts` + `monitor-registry.ts` |
| Change how monitor wakes the session | `monitor-notify.ts` |
| Change footer terminal status | `monitor-status.ts`, `monitor-status-ticker.ts` |
| Change defaults (size, scrollback, caps) | `shared.ts` |
| Survive an extension reload | `session-bundle.ts` |
| Survive a full restart (what is persisted) | `terminal-manifest.ts` |
| Change restore classification or the digest | `restore.ts` |
| Change how a durable class comes back | `durable-file.ts`, `durable-command.ts` |
| Change single-live-process ownership | `manifest-lease.ts` |

## CONVENTIONS

- **Monitor is the wait mechanism.** Observable state changes are delivered as events that wake the session; `bash_output` is for peeking, not waiting.
- **Companion tools stay active together** with `bash` and are synchronized with extension lifecycle and session-reload bundles — a companion tool without a live PTY is a bug.
- Tool output is capped at 2,000 lines / 50 KiB and sanitized before it reaches the model; monitor status refreshes on a 1-second unref'd interval.
- TypeBox schemas are **flat root objects, never root unions** (`tools/monitor.ts` is the reference) — several provider conversions rebuild schemas from top-level `properties` and a root `anyOf` arrives empty.
- Environment overrides are injected for tests rather than mutating `process.env`.
- Cross-extension seam: `monitor-state-event.ts` (parent dir) carries `TerminalMonitorStateEvent`, consumed by `goal/`.
- **`persistent` means durable, not merely long-lived.** `persistent: true` is the standing-watch switch: no deadline, and the entry is persisted in a durability class (`restartable-command` for `command`, `checkpointed-file` for `path`) that a restart brings back. Without it the entry is `ephemeral` and dies with the process — so any new durability behavior belongs behind that one flag, never behind a new parameter or action.
- **ONE digest per restart.** `restoreTerminalState` returns counts, `extension.ts` turns them into exactly one coalesced sentence. Never notify per monitor: a session with five durable watches must still wake once.
- **Write on transition only.** `TerminalManifestWriter` persists lifecycle transitions (register, settle, pause/resume, background start/exit, shutdown) plus debounced checkpoints — never per output line, never a runtime handle. `adoptRestored` deliberately does not write; the restored entry reaches disk on the next real transition.
- **`pause`/`resume`/`rearm` resolve RUNTIME ids only.** `MonitorRegistry` keys records by `bash_N`/`watch_N`, so handing it a stable `mon_` id silently no-ops. A restore must re-apply a persisted mute with the FRESH runtime id it just allocated (`reapplyPersistedMute`); resolve a caller-supplied id through `resolveTerminalId` first.

## ANTI-PATTERNS

- Using `tmux` for long-running/interactive work through terminal `bash` — the PTY session is the mechanism.
- Polling with `sleep`, foreground wait loops, or repeated `bash_output` while waiting on observable state.
- Letting a companion tool dangle without a live PTY `bash`, or letting an output observer interfere with session ingest.
- Assuming the injected `timeout` kills a background session — it never does; use `kill_bash`.

## NOTES

- `changes.md` records `wait_for`, `block`, and `timeout` as **deprecated ghost schema parameters**: still accepted for compatibility, but not the current control model. Do not build new behavior on them.
- The default bash timeout itself is owned by `builtin/bash-timeout/`, not here; terminal consumes the resolved value.