feat(session): add per-message created_at timestamps

- Persistence layer (jsonl.go addMsg/SetHistory) normalizes CreatedAt
  when missing so the invariant is guaranteed at the storage boundary
- API layer (session.go) exposes created_at on all transcript message
  types with session.updated fallback for legacy messages
- Frontend uses per-message timestamps when available
- messagesContentEqual ignores CreatedAt for tail-matching after
  JSONL roundtrip

Fixes #2787
This commit is contained in:
LiusCraft
2026-05-11 00:45:01 +08:00
parent 2992eccbf0
commit 81bbef62b1
10 changed files with 206 additions and 14 deletions
+16 -1
View File
@@ -686,10 +686,25 @@ func (ts *turnState) restoreSession(agent *AgentInstance) error {
return agent.Sessions.Save(ts.sessionKey)
}
// messagesContentEqual compares two message slices by content only, ignoring CreatedAt.
// JSON roundtrip loses the monotonic clock portion of time.Time, so direct
// reflect.DeepEqual would always differ on messages that roundtripped through
// the JSONL store.
func messagesContentEqual(a, b []providers.Message) bool {
for i := range a {
aCopy, bCopy := a[i], b[i]
aCopy.CreatedAt, bCopy.CreatedAt = nil, nil
if !reflect.DeepEqual(aCopy, bCopy) {
return false
}
}
return true
}
func matchingTurnMessageTail(history, persisted []providers.Message) int {
maxMatch := min(len(history), len(persisted))
for size := maxMatch; size > 0; size-- {
if reflect.DeepEqual(history[len(history)-size:], persisted[len(persisted)-size:]) {
if messagesContentEqual(history[len(history)-size:], persisted[len(persisted)-size:]) {
return size
}
}