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:
@@ -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')
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user