Consolidate cancellation flow documentation

This commit is contained in:
Ollie Agent 2026-08-16 19:24:51 +02:00
parent 97b14dfce3
commit 09f3c8948c
3 changed files with 38 additions and 167 deletions

View File

@ -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.

View File

@ -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`.

View File

@ -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