feat(plan-mode): minimal plan mode with mutation gate and resume#61845
feat(plan-mode): minimal plan mode with mutation gate and resume#61845nick0809cn-ux wants to merge 4 commits into
Conversation
…gistration (Slice 2-3)
…ion gate (Slice 4)
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 70141855f8
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| ? readSessionRuntimeStateFromStorePath({ | ||
| sessionKey: params.sessionKey, | ||
| storePath: path.join(path.dirname(path.resolve(params.sessionFile)), "sessions.json"), |
There was a problem hiding this comment.
Read plan mode state from configured session store path
This lookup hardcodes <sessionFileDir>/sessions.json, so resumed runs will miss persisted plan mode state whenever session.store uses a custom filename/path (for example /var/openclaw/custom-store.json or a non-default per-agent template). In that configuration, plan mode is still active in the real store but buildEmbeddedSystemPrompt won't get planModeActive, so the ## Plan Mode guardrails silently disappear on resume. Resolve the store path from config/session key here instead of assuming sessions.json.
Useful? React with 👍 / 👎.
| if (!SESSION_RUNTIME_MODES.includes(mode as (typeof SESSION_RUNTIME_MODES)[number])) { | ||
| failInvalidOption(`Runtime mode must be one of ${SESSION_RUNTIME_MODES.join(", ")}.`); |
There was a problem hiding this comment.
Preserve backend-defined ACP runtime modes
This new allowlist rejects every runtime mode except plan, normal, and auto, but ACP mode values are backend-defined (setMode accepts arbitrary strings) and were previously passed through. As a result, /acp set-mode <custom-mode> now fails before reaching compatible backends, which is a compatibility regression for adapters that expose non-core mode tokens.
Useful? React with 👍 / 👎.
| nextEntry.acp = { | ||
| ...params.entry.acp, | ||
| runtimeOptions: { | ||
| ...params.entry.acp.runtimeOptions, | ||
| runtimeMode: params.mode, |
There was a problem hiding this comment.
Avoid forcing ACP mode overrides during plan toggles
Mirroring every plan-mode change into acp.runtimeOptions.runtimeMode makes enter_plan_mode/exit_plan_mode implicitly set ACP runtime controls for any ACP-bound session. For runtimes without session/set_mode, later control application throws unsupported-control errors, so simply toggling plan mode can break subsequent ACP turns. This write needs capability-aware gating (or should stay session-local) instead of unconditional ACP option mutation.
Useful? React with 👍 / 👎.
Greptile SummaryAdds a minimal Plan Mode runtime with a mutation gate, session-persistent plan state, and resume support. The implementation is well-structured and backed by solid test coverage across 7 test files. All findings are P2. Confidence Score: 5/5Safe to merge; all findings are P2 style/cleanup items with no blocking correctness issues. No P0 or P1 issues found. The mutation gate, session persistence, and system-prompt injection are all correct. The three findings are a misleading debug-log count, a potential store-path mismatch under a non-default session.store config, and a silent plan-state reset when re-entering plan mode — none blocking. src/agents/pi-embedded-runner/run/attempt.ts (store path derivation at line 718) and src/plugins/hook-runner-global.ts (hook count log at line 50)
|
| const planModeState = params.sessionKey | ||
| ? readSessionRuntimeStateFromStorePath({ | ||
| sessionKey: params.sessionKey, | ||
| storePath: path.join(path.dirname(path.resolve(params.sessionFile)), "sessions.json"), | ||
| }) | ||
| : null; |
There was a problem hiding this comment.
Hardcoded store path may miss custom
session.store config
The store path is derived from the transcript file's parent directory rather than from resolveStorePath. For users with a custom session.store config the two paths can diverge, so planModeActive will always be false on resume and the ## Plan Mode system-prompt section will never be injected. params.config and sessionAgentId are both in scope here:
// add import: import { resolveStorePath } from "../../../config/sessions/paths.js";
const planModeState = params.sessionKey
? readSessionRuntimeStateFromStorePath({
sessionKey: params.sessionKey,
storePath: resolveStorePath(params.config?.session?.store, {
agentId: sessionAgentId,
}),
})
: null;
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/agents/pi-embedded-runner/run/attempt.ts
Line: 715-720
Comment:
**Hardcoded store path may miss custom `session.store` config**
The store path is derived from the transcript file's parent directory rather than from `resolveStorePath`. For users with a custom `session.store` config the two paths can diverge, so `planModeActive` will always be `false` on resume and the `## Plan Mode` system-prompt section will never be injected. `params.config` and `sessionAgentId` are both in scope here:
```
// add import: import { resolveStorePath } from "../../../config/sessions/paths.js";
const planModeState = params.sessionKey
? readSessionRuntimeStateFromStorePath({
sessionKey: params.sessionKey,
storePath: resolveStorePath(params.config?.session?.store, {
agentId: sessionAgentId,
}),
})
: null;
```
How can I resolve this? If you propose a fix, please make it concise.| execute: async () => { | ||
| const cfg = opts?.config ?? loadConfig(); | ||
| const sessionKey = requireSessionKey(opts?.agentSessionKey); | ||
| const now = Date.now(); | ||
| await setSessionRuntimeMode(sessionKey, "plan", cfg); | ||
| const state = await updateSessionPlanState({ | ||
| sessionKey, | ||
| cfg, | ||
| mutate: () => ({ | ||
| enteredAt: now, | ||
| updatedAt: now, | ||
| }), | ||
| }); | ||
| return textResult("Plan mode enabled.", { | ||
| status: "enabled" as const, | ||
| sessionKey, | ||
| runtimeMode: "plan" as const, | ||
| planState: state?.planState ?? { enteredAt: now, updatedAt: now }, | ||
| }); | ||
| }, | ||
| }; |
There was a problem hiding this comment.
Re-entering plan mode silently clears existing plan state
enter_plan_mode is not in the mutation blocklist, so it can be called while plan mode is already active. The mutate callback returns only { enteredAt, updatedAt } — updateSessionPlanState replaces the entire planState field with this object, discarding any previously written content and todos. If the reset is intentional a short comment or an early-return guard (when runtimeMode === "plan" already) would make the behavior explicit.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/agents/tools/plan-mode-tools.ts
Line: 194-214
Comment:
**Re-entering plan mode silently clears existing plan state**
`enter_plan_mode` is not in the mutation blocklist, so it can be called while plan mode is already active. The `mutate` callback returns only `{ enteredAt, updatedAt }` — `updateSessionPlanState` replaces the entire `planState` field with this object, discarding any previously written `content` and `todos`. If the reset is intentional a short comment or an early-return guard (when `runtimeMode === "plan"` already) would make the behavior explicit.
How can I resolve this? If you propose a fix, please make it concise.There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: f784b1182d
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| export const DEFAULT_PLAN_MODE_MUTATION_TOOL_NAMES = [ | ||
| "apply_patch", | ||
| "edit", | ||
| "exec", | ||
| "gateway", |
There was a problem hiding this comment.
Block cron mutations while plan mode is active
The plan-mode mutation gate never treats cron as a mutation tool, so while runtimeMode === "plan" the agent can still call cron with mutating actions (for example add/update/remove/run) and cause side effects before user confirmation. That bypasses the core safety guarantee of plan mode and can modify scheduled jobs even though mutation tools are supposed to be blocked until exit_plan_mode.
Useful? React with 👍 / 👎.
| const runtimeMode = | ||
| (typeof params.cfg.session?.store === "string" | ||
| ? getSessionRuntimeMode(params.sessionKey, params.cfg) | ||
| : undefined) ?? | ||
| fallback?.runtimeMode ?? |
There was a problem hiding this comment.
Let store-path fallback run when runtime mode lookup misses
resolvePlanModeSnapshot intends to fall back to readSessionRuntimeStateFromStorePath, but getSessionRuntimeMode(...) already defaults to "auto" for any non-empty session key, so this branch never becomes undefined and the fallback runtime mode is effectively dead. In cases where the helper resolves the wrong store (for example legacy/non-agent keys in status lookups), session_status can report auto even when the actual resolved store entry is in plan mode.
Useful? React with 👍 / 👎.
|
Closing this as duplicate or superseded after Codex automated review. Close #61845 as superseded. Current main does not ship this PR's minimal plan-mode runtime; the broader Plan Mode direction moved through later plan-mode rollout attempts and is now represented by the active #71731 plugin-host RFC path plus #71732-#71737 decision threads. #61845 is no longer the right merge vehicle because it is stale, dirty, and has unresolved review concerns. Best possible solution: Close #61845 and keep remaining Plan Mode design on #71731 and #71732-#71737. If the feature moves forward, it should land as a fresh clean implementation after the plugin-host seams are settled, or through an explicit maintainer-approved core PR. What I checked:
So I’m closing this here and keeping the remaining discussion on the canonical linked item. Codex Review notes: model gpt-5.5, reasoning high; reviewed against 06d409dc2738. |
Summary
Adds a minimal Plan Mode runtime mode that lets agents plan before executing complex changes. When active, mutation tools are gated behind an explicit confirmation step.
Changes
Core: Runtime Mode Enum & Session Persistence
SessionRuntimeMode = "plan" | "normal" | "auto"onSessionEntryandAcpSessionRuntimeOptionsgetSessionRuntimeMode,setSessionRuntimeMode,getSessionPlanState,updateSessionPlanStateinsrc/config/sessions/runtime-mode.tsreadSessionRuntimeStateFromStorePathallows reading mode directly from a known store path (no config dependency)SessionRuntimeModeenumTools: Plan Mode Tool Set (5 tools)
enter_plan_mode— sets runtime to"plan", initializes plan stateexit_plan_mode— sets runtime to"normal", marksconfirmedAttodo_write— writes/updates plan content and todo list in session metadatatask_create— creates a session-scoped task from a plan todo (reuses Task Registry)task_update— updates task status, progress, and terminal summariesSafety: Mutation Gate Hook
before_tool_callhook (plan-mode-hook.ts) that blocks mutation tools whenruntimeMode === "plan"apply_patch,edit,exec,gateway,message,nodes,process,sessions_send,sessions_spawn,subagents,write.write,.edit, or.deleteis also blockedinitializeGlobalHookRunnerResume: Persistence & System Prompt Integration
buildEmbeddedSystemPromptinjects a## Plan Modesection with current plan and todossession_statustool surfaces plan mode, plan content, and todo listTest Coverage
20 tests across 7 test files:
Files Changed
New files:
src/config/sessions/runtime-mode.ts+.test.tssrc/acp/control-plane/runtime-options.test.tssrc/agents/tools/plan-mode-tools.ts+.test.tssrc/agents/plan-mode-hook.ts+.test.tssrc/agents/plan-mode.before-tool-call.integration.test.tssrc/agents/plan-mode-resume.test.tsModified files:
src/config/sessions/types.ts— SessionRuntimeMode, SessionPlanState typessrc/config/sessions.ts— re-export runtime-mode modulesrc/acp/control-plane/runtime-options.ts— typed runtimeMode validationsrc/agents/openclaw-tools.ts— register 5 plan mode toolssrc/agents/tool-description-presets.ts— tool descriptionssrc/agents/tool-catalog.ts+tool-display-config.ts— display configsrc/agents/pi-embedded-runner/system-prompt.ts— plan mode prompt sectionsrc/agents/pi-embedded-runner/run/attempt.ts— plan state injectionsrc/agents/tools/session-status-tool.ts— plan mode status displaysrc/plugins/hook-runner-global.ts— register mutation gate hooksrc/tasks/task-registry.ts— export helpers for plan mode toolsDesign Document
See
docs/design/plan-mode-slice.mdfor the original design rationale and implementation slices.