fix(observer): narrow parallel_session detector to tool_result evidence (C-2)

extractEnvironment was scanning JSON.stringify(turn) for collision markers
(чужой staged / foreign git index / index.lock / another git process). Prose
mentions in user/assistant text flipped parallel_session=true. Live FP proven
on episodes-2026-05.jsonl line 20: my own analysis turn was non-parallel but
recorded parallel_session: true because the finding text mentioned the markers.

Fix: collectToolResultText(turn) — gather text only from tool_result blocks
(both string content and structured `[{type:text,text}]` arrays). Scan THAT
for collision markers; prose is no longer a signal.

Tests: rewrote `parallel_session narrowed` block — false on user/assistant
prose / no-tool-result turns; true on tool_result strings + structured form.
106/106 parser tests green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Дмитрий
2026-05-20 10:58:37 +03:00
parent 9f8482277d
commit 8496b52bd1
2 changed files with 100 additions and 13 deletions
+69 -12
View File
@@ -670,26 +670,83 @@ describe('parseTranscript — AskUserQuestion in-turn choice', () => {
});
});
describe('extractEnvironment — parallel_session narrowed', () => {
const wrap = (text) => [
{
message: { role: 'user', content: text },
timestamp: '2026-05-19T10:00:00.000Z',
describe('extractEnvironment — parallel_session narrowed to tool_result evidence', () => {
// Collision detection scans ONLY tool_result content (real command output).
// Prose mentions in user prompts / assistant text — including analysis text
// that references collision phrases — must not trigger.
const userMsg = (text) => ({
message: { role: 'user', content: text },
timestamp: '2026-05-19T10:00:00.000Z',
});
const assistantText = (text) => ({
message: { role: 'assistant', content: [{ type: 'text', text }] },
timestamp: '2026-05-19T10:00:01.000Z',
});
const toolResultText = (text, isError = false) => ({
message: {
role: 'user',
content: [{ type: 'tool_result', tool_use_id: 't1', content: text, is_error: isError }],
},
];
timestamp: '2026-05-19T10:00:02.000Z',
});
it('is false when text only casually mentions parallel sessions', () => {
const env = extractEnvironment(wrap('давай обсудим фичу параллельные сессии и parallel session coordination'), 0);
it('is false when a USER prompt only mentions a collision marker', () => {
const env = extractEnvironment([userMsg('в моём ходе появился чужой staged-файл — что делать?')], 0);
expect(env.parallel_session).toBe(false);
});
it('is true on a real collision marker (foreign git index)', () => {
const env = extractEnvironment(wrap('git commit поймал foreign git index'), 0);
it('is false when ASSISTANT prose mentions a collision marker (e.g., analysis text)', () => {
const env = extractEnvironment(
[userMsg('go'), assistantText('я писал про чужой staged и foreign git index в разборе')],
0
);
expect(env.parallel_session).toBe(false);
});
it('is true when a tool_result (Bash output) contains "foreign git index"', () => {
const env = extractEnvironment(
[
userMsg('go'),
toolResultText('fatal: another git process is running\nforeign git index detected', true),
],
0
);
expect(env.parallel_session).toBe(true);
});
it('is true on a real collision marker (чужой staged)', () => {
const env = extractEnvironment(wrap('в коммит попал чужой staged-файл'), 0);
it('is true when a tool_result contains "чужой staged"', () => {
const env = extractEnvironment(
[userMsg('go'), toolResultText('error: чужой staged-файл присутствует в индексе')],
0
);
expect(env.parallel_session).toBe(true);
});
it('is false when there are no tool_result entries at all', () => {
const env = extractEnvironment([userMsg('говорим о параллельных сессиях вообще')], 0);
expect(env.parallel_session).toBe(false);
});
it('handles tool_result with content as a structured array of text blocks', () => {
const env = extractEnvironment(
[
userMsg('go'),
{
message: {
role: 'user',
content: [
{
type: 'tool_result',
tool_use_id: 't1',
content: [{ type: 'text', text: 'index.lock exists, another process is holding it' }],
},
],
},
timestamp: '2026-05-19T10:00:02.000Z',
},
],
0
);
expect(env.parallel_session).toBe(true);
});
});