mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-17 22:05:50 +00:00
feat(remote-control): 优化 Web 展示、状态同步与桥接控制流程 (#288)
Co-authored-by: chengzifeng <chengzifeng@meituan.com>
This commit is contained in:
96
src/bridge/__tests__/bridgeMessaging.test.ts
Normal file
96
src/bridge/__tests__/bridgeMessaging.test.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
import { describe, expect, test } from 'bun:test'
|
||||
|
||||
import {
|
||||
shouldReportRunningForMessage,
|
||||
shouldReportRunningForMessages,
|
||||
} from '../bridgeMessaging.js'
|
||||
import { createUserMessage } from '../../utils/messages.js'
|
||||
|
||||
describe('bridge running-state classification', () => {
|
||||
test('treats real user prompts as turn-starting work', () => {
|
||||
expect(
|
||||
shouldReportRunningForMessage(
|
||||
createUserMessage({ content: 'please inspect the repo' }),
|
||||
),
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
test('keeps tool-result style user messages eligible during mid-turn attach', () => {
|
||||
expect(
|
||||
shouldReportRunningForMessage(
|
||||
createUserMessage({
|
||||
content: '<local-command-stdout>done</local-command-stdout>',
|
||||
toolUseResult: { ok: true },
|
||||
}),
|
||||
),
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
test('ignores local slash-command scaffolding that should not reopen a turn', () => {
|
||||
expect(
|
||||
shouldReportRunningForMessage(
|
||||
createUserMessage({
|
||||
content:
|
||||
'<local-command-caveat>Caveat: hidden local command scaffolding</local-command-caveat>',
|
||||
isMeta: true,
|
||||
}),
|
||||
),
|
||||
).toBe(false)
|
||||
|
||||
expect(
|
||||
shouldReportRunningForMessage(
|
||||
createUserMessage({
|
||||
content:
|
||||
'<system-reminder>\nProactive mode is now enabled. You will receive periodic <tick> prompts.\n</system-reminder>',
|
||||
isMeta: true,
|
||||
}),
|
||||
),
|
||||
).toBe(false)
|
||||
})
|
||||
|
||||
test('still marks real automation triggers as running', () => {
|
||||
expect(
|
||||
shouldReportRunningForMessage(
|
||||
createUserMessage({
|
||||
content: '<tick>2:56:47 PM</tick>',
|
||||
isMeta: true,
|
||||
}),
|
||||
),
|
||||
).toBe(true)
|
||||
|
||||
expect(
|
||||
shouldReportRunningForMessage(
|
||||
createUserMessage({
|
||||
content: 'scheduled job: refresh analytics cache',
|
||||
isMeta: true,
|
||||
}),
|
||||
),
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
test('classifies batches by any work-starting message', () => {
|
||||
const scaffoldingOnly = [
|
||||
createUserMessage({
|
||||
content:
|
||||
'<local-command-caveat>Caveat: hidden local command scaffolding</local-command-caveat>',
|
||||
isMeta: true,
|
||||
}),
|
||||
createUserMessage({
|
||||
content:
|
||||
'<system-reminder>\nProactive mode is now enabled.\n</system-reminder>',
|
||||
isMeta: true,
|
||||
}),
|
||||
]
|
||||
expect(shouldReportRunningForMessages(scaffoldingOnly)).toBe(false)
|
||||
|
||||
expect(
|
||||
shouldReportRunningForMessages([
|
||||
...scaffoldingOnly,
|
||||
createUserMessage({
|
||||
content: '<tick>2:57:17 PM</tick>',
|
||||
isMeta: true,
|
||||
}),
|
||||
]),
|
||||
).toBe(true)
|
||||
})
|
||||
})
|
||||
76
src/bridge/__tests__/bridgePermissionCallbacks.test.ts
Normal file
76
src/bridge/__tests__/bridgePermissionCallbacks.test.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import { describe, expect, test } from 'bun:test'
|
||||
|
||||
import { parseBridgePermissionResponse } from '../bridgePermissionCallbacks.js'
|
||||
import type { SDKControlResponse } from '../../entrypoints/sdk/controlTypes.js'
|
||||
|
||||
describe('parseBridgePermissionResponse', () => {
|
||||
test('passes through allow responses', () => {
|
||||
const message: SDKControlResponse = {
|
||||
type: 'control_response',
|
||||
response: {
|
||||
subtype: 'success',
|
||||
request_id: 'req-1',
|
||||
response: {
|
||||
behavior: 'allow',
|
||||
updatedPermissions: [
|
||||
{ type: 'setMode', mode: 'acceptEdits', destination: 'session' },
|
||||
],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
expect(parseBridgePermissionResponse(message)).toEqual({
|
||||
behavior: 'allow',
|
||||
updatedPermissions: [
|
||||
{ type: 'setMode', mode: 'acceptEdits', destination: 'session' },
|
||||
],
|
||||
})
|
||||
})
|
||||
|
||||
test('maps error responses with feedback to deny', () => {
|
||||
const message = {
|
||||
type: 'control_response',
|
||||
response: {
|
||||
subtype: 'error',
|
||||
request_id: 'req-2',
|
||||
error: 'Permission denied by user',
|
||||
response: { behavior: 'deny' },
|
||||
message: 'Need more detail',
|
||||
},
|
||||
} as unknown as SDKControlResponse
|
||||
|
||||
expect(parseBridgePermissionResponse(message)).toEqual({
|
||||
behavior: 'deny',
|
||||
message: 'Need more detail',
|
||||
})
|
||||
})
|
||||
|
||||
test('falls back to error text when deny feedback is absent', () => {
|
||||
const message = {
|
||||
type: 'control_response',
|
||||
response: {
|
||||
subtype: 'error',
|
||||
request_id: 'req-3',
|
||||
error: 'Permission denied by user',
|
||||
},
|
||||
} as unknown as SDKControlResponse
|
||||
|
||||
expect(parseBridgePermissionResponse(message)).toEqual({
|
||||
behavior: 'deny',
|
||||
message: 'Permission denied by user',
|
||||
})
|
||||
})
|
||||
|
||||
test('returns null for unrelated control responses', () => {
|
||||
const message = {
|
||||
type: 'control_response',
|
||||
response: {
|
||||
subtype: 'error',
|
||||
request_id: 'req-4',
|
||||
error: '',
|
||||
},
|
||||
} as unknown as SDKControlResponse
|
||||
|
||||
expect(parseBridgePermissionResponse(message)).toBeNull()
|
||||
})
|
||||
})
|
||||
53
src/bridge/__tests__/bridgeResultScheduling.test.ts
Normal file
53
src/bridge/__tests__/bridgeResultScheduling.test.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import { describe, expect, test } from 'bun:test'
|
||||
|
||||
import {
|
||||
hasPendingBridgeMessages,
|
||||
isTranscriptResetResultReady,
|
||||
shouldDeferBridgeResult,
|
||||
} from '../bridgeResultScheduling.js'
|
||||
|
||||
describe('bridgeResultScheduling', () => {
|
||||
test('detects pending mirrored messages', () => {
|
||||
expect(hasPendingBridgeMessages(2, 3)).toBe(true)
|
||||
expect(hasPendingBridgeMessages(3, 3)).toBe(false)
|
||||
})
|
||||
|
||||
test('defers when the bridge handle is unavailable', () => {
|
||||
expect(
|
||||
shouldDeferBridgeResult({
|
||||
hasHandle: false,
|
||||
isConnected: true,
|
||||
lastWrittenIndex: 3,
|
||||
messageCount: 3,
|
||||
}),
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
test('defers when the bridge is connected but transcript flush is pending', () => {
|
||||
expect(
|
||||
shouldDeferBridgeResult({
|
||||
hasHandle: true,
|
||||
isConnected: true,
|
||||
lastWrittenIndex: 1,
|
||||
messageCount: 2,
|
||||
}),
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
test('sends immediately once the latest transcript is already mirrored', () => {
|
||||
expect(
|
||||
shouldDeferBridgeResult({
|
||||
hasHandle: true,
|
||||
isConnected: true,
|
||||
lastWrittenIndex: 2,
|
||||
messageCount: 2,
|
||||
}),
|
||||
).toBe(false)
|
||||
})
|
||||
|
||||
test('treats transcript reset as ready only after the transcript is empty', () => {
|
||||
expect(isTranscriptResetResultReady(true, 0)).toBe(true)
|
||||
expect(isTranscriptResetResultReady(true, 1)).toBe(false)
|
||||
expect(isTranscriptResetResultReady(false, 0)).toBe(false)
|
||||
})
|
||||
})
|
||||
37
src/bridge/__tests__/remoteInterruptHandling.test.ts
Normal file
37
src/bridge/__tests__/remoteInterruptHandling.test.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { feature } from 'bun:bundle'
|
||||
import { afterEach, describe, expect, test } from 'bun:test'
|
||||
|
||||
import { handleRemoteInterrupt } from '../remoteInterruptHandling.js'
|
||||
import {
|
||||
activateProactive,
|
||||
deactivateProactive,
|
||||
isProactivePaused,
|
||||
} from '../../proactive/index.js'
|
||||
|
||||
function isProactiveFeatureEnabled() {
|
||||
if (feature('PROACTIVE')) return true
|
||||
return feature('KAIROS') ? true : false
|
||||
}
|
||||
|
||||
describe('handleRemoteInterrupt', () => {
|
||||
afterEach(() => {
|
||||
deactivateProactive()
|
||||
})
|
||||
|
||||
test('always aborts the active request', () => {
|
||||
const controller = new AbortController()
|
||||
|
||||
handleRemoteInterrupt(controller)
|
||||
|
||||
expect(controller.signal.aborted).toBe(true)
|
||||
})
|
||||
|
||||
test('pauses proactive mode to return control to the user', () => {
|
||||
activateProactive('test')
|
||||
expect(isProactivePaused()).toBe(false)
|
||||
|
||||
handleRemoteInterrupt(new AbortController())
|
||||
|
||||
expect(isProactivePaused()).toBe(isProactiveFeatureEnabled())
|
||||
})
|
||||
})
|
||||
@@ -28,6 +28,18 @@ import { errorMessage } from '../utils/errors.js'
|
||||
import type { PermissionMode } from '../utils/permissions/PermissionMode.js'
|
||||
import { jsonParse } from '../utils/slowOperations.js'
|
||||
import type { ReplBridgeTransport } from './replBridgeTransport.js'
|
||||
import {
|
||||
BASH_INPUT_TAG,
|
||||
CHANNEL_MESSAGE_TAG,
|
||||
CROSS_SESSION_MESSAGE_TAG,
|
||||
LOCAL_COMMAND_CAVEAT_TAG,
|
||||
REMOTE_REVIEW_PROGRESS_TAG,
|
||||
REMOTE_REVIEW_TAG,
|
||||
TASK_NOTIFICATION_TAG,
|
||||
TEAMMATE_MESSAGE_TAG,
|
||||
TICK_TAG,
|
||||
ULTRAPLAN_TAG,
|
||||
} from '../constants/xml.js'
|
||||
|
||||
// ─── Type guards ─────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -122,6 +134,85 @@ export function extractTitleText(m: Message): string | undefined {
|
||||
return clean || undefined
|
||||
}
|
||||
|
||||
const SYSTEM_REMINDER_TAG = 'system-reminder'
|
||||
const XML_BLOCK_PATTERN =
|
||||
/\s*<([a-z][\w-]*)(?:\s[^>]*)?>[\s\S]*?<\/\1>\s*/gy
|
||||
const RUNNING_STATE_META_TAGS = new Set([
|
||||
BASH_INPUT_TAG,
|
||||
CHANNEL_MESSAGE_TAG,
|
||||
CROSS_SESSION_MESSAGE_TAG,
|
||||
REMOTE_REVIEW_PROGRESS_TAG,
|
||||
REMOTE_REVIEW_TAG,
|
||||
TASK_NOTIFICATION_TAG,
|
||||
TEAMMATE_MESSAGE_TAG,
|
||||
TICK_TAG,
|
||||
ULTRAPLAN_TAG,
|
||||
])
|
||||
|
||||
function extractUserMessageText(message: Message): string {
|
||||
const content = message.message?.content
|
||||
if (typeof content === 'string') return content
|
||||
if (!Array.isArray(content)) return ''
|
||||
return content
|
||||
.filter(
|
||||
(
|
||||
block,
|
||||
): block is {
|
||||
type: 'text'
|
||||
text: string
|
||||
} =>
|
||||
!!block &&
|
||||
typeof block === 'object' &&
|
||||
block.type === 'text' &&
|
||||
typeof block.text === 'string',
|
||||
)
|
||||
.map(block => block.text)
|
||||
.join('')
|
||||
}
|
||||
|
||||
function getEnvelopeTagNames(text: string): string[] | null {
|
||||
const trimmed = text.trim()
|
||||
if (!trimmed) return null
|
||||
XML_BLOCK_PATTERN.lastIndex = 0
|
||||
const tags: string[] = []
|
||||
while (XML_BLOCK_PATTERN.lastIndex < trimmed.length) {
|
||||
const match = XML_BLOCK_PATTERN.exec(trimmed)
|
||||
if (!match) return null
|
||||
tags.push(match[1]!)
|
||||
}
|
||||
return tags.length > 0 ? tags : null
|
||||
}
|
||||
|
||||
/**
|
||||
* Remote Control uses user messages to infer "a turn is actively running" in
|
||||
* places where the server does not derive that state for us. Hidden local
|
||||
* slash-command scaffolding (for example `<local-command-caveat>` and pure
|
||||
* `<system-reminder>` wrappers from `/proactive`) should not flip the session
|
||||
* back to running after the command has already completed.
|
||||
*/
|
||||
export function shouldReportRunningForMessage(message: Message): boolean {
|
||||
if (message.type !== 'user') return false
|
||||
if (message.isVisibleInTranscriptOnly) return false
|
||||
if (message.toolUseResult !== undefined) return true
|
||||
if (!message.isMeta) return true
|
||||
|
||||
const tags = getEnvelopeTagNames(extractUserMessageText(message))
|
||||
if (!tags) return true
|
||||
|
||||
return tags.some(
|
||||
tag =>
|
||||
tag !== LOCAL_COMMAND_CAVEAT_TAG &&
|
||||
tag !== SYSTEM_REMINDER_TAG &&
|
||||
RUNNING_STATE_META_TAGS.has(tag),
|
||||
)
|
||||
}
|
||||
|
||||
export function shouldReportRunningForMessages(
|
||||
messages: readonly Message[],
|
||||
): boolean {
|
||||
return messages.some(shouldReportRunningForMessage)
|
||||
}
|
||||
|
||||
// ─── Ingress routing ─────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { PermissionUpdate } from '../utils/permissions/PermissionUpdateSchema.js'
|
||||
import type { SDKControlResponse } from '../entrypoints/sdk/controlTypes.js'
|
||||
|
||||
type BridgePermissionResponse = {
|
||||
behavior: 'allow' | 'deny'
|
||||
@@ -39,5 +40,65 @@ function isBridgePermissionResponse(
|
||||
)
|
||||
}
|
||||
|
||||
export { isBridgePermissionResponse }
|
||||
function toBridgePermissionMessage(
|
||||
controlResponse: Record<string, unknown>,
|
||||
parsed: BridgePermissionResponse | undefined,
|
||||
): string | undefined {
|
||||
if (typeof controlResponse.message === 'string' && controlResponse.message) {
|
||||
return controlResponse.message
|
||||
}
|
||||
if (typeof parsed?.message === 'string' && parsed.message) {
|
||||
return parsed.message
|
||||
}
|
||||
if (typeof controlResponse.error === 'string' && controlResponse.error) {
|
||||
return controlResponse.error
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize a control_response from the bridge transport into the simplified
|
||||
* allow/deny shape used by interactive permission handlers.
|
||||
*/
|
||||
function parseBridgePermissionResponse(
|
||||
message: SDKControlResponse,
|
||||
): BridgePermissionResponse | null {
|
||||
const controlResponse = message.response
|
||||
if (!controlResponse || typeof controlResponse !== 'object') return null
|
||||
|
||||
if (
|
||||
controlResponse.subtype === 'success' &&
|
||||
'response' in controlResponse &&
|
||||
isBridgePermissionResponse(controlResponse.response)
|
||||
) {
|
||||
return controlResponse.response
|
||||
}
|
||||
|
||||
if (controlResponse.subtype !== 'error') {
|
||||
return null
|
||||
}
|
||||
|
||||
const nested =
|
||||
'response' in controlResponse &&
|
||||
isBridgePermissionResponse(controlResponse.response)
|
||||
? controlResponse.response
|
||||
: undefined
|
||||
|
||||
const messageText = toBridgePermissionMessage(controlResponse, nested)
|
||||
|
||||
if (nested) {
|
||||
return messageText ? { ...nested, message: messageText } : nested
|
||||
}
|
||||
|
||||
if (messageText) {
|
||||
return {
|
||||
behavior: 'deny',
|
||||
message: messageText,
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export { isBridgePermissionResponse, parseBridgePermissionResponse }
|
||||
export type { BridgePermissionCallbacks, BridgePermissionResponse }
|
||||
|
||||
28
src/bridge/bridgeResultScheduling.ts
Normal file
28
src/bridge/bridgeResultScheduling.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
export function hasPendingBridgeMessages(
|
||||
lastWrittenIndex: number,
|
||||
messageCount: number,
|
||||
): boolean {
|
||||
return lastWrittenIndex < messageCount
|
||||
}
|
||||
|
||||
export function isTranscriptResetResultReady(
|
||||
transcriptResetPending: boolean,
|
||||
messageCount: number,
|
||||
): boolean {
|
||||
return transcriptResetPending && messageCount === 0
|
||||
}
|
||||
|
||||
export function shouldDeferBridgeResult({
|
||||
hasHandle,
|
||||
isConnected,
|
||||
lastWrittenIndex,
|
||||
messageCount,
|
||||
}: {
|
||||
hasHandle: boolean
|
||||
isConnected: boolean
|
||||
lastWrittenIndex: number
|
||||
messageCount: number
|
||||
}): boolean {
|
||||
if (!hasHandle || !isConnected) return true
|
||||
return hasPendingBridgeMessages(lastWrittenIndex, messageCount)
|
||||
}
|
||||
@@ -49,6 +49,8 @@ import {
|
||||
makeResultMessage,
|
||||
isEligibleBridgeMessage,
|
||||
extractTitleText,
|
||||
shouldReportRunningForMessage,
|
||||
shouldReportRunningForMessages,
|
||||
BoundedUUIDSet,
|
||||
} from './bridgeMessaging.js'
|
||||
import { logBridgeSkip } from './debugUtils.js'
|
||||
@@ -72,6 +74,7 @@ import type {
|
||||
import type { StdoutMessage } from '../entrypoints/sdk/controlTypes.js'
|
||||
import type { SDKResultSuccess } from '../entrypoints/sdk/coreTypes.js'
|
||||
import type { PermissionMode } from '../utils/permissions/PermissionMode.js'
|
||||
import { setSessionMetadataChangedListener } from '../utils/sessionState.js'
|
||||
|
||||
/**
|
||||
* StdoutMessage with optional session_id. The transport layer accepts
|
||||
@@ -321,6 +324,18 @@ export async function initEnvLessBridgeCore(
|
||||
})
|
||||
}
|
||||
|
||||
// Mirror external metadata updates from the live REPL into the bridge's
|
||||
// CCR worker channel. Without this, proactive wait/sleep only changes local
|
||||
// UI state and the web session detail falls back to the generic working
|
||||
// spinner because automation_state never reaches remote-control.
|
||||
setSessionMetadataChangedListener(
|
||||
metadata => {
|
||||
if (tornDown) return
|
||||
transport.reportMetadata(metadata)
|
||||
},
|
||||
{ replayCurrent: true },
|
||||
)
|
||||
|
||||
// ── 5. JWT refresh scheduler ────────────────────────────────────────────
|
||||
// Schedule a callback 5min before expiry (per response.expires_in). On fire,
|
||||
// re-fetch /bridge with OAuth → rebuild transport with fresh credentials.
|
||||
@@ -625,7 +640,7 @@ export async function initEnvLessBridgeCore(
|
||||
...m,
|
||||
session_id: sessionId,
|
||||
})) as TransportMessage[]
|
||||
if (msgs.some(m => m.type === 'user')) {
|
||||
if (shouldReportRunningForMessages(msgs)) {
|
||||
transport.reportState('running')
|
||||
}
|
||||
logForDebugging(
|
||||
@@ -655,13 +670,13 @@ export async function initEnvLessBridgeCore(
|
||||
})) as TransportMessage[]
|
||||
if (events.length === 0) return
|
||||
// Mid-turn init: if Remote Control is enabled while a query is running,
|
||||
// the last eligible message is a user prompt or tool_result (both 'user'
|
||||
// type). Without this the init PUT's 'idle' sticks until the next user-
|
||||
// type message forwards via writeMessages — which for a pure-text turn
|
||||
// is never (only assistant chunks stream post-init). Check eligible (pre-
|
||||
// cap), not capped: the cap may truncate to a user message even when the
|
||||
// actual trailing message is assistant.
|
||||
if (eligible.at(-1)?.type === 'user') {
|
||||
// the last eligible message may be a real user prompt or tool_result.
|
||||
// Hidden slash-command scaffolding and pure reminder wrappers should not
|
||||
// resurrect a completed turn into "running". Check eligible (pre-cap),
|
||||
// not capped: the cap may truncate to a user message even when the actual
|
||||
// trailing message is assistant.
|
||||
const lastEligible = eligible.at(-1)
|
||||
if (lastEligible && shouldReportRunningForMessage(lastEligible)) {
|
||||
transport.reportState('running')
|
||||
}
|
||||
logForDebugging(`[remote-bridge] Flushing ${events.length} history events`)
|
||||
@@ -817,10 +832,11 @@ export async function initEnvLessBridgeCore(
|
||||
})) as TransportMessage[]
|
||||
// v2 does not derive worker_status from events server-side (unlike v1
|
||||
// session-ingress session_status_updater.go). Push it from here so the
|
||||
// CCR web session list shows Running instead of stuck on Idle. A user
|
||||
// message in the batch marks turn start. CCRClient.reportState dedupes
|
||||
// consecutive same-state pushes.
|
||||
if (filtered.some(m => m.type === 'user')) {
|
||||
// CCR web session list shows Running instead of stuck on Idle. Only
|
||||
// work-starting user messages mark turn start; hidden local-command
|
||||
// scaffolding and pure reminders should not re-open a completed turn.
|
||||
// CCRClient.reportState dedupes consecutive same-state pushes.
|
||||
if (shouldReportRunningForMessages(filtered)) {
|
||||
transport.reportState('running')
|
||||
}
|
||||
logForDebugging(`[remote-bridge] Sending ${filtered.length} message(s)`)
|
||||
|
||||
13
src/bridge/remoteInterruptHandling.ts
Normal file
13
src/bridge/remoteInterruptHandling.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { feature } from 'bun:bundle'
|
||||
|
||||
export function handleRemoteInterrupt(
|
||||
abortController: AbortController | null,
|
||||
): void {
|
||||
if (feature('PROACTIVE') || feature('KAIROS')) {
|
||||
const { pauseProactive } =
|
||||
require('../proactive/index.js') as typeof import('../proactive/index.js')
|
||||
pauseProactive()
|
||||
}
|
||||
|
||||
abortController?.abort()
|
||||
}
|
||||
@@ -39,6 +39,7 @@ import {
|
||||
createV2ReplTransport,
|
||||
} from './replBridgeTransport.js'
|
||||
import { updateSessionIngressAuthToken } from '../utils/sessionIngressAuth.js'
|
||||
import { setSessionMetadataChangedListener } from '../utils/sessionState.js'
|
||||
import { isEnvTruthy, isInProtectedNamespace } from '../utils/envUtils.js'
|
||||
import { validateBridgeId } from './bridgeApi.js'
|
||||
import {
|
||||
@@ -87,6 +88,7 @@ export type ReplBridgeHandle = {
|
||||
sessionIngressUrl: string
|
||||
writeMessages(messages: Message[]): void
|
||||
writeSdkMessages(messages: SDKMessage[]): void
|
||||
markTranscriptReset?(): void
|
||||
sendControlRequest(request: SDKControlRequest): void
|
||||
sendControlResponse(response: SDKControlResponse): void
|
||||
sendControlCancelRequest(requestId: string): void
|
||||
@@ -555,6 +557,17 @@ export async function initBridgeCore(
|
||||
// server-driven via secret.use_code_sessions, with CLAUDE_BRIDGE_USE_CCR_V2
|
||||
// as an ant-dev override.
|
||||
let transport: ReplBridgeTransport | null = null
|
||||
// Mirror external metadata updates from the active REPL into whichever
|
||||
// transport currently owns the remote-control session. v1 ignores the call;
|
||||
// v2 forwards it to CCR /worker external_metadata so standby/sleeping and
|
||||
// other session metadata survive on web/mobile.
|
||||
setSessionMetadataChangedListener(
|
||||
metadata => {
|
||||
if (pollController.signal.aborted) return
|
||||
transport?.reportMetadata(metadata)
|
||||
},
|
||||
{ replayCurrent: true },
|
||||
)
|
||||
// Bumped on every onWorkReceived. Captured in createV2ReplTransport's .then()
|
||||
// closure to detect stale resolutions: if two calls race while transport is
|
||||
// null, both registerWorker() (bumping server epoch), and whichever resolves
|
||||
@@ -1869,6 +1882,7 @@ export async function initBridgeCore(
|
||||
)
|
||||
return
|
||||
}
|
||||
transport.reportState('idle')
|
||||
const resultMsg = {
|
||||
...makeResultMessage(currentSessionId),
|
||||
session_id: currentSessionId,
|
||||
|
||||
Reference in New Issue
Block a user