diff --git a/cmd/olliesrv/internal/agent/chat.go b/cmd/olliesrv/internal/agent/chat.go index d0e9d3c..bcdd0b2 100644 --- a/cmd/olliesrv/internal/agent/chat.go +++ b/cmd/olliesrv/internal/agent/chat.go @@ -7,6 +7,7 @@ package agent import ( + "bytes" "fmt" "sync" "sync/atomic" @@ -99,6 +100,43 @@ func (ag *Agent) ChatRead(base string) ([]byte, string, error) { return data, fmt.Sprintf("%d", newOffset), nil } +// ChatSearchByID returns the block content for the given block ID. +// Returns the block text (without markers) and true if found, empty and false otherwise. +func (ag *Agent) ChatSearchByID(blockID string) ([]byte, bool) { + if blockID == "" { + return nil, false + } + ag.chatMu.RLock() + log := ag.chatLog + ag.chatMu.RUnlock() + + // Parse blocks looking for the target ID + // Format: [[[role:name#id]]] ... [[[end]]] + target := "#" + blockID + "]]]" + idx := bytes.Index(log, []byte(target)) + if idx < 0 { + return nil, false + } + + // Find the start of this block header + headerStart := bytes.LastIndex(log[:idx], []byte("[[[")) + if headerStart < 0 { + return nil, false + } + + // Find [[[end]]] after the header + endMarker := []byte("[[[end]]]") + endIdx := bytes.Index(log[idx:], endMarker) + if endIdx < 0 { + return nil, false + } + blockEnd := idx + endIdx + len(endMarker) + + // Extract block including header and end marker + block := log[headerStart:blockEnd] + return block, true +} + // --- Plan methods --- // Plan returns a copy of the plan. diff --git a/cmd/olliesrv/internal/fs/spec.go b/cmd/olliesrv/internal/fs/spec.go index c044b60..9d512f2 100644 --- a/cmd/olliesrv/internal/fs/spec.go +++ b/cmd/olliesrv/internal/fs/spec.go @@ -851,8 +851,19 @@ func buildAgentChildren(a *agent.Agent, s *session.Session) []virtfs.FsNodeDecl return data, nil }), virtfs.Stream(a.ChatRead, a.ChatSignal), - ), - virtfs.FileNode("state", 0444, // world-readable (non-sensitive) + ), + virtfs.FileNode("chat.search", 0660, // rdwr - write blockId, read block + virtfs.Doc("Search chat by block ID. Write ID, read block content."), + virtfs.Rdwr(func(_ context.Context, data []byte) ([]byte, error) { + blockID := strings.TrimSpace(string(data)) + block, found := a.ChatSearchByID(blockID) + if !found { + return nil, fmt.Errorf("block not found: %s", blockID) + } + return block, nil + }), + ), + virtfs.FileNode("state", 0444, // world-readable (non-sensitive) virtfs.Doc("Current agent state (idle, calling, thinking, paused)"), virtfs.Read(func() ([]byte, error) { return []byte(a.State() + "\n"), nil