@m4ike1/ion-session-backend-sqlite-node 0.1.0

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.
Files changed (64) hide show
  1. package/CHANGELOG.md +69 -0
  2. package/README.md +62 -0
  3. package/dist/index.d.ts +6 -0
  4. package/dist/index.d.ts.map +1 -0
  5. package/dist/index.js +103 -0
  6. package/dist/index.js.map +1 -0
  7. package/dist/sqlite/index.d.ts +5 -0
  8. package/dist/sqlite/index.d.ts.map +1 -0
  9. package/dist/sqlite/index.js +4 -0
  10. package/dist/sqlite/index.js.map +1 -0
  11. package/dist/sqlite/migrations/001_initial.sql +121 -0
  12. package/dist/sqlite/migrations.d.ts +3 -0
  13. package/dist/sqlite/migrations.d.ts.map +1 -0
  14. package/dist/sqlite/migrations.js +7 -0
  15. package/dist/sqlite/migrations.js.map +1 -0
  16. package/dist/sqlite/repo.d.ts +41 -0
  17. package/dist/sqlite/repo.d.ts.map +1 -0
  18. package/dist/sqlite/repo.js +408 -0
  19. package/dist/sqlite/repo.js.map +1 -0
  20. package/dist/sqlite/session/branch-entries.d.ts +6 -0
  21. package/dist/sqlite/session/branch-entries.d.ts.map +1 -0
  22. package/dist/sqlite/session/branch-entries.js +213 -0
  23. package/dist/sqlite/session/branch-entries.js.map +1 -0
  24. package/dist/sqlite/session/entries.d.ts +24 -0
  25. package/dist/sqlite/session/entries.d.ts.map +1 -0
  26. package/dist/sqlite/session/entries.js +126 -0
  27. package/dist/sqlite/session/entries.js.map +1 -0
  28. package/dist/sqlite/session/session-row.d.ts +23 -0
  29. package/dist/sqlite/session/session-row.d.ts.map +1 -0
  30. package/dist/sqlite/session/session-row.js +69 -0
  31. package/dist/sqlite/session/session-row.js.map +1 -0
  32. package/dist/sqlite/session/session-sequences.d.ts +4 -0
  33. package/dist/sqlite/session/session-sequences.d.ts.map +1 -0
  34. package/dist/sqlite/session/session-sequences.js +13 -0
  35. package/dist/sqlite/session/session-sequences.js.map +1 -0
  36. package/dist/sqlite/session/session-stats.d.ts +6 -0
  37. package/dist/sqlite/session/session-stats.d.ts.map +1 -0
  38. package/dist/sqlite/session/session-stats.js +39 -0
  39. package/dist/sqlite/session/session-stats.js.map +1 -0
  40. package/dist/sqlite/session/usage-ledger.d.ts +20 -0
  41. package/dist/sqlite/session/usage-ledger.d.ts.map +1 -0
  42. package/dist/sqlite/session/usage-ledger.js +50 -0
  43. package/dist/sqlite/session/usage-ledger.js.map +1 -0
  44. package/dist/sqlite/session/values.d.ts +23 -0
  45. package/dist/sqlite/session/values.d.ts.map +1 -0
  46. package/dist/sqlite/session/values.js +91 -0
  47. package/dist/sqlite/session/values.js.map +1 -0
  48. package/dist/sqlite/session.d.ts +42 -0
  49. package/dist/sqlite/session.d.ts.map +1 -0
  50. package/dist/sqlite/session.js +163 -0
  51. package/dist/sqlite/session.js.map +1 -0
  52. package/dist/sqlite/sql.d.ts +19 -0
  53. package/dist/sqlite/sql.d.ts.map +1 -0
  54. package/dist/sqlite/sql.js +58 -0
  55. package/dist/sqlite/sql.js.map +1 -0
  56. package/dist/sqlite/storage.d.ts +38 -0
  57. package/dist/sqlite/storage.d.ts.map +1 -0
  58. package/dist/sqlite/storage.js +161 -0
  59. package/dist/sqlite/storage.js.map +1 -0
  60. package/dist/sqlite/types.d.ts +29 -0
  61. package/dist/sqlite/types.d.ts.map +1 -0
  62. package/dist/sqlite/types.js +2 -0
  63. package/dist/sqlite/types.js.map +1 -0
  64. package/package.json +45 -0
@@ -0,0 +1,213 @@
1
+ import { joinSqlFragments, sql } from "../sql.js";
2
+ import { decodeEntryRow } from "./entries.js";
3
+ function readBranchMembership(db, sessionId, entryId) {
4
+ const row = sql `SELECT b.branch_id, b.entry_seq
5
+ FROM branch_entries b
6
+ JOIN branch_meta m ON m.session_id = b.session_id AND m.branch_id = b.branch_id
7
+ WHERE b.session_id = ${sessionId}
8
+ AND b.entry_id = ${entryId}
9
+ AND ((m.base_seq IS NULL AND b.entry_seq > 0) OR (m.base_seq IS NOT NULL AND b.entry_seq > m.base_seq))
10
+ AND b.entry_seq <= m.tip_seq
11
+ ORDER BY m.tip_seq DESC, b.branch_id
12
+ LIMIT 1`.get(db);
13
+ if (row === undefined)
14
+ throw new Error(`Branch cache missing entry ${entryId}`);
15
+ return row;
16
+ }
17
+ function readBranchMeta(db, sessionId, branchId) {
18
+ const row = sql `SELECT branch_id, tip_entry_id, tip_seq, base_branch_id, base_seq
19
+ FROM branch_meta
20
+ WHERE session_id = ${sessionId} AND branch_id = ${branchId}`.get(db);
21
+ if (row === undefined)
22
+ throw new Error(`Branch metadata missing for branch ${branchId}`);
23
+ return row;
24
+ }
25
+ function insertBranchEntry(db, sessionId, branchId, entry) {
26
+ sql `INSERT INTO branch_entries (session_id, branch_id, entry_id, entry_seq, entry_type)
27
+ VALUES (${sessionId}, ${branchId}, ${entry.id}, ${entry.seq}, ${entry.type})`.run(db);
28
+ }
29
+ function readBranchTipForParent(db, sessionId, parentId) {
30
+ return sql `SELECT branch_id
31
+ FROM branch_meta
32
+ WHERE session_id = ${sessionId} AND tip_entry_id = ${parentId}`.get(db);
33
+ }
34
+ function createRootBranchForEntry(db, sessionId, entry) {
35
+ sql `INSERT INTO branch_meta (session_id, branch_id, tip_entry_id, tip_seq, base_branch_id, base_seq)
36
+ VALUES (${sessionId}, ${entry.id}, ${entry.id}, ${entry.seq}, ${null}, ${null})`.run(db);
37
+ insertBranchEntry(db, sessionId, entry.id, entry);
38
+ }
39
+ function appendEntryToExistingBranch(db, sessionId, branchId, entry) {
40
+ insertBranchEntry(db, sessionId, branchId, entry);
41
+ const result = sql `UPDATE branch_meta
42
+ SET tip_entry_id = ${entry.id}, tip_seq = ${entry.seq}
43
+ WHERE session_id = ${sessionId} AND branch_id = ${branchId}`.run(db);
44
+ if (result.changes !== 1)
45
+ throw new Error(`Expected to update branch ${branchId}, updated ${result.changes}`);
46
+ }
47
+ function readBranchSegmentsNewestFirst(db, sessionId, start) {
48
+ let { branch_id: branchId, entry_seq: upperSeq } = readBranchMembership(db, sessionId, start);
49
+ const segments = [];
50
+ while (true) {
51
+ const meta = readBranchMeta(db, sessionId, branchId);
52
+ const lowerSeq = meta.base_seq ?? 0;
53
+ segments.push({ branchId, lowerSeq, upperSeq });
54
+ if (meta.base_branch_id === null)
55
+ break;
56
+ if (meta.base_seq === null)
57
+ throw new Error(`Branch ${branchId} has base branch without base_seq`);
58
+ branchId = meta.base_branch_id;
59
+ upperSeq = meta.base_seq;
60
+ }
61
+ return segments;
62
+ }
63
+ function readNewestCompactionBoundary(db, sessionId, segmentsNewestFirst) {
64
+ for (const segment of segmentsNewestFirst) {
65
+ const row = sql `SELECT MAX(entry_seq) AS entry_seq
66
+ FROM branch_entries
67
+ WHERE session_id = ${sessionId}
68
+ AND branch_id = ${segment.branchId}
69
+ AND entry_seq > ${segment.lowerSeq}
70
+ AND entry_seq <= ${segment.upperSeq}
71
+ AND entry_type = ${"compaction"}`.get(db);
72
+ if (row?.entry_seq !== null && row?.entry_seq !== undefined)
73
+ return { branchId: segment.branchId, seq: row.entry_seq };
74
+ }
75
+ return undefined;
76
+ }
77
+ function copyBranchEntriesAfterSeqThroughParent(db, sessionId, targetBranchId, segmentsNewestFirst, afterSeq) {
78
+ for (const segment of [...segmentsNewestFirst].reverse()) {
79
+ const lowerSeq = Math.max(segment.lowerSeq, afterSeq);
80
+ if (segment.upperSeq <= lowerSeq)
81
+ continue;
82
+ sql `INSERT INTO branch_entries (session_id, branch_id, entry_id, entry_seq, entry_type)
83
+ SELECT ${sessionId}, ${targetBranchId}, entry_id, entry_seq, entry_type
84
+ FROM branch_entries
85
+ WHERE session_id = ${sessionId}
86
+ AND branch_id = ${segment.branchId}
87
+ AND entry_seq > ${lowerSeq}
88
+ AND entry_seq <= ${segment.upperSeq}`.run(db);
89
+ }
90
+ }
91
+ function createDivergentBranchForEntry(db, sessionId, entry) {
92
+ if (entry.parentId === null)
93
+ throw new Error("Root entries do not create divergent branches");
94
+ const segmentsNewestFirst = readBranchSegmentsNewestFirst(db, sessionId, entry.parentId);
95
+ const compaction = readNewestCompactionBoundary(db, sessionId, segmentsNewestFirst);
96
+ const branchId = entry.id;
97
+ // A null base means this segment stores its own root-through-parent prefix.
98
+ sql `INSERT INTO branch_meta (session_id, branch_id, tip_entry_id, tip_seq, base_branch_id, base_seq)
99
+ VALUES (${sessionId}, ${branchId}, ${entry.id}, ${entry.seq}, ${compaction?.branchId ?? null}, ${compaction?.seq ?? null})`.run(db);
100
+ copyBranchEntriesAfterSeqThroughParent(db, sessionId, branchId, segmentsNewestFirst, compaction?.seq ?? 0);
101
+ insertBranchEntry(db, sessionId, branchId, entry);
102
+ }
103
+ export function appendEntryToBranchIndex(db, sessionId, entry) {
104
+ if (entry.parentId === null) {
105
+ createRootBranchForEntry(db, sessionId, entry);
106
+ return;
107
+ }
108
+ const branch = readBranchTipForParent(db, sessionId, entry.parentId);
109
+ if (branch === undefined) {
110
+ createDivergentBranchForEntry(db, sessionId, entry);
111
+ return;
112
+ }
113
+ appendEntryToExistingBranch(db, sessionId, branch.branch_id, entry);
114
+ }
115
+ function stopPredicates(query) {
116
+ const predicates = [];
117
+ if (query.stopAtType !== undefined)
118
+ predicates.push(sql `b.entry_type = ${query.stopAtType}`);
119
+ if (query.stopAtId !== undefined)
120
+ predicates.push(sql `b.entry_id = ${query.stopAtId}`);
121
+ return predicates;
122
+ }
123
+ function readStopSeq(db, sessionId, segment, query, oldestFirst) {
124
+ const stop = stopPredicates(query);
125
+ if (stop.length === 0)
126
+ return undefined;
127
+ const aggregate = oldestFirst ? sql `MIN(b.entry_seq)` : sql `MAX(b.entry_seq)`;
128
+ const row = sql `SELECT ${aggregate} AS stop_seq
129
+ FROM branch_entries b
130
+ WHERE b.session_id = ${sessionId}
131
+ AND b.branch_id = ${segment.branchId}
132
+ AND b.entry_seq > ${segment.lowerSeq}
133
+ AND b.entry_seq <= ${segment.upperSeq}
134
+ AND (${joinSqlFragments(stop, " OR ")})`.get(db);
135
+ return row?.stop_seq ?? undefined;
136
+ }
137
+ function branchScanPredicates(sessionId, segment, query, oldestFirst, stopSeq) {
138
+ const predicates = [
139
+ sql `b.session_id = ${sessionId}`,
140
+ sql `b.branch_id = ${segment.branchId}`,
141
+ sql `b.entry_seq > ${segment.lowerSeq}`,
142
+ sql `b.entry_seq <= ${segment.upperSeq}`,
143
+ sql `e.session_id = b.session_id`,
144
+ ];
145
+ if (stopSeq !== undefined)
146
+ predicates.push(oldestFirst ? sql `b.entry_seq <= ${stopSeq}` : sql `b.entry_seq >= ${stopSeq}`);
147
+ if (query.type !== undefined)
148
+ predicates.push(sql `b.entry_type = ${query.type}`);
149
+ if (query.customType !== undefined)
150
+ predicates.push(sql `e.custom_type = ${query.customType}`);
151
+ if (query.cursor !== undefined) {
152
+ predicates.push(oldestFirst ? sql `b.entry_seq > ${query.cursor.seq}` : sql `b.entry_seq < ${query.cursor.seq}`);
153
+ }
154
+ return predicates;
155
+ }
156
+ function limitSql(limit) {
157
+ return limit === undefined ? sql `` : sql `LIMIT ${Math.max(0, limit)}`;
158
+ }
159
+ function scanEntrySegmentRows(db, sessionId, segment, query, oldestFirst, stopSeq, limit) {
160
+ const predicates = branchScanPredicates(sessionId, segment, query, oldestFirst, stopSeq);
161
+ const order = oldestFirst ? sql `ASC` : sql `DESC`;
162
+ return sql `SELECT e.id, e.parent_id, e.seq, e.type, e.custom_type, e.timestamp, e.payload
163
+ FROM branch_entries b
164
+ CROSS JOIN entries e ON e.session_id = b.session_id AND e.id = b.entry_id
165
+ WHERE ${joinSqlFragments(predicates, " AND ")}
166
+ ORDER BY b.entry_seq ${order} ${limitSql(limit)}`.all(db);
167
+ }
168
+ function decodeEntryStructureRow(row) {
169
+ return {
170
+ id: row.id,
171
+ parentId: row.parent_id,
172
+ seq: row.seq,
173
+ timestamp: row.timestamp,
174
+ type: row.type,
175
+ ...(row.custom_type === null ? {} : { customType: row.custom_type }),
176
+ };
177
+ }
178
+ function scanStructureSegmentRows(db, sessionId, segment, query, oldestFirst, stopSeq, limit) {
179
+ const predicates = branchScanPredicates(sessionId, segment, query, oldestFirst, stopSeq);
180
+ const order = oldestFirst ? sql `ASC` : sql `DESC`;
181
+ const rows = sql `SELECT e.id, e.parent_id, e.seq, e.type, e.custom_type, e.timestamp
182
+ FROM branch_entries b
183
+ CROSS JOIN entries e ON e.session_id = b.session_id AND e.id = b.entry_id
184
+ WHERE ${joinSqlFragments(predicates, " AND ")}
185
+ ORDER BY b.entry_seq ${order} ${limitSql(limit)}`.all(db);
186
+ return rows.map(decodeEntryStructureRow);
187
+ }
188
+ function scanBranchSegments(db, sessionId, query, readSegment) {
189
+ const oldestFirst = query.order === "oldestFirst";
190
+ const segmentsNewestFirst = readBranchSegmentsNewestFirst(db, sessionId, query.start);
191
+ const segments = oldestFirst ? [...segmentsNewestFirst].reverse() : segmentsNewestFirst;
192
+ const limit = query.limit === undefined ? undefined : Math.max(0, query.limit);
193
+ if (limit === 0)
194
+ return [];
195
+ const rows = [];
196
+ for (const segment of segments) {
197
+ const remaining = limit === undefined ? undefined : limit - rows.length;
198
+ if (remaining !== undefined && remaining <= 0)
199
+ break;
200
+ const stopSeq = readStopSeq(db, sessionId, segment, query, oldestFirst);
201
+ rows.push(...readSegment(db, sessionId, segment, query, oldestFirst, stopSeq, remaining));
202
+ if (stopSeq !== undefined)
203
+ break;
204
+ }
205
+ return rows;
206
+ }
207
+ export function scanBranchEntries(db, sessionId, query) {
208
+ return scanBranchSegments(db, sessionId, query, scanEntrySegmentRows).map(decodeEntryRow);
209
+ }
210
+ export function scanBranchEntryStructures(db, sessionId, query) {
211
+ return scanBranchSegments(db, sessionId, query, scanStructureSegmentRows);
212
+ }
213
+ //# sourceMappingURL=branch-entries.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"branch-entries.js","sourceRoot":"","sources":["../../../src/sqlite/session/branch-entries.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Entry, EntryStructure, StorageBranchScan } from \"@m4ike1/ion-agent-core\";\nimport { joinSqlFragments, type SqlQuery, sql } from \"../sql.ts\";\nimport type { SqliteDatabase } from \"../types.ts\";\nimport { decodeEntryRow, type EntryRow } from \"./entries.ts\";\n\ninterface BranchMembershipRow {\n\tbranch_id: string;\n\tentry_seq: number;\n}\n\ninterface BranchMetaRow {\n\tbranch_id: string;\n\ttip_entry_id: string;\n\ttip_seq: number;\n\tbase_branch_id: string | null;\n\tbase_seq: number | null;\n}\n\ninterface BranchSegment {\n\tbranchId: string;\n\tlowerSeq: number;\n\tupperSeq: number;\n}\n\ninterface StopSeqRow {\n\tstop_seq: number | null;\n}\n\ninterface EntryStructureRow {\n\tid: string;\n\tparent_id: string | null;\n\tseq: number;\n\ttype: Entry[\"type\"];\n\tcustom_type: string | null;\n\ttimestamp: number;\n}\n\ninterface BranchTipRow {\n\tbranch_id: string;\n}\n\ninterface CompactionBoundary {\n\tbranchId: string;\n\tseq: number;\n}\n\ninterface CompactionBoundaryRow {\n\tentry_seq: number | null;\n}\n\nfunction readBranchMembership(db: SqliteDatabase, sessionId: string, entryId: string): BranchMembershipRow {\n\tconst row = sql`SELECT b.branch_id, b.entry_seq\n\t\tFROM branch_entries b\n\t\tJOIN branch_meta m ON m.session_id = b.session_id AND m.branch_id = b.branch_id\n\t\tWHERE b.session_id = ${sessionId}\n\t\t\tAND b.entry_id = ${entryId}\n\t\t\tAND ((m.base_seq IS NULL AND b.entry_seq > 0) OR (m.base_seq IS NOT NULL AND b.entry_seq > m.base_seq))\n\t\t\tAND b.entry_seq <= m.tip_seq\n\t\tORDER BY m.tip_seq DESC, b.branch_id\n\t\tLIMIT 1`.get<BranchMembershipRow>(db);\n\tif (row === undefined) throw new Error(`Branch cache missing entry ${entryId}`);\n\treturn row;\n}\n\nfunction readBranchMeta(db: SqliteDatabase, sessionId: string, branchId: string): BranchMetaRow {\n\tconst row = sql`SELECT branch_id, tip_entry_id, tip_seq, base_branch_id, base_seq\n\t\tFROM branch_meta\n\t\tWHERE session_id = ${sessionId} AND branch_id = ${branchId}`.get<BranchMetaRow>(db);\n\tif (row === undefined) throw new Error(`Branch metadata missing for branch ${branchId}`);\n\treturn row;\n}\n\nfunction insertBranchEntry(db: SqliteDatabase, sessionId: string, branchId: string, entry: Entry): void {\n\tsql`INSERT INTO branch_entries (session_id, branch_id, entry_id, entry_seq, entry_type)\n\t\tVALUES (${sessionId}, ${branchId}, ${entry.id}, ${entry.seq}, ${entry.type})`.run(db);\n}\n\nfunction readBranchTipForParent(db: SqliteDatabase, sessionId: string, parentId: string): BranchTipRow | undefined {\n\treturn sql`SELECT branch_id\n\t\tFROM branch_meta\n\t\tWHERE session_id = ${sessionId} AND tip_entry_id = ${parentId}`.get<BranchTipRow>(db);\n}\n\nfunction createRootBranchForEntry(db: SqliteDatabase, sessionId: string, entry: Entry): void {\n\tsql`INSERT INTO branch_meta (session_id, branch_id, tip_entry_id, tip_seq, base_branch_id, base_seq)\n\t\tVALUES (${sessionId}, ${entry.id}, ${entry.id}, ${entry.seq}, ${null}, ${null})`.run(db);\n\tinsertBranchEntry(db, sessionId, entry.id, entry);\n}\n\nfunction appendEntryToExistingBranch(db: SqliteDatabase, sessionId: string, branchId: string, entry: Entry): void {\n\tinsertBranchEntry(db, sessionId, branchId, entry);\n\tconst result = sql`UPDATE branch_meta\n\t\tSET tip_entry_id = ${entry.id}, tip_seq = ${entry.seq}\n\t\tWHERE session_id = ${sessionId} AND branch_id = ${branchId}`.run(db);\n\tif (result.changes !== 1) throw new Error(`Expected to update branch ${branchId}, updated ${result.changes}`);\n}\n\nfunction readBranchSegmentsNewestFirst(db: SqliteDatabase, sessionId: string, start: string): BranchSegment[] {\n\tlet { branch_id: branchId, entry_seq: upperSeq } = readBranchMembership(db, sessionId, start);\n\tconst segments: BranchSegment[] = [];\n\twhile (true) {\n\t\tconst meta = readBranchMeta(db, sessionId, branchId);\n\t\tconst lowerSeq = meta.base_seq ?? 0;\n\t\tsegments.push({ branchId, lowerSeq, upperSeq });\n\t\tif (meta.base_branch_id === null) break;\n\t\tif (meta.base_seq === null) throw new Error(`Branch ${branchId} has base branch without base_seq`);\n\t\tbranchId = meta.base_branch_id;\n\t\tupperSeq = meta.base_seq;\n\t}\n\treturn segments;\n}\n\nfunction readNewestCompactionBoundary(\n\tdb: SqliteDatabase,\n\tsessionId: string,\n\tsegmentsNewestFirst: readonly BranchSegment[],\n): CompactionBoundary | undefined {\n\tfor (const segment of segmentsNewestFirst) {\n\t\tconst row = sql`SELECT MAX(entry_seq) AS entry_seq\n\t\t\tFROM branch_entries\n\t\t\tWHERE session_id = ${sessionId}\n\t\t\t\tAND branch_id = ${segment.branchId}\n\t\t\t\tAND entry_seq > ${segment.lowerSeq}\n\t\t\t\tAND entry_seq <= ${segment.upperSeq}\n\t\t\t\tAND entry_type = ${\"compaction\"}`.get<CompactionBoundaryRow>(db);\n\t\tif (row?.entry_seq !== null && row?.entry_seq !== undefined)\n\t\t\treturn { branchId: segment.branchId, seq: row.entry_seq };\n\t}\n\treturn undefined;\n}\n\nfunction copyBranchEntriesAfterSeqThroughParent(\n\tdb: SqliteDatabase,\n\tsessionId: string,\n\ttargetBranchId: string,\n\tsegmentsNewestFirst: readonly BranchSegment[],\n\tafterSeq: number,\n): void {\n\tfor (const segment of [...segmentsNewestFirst].reverse()) {\n\t\tconst lowerSeq = Math.max(segment.lowerSeq, afterSeq);\n\t\tif (segment.upperSeq <= lowerSeq) continue;\n\t\tsql`INSERT INTO branch_entries (session_id, branch_id, entry_id, entry_seq, entry_type)\n\t\t\tSELECT ${sessionId}, ${targetBranchId}, entry_id, entry_seq, entry_type\n\t\t\tFROM branch_entries\n\t\t\tWHERE session_id = ${sessionId}\n\t\t\t\tAND branch_id = ${segment.branchId}\n\t\t\t\tAND entry_seq > ${lowerSeq}\n\t\t\t\tAND entry_seq <= ${segment.upperSeq}`.run(db);\n\t}\n}\n\nfunction createDivergentBranchForEntry(db: SqliteDatabase, sessionId: string, entry: Entry): void {\n\tif (entry.parentId === null) throw new Error(\"Root entries do not create divergent branches\");\n\tconst segmentsNewestFirst = readBranchSegmentsNewestFirst(db, sessionId, entry.parentId);\n\tconst compaction = readNewestCompactionBoundary(db, sessionId, segmentsNewestFirst);\n\tconst branchId = entry.id;\n\t// A null base means this segment stores its own root-through-parent prefix.\n\tsql`INSERT INTO branch_meta (session_id, branch_id, tip_entry_id, tip_seq, base_branch_id, base_seq)\n\t\tVALUES (${sessionId}, ${branchId}, ${entry.id}, ${entry.seq}, ${compaction?.branchId ?? null}, ${compaction?.seq ?? null})`.run(\n\t\tdb,\n\t);\n\tcopyBranchEntriesAfterSeqThroughParent(db, sessionId, branchId, segmentsNewestFirst, compaction?.seq ?? 0);\n\tinsertBranchEntry(db, sessionId, branchId, entry);\n}\n\nexport function appendEntryToBranchIndex(db: SqliteDatabase, sessionId: string, entry: Entry): void {\n\tif (entry.parentId === null) {\n\t\tcreateRootBranchForEntry(db, sessionId, entry);\n\t\treturn;\n\t}\n\n\tconst branch = readBranchTipForParent(db, sessionId, entry.parentId);\n\tif (branch === undefined) {\n\t\tcreateDivergentBranchForEntry(db, sessionId, entry);\n\t\treturn;\n\t}\n\tappendEntryToExistingBranch(db, sessionId, branch.branch_id, entry);\n}\n\nfunction stopPredicates(query: StorageBranchScan): SqlQuery[] {\n\tconst predicates: SqlQuery[] = [];\n\tif (query.stopAtType !== undefined) predicates.push(sql`b.entry_type = ${query.stopAtType}`);\n\tif (query.stopAtId !== undefined) predicates.push(sql`b.entry_id = ${query.stopAtId}`);\n\treturn predicates;\n}\n\nfunction readStopSeq(\n\tdb: SqliteDatabase,\n\tsessionId: string,\n\tsegment: BranchSegment,\n\tquery: StorageBranchScan,\n\toldestFirst: boolean,\n): number | undefined {\n\tconst stop = stopPredicates(query);\n\tif (stop.length === 0) return undefined;\n\tconst aggregate = oldestFirst ? sql`MIN(b.entry_seq)` : sql`MAX(b.entry_seq)`;\n\tconst row = sql`SELECT ${aggregate} AS stop_seq\n\t\tFROM branch_entries b\n\t\tWHERE b.session_id = ${sessionId}\n\t\t\tAND b.branch_id = ${segment.branchId}\n\t\t\tAND b.entry_seq > ${segment.lowerSeq}\n\t\t\tAND b.entry_seq <= ${segment.upperSeq}\n\t\t\tAND (${joinSqlFragments(stop, \" OR \")})`.get<StopSeqRow>(db);\n\treturn row?.stop_seq ?? undefined;\n}\n\nfunction branchScanPredicates(\n\tsessionId: string,\n\tsegment: BranchSegment,\n\tquery: StorageBranchScan,\n\toldestFirst: boolean,\n\tstopSeq: number | undefined,\n): SqlQuery[] {\n\tconst predicates: SqlQuery[] = [\n\t\tsql`b.session_id = ${sessionId}`,\n\t\tsql`b.branch_id = ${segment.branchId}`,\n\t\tsql`b.entry_seq > ${segment.lowerSeq}`,\n\t\tsql`b.entry_seq <= ${segment.upperSeq}`,\n\t\tsql`e.session_id = b.session_id`,\n\t];\n\tif (stopSeq !== undefined)\n\t\tpredicates.push(oldestFirst ? sql`b.entry_seq <= ${stopSeq}` : sql`b.entry_seq >= ${stopSeq}`);\n\tif (query.type !== undefined) predicates.push(sql`b.entry_type = ${query.type}`);\n\tif (query.customType !== undefined) predicates.push(sql`e.custom_type = ${query.customType}`);\n\tif (query.cursor !== undefined) {\n\t\tpredicates.push(oldestFirst ? sql`b.entry_seq > ${query.cursor.seq}` : sql`b.entry_seq < ${query.cursor.seq}`);\n\t}\n\treturn predicates;\n}\n\nfunction limitSql(limit: number | undefined): SqlQuery {\n\treturn limit === undefined ? sql`` : sql`LIMIT ${Math.max(0, limit)}`;\n}\n\nfunction scanEntrySegmentRows(\n\tdb: SqliteDatabase,\n\tsessionId: string,\n\tsegment: BranchSegment,\n\tquery: StorageBranchScan,\n\toldestFirst: boolean,\n\tstopSeq: number | undefined,\n\tlimit: number | undefined,\n): EntryRow[] {\n\tconst predicates = branchScanPredicates(sessionId, segment, query, oldestFirst, stopSeq);\n\tconst order = oldestFirst ? sql`ASC` : sql`DESC`;\n\treturn sql`SELECT e.id, e.parent_id, e.seq, e.type, e.custom_type, e.timestamp, e.payload\n\t\tFROM branch_entries b\n\t\tCROSS JOIN entries e ON e.session_id = b.session_id AND e.id = b.entry_id\n\t\tWHERE ${joinSqlFragments(predicates, \" AND \")}\n\t\tORDER BY b.entry_seq ${order} ${limitSql(limit)}`.all<EntryRow>(db);\n}\n\nfunction decodeEntryStructureRow(row: EntryStructureRow): EntryStructure {\n\treturn {\n\t\tid: row.id,\n\t\tparentId: row.parent_id,\n\t\tseq: row.seq,\n\t\ttimestamp: row.timestamp,\n\t\ttype: row.type,\n\t\t...(row.custom_type === null ? {} : { customType: row.custom_type }),\n\t};\n}\n\nfunction scanStructureSegmentRows(\n\tdb: SqliteDatabase,\n\tsessionId: string,\n\tsegment: BranchSegment,\n\tquery: StorageBranchScan,\n\toldestFirst: boolean,\n\tstopSeq: number | undefined,\n\tlimit: number | undefined,\n): EntryStructure[] {\n\tconst predicates = branchScanPredicates(sessionId, segment, query, oldestFirst, stopSeq);\n\tconst order = oldestFirst ? sql`ASC` : sql`DESC`;\n\tconst rows = sql`SELECT e.id, e.parent_id, e.seq, e.type, e.custom_type, e.timestamp\n\t\tFROM branch_entries b\n\t\tCROSS JOIN entries e ON e.session_id = b.session_id AND e.id = b.entry_id\n\t\tWHERE ${joinSqlFragments(predicates, \" AND \")}\n\t\tORDER BY b.entry_seq ${order} ${limitSql(limit)}`.all<EntryStructureRow>(db);\n\treturn rows.map(decodeEntryStructureRow);\n}\n\nfunction scanBranchSegments<T>(\n\tdb: SqliteDatabase,\n\tsessionId: string,\n\tquery: StorageBranchScan,\n\treadSegment: (\n\t\tdb: SqliteDatabase,\n\t\tsessionId: string,\n\t\tsegment: BranchSegment,\n\t\tquery: StorageBranchScan,\n\t\toldestFirst: boolean,\n\t\tstopSeq: number | undefined,\n\t\tlimit: number | undefined,\n\t) => T[],\n): T[] {\n\tconst oldestFirst = query.order === \"oldestFirst\";\n\tconst segmentsNewestFirst = readBranchSegmentsNewestFirst(db, sessionId, query.start);\n\tconst segments = oldestFirst ? [...segmentsNewestFirst].reverse() : segmentsNewestFirst;\n\tconst limit = query.limit === undefined ? undefined : Math.max(0, query.limit);\n\tif (limit === 0) return [];\n\n\tconst rows: T[] = [];\n\tfor (const segment of segments) {\n\t\tconst remaining = limit === undefined ? undefined : limit - rows.length;\n\t\tif (remaining !== undefined && remaining <= 0) break;\n\t\tconst stopSeq = readStopSeq(db, sessionId, segment, query, oldestFirst);\n\t\trows.push(...readSegment(db, sessionId, segment, query, oldestFirst, stopSeq, remaining));\n\t\tif (stopSeq !== undefined) break;\n\t}\n\treturn rows;\n}\n\nexport function scanBranchEntries(db: SqliteDatabase, sessionId: string, query: StorageBranchScan): Entry[] {\n\treturn scanBranchSegments(db, sessionId, query, scanEntrySegmentRows).map(decodeEntryRow);\n}\n\nexport function scanBranchEntryStructures(\n\tdb: SqliteDatabase,\n\tsessionId: string,\n\tquery: StorageBranchScan,\n): EntryStructure[] {\n\treturn scanBranchSegments(db, sessionId, query, scanStructureSegmentRows);\n}\n"]}
@@ -0,0 +1,24 @@
1
+ import type { Entry, EntryScan, EntryStructure } from "@m4ike1/ion-agent-core";
2
+ import type { SqliteDatabase } from "../types.ts";
3
+ export interface EntryRow {
4
+ id: string;
5
+ parent_id: string | null;
6
+ seq: number;
7
+ type: Entry["type"];
8
+ custom_type: string | null;
9
+ timestamp: number;
10
+ payload: string;
11
+ }
12
+ export declare class EntryRowWriter {
13
+ private readonly insertStatement;
14
+ private readonly sessionId;
15
+ constructor(db: SqliteDatabase, sessionId: string);
16
+ insert(entry: Entry): void;
17
+ }
18
+ export declare function insertEntryRow(db: SqliteDatabase, sessionId: string, entry: Entry): void;
19
+ export declare function decodeEntryRow(row: EntryRow): Entry;
20
+ export declare function entryStructureFromRow(row: EntryRow): EntryStructure;
21
+ export declare function readEntryRows(db: SqliteDatabase, sessionId: string, ids: readonly string[]): EntryRow[];
22
+ export declare function readAllEntryRows(db: SqliteDatabase, sessionId: string): EntryRow[];
23
+ export declare function scanEntryRows(db: SqliteDatabase, sessionId: string, query: EntryScan): EntryRow[];
24
+ //# sourceMappingURL=entries.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"entries.d.ts","sourceRoot":"","sources":["../../../src/sqlite/session/entries.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAIX,KAAK,EACL,SAAS,EACT,cAAc,EAEd,MAAM,wBAAwB,CAAC;AAEhC,OAAO,KAAK,EAAE,cAAc,EAAmB,MAAM,aAAa,CAAC;AAEnE,MAAM,WAAW,QAAQ;IACxB,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IACpB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;CAChB;AAgED,qBAAa,cAAc;IAC1B,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAkB;IAClD,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IAEnC,YAAY,EAAE,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,EAGhD;IAED,MAAM,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI,CAEzB;CACD;AAED,wBAAgB,cAAc,CAAC,EAAE,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,GAAG,IAAI,CAExF;AAED,wBAAgB,cAAc,CAAC,GAAG,EAAE,QAAQ,GAAG,KAAK,CAkBnD;AAED,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,QAAQ,GAAG,cAAc,CASnE;AAED,wBAAgB,aAAa,CAAC,EAAE,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,EAAE,GAAG,EAAE,SAAS,MAAM,EAAE,GAAG,QAAQ,EAAE,CASvG;AAED,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,GAAG,QAAQ,EAAE,CAGlF;AAED,wBAAgB,aAAa,CAAC,EAAE,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,GAAG,QAAQ,EAAE,CAWjG","sourcesContent":["import type {\n\tBranchSummaryEntry,\n\tCompactionEntry,\n\tCustomEntry,\n\tEntry,\n\tEntryScan,\n\tEntryStructure,\n\tMessageEntry,\n} from \"@m4ike1/ion-agent-core\";\nimport { joinSqlFragments, type SqlQuery, sql } from \"../sql.ts\";\nimport type { SqliteDatabase, SqliteStatement } from \"../types.ts\";\n\nexport interface EntryRow {\n\tid: string;\n\tparent_id: string | null;\n\tseq: number;\n\ttype: Entry[\"type\"];\n\tcustom_type: string | null;\n\ttimestamp: number;\n\tpayload: string;\n}\n\ntype StoredEntryPayload<TEntry extends Entry> = Omit<\n\tTEntry,\n\t\"id\" | \"parentId\" | \"seq\" | \"timestamp\" | \"type\" | \"customType\"\n>;\n\nfunction entryPayload(entry: Entry): StoredEntryPayload<Entry> {\n\tswitch (entry.type) {\n\t\tcase \"message\": {\n\t\t\tconst payload: StoredEntryPayload<MessageEntry> = {\n\t\t\t\tmessage: entry.message,\n\t\t\t\t...(entry.terminate === undefined ? {} : { terminate: entry.terminate }),\n\t\t\t};\n\t\t\treturn payload;\n\t\t}\n\t\tcase \"compaction\": {\n\t\t\tconst payload: StoredEntryPayload<CompactionEntry> = {\n\t\t\t\tsummary: entry.summary,\n\t\t\t\tretainedTail: entry.retainedTail,\n\t\t\t\ttokensBefore: entry.tokensBefore,\n\t\t\t\t...(entry.details === undefined ? {} : { details: entry.details }),\n\t\t\t\t...(entry.usage === undefined ? {} : { usage: entry.usage }),\n\t\t\t\tfromHook: entry.fromHook,\n\t\t\t};\n\t\t\treturn payload;\n\t\t}\n\t\tcase \"branch_summary\": {\n\t\t\tconst payload: StoredEntryPayload<BranchSummaryEntry> = {\n\t\t\t\tfromId: entry.fromId,\n\t\t\t\tsummary: entry.summary,\n\t\t\t\t...(entry.details === undefined ? {} : { details: entry.details }),\n\t\t\t\t...(entry.usage === undefined ? {} : { usage: entry.usage }),\n\t\t\t\tfromHook: entry.fromHook,\n\t\t\t};\n\t\t\treturn payload;\n\t\t}\n\t\tcase \"custom\": {\n\t\t\tconst payload: StoredEntryPayload<CustomEntry> = entry.data === undefined ? {} : { data: entry.data };\n\t\t\treturn payload;\n\t\t}\n\t}\n}\n\nfunction parsePayload<TEntry extends Entry>(row: EntryRow): StoredEntryPayload<TEntry> {\n\treturn JSON.parse(row.payload) as StoredEntryPayload<TEntry>;\n}\n\nconst INSERT_ENTRY_SQL = `INSERT INTO entries (session_id, id, parent_id, seq, type, custom_type, timestamp, payload)\n\tVALUES (?, ?, ?, ?, ?, ?, ?, ?)`;\n\nfunction entryRowParams(sessionId: string, entry: Entry): unknown[] {\n\treturn [\n\t\tsessionId,\n\t\tentry.id,\n\t\tentry.parentId,\n\t\tentry.seq,\n\t\tentry.type,\n\t\tentry.type === \"custom\" ? entry.customType : null,\n\t\tentry.timestamp,\n\t\tJSON.stringify(entryPayload(entry)),\n\t];\n}\n\nexport class EntryRowWriter {\n\tprivate readonly insertStatement: SqliteStatement;\n\tprivate readonly sessionId: string;\n\n\tconstructor(db: SqliteDatabase, sessionId: string) {\n\t\tthis.insertStatement = db.prepare(INSERT_ENTRY_SQL);\n\t\tthis.sessionId = sessionId;\n\t}\n\n\tinsert(entry: Entry): void {\n\t\tthis.insertStatement.run(...entryRowParams(this.sessionId, entry));\n\t}\n}\n\nexport function insertEntryRow(db: SqliteDatabase, sessionId: string, entry: Entry): void {\n\tdb.prepare(INSERT_ENTRY_SQL).run(...entryRowParams(sessionId, entry));\n}\n\nexport function decodeEntryRow(row: EntryRow): Entry {\n\tconst base = {\n\t\tid: row.id,\n\t\tparentId: row.parent_id,\n\t\tseq: row.seq,\n\t\ttimestamp: row.timestamp,\n\t};\n\tswitch (row.type) {\n\t\tcase \"message\":\n\t\t\treturn { ...base, type: \"message\", ...parsePayload<MessageEntry>(row) };\n\t\tcase \"compaction\":\n\t\t\treturn { ...base, type: \"compaction\", ...parsePayload<CompactionEntry>(row) };\n\t\tcase \"branch_summary\":\n\t\t\treturn { ...base, type: \"branch_summary\", ...parsePayload<BranchSummaryEntry>(row) };\n\t\tcase \"custom\":\n\t\t\tif (row.custom_type === null) throw new Error(`Custom entry ${row.id} is missing custom_type`);\n\t\t\treturn { ...base, type: \"custom\", customType: row.custom_type, ...parsePayload<CustomEntry>(row) };\n\t}\n}\n\nexport function entryStructureFromRow(row: EntryRow): EntryStructure {\n\treturn {\n\t\tid: row.id,\n\t\tparentId: row.parent_id,\n\t\tseq: row.seq,\n\t\ttimestamp: row.timestamp,\n\t\ttype: row.type,\n\t\t...(row.custom_type === null ? {} : { customType: row.custom_type }),\n\t};\n}\n\nexport function readEntryRows(db: SqliteDatabase, sessionId: string, ids: readonly string[]): EntryRow[] {\n\tif (ids.length === 0) return [];\n\tconst placeholders = joinSqlFragments(\n\t\tids.map((id) => sql`${id}`),\n\t\t\", \",\n\t);\n\treturn sql`SELECT id, parent_id, seq, type, custom_type, timestamp, payload\n\t\tFROM entries\n\t\tWHERE session_id = ${sessionId} AND id IN (${placeholders})`.all<EntryRow>(db);\n}\n\nexport function readAllEntryRows(db: SqliteDatabase, sessionId: string): EntryRow[] {\n\treturn sql`SELECT id, parent_id, seq, type, custom_type, timestamp, payload\n\t\tFROM entries WHERE session_id = ${sessionId} ORDER BY seq ASC`.all<EntryRow>(db);\n}\n\nexport function scanEntryRows(db: SqliteDatabase, sessionId: string, query: EntryScan): EntryRow[] {\n\tconst filters: SqlQuery[] = [sql`session_id = ${sessionId}`];\n\tif (query.type !== undefined) filters.push(sql`type = ${query.type}`);\n\tif (query.customType !== undefined) filters.push(sql`custom_type = ${query.customType}`);\n\tif (query.fromSeq !== undefined) filters.push(sql`seq >= ${query.fromSeq}`);\n\tif (query.toSeq !== undefined) filters.push(sql`seq <= ${query.toSeq}`);\n\n\tconst order = query.order === \"desc\" ? sql`ORDER BY seq DESC` : sql`ORDER BY seq ASC`;\n\tconst limit = query.limit === undefined ? sql`` : sql`LIMIT ${Math.max(0, query.limit)}`;\n\treturn sql`SELECT id, parent_id, seq, type, custom_type, timestamp, payload\n\t\tFROM entries WHERE ${joinSqlFragments(filters, \" AND \")} ${order} ${limit}`.all<EntryRow>(db);\n}\n"]}
@@ -0,0 +1,126 @@
1
+ import { joinSqlFragments, sql } from "../sql.js";
2
+ function entryPayload(entry) {
3
+ switch (entry.type) {
4
+ case "message": {
5
+ const payload = {
6
+ message: entry.message,
7
+ ...(entry.terminate === undefined ? {} : { terminate: entry.terminate }),
8
+ };
9
+ return payload;
10
+ }
11
+ case "compaction": {
12
+ const payload = {
13
+ summary: entry.summary,
14
+ retainedTail: entry.retainedTail,
15
+ tokensBefore: entry.tokensBefore,
16
+ ...(entry.details === undefined ? {} : { details: entry.details }),
17
+ ...(entry.usage === undefined ? {} : { usage: entry.usage }),
18
+ fromHook: entry.fromHook,
19
+ };
20
+ return payload;
21
+ }
22
+ case "branch_summary": {
23
+ const payload = {
24
+ fromId: entry.fromId,
25
+ summary: entry.summary,
26
+ ...(entry.details === undefined ? {} : { details: entry.details }),
27
+ ...(entry.usage === undefined ? {} : { usage: entry.usage }),
28
+ fromHook: entry.fromHook,
29
+ };
30
+ return payload;
31
+ }
32
+ case "custom": {
33
+ const payload = entry.data === undefined ? {} : { data: entry.data };
34
+ return payload;
35
+ }
36
+ }
37
+ }
38
+ function parsePayload(row) {
39
+ return JSON.parse(row.payload);
40
+ }
41
+ const INSERT_ENTRY_SQL = `INSERT INTO entries (session_id, id, parent_id, seq, type, custom_type, timestamp, payload)
42
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`;
43
+ function entryRowParams(sessionId, entry) {
44
+ return [
45
+ sessionId,
46
+ entry.id,
47
+ entry.parentId,
48
+ entry.seq,
49
+ entry.type,
50
+ entry.type === "custom" ? entry.customType : null,
51
+ entry.timestamp,
52
+ JSON.stringify(entryPayload(entry)),
53
+ ];
54
+ }
55
+ export class EntryRowWriter {
56
+ insertStatement;
57
+ sessionId;
58
+ constructor(db, sessionId) {
59
+ this.insertStatement = db.prepare(INSERT_ENTRY_SQL);
60
+ this.sessionId = sessionId;
61
+ }
62
+ insert(entry) {
63
+ this.insertStatement.run(...entryRowParams(this.sessionId, entry));
64
+ }
65
+ }
66
+ export function insertEntryRow(db, sessionId, entry) {
67
+ db.prepare(INSERT_ENTRY_SQL).run(...entryRowParams(sessionId, entry));
68
+ }
69
+ export function decodeEntryRow(row) {
70
+ const base = {
71
+ id: row.id,
72
+ parentId: row.parent_id,
73
+ seq: row.seq,
74
+ timestamp: row.timestamp,
75
+ };
76
+ switch (row.type) {
77
+ case "message":
78
+ return { ...base, type: "message", ...parsePayload(row) };
79
+ case "compaction":
80
+ return { ...base, type: "compaction", ...parsePayload(row) };
81
+ case "branch_summary":
82
+ return { ...base, type: "branch_summary", ...parsePayload(row) };
83
+ case "custom":
84
+ if (row.custom_type === null)
85
+ throw new Error(`Custom entry ${row.id} is missing custom_type`);
86
+ return { ...base, type: "custom", customType: row.custom_type, ...parsePayload(row) };
87
+ }
88
+ }
89
+ export function entryStructureFromRow(row) {
90
+ return {
91
+ id: row.id,
92
+ parentId: row.parent_id,
93
+ seq: row.seq,
94
+ timestamp: row.timestamp,
95
+ type: row.type,
96
+ ...(row.custom_type === null ? {} : { customType: row.custom_type }),
97
+ };
98
+ }
99
+ export function readEntryRows(db, sessionId, ids) {
100
+ if (ids.length === 0)
101
+ return [];
102
+ const placeholders = joinSqlFragments(ids.map((id) => sql `${id}`), ", ");
103
+ return sql `SELECT id, parent_id, seq, type, custom_type, timestamp, payload
104
+ FROM entries
105
+ WHERE session_id = ${sessionId} AND id IN (${placeholders})`.all(db);
106
+ }
107
+ export function readAllEntryRows(db, sessionId) {
108
+ return sql `SELECT id, parent_id, seq, type, custom_type, timestamp, payload
109
+ FROM entries WHERE session_id = ${sessionId} ORDER BY seq ASC`.all(db);
110
+ }
111
+ export function scanEntryRows(db, sessionId, query) {
112
+ const filters = [sql `session_id = ${sessionId}`];
113
+ if (query.type !== undefined)
114
+ filters.push(sql `type = ${query.type}`);
115
+ if (query.customType !== undefined)
116
+ filters.push(sql `custom_type = ${query.customType}`);
117
+ if (query.fromSeq !== undefined)
118
+ filters.push(sql `seq >= ${query.fromSeq}`);
119
+ if (query.toSeq !== undefined)
120
+ filters.push(sql `seq <= ${query.toSeq}`);
121
+ const order = query.order === "desc" ? sql `ORDER BY seq DESC` : sql `ORDER BY seq ASC`;
122
+ const limit = query.limit === undefined ? sql `` : sql `LIMIT ${Math.max(0, query.limit)}`;
123
+ return sql `SELECT id, parent_id, seq, type, custom_type, timestamp, payload
124
+ FROM entries WHERE ${joinSqlFragments(filters, " AND ")} ${order} ${limit}`.all(db);
125
+ }
126
+ //# sourceMappingURL=entries.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"entries.js","sourceRoot":"","sources":["../../../src/sqlite/session/entries.ts"],"names":[],"mappings":"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","sourcesContent":["import type {\n\tBranchSummaryEntry,\n\tCompactionEntry,\n\tCustomEntry,\n\tEntry,\n\tEntryScan,\n\tEntryStructure,\n\tMessageEntry,\n} from \"@m4ike1/ion-agent-core\";\nimport { joinSqlFragments, type SqlQuery, sql } from \"../sql.ts\";\nimport type { SqliteDatabase, SqliteStatement } from \"../types.ts\";\n\nexport interface EntryRow {\n\tid: string;\n\tparent_id: string | null;\n\tseq: number;\n\ttype: Entry[\"type\"];\n\tcustom_type: string | null;\n\ttimestamp: number;\n\tpayload: string;\n}\n\ntype StoredEntryPayload<TEntry extends Entry> = Omit<\n\tTEntry,\n\t\"id\" | \"parentId\" | \"seq\" | \"timestamp\" | \"type\" | \"customType\"\n>;\n\nfunction entryPayload(entry: Entry): StoredEntryPayload<Entry> {\n\tswitch (entry.type) {\n\t\tcase \"message\": {\n\t\t\tconst payload: StoredEntryPayload<MessageEntry> = {\n\t\t\t\tmessage: entry.message,\n\t\t\t\t...(entry.terminate === undefined ? {} : { terminate: entry.terminate }),\n\t\t\t};\n\t\t\treturn payload;\n\t\t}\n\t\tcase \"compaction\": {\n\t\t\tconst payload: StoredEntryPayload<CompactionEntry> = {\n\t\t\t\tsummary: entry.summary,\n\t\t\t\tretainedTail: entry.retainedTail,\n\t\t\t\ttokensBefore: entry.tokensBefore,\n\t\t\t\t...(entry.details === undefined ? {} : { details: entry.details }),\n\t\t\t\t...(entry.usage === undefined ? {} : { usage: entry.usage }),\n\t\t\t\tfromHook: entry.fromHook,\n\t\t\t};\n\t\t\treturn payload;\n\t\t}\n\t\tcase \"branch_summary\": {\n\t\t\tconst payload: StoredEntryPayload<BranchSummaryEntry> = {\n\t\t\t\tfromId: entry.fromId,\n\t\t\t\tsummary: entry.summary,\n\t\t\t\t...(entry.details === undefined ? {} : { details: entry.details }),\n\t\t\t\t...(entry.usage === undefined ? {} : { usage: entry.usage }),\n\t\t\t\tfromHook: entry.fromHook,\n\t\t\t};\n\t\t\treturn payload;\n\t\t}\n\t\tcase \"custom\": {\n\t\t\tconst payload: StoredEntryPayload<CustomEntry> = entry.data === undefined ? {} : { data: entry.data };\n\t\t\treturn payload;\n\t\t}\n\t}\n}\n\nfunction parsePayload<TEntry extends Entry>(row: EntryRow): StoredEntryPayload<TEntry> {\n\treturn JSON.parse(row.payload) as StoredEntryPayload<TEntry>;\n}\n\nconst INSERT_ENTRY_SQL = `INSERT INTO entries (session_id, id, parent_id, seq, type, custom_type, timestamp, payload)\n\tVALUES (?, ?, ?, ?, ?, ?, ?, ?)`;\n\nfunction entryRowParams(sessionId: string, entry: Entry): unknown[] {\n\treturn [\n\t\tsessionId,\n\t\tentry.id,\n\t\tentry.parentId,\n\t\tentry.seq,\n\t\tentry.type,\n\t\tentry.type === \"custom\" ? entry.customType : null,\n\t\tentry.timestamp,\n\t\tJSON.stringify(entryPayload(entry)),\n\t];\n}\n\nexport class EntryRowWriter {\n\tprivate readonly insertStatement: SqliteStatement;\n\tprivate readonly sessionId: string;\n\n\tconstructor(db: SqliteDatabase, sessionId: string) {\n\t\tthis.insertStatement = db.prepare(INSERT_ENTRY_SQL);\n\t\tthis.sessionId = sessionId;\n\t}\n\n\tinsert(entry: Entry): void {\n\t\tthis.insertStatement.run(...entryRowParams(this.sessionId, entry));\n\t}\n}\n\nexport function insertEntryRow(db: SqliteDatabase, sessionId: string, entry: Entry): void {\n\tdb.prepare(INSERT_ENTRY_SQL).run(...entryRowParams(sessionId, entry));\n}\n\nexport function decodeEntryRow(row: EntryRow): Entry {\n\tconst base = {\n\t\tid: row.id,\n\t\tparentId: row.parent_id,\n\t\tseq: row.seq,\n\t\ttimestamp: row.timestamp,\n\t};\n\tswitch (row.type) {\n\t\tcase \"message\":\n\t\t\treturn { ...base, type: \"message\", ...parsePayload<MessageEntry>(row) };\n\t\tcase \"compaction\":\n\t\t\treturn { ...base, type: \"compaction\", ...parsePayload<CompactionEntry>(row) };\n\t\tcase \"branch_summary\":\n\t\t\treturn { ...base, type: \"branch_summary\", ...parsePayload<BranchSummaryEntry>(row) };\n\t\tcase \"custom\":\n\t\t\tif (row.custom_type === null) throw new Error(`Custom entry ${row.id} is missing custom_type`);\n\t\t\treturn { ...base, type: \"custom\", customType: row.custom_type, ...parsePayload<CustomEntry>(row) };\n\t}\n}\n\nexport function entryStructureFromRow(row: EntryRow): EntryStructure {\n\treturn {\n\t\tid: row.id,\n\t\tparentId: row.parent_id,\n\t\tseq: row.seq,\n\t\ttimestamp: row.timestamp,\n\t\ttype: row.type,\n\t\t...(row.custom_type === null ? {} : { customType: row.custom_type }),\n\t};\n}\n\nexport function readEntryRows(db: SqliteDatabase, sessionId: string, ids: readonly string[]): EntryRow[] {\n\tif (ids.length === 0) return [];\n\tconst placeholders = joinSqlFragments(\n\t\tids.map((id) => sql`${id}`),\n\t\t\", \",\n\t);\n\treturn sql`SELECT id, parent_id, seq, type, custom_type, timestamp, payload\n\t\tFROM entries\n\t\tWHERE session_id = ${sessionId} AND id IN (${placeholders})`.all<EntryRow>(db);\n}\n\nexport function readAllEntryRows(db: SqliteDatabase, sessionId: string): EntryRow[] {\n\treturn sql`SELECT id, parent_id, seq, type, custom_type, timestamp, payload\n\t\tFROM entries WHERE session_id = ${sessionId} ORDER BY seq ASC`.all<EntryRow>(db);\n}\n\nexport function scanEntryRows(db: SqliteDatabase, sessionId: string, query: EntryScan): EntryRow[] {\n\tconst filters: SqlQuery[] = [sql`session_id = ${sessionId}`];\n\tif (query.type !== undefined) filters.push(sql`type = ${query.type}`);\n\tif (query.customType !== undefined) filters.push(sql`custom_type = ${query.customType}`);\n\tif (query.fromSeq !== undefined) filters.push(sql`seq >= ${query.fromSeq}`);\n\tif (query.toSeq !== undefined) filters.push(sql`seq <= ${query.toSeq}`);\n\n\tconst order = query.order === \"desc\" ? sql`ORDER BY seq DESC` : sql`ORDER BY seq ASC`;\n\tconst limit = query.limit === undefined ? sql`` : sql`LIMIT ${Math.max(0, query.limit)}`;\n\treturn sql`SELECT id, parent_id, seq, type, custom_type, timestamp, payload\n\t\tFROM entries WHERE ${joinSqlFragments(filters, \" AND \")} ${order} ${limit}`.all<EntryRow>(db);\n}\n"]}
@@ -0,0 +1,23 @@
1
+ import type { SessionMetadata } from "@m4ike1/ion-agent-core";
2
+ import type { SqliteDatabase } from "../types.ts";
3
+ export interface SessionRow {
4
+ id: string;
5
+ created_at: number;
6
+ parent_session_id: string | null;
7
+ storage_version: number;
8
+ metadata: string | null;
9
+ message_count: number;
10
+ usage_payload: string;
11
+ next_seq: number;
12
+ }
13
+ export interface SqliteSessionMetadata extends SessionMetadata {
14
+ /** SQLite container/shard path containing this session. */
15
+ path: string;
16
+ }
17
+ export declare function readSessionRow(db: SqliteDatabase, sessionId: string): SessionRow;
18
+ export declare function readAllSessionRows(db: SqliteDatabase): SessionRow[];
19
+ export declare function hasSessionRow(db: SqliteDatabase, sessionId: string): boolean;
20
+ export declare function metadataFromSessionRow(path: string, row: SessionRow, currentStorageVersion: number): SqliteSessionMetadata;
21
+ export declare function insertSessionRow(db: SqliteDatabase, metadata: SqliteSessionMetadata, storageVersion: number, nextSeq: number): void;
22
+ export declare function deleteSessionRows(db: SqliteDatabase, sessionId: string): void;
23
+ //# sourceMappingURL=session-row.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-row.d.ts","sourceRoot":"","sources":["../../../src/sqlite/session/session-row.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAG9D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAElD,MAAM,WAAW,UAAU;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,UAAU,EAAE,MAAM,CAAC;IACnB,iBAAiB,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,eAAe,EAAE,MAAM,CAAC;IACxB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,aAAa,EAAE,MAAM,CAAC;IACtB,aAAa,EAAE,MAAM,CAAC;IACtB,QAAQ,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,qBAAsB,SAAQ,eAAe;IAC7D,2DAA2D;IAC3D,IAAI,EAAE,MAAM,CAAC;CACb;AAaD,wBAAgB,cAAc,CAAC,EAAE,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,GAAG,UAAU,CAOhF;AAED,wBAAgB,kBAAkB,CAAC,EAAE,EAAE,cAAc,GAAG,UAAU,EAAE,CAInE;AAED,wBAAgB,aAAa,CAAC,EAAE,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAE5E;AAED,wBAAgB,sBAAsB,CACrC,IAAI,EAAE,MAAM,EACZ,GAAG,EAAE,UAAU,EACf,qBAAqB,EAAE,MAAM,GAC3B,qBAAqB,CAcvB;AAED,wBAAgB,gBAAgB,CAC/B,EAAE,EAAE,cAAc,EAClB,QAAQ,EAAE,qBAAqB,EAC/B,cAAc,EAAE,MAAM,EACtB,OAAO,EAAE,MAAM,GACb,IAAI,CAaN;AAED,wBAAgB,iBAAiB,CAAC,EAAE,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI,CAU7E","sourcesContent":["import type { SessionMetadata } from \"@m4ike1/ion-agent-core\";\nimport type { Usage } from \"@m4ike1/ion-ai\";\nimport { sql } from \"../sql.ts\";\nimport type { SqliteDatabase } from \"../types.ts\";\n\nexport interface SessionRow {\n\tid: string;\n\tcreated_at: number;\n\tparent_session_id: string | null;\n\tstorage_version: number;\n\tmetadata: string | null;\n\tmessage_count: number;\n\tusage_payload: string;\n\tnext_seq: number;\n}\n\nexport interface SqliteSessionMetadata extends SessionMetadata {\n\t/** SQLite container/shard path containing this session. */\n\tpath: string;\n}\n\nfunction zeroUsage(): Usage {\n\treturn {\n\t\tinput: 0,\n\t\toutput: 0,\n\t\tcacheRead: 0,\n\t\tcacheWrite: 0,\n\t\ttotalTokens: 0,\n\t\tcost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },\n\t};\n}\n\nexport function readSessionRow(db: SqliteDatabase, sessionId: string): SessionRow {\n\tconst row = sql`SELECT id, created_at, parent_session_id, storage_version, metadata,\n\t\t\tmessage_count, usage_payload, next_seq\n\t\tFROM sessions\n\t\tWHERE id = ${sessionId}`.get<SessionRow>(db);\n\tif (row === undefined) throw new Error(`Unknown SQLite session: ${sessionId}`);\n\treturn row;\n}\n\nexport function readAllSessionRows(db: SqliteDatabase): SessionRow[] {\n\treturn sql`SELECT id, created_at, parent_session_id, storage_version, metadata,\n\t\t\tmessage_count, usage_payload, next_seq\n\t\tFROM sessions`.all<SessionRow>(db);\n}\n\nexport function hasSessionRow(db: SqliteDatabase, sessionId: string): boolean {\n\treturn sql`SELECT id FROM sessions WHERE id = ${sessionId}`.get<{ id: string }>(db) !== undefined;\n}\n\nexport function metadataFromSessionRow(\n\tpath: string,\n\trow: SessionRow,\n\tcurrentStorageVersion: number,\n): SqliteSessionMetadata {\n\tif (row.storage_version > currentStorageVersion) {\n\t\tthrow new Error(`SQLite session storage version ${row.storage_version} is newer than ${currentStorageVersion}`);\n\t}\n\tif (row.storage_version < currentStorageVersion) {\n\t\tthrow new Error(`SQLite session storage version ${row.storage_version} requires migrations`);\n\t}\n\treturn {\n\t\tid: row.id,\n\t\tcreatedAt: row.created_at,\n\t\tstorageVersion: row.storage_version,\n\t\t...(row.parent_session_id === null ? {} : { parentSessionId: row.parent_session_id }),\n\t\tpath,\n\t};\n}\n\nexport function insertSessionRow(\n\tdb: SqliteDatabase,\n\tmetadata: SqliteSessionMetadata,\n\tstorageVersion: number,\n\tnextSeq: number,\n): void {\n\tsql`INSERT INTO sessions\n\t\t\t(id, created_at, parent_session_id, storage_version, metadata, message_count, usage_payload, next_seq)\n\t\tVALUES (\n\t\t\t${metadata.id},\n\t\t\t${metadata.createdAt},\n\t\t\t${metadata.parentSessionId ?? null},\n\t\t\t${storageVersion},\n\t\t\t${null},\n\t\t\t${0},\n\t\t\t${JSON.stringify(zeroUsage())},\n\t\t\t${nextSeq}\n\t\t)`.run(db);\n}\n\nexport function deleteSessionRows(db: SqliteDatabase, sessionId: string): void {\n\tsql`DELETE FROM entries WHERE session_id = ${sessionId}`.run(db);\n\tsql`DELETE FROM scalar_values WHERE session_id = ${sessionId}`.run(db);\n\tsql`DELETE FROM list_values WHERE session_id = ${sessionId}`.run(db);\n\tsql`DELETE FROM usage_ledger WHERE session_id = ${sessionId}`.run(db);\n\tsql`DELETE FROM branch_entries WHERE session_id = ${sessionId}`.run(db);\n\tsql`DELETE FROM branch_meta WHERE session_id = ${sessionId}`.run(db);\n\tconst result = sql`DELETE FROM sessions WHERE id = ${sessionId}`.run(db);\n\tif (result.changes !== 1)\n\t\tthrow new Error(`Expected to delete one SQLite session ${sessionId}, deleted ${result.changes}`);\n}\n"]}
@@ -0,0 +1,69 @@
1
+ import { sql } from "../sql.js";
2
+ function zeroUsage() {
3
+ return {
4
+ input: 0,
5
+ output: 0,
6
+ cacheRead: 0,
7
+ cacheWrite: 0,
8
+ totalTokens: 0,
9
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
10
+ };
11
+ }
12
+ export function readSessionRow(db, sessionId) {
13
+ const row = sql `SELECT id, created_at, parent_session_id, storage_version, metadata,
14
+ message_count, usage_payload, next_seq
15
+ FROM sessions
16
+ WHERE id = ${sessionId}`.get(db);
17
+ if (row === undefined)
18
+ throw new Error(`Unknown SQLite session: ${sessionId}`);
19
+ return row;
20
+ }
21
+ export function readAllSessionRows(db) {
22
+ return sql `SELECT id, created_at, parent_session_id, storage_version, metadata,
23
+ message_count, usage_payload, next_seq
24
+ FROM sessions`.all(db);
25
+ }
26
+ export function hasSessionRow(db, sessionId) {
27
+ return sql `SELECT id FROM sessions WHERE id = ${sessionId}`.get(db) !== undefined;
28
+ }
29
+ export function metadataFromSessionRow(path, row, currentStorageVersion) {
30
+ if (row.storage_version > currentStorageVersion) {
31
+ throw new Error(`SQLite session storage version ${row.storage_version} is newer than ${currentStorageVersion}`);
32
+ }
33
+ if (row.storage_version < currentStorageVersion) {
34
+ throw new Error(`SQLite session storage version ${row.storage_version} requires migrations`);
35
+ }
36
+ return {
37
+ id: row.id,
38
+ createdAt: row.created_at,
39
+ storageVersion: row.storage_version,
40
+ ...(row.parent_session_id === null ? {} : { parentSessionId: row.parent_session_id }),
41
+ path,
42
+ };
43
+ }
44
+ export function insertSessionRow(db, metadata, storageVersion, nextSeq) {
45
+ sql `INSERT INTO sessions
46
+ (id, created_at, parent_session_id, storage_version, metadata, message_count, usage_payload, next_seq)
47
+ VALUES (
48
+ ${metadata.id},
49
+ ${metadata.createdAt},
50
+ ${metadata.parentSessionId ?? null},
51
+ ${storageVersion},
52
+ ${null},
53
+ ${0},
54
+ ${JSON.stringify(zeroUsage())},
55
+ ${nextSeq}
56
+ )`.run(db);
57
+ }
58
+ export function deleteSessionRows(db, sessionId) {
59
+ sql `DELETE FROM entries WHERE session_id = ${sessionId}`.run(db);
60
+ sql `DELETE FROM scalar_values WHERE session_id = ${sessionId}`.run(db);
61
+ sql `DELETE FROM list_values WHERE session_id = ${sessionId}`.run(db);
62
+ sql `DELETE FROM usage_ledger WHERE session_id = ${sessionId}`.run(db);
63
+ sql `DELETE FROM branch_entries WHERE session_id = ${sessionId}`.run(db);
64
+ sql `DELETE FROM branch_meta WHERE session_id = ${sessionId}`.run(db);
65
+ const result = sql `DELETE FROM sessions WHERE id = ${sessionId}`.run(db);
66
+ if (result.changes !== 1)
67
+ throw new Error(`Expected to delete one SQLite session ${sessionId}, deleted ${result.changes}`);
68
+ }
69
+ //# sourceMappingURL=session-row.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-row.js","sourceRoot":"","sources":["../../../src/sqlite/session/session-row.ts"],"names":[],"mappings":"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","sourcesContent":["import type { SessionMetadata } from \"@m4ike1/ion-agent-core\";\nimport type { Usage } from \"@m4ike1/ion-ai\";\nimport { sql } from \"../sql.ts\";\nimport type { SqliteDatabase } from \"../types.ts\";\n\nexport interface SessionRow {\n\tid: string;\n\tcreated_at: number;\n\tparent_session_id: string | null;\n\tstorage_version: number;\n\tmetadata: string | null;\n\tmessage_count: number;\n\tusage_payload: string;\n\tnext_seq: number;\n}\n\nexport interface SqliteSessionMetadata extends SessionMetadata {\n\t/** SQLite container/shard path containing this session. */\n\tpath: string;\n}\n\nfunction zeroUsage(): Usage {\n\treturn {\n\t\tinput: 0,\n\t\toutput: 0,\n\t\tcacheRead: 0,\n\t\tcacheWrite: 0,\n\t\ttotalTokens: 0,\n\t\tcost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },\n\t};\n}\n\nexport function readSessionRow(db: SqliteDatabase, sessionId: string): SessionRow {\n\tconst row = sql`SELECT id, created_at, parent_session_id, storage_version, metadata,\n\t\t\tmessage_count, usage_payload, next_seq\n\t\tFROM sessions\n\t\tWHERE id = ${sessionId}`.get<SessionRow>(db);\n\tif (row === undefined) throw new Error(`Unknown SQLite session: ${sessionId}`);\n\treturn row;\n}\n\nexport function readAllSessionRows(db: SqliteDatabase): SessionRow[] {\n\treturn sql`SELECT id, created_at, parent_session_id, storage_version, metadata,\n\t\t\tmessage_count, usage_payload, next_seq\n\t\tFROM sessions`.all<SessionRow>(db);\n}\n\nexport function hasSessionRow(db: SqliteDatabase, sessionId: string): boolean {\n\treturn sql`SELECT id FROM sessions WHERE id = ${sessionId}`.get<{ id: string }>(db) !== undefined;\n}\n\nexport function metadataFromSessionRow(\n\tpath: string,\n\trow: SessionRow,\n\tcurrentStorageVersion: number,\n): SqliteSessionMetadata {\n\tif (row.storage_version > currentStorageVersion) {\n\t\tthrow new Error(`SQLite session storage version ${row.storage_version} is newer than ${currentStorageVersion}`);\n\t}\n\tif (row.storage_version < currentStorageVersion) {\n\t\tthrow new Error(`SQLite session storage version ${row.storage_version} requires migrations`);\n\t}\n\treturn {\n\t\tid: row.id,\n\t\tcreatedAt: row.created_at,\n\t\tstorageVersion: row.storage_version,\n\t\t...(row.parent_session_id === null ? {} : { parentSessionId: row.parent_session_id }),\n\t\tpath,\n\t};\n}\n\nexport function insertSessionRow(\n\tdb: SqliteDatabase,\n\tmetadata: SqliteSessionMetadata,\n\tstorageVersion: number,\n\tnextSeq: number,\n): void {\n\tsql`INSERT INTO sessions\n\t\t\t(id, created_at, parent_session_id, storage_version, metadata, message_count, usage_payload, next_seq)\n\t\tVALUES (\n\t\t\t${metadata.id},\n\t\t\t${metadata.createdAt},\n\t\t\t${metadata.parentSessionId ?? null},\n\t\t\t${storageVersion},\n\t\t\t${null},\n\t\t\t${0},\n\t\t\t${JSON.stringify(zeroUsage())},\n\t\t\t${nextSeq}\n\t\t)`.run(db);\n}\n\nexport function deleteSessionRows(db: SqliteDatabase, sessionId: string): void {\n\tsql`DELETE FROM entries WHERE session_id = ${sessionId}`.run(db);\n\tsql`DELETE FROM scalar_values WHERE session_id = ${sessionId}`.run(db);\n\tsql`DELETE FROM list_values WHERE session_id = ${sessionId}`.run(db);\n\tsql`DELETE FROM usage_ledger WHERE session_id = ${sessionId}`.run(db);\n\tsql`DELETE FROM branch_entries WHERE session_id = ${sessionId}`.run(db);\n\tsql`DELETE FROM branch_meta WHERE session_id = ${sessionId}`.run(db);\n\tconst result = sql`DELETE FROM sessions WHERE id = ${sessionId}`.run(db);\n\tif (result.changes !== 1)\n\t\tthrow new Error(`Expected to delete one SQLite session ${sessionId}, deleted ${result.changes}`);\n}\n"]}
@@ -0,0 +1,4 @@
1
+ import type { SqliteDatabase } from "../types.ts";
2
+ export declare function readNextSeq(db: SqliteDatabase, sessionId: string): number;
3
+ export declare function advanceNextSeq(db: SqliteDatabase, sessionId: string, nextSeq: number): void;
4
+ //# sourceMappingURL=session-sequences.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-sequences.d.ts","sourceRoot":"","sources":["../../../src/sqlite/session/session-sequences.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAElD,wBAAgB,WAAW,CAAC,EAAE,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAIzE;AAED,wBAAgB,cAAc,CAAC,EAAE,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAI3F","sourcesContent":["import { sql } from \"../sql.ts\";\nimport type { SqliteDatabase } from \"../types.ts\";\n\nexport function readNextSeq(db: SqliteDatabase, sessionId: string): number {\n\tconst row = sql`SELECT next_seq FROM sessions WHERE id = ${sessionId}`.get<{ next_seq: number }>(db);\n\tif (row === undefined) throw new Error(`Unknown SQLite session: ${sessionId}`);\n\treturn row.next_seq;\n}\n\nexport function advanceNextSeq(db: SqliteDatabase, sessionId: string, nextSeq: number): void {\n\tconst result = sql`UPDATE sessions SET next_seq = ${nextSeq} WHERE id = ${sessionId}`.run(db);\n\tif (result.changes !== 1)\n\t\tthrow new Error(`Expected to update one SQLite session ${sessionId}, updated ${result.changes}`);\n}\n"]}
@@ -0,0 +1,13 @@
1
+ import { sql } from "../sql.js";
2
+ export function readNextSeq(db, sessionId) {
3
+ const row = sql `SELECT next_seq FROM sessions WHERE id = ${sessionId}`.get(db);
4
+ if (row === undefined)
5
+ throw new Error(`Unknown SQLite session: ${sessionId}`);
6
+ return row.next_seq;
7
+ }
8
+ export function advanceNextSeq(db, sessionId, nextSeq) {
9
+ const result = sql `UPDATE sessions SET next_seq = ${nextSeq} WHERE id = ${sessionId}`.run(db);
10
+ if (result.changes !== 1)
11
+ throw new Error(`Expected to update one SQLite session ${sessionId}, updated ${result.changes}`);
12
+ }
13
+ //# sourceMappingURL=session-sequences.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-sequences.js","sourceRoot":"","sources":["../../../src/sqlite/session/session-sequences.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,MAAM,WAAW,CAAC;AAGhC,MAAM,UAAU,WAAW,CAAC,EAAkB,EAAE,SAAiB,EAAU;IAC1E,MAAM,GAAG,GAAG,GAAG,CAAA,4CAA4C,SAAS,EAAE,CAAC,GAAG,CAAuB,EAAE,CAAC,CAAC;IACrG,IAAI,GAAG,KAAK,SAAS;QAAE,MAAM,IAAI,KAAK,CAAC,2BAA2B,SAAS,EAAE,CAAC,CAAC;IAC/E,OAAO,GAAG,CAAC,QAAQ,CAAC;AAAA,CACpB;AAED,MAAM,UAAU,cAAc,CAAC,EAAkB,EAAE,SAAiB,EAAE,OAAe,EAAQ;IAC5F,MAAM,MAAM,GAAG,GAAG,CAAA,kCAAkC,OAAO,eAAe,SAAS,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;IAC9F,IAAI,MAAM,CAAC,OAAO,KAAK,CAAC;QACvB,MAAM,IAAI,KAAK,CAAC,yCAAyC,SAAS,aAAa,MAAM,CAAC,OAAO,EAAE,CAAC,CAAC;AAAA,CAClG","sourcesContent":["import { sql } from \"../sql.ts\";\nimport type { SqliteDatabase } from \"../types.ts\";\n\nexport function readNextSeq(db: SqliteDatabase, sessionId: string): number {\n\tconst row = sql`SELECT next_seq FROM sessions WHERE id = ${sessionId}`.get<{ next_seq: number }>(db);\n\tif (row === undefined) throw new Error(`Unknown SQLite session: ${sessionId}`);\n\treturn row.next_seq;\n}\n\nexport function advanceNextSeq(db: SqliteDatabase, sessionId: string, nextSeq: number): void {\n\tconst result = sql`UPDATE sessions SET next_seq = ${nextSeq} WHERE id = ${sessionId}`.run(db);\n\tif (result.changes !== 1)\n\t\tthrow new Error(`Expected to update one SQLite session ${sessionId}, updated ${result.changes}`);\n}\n"]}
@@ -0,0 +1,6 @@
1
+ import type { SessionStats, UsageRow } from "@m4ike1/ion-agent-core";
2
+ import type { SqliteDatabase } from "../types.ts";
3
+ export declare function readSessionStats(db: SqliteDatabase, sessionId: string): SessionStats;
4
+ export declare function incrementMessageCount(db: SqliteDatabase, sessionId: string): void;
5
+ export declare function addUsageToSessionStats(db: SqliteDatabase, sessionId: string, usage: UsageRow["usage"]): void;
6
+ //# sourceMappingURL=session-stats.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-stats.d.ts","sourceRoot":"","sources":["../../../src/sqlite/session/session-stats.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,wBAAwB,CAAC;AAGrE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AA0BlD,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,GAAG,YAAY,CAMpF;AAED,wBAAgB,qBAAqB,CAAC,EAAE,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI,CAEjF;AAED,wBAAgB,sBAAsB,CAAC,EAAE,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,QAAQ,CAAC,OAAO,CAAC,GAAG,IAAI,CAG5G","sourcesContent":["import type { SessionStats, UsageRow } from \"@m4ike1/ion-agent-core\";\nimport type { Usage } from \"@m4ike1/ion-ai\";\nimport { sql } from \"../sql.ts\";\nimport type { SqliteDatabase } from \"../types.ts\";\nimport { readSessionRow } from \"./session-row.ts\";\n\nfunction addUsage(left: Usage, right: Usage): Usage {\n\treturn {\n\t\tinput: left.input + right.input,\n\t\toutput: left.output + right.output,\n\t\tcacheRead: left.cacheRead + right.cacheRead,\n\t\tcacheWrite: left.cacheWrite + right.cacheWrite,\n\t\t...(left.cacheWrite1h === undefined && right.cacheWrite1h === undefined\n\t\t\t? {}\n\t\t\t: { cacheWrite1h: (left.cacheWrite1h ?? 0) + (right.cacheWrite1h ?? 0) }),\n\t\t...(left.reasoning === undefined && right.reasoning === undefined\n\t\t\t? {}\n\t\t\t: { reasoning: (left.reasoning ?? 0) + (right.reasoning ?? 0) }),\n\t\ttotalTokens: left.totalTokens + right.totalTokens,\n\t\tcost: {\n\t\t\tinput: left.cost.input + right.cost.input,\n\t\t\toutput: left.cost.output + right.cost.output,\n\t\t\tcacheRead: left.cost.cacheRead + right.cost.cacheRead,\n\t\t\tcacheWrite: left.cost.cacheWrite + right.cost.cacheWrite,\n\t\t\ttotal: left.cost.total + right.cost.total,\n\t\t},\n\t};\n}\n\nexport function readSessionStats(db: SqliteDatabase, sessionId: string): SessionStats {\n\tconst row = readSessionRow(db, sessionId);\n\treturn {\n\t\tmessageCount: row.message_count,\n\t\tusage: JSON.parse(row.usage_payload) as Usage,\n\t};\n}\n\nexport function incrementMessageCount(db: SqliteDatabase, sessionId: string): void {\n\tsql`UPDATE sessions SET message_count = message_count + 1 WHERE id = ${sessionId}`.run(db);\n}\n\nexport function addUsageToSessionStats(db: SqliteDatabase, sessionId: string, usage: UsageRow[\"usage\"]): void {\n\tconst current = readSessionStats(db, sessionId).usage;\n\tsql`UPDATE sessions SET usage_payload = ${JSON.stringify(addUsage(current, usage))} WHERE id = ${sessionId}`.run(db);\n}\n"]}