| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758 |
- import 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-isolation
- import { 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 hooks
- type 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 MODIFYING
- function 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 HERE
- const 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 = 0
- let currentTurnTokenBudget: number | null = null
- export function getTurnOutputTokens(): number {
- return getTotalOutputTokens() - outputTokensAtTurnStart
- }
- export function getCurrentTurnTokenBudget(): number | null {
- return currentTurnTokenBudget
- }
- let budgetContinuationCount = 0
- export 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 = false
- let scrollDrainTimer: ReturnType<typeof setTimeout> | undefined
- const 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 tests
- export 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.ts
- export 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 tracking
- export function hasShownLspRecommendationThisSession(): boolean {
- return STATE.lspRecommendationShownThisSession
- }
- export function setLspRecommendationShownThisSession(value: boolean): void {
- STATE.lspRecommendationShownThisSession = value
- }
- // SDK init event state
- export 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 logging
- export 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 compaction
- export 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 bar
- const MAX_SLOW_OPERATIONS = 10
- const 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
- }
- export type isReplBridgeActive = any;
|