fix: 消除会话列表解析阻塞并按需加载工具详情

This commit is contained in:
shiyue
2026-10-19 10:58:22 +08:00
parent fb2c3360c9
commit 99cc807f4b
35 changed files with 1468 additions and 214 deletions

View File

@@ -49,7 +49,7 @@ async function connect(port) {
assert(auth.success);
return { ws, messages, token: auth.token };
}
function createFixture(root) {
function createFixture(root, options = {}) {
const sessionsDir = path.join(root, 'sessions');
const homeDir = path.join(root, 'home');
fs.mkdirSync(sessionsDir, { recursive: true });
@@ -66,6 +66,18 @@ function createFixture(root) {
return session;
}
seed(A); seed(B); seed('http-corrupt-chunk'); seed('http-corrupt-manifest');
if (options.largeTools !== false) {
const large = seed('http-large-tools', 17);
large.messages[15].toolCalls = Array.from({ length: 100 }, (_, index) => ({
id: 'large-tool-' + index, name: 'exec_command', kind: 'command_execution', done: true,
input: { command: '读取完整工具记录', payload: '输入'.repeat(10000) },
result: '结果起点' + '完整输出'.repeat(10000) + '结果终点', meta: { exitCode: 0 },
}));
large.messages[3].toolCalls = [{ id: 'older-tool', name: 'read_file', done: true,
input: { path: '/隔离夹具' }, result: '早期完整结果'.repeat(10000) }];
delete large.messages[3].id;
store.save(large);
}
const corruptPath = path.join(sessionsDir, '_history', 'http-corrupt-chunk');
const manifest = JSON.parse(fs.readFileSync(path.join(corruptPath, 'manifest.json'), 'utf8'));
fs.writeFileSync(path.join(corruptPath, 'chunks', manifest.entries.at(-1).file), '{破损块');
@@ -97,6 +109,8 @@ function createFixture(root) {
assert(mock.includes(timer));
fs.writeFileSync(mockPath, mock.replace(timer, [
"send({method:'item/agentMessage/delta',params:{threadId:thread.id,turnId,itemId:'agent-msg',delta:" + JSON.stringify(STREAM) + '}});',
"send({method:'item/started',params:{threadId:thread.id,turnId,item:{id:'large-runtime-tool',type:'commandExecution',command:'运行工具夹具',status:'inProgress'}}});",
"send({method:'item/completed',params:{threadId:thread.id,turnId,item:{id:'large-runtime-tool',type:'commandExecution',command:'运行工具夹具',aggregatedOutput:'运行结果起点'+'完整运行输出'.repeat(100000)+'运行结果终点',exitCode:0,status:'completed'}}});",
"thread.timer = setInterval(() => { if (!require('fs').existsSync(" + JSON.stringify(completeFlag) + ')) return;',
'clearInterval(thread.timer); completeTurn(thread, turnId, text); }, 50);',
].join('\n')));
@@ -150,6 +164,21 @@ async function runApi(port, fixture) {
assert.equal(result.payload.sessionId, id);
}
assert.equal((await query('http-legacy-only')).payload.messages.length, 12);
const large = (await query('http-large-tools')).payload;
const summary = large.messages.find(message => message.toolCalls?.length === 100);
assert(summary, '首屏必须保留全部100个工具概要');
assert(Buffer.byteLength(JSON.stringify(large)) < 100000, '多兆工具输出不能绑在首屏');
assert(!summary.toolCalls[0].result);
const detailSuffix = ref => '?toolDetail=' + encodeURIComponent(JSON.stringify(ref));
const detail = await query('http-large-tools', detailSuffix(summary.toolCalls[0].detailRef));
assert.deepEqual(detail.payload.tool, fixture.store.load('http-large-tools').messages[15].toolCalls[0], '输入输出详情必须完整');
assert.equal((await query(B, detailSuffix(summary.toolCalls[0].detailRef))).response.status, 400, '不能跨会话读取');
assert.equal((await query('http-large-tools', detailSuffix({ ...summary.toolCalls[0].detailRef, signature: '过期' }))).response.status, 409);
assert.equal((await query('http-large-tools', '?toolDetail=broken')).response.status, 400);
assert.equal((await query('http-large-tools', detailSuffix(summary.toolCalls[0].detailRef), false)).response.status, 401);
const older = (await query('http-large-tools', '/history?before=' + large.historyCursor)).payload;
const olderTool = older.messages.find(message => message.toolCalls?.length)?.toolCalls[0];
assert.equal((await query('http-large-tools', detailSuffix(olderTool.detailRef))).payload.tool.result, '早期完整结果'.repeat(10000));
connection = await connect(port);
connection.ws.send(JSON.stringify({ type: 'subscribe_session', sessionId: B,
requestId: 'subscribe-before-commit', historyRevision: first.payload.historyRevision }));
@@ -174,6 +203,18 @@ async function runApi(port, fixture) {
assert.equal(running.historyPending, false);
assert.equal(running.messages.length, 12);
assert.equal(running.runtime.type, 'resume_generating');
const runtime = await waitFor(async () => {
const snapshot = (await query(A)).payload.runtime;
return snapshot?.toolCalls?.some(tool => tool.kind === 'command_execution' && tool.done && tool.detailRef) ? snapshot : null;
}, '运行工具概要缺失');
assert(Buffer.byteLength(JSON.stringify(runtime)) < 10000, '运行快照不能捆绑完整工具输出');
const runtimeTool = runtime.toolCalls.find(tool => tool.kind === 'command_execution' && tool.done && tool.detailRef);
const runtimeDetail = (await query(A, detailSuffix(runtimeTool.detailRef))).payload.tool;
assert(runtimeDetail.result.includes('运行结果终点'), '运行工具详情不能被预览限制裁剪');
connection.ws.send(JSON.stringify({ type: 'subscribe_session', sessionId: A, requestId: 'compact-runtime' }));
const resumed = await waitFor(() => connection.messages.find(msg => msg.type === 'resume_generating' && msg.requestId === 'compact-runtime'), '订阅运行快照缺失');
assert(Buffer.byteLength(JSON.stringify(resumed)) < 10000, 'WS不能再次重放完整工具输出');
assert(resumed.toolCalls.some(tool => tool.detailRef));
assert(!connection.messages.some(msg => msg.type === 'session_history_chunk'));
return { connection, token, query };
} catch (error) {
@@ -216,4 +257,6 @@ async function main() {
log.end();
}
}
main().catch(error => { console.error(error.stack || error); process.exitCode = 1; });
if (require.main === module) main().catch(error => { console.error(error.stack || error); process.exitCode = 1; });
module.exports = { createFixture, waitFor, freePort, stopChild, connect, sleep, PASSWORD };