Kaede AkinoandClaude Opus 5 4b861eae26 Reuse one SSH session per host, not one per command
`ssh_exec` built and tore down a complete SSH connection every time it
ran. Its `defer` freed the session and closed the transport before
returning, so each command paid a TCP connect, a handshake, a host-key
check, and a public-key or Secure Enclave signature. With a jump host
the whole route was rebuilt per hop, per command: an agent turn running
five commands behind one jump performed ten handshakes to do work that
needs one connection and five channels.

SFTP was pooled per `(runID, hostID)`, which is barely a pool — a run is
short — and it had a reentrancy hole. Two tool calls that both reached
the `await` on `connectSFTP` before either stored its client each built
a connection, and one was never disconnected.

The pool is now keyed by host alone and outlives runs and conversations,
which is what makes an idle timeout meaningful: run-scoped connections
are already bounded, so a sweep would rarely have fired. A follow-up
message reuses the connection; three idle minutes gives it back. Exec
and SFTP stay on separate sessions so a long command cannot block a
directory listing — at most two connections per host, against one per
command before.

Two boundaries make reuse safe rather than merely cheap.

A retry only happens while the command is provably not running.
`channelUnavailable` is thrown for a dead session, a channel that would
not open, or a `process_startup` that failed; everything after startup
succeeds is rethrown unchanged. A socket that drops mid-read must not
silently run half an `rm` a second time.

A command that timed out or was cancelled leaves the channel mid-stream
with the remote possibly still working. Closing it courteously means a
blocking `libssh2_channel_close` on the queue every later command for
that host has to wait behind, so the session is abandoned instead and
`libssh2_session_free` reclaims the channel with it.

Reuse also has to keep answering the same question. Each entry carries a
route fingerprint — host, port, username, known host keys, proxy, jump
chain — built only from records the store already holds. No Keychain
read: this runs before every command, and a rotated credential on an
unchanged identity is not a mismatch, because the live session is still
the same box and the same login. Editing a host's address retires the
connection bound to the old one rather than running the next command on
a machine the user no longer means.

No connect or disconnect tool. The model does not manage this, so
putting four more definitions in every request would buy nothing.

`AgentRuntime.runCleanup` goes with it: closing pooled connections was
its only caller, and leaving it wired to a no-op invites someone to hang
per-run teardown off a pool that is no longer per-run.

The unit tests run against stubs and prove the pool's behaviour, not
libssh2's. That one connection now carries five commands still has to be
confirmed against a real host, in its `auth.log`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 22:58:31 +08:00
2026-08-24 18:20:20 +08:00
2026-08-24 18:20:20 +08:00
2026-08-24 18:20:20 +08:00
2026-08-25 20:25:27 +08:00
S
Description
AI-driven remote workspace for Mac, iPhone, and iPad.
https://termind.akinokaede.com
GPL-3.0
23 MiB
0 Stars 1 Watchers 0 Forks
Languages
Swift 98.9%
Python 0.8%
Ruby 0.3%