Something went wrong. Try again.
source dump of claude code forked from oppi.li/claude-code
Something went wrong. Try again.
13 kB · 349 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350// CCR session polling for /ultraplan. Waits for an approved ExitPlanMode// tool_result, then extracts the plan text. Uses pollRemoteSessionEvents// (shared with RemoteAgentTask) for pagination + typed SDKMessage[].// Plan mode is set via set_permission_mode control_request in// teleportToRemote's CreateSession events array.
import type { ToolResultBlockParam, ToolUseBlock,} from '@anthropic-ai/sdk/resources'import type { SDKMessage } from '../../entrypoints/agentSdkTypes.js'import { EXIT_PLAN_MODE_V2_TOOL_NAME } from '../../tools/ExitPlanModeTool/constants.js'import { logForDebugging } from '../debug.js'import { sleep } from '../sleep.js'import { isTransientNetworkError } from '../teleport/api.js'import { type PollRemoteSessionResponse, pollRemoteSessionEvents,} from '../teleport.js'
const POLL_INTERVAL_MS = 3000// pollRemoteSessionEvents doesn't retry. A 30min poll makes ~600 calls;// at any nonzero 5xx rate one blip would kill the run.const MAX_CONSECUTIVE_FAILURES = 5
export type PollFailReason = | 'terminated' | 'timeout_pending' | 'timeout_no_plan' | 'extract_marker_missing' | 'network_or_unknown' | 'stopped'
export class UltraplanPollError extends Error { constructor( message: string, readonly reason: PollFailReason, readonly rejectCount: number, options?: ErrorOptions, ) { super(message, options) this.name = 'UltraplanPollError' }}
// Sentinel string the browser PlanModal includes in the feedback when the user// clicks "teleport back to terminal". Plan text follows on the next line.export const ULTRAPLAN_TELEPORT_SENTINEL = '__ULTRAPLAN_TELEPORT_LOCAL__'
export type ScanResult = | { kind: 'approved'; plan: string } | { kind: 'teleport'; plan: string } | { kind: 'rejected'; id: string } | { kind: 'pending' } | { kind: 'terminated'; subtype: string } | { kind: 'unchanged' }
/** * Pill/detail-view state derived from the event stream. Transitions: * running → (turn ends, no ExitPlanMode) → needs_input * needs_input → (user replies in browser) → running * running → (ExitPlanMode emitted, no result yet) → plan_ready * plan_ready → (rejected) → running * plan_ready → (approved) → poll resolves, pill removed */export type UltraplanPhase = 'running' | 'needs_input' | 'plan_ready'
/** * Pure stateful classifier for the CCR event stream. Ingests SDKMessage[] * batches (as delivered by pollRemoteSessionEvents) and returns the current * ExitPlanMode verdict. No I/O, no timers — feed it synthetic or recorded * events for unit tests and offline replay. * * Precedence (approved > terminated > rejected > pending > unchanged): * pollRemoteSessionEvents paginates up to 50 pages per call, so one ingest * can span seconds of session activity. A batch may contain both an approved * tool_result AND a subsequent {type:'result'} (user approved, then remote * crashed). The approved plan is real and in threadstore — don't drop it. */export class ExitPlanModeScanner { private exitPlanCalls: string[] = [] private results = new Map<string, ToolResultBlockParam>() private rejectedIds = new Set<string>() private terminated: { subtype: string } | null = null private rescanAfterRejection = false everSeenPending = false
get rejectCount(): number { return this.rejectedIds.size }
/** * True when an ExitPlanMode tool_use exists with no tool_result yet — * the remote is showing the approval dialog in the browser. */ get hasPendingPlan(): boolean { const id = this.exitPlanCalls.findLast(c => !this.rejectedIds.has(c)) return id !== undefined && !this.results.has(id) }
ingest(newEvents: SDKMessage[]): ScanResult { for (const m of newEvents) { if (m.type === 'assistant') { for (const block of m.message.content) { if (block.type !== 'tool_use') continue const tu = block as ToolUseBlock if (tu.name === EXIT_PLAN_MODE_V2_TOOL_NAME) { this.exitPlanCalls.push(tu.id) } } } else if (m.type === 'user') { const content = m.message.content if (!Array.isArray(content)) continue for (const block of content) { if (block.type === 'tool_result') { this.results.set(block.tool_use_id, block) } } } else if (m.type === 'result' && m.subtype !== 'success') { // result(success) fires after EVERY CCR turn // If the remote asks a clarifying question (turn ends without // ExitPlanMode), we must keep polling — the user can reply in // the browser and reach ExitPlanMode in a later turn. // Only error subtypes (error_during_execution, error_max_turns, // etc.) mean the session is actually dead. this.terminated = { subtype: m.subtype } } }
// Skip-scan when nothing could have moved the target: no new events, no // rejection last tick. A rejection moves the newest-non-rejected target. const shouldScan = newEvents.length > 0 || this.rescanAfterRejection this.rescanAfterRejection = false
let found: | { kind: 'approved'; plan: string } | { kind: 'teleport'; plan: string } | { kind: 'rejected'; id: string } | { kind: 'pending' } | null = null if (shouldScan) { for (let i = this.exitPlanCalls.length - 1; i >= 0; i--) { const id = this.exitPlanCalls[i]! if (this.rejectedIds.has(id)) continue const tr = this.results.get(id) if (!tr) { found = { kind: 'pending' } } else if (tr.is_error === true) { const teleportPlan = extractTeleportPlan(tr.content) found = teleportPlan !== null ? { kind: 'teleport', plan: teleportPlan } : { kind: 'rejected', id } } else { found = { kind: 'approved', plan: extractApprovedPlan(tr.content) } } break } if (found?.kind === 'approved' || found?.kind === 'teleport') return found }
// Bookkeeping before the terminated check — a batch can contain BOTH a // rejected tool_result and a {type:'result'}; rejectCount must reflect // the rejection even though terminated takes return precedence. if (found?.kind === 'rejected') { this.rejectedIds.add(found.id) this.rescanAfterRejection = true } if (this.terminated) { return { kind: 'terminated', subtype: this.terminated.subtype } } if (found?.kind === 'rejected') { return found } if (found?.kind === 'pending') { this.everSeenPending = true return found } return { kind: 'unchanged' } }}
export type PollResult = { plan: string rejectCount: number /** 'local' = user clicked teleport (execute here, archive remote). 'remote' = user approved in-CCR execution (don't archive). */ executionTarget: 'local' | 'remote'}
// Returns the approved plan text and where the user wants it executed.// 'approved' scrapes from the "## Approved Plan:" marker (ExitPlanModeV2Tool// default branch) — the model writes plan to a file inside CCR and calls// ExitPlanMode({allowedPrompts}), so input.plan is never in threadstore.// 'teleport' scrapes from the ULTRAPLAN_TELEPORT_SENTINEL in a deny tool_result —// browser sends a rejection so the remote stays in plan mode, with the plan// text embedded in the feedback. Normal rejections (is_error === true, no// sentinel) are tracked and skipped so the user can iterate in the browser.export async function pollForApprovedExitPlanMode( sessionId: string, timeoutMs: number, onPhaseChange?: (phase: UltraplanPhase) => void, shouldStop?: () => boolean,): Promise<PollResult> { const deadline = Date.now() + timeoutMs const scanner = new ExitPlanModeScanner() let cursor: string | null = null let failures = 0 let lastPhase: UltraplanPhase = 'running'
while (Date.now() < deadline) { if (shouldStop?.()) { throw new UltraplanPollError( 'poll stopped by caller', 'stopped', scanner.rejectCount, ) } let newEvents: SDKMessage[] let sessionStatus: PollRemoteSessionResponse['sessionStatus'] try { // Metadata fetch (session_status) is the needs_input signal — // threadstore doesn't persist result(success) turn-end events, so // idle status is the only authoritative "remote is waiting" marker. const resp = await pollRemoteSessionEvents(sessionId, cursor) newEvents = resp.newEvents cursor = resp.lastEventId sessionStatus = resp.sessionStatus failures = 0 } catch (e) { const transient = isTransientNetworkError(e) if (!transient || ++failures >= MAX_CONSECUTIVE_FAILURES) { throw new UltraplanPollError( e instanceof Error ? e.message : String(e), 'network_or_unknown', scanner.rejectCount, { cause: e }, ) } await sleep(POLL_INTERVAL_MS) continue }
let result: ScanResult try { result = scanner.ingest(newEvents) } catch (e) { throw new UltraplanPollError( e instanceof Error ? e.message : String(e), 'extract_marker_missing', scanner.rejectCount, ) } if (result.kind === 'approved') { return { plan: result.plan, rejectCount: scanner.rejectCount, executionTarget: 'remote', } } if (result.kind === 'teleport') { return { plan: result.plan, rejectCount: scanner.rejectCount, executionTarget: 'local', } } if (result.kind === 'terminated') { throw new UltraplanPollError( `remote session ended (${result.subtype}) before plan approval`, 'terminated', scanner.rejectCount, ) } // plan_ready from the event stream wins; otherwise idle session status // means the remote asked a question and is waiting for a browser reply. // requires_action with no pending plan is also needs_input — the remote // may be blocked on a non-ExitPlanMode permission prompt. // CCR briefly flips to 'idle' between tool turns (see STABLE_IDLE_POLLS // in RemoteAgentTask). Only trust idle when no new events arrived — // events flowing means the session is working regardless of the status // snapshot. This also makes needs_input → running snap back on the first // poll that sees the user's reply event, even if session_status lags. const quietIdle = (sessionStatus === 'idle' || sessionStatus === 'requires_action') && newEvents.length === 0 const phase: UltraplanPhase = scanner.hasPendingPlan ? 'plan_ready' : quietIdle ? 'needs_input' : 'running' if (phase !== lastPhase) { logForDebugging(`[ultraplan] phase ${lastPhase} → ${phase}`) lastPhase = phase onPhaseChange?.(phase) } await sleep(POLL_INTERVAL_MS) }
throw new UltraplanPollError( scanner.everSeenPending ? `no approval after ${timeoutMs / 1000}s` : `ExitPlanMode never reached after ${timeoutMs / 1000}s (the remote container failed to start, or session ID mismatch?)`, scanner.everSeenPending ? 'timeout_pending' : 'timeout_no_plan', scanner.rejectCount, )}
// tool_result content may be string or [{type:'text',text}] depending on// threadstore encoding.function contentToText(content: ToolResultBlockParam['content']): string { return typeof content === 'string' ? content : Array.isArray(content) ? content.map(b => ('text' in b ? b.text : '')).join('') : ''}
// Extracts the plan text after the ULTRAPLAN_TELEPORT_SENTINEL marker.// Returns null when the sentinel is absent — callers treat null as a normal// user rejection (scanner falls through to { kind: 'rejected' }).function extractTeleportPlan( content: ToolResultBlockParam['content'],): string | null { const text = contentToText(content) const marker = `${ULTRAPLAN_TELEPORT_SENTINEL}\n` const idx = text.indexOf(marker) if (idx === -1) return null return text.slice(idx + marker.length).trimEnd()}
// Plan is echoed in tool_result content as "## Approved Plan:\n<text>" or// "## Approved Plan (edited by user):\n<text>" (ExitPlanModeV2Tool).function extractApprovedPlan(content: ToolResultBlockParam['content']): string { const text = contentToText(content) // Try both markers — edited plans use a different label. const markers = [ '## Approved Plan (edited by user):\n', '## Approved Plan:\n', ] for (const marker of markers) { const idx = text.indexOf(marker) if (idx !== -1) { return text.slice(idx + marker.length).trimEnd() } } throw new Error( `ExitPlanMode approved but tool_result has no "## Approved Plan:" marker — remote may have hit the empty-plan or isAgent branch. Content preview: ${text.slice(0, 200)}`, )}