/** * **件上按了保存之后**(2026-08-29,spec §5.4)。 * * 两件事按序: * ① 重推件 —— 数据变了,算出来的那些(红带缺几格 / 分布条 / 去向)得重新求值; * ② **叫醒任务** —— auto 步的配方这才有机会按最新的草稿把该铺的行铺出来。 * 人挑了芯片类型、按了保存,参数格就在这一下之后出现。 * * 从前只有两条回传路由调 `wake`;保存不是回传,于是「保存即铺格」永远等不到那一拍。 * * **只在这一单有未答的卡时才叫醒**(2026-08-29 终审 F1)。`TaskService.wake` 对 * `awaiting-event` / `awaiting-human` 一律拨成 `running`,推进器随即推一拍并**记一步预算**。 * 而现有九个活动包一个 `auto` 都没有:人每按一次保存就烧掉一步预算、界面闪一次 * 「AI 正在推进这单」,那一拍走到 `cards.unanswered` 就原路返回;更糟的是任务本来 * `awaiting-human`(等人裁决)时被叫醒,那一拍结束会把状态写成 `awaiting-event` —— * 从「等人办事」降级成「等外部回音」,而实际等的仍是人。**这是界面撒谎。** * 而「保存即铺格」的场景恰恰都有卡在场(人正对着那张卡在件上填),验收态一点都不碰。 * * **叫不醒不抬手**:保存本身是成功的,不该因为叫不醒把 200 变成 500 —— * 下一次推进器扫到它照样会跑。但**不再静默**:它是「保存即铺格」的唯一触发器, * 吞掉就等于人按了保存、格子没出现、系统一个字不说。 */ import type { TaskId } from '@contracts'; function grumble(what: string, error: unknown): void { const why = error instanceof Error ? error.message : String(error); globalThis.process.stderr.write(`[save-wake] ${what}:${why}\n`); } export async function afterSave( deps: { readonly views: { repush(taskId: TaskId): Promise }; readonly task: { wake(taskId: TaskId): Promise }; /** 这一单正等人交回的卡(工单没办结的 + 对话卡没作答的)—— 用既有那条,不新造 */ readonly humanTool: { pendingCards(taskId: TaskId): Promise> }; }, taskId: TaskId, rows: number, ): Promise { if (rows <= 0) return; await deps.views.repush(taskId); /* 问不出来就不叫醒:宁可少推一拍(推进器下一轮照样扫到它),也不平白烧一步预算 */ const pending = await deps.humanTool.pendingCards(taskId).catch((error: unknown) => { grumble('问不出这一单有没有卡在等人,这次不叫醒', error); return []; }); if (pending.length === 0) return; await deps.task.wake(taskId).catch((error: unknown) => grumble('叫醒失败,件已重推,等推进器下一轮', error)); }