Harden Claude provider lifecycle and clean temp artifacts

This commit is contained in:
Mohamed Boudra
2026-02-22 11:44:56 +07:00
parent c1e4b0fcbf
commit ccd42751d2
16 changed files with 1845 additions and 373 deletions
@@ -29,6 +29,7 @@ function createMockTransport() {
let onOpen: () => void = () => {}
let onClose: (_event?: unknown) => void = () => {}
let onError: (_event?: unknown) => void = () => {}
let welcomeOrdinal = 1
const transport: DaemonTransport = {
send: (data) => sent.push(data),
@@ -54,7 +55,20 @@ function createMockTransport() {
return {
transport,
sent,
triggerOpen: () => onOpen(),
triggerOpen: () => {
onOpen()
// Ignore HELLO handshake payloads in assertions.
sent.length = 0
onMessage(
JSON.stringify({
type: 'welcome',
serverId: `srv_test_${welcomeOrdinal++}`,
hostname: null,
version: null,
resumed: false,
})
)
},
triggerClose: (event?: unknown) => onClose(event),
triggerError: (event?: unknown) => onError(event),
triggerMessage: (data: unknown) => onMessage(data),
@@ -309,7 +323,7 @@ describe('DaemonClient', () => {
mock.triggerOpen()
await connectPromise
const transitionPayloads = logger.info.mock.calls
const transitionPayloads = logger.debug.mock.calls
.filter(([, message]) => message === 'DaemonClientTransition')
.map(([payload]) => payload as { generation?: number | null })
expect(transitionPayloads.length).toBeGreaterThan(0)
@@ -868,11 +882,15 @@ describe('DaemonClient', () => {
vi.useFakeTimers()
const logger = createMockLogger()
const mock = createMockTransport()
let sendCount = 0
const transportFactory = () => ({
...mock.transport,
send: () => {
throw new Error('boom')
sendCount += 1
if (sendCount > 1) {
throw new Error('boom')
}
},
})
+96 -10
View File
@@ -91,7 +91,7 @@ export interface Logger {
}
const consoleLogger: Logger = {
debug: (obj, msg) => console.debug(msg, obj),
debug: () => {},
info: (obj, msg) => console.info(msg, obj),
warn: (obj, msg) => console.warn(msg, obj),
error: (obj, msg) => console.error(msg, obj),
@@ -2283,15 +2283,101 @@ export class DaemonClient {
predicate: (snapshot: AgentSnapshotPayload) => boolean,
timeout = 60000
): Promise<AgentSnapshotPayload> {
const startedAt = Date.now()
while (Date.now() - startedAt < timeout) {
const snapshot = await this.fetchAgent(agentId).catch(() => null)
if (snapshot && predicate(snapshot)) {
return snapshot
}
await new Promise((resolve) => setTimeout(resolve, 250))
const initial = await this.fetchAgent(agentId).catch(() => null)
if (initial && predicate(initial)) {
return initial
}
throw new Error(`Timed out waiting for agent ${agentId}`)
const deadline = Date.now() + timeout
return await new Promise<AgentSnapshotPayload>((resolve, reject) => {
let settled = false
let pollInFlight = false
let pollTimer: ReturnType<typeof setInterval> | null = null
let timeoutTimer: ReturnType<typeof setTimeout> | null = null
let unsubscribe: (() => void) | null = null
const finish = (
result:
| { kind: 'ok'; snapshot: AgentSnapshotPayload }
| { kind: 'error'; error: Error }
) => {
if (settled) {
return
}
settled = true
if (timeoutTimer) {
clearTimeout(timeoutTimer)
timeoutTimer = null
}
if (pollTimer) {
clearInterval(pollTimer)
pollTimer = null
}
if (unsubscribe) {
unsubscribe()
unsubscribe = null
}
if (result.kind === 'ok') {
resolve(result.snapshot)
return
}
reject(result.error)
}
const maybeResolve = (snapshot: AgentSnapshotPayload | null) => {
if (!snapshot) {
return false
}
if (!predicate(snapshot)) {
return false
}
finish({ kind: 'ok', snapshot })
return true
}
const poll = async () => {
if (settled || pollInFlight) {
return
}
pollInFlight = true
try {
const snapshot = await this.fetchAgent(agentId).catch(() => null)
maybeResolve(snapshot)
} finally {
pollInFlight = false
}
}
unsubscribe = this.on('agent_update', (message) => {
if (settled) {
return
}
if (message.type !== 'agent_update') {
return
}
if (message.payload.kind !== 'upsert') {
return
}
const snapshot = message.payload.agent
if (snapshot.id !== agentId) {
return
}
maybeResolve(snapshot)
})
const remaining = Math.max(1, deadline - Date.now())
timeoutTimer = setTimeout(() => {
finish({
kind: 'error',
error: new Error(`Timed out waiting for agent ${agentId}`),
})
}, remaining)
pollTimer = setInterval(() => {
void poll()
}, 250)
void poll()
})
}
async waitForFinish(agentId: string, timeout = 60000): Promise<WaitForFinishResult> {
@@ -2715,7 +2801,7 @@ export class DaemonClient {
: null
const reason = metadata?.reason ?? reasonFromNext
const reasonCode = metadata?.reasonCode ?? toReasonCode(reason)
this.logger.info(
this.logger.debug(
{
serverId: this.logServerId,
clientIdHash: this.logClientIdHash,