@sentry/junior 0.237.0 → 0.238.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/{context-0Et9iVSB.js → actor-BTtLaeWy.js} +1 -96
- package/dist/admin-ZGlZ8lY6.js +33 -0
- package/dist/{agent-hooks-otHGbhBx.js → agent-hooks-CxdSqU2u.js} +1423 -2973
- package/dist/{agent-runner-DMAsnPb9.js → agent-runner-ZS1LHtSR.js} +171 -139
- package/dist/api/schema.d.ts +4 -0
- package/dist/api/schema.js +3 -2
- package/dist/api.d.ts +1 -1
- package/dist/api.js +31 -24
- package/dist/{app-MEd7wbF0.d.ts → app-FB2hmHpZ.d.ts} +1 -1
- package/dist/app.d.ts +1 -1
- package/dist/app.js +26 -22
- package/dist/{briefs-DDA1ugXk.js → briefs-B6IhUAgK.js} +4 -4
- package/dist/{catalog-runtime-DlXuhrmk.js → catalog-runtime-BzNg07bb.js} +1 -1
- package/dist/{checkpoint-BlIIiHG5.js → checkpoint-DWGyJg4c.js} +19 -10
- package/dist/cli/chat.js +13 -13
- package/dist/cli/check.js +3 -3
- package/dist/cli/main.js +6 -2
- package/dist/cli/plugins.js +5 -3
- package/dist/cli/run.d.ts +2 -1
- package/dist/cli/run.js +2 -1
- package/dist/cli/snapshot-create.js +3 -3
- package/dist/cli/upgrade.d.ts +1 -1
- package/dist/cli/upgrade.js +3 -3
- package/dist/{client-CPMbTmJU.js → client-CKs0rj3A.js} +2 -2
- package/dist/{config-CxpPG6nR.js → config-DRw358bB.js} +2 -2
- package/dist/{conversation-9Fxa1Zbq.js → conversation-DwD-vquD.js} +3 -517
- package/dist/{conversation-privacy-Dbkdvozu.js → conversation-privacy-DAYxHp74.js} +1 -1
- package/dist/{credential-sync-D9yGg3q5.js → credential-sync-DmRbuyE4.js} +15 -14
- package/dist/{db-Ca9Nt5ci.d.ts → db-CWZP77Ex.d.ts} +1 -1
- package/dist/{executor-BNxfDS1V.js → executor-DgPk8Lsx.js} +4 -2
- package/dist/experimental-Qn9rKLgx.js +50 -0
- package/dist/history-BLTUgFYO.js +518 -0
- package/dist/{images-D6Y3FJDQ.js → images-puyEuXoY.js} +3 -3
- package/dist/instrumentation.js +2 -2
- package/dist/{job-queue-Dq5_zt_l.js → job-queue-BL9stv_E.js} +1 -1
- package/dist/{logging-rB_ngE_R.js → logging-BNi_jyHb.js} +1 -1
- package/dist/nitro.d.ts +1 -1
- package/dist/nitro.js +3 -3
- package/dist/{oauth-authorization-5T1S1v_1.js → oauth-authorization-Zv1oLB1J.js} +1 -1
- package/dist/{oauth-callback-server-Csw7RssK.js → oauth-callback-server-BHtVbKvO.js} +2 -2
- package/dist/pending-auth-DulDjfwL.js +433 -0
- package/dist/{profile-D6apIG2N.js → profile-C2TScny6.js} +5 -7
- package/dist/{prompt-Ctwy6hSI.js → prompt-CMU-B4FA.js} +1 -1
- package/dist/{registry-zfTzTHe6.js → registry-D1RsbgDb.js} +111 -3
- package/dist/{resolve-sJtTzuBO.js → resolve-CuY3dhxy.js} +2 -2
- package/dist/{runner-DkCk8cbc.js → runner-BUkReLln.js} +10 -10
- package/dist/{sandbox-egress-signals-tSQ8OEcw.js → sandbox-egress-signals-etjG6qQX.js} +1 -1
- package/dist/{schema-C5bgOpRg.js → schema-CxWAN2zq.js} +1 -1
- package/dist/{skills-BVpr_Gnv.js → skills-BGhThSGR.js} +2 -2
- package/dist/{spawn-CJhmyE4E.js → spawn-PtyOnoXt.js} +2 -2
- package/dist/{store-DoewL5mG.d.ts → store-BxxbxBlm.d.ts} +7 -0
- package/dist/{task-queue-BAiZ7_CF.js → task-queue-Bi2uNATu.js} +3 -3
- package/dist/{task-runner-Bh8XONXT.js → task-runner-CDoZCo8g.js} +20 -10
- package/dist/{thread-state-BnQln4jO.js → thread-state-B-t88gH0.js} +6 -5
- package/dist/{turn-failure-response-Ba7_kItn.js → turn-failure-response-di4Jx90o.js} +10 -9
- package/dist/{turn-lifecycle-D6CTbkb9.js → turn-lifecycle-ByhUHYjD.js} +1 -1
- package/dist/{turn-session-routing-DNevs9H7.js → turn-session-routing-QjNvsevm.js} +1 -1
- package/dist/{validation-BNPhuAvt.js → validation-D3li76Ti.js} +1 -1
- package/dist/{version-EQ4MWuUJ.js → version-Chc6bVG5.js} +1 -1
- package/dist/version.js +1 -1
- package/dist/viewer-DsyM9kb9.js +1636 -0
- package/dist/{web-authorization-DDt4FZld.js → web-authorization-Enb5lVYt.js} +6 -6
- package/dist/{web-input-C56ivZuH.js → web-input-efQlcCis.js} +63 -433
- package/dist/{work-DJ7hb9Ul.js → work-CeiA_ZxD.js} +9 -10
- package/dist/{worker-DiiV8Lba.js → worker-BJb2hsUB.js} +5 -5
- package/migrations/0048_user_admin.sql +1 -0
- package/migrations/meta/0048_snapshot.json +3720 -0
- package/migrations/meta/_journal.json +7 -0
- package/package.json +4 -4
- package/dist/pending-auth-moCA2mTc.js +0 -59
|
@@ -0,0 +1,1636 @@
|
|
|
1
|
+
import { t as __exportAll } from "./rolldown-runtime-B4FMCO8f.js";
|
|
2
|
+
import { F as normalizeIdentityEmail } from "./logging-BNi_jyHb.js";
|
|
3
|
+
import { i as getChatConfig } from "./config-DRw358bB.js";
|
|
4
|
+
import { d as parseSlackTeamId, o as parseActorUserId, u as parseSlackChannelReferenceId } from "./actor-BTtLaeWy.js";
|
|
5
|
+
import { n as juniorDestinations } from "./destinations-BeBiJeEM.js";
|
|
6
|
+
import { D as juniorConversations, E as conversationReadColumns, O as juniorIdentities, S as juniorConversationAnnotations, b as juniorConversationEvents, g as juniorConversationBindings, h as juniorConversationParticipants, k as juniorUsers, t as createJuniorSqlExecutor, v as CONVERSATION_METRICS, y as juniorConversationMetrics } from "./executor-DgPk8Lsx.js";
|
|
7
|
+
import { c as newConversationEventSchema, l as encodeHistoryPayload, m as usageSchema, o as decodeStoredConversationEvent, r as conversationEventDataSchema, s as historyReplacementSchema } from "./history-BLTUgFYO.js";
|
|
8
|
+
import { randomUUID } from "node:crypto";
|
|
9
|
+
import { and, asc, desc, eq, gt, gte, inArray, isNull, lt, lte, ne, or, sql } from "drizzle-orm";
|
|
10
|
+
import { destinationSchema, identitySchema, locationSchema, sourceSchema, userSchema } from "@sentry/junior-plugin-api";
|
|
11
|
+
//#region src/chat/destination.ts
|
|
12
|
+
/** Build Junior's canonical destination from Slack workspace and channel ids. */
|
|
13
|
+
function createSlackDestination(input) {
|
|
14
|
+
const channelId = parseSlackChannelReferenceId(input.channelId);
|
|
15
|
+
const teamId = parseSlackTeamId(input.teamId);
|
|
16
|
+
if (!channelId || !teamId) return;
|
|
17
|
+
return {
|
|
18
|
+
platform: "slack",
|
|
19
|
+
teamId,
|
|
20
|
+
channelId
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
/** Parse and validate a serialized destination that crossed a runtime boundary. */
|
|
24
|
+
function parseDestination(value) {
|
|
25
|
+
const parsed = destinationSchema.safeParse(value);
|
|
26
|
+
return parsed.success ? parsed.data : void 0;
|
|
27
|
+
}
|
|
28
|
+
/** Require a Slack destination at a Slack-only runtime boundary. */
|
|
29
|
+
function requireSlackDestination(destination, action) {
|
|
30
|
+
if (destination?.platform === "slack") return destination;
|
|
31
|
+
throw new Error(`${action} requires a Slack destination`);
|
|
32
|
+
}
|
|
33
|
+
/** Compare two destinations without relying on object identity. */
|
|
34
|
+
function sameDestination(left, right) {
|
|
35
|
+
if (left.platform !== right.platform) return false;
|
|
36
|
+
if (left.platform === "local" && right.platform === "local") return left.conversationId === right.conversationId;
|
|
37
|
+
if (left.platform === "slack" && right.platform === "slack") return left.teamId === right.teamId && left.channelId === right.channelId;
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
//#endregion
|
|
41
|
+
//#region src/chat/identities/sql.ts
|
|
42
|
+
function dateFromMs$1(ms) {
|
|
43
|
+
return new Date(ms);
|
|
44
|
+
}
|
|
45
|
+
function tenantId$1(value) {
|
|
46
|
+
return value ?? "";
|
|
47
|
+
}
|
|
48
|
+
async function upsertUser(executor, args) {
|
|
49
|
+
const id = (await executor.db().insert(juniorUsers).values({
|
|
50
|
+
id: randomUUID(),
|
|
51
|
+
primaryEmail: args.email,
|
|
52
|
+
primaryEmailNormalized: args.emailNormalized,
|
|
53
|
+
displayName: args.displayName ?? null,
|
|
54
|
+
createdAt: dateFromMs$1(args.nowMs),
|
|
55
|
+
updatedAt: dateFromMs$1(args.nowMs)
|
|
56
|
+
}).onConflictDoUpdate({
|
|
57
|
+
target: juniorUsers.primaryEmailNormalized,
|
|
58
|
+
set: {
|
|
59
|
+
displayName: sql`coalesce(${juniorUsers.displayName}, excluded.display_name)`,
|
|
60
|
+
updatedAt: sql`excluded.updated_at`
|
|
61
|
+
}
|
|
62
|
+
}).returning({ id: juniorUsers.id }))[0]?.id;
|
|
63
|
+
if (!id) throw new Error("User identity upsert returned no row");
|
|
64
|
+
return id;
|
|
65
|
+
}
|
|
66
|
+
async function existingIdentity(executor, identity) {
|
|
67
|
+
return (await executor.db().select().from(juniorIdentities).where(and(eq(juniorIdentities.provider, identity.provider), eq(juniorIdentities.providerTenantId, tenantId$1(identity.providerTenantId)), eq(juniorIdentities.providerSubjectId, identity.providerSubjectId))))[0];
|
|
68
|
+
}
|
|
69
|
+
async function upsertIdentityRecord(executor, identity, linkedUserId, nowMs) {
|
|
70
|
+
const emailNormalized = normalizeIdentityEmail(identity.email);
|
|
71
|
+
const email = emailNormalized ? identity.email?.trim() || emailNormalized : void 0;
|
|
72
|
+
const existing = await existingIdentity(executor, identity);
|
|
73
|
+
const userEmailNormalized = existing?.emailVerified && existing.emailNormalized ? existing.emailNormalized : identity.emailVerified ? emailNormalized : void 0;
|
|
74
|
+
const userEmail = existing?.emailVerified && existing.email ? existing.email : email ?? userEmailNormalized;
|
|
75
|
+
const verifiedUserId = identity.kind === "user" && userEmailNormalized ? await upsertUser(executor, {
|
|
76
|
+
email: userEmail ?? userEmailNormalized,
|
|
77
|
+
emailNormalized: userEmailNormalized,
|
|
78
|
+
nowMs,
|
|
79
|
+
...existing?.displayName || identity.displayName ? { displayName: existing?.displayName ?? identity.displayName } : void 0
|
|
80
|
+
}) : void 0;
|
|
81
|
+
if (existing?.userId && verifiedUserId && existing.userId !== verifiedUserId) throw new Error("Identity verified email conflicts with linked user");
|
|
82
|
+
if (linkedUserId && verifiedUserId && linkedUserId !== verifiedUserId) throw new Error("Linked identity conflicts with verified email user");
|
|
83
|
+
if (existing?.userId && linkedUserId && existing.userId !== linkedUserId) throw new Error("Identity conflicts with linked user");
|
|
84
|
+
const userId = existing?.userId ?? linkedUserId ?? verifiedUserId;
|
|
85
|
+
const row = (await executor.db().insert(juniorIdentities).values({
|
|
86
|
+
id: randomUUID(),
|
|
87
|
+
userId: userId ?? null,
|
|
88
|
+
kind: identity.kind,
|
|
89
|
+
provider: identity.provider,
|
|
90
|
+
providerTenantId: tenantId$1(identity.providerTenantId),
|
|
91
|
+
providerSubjectId: identity.providerSubjectId,
|
|
92
|
+
displayName: identity.displayName ?? null,
|
|
93
|
+
handle: identity.handle ?? null,
|
|
94
|
+
email: email ?? null,
|
|
95
|
+
emailNormalized: emailNormalized ?? null,
|
|
96
|
+
emailVerified: Boolean(identity.emailVerified && emailNormalized),
|
|
97
|
+
avatarUrl: null,
|
|
98
|
+
metadata: identity.metadata ?? null,
|
|
99
|
+
createdAt: dateFromMs$1(nowMs),
|
|
100
|
+
updatedAt: dateFromMs$1(nowMs)
|
|
101
|
+
}).onConflictDoUpdate({
|
|
102
|
+
target: [
|
|
103
|
+
juniorIdentities.provider,
|
|
104
|
+
juniorIdentities.providerTenantId,
|
|
105
|
+
juniorIdentities.providerSubjectId
|
|
106
|
+
],
|
|
107
|
+
set: {
|
|
108
|
+
kind: sql`excluded.kind`,
|
|
109
|
+
userId: sql`coalesce(${juniorIdentities.userId}, excluded.user_id)`,
|
|
110
|
+
displayName: linkedUserId ? sql`coalesce(excluded.display_name, ${juniorIdentities.displayName})` : sql`coalesce(${juniorIdentities.displayName}, excluded.display_name)`,
|
|
111
|
+
handle: linkedUserId ? sql`coalesce(excluded.handle, ${juniorIdentities.handle})` : sql`coalesce(${juniorIdentities.handle}, excluded.handle)`,
|
|
112
|
+
email: sql`case when ${juniorIdentities.emailVerified} then coalesce(${juniorIdentities.email}, excluded.email) when excluded.email_verified then excluded.email else coalesce(${juniorIdentities.email}, excluded.email) end`,
|
|
113
|
+
emailNormalized: sql`case when ${juniorIdentities.emailVerified} then coalesce(${juniorIdentities.emailNormalized}, excluded.email_normalized) when excluded.email_verified then excluded.email_normalized else coalesce(${juniorIdentities.emailNormalized}, excluded.email_normalized) end`,
|
|
114
|
+
emailVerified: sql`${juniorIdentities.emailVerified} OR excluded.email_verified`,
|
|
115
|
+
avatarUrl: sql`coalesce(${juniorIdentities.avatarUrl}, excluded.avatar_url)`,
|
|
116
|
+
metadata: sql`coalesce(${juniorIdentities.metadata}, excluded.metadata_json)`,
|
|
117
|
+
updatedAt: sql`excluded.updated_at`
|
|
118
|
+
},
|
|
119
|
+
...linkedUserId ? { setWhere: sql`${juniorIdentities.userId} IS NULL OR ${juniorIdentities.userId} = excluded.user_id` } : void 0
|
|
120
|
+
}).returning({
|
|
121
|
+
id: juniorIdentities.id,
|
|
122
|
+
userId: juniorIdentities.userId
|
|
123
|
+
}))[0];
|
|
124
|
+
if (!row) {
|
|
125
|
+
if (linkedUserId) throw new Error("Identity conflicts with linked user");
|
|
126
|
+
throw new Error("Identity upsert returned no row");
|
|
127
|
+
}
|
|
128
|
+
return {
|
|
129
|
+
id: row.id,
|
|
130
|
+
...row.userId ? { userId: row.userId } : void 0
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
/** Read the saved timezone for one user. */
|
|
134
|
+
async function readUserTimezone(db, userId) {
|
|
135
|
+
return (await db.select({ timezone: juniorUsers.timezone }).from(juniorUsers).where(eq(juniorUsers.id, userId)).limit(1))[0]?.timezone ?? void 0;
|
|
136
|
+
}
|
|
137
|
+
/** Save the IANA timezone for one user. */
|
|
138
|
+
async function saveUserTimezone(db, userId, timezone, nowMs) {
|
|
139
|
+
await db.update(juniorUsers).set({
|
|
140
|
+
timezone,
|
|
141
|
+
updatedAt: dateFromMs$1(nowMs)
|
|
142
|
+
}).where(and(eq(juniorUsers.id, userId), sql`${juniorUsers.timezone} is distinct from ${timezone}`));
|
|
143
|
+
}
|
|
144
|
+
/** Persist one provider identity observation and link verified emails to users. */
|
|
145
|
+
async function upsertIdentity(executor, identity, nowMs = Date.now()) {
|
|
146
|
+
return await upsertIdentityRecord(executor, identity, void 0, nowMs);
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Persist one provider identity after a trusted account-linking flow.
|
|
150
|
+
* Callers must use provider-verified account data, such as GitHub's authenticated
|
|
151
|
+
* user response, never identity claims inferred from content such as Git commits.
|
|
152
|
+
*/
|
|
153
|
+
async function upsertLinkedIdentity(executor, userId, identity, nowMs = Date.now()) {
|
|
154
|
+
return await upsertIdentityRecord(executor, identity, userId, nowMs);
|
|
155
|
+
}
|
|
156
|
+
/** Remove one exact provider identity owned by a workspace Slack user. */
|
|
157
|
+
async function deleteProviderIdentityForSlackUser(executor, slackTeamId, slackUserId, provider, providerSubjectId) {
|
|
158
|
+
const userId = (await executor.db().select({ userId: juniorIdentities.userId }).from(juniorIdentities).where(and(eq(juniorIdentities.kind, "user"), eq(juniorIdentities.provider, "slack"), eq(juniorIdentities.providerTenantId, slackTeamId), eq(juniorIdentities.providerSubjectId, slackUserId))).limit(1))[0]?.userId;
|
|
159
|
+
if (!userId) return;
|
|
160
|
+
await executor.db().delete(juniorIdentities).where(and(eq(juniorIdentities.userId, userId), eq(juniorIdentities.provider, provider), eq(juniorIdentities.providerTenantId, ""), eq(juniorIdentities.providerSubjectId, providerSubjectId)));
|
|
161
|
+
}
|
|
162
|
+
//#endregion
|
|
163
|
+
//#region src/chat/conversations/sql/participants.ts
|
|
164
|
+
/** Resolve the root conversation id that owns personal-feed membership. */
|
|
165
|
+
async function resolveRootConversationId(executor, conversationId) {
|
|
166
|
+
const [row] = await executor.db().select({ rootConversationId: juniorConversations.rootConversationId }).from(juniorConversations).where(eq(juniorConversations.conversationId, conversationId)).limit(1);
|
|
167
|
+
return row?.rootConversationId ?? conversationId;
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Upsert one linked user as a participant on a root conversation.
|
|
171
|
+
* No-ops when the identity is missing, unlinked, or not a user.
|
|
172
|
+
*/
|
|
173
|
+
async function recordConversationParticipant(executor, args) {
|
|
174
|
+
const [identity] = await executor.db().select({
|
|
175
|
+
kind: juniorIdentities.kind,
|
|
176
|
+
userId: juniorIdentities.userId
|
|
177
|
+
}).from(juniorIdentities).where(eq(juniorIdentities.id, args.actorIdentityId)).limit(1);
|
|
178
|
+
if (!identity?.userId || identity.kind !== "user") return;
|
|
179
|
+
const rootConversationId = await resolveRootConversationId(executor, args.conversationId);
|
|
180
|
+
const at = new Date(args.atMs);
|
|
181
|
+
await executor.db().insert(juniorConversationParticipants).values({
|
|
182
|
+
userId: identity.userId,
|
|
183
|
+
rootConversationId,
|
|
184
|
+
lastMessageAt: at
|
|
185
|
+
}).onConflictDoUpdate({
|
|
186
|
+
target: [juniorConversationParticipants.userId, juniorConversationParticipants.rootConversationId],
|
|
187
|
+
set: {
|
|
188
|
+
lastMessageAt: sql`greatest(${juniorConversationParticipants.lastMessageAt}, excluded.last_message_at)`,
|
|
189
|
+
...args.restoreArchive ? { archivedAt: null } : void 0
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
//#endregion
|
|
194
|
+
//#region src/chat/usage.ts
|
|
195
|
+
const agentTurnUsageSchema = usageSchema;
|
|
196
|
+
const COMPONENT_USAGE_FIELDS = [
|
|
197
|
+
"inputTokens",
|
|
198
|
+
"outputTokens",
|
|
199
|
+
"cachedInputTokens",
|
|
200
|
+
"cacheCreationTokens"
|
|
201
|
+
];
|
|
202
|
+
const COST_COMPONENT_FIELDS = [
|
|
203
|
+
"input",
|
|
204
|
+
"output",
|
|
205
|
+
"cacheRead",
|
|
206
|
+
"cacheWrite"
|
|
207
|
+
];
|
|
208
|
+
/** Return whether any token counter is present on a usage record. */
|
|
209
|
+
function hasAgentTurnUsage(usage) {
|
|
210
|
+
return Boolean(usage && (Object.entries(usage).some(([field, value]) => field !== "cost" && typeof value === "number" && Number.isFinite(value)) || Object.values(usage.cost ?? {}).some((value) => typeof value === "number" && Number.isFinite(value))));
|
|
211
|
+
}
|
|
212
|
+
function getFiniteCount(value) {
|
|
213
|
+
return typeof value === "number" && Number.isFinite(value) ? Math.max(0, Math.floor(value)) : void 0;
|
|
214
|
+
}
|
|
215
|
+
function getFiniteCost(value) {
|
|
216
|
+
return typeof value === "number" && Number.isFinite(value) ? Math.max(0, value) : void 0;
|
|
217
|
+
}
|
|
218
|
+
function addCost(left, right) {
|
|
219
|
+
return Math.round(((left ?? 0) + right) * 0xe8d4a51000) / 0xe8d4a51000;
|
|
220
|
+
}
|
|
221
|
+
function getComponentTotal(usage) {
|
|
222
|
+
let total;
|
|
223
|
+
for (const field of COMPONENT_USAGE_FIELDS) {
|
|
224
|
+
const value = getFiniteCount(usage[field]);
|
|
225
|
+
if (value === void 0) continue;
|
|
226
|
+
total = (total ?? 0) + value;
|
|
227
|
+
}
|
|
228
|
+
return total;
|
|
229
|
+
}
|
|
230
|
+
/** Estimated total USD cost from one usage record, when any cost field is present. */
|
|
231
|
+
function agentTurnCostUsd(usage) {
|
|
232
|
+
if (!usage?.cost) return void 0;
|
|
233
|
+
const total = getFiniteCost(usage.cost.total);
|
|
234
|
+
if (total !== void 0) return total;
|
|
235
|
+
let componentTotal = 0;
|
|
236
|
+
let hasComponent = false;
|
|
237
|
+
for (const field of COST_COMPONENT_FIELDS) {
|
|
238
|
+
const value = getFiniteCost(usage.cost[field]);
|
|
239
|
+
if (value === void 0) continue;
|
|
240
|
+
hasComponent = true;
|
|
241
|
+
componentTotal = addCost(componentTotal, value);
|
|
242
|
+
}
|
|
243
|
+
return hasComponent ? componentTotal : void 0;
|
|
244
|
+
}
|
|
245
|
+
/** Total token count from one usage record, preferring component counters. */
|
|
246
|
+
function agentTurnTotalTokens(usage) {
|
|
247
|
+
if (!usage) return void 0;
|
|
248
|
+
const componentTotal = getComponentTotal(usage);
|
|
249
|
+
if (componentTotal !== void 0) return componentTotal;
|
|
250
|
+
return getFiniteCount(usage.totalTokens);
|
|
251
|
+
}
|
|
252
|
+
/** Aggregate token usage across slices without double-counting provider totals. */
|
|
253
|
+
function addAgentTurnUsage(...usages) {
|
|
254
|
+
const components = {};
|
|
255
|
+
let componentTotal;
|
|
256
|
+
let totalOnlyTokens;
|
|
257
|
+
let reasoningTokens;
|
|
258
|
+
const cost = {};
|
|
259
|
+
for (const usage of usages) {
|
|
260
|
+
if (!usage) continue;
|
|
261
|
+
const reasoning = getFiniteCount(usage.reasoningTokens);
|
|
262
|
+
if (reasoning !== void 0) reasoningTokens = (reasoningTokens ?? 0) + reasoning;
|
|
263
|
+
if (usage.cost) for (const field of [...COST_COMPONENT_FIELDS, "total"]) {
|
|
264
|
+
const value = getFiniteCost(usage.cost[field]);
|
|
265
|
+
if (value === void 0) continue;
|
|
266
|
+
cost[field] = addCost(cost[field], value);
|
|
267
|
+
}
|
|
268
|
+
const usageComponentTotal = getComponentTotal(usage);
|
|
269
|
+
if (usageComponentTotal !== void 0) {
|
|
270
|
+
componentTotal = (componentTotal ?? 0) + usageComponentTotal;
|
|
271
|
+
for (const field of COMPONENT_USAGE_FIELDS) {
|
|
272
|
+
const value = getFiniteCount(usage[field]);
|
|
273
|
+
if (value === void 0) continue;
|
|
274
|
+
components[field] = (components[field] ?? 0) + value;
|
|
275
|
+
}
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
const totalTokens = getFiniteCount(usage.totalTokens);
|
|
279
|
+
if (totalTokens !== void 0) totalOnlyTokens = (totalOnlyTokens ?? 0) + totalTokens;
|
|
280
|
+
}
|
|
281
|
+
if (totalOnlyTokens !== void 0) return {
|
|
282
|
+
totalTokens: totalOnlyTokens + (componentTotal ?? 0),
|
|
283
|
+
...reasoningTokens !== void 0 ? { reasoningTokens } : void 0,
|
|
284
|
+
...Object.keys(cost).length > 0 ? { cost } : void 0
|
|
285
|
+
};
|
|
286
|
+
const result = {
|
|
287
|
+
...components,
|
|
288
|
+
...reasoningTokens !== void 0 ? { reasoningTokens } : void 0,
|
|
289
|
+
...Object.keys(cost).length > 0 ? { cost } : void 0
|
|
290
|
+
};
|
|
291
|
+
return hasAgentTurnUsage(result) ? result : void 0;
|
|
292
|
+
}
|
|
293
|
+
//#endregion
|
|
294
|
+
//#region src/chat/conversations/metrics.ts
|
|
295
|
+
function finite(value) {
|
|
296
|
+
return value !== void 0 && Number.isFinite(value) && value >= 0 ? value : void 0;
|
|
297
|
+
}
|
|
298
|
+
/** Convert one Run summary to metric rows. */
|
|
299
|
+
function conversationMetricValues(args) {
|
|
300
|
+
const usage = args.usage;
|
|
301
|
+
return {
|
|
302
|
+
duration_ms: Math.max(0, args.durationMs),
|
|
303
|
+
total_tokens: agentTurnTotalTokens(usage),
|
|
304
|
+
input_tokens: finite(usage?.inputTokens),
|
|
305
|
+
output_tokens: finite(usage?.outputTokens),
|
|
306
|
+
cached_input_tokens: finite(usage?.cachedInputTokens),
|
|
307
|
+
cache_creation_tokens: finite(usage?.cacheCreationTokens),
|
|
308
|
+
reasoning_tokens: finite(usage?.reasoningTokens),
|
|
309
|
+
cost_usd: agentTurnCostUsd(usage)
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
/** Replace one Run's metric rows and refresh its Conversation totals. */
|
|
313
|
+
async function replaceConversationMetrics(executor, args) {
|
|
314
|
+
const [current] = await executor.db().select({ updatedAt: juniorConversationMetrics.updatedAt }).from(juniorConversationMetrics).where(and(eq(juniorConversationMetrics.conversationId, args.conversationId), eq(juniorConversationMetrics.runId, args.runId))).limit(1);
|
|
315
|
+
if (current && current.updatedAt.getTime() > args.updatedAtMs) return;
|
|
316
|
+
await executor.db().delete(juniorConversationMetrics).where(and(eq(juniorConversationMetrics.conversationId, args.conversationId), eq(juniorConversationMetrics.runId, args.runId)));
|
|
317
|
+
const metricValues = conversationMetricValues({
|
|
318
|
+
durationMs: args.durationMs,
|
|
319
|
+
usage: args.usage
|
|
320
|
+
});
|
|
321
|
+
const values = CONVERSATION_METRICS.flatMap((metric) => {
|
|
322
|
+
const value = metricValues[metric];
|
|
323
|
+
return value === void 0 ? [] : [{
|
|
324
|
+
conversationId: args.conversationId,
|
|
325
|
+
runId: args.runId,
|
|
326
|
+
metric,
|
|
327
|
+
value,
|
|
328
|
+
occurredAt: new Date(args.occurredAtMs),
|
|
329
|
+
updatedAt: new Date(args.updatedAtMs)
|
|
330
|
+
}];
|
|
331
|
+
});
|
|
332
|
+
if (values.length > 0) await executor.db().insert(juniorConversationMetrics).values(values);
|
|
333
|
+
const metric = (name) => sql`(
|
|
334
|
+
select sum(${juniorConversationMetrics.value})
|
|
335
|
+
from ${juniorConversationMetrics}
|
|
336
|
+
where ${juniorConversationMetrics.conversationId} = ${args.conversationId}
|
|
337
|
+
and ${juniorConversationMetrics.metric} = ${name}
|
|
338
|
+
)`;
|
|
339
|
+
const totalTokens = metric("total_tokens");
|
|
340
|
+
const componentTokens = sql`
|
|
341
|
+
coalesce(${metric("input_tokens")}, 0)
|
|
342
|
+
+ coalesce(${metric("output_tokens")}, 0)
|
|
343
|
+
+ coalesce(${metric("cached_input_tokens")}, 0)
|
|
344
|
+
+ coalesce(${metric("cache_creation_tokens")}, 0)
|
|
345
|
+
`;
|
|
346
|
+
const hasComponents = sql`
|
|
347
|
+
${metric("input_tokens")} is not null
|
|
348
|
+
or ${metric("output_tokens")} is not null
|
|
349
|
+
or ${metric("cached_input_tokens")} is not null
|
|
350
|
+
or ${metric("cache_creation_tokens")} is not null
|
|
351
|
+
`;
|
|
352
|
+
const cost = sql`case when ${metric("cost_usd")} is null then null
|
|
353
|
+
else jsonb_build_object('total', ${metric("cost_usd")}) end`;
|
|
354
|
+
await executor.db().update(juniorConversations).set({
|
|
355
|
+
durationMs: sql`coalesce(${metric("duration_ms")}, 0)`,
|
|
356
|
+
usage: sql`case
|
|
357
|
+
when ${totalTokens} is not null
|
|
358
|
+
and (${hasComponents})
|
|
359
|
+
and ${totalTokens} <> ${componentTokens}
|
|
360
|
+
then jsonb_strip_nulls(jsonb_build_object(
|
|
361
|
+
'totalTokens', ${totalTokens},
|
|
362
|
+
'reasoningTokens', ${metric("reasoning_tokens")},
|
|
363
|
+
'cost', ${cost}
|
|
364
|
+
))
|
|
365
|
+
else jsonb_strip_nulls(jsonb_build_object(
|
|
366
|
+
'totalTokens', ${totalTokens},
|
|
367
|
+
'inputTokens', ${metric("input_tokens")},
|
|
368
|
+
'outputTokens', ${metric("output_tokens")},
|
|
369
|
+
'cachedInputTokens', ${metric("cached_input_tokens")},
|
|
370
|
+
'cacheCreationTokens', ${metric("cache_creation_tokens")},
|
|
371
|
+
'reasoningTokens', ${metric("reasoning_tokens")},
|
|
372
|
+
'cost', ${cost}
|
|
373
|
+
))
|
|
374
|
+
end`
|
|
375
|
+
}).where(eq(juniorConversations.conversationId, args.conversationId));
|
|
376
|
+
}
|
|
377
|
+
//#endregion
|
|
378
|
+
//#region src/chat/source.ts
|
|
379
|
+
/**
|
|
380
|
+
* Normalize a Turn Source into legacy provider fields stored on a Conversation.
|
|
381
|
+
* System Sources do not contain a Conversation Location.
|
|
382
|
+
*/
|
|
383
|
+
function normalizeSessionSource(value) {
|
|
384
|
+
if (!value) return;
|
|
385
|
+
if (value.kind === "local") return {
|
|
386
|
+
kind: "local",
|
|
387
|
+
visibility: value.visibility,
|
|
388
|
+
conversationId: value.conversationId
|
|
389
|
+
};
|
|
390
|
+
if (value.kind === "web") return {
|
|
391
|
+
kind: "web",
|
|
392
|
+
visibility: value.visibility,
|
|
393
|
+
conversationId: value.conversationId
|
|
394
|
+
};
|
|
395
|
+
if (value.kind === "event" || value.kind === "scheduled_automation" || value.kind === "event_automation" || value.kind === "plugin_dispatch" || value.kind === "agent_invocation") return;
|
|
396
|
+
const threadTs = value.threadTs?.trim();
|
|
397
|
+
return {
|
|
398
|
+
kind: "slack",
|
|
399
|
+
visibility: value.visibility,
|
|
400
|
+
teamId: value.teamId,
|
|
401
|
+
channelId: value.channelId,
|
|
402
|
+
...threadTs ? { threadTs } : void 0
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
/** Parse a serialized Source into legacy provider fields stored on a Conversation. */
|
|
406
|
+
function parseSessionSource(value) {
|
|
407
|
+
const parsed = sourceSchema.safeParse(value);
|
|
408
|
+
return parsed.success ? normalizeSessionSource(parsed.data) : void 0;
|
|
409
|
+
}
|
|
410
|
+
//#endregion
|
|
411
|
+
//#region src/chat/conversations/sql/bindings.ts
|
|
412
|
+
/** Resolve the durable conversation bound to provider-owned coordinates. */
|
|
413
|
+
async function getConversationIdByProviderConversation(executor, args) {
|
|
414
|
+
return (await executor.db().select({ conversationId: juniorConversationBindings.conversationId }).from(juniorConversationBindings).where(and(eq(juniorConversationBindings.provider, args.provider), eq(juniorConversationBindings.providerTenantId, args.providerTenantId), eq(juniorConversationBindings.providerDestinationId, args.providerDestinationId), eq(juniorConversationBindings.providerConversationId, args.providerConversationId))).limit(1))[0]?.conversationId;
|
|
415
|
+
}
|
|
416
|
+
/** Return the first provider conversation bound in one destination. */
|
|
417
|
+
async function getFirstProviderConversationId(executor, args) {
|
|
418
|
+
return (await executor.db().select({ providerConversationId: juniorConversationBindings.providerConversationId }).from(juniorConversationBindings).where(and(eq(juniorConversationBindings.conversationId, args.conversationId), eq(juniorConversationBindings.provider, args.provider), eq(juniorConversationBindings.providerTenantId, args.providerTenantId), eq(juniorConversationBindings.providerDestinationId, args.providerDestinationId))).orderBy(asc(juniorConversationBindings.createdAt), asc(juniorConversationBindings.providerConversationId)).limit(1))[0]?.providerConversationId;
|
|
419
|
+
}
|
|
420
|
+
/** Bind provider coordinates to a durable conversation in the caller's SQL scope. */
|
|
421
|
+
async function bindProviderConversation(executor, args) {
|
|
422
|
+
if (((await executor.db().insert(juniorConversationBindings).values({
|
|
423
|
+
...args,
|
|
424
|
+
createdAt: /* @__PURE__ */ new Date()
|
|
425
|
+
}).onConflictDoNothing().returning({ conversationId: juniorConversationBindings.conversationId }))[0]?.conversationId ?? await getConversationIdByProviderConversation(executor, args)) !== args.conversationId) throw new Error("Provider conversation is already bound to another conversation");
|
|
426
|
+
}
|
|
427
|
+
//#endregion
|
|
428
|
+
//#region src/chat/conversations/sql/actor-identity.ts
|
|
429
|
+
/** Build a durable identity observation from a stored conversation actor. */
|
|
430
|
+
function identityFromActor(actor) {
|
|
431
|
+
if (actor?.slackUserId) return {
|
|
432
|
+
kind: "user",
|
|
433
|
+
provider: "slack",
|
|
434
|
+
providerTenantId: actor.teamId,
|
|
435
|
+
providerSubjectId: actor.slackUserId,
|
|
436
|
+
...actor.fullName ? { displayName: actor.fullName } : void 0,
|
|
437
|
+
...actor.slackUserName ? { handle: actor.slackUserName } : void 0,
|
|
438
|
+
...actor.email ? {
|
|
439
|
+
email: actor.email,
|
|
440
|
+
emailVerified: true
|
|
441
|
+
} : void 0,
|
|
442
|
+
metadata: { platform: "slack" }
|
|
443
|
+
};
|
|
444
|
+
const email = actor?.email?.trim().toLowerCase();
|
|
445
|
+
if (!email) return;
|
|
446
|
+
return {
|
|
447
|
+
kind: "user",
|
|
448
|
+
provider: "junior",
|
|
449
|
+
providerSubjectId: email,
|
|
450
|
+
email,
|
|
451
|
+
emailVerified: true,
|
|
452
|
+
...actor?.fullName ? { displayName: actor.fullName } : void 0,
|
|
453
|
+
metadata: { platform: "web" }
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
/** Build a system identity when a conversation has no user actor. */
|
|
457
|
+
function systemIdentityFromSource(source) {
|
|
458
|
+
if (source === "scheduler") return {
|
|
459
|
+
kind: "system",
|
|
460
|
+
provider: "junior",
|
|
461
|
+
providerSubjectId: "scheduler",
|
|
462
|
+
displayName: "Junior Scheduler"
|
|
463
|
+
};
|
|
464
|
+
if (source === "local") return {
|
|
465
|
+
kind: "system",
|
|
466
|
+
provider: "junior",
|
|
467
|
+
providerSubjectId: "local-cli",
|
|
468
|
+
displayName: "Local CLI"
|
|
469
|
+
};
|
|
470
|
+
if (source === "event") return {
|
|
471
|
+
kind: "system",
|
|
472
|
+
provider: "junior",
|
|
473
|
+
providerSubjectId: "event",
|
|
474
|
+
displayName: "Event"
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
/** Prefer the user actor identity, else a source-derived system identity. */
|
|
478
|
+
function actorIdentityForConversation(conversation) {
|
|
479
|
+
return identityFromActor(conversation.actor) ?? systemIdentityFromSource(conversation.source);
|
|
480
|
+
}
|
|
481
|
+
/**
|
|
482
|
+
* Reconstruct a stored actor with the linked user name and identity-scoped
|
|
483
|
+
* provider fields.
|
|
484
|
+
*/
|
|
485
|
+
function actorFromIdentityRow(identity, userDisplayName) {
|
|
486
|
+
if (!identity) return;
|
|
487
|
+
const fullName = userDisplayName?.trim() ? userDisplayName : identity.displayName;
|
|
488
|
+
const email = identity.emailNormalized ?? identity.email ?? void 0;
|
|
489
|
+
if (identity.provider === "slack") return {
|
|
490
|
+
...email ? { email } : void 0,
|
|
491
|
+
...fullName ? { fullName } : void 0,
|
|
492
|
+
platform: "slack",
|
|
493
|
+
slackUserId: identity.providerSubjectId,
|
|
494
|
+
...identity.handle ? { slackUserName: identity.handle } : void 0,
|
|
495
|
+
...identity.providerTenantId ? { teamId: identity.providerTenantId } : void 0
|
|
496
|
+
};
|
|
497
|
+
if (identity.provider === "junior" && email) return {
|
|
498
|
+
email,
|
|
499
|
+
...fullName ? { fullName } : void 0
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
/** Merge later actor observations without replacing a durable Slack subject. */
|
|
503
|
+
function mergeActor(current, next) {
|
|
504
|
+
if (!current) return next;
|
|
505
|
+
if (!next) return current;
|
|
506
|
+
if (current.slackUserId && next.slackUserId && current.slackUserId !== next.slackUserId) return current;
|
|
507
|
+
return {
|
|
508
|
+
...current,
|
|
509
|
+
...current.email ?? next.email ? { email: current.email ?? next.email } : void 0,
|
|
510
|
+
...current.fullName ?? next.fullName ? { fullName: current.fullName ?? next.fullName } : void 0,
|
|
511
|
+
...current.platform ?? next.platform ? { platform: current.platform ?? next.platform } : void 0,
|
|
512
|
+
...current.slackUserId ?? next.slackUserId ? { slackUserId: current.slackUserId ?? next.slackUserId } : void 0,
|
|
513
|
+
...current.slackUserName ?? next.slackUserName ? { slackUserName: current.slackUserName ?? next.slackUserName } : void 0,
|
|
514
|
+
...current.teamId ?? next.teamId ? { teamId: current.teamId ?? next.teamId } : void 0
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
//#endregion
|
|
518
|
+
//#region src/chat/conversations/sql/location.ts
|
|
519
|
+
/** Build the complete Location for one Conversation SQL write. */
|
|
520
|
+
function locationForWrite(args) {
|
|
521
|
+
const location = args.location ? locationSchema.parse(args.location) : void 0;
|
|
522
|
+
const destination = args.destination?.platform === "slack" ? args.destination : void 0;
|
|
523
|
+
const source = args.sessionSource?.kind === "slack" ? args.sessionSource : void 0;
|
|
524
|
+
if (location) {
|
|
525
|
+
if (destination && (location.id !== args.destinationId || location.teamId !== destination.teamId || location.channelId !== destination.channelId) || source && (location.teamId !== source.teamId || location.channelId !== source.channelId)) throw new Error("Conversation Location changed");
|
|
526
|
+
const threadTs = location.threadTs ?? destination?.threadTs ?? source?.threadTs;
|
|
527
|
+
return threadTs === location.threadTs ? location : {
|
|
528
|
+
...location,
|
|
529
|
+
threadTs
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
if (destination && source && (destination.teamId !== source.teamId || destination.channelId !== source.channelId)) throw new Error("Conversation Location does not match its session Source");
|
|
533
|
+
if (!destination || !args.destinationId) return;
|
|
534
|
+
const threadTs = destination.threadTs ?? source?.threadTs;
|
|
535
|
+
return locationSchema.parse({
|
|
536
|
+
id: args.destinationId,
|
|
537
|
+
provider: "slack",
|
|
538
|
+
teamId: destination.teamId,
|
|
539
|
+
channelId: destination.channelId,
|
|
540
|
+
...threadTs ? { threadTs } : void 0
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
/** Read one complete Location from SQL. */
|
|
544
|
+
function locationFromRow(value, row, sessionSource) {
|
|
545
|
+
if (value !== void 0 && value !== null) {
|
|
546
|
+
const location = locationSchema.parse(value);
|
|
547
|
+
const slackSource = !location.threadTs && sessionSource?.kind === "slack" && sessionSource.teamId === location.teamId && sessionSource.channelId === location.channelId ? sessionSource : void 0;
|
|
548
|
+
return slackSource?.threadTs ? {
|
|
549
|
+
...location,
|
|
550
|
+
threadTs: slackSource.threadTs
|
|
551
|
+
} : location;
|
|
552
|
+
}
|
|
553
|
+
if (!row || row.provider === "local") return;
|
|
554
|
+
if (row.provider !== "slack") throw new Error("Conversation Location provider is not supported");
|
|
555
|
+
const slackSource = sessionSource?.kind === "slack" && sessionSource.teamId === row.providerTenantId && sessionSource.channelId === row.providerDestinationId ? sessionSource : void 0;
|
|
556
|
+
return locationSchema.parse({
|
|
557
|
+
id: row.id,
|
|
558
|
+
provider: "slack",
|
|
559
|
+
teamId: row.providerTenantId,
|
|
560
|
+
channelId: row.providerDestinationId,
|
|
561
|
+
...slackSource?.threadTs ? { threadTs: slackSource.threadTs } : void 0
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
/** Read confirmed visibility from one linked Location row. */
|
|
565
|
+
function visibilityFromLocationRow(row) {
|
|
566
|
+
if (!row) return;
|
|
567
|
+
if (row.visibility === "public" || row.visibility === "private") return row.visibility;
|
|
568
|
+
}
|
|
569
|
+
//#endregion
|
|
570
|
+
//#region src/chat/conversations/sql/store.ts
|
|
571
|
+
const CONVERSATION_MUTATION_LOCK_PREFIX = "junior_conversation";
|
|
572
|
+
/** Serialize one conversation's durable mutations inside a SQL transaction. */
|
|
573
|
+
async function withConversationMutationLock(executor, conversationId, callback) {
|
|
574
|
+
return await executor.withLock(`${CONVERSATION_MUTATION_LOCK_PREFIX}:${conversationId}`, async () => await executor.transaction(callback));
|
|
575
|
+
}
|
|
576
|
+
function now() {
|
|
577
|
+
return Date.now();
|
|
578
|
+
}
|
|
579
|
+
function dateFromMs(ms) {
|
|
580
|
+
return new Date(ms);
|
|
581
|
+
}
|
|
582
|
+
function msFromDate(value) {
|
|
583
|
+
if (value === null || value === void 0) return;
|
|
584
|
+
return (value instanceof Date ? value : new Date(value)).getTime();
|
|
585
|
+
}
|
|
586
|
+
function requiredMsFromDate(value) {
|
|
587
|
+
const ms = msFromDate(value);
|
|
588
|
+
if (typeof ms !== "number" || Number.isNaN(ms)) throw new Error("Conversation record timestamp is invalid");
|
|
589
|
+
return ms;
|
|
590
|
+
}
|
|
591
|
+
function tenantId(value) {
|
|
592
|
+
return value ?? "";
|
|
593
|
+
}
|
|
594
|
+
function sourceFromValue(value) {
|
|
595
|
+
if (value === "api" || value === "internal" || value === "local" || value === "plugin" || value === "event" || value === "scheduler" || value === "slack" || value === "web") return value;
|
|
596
|
+
}
|
|
597
|
+
function originTypeFromSource(source) {
|
|
598
|
+
return source;
|
|
599
|
+
}
|
|
600
|
+
function localWorkspaceFromConversationId(conversationId) {
|
|
601
|
+
return /^local:([^:]+):/.exec(conversationId)?.[1];
|
|
602
|
+
}
|
|
603
|
+
function destinationUpsertFromDestination(args) {
|
|
604
|
+
const { destination } = args;
|
|
605
|
+
if (!destination) return;
|
|
606
|
+
if (destination.platform === "slack") {
|
|
607
|
+
const channelId = destination.channelId;
|
|
608
|
+
return {
|
|
609
|
+
kind: channelId.startsWith("D") ? "dm" : channelId.startsWith("G") ? "group" : "channel",
|
|
610
|
+
provider: "slack",
|
|
611
|
+
providerTenantId: destination.teamId,
|
|
612
|
+
providerDestinationId: channelId,
|
|
613
|
+
refreshVisibility: args.visibility !== void 0,
|
|
614
|
+
visibility: args.visibility ?? "unknown",
|
|
615
|
+
...args.channelName ? { displayName: args.channelName } : void 0,
|
|
616
|
+
metadata: { platform: "slack" }
|
|
617
|
+
};
|
|
618
|
+
}
|
|
619
|
+
return {
|
|
620
|
+
kind: "local_conversation",
|
|
621
|
+
provider: "local",
|
|
622
|
+
providerTenantId: localWorkspaceFromConversationId(destination.conversationId) ?? localWorkspaceFromConversationId(args.conversationId ?? ""),
|
|
623
|
+
providerDestinationId: destination.conversationId,
|
|
624
|
+
refreshVisibility: args.visibility !== void 0,
|
|
625
|
+
visibility: args.visibility ?? "direct",
|
|
626
|
+
metadata: { platform: "local" }
|
|
627
|
+
};
|
|
628
|
+
}
|
|
629
|
+
function executionStatusFromValue(value) {
|
|
630
|
+
if (value === "awaiting_resume" || value === "paused") return "paused";
|
|
631
|
+
if (value === "failed" || value === "idle" || value === "pending" || value === "running") return value;
|
|
632
|
+
throw new Error("Conversation record execution status is invalid");
|
|
633
|
+
}
|
|
634
|
+
function executionStatusToSql(status) {
|
|
635
|
+
return status === "paused" ? "awaiting_resume" : status;
|
|
636
|
+
}
|
|
637
|
+
function destinationFromRow(destination) {
|
|
638
|
+
return parseDestination(destination?.provider === "slack" ? {
|
|
639
|
+
platform: "slack",
|
|
640
|
+
teamId: destination.providerTenantId,
|
|
641
|
+
channelId: destination.providerDestinationId
|
|
642
|
+
} : destination?.provider === "local" ? {
|
|
643
|
+
platform: "local",
|
|
644
|
+
conversationId: destination.providerDestinationId
|
|
645
|
+
} : void 0);
|
|
646
|
+
}
|
|
647
|
+
/** Decode one SQL row and reject invalid durable conversation records. */
|
|
648
|
+
function conversationFromRow(readRow) {
|
|
649
|
+
const row = readRow.conversation;
|
|
650
|
+
const visibility = visibilityFromLocationRow(readRow.destination);
|
|
651
|
+
if (row.schemaVersion !== 1) throw new Error("Conversation record schema version is invalid");
|
|
652
|
+
if (row.destination !== null && readRow.destination === null) throw new Error("Conversation legacy destination is not migrated");
|
|
653
|
+
if (row.actor !== null && readRow.actorIdentity === null) throw new Error("Conversation legacy actor is not migrated");
|
|
654
|
+
const destination = destinationFromRow(readRow.destination);
|
|
655
|
+
const actor = actorFromIdentityRow(readRow.actorIdentity, readRow.actorUserDisplayName);
|
|
656
|
+
if (readRow.destination !== null && !destination) throw new Error("Conversation record destination is invalid");
|
|
657
|
+
const source = row.source === void 0 || row.source === null ? void 0 : sourceFromValue(row.source);
|
|
658
|
+
if (row.source !== void 0 && row.source !== null && !source) throw new Error("Conversation record source is invalid");
|
|
659
|
+
const sessionSource = row.sessionSource === void 0 || row.sessionSource === null ? void 0 : parseSessionSource(row.sessionSource);
|
|
660
|
+
if (row.sessionSource !== void 0 && row.sessionSource !== null && !sessionSource) throw new Error("Conversation record session source is invalid");
|
|
661
|
+
const execution = {
|
|
662
|
+
status: executionStatusFromValue(row.executionStatus),
|
|
663
|
+
lastCheckpointAtMs: msFromDate(row.lastCheckpointAt),
|
|
664
|
+
lastEnqueuedAtMs: msFromDate(row.lastEnqueuedAt),
|
|
665
|
+
...row.runId ? { runId: row.runId } : void 0,
|
|
666
|
+
updatedAtMs: msFromDate(row.executionUpdatedAt) ?? requiredMsFromDate(row.updatedAt)
|
|
667
|
+
};
|
|
668
|
+
const location = locationFromRow(row.location, readRow.destination, sessionSource);
|
|
669
|
+
return {
|
|
670
|
+
schemaVersion: 1,
|
|
671
|
+
conversationId: row.conversationId,
|
|
672
|
+
createdAtMs: requiredMsFromDate(row.createdAt),
|
|
673
|
+
lastActivityAtMs: requiredMsFromDate(row.lastActivityAt),
|
|
674
|
+
updatedAtMs: requiredMsFromDate(row.updatedAt),
|
|
675
|
+
execution,
|
|
676
|
+
executionMetrics: {
|
|
677
|
+
durationMs: row.executionDurationMs,
|
|
678
|
+
...row.metricRunId ? { runId: row.metricRunId } : void 0,
|
|
679
|
+
...row.executionUsage ? { usage: row.executionUsage } : void 0
|
|
680
|
+
},
|
|
681
|
+
...row.parentConversationId ? { parentConversationId: row.parentConversationId } : void 0,
|
|
682
|
+
...destination ? { destination } : void 0,
|
|
683
|
+
...location ? { location } : void 0,
|
|
684
|
+
...actor ? { actor } : void 0,
|
|
685
|
+
...row.channelName ? { channelName: row.channelName } : void 0,
|
|
686
|
+
...source ? { source } : void 0,
|
|
687
|
+
...sessionSource ? { sessionSource } : void 0,
|
|
688
|
+
...row.title ? { title: row.title } : void 0,
|
|
689
|
+
...msFromDate(row.transcriptPurgedAt) !== void 0 ? { transcriptPurgedAtMs: msFromDate(row.transcriptPurgedAt) } : void 0,
|
|
690
|
+
...visibility ? { visibility } : void 0
|
|
691
|
+
};
|
|
692
|
+
}
|
|
693
|
+
function emptyConversation(args) {
|
|
694
|
+
return {
|
|
695
|
+
schemaVersion: 1,
|
|
696
|
+
conversationId: args.conversationId,
|
|
697
|
+
createdAtMs: args.nowMs,
|
|
698
|
+
lastActivityAtMs: args.nowMs,
|
|
699
|
+
updatedAtMs: args.nowMs,
|
|
700
|
+
...args.destination ? { destination: args.destination } : void 0,
|
|
701
|
+
...args.source ? { source: args.source } : void 0,
|
|
702
|
+
...args.sessionSource ? { sessionSource: args.sessionSource } : void 0,
|
|
703
|
+
execution: {
|
|
704
|
+
status: "idle",
|
|
705
|
+
updatedAtMs: args.nowMs
|
|
706
|
+
}
|
|
707
|
+
};
|
|
708
|
+
}
|
|
709
|
+
function assertSameConversationDestination(args) {
|
|
710
|
+
if (!args.current || sameDestination(args.current, args.next)) return;
|
|
711
|
+
throw new Error(`Conversation destination changed for ${args.conversationId}`);
|
|
712
|
+
}
|
|
713
|
+
var SqlStore = class {
|
|
714
|
+
executor;
|
|
715
|
+
constructor(executor) {
|
|
716
|
+
this.executor = executor;
|
|
717
|
+
}
|
|
718
|
+
async getConversationIdByProviderConversation(args) {
|
|
719
|
+
return await getConversationIdByProviderConversation(this.executor, args);
|
|
720
|
+
}
|
|
721
|
+
async getFirstProviderConversationId(args) {
|
|
722
|
+
return await getFirstProviderConversationId(this.executor, args);
|
|
723
|
+
}
|
|
724
|
+
async bindProviderConversation(args) {
|
|
725
|
+
await bindProviderConversation(this.executor, args);
|
|
726
|
+
}
|
|
727
|
+
async createChild(args) {
|
|
728
|
+
const nowMs = args.nowMs ?? now();
|
|
729
|
+
await this.withConversationMutation(args.parentConversationId, async () => {
|
|
730
|
+
await this.withConversationMutation(args.childConversationId, async () => {
|
|
731
|
+
const existing = await this.get({ conversationId: args.childConversationId });
|
|
732
|
+
if (existing) {
|
|
733
|
+
if (existing.parentConversationId !== args.parentConversationId) throw new Error(`Conversation parent changed for ${args.childConversationId}`);
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
736
|
+
const parent = await this.get({ conversationId: args.parentConversationId });
|
|
737
|
+
if (!parent) throw new Error(`Conversation parent is missing for ${args.childConversationId}`);
|
|
738
|
+
if (parent.parentConversationId) throw new Error("Recursive agent delegation is not enabled");
|
|
739
|
+
const child = {
|
|
740
|
+
...emptyConversation({
|
|
741
|
+
conversationId: args.childConversationId,
|
|
742
|
+
nowMs,
|
|
743
|
+
source: args.source
|
|
744
|
+
}),
|
|
745
|
+
parentConversationId: args.parentConversationId
|
|
746
|
+
};
|
|
747
|
+
await this.upsertConversation({ conversation: child });
|
|
748
|
+
});
|
|
749
|
+
});
|
|
750
|
+
}
|
|
751
|
+
async get(args) {
|
|
752
|
+
const row = await this.readConversationRow(args.conversationId);
|
|
753
|
+
if (!row) return;
|
|
754
|
+
return conversationFromRow(row);
|
|
755
|
+
}
|
|
756
|
+
async recordActivity(args) {
|
|
757
|
+
const nowMs = args.nowMs ?? now();
|
|
758
|
+
const activityAtMs = args.activityAtMs ?? nowMs;
|
|
759
|
+
const sessionSource = normalizeSessionSource(args.sessionSource);
|
|
760
|
+
await this.withConversationMutation(args.conversationId, async () => {
|
|
761
|
+
const existing = await this.get({ conversationId: args.conversationId });
|
|
762
|
+
if (existing && args.destination) assertSameConversationDestination({
|
|
763
|
+
conversationId: args.conversationId,
|
|
764
|
+
current: existing.destination,
|
|
765
|
+
next: args.destination
|
|
766
|
+
});
|
|
767
|
+
const current = existing ?? emptyConversation({
|
|
768
|
+
conversationId: args.conversationId,
|
|
769
|
+
destination: args.destination,
|
|
770
|
+
nowMs,
|
|
771
|
+
source: args.source,
|
|
772
|
+
...sessionSource ? { sessionSource } : void 0
|
|
773
|
+
});
|
|
774
|
+
const { sessionSource: _persistedSessionSource, visibility: _persistedVisibility, ...currentWithoutPersistedSignals } = current;
|
|
775
|
+
await this.upsertConversation({ conversation: {
|
|
776
|
+
...currentWithoutPersistedSignals,
|
|
777
|
+
destination: current.destination ?? args.destination,
|
|
778
|
+
source: current.source ?? args.source,
|
|
779
|
+
...sessionSource ? { sessionSource } : void 0,
|
|
780
|
+
channelName: current.channelName ?? args.channelName,
|
|
781
|
+
actor: mergeActor(current.actor, args.actor),
|
|
782
|
+
title: current.title ?? args.title,
|
|
783
|
+
lastActivityAtMs: Math.max(current.lastActivityAtMs, activityAtMs),
|
|
784
|
+
updatedAtMs: nowMs,
|
|
785
|
+
execution: {
|
|
786
|
+
...current.execution,
|
|
787
|
+
updatedAtMs: current.execution.updatedAtMs ?? nowMs
|
|
788
|
+
},
|
|
789
|
+
...args.visibility ? { visibility: args.visibility } : void 0
|
|
790
|
+
} });
|
|
791
|
+
});
|
|
792
|
+
}
|
|
793
|
+
async recordExecution(args) {
|
|
794
|
+
if (args.metrics && !args.execution.runId) throw new Error("Conversation Run metrics require a Run id");
|
|
795
|
+
await this.withConversationMutation(args.conversationId, async () => {
|
|
796
|
+
const existingRow = await this.readConversationRow(args.conversationId);
|
|
797
|
+
const existing = existingRow ? conversationFromRow(existingRow) : void 0;
|
|
798
|
+
if (!existing && !args.destination) return;
|
|
799
|
+
const incomingExecutionAt = args.execution.updatedAtMs ?? args.updatedAtMs;
|
|
800
|
+
const incomingIsFresh = incomingExecutionAt >= (existing?.execution.updatedAtMs ?? existing?.updatedAtMs ?? 0);
|
|
801
|
+
const execution = incomingIsFresh ? args.execution : existing?.execution ?? args.execution;
|
|
802
|
+
await this.upsertConversation({ conversation: {
|
|
803
|
+
schemaVersion: 1,
|
|
804
|
+
conversationId: args.conversationId,
|
|
805
|
+
createdAtMs: args.createdAtMs,
|
|
806
|
+
lastActivityAtMs: args.lastActivityAtMs,
|
|
807
|
+
updatedAtMs: args.updatedAtMs,
|
|
808
|
+
...existing?.parentConversationId ? { parentConversationId: existing.parentConversationId } : void 0,
|
|
809
|
+
...existing?.location ? { location: existing.location } : void 0,
|
|
810
|
+
...args.channelName ? { channelName: args.channelName } : void 0,
|
|
811
|
+
...args.destination ? { destination: args.destination } : void 0,
|
|
812
|
+
...args.actor ? { actor: args.actor } : void 0,
|
|
813
|
+
...args.source ? { source: args.source } : void 0,
|
|
814
|
+
...args.title ? { title: args.title } : void 0,
|
|
815
|
+
...args.visibility ? { visibility: args.visibility } : void 0,
|
|
816
|
+
execution
|
|
817
|
+
} });
|
|
818
|
+
if (args.metrics && args.execution.runId) {
|
|
819
|
+
await replaceConversationMetrics(this.executor, {
|
|
820
|
+
conversationId: args.conversationId,
|
|
821
|
+
runId: args.execution.runId,
|
|
822
|
+
occurredAtMs: args.createdAtMs,
|
|
823
|
+
updatedAtMs: incomingExecutionAt,
|
|
824
|
+
...args.metrics
|
|
825
|
+
});
|
|
826
|
+
if (incomingIsFresh) await this.executor.db().update(juniorConversations).set({
|
|
827
|
+
metricRunId: args.execution.runId,
|
|
828
|
+
executionDurationMs: args.metrics.durationMs,
|
|
829
|
+
executionUsage: args.metrics.usage ?? null
|
|
830
|
+
}).where(eq(juniorConversations.conversationId, args.conversationId));
|
|
831
|
+
}
|
|
832
|
+
});
|
|
833
|
+
}
|
|
834
|
+
async listByActivity(args = {}) {
|
|
835
|
+
const rows = await this.executor.db().select({
|
|
836
|
+
conversation: conversationReadColumns(),
|
|
837
|
+
destination: juniorDestinations,
|
|
838
|
+
actorIdentity: juniorIdentities,
|
|
839
|
+
actorUserDisplayName: juniorUsers.displayName
|
|
840
|
+
}).from(juniorConversations).leftJoin(juniorDestinations, eq(juniorDestinations.id, juniorConversations.destinationId)).leftJoin(juniorIdentities, eq(juniorIdentities.id, juniorConversations.actorIdentityId)).leftJoin(juniorUsers, eq(juniorUsers.id, juniorIdentities.userId)).where(isNull(juniorConversations.parentConversationId)).orderBy(desc(juniorConversations.lastActivityAt), asc(juniorConversations.conversationId)).limit(Math.max(0, args.limit ?? 1e4)).offset(Math.max(0, args.offset ?? 0));
|
|
841
|
+
const conversations = [];
|
|
842
|
+
for (const row of rows) conversations.push(conversationFromRow(row));
|
|
843
|
+
return conversations;
|
|
844
|
+
}
|
|
845
|
+
async getDestinationVisibility(args) {
|
|
846
|
+
const row = (await this.executor.db().select({ visibility: juniorDestinations.visibility }).from(juniorDestinations).where(and(eq(juniorDestinations.provider, args.provider), eq(juniorDestinations.providerTenantId, tenantId(args.providerTenantId)), eq(juniorDestinations.providerDestinationId, args.providerDestinationId))))[0];
|
|
847
|
+
if (!row) return;
|
|
848
|
+
if (row.visibility === "public" || row.visibility === "private") return row.visibility;
|
|
849
|
+
}
|
|
850
|
+
async findSlackDestinationByName(args) {
|
|
851
|
+
const normalized = args.channelName.trim().replace(/^#/, "").toLowerCase();
|
|
852
|
+
if (!normalized) return;
|
|
853
|
+
const rows = await this.executor.db().select({
|
|
854
|
+
channelId: juniorDestinations.providerDestinationId,
|
|
855
|
+
channelName: juniorDestinations.displayName
|
|
856
|
+
}).from(juniorDestinations).where(and(eq(juniorDestinations.provider, "slack"), eq(juniorDestinations.providerTenantId, tenantId(args.teamId)), eq(juniorDestinations.kind, "channel"), sql`lower(ltrim(${juniorDestinations.displayName}, '#')) = ${normalized}`)).limit(2);
|
|
857
|
+
if (rows.length !== 1) return;
|
|
858
|
+
const row = rows[0];
|
|
859
|
+
if (!row?.channelId) return;
|
|
860
|
+
return {
|
|
861
|
+
channelId: row.channelId,
|
|
862
|
+
...row.channelName ? { channelName: row.channelName } : void 0
|
|
863
|
+
};
|
|
864
|
+
}
|
|
865
|
+
/** Serialize all durable mutations for one conversation inside a SQL transaction. */
|
|
866
|
+
async withConversationMutation(conversationId, callback) {
|
|
867
|
+
return await withConversationMutationLock(this.executor, conversationId, callback);
|
|
868
|
+
}
|
|
869
|
+
async readConversationRow(conversationId) {
|
|
870
|
+
return (await this.executor.db().select({
|
|
871
|
+
conversation: conversationReadColumns(),
|
|
872
|
+
destination: juniorDestinations,
|
|
873
|
+
actorIdentity: juniorIdentities,
|
|
874
|
+
actorUserDisplayName: juniorUsers.displayName
|
|
875
|
+
}).from(juniorConversations).leftJoin(juniorDestinations, eq(juniorDestinations.id, juniorConversations.destinationId)).leftJoin(juniorIdentities, eq(juniorIdentities.id, juniorConversations.actorIdentityId)).leftJoin(juniorUsers, eq(juniorUsers.id, juniorIdentities.userId)).where(eq(juniorConversations.conversationId, conversationId)))[0];
|
|
876
|
+
}
|
|
877
|
+
/** Upsert the conversation row while preserving previously discovered nullable metadata fields. */
|
|
878
|
+
async upsertConversation(args) {
|
|
879
|
+
const { conversation } = args;
|
|
880
|
+
if (!conversation.parentConversationId && !conversation.destination) {
|
|
881
|
+
if (!await this.get({ conversationId: conversation.conversationId })) throw new Error(`Conversation ${conversation.conversationId} requires a destination at create`);
|
|
882
|
+
}
|
|
883
|
+
const incomingExecutionVersion = sql`coalesce(excluded.execution_updated_at, excluded.updated_at)`;
|
|
884
|
+
const currentExecutionVersion = sql`coalesce(${juniorConversations.executionUpdatedAt}, ${juniorConversations.updatedAt})`;
|
|
885
|
+
const incomingExecutionIsFresh = sql`${incomingExecutionVersion} >= ${currentExecutionVersion}`;
|
|
886
|
+
const destinationId = await this.upsertDestination(destinationUpsertFromDestination({
|
|
887
|
+
channelName: conversation.channelName,
|
|
888
|
+
conversationId: conversation.conversationId,
|
|
889
|
+
destination: conversation.destination,
|
|
890
|
+
...conversation.visibility ? { visibility: conversation.visibility } : void 0
|
|
891
|
+
}), conversation.updatedAtMs);
|
|
892
|
+
const location = locationForWrite({
|
|
893
|
+
destination: conversation.destination,
|
|
894
|
+
destinationId,
|
|
895
|
+
location: conversation.location,
|
|
896
|
+
sessionSource: conversation.sessionSource
|
|
897
|
+
});
|
|
898
|
+
const actorIdentityObservation = actorIdentityForConversation(conversation);
|
|
899
|
+
const actorIdentity = actorIdentityObservation ? await upsertIdentity(this.executor, actorIdentityObservation, conversation.updatedAtMs) : void 0;
|
|
900
|
+
const rootConversationId = conversation.parentConversationId ? sql`(
|
|
901
|
+
select parent.root_conversation_id
|
|
902
|
+
from junior_conversations parent
|
|
903
|
+
where parent.conversation_id = ${conversation.parentConversationId}
|
|
904
|
+
)` : conversation.conversationId;
|
|
905
|
+
const row = (await this.executor.db().insert(juniorConversations).values({
|
|
906
|
+
conversationId: conversation.conversationId,
|
|
907
|
+
schemaVersion: 1,
|
|
908
|
+
source: conversation.source ?? null,
|
|
909
|
+
sessionSource: conversation.sessionSource ?? null,
|
|
910
|
+
originType: originTypeFromSource(conversation.source) ?? null,
|
|
911
|
+
originId: null,
|
|
912
|
+
originRunId: null,
|
|
913
|
+
location: location ?? null,
|
|
914
|
+
destinationId: destinationId ?? null,
|
|
915
|
+
destination: null,
|
|
916
|
+
actorIdentityId: actorIdentity?.id ?? null,
|
|
917
|
+
creatorIdentityId: null,
|
|
918
|
+
credentialSubjectIdentityId: null,
|
|
919
|
+
actor: null,
|
|
920
|
+
channelName: conversation.channelName ?? null,
|
|
921
|
+
title: conversation.title ?? null,
|
|
922
|
+
createdAt: dateFromMs(conversation.createdAtMs),
|
|
923
|
+
lastActivityAt: dateFromMs(conversation.lastActivityAtMs),
|
|
924
|
+
updatedAt: dateFromMs(conversation.updatedAtMs),
|
|
925
|
+
executionUpdatedAt: conversation.execution.updatedAtMs === void 0 ? null : dateFromMs(conversation.execution.updatedAtMs),
|
|
926
|
+
executionStatus: executionStatusToSql(conversation.execution.status),
|
|
927
|
+
runId: conversation.execution.runId ?? null,
|
|
928
|
+
lastCheckpointAt: conversation.execution.lastCheckpointAtMs === void 0 ? null : dateFromMs(conversation.execution.lastCheckpointAtMs),
|
|
929
|
+
lastEnqueuedAt: conversation.execution.lastEnqueuedAtMs === void 0 ? null : dateFromMs(conversation.execution.lastEnqueuedAtMs),
|
|
930
|
+
parentConversationId: conversation.parentConversationId ?? null,
|
|
931
|
+
rootConversationId
|
|
932
|
+
}).onConflictDoUpdate({
|
|
933
|
+
target: juniorConversations.conversationId,
|
|
934
|
+
set: {
|
|
935
|
+
source: sql`coalesce(${juniorConversations.source}, excluded.source)`,
|
|
936
|
+
sessionSource: sql`coalesce(${juniorConversations.sessionSource}, excluded.source_json)`,
|
|
937
|
+
originType: sql`coalesce(excluded.origin_type, ${juniorConversations.originType})`,
|
|
938
|
+
originId: sql`coalesce(excluded.origin_id, ${juniorConversations.originId})`,
|
|
939
|
+
originRunId: sql`coalesce(excluded.origin_run_id, ${juniorConversations.originRunId})`,
|
|
940
|
+
location: sql`coalesce(excluded.location_json, ${juniorConversations.location})`,
|
|
941
|
+
destinationId: sql`coalesce(excluded.destination_id, ${juniorConversations.destinationId})`,
|
|
942
|
+
actorIdentityId: sql`coalesce(excluded.actor_identity_id, ${juniorConversations.actorIdentityId})`,
|
|
943
|
+
creatorIdentityId: sql`coalesce(excluded.creator_identity_id, ${juniorConversations.creatorIdentityId})`,
|
|
944
|
+
credentialSubjectIdentityId: sql`coalesce(excluded.credential_subject_identity_id, ${juniorConversations.credentialSubjectIdentityId})`,
|
|
945
|
+
channelName: sql`coalesce(excluded.channel_name, ${juniorConversations.channelName})`,
|
|
946
|
+
title: sql`coalesce(excluded.title, ${juniorConversations.title})`,
|
|
947
|
+
createdAt: sql`least(${juniorConversations.createdAt}, excluded.created_at)`,
|
|
948
|
+
lastActivityAt: sql`greatest(${juniorConversations.lastActivityAt}, excluded.last_activity_at)`,
|
|
949
|
+
updatedAt: sql`greatest(${juniorConversations.updatedAt}, excluded.updated_at)`,
|
|
950
|
+
executionUpdatedAt: sql`case when ${incomingExecutionIsFresh} then excluded.execution_updated_at else ${juniorConversations.executionUpdatedAt} end`,
|
|
951
|
+
executionStatus: sql`case when ${incomingExecutionIsFresh} then excluded.execution_status else ${juniorConversations.executionStatus} end`,
|
|
952
|
+
runId: sql`case when ${incomingExecutionIsFresh} then excluded.run_id else ${juniorConversations.runId} end`,
|
|
953
|
+
lastCheckpointAt: sql`case when ${incomingExecutionIsFresh} then coalesce(excluded.last_checkpoint_at, ${juniorConversations.lastCheckpointAt}) else ${juniorConversations.lastCheckpointAt} end`,
|
|
954
|
+
lastEnqueuedAt: sql`case when ${incomingExecutionIsFresh} then coalesce(excluded.last_enqueued_at, ${juniorConversations.lastEnqueuedAt}) else ${juniorConversations.lastEnqueuedAt} end`,
|
|
955
|
+
rootConversationId: sql`coalesce(${juniorConversations.rootConversationId}, excluded.root_conversation_id)`
|
|
956
|
+
}
|
|
957
|
+
}).returning({
|
|
958
|
+
actorIdentityId: juniorConversations.actorIdentityId,
|
|
959
|
+
parentConversationId: juniorConversations.parentConversationId,
|
|
960
|
+
rootConversationId: juniorConversations.rootConversationId
|
|
961
|
+
}))[0];
|
|
962
|
+
if (!row?.rootConversationId) throw new Error("Conversation parent is missing its persisted root");
|
|
963
|
+
if (row.actorIdentityId && row.parentConversationId === null) await recordConversationParticipant(this.executor, {
|
|
964
|
+
actorIdentityId: row.actorIdentityId,
|
|
965
|
+
conversationId: conversation.conversationId,
|
|
966
|
+
atMs: conversation.lastActivityAtMs
|
|
967
|
+
});
|
|
968
|
+
}
|
|
969
|
+
async upsertDestination(destination, nowMs) {
|
|
970
|
+
if (!destination) return;
|
|
971
|
+
const visibilityUpdate = destination.refreshVisibility ? sql`excluded.visibility` : juniorDestinations.visibility;
|
|
972
|
+
return (await this.executor.db().insert(juniorDestinations).values({
|
|
973
|
+
id: randomUUID(),
|
|
974
|
+
provider: destination.provider,
|
|
975
|
+
providerTenantId: tenantId(destination.providerTenantId),
|
|
976
|
+
providerDestinationId: destination.providerDestinationId,
|
|
977
|
+
kind: destination.kind,
|
|
978
|
+
parentDestinationId: null,
|
|
979
|
+
displayName: destination.displayName ?? null,
|
|
980
|
+
visibility: destination.visibility,
|
|
981
|
+
metadata: destination.metadata ?? null,
|
|
982
|
+
createdAt: dateFromMs(nowMs),
|
|
983
|
+
updatedAt: dateFromMs(nowMs)
|
|
984
|
+
}).onConflictDoUpdate({
|
|
985
|
+
target: [
|
|
986
|
+
juniorDestinations.provider,
|
|
987
|
+
juniorDestinations.providerTenantId,
|
|
988
|
+
juniorDestinations.providerDestinationId
|
|
989
|
+
],
|
|
990
|
+
set: {
|
|
991
|
+
kind: sql`excluded.kind`,
|
|
992
|
+
displayName: sql`coalesce(excluded.display_name, ${juniorDestinations.displayName})`,
|
|
993
|
+
visibility: visibilityUpdate,
|
|
994
|
+
metadata: sql`coalesce(excluded.metadata_json, ${juniorDestinations.metadata})`,
|
|
995
|
+
updatedAt: sql`excluded.updated_at`
|
|
996
|
+
}
|
|
997
|
+
}).returning({ id: juniorDestinations.id }))[0]?.id;
|
|
998
|
+
}
|
|
999
|
+
};
|
|
1000
|
+
/** Create a SQL-backed conversation store. */
|
|
1001
|
+
function createSqlStore(executor) {
|
|
1002
|
+
return new SqlStore(executor);
|
|
1003
|
+
}
|
|
1004
|
+
//#endregion
|
|
1005
|
+
//#region src/chat/conversations/sql/conversation-row.ts
|
|
1006
|
+
/**
|
|
1007
|
+
* Establish the conversation metadata row on first contact for content writes
|
|
1008
|
+
* (conversation events, visible transcript) that can land before activity has
|
|
1009
|
+
* created the row, and whose tables FK to it.
|
|
1010
|
+
*
|
|
1011
|
+
* First contact creates the row; every later content write advances the
|
|
1012
|
+
* activity clock so retention mirrors append-refresh semantics (each content
|
|
1013
|
+
* append refreshes the retention window). Background observations may opt out
|
|
1014
|
+
* and leave an existing row unchanged. The timestamp is content-intrinsic (the
|
|
1015
|
+
* first item's `createdAtMs`), and `greatest(...)` guarantees a backfilled or
|
|
1016
|
+
* imported historical write — which carries an old timestamp — can never
|
|
1017
|
+
* regress `last_activity_at`.
|
|
1018
|
+
*/
|
|
1019
|
+
async function ensureConversationRow(executor, conversationId, atMs, options = {}) {
|
|
1020
|
+
const at = new Date(atMs);
|
|
1021
|
+
const insert = executor.db().insert(juniorConversations).values({
|
|
1022
|
+
conversationId,
|
|
1023
|
+
createdAt: at,
|
|
1024
|
+
lastActivityAt: at,
|
|
1025
|
+
updatedAt: at,
|
|
1026
|
+
executionStatus: "idle"
|
|
1027
|
+
});
|
|
1028
|
+
if (options.activity === "preserve") {
|
|
1029
|
+
await insert.onConflictDoNothing({ target: juniorConversations.conversationId });
|
|
1030
|
+
return;
|
|
1031
|
+
}
|
|
1032
|
+
await insert.onConflictDoUpdate({
|
|
1033
|
+
target: juniorConversations.conversationId,
|
|
1034
|
+
set: {
|
|
1035
|
+
lastActivityAt: sql`greatest(${juniorConversations.lastActivityAt}, excluded.last_activity_at)`,
|
|
1036
|
+
updatedAt: sql`greatest(${juniorConversations.updatedAt}, excluded.updated_at)`,
|
|
1037
|
+
transcriptPurgedAt: null
|
|
1038
|
+
}
|
|
1039
|
+
});
|
|
1040
|
+
}
|
|
1041
|
+
//#endregion
|
|
1042
|
+
//#region src/chat/conversations/sql/event-actor.ts
|
|
1043
|
+
const SLACK_USER_ID_PATTERN = /^[UW][A-Z0-9]{5,}$/;
|
|
1044
|
+
function isRecord(value) {
|
|
1045
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
1046
|
+
}
|
|
1047
|
+
function readAuthor(meta) {
|
|
1048
|
+
if (!isRecord(meta) || !isRecord(meta.author)) return {};
|
|
1049
|
+
const author = meta.author;
|
|
1050
|
+
return {
|
|
1051
|
+
...typeof author.email === "string" ? { email: author.email } : void 0,
|
|
1052
|
+
...typeof author.fullName === "string" ? { fullName: author.fullName } : void 0,
|
|
1053
|
+
...typeof author.isBot === "boolean" ? { isBot: author.isBot } : void 0,
|
|
1054
|
+
...typeof author.userId === "string" ? { userId: author.userId } : void 0,
|
|
1055
|
+
...typeof author.userName === "string" ? { userName: author.userName } : void 0
|
|
1056
|
+
};
|
|
1057
|
+
}
|
|
1058
|
+
async function loadConversationSlackTenantId(executor, conversationId) {
|
|
1059
|
+
const [row] = await executor.db().select({
|
|
1060
|
+
providerTenantId: juniorDestinations.providerTenantId,
|
|
1061
|
+
sessionSource: juniorConversations.sessionSource
|
|
1062
|
+
}).from(juniorConversations).leftJoin(juniorDestinations, eq(juniorDestinations.id, juniorConversations.destinationId)).where(eq(juniorConversations.conversationId, conversationId)).limit(1);
|
|
1063
|
+
const destinationTenant = row?.providerTenantId?.trim();
|
|
1064
|
+
if (destinationTenant) return destinationTenant;
|
|
1065
|
+
const sessionSource = parseSessionSource(row?.sessionSource);
|
|
1066
|
+
if (sessionSource?.kind === "slack") return sessionSource.teamId;
|
|
1067
|
+
}
|
|
1068
|
+
async function findIdentityIdByProviderSubject(executor, args) {
|
|
1069
|
+
return (await executor.db().select({ id: juniorIdentities.id }).from(juniorIdentities).where(and(eq(juniorIdentities.provider, args.provider), eq(juniorIdentities.providerSubjectId, args.providerSubjectId), args.providerTenantId === void 0 ? void 0 : eq(juniorIdentities.providerTenantId, args.providerTenantId))).limit(1))[0]?.id;
|
|
1070
|
+
}
|
|
1071
|
+
async function findJuniorIdentityIdByEmail(executor, email) {
|
|
1072
|
+
const normalized = email.trim().toLowerCase();
|
|
1073
|
+
if (!normalized) return void 0;
|
|
1074
|
+
return (await executor.db().select({ id: juniorIdentities.id }).from(juniorIdentities).where(and(eq(juniorIdentities.provider, "junior"), eq(juniorIdentities.kind, "user"), or(eq(juniorIdentities.providerSubjectId, normalized), and(eq(juniorIdentities.emailNormalized, normalized), eq(juniorIdentities.emailVerified, true))))).limit(1))[0]?.id;
|
|
1075
|
+
}
|
|
1076
|
+
/**
|
|
1077
|
+
* Resolve a dashboard:<hash> author against junior identities keyed by email.
|
|
1078
|
+
* Web actors store userId as dashboard:${sha256(email).slice(0,24)} while the
|
|
1079
|
+
* durable identity subject remains the verified email.
|
|
1080
|
+
*/
|
|
1081
|
+
async function findJuniorIdentityIdByDashboardAuthorId(executor, dashboardAuthorId) {
|
|
1082
|
+
if (!dashboardAuthorId.startsWith("dashboard:")) return;
|
|
1083
|
+
return (await executor.db().select({ id: juniorIdentities.id }).from(juniorIdentities).where(and(eq(juniorIdentities.provider, "junior"), eq(juniorIdentities.kind, "user"), eq(juniorIdentities.emailVerified, true), sql`${juniorIdentities.emailNormalized} is not null`, sql`concat(
|
|
1084
|
+
'dashboard:',
|
|
1085
|
+
left(encode(digest(${juniorIdentities.emailNormalized}, 'sha256'), 'hex'), 24)
|
|
1086
|
+
) = ${dashboardAuthorId}`)).limit(1))[0]?.id;
|
|
1087
|
+
}
|
|
1088
|
+
/**
|
|
1089
|
+
* Resolve the durable actor identity for one event payload.
|
|
1090
|
+
* Human user messages get an identity when author data is enough; other events
|
|
1091
|
+
* keep a null actor.
|
|
1092
|
+
*/
|
|
1093
|
+
async function resolveEventActorIdentityId(executor, args) {
|
|
1094
|
+
const data = args.data;
|
|
1095
|
+
if (data.type !== "message" && data.type !== "message_updated") return;
|
|
1096
|
+
if (typeof data.authorIdentityId === "string" && data.authorIdentityId) return data.authorIdentityId;
|
|
1097
|
+
if (data.role !== "user") return;
|
|
1098
|
+
const author = readAuthor(data.meta);
|
|
1099
|
+
if (author.isBot === true) return;
|
|
1100
|
+
const userId = parseActorUserId(author.userId);
|
|
1101
|
+
if (!userId) return;
|
|
1102
|
+
if (SLACK_USER_ID_PATTERN.test(userId)) {
|
|
1103
|
+
const tenantId = await loadConversationSlackTenantId(executor, args.conversationId);
|
|
1104
|
+
const observation = {
|
|
1105
|
+
kind: "user",
|
|
1106
|
+
provider: "slack",
|
|
1107
|
+
providerSubjectId: userId,
|
|
1108
|
+
...tenantId ? { providerTenantId: tenantId } : void 0,
|
|
1109
|
+
...author.fullName ? { displayName: author.fullName } : void 0,
|
|
1110
|
+
...author.userName ? { handle: author.userName } : void 0,
|
|
1111
|
+
...author.email ? {
|
|
1112
|
+
email: author.email,
|
|
1113
|
+
emailVerified: true
|
|
1114
|
+
} : void 0
|
|
1115
|
+
};
|
|
1116
|
+
if (tenantId) return (await upsertIdentity(executor, observation, args.nowMs)).id;
|
|
1117
|
+
return await findIdentityIdByProviderSubject(executor, {
|
|
1118
|
+
provider: "slack",
|
|
1119
|
+
providerSubjectId: userId
|
|
1120
|
+
}) ?? void 0;
|
|
1121
|
+
}
|
|
1122
|
+
const email = typeof author.email === "string" && author.email.includes("@") ? author.email.trim().toLowerCase() : userId.includes("@") ? userId.trim().toLowerCase() : void 0;
|
|
1123
|
+
if (email) return (await upsertIdentity(executor, {
|
|
1124
|
+
kind: "user",
|
|
1125
|
+
provider: "junior",
|
|
1126
|
+
providerSubjectId: email,
|
|
1127
|
+
email,
|
|
1128
|
+
emailVerified: true,
|
|
1129
|
+
...author.fullName ? { displayName: author.fullName } : void 0,
|
|
1130
|
+
metadata: { platform: "web" }
|
|
1131
|
+
}, args.nowMs)).id;
|
|
1132
|
+
if (userId.startsWith("dashboard:")) return findJuniorIdentityIdByDashboardAuthorId(executor, userId);
|
|
1133
|
+
return findJuniorIdentityIdByEmail(executor, userId);
|
|
1134
|
+
}
|
|
1135
|
+
/** Drop payload-only authorIdentityId after lifting it onto the event column. */
|
|
1136
|
+
function stripPayloadAuthorIdentityId(data) {
|
|
1137
|
+
if (data.type !== "message" && data.type !== "message_updated" || data.authorIdentityId === void 0) return data;
|
|
1138
|
+
const { authorIdentityId: _authorIdentityId, ...rest } = data;
|
|
1139
|
+
return rest;
|
|
1140
|
+
}
|
|
1141
|
+
//#endregion
|
|
1142
|
+
//#region src/db/postgres-json.ts
|
|
1143
|
+
/**
|
|
1144
|
+
* Replace actual NUL characters that PostgreSQL cannot store in JSONB.
|
|
1145
|
+
* Literal `\\u0000` text is preserved because it contains no NUL character.
|
|
1146
|
+
*/
|
|
1147
|
+
function sanitizePostgresJson(value) {
|
|
1148
|
+
if (typeof value === "string") return value.replaceAll("\0", " ");
|
|
1149
|
+
if (Array.isArray(value)) return value.map((item) => sanitizePostgresJson(item));
|
|
1150
|
+
if (value === null || typeof value !== "object") return value;
|
|
1151
|
+
const prototype = Object.getPrototypeOf(value);
|
|
1152
|
+
if (prototype !== Object.prototype && prototype !== null) return value;
|
|
1153
|
+
return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, sanitizePostgresJson(item)]));
|
|
1154
|
+
}
|
|
1155
|
+
//#endregion
|
|
1156
|
+
//#region src/chat/conversations/sql/event-lock.ts
|
|
1157
|
+
/** Serialize event sequence allocation with other conversation writes. */
|
|
1158
|
+
async function withConversationEventLock(executor, conversationId, callback) {
|
|
1159
|
+
return executor.withLock(`junior_conversation:${conversationId}`, callback);
|
|
1160
|
+
}
|
|
1161
|
+
//#endregion
|
|
1162
|
+
//#region src/chat/conversations/sql/history.ts
|
|
1163
|
+
const messageHistoryEventTypes = [
|
|
1164
|
+
"message",
|
|
1165
|
+
"message_updated",
|
|
1166
|
+
"message_handled"
|
|
1167
|
+
];
|
|
1168
|
+
const HUMAN_INSTRUCTION_PLATFORMS = /* @__PURE__ */ new Set([
|
|
1169
|
+
"slack",
|
|
1170
|
+
"local",
|
|
1171
|
+
"web"
|
|
1172
|
+
]);
|
|
1173
|
+
/**
|
|
1174
|
+
* Whether one event should restore an archived conversation to the feed.
|
|
1175
|
+
*
|
|
1176
|
+
* Archive hides finished noise until a human comes back. Events,
|
|
1177
|
+
* turn lifecycle, compaction, and other system writes may still refresh
|
|
1178
|
+
* activity clocks, but they must not unarchive on their own.
|
|
1179
|
+
*/
|
|
1180
|
+
function eventUnarchivesConversation(data) {
|
|
1181
|
+
if (data.type === "user_message") {
|
|
1182
|
+
const provenance = data.provenance;
|
|
1183
|
+
if (provenance.authority !== "instruction") return false;
|
|
1184
|
+
const platform = provenance.actor?.platform;
|
|
1185
|
+
return platform === void 0 || HUMAN_INSTRUCTION_PLATFORMS.has(platform);
|
|
1186
|
+
}
|
|
1187
|
+
if (data.type !== "message") return false;
|
|
1188
|
+
if (data.role !== "user") return false;
|
|
1189
|
+
const meta = data.meta;
|
|
1190
|
+
if (!meta) return true;
|
|
1191
|
+
if (typeof meta.eventType === "string" && meta.eventType.length > 0) return false;
|
|
1192
|
+
const author = meta.author;
|
|
1193
|
+
if (author && typeof author === "object" && !Array.isArray(author) && author.isBot === true) return false;
|
|
1194
|
+
return true;
|
|
1195
|
+
}
|
|
1196
|
+
/** Split validated event data into column-lifted and JSON payload fields. */
|
|
1197
|
+
function insertFromEvent(conversationId, seq, historyVersion, event, actorIdentityId) {
|
|
1198
|
+
const stripped = stripPayloadAuthorIdentityId(event.data);
|
|
1199
|
+
const data = conversationEventDataSchema.parse(stripped);
|
|
1200
|
+
const encoded = encodeHistoryPayload(data);
|
|
1201
|
+
return {
|
|
1202
|
+
conversationId,
|
|
1203
|
+
seq,
|
|
1204
|
+
historyVersion,
|
|
1205
|
+
schemaVersion: encoded.schemaVersion,
|
|
1206
|
+
idempotencyKey: event.idempotencyKey ?? null,
|
|
1207
|
+
type: data.type,
|
|
1208
|
+
payload: sanitizePostgresJson(encoded.payload),
|
|
1209
|
+
actorIdentityId: actorIdentityId ?? null,
|
|
1210
|
+
createdAt: new Date(event.createdAtMs)
|
|
1211
|
+
};
|
|
1212
|
+
}
|
|
1213
|
+
/** Parse one physical event row into the storage-compatible domain envelope. */
|
|
1214
|
+
function eventFromRow(row) {
|
|
1215
|
+
const payload = row.actorIdentityId && typeof row.payload === "object" && row.payload !== null && !Array.isArray(row.payload) && (row.type === "message" || row.type === "message_updated") && !("authorIdentityId" in row.payload) ? {
|
|
1216
|
+
...row.payload,
|
|
1217
|
+
authorIdentityId: row.actorIdentityId
|
|
1218
|
+
} : row.payload;
|
|
1219
|
+
return decodeStoredConversationEvent({
|
|
1220
|
+
schemaVersion: row.schemaVersion,
|
|
1221
|
+
seq: row.seq,
|
|
1222
|
+
historyVersion: row.historyVersion,
|
|
1223
|
+
...row.idempotencyKey ? { idempotencyKey: row.idempotencyKey } : void 0,
|
|
1224
|
+
createdAtMs: row.createdAt.getTime(),
|
|
1225
|
+
type: row.type,
|
|
1226
|
+
payload
|
|
1227
|
+
});
|
|
1228
|
+
}
|
|
1229
|
+
/** Decode the summary row that defines a readable message-history suffix. */
|
|
1230
|
+
function messagesSummarizedEventFromRow(row) {
|
|
1231
|
+
const event = eventFromRow(row);
|
|
1232
|
+
if (event.schemaVersion !== 1 || event.data.type !== "messages_summarized") throw new Error("Message compaction row did not decode as messages_summarized");
|
|
1233
|
+
return {
|
|
1234
|
+
...event,
|
|
1235
|
+
schemaVersion: 1,
|
|
1236
|
+
data: event.data
|
|
1237
|
+
};
|
|
1238
|
+
}
|
|
1239
|
+
var SqlConversationEventStore = class {
|
|
1240
|
+
executor;
|
|
1241
|
+
constructor(executor) {
|
|
1242
|
+
this.executor = executor;
|
|
1243
|
+
}
|
|
1244
|
+
async append(conversationId, events, options = {}) {
|
|
1245
|
+
const parsed = events.map((event) => newConversationEventSchema.parse(event));
|
|
1246
|
+
if (parsed.length === 0) return [];
|
|
1247
|
+
return withConversationEventLock(this.executor, conversationId, async () => {
|
|
1248
|
+
const existingKeys = parsed.map((event) => event.idempotencyKey).filter((key) => key !== void 0);
|
|
1249
|
+
const persistedKeys = existingKeys.length === 0 ? /* @__PURE__ */ new Set() : new Set((await this.executor.db().select({ key: juniorConversationEvents.idempotencyKey }).from(juniorConversationEvents).where(and(eq(juniorConversationEvents.conversationId, conversationId), inArray(juniorConversationEvents.idempotencyKey, existingKeys)))).flatMap((row) => row.key ? [row.key] : []));
|
|
1250
|
+
const acceptedKeys = new Set(persistedKeys);
|
|
1251
|
+
const pending = parsed.filter((event) => {
|
|
1252
|
+
if (event.idempotencyKey === void 0) return true;
|
|
1253
|
+
if (acceptedKeys.has(event.idempotencyKey)) return false;
|
|
1254
|
+
acceptedKeys.add(event.idempotencyKey);
|
|
1255
|
+
return true;
|
|
1256
|
+
});
|
|
1257
|
+
if (pending.length === 0) return [];
|
|
1258
|
+
const newestCreatedAtMs = Math.max(...pending.map((event) => event.createdAtMs));
|
|
1259
|
+
await ensureConversationRow(this.executor, conversationId, newestCreatedAtMs, options);
|
|
1260
|
+
const cursor = await this.readCursor(conversationId);
|
|
1261
|
+
const historyVersion = cursor.maxHistoryVersion ?? 0;
|
|
1262
|
+
let seq = cursor.nextSeq;
|
|
1263
|
+
const rows = [];
|
|
1264
|
+
for (const event of pending) {
|
|
1265
|
+
const actorIdentityId = await resolveEventActorIdentityId(this.executor, {
|
|
1266
|
+
conversationId,
|
|
1267
|
+
data: event.data,
|
|
1268
|
+
nowMs: event.createdAtMs
|
|
1269
|
+
});
|
|
1270
|
+
rows.push(insertFromEvent(conversationId, seq++, historyVersion, event, actorIdentityId));
|
|
1271
|
+
if (actorIdentityId) await recordConversationParticipant(this.executor, {
|
|
1272
|
+
actorIdentityId,
|
|
1273
|
+
conversationId,
|
|
1274
|
+
atMs: event.createdAtMs,
|
|
1275
|
+
restoreArchive: eventUnarchivesConversation(event.data)
|
|
1276
|
+
});
|
|
1277
|
+
}
|
|
1278
|
+
await this.executor.db().insert(juniorConversationEvents).values(rows);
|
|
1279
|
+
return rows.map((row) => ({
|
|
1280
|
+
seq: row.seq,
|
|
1281
|
+
historyVersion: row.historyVersion
|
|
1282
|
+
}));
|
|
1283
|
+
});
|
|
1284
|
+
}
|
|
1285
|
+
async replaceHistory(conversationId, replacement) {
|
|
1286
|
+
const parsed = historyReplacementSchema.parse(replacement);
|
|
1287
|
+
await withConversationEventLock(this.executor, conversationId, async () => {
|
|
1288
|
+
await ensureConversationRow(this.executor, conversationId, Date.now());
|
|
1289
|
+
const cursor = await this.readCursor(conversationId);
|
|
1290
|
+
const historyVersion = (cursor.maxHistoryVersion ?? 0) + 1;
|
|
1291
|
+
await this.executor.db().insert(juniorConversationEvents).values(insertFromEvent(conversationId, cursor.nextSeq, historyVersion, parsed));
|
|
1292
|
+
});
|
|
1293
|
+
}
|
|
1294
|
+
async loadCurrentHistoryVersion(conversationId) {
|
|
1295
|
+
return (await this.readCursor(conversationId)).maxHistoryVersion ?? 0;
|
|
1296
|
+
}
|
|
1297
|
+
async loadCurrentHistory(conversationId) {
|
|
1298
|
+
const cursor = await this.readCursor(conversationId);
|
|
1299
|
+
if (cursor.maxHistoryVersion === null) return [];
|
|
1300
|
+
return (await this.executor.db().select().from(juniorConversationEvents).where(and(eq(juniorConversationEvents.conversationId, conversationId), eq(juniorConversationEvents.historyVersion, cursor.maxHistoryVersion))).orderBy(asc(juniorConversationEvents.seq))).map(eventFromRow);
|
|
1301
|
+
}
|
|
1302
|
+
async loadByIdempotencyKey(conversationId, idempotencyKey) {
|
|
1303
|
+
const [row] = await this.executor.db().select().from(juniorConversationEvents).where(and(eq(juniorConversationEvents.conversationId, conversationId), eq(juniorConversationEvents.idempotencyKey, idempotencyKey))).limit(1);
|
|
1304
|
+
return row ? eventFromRow(row) : void 0;
|
|
1305
|
+
}
|
|
1306
|
+
async loadHistoryContaining(conversationId, seq, throughSeq) {
|
|
1307
|
+
const [boundary] = await this.executor.db().select({ historyVersion: juniorConversationEvents.historyVersion }).from(juniorConversationEvents).where(and(eq(juniorConversationEvents.conversationId, conversationId), eq(juniorConversationEvents.seq, seq))).limit(1);
|
|
1308
|
+
if (!boundary) return void 0;
|
|
1309
|
+
return (await this.executor.db().select().from(juniorConversationEvents).where(and(eq(juniorConversationEvents.conversationId, conversationId), eq(juniorConversationEvents.historyVersion, boundary.historyVersion), throughSeq === void 0 ? void 0 : lte(juniorConversationEvents.seq, throughSeq))).orderBy(asc(juniorConversationEvents.seq))).map(eventFromRow);
|
|
1310
|
+
}
|
|
1311
|
+
async loadLatestStructuredEvent(conversationId, namespace, name) {
|
|
1312
|
+
const [row] = await this.executor.db().select().from(juniorConversationEvents).where(and(eq(juniorConversationEvents.conversationId, conversationId), eq(juniorConversationEvents.type, "structured_event"), sql`${juniorConversationEvents.payload}->>'namespace' = ${namespace}`, sql`${juniorConversationEvents.payload}->>'name' = ${name}`)).orderBy(desc(juniorConversationEvents.seq)).limit(1);
|
|
1313
|
+
return row ? eventFromRow(row) : void 0;
|
|
1314
|
+
}
|
|
1315
|
+
async loadLatestInstruction(conversationId) {
|
|
1316
|
+
const [row] = await this.executor.db().select().from(juniorConversationEvents).where(and(eq(juniorConversationEvents.conversationId, conversationId), eq(juniorConversationEvents.type, "user_message"), sql`${juniorConversationEvents.payload}->'provenance'->>'authority' = 'instruction'`)).orderBy(desc(juniorConversationEvents.seq)).limit(1);
|
|
1317
|
+
return row ? eventFromRow(row) : void 0;
|
|
1318
|
+
}
|
|
1319
|
+
async loadMessageHistory(conversationId) {
|
|
1320
|
+
const [compactionRow] = await this.executor.db().select().from(juniorConversationEvents).where(and(eq(juniorConversationEvents.conversationId, conversationId), eq(juniorConversationEvents.type, "messages_summarized"))).orderBy(desc(juniorConversationEvents.seq)).limit(1);
|
|
1321
|
+
const compaction = compactionRow ? messagesSummarizedEventFromRow(compactionRow) : void 0;
|
|
1322
|
+
const historyFromSeq = compaction?.data.historyFromSeq ?? 0;
|
|
1323
|
+
return {
|
|
1324
|
+
events: (await this.executor.db().select().from(juniorConversationEvents).where(and(eq(juniorConversationEvents.conversationId, conversationId), inArray(juniorConversationEvents.type, messageHistoryEventTypes), gte(juniorConversationEvents.seq, historyFromSeq))).orderBy(asc(juniorConversationEvents.seq))).map(eventFromRow),
|
|
1325
|
+
compaction,
|
|
1326
|
+
historyFromSeq
|
|
1327
|
+
};
|
|
1328
|
+
}
|
|
1329
|
+
async loadHistory(conversationId) {
|
|
1330
|
+
return (await this.executor.db().select().from(juniorConversationEvents).where(eq(juniorConversationEvents.conversationId, conversationId)).orderBy(asc(juniorConversationEvents.seq))).map(eventFromRow);
|
|
1331
|
+
}
|
|
1332
|
+
async query(conversationId, query) {
|
|
1333
|
+
if (query.limit < 1) throw new Error("Conversation event query limit must be at least 1");
|
|
1334
|
+
if (query.afterSeq !== void 0 && query.beforeSeq !== void 0 && query.afterSeq >= query.beforeSeq) return {
|
|
1335
|
+
events: [],
|
|
1336
|
+
hasOlder: false,
|
|
1337
|
+
hasNewer: false
|
|
1338
|
+
};
|
|
1339
|
+
const typeFilter = query.types && query.types.length > 0 ? inArray(juniorConversationEvents.type, [...query.types]) : void 0;
|
|
1340
|
+
const bounds = and(eq(juniorConversationEvents.conversationId, conversationId), query.afterSeq === void 0 ? void 0 : gt(juniorConversationEvents.seq, query.afterSeq), query.beforeSeq === void 0 ? void 0 : lt(juniorConversationEvents.seq, query.beforeSeq), typeFilter);
|
|
1341
|
+
const newestFirst = query.afterSeq === void 0;
|
|
1342
|
+
const rows = await this.executor.db().select().from(juniorConversationEvents).where(bounds).orderBy(newestFirst ? desc(juniorConversationEvents.seq) : asc(juniorConversationEvents.seq)).limit(query.limit + 1);
|
|
1343
|
+
const overflow = rows.length > query.limit;
|
|
1344
|
+
const pageRows = overflow ? rows.slice(0, query.limit) : rows;
|
|
1345
|
+
const events = (newestFirst ? [...pageRows].reverse() : pageRows).map(eventFromRow);
|
|
1346
|
+
if (events.length === 0) return {
|
|
1347
|
+
events,
|
|
1348
|
+
hasOlder: false,
|
|
1349
|
+
hasNewer: false
|
|
1350
|
+
};
|
|
1351
|
+
const firstSeq = events[0].seq;
|
|
1352
|
+
const lastSeq = events[events.length - 1].seq;
|
|
1353
|
+
return {
|
|
1354
|
+
events,
|
|
1355
|
+
hasOlder: newestFirst && overflow ? true : await this.hasMatchingEvent({
|
|
1356
|
+
conversationId,
|
|
1357
|
+
beforeSeq: firstSeq,
|
|
1358
|
+
typeFilter
|
|
1359
|
+
}),
|
|
1360
|
+
hasNewer: !newestFirst && overflow ? true : await this.hasMatchingEvent({
|
|
1361
|
+
conversationId,
|
|
1362
|
+
afterSeq: lastSeq,
|
|
1363
|
+
typeFilter
|
|
1364
|
+
})
|
|
1365
|
+
};
|
|
1366
|
+
}
|
|
1367
|
+
/** Return whether any matching event exists inside the exclusive seq window. */
|
|
1368
|
+
async hasMatchingEvent(args) {
|
|
1369
|
+
if (args.afterSeq !== void 0 && args.beforeSeq !== void 0 && args.afterSeq >= args.beforeSeq) return false;
|
|
1370
|
+
const [row] = await this.executor.db().select({ seq: juniorConversationEvents.seq }).from(juniorConversationEvents).where(and(eq(juniorConversationEvents.conversationId, args.conversationId), args.afterSeq === void 0 ? void 0 : gt(juniorConversationEvents.seq, args.afterSeq), args.beforeSeq === void 0 ? void 0 : lt(juniorConversationEvents.seq, args.beforeSeq), args.typeFilter)).limit(1);
|
|
1371
|
+
return row !== void 0;
|
|
1372
|
+
}
|
|
1373
|
+
/** Read the next sequence and active model-history version. */
|
|
1374
|
+
async readCursor(conversationId) {
|
|
1375
|
+
const rows = await this.executor.db().select({
|
|
1376
|
+
maxSeq: sql`max(${juniorConversationEvents.seq})`,
|
|
1377
|
+
maxHistoryVersion: sql`max(${juniorConversationEvents.historyVersion})`
|
|
1378
|
+
}).from(juniorConversationEvents).where(eq(juniorConversationEvents.conversationId, conversationId));
|
|
1379
|
+
const maxSeq = rows[0]?.maxSeq;
|
|
1380
|
+
const maxHistoryVersion = rows[0]?.maxHistoryVersion;
|
|
1381
|
+
return {
|
|
1382
|
+
maxHistoryVersion: maxHistoryVersion === null || maxHistoryVersion === void 0 ? null : Number(maxHistoryVersion),
|
|
1383
|
+
nextSeq: maxSeq === null || maxSeq === void 0 ? 0 : Number(maxSeq) + 1
|
|
1384
|
+
};
|
|
1385
|
+
}
|
|
1386
|
+
};
|
|
1387
|
+
/** Create a SQL-backed canonical conversation event store. */
|
|
1388
|
+
function createSqlConversationEventStore(executor) {
|
|
1389
|
+
return new SqlConversationEventStore(executor);
|
|
1390
|
+
}
|
|
1391
|
+
//#endregion
|
|
1392
|
+
//#region src/chat/conversations/sql/message-search.ts
|
|
1393
|
+
const EXCERPT_WITHOUT_QUERY_CHARS = 240;
|
|
1394
|
+
var SqlConversationMessageSearchStore = class {
|
|
1395
|
+
executor;
|
|
1396
|
+
constructor(executor) {
|
|
1397
|
+
this.executor = executor;
|
|
1398
|
+
}
|
|
1399
|
+
async search(args) {
|
|
1400
|
+
const db = this.executor.db();
|
|
1401
|
+
const query = args.filters.query?.trim() || void 0;
|
|
1402
|
+
const text = sql`${juniorConversationEvents.payload}->>'text'`;
|
|
1403
|
+
const role = sql`${juniorConversationEvents.payload}->>'role'`;
|
|
1404
|
+
const messageId = sql`${juniorConversationEvents.payload}->>'messageId'`;
|
|
1405
|
+
const tsquery = query ? sql`websearch_to_tsquery('english', ${query})` : void 0;
|
|
1406
|
+
const rank = tsquery ? sql`ts_rank_cd(to_tsvector('english', ${text}), ${tsquery})` : sql`1`;
|
|
1407
|
+
const excerpt = tsquery ? sql`ts_headline('english', ${text}, ${tsquery}, 'MaxFragments=2, MinWords=8, MaxWords=40, FragmentDelimiter=" … ", StartSel=**, StopSel=**')` : sql`left(${text}, ${EXCERPT_WITHOUT_QUERY_CHARS})`;
|
|
1408
|
+
const conditions = [
|
|
1409
|
+
eq(juniorConversations.source, "slack"),
|
|
1410
|
+
isNull(juniorConversations.parentConversationId),
|
|
1411
|
+
isNull(juniorConversations.transcriptPurgedAt),
|
|
1412
|
+
ne(juniorConversations.conversationId, args.currentConversationId),
|
|
1413
|
+
eq(juniorDestinations.provider, args.scope.provider),
|
|
1414
|
+
eq(juniorDestinations.providerTenantId, args.scope.providerTenantId),
|
|
1415
|
+
eq(juniorDestinations.visibility, "public"),
|
|
1416
|
+
eq(juniorConversationEvents.type, "message"),
|
|
1417
|
+
sql`${role} in ('user', 'assistant')`
|
|
1418
|
+
];
|
|
1419
|
+
if (tsquery) conditions.push(sql`to_tsvector('english', ${text}) @@ ${tsquery}`);
|
|
1420
|
+
if (args.filters.channelId) conditions.push(eq(juniorDestinations.providerDestinationId, args.filters.channelId));
|
|
1421
|
+
if (args.filters.afterMs !== void 0 && Number.isFinite(args.filters.afterMs)) conditions.push(gte(juniorConversations.lastActivityAt, new Date(args.filters.afterMs)));
|
|
1422
|
+
if (args.filters.beforeMs !== void 0 && Number.isFinite(args.filters.beforeMs)) conditions.push(lt(juniorConversations.lastActivityAt, new Date(args.filters.beforeMs)));
|
|
1423
|
+
if (args.filters.annotation) {
|
|
1424
|
+
const annotation = args.filters.annotation.toLowerCase();
|
|
1425
|
+
const nestedAnnotationPrefix = `${annotation}#`;
|
|
1426
|
+
conditions.push(sql`exists (
|
|
1427
|
+
select 1
|
|
1428
|
+
from ${juniorConversationAnnotations}
|
|
1429
|
+
where ${and(eq(juniorConversationAnnotations.conversationId, juniorConversations.conversationId), sql`(
|
|
1430
|
+
lower(${juniorConversationAnnotations.key}) = ${annotation}
|
|
1431
|
+
or starts_with(
|
|
1432
|
+
lower(${juniorConversationAnnotations.key}),
|
|
1433
|
+
${nestedAnnotationPrefix}
|
|
1434
|
+
)
|
|
1435
|
+
)`)}
|
|
1436
|
+
)`);
|
|
1437
|
+
}
|
|
1438
|
+
const bestPerConversation = db.selectDistinctOn([juniorConversations.conversationId], {
|
|
1439
|
+
channelName: juniorDestinations.displayName,
|
|
1440
|
+
conversationId: juniorConversations.conversationId,
|
|
1441
|
+
excerpt: excerpt.as("excerpt"),
|
|
1442
|
+
lastActivityAt: juniorConversations.lastActivityAt,
|
|
1443
|
+
messageCreatedAt: juniorConversationEvents.createdAt,
|
|
1444
|
+
messageId: messageId.as("message_id"),
|
|
1445
|
+
providerDestinationId: juniorDestinations.providerDestinationId,
|
|
1446
|
+
rank: rank.as("rank"),
|
|
1447
|
+
role: role.as("role")
|
|
1448
|
+
}).from(juniorConversationEvents).innerJoin(juniorConversations, eq(juniorConversations.conversationId, juniorConversationEvents.conversationId)).innerJoin(juniorDestinations, eq(juniorDestinations.id, juniorConversations.destinationId)).where(and(...conditions)).orderBy(juniorConversations.conversationId, desc(rank), desc(juniorConversationEvents.createdAt)).as("best_conversation_matches");
|
|
1449
|
+
return (await db.select().from(bestPerConversation).orderBy(desc(bestPerConversation.rank), desc(bestPerConversation.lastActivityAt), desc(bestPerConversation.messageCreatedAt)).limit(args.limit)).map((row) => ({
|
|
1450
|
+
conversationId: row.conversationId,
|
|
1451
|
+
excerpt: row.excerpt,
|
|
1452
|
+
messageCreatedAtMs: row.messageCreatedAt.getTime(),
|
|
1453
|
+
messageId: row.messageId,
|
|
1454
|
+
providerDestinationId: row.providerDestinationId,
|
|
1455
|
+
role: row.role,
|
|
1456
|
+
...row.channelName ? { channelName: row.channelName } : void 0
|
|
1457
|
+
}));
|
|
1458
|
+
}
|
|
1459
|
+
};
|
|
1460
|
+
/** Create a SQL-backed public workspace message search store. */
|
|
1461
|
+
function createSqlConversationMessageSearchStore(executor) {
|
|
1462
|
+
return new SqlConversationMessageSearchStore(executor);
|
|
1463
|
+
}
|
|
1464
|
+
//#endregion
|
|
1465
|
+
//#region src/chat/db.ts
|
|
1466
|
+
var db_exports = /* @__PURE__ */ __exportAll({
|
|
1467
|
+
closeDb: () => closeDb,
|
|
1468
|
+
getConversationEventStore: () => getConversationEventStore,
|
|
1469
|
+
getConversationMessageSearchStore: () => getConversationMessageSearchStore,
|
|
1470
|
+
getConversationStore: () => getConversationStore,
|
|
1471
|
+
getDb: () => getDb,
|
|
1472
|
+
getSqlExecutor: () => getSqlExecutor
|
|
1473
|
+
});
|
|
1474
|
+
let current;
|
|
1475
|
+
function createDb(args) {
|
|
1476
|
+
return createJuniorSqlExecutor({
|
|
1477
|
+
connectionString: args.databaseUrl,
|
|
1478
|
+
driver: args.driver,
|
|
1479
|
+
statementTimeoutMs: args.statementTimeoutMs
|
|
1480
|
+
});
|
|
1481
|
+
}
|
|
1482
|
+
/** Return the process SQL executor for SQL-specific queries and transactions. */
|
|
1483
|
+
function getSqlExecutor() {
|
|
1484
|
+
const { sql } = getChatConfig();
|
|
1485
|
+
if (current?.databaseUrl !== sql.databaseUrl || current.driver !== sql.driver || current.statementTimeoutMs !== sql.statementTimeoutMs) {
|
|
1486
|
+
if (current) {
|
|
1487
|
+
const previous = current;
|
|
1488
|
+
current = void 0;
|
|
1489
|
+
previous.db.close().catch(() => void 0);
|
|
1490
|
+
}
|
|
1491
|
+
const db = createDb({
|
|
1492
|
+
databaseUrl: sql.databaseUrl,
|
|
1493
|
+
driver: sql.driver,
|
|
1494
|
+
statementTimeoutMs: sql.statementTimeoutMs
|
|
1495
|
+
});
|
|
1496
|
+
current = {
|
|
1497
|
+
databaseUrl: sql.databaseUrl,
|
|
1498
|
+
driver: sql.driver,
|
|
1499
|
+
statementTimeoutMs: sql.statementTimeoutMs,
|
|
1500
|
+
db,
|
|
1501
|
+
store: createSqlStore(db),
|
|
1502
|
+
eventStore: createSqlConversationEventStore(db),
|
|
1503
|
+
searchStore: createSqlConversationMessageSearchStore(db)
|
|
1504
|
+
};
|
|
1505
|
+
}
|
|
1506
|
+
return current.db;
|
|
1507
|
+
}
|
|
1508
|
+
/** Return the process Drizzle database. */
|
|
1509
|
+
function getDb() {
|
|
1510
|
+
return getSqlExecutor().db();
|
|
1511
|
+
}
|
|
1512
|
+
/** Return the SQL-backed conversation store. */
|
|
1513
|
+
function getConversationStore() {
|
|
1514
|
+
getSqlExecutor();
|
|
1515
|
+
return current.store;
|
|
1516
|
+
}
|
|
1517
|
+
/** Return the canonical SQL-backed conversation event store. */
|
|
1518
|
+
function getConversationEventStore() {
|
|
1519
|
+
getSqlExecutor();
|
|
1520
|
+
return current.eventStore;
|
|
1521
|
+
}
|
|
1522
|
+
/** Return the SQL-backed public provider-tenant conversation search store. */
|
|
1523
|
+
function getConversationMessageSearchStore() {
|
|
1524
|
+
getSqlExecutor();
|
|
1525
|
+
return current.searchStore;
|
|
1526
|
+
}
|
|
1527
|
+
/** Close the process SQL database when it has been opened. */
|
|
1528
|
+
async function closeDb() {
|
|
1529
|
+
const previous = current;
|
|
1530
|
+
current = void 0;
|
|
1531
|
+
await previous?.db.close();
|
|
1532
|
+
}
|
|
1533
|
+
//#endregion
|
|
1534
|
+
//#region src/chat/plugins/viewer.ts
|
|
1535
|
+
function identityFromRow(row) {
|
|
1536
|
+
return identitySchema.parse({
|
|
1537
|
+
id: row.id,
|
|
1538
|
+
provider: row.provider,
|
|
1539
|
+
providerSubjectId: row.providerSubjectId,
|
|
1540
|
+
...row.providerTenantId ? { providerTenantId: row.providerTenantId } : void 0,
|
|
1541
|
+
...row.displayName ? { displayName: row.displayName } : void 0,
|
|
1542
|
+
...row.handle ? { handle: row.handle } : void 0
|
|
1543
|
+
});
|
|
1544
|
+
}
|
|
1545
|
+
async function readUserById(db, userRow) {
|
|
1546
|
+
const identityRows = await db.select().from(juniorIdentities).where(and(eq(juniorIdentities.userId, userRow.id), eq(juniorIdentities.kind, "user"))).orderBy(asc(juniorIdentities.provider), asc(juniorIdentities.providerTenantId), asc(juniorIdentities.providerSubjectId));
|
|
1547
|
+
return userSchema.parse({
|
|
1548
|
+
email: userRow.primaryEmail,
|
|
1549
|
+
id: userRow.id,
|
|
1550
|
+
identities: identityRows.map(identityFromRow),
|
|
1551
|
+
...userRow.displayName ? { displayName: userRow.displayName } : void 0,
|
|
1552
|
+
...userRow.isAdmin ? { isAdmin: true } : void 0
|
|
1553
|
+
});
|
|
1554
|
+
}
|
|
1555
|
+
/** Resolve or create the canonical user for one verified viewer email. */
|
|
1556
|
+
async function resolveViewerUserFromSql(db, email) {
|
|
1557
|
+
const normalizedEmail = normalizeIdentityEmail(email);
|
|
1558
|
+
if (!normalizedEmail) return void 0;
|
|
1559
|
+
const now = /* @__PURE__ */ new Date();
|
|
1560
|
+
const userRow = (await db.insert(juniorUsers).values({
|
|
1561
|
+
id: randomUUID(),
|
|
1562
|
+
primaryEmail: email.trim(),
|
|
1563
|
+
primaryEmailNormalized: normalizedEmail,
|
|
1564
|
+
displayName: null,
|
|
1565
|
+
createdAt: now,
|
|
1566
|
+
updatedAt: now
|
|
1567
|
+
}).onConflictDoNothing({ target: juniorUsers.primaryEmailNormalized }).returning())[0] ?? (await db.select().from(juniorUsers).where(eq(juniorUsers.primaryEmailNormalized, normalizedEmail)).limit(1))[0];
|
|
1568
|
+
return userRow ? await readUserById(db, userRow) : void 0;
|
|
1569
|
+
}
|
|
1570
|
+
/** Resolve or create the canonical user for one authenticated viewer. */
|
|
1571
|
+
async function resolveViewerUser(email) {
|
|
1572
|
+
return await resolveViewerUserFromSql(getDb(), email);
|
|
1573
|
+
}
|
|
1574
|
+
/** Read one existing canonical user by verified email without creating a row. */
|
|
1575
|
+
async function findUserByEmailFromSql(db, email) {
|
|
1576
|
+
const normalizedEmail = normalizeIdentityEmail(email);
|
|
1577
|
+
if (!normalizedEmail) return void 0;
|
|
1578
|
+
const userRow = (await db.select().from(juniorUsers).where(eq(juniorUsers.primaryEmailNormalized, normalizedEmail)).limit(1))[0];
|
|
1579
|
+
return userRow ? await readUserById(db, userRow) : void 0;
|
|
1580
|
+
}
|
|
1581
|
+
/** Read one existing canonical user by verified email without creating a row. */
|
|
1582
|
+
async function findUserByEmail(email) {
|
|
1583
|
+
return await findUserByEmailFromSql(getDb(), email);
|
|
1584
|
+
}
|
|
1585
|
+
/** Update one canonical user's display name in SQL. */
|
|
1586
|
+
async function updateViewerDisplayNameFromSql(db, userId, displayName) {
|
|
1587
|
+
const userRow = (await db.update(juniorUsers).set({
|
|
1588
|
+
displayName,
|
|
1589
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
1590
|
+
}).where(eq(juniorUsers.id, userId)).returning())[0];
|
|
1591
|
+
return userRow ? await readUserById(db, userRow) : void 0;
|
|
1592
|
+
}
|
|
1593
|
+
/** Update one canonical user's display name. */
|
|
1594
|
+
async function updateViewerDisplayName(userId, displayName) {
|
|
1595
|
+
return await updateViewerDisplayNameFromSql(getDb(), userId, displayName);
|
|
1596
|
+
}
|
|
1597
|
+
/**
|
|
1598
|
+
* Grant or revoke the Junior-wide admin role for one email.
|
|
1599
|
+
*
|
|
1600
|
+
* Granting creates the canonical user when needed, so an operator can grant
|
|
1601
|
+
* admin before that person first signs in. Revoking never creates a user.
|
|
1602
|
+
*/
|
|
1603
|
+
async function setUserAdminFromSql(db, email, isAdmin) {
|
|
1604
|
+
const user = isAdmin ? await resolveViewerUserFromSql(db, email) : await findUserByEmailFromSql(db, email);
|
|
1605
|
+
if (!user) return void 0;
|
|
1606
|
+
const userRow = (await db.update(juniorUsers).set({
|
|
1607
|
+
isAdmin,
|
|
1608
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
1609
|
+
}).where(eq(juniorUsers.id, user.id)).returning())[0];
|
|
1610
|
+
return userRow ? await readUserById(db, userRow) : void 0;
|
|
1611
|
+
}
|
|
1612
|
+
/** List every Junior admin by primary email. */
|
|
1613
|
+
async function listAdminUsersFromSql(db) {
|
|
1614
|
+
const rows = await db.select().from(juniorUsers).where(eq(juniorUsers.isAdmin, true)).orderBy(asc(juniorUsers.primaryEmailNormalized));
|
|
1615
|
+
return await Promise.all(rows.map((row) => readUserById(db, row)));
|
|
1616
|
+
}
|
|
1617
|
+
/** Resolve the stored identity and linked user for one runtime actor. */
|
|
1618
|
+
async function readActorIdentityFromSql(db, actor) {
|
|
1619
|
+
if (actor.platform === "system") return void 0;
|
|
1620
|
+
const providerTenantId = actor.platform === "slack" ? actor.teamId : "";
|
|
1621
|
+
const row = (await db.select({
|
|
1622
|
+
identity: juniorIdentities,
|
|
1623
|
+
user: juniorUsers
|
|
1624
|
+
}).from(juniorIdentities).leftJoin(juniorUsers, eq(juniorUsers.id, juniorIdentities.userId)).where(and(eq(juniorIdentities.kind, "user"), eq(juniorIdentities.provider, actor.platform), eq(juniorIdentities.providerTenantId, providerTenantId), eq(juniorIdentities.providerSubjectId, actor.userId))).limit(1))[0];
|
|
1625
|
+
if (!row) return void 0;
|
|
1626
|
+
return {
|
|
1627
|
+
identity: identityFromRow(row.identity),
|
|
1628
|
+
...row.user ? { user: await readUserById(db, row.user) } : void 0
|
|
1629
|
+
};
|
|
1630
|
+
}
|
|
1631
|
+
/** Resolve the stored identity and linked user for one runtime actor. */
|
|
1632
|
+
async function readActorIdentity(actor) {
|
|
1633
|
+
return await readActorIdentityFromSql(getDb(), actor);
|
|
1634
|
+
}
|
|
1635
|
+
//#endregion
|
|
1636
|
+
export { upsertLinkedIdentity as A, agentTurnUsageSchema as C, readUserTimezone as D, deleteProviderIdentityForSlackUser as E, parseDestination as M, requireSlackDestination as N, saveUserTimezone as O, sameDestination as P, agentTurnTotalTokens as S, resolveRootConversationId as T, locationFromRow as _, setUserAdminFromSql as a, addAgentTurnUsage as b, getConversationEventStore as c, getDb as d, getSqlExecutor as f, withConversationMutationLock as g, createSqlStore as h, resolveViewerUser as i, createSlackDestination as j, upsertIdentity as k, getConversationMessageSearchStore as l, withConversationEventLock as m, listAdminUsersFromSql as n, updateViewerDisplayName as o, createSqlConversationEventStore as p, readActorIdentity as r, db_exports as s, findUserByEmail as t, getConversationStore as u, bindProviderConversation as v, hasAgentTurnUsage as w, agentTurnCostUsd as x, parseSessionSource as y };
|