前端:埋点句柄只在结构化拒单时清掉

- `runTurn` 的 catch 先读结构化拒单,只有"这一轮没接单"的拒单才清 `pendingRunAnalyticsRef`;非结构化错误(IPC 失败、命令 panic)可能发生在接单之后,句柄留着等 `turn.completed` 结算,不再让宿主侧这一轮的候选永远没人结算
- chat-composer 用例的注释同步:这一轮没接单,就没有回合终态事件来驱动结算
This commit is contained in:
2026-09-24 14:48:32 +08:00
parent 6cee61b973
commit 7941aaa660
2 changed files with 10 additions and 5 deletions
@@ -666,13 +666,18 @@ export function useDirectProjectChatController({
turnAccepted = true;
// 清单刷新统一交给 startTurn 的 finally:成功与报错路径都覆盖,且只读一次。
} catch (error) {
// 拒单那一轮没有接单,也就不会有埋点候选:句柄只清不发(发出去也只是空操作)。
if (pendingRunAnalyticsRef.current?.clientTurnId === input.clientTurnId) {
pendingRunAnalyticsRef.current = null;
}
// 命令的拒单是**结构化的**:命令返回 `Ok` 只说明接单成立,所以这条 catch 从接单化之后
// 只剩"拒单"一种输入(整轮结果由 `turn.completed` 事件回答,不再回到这里)。
const rejection = readDirectTurnRejection(error);
// 只有结构化拒单能证明"这一轮没接单"(拒单不产生回合事件、也就不会有埋点候选),句柄才
// 只清不发;非结构化错误(IPC 失败、命令 panic)可能发生在接单之后,那时必须留着句柄等
// `turn.completed` 来结算——提前清掉会让宿主侧这一轮的候选永远没有人结算。
if (
rejection &&
pendingRunAnalyticsRef.current?.clientTurnId === input.clientTurnId
) {
pendingRunAnalyticsRef.current = null;
}
if (rejection) {
const notice = directTurnRejectionNotice(rejection);
if (notice) {
@@ -420,7 +420,7 @@ export function registerChatComposerControlTests() {
);
expect(attempts).toHaveLength(1);
expect(refresh).not.toHaveBeenCalled();
// 拒单没有接单、也就没有埋点候选:句柄直接丢掉,不产生一次注定被丢弃的结算。
// 这一轮没有接单,不会有回合终态事件来驱动结算:埋点一次也不结算。
expect(
invoke.mock.calls.filter(
([command]) => command === 'settle_direct_run_analytics',