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.
- package/package.json +1 -1
- package/src/bin/opencode-manager.ts +1 -1
- package/src/cli/commands/tui.ts +8 -1
- package/src/cli/index.ts +1 -1
- package/src/lib/opencode-data-sqlite.ts +646 -149
- package/src/tui/app.tsx +129 -63
- package/src/tui/args.ts +48 -3
- package/src/tui/index.tsx +15 -1
|
@@ -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
|
-
|
|
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(
|
|
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
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
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
|
|
498
|
-
const worktree = expandUserPath(
|
|
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
|
|
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
|
|
548
|
-
parent_id
|
|
549
|
-
created_at
|
|
550
|
-
updated_at
|
|
551
|
-
data
|
|
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
|
-
|
|
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 =
|
|
755
|
+
let query = `SELECT ${selectColumns.join(", ")} FROM session`
|
|
592
756
|
const params: string[] = []
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
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
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
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
|
-
|
|
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
|
-
|
|
640
|
-
const
|
|
641
|
-
const
|
|
642
|
-
const directory = expandUserPath(
|
|
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
|
|
825
|
+
projectId,
|
|
649
826
|
directory: directory ?? "",
|
|
650
|
-
title
|
|
651
|
-
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
|
|
694
|
-
created_at
|
|
695
|
-
data
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
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
|
-
|
|
825
|
-
|
|
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 =
|
|
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"
|
|
834
|
-
|
|
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
|
|
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
|
|
890
|
-
session_id
|
|
891
|
-
data
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
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
|
|
1439
|
+
let sessionColumns: Set<string> | null
|
|
1440
|
+
let messageColumns: Set<string> | null
|
|
1441
|
+
let partColumns: Set<string> | null
|
|
1442
|
+
|
|
1117
1443
|
try {
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
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 =
|
|
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
|
-
|
|
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(
|
|
1461
|
+
throw new Error(message)
|
|
1136
1462
|
}
|
|
1137
|
-
warnSqlite(options,
|
|
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:
|
|
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
|
|
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
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
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 =
|
|
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
|
-
|
|
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(
|
|
1743
|
+
throw new Error(message)
|
|
1339
1744
|
}
|
|
1340
|
-
warnSqlite(options,
|
|
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:
|
|
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
|
|
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
|
|
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
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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,
|