Design: adapter stop() method and interrupt vs cancel semantics #4

Closed
opened 2026-04-16 02:17:50 +00:00 by cpfarhood · 0 comments
cpfarhood commented 2026-04-16 02:17:50 +00:00 (Migrated from github.com)

Problem

When a user cancels an agent run in Paperclip, the running adapter needs to stop its work. For local adapters (Claude Code, Codex) that spawn child processes, Paperclip uses signalRunningProcess() with SIGTERM/SIGKILL. For K8s-based adapters (paperclip-adapter-claude-k8s, paperclip-adapter-opencode-k8s), the work runs as Kubernetes Jobs outside the Node.js process.

Additionally, "Interrupt" is like Claude Code's Escape key - it injects context into the LLM's thinking rather than stopping work. This is not the same as cancel/stop.

Current Understanding

Methods on ServerAdapterModule

  1. stop(runId: string): Promise - REQUIRED on all adapters

    • Called when user wants to cancel/stop a run
    • Must handle both queued runs (before they start) and active runs (graceful then hard kill)
    • For K8s: delete the Job; for local: send SIGTERM then SIGKILL
    • Idempotent: safe to call multiple times for the same runId
    • Return void - Paperclip core treats exceptions as unexpected errors and logs them
  2. supportsInterrupt?: boolean - OPTIONAL capability flag

    • True only for adapters that support mid-execution context injection (Escape key behavior)
    • Local adapters (Claude Code, Codex) that have a live LLM conversation can handle this
    • K8s adapters cannot support interrupt (containers run to completion, no injection mechanism)
    • UI checks this to conditionally render the interrupt button

Interrupt vs Cancel

  • Interrupt (Escape key): Inject something into the LLM's thinking to redirect the agent. Not all adapters support this. K8s adapters: false.
  • Cancel/Stop: Stop the job. All adapters must implement this. K8s adapters delete the Job.

Open Questions

  1. What does stop() do for Hermes? Hermes is a long-running agent. Does stop() signal the agent to stop tracking the run? Send SIGINT? Something else?
  2. K8s graceful vs hard kill: K8s Jobs don't have the same graceful/hard kill semantics as local processes. Should stop() just delete the Job immediately, or is there a grace period concept?
  3. stop() return type: Should stop() return a result object so Paperclip knows if the run was actually stopped vs unknown runId?
  4. interrupt for local adapters: Is interrupt actually a separate method on the adapter, or does signalRunningProcess() handle both interrupt and stop?

Related

  • ServerAdapterModule interface in packages/adapter-utils/src/types.ts
  • signalRunningProcess() in packages/adapter-utils/src/server-utils.ts
  • Relevant for: opencode_k8s, paperclip-adapter-claude-k8s, Hermes adapter

Next Steps

  • Define stop() semantics more precisely for K8s adapters
  • Determine Hermes stop() behavior
  • Decide on return type (void vs result object)
## Problem When a user cancels an agent run in Paperclip, the running adapter needs to stop its work. For local adapters (Claude Code, Codex) that spawn child processes, Paperclip uses signalRunningProcess() with SIGTERM/SIGKILL. For K8s-based adapters (paperclip-adapter-claude-k8s, paperclip-adapter-opencode-k8s), the work runs as Kubernetes Jobs outside the Node.js process. Additionally, "Interrupt" is like Claude Code's Escape key - it injects context into the LLM's thinking rather than stopping work. This is not the same as cancel/stop. ## Current Understanding ### Methods on ServerAdapterModule 1. stop(runId: string): Promise<void> - REQUIRED on all adapters - Called when user wants to cancel/stop a run - Must handle both queued runs (before they start) and active runs (graceful then hard kill) - For K8s: delete the Job; for local: send SIGTERM then SIGKILL - Idempotent: safe to call multiple times for the same runId - Return void - Paperclip core treats exceptions as unexpected errors and logs them 2. supportsInterrupt?: boolean - OPTIONAL capability flag - True only for adapters that support mid-execution context injection (Escape key behavior) - Local adapters (Claude Code, Codex) that have a live LLM conversation can handle this - K8s adapters cannot support interrupt (containers run to completion, no injection mechanism) - UI checks this to conditionally render the interrupt button ### Interrupt vs Cancel - Interrupt (Escape key): Inject something into the LLM's thinking to redirect the agent. Not all adapters support this. K8s adapters: false. - Cancel/Stop: Stop the job. All adapters must implement this. K8s adapters delete the Job. ## Open Questions 1. What does stop() do for Hermes? Hermes is a long-running agent. Does stop() signal the agent to stop tracking the run? Send SIGINT? Something else? 2. K8s graceful vs hard kill: K8s Jobs don't have the same graceful/hard kill semantics as local processes. Should stop() just delete the Job immediately, or is there a grace period concept? 3. stop() return type: Should stop() return a result object so Paperclip knows if the run was actually stopped vs unknown runId? 4. interrupt for local adapters: Is interrupt actually a separate method on the adapter, or does signalRunningProcess() handle both interrupt and stop? ## Related - ServerAdapterModule interface in packages/adapter-utils/src/types.ts - signalRunningProcess() in packages/adapter-utils/src/server-utils.ts - Relevant for: opencode_k8s, paperclip-adapter-claude-k8s, Hermes adapter ## Next Steps - Define stop() semantics more precisely for K8s adapters - Determine Hermes stop() behavior - Decide on return type (void vs result object)
Sign in to join this conversation.