From 2bb974a10d9d046b4ef7ac20107aaa54d1468ced Mon Sep 17 00:00:00 2001 From: xcs Date: Sat, 29 Aug 2026 07:09:10 +0800 Subject: [PATCH] =?UTF-8?q?fix(loop-engine):=20=E3=80=8C=E9=85=8D=E6=96=B9?= =?UTF-8?q?=E8=B7=91=E5=AE=8C=E4=BA=86=E8=BF=98=E6=B2=A1=E7=AE=97=E5=8A=9E?= =?UTF-8?q?=E5=AE=8C=E3=80=8D=E4=BA=A4=E4=BA=BA=E6=B6=88=E6=81=AF=E6=94=B9?= =?UTF-8?q?=20kind:=20observation=20=E2=80=94=E2=80=94=20R10=20=E7=AC=AC?= =?UTF-8?q?=E5=85=AD=E5=A4=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 这条是五(六)处里最不能隐身的:它是配方没做到位时交人的那一句,人打开工作日志 想知道的第一件事就是「为什么停了」,而从前 kind: 'result' 不在 SKELETON 里, 这条消息永远进不了工作日志,单子看着像凭空卡住。只改 kind,detail 不动。 card 动作里「这一步已经发过卡,不再发第二张」那条保持 internal: true 不改 —— 它每次重入(卡开着的每一拍)都会打一遍,是典型的排障痕,machinery() 的过滤 存在的意义就是滤掉这类;摆到人面前只会把真正要看的东西淹掉。 新增回归测试:跑真实的 turn-runner.ts,让案例落到「没 guide、没发卡、doneWhen 不成立」这条分支,把捕获到的留痕喂给 structuredLog 全程,断言可见且归系统。 Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01LKMZDwMbyKzjw6Hembwvet --- .../__tests__/turn-runner-acceptance.test.ts | 32 +++++++++++++++++++ .../internal/usecase/turn-runner.ts | 9 ++++-- 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/apps/api/src/modules/loop-engine/__tests__/turn-runner-acceptance.test.ts b/apps/api/src/modules/loop-engine/__tests__/turn-runner-acceptance.test.ts index c22a919..c7ad246 100644 --- a/apps/api/src/modules/loop-engine/__tests__/turn-runner-acceptance.test.ts +++ b/apps/api/src/modules/loop-engine/__tests__/turn-runner-acceptance.test.ts @@ -2285,6 +2285,38 @@ describe('auto 步:配方在拍首跑', () => { expect(h.appended.some((a) => a.title.includes('nope.nope'))).toBe(true); }); + it('配方跑完了、这一步还没算办完(没 guide、没发卡、doneWhen 不成立)→ 交人,且这条消息真的到得了工作日志(R10 第六处)', async () => { + const h = harness({ + parsed: AUTO_MODEL, + runtime: AUTO_RUNTIME, + sfeelCondition: () => false, + steps: [ + { + item: 'S1', + guide: '', + writes: [], + reads: [], + tools: [], + // say 不写不撤、不发卡 —— 配方跑完仍落到「没算办完,交人」那条分支 + auto: { do: [{ kind: 'say', text: '已核对,尚缺关键信息' }] }, + }, + ], + }); + const out = await h.runner.runTurn(TASK); + expect(h.decided).toHaveLength(0); + expect(out.status).toBe('awaiting'); + const line = h.appended.find((a) => a.title.includes('还没算办完')); + expect(line).toBeDefined(); + /* + * **这一条是五处里最不能隐身的**(2026-08-29,coordinator 裁定):它是交人的那一句 —— + * 配方没做到位、单子就停在这儿了,人打开工作日志想知道的第一件事就是「为什么停了」。 + * 从前 kind: 'result' 不在 SKELETON 里,这一笔永远进不了工作日志,单子看着像凭空卡住。 + */ + const lines = structuredLog([asLogEntry(line!)]); + expect(lines).toHaveLength(1); + expect(lines[0]?.who).toBe('system'); + }); + it('这一步有 guide:配方跑完照常问模型(半 auto)', async () => { const h = harness({ parsed: OPEN_MODEL, diff --git a/apps/api/src/modules/loop-engine/internal/usecase/turn-runner.ts b/apps/api/src/modules/loop-engine/internal/usecase/turn-runner.ts index ff2ec10..1267080 100644 --- a/apps/api/src/modules/loop-engine/internal/usecase/turn-runner.ts +++ b/apps/api/src/modules/loop-engine/internal/usecase/turn-runner.ts @@ -2052,10 +2052,15 @@ export function createTurnRunner(deps: TurnRunnerDeps): LoopEngineService { } if (current.guide.trim() === '') { - /* 没有 guide、没发卡、doneWhen 也不成立 → 配方没做到位,是包的 bug,交人 */ + /* + * 没有 guide、没发卡、doneWhen 也不成立 → 配方没做到位,是包的 bug,交人。 + * **kind: 'observation'**(2026-08-29,R10 第六处)—— 从前是 `kind: 'result'`, + * 不在 `SKELETON` 里,这条**交人**的消息永远进不了工作日志,单子看着像凭空卡住, + * 而人打开日志想知道的第一件事就是「为什么停了」。只改 kind,detail 不动。 + */ await deps.memory.append({ taskId, - kind: 'result', + kind: 'observation', title: `「${current.item}」的配方跑完了,可这一步还没算办完 —— 交给你看一眼`, detail: { auto: current.item }, });