import assert from 'node:assert/strict' import { randomUUID } from 'node:crypto' import { existsSync } from 'node:fs' import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises' import { createServer, type ServerResponse } from 'node:http' import { tmpdir } from 'node:os' import { join } from 'node:path' import { query, type SDKMessage, type SDKUserMessage } from '@anthropic-ai/claude-agent-sdk' interface CapturedRequest { index: number url: string headers: Record body: Record userTexts: string[] latestText: string completed: boolean aborted: boolean responseText?: string } interface Deferred { promise: Promise resolve(value: T): void } class AsyncQueue implements AsyncIterable { private readonly values: T[] = [] private readonly waiters: Array<(result: IteratorResult) => void> = [] private closed = false push(value: T): void { if (this.closed) throw new Error('input queue is closed') const waiter = this.waiters.shift() if (waiter) waiter({ value, done: false }) else this.values.push(value) } close(): void { if (this.closed) return this.closed = true for (const waiter of this.waiters.splice(0)) waiter({ value: undefined, done: true }) } [Symbol.asyncIterator](): AsyncIterator { return { next: () => { const value = this.values.shift() if (value !== undefined) return Promise.resolve({ value, done: false }) if (this.closed) return Promise.resolve({ value: undefined, done: true }) return new Promise>((resolve) => this.waiters.push(resolve)) } } } } function deferred(): Deferred { let resolve!: (value: T) => void const promise = new Promise((resolvePromise) => { resolve = resolvePromise }) return { promise, resolve } } function sse(res: ServerResponse, event: string, data: unknown): void { res.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`) } function messageStart(res: ServerResponse, id: string): void { sse(res, 'message_start', { type: 'message_start', message: { id, type: 'message', role: 'assistant', model: 'claude-append-probe', content: [], stop_reason: null, stop_sequence: null, usage: { input_tokens: 8, output_tokens: 1 } } }) } function textBlock(res: ServerResponse, text: string): void { sse(res, 'content_block_start', { type: 'content_block_start', index: 0, content_block: { type: 'text', text: '' } }) sse(res, 'content_block_delta', { type: 'content_block_delta', index: 0, delta: { type: 'text_delta', text } }) sse(res, 'content_block_stop', { type: 'content_block_stop', index: 0 }) } function messageEnd(res: ServerResponse, stopReason = 'end_turn'): void { sse(res, 'message_delta', { type: 'message_delta', delta: { stop_reason: stopReason, stop_sequence: null }, usage: { output_tokens: 4 } }) sse(res, 'message_stop', { type: 'message_stop' }) res.end() } function plainText(res: ServerResponse, id: string, text: string): void { messageStart(res, id) textBlock(res, text) messageEnd(res) } function contentTexts(body: Record): string[] { const messages = Array.isArray(body.messages) ? body.messages : [] const texts: string[] = [] for (const rawMessage of messages) { if (!rawMessage || typeof rawMessage !== 'object') continue const message = rawMessage as Record if (message.role !== 'user') continue const content = message.content if (typeof content === 'string') { texts.push(content) continue } if (!Array.isArray(content)) continue for (const block of content) { if (!block || typeof block !== 'object') continue const item = block as Record if (item.type === 'text' && typeof item.text === 'string') texts.push(item.text) } } return texts } function requestTrace(body: Record): Array> { const messages = Array.isArray(body.messages) ? body.messages : [] return messages.flatMap((rawMessage) => { if (!rawMessage || typeof rawMessage !== 'object') return [] const message = rawMessage as Record const content = message.content const parts = typeof content === 'string' ? [{ type: 'text', text: content }] : Array.isArray(content) ? content.flatMap((block) => { if (!block || typeof block !== 'object') return [] const item = block as Record return item.type === 'text' && typeof item.text === 'string' ? [{ type: 'text', text: item.text }] : [{ type: String(item.type) }] }) : [] const text = parts.flatMap((part) => 'text' in part ? [part.text] : []).join('') if (!text.includes('PROBE_') && !text.includes('answer-')) return [] return [{ role: message.role, content: parts }] }) } function compactFrame(message: SDKMessage): Record { const value = message as unknown as Record const inner = value.message && typeof value.message === 'object' ? value.message as Record : undefined return { type: value.type, subtype: value.subtype, uuid: value.uuid, prompt_id: value.prompt_id, user_message_uuid: value.user_message_uuid, user_message_uuids: value.user_message_uuids, result_index: value.result_index, queued_turn_count: value.queued_turn_count, is_error: value.is_error, result: value.result, parent_tool_use_id: value.parent_tool_use_id, message_id: inner?.id, message_role: inner?.role, message_stop_reason: inner?.stop_reason, content_types: Array.isArray(inner?.content) ? inner.content.map((block) => block && typeof block === 'object' ? (block as Record).type : typeof block) : undefined } } function inputMessage(sessionId: string, uuid: string, text: string): SDKUserMessage { return { type: 'user', message: { role: 'user', content: [{ type: 'text', text }] }, parent_tool_use_id: null, uuid: uuid as SDKUserMessage['uuid'], session_id: sessionId } } function executablePath(): string { const executable = process.platform === 'win32' ? 'claude.exe' : 'claude' const packageName = `@anthropic-ai/claude-agent-sdk-${process.platform}-${process.arch}` const candidates = [ join(process.cwd(), 'node_modules', ...packageName.split('/'), executable), join(process.cwd(), 'node_modules', '@anthropic-ai', 'claude-agent-sdk', 'vendor', executable) ] const found = candidates.find((candidate) => existsSync(candidate)) if (!found) throw new Error(`Claude executable not found: ${candidates.join(', ')}`) return found } function isolatedEnv(tempRoot: string, baseUrl: string): NodeJS.ProcessEnv { const env: NodeJS.ProcessEnv = { ...process.env, HOME: join(tempRoot, 'home'), USERPROFILE: join(tempRoot, 'home'), CLAUDE_CONFIG_DIR: join(tempRoot, 'claude-config'), CLAUDE_SECURESTORAGE_CONFIG_DIR: join(tempRoot, 'claude-config'), ANTHROPIC_BASE_URL: baseUrl, ANTHROPIC_API_KEY: 'ycode-loopback-probe-key', ANTHROPIC_MODEL: 'claude-append-probe', ANTHROPIC_DEFAULT_OPUS_MODEL: 'claude-append-probe', ANTHROPIC_DEFAULT_SONNET_MODEL: 'claude-append-probe', ANTHROPIC_DEFAULT_HAIKU_MODEL: 'claude-append-probe', ANTHROPIC_SMALL_FAST_MODEL: 'claude-append-probe', CLAUDE_CODE_MAX_CONTEXT_TOKENS: '200000', CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: '1', DISABLE_TELEMETRY: '1' } for (const name of [ 'ANTHROPIC_AUTH_TOKEN', 'CLAUDE_CODE_OAUTH_TOKEN', 'CLAUDE_CODE_USE_BEDROCK', 'CLAUDE_CODE_USE_VERTEX', 'CLAUDE_CODE_USE_FOUNDRY', 'CLAUDE_CODE_USE_ANTHROPIC_AWS', 'CLAUDE_CODE_USE_ANTHROPIC_GOOGLE_CLOUD', 'CLAUDE_CODE_USE_MANTLE', 'CLAUDE_CODE_USE_GATEWAY' ]) delete env[name] return env } async function waitFor(description: string, predicate: () => T | undefined | false, timeoutMs = 20_000): Promise { const deadline = Date.now() + timeoutMs while (Date.now() < deadline) { const value = predicate() if (value) return value await new Promise((resolve) => setTimeout(resolve, 20)) } throw new Error(`timed out waiting for ${description}`) } const sdkPackage = JSON.parse(await readFile( join(process.cwd(), 'node_modules', '@anthropic-ai', 'claude-agent-sdk', 'package.json'), 'utf8' )) as { version?: string; claudeCodeVersion?: string } const tempRoot = await mkdtemp(join(tmpdir(), 'ycode-claude-append-probe-')) const workspace = join(tempRoot, 'workspace') const outputPath = join(process.cwd(), '.tmp', 'claude-append-protocol-probe.json') await mkdir(workspace, { recursive: true }) await mkdir(join(tempRoot, 'home'), { recursive: true }) await mkdir(join(tempRoot, 'claude-config'), { recursive: true }) await mkdir(join(process.cwd(), '.tmp'), { recursive: true }) await writeFile(join(workspace, 'README.md'), 'append protocol probe\n', 'utf8') const requests: CapturedRequest[] = [] const runtimeMessages: SDKMessage[] = [] const stderr: string[] = [] const firstResponseGate = deferred() const processAbort = new AbortController() const server = createServer((req, res) => { let raw = '' req.on('data', (chunk) => { raw += chunk }) req.on('end', async () => { let body: Record = {} try { body = JSON.parse(raw || '{}') as Record } catch { body = { raw } } if (!req.url?.includes('/messages')) { res.writeHead(200, { 'content-type': 'application/json' }) res.end('{}') return } const userTexts = contentTexts(body) const captured: CapturedRequest = { index: requests.length + 1, url: req.url ?? '', headers: req.headers, body, userTexts, latestText: userTexts.at(-1) ?? '', completed: false, aborted: false } requests.push(captured) res.on('close', () => { if (!captured.completed) captured.aborted = true }) res.writeHead(200, { 'content-type': 'text/event-stream', 'cache-control': 'no-cache', connection: 'keep-alive' }) const responseText = `answer-${captured.index}:${captured.userTexts.join('|')}` captured.responseText = responseText if (captured.index === 1) { await firstResponseGate.promise } if (!res.destroyed) { captured.completed = true plainText(res, `msg_append_${captured.index}`, responseText) } }) }) await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve)) const address = server.address() if (!address || typeof address === 'string') throw new Error('loopback model did not bind') const baseUrl = `http://127.0.0.1:${address.port}` const sessionId = randomUUID() const uuids = { initial: randomUUID(), appendA: randomUUID(), appendB: randomUUID() } const input = new AsyncQueue() let instance: ReturnType | null = null let pumpError: unknown let pump = Promise.resolve() let appendsQueuedBeforeFirstResponse = false try { instance = query({ prompt: input, options: { cwd: workspace, model: 'claude-append-probe', pathToClaudeCodeExecutable: executablePath(), env: isolatedEnv(tempRoot, baseUrl), includePartialMessages: true, abortController: processAbort, permissionMode: 'default', settingSources: [], strictMcpConfig: true, tools: [], allowedTools: [], plugins: [], agents: {}, skills: [], systemPrompt: { type: 'preset', preset: 'claude_code' }, sessionId, maxTurns: 4, stderr: (data) => { stderr.push(data) if (process.env.DEBUG_CLAUDE_APPEND_PROBE === '1') process.stderr.write(`[claude-probe] ${data}`) } } }) const currentInstance = instance pump = (async () => { try { for await (const message of currentInstance) runtimeMessages.push(message) } catch (error) { pumpError = error } })() input.push(inputMessage(sessionId, uuids.initial, 'PROBE_INITIAL')) await waitFor('first model request', () => requests[0]) // Hold request 1 so both appends are submitted while the turn is active. input.push(inputMessage(sessionId, uuids.appendA, 'PROBE_APPEND_A')) input.push(inputMessage(sessionId, uuids.appendB, 'PROBE_APPEND_B')) appendsQueuedBeforeFirstResponse = true await new Promise((resolve) => setTimeout(resolve, 250)) firstResponseGate.resolve() await waitFor('all queued turns to settle', () => { const results = runtimeMessages.filter((message) => message.type === 'result') const last = results.at(-1) as unknown as Record | undefined return results.length >= 2 && last?.queued_turn_count === 0 }, 30_000) await new Promise((resolve) => setTimeout(resolve, 300)) const results = runtimeMessages .filter((message) => message.type === 'result') .map((message) => message as unknown as Record) const assistants = runtimeMessages .filter((message) => message.type === 'assistant') .map((message) => message as unknown as Record) const userFrames = runtimeMessages .filter((message) => message.type === 'user') .map((message) => message as unknown as Record) const firstResult = results[0] const finalResult = results.at(-1) const firstResultUuids = Array.isArray(firstResult?.user_message_uuids) ? firstResult.user_message_uuids as string[] : [] const finalResultUuids = Array.isArray(finalResult?.user_message_uuids) ? finalResult.user_message_uuids as string[] : [] assert.equal(requests[0]?.aborted, false, 'the first model request must complete without cancellation') assert.ok(requests.length >= 2, 'queued appends must produce another model request') assert.ok(firstResult, 'the initial turn must emit a result') assert.ok(finalResult, 'the queued append turn must emit a result') assert.ok(firstResultUuids.includes(uuids.initial), 'the first result must be attributed to the initial UUID') assert.ok(!firstResultUuids.includes(uuids.appendA), 'the first result must not claim append A') assert.ok(!firstResultUuids.includes(uuids.appendB), 'the first result must not claim append B') assert.ok( finalResultUuids.includes(uuids.appendA) && finalResultUuids.includes(uuids.appendB), 'a later result must cover both queued appends' ) assert.ok(results.length >= 2, 'one result must not stand for every queued send') assert.equal(userFrames.length, 0, 'text sends must not be replayed as SDK user frames') assert.equal(assistants.length, 2, 'each logical turn must emit one complete assistant frame') const assistantUuidSets = assistants.map((assistant) => Array.isArray(assistant.user_message_uuids) ? assistant.user_message_uuids as string[] : []) assert.ok(!assistantUuidSets[0]?.includes(uuids.appendA), 'the first assistant must not claim append A') assert.ok( assistantUuidSets[1]?.includes(uuids.appendA) && assistantUuidSets[1]?.includes(uuids.appendB), 'the second assistant must cover both coalesced appends' ) for (const [index, frame] of [...assistants, ...results].entries()) { const frameUuids = Array.isArray(frame.user_message_uuids) ? frame.user_message_uuids as string[] : [] assert.equal(frameUuids.at(-1), frame.user_message_uuid, `frame ${index} must use the last member as user_message_uuid`) } assert.equal(pumpError, undefined, 'the SDK iterator must not fail during the probe') const report = { result: 'PASS', sdkVersion: sdkPackage.version, claudeCodeVersion: sdkPackage.claudeCodeVersion, loopback: baseUrl, activeAppend: { appendsQueuedBeforeFirstResponse, interruptCalled: false, inputClosedBeforeResults: false }, uuids, requests: requests.map((request) => ({ index: request.index, url: request.url, completed: request.completed, aborted: request.aborted, latestText: request.latestText, userTexts: request.userTexts .filter((text) => text.includes('PROBE_') || text.includes('answer-')) .slice(-8), messages: requestTrace(request.body) })), results: results.map((result) => ({ subtype: result.subtype, is_error: result.is_error, result_index: result.result_index, queued_turn_count: result.queued_turn_count, user_message_uuid: result.user_message_uuid, user_message_uuids: result.user_message_uuids, prompt_id: result.prompt_id, result: result.result })), assistantKeys: assistants.map((assistant) => Object.keys(assistant).sort()), resultKeys: results.map((result) => Object.keys(result).sort()), assistants: assistants.map((assistant) => ({ uuid: assistant.uuid, user_message_uuid: assistant.user_message_uuid, user_message_uuids: assistant.user_message_uuids, prompt_id: assistant.prompt_id, parent_tool_use_id: assistant.parent_tool_use_id, message_id: (assistant.message as Record | undefined)?.id })), userFrameCount: userFrames.length, userFrames: userFrames.map((frame) => ({ uuid: frame.uuid, user_message_uuid: frame.user_message_uuid, user_message_uuids: frame.user_message_uuids, prompt_id: frame.prompt_id, message: frame.message })), promptIdObserved: runtimeMessages.some((message) => { const value = message as unknown as Record return typeof value.prompt_id === 'string' }), assistantPromptIdObserved: assistants.some((assistant) => typeof assistant.prompt_id === 'string'), frames: runtimeMessages.map(compactFrame), stderr: stderr.slice(-20) } await writeFile(outputPath, `${JSON.stringify(report, null, 2)}\n`, 'utf8') console.log(JSON.stringify({ result: report.result, outputPath, uuids, activeAppend: report.activeAppend, requests: report.requests, results: report.results, assistants: report.assistants, userFrameCount: report.userFrameCount, userFrames: report.userFrames, assistantKeys: report.assistantKeys, resultKeys: report.resultKeys, promptIdObserved: report.promptIdObserved, assistantPromptIdObserved: report.assistantPromptIdObserved }, null, 2)) } catch (error) { const diagnostic = { result: 'FAIL', error: error instanceof Error ? `${error.name}: ${error.message}` : String(error), pumpError: pumpError instanceof Error ? `${pumpError.name}: ${pumpError.message}` : pumpError, uuids, requests: requests.map((request) => ({ index: request.index, completed: request.completed, aborted: request.aborted, userTexts: request.userTexts })), frames: runtimeMessages.map(compactFrame), stderr: stderr.slice(-40) } await writeFile(outputPath, `${JSON.stringify(diagnostic, null, 2)}\n`, 'utf8') console.error(JSON.stringify(diagnostic, null, 2)) throw error } finally { input.close() processAbort.abort() instance?.close() await Promise.race([ pump, new Promise((resolve) => setTimeout(resolve, 2000)) ]) await new Promise((resolve) => server.close(() => resolve())) await rm(tempRoot, { recursive: true, force: true, maxRetries: 5, retryDelay: 200 }).catch(() => undefined) }