fix(observer): exclude synthetic user-role messages from turn detection

Root cause (systematic-debugging): isRealUserPrompt treated skill-content
("Base directory for this skill:"), local-command output
(<local-command-stdout>), and interrupt markers as genuine prompts.
findTurnStart then anchored a turn on the synthetic message — the turn
slice missed the genuine prompt's UserPromptSubmit hook_additional_context
attachment → economy_level: null, wrong prompt_signal/task_classification.
Same cause made extractLastUserPromptText return skill content, so the
Stop-hook routing-gate false-positive-blocked autonomous §12 skill
invocations (detectMethodDirected saw the node name in skill text).

Fix: SYNTHETIC_PROMPT_MARKERS + isSyntheticPrompt — isRealUserPrompt
returns false for synthetic messages. One fix closes both the
economy_level capture gap and the 2nd routing-gate FP class.

160/160 tools tests GREEN (+3 new).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Дмитрий
2026-05-19 13:22:01 +03:00
parent 26a7e95c37
commit 647394b037
2 changed files with 113 additions and 3 deletions
+27 -3
View File
@@ -36,16 +36,40 @@ function parseLines(text) {
return { entries, broken, total };
}
// A genuine user prompt (turn boundary) — not a tool_result carrier message.
// Synthetic user-role messages — NOT genuine prompts, must not be turn boundaries.
// Skill invocation content, local slash-command output/invocation, interrupt markers
// are recorded with role:'user' but carry no UserPromptSubmit hook context.
const SYNTHETIC_PROMPT_MARKERS = [
'Base directory for this skill:',
'<local-command-stdout>',
'<local-command-caveat>',
'<command-name>',
'[Request interrupted by user]',
];
function isSyntheticPrompt(text) {
const t = String(text || '').trimStart();
return SYNTHETIC_PROMPT_MARKERS.some((m) => t.startsWith(m));
}
// A genuine user prompt (turn boundary) — not a tool_result carrier nor a
// synthetic skill/command/interrupt message.
function isRealUserPrompt(entry) {
const msg = entry && entry.message;
if (!msg || msg.role !== 'user') return false;
const c = msg.content;
if (typeof c === 'string') return c.trim().length > 0;
if (typeof c === 'string') {
return c.trim().length > 0 && !isSyntheticPrompt(c);
}
if (Array.isArray(c)) {
const hasToolResult = c.some((b) => b && b.type === 'tool_result');
const hasText = c.some((b) => b && b.type === 'text');
return hasText && !hasToolResult;
if (!hasText || hasToolResult) return false;
const text = c
.filter((b) => b && b.type === 'text')
.map((b) => b.text || '')
.join(' ');
return !isSyntheticPrompt(text);
}
return false;
}