ollie/docs/context-cancellation-flow.md

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

  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

  3. Background processes: Background tools (via proc/new.bg) are NOT cancelled by this mechanism - they run until completion or explicit kill