@sixb/sqlite 0.1.20 → 0.1.22
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/dist/action-run-storage.d.ts +4 -0
- package/dist/action-run-storage.d.ts.map +1 -1
- package/dist/activity-storage.d.ts +20 -0
- package/dist/activity-storage.d.ts.map +1 -0
- package/dist/agents/agent-storage.d.ts +8 -2
- package/dist/agents/agent-storage.d.ts.map +1 -1
- package/dist/agents/approvals.d.ts +51 -0
- package/dist/agents/approvals.d.ts.map +1 -0
- package/dist/agents/checkpoints.d.ts +1 -0
- package/dist/agents/checkpoints.d.ts.map +1 -1
- package/dist/agents/index.d.ts +1 -0
- package/dist/agents/index.d.ts.map +1 -1
- package/dist/agents/memory.d.ts +21 -0
- package/dist/agents/memory.d.ts.map +1 -0
- package/dist/agents/messages.d.ts.map +1 -1
- package/dist/agents/rows.d.ts +15 -1
- package/dist/agents/rows.d.ts.map +1 -1
- package/dist/agents/runs.d.ts +14 -1
- package/dist/agents/runs.d.ts.map +1 -1
- package/dist/agents/settings.d.ts +29 -0
- package/dist/agents/settings.d.ts.map +1 -0
- package/dist/agents/threads.d.ts +16 -1
- package/dist/agents/threads.d.ts.map +1 -1
- package/dist/ai-cost-groups.d.ts.map +1 -1
- package/dist/ai-cost-storage.d.ts.map +1 -1
- package/dist/ai-usage-storage.d.ts +4 -1
- package/dist/ai-usage-storage.d.ts.map +1 -1
- package/dist/artifact-storage.d.ts +31 -0
- package/dist/artifact-storage.d.ts.map +1 -0
- package/dist/auth-storage/auth-storage.d.ts +3 -2
- package/dist/auth-storage/auth-storage.d.ts.map +1 -1
- package/dist/auth-storage/sessions.d.ts +2 -1
- package/dist/auth-storage/sessions.d.ts.map +1 -1
- package/dist/auth-storage/users.d.ts +7 -3
- package/dist/auth-storage/users.d.ts.map +1 -1
- package/dist/execution-storage.d.ts +3 -1
- package/dist/execution-storage.d.ts.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5216 -2517
- package/dist/index.js.map +39 -31
- package/dist/migrations.d.ts +0 -1
- package/dist/migrations.d.ts.map +1 -1
- package/dist/objects/conditional-reader.d.ts +8 -0
- package/dist/objects/conditional-reader.d.ts.map +1 -0
- package/dist/objects/query-compiler.d.ts +26 -1
- package/dist/objects/query-compiler.d.ts.map +1 -1
- package/dist/objects/read-scope.d.ts +10 -1
- package/dist/objects/read-scope.d.ts.map +1 -1
- package/dist/objects/reader.d.ts +2 -0
- package/dist/objects/reader.d.ts.map +1 -1
- package/dist/objects/scoped-reader.d.ts +15 -0
- package/dist/objects/scoped-reader.d.ts.map +1 -0
- package/dist/objects/selected-reader.d.ts.map +1 -1
- package/dist/objects/storage.d.ts +6 -1
- package/dist/objects/storage.d.ts.map +1 -1
- package/dist/ontology-storage/index.d.ts +2 -0
- package/dist/ontology-storage/index.d.ts.map +1 -1
- package/dist/ontology-storage/materialization-writer.d.ts +2 -0
- package/dist/ontology-storage/materialization-writer.d.ts.map +1 -1
- package/dist/ontology-storage/materializations.d.ts.map +1 -1
- package/dist/ontology-storage/sources.d.ts +2 -1
- package/dist/ontology-storage/sources.d.ts.map +1 -1
- package/dist/ontology-storage/unique-values.d.ts +42 -0
- package/dist/ontology-storage/unique-values.d.ts.map +1 -0
- package/dist/share-grant-storage.d.ts +2 -1
- package/dist/share-grant-storage.d.ts.map +1 -1
- package/dist/workflow-run-storage.d.ts +4 -0
- package/dist/workflow-run-storage.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/action-run-storage.ts +27 -0
- package/src/activity-storage.ts +588 -0
- package/src/agents/agent-storage.ts +11 -2
- package/src/agents/approvals.ts +456 -0
- package/src/agents/checkpoints.ts +9 -4
- package/src/agents/index.ts +1 -0
- package/src/agents/memory.ts +195 -0
- package/src/agents/messages.ts +21 -18
- package/src/agents/rows.ts +46 -1
- package/src/agents/runs.ts +202 -21
- package/src/agents/settings.ts +112 -0
- package/src/agents/threads.ts +412 -1
- package/src/ai-cost-groups.ts +11 -1
- package/src/ai-cost-storage.ts +54 -12
- package/src/ai-usage-storage.ts +79 -2
- package/src/artifact-storage.ts +553 -0
- package/src/auth-storage/auth-storage.ts +58 -26
- package/src/auth-storage/sessions.ts +27 -0
- package/src/auth-storage/users.ts +106 -60
- package/src/execution-storage.ts +95 -0
- package/src/index.ts +10 -0
- package/src/migrations/062-artifacts.sql +71 -0
- package/src/migrations/063-drop-output-diagnostics.sql +19 -0
- package/src/migrations/064-activity.sql +61 -0
- package/src/migrations/065-activity-timeline.sql +19 -0
- package/src/migrations/066-activity-interactions.sql +20 -0
- package/src/migrations/067-artifact-kinds.sql +32 -0
- package/src/migrations/068-artifact-state.sql +15 -0
- package/src/migrations/069-agent-thread-participants.sql +16 -0
- package/src/migrations/070-forked-context-checkpoints.sql +45 -0
- package/src/migrations/071-unique-values.sql +22 -0
- package/src/migrations/072-generated-media-usage.sql +32 -0
- package/src/migrations/073-model-call-files.sql +11 -0
- package/src/migrations/074-agent-memory.sql +32 -0
- package/src/migrations/075-artifact-threads.sql +21 -0
- package/src/migrations/076-agent-approvals.sql +59 -0
- package/src/migrations/077-agent-approval-resume.sql +20 -0
- package/src/migrations/078-subagent-threads.sql +241 -0
- package/src/migrations/079-subagent-questions.sql +6 -0
- package/src/migrations.ts +89 -1
- package/src/objects/README.md +7 -0
- package/src/objects/conditional-reader.ts +27 -0
- package/src/objects/query-compiler.ts +36 -6
- package/src/objects/read-scope.ts +140 -23
- package/src/objects/reader.ts +47 -14
- package/src/objects/scoped-reader.ts +103 -0
- package/src/objects/selected-reader.ts +12 -75
- package/src/objects/storage.ts +16 -0
- package/src/ontology-storage/index.ts +3 -0
- package/src/ontology-storage/materialization-writer.ts +14 -0
- package/src/ontology-storage/materializations.ts +16 -1
- package/src/ontology-storage/sources.ts +41 -0
- package/src/ontology-storage/unique-values.ts +187 -0
- package/src/share-grant-storage.ts +25 -0
- package/src/transactions.ts +1 -1
- package/src/workflow-run-storage.ts +27 -0
|
@@ -0,0 +1,588 @@
|
|
|
1
|
+
import type { Database, SQLQueryBindings } from "bun:sqlite"
|
|
2
|
+
import {
|
|
3
|
+
ACTIVITY_MAX_VISIT_MS,
|
|
4
|
+
type ActivitySourceState,
|
|
5
|
+
activityActor,
|
|
6
|
+
activityCalendarDays,
|
|
7
|
+
parseActivityAttribution,
|
|
8
|
+
planActivityAppend,
|
|
9
|
+
type StoredActivityEvent,
|
|
10
|
+
serializeActivityAttribution,
|
|
11
|
+
validateActivityAppend,
|
|
12
|
+
} from "@sixb/core/internal/storage"
|
|
13
|
+
import type {
|
|
14
|
+
ActivityCalendarInput,
|
|
15
|
+
ActivityCursor,
|
|
16
|
+
ActivityDay,
|
|
17
|
+
ActivityInteraction,
|
|
18
|
+
ActivityPageView,
|
|
19
|
+
ActivityPeriodInput,
|
|
20
|
+
ActivitySessionVisits,
|
|
21
|
+
ActivityStorage,
|
|
22
|
+
ActivitySummary,
|
|
23
|
+
ActivityUsersPage,
|
|
24
|
+
ActivityVisit,
|
|
25
|
+
ActivityVisitsPage,
|
|
26
|
+
AppendActivityInput,
|
|
27
|
+
AppendActivityResult,
|
|
28
|
+
ListActivitySessionVisitsInput,
|
|
29
|
+
ListActivityUsersInput,
|
|
30
|
+
ListActivityVisitsInput,
|
|
31
|
+
ReadActivityVisitInput,
|
|
32
|
+
StoredActivityVisitPage,
|
|
33
|
+
} from "@sixb/core/storage"
|
|
34
|
+
import { runImmediateTransaction, type SqliteStoreConnection } from "./transactions"
|
|
35
|
+
|
|
36
|
+
const VISIT_COLUMNS = `
|
|
37
|
+
v.id, v.actor_type, v.user_id, v.grant_id, v.started_at, v.last_seen_at, v.page_views,
|
|
38
|
+
v.interactions, v.attribution
|
|
39
|
+
`
|
|
40
|
+
|
|
41
|
+
// Sorts after every stored timestamp, so a first page starts from the newest row.
|
|
42
|
+
const NO_CURSOR = { at: "9999-12-31T23:59:59.999Z", id: "" }
|
|
43
|
+
|
|
44
|
+
/** SQLite activity. The immediate transaction is the source lock; the policy decides the rest. */
|
|
45
|
+
export class SqliteActivityStorage implements ActivityStorage {
|
|
46
|
+
private readonly db: Database
|
|
47
|
+
|
|
48
|
+
constructor(connection: SqliteStoreConnection) {
|
|
49
|
+
this.db = connection.db
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async append(input: AppendActivityInput): Promise<AppendActivityResult> {
|
|
53
|
+
validateActivityAppend(input)
|
|
54
|
+
|
|
55
|
+
return runImmediateTransaction(this.db, () => {
|
|
56
|
+
const plan = planActivityAppend(input, this.source(input), this.storedEvents(input))
|
|
57
|
+
if (plan.status !== "append") {
|
|
58
|
+
return { status: plan.status === "duplicate" ? "accepted" : "throttled", accepted: 0 }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const { state } = plan
|
|
62
|
+
const lastSeenAt = iso(state.lastSeenAt)
|
|
63
|
+
|
|
64
|
+
if (plan.newVisit) {
|
|
65
|
+
this.run(
|
|
66
|
+
`
|
|
67
|
+
INSERT INTO activity_visits (
|
|
68
|
+
project_id, id, actor_type, user_id, grant_id, session_id,
|
|
69
|
+
started_at, last_seen_at, page_views, interactions, attribution
|
|
70
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
71
|
+
`,
|
|
72
|
+
input.projectId,
|
|
73
|
+
state.visitId,
|
|
74
|
+
input.actor.type,
|
|
75
|
+
input.actor.type === "user" ? input.actor.userId : null,
|
|
76
|
+
input.actor.type === "share" ? input.actor.grantId : null,
|
|
77
|
+
input.sessionId,
|
|
78
|
+
lastSeenAt,
|
|
79
|
+
lastSeenAt,
|
|
80
|
+
plan.pageViews.length,
|
|
81
|
+
plan.interactions.length,
|
|
82
|
+
plan.newVisit.attribution === null
|
|
83
|
+
? null
|
|
84
|
+
: serializeActivityAttribution(plan.newVisit.attribution)
|
|
85
|
+
)
|
|
86
|
+
} else {
|
|
87
|
+
this.run(
|
|
88
|
+
`
|
|
89
|
+
UPDATE activity_visits
|
|
90
|
+
SET
|
|
91
|
+
last_seen_at = ?,
|
|
92
|
+
page_views = page_views + ?,
|
|
93
|
+
interactions = interactions + ?
|
|
94
|
+
WHERE project_id = ? AND id = ?
|
|
95
|
+
`,
|
|
96
|
+
lastSeenAt,
|
|
97
|
+
plan.pageViews.length,
|
|
98
|
+
plan.interactions.length,
|
|
99
|
+
input.projectId,
|
|
100
|
+
state.visitId
|
|
101
|
+
)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
for (const pageView of plan.pageViews) {
|
|
105
|
+
this.run(
|
|
106
|
+
`
|
|
107
|
+
INSERT INTO activity_page_views (
|
|
108
|
+
project_id, visit_id, sequence, id, route, received_at, attribution
|
|
109
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
110
|
+
`,
|
|
111
|
+
input.projectId,
|
|
112
|
+
pageView.visitId,
|
|
113
|
+
pageView.sequence,
|
|
114
|
+
pageView.id,
|
|
115
|
+
pageView.route,
|
|
116
|
+
pageView.receivedAt.toISOString(),
|
|
117
|
+
serializeActivityAttribution(pageView.attribution)
|
|
118
|
+
)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
for (const interaction of plan.interactions) {
|
|
122
|
+
this.run(
|
|
123
|
+
`
|
|
124
|
+
INSERT INTO activity_interactions (
|
|
125
|
+
project_id, visit_id, sequence, id, name, route, received_at
|
|
126
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
127
|
+
`,
|
|
128
|
+
input.projectId,
|
|
129
|
+
interaction.visitId,
|
|
130
|
+
interaction.sequence,
|
|
131
|
+
interaction.id,
|
|
132
|
+
interaction.name,
|
|
133
|
+
interaction.route,
|
|
134
|
+
interaction.receivedAt.toISOString()
|
|
135
|
+
)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
this.run(
|
|
139
|
+
`
|
|
140
|
+
INSERT INTO activity_sources (
|
|
141
|
+
project_id, actor_type, session_id, visit_id, last_seen_at, sequence,
|
|
142
|
+
window_start, window_count
|
|
143
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
144
|
+
ON CONFLICT (project_id, actor_type, session_id) DO UPDATE SET
|
|
145
|
+
visit_id = excluded.visit_id,
|
|
146
|
+
last_seen_at = excluded.last_seen_at,
|
|
147
|
+
sequence = excluded.sequence,
|
|
148
|
+
window_start = excluded.window_start,
|
|
149
|
+
window_count = excluded.window_count
|
|
150
|
+
`,
|
|
151
|
+
input.projectId,
|
|
152
|
+
input.actor.type,
|
|
153
|
+
input.sessionId,
|
|
154
|
+
state.visitId,
|
|
155
|
+
lastSeenAt,
|
|
156
|
+
state.sequence,
|
|
157
|
+
iso(state.windowStart),
|
|
158
|
+
state.windowCount
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
return { status: "accepted", accepted: plan.accepted }
|
|
162
|
+
})
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async list(input: ListActivityVisitsInput): Promise<ActivityVisitsPage> {
|
|
166
|
+
const period = overlapping(input)
|
|
167
|
+
const before = cursorOf(input.before)
|
|
168
|
+
const rows = this.all<VisitRow>(
|
|
169
|
+
`
|
|
170
|
+
SELECT ${VISIT_COLUMNS}
|
|
171
|
+
FROM activity_visits v
|
|
172
|
+
WHERE ${period.where}
|
|
173
|
+
AND (v.last_seen_at < ? OR (v.last_seen_at = ? AND v.id < ?))
|
|
174
|
+
ORDER BY v.last_seen_at DESC, v.id DESC
|
|
175
|
+
LIMIT ?
|
|
176
|
+
`,
|
|
177
|
+
...period.params,
|
|
178
|
+
before.at,
|
|
179
|
+
before.at,
|
|
180
|
+
before.id,
|
|
181
|
+
input.limit + 1
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
return {
|
|
185
|
+
visits: rows.slice(0, input.limit).map(visitFromRow),
|
|
186
|
+
hasMore: rows.length > input.limit,
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async users(input: ListActivityUsersInput): Promise<ActivityUsersPage> {
|
|
191
|
+
const period = overlapping(input)
|
|
192
|
+
const before = cursorOf(input.before)
|
|
193
|
+
const rows = this.all<UserRow>(
|
|
194
|
+
`
|
|
195
|
+
SELECT
|
|
196
|
+
v.user_id,
|
|
197
|
+
MAX(v.last_seen_at) AS last_seen_at,
|
|
198
|
+
COUNT(DISTINCT v.id) AS visits,
|
|
199
|
+
COUNT(p.id) AS page_views
|
|
200
|
+
FROM activity_visits v
|
|
201
|
+
LEFT JOIN activity_page_views p
|
|
202
|
+
ON p.project_id = v.project_id
|
|
203
|
+
AND p.visit_id = v.id
|
|
204
|
+
AND p.received_at >= ?
|
|
205
|
+
AND p.received_at < ?
|
|
206
|
+
WHERE ${period.where} AND v.actor_type = 'user'
|
|
207
|
+
GROUP BY v.user_id
|
|
208
|
+
HAVING MAX(v.last_seen_at) < ? OR (MAX(v.last_seen_at) = ? AND v.user_id < ?)
|
|
209
|
+
ORDER BY last_seen_at DESC, v.user_id DESC
|
|
210
|
+
LIMIT ?
|
|
211
|
+
`,
|
|
212
|
+
input.from.toISOString(),
|
|
213
|
+
input.to.toISOString(),
|
|
214
|
+
...period.params,
|
|
215
|
+
before.at,
|
|
216
|
+
before.at,
|
|
217
|
+
before.id,
|
|
218
|
+
input.limit + 1
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
return {
|
|
222
|
+
users: rows.slice(0, input.limit).map((row) => ({
|
|
223
|
+
userId: row.user_id,
|
|
224
|
+
lastSeenAt: new Date(row.last_seen_at),
|
|
225
|
+
visits: Number(row.visits),
|
|
226
|
+
pageViews: Number(row.page_views),
|
|
227
|
+
})),
|
|
228
|
+
hasMore: rows.length > input.limit,
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
async summarize(input: ActivityPeriodInput): Promise<ActivitySummary> {
|
|
233
|
+
const period = overlapping(input)
|
|
234
|
+
const [row] = this.all<SummaryRow>(
|
|
235
|
+
`
|
|
236
|
+
SELECT
|
|
237
|
+
COUNT(DISTINCT v.user_id) AS users,
|
|
238
|
+
COUNT(DISTINCT v.id) AS visits,
|
|
239
|
+
COUNT(DISTINCT CASE WHEN v.actor_type = 'share' THEN v.id END) AS shared_visits,
|
|
240
|
+
COUNT(p.id) AS page_views
|
|
241
|
+
FROM activity_visits v
|
|
242
|
+
LEFT JOIN activity_page_views p
|
|
243
|
+
ON p.project_id = v.project_id
|
|
244
|
+
AND p.visit_id = v.id
|
|
245
|
+
AND p.received_at >= ?
|
|
246
|
+
AND p.received_at < ?
|
|
247
|
+
WHERE ${period.where}
|
|
248
|
+
`,
|
|
249
|
+
input.from.toISOString(),
|
|
250
|
+
input.to.toISOString(),
|
|
251
|
+
...period.params
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
return {
|
|
255
|
+
users: Number(row?.users ?? 0),
|
|
256
|
+
visits: Number(row?.visits ?? 0),
|
|
257
|
+
sharedVisits: Number(row?.shared_visits ?? 0),
|
|
258
|
+
pageViews: Number(row?.page_views ?? 0),
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
async calendar(input: ActivityCalendarInput): Promise<readonly ActivityDay[]> {
|
|
263
|
+
const period = overlapping(input)
|
|
264
|
+
const visits = this.all<{ started_at: string; last_seen_at: string }>(
|
|
265
|
+
`
|
|
266
|
+
SELECT v.started_at, v.last_seen_at
|
|
267
|
+
FROM activity_visits v
|
|
268
|
+
WHERE ${period.where}
|
|
269
|
+
`,
|
|
270
|
+
...period.params
|
|
271
|
+
)
|
|
272
|
+
const days = this.all<{ day: string; page_views: number }>(
|
|
273
|
+
`
|
|
274
|
+
SELECT substr(p.received_at, 1, 10) AS day, COUNT(*) AS page_views
|
|
275
|
+
FROM activity_visits v
|
|
276
|
+
JOIN activity_page_views p ON p.project_id = v.project_id AND p.visit_id = v.id
|
|
277
|
+
WHERE ${period.where}
|
|
278
|
+
AND p.received_at >= ?
|
|
279
|
+
AND p.received_at < ?
|
|
280
|
+
GROUP BY day
|
|
281
|
+
`,
|
|
282
|
+
...period.params,
|
|
283
|
+
input.from.toISOString(),
|
|
284
|
+
input.to.toISOString()
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
return activityCalendarDays(
|
|
288
|
+
input,
|
|
289
|
+
visits.map((visit) => ({
|
|
290
|
+
startedAt: new Date(visit.started_at),
|
|
291
|
+
lastSeenAt: new Date(visit.last_seen_at),
|
|
292
|
+
})),
|
|
293
|
+
new Map(days.map((row) => [row.day, Number(row.page_views)]))
|
|
294
|
+
)
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
async read(input: ReadActivityVisitInput): Promise<StoredActivityVisitPage | null> {
|
|
298
|
+
const [visit] = this.all<VisitRow & { readonly session_id: string }>(
|
|
299
|
+
`
|
|
300
|
+
SELECT ${VISIT_COLUMNS}, v.session_id
|
|
301
|
+
FROM activity_visits v
|
|
302
|
+
WHERE v.project_id = ? AND v.id = ?
|
|
303
|
+
`,
|
|
304
|
+
input.projectId,
|
|
305
|
+
input.id
|
|
306
|
+
)
|
|
307
|
+
if (!visit) {
|
|
308
|
+
return null
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Page views and interactions share the visit's sequence, so one bound covers both.
|
|
312
|
+
const rows = this.all<EventRow>(
|
|
313
|
+
`
|
|
314
|
+
SELECT id, visit_id, sequence, route, NULL AS name, received_at
|
|
315
|
+
FROM activity_page_views
|
|
316
|
+
WHERE project_id = ? AND visit_id = ?
|
|
317
|
+
UNION ALL
|
|
318
|
+
SELECT id, visit_id, sequence, route, name, received_at
|
|
319
|
+
FROM activity_interactions
|
|
320
|
+
WHERE project_id = ? AND visit_id = ?
|
|
321
|
+
ORDER BY sequence
|
|
322
|
+
LIMIT ?
|
|
323
|
+
`,
|
|
324
|
+
input.projectId,
|
|
325
|
+
input.id,
|
|
326
|
+
input.projectId,
|
|
327
|
+
input.id,
|
|
328
|
+
input.limit + 1
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
const pageViews: ActivityPageView[] = []
|
|
332
|
+
const interactions: ActivityInteraction[] = []
|
|
333
|
+
for (const row of rows.slice(0, input.limit)) {
|
|
334
|
+
if (row.name === null) {
|
|
335
|
+
pageViews.push(pageViewFromRow(row))
|
|
336
|
+
} else {
|
|
337
|
+
interactions.push({ ...pageViewFromRow(row), name: row.name })
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
return {
|
|
342
|
+
visit: visitFromRow(visit),
|
|
343
|
+
sessionId: visit.session_id,
|
|
344
|
+
pageViews,
|
|
345
|
+
interactions,
|
|
346
|
+
hasMore: rows.length > input.limit,
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
async sessionVisits(
|
|
351
|
+
input: ListActivitySessionVisitsInput
|
|
352
|
+
): Promise<readonly ActivitySessionVisits[]> {
|
|
353
|
+
if (input.sessionIds.length === 0) {
|
|
354
|
+
return []
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// A session opens a visit per return after inactivity, so each has a few to rank.
|
|
358
|
+
const rows = this.all<SessionVisitRow>(
|
|
359
|
+
`
|
|
360
|
+
SELECT *
|
|
361
|
+
FROM (
|
|
362
|
+
SELECT
|
|
363
|
+
${VISIT_COLUMNS},
|
|
364
|
+
v.session_id,
|
|
365
|
+
ROW_NUMBER() OVER (
|
|
366
|
+
PARTITION BY v.session_id ORDER BY v.started_at DESC, v.id DESC
|
|
367
|
+
) AS position,
|
|
368
|
+
COUNT(*) OVER (PARTITION BY v.session_id) AS total
|
|
369
|
+
FROM activity_visits v
|
|
370
|
+
WHERE v.project_id = ?
|
|
371
|
+
AND v.actor_type = 'user'
|
|
372
|
+
AND v.session_id IN (SELECT value FROM json_each(?))
|
|
373
|
+
AND v.user_id = ?
|
|
374
|
+
)
|
|
375
|
+
WHERE position <= ?
|
|
376
|
+
ORDER BY session_id, position
|
|
377
|
+
`,
|
|
378
|
+
input.projectId,
|
|
379
|
+
JSON.stringify(input.sessionIds),
|
|
380
|
+
input.userId,
|
|
381
|
+
input.limit
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
const bySession = new Map<string, { visits: number; latest: ActivityVisit[] }>()
|
|
385
|
+
for (const row of rows) {
|
|
386
|
+
const session = bySession.get(row.session_id) ?? { visits: Number(row.total), latest: [] }
|
|
387
|
+
session.latest.push(visitFromRow(row))
|
|
388
|
+
bySession.set(row.session_id, session)
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
return [...bySession].map(([sessionId, session]) => ({ sessionId, ...session }))
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
private source(input: AppendActivityInput): ActivitySourceState | undefined {
|
|
395
|
+
const [row] = this.all<SourceRow>(
|
|
396
|
+
`
|
|
397
|
+
SELECT
|
|
398
|
+
s.visit_id, v.started_at AS visit_started_at, s.last_seen_at, s.sequence,
|
|
399
|
+
s.window_start, s.window_count
|
|
400
|
+
FROM activity_sources s
|
|
401
|
+
LEFT JOIN activity_visits v ON v.project_id = s.project_id AND v.id = s.visit_id
|
|
402
|
+
WHERE s.project_id = ? AND s.actor_type = ? AND s.session_id = ?
|
|
403
|
+
`,
|
|
404
|
+
input.projectId,
|
|
405
|
+
input.actor.type,
|
|
406
|
+
input.sessionId
|
|
407
|
+
)
|
|
408
|
+
if (!row) {
|
|
409
|
+
return undefined
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// A deleted visit leaves its source behind; the next batch starts another visit.
|
|
413
|
+
return {
|
|
414
|
+
visitId: row.visit_id,
|
|
415
|
+
visitStartedAt: row.visit_started_at === null ? null : Date.parse(row.visit_started_at),
|
|
416
|
+
lastSeenAt: Date.parse(row.last_seen_at),
|
|
417
|
+
sequence: Number(row.sequence),
|
|
418
|
+
windowStart: Date.parse(row.window_start),
|
|
419
|
+
windowCount: Number(row.window_count),
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/** The session's page views and interactions with an id of the batch, to recognize retries. */
|
|
424
|
+
private storedEvents(input: AppendActivityInput): StoredActivityEvent[] {
|
|
425
|
+
const ids = JSON.stringify(input.events.map((event) => event.id))
|
|
426
|
+
const rows = this.all<{
|
|
427
|
+
id: string
|
|
428
|
+
route: string
|
|
429
|
+
name: string | null
|
|
430
|
+
attribution: string | null
|
|
431
|
+
}>(
|
|
432
|
+
`
|
|
433
|
+
SELECT p.id, p.route, NULL AS name, p.attribution
|
|
434
|
+
FROM activity_page_views p
|
|
435
|
+
JOIN activity_visits v ON v.project_id = p.project_id AND v.id = p.visit_id
|
|
436
|
+
WHERE p.project_id = ?
|
|
437
|
+
AND p.id IN (SELECT value FROM json_each(?))
|
|
438
|
+
AND v.actor_type = ?
|
|
439
|
+
AND v.session_id = ?
|
|
440
|
+
UNION ALL
|
|
441
|
+
SELECT i.id, i.route, i.name, NULL AS attribution
|
|
442
|
+
FROM activity_interactions i
|
|
443
|
+
JOIN activity_visits v ON v.project_id = i.project_id AND v.id = i.visit_id
|
|
444
|
+
WHERE i.project_id = ?
|
|
445
|
+
AND i.id IN (SELECT value FROM json_each(?))
|
|
446
|
+
AND v.actor_type = ?
|
|
447
|
+
AND v.session_id = ?
|
|
448
|
+
`,
|
|
449
|
+
input.projectId,
|
|
450
|
+
ids,
|
|
451
|
+
input.actor.type,
|
|
452
|
+
input.sessionId,
|
|
453
|
+
input.projectId,
|
|
454
|
+
ids,
|
|
455
|
+
input.actor.type,
|
|
456
|
+
input.sessionId
|
|
457
|
+
)
|
|
458
|
+
|
|
459
|
+
return rows.map((row) => {
|
|
460
|
+
if (row.name !== null) {
|
|
461
|
+
return { id: row.id, route: row.route, name: row.name }
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
const attribution = parseActivityAttribution(row.attribution)
|
|
465
|
+
return { id: row.id, route: row.route, ...(attribution ? { attribution } : {}) }
|
|
466
|
+
})
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
private run(sql: string, ...params: SQLQueryBindings[]): void {
|
|
470
|
+
this.db.query<unknown, SQLQueryBindings[]>(sql).run(...params)
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
private all<T>(sql: string, ...params: SQLQueryBindings[]): T[] {
|
|
474
|
+
return this.db.query<T, SQLQueryBindings[]>(sql).all(...params)
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
interface SourceRow {
|
|
479
|
+
readonly visit_id: string
|
|
480
|
+
readonly visit_started_at: string | null
|
|
481
|
+
readonly last_seen_at: string
|
|
482
|
+
readonly sequence: number
|
|
483
|
+
readonly window_start: string
|
|
484
|
+
readonly window_count: number
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
interface VisitRow {
|
|
488
|
+
readonly id: string
|
|
489
|
+
readonly actor_type: string
|
|
490
|
+
readonly user_id: string | null
|
|
491
|
+
readonly grant_id: string | null
|
|
492
|
+
readonly started_at: string
|
|
493
|
+
readonly last_seen_at: string
|
|
494
|
+
readonly page_views: number
|
|
495
|
+
readonly interactions: number
|
|
496
|
+
readonly attribution: string | null
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
interface SessionVisitRow extends VisitRow {
|
|
500
|
+
readonly session_id: string
|
|
501
|
+
readonly total: number
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
interface UserRow {
|
|
505
|
+
readonly user_id: string
|
|
506
|
+
readonly last_seen_at: string
|
|
507
|
+
readonly visits: number
|
|
508
|
+
readonly page_views: number
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
interface SummaryRow {
|
|
512
|
+
readonly users: number
|
|
513
|
+
readonly visits: number
|
|
514
|
+
readonly shared_visits: number
|
|
515
|
+
readonly page_views: number
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
interface PageViewRow {
|
|
519
|
+
readonly id: string
|
|
520
|
+
readonly visit_id: string
|
|
521
|
+
readonly sequence: number
|
|
522
|
+
readonly route: string
|
|
523
|
+
readonly received_at: string
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
/** A page view or, with a name, an interaction. */
|
|
527
|
+
interface EventRow extends PageViewRow {
|
|
528
|
+
readonly name: string | null
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/**
|
|
532
|
+
* The visits `v` active in the period, and only the account's when one is asked for. A visit lasts
|
|
533
|
+
* less than a day, so it started at most a day before the period: that bounds the index range.
|
|
534
|
+
*/
|
|
535
|
+
function overlapping(input: ActivityPeriodInput | ActivityCalendarInput): {
|
|
536
|
+
readonly where: string
|
|
537
|
+
readonly params: SQLQueryBindings[]
|
|
538
|
+
} {
|
|
539
|
+
const conditions = [
|
|
540
|
+
"v.project_id = ?",
|
|
541
|
+
"v.started_at >= ?",
|
|
542
|
+
"v.started_at < ?",
|
|
543
|
+
"v.last_seen_at >= ?",
|
|
544
|
+
]
|
|
545
|
+
const params: SQLQueryBindings[] = [
|
|
546
|
+
input.projectId,
|
|
547
|
+
new Date(input.from.getTime() - ACTIVITY_MAX_VISIT_MS).toISOString(),
|
|
548
|
+
input.to.toISOString(),
|
|
549
|
+
input.from.toISOString(),
|
|
550
|
+
]
|
|
551
|
+
|
|
552
|
+
if (input.userId !== undefined) {
|
|
553
|
+
conditions.push("v.user_id = ?")
|
|
554
|
+
params.push(input.userId)
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
return { where: conditions.join(" AND "), params }
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
function cursorOf(cursor: ActivityCursor | undefined): { at: string; id: string } {
|
|
561
|
+
return cursor ? { at: cursor.at.toISOString(), id: cursor.id } : NO_CURSOR
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
function visitFromRow(row: VisitRow): ActivityVisit {
|
|
565
|
+
return {
|
|
566
|
+
id: row.id,
|
|
567
|
+
actor: activityActor(row.actor_type, row.user_id, row.grant_id),
|
|
568
|
+
attribution: parseActivityAttribution(row.attribution),
|
|
569
|
+
startedAt: new Date(row.started_at),
|
|
570
|
+
lastSeenAt: new Date(row.last_seen_at),
|
|
571
|
+
pageViews: Number(row.page_views),
|
|
572
|
+
interactions: Number(row.interactions),
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
function pageViewFromRow(row: PageViewRow): ActivityPageView {
|
|
577
|
+
return {
|
|
578
|
+
id: row.id,
|
|
579
|
+
visitId: row.visit_id,
|
|
580
|
+
sequence: Number(row.sequence),
|
|
581
|
+
route: row.route,
|
|
582
|
+
receivedAt: new Date(row.received_at),
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
function iso(epochMs: number): string {
|
|
587
|
+
return new Date(epochMs).toISOString()
|
|
588
|
+
}
|
|
@@ -4,9 +4,12 @@ import {
|
|
|
4
4
|
openSqliteStoreConnection,
|
|
5
5
|
type SqliteStoreConnection,
|
|
6
6
|
} from "../transactions"
|
|
7
|
+
import { SqliteAgentApprovalStore } from "./approvals"
|
|
7
8
|
import { SqliteAgentContextCheckpointStore } from "./checkpoints"
|
|
9
|
+
import { SqliteAgentMemoryStore } from "./memory"
|
|
8
10
|
import { SqliteAgentMessageStore } from "./messages"
|
|
9
11
|
import { SqliteAgentRunStore } from "./runs"
|
|
12
|
+
import { SqliteAgentApprovalModeStore } from "./settings"
|
|
10
13
|
import { SqliteAgentThreadStore } from "./threads"
|
|
11
14
|
|
|
12
15
|
export interface SqliteAgentStorageOptions {
|
|
@@ -19,8 +22,8 @@ export interface SqliteAgentStorageOptions {
|
|
|
19
22
|
}
|
|
20
23
|
|
|
21
24
|
/**
|
|
22
|
-
* SQLite-backed agent persistence: thread, run, message,
|
|
23
|
-
* one connection. Cross-table writes run in SQLite transactions; the bundled {@link SqliteStorage}
|
|
25
|
+
* SQLite-backed agent persistence: thread, run, message, context-checkpoint, memory, approval and
|
|
26
|
+
* approval-mode sub-stores sharing one connection. Cross-table writes run in SQLite transactions; the bundled {@link SqliteStorage}
|
|
24
27
|
* serializes them for single-flight and checkpoint compare-and-swap safety.
|
|
25
28
|
*/
|
|
26
29
|
export class SqliteAgentStorage implements AgentStorage {
|
|
@@ -28,6 +31,9 @@ export class SqliteAgentStorage implements AgentStorage {
|
|
|
28
31
|
readonly runs: SqliteAgentRunStore
|
|
29
32
|
readonly messages: SqliteAgentMessageStore
|
|
30
33
|
readonly checkpoints: SqliteAgentContextCheckpointStore
|
|
34
|
+
readonly memory: SqliteAgentMemoryStore
|
|
35
|
+
readonly approvals: SqliteAgentApprovalStore
|
|
36
|
+
readonly approvalModes: SqliteAgentApprovalModeStore
|
|
31
37
|
|
|
32
38
|
private readonly connection: SqliteStoreConnection
|
|
33
39
|
|
|
@@ -38,6 +44,9 @@ export class SqliteAgentStorage implements AgentStorage {
|
|
|
38
44
|
this.runs = new SqliteAgentRunStore(this.connection.db, options.executions)
|
|
39
45
|
this.messages = new SqliteAgentMessageStore(this.connection.db)
|
|
40
46
|
this.checkpoints = new SqliteAgentContextCheckpointStore(this.connection.db)
|
|
47
|
+
this.memory = new SqliteAgentMemoryStore(this.connection.db)
|
|
48
|
+
this.approvals = new SqliteAgentApprovalStore(this.connection.db)
|
|
49
|
+
this.approvalModes = new SqliteAgentApprovalModeStore(this.connection.db)
|
|
41
50
|
}
|
|
42
51
|
|
|
43
52
|
close(): void {
|