fix(secretary): разбор хвоста пропускает tool_result (ловит промпт+действия), провенанс по реальному ходу, кириллица в имени дела

Корень бага: в формате Anthropic tool_result — сообщения role:user; parseLastExchange
брал их вместо настоящего промпта, теряя текст юзера и действия. + хук форсит реальный
turn (Хайку его не знает) + work-slug принимает кириллицу.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Дмитрий
2026-06-22 07:21:09 +03:00
parent aca831eb54
commit 4253cd7114
4 changed files with 31 additions and 3 deletions
+1 -1
View File
@@ -29,7 +29,7 @@ function main() {
try { mkdirSync(dirname(FLAG), { recursive: true }); } catch { /* ignore */ }
if (cmd === 'on') {
const m = prompt.match(/секретар[а-я]*\s+(?:для\s+|по\s+)?([a-zA-Z0-9-]{2,})/);
const m = prompt.match(/секретар[а-я]*\s+(?:для\s+|по\s+)?([a-zA-Zа-яёА-ЯЁ0-9-]{2,})/);
const work = (m && m[1]) || 'general';
try { writeFileSync(FLAG, JSON.stringify({ mode: 'on', startedAtTurn: turnCount(rawFile), work, session })); } catch { /* ignore */ }
} else if (cmd === 'off') {
+4 -1
View File
@@ -61,7 +61,10 @@ async function main() {
});
const extraction = parseExtractionResponse(typeof text === 'string' ? text : '');
if (extraction) {
for (const d of extraction.decisions) { if (!Array.isArray(d.turns) || !d.turns.length) d.turns = [turn]; }
// Номер хода знает только хук — форсим реальный turn на все записи (Хайку его не знает).
for (const arr of [extraction.decisions, extraction.will, extraction.open, extraction.doneNext, extraction.supersede]) {
for (const e of (arr || [])) { e.turns = [turn]; }
}
const workDir = join(secdir, work);
const protoJson = join(workDir, 'protocol.json');
let proto = EMPTY_PROTOCOL();
+12 -1
View File
@@ -12,12 +12,23 @@ function parseLines(text) {
return entries;
}
// Настоящий промпт пользователя (НЕ tool_result): content — строка или массив с text-блоком.
// В формате Anthropic tool_result — это сообщения role:user, их пропускаем, иначе теряются
// и настоящий промпт, и все действия ассистента до него.
function isRealUserPrompt(msg) {
if (!msg || msg.role !== 'user') return false;
const c = msg.content;
if (typeof c === 'string') return true;
if (Array.isArray(c)) return c.some((b) => b && b.type === 'text');
return false;
}
/** Последний обмен из стенограммы: { user, assistant, actions:[{tool,input}] }. */
export function parseLastExchange(transcriptText) {
const entries = parseLines(transcriptText);
let u = -1;
for (let i = entries.length - 1; i >= 0; i--) {
if (entries[i] && entries[i].message && entries[i].message.role === 'user') { u = i; break; }
if (entries[i] && isRealUserPrompt(entries[i].message)) { u = i; break; }
}
const userContent = u >= 0 ? entries[u].message.content : '';
const user = typeof userContent === 'string'
+14
View File
@@ -24,4 +24,18 @@ describe('parseLastExchange', () => {
expect(ex.assistant).toBe('а');
expect(ex.actions).toEqual([]);
});
it('пропускает tool_result (role:user) — берёт настоящий промпт + все действия', () => {
const t = [
JSON.stringify({ message: { role: 'user', content: 'настоящий вопрос' } }),
JSON.stringify({ message: { role: 'assistant', content: [
{ type: 'text', text: 'думаю' }, { type: 'tool_use', name: 'Read', input: { f: 'a' } }] } }),
JSON.stringify({ message: { role: 'user', content: [{ type: 'tool_result', content: 'результат' }] } }),
JSON.stringify({ message: { role: 'assistant', content: [{ type: 'text', text: 'готово' }] } }),
].join('\n');
const ex = parseLastExchange(t);
expect(ex.user).toBe('настоящий вопрос');
expect(ex.assistant).toContain('думаю');
expect(ex.assistant).toContain('готово');
expect(ex.actions).toEqual([{ tool: 'Read', input: '{"f":"a"}' }]);
});
});