diff --git a/packages/agent/src/harness/session/search.ts b/packages/agent/src/harness/session/search.ts index 0bd44c44c..d75a606eb 100644 --- a/packages/agent/src/harness/session/search.ts +++ b/packages/agent/src/harness/session/search.ts @@ -10,7 +10,7 @@ export interface SessionSearchOptions { export interface SessionSearchHit { metadata: TMetadata; entryId: string; - timestamp: string; + timestamp: number; snippet?: string; score?: number; } @@ -53,7 +53,7 @@ class ScanningSessionSearch hits.push({ metadata, entryId: entry.id, - timestamp: new Date(entry.timestamp).toISOString(), + timestamp: entry.timestamp, snippet: payload, }); } diff --git a/packages/session-backends/sqlite-node/src/sqlite/migrations/001_initial.sql b/packages/session-backends/sqlite-node/src/sqlite/migrations/001_initial.sql index 706b0b04d..fc9e277e2 100644 --- a/packages/session-backends/sqlite-node/src/sqlite/migrations/001_initial.sql +++ b/packages/session-backends/sqlite-node/src/sqlite/migrations/001_initial.sql @@ -1,6 +1,6 @@ CREATE TABLE IF NOT EXISTS sessions ( id TEXT PRIMARY KEY, - created_at TEXT NOT NULL, + created_at INTEGER NOT NULL, cwd TEXT NOT NULL, parent_session_id TEXT NULL, metadata TEXT NULL @@ -15,7 +15,7 @@ CREATE TABLE IF NOT EXISTS entries ( id TEXT NOT NULL, parent_id TEXT NULL, type TEXT NOT NULL, - timestamp TEXT NOT NULL, + timestamp INTEGER NOT NULL, payload TEXT NOT NULL, PRIMARY KEY (session_id, id), UNIQUE (session_id, seq) @@ -71,7 +71,7 @@ CREATE TABLE IF NOT EXISTS records ( run_id TEXT NULL, type TEXT NOT NULL, op_kind TEXT NULL, - timestamp TEXT NOT NULL, + timestamp INTEGER NOT NULL, payload TEXT NOT NULL, PRIMARY KEY (session_id, id), UNIQUE (session_id, seq) diff --git a/packages/session-backends/sqlite-node/src/sqlite/repo.ts b/packages/session-backends/sqlite-node/src/sqlite/repo.ts index 8135d7e32..81f86bea9 100644 --- a/packages/session-backends/sqlite-node/src/sqlite/repo.ts +++ b/packages/session-backends/sqlite-node/src/sqlite/repo.ts @@ -175,14 +175,6 @@ function configureSqliteDatabase(db: SqliteDatabase): void { sql`PRAGMA busy_timeout=5000`.exec(db); } -function timestampToText(timestamp: number): string { - return new Date(timestamp).toISOString(); -} - -function timestampFromText(timestamp: string): number { - return Date.parse(timestamp); -} - function entryRowFromCached(row: CachedBranchEntryRow): EntryRow { return { ...row, seq: row.entry_seq, type: row.type as Entry["type"] }; } @@ -198,9 +190,7 @@ function readObjectPayload(row: EntryRow): Record { function decodeEntry(row: EntryRow): Entry { try { const payload = readObjectPayload(row); - const timestamp = timestampFromText(row.timestamp); - if (!Number.isFinite(timestamp)) throw new Error(`Invalid timestamp ${row.timestamp}`); - const base = { id: row.id, seq: row.seq, parentId: row.parent_id, timestamp }; + const base = { id: row.id, seq: row.seq, parentId: row.parent_id, timestamp: row.timestamp }; switch (row.type) { case "message": if (typeof payload.message !== "object" || payload.message === null) throw new Error("Missing message"); @@ -283,14 +273,12 @@ function recordOpKind(record: NewRecord): string | undefined { return record.type === "operation_started" ? record.intent.kind : undefined; } -function decodeRecord(row: { seq: number; timestamp: string; payload: string }): LaneRecord { +function decodeRecord(row: { seq: number; timestamp: number; payload: string }): LaneRecord { try { - const timestamp = timestampFromText(row.timestamp); - if (!Number.isFinite(timestamp)) throw new Error(`Invalid timestamp ${row.timestamp}`); return { ...(JSON.parse(row.payload) as object), seq: row.seq, - timestamp, + timestamp: row.timestamp, } as LaneRecord; } catch (error) { throw new SessionError( @@ -476,7 +464,7 @@ class SqliteSessionStorage implements SessionStorage { id: committed.id, parentId: committed.parentId, type: committed.type, - timestamp: timestampToText(committed.timestamp), + timestamp: committed.timestamp, payload: JSON.stringify(entryPayload(committed)), }); setLaneLeaf(this.db, this.metadata.id, lane, committed.id); @@ -514,7 +502,7 @@ class SqliteSessionStorage implements SessionStorage { runId: recordRunId(record), type: record.type, opKind: recordOpKind(record), - timestamp: timestampToText(committed.timestamp), + timestamp: committed.timestamp, payload: JSON.stringify(record), }); if (record.type === "operation_finished") { @@ -739,7 +727,7 @@ export class SqliteSessionRepository const lease = db.transaction(() => { insertSessionRow(db, { id, - createdAt: timestampToText(createdAt), + createdAt, cwd: options.cwd, parentSessionId: options.parentSessionId, metadata: options.metadata, @@ -870,7 +858,7 @@ export class SqliteSessionRepository lease = db.transaction(() => { insertSessionRow(db, { id, - createdAt: timestampToText(createdAt), + createdAt, cwd: options.cwd, parentSessionId: options.parentSessionId ?? source.id, metadata, diff --git a/packages/session-backends/sqlite-node/src/sqlite/search-backend.ts b/packages/session-backends/sqlite-node/src/sqlite/search-backend.ts index e22de3340..cf6976dfd 100644 --- a/packages/session-backends/sqlite-node/src/sqlite/search-backend.ts +++ b/packages/session-backends/sqlite-node/src/sqlite/search-backend.ts @@ -121,7 +121,7 @@ class SqliteSessionSearch implements SessionSearch { WHERE f.session_id = s.id AND f.kind = 'name' AND f.key IS NULL ) WHERE session_search_fts MATCH ${query} AND (${cwd} IS NULL OR s.cwd = ${cwd}) - ORDER BY score`.all(db); + ORDER BY score`.all(db); const path = await this.getDatabasePath(); return rows.map((row) => ({ metadata: decodeSessionMetadata(row, path), diff --git a/packages/session-backends/sqlite-node/src/sqlite/storage/branch-entries.ts b/packages/session-backends/sqlite-node/src/sqlite/storage/branch-entries.ts index 76016a71c..276ca8947 100644 --- a/packages/session-backends/sqlite-node/src/sqlite/storage/branch-entries.ts +++ b/packages/session-backends/sqlite-node/src/sqlite/storage/branch-entries.ts @@ -14,7 +14,7 @@ export interface CachedBranchEntryRow { entry_seq: number; parent_id: string | null; type: Entry["type"]; - timestamp: string; + timestamp: number; payload: string; } diff --git a/packages/session-backends/sqlite-node/src/sqlite/storage/entries.ts b/packages/session-backends/sqlite-node/src/sqlite/storage/entries.ts index e76ef3cf6..2c5b9472c 100644 --- a/packages/session-backends/sqlite-node/src/sqlite/storage/entries.ts +++ b/packages/session-backends/sqlite-node/src/sqlite/storage/entries.ts @@ -8,7 +8,7 @@ export interface EntryRow { id: string; parent_id: string | null; type: Entry["type"]; - timestamp: string; + timestamp: number; payload: string; } @@ -17,7 +17,7 @@ export interface NewEntryRow { id: string; parentId: string | null; type: Entry["type"]; - timestamp: string; + timestamp: number; payload: string; } diff --git a/packages/session-backends/sqlite-node/src/sqlite/storage/records.ts b/packages/session-backends/sqlite-node/src/sqlite/storage/records.ts index 1dc357227..7c810c2c8 100644 --- a/packages/session-backends/sqlite-node/src/sqlite/storage/records.ts +++ b/packages/session-backends/sqlite-node/src/sqlite/storage/records.ts @@ -10,7 +10,7 @@ export interface RecordRow { run_id: string | null; type: string; op_kind: string | null; - timestamp: string; + timestamp: number; payload: string; } @@ -21,7 +21,7 @@ export interface NewRecordRow { runId?: string; type: string; opKind?: string; - timestamp: string; + timestamp: number; payload: string; } diff --git a/packages/session-backends/sqlite-node/src/sqlite/storage/sessions.ts b/packages/session-backends/sqlite-node/src/sqlite/storage/sessions.ts index f4c54ecf5..e954c0b38 100644 --- a/packages/session-backends/sqlite-node/src/sqlite/storage/sessions.ts +++ b/packages/session-backends/sqlite-node/src/sqlite/storage/sessions.ts @@ -4,7 +4,7 @@ import type { SqliteDatabase, SqliteSessionMetadata } from "../types.ts"; export interface SessionRow { id: string; - created_at: string; + created_at: number; metadata: string | null; cwd: string; parent_session_id: string | null; @@ -14,7 +14,7 @@ export interface SessionRow { export interface NewSessionRow { id: string; - createdAt: string; + createdAt: number; cwd: string; parentSessionId?: string; metadata?: Record; @@ -121,7 +121,7 @@ export function decodeSessionMetadata(row: SessionRow, path: string): SqliteSess const name = row.has_session_name === 0 ? undefined : parseSessionName(row.session_name, row.id); return { id: row.id, - createdAt: Date.parse(row.created_at), + createdAt: row.created_at, ...(name === undefined ? {} : { name }), cwd: row.cwd, path, diff --git a/packages/session-backends/sqlite-node/test/search.test.ts b/packages/session-backends/sqlite-node/test/search.test.ts index f9f5cec4c..497a5582c 100644 --- a/packages/session-backends/sqlite-node/test/search.test.ts +++ b/packages/session-backends/sqlite-node/test/search.test.ts @@ -30,8 +30,10 @@ describe("SQLite FTS5 session search", () => { await expect(search.search({ text: "auth", cwd: root })).resolves.toEqual([ expect.objectContaining({ entryId, + timestamp: expect.any(Number), metadata: expect.objectContaining({ id: "included", + createdAt: expect.any(Number), name: "Canonical name", metadata: { name: "application-owned" }, }),