@opengeni/core 0.21.10 → 0.28.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.
- package/dist/access/index.d.ts +1 -1
- package/dist/application/new-session-drafts.d.ts +1 -1
- package/dist/application/session-commands.d.ts +2 -2
- package/dist/billing/limits.d.ts +1 -1
- package/dist/canonical-human-identities.d.ts +12 -0
- package/dist/canonical-human-identities.js +23 -0
- package/dist/canonical-human-identities.js.map +1 -0
- package/dist/chunk-6WKPWE5S.js +2380 -0
- package/dist/chunk-6WKPWE5S.js.map +1 -0
- package/dist/chunk-IBOEYG6N.js +34 -0
- package/dist/chunk-IBOEYG6N.js.map +1 -0
- package/dist/chunk-JJU6I5XD.js +2713 -0
- package/dist/chunk-JJU6I5XD.js.map +1 -0
- package/dist/chunk-NYPMQ7NO.js +2270 -0
- package/dist/chunk-NYPMQ7NO.js.map +1 -0
- package/dist/dependencies.d.ts +30 -6
- package/dist/domain/capabilities.d.ts +28 -3
- package/dist/domain/company-profile-durable-learning-adapter.d.ts +39 -0
- package/dist/domain/conversation-integrations.d.ts +225 -0
- package/dist/domain/durable-learning-slack-publication.d.ts +72 -0
- package/dist/domain/editable-artifacts/agent-application.d.ts +192 -0
- package/dist/domain/editable-artifacts/durable-export-postgres.d.ts +4 -0
- package/dist/domain/editable-artifacts/durable-export.d.ts +212 -0
- package/dist/domain/editable-artifacts/errors.d.ts +47 -0
- package/dist/domain/editable-artifacts/hash.d.ts +11 -0
- package/dist/domain/editable-artifacts/in-memory.d.ts +99 -0
- package/dist/domain/editable-artifacts/index.d.ts +14 -0
- package/dist/domain/editable-artifacts/ports.d.ts +341 -0
- package/dist/domain/editable-artifacts/postgres-adapters.d.ts +13 -0
- package/dist/domain/editable-artifacts/service.d.ts +105 -0
- package/dist/domain/editable-artifacts/snapshot-compaction.d.ts +44 -0
- package/dist/domain/editable-artifacts/snapshot-verifier-genesis.d.ts +50 -0
- package/dist/domain/editable-artifacts/snapshot-verifier-materialization.d.ts +21 -0
- package/dist/domain/editable-artifacts/snapshot-verifier.d.ts +118 -0
- package/dist/domain/editable-artifacts/types.d.ts +410 -0
- package/dist/domain/fiken.d.ts +35 -0
- package/dist/domain/memory-slack-delivery.d.ts +19 -0
- package/dist/domain/packs.d.ts +29 -1
- package/dist/domain/personal-connection-delegations.d.ts +9 -1
- package/dist/domain/scheduled-tasks.d.ts +2 -2
- package/dist/domain/session-tool-policy.d.ts +2 -2
- package/dist/domain/sessions.d.ts +24 -8
- package/dist/domain/skill-imports.d.ts +32 -0
- package/dist/domain/slack-publication-secret-safety.d.ts +8 -0
- package/dist/domain/timeline-annotations.d.ts +5 -0
- package/dist/domain/video-generation-capabilities.d.ts +10 -0
- package/dist/domain/video-generation.d.ts +47 -0
- package/dist/editable-artifact-live/application.d.ts +69 -0
- package/dist/editable-artifact-live/errors.d.ts +1 -0
- package/dist/editable-artifact-live/index.d.ts +8 -0
- package/dist/editable-artifact-live/outbox-dispatcher.d.ts +162 -0
- package/dist/editable-artifact-live/ports.d.ts +100 -0
- package/dist/editable-artifact-live/server.d.ts +71 -0
- package/dist/editable-artifact-live/ticket.d.ts +40 -0
- package/dist/editable-artifact-live/types.d.ts +213 -0
- package/dist/editable-artifact-live/wire.d.ts +34 -0
- package/dist/editable-artifact-live.d.ts +2 -0
- package/dist/editable-artifact-live.js +58 -0
- package/dist/editable-artifact-live.js.map +1 -0
- package/dist/editable-artifacts.d.ts +2 -0
- package/dist/editable-artifacts.js +179 -0
- package/dist/editable-artifacts.js.map +1 -0
- package/dist/index.d.ts +37 -26
- package/dist/index.js +4145 -959
- package/dist/index.js.map +1 -1
- package/dist/managed-session.d.ts +6 -2
- package/dist/rigs/index.d.ts +1 -1
- package/dist/rigs/provider-images.d.ts +31 -0
- package/dist/sandbox/fleet.d.ts +8 -5
- package/dist/sandbox/routing.d.ts +1 -0
- package/dist/session-authorization.d.ts +4 -2
- package/dist/transcription.d.ts +5 -0
- package/dist/workflow-wake-contract.d.ts +7 -0
- package/package.json +22 -10
- package/src/access/index.ts +14 -2
- package/src/application/new-session-drafts.ts +2 -0
- package/src/application/session-commands.ts +188 -107
- package/src/canonical-human-identities.ts +27 -0
- package/src/dependencies.ts +34 -2
- package/src/domain/capabilities.ts +729 -408
- package/src/domain/company-profile-durable-learning-adapter.ts +221 -0
- package/src/domain/conversation-integrations.ts +1238 -0
- package/src/domain/durable-learning-slack-publication.ts +202 -0
- package/src/domain/editable-artifacts/agent-application.ts +687 -0
- package/src/domain/editable-artifacts/durable-export-postgres.ts +139 -0
- package/src/domain/editable-artifacts/durable-export.ts +734 -0
- package/src/domain/editable-artifacts/errors.ts +115 -0
- package/src/domain/editable-artifacts/hash.ts +131 -0
- package/src/domain/editable-artifacts/in-memory.ts +1662 -0
- package/src/domain/editable-artifacts/index.ts +14 -0
- package/src/domain/editable-artifacts/ports.ts +462 -0
- package/src/domain/editable-artifacts/postgres-adapters.ts +49 -0
- package/src/domain/editable-artifacts/service.ts +2402 -0
- package/src/domain/editable-artifacts/snapshot-compaction.ts +246 -0
- package/src/domain/editable-artifacts/snapshot-verifier-genesis.ts +235 -0
- package/src/domain/editable-artifacts/snapshot-verifier-materialization.ts +74 -0
- package/src/domain/editable-artifacts/snapshot-verifier.ts +687 -0
- package/src/domain/editable-artifacts/types.ts +835 -0
- package/src/domain/fiken.ts +88 -0
- package/src/domain/insights.ts +163 -50
- package/src/domain/memory-slack-delivery.ts +204 -0
- package/src/domain/memory-slack-publication.ts +5 -2
- package/src/domain/packs.ts +266 -5
- package/src/domain/personal-connection-delegations.ts +97 -6
- package/src/domain/scheduled-tasks.ts +171 -29
- package/src/domain/session-tool-policy.ts +8 -2
- package/src/domain/sessions.ts +395 -148
- package/src/domain/skill-imports.ts +350 -0
- package/src/domain/slack-publication-secret-safety.ts +42 -0
- package/src/domain/timeline-annotations.ts +204 -0
- package/src/domain/video-generation-capabilities.ts +104 -0
- package/src/domain/video-generation.ts +249 -0
- package/src/editable-artifact-live/application.ts +182 -0
- package/src/editable-artifact-live/errors.ts +4 -0
- package/src/editable-artifact-live/index.ts +8 -0
- package/src/editable-artifact-live/outbox-dispatcher.ts +916 -0
- package/src/editable-artifact-live/ports.ts +139 -0
- package/src/editable-artifact-live/server.ts +1707 -0
- package/src/editable-artifact-live/ticket.ts +217 -0
- package/src/editable-artifact-live/types.ts +286 -0
- package/src/editable-artifact-live/wire.ts +95 -0
- package/src/editable-artifact-live.ts +2 -0
- package/src/editable-artifacts.ts +2 -0
- package/src/index.ts +15 -0
- package/src/managed-session.ts +19 -2
- package/src/rigs/index.ts +26 -19
- package/src/rigs/provider-images.ts +110 -0
- package/src/sandbox/fleet.ts +72 -66
- package/src/sandbox/routing.ts +6 -3
- package/src/session-authorization.ts +187 -52
- package/src/transcription.ts +5 -0
- package/src/workflow-wake-contract.ts +8 -0
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import {
|
|
2
|
+
FIKEN_CREDENTIAL_LABEL,
|
|
3
|
+
FIKEN_CREDENTIAL_ROLE,
|
|
4
|
+
FIKEN_PROVIDER_DOMAIN,
|
|
5
|
+
FikenConnectionMetadata,
|
|
6
|
+
type ConnectionMetadata,
|
|
7
|
+
type FikenCompanySummary,
|
|
8
|
+
type FikenConnectionMetadata as FikenMetadata,
|
|
9
|
+
} from "@opengeni/contracts";
|
|
10
|
+
|
|
11
|
+
export function fikenConnectionMetadata(metadata: Record<string, unknown>): FikenMetadata | null {
|
|
12
|
+
const parsed = FikenConnectionMetadata.safeParse(metadata);
|
|
13
|
+
return parsed.success ? parsed.data : null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* A workspace-shared Fiken connection created through one of the verified
|
|
18
|
+
* install paths: the pasted personal API token (`api_key`) or the Fiken OAuth
|
|
19
|
+
* app flow (`oauth2`). Both are deliberately workspace-owned; personal
|
|
20
|
+
* ("Connect only for me") ownership needs the delegation-snapshot lane and is
|
|
21
|
+
* not yet wired for the first-party fiken tools.
|
|
22
|
+
*/
|
|
23
|
+
export function isFikenConnection(connection: ConnectionMetadata): boolean {
|
|
24
|
+
return (
|
|
25
|
+
connection.subjectId === null &&
|
|
26
|
+
connection.providerDomain === FIKEN_PROVIDER_DOMAIN &&
|
|
27
|
+
(connection.kind === "api_key" || connection.kind === "oauth2") &&
|
|
28
|
+
fikenConnectionMetadata(connection.metadata)?.credentialRole === FIKEN_CREDENTIAL_ROLE
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The Fiken credential role is reserved for the verified install routes.
|
|
34
|
+
* Generic connection create/update must reject it so a caller cannot forge a
|
|
35
|
+
* "verified" Fiken row (with attacker-chosen companies metadata) that the
|
|
36
|
+
* first-party fiken tools would then bind to. Mirrors the Slack bot guard.
|
|
37
|
+
*/
|
|
38
|
+
export function hasReservedFikenMetadata(
|
|
39
|
+
metadata: Record<string, unknown> | null | undefined,
|
|
40
|
+
): boolean {
|
|
41
|
+
return (
|
|
42
|
+
metadata?.credentialRole === FIKEN_CREDENTIAL_ROLE ||
|
|
43
|
+
metadata?.credentialLabel === FIKEN_CREDENTIAL_LABEL
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* One default-company rule for both verified lanes: an explicit request wins
|
|
49
|
+
* when the credential can access it, then a still-accessible previous default
|
|
50
|
+
* survives reconnect, then a single-company credential auto-selects. Returns
|
|
51
|
+
* null (caller decides whether that is an error) when a requested slug is not
|
|
52
|
+
* among the verified companies.
|
|
53
|
+
*/
|
|
54
|
+
export function resolveFikenDefaultCompanySlug(input: {
|
|
55
|
+
requested: string | null;
|
|
56
|
+
previous: string | null;
|
|
57
|
+
companies: readonly FikenCompanySummary[];
|
|
58
|
+
}): string | null {
|
|
59
|
+
const accessible = (slug: string | null): slug is string =>
|
|
60
|
+
slug !== null && input.companies.some((company) => company.slug === slug);
|
|
61
|
+
if (input.requested !== null) {
|
|
62
|
+
return accessible(input.requested) ? input.requested : null;
|
|
63
|
+
}
|
|
64
|
+
if (accessible(input.previous)) {
|
|
65
|
+
return input.previous;
|
|
66
|
+
}
|
|
67
|
+
return input.companies.length === 1 ? input.companies[0]!.slug : null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* The row a capability tile or tool default should bind to when several Fiken
|
|
72
|
+
* connections exist: usable status first, then newest update, then immutable
|
|
73
|
+
* UUID descending as the stable tie-breaker.
|
|
74
|
+
*/
|
|
75
|
+
export function preferredFikenConnection<
|
|
76
|
+
T extends Pick<ConnectionMetadata, "status" | "updatedAt" | "id">,
|
|
77
|
+
>(connections: readonly T[]): T | null {
|
|
78
|
+
const statusRank = (status: ConnectionMetadata["status"]): number =>
|
|
79
|
+
status === "active" ? 0 : status === "needs_reauth" ? 1 : 2;
|
|
80
|
+
return (
|
|
81
|
+
[...connections].sort(
|
|
82
|
+
(left, right) =>
|
|
83
|
+
statusRank(left.status) - statusRank(right.status) ||
|
|
84
|
+
right.updatedAt.localeCompare(left.updatedAt) ||
|
|
85
|
+
right.id.localeCompare(left.id),
|
|
86
|
+
)[0] ?? null
|
|
87
|
+
);
|
|
88
|
+
}
|
package/src/domain/insights.ts
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
import {
|
|
8
8
|
aggregateModelCallFacts,
|
|
9
9
|
aggregateModelCallFactsByDay,
|
|
10
|
+
aggregateModelCallFactsByHour,
|
|
10
11
|
aggregateRootSessionDrivers,
|
|
11
12
|
aggregateScheduleFacts,
|
|
12
13
|
aggregateSessionDepth,
|
|
@@ -15,13 +16,16 @@ import {
|
|
|
15
16
|
countScheduledTaskFires,
|
|
16
17
|
countSessionsAttachedToGroups,
|
|
17
18
|
enumerateUtcDays,
|
|
19
|
+
enumerateUtcHours,
|
|
18
20
|
listFloorSessions,
|
|
19
21
|
listLiveWarmLeases,
|
|
20
22
|
listModelCallFacets,
|
|
23
|
+
listRecentModelCalls,
|
|
21
24
|
listScheduledTasks,
|
|
22
25
|
requireWorkspace,
|
|
23
26
|
sumUsageQuantity,
|
|
24
27
|
sumUsageQuantityByDay,
|
|
28
|
+
sumUsageQuantityByHour,
|
|
25
29
|
sumUsageQuantityInRange,
|
|
26
30
|
type Database,
|
|
27
31
|
} from "@opengeni/db";
|
|
@@ -37,7 +41,7 @@ export type GetWorkspaceInsightsInput = {
|
|
|
37
41
|
};
|
|
38
42
|
|
|
39
43
|
function microsToUsd(micros: number): number {
|
|
40
|
-
return
|
|
44
|
+
return micros / 1_000_000;
|
|
41
45
|
}
|
|
42
46
|
|
|
43
47
|
function cacheHitPct(cached: number, input: number): number {
|
|
@@ -81,29 +85,29 @@ function resolveRangeWindow(
|
|
|
81
85
|
since = startOfUtcDay(now);
|
|
82
86
|
rangeLabel = "Today (UTC)";
|
|
83
87
|
priorLabel = "Prior equal window";
|
|
84
|
-
seriesLabel = "Credit $
|
|
85
|
-
cacheSeriesLabel = "Cache hit %";
|
|
88
|
+
seriesLabel = "Credit $ / UTC hour";
|
|
89
|
+
cacheSeriesLabel = "Cache hit % / UTC hour";
|
|
86
90
|
break;
|
|
87
91
|
case "week":
|
|
88
92
|
since = new Date(startOfUtcDay(now).getTime() - 6 * 24 * 60 * 60 * 1000);
|
|
89
93
|
rangeLabel = "Last 7 days (UTC)";
|
|
90
94
|
priorLabel = "Prior 7 days";
|
|
91
|
-
seriesLabel = "Credit $ / day";
|
|
92
|
-
cacheSeriesLabel = "Cache hit % / day";
|
|
95
|
+
seriesLabel = "Credit $ / UTC day";
|
|
96
|
+
cacheSeriesLabel = "Cache hit % / UTC day";
|
|
93
97
|
break;
|
|
94
98
|
case "month":
|
|
95
99
|
since = startOfUtcMonth(now);
|
|
96
100
|
rangeLabel = "This month (UTC)";
|
|
97
101
|
priorLabel = "Prior equal window";
|
|
98
|
-
seriesLabel = "Credit $ / day";
|
|
99
|
-
cacheSeriesLabel = "Cache hit % / day";
|
|
102
|
+
seriesLabel = "Credit $ / UTC day";
|
|
103
|
+
cacheSeriesLabel = "Cache hit % / UTC day";
|
|
100
104
|
break;
|
|
101
105
|
case "ytd":
|
|
102
106
|
since = startOfUtcYear(now);
|
|
103
107
|
rangeLabel = "Year to date (UTC)";
|
|
104
108
|
priorLabel = "Prior equal window";
|
|
105
|
-
seriesLabel = "Credit $ / day";
|
|
106
|
-
cacheSeriesLabel = "Cache hit % / day";
|
|
109
|
+
seriesLabel = "Credit $ / UTC day";
|
|
110
|
+
cacheSeriesLabel = "Cache hit % / UTC day";
|
|
107
111
|
break;
|
|
108
112
|
default: {
|
|
109
113
|
const _exhaustive: never = range;
|
|
@@ -163,6 +167,12 @@ function billingPathOf(value: string): "opengeni_credits" | "external" {
|
|
|
163
167
|
return value === "external" ? "external" : "opengeni_credits";
|
|
164
168
|
}
|
|
165
169
|
|
|
170
|
+
function pricingSourceOf(
|
|
171
|
+
value: string | null,
|
|
172
|
+
): "configured_list_price" | "gateway_reported" | null {
|
|
173
|
+
return value === "configured_list_price" || value === "gateway_reported" ? value : null;
|
|
174
|
+
}
|
|
175
|
+
|
|
166
176
|
export async function getWorkspaceInsights(
|
|
167
177
|
db: Database,
|
|
168
178
|
settings: Settings,
|
|
@@ -175,6 +185,10 @@ export async function getWorkspaceInsights(
|
|
|
175
185
|
const model = input.model?.trim() || null;
|
|
176
186
|
const modelFilterActive = Boolean(provider || model);
|
|
177
187
|
const filter = { provider, model };
|
|
188
|
+
const aggregateFactsForSeries =
|
|
189
|
+
input.range === "today" ? aggregateModelCallFactsByHour : aggregateModelCallFactsByDay;
|
|
190
|
+
const sumUsageForSeries =
|
|
191
|
+
input.range === "today" ? sumUsageQuantityByHour : sumUsageQuantityByDay;
|
|
178
192
|
|
|
179
193
|
const [
|
|
180
194
|
workspaceCreditMicros,
|
|
@@ -197,6 +211,7 @@ export async function getWorkspaceInsights(
|
|
|
197
211
|
billableTokensUsed,
|
|
198
212
|
agentRunsUsed,
|
|
199
213
|
facets,
|
|
214
|
+
recentCalls,
|
|
200
215
|
] = await Promise.all([
|
|
201
216
|
sumUsageQuantityInRange(db, {
|
|
202
217
|
workspaceId: input.workspaceId,
|
|
@@ -234,19 +249,19 @@ export async function getWorkspaceInsights(
|
|
|
234
249
|
until: window.priorUntil,
|
|
235
250
|
...filter,
|
|
236
251
|
}),
|
|
237
|
-
|
|
252
|
+
aggregateFactsForSeries(db, {
|
|
238
253
|
workspaceId: input.workspaceId,
|
|
239
254
|
since: window.since,
|
|
240
255
|
until: window.until,
|
|
241
256
|
...filter,
|
|
242
257
|
}),
|
|
243
|
-
|
|
258
|
+
sumUsageForSeries(db, {
|
|
244
259
|
workspaceId: input.workspaceId,
|
|
245
260
|
eventType: "sandbox.warm_seconds",
|
|
246
261
|
since: window.since,
|
|
247
262
|
until: window.until,
|
|
248
263
|
}),
|
|
249
|
-
|
|
264
|
+
sumUsageForSeries(db, {
|
|
250
265
|
workspaceId: input.workspaceId,
|
|
251
266
|
eventType: "model.cost",
|
|
252
267
|
since: window.since,
|
|
@@ -276,7 +291,7 @@ export async function getWorkspaceInsights(
|
|
|
276
291
|
aggregateSessionDepth(db, input.workspaceId),
|
|
277
292
|
listFloorSessions(db, input.workspaceId, 24),
|
|
278
293
|
settings.sandboxSelfhostedEnabled
|
|
279
|
-
? countOnlineMachines(db, input.workspaceId, MACHINE_HEARTBEAT_FRESH_MS)
|
|
294
|
+
? countOnlineMachines(db, input.workspaceId, MACHINE_HEARTBEAT_FRESH_MS, now)
|
|
280
295
|
: Promise.resolve(0),
|
|
281
296
|
sumUsageQuantity(db, {
|
|
282
297
|
workspaceId: input.workspaceId,
|
|
@@ -293,23 +308,37 @@ export async function getWorkspaceInsights(
|
|
|
293
308
|
since: window.since,
|
|
294
309
|
until: window.until,
|
|
295
310
|
}),
|
|
311
|
+
listRecentModelCalls(db, {
|
|
312
|
+
workspaceId: input.workspaceId,
|
|
313
|
+
since: window.since,
|
|
314
|
+
until: window.until,
|
|
315
|
+
...filter,
|
|
316
|
+
limit: 50,
|
|
317
|
+
}),
|
|
296
318
|
]);
|
|
297
319
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
320
|
+
const [priorRootDrivers, attached, fireCounts] = await Promise.all([
|
|
321
|
+
// Exact prior costs for the current top drivers — never a separate top-N page that
|
|
322
|
+
// drops roots and invents +$full as "new" spend.
|
|
323
|
+
aggregateRootSessionDrivers(db, {
|
|
324
|
+
workspaceId: input.workspaceId,
|
|
325
|
+
since: window.priorSince,
|
|
326
|
+
until: window.priorUntil,
|
|
327
|
+
...filter,
|
|
328
|
+
rootSessionIds: rootDrivers.map((row) => row.rootSessionId),
|
|
329
|
+
}),
|
|
330
|
+
countSessionsAttachedToGroups(
|
|
331
|
+
db,
|
|
332
|
+
input.workspaceId,
|
|
333
|
+
warmGroups.map((group) => group.groupId),
|
|
334
|
+
),
|
|
335
|
+
countScheduledTaskFires(db, {
|
|
336
|
+
workspaceId: input.workspaceId,
|
|
337
|
+
since: window.since,
|
|
338
|
+
until: window.until,
|
|
339
|
+
taskIds: tasks.map((task) => task.id),
|
|
340
|
+
}),
|
|
341
|
+
]);
|
|
313
342
|
const backendByGroup = new Map(liveWarm.map((lease) => [lease.groupId, lease.backend]));
|
|
314
343
|
const warmSecondsByGroup = new Map(warmGroups.map((group) => [group.groupId, group.warmSeconds]));
|
|
315
344
|
|
|
@@ -323,36 +352,82 @@ export async function getWorkspaceInsights(
|
|
|
323
352
|
inputTokens: row.inputTokens,
|
|
324
353
|
outputTokens: row.outputTokens,
|
|
325
354
|
cachedTokens: row.cachedTokens,
|
|
355
|
+
cacheInputTokens: row.cacheInputTokens,
|
|
326
356
|
cacheWriteTokens: row.cacheWriteTokens,
|
|
327
357
|
reasoningTokens: row.reasoningTokens,
|
|
358
|
+
totalTokens: row.totalTokens,
|
|
359
|
+
tokenKnownCalls: row.tokenKnownCalls,
|
|
360
|
+
cacheKnownCalls: row.cacheKnownCalls,
|
|
328
361
|
creditUsd: microsToUsd(row.pricedCostMicros),
|
|
362
|
+
estimatedProviderUsd: microsToUsd(row.estimatedProviderCostMicros),
|
|
363
|
+
estimatedProviderCostKnownCalls: row.estimatedProviderCostKnownCalls,
|
|
329
364
|
}))
|
|
330
|
-
.sort((a, b) => b.
|
|
365
|
+
.sort((a, b) => b.totalTokens - a.totalTokens);
|
|
331
366
|
|
|
332
367
|
const creditMicros = modelRows.reduce((sum, row) => sum + row.pricedCostMicros, 0);
|
|
333
368
|
const priorCreditMicros = priorModelRows.reduce((sum, row) => sum + row.pricedCostMicros, 0);
|
|
369
|
+
const estimatedProviderCostMicros = modelRows.reduce(
|
|
370
|
+
(sum, row) => sum + row.estimatedProviderCostMicros,
|
|
371
|
+
0,
|
|
372
|
+
);
|
|
373
|
+
const priorEstimatedProviderCostMicros = priorModelRows.reduce(
|
|
374
|
+
(sum, row) => sum + row.estimatedProviderCostMicros,
|
|
375
|
+
0,
|
|
376
|
+
);
|
|
377
|
+
const estimatedProviderCostKnownCalls = modelRows.reduce(
|
|
378
|
+
(sum, row) => sum + row.estimatedProviderCostKnownCalls,
|
|
379
|
+
0,
|
|
380
|
+
);
|
|
381
|
+
const priorEstimatedProviderCostKnownCalls = priorModelRows.reduce(
|
|
382
|
+
(sum, row) => sum + row.estimatedProviderCostKnownCalls,
|
|
383
|
+
0,
|
|
384
|
+
);
|
|
385
|
+
const modelCalls = modelRows.reduce((sum, row) => sum + row.calls, 0);
|
|
334
386
|
const priorInputTokens = priorModelRows.reduce((sum, row) => sum + row.inputTokens, 0);
|
|
387
|
+
const priorTotalTokens = priorModelRows.reduce((sum, row) => sum + row.totalTokens, 0);
|
|
335
388
|
const priorCachedTokens = priorModelRows.reduce((sum, row) => sum + row.cachedTokens, 0);
|
|
389
|
+
const priorCacheInputTokens = priorModelRows.reduce((sum, row) => sum + row.cacheInputTokens, 0);
|
|
336
390
|
const priorCalls = priorModelRows.reduce((sum, row) => sum + row.calls, 0);
|
|
337
391
|
|
|
338
|
-
const
|
|
339
|
-
|
|
340
|
-
|
|
392
|
+
const buckets =
|
|
393
|
+
input.range === "today"
|
|
394
|
+
? enumerateUtcHours(window.since, window.until)
|
|
395
|
+
: enumerateUtcDays(window.since, window.until);
|
|
396
|
+
const series = buckets.map((bucket) => {
|
|
397
|
+
const facts = factDays.get(bucket) ?? {
|
|
341
398
|
costMicros: 0,
|
|
399
|
+
estimatedProviderCostMicros: 0,
|
|
400
|
+
estimatedProviderCostKnownCalls: 0,
|
|
342
401
|
inputTokens: 0,
|
|
402
|
+
outputTokens: 0,
|
|
343
403
|
cachedTokens: 0,
|
|
404
|
+
cacheInputTokens: 0,
|
|
405
|
+
cacheWriteTokens: 0,
|
|
406
|
+
reasoningTokens: 0,
|
|
407
|
+
totalTokens: 0,
|
|
408
|
+
tokenKnownCalls: 0,
|
|
409
|
+
cacheKnownCalls: 0,
|
|
344
410
|
calls: 0,
|
|
345
411
|
};
|
|
346
412
|
const modelCostMicros = modelFilterActive
|
|
347
413
|
? facts.costMicros
|
|
348
|
-
: (costDays.get(
|
|
414
|
+
: (costDays.get(bucket) ?? facts.costMicros);
|
|
349
415
|
return {
|
|
350
|
-
label:
|
|
416
|
+
label: input.range === "today" ? bucket.slice(11) : bucket.slice(5),
|
|
351
417
|
modelCostUsd: microsToUsd(modelCostMicros),
|
|
352
|
-
|
|
418
|
+
estimatedProviderUsd: microsToUsd(facts.estimatedProviderCostMicros),
|
|
419
|
+
estimatedProviderCostKnownCalls: facts.estimatedProviderCostKnownCalls,
|
|
420
|
+
warmSeconds: warmDays.get(bucket) ?? 0,
|
|
353
421
|
inputTokens: facts.inputTokens,
|
|
422
|
+
outputTokens: facts.outputTokens,
|
|
354
423
|
cachedTokens: facts.cachedTokens,
|
|
355
|
-
|
|
424
|
+
cacheInputTokens: facts.cacheInputTokens,
|
|
425
|
+
cacheWriteTokens: facts.cacheWriteTokens,
|
|
426
|
+
reasoningTokens: facts.reasoningTokens,
|
|
427
|
+
totalTokens: facts.totalTokens,
|
|
428
|
+
tokenKnownCalls: facts.tokenKnownCalls,
|
|
429
|
+
cacheKnownCalls: facts.cacheKnownCalls,
|
|
430
|
+
cacheHitPct: cacheHitPct(facts.cachedTokens, facts.cacheInputTokens),
|
|
356
431
|
calls: facts.calls,
|
|
357
432
|
};
|
|
358
433
|
});
|
|
@@ -360,7 +435,10 @@ export async function getWorkspaceInsights(
|
|
|
360
435
|
const priorDriverByRoot = new Map(
|
|
361
436
|
priorRootDrivers.map((row) => [row.rootSessionId, row.pricedCostMicros]),
|
|
362
437
|
);
|
|
363
|
-
const
|
|
438
|
+
const totalTokensForPct = Math.max(
|
|
439
|
+
rootDrivers.reduce((sum, row) => sum + row.totalTokens, 0),
|
|
440
|
+
1,
|
|
441
|
+
);
|
|
364
442
|
const drivers = rootDrivers.map((row) => {
|
|
365
443
|
const creditUsd = microsToUsd(row.pricedCostMicros);
|
|
366
444
|
const priorUsd = microsToUsd(priorDriverByRoot.get(row.rootSessionId) ?? 0);
|
|
@@ -369,19 +447,19 @@ export async function getWorkspaceInsights(
|
|
|
369
447
|
groupBy: "root_session" as const,
|
|
370
448
|
label: row.title?.trim() || row.rootSessionId.slice(0, 8),
|
|
371
449
|
creditUsd,
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
450
|
+
estimatedProviderUsd: microsToUsd(row.estimatedProviderCostMicros),
|
|
451
|
+
estimatedProviderCostKnownCalls: row.estimatedProviderCostKnownCalls,
|
|
452
|
+
tokens: row.totalTokens,
|
|
453
|
+
cacheHitPct: cacheHitPct(row.cachedTokens, row.cacheInputTokens),
|
|
454
|
+
pctOfCreditUsd:
|
|
455
|
+
creditMicros > 0
|
|
456
|
+
? Math.min(100, Math.round((row.pricedCostMicros / creditMicros) * 100))
|
|
457
|
+
: 0,
|
|
458
|
+
pctOfTokens: Math.min(100, Math.round((row.totalTokens / totalTokensForPct) * 100)),
|
|
459
|
+
deltaUsdVsPrior: creditUsd - priorUsd,
|
|
376
460
|
};
|
|
377
461
|
});
|
|
378
462
|
|
|
379
|
-
const fireCounts = await countScheduledTaskFires(db, {
|
|
380
|
-
workspaceId: input.workspaceId,
|
|
381
|
-
since: window.since,
|
|
382
|
-
until: window.until,
|
|
383
|
-
taskIds: tasks.map((task) => task.id),
|
|
384
|
-
});
|
|
385
463
|
const scheduleFactById = new Map(scheduleFacts.map((row) => [row.scheduledTaskId, row] as const));
|
|
386
464
|
const schedules = tasks.map((task) => {
|
|
387
465
|
const fact = scheduleFactById.get(task.id);
|
|
@@ -390,8 +468,10 @@ export async function getWorkspaceInsights(
|
|
|
390
468
|
name: task.name,
|
|
391
469
|
fires: fireCounts.get(task.id) ?? 0,
|
|
392
470
|
creditUsd: fact ? microsToUsd(fact.pricedCostMicros) : null,
|
|
393
|
-
|
|
394
|
-
|
|
471
|
+
estimatedProviderUsd: fact ? microsToUsd(fact.estimatedProviderCostMicros) : null,
|
|
472
|
+
estimatedProviderCostKnownCalls: fact ? fact.estimatedProviderCostKnownCalls : null,
|
|
473
|
+
tokens: fact ? fact.totalTokens : null,
|
|
474
|
+
cacheHitPct: fact ? cacheHitPct(fact.cachedTokens, fact.cacheInputTokens) : null,
|
|
395
475
|
billing: fact ? billingPathOf(fact.billingPath) : null,
|
|
396
476
|
};
|
|
397
477
|
});
|
|
@@ -422,6 +502,9 @@ export async function getWorkspaceInsights(
|
|
|
422
502
|
priorLabel: window.priorLabel,
|
|
423
503
|
seriesLabel: window.seriesLabel,
|
|
424
504
|
cacheSeriesLabel: window.cacheSeriesLabel,
|
|
505
|
+
windowStart: window.since.toISOString(),
|
|
506
|
+
windowEnd: window.until.toISOString(),
|
|
507
|
+
generatedAt: now.toISOString(),
|
|
425
508
|
timezone: "UTC",
|
|
426
509
|
models,
|
|
427
510
|
facets,
|
|
@@ -432,6 +515,30 @@ export async function getWorkspaceInsights(
|
|
|
432
515
|
})),
|
|
433
516
|
drivers,
|
|
434
517
|
schedules,
|
|
518
|
+
recentCalls: recentCalls.map((row) => ({
|
|
519
|
+
id: row.id,
|
|
520
|
+
occurredAt: row.occurredAt.toISOString(),
|
|
521
|
+
recordedAt: row.recordedAt.toISOString(),
|
|
522
|
+
sessionId: row.sessionId,
|
|
523
|
+
sessionTitle: row.sessionTitle?.trim() || "Untitled session",
|
|
524
|
+
turnId: row.turnId,
|
|
525
|
+
provider: row.provider,
|
|
526
|
+
providerApi: row.providerApi,
|
|
527
|
+
model: row.model,
|
|
528
|
+
billing: billingPathOf(row.billingPath),
|
|
529
|
+
inputTokens: row.inputTokens,
|
|
530
|
+
outputTokens: row.outputTokens,
|
|
531
|
+
cachedTokens: row.cachedTokens,
|
|
532
|
+
cacheWriteTokens: row.cacheWriteTokens,
|
|
533
|
+
reasoningTokens: row.reasoningTokens,
|
|
534
|
+
totalTokens: row.totalTokens,
|
|
535
|
+
creditUsd: microsToUsd(row.pricedCostMicros),
|
|
536
|
+
estimatedProviderUsd:
|
|
537
|
+
row.estimatedProviderCostMicros == null
|
|
538
|
+
? null
|
|
539
|
+
: microsToUsd(row.estimatedProviderCostMicros),
|
|
540
|
+
pricingSource: pricingSourceOf(row.pricingSource),
|
|
541
|
+
})),
|
|
435
542
|
warmSeconds,
|
|
436
543
|
priorWarmSeconds,
|
|
437
544
|
warmGroups: warmGroups.map((group) => ({
|
|
@@ -458,8 +565,14 @@ export async function getWorkspaceInsights(
|
|
|
458
565
|
priorWorkspaceCreditUsd: microsToUsd(priorWorkspaceCreditMicros),
|
|
459
566
|
creditUsd: microsToUsd(creditMicros),
|
|
460
567
|
priorCreditUsd: microsToUsd(priorCreditMicros),
|
|
568
|
+
estimatedProviderUsd: microsToUsd(estimatedProviderCostMicros),
|
|
569
|
+
priorEstimatedProviderUsd: microsToUsd(priorEstimatedProviderCostMicros),
|
|
570
|
+
estimatedProviderCostKnownCalls,
|
|
571
|
+
priorEstimatedProviderCostKnownCalls,
|
|
572
|
+
modelCalls,
|
|
461
573
|
priorInputTokens,
|
|
462
|
-
|
|
574
|
+
priorTotalTokens,
|
|
575
|
+
priorCacheHitPct: cacheHitPct(priorCachedTokens, priorCacheInputTokens),
|
|
463
576
|
priorCalls,
|
|
464
577
|
goalsActive: depth.goalsActive,
|
|
465
578
|
goalsCompleted: depth.goalsCompleted,
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import type { MemorySlackPublicationDistribution } from "@opengeni/contracts";
|
|
2
|
+
import {
|
|
3
|
+
correctWorkspaceMemory,
|
|
4
|
+
enqueueMemorySlackPublication,
|
|
5
|
+
getCurrentMemorySlackPublicationConfiguration,
|
|
6
|
+
getWorkspaceMemorySlackPublicationSnapshot,
|
|
7
|
+
saveWorkspaceMemory,
|
|
8
|
+
withWorkspaceRls,
|
|
9
|
+
type CorrectWorkspaceMemoryInput,
|
|
10
|
+
type CorrectWorkspaceMemoryResult,
|
|
11
|
+
type Database,
|
|
12
|
+
type EnqueueMemorySlackPublicationResult,
|
|
13
|
+
type MemoryEmbedder,
|
|
14
|
+
type MemorySlackPublicationActor,
|
|
15
|
+
type SaveWorkspaceMemoryInput,
|
|
16
|
+
type SaveWorkspaceMemoryResult,
|
|
17
|
+
type WorkspaceMemorySlackPublicationSnapshot,
|
|
18
|
+
} from "@opengeni/db";
|
|
19
|
+
import {
|
|
20
|
+
evaluateMemorySlackPublication,
|
|
21
|
+
type MemorySlackOrigin,
|
|
22
|
+
type MemorySlackPublicationDecision,
|
|
23
|
+
} from "./memory-slack-publication";
|
|
24
|
+
|
|
25
|
+
export type MemorySlackPublicationCommitRequest = {
|
|
26
|
+
distribution: MemorySlackPublicationDistribution;
|
|
27
|
+
actor: MemorySlackPublicationActor;
|
|
28
|
+
ownerLabel?: string | null;
|
|
29
|
+
origin?: MemorySlackOrigin;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export type MemorySlackPublicationCommitResult = {
|
|
33
|
+
decision: MemorySlackPublicationDecision | null;
|
|
34
|
+
enqueue: EnqueueMemorySlackPublicationResult | null;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export async function saveWorkspaceMemoryWithSlackPublication(
|
|
38
|
+
db: Database,
|
|
39
|
+
input: SaveWorkspaceMemoryInput,
|
|
40
|
+
publication: MemorySlackPublicationCommitRequest | null,
|
|
41
|
+
embedder?: MemoryEmbedder,
|
|
42
|
+
): Promise<SaveWorkspaceMemoryResult & { slackPublication: MemorySlackPublicationCommitResult }> {
|
|
43
|
+
return await withWorkspaceRls(db, input.workspaceId, async (scopedDb) => {
|
|
44
|
+
const result = await saveWorkspaceMemory(scopedDb, input, embedder);
|
|
45
|
+
const slackPublication = publication
|
|
46
|
+
? await publishSavedMemoryMutation(scopedDb, input, result, publication)
|
|
47
|
+
: { decision: null, enqueue: null };
|
|
48
|
+
return { ...result, slackPublication };
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export async function correctWorkspaceMemoryWithSlackPublication(
|
|
53
|
+
db: Database,
|
|
54
|
+
input: CorrectWorkspaceMemoryInput,
|
|
55
|
+
publication: MemorySlackPublicationCommitRequest | null,
|
|
56
|
+
embedder?: MemoryEmbedder,
|
|
57
|
+
): Promise<
|
|
58
|
+
CorrectWorkspaceMemoryResult & { slackPublication: MemorySlackPublicationCommitResult }
|
|
59
|
+
> {
|
|
60
|
+
return await withWorkspaceRls(db, input.workspaceId, async (scopedDb) => {
|
|
61
|
+
const result = await correctWorkspaceMemory(scopedDb, input, embedder);
|
|
62
|
+
if (!publication || result.action !== "superseded" || !result.replacement) {
|
|
63
|
+
return { ...result, slackPublication: { decision: null, enqueue: null } };
|
|
64
|
+
}
|
|
65
|
+
const replacement = await requiredSnapshot(scopedDb, input.workspaceId, result.replacement.id);
|
|
66
|
+
const slackPublication = await evaluateAndEnqueue(scopedDb, {
|
|
67
|
+
accountId: input.accountId,
|
|
68
|
+
workspaceId: input.workspaceId,
|
|
69
|
+
snapshot: replacement,
|
|
70
|
+
changeKind: "corrected",
|
|
71
|
+
relatedMemoryId: result.memory.id,
|
|
72
|
+
occurredAt: result.replacement.updatedAt,
|
|
73
|
+
publication,
|
|
74
|
+
});
|
|
75
|
+
return { ...result, slackPublication };
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function publishSavedMemoryMutation(
|
|
80
|
+
db: Database,
|
|
81
|
+
input: SaveWorkspaceMemoryInput,
|
|
82
|
+
result: SaveWorkspaceMemoryResult,
|
|
83
|
+
publication: MemorySlackPublicationCommitRequest,
|
|
84
|
+
): Promise<MemorySlackPublicationCommitResult> {
|
|
85
|
+
if (result.updated || (result.deduped && !result.superseded)) {
|
|
86
|
+
return { decision: null, enqueue: null };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (result.superseded) {
|
|
90
|
+
const replacement = await requiredSnapshot(db, input.workspaceId, result.memory.id);
|
|
91
|
+
if (replacement.supersedesId === result.superseded.id) {
|
|
92
|
+
return await evaluateAndEnqueue(db, {
|
|
93
|
+
accountId: input.accountId,
|
|
94
|
+
workspaceId: input.workspaceId,
|
|
95
|
+
snapshot: replacement,
|
|
96
|
+
changeKind: "corrected",
|
|
97
|
+
relatedMemoryId: result.superseded.id,
|
|
98
|
+
occurredAt: result.memory.updatedAt,
|
|
99
|
+
publication,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
const retired = await requiredSnapshot(db, input.workspaceId, result.superseded.id);
|
|
103
|
+
return await evaluateAndEnqueue(db, {
|
|
104
|
+
accountId: input.accountId,
|
|
105
|
+
workspaceId: input.workspaceId,
|
|
106
|
+
snapshot: retired,
|
|
107
|
+
changeKind: "superseded",
|
|
108
|
+
relatedMemoryId: result.memory.id,
|
|
109
|
+
occurredAt: result.superseded.updatedAt,
|
|
110
|
+
publication,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const created = await requiredSnapshot(db, input.workspaceId, result.memory.id);
|
|
115
|
+
return await evaluateAndEnqueue(db, {
|
|
116
|
+
accountId: input.accountId,
|
|
117
|
+
workspaceId: input.workspaceId,
|
|
118
|
+
snapshot: created,
|
|
119
|
+
changeKind: "created",
|
|
120
|
+
relatedMemoryId: null,
|
|
121
|
+
occurredAt: result.memory.createdAt,
|
|
122
|
+
publication,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async function evaluateAndEnqueue(
|
|
127
|
+
db: Database,
|
|
128
|
+
input: {
|
|
129
|
+
accountId: string;
|
|
130
|
+
workspaceId: string;
|
|
131
|
+
snapshot: WorkspaceMemorySlackPublicationSnapshot;
|
|
132
|
+
changeKind: "created" | "corrected" | "superseded";
|
|
133
|
+
relatedMemoryId: string | null;
|
|
134
|
+
occurredAt: string;
|
|
135
|
+
publication: MemorySlackPublicationCommitRequest;
|
|
136
|
+
},
|
|
137
|
+
): Promise<MemorySlackPublicationCommitResult> {
|
|
138
|
+
const configuration = await getCurrentMemorySlackPublicationConfiguration(db, input.workspaceId);
|
|
139
|
+
const decision = evaluateMemorySlackPublication({
|
|
140
|
+
context: {
|
|
141
|
+
accountId: input.accountId,
|
|
142
|
+
workspaceId: input.workspaceId,
|
|
143
|
+
now: input.occurredAt,
|
|
144
|
+
},
|
|
145
|
+
policy: configuration
|
|
146
|
+
? {
|
|
147
|
+
enabled: configuration.enabled,
|
|
148
|
+
autoImportances: configuration.autoImportances,
|
|
149
|
+
reviewImportances: configuration.reviewImportances,
|
|
150
|
+
}
|
|
151
|
+
: undefined,
|
|
152
|
+
memory: {
|
|
153
|
+
sourceType: "workspace_memory",
|
|
154
|
+
accountId: input.snapshot.accountId,
|
|
155
|
+
workspaceId: input.snapshot.workspaceId,
|
|
156
|
+
id: input.snapshot.id,
|
|
157
|
+
version: input.snapshot.version,
|
|
158
|
+
status: input.snapshot.status as Parameters<
|
|
159
|
+
typeof evaluateMemorySlackPublication
|
|
160
|
+
>[0]["memory"]["status"],
|
|
161
|
+
kind: input.snapshot.kind as Parameters<
|
|
162
|
+
typeof evaluateMemorySlackPublication
|
|
163
|
+
>[0]["memory"]["kind"],
|
|
164
|
+
scopeType: input.snapshot.scopeType as Parameters<
|
|
165
|
+
typeof evaluateMemorySlackPublication
|
|
166
|
+
>[0]["memory"]["scopeType"],
|
|
167
|
+
namespace: input.snapshot.namespace,
|
|
168
|
+
labels: input.snapshot.labels,
|
|
169
|
+
validFrom: input.snapshot.validFrom,
|
|
170
|
+
validUntil: input.snapshot.validUntil,
|
|
171
|
+
supersedesId: input.snapshot.supersedesId,
|
|
172
|
+
supersededById: input.snapshot.supersededById,
|
|
173
|
+
},
|
|
174
|
+
change: {
|
|
175
|
+
kind: input.changeKind,
|
|
176
|
+
relatedMemoryId: input.relatedMemoryId,
|
|
177
|
+
occurredAt: input.occurredAt,
|
|
178
|
+
origin: input.publication.origin ?? "native",
|
|
179
|
+
ownerLabel: input.publication.ownerLabel ?? null,
|
|
180
|
+
},
|
|
181
|
+
distribution: input.publication.distribution,
|
|
182
|
+
});
|
|
183
|
+
if (!decision.eligible) return { decision, enqueue: null };
|
|
184
|
+
|
|
185
|
+
const enqueue = await enqueueMemorySlackPublication(db, {
|
|
186
|
+
accountId: input.accountId,
|
|
187
|
+
workspaceId: input.workspaceId,
|
|
188
|
+
sourceType: "workspace_memory",
|
|
189
|
+
sourceId: input.snapshot.id,
|
|
190
|
+
sourceVersion: String(input.snapshot.version),
|
|
191
|
+
sourceIdempotencyKey: decision.idempotencyKey,
|
|
192
|
+
projection: decision.projection,
|
|
193
|
+
importance: decision.projection.importance,
|
|
194
|
+
deliveryMode: decision.deliveryMode,
|
|
195
|
+
actor: input.publication.actor,
|
|
196
|
+
});
|
|
197
|
+
return { decision, enqueue };
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async function requiredSnapshot(db: Database, workspaceId: string, memoryId: string) {
|
|
201
|
+
const snapshot = await getWorkspaceMemorySlackPublicationSnapshot(db, workspaceId, memoryId);
|
|
202
|
+
if (!snapshot) throw new Error("Memory Slack publication source disappeared before commit");
|
|
203
|
+
return snapshot;
|
|
204
|
+
}
|