Checkpoint the full local bridge and audit work before telemetry removal

You asked for all local code to be committed before the broader telemetry-removal pass. This commit snapshots the current bridge/session ingress changes together with the local audit documents so the next cleanup can proceed from a stable rollback point.

Constraint: Preserve the exact local worktree state before the telemetry-removal refactor begins
Constraint: Avoid mixing this baseline snapshot with the upcoming telemetry deletions
Rejected: Fold these staged changes into the telemetry-removal commit | Would blur the before/after boundary and make rollback harder
Confidence: medium
Scope-risk: moderate
Reversibility: clean
Directive: Treat this commit as the pre-removal checkpoint when reviewing later telemetry cleanup diffs
Tested: Not run (baseline snapshot commit requested before the next cleanup pass)
Not-tested: Runtime, build, and typecheck for the staged bridge/session changes
This commit is contained in:
2026-04-09 14:09:44 +08:00
parent 523b8c0a4a
commit 5af8acb2bb
8 changed files with 1382 additions and 91 deletions

View File

@@ -1,10 +1,9 @@
import { type ChildProcess, spawn } from 'child_process'
import { createWriteStream, type WriteStream } from 'fs'
import { tmpdir } from 'os'
import { dirname, join } from 'path'
import { basename, dirname, join } from 'path'
import { createInterface } from 'readline'
import { jsonParse, jsonStringify } from '../utils/slowOperations.js'
import { debugTruncate } from './debugUtils.js'
import type {
SessionActivity,
SessionDoneStatus,
@@ -25,6 +24,61 @@ export function safeFilenameId(id: string): string {
return id.replace(/[^a-zA-Z0-9_-]/g, '_')
}
function summarizeSessionRunnerErrorForDebug(error: unknown): string {
return jsonStringify({
errorType:
error instanceof Error ? error.constructor.name : typeof error,
errorName: error instanceof Error ? error.name : undefined,
hasMessage: error instanceof Error ? error.message.length > 0 : false,
hasStack: error instanceof Error ? Boolean(error.stack) : false,
})
}
function summarizeSessionRunnerFrameForDebug(data: string): string {
try {
const parsed = jsonParse(data)
if (parsed && typeof parsed === 'object') {
const value = parsed as Record<string, unknown>
return jsonStringify({
frameType: typeof value.type === 'string' ? value.type : 'unknown',
subtype:
typeof value.subtype === 'string'
? value.subtype
: value.response &&
typeof value.response === 'object' &&
typeof (value.response as Record<string, unknown>).subtype ===
'string'
? (value.response as Record<string, unknown>).subtype
: value.request &&
typeof value.request === 'object' &&
typeof (value.request as Record<string, unknown>).subtype ===
'string'
? (value.request as Record<string, unknown>).subtype
: undefined,
hasUuid: typeof value.uuid === 'string',
length: data.length,
})
}
} catch {
// fall through to raw-length summary
}
return jsonStringify({
frameType: 'unparsed',
length: data.length,
})
}
function summarizeSessionRunnerArgsForDebug(args: string[]): string {
return jsonStringify({
argCount: args.length,
hasSdkUrl: args.includes('--sdk-url'),
hasSessionId: args.includes('--session-id'),
hasDebugFile: args.includes('--debug-file'),
hasVerbose: args.includes('--verbose'),
hasPermissionMode: args.includes('--permission-mode'),
})
}
/**
* A control_request emitted by the child CLI when it needs permission to
* execute a **specific** tool invocation (not a general capability check).
@@ -144,9 +198,7 @@ function extractActivities(
summary,
timestamp: now,
})
onDebug(
`[bridge:activity] sessionId=${sessionId} tool_use name=${name} ${inputPreview(input)}`,
)
onDebug(`[bridge:activity] tool_use name=${name}`)
} else if (b.type === 'text') {
const text = (b.text as string) ?? ''
if (text.length > 0) {
@@ -156,7 +208,7 @@ function extractActivities(
timestamp: now,
})
onDebug(
`[bridge:activity] sessionId=${sessionId} text "${text.slice(0, 100)}"`,
`[bridge:activity] text length=${text.length}`,
)
}
}
@@ -171,9 +223,7 @@ function extractActivities(
summary: 'Session completed',
timestamp: now,
})
onDebug(
`[bridge:activity] sessionId=${sessionId} result subtype=success`,
)
onDebug('[bridge:activity] result subtype=success')
} else if (subtype) {
const errors = msg.errors as string[] | undefined
const errorSummary = errors?.[0] ?? `Error: ${subtype}`
@@ -182,13 +232,9 @@ function extractActivities(
summary: errorSummary,
timestamp: now,
})
onDebug(
`[bridge:activity] sessionId=${sessionId} result subtype=${subtype} error="${errorSummary}"`,
)
onDebug(`[bridge:activity] result subtype=${subtype}`)
} else {
onDebug(
`[bridge:activity] sessionId=${sessionId} result subtype=undefined`,
)
onDebug('[bridge:activity] result subtype=undefined')
}
break
}
@@ -233,18 +279,6 @@ function extractUserMessageText(
return text ? text : undefined
}
/** Build a short preview of tool input for debug logging. */
function inputPreview(input: Record<string, unknown>): string {
const parts: string[] = []
for (const [key, val] of Object.entries(input)) {
if (typeof val === 'string') {
parts.push(`${key}="${val.slice(0, 100)}"`)
}
if (parts.length >= 3) break
}
return parts.join(' ')
}
export function createSessionSpawner(deps: SessionSpawnerDeps): SessionSpawner {
return {
spawn(opts: SessionSpawnOpts, dir: string): SessionHandle {
@@ -277,11 +311,15 @@ export function createSessionSpawner(deps: SessionSpawnerDeps): SessionSpawner {
transcriptStream = createWriteStream(transcriptPath, { flags: 'a' })
transcriptStream.on('error', err => {
deps.onDebug(
`[bridge:session] Transcript write error: ${err.message}`,
`[bridge:session] Transcript write error: ${summarizeSessionRunnerErrorForDebug(
err,
)}`,
)
transcriptStream = null
})
deps.onDebug(`[bridge:session] Transcript log: ${transcriptPath}`)
deps.onDebug(
`[bridge:session] Transcript log configured (${basename(transcriptPath)})`,
)
}
const args = [
@@ -323,11 +361,15 @@ export function createSessionSpawner(deps: SessionSpawnerDeps): SessionSpawner {
}
deps.onDebug(
`[bridge:session] Spawning sessionId=${opts.sessionId} sdkUrl=${opts.sdkUrl} accessToken=${opts.accessToken ? 'present' : 'MISSING'}`,
`[bridge:session] Spawning child session process (accessToken=${opts.accessToken ? 'present' : 'MISSING'})`,
)
deps.onDebug(
`[bridge:session] Child args: ${summarizeSessionRunnerArgsForDebug(args)}`,
)
deps.onDebug(`[bridge:session] Child args: ${args.join(' ')}`)
if (debugFile) {
deps.onDebug(`[bridge:session] Debug log: ${debugFile}`)
deps.onDebug(
`[bridge:session] Debug log configured (${basename(debugFile)})`,
)
}
// Pipe all three streams: stdin for control, stdout for NDJSON parsing,
@@ -339,9 +381,7 @@ export function createSessionSpawner(deps: SessionSpawnerDeps): SessionSpawner {
windowsHide: true,
})
deps.onDebug(
`[bridge:session] sessionId=${opts.sessionId} pid=${child.pid}`,
)
deps.onDebug('[bridge:session] Child process started')
const activities: SessionActivity[] = []
let currentActivity: SessionActivity | null = null
@@ -376,7 +416,7 @@ export function createSessionSpawner(deps: SessionSpawnerDeps): SessionSpawner {
// Log all messages flowing from the child CLI to the bridge
deps.onDebug(
`[bridge:ws] sessionId=${opts.sessionId} <<< ${debugTruncate(line)}`,
`[bridge:ws] <<< ${summarizeSessionRunnerFrameForDebug(line)}`,
)
// In verbose mode, forward raw output to stderr
@@ -455,25 +495,23 @@ export function createSessionSpawner(deps: SessionSpawnerDeps): SessionSpawner {
if (signal === 'SIGTERM' || signal === 'SIGINT') {
deps.onDebug(
`[bridge:session] sessionId=${opts.sessionId} interrupted signal=${signal} pid=${child.pid}`,
`[bridge:session] interrupted signal=${signal ?? 'unknown'}`,
)
resolve('interrupted')
} else if (code === 0) {
deps.onDebug(
`[bridge:session] sessionId=${opts.sessionId} completed exit_code=0 pid=${child.pid}`,
)
deps.onDebug('[bridge:session] completed exit_code=0')
resolve('completed')
} else {
deps.onDebug(
`[bridge:session] sessionId=${opts.sessionId} failed exit_code=${code} pid=${child.pid}`,
)
deps.onDebug(`[bridge:session] failed exit_code=${code}`)
resolve('failed')
}
})
child.on('error', err => {
deps.onDebug(
`[bridge:session] sessionId=${opts.sessionId} spawn error: ${err.message}`,
`[bridge:session] spawn error: ${summarizeSessionRunnerErrorForDebug(
err,
)}`,
)
resolve('failed')
})
@@ -490,9 +528,7 @@ export function createSessionSpawner(deps: SessionSpawnerDeps): SessionSpawner {
},
kill(): void {
if (!child.killed) {
deps.onDebug(
`[bridge:session] Sending SIGTERM to sessionId=${opts.sessionId} pid=${child.pid}`,
)
deps.onDebug('[bridge:session] Sending SIGTERM to child process')
// On Windows, child.kill('SIGTERM') throws; use default signal.
if (process.platform === 'win32') {
child.kill()
@@ -506,9 +542,7 @@ export function createSessionSpawner(deps: SessionSpawnerDeps): SessionSpawner {
// not when the process exits. We need to send SIGKILL even after SIGTERM.
if (!sigkillSent && child.pid) {
sigkillSent = true
deps.onDebug(
`[bridge:session] Sending SIGKILL to sessionId=${opts.sessionId} pid=${child.pid}`,
)
deps.onDebug('[bridge:session] Sending SIGKILL to child process')
if (process.platform === 'win32') {
child.kill()
} else {
@@ -519,7 +553,7 @@ export function createSessionSpawner(deps: SessionSpawnerDeps): SessionSpawner {
writeStdin(data: string): void {
if (child.stdin && !child.stdin.destroyed) {
deps.onDebug(
`[bridge:ws] sessionId=${opts.sessionId} >>> ${debugTruncate(data)}`,
`[bridge:ws] >>> ${summarizeSessionRunnerFrameForDebug(data)}`,
)
child.stdin.write(data)
}
@@ -536,9 +570,7 @@ export function createSessionSpawner(deps: SessionSpawnerDeps): SessionSpawner {
variables: { CLAUDE_CODE_SESSION_ACCESS_TOKEN: token },
}) + '\n',
)
deps.onDebug(
`[bridge:session] Sent token refresh via stdin for sessionId=${opts.sessionId}`,
)
deps.onDebug('[bridge:session] Sent token refresh via stdin')
},
}