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:
@@ -502,3 +502,89 @@ describe('parseTranscript — user_chose_from_options', () => {
|
||||
expect(ep.decision_provenance.kind).toBe('user_directed_method');
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseTranscript — synthetic user messages skipped', () => {
|
||||
it('captures economy_level from genuine prompt even when skill-content message follows', () => {
|
||||
const lines = [
|
||||
JSON.stringify({
|
||||
type: 'user',
|
||||
timestamp: '2026-05-19T10:00:00.000Z',
|
||||
sessionId: 's1',
|
||||
message: { role: 'user', content: 'почини баг в парсере' },
|
||||
}),
|
||||
JSON.stringify({
|
||||
type: 'attachment',
|
||||
timestamp: '2026-05-19T10:00:00.500Z',
|
||||
sessionId: 's1',
|
||||
attachment: {
|
||||
type: 'hook_additional_context',
|
||||
hookName: 'UserPromptSubmit',
|
||||
content: ['=== ECONOMY MODE: 5% (тест) ===\nинструкции режима...'],
|
||||
},
|
||||
}),
|
||||
JSON.stringify({
|
||||
type: 'assistant',
|
||||
timestamp: '2026-05-19T10:00:01.000Z',
|
||||
sessionId: 's1',
|
||||
message: { role: 'assistant', content: [{ type: 'text', text: 'смотрю' }] },
|
||||
}),
|
||||
JSON.stringify({
|
||||
type: 'user',
|
||||
timestamp: '2026-05-19T10:00:02.000Z',
|
||||
sessionId: 's1',
|
||||
message: { role: 'user', content: 'Base directory for this skill: C:\\path\\skill\n\n# Some Skill\nbody' },
|
||||
}),
|
||||
JSON.stringify({
|
||||
type: 'assistant',
|
||||
timestamp: '2026-05-19T10:00:03.000Z',
|
||||
sessionId: 's1',
|
||||
message: { role: 'assistant', content: [{ type: 'text', text: 'готово' }] },
|
||||
}),
|
||||
].join('\n');
|
||||
const ep = parseTranscript(lines, 'fallback');
|
||||
expect(ep.environment.economy_level).toBe(5);
|
||||
expect(ep.primary_rationale.task_classification).toBe('bugfix');
|
||||
});
|
||||
|
||||
it('extractLastUserPromptText skips skill-content and returns genuine prompt', () => {
|
||||
const lines = [
|
||||
JSON.stringify({
|
||||
type: 'user',
|
||||
timestamp: '2026-05-19T10:00:00.000Z',
|
||||
sessionId: 's1',
|
||||
message: { role: 'user', content: 'добавь колонку Город' },
|
||||
}),
|
||||
JSON.stringify({
|
||||
type: 'user',
|
||||
timestamp: '2026-05-19T10:00:02.000Z',
|
||||
sessionId: 's1',
|
||||
message: { role: 'user', content: 'Base directory for this skill: C:\\x\n# Skill body' },
|
||||
}),
|
||||
].join('\n');
|
||||
expect(extractLastUserPromptText(lines)).toBe('добавь колонку Город');
|
||||
});
|
||||
|
||||
it('extractLastUserPromptText skips local-command output and interrupt markers', () => {
|
||||
const lines = [
|
||||
JSON.stringify({
|
||||
type: 'user',
|
||||
timestamp: '2026-05-19T10:00:00.000Z',
|
||||
sessionId: 's1',
|
||||
message: { role: 'user', content: 'сделай X' },
|
||||
}),
|
||||
JSON.stringify({
|
||||
type: 'user',
|
||||
timestamp: '2026-05-19T10:00:01.000Z',
|
||||
sessionId: 's1',
|
||||
message: { role: 'user', content: '[Request interrupted by user]' },
|
||||
}),
|
||||
JSON.stringify({
|
||||
type: 'user',
|
||||
timestamp: '2026-05-19T10:00:02.000Z',
|
||||
sessionId: 's1',
|
||||
message: { role: 'user', content: '<local-command-stdout>some output</local-command-stdout>' },
|
||||
}),
|
||||
].join('\n');
|
||||
expect(extractLastUserPromptText(lines)).toBe('сделай X');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user