Expand description
Gopher (RFC 1436) client.
§One connection per request, deliberately
Gopher has no session. The client sends one selector line, the server answers and closes — the close is the framing, because the reply carries no length and no content type. So there is no persistent socket for this client to hold, and “browsing” means opening a new connection per item.
That is exactly what happens here: GopherClient::connect_with_llm_actions opens one
connection to prove the hole is reachable and to obtain the local address the Client
trait must return, then closes it. Every fetch afterwards — the model’s, or one injected
from the dashboard — opens its own connection to whatever host and port the action names,
which is what lets a menu item point at a different server and still be followable.
§Menu or document: the requesting item’s type decides
Nothing in a Gopher reply says which it is. The item type in the request is the only
signal, so send_gopher_request carries an item_type and this module reads the reply
according to it: 1 and 7 are parsed as menus, everything else as a document. There is
exactly one exception, and it is not a heuristic about content: a reply whose first line
is a type-3 item is reported as an error whatever was asked for, because type 3 is the
only error channel the protocol has and a server uses it to refuse any request.
When the guess is wrong the reply is not thrown away. A menu request whose reply contains
no parseable menu line at all is reported as gopher_document_received with
requested_item_type still set, so the model can see the mismatch; a menu request whose
reply is mostly menu lines keeps the odd ones in malformed_lines. The reverse is not
attempted — a document may legitimately be full of tabs, so a document reply is never
re-read as a menu.
§The follow-up chain
Browsing is iterative by nature: fetch a menu, pick an item, fetch that, repeat. A
follow-up fetch that raised no event would give the model exactly one turn and then go
deaf, which is the elasticsearch/http2 defect the root CLAUDE.md catalogues. So every
fetch raises its event and asks the model again, and the recursion is bounded by
MAX_FOLLOWUP_DEPTH rather than by silence. The recursive call is boxed (an async fn
that awaits itself has an infinitely-sized future) and the boxed type names + Send
explicitly, because it is awaited inside a tokio::spawn.
Re-exports§
pub use actions::GopherClientProtocol;
Modules§
- actions
- Gopher (RFC 1436) client protocol actions.
Structs§
- Gopher
Client - Gopher client that browses a gopher hole under LLM control.
- Gopher
Menu Item - One parsed menu line.
Enums§
- Gopher
Reply - What a reply turned out to be.
Constants§
- DEFAULT_
GOPHER_ PORT - The default port when a remote address names no port. RFC 1436 §2.
- MAX_
FOLLOWUP_ DEPTH - How many model round-trips one automatic browse may chain.
- MAX_
MENU_ ITEMS - Menu items kept from one reply. A menu longer than this is a server misbehaving.
- MAX_
REPLY_ BYTES - Largest reply this client will hold. Gopher sends no length, so without a cap any server can make the client allocate until it dies.
Functions§
- item_
type_ name - What an RFC 1436 item type character means, for the model’s benefit.
- parse_
gopher_ reply - Read a reply according to the item type that was asked for.
- request_
line - Build the request line. A query makes it a type-7 search.