15 KiB
15 KiB
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
-
Race condition: The Tclunk might arrive after the tool has already completed
- This is benign: cancelling a completed context is a no-op
-
Long cleanup: If the tool doesn't respond to SIGKILL quickly, the cancellation may appear to hang briefly
-
Background processes: Background tools (via
proc/new.bg) are NOT cancelled by this mechanism - they run until completion or explicit kill