feat(gateway): 文档多值化 + 抽取结论回写(design/24 S3.5)
- 缺口①:「一回合最多 5 个附件、落库只留 1 个」——document 列语义升级为 ≤5 个引用的**数组**(列名/字段名不变、无需迁移,旧单对象行可读回);落库收集全部引用、转录还原全部部件。 - 缺口②:抽取结论 status/chars/truncated 经 completeTurn 回合后**回写**该回合用户消息的同一列(id 按代次配对,忙时消息挂 pendingMsg.persisted 并在 drain 时以新 gen 重新配对);历史 DTO 改为 **documents: []**(omitempty ⇒ 0 个附件时省略)。 - 防御:编/解码超 5 都报 ErrTooManyDocuments(报错、绝不裁剪);坏列走既有不静默路径(转录 WARN + DTO unreadable 最小引用);>5 上限值被独立用具例钉死。 - 顺手修 S3-B 遗留的函数越界:submitInternal 81 → 79 行(播种块抽成 seedTurnLocked);session_orchestrator.go 484 行。 - 主会话独立验收:隔离 worktree(HEAD + 本切片 12 文件)build/vet 绿、全树 59 包 0 FAIL、linux+darwin 交叉编译绿、gateway+history 437 PASS;变异独立复现(只留第一个附件 ⇒ 2 条红,含 HTTP 端到端 want 3 条)。 - 遗留(已登记):chat_documents_test.go 510 行(S3-B 遗留越界,本次未拆);中断/被取代的回合不回写结论;回写后不补发实时帧(status 需重新拉取/回放可见)。
This commit is contained in:
1 parent
20c1edb36a
commit
5b2b385524
12 files changed
+1255
-166
No files matched your search
@@ -142,6 +142,10 @@ type sessionState struct {
|
||||
// 队列是一条只在队尾追加、可能被整体弹出/清空的先进先出队列,因此「第几条」
|
||||
// 不稳定;按 seq 认领已注入的消息,才不会被同一时刻的追加或清空误伤。
|
||||
pendingSeq uint64
|
||||
// turnDocs 是本回合已落库的用户消息(id + 只有引用的 document 列),供回合
|
||||
// 结算时把抽取结论回写进那一列(design/24 §6.1)。它按代次配对,因此被中断/
|
||||
// 取代的陈旧回合读不到本回合的条目。见 session_document_status.go。
|
||||
turnDocs turnDocuments
|
||||
}
|
||||
|
||||
// pendingMsg 是一条已排队用户回合。它携带自己的 env,使续跑
|
||||
@@ -161,6 +165,10 @@ type pendingMsg struct {
|
||||
// 它只在状态锁下分配,是转向注入「注入=已消费」认领的依据:队列会被追加、
|
||||
// 被弹出、被中断清空,序号却能唯一定位这一条。
|
||||
seq uint64
|
||||
// persisted 是这条待处理回合已落库的用户消息,供它的续跑回合结算时回写
|
||||
// 文档抽取结论(design/24 §6.1):它在入队那一刻就落了库,而那时抽取还没
|
||||
// 发生。见 session_document_status.go。
|
||||
persisted []history.Message
|
||||
}
|
||||
|
||||
// Orchestrator 拥有每会话的消息状态机。它对并发安全
|
||||
@@ -263,11 +271,14 @@ func (o *Orchestrator) submitInternal(ctx context.Context, sessionID string, mes
|
||||
ctx = withClientMsgHint(ctx, a.hint)
|
||||
if st.mode != ModeIdle {
|
||||
st.pendingSeq++
|
||||
st.pending = append(st.pending, pendingMsg{messages: messages, env: env, deviceID: deviceID, mode: msgMode, at: time.Now(), seq: st.pendingSeq})
|
||||
seq := st.pendingSeq
|
||||
st.pending = append(st.pending, pendingMsg{messages: messages, env: env, deviceID: deviceID, mode: msgMode, at: time.Now(), seq: seq})
|
||||
depth := len(st.pending)
|
||||
mode := st.mode
|
||||
o.mu.Unlock()
|
||||
o.persistUserMessages(ctx, sessionID, messages, deviceID, msgMode, env.PersonaID)
|
||||
// 落库在锁外(慢操作)完成,随后把它挂回那条待处理回合:抽取结论要等
|
||||
// 这个回合真正跑起来才有,只能在它的续跑结算时回写(design/24 §6.1)。
|
||||
o.attachPendingDocuments(sessionID, seq, o.persistUserMessages(ctx, sessionID, messages, deviceID, msgMode, env.PersonaID))
|
||||
o.emitState(sessionID, EventPendingAck, map[string]any{"sessionId": sessionID, "mode": mode, "queueDepth": depth})
|
||||
return SubmitResult{Accepted: true, Pending: true, QueueDepth: depth, MessageID: a.hint.MessageID}, nil
|
||||
}
|
||||
@@ -279,16 +290,7 @@ func (o *Orchestrator) submitInternal(ctx context.Context, sessionID string, mes
|
||||
// 消息不会抢在被取消回合落定旧快照之前开跑(session_turn_fence.go)。
|
||||
fence := newTurnFence(st.fence.done)
|
||||
st.fence = fence
|
||||
// 新回合延续该会话的对话:持久化转录被播种在
|
||||
// 刚发送的消息之前(design/15 §2.2/§4,design/22
|
||||
// §7-5),且其助手话语保持说话时的角色。该读取
|
||||
// 在此发生,在下面 persistUserMessages 写入新消息之前,因此刚
|
||||
// 发送的消息绝不在上下文中重复。没有历史(或空会话)的
|
||||
// 存储让该回合只使用调用方给出的消息。它是在状态锁下
|
||||
// 的有界索引读取(transcriptContextCount 行),而状态锁
|
||||
// 从不会被运行中的回合持有,因此它无法把 Submit 堵在回合后面。
|
||||
turn := o.startTurnMessages(ctx, sessionID, messages)
|
||||
st.tail = o.boundedTail(turn)
|
||||
turn := o.seedTurnLocked(ctx, st, sessionID, messages)
|
||||
runCtx, cancel := context.WithCancel(ctx)
|
||||
st.cancel = cancel
|
||||
o.mu.Unlock()
|
||||
@@ -299,9 +301,10 @@ func (o *Orchestrator) submitInternal(ctx context.Context, sessionID string, mes
|
||||
runCtx = loop.WithSteerSource(runCtx, o.beginSteer(sessionID, gen))
|
||||
|
||||
o.emitState(sessionID, EventSessionBusy, map[string]any{"sessionId": sessionID, "mode": ModeThinking})
|
||||
// 只有此刻到达的消息会被持久化:已播种的转录
|
||||
// 已经持久,重新追加它会重复整个对话。
|
||||
o.persistUserMessages(ctx, sessionID, messages, deviceID, msgMode, env.PersonaID)
|
||||
// 只有此刻到达的消息会被持久化(已播种的转录已持久,重追加会重复整个对话),
|
||||
// 且落库只写**引用** —— 抽取还发生在后面,结论由回合结算时补进同一列
|
||||
// (两段式见 session_document_status.go)。
|
||||
o.rememberTurnDocuments(sessionID, gen, o.persistUserMessages(ctx, sessionID, messages, deviceID, msgMode, env.PersonaID))
|
||||
|
||||
if sync {
|
||||
return o.runImmediateSync(runCtx, sessionID, gen, turn, env, fence, deviceID, msgMode, a.hint.MessageID)
|
||||
@@ -351,8 +354,13 @@ func (o *Orchestrator) goTurn(sessionID string, gen uint64, messages []model.Mes
|
||||
// deviceID 是产出回合的客户端 id,mode 是其行为模式,
|
||||
// personaID 是它说话时的角色,都用于标注持久化的回复
|
||||
// (design/16 §4.5、§5.1;design/22 §7-5)。
|
||||
//
|
||||
// 它同时是文档抽取结论回写的落点(design/24 §6.1):本回合用户消息的
|
||||
// document 列里此前只有引用,装配期解析出的 status/chars/truncated 在这里补上
|
||||
// —— 见 session_document_status.go 的两段式说明。
|
||||
func (o *Orchestrator) completeTurn(sessionID string, gen uint64, res *loop.Result, runErr error, deviceID, mode, personaID string) error {
|
||||
o.persistAssistantReply(sessionID, res, deviceID, mode, personaID)
|
||||
o.persistDocumentStatus(sessionID, gen, res)
|
||||
o.mu.Lock()
|
||||
st := o.states[sessionID]
|
||||
if st == nil || st.gen != gen {
|
||||
@@ -378,6 +386,9 @@ func (o *Orchestrator) completeTurn(sessionID string, gen uint64, res *loop.Resu
|
||||
// 的回合无需再等任何在途回合:围栏只负责放行它后面的那一个。
|
||||
fence := newTurnFence(nil)
|
||||
st.fence = fence
|
||||
// 续跑的这一轮同样有「回合前已落库的用户消息」,因此把它们的文档状态
|
||||
// 记到新代次上(design/24 §6.1)。
|
||||
st.turnDocs = turnDocuments{gen: gen, msgs: next.persisted}
|
||||
depth := len(st.pending)
|
||||
resume := append([]model.Message(nil), st.tail...)
|
||||
env := next.env
|
||||
@@ -471,20 +482,3 @@ func (o *Orchestrator) notifyPersisted(msg history.Message) {
|
||||
o.onPersisted(msg)
|
||||
}
|
||||
}
|
||||
|
||||
// deviceIDContextKey 是携带消息客户端 deviceID 的 context 键
|
||||
// (design/16 §4.5)从 HTTP/WS 边界引入编排器的持久化。
|
||||
type deviceIDContextKey struct{}
|
||||
|
||||
// withDeviceID 返回携带 deviceID 的 context,使编排器能标注
|
||||
// 持久化的用户消息。空 deviceID 是空操作(不写入标签)。
|
||||
func withDeviceID(ctx context.Context, deviceID string) context.Context {
|
||||
return context.WithValue(ctx, deviceIDContextKey{}, deviceID)
|
||||
}
|
||||
|
||||
// deviceIDFrom 返回 ctx 携带的 deviceID,未设置时返回 ""。它
|
||||
// 只返回格式良好的非空值;其他一律视为缺失。
|
||||
func deviceIDFrom(ctx context.Context) string {
|
||||
v, _ := ctx.Value(deviceIDContextKey{}).(string)
|
||||
return v
|
||||
}
|
||||
Reference in new issue
Block a user