ollie/virtfs
Ollie Agent 89fbb7438a Stream assistant text incrementally to log/chat; fix stream lost-wakeup
Two fixes for the rendered text views (chat stream, log snapshot):

1. Lost wakeup: blocking stream readers captured the signal channel
   after their data check, so a write landing in that window signaled a
   now-stale channel and the reader blocked until the next event (the
   TUI's last message appeared only after the next prompt). Stream and
   BlockOnce now capture the signal channel before reading; chat.go
   swaps the channel under chatMu via signalChatLocked and RawLogStream
   captures it while holding the lock.

2. No streaming in TUI: textLog only received finalized blocks, so the
   chat text stream showed nothing until a block completed. SetPartial
   now appends the assistant partial's new suffix to textLog
   incrementally (tracking textPartialLen); AppendBlock appends only the
   trailing separator on finalization to avoid duplicating the body.
2026-10-10 16:25:14 +02:00
..
LICENSE.md rename fsedsl → virtfs 2026-08-11 17:21:44 +02:00
README.md virtfs: rewrite README for closure-capture model 2026-08-11 17:22:39 +02:00
builder.go implement namespace-bounded capability model 2026-10-06 17:41:27 +02:00
decl.go Stream assistant text incrementally to log/chat; fix stream lost-wakeup 2026-10-10 16:25:14 +02:00
decl_test.go refactor: remove dead code from embedding/virtfs/skills (-108 lines) 2026-08-21 17:20:13 +02:00
file.go server: reduce statewait timeout from 5s to 500ms 2026-10-06 11:32:31 +02:00
go.mod rename fsedsl → virtfs 2026-08-11 17:21:44 +02:00
help.go rename fsedsl → virtfs 2026-08-11 17:21:44 +02:00
tree.go refactor: remove dead code from embedding/virtfs/skills (-108 lines) 2026-08-21 17:20:13 +02:00
tree_test.go refactor: remove dead code from embedding/virtfs/skills (-108 lines) 2026-08-21 17:20:13 +02:00

README.md

virtfs

A Go library for declaring synthetic filesystems. Define your namespace structure statically, populate it dynamically at runtime with closures.

Produces a *Tree usable with any filesystem protocol (9P, FUSE, etc.) or as an in-memory filesystem for testing.

Design

Handlers are plain closures that capture their state at binding time. There is no context type parameter — each handler already knows its target via closure capture.

Dynamic subtrees use Each: a bindings function returns []Binding, where each binding carries pre-bound Children (fully-wired subtrees with their own closures).

Quick Start

package main

import (
    "fmt"
    "ollie/virtfs"
)

type User struct {
    ID   string
    Name string
}

var users = []*User{
    {ID: "1", Name: "alice"},
    {ID: "2", Name: "bob"},
}

func main() {
    spec := virtfs.DirNode("/",
        virtfs.FileNode("version", 0444,
            virtfs.Doc("API version"),
            virtfs.Read(func() ([]byte, error) {
                return []byte("1.0\n"), nil
            }),
        ),
        virtfs.Each("{user}", func() ([]virtfs.Binding, error) {
            var out []virtfs.Binding
            for _, u := range users {
                user := u // capture
                out = append(out, virtfs.Binding{
                    Name:    user.Name,
                    Aliases: []string{user.ID},
                    Children: []virtfs.FsNodeDecl{
                        virtfs.FileNode("id", 0444,
                            virtfs.Read(func() ([]byte, error) {
                                return []byte(user.ID + "\n"), nil
                            }),
                        ),
                        virtfs.FileNode("name", 0444,
                            virtfs.Read(func() ([]byte, error) {
                                return []byte(user.Name + "\n"), nil
                            }),
                        ),
                    },
                })
            }
            return out, nil
        }),
    )

    tree := virtfs.BuildTree(spec)

    entries, _ := tree.List()
    for _, e := range entries {
        fmt.Println(e.Name())
    }

    f, _ := tree.Open("alice/name")
    data, _ := f.Read()
    fmt.Printf("alice/name: %s", data)
}

API

Constructors

Function Description
DirNode(name, children/options...) Static directory
FileNode(name, mode, options...) File with handlers
Each(name, bindingsFn, options...) Dynamic directory — children come from bindings

Options (NodeOption)

Option Signature Description
Read(fn) func() ([]byte, error) Non-blocking read
Write(fn) func([]byte) error Write handler
Stream(fn) func(context.Context, string) ([]byte, string, error) Blocking streaming read
BlockOnce(fn) func(context.Context, string) ([]byte, string, error) Blocking read, returns once
Request(fn) func([]byte) ([]byte, error) Write-then-read (rdwr)
Doc(desc) string Human-readable description
UID(uid) string Set owner
GID(gid) string Set group
StatOverride(fn) func() os.FileInfo Custom stat
RemoveNode(fn) func() error Removal handler

Binding

Returned by the Each bindings function. Each binding represents one dynamic child.

virtfs.Binding{
    Name:     "alice",
    Aliases:  []string{"user-id-123"},
    Children: []virtfs.FsNodeDecl{ ... }, // pre-bound subtree
    Remove:   func() { ... },             // optional
    Rename:   func(newName string) error { ... }, // optional
}

Children are fully-wired — their handlers are closures that already captured the specific instance (no applier/context needed).

Tree Operations

tree := virtfs.BuildTree(spec)

tree.List()             // list root entries
tree.Stat(name)         // stat a path
tree.Open(name)         // open file for read/write
tree.Create(name)       // create file
tree.Delete(name)       // remove file
tree.Rename(old, new)   // rename file
tree.Readdir(path)      // list subdirectory

Help Generation

help := virtfs.GenerateHelp(spec)
// Produces formatted docs from Doc() annotations

License

GPLv3