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
+31 -1
View File
@@ -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;