删掉 TurnAlreadyRunning:入队化后没有并发撞单的生产点

- 删 DirectTurnError::TurnAlreadyRunning 变体,以及 is_reportable / Display 的对应分支与专属用例
- 前端 directTurnEnqueueFailureNotice 删掉 turnAlreadyRunning 分支;ts-rs 绑定同步删除该变体
- 结构化载荷用例的样本换成 queueFull,本地说明用例的文案样本换成队列已满提示
This commit is contained in:
2026-09-30 10:39:53 +08:00
parent ecac9d8e15
commit 4e8b269816
5 changed files with 9 additions and 69 deletions
@@ -390,13 +390,6 @@ pub(crate) enum DirectTurnError {
ClientTurnIdMissing,
/// `clientTurnId` 形状非法:长度与字符集由宿主定,界面按同一份约束生成。
ClientTurnIdMalformed { min_chars: usize, max_chars: usize },
/// 同一项目已有另一条回合在跑(或同一 `clientTurnId` 并发复用)。
///
/// 两个身份都要带上,因为"撞的是哪一轮"决定界面该不该动当前回合。
TurnAlreadyRunning {
existing_invocation_id: String,
incoming_invocation_id: String,
},
/// 待发消息队列已满:这一条没进队,等前面几条发完再发。
///
/// 上限只落在宿主这一处(`MAX_PENDING_DIRECT_TURNS`),随载荷带出去,界面不自己数一份。
@@ -487,7 +480,6 @@ impl DirectTurnError {
Self::ClientTurnIdMissing
| Self::ClientTurnIdMalformed { .. }
| Self::QueueFull { .. }
| Self::TurnAlreadyRunning { .. }
// 项目目录锚不定(符号链接 / 权限 / 目录被删)与目录不存在同类:都是用户能自己修好的
// 文件系统事实,诊断文案不该顶替那句"无法锚定 Direct 调用项目目录:{cause}"。
| Self::ProjectRootUnanchored { .. }
@@ -653,23 +645,6 @@ impl fmt::Display for DirectTurnError {
formatter,
"待发消息已达上限(最多 {limit} 条),请等前面几条发完再发送"
),
Self::TurnAlreadyRunning {
existing_invocation_id,
incoming_invocation_id,
} => {
// 两条文案按身份是否相同分岔,但**不再有机器前缀**:界面按 `error.type` 与其
// 两个身份字段分流,不解析文案(前缀曾经是协议约定,现在只是噪声)。
if existing_invocation_id == incoming_invocation_id {
formatter.write_str(
"同一轮消息仍在处理中,已拒绝并发复用同一 clientTurnId;请等它结束或点「终止」后再发送",
)
} else {
write!(
formatter,
"当前项目已有另一条 Direct 客户端回合正在运行,已拒绝混用付费生成身份;可在输入盒点「终止」结束它,或等它结束后再发送"
)
}
}
Self::ProjectRootUnanchored { cause } => {
write!(formatter, "无法锚定 Direct 调用项目目录:{cause}")
}
@@ -1022,11 +997,6 @@ mod tests {
policy_detail: "项目权限策略拒绝执行:conversation.write".into(),
}
.is_reportable());
assert!(!DirectTurnError::TurnAlreadyRunning {
existing_invocation_id: "turn-1".into(),
incoming_invocation_id: "turn-2".into(),
}
.is_reportable());
// 回合级失败在上游已经写过诊断。
assert!(
!DirectTurnError::turn_failed(DirectCodexFailureStage::CodeGeneration, "模型失败")
@@ -1034,26 +1004,6 @@ mod tests {
);
}
/// 并发拒绝的两条文案按身份是否相同分岔,身份必须原样带出来。
#[test]
fn concurrent_rejection_keeps_both_invocations_and_splits_the_copy() {
let same = DirectTurnError::TurnAlreadyRunning {
existing_invocation_id: "turn-1".into(),
incoming_invocation_id: "turn-1".into(),
};
assert!(same.to_string().contains("同一轮消息仍在处理中"));
let different = DirectTurnError::TurnAlreadyRunning {
existing_invocation_id: "turn-1".into(),
incoming_invocation_id: "turn-2".into(),
};
assert!(different
.to_string()
.contains("另一条 Direct 客户端回合正在运行"));
assert!(!different
.to_string()
.starts_with("direct-codex-turn-already-running:"));
}
/// 边界序列化:字符串只由 Display 生成,且与改造前的可见文本一致。
#[test]
fn boundary_serialization_uses_display() {
@@ -102,7 +102,6 @@ export function directTurnEnqueueFailureNotice(
case 'clientTurnIdMissing':
case 'clientTurnIdMalformed':
case 'queueFull':
case 'turnAlreadyRunning':
case 'projectRootUnanchored':
case 'projectRootUnusable':
case 'permissionRejected':
@@ -9,11 +9,6 @@ import type { DirectTurnDeadline } from './DirectTurnDeadline';
export type DirectTurnError =
| { type: 'clientTurnIdMissing' }
| { type: 'clientTurnIdMalformed'; minChars: number; maxChars: number }
| {
type: 'turnAlreadyRunning';
existingInvocationId: string;
incomingInvocationId: string;
}
| { type: 'queueFull'; limit: number }
| { type: 'projectRootUnanchored'; cause: string }
| { type: 'projectRootUnusable' }
@@ -23,19 +23,15 @@ import {
export function registerProjectConversationTests() {
it('splits structured rejections by variant and never by copy', () => {
// 入队失败是**结构化**载荷:分流只看 `error.type`,文案不参与任何判断。
const concurrent = {
error: {
type: 'turnAlreadyRunning' as const,
existingInvocationId: 'turn-1',
incomingInvocationId: 'turn-1',
},
message: '同一轮消息仍在处理中',
const queueFull = {
error: { type: 'queueFull' as const, limit: 5 },
message: '待发消息已达上限(最多 5 条),请等前面几条发完再发送',
};
expect(readDirectTurnEnqueueFailure(concurrent)?.error.type).toBe(
'turnAlreadyRunning',
expect(readDirectTurnEnqueueFailure(queueFull)?.error.type).toBe(
'queueFull',
);
expect(directTurnEnqueueFailureNotice(concurrent)).toBe(
'同一轮消息仍在处理中',
expect(directTurnEnqueueFailureNotice(queueFull)).toBe(
'待发消息已达上限(最多 5 条),请等前面几条发完再发送',
);
// 认得的参数 / 前置条件类都给同级提示。
expect(
@@ -356,7 +356,7 @@ describe('DirectProject 聊天分区', () => {
entries: [userEntry('u1'), assistantEntry('a1', '上一轮答复')],
localMessages: [
localNotice(
'同一轮消息仍在处理中',
'待发消息已达上限(最多 5 条),请等前面几条发完再发送',
'direct-codex:turn-9:user:rejected',
),
],
@@ -370,7 +370,7 @@ describe('DirectProject 聊天分区', () => {
expect(turns[1]?.users).toEqual([]);
expect(turns[1]?.startedAt).toBe(0);
expect(turns[1]?.finals.map((block) => block.text)).toEqual([
'同一轮消息仍在处理中',
'待发消息已达上限(最多 5 条),请等前面几条发完再发送',
]);
});