66 "encoding/json"
77 "errors"
88 "fmt"
9+ "math"
910 "os"
1011 "path/filepath"
1112 "strings"
@@ -86,27 +87,27 @@ func ForEachDevinSessionMeta(
8687
8788 for rows .Next () {
8889 var meta DevinSessionMeta
89- var createdAtMS int64
90- var lastActivityMS sql.NullInt64
91- var updatedAtMS int64
90+ var createdAt int64
91+ var lastActivity sql.NullInt64
92+ var updatedAt int64
9293 if err := rows .Scan (
9394 & meta .RawSessionID ,
9495 & meta .Title ,
9596 & meta .CWD ,
9697 & meta .Model ,
97- & createdAtMS ,
98- & lastActivityMS ,
99- & updatedAtMS ,
98+ & createdAt ,
99+ & lastActivity ,
100+ & updatedAt ,
100101 ); err != nil {
101102 return fmt .Errorf ("scanning devin session meta: %w" , err )
102103 }
103104 meta .VirtualPath = VirtualSourcePath (dbPath , meta .RawSessionID )
104- meta .CreatedAt = devinUnixMilli ( createdAtMS )
105- if lastActivityMS .Valid {
106- meta .LastActivity = devinUnixMilli ( lastActivityMS .Int64 )
105+ meta .CreatedAt = devinUnixSec ( createdAt )
106+ if lastActivity .Valid {
107+ meta .LastActivity = devinUnixSec ( lastActivity .Int64 )
107108 }
108- meta .UpdatedAt = devinUnixMilli ( updatedAtMS )
109- meta .FileMtime = updatedAtMS * 1_000_000
109+ meta .UpdatedAt = devinUnixSec ( updatedAt )
110+ meta .FileMtime = devinFileMtimeNS ( updatedAt )
110111 observeStreamingDiscoveryBuffer (ctx , 1 )
111112 if err := yield (meta ); err != nil {
112113 return err
@@ -137,9 +138,9 @@ func getDevinSessionMeta(
137138 defer db .Close ()
138139
139140 var meta DevinSessionMeta
140- var createdAtMS int64
141- var lastActivityMS sql.NullInt64
142- var updatedAtMS int64
141+ var createdAt int64
142+ var lastActivity sql.NullInt64
143+ var updatedAt int64
143144 err = db .QueryRow (`
144145 SELECT id,
145146 COALESCE(title, ''),
@@ -156,9 +157,9 @@ func getDevinSessionMeta(
156157 & meta .Title ,
157158 & meta .CWD ,
158159 & meta .Model ,
159- & createdAtMS ,
160- & lastActivityMS ,
161- & updatedAtMS ,
160+ & createdAt ,
161+ & lastActivity ,
162+ & updatedAt ,
162163 )
163164 if err != nil {
164165 if err == sql .ErrNoRows {
@@ -168,20 +169,46 @@ func getDevinSessionMeta(
168169 }
169170
170171 meta .VirtualPath = VirtualSourcePath (dbPath , meta .RawSessionID )
171- meta .CreatedAt = devinUnixMilli ( createdAtMS )
172- if lastActivityMS .Valid {
173- meta .LastActivity = devinUnixMilli ( lastActivityMS .Int64 )
172+ meta .CreatedAt = devinUnixSec ( createdAt )
173+ if lastActivity .Valid {
174+ meta .LastActivity = devinUnixSec ( lastActivity .Int64 )
174175 }
175- meta .UpdatedAt = devinUnixMilli ( updatedAtMS )
176- meta .FileMtime = updatedAtMS * 1_000_000
176+ meta .UpdatedAt = devinUnixSec ( updatedAt )
177+ meta .FileMtime = devinFileMtimeNS ( updatedAt )
177178 return & meta , nil
178179}
179180
180- func devinUnixMilli (ms int64 ) time.Time {
181- if ms <= 0 {
181+ // devinMaxEpochSec is the largest epoch-second value whose nanosecond form
182+ // still fits in int64 (year 2262). Anything larger is not a plausible Devin
183+ // timestamp and signals a unit mismatch -- a 13-digit millisecond value, for
184+ // example. Rejecting it keeps a bad column from silently overflowing into a
185+ // far-future mtime, which would wedge change detection: devinApplyFileInfoTimes
186+ // only ever raises Mtime, so a wrapped value can never be superseded and the
187+ // session would stop resyncing.
188+ const devinMaxEpochSec = math .MaxInt64 / int64 (time .Second )
189+
190+ // devinPlausibleEpochSec reports whether sec is a usable Devin epoch-second
191+ // timestamp. Devin stores created_at, last_activity_at, and
192+ // message_nodes.created_at as Unix seconds.
193+ func devinPlausibleEpochSec (sec int64 ) bool {
194+ return sec > 0 && sec <= devinMaxEpochSec
195+ }
196+
197+ func devinUnixSec (sec int64 ) time.Time {
198+ if ! devinPlausibleEpochSec (sec ) {
182199 return time.Time {}
183200 }
184- return time .UnixMilli (ms ).UTC ()
201+ return time .Unix (sec , 0 ).UTC ()
202+ }
203+
204+ // devinFileMtimeNS converts a Devin epoch-second timestamp to the nanosecond
205+ // mtime the sync layer compares against. Implausible values yield 0, which
206+ // callers already treat as "no synthetic mtime available".
207+ func devinFileMtimeNS (sec int64 ) int64 {
208+ if ! devinPlausibleEpochSec (sec ) {
209+ return 0
210+ }
211+ return sec * int64 (time .Second )
185212}
186213
187214type devinTranscriptError struct {
@@ -406,7 +433,7 @@ type devinMessageNodeRow struct {
406433 NodeID int64
407434 ParentNodeID sql.NullInt64
408435 ChatMessage string
409- CreatedAtMS int64
436+ CreatedAt int64
410437}
411438
412439func listDevinMessageNodes (dbPath , rawSessionID string ) ([]devinMessageNodeRow , error ) {
@@ -434,7 +461,7 @@ func listDevinMessageNodes(dbPath, rawSessionID string) ([]devinMessageNodeRow,
434461 var nodes []devinMessageNodeRow
435462 for rows .Next () {
436463 var row devinMessageNodeRow
437- if err := rows .Scan (& row .RowID , & row .NodeID , & row .ParentNodeID , & row .ChatMessage , & row .CreatedAtMS ); err != nil {
464+ if err := rows .Scan (& row .RowID , & row .NodeID , & row .ParentNodeID , & row .ChatMessage , & row .CreatedAt ); err != nil {
438465 return nil , err
439466 }
440467 nodes = append (nodes , row )
@@ -489,7 +516,7 @@ func parseDevinDBMessageNode(
489516 Role : role ,
490517 Content : content ,
491518 ThinkingText : thinking ,
492- Timestamp : devinUnixMilli (row .CreatedAtMS ),
519+ Timestamp : devinUnixSec (row .CreatedAt ),
493520 HasThinking : hasThinking ,
494521 HasToolUse : hasToolUse || len (toolCalls ) > 0 ,
495522 IsSystem : isSystem ,
0 commit comments