Compare commits
15 Commits
20523234e0
...
codex-remo
| Author | SHA1 | Date | |
|---|---|---|---|
| 497f81f4f9 | |||
| 9e7338d54c | |||
| d04ef7e701 | |||
| 3606a51288 | |||
| 2cf6c23fdc | |||
| 4eb053eef5 | |||
| d7bb164a05 | |||
| 108704ef22 | |||
| 04f612db35 | |||
| 64c2b201be | |||
| 9a4bb0e1ca | |||
| af2f2b0e34 | |||
| 097bc753f0 | |||
| f25dc3b6be | |||
| 9410e2f394 |
20
README.md
20
README.md
@@ -33,3 +33,23 @@ bun run compile
|
|||||||
|
|
||||||
- `node_modules/`, `dist/`, and generated CLI binaries are ignored by Git.
|
- `node_modules/`, `dist/`, and generated CLI binaries are ignored by Git.
|
||||||
- `bun.lock` is kept in the repository for reproducible installs.
|
- `bun.lock` is kept in the repository for reproducible installs.
|
||||||
|
|
||||||
|
## Local Info Egress Status
|
||||||
|
|
||||||
|
This fork has removed several local system and project metadata egress paths that existed in the recovered upstream code.
|
||||||
|
|
||||||
|
Removed in this repository:
|
||||||
|
|
||||||
|
- Model-request context injection of working directory, git status/history, `CLAUDE.md`, current date, platform, shell, and OS version.
|
||||||
|
- Feedback upload and transcript-share upload paths.
|
||||||
|
- Remote Control / Bridge registration fields that sent machine name, git branch, and git repository URL, plus git source/outcome data in bridge session creation.
|
||||||
|
- Trusted-device enrollment and trusted-device token header emission for bridge requests.
|
||||||
|
- `/insights` automatic S3 upload; reports now stay local via `file://` paths only.
|
||||||
|
- Datadog analytics and Anthropic 1P event-logging egress.
|
||||||
|
- GrowthBook remote evaluation/network fetches; local env/config overrides and cached values remain available for compatibility.
|
||||||
|
- OpenTelemetry initialization and event export paths.
|
||||||
|
|
||||||
|
Still present:
|
||||||
|
|
||||||
|
- Normal Claude API requests are still part of product functionality; this fork only removes extra local metadata injection, not core model/network access.
|
||||||
|
- Compatibility scaffolding for analytics, GrowthBook, and telemetry still exists in the tree as local no-op or cache-only code.
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import type {
|
|||||||
SDKPermissionDenial,
|
SDKPermissionDenial,
|
||||||
SDKStatus,
|
SDKStatus,
|
||||||
SDKUserMessageReplay,
|
SDKUserMessageReplay,
|
||||||
} from 'src/entrypoints/agentSdkTypes.js'
|
} from 'src/entrypoints/agentSdkTypes.ts'
|
||||||
import { accumulateUsage, updateUsage } from 'src/services/api/claude.js'
|
import { accumulateUsage, updateUsage } from 'src/services/api/claude.js'
|
||||||
import type { NonNullableUsage } from 'src/services/api/logging.js'
|
import type { NonNullableUsage } from 'src/services/api/logging.js'
|
||||||
import { EMPTY_USAGE } from 'src/services/api/logging.js'
|
import { EMPTY_USAGE } from 'src/services/api/logging.js'
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ export type {
|
|||||||
|
|
||||||
import type { SpinnerMode } from './components/Spinner.js'
|
import type { SpinnerMode } from './components/Spinner.js'
|
||||||
import type { QuerySource } from './constants/querySource.js'
|
import type { QuerySource } from './constants/querySource.js'
|
||||||
import type { SDKStatus } from './entrypoints/agentSdkTypes.js'
|
import type { SDKStatus } from './entrypoints/agentSdkTypes.ts'
|
||||||
import type { AppState } from './state/AppState.js'
|
import type { AppState } from './state/AppState.js'
|
||||||
import type {
|
import type {
|
||||||
HookProgress,
|
HookProgress,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import { getOauthConfig } from '../constants/oauth.js'
|
import { getOauthConfig } from '../constants/oauth.js'
|
||||||
import type { SDKMessage } from '../entrypoints/agentSdkTypes.js'
|
import type { SDKMessage } from '../entrypoints/agentSdkTypes.ts'
|
||||||
import { logForDebugging } from '../utils/debug.js'
|
import { logForDebugging } from '../utils/debug.js'
|
||||||
import { getOAuthHeaders, prepareApiRequest } from '../utils/teleport/api.js'
|
import { getOAuthHeaders, prepareApiRequest } from '../utils/teleport/api.js'
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import type { BasicTracerProvider } from '@opentelemetry/sdk-trace-base'
|
|||||||
import { realpathSync } from 'fs'
|
import { realpathSync } from 'fs'
|
||||||
import sumBy from 'lodash-es/sumBy.js'
|
import sumBy from 'lodash-es/sumBy.js'
|
||||||
import { cwd } from 'process'
|
import { cwd } from 'process'
|
||||||
import type { HookEvent, ModelUsage } from 'src/entrypoints/agentSdkTypes.js'
|
import type { HookEvent, ModelUsage } from 'src/entrypoints/agentSdkTypes.ts'
|
||||||
import type { AgentColorName } from 'src/tools/AgentTool/agentColorManager.js'
|
import type { AgentColorName } from 'src/tools/AgentTool/agentColorManager.js'
|
||||||
import type { HookCallbackMatcher } from 'src/types/hooks.js'
|
import type { HookCallbackMatcher } from 'src/types/hooks.js'
|
||||||
// Indirection for browser-sdk build (package.json "browser" field swaps
|
// Indirection for browser-sdk build (package.json "browser" field swaps
|
||||||
|
|||||||
@@ -154,10 +154,6 @@ export function createBridgeApiClient(deps: BridgeApiDeps): BridgeApiClient {
|
|||||||
}>(
|
}>(
|
||||||
`${deps.baseUrl}/v1/environments/bridge`,
|
`${deps.baseUrl}/v1/environments/bridge`,
|
||||||
{
|
{
|
||||||
machine_name: config.machineName,
|
|
||||||
directory: config.dir,
|
|
||||||
branch: config.branch,
|
|
||||||
git_repo_url: config.gitRepoUrl,
|
|
||||||
// Advertise session capacity so claude.ai/code can show
|
// Advertise session capacity so claude.ai/code can show
|
||||||
// "2/4 sessions" badges and only block the picker when
|
// "2/4 sessions" badges and only block the picker when
|
||||||
// actually at capacity. Backends that don't yet accept
|
// actually at capacity. Backends that don't yet accept
|
||||||
@@ -190,7 +186,7 @@ export function createBridgeApiClient(deps: BridgeApiDeps): BridgeApiClient {
|
|||||||
`[bridge:api] POST /v1/environments/bridge -> ${response.status} environment_id=${response.data.environment_id}`,
|
`[bridge:api] POST /v1/environments/bridge -> ${response.status} environment_id=${response.data.environment_id}`,
|
||||||
)
|
)
|
||||||
debug(
|
debug(
|
||||||
`[bridge:api] >>> ${debugBody({ machine_name: config.machineName, directory: config.dir, branch: config.branch, git_repo_url: config.gitRepoUrl, max_sessions: config.maxSessions, metadata: { worker_type: config.workerType } })}`,
|
`[bridge:api] >>> ${debugBody({ max_sessions: config.maxSessions, metadata: { worker_type: config.workerType } })}`,
|
||||||
)
|
)
|
||||||
debug(`[bridge:api] <<< ${debugBody(response.data)}`)
|
debug(`[bridge:api] <<< ${debugBody(response.data)}`)
|
||||||
return response.data
|
return response.data
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { feature } from 'bun:bundle'
|
import { feature } from 'bun:bundle'
|
||||||
import { randomUUID } from 'crypto'
|
import { randomUUID } from 'crypto'
|
||||||
import { hostname, tmpdir } from 'os'
|
import { tmpdir } from 'os'
|
||||||
import { basename, join, resolve } from 'path'
|
import { basename, join, resolve } from 'path'
|
||||||
import { getRemoteSessionUrl } from '../constants/product.js'
|
import { getRemoteSessionUrl } from '../constants/product.js'
|
||||||
import { shutdownDatadog } from '../services/analytics/datadog.js'
|
import { shutdownDatadog } from '../services/analytics/datadog.js'
|
||||||
@@ -2203,9 +2203,7 @@ export async function bridgeMain(args: string[]): Promise<void> {
|
|||||||
? process.env.CLAUDE_BRIDGE_SESSION_INGRESS_URL
|
? process.env.CLAUDE_BRIDGE_SESSION_INGRESS_URL
|
||||||
: baseUrl
|
: baseUrl
|
||||||
|
|
||||||
const { getBranch, getRemoteUrl, findGitRoot } = await import(
|
const { findGitRoot } = await import('../utils/git.js')
|
||||||
'../utils/git.js'
|
|
||||||
)
|
|
||||||
|
|
||||||
// Precheck worktree availability for the first-run dialog and the `w`
|
// Precheck worktree availability for the first-run dialog and the `w`
|
||||||
// toggle. Unconditional so we know upfront whether worktree is an option.
|
// toggle. Unconditional so we know upfront whether worktree is an option.
|
||||||
@@ -2337,9 +2335,6 @@ export async function bridgeMain(args: string[]): Promise<void> {
|
|||||||
process.exit(1)
|
process.exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
const branch = await getBranch()
|
|
||||||
const gitRepoUrl = await getRemoteUrl()
|
|
||||||
const machineName = hostname()
|
|
||||||
const bridgeId = randomUUID()
|
const bridgeId = randomUUID()
|
||||||
|
|
||||||
const { handleOAuth401Error } = await import('../utils/auth.js')
|
const { handleOAuth401Error } = await import('../utils/auth.js')
|
||||||
@@ -2417,9 +2412,6 @@ export async function bridgeMain(args: string[]): Promise<void> {
|
|||||||
|
|
||||||
const config: BridgeConfig = {
|
const config: BridgeConfig = {
|
||||||
dir,
|
dir,
|
||||||
machineName,
|
|
||||||
branch,
|
|
||||||
gitRepoUrl,
|
|
||||||
maxSessions,
|
maxSessions,
|
||||||
spawnMode,
|
spawnMode,
|
||||||
verbose,
|
verbose,
|
||||||
@@ -2435,7 +2427,7 @@ export async function bridgeMain(args: string[]): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
logForDebugging(
|
logForDebugging(
|
||||||
`[bridge:init] bridgeId=${bridgeId}${reuseEnvironmentId ? ` reuseEnvironmentId=${reuseEnvironmentId}` : ''} dir=${dir} branch=${branch} gitRepoUrl=${gitRepoUrl} machine=${machineName}`,
|
`[bridge:init] bridgeId=${bridgeId}${reuseEnvironmentId ? ` reuseEnvironmentId=${reuseEnvironmentId}` : ''} dir=${dir}`,
|
||||||
)
|
)
|
||||||
logForDebugging(
|
logForDebugging(
|
||||||
`[bridge:init] apiBaseUrl=${baseUrl} sessionIngressUrl=${sessionIngressUrl}`,
|
`[bridge:init] apiBaseUrl=${baseUrl} sessionIngressUrl=${sessionIngressUrl}`,
|
||||||
@@ -2591,11 +2583,7 @@ export async function bridgeMain(args: string[]): Promise<void> {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const logger = createBridgeLogger({ verbose })
|
const logger = createBridgeLogger({ verbose })
|
||||||
const { parseGitHubRepository } = await import('../utils/detectRepository.js')
|
logger.setRepoInfo(basename(dir), '')
|
||||||
const ownerRepo = gitRepoUrl ? parseGitHubRepository(gitRepoUrl) : null
|
|
||||||
// Use the repo name from the parsed owner/repo, or fall back to the dir basename
|
|
||||||
const repoName = ownerRepo ? ownerRepo.split('/').pop()! : basename(dir)
|
|
||||||
logger.setRepoInfo(repoName, branch)
|
|
||||||
|
|
||||||
// `w` toggle is available iff we're in a multi-session mode AND worktree
|
// `w` toggle is available iff we're in a multi-session mode AND worktree
|
||||||
// is a valid option. When unavailable, the mode suffix and hint are hidden.
|
// is a valid option. When unavailable, the mode suffix and hint are hidden.
|
||||||
@@ -2678,8 +2666,6 @@ export async function bridgeMain(args: string[]): Promise<void> {
|
|||||||
environmentId,
|
environmentId,
|
||||||
title: name,
|
title: name,
|
||||||
events: [],
|
events: [],
|
||||||
gitRepoUrl,
|
|
||||||
branch,
|
|
||||||
signal: controller.signal,
|
signal: controller.signal,
|
||||||
baseUrl,
|
baseUrl,
|
||||||
getAccessToken: getBridgeAccessToken,
|
getAccessToken: getBridgeAccessToken,
|
||||||
@@ -2856,9 +2842,7 @@ export async function runBridgeHeadless(
|
|||||||
? process.env.CLAUDE_BRIDGE_SESSION_INGRESS_URL
|
? process.env.CLAUDE_BRIDGE_SESSION_INGRESS_URL
|
||||||
: baseUrl
|
: baseUrl
|
||||||
|
|
||||||
const { getBranch, getRemoteUrl, findGitRoot } = await import(
|
const { findGitRoot } = await import('../utils/git.js')
|
||||||
'../utils/git.js'
|
|
||||||
)
|
|
||||||
const { hasWorktreeCreateHook } = await import('../utils/hooks.js')
|
const { hasWorktreeCreateHook } = await import('../utils/hooks.js')
|
||||||
|
|
||||||
if (opts.spawnMode === 'worktree') {
|
if (opts.spawnMode === 'worktree') {
|
||||||
@@ -2871,16 +2855,10 @@ export async function runBridgeHeadless(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const branch = await getBranch()
|
|
||||||
const gitRepoUrl = await getRemoteUrl()
|
|
||||||
const machineName = hostname()
|
|
||||||
const bridgeId = randomUUID()
|
const bridgeId = randomUUID()
|
||||||
|
|
||||||
const config: BridgeConfig = {
|
const config: BridgeConfig = {
|
||||||
dir,
|
dir,
|
||||||
machineName,
|
|
||||||
branch,
|
|
||||||
gitRepoUrl,
|
|
||||||
maxSessions: opts.capacity,
|
maxSessions: opts.capacity,
|
||||||
spawnMode: opts.spawnMode,
|
spawnMode: opts.spawnMode,
|
||||||
verbose: false,
|
verbose: false,
|
||||||
@@ -2934,8 +2912,6 @@ export async function runBridgeHeadless(
|
|||||||
environmentId,
|
environmentId,
|
||||||
title: opts.name,
|
title: opts.name,
|
||||||
events: [],
|
events: [],
|
||||||
gitRepoUrl,
|
|
||||||
branch,
|
|
||||||
signal,
|
signal,
|
||||||
baseUrl,
|
baseUrl,
|
||||||
getAccessToken: opts.getAccessToken,
|
getAccessToken: opts.getAccessToken,
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { randomUUID } from 'crypto'
|
import { randomUUID } from 'crypto'
|
||||||
import type { SDKMessage } from '../entrypoints/agentSdkTypes.js'
|
import type { SDKMessage } from '../entrypoints/agentSdkTypes.ts'
|
||||||
import type {
|
import type {
|
||||||
SDKControlRequest,
|
SDKControlRequest,
|
||||||
SDKControlResponse,
|
SDKControlResponse,
|
||||||
|
|||||||
@@ -1,20 +1,9 @@
|
|||||||
import type { SDKMessage } from '../entrypoints/agentSdkTypes.js'
|
import type { SDKMessage } from '../entrypoints/agentSdkTypes.ts'
|
||||||
import { logForDebugging } from '../utils/debug.js'
|
import { logForDebugging } from '../utils/debug.js'
|
||||||
import { errorMessage } from '../utils/errors.js'
|
import { errorMessage } from '../utils/errors.js'
|
||||||
import { extractErrorDetail } from './debugUtils.js'
|
import { extractErrorDetail } from './debugUtils.js'
|
||||||
import { toCompatSessionId } from './sessionIdCompat.js'
|
import { toCompatSessionId } from './sessionIdCompat.js'
|
||||||
|
|
||||||
type GitSource = {
|
|
||||||
type: 'git_repository'
|
|
||||||
url: string
|
|
||||||
revision?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
type GitOutcome = {
|
|
||||||
type: 'git_repository'
|
|
||||||
git_info: { type: 'github'; repo: string; branches: string[] }
|
|
||||||
}
|
|
||||||
|
|
||||||
// Events must be wrapped in { type: 'event', data: <sdk_message> } for the
|
// Events must be wrapped in { type: 'event', data: <sdk_message> } for the
|
||||||
// POST /v1/sessions endpoint (discriminated union format).
|
// POST /v1/sessions endpoint (discriminated union format).
|
||||||
type SessionEvent = {
|
type SessionEvent = {
|
||||||
@@ -35,8 +24,6 @@ export async function createBridgeSession({
|
|||||||
environmentId,
|
environmentId,
|
||||||
title,
|
title,
|
||||||
events,
|
events,
|
||||||
gitRepoUrl,
|
|
||||||
branch,
|
|
||||||
signal,
|
signal,
|
||||||
baseUrl: baseUrlOverride,
|
baseUrl: baseUrlOverride,
|
||||||
getAccessToken,
|
getAccessToken,
|
||||||
@@ -45,8 +32,6 @@ export async function createBridgeSession({
|
|||||||
environmentId: string
|
environmentId: string
|
||||||
title?: string
|
title?: string
|
||||||
events: SessionEvent[]
|
events: SessionEvent[]
|
||||||
gitRepoUrl: string | null
|
|
||||||
branch: string
|
|
||||||
signal: AbortSignal
|
signal: AbortSignal
|
||||||
baseUrl?: string
|
baseUrl?: string
|
||||||
getAccessToken?: () => string | undefined
|
getAccessToken?: () => string | undefined
|
||||||
@@ -56,8 +41,6 @@ export async function createBridgeSession({
|
|||||||
const { getOrganizationUUID } = await import('../services/oauth/client.js')
|
const { getOrganizationUUID } = await import('../services/oauth/client.js')
|
||||||
const { getOauthConfig } = await import('../constants/oauth.js')
|
const { getOauthConfig } = await import('../constants/oauth.js')
|
||||||
const { getOAuthHeaders } = await import('../utils/teleport/api.js')
|
const { getOAuthHeaders } = await import('../utils/teleport/api.js')
|
||||||
const { parseGitHubRepository } = await import('../utils/detectRepository.js')
|
|
||||||
const { getDefaultBranch } = await import('../utils/git.js')
|
|
||||||
const { getMainLoopModel } = await import('../utils/model/model.js')
|
const { getMainLoopModel } = await import('../utils/model/model.js')
|
||||||
const { default: axios } = await import('axios')
|
const { default: axios } = await import('axios')
|
||||||
|
|
||||||
@@ -74,60 +57,12 @@ export async function createBridgeSession({
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build git source and outcome context
|
|
||||||
let gitSource: GitSource | null = null
|
|
||||||
let gitOutcome: GitOutcome | null = null
|
|
||||||
|
|
||||||
if (gitRepoUrl) {
|
|
||||||
const { parseGitRemote } = await import('../utils/detectRepository.js')
|
|
||||||
const parsed = parseGitRemote(gitRepoUrl)
|
|
||||||
if (parsed) {
|
|
||||||
const { host, owner, name } = parsed
|
|
||||||
const revision = branch || (await getDefaultBranch()) || undefined
|
|
||||||
gitSource = {
|
|
||||||
type: 'git_repository',
|
|
||||||
url: `https://${host}/${owner}/${name}`,
|
|
||||||
revision,
|
|
||||||
}
|
|
||||||
gitOutcome = {
|
|
||||||
type: 'git_repository',
|
|
||||||
git_info: {
|
|
||||||
type: 'github',
|
|
||||||
repo: `${owner}/${name}`,
|
|
||||||
branches: [`claude/${branch || 'task'}`],
|
|
||||||
},
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Fallback: try parseGitHubRepository for owner/repo format
|
|
||||||
const ownerRepo = parseGitHubRepository(gitRepoUrl)
|
|
||||||
if (ownerRepo) {
|
|
||||||
const [owner, name] = ownerRepo.split('/')
|
|
||||||
if (owner && name) {
|
|
||||||
const revision = branch || (await getDefaultBranch()) || undefined
|
|
||||||
gitSource = {
|
|
||||||
type: 'git_repository',
|
|
||||||
url: `https://github.com/${owner}/${name}`,
|
|
||||||
revision,
|
|
||||||
}
|
|
||||||
gitOutcome = {
|
|
||||||
type: 'git_repository',
|
|
||||||
git_info: {
|
|
||||||
type: 'github',
|
|
||||||
repo: `${owner}/${name}`,
|
|
||||||
branches: [`claude/${branch || 'task'}`],
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const requestBody = {
|
const requestBody = {
|
||||||
...(title !== undefined && { title }),
|
...(title !== undefined && { title }),
|
||||||
events,
|
events,
|
||||||
session_context: {
|
session_context: {
|
||||||
sources: gitSource ? [gitSource] : [],
|
sources: [],
|
||||||
outcomes: gitOutcome ? [gitOutcome] : [],
|
outcomes: [],
|
||||||
model: getMainLoopModel(),
|
model: getMainLoopModel(),
|
||||||
},
|
},
|
||||||
environment_id: environmentId,
|
environment_id: environmentId,
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import type {
|
|||||||
ImageBlockParam,
|
ImageBlockParam,
|
||||||
} from '@anthropic-ai/sdk/resources/messages.mjs'
|
} from '@anthropic-ai/sdk/resources/messages.mjs'
|
||||||
import type { UUID } from 'crypto'
|
import type { UUID } from 'crypto'
|
||||||
import type { SDKMessage } from '../entrypoints/agentSdkTypes.js'
|
import type { SDKMessage } from '../entrypoints/agentSdkTypes.ts'
|
||||||
import { detectImageFormatFromBase64 } from '../utils/imageResizer.js'
|
import { detectImageFormatFromBase64 } from '../utils/imageResizer.js'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -14,9 +14,8 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { feature } from 'bun:bundle'
|
import { feature } from 'bun:bundle'
|
||||||
import { hostname } from 'os'
|
|
||||||
import { getOriginalCwd, getSessionId } from '../bootstrap/state.js'
|
import { getOriginalCwd, getSessionId } from '../bootstrap/state.js'
|
||||||
import type { SDKMessage } from '../entrypoints/agentSdkTypes.js'
|
import type { SDKMessage } from '../entrypoints/agentSdkTypes.ts'
|
||||||
import type { SDKControlResponse } from '../entrypoints/sdk/controlTypes.ts'
|
import type { SDKControlResponse } from '../entrypoints/sdk/controlTypes.ts'
|
||||||
import { getFeatureValue_CACHED_WITH_REFRESH } from '../services/analytics/growthbook.js'
|
import { getFeatureValue_CACHED_WITH_REFRESH } from '../services/analytics/growthbook.js'
|
||||||
import { getOrganizationUUID } from '../services/oauth/client.js'
|
import { getOrganizationUUID } from '../services/oauth/client.js'
|
||||||
@@ -34,7 +33,6 @@ import { getGlobalConfig, saveGlobalConfig } from '../utils/config.js'
|
|||||||
import { logForDebugging } from '../utils/debug.js'
|
import { logForDebugging } from '../utils/debug.js'
|
||||||
import { stripDisplayTagsAllowEmpty } from '../utils/displayTags.js'
|
import { stripDisplayTagsAllowEmpty } from '../utils/displayTags.js'
|
||||||
import { errorMessage } from '../utils/errors.js'
|
import { errorMessage } from '../utils/errors.js'
|
||||||
import { getBranch, getRemoteUrl } from '../utils/git.js'
|
|
||||||
import { toSDKMessages } from '../utils/messages/mappers.js'
|
import { toSDKMessages } from '../utils/messages/mappers.js'
|
||||||
import {
|
import {
|
||||||
getContentText,
|
getContentText,
|
||||||
@@ -460,10 +458,6 @@ export async function initReplBridge(
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
// Gather git context — this is the bootstrap-read boundary.
|
|
||||||
// Everything from here down is passed explicitly to bridgeCore.
|
|
||||||
const branch = await getBranch()
|
|
||||||
const gitRepoUrl = await getRemoteUrl()
|
|
||||||
const sessionIngressUrl =
|
const sessionIngressUrl =
|
||||||
process.env.USER_TYPE === 'ant' &&
|
process.env.USER_TYPE === 'ant' &&
|
||||||
process.env.CLAUDE_BRIDGE_SESSION_INGRESS_URL
|
process.env.CLAUDE_BRIDGE_SESSION_INGRESS_URL
|
||||||
@@ -489,9 +483,6 @@ export async function initReplBridge(
|
|||||||
// so no adapter needed — just the narrower type on the way out.
|
// so no adapter needed — just the narrower type on the way out.
|
||||||
return initBridgeCore({
|
return initBridgeCore({
|
||||||
dir: getOriginalCwd(),
|
dir: getOriginalCwd(),
|
||||||
machineName: hostname(),
|
|
||||||
branch,
|
|
||||||
gitRepoUrl,
|
|
||||||
title,
|
title,
|
||||||
baseUrl,
|
baseUrl,
|
||||||
sessionIngressUrl,
|
sessionIngressUrl,
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ import {
|
|||||||
} from '../services/analytics/index.js'
|
} from '../services/analytics/index.js'
|
||||||
import type { ReplBridgeHandle, BridgeState } from './replBridge.js'
|
import type { ReplBridgeHandle, BridgeState } from './replBridge.js'
|
||||||
import type { Message } from '../types/message.js'
|
import type { Message } from '../types/message.js'
|
||||||
import type { SDKMessage } from '../entrypoints/agentSdkTypes.js'
|
import type { SDKMessage } from '../entrypoints/agentSdkTypes.ts'
|
||||||
import type {
|
import type {
|
||||||
SDKControlRequest,
|
SDKControlRequest,
|
||||||
SDKControlResponse,
|
SDKControlResponse,
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ import {
|
|||||||
logBridgeSkip,
|
logBridgeSkip,
|
||||||
} from './debugUtils.js'
|
} from './debugUtils.js'
|
||||||
import type { Message } from '../types/message.js'
|
import type { Message } from '../types/message.js'
|
||||||
import type { SDKMessage } from '../entrypoints/agentSdkTypes.js'
|
import type { SDKMessage } from '../entrypoints/agentSdkTypes.ts'
|
||||||
import type { PermissionMode } from '../utils/permissions/PermissionMode.js'
|
import type { PermissionMode } from '../utils/permissions/PermissionMode.js'
|
||||||
import type {
|
import type {
|
||||||
SDKControlRequest,
|
SDKControlRequest,
|
||||||
@@ -84,15 +84,12 @@ export type BridgeState = 'ready' | 'connected' | 'reconnecting' | 'failed'
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Explicit-param input to initBridgeCore. Everything initReplBridge reads
|
* Explicit-param input to initBridgeCore. Everything initReplBridge reads
|
||||||
* from bootstrap state (cwd, session ID, git, OAuth) becomes a field here.
|
* from bootstrap state (cwd, session ID, OAuth) becomes a field here.
|
||||||
* A daemon caller (Agent SDK, PR 4) that never runs main.tsx fills these
|
* A daemon caller (Agent SDK, PR 4) that never runs main.tsx fills these
|
||||||
* in itself.
|
* in itself.
|
||||||
*/
|
*/
|
||||||
export type BridgeCoreParams = {
|
export type BridgeCoreParams = {
|
||||||
dir: string
|
dir: string
|
||||||
machineName: string
|
|
||||||
branch: string
|
|
||||||
gitRepoUrl: string | null
|
|
||||||
title: string
|
title: string
|
||||||
baseUrl: string
|
baseUrl: string
|
||||||
sessionIngressUrl: string
|
sessionIngressUrl: string
|
||||||
@@ -113,14 +110,12 @@ export type BridgeCoreParams = {
|
|||||||
* Daemon wrapper passes `createBridgeSessionLean` from `sessionApi.ts`
|
* Daemon wrapper passes `createBridgeSessionLean` from `sessionApi.ts`
|
||||||
* (HTTP-only, orgUUID+model supplied by the daemon caller).
|
* (HTTP-only, orgUUID+model supplied by the daemon caller).
|
||||||
*
|
*
|
||||||
* Receives `gitRepoUrl`+`branch` so the REPL wrapper can build the git
|
* Receives the registered environment ID and session title. Daemon callers
|
||||||
* source/outcome for claude.ai's session card. Daemon ignores them.
|
* may supply their own lean session-creation implementation.
|
||||||
*/
|
*/
|
||||||
createSession: (opts: {
|
createSession: (opts: {
|
||||||
environmentId: string
|
environmentId: string
|
||||||
title: string
|
title: string
|
||||||
gitRepoUrl: string | null
|
|
||||||
branch: string
|
|
||||||
signal: AbortSignal
|
signal: AbortSignal
|
||||||
}) => Promise<string | null>
|
}) => Promise<string | null>
|
||||||
/**
|
/**
|
||||||
@@ -262,9 +257,6 @@ export async function initBridgeCore(
|
|||||||
): Promise<BridgeCoreHandle | null> {
|
): Promise<BridgeCoreHandle | null> {
|
||||||
const {
|
const {
|
||||||
dir,
|
dir,
|
||||||
machineName,
|
|
||||||
branch,
|
|
||||||
gitRepoUrl,
|
|
||||||
title,
|
title,
|
||||||
baseUrl,
|
baseUrl,
|
||||||
sessionIngressUrl,
|
sessionIngressUrl,
|
||||||
@@ -331,9 +323,6 @@ export async function initBridgeCore(
|
|||||||
|
|
||||||
const bridgeConfig: BridgeConfig = {
|
const bridgeConfig: BridgeConfig = {
|
||||||
dir,
|
dir,
|
||||||
machineName,
|
|
||||||
branch,
|
|
||||||
gitRepoUrl,
|
|
||||||
maxSessions: 1,
|
maxSessions: 1,
|
||||||
spawnMode: 'single-session',
|
spawnMode: 'single-session',
|
||||||
verbose: false,
|
verbose: false,
|
||||||
@@ -457,8 +446,6 @@ export async function initBridgeCore(
|
|||||||
const createdSessionId = await createSession({
|
const createdSessionId = await createSession({
|
||||||
environmentId,
|
environmentId,
|
||||||
title,
|
title,
|
||||||
gitRepoUrl,
|
|
||||||
branch,
|
|
||||||
signal: AbortSignal.timeout(15_000),
|
signal: AbortSignal.timeout(15_000),
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -764,8 +751,6 @@ export async function initBridgeCore(
|
|||||||
const newSessionId = await createSession({
|
const newSessionId = await createSession({
|
||||||
environmentId,
|
environmentId,
|
||||||
title: currentTitle,
|
title: currentTitle,
|
||||||
gitRepoUrl,
|
|
||||||
branch,
|
|
||||||
signal: AbortSignal.timeout(15_000),
|
signal: AbortSignal.timeout(15_000),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,5 @@
|
|||||||
import axios from 'axios'
|
|
||||||
import memoize from 'lodash-es/memoize.js'
|
|
||||||
import { hostname } from 'os'
|
|
||||||
import { getOauthConfig } from '../constants/oauth.js'
|
|
||||||
import {
|
|
||||||
checkGate_CACHED_OR_BLOCKING,
|
|
||||||
getFeatureValue_CACHED_MAY_BE_STALE,
|
|
||||||
} from '../services/analytics/growthbook.js'
|
|
||||||
import { logForDebugging } from '../utils/debug.js'
|
import { logForDebugging } from '../utils/debug.js'
|
||||||
import { errorMessage } from '../utils/errors.js'
|
|
||||||
import { isEssentialTrafficOnly } from '../utils/privacyLevel.js'
|
|
||||||
import { getSecureStorage } from '../utils/secureStorage/index.js'
|
import { getSecureStorage } from '../utils/secureStorage/index.js'
|
||||||
import { jsonStringify } from '../utils/slowOperations.js'
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Trusted device token source for bridge (remote-control) sessions.
|
* Trusted device token source for bridge (remote-control) sessions.
|
||||||
@@ -30,49 +19,20 @@ import { jsonStringify } from '../utils/slowOperations.js'
|
|||||||
* #295987 (B2 Python routes), #307150 (C1' CCR v2 gate).
|
* #295987 (B2 Python routes), #307150 (C1' CCR v2 gate).
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const TRUSTED_DEVICE_GATE = 'tengu_sessions_elevated_auth_enforcement'
|
|
||||||
|
|
||||||
function isGateEnabled(): boolean {
|
|
||||||
return getFeatureValue_CACHED_MAY_BE_STALE(TRUSTED_DEVICE_GATE, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Memoized — secureStorage.read() spawns a macOS `security` subprocess (~40ms).
|
|
||||||
// bridgeApi.ts calls this from getHeaders() on every poll/heartbeat/ack.
|
|
||||||
// Cache cleared after enrollment (below) and on logout (clearAuthRelatedCaches).
|
|
||||||
//
|
|
||||||
// Only the storage read is memoized — the GrowthBook gate is checked live so
|
|
||||||
// that a gate flip after GrowthBook refresh takes effect without a restart.
|
|
||||||
const readStoredToken = memoize((): string | undefined => {
|
|
||||||
// Env var takes precedence for testing/canary.
|
|
||||||
const envToken = process.env.CLAUDE_TRUSTED_DEVICE_TOKEN
|
|
||||||
if (envToken) {
|
|
||||||
return envToken
|
|
||||||
}
|
|
||||||
return getSecureStorage().read()?.trustedDeviceToken
|
|
||||||
})
|
|
||||||
|
|
||||||
export function getTrustedDeviceToken(): string | undefined {
|
export function getTrustedDeviceToken(): string | undefined {
|
||||||
if (!isGateEnabled()) {
|
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
return readStoredToken()
|
|
||||||
}
|
|
||||||
|
|
||||||
export function clearTrustedDeviceTokenCache(): void {
|
export function clearTrustedDeviceTokenCache(): void {
|
||||||
readStoredToken.cache?.clear?.()
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clear the stored trusted device token from secure storage and the memo cache.
|
* Clear the stored trusted device token from secure storage and the memo cache.
|
||||||
* Called before enrollTrustedDevice() during /login so a stale token from the
|
* Called during /login so a stale token from the previous account isn't sent
|
||||||
* previous account isn't sent as X-Trusted-Device-Token while enrollment is
|
* as X-Trusted-Device-Token after account switches.
|
||||||
* in-flight (enrollTrustedDevice is async — bridge API calls between login and
|
|
||||||
* enrollment completion would otherwise still read the old cached token).
|
|
||||||
*/
|
*/
|
||||||
export function clearTrustedDeviceToken(): void {
|
export function clearTrustedDeviceToken(): void {
|
||||||
if (!isGateEnabled()) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const secureStorage = getSecureStorage()
|
const secureStorage = getSecureStorage()
|
||||||
try {
|
try {
|
||||||
const data = secureStorage.read()
|
const data = secureStorage.read()
|
||||||
@@ -83,128 +43,14 @@ export function clearTrustedDeviceToken(): void {
|
|||||||
} catch {
|
} catch {
|
||||||
// Best-effort — don't block login if storage is inaccessible
|
// Best-effort — don't block login if storage is inaccessible
|
||||||
}
|
}
|
||||||
readStoredToken.cache?.clear?.()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Enroll this device via POST /auth/trusted_devices and persist the token
|
* Trusted-device enrollment is disabled in this build. Keep the no-op entry
|
||||||
* to keychain. Best-effort — logs and returns on failure so callers
|
* point so callers can continue to invoke it without branching.
|
||||||
* (post-login hooks) don't block the login flow.
|
|
||||||
*
|
|
||||||
* The server gates enrollment on account_session.created_at < 10min, so
|
|
||||||
* this must be called immediately after a fresh /login. Calling it later
|
|
||||||
* (e.g. lazy enrollment on /bridge 403) will fail with 403 stale_session.
|
|
||||||
*/
|
*/
|
||||||
export async function enrollTrustedDevice(): Promise<void> {
|
export async function enrollTrustedDevice(): Promise<void> {
|
||||||
try {
|
|
||||||
// checkGate_CACHED_OR_BLOCKING awaits any in-flight GrowthBook re-init
|
|
||||||
// (triggered by refreshGrowthBookAfterAuthChange in login.tsx) before
|
|
||||||
// reading the gate, so we get the post-refresh value.
|
|
||||||
if (!(await checkGate_CACHED_OR_BLOCKING(TRUSTED_DEVICE_GATE))) {
|
|
||||||
logForDebugging(
|
logForDebugging(
|
||||||
`[trusted-device] Gate ${TRUSTED_DEVICE_GATE} is off, skipping enrollment`,
|
'[trusted-device] Enrollment disabled in this build; skipping trusted device registration',
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// If CLAUDE_TRUSTED_DEVICE_TOKEN is set (e.g. by an enterprise wrapper),
|
|
||||||
// skip enrollment — the env var takes precedence in readStoredToken() so
|
|
||||||
// any enrolled token would be shadowed and never used.
|
|
||||||
if (process.env.CLAUDE_TRUSTED_DEVICE_TOKEN) {
|
|
||||||
logForDebugging(
|
|
||||||
'[trusted-device] CLAUDE_TRUSTED_DEVICE_TOKEN env var is set, skipping enrollment (env var takes precedence)',
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// Lazy require — utils/auth.ts transitively pulls ~1300 modules
|
|
||||||
// (config → file → permissions → sessionStorage → commands). Daemon callers
|
|
||||||
// of getTrustedDeviceToken() don't need this; only /login does.
|
|
||||||
/* eslint-disable @typescript-eslint/no-require-imports */
|
|
||||||
const { getClaudeAIOAuthTokens } =
|
|
||||||
require('../utils/auth.js') as typeof import('../utils/auth.js')
|
|
||||||
/* eslint-enable @typescript-eslint/no-require-imports */
|
|
||||||
const accessToken = getClaudeAIOAuthTokens()?.accessToken
|
|
||||||
if (!accessToken) {
|
|
||||||
logForDebugging('[trusted-device] No OAuth token, skipping enrollment')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// Always re-enroll on /login — the existing token may belong to a
|
|
||||||
// different account (account-switch without /logout). Skipping enrollment
|
|
||||||
// would send the old account's token on the new account's bridge calls.
|
|
||||||
const secureStorage = getSecureStorage()
|
|
||||||
|
|
||||||
if (isEssentialTrafficOnly()) {
|
|
||||||
logForDebugging(
|
|
||||||
'[trusted-device] Essential traffic only, skipping enrollment',
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const baseUrl = getOauthConfig().BASE_API_URL
|
|
||||||
let response
|
|
||||||
try {
|
|
||||||
response = await axios.post<{
|
|
||||||
device_token?: string
|
|
||||||
device_id?: string
|
|
||||||
}>(
|
|
||||||
`${baseUrl}/api/auth/trusted_devices`,
|
|
||||||
{ display_name: `Claude Code on ${hostname()} · ${process.platform}` },
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${accessToken}`,
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
timeout: 10_000,
|
|
||||||
validateStatus: s => s < 500,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
} catch (err: unknown) {
|
|
||||||
logForDebugging(
|
|
||||||
`[trusted-device] Enrollment request failed: ${errorMessage(err)}`,
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (response.status !== 200 && response.status !== 201) {
|
|
||||||
logForDebugging(
|
|
||||||
`[trusted-device] Enrollment failed ${response.status}: ${jsonStringify(response.data).slice(0, 200)}`,
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const token = response.data?.device_token
|
|
||||||
if (!token || typeof token !== 'string') {
|
|
||||||
logForDebugging(
|
|
||||||
'[trusted-device] Enrollment response missing device_token field',
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const storageData = secureStorage.read()
|
|
||||||
if (!storageData) {
|
|
||||||
logForDebugging(
|
|
||||||
'[trusted-device] Cannot read storage, skipping token persist',
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
storageData.trustedDeviceToken = token
|
|
||||||
const result = secureStorage.update(storageData)
|
|
||||||
if (!result.success) {
|
|
||||||
logForDebugging(
|
|
||||||
`[trusted-device] Failed to persist token: ${result.warning ?? 'unknown'}`,
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
readStoredToken.cache?.clear?.()
|
|
||||||
logForDebugging(
|
|
||||||
`[trusted-device] Enrolled device_id=${response.data.device_id ?? 'unknown'}`,
|
|
||||||
)
|
|
||||||
} catch (err: unknown) {
|
|
||||||
logForDebugging(
|
|
||||||
`[trusted-device] Storage write failed: ${errorMessage(err)}`,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} catch (err: unknown) {
|
|
||||||
logForDebugging(`[trusted-device] Enrollment error: ${errorMessage(err)}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -80,9 +80,6 @@ export type BridgeWorkerType = 'claude_code' | 'claude_code_assistant'
|
|||||||
|
|
||||||
export type BridgeConfig = {
|
export type BridgeConfig = {
|
||||||
dir: string
|
dir: string
|
||||||
machineName: string
|
|
||||||
branch: string
|
|
||||||
gitRepoUrl: string | null
|
|
||||||
maxSessions: number
|
maxSessions: number
|
||||||
spawnMode: SpawnMode
|
spawnMode: SpawnMode
|
||||||
verbose: boolean
|
verbose: boolean
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { execFileSync } from 'child_process'
|
|
||||||
import { diffLines } from 'diff'
|
import { diffLines } from 'diff'
|
||||||
import { constants as fsConstants } from 'fs'
|
import { constants as fsConstants } from 'fs'
|
||||||
import {
|
import {
|
||||||
@@ -2674,7 +2673,7 @@ export type InsightsExport = {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Build export data from already-computed values.
|
* Build export data from already-computed values.
|
||||||
* Used by background upload to S3.
|
* Used by the local report writer.
|
||||||
*/
|
*/
|
||||||
export function buildExportData(
|
export function buildExportData(
|
||||||
data: AggregatedData,
|
data: AggregatedData,
|
||||||
@@ -3069,35 +3068,8 @@ const usageReport: Command = {
|
|||||||
{ collectRemote },
|
{ collectRemote },
|
||||||
)
|
)
|
||||||
|
|
||||||
let reportUrl = `file://${htmlPath}`
|
const reportUrl = `file://${htmlPath}`
|
||||||
let uploadHint = ''
|
const uploadHint = ''
|
||||||
|
|
||||||
if (process.env.USER_TYPE === 'ant') {
|
|
||||||
// Try to upload to S3
|
|
||||||
const timestamp = new Date()
|
|
||||||
.toISOString()
|
|
||||||
.replace(/[-:]/g, '')
|
|
||||||
.replace('T', '_')
|
|
||||||
.slice(0, 15)
|
|
||||||
const username = process.env.SAFEUSER || process.env.USER || 'unknown'
|
|
||||||
const filename = `${username}_insights_${timestamp}.html`
|
|
||||||
const s3Path = `s3://anthropic-serve/atamkin/cc-user-reports/${filename}`
|
|
||||||
const s3Url = `https://s3-frontend.infra.ant.dev/anthropic-serve/atamkin/cc-user-reports/${filename}`
|
|
||||||
|
|
||||||
reportUrl = s3Url
|
|
||||||
try {
|
|
||||||
execFileSync('ff', ['cp', htmlPath, s3Path], {
|
|
||||||
timeout: 60000,
|
|
||||||
stdio: 'pipe', // Suppress output
|
|
||||||
})
|
|
||||||
} catch {
|
|
||||||
// Upload failed - fall back to local file and show upload command
|
|
||||||
reportUrl = `file://${htmlPath}`
|
|
||||||
uploadHint = `\nAutomatic upload failed. Are you on the boron namespace? Try \`use-bo\` and ensure you've run \`sso\`.
|
|
||||||
To share, run: ff cp ${htmlPath} ${s3Path}
|
|
||||||
Then access at: ${s3Url}`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build header with stats
|
// Build header with stats
|
||||||
const sessionLabel =
|
const sessionLabel =
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -1,23 +1,9 @@
|
|||||||
import axios from 'axios'
|
|
||||||
import { readFile, stat } from 'fs/promises'
|
|
||||||
import type { Message } from '../../types/message.js'
|
import type { Message } from '../../types/message.js'
|
||||||
import { checkAndRefreshOAuthTokenIfNeeded } from '../../utils/auth.js'
|
|
||||||
import { logForDebugging } from '../../utils/debug.js'
|
|
||||||
import { errorMessage } from '../../utils/errors.js'
|
|
||||||
import { getAuthHeaders, getUserAgent } from '../../utils/http.js'
|
|
||||||
import { normalizeMessagesForAPI } from '../../utils/messages.js'
|
|
||||||
import {
|
|
||||||
extractAgentIdsFromMessages,
|
|
||||||
getTranscriptPath,
|
|
||||||
loadSubagentTranscripts,
|
|
||||||
MAX_TRANSCRIPT_READ_BYTES,
|
|
||||||
} from '../../utils/sessionStorage.js'
|
|
||||||
import { jsonStringify } from '../../utils/slowOperations.js'
|
|
||||||
import { redactSensitiveInfo } from '../Feedback.js'
|
|
||||||
|
|
||||||
type TranscriptShareResult = {
|
type TranscriptShareResult = {
|
||||||
success: boolean
|
success: boolean
|
||||||
transcriptId?: string
|
transcriptId?: string
|
||||||
|
disabled?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TranscriptShareTrigger =
|
export type TranscriptShareTrigger =
|
||||||
@@ -27,86 +13,12 @@ export type TranscriptShareTrigger =
|
|||||||
| 'memory_survey'
|
| 'memory_survey'
|
||||||
|
|
||||||
export async function submitTranscriptShare(
|
export async function submitTranscriptShare(
|
||||||
messages: Message[],
|
_messages: Message[],
|
||||||
trigger: TranscriptShareTrigger,
|
_trigger: TranscriptShareTrigger,
|
||||||
appearanceId: string,
|
_appearanceId: string,
|
||||||
): Promise<TranscriptShareResult> {
|
): Promise<TranscriptShareResult> {
|
||||||
try {
|
|
||||||
logForDebugging('Collecting transcript for sharing', { level: 'info' })
|
|
||||||
|
|
||||||
const transcript = normalizeMessagesForAPI(messages)
|
|
||||||
|
|
||||||
// Collect subagent transcripts
|
|
||||||
const agentIds = extractAgentIdsFromMessages(messages)
|
|
||||||
const subagentTranscripts = await loadSubagentTranscripts(agentIds)
|
|
||||||
|
|
||||||
// Read raw JSONL transcript (with size guard to prevent OOM)
|
|
||||||
let rawTranscriptJsonl: string | undefined
|
|
||||||
try {
|
|
||||||
const transcriptPath = getTranscriptPath()
|
|
||||||
const { size } = await stat(transcriptPath)
|
|
||||||
if (size <= MAX_TRANSCRIPT_READ_BYTES) {
|
|
||||||
rawTranscriptJsonl = await readFile(transcriptPath, 'utf-8')
|
|
||||||
} else {
|
|
||||||
logForDebugging(
|
|
||||||
`Skipping raw transcript read: file too large (${size} bytes)`,
|
|
||||||
{ level: 'warn' },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// File may not exist
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = {
|
|
||||||
trigger,
|
|
||||||
version: MACRO.VERSION,
|
|
||||||
platform: process.platform,
|
|
||||||
transcript,
|
|
||||||
subagentTranscripts:
|
|
||||||
Object.keys(subagentTranscripts).length > 0
|
|
||||||
? subagentTranscripts
|
|
||||||
: undefined,
|
|
||||||
rawTranscriptJsonl,
|
|
||||||
}
|
|
||||||
|
|
||||||
const content = redactSensitiveInfo(jsonStringify(data))
|
|
||||||
|
|
||||||
await checkAndRefreshOAuthTokenIfNeeded()
|
|
||||||
|
|
||||||
const authResult = getAuthHeaders()
|
|
||||||
if (authResult.error) {
|
|
||||||
return { success: false }
|
|
||||||
}
|
|
||||||
|
|
||||||
const headers: Record<string, string> = {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'User-Agent': getUserAgent(),
|
|
||||||
...authResult.headers,
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await axios.post(
|
|
||||||
'https://api.anthropic.com/api/claude_code_shared_session_transcripts',
|
|
||||||
{ content, appearance_id: appearanceId },
|
|
||||||
{
|
|
||||||
headers,
|
|
||||||
timeout: 30000,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
if (response.status === 200 || response.status === 201) {
|
|
||||||
const result = response.data
|
|
||||||
logForDebugging('Transcript shared successfully', { level: 'info' })
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: false,
|
||||||
transcriptId: result?.transcript_id,
|
disabled: true,
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return { success: false }
|
|
||||||
} catch (err) {
|
|
||||||
logForDebugging(errorMessage(err), {
|
|
||||||
level: 'error',
|
|
||||||
})
|
|
||||||
return { success: false }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import { c as _c } from "react/compiler-runtime";
|
|||||||
*/
|
*/
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { useCallback, useMemo, useState } from 'react';
|
import { useCallback, useMemo, useState } from 'react';
|
||||||
import type { HookEvent } from 'src/entrypoints/agentSdkTypes.js';
|
import type { HookEvent } from 'src/entrypoints/agentSdkTypes.ts';
|
||||||
import { useAppState, useAppStateStore } from 'src/state/AppState.js';
|
import { useAppState, useAppStateStore } from 'src/state/AppState.js';
|
||||||
import type { CommandResultDisplay } from '../../commands.js';
|
import type { CommandResultDisplay } from '../../commands.js';
|
||||||
import { useSettingsChange } from '../../hooks/useSettingsChange.js';
|
import { useSettingsChange } from '../../hooks/useSettingsChange.js';
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { c as _c } from "react/compiler-runtime";
|
|||||||
|
|
||||||
import figures from 'figures';
|
import figures from 'figures';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import type { HookEvent } from 'src/entrypoints/agentSdkTypes.js';
|
import type { HookEvent } from 'src/entrypoints/agentSdkTypes.ts';
|
||||||
import type { HookEventMetadata } from 'src/utils/hooks/hooksConfigManager.js';
|
import type { HookEventMetadata } from 'src/utils/hooks/hooksConfigManager.js';
|
||||||
import { Box, Link, Text } from '../../ink.js';
|
import { Box, Link, Text } from '../../ink.js';
|
||||||
import { plural } from '../../utils/stringUtils.js';
|
import { plural } from '../../utils/stringUtils.js';
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { c as _c } from "react/compiler-runtime";
|
|||||||
* confirmation.
|
* confirmation.
|
||||||
*/
|
*/
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import type { HookEvent } from 'src/entrypoints/agentSdkTypes.js';
|
import type { HookEvent } from 'src/entrypoints/agentSdkTypes.ts';
|
||||||
import type { HookEventMetadata } from 'src/utils/hooks/hooksConfigManager.js';
|
import type { HookEventMetadata } from 'src/utils/hooks/hooksConfigManager.js';
|
||||||
import { Box, Text } from '../../ink.js';
|
import { Box, Text } from '../../ink.js';
|
||||||
import { getHookDisplayText, hookSourceHeaderDisplayString, type IndividualHookConfig } from '../../utils/hooks/hooksSettings.js';
|
import { getHookDisplayText, hookSourceHeaderDisplayString, type IndividualHookConfig } from '../../utils/hooks/hooksSettings.js';
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { c as _c } from "react/compiler-runtime";
|
|||||||
* and simply lets the user drill into each matcher to see its hooks.
|
* and simply lets the user drill into each matcher to see its hooks.
|
||||||
*/
|
*/
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import type { HookEvent } from 'src/entrypoints/agentSdkTypes.js';
|
import type { HookEvent } from 'src/entrypoints/agentSdkTypes.ts';
|
||||||
import { Box, Text } from '../../ink.js';
|
import { Box, Text } from '../../ink.js';
|
||||||
import { type HookSource, hookSourceInlineDisplayString, type IndividualHookConfig } from '../../utils/hooks/hooksSettings.js';
|
import { type HookSource, hookSourceInlineDisplayString, type IndividualHookConfig } from '../../utils/hooks/hooksSettings.js';
|
||||||
import { plural } from '../../utils/stringUtils.js';
|
import { plural } from '../../utils/stringUtils.js';
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { c as _c } from "react/compiler-runtime";
|
import { c as _c } from "react/compiler-runtime";
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import type { HookEvent } from 'src/entrypoints/agentSdkTypes.js';
|
import type { HookEvent } from 'src/entrypoints/agentSdkTypes.ts';
|
||||||
import type { buildMessageLookups } from 'src/utils/messages.js';
|
import type { buildMessageLookups } from 'src/utils/messages.js';
|
||||||
import { Box, Text } from '../../ink.js';
|
import { Box, Text } from '../../ink.js';
|
||||||
import { MessageResponse } from '../MessageResponse.js';
|
import { MessageResponse } from '../MessageResponse.js';
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { c as _c } from "react/compiler-runtime";
|
import { c as _c } from "react/compiler-runtime";
|
||||||
import figures from 'figures';
|
import figures from 'figures';
|
||||||
import React, { useMemo, useState } from 'react';
|
import React, { useMemo, useState } from 'react';
|
||||||
import type { SDKMessage } from 'src/entrypoints/agentSdkTypes.js';
|
import type { SDKMessage } from 'src/entrypoints/agentSdkTypes.ts';
|
||||||
import type { ToolUseContext } from 'src/Tool.js';
|
import type { ToolUseContext } from 'src/Tool.js';
|
||||||
import type { DeepImmutable } from 'src/types/utils.js';
|
import type { DeepImmutable } from 'src/types/utils.js';
|
||||||
import type { CommandResultDisplay } from '../../commands.js';
|
import type { CommandResultDisplay } from '../../commands.js';
|
||||||
|
|||||||
@@ -448,9 +448,7 @@ export async function getSystemPrompt(
|
|||||||
mcpClients?: MCPServerConnection[],
|
mcpClients?: MCPServerConnection[],
|
||||||
): Promise<string[]> {
|
): Promise<string[]> {
|
||||||
if (isEnvTruthy(process.env.CLAUDE_CODE_SIMPLE)) {
|
if (isEnvTruthy(process.env.CLAUDE_CODE_SIMPLE)) {
|
||||||
return [
|
return [`You are Claude Code, Anthropic's official CLI for Claude.`]
|
||||||
`You are Claude Code, Anthropic's official CLI for Claude.\n\nCWD: ${getCwd()}\nDate: ${getSessionStartDate()}`,
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const cwd = getCwd()
|
const cwd = getCwd()
|
||||||
@@ -607,8 +605,6 @@ export async function computeEnvInfo(
|
|||||||
modelId: string,
|
modelId: string,
|
||||||
additionalWorkingDirectories?: string[],
|
additionalWorkingDirectories?: string[],
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
const [isGit, unameSR] = await Promise.all([getIsGit(), getUnameSR()])
|
|
||||||
|
|
||||||
// Undercover: keep ALL model names/IDs out of the system prompt so nothing
|
// Undercover: keep ALL model names/IDs out of the system prompt so nothing
|
||||||
// internal can leak into public commits/PRs. This includes the public
|
// internal can leak into public commits/PRs. This includes the public
|
||||||
// FRONTIER_MODEL_* constants — if those ever point at an unannounced model,
|
// FRONTIER_MODEL_* constants — if those ever point at an unannounced model,
|
||||||
@@ -627,33 +623,20 @@ export async function computeEnvInfo(
|
|||||||
: `You are powered by the model ${modelId}.`
|
: `You are powered by the model ${modelId}.`
|
||||||
}
|
}
|
||||||
|
|
||||||
const additionalDirsInfo =
|
|
||||||
additionalWorkingDirectories && additionalWorkingDirectories.length > 0
|
|
||||||
? `Additional working directories: ${additionalWorkingDirectories.join(', ')}\n`
|
|
||||||
: ''
|
|
||||||
|
|
||||||
const cutoff = getKnowledgeCutoff(modelId)
|
const cutoff = getKnowledgeCutoff(modelId)
|
||||||
const knowledgeCutoffMessage = cutoff
|
const knowledgeCutoffMessage = cutoff
|
||||||
? `\n\nAssistant knowledge cutoff is ${cutoff}.`
|
? `\n\nAssistant knowledge cutoff is ${cutoff}.`
|
||||||
: ''
|
: ''
|
||||||
|
|
||||||
return `Here is useful information about the environment you are running in:
|
return [`# Environment`, `You are Claude Code.`, modelDescription, knowledgeCutoffMessage]
|
||||||
<env>
|
.filter(Boolean)
|
||||||
Working directory: ${getCwd()}
|
.join('\n')
|
||||||
Is directory a git repo: ${isGit ? 'Yes' : 'No'}
|
|
||||||
${additionalDirsInfo}Platform: ${env.platform}
|
|
||||||
${getShellInfoLine()}
|
|
||||||
OS Version: ${unameSR}
|
|
||||||
</env>
|
|
||||||
${modelDescription}${knowledgeCutoffMessage}`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function computeSimpleEnvInfo(
|
export async function computeSimpleEnvInfo(
|
||||||
modelId: string,
|
modelId: string,
|
||||||
additionalWorkingDirectories?: string[],
|
additionalWorkingDirectories?: string[],
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
const [isGit, unameSR] = await Promise.all([getIsGit(), getUnameSR()])
|
|
||||||
|
|
||||||
// Undercover: strip all model name/ID references. See computeEnvInfo.
|
// Undercover: strip all model name/ID references. See computeEnvInfo.
|
||||||
// DCE: inline the USER_TYPE check at each site — do NOT hoist to a const.
|
// DCE: inline the USER_TYPE check at each site — do NOT hoist to a const.
|
||||||
let modelDescription: string | null = null
|
let modelDescription: string | null = null
|
||||||
@@ -671,42 +654,14 @@ export async function computeSimpleEnvInfo(
|
|||||||
? `Assistant knowledge cutoff is ${cutoff}.`
|
? `Assistant knowledge cutoff is ${cutoff}.`
|
||||||
: null
|
: null
|
||||||
|
|
||||||
const cwd = getCwd()
|
|
||||||
const isWorktree = getCurrentWorktreeSession() !== null
|
|
||||||
|
|
||||||
const envItems = [
|
|
||||||
`Primary working directory: ${cwd}`,
|
|
||||||
isWorktree
|
|
||||||
? `This is a git worktree — an isolated copy of the repository. Run all commands from this directory. Do NOT \`cd\` to the original repository root.`
|
|
||||||
: null,
|
|
||||||
[`Is a git repository: ${isGit}`],
|
|
||||||
additionalWorkingDirectories && additionalWorkingDirectories.length > 0
|
|
||||||
? `Additional working directories:`
|
|
||||||
: null,
|
|
||||||
additionalWorkingDirectories && additionalWorkingDirectories.length > 0
|
|
||||||
? additionalWorkingDirectories
|
|
||||||
: null,
|
|
||||||
`Platform: ${env.platform}`,
|
|
||||||
getShellInfoLine(),
|
|
||||||
`OS Version: ${unameSR}`,
|
|
||||||
modelDescription,
|
|
||||||
knowledgeCutoffMessage,
|
|
||||||
process.env.USER_TYPE === 'ant' && isUndercover()
|
|
||||||
? null
|
|
||||||
: `The most recent Claude model family is Claude 4.5/4.6. Model IDs — Opus 4.6: '${CLAUDE_4_5_OR_4_6_MODEL_IDS.opus}', Sonnet 4.6: '${CLAUDE_4_5_OR_4_6_MODEL_IDS.sonnet}', Haiku 4.5: '${CLAUDE_4_5_OR_4_6_MODEL_IDS.haiku}'. When building AI applications, default to the latest and most capable Claude models.`,
|
|
||||||
process.env.USER_TYPE === 'ant' && isUndercover()
|
|
||||||
? null
|
|
||||||
: `Claude Code is available as a CLI in the terminal, desktop app (Mac/Windows), web app (claude.ai/code), and IDE extensions (VS Code, JetBrains).`,
|
|
||||||
process.env.USER_TYPE === 'ant' && isUndercover()
|
|
||||||
? null
|
|
||||||
: `Fast mode for Claude Code uses the same ${FRONTIER_MODEL_NAME} model with faster output. It does NOT switch to a different model. It can be toggled with /fast.`,
|
|
||||||
].filter(item => item !== null)
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
`# Environment`,
|
`# Environment`,
|
||||||
`You have been invoked in the following environment: `,
|
`You are Claude Code.`,
|
||||||
...prependBullets(envItems),
|
modelDescription,
|
||||||
].join(`\n`)
|
knowledgeCutoffMessage,
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(`\n`)
|
||||||
}
|
}
|
||||||
|
|
||||||
// @[MODEL LAUNCH]: Add a knowledge cutoff date for the new model.
|
// @[MODEL LAUNCH]: Add a knowledge cutoff date for the new model.
|
||||||
|
|||||||
177
src/context.ts
177
src/context.ts
@@ -1,25 +1,7 @@
|
|||||||
import { feature } from 'bun:bundle'
|
|
||||||
import memoize from 'lodash-es/memoize.js'
|
import memoize from 'lodash-es/memoize.js'
|
||||||
import {
|
import { setCachedClaudeMdContent } from './bootstrap/state.js'
|
||||||
getAdditionalDirectoriesForClaudeMd,
|
|
||||||
setCachedClaudeMdContent,
|
|
||||||
} from './bootstrap/state.js'
|
|
||||||
import { getLocalISODate } from './constants/common.js'
|
|
||||||
import {
|
|
||||||
filterInjectedMemoryFiles,
|
|
||||||
getClaudeMds,
|
|
||||||
getMemoryFiles,
|
|
||||||
} from './utils/claudemd.js'
|
|
||||||
import { logForDiagnosticsNoPII } from './utils/diagLogs.js'
|
|
||||||
import { isBareMode, isEnvTruthy } from './utils/envUtils.js'
|
|
||||||
import { execFileNoThrow } from './utils/execFileNoThrow.js'
|
|
||||||
import { getBranch, getDefaultBranch, getIsGit, gitExe } from './utils/git.js'
|
|
||||||
import { shouldIncludeGitInstructions } from './utils/gitSettings.js'
|
|
||||||
import { logError } from './utils/log.js'
|
|
||||||
|
|
||||||
const MAX_STATUS_CHARS = 2000
|
// System prompt injection remains a local cache-busting hook only.
|
||||||
|
|
||||||
// System prompt injection for cache breaking (ant-only, ephemeral debugging state)
|
|
||||||
let systemPromptInjection: string | null = null
|
let systemPromptInjection: string | null = null
|
||||||
|
|
||||||
export function getSystemPromptInjection(): string | null {
|
export function getSystemPromptInjection(): string | null {
|
||||||
@@ -28,162 +10,17 @@ export function getSystemPromptInjection(): string | null {
|
|||||||
|
|
||||||
export function setSystemPromptInjection(value: string | null): void {
|
export function setSystemPromptInjection(value: string | null): void {
|
||||||
systemPromptInjection = value
|
systemPromptInjection = value
|
||||||
// Clear context caches immediately when injection changes
|
|
||||||
getUserContext.cache.clear?.()
|
getUserContext.cache.clear?.()
|
||||||
getSystemContext.cache.clear?.()
|
getSystemContext.cache.clear?.()
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getGitStatus = memoize(async (): Promise<string | null> => {
|
export const getGitStatus = memoize(async (): Promise<string | null> => null)
|
||||||
if (process.env.NODE_ENV === 'test') {
|
|
||||||
// Avoid cycles in tests
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
const startTime = Date.now()
|
|
||||||
logForDiagnosticsNoPII('info', 'git_status_started')
|
|
||||||
|
|
||||||
const isGitStart = Date.now()
|
|
||||||
const isGit = await getIsGit()
|
|
||||||
logForDiagnosticsNoPII('info', 'git_is_git_check_completed', {
|
|
||||||
duration_ms: Date.now() - isGitStart,
|
|
||||||
is_git: isGit,
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!isGit) {
|
|
||||||
logForDiagnosticsNoPII('info', 'git_status_skipped_not_git', {
|
|
||||||
duration_ms: Date.now() - startTime,
|
|
||||||
})
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const gitCmdsStart = Date.now()
|
|
||||||
const [branch, mainBranch, status, log, userName] = await Promise.all([
|
|
||||||
getBranch(),
|
|
||||||
getDefaultBranch(),
|
|
||||||
execFileNoThrow(gitExe(), ['--no-optional-locks', 'status', '--short'], {
|
|
||||||
preserveOutputOnError: false,
|
|
||||||
}).then(({ stdout }) => stdout.trim()),
|
|
||||||
execFileNoThrow(
|
|
||||||
gitExe(),
|
|
||||||
['--no-optional-locks', 'log', '--oneline', '-n', '5'],
|
|
||||||
{
|
|
||||||
preserveOutputOnError: false,
|
|
||||||
},
|
|
||||||
).then(({ stdout }) => stdout.trim()),
|
|
||||||
execFileNoThrow(gitExe(), ['config', 'user.name'], {
|
|
||||||
preserveOutputOnError: false,
|
|
||||||
}).then(({ stdout }) => stdout.trim()),
|
|
||||||
])
|
|
||||||
|
|
||||||
logForDiagnosticsNoPII('info', 'git_commands_completed', {
|
|
||||||
duration_ms: Date.now() - gitCmdsStart,
|
|
||||||
status_length: status.length,
|
|
||||||
})
|
|
||||||
|
|
||||||
// Check if status exceeds character limit
|
|
||||||
const truncatedStatus =
|
|
||||||
status.length > MAX_STATUS_CHARS
|
|
||||||
? status.substring(0, MAX_STATUS_CHARS) +
|
|
||||||
'\n... (truncated because it exceeds 2k characters. If you need more information, run "git status" using BashTool)'
|
|
||||||
: status
|
|
||||||
|
|
||||||
logForDiagnosticsNoPII('info', 'git_status_completed', {
|
|
||||||
duration_ms: Date.now() - startTime,
|
|
||||||
truncated: status.length > MAX_STATUS_CHARS,
|
|
||||||
})
|
|
||||||
|
|
||||||
return [
|
|
||||||
`This is the git status at the start of the conversation. Note that this status is a snapshot in time, and will not update during the conversation.`,
|
|
||||||
`Current branch: ${branch}`,
|
|
||||||
`Main branch (you will usually use this for PRs): ${mainBranch}`,
|
|
||||||
...(userName ? [`Git user: ${userName}`] : []),
|
|
||||||
`Status:\n${truncatedStatus || '(clean)'}`,
|
|
||||||
`Recent commits:\n${log}`,
|
|
||||||
].join('\n\n')
|
|
||||||
} catch (error) {
|
|
||||||
logForDiagnosticsNoPII('error', 'git_status_failed', {
|
|
||||||
duration_ms: Date.now() - startTime,
|
|
||||||
})
|
|
||||||
logError(error)
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This context is prepended to each conversation, and cached for the duration of the conversation.
|
|
||||||
*/
|
|
||||||
export const getSystemContext = memoize(
|
export const getSystemContext = memoize(
|
||||||
async (): Promise<{
|
async (): Promise<Record<string, string>> => ({}),
|
||||||
[k: string]: string
|
|
||||||
}> => {
|
|
||||||
const startTime = Date.now()
|
|
||||||
logForDiagnosticsNoPII('info', 'system_context_started')
|
|
||||||
|
|
||||||
// Skip git status in CCR (unnecessary overhead on resume) or when git instructions are disabled
|
|
||||||
const gitStatus =
|
|
||||||
isEnvTruthy(process.env.CLAUDE_CODE_REMOTE) ||
|
|
||||||
!shouldIncludeGitInstructions()
|
|
||||||
? null
|
|
||||||
: await getGitStatus()
|
|
||||||
|
|
||||||
// Include system prompt injection if set (for cache breaking, ant-only)
|
|
||||||
const injection = feature('BREAK_CACHE_COMMAND')
|
|
||||||
? getSystemPromptInjection()
|
|
||||||
: null
|
|
||||||
|
|
||||||
logForDiagnosticsNoPII('info', 'system_context_completed', {
|
|
||||||
duration_ms: Date.now() - startTime,
|
|
||||||
has_git_status: gitStatus !== null,
|
|
||||||
has_injection: injection !== null,
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
|
||||||
...(gitStatus && { gitStatus }),
|
|
||||||
...(feature('BREAK_CACHE_COMMAND') && injection
|
|
||||||
? {
|
|
||||||
cacheBreaker: `[CACHE_BREAKER: ${injection}]`,
|
|
||||||
}
|
|
||||||
: {}),
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
export const getUserContext = memoize(async (): Promise<Record<string, string>> => {
|
||||||
* This context is prepended to each conversation, and cached for the duration of the conversation.
|
setCachedClaudeMdContent(null)
|
||||||
*/
|
return {}
|
||||||
export const getUserContext = memoize(
|
|
||||||
async (): Promise<{
|
|
||||||
[k: string]: string
|
|
||||||
}> => {
|
|
||||||
const startTime = Date.now()
|
|
||||||
logForDiagnosticsNoPII('info', 'user_context_started')
|
|
||||||
|
|
||||||
// CLAUDE_CODE_DISABLE_CLAUDE_MDS: hard off, always.
|
|
||||||
// --bare: skip auto-discovery (cwd walk), BUT honor explicit --add-dir.
|
|
||||||
// --bare means "skip what I didn't ask for", not "ignore what I asked for".
|
|
||||||
const shouldDisableClaudeMd =
|
|
||||||
isEnvTruthy(process.env.CLAUDE_CODE_DISABLE_CLAUDE_MDS) ||
|
|
||||||
(isBareMode() && getAdditionalDirectoriesForClaudeMd().length === 0)
|
|
||||||
// Await the async I/O (readFile/readdir directory walk) so the event
|
|
||||||
// loop yields naturally at the first fs.readFile.
|
|
||||||
const claudeMd = shouldDisableClaudeMd
|
|
||||||
? null
|
|
||||||
: getClaudeMds(filterInjectedMemoryFiles(await getMemoryFiles()))
|
|
||||||
// Cache for the auto-mode classifier (yoloClassifier.ts reads this
|
|
||||||
// instead of importing claudemd.ts directly, which would create a
|
|
||||||
// cycle through permissions/filesystem → permissions → yoloClassifier).
|
|
||||||
setCachedClaudeMdContent(claudeMd || null)
|
|
||||||
|
|
||||||
logForDiagnosticsNoPII('info', 'user_context_completed', {
|
|
||||||
duration_ms: Date.now() - startTime,
|
|
||||||
claudemd_length: claudeMd?.length ?? 0,
|
|
||||||
claudemd_disabled: Boolean(shouldDisableClaudeMd),
|
|
||||||
})
|
})
|
||||||
|
|
||||||
return {
|
|
||||||
...(claudeMd && { claudeMd }),
|
|
||||||
currentDate: `Today's date is ${getLocalISODate()}.`,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ import {
|
|||||||
setCostStateForRestore,
|
setCostStateForRestore,
|
||||||
setHasUnknownModelCost,
|
setHasUnknownModelCost,
|
||||||
} from './bootstrap/state.js'
|
} from './bootstrap/state.js'
|
||||||
import type { ModelUsage } from './entrypoints/agentSdkTypes.js'
|
import type { ModelUsage } from './entrypoints/agentSdkTypes.ts'
|
||||||
import {
|
import {
|
||||||
type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||||
logEvent,
|
logEvent,
|
||||||
|
|||||||
@@ -1,16 +1,24 @@
|
|||||||
import { feature } from 'bun:bundle';
|
import { feature } from 'bun:bundle';
|
||||||
|
|
||||||
const CLI_MACRO =
|
// Define MACRO global for development (normally injected by bun build --define)
|
||||||
typeof MACRO !== 'undefined'
|
if (typeof MACRO === 'undefined') {
|
||||||
? MACRO
|
(globalThis as typeof globalThis & {
|
||||||
: {
|
MACRO: {
|
||||||
VERSION: 'dev',
|
VERSION: string
|
||||||
BUILD_TIME: '',
|
BUILD_TIME: string
|
||||||
PACKAGE_URL: '@anthropic-ai/claude-code',
|
PACKAGE_URL: string
|
||||||
|
ISSUES_EXPLAINER: string
|
||||||
|
FEEDBACK_CHANNEL: string
|
||||||
|
}
|
||||||
|
}).MACRO = {
|
||||||
|
VERSION: '2.1.88-dev',
|
||||||
|
BUILD_TIME: new Date().toISOString(),
|
||||||
|
PACKAGE_URL: 'claude-code-recover',
|
||||||
ISSUES_EXPLAINER:
|
ISSUES_EXPLAINER:
|
||||||
'https://docs.anthropic.com/en/docs/claude-code/feedback',
|
'https://docs.anthropic.com/en/docs/claude-code/feedback',
|
||||||
FEEDBACK_CHANNEL: 'github',
|
FEEDBACK_CHANNEL: 'github',
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// Bugfix for corepack auto-pinning, which adds yarnpkg to peoples' package.jsons
|
// Bugfix for corepack auto-pinning, which adds yarnpkg to peoples' package.jsons
|
||||||
// eslint-disable-next-line custom-rules/no-top-level-side-effects
|
// eslint-disable-next-line custom-rules/no-top-level-side-effects
|
||||||
@@ -49,7 +57,7 @@ async function main(): Promise<void> {
|
|||||||
if (args.length === 1 && (args[0] === '--version' || args[0] === '-v' || args[0] === '-V')) {
|
if (args.length === 1 && (args[0] === '--version' || args[0] === '-v' || args[0] === '-V')) {
|
||||||
// MACRO.VERSION is inlined at build time
|
// MACRO.VERSION is inlined at build time
|
||||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||||
console.log(`${CLI_MACRO.VERSION} (Claude Code)`);
|
console.log(`${MACRO.VERSION} (Claude Code)`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -87,22 +87,8 @@ export const init = memoize(async (): Promise<void> => {
|
|||||||
setupGracefulShutdown()
|
setupGracefulShutdown()
|
||||||
profileCheckpoint('init_after_graceful_shutdown')
|
profileCheckpoint('init_after_graceful_shutdown')
|
||||||
|
|
||||||
// Initialize 1P event logging (no security concerns, but deferred to avoid
|
// Telemetry/log export is disabled in this build. Keep the startup
|
||||||
// loading OpenTelemetry sdk-logs at startup). growthbook.js is already in
|
// checkpoint so callers depending on the init timeline still see it.
|
||||||
// the module cache by this point (firstPartyEventLogger imports it), so the
|
|
||||||
// second dynamic import adds no load cost.
|
|
||||||
void Promise.all([
|
|
||||||
import('../services/analytics/firstPartyEventLogger.js'),
|
|
||||||
import('../services/analytics/growthbook.js'),
|
|
||||||
]).then(([fp, gb]) => {
|
|
||||||
fp.initialize1PEventLogging()
|
|
||||||
// Rebuild the logger provider if tengu_1p_event_batch_config changes
|
|
||||||
// mid-session. Change detection (isEqual) is inside the handler so
|
|
||||||
// unchanged refreshes are no-ops.
|
|
||||||
gb.onGrowthBookRefresh(() => {
|
|
||||||
void fp.reinitialize1PEventLoggingIfConfigChanged()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
profileCheckpoint('init_after_1p_event_logging')
|
profileCheckpoint('init_after_1p_event_logging')
|
||||||
|
|
||||||
// Populate OAuth account info if it is not already cached in config. This is needed since the
|
// Populate OAuth account info if it is not already cached in config. This is needed since the
|
||||||
@@ -245,96 +231,14 @@ export const init = memoize(async (): Promise<void> => {
|
|||||||
* This should only be called once, after the trust dialog has been accepted.
|
* This should only be called once, after the trust dialog has been accepted.
|
||||||
*/
|
*/
|
||||||
export function initializeTelemetryAfterTrust(): void {
|
export function initializeTelemetryAfterTrust(): void {
|
||||||
if (isEligibleForRemoteManagedSettings()) {
|
|
||||||
// For SDK/headless mode with beta tracing, initialize eagerly first
|
|
||||||
// to ensure the tracer is ready before the first query runs.
|
|
||||||
// The async path below will still run but doInitializeTelemetry() guards against double init.
|
|
||||||
if (getIsNonInteractiveSession() && isBetaTracingEnabled()) {
|
|
||||||
void doInitializeTelemetry().catch(error => {
|
|
||||||
logForDebugging(
|
|
||||||
`[3P telemetry] Eager telemetry init failed (beta tracing): ${errorMessage(error)}`,
|
|
||||||
{ level: 'error' },
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
logForDebugging(
|
|
||||||
'[3P telemetry] Waiting for remote managed settings before telemetry init',
|
|
||||||
)
|
|
||||||
void waitForRemoteManagedSettingsToLoad()
|
|
||||||
.then(async () => {
|
|
||||||
logForDebugging(
|
|
||||||
'[3P telemetry] Remote managed settings loaded, initializing telemetry',
|
|
||||||
)
|
|
||||||
// Re-apply env vars to pick up remote settings before initializing telemetry.
|
|
||||||
applyConfigEnvironmentVariables()
|
|
||||||
await doInitializeTelemetry()
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
logForDebugging(
|
|
||||||
`[3P telemetry] Telemetry init failed (remote settings path): ${errorMessage(error)}`,
|
|
||||||
{ level: 'error' },
|
|
||||||
)
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
void doInitializeTelemetry().catch(error => {
|
|
||||||
logForDebugging(
|
|
||||||
`[3P telemetry] Telemetry init failed: ${errorMessage(error)}`,
|
|
||||||
{ level: 'error' },
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function doInitializeTelemetry(): Promise<void> {
|
|
||||||
if (telemetryInitialized) {
|
|
||||||
// Already initialized, nothing to do
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set flag before init to prevent double initialization
|
async function doInitializeTelemetry(): Promise<void> {
|
||||||
telemetryInitialized = true
|
void telemetryInitialized
|
||||||
try {
|
return
|
||||||
await setMeterState()
|
|
||||||
} catch (error) {
|
|
||||||
// Reset flag on failure so subsequent calls can retry
|
|
||||||
telemetryInitialized = false
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function setMeterState(): Promise<void> {
|
async function setMeterState(): Promise<void> {
|
||||||
// Lazy-load instrumentation to defer ~400KB of OpenTelemetry + protobuf
|
return
|
||||||
const { initializeTelemetry } = await import(
|
|
||||||
'../utils/telemetry/instrumentation.js'
|
|
||||||
)
|
|
||||||
// Initialize customer OTLP telemetry (metrics, logs, traces)
|
|
||||||
const meter = await initializeTelemetry()
|
|
||||||
if (meter) {
|
|
||||||
// Create factory function for attributed counters
|
|
||||||
const createAttributedCounter = (
|
|
||||||
name: string,
|
|
||||||
options: MetricOptions,
|
|
||||||
): AttributedCounter => {
|
|
||||||
const counter = meter?.createCounter(name, options)
|
|
||||||
|
|
||||||
return {
|
|
||||||
add(value: number, additionalAttributes: Attributes = {}) {
|
|
||||||
// Always fetch fresh telemetry attributes to ensure they're up to date
|
|
||||||
const currentAttributes = getTelemetryAttributes()
|
|
||||||
const mergedAttributes = {
|
|
||||||
...currentAttributes,
|
|
||||||
...additionalAttributes,
|
|
||||||
}
|
|
||||||
counter?.add(value, mergedAttributes)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setMeter(meter, createAttributedCounter)
|
|
||||||
|
|
||||||
// Increment session counter here because the startup telemetry path
|
|
||||||
// runs before this async initialization completes, so the counter
|
|
||||||
// would be null there.
|
|
||||||
getSessionCounter()?.add(1)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +0,0 @@
|
|||||||
export {
|
|
||||||
SDKControlRequest,
|
|
||||||
SDKControlResponse,
|
|
||||||
} from './controlTypes.ts'
|
|
||||||
@@ -1,14 +1,325 @@
|
|||||||
export type SDKControlRequest = Record<string, unknown> & {
|
import type { SDKMessage } from './coreTypes.ts'
|
||||||
subtype?: string
|
|
||||||
|
export type SDKPermissionResponse =
|
||||||
|
| {
|
||||||
|
behavior: 'allow'
|
||||||
|
updatedInput?: Record<string, unknown>
|
||||||
|
message?: string
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
behavior: 'deny' | 'ask'
|
||||||
|
message?: string
|
||||||
|
updatedInput?: Record<string, unknown>
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SDKControlResponse = Record<string, unknown> & {
|
export type SDKControlInterruptRequest = {
|
||||||
type?: string
|
subtype: 'interrupt'
|
||||||
subtype?: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type StdoutMessage = SDKControlResponse
|
export type SDKControlPermissionRequest = {
|
||||||
|
subtype: 'can_use_tool'
|
||||||
|
tool_name: string
|
||||||
|
input: Record<string, unknown>
|
||||||
|
permission_suggestions?: Array<Record<string, unknown>>
|
||||||
|
blocked_path?: string
|
||||||
|
decision_reason?: string
|
||||||
|
title?: string
|
||||||
|
display_name?: string
|
||||||
|
tool_use_id: string
|
||||||
|
agent_id?: string
|
||||||
|
description?: string
|
||||||
|
}
|
||||||
|
|
||||||
export class SDKControlRequest {}
|
export type SDKControlInitializeRequest = {
|
||||||
|
subtype: 'initialize'
|
||||||
|
hooks?: Record<string, Array<Record<string, unknown>>>
|
||||||
|
sdkMcpServers?: string[]
|
||||||
|
jsonSchema?: Record<string, unknown>
|
||||||
|
systemPrompt?: string
|
||||||
|
appendSystemPrompt?: string
|
||||||
|
agents?: Record<string, unknown>
|
||||||
|
promptSuggestions?: boolean
|
||||||
|
agentProgressSummaries?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
export class SDKControlResponse {}
|
export type SDKControlSetPermissionModeRequest = {
|
||||||
|
subtype: 'set_permission_mode'
|
||||||
|
mode: string
|
||||||
|
ultraplan?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SDKControlSetModelRequest = {
|
||||||
|
subtype: 'set_model'
|
||||||
|
model?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SDKControlSetMaxThinkingTokensRequest = {
|
||||||
|
subtype: 'set_max_thinking_tokens'
|
||||||
|
max_thinking_tokens: number | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SDKControlMcpStatusRequest = {
|
||||||
|
subtype: 'mcp_status'
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SDKControlGetContextUsageRequest = {
|
||||||
|
subtype: 'get_context_usage'
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SDKControlRewindFilesRequest = {
|
||||||
|
subtype: 'rewind_files'
|
||||||
|
paths?: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SDKControlCancelAsyncMessageRequest = {
|
||||||
|
subtype: 'cancel_async_message'
|
||||||
|
message_uuid: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SDKControlSeedReadStateRequest = {
|
||||||
|
subtype: 'seed_read_state'
|
||||||
|
path: string
|
||||||
|
mtime: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SDKHookCallbackRequest = {
|
||||||
|
subtype: 'hook_callback'
|
||||||
|
callback_id: string
|
||||||
|
input: Record<string, unknown>
|
||||||
|
tool_use_id?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SDKControlMcpMessageRequest = {
|
||||||
|
subtype: 'mcp_message'
|
||||||
|
server_name: string
|
||||||
|
message: Record<string, unknown>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SDKControlMcpSetServersRequest = {
|
||||||
|
subtype: 'mcp_set_servers'
|
||||||
|
servers: Record<string, Record<string, unknown>>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SDKControlReloadPluginsRequest = {
|
||||||
|
subtype: 'reload_plugins'
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SDKControlMcpReconnectRequest = {
|
||||||
|
subtype: 'mcp_reconnect'
|
||||||
|
serverName: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SDKControlMcpToggleRequest = {
|
||||||
|
subtype: 'mcp_toggle'
|
||||||
|
serverName: string
|
||||||
|
enabled: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SDKControlStopTaskRequest = {
|
||||||
|
subtype: 'stop_task'
|
||||||
|
task_id: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SDKControlApplyFlagSettingsRequest = {
|
||||||
|
subtype: 'apply_flag_settings'
|
||||||
|
settings?: Record<string, unknown>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SDKControlGetSettingsRequest = {
|
||||||
|
subtype: 'get_settings'
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SDKControlElicitationRequest = {
|
||||||
|
subtype: 'elicitation'
|
||||||
|
prompt?: string
|
||||||
|
spec?: Record<string, unknown>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SDKControlRequestInner =
|
||||||
|
| SDKControlInterruptRequest
|
||||||
|
| SDKControlPermissionRequest
|
||||||
|
| SDKControlInitializeRequest
|
||||||
|
| SDKControlSetPermissionModeRequest
|
||||||
|
| SDKControlSetModelRequest
|
||||||
|
| SDKControlSetMaxThinkingTokensRequest
|
||||||
|
| SDKControlMcpStatusRequest
|
||||||
|
| SDKControlGetContextUsageRequest
|
||||||
|
| SDKHookCallbackRequest
|
||||||
|
| SDKControlRewindFilesRequest
|
||||||
|
| SDKControlCancelAsyncMessageRequest
|
||||||
|
| SDKControlSeedReadStateRequest
|
||||||
|
| SDKControlMcpMessageRequest
|
||||||
|
| SDKControlMcpSetServersRequest
|
||||||
|
| SDKControlReloadPluginsRequest
|
||||||
|
| SDKControlMcpReconnectRequest
|
||||||
|
| SDKControlMcpToggleRequest
|
||||||
|
| SDKControlStopTaskRequest
|
||||||
|
| SDKControlApplyFlagSettingsRequest
|
||||||
|
| SDKControlGetSettingsRequest
|
||||||
|
| SDKControlElicitationRequest
|
||||||
|
| ({
|
||||||
|
subtype: string
|
||||||
|
} & Record<string, unknown>)
|
||||||
|
|
||||||
|
export type SDKControlRequest = {
|
||||||
|
type: 'control_request'
|
||||||
|
request_id: string
|
||||||
|
request: SDKControlRequestInner
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SDKControlSuccessResponse = {
|
||||||
|
subtype: 'success'
|
||||||
|
request_id: string
|
||||||
|
response?: SDKPermissionResponse | Record<string, unknown>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SDKControlErrorResponse = {
|
||||||
|
subtype: 'error'
|
||||||
|
request_id: string
|
||||||
|
error: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SDKControlInitializeResponse = {
|
||||||
|
subtype: 'initialize'
|
||||||
|
request_id: string
|
||||||
|
commands?: Array<Record<string, unknown>>
|
||||||
|
agents?: Array<Record<string, unknown>>
|
||||||
|
output_style?: string
|
||||||
|
available_output_styles?: string[]
|
||||||
|
models?: Array<Record<string, unknown>>
|
||||||
|
account?: Record<string, unknown>
|
||||||
|
pid?: number
|
||||||
|
fast_mode_state?: Record<string, unknown>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SDKControlMcpStatusResponse = {
|
||||||
|
subtype: 'mcp_status'
|
||||||
|
request_id: string
|
||||||
|
mcpServers?: Array<Record<string, unknown>>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SDKControlCancelAsyncMessageResponse = {
|
||||||
|
subtype: 'cancel_async_message'
|
||||||
|
request_id: string
|
||||||
|
cancelled: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SDKControlMcpSetServersResponse = {
|
||||||
|
subtype: 'mcp_set_servers'
|
||||||
|
request_id: string
|
||||||
|
added: string[]
|
||||||
|
removed: string[]
|
||||||
|
errors: Record<string, string>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SDKControlReloadPluginsResponse = {
|
||||||
|
subtype: 'reload_plugins'
|
||||||
|
request_id: string
|
||||||
|
commands?: Array<Record<string, unknown>>
|
||||||
|
agents?: Array<Record<string, unknown>>
|
||||||
|
plugins?: Array<Record<string, unknown>>
|
||||||
|
mcpServers?: Array<Record<string, unknown>>
|
||||||
|
error_count?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SDKControlGetContextUsageResponse = {
|
||||||
|
subtype: 'get_context_usage'
|
||||||
|
request_id: string
|
||||||
|
categories?: Array<Record<string, unknown>>
|
||||||
|
totalTokens?: number
|
||||||
|
maxTokens?: number
|
||||||
|
rawMaxTokens?: number
|
||||||
|
percentage?: number
|
||||||
|
gridRows?: Array<Array<Record<string, unknown>>>
|
||||||
|
model?: string
|
||||||
|
memoryFiles?: Array<Record<string, unknown>>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SDKControlGetSettingsResponse = {
|
||||||
|
subtype: 'get_settings'
|
||||||
|
request_id: string
|
||||||
|
effective: Record<string, unknown>
|
||||||
|
sources: Array<{
|
||||||
|
source:
|
||||||
|
| 'userSettings'
|
||||||
|
| 'projectSettings'
|
||||||
|
| 'localSettings'
|
||||||
|
| 'flagSettings'
|
||||||
|
| 'policySettings'
|
||||||
|
settings: Record<string, unknown>
|
||||||
|
}>
|
||||||
|
applied?: {
|
||||||
|
model: string
|
||||||
|
effort: 'low' | 'medium' | 'high' | 'max' | null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SDKControlElicitationResponse = {
|
||||||
|
subtype: 'elicitation'
|
||||||
|
request_id: string
|
||||||
|
response?: {
|
||||||
|
action?: 'accept' | 'decline' | 'cancel'
|
||||||
|
content?: Record<string, unknown>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SDKControlResponseInner =
|
||||||
|
| SDKControlSuccessResponse
|
||||||
|
| SDKControlErrorResponse
|
||||||
|
| SDKControlInitializeResponse
|
||||||
|
| SDKControlMcpStatusResponse
|
||||||
|
| SDKControlCancelAsyncMessageResponse
|
||||||
|
| SDKControlMcpSetServersResponse
|
||||||
|
| SDKControlReloadPluginsResponse
|
||||||
|
| SDKControlGetContextUsageResponse
|
||||||
|
| SDKControlGetSettingsResponse
|
||||||
|
| SDKControlElicitationResponse
|
||||||
|
| ({
|
||||||
|
subtype: string
|
||||||
|
request_id: string
|
||||||
|
} & Record<string, unknown>)
|
||||||
|
|
||||||
|
export type SDKControlResponse = {
|
||||||
|
type: 'control_response'
|
||||||
|
response: SDKControlResponseInner
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SDKControlCancelRequest = {
|
||||||
|
type: 'control_cancel_request'
|
||||||
|
request_id: string
|
||||||
|
tool_use_id?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type KeepAliveMessage = {
|
||||||
|
type: 'keep_alive'
|
||||||
|
[key: string]: unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SDKUpdateEnvironmentVariablesMessage = {
|
||||||
|
type: 'update_environment_variables'
|
||||||
|
variables: Record<string, string>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SDKStreamlinedTextMessage = {
|
||||||
|
type: 'streamlined_text'
|
||||||
|
text: string
|
||||||
|
session_id?: string
|
||||||
|
uuid?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SDKStreamlinedToolUseSummaryMessage = {
|
||||||
|
type: 'streamlined_tool_use_summary'
|
||||||
|
tool_summary: string
|
||||||
|
session_id?: string
|
||||||
|
uuid?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type StdoutMessage =
|
||||||
|
| SDKMessage
|
||||||
|
| SDKControlRequest
|
||||||
|
| SDKControlResponse
|
||||||
|
| SDKControlCancelRequest
|
||||||
|
| KeepAliveMessage
|
||||||
|
| SDKUpdateEnvironmentVariablesMessage
|
||||||
|
| SDKStreamlinedTextMessage
|
||||||
|
| SDKStreamlinedToolUseSummaryMessage
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
export * from './coreTypes.generated.ts'
|
|
||||||
@@ -16,7 +16,7 @@ export type {
|
|||||||
SandboxSettings,
|
SandboxSettings,
|
||||||
} from '../sandboxTypes.js'
|
} from '../sandboxTypes.js'
|
||||||
// Re-export all generated types
|
// Re-export all generated types
|
||||||
export * from './coreTypes.generated.js'
|
export * from './coreTypes.generated.ts'
|
||||||
|
|
||||||
// Re-export utility types that can't be expressed as Zod schemas
|
// Re-export utility types that can't be expressed as Zod schemas
|
||||||
export type { NonNullableUsage } from './sdkUtilityTypes.js'
|
export type { NonNullableUsage } from './sdkUtilityTypes.js'
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
export * from './runtimeTypes.ts'
|
|
||||||
@@ -1,5 +1,11 @@
|
|||||||
import type { CallToolResult, ToolAnnotations } from '@modelcontextprotocol/sdk/types.js'
|
import type { CallToolResult, ToolAnnotations } from '@modelcontextprotocol/sdk/types.js'
|
||||||
import type { ZodTypeAny } from 'zod/v4'
|
import type { ZodTypeAny } from 'zod/v4'
|
||||||
|
import type {
|
||||||
|
SDKMessage,
|
||||||
|
SDKResultMessage,
|
||||||
|
SDKSessionInfo as CoreSDKSessionInfo,
|
||||||
|
SDKUserMessage,
|
||||||
|
} from './coreTypes.ts'
|
||||||
|
|
||||||
export type EffortLevel = 'low' | 'medium' | 'high' | 'max'
|
export type EffortLevel = 'low' | 'medium' | 'high' | 'max'
|
||||||
|
|
||||||
@@ -19,17 +25,33 @@ export type SdkMcpToolDefinition<Schema extends AnyZodRawShape> = {
|
|||||||
alwaysLoad?: boolean
|
alwaysLoad?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export type McpSdkServerConfigWithInstance = Record<string, unknown>
|
export type McpSdkServerConfigWithInstance = Record<string, unknown> & {
|
||||||
|
name?: string
|
||||||
export type Options = Record<string, unknown>
|
version?: string
|
||||||
export type InternalOptions = Options
|
instance?: unknown
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
export type SDKSessionOptions = Options & {
|
tools?: Array<SdkMcpToolDefinition<any>>
|
||||||
model?: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Query = AsyncIterable<unknown>
|
export type Options = Record<string, unknown> & {
|
||||||
export type InternalQuery = AsyncIterable<unknown>
|
cwd?: string
|
||||||
|
model?: string
|
||||||
|
permissionMode?: string
|
||||||
|
maxThinkingTokens?: number | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export type InternalOptions = Options & {
|
||||||
|
systemPrompt?: string
|
||||||
|
appendSystemPrompt?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SDKSessionOptions = Options & {
|
||||||
|
cwd?: string
|
||||||
|
resume?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Query = AsyncIterable<SDKMessage>
|
||||||
|
export type InternalQuery = AsyncIterable<SDKMessage>
|
||||||
|
|
||||||
export type SessionMutationOptions = {
|
export type SessionMutationOptions = {
|
||||||
dir?: string
|
dir?: string
|
||||||
@@ -48,7 +70,10 @@ export type GetSessionMessagesOptions = SessionMutationOptions & {
|
|||||||
includeSystemMessages?: boolean
|
includeSystemMessages?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ForkSessionOptions = SessionMutationOptions
|
export type ForkSessionOptions = SessionMutationOptions & {
|
||||||
|
upToMessageId?: string
|
||||||
|
title?: string
|
||||||
|
}
|
||||||
|
|
||||||
export type ForkSessionResult = {
|
export type ForkSessionResult = {
|
||||||
sessionId: string
|
sessionId: string
|
||||||
@@ -56,4 +81,13 @@ export type ForkSessionResult = {
|
|||||||
|
|
||||||
export type SDKSession = {
|
export type SDKSession = {
|
||||||
id: string
|
id: string
|
||||||
|
query(
|
||||||
|
prompt: string | AsyncIterable<SDKUserMessage>,
|
||||||
|
options?: Options,
|
||||||
|
): Query
|
||||||
|
interrupt(): void
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type SessionMessage = SDKMessage
|
||||||
|
|
||||||
|
export type SDKSessionInfo = CoreSDKSessionInfo
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
export * from './settingsTypes.generated.ts'
|
|
||||||
@@ -1 +1,13 @@
|
|||||||
export type Settings = Record<string, unknown>
|
export type SettingsValue =
|
||||||
|
| string
|
||||||
|
| number
|
||||||
|
| boolean
|
||||||
|
| null
|
||||||
|
| SettingsObject
|
||||||
|
| SettingsValue[]
|
||||||
|
|
||||||
|
export type SettingsObject = {
|
||||||
|
[key: string]: SettingsValue
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Settings = SettingsObject
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
export * from './toolTypes.ts'
|
|
||||||
23
src/entrypoints/sdk/toolTypes.ts
Executable file → Normal file
23
src/entrypoints/sdk/toolTypes.ts
Executable file → Normal file
@@ -1 +1,22 @@
|
|||||||
export {}
|
import type { CallToolResult, ToolAnnotations } from '@modelcontextprotocol/sdk/types.js'
|
||||||
|
|
||||||
|
import type { AnyZodRawShape, InferShape, SdkMcpToolDefinition } from './runtimeTypes.ts'
|
||||||
|
|
||||||
|
export type { AnyZodRawShape, InferShape, SdkMcpToolDefinition }
|
||||||
|
|
||||||
|
export type SdkMcpToolHandler<Schema extends AnyZodRawShape> = (
|
||||||
|
args: InferShape<Schema>,
|
||||||
|
extra: unknown,
|
||||||
|
) => Promise<CallToolResult>
|
||||||
|
|
||||||
|
export type SdkMcpToolExtras = {
|
||||||
|
annotations?: ToolAnnotations
|
||||||
|
searchHint?: string
|
||||||
|
alwaysLoad?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SdkMcpToolDescriptor<Schema extends AnyZodRawShape> = Pick<
|
||||||
|
SdkMcpToolDefinition<Schema>,
|
||||||
|
'name' | 'description' | 'inputSchema' | 'handler'
|
||||||
|
> &
|
||||||
|
SdkMcpToolExtras
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import type { Command } from '../commands.js';
|
|||||||
import { getSlashCommandToolSkills, isBridgeSafeCommand } from '../commands.js';
|
import { getSlashCommandToolSkills, isBridgeSafeCommand } from '../commands.js';
|
||||||
import { getRemoteSessionUrl } from '../constants/product.js';
|
import { getRemoteSessionUrl } from '../constants/product.js';
|
||||||
import { useNotifications } from '../context/notifications.js';
|
import { useNotifications } from '../context/notifications.js';
|
||||||
import type { PermissionMode, SDKMessage } from '../entrypoints/agentSdkTypes.js';
|
import type { PermissionMode, SDKMessage } from '../entrypoints/agentSdkTypes.ts';
|
||||||
import type { SDKControlResponse } from '../entrypoints/sdk/controlTypes.ts';
|
import type { SDKControlResponse } from '../entrypoints/sdk/controlTypes.ts';
|
||||||
import { Text } from '../ink.js';
|
import { Text } from '../ink.js';
|
||||||
import { getFeatureValue_CACHED_MAY_BE_STALE } from '../services/analytics/growthbook.js';
|
import { getFeatureValue_CACHED_MAY_BE_STALE } from '../services/analytics/growthbook.js';
|
||||||
|
|||||||
14
src/main.tsx
14
src/main.tsx
@@ -65,18 +65,6 @@ import { computeInitialTeamContext } from './utils/swarm/reconnection.js';
|
|||||||
import { initializeWarningHandler } from './utils/warningHandler.js';
|
import { initializeWarningHandler } from './utils/warningHandler.js';
|
||||||
import { isWorktreeModeEnabled } from './utils/worktreeModeEnabled.js';
|
import { isWorktreeModeEnabled } from './utils/worktreeModeEnabled.js';
|
||||||
|
|
||||||
const MAIN_MACRO =
|
|
||||||
typeof MACRO !== 'undefined'
|
|
||||||
? MACRO
|
|
||||||
: {
|
|
||||||
VERSION: 'dev',
|
|
||||||
BUILD_TIME: '',
|
|
||||||
PACKAGE_URL: '@anthropic-ai/claude-code',
|
|
||||||
ISSUES_EXPLAINER:
|
|
||||||
'https://docs.anthropic.com/en/docs/claude-code/feedback',
|
|
||||||
FEEDBACK_CHANNEL: 'github',
|
|
||||||
};
|
|
||||||
|
|
||||||
// Lazy require to avoid circular dependency: teammate.ts -> AppState.tsx -> ... -> main.tsx
|
// Lazy require to avoid circular dependency: teammate.ts -> AppState.tsx -> ... -> main.tsx
|
||||||
/* eslint-disable @typescript-eslint/no-require-imports */
|
/* eslint-disable @typescript-eslint/no-require-imports */
|
||||||
const getTeammateUtils = () => require('./utils/teammate.js') as typeof import('./utils/teammate.js');
|
const getTeammateUtils = () => require('./utils/teammate.js') as typeof import('./utils/teammate.js');
|
||||||
@@ -3817,7 +3805,7 @@ async function run(): Promise<CommanderCommand> {
|
|||||||
pendingHookMessages
|
pendingHookMessages
|
||||||
}, renderAndRun);
|
}, renderAndRun);
|
||||||
}
|
}
|
||||||
}).version(`${MAIN_MACRO.VERSION} (Claude Code)`, '-v, --version', 'Output the version number');
|
}).version(`${MACRO.VERSION} (Claude Code)`, '-v, --version', 'Output the version number');
|
||||||
|
|
||||||
// Worktree flags
|
// Worktree flags
|
||||||
program.option('-w, --worktree [name]', 'Create a new git worktree for this session (optionally specify a name)');
|
program.option('-w, --worktree [name]', 'Create a new git worktree for this session (optionally specify a name)');
|
||||||
|
|||||||
@@ -55,7 +55,6 @@ import {
|
|||||||
stripSignatureBlocks,
|
stripSignatureBlocks,
|
||||||
} from './utils/messages.js'
|
} from './utils/messages.js'
|
||||||
import { generateToolUseSummary } from './services/toolUseSummary/toolUseSummaryGenerator.js'
|
import { generateToolUseSummary } from './services/toolUseSummary/toolUseSummaryGenerator.js'
|
||||||
import { prependUserContext, appendSystemContext } from './utils/api.js'
|
|
||||||
import {
|
import {
|
||||||
createAttachmentMessage,
|
createAttachmentMessage,
|
||||||
filterDuplicateMemoryAttachments,
|
filterDuplicateMemoryAttachments,
|
||||||
@@ -446,9 +445,7 @@ async function* queryLoop(
|
|||||||
messagesForQuery = collapseResult.messages
|
messagesForQuery = collapseResult.messages
|
||||||
}
|
}
|
||||||
|
|
||||||
const fullSystemPrompt = asSystemPrompt(
|
const fullSystemPrompt = asSystemPrompt(systemPrompt)
|
||||||
appendSystemContext(systemPrompt, systemContext),
|
|
||||||
)
|
|
||||||
|
|
||||||
queryCheckpoint('query_autocompact_start')
|
queryCheckpoint('query_autocompact_start')
|
||||||
const { compactionResult, consecutiveFailures } = await deps.autocompact(
|
const { compactionResult, consecutiveFailures } = await deps.autocompact(
|
||||||
@@ -657,7 +654,7 @@ async function* queryLoop(
|
|||||||
let streamingFallbackOccured = false
|
let streamingFallbackOccured = false
|
||||||
queryCheckpoint('query_api_streaming_start')
|
queryCheckpoint('query_api_streaming_start')
|
||||||
for await (const message of deps.callModel({
|
for await (const message of deps.callModel({
|
||||||
messages: prependUserContext(messagesForQuery, userContext),
|
messages: messagesForQuery,
|
||||||
systemPrompt: fullSystemPrompt,
|
systemPrompt: fullSystemPrompt,
|
||||||
thinkingConfig: toolUseContext.options.thinkingConfig,
|
thinkingConfig: toolUseContext.options.thinkingConfig,
|
||||||
tools: toolUseContext.options.tools,
|
tools: toolUseContext.options.tools,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { SDKMessage } from '../entrypoints/agentSdkTypes.js'
|
import type { SDKMessage } from '../entrypoints/agentSdkTypes.ts'
|
||||||
import type {
|
import type {
|
||||||
SDKControlCancelRequest,
|
SDKControlCancelRequest,
|
||||||
SDKControlPermissionRequest,
|
SDKControlPermissionRequest,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { randomUUID } from 'crypto'
|
import { randomUUID } from 'crypto'
|
||||||
import { getOauthConfig } from '../constants/oauth.js'
|
import { getOauthConfig } from '../constants/oauth.js'
|
||||||
import type { SDKMessage } from '../entrypoints/agentSdkTypes.js'
|
import type { SDKMessage } from '../entrypoints/agentSdkTypes.ts'
|
||||||
import type {
|
import type {
|
||||||
SDKControlCancelRequest,
|
SDKControlCancelRequest,
|
||||||
SDKControlRequest,
|
SDKControlRequest,
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import type {
|
|||||||
SDKStatusMessage,
|
SDKStatusMessage,
|
||||||
SDKSystemMessage,
|
SDKSystemMessage,
|
||||||
SDKToolProgressMessage,
|
SDKToolProgressMessage,
|
||||||
} from '../entrypoints/agentSdkTypes.js'
|
} from '../entrypoints/agentSdkTypes.ts'
|
||||||
import type {
|
import type {
|
||||||
AssistantMessage,
|
AssistantMessage,
|
||||||
Message,
|
Message,
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
* Both files now import from this shared location instead of each other.
|
* Both files now import from this shared location instead of each other.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { HOOK_EVENTS, type HookEvent } from 'src/entrypoints/agentSdkTypes.js'
|
import { HOOK_EVENTS, type HookEvent } from 'src/entrypoints/agentSdkTypes.ts'
|
||||||
import { z } from 'zod/v4'
|
import { z } from 'zod/v4'
|
||||||
import { lazySchema } from '../utils/lazySchema.js'
|
import { lazySchema } from '../utils/lazySchema.js'
|
||||||
import { SHELL_TYPES } from '../utils/shell/shellProvider.js'
|
import { SHELL_TYPES } from '../utils/shell/shellProvider.js'
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/* eslint-disable eslint-plugin-n/no-unsupported-features/node-builtins */
|
/* eslint-disable eslint-plugin-n/no-unsupported-features/node-builtins */
|
||||||
|
|
||||||
import type { SDKMessage } from '../entrypoints/agentSdkTypes.js'
|
import type { SDKMessage } from '../entrypoints/agentSdkTypes.ts'
|
||||||
import type {
|
import type {
|
||||||
SDKControlPermissionRequest,
|
SDKControlPermissionRequest,
|
||||||
StdoutMessage,
|
StdoutMessage,
|
||||||
|
|||||||
@@ -1,307 +1,20 @@
|
|||||||
import axios from 'axios'
|
|
||||||
import { createHash } from 'crypto'
|
|
||||||
import memoize from 'lodash-es/memoize.js'
|
|
||||||
import { getOrCreateUserID } from '../../utils/config.js'
|
|
||||||
import { logError } from '../../utils/log.js'
|
|
||||||
import { getCanonicalName } from '../../utils/model/model.js'
|
|
||||||
import { getAPIProvider } from '../../utils/model/providers.js'
|
|
||||||
import { MODEL_COSTS } from '../../utils/modelCost.js'
|
|
||||||
import { isAnalyticsDisabled } from './config.js'
|
|
||||||
import { getEventMetadata } from './metadata.js'
|
|
||||||
|
|
||||||
const DATADOG_LOGS_ENDPOINT =
|
|
||||||
'https://http-intake.logs.us5.datadoghq.com/api/v2/logs'
|
|
||||||
const DATADOG_CLIENT_TOKEN = 'pubbbf48e6d78dae54bceaa4acf463299bf'
|
|
||||||
const DEFAULT_FLUSH_INTERVAL_MS = 15000
|
|
||||||
const MAX_BATCH_SIZE = 100
|
|
||||||
const NETWORK_TIMEOUT_MS = 5000
|
|
||||||
|
|
||||||
const DATADOG_ALLOWED_EVENTS = new Set([
|
|
||||||
'chrome_bridge_connection_succeeded',
|
|
||||||
'chrome_bridge_connection_failed',
|
|
||||||
'chrome_bridge_disconnected',
|
|
||||||
'chrome_bridge_tool_call_completed',
|
|
||||||
'chrome_bridge_tool_call_error',
|
|
||||||
'chrome_bridge_tool_call_started',
|
|
||||||
'chrome_bridge_tool_call_timeout',
|
|
||||||
'tengu_api_error',
|
|
||||||
'tengu_api_success',
|
|
||||||
'tengu_brief_mode_enabled',
|
|
||||||
'tengu_brief_mode_toggled',
|
|
||||||
'tengu_brief_send',
|
|
||||||
'tengu_cancel',
|
|
||||||
'tengu_compact_failed',
|
|
||||||
'tengu_exit',
|
|
||||||
'tengu_flicker',
|
|
||||||
'tengu_init',
|
|
||||||
'tengu_model_fallback_triggered',
|
|
||||||
'tengu_oauth_error',
|
|
||||||
'tengu_oauth_success',
|
|
||||||
'tengu_oauth_token_refresh_failure',
|
|
||||||
'tengu_oauth_token_refresh_success',
|
|
||||||
'tengu_oauth_token_refresh_lock_acquiring',
|
|
||||||
'tengu_oauth_token_refresh_lock_acquired',
|
|
||||||
'tengu_oauth_token_refresh_starting',
|
|
||||||
'tengu_oauth_token_refresh_completed',
|
|
||||||
'tengu_oauth_token_refresh_lock_releasing',
|
|
||||||
'tengu_oauth_token_refresh_lock_released',
|
|
||||||
'tengu_query_error',
|
|
||||||
'tengu_session_file_read',
|
|
||||||
'tengu_started',
|
|
||||||
'tengu_tool_use_error',
|
|
||||||
'tengu_tool_use_granted_in_prompt_permanent',
|
|
||||||
'tengu_tool_use_granted_in_prompt_temporary',
|
|
||||||
'tengu_tool_use_rejected_in_prompt',
|
|
||||||
'tengu_tool_use_success',
|
|
||||||
'tengu_uncaught_exception',
|
|
||||||
'tengu_unhandled_rejection',
|
|
||||||
'tengu_voice_recording_started',
|
|
||||||
'tengu_voice_toggled',
|
|
||||||
'tengu_team_mem_sync_pull',
|
|
||||||
'tengu_team_mem_sync_push',
|
|
||||||
'tengu_team_mem_sync_started',
|
|
||||||
'tengu_team_mem_entries_capped',
|
|
||||||
])
|
|
||||||
|
|
||||||
const TAG_FIELDS = [
|
|
||||||
'arch',
|
|
||||||
'clientType',
|
|
||||||
'errorType',
|
|
||||||
'http_status_range',
|
|
||||||
'http_status',
|
|
||||||
'kairosActive',
|
|
||||||
'model',
|
|
||||||
'platform',
|
|
||||||
'provider',
|
|
||||||
'skillMode',
|
|
||||||
'subscriptionType',
|
|
||||||
'toolName',
|
|
||||||
'userBucket',
|
|
||||||
'userType',
|
|
||||||
'version',
|
|
||||||
'versionBase',
|
|
||||||
]
|
|
||||||
|
|
||||||
function camelToSnakeCase(str: string): string {
|
|
||||||
return str.replace(/[A-Z]/g, letter => `_${letter.toLowerCase()}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
type DatadogLog = {
|
|
||||||
ddsource: string
|
|
||||||
ddtags: string
|
|
||||||
message: string
|
|
||||||
service: string
|
|
||||||
hostname: string
|
|
||||||
[key: string]: unknown
|
|
||||||
}
|
|
||||||
|
|
||||||
let logBatch: DatadogLog[] = []
|
|
||||||
let flushTimer: NodeJS.Timeout | null = null
|
|
||||||
let datadogInitialized: boolean | null = null
|
|
||||||
|
|
||||||
async function flushLogs(): Promise<void> {
|
|
||||||
if (logBatch.length === 0) return
|
|
||||||
|
|
||||||
const logsToSend = logBatch
|
|
||||||
logBatch = []
|
|
||||||
|
|
||||||
try {
|
|
||||||
await axios.post(DATADOG_LOGS_ENDPOINT, logsToSend, {
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'DD-API-KEY': DATADOG_CLIENT_TOKEN,
|
|
||||||
},
|
|
||||||
timeout: NETWORK_TIMEOUT_MS,
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
logError(error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function scheduleFlush(): void {
|
|
||||||
if (flushTimer) return
|
|
||||||
|
|
||||||
flushTimer = setTimeout(() => {
|
|
||||||
flushTimer = null
|
|
||||||
void flushLogs()
|
|
||||||
}, getFlushIntervalMs()).unref()
|
|
||||||
}
|
|
||||||
|
|
||||||
export const initializeDatadog = memoize(async (): Promise<boolean> => {
|
|
||||||
if (isAnalyticsDisabled()) {
|
|
||||||
datadogInitialized = false
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
datadogInitialized = true
|
|
||||||
return true
|
|
||||||
} catch (error) {
|
|
||||||
logError(error)
|
|
||||||
datadogInitialized = false
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Flush remaining Datadog logs and shut down.
|
* Datadog analytics egress is disabled in this build.
|
||||||
* Called from gracefulShutdown() before process.exit() since
|
*
|
||||||
* forceExit() prevents the beforeExit handler from firing.
|
* The exported functions remain so existing call sites do not need to branch.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
export async function initializeDatadog(): Promise<boolean> {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
export async function shutdownDatadog(): Promise<void> {
|
export async function shutdownDatadog(): Promise<void> {
|
||||||
if (flushTimer) {
|
return
|
||||||
clearTimeout(flushTimer)
|
|
||||||
flushTimer = null
|
|
||||||
}
|
|
||||||
await flushLogs()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NOTE: use via src/services/analytics/index.ts > logEvent
|
|
||||||
export async function trackDatadogEvent(
|
export async function trackDatadogEvent(
|
||||||
eventName: string,
|
_eventName: string,
|
||||||
properties: { [key: string]: boolean | number | undefined },
|
_properties: { [key: string]: boolean | number | undefined },
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
if (process.env.NODE_ENV !== 'production') {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Don't send events for 3P providers (Bedrock, Vertex, Foundry)
|
|
||||||
if (getAPIProvider() !== 'firstParty') {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fast path: use cached result if available to avoid await overhead
|
|
||||||
let initialized = datadogInitialized
|
|
||||||
if (initialized === null) {
|
|
||||||
initialized = await initializeDatadog()
|
|
||||||
}
|
|
||||||
if (!initialized || !DATADOG_ALLOWED_EVENTS.has(eventName)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const metadata = await getEventMetadata({
|
|
||||||
model: properties.model,
|
|
||||||
betas: properties.betas,
|
|
||||||
})
|
|
||||||
// Destructure to avoid duplicate envContext (once nested, once flattened)
|
|
||||||
const { envContext, ...restMetadata } = metadata
|
|
||||||
const allData: Record<string, unknown> = {
|
|
||||||
...restMetadata,
|
|
||||||
...envContext,
|
|
||||||
...properties,
|
|
||||||
userBucket: getUserBucket(),
|
|
||||||
}
|
|
||||||
|
|
||||||
// Normalize MCP tool names to "mcp" for cardinality reduction
|
|
||||||
if (
|
|
||||||
typeof allData.toolName === 'string' &&
|
|
||||||
allData.toolName.startsWith('mcp__')
|
|
||||||
) {
|
|
||||||
allData.toolName = 'mcp'
|
|
||||||
}
|
|
||||||
|
|
||||||
// Normalize model names for cardinality reduction (external users only)
|
|
||||||
if (process.env.USER_TYPE !== 'ant' && typeof allData.model === 'string') {
|
|
||||||
const shortName = getCanonicalName(allData.model.replace(/\[1m]$/i, ''))
|
|
||||||
allData.model = shortName in MODEL_COSTS ? shortName : 'other'
|
|
||||||
}
|
|
||||||
|
|
||||||
// Truncate dev version to base + date (remove timestamp and sha for cardinality reduction)
|
|
||||||
// e.g. "2.0.53-dev.20251124.t173302.sha526cc6a" -> "2.0.53-dev.20251124"
|
|
||||||
if (typeof allData.version === 'string') {
|
|
||||||
allData.version = allData.version.replace(
|
|
||||||
/^(\d+\.\d+\.\d+-dev\.\d{8})\.t\d+\.sha[a-f0-9]+$/,
|
|
||||||
'$1',
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Transform status to http_status and http_status_range to avoid Datadog reserved field
|
|
||||||
if (allData.status !== undefined && allData.status !== null) {
|
|
||||||
const statusCode = String(allData.status)
|
|
||||||
allData.http_status = statusCode
|
|
||||||
|
|
||||||
// Determine status range (1xx, 2xx, 3xx, 4xx, 5xx)
|
|
||||||
const firstDigit = statusCode.charAt(0)
|
|
||||||
if (firstDigit >= '1' && firstDigit <= '5') {
|
|
||||||
allData.http_status_range = `${firstDigit}xx`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove original status field to avoid conflict with Datadog's reserved field
|
|
||||||
delete allData.status
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build ddtags with high-cardinality fields for filtering.
|
|
||||||
// event:<name> is prepended so the event name is searchable via the
|
|
||||||
// log search API — the `message` field (where eventName also lives)
|
|
||||||
// is a DD reserved field and is NOT queryable from dashboard widget
|
|
||||||
// queries or the aggregation API. See scripts/release/MONITORING.md.
|
|
||||||
const allDataRecord = allData
|
|
||||||
const tags = [
|
|
||||||
`event:${eventName}`,
|
|
||||||
...TAG_FIELDS.filter(
|
|
||||||
field =>
|
|
||||||
allDataRecord[field] !== undefined && allDataRecord[field] !== null,
|
|
||||||
).map(field => `${camelToSnakeCase(field)}:${allDataRecord[field]}`),
|
|
||||||
]
|
|
||||||
|
|
||||||
const log: DatadogLog = {
|
|
||||||
ddsource: 'nodejs',
|
|
||||||
ddtags: tags.join(','),
|
|
||||||
message: eventName,
|
|
||||||
service: 'claude-code',
|
|
||||||
hostname: 'claude-code',
|
|
||||||
env: process.env.USER_TYPE,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add all fields as searchable attributes (not duplicated in tags)
|
|
||||||
for (const [key, value] of Object.entries(allData)) {
|
|
||||||
if (value !== undefined && value !== null) {
|
|
||||||
log[camelToSnakeCase(key)] = value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
logBatch.push(log)
|
|
||||||
|
|
||||||
// Flush immediately if batch is full, otherwise schedule
|
|
||||||
if (logBatch.length >= MAX_BATCH_SIZE) {
|
|
||||||
if (flushTimer) {
|
|
||||||
clearTimeout(flushTimer)
|
|
||||||
flushTimer = null
|
|
||||||
}
|
|
||||||
void flushLogs()
|
|
||||||
} else {
|
|
||||||
scheduleFlush()
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
logError(error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const NUM_USER_BUCKETS = 30
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets a 'bucket' that the user ID falls into.
|
|
||||||
*
|
|
||||||
* For alerting purposes, we want to alert on the number of users impacted
|
|
||||||
* by an issue, rather than the number of events- often a small number of users
|
|
||||||
* can generate a large number of events (e.g. due to retries). To approximate
|
|
||||||
* this without ruining cardinality by counting user IDs directly, we hash the user ID
|
|
||||||
* and assign it to one of a fixed number of buckets.
|
|
||||||
*
|
|
||||||
* This allows us to estimate the number of unique users by counting unique buckets,
|
|
||||||
* while preserving user privacy and reducing cardinality.
|
|
||||||
*/
|
|
||||||
const getUserBucket = memoize((): number => {
|
|
||||||
const userId = getOrCreateUserID()
|
|
||||||
const hash = createHash('sha256').update(userId).digest('hex')
|
|
||||||
return parseInt(hash.slice(0, 8), 16) % NUM_USER_BUCKETS
|
|
||||||
})
|
|
||||||
|
|
||||||
function getFlushIntervalMs(): number {
|
|
||||||
// Allow tests to override to not block on the default flush interval.
|
|
||||||
return (
|
|
||||||
parseInt(process.env.CLAUDE_CODE_DATADOG_FLUSH_INTERVAL_MS || '', 10) ||
|
|
||||||
DEFAULT_FLUSH_INTERVAL_MS
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,237 +1,41 @@
|
|||||||
import type { AnyValueMap, Logger, logs } from '@opentelemetry/api-logs'
|
|
||||||
import { resourceFromAttributes } from '@opentelemetry/resources'
|
|
||||||
import {
|
|
||||||
BatchLogRecordProcessor,
|
|
||||||
LoggerProvider,
|
|
||||||
} from '@opentelemetry/sdk-logs'
|
|
||||||
import {
|
|
||||||
ATTR_SERVICE_NAME,
|
|
||||||
ATTR_SERVICE_VERSION,
|
|
||||||
} from '@opentelemetry/semantic-conventions'
|
|
||||||
import { randomUUID } from 'crypto'
|
|
||||||
import { isEqual } from 'lodash-es'
|
|
||||||
import { getOrCreateUserID } from '../../utils/config.js'
|
|
||||||
import { logForDebugging } from '../../utils/debug.js'
|
|
||||||
import { logError } from '../../utils/log.js'
|
|
||||||
import { getPlatform, getWslVersion } from '../../utils/platform.js'
|
|
||||||
import { jsonStringify } from '../../utils/slowOperations.js'
|
|
||||||
import { profileCheckpoint } from '../../utils/startupProfiler.js'
|
|
||||||
import { getCoreUserData } from '../../utils/user.js'
|
|
||||||
import { isAnalyticsDisabled } from './config.js'
|
|
||||||
import { FirstPartyEventLoggingExporter } from './firstPartyEventLoggingExporter.js'
|
|
||||||
import type { GrowthBookUserAttributes } from './growthbook.js'
|
|
||||||
import { getDynamicConfig_CACHED_MAY_BE_STALE } from './growthbook.js'
|
|
||||||
import { getEventMetadata } from './metadata.js'
|
|
||||||
import { isSinkKilled } from './sinkKillswitch.js'
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Configuration for sampling individual event types.
|
* Anthropic 1P event logging egress is disabled in this build.
|
||||||
* Each event name maps to an object containing sample_rate (0-1).
|
*
|
||||||
* Events not in the config are logged at 100% rate.
|
* The module keeps its public API so the rest of the app can call into it
|
||||||
|
* without conditional imports.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import type { GrowthBookUserAttributes } from './growthbook.js'
|
||||||
|
|
||||||
export type EventSamplingConfig = {
|
export type EventSamplingConfig = {
|
||||||
[eventName: string]: {
|
[eventName: string]: {
|
||||||
sample_rate: number
|
sample_rate: number
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const EVENT_SAMPLING_CONFIG_NAME = 'tengu_event_sampling_config'
|
|
||||||
/**
|
|
||||||
* Get the event sampling configuration from GrowthBook.
|
|
||||||
* Uses cached value if available, updates cache in background.
|
|
||||||
*/
|
|
||||||
export function getEventSamplingConfig(): EventSamplingConfig {
|
export function getEventSamplingConfig(): EventSamplingConfig {
|
||||||
return getDynamicConfig_CACHED_MAY_BE_STALE<EventSamplingConfig>(
|
return {}
|
||||||
EVENT_SAMPLING_CONFIG_NAME,
|
|
||||||
{},
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
export function shouldSampleEvent(_eventName: string): number | null {
|
||||||
* Determine if an event should be sampled based on its sample rate.
|
|
||||||
* Returns the sample rate if sampled, null if not sampled.
|
|
||||||
*
|
|
||||||
* @param eventName - Name of the event to check
|
|
||||||
* @returns The sample_rate if event should be logged, null if it should be dropped
|
|
||||||
*/
|
|
||||||
export function shouldSampleEvent(eventName: string): number | null {
|
|
||||||
const config = getEventSamplingConfig()
|
|
||||||
const eventConfig = config[eventName]
|
|
||||||
|
|
||||||
// If no config for this event, log at 100% rate (no sampling)
|
|
||||||
if (!eventConfig) {
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
const sampleRate = eventConfig.sample_rate
|
|
||||||
|
|
||||||
// Validate sample rate is in valid range
|
|
||||||
if (typeof sampleRate !== 'number' || sampleRate < 0 || sampleRate > 1) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sample rate of 1 means log everything (no need to add metadata)
|
|
||||||
if (sampleRate >= 1) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sample rate of 0 means drop everything
|
|
||||||
if (sampleRate <= 0) {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// Randomly decide whether to sample this event
|
|
||||||
return Math.random() < sampleRate ? sampleRate : 0
|
|
||||||
}
|
|
||||||
|
|
||||||
const BATCH_CONFIG_NAME = 'tengu_1p_event_batch_config'
|
|
||||||
type BatchConfig = {
|
|
||||||
scheduledDelayMillis?: number
|
|
||||||
maxExportBatchSize?: number
|
|
||||||
maxQueueSize?: number
|
|
||||||
skipAuth?: boolean
|
|
||||||
maxAttempts?: number
|
|
||||||
path?: string
|
|
||||||
baseUrl?: string
|
|
||||||
}
|
|
||||||
function getBatchConfig(): BatchConfig {
|
|
||||||
return getDynamicConfig_CACHED_MAY_BE_STALE<BatchConfig>(
|
|
||||||
BATCH_CONFIG_NAME,
|
|
||||||
{},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Module-local state for event logging (not exposed globally)
|
|
||||||
let firstPartyEventLogger: ReturnType<typeof logs.getLogger> | null = null
|
|
||||||
let firstPartyEventLoggerProvider: LoggerProvider | null = null
|
|
||||||
// Last batch config used to construct the provider — used by
|
|
||||||
// reinitialize1PEventLoggingIfConfigChanged to decide whether a rebuild is
|
|
||||||
// needed when GrowthBook refreshes.
|
|
||||||
let lastBatchConfig: BatchConfig | null = null
|
|
||||||
/**
|
|
||||||
* Flush and shutdown the 1P event logger.
|
|
||||||
* This should be called as the final step before process exit to ensure
|
|
||||||
* all events (including late ones from API responses) are exported.
|
|
||||||
*/
|
|
||||||
export async function shutdown1PEventLogging(): Promise<void> {
|
export async function shutdown1PEventLogging(): Promise<void> {
|
||||||
if (!firstPartyEventLoggerProvider) {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
try {
|
|
||||||
await firstPartyEventLoggerProvider.shutdown()
|
|
||||||
if (process.env.USER_TYPE === 'ant') {
|
|
||||||
logForDebugging('1P event logging: final shutdown complete')
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// Ignore shutdown errors
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if 1P event logging is enabled.
|
|
||||||
* Respects the same opt-outs as other analytics sinks:
|
|
||||||
* - Test environment
|
|
||||||
* - Third-party cloud providers (Bedrock/Vertex)
|
|
||||||
* - Global telemetry opt-outs
|
|
||||||
* - Non-essential traffic disabled
|
|
||||||
*
|
|
||||||
* Note: Unlike BigQuery metrics, event logging does NOT check organization-level
|
|
||||||
* metrics opt-out via API. It follows the same pattern as Statsig event logging.
|
|
||||||
*/
|
|
||||||
export function is1PEventLoggingEnabled(): boolean {
|
export function is1PEventLoggingEnabled(): boolean {
|
||||||
// Respect standard analytics opt-outs
|
return false
|
||||||
return !isAnalyticsDisabled()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Log a 1st-party event for internal analytics (async version).
|
|
||||||
* Events are batched and exported to /api/event_logging/batch
|
|
||||||
*
|
|
||||||
* This enriches the event with core metadata (model, session, env context, etc.)
|
|
||||||
* at log time, similar to logEventToStatsig.
|
|
||||||
*
|
|
||||||
* @param eventName - Name of the event (e.g., 'tengu_api_query')
|
|
||||||
* @param metadata - Additional metadata for the event (intentionally no strings, to avoid accidentally logging code/filepaths)
|
|
||||||
*/
|
|
||||||
async function logEventTo1PAsync(
|
|
||||||
firstPartyEventLogger: Logger,
|
|
||||||
eventName: string,
|
|
||||||
metadata: Record<string, number | boolean | undefined> = {},
|
|
||||||
): Promise<void> {
|
|
||||||
try {
|
|
||||||
// Enrich with core metadata at log time (similar to Statsig pattern)
|
|
||||||
const coreMetadata = await getEventMetadata({
|
|
||||||
model: metadata.model,
|
|
||||||
betas: metadata.betas,
|
|
||||||
})
|
|
||||||
|
|
||||||
// Build attributes - OTel supports nested objects natively via AnyValueMap
|
|
||||||
// Cast through unknown since our nested objects are structurally compatible
|
|
||||||
// with AnyValue but TS doesn't recognize it due to missing index signatures
|
|
||||||
const attributes = {
|
|
||||||
event_name: eventName,
|
|
||||||
event_id: randomUUID(),
|
|
||||||
// Pass objects directly - no JSON serialization needed
|
|
||||||
core_metadata: coreMetadata,
|
|
||||||
user_metadata: getCoreUserData(true),
|
|
||||||
event_metadata: metadata,
|
|
||||||
} as unknown as AnyValueMap
|
|
||||||
|
|
||||||
// Add user_id if available
|
|
||||||
const userId = getOrCreateUserID()
|
|
||||||
if (userId) {
|
|
||||||
attributes.user_id = userId
|
|
||||||
}
|
|
||||||
|
|
||||||
// Debug logging when debug mode is enabled
|
|
||||||
if (process.env.USER_TYPE === 'ant') {
|
|
||||||
logForDebugging(
|
|
||||||
`[ANT-ONLY] 1P event: ${eventName} ${jsonStringify(metadata, null, 0)}`,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Emit log record
|
|
||||||
firstPartyEventLogger.emit({
|
|
||||||
body: eventName,
|
|
||||||
attributes,
|
|
||||||
})
|
|
||||||
} catch (e) {
|
|
||||||
if (process.env.NODE_ENV === 'development') {
|
|
||||||
throw e
|
|
||||||
}
|
|
||||||
if (process.env.USER_TYPE === 'ant') {
|
|
||||||
logError(e as Error)
|
|
||||||
}
|
|
||||||
// swallow
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Log a 1st-party event for internal analytics.
|
|
||||||
* Events are batched and exported to /api/event_logging/batch
|
|
||||||
*
|
|
||||||
* @param eventName - Name of the event (e.g., 'tengu_api_query')
|
|
||||||
* @param metadata - Additional metadata for the event (intentionally no strings, to avoid accidentally logging code/filepaths)
|
|
||||||
*/
|
|
||||||
export function logEventTo1P(
|
export function logEventTo1P(
|
||||||
eventName: string,
|
_eventName: string,
|
||||||
metadata: Record<string, number | boolean | undefined> = {},
|
_metadata: Record<string, number | boolean | undefined> = {},
|
||||||
): void {
|
): void {
|
||||||
if (!is1PEventLoggingEnabled()) {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!firstPartyEventLogger || isSinkKilled('firstParty')) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fire and forget - don't block on metadata enrichment
|
|
||||||
void logEventTo1PAsync(firstPartyEventLogger, eventName, metadata)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* GrowthBook experiment event data for logging
|
|
||||||
*/
|
|
||||||
export type GrowthBookExperimentData = {
|
export type GrowthBookExperimentData = {
|
||||||
experimentId: string
|
experimentId: string
|
||||||
variationId: number
|
variationId: number
|
||||||
@@ -239,211 +43,16 @@ export type GrowthBookExperimentData = {
|
|||||||
experimentMetadata?: Record<string, unknown>
|
experimentMetadata?: Record<string, unknown>
|
||||||
}
|
}
|
||||||
|
|
||||||
// api.anthropic.com only serves the "production" GrowthBook environment
|
|
||||||
// (see starling/starling/cli/cli.py DEFAULT_ENVIRONMENTS). Staging and
|
|
||||||
// development environments are not exported to the prod API.
|
|
||||||
function getEnvironmentForGrowthBook(): string {
|
|
||||||
return 'production'
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Log a GrowthBook experiment assignment event to 1P.
|
|
||||||
* Events are batched and exported to /api/event_logging/batch
|
|
||||||
*
|
|
||||||
* @param data - GrowthBook experiment assignment data
|
|
||||||
*/
|
|
||||||
export function logGrowthBookExperimentTo1P(
|
export function logGrowthBookExperimentTo1P(
|
||||||
data: GrowthBookExperimentData,
|
_data: GrowthBookExperimentData,
|
||||||
): void {
|
): void {
|
||||||
if (!is1PEventLoggingEnabled()) {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!firstPartyEventLogger || isSinkKilled('firstParty')) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const userId = getOrCreateUserID()
|
|
||||||
const { accountUuid, organizationUuid } = getCoreUserData(true)
|
|
||||||
|
|
||||||
// Build attributes for GrowthbookExperimentEvent
|
|
||||||
const attributes = {
|
|
||||||
event_type: 'GrowthbookExperimentEvent',
|
|
||||||
event_id: randomUUID(),
|
|
||||||
experiment_id: data.experimentId,
|
|
||||||
variation_id: data.variationId,
|
|
||||||
...(userId && { device_id: userId }),
|
|
||||||
...(accountUuid && { account_uuid: accountUuid }),
|
|
||||||
...(organizationUuid && { organization_uuid: organizationUuid }),
|
|
||||||
...(data.userAttributes && {
|
|
||||||
session_id: data.userAttributes.sessionId,
|
|
||||||
user_attributes: jsonStringify(data.userAttributes),
|
|
||||||
}),
|
|
||||||
...(data.experimentMetadata && {
|
|
||||||
experiment_metadata: jsonStringify(data.experimentMetadata),
|
|
||||||
}),
|
|
||||||
environment: getEnvironmentForGrowthBook(),
|
|
||||||
}
|
|
||||||
|
|
||||||
if (process.env.USER_TYPE === 'ant') {
|
|
||||||
logForDebugging(
|
|
||||||
`[ANT-ONLY] 1P GrowthBook experiment: ${data.experimentId} variation=${data.variationId}`,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
firstPartyEventLogger.emit({
|
|
||||||
body: 'growthbook_experiment',
|
|
||||||
attributes,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const DEFAULT_LOGS_EXPORT_INTERVAL_MS = 10000
|
|
||||||
const DEFAULT_MAX_EXPORT_BATCH_SIZE = 200
|
|
||||||
const DEFAULT_MAX_QUEUE_SIZE = 8192
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize 1P event logging infrastructure.
|
|
||||||
* This creates a separate LoggerProvider for internal event logging,
|
|
||||||
* independent of customer OTLP telemetry.
|
|
||||||
*
|
|
||||||
* This uses its own minimal resource configuration with just the attributes
|
|
||||||
* we need for internal analytics (service name, version, platform info).
|
|
||||||
*/
|
|
||||||
export function initialize1PEventLogging(): void {
|
export function initialize1PEventLogging(): void {
|
||||||
profileCheckpoint('1p_event_logging_start')
|
|
||||||
const enabled = is1PEventLoggingEnabled()
|
|
||||||
|
|
||||||
if (!enabled) {
|
|
||||||
if (process.env.USER_TYPE === 'ant') {
|
|
||||||
logForDebugging('1P event logging not enabled')
|
|
||||||
}
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch batch processor configuration from GrowthBook dynamic config
|
|
||||||
// Uses cached value if available, refreshes in background
|
|
||||||
const batchConfig = getBatchConfig()
|
|
||||||
lastBatchConfig = batchConfig
|
|
||||||
profileCheckpoint('1p_event_after_growthbook_config')
|
|
||||||
|
|
||||||
const scheduledDelayMillis =
|
|
||||||
batchConfig.scheduledDelayMillis ||
|
|
||||||
parseInt(
|
|
||||||
process.env.OTEL_LOGS_EXPORT_INTERVAL ||
|
|
||||||
DEFAULT_LOGS_EXPORT_INTERVAL_MS.toString(),
|
|
||||||
)
|
|
||||||
|
|
||||||
const maxExportBatchSize =
|
|
||||||
batchConfig.maxExportBatchSize || DEFAULT_MAX_EXPORT_BATCH_SIZE
|
|
||||||
|
|
||||||
const maxQueueSize = batchConfig.maxQueueSize || DEFAULT_MAX_QUEUE_SIZE
|
|
||||||
|
|
||||||
// Build our own resource for 1P event logging with minimal attributes
|
|
||||||
const platform = getPlatform()
|
|
||||||
const attributes: Record<string, string> = {
|
|
||||||
[ATTR_SERVICE_NAME]: 'claude-code',
|
|
||||||
[ATTR_SERVICE_VERSION]: MACRO.VERSION,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add WSL-specific attributes if running on WSL
|
|
||||||
if (platform === 'wsl') {
|
|
||||||
const wslVersion = getWslVersion()
|
|
||||||
if (wslVersion) {
|
|
||||||
attributes['wsl.version'] = wslVersion
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const resource = resourceFromAttributes(attributes)
|
|
||||||
|
|
||||||
// Create a new LoggerProvider with the EventLoggingExporter
|
|
||||||
// NOTE: This is kept separate from customer telemetry logs to ensure
|
|
||||||
// internal events don't leak to customer endpoints and vice versa.
|
|
||||||
// We don't register this globally - it's only used for internal event logging.
|
|
||||||
const eventLoggingExporter = new FirstPartyEventLoggingExporter({
|
|
||||||
maxBatchSize: maxExportBatchSize,
|
|
||||||
skipAuth: batchConfig.skipAuth,
|
|
||||||
maxAttempts: batchConfig.maxAttempts,
|
|
||||||
path: batchConfig.path,
|
|
||||||
baseUrl: batchConfig.baseUrl,
|
|
||||||
isKilled: () => isSinkKilled('firstParty'),
|
|
||||||
})
|
|
||||||
firstPartyEventLoggerProvider = new LoggerProvider({
|
|
||||||
resource,
|
|
||||||
processors: [
|
|
||||||
new BatchLogRecordProcessor(eventLoggingExporter, {
|
|
||||||
scheduledDelayMillis,
|
|
||||||
maxExportBatchSize,
|
|
||||||
maxQueueSize,
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
})
|
|
||||||
|
|
||||||
// Initialize event logger from our internal provider (NOT from global API)
|
|
||||||
// IMPORTANT: We must get the logger from our local provider, not logs.getLogger()
|
|
||||||
// because logs.getLogger() returns a logger from the global provider, which is
|
|
||||||
// separate and used for customer telemetry.
|
|
||||||
firstPartyEventLogger = firstPartyEventLoggerProvider.getLogger(
|
|
||||||
'com.anthropic.claude_code.events',
|
|
||||||
MACRO.VERSION,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Rebuild the 1P event logging pipeline if the batch config changed.
|
|
||||||
* Register this with onGrowthBookRefresh so long-running sessions pick up
|
|
||||||
* changes to batch size, delay, endpoint, etc.
|
|
||||||
*
|
|
||||||
* Event-loss safety:
|
|
||||||
* 1. Null the logger first — concurrent logEventTo1P() calls hit the
|
|
||||||
* !firstPartyEventLogger guard and bail during the swap window. This drops
|
|
||||||
* a handful of events but prevents emitting to a draining provider.
|
|
||||||
* 2. forceFlush() drains the old BatchLogRecordProcessor buffer to the
|
|
||||||
* exporter. Export failures go to disk at getCurrentBatchFilePath() which
|
|
||||||
* is keyed by module-level BATCH_UUID + sessionId — unchanged across
|
|
||||||
* reinit — so the NEW exporter's disk-backed retry picks them up.
|
|
||||||
* 3. Swap to new provider/logger; old provider shutdown runs in background
|
|
||||||
* (buffer already drained, just cleanup).
|
|
||||||
*/
|
|
||||||
export async function reinitialize1PEventLoggingIfConfigChanged(): Promise<void> {
|
export async function reinitialize1PEventLoggingIfConfigChanged(): Promise<void> {
|
||||||
if (!is1PEventLoggingEnabled() || !firstPartyEventLoggerProvider) {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const newConfig = getBatchConfig()
|
|
||||||
|
|
||||||
if (isEqual(newConfig, lastBatchConfig)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (process.env.USER_TYPE === 'ant') {
|
|
||||||
logForDebugging(
|
|
||||||
`1P event logging: ${BATCH_CONFIG_NAME} changed, reinitializing`,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const oldProvider = firstPartyEventLoggerProvider
|
|
||||||
const oldLogger = firstPartyEventLogger
|
|
||||||
firstPartyEventLogger = null
|
|
||||||
|
|
||||||
try {
|
|
||||||
await oldProvider.forceFlush()
|
|
||||||
} catch {
|
|
||||||
// Export failures are already on disk; new exporter will retry them.
|
|
||||||
}
|
|
||||||
|
|
||||||
firstPartyEventLoggerProvider = null
|
|
||||||
try {
|
|
||||||
initialize1PEventLogging()
|
|
||||||
} catch (e) {
|
|
||||||
// Restore so the next GrowthBook refresh can retry. oldProvider was
|
|
||||||
// only forceFlush()'d, not shut down — it's still functional. Without
|
|
||||||
// this, both stay null and the !firstPartyEventLoggerProvider gate at
|
|
||||||
// the top makes recovery impossible.
|
|
||||||
firstPartyEventLoggerProvider = oldProvider
|
|
||||||
firstPartyEventLogger = oldLogger
|
|
||||||
logError(e)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
void oldProvider.shutdown().catch(() => {})
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ import {
|
|||||||
getUserForGrowthBook,
|
getUserForGrowthBook,
|
||||||
} from '../../utils/user.js'
|
} from '../../utils/user.js'
|
||||||
import {
|
import {
|
||||||
is1PEventLoggingEnabled,
|
|
||||||
logGrowthBookExperimentTo1P,
|
logGrowthBookExperimentTo1P,
|
||||||
} from './firstPartyEventLogger.js'
|
} from './firstPartyEventLogger.js'
|
||||||
|
|
||||||
@@ -219,6 +218,19 @@ function getConfigOverrides(): Record<string, unknown> | undefined {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getCachedGrowthBookFeature<T>(feature: string): T | undefined {
|
||||||
|
if (remoteEvalFeatureValues.has(feature)) {
|
||||||
|
return remoteEvalFeatureValues.get(feature) as T
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const cached = getGlobalConfig().cachedGrowthBookFeatures?.[feature]
|
||||||
|
return cached !== undefined ? (cached as T) : undefined
|
||||||
|
} catch {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Enumerate all known GrowthBook features and their current resolved values
|
* Enumerate all known GrowthBook features and their current resolved values
|
||||||
* (not including overrides). In-memory payload first, disk cache fallback —
|
* (not including overrides). In-memory payload first, disk cache fallback —
|
||||||
@@ -420,8 +432,9 @@ function syncRemoteEvalToDisk(): void {
|
|||||||
* Check if GrowthBook operations should be enabled
|
* Check if GrowthBook operations should be enabled
|
||||||
*/
|
*/
|
||||||
function isGrowthBookEnabled(): boolean {
|
function isGrowthBookEnabled(): boolean {
|
||||||
// GrowthBook depends on 1P event logging.
|
// Network-backed GrowthBook egress is disabled in this build. Callers still
|
||||||
return is1PEventLoggingEnabled()
|
// read local cache and explicit overrides through the helpers below.
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -682,6 +695,11 @@ async function getFeatureValueInternal<T>(
|
|||||||
return configOverrides[feature] as T
|
return configOverrides[feature] as T
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const cached = getCachedGrowthBookFeature<T>(feature)
|
||||||
|
if (cached !== undefined) {
|
||||||
|
return cached
|
||||||
|
}
|
||||||
|
|
||||||
if (!isGrowthBookEnabled()) {
|
if (!isGrowthBookEnabled()) {
|
||||||
return defaultValue
|
return defaultValue
|
||||||
}
|
}
|
||||||
@@ -745,6 +763,11 @@ export function getFeatureValue_CACHED_MAY_BE_STALE<T>(
|
|||||||
return configOverrides[feature] as T
|
return configOverrides[feature] as T
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const cached = getCachedGrowthBookFeature<T>(feature)
|
||||||
|
if (cached !== undefined) {
|
||||||
|
return cached
|
||||||
|
}
|
||||||
|
|
||||||
if (!isGrowthBookEnabled()) {
|
if (!isGrowthBookEnabled()) {
|
||||||
return defaultValue
|
return defaultValue
|
||||||
}
|
}
|
||||||
@@ -814,6 +837,16 @@ export function checkStatsigFeatureGate_CACHED_MAY_BE_STALE(
|
|||||||
return Boolean(configOverrides[gate])
|
return Boolean(configOverrides[gate])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const cached = getCachedGrowthBookFeature<boolean>(gate)
|
||||||
|
if (cached !== undefined) {
|
||||||
|
return Boolean(cached)
|
||||||
|
}
|
||||||
|
|
||||||
|
const statsigCached = getGlobalConfig().cachedStatsigGates?.[gate]
|
||||||
|
if (statsigCached !== undefined) {
|
||||||
|
return Boolean(statsigCached)
|
||||||
|
}
|
||||||
|
|
||||||
if (!isGrowthBookEnabled()) {
|
if (!isGrowthBookEnabled()) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -861,6 +894,16 @@ export async function checkSecurityRestrictionGate(
|
|||||||
return Boolean(configOverrides[gate])
|
return Boolean(configOverrides[gate])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const cached = getCachedGrowthBookFeature<boolean>(gate)
|
||||||
|
if (cached !== undefined) {
|
||||||
|
return Boolean(cached)
|
||||||
|
}
|
||||||
|
|
||||||
|
const statsigCached = getGlobalConfig().cachedStatsigGates?.[gate]
|
||||||
|
if (statsigCached !== undefined) {
|
||||||
|
return Boolean(statsigCached)
|
||||||
|
}
|
||||||
|
|
||||||
if (!isGrowthBookEnabled()) {
|
if (!isGrowthBookEnabled()) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -871,19 +914,6 @@ export async function checkSecurityRestrictionGate(
|
|||||||
await reinitializingPromise
|
await reinitializingPromise
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check Statsig cache first - it may have correct value from previous logged-in session
|
|
||||||
const config = getGlobalConfig()
|
|
||||||
const statsigCached = config.cachedStatsigGates?.[gate]
|
|
||||||
if (statsigCached !== undefined) {
|
|
||||||
return Boolean(statsigCached)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Then check GrowthBook cache
|
|
||||||
const gbCached = config.cachedGrowthBookFeatures?.[gate]
|
|
||||||
if (gbCached !== undefined) {
|
|
||||||
return Boolean(gbCached)
|
|
||||||
}
|
|
||||||
|
|
||||||
// No cache - return false (don't block on init for uncached gates)
|
// No cache - return false (don't block on init for uncached gates)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -914,13 +944,23 @@ export async function checkGate_CACHED_OR_BLOCKING(
|
|||||||
return Boolean(configOverrides[gate])
|
return Boolean(configOverrides[gate])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const cached = getCachedGrowthBookFeature<boolean>(gate)
|
||||||
|
if (cached !== undefined) {
|
||||||
|
return Boolean(cached)
|
||||||
|
}
|
||||||
|
|
||||||
|
const statsigCached = getGlobalConfig().cachedStatsigGates?.[gate]
|
||||||
|
if (statsigCached !== undefined) {
|
||||||
|
return Boolean(statsigCached)
|
||||||
|
}
|
||||||
|
|
||||||
if (!isGrowthBookEnabled()) {
|
if (!isGrowthBookEnabled()) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fast path: disk cache already says true — trust it
|
// Fast path: disk cache already says true — trust it
|
||||||
const cached = getGlobalConfig().cachedGrowthBookFeatures?.[gate]
|
const diskCached = getGlobalConfig().cachedGrowthBookFeatures?.[gate]
|
||||||
if (cached === true) {
|
if (diskCached === true) {
|
||||||
// Log experiment exposure if data is available, otherwise defer
|
// Log experiment exposure if data is available, otherwise defer
|
||||||
if (experimentDataByFeature.has(gate)) {
|
if (experimentDataByFeature.has(gate)) {
|
||||||
logExposureForFeature(gate)
|
logExposureForFeature(gate)
|
||||||
|
|||||||
@@ -1,111 +1,32 @@
|
|||||||
/**
|
/**
|
||||||
* Analytics sink implementation
|
* Analytics sink implementation
|
||||||
*
|
*
|
||||||
* This module contains the actual analytics routing logic and should be
|
* This open build keeps the analytics sink boundary for compatibility, but
|
||||||
* initialized during app startup. It routes events to Datadog and 1P event
|
* drops all queued analytics events locally instead of routing them onward.
|
||||||
* logging.
|
|
||||||
*
|
|
||||||
* Usage: Call initializeAnalyticsSink() during app startup to attach the sink.
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { trackDatadogEvent } from './datadog.js'
|
import { attachAnalyticsSink } from './index.js'
|
||||||
import { logEventTo1P, shouldSampleEvent } from './firstPartyEventLogger.js'
|
|
||||||
import { checkStatsigFeatureGate_CACHED_MAY_BE_STALE } from './growthbook.js'
|
|
||||||
import { attachAnalyticsSink, stripProtoFields } from './index.js'
|
|
||||||
import { isSinkKilled } from './sinkKillswitch.js'
|
|
||||||
|
|
||||||
// Local type matching the logEvent metadata signature
|
|
||||||
type LogEventMetadata = { [key: string]: boolean | number | undefined }
|
type LogEventMetadata = { [key: string]: boolean | number | undefined }
|
||||||
|
|
||||||
const DATADOG_GATE_NAME = 'tengu_log_datadog_events'
|
function logEventImpl(
|
||||||
|
_eventName: string,
|
||||||
// Module-level gate state - starts undefined, initialized during startup
|
_metadata: LogEventMetadata,
|
||||||
let isDatadogGateEnabled: boolean | undefined = undefined
|
): void {
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if Datadog tracking is enabled.
|
|
||||||
* Falls back to cached value from previous session if not yet initialized.
|
|
||||||
*/
|
|
||||||
function shouldTrackDatadog(): boolean {
|
|
||||||
if (isSinkKilled('datadog')) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if (isDatadogGateEnabled !== undefined) {
|
|
||||||
return isDatadogGateEnabled
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback to cached value from previous session
|
|
||||||
try {
|
|
||||||
return checkStatsigFeatureGate_CACHED_MAY_BE_STALE(DATADOG_GATE_NAME)
|
|
||||||
} catch {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Log an event (synchronous implementation)
|
|
||||||
*/
|
|
||||||
function logEventImpl(eventName: string, metadata: LogEventMetadata): void {
|
|
||||||
// Check if this event should be sampled
|
|
||||||
const sampleResult = shouldSampleEvent(eventName)
|
|
||||||
|
|
||||||
// If sample result is 0, the event was not selected for logging
|
|
||||||
if (sampleResult === 0) {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// If sample result is a positive number, add it to metadata
|
|
||||||
const metadataWithSampleRate =
|
|
||||||
sampleResult !== null
|
|
||||||
? { ...metadata, sample_rate: sampleResult }
|
|
||||||
: metadata
|
|
||||||
|
|
||||||
if (shouldTrackDatadog()) {
|
|
||||||
// Datadog is a general-access backend — strip _PROTO_* keys
|
|
||||||
// (unredacted PII-tagged values meant only for the 1P privileged column).
|
|
||||||
void trackDatadogEvent(eventName, stripProtoFields(metadataWithSampleRate))
|
|
||||||
}
|
|
||||||
|
|
||||||
// 1P receives the full payload including _PROTO_* — the exporter
|
|
||||||
// destructures and routes those keys to proto fields itself.
|
|
||||||
logEventTo1P(eventName, metadataWithSampleRate)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Log an event (asynchronous implementation)
|
|
||||||
*
|
|
||||||
* With Segment removed the two remaining sinks are fire-and-forget, so this
|
|
||||||
* just wraps the sync impl — kept to preserve the sink interface contract.
|
|
||||||
*/
|
|
||||||
function logEventAsyncImpl(
|
function logEventAsyncImpl(
|
||||||
eventName: string,
|
_eventName: string,
|
||||||
metadata: LogEventMetadata,
|
_metadata: LogEventMetadata,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
logEventImpl(eventName, metadata)
|
|
||||||
return Promise.resolve()
|
return Promise.resolve()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize analytics gates during startup.
|
|
||||||
*
|
|
||||||
* Updates gate values from server. Early events use cached values from previous
|
|
||||||
* session to avoid data loss during initialization.
|
|
||||||
*
|
|
||||||
* Called from main.tsx during setupBackend().
|
|
||||||
*/
|
|
||||||
export function initializeAnalyticsGates(): void {
|
export function initializeAnalyticsGates(): void {
|
||||||
isDatadogGateEnabled =
|
return
|
||||||
checkStatsigFeatureGate_CACHED_MAY_BE_STALE(DATADOG_GATE_NAME)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize the analytics sink.
|
|
||||||
*
|
|
||||||
* Call this during app startup to attach the analytics backend.
|
|
||||||
* Any events logged before this is called will be queued and drained.
|
|
||||||
*
|
|
||||||
* Idempotent: safe to call multiple times (subsequent calls are no-ops).
|
|
||||||
*/
|
|
||||||
export function initializeAnalyticsSink(): void {
|
export function initializeAnalyticsSink(): void {
|
||||||
attachAnalyticsSink({
|
attachAnalyticsSink({
|
||||||
logEvent: logEventImpl,
|
logEvent: logEventImpl,
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import type {
|
|||||||
BetaStopReason,
|
BetaStopReason,
|
||||||
} from '@anthropic-ai/sdk/resources/beta/messages/messages.mjs'
|
} from '@anthropic-ai/sdk/resources/beta/messages/messages.mjs'
|
||||||
import { AFK_MODE_BETA_HEADER } from 'src/constants/betas.js'
|
import { AFK_MODE_BETA_HEADER } from 'src/constants/betas.js'
|
||||||
import type { SDKAssistantMessageError } from 'src/entrypoints/agentSdkTypes.js'
|
import type { SDKAssistantMessageError } from 'src/entrypoints/agentSdkTypes.ts'
|
||||||
import type {
|
import type {
|
||||||
AssistantMessage,
|
AssistantMessage,
|
||||||
Message,
|
Message,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { ToolUseBlock } from '@anthropic-ai/sdk/resources';
|
import type { ToolUseBlock } from '@anthropic-ai/sdk/resources';
|
||||||
import { getRemoteSessionUrl } from '../../constants/product.js';
|
import { getRemoteSessionUrl } from '../../constants/product.js';
|
||||||
import { OUTPUT_FILE_TAG, REMOTE_REVIEW_PROGRESS_TAG, REMOTE_REVIEW_TAG, STATUS_TAG, SUMMARY_TAG, TASK_ID_TAG, TASK_NOTIFICATION_TAG, TASK_TYPE_TAG, TOOL_USE_ID_TAG, ULTRAPLAN_TAG } from '../../constants/xml.js';
|
import { OUTPUT_FILE_TAG, REMOTE_REVIEW_PROGRESS_TAG, REMOTE_REVIEW_TAG, STATUS_TAG, SUMMARY_TAG, TASK_ID_TAG, TASK_NOTIFICATION_TAG, TASK_TYPE_TAG, TOOL_USE_ID_TAG, ULTRAPLAN_TAG } from '../../constants/xml.js';
|
||||||
import type { SDKAssistantMessage, SDKMessage } from '../../entrypoints/agentSdkTypes.js';
|
import type { SDKAssistantMessage, SDKMessage } from '../../entrypoints/agentSdkTypes.ts';
|
||||||
import type { SetAppState, Task, TaskContext, TaskStateBase } from '../../Task.js';
|
import type { SetAppState, Task, TaskContext, TaskStateBase } from '../../Task.js';
|
||||||
import { createTaskStateBase, generateTaskId } from '../../Task.js';
|
import { createTaskStateBase, generateTaskId } from '../../Task.js';
|
||||||
import { TodoWriteTool } from '../../tools/TodoWriteTool/TodoWriteTool.js';
|
import { TodoWriteTool } from '../../tools/TodoWriteTool/TodoWriteTool.js';
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ import { TodoWriteTool } from './tools/TodoWriteTool/TodoWriteTool.js'
|
|||||||
import { ExitPlanModeV2Tool } from './tools/ExitPlanModeTool/ExitPlanModeV2Tool.js'
|
import { ExitPlanModeV2Tool } from './tools/ExitPlanModeTool/ExitPlanModeV2Tool.js'
|
||||||
import { TestingPermissionTool } from './tools/testing/TestingPermissionTool.js'
|
import { TestingPermissionTool } from './tools/testing/TestingPermissionTool.js'
|
||||||
import { GrepTool } from './tools/GrepTool/GrepTool.js'
|
import { GrepTool } from './tools/GrepTool/GrepTool.js'
|
||||||
import { TungstenTool } from './tools/TungstenTool/TungstenTool.js'
|
import { TungstenTool } from './tools/TungstenTool/TungstenTool.ts'
|
||||||
// Lazy require to break circular dependency: tools.ts -> TeamCreateTool/TeamDeleteTool -> ... -> tools.ts
|
// Lazy require to break circular dependency: tools.ts -> TeamCreateTool/TeamDeleteTool -> ... -> tools.ts
|
||||||
/* eslint-disable @typescript-eslint/no-require-imports */
|
/* eslint-disable @typescript-eslint/no-require-imports */
|
||||||
const getTeamCreateTool = () =>
|
const getTeamCreateTool = () =>
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
export { TungstenLiveMonitor } from './TungstenLiveMonitor.ts'
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
export {
|
|
||||||
TUNGSTEN_TOOL_NAME,
|
|
||||||
TungstenTool,
|
|
||||||
clearSessionsWithTungstenUsage,
|
|
||||||
resetInitializationState,
|
|
||||||
} from './TungstenTool.ts'
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export { WORKFLOW_TOOL_NAME } from './constants.ts'
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export { isConnectorTextBlock } from './connectorText.ts'
|
|
||||||
@@ -6,12 +6,12 @@ import {
|
|||||||
HOOK_EVENTS,
|
HOOK_EVENTS,
|
||||||
type HookInput,
|
type HookInput,
|
||||||
type PermissionUpdate,
|
type PermissionUpdate,
|
||||||
} from 'src/entrypoints/agentSdkTypes.js'
|
} from 'src/entrypoints/agentSdkTypes.ts'
|
||||||
import type {
|
import type {
|
||||||
HookJSONOutput,
|
HookJSONOutput,
|
||||||
AsyncHookJSONOutput,
|
AsyncHookJSONOutput,
|
||||||
SyncHookJSONOutput,
|
SyncHookJSONOutput,
|
||||||
} from 'src/entrypoints/agentSdkTypes.js'
|
} from 'src/entrypoints/agentSdkTypes.ts'
|
||||||
import type { Message } from 'src/types/message.js'
|
import type { Message } from 'src/types/message.js'
|
||||||
import type { PermissionResult } from 'src/utils/permissions/PermissionResult.js'
|
import type { PermissionResult } from 'src/utils/permissions/PermissionResult.js'
|
||||||
import { permissionBehaviorSchema } from 'src/utils/permissions/PermissionRule.js'
|
import { permissionBehaviorSchema } from 'src/utils/permissions/PermissionRule.js'
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { ContentBlockParam } from '@anthropic-ai/sdk/resources/messages.mjs'
|
import type { ContentBlockParam } from '@anthropic-ai/sdk/resources/messages.mjs'
|
||||||
import type { UUID } from 'crypto'
|
import type { UUID } from 'crypto'
|
||||||
import type React from 'react'
|
import type React from 'react'
|
||||||
import type { PermissionResult } from '../entrypoints/agentSdkTypes.js'
|
import type { PermissionResult } from '../entrypoints/agentSdkTypes.ts'
|
||||||
import type { Key } from '../ink.js'
|
import type { Key } from '../ink.js'
|
||||||
import type { PastedContent } from '../utils/config.js'
|
import type { PastedContent } from '../utils/config.js'
|
||||||
import type { ImageDimensions } from '../utils/imageResizer.js'
|
import type { ImageDimensions } from '../utils/imageResizer.js'
|
||||||
|
|||||||
106
src/utils/api.ts
106
src/utils/api.ts
@@ -438,41 +438,16 @@ export function appendSystemContext(
|
|||||||
systemPrompt: SystemPrompt,
|
systemPrompt: SystemPrompt,
|
||||||
context: { [k: string]: string },
|
context: { [k: string]: string },
|
||||||
): string[] {
|
): string[] {
|
||||||
return [
|
return systemPrompt
|
||||||
...systemPrompt,
|
|
||||||
Object.entries(context)
|
|
||||||
.map(([key, value]) => `${key}: ${value}`)
|
|
||||||
.join('\n'),
|
|
||||||
].filter(Boolean)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function prependUserContext(
|
export function prependUserContext(
|
||||||
messages: Message[],
|
messages: Message[],
|
||||||
context: { [k: string]: string },
|
context: { [k: string]: string },
|
||||||
): Message[] {
|
): Message[] {
|
||||||
if (process.env.NODE_ENV === 'test') {
|
|
||||||
return messages
|
return messages
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Object.entries(context).length === 0) {
|
|
||||||
return messages
|
|
||||||
}
|
|
||||||
|
|
||||||
return [
|
|
||||||
createUserMessage({
|
|
||||||
content: `<system-reminder>\nAs you answer the user's questions, you can use the following context:\n${Object.entries(
|
|
||||||
context,
|
|
||||||
)
|
|
||||||
.map(([key, value]) => `# ${key}\n${value}`)
|
|
||||||
.join('\n')}
|
|
||||||
|
|
||||||
IMPORTANT: this context may or may not be relevant to your tasks. You should not respond to this context unless it is highly relevant to your task.\n</system-reminder>\n`,
|
|
||||||
isMeta: true,
|
|
||||||
}),
|
|
||||||
...messages,
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Log metrics about context and system prompt size
|
* Log metrics about context and system prompt size
|
||||||
*/
|
*/
|
||||||
@@ -480,87 +455,8 @@ export async function logContextMetrics(
|
|||||||
mcpConfigs: Record<string, ScopedMcpServerConfig>,
|
mcpConfigs: Record<string, ScopedMcpServerConfig>,
|
||||||
toolPermissionContext: ToolPermissionContext,
|
toolPermissionContext: ToolPermissionContext,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
// Early return if logging is disabled
|
|
||||||
if (isAnalyticsDisabled()) {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const [{ tools: mcpTools }, tools, userContext, systemContext] =
|
|
||||||
await Promise.all([
|
|
||||||
prefetchAllMcpResources(mcpConfigs),
|
|
||||||
getTools(toolPermissionContext),
|
|
||||||
getUserContext(),
|
|
||||||
getSystemContext(),
|
|
||||||
])
|
|
||||||
// Extract individual context sizes and calculate total
|
|
||||||
const gitStatusSize = systemContext.gitStatus?.length ?? 0
|
|
||||||
const claudeMdSize = userContext.claudeMd?.length ?? 0
|
|
||||||
|
|
||||||
// Calculate total context size
|
|
||||||
const totalContextSize = gitStatusSize + claudeMdSize
|
|
||||||
|
|
||||||
// Get file count using ripgrep (rounded to nearest power of 10 for privacy)
|
|
||||||
const currentDir = getCwd()
|
|
||||||
const ignorePatternsByRoot = getFileReadIgnorePatterns(toolPermissionContext)
|
|
||||||
const normalizedIgnorePatterns = normalizePatternsToPath(
|
|
||||||
ignorePatternsByRoot,
|
|
||||||
currentDir,
|
|
||||||
)
|
|
||||||
const fileCount = await countFilesRoundedRg(
|
|
||||||
currentDir,
|
|
||||||
AbortSignal.timeout(1000),
|
|
||||||
normalizedIgnorePatterns,
|
|
||||||
)
|
|
||||||
|
|
||||||
// Calculate tool metrics
|
|
||||||
let mcpToolsCount = 0
|
|
||||||
let mcpServersCount = 0
|
|
||||||
let mcpToolsTokens = 0
|
|
||||||
let nonMcpToolsCount = 0
|
|
||||||
let nonMcpToolsTokens = 0
|
|
||||||
|
|
||||||
const nonMcpTools = tools.filter(tool => !tool.isMcp)
|
|
||||||
mcpToolsCount = mcpTools.length
|
|
||||||
nonMcpToolsCount = nonMcpTools.length
|
|
||||||
|
|
||||||
// Extract unique server names from MCP tool names (format: mcp__servername__toolname)
|
|
||||||
const serverNames = new Set<string>()
|
|
||||||
for (const tool of mcpTools) {
|
|
||||||
const parts = tool.name.split('__')
|
|
||||||
if (parts.length >= 3 && parts[1]) {
|
|
||||||
serverNames.add(parts[1])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
mcpServersCount = serverNames.size
|
|
||||||
|
|
||||||
// Estimate tool tokens locally for analytics (avoids N API calls per session)
|
|
||||||
// Use inputJSONSchema (plain JSON Schema) when available, otherwise convert Zod schema
|
|
||||||
for (const tool of mcpTools) {
|
|
||||||
const schema =
|
|
||||||
'inputJSONSchema' in tool && tool.inputJSONSchema
|
|
||||||
? tool.inputJSONSchema
|
|
||||||
: zodToJsonSchema(tool.inputSchema)
|
|
||||||
mcpToolsTokens += roughTokenCountEstimation(jsonStringify(schema))
|
|
||||||
}
|
|
||||||
for (const tool of nonMcpTools) {
|
|
||||||
const schema =
|
|
||||||
'inputJSONSchema' in tool && tool.inputJSONSchema
|
|
||||||
? tool.inputJSONSchema
|
|
||||||
: zodToJsonSchema(tool.inputSchema)
|
|
||||||
nonMcpToolsTokens += roughTokenCountEstimation(jsonStringify(schema))
|
|
||||||
}
|
|
||||||
|
|
||||||
logEvent('tengu_context_size', {
|
|
||||||
git_status_size: gitStatusSize,
|
|
||||||
claude_md_size: claudeMdSize,
|
|
||||||
total_context_size: totalContextSize,
|
|
||||||
project_file_count_rounded: fileCount,
|
|
||||||
mcp_tools_count: mcpToolsCount,
|
|
||||||
mcp_servers_count: mcpServersCount,
|
|
||||||
mcp_tools_tokens: mcpToolsTokens,
|
|
||||||
non_mcp_tools_count: nonMcpToolsCount,
|
|
||||||
non_mcp_tools_tokens: nonMcpToolsTokens,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Generalize this to all tools
|
// TODO: Generalize this to all tools
|
||||||
export function normalizeToolInput<T extends Tool>(
|
export function normalizeToolInput<T extends Tool>(
|
||||||
|
|||||||
@@ -178,7 +178,7 @@ import type { MCPServerConnection } from '../services/mcp/types.js'
|
|||||||
import type {
|
import type {
|
||||||
HookEvent,
|
HookEvent,
|
||||||
SyncHookJSONOutput,
|
SyncHookJSONOutput,
|
||||||
} from 'src/entrypoints/agentSdkTypes.js'
|
} from 'src/entrypoints/agentSdkTypes.ts'
|
||||||
import {
|
import {
|
||||||
checkForAsyncHookResponses,
|
checkForAsyncHookResponses,
|
||||||
removeDeliveredAsyncHooks,
|
removeDeliveredAsyncHooks,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { SDKMessage } from 'src/entrypoints/agentSdkTypes.js'
|
import type { SDKMessage } from 'src/entrypoints/agentSdkTypes.ts'
|
||||||
import { checkGate_CACHED_OR_BLOCKING } from '../../../services/analytics/growthbook.js'
|
import { checkGate_CACHED_OR_BLOCKING } from '../../../services/analytics/growthbook.js'
|
||||||
import { isPolicyAllowed } from '../../../services/policyLimits/index.js'
|
import { isPolicyAllowed } from '../../../services/policyLimits/index.js'
|
||||||
import { detectCurrentRepositoryWithHost } from '../../detectRepository.js'
|
import { detectCurrentRepositoryWithHost } from '../../detectRepository.js'
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import chalk from 'chalk'
|
|||||||
import { writeSync } from 'fs'
|
import { writeSync } from 'fs'
|
||||||
import memoize from 'lodash-es/memoize.js'
|
import memoize from 'lodash-es/memoize.js'
|
||||||
import { onExit } from 'signal-exit'
|
import { onExit } from 'signal-exit'
|
||||||
import type { ExitReason } from 'src/entrypoints/agentSdkTypes.js'
|
import type { ExitReason } from 'src/entrypoints/agentSdkTypes.ts'
|
||||||
import {
|
import {
|
||||||
getIsInteractive,
|
getIsInteractive,
|
||||||
getIsScrollDraining,
|
getIsScrollDraining,
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ import type {
|
|||||||
ExitReason,
|
ExitReason,
|
||||||
SyncHookJSONOutput,
|
SyncHookJSONOutput,
|
||||||
AsyncHookJSONOutput,
|
AsyncHookJSONOutput,
|
||||||
} from 'src/entrypoints/agentSdkTypes.js'
|
} from 'src/entrypoints/agentSdkTypes.ts'
|
||||||
import type { StatusLineCommandInput } from '../types/statusLine.js'
|
import type { StatusLineCommandInput } from '../types/statusLine.js'
|
||||||
import type { ElicitResult } from '@modelcontextprotocol/sdk/types.js'
|
import type { ElicitResult } from '@modelcontextprotocol/sdk/types.js'
|
||||||
import type { FileSuggestionCommandInput } from '../types/fileSuggestion.js'
|
import type { FileSuggestionCommandInput } from '../types/fileSuggestion.js'
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import type {
|
|||||||
AsyncHookJSONOutput,
|
AsyncHookJSONOutput,
|
||||||
HookEvent,
|
HookEvent,
|
||||||
SyncHookJSONOutput,
|
SyncHookJSONOutput,
|
||||||
} from 'src/entrypoints/agentSdkTypes.js'
|
} from 'src/entrypoints/agentSdkTypes.ts'
|
||||||
import { logForDebugging } from '../debug.js'
|
import { logForDebugging } from '../debug.js'
|
||||||
import type { ShellCommand } from '../ShellCommand.js'
|
import type { ShellCommand } from '../ShellCommand.js'
|
||||||
import { invalidateSessionEnvCache } from '../sessionEnvironment.js'
|
import { invalidateSessionEnvCache } from '../sessionEnvironment.js'
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { randomUUID } from 'crypto'
|
import { randomUUID } from 'crypto'
|
||||||
import type { HookEvent } from 'src/entrypoints/agentSdkTypes.js'
|
import type { HookEvent } from 'src/entrypoints/agentSdkTypes.ts'
|
||||||
import { query } from '../../query.js'
|
import { query } from '../../query.js'
|
||||||
import { logEvent } from '../../services/analytics/index.js'
|
import { logEvent } from '../../services/analytics/index.js'
|
||||||
import type { AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS } from '../../services/analytics/metadata.js'
|
import type { AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS } from '../../services/analytics/metadata.js'
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import type { HookEvent } from 'src/entrypoints/agentSdkTypes.js'
|
import type { HookEvent } from 'src/entrypoints/agentSdkTypes.ts'
|
||||||
import { createCombinedAbortSignal } from '../combinedAbortSignal.js'
|
import { createCombinedAbortSignal } from '../combinedAbortSignal.js'
|
||||||
import { logForDebugging } from '../debug.js'
|
import { logForDebugging } from '../debug.js'
|
||||||
import { errorMessage } from '../errors.js'
|
import { errorMessage } from '../errors.js'
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { randomUUID } from 'crypto'
|
import { randomUUID } from 'crypto'
|
||||||
import type { HookEvent } from 'src/entrypoints/agentSdkTypes.js'
|
import type { HookEvent } from 'src/entrypoints/agentSdkTypes.ts'
|
||||||
import { queryModelWithoutStreaming } from '../../services/api/claude.js'
|
import { queryModelWithoutStreaming } from '../../services/api/claude.js'
|
||||||
import type { ToolUseContext } from '../../Tool.js'
|
import type { ToolUseContext } from '../../Tool.js'
|
||||||
import type { Message } from '../../types/message.js'
|
import type { Message } from '../../types/message.js'
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import memoize from 'lodash-es/memoize.js'
|
import memoize from 'lodash-es/memoize.js'
|
||||||
import type { HookEvent } from 'src/entrypoints/agentSdkTypes.js'
|
import type { HookEvent } from 'src/entrypoints/agentSdkTypes.ts'
|
||||||
import { getRegisteredHooks } from '../../bootstrap/state.js'
|
import { getRegisteredHooks } from '../../bootstrap/state.js'
|
||||||
import type { AppState } from '../../state/AppState.js'
|
import type { AppState } from '../../state/AppState.js'
|
||||||
import {
|
import {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { resolve } from 'path'
|
import { resolve } from 'path'
|
||||||
import type { HookEvent } from 'src/entrypoints/agentSdkTypes.js'
|
import type { HookEvent } from 'src/entrypoints/agentSdkTypes.ts'
|
||||||
import { getSessionId } from '../../bootstrap/state.js'
|
import { getSessionId } from '../../bootstrap/state.js'
|
||||||
import type { AppState } from '../../state/AppState.js'
|
import type { AppState } from '../../state/AppState.js'
|
||||||
import type { EditableSettingSource } from '../settings/constants.js'
|
import type { EditableSettingSource } from '../settings/constants.js'
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { HOOK_EVENTS, type HookEvent } from 'src/entrypoints/agentSdkTypes.js'
|
import { HOOK_EVENTS, type HookEvent } from 'src/entrypoints/agentSdkTypes.ts'
|
||||||
import type { AppState } from 'src/state/AppState.js'
|
import type { AppState } from 'src/state/AppState.js'
|
||||||
import { logForDebugging } from '../debug.js'
|
import { logForDebugging } from '../debug.js'
|
||||||
import type { HooksSettings } from '../settings/types.js'
|
import type { HooksSettings } from '../settings/types.js'
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { HOOK_EVENTS } from 'src/entrypoints/agentSdkTypes.js'
|
import { HOOK_EVENTS } from 'src/entrypoints/agentSdkTypes.ts'
|
||||||
import type { AppState } from 'src/state/AppState.js'
|
import type { AppState } from 'src/state/AppState.js'
|
||||||
import { logForDebugging } from '../debug.js'
|
import { logForDebugging } from '../debug.js'
|
||||||
import type { HooksSettings } from '../settings/types.js'
|
import type { HooksSettings } from '../settings/types.js'
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { HOOK_EVENTS, type HookEvent } from 'src/entrypoints/agentSdkTypes.js'
|
import { HOOK_EVENTS, type HookEvent } from 'src/entrypoints/agentSdkTypes.ts'
|
||||||
import type { AppState } from 'src/state/AppState.js'
|
import type { AppState } from 'src/state/AppState.js'
|
||||||
import type { Message } from 'src/types/message.js'
|
import type { Message } from 'src/types/message.js'
|
||||||
import { logForDebugging } from '../debug.js'
|
import { logForDebugging } from '../debug.js'
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ import type {
|
|||||||
import type {
|
import type {
|
||||||
HookEvent,
|
HookEvent,
|
||||||
SDKAssistantMessageError,
|
SDKAssistantMessageError,
|
||||||
} from 'src/entrypoints/agentSdkTypes.js'
|
} from 'src/entrypoints/agentSdkTypes.ts'
|
||||||
import { EXPLORE_AGENT } from 'src/tools/AgentTool/built-in/exploreAgent.js'
|
import { EXPLORE_AGENT } from 'src/tools/AgentTool/built-in/exploreAgent.js'
|
||||||
import { PLAN_AGENT } from 'src/tools/AgentTool/built-in/planAgent.js'
|
import { PLAN_AGENT } from 'src/tools/AgentTool/built-in/planAgent.js'
|
||||||
import { areExplorePlanAgentsEnabled } from 'src/tools/AgentTool/builtInAgents.js'
|
import { areExplorePlanAgentsEnabled } from 'src/tools/AgentTool/builtInAgents.js'
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import type {
|
|||||||
SDKCompactBoundaryMessage,
|
SDKCompactBoundaryMessage,
|
||||||
SDKMessage,
|
SDKMessage,
|
||||||
SDKRateLimitInfo,
|
SDKRateLimitInfo,
|
||||||
} from 'src/entrypoints/agentSdkTypes.js'
|
} from 'src/entrypoints/agentSdkTypes.ts'
|
||||||
import type { ClaudeAILimits } from 'src/services/claudeAiLimits.js'
|
import type { ClaudeAILimits } from 'src/services/claudeAiLimits.js'
|
||||||
import { EXIT_PLAN_MODE_V2_TOOL_NAME } from 'src/tools/ExitPlanModeTool/constants.js'
|
import { EXIT_PLAN_MODE_V2_TOOL_NAME } from 'src/tools/ExitPlanModeTool/constants.js'
|
||||||
import type {
|
import type {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import type {
|
|||||||
ApiKeySource,
|
ApiKeySource,
|
||||||
PermissionMode,
|
PermissionMode,
|
||||||
SDKMessage,
|
SDKMessage,
|
||||||
} from 'src/entrypoints/agentSdkTypes.js'
|
} from 'src/entrypoints/agentSdkTypes.ts'
|
||||||
import {
|
import {
|
||||||
AGENT_TOOL_NAME,
|
AGENT_TOOL_NAME,
|
||||||
LEGACY_AGENT_TOOL_NAME,
|
LEGACY_AGENT_TOOL_NAME,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import memoize from 'lodash-es/memoize.js'
|
import memoize from 'lodash-es/memoize.js'
|
||||||
import type { HookEvent } from 'src/entrypoints/agentSdkTypes.js'
|
import type { HookEvent } from 'src/entrypoints/agentSdkTypes.ts'
|
||||||
import {
|
import {
|
||||||
clearRegisteredPluginHooks,
|
clearRegisteredPluginHooks,
|
||||||
getRegisteredHooks,
|
getRegisteredHooks,
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import {
|
|||||||
getSessionId,
|
getSessionId,
|
||||||
isSessionPersistenceDisabled,
|
isSessionPersistenceDisabled,
|
||||||
} from 'src/bootstrap/state.js'
|
} from 'src/bootstrap/state.js'
|
||||||
import type { SDKMessage } from 'src/entrypoints/agentSdkTypes.js'
|
import type { SDKMessage } from 'src/entrypoints/agentSdkTypes.ts'
|
||||||
import type { CanUseToolFn } from '../hooks/useCanUseTool.js'
|
import type { CanUseToolFn } from '../hooks/useCanUseTool.js'
|
||||||
import { runTools } from '../services/tools/toolOrchestration.js'
|
import { runTools } from '../services/tools/toolOrchestration.js'
|
||||||
import { findToolByName, type Tool, type Tools } from '../Tool.js'
|
import { findToolByName, type Tool, type Tools } from '../Tool.js'
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
*/
|
*/
|
||||||
import { feature } from 'bun:bundle'
|
import { feature } from 'bun:bundle'
|
||||||
import { registerHookCallbacks } from '../bootstrap/state.js'
|
import { registerHookCallbacks } from '../bootstrap/state.js'
|
||||||
import type { HookInput, HookJSONOutput } from '../entrypoints/agentSdkTypes.js'
|
import type { HookInput, HookJSONOutput } from '../entrypoints/agentSdkTypes.ts'
|
||||||
import {
|
import {
|
||||||
type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||||
logEvent,
|
logEvent,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { feature } from 'bun:bundle'
|
import { feature } from 'bun:bundle'
|
||||||
import { open } from 'fs/promises'
|
import { open } from 'fs/promises'
|
||||||
import { basename, dirname, join, sep } from 'path'
|
import { basename, dirname, join, sep } from 'path'
|
||||||
import type { ModelUsage } from 'src/entrypoints/agentSdkTypes.js'
|
import type { ModelUsage } from 'src/entrypoints/agentSdkTypes.ts'
|
||||||
import type { Entry, TranscriptMessage } from '../types/logs.js'
|
import type { Entry, TranscriptMessage } from '../types/logs.js'
|
||||||
import { logForDebugging } from './debug.js'
|
import { logForDebugging } from './debug.js'
|
||||||
import { errorMessage, isENOENT } from './errors.js'
|
import { errorMessage, isENOENT } from './errors.js'
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { feature } from 'bun:bundle'
|
|||||||
import { randomBytes } from 'crypto'
|
import { randomBytes } from 'crypto'
|
||||||
import { open } from 'fs/promises'
|
import { open } from 'fs/promises'
|
||||||
import { join } from 'path'
|
import { join } from 'path'
|
||||||
import type { ModelUsage } from '../entrypoints/agentSdkTypes.js'
|
import type { ModelUsage } from '../entrypoints/agentSdkTypes.ts'
|
||||||
import { logForDebugging } from './debug.js'
|
import { logForDebugging } from './debug.js'
|
||||||
import { getClaudeConfigHomeDir } from './envUtils.js'
|
import { getClaudeConfigHomeDir } from './envUtils.js'
|
||||||
import { errorMessage } from './errors.js'
|
import { errorMessage } from './errors.js'
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
* - Strips tool list and model info from init messages
|
* - Strips tool list and model info from init messages
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { SDKAssistantMessage } from 'src/entrypoints/agentSdkTypes.js'
|
import type { SDKAssistantMessage } from 'src/entrypoints/agentSdkTypes.ts'
|
||||||
import type { StdoutMessage } from 'src/entrypoints/sdk/controlTypes.ts'
|
import type { StdoutMessage } from 'src/entrypoints/sdk/controlTypes.ts'
|
||||||
import { FILE_EDIT_TOOL_NAME } from 'src/tools/FileEditTool/constants.js'
|
import { FILE_EDIT_TOOL_NAME } from 'src/tools/FileEditTool/constants.js'
|
||||||
import { FILE_READ_TOOL_NAME } from 'src/tools/FileReadTool/prompt.js'
|
import { FILE_READ_TOOL_NAME } from 'src/tools/FileReadTool/prompt.js'
|
||||||
|
|||||||
@@ -1,75 +1,14 @@
|
|||||||
import type { Attributes } from '@opentelemetry/api'
|
/**
|
||||||
import { getEventLogger, getPromptId } from 'src/bootstrap/state.js'
|
* OpenTelemetry event egress is disabled in this build.
|
||||||
import { logForDebugging } from '../debug.js'
|
*/
|
||||||
import { isEnvTruthy } from '../envUtils.js'
|
|
||||||
import { getTelemetryAttributes } from '../telemetryAttributes.js'
|
|
||||||
|
|
||||||
// Monotonically increasing counter for ordering events within a session
|
export function redactIfDisabled(_content: string): string {
|
||||||
let eventSequence = 0
|
return '<REDACTED>'
|
||||||
|
|
||||||
// Track whether we've already warned about a null event logger to avoid spamming
|
|
||||||
let hasWarnedNoEventLogger = false
|
|
||||||
|
|
||||||
function isUserPromptLoggingEnabled() {
|
|
||||||
return isEnvTruthy(process.env.OTEL_LOG_USER_PROMPTS)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function redactIfDisabled(content: string): string {
|
|
||||||
return isUserPromptLoggingEnabled() ? content : '<REDACTED>'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function logOTelEvent(
|
export async function logOTelEvent(
|
||||||
eventName: string,
|
_eventName: string,
|
||||||
metadata: { [key: string]: string | undefined } = {},
|
_metadata: { [key: string]: string | undefined } = {},
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const eventLogger = getEventLogger()
|
|
||||||
if (!eventLogger) {
|
|
||||||
if (!hasWarnedNoEventLogger) {
|
|
||||||
hasWarnedNoEventLogger = true
|
|
||||||
logForDebugging(
|
|
||||||
`[3P telemetry] Event dropped (no event logger initialized): ${eventName}`,
|
|
||||||
{ level: 'warn' },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Skip logging in test environment
|
|
||||||
if (process.env.NODE_ENV === 'test') {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const attributes: Attributes = {
|
|
||||||
...getTelemetryAttributes(),
|
|
||||||
'event.name': eventName,
|
|
||||||
'event.timestamp': new Date().toISOString(),
|
|
||||||
'event.sequence': eventSequence++,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add prompt ID to events (but not metrics, where it would cause unbounded cardinality)
|
|
||||||
const promptId = getPromptId()
|
|
||||||
if (promptId) {
|
|
||||||
attributes['prompt.id'] = promptId
|
|
||||||
}
|
|
||||||
|
|
||||||
// Workspace directory from the desktop app (host path). Events only —
|
|
||||||
// filesystem paths are too high-cardinality for metric dimensions, and
|
|
||||||
// the BQ metrics pipeline must never see them.
|
|
||||||
const workspaceDir = process.env.CLAUDE_CODE_WORKSPACE_HOST_PATHS
|
|
||||||
if (workspaceDir) {
|
|
||||||
attributes['workspace.host_paths'] = workspaceDir.split('|')
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add metadata as attributes - all values are already strings
|
|
||||||
for (const [key, value] of Object.entries(metadata)) {
|
|
||||||
if (value !== undefined) {
|
|
||||||
attributes[key] = value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Emit log record as an event
|
|
||||||
eventLogger.emit({
|
|
||||||
body: `claude_code.${eventName}`,
|
|
||||||
attributes,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,123 +1,5 @@
|
|||||||
import { DiagLogLevel, diag, trace } from '@opentelemetry/api'
|
export function bootstrapTelemetry(): void {}
|
||||||
import { logs } from '@opentelemetry/api-logs'
|
|
||||||
// OTLP/Prometheus exporters are dynamically imported inside the protocol
|
|
||||||
// switch statements below. A process uses at most one protocol variant per
|
|
||||||
// signal, but static imports would load all 6 (~1.2MB) on every startup.
|
|
||||||
import {
|
|
||||||
envDetector,
|
|
||||||
hostDetector,
|
|
||||||
osDetector,
|
|
||||||
resourceFromAttributes,
|
|
||||||
} from '@opentelemetry/resources'
|
|
||||||
import {
|
|
||||||
BatchLogRecordProcessor,
|
|
||||||
ConsoleLogRecordExporter,
|
|
||||||
LoggerProvider,
|
|
||||||
} from '@opentelemetry/sdk-logs'
|
|
||||||
import {
|
|
||||||
ConsoleMetricExporter,
|
|
||||||
MeterProvider,
|
|
||||||
PeriodicExportingMetricReader,
|
|
||||||
} from '@opentelemetry/sdk-metrics'
|
|
||||||
import {
|
|
||||||
BasicTracerProvider,
|
|
||||||
BatchSpanProcessor,
|
|
||||||
ConsoleSpanExporter,
|
|
||||||
} from '@opentelemetry/sdk-trace-base'
|
|
||||||
import {
|
|
||||||
ATTR_SERVICE_NAME,
|
|
||||||
ATTR_SERVICE_VERSION,
|
|
||||||
SEMRESATTRS_HOST_ARCH,
|
|
||||||
} from '@opentelemetry/semantic-conventions'
|
|
||||||
import { HttpsProxyAgent } from 'https-proxy-agent'
|
|
||||||
import {
|
|
||||||
getLoggerProvider,
|
|
||||||
getMeterProvider,
|
|
||||||
getTracerProvider,
|
|
||||||
setEventLogger,
|
|
||||||
setLoggerProvider,
|
|
||||||
setMeterProvider,
|
|
||||||
setTracerProvider,
|
|
||||||
} from 'src/bootstrap/state.js'
|
|
||||||
import {
|
|
||||||
getOtelHeadersFromHelper,
|
|
||||||
getSubscriptionType,
|
|
||||||
is1PApiCustomer,
|
|
||||||
isClaudeAISubscriber,
|
|
||||||
} from 'src/utils/auth.js'
|
|
||||||
import { getPlatform, getWslVersion } from 'src/utils/platform.js'
|
|
||||||
|
|
||||||
import { getCACertificates } from '../caCerts.js'
|
|
||||||
import { registerCleanup } from '../cleanupRegistry.js'
|
|
||||||
import { getHasFormattedOutput, logForDebugging } from '../debug.js'
|
|
||||||
import { isEnvTruthy } from '../envUtils.js'
|
|
||||||
import { errorMessage } from '../errors.js'
|
|
||||||
import { getMTLSConfig } from '../mtls.js'
|
|
||||||
import { getProxyUrl, shouldBypassProxy } from '../proxy.js'
|
|
||||||
import { getSettings_DEPRECATED } from '../settings/settings.js'
|
|
||||||
import { jsonStringify } from '../slowOperations.js'
|
|
||||||
import { profileCheckpoint } from '../startupProfiler.js'
|
|
||||||
import { isBetaTracingEnabled } from './betaSessionTracing.js'
|
|
||||||
import { BigQueryMetricsExporter } from './bigqueryExporter.js'
|
|
||||||
import { ClaudeCodeDiagLogger } from './logger.js'
|
|
||||||
import { initializePerfettoTracing } from './perfettoTracing.js'
|
|
||||||
import {
|
|
||||||
endInteractionSpan,
|
|
||||||
isEnhancedTelemetryEnabled,
|
|
||||||
} from './sessionTracing.js'
|
|
||||||
|
|
||||||
const DEFAULT_METRICS_EXPORT_INTERVAL_MS = 60000
|
|
||||||
const DEFAULT_LOGS_EXPORT_INTERVAL_MS = 5000
|
|
||||||
const DEFAULT_TRACES_EXPORT_INTERVAL_MS = 5000
|
|
||||||
|
|
||||||
class TelemetryTimeoutError extends Error {}
|
|
||||||
|
|
||||||
function telemetryTimeout(ms: number, message: string): Promise<never> {
|
|
||||||
return new Promise((_, reject) => {
|
|
||||||
setTimeout(
|
|
||||||
(rej: (e: Error) => void, msg: string) =>
|
|
||||||
rej(new TelemetryTimeoutError(msg)),
|
|
||||||
ms,
|
|
||||||
reject,
|
|
||||||
message,
|
|
||||||
).unref()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export function bootstrapTelemetry() {
|
|
||||||
if (process.env.USER_TYPE === 'ant') {
|
|
||||||
// Read from ANT_ prefixed variables that are defined at build time
|
|
||||||
if (process.env.ANT_OTEL_METRICS_EXPORTER) {
|
|
||||||
process.env.OTEL_METRICS_EXPORTER = process.env.ANT_OTEL_METRICS_EXPORTER
|
|
||||||
}
|
|
||||||
if (process.env.ANT_OTEL_LOGS_EXPORTER) {
|
|
||||||
process.env.OTEL_LOGS_EXPORTER = process.env.ANT_OTEL_LOGS_EXPORTER
|
|
||||||
}
|
|
||||||
if (process.env.ANT_OTEL_TRACES_EXPORTER) {
|
|
||||||
process.env.OTEL_TRACES_EXPORTER = process.env.ANT_OTEL_TRACES_EXPORTER
|
|
||||||
}
|
|
||||||
if (process.env.ANT_OTEL_EXPORTER_OTLP_PROTOCOL) {
|
|
||||||
process.env.OTEL_EXPORTER_OTLP_PROTOCOL =
|
|
||||||
process.env.ANT_OTEL_EXPORTER_OTLP_PROTOCOL
|
|
||||||
}
|
|
||||||
if (process.env.ANT_OTEL_EXPORTER_OTLP_ENDPOINT) {
|
|
||||||
process.env.OTEL_EXPORTER_OTLP_ENDPOINT =
|
|
||||||
process.env.ANT_OTEL_EXPORTER_OTLP_ENDPOINT
|
|
||||||
}
|
|
||||||
if (process.env.ANT_OTEL_EXPORTER_OTLP_HEADERS) {
|
|
||||||
process.env.OTEL_EXPORTER_OTLP_HEADERS =
|
|
||||||
process.env.ANT_OTEL_EXPORTER_OTLP_HEADERS
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set default tempoality to 'delta' because it's the more sane default
|
|
||||||
if (!process.env.OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE) {
|
|
||||||
process.env.OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE = 'delta'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Per OTEL spec, "none" means "no automatically configured exporter for this signal".
|
|
||||||
// https://opentelemetry.io/docs/specs/otel/configuration/sdk-environment-variables/#exporter-selection
|
|
||||||
export function parseExporterTypes(value: string | undefined): string[] {
|
export function parseExporterTypes(value: string | undefined): string[] {
|
||||||
return (value || '')
|
return (value || '')
|
||||||
.trim()
|
.trim()
|
||||||
@@ -127,699 +9,14 @@ export function parseExporterTypes(value: string | undefined): string[] {
|
|||||||
.filter(t => t !== 'none')
|
.filter(t => t !== 'none')
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getOtlpReaders() {
|
export function isTelemetryEnabled(): boolean {
|
||||||
const exporterTypes = parseExporterTypes(process.env.OTEL_METRICS_EXPORTER)
|
return false
|
||||||
const exportInterval = parseInt(
|
|
||||||
process.env.OTEL_METRIC_EXPORT_INTERVAL ||
|
|
||||||
DEFAULT_METRICS_EXPORT_INTERVAL_MS.toString(),
|
|
||||||
)
|
|
||||||
|
|
||||||
const exporters = []
|
|
||||||
for (const exporterType of exporterTypes) {
|
|
||||||
if (exporterType === 'console') {
|
|
||||||
// Custom console exporter that shows resource attributes
|
|
||||||
const consoleExporter = new ConsoleMetricExporter()
|
|
||||||
const originalExport = consoleExporter.export.bind(consoleExporter)
|
|
||||||
|
|
||||||
consoleExporter.export = (metrics, callback) => {
|
|
||||||
// Log resource attributes once at the start
|
|
||||||
if (metrics.resource && metrics.resource.attributes) {
|
|
||||||
// The console exporter is for debugging, so console output is intentional here
|
|
||||||
|
|
||||||
logForDebugging('\n=== Resource Attributes ===')
|
|
||||||
logForDebugging(jsonStringify(metrics.resource.attributes))
|
|
||||||
logForDebugging('===========================\n')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return originalExport(metrics, callback)
|
export async function initializeTelemetry(): Promise<null> {
|
||||||
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
exporters.push(consoleExporter)
|
|
||||||
} else if (exporterType === 'otlp') {
|
|
||||||
const protocol =
|
|
||||||
process.env.OTEL_EXPORTER_OTLP_METRICS_PROTOCOL?.trim() ||
|
|
||||||
process.env.OTEL_EXPORTER_OTLP_PROTOCOL?.trim()
|
|
||||||
|
|
||||||
const httpConfig = getOTLPExporterConfig()
|
|
||||||
|
|
||||||
switch (protocol) {
|
|
||||||
case 'grpc': {
|
|
||||||
// Lazy-import to keep @grpc/grpc-js (~700KB) out of the telemetry chunk
|
|
||||||
// when the protocol is http/protobuf (ant default) or http/json.
|
|
||||||
const { OTLPMetricExporter } = await import(
|
|
||||||
'@opentelemetry/exporter-metrics-otlp-grpc'
|
|
||||||
)
|
|
||||||
exporters.push(new OTLPMetricExporter())
|
|
||||||
break
|
|
||||||
}
|
|
||||||
case 'http/json': {
|
|
||||||
const { OTLPMetricExporter } = await import(
|
|
||||||
'@opentelemetry/exporter-metrics-otlp-http'
|
|
||||||
)
|
|
||||||
exporters.push(new OTLPMetricExporter(httpConfig))
|
|
||||||
break
|
|
||||||
}
|
|
||||||
case 'http/protobuf': {
|
|
||||||
const { OTLPMetricExporter } = await import(
|
|
||||||
'@opentelemetry/exporter-metrics-otlp-proto'
|
|
||||||
)
|
|
||||||
exporters.push(new OTLPMetricExporter(httpConfig))
|
|
||||||
break
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
throw new Error(
|
|
||||||
`Unknown protocol set in OTEL_EXPORTER_OTLP_METRICS_PROTOCOL or OTEL_EXPORTER_OTLP_PROTOCOL env var: ${protocol}`,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else if (exporterType === 'prometheus') {
|
|
||||||
const { PrometheusExporter } = await import(
|
|
||||||
'@opentelemetry/exporter-prometheus'
|
|
||||||
)
|
|
||||||
exporters.push(new PrometheusExporter())
|
|
||||||
} else {
|
|
||||||
throw new Error(
|
|
||||||
`Unknown exporter type set in OTEL_EXPORTER_OTLP_METRICS_PROTOCOL or OTEL_EXPORTER_OTLP_PROTOCOL env var: ${exporterType}`,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return exporters.map(exporter => {
|
|
||||||
if ('export' in exporter) {
|
|
||||||
return new PeriodicExportingMetricReader({
|
|
||||||
exporter,
|
|
||||||
exportIntervalMillis: exportInterval,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return exporter
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getOtlpLogExporters() {
|
|
||||||
const exporterTypes = parseExporterTypes(process.env.OTEL_LOGS_EXPORTER)
|
|
||||||
|
|
||||||
const protocol =
|
|
||||||
process.env.OTEL_EXPORTER_OTLP_LOGS_PROTOCOL?.trim() ||
|
|
||||||
process.env.OTEL_EXPORTER_OTLP_PROTOCOL?.trim()
|
|
||||||
const endpoint = process.env.OTEL_EXPORTER_OTLP_ENDPOINT
|
|
||||||
|
|
||||||
logForDebugging(
|
|
||||||
`[3P telemetry] getOtlpLogExporters: types=${jsonStringify(exporterTypes)}, protocol=${protocol}, endpoint=${endpoint}`,
|
|
||||||
)
|
|
||||||
|
|
||||||
const exporters = []
|
|
||||||
for (const exporterType of exporterTypes) {
|
|
||||||
if (exporterType === 'console') {
|
|
||||||
exporters.push(new ConsoleLogRecordExporter())
|
|
||||||
} else if (exporterType === 'otlp') {
|
|
||||||
const httpConfig = getOTLPExporterConfig()
|
|
||||||
|
|
||||||
switch (protocol) {
|
|
||||||
case 'grpc': {
|
|
||||||
const { OTLPLogExporter } = await import(
|
|
||||||
'@opentelemetry/exporter-logs-otlp-grpc'
|
|
||||||
)
|
|
||||||
exporters.push(new OTLPLogExporter())
|
|
||||||
break
|
|
||||||
}
|
|
||||||
case 'http/json': {
|
|
||||||
const { OTLPLogExporter } = await import(
|
|
||||||
'@opentelemetry/exporter-logs-otlp-http'
|
|
||||||
)
|
|
||||||
exporters.push(new OTLPLogExporter(httpConfig))
|
|
||||||
break
|
|
||||||
}
|
|
||||||
case 'http/protobuf': {
|
|
||||||
const { OTLPLogExporter } = await import(
|
|
||||||
'@opentelemetry/exporter-logs-otlp-proto'
|
|
||||||
)
|
|
||||||
exporters.push(new OTLPLogExporter(httpConfig))
|
|
||||||
break
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
throw new Error(
|
|
||||||
`Unknown protocol set in OTEL_EXPORTER_OTLP_LOGS_PROTOCOL or OTEL_EXPORTER_OTLP_PROTOCOL env var: ${protocol}`,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
throw new Error(
|
|
||||||
`Unknown exporter type set in OTEL_LOGS_EXPORTER env var: ${exporterType}`,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return exporters
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getOtlpTraceExporters() {
|
|
||||||
const exporterTypes = parseExporterTypes(process.env.OTEL_TRACES_EXPORTER)
|
|
||||||
|
|
||||||
const exporters = []
|
|
||||||
for (const exporterType of exporterTypes) {
|
|
||||||
if (exporterType === 'console') {
|
|
||||||
exporters.push(new ConsoleSpanExporter())
|
|
||||||
} else if (exporterType === 'otlp') {
|
|
||||||
const protocol =
|
|
||||||
process.env.OTEL_EXPORTER_OTLP_TRACES_PROTOCOL?.trim() ||
|
|
||||||
process.env.OTEL_EXPORTER_OTLP_PROTOCOL?.trim()
|
|
||||||
|
|
||||||
const httpConfig = getOTLPExporterConfig()
|
|
||||||
|
|
||||||
switch (protocol) {
|
|
||||||
case 'grpc': {
|
|
||||||
const { OTLPTraceExporter } = await import(
|
|
||||||
'@opentelemetry/exporter-trace-otlp-grpc'
|
|
||||||
)
|
|
||||||
exporters.push(new OTLPTraceExporter())
|
|
||||||
break
|
|
||||||
}
|
|
||||||
case 'http/json': {
|
|
||||||
const { OTLPTraceExporter } = await import(
|
|
||||||
'@opentelemetry/exporter-trace-otlp-http'
|
|
||||||
)
|
|
||||||
exporters.push(new OTLPTraceExporter(httpConfig))
|
|
||||||
break
|
|
||||||
}
|
|
||||||
case 'http/protobuf': {
|
|
||||||
const { OTLPTraceExporter } = await import(
|
|
||||||
'@opentelemetry/exporter-trace-otlp-proto'
|
|
||||||
)
|
|
||||||
exporters.push(new OTLPTraceExporter(httpConfig))
|
|
||||||
break
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
throw new Error(
|
|
||||||
`Unknown protocol set in OTEL_EXPORTER_OTLP_TRACES_PROTOCOL or OTEL_EXPORTER_OTLP_PROTOCOL env var: ${protocol}`,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
throw new Error(
|
|
||||||
`Unknown exporter type set in OTEL_TRACES_EXPORTER env var: ${exporterType}`,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return exporters
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isTelemetryEnabled() {
|
|
||||||
return isEnvTruthy(process.env.CLAUDE_CODE_ENABLE_TELEMETRY)
|
|
||||||
}
|
|
||||||
|
|
||||||
function getBigQueryExportingReader() {
|
|
||||||
const bigqueryExporter = new BigQueryMetricsExporter()
|
|
||||||
return new PeriodicExportingMetricReader({
|
|
||||||
exporter: bigqueryExporter,
|
|
||||||
exportIntervalMillis: 5 * 60 * 1000, // 5mins for BigQuery metrics exporter to reduce load
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function isBigQueryMetricsEnabled() {
|
|
||||||
// BigQuery metrics are enabled for:
|
|
||||||
// 1. API customers (excluding Claude.ai subscribers and Bedrock/Vertex)
|
|
||||||
// 2. Claude for Enterprise (C4E) users
|
|
||||||
// 3. Claude for Teams users
|
|
||||||
const subscriptionType = getSubscriptionType()
|
|
||||||
const isC4EOrTeamUser =
|
|
||||||
isClaudeAISubscriber() &&
|
|
||||||
(subscriptionType === 'enterprise' || subscriptionType === 'team')
|
|
||||||
|
|
||||||
return is1PApiCustomer() || isC4EOrTeamUser
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize beta tracing - a separate code path for detailed debugging.
|
|
||||||
* Uses BETA_TRACING_ENDPOINT instead of OTEL_EXPORTER_OTLP_ENDPOINT.
|
|
||||||
*/
|
|
||||||
async function initializeBetaTracing(
|
|
||||||
resource: ReturnType<typeof resourceFromAttributes>,
|
|
||||||
): Promise<void> {
|
|
||||||
const endpoint = process.env.BETA_TRACING_ENDPOINT
|
|
||||||
if (!endpoint) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const [{ OTLPTraceExporter }, { OTLPLogExporter }] = await Promise.all([
|
|
||||||
import('@opentelemetry/exporter-trace-otlp-http'),
|
|
||||||
import('@opentelemetry/exporter-logs-otlp-http'),
|
|
||||||
])
|
|
||||||
|
|
||||||
const httpConfig = {
|
|
||||||
url: `${endpoint}/v1/traces`,
|
|
||||||
}
|
|
||||||
|
|
||||||
const logHttpConfig = {
|
|
||||||
url: `${endpoint}/v1/logs`,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize trace exporter
|
|
||||||
const traceExporter = new OTLPTraceExporter(httpConfig)
|
|
||||||
const spanProcessor = new BatchSpanProcessor(traceExporter, {
|
|
||||||
scheduledDelayMillis: DEFAULT_TRACES_EXPORT_INTERVAL_MS,
|
|
||||||
})
|
|
||||||
|
|
||||||
const tracerProvider = new BasicTracerProvider({
|
|
||||||
resource,
|
|
||||||
spanProcessors: [spanProcessor],
|
|
||||||
})
|
|
||||||
|
|
||||||
trace.setGlobalTracerProvider(tracerProvider)
|
|
||||||
setTracerProvider(tracerProvider)
|
|
||||||
|
|
||||||
// Initialize log exporter
|
|
||||||
const logExporter = new OTLPLogExporter(logHttpConfig)
|
|
||||||
const loggerProvider = new LoggerProvider({
|
|
||||||
resource,
|
|
||||||
processors: [
|
|
||||||
new BatchLogRecordProcessor(logExporter, {
|
|
||||||
scheduledDelayMillis: DEFAULT_LOGS_EXPORT_INTERVAL_MS,
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
})
|
|
||||||
|
|
||||||
logs.setGlobalLoggerProvider(loggerProvider)
|
|
||||||
setLoggerProvider(loggerProvider)
|
|
||||||
|
|
||||||
// Initialize event logger
|
|
||||||
const eventLogger = logs.getLogger(
|
|
||||||
'com.anthropic.claude_code.events',
|
|
||||||
MACRO.VERSION,
|
|
||||||
)
|
|
||||||
setEventLogger(eventLogger)
|
|
||||||
|
|
||||||
// Setup flush handlers - flush both logs AND traces
|
|
||||||
process.on('beforeExit', async () => {
|
|
||||||
await loggerProvider?.forceFlush()
|
|
||||||
await tracerProvider?.forceFlush()
|
|
||||||
})
|
|
||||||
|
|
||||||
process.on('exit', () => {
|
|
||||||
void loggerProvider?.forceFlush()
|
|
||||||
void tracerProvider?.forceFlush()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function initializeTelemetry() {
|
|
||||||
profileCheckpoint('telemetry_init_start')
|
|
||||||
bootstrapTelemetry()
|
|
||||||
|
|
||||||
// Console exporters call console.dir on a timer (5s logs/traces, 60s
|
|
||||||
// metrics), writing pretty-printed objects to stdout. In stream-json
|
|
||||||
// mode stdout is the SDK message channel; the first line (`{`) breaks
|
|
||||||
// the SDK's line reader. Stripped here (not main.tsx) because init.ts
|
|
||||||
// re-runs applyConfigEnvironmentVariables() inside initializeTelemetry-
|
|
||||||
// AfterTrust for remote-managed-settings users, and bootstrapTelemetry
|
|
||||||
// above copies ANT_OTEL_* for ant users — both would undo an earlier strip.
|
|
||||||
if (getHasFormattedOutput()) {
|
|
||||||
for (const key of [
|
|
||||||
'OTEL_METRICS_EXPORTER',
|
|
||||||
'OTEL_LOGS_EXPORTER',
|
|
||||||
'OTEL_TRACES_EXPORTER',
|
|
||||||
] as const) {
|
|
||||||
const v = process.env[key]
|
|
||||||
if (v?.includes('console')) {
|
|
||||||
process.env[key] = v
|
|
||||||
.split(',')
|
|
||||||
.map(s => s.trim())
|
|
||||||
.filter(s => s !== 'console')
|
|
||||||
.join(',')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
diag.setLogger(new ClaudeCodeDiagLogger(), DiagLogLevel.ERROR)
|
|
||||||
|
|
||||||
// Initialize Perfetto tracing (independent of OTEL)
|
|
||||||
// Enable via CLAUDE_CODE_PERFETTO_TRACE=1 or CLAUDE_CODE_PERFETTO_TRACE=<path>
|
|
||||||
initializePerfettoTracing()
|
|
||||||
|
|
||||||
const readers = []
|
|
||||||
|
|
||||||
// Add customer exporters (if enabled)
|
|
||||||
const telemetryEnabled = isTelemetryEnabled()
|
|
||||||
logForDebugging(
|
|
||||||
`[3P telemetry] isTelemetryEnabled=${telemetryEnabled} (CLAUDE_CODE_ENABLE_TELEMETRY=${process.env.CLAUDE_CODE_ENABLE_TELEMETRY})`,
|
|
||||||
)
|
|
||||||
if (telemetryEnabled) {
|
|
||||||
readers.push(...(await getOtlpReaders()))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add BigQuery exporter (for API customers, C4E users, and internal users)
|
|
||||||
if (isBigQueryMetricsEnabled()) {
|
|
||||||
readers.push(getBigQueryExportingReader())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create base resource with service attributes
|
|
||||||
const platform = getPlatform()
|
|
||||||
const baseAttributes: Record<string, string> = {
|
|
||||||
[ATTR_SERVICE_NAME]: 'claude-code',
|
|
||||||
[ATTR_SERVICE_VERSION]: MACRO.VERSION,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add WSL-specific attributes if running on WSL
|
|
||||||
if (platform === 'wsl') {
|
|
||||||
const wslVersion = getWslVersion()
|
|
||||||
if (wslVersion) {
|
|
||||||
baseAttributes['wsl.version'] = wslVersion
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const baseResource = resourceFromAttributes(baseAttributes)
|
|
||||||
|
|
||||||
// Use OpenTelemetry detectors
|
|
||||||
const osResource = resourceFromAttributes(
|
|
||||||
osDetector.detect().attributes || {},
|
|
||||||
)
|
|
||||||
|
|
||||||
// Extract only host.arch from hostDetector
|
|
||||||
const hostDetected = hostDetector.detect()
|
|
||||||
const hostArchAttributes = hostDetected.attributes?.[SEMRESATTRS_HOST_ARCH]
|
|
||||||
? {
|
|
||||||
[SEMRESATTRS_HOST_ARCH]: hostDetected.attributes[SEMRESATTRS_HOST_ARCH],
|
|
||||||
}
|
|
||||||
: {}
|
|
||||||
const hostArchResource = resourceFromAttributes(hostArchAttributes)
|
|
||||||
|
|
||||||
const envResource = resourceFromAttributes(
|
|
||||||
envDetector.detect().attributes || {},
|
|
||||||
)
|
|
||||||
|
|
||||||
// Merge resources - later resources take precedence
|
|
||||||
const resource = baseResource
|
|
||||||
.merge(osResource)
|
|
||||||
.merge(hostArchResource)
|
|
||||||
.merge(envResource)
|
|
||||||
|
|
||||||
// Check if beta tracing is enabled - this is a separate code path
|
|
||||||
// Available to all users who set ENABLE_BETA_TRACING_DETAILED=1 and BETA_TRACING_ENDPOINT
|
|
||||||
if (isBetaTracingEnabled()) {
|
|
||||||
void initializeBetaTracing(resource).catch(e =>
|
|
||||||
logForDebugging(`Beta tracing init failed: ${e}`, { level: 'error' }),
|
|
||||||
)
|
|
||||||
// Still set up meter provider for metrics (but skip regular logs/traces setup)
|
|
||||||
const meterProvider = new MeterProvider({
|
|
||||||
resource,
|
|
||||||
views: [],
|
|
||||||
readers,
|
|
||||||
})
|
|
||||||
setMeterProvider(meterProvider)
|
|
||||||
|
|
||||||
// Register shutdown for beta tracing
|
|
||||||
const shutdownTelemetry = async () => {
|
|
||||||
const timeoutMs = parseInt(
|
|
||||||
process.env.CLAUDE_CODE_OTEL_SHUTDOWN_TIMEOUT_MS || '2000',
|
|
||||||
)
|
|
||||||
try {
|
|
||||||
endInteractionSpan()
|
|
||||||
|
|
||||||
// Force flush + shutdown together inside the timeout. Previously forceFlush
|
|
||||||
// was awaited unbounded BEFORE the race, blocking exit on slow OTLP endpoints.
|
|
||||||
// Each provider's flush→shutdown is chained independently so a slow logger
|
|
||||||
// flush doesn't delay meterProvider/tracerProvider shutdown (no waterfall).
|
|
||||||
const loggerProvider = getLoggerProvider()
|
|
||||||
const tracerProvider = getTracerProvider()
|
|
||||||
|
|
||||||
const chains: Promise<void>[] = [meterProvider.shutdown()]
|
|
||||||
if (loggerProvider) {
|
|
||||||
chains.push(
|
|
||||||
loggerProvider.forceFlush().then(() => loggerProvider.shutdown()),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (tracerProvider) {
|
|
||||||
chains.push(
|
|
||||||
tracerProvider.forceFlush().then(() => tracerProvider.shutdown()),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
await Promise.race([
|
|
||||||
Promise.all(chains),
|
|
||||||
telemetryTimeout(timeoutMs, 'OpenTelemetry shutdown timeout'),
|
|
||||||
])
|
|
||||||
} catch {
|
|
||||||
// Ignore shutdown errors
|
|
||||||
}
|
|
||||||
}
|
|
||||||
registerCleanup(shutdownTelemetry)
|
|
||||||
|
|
||||||
return meterProvider.getMeter('com.anthropic.claude_code', MACRO.VERSION)
|
|
||||||
}
|
|
||||||
|
|
||||||
const meterProvider = new MeterProvider({
|
|
||||||
resource,
|
|
||||||
views: [],
|
|
||||||
readers,
|
|
||||||
})
|
|
||||||
|
|
||||||
// Store reference in state for flushing
|
|
||||||
setMeterProvider(meterProvider)
|
|
||||||
|
|
||||||
// Initialize logs if telemetry is enabled
|
|
||||||
if (telemetryEnabled) {
|
|
||||||
const logExporters = await getOtlpLogExporters()
|
|
||||||
logForDebugging(
|
|
||||||
`[3P telemetry] Created ${logExporters.length} log exporter(s)`,
|
|
||||||
)
|
|
||||||
|
|
||||||
if (logExporters.length > 0) {
|
|
||||||
const loggerProvider = new LoggerProvider({
|
|
||||||
resource,
|
|
||||||
// Add batch processors for each exporter
|
|
||||||
processors: logExporters.map(
|
|
||||||
exporter =>
|
|
||||||
new BatchLogRecordProcessor(exporter, {
|
|
||||||
scheduledDelayMillis: parseInt(
|
|
||||||
process.env.OTEL_LOGS_EXPORT_INTERVAL ||
|
|
||||||
DEFAULT_LOGS_EXPORT_INTERVAL_MS.toString(),
|
|
||||||
),
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
})
|
|
||||||
|
|
||||||
// Register the logger provider globally
|
|
||||||
logs.setGlobalLoggerProvider(loggerProvider)
|
|
||||||
setLoggerProvider(loggerProvider)
|
|
||||||
|
|
||||||
// Initialize event logger
|
|
||||||
const eventLogger = logs.getLogger(
|
|
||||||
'com.anthropic.claude_code.events',
|
|
||||||
MACRO.VERSION,
|
|
||||||
)
|
|
||||||
setEventLogger(eventLogger)
|
|
||||||
logForDebugging('[3P telemetry] Event logger set successfully')
|
|
||||||
|
|
||||||
// 'beforeExit' is emitted when Node.js empties its event loop and has no additional work to schedule.
|
|
||||||
// Unlike 'exit', it allows us to perform async operations, so it works well for letting
|
|
||||||
// network requests complete before the process exits naturally.
|
|
||||||
process.on('beforeExit', async () => {
|
|
||||||
await loggerProvider?.forceFlush()
|
|
||||||
// Also flush traces - they use BatchSpanProcessor which needs explicit flush
|
|
||||||
const tracerProvider = getTracerProvider()
|
|
||||||
await tracerProvider?.forceFlush()
|
|
||||||
})
|
|
||||||
|
|
||||||
process.on('exit', () => {
|
|
||||||
// Final attempt to flush logs and traces
|
|
||||||
void loggerProvider?.forceFlush()
|
|
||||||
void getTracerProvider()?.forceFlush()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize tracing if enhanced telemetry is enabled (BETA)
|
|
||||||
if (telemetryEnabled && isEnhancedTelemetryEnabled()) {
|
|
||||||
const traceExporters = await getOtlpTraceExporters()
|
|
||||||
if (traceExporters.length > 0) {
|
|
||||||
// Create span processors for each exporter
|
|
||||||
const spanProcessors = traceExporters.map(
|
|
||||||
exporter =>
|
|
||||||
new BatchSpanProcessor(exporter, {
|
|
||||||
scheduledDelayMillis: parseInt(
|
|
||||||
process.env.OTEL_TRACES_EXPORT_INTERVAL ||
|
|
||||||
DEFAULT_TRACES_EXPORT_INTERVAL_MS.toString(),
|
|
||||||
),
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
const tracerProvider = new BasicTracerProvider({
|
|
||||||
resource,
|
|
||||||
spanProcessors,
|
|
||||||
})
|
|
||||||
|
|
||||||
// Register the tracer provider globally
|
|
||||||
trace.setGlobalTracerProvider(tracerProvider)
|
|
||||||
setTracerProvider(tracerProvider)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Shutdown metrics and logs on exit (flushes and closes exporters)
|
|
||||||
const shutdownTelemetry = async () => {
|
|
||||||
const timeoutMs = parseInt(
|
|
||||||
process.env.CLAUDE_CODE_OTEL_SHUTDOWN_TIMEOUT_MS || '2000',
|
|
||||||
)
|
|
||||||
|
|
||||||
try {
|
|
||||||
// End any active interaction span before shutdown
|
|
||||||
endInteractionSpan()
|
|
||||||
|
|
||||||
const shutdownPromises = [meterProvider.shutdown()]
|
|
||||||
const loggerProvider = getLoggerProvider()
|
|
||||||
if (loggerProvider) {
|
|
||||||
shutdownPromises.push(loggerProvider.shutdown())
|
|
||||||
}
|
|
||||||
const tracerProvider = getTracerProvider()
|
|
||||||
if (tracerProvider) {
|
|
||||||
shutdownPromises.push(tracerProvider.shutdown())
|
|
||||||
}
|
|
||||||
|
|
||||||
await Promise.race([
|
|
||||||
Promise.all(shutdownPromises),
|
|
||||||
telemetryTimeout(timeoutMs, 'OpenTelemetry shutdown timeout'),
|
|
||||||
])
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof Error && error.message.includes('timeout')) {
|
|
||||||
logForDebugging(
|
|
||||||
`
|
|
||||||
OpenTelemetry telemetry flush timed out after ${timeoutMs}ms
|
|
||||||
|
|
||||||
To resolve this issue, you can:
|
|
||||||
1. Increase the timeout by setting CLAUDE_CODE_OTEL_SHUTDOWN_TIMEOUT_MS env var (e.g., 5000 for 5 seconds)
|
|
||||||
2. Check if your OpenTelemetry backend is experiencing scalability issues
|
|
||||||
3. Disable OpenTelemetry by unsetting CLAUDE_CODE_ENABLE_TELEMETRY env var
|
|
||||||
|
|
||||||
Current timeout: ${timeoutMs}ms
|
|
||||||
`,
|
|
||||||
{ level: 'error' },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Always register shutdown (internal metrics are always enabled)
|
|
||||||
registerCleanup(shutdownTelemetry)
|
|
||||||
|
|
||||||
return meterProvider.getMeter('com.anthropic.claude_code', MACRO.VERSION)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Flush all pending telemetry data immediately.
|
|
||||||
* This should be called before logout or org switching to prevent data leakage.
|
|
||||||
*/
|
|
||||||
export async function flushTelemetry(): Promise<void> {
|
export async function flushTelemetry(): Promise<void> {
|
||||||
const meterProvider = getMeterProvider()
|
|
||||||
if (!meterProvider) {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const timeoutMs = parseInt(
|
|
||||||
process.env.CLAUDE_CODE_OTEL_FLUSH_TIMEOUT_MS || '5000',
|
|
||||||
)
|
|
||||||
|
|
||||||
try {
|
|
||||||
const flushPromises = [meterProvider.forceFlush()]
|
|
||||||
const loggerProvider = getLoggerProvider()
|
|
||||||
if (loggerProvider) {
|
|
||||||
flushPromises.push(loggerProvider.forceFlush())
|
|
||||||
}
|
|
||||||
const tracerProvider = getTracerProvider()
|
|
||||||
if (tracerProvider) {
|
|
||||||
flushPromises.push(tracerProvider.forceFlush())
|
|
||||||
}
|
|
||||||
|
|
||||||
await Promise.race([
|
|
||||||
Promise.all(flushPromises),
|
|
||||||
telemetryTimeout(timeoutMs, 'OpenTelemetry flush timeout'),
|
|
||||||
])
|
|
||||||
|
|
||||||
logForDebugging('Telemetry flushed successfully')
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof TelemetryTimeoutError) {
|
|
||||||
logForDebugging(
|
|
||||||
`Telemetry flush timed out after ${timeoutMs}ms. Some metrics may not be exported.`,
|
|
||||||
{ level: 'warn' },
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
logForDebugging(`Telemetry flush failed: ${errorMessage(error)}`, {
|
|
||||||
level: 'error',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
// Don't throw - allow logout to continue even if flush fails
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseOtelHeadersEnvVar(): Record<string, string> {
|
|
||||||
const headers: Record<string, string> = {}
|
|
||||||
const envHeaders = process.env.OTEL_EXPORTER_OTLP_HEADERS
|
|
||||||
if (envHeaders) {
|
|
||||||
for (const pair of envHeaders.split(',')) {
|
|
||||||
const [key, ...valueParts] = pair.split('=')
|
|
||||||
if (key && valueParts.length > 0) {
|
|
||||||
headers[key.trim()] = valueParts.join('=').trim()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return headers
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get configuration for OTLP exporters including:
|
|
||||||
* - HTTP agent options (proxy, mTLS)
|
|
||||||
* - Dynamic headers via otelHeadersHelper or static headers from env var
|
|
||||||
*/
|
|
||||||
function getOTLPExporterConfig() {
|
|
||||||
const proxyUrl = getProxyUrl()
|
|
||||||
const mtlsConfig = getMTLSConfig()
|
|
||||||
const settings = getSettings_DEPRECATED()
|
|
||||||
|
|
||||||
// Build base config
|
|
||||||
const config: Record<string, unknown> = {}
|
|
||||||
|
|
||||||
// Parse static headers from env var once (doesn't change at runtime)
|
|
||||||
const staticHeaders = parseOtelHeadersEnvVar()
|
|
||||||
|
|
||||||
// If otelHeadersHelper is configured, use async headers function for dynamic refresh
|
|
||||||
// Otherwise just return static headers if any exist
|
|
||||||
if (settings?.otelHeadersHelper) {
|
|
||||||
config.headers = async (): Promise<Record<string, string>> => {
|
|
||||||
const dynamicHeaders = getOtelHeadersFromHelper()
|
|
||||||
return { ...staticHeaders, ...dynamicHeaders }
|
|
||||||
}
|
|
||||||
} else if (Object.keys(staticHeaders).length > 0) {
|
|
||||||
config.headers = async (): Promise<Record<string, string>> => staticHeaders
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if we should bypass proxy for OTEL endpoint
|
|
||||||
const otelEndpoint = process.env.OTEL_EXPORTER_OTLP_ENDPOINT
|
|
||||||
if (!proxyUrl || (otelEndpoint && shouldBypassProxy(otelEndpoint))) {
|
|
||||||
// No proxy configured or OTEL endpoint should bypass proxy
|
|
||||||
const caCerts = getCACertificates()
|
|
||||||
if (mtlsConfig || caCerts) {
|
|
||||||
config.httpAgentOptions = {
|
|
||||||
...mtlsConfig,
|
|
||||||
...(caCerts && { ca: caCerts }),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return config
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return an HttpAgentFactory function that creates our proxy agent
|
|
||||||
const caCerts = getCACertificates()
|
|
||||||
const agentFactory = (_protocol: string) => {
|
|
||||||
// Create and return the proxy agent with mTLS and CA cert config
|
|
||||||
const proxyAgent =
|
|
||||||
mtlsConfig || caCerts
|
|
||||||
? new HttpsProxyAgent(proxyUrl, {
|
|
||||||
...(mtlsConfig && {
|
|
||||||
cert: mtlsConfig.cert,
|
|
||||||
key: mtlsConfig.key,
|
|
||||||
passphrase: mtlsConfig.passphrase,
|
|
||||||
}),
|
|
||||||
...(caCerts && { ca: caCerts }),
|
|
||||||
})
|
|
||||||
: new HttpsProxyAgent(proxyUrl)
|
|
||||||
|
|
||||||
return proxyAgent
|
|
||||||
}
|
|
||||||
|
|
||||||
config.httpAgentOptions = agentFactory
|
|
||||||
return config
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { isPolicyAllowed } from 'src/services/policyLimits/index.js';
|
|||||||
import { z } from 'zod/v4';
|
import { z } from 'zod/v4';
|
||||||
import { getTeleportErrors, TeleportError, type TeleportLocalErrorType } from '../components/TeleportError.js';
|
import { getTeleportErrors, TeleportError, type TeleportLocalErrorType } from '../components/TeleportError.js';
|
||||||
import { getOauthConfig } from '../constants/oauth.js';
|
import { getOauthConfig } from '../constants/oauth.js';
|
||||||
import type { SDKMessage } from '../entrypoints/agentSdkTypes.js';
|
import type { SDKMessage } from '../entrypoints/agentSdkTypes.ts';
|
||||||
import type { Root } from '../ink.js';
|
import type { Root } from '../ink.js';
|
||||||
import { KeybindingSetup } from '../keybindings/KeybindingProviderSetup.js';
|
import { KeybindingSetup } from '../keybindings/KeybindingProviderSetup.js';
|
||||||
import { queryHaiku } from '../services/api/claude.js';
|
import { queryHaiku } from '../services/api/claude.js';
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import type {
|
|||||||
ToolResultBlockParam,
|
ToolResultBlockParam,
|
||||||
ToolUseBlock,
|
ToolUseBlock,
|
||||||
} from '@anthropic-ai/sdk/resources'
|
} from '@anthropic-ai/sdk/resources'
|
||||||
import type { SDKMessage } from '../../entrypoints/agentSdkTypes.js'
|
import type { SDKMessage } from '../../entrypoints/agentSdkTypes.ts'
|
||||||
import { EXIT_PLAN_MODE_V2_TOOL_NAME } from '../../tools/ExitPlanModeTool/constants.js'
|
import { EXIT_PLAN_MODE_V2_TOOL_NAME } from '../../tools/ExitPlanModeTool/constants.js'
|
||||||
import { logForDebugging } from '../debug.js'
|
import { logForDebugging } from '../debug.js'
|
||||||
import { sleep } from '../sleep.js'
|
import { sleep } from '../sleep.js'
|
||||||
|
|||||||
Reference in New Issue
Block a user