feat(gateway): 接线 chat_messages.turn_id 写入(A–D 四点齐)
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled

A 新起一路:publishLegStart 在 persistUserMessages 之前 withTurnID(ctx, leg.turnID)
(该行随 ef3dcb6 已进 HEAD)。
B 队首续跑路:completeTurn 在 o.mu.Unlock() 之后 assignTurnID(sessionID, runLeg.turnID, next.persisted)。
C 被 steer 吃进:consumePending 认领成功后 assignTurnID(sessionID, s.turnID, steerRows(candidates))。
D 助手回复:persistTurnReply 增 turnID 形参,completeTurn 传 leg.turnID —— 刻意**不用** turnIDOfLeg:
interrupt scope=all 会清空 st.legs,而被打断那一路已产出的正文仍要照原样落库(内容是真的),
按 gen 回查只能拿到空串 ⇒ 配对丢失。
删掉 history_turnid_test.go 的 t.Skip:TestSubmitWiresTurnIDForBothRows 真跑并通过。
新增 session_turnid_wiring_test.go(真 SQLite 内存存储 + 真编排器 + 可逐路放行 runner):
单路两条同值 / 并行两路各自同值且两两不同 / 续跑补写(先钉「补写前为空」)/ steer 补写(用该路自己的转向源)/
TestAssistantTurnIDSurvivesInterruptAll(钉住 D 的取值来源)。
变异 5 条(A/B/C/D 各自 + D′ 改回 turnIDOfLeg —— 只有那条守门用例红,证明它守的正是这个取舍)。
诚实边界:turnIDOfLeg 现无生产调用方(保留为兜底);先到先得竞态未构造对抗用例;
IM 渠道桥不走本编排器 ⇒ 其行 turn_id 留空属预期(契约里键缺席)。
This commit is contained in:
AskaEth committed 2026-10-05 10:00:17 +08:00
1 parent 3589f4b14f
commit d804c4e675
5 files changed
+281 -10

No files matched your search

@@ -388,7 +388,7 @@ func (o *Orchestrator) goTurn(sessionID string, leg *turnLeg, messages []model.M
// 额度);只有在途路全空且队列也空时才报空闲 —— 单路时该条件恒等价于现状。
func (o *Orchestrator) completeTurn(sessionID string, leg *turnLeg, res *loop.Result, runErr error, deviceID, mode, personaID string) error {
gen := leg.gen
res = o.persistTurnReply(sessionID, gen, res, runErr, deviceID, mode, personaID)
res = o.persistTurnReply(sessionID, gen, res, runErr, deviceID, mode, personaID, leg.turnID)
o.persistDocumentStatus(sessionID, gen, res)
o.dispatchVoiceReply(sessionID, gen, res)
o.mu.Lock()
@@ -428,6 +428,10 @@ func (o *Orchestrator) completeTurn(sessionID string, leg *turnLeg, res *loop.Re
// 归属消息就是刚从队首弹出的那条(它在入队时就已落库,id 取自
// next.persisted)——没有这一帧,客户端就不知道为什么气泡突然开始回答了。
o.emitTurnBegin(sessionID, runLeg, userMessageIDs(next.persisted), activeTurns)
// 续跑路刚起,就把刚从队首弹出的那几条消息认领到它名下(design/15 §2.5.9
// 的两段式第二段):它们入队那一刻就落了库,而那时还没有路,此刻才谈得上
// 归属 —— 补写之后它们与该路的助手回复同值。必须在锁外(它写库)。
o.assignTurnID(sessionID, runLeg.turnID, next.persisted)
go o.goTurn(sessionID, runLeg, resume, env, runCtx, cancel, next.deviceID, next.mode)
return nil
}