From 72ce04bd9fb59c96b3444cc17263c6351c3db4c9 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 12 Apr 2026 05:59:49 +0000 Subject: [PATCH 1/4] fix: resolve "Invalid time value" crash in timeline/search endpoints The root cause was Drizzle ORM's `timestamp()` (without timezone) using a fragile date conversion: `new Date(value + "+0000")` which produces non-standard date strings that can result in Invalid Date objects depending on the runtime environment. Two-part fix: 1. Schema: Change all `timestamp()` columns to `timestamp({ withTimezone: true })` so Drizzle uses `new Date(value)` directly on timezone-aware PostgreSQL strings. Includes migration 0009 to convert existing columns. 2. Defense: Add `safeFormatISO` / `safeToISOString` utilities that validate dates before formatting, preventing RangeError crashes in `formatISO` and `.toISOString()` call sites across formatting, extract-graph, dream, and ingest-conversation. https://claude.ai/code/session_01SDE1HZqgcEgcjWs1UPqAY6 --- drizzle/0009_timestamp_to_timestamptz.sql | 17 +++++++++++++ drizzle/meta/_journal.json | 7 ++++++ src/db/schema.ts | 28 +++++++++++----------- src/lib/conversation-store.ts | 4 +++- src/lib/extract-graph.ts | 3 ++- src/lib/formatting.ts | 10 ++++---- src/lib/jobs/dream.ts | 3 ++- src/lib/jobs/ingest-conversation.ts | 3 ++- src/lib/safe-date.ts | 29 +++++++++++++++++++++++ 9 files changed, 81 insertions(+), 23 deletions(-) create mode 100644 drizzle/0009_timestamp_to_timestamptz.sql create mode 100644 src/lib/safe-date.ts diff --git a/drizzle/0009_timestamp_to_timestamptz.sql b/drizzle/0009_timestamp_to_timestamptz.sql new file mode 100644 index 0000000..0f994d9 --- /dev/null +++ b/drizzle/0009_timestamp_to_timestamptz.sql @@ -0,0 +1,17 @@ +-- Convert all timestamp columns to timestamp with time zone. +-- PostgreSQL interprets existing values as UTC during conversion. + +ALTER TABLE "nodes" ALTER COLUMN "created_at" TYPE timestamptz USING "created_at" AT TIME ZONE 'UTC';--> statement-breakpoint +ALTER TABLE "node_metadata" ALTER COLUMN "created_at" TYPE timestamptz USING "created_at" AT TIME ZONE 'UTC';--> statement-breakpoint +ALTER TABLE "edges" ALTER COLUMN "created_at" TYPE timestamptz USING "created_at" AT TIME ZONE 'UTC';--> statement-breakpoint +ALTER TABLE "node_embeddings" ALTER COLUMN "created_at" TYPE timestamptz USING "created_at" AT TIME ZONE 'UTC';--> statement-breakpoint +ALTER TABLE "edge_embeddings" ALTER COLUMN "created_at" TYPE timestamptz USING "created_at" AT TIME ZONE 'UTC';--> statement-breakpoint +ALTER TABLE "aliases" ALTER COLUMN "created_at" TYPE timestamptz USING "created_at" AT TIME ZONE 'UTC';--> statement-breakpoint +ALTER TABLE "sources" ALTER COLUMN "last_ingested_at" TYPE timestamptz USING "last_ingested_at" AT TIME ZONE 'UTC';--> statement-breakpoint +ALTER TABLE "sources" ALTER COLUMN "created_at" TYPE timestamptz USING "created_at" AT TIME ZONE 'UTC';--> statement-breakpoint +ALTER TABLE "sources" ALTER COLUMN "deleted_at" TYPE timestamptz USING "deleted_at" AT TIME ZONE 'UTC';--> statement-breakpoint +ALTER TABLE "source_links" ALTER COLUMN "created_at" TYPE timestamptz USING "created_at" AT TIME ZONE 'UTC';--> statement-breakpoint +ALTER TABLE "user_profiles" ALTER COLUMN "last_updated_at" TYPE timestamptz USING "last_updated_at" AT TIME ZONE 'UTC';--> statement-breakpoint +ALTER TABLE "user_profiles" ALTER COLUMN "created_at" TYPE timestamptz USING "created_at" AT TIME ZONE 'UTC';--> statement-breakpoint +ALTER TABLE "scratchpads" ALTER COLUMN "updated_at" TYPE timestamptz USING "updated_at" AT TIME ZONE 'UTC';--> statement-breakpoint +ALTER TABLE "scratchpads" ALTER COLUMN "created_at" TYPE timestamptz USING "created_at" AT TIME ZONE 'UTC'; diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index 05af970..a8b6d0c 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -64,6 +64,13 @@ "when": 1775920239473, "tag": "0008_worthless_bullseye", "breakpoints": true + }, + { + "idx": 9, + "version": "7", + "when": 1776134400000, + "tag": "0009_timestamp_to_timestamptz", + "breakpoints": true } ] } \ No newline at end of file diff --git a/src/db/schema.ts b/src/db/schema.ts index 1b19ada..129142c 100644 --- a/src/db/schema.ts +++ b/src/db/schema.ts @@ -27,7 +27,7 @@ export const nodes = pgTable( .references(() => users.id) .notNull(), nodeType: varchar("node_type", { length: 50 }).notNull().$type(), - createdAt: timestamp().defaultNow().notNull(), + createdAt: timestamp({ withTimezone: true }).defaultNow().notNull(), // Index on (userId, nodeType) might be useful }, (table) => [ @@ -60,7 +60,7 @@ export const nodeMetadata = pgTable( canonicalLabel: text("canonical_label"), description: text(), additionalData: jsonb(), - createdAt: timestamp("created_at").defaultNow().notNull(), + createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(), }, (table) => [ index("node_metadata_node_id_idx").on(table.nodeId), @@ -96,7 +96,7 @@ export const edges = pgTable( // Temporal aspect for relationships // validFrom: timestamp('valid_from'), // validTo: timestamp('valid_to'), - createdAt: timestamp().defaultNow().notNull(), + createdAt: timestamp({ withTimezone: true }).defaultNow().notNull(), // Indexes on (userId, sourceNodeId), (userId, targetNodeId), (userId, edgeType) }, (table) => [ @@ -139,7 +139,7 @@ export const nodeEmbeddings = pgTable( .notNull(), embedding: vector("embedding", { dimensions: 1024 }).notNull(), // Dimension depends on model modelName: varchar("model_name", { length: 100 }).notNull(), - createdAt: timestamp().defaultNow().notNull(), + createdAt: timestamp({ withTimezone: true }).defaultNow().notNull(), // Unique constraint on (nodeId, modelName)? Or allow multiple embeddings per node? Let's start with unique. }, (table) => [ @@ -167,7 +167,7 @@ export const edgeEmbeddings = pgTable( .notNull(), embedding: vector("embedding", { dimensions: 1024 }).notNull(), // Same dimension as node embeddings modelName: varchar("model_name", { length: 100 }).notNull(), - createdAt: timestamp().defaultNow().notNull(), + createdAt: timestamp({ withTimezone: true }).defaultNow().notNull(), }, (table) => [ index("edge_embeddings_embedding_idx").using( @@ -196,7 +196,7 @@ export const aliases = pgTable("aliases", { canonicalNodeId: typeId("node") .references(() => nodes.id, { onDelete: "cascade" }) .notNull(), // The node this alias refers to - createdAt: timestamp().defaultNow().notNull(), + createdAt: timestamp({ withTimezone: true }).defaultNow().notNull(), // Index on (userId, aliasText) for fast lookups // Index on (userId, canonicalNodeId) }); @@ -226,12 +226,12 @@ export const sources = pgTable( parentSource: typeId("source"), metadata: jsonb(), // e.g., Notion page title, chat participants - lastIngestedAt: timestamp(), + lastIngestedAt: timestamp({ withTimezone: true }), status: varchar("status", { length: 20 }) .default("pending") .$type(), // e.g., 'pending', 'processing', 'completed', 'failed', 'summarized' - createdAt: timestamp().defaultNow().notNull(), - deletedAt: timestamp("deleted_at"), + createdAt: timestamp({ withTimezone: true }).defaultNow().notNull(), + deletedAt: timestamp("deleted_at", { withTimezone: true }), contentType: varchar("content_type", { length: 100 }), contentLength: integer("content_length"), }, @@ -268,7 +268,7 @@ export const sourceLinks = pgTable( .notNull(), // The ID of the node or edge // Optional: more specific location within the source (e.g., block ID, line number, timestamp in audio) specificLocation: text(), - createdAt: timestamp().defaultNow().notNull(), + createdAt: timestamp({ withTimezone: true }).defaultNow().notNull(), }, (table) => [ unique().on(table.sourceId, table.nodeId), @@ -296,8 +296,8 @@ export const userProfiles = pgTable("user_profiles", { .references(() => users.id) .notNull(), content: text().notNull(), // The descriptive text - lastUpdatedAt: timestamp().defaultNow().notNull(), - createdAt: timestamp().defaultNow().notNull(), + lastUpdatedAt: timestamp({ withTimezone: true }).defaultNow().notNull(), + createdAt: timestamp({ withTimezone: true }).defaultNow().notNull(), // Index on (userId) }); @@ -316,8 +316,8 @@ export const scratchpads = pgTable( .references(() => users.id) .notNull(), content: text().notNull().default(""), - updatedAt: timestamp("updated_at").defaultNow().notNull(), - createdAt: timestamp("created_at").defaultNow().notNull(), + updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(), + createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(), }, (table) => [ unique().on(table.userId), diff --git a/src/lib/conversation-store.ts b/src/lib/conversation-store.ts index d81739b..2ec85c1 100644 --- a/src/lib/conversation-store.ts +++ b/src/lib/conversation-store.ts @@ -57,7 +57,9 @@ export async function loadConversationTurns( role: meta.role, name: meta.name, content: meta.rawContent, - timestamp: new Date(meta.timestamp), + timestamp: isNaN(new Date(meta.timestamp).getTime()) + ? new Date() + : new Date(meta.timestamp), }; }); } diff --git a/src/lib/extract-graph.ts b/src/lib/extract-graph.ts index 8ceb975..7464103 100644 --- a/src/lib/extract-graph.ts +++ b/src/lib/extract-graph.ts @@ -5,6 +5,7 @@ import { } from "./embeddings-util"; import { formatNodesForPrompt } from "./formatting"; import { findSimilarNodes, findOneHopNodes, findNodesByType } from "./graph"; +import { safeToISOString } from "./safe-date"; import { normalizeLabel } from "./label"; import { TemporaryIdMapper } from "./temporary-id-mapper"; import { and, eq, inArray } from "drizzle-orm"; @@ -105,7 +106,7 @@ export async function extractGraph({ type: node.type, label: node.label, description: node.description, - timestamp: node.timestamp.toISOString(), + timestamp: safeToISOString(node.timestamp), }); } diff --git a/src/lib/formatting.ts b/src/lib/formatting.ts index 7b2b697..f5c7381 100644 --- a/src/lib/formatting.ts +++ b/src/lib/formatting.ts @@ -1,9 +1,9 @@ -import { formatISO } from "date-fns"; import type { NodeSearchResult, EdgeSearchResult, OneHopNode, } from "~/lib/graph"; +import { safeFormatISO } from "~/lib/safe-date"; interface Message { content: string; @@ -19,7 +19,7 @@ export function formatConversationAsXml(messages: Message[]): string { return messages .map( (message, index) => - ` + ` ${message.content.replace(//g, ">")} `, ) @@ -102,7 +102,7 @@ export type SearchResults = SearchResultItem[]; // Helpers for formatting individual result items function formatSearchNode(node: NodeSearchResult): string { - return ` + return ` ${escapeXml(node.description ?? "")} `; @@ -111,7 +111,7 @@ function formatSearchNode(node: NodeSearchResult): string { function formatSearchEdge(edge: EdgeSearchResult): string { return ` + )}" type="${escapeXml(edge.edgeType)}" timestamp="${safeFormatISO(edge.timestamp)}"> ${escapeXml(edge.description ?? "")} `; } @@ -119,7 +119,7 @@ function formatSearchEdge(edge: EdgeSearchResult): string { function formatSearchConnection(conn: OneHopNode): string { return ` + )}" type="${escapeXml(conn.edgeType)}" timestamp="${safeFormatISO(conn.timestamp)}"> ${escapeXml(conn.description ?? "")} `; diff --git a/src/lib/jobs/dream.ts b/src/lib/jobs/dream.ts index dca8c13..7784b6d 100644 --- a/src/lib/jobs/dream.ts +++ b/src/lib/jobs/dream.ts @@ -1,4 +1,5 @@ import { addDays, formatISO } from "date-fns"; +import { safeToISOString } from "~/lib/safe-date"; import { eq } from "drizzle-orm"; import { z } from "zod"; import type { DrizzleDB } from "~/db"; @@ -91,7 +92,7 @@ async function handleTopic( label: n.label, description: n.description, tempId: n.id, - timestamp: n.timestamp.toISOString(), + timestamp: safeToISOString(n.timestamp), })); const context = formatNodesForPrompt(nodesForPrompt); const dream = await generateDreamContent( diff --git a/src/lib/jobs/ingest-conversation.ts b/src/lib/jobs/ingest-conversation.ts index 7d2251e..8a10ea2 100644 --- a/src/lib/jobs/ingest-conversation.ts +++ b/src/lib/jobs/ingest-conversation.ts @@ -3,6 +3,7 @@ import { formatConversationAsXml } from "../formatting"; import { ensureSourceNode } from "../ingestion/ensure-source-node"; import { ensureUser } from "../ingestion/ensure-user"; import { insertNewSources } from "../ingestion/insert-new-sources"; +import { safeToISOString } from "../safe-date"; import { z } from "zod"; import { DrizzleDB } from "~/db"; import { type ConversationTurn } from "~/lib/conversation-store"; @@ -106,7 +107,7 @@ async function initializeConversation( rawContent: m.content, role: m.role, name: m.name, - timestamp: m.timestamp.toISOString(), + timestamp: safeToISOString(m.timestamp), }, })), }); diff --git a/src/lib/safe-date.ts b/src/lib/safe-date.ts new file mode 100644 index 0000000..699361a --- /dev/null +++ b/src/lib/safe-date.ts @@ -0,0 +1,29 @@ +import { formatISO } from "date-fns"; + +/** + * Converts a value to a valid Date, returning the current time if invalid. + */ +function toValidDate(value: Date | string | number): Date { + const date = value instanceof Date ? value : new Date(value); + if (isNaN(date.getTime())) { + return new Date(); + } + return date; +} + +/** + * Safely formats a date value as ISO 8601. + * Falls back to the current time if the input is an invalid date, + * preventing RangeError: Invalid time value crashes. + */ +export function safeFormatISO(value: Date | string | number): string { + return formatISO(toValidDate(value)); +} + +/** + * Safely converts a date value to an ISO string. + * Falls back to the current time if the input is an invalid date. + */ +export function safeToISOString(value: Date | string | number): string { + return toValidDate(value).toISOString(); +} From 33d4318661004c5fd2f4af6c189163c173cc389e Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 12 Apr 2026 06:02:59 +0000 Subject: [PATCH 2/4] fix: use drizzle-kit generated migration for timestamptz conversion Replace manually written migration with proper drizzle-kit generate output, which includes the snapshot JSON needed for drizzle-kit migrate to work. https://claude.ai/code/session_01SDE1HZqgcEgcjWs1UPqAY6 --- drizzle/0009_married_smasher.sql | 14 + drizzle/meta/0009_snapshot.json | 1059 ++++++++++++++++++++++++++++++ drizzle/meta/_journal.json | 4 +- 3 files changed, 1075 insertions(+), 2 deletions(-) create mode 100644 drizzle/0009_married_smasher.sql create mode 100644 drizzle/meta/0009_snapshot.json diff --git a/drizzle/0009_married_smasher.sql b/drizzle/0009_married_smasher.sql new file mode 100644 index 0000000..c5a3b90 --- /dev/null +++ b/drizzle/0009_married_smasher.sql @@ -0,0 +1,14 @@ +ALTER TABLE "aliases" ALTER COLUMN "created_at" SET DATA TYPE timestamp with time zone;--> statement-breakpoint +ALTER TABLE "edge_embeddings" ALTER COLUMN "created_at" SET DATA TYPE timestamp with time zone;--> statement-breakpoint +ALTER TABLE "edges" ALTER COLUMN "created_at" SET DATA TYPE timestamp with time zone;--> statement-breakpoint +ALTER TABLE "node_embeddings" ALTER COLUMN "created_at" SET DATA TYPE timestamp with time zone;--> statement-breakpoint +ALTER TABLE "node_metadata" ALTER COLUMN "created_at" SET DATA TYPE timestamp with time zone;--> statement-breakpoint +ALTER TABLE "nodes" ALTER COLUMN "created_at" SET DATA TYPE timestamp with time zone;--> statement-breakpoint +ALTER TABLE "scratchpads" ALTER COLUMN "updated_at" SET DATA TYPE timestamp with time zone;--> statement-breakpoint +ALTER TABLE "scratchpads" ALTER COLUMN "created_at" SET DATA TYPE timestamp with time zone;--> statement-breakpoint +ALTER TABLE "source_links" ALTER COLUMN "created_at" SET DATA TYPE timestamp with time zone;--> statement-breakpoint +ALTER TABLE "sources" ALTER COLUMN "last_ingested_at" SET DATA TYPE timestamp with time zone;--> statement-breakpoint +ALTER TABLE "sources" ALTER COLUMN "created_at" SET DATA TYPE timestamp with time zone;--> statement-breakpoint +ALTER TABLE "sources" ALTER COLUMN "deleted_at" SET DATA TYPE timestamp with time zone;--> statement-breakpoint +ALTER TABLE "user_profiles" ALTER COLUMN "last_updated_at" SET DATA TYPE timestamp with time zone;--> statement-breakpoint +ALTER TABLE "user_profiles" ALTER COLUMN "created_at" SET DATA TYPE timestamp with time zone; \ No newline at end of file diff --git a/drizzle/meta/0009_snapshot.json b/drizzle/meta/0009_snapshot.json new file mode 100644 index 0000000..c8ae628 --- /dev/null +++ b/drizzle/meta/0009_snapshot.json @@ -0,0 +1,1059 @@ +{ + "id": "82ac1333-e0c0-4179-900f-35e92b5754af", + "prevId": "ec92429f-cdba-4dc8-a78d-eb69e7ea7a46", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.aliases": { + "name": "aliases", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "alias_text": { + "name": "alias_text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "canonical_node_id": { + "name": "canonical_node_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "aliases_user_id_users_id_fk": { + "name": "aliases_user_id_users_id_fk", + "tableFrom": "aliases", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "aliases_canonical_node_id_nodes_id_fk": { + "name": "aliases_canonical_node_id_nodes_id_fk", + "tableFrom": "aliases", + "tableTo": "nodes", + "columnsFrom": [ + "canonical_node_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.edge_embeddings": { + "name": "edge_embeddings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "edge_id": { + "name": "edge_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "embedding": { + "name": "embedding", + "type": "vector(1024)", + "primaryKey": false, + "notNull": true + }, + "model_name": { + "name": "model_name", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "edge_embeddings_embedding_idx": { + "name": "edge_embeddings_embedding_idx", + "columns": [ + { + "expression": "embedding", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "vector_cosine_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": {} + }, + "edge_embeddings_edge_id_idx": { + "name": "edge_embeddings_edge_id_idx", + "columns": [ + { + "expression": "edge_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "edge_embeddings_edge_id_edges_id_fk": { + "name": "edge_embeddings_edge_id_edges_id_fk", + "tableFrom": "edge_embeddings", + "tableTo": "edges", + "columnsFrom": [ + "edge_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.edges": { + "name": "edges", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_node_id": { + "name": "source_node_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_node_id": { + "name": "target_node_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "edge_type": { + "name": "edge_type", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "edges_user_id_source_node_id_idx": { + "name": "edges_user_id_source_node_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_node_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "edges_user_id_target_node_id_idx": { + "name": "edges_user_id_target_node_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_node_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "edges_user_id_edge_type_idx": { + "name": "edges_user_id_edge_type_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "edge_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "edges_user_id_users_id_fk": { + "name": "edges_user_id_users_id_fk", + "tableFrom": "edges", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "edges_source_node_id_nodes_id_fk": { + "name": "edges_source_node_id_nodes_id_fk", + "tableFrom": "edges", + "tableTo": "nodes", + "columnsFrom": [ + "source_node_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "edges_target_node_id_nodes_id_fk": { + "name": "edges_target_node_id_nodes_id_fk", + "tableFrom": "edges", + "tableTo": "nodes", + "columnsFrom": [ + "target_node_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "edges_sourceNodeId_targetNodeId_edge_type_unique": { + "name": "edges_sourceNodeId_targetNodeId_edge_type_unique", + "nullsNotDistinct": false, + "columns": [ + "source_node_id", + "target_node_id", + "edge_type" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.node_embeddings": { + "name": "node_embeddings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "node_id": { + "name": "node_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "embedding": { + "name": "embedding", + "type": "vector(1024)", + "primaryKey": false, + "notNull": true + }, + "model_name": { + "name": "model_name", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "node_embeddings_embedding_idx": { + "name": "node_embeddings_embedding_idx", + "columns": [ + { + "expression": "embedding", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "vector_cosine_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": {} + }, + "node_embeddings_node_id_idx": { + "name": "node_embeddings_node_id_idx", + "columns": [ + { + "expression": "node_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "node_embeddings_node_id_nodes_id_fk": { + "name": "node_embeddings_node_id_nodes_id_fk", + "tableFrom": "node_embeddings", + "tableTo": "nodes", + "columnsFrom": [ + "node_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.node_metadata": { + "name": "node_metadata", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "node_id": { + "name": "node_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "canonical_label": { + "name": "canonical_label", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "additional_data": { + "name": "additional_data", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "node_metadata_node_id_idx": { + "name": "node_metadata_node_id_idx", + "columns": [ + { + "expression": "node_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "node_metadata_canonical_label_idx": { + "name": "node_metadata_canonical_label_idx", + "columns": [ + { + "expression": "canonical_label", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "node_metadata_node_id_nodes_id_fk": { + "name": "node_metadata_node_id_nodes_id_fk", + "tableFrom": "node_metadata", + "tableTo": "nodes", + "columnsFrom": [ + "node_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "node_metadata_nodeId_unique": { + "name": "node_metadata_nodeId_unique", + "nullsNotDistinct": false, + "columns": [ + "node_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.nodes": { + "name": "nodes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "node_type": { + "name": "node_type", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "nodes_user_id_idx": { + "name": "nodes_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "nodes_user_id_node_type_idx": { + "name": "nodes_user_id_node_type_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "node_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "nodes_user_id_users_id_fk": { + "name": "nodes_user_id_users_id_fk", + "tableFrom": "nodes", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scratchpads": { + "name": "scratchpads", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "scratchpads_user_id_idx": { + "name": "scratchpads_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scratchpads_user_id_users_id_fk": { + "name": "scratchpads_user_id_users_id_fk", + "tableFrom": "scratchpads", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "scratchpads_userId_unique": { + "name": "scratchpads_userId_unique", + "nullsNotDistinct": false, + "columns": [ + "user_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.source_links": { + "name": "source_links", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "node_id": { + "name": "node_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "specific_location": { + "name": "specific_location", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "source_links_source_id_idx": { + "name": "source_links_source_id_idx", + "columns": [ + { + "expression": "source_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "source_links_node_id_idx": { + "name": "source_links_node_id_idx", + "columns": [ + { + "expression": "node_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "source_links_source_id_sources_id_fk": { + "name": "source_links_source_id_sources_id_fk", + "tableFrom": "source_links", + "tableTo": "sources", + "columnsFrom": [ + "source_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "source_links_node_id_nodes_id_fk": { + "name": "source_links_node_id_nodes_id_fk", + "tableFrom": "source_links", + "tableTo": "nodes", + "columnsFrom": [ + "node_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "source_links_sourceId_nodeId_unique": { + "name": "source_links_sourceId_nodeId_unique", + "nullsNotDistinct": false, + "columns": [ + "source_id", + "node_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sources": { + "name": "sources", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_source": { + "name": "parent_source", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "last_ingested_at": { + "name": "last_ingested_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false, + "default": "'pending'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "content_type": { + "name": "content_type", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "content_length": { + "name": "content_length", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "sources_user_id_idx": { + "name": "sources_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sources_status_idx": { + "name": "sources_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sources_user_id_users_id_fk": { + "name": "sources_user_id_users_id_fk", + "tableFrom": "sources", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "sources_userId_type_externalId_unique": { + "name": "sources_userId_type_externalId_unique", + "nullsNotDistinct": false, + "columns": [ + "user_id", + "type", + "external_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_profiles": { + "name": "user_profiles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_updated_at": { + "name": "last_updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "user_profiles_user_id_users_id_fk": { + "name": "user_profiles_user_id_users_id_fk", + "tableFrom": "user_profiles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index a8b6d0c..72b7a0d 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -68,8 +68,8 @@ { "idx": 9, "version": "7", - "when": 1776134400000, - "tag": "0009_timestamp_to_timestamptz", + "when": 1775973748566, + "tag": "0009_married_smasher", "breakpoints": true } ] From 5c064bc28919793258d30e1fcfe2c970d2af93ce Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 12 Apr 2026 06:03:32 +0000 Subject: [PATCH 3/4] chore: remove orphaned manual migration file The manually-written 0009_timestamp_to_timestamptz.sql was replaced by the drizzle-kit generated 0009_married_smasher.sql but the deletion wasn't staged. https://claude.ai/code/session_01SDE1HZqgcEgcjWs1UPqAY6 --- drizzle/0009_timestamp_to_timestamptz.sql | 17 ----------------- 1 file changed, 17 deletions(-) delete mode 100644 drizzle/0009_timestamp_to_timestamptz.sql diff --git a/drizzle/0009_timestamp_to_timestamptz.sql b/drizzle/0009_timestamp_to_timestamptz.sql deleted file mode 100644 index 0f994d9..0000000 --- a/drizzle/0009_timestamp_to_timestamptz.sql +++ /dev/null @@ -1,17 +0,0 @@ --- Convert all timestamp columns to timestamp with time zone. --- PostgreSQL interprets existing values as UTC during conversion. - -ALTER TABLE "nodes" ALTER COLUMN "created_at" TYPE timestamptz USING "created_at" AT TIME ZONE 'UTC';--> statement-breakpoint -ALTER TABLE "node_metadata" ALTER COLUMN "created_at" TYPE timestamptz USING "created_at" AT TIME ZONE 'UTC';--> statement-breakpoint -ALTER TABLE "edges" ALTER COLUMN "created_at" TYPE timestamptz USING "created_at" AT TIME ZONE 'UTC';--> statement-breakpoint -ALTER TABLE "node_embeddings" ALTER COLUMN "created_at" TYPE timestamptz USING "created_at" AT TIME ZONE 'UTC';--> statement-breakpoint -ALTER TABLE "edge_embeddings" ALTER COLUMN "created_at" TYPE timestamptz USING "created_at" AT TIME ZONE 'UTC';--> statement-breakpoint -ALTER TABLE "aliases" ALTER COLUMN "created_at" TYPE timestamptz USING "created_at" AT TIME ZONE 'UTC';--> statement-breakpoint -ALTER TABLE "sources" ALTER COLUMN "last_ingested_at" TYPE timestamptz USING "last_ingested_at" AT TIME ZONE 'UTC';--> statement-breakpoint -ALTER TABLE "sources" ALTER COLUMN "created_at" TYPE timestamptz USING "created_at" AT TIME ZONE 'UTC';--> statement-breakpoint -ALTER TABLE "sources" ALTER COLUMN "deleted_at" TYPE timestamptz USING "deleted_at" AT TIME ZONE 'UTC';--> statement-breakpoint -ALTER TABLE "source_links" ALTER COLUMN "created_at" TYPE timestamptz USING "created_at" AT TIME ZONE 'UTC';--> statement-breakpoint -ALTER TABLE "user_profiles" ALTER COLUMN "last_updated_at" TYPE timestamptz USING "last_updated_at" AT TIME ZONE 'UTC';--> statement-breakpoint -ALTER TABLE "user_profiles" ALTER COLUMN "created_at" TYPE timestamptz USING "created_at" AT TIME ZONE 'UTC';--> statement-breakpoint -ALTER TABLE "scratchpads" ALTER COLUMN "updated_at" TYPE timestamptz USING "updated_at" AT TIME ZONE 'UTC';--> statement-breakpoint -ALTER TABLE "scratchpads" ALTER COLUMN "created_at" TYPE timestamptz USING "created_at" AT TIME ZONE 'UTC'; From b885fb671d797e791069d1bb0469bf0d5a177161 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 12 Apr 2026 06:08:22 +0000 Subject: [PATCH 4/4] fix: address review feedback and formatting CI failure - Export `safeToDate` from safe-date.ts (renamed from private `toValidDate`) per Gemini review: makes the helper reusable across the codebase - Use `safeToDate` in conversation-store.ts instead of inline validation, removing duplicated logic - Fix Prettier formatting in schema.ts, extract-graph.ts, dream.ts https://claude.ai/code/session_01SDE1HZqgcEgcjWs1UPqAY6 --- src/db/schema.ts | 12 +++++++++--- src/lib/conversation-store.ts | 5 ++--- src/lib/extract-graph.ts | 2 +- src/lib/jobs/dream.ts | 2 +- src/lib/safe-date.ts | 6 +++--- 5 files changed, 16 insertions(+), 11 deletions(-) diff --git a/src/db/schema.ts b/src/db/schema.ts index 129142c..541d4ed 100644 --- a/src/db/schema.ts +++ b/src/db/schema.ts @@ -60,7 +60,9 @@ export const nodeMetadata = pgTable( canonicalLabel: text("canonical_label"), description: text(), additionalData: jsonb(), - createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(), + createdAt: timestamp("created_at", { withTimezone: true }) + .defaultNow() + .notNull(), }, (table) => [ index("node_metadata_node_id_idx").on(table.nodeId), @@ -316,8 +318,12 @@ export const scratchpads = pgTable( .references(() => users.id) .notNull(), content: text().notNull().default(""), - updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(), - createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(), + updatedAt: timestamp("updated_at", { withTimezone: true }) + .defaultNow() + .notNull(), + createdAt: timestamp("created_at", { withTimezone: true }) + .defaultNow() + .notNull(), }, (table) => [ unique().on(table.userId), diff --git a/src/lib/conversation-store.ts b/src/lib/conversation-store.ts index 2ec85c1..7790e24 100644 --- a/src/lib/conversation-store.ts +++ b/src/lib/conversation-store.ts @@ -1,5 +1,6 @@ import { z } from "zod"; import { DrizzleDB } from "~/db"; +import { safeToDate } from "~/lib/safe-date"; import { TypeId } from "~/types/typeid"; // Schema to parse stored metadata for conversation messages @@ -57,9 +58,7 @@ export async function loadConversationTurns( role: meta.role, name: meta.name, content: meta.rawContent, - timestamp: isNaN(new Date(meta.timestamp).getTime()) - ? new Date() - : new Date(meta.timestamp), + timestamp: safeToDate(meta.timestamp), }; }); } diff --git a/src/lib/extract-graph.ts b/src/lib/extract-graph.ts index 7464103..82eaa00 100644 --- a/src/lib/extract-graph.ts +++ b/src/lib/extract-graph.ts @@ -5,8 +5,8 @@ import { } from "./embeddings-util"; import { formatNodesForPrompt } from "./formatting"; import { findSimilarNodes, findOneHopNodes, findNodesByType } from "./graph"; -import { safeToISOString } from "./safe-date"; import { normalizeLabel } from "./label"; +import { safeToISOString } from "./safe-date"; import { TemporaryIdMapper } from "./temporary-id-mapper"; import { and, eq, inArray } from "drizzle-orm"; import { zodResponseFormat } from "openai/helpers/zod.mjs"; diff --git a/src/lib/jobs/dream.ts b/src/lib/jobs/dream.ts index 7784b6d..92e8004 100644 --- a/src/lib/jobs/dream.ts +++ b/src/lib/jobs/dream.ts @@ -1,5 +1,4 @@ import { addDays, formatISO } from "date-fns"; -import { safeToISOString } from "~/lib/safe-date"; import { eq } from "drizzle-orm"; import { z } from "zod"; import type { DrizzleDB } from "~/db"; @@ -8,6 +7,7 @@ import { crateTextCompletion, performStructuredAnalysis } from "~/lib/ai"; import { generateEmbeddings } from "~/lib/embeddings"; import { formatNodesForPrompt } from "~/lib/formatting"; import { findSimilarNodes, type NodeSearchResult } from "~/lib/graph"; +import { safeToISOString } from "~/lib/safe-date"; import { NodeTypeEnum } from "~/types/graph"; import { TypeId } from "~/types/typeid"; import { useDatabase } from "~/utils/db"; diff --git a/src/lib/safe-date.ts b/src/lib/safe-date.ts index 699361a..f7b95fb 100644 --- a/src/lib/safe-date.ts +++ b/src/lib/safe-date.ts @@ -3,7 +3,7 @@ import { formatISO } from "date-fns"; /** * Converts a value to a valid Date, returning the current time if invalid. */ -function toValidDate(value: Date | string | number): Date { +export function safeToDate(value: Date | string | number): Date { const date = value instanceof Date ? value : new Date(value); if (isNaN(date.getTime())) { return new Date(); @@ -17,7 +17,7 @@ function toValidDate(value: Date | string | number): Date { * preventing RangeError: Invalid time value crashes. */ export function safeFormatISO(value: Date | string | number): string { - return formatISO(toValidDate(value)); + return formatISO(safeToDate(value)); } /** @@ -25,5 +25,5 @@ export function safeFormatISO(value: Date | string | number): string { * Falls back to the current time if the input is an invalid date. */ export function safeToISOString(value: Date | string | number): string { - return toValidDate(value).toISOString(); + return safeToDate(value).toISOString(); }