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