Skip to content

Commit 6f82c68

Browse files
xuiocodex
andcommitted
Add native Codex session cancellation
Co-Authored-By: OpenAI Codex <noreply@openai.com>
1 parent 8c9ee98 commit 6f82c68

7 files changed

Lines changed: 288 additions & 43 deletions

File tree

README.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -88,9 +88,9 @@ Start a long-running Codex session on this repo, then let me send follow-up prom
8888
```
8989

9090
Claude should use `codex_task` for the initial prompt, preserve the returned
91-
`session_id`, and use `codex_followup` to continue, steer, or wait on that same
92-
Codex context. For a completed first turn, Claude should set `keep_session: true`;
93-
for long first turns, Claude should set `background: true`.
91+
`session_id`, and use `codex_followup` to continue, steer, wait on, or cancel
92+
that same Codex context. For a completed first turn, Claude should set
93+
`keep_session: true`; for long first turns, Claude should set `background: true`.
9494

9595
## Safety Model
9696

@@ -138,6 +138,7 @@ writes and DNS/network remain disabled unless `full_access: true` is set.
138138
| Persistent context | `codex_task` with `keep_session: true`, then `codex_followup` |
139139
| Long-running sessions | `codex_task` with `background: true`, then `codex_followup` |
140140
| Live steering | `codex_followup` with `mode: "steer"` |
141+
| Stop running work | `codex_followup` with `mode: "cancel"` |
141142
| Diagnostics | MCP resources `codex://status`, `codex://doctor`, `codex://usage` |
142143

143144
Legacy tools such as `ask_codex`, `run_agent`, `run_agents`, `start_session`, and

dist/index.js

Lines changed: 78 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -25162,18 +25162,18 @@ var CodexSessionManager = class {
2516225162
timeoutReason: "wait_timeout"
2516325163
};
2516425164
}
25165-
cancel(id) {
25165+
cancel(id, reason) {
2516625166
const session = this.sessions.get(id);
2516725167
if (!session) {
2516825168
logger.warn("session.cancel_unknown", { sessionId: id });
2516925169
return void 0;
2517025170
}
25171-
logger.warn("session.cancel", { sessionId: id, active: Boolean(session.controller) });
25171+
logger.warn("session.cancel", { sessionId: id, active: Boolean(session.controller), reason });
2517225172
session.cancelRequested = true;
2517325173
for (const turn of session.queuedTurns) {
2517425174
turn.status = "cancelled";
2517525175
turn.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
25176-
turn.error = "Session was cancelled before this turn started.";
25176+
turn.error = reason ? `Session cancelled: ${reason}` : "Session was cancelled before this turn started.";
2517725177
this.notifyTurn(turn);
2517825178
}
2517925179
session.queuedTurns = [];
@@ -25736,8 +25736,8 @@ var usageGuide = [
2573625736
"Tool choice:",
2573725737
"- Use codex_task for one delegated Codex task. It is the native Claude-like front door: description plus prompt, read-only by default, and answer-first result. Call codex_task multiple times in one assistant turn when independent investigations can run in parallel.",
2573825738
"- Use codex_task_group when the work can be split into independent concurrent tasks and Claude wants one combined response with rolled-up per-task findings.",
25739-
"- Use codex_followup when Claude already has a session_id from codex_task or codex_task_group and wants to continue, steer, or wait on that same Codex context.",
25740-
"- Set codex_task background true for long-running work so Claude gets a session_id immediately, then use codex_followup mode wait or steer.",
25739+
"- Use codex_followup when Claude already has a session_id from codex_task or codex_task_group and wants to continue, steer, wait on, or cancel that same Codex context.",
25740+
"- Set codex_task background true for long-running work so Claude gets a session_id immediately, then use codex_followup mode wait, steer, or cancel.",
2574125741
"- Diagnostics are resources by default: read codex://status, codex://doctor, or codex://usage when a prior call failed or availability is uncertain.",
2574225742
"- Debug tools such as codex_status, codex_doctor, codex_usage_guide, codex_choose_tool, and codex_export_debug_bundle are hidden unless CODEX_SUBAGENTS_ENABLE_DEBUG_TOOLS=1.",
2574325743
"- Legacy/manual tools such as ask_codex, run_agent, run_agents, and old session names are hidden unless CODEX_SUBAGENTS_ENABLE_LEGACY_TOOLS=1.",
@@ -25748,6 +25748,7 @@ var usageGuide = [
2574825748
"- If the user explicitly asks for non-sandbox/full local capabilities, set full_access true. This maps to Codex's --dangerously-bypass-approvals-and-sandbox flag and allows DNS/network plus unrestricted file and git writes.",
2574925749
"- Approvals are non-interactive; do not expect Codex to ask permission.",
2575025750
'- If codex_followup mode wait returns completed false with timeoutReason "wait_timeout", the session is still running unless its status says otherwise.',
25751+
"- Use codex_followup mode cancel to stop a background or actively running Codex session early. The response includes whatever partial output streamed before the interrupt.",
2575125752
'- If a tool returns error.kind "backpressure", reduce max_parallel or wait before retrying. codex://status exposes current queue/session limits.',
2575225753
"- If a response mentions outputArtifacts, use the artifact paths for full retained output instead of asking Codex to resend huge stdout/stderr.",
2575325754
'- Do not use model_preset "spark" by default. Use Spark only when the user asks for Spark or when a quick focused sidecar check is clearly more appropriate than the default Codex model.',
@@ -26336,17 +26337,22 @@ function nativeTaskPrompt(args) {
2633626337

2633726338
${args.prompt}`;
2633826339
}
26339-
function sessionProgressPayload(session, preferredResult) {
26340-
if (!session || typeof session !== "object") return {};
26340+
function sessionPartialMessage(session, preferredResult) {
26341+
if (!session || typeof session !== "object") return void 0;
2634126342
const value = session;
2634226343
const partial2 = value.partial && typeof value.partial === "object" ? value.partial : void 0;
2634326344
const lastResult = value.lastResult && typeof value.lastResult === "object" ? value.lastResult : void 0;
2634426345
const preferred = preferredResult && typeof preferredResult === "object" ? preferredResult : void 0;
26346+
return typeof partial2?.lastAgentMessage === "string" ? partial2.lastAgentMessage : typeof preferred?.finalMessage === "string" ? preferred.finalMessage : typeof lastResult?.finalMessage === "string" ? lastResult.finalMessage : void 0;
26347+
}
26348+
function sessionProgressPayload(session, preferredResult) {
26349+
if (!session || typeof session !== "object") return {};
26350+
const value = session;
26351+
const partialResult = sessionPartialMessage(session, preferredResult);
2634526352
const activeTurn = value.activeTurn && typeof value.activeTurn === "object" ? value.activeTurn : void 0;
2634626353
const updatedAt = typeof value.updatedAt === "string" ? Date.parse(value.updatedAt) : NaN;
2634726354
const createdAt = typeof activeTurn?.createdAt === "string" ? Date.parse(activeTurn.createdAt) : NaN;
2634826355
const elapsedBase = Number.isFinite(createdAt) ? createdAt : updatedAt;
26349-
const partialResult = typeof partial2?.lastAgentMessage === "string" ? partial2.lastAgentMessage : typeof preferred?.finalMessage === "string" ? preferred.finalMessage : typeof lastResult?.finalMessage === "string" ? lastResult.finalMessage : void 0;
2635026356
return {
2635126357
partial_result: partialResult,
2635226358
last_event: typeof activeTurn?.status === "string" ? `${activeTurn.kind ?? "turn"}:${activeTurn.status}` : typeof value.status === "string" ? value.status : void 0,
@@ -27005,7 +27011,7 @@ registerTool(
2700527011
result: `Codex task started in the background. Session: ${session2.id}`,
2700627012
session_id: session2.id,
2700727013
turn,
27008-
hint: "Use codex_followup mode wait or steer with this session_id."
27014+
hint: "Use codex_followup mode wait, steer, or cancel with this session_id."
2700927015
};
2701027016
if (args.advanced?.include_diagnostics) {
2701127017
payload.diagnostics = {
@@ -27227,7 +27233,7 @@ var nativeTaskGroupTaskSchema = external_exports.object({
2722727233
keep_session: external_exports.boolean().default(false).describe("Return this task's session_id after completion so Claude can follow up. Leave false for native Task-like one-shot work."),
2722827234
...nativeBaseInputSchema
2722927235
});
27230-
var followupModeSchema = external_exports.enum(["queue", "steer", "wait"]);
27236+
var followupModeSchema = external_exports.enum(["queue", "steer", "wait", "cancel"]);
2723127237
registerTool(
2723227238
"codex_task_group",
2723327239
{
@@ -27297,12 +27303,13 @@ registerTool(
2729727303
"codex_followup",
2729827304
{
2729927305
title: "Followup",
27300-
description: "Continue, steer, or poll a Codex session from a prior background or keep_session task. Use queue for another prompt in the same context, steer to redirect active work, and wait to check whether the current work has finished.",
27306+
description: "Continue, steer, poll, or cancel a Codex session from a prior background or keep_session task. Use queue for another prompt, steer to redirect active work, wait to check completion, and cancel to stop running work.",
2730127307
inputSchema: {
2730227308
session_id: external_exports.string().trim().min(1).describe("session_id returned by codex_task or codex_task_group."),
27303-
prompt: external_exports.string().min(1).optional().describe("Follow-up or steering prompt. Required for mode queue and mode steer; omit for mode wait."),
27309+
prompt: external_exports.string().min(1).optional().describe("Follow-up or steering prompt. Required for mode queue and mode steer; omit for mode wait or cancel."),
2730427310
description: external_exports.string().trim().min(1).optional().describe("Optional short label for this follow-up turn."),
27305-
mode: followupModeSchema.default("queue").describe("queue continues the Codex context, steer redirects active work, wait collects an existing result."),
27311+
reason: external_exports.string().trim().min(1).max(500).optional().describe("Optional reason for mode cancel; logged and echoed in the response."),
27312+
mode: followupModeSchema.default("queue").describe("queue continues the Codex context, steer redirects active work, wait collects an existing result, cancel stops running work."),
2730627313
interrupt_current: external_exports.boolean().default(false).describe("For mode steer, cancel the active Codex turn and run this steering prompt next. Leave false unless the user explicitly wants interruption."),
2730727314
background: external_exports.boolean().default(false).describe("Return after queueing or steering instead of waiting for the Codex turn to finish."),
2730827315
turn_id: external_exports.string().trim().min(1).optional().describe("For mode wait, optionally wait for one specific turn."),
@@ -27316,7 +27323,7 @@ registerTool(
2731627323
const mode = args.mode ?? "queue";
2731727324
const prompt = args.prompt?.trim();
2731827325
try {
27319-
if (mode !== "wait" && !prompt) {
27326+
if (mode !== "wait" && mode !== "cancel" && !prompt) {
2732027327
return nativeErrorResult(new Error(`codex_followup mode ${mode} requires prompt.`), "codex_followup");
2732127328
}
2732227329
if (mode === "wait") {
@@ -27364,6 +27371,62 @@ registerTool(
2736427371
}
2736527372
return nativeTextResult(payload2, waited.timeoutReason === "wait_cancelled");
2736627373
}
27374+
if (mode === "cancel") {
27375+
await progress.send(`Cancelling Codex session ${args.session_id}`);
27376+
const sessionBefore = sessionManager.get(args.session_id);
27377+
if (!sessionBefore) {
27378+
await progress.flush();
27379+
return nativeErrorResult(new Error(`Unknown session_id: ${args.session_id}`), "codex_followup");
27380+
}
27381+
const wasActive = sessionBefore.active;
27382+
const activeTurn = sessionBefore.activeTurn;
27383+
const lastResult = sessionBefore.lastResult;
27384+
if (!wasActive && sessionBefore.queuedTurns.length === 0 && lastResult?.status === "completed") {
27385+
await progress.flush();
27386+
const compactSession3 = compactSessionSnapshotForMcp(sessionBefore);
27387+
const compactResult = compactAgentResultForMcp(lastResult);
27388+
const resultValue = compactResult.structuredOutput ?? compactResult.finalMessage;
27389+
const resultText = stringifyResultValue(resultValue, compactResult.finalMessage);
27390+
return nativeTextResult({
27391+
ok: true,
27392+
status: "already_completed",
27393+
cancelled: false,
27394+
was_active: false,
27395+
summary: "Codex session had already completed.",
27396+
result: resultText || "Codex session had already completed.",
27397+
session_id: args.session_id,
27398+
elapsed_ms: sessionProgressPayload(compactSession3, compactResult).elapsed_ms,
27399+
diagnostics: args.advanced?.include_diagnostics ? { session: compactSession3, result: compactResult } : void 0,
27400+
hint: "The session had already completed. Start a new codex_task if more work is needed."
27401+
});
27402+
}
27403+
const cancelled = sessionManager.cancel(args.session_id, args.reason);
27404+
await progress.flush();
27405+
if (!cancelled) {
27406+
return nativeErrorResult(new Error(`Unknown session_id: ${args.session_id}`), "codex_followup");
27407+
}
27408+
const compactSession2 = compactSessionSnapshotForMcp(cancelled);
27409+
const partialMessage = sessionPartialMessage(compactSession2);
27410+
const activeTurnStartedMs = activeTurn?.createdAt ? Date.parse(activeTurn.createdAt) : NaN;
27411+
const elapsedMs = Number.isFinite(activeTurnStartedMs) ? Math.max(0, Date.now() - activeTurnStartedMs) : sessionProgressPayload(compactSession2).elapsed_ms;
27412+
return nativeTextResult({
27413+
ok: true,
27414+
status: "cancelled",
27415+
cancelled: true,
27416+
was_active: wasActive,
27417+
reason: args.reason,
27418+
summary: wasActive ? `Codex session cancelled${typeof elapsedMs === "number" ? ` after ${(elapsedMs / 1e3).toFixed(1)}s` : ""}.` : "Codex session marked cancelled (was idle).",
27419+
result: partialMessage || (wasActive ? "Codex was cancelled mid-turn; no partial output was captured." : "Codex session was already idle when cancelled."),
27420+
session_id: args.session_id,
27421+
cancelled_turn: activeTurn ? { ...activeTurn, status: "cancelled" } : void 0,
27422+
elapsed_ms: elapsedMs,
27423+
diagnostics: args.advanced?.include_diagnostics ? {
27424+
session: compactSession2,
27425+
...sessionProgressPayload(compactSession2)
27426+
} : void 0,
27427+
hint: "The session is closed. Start a new codex_task if more work is needed."
27428+
});
27429+
}
2736727430
const description = args.description;
2736827431
const runOptions = publicRunOptions({
2736927432
...args,
@@ -27415,7 +27478,7 @@ registerTool(
2741527478
session_id: args.session_id,
2741627479
turn: response.turn,
2741727480
delivery,
27418-
hint: "Use codex_followup mode wait or steer with this session_id."
27481+
hint: "Use codex_followup mode wait, steer, or cancel with this session_id."
2741927482
};
2742027483
if (args.advanced?.include_diagnostics) {
2742127484
payload.diagnostics = {

docs/USAGE.md

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,9 @@ Prefer these tools in normal Claude usage:
2626

2727
- `codex_task` - one Task-like Codex subagent with an answer-first result.
2828
- `codex_task_group` - several independent Task-like Codex subagents in parallel.
29-
- `codex_followup` - continue, steer, or wait on the `session_id` returned by
30-
`codex_task` or `codex_task_group` when `background` or `keep_session` is used.
29+
- `codex_followup` - continue, steer, wait on, or cancel the `session_id`
30+
returned by `codex_task` or `codex_task_group` when `background` or
31+
`keep_session` is used.
3132

3233
Legacy compatibility tools are hidden by default. Set
3334
`CODEX_SUBAGENTS_ENABLE_LEGACY_TOOLS=1` only for older clients that still call
@@ -65,6 +66,7 @@ Use this decision path when writing prompts or debugging Claude tool choice:
6566
| Add a normal follow-up to a running session | `codex_followup` with `mode: "queue"` |
6667
| Redirect the active app-server turn | `codex_followup` with `mode: "steer"` |
6768
| Wait for a background session | `codex_followup` with `mode: "wait"` |
69+
| Stop a background or running session | `codex_followup` with `mode: "cancel"` |
6870

6971
When in doubt, read `codex://usage` and then choose among the three native tools.
7072

@@ -161,6 +163,20 @@ To steer an active app-server turn:
161163
If `session.supportsRealSteering` is false, the session fell back to the exec
162164
protocol and steering becomes a high-priority queued turn.
163165

166+
To stop a background or actively running session:
167+
168+
```json
169+
{
170+
"session_id": "session-...",
171+
"mode": "cancel",
172+
"reason": "user changed direction"
173+
}
174+
```
175+
176+
The cancel response includes partial output if Codex streamed any before the
177+
interrupt. Foreground `codex_task` calls are cancelled by Claude Code's normal
178+
request interruption path, not by a tool call from the same in-flight turn.
179+
164180
## Spark And Reasoning
165181

166182
Do not use `advanced.model: "spark"` by default. Use Spark only when the user asks

0 commit comments

Comments
 (0)