Archived
fix(loop-engine): 有卡在等人交回时配方写下新行,收拍前重推画面 —— 短路收拍从不推帧
浏览器走查实测(ACT-WORK-05 数据工单):人给候选料选好芯片类型、按保存,S1_inputs 的
配方在 quiet 模式下铺出 7 行新参数骨架、红带同步点名要填这 7 格,而 `runTurn` 的
「叫了人就等人」分支(spec §5.4)写完就直接 `return { status: 'awaiting' }` —— 真正的
「推一帧」逻辑住在同一函数里更靠后的位置(action 循环之后 / `issuedCardId` 分支),
这条早退根本到不了那里。人刷新页面看到的仍是保存前那份服务端快照(件是渲好推来的,
重放不重渲),被红带点名要填的格子屏上根本不存在,走查到此卡死。
修法:quiet 分支里捕获 `autoRunner.run()` 的返回值,若这一拍真写下或撤掉过行
(`wrote>0 || dropped>0`)且当前在办的这一步声明了 `view`,收拍前调用同一个
`showView` 重推 —— 与 `issuedCardId` 分支「声明了画面才推」同一条纪律;quiet 模式
不迁移计划项状态,`runtime` 用的还是拍首那份,不会重犯 2026-08-23 那次「推错哪一步
画面」的错(这次没跨步,只是把同一步最新的草稿重渲一遍)。
回归测试用真 `createTurnRunner`/真 `createAutoRunner`(不手抄收拍逻辑),给共享 harness
加 `pushedScopes` 记录每次推帧的 scope,断言画面里真带着刚写的那一行(内容比对,不是
计数)。变异验证:git stash 掉修复后该测试从 157/157 变 156 passed + 1 failed
(`expected [] to deeply equal ['input-supplement']`),stash pop 后回到 157/157。
`progress.md` H49 记录过一个高度相似的怀疑:e2e 反复撞见的「答完卡不推进」(案例4×3、
案例3×1,advancing_until 全程为空、零报错)——两者终态日志的最后一行逐字相同
(`等人交回:数据工单… —— 这一拍不动`,task_memory 定格在同一条、之后再无记录)。
候选缺库类案例的两阶段表单(选类型→保存→配方铺出新参数格→还要再填)恰好依赖画面
重推来发现第二轮要填什么;这条早退不推帧能完整解释「零报错 + 永久卡死」。未另跑
e2e 坐实(任务明令不跑,浏览器里留着一单要用来验证),记为高置信度推断。
pnpm gates 退出码 0(2448 passed | 34 skipped,基线 2447 + 本次新增 1 条)。
E04(crm-relationship-coverage)92/92、I01(brand-content-toolkit)55/55,均 exit 0。
manifest.ts 里那处与本次无关的未提交版本号改动(2.1.5→2.1.6)未动、未纳入本提交。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LKMZDwMbyKzjw6Hembwvet
This commit is contained in:
@@ -63,6 +63,8 @@ type Harness = {
|
|||||||
readonly pushed: string[];
|
readonly pushed: string[];
|
||||||
/** 每次推帧时「已写下几行」—— 帧推在行动之前的话,它是 0 */
|
/** 每次推帧时「已写下几行」—— 帧推在行动之前的话,它是 0 */
|
||||||
readonly writesAtPush: number[];
|
readonly writesAtPush: number[];
|
||||||
|
/** 每次推帧时携带的 scope(`showView` 现读的 `deps.data.view()`)—— 断言「画面里真的有新行」走这里 */
|
||||||
|
readonly pushedScopes: Array<Record<string, unknown>>;
|
||||||
/** 这一拍写下的全部留痕 —— 断言「被拒/无事发生」走这里 */
|
/** 这一拍写下的全部留痕 —— 断言「被拒/无事发生」走这里 */
|
||||||
readonly appended: Array<{ kind: string; title: string; detail: Record<string, unknown> | undefined }>;
|
readonly appended: Array<{ kind: string; title: string; detail: Record<string, unknown> | undefined }>;
|
||||||
/** 真被调到的工具名 —— 断言「拦没拦住调用」走这里 */
|
/** 真被调到的工具名 —— 断言「拦没拦住调用」走这里 */
|
||||||
@@ -110,6 +112,7 @@ function harness(
|
|||||||
const dropped: Harness['dropped'] = [];
|
const dropped: Harness['dropped'] = [];
|
||||||
const pushed: string[] = [];
|
const pushed: string[] = [];
|
||||||
const writesAtPush: number[] = [];
|
const writesAtPush: number[] = [];
|
||||||
|
const pushedScopes: Harness['pushedScopes'] = [];
|
||||||
const appended: Harness['appended'] = [];
|
const appended: Harness['appended'] = [];
|
||||||
const toolCalls: string[] = [];
|
const toolCalls: string[] = [];
|
||||||
const listened: Array<{ eventType: string }> = [];
|
const listened: Array<{ eventType: string }> = [];
|
||||||
@@ -121,6 +124,7 @@ function harness(
|
|||||||
dropped,
|
dropped,
|
||||||
pushed,
|
pushed,
|
||||||
writesAtPush,
|
writesAtPush,
|
||||||
|
pushedScopes,
|
||||||
appended,
|
appended,
|
||||||
toolCalls,
|
toolCalls,
|
||||||
listened,
|
listened,
|
||||||
@@ -210,10 +214,11 @@ function harness(
|
|||||||
window: async () => over.trail ?? [],
|
window: async () => over.trail ?? [],
|
||||||
} as never,
|
} as never,
|
||||||
views: {
|
views: {
|
||||||
push: async (input: { template: string }) => {
|
push: async (input: { template: string; scope?: Record<string, unknown> }) => {
|
||||||
pushed.push(input.template);
|
pushed.push(input.template);
|
||||||
// 推这一帧时已经写下了几行 —— 帧推早了就是 0(见「推在行动之后」那条)
|
// 推这一帧时已经写下了几行 —— 帧推早了就是 0(见「推在行动之后」那条)
|
||||||
writesAtPush.push(written.length);
|
writesAtPush.push(written.length);
|
||||||
|
pushedScopes.push(input.scope ?? {});
|
||||||
return { pushed: (over.templates ?? ['case-overview']).includes(input.template), templates: over.templates ?? ['case-overview'] };
|
return { pushed: (over.templates ?? ['case-overview']).includes(input.template), templates: over.templates ?? ['case-overview'] };
|
||||||
},
|
},
|
||||||
} as never,
|
} as never,
|
||||||
@@ -2266,6 +2271,45 @@ describe('auto 步:配方在拍首跑', () => {
|
|||||||
expect(h.issued).toHaveLength(0);
|
expect(h.issued).toHaveLength(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
|
* **这一拍真写下过东西,收拍前要把画面重推一遍**(2026-08-30,W05 数据工单浏览器走查实测:
|
||||||
|
* 人给候选料选好芯片类型、按保存,配方铺出 7 行新参数、红带也点名要填这 7 格 —— 而这条路
|
||||||
|
* 从不推帧,任务停在 `awaiting-event`,人刷新页面看到的仍是保存前那份快照(件是服务端渲好
|
||||||
|
* 推来的快照,重放不重渲),被点名的格子屏上根本不存在,走查到此卡死。
|
||||||
|
* 上一条用例(不发新卡)只断言了 `written`/`issued`,从没问过画面有没有跟上;这一条补上。
|
||||||
|
*/
|
||||||
|
it('有卡在等人交回:配方写下新行 → 收拍前重推画面,画面里带着刚写的那一行', async () => {
|
||||||
|
const h = harness({
|
||||||
|
parsed: AUTO_MODEL,
|
||||||
|
runtime: AUTO_RUNTIME,
|
||||||
|
unanswered: [{ id: 'card-9', title: '补数据' }],
|
||||||
|
sfeelCondition: () => false,
|
||||||
|
templates: ['input-supplement'],
|
||||||
|
// 画面的 scope 现读「已写下的那几行」—— 与 `showView` 实际调用 `deps.data.view()` 同一条路
|
||||||
|
viewFn: (written) => ({ pns: written.map((w) => w.key['pn']) }),
|
||||||
|
steps: [
|
||||||
|
{
|
||||||
|
item: 'S1',
|
||||||
|
guide: '',
|
||||||
|
writes: [],
|
||||||
|
reads: [],
|
||||||
|
tools: [],
|
||||||
|
view: 'input-supplement',
|
||||||
|
auto: {
|
||||||
|
do: [{ kind: 'project', into: 'quote_intake.record', from: { once: true }, fields: { pn: 'MP2596-5.0-WALK', eau: 1 } }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
const out = await h.runner.runTurn(TASK);
|
||||||
|
expect(h.written).toHaveLength(1);
|
||||||
|
expect(out.status).toBe('awaiting');
|
||||||
|
// 画面真被重推了一次 —— 不是「一帧不推,留着保存前那份快照」
|
||||||
|
expect(h.pushed).toEqual(['input-supplement']);
|
||||||
|
// 推的时候真带着刚写的那一行,不是拍首、写之前的那份旧 scope
|
||||||
|
expect(h.pushedScopes.at(-1)?.['pns']).toEqual(['MP2596-5.0-WALK']);
|
||||||
|
});
|
||||||
|
|
||||||
it('配方抛错(地址不在台账声明里)→ 交人,不问模型、不重试', async () => {
|
it('配方抛错(地址不在台账声明里)→ 交人,不问模型、不重试', async () => {
|
||||||
const h = harness({
|
const h = harness({
|
||||||
parsed: AUTO_MODEL,
|
parsed: AUTO_MODEL,
|
||||||
|
|||||||
@@ -1727,8 +1727,9 @@ export function createTurnRunner(deps: TurnRunnerDeps): LoopEngineService {
|
|||||||
new Set(runtime.items.filter((i) => i.state === 'Completed' || i.state === 'Terminated').map((i) => i.id)),
|
new Set(runtime.items.filter((i) => i.state === 'Completed' || i.state === 'Terminated').map((i) => i.id)),
|
||||||
[...enabled.inProgress, ...enabled.entries],
|
[...enabled.inProgress, ...enabled.entries],
|
||||||
);
|
);
|
||||||
|
let quietWrote = false;
|
||||||
if (quietStep?.auto !== undefined) {
|
if (quietStep?.auto !== undefined) {
|
||||||
await autoRunner.run(quietStep.auto, {
|
const outcome = await autoRunner.run(quietStep.auto, {
|
||||||
taskId,
|
taskId,
|
||||||
stepId: quietStep.item,
|
stepId: quietStep.item,
|
||||||
ownerId: (await deps.tasks.of(taskId)).ownerId,
|
ownerId: (await deps.tasks.of(taskId)).ownerId,
|
||||||
@@ -1736,6 +1737,27 @@ export function createTurnRunner(deps: TurnRunnerDeps): LoopEngineService {
|
|||||||
quiet: true,
|
quiet: true,
|
||||||
carded: true,
|
carded: true,
|
||||||
});
|
});
|
||||||
|
quietWrote = outcome.wrote > 0 || outcome.dropped > 0;
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
* **这一拍真写下过东西,收拍前把画面重推一遍**(2026-08-30,W05 数据工单浏览器走查
|
||||||
|
* 实测:人给候选料选好芯片类型、按保存,上面的配方铺出 7 行新参数、红带也点名要填
|
||||||
|
* 这 7 格,而这条路从不推帧 —— 任务停在 `awaiting-event`,人刷新页面看到的仍是保存前
|
||||||
|
* 那份快照(件是服务端渲好推来的快照,重放不重渲),被点名的格子屏上根本不存在,
|
||||||
|
* 走查到此卡死。**这极可能与 e2e 反复撞见的「答完卡不推进」异常同一个短路**:
|
||||||
|
* 数据工单的下一轮填报要看这一步刚铺出来的新字段,画面不重推,人 / 脚本都无从下手,
|
||||||
|
* 之后再没有任何事件会把它唤醒。
|
||||||
|
*
|
||||||
|
* 走到这儿状态机什么都没变(quiet 只跑 project/say,不迁移计划项),`runtime` 仍是
|
||||||
|
* 拍首那份,画面按 `quietStep` 声明的那张推就够 —— 与「拍首跑配方」发卡那条同一份
|
||||||
|
* 判断(下面 `issuedCardId` 那处:「这一步声明了画面才推,没声明就沿用上一帧」)。
|
||||||
|
*/
|
||||||
|
if (quietWrote && quietStep?.view !== undefined && quietStep.view !== '') {
|
||||||
|
try {
|
||||||
|
await showView(taskId, quietStep.view, runtime, quietStep.view);
|
||||||
|
} catch {
|
||||||
|
// 呈现失败不影响回路:画面丢一帧,事实一件不少(与末尾推帧同一条纪律)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
await deps.memory.append({
|
await deps.memory.append({
|
||||||
taskId,
|
taskId,
|
||||||
|
|||||||
Reference in New Issue
Block a user