package flowgraph // 本文件是**轮次计数器的播种读侧**(design/19 §6.1「`graph.round`:每会话单调」; // 切片 X1,2026-10-06)。 // // 它补的是真机挖出的第一处真缺陷:发布器的轮次号是**进程内**计数 // (`loop.FlowPublisher` 的 `rounds`/`seq`),后端一重启就从 1 重来,而落盘的 // `flow_graphs` 保留 168h ⇒ 时间轴(`Timeline`)按轮次号分组时把**三个真实轮次并成 // 一块**。真机实测(`session=web`):分组键 `r:1` 下有 13:52:53 / 15:42:27 / 18:40:44 // 三次真实的轮,块标题「第 1 轮 · 3 路」是失真的。 // // 本文件只提供**一个只读出口**(`MaxRound`):把一个会话**已经落盘**的最大轮次号交回 // 调用方,让它把进程内的计数器播到那个值之上。播种与发号在 `loop` 侧 // (`internal/runtime/loop/flow_roundseed.go`)—— 本包不认识发布器、不判会话、也绝不 // 改任何会话的状态(与 `Timeline` 同一条只读纪律)。 import ( "context" "fmt" ) // maxRoundSQL 取该会话已落盘的最大轮次号(一轮一行,因此聚合面有界)。 // // 三条口径与 `timeline.go` 的 `timelineRoundKeySQL` **逐字同源**: // // 1. `json_valid` 是**保护**:JSON 破损的行根本不参与解析 —— 否则 `json_extract` // 会让整条查询报错,把「一行坏数据」升级成「播种整体失败」(坏行不该拖垮重启后的 // 计数器)。用嵌套 `CASE`(而不是 `json_valid(...) AND ...`)是因为 SQLite 不保证 // AND 的操作数求值次序,而 CASE 只求值命中的那一支。 // 2. **非正数不算真实轮次**:`round` 键缺席 / 为 0 / 为负 / 非数字(`CAST` 归 0)的行 // 都给不出一个「更大的号」;一个会话若只剩下这类行,聚合结果就是 0 或负数 ⇒ Go 侧 // 归 0 —— 绝不冒充一个真实轮次号(design/19 §6.1 的缺席语义)。 // 3. 没有可用行时 `MAX` 是 NULL ⇒ `COALESCE` 归 **0**(=「库里没有该会话的轮次」), // 调用方据此从 1 起发号(与引入播种之前的行为一致)。 const maxRoundSQL = `SELECT COALESCE(MAX(CASE WHEN json_valid(graph_json) THEN CAST(json_extract(graph_json,'$.round') AS INTEGER) END), 0) FROM flow_graphs WHERE session_id = ?` // MaxRound 返回该会话**已落盘**的最大轮次号;库里没有该会话的可用行时返回 0。 // // 语义与边界: // - **只读**:不写库、不删行、不碰保留期(与 `Graph` / `Latest` / `Timeline` 同档); // - **保留期决定它能看多远**:已被 `Sweep` 清掉的轮次不在这里的账上 ⇒ 一个会话若整段 // 历史都被清扫过,返回值会回落到 0,调用方随即从 1 起发号 —— 那**不会**造成时间轴 // 上的轮次合并(旧行已经不在了),只是轮次号从头开始(design/19 §7.4 的诚实边界); // - `ctx` 的上界由调用方给(发布器侧用一个有界的读取上界,绝不把一次库读挂在会话收尾 // 或轮次开始上无限等); // - 失败**如实上抛**(调用方记 WARN 后退回进程内计数):宁可让计数器退回修复前的行为, // 也绝不把「读不到」当成「库里没有轮次」而悄悄发一个会撞旧行的号。 func (s *SQLStore) MaxRound(ctx context.Context, sessionID string) (int, error) { if s == nil || s.db == nil { return 0, fmt.Errorf("flowgraph: query max round: %w", ErrNilDB) } var n int if err := s.db.QueryRowContext(ctx, maxRoundSQL, sessionID).Scan(&n); err != nil { return 0, fmt.Errorf("flowgraph: 查最大轮次号(%s): %w", sessionID, err) } if n < 0 { return 0, nil } return n, nil }