Something went wrong. Try again.
source dump of claude code forked from oppi.li/claude-code
Something went wrong. Try again.
56 kB · 1758 lines
TypeScript
at main
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759import type { BetaMessageStreamParams } from '@anthropic-ai/sdk/resources/beta/messages/messages.mjs'import type { Attributes, Meter, MetricOptions } from '@opentelemetry/api'import type { logs } from '@opentelemetry/api-logs'import type { LoggerProvider } from '@opentelemetry/sdk-logs'import type { MeterProvider } from '@opentelemetry/sdk-metrics'import type { BasicTracerProvider } from '@opentelemetry/sdk-trace-base'import { realpathSync } from 'fs'import sumBy from 'lodash-es/sumBy.js'import { cwd } from 'process'import type { HookEvent, ModelUsage } from 'src/entrypoints/agentSdkTypes.js'import type { AgentColorName } from 'src/tools/AgentTool/agentColorManager.js'import type { HookCallbackMatcher } from 'src/types/hooks.js'// Indirection for browser-sdk build (package.json "browser" field swaps// crypto.ts for crypto.browser.ts). Pure leaf re-export of node:crypto —// zero circular-dep risk. Path-alias import bypasses bootstrap-isolation// (rule only checks ./ and / prefixes); explicit disable documents intent.// eslint-disable-next-line custom-rules/bootstrap-isolationimport { randomUUID } from 'src/utils/crypto.js'import type { ModelSetting } from 'src/utils/model/model.js'import type { ModelStrings } from 'src/utils/model/modelStrings.js'import type { SettingSource } from 'src/utils/settings/constants.js'import { resetSettingsCache } from 'src/utils/settings/settingsCache.js'import type { PluginHookMatcher } from 'src/utils/settings/types.js'import { createSignal } from 'src/utils/signal.js'
// Union type for registered hooks - can be SDK callbacks or native plugin hookstype RegisteredHookMatcher = HookCallbackMatcher | PluginHookMatcher
import type { SessionId } from 'src/types/ids.js'
// DO NOT ADD MORE STATE HERE - BE JUDICIOUS WITH GLOBAL STATE
// dev: true on entries that came via --dangerously-load-development-channels.// The allowlist gate checks this per-entry (not the session-wide// hasDevChannels bit) so passing both flags doesn't let the dev dialog's// acceptance leak allowlist-bypass to the --channels entries.export type ChannelEntry = | { kind: 'plugin'; name: string; marketplace: string; dev?: boolean } | { kind: 'server'; name: string; dev?: boolean }
export type AttributedCounter = { add(value: number, additionalAttributes?: Attributes): void}
type State = { originalCwd: string // Stable project root - set once at startup (including by --worktree flag), // never updated by mid-session EnterWorktreeTool. // Use for project identity (history, skills, sessions) not file operations. projectRoot: string totalCostUSD: number totalAPIDuration: number totalAPIDurationWithoutRetries: number totalToolDuration: number turnHookDurationMs: number turnToolDurationMs: number turnClassifierDurationMs: number turnToolCount: number turnHookCount: number turnClassifierCount: number startTime: number lastInteractionTime: number totalLinesAdded: number totalLinesRemoved: number hasUnknownModelCost: boolean cwd: string modelUsage: { [modelName: string]: ModelUsage } mainLoopModelOverride: ModelSetting | undefined initialMainLoopModel: ModelSetting modelStrings: ModelStrings | null isInteractive: boolean kairosActive: boolean // When true, ensureToolResultPairing throws on mismatch instead of // repairing with synthetic placeholders. HFI opts in at startup so // trajectories fail fast rather than conditioning the model on fake // tool_results. strictToolResultPairing: boolean sdkAgentProgressSummariesEnabled: boolean userMsgOptIn: boolean clientType: string sessionSource: string | undefined questionPreviewFormat: 'markdown' | 'html' | undefined flagSettingsPath: string | undefined flagSettingsInline: Record<string, unknown> | null allowedSettingSources: SettingSource[] sessionIngressToken: string | null | undefined oauthTokenFromFd: string | null | undefined apiKeyFromFd: string | null | undefined // Telemetry state meter: Meter | null sessionCounter: AttributedCounter | null locCounter: AttributedCounter | null prCounter: AttributedCounter | null commitCounter: AttributedCounter | null costCounter: AttributedCounter | null tokenCounter: AttributedCounter | null codeEditToolDecisionCounter: AttributedCounter | null activeTimeCounter: AttributedCounter | null statsStore: { observe(name: string, value: number): void } | null sessionId: SessionId // Parent session ID for tracking session lineage (e.g., plan mode -> implementation) parentSessionId: SessionId | undefined // Logger state loggerProvider: LoggerProvider | null eventLogger: ReturnType<typeof logs.getLogger> | null // Meter provider state meterProvider: MeterProvider | null // Tracer provider state tracerProvider: BasicTracerProvider | null // Agent color state agentColorMap: Map<string, AgentColorName> agentColorIndex: number // Last API request for bug reports lastAPIRequest: Omit<BetaMessageStreamParams, 'messages'> | null // Messages from the last API request (ant-only; reference, not clone). // Captures the exact post-compaction, CLAUDE.md-injected message set sent // to the API so /share's serialized_conversation.json reflects reality. lastAPIRequestMessages: BetaMessageStreamParams['messages'] | null // Last auto-mode classifier request(s) for /share transcript lastClassifierRequests: unknown[] | null // CLAUDE.md content cached by context.ts for the auto-mode classifier. // Breaks the yoloClassifier → claudemd → filesystem → permissions cycle. cachedClaudeMdContent: string | null // In-memory error log for recent errors inMemoryErrorLog: Array<{ error: string; timestamp: string }> // Session-only plugins from --plugin-dir flag inlinePlugins: Array<string> // Explicit --chrome / --no-chrome flag value (undefined = not set on CLI) chromeFlagOverride: boolean | undefined // Use cowork_plugins directory instead of plugins (--cowork flag or env var) useCoworkPlugins: boolean // Session-only bypass permissions mode flag (not persisted) sessionBypassPermissionsMode: boolean // Session-only flag gating the .claude/scheduled_tasks.json watcher // (useScheduledTasks). Set by cronScheduler.start() when the JSON has // entries, or by CronCreateTool. Not persisted. scheduledTasksEnabled: boolean // Session-only cron tasks created via CronCreate with durable: false. // Fire on schedule like file-backed tasks but are never written to // .claude/scheduled_tasks.json — they die with the process. Typed via // SessionCronTask below (not importing from cronTasks.ts keeps // bootstrap a leaf of the import DAG). sessionCronTasks: SessionCronTask[] // Teams created this session via TeamCreate. cleanupSessionTeams() // removes these on gracefulShutdown so subagent-created teams don't // persist on disk forever (gh-32730). TeamDelete removes entries to // avoid double-cleanup. Lives here (not teamHelpers.ts) so // resetStateForTests() clears it between tests. sessionCreatedTeams: Set<string> // Session-only trust flag for home directory (not persisted to disk) // When running from home dir, trust dialog is shown but not saved to disk. // This flag allows features requiring trust to work during the session. sessionTrustAccepted: boolean // Session-only flag to disable session persistence to disk sessionPersistenceDisabled: boolean // Track if user has exited plan mode in this session (for re-entry guidance) hasExitedPlanMode: boolean // Track if we need to show the plan mode exit attachment (one-time notification) needsPlanModeExitAttachment: boolean // Track if we need to show the auto mode exit attachment (one-time notification) needsAutoModeExitAttachment: boolean // Track if LSP plugin recommendation has been shown this session (only show once) lspRecommendationShownThisSession: boolean // SDK init event state - jsonSchema for structured output initJsonSchema: Record<string, unknown> | null // Registered hooks - SDK callbacks and plugin native hooks registeredHooks: Partial<Record<HookEvent, RegisteredHookMatcher[]>> | null // Cache for plan slugs: sessionId -> wordSlug planSlugCache: Map<string, string> // Track teleported session for reliability logging teleportedSessionInfo: { isTeleported: boolean hasLoggedFirstMessage: boolean sessionId: string | null } | null // Track invoked skills for preservation across compaction // Keys are composite: `${agentId ?? ''}:${skillName}` to prevent cross-agent overwrites invokedSkills: Map< string, { skillName: string skillPath: string content: string invokedAt: number agentId: string | null } > // Track slow operations for dev bar display (ant-only) slowOperations: Array<{ operation: string durationMs: number timestamp: number }> // SDK-provided betas (e.g., context-1m-2025-08-07) sdkBetas: string[] | undefined // Main thread agent type (from --agent flag or settings) mainThreadAgentType: string | undefined // Remote mode (--remote flag) isRemoteMode: boolean // Direct connect server URL (for display in header) directConnectServerUrl: string | undefined // System prompt section cache state systemPromptSectionCache: Map<string, string | null> // Last date emitted to the model (for detecting midnight date changes) lastEmittedDate: string | null // Additional directories from --add-dir flag (for CLAUDE.md loading) additionalDirectoriesForClaudeMd: string[] // Channel server allowlist from --channels flag (servers whose channel // notifications should register this session). Parsed once in main.tsx — // the tag decides trust model: 'plugin' → marketplace verification + // allowlist, 'server' → allowlist always fails (schema is plugin-only). // Either kind needs entry.dev to bypass allowlist. allowedChannels: ChannelEntry[] // True if any entry in allowedChannels came from // --dangerously-load-development-channels (so ChannelsNotice can name the // right flag in policy-blocked messages) hasDevChannels: boolean // Dir containing the session's `.jsonl`; null = derive from originalCwd. sessionProjectDir: string | null // Cached prompt cache 1h TTL allowlist from GrowthBook (session-stable) promptCache1hAllowlist: string[] | null // Cached 1h TTL user eligibility (session-stable). Latched on first // evaluation so mid-session overage flips don't change the cache_control // TTL, which would bust the server-side prompt cache. promptCache1hEligible: boolean | null // Sticky-on latch for AFK_MODE_BETA_HEADER. Once auto mode is first // activated, keep sending the header for the rest of the session so // Shift+Tab toggles don't bust the ~50-70K token prompt cache. afkModeHeaderLatched: boolean | null // Sticky-on latch for FAST_MODE_BETA_HEADER. Once fast mode is first // enabled, keep sending the header so cooldown enter/exit doesn't // double-bust the prompt cache. The `speed` body param stays dynamic. fastModeHeaderLatched: boolean | null // Sticky-on latch for the cache-editing beta header. Once cached // microcompact is first enabled, keep sending the header so mid-session // GrowthBook/settings toggles don't bust the prompt cache. cacheEditingHeaderLatched: boolean | null // Sticky-on latch for clearing thinking from prior tool loops. Triggered // when >1h since last API call (confirmed cache miss — no cache-hit // benefit to keeping thinking). Once latched, stays on so the newly-warmed // thinking-cleared cache isn't busted by flipping back to keep:'all'. thinkingClearLatched: boolean | null // Current prompt ID (UUID) correlating a user prompt with subsequent OTel events promptId: string | null // Last API requestId for the main conversation chain (not subagents). // Updated after each successful API response for main-session queries. // Read at shutdown to send cache eviction hints to inference. lastMainRequestId: string | undefined // Timestamp (Date.now()) of the last successful API call completion. // Used to compute timeSinceLastApiCallMs in tengu_api_success for // correlating cache misses with idle time (cache TTL is ~5min). lastApiCompletionTimestamp: number | null // Set to true after compaction (auto or manual /compact). Consumed by // logAPISuccess to tag the first post-compaction API call so we can // distinguish compaction-induced cache misses from TTL expiry. pendingPostCompaction: boolean}
// ALSO HERE - THINK THRICE BEFORE MODIFYINGfunction getInitialState(): State { // Resolve symlinks in cwd to match behavior of shell.ts setCwd // This ensures consistency with how paths are sanitized for session storage let resolvedCwd = '' if ( typeof process !== 'undefined' && typeof process.cwd === 'function' && typeof realpathSync === 'function' ) { const rawCwd = cwd() try { resolvedCwd = realpathSync(rawCwd).normalize('NFC') } catch { // File Provider EPERM on CloudStorage mounts (lstat per path component). resolvedCwd = rawCwd.normalize('NFC') } } const state: State = { originalCwd: resolvedCwd, projectRoot: resolvedCwd, totalCostUSD: 0, totalAPIDuration: 0, totalAPIDurationWithoutRetries: 0, totalToolDuration: 0, turnHookDurationMs: 0, turnToolDurationMs: 0, turnClassifierDurationMs: 0, turnToolCount: 0, turnHookCount: 0, turnClassifierCount: 0, startTime: Date.now(), lastInteractionTime: Date.now(), totalLinesAdded: 0, totalLinesRemoved: 0, hasUnknownModelCost: false, cwd: resolvedCwd, modelUsage: {}, mainLoopModelOverride: undefined, initialMainLoopModel: null, modelStrings: null, isInteractive: false, kairosActive: false, strictToolResultPairing: false, sdkAgentProgressSummariesEnabled: false, userMsgOptIn: false, clientType: 'cli', sessionSource: undefined, questionPreviewFormat: undefined, sessionIngressToken: undefined, oauthTokenFromFd: undefined, apiKeyFromFd: undefined, flagSettingsPath: undefined, flagSettingsInline: null, allowedSettingSources: [ 'userSettings', 'projectSettings', 'localSettings', 'flagSettings', 'policySettings', ], // Telemetry state meter: null, sessionCounter: null, locCounter: null, prCounter: null, commitCounter: null, costCounter: null, tokenCounter: null, codeEditToolDecisionCounter: null, activeTimeCounter: null, statsStore: null, sessionId: randomUUID() as SessionId, parentSessionId: undefined, // Logger state loggerProvider: null, eventLogger: null, // Meter provider state meterProvider: null, tracerProvider: null, // Agent color state agentColorMap: new Map(), agentColorIndex: 0, // Last API request for bug reports lastAPIRequest: null, lastAPIRequestMessages: null, // Last auto-mode classifier request(s) for /share transcript lastClassifierRequests: null, cachedClaudeMdContent: null, // In-memory error log for recent errors inMemoryErrorLog: [], // Session-only plugins from --plugin-dir flag inlinePlugins: [], // Explicit --chrome / --no-chrome flag value (undefined = not set on CLI) chromeFlagOverride: undefined, // Use cowork_plugins directory instead of plugins useCoworkPlugins: false, // Session-only bypass permissions mode flag (not persisted) sessionBypassPermissionsMode: false, // Scheduled tasks disabled until flag or dialog enables them scheduledTasksEnabled: false, sessionCronTasks: [], sessionCreatedTeams: new Set(), // Session-only trust flag (not persisted to disk) sessionTrustAccepted: false, // Session-only flag to disable session persistence to disk sessionPersistenceDisabled: false, // Track if user has exited plan mode in this session hasExitedPlanMode: false, // Track if we need to show the plan mode exit attachment needsPlanModeExitAttachment: false, // Track if we need to show the auto mode exit attachment needsAutoModeExitAttachment: false, // Track if LSP plugin recommendation has been shown this session lspRecommendationShownThisSession: false, // SDK init event state initJsonSchema: null, registeredHooks: null, // Cache for plan slugs planSlugCache: new Map(), // Track teleported session for reliability logging teleportedSessionInfo: null, // Track invoked skills for preservation across compaction invokedSkills: new Map(), // Track slow operations for dev bar display slowOperations: [], // SDK-provided betas sdkBetas: undefined, // Main thread agent type mainThreadAgentType: undefined, // Remote mode isRemoteMode: false, ...(process.env.USER_TYPE === 'ant' ? { replBridgeActive: false, } : {}), // Direct connect server URL directConnectServerUrl: undefined, // System prompt section cache state systemPromptSectionCache: new Map(), // Last date emitted to the model lastEmittedDate: null, // Additional directories from --add-dir flag (for CLAUDE.md loading) additionalDirectoriesForClaudeMd: [], // Channel server allowlist from --channels flag allowedChannels: [], hasDevChannels: false, // Session project dir (null = derive from originalCwd) sessionProjectDir: null, // Prompt cache 1h allowlist (null = not yet fetched from GrowthBook) promptCache1hAllowlist: null, // Prompt cache 1h eligibility (null = not yet evaluated) promptCache1hEligible: null, // Beta header latches (null = not yet triggered) afkModeHeaderLatched: null, fastModeHeaderLatched: null, cacheEditingHeaderLatched: null, thinkingClearLatched: null, // Current prompt ID promptId: null, lastMainRequestId: undefined, lastApiCompletionTimestamp: null, pendingPostCompaction: false, }
return state}
// AND ESPECIALLY HEREconst STATE: State = getInitialState()
export function getSessionId(): SessionId { return STATE.sessionId}
export function regenerateSessionId( options: { setCurrentAsParent?: boolean } = {},): SessionId { if (options.setCurrentAsParent) { STATE.parentSessionId = STATE.sessionId } // Drop the outgoing session's plan-slug entry so the Map doesn't // accumulate stale keys. Callers that need to carry the slug across // (REPL.tsx clearContext) read it before calling clearConversation. STATE.planSlugCache.delete(STATE.sessionId) // Regenerated sessions live in the current project: reset projectDir to // null so getTranscriptPath() derives from originalCwd. STATE.sessionId = randomUUID() as SessionId STATE.sessionProjectDir = null return STATE.sessionId}
export function getParentSessionId(): SessionId | undefined { return STATE.parentSessionId}
/** * Atomically switch the active session. `sessionId` and `sessionProjectDir` * always change together — there is no separate setter for either, so they * cannot drift out of sync (CC-34). * * @param projectDir — directory containing `<sessionId>.jsonl`. Omit (or * pass `null`) for sessions in the current project — the path will derive * from originalCwd at read time. Pass `dirname(transcriptPath)` when the * session lives in a different project directory (git worktrees, * cross-project resume). Every call resets the project dir; it never * carries over from the previous session. */export function switchSession( sessionId: SessionId, projectDir: string | null = null,): void { // Drop the outgoing session's plan-slug entry so the Map stays bounded // across repeated /resume. Only the current session's slug is ever read // (plans.ts getPlanSlug defaults to getSessionId()). STATE.planSlugCache.delete(STATE.sessionId) STATE.sessionId = sessionId STATE.sessionProjectDir = projectDir sessionSwitched.emit(sessionId)}
const sessionSwitched = createSignal<[id: SessionId]>()
/** * Register a callback that fires when switchSession changes the active * sessionId. bootstrap can't import listeners directly (DAG leaf), so * callers register themselves. concurrentSessions.ts uses this to keep the * PID file's sessionId in sync with --resume. */export const onSessionSwitch = sessionSwitched.subscribe
/** * Project directory the current session's transcript lives in, or `null` if * the session was created in the current project (common case — derive from * originalCwd). See `switchSession()`. */export function getSessionProjectDir(): string | null { return STATE.sessionProjectDir}
export function getOriginalCwd(): string { return STATE.originalCwd}
/** * Get the stable project root directory. * Unlike getOriginalCwd(), this is never updated by mid-session EnterWorktreeTool * (so skills/history stay stable when entering a throwaway worktree). * It IS set at startup by --worktree, since that worktree is the session's project. * Use for project identity (history, skills, sessions) not file operations. */export function getProjectRoot(): string { return STATE.projectRoot}
export function setOriginalCwd(cwd: string): void { STATE.originalCwd = cwd.normalize('NFC')}
/** * Only for --worktree startup flag. Mid-session EnterWorktreeTool must NOT * call this — skills/history should stay anchored to where the session started. */export function setProjectRoot(cwd: string): void { STATE.projectRoot = cwd.normalize('NFC')}
export function getCwdState(): string { return STATE.cwd}
export function setCwdState(cwd: string): void { STATE.cwd = cwd.normalize('NFC')}
export function getDirectConnectServerUrl(): string | undefined { return STATE.directConnectServerUrl}
export function setDirectConnectServerUrl(url: string): void { STATE.directConnectServerUrl = url}
export function addToTotalDurationState( duration: number, durationWithoutRetries: number,): void { STATE.totalAPIDuration += duration STATE.totalAPIDurationWithoutRetries += durationWithoutRetries}
export function resetTotalDurationStateAndCost_FOR_TESTS_ONLY(): void { STATE.totalAPIDuration = 0 STATE.totalAPIDurationWithoutRetries = 0 STATE.totalCostUSD = 0}
export function addToTotalCostState( cost: number, modelUsage: ModelUsage, model: string,): void { STATE.modelUsage[model] = modelUsage STATE.totalCostUSD += cost}
export function getTotalCostUSD(): number { return STATE.totalCostUSD}
export function getTotalAPIDuration(): number { return STATE.totalAPIDuration}
export function getTotalDuration(): number { return Date.now() - STATE.startTime}
export function getTotalAPIDurationWithoutRetries(): number { return STATE.totalAPIDurationWithoutRetries}
export function getTotalToolDuration(): number { return STATE.totalToolDuration}
export function addToToolDuration(duration: number): void { STATE.totalToolDuration += duration STATE.turnToolDurationMs += duration STATE.turnToolCount++}
export function getTurnHookDurationMs(): number { return STATE.turnHookDurationMs}
export function addToTurnHookDuration(duration: number): void { STATE.turnHookDurationMs += duration STATE.turnHookCount++}
export function resetTurnHookDuration(): void { STATE.turnHookDurationMs = 0 STATE.turnHookCount = 0}
export function getTurnHookCount(): number { return STATE.turnHookCount}
export function getTurnToolDurationMs(): number { return STATE.turnToolDurationMs}
export function resetTurnToolDuration(): void { STATE.turnToolDurationMs = 0 STATE.turnToolCount = 0}
export function getTurnToolCount(): number { return STATE.turnToolCount}
export function getTurnClassifierDurationMs(): number { return STATE.turnClassifierDurationMs}
export function addToTurnClassifierDuration(duration: number): void { STATE.turnClassifierDurationMs += duration STATE.turnClassifierCount++}
export function resetTurnClassifierDuration(): void { STATE.turnClassifierDurationMs = 0 STATE.turnClassifierCount = 0}
export function getTurnClassifierCount(): number { return STATE.turnClassifierCount}
export function getStatsStore(): { observe(name: string, value: number): void} | null { return STATE.statsStore}
export function setStatsStore( store: { observe(name: string, value: number): void } | null,): void { STATE.statsStore = store}
/** * Marks that an interaction occurred. * * By default the actual Date.now() call is deferred until the next Ink render * frame (via flushInteractionTime()) so we avoid calling Date.now() on every * single keypress. * * Pass `immediate = true` when calling from React useEffect callbacks or * other code that runs *after* the Ink render cycle has already flushed. * Without it the timestamp stays stale until the next render, which may never * come if the user is idle (e.g. permission dialog waiting for input). */let interactionTimeDirty = false
export function updateLastInteractionTime(immediate?: boolean): void { if (immediate) { flushInteractionTime_inner() } else { interactionTimeDirty = true }}
/** * If an interaction was recorded since the last flush, update the timestamp * now. Called by Ink before each render cycle so we batch many keypresses into * a single Date.now() call. */export function flushInteractionTime(): void { if (interactionTimeDirty) { flushInteractionTime_inner() }}
function flushInteractionTime_inner(): void { STATE.lastInteractionTime = Date.now() interactionTimeDirty = false}
export function addToTotalLinesChanged(added: number, removed: number): void { STATE.totalLinesAdded += added STATE.totalLinesRemoved += removed}
export function getTotalLinesAdded(): number { return STATE.totalLinesAdded}
export function getTotalLinesRemoved(): number { return STATE.totalLinesRemoved}
export function getTotalInputTokens(): number { return sumBy(Object.values(STATE.modelUsage), 'inputTokens')}
export function getTotalOutputTokens(): number { return sumBy(Object.values(STATE.modelUsage), 'outputTokens')}
export function getTotalCacheReadInputTokens(): number { return sumBy(Object.values(STATE.modelUsage), 'cacheReadInputTokens')}
export function getTotalCacheCreationInputTokens(): number { return sumBy(Object.values(STATE.modelUsage), 'cacheCreationInputTokens')}
export function getTotalWebSearchRequests(): number { return sumBy(Object.values(STATE.modelUsage), 'webSearchRequests')}
let outputTokensAtTurnStart = 0let currentTurnTokenBudget: number | null = nullexport function getTurnOutputTokens(): number { return getTotalOutputTokens() - outputTokensAtTurnStart}export function getCurrentTurnTokenBudget(): number | null { return currentTurnTokenBudget}let budgetContinuationCount = 0export function snapshotOutputTokensForTurn(budget: number | null): void { outputTokensAtTurnStart = getTotalOutputTokens() currentTurnTokenBudget = budget budgetContinuationCount = 0}export function getBudgetContinuationCount(): number { return budgetContinuationCount}export function incrementBudgetContinuationCount(): void { budgetContinuationCount++}
export function setHasUnknownModelCost(): void { STATE.hasUnknownModelCost = true}
export function hasUnknownModelCost(): boolean { return STATE.hasUnknownModelCost}
export function getLastMainRequestId(): string | undefined { return STATE.lastMainRequestId}
export function setLastMainRequestId(requestId: string): void { STATE.lastMainRequestId = requestId}
export function getLastApiCompletionTimestamp(): number | null { return STATE.lastApiCompletionTimestamp}
export function setLastApiCompletionTimestamp(timestamp: number): void { STATE.lastApiCompletionTimestamp = timestamp}
/** Mark that a compaction just occurred. The next API success event will * include isPostCompaction=true, then the flag auto-resets. */export function markPostCompaction(): void { STATE.pendingPostCompaction = true}
/** Consume the post-compaction flag. Returns true once after compaction, * then returns false until the next compaction. */export function consumePostCompaction(): boolean { const was = STATE.pendingPostCompaction STATE.pendingPostCompaction = false return was}
export function getLastInteractionTime(): number { return STATE.lastInteractionTime}
// Scroll drain suspension — background intervals check this before doing work// so they don't compete with scroll frames for the event loop. Set by// ScrollBox scrollBy/scrollTo, cleared SCROLL_DRAIN_IDLE_MS after the last// scroll event. Module-scope (not in STATE) — ephemeral hot-path flag, no// test-reset needed since the debounce timer self-clears.let scrollDraining = falselet scrollDrainTimer: ReturnType<typeof setTimeout> | undefinedconst SCROLL_DRAIN_IDLE_MS = 150
/** Mark that a scroll event just happened. Background intervals gate on * getIsScrollDraining() and skip their work until the debounce clears. */export function markScrollActivity(): void { scrollDraining = true if (scrollDrainTimer) clearTimeout(scrollDrainTimer) scrollDrainTimer = setTimeout(() => { scrollDraining = false scrollDrainTimer = undefined }, SCROLL_DRAIN_IDLE_MS) scrollDrainTimer.unref?.()}
/** True while scroll is actively draining (within 150ms of last event). * Intervals should early-return when this is set — the work picks up next * tick after scroll settles. */export function getIsScrollDraining(): boolean { return scrollDraining}
/** Await this before expensive one-shot work (network, subprocess) that could * coincide with scroll. Resolves immediately if not scrolling; otherwise * polls at the idle interval until the flag clears. */export async function waitForScrollIdle(): Promise<void> { while (scrollDraining) { // bootstrap-isolation forbids importing sleep() from src/utils/ // eslint-disable-next-line no-restricted-syntax await new Promise(r => setTimeout(r, SCROLL_DRAIN_IDLE_MS).unref?.()) }}
export function getModelUsage(): { [modelName: string]: ModelUsage } { return STATE.modelUsage}
export function getUsageForModel(model: string): ModelUsage | undefined { return STATE.modelUsage[model]}
/** * Gets the model override set from the --model CLI flag or after the user * updates their configured model. */export function getMainLoopModelOverride(): ModelSetting | undefined { return STATE.mainLoopModelOverride}
export function getInitialMainLoopModel(): ModelSetting { return STATE.initialMainLoopModel}
export function setMainLoopModelOverride( model: ModelSetting | undefined,): void { STATE.mainLoopModelOverride = model}
export function setInitialMainLoopModel(model: ModelSetting): void { STATE.initialMainLoopModel = model}
export function getSdkBetas(): string[] | undefined { return STATE.sdkBetas}
export function setSdkBetas(betas: string[] | undefined): void { STATE.sdkBetas = betas}
export function resetCostState(): void { STATE.totalCostUSD = 0 STATE.totalAPIDuration = 0 STATE.totalAPIDurationWithoutRetries = 0 STATE.totalToolDuration = 0 STATE.startTime = Date.now() STATE.totalLinesAdded = 0 STATE.totalLinesRemoved = 0 STATE.hasUnknownModelCost = false STATE.modelUsage = {} STATE.promptId = null}
/** * Sets cost state values for session restore. * Called by restoreCostStateForSession in cost-tracker.ts. */export function setCostStateForRestore({ totalCostUSD, totalAPIDuration, totalAPIDurationWithoutRetries, totalToolDuration, totalLinesAdded, totalLinesRemoved, lastDuration, modelUsage,}: { totalCostUSD: number totalAPIDuration: number totalAPIDurationWithoutRetries: number totalToolDuration: number totalLinesAdded: number totalLinesRemoved: number lastDuration: number | undefined modelUsage: { [modelName: string]: ModelUsage } | undefined}): void { STATE.totalCostUSD = totalCostUSD STATE.totalAPIDuration = totalAPIDuration STATE.totalAPIDurationWithoutRetries = totalAPIDurationWithoutRetries STATE.totalToolDuration = totalToolDuration STATE.totalLinesAdded = totalLinesAdded STATE.totalLinesRemoved = totalLinesRemoved
// Restore per-model usage breakdown if (modelUsage) { STATE.modelUsage = modelUsage }
// Adjust startTime to make wall duration accumulate if (lastDuration) { STATE.startTime = Date.now() - lastDuration }}
// Only used in testsexport function resetStateForTests(): void { if (process.env.NODE_ENV !== 'test') { throw new Error('resetStateForTests can only be called in tests') } Object.entries(getInitialState()).forEach(([key, value]) => { STATE[key as keyof State] = value as never }) outputTokensAtTurnStart = 0 currentTurnTokenBudget = null budgetContinuationCount = 0 sessionSwitched.clear()}
// You shouldn't use this directly. See src/utils/model/modelStrings.ts::getModelStrings()export function getModelStrings(): ModelStrings | null { return STATE.modelStrings}
// You shouldn't use this directly. See src/utils/model/modelStrings.tsexport function setModelStrings(modelStrings: ModelStrings): void { STATE.modelStrings = modelStrings}
// Test utility function to reset model strings for re-initialization.// Separate from setModelStrings because we only want to accept 'null' in tests.export function resetModelStringsForTestingOnly() { STATE.modelStrings = null}
export function setMeter( meter: Meter, createCounter: (name: string, options: MetricOptions) => AttributedCounter,): void { STATE.meter = meter
// Initialize all counters using the provided factory STATE.sessionCounter = createCounter('claude_code.session.count', { description: 'Count of CLI sessions started', }) STATE.locCounter = createCounter('claude_code.lines_of_code.count', { description: "Count of lines of code modified, with the 'type' attribute indicating whether lines were added or removed", }) STATE.prCounter = createCounter('claude_code.pull_request.count', { description: 'Number of pull requests created', }) STATE.commitCounter = createCounter('claude_code.commit.count', { description: 'Number of git commits created', }) STATE.costCounter = createCounter('claude_code.cost.usage', { description: 'Cost of the Claude Code session', unit: 'USD', }) STATE.tokenCounter = createCounter('claude_code.token.usage', { description: 'Number of tokens used', unit: 'tokens', }) STATE.codeEditToolDecisionCounter = createCounter( 'claude_code.code_edit_tool.decision', { description: 'Count of code editing tool permission decisions (accept/reject) for Edit, Write, and NotebookEdit tools', }, ) STATE.activeTimeCounter = createCounter('claude_code.active_time.total', { description: 'Total active time in seconds', unit: 's', })}
export function getMeter(): Meter | null { return STATE.meter}
export function getSessionCounter(): AttributedCounter | null { return STATE.sessionCounter}
export function getLocCounter(): AttributedCounter | null { return STATE.locCounter}
export function getPrCounter(): AttributedCounter | null { return STATE.prCounter}
export function getCommitCounter(): AttributedCounter | null { return STATE.commitCounter}
export function getCostCounter(): AttributedCounter | null { return STATE.costCounter}
export function getTokenCounter(): AttributedCounter | null { return STATE.tokenCounter}
export function getCodeEditToolDecisionCounter(): AttributedCounter | null { return STATE.codeEditToolDecisionCounter}
export function getActiveTimeCounter(): AttributedCounter | null { return STATE.activeTimeCounter}
export function getLoggerProvider(): LoggerProvider | null { return STATE.loggerProvider}
export function setLoggerProvider(provider: LoggerProvider | null): void { STATE.loggerProvider = provider}
export function getEventLogger(): ReturnType<typeof logs.getLogger> | null { return STATE.eventLogger}
export function setEventLogger( logger: ReturnType<typeof logs.getLogger> | null,): void { STATE.eventLogger = logger}
export function getMeterProvider(): MeterProvider | null { return STATE.meterProvider}
export function setMeterProvider(provider: MeterProvider | null): void { STATE.meterProvider = provider}export function getTracerProvider(): BasicTracerProvider | null { return STATE.tracerProvider}export function setTracerProvider(provider: BasicTracerProvider | null): void { STATE.tracerProvider = provider}
export function getIsNonInteractiveSession(): boolean { return !STATE.isInteractive}
export function getIsInteractive(): boolean { return STATE.isInteractive}
export function setIsInteractive(value: boolean): void { STATE.isInteractive = value}
export function getClientType(): string { return STATE.clientType}
export function setClientType(type: string): void { STATE.clientType = type}
export function getSdkAgentProgressSummariesEnabled(): boolean { return STATE.sdkAgentProgressSummariesEnabled}
export function setSdkAgentProgressSummariesEnabled(value: boolean): void { STATE.sdkAgentProgressSummariesEnabled = value}
export function getKairosActive(): boolean { return STATE.kairosActive}
export function setKairosActive(value: boolean): void { STATE.kairosActive = value}
export function getStrictToolResultPairing(): boolean { return STATE.strictToolResultPairing}
export function setStrictToolResultPairing(value: boolean): void { STATE.strictToolResultPairing = value}
// Field name 'userMsgOptIn' avoids excluded-string substrings ('BriefTool',// 'SendUserMessage' — case-insensitive). All callers are inside feature()// guards so these accessors don't need their own (matches getKairosActive).export function getUserMsgOptIn(): boolean { return STATE.userMsgOptIn}
export function setUserMsgOptIn(value: boolean): void { STATE.userMsgOptIn = value}
export function getSessionSource(): string | undefined { return STATE.sessionSource}
export function setSessionSource(source: string): void { STATE.sessionSource = source}
export function getQuestionPreviewFormat(): 'markdown' | 'html' | undefined { return STATE.questionPreviewFormat}
export function setQuestionPreviewFormat(format: 'markdown' | 'html'): void { STATE.questionPreviewFormat = format}
export function getAgentColorMap(): Map<string, AgentColorName> { return STATE.agentColorMap}
export function getFlagSettingsPath(): string | undefined { return STATE.flagSettingsPath}
export function setFlagSettingsPath(path: string | undefined): void { STATE.flagSettingsPath = path}
export function getFlagSettingsInline(): Record<string, unknown> | null { return STATE.flagSettingsInline}
export function setFlagSettingsInline( settings: Record<string, unknown> | null,): void { STATE.flagSettingsInline = settings}
export function getSessionIngressToken(): string | null | undefined { return STATE.sessionIngressToken}
export function setSessionIngressToken(token: string | null): void { STATE.sessionIngressToken = token}
export function getOauthTokenFromFd(): string | null | undefined { return STATE.oauthTokenFromFd}
export function setOauthTokenFromFd(token: string | null): void { STATE.oauthTokenFromFd = token}
export function getApiKeyFromFd(): string | null | undefined { return STATE.apiKeyFromFd}
export function setApiKeyFromFd(key: string | null): void { STATE.apiKeyFromFd = key}
export function setLastAPIRequest( params: Omit<BetaMessageStreamParams, 'messages'> | null,): void { STATE.lastAPIRequest = params}
export function getLastAPIRequest(): Omit< BetaMessageStreamParams, 'messages'> | null { return STATE.lastAPIRequest}
export function setLastAPIRequestMessages( messages: BetaMessageStreamParams['messages'] | null,): void { STATE.lastAPIRequestMessages = messages}
export function getLastAPIRequestMessages(): | BetaMessageStreamParams['messages'] | null { return STATE.lastAPIRequestMessages}
export function setLastClassifierRequests(requests: unknown[] | null): void { STATE.lastClassifierRequests = requests}
export function getLastClassifierRequests(): unknown[] | null { return STATE.lastClassifierRequests}
export function setCachedClaudeMdContent(content: string | null): void { STATE.cachedClaudeMdContent = content}
export function getCachedClaudeMdContent(): string | null { return STATE.cachedClaudeMdContent}
export function addToInMemoryErrorLog(errorInfo: { error: string timestamp: string}): void { const MAX_IN_MEMORY_ERRORS = 100 if (STATE.inMemoryErrorLog.length >= MAX_IN_MEMORY_ERRORS) { STATE.inMemoryErrorLog.shift() // Remove oldest error } STATE.inMemoryErrorLog.push(errorInfo)}
export function getAllowedSettingSources(): SettingSource[] { return STATE.allowedSettingSources}
export function setAllowedSettingSources(sources: SettingSource[]): void { STATE.allowedSettingSources = sources}
export function preferThirdPartyAuthentication(): boolean { // IDE extension should behave as 1P for authentication reasons. return getIsNonInteractiveSession() && STATE.clientType !== 'claude-vscode'}
export function setInlinePlugins(plugins: Array<string>): void { STATE.inlinePlugins = plugins}
export function getInlinePlugins(): Array<string> { return STATE.inlinePlugins}
export function setChromeFlagOverride(value: boolean | undefined): void { STATE.chromeFlagOverride = value}
export function getChromeFlagOverride(): boolean | undefined { return STATE.chromeFlagOverride}
export function setUseCoworkPlugins(value: boolean): void { STATE.useCoworkPlugins = value resetSettingsCache()}
export function getUseCoworkPlugins(): boolean { return STATE.useCoworkPlugins}
export function setSessionBypassPermissionsMode(enabled: boolean): void { STATE.sessionBypassPermissionsMode = enabled}
export function getSessionBypassPermissionsMode(): boolean { return STATE.sessionBypassPermissionsMode}
export function setScheduledTasksEnabled(enabled: boolean): void { STATE.scheduledTasksEnabled = enabled}
export function getScheduledTasksEnabled(): boolean { return STATE.scheduledTasksEnabled}
export type SessionCronTask = { id: string cron: string prompt: string createdAt: number recurring?: boolean /** * When set, the task was created by an in-process teammate (not the team lead). * The scheduler routes fires to that teammate's pendingUserMessages queue * instead of the main REPL command queue. Session-only — never written to disk. */ agentId?: string}
export function getSessionCronTasks(): SessionCronTask[] { return STATE.sessionCronTasks}
export function addSessionCronTask(task: SessionCronTask): void { STATE.sessionCronTasks.push(task)}
/** * Returns the number of tasks actually removed. Callers use this to skip * downstream work (e.g. the disk read in removeCronTasks) when all ids * were accounted for here. */export function removeSessionCronTasks(ids: readonly string[]): number { if (ids.length === 0) return 0 const idSet = new Set(ids) const remaining = STATE.sessionCronTasks.filter(t => !idSet.has(t.id)) const removed = STATE.sessionCronTasks.length - remaining.length if (removed === 0) return 0 STATE.sessionCronTasks = remaining return removed}
export function setSessionTrustAccepted(accepted: boolean): void { STATE.sessionTrustAccepted = accepted}
export function getSessionTrustAccepted(): boolean { return STATE.sessionTrustAccepted}
export function setSessionPersistenceDisabled(disabled: boolean): void { STATE.sessionPersistenceDisabled = disabled}
export function isSessionPersistenceDisabled(): boolean { return STATE.sessionPersistenceDisabled}
export function hasExitedPlanModeInSession(): boolean { return STATE.hasExitedPlanMode}
export function setHasExitedPlanMode(value: boolean): void { STATE.hasExitedPlanMode = value}
export function needsPlanModeExitAttachment(): boolean { return STATE.needsPlanModeExitAttachment}
export function setNeedsPlanModeExitAttachment(value: boolean): void { STATE.needsPlanModeExitAttachment = value}
export function handlePlanModeTransition( fromMode: string, toMode: string,): void { // If switching TO plan mode, clear any pending exit attachment // This prevents sending both plan_mode and plan_mode_exit when user toggles quickly if (toMode === 'plan' && fromMode !== 'plan') { STATE.needsPlanModeExitAttachment = false }
// If switching out of plan mode, trigger the plan_mode_exit attachment if (fromMode === 'plan' && toMode !== 'plan') { STATE.needsPlanModeExitAttachment = true }}
export function needsAutoModeExitAttachment(): boolean { return STATE.needsAutoModeExitAttachment}
export function setNeedsAutoModeExitAttachment(value: boolean): void { STATE.needsAutoModeExitAttachment = value}
export function handleAutoModeTransition( fromMode: string, toMode: string,): void { // Auto↔plan transitions are handled by prepareContextForPlanMode (auto may // stay active through plan if opted in) and ExitPlanMode (restores mode). // Skip both directions so this function only handles direct auto transitions. if ( (fromMode === 'auto' && toMode === 'plan') || (fromMode === 'plan' && toMode === 'auto') ) { return } const fromIsAuto = fromMode === 'auto' const toIsAuto = toMode === 'auto'
// If switching TO auto mode, clear any pending exit attachment // This prevents sending both auto_mode and auto_mode_exit when user toggles quickly if (toIsAuto && !fromIsAuto) { STATE.needsAutoModeExitAttachment = false }
// If switching out of auto mode, trigger the auto_mode_exit attachment if (fromIsAuto && !toIsAuto) { STATE.needsAutoModeExitAttachment = true }}
// LSP plugin recommendation session trackingexport function hasShownLspRecommendationThisSession(): boolean { return STATE.lspRecommendationShownThisSession}
export function setLspRecommendationShownThisSession(value: boolean): void { STATE.lspRecommendationShownThisSession = value}
// SDK init event stateexport function setInitJsonSchema(schema: Record<string, unknown>): void { STATE.initJsonSchema = schema}
export function getInitJsonSchema(): Record<string, unknown> | null { return STATE.initJsonSchema}
export function registerHookCallbacks( hooks: Partial<Record<HookEvent, RegisteredHookMatcher[]>>,): void { if (!STATE.registeredHooks) { STATE.registeredHooks = {} }
// `registerHookCallbacks` may be called multiple times, so we need to merge (not overwrite) for (const [event, matchers] of Object.entries(hooks)) { const eventKey = event as HookEvent if (!STATE.registeredHooks[eventKey]) { STATE.registeredHooks[eventKey] = [] } STATE.registeredHooks[eventKey]!.push(...matchers) }}
export function getRegisteredHooks(): Partial< Record<HookEvent, RegisteredHookMatcher[]>> | null { return STATE.registeredHooks}
export function clearRegisteredHooks(): void { STATE.registeredHooks = null}
export function clearRegisteredPluginHooks(): void { if (!STATE.registeredHooks) { return }
const filtered: Partial<Record<HookEvent, RegisteredHookMatcher[]>> = {} for (const [event, matchers] of Object.entries(STATE.registeredHooks)) { // Keep only callback hooks (those without pluginRoot) const callbackHooks = matchers.filter(m => !('pluginRoot' in m)) if (callbackHooks.length > 0) { filtered[event as HookEvent] = callbackHooks } }
STATE.registeredHooks = Object.keys(filtered).length > 0 ? filtered : null}
export function resetSdkInitState(): void { STATE.initJsonSchema = null STATE.registeredHooks = null}
export function getPlanSlugCache(): Map<string, string> { return STATE.planSlugCache}
export function getSessionCreatedTeams(): Set<string> { return STATE.sessionCreatedTeams}
// Teleported session tracking for reliability loggingexport function setTeleportedSessionInfo(info: { sessionId: string | null}): void { STATE.teleportedSessionInfo = { isTeleported: true, hasLoggedFirstMessage: false, sessionId: info.sessionId, }}
export function getTeleportedSessionInfo(): { isTeleported: boolean hasLoggedFirstMessage: boolean sessionId: string | null} | null { return STATE.teleportedSessionInfo}
export function markFirstTeleportMessageLogged(): void { if (STATE.teleportedSessionInfo) { STATE.teleportedSessionInfo.hasLoggedFirstMessage = true }}
// Invoked skills tracking for preservation across compactionexport type InvokedSkillInfo = { skillName: string skillPath: string content: string invokedAt: number agentId: string | null}
export function addInvokedSkill( skillName: string, skillPath: string, content: string, agentId: string | null = null,): void { const key = `${agentId ?? ''}:${skillName}` STATE.invokedSkills.set(key, { skillName, skillPath, content, invokedAt: Date.now(), agentId, })}
export function getInvokedSkills(): Map<string, InvokedSkillInfo> { return STATE.invokedSkills}
export function getInvokedSkillsForAgent( agentId: string | undefined | null,): Map<string, InvokedSkillInfo> { const normalizedId = agentId ?? null const filtered = new Map<string, InvokedSkillInfo>() for (const [key, skill] of STATE.invokedSkills) { if (skill.agentId === normalizedId) { filtered.set(key, skill) } } return filtered}
export function clearInvokedSkills( preservedAgentIds?: ReadonlySet<string>,): void { if (!preservedAgentIds || preservedAgentIds.size === 0) { STATE.invokedSkills.clear() return } for (const [key, skill] of STATE.invokedSkills) { if (skill.agentId === null || !preservedAgentIds.has(skill.agentId)) { STATE.invokedSkills.delete(key) } }}
export function clearInvokedSkillsForAgent(agentId: string): void { for (const [key, skill] of STATE.invokedSkills) { if (skill.agentId === agentId) { STATE.invokedSkills.delete(key) } }}
// Slow operations tracking for dev barconst MAX_SLOW_OPERATIONS = 10const SLOW_OPERATION_TTL_MS = 10000
export function addSlowOperation(operation: string, durationMs: number): void { if (process.env.USER_TYPE !== 'ant') return // Skip tracking for editor sessions (user editing a prompt file in $EDITOR) // These are intentionally slow since the user is drafting text if (operation.includes('exec') && operation.includes('claude-prompt-')) { return } const now = Date.now() // Remove stale operations STATE.slowOperations = STATE.slowOperations.filter( op => now - op.timestamp < SLOW_OPERATION_TTL_MS, ) // Add new operation STATE.slowOperations.push({ operation, durationMs, timestamp: now }) // Keep only the most recent operations if (STATE.slowOperations.length > MAX_SLOW_OPERATIONS) { STATE.slowOperations = STATE.slowOperations.slice(-MAX_SLOW_OPERATIONS) }}
const EMPTY_SLOW_OPERATIONS: ReadonlyArray<{ operation: string durationMs: number timestamp: number}> = []
export function getSlowOperations(): ReadonlyArray<{ operation: string durationMs: number timestamp: number}> { // Most common case: nothing tracked. Return a stable reference so the // caller's setState() can bail via Object.is instead of re-rendering at 2fps. if (STATE.slowOperations.length === 0) { return EMPTY_SLOW_OPERATIONS } const now = Date.now() // Only allocate a new array when something actually expired; otherwise keep // the reference stable across polls while ops are still fresh. if ( STATE.slowOperations.some(op => now - op.timestamp >= SLOW_OPERATION_TTL_MS) ) { STATE.slowOperations = STATE.slowOperations.filter( op => now - op.timestamp < SLOW_OPERATION_TTL_MS, ) if (STATE.slowOperations.length === 0) { return EMPTY_SLOW_OPERATIONS } } // Safe to return directly: addSlowOperation() reassigns STATE.slowOperations // before pushing, so the array held in React state is never mutated. return STATE.slowOperations}
export function getMainThreadAgentType(): string | undefined { return STATE.mainThreadAgentType}
export function setMainThreadAgentType(agentType: string | undefined): void { STATE.mainThreadAgentType = agentType}
export function getIsRemoteMode(): boolean { return STATE.isRemoteMode}
export function setIsRemoteMode(value: boolean): void { STATE.isRemoteMode = value}
// System prompt section accessors
export function getSystemPromptSectionCache(): Map<string, string | null> { return STATE.systemPromptSectionCache}
export function setSystemPromptSectionCacheEntry( name: string, value: string | null,): void { STATE.systemPromptSectionCache.set(name, value)}
export function clearSystemPromptSectionState(): void { STATE.systemPromptSectionCache.clear()}
// Last emitted date accessors (for detecting midnight date changes)
export function getLastEmittedDate(): string | null { return STATE.lastEmittedDate}
export function setLastEmittedDate(date: string | null): void { STATE.lastEmittedDate = date}
export function getAdditionalDirectoriesForClaudeMd(): string[] { return STATE.additionalDirectoriesForClaudeMd}
export function setAdditionalDirectoriesForClaudeMd( directories: string[],): void { STATE.additionalDirectoriesForClaudeMd = directories}
export function getAllowedChannels(): ChannelEntry[] { return STATE.allowedChannels}
export function setAllowedChannels(entries: ChannelEntry[]): void { STATE.allowedChannels = entries}
export function getHasDevChannels(): boolean { return STATE.hasDevChannels}
export function setHasDevChannels(value: boolean): void { STATE.hasDevChannels = value}
export function getPromptCache1hAllowlist(): string[] | null { return STATE.promptCache1hAllowlist}
export function setPromptCache1hAllowlist(allowlist: string[] | null): void { STATE.promptCache1hAllowlist = allowlist}
export function getPromptCache1hEligible(): boolean | null { return STATE.promptCache1hEligible}
export function setPromptCache1hEligible(eligible: boolean | null): void { STATE.promptCache1hEligible = eligible}
export function getAfkModeHeaderLatched(): boolean | null { return STATE.afkModeHeaderLatched}
export function setAfkModeHeaderLatched(v: boolean): void { STATE.afkModeHeaderLatched = v}
export function getFastModeHeaderLatched(): boolean | null { return STATE.fastModeHeaderLatched}
export function setFastModeHeaderLatched(v: boolean): void { STATE.fastModeHeaderLatched = v}
export function getCacheEditingHeaderLatched(): boolean | null { return STATE.cacheEditingHeaderLatched}
export function setCacheEditingHeaderLatched(v: boolean): void { STATE.cacheEditingHeaderLatched = v}
export function getThinkingClearLatched(): boolean | null { return STATE.thinkingClearLatched}
export function setThinkingClearLatched(v: boolean): void { STATE.thinkingClearLatched = v}
/** * Reset beta header latches to null. Called on /clear and /compact so a * fresh conversation gets fresh header evaluation. */export function clearBetaHeaderLatches(): void { STATE.afkModeHeaderLatched = null STATE.fastModeHeaderLatched = null STATE.cacheEditingHeaderLatched = null STATE.thinkingClearLatched = null}
export function getPromptId(): string | null { return STATE.promptId}
export function setPromptId(id: string | null): void { STATE.promptId = id}