From 01b6bf5bb7307246cb2cca7f1cbc8aba693941cc Mon Sep 17 00:00:00 2001 From: adamdottv <2363879+adamdottv@users.noreply.github.com> Date: Tue, 13 May 2025 13:08:43 -0500 Subject: [PATCH] chore: refactor db --- internal/db/files.sql.go | 32 ++++++----- internal/db/logs.sql.go | 17 +++--- internal/db/messages.sql.go | 16 +++--- ...20250502063010_add_summary_to_sessions.sql | 11 ---- .../20250508122310_create_logs_table.sql | 16 ------ ...initial.sql => 20250513000000_initial.sql} | 53 ++++++++++++++----- internal/db/models.go | 32 +++++------ internal/db/sessions.sql.go | 36 ++++++------- internal/db/sql/files.sql | 8 ++- internal/db/sql/logs.sql | 4 +- internal/db/sql/messages.sql | 8 ++- internal/db/sql/sessions.sql | 8 +-- internal/history/history.go | 25 +++++++-- internal/llm/agent/agent.go | 2 +- internal/logging/logging.go | 23 ++++++-- internal/message/message.go | 35 ++++++++---- internal/session/session.go | 18 ++++--- internal/tui/components/chat/message.go | 2 +- internal/tui/components/logs/table.go | 3 +- 19 files changed, 193 insertions(+), 156 deletions(-) delete mode 100644 internal/db/migrations/20250502063010_add_summary_to_sessions.sql delete mode 100644 internal/db/migrations/20250508122310_create_logs_table.sql rename internal/db/migrations/{20250424200609_initial.sql => 20250513000000_initial.sql} (56%) diff --git a/internal/db/files.sql.go b/internal/db/files.sql.go index 28abaa55..39426a73 100644 --- a/internal/db/files.sql.go +++ b/internal/db/files.sql.go @@ -15,13 +15,11 @@ INSERT INTO files ( session_id, path, content, - version, - created_at, - updated_at + version ) VALUES ( - ?, ?, ?, ?, ?, strftime('%s', 'now'), strftime('%s', 'now') + ?, ?, ?, ?, ? ) -RETURNING id, session_id, path, content, version, created_at, updated_at +RETURNING id, session_id, path, content, version, is_new, created_at, updated_at ` type CreateFileParams struct { @@ -47,6 +45,7 @@ func (q *Queries) CreateFile(ctx context.Context, arg CreateFileParams) (File, e &i.Path, &i.Content, &i.Version, + &i.IsNew, &i.CreatedAt, &i.UpdatedAt, ) @@ -74,7 +73,7 @@ func (q *Queries) DeleteSessionFiles(ctx context.Context, sessionID string) erro } const getFile = `-- name: GetFile :one -SELECT id, session_id, path, content, version, created_at, updated_at +SELECT id, session_id, path, content, version, is_new, created_at, updated_at FROM files WHERE id = ? LIMIT 1 ` @@ -88,6 +87,7 @@ func (q *Queries) GetFile(ctx context.Context, id string) (File, error) { &i.Path, &i.Content, &i.Version, + &i.IsNew, &i.CreatedAt, &i.UpdatedAt, ) @@ -95,7 +95,7 @@ func (q *Queries) GetFile(ctx context.Context, id string) (File, error) { } const getFileByPathAndSession = `-- name: GetFileByPathAndSession :one -SELECT id, session_id, path, content, version, created_at, updated_at +SELECT id, session_id, path, content, version, is_new, created_at, updated_at FROM files WHERE path = ? AND session_id = ? ORDER BY created_at DESC @@ -116,6 +116,7 @@ func (q *Queries) GetFileByPathAndSession(ctx context.Context, arg GetFileByPath &i.Path, &i.Content, &i.Version, + &i.IsNew, &i.CreatedAt, &i.UpdatedAt, ) @@ -123,7 +124,7 @@ func (q *Queries) GetFileByPathAndSession(ctx context.Context, arg GetFileByPath } const listFilesByPath = `-- name: ListFilesByPath :many -SELECT id, session_id, path, content, version, created_at, updated_at +SELECT id, session_id, path, content, version, is_new, created_at, updated_at FROM files WHERE path = ? ORDER BY created_at DESC @@ -144,6 +145,7 @@ func (q *Queries) ListFilesByPath(ctx context.Context, path string) ([]File, err &i.Path, &i.Content, &i.Version, + &i.IsNew, &i.CreatedAt, &i.UpdatedAt, ); err != nil { @@ -161,7 +163,7 @@ func (q *Queries) ListFilesByPath(ctx context.Context, path string) ([]File, err } const listFilesBySession = `-- name: ListFilesBySession :many -SELECT id, session_id, path, content, version, created_at, updated_at +SELECT id, session_id, path, content, version, is_new, created_at, updated_at FROM files WHERE session_id = ? ORDER BY created_at ASC @@ -182,6 +184,7 @@ func (q *Queries) ListFilesBySession(ctx context.Context, sessionID string) ([]F &i.Path, &i.Content, &i.Version, + &i.IsNew, &i.CreatedAt, &i.UpdatedAt, ); err != nil { @@ -199,7 +202,7 @@ func (q *Queries) ListFilesBySession(ctx context.Context, sessionID string) ([]F } const listLatestSessionFiles = `-- name: ListLatestSessionFiles :many -SELECT f.id, f.session_id, f.path, f.content, f.version, f.created_at, f.updated_at +SELECT f.id, f.session_id, f.path, f.content, f.version, f.is_new, f.created_at, f.updated_at FROM files f INNER JOIN ( SELECT path, MAX(created_at) as max_created_at @@ -225,6 +228,7 @@ func (q *Queries) ListLatestSessionFiles(ctx context.Context, sessionID string) &i.Path, &i.Content, &i.Version, + &i.IsNew, &i.CreatedAt, &i.UpdatedAt, ); err != nil { @@ -242,7 +246,7 @@ func (q *Queries) ListLatestSessionFiles(ctx context.Context, sessionID string) } const listNewFiles = `-- name: ListNewFiles :many -SELECT id, session_id, path, content, version, created_at, updated_at +SELECT id, session_id, path, content, version, is_new, created_at, updated_at FROM files WHERE is_new = 1 ORDER BY created_at DESC @@ -263,6 +267,7 @@ func (q *Queries) ListNewFiles(ctx context.Context) ([]File, error) { &i.Path, &i.Content, &i.Version, + &i.IsNew, &i.CreatedAt, &i.UpdatedAt, ); err != nil { @@ -284,9 +289,9 @@ UPDATE files SET content = ?, version = ?, - updated_at = strftime('%s', 'now') + updated_at = strftime('%Y-%m-%dT%H:%M:%f000Z', 'now') WHERE id = ? -RETURNING id, session_id, path, content, version, created_at, updated_at +RETURNING id, session_id, path, content, version, is_new, created_at, updated_at ` type UpdateFileParams struct { @@ -304,6 +309,7 @@ func (q *Queries) UpdateFile(ctx context.Context, arg UpdateFileParams) (File, e &i.Path, &i.Content, &i.Version, + &i.IsNew, &i.CreatedAt, &i.UpdatedAt, ) diff --git a/internal/db/logs.sql.go b/internal/db/logs.sql.go index aa89d7d2..5c6ea672 100644 --- a/internal/db/logs.sql.go +++ b/internal/db/logs.sql.go @@ -17,27 +17,24 @@ INSERT INTO logs ( timestamp, level, message, - attributes, - created_at + attributes ) VALUES ( ?, ?, ?, ?, ?, - ?, ? -) RETURNING id, session_id, timestamp, level, message, attributes, created_at +) RETURNING id, session_id, timestamp, level, message, attributes, created_at, updated_at ` type CreateLogParams struct { ID string `json:"id"` SessionID sql.NullString `json:"session_id"` - Timestamp int64 `json:"timestamp"` + Timestamp string `json:"timestamp"` Level string `json:"level"` Message string `json:"message"` Attributes sql.NullString `json:"attributes"` - CreatedAt int64 `json:"created_at"` } func (q *Queries) CreateLog(ctx context.Context, arg CreateLogParams) (Log, error) { @@ -48,7 +45,6 @@ func (q *Queries) CreateLog(ctx context.Context, arg CreateLogParams) (Log, erro arg.Level, arg.Message, arg.Attributes, - arg.CreatedAt, ) var i Log err := row.Scan( @@ -59,12 +55,13 @@ func (q *Queries) CreateLog(ctx context.Context, arg CreateLogParams) (Log, erro &i.Message, &i.Attributes, &i.CreatedAt, + &i.UpdatedAt, ) return i, err } const listAllLogs = `-- name: ListAllLogs :many -SELECT id, session_id, timestamp, level, message, attributes, created_at FROM logs +SELECT id, session_id, timestamp, level, message, attributes, created_at, updated_at FROM logs ORDER BY timestamp DESC LIMIT ? ` @@ -86,6 +83,7 @@ func (q *Queries) ListAllLogs(ctx context.Context, limit int64) ([]Log, error) { &i.Message, &i.Attributes, &i.CreatedAt, + &i.UpdatedAt, ); err != nil { return nil, err } @@ -101,7 +99,7 @@ func (q *Queries) ListAllLogs(ctx context.Context, limit int64) ([]Log, error) { } const listLogsBySession = `-- name: ListLogsBySession :many -SELECT id, session_id, timestamp, level, message, attributes, created_at FROM logs +SELECT id, session_id, timestamp, level, message, attributes, created_at, updated_at FROM logs WHERE session_id = ? ORDER BY timestamp ASC ` @@ -123,6 +121,7 @@ func (q *Queries) ListLogsBySession(ctx context.Context, sessionID sql.NullStrin &i.Message, &i.Attributes, &i.CreatedAt, + &i.UpdatedAt, ); err != nil { return nil, err } diff --git a/internal/db/messages.sql.go b/internal/db/messages.sql.go index 15ef7695..57d94087 100644 --- a/internal/db/messages.sql.go +++ b/internal/db/messages.sql.go @@ -16,11 +16,9 @@ INSERT INTO messages ( session_id, role, parts, - model, - created_at, - updated_at + model ) VALUES ( - ?, ?, ?, ?, ?, strftime('%s', 'now'), strftime('%s', 'now') + ?, ?, ?, ?, ? ) RETURNING id, session_id, role, parts, model, created_at, updated_at, finished_at ` @@ -145,7 +143,7 @@ ORDER BY created_at ASC type ListMessagesBySessionAfterParams struct { SessionID string `json:"session_id"` - CreatedAt int64 `json:"created_at"` + CreatedAt string `json:"created_at"` } func (q *Queries) ListMessagesBySessionAfter(ctx context.Context, arg ListMessagesBySessionAfterParams) ([]Message, error) { @@ -185,14 +183,14 @@ UPDATE messages SET parts = ?, finished_at = ?, - updated_at = strftime('%s', 'now') + updated_at = strftime('%Y-%m-%dT%H:%M:%f000Z', 'now') WHERE id = ? ` type UpdateMessageParams struct { - Parts string `json:"parts"` - FinishedAt sql.NullInt64 `json:"finished_at"` - ID string `json:"id"` + Parts string `json:"parts"` + FinishedAt sql.NullString `json:"finished_at"` + ID string `json:"id"` } func (q *Queries) UpdateMessage(ctx context.Context, arg UpdateMessageParams) error { diff --git a/internal/db/migrations/20250502063010_add_summary_to_sessions.sql b/internal/db/migrations/20250502063010_add_summary_to_sessions.sql deleted file mode 100644 index 9cecfced..00000000 --- a/internal/db/migrations/20250502063010_add_summary_to_sessions.sql +++ /dev/null @@ -1,11 +0,0 @@ --- +goose Up --- +goose StatementBegin -ALTER TABLE sessions ADD COLUMN summary TEXT; -ALTER TABLE sessions ADD COLUMN summarized_at INTEGER; --- +goose StatementEnd - --- +goose Down --- +goose StatementBegin -ALTER TABLE sessions DROP COLUMN summarized_at; -ALTER TABLE sessions DROP COLUMN summary; --- +goose StatementEnd \ No newline at end of file diff --git a/internal/db/migrations/20250508122310_create_logs_table.sql b/internal/db/migrations/20250508122310_create_logs_table.sql deleted file mode 100644 index f4876fba..00000000 --- a/internal/db/migrations/20250508122310_create_logs_table.sql +++ /dev/null @@ -1,16 +0,0 @@ --- +goose Up -CREATE TABLE logs ( - id TEXT PRIMARY KEY, - session_id TEXT REFERENCES sessions(id) ON DELETE CASCADE, - timestamp INTEGER NOT NULL, - level TEXT NOT NULL, - message TEXT NOT NULL, - attributes TEXT, - created_at INTEGER NOT NULL -); - -CREATE INDEX logs_session_id_idx ON logs(session_id); -CREATE INDEX logs_timestamp_idx ON logs(timestamp); - --- +goose Down -DROP TABLE logs; \ No newline at end of file diff --git a/internal/db/migrations/20250424200609_initial.sql b/internal/db/migrations/20250513000000_initial.sql similarity index 56% rename from internal/db/migrations/20250424200609_initial.sql rename to internal/db/migrations/20250513000000_initial.sql index 02caecf0..ad97a4ad 100644 --- a/internal/db/migrations/20250424200609_initial.sql +++ b/internal/db/migrations/20250513000000_initial.sql @@ -6,17 +6,19 @@ CREATE TABLE IF NOT EXISTS sessions ( parent_session_id TEXT, title TEXT NOT NULL, message_count INTEGER NOT NULL DEFAULT 0 CHECK (message_count >= 0), - prompt_tokens INTEGER NOT NULL DEFAULT 0 CHECK (prompt_tokens >= 0), - completion_tokens INTEGER NOT NULL DEFAULT 0 CHECK (completion_tokens>= 0), + prompt_tokens INTEGER NOT NULL DEFAULT 0 CHECK (prompt_tokens >= 0), + completion_tokens INTEGER NOT NULL DEFAULT 0 CHECK (completion_tokens >= 0), cost REAL NOT NULL DEFAULT 0.0 CHECK (cost >= 0.0), - updated_at INTEGER NOT NULL, -- Unix timestamp in milliseconds - created_at INTEGER NOT NULL -- Unix timestamp in milliseconds + summary TEXT, + summarized_at TEXT, + updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f000Z', 'now')), + created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f000Z', 'now')) ); CREATE TRIGGER IF NOT EXISTS update_sessions_updated_at AFTER UPDATE ON sessions BEGIN -UPDATE sessions SET updated_at = strftime('%s', 'now') +UPDATE sessions SET updated_at = strftime('%Y-%m-%dT%H:%M:%f000Z', 'now') WHERE id = new.id; END; @@ -27,8 +29,9 @@ CREATE TABLE IF NOT EXISTS files ( path TEXT NOT NULL, content TEXT NOT NULL, version TEXT NOT NULL, - created_at INTEGER NOT NULL, -- Unix timestamp in milliseconds - updated_at INTEGER NOT NULL, -- Unix timestamp in milliseconds + is_new INTEGER DEFAULT 0, + created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f000Z', 'now')), + updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f000Z', 'now')), FOREIGN KEY (session_id) REFERENCES sessions (id) ON DELETE CASCADE, UNIQUE(path, session_id, version) ); @@ -39,7 +42,7 @@ CREATE INDEX IF NOT EXISTS idx_files_path ON files (path); CREATE TRIGGER IF NOT EXISTS update_files_updated_at AFTER UPDATE ON files BEGIN -UPDATE files SET updated_at = strftime('%s', 'now') +UPDATE files SET updated_at = strftime('%Y-%m-%dT%H:%M:%f000Z', 'now') WHERE id = new.id; END; @@ -50,9 +53,9 @@ CREATE TABLE IF NOT EXISTS messages ( role TEXT NOT NULL, parts TEXT NOT NULL default '[]', model TEXT, - created_at INTEGER NOT NULL, -- Unix timestamp in milliseconds - updated_at INTEGER NOT NULL, -- Unix timestamp in milliseconds - finished_at INTEGER, -- Unix timestamp in milliseconds + created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f000Z', 'now')), + updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f000Z', 'now')), + finished_at TEXT, FOREIGN KEY (session_id) REFERENCES sessions (id) ON DELETE CASCADE ); @@ -61,7 +64,7 @@ CREATE INDEX IF NOT EXISTS idx_messages_session_id ON messages (session_id); CREATE TRIGGER IF NOT EXISTS update_messages_updated_at AFTER UPDATE ON messages BEGIN -UPDATE messages SET updated_at = strftime('%s', 'now') +UPDATE messages SET updated_at = strftime('%Y-%m-%dT%H:%M:%f000Z', 'now') WHERE id = new.id; END; @@ -81,6 +84,28 @@ UPDATE sessions SET WHERE id = old.session_id; END; +-- Logs +CREATE TABLE IF NOT EXISTS logs ( + id TEXT PRIMARY KEY, + session_id TEXT REFERENCES sessions(id) ON DELETE CASCADE, + timestamp TEXT NOT NULL, + level TEXT NOT NULL, + message TEXT NOT NULL, + attributes TEXT, + created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f000Z', 'now')), + updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f000Z', 'now')) +); + +CREATE INDEX logs_session_id_idx ON logs(session_id); +CREATE INDEX logs_timestamp_idx ON logs(timestamp); + +CREATE TRIGGER IF NOT EXISTS update_logs_updated_at +AFTER UPDATE ON logs +BEGIN +UPDATE logs SET updated_at = strftime('%Y-%m-%dT%H:%M:%f000Z', 'now') +WHERE id = new.id; +END; + -- +goose StatementEnd -- +goose Down @@ -88,11 +113,13 @@ END; DROP TRIGGER IF EXISTS update_sessions_updated_at; DROP TRIGGER IF EXISTS update_messages_updated_at; DROP TRIGGER IF EXISTS update_files_updated_at; +DROP TRIGGER IF EXISTS update_logs_updated_at; DROP TRIGGER IF EXISTS update_session_message_count_on_delete; DROP TRIGGER IF EXISTS update_session_message_count_on_insert; -DROP TABLE IF EXISTS sessions; +DROP TABLE IF EXISTS logs; DROP TABLE IF EXISTS messages; DROP TABLE IF EXISTS files; +DROP TABLE IF EXISTS sessions; -- +goose StatementEnd diff --git a/internal/db/models.go b/internal/db/models.go index 473e18db..e016ef79 100644 --- a/internal/db/models.go +++ b/internal/db/models.go @@ -9,23 +9,25 @@ import ( ) type File struct { - ID string `json:"id"` - SessionID string `json:"session_id"` - Path string `json:"path"` - Content string `json:"content"` - Version string `json:"version"` - CreatedAt int64 `json:"created_at"` - UpdatedAt int64 `json:"updated_at"` + ID string `json:"id"` + SessionID string `json:"session_id"` + Path string `json:"path"` + Content string `json:"content"` + Version string `json:"version"` + IsNew sql.NullInt64 `json:"is_new"` + CreatedAt string `json:"created_at"` + UpdatedAt string `json:"updated_at"` } type Log struct { ID string `json:"id"` SessionID sql.NullString `json:"session_id"` - Timestamp int64 `json:"timestamp"` + Timestamp string `json:"timestamp"` Level string `json:"level"` Message string `json:"message"` Attributes sql.NullString `json:"attributes"` - CreatedAt int64 `json:"created_at"` + CreatedAt string `json:"created_at"` + UpdatedAt string `json:"updated_at"` } type Message struct { @@ -34,9 +36,9 @@ type Message struct { Role string `json:"role"` Parts string `json:"parts"` Model sql.NullString `json:"model"` - CreatedAt int64 `json:"created_at"` - UpdatedAt int64 `json:"updated_at"` - FinishedAt sql.NullInt64 `json:"finished_at"` + CreatedAt string `json:"created_at"` + UpdatedAt string `json:"updated_at"` + FinishedAt sql.NullString `json:"finished_at"` } type Session struct { @@ -47,8 +49,8 @@ type Session struct { PromptTokens int64 `json:"prompt_tokens"` CompletionTokens int64 `json:"completion_tokens"` Cost float64 `json:"cost"` - UpdatedAt int64 `json:"updated_at"` - CreatedAt int64 `json:"created_at"` Summary sql.NullString `json:"summary"` - SummarizedAt sql.NullInt64 `json:"summarized_at"` + SummarizedAt sql.NullString `json:"summarized_at"` + UpdatedAt string `json:"updated_at"` + CreatedAt string `json:"created_at"` } diff --git a/internal/db/sessions.sql.go b/internal/db/sessions.sql.go index f6150b40..8be220cc 100644 --- a/internal/db/sessions.sql.go +++ b/internal/db/sessions.sql.go @@ -20,9 +20,7 @@ INSERT INTO sessions ( completion_tokens, cost, summary, - summarized_at, - updated_at, - created_at + summarized_at ) VALUES ( ?, ?, @@ -32,10 +30,8 @@ INSERT INTO sessions ( ?, ?, ?, - ?, - strftime('%s', 'now'), - strftime('%s', 'now') -) RETURNING id, parent_session_id, title, message_count, prompt_tokens, completion_tokens, cost, updated_at, created_at, summary, summarized_at + ? +) RETURNING id, parent_session_id, title, message_count, prompt_tokens, completion_tokens, cost, summary, summarized_at, updated_at, created_at ` type CreateSessionParams struct { @@ -47,7 +43,7 @@ type CreateSessionParams struct { CompletionTokens int64 `json:"completion_tokens"` Cost float64 `json:"cost"` Summary sql.NullString `json:"summary"` - SummarizedAt sql.NullInt64 `json:"summarized_at"` + SummarizedAt sql.NullString `json:"summarized_at"` } func (q *Queries) CreateSession(ctx context.Context, arg CreateSessionParams) (Session, error) { @@ -71,10 +67,10 @@ func (q *Queries) CreateSession(ctx context.Context, arg CreateSessionParams) (S &i.PromptTokens, &i.CompletionTokens, &i.Cost, - &i.UpdatedAt, - &i.CreatedAt, &i.Summary, &i.SummarizedAt, + &i.UpdatedAt, + &i.CreatedAt, ) return i, err } @@ -90,7 +86,7 @@ func (q *Queries) DeleteSession(ctx context.Context, id string) error { } const getSessionByID = `-- name: GetSessionByID :one -SELECT id, parent_session_id, title, message_count, prompt_tokens, completion_tokens, cost, updated_at, created_at, summary, summarized_at +SELECT id, parent_session_id, title, message_count, prompt_tokens, completion_tokens, cost, summary, summarized_at, updated_at, created_at FROM sessions WHERE id = ? LIMIT 1 ` @@ -106,16 +102,16 @@ func (q *Queries) GetSessionByID(ctx context.Context, id string) (Session, error &i.PromptTokens, &i.CompletionTokens, &i.Cost, - &i.UpdatedAt, - &i.CreatedAt, &i.Summary, &i.SummarizedAt, + &i.UpdatedAt, + &i.CreatedAt, ) return i, err } const listSessions = `-- name: ListSessions :many -SELECT id, parent_session_id, title, message_count, prompt_tokens, completion_tokens, cost, updated_at, created_at, summary, summarized_at +SELECT id, parent_session_id, title, message_count, prompt_tokens, completion_tokens, cost, summary, summarized_at, updated_at, created_at FROM sessions WHERE parent_session_id is NULL ORDER BY created_at DESC @@ -138,10 +134,10 @@ func (q *Queries) ListSessions(ctx context.Context) ([]Session, error) { &i.PromptTokens, &i.CompletionTokens, &i.Cost, - &i.UpdatedAt, - &i.CreatedAt, &i.Summary, &i.SummarizedAt, + &i.UpdatedAt, + &i.CreatedAt, ); err != nil { return nil, err } @@ -166,7 +162,7 @@ SET summary = ?, summarized_at = ? WHERE id = ? -RETURNING id, parent_session_id, title, message_count, prompt_tokens, completion_tokens, cost, updated_at, created_at, summary, summarized_at +RETURNING id, parent_session_id, title, message_count, prompt_tokens, completion_tokens, cost, summary, summarized_at, updated_at, created_at ` type UpdateSessionParams struct { @@ -175,7 +171,7 @@ type UpdateSessionParams struct { CompletionTokens int64 `json:"completion_tokens"` Cost float64 `json:"cost"` Summary sql.NullString `json:"summary"` - SummarizedAt sql.NullInt64 `json:"summarized_at"` + SummarizedAt sql.NullString `json:"summarized_at"` ID string `json:"id"` } @@ -198,10 +194,10 @@ func (q *Queries) UpdateSession(ctx context.Context, arg UpdateSessionParams) (S &i.PromptTokens, &i.CompletionTokens, &i.Cost, - &i.UpdatedAt, - &i.CreatedAt, &i.Summary, &i.SummarizedAt, + &i.UpdatedAt, + &i.CreatedAt, ) return i, err } diff --git a/internal/db/sql/files.sql b/internal/db/sql/files.sql index aba2a611..560a6984 100644 --- a/internal/db/sql/files.sql +++ b/internal/db/sql/files.sql @@ -28,11 +28,9 @@ INSERT INTO files ( session_id, path, content, - version, - created_at, - updated_at + version ) VALUES ( - ?, ?, ?, ?, ?, strftime('%s', 'now'), strftime('%s', 'now') + ?, ?, ?, ?, ? ) RETURNING *; @@ -41,7 +39,7 @@ UPDATE files SET content = ?, version = ?, - updated_at = strftime('%s', 'now') + updated_at = strftime('%Y-%m-%dT%H:%M:%f000Z', 'now') WHERE id = ? RETURNING *; diff --git a/internal/db/sql/logs.sql b/internal/db/sql/logs.sql index 58414f88..4fc64083 100644 --- a/internal/db/sql/logs.sql +++ b/internal/db/sql/logs.sql @@ -5,15 +5,13 @@ INSERT INTO logs ( timestamp, level, message, - attributes, - created_at + attributes ) VALUES ( ?, ?, ?, ?, ?, - ?, ? ) RETURNING *; diff --git a/internal/db/sql/messages.sql b/internal/db/sql/messages.sql index 475b23a8..6a3b69f6 100644 --- a/internal/db/sql/messages.sql +++ b/internal/db/sql/messages.sql @@ -21,11 +21,9 @@ INSERT INTO messages ( session_id, role, parts, - model, - created_at, - updated_at + model ) VALUES ( - ?, ?, ?, ?, ?, strftime('%s', 'now'), strftime('%s', 'now') + ?, ?, ?, ?, ? ) RETURNING *; @@ -34,7 +32,7 @@ UPDATE messages SET parts = ?, finished_at = ?, - updated_at = strftime('%s', 'now') + updated_at = strftime('%Y-%m-%dT%H:%M:%f000Z', 'now') WHERE id = ?; diff --git a/internal/db/sql/sessions.sql b/internal/db/sql/sessions.sql index 81abebd3..fd7ffe56 100644 --- a/internal/db/sql/sessions.sql +++ b/internal/db/sql/sessions.sql @@ -8,9 +8,7 @@ INSERT INTO sessions ( completion_tokens, cost, summary, - summarized_at, - updated_at, - created_at + summarized_at ) VALUES ( ?, ?, @@ -20,9 +18,7 @@ INSERT INTO sessions ( ?, ?, ?, - ?, - strftime('%s', 'now'), - strftime('%s', 'now') + ? ) RETURNING *; -- name: GetSessionByID :one diff --git a/internal/history/history.go b/internal/history/history.go index 80fa09d9..12c94a39 100644 --- a/internal/history/history.go +++ b/internal/history/history.go @@ -113,7 +113,13 @@ func (s *service) CreateVersion(ctx context.Context, sessionID, path, content st if b.Version == InitialVersion && a.Version != InitialVersion { return -1 } - return int(b.CreatedAt - a.CreatedAt) // Fallback to timestamp + // Compare timestamps as strings (ISO format sorts correctly) + if b.CreatedAt > a.CreatedAt { + return 1 + } else if a.CreatedAt > b.CreatedAt { + return -1 + } + return 0 // Equal timestamps }) latestFile := files[0] @@ -362,14 +368,27 @@ func (s *service) Subscribe(ctx context.Context) <-chan pubsub.Event[File] { } func (s *service) fromDBItem(item db.File) File { + // Parse timestamps from ISO strings + createdAt, err := time.Parse(time.RFC3339Nano, item.CreatedAt) + if err != nil { + slog.Error("Failed to parse created_at", "value", item.CreatedAt, "error", err) + createdAt = time.Now() // Fallback + } + + updatedAt, err := time.Parse(time.RFC3339Nano, item.UpdatedAt) + if err != nil { + slog.Error("Failed to parse created_at", "value", item.CreatedAt, "error", err) + updatedAt = time.Now() // Fallback + } + return File{ ID: item.ID, SessionID: item.SessionID, Path: item.Path, Content: item.Content, Version: item.Version, - CreatedAt: time.UnixMilli(item.CreatedAt * 1000), - UpdatedAt: time.UnixMilli(item.UpdatedAt * 1000), + CreatedAt: createdAt, + UpdatedAt: updatedAt, } } diff --git a/internal/llm/agent/agent.go b/internal/llm/agent/agent.go index 4c838b73..8be04072 100644 --- a/internal/llm/agent/agent.go +++ b/internal/llm/agent/agent.go @@ -209,7 +209,7 @@ func (a *agent) prepareMessageHistory(ctx context.Context, sessionID string) (se var sessionMessages []message.Message if currentSession.Summary != "" && !currentSession.SummarizedAt.IsZero() { // If summary exists, only fetch messages after the summarization timestamp - sessionMessages, err = a.messages.ListAfter(ctx, sessionID, currentSession.SummarizedAt.UnixMilli()) + sessionMessages, err = a.messages.ListAfter(ctx, sessionID, currentSession.SummarizedAt) if err != nil { return currentSession, nil, fmt.Errorf("failed to list messages after summary: %w", err) } diff --git a/internal/logging/logging.go b/internal/logging/logging.go index b42f7caf..2ba42675 100644 --- a/internal/logging/logging.go +++ b/internal/logging/logging.go @@ -86,11 +86,10 @@ func (s *service) Create(ctx context.Context, timestamp time.Time, level, messag dbLog, err := s.db.CreateLog(ctx, db.CreateLogParams{ ID: uuid.New().String(), SessionID: sql.NullString{String: sessionID, Valid: sessionID != ""}, - Timestamp: timestamp.UnixMilli(), + Timestamp: timestamp.UTC().Format(time.RFC3339Nano), Level: level, Message: message, Attributes: attributesJSON, - CreatedAt: time.Now().UnixMilli(), }) if err != nil { @@ -135,10 +134,24 @@ func (s *service) fromDBItem(item db.Log) Log { log := Log{ ID: item.ID, SessionID: item.SessionID.String, - Timestamp: time.UnixMilli(item.Timestamp), Level: item.Level, Message: item.Message, - CreatedAt: time.UnixMilli(item.CreatedAt), + } + + // Parse timestamp from ISO string + timestamp, err := time.Parse(time.RFC3339Nano, item.Timestamp) + if err == nil { + log.Timestamp = timestamp + } else { + log.Timestamp = time.Now() // Fallback + } + + // Parse created_at from ISO string + createdAt, err := time.Parse(time.RFC3339Nano, item.CreatedAt) + if err == nil { + log.CreatedAt = createdAt + } else { + log.CreatedAt = time.Now() // Fallback } if item.Attributes.Valid && item.Attributes.String != "" { @@ -195,7 +208,7 @@ func (sw *slogWriter) Write(p []byte) (n int, err error) { parsedTime, timeErr = time.Parse(time.RFC3339, value) if timeErr != nil { slog.Error("Failed to parse time in slog writer", "value", value, "error", timeErr) - timestamp = time.Now() + timestamp = time.Now().UTC() hasTimestamp = true continue } diff --git a/internal/message/message.go b/internal/message/message.go index 35a41453..e20fba74 100644 --- a/internal/message/message.go +++ b/internal/message/message.go @@ -45,7 +45,7 @@ type Service interface { Update(ctx context.Context, message Message) (Message, error) Get(ctx context.Context, id string) (Message, error) List(ctx context.Context, sessionID string) ([]Message, error) - ListAfter(ctx context.Context, sessionID string, timestampMillis int64) ([]Message, error) + ListAfter(ctx context.Context, sessionID string, timestamp time.Time) ([]Message, error) Delete(ctx context.Context, id string) error DeleteSessionMessages(ctx context.Context, sessionID string) error } @@ -134,12 +134,12 @@ func (s *service) Update(ctx context.Context, message Message) (Message, error) return Message{}, fmt.Errorf("failed to marshal message parts for update: %w", err) } - var dbFinishedAt sql.NullInt64 + var dbFinishedAt sql.NullString finishPart := message.FinishPart() if finishPart != nil && !finishPart.Time.IsZero() { - dbFinishedAt = sql.NullInt64{ - Int64: finishPart.Time.UnixMilli(), - Valid: true, + dbFinishedAt = sql.NullString{ + String: finishPart.Time.UTC().Format(time.RFC3339Nano), + Valid: true, } } @@ -199,13 +199,13 @@ func (s *service) List(ctx context.Context, sessionID string) ([]Message, error) return messages, nil } -func (s *service) ListAfter(ctx context.Context, sessionID string, timestampMillis int64) ([]Message, error) { +func (s *service) ListAfter(ctx context.Context, sessionID string, timestamp time.Time) ([]Message, error) { s.mu.RLock() defer s.mu.RUnlock() dbMessages, err := s.db.ListMessagesBySessionAfter(ctx, db.ListMessagesBySessionAfterParams{ SessionID: sessionID, - CreatedAt: timestampMillis, + CreatedAt: timestamp.Format(time.RFC3339Nano), }) if err != nil { return nil, fmt.Errorf("db.ListMessagesBySessionAfter: %w", err) @@ -294,14 +294,27 @@ func (s *service) fromDBItem(item db.Message) (Message, error) { return Message{}, fmt.Errorf("unmarshallParts for message ID %s: %w. Raw parts: %s", item.ID, err, item.Parts) } + // Parse timestamps from ISO strings + createdAt, err := time.Parse(time.RFC3339Nano, item.CreatedAt) + if err != nil { + slog.Error("Failed to parse created_at", "value", item.CreatedAt, "error", err) + createdAt = time.Now() // Fallback + } + + updatedAt, err := time.Parse(time.RFC3339Nano, item.UpdatedAt) + if err != nil { + slog.Error("Failed to parse created_at", "value", item.CreatedAt, "error", err) + updatedAt = time.Now() // Fallback + } + msg := Message{ ID: item.ID, SessionID: item.SessionID, Role: MessageRole(item.Role), Parts: parts, Model: models.ModelID(item.Model.String), - CreatedAt: time.UnixMilli(item.CreatedAt), - UpdatedAt: time.UnixMilli(item.UpdatedAt), + CreatedAt: createdAt, + UpdatedAt: updatedAt, } return msg, nil @@ -323,8 +336,8 @@ func List(ctx context.Context, sessionID string) ([]Message, error) { return GetService().List(ctx, sessionID) } -func ListAfter(ctx context.Context, sessionID string, timestampMillis int64) ([]Message, error) { - return GetService().ListAfter(ctx, sessionID, timestampMillis) +func ListAfter(ctx context.Context, sessionID string, timestamp time.Time) ([]Message, error) { + return GetService().ListAfter(ctx, sessionID, timestamp) } func Delete(ctx context.Context, id string) error { diff --git a/internal/session/session.go b/internal/session/session.go index b961959f..9df3c2b3 100644 --- a/internal/session/session.go +++ b/internal/session/session.go @@ -153,10 +153,6 @@ func (s *service) Update(ctx context.Context, session Session) (Session, error) if session.ID == "" { return Session{}, fmt.Errorf("cannot update session with empty ID") } - var summarizedAt sql.NullInt64 - if !session.SummarizedAt.IsZero() { - summarizedAt = sql.NullInt64{Int64: session.SummarizedAt.UnixMilli(), Valid: true} - } params := db.UpdateSessionParams{ ID: session.ID, @@ -165,7 +161,7 @@ func (s *service) Update(ctx context.Context, session Session) (Session, error) CompletionTokens: session.CompletionTokens, Cost: session.Cost, Summary: sql.NullString{String: session.Summary, Valid: session.Summary != ""}, - SummarizedAt: summarizedAt, + SummarizedAt: sql.NullString{String: session.SummarizedAt.UTC().Format(time.RFC3339Nano), Valid: !session.SummarizedAt.IsZero()}, } dbSession, err := s.db.UpdateSession(ctx, params) if err != nil { @@ -206,9 +202,15 @@ func (s *service) Subscribe(ctx context.Context) <-chan pubsub.Event[Session] { func (s *service) fromDBItem(item db.Session) Session { var summarizedAt time.Time if item.SummarizedAt.Valid { - summarizedAt = time.UnixMilli(item.SummarizedAt.Int64) + parsedTime, err := time.Parse(time.RFC3339Nano, item.SummarizedAt.String) + if err == nil { + summarizedAt = parsedTime + } } + createdAt, _ := time.Parse(time.RFC3339Nano, item.CreatedAt) + updatedAt, _ := time.Parse(time.RFC3339Nano, item.UpdatedAt) + return Session{ ID: item.ID, ParentSessionID: item.ParentSessionID.String, @@ -219,8 +221,8 @@ func (s *service) fromDBItem(item db.Session) Session { Cost: item.Cost, Summary: item.Summary.String, SummarizedAt: summarizedAt, - CreatedAt: time.UnixMilli(item.CreatedAt), - UpdatedAt: time.UnixMilli(item.UpdatedAt), + CreatedAt: createdAt, + UpdatedAt: updatedAt, } } diff --git a/internal/tui/components/chat/message.go b/internal/tui/components/chat/message.go index e7a6f723..9c7ac2aa 100644 --- a/internal/tui/components/chat/message.go +++ b/internal/tui/components/chat/message.go @@ -605,7 +605,7 @@ func renderToolMessage( return toolMsg } - params := renderToolParams(width-2-lipgloss.Width(toolNameText), toolCall) + params := renderToolParams(width-1-lipgloss.Width(toolNameText), toolCall) responseContent := "" if response != nil { responseContent = renderToolResponse(toolCall, *response, width-2) diff --git a/internal/tui/components/logs/table.go b/internal/tui/components/logs/table.go index dbfc9c9f..f78e4c49 100644 --- a/internal/tui/components/logs/table.go +++ b/internal/tui/components/logs/table.go @@ -159,8 +159,7 @@ func (i *tableCmp) updateRows() { rows := make([]table.Row, 0, len(i.logs)) for _, log := range i.logs { - // Format timestamp as time - timeStr := log.Timestamp.Format("15:04:05") + timeStr := log.Timestamp.Local().Format("15:04:05") // Include ID as hidden first column for selection row := table.Row{