mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-15 12:55:51 +00:00
fix: close peer socket listener handoff window
CodeRabbit and Claude review found that documenting caller-owned raw socket errors still left a Promise handoff window and a stale timeout-listener risk. The peer connection API now requires a caller error handler and installs it before resolving, while cleanup removes internal error and timeout listeners on every path. Constraint: Keep the fix precise to PR #375 review feedback and avoid warning suppression or fallback behavior. Rejected: Leave the behavior documented only | still permits an unhandled socket error window between resolve and caller listener attachment. Rejected: Keep a no-op internal error listener | would silently swallow caller-owned socket errors. Confidence: high Scope-risk: narrow Directive: Do not add raw connectToPeer callers without providing a real onSocketError handler and capability handshake. Tested: bun test src/utils/__tests__/udsMessaging.test.ts src/services/AgentSummary/__tests__/agentSummary.test.ts Tested: bunx tsc --noEmit --pretty false Tested: bun run lint Tested: bun run test:all Tested: bun test --coverage --coverage-reporter lcov --coverage-dir coverage Tested: bun run build Tested: bun run build:vite Tested: bun audit Not-tested: Manual external ACP peer runtime beyond repository tests.
This commit is contained in:
@@ -161,7 +161,9 @@ describe('startAgentSummarization', () => {
|
||||
|
||||
expect(forkCalls).toEqual([])
|
||||
expect(updateCalls).toEqual([])
|
||||
expectDebugLogContaining('no bounded context available')
|
||||
expectDebugLogContaining(
|
||||
'[AgentSummary] Skipping summary for task-1: no bounded context available',
|
||||
)
|
||||
})
|
||||
|
||||
test('skips summarization before building context when transcript is too short', async () => {
|
||||
@@ -173,7 +175,9 @@ describe('startAgentSummarization', () => {
|
||||
|
||||
expect(forkCalls).toEqual([])
|
||||
expect(updateCalls).toEqual([])
|
||||
expectDebugLogContaining('not enough messages (2)')
|
||||
expectDebugLogContaining(
|
||||
'[AgentSummary] Skipping summary for task-1: not enough messages (2)',
|
||||
)
|
||||
})
|
||||
|
||||
test('skips and reschedules while poor mode is active', async () => {
|
||||
@@ -188,7 +192,7 @@ describe('startAgentSummarization', () => {
|
||||
|
||||
expect(forkCalls).toEqual([])
|
||||
expect(updateCalls).toEqual([])
|
||||
expectDebugLogContaining('poor mode active')
|
||||
expectDebugLogContaining('[AgentSummary] Skipping summary — poor mode active')
|
||||
expect(scheduledCount).toBe(initialScheduledCount + 1)
|
||||
expect(lastTimerHandle).not.toBe(initialTimerHandle)
|
||||
})
|
||||
@@ -218,7 +222,7 @@ describe('startAgentSummarization', () => {
|
||||
|
||||
handle.stop()
|
||||
|
||||
expectDebugLogContaining('Stopping summarization for task-1')
|
||||
expectDebugLogContaining('[AgentSummary] Stopping summarization for task-1')
|
||||
expect(clearedHandles).toEqual([pendingHandle])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -307,7 +307,9 @@ describe('UDS inbox retention', () => {
|
||||
'../udsClient.js'
|
||||
)
|
||||
|
||||
const error = await connectToPeer(path).then(
|
||||
const error = await connectToPeer(path, () => {
|
||||
throw new Error('Unexpected post-connect socket error')
|
||||
}).then(
|
||||
() => undefined,
|
||||
err => err,
|
||||
)
|
||||
@@ -338,13 +340,25 @@ describe('UDS inbox retention', () => {
|
||||
})
|
||||
|
||||
let client: Socket | undefined
|
||||
const socketErrors: Error[] = []
|
||||
try {
|
||||
const { connectToPeer } = await import('../udsClient.js')
|
||||
client = await connectToPeer(path, 1000)
|
||||
client = await connectToPeer(
|
||||
path,
|
||||
error => {
|
||||
socketErrors.push(error)
|
||||
},
|
||||
1000,
|
||||
)
|
||||
await new Promise(resolve => setTimeout(resolve, 100))
|
||||
|
||||
expect(client.destroyed).toBe(false)
|
||||
expect(client.listenerCount('error')).toBe(0)
|
||||
expect(client.listenerCount('error')).toBe(1)
|
||||
expect(client.listenerCount('timeout')).toBe(0)
|
||||
|
||||
const socketError = new Error('post-connect failure')
|
||||
client.emit('error', socketError)
|
||||
expect(socketErrors).toEqual([socketError])
|
||||
} finally {
|
||||
client?.destroy()
|
||||
for (const socket of sockets) {
|
||||
|
||||
@@ -266,36 +266,49 @@ export async function sendToUdsSocket(
|
||||
|
||||
/**
|
||||
* Connect to a peer and return the raw socket for bidirectional communication.
|
||||
* The caller is responsible for managing the connection lifecycle, including
|
||||
* attaching an 'error' listener immediately after the Promise resolves. This
|
||||
* function detaches its internal error listener on successful connect so
|
||||
* caller-owned socket errors are not silently swallowed.
|
||||
* The caller owns the post-connect lifecycle through onSocketError, which is
|
||||
* attached before the Promise resolves so peer socket errors cannot be
|
||||
* swallowed or surface through a listener handoff window.
|
||||
* Pre-connect failures reject with UdsPeerConnectionError.
|
||||
* This only opens the transport; callers still own any capability handshake.
|
||||
*/
|
||||
export function connectToPeer(
|
||||
socketPath: string,
|
||||
onSocketError: (error: Error) => void,
|
||||
timeoutMs = 5000,
|
||||
): Promise<Socket> {
|
||||
return new Promise<Socket>((resolve, reject) => {
|
||||
const conn = createConnection(socketPath)
|
||||
let settled = false
|
||||
const fail = (cause: unknown) => {
|
||||
const onTimeout = () => {
|
||||
fail(new Error('Connection timed out'))
|
||||
}
|
||||
function cleanupListeners(): void {
|
||||
conn.setTimeout(0)
|
||||
conn.off('error', fail)
|
||||
conn.off('timeout', onTimeout)
|
||||
}
|
||||
function fail(cause: unknown): void {
|
||||
if (settled) {
|
||||
return
|
||||
}
|
||||
settled = true
|
||||
cleanupListeners()
|
||||
conn.destroy()
|
||||
reject(new UdsPeerConnectionError(socketPath, cause))
|
||||
}
|
||||
conn.once('connect', () => {
|
||||
if (settled) {
|
||||
return
|
||||
}
|
||||
settled = true
|
||||
conn.setTimeout(0)
|
||||
conn.off('error', fail)
|
||||
cleanupListeners()
|
||||
conn.on('error', onSocketError)
|
||||
resolve(conn)
|
||||
})
|
||||
conn.on('error', fail)
|
||||
conn.setTimeout(timeoutMs, () => {
|
||||
fail(new Error('Connection timed out'))
|
||||
})
|
||||
conn.once('timeout', onTimeout)
|
||||
conn.setTimeout(timeoutMs)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user