make plan mode reachable in chat
opusplan has been in the picker since 57bbcca9 and did nothing: it means
'Opus while planning, Sonnet at rest', and claude-manager hardcoded
permissionMode: 'bypassPermissions', so a session never entered plan mode
and opusplan rested forever on Sonnet.
verified against a real claude first (SDK 0.3.233), and one finding changed
the design: approving ExitPlanMode drops the harness into 'default', NOT back
into bypassPermissions, and the agent carries on inside the same turn. so the
restore is fired the moment approval is given rather than at turn end, and
canUseTool allows everything that is not ExitPlanMode - for a few hundred ms
it is the only thing between the agent and its tools, and a default-deny there
would jail an agent the platform deliberately does not jail.
canUseTool is passed always. under bypassPermissions the SDK shadows it
entirely (CLAUDE_SDK_CAN_USE_TOOL_SHADOWED) so it costs a log line, and since
it is a query() option read once, passing it unconditionally is what makes the
mode toggleable on a live conversation instead of only at session start.
sticky per session on chat_session_models.plan_mode, beside the model because
they are one decision about what a turn costs. boolean rather than the
PermissionMode string: officer exposes two of the five, and a text column would
accept three the sidecar cannot handle.
plan approval is the only place the agent blocks on officer - a canUseTool
promise held in the sidecar while the plan round-trips to a browser. every path
that ends a session or a turn settles it as refused, plus a 20m timeout, because
a leaked entry is a hung turn rather than a stale value.
not auto-entered for opusplan: the CLI does not either. the toggle says so
instead - 'opusplan rests on Sonnet, turn on Plan for Opus'.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -59,6 +59,27 @@ export type SidecarCommand =
|
||||
// Is a turn still running for this session? Only the process that owns the session can say, which is
|
||||
// exactly why it is asked over the wire — see `isClaudeGenerating` in sidecar-registry.
|
||||
| { type: 'claude:is-generating'; id: string; sessionKey: string; userId: number }
|
||||
// Turn plan mode on or off on a session that is already live, without waiting for the next turn. The
|
||||
// toggle is meant to be usable while reading the agent's last answer, which is exactly when no turn is
|
||||
// in flight to carry it.
|
||||
| { type: 'claude:set-plan-mode'; id: string; sessionKey: string; userId: number; enabled: boolean }
|
||||
/**
|
||||
* The user's answer to a plan the agent asked to exit with.
|
||||
*
|
||||
* This is the only command that resolves a promise the AGENT is blocked on: `canUseTool` is holding
|
||||
* `ExitPlanMode` open inside the sidecar while this round-trips to a browser and back. `requestId`
|
||||
* identifies which one — a session can only have a single plan outstanding today, but keying on the id
|
||||
* means a stale answer from a reconnected client cannot resolve a newer plan.
|
||||
*/
|
||||
| {
|
||||
type: 'claude:plan-decision';
|
||||
id: string;
|
||||
sessionKey: string;
|
||||
userId: number;
|
||||
requestId: string;
|
||||
approve: boolean;
|
||||
feedback?: string;
|
||||
}
|
||||
// Which session key owns this transcript? The map lives on the agent's disk, so only it can answer —
|
||||
// see `findClaudeSessionKey` in sidecar-registry, and `attach` in the chat socket for why it is asked.
|
||||
| { type: 'claude:find-session'; id: string; claudeSessionId: string; userId: number }
|
||||
@@ -187,6 +208,15 @@ export type ClaudeSpawnStreamingParams = {
|
||||
cwd?: string;
|
||||
model?: string;
|
||||
resumeSessionId?: string; // resume this Claude session uuid (from the /chat session list)
|
||||
/**
|
||||
* Run this turn in PLAN mode — research and propose, execute nothing until the plan is approved.
|
||||
*
|
||||
* Carried on the turn for the same reason `model` is: both are `query()` options read once at session
|
||||
* creation, so both need a control request to change on a live session, and the turn is where the client's
|
||||
* current intent arrives. Absent means "no opinion" and leaves a live session's mode alone; an explicit
|
||||
* `false` turns planning off.
|
||||
*/
|
||||
planMode?: boolean;
|
||||
// Whether turn output should be committed to chat_session_events (default true). A chat session wants
|
||||
// it — that is what survives an officer restart. A pipeline step does not: its sessionKey is a throwaway
|
||||
// uuid no browser will ever replay, and the job's own event log is its record.
|
||||
|
||||
Reference in New Issue
Block a user