From 8496b52bd1502f2cce2b8e839de806cd1726ecc6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=BC=D0=B8=D1=82=D1=80=D0=B8=D0=B9?= Date: Wed, 20 May 2026 10:58:37 +0300 Subject: [PATCH] fix(observer): narrow parallel_session detector to tool_result evidence (C-2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- tools/observer-transcript-parser.mjs | 32 ++++++++- tools/observer-transcript-parser.test.mjs | 81 +++++++++++++++++++---- 2 files changed, 100 insertions(+), 13 deletions(-) diff --git a/tools/observer-transcript-parser.mjs b/tools/observer-transcript-parser.mjs index ae7ec319..b4f4d875 100644 --- a/tools/observer-transcript-parser.mjs +++ b/tools/observer-transcript-parser.mjs @@ -165,11 +165,41 @@ export function extractEnvironment(allEntries, turnStartIdx) { // Only strong collision evidence — a bare mention of "parallel sessions" is // not a signal (best-effort per spec R2; prefer false-negative over false-positive). - const parallel_session = /чужой staged|foreign git index|index\.lock|another git process/i.test(rawTurn); + // Scope NARROWED to tool_result content (real command output / Bash stderr): prose + // mentions in user prompts / assistant text — including analysis text that + // references collision phrases — must not trigger. Fixes live FP (episode line 20). + const parallel_session = /чужой staged|foreign git index|index\.lock|another git process/i.test( + collectToolResultText(turn) + ); return { economy_level, model, post_compaction, session_turn, parallel_session }; } +/** + * Collect text content from tool_result blocks in the turn — the only surface + * trusted for parallel_session collision evidence (see extractEnvironment). + * Supports both string content and the structured array form + * (`content: [{ type: 'text', text }]`). + */ +function collectToolResultText(turn) { + const parts = []; + for (const e of turn) { + const content = e && e.message && Array.isArray(e.message.content) ? e.message.content : []; + for (const b of content) { + if (!b || b.type !== 'tool_result') continue; + const c = b.content; + if (typeof c === 'string') { + parts.push(c); + } else if (Array.isArray(c)) { + for (const sub of c) { + if (sub && typeof sub.text === 'string') parts.push(sub.text); + } + } + } + } + return parts.join('\n'); +} + /** Task size: total tool calls + unique file paths touched (per spec §3, gap-resolution 2). */ export function extractTaskSize(turn) { let tool_calls = 0; diff --git a/tools/observer-transcript-parser.test.mjs b/tools/observer-transcript-parser.test.mjs index 52dd46b8..05097223 100644 --- a/tools/observer-transcript-parser.test.mjs +++ b/tools/observer-transcript-parser.test.mjs @@ -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); }); });