判据 ⑥ 解锁:容忍分支推得动汇合,汇合不再等一个永不到来的网关

三处修:

① `joinReady` 把**网关直连汇合的直通道**也算作一票要等的分支。
   网关不是责任单元、永远不会有实例,于是汇合永远等不齐,整条流卡死。
   改为:来源是网关的入边不参与等待 —— 分叉一发生它就算过了。
   (统一报价图里这条直通道就是 TYPE-A 快路。)加了回归守卫测试。

② 容忍(tolerate)只把实例标成「已消化」就收手,不往下推 ——
   `joinReady` 认 tolerated,可没人再去问一次汇合够不够齐。改为标完即续推。

③ 蒸馏钩子失败会把已落定的终态掀翻(治理台终止返回 500,任务卡在中间态)。
   蒸馏是进化管线的入口、不是终态的一部分:失败就失败,终态照落。
   失败也不再往任务记忆里追加 —— 此刻记忆已封卷,那个守卫拒得对。

结果:A1 released → G2 分叉 → A3 / A2 两个 loop 起在正确工位 →
按声明容忍收束 → **汇合回齐 → A4 成本定价起到冯兵工位**。

gates: tsc / eslint / depcruise / 397 测试 全绿

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KeDBx2PHLavbURXB8LJJ7m
This commit is contained in:
2026-07-27 05:03:58 +08:00
co-authored by Claude Opus 5
parent c68a562db1
commit 9201f6b094
6 changed files with 109 additions and 8 deletions
@@ -87,14 +87,22 @@ export function selectEdges(
return outgoing;
}
/** AND 汇合是否回齐:全部入边的来源分支都已收敛 */
/**
* AND 汇合是否回齐:全部**责任分支**的来源都已收敛。
*
* 网关本身不是责任单元、永远不会有实例,所以来源是网关的入边(分叉直连汇合的**直通道**)
* 不参与等待 —— 分叉一发生它就算过了。若把它也算进去,汇合会永远等一个不可能到来的
* 「网关收敛」,整条流卡死在这里。
*/
export function joinReady(
pkg: ProcessPackage,
joinNodeId: string,
converged: ReadonlySet<string>,
tolerated: ReadonlySet<string>,
): boolean {
const inbound = pkg.edges.filter((e) => e.to === joinNodeId);
const isGateway = (nodeId: string): boolean =>
pkg.nodes.find((n) => n.id === nodeId)?.kind === 'gateway';
const inbound = pkg.edges.filter((e) => e.to === joinNodeId && !isGateway(e.from));
// 容忍分支以「已消化」计,不阻汇合
return inbound.every((e) => converged.has(e.from) || tolerated.has(e.from));
}
@@ -337,6 +337,16 @@ export function createProcessService(deps: ProcessServiceDeps): ProcessService {
if (target !== undefined) await enter(record, pkg, target, { __terminal: terminal, __from: nodeId });
}
if (disposition.kind === 'tolerate') {
/*
* 容忍 = 这条支线的失败被吸收,**流继续往下走**。
* 只把实例标成「已消化」而不往下推,汇合就永远等不到它:
* `joinReady` 认 tolerated,可没人再去问一次它够不够齐。
* 载荷为空 —— 容忍分支本就没有产出,下游的必填映射不得依赖它(闭合校验已拦)。
*/
await advanceFrom(record, pkg, nodeId, {});
}
return this.refreshState(workflowId);
},