fix(persist,channel): 同类定宽时间戳修复 — checkpoint 恢复与渠道会话列表排序

- persist: formatTime 定宽;loadLatestSQL 补 rowid 兜底;New 时归一化历史行(避免 LoadLatest 挑到更旧检查点)
- channel/session: 写入定宽;listSessionSQL 补 rowid 兜底;核实无 INSERT 吃 DDL 默认值,故只加注释不改默认值
- 新增回归测试并做变异验证(改回 RFC3339Nano 即 FAIL)
This commit is contained in:
AskaEth committed 2026-10-03 16:08:51 +08:00
1 parent 31086ed1ab
commit fbbfcc95ee
7 files changed
+445 -14

No files matched your search

+10 -11
View File
@@ -94,9 +94,13 @@ const (
loadSQL = `SELECT id, agent_id, step, state, created_at, updated_at
FROM checkpoints WHERE id = ?`
// updated_at is stored as fixed-width UTC text (see formatTime), so its text
// order is its time order. step breaks ties between checkpoints written in
// the same tick, and rowid makes that tiebreak deterministic (checkpoints is
// an ordinary rowid table, not WITHOUT ROWID).
loadLatestSQL = `SELECT id, agent_id, step, state, created_at, updated_at
FROM checkpoints WHERE agent_id = ?
ORDER BY updated_at DESC, step DESC LIMIT 1`
ORDER BY updated_at DESC, step DESC, rowid DESC LIMIT 1`
listSQL = `SELECT id, agent_id, step, state, created_at, updated_at
FROM checkpoints WHERE agent_id = ?
@@ -112,6 +116,8 @@ const (
// which already constrains the pool to a single connection for write
// serialization. The caller owns db and must Close it when done. log may be nil
// and is replaced with a discard logger so the Store can always log safely.
// Legacy variable-width timestamps are normalized in place (see
// normalizeTimestamps) so text ordering matches time ordering.
func New(ctx context.Context, db *sql.DB, log *slog.Logger) (*Store, error) {
if db == nil {
return nil, fmt.Errorf("persist: new: %w", ErrNilDB)
@@ -124,6 +130,9 @@ func New(ctx context.Context, db *sql.DB, log *slog.Logger) (*Store, error) {
if _, err := db.ExecContext(ctx, createIndexSQL); err != nil {
return nil, fmt.Errorf("persist: create index: %w", err)
}
if err := normalizeTimestamps(ctx, db); err != nil {
return nil, err
}
return &Store{db: db, log: log}, nil
}
@@ -270,16 +279,6 @@ func scanCheckpoint(sc rowScanner) (*Checkpoint, error) {
}, nil
}
// formatTime renders t as a UTC RFC3339 timestamp with nanosecond precision.
func formatTime(t time.Time) string {
return t.UTC().Format(time.RFC3339Nano)
}
// parseTime parses a timestamp written by formatTime.
func parseTime(s string) (time.Time, error) {
return time.Parse(time.RFC3339Nano, s)
}
// discardLogger returns log if non-nil, otherwise a logger that writes nowhere,
// so the Store can always log safely.
func discardLogger(log *slog.Logger) *slog.Logger {