From 124dbeef95a3ee3f86c602ced44f6daf4763e47b 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: Tue, 23 Jun 2026 09:28:14 +0300 Subject: [PATCH] =?UTF-8?q?feat(secretary):=20reconcileTurn=20=D0=BF=D1=80?= =?UTF-8?q?=D0=BE=D0=BA=D0=B8=D0=B4=D1=8B=D0=B2=D0=B0=D0=B5=D1=82=20step?= =?UTF-8?q?=20=D0=B2=20=D1=80=D0=B5=D0=B7=D1=83=D0=BB=D1=8C=D1=82=D0=B0?= =?UTF-8?q?=D1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Task 3/5 плана. stampProvenance отдаёт фиксированную форму без step, поэтому reconcileTurn оборачивает результат: finish() прикладывает parsed.step к возвращаемому объекту (транзитно), при отсутствии — поля нет. Свод 111/111. Co-Authored-By: Claude Opus 4.8 (1M context) --- tools/secretary-reconcile.mjs | 6 ++++-- tools/secretary-reconcile.test.mjs | 8 ++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/tools/secretary-reconcile.mjs b/tools/secretary-reconcile.mjs index 5df00bf..44881dd 100644 --- a/tools/secretary-reconcile.mjs +++ b/tools/secretary-reconcile.mjs @@ -275,13 +275,15 @@ export async function reconcileTurn({ proto, ex, turn, session, callModel, diag catch (e) { report({ reason: 'model-threw', error: String((e && e.message) || e) }); return null; } const parsed = parseReconcileResponse(typeof text === 'string' ? text : ''); if (!parsed) { report({ reason: 'bad-json' }); return null; } // кривой JSON — прежний протокол цел + const step = parsed.step || null; + const finish = (p) => { const out = collapseProtocol(p); return step ? { ...out, step } : out; }; const returned = collapseProtocol(parsed); const guard = reconcileGuard(clean, returned); - if (guard.ok) return collapseProtocol(stampProvenance(clean, returned, turn, session)); + if (guard.ok) return finish(stampProvenance(clean, returned, turn, session)); // Потеряны строки → НЕ выкидываем ход: возвращаем пропавшие старые строки на место (модель-агностично). // Что модель уронила — хук вернул; что обновила (закрыла вопрос, добавила решение) — сохранено. report({ reason: 'guard-restored', lost: guard.lost }); - return collapseProtocol(stampProvenance(clean, restoreLostLines(clean, returned), turn, session)); + return finish(stampProvenance(clean, restoreLostLines(clean, returned), turn, session)); } /** Протокол к записи независимо от исхода reconcile: при успехе база — updated, при срыве — diff --git a/tools/secretary-reconcile.test.mjs b/tools/secretary-reconcile.test.mjs index e681c27..005a6b0 100644 --- a/tools/secretary-reconcile.test.mjs +++ b/tools/secretary-reconcile.test.mjs @@ -164,6 +164,14 @@ describe('reconcileTurn', () => { expect(out).toBeNull(); expect(n).toBe(1); }); + it('проброс step из ответа модели в результат; без step — поля нет', async () => { + const withStep = async () => '{ "subject":"дело", "decisions":[{"text":"A","struck":false}], "open":[{"text":"Q?","struck":true}], "will":[], "doneNext":[], "step":{"user":"u","assistant":"a"} }'; + const r1 = await reconcileTurn({ proto, ex, turn: 5, session: 's1', callModel: withStep }); + expect(r1.step).toEqual({ user: 'u', assistant: 'a' }); + const noStep = async () => '{ "subject":"дело", "decisions":[{"text":"A","struck":false}], "open":[{"text":"Q?","struck":true}], "will":[], "doneNext":[] }'; + const r2 = await reconcileTurn({ proto, ex, turn: 5, session: 's1', callModel: noStep }); + expect(r2.step).toBeUndefined(); + }); }); import { mergeTurnIntoProtocol, formatReconcileLogLine, restoreLostLines, collapseProtocol } from './secretary-reconcile.mjs';