@sixb/sqlite 0.0.1

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 (152) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +57 -0
  3. package/dist/action-run-storage.d.ts +29 -0
  4. package/dist/action-run-storage.d.ts.map +1 -0
  5. package/dist/agents/agent-storage.d.ts +25 -0
  6. package/dist/agents/agent-storage.d.ts.map +1 -0
  7. package/dist/agents/index.d.ts +6 -0
  8. package/dist/agents/index.d.ts.map +1 -0
  9. package/dist/agents/messages.d.ts +14 -0
  10. package/dist/agents/messages.d.ts.map +1 -0
  11. package/dist/agents/rows.d.ts +77 -0
  12. package/dist/agents/rows.d.ts.map +1 -0
  13. package/dist/agents/runs.d.ts +26 -0
  14. package/dist/agents/runs.d.ts.map +1 -0
  15. package/dist/agents/threads.d.ts +14 -0
  16. package/dist/agents/threads.d.ts.map +1 -0
  17. package/dist/auth-storage/access-tokens.d.ts +33 -0
  18. package/dist/auth-storage/access-tokens.d.ts.map +1 -0
  19. package/dist/auth-storage/auth-storage.d.ts +46 -0
  20. package/dist/auth-storage/auth-storage.d.ts.map +1 -0
  21. package/dist/auth-storage/group-memberships.d.ts +21 -0
  22. package/dist/auth-storage/group-memberships.d.ts.map +1 -0
  23. package/dist/auth-storage/identities.d.ts +17 -0
  24. package/dist/auth-storage/identities.d.ts.map +1 -0
  25. package/dist/auth-storage/index.d.ts +3 -0
  26. package/dist/auth-storage/index.d.ts.map +1 -0
  27. package/dist/auth-storage/invitations.d.ts +28 -0
  28. package/dist/auth-storage/invitations.d.ts.map +1 -0
  29. package/dist/auth-storage/magic-links.d.ts +28 -0
  30. package/dist/auth-storage/magic-links.d.ts.map +1 -0
  31. package/dist/auth-storage/oidc-attempts.d.ts +18 -0
  32. package/dist/auth-storage/oidc-attempts.d.ts.map +1 -0
  33. package/dist/auth-storage/rows.d.ts +134 -0
  34. package/dist/auth-storage/rows.d.ts.map +1 -0
  35. package/dist/auth-storage/service-account-group-memberships.d.ts +17 -0
  36. package/dist/auth-storage/service-account-group-memberships.d.ts.map +1 -0
  37. package/dist/auth-storage/service-accounts.d.ts +14 -0
  38. package/dist/auth-storage/service-accounts.d.ts.map +1 -0
  39. package/dist/auth-storage/sessions.d.ts +55 -0
  40. package/dist/auth-storage/sessions.d.ts.map +1 -0
  41. package/dist/auth-storage/shared.d.ts +131 -0
  42. package/dist/auth-storage/shared.d.ts.map +1 -0
  43. package/dist/auth-storage/users.d.ts +19 -0
  44. package/dist/auth-storage/users.d.ts.map +1 -0
  45. package/dist/index.d.ts +71 -0
  46. package/dist/index.d.ts.map +1 -0
  47. package/dist/index.js +10204 -0
  48. package/dist/index.js.map +54 -0
  49. package/dist/latest-run-query.d.ts +22 -0
  50. package/dist/latest-run-query.d.ts.map +1 -0
  51. package/dist/migrations.d.ts +18 -0
  52. package/dist/migrations.d.ts.map +1 -0
  53. package/dist/object-query-compiler.d.ts +44 -0
  54. package/dist/object-query-compiler.d.ts.map +1 -0
  55. package/dist/object-storage.d.ts +93 -0
  56. package/dist/object-storage.d.ts.map +1 -0
  57. package/dist/ontology-storage/commits.d.ts +14 -0
  58. package/dist/ontology-storage/commits.d.ts.map +1 -0
  59. package/dist/ontology-storage/index.d.ts +22 -0
  60. package/dist/ontology-storage/index.d.ts.map +1 -0
  61. package/dist/ontology-storage/materialization-session.d.ts +49 -0
  62. package/dist/ontology-storage/materialization-session.d.ts.map +1 -0
  63. package/dist/ontology-storage/materialization-state.d.ts +63 -0
  64. package/dist/ontology-storage/materialization-state.d.ts.map +1 -0
  65. package/dist/ontology-storage/materialization-writer.d.ts +15 -0
  66. package/dist/ontology-storage/materialization-writer.d.ts.map +1 -0
  67. package/dist/ontology-storage/materializations.d.ts +32 -0
  68. package/dist/ontology-storage/materializations.d.ts.map +1 -0
  69. package/dist/ontology-storage/outbox.d.ts +14 -0
  70. package/dist/ontology-storage/outbox.d.ts.map +1 -0
  71. package/dist/ontology-storage/shared.d.ts +91 -0
  72. package/dist/ontology-storage/shared.d.ts.map +1 -0
  73. package/dist/ontology-storage/sources.d.ts +26 -0
  74. package/dist/ontology-storage/sources.d.ts.map +1 -0
  75. package/dist/pipeline-run-storage.d.ts +26 -0
  76. package/dist/pipeline-run-storage.d.ts.map +1 -0
  77. package/dist/projection-run-storage.d.ts +32 -0
  78. package/dist/projection-run-storage.d.ts.map +1 -0
  79. package/dist/rules-storage.d.ts +33 -0
  80. package/dist/rules-storage.d.ts.map +1 -0
  81. package/dist/run-list-query.d.ts +27 -0
  82. package/dist/run-list-query.d.ts.map +1 -0
  83. package/dist/storage-errors.d.ts +2 -0
  84. package/dist/storage-errors.d.ts.map +1 -0
  85. package/dist/sync-run-storage.d.ts +23 -0
  86. package/dist/sync-run-storage.d.ts.map +1 -0
  87. package/dist/timeseries-storage.d.ts +41 -0
  88. package/dist/timeseries-storage.d.ts.map +1 -0
  89. package/dist/transactions.d.ts +15 -0
  90. package/dist/transactions.d.ts.map +1 -0
  91. package/dist/tsconfig.json +5 -0
  92. package/dist/validation.d.ts +5 -0
  93. package/dist/validation.d.ts.map +1 -0
  94. package/dist/webhook-delivery-storage.d.ts +27 -0
  95. package/dist/webhook-delivery-storage.d.ts.map +1 -0
  96. package/dist/webhook-run-storage.d.ts +22 -0
  97. package/dist/webhook-run-storage.d.ts.map +1 -0
  98. package/dist/workflow-intervention-storage.d.ts +26 -0
  99. package/dist/workflow-intervention-storage.d.ts.map +1 -0
  100. package/dist/workflow-run-storage.d.ts +63 -0
  101. package/dist/workflow-run-storage.d.ts.map +1 -0
  102. package/package.json +58 -0
  103. package/src/action-run-storage.ts +761 -0
  104. package/src/agents/agent-storage.ts +46 -0
  105. package/src/agents/index.ts +5 -0
  106. package/src/agents/messages.ts +162 -0
  107. package/src/agents/rows.ts +205 -0
  108. package/src/agents/runs.ts +389 -0
  109. package/src/agents/threads.ts +133 -0
  110. package/src/auth-storage/access-tokens.ts +289 -0
  111. package/src/auth-storage/auth-storage.ts +784 -0
  112. package/src/auth-storage/group-memberships.ts +52 -0
  113. package/src/auth-storage/identities.ts +98 -0
  114. package/src/auth-storage/index.ts +2 -0
  115. package/src/auth-storage/invitations.ts +258 -0
  116. package/src/auth-storage/magic-links.ts +149 -0
  117. package/src/auth-storage/oidc-attempts.ts +109 -0
  118. package/src/auth-storage/rows.ts +338 -0
  119. package/src/auth-storage/service-account-group-memberships.ts +161 -0
  120. package/src/auth-storage/service-accounts.ts +168 -0
  121. package/src/auth-storage/sessions.ts +230 -0
  122. package/src/auth-storage/shared.ts +707 -0
  123. package/src/auth-storage/users.ts +226 -0
  124. package/src/index.ts +292 -0
  125. package/src/latest-run-query.ts +49 -0
  126. package/src/migrations/001-initial-schema.sql +1068 -0
  127. package/src/migrations.ts +245 -0
  128. package/src/object-query-compiler.ts +1198 -0
  129. package/src/object-storage.ts +605 -0
  130. package/src/ontology-storage/commits.ts +127 -0
  131. package/src/ontology-storage/index.ts +35 -0
  132. package/src/ontology-storage/materialization-session.ts +393 -0
  133. package/src/ontology-storage/materialization-state.ts +901 -0
  134. package/src/ontology-storage/materialization-writer.ts +394 -0
  135. package/src/ontology-storage/materializations.ts +838 -0
  136. package/src/ontology-storage/outbox.ts +237 -0
  137. package/src/ontology-storage/shared.ts +269 -0
  138. package/src/ontology-storage/sources.ts +653 -0
  139. package/src/pipeline-run-storage.ts +506 -0
  140. package/src/projection-run-storage.ts +592 -0
  141. package/src/rules-storage.ts +375 -0
  142. package/src/run-list-query.ts +98 -0
  143. package/src/sql.d.ts +4 -0
  144. package/src/storage-errors.ts +3 -0
  145. package/src/sync-run-storage.ts +299 -0
  146. package/src/timeseries-storage.ts +212 -0
  147. package/src/transactions.ts +96 -0
  148. package/src/validation.ts +30 -0
  149. package/src/webhook-delivery-storage.ts +272 -0
  150. package/src/webhook-run-storage.ts +240 -0
  151. package/src/workflow-intervention-storage.ts +398 -0
  152. package/src/workflow-run-storage.ts +1148 -0
@@ -0,0 +1,46 @@
1
+ import type { AgentStorage } from "@sixb/core/storage"
2
+ import { installFreshSqliteSchema } from "../migrations"
3
+ import {
4
+ closeSqliteStoreConnection,
5
+ openSqliteStoreConnection,
6
+ type SqliteStoreConnection,
7
+ } from "../transactions"
8
+ import { SqliteAgentMessageStore } from "./messages"
9
+ import { SqliteAgentRunStore } from "./runs"
10
+ import { SqliteAgentThreadStore } from "./threads"
11
+
12
+ export interface SqliteAgentStorageOptions {
13
+ /** Path to SQLite database file. Defaults to ':memory:' for in-memory database. */
14
+ path?: string
15
+ /** Internal shared connection used by bundled SqliteStorage. */
16
+ connection?: SqliteStoreConnection
17
+ }
18
+
19
+ /**
20
+ * SQLite-backed agent persistence: thread / run / message sub-stores sharing one connection. The
21
+ * cross-table writes (reserve + finish touch the thread anchor, append bumps thread stats) run in
22
+ * SQLite transactions; the bundled {@link SqliteStorage} serializes all of them on one connection.
23
+ */
24
+ export class SqliteAgentStorage implements AgentStorage {
25
+ readonly threads: SqliteAgentThreadStore
26
+ readonly runs: SqliteAgentRunStore
27
+ readonly messages: SqliteAgentMessageStore
28
+
29
+ private readonly connection: SqliteStoreConnection
30
+
31
+ constructor(options: SqliteAgentStorageOptions = {}) {
32
+ this.connection = openSqliteStoreConnection(options)
33
+
34
+ if (this.connection.installFreshSchema) {
35
+ installFreshSqliteSchema(this.connection.db)
36
+ }
37
+
38
+ this.threads = new SqliteAgentThreadStore(this.connection.db)
39
+ this.runs = new SqliteAgentRunStore(this.connection.db)
40
+ this.messages = new SqliteAgentMessageStore(this.connection.db)
41
+ }
42
+
43
+ close(): void {
44
+ closeSqliteStoreConnection(this.connection)
45
+ }
46
+ }
@@ -0,0 +1,5 @@
1
+ export type { SqliteAgentStorageOptions } from "./agent-storage"
2
+ export { SqliteAgentStorage } from "./agent-storage"
3
+ export { SqliteAgentMessageStore } from "./messages"
4
+ export { SqliteAgentRunStore } from "./runs"
5
+ export { SqliteAgentThreadStore } from "./threads"
@@ -0,0 +1,162 @@
1
+ import type { Database } from "bun:sqlite"
2
+ import { AGENT_MESSAGE_CONTENT_VERSION } from "@sixb/core/internal/agents"
3
+ import {
4
+ type AgentMessageRecord,
5
+ type AgentMessageStore,
6
+ AgentStorageError,
7
+ type AppendAgentMessageInput,
8
+ type ListAgentMessagesInput,
9
+ type ListAgentMessagesResult,
10
+ } from "@sixb/core/storage"
11
+ import type { SqliteValue } from "../run-list-query"
12
+ import { isUniqueConstraintError } from "../storage-errors"
13
+ import { type AgentMessageRow, queryAgentList, rowToMessageRecord } from "./rows"
14
+
15
+ export class SqliteAgentMessageStore implements AgentMessageStore {
16
+ constructor(private readonly db: Database) {}
17
+
18
+ async append(input: AppendAgentMessageInput): Promise<AgentMessageRecord> {
19
+ const createdAt = input.createdAt ?? new Date()
20
+
21
+ return this.db.transaction(() => {
22
+ const thread = this.db
23
+ .query("SELECT 1 AS present FROM agent_threads WHERE project_id = ? AND id = ?")
24
+ .get(input.projectId, input.threadId) as { present: number } | null
25
+
26
+ if (!thread) {
27
+ throw new AgentStorageError(
28
+ "thread_not_found",
29
+ `[SixbSqlite] Agent thread '${input.threadId}' not found for project '${input.projectId}'.`
30
+ )
31
+ }
32
+
33
+ if (input.runId !== null) {
34
+ const run = this.db
35
+ .query("SELECT 1 AS present FROM agent_runs WHERE project_id = ? AND id = ?")
36
+ .get(input.projectId, input.runId) as { present: number } | null
37
+
38
+ if (!run) {
39
+ throw new AgentStorageError(
40
+ "run_not_found",
41
+ `[SixbSqlite] Agent run '${input.runId}' not found for project '${input.projectId}'.`
42
+ )
43
+ }
44
+ }
45
+
46
+ const seqRow = this.db
47
+ .query(
48
+ "SELECT COALESCE(MAX(seq), 0) + 1 AS next FROM agent_messages WHERE project_id = ? AND thread_id = ?"
49
+ )
50
+ .get(input.projectId, input.threadId) as { next: number }
51
+
52
+ try {
53
+ this.db
54
+ .query(
55
+ `
56
+ INSERT INTO agent_messages (
57
+ project_id,
58
+ id,
59
+ thread_id,
60
+ run_id,
61
+ role,
62
+ author_principal_type,
63
+ author_principal_id,
64
+ seq,
65
+ parts,
66
+ metadata,
67
+ content_version,
68
+ created_at,
69
+ completed_at
70
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
71
+ `
72
+ )
73
+ .run(
74
+ input.projectId,
75
+ input.id,
76
+ input.threadId,
77
+ input.runId,
78
+ input.role,
79
+ input.authorPrincipal?.type ?? null,
80
+ input.authorPrincipal?.id ?? null,
81
+ seqRow.next,
82
+ JSON.stringify(input.parts),
83
+ input.metadata === undefined ? null : JSON.stringify(input.metadata),
84
+ AGENT_MESSAGE_CONTENT_VERSION,
85
+ createdAt.toISOString(),
86
+ input.completedAt ? input.completedAt.toISOString() : null
87
+ )
88
+ } catch (error) {
89
+ if (isUniqueConstraintError(error)) {
90
+ throw new AgentStorageError(
91
+ "duplicate_id",
92
+ `[SixbSqlite] Agent message '${input.id}' already exists for project '${input.projectId}'.`
93
+ )
94
+ }
95
+
96
+ throw error
97
+ }
98
+
99
+ this.db
100
+ .query(
101
+ `
102
+ UPDATE agent_threads
103
+ SET message_count = message_count + 1, last_message_at = ?, updated_at = ?
104
+ WHERE project_id = ? AND id = ?
105
+ `
106
+ )
107
+ .run(createdAt.toISOString(), createdAt.toISOString(), input.projectId, input.threadId)
108
+
109
+ return this.requireMessage(input.projectId, input.id)
110
+ })()
111
+ }
112
+
113
+ async getById(params: { projectId: string; id: string }): Promise<AgentMessageRecord | null> {
114
+ const row = this.db
115
+ .query("SELECT * FROM agent_messages WHERE project_id = ? AND id = ?")
116
+ .get(params.projectId, params.id) as AgentMessageRow | null
117
+
118
+ return row ? rowToMessageRecord(row) : null
119
+ }
120
+
121
+ async list(input: ListAgentMessagesInput): Promise<ListAgentMessagesResult> {
122
+ if (input.roles !== undefined && input.roles.length === 0) {
123
+ return { messages: [], hasMore: false, total: 0 }
124
+ }
125
+
126
+ const whereClauses = ["project_id = ?", "thread_id = ?"]
127
+ const args: SqliteValue[] = [input.projectId, input.threadId]
128
+
129
+ if (input.roles) {
130
+ whereClauses.push(`role IN (${input.roles.map(() => "?").join(", ")})`)
131
+ args.push(...input.roles)
132
+ }
133
+
134
+ const dir = input.order === "desc" ? "DESC" : "ASC"
135
+ const { rows, total, hasMore } = queryAgentList<AgentMessageRow>({
136
+ db: this.db,
137
+ table: "agent_messages",
138
+ whereClauses,
139
+ args,
140
+ orderBy: `seq ${dir}`,
141
+ limit: input.limit,
142
+ offset: input.offset,
143
+ })
144
+
145
+ return { messages: rows.map(rowToMessageRecord), hasMore, total }
146
+ }
147
+
148
+ private requireMessage(projectId: string, id: string): AgentMessageRecord {
149
+ const row = this.db
150
+ .query("SELECT * FROM agent_messages WHERE project_id = ? AND id = ?")
151
+ .get(projectId, id) as AgentMessageRow | null
152
+
153
+ if (!row) {
154
+ throw new AgentStorageError(
155
+ "invalid_state",
156
+ `[SixbSqlite] Failed to load agent message '${id}' for project '${projectId}'.`
157
+ )
158
+ }
159
+
160
+ return rowToMessageRecord(row)
161
+ }
162
+ }
@@ -0,0 +1,205 @@
1
+ import type { Database } from "bun:sqlite"
2
+ import { type AgentMessagePart, type Principal, SYSTEM_PRINCIPAL } from "@sixb/core"
3
+ import {
4
+ type AgentMessageRecord,
5
+ type AgentRunDiagnostic,
6
+ type AgentRunRecord,
7
+ type AgentRunUsage,
8
+ type AgentThreadRecord,
9
+ coerceAgentRunFinishReason,
10
+ } from "@sixb/core/storage"
11
+ import type { SqliteValue } from "../run-list-query"
12
+
13
+ // ── row shapes (snake_case, as stored) ──────────────────────────────────────────────────────────
14
+
15
+ export interface AgentThreadRow {
16
+ project_id: string
17
+ id: string
18
+ agent_id: string
19
+ owner_principal_type: Principal["type"]
20
+ owner_principal_id: string
21
+ title: string | null
22
+ status: AgentThreadRecord["status"]
23
+ active_run_id: string | null
24
+ last_message_at: string | null
25
+ message_count: number
26
+ created_at: string
27
+ updated_at: string
28
+ }
29
+
30
+ export interface AgentRunRow {
31
+ project_id: string
32
+ id: string
33
+ thread_id: string
34
+ agent_id: string
35
+ trigger_message_id: string
36
+ requested_by_principal_type: Principal["type"] | null
37
+ requested_by_principal_id: string | null
38
+ execution_principal_type: "serviceAccount" | null
39
+ execution_principal_id: string | null
40
+ status: AgentRunRecord["status"]
41
+ model_id: string | null
42
+ finish_reason: string | null
43
+ usage_input_tokens: number | null
44
+ usage_output_tokens: number | null
45
+ usage_total_tokens: number | null
46
+ usage_reasoning_tokens: number | null
47
+ usage_cached_input_tokens: number | null
48
+ error: string | null
49
+ diagnostics: string | null
50
+ attempt: number
51
+ execution_token: string | null
52
+ execution_queue_lease_expires_at: string | null
53
+ created_at: string
54
+ started_at: string | null
55
+ completed_at: string | null
56
+ }
57
+
58
+ export interface AgentMessageRow {
59
+ project_id: string
60
+ id: string
61
+ thread_id: string
62
+ run_id: string | null
63
+ role: AgentMessageRecord["role"]
64
+ author_principal_type: Principal["type"] | null
65
+ author_principal_id: string | null
66
+ seq: number
67
+ parts: string
68
+ metadata: string | null
69
+ content_version: number
70
+ created_at: string
71
+ completed_at: string | null
72
+ }
73
+
74
+ // ── row → record mappers ────────────────────────────────────────────────────────────────────────
75
+
76
+ export function rowToThreadRecord(row: AgentThreadRow): AgentThreadRecord {
77
+ return {
78
+ id: row.id,
79
+ projectId: row.project_id,
80
+ agentId: row.agent_id,
81
+ ownerPrincipal: { type: row.owner_principal_type, id: row.owner_principal_id },
82
+ title: row.title ?? undefined,
83
+ status: row.status,
84
+ activeRunId: row.active_run_id,
85
+ lastMessageAt: row.last_message_at ? new Date(row.last_message_at) : undefined,
86
+ messageCount: row.message_count,
87
+ createdAt: new Date(row.created_at),
88
+ updatedAt: new Date(row.updated_at),
89
+ }
90
+ }
91
+
92
+ export function rowToRunRecord(row: AgentRunRow): AgentRunRecord {
93
+ return {
94
+ id: row.id,
95
+ projectId: row.project_id,
96
+ threadId: row.thread_id,
97
+ agentId: row.agent_id,
98
+ triggerMessageId: row.trigger_message_id,
99
+ requestedByPrincipal:
100
+ principalFromColumns(row.requested_by_principal_type, row.requested_by_principal_id) ??
101
+ SYSTEM_PRINCIPAL,
102
+ executionPrincipal: serviceAccountPrincipalFromColumns(
103
+ row.execution_principal_type,
104
+ row.execution_principal_id
105
+ ),
106
+ status: row.status,
107
+ modelId: row.model_id ?? undefined,
108
+ finishReason: coerceAgentRunFinishReason(row.finish_reason),
109
+ usage: rowToUsage(row),
110
+ error: row.error ?? undefined,
111
+ diagnostics:
112
+ row.diagnostics === null ? undefined : (JSON.parse(row.diagnostics) as AgentRunDiagnostic[]),
113
+ attempt: row.attempt,
114
+ execution:
115
+ row.execution_token && row.execution_queue_lease_expires_at
116
+ ? {
117
+ token: row.execution_token,
118
+ queueLeaseExpiresAt: new Date(row.execution_queue_lease_expires_at),
119
+ }
120
+ : undefined,
121
+ createdAt: new Date(row.created_at),
122
+ startedAt: row.started_at ? new Date(row.started_at) : undefined,
123
+ completedAt: row.completed_at ? new Date(row.completed_at) : undefined,
124
+ }
125
+ }
126
+
127
+ export function rowToMessageRecord(row: AgentMessageRow): AgentMessageRecord {
128
+ return {
129
+ id: row.id,
130
+ projectId: row.project_id,
131
+ threadId: row.thread_id,
132
+ runId: row.run_id,
133
+ role: row.role,
134
+ authorPrincipal: principalFromColumns(row.author_principal_type, row.author_principal_id),
135
+ seq: row.seq,
136
+ parts: JSON.parse(row.parts) as AgentMessagePart[],
137
+ // A null/absent column and an explicit JSON `null` both mean "no metadata" (jsonb cannot tell
138
+ // SQL NULL from a json null on read, so both backends normalise to `undefined` for parity).
139
+ metadata: row.metadata === null ? undefined : (JSON.parse(row.metadata) ?? undefined),
140
+ contentVersion: row.content_version,
141
+ createdAt: new Date(row.created_at),
142
+ completedAt: row.completed_at ? new Date(row.completed_at) : undefined,
143
+ }
144
+ }
145
+
146
+ function principalFromColumns(
147
+ type: Principal["type"] | null | undefined,
148
+ id: string | null | undefined
149
+ ): Principal | undefined {
150
+ return type && id ? { type, id } : undefined
151
+ }
152
+
153
+ function serviceAccountPrincipalFromColumns(
154
+ type: "serviceAccount" | null | undefined,
155
+ id: string | null | undefined
156
+ ): Extract<Principal, { readonly type: "serviceAccount" }> | undefined {
157
+ return type === "serviceAccount" && id ? { type, id } : undefined
158
+ }
159
+
160
+ function rowToUsage(row: AgentRunRow): AgentRunUsage | undefined {
161
+ const usage: AgentRunUsage = {
162
+ ...(row.usage_input_tokens === null ? {} : { inputTokens: row.usage_input_tokens }),
163
+ ...(row.usage_output_tokens === null ? {} : { outputTokens: row.usage_output_tokens }),
164
+ ...(row.usage_total_tokens === null ? {} : { totalTokens: row.usage_total_tokens }),
165
+ ...(row.usage_reasoning_tokens === null ? {} : { reasoningTokens: row.usage_reasoning_tokens }),
166
+ ...(row.usage_cached_input_tokens === null
167
+ ? {}
168
+ : { cachedInputTokens: row.usage_cached_input_tokens }),
169
+ }
170
+ return Object.keys(usage).length === 0 ? undefined : usage
171
+ }
172
+
173
+ // ── offset pagination (threads / messages order by their own column, not started_at) ─────────────
174
+
175
+ export function queryAgentList<TRow>(input: {
176
+ readonly db: Database
177
+ readonly table: "agent_threads" | "agent_messages"
178
+ readonly whereClauses: readonly string[]
179
+ readonly args: readonly SqliteValue[]
180
+ readonly orderBy: string
181
+ readonly limit?: number
182
+ readonly offset?: number
183
+ }): { readonly rows: readonly TRow[]; readonly total: number; readonly hasMore: boolean } {
184
+ const where = `WHERE ${input.whereClauses.join(" AND ")}`
185
+ const offset = input.offset ?? 0
186
+
187
+ const totalRow = input.db
188
+ .query(`SELECT COUNT(*) AS count FROM ${input.table} ${where}`)
189
+ .get(...input.args) as { count: number }
190
+
191
+ let query = `SELECT * FROM ${input.table} ${where} ORDER BY ${input.orderBy}`
192
+ const queryArgs = [...input.args]
193
+
194
+ if (input.limit !== undefined) {
195
+ query += " LIMIT ? OFFSET ?"
196
+ queryArgs.push(input.limit, offset)
197
+ } else if (offset > 0) {
198
+ query += " LIMIT -1 OFFSET ?"
199
+ queryArgs.push(offset)
200
+ }
201
+
202
+ const rows = input.db.query(query).all(...queryArgs) as TRow[]
203
+ const total = totalRow.count
204
+ return { rows, total, hasMore: offset + rows.length < total }
205
+ }