state.ts 55 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748
  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. // 用于浏览器端构建的间接引用(package.json 的 "browser" 字段会将
  14. // crypto.ts 替换为 crypto.browser.ts)。纯叶子节点重新导出 node:crypto —
  15. // 零循环依赖风险。路径别名导入绕过了启动隔离(
  16. // 规则仅检查 ./ 和 / 前缀);显式禁用说明了意图。
  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. // 注册钩子的联合类型——可以是 SDK 回调或原生插件钩子
  26. type RegisteredHookMatcher = HookCallbackMatcher | PluginHookMatcher
  27. import type { SessionId } from 'src/types/ids.js'
  28. // 不要再加状态了——请谨慎对待全局状态
  29. // dev: 对于通过 --dangerously-load-development-channels 传入的条目为 true。
  30. // 白名单门控逐条目检查此项(而非会话范围的 hasDevChannels 标志),
  31. // 因此同时传入两个标志不会让开发对话框的接受行为泄漏白名单绕过权限到 --channels 条目。
  32. export type ChannelEntry =
  33. | { kind: 'plugin'; name: string; marketplace: string; dev?: boolean }
  34. | { kind: 'server'; name: string; dev?: boolean }
  35. export type AttributedCounter = {
  36. add(value: number, additionalAttributes?: Attributes): void
  37. }
  38. type State = {
  39. originalCwd: string
  40. // 稳定的项目根目录 - 启动时设置一次(包括 --worktree 标志),
  41. // 会话中途的 EnterWorktreeTool 永远不会更新它。
  42. // 用于项目标识(历史记录、技能、会话),不用于文件操作。
  43. projectRoot: string
  44. totalCostUSD: number
  45. totalAPIDuration: number
  46. totalAPIDurationWithoutRetries: number
  47. totalToolDuration: number
  48. turnHookDurationMs: number
  49. turnToolDurationMs: number
  50. turnClassifierDurationMs: number
  51. turnToolCount: number
  52. turnHookCount: number
  53. turnClassifierCount: number
  54. startTime: number
  55. lastInteractionTime: number
  56. totalLinesAdded: number
  57. totalLinesRemoved: number
  58. hasUnknownModelCost: boolean
  59. cwd: string
  60. modelUsage: { [modelName: string]: ModelUsage }
  61. mainLoopModelOverride: ModelSetting | undefined
  62. initialMainLoopModel: ModelSetting
  63. modelStrings: ModelStrings | null
  64. isInteractive: boolean
  65. kairosActive: boolean
  66. // 当为 true 时,ensureToolResultPairing 在不匹配时抛出异常,
  67. // 而不是用合成占位符修复。HFI 在启动时选择加入,因此
  68. // 轨迹会快速失败,而不是用假的 tool_result 训练模型。
  69. strictToolResultPairing: boolean
  70. sdkAgentProgressSummariesEnabled: boolean
  71. userMsgOptIn: boolean
  72. clientType: string
  73. sessionSource: string | undefined
  74. questionPreviewFormat: 'markdown' | 'html' | undefined
  75. flagSettingsPath: string | undefined
  76. flagSettingsInline: Record<string, unknown> | null
  77. allowedSettingSources: SettingSource[]
  78. sessionIngressToken: string | null | undefined
  79. oauthTokenFromFd: string | null | undefined
  80. apiKeyFromFd: string | null | undefined
  81. // Telemetry state
  82. meter: Meter | null
  83. sessionCounter: AttributedCounter | null
  84. locCounter: AttributedCounter | null
  85. prCounter: AttributedCounter | null
  86. commitCounter: AttributedCounter | null
  87. costCounter: AttributedCounter | null
  88. tokenCounter: AttributedCounter | null
  89. codeEditToolDecisionCounter: AttributedCounter | null
  90. activeTimeCounter: AttributedCounter | null
  91. statsStore: { observe(name: string, value: number): void } | null
  92. sessionId: SessionId
  93. // 父会话 ID,用于跟踪会话谱系(例如,规划模式 -> 实现)
  94. parentSessionId: SessionId | undefined
  95. // Logger state
  96. loggerProvider: LoggerProvider | null
  97. eventLogger: ReturnType<typeof logs.getLogger> | null
  98. // Meter provider state
  99. meterProvider: MeterProvider | null
  100. // Tracer provider state
  101. tracerProvider: BasicTracerProvider | null
  102. // Agent color state
  103. agentColorMap: Map<string, AgentColorName>
  104. agentColorIndex: number
  105. // Last API request for bug reports
  106. lastAPIRequest: Omit<BetaMessageStreamParams, 'messages'> | null
  107. // 上次 API 请求的消息(ant 专用;引用,非克隆)。
  108. // 捕获发送给 API 的压缩后、CLAUDE.md 注入的确切消息集,
  109. // 以便 /share 的 serialized_conversation.json 反映实际情况。
  110. lastAPIRequestMessages: BetaMessageStreamParams['messages'] | null
  111. // Last auto-mode classifier request(s) for /share transcript
  112. lastClassifierRequests: unknown[] | null
  113. // CLAUDE.md 内容缓存,供自动模式分类器使用。
  114. // 打破 yoloClassifier → claudemd → 文件系统 → 权限的循环。
  115. cachedClaudeMdContent: string | null
  116. // In-memory error log for recent errors
  117. inMemoryErrorLog: Array<{ error: string; timestamp: string }>
  118. // 来自 --plugin-dir 标志的仅会话插件
  119. inlinePlugins: Array<string>
  120. // 显式 --chrome / --no-chrome 标志值(undefined = 未在 CLI 设置)
  121. chromeFlagOverride: boolean | undefined
  122. // 使用 cowork_plugins 目录代替 plugins(--cowork 标志或环境变量)
  123. useCoworkPlugins: boolean
  124. // 仅会话的绕过权限模式标志(不持久化)
  125. sessionBypassPermissionsMode: boolean
  126. // 仅会话的标志,用于控制 .claude/scheduled_tasks.json 监视器
  127. // (useScheduledTasks)。当 JSON 有条目时由 cronScheduler.start() 设置,
  128. // 或由 CronCreateTool 设置。不持久化。
  129. scheduledTasksEnabled: boolean
  130. // 通过 CronCreate 以 durable: false 创建的仅会话定时任务。
  131. // 按调度触发,类似文件支持的任务,但永远不会写入
  132. // .claude/scheduled_tasks.json —— 它们随进程终止而消失。类型通过
  133. // 下面的 SessionCronTask 定义(不从 cronTasks.ts 导入以保持
  134. // bootstrap 为导入 DAG 的叶子节点)。
  135. sessionCronTasks: SessionCronTask[]
  136. // 本会话通过 TeamCreate 创建的团队。cleanupSessionTeams()
  137. // 在优雅关闭时移除它们,以避免子代理创建的团队永远残留在磁盘上
  138. // (gh-32730)。TeamDelete 移除条目以避免重复清理。
  139. // 放在这里(而非 teamHelpers.ts)以便 resetStateForTests() 在测试间清除。
  140. sessionCreatedTeams: Set<string>
  141. // 仅会话的家庭目录信任标志(不持久化到磁盘)
  142. // 从家庭目录运行时,显示信任对话框但不保存到磁盘。
  143. // 此标志允许需要信任的功能在会话期间正常工作。
  144. sessionTrustAccepted: boolean
  145. // 仅会话的标志,禁用会话持久化到磁盘
  146. sessionPersistenceDisabled: boolean
  147. // 跟踪用户是否在此会话中退出了规划模式(用于重新进入引导)
  148. hasExitedPlanMode: boolean
  149. // 跟踪是否需要显示规划模式退出附件(一次性通知)
  150. needsPlanModeExitAttachment: boolean
  151. // 跟踪是否需要显示自动模式退出附件(一次性通知)
  152. needsAutoModeExitAttachment: boolean
  153. // 跟踪 LSP 插件推荐是否已在本会话中显示(仅显示一次)
  154. lspRecommendationShownThisSession: boolean
  155. // SDK 初始化事件状态 - 结构化输出的 jsonSchema
  156. initJsonSchema: Record<string, unknown> | null
  157. // 已注册的钩子 - SDK 回调和插件原生钩子
  158. registeredHooks: Partial<Record<HookEvent, RegisteredHookMatcher[]>> | null
  159. // 规划 slug 缓存:sessionId -> wordSlug
  160. planSlugCache: Map<string, string>
  161. // 跟踪远程连接会话以进行可靠性日志记录
  162. teleportedSessionInfo: {
  163. isTeleported: boolean
  164. hasLoggedFirstMessage: boolean
  165. sessionId: string | null
  166. } | null
  167. // 跟踪调用的技能,以便在压缩时保留
  168. // 键是组合的:`${agentId ?? ''}:${skillName}` 以防止跨代理覆盖
  169. invokedSkills: Map<
  170. string,
  171. {
  172. skillName: string
  173. skillPath: string
  174. content: string
  175. invokedAt: number
  176. agentId: string | null
  177. }
  178. >
  179. // 跟踪慢速操作以在开发栏显示(仅 ant)
  180. slowOperations: Array<{
  181. operation: string
  182. durationMs: number
  183. timestamp: number
  184. }>
  185. // SDK 提供的 beta 功能(例如,context-1m-2025-08-07)
  186. sdkBetas: string[] | undefined
  187. // 主线程代理类型(来自 --agent 标志或设置)
  188. mainThreadAgentType: string | undefined
  189. // 远程模式(--remote 标志)
  190. isRemoteMode: boolean
  191. // 直连服务器 URL(用于在头部显示)
  192. directConnectServerUrl: string | undefined
  193. // 系统提示片段缓存状态
  194. systemPromptSectionCache: Map<string, string | null>
  195. // 上次向模型发送的日期(用于检测午夜日期变化)
  196. lastEmittedDate: string | null
  197. // 来自 --add-dir 标志的额外目录(用于 CLAUDE.md 加载)
  198. additionalDirectoriesForClaudeMd: string[]
  199. // 来自 --channels 标志的通道服务器白名单(其通道通知应注册到本会话的服务器)
  200. // 在 main.tsx 中解析一次——标签决定信任模型:'plugin' → 市场验证 +
  201. // 白名单,'server' → 白名单始终失败(schema 仅限插件)。
  202. // 两种类型都需要 entry.dev 来绕过白名单。
  203. allowedChannels: ChannelEntry[]
  204. // 如果 allowedChannels 中任何条目来自
  205. // --dangerously-load-development-channels(以便 ChannelsNotice 可在被策略拦截的消息中
  206. // 指出正确的标志名称)
  207. hasDevChannels: boolean
  208. // 包含会话 `.jsonl` 的目录;null = 从 originalCwd 推导。
  209. sessionProjectDir: string | null
  210. // GrowthBook 缓存的 prompt cache 1h TTL 白名单(会话稳定)
  211. promptCache1hAllowlist: string[] | null
  212. // 缓存的 1h TTL 用户资格(会话稳定)。首次评估时锁定,使会话中的超额翻转
  213. // 不会改变 cache_control TTL,否则会破坏服务器端 prompt 缓存。
  214. promptCache1hEligible: boolean | null
  215. // AFK_MODE_BETA_HEADER 的粘性开启锁存。首次激活自动模式后,
  216. // 会话剩余时间持续发送此标头,使 Shift+Tab 切换不会破坏 ~50-70K 令牌的 prompt 缓存。
  217. afkModeHeaderLatched: boolean | null
  218. // FAST_MODE_BETA_HEADER 的粘性开启锁存。首次启用快速模式后,
  219. // 保持发送标头,避免冷启动进出双重破坏 prompt 缓存。`speed` body 参数保持动态。
  220. fastModeHeaderLatched: boolean | null
  221. // 缓存编辑 beta 标头的粘性开启锁存。首次启用缓存微压缩后,
  222. // 保持发送标头,避免会话中的 GrowthBook/设置切换破坏 prompt 缓存。
  223. cacheEditingHeaderLatched: boolean | null
  224. // 清除之前工具循环思考的粘性开启锁存。当距上次 API 调用超过 1h 时触发
  225. // (确认缓存未命中——保留思考无缓存命中好处)。锁定后保持开启,
  226. // 使新预热的清除思考缓存不会因切回 keep:'all' 而被破坏。
  227. // 当前提示 ID (UUID),将用户提示与后续 OTel 事件关联
  228. promptId: string | null
  229. // 主对话链的最后 API requestId(不含子代理)。
  230. // 在每次主会话查询的 API 响应成功后更新。
  231. // 在关闭时读取,以向推理端发送缓存驱逐提示。
  232. lastMainRequestId: string | undefined
  233. // 上次成功 API 调用完成的时间戳(Date.now())。
  234. // 用于在 tengu_api_success 中计算 timeSinceLastApiCallMs,
  235. // 以将缓存未命中与空闲时间关联(缓存 TTL 约 5 分钟)。
  236. lastApiCompletionTimestamp: number | null
  237. // 压缩(自动或手动 /compact)后设置为 true。由
  238. // logAPISuccess 消费,以标记压缩后的第一次 API 调用,使我们能
  239. // 区分压缩导致的缓存未命中与 TTL 过期。
  240. pendingPostCompaction: boolean
  241. }
  242. // 这里是——修改前三思
  243. function getInitialState(): State {
  244. // 解析 cwd 中的符号链接,以匹配 shell.ts 的 setCwd 行为
  245. // 这确保路径清理的一致性,以便会话存储
  246. let resolvedCwd = ''
  247. if (
  248. typeof process !== 'undefined' &&
  249. typeof process.cwd === 'function' &&
  250. typeof realpathSync === 'function'
  251. ) {
  252. const rawCwd = cwd()
  253. try {
  254. resolvedCwd = realpathSync(rawCwd).normalize('NFC')
  255. } catch {
  256. // CloudStorage 挂载时文件提供程序 EPERM(逐路径组件 lstat)。
  257. resolvedCwd = rawCwd.normalize('NFC')
  258. }
  259. }
  260. const state: State = {
  261. originalCwd: resolvedCwd,
  262. projectRoot: resolvedCwd,
  263. totalCostUSD: 0,
  264. totalAPIDuration: 0,
  265. totalAPIDurationWithoutRetries: 0,
  266. totalToolDuration: 0,
  267. turnHookDurationMs: 0,
  268. turnToolDurationMs: 0,
  269. turnClassifierDurationMs: 0,
  270. turnToolCount: 0,
  271. turnHookCount: 0,
  272. turnClassifierCount: 0,
  273. startTime: Date.now(),
  274. lastInteractionTime: Date.now(),
  275. totalLinesAdded: 0,
  276. totalLinesRemoved: 0,
  277. hasUnknownModelCost: false,
  278. cwd: resolvedCwd,
  279. modelUsage: {},
  280. mainLoopModelOverride: undefined,
  281. initialMainLoopModel: null,
  282. modelStrings: null,
  283. isInteractive: false,
  284. kairosActive: false,
  285. strictToolResultPairing: false,
  286. sdkAgentProgressSummariesEnabled: false,
  287. userMsgOptIn: false,
  288. clientType: 'cli',
  289. sessionSource: undefined,
  290. questionPreviewFormat: undefined,
  291. sessionIngressToken: undefined,
  292. oauthTokenFromFd: undefined,
  293. apiKeyFromFd: undefined,
  294. flagSettingsPath: undefined,
  295. flagSettingsInline: null,
  296. allowedSettingSources: [
  297. 'userSettings',
  298. 'projectSettings',
  299. 'localSettings',
  300. 'flagSettings',
  301. 'policySettings',
  302. ],
  303. // 遥测状态
  304. meter: null,
  305. sessionCounter: null,
  306. locCounter: null,
  307. prCounter: null,
  308. commitCounter: null,
  309. costCounter: null,
  310. tokenCounter: null,
  311. codeEditToolDecisionCounter: null,
  312. activeTimeCounter: null,
  313. statsStore: null,
  314. sessionId: randomUUID() as SessionId,
  315. parentSessionId: undefined,
  316. // 日志记录器状态
  317. loggerProvider: null,
  318. eventLogger: null,
  319. // Meter 提供者状态
  320. meterProvider: null,
  321. tracerProvider: null,
  322. // 代理颜色状态
  323. agentColorMap: new Map(),
  324. agentColorIndex: 0,
  325. // 用于 bug 报告的上次 API 请求
  326. lastAPIRequest: null,
  327. lastAPIRequestMessages: null,
  328. // 用于 /share 转录的上次自动模式分类器请求
  329. lastClassifierRequests: null,
  330. cachedClaudeMdContent: null,
  331. // 最近错误的内存日志
  332. inMemoryErrorLog: [],
  333. // 来自 --plugin-dir 标志的仅会话插件
  334. inlinePlugins: [],
  335. // 显式 --chrome / --no-chrome 标志值(undefined = 未在 CLI 设置)
  336. chromeFlagOverride: undefined,
  337. // 使用 cowork_plugins 目录代替 plugins
  338. useCoworkPlugins: false,
  339. // 仅会话的绕过权限模式标志(不持久化)
  340. sessionBypassPermissionsMode: false,
  341. // 定时任务默认禁用,直到标志或对话框启用
  342. scheduledTasksEnabled: false,
  343. sessionCronTasks: [],
  344. sessionCreatedTeams: new Set(),
  345. // 仅会话的信任标志(不持久化到磁盘)
  346. sessionTrustAccepted: false,
  347. // 仅会话的标志,禁用会话持久化到磁盘
  348. sessionPersistenceDisabled: false,
  349. // 跟踪用户是否在此会话中退出了规划模式
  350. hasExitedPlanMode: false,
  351. // 跟踪是否需要显示规划模式退出附件
  352. needsPlanModeExitAttachment: false,
  353. // 跟踪是否需要显示自动模式退出附件
  354. needsAutoModeExitAttachment: false,
  355. // 跟踪 LSP 插件推荐是否已在本会话中显示
  356. lspRecommendationShownThisSession: false,
  357. // SDK 初始化事件状态
  358. initJsonSchema: null,
  359. registeredHooks: null,
  360. // 规划 slug 缓存
  361. planSlugCache: new Map(),
  362. // 跟踪远程连接会话以进行可靠性日志记录
  363. teleportedSessionInfo: null,
  364. // 跟踪调用的技能以在压缩时保留
  365. invokedSkills: new Map(),
  366. // 跟踪开发栏显示的慢速操作
  367. slowOperations: [],
  368. // SDK 提供的 beta 功能
  369. sdkBetas: undefined,
  370. // 主线程代理类型
  371. mainThreadAgentType: undefined,
  372. // 远程模式
  373. isRemoteMode: false,
  374. ...(process.env.USER_TYPE === 'ant'
  375. ? {
  376. replBridgeActive: false,
  377. }
  378. : {}),
  379. // 直连服务器 URL
  380. directConnectServerUrl: undefined,
  381. // 系统提示片段缓存状态
  382. systemPromptSectionCache: new Map(),
  383. // 上次向模型发送的日期
  384. lastEmittedDate: null,
  385. // 来自 --add-dir 标志的额外目录(用于 CLAUDE.md 加载)
  386. additionalDirectoriesForClaudeMd: [],
  387. // 来自 --channels 标志的通道服务器白名单
  388. allowedChannels: [],
  389. hasDevChannels: false,
  390. // 会话项目目录(null = 从 originalCwd 推导)
  391. sessionProjectDir: null,
  392. // Prompt cache 1h 白名单(null = 尚未从 GrowthBook 获取)
  393. promptCache1hAllowlist: null,
  394. // Prompt cache 1h 资格(null = 尚未评估)
  395. promptCache1hEligible: null,
  396. // Beta 标头锁存(null = 尚未触发)
  397. afkModeHeaderLatched: null,
  398. fastModeHeaderLatched: null,
  399. cacheEditingHeaderLatched: null,
  400. thinkingClearLatched: null,
  401. // 当前提示 ID
  402. promptId: null,
  403. lastMainRequestId: undefined,
  404. lastApiCompletionTimestamp: null,
  405. pendingPostCompaction: false,
  406. }
  407. return state
  408. }
  409. // 特别是这里
  410. const STATE: State = getInitialState()
  411. export function getSessionId(): SessionId {
  412. return STATE.sessionId
  413. }
  414. export function regenerateSessionId(
  415. options: { setCurrentAsParent?: boolean } = {},
  416. ): SessionId {
  417. if (options.setCurrentAsParent) {
  418. STATE.parentSessionId = STATE.sessionId
  419. }
  420. // Drop the outgoing session's plan-slug entry so the Map doesn't
  421. // accumulate stale keys. Callers that need to carry the slug across
  422. // (REPL.tsx clearContext) read it before calling clearConversation.
  423. STATE.planSlugCache.delete(STATE.sessionId)
  424. // Regenerated sessions live in the current project: reset projectDir to
  425. // null so getTranscriptPath() derives from originalCwd.
  426. STATE.sessionId = randomUUID() as SessionId
  427. STATE.sessionProjectDir = null
  428. return STATE.sessionId
  429. }
  430. export function getParentSessionId(): SessionId | undefined {
  431. return STATE.parentSessionId
  432. }
  433. /**
  434. * Atomically switch the active session. `sessionId` and `sessionProjectDir`
  435. * always change together — there is no separate setter for either, so they
  436. * cannot drift out of sync (CC-34).
  437. *
  438. * @param projectDir — directory containing `<sessionId>.jsonl`. Omit (or
  439. * pass `null`) for sessions in the current project — the path will derive
  440. * from originalCwd at read time. Pass `dirname(transcriptPath)` when the
  441. * session lives in a different project directory (git worktrees,
  442. * cross-project resume). Every call resets the project dir; it never
  443. * carries over from the previous session.
  444. */
  445. export function switchSession(
  446. sessionId: SessionId,
  447. projectDir: string | null = null,
  448. ): void {
  449. // Drop the outgoing session's plan-slug entry so the Map stays bounded
  450. // across repeated /resume. Only the current session's slug is ever read
  451. // (plans.ts getPlanSlug defaults to getSessionId()).
  452. STATE.planSlugCache.delete(STATE.sessionId)
  453. STATE.sessionId = sessionId
  454. STATE.sessionProjectDir = projectDir
  455. sessionSwitched.emit(sessionId)
  456. }
  457. const sessionSwitched = createSignal<[id: SessionId]>()
  458. /**
  459. * Register a callback that fires when switchSession changes the active
  460. * sessionId. bootstrap can't import listeners directly (DAG leaf), so
  461. * callers register themselves. concurrentSessions.ts uses this to keep the
  462. * PID file's sessionId in sync with --resume.
  463. */
  464. export const onSessionSwitch = sessionSwitched.subscribe
  465. /**
  466. * Project directory the current session's transcript lives in, or `null` if
  467. * the session was created in the current project (common case — derive from
  468. * originalCwd). See `switchSession()`.
  469. */
  470. export function getSessionProjectDir(): string | null {
  471. return STATE.sessionProjectDir
  472. }
  473. export function getOriginalCwd(): string {
  474. return STATE.originalCwd
  475. }
  476. /**
  477. * Get the stable project root directory.
  478. * Unlike getOriginalCwd(), this is never updated by mid-session EnterWorktreeTool
  479. * (so skills/history stay stable when entering a throwaway worktree).
  480. * It IS set at startup by --worktree, since that worktree is the session's project.
  481. * Use for project identity (history, skills, sessions) not file operations.
  482. */
  483. export function getProjectRoot(): string {
  484. return STATE.projectRoot
  485. }
  486. export function setOriginalCwd(cwd: string): void {
  487. STATE.originalCwd = cwd.normalize('NFC')
  488. }
  489. /**
  490. * Only for --worktree startup flag. Mid-session EnterWorktreeTool must NOT
  491. * call this — skills/history should stay anchored to where the session started.
  492. */
  493. export function setProjectRoot(cwd: string): void {
  494. STATE.projectRoot = cwd.normalize('NFC')
  495. }
  496. export function getCwdState(): string {
  497. return STATE.cwd
  498. }
  499. export function setCwdState(cwd: string): void {
  500. STATE.cwd = cwd.normalize('NFC')
  501. }
  502. export function getDirectConnectServerUrl(): string | undefined {
  503. return STATE.directConnectServerUrl
  504. }
  505. export function setDirectConnectServerUrl(url: string): void {
  506. STATE.directConnectServerUrl = url
  507. }
  508. export function addToTotalDurationState(
  509. duration: number,
  510. durationWithoutRetries: number,
  511. ): void {
  512. STATE.totalAPIDuration += duration
  513. STATE.totalAPIDurationWithoutRetries += durationWithoutRetries
  514. }
  515. export function resetTotalDurationStateAndCost_FOR_TESTS_ONLY(): void {
  516. STATE.totalAPIDuration = 0
  517. STATE.totalAPIDurationWithoutRetries = 0
  518. STATE.totalCostUSD = 0
  519. }
  520. export function addToTotalCostState(
  521. cost: number,
  522. modelUsage: ModelUsage,
  523. model: string,
  524. ): void {
  525. STATE.modelUsage[model] = modelUsage
  526. STATE.totalCostUSD += cost
  527. }
  528. export function getTotalCostUSD(): number {
  529. return STATE.totalCostUSD
  530. }
  531. export function getTotalAPIDuration(): number {
  532. return STATE.totalAPIDuration
  533. }
  534. export function getTotalDuration(): number {
  535. return Date.now() - STATE.startTime
  536. }
  537. export function getTotalAPIDurationWithoutRetries(): number {
  538. return STATE.totalAPIDurationWithoutRetries
  539. }
  540. export function getTotalToolDuration(): number {
  541. return STATE.totalToolDuration
  542. }
  543. export function addToToolDuration(duration: number): void {
  544. STATE.totalToolDuration += duration
  545. STATE.turnToolDurationMs += duration
  546. STATE.turnToolCount++
  547. }
  548. export function getTurnHookDurationMs(): number {
  549. return STATE.turnHookDurationMs
  550. }
  551. export function addToTurnHookDuration(duration: number): void {
  552. STATE.turnHookDurationMs += duration
  553. STATE.turnHookCount++
  554. }
  555. export function resetTurnHookDuration(): void {
  556. STATE.turnHookDurationMs = 0
  557. STATE.turnHookCount = 0
  558. }
  559. export function getTurnHookCount(): number {
  560. return STATE.turnHookCount
  561. }
  562. export function getTurnToolDurationMs(): number {
  563. return STATE.turnToolDurationMs
  564. }
  565. export function resetTurnToolDuration(): void {
  566. STATE.turnToolDurationMs = 0
  567. STATE.turnToolCount = 0
  568. }
  569. export function getTurnToolCount(): number {
  570. return STATE.turnToolCount
  571. }
  572. export function getTurnClassifierDurationMs(): number {
  573. return STATE.turnClassifierDurationMs
  574. }
  575. export function addToTurnClassifierDuration(duration: number): void {
  576. STATE.turnClassifierDurationMs += duration
  577. STATE.turnClassifierCount++
  578. }
  579. export function resetTurnClassifierDuration(): void {
  580. STATE.turnClassifierDurationMs = 0
  581. STATE.turnClassifierCount = 0
  582. }
  583. export function getTurnClassifierCount(): number {
  584. return STATE.turnClassifierCount
  585. }
  586. export function getStatsStore(): {
  587. observe(name: string, value: number): void
  588. } | null {
  589. return STATE.statsStore
  590. }
  591. export function setStatsStore(
  592. store: { observe(name: string, value: number): void } | null,
  593. ): void {
  594. STATE.statsStore = store
  595. }
  596. /**
  597. * Marks that an interaction occurred.
  598. *
  599. * By default the actual Date.now() call is deferred until the next Ink render
  600. * frame (via flushInteractionTime()) so we avoid calling Date.now() on every
  601. * single keypress.
  602. *
  603. * Pass `immediate = true` when calling from React useEffect callbacks or
  604. * other code that runs *after* the Ink render cycle has already flushed.
  605. * Without it the timestamp stays stale until the next render, which may never
  606. * come if the user is idle (e.g. permission dialog waiting for input).
  607. */
  608. let interactionTimeDirty = false
  609. export function updateLastInteractionTime(immediate?: boolean): void {
  610. if (immediate) {
  611. flushInteractionTime_inner()
  612. } else {
  613. interactionTimeDirty = true
  614. }
  615. }
  616. /**
  617. * If an interaction was recorded since the last flush, update the timestamp
  618. * now. Called by Ink before each render cycle so we batch many keypresses into
  619. * a single Date.now() call.
  620. */
  621. export function flushInteractionTime(): void {
  622. if (interactionTimeDirty) {
  623. flushInteractionTime_inner()
  624. }
  625. }
  626. function flushInteractionTime_inner(): void {
  627. STATE.lastInteractionTime = Date.now()
  628. interactionTimeDirty = false
  629. }
  630. export function addToTotalLinesChanged(added: number, removed: number): void {
  631. STATE.totalLinesAdded += added
  632. STATE.totalLinesRemoved += removed
  633. }
  634. export function getTotalLinesAdded(): number {
  635. return STATE.totalLinesAdded
  636. }
  637. export function getTotalLinesRemoved(): number {
  638. return STATE.totalLinesRemoved
  639. }
  640. export function getTotalInputTokens(): number {
  641. return sumBy(Object.values(STATE.modelUsage), 'inputTokens')
  642. }
  643. export function getTotalOutputTokens(): number {
  644. return sumBy(Object.values(STATE.modelUsage), 'outputTokens')
  645. }
  646. export function getTotalCacheReadInputTokens(): number {
  647. return sumBy(Object.values(STATE.modelUsage), 'cacheReadInputTokens')
  648. }
  649. export function getTotalCacheCreationInputTokens(): number {
  650. return sumBy(Object.values(STATE.modelUsage), 'cacheCreationInputTokens')
  651. }
  652. export function getTotalWebSearchRequests(): number {
  653. return sumBy(Object.values(STATE.modelUsage), 'webSearchRequests')
  654. }
  655. let outputTokensAtTurnStart = 0
  656. let currentTurnTokenBudget: number | null = null
  657. export function getTurnOutputTokens(): number {
  658. return getTotalOutputTokens() - outputTokensAtTurnStart
  659. }
  660. export function getCurrentTurnTokenBudget(): number | null {
  661. return currentTurnTokenBudget
  662. }
  663. let budgetContinuationCount = 0
  664. export function snapshotOutputTokensForTurn(budget: number | null): void {
  665. outputTokensAtTurnStart = getTotalOutputTokens()
  666. currentTurnTokenBudget = budget
  667. budgetContinuationCount = 0
  668. }
  669. export function getBudgetContinuationCount(): number {
  670. return budgetContinuationCount
  671. }
  672. export function incrementBudgetContinuationCount(): void {
  673. budgetContinuationCount++
  674. }
  675. export function setHasUnknownModelCost(): void {
  676. STATE.hasUnknownModelCost = true
  677. }
  678. export function hasUnknownModelCost(): boolean {
  679. return STATE.hasUnknownModelCost
  680. }
  681. export function getLastMainRequestId(): string | undefined {
  682. return STATE.lastMainRequestId
  683. }
  684. export function setLastMainRequestId(requestId: string): void {
  685. STATE.lastMainRequestId = requestId
  686. }
  687. export function getLastApiCompletionTimestamp(): number | null {
  688. return STATE.lastApiCompletionTimestamp
  689. }
  690. export function setLastApiCompletionTimestamp(timestamp: number): void {
  691. STATE.lastApiCompletionTimestamp = timestamp
  692. }
  693. /** Mark that a compaction just occurred. The next API success event will
  694. * include isPostCompaction=true, then the flag auto-resets. */
  695. export function markPostCompaction(): void {
  696. STATE.pendingPostCompaction = true
  697. }
  698. /** Consume the post-compaction flag. Returns true once after compaction,
  699. * then returns false until the next compaction. */
  700. export function consumePostCompaction(): boolean {
  701. const was = STATE.pendingPostCompaction
  702. STATE.pendingPostCompaction = false
  703. return was
  704. }
  705. export function getLastInteractionTime(): number {
  706. return STATE.lastInteractionTime
  707. }
  708. // Scroll drain suspension — background intervals check this before doing work
  709. // so they don't compete with scroll frames for the event loop. Set by
  710. // ScrollBox scrollBy/scrollTo, cleared SCROLL_DRAIN_IDLE_MS after the last
  711. // scroll event. Module-scope (not in STATE) — ephemeral hot-path flag, no
  712. // test-reset needed since the debounce timer self-clears.
  713. let scrollDraining = false
  714. let scrollDrainTimer: ReturnType<typeof setTimeout> | undefined
  715. const SCROLL_DRAIN_IDLE_MS = 150
  716. /** Mark that a scroll event just happened. Background intervals gate on
  717. * getIsScrollDraining() and skip their work until the debounce clears. */
  718. export function markScrollActivity(): void {
  719. scrollDraining = true
  720. if (scrollDrainTimer) clearTimeout(scrollDrainTimer)
  721. scrollDrainTimer = setTimeout(() => {
  722. scrollDraining = false
  723. scrollDrainTimer = undefined
  724. }, SCROLL_DRAIN_IDLE_MS)
  725. scrollDrainTimer.unref?.()
  726. }
  727. /** True while scroll is actively draining (within 150ms of last event).
  728. * Intervals should early-return when this is set — the work picks up next
  729. * tick after scroll settles. */
  730. export function getIsScrollDraining(): boolean {
  731. return scrollDraining
  732. }
  733. /** Await this before expensive one-shot work (network, subprocess) that could
  734. * coincide with scroll. Resolves immediately if not scrolling; otherwise
  735. * polls at the idle interval until the flag clears. */
  736. export async function waitForScrollIdle(): Promise<void> {
  737. while (scrollDraining) {
  738. // bootstrap-isolation forbids importing sleep() from src/utils/
  739. // eslint-disable-next-line no-restricted-syntax
  740. await new Promise(r => setTimeout(r, SCROLL_DRAIN_IDLE_MS).unref?.())
  741. }
  742. }
  743. export function getModelUsage(): { [modelName: string]: ModelUsage } {
  744. return STATE.modelUsage
  745. }
  746. export function getUsageForModel(model: string): ModelUsage | undefined {
  747. return STATE.modelUsage[model]
  748. }
  749. /**
  750. * Gets the model override set from the --model CLI flag or after the user
  751. * updates their configured model.
  752. */
  753. export function getMainLoopModelOverride(): ModelSetting | undefined {
  754. return STATE.mainLoopModelOverride
  755. }
  756. export function getInitialMainLoopModel(): ModelSetting {
  757. return STATE.initialMainLoopModel
  758. }
  759. export function setMainLoopModelOverride(
  760. model: ModelSetting | undefined,
  761. ): void {
  762. STATE.mainLoopModelOverride = model
  763. }
  764. export function setInitialMainLoopModel(model: ModelSetting): void {
  765. STATE.initialMainLoopModel = model
  766. }
  767. export function getSdkBetas(): string[] | undefined {
  768. return STATE.sdkBetas
  769. }
  770. export function setSdkBetas(betas: string[] | undefined): void {
  771. STATE.sdkBetas = betas
  772. }
  773. export function resetCostState(): void {
  774. STATE.totalCostUSD = 0
  775. STATE.totalAPIDuration = 0
  776. STATE.totalAPIDurationWithoutRetries = 0
  777. STATE.totalToolDuration = 0
  778. STATE.startTime = Date.now()
  779. STATE.totalLinesAdded = 0
  780. STATE.totalLinesRemoved = 0
  781. STATE.hasUnknownModelCost = false
  782. STATE.modelUsage = {}
  783. STATE.promptId = null
  784. }
  785. /**
  786. * Sets cost state values for session restore.
  787. * Called by restoreCostStateForSession in cost-tracker.ts.
  788. */
  789. export function setCostStateForRestore({
  790. totalCostUSD,
  791. totalAPIDuration,
  792. totalAPIDurationWithoutRetries,
  793. totalToolDuration,
  794. totalLinesAdded,
  795. totalLinesRemoved,
  796. lastDuration,
  797. modelUsage,
  798. }: {
  799. totalCostUSD: number
  800. totalAPIDuration: number
  801. totalAPIDurationWithoutRetries: number
  802. totalToolDuration: number
  803. totalLinesAdded: number
  804. totalLinesRemoved: number
  805. lastDuration: number | undefined
  806. modelUsage: { [modelName: string]: ModelUsage } | undefined
  807. }): void {
  808. STATE.totalCostUSD = totalCostUSD
  809. STATE.totalAPIDuration = totalAPIDuration
  810. STATE.totalAPIDurationWithoutRetries = totalAPIDurationWithoutRetries
  811. STATE.totalToolDuration = totalToolDuration
  812. STATE.totalLinesAdded = totalLinesAdded
  813. STATE.totalLinesRemoved = totalLinesRemoved
  814. // Restore per-model usage breakdown
  815. if (modelUsage) {
  816. STATE.modelUsage = modelUsage
  817. }
  818. // Adjust startTime to make wall duration accumulate
  819. if (lastDuration) {
  820. STATE.startTime = Date.now() - lastDuration
  821. }
  822. }
  823. // Only used in tests
  824. export function resetStateForTests(): void {
  825. if (process.env.NODE_ENV !== 'test') {
  826. throw new Error('resetStateForTests can only be called in tests')
  827. }
  828. Object.entries(getInitialState()).forEach(([key, value]) => {
  829. STATE[key as keyof State] = value as never
  830. })
  831. outputTokensAtTurnStart = 0
  832. currentTurnTokenBudget = null
  833. budgetContinuationCount = 0
  834. sessionSwitched.clear()
  835. }
  836. // You shouldn't use this directly. See src/utils/model/modelStrings.ts::getModelStrings()
  837. export function getModelStrings(): ModelStrings | null {
  838. return STATE.modelStrings
  839. }
  840. // You shouldn't use this directly. See src/utils/model/modelStrings.ts
  841. export function setModelStrings(modelStrings: ModelStrings): void {
  842. STATE.modelStrings = modelStrings
  843. }
  844. // Test utility function to reset model strings for re-initialization.
  845. // Separate from setModelStrings because we only want to accept 'null' in tests.
  846. export function resetModelStringsForTestingOnly() {
  847. STATE.modelStrings = null
  848. }
  849. export function setMeter(
  850. meter: Meter,
  851. createCounter: (name: string, options: MetricOptions) => AttributedCounter,
  852. ): void {
  853. STATE.meter = meter
  854. // Initialize all counters using the provided factory
  855. STATE.sessionCounter = createCounter('claude_code.session.count', {
  856. description: 'CLI 会话启动次数',
  857. })
  858. STATE.locCounter = createCounter('claude_code.lines_of_code.count', {
  859. description:
  860. "修改的代码行数,'type' 属性指示是添加还是删除的行",
  861. })
  862. STATE.prCounter = createCounter('claude_code.pull_request.count', {
  863. description: '创建的拉取请求数量',
  864. })
  865. STATE.commitCounter = createCounter('claude_code.commit.count', {
  866. description: '创建的 Git 提交数量',
  867. })
  868. STATE.costCounter = createCounter('claude_code.cost.usage', {
  869. description: 'Claude Code 会话成本',
  870. unit: 'USD',
  871. })
  872. STATE.tokenCounter = createCounter('claude_code.token.usage', {
  873. description: '使用的令牌数量',
  874. unit: 'tokens',
  875. })
  876. STATE.codeEditToolDecisionCounter = createCounter(
  877. 'claude_code.code_edit_tool.decision',
  878. {
  879. description:
  880. '代码编辑工具权限决策次数(接受/拒绝),适用于 Edit、Write 和 NotebookEdit 工具',
  881. },
  882. )
  883. STATE.activeTimeCounter = createCounter('claude_code.active_time.total', {
  884. description: '总活跃时间(秒)',
  885. unit: 's',
  886. })
  887. }
  888. export function getMeter(): Meter | null {
  889. return STATE.meter
  890. }
  891. export function getSessionCounter(): AttributedCounter | null {
  892. return STATE.sessionCounter
  893. }
  894. export function getLocCounter(): AttributedCounter | null {
  895. return STATE.locCounter
  896. }
  897. export function getPrCounter(): AttributedCounter | null {
  898. return STATE.prCounter
  899. }
  900. export function getCommitCounter(): AttributedCounter | null {
  901. return STATE.commitCounter
  902. }
  903. export function getCostCounter(): AttributedCounter | null {
  904. return STATE.costCounter
  905. }
  906. export function getTokenCounter(): AttributedCounter | null {
  907. return STATE.tokenCounter
  908. }
  909. export function getCodeEditToolDecisionCounter(): AttributedCounter | null {
  910. return STATE.codeEditToolDecisionCounter
  911. }
  912. export function getActiveTimeCounter(): AttributedCounter | null {
  913. return STATE.activeTimeCounter
  914. }
  915. export function getLoggerProvider(): LoggerProvider | null {
  916. return STATE.loggerProvider
  917. }
  918. export function setLoggerProvider(provider: LoggerProvider | null): void {
  919. STATE.loggerProvider = provider
  920. }
  921. export function getEventLogger(): ReturnType<typeof logs.getLogger> | null {
  922. return STATE.eventLogger
  923. }
  924. export function setEventLogger(
  925. logger: ReturnType<typeof logs.getLogger> | null,
  926. ): void {
  927. STATE.eventLogger = logger
  928. }
  929. export function getMeterProvider(): MeterProvider | null {
  930. return STATE.meterProvider
  931. }
  932. export function setMeterProvider(provider: MeterProvider | null): void {
  933. STATE.meterProvider = provider
  934. }
  935. export function getTracerProvider(): BasicTracerProvider | null {
  936. return STATE.tracerProvider
  937. }
  938. export function setTracerProvider(provider: BasicTracerProvider | null): void {
  939. STATE.tracerProvider = provider
  940. }
  941. export function getIsNonInteractiveSession(): boolean {
  942. return !STATE.isInteractive
  943. }
  944. export function getIsInteractive(): boolean {
  945. return STATE.isInteractive
  946. }
  947. export function setIsInteractive(value: boolean): void {
  948. STATE.isInteractive = value
  949. }
  950. export function getClientType(): string {
  951. return STATE.clientType
  952. }
  953. export function setClientType(type: string): void {
  954. STATE.clientType = type
  955. }
  956. export function getSdkAgentProgressSummariesEnabled(): boolean {
  957. return STATE.sdkAgentProgressSummariesEnabled
  958. }
  959. export function setSdkAgentProgressSummariesEnabled(value: boolean): void {
  960. STATE.sdkAgentProgressSummariesEnabled = value
  961. }
  962. export function getKairosActive(): boolean {
  963. return STATE.kairosActive
  964. }
  965. export function setKairosActive(value: boolean): void {
  966. STATE.kairosActive = value
  967. }
  968. export function getStrictToolResultPairing(): boolean {
  969. return STATE.strictToolResultPairing
  970. }
  971. export function setStrictToolResultPairing(value: boolean): void {
  972. STATE.strictToolResultPairing = value
  973. }
  974. // Field name 'userMsgOptIn' avoids excluded-string substrings ('BriefTool',
  975. // 'SendUserMessage' — case-insensitive). All callers are inside feature()
  976. // guards so these accessors don't need their own (matches getKairosActive).
  977. export function getUserMsgOptIn(): boolean {
  978. return STATE.userMsgOptIn
  979. }
  980. export function setUserMsgOptIn(value: boolean): void {
  981. STATE.userMsgOptIn = value
  982. }
  983. export function getSessionSource(): string | undefined {
  984. return STATE.sessionSource
  985. }
  986. export function setSessionSource(source: string): void {
  987. STATE.sessionSource = source
  988. }
  989. export function getQuestionPreviewFormat(): 'markdown' | 'html' | undefined {
  990. return STATE.questionPreviewFormat
  991. }
  992. export function setQuestionPreviewFormat(format: 'markdown' | 'html'): void {
  993. STATE.questionPreviewFormat = format
  994. }
  995. export function getAgentColorMap(): Map<string, AgentColorName> {
  996. return STATE.agentColorMap
  997. }
  998. export function getFlagSettingsPath(): string | undefined {
  999. return STATE.flagSettingsPath
  1000. }
  1001. export function setFlagSettingsPath(path: string | undefined): void {
  1002. STATE.flagSettingsPath = path
  1003. }
  1004. export function getFlagSettingsInline(): Record<string, unknown> | null {
  1005. return STATE.flagSettingsInline
  1006. }
  1007. export function setFlagSettingsInline(
  1008. settings: Record<string, unknown> | null,
  1009. ): void {
  1010. STATE.flagSettingsInline = settings
  1011. }
  1012. export function getSessionIngressToken(): string | null | undefined {
  1013. return STATE.sessionIngressToken
  1014. }
  1015. export function setSessionIngressToken(token: string | null): void {
  1016. STATE.sessionIngressToken = token
  1017. }
  1018. export function getOauthTokenFromFd(): string | null | undefined {
  1019. return STATE.oauthTokenFromFd
  1020. }
  1021. export function setOauthTokenFromFd(token: string | null): void {
  1022. STATE.oauthTokenFromFd = token
  1023. }
  1024. export function getApiKeyFromFd(): string | null | undefined {
  1025. return STATE.apiKeyFromFd
  1026. }
  1027. export function setApiKeyFromFd(key: string | null): void {
  1028. STATE.apiKeyFromFd = key
  1029. }
  1030. export function setLastAPIRequest(
  1031. params: Omit<BetaMessageStreamParams, 'messages'> | null,
  1032. ): void {
  1033. STATE.lastAPIRequest = params
  1034. }
  1035. export function getLastAPIRequest(): Omit<
  1036. BetaMessageStreamParams,
  1037. 'messages'
  1038. > | null {
  1039. return STATE.lastAPIRequest
  1040. }
  1041. export function setLastAPIRequestMessages(
  1042. messages: BetaMessageStreamParams['messages'] | null,
  1043. ): void {
  1044. STATE.lastAPIRequestMessages = messages
  1045. }
  1046. export function getLastAPIRequestMessages():
  1047. | BetaMessageStreamParams['messages']
  1048. | null {
  1049. return STATE.lastAPIRequestMessages
  1050. }
  1051. export function setLastClassifierRequests(requests: unknown[] | null): void {
  1052. STATE.lastClassifierRequests = requests
  1053. }
  1054. export function getLastClassifierRequests(): unknown[] | null {
  1055. return STATE.lastClassifierRequests
  1056. }
  1057. export function setCachedClaudeMdContent(content: string | null): void {
  1058. STATE.cachedClaudeMdContent = content
  1059. }
  1060. export function getCachedClaudeMdContent(): string | null {
  1061. return STATE.cachedClaudeMdContent
  1062. }
  1063. export function addToInMemoryErrorLog(errorInfo: {
  1064. error: string
  1065. timestamp: string
  1066. }): void {
  1067. const MAX_IN_MEMORY_ERRORS = 100
  1068. if (STATE.inMemoryErrorLog.length >= MAX_IN_MEMORY_ERRORS) {
  1069. STATE.inMemoryErrorLog.shift() // Remove oldest error
  1070. }
  1071. STATE.inMemoryErrorLog.push(errorInfo)
  1072. }
  1073. export function getAllowedSettingSources(): SettingSource[] {
  1074. return STATE.allowedSettingSources
  1075. }
  1076. export function setAllowedSettingSources(sources: SettingSource[]): void {
  1077. STATE.allowedSettingSources = sources
  1078. }
  1079. export function preferThirdPartyAuthentication(): boolean {
  1080. // IDE extension should behave as 1P for authentication reasons.
  1081. return getIsNonInteractiveSession() && STATE.clientType !== 'claude-vscode'
  1082. }
  1083. export function setInlinePlugins(plugins: Array<string>): void {
  1084. STATE.inlinePlugins = plugins
  1085. }
  1086. export function getInlinePlugins(): Array<string> {
  1087. return STATE.inlinePlugins
  1088. }
  1089. export function setChromeFlagOverride(value: boolean | undefined): void {
  1090. STATE.chromeFlagOverride = value
  1091. }
  1092. export function getChromeFlagOverride(): boolean | undefined {
  1093. return STATE.chromeFlagOverride
  1094. }
  1095. export function setUseCoworkPlugins(value: boolean): void {
  1096. STATE.useCoworkPlugins = value
  1097. resetSettingsCache()
  1098. }
  1099. export function getUseCoworkPlugins(): boolean {
  1100. return STATE.useCoworkPlugins
  1101. }
  1102. export function setSessionBypassPermissionsMode(enabled: boolean): void {
  1103. STATE.sessionBypassPermissionsMode = enabled
  1104. }
  1105. export function getSessionBypassPermissionsMode(): boolean {
  1106. return STATE.sessionBypassPermissionsMode
  1107. }
  1108. export function setScheduledTasksEnabled(enabled: boolean): void {
  1109. STATE.scheduledTasksEnabled = enabled
  1110. }
  1111. export function getScheduledTasksEnabled(): boolean {
  1112. return STATE.scheduledTasksEnabled
  1113. }
  1114. export type SessionCronTask = {
  1115. id: string
  1116. cron: string
  1117. prompt: string
  1118. createdAt: number
  1119. recurring?: boolean
  1120. /**
  1121. * When set, the task was created by an in-process teammate (not the team lead).
  1122. * The scheduler routes fires to that teammate's pendingUserMessages queue
  1123. * instead of the main REPL command queue. Session-only — never written to disk.
  1124. */
  1125. agentId?: string
  1126. }
  1127. export function getSessionCronTasks(): SessionCronTask[] {
  1128. return STATE.sessionCronTasks
  1129. }
  1130. export function addSessionCronTask(task: SessionCronTask): void {
  1131. STATE.sessionCronTasks.push(task)
  1132. }
  1133. /**
  1134. * Returns the number of tasks actually removed. Callers use this to skip
  1135. * downstream work (e.g. the disk read in removeCronTasks) when all ids
  1136. * were accounted for here.
  1137. */
  1138. export function removeSessionCronTasks(ids: readonly string[]): number {
  1139. if (ids.length === 0) return 0
  1140. const idSet = new Set(ids)
  1141. const remaining = STATE.sessionCronTasks.filter(t => !idSet.has(t.id))
  1142. const removed = STATE.sessionCronTasks.length - remaining.length
  1143. if (removed === 0) return 0
  1144. STATE.sessionCronTasks = remaining
  1145. return removed
  1146. }
  1147. export function setSessionTrustAccepted(accepted: boolean): void {
  1148. STATE.sessionTrustAccepted = accepted
  1149. }
  1150. export function getSessionTrustAccepted(): boolean {
  1151. return STATE.sessionTrustAccepted
  1152. }
  1153. export function setSessionPersistenceDisabled(disabled: boolean): void {
  1154. STATE.sessionPersistenceDisabled = disabled
  1155. }
  1156. export function isSessionPersistenceDisabled(): boolean {
  1157. return STATE.sessionPersistenceDisabled
  1158. }
  1159. export function hasExitedPlanModeInSession(): boolean {
  1160. return STATE.hasExitedPlanMode
  1161. }
  1162. export function setHasExitedPlanMode(value: boolean): void {
  1163. STATE.hasExitedPlanMode = value
  1164. }
  1165. export function needsPlanModeExitAttachment(): boolean {
  1166. return STATE.needsPlanModeExitAttachment
  1167. }
  1168. export function setNeedsPlanModeExitAttachment(value: boolean): void {
  1169. STATE.needsPlanModeExitAttachment = value
  1170. }
  1171. export function handlePlanModeTransition(
  1172. fromMode: string,
  1173. toMode: string,
  1174. ): void {
  1175. // If switching TO plan mode, clear any pending exit attachment
  1176. // This prevents sending both plan_mode and plan_mode_exit when user toggles quickly
  1177. if (toMode === 'plan' && fromMode !== 'plan') {
  1178. STATE.needsPlanModeExitAttachment = false
  1179. }
  1180. // If switching out of plan mode, trigger the plan_mode_exit attachment
  1181. if (fromMode === 'plan' && toMode !== 'plan') {
  1182. STATE.needsPlanModeExitAttachment = true
  1183. }
  1184. }
  1185. export function needsAutoModeExitAttachment(): boolean {
  1186. return STATE.needsAutoModeExitAttachment
  1187. }
  1188. export function setNeedsAutoModeExitAttachment(value: boolean): void {
  1189. STATE.needsAutoModeExitAttachment = value
  1190. }
  1191. export function handleAutoModeTransition(
  1192. fromMode: string,
  1193. toMode: string,
  1194. ): void {
  1195. // Auto↔plan transitions are handled by prepareContextForPlanMode (auto may
  1196. // stay active through plan if opted in) and ExitPlanMode (restores mode).
  1197. // Skip both directions so this function only handles direct auto transitions.
  1198. if (
  1199. (fromMode === 'auto' && toMode === 'plan') ||
  1200. (fromMode === 'plan' && toMode === 'auto')
  1201. ) {
  1202. return
  1203. }
  1204. const fromIsAuto = fromMode === 'auto'
  1205. const toIsAuto = toMode === 'auto'
  1206. // If switching TO auto mode, clear any pending exit attachment
  1207. // This prevents sending both auto_mode and auto_mode_exit when user toggles quickly
  1208. if (toIsAuto && !fromIsAuto) {
  1209. STATE.needsAutoModeExitAttachment = false
  1210. }
  1211. // If switching out of auto mode, trigger the auto_mode_exit attachment
  1212. if (fromIsAuto && !toIsAuto) {
  1213. STATE.needsAutoModeExitAttachment = true
  1214. }
  1215. }
  1216. // LSP plugin recommendation session tracking
  1217. export function hasShownLspRecommendationThisSession(): boolean {
  1218. return STATE.lspRecommendationShownThisSession
  1219. }
  1220. export function setLspRecommendationShownThisSession(value: boolean): void {
  1221. STATE.lspRecommendationShownThisSession = value
  1222. }
  1223. // SDK init event state
  1224. export function setInitJsonSchema(schema: Record<string, unknown>): void {
  1225. STATE.initJsonSchema = schema
  1226. }
  1227. export function getInitJsonSchema(): Record<string, unknown> | null {
  1228. return STATE.initJsonSchema
  1229. }
  1230. export function registerHookCallbacks(
  1231. hooks: Partial<Record<HookEvent, RegisteredHookMatcher[]>>,
  1232. ): void {
  1233. if (!STATE.registeredHooks) {
  1234. STATE.registeredHooks = {}
  1235. }
  1236. // `registerHookCallbacks` may be called multiple times, so we need to merge (not overwrite)
  1237. for (const [event, matchers] of Object.entries(hooks)) {
  1238. const eventKey = event as HookEvent
  1239. if (!STATE.registeredHooks[eventKey]) {
  1240. STATE.registeredHooks[eventKey] = []
  1241. }
  1242. STATE.registeredHooks[eventKey]!.push(...matchers)
  1243. }
  1244. }
  1245. export function getRegisteredHooks(): Partial<
  1246. Record<HookEvent, RegisteredHookMatcher[]>
  1247. > | null {
  1248. return STATE.registeredHooks
  1249. }
  1250. export function clearRegisteredHooks(): void {
  1251. STATE.registeredHooks = null
  1252. }
  1253. export function clearRegisteredPluginHooks(): void {
  1254. if (!STATE.registeredHooks) {
  1255. return
  1256. }
  1257. const filtered: Partial<Record<HookEvent, RegisteredHookMatcher[]>> = {}
  1258. for (const [event, matchers] of Object.entries(STATE.registeredHooks)) {
  1259. // Keep only callback hooks (those without pluginRoot)
  1260. const callbackHooks = matchers.filter(m => !('pluginRoot' in m))
  1261. if (callbackHooks.length > 0) {
  1262. filtered[event as HookEvent] = callbackHooks
  1263. }
  1264. }
  1265. STATE.registeredHooks = Object.keys(filtered).length > 0 ? filtered : null
  1266. }
  1267. export function resetSdkInitState(): void {
  1268. STATE.initJsonSchema = null
  1269. STATE.registeredHooks = null
  1270. }
  1271. export function getPlanSlugCache(): Map<string, string> {
  1272. return STATE.planSlugCache
  1273. }
  1274. export function getSessionCreatedTeams(): Set<string> {
  1275. return STATE.sessionCreatedTeams
  1276. }
  1277. // Teleported session tracking for reliability logging
  1278. export function setTeleportedSessionInfo(info: {
  1279. sessionId: string | null
  1280. }): void {
  1281. STATE.teleportedSessionInfo = {
  1282. isTeleported: true,
  1283. hasLoggedFirstMessage: false,
  1284. sessionId: info.sessionId,
  1285. }
  1286. }
  1287. export function getTeleportedSessionInfo(): {
  1288. isTeleported: boolean
  1289. hasLoggedFirstMessage: boolean
  1290. sessionId: string | null
  1291. } | null {
  1292. return STATE.teleportedSessionInfo
  1293. }
  1294. export function markFirstTeleportMessageLogged(): void {
  1295. if (STATE.teleportedSessionInfo) {
  1296. STATE.teleportedSessionInfo.hasLoggedFirstMessage = true
  1297. }
  1298. }
  1299. // Invoked skills tracking for preservation across compaction
  1300. export type InvokedSkillInfo = {
  1301. skillName: string
  1302. skillPath: string
  1303. content: string
  1304. invokedAt: number
  1305. agentId: string | null
  1306. }
  1307. export function addInvokedSkill(
  1308. skillName: string,
  1309. skillPath: string,
  1310. content: string,
  1311. agentId: string | null = null,
  1312. ): void {
  1313. const key = `${agentId ?? ''}:${skillName}`
  1314. STATE.invokedSkills.set(key, {
  1315. skillName,
  1316. skillPath,
  1317. content,
  1318. invokedAt: Date.now(),
  1319. agentId,
  1320. })
  1321. }
  1322. export function getInvokedSkills(): Map<string, InvokedSkillInfo> {
  1323. return STATE.invokedSkills
  1324. }
  1325. export function getInvokedSkillsForAgent(
  1326. agentId: string | undefined | null,
  1327. ): Map<string, InvokedSkillInfo> {
  1328. const normalizedId = agentId ?? null
  1329. const filtered = new Map<string, InvokedSkillInfo>()
  1330. for (const [key, skill] of STATE.invokedSkills) {
  1331. if (skill.agentId === normalizedId) {
  1332. filtered.set(key, skill)
  1333. }
  1334. }
  1335. return filtered
  1336. }
  1337. export function clearInvokedSkills(
  1338. preservedAgentIds?: ReadonlySet<string>,
  1339. ): void {
  1340. if (!preservedAgentIds || preservedAgentIds.size === 0) {
  1341. STATE.invokedSkills.clear()
  1342. return
  1343. }
  1344. for (const [key, skill] of STATE.invokedSkills) {
  1345. if (skill.agentId === null || !preservedAgentIds.has(skill.agentId)) {
  1346. STATE.invokedSkills.delete(key)
  1347. }
  1348. }
  1349. }
  1350. export function clearInvokedSkillsForAgent(agentId: string): void {
  1351. for (const [key, skill] of STATE.invokedSkills) {
  1352. if (skill.agentId === agentId) {
  1353. STATE.invokedSkills.delete(key)
  1354. }
  1355. }
  1356. }
  1357. // Slow operations tracking for dev bar
  1358. const MAX_SLOW_OPERATIONS = 10
  1359. const SLOW_OPERATION_TTL_MS = 10000
  1360. export function addSlowOperation(operation: string, durationMs: number): void {
  1361. if (process.env.USER_TYPE !== 'ant') return
  1362. // Skip tracking for editor sessions (user editing a prompt file in $EDITOR)
  1363. // These are intentionally slow since the user is drafting text
  1364. if (operation.includes('exec') && operation.includes('claude-prompt-')) {
  1365. return
  1366. }
  1367. const now = Date.now()
  1368. // Remove stale operations
  1369. STATE.slowOperations = STATE.slowOperations.filter(
  1370. op => now - op.timestamp < SLOW_OPERATION_TTL_MS,
  1371. )
  1372. // Add new operation
  1373. STATE.slowOperations.push({ operation, durationMs, timestamp: now })
  1374. // Keep only the most recent operations
  1375. if (STATE.slowOperations.length > MAX_SLOW_OPERATIONS) {
  1376. STATE.slowOperations = STATE.slowOperations.slice(-MAX_SLOW_OPERATIONS)
  1377. }
  1378. }
  1379. const EMPTY_SLOW_OPERATIONS: ReadonlyArray<{
  1380. operation: string
  1381. durationMs: number
  1382. timestamp: number
  1383. }> = []
  1384. export function getSlowOperations(): ReadonlyArray<{
  1385. operation: string
  1386. durationMs: number
  1387. timestamp: number
  1388. }> {
  1389. // Most common case: nothing tracked. Return a stable reference so the
  1390. // caller's setState() can bail via Object.is instead of re-rendering at 2fps.
  1391. if (STATE.slowOperations.length === 0) {
  1392. return EMPTY_SLOW_OPERATIONS
  1393. }
  1394. const now = Date.now()
  1395. // Only allocate a new array when something actually expired; otherwise keep
  1396. // the reference stable across polls while ops are still fresh.
  1397. if (
  1398. STATE.slowOperations.some(op => now - op.timestamp >= SLOW_OPERATION_TTL_MS)
  1399. ) {
  1400. STATE.slowOperations = STATE.slowOperations.filter(
  1401. op => now - op.timestamp < SLOW_OPERATION_TTL_MS,
  1402. )
  1403. if (STATE.slowOperations.length === 0) {
  1404. return EMPTY_SLOW_OPERATIONS
  1405. }
  1406. }
  1407. // Safe to return directly: addSlowOperation() reassigns STATE.slowOperations
  1408. // before pushing, so the array held in React state is never mutated.
  1409. return STATE.slowOperations
  1410. }
  1411. export function getMainThreadAgentType(): string | undefined {
  1412. return STATE.mainThreadAgentType
  1413. }
  1414. export function setMainThreadAgentType(agentType: string | undefined): void {
  1415. STATE.mainThreadAgentType = agentType
  1416. }
  1417. export function getIsRemoteMode(): boolean {
  1418. return STATE.isRemoteMode
  1419. }
  1420. export function setIsRemoteMode(value: boolean): void {
  1421. STATE.isRemoteMode = value
  1422. }
  1423. // System prompt section accessors
  1424. export function getSystemPromptSectionCache(): Map<string, string | null> {
  1425. return STATE.systemPromptSectionCache
  1426. }
  1427. export function setSystemPromptSectionCacheEntry(
  1428. name: string,
  1429. value: string | null,
  1430. ): void {
  1431. STATE.systemPromptSectionCache.set(name, value)
  1432. }
  1433. export function clearSystemPromptSectionState(): void {
  1434. STATE.systemPromptSectionCache.clear()
  1435. }
  1436. // Last emitted date accessors (for detecting midnight date changes)
  1437. export function getLastEmittedDate(): string | null {
  1438. return STATE.lastEmittedDate
  1439. }
  1440. export function setLastEmittedDate(date: string | null): void {
  1441. STATE.lastEmittedDate = date
  1442. }
  1443. export function getAdditionalDirectoriesForClaudeMd(): string[] {
  1444. return STATE.additionalDirectoriesForClaudeMd
  1445. }
  1446. export function setAdditionalDirectoriesForClaudeMd(
  1447. directories: string[],
  1448. ): void {
  1449. STATE.additionalDirectoriesForClaudeMd = directories
  1450. }
  1451. export function getAllowedChannels(): ChannelEntry[] {
  1452. return STATE.allowedChannels
  1453. }
  1454. export function setAllowedChannels(entries: ChannelEntry[]): void {
  1455. STATE.allowedChannels = entries
  1456. }
  1457. export function getHasDevChannels(): boolean {
  1458. return STATE.hasDevChannels
  1459. }
  1460. export function setHasDevChannels(value: boolean): void {
  1461. STATE.hasDevChannels = value
  1462. }
  1463. export function getPromptCache1hAllowlist(): string[] | null {
  1464. return STATE.promptCache1hAllowlist
  1465. }
  1466. export function setPromptCache1hAllowlist(allowlist: string[] | null): void {
  1467. STATE.promptCache1hAllowlist = allowlist
  1468. }
  1469. export function getPromptCache1hEligible(): boolean | null {
  1470. return STATE.promptCache1hEligible
  1471. }
  1472. export function setPromptCache1hEligible(eligible: boolean | null): void {
  1473. STATE.promptCache1hEligible = eligible
  1474. }
  1475. export function getAfkModeHeaderLatched(): boolean | null {
  1476. return STATE.afkModeHeaderLatched
  1477. }
  1478. export function setAfkModeHeaderLatched(v: boolean): void {
  1479. STATE.afkModeHeaderLatched = v
  1480. }
  1481. export function getFastModeHeaderLatched(): boolean | null {
  1482. return STATE.fastModeHeaderLatched
  1483. }
  1484. export function setFastModeHeaderLatched(v: boolean): void {
  1485. STATE.fastModeHeaderLatched = v
  1486. }
  1487. export function getCacheEditingHeaderLatched(): boolean | null {
  1488. return STATE.cacheEditingHeaderLatched
  1489. }
  1490. export function setCacheEditingHeaderLatched(v: boolean): void {
  1491. STATE.cacheEditingHeaderLatched = v
  1492. }
  1493. export function getThinkingClearLatched(): boolean | null {
  1494. return STATE.thinkingClearLatched
  1495. }
  1496. export function setThinkingClearLatched(v: boolean): void {
  1497. STATE.thinkingClearLatched = v
  1498. }
  1499. /**
  1500. * Reset beta header latches to null. Called on /clear and /compact so a
  1501. * fresh conversation gets fresh header evaluation.
  1502. */
  1503. export function clearBetaHeaderLatches(): void {
  1504. STATE.afkModeHeaderLatched = null
  1505. STATE.fastModeHeaderLatched = null
  1506. STATE.cacheEditingHeaderLatched = null
  1507. STATE.thinkingClearLatched = null
  1508. }
  1509. export function getPromptId(): string | null {
  1510. return STATE.promptId
  1511. }
  1512. export function setPromptId(id: string | null): void {
  1513. STATE.promptId = id
  1514. }