ellie.el previously accessed ollie through a FUSE-mounted 9P
filesystem (plain file I/O via insert-file-contents/write-region).
This had several failure modes: stale mounts after suspend/resume,
no error propagation from 9P Rerror, no streaming support.
Now uses the ollie-9p CLI tool which speaks the 9P protocol
directly over the Unix socket. Also removed ellie-dired binding
(C-c C-d) and the ellie-mount-directory customization since
there is no mount point.
The completion subsystem was updated to use ollie-9p rdwr complete
instead of a shell pipeline to a 9P mount path.
session/{id}/agent/{aid}/plan → ollie-9p write
session/$OLLIE_SESSION_ID/agent/$OLLIE_UNAME/plan
UNAME is the agent's immutable ID, matching the agent directory
name. Using ollie-9p write instead of a mounted filesystem path.
plan now lives at session/{id}/agent/{aid}/plan instead of
session/{id}/plan. Moved the Leaf declaration from fs/sessionfiles.go
to fs/agentfiles.go. Updated both prompt docs to match.
Each lsp_*.meta now lives in tools/lsp/cmd/<name>/ alongside
main.go, rather than in data/tools/. The lsp-tools just target
was updated to install both the binary and its .meta sidecar.
AGENTS.md updated to document the pattern for compiled tools.
With the zero-tools transition complete, the tools/builtin/ package
served no purpose — Builtins() returned nil. Removed:
- tools/builtin/builtins.go and the directory
- builtins field, WithBuiltins, Dispatch from toolsrv.Server
- Handler type from toolsrv (only used by builtins)
- OnPreDispatch/WithOnPreDispatch (dead code)
- Strict field and WithStrict (dead code, never read)
- CallTool simplified — no fallback to Dispatch
- builtin import and WithBuiltins/WithStrict calls from ollie-remote
- tools/builtin from justfile build/test targets
Updated AGENTS.md, ARCHITECTURE.md, README.md, EVOLUTION.md
When a streaming tool completes, emit a final empty-Content event so the
log handler always closes the markdown source fence. In the handler,
emit [[[end]]] directly instead of a blank line, so the GUI block parser
transitions back to BlockStart regardless of what follows.
Add a tool_load tool that writes a tool name to the session's 9P
tools file via ollie-9p, enabling on-demand tool loading.
Reduce autoLoad lists in default.json and theo.json to bare
essentials — everything else is loaded via tool_load.
The panic at fs/lifecycle.go:463 was a nil function pointer: fs.Config.MkdirAll
is a func field, not a method, and the struct literal in cmd/olliesrv/main.go
omitted it, defaulting to nil. When CreateAgent called
rs.cfg.MkdirAll(...), it panicked before ever reaching the autoload loop.
- Add MkdirAll: os.MkdirAll to fs.Config in main.go
- Spawn tool server in Create() so empty session/new sessions are
operational, then reuse it in CreateAgent (else if sess.proc != nil)
- Wrap create agent error with fmt.Errorf for clearer messages
- Add panic stack trace to stderr for debugging
Ollie's extensibility is the opposite of Emacs: instead of building
capabilities on top of an interpreter, the core provides integration
surfaces (9P, shell) and pushes everything else to the surrounding
system. This is the Acme/Plan 9 model.
After Tcreate on a file, the fid is open and should be usable for
read/write without a separate Topen. Set f.entry via root.Open()
after creation so subsequent reads/writes on the same fid work.
Tree.Open() returns an error for directories (is a directory), so
f.entry stays nil after Topen on a dir. The entry == nil guard in
read() was firing before the isDir branch, breaking directory reads.
Moved the guard after the isDir check — directories use buildDirData
directly and never need entry.
- handle() is gone — the dispatch + panic recovery is inlined into
the per-request goroutine in Start(). Panic recovery is lifecycle,
not protocol handling.
- Serve → Start: accepts a connection and runs the 9P loop
- Shutdown → Kill: stops accepting, cancels sessions, waits for
goroutines to drain.
- 2 methods remain on *Server: Start, Kill. Server is purely lifecycle.
- All 9P protocol handlers are package-level functions.
- attach() is now a function taking (*olog.Logger, *GroupTable, ...)
- open() is now a function taking (*fs.Tree, *GroupTable, ...)
- Both captured in the handler map via closures over s.sink, s.groups
- 3 methods remain on *Server: Serve, handle, Shutdown
- Added entry (fs.File) field to fid struct
- open() stores the result of rootTree.Open() on f.entry after
permission check succeeds
- read() uses f.entry instead of calling root.Open(path[1:]) —
fids survive renames because the File object references the
tree entry by pointer, not by path
- write() uses f.entry for RequestRespMode writes — no longer
needs root parameter at all; it's a pure connState+fid function
- attach: reject if fc.Fid already in use (was silently overwriting)
- walk: reject if fc.Newfid already in use (both clone and full walk)
- open: validate fid path exists; delete stale fid if path is gone
- stat: validate fid path exists; delete stale fid if path is gone
- wstat: after rename, GC all fids on this connection that reference
the old path tree (was previously updating paths — now just deletes)
The server fully owns the fid map. The client proposes fid numbers,
the server validates and accepts or rejects. Stale fids (pointing to
renamed/deleted paths) are garbage collected when detected.
- GroupTable is a standalone type with Add/Remove/IsMember, owns its
own mutex and map. Server now holds groups *GroupTable.
- Removed AddGroup/RemoveGroup/InGroup from *Server (3 fewer methods).
- Permission check in open() now computes effective bits as a mask:
bits := perm & 7 // world bits
if in group { bits |= (perm >> 3) & 7 } // group bits
if is owner { bits |= (perm >> 6) & 7 } // owner bits
if !hasPermBits(mode, bits) → denied
- No boolean accumulator, no tiered if/else, just bitmask OR.
- 6 remaining Server methods (down from 23).
- hasPermBits uses a 4-entry mask array indexed by mode&3 to select
the required permission bits: OREAD→4, OWRITE→2, ORDWR→6, OEXEC→1
- Zero branches, zero error paths — returns bits&mask == mask
- open() uses OR semantics: checks owner, group, world independently;
any tier granting access is sufficient
- checkPerm removed entirely; permission logic is pure boolean math
- checkPerm no longer needs *Server: it takes uid, perm, and inGroup
directly. The caller (open) resolves uid/gid via fileOwnerGroup,
perm via makeStat, and group membership via InGroup.
- checkPerm is now a pure function: (uname, mode, uid, perm, inGroup)
→ returns only on permissions, needs nothing from Server state.
- open is the remaining method that resolves these values.
server.go: 10 remaining Server methods.
Converted walk, create, read, write, stat, clunk, remove, makeStat,
and buildDirData from *Server methods to functions taking *fs.Tree
as first parameter. Exactly the same pattern as fileOwnerGroup.
Also:
- Replaced the switch in handle() with a s.handler map, built once
in New(). All 11 9P message types (Tversion through Tremove) now
dispatch through the map.
- Extracted flush() as a package-level function (was inlined in switch)
- handle() is now: recover + map lookup. No inline cases, no switch.
- server.go: 802 lines, 10 remaining Server methods (down from 23)
- 20 package-level functions handle the actual protocol logic
- readFile was dead code (defined but never called)
- writeFile was a 7-line helper called only once in clunk;
inlined as direct s.rootTree.Open + e.Write
- FileTree type alias now unused after both deletions
fileOwnerGroup only needed s.rootTree, not the full *Server. Making
it a function taking (*fs.Tree, string) removes one more method from
Server, makes the dependency explicit, and makes it unit-testable.