state.ts 55 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758
  1. import type { BetaMessageStreamParams } from '@anthropic-ai/sdk/resources/beta/messages/messages.mjs'
  2. import type { Attributes, Meter, MetricOptions } from '@opentelemetry/api'
  3. import type { logs } from '@opentelemetry/api-logs'
  4. import type { LoggerProvider } from '@opentelemetry/sdk-logs'
  5. import type { MeterProvider } from '@opentelemetry/sdk-metrics'
  6. import type { BasicTracerProvider } from '@opentelemetry/sdk-trace-base'
  7. import { realpathSync } from 'fs'
  8. import sumBy from 'lodash-es/sumBy.js'
  9. import { cwd } from 'process'
  10. import type { HookEvent, ModelUsage } from 'src/entrypoints/agentSdkTypes.js'
  11. import type { AgentColorName } from 'src/tools/AgentTool/agentColorManager.js'
  12. import type { HookCallbackMatcher } from 'src/types/hooks.js'
  13. // Indirection for browser-sdk build (package.json "browser" field swaps
  14. // crypto.ts for crypto.browser.ts). Pure leaf re-export of node:crypto —
  15. // zero circular-dep risk. Path-alias import bypasses bootstrap-isolation
  16. // (rule only checks ./ and / prefixes); explicit disable documents intent.
  17. // eslint-disable-next-line custom-rules/bootstrap-isolation
  18. import { randomUUID } from 'src/utils/crypto.js'
  19. import type { ModelSetting } from 'src/utils/model/model.js'
  20. import type { ModelStrings } from 'src/utils/model/modelStrings.js'
  21. import type { SettingSource } from 'src/utils/settings/constants.js'
  22. import { resetSettingsCache } from 'src/utils/settings/settingsCache.js'
  23. import type { PluginHookMatcher } from 'src/utils/settings/types.js'
  24. import { createSignal } from 'src/utils/signal.js'
  25. // Union type for registered hooks - can be SDK callbacks or native plugin hooks
  26. type RegisteredHookMatcher = HookCallbackMatcher | PluginHookMatcher
  27. import type { SessionId } from 'src/types/ids.js'
  28. // DO NOT ADD MORE STATE HERE - BE JUDICIOUS WITH GLOBAL STATE
  29. // dev: true on entries that came via --dangerously-load-development-channels.
  30. // The allowlist gate checks this per-entry (not the session-wide
  31. // hasDevChannels bit) so passing both flags doesn't let the dev dialog's
  32. // acceptance leak allowlist-bypass to the --channels entries.
  33. export type ChannelEntry =
  34. | { kind: 'plugin'; name: string; marketplace: string; dev?: boolean }
  35. | { kind: 'server'; name: string; dev?: boolean }
  36. export type AttributedCounter = {
  37. add(value: number, additionalAttributes?: Attributes): void
  38. }
  39. type State = {
  40. originalCwd: string
  41. // Stable project root - set once at startup (including by --worktree flag),
  42. // never updated by mid-session EnterWorktreeTool.
  43. // Use for project identity (history, skills, sessions) not file operations.
  44. projectRoot: string
  45. totalCostUSD: number
  46. totalAPIDuration: number
  47. totalAPIDurationWithoutRetries: number
  48. totalToolDuration: number
  49. turnHookDurationMs: number
  50. turnToolDurationMs: number
  51. turnClassifierDurationMs: number
  52. turnToolCount: number
  53. turnHookCount: number
  54. turnClassifierCount: number
  55. startTime: number
  56. lastInteractionTime: number
  57. totalLinesAdded: number
  58. totalLinesRemoved: number
  59. hasUnknownModelCost: boolean
  60. cwd: string
  61. modelUsage: { [modelName: string]: ModelUsage }
  62. mainLoopModelOverride: ModelSetting | undefined
  63. initialMainLoopModel: ModelSetting
  64. modelStrings: ModelStrings | null
  65. isInteractive: boolean
  66. kairosActive: boolean
  67. // When true, ensureToolResultPairing throws on mismatch instead of
  68. // repairing with synthetic placeholders. HFI opts in at startup so
  69. // trajectories fail fast rather than conditioning the model on fake
  70. // tool_results.
  71. strictToolResultPairing: boolean
  72. sdkAgentProgressSummariesEnabled: boolean
  73. userMsgOptIn: boolean
  74. clientType: string
  75. sessionSource: string | undefined
  76. questionPreviewFormat: 'markdown' | 'html' | undefined
  77. flagSettingsPath: string | undefined
  78. flagSettingsInline: Record<string, unknown> | null
  79. allowedSettingSources: SettingSource[]
  80. sessionIngressToken: string | null | undefined
  81. oauthTokenFromFd: string | null | undefined
  82. apiKeyFromFd: string | null | undefined
  83. // Telemetry state
  84. meter: Meter | null
  85. sessionCounter: AttributedCounter | null
  86. locCounter: AttributedCounter | null
  87. prCounter: AttributedCounter | null
  88. commitCounter: AttributedCounter | null
  89. costCounter: AttributedCounter | null
  90. tokenCounter: AttributedCounter | null
  91. codeEditToolDecisionCounter: AttributedCounter | null
  92. activeTimeCounter: AttributedCounter | null
  93. statsStore: { observe(name: string, value: number): void } | null
  94. sessionId: SessionId
  95. // Parent session ID for tracking session lineage (e.g., plan mode -> implementation)
  96. parentSessionId: SessionId | undefined
  97. // Logger state
  98. loggerProvider: LoggerProvider | null
  99. eventLogger: ReturnType<typeof logs.getLogger> | null
  100. // Meter provider state
  101. meterProvider: MeterProvider | null
  102. // Tracer provider state
  103. tracerProvider: BasicTracerProvider | null
  104. // Agent color state
  105. agentColorMap: Map<string, AgentColorName>
  106. agentColorIndex: number
  107. // Last API request for bug reports
  108. lastAPIRequest: Omit<BetaMessageStreamParams, 'messages'> | null
  109. // Messages from the last API request (ant-only; reference, not clone).
  110. // Captures the exact post-compaction, CLAUDE.md-injected message set sent
  111. // to the API so /share's serialized_conversation.json reflects reality.
  112. lastAPIRequestMessages: BetaMessageStreamParams['messages'] | null
  113. // Last auto-mode classifier request(s) for /share transcript
  114. lastClassifierRequests: unknown[] | null
  115. // CLAUDE.md content cached by context.ts for the auto-mode classifier.
  116. // Breaks the yoloClassifier → claudemd → filesystem → permissions cycle.
  117. cachedClaudeMdContent: string | null
  118. // In-memory error log for recent errors
  119. inMemoryErrorLog: Array<{ error: string; timestamp: string }>
  120. // Session-only plugins from --plugin-dir flag
  121. inlinePlugins: Array<string>
  122. // Explicit --chrome / --no-chrome flag value (undefined = not set on CLI)
  123. chromeFlagOverride: boolean | undefined
  124. // Use cowork_plugins directory instead of plugins (--cowork flag or env var)
  125. useCoworkPlugins: boolean
  126. // Session-only bypass permissions mode flag (not persisted)
  127. sessionBypassPermissionsMode: boolean
  128. // Session-only flag gating the .claude/scheduled_tasks.json watcher
  129. // (useScheduledTasks). Set by cronScheduler.start() when the JSON has
  130. // entries, or by CronCreateTool. Not persisted.
  131. scheduledTasksEnabled: boolean
  132. // Session-only cron tasks created via CronCreate with durable: false.
  133. // Fire on schedule like file-backed tasks but are never written to
  134. // .claude/scheduled_tasks.json — they die with the process. Typed via
  135. // SessionCronTask below (not importing from cronTasks.ts keeps
  136. // bootstrap a leaf of the import DAG).
  137. sessionCronTasks: SessionCronTask[]
  138. // Teams created this session via TeamCreate. cleanupSessionTeams()
  139. // removes these on gracefulShutdown so subagent-created teams don't
  140. // persist on disk forever (gh-32730). TeamDelete removes entries to
  141. // avoid double-cleanup. Lives here (not teamHelpers.ts) so
  142. // resetStateForTests() clears it between tests.
  143. sessionCreatedTeams: Set<string>
  144. // Session-only trust flag for home directory (not persisted to disk)
  145. // When running from home dir, trust dialog is shown but not saved to disk.
  146. // This flag allows features requiring trust to work during the session.
  147. sessionTrustAccepted: boolean
  148. // Session-only flag to disable session persistence to disk
  149. sessionPersistenceDisabled: boolean
  150. // Track if user has exited plan mode in this session (for re-entry guidance)
  151. hasExitedPlanMode: boolean
  152. // Track if we need to show the plan mode exit attachment (one-time notification)
  153. needsPlanModeExitAttachment: boolean
  154. // Track if we need to show the auto mode exit attachment (one-time notification)
  155. needsAutoModeExitAttachment: boolean
  156. // Track if LSP plugin recommendation has been shown this session (only show once)
  157. lspRecommendationShownThisSession: boolean
  158. // SDK init event state - jsonSchema for structured output
  159. initJsonSchema: Record<string, unknown> | null
  160. // Registered hooks - SDK callbacks and plugin native hooks
  161. registeredHooks: Partial<Record<HookEvent, RegisteredHookMatcher[]>> | null
  162. // Cache for plan slugs: sessionId -> wordSlug
  163. planSlugCache: Map<string, string>
  164. // Track teleported session for reliability logging
  165. teleportedSessionInfo: {
  166. isTeleported: boolean
  167. hasLoggedFirstMessage: boolean
  168. sessionId: string | null
  169. } | null
  170. // Track invoked skills for preservation across compaction
  171. // Keys are composite: `${agentId ?? ''}:${skillName}` to prevent cross-agent overwrites
  172. invokedSkills: Map<
  173. string,
  174. {
  175. skillName: string
  176. skillPath: string
  177. content: string
  178. invokedAt: number
  179. agentId: string | null
  180. }
  181. >
  182. // Track slow operations for dev bar display (ant-only)
  183. slowOperations: Array<{
  184. operation: string
  185. durationMs: number
  186. timestamp: number
  187. }>
  188. // SDK-provided betas (e.g., context-1m-2025-08-07)
  189. sdkBetas: string[] | undefined
  190. // Main thread agent type (from --agent flag or settings)
  191. mainThreadAgentType: string | undefined
  192. // Remote mode (--remote flag)
  193. isRemoteMode: boolean
  194. // Direct connect server URL (for display in header)
  195. directConnectServerUrl: string | undefined
  196. // System prompt section cache state
  197. systemPromptSectionCache: Map<string, string | null>
  198. // Last date emitted to the model (for detecting midnight date changes)
  199. lastEmittedDate: string | null
  200. // Additional directories from --add-dir flag (for CLAUDE.md loading)
  201. additionalDirectoriesForClaudeMd: string[]
  202. // Channel server allowlist from --channels flag (servers whose channel
  203. // notifications should register this session). Parsed once in main.tsx —
  204. // the tag decides trust model: 'plugin' → marketplace verification +
  205. // allowlist, 'server' → allowlist always fails (schema is plugin-only).
  206. // Either kind needs entry.dev to bypass allowlist.
  207. allowedChannels: ChannelEntry[]
  208. // True if any entry in allowedChannels came from
  209. // --dangerously-load-development-channels (so ChannelsNotice can name the
  210. // right flag in policy-blocked messages)
  211. hasDevChannels: boolean
  212. // Dir containing the session's `.jsonl`; null = derive from originalCwd.
  213. sessionProjectDir: string | null
  214. // Cached prompt cache 1h TTL allowlist from GrowthBook (session-stable)
  215. promptCache1hAllowlist: string[] | null
  216. // Cached 1h TTL user eligibility (session-stable). Latched on first
  217. // evaluation so mid-session overage flips don't change the cache_control
  218. // TTL, which would bust the server-side prompt cache.
  219. promptCache1hEligible: boolean | null
  220. // Sticky-on latch for AFK_MODE_BETA_HEADER. Once auto mode is first
  221. // activated, keep sending the header for the rest of the session so
  222. // Shift+Tab toggles don't bust the ~50-70K token prompt cache.
  223. afkModeHeaderLatched: boolean | null
  224. // Sticky-on latch for FAST_MODE_BETA_HEADER. Once fast mode is first
  225. // enabled, keep sending the header so cooldown enter/exit doesn't
  226. // double-bust the prompt cache. The `speed` body param stays dynamic.
  227. fastModeHeaderLatched: boolean | null
  228. // Sticky-on latch for the cache-editing beta header. Once cached
  229. // microcompact is first enabled, keep sending the header so mid-session
  230. // GrowthBook/settings toggles don't bust the prompt cache.
  231. cacheEditingHeaderLatched: boolean | null
  232. // Sticky-on latch for clearing thinking from prior tool loops. Triggered
  233. // when >1h since last API call (confirmed cache miss — no cache-hit
  234. // benefit to keeping thinking). Once latched, stays on so the newly-warmed
  235. // thinking-cleared cache isn't busted by flipping back to keep:'all'.
  236. thinkingClearLatched: boolean | null
  237. // Current prompt ID (UUID) correlating a user prompt with subsequent OTel events
  238. promptId: string | null
  239. // Last API requestId for the main conversation chain (not subagents).
  240. // Updated after each successful API response for main-session queries.
  241. // Read at shutdown to send cache eviction hints to inference.
  242. lastMainRequestId: string | undefined
  243. // Timestamp (Date.now()) of the last successful API call completion.
  244. // Used to compute timeSinceLastApiCallMs in tengu_api_success for
  245. // correlating cache misses with idle time (cache TTL is ~5min).
  246. lastApiCompletionTimestamp: number | null
  247. // Set to true after compaction (auto or manual /compact). Consumed by
  248. // logAPISuccess to tag the first post-compaction API call so we can
  249. // distinguish compaction-induced cache misses from TTL expiry.
  250. pendingPostCompaction: boolean
  251. }
  252. // ALSO HERE - THINK THRICE BEFORE MODIFYING
  253. function getInitialState(): State {
  254. // Resolve symlinks in cwd to match behavior of shell.ts setCwd
  255. // This ensures consistency with how paths are sanitized for session storage
  256. let resolvedCwd = ''
  257. if (
  258. typeof process !== 'undefined' &&
  259. typeof process.cwd === 'function' &&
  260. typeof realpathSync === 'function'
  261. ) {
  262. const rawCwd = cwd()
  263. try {
  264. resolvedCwd = realpathSync(rawCwd).normalize('NFC')
  265. } catch {
  266. // File Provider EPERM on CloudStorage mounts (lstat per path component).
  267. resolvedCwd = rawCwd.normalize('NFC')
  268. }
  269. }
  270. const state: State = {
  271. originalCwd: resolvedCwd,
  272. projectRoot: resolvedCwd,
  273. totalCostUSD: 0,
  274. totalAPIDuration: 0,
  275. totalAPIDurationWithoutRetries: 0,
  276. totalToolDuration: 0,
  277. turnHookDurationMs: 0,
  278. turnToolDurationMs: 0,
  279. turnClassifierDurationMs: 0,
  280. turnToolCount: 0,
  281. turnHookCount: 0,
  282. turnClassifierCount: 0,
  283. startTime: Date.now(),
  284. lastInteractionTime: Date.now(),
  285. totalLinesAdded: 0,
  286. totalLinesRemoved: 0,
  287. hasUnknownModelCost: false,
  288. cwd: resolvedCwd,
  289. modelUsage: {},
  290. mainLoopModelOverride: undefined,
  291. initialMainLoopModel: null,
  292. modelStrings: null,
  293. isInteractive: false,
  294. kairosActive: false,
  295. strictToolResultPairing: false,
  296. sdkAgentProgressSummariesEnabled: false,
  297. userMsgOptIn: false,
  298. clientType: 'cli',
  299. sessionSource: undefined,
  300. questionPreviewFormat: undefined,
  301. sessionIngressToken: undefined,
  302. oauthTokenFromFd: undefined,
  303. apiKeyFromFd: undefined,
  304. flagSettingsPath: undefined,
  305. flagSettingsInline: null,
  306. allowedSettingSources: [
  307. 'userSettings',
  308. 'projectSettings',
  309. 'localSettings',
  310. 'flagSettings',
  311. 'policySettings',
  312. ],
  313. // Telemetry state
  314. meter: null,
  315. sessionCounter: null,
  316. locCounter: null,
  317. prCounter: null,
  318. commitCounter: null,
  319. costCounter: null,
  320. tokenCounter: null,
  321. codeEditToolDecisionCounter: null,
  322. activeTimeCounter: null,
  323. statsStore: null,
  324. sessionId: randomUUID() as SessionId,
  325. parentSessionId: undefined,
  326. // Logger state
  327. loggerProvider: null,
  328. eventLogger: null,
  329. // Meter provider state
  330. meterProvider: null,
  331. tracerProvider: null,
  332. // Agent color state
  333. agentColorMap: new Map(),
  334. agentColorIndex: 0,
  335. // Last API request for bug reports
  336. lastAPIRequest: null,
  337. lastAPIRequestMessages: null,
  338. // Last auto-mode classifier request(s) for /share transcript
  339. lastClassifierRequests: null,
  340. cachedClaudeMdContent: null,
  341. // In-memory error log for recent errors
  342. inMemoryErrorLog: [],
  343. // Session-only plugins from --plugin-dir flag
  344. inlinePlugins: [],
  345. // Explicit --chrome / --no-chrome flag value (undefined = not set on CLI)
  346. chromeFlagOverride: undefined,
  347. // Use cowork_plugins directory instead of plugins
  348. useCoworkPlugins: false,
  349. // Session-only bypass permissions mode flag (not persisted)
  350. sessionBypassPermissionsMode: false,
  351. // Scheduled tasks disabled until flag or dialog enables them
  352. scheduledTasksEnabled: false,
  353. sessionCronTasks: [],
  354. sessionCreatedTeams: new Set(),
  355. // Session-only trust flag (not persisted to disk)
  356. sessionTrustAccepted: false,
  357. // Session-only flag to disable session persistence to disk
  358. sessionPersistenceDisabled: false,
  359. // Track if user has exited plan mode in this session
  360. hasExitedPlanMode: false,
  361. // Track if we need to show the plan mode exit attachment
  362. needsPlanModeExitAttachment: false,
  363. // Track if we need to show the auto mode exit attachment
  364. needsAutoModeExitAttachment: false,
  365. // Track if LSP plugin recommendation has been shown this session
  366. lspRecommendationShownThisSession: false,
  367. // SDK init event state
  368. initJsonSchema: null,
  369. registeredHooks: null,
  370. // Cache for plan slugs
  371. planSlugCache: new Map(),
  372. // Track teleported session for reliability logging
  373. teleportedSessionInfo: null,
  374. // Track invoked skills for preservation across compaction
  375. invokedSkills: new Map(),
  376. // Track slow operations for dev bar display
  377. slowOperations: [],
  378. // SDK-provided betas
  379. sdkBetas: undefined,
  380. // Main thread agent type
  381. mainThreadAgentType: undefined,
  382. // Remote mode
  383. isRemoteMode: false,
  384. ...(process.env.USER_TYPE === 'ant'
  385. ? {
  386. replBridgeActive: false,
  387. }
  388. : {}),
  389. // Direct connect server URL
  390. directConnectServerUrl: undefined,
  391. // System prompt section cache state
  392. systemPromptSectionCache: new Map(),
  393. // Last date emitted to the model
  394. lastEmittedDate: null,
  395. // Additional directories from --add-dir flag (for CLAUDE.md loading)
  396. additionalDirectoriesForClaudeMd: [],
  397. // Channel server allowlist from --channels flag
  398. allowedChannels: [],
  399. hasDevChannels: false,
  400. // Session project dir (null = derive from originalCwd)
  401. sessionProjectDir: null,
  402. // Prompt cache 1h allowlist (null = not yet fetched from GrowthBook)
  403. promptCache1hAllowlist: null,
  404. // Prompt cache 1h eligibility (null = not yet evaluated)
  405. promptCache1hEligible: null,
  406. // Beta header latches (null = not yet triggered)
  407. afkModeHeaderLatched: null,
  408. fastModeHeaderLatched: null,
  409. cacheEditingHeaderLatched: null,
  410. thinkingClearLatched: null,
  411. // Current prompt ID
  412. promptId: null,
  413. lastMainRequestId: undefined,
  414. lastApiCompletionTimestamp: null,
  415. pendingPostCompaction: false,
  416. }
  417. return state
  418. }
  419. // AND ESPECIALLY HERE
  420. const STATE: State = getInitialState()
  421. export function getSessionId(): SessionId {
  422. return STATE.sessionId
  423. }
  424. export function regenerateSessionId(
  425. options: { setCurrentAsParent?: boolean } = {},
  426. ): SessionId {
  427. if (options.setCurrentAsParent) {
  428. STATE.parentSessionId = STATE.sessionId
  429. }
  430. // Drop the outgoing session's plan-slug entry so the Map doesn't
  431. // accumulate stale keys. Callers that need to carry the slug across
  432. // (REPL.tsx clearContext) read it before calling clearConversation.
  433. STATE.planSlugCache.delete(STATE.sessionId)
  434. // Regenerated sessions live in the current project: reset projectDir to
  435. // null so getTranscriptPath() derives from originalCwd.
  436. STATE.sessionId = randomUUID() as SessionId
  437. STATE.sessionProjectDir = null
  438. return STATE.sessionId
  439. }
  440. export function getParentSessionId(): SessionId | undefined {
  441. return STATE.parentSessionId
  442. }
  443. /**
  444. * Atomically switch the active session. `sessionId` and `sessionProjectDir`
  445. * always change together — there is no separate setter for either, so they
  446. * cannot drift out of sync (CC-34).
  447. *
  448. * @param projectDir — directory containing `<sessionId>.jsonl`. Omit (or
  449. * pass `null`) for sessions in the current project — the path will derive
  450. * from originalCwd at read time. Pass `dirname(transcriptPath)` when the
  451. * session lives in a different project directory (git worktrees,
  452. * cross-project resume). Every call resets the project dir; it never
  453. * carries over from the previous session.
  454. */
  455. export function switchSession(
  456. sessionId: SessionId,
  457. projectDir: string | null = null,
  458. ): void {
  459. // Drop the outgoing session's plan-slug entry so the Map stays bounded
  460. // across repeated /resume. Only the current session's slug is ever read
  461. // (plans.ts getPlanSlug defaults to getSessionId()).
  462. STATE.planSlugCache.delete(STATE.sessionId)
  463. STATE.sessionId = sessionId
  464. STATE.sessionProjectDir = projectDir
  465. sessionSwitched.emit(sessionId)
  466. }
  467. const sessionSwitched = createSignal<[id: SessionId]>()
  468. /**
  469. * Register a callback that fires when switchSession changes the active
  470. * sessionId. bootstrap can't import listeners directly (DAG leaf), so
  471. * callers register themselves. concurrentSessions.ts uses this to keep the
  472. * PID file's sessionId in sync with --resume.
  473. */
  474. export const onSessionSwitch = sessionSwitched.subscribe
  475. /**
  476. * Project directory the current session's transcript lives in, or `null` if
  477. * the session was created in the current project (common case — derive from
  478. * originalCwd). See `switchSession()`.
  479. */
  480. export function getSessionProjectDir(): string | null {
  481. return STATE.sessionProjectDir
  482. }
  483. export function getOriginalCwd(): string {
  484. return STATE.originalCwd
  485. }
  486. /**
  487. * Get the stable project root directory.
  488. * Unlike getOriginalCwd(), this is never updated by mid-session EnterWorktreeTool
  489. * (so skills/history stay stable when entering a throwaway worktree).
  490. * It IS set at startup by --worktree, since that worktree is the session's project.
  491. * Use for project identity (history, skills, sessions) not file operations.
  492. */
  493. export function getProjectRoot(): string {
  494. return STATE.projectRoot
  495. }
  496. export function setOriginalCwd(cwd: string): void {
  497. STATE.originalCwd = cwd.normalize('NFC')
  498. }
  499. /**
  500. * Only for --worktree startup flag. Mid-session EnterWorktreeTool must NOT
  501. * call this — skills/history should stay anchored to where the session started.
  502. */
  503. export function setProjectRoot(cwd: string): void {
  504. STATE.projectRoot = cwd.normalize('NFC')
  505. }
  506. export function getCwdState(): string {
  507. return STATE.cwd
  508. }
  509. export function setCwdState(cwd: string): void {
  510. STATE.cwd = cwd.normalize('NFC')
  511. }
  512. export function getDirectConnectServerUrl(): string | undefined {
  513. return STATE.directConnectServerUrl
  514. }
  515. export function setDirectConnectServerUrl(url: string): void {
  516. STATE.directConnectServerUrl = url
  517. }
  518. export function addToTotalDurationState(
  519. duration: number,
  520. durationWithoutRetries: number,
  521. ): void {
  522. STATE.totalAPIDuration += duration
  523. STATE.totalAPIDurationWithoutRetries += durationWithoutRetries
  524. }
  525. export function resetTotalDurationStateAndCost_FOR_TESTS_ONLY(): void {
  526. STATE.totalAPIDuration = 0
  527. STATE.totalAPIDurationWithoutRetries = 0
  528. STATE.totalCostUSD = 0
  529. }
  530. export function addToTotalCostState(
  531. cost: number,
  532. modelUsage: ModelUsage,
  533. model: string,
  534. ): void {
  535. STATE.modelUsage[model] = modelUsage
  536. STATE.totalCostUSD += cost
  537. }
  538. export function getTotalCostUSD(): number {
  539. return STATE.totalCostUSD
  540. }
  541. export function getTotalAPIDuration(): number {
  542. return STATE.totalAPIDuration
  543. }
  544. export function getTotalDuration(): number {
  545. return Date.now() - STATE.startTime
  546. }
  547. export function getTotalAPIDurationWithoutRetries(): number {
  548. return STATE.totalAPIDurationWithoutRetries
  549. }
  550. export function getTotalToolDuration(): number {
  551. return STATE.totalToolDuration
  552. }
  553. export function addToToolDuration(duration: number): void {
  554. STATE.totalToolDuration += duration
  555. STATE.turnToolDurationMs += duration
  556. STATE.turnToolCount++
  557. }
  558. export function getTurnHookDurationMs(): number {
  559. return STATE.turnHookDurationMs
  560. }
  561. export function addToTurnHookDuration(duration: number): void {
  562. STATE.turnHookDurationMs += duration
  563. STATE.turnHookCount++
  564. }
  565. export function resetTurnHookDuration(): void {
  566. STATE.turnHookDurationMs = 0
  567. STATE.turnHookCount = 0
  568. }
  569. export function getTurnHookCount(): number {
  570. return STATE.turnHookCount
  571. }
  572. export function getTurnToolDurationMs(): number {
  573. return STATE.turnToolDurationMs
  574. }
  575. export function resetTurnToolDuration(): void {
  576. STATE.turnToolDurationMs = 0
  577. STATE.turnToolCount = 0
  578. }
  579. export function getTurnToolCount(): number {
  580. return STATE.turnToolCount
  581. }
  582. export function getTurnClassifierDurationMs(): number {
  583. return STATE.turnClassifierDurationMs
  584. }
  585. export function addToTurnClassifierDuration(duration: number): void {
  586. STATE.turnClassifierDurationMs += duration
  587. STATE.turnClassifierCount++
  588. }
  589. export function resetTurnClassifierDuration(): void {
  590. STATE.turnClassifierDurationMs = 0
  591. STATE.turnClassifierCount = 0
  592. }
  593. export function getTurnClassifierCount(): number {
  594. return STATE.turnClassifierCount
  595. }
  596. export function getStatsStore(): {
  597. observe(name: string, value: number): void
  598. } | null {
  599. return STATE.statsStore
  600. }
  601. export function setStatsStore(
  602. store: { observe(name: string, value: number): void } | null,
  603. ): void {
  604. STATE.statsStore = store
  605. }
  606. /**
  607. * Marks that an interaction occurred.
  608. *
  609. * By default the actual Date.now() call is deferred until the next Ink render
  610. * frame (via flushInteractionTime()) so we avoid calling Date.now() on every
  611. * single keypress.
  612. *
  613. * Pass `immediate = true` when calling from React useEffect callbacks or
  614. * other code that runs *after* the Ink render cycle has already flushed.
  615. * Without it the timestamp stays stale until the next render, which may never
  616. * come if the user is idle (e.g. permission dialog waiting for input).
  617. */
  618. let interactionTimeDirty = false
  619. export function updateLastInteractionTime(immediate?: boolean): void {
  620. if (immediate) {
  621. flushInteractionTime_inner()
  622. } else {
  623. interactionTimeDirty = true
  624. }
  625. }
  626. /**
  627. * If an interaction was recorded since the last flush, update the timestamp
  628. * now. Called by Ink before each render cycle so we batch many keypresses into
  629. * a single Date.now() call.
  630. */
  631. export function flushInteractionTime(): void {
  632. if (interactionTimeDirty) {
  633. flushInteractionTime_inner()
  634. }
  635. }
  636. function flushInteractionTime_inner(): void {
  637. STATE.lastInteractionTime = Date.now()
  638. interactionTimeDirty = false
  639. }
  640. export function addToTotalLinesChanged(added: number, removed: number): void {
  641. STATE.totalLinesAdded += added
  642. STATE.totalLinesRemoved += removed
  643. }
  644. export function getTotalLinesAdded(): number {
  645. return STATE.totalLinesAdded
  646. }
  647. export function getTotalLinesRemoved(): number {
  648. return STATE.totalLinesRemoved
  649. }
  650. export function getTotalInputTokens(): number {
  651. return sumBy(Object.values(STATE.modelUsage), 'inputTokens')
  652. }
  653. export function getTotalOutputTokens(): number {
  654. return sumBy(Object.values(STATE.modelUsage), 'outputTokens')
  655. }
  656. export function getTotalCacheReadInputTokens(): number {
  657. return sumBy(Object.values(STATE.modelUsage), 'cacheReadInputTokens')
  658. }
  659. export function getTotalCacheCreationInputTokens(): number {
  660. return sumBy(Object.values(STATE.modelUsage), 'cacheCreationInputTokens')
  661. }
  662. export function getTotalWebSearchRequests(): number {
  663. return sumBy(Object.values(STATE.modelUsage), 'webSearchRequests')
  664. }
  665. let outputTokensAtTurnStart = 0
  666. let currentTurnTokenBudget: number | null = null
  667. export function getTurnOutputTokens(): number {
  668. return getTotalOutputTokens() - outputTokensAtTurnStart
  669. }
  670. export function getCurrentTurnTokenBudget(): number | null {
  671. return currentTurnTokenBudget
  672. }
  673. let budgetContinuationCount = 0
  674. export function snapshotOutputTokensForTurn(budget: number | null): void {
  675. outputTokensAtTurnStart = getTotalOutputTokens()
  676. currentTurnTokenBudget = budget
  677. budgetContinuationCount = 0
  678. }
  679. export function getBudgetContinuationCount(): number {
  680. return budgetContinuationCount
  681. }
  682. export function incrementBudgetContinuationCount(): void {
  683. budgetContinuationCount++
  684. }
  685. export function setHasUnknownModelCost(): void {
  686. STATE.hasUnknownModelCost = true
  687. }
  688. export function hasUnknownModelCost(): boolean {
  689. return STATE.hasUnknownModelCost
  690. }
  691. export function getLastMainRequestId(): string | undefined {
  692. return STATE.lastMainRequestId
  693. }
  694. export function setLastMainRequestId(requestId: string): void {
  695. STATE.lastMainRequestId = requestId
  696. }
  697. export function getLastApiCompletionTimestamp(): number | null {
  698. return STATE.lastApiCompletionTimestamp
  699. }
  700. export function setLastApiCompletionTimestamp(timestamp: number): void {
  701. STATE.lastApiCompletionTimestamp = timestamp
  702. }
  703. /** Mark that a compaction just occurred. The next API success event will
  704. * include isPostCompaction=true, then the flag auto-resets. */
  705. export function markPostCompaction(): void {
  706. STATE.pendingPostCompaction = true
  707. }
  708. /** Consume the post-compaction flag. Returns true once after compaction,
  709. * then returns false until the next compaction. */
  710. export function consumePostCompaction(): boolean {
  711. const was = STATE.pendingPostCompaction
  712. STATE.pendingPostCompaction = false
  713. return was
  714. }
  715. export function getLastInteractionTime(): number {
  716. return STATE.lastInteractionTime
  717. }
  718. // Scroll drain suspension — background intervals check this before doing work
  719. // so they don't compete with scroll frames for the event loop. Set by
  720. // ScrollBox scrollBy/scrollTo, cleared SCROLL_DRAIN_IDLE_MS after the last
  721. // scroll event. Module-scope (not in STATE) — ephemeral hot-path flag, no
  722. // test-reset needed since the debounce timer self-clears.
  723. let scrollDraining = false
  724. let scrollDrainTimer: ReturnType<typeof setTimeout> | undefined
  725. const SCROLL_DRAIN_IDLE_MS = 150
  726. /** Mark that a scroll event just happened. Background intervals gate on
  727. * getIsScrollDraining() and skip their work until the debounce clears. */
  728. export function markScrollActivity(): void {
  729. scrollDraining = true
  730. if (scrollDrainTimer) clearTimeout(scrollDrainTimer)
  731. scrollDrainTimer = setTimeout(() => {
  732. scrollDraining = false
  733. scrollDrainTimer = undefined
  734. }, SCROLL_DRAIN_IDLE_MS)
  735. scrollDrainTimer.unref?.()
  736. }
  737. /** True while scroll is actively draining (within 150ms of last event).
  738. * Intervals should early-return when this is set — the work picks up next
  739. * tick after scroll settles. */
  740. export function getIsScrollDraining(): boolean {
  741. return scrollDraining
  742. }
  743. /** Await this before expensive one-shot work (network, subprocess) that could
  744. * coincide with scroll. Resolves immediately if not scrolling; otherwise
  745. * polls at the idle interval until the flag clears. */
  746. export async function waitForScrollIdle(): Promise<void> {
  747. while (scrollDraining) {
  748. // bootstrap-isolation forbids importing sleep() from src/utils/
  749. // eslint-disable-next-line no-restricted-syntax
  750. await new Promise(r => setTimeout(r, SCROLL_DRAIN_IDLE_MS).unref?.())
  751. }
  752. }
  753. export function getModelUsage(): { [modelName: string]: ModelUsage } {
  754. return STATE.modelUsage
  755. }
  756. export function getUsageForModel(model: string): ModelUsage | undefined {
  757. return STATE.modelUsage[model]
  758. }
  759. /**
  760. * Gets the model override set from the --model CLI flag or after the user
  761. * updates their configured model.
  762. */
  763. export function getMainLoopModelOverride(): ModelSetting | undefined {
  764. return STATE.mainLoopModelOverride
  765. }
  766. export function getInitialMainLoopModel(): ModelSetting {
  767. return STATE.initialMainLoopModel
  768. }
  769. export function setMainLoopModelOverride(
  770. model: ModelSetting | undefined,
  771. ): void {
  772. STATE.mainLoopModelOverride = model
  773. }
  774. export function setInitialMainLoopModel(model: ModelSetting): void {
  775. STATE.initialMainLoopModel = model
  776. }
  777. export function getSdkBetas(): string[] | undefined {
  778. return STATE.sdkBetas
  779. }
  780. export function setSdkBetas(betas: string[] | undefined): void {
  781. STATE.sdkBetas = betas
  782. }
  783. export function resetCostState(): void {
  784. STATE.totalCostUSD = 0
  785. STATE.totalAPIDuration = 0
  786. STATE.totalAPIDurationWithoutRetries = 0
  787. STATE.totalToolDuration = 0
  788. STATE.startTime = Date.now()
  789. STATE.totalLinesAdded = 0
  790. STATE.totalLinesRemoved = 0
  791. STATE.hasUnknownModelCost = false
  792. STATE.modelUsage = {}
  793. STATE.promptId = null
  794. }
  795. /**
  796. * Sets cost state values for session restore.
  797. * Called by restoreCostStateForSession in cost-tracker.ts.
  798. */
  799. export function setCostStateForRestore({
  800. totalCostUSD,
  801. totalAPIDuration,
  802. totalAPIDurationWithoutRetries,
  803. totalToolDuration,
  804. totalLinesAdded,
  805. totalLinesRemoved,
  806. lastDuration,
  807. modelUsage,
  808. }: {
  809. totalCostUSD: number
  810. totalAPIDuration: number
  811. totalAPIDurationWithoutRetries: number
  812. totalToolDuration: number
  813. totalLinesAdded: number
  814. totalLinesRemoved: number
  815. lastDuration: number | undefined
  816. modelUsage: { [modelName: string]: ModelUsage } | undefined
  817. }): void {
  818. STATE.totalCostUSD = totalCostUSD
  819. STATE.totalAPIDuration = totalAPIDuration
  820. STATE.totalAPIDurationWithoutRetries = totalAPIDurationWithoutRetries
  821. STATE.totalToolDuration = totalToolDuration
  822. STATE.totalLinesAdded = totalLinesAdded
  823. STATE.totalLinesRemoved = totalLinesRemoved
  824. // Restore per-model usage breakdown
  825. if (modelUsage) {
  826. STATE.modelUsage = modelUsage
  827. }
  828. // Adjust startTime to make wall duration accumulate
  829. if (lastDuration) {
  830. STATE.startTime = Date.now() - lastDuration
  831. }
  832. }
  833. // Only used in tests
  834. export function resetStateForTests(): void {
  835. if (process.env.NODE_ENV !== 'test') {
  836. throw new Error('resetStateForTests can only be called in tests')
  837. }
  838. Object.entries(getInitialState()).forEach(([key, value]) => {
  839. STATE[key as keyof State] = value as never
  840. })
  841. outputTokensAtTurnStart = 0
  842. currentTurnTokenBudget = null
  843. budgetContinuationCount = 0
  844. sessionSwitched.clear()
  845. }
  846. // You shouldn't use this directly. See src/utils/model/modelStrings.ts::getModelStrings()
  847. export function getModelStrings(): ModelStrings | null {
  848. return STATE.modelStrings
  849. }
  850. // You shouldn't use this directly. See src/utils/model/modelStrings.ts
  851. export function setModelStrings(modelStrings: ModelStrings): void {
  852. STATE.modelStrings = modelStrings
  853. }
  854. // Test utility function to reset model strings for re-initialization.
  855. // Separate from setModelStrings because we only want to accept 'null' in tests.
  856. export function resetModelStringsForTestingOnly() {
  857. STATE.modelStrings = null
  858. }
  859. export function setMeter(
  860. meter: Meter,
  861. createCounter: (name: string, options: MetricOptions) => AttributedCounter,
  862. ): void {
  863. STATE.meter = meter
  864. // Initialize all counters using the provided factory
  865. STATE.sessionCounter = createCounter('claude_code.session.count', {
  866. description: 'Count of CLI sessions started',
  867. })
  868. STATE.locCounter = createCounter('claude_code.lines_of_code.count', {
  869. description:
  870. "Count of lines of code modified, with the 'type' attribute indicating whether lines were added or removed",
  871. })
  872. STATE.prCounter = createCounter('claude_code.pull_request.count', {
  873. description: 'Number of pull requests created',
  874. })
  875. STATE.commitCounter = createCounter('claude_code.commit.count', {
  876. description: 'Number of git commits created',
  877. })
  878. STATE.costCounter = createCounter('claude_code.cost.usage', {
  879. description: 'Cost of the Claude Code session',
  880. unit: 'USD',
  881. })
  882. STATE.tokenCounter = createCounter('claude_code.token.usage', {
  883. description: 'Number of tokens used',
  884. unit: 'tokens',
  885. })
  886. STATE.codeEditToolDecisionCounter = createCounter(
  887. 'claude_code.code_edit_tool.decision',
  888. {
  889. description:
  890. 'Count of code editing tool permission decisions (accept/reject) for Edit, Write, and NotebookEdit tools',
  891. },
  892. )
  893. STATE.activeTimeCounter = createCounter('claude_code.active_time.total', {
  894. description: 'Total active time in seconds',
  895. unit: 's',
  896. })
  897. }
  898. export function getMeter(): Meter | null {
  899. return STATE.meter
  900. }
  901. export function getSessionCounter(): AttributedCounter | null {
  902. return STATE.sessionCounter
  903. }
  904. export function getLocCounter(): AttributedCounter | null {
  905. return STATE.locCounter
  906. }
  907. export function getPrCounter(): AttributedCounter | null {
  908. return STATE.prCounter
  909. }
  910. export function getCommitCounter(): AttributedCounter | null {
  911. return STATE.commitCounter
  912. }
  913. export function getCostCounter(): AttributedCounter | null {
  914. return STATE.costCounter
  915. }
  916. export function getTokenCounter(): AttributedCounter | null {
  917. return STATE.tokenCounter
  918. }
  919. export function getCodeEditToolDecisionCounter(): AttributedCounter | null {
  920. return STATE.codeEditToolDecisionCounter
  921. }
  922. export function getActiveTimeCounter(): AttributedCounter | null {
  923. return STATE.activeTimeCounter
  924. }
  925. export function getLoggerProvider(): LoggerProvider | null {
  926. return STATE.loggerProvider
  927. }
  928. export function setLoggerProvider(provider: LoggerProvider | null): void {
  929. STATE.loggerProvider = provider
  930. }
  931. export function getEventLogger(): ReturnType<typeof logs.getLogger> | null {
  932. return STATE.eventLogger
  933. }
  934. export function setEventLogger(
  935. logger: ReturnType<typeof logs.getLogger> | null,
  936. ): void {
  937. STATE.eventLogger = logger
  938. }
  939. export function getMeterProvider(): MeterProvider | null {
  940. return STATE.meterProvider
  941. }
  942. export function setMeterProvider(provider: MeterProvider | null): void {
  943. STATE.meterProvider = provider
  944. }
  945. export function getTracerProvider(): BasicTracerProvider | null {
  946. return STATE.tracerProvider
  947. }
  948. export function setTracerProvider(provider: BasicTracerProvider | null): void {
  949. STATE.tracerProvider = provider
  950. }
  951. export function getIsNonInteractiveSession(): boolean {
  952. return !STATE.isInteractive
  953. }
  954. export function getIsInteractive(): boolean {
  955. return STATE.isInteractive
  956. }
  957. export function setIsInteractive(value: boolean): void {
  958. STATE.isInteractive = value
  959. }
  960. export function getClientType(): string {
  961. return STATE.clientType
  962. }
  963. export function setClientType(type: string): void {
  964. STATE.clientType = type
  965. }
  966. export function getSdkAgentProgressSummariesEnabled(): boolean {
  967. return STATE.sdkAgentProgressSummariesEnabled
  968. }
  969. export function setSdkAgentProgressSummariesEnabled(value: boolean): void {
  970. STATE.sdkAgentProgressSummariesEnabled = value
  971. }
  972. export function getKairosActive(): boolean {
  973. return STATE.kairosActive
  974. }
  975. export function setKairosActive(value: boolean): void {
  976. STATE.kairosActive = value
  977. }
  978. export function getStrictToolResultPairing(): boolean {
  979. return STATE.strictToolResultPairing
  980. }
  981. export function setStrictToolResultPairing(value: boolean): void {
  982. STATE.strictToolResultPairing = value
  983. }
  984. // Field name 'userMsgOptIn' avoids excluded-string substrings ('BriefTool',
  985. // 'SendUserMessage' — case-insensitive). All callers are inside feature()
  986. // guards so these accessors don't need their own (matches getKairosActive).
  987. export function getUserMsgOptIn(): boolean {
  988. return STATE.userMsgOptIn
  989. }
  990. export function setUserMsgOptIn(value: boolean): void {
  991. STATE.userMsgOptIn = value
  992. }
  993. export function getSessionSource(): string | undefined {
  994. return STATE.sessionSource
  995. }
  996. export function setSessionSource(source: string): void {
  997. STATE.sessionSource = source
  998. }
  999. export function getQuestionPreviewFormat(): 'markdown' | 'html' | undefined {
  1000. return STATE.questionPreviewFormat
  1001. }
  1002. export function setQuestionPreviewFormat(format: 'markdown' | 'html'): void {
  1003. STATE.questionPreviewFormat = format
  1004. }
  1005. export function getAgentColorMap(): Map<string, AgentColorName> {
  1006. return STATE.agentColorMap
  1007. }
  1008. export function getFlagSettingsPath(): string | undefined {
  1009. return STATE.flagSettingsPath
  1010. }
  1011. export function setFlagSettingsPath(path: string | undefined): void {
  1012. STATE.flagSettingsPath = path
  1013. }
  1014. export function getFlagSettingsInline(): Record<string, unknown> | null {
  1015. return STATE.flagSettingsInline
  1016. }
  1017. export function setFlagSettingsInline(
  1018. settings: Record<string, unknown> | null,
  1019. ): void {
  1020. STATE.flagSettingsInline = settings
  1021. }
  1022. export function getSessionIngressToken(): string | null | undefined {
  1023. return STATE.sessionIngressToken
  1024. }
  1025. export function setSessionIngressToken(token: string | null): void {
  1026. STATE.sessionIngressToken = token
  1027. }
  1028. export function getOauthTokenFromFd(): string | null | undefined {
  1029. return STATE.oauthTokenFromFd
  1030. }
  1031. export function setOauthTokenFromFd(token: string | null): void {
  1032. STATE.oauthTokenFromFd = token
  1033. }
  1034. export function getApiKeyFromFd(): string | null | undefined {
  1035. return STATE.apiKeyFromFd
  1036. }
  1037. export function setApiKeyFromFd(key: string | null): void {
  1038. STATE.apiKeyFromFd = key
  1039. }
  1040. export function setLastAPIRequest(
  1041. params: Omit<BetaMessageStreamParams, 'messages'> | null,
  1042. ): void {
  1043. STATE.lastAPIRequest = params
  1044. }
  1045. export function getLastAPIRequest(): Omit<
  1046. BetaMessageStreamParams,
  1047. 'messages'
  1048. > | null {
  1049. return STATE.lastAPIRequest
  1050. }
  1051. export function setLastAPIRequestMessages(
  1052. messages: BetaMessageStreamParams['messages'] | null,
  1053. ): void {
  1054. STATE.lastAPIRequestMessages = messages
  1055. }
  1056. export function getLastAPIRequestMessages():
  1057. | BetaMessageStreamParams['messages']
  1058. | null {
  1059. return STATE.lastAPIRequestMessages
  1060. }
  1061. export function setLastClassifierRequests(requests: unknown[] | null): void {
  1062. STATE.lastClassifierRequests = requests
  1063. }
  1064. export function getLastClassifierRequests(): unknown[] | null {
  1065. return STATE.lastClassifierRequests
  1066. }
  1067. export function setCachedClaudeMdContent(content: string | null): void {
  1068. STATE.cachedClaudeMdContent = content
  1069. }
  1070. export function getCachedClaudeMdContent(): string | null {
  1071. return STATE.cachedClaudeMdContent
  1072. }
  1073. export function addToInMemoryErrorLog(errorInfo: {
  1074. error: string
  1075. timestamp: string
  1076. }): void {
  1077. const MAX_IN_MEMORY_ERRORS = 100
  1078. if (STATE.inMemoryErrorLog.length >= MAX_IN_MEMORY_ERRORS) {
  1079. STATE.inMemoryErrorLog.shift() // Remove oldest error
  1080. }
  1081. STATE.inMemoryErrorLog.push(errorInfo)
  1082. }
  1083. export function getAllowedSettingSources(): SettingSource[] {
  1084. return STATE.allowedSettingSources
  1085. }
  1086. export function setAllowedSettingSources(sources: SettingSource[]): void {
  1087. STATE.allowedSettingSources = sources
  1088. }
  1089. export function preferThirdPartyAuthentication(): boolean {
  1090. // IDE extension should behave as 1P for authentication reasons.
  1091. return getIsNonInteractiveSession() && STATE.clientType !== 'claude-vscode'
  1092. }
  1093. export function setInlinePlugins(plugins: Array<string>): void {
  1094. STATE.inlinePlugins = plugins
  1095. }
  1096. export function getInlinePlugins(): Array<string> {
  1097. return STATE.inlinePlugins
  1098. }
  1099. export function setChromeFlagOverride(value: boolean | undefined): void {
  1100. STATE.chromeFlagOverride = value
  1101. }
  1102. export function getChromeFlagOverride(): boolean | undefined {
  1103. return STATE.chromeFlagOverride
  1104. }
  1105. export function setUseCoworkPlugins(value: boolean): void {
  1106. STATE.useCoworkPlugins = value
  1107. resetSettingsCache()
  1108. }
  1109. export function getUseCoworkPlugins(): boolean {
  1110. return STATE.useCoworkPlugins
  1111. }
  1112. export function setSessionBypassPermissionsMode(enabled: boolean): void {
  1113. STATE.sessionBypassPermissionsMode = enabled
  1114. }
  1115. export function getSessionBypassPermissionsMode(): boolean {
  1116. return STATE.sessionBypassPermissionsMode
  1117. }
  1118. export function setScheduledTasksEnabled(enabled: boolean): void {
  1119. STATE.scheduledTasksEnabled = enabled
  1120. }
  1121. export function getScheduledTasksEnabled(): boolean {
  1122. return STATE.scheduledTasksEnabled
  1123. }
  1124. export type SessionCronTask = {
  1125. id: string
  1126. cron: string
  1127. prompt: string
  1128. createdAt: number
  1129. recurring?: boolean
  1130. /**
  1131. * When set, the task was created by an in-process teammate (not the team lead).
  1132. * The scheduler routes fires to that teammate's pendingUserMessages queue
  1133. * instead of the main REPL command queue. Session-only — never written to disk.
  1134. */
  1135. agentId?: string
  1136. }
  1137. export function getSessionCronTasks(): SessionCronTask[] {
  1138. return STATE.sessionCronTasks
  1139. }
  1140. export function addSessionCronTask(task: SessionCronTask): void {
  1141. STATE.sessionCronTasks.push(task)
  1142. }
  1143. /**
  1144. * Returns the number of tasks actually removed. Callers use this to skip
  1145. * downstream work (e.g. the disk read in removeCronTasks) when all ids
  1146. * were accounted for here.
  1147. */
  1148. export function removeSessionCronTasks(ids: readonly string[]): number {
  1149. if (ids.length === 0) return 0
  1150. const idSet = new Set(ids)
  1151. const remaining = STATE.sessionCronTasks.filter(t => !idSet.has(t.id))
  1152. const removed = STATE.sessionCronTasks.length - remaining.length
  1153. if (removed === 0) return 0
  1154. STATE.sessionCronTasks = remaining
  1155. return removed
  1156. }
  1157. export function setSessionTrustAccepted(accepted: boolean): void {
  1158. STATE.sessionTrustAccepted = accepted
  1159. }
  1160. export function getSessionTrustAccepted(): boolean {
  1161. return STATE.sessionTrustAccepted
  1162. }
  1163. export function setSessionPersistenceDisabled(disabled: boolean): void {
  1164. STATE.sessionPersistenceDisabled = disabled
  1165. }
  1166. export function isSessionPersistenceDisabled(): boolean {
  1167. return STATE.sessionPersistenceDisabled
  1168. }
  1169. export function hasExitedPlanModeInSession(): boolean {
  1170. return STATE.hasExitedPlanMode
  1171. }
  1172. export function setHasExitedPlanMode(value: boolean): void {
  1173. STATE.hasExitedPlanMode = value
  1174. }
  1175. export function needsPlanModeExitAttachment(): boolean {
  1176. return STATE.needsPlanModeExitAttachment
  1177. }
  1178. export function setNeedsPlanModeExitAttachment(value: boolean): void {
  1179. STATE.needsPlanModeExitAttachment = value
  1180. }
  1181. export function handlePlanModeTransition(
  1182. fromMode: string,
  1183. toMode: string,
  1184. ): void {
  1185. // If switching TO plan mode, clear any pending exit attachment
  1186. // This prevents sending both plan_mode and plan_mode_exit when user toggles quickly
  1187. if (toMode === 'plan' && fromMode !== 'plan') {
  1188. STATE.needsPlanModeExitAttachment = false
  1189. }
  1190. // If switching out of plan mode, trigger the plan_mode_exit attachment
  1191. if (fromMode === 'plan' && toMode !== 'plan') {
  1192. STATE.needsPlanModeExitAttachment = true
  1193. }
  1194. }
  1195. export function needsAutoModeExitAttachment(): boolean {
  1196. return STATE.needsAutoModeExitAttachment
  1197. }
  1198. export function setNeedsAutoModeExitAttachment(value: boolean): void {
  1199. STATE.needsAutoModeExitAttachment = value
  1200. }
  1201. export function handleAutoModeTransition(
  1202. fromMode: string,
  1203. toMode: string,
  1204. ): void {
  1205. // Auto↔plan transitions are handled by prepareContextForPlanMode (auto may
  1206. // stay active through plan if opted in) and ExitPlanMode (restores mode).
  1207. // Skip both directions so this function only handles direct auto transitions.
  1208. if (
  1209. (fromMode === 'auto' && toMode === 'plan') ||
  1210. (fromMode === 'plan' && toMode === 'auto')
  1211. ) {
  1212. return
  1213. }
  1214. const fromIsAuto = fromMode === 'auto'
  1215. const toIsAuto = toMode === 'auto'
  1216. // If switching TO auto mode, clear any pending exit attachment
  1217. // This prevents sending both auto_mode and auto_mode_exit when user toggles quickly
  1218. if (toIsAuto && !fromIsAuto) {
  1219. STATE.needsAutoModeExitAttachment = false
  1220. }
  1221. // If switching out of auto mode, trigger the auto_mode_exit attachment
  1222. if (fromIsAuto && !toIsAuto) {
  1223. STATE.needsAutoModeExitAttachment = true
  1224. }
  1225. }
  1226. // LSP plugin recommendation session tracking
  1227. export function hasShownLspRecommendationThisSession(): boolean {
  1228. return STATE.lspRecommendationShownThisSession
  1229. }
  1230. export function setLspRecommendationShownThisSession(value: boolean): void {
  1231. STATE.lspRecommendationShownThisSession = value
  1232. }
  1233. // SDK init event state
  1234. export function setInitJsonSchema(schema: Record<string, unknown>): void {
  1235. STATE.initJsonSchema = schema
  1236. }
  1237. export function getInitJsonSchema(): Record<string, unknown> | null {
  1238. return STATE.initJsonSchema
  1239. }
  1240. export function registerHookCallbacks(
  1241. hooks: Partial<Record<HookEvent, RegisteredHookMatcher[]>>,
  1242. ): void {
  1243. if (!STATE.registeredHooks) {
  1244. STATE.registeredHooks = {}
  1245. }
  1246. // `registerHookCallbacks` may be called multiple times, so we need to merge (not overwrite)
  1247. for (const [event, matchers] of Object.entries(hooks)) {
  1248. const eventKey = event as HookEvent
  1249. if (!STATE.registeredHooks[eventKey]) {
  1250. STATE.registeredHooks[eventKey] = []
  1251. }
  1252. STATE.registeredHooks[eventKey]!.push(...matchers)
  1253. }
  1254. }
  1255. export function getRegisteredHooks(): Partial<
  1256. Record<HookEvent, RegisteredHookMatcher[]>
  1257. > | null {
  1258. return STATE.registeredHooks
  1259. }
  1260. export function clearRegisteredHooks(): void {
  1261. STATE.registeredHooks = null
  1262. }
  1263. export function clearRegisteredPluginHooks(): void {
  1264. if (!STATE.registeredHooks) {
  1265. return
  1266. }
  1267. const filtered: Partial<Record<HookEvent, RegisteredHookMatcher[]>> = {}
  1268. for (const [event, matchers] of Object.entries(STATE.registeredHooks)) {
  1269. // Keep only callback hooks (those without pluginRoot)
  1270. const callbackHooks = matchers.filter(m => !('pluginRoot' in m))
  1271. if (callbackHooks.length > 0) {
  1272. filtered[event as HookEvent] = callbackHooks
  1273. }
  1274. }
  1275. STATE.registeredHooks = Object.keys(filtered).length > 0 ? filtered : null
  1276. }
  1277. export function resetSdkInitState(): void {
  1278. STATE.initJsonSchema = null
  1279. STATE.registeredHooks = null
  1280. }
  1281. export function getPlanSlugCache(): Map<string, string> {
  1282. return STATE.planSlugCache
  1283. }
  1284. export function getSessionCreatedTeams(): Set<string> {
  1285. return STATE.sessionCreatedTeams
  1286. }
  1287. // Teleported session tracking for reliability logging
  1288. export function setTeleportedSessionInfo(info: {
  1289. sessionId: string | null
  1290. }): void {
  1291. STATE.teleportedSessionInfo = {
  1292. isTeleported: true,
  1293. hasLoggedFirstMessage: false,
  1294. sessionId: info.sessionId,
  1295. }
  1296. }
  1297. export function getTeleportedSessionInfo(): {
  1298. isTeleported: boolean
  1299. hasLoggedFirstMessage: boolean
  1300. sessionId: string | null
  1301. } | null {
  1302. return STATE.teleportedSessionInfo
  1303. }
  1304. export function markFirstTeleportMessageLogged(): void {
  1305. if (STATE.teleportedSessionInfo) {
  1306. STATE.teleportedSessionInfo.hasLoggedFirstMessage = true
  1307. }
  1308. }
  1309. // Invoked skills tracking for preservation across compaction
  1310. export type InvokedSkillInfo = {
  1311. skillName: string
  1312. skillPath: string
  1313. content: string
  1314. invokedAt: number
  1315. agentId: string | null
  1316. }
  1317. export function addInvokedSkill(
  1318. skillName: string,
  1319. skillPath: string,
  1320. content: string,
  1321. agentId: string | null = null,
  1322. ): void {
  1323. const key = `${agentId ?? ''}:${skillName}`
  1324. STATE.invokedSkills.set(key, {
  1325. skillName,
  1326. skillPath,
  1327. content,
  1328. invokedAt: Date.now(),
  1329. agentId,
  1330. })
  1331. }
  1332. export function getInvokedSkills(): Map<string, InvokedSkillInfo> {
  1333. return STATE.invokedSkills
  1334. }
  1335. export function getInvokedSkillsForAgent(
  1336. agentId: string | undefined | null,
  1337. ): Map<string, InvokedSkillInfo> {
  1338. const normalizedId = agentId ?? null
  1339. const filtered = new Map<string, InvokedSkillInfo>()
  1340. for (const [key, skill] of STATE.invokedSkills) {
  1341. if (skill.agentId === normalizedId) {
  1342. filtered.set(key, skill)
  1343. }
  1344. }
  1345. return filtered
  1346. }
  1347. export function clearInvokedSkills(
  1348. preservedAgentIds?: ReadonlySet<string>,
  1349. ): void {
  1350. if (!preservedAgentIds || preservedAgentIds.size === 0) {
  1351. STATE.invokedSkills.clear()
  1352. return
  1353. }
  1354. for (const [key, skill] of STATE.invokedSkills) {
  1355. if (skill.agentId === null || !preservedAgentIds.has(skill.agentId)) {
  1356. STATE.invokedSkills.delete(key)
  1357. }
  1358. }
  1359. }
  1360. export function clearInvokedSkillsForAgent(agentId: string): void {
  1361. for (const [key, skill] of STATE.invokedSkills) {
  1362. if (skill.agentId === agentId) {
  1363. STATE.invokedSkills.delete(key)
  1364. }
  1365. }
  1366. }
  1367. // Slow operations tracking for dev bar
  1368. const MAX_SLOW_OPERATIONS = 10
  1369. const SLOW_OPERATION_TTL_MS = 10000
  1370. export function addSlowOperation(operation: string, durationMs: number): void {
  1371. if (process.env.USER_TYPE !== 'ant') return
  1372. // Skip tracking for editor sessions (user editing a prompt file in $EDITOR)
  1373. // These are intentionally slow since the user is drafting text
  1374. if (operation.includes('exec') && operation.includes('claude-prompt-')) {
  1375. return
  1376. }
  1377. const now = Date.now()
  1378. // Remove stale operations
  1379. STATE.slowOperations = STATE.slowOperations.filter(
  1380. op => now - op.timestamp < SLOW_OPERATION_TTL_MS,
  1381. )
  1382. // Add new operation
  1383. STATE.slowOperations.push({ operation, durationMs, timestamp: now })
  1384. // Keep only the most recent operations
  1385. if (STATE.slowOperations.length > MAX_SLOW_OPERATIONS) {
  1386. STATE.slowOperations = STATE.slowOperations.slice(-MAX_SLOW_OPERATIONS)
  1387. }
  1388. }
  1389. const EMPTY_SLOW_OPERATIONS: ReadonlyArray<{
  1390. operation: string
  1391. durationMs: number
  1392. timestamp: number
  1393. }> = []
  1394. export function getSlowOperations(): ReadonlyArray<{
  1395. operation: string
  1396. durationMs: number
  1397. timestamp: number
  1398. }> {
  1399. // Most common case: nothing tracked. Return a stable reference so the
  1400. // caller's setState() can bail via Object.is instead of re-rendering at 2fps.
  1401. if (STATE.slowOperations.length === 0) {
  1402. return EMPTY_SLOW_OPERATIONS
  1403. }
  1404. const now = Date.now()
  1405. // Only allocate a new array when something actually expired; otherwise keep
  1406. // the reference stable across polls while ops are still fresh.
  1407. if (
  1408. STATE.slowOperations.some(op => now - op.timestamp >= SLOW_OPERATION_TTL_MS)
  1409. ) {
  1410. STATE.slowOperations = STATE.slowOperations.filter(
  1411. op => now - op.timestamp < SLOW_OPERATION_TTL_MS,
  1412. )
  1413. if (STATE.slowOperations.length === 0) {
  1414. return EMPTY_SLOW_OPERATIONS
  1415. }
  1416. }
  1417. // Safe to return directly: addSlowOperation() reassigns STATE.slowOperations
  1418. // before pushing, so the array held in React state is never mutated.
  1419. return STATE.slowOperations
  1420. }
  1421. export function getMainThreadAgentType(): string | undefined {
  1422. return STATE.mainThreadAgentType
  1423. }
  1424. export function setMainThreadAgentType(agentType: string | undefined): void {
  1425. STATE.mainThreadAgentType = agentType
  1426. }
  1427. export function getIsRemoteMode(): boolean {
  1428. return STATE.isRemoteMode
  1429. }
  1430. export function setIsRemoteMode(value: boolean): void {
  1431. STATE.isRemoteMode = value
  1432. }
  1433. // System prompt section accessors
  1434. export function getSystemPromptSectionCache(): Map<string, string | null> {
  1435. return STATE.systemPromptSectionCache
  1436. }
  1437. export function setSystemPromptSectionCacheEntry(
  1438. name: string,
  1439. value: string | null,
  1440. ): void {
  1441. STATE.systemPromptSectionCache.set(name, value)
  1442. }
  1443. export function clearSystemPromptSectionState(): void {
  1444. STATE.systemPromptSectionCache.clear()
  1445. }
  1446. // Last emitted date accessors (for detecting midnight date changes)
  1447. export function getLastEmittedDate(): string | null {
  1448. return STATE.lastEmittedDate
  1449. }
  1450. export function setLastEmittedDate(date: string | null): void {
  1451. STATE.lastEmittedDate = date
  1452. }
  1453. export function getAdditionalDirectoriesForClaudeMd(): string[] {
  1454. return STATE.additionalDirectoriesForClaudeMd
  1455. }
  1456. export function setAdditionalDirectoriesForClaudeMd(
  1457. directories: string[],
  1458. ): void {
  1459. STATE.additionalDirectoriesForClaudeMd = directories
  1460. }
  1461. export function getAllowedChannels(): ChannelEntry[] {
  1462. return STATE.allowedChannels
  1463. }
  1464. export function setAllowedChannels(entries: ChannelEntry[]): void {
  1465. STATE.allowedChannels = entries
  1466. }
  1467. export function getHasDevChannels(): boolean {
  1468. return STATE.hasDevChannels
  1469. }
  1470. export function setHasDevChannels(value: boolean): void {
  1471. STATE.hasDevChannels = value
  1472. }
  1473. export function getPromptCache1hAllowlist(): string[] | null {
  1474. return STATE.promptCache1hAllowlist
  1475. }
  1476. export function setPromptCache1hAllowlist(allowlist: string[] | null): void {
  1477. STATE.promptCache1hAllowlist = allowlist
  1478. }
  1479. export function getPromptCache1hEligible(): boolean | null {
  1480. return STATE.promptCache1hEligible
  1481. }
  1482. export function setPromptCache1hEligible(eligible: boolean | null): void {
  1483. STATE.promptCache1hEligible = eligible
  1484. }
  1485. export function getAfkModeHeaderLatched(): boolean | null {
  1486. return STATE.afkModeHeaderLatched
  1487. }
  1488. export function setAfkModeHeaderLatched(v: boolean): void {
  1489. STATE.afkModeHeaderLatched = v
  1490. }
  1491. export function getFastModeHeaderLatched(): boolean | null {
  1492. return STATE.fastModeHeaderLatched
  1493. }
  1494. export function setFastModeHeaderLatched(v: boolean): void {
  1495. STATE.fastModeHeaderLatched = v
  1496. }
  1497. export function getCacheEditingHeaderLatched(): boolean | null {
  1498. return STATE.cacheEditingHeaderLatched
  1499. }
  1500. export function setCacheEditingHeaderLatched(v: boolean): void {
  1501. STATE.cacheEditingHeaderLatched = v
  1502. }
  1503. export function getThinkingClearLatched(): boolean | null {
  1504. return STATE.thinkingClearLatched
  1505. }
  1506. export function setThinkingClearLatched(v: boolean): void {
  1507. STATE.thinkingClearLatched = v
  1508. }
  1509. /**
  1510. * Reset beta header latches to null. Called on /clear and /compact so a
  1511. * fresh conversation gets fresh header evaluation.
  1512. */
  1513. export function clearBetaHeaderLatches(): void {
  1514. STATE.afkModeHeaderLatched = null
  1515. STATE.fastModeHeaderLatched = null
  1516. STATE.cacheEditingHeaderLatched = null
  1517. STATE.thinkingClearLatched = null
  1518. }
  1519. export function getPromptId(): string | null {
  1520. return STATE.promptId
  1521. }
  1522. export function setPromptId(id: string | null): void {
  1523. STATE.promptId = id
  1524. }
  1525. export type isReplBridgeActive = any;