opencode-manager 0.4.2 → 0.4.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -183,6 +183,24 @@ function msToDate(ms?: number | null): Date | null {
183
183
  return new Date(ms)
184
184
  }
185
185
 
186
+ function parseTimestamp(value: unknown): Date | null {
187
+ if (value instanceof Date) {
188
+ return Number.isNaN(value.getTime()) ? null : value
189
+ }
190
+ if (typeof value === "number") {
191
+ return msToDate(value)
192
+ }
193
+ if (typeof value === "string") {
194
+ const numeric = Number(value)
195
+ if (!Number.isNaN(numeric)) {
196
+ return msToDate(numeric)
197
+ }
198
+ const parsed = new Date(value)
199
+ return Number.isNaN(parsed.getTime()) ? null : parsed
200
+ }
201
+ return null
202
+ }
203
+
186
204
  /**
187
205
  * Check if a path exists and is a directory.
188
206
  */
@@ -311,6 +329,75 @@ function validateSchemaForTables(db: Database, requirements: SchemaRequirements)
311
329
  }
312
330
  }
313
331
 
332
+ function getTableColumns(db: Database, table: string): string[] | null {
333
+ const tableRow = db.query("SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?").get(table) as { name?: string } | undefined
334
+ if (!tableRow?.name) {
335
+ return null
336
+ }
337
+ const columnRows = db.query(`PRAGMA table_info(${table})`).all() as { name: string }[]
338
+ return columnRows.map((row) => row.name)
339
+ }
340
+
341
+ function ensureTableColumns(
342
+ db: Database,
343
+ table: string,
344
+ required: string[],
345
+ options?: SqliteLoadOptions,
346
+ context?: string,
347
+ allowForceWrite = false
348
+ ): Set<string> | null {
349
+ let columns: string[] | null
350
+ try {
351
+ columns = getTableColumns(db, table)
352
+ } catch (error) {
353
+ const message = formatSqliteErrorMessage(error, "Failed to read SQLite schema", {
354
+ forceWrite: options?.forceWrite,
355
+ allowForceWrite,
356
+ })
357
+ if (isSqliteBusyError(error) || options?.strict) {
358
+ throw new Error(message)
359
+ }
360
+ warnSqlite(options, message)
361
+ return null
362
+ }
363
+
364
+ if (!columns) {
365
+ const message = `${context ? `${context}: ` : ""}SQLite schema is invalid (missing table: ${table}).`
366
+ if (options?.strict) {
367
+ throw new Error(message)
368
+ }
369
+ warnSqlite(options, message)
370
+ return null
371
+ }
372
+
373
+ const columnSet = new Set(columns)
374
+ const missing = required.filter((column) => !columnSet.has(column))
375
+ if (missing.length > 0) {
376
+ const available = Array.from(columnSet).join(", ")
377
+ const message = `${context ? `${context}: ` : ""}SQLite schema is invalid (missing columns: ${missing.map((col) => `${table}.${col}`).join(", ")}). Available columns: ${available}.`
378
+ if (options?.strict) {
379
+ throw new Error(message)
380
+ }
381
+ warnSqlite(options, message)
382
+ return null
383
+ }
384
+
385
+ return columnSet
386
+ }
387
+
388
+ function pickColumn(columns: Set<string>, candidates: string[]): string | null {
389
+ for (const candidate of candidates) {
390
+ if (columns.has(candidate)) {
391
+ return candidate
392
+ }
393
+ }
394
+ return null
395
+ }
396
+
397
+ function buildColumnAlias(column: string | null, alias: string): string {
398
+ return column ? `${column} as ${alias}` : `NULL as ${alias}`
399
+ }
400
+
314
401
  function formatSchemaIssues(result: SchemaValidationResult, context?: string): string {
315
402
  const parts: string[] = []
316
403
  if (result.missingTables.length > 0) {
@@ -420,8 +507,11 @@ export function validateSchema(
420
507
  * Raw row structure from the SQLite project table.
421
508
  */
422
509
  interface ProjectRow {
423
- id: string
424
- data: string
510
+ id: string | null
511
+ data: string | null
512
+ worktree?: string | null
513
+ vcs?: string | null
514
+ created_at?: number | string | null
425
515
  }
426
516
 
427
517
  /**
@@ -430,6 +520,9 @@ interface ProjectRow {
430
520
  interface ProjectData {
431
521
  id?: string
432
522
  worktree?: string
523
+ directory?: string
524
+ path?: string
525
+ root?: string
433
526
  vcs?: string
434
527
  time?: {
435
528
  created?: number
@@ -452,14 +545,39 @@ export async function loadProjectRecordsSqlite(
452
545
  const records: ProjectRecord[] = []
453
546
 
454
547
  try {
455
- if (!ensureSchema(db, buildSchemaRequirements(["project"]), options, "loadProjectRecords")) {
548
+ const columns = ensureTableColumns(db, "project", [], options, "loadProjectRecords")
549
+ if (!columns) {
550
+ return []
551
+ }
552
+
553
+ const idColumn = pickColumn(columns, ["id", "project_id"])
554
+ if (!idColumn) {
555
+ const available = Array.from(columns).join(", ")
556
+ const message = `loadProjectRecords: SQLite schema is invalid (missing columns: project.id). Available columns: ${available}.`
557
+ if (options.strict) {
558
+ throw new Error(message)
559
+ }
560
+ warnSqlite(options, message)
456
561
  return []
457
562
  }
458
563
 
564
+ const dataColumn = pickColumn(columns, ["data", "metadata", "payload", "json"])
565
+ const worktreeColumn = pickColumn(columns, ["worktree", "directory", "path", "root", "repo_path"])
566
+ const vcsColumn = pickColumn(columns, ["vcs", "scm", "vcs_type"])
567
+ const createdColumn = pickColumn(columns, ["created_at", "created", "created_ms", "createdAt"])
568
+
569
+ const selectColumns = [
570
+ buildColumnAlias(idColumn, "id"),
571
+ buildColumnAlias(dataColumn, "data"),
572
+ buildColumnAlias(worktreeColumn, "worktree"),
573
+ buildColumnAlias(vcsColumn, "vcs"),
574
+ buildColumnAlias(createdColumn, "created_at"),
575
+ ]
576
+
459
577
  // Query all projects from the database
460
578
  let rows: ProjectRow[] = []
461
579
  try {
462
- rows = db.query("SELECT id, data FROM project").all() as ProjectRow[]
580
+ rows = db.query(`SELECT ${selectColumns.join(", ")} FROM project`).all() as ProjectRow[]
463
581
  } catch (error) {
464
582
  const message = formatSqliteErrorMessage(error, "Failed to query project table", {
465
583
  forceWrite: options.forceWrite,
@@ -476,26 +594,33 @@ export async function loadProjectRecordsSqlite(
476
594
  }
477
595
 
478
596
  for (const row of rows) {
597
+ if (!row.id) {
598
+ continue
599
+ }
479
600
  let data: ProjectData = {}
480
601
 
481
- // Parse JSON data column, skip malformed entries
482
- try {
483
- data = JSON.parse(row.data) as ProjectData
484
- } catch (error) {
485
- const message = formatSqliteErrorMessage(
486
- error,
487
- `Malformed JSON in project row "${row.id}"`,
488
- options
489
- )
490
- if (options.strict) {
491
- throw new Error(message)
602
+ if (row.data && row.data.trim().length > 0) {
603
+ // Parse JSON data column, skip malformed entries
604
+ try {
605
+ data = JSON.parse(row.data) as ProjectData
606
+ } catch (error) {
607
+ const message = formatSqliteErrorMessage(
608
+ error,
609
+ `Malformed JSON in project row "${row.id}"`,
610
+ options
611
+ )
612
+ if (options.strict) {
613
+ throw new Error(message)
614
+ }
615
+ warnSqlite(options, message)
616
+ continue
492
617
  }
493
- warnSqlite(options, message)
494
- continue
495
618
  }
496
619
 
497
- const createdAt = msToDate(data.time?.created)
498
- const worktree = expandUserPath(data.worktree)
620
+ const worktreeRaw = row.worktree ?? data.worktree ?? data.directory ?? data.path ?? data.root
621
+ const worktree = expandUserPath(worktreeRaw ?? null)
622
+ const createdAt = parseTimestamp(row.created_at) ?? parseTimestamp(data.time?.created)
623
+ const vcs = typeof row.vcs === "string" ? row.vcs : (typeof data.vcs === "string" ? data.vcs : null)
499
624
  const state = await computeState(worktree)
500
625
 
501
626
  records.push({
@@ -504,7 +629,7 @@ export async function loadProjectRecordsSqlite(
504
629
  filePath: `sqlite:project:${row.id}`, // Virtual path for SQLite records
505
630
  projectId: row.id,
506
631
  worktree: worktree ?? "",
507
- vcs: typeof data.vcs === "string" ? data.vcs : null,
632
+ vcs,
508
633
  createdAt,
509
634
  state,
510
635
  })
@@ -543,12 +668,15 @@ export interface SqliteSessionLoadOptions extends SqliteLoadOptions {
543
668
  * Raw row structure from the SQLite session table.
544
669
  */
545
670
  interface SessionRow {
546
- id: string
547
- project_id: string
548
- parent_id: string | null
549
- created_at: number | null
550
- updated_at: number | null
551
- data: string
671
+ id: string | null
672
+ project_id?: string | null
673
+ parent_id?: string | null
674
+ created_at?: number | string | null
675
+ updated_at?: number | string | null
676
+ data?: string | null
677
+ directory?: string | null
678
+ title?: string | null
679
+ version?: string | null
552
680
  }
553
681
 
554
682
  /**
@@ -557,8 +685,11 @@ interface SessionRow {
557
685
  interface SessionData {
558
686
  id?: string
559
687
  projectID?: string
688
+ projectId?: string
560
689
  parentID?: string
561
690
  directory?: string
691
+ cwd?: string
692
+ path?: string
562
693
  title?: string
563
694
  version?: string
564
695
  time?: {
@@ -583,16 +714,50 @@ export async function loadSessionRecordsSqlite(
583
714
  const records: SessionRecord[] = []
584
715
 
585
716
  try {
586
- if (!ensureSchema(db, buildSchemaRequirements(["session"]), options, "loadSessionRecords")) {
717
+ const columns = ensureTableColumns(db, "session", [], options, "loadSessionRecords")
718
+ if (!columns) {
587
719
  return []
588
720
  }
589
721
 
722
+ const idColumn = pickColumn(columns, ["id", "session_id"])
723
+ if (!idColumn) {
724
+ const available = Array.from(columns).join(", ")
725
+ const message = `loadSessionRecords: SQLite schema is invalid (missing columns: session.id). Available columns: ${available}.`
726
+ if (options.strict) {
727
+ throw new Error(message)
728
+ }
729
+ warnSqlite(options, message)
730
+ return []
731
+ }
732
+
733
+ const projectIdColumn = pickColumn(columns, ["project_id", "projectID", "projectId", "project"])
734
+ const parentIdColumn = pickColumn(columns, ["parent_id", "parentID", "parentId", "parent"])
735
+ const createdColumn = pickColumn(columns, ["created_at", "created", "created_ms", "createdAt"])
736
+ const updatedColumn = pickColumn(columns, ["updated_at", "updated", "updated_ms", "updatedAt"])
737
+ const dataColumn = pickColumn(columns, ["data", "metadata", "payload", "json"])
738
+ const directoryColumn = pickColumn(columns, ["directory", "cwd", "path", "worktree", "root"])
739
+ const titleColumn = pickColumn(columns, ["title", "name"])
740
+ const versionColumn = pickColumn(columns, ["version", "client_version", "opencode_version"])
741
+
742
+ const selectColumns = [
743
+ buildColumnAlias(idColumn, "id"),
744
+ buildColumnAlias(projectIdColumn, "project_id"),
745
+ buildColumnAlias(parentIdColumn, "parent_id"),
746
+ buildColumnAlias(createdColumn, "created_at"),
747
+ buildColumnAlias(updatedColumn, "updated_at"),
748
+ buildColumnAlias(dataColumn, "data"),
749
+ buildColumnAlias(directoryColumn, "directory"),
750
+ buildColumnAlias(titleColumn, "title"),
751
+ buildColumnAlias(versionColumn, "version"),
752
+ ]
753
+
590
754
  // Build query with optional project_id filter
591
- let query = "SELECT id, project_id, parent_id, created_at, updated_at, data FROM session"
755
+ let query = `SELECT ${selectColumns.join(", ")} FROM session`
592
756
  const params: string[] = []
593
-
594
- if (options.projectId) {
595
- query += " WHERE project_id = ?"
757
+ const postFilterProjectId = options.projectId && !projectIdColumn ? options.projectId : null
758
+
759
+ if (options.projectId && projectIdColumn) {
760
+ query += ` WHERE ${projectIdColumn} = ?`
596
761
  params.push(options.projectId)
597
762
  }
598
763
 
@@ -617,38 +782,50 @@ export async function loadSessionRecordsSqlite(
617
782
  }
618
783
 
619
784
  for (const row of rows) {
785
+ if (!row.id) {
786
+ continue
787
+ }
620
788
  let data: SessionData = {}
621
789
 
622
- // Parse JSON data column, skip malformed entries
623
- try {
624
- data = JSON.parse(row.data) as SessionData
625
- } catch (error) {
626
- const message = formatSqliteErrorMessage(
627
- error,
628
- `Malformed JSON in session row "${row.id}"`,
629
- options
630
- )
631
- if (options.strict) {
632
- throw new Error(message)
790
+ if (row.data && row.data.trim().length > 0) {
791
+ // Parse JSON data column, skip malformed entries
792
+ try {
793
+ data = JSON.parse(row.data) as SessionData
794
+ } catch (error) {
795
+ const message = formatSqliteErrorMessage(
796
+ error,
797
+ `Malformed JSON in session row "${row.id}"`,
798
+ options
799
+ )
800
+ if (options.strict) {
801
+ throw new Error(message)
802
+ }
803
+ warnSqlite(options, message)
804
+ continue
633
805
  }
634
- warnSqlite(options, message)
806
+ }
807
+
808
+ const projectId = row.project_id ?? data.projectID ?? data.projectId ?? ""
809
+ if (postFilterProjectId && projectId !== postFilterProjectId) {
635
810
  continue
636
811
  }
637
812
 
638
813
  // Use column values first, fall back to data JSON
639
- // created_at/updated_at columns are epoch milliseconds
640
- const createdAt = msToDate(row.created_at) ?? msToDate(data.time?.created)
641
- const updatedAt = msToDate(row.updated_at) ?? msToDate(data.time?.updated)
642
- const directory = expandUserPath(data.directory)
814
+ const createdAt = parseTimestamp(row.created_at) ?? parseTimestamp(data.time?.created)
815
+ const updatedAt = parseTimestamp(row.updated_at) ?? parseTimestamp(data.time?.updated)
816
+ const directoryRaw = row.directory ?? data.directory ?? data.cwd ?? data.path
817
+ const directory = expandUserPath(directoryRaw ?? null)
818
+ const title = typeof row.title === "string" ? row.title : (typeof data.title === "string" ? data.title : "")
819
+ const version = typeof row.version === "string" ? row.version : (typeof data.version === "string" ? data.version : "")
643
820
 
644
821
  records.push({
645
822
  index: 0, // Will be set by withIndex
646
823
  filePath: `sqlite:session:${row.id}`, // Virtual path for SQLite records
647
824
  sessionId: row.id,
648
- projectId: row.project_id ?? data.projectID ?? "",
825
+ projectId,
649
826
  directory: directory ?? "",
650
- title: typeof data.title === "string" ? data.title : "",
651
- version: typeof data.version === "string" ? data.version : "",
827
+ title,
828
+ version,
652
829
  createdAt,
653
830
  updatedAt,
654
831
  })
@@ -689,10 +866,18 @@ export interface SqliteChatLoadOptions extends SqliteLoadOptions {
689
866
  * Raw row structure from the SQLite message table.
690
867
  */
691
868
  interface MessageRow {
692
- id: string
693
- session_id: string
694
- created_at: number | null
695
- data: string
869
+ id: string | null
870
+ session_id?: string | null
871
+ created_at?: number | string | null
872
+ data?: string | null
873
+ role?: string | null
874
+ parent_id?: string | null
875
+ tokens_json?: string | null
876
+ input_tokens?: number | null
877
+ output_tokens?: number | null
878
+ reasoning_tokens?: number | null
879
+ cache_read?: number | null
880
+ cache_write?: number | null
696
881
  }
697
882
 
698
883
  /**
@@ -761,6 +946,42 @@ function parseMessageTokens(tokens: MessageData["tokens"]): TokenBreakdown | nul
761
946
  return breakdown
762
947
  }
763
948
 
949
+ function parseTokenColumns(row: MessageRow): TokenBreakdown | null {
950
+ if (row.tokens_json) {
951
+ try {
952
+ const parsed = JSON.parse(row.tokens_json) as MessageData["tokens"]
953
+ const fromJson = parseMessageTokens(parsed)
954
+ if (fromJson) {
955
+ return fromJson
956
+ }
957
+ } catch {
958
+ // fall through to numeric columns
959
+ }
960
+ }
961
+
962
+ const input = asTokenNumber(row.input_tokens)
963
+ const output = asTokenNumber(row.output_tokens)
964
+ const reasoning = asTokenNumber(row.reasoning_tokens)
965
+ const cacheRead = asTokenNumber(row.cache_read)
966
+ const cacheWrite = asTokenNumber(row.cache_write)
967
+
968
+ const hasAny = input !== null || output !== null || reasoning !== null || cacheRead !== null || cacheWrite !== null
969
+ if (!hasAny) {
970
+ return null
971
+ }
972
+
973
+ const breakdown: TokenBreakdown = {
974
+ input: input ?? 0,
975
+ output: output ?? 0,
976
+ reasoning: reasoning ?? 0,
977
+ cacheRead: cacheRead ?? 0,
978
+ cacheWrite: cacheWrite ?? 0,
979
+ total: 0,
980
+ }
981
+ breakdown.total = breakdown.input + breakdown.output + breakdown.reasoning + breakdown.cacheRead + breakdown.cacheWrite
982
+ return breakdown
983
+ }
984
+
764
985
  /**
765
986
  * Load chat message index for a session from SQLite (metadata only, no parts).
766
987
  * Returns an array of ChatMessage stubs with parts set to null.
@@ -775,15 +996,56 @@ export async function loadSessionChatIndexSqlite(
775
996
  const messages: ChatMessage[] = []
776
997
 
777
998
  try {
778
- if (!ensureSchema(db, buildSchemaRequirements(["message"]), options, "loadSessionChatIndex")) {
999
+ const columns = ensureTableColumns(db, "message", [], options, "loadSessionChatIndex")
1000
+ if (!columns) {
1001
+ return []
1002
+ }
1003
+
1004
+ const idColumn = pickColumn(columns, ["id", "message_id"])
1005
+ const sessionIdColumn = pickColumn(columns, ["session_id", "sessionId"])
1006
+ if (!idColumn || !sessionIdColumn) {
1007
+ const available = Array.from(columns).join(", ")
1008
+ const message = `loadSessionChatIndex: SQLite schema is invalid (missing columns: message.id or message.session_id). Available columns: ${available}.`
1009
+ if (options.strict) {
1010
+ throw new Error(message)
1011
+ }
1012
+ warnSqlite(options, message)
779
1013
  return []
780
1014
  }
781
1015
 
782
- // Query messages for the given session, ordered by created_at ascending
1016
+ const createdColumn = pickColumn(columns, ["created_at", "created", "created_ms", "createdAt"])
1017
+ const dataColumn = pickColumn(columns, ["data", "metadata", "payload", "json"])
1018
+ const roleColumn = pickColumn(columns, ["role", "type"])
1019
+ const parentColumn = pickColumn(columns, ["parent_id", "parentID", "parentId", "parent"])
1020
+ const tokensColumn = pickColumn(columns, ["tokens", "token_data", "token_json"])
1021
+ const inputTokensColumn = pickColumn(columns, ["input_tokens", "tokens_input", "input"])
1022
+ const outputTokensColumn = pickColumn(columns, ["output_tokens", "tokens_output", "output"])
1023
+ const reasoningTokensColumn = pickColumn(columns, ["reasoning_tokens", "tokens_reasoning", "reasoning"])
1024
+ const cacheReadColumn = pickColumn(columns, ["cache_read", "cacheRead", "tokens_cache_read"])
1025
+ const cacheWriteColumn = pickColumn(columns, ["cache_write", "cacheWrite", "tokens_cache_write"])
1026
+
1027
+ const selectColumns = [
1028
+ buildColumnAlias(idColumn, "id"),
1029
+ buildColumnAlias(sessionIdColumn, "session_id"),
1030
+ buildColumnAlias(createdColumn, "created_at"),
1031
+ buildColumnAlias(dataColumn, "data"),
1032
+ buildColumnAlias(roleColumn, "role"),
1033
+ buildColumnAlias(parentColumn, "parent_id"),
1034
+ buildColumnAlias(tokensColumn, "tokens_json"),
1035
+ buildColumnAlias(inputTokensColumn, "input_tokens"),
1036
+ buildColumnAlias(outputTokensColumn, "output_tokens"),
1037
+ buildColumnAlias(reasoningTokensColumn, "reasoning_tokens"),
1038
+ buildColumnAlias(cacheReadColumn, "cache_read"),
1039
+ buildColumnAlias(cacheWriteColumn, "cache_write"),
1040
+ ]
1041
+
1042
+ const orderBy = createdColumn ? ` ORDER BY ${createdColumn} ASC` : ` ORDER BY ${idColumn} ASC`
1043
+
1044
+ // Query messages for the given session
783
1045
  let rows: MessageRow[] = []
784
1046
  try {
785
1047
  rows = db.query(
786
- "SELECT id, session_id, created_at, data FROM message WHERE session_id = ? ORDER BY created_at ASC"
1048
+ `SELECT ${selectColumns.join(", ")} FROM message WHERE ${sessionIdColumn} = ?${orderBy}`
787
1049
  ).all(options.sessionId) as MessageRow[]
788
1050
  } catch (error) {
789
1051
  const message = formatSqliteErrorMessage(error, "Failed to query message table", {
@@ -801,48 +1063,52 @@ export async function loadSessionChatIndexSqlite(
801
1063
  }
802
1064
 
803
1065
  for (const row of rows) {
1066
+ if (!row.id) {
1067
+ continue
1068
+ }
804
1069
  let data: MessageData = {}
805
1070
 
806
- // Parse JSON data column, skip malformed entries
807
- try {
808
- data = JSON.parse(row.data) as MessageData
809
- } catch (error) {
810
- const message = formatSqliteErrorMessage(
811
- error,
812
- `Malformed JSON in message row "${row.id}"`,
813
- options
814
- )
815
- if (options.strict) {
816
- throw new Error(message)
1071
+ if (row.data && row.data.trim().length > 0) {
1072
+ // Parse JSON data column, skip malformed entries
1073
+ try {
1074
+ data = JSON.parse(row.data) as MessageData
1075
+ } catch (error) {
1076
+ const message = formatSqliteErrorMessage(
1077
+ error,
1078
+ `Malformed JSON in message row "${row.id}"`,
1079
+ options
1080
+ )
1081
+ if (options.strict) {
1082
+ throw new Error(message)
1083
+ }
1084
+ warnSqlite(options, message)
1085
+ continue
817
1086
  }
818
- warnSqlite(options, message)
819
- continue
820
1087
  }
821
1088
 
822
1089
  // Determine role
1090
+ const roleRaw = typeof row.role === "string" ? row.role : data.role
823
1091
  const role: ChatRole =
824
- data.role === "user" ? "user" :
825
- data.role === "assistant" ? "assistant" :
1092
+ roleRaw === "user" ? "user" :
1093
+ roleRaw === "assistant" ? "assistant" :
826
1094
  "unknown"
827
1095
 
828
1096
  // Use column timestamp first, fall back to data JSON
829
- const createdAt = msToDate(row.created_at) ?? msToDate(data.time?.created)
1097
+ const createdAt = parseTimestamp(row.created_at) ?? parseTimestamp(data.time?.created)
830
1098
 
831
1099
  // Parse tokens for assistant messages
832
1100
  let tokens: TokenBreakdown | undefined
833
- if (role === "assistant" && data.tokens) {
834
- const parsed = parseMessageTokens(data.tokens)
835
- if (parsed) {
836
- tokens = parsed
837
- }
1101
+ if (role === "assistant") {
1102
+ tokens = parseMessageTokens(data.tokens) ?? parseTokenColumns(row) ?? undefined
838
1103
  }
839
1104
 
1105
+ const sessionId = row.session_id ?? data.sessionID ?? options.sessionId
840
1106
  messages.push({
841
- sessionId: row.session_id,
1107
+ sessionId,
842
1108
  messageId: row.id,
843
1109
  role,
844
1110
  createdAt,
845
- parentId: data.parentID,
1111
+ parentId: row.parent_id ?? data.parentID,
846
1112
  tokens,
847
1113
  parts: null,
848
1114
  previewText: "[loading...]",
@@ -885,10 +1151,14 @@ export interface SqlitePartsLoadOptions extends SqliteLoadOptions {
885
1151
  * Raw row structure from the SQLite part table.
886
1152
  */
887
1153
  interface PartRow {
888
- id: string
889
- message_id: string
890
- session_id: string
891
- data: string
1154
+ id: string | null
1155
+ message_id?: string | null
1156
+ session_id?: string | null
1157
+ data?: string | null
1158
+ type?: string | null
1159
+ text?: string | null
1160
+ tool?: string | null
1161
+ state?: string | null
892
1162
  }
893
1163
 
894
1164
  /**
@@ -977,15 +1247,46 @@ export async function loadMessagePartsSqlite(
977
1247
  const parts: ChatPart[] = []
978
1248
 
979
1249
  try {
980
- if (!ensureSchema(db, buildSchemaRequirements(["part"]), options, "loadMessageParts")) {
1250
+ const columns = ensureTableColumns(db, "part", [], options, "loadMessageParts")
1251
+ if (!columns) {
1252
+ return []
1253
+ }
1254
+
1255
+ const idColumn = pickColumn(columns, ["id", "part_id"])
1256
+ const messageIdColumn = pickColumn(columns, ["message_id", "messageId"])
1257
+ if (!idColumn || !messageIdColumn) {
1258
+ const available = Array.from(columns).join(", ")
1259
+ const message = `loadMessageParts: SQLite schema is invalid (missing columns: part.id or part.message_id). Available columns: ${available}.`
1260
+ if (options.strict) {
1261
+ throw new Error(message)
1262
+ }
1263
+ warnSqlite(options, message)
981
1264
  return []
982
1265
  }
983
1266
 
1267
+ const sessionIdColumn = pickColumn(columns, ["session_id", "sessionId"])
1268
+ const dataColumn = pickColumn(columns, ["data", "metadata", "payload", "json"])
1269
+ const typeColumn = pickColumn(columns, ["type", "part_type"])
1270
+ const textColumn = pickColumn(columns, ["text", "content", "body"])
1271
+ const toolColumn = pickColumn(columns, ["tool", "tool_name"])
1272
+ const stateColumn = pickColumn(columns, ["state", "tool_state"])
1273
+
1274
+ const selectColumns = [
1275
+ buildColumnAlias(idColumn, "id"),
1276
+ buildColumnAlias(messageIdColumn, "message_id"),
1277
+ buildColumnAlias(sessionIdColumn, "session_id"),
1278
+ buildColumnAlias(dataColumn, "data"),
1279
+ buildColumnAlias(typeColumn, "type"),
1280
+ buildColumnAlias(textColumn, "text"),
1281
+ buildColumnAlias(toolColumn, "tool"),
1282
+ buildColumnAlias(stateColumn, "state"),
1283
+ ]
1284
+
984
1285
  // Query parts for the given message
985
1286
  let rows: PartRow[] = []
986
1287
  try {
987
1288
  rows = db.query(
988
- "SELECT id, message_id, session_id, data FROM part WHERE message_id = ?"
1289
+ `SELECT ${selectColumns.join(", ")} FROM part WHERE ${messageIdColumn} = ?`
989
1290
  ).all(options.messageId) as PartRow[]
990
1291
  } catch (error) {
991
1292
  const message = formatSqliteErrorMessage(error, "Failed to query part table", {
@@ -1003,22 +1304,44 @@ export async function loadMessagePartsSqlite(
1003
1304
  }
1004
1305
 
1005
1306
  for (const row of rows) {
1307
+ if (!row.id) {
1308
+ continue
1309
+ }
1006
1310
  let data: PartData = {}
1007
1311
 
1008
- // Parse JSON data column, skip malformed entries
1009
- try {
1010
- data = JSON.parse(row.data) as PartData
1011
- } catch (error) {
1012
- const message = formatSqliteErrorMessage(
1013
- error,
1014
- `Malformed JSON in part row "${row.id}"`,
1015
- options
1016
- )
1017
- if (options.strict) {
1018
- throw new Error(message)
1312
+ if (row.data && row.data.trim().length > 0) {
1313
+ // Parse JSON data column, skip malformed entries
1314
+ try {
1315
+ data = JSON.parse(row.data) as PartData
1316
+ } catch (error) {
1317
+ const message = formatSqliteErrorMessage(
1318
+ error,
1319
+ `Malformed JSON in part row "${row.id}"`,
1320
+ options
1321
+ )
1322
+ if (options.strict) {
1323
+ throw new Error(message)
1324
+ }
1325
+ warnSqlite(options, message)
1326
+ continue
1327
+ }
1328
+ }
1329
+
1330
+ if (typeof row.type === "string" && !data.type) {
1331
+ data.type = row.type
1332
+ }
1333
+ if (typeof row.text === "string" && data.text === undefined) {
1334
+ data.text = row.text
1335
+ }
1336
+ if (typeof row.tool === "string" && !data.tool) {
1337
+ data.tool = row.tool
1338
+ }
1339
+ if (row.state && !data.state) {
1340
+ try {
1341
+ data.state = JSON.parse(row.state) as PartData["state"]
1342
+ } catch {
1343
+ data.state = { status: row.state }
1019
1344
  }
1020
- warnSqlite(options, message)
1021
- continue
1022
1345
  }
1023
1346
 
1024
1347
  // Determine part type
@@ -1033,7 +1356,7 @@ export async function loadMessagePartsSqlite(
1033
1356
 
1034
1357
  parts.push({
1035
1358
  partId: row.id,
1036
- messageId: row.message_id,
1359
+ messageId: row.message_id ?? data.messageID ?? options.messageId,
1037
1360
  type,
1038
1361
  text: extracted.text,
1039
1362
  toolName: extracted.toolName,
@@ -1113,15 +1436,16 @@ export async function deleteSessionMetadataSqlite(
1113
1436
  return { removed, failed }
1114
1437
  }
1115
1438
 
1116
- let schemaMessage: string | null
1439
+ let sessionColumns: Set<string> | null
1440
+ let messageColumns: Set<string> | null
1441
+ let partColumns: Set<string> | null
1442
+
1117
1443
  try {
1118
- schemaMessage = getSchemaIssueMessage(
1119
- db,
1120
- buildSchemaRequirements(["session", "message", "part"]),
1121
- "deleteSessionMetadata"
1122
- )
1444
+ sessionColumns = ensureTableColumns(db, "session", [], options, "deleteSessionMetadata", true)
1445
+ messageColumns = ensureTableColumns(db, "message", [], options, "deleteSessionMetadata", true)
1446
+ partColumns = ensureTableColumns(db, "part", [], options, "deleteSessionMetadata", true)
1123
1447
  } catch (error) {
1124
- const message = formatSqliteErrorMessage(error, "Failed to read SQLite schema", options)
1448
+ const message = error instanceof Error ? error.message : String(error)
1125
1449
  if (options.strict) {
1126
1450
  throw new Error(message)
1127
1451
  }
@@ -1130,13 +1454,72 @@ export async function deleteSessionMetadataSqlite(
1130
1454
  }
1131
1455
  return { removed, failed }
1132
1456
  }
1133
- if (schemaMessage) {
1457
+
1458
+ if (!sessionColumns || !messageColumns || !partColumns) {
1459
+ const message = "deleteSessionMetadata: SQLite schema is invalid (missing required tables)."
1134
1460
  if (options.strict) {
1135
- throw new Error(schemaMessage)
1461
+ throw new Error(message)
1136
1462
  }
1137
- warnSqlite(options, schemaMessage)
1463
+ warnSqlite(options, message)
1464
+ for (const sessionId of sessionIds) {
1465
+ failed.push({ path: `sqlite:session:${sessionId}`, error: message })
1466
+ }
1467
+ return { removed, failed }
1468
+ }
1469
+
1470
+ const sessionIdColumn = pickColumn(sessionColumns, ["id", "session_id"])
1471
+ if (!sessionIdColumn) {
1472
+ const available = Array.from(sessionColumns).join(", ")
1473
+ const message = `deleteSessionMetadata: SQLite schema is invalid (missing columns: session.id). Available columns: ${available}.`
1474
+ if (options.strict) {
1475
+ throw new Error(message)
1476
+ }
1477
+ warnSqlite(options, message)
1478
+ for (const sessionId of sessionIds) {
1479
+ failed.push({ path: `sqlite:session:${sessionId}`, error: message })
1480
+ }
1481
+ return { removed, failed }
1482
+ }
1483
+
1484
+ const messageSessionIdColumn = pickColumn(messageColumns, ["session_id", "sessionId"])
1485
+ const messageIdColumn = pickColumn(messageColumns, ["id", "message_id"])
1486
+ if (!messageSessionIdColumn) {
1487
+ const available = Array.from(messageColumns).join(", ")
1488
+ const message = `deleteSessionMetadata: SQLite schema is invalid (missing columns: message.session_id). Available columns: ${available}.`
1489
+ if (options.strict) {
1490
+ throw new Error(message)
1491
+ }
1492
+ warnSqlite(options, message)
1138
1493
  for (const sessionId of sessionIds) {
1139
- failed.push({ path: `sqlite:session:${sessionId}`, error: schemaMessage })
1494
+ failed.push({ path: `sqlite:session:${sessionId}`, error: message })
1495
+ }
1496
+ return { removed, failed }
1497
+ }
1498
+
1499
+ const partSessionIdColumn = pickColumn(partColumns, ["session_id", "sessionId"])
1500
+ const partMessageIdColumn = pickColumn(partColumns, ["message_id", "messageId"])
1501
+ if (!partSessionIdColumn && !partMessageIdColumn) {
1502
+ const available = Array.from(partColumns).join(", ")
1503
+ const message = `deleteSessionMetadata: SQLite schema is invalid (missing columns: part.session_id or part.message_id). Available columns: ${available}.`
1504
+ if (options.strict) {
1505
+ throw new Error(message)
1506
+ }
1507
+ warnSqlite(options, message)
1508
+ for (const sessionId of sessionIds) {
1509
+ failed.push({ path: `sqlite:session:${sessionId}`, error: message })
1510
+ }
1511
+ return { removed, failed }
1512
+ }
1513
+
1514
+ if (!partSessionIdColumn && !messageIdColumn) {
1515
+ const available = Array.from(messageColumns).join(", ")
1516
+ const message = `deleteSessionMetadata: SQLite schema is invalid (missing columns: message.id). Available columns: ${available}.`
1517
+ if (options.strict) {
1518
+ throw new Error(message)
1519
+ }
1520
+ warnSqlite(options, message)
1521
+ for (const sessionId of sessionIds) {
1522
+ failed.push({ path: `sqlite:session:${sessionId}`, error: message })
1140
1523
  }
1141
1524
  return { removed, failed }
1142
1525
  }
@@ -1145,7 +1528,7 @@ export async function deleteSessionMetadataSqlite(
1145
1528
  // Dry run: just check which sessions exist and would be deleted
1146
1529
  const placeholders = sessionIds.map(() => "?").join(", ")
1147
1530
  const selectStmt = db.prepare(
1148
- `SELECT id FROM session WHERE id IN (${placeholders})`
1531
+ `SELECT ${sessionIdColumn} as id FROM session WHERE ${sessionIdColumn} IN (${placeholders})`
1149
1532
  )
1150
1533
  const existingRows = selectStmt.all(...sessionIds) as { id: string }[]
1151
1534
 
@@ -1185,29 +1568,46 @@ export async function deleteSessionMetadataSqlite(
1185
1568
  // Build parameterized query with placeholders
1186
1569
  const placeholders = sessionIds.map(() => "?").join(", ")
1187
1570
 
1571
+ let messageIds: string[] = []
1572
+ if (!partSessionIdColumn && messageIdColumn) {
1573
+ const selectMessageIds = db.prepare(
1574
+ `SELECT ${messageIdColumn} as id FROM message WHERE ${messageSessionIdColumn} IN (${placeholders})`
1575
+ )
1576
+ const messageRows = selectMessageIds.all(...sessionIds) as { id: string }[]
1577
+ messageIds = messageRows.map(r => r.id)
1578
+ }
1579
+
1188
1580
  // Delete parts first (child of message, also references session_id directly)
1189
- const deleteParts = db.prepare(
1190
- `DELETE FROM part WHERE session_id IN (${placeholders})`
1191
- )
1192
- deleteParts.run(...sessionIds)
1581
+ if (partSessionIdColumn) {
1582
+ const deleteParts = db.prepare(
1583
+ `DELETE FROM part WHERE ${partSessionIdColumn} IN (${placeholders})`
1584
+ )
1585
+ deleteParts.run(...sessionIds)
1586
+ } else if (partMessageIdColumn && messageIds.length > 0) {
1587
+ const messagePlaceholders = messageIds.map(() => "?").join(", ")
1588
+ const deleteParts = db.prepare(
1589
+ `DELETE FROM part WHERE ${partMessageIdColumn} IN (${messagePlaceholders})`
1590
+ )
1591
+ deleteParts.run(...messageIds)
1592
+ }
1193
1593
 
1194
1594
  // Delete messages next (child of session)
1195
1595
  const deleteMessages = db.prepare(
1196
- `DELETE FROM message WHERE session_id IN (${placeholders})`
1596
+ `DELETE FROM message WHERE ${messageSessionIdColumn} IN (${placeholders})`
1197
1597
  )
1198
1598
  deleteMessages.run(...sessionIds)
1199
1599
 
1200
1600
  // Finally delete sessions
1201
1601
  // Get the list of actually deleted sessions for accurate reporting
1202
1602
  const selectSessions = db.prepare(
1203
- `SELECT id FROM session WHERE id IN (${placeholders})`
1603
+ `SELECT ${sessionIdColumn} as id FROM session WHERE ${sessionIdColumn} IN (${placeholders})`
1204
1604
  )
1205
1605
  const existingRows = selectSessions.all(...sessionIds) as { id: string }[]
1206
1606
 
1207
1607
  const existingIds = new Set(existingRows.map(r => r.id))
1208
1608
 
1209
1609
  const deleteSessions = db.prepare(
1210
- `DELETE FROM session WHERE id IN (${placeholders})`
1610
+ `DELETE FROM session WHERE ${sessionIdColumn} IN (${placeholders})`
1211
1611
  )
1212
1612
  deleteSessions.run(...sessionIds)
1213
1613
 
@@ -1316,15 +1716,18 @@ export async function deleteProjectMetadataSqlite(
1316
1716
  return { removed, failed }
1317
1717
  }
1318
1718
 
1319
- let schemaMessage: string | null
1719
+ let projectColumns: Set<string> | null
1720
+ let sessionColumns: Set<string> | null
1721
+ let messageColumns: Set<string> | null
1722
+ let partColumns: Set<string> | null
1723
+
1320
1724
  try {
1321
- schemaMessage = getSchemaIssueMessage(
1322
- db,
1323
- buildSchemaRequirements(["project", "session", "message", "part"]),
1324
- "deleteProjectMetadata"
1325
- )
1725
+ projectColumns = ensureTableColumns(db, "project", [], options, "deleteProjectMetadata", true)
1726
+ sessionColumns = ensureTableColumns(db, "session", [], options, "deleteProjectMetadata", true)
1727
+ messageColumns = ensureTableColumns(db, "message", [], options, "deleteProjectMetadata", true)
1728
+ partColumns = ensureTableColumns(db, "part", [], options, "deleteProjectMetadata", true)
1326
1729
  } catch (error) {
1327
- const message = formatSqliteErrorMessage(error, "Failed to read SQLite schema", options)
1730
+ const message = error instanceof Error ? error.message : String(error)
1328
1731
  if (options.strict) {
1329
1732
  throw new Error(message)
1330
1733
  }
@@ -1333,13 +1736,87 @@ export async function deleteProjectMetadataSqlite(
1333
1736
  }
1334
1737
  return { removed, failed }
1335
1738
  }
1336
- if (schemaMessage) {
1739
+
1740
+ if (!projectColumns || !sessionColumns || !messageColumns || !partColumns) {
1741
+ const message = "deleteProjectMetadata: SQLite schema is invalid (missing required tables)."
1337
1742
  if (options.strict) {
1338
- throw new Error(schemaMessage)
1743
+ throw new Error(message)
1339
1744
  }
1340
- warnSqlite(options, schemaMessage)
1745
+ warnSqlite(options, message)
1746
+ for (const projectId of projectIds) {
1747
+ failed.push({ path: `sqlite:project:${projectId}`, error: message })
1748
+ }
1749
+ return { removed, failed }
1750
+ }
1751
+
1752
+ const projectIdColumn = pickColumn(projectColumns, ["id", "project_id"])
1753
+ if (!projectIdColumn) {
1754
+ const available = Array.from(projectColumns).join(", ")
1755
+ const message = `deleteProjectMetadata: SQLite schema is invalid (missing columns: project.id). Available columns: ${available}.`
1756
+ if (options.strict) {
1757
+ throw new Error(message)
1758
+ }
1759
+ warnSqlite(options, message)
1760
+ for (const projectId of projectIds) {
1761
+ failed.push({ path: `sqlite:project:${projectId}`, error: message })
1762
+ }
1763
+ return { removed, failed }
1764
+ }
1765
+
1766
+ const sessionIdColumn = pickColumn(sessionColumns, ["id", "session_id"])
1767
+ const sessionProjectIdColumn = pickColumn(sessionColumns, ["project_id", "projectID", "projectId", "project"])
1768
+ if (!sessionIdColumn || !sessionProjectIdColumn) {
1769
+ const available = Array.from(sessionColumns).join(", ")
1770
+ const message = `deleteProjectMetadata: SQLite schema is invalid (missing columns: session.id or session.project_id). Available columns: ${available}.`
1771
+ if (options.strict) {
1772
+ throw new Error(message)
1773
+ }
1774
+ warnSqlite(options, message)
1341
1775
  for (const projectId of projectIds) {
1342
- failed.push({ path: `sqlite:project:${projectId}`, error: schemaMessage })
1776
+ failed.push({ path: `sqlite:project:${projectId}`, error: message })
1777
+ }
1778
+ return { removed, failed }
1779
+ }
1780
+
1781
+ const messageSessionIdColumn = pickColumn(messageColumns, ["session_id", "sessionId"])
1782
+ const messageIdColumn = pickColumn(messageColumns, ["id", "message_id"])
1783
+ if (!messageSessionIdColumn) {
1784
+ const available = Array.from(messageColumns).join(", ")
1785
+ const message = `deleteProjectMetadata: SQLite schema is invalid (missing columns: message.session_id). Available columns: ${available}.`
1786
+ if (options.strict) {
1787
+ throw new Error(message)
1788
+ }
1789
+ warnSqlite(options, message)
1790
+ for (const projectId of projectIds) {
1791
+ failed.push({ path: `sqlite:project:${projectId}`, error: message })
1792
+ }
1793
+ return { removed, failed }
1794
+ }
1795
+
1796
+ const partSessionIdColumn = pickColumn(partColumns, ["session_id", "sessionId"])
1797
+ const partMessageIdColumn = pickColumn(partColumns, ["message_id", "messageId"])
1798
+ if (!partSessionIdColumn && !partMessageIdColumn) {
1799
+ const available = Array.from(partColumns).join(", ")
1800
+ const message = `deleteProjectMetadata: SQLite schema is invalid (missing columns: part.session_id or part.message_id). Available columns: ${available}.`
1801
+ if (options.strict) {
1802
+ throw new Error(message)
1803
+ }
1804
+ warnSqlite(options, message)
1805
+ for (const projectId of projectIds) {
1806
+ failed.push({ path: `sqlite:project:${projectId}`, error: message })
1807
+ }
1808
+ return { removed, failed }
1809
+ }
1810
+
1811
+ if (!partSessionIdColumn && !messageIdColumn) {
1812
+ const available = Array.from(messageColumns).join(", ")
1813
+ const message = `deleteProjectMetadata: SQLite schema is invalid (missing columns: message.id). Available columns: ${available}.`
1814
+ if (options.strict) {
1815
+ throw new Error(message)
1816
+ }
1817
+ warnSqlite(options, message)
1818
+ for (const projectId of projectIds) {
1819
+ failed.push({ path: `sqlite:project:${projectId}`, error: message })
1343
1820
  }
1344
1821
  return { removed, failed }
1345
1822
  }
@@ -1349,7 +1826,7 @@ export async function deleteProjectMetadataSqlite(
1349
1826
 
1350
1827
  // First, get all session IDs for these projects
1351
1828
  const selectSessions = db.prepare(
1352
- `SELECT id FROM session WHERE project_id IN (${placeholders})`
1829
+ `SELECT ${sessionIdColumn} as id FROM session WHERE ${sessionProjectIdColumn} IN (${placeholders})`
1353
1830
  )
1354
1831
  const sessionRows = selectSessions.all(...projectIds) as { id: string }[]
1355
1832
  const sessionIds = sessionRows.map(r => r.id)
@@ -1358,7 +1835,7 @@ export async function deleteProjectMetadataSqlite(
1358
1835
  if (options.dryRun) {
1359
1836
  // Dry run: just check which projects exist and would be deleted
1360
1837
  const selectProjects = db.prepare(
1361
- `SELECT id FROM project WHERE id IN (${placeholders})`
1838
+ `SELECT ${projectIdColumn} as id FROM project WHERE ${projectIdColumn} IN (${placeholders})`
1362
1839
  )
1363
1840
  const existingRows = selectProjects.all(...projectIds) as { id: string }[]
1364
1841
 
@@ -1393,29 +1870,46 @@ export async function deleteProjectMetadataSqlite(
1393
1870
  }
1394
1871
 
1395
1872
  try {
1873
+ let messageIds: string[] = []
1874
+ if (!partSessionIdColumn && messageIdColumn && sessionIds.length > 0) {
1875
+ const selectMessageIds = db.prepare(
1876
+ `SELECT ${messageIdColumn} as id FROM message WHERE ${messageSessionIdColumn} IN (${sessionPlaceholders})`
1877
+ )
1878
+ const messageRows = selectMessageIds.all(...sessionIds) as { id: string }[]
1879
+ messageIds = messageRows.map(r => r.id)
1880
+ }
1881
+
1396
1882
  // Delete parts first (child of message, also references session_id directly)
1397
1883
  if (sessionIds.length > 0) {
1398
- const deleteParts = db.prepare(
1399
- `DELETE FROM part WHERE session_id IN (${sessionPlaceholders})`
1400
- )
1401
- deleteParts.run(...sessionIds)
1884
+ if (partSessionIdColumn) {
1885
+ const deleteParts = db.prepare(
1886
+ `DELETE FROM part WHERE ${partSessionIdColumn} IN (${sessionPlaceholders})`
1887
+ )
1888
+ deleteParts.run(...sessionIds)
1889
+ } else if (partMessageIdColumn && messageIds.length > 0) {
1890
+ const messagePlaceholders = messageIds.map(() => "?").join(", ")
1891
+ const deleteParts = db.prepare(
1892
+ `DELETE FROM part WHERE ${partMessageIdColumn} IN (${messagePlaceholders})`
1893
+ )
1894
+ deleteParts.run(...messageIds)
1895
+ }
1402
1896
 
1403
1897
  // Delete messages next (child of session)
1404
1898
  const deleteMessages = db.prepare(
1405
- `DELETE FROM message WHERE session_id IN (${sessionPlaceholders})`
1899
+ `DELETE FROM message WHERE ${messageSessionIdColumn} IN (${sessionPlaceholders})`
1406
1900
  )
1407
1901
  deleteMessages.run(...sessionIds)
1408
1902
  }
1409
1903
 
1410
1904
  // Delete sessions (child of project)
1411
1905
  const deleteSessions = db.prepare(
1412
- `DELETE FROM session WHERE project_id IN (${placeholders})`
1906
+ `DELETE FROM session WHERE ${sessionProjectIdColumn} IN (${placeholders})`
1413
1907
  )
1414
1908
  deleteSessions.run(...projectIds)
1415
1909
 
1416
1910
  // Get the list of actually existing projects for accurate reporting
1417
1911
  const selectProjects = db.prepare(
1418
- `SELECT id FROM project WHERE id IN (${placeholders})`
1912
+ `SELECT ${projectIdColumn} as id FROM project WHERE ${projectIdColumn} IN (${placeholders})`
1419
1913
  )
1420
1914
  const existingRows = selectProjects.all(...projectIds) as { id: string }[]
1421
1915
 
@@ -1423,7 +1917,7 @@ export async function deleteProjectMetadataSqlite(
1423
1917
 
1424
1918
  // Finally delete projects
1425
1919
  const deleteProjects = db.prepare(
1426
- `DELETE FROM project WHERE id IN (${placeholders})`
1920
+ `DELETE FROM project WHERE ${projectIdColumn} IN (${placeholders})`
1427
1921
  )
1428
1922
  deleteProjects.run(...projectIds)
1429
1923
 
@@ -1645,7 +2139,7 @@ export async function moveSessionSqlite(
1645
2139
  // Parse existing JSON data
1646
2140
  let data: SessionData
1647
2141
  try {
1648
- data = JSON.parse(row.data) as SessionData
2142
+ data = JSON.parse(row.data ?? "{}") as SessionData
1649
2143
  } catch (error) {
1650
2144
  throw new Error(
1651
2145
  formatSqliteErrorMessage(error, `Failed to parse session data for: ${options.sessionId}`, options)
@@ -1669,13 +2163,13 @@ export async function moveSessionSqlite(
1669
2163
  }
1670
2164
 
1671
2165
  // Build and return the updated session record
1672
- const createdAt = msToDate(row.created_at) ?? msToDate(data.time?.created)
2166
+ const createdAt = msToDate(row.created_at as number | null | undefined) ?? msToDate(data.time?.created)
1673
2167
  const directory = expandUserPath(data.directory)
1674
2168
 
1675
2169
  return {
1676
2170
  index: 1, // Single result, so index is 1
1677
2171
  filePath: `sqlite:session:${row.id}`,
1678
- sessionId: row.id,
2172
+ sessionId: row.id ?? "",
1679
2173
  projectId: options.targetProjectId,
1680
2174
  directory: directory ?? "",
1681
2175
  title: typeof data.title === "string" ? data.title : "",
@@ -1779,7 +2273,7 @@ export async function copySessionSqlite(
1779
2273
  // Parse existing session JSON data
1780
2274
  let sessionData: SessionData
1781
2275
  try {
1782
- sessionData = JSON.parse(sessionRow.data) as SessionData
2276
+ sessionData = JSON.parse(sessionRow.data ?? "{}") as SessionData
1783
2277
  } catch (error) {
1784
2278
  throw new Error(
1785
2279
  formatSqliteErrorMessage(error, `Failed to parse session data for: ${options.sessionId}`, options)
@@ -1825,7 +2319,9 @@ export async function copySessionSqlite(
1825
2319
  // Create ID mapping for messages (old ID -> new ID)
1826
2320
  const messageIdMap = new Map<string, string>()
1827
2321
  for (const msg of messageRows) {
1828
- messageIdMap.set(msg.id, generateId("msg"))
2322
+ if (msg.id) {
2323
+ messageIdMap.set(msg.id, generateId("msg"))
2324
+ }
1829
2325
  }
1830
2326
 
1831
2327
  // Begin transaction for atomicity
@@ -1856,12 +2352,13 @@ export async function copySessionSqlite(
1856
2352
  )
1857
2353
 
1858
2354
  for (const msgRow of messageRows) {
2355
+ if (!msgRow.id) continue
1859
2356
  const newMessageId = messageIdMap.get(msgRow.id)!
1860
2357
 
1861
2358
  // Parse and update message data
1862
2359
  let msgData: MessageData
1863
2360
  try {
1864
- msgData = JSON.parse(msgRow.data) as MessageData
2361
+ msgData = JSON.parse(msgRow.data ?? "{}") as MessageData
1865
2362
  } catch (error) {
1866
2363
  const message = formatSqliteErrorMessage(
1867
2364
  error,
@@ -1884,7 +2381,7 @@ export async function copySessionSqlite(
1884
2381
  insertMessageStmt.run(
1885
2382
  newMessageId,
1886
2383
  newSessionId,
1887
- msgRow.created_at,
2384
+ (msgRow.created_at as number | null | undefined) ?? null,
1888
2385
  JSON.stringify(newMsgData)
1889
2386
  )
1890
2387
  }
@@ -1897,7 +2394,7 @@ export async function copySessionSqlite(
1897
2394
  )
1898
2395
 
1899
2396
  for (const partRow of partRows) {
1900
- const newMessageId = messageIdMap.get(partRow.message_id)
2397
+ const newMessageId = messageIdMap.get(partRow.message_id ?? "")
1901
2398
  if (!newMessageId) {
1902
2399
  // Skip orphaned parts (message was skipped due to malformed data)
1903
2400
  continue
@@ -1908,7 +2405,7 @@ export async function copySessionSqlite(
1908
2405
  // Parse and update part data
1909
2406
  let partData: PartData
1910
2407
  try {
1911
- partData = JSON.parse(partRow.data) as PartData
2408
+ partData = JSON.parse(partRow.data ?? "{}") as PartData
1912
2409
  } catch (error) {
1913
2410
  const message = formatSqliteErrorMessage(
1914
2411
  error,