Archived
fix(engine): 人点「重做」不是「办完了」—— 打回即重开一轮,不再一路推到放行
用户实测:「我重做后怎么直接推进了?不应该是重做刚才那一步吗?」那一单的轨迹逐条对得上: 审核卡发出 → 人带意见交回 →「这一步已经发过卡,不再发第二张」→「自动收步:S3_review」 → 下一拍发出落账放行的验收卡。意见落进了轨迹(还被 note-anchors 逐行对上了), 而再没有任何东西会去读它。 这条缺陷 2026-08-30 全分支终审已记录成 Important 2、并留着一条坐实病灶的测试, 连「半个修法会把单子卡死」都预判对了。照那个方向修完,两半缺一不可: ① 收步那道门认答复(answerSettlesStep:custom:(人没选任何选项、写了自己的话)与 约定的 reject 都不算办完;空答复照旧算「照原样确认」); ② 同一句判断也让 carded 让位 —— 幂等挡的是「同一问被重复发出去」,而人点重做正是说 「这一轮不算,改了再问我一遍」:配方重跑(该重算的重算)再发一张,日志里说一句 「人把这一步打回重做:<意见>」。 测:坐实那条改写成钉死修复后的行为(历史诊断原样留着);新增「重做但发不出新卡也不收步」 (这一条专管收步门那一半)与「通过照旧收步」;3 条纯函数测。两半各自变异验证。 e2e w05 5/5 released —— 案例 0 现在真的发出两张审核卡(先带意见驳回、再确认通过)。 顺带撞见一条独立的老毛病,已记 debt 未修:人答得比发卡那一拍收尾还快时,回传被判 stale、 wake 又被那一拍的 park 覆盖,单子停在 awaiting-event 再没人叫醒(手工拨回 running 立刻走完)。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LKMZDwMbyKzjw6Hembwvet
This commit is contained in:
@@ -324,9 +324,9 @@ export function resendVerdict(
|
||||
* 只在「最后一次卡的往来是回传」时摆(后面又发了别的卡就说明有新问题在等,不摆);
|
||||
* 决策卡摆出选中的值,执行工单如实说「空答复 = 照原样确认」。
|
||||
*/
|
||||
export function answeredCardFact(
|
||||
export function lastCardAnswer(
|
||||
window: ReadonlyArray<{ readonly kind: string; readonly title: string; readonly detail?: Record<string, unknown> | undefined }>,
|
||||
): string {
|
||||
): { readonly title: string; readonly chose: string | undefined } | undefined {
|
||||
let at = -1;
|
||||
for (let i = window.length - 1; i >= 0; i -= 1) {
|
||||
if (window[i]?.kind === 'card-reply') {
|
||||
@@ -335,15 +335,40 @@ export function answeredCardFact(
|
||||
}
|
||||
}
|
||||
const reply = at < 0 ? undefined : window[at];
|
||||
if (reply === undefined) return '';
|
||||
const newer = window.slice(at + 1).some((t) => t.kind === 'card-sent' && !sameQuestion(t.title, reply.title));
|
||||
if (newer) return '';
|
||||
if (reply === undefined) return undefined;
|
||||
/* 后面又发了别的卡 = 有新问题在等,这条回传已经翻篇 */
|
||||
if (window.slice(at + 1).some((t) => t.kind === 'card-sent' && !sameQuestion(t.title, reply.title))) return undefined;
|
||||
const value = reply.detail?.['value'];
|
||||
const chose = typeof value === 'string' && value.trim() !== '' ? value.trim() : undefined;
|
||||
const head = `【这张卡人已经答过了】「${reply.title.length <= 60 ? reply.title : `${reply.title.slice(0, 60)}…`}」`;
|
||||
return chose === undefined
|
||||
return { title: reply.title, chose: typeof value === 'string' && value.trim() !== '' ? value.trim() : undefined };
|
||||
}
|
||||
|
||||
/**
|
||||
* **人的这条答复算不算「这一步办完了」**(2026-08-31 用户实测:「我重做后怎么直接推进了?
|
||||
* 不应该是重做刚才那一步吗?」)。
|
||||
*
|
||||
* 病:「卡答过了就收步」那条近道(全 auto 的步、人答完就收)**不看人答的是什么** ——
|
||||
* 审核卡上点「重做」并写了意见,与点「确认通过」在它眼里一模一样:当场收掉审核那一步、
|
||||
* 发出落账放行的验收卡。意见倒是落进了轨迹,可再没有任何东西会去读它。
|
||||
*
|
||||
* 平台只判它知道的那两件(选项值的语义平台不知道 —— 见 `chosenGuidance`):
|
||||
* · `custom:` 开头 = 他没选任何一个选项、写的是自己的话 —— 「重做」那一钮交上来的正是它;
|
||||
* · `reject` = 验收面孔约定的驳回项(`review-face.ts` 按这个值找那一钮)。
|
||||
* 空答复照旧算**照原样确认**(既有口径,见下面 `answeredCardFact` 那句话)。
|
||||
*/
|
||||
export function answerSettlesStep(chose: string | undefined): boolean {
|
||||
const value = chose?.trim() ?? '';
|
||||
return value !== 'reject' && !value.startsWith('custom:');
|
||||
}
|
||||
|
||||
export function answeredCardFact(
|
||||
window: ReadonlyArray<{ readonly kind: string; readonly title: string; readonly detail?: Record<string, unknown> | undefined }>,
|
||||
): string {
|
||||
const answer = lastCardAnswer(window);
|
||||
if (answer === undefined) return '';
|
||||
const head = `【这张卡人已经答过了】「${answer.title.length <= 60 ? answer.title : `${answer.title.slice(0, 60)}…`}」`;
|
||||
return answer.chose === undefined
|
||||
? `${head} → 人已交回(**空答复 = 照原样确认**,不是没答)。别再发一张一样的卡 —— 照说明书里「人交回之后」那一段办,办完 finish 这一项。`
|
||||
: `${head} → ${chosenGuidance(chose)}`;
|
||||
: `${head} → ${chosenGuidance(answer.chose)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2048,13 +2073,35 @@ export function createTurnRunner(deps: TurnRunnerDeps): LoopEngineService {
|
||||
/* 配方**只认 `liveStep`** —— 见上面 `current` 兜底那条注释(I6) */
|
||||
const current = liveStep;
|
||||
const recipe = liveStep.auto;
|
||||
/*
|
||||
* **被打回就重开一轮**(2026-08-31 用户实测:「我重做后怎么直接推进了?」)。
|
||||
*
|
||||
* 「发过就不再发」的幂等挡的是**同一问被重复发出去**;而人点「重做」正是说
|
||||
* 「这一轮不算,按我的意见改了再问我一遍」—— 那不是重复,是下一轮。
|
||||
* 于是这一拍的 `carded` 让位:配方重新跑一遍(该重算的重算),再把卡发出去。
|
||||
* 收步那条近道下面也认同一件事 —— 两处共用 `answerSettlesStep`,一处真源。
|
||||
*/
|
||||
const answer = lastCardAnswer(trail);
|
||||
const settled = answerSettlesStep(answer?.chose);
|
||||
const touchedItem = stateRuntime.items.find((i) => i.id === current.item)?.worldTouched === true;
|
||||
if (touchedItem && !settled) {
|
||||
await deps.memory.append({
|
||||
taskId,
|
||||
kind: 'observation',
|
||||
title: `人把这一步打回重做${answer?.chose?.startsWith('custom:') === true ? `:${answer.chose.slice(7).trim()}` : ''}`,
|
||||
detail: {
|
||||
item: current.item,
|
||||
hint: '这一步不算办完 —— 按他的意见重算之后再问他一遍(卡会重新发一张,不是同一张的复读)。',
|
||||
},
|
||||
});
|
||||
}
|
||||
const auto = await autoRunner.run(recipe, {
|
||||
taskId,
|
||||
stepId: current.item,
|
||||
ownerId: (await deps.tasks.of(taskId)).ownerId,
|
||||
humanCallCap: (await deps.tasks.of(taskId)).humanCallCap,
|
||||
quiet: false,
|
||||
carded: stateRuntime.items.find((i) => i.id === current.item)?.worldTouched === true,
|
||||
carded: touchedItem && settled,
|
||||
});
|
||||
if (auto.failedAt !== undefined) {
|
||||
/*
|
||||
@@ -2248,7 +2295,14 @@ export function createTurnRunner(deps: TurnRunnerDeps): LoopEngineService {
|
||||
*/
|
||||
const noDoneWhen = current.doneWhen === undefined || current.doneWhen.trim() === '';
|
||||
const carded = stateRuntime.items.find((i) => i.id === current.item)?.worldTouched === true;
|
||||
if (noDoneWhen && carded && recipeIssuesCard(recipe) && current.guide.trim() === '') {
|
||||
/*
|
||||
* **人签了字才叫办完 —— 他说重做就不是**(2026-08-31,见 `answerSettlesStep` 的头注:
|
||||
* 打回与通过从前在这儿一模一样,于是「重做」当场变成了「推进」)。
|
||||
* 走到这儿说明配方这一拍没能重新发出卡(条件不成立 / 预算见底)——
|
||||
* 那就停在这一步不收,下面那条「配方跑完了,可这一步还没算办完」把话说给人听,
|
||||
* 而不是替他把字签了。
|
||||
*/
|
||||
if (noDoneWhen && carded && settled && recipeIssuesCard(recipe) && current.guide.trim() === '') {
|
||||
const active = readyToWork(parsed, stateRuntime, current.item);
|
||||
const settled = transitionItem(active, current.item, 'Completed');
|
||||
await deps.memory.append({
|
||||
|
||||
Reference in New Issue
Block a user