Add owner-scoped cancel action for saved Context requests
Summary
Adds the shared cancel action to POST /api/context and the MCP context tool so a workspace member can stop a saved Context request before completion with a deterministic persisted outcome. cancelled is a status that runContextRequest, claim_context_request, create_context_execution, claim_context_execution_node and save_context_brief already honored, but nothing ever set it. The operation calls the new service-only cancel_context_request RPC through lib/supabase/context_requests/cancelContextRequest.ts, which validates the receipt (cancelled / already_cancelled / not_cancellable), rejects a receipt whose outcome contradicts the saved status or that leaks claim_token, and processCancelContextRequest.ts rechecks the workspace owner (case-insensitively, since Postgres returns lowercase UUIDs). The action never dispatches a workflow, changes credits, settles provider attempts or withdraws accepted evidence; completed or partial requests keep their saved evidence, and a failed request keeps its saved error. A workflow run that reaches a cancelled request (dispatched before the cancel, raced by an ingest, or retried after losing its claim) ends with the saved state and no longer records a plan, so CONTEXT_RECORD_BLOCKED_PLAN_ENABLED=true does not turn a cancel into a failing run. The MCP OAuth catalog gains one delegated tool (cancel_music_context_request), annotated destructiveHint: true because there is no un-cancel. This advances ticket item "Cancellation, permission changes and retry limits have deterministic persisted outcomes".
Refs recoupable/app#2122 (child of epic recoupable/app#2116).
Merge sequencing
docs recoupable/docs#359 → database recoupable/database#106 (adds public.cancel_context_request; must be applied before this PR is deployed, otherwise cancel returns 409 "Unknown function") → api (this PR). No app/chat PR. Depends on no open PR. Edits to processContextOperation.ts are limited to one schema entry after read and a four-line branch beside read_execution; the logic is in new single-purpose files.
Sibling friction to expect at rebase time:
lib/mcp/oauth/__tests__/fullOAuthCatalog.test.tstool count moves 84 → 85 here; open api#1008 and api#1019 make the same 84 → 85 change and api#1023 sets 86, so whichever merges later must recount (api#996 may also touch it).- Open api#1011 (
ownershipParity.test.ts) iterates everycontextOperationSchemaoption generically; once both merge it will exercisecancelthrough thecancelContextRequestwrapper. - Open api#1001 writes classified failure reasons into
context_requests.error; database#106 now leaves that column untouched on cancel, so those reasons survive.
Verification
- Ran:
pnpm exec vitest run lib/context→ 68 files passed, 8 skipped (opt-in live tests); 336 tests passed, 8 skipped, 0 failed. - Ran:
pnpm exec vitest run app/workflows/context lib/mcp/oauth/__tests__/fullOAuthCatalog.test.ts lib/mcp/oauth/__tests__/contextToolOperations.test.ts lib/mcp/oauth/__tests__/registerFullOAuthTools.test.ts lib/supabase/context_requests→ 19 files, 67 tests passed, 0 failed. - Ran:
pnpm exec vitest run lib/mcp/oauth→ 9 files / 43 tests passed;fullOAuthRouting.test.tscannot load on a fresh checkout because it imports the gitignored build artifactlib/mcp/extension/generated-ui.ts(pre-existing). With a temporary uncommitted stub for that file: 10 files / 46 tests passed. - Ran:
pnpm exec tsc --noEmit→ 199 errors, all pre-existing on origin/main in unrelated__tests__files and the gitignoredgenerated-uiimport; 0 in touched files. - Ran:
pnpm exec eslint --max-warnings=0 <changed .ts files>→ clean;pnpm exec prettier --check <changed files incl. lib/context/README.md>→ clean. - Red → green (initial commit):
cancelOperation.test.tsfailed withZodError: No matching discriminator,cancelContextRequest.test.tswithCannot find module '../cancelContextRequest', and the transports case returned HTTP 400 instead of 200. - Red → green (review fixes): before the fix commit, 3 new assertions failed — uppercase
organization_idrejected with "Context request not found" after the database had committed the cancel;contextWorkflowcalled the plan step for a cancelled request;cancel_music_context_requestreporteddestructiveHint: false. All pass after the fix. - Coverage only: the
spotifyPipeline.test.tscancelledcase passes without this PR too (runContextRequestalready returned early forcancelled); it pins that behavior and was never red. - Not verified: hosted deployment, live providers, production database, the MCP catalog against a hosted OAuth server,
pnpm build:extension.
Out of scope / known gaps
- Module executions:
create_context_execution/claim_context_execution_nodeonly accept completed or partial requests, whichcancelreportsnot_cancellable, so this slice cancels the metadata stage only. Cancelling paid per-module execution, and its reservation/settlement treatment, stays with the paid-execution lane (CE07 #2123/#2105); the documented contract will need revising when per-module execution lands. - Guest adoption: if a member cancels a request that a guest workspace is still completing into,
complete_context_guestreports "Destination request busy" and the guest worker fails and retries (bounded, free provider). Making that a clean terminal outcome needs a change to the guest migration and is left for a follow-up. - Audit: the actor is authorized in storage but not recorded (no
cancelled_by/cancelled_at). - Scheduled dispatch-gap sweep; Chat UI cancel control.
🤖 Generated with Claude Code
Summary by cubic
Adds a cancel action to POST /api/context and the MCP context tool so a workspace member can stop a saved Context request before it completes.
cancelled was already a status the request pipeline honored, but nothing ever set it. The new operation validates the storage receipt, rejects inconsistent or leaked records, and rechecks workspace ownership inside the transaction case-insensitively, so an uppercase organization_id still matches the stored UUID. A cancelled request is never claimed again, and an already-dispatched workflow run reads that status and ends with its saved state without recording a plan. The MCP tool cancel_music_context_request is marked destructive because there is no un-cancel. Cancellation only changes request status: it never dispatches a workflow, refunds credits, settles provider attempts, or withdraws accepted evidence — completed or partial requests keep their saved evidence.
Migration
- Deploy the
cancel_context_requestdatabase function before this PR; otherwise,cancelreturns 409 "Unknown function".