Tighten bridge and teleport debug redaction

This commit is contained in:
2026-04-04 11:09:07 +08:00
parent 5149320afd
commit 86e7dbd1ab
8 changed files with 447 additions and 111 deletions

View File

@@ -55,6 +55,36 @@ export class BridgeFatalError extends Error {
} }
} }
function summarizeBridgeApiPayloadForDebug(data: unknown): string {
if (data === null) return 'null'
if (data === undefined) return 'undefined'
if (Array.isArray(data)) {
return debugBody({
type: 'array',
length: data.length,
})
}
if (typeof data !== 'object') {
return String(data)
}
const value = data as Record<string, unknown>
const workData =
value.data && typeof value.data === 'object'
? (value.data as Record<string, unknown>)
: undefined
return debugBody({
type: 'object',
keys: Object.keys(value)
.sort()
.slice(0, 10),
hasEnvironmentId: typeof value.environment_id === 'string',
hasEnvironmentSecret: typeof value.environment_secret === 'string',
hasWorkId: typeof value.id === 'string',
workType: typeof workData?.type === 'string' ? workData.type : undefined,
hasSessionId: typeof workData?.id === 'string',
})
}
export function createBridgeApiClient(deps: BridgeApiDeps): BridgeApiClient { export function createBridgeApiClient(deps: BridgeApiDeps): BridgeApiClient {
function debug(msg: string): void { function debug(msg: string): void {
deps.onDebug?.(msg) deps.onDebug?.(msg)
@@ -168,12 +198,14 @@ export function createBridgeApiClient(deps: BridgeApiDeps): BridgeApiClient {
handleErrorStatus(response.status, response.data, 'Registration') handleErrorStatus(response.status, response.data, 'Registration')
debug( debug(
`[bridge:api] POST /v1/environments/bridge -> ${response.status} environment_id=${response.data.environment_id}`, `[bridge:api] POST /v1/environments/bridge -> ${response.status}`,
) )
debug( debug(
`[bridge:api] >>> ${debugBody({ 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] <<< ${summarizeBridgeApiPayloadForDebug(response.data)}`,
)
return response.data return response.data
}, },
@@ -221,9 +253,11 @@ export function createBridgeApiClient(deps: BridgeApiDeps): BridgeApiClient {
} }
debug( debug(
`[bridge:api] GET .../work/poll -> ${response.status} workId=${response.data.id} type=${response.data.data?.type}${response.data.data?.id ? ` sessionId=${response.data.data.id}` : ''}`, `[bridge:api] GET .../work/poll -> ${response.status} type=${response.data.data?.type ?? 'unknown'}`,
)
debug(
`[bridge:api] <<< ${summarizeBridgeApiPayloadForDebug(response.data)}`,
) )
debug(`[bridge:api] <<< ${debugBody(response.data)}`)
return response.data return response.data
}, },
@@ -427,7 +461,9 @@ export function createBridgeApiClient(deps: BridgeApiDeps): BridgeApiClient {
`[bridge:api] POST /v1/sessions/${sessionId}/events -> ${response.status}`, `[bridge:api] POST /v1/sessions/${sessionId}/events -> ${response.status}`,
) )
debug(`[bridge:api] >>> ${debugBody({ events: [event] })}`) debug(`[bridge:api] >>> ${debugBody({ events: [event] })}`)
debug(`[bridge:api] <<< ${debugBody(response.data)}`) debug(
`[bridge:api] <<< ${summarizeBridgeApiPayloadForDebug(response.data)}`,
)
}, },
} }
} }

View File

@@ -9,7 +9,7 @@
import axios from 'axios' import axios from 'axios'
import { logForDebugging } from '../utils/debug.js' import { logForDebugging } from '../utils/debug.js'
import { errorMessage } from '../utils/errors.js' import { toError } from '../utils/errors.js'
import { jsonStringify } from '../utils/slowOperations.js' import { jsonStringify } from '../utils/slowOperations.js'
import { extractErrorDetail } from './debugUtils.js' import { extractErrorDetail } from './debugUtils.js'
@@ -23,6 +23,62 @@ function oauthHeaders(accessToken: string): Record<string, string> {
} }
} }
function summarizeCodeSessionResponseForDebug(data: unknown): string {
if (data === null) return 'null'
if (data === undefined) return 'undefined'
if (Array.isArray(data)) {
return jsonStringify({
payloadType: 'array',
length: data.length,
})
}
if (typeof data === 'object') {
const value = data as Record<string, unknown>
const session =
value.session && typeof value.session === 'object'
? (value.session as Record<string, unknown>)
: undefined
return jsonStringify({
payloadType: 'object',
keys: Object.keys(value)
.sort()
.slice(0, 10),
hasSession: Boolean(session),
hasSessionId: typeof session?.id === 'string',
hasWorkerJwt: typeof value.worker_jwt === 'string',
hasApiBaseUrl: typeof value.api_base_url === 'string',
hasExpiresIn: typeof value.expires_in === 'number',
hasWorkerEpoch:
typeof value.worker_epoch === 'number' ||
typeof value.worker_epoch === 'string',
})
}
return typeof data
}
function summarizeCodeSessionErrorForDebug(err: unknown): string {
const error = toError(err)
const summary: Record<string, unknown> = {
errorType: error.constructor.name,
errorName: error.name,
hasMessage: error.message.length > 0,
hasStack: Boolean(error.stack),
}
if (err && typeof err === 'object') {
const errorObj = err as Record<string, unknown>
if (typeof errorObj.code === 'string' || typeof errorObj.code === 'number') {
summary.code = errorObj.code
}
if (errorObj.response && typeof errorObj.response === 'object') {
const response = errorObj.response as Record<string, unknown>
if (typeof response.status === 'number') {
summary.httpStatus = response.status
}
}
}
return jsonStringify(summary)
}
export async function createCodeSession( export async function createCodeSession(
baseUrl: string, baseUrl: string,
accessToken: string, accessToken: string,
@@ -47,7 +103,9 @@ export async function createCodeSession(
) )
} catch (err: unknown) { } catch (err: unknown) {
logForDebugging( logForDebugging(
`[code-session] Session create request failed: ${errorMessage(err)}`, `[code-session] Session create request failed: ${summarizeCodeSessionErrorForDebug(
err,
)}`,
) )
return null return null
} }
@@ -72,7 +130,9 @@ export async function createCodeSession(
!data.session.id.startsWith('cse_') !data.session.id.startsWith('cse_')
) { ) {
logForDebugging( logForDebugging(
`[code-session] No session.id (cse_*) in response: ${jsonStringify(data).slice(0, 200)}`, `[code-session] No session.id (cse_*) in response: ${summarizeCodeSessionResponseForDebug(
data,
)}`,
) )
return null return null
} }
@@ -110,7 +170,9 @@ export async function fetchRemoteCredentials(
) )
} catch (err: unknown) { } catch (err: unknown) {
logForDebugging( logForDebugging(
`[code-session] /bridge request failed: ${errorMessage(err)}`, `[code-session] /bridge request failed: ${summarizeCodeSessionErrorForDebug(
err,
)}`,
) )
return null return null
} }
@@ -136,7 +198,9 @@ export async function fetchRemoteCredentials(
!('worker_epoch' in data) !('worker_epoch' in data)
) { ) {
logForDebugging( logForDebugging(
`[code-session] /bridge response malformed (need worker_jwt, expires_in, api_base_url, worker_epoch): ${jsonStringify(data).slice(0, 200)}`, `[code-session] /bridge response malformed (need worker_jwt, expires_in, api_base_url, worker_epoch): ${summarizeCodeSessionResponseForDebug(
data,
)}`,
) )
return null return null
} }

View File

@@ -21,15 +21,10 @@ const SECRET_PATTERN = new RegExp(
'g', 'g',
) )
const REDACT_MIN_LENGTH = 16
export function redactSecrets(s: string): string { export function redactSecrets(s: string): string {
return s.replace(SECRET_PATTERN, (_match, field: string, value: string) => { return s.replace(SECRET_PATTERN, (_match, field: string, value: string) => {
if (value.length < REDACT_MIN_LENGTH) { void value
return `"${field}":"[REDACTED]"` return `"${field}":"[REDACTED]"`
}
const redacted = `${value.slice(0, 8)}...${value.slice(-4)}`
return `"${field}":"${redacted}"`
}) })
} }
@@ -52,6 +47,73 @@ export function debugBody(data: unknown): string {
return s.slice(0, DEBUG_MSG_LIMIT) + `... (${s.length} chars)` return s.slice(0, DEBUG_MSG_LIMIT) + `... (${s.length} chars)`
} }
function summarizeValueShapeForDebug(value: unknown): unknown {
if (value === null) return 'null'
if (value === undefined) return 'undefined'
if (Array.isArray(value)) {
return {
type: 'array',
length: value.length,
}
}
if (typeof value === 'object') {
return {
type: 'object',
keys: Object.keys(value as Record<string, unknown>)
.sort()
.slice(0, 10),
}
}
return typeof value
}
export function summarizeBridgeErrorForDebug(err: unknown): string {
const summary: Record<string, unknown> = {}
if (err instanceof Error) {
summary.errorType = err.constructor.name
summary.errorName = err.name
summary.hasMessage = err.message.length > 0
summary.hasStack = Boolean(err.stack)
} else {
summary.errorType = typeof err
summary.hasValue = err !== undefined && err !== null
}
if (err && typeof err === 'object') {
const errorObj = err as Record<string, unknown>
if (
typeof errorObj.code === 'string' ||
typeof errorObj.code === 'number'
) {
summary.code = errorObj.code
}
if (
typeof errorObj.errno === 'string' ||
typeof errorObj.errno === 'number'
) {
summary.errno = errorObj.errno
}
if (typeof errorObj.status === 'number') {
summary.status = errorObj.status
}
if (typeof errorObj.syscall === 'string') {
summary.syscall = errorObj.syscall
}
if (errorObj.response && typeof errorObj.response === 'object') {
const response = errorObj.response as Record<string, unknown>
if (typeof response.status === 'number') {
summary.httpStatus = response.status
}
if ('data' in response) {
summary.responseData = summarizeValueShapeForDebug(response.data)
}
}
}
return jsonStringify(summary)
}
/** /**
* Extract a descriptive error message from an axios error (or any error). * Extract a descriptive error message from an axios error (or any error).
* For HTTP errors, appends the server's response body message if available, * For HTTP errors, appends the server's response body message if available,

View File

@@ -107,7 +107,7 @@ export function createTokenRefreshScheduler({
// (such as the follow-up refresh set by doRefresh) so the refresh // (such as the follow-up refresh set by doRefresh) so the refresh
// chain is not broken. // chain is not broken.
logForDebugging( logForDebugging(
`[${label}:token] Could not decode JWT expiry for sessionId=${sessionId}, token prefix=${token.slice(0, 15)}…, keeping existing timer`, `[${label}:token] Could not decode JWT expiry for sessionId=${sessionId}, keeping existing timer`,
) )
return return
} }
@@ -209,7 +209,7 @@ export function createTokenRefreshScheduler({
failureCounts.delete(sessionId) failureCounts.delete(sessionId)
logForDebugging( logForDebugging(
`[${label}:token] Refreshing token for sessionId=${sessionId}: new token prefix=${oauthToken.slice(0, 15)}`, `[${label}:token] Refreshing token for sessionId=${sessionId}`,
) )
logEvent('tengu_bridge_token_refreshed', {}) logEvent('tengu_bridge_token_refreshed', {})
onRefresh(sessionId, oauthToken) onRefresh(sessionId, oauthToken)

View File

@@ -50,7 +50,10 @@ import {
extractTitleText, extractTitleText,
BoundedUUIDSet, BoundedUUIDSet,
} from './bridgeMessaging.js' } from './bridgeMessaging.js'
import { logBridgeSkip } from './debugUtils.js' import {
logBridgeSkip,
summarizeBridgeErrorForDebug,
} from './debugUtils.js'
import { logForDebugging } from '../utils/debug.js' import { logForDebugging } from '../utils/debug.js'
import { logForDiagnosticsNoPII } from '../utils/diagLogs.js' import { logForDiagnosticsNoPII } from '../utils/diagLogs.js'
import { isInProtectedNamespace } from '../utils/envUtils.js' import { isInProtectedNamespace } from '../utils/envUtils.js'
@@ -235,10 +238,12 @@ export async function initEnvLessBridgeCore(
}) })
} catch (err) { } catch (err) {
logForDebugging( logForDebugging(
`[remote-bridge] v2 transport setup failed: ${errorMessage(err)}`, `[remote-bridge] v2 transport setup failed: ${summarizeBridgeErrorForDebug(
err,
)}`,
{ level: 'error' }, { level: 'error' },
) )
onStateChange?.('failed', `Transport setup failed: ${errorMessage(err)}`) onStateChange?.('failed', 'Transport setup failed')
logBridgeSkip('v2_transport_setup_failed', undefined, true) logBridgeSkip('v2_transport_setup_failed', undefined, true)
void archiveSession( void archiveSession(
sessionId, sessionId,
@@ -356,7 +361,9 @@ export async function initEnvLessBridgeCore(
) )
} catch (err) { } catch (err) {
logForDebugging( logForDebugging(
`[remote-bridge] Proactive refresh rebuild failed: ${errorMessage(err)}`, `[remote-bridge] Proactive refresh rebuild failed: ${summarizeBridgeErrorForDebug(
err,
)}`,
{ level: 'error' }, { level: 'error' },
) )
logForDiagnosticsNoPII( logForDiagnosticsNoPII(
@@ -364,7 +371,7 @@ export async function initEnvLessBridgeCore(
'bridge_repl_v2_proactive_refresh_failed', 'bridge_repl_v2_proactive_refresh_failed',
) )
if (!tornDown) { if (!tornDown) {
onStateChange?.('failed', `Refresh failed: ${errorMessage(err)}`) onStateChange?.('failed', 'Refresh failed')
} }
} finally { } finally {
authRecoveryInFlight = false authRecoveryInFlight = false
@@ -394,9 +401,13 @@ export async function initEnvLessBridgeCore(
// (Same guard pattern as replBridge.ts:1119.) // (Same guard pattern as replBridge.ts:1119.)
const flushTransport = transport const flushTransport = transport
void flushHistory(initialMessages) void flushHistory(initialMessages)
.catch(e => .catch(e => {
logForDebugging(`[remote-bridge] flushHistory failed: ${e}`), logForDebugging(
`[remote-bridge] flushHistory failed: ${summarizeBridgeErrorForDebug(
e,
)}`,
) )
})
.finally(() => { .finally(() => {
// authRecoveryInFlight catches the v1-vs-v2 asymmetry: v1 nulls // authRecoveryInFlight catches the v1-vs-v2 asymmetry: v1 nulls
// transport synchronously in setOnClose (replBridge.ts:1175), so // transport synchronously in setOnClose (replBridge.ts:1175), so
@@ -576,12 +587,14 @@ export async function initEnvLessBridgeCore(
logForDebugging('[remote-bridge] Transport rebuilt after 401') logForDebugging('[remote-bridge] Transport rebuilt after 401')
} catch (err) { } catch (err) {
logForDebugging( logForDebugging(
`[remote-bridge] 401 recovery failed: ${errorMessage(err)}`, `[remote-bridge] 401 recovery failed: ${summarizeBridgeErrorForDebug(
err,
)}`,
{ level: 'error' }, { level: 'error' },
) )
logForDiagnosticsNoPII('error', 'bridge_repl_v2_jwt_refresh_failed') logForDiagnosticsNoPII('error', 'bridge_repl_v2_jwt_refresh_failed')
if (!tornDown) { if (!tornDown) {
onStateChange?.('failed', `JWT refresh failed: ${errorMessage(err)}`) onStateChange?.('failed', 'JWT refresh failed')
} }
} finally { } finally {
authRecoveryInFlight = false authRecoveryInFlight = false
@@ -706,7 +719,9 @@ export async function initEnvLessBridgeCore(
) )
} catch (err) { } catch (err) {
logForDebugging( logForDebugging(
`[remote-bridge] Teardown 401 retry threw: ${errorMessage(err)}`, `[remote-bridge] Teardown 401 retry threw: ${summarizeBridgeErrorForDebug(
err,
)}`,
{ level: 'error' }, { level: 'error' },
) )
} }
@@ -823,7 +838,7 @@ export async function initEnvLessBridgeCore(
sendControlRequest(request: SDKControlRequest) { sendControlRequest(request: SDKControlRequest) {
if (authRecoveryInFlight) { if (authRecoveryInFlight) {
logForDebugging( logForDebugging(
`[remote-bridge] Dropping control_request during 401 recovery: ${request.request_id}`, '[remote-bridge] Dropping control_request during 401 recovery',
) )
return return
} }
@@ -832,9 +847,7 @@ export async function initEnvLessBridgeCore(
transport.reportState('requires_action') transport.reportState('requires_action')
} }
void transport.write(event) void transport.write(event)
logForDebugging( logForDebugging('[remote-bridge] Sent control_request')
`[remote-bridge] Sent control_request request_id=${request.request_id}`,
)
}, },
sendControlResponse(response: SDKControlResponse) { sendControlResponse(response: SDKControlResponse) {
if (authRecoveryInFlight) { if (authRecoveryInFlight) {
@@ -851,7 +864,7 @@ export async function initEnvLessBridgeCore(
sendControlCancelRequest(requestId: string) { sendControlCancelRequest(requestId: string) {
if (authRecoveryInFlight) { if (authRecoveryInFlight) {
logForDebugging( logForDebugging(
`[remote-bridge] Dropping control_cancel_request during 401 recovery: ${requestId}`, '[remote-bridge] Dropping control_cancel_request during 401 recovery',
) )
return return
} }
@@ -865,9 +878,7 @@ export async function initEnvLessBridgeCore(
// those paths, so without this the server stays on requires_action. // those paths, so without this the server stays on requires_action.
transport.reportState('running') transport.reportState('running')
void transport.write(event) void transport.write(event)
logForDebugging( logForDebugging('[remote-bridge] Sent control_cancel_request')
`[remote-bridge] Sent control_cancel_request request_id=${requestId}`,
)
}, },
sendResult() { sendResult() {
if (authRecoveryInFlight) { if (authRecoveryInFlight) {
@@ -876,7 +887,7 @@ export async function initEnvLessBridgeCore(
} }
transport.reportState('idle') transport.reportState('idle')
void transport.write(makeResultMessage(sessionId)) void transport.write(makeResultMessage(sessionId))
logForDebugging(`[remote-bridge] Sent result`) logForDebugging('[remote-bridge] Sent result')
}, },
async teardown() { async teardown() {
unregister() unregister()
@@ -992,12 +1003,13 @@ async function archiveSession(
}, },
) )
logForDebugging( logForDebugging(
`[remote-bridge] Archive ${compatId} status=${response.status}`, `[remote-bridge] Archive status=${response.status}`,
) )
return response.status return response.status
} catch (err) { } catch (err) {
const msg = errorMessage(err) logForDebugging(
logForDebugging(`[remote-bridge] Archive failed: ${msg}`) `[remote-bridge] Archive failed: ${summarizeBridgeErrorForDebug(err)}`,
)
return axios.isAxiosError(err) && err.code === 'ECONNABORTED' return axios.isAxiosError(err) && err.code === 'ECONNABORTED'
? 'timeout' ? 'timeout'
: 'error' : 'error'

View File

@@ -43,6 +43,7 @@ import {
describeAxiosError, describeAxiosError,
extractHttpStatus, extractHttpStatus,
logBridgeSkip, logBridgeSkip,
summarizeBridgeErrorForDebug,
} 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.ts' import type { SDKMessage } from '../entrypoints/agentSdkTypes.ts'
@@ -303,7 +304,7 @@ export async function initBridgeCore(
const prior = rawPrior?.source === 'repl' ? rawPrior : null const prior = rawPrior?.source === 'repl' ? rawPrior : null
logForDebugging( logForDebugging(
`[bridge:repl] initBridgeCore #${seq} starting (initialMessages=${initialMessages?.length ?? 0}${prior ? ` perpetual prior=env:${prior.environmentId}` : ''})`, `[bridge:repl] initBridgeCore #${seq} starting (initialMessages=${initialMessages?.length ?? 0}${prior ? ' perpetual prior pointer present' : ''})`,
) )
// 5. Register bridge environment // 5. Register bridge environment
@@ -342,7 +343,9 @@ export async function initBridgeCore(
} catch (err) { } catch (err) {
logBridgeSkip( logBridgeSkip(
'registration_failed', 'registration_failed',
`[bridge:repl] Environment registration failed: ${errorMessage(err)}`, `[bridge:repl] Environment registration failed: ${summarizeBridgeErrorForDebug(
err,
)}`,
) )
// Stale pointer may be the cause (expired/deleted env) — clear it so // Stale pointer may be the cause (expired/deleted env) — clear it so
// the next start doesn't retry the same dead ID. // the next start doesn't retry the same dead ID.
@@ -353,7 +356,7 @@ export async function initBridgeCore(
return null return null
} }
logForDebugging(`[bridge:repl] Environment registered: ${environmentId}`) logForDebugging('[bridge:repl] Environment registered')
logForDiagnosticsNoPII('info', 'bridge_repl_env_registered') logForDiagnosticsNoPII('info', 'bridge_repl_env_registered')
logEvent('tengu_bridge_repl_env_registered', {}) logEvent('tengu_bridge_repl_env_registered', {})
@@ -371,7 +374,7 @@ export async function initBridgeCore(
): Promise<boolean> { ): Promise<boolean> {
if (environmentId !== requestedEnvId) { if (environmentId !== requestedEnvId) {
logForDebugging( logForDebugging(
`[bridge:repl] Env mismatch (requested ${requestedEnvId}, got ${environmentId}) — cannot reconnect in place`, '[bridge:repl] Env mismatch — cannot reconnect in place',
) )
return false return false
} }
@@ -389,13 +392,13 @@ export async function initBridgeCore(
for (const id of candidates) { for (const id of candidates) {
try { try {
await api.reconnectSession(environmentId, id) await api.reconnectSession(environmentId, id)
logForDebugging( logForDebugging('[bridge:repl] Reconnected existing session in place')
`[bridge:repl] Reconnected session ${id} in place on env ${environmentId}`,
)
return true return true
} catch (err) { } catch (err) {
logForDebugging( logForDebugging(
`[bridge:repl] reconnectSession(${id}) failed: ${errorMessage(err)}`, `[bridge:repl] reconnectSession failed: ${summarizeBridgeErrorForDebug(
err,
)}`,
) )
} }
} }
@@ -679,7 +682,9 @@ export async function initBridgeCore(
} catch (err) { } catch (err) {
bridgeConfig.reuseEnvironmentId = undefined bridgeConfig.reuseEnvironmentId = undefined
logForDebugging( logForDebugging(
`[bridge:repl] Environment re-registration failed: ${errorMessage(err)}`, `[bridge:repl] Environment re-registration failed: ${summarizeBridgeErrorForDebug(
err,
)}`,
) )
return false return false
} }
@@ -688,7 +693,7 @@ export async function initBridgeCore(
bridgeConfig.reuseEnvironmentId = undefined bridgeConfig.reuseEnvironmentId = undefined
logForDebugging( logForDebugging(
`[bridge:repl] Re-registered: requested=${requestedEnvId} got=${environmentId}`, '[bridge:repl] Re-registered environment',
) )
// Bail out if teardown started while we were registering // Bail out if teardown started while we were registering
@@ -984,7 +989,7 @@ export async function initBridgeCore(
injectFault: injectBridgeFault, injectFault: injectBridgeFault,
wakePollLoop, wakePollLoop,
describe: () => describe: () =>
`env=${environmentId} session=${currentSessionId} transport=${transport?.getStateLabel() ?? 'null'} workId=${currentWorkId ?? 'null'}`, `transport=${transport?.getStateLabel() ?? 'null'} hasSession=${Boolean(currentSessionId)} hasWork=${Boolean(currentWorkId)}`,
}) })
} }
@@ -1038,7 +1043,9 @@ export async function initBridgeCore(
.stopWork(environmentId, currentWorkId, false) .stopWork(environmentId, currentWorkId, false)
.catch((e: unknown) => { .catch((e: unknown) => {
logForDebugging( logForDebugging(
`[bridge:repl] stopWork after heartbeat fatal: ${errorMessage(e)}`, `[bridge:repl] stopWork after heartbeat fatal: ${summarizeBridgeErrorForDebug(
e,
)}`,
) )
}) })
} }
@@ -1365,7 +1372,7 @@ export async function initBridgeCore(
const sessionUrl = buildCCRv2SdkUrl(baseUrl, workSessionId) const sessionUrl = buildCCRv2SdkUrl(baseUrl, workSessionId)
const thisGen = v2Generation const thisGen = v2Generation
logForDebugging( logForDebugging(
`[bridge:repl] CCR v2: creating transport for session=${workSessionId} gen=${thisGen}`, `[bridge:repl] CCR v2: creating transport gen=${thisGen}`,
) )
void createV2ReplTransport({ void createV2ReplTransport({
sessionUrl, sessionUrl,
@@ -1399,7 +1406,9 @@ export async function initBridgeCore(
}, },
(err: unknown) => { (err: unknown) => {
logForDebugging( logForDebugging(
`[bridge:repl] CCR v2: createV2ReplTransport failed: ${errorMessage(err)}`, `[bridge:repl] CCR v2: createV2ReplTransport failed: ${summarizeBridgeErrorForDebug(
err,
)}`,
{ level: 'error' }, { level: 'error' },
) )
logEvent('tengu_bridge_repl_ccr_v2_init_failed', {}) logEvent('tengu_bridge_repl_ccr_v2_init_failed', {})
@@ -1414,7 +1423,9 @@ export async function initBridgeCore(
.stopWork(environmentId, currentWorkId, false) .stopWork(environmentId, currentWorkId, false)
.catch((e: unknown) => { .catch((e: unknown) => {
logForDebugging( logForDebugging(
`[bridge:repl] stopWork after v2 init failure: ${errorMessage(e)}`, `[bridge:repl] stopWork after v2 init failure: ${summarizeBridgeErrorForDebug(
e,
)}`,
) )
}) })
currentWorkId = null currentWorkId = null
@@ -1436,9 +1447,7 @@ export async function initBridgeCore(
// WS reconnect attempt. // WS reconnect attempt.
const wsUrl = buildSdkUrl(sessionIngressUrl, workSessionId) const wsUrl = buildSdkUrl(sessionIngressUrl, workSessionId)
logForDebugging('[bridge:repl] Using session ingress WebSocket endpoint') logForDebugging('[bridge:repl] Using session ingress WebSocket endpoint')
logForDebugging( logForDebugging('[bridge:repl] Creating HybridTransport')
`[bridge:repl] Creating HybridTransport: session=${workSessionId}`,
)
// v1OauthToken was validated non-null above (we'd have returned early). // v1OauthToken was validated non-null above (we'd have returned early).
const oauthToken = v1OauthToken ?? '' const oauthToken = v1OauthToken ?? ''
wireTransport( wireTransport(
@@ -1523,7 +1532,9 @@ export async function initBridgeCore(
logForDebugging('[bridge:repl] keep_alive sent') logForDebugging('[bridge:repl] keep_alive sent')
void transport.write({ type: 'keep_alive' }).catch((err: unknown) => { void transport.write({ type: 'keep_alive' }).catch((err: unknown) => {
logForDebugging( logForDebugging(
`[bridge:repl] keep_alive write failed: ${errorMessage(err)}`, `[bridge:repl] keep_alive write failed: ${summarizeBridgeErrorForDebug(
err,
)}`,
) )
}) })
}, keepAliveIntervalMs) }, keepAliveIntervalMs)
@@ -1536,15 +1547,13 @@ export async function initBridgeCore(
doTeardownImpl = async (): Promise<void> => { doTeardownImpl = async (): Promise<void> => {
if (teardownStarted) { if (teardownStarted) {
logForDebugging( logForDebugging(
`[bridge:repl] Teardown already in progress, skipping duplicate call env=${environmentId} session=${currentSessionId}`, '[bridge:repl] Teardown already in progress, skipping duplicate call',
) )
return return
} }
teardownStarted = true teardownStarted = true
const teardownStart = Date.now() const teardownStart = Date.now()
logForDebugging( logForDebugging('[bridge:repl] Teardown starting')
`[bridge:repl] Teardown starting: env=${environmentId} session=${currentSessionId} workId=${currentWorkId ?? 'none'} transportState=${transport?.getStateLabel() ?? 'null'}`,
)
if (pointerRefreshTimer !== null) { if (pointerRefreshTimer !== null) {
clearInterval(pointerRefreshTimer) clearInterval(pointerRefreshTimer)
@@ -1593,7 +1602,7 @@ export async function initBridgeCore(
source: 'repl', source: 'repl',
}) })
logForDebugging( logForDebugging(
`[bridge:repl] Teardown (perpetual): leaving env=${environmentId} session=${currentSessionId} alive on server, duration=${Date.now() - teardownStart}ms`, `[bridge:repl] Teardown (perpetual): leaving bridge session alive on server, duration=${Date.now() - teardownStart}ms`,
) )
return return
} }
@@ -1619,7 +1628,9 @@ export async function initBridgeCore(
}) })
.catch((err: unknown) => { .catch((err: unknown) => {
logForDebugging( logForDebugging(
`[bridge:repl] Teardown stopWork failed: ${errorMessage(err)}`, `[bridge:repl] Teardown stopWork failed: ${summarizeBridgeErrorForDebug(
err,
)}`,
) )
}) })
: Promise.resolve() : Promise.resolve()
@@ -1636,7 +1647,9 @@ export async function initBridgeCore(
await api.deregisterEnvironment(environmentId).catch((err: unknown) => { await api.deregisterEnvironment(environmentId).catch((err: unknown) => {
logForDebugging( logForDebugging(
`[bridge:repl] Teardown deregister failed: ${errorMessage(err)}`, `[bridge:repl] Teardown deregister failed: ${summarizeBridgeErrorForDebug(
err,
)}`,
) )
}) })
@@ -1646,16 +1659,14 @@ export async function initBridgeCore(
await clearBridgePointer(dir) await clearBridgePointer(dir)
logForDebugging( logForDebugging(
`[bridge:repl] Teardown complete: env=${environmentId} duration=${Date.now() - teardownStart}ms`, `[bridge:repl] Teardown complete: duration=${Date.now() - teardownStart}ms`,
) )
} }
// 8. Register cleanup for graceful shutdown // 8. Register cleanup for graceful shutdown
const unregister = registerCleanup(() => doTeardownImpl?.()) const unregister = registerCleanup(() => doTeardownImpl?.())
logForDebugging( logForDebugging('[bridge:repl] Ready')
`[bridge:repl] Ready: env=${environmentId} session=${currentSessionId}`,
)
onStateChange?.('ready') onStateChange?.('ready')
return { return {
@@ -1713,7 +1724,7 @@ export async function initBridgeCore(
if (!transport) { if (!transport) {
const types = filtered.map(m => m.type).join(',') const types = filtered.map(m => m.type).join(',')
logForDebugging( logForDebugging(
`[bridge:repl] Transport not configured, dropping ${filtered.length} message(s) [${types}] for session=${currentSessionId}`, `[bridge:repl] Transport not configured, dropping ${filtered.length} message(s) [${types}]`,
{ level: 'warn' }, { level: 'warn' },
) )
return return
@@ -1748,7 +1759,7 @@ export async function initBridgeCore(
if (filtered.length === 0) return if (filtered.length === 0) return
if (!transport) { if (!transport) {
logForDebugging( logForDebugging(
`[bridge:repl] Transport not configured, dropping ${filtered.length} SDK message(s) for session=${currentSessionId}`, `[bridge:repl] Transport not configured, dropping ${filtered.length} SDK message(s)`,
{ level: 'warn' }, { level: 'warn' },
) )
return return
@@ -1768,9 +1779,7 @@ export async function initBridgeCore(
} }
const event = { ...request, session_id: currentSessionId } const event = { ...request, session_id: currentSessionId }
void transport.write(event) void transport.write(event)
logForDebugging( logForDebugging('[bridge:repl] Sent control_request')
`[bridge:repl] Sent control_request request_id=${request.request_id}`,
)
}, },
sendControlResponse(response: SDKControlResponse) { sendControlResponse(response: SDKControlResponse) {
if (!transport) { if (!transport) {
@@ -1796,21 +1805,17 @@ export async function initBridgeCore(
session_id: currentSessionId, session_id: currentSessionId,
} }
void transport.write(event) void transport.write(event)
logForDebugging( logForDebugging('[bridge:repl] Sent control_cancel_request')
`[bridge:repl] Sent control_cancel_request request_id=${requestId}`,
)
}, },
sendResult() { sendResult() {
if (!transport) { if (!transport) {
logForDebugging( logForDebugging(
`[bridge:repl] sendResult: skipping, transport not configured session=${currentSessionId}`, '[bridge:repl] sendResult: skipping, transport not configured',
) )
return return
} }
void transport.write(makeResultMessage(currentSessionId)) void transport.write(makeResultMessage(currentSessionId))
logForDebugging( logForDebugging('[bridge:repl] Sent result')
`[bridge:repl] Sent result for session=${currentSessionId}`,
)
}, },
async teardown() { async teardown() {
unregister() unregister()
@@ -1903,7 +1908,7 @@ async function startWorkPollLoop({
const MAX_ENVIRONMENT_RECREATIONS = 3 const MAX_ENVIRONMENT_RECREATIONS = 3
logForDebugging( logForDebugging(
`[bridge:repl] Starting work poll loop for env=${getCredentials().environmentId}`, '[bridge:repl] Starting work poll loop',
) )
let consecutiveErrors = 0 let consecutiveErrors = 0
@@ -2006,7 +2011,9 @@ async function startWorkPollLoop({
) )
} catch (err) { } catch (err) {
logForDebugging( logForDebugging(
`[bridge:repl:heartbeat] Failed: ${errorMessage(err)}`, `[bridge:repl:heartbeat] Failed: ${summarizeBridgeErrorForDebug(
err,
)}`,
) )
if (err instanceof BridgeFatalError) { if (err instanceof BridgeFatalError) {
cap.cleanup() cap.cleanup()
@@ -2124,7 +2131,9 @@ async function startWorkPollLoop({
secret = decodeWorkSecret(work.secret) secret = decodeWorkSecret(work.secret)
} catch (err) { } catch (err) {
logForDebugging( logForDebugging(
`[bridge:repl] Failed to decode work secret: ${errorMessage(err)}`, `[bridge:repl] Failed to decode work secret: ${summarizeBridgeErrorForDebug(
err,
)}`,
) )
logEvent('tengu_bridge_repl_work_secret_failed', {}) logEvent('tengu_bridge_repl_work_secret_failed', {})
// Can't ack (needs the JWT we failed to decode). stopWork uses OAuth. // Can't ack (needs the JWT we failed to decode). stopWork uses OAuth.
@@ -2135,12 +2144,14 @@ async function startWorkPollLoop({
// Explicitly acknowledge to prevent redelivery. Non-fatal on failure: // Explicitly acknowledge to prevent redelivery. Non-fatal on failure:
// server re-delivers, and the onWorkReceived callback handles dedup. // server re-delivers, and the onWorkReceived callback handles dedup.
logForDebugging(`[bridge:repl] Acknowledging workId=${work.id}`) logForDebugging('[bridge:repl] Acknowledging work item')
try { try {
await api.acknowledgeWork(envId, work.id, secret.session_ingress_token) await api.acknowledgeWork(envId, work.id, secret.session_ingress_token)
} catch (err) { } catch (err) {
logForDebugging( logForDebugging(
`[bridge:repl] Acknowledge failed workId=${work.id}: ${errorMessage(err)}`, `[bridge:repl] Acknowledge failed: ${summarizeBridgeErrorForDebug(
err,
)}`,
) )
} }
@@ -2192,7 +2203,7 @@ async function startWorkPollLoop({
const currentEnvId = getCredentials().environmentId const currentEnvId = getCredentials().environmentId
if (envId !== currentEnvId) { if (envId !== currentEnvId) {
logForDebugging( logForDebugging(
`[bridge:repl] Stale poll error for old env=${envId}, current env=${currentEnvId} — skipping onEnvironmentLost`, '[bridge:repl] Stale poll error for superseded environment — skipping onEnvironmentLost',
) )
consecutiveErrors = 0 consecutiveErrors = 0
firstErrorTime = null firstErrorTime = null
@@ -2238,9 +2249,7 @@ async function startWorkPollLoop({
consecutiveErrors = 0 consecutiveErrors = 0
firstErrorTime = null firstErrorTime = null
onStateChange?.('ready') onStateChange?.('ready')
logForDebugging( logForDebugging('[bridge:repl] Re-registered environment')
`[bridge:repl] Re-registered environment: ${newCreds.environmentId}`,
)
continue continue
} }
@@ -2376,7 +2385,7 @@ async function startWorkPollLoop({
} }
logForDebugging( logForDebugging(
`[bridge:repl] Work poll loop ended (aborted=${signal.aborted}) env=${getCredentials().environmentId}`, `[bridge:repl] Work poll loop ended (aborted=${signal.aborted})`,
) )
} }

View File

@@ -2,6 +2,33 @@ import axios from 'axios'
import { jsonParse, jsonStringify } from '../utils/slowOperations.js' import { jsonParse, jsonStringify } from '../utils/slowOperations.js'
import type { WorkSecret } from './types.js' import type { WorkSecret } from './types.js'
function summarizeRegisterWorkerResponseForDebug(data: unknown): string {
if (data === null) return 'null'
if (data === undefined) return 'undefined'
if (Array.isArray(data)) {
return jsonStringify({
payloadType: 'array',
length: data.length,
})
}
if (typeof data === 'object') {
const value = data as Record<string, unknown>
return jsonStringify({
payloadType: 'object',
keys: Object.keys(value)
.sort()
.slice(0, 10),
hasWorkerEpoch:
typeof value.worker_epoch === 'number' ||
typeof value.worker_epoch === 'string',
hasSessionIngressToken:
typeof value.session_ingress_token === 'string',
hasApiBaseUrl: typeof value.api_base_url === 'string',
})
}
return typeof data
}
/** Decode a base64url-encoded work secret and validate its version. */ /** Decode a base64url-encoded work secret and validate its version. */
export function decodeWorkSecret(secret: string): WorkSecret { export function decodeWorkSecret(secret: string): WorkSecret {
const json = Buffer.from(secret, 'base64url').toString('utf-8') const json = Buffer.from(secret, 'base64url').toString('utf-8')
@@ -120,7 +147,9 @@ export async function registerWorker(
!Number.isSafeInteger(epoch) !Number.isSafeInteger(epoch)
) { ) {
throw new Error( throw new Error(
`registerWorker: invalid worker_epoch in response: ${jsonStringify(response.data)}`, `registerWorker: invalid worker_epoch in response: ${summarizeRegisterWorkerResponseForDebug(
response.data,
)}`,
) )
} }
return epoch return epoch

View File

@@ -47,6 +47,119 @@ export type TeleportResult = {
export type TeleportProgressStep = 'validating' | 'fetching_logs' | 'fetching_branch' | 'checking_out' | 'done'; export type TeleportProgressStep = 'validating' | 'fetching_logs' | 'fetching_branch' | 'checking_out' | 'done';
export type TeleportProgressCallback = (step: TeleportProgressStep) => void; export type TeleportProgressCallback = (step: TeleportProgressStep) => void;
function summarizeTeleportPayloadForDebug(payload: {
title: string;
events: Array<{
type: string;
data?: Record<string, unknown>;
}>;
session_context: {
sources?: Array<{
type?: string;
}>;
outcomes?: Array<{
type?: string;
}>;
environment_variables?: Record<string, string>;
seed_bundle_file_id?: string;
model?: string;
reuse_outcome_branches?: boolean;
github_pr?: unknown;
};
environment_id: string;
}): string {
const eventTypes = payload.events.map(event => {
if (event.type !== 'event') return event.type;
const request =
event.data && typeof event.data.request === 'object' && event.data.request
? (event.data.request as Record<string, unknown>)
: undefined;
const subtype =
request && typeof request.subtype === 'string' ? request.subtype : undefined;
const dataType =
event.data && typeof event.data.type === 'string' ? event.data.type : 'unknown';
return subtype ? `event:${dataType}:${subtype}` : `event:${dataType}`;
});
return jsonStringify({
titleLength: payload.title.length,
eventCount: payload.events.length,
eventTypes,
sourceTypes: (payload.session_context.sources ?? []).map(source => source.type ?? 'unknown'),
outcomeTypes: (payload.session_context.outcomes ?? []).map(outcome => outcome.type ?? 'unknown'),
envVarCount: Object.keys(payload.session_context.environment_variables ?? {}).length,
hasSeedBundle: typeof payload.session_context.seed_bundle_file_id === 'string',
hasModel: typeof payload.session_context.model === 'string',
reuseOutcomeBranches: Boolean(payload.session_context.reuse_outcome_branches),
hasGithubPr: Boolean(payload.session_context.github_pr),
hasEnvironmentId: Boolean(payload.environment_id)
});
}
function summarizeTeleportResponseDataForDebug(data: unknown): string {
if (data === null) return 'null';
if (data === undefined) return 'undefined';
if (Array.isArray(data)) {
return jsonStringify({
payloadType: 'array',
length: data.length
});
}
if (typeof data === 'object') {
const value = data as Record<string, unknown>;
return jsonStringify({
payloadType: 'object',
keys: Object.keys(value).sort().slice(0, 12),
hasId: typeof value.id === 'string',
hasTitle: typeof value.title === 'string',
hasSessionStatus: typeof value.session_status === 'string',
loglinesCount: Array.isArray(value.loglines) ? value.loglines.length : 0,
dataCount: Array.isArray(value.data) ? value.data.length : 0
});
}
return typeof data;
}
function summarizeTeleportEnvironmentsForDebug(environments: Array<{
kind: string;
}>): string {
const kinds = [...new Set(environments.map(environment => environment.kind))].sort();
return jsonStringify({
count: environments.length,
kinds,
hasAnthropicCloud: kinds.includes('anthropic_cloud'),
hasBridge: kinds.includes('bridge')
});
}
function summarizeTeleportErrorForDebug(error: unknown): string {
const err = toError(error);
const summary: Record<string, unknown> = {
errorType: err.constructor.name,
errorName: err.name,
hasMessage: err.message.length > 0,
hasStack: Boolean(err.stack)
};
if (error && typeof error === 'object') {
const errorObj = error as Record<string, unknown>;
if (typeof errorObj.code === 'string' || typeof errorObj.code === 'number') {
summary.code = errorObj.code;
}
if (typeof errorObj.status === 'number') {
summary.status = errorObj.status;
}
if (errorObj.response && typeof errorObj.response === 'object') {
const response = errorObj.response as Record<string, unknown>;
if (typeof response.status === 'number') {
summary.httpStatus = response.status;
}
if ('data' in response) {
summary.response = summarizeTeleportResponseDataForDebug(response.data);
}
}
}
return jsonStringify(summary);
}
/** /**
* Creates a system message to inform about teleport session resume * Creates a system message to inform about teleport session resume
* @returns SystemMessage indicating session was resumed from another machine * @returns SystemMessage indicating session was resumed from another machine
@@ -702,7 +815,7 @@ export async function pollRemoteSessionEvents(sessionId: string, afterId: string
branch = getBranchFromSession(sessionData); branch = getBranchFromSession(sessionData);
sessionStatus = sessionData.session_status as PollRemoteSessionResponse['sessionStatus']; sessionStatus = sessionData.session_status as PollRemoteSessionResponse['sessionStatus'];
} catch (e) { } catch (e) {
logForDebugging(`teleport: failed to fetch session ${sessionId} metadata: ${e}`, { logForDebugging(`teleport: failed to fetch session metadata: ${summarizeTeleportErrorForDebug(e)}`, {
level: 'debug' level: 'debug'
}); });
} }
@@ -877,18 +990,23 @@ export async function teleportToRemote(options: {
}, },
environment_id: options.environmentId environment_id: options.environmentId
}; };
logForDebugging(`[teleportToRemote] explicit env ${options.environmentId}, ${Object.keys(envVars).length} env vars, ${seedBundleFileId ? `bundle=${seedBundleFileId}` : `source=${gitSource?.url ?? 'none'}@${options.branchName ?? 'default'}`}`); logForDebugging(`[teleportToRemote] explicit environment request ${jsonStringify({
envVarCount: Object.keys(envVars).length,
hasBundle: Boolean(seedBundleFileId),
hasGitSource: Boolean(gitSource),
hasBranchOverride: Boolean(options.branchName)
})}`);
const response = await axios.post(url, requestBody, { const response = await axios.post(url, requestBody, {
headers, headers,
signal signal
}); });
if (response.status !== 200 && response.status !== 201) { if (response.status !== 200 && response.status !== 201) {
logError(new Error(`CreateSession ${response.status}: ${jsonStringify(response.data)}`)); logError(new Error(`CreateSession ${response.status}: ${summarizeTeleportResponseDataForDebug(response.data)}`));
return null; return null;
} }
const sessionData = response.data as SessionResource; const sessionData = response.data as SessionResource;
if (!sessionData || typeof sessionData.id !== 'string') { if (!sessionData || typeof sessionData.id !== 'string') {
logError(new Error(`No session id in response: ${jsonStringify(response.data)}`)); logError(new Error(`No session id in response: ${summarizeTeleportResponseDataForDebug(response.data)}`));
return null; return null;
} }
return { return {
@@ -969,7 +1087,10 @@ export async function teleportToRemote(options: {
} = repoInfo; } = repoInfo;
// Resolve the base branch: prefer explicit branchName, fall back to default branch // Resolve the base branch: prefer explicit branchName, fall back to default branch
const revision = options.branchName ?? (await getDefaultBranch()) ?? undefined; const revision = options.branchName ?? (await getDefaultBranch()) ?? undefined;
logForDebugging(`[teleportToRemote] Git source: ${host}/${owner}/${name}, revision: ${revision ?? 'none'}`); logForDebugging(`[teleportToRemote] Git source selected ${jsonStringify({
hasRevision: Boolean(revision),
allowUnrestrictedPush: Boolean(options.reuseOutcomeBranch)
})}`);
gitSource = { gitSource = {
type: 'git_repository', type: 'git_repository',
url: `https://${host}/${owner}/${name}`, url: `https://${host}/${owner}/${name}`,
@@ -1057,7 +1178,7 @@ export async function teleportToRemote(options: {
logError(new Error('No environments available for session creation')); logError(new Error('No environments available for session creation'));
return null; return null;
} }
logForDebugging(`Available environments: ${environments.map(e => `${e.environment_id} (${e.name}, ${e.kind})`).join(', ')}`); logForDebugging(`Available environments: ${summarizeTeleportEnvironmentsForDebug(environments)}`);
// Select environment based on settings, then anthropic_cloud preference, then first available. // Select environment based on settings, then anthropic_cloud preference, then first available.
// Prefer anthropic_cloud environments over byoc: anthropic_cloud environments (e.g. "Default") // Prefer anthropic_cloud environments over byoc: anthropic_cloud environments (e.g. "Default")
@@ -1075,7 +1196,7 @@ export async function teleportToRemote(options: {
const retried = await fetchEnvironments(); const retried = await fetchEnvironments();
cloudEnv = retried?.find(env => env.kind === 'anthropic_cloud'); cloudEnv = retried?.find(env => env.kind === 'anthropic_cloud');
if (!cloudEnv) { if (!cloudEnv) {
logError(new Error(`No anthropic_cloud environment available after retry (got: ${(retried ?? environments).map(e => `${e.name} (${e.kind})`).join(', ')}). Silent byoc fallthrough would launch into a dead env — fail fast instead.`)); logError(new Error(`No anthropic_cloud environment available after retry (${summarizeTeleportEnvironmentsForDebug(retried ?? environments)}). Silent byoc fallthrough would launch into a dead env — fail fast instead.`));
return null; return null;
} }
if (retried) environments = retried; if (retried) environments = retried;
@@ -1087,10 +1208,13 @@ export async function teleportToRemote(options: {
} }
if (defaultEnvironmentId) { if (defaultEnvironmentId) {
const matchedDefault = selectedEnvironment.environment_id === defaultEnvironmentId; const matchedDefault = selectedEnvironment.environment_id === defaultEnvironmentId;
logForDebugging(matchedDefault ? `Using configured default environment: ${defaultEnvironmentId}` : `Configured default environment ${defaultEnvironmentId} not found, using first available`); logForDebugging(matchedDefault ? 'Using configured default environment' : 'Configured default environment not found, using fallback environment');
} }
const environmentId = selectedEnvironment.environment_id; const environmentId = selectedEnvironment.environment_id;
logForDebugging(`Selected environment: ${environmentId} (${selectedEnvironment.name}, ${selectedEnvironment.kind})`); logForDebugging(`Selected environment: ${jsonStringify({
kind: selectedEnvironment.kind,
usedConfiguredDefault: Boolean(defaultEnvironmentId && selectedEnvironment.environment_id === defaultEnvironmentId)
})}`);
// Prepare API request for Sessions API // Prepare API request for Sessions API
const url = `${getOauthConfig().BASE_API_URL}/v1/sessions`; const url = `${getOauthConfig().BASE_API_URL}/v1/sessions`;
@@ -1158,7 +1282,7 @@ export async function teleportToRemote(options: {
session_context: sessionContext, session_context: sessionContext,
environment_id: environmentId environment_id: environmentId
}; };
logForDebugging(`Creating session with payload: ${jsonStringify(requestBody, null, 2)}`); logForDebugging(`Creating session with payload summary: ${summarizeTeleportPayloadForDebug(requestBody)}`);
// Make API call // Make API call
const response = await axios.post(url, requestBody, { const response = await axios.post(url, requestBody, {
@@ -1167,17 +1291,17 @@ export async function teleportToRemote(options: {
}); });
const isSuccess = response.status === 200 || response.status === 201; const isSuccess = response.status === 200 || response.status === 201;
if (!isSuccess) { if (!isSuccess) {
logError(new Error(`API request failed with status ${response.status}: ${response.statusText}\n\nResponse data: ${jsonStringify(response.data, null, 2)}`)); logError(new Error(`API request failed with status ${response.status}: ${summarizeTeleportResponseDataForDebug(response.data)}`));
return null; return null;
} }
// Parse response as SessionResource // Parse response as SessionResource
const sessionData = response.data as SessionResource; const sessionData = response.data as SessionResource;
if (!sessionData || typeof sessionData.id !== 'string') { if (!sessionData || typeof sessionData.id !== 'string') {
logError(new Error(`Cannot determine session ID from API response: ${jsonStringify(response.data)}`)); logError(new Error(`Cannot determine session ID from API response: ${summarizeTeleportResponseDataForDebug(response.data)}`));
return null; return null;
} }
logForDebugging(`Successfully created remote session: ${sessionData.id}`); logForDebugging('Successfully created remote session');
return { return {
id: sessionData.id, id: sessionData.id,
title: sessionData.title || requestBody.title title: sessionData.title || requestBody.title
@@ -1215,9 +1339,9 @@ export async function archiveRemoteSession(sessionId: string): Promise<void> {
validateStatus: s => s < 500 validateStatus: s => s < 500
}); });
if (resp.status === 200 || resp.status === 409) { if (resp.status === 200 || resp.status === 409) {
logForDebugging(`[archiveRemoteSession] archived ${sessionId}`); logForDebugging('[archiveRemoteSession] archived remote session');
} else { } else {
logForDebugging(`[archiveRemoteSession] ${sessionId} failed ${resp.status}: ${jsonStringify(resp.data)}`); logForDebugging(`[archiveRemoteSession] archive failed status=${resp.status} ${summarizeTeleportResponseDataForDebug(resp.data)}`);
} }
} catch (err) { } catch (err) {
logError(err); logError(err);