Skip to content

feat(plan-mode): universal /plan slash commands across all channels (PR-11)#68441

Closed
100yenadmin wants to merge 129 commits into
openclaw:mainfrom
electricsheephq:feat/plan-channel-parity
Closed

feat(plan-mode): universal /plan slash commands across all channels (PR-11)#68441
100yenadmin wants to merge 129 commits into
openclaw:mainfrom
electricsheephq:feat/plan-channel-parity

Conversation

@100yenadmin

Copy link
Copy Markdown
Contributor

Summary

PR-11 of the plan-mode rollout series. Sits on top of PR-10 (#68440) which is a hard prerequisite. Net-new in this PR are 2 commits (`29bf11710e` + `fe274f395a`) — focused review surface is `git diff feat/plan-archetype-and-questions..feat/plan-channel-parity`.

  • Problem: PR-10's `Plan ⚡` chip + `/plan auto on|off` only worked in the Control UI webchat. Telegram / Discord / Signal / iMessage / Slack / CLI users couldn't drive plan-mode approvals from their own channel — they had to switch to the UI to accept/revise plans.
  • Why it matters: Telegram is the user's primary platform; webchat-only approval surfaces a lot of context-switching.
  • What changed:
    • Universal `/plan` slash commands that work on every messaging channel (Telegram, Discord, Signal, iMessage, Slack, CLI, web, Matrix, Mattermost, MSTeams, GoogleChat, Feishu, WhatsApp, voice, IRC, Nostr, Line, QQ, Zalo, etc):
      • `/plan accept` → `sessions.patch { planApproval: { action: "approve" }}`
      • `/plan accept edits` → `action: "edit"`
      • `/plan revise ` → `action: "reject", feedback` (feedback REQUIRED)
      • `/plan auto on|off` → `action: "auto", autoEnabled`
      • `/plan on|off` → `planMode: "plan"|"normal"` toggle
      • `/plan status` → prints current plan-mode state (read-only)
      • `/plan view` → directs user to /plan restate on text channels
      • `/plan restate` → re-renders `lastPlanSteps` checklist using channel-appropriate format (Telegram→HTML, Slack→mrkdwn, SMS-like→plaintext, default→markdown), gated on operator auth
  • What did NOT change: PR-10's webchat surface stays identical; this PR adds an alternative entry point. Server-side `sessions.patch` semantics unchanged.

Change Type

  • Feature
  • Security hardening (mention-bomb fix in renderPlanChecklist markdown branch, length caps, plugin-shadow protection)

Scope

  • Integrations (Telegram/Discord/Slack/Signal/iMessage/Matrix/etc — every chat channel)
  • Skills / tool execution (universal /plan handler)
  • API / contracts (sessions.patch schema length caps)
  • Auth / tokens (read-only carve-out for /plan status)

Linked Issue/PR

Root Cause

N/A — feature work + hardening of pre-existing renderPlanChecklist markdown branch.

Testing

  • 30 net-new tests (`commands-plan.test.ts`) covering parser dispatch, payload shapes, restate rendering per channel, error surfaces, auth carve-outs, mention neutralization, truncation
  • 6 net-new tests in `plan-render.test.ts` covering markdown + HTML mention neutralization
  • 1 net-new test in `sessions-patch.test.ts` for length-cap validation
  • Two adversarial-review passes (first-pass caught H1/H2/M1/M3/M4/L3; second-pass caught BLOCKER mention-bomb in markdown branch + reservedCommands shadow risk + length-cap gap + feedback echo)

Evidence

  • Failing test/log before + passing after (BLOCKER fix has a paired regression test in `plan-render.test.ts` covering @-mentions in markdown + HTML)
  • All scoped tests passing locally (52 in plan-render, 30 in commands-plan, 41 in sessions-patch)

Human Verification

  • Verified scenarios: `pnpm format:fix`, `pnpm lint` (0 errors), `pnpm tsgo` (only pre-existing plan-store.test.ts error from feat(agents): cross-session plan store with file-level locking [Phase 4.2] #67542 baseline, not PR-11), `pnpm build`, `pnpm ui:build`
  • Edge cases checked: `/plan@otherbot` regex on non-Telegram channels (must not misfire), `/plan revise` empty feedback (now hard-blocks), `/plan accept` without pending approval (friendly bail), `/plan restate` with 100-step plan (truncates at 3500 chars), Discord raw mentions `<@!123>` neutralization
  • What I did not verify: end-to-end live runtime on each of the 17+ channels (deferred to operator install + per-channel smoke)

Review Conversations

  • I'll respond to bot reviews as they come in.

Compatibility / Migration

  • Backward compatible? Yes — adds a new slash command, doesn't change existing /approve / /plan behavior. Plugin authors registering `/plan` will now see a validation error (intentional — was previously a silent shadow).
  • Config/env changes? No.
  • Migration needed? No.

Risks and Mitigations

  • Risk: `/plan status` reveals plan-mode state to any chat participant in shared channels (read-only carve-out skips auth).
    • Mitigation: Documented in deep-dive review; future PR can mask sensitive fields (autoApprove flag, rejectionCount) when caller is unauthorized. Plan step text is NOT exposed by /plan status (that's restate, which IS gated).
  • Risk: `pickPlanRenderFormat` hardcodes a channel ID list instead of consulting `isMarkdownCapableMessageChannel` registry. Third-party channel plugins that declare `markdownCapable: false` won't get plaintext rendering.
    • Mitigation: Documented as follow-up; current PLAINTEXT_ONLY_CHANNELS set covers all bundled plugins per the manifest inventory.
  • Risk: Plan step text in `/plan restate` may contain file paths or sensitive context the agent has seen.
    • Mitigation: Restate is now gated on operator auth (post-M3 fix); only operators can pull the plan. Mentions are neutralized on all formats (post-B1 fix).

Copilot AI review requested due to automatic review settings April 18, 2026 06:41
@openclaw-barnacle openclaw-barnacle Bot added docs Improvements or additions to documentation app: web-ui App: web-ui gateway Gateway runtime agents Agent runtime and tooling extensions: openai size: XL labels Apr 18, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds universal plan-mode control and approval handling across channels by introducing a built-in /plan command and strengthening plan-mode infrastructure (state persistence, rendering, gating, and prompt guidance).

Changes:

  • Introduces /plan command handling (UI + backend) and reserves plan to prevent plugin command shadowing.
  • Persists and surfaces plan-mode session state and last plan snapshot to support channel parity and UI rebuild-after-refresh.
  • Adds/extends plan-mode runtime primitives (mutation gate, approval state machine, plan nudges, skill plan templates, prompt tuning) plus extensive tests.

Reviewed changes

Copilot reviewed 119 out of 120 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
ui/src/ui/views/plan-approval-inline.ts Inline plan approval + question variant rendering above chat input
ui/src/ui/types.ts Adds plan-mode/permission-mode fields to UI session row typing
ui/src/ui/chat/slash-commands.ts Registers /plan as a local UI slash command (autocomplete/dispatch)
ui/src/ui/chat/slash-command-executor.ts Implements /plan execution (on/off/status/view/auto) + patch helpers
ui/src/ui/chat/slash-command-executor.node.test.ts Tests /plan auto behavior and error surfaces
ui/src/ui/chat/plan-cards.ts Adds expandable plan cards + markdown formatting helper
ui/src/ui/chat/plan-cards.test.ts Tests plan card DOM rendering + markdown formatting
ui/src/ui/app-view-state.ts Adds plan-approval and plan-sidebar state/actions to app view state
ui/src/ui/app-render.ts Wires plan approval UI + mode chip behavior + plan sidebar open rendering
ui/src/ui/app-render.helpers.ts Adds plan sidebar toggle button and aria state handling
ui/src/ui/app-chat.ts Handles new slash action toggle-plan-view
ui/src/styles/chat/plan-cards.css Styles for plan cards in message thread
ui/src/styles/chat/layout.css Mode switcher + inline plan approval card styles
ui/src/styles/chat.css Imports plan-cards stylesheet
ui/src/i18n/locales/en.ts Adds chat.planViewToggle string
src/plugins/command-registration.ts Reserves /plan and other built-in command names to prevent shadowing
src/infra/heartbeat-runner.ts Prepends plan “continue” nudge to heartbeat prompt when plan is active
src/infra/heartbeat-runner.plan-nudge.test.ts Tests plan nudge prefix selection logic
src/gateway/session-utils.types.ts Exposes execSecurity/execAsk/planMode on gateway session rows
src/gateway/session-utils.ts Populates execSecurity/execAsk/planMode in gateway session rows
src/gateway/server.impl.ts Threads planSnapshot subscription unsubscribe through server runtime state
src/gateway/server-runtime-subscriptions.ts Starts plan snapshot persister subscription
src/gateway/server-runtime-handles.ts Tracks plan snapshot unsubscribe handle in mutable server state
src/gateway/server-methods/sessions.ts Includes mode/plan state in sessions.changed payloads
src/gateway/server-close.ts Unsubscribes plan snapshot persister on gateway shutdown
src/gateway/server-close.test.ts Updates shutdown tests for new unsubscribe handle
src/gateway/protocol/schema/sessions.ts Extends sessions.patch schema for planMode/planApproval/lastPlanSteps + length caps
src/gateway/plan-snapshot-persister.ts Persists lastPlanSteps to session store on plan events; auto-close on completed phase
src/cron/normalize.ts Consolidates “current sessionTarget” resolution logic (cron resume behavior)
src/config/zod-schema.ts Adds skills.limits.maxPlanTemplateSteps config
src/config/zod-schema.agent-runtime.ts Adds per-agent embeddedPi autoContinue/maxIterations overrides
src/config/zod-schema.agent-defaults.ts Adds embeddedPi autoContinue/maxIterations + planMode defaults schema
src/config/types.skills.ts Adds maxPlanTemplateSteps to skills limits types
src/config/types.agents.ts Adds embeddedPi autoContinue/maxIterations per-agent types
src/config/types.agent-defaults.ts Adds embeddedPi autoContinue/maxIterations + planMode defaults types
src/config/sessions/types.ts Extends SessionEntry.planMode shape (snapshots, nudges, autoApprove, etc.)
src/auto-reply/reply/commands-system-prompt.ts Threads provider/model identity into system prompt builder
src/auto-reply/reply/commands-handlers.runtime.ts Registers new runtime handler for /plan
src/auto-reply/reply/agent-runner-execution.ts Mirrors plan mode + approval state into AgentRunContext at registration
src/auto-reply/commands-registry.shared.ts Registers /plan in builtin commands registry
src/agents/transport-message-transform.ts Improves missing tool_result placeholder text and logging
src/agents/tools/update-plan-tool.test.ts Adjusts expectations + adds close-on-complete + closure gate tests
src/agents/tools/sessions-spawn-tool.ts Tracks parent runId context; forces cleanup keep in plan mode; tracks open subagents
src/agents/tools/enter-plan-mode-tool.ts Adds enter_plan_mode agent tool definition
src/agents/tools/cron-tool.ts Documents cron “resume after wait” recipe (sessionTarget=current)
src/agents/tools/ask-user-question-tool.ts Adds ask_user_question tool (approval pipeline question)
src/agents/tools/ask-user-question-tool.test.ts Tests ask_user_question validation + deterministic IDs
src/agents/tool-description-presets.ts Adds summaries + descriptions for plan-mode tools and ask_user_question
src/agents/tool-catalog.ts Registers plan-mode tools and ask_user_question in tool catalog
src/agents/test-helpers/fast-openclaw-tools-sessions.ts Updates update_plan mock signature + exports PLAN_STEP_STATUSES
src/agents/system-prompt.ts Adds GPT-5 context file reorder + injection-safe context file headings/content
src/agents/system-prompt-gpt5-boot-reorder.test.ts Tests GPT-5 provider/model-specific context file ordering
src/agents/system-prompt-contribution.ts Adds new provider section id tool_enforcement
src/agents/subagent-registry-run-manager.ts Drains completed/killed child runs from parents’ openSubagentRunIds
src/agents/skills/workspace.ts Carries plan templates into workspace skill snapshot
src/agents/skills/types.ts Adds SkillPlanTemplateStep + resolvedPlanTemplates on SkillSnapshot
src/agents/skills/skill-planner.ts Builds update_plan payload from skill plan templates with caps/dedup
src/agents/skills/frontmatter.ts Parses plan-template / planTemplate from skill frontmatter
src/agents/skills/frontmatter.test.ts Tests planTemplate parsing + precedence rules
src/agents/plan-mode/types.ts Adds plan-mode session types + approvalId generation + decision injection
src/agents/plan-mode/plan-nudge-crons.ts Schedules/cleans plan nudge cron jobs
src/agents/plan-mode/plan-archetype-prompt.ts Adds plan archetype prompt + plan filename helpers
src/agents/plan-mode/plan-archetype-prompt.test.ts Tests prompt fragment + filename helpers
src/agents/plan-mode/mutation-gate.ts Implements plan-mode mutation gate + exec read-only allowlist
src/agents/plan-mode/mutation-gate.test.ts Tests mutation gate behavior and safety checks
src/agents/plan-mode/index.ts Re-exports plan-mode modules
src/agents/plan-mode/approval.ts Approval state machine (approve/edit/reject/timeout + stale approvalId guard)
src/agents/plan-hydration.ts Formats active plan injection after compaction
src/agents/plan-hydration.test.ts Tests plan hydration formatting and filtering
src/agents/pi-tools.ts Threads planMode into before-tool-call hook context
src/agents/pi-tools.before-tool-call.ts Enforces mutation gate before plugin hooks run
src/agents/pi-embedded-runner/system-prompt.ts Threads provider/model identity into embedded prompt build
src/agents/pi-embedded-runner/run/params.ts Adds run param for planMode threading
src/agents/pi-embedded-runner/run/helpers.ts Updates max iteration defaults; adds user overrides; subagent cap constant
src/agents/pi-embedded-runner/run/attempt.ts Seeds plan from skill templates; prepends plan-mode prompt rules; passes planMode to tools
src/agents/pi-embedded-runner/run/attempt.spawn-workspace.test-support.ts Stubs skill-template seeder in test support
src/agents/pi-embedded-runner/run.incomplete-turn.test.ts Updates retry cap expectations; tests escalation and plan-mode carve-outs
src/agents/openclaw-tools.ts Registers plan-mode tools + ask_user_question; threads runId into tools
src/agents/openclaw-tools.registration.ts Adds config gate for plan-mode tool registration
src/agents/agent-scope.ts Adds autoContinue + maxIterations resolvers with per-field merge
src/agents/agent-scope.test.ts Tests autoContinue per-field merge behavior
qa/scenarios/gpt54-plan-mode-default-off.md QA scenario asserting plan mode stays off by default
qa/scenarios/gpt54-mandatory-tool-use.md QA scenario asserting tool use for factual queries
qa/scenarios/gpt54-injection-scan.md QA scenario for injection scanner baseline
qa/scenarios/gpt54-cancelled-status.md QA scenario for cancelled plan step status behavior
qa/scenarios/gpt54-act-dont-ask.md QA scenario for “act on defaults” behavior
extensions/openai/prompt-overlay.ts Adds tool enforcement block + identity/conciseness guidance + plan-mode carve-out
extensions/openai/index.test.ts Updates OpenAI extension tests for new overlay section override

Comment thread src/gateway/sessions-patch.ts
Comment thread ui/src/ui/views/plan-approval-inline.ts Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: fe274f395a

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/auto-reply/reply/commands-plan.ts Outdated
Comment thread src/auto-reply/reply/commands-plan.ts Outdated
@100yenadmin

Copy link
Copy Markdown
Contributor Author

PR-12 hardening commit pushed (2abc20fec8) addressing live-test feedback:

Cron-nudge bugs (fixed in this PR)

The plan-nudge cron infrastructure (PR-9 Wave B3) had three issues that surfaced under multi-cycle testing:

  • Bug A1 — orphan accumulation: only the /plan off path called cleanupPlanNudges. Every approve/reject/edit cycle deleted the planMode entry without first cancelling the 3 scheduled wake-ups; the next enter_plan_mode scheduled a fresh batch. After N cycles, 3N orphaned crons would fire and interrupt unrelated work. Fix: capture nudgeJobIds BEFORE rewriting the entry in the planApproval block of sessions-patch.ts.
  • Bug A2 — interrupt during pending approval: buildActivePlanNudge fired regardless of planMode.approval state. A cron firing while the user had a pending Approve/Reject/Edit triad or question card on screen would re-engage the agent mid-prompt and clobber the popup. Fix: return null when planMode.approval === \"pending\".
  • Bug A3 — interrupt during active conversation: no idle-window guard. If the user had been chatting with the agent in the last few minutes, a cron-driven nudge still fired as an unsolicited "continue" prompt. Fix: suppress when Date.now() - planMode.updatedAt < 5min (configurable; set to 0 to disable).

Tests: +5 in heartbeat-runner.plan-nudge.test.ts, +1 in sessions-patch.test.ts.

Live-test bugs deferred to PR-13 (next session)

  • Bug B (Telegram bridge): plan-mode emits agent-event stream:\"approval\" but Telegram listens to gateway plugin.approval.requested. The Web UI consumes the agent-event stream directly; Telegram never sees plan approvals. Approval flow works in webchat when initiated from Telegram (cross-channel propagation works) but Telegram itself shows nothing. Fix path identified by deep-dive review: in `pi-embedded-subscribe.handlers.tools.ts:1626` (after `persistPlanApprovalRequest`), construct a `PluginApprovalRequest` from the plan details + run-context channel binding (`turnSourceChannel`/`turnSourceTo`/`turnSourceAccountId`) and emit via the existing gateway plugin-approval pipeline. Existing `telegramApprovalNativeRuntime` will pick it up automatically (already declares `eventKinds: ["exec", "plugin"]`). ~150 LoC + needs `AgentRunContext` channel-binding plumbing that doesn't exist yet.
  • Bug 1 (multi-question batching): redesign `ask_user_question` to support 2-N questions per tool call with vertical-list pagination (matches Codex's `1 of 3 ›` pattern). Current single-horizontal-buttons design clips long options and forces extra turns when the agent has multiple things to clarify. Feature.
  • Bug 2 (Other/Revision back-out): replace `window.prompt` with inline textarea (mirroring the Revise UX pattern) so cancelling returns to the option list instead of (perceptibly) exiting the sequence. Will likely be addressed alongside Bug 1.

Build: live locally as OpenClaw 2026.4.15 (2abc20f) — gateway reachable in 61ms, kicked via launchctl kickstart -k.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2abc20fec8

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/agents/tools/update-plan-tool.ts
Comment thread src/gateway/plan-snapshot-persister.ts Outdated
@100yenadmin

Copy link
Copy Markdown
Contributor Author

📋 Plan-mode rollout — full PR series overview (10 PRs)

Authoritative status snapshot for the entire plan-mode work, current as of 3a6ec73433. This branch (feat/plan-channel-parity) is the cumulative tip — running locally as OpenClaw 2026.4.15 (3a6ec73) on the maintainer's box.

The 10 open PRs in dependency order

# PR Role Mergeable Live?
1 #67512 GPT-5.4 prompt discipline + personality bridge + context-file injection scanner ⚠️ CONFLICTING with main
2 #67514 Task system parity — cancelled status, merge:true mode, activeForm, plan hydration ✅ MERGEABLE
3 #67534 Plan checklist renderer (4 formats: html/markdown/plaintext/slack-mrkdwn, mention bombs neutralized — see PR-13 hardening below) ✅ MERGEABLE
4 #67538 Plan-mode runtime: escalating retry, auto-continue, mutation gate primitives ✅ MERGEABLE
5 #67541 Skill plan templates (skill-driven planning) ✅ MERGEABLE
6 #67542 Cross-session plan store (file-level locking, security-hardened) ✅ MERGEABLE
7 #67721 UI mode switcher chip + clickable plan cards + channel-aware plan delivery ❓ UNKNOWN
8 #67840 Plan-mode integration bridge (PR-8) — wires tools + mutation gate + sessions.patch schema. Hard prerequisite for all subsequent plan-mode UX. ❓ UNKNOWN
9 #68440 PR-10 — plan archetype enforcement (title/analysis/assumptions/risks/verification/references) + ask_user_question tool + Plan ⚡ auto-mode + 5 deep-dive review fixes (B1/B2/M2/M3/H1/H5) ❓ UNKNOWN
10 #68441 PR-11 (this PR) — universal `/plan accept accept edits revise

(Closed/deprecated: #67518 Gemini guidance — explicitly dropped per maintainer.)

What's NEW since the upstream PRs were opened

PR-12 (cron-nudge fixes) and PR-13 (vertical question layout + inline Other textarea) were applied on top of #68441's branch as additional hardening — they don't have separate upstream PRs because they're polish on top of #68441's surface. Folded into this PR's diff.

PR-12 (cron-nudge orphan + interrupt fixes) — commit 2abc20fec8:

  • Bug A1: cleanup nudges on approve/reject/edit (was leaking 3 wake-ups per cycle)
  • Bug A2: suppress nudge firing when planMode.approval === "pending" (was clobbering popup flows)
  • Bug A3: suppress when agent active in last 5 min (was interrupting active conversations)

PR-13 (question-card UX fixes) — commit 3a6ec73433:

  • Bug 1: vertical option layout for question cards (was: horizontal flex-wrap clipping long answers)
  • Bug 2: inline Other textarea replaces window.prompt (Cancel returns to options, doesn't exit sequence)

Deferred to PR-14 (next session — well-scoped, ~150 LoC)

  • Bug B (Telegram approval-card visibility): plan-mode emits agent-event stream:"approval" but Telegram listens to gateway plugin.approval.requested. Investigation found the bridge requires either (a) gateway plugin-approval RPC + new translator for the dual approval-id problem, or (b) channel-message emit + reliance on /plan slash commands for resolution. Approach (b) is simpler; approach (a) gives true inline buttons. Recipe documented in commit 2abc20fec8 body.
  • Bug 1 multi-question batching: redesign ask_user_question schema to accept N questions (Codex's 1 of 3 › pagination pattern). Current single-question + vertical layout (PR-13) covers 80% of the value; batching is polish for complex multi-clarification flows.

Recommended landing order

  1. Land feat(agents): task system parity — cancelled status, merge mode, activeForm, plan hydration [Phase 3.A] #67514 + feat(agents): plan checklist renderer — 4 formats, all statuses, activeForm [Phase 3.B] #67534 + feat(agents): cross-session plan store with file-level locking [Phase 4.2] #67542 first (independent, no plan-mode deps)
  2. Land feat(skills): plan template support for skill-driven planning [Phase 4.1] #67541 (depends on feat(agents): task system parity — cancelled status, merge mode, activeForm, plan hydration [Phase 3.A] #67514's merge semantics)
  3. Land feat(agents): plan mode runtime + escalating retry + auto-continue [Phase 3.C] #67538 + feat(ui): mode switcher + clickable plan cards + channel-aware plan delivery #67721 + feat(plan-mode): integration bridge wiring tools + mutation gate + sessions.patch [PR-8] #67840 as a coordinated WAVE (each is dead code without the others)
  4. Land feat(openai,agents): GPT-5.4 prompt discipline + personality bridge + injection scanning #67512 (resolve main-conflict; independent of plan-mode work)
  5. Land feat(plan-mode): plan archetype + ask_user_question + auto mode (PR-10) #68440 (PR-10) — depends on Wave 3 being in
  6. Land feat(plan-mode): universal /plan slash commands across all channels (PR-11) #68441 (PR-11 + PR-12 + PR-13) — depends on feat(plan-mode): plan archetype + ask_user_question + auto mode (PR-10) #68440 (auto action) + Wave 3

Co-merge guidance: #67538/#67721/#67840 each ship dead code on their own. Land all three in one merge window to keep main always-bootable.

Test coverage at the tip

  • 28 PR-11 slash-command tests + 6 plan-render mention-neutralization + 1 length-cap test (PR-11 hardening)
  • 5 cron-nudge guard tests + 1 nudgeJobIds-leak test (PR-12)
  • 159 PR-10 tests across mode-switcher / sessions-patch / slash-command / archetype / ask-user-question
  • All tests pass locally; lint clean; tsgo only flags one pre-existing baseline error from feat(agents): cross-session plan store with file-level locking [Phase 4.2] #67542's plan-store.test.ts

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3a6ec73433

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/auto-reply/reply/commands-plan.ts
Comment thread src/agents/plan-render.ts Outdated
@openclaw-barnacle openclaw-barnacle Bot added the channel: telegram Channel integration: telegram label Apr 18, 2026
@100yenadmin

Copy link
Copy Markdown
Contributor Author

PR-14 hardening commit pushed (`8e49c0850f`) — Telegram plan-mode visibility via markdown attachment

Per maintainer feedback ("why wouldn't we just add this to the Telegram PR that already exists?"), the Telegram visibility bridge lands here on PR #68441 instead of a separate PR.

What it does

When the runtime emits a plan-mode approval, it now ALSO:

  1. Renders the full plan archetype as a markdown document (title + summary + analysis + plan checklist + assumptions + risks + verification + references — sections omitted when fields absent).
  2. Persists the markdown to `~/.openclaw/agents//plans/plan-YYYY-MM-DD-.md` regardless of channel — durable audit artifact for ALL sessions, not just Telegram.
  3. If the originating session is from Telegram, sends the markdown file as a document attachment to the chat with a short caption containing the universal /plan resolution slash commands.

Resolution stays text-based via PR-11's `/plan accept | accept edits | revise ` slash commands (already work on Telegram). This sidesteps the dual approval-id problem of bridging inline-button approvals through the gateway plugin-approval pipeline.

User-visible behavior on Telegram

  1. User asks Eva to draft a plan from their phone.
  2. Eva responds + calls `exit_plan_mode` with the full archetype.
  3. Webchat shows the approval card as today.
  4. Telegram chat receives the .md file as a document attachment. Caption: `Plan title — plan submitted for approval. See attached.` + `Resolve with: /plan accept | /plan accept edits | /plan revise `.
  5. User opens the file in Telegram's preview, reads the full plan inline.
  6. User replies on Telegram with `/plan accept` (or `revise `, or `auto on`) → existing PR-11 path resolves the plan → agent executes.

Architecture (for review)

  • New `renderFullPlanArchetypeMarkdown` in `src/agents/plan-render.ts` — pure, reuses existing `escapeMarkdown` + `neutralizeMentions` for injection hardening (parity with PR-11 BLOCKER B1 fix on the markdown branch).
  • New `src/agents/plan-mode/plan-archetype-persist.ts` — filesystem write with collision suffix (`-2.md`, `-3.md`, ... up to `-99`) preserving multi-cycle history. Path-traversal guard on `agentId`. Optional `baseDir` override for tests (ESM doesn't allow spying on `os.homedir`).
  • New `src/agents/plan-mode/plan-archetype-bridge.ts` — orchestrator. Best-effort fire-and-forget; failures log at warn and never propagate. Web/CLI sessions silently skip the Telegram send (markdown still persisted).
  • New `sendDocumentTelegram` in `extensions/telegram/src/send.ts` — wraps Grammy `api.sendDocument` with the same retry/diag/threading machinery as `sendMessageTelegram`. HTML caption (truncated to 1024 chars), 50 MB pre-check, threading + reply-to support. Re-exported via `extensions/telegram/runtime-api.ts`.
  • New facade in `src/plugin-sdk/telegram.ts` using `loadBundledPluginPublicSurfaceModule` (async lazy-load — keeps Telegram bundle out of cold runtime startup paths). Per the SDK boundary rules, runtime imports the SDK facade, not the plugin runtime-api directly.
  • 1-line insertion in `src/agents/pi-embedded-subscribe.handlers.tools.ts:1659` after `emitAgentApprovalEvent` and before `autoApproveIfEnabled`. `void`-fired so it never blocks.

Tests: +32

  • 14 `renderFullPlanArchetypeMarkdown`: minimal/full/partial sections, paragraph preservation, mention-bomb neutralization in title + analysis, markdown injection escaping, empty plan placeholder, fallback title.
  • 9 `persistPlanArchetypeMarkdown`: collision suffix `-2`/`-3`, mkdir-recursive, UTF-8 round-trip, agentId validation, path-traversal rejection, baseDir override, special-char agentIds.
  • 9 `dispatchPlanArchetypeAttachment` + `buildPlanAttachmentCaption`: Telegram session → expected send shape; web session → no send (markdown still persisted); missing `to` → no send; send throws → caller doesn't throw + warn logged + markdown persisted; multi-cycle suffix; missing entry; HTML escape in caption (defeats parse_mode="HTML" injection).

All scoped tests passing locally. `pnpm lint` clean. `pnpm build` + `pnpm ui:build` clean. `pnpm tsgo` only flags the pre-existing baseline error from #67542's `plan-store.test.ts:249` (not from PR-14).

Live install

`OpenClaw 2026.4.15 (8e49c08)` running locally, gateway reachable in 52ms.

What's STILL deferred (PR-15+)

  • Multi-question batching (Codex-style `1 of 3 ›` pagination for `ask_user_question`). PR-13's vertical layout already covers the primary requirement; batching is polish for complex multi-clarification flows. The agent can already call `ask_user_question` multiple times in sequence.
  • Inline-button approval bridge for Telegram (true `/approve` button click → resolves the runtime plan-mode state). Needs a gateway translator that maps the gateway-side `plugin:` ID back to the runtime's `approvalId` so a Telegram button click advances both. ~150 LoC across the gateway plugin-approval handler + a translator hook in `exec-approval-channel-runtime.ts`. Lower priority now that text resolution via `/plan accept|revise|auto` covers the use case.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8e49c0850f

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/auto-reply/reply/agent-runner-execution.ts Outdated
Comment thread src/gateway/sessions-patch.ts Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 126 out of 127 changed files in this pull request and generated 3 comments.

Comment thread src/gateway/server-runtime-subscriptions.ts
Comment thread src/agents/plan-mode/plan-archetype-persist.ts
Comment thread ui/src/ui/chat/slash-commands.ts
@100yenadmin

Copy link
Copy Markdown
Contributor Author

PR review-loop pass 1 complete (commit c9287908eb)

Resolved: 13 of 13 inline comments — 13 fixed, 0 false-positive, 0 won't-fix.

# Comment Outcome
#3104741699 sessions-patch state-machine gap Fixed (server-side pending guard)
#3104741709 onAnswerOption typing Fixed (runtime fallback + warning UI)
#3104742928 P1 HTML truncation safety Fixed (step-aware truncation)
#3104742929 P2 Channel registry Fixed (isMarkdownCapableMessageChannel)
#3105040898 P1 Closure criteria after merge Fixed (re-validate merged plan)
#3105040900 P2 autoApprove on close-on-complete Fixed (preserve flag in normal-mode branch)
#3105075577 P1 /plan answer subcommand Fixed (added to commands-plan.ts + webchat)
#3105075579 P2 Verified criteria normalization Fixed (normalize-then-compare)
#3105134661 P1 CRITICAL: planMode threading Fixed (mutation gate now activates)
#3105134664 P2 Preserve lastPlanSteps on /plan off Fixed (same patch as autoApprove)
#3105169600 Snapshot persister sessions.changed Fixed (callback wired)
#3105169607 agentId path traversal (security) Fixed (reject '.' / '..' + realpath confine)
#3105169610 LOCAL_COMMANDS shadows /plan Fixed (extended webchat executor)

Live install: OpenClaw 2026.4.15 (c928790) — gateway reachable.

Build/lint/test: lint 0 errors; tsgo only the pre-existing baseline error from #67542's plan-store.test.ts; build + ui:build clean; all touched-surface tests pass.

Pass 2 (planned): re-trigger Greptile + Copilot + Codex; address any new comments. Greptile/Copilot/Codex notifications via @-mention follow.

cc @copilot @greptile-apps please re-review the latest commit c9287908eb with the 13 review fixes.

Eva added 25 commits April 19, 2026 18:33
Resolves the previously-escalated PR-10 review comment #3104743333
(Codex P2 — sidebar refresh in update_plan merge mode) using
option C from the deferred-decision tree:

Option C: re-emit merged steps via the existing agent_plan_event
channel after merge. Lowest perf overhead (no SessionEntry hot-path
read), no new event type, persister already subscribes to this stream.

Implementation:
- src/infra/agent-events.ts — extend AgentPlanEventData with
  optional mergedSteps field carrying full structured plan
  (status/activeForm/acceptanceCriteria/verifiedCriteria). Legacy
  steps[] field kept for backwards compat.
- src/agents/tools/update-plan-tool.ts — emitAgentPlanEvent now
  includes mergedSteps in both 'update' and 'completed' phases.
- ui/src/ui/app-tool-stream.ts:
  - maybeForwardLivePlanUpdate now SKIPS merge calls (the legacy
    args.plan path was reading the delta instead of merged result)
  - new maybeForwardMergedPlanEvent subscribes to stream:'plan'
    events and refreshes the sidebar from mergedSteps. Authoritative
    refresh path for merge mode.

Architecture documentation: docs/plans/PLAN-MODE-ARCHITECTURE.md is
the new single-source-of-truth document tracking:
- All 10 PRs (PR-A..PR-11) + their dependency graph
- Recommended landing order (5 waves)
- Feature behavior diagrams (plan-mode lifecycle, mutation gate,
  auto-mode, universal /plan commands, Telegram visibility)
- Critical files reference
- Hardening status per PR
- Process going forward (naming, branch policy, push loop, gates)
- Beta-readiness checklist
This survives Claude Code session compactions.

Notes:
- Pre-existing tsgo baseline error in plan-store.test.ts:249 from
  #67542 unchanged (not from this iteration).
- Lint clean, builds clean, tests pass on touched surfaces.
… truncation single-step cap, acceptanceCriteria parse, question-answer recovery
…p check in replace mode, fix description repeat, normalize step newlines in hydration, clarify test name
…longer mangles emails, header comment scope, bounded warnedStatuses Set, +3 hardening tests
…k O_NOFOLLOW + Windows compat, hard-cap deadman for PID reuse, EEXIST type-narrow, retry-after-stale-unlink, doc 60s, +stale-lock test, fix baseline tsgo error
…scan default allowlist to basenames + always emit warn on bypass, fix m+s comment, +case-insens + warn-on-bypass tests
… find -fprint dangerous flags, align buildPlanDecisionInjection with PlanApprovalState union, lower test maxCycles to fit schema cap, explicit toolCalls assertion in plan-mode-default-off scenario
…r old sessions, content alias for step, kebab-case fall-through to camelCase on invalid, use resolved config in eligibility filter, drop redundant null-check, fix docstrings + warning event wording
…lRequest + decision handler wired, strip ephemeral run IDs from comments + describe blocks, generalize Ctrl+1..N comment
…AN_STEP_STATUSES via import in exit-plan-mode-tool, remove non-existent lcm_grep from internal investigative-tool set
…t-approval state and synthetic-message injection — recentlyApprovedAt fully wired (write → mirror → consume), pendingAgentInjection persistence layer in place (consumer wiring deferred to PR-15)
…jection to PLAN_DECISION on approve/edit/reject, gate plan-snapshot auto-close on approval state, parseTelegramThreadId for scoped thread routing in PR-14 bridge
…ce of truth for [QUESTION_ANSWER]/[PLAN_DECISION] across all channels), revert lcm_grep removal (real lossless-claw plugin tool), schema-reserved doc note for autoEnableFor + approvalTimeoutSeconds, planApproval edit payload schema doc clarification
…le required, mutation-gate freshness via getLatestPlanMode (Bug 3+4), ACK retry uses live planMode + status-without-yield prompt (Bug 4), pending-guard in auto-close + UI graceful dismissal (Bug 5), [PLAN_COMPLETE] injection (Bug 6)
The v2 fix relied on `params.getActiveSessionEntry()` for live state, but
that callback in `agent-runner.ts:1207` is a closure over a captured
`let activeSessionEntry` (line 921) that only refreshes at compaction /
memory flush / error-recovery — NOT mid-turn. So a `sessions.patch` (UI
approval) flipping `planMode.mode → "normal"` between two tool calls in
the same run left the gate's view stale; agent reported the bug still
firing after v2.

v3 fix: replace closure-snapshot reads with `loadSessionStore(storePath,
{ skipCache: true })` for true fresh disk reads. Disk I/O is negligible
(small file, OS page cache, runs at turn-start + per-tool-call only).

Two snapshot sites converted (the v2 callbacks were already converted
in this branch):

* `registerAgentRunContext` block (~line 647) — fresh-reads all 4
  mirrors at turn-start: `inPlanMode`, `planApproval`,
  `recentlyApprovedAt`, `pendingAgentInjection`. Falls back to closure
  on disk error (no worse than pre-fix behavior).

* `sessionPlanModeMode` capture (~line 1080) — fresh-read for the
  initial `planMode` flag passed to `runEmbeddedPiAgent`. Mid-turn
  drift is still covered by `getLatestPlanMode`, but this matters for
  the first tool call's gate check before the callback fires.

Plus LCM tool allowlist additions (user-reported gap):
`PLAN_MODE_INVESTIGATIVE_TOOL_NAMES` only had `lcm_grep`; the lossless-
claw plugin also exposes `lcm_describe`, `lcm_expand_query`, and
`lcm_expand` (all read-only). Without them in the set, agents using
those tools at planning time triggered premature retry pressure.
Updated the comment block to document the full LCM family catalog,
the prompt example list in `attempt.ts`, and the
`PLAN_MODE_ACK_ONLY_RETRY_INSTRUCTION` user-facing tool list.

Verification:
- pnpm tsgo clean
- 123 scoped tests pass (incomplete-turn, pending-injection,
  mutation-gate, exit-plan-mode-tool)
- pnpm check failure pre-exists on cumulative branch (telegram
  import cycle, unrelated to this change)
Extracts the inline fresh-disk-read pattern from agent-runner-execution.ts
into a dedicated helper `readLatestSessionEntryFresh` and adds a focused
test file that locks in the closure-bypass behavior.

Refactor:
* New `src/auto-reply/reply/fresh-session-entry.ts` (~50 LoC): pure
  helper with no side effects, documented contract (returns LIVE entry
  when disk has fresher state, falls back to closure entry when
  storePath/sessionKey missing or load fails — pure superset semantic).
* `agent-runner-execution.ts` Edit 1 + Edit 2 sites collapse from
  inline IIFEs (~30 LoC each) to single helper calls. Same runtime
  behavior, less code, single point of truth for the fresh-read
  pattern.

Test coverage (9 tests, all green):
* Live wins over stale planMode.mode (the original bug scenario)
* Live wins over stale recentlyApprovedAt (yield-detector path)
* Live wins over stale pendingAgentInjection (PR-15 injection path)
* Falls back to closure when storePath / sessionKey / store-entry
  missing
* Falls back to closure when loadSessionStore throws (corrupt JSON,
  ENOENT)
* Pure-superset guarantee (multi-field fallback fully preserved on
  load failure)
* Returns undefined when no fallback and store empty

If any of these tests start failing, the closure-stale-ref class of
bug is back — do NOT silence the test, find the regression in
loadSessionStore's skipCache contract or in the helper.

Verification:
- pnpm tsgo clean
- 132 scoped tests pass (111 unit-fast + 9 helper + 12 agents)
Live webchat testing surfaced 4 issues; addresses all of them in one
commit. Touched-surface tests pass (180 across 7 test files); tsgo
clean.

5 retry-instruction constants in `incomplete-turn.ts` and the plan-
nudge wake-up text in `plan-nudge-crons.ts` lacked the `[PLAN_*]:`
first-line tag that `[PLAN_DECISION]:` / `[QUESTION_ANSWER]:` /
`[PLAN_COMPLETE]:` already used. Added:

  • PLAN_MODE_ACK_ONLY_RETRY_INSTRUCTION{,_FIRM} → `[PLAN_ACK_ONLY]:`
  • PLAN_APPROVED_YIELD_RETRY_INSTRUCTION{,_FIRM} → `[PLAN_YIELD]:`
  • PLANNING_ONLY_RETRY_INSTRUCTION{,_FIRM,_FINAL} → `[PLANNING_RETRY]:`
  • plan-nudge wake-up text → `[PLAN_NUDGE]:`

Sets up a future PR to hide them from user-visible chat with a single
regex. Existing tests assert by constant identity (not literal value)
so they still pass.

Root cause: `SessionEntry.planMode` had NO `title` field. The
`exit_plan_mode` tool emits the title in `agent_approval_event` but
the plan-snapshot-persister never captured it; the UI side panel at
`ui/src/ui/app.ts:265` fell back to "Active plan" generic label.

Fix:
* Added `planMode.title` + `planMode.approvalRunId` fields to
  `SessionEntry` (also mirrored in `PlanModeSessionState`).
* `plan-snapshot-persister.ts` adds a second `onAgentEvent("approval")`
  listener that captures `title` + parent `runId` + `approvalId` from
  the event and writes them to `SessionEntry.planMode` via direct
  in-place store mutation (server-internal metadata; per
  `protocol/CLAUDE.md`, no wire schema change needed since the
  existing sessions.list response already carries the full planMode
  object to the UI).
* UI `buildPlanViewMarkdown` accepts a new `title?` param that takes
  precedence over `summary` for the header. All 3 call sites
  (`hydratePlanViewFromSession`, `refreshLivePlanSidebar`,
  `openPlanInSidebar`) read `row.planMode.title` and pass it.
* Pre-`exit_plan_mode` state (only `update_plan` has fired): header
  shows `(planning)` — honest signal that no real title exists yet.
* Post-`exit_plan_mode`: header anchors on the agent-supplied title
  through every lifecycle phase (planning → submitted → approved →
  executing → completed).

The persisted markdown filename pattern `plan-YYYY-MM-DD-<slug>.md`
already worked (verified in `plan-archetype-prompt.ts:164`); no
change there.

Root cause: tool-side gate at `exit-plan-mode-tool.ts:230` reads
live `openSubagentRunIds` at submission time; if a NEW subagent
spawns during the user's approval window between the card showing
and the user clicking Approve, the original gate is already passed
and the approval handler at `sessions-patch.ts:572` had NO subagent
check — agent would proceed mid-flight subagent results.

Server fix (`sessions-patch.ts`):
* Reads `next.planMode.approvalRunId` (set by Bug 2 persister) for
  `approve`/`edit` actions and looks up parent ctx via
  `getAgentRunContext`. Throws new `PLAN_APPROVAL_BLOCKED_BY_SUBAGENTS`
  ErrorCode if `openSubagentRunIds.size > 0`. `reject` is NOT gated
  (negative feedback always accepted).
* Extended `invalid()` helper to accept optional code + details so
  the error shape carries `openSubagentRunIds` for the UI to
  display in a toast tooltip.
* Added `ErrorCode` type re-export from `protocol/index.ts` and
  `ErrorCodes.PLAN_APPROVAL_BLOCKED_BY_SUBAGENTS` to the closed
  enum.

UI fix (`app.ts` + `app-tool-stream.ts` + `chat.ts`):
* New `SubagentBlockingStatus` type + `@state() subagentBlockingStatus`
  field. Mirrors `FallbackStatus` shape.
* `handlePlanApprovalDecision` catches the error code (also matches
  on message-substring as a transport fallback), restores the
  approval card so the user can retry, sets
  `subagentBlockingStatus` and schedules an 8s auto-clear.
* New `renderSubagentBlockingIndicator` in `chat.ts` renders the
  toast in the same bottom region as `renderFallbackIndicator`
  (the model-fallback toast — the only toast surface the user has
  reported actually seeing in webchat). CSS class
  `compaction-indicator--fallback` reused.
* Wording: "Subagents still running — try again after subagent
  results return" (per user feedback — clearer than the earlier
  "try again in a moment" draft).

7 new tests in `sessions-patch.subagent-gate.test.ts` cover the
gate (allow/block per action, fallthrough conditions, error format
including the truncated "and N more" suffix).

Opt-in `OPENCLAW_DEBUG_PLAN_MODE=1` env var that turns on detailed
plan-mode lifecycle logging at every state transition / gate
decision / tool call / synthetic injection / approval event. Off by
default (zero perf impact via short-circuit on env-var check).

* New `src/agents/plan-mode/plan-mode-debug-log.ts` (~115 LoC) with
  discriminated event union (`PlanModeDebugEvent` — 8 kinds).
  `logPlanModeDebug(event)` no-ops when env unset, otherwise
  emits via `createSubsystemLogger("plan-mode")` to
  `gateway.err.log` with `[plan-mode/<kind>]` tag prefix.
* 12 tests in `plan-mode-debug-log.test.ts` cover env-var gate
  (including late-set env), serialization shape per event kind,
  and the no-op fast path.
* Instrumentation at the high-value sites:
  - `sessions-patch.ts` — state_transition (planMode flip),
    approval_event (success + subagent-gate rejection),
    synthetic_injection ([PLAN_DECISION]: write)
  - `exit-plan-mode-tool.ts` — gate_decision (subagent in-flight
    block + allow path)
  - `plan-snapshot-persister.ts` — tool_call (exit_plan_mode →
    title persist trigger)

Activation:

  OPENCLAW_DEBUG_PLAN_MODE=1 launchctl kickstart -k gui/$UID/ai.openclaw.gateway
  tail -F ~/.openclaw/logs/gateway.err.log | grep '\[plan-mode/'

`docs/plans/PLAN-MODE-ARCHITECTURE.md` bumped with a new "Live
testing iteration 1" section listing all 4 fixes + the activation
instructions for Bug 4 debug logging. Long-form rollout history
stays in that doc as the cross-session checkpoint.

* pnpm tsgo clean
* 180 scoped tests pass:
  - 12 plan-mode-debug-log
  - 7 sessions-patch.subagent-gate (NEW)
  - 29 incomplete-turn (with new prefixes — assertions still pass
    because tests import constants by reference)
  - 41 sessions-patch (existing)
  - 12 mutation-gate
  - 30 exit-plan-mode-tool
  - 9 fresh-session-entry
  - misc utility tests
* pnpm check failure pre-exists on this branch (telegram extension
  import cycle, unrelated)
…ng fixes

Live webchat testing of iter-1 (commit 3024c6b) surfaced 6 deeper
edge cases. This commit addresses Bugs A, C, D, E, F (Bug B server-
guard + UI auto-dismiss deferred to Phase 2). Touched-surface tests
pass (120 across 6 test files); tsgo clean.

## Bug A — getLatestPlanMode false-positives after planMode deletion (THE big one)

Root cause traced via log + code:
  sessions-patch.ts:751 DELETES `next.planMode` entirely on
  approve/edit (when no autoApprove flag to preserve)
  ↓
  getLatestPlanMode returned `undefined` for the missing-planMode case
  ↓
  Consumers used `?? args.ctx.planMode` fallback chain
  ↓
  fallback returned the STALE "plan" snapshot from run-start
  ↓
  mutation gate kept blocking write/edit AFTER approval; ack-only
  detector kept firing post-approval; agent stuck in retry loop
  → "incomplete turn detected, surfacing error to user"

Fix:
* New `resolveLatestPlanModeFromDisk` helper in `fresh-session-entry.ts`
  encodes the deletion-as-normal contract: when entry exists but
  planMode object is missing, return "normal" (NOT undefined).
  Returns undefined ONLY when the disk truly can't be read (missing
  path/key, throw, no entry for key).
* Both `getLatestPlanMode` callbacks in `agent-runner-execution.ts`
  refactored to use the helper. Net -73 LoC across the two callbacks
  (collapsed inline IIFEs + duplicate logic).
* Defense-in-depth: `pi-tools.before-tool-call.ts:230` and
  `run.ts:1834` consumers now explicit-check `liveMode !== undefined`
  instead of `??` so a future helper change can't reintroduce the
  stale-fallback bug. Cached snapshot is reserved for true
  disk-failure cases only.
* 8 new tests in `fresh-session-entry.test.ts` lock in the
  deletion-as-normal contract.

## Bug E — [PLAN_DECISION] format inconsistency

Two emission sites had two different formats:
* `types.ts:buildPlanDecisionInjection` (block format on reject/timeout)
* `sessions-patch.ts:606` (one-line on approve/edit)

Unified on the one-line `[PLAN_DECISION]: <decision>` opener so it
matches every other plan-mode synthetic message ([QUESTION_ANSWER]:,
[PLAN_COMPLETE]:, [PLAN_ACK_ONLY]:, [PLAN_YIELD]:, [PLAN_NUDGE]:,
[PLANNING_RETRY]:). Future "hide PLAN_* in webchat" filter can be a
single regex. Adversarial-feedback sanitization (closing-marker
neutralization) preserved.

6 tests updated in `approval.test.ts` to assert the one-line opener.

## Bug F — Tool description + agent guidance

Agent demonstrably:
* Posted assistant text AFTER `exit_plan_mode` in same turn (Bug A
  trigger surface)
* Confused `update_plan` with `exit_plan_mode`
* Read multi-MB rolling logs from line 1 instead of tailing

Updated:
* `describeExitPlanModeTool`: STOP-AFTER-TOOL-CALL is the FIRST
  description sentence.
* `describeUpdatePlanTool`: TRACKING-ONLY clarification.
* `describeEnterPlanModeTool`: explicit lifecycle pattern.
* `attempt.ts:549-557`: log-triage hygiene + "no chat after
  exit_plan_mode" reminder.

## Bug D — Config-flag activation for plan-mode debug log

`OPENCLAW_DEBUG_PLAN_MODE=1` set via `launchctl setenv` produced ZERO
`[plan-mode/...]` lines in iter-1 testing. Root cause: macOS
`launchctl setenv` only affects FUTURE launchd-spawned processes,
not running children of the OpenClaw Mac app.

Fix:
* New config field `agents.defaults.planMode.debug?: boolean`.
* `plan-mode-debug-log.ts:isDebugEnabled` now reads BOTH env var and
  config (env wins). Fail-closed on config load errors.
* 5 new tests cover the config-flag path.

Activation:

  openclaw config set agents.defaults.planMode.debug true
  launchctl kickstart -k gui/$UID/ai.openclaw.gateway
  tail -F ~/.openclaw/logs/gateway.err.log | grep '\[plan-mode/'

## Bug C — Always-on diagnostic log at approval-side subagent gate

iter-1 added the gate but Eva couldn't tell from observation whether
the gate fired (toast didn't visibly surface). With debug log silent
(Bug D), there was no way to verify gate firings.

Fix:
* New always-on subsystem logger `gateway/plan-approval-gate` emits
  `gate decision: action=approve sessionKey=… approvalRunId=…
  openSubagents=N result=blocked|allowed` on every approve/edit.
* Also emits `gate disabled: …` warn when `approvalRunId` not
  persisted.

## Architecture doc

`docs/plans/PLAN-MODE-ARCHITECTURE.md` updated with iter-2 findings
table + activation instructions for the config-flag debug path.

## Verification

* pnpm tsgo clean
* 120 scoped tests pass:
  - 17 fresh-session-entry (9 + 8 new)
  - 17 plan-mode-debug-log (12 + 5 new)
  - 32 approval (26 + 6 updated)
  - 7 sessions-patch.subagent-gate
  - 29 incomplete-turn
  - 18 exit-plan-mode-tool

## Known follow-up (Phase 2 — not in this commit)

Bug B (stale approval card after planMode auto-cleared, double-popup,
"requires an active plan-mode session" raw error) needs UI work +
new PLAN_APPROVAL_EXPIRED error code. Deferred to next commit so
this stays focused on the runtime correctness fix (Bug A).
The TypeScript type for `agents.defaults.planMode.debug` was added in
98ac5e1 but the runtime Zod validation schema in
`zod-schema.agent-defaults.ts` rejected the field with
`Unrecognized key: 'debug'`. Added the matching `z.boolean().optional()`
entry so `openclaw config set agents.defaults.planMode.debug true`
validates and persists.

Verified end-to-end: `openclaw config set` accepts and writes the value
after this fix.
Closes the user-reported meta-gap: "will plan mode work reliably for
ANY install, on ANY agent, including agents that just installed the
patch and have never seen plan mode before?" Audit found 0/5 score
on bootstrap, skills, user docs, and diagrams — agent had a 2-turn
learning curve relying on the in-mode prompt only.

Phase 1 ships 5 self-discovery deliverables; Phase 2 (self-test
slash command + introspection tool) and Phase 3 (robustness fixes)
land in follow-up commits.

## D1 — Plan-mode reference card injected on every in-mode turn

New `src/agents/plan-mode/reference-card.ts` (~140 LoC) — compact
ASCII state-machine diagram + tool contract one-liners + [PLAN_*]:
tag taxonomy + /plan slash-command surface + common pitfalls +
debugging tips.

Wired into `src/agents/pi-embedded-runner/run/attempt.ts:567` so
the card sits alongside PLAN_ARCHETYPE_PROMPT in the in-mode
system prompt. Eliminates the 2-turn learning curve: agent sees
the full reference on EVERY in-mode turn from turn 1.

## D2 — One-shot first-time intro injection

When `sessions.patch { planMode: "plan" }` fires AND the session
has never seen plan mode before (no `planModeIntroDeliveredAt`
marker on `SessionEntry`), write a `[PLAN_MODE_INTRO]: ...`
synthetic message to `pendingAgentInjection`. Agent's next turn
opens with a quick lifecycle overview + pointer to /plan
self-test.

* New `SessionEntry.planModeIntroDeliveredAt?: number` field
  (root level, survives planMode delete on approve/edit)
* Wired in `src/gateway/sessions-patch.ts` planMode→"plan"
  transition (around line 502)
* One-shot semantic: subsequent enter_plan_mode calls in the
  same session skip the intro
* Composes cleanly with existing `pendingAgentInjection`
  consumers (e.g. [QUESTION_ANSWER]: from a prior turn) — appends
  rather than overwriting

## D3 — Tool descriptions point at the reference card + self-test

Appended to all 3 plan-mode tool descriptions in
`tool-description-presets.ts`:
"For the full plan-mode reference (state diagram, [PLAN_*]: tag
taxonomy, /plan slash commands, common pitfalls, debugging tips):
see the bootstrap-injected reference card visible on every
in-mode turn. Verify your local install with `/plan self-test`."

Gives the agent a discoverable pointer even before it enters plan
mode (tools are visible in normal mode too).

## D4 — User-facing concept doc

New `docs/concepts/plan-mode.md` (~140 LoC) — product-facing "plan
mode for users" doc following the same frontmatter style as
`docs/concepts/context-engine.md`. Covers: when to use, lifecycle,
slash commands, multi-channel behavior, persisted plans,
auto-approve mode, subagent gating, troubleshooting.

Distinct from `docs/plans/PLAN-MODE-ARCHITECTURE.md` (internal
architecture + iteration history). Also discoverable to agents
that grep `docs/`.

## D7 — `plan-mode-101` skill

New `skills/plan-mode-101/SKILL.md` — same content as the
in-mode reference card (D1) but in skill format with frontmatter
description. Trigger phrases: "explain plan mode", "test plan
mode", "plan mode help", "why was my tool blocked in plan mode",
"what does [PLAN_DECISION] mean", "what does /plan accept do".

The skill works in NORMAL mode (where the in-mode reference
card isn't injected), so an agent that's asked "explain plan
mode" or "what does [PLAN_NUDGE] mean" can invoke the skill
on-demand even before it enters plan mode.

## Verification

* pnpm tsgo clean
* 85 scoped tests pass (44 plan-archetype + 41 sessions-patch)
* Reference card content + skill content + concept doc kept in
  sync (manual; future iter could automate via codegen)

## Known follow-ups

* Phase 2: `/plan self-test` slash command + `plan_mode_status`
  introspection tool (deferred to next commit for review clarity)
* Phase 3: Top 5 robustness fixes from the failure-mode audit
  (subagent crash drainage, cron-nudge suppression, title XSS
  audit, disk-full graceful error, multi-channel approval dedup
  test) — also deferred
* Bug B (stale approval card UI auto-dismiss + new error code) —
  still deferred from iter-2; targeted for iter-4
…n tool

Closes the live-test bug the user reported on iter-2 build: subagent
spawned during plan-mode investigation, agent called exit_plan_mode
while subagent still running, gate didn't fire, approval card showed,
user clicked Approve, agent stalled because subagent return arrived
AFTER approval and the agent narrated the result instead of
continuing the plan-mode flow.

## R6a — Always-on diagnostic at exit_plan_mode subagent gate

Symptom: silent gate bypass. Iter-1 added the gate but only logged
via the env-gated plan-mode debug log; bypass cases (no runId, ctx
not registered, openSubagentRunIds undefined) left no trace, so we
couldn't tell from logs whether the gate fired and was empty vs.
silently skipped.

Fix: new always-on `agents/exit-plan-gate` subsystem logger emits
ONE diagnostic line for EVERY exit_plan_mode call:

  agents/exit-plan-gate gate decision: result=blocked|allowed
    runId=<id> sessionKey=<key> openSubagents=N
    reason=<one of: gated|no runId|ctx not registered|
      openSubagentRunIds undefined|empty>

Now operators can grep `agents/exit-plan-gate` in gateway.err.log
to see every submission attempt + the precise reason the gate did
or didn't fire. This is the diagnostic prerequisite for proving (or
disproving) future race-window hypotheses.

## R6b — Subagent announce-turn injection plan-mode-aware

Symptom: agent narrated subagent result and stopped (TERMINAL turn),
breaking the plan-mode flow. The standard announce reply instruction
at `subagent-announce.ts:buildAnnounceReplyInstruction` told the
agent "send a user-facing update now" — agent read that as "this is
your final action this turn" and didn't call exit_plan_mode after
incorporating the result.

Fix: announce reply instruction is now plan-mode-aware. When the
requester session's `planMode.mode === "plan"`, an explicit suffix
is appended:

  "You are currently in PLAN MODE — do not stop after the
   user-facing update. Your next action MUST be either: (a) call
   `exit_plan_mode(...)` if this subagent's result completes your
   investigation, OR (b) continue investigation with another
   read-only tool call. Trailing chat alone is treated as yielding
   without acting and will trigger a [PLAN_ACK_ONLY] retry."

Read at announce-build time from `loadRequesterSessionEntry`'s
entry.planMode.mode. Best-effort lookup; falls back to default
instruction (no plan-mode suffix) if the lookup throws.

## D6 — `plan_mode_status` introspection tool

Symptom: agent had to INFER plan-mode state from tool-rejection
errors. No way to programmatically check "am I in plan mode?" /
"what's the title?" / "how many subagents are in flight?" / "is the
debug log enabled?".

Fix: new read-only tool `plan_mode_status` that returns a
structured snapshot of every plan-mode field plus the debug-log
status. Self-resolves storePath via `resolveDefaultSessionStorePath`
so it works without storePath plumbing through the registry. Wired
into the bundled toolset in `openclaw-tools.ts:283` alongside
`enter_plan_mode` / `exit_plan_mode` / `ask_user_question`.

Returns:
  - inPlanMode, approval, title, approvalRunId
  - planStepCount, openSubagentCount, openSubagentRunIds[]
  - recentlyApprovedAt, pendingAgentInjectionPreview
  - planModeIntroDeliveredAt, autoApprove, debugLogEnabled

Used by:
  - Agent self-diagnosis ("what's my plan-mode state?")
  - Future `/plan self-test` slash command (D5, deferred)
  - Future debugging skills + workflows

## Verification

* pnpm tsgo clean
* 18 scoped tests pass on exit-plan-mode-tool

## Deferred to iter-3 commit 3 (next focused commit)

* D5 — `/plan self-test` slash command
* R1 — Subagent cleanup on crash/timeout (drain openSubagentRunIds
  on error paths so the gate doesn't deadlock indefinitely)
* R2 — Cron-nudge suppression when approval pending (heartbeat path
  already does this via `buildActivePlanNudge:742`; cron-fire path
  needs the same check)
* R3 — Plan title XSS sanitization audit + test
* R4 — Disk-full graceful error in sessions-patch.ts
* R5 — Multi-channel approval dedup test
* Bug B (still deferred from iter-2) — stale approval card UI
  auto-dismiss + new PLAN_APPROVAL_EXPIRED error code

## Architecture doc

`docs/plans/PLAN-MODE-ARCHITECTURE.md` updated with iter-3 status
section listing both Phase 1 (already shipped in c262bff) and
the R6+D6 deliverables in this commit.
…wlist fixes

User-reported live test (17:54-17:58) surfaced TWO critical bugs
plus exposed that iter-1 R3 (approval-side subagent gate) had been
silently broken since landing.

## X1 — plan_mode_status blocked by mutation gate (regression I just introduced)

Iter-3 commit 2 (`ffbdd7cd58`) added the new `plan_mode_status`
introspection tool but did NOT add it to
`PLAN_MODE_ALLOWED_TOOLS`. The mutation gate's default-deny then
blocked the tool in plan mode — the most important place to use it.

User hit this exact scenario: agent called `plan_mode_status`
mid-pending-approval to self-diagnose, got
"Tool 'plan_mode_status' is not in the plan-mode allowlist and is
blocked by default. Call exit_plan_mode to proceed."

Fix: added `plan_mode_status` to `PLAN_MODE_ALLOWED_TOOLS` in
`mutation-gate.ts`. Also added `sessions_list` and `sessions_history`
(read-only sessions tools that are useful during plan-mode
investigation and shouldn't require allowlist gymnastics).

## X2 — Persister phase typo silently broke iter-1 R3 + Bug 2 entirely

ROOT CAUSE for the user's earlier live-test failures (Bug 3 from
iter-1, R6 from iter-3 commit 2 retest) was a SILENT TYPO in the
plan-snapshot-persister's approval-event filter:

  Filter checked:  phase === "request"
  Actual emit was: phase === "requested"  (past tense)

The phase enum is `AgentApprovalEventPhase = "requested" | "resolved"`
(see `agent-events.ts:90`). The persister listener silently skipped
EVERY plan submission because `"requested" !== "request"`. Cascading
effects:

* `SessionEntry.planMode.title` was NEVER persisted to disk —
  side panel showed "(unset)", `plan_mode_status` reported
  `title="(unset)"` even right after `exit_plan_mode` submitted
  with a real title.

* `SessionEntry.planMode.approvalRunId` was NEVER persisted to
  disk — which means the iter-1 approval-side subagent gate at
  `sessions-patch.ts:620+` always fell through silently. The gate
  reads `next.planMode.approvalRunId` to look up the parent's
  `openSubagentRunIds` via `getAgentRunContext`. With approvalRunId
  undefined, the gate emits the "approvalRunId not persisted (Bug 2
  wiring may be missing)" warn line and proceeds without checking
  subagents.

So iter-1 R3 (the entire approval-side subagent gate) HAS BEEN
SILENTLY BROKEN since iter-1 landed. Tests passed because they
manually injected `approvalRunId` into test fixtures; production
NEVER saw the field populated.

Fix: persister filter now accepts BOTH `"requested"` (canonical) AND
`"request"` (defensive, in case the event shape ever changes back).

Smoking-gun evidence from user's live test:
  17:54: enter_plan_mode → spawn subagent → yield (1m14s)
  17:55: subagent completes (drained from openSubagentRunIds)
  17:56: subagent announce arrives at parent
  17:56: parent narrates result + calls exit_plan_mode
         → approval card appears
         → "Plan submitted for approval — Test plan-mode child-return
            continuation (4 steps)."
  17:56: agent calls plan_mode_status
         → output: title="(unset)" ← SMOKING GUN
         (title should have been "Test plan-mode child-return continuation")

This was the persister silently failing for the entire iter-1 +
iter-2 lifetime. Fixed now.

## What this cascade-fixes

* iter-1 Bug 2 (title persistence) — ACTUALLY works now
* iter-1 Bug 3 / R3 (approval-side subagent gate) — ACTUALLY works
  now (approvalRunId is finally populated for the gate to look up)
* iter-3 R6 (subagent gate hardening) — already-shipped diagnostic
  log + announce-aware reply suffix still apply, but the underlying
  gate logic now has its required input data
* User's live-test stall — likely root cause was the cascade of
  X2 (gate fell through silently → approve let through with stale
  state). Should be substantially better after this commit; retest
  needed to confirm.

## Verification

* pnpm tsgo clean
* 82 scoped tests pass (mutation-gate + exit-plan-mode-tool)

## Still deferred

X3 (announce-race micro-window): with X2 fixed, the approval-side
gate now actually fires; this provides a second-line defense against
the subagent-spawned-during-approval-window race. The pure
"subagent drained but announce not yet delivered" race is largely
academic now — the parent's wake-up IS the announce delivery, so
by the time the parent CAN call exit_plan_mode, the announce has
been processed.

X4 (post-approve stall): need a fresh test with X1+X2 fixed to see
if the stall persists. Likely it was a downstream effect of X2 (no
title, no approvalRunId → confused state).

D5, R1, R2, R3, R4, R5, Bug B: still deferred to follow-up commits.
Two type errors surfaced by pnpm tsgo after rebase + one lint
cleanup:

* src/agents/plan-mode/plan-archetype-bridge.ts:201 referenced
  ../../plugin-sdk/telegram.js which upstream removed in the
  plugin-sdk restructure. PR-14 (Telegram visibility) deferred to
  follow-up; markdown plan file still persists, only Telegram
  document upload skipped (with warn log line).

* src/gateway/server-runtime-subscriptions.ts:82 referenced
  params.minimalTestGateway which doesn't exist on the param type.
  Removed the conditional; persister always starts now.

* parseTelegramThreadId helper preserved in comments so PR-14
  re-wire follow-up can resurrect it without rewriting parsing.

Verification: pnpm tsgo clean, scoped plan-mode tests pass (81
across 5 files).
@100yenadmin
100yenadmin force-pushed the feat/plan-channel-parity branch from bee5e8c to 01ed636 Compare April 19, 2026 11:41
@100yenadmin

Copy link
Copy Markdown
Contributor Author

Closing in favor of consolidated PR (to be opened immediately).

Rationale: this branch was 734 commits behind upstream/main and the PR's review feedback was firing against a stale base. Rebased the full 135-commit feature work onto upstream/main @ v2026.4.19-beta.2 (only 5 conflicts to resolve, all incidental file overlaps — no semantic feature collision since plan mode is exclusively our work) and consolidated the 10-PR series into a single umbrella PR for cleaner bot review + faster maintainer context-loading.

The full iteration history + decision log lives in docs/plans/PLAN-MODE-ARCHITECTURE.md on the new branch. All reviewed-and-resolved threads from this PR are honored — see the architecture doc for what landed where.

Plus this PR's branch (feat/plan-channel-parity) is being reused as the consolidated umbrella branch; the new umbrella PR ships from the same branch but as a fresh PR slot to escape stale-base review noise.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 01ed63633e

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +233 to +234
const allowEdits = raw === "accept edits" || raw === "accept edit";
try {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Tokenize /plan accept edits before choosing action

The web slash executor treats allowEdits as true only for exact strings ("accept edits" / "accept edit"), so valid variants like /plan accept edits or /plan accept edits please fall back to action: "approve". That silently changes user intent from “accept with edits” to plain approve, which can block expected plan adjustments after approval. The backend parser already tokenizes this command, so the web path should match that behavior.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agents Agent runtime and tooling app: web-ui App: web-ui channel: telegram Channel integration: telegram docs Improvements or additions to documentation extensions: openai gateway Gateway runtime size: XL

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants