fix(cli): preserve permission mode after ExitPlanMode + sidechain UUID chain fix#658
fix(cli): preserve permission mode after ExitPlanMode + sidechain UUID chain fix#658yrom wants to merge 3 commits into
Conversation
When ExitPlanMode is approved, use the current permissionMode as fallback instead of hardcoded 'default', and honor explicit mode selections from the approval response.
…it messages System init/result messages updated sidechainLastUUID but were skipped by the tracer's normalizer, orphaning all subsequent subagent messages including permission request cards in the web UI. Fixes tienn/hapi#587
There was a problem hiding this comment.
Findings
- [Major] Consume auto-approved tool calls before name-only matching —
resolveToolCallId()now takes the first unused call with the same tool name. Auto-approved paths above it (allowedTools, Bash allow rules,bypassPermissions,acceptEdits) return before calling the resolver, so those tool calls remainused: falseuntil their latertool_resultarrives. If Claude emits multiple same-name tools in one turn and an earlier one is auto-approved while a later one needs approval, the later permission request can be attached to the earlier tool id, which releases/records permissions against the wrong tool card and can abort/log the wrong result. Evidence:cli/src/claude/utils/permissionHandler.ts:414.
Suggested fix:Apply the same consumption before the other early allow returns, then add a regression test where the firstprivate consumeToolCallId(name: string, input: unknown): void { this.resolveToolCallId(name, input); } // Before every early allow return, consume the emitted tool_use id too. if (this.allowedBashLiterals.has(inputObj.command)) { this.consumeToolCallId(toolName, input); return { behavior: 'allow', updatedInput: input as Record<string, unknown> }; }
Bashcall is allowed byallowTools/prefix and the second same-nameBashcall requires approval.
Summary
Review mode: initial
One major issue found in the permission resolver change. Sidechain UUID handling looked consistent with the web normalizer/tracer behavior in surrounding code.
Testing
Not run (automation/security: review-only; did not execute PR code).
HAPI Bot
81826bb to
ecfbd74
Compare
There was a problem hiding this comment.
Findings
- [Major] ExitPlanMode approval can restart Claude back in plan mode — normal approval sends no
mode, so when the active mode isplan, the new fallback requeuesPLAN_FAKE_RESTARTwith{ permissionMode: 'plan' }instead of leaving plan mode. That can loop the approved plan back into planning instead of executing it. Evidence:cli/src/claude/utils/permissionHandler.ts:262.
Suggested fix:const nextMode = response.mode && PLAN_EXIT_MODES.includes(response.mode) ? response.mode : this.permissionMode === 'plan' ? 'default' : this.permissionMode; this.session.queue.unshift(PLAN_FAKE_RESTART, { permissionMode: nextMode });
Summary
Review mode: follow-up after new commits
One major issue found in the follow-up diff. The previous bot finding about same-name tool call resolution appears addressed in the current head because resolveToolCallId() is back to input-aware matching.
Testing
Not run (automation/security: review-only; did not execute PR code). Missing regression coverage for approving ExitPlanMode while current permission mode is plan.
HAPI Bot
| } | ||
| const nextMode = response.mode && PLAN_EXIT_MODES.includes(response.mode) | ||
| ? response.mode | ||
| : this.permissionMode; |
There was a problem hiding this comment.
[MAJOR] In the normal ExitPlanMode approval path the web/Telegram/voice callers approve without a mode, so while the session is actually in plan mode this fallback uses this.permissionMode === 'plan' and requeues PLAN_FAKE_RESTART with { permissionMode: 'plan' }. That restarts Claude in planning mode again instead of executing the approved plan.
Suggested fix:
const nextMode = response.mode && PLAN_EXIT_MODES.includes(response.mode)
? response.mode
: this.permissionMode === 'plan'
? 'default'
: this.permissionMode;
this.session.queue.unshift(PLAN_FAKE_RESTART, { permissionMode: nextMode });Add a regression test for approving ExitPlanMode with current mode plan and no response mode.
When approving ExitPlanMode without an explicit mode response, falling back to the current permissionMode would requeue with 'plan', causing the approved plan to loop back into planning instead of executing. Also fixes TS type errors in the test file that caused CI typecheck to fail.
There was a problem hiding this comment.
Findings
- [Major] Sidechain still orphans after visible system events —
api_error/turn-duration/compact system records are not skipped byisClaudeChatVisibleMessage, butnormalizeAgentRecord()converts them torole: 'event'without UUID-bearing agent content, andtraceMessages()only registers UUIDs from sidechain agent messages. AdvancingsidechainLastUUIDto the system event UUID therefore makes the next sidechain assistant point at a UUID the tracer never stores. Evidence:cli/src/claude/utils/sdkToLogConverter.ts:161, related contextweb/src/chat/normalizeAgent.ts:451andweb/src/chat/tracer.ts:73.
Suggested fix:const shouldAdvanceSidechainChain = sdkMessage.type !== 'system'; if (shouldAdvanceSidechainChain) { this.sidechainLastUUID.set((sdkMessage as any).parent_tool_use_id!, uuid); }
Summary
Review mode: follow-up after new commits
One major issue remains in the current sidechain UUID-chain fix. The prior ExitPlanMode finding is addressed in this head.
Testing
Not run (automation/security: review-only; bun is not installed in this runner).
HAPI Bot
| // the tracer can never resolve, orphaning the entire sidechain. | ||
| const isInvisibleToTracer = sdkMessage.type === 'system' | ||
| && ((sdkMessage as SDKSystemMessage).subtype === 'init' || (sdkMessage as SDKSystemMessage).subtype === 'result'); | ||
| if (!isInvisibleToTracer) { |
There was a problem hiding this comment.
[MAJOR] This still advances the sidechain parent map to system records that the tracer cannot resolve. api_error/turn-duration/compact system records are normalized as role: 'event' without UUID-bearing agent content, and traceMessages() only stores UUIDs from sidechain agent messages, so the next sidechain assistant becomes an orphan when its parentUuid is this system UUID.
Suggested fix:
const shouldAdvanceSidechainChain = sdkMessage.type !== 'system';
if (shouldAdvanceSidechainChain) {
this.sidechainLastUUID.set((sdkMessage as any).parent_tool_use_id!, uuid);
}
Summary
ExitPlanModeis approved, use the currentpermissionModeas fallback instead of hardcoded'default', and honor explicit mode selections from the approval response.sidechainLastUUIDbut were skipped by the tracer's normalizer, orphaning all subsequent subagent messages including permission request cards in the web UI. Fixes [BUG] Permission request fails to show for sub-agent runs in Claude Code #587.Test plan
permissionHandler.test.ts— 4 tests (ExitPlanMode default mode preservation + YOLO plan mode)sdkToLogConverter.test.ts— 2 tests for sidechain UUID chain with system init messagescd cli && bunx vitest run src/claude/utils/permissionHandler.test.ts src/claude/utils/sdkToLogConverter.test.ts