From 09f3c8948cf8df63db0fcb593547fed88d255e17 Mon Sep 17 00:00:00 2001 From: Ollie Agent Date: Sun, 16 Aug 2026 19:24:51 +0200 Subject: [PATCH] Consolidate cancellation flow documentation --- doc/TODO.md | 9 -- doc/architecture.md | 38 ++++++++ doc/context-cancellation-flow.md | 158 ------------------------------- 3 files changed, 38 insertions(+), 167 deletions(-) delete mode 100644 doc/TODO.md delete mode 100644 doc/context-cancellation-flow.md diff --git a/doc/TODO.md b/doc/TODO.md deleted file mode 100644 index 7b7dc3e..0000000 --- a/doc/TODO.md +++ /dev/null @@ -1,9 +0,0 @@ -# TODO - -## Future Improvements - -### Proc Ownership Filtering -Currently all agents in a session share the same toolsrv proc namespace. If multi-agent sessions become common, consider adding agent ID filtering to `ListProcs()` so agents only see their own processes. - -### Proc GC Tuning -Background proc GC currently uses a fixed 10-minute TTL after output is read. Consider making this configurable or adding explicit dismiss-on-read semantics for certain use cases. diff --git a/doc/architecture.md b/doc/architecture.md index 5f4a70f..c550799 100644 --- a/doc/architecture.md +++ b/doc/architecture.md @@ -118,3 +118,41 @@ The 9P filesystem is the runtime boundary. `fs.NewRoot` installs the session pac ### Session restoration `fs.RestoreSessions()` reads JSON files from the active-session directory through `session.RestoreAllSessions()`. Restoration runs concurrently per file. Invalid files are logged and removed. Paused sessions rebuild minimal agents without a tool server or backend connection; active sessions rebuild their agents and connections. Restored agents are replayed into the live filesystem and wired to agent events after the listener is ready. + +## Context cancellation + +Cancellation propagates from an agent interrupt through the active turn, the 9P tool call, and the tool process: + +```mermaid +sequenceDiagram + participant U as Client + participant A as Agent + participant C as toolsrv client + participant S as toolsrv 9P server + participant P as Tool process + + U->>A: write stop to agent ctl + A->>A: cancel active action with cause + A->>C: execute tool with turn context + C->>C: close proc fid when ctx.Done() + C->>S: Tclunk + S->>S: cancel request context + S->>P: CommandContext terminates process + P-->>A: cancellation error + A-->>U: cancelled turn state/result +``` + +The daemon owns a root context. Each session derives its own context; each agent turn derives a cancellable action context with `context.WithCancelCause`. The turn context is passed to backend calls and tool execution. An interrupt cancels the active action without cancelling the entire session. + +The toolsrv client watches the call context while reading the blocking 9P result. Cancellation closes the request fid, which sends `Tclunk` to toolsrv. The request entry's close function cancels the server-side request context. Tool execution uses `exec.CommandContext`, so the child process is terminated when that context is cancelled. The agent loop checks cancellation before tool execution and returns a cancelled result instead of starting another tool. + +Daemon shutdown cancels the root context, which propagates through sessions, agents, tool clients, and server-side requests. Closing listeners and killing the 9P server completes transport shutdown; the daemon then removes its Unix socket and flushes logs. + +Relevant implementation points: + +- `cmd/olliesrv/internal/agent/agent.go` — interrupt cancels the active action. +- `cmd/olliesrv/internal/agent/turn.go` — creates the action context for each turn. +- `cmd/olliesrv/internal/agent/loop.go` — checks cancellation before executing tools. +- `cmd/olliesrv/internal/toolclient/spawn.go` — runs local and remote toolsrv processes with derived contexts. +- `cmd/toolsrv/internal/server/proc.go` — binds request cancellation to tool execution. +- `cmd/toolsrv/internal/exec/exec.go` — executes tools with `exec.CommandContext`. \ No newline at end of file diff --git a/doc/context-cancellation-flow.md b/doc/context-cancellation-flow.md deleted file mode 100644 index 4801fc1..0000000 --- a/doc/context-cancellation-flow.md +++ /dev/null @@ -1,158 +0,0 @@ -# Context Cancellation Flow - -This document traces context cancellation from user interrupt to tool process termination. - -## Overview - -``` -┌─────────────────────────────────────────────────────────────────────────────┐ -│ OLLIESRV │ -│ │ -│ User sends "stop" to agent ctl │ -│ │ │ -│ ▼ │ -│ ┌─────────────────┐ │ -│ │ Agent.Interrupt │ Loads currentAction (actionHandle) │ -│ │ (cause) │ Calls h.cancel(cause) │ -│ └────────┬────────┘ │ -│ │ │ -│ ▼ │ -│ ┌─────────────────────────────────────────┐ │ -│ │ actCtx (context.WithCancelCause) │ Created in Agent.Run() │ -│ │ │ at turn start (turn.go:95) │ -│ └────────┬────────────────────────────────┘ │ -│ │ │ -│ ▼ │ -│ ┌─────────────────┐ │ -│ │ TurnCtx (embeds │ Passed to loop.go, execOne(), etc. │ -│ │ context.Context)│ │ -│ └────────┬────────┘ │ -│ │ │ -│ ▼ │ -│ ┌─────────────────────────────────────────┐ │ -│ │ execOne() checks ctx.Err() │ loop.go:540 │ -│ │ If cancelled → returns immediately │ │ -│ │ with status:"cancelled" │ │ -│ └────────┬────────────────────────────────┘ │ -│ │ │ -│ │ If not cancelled, calls rt.Exec(ctx, name, args) │ -│ │ which wraps srv.CallTool(ctx, ...) (runtime.go:181) │ -│ ▼ │ -└───────────┼──────────────────────────────────────────────────────────────────┘ - │ - │ UNIX SOCKET (9P PROTOCOL) - │ -┌───────────┼──────────────────────────────────────────────────────────────────┐ -│ ▼ │ -│ ┌─────────────────────────────────────────────────────────────────┐ │ -│ │ toolsrv/client9p.go: CallTool() │ │ -│ │ │ │ -│ │ 1. Opens "proc/new" file (9P fid) │ │ -│ │ │ │ -│ │ 2. Starts goroutine watching ctx.Done(): │ │ -│ │ go func() { │ │ -│ │ select { │ │ -│ │ case <-ctx.Done(): │ │ -│ │ fid.Close() // ← Force close to unblock read │ │ -│ │ case <-done: │ │ -│ │ } │ │ -│ │ }() │ │ -│ │ │ │ -│ │ 3. Writes payload to fid (Twrite) │ │ -│ │ 4. Reads result from fid (Tread) — BLOCKS │ │ -│ │ 5. If read fails AND ctx.Err() != nil → return ctx.Err() │ │ -│ └────────┬────────────────────────────────────────────────────────┘ │ -│ │ │ -│ │ fid.Close() sends Tclunk to toolsrv │ -│ ▼ │ -└───────────┼──────────────────────────────────────────────────────────────────┘ - │ -┌───────────┼──────────────────────────────────────────────────────────────────┐ -│ │ TOOLSRV │ -│ ▼ │ -│ ┌─────────────────────────────────────────────────────────────────┐ │ -│ │ p9.go: handleClunk() receives Tclunk │ │ -│ │ │ │ -│ │ 1. Removes fid from map │ │ -│ │ 2. Calls f.entry.Close() (p9.go:550) │ │ -│ └────────┬────────────────────────────────────────────────────────┘ │ -│ │ │ -│ ▼ │ -│ ┌─────────────────────────────────────────────────────────────────┐ │ -│ │ virtfs/builder.go: closeFn() for Request mode │ │ -│ │ │ │ -│ │ closeFn := func() error { │ │ -│ │ if reqCancel != nil { │ │ -│ │ reqCancel() // ← Cancels the request context │ │ -│ │ } │ │ -│ │ return nil │ │ -│ │ } │ │ -│ │ │ │ -│ │ This cancels reqCtx created in writeFn (builder.go:274) │ │ -│ └────────┬────────────────────────────────────────────────────────┘ │ -│ │ │ -│ ▼ │ -│ ┌─────────────────────────────────────────────────────────────────┐ │ -│ │ server/proc.go: HandleProcNew() receives cancelled context │ │ -│ │ │ │ -│ │ The Request handler was called with reqCtx: │ │ -│ │ virtfs.Request(func(ctx context.Context, data []byte) ...) │ │ -│ │ │ │ -│ │ When reqCancel() is called, ctx.Done() becomes readable │ │ -│ └────────┬────────────────────────────────────────────────────────┘ │ -│ │ │ -│ ▼ │ -│ ┌─────────────────────────────────────────────────────────────────┐ │ -│ │ exec/exec.go: ExecuteTool() sees context cancellation │ │ -│ │ │ │ -│ │ The exec code should check ctx.Done() and: │ │ -│ │ 1. Send SIGKILL to the child process │ │ -│ │ 2. Return error to caller │ │ -│ └─────────────────────────────────────────────────────────────────┘ │ -│ │ -└──────────────────────────────────────────────────────────────────────────────┘ -``` - -## Key Files - -| Location | Responsibility | -|----------|----------------| -| `cmd/olliesrv/internal/agent/agent.go:308` | `Interrupt()` calls `h.cancel(cause)` | -| `cmd/olliesrv/internal/agent/turn.go:95` | Creates `actCtx` with `WithCancelCause` | -| `cmd/olliesrv/internal/agent/loop.go:540` | `execOne()` checks `ctx.Err()` early | -| `cmd/olliesrv/internal/agent/runtime.go:181` | `rt.Exec` calls `srv.CallTool(ctx, ...)` | -| `toolsrv/client9p.go:140-155` | `CallTool()` goroutine closes fid on ctx.Done() | -| `cmd/toolsrv/p9.go:549-551` | `handleClunk()` calls `f.entry.Close()` | -| `virtfs/builder.go:282-286` | `closeFn` cancels `reqCtx` | -| `cmd/toolsrv/internal/exec/exec.go` | Should kill process on context cancellation | - -## Sequence Diagram - -``` -User Agent CallTool 9P Server virtfs exec - │ │ │ │ │ │ - │──"stop"────▶│ │ │ │ │ - │ │ │ │ │ │ - │ │──cancel()─────▶│ │ │ │ - │ │ │ │ │ │ - │ │ │──fid.Close()──▶│ │ │ - │ │ │ (Tclunk) │ │ │ - │ │ │ │ │ │ - │ │ │ │──Close()─────▶│ │ - │ │ │ │ │ │ - │ │ │ │ │──reqCancel()─▶│ - │ │ │ │ │ │ - │ │ │ │ │ │──SIGKILL──▶[process] - │ │ │ │ │ │ - │ │◀───ctx.Err()───│ │ │ │ - │ │ │ │ │ │ - │◀──cancelled─│ │ │ │ │ -``` - -## Potential Issues - -1. **Race condition**: The Tclunk might arrive after the tool has already completed - - This is benign: cancelling a completed context is a no-op - -2. **Long cleanup**: If the tool doesn't respond to SIGKILL quickly, the cancellation - may appear to hang briefly