Expand description
In-memory registry of PtyShell agent leases — the authoritative, hot-path answer to “is a human’s keystroke for this block currently locked out?”.
§Why this exists as its own thing rather than a meta read
The lease (term:agentlockuntil,
docs/specs/SPEC_AGENT_INTERACTIVE_PTY_SHELL_API_2026_09_10.md §10.3) is
enforced in controllerinput (server/websocket.rs). That handler
serves the Stop button’s SIGINT (useAgentCommands.ts) and the
debounced PTY-width resizes usePtyWidth.ts sends — the latter in bursts
while a pane is actively being dragged, which is precisely when the UI is
most latency-sensitive. (It is NOT the keystroke path: terminal
keystrokes go through the blockinput WS command instead —
termViewModel.ts, AgentShellSubblock.tsx — see the gap note at the
bottom of this comment.) The first implementation answered the
is-it-leased question by reading the block out of Store:
mstore.get::<Block>(&cmd.blockid) // synchronous SQLite, on every keystrokeStore is one process-wide SQLite connection behind one Mutex<Connection>
(storage/store.rs’s own doc comment: “matching Go’s MaxOpenConns(1)”),
and Store::get is a plain blocking call — so that read put a real disk
query, serialized against every other Store user in the process, inside
an async WS handler, inline on a Tokio worker thread with no
block_in_place/spawn_blocking. That is the same failure class the
sysinfo incident already cost this repo once (commit 0f34704a8, #1782).
See docs/analysis/ANALYSIS_CROSS_PANE_INPUT_DELAY_REGRESSION_2026_09_15.md.
The lease is short-lived (AGENT_LOCK_WINDOW_MS, 4s), process-local, and
recreated on every agent write. None of that needs durability — so it
lives here instead: a HashMap behind an uncontended Mutex, held for
nanoseconds with no I/O inside it. For the overwhelmingly common case
(no agent is driving any shell) the map is empty and the check is a hash
lookup that misses.
§Formerly a known gap, closed 2026-09-16
controllerinput is not the path a human’s keystrokes actually take.
Both the Terminal pane (termViewModel.ts) and the agent pane’s drawer
shell (AgentShellSubblock.tsx) send keystrokes via the blockinput WS
command, which called blockcontroller::send_input directly with no
lease check at all — so the server-side enforcement added in #3194 did
not in fact cover the human-vs-agent write collision it was added for.
The only thing standing in the way there was AgentShellSubblock’s own
agentLocked() gate, i.e. exactly the eventually-consistent frontend
check that enforcement was meant to backstop. This surfaced for real
during a live vim investigation on 2026-09-16: an agent driving vim
through PtyShellInput on its own composer-drawer shell (the same PTY
a human’s blockinput keystrokes reach) had its writes interleave with
other input on that pane, corrupting vim’s cursor-addressed
alternate-screen redraw into a visibly garbled pane — not a hang, byte
interleaving mid-escape-sequence. dispatch_blockinput
(server/websocket.rs) now calls is_locked too, mirroring
controllerinput’s check — see
blockinput_is_dropped_while_an_agent_lock_is_active there for the
regression test.
§Relationship to term:agentlockuntil meta
The meta key is still written, and is still what AgentShellSubblock.tsx
renders its “Agent is using this shell” badge from — but it is now the UX
layer only, exactly as that handler’s own comment already described it
(“the frontend gate is the UX layer that keeps it from happening in the
first place, not the correctness boundary”). This registry is the
correctness boundary. They’re written together (lock_shell_for_agent)
and cleared together (handle_pty_shell_stop, and shell exit — below).
§Fail-open on restart, deliberately
A restarted srv starts with an empty registry, so no lease survives it.
That is the right direction to fail for a lock whose entire purpose is a
UX-level write-collision guard, not a security boundary: the failure mode
of a stale lease is a human who cannot type in their own terminal — at
worst, cannot type exit to close the pane — which is strictly worse than
an agent’s write interleaving with a human’s. Persisted meta could
previously outlive the process that set it; memory cannot.
Statics§
- LEASES 🔒
- block_id → absolute unix-ms timestamp the lease expires at.
Functions§
- is_
locked - Is a human’s input for
block_idcurrently locked out by an agent lease? - leases 🔒
- lock_
until - Lease
block_idagainst human input untiluntil_ms(absolute unix ms). - release
- Release
block_id’s lease immediately. Returns whether it was actually held (and unexpired) at the moment of the call —PtyShellStopreports that back to the agent asreleased. - release_
and_ clear_ meta - Release the lease AND clear the
term:agentlockuntilmeta copy the frontend gates on, broadcasting the update so it takes effect now.