Agent-First Terminal v0.3.0

by Agent-First Kit Contributors

A program that stops reading its input could freeze every other session, a client that stops reading events could freeze the server, and a supervisor's SIGTERM was not a stop. Writes, subscriptions and shutdown are now bounded by their own owner, an SSE resume says when history is missing, and a secret window no longer leaves its screen behind.

A terminal is two programs that each assume the other keeps up. The one inside the PTY assumes someone reads its output; the one outside assumes its input gets read. afterminal sits in the middle, and this release is about what happens in the middle when either side simply stops.

A program that stops reading

A PTY has a small kernel buffer. When the program inside stops reading — a paused process, a full-screen app busy elsewhere, a shell waiting on something — a large write blocks until it does.

That write ran on an async worker, while holding the lock that coordinates every session. So one session’s stuck input could stop screen, close and every other call for every other session, and the UI with them.

Each chunk is now authorised under the manager lock and then written outside it, under only that session’s writer lock, on a blocking task rather than an async worker. The stock UI keeps processing events and actions for other sessions while an input write is pending. A later lease change does not cancel a chunk whose write was already authorised.

That opened one boundary that had to be closed again: a secret-input window could start or end while a chunk was still being written, moving the line between ordinary and private input. Starting or ending a window in that interval now returns a retryable input_in_progress.

A client that stops reading

Event subscriptions — raw output and terminal events — had unbounded queues. A client that connected and stopped reading made the server buffer for it forever, and shutdown could wait on a producer blocked behind it.

Each subscription now holds 128 entries. When it fills, that subscriber is disconnected; the fast ones keep going. The stock UI resubscribes and publishes the full current state, so a dropped page does not stay on an older screen. On shutdown, event responses end before the HTTP server drains, which also releases a producer blocked on a client that stopped reading.

Resuming says when history is missing

The SSE stream keeps the newest 512 events. A client resuming with Last-Event-ID used to get whatever was retained after its cursor, which is indistinguishable from complete history when the cursor had already fallen out of the window.

A cursor that cannot resume — invalid, older than the retained window, or ahead of the latest sequence — now first receives a named replay_gap event with the reason and the retained bounds, then the whole retained backlog. The notice has no SSE id, so it does not move the client’s cursor, and ordinary events are unchanged. A client that sees it should reread the session list and screens; the retained events cannot reconstruct what is gone.

A stop is a stop

afterminal serve stopped on Ctrl-C and nothing else. A supervisor sending SIGTERM, or a closed terminal sending SIGHUP, did not get a clean shutdown. Both are handled now, and the handlers are registered before the ready event is printed, so a supervisor that signals as soon as it sees readiness still gets one.

Exit state became something afterminal keeps rather than reconstructs. On Windows, ConPTY can hold its output pipe open after the program exits, so an exit was only noticed at EOF; status polling now publishes it without one. An exit after a reader error is announced once, and a known exit code is never replaced by none. Waiting for that code also no longer holds the session’s child lock, which had stalled every API call on that session for up to 250 ms.

Teardown releases what it owns. A child that is being terminated is reaped even if the signal races its exit, PTY descriptors are acquired before a child exists so an exhausted descriptor table fails cleanly instead of leaving a spawned process behind, and a reader is woken on close even when another process still holds the PTY open.

A secret window leaves nothing behind

Closing a secret-input window used to make the screen visible again exactly as it was. If the program echoed what was typed, the secret was on the screen the agent was about to read.

After a secret window closes, the screen starts blank and contains only new output. Application cursor, bracketed paste and alternate-screen modes are preserved; the picture is not. Ask the program to redraw — Ctrl-L, usually — when a full screen is needed.

Smaller things

On Windows, only kill is supported, and it terminates the process started for the session rather than a foreground process group. interrupt and terminate return signal_not_supported, and the docs say so.

cargo install agent-first-terminal now builds the same CLI the release packages ship: the api feature used to be release-only, and is in the default set now. Library users that want only the core take default-features = false.

--version reports the build it came from, as every other spore’s does. The attach example takes the session id positionally, as the CLI does, and the skill reads quiescence from the screen snapshot instead of waiting for an event that does not exist.

Breaking changes

Getting it

$ brew install agentfirstkit/tap/afterminal
$ cargo install agent-first-terminal