fix: preserve nested ACP startup frames

This commit is contained in:
mehmet turac 2026-05-31 14:40:53 +03:00
parent 112b8bf182
commit 86884bcaf2
No known key found for this signature in database
GPG key ID: 20D5F9AEE1833B0F
2 changed files with 85 additions and 11 deletions

View file

@ -305,28 +305,40 @@ export function createJsonLineStream(onMessage: (message: unknown, rawLine: stri
pendingJsonLineCount = 1;
};
const resetPendingJson = () => {
pendingJson = '';
pendingJsonLineCount = 0;
};
const handleLine = (line: string) => {
const trimmed = line.trim();
if (!trimmed) return;
if (pendingJson) {
if ((trimmed.startsWith('{') || trimmed.startsWith('[')) && emit(trimmed)) {
pendingJson = '';
pendingJsonLineCount = 0;
return;
}
const nextCandidate = `${pendingJson}\n${trimmed}`;
if (emit(nextCandidate)) {
pendingJson = '';
pendingJsonLineCount = 0;
resetPendingJson();
return;
}
pendingJsonLineCount += 1;
if (nextCandidate.length <= 128_000 && pendingJsonLineCount <= 256) {
if (
pendingJsonLineCount === 2 &&
pendingJson !== '{' &&
pendingJson !== '[' &&
emit(trimmed)
) {
resetPendingJson();
return;
}
const state = classifyJsonCandidate(nextCandidate);
if (
state === 'incomplete' &&
nextCandidate.length <= 128_000 &&
pendingJsonLineCount <= 256
) {
pendingJson = nextCandidate;
return;
}
pendingJson = '';
pendingJsonLineCount = 0;
resetPendingJson();
handleLine(trimmed);
return;
}
@ -363,6 +375,68 @@ export function createJsonLineStream(onMessage: (message: unknown, rawLine: stri
};
}
function classifyJsonCandidate(value: string): 'complete' | 'incomplete' | 'invalid' {
const stack: string[] = [];
let started = false;
let complete = false;
let inString = false;
let escaping = false;
for (const char of value) {
if (!started) {
if (/\s/.test(char)) continue;
if (char === '{') {
started = true;
stack.push('}');
continue;
}
if (char === '[') {
started = true;
stack.push(']');
continue;
}
return 'invalid';
}
if (complete) {
if (/\s/.test(char)) continue;
return 'invalid';
}
if (inString) {
if (escaping) {
escaping = false;
} else if (char === '\\') {
escaping = true;
} else if (char === '"') {
inString = false;
}
continue;
}
if (char === '"') {
inString = true;
continue;
}
if (char === '{') {
stack.push('}');
continue;
}
if (char === '[') {
stack.push(']');
continue;
}
if (char === '}' || char === ']') {
if (stack.pop() !== char) return 'invalid';
if (stack.length === 0) complete = true;
}
}
if (!started) return 'invalid';
if (inString || escaping || stack.length > 0) return 'incomplete';
return complete ? 'complete' : 'invalid';
}
export async function detectAcpModels({
bin,
args,

View file

@ -398,7 +398,7 @@ test('attachAcpSession accepts pretty-printed ACP startup responses', () => {
send: (event, payload) => events.push({ event, payload }),
});
child.stdout.write('{\n "id": 1,\n "result": {}\n}\n');
child.stdout.write('{\n "id": 1,\n "result":\n {}\n}\n');
child.stdout.write('{\n "id": 2,\n "result":\n {\n "sessionId": "session-1"\n }\n}\n');
const methods = parseRpcWrites(writes)