@sentry/junior 0.267.0 → 0.269.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/README.md +2 -9
- package/dist/{actor-DZSokpwj.js → actor-CP9K2Idu.js} +2 -1
- package/dist/{adapter-BybMEezj.js → adapter-2-CG25be.js} +3 -51
- package/dist/{admin-DOs1_EiW.js → admin-DSs8S5Uy.js} +1 -1
- package/dist/{agent-invocations-CxKldAeB.js → agent-invocations-C1RNOCs8.js} +2 -0
- package/dist/{agent-runner-DgNATN4D.js → agent-runner-CrDrPUtY.js} +3411 -1640
- package/dist/api/schema.d.ts +2 -5630
- package/dist/api/schema.js +7 -7
- package/dist/api.d.ts +3 -1
- package/dist/api.js +268 -287
- package/dist/{app-DAB3fzyp.d.ts → app-BrMMzjYm.d.ts} +10 -3
- package/dist/app.d.ts +1 -1
- package/dist/app.js +371 -223
- package/dist/{automation-CmeFBSEh.js → automation-p87hg_AV.js} +74 -74
- package/dist/{briefs-CWKjka7v.js → briefs-PtODdLNu.js} +4 -4
- package/dist/{catalog-runtime-cMlAI5LY.js → catalog-runtime-BsoSmTM0.js} +1 -1
- package/dist/cli/chat.js +16 -26
- package/dist/cli/check.js +3 -3
- package/dist/cli/main.js +5 -3
- package/dist/cli/plugins.js +4 -4
- package/dist/cli/snapshot-create.js +4 -4
- package/dist/cli/upgrade.d.ts +1 -1
- package/dist/cli/upgrade.js +3 -3
- package/dist/{client-BjgbMawj.js → client-Cqf4TboL.js} +20 -15
- package/dist/{config-CdB6yJj5.js → config-CPsE4T5L.js} +2 -2
- package/dist/{connections-RE_65jzK.js → connections-upFXLmhG.js} +10 -11
- package/dist/{conversation-DSo310Ps.js → conversation-CTpD_PGk.js} +7 -84
- package/dist/{conversation-privacy-BzgJC5k0.js → conversation-privacy-D6zJc8ff.js} +1 -1
- package/dist/{credential-sync-DhEGEpgq.js → credential-sync-CS_6IC-f.js} +30 -34
- package/dist/{credentialed-CIGwoLU2.js → credentialed-BU-OW7HP.js} +577 -74
- package/dist/{store-B8CABbmR.d.ts → db-BzOCzVb1.d.ts} +17 -7
- package/dist/{executor-VYrx_K4w.js → executor-CNS7ONt-.js} +78 -4
- package/dist/instrumentation.js +2 -2
- package/dist/{assistant-reply-CT7VcXQe.js → interruption-marker-g6Xup3Fk.js} +8 -2
- package/dist/{job-queue-C0BZSa7r.js → job-queue-DI-k-Bbk.js} +2 -2
- package/dist/{list-1XQP-o6K.js → list-CGoQdXTp.js} +429 -1401
- package/dist/locks-D-0h111u.js +48 -0
- package/dist/{logging-DuZ1A4ab.js → logging-n7knse-O.js} +1 -1
- package/dist/{history-C84mlAwO.js → media-85PDJxO_.js} +83 -1
- package/dist/{message-NKSbbnA6.js → message-BpaWd8TT.js} +4 -4
- package/dist/nitro.d.ts +1 -1
- package/dist/nitro.js +3 -3
- package/dist/{oauth-callback-server-qAHLPtml.js → oauth-callback-server-CpGmEw_i.js} +2 -2
- package/dist/{prompt-BW9jFsqZ.js → prompt-BlQB0QQt.js} +1 -1
- package/dist/{registry-FQutO926.js → registry-By36_xsm.js} +2 -2
- package/dist/reporting-schema-CZREhYc-.js +104 -0
- package/dist/reporting-window-CVarfId9.js +255 -0
- package/dist/{resolve-Bk4yeuHy.js → resolve-CUIBcdPd.js} +3 -3
- package/dist/{result-B9e9ATOU.js → result-BemsOMOO.js} +31 -12
- package/dist/{runner-C3S2ObH-.js → runner-BVok7PAA.js} +12 -11
- package/dist/{sandbox-egress-signals-DqpGzzMB.js → sandbox-egress-signals-BSUWQI95.js} +1 -1
- package/dist/{schema-X7Fj4fv1.js → schema-ChnFo5f2.js} +98 -6
- package/dist/schema-jK4-1u_H.d.ts +5927 -0
- package/dist/{service-account-Do30HUNv.js → service-account-DHvlLnZw.js} +4 -3
- package/dist/{service-account-BQasl-qq.js → service-account-hMmESDcr.js} +3 -3
- package/dist/{session-stop-DkOjt46_.js → session-stop-BQW_iFcd.js} +499 -23
- package/dist/{skills-CQHrwfOa.js → skills-DmJJIuLF.js} +2 -2
- package/dist/{spawn-BHuqN14f.js → spawn-ChCm46i-.js} +2 -2
- package/dist/{store-6EetVP-m.js → store-1DVhoKOL.js} +4 -4
- package/dist/{store-DvNZIsCr.js → store-DCo2n5iJ.js} +3 -3
- package/dist/{task-queue-B0DLgd2P.js → task-queue-HMC4QjFm.js} +3 -3
- package/dist/task-runner-cA6yjn_E.js +269 -0
- package/dist/{thread-state-DRmdJ2QN.js → thread-state-CHy8LS0C.js} +8 -18
- package/dist/{turn-failure-response-G11cs2gE.js → turn-failure-response-Dr7erLWs.js} +11 -16
- package/dist/{turn-lifecycle-OzLIwvi1.js → turn-lifecycle-bf2fIYi_.js} +1 -1
- package/dist/{tasks-0ilmuCux.js → turn-session-routing-BqO0qXLk.js} +959 -44
- package/dist/{validation-CxEmt1id.js → validation-DHR1ivfh.js} +1 -1
- package/dist/{version-Bapedn4A.js → version-CXCHLgWm.js} +1 -1
- package/dist/version.js +1 -1
- package/dist/{viewer-DrN3oC2X.js → viewer-DSbD6DF6.js} +503 -241
- package/dist/{work-DUQoNcqP.js → work-BE_qTYGi.js} +17 -36
- package/dist/{worker-D1l4Ugk0.js → worker-Cm9_sWXg.js} +15 -7
- package/migrations/0065_user_customization.sql +1 -0
- package/migrations/0066_review_items.sql +53 -0
- package/migrations/0067_review_item_dedupe_tenant.sql +2 -0
- package/migrations/0068_review_item_slack_cards.sql +10 -0
- package/migrations/0069_system_identity.sql +25 -0
- package/migrations/0070_review_item_foreign_keys.sql +5 -0
- package/migrations/AGENTS.md +17 -0
- package/migrations/meta/0065_snapshot.json +3511 -0
- package/migrations/meta/0066_snapshot.json +4099 -0
- package/migrations/meta/0067_snapshot.json +3921 -0
- package/migrations/meta/0068_snapshot.json +4176 -0
- package/migrations/meta/0069_snapshot.json +3979 -0
- package/migrations/meta/0070_snapshot.json +3997 -0
- package/migrations/meta/_journal.json +42 -0
- package/package.json +5 -5
- package/dist/db-Bg-zC3r5.d.ts +0 -5
- package/dist/history-BgI33Kie.d.ts +0 -78
- package/dist/images-CecjXAAz.js +0 -64
- package/dist/task-runner-B-PD_JOj.js +0 -1074
- package/dist/turn-session-routing-Dyut9l7Y.js +0 -76
- package/dist/user-input-request-BDFBdF8r.js +0 -116
- package/dist/web-message-id-CJSUiksE.js +0 -236
- package/migrations/README.md +0 -30
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { t as __exportAll } from "./rolldown-runtime-nwUcYXjP.js";
|
|
2
|
-
import { i as getChatConfig } from "./config-
|
|
3
|
-
import { L as GOOGLE_IDENTITY_PROVIDER, z as normalizeIdentityEmail } from "./logging-
|
|
4
|
-
import {
|
|
5
|
-
import { c as juniorIdentities, d as juniorDestinations, l as juniorUsers, o as conversationReadColumns, s as juniorConversations } from "./agent-invocations-
|
|
6
|
-
import { _ as
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
2
|
+
import { i as getChatConfig } from "./config-CPsE4T5L.js";
|
|
3
|
+
import { L as GOOGLE_IDENTITY_PROVIDER, h as logWarn, z as normalizeIdentityEmail } from "./logging-n7knse-O.js";
|
|
4
|
+
import { a as isSlackUserId, d as parseSlackChannelReferenceId, f as parseSlackTeamId, s as parseActorUserId } from "./actor-CP9K2Idu.js";
|
|
5
|
+
import { c as juniorIdentities, d as juniorDestinations, l as juniorUsers, o as conversationReadColumns, s as juniorConversations } from "./agent-invocations-C1RNOCs8.js";
|
|
6
|
+
import { _ as CONVERSATION_METRICS, h as juniorConversationBindings, m as juniorConversationParticipants, t as createJuniorSqlExecutor, v as juniorConversationMetrics, x as juniorConversationAnnotations, y as juniorConversationEvents } from "./executor-CNS7ONt-.js";
|
|
7
|
+
import { d as newConversationEventSchema, f as encodeHistoryPayload, l as decodeStoredConversationEvent, n as detectImageMediaType, o as conversationEventDataSchema, u as historyReplacementSchema, v as usageSchema } from "./media-85PDJxO_.js";
|
|
8
|
+
import { a as withLock } from "./locks-D-0h111u.js";
|
|
9
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
10
|
+
import { and, asc, desc, eq, gt, gte, inArray, isNotNull, isNull, lt, lte, ne, or, sql } from "drizzle-orm";
|
|
10
11
|
import { destinationSchema, identitySchema, locationSchema, sourceSchema, userSchema } from "@sentry/junior-plugin-api";
|
|
11
12
|
// src/chat/destination.ts
|
|
12
13
|
/** Build Junior's canonical destination from Slack workspace and channel ids. */
|
|
@@ -37,161 +38,6 @@ function sameDestination(left, right) {
|
|
|
37
38
|
if (left.platform === "slack" && right.platform === "slack") return left.teamId === right.teamId && left.channelId === right.channelId;
|
|
38
39
|
return false;
|
|
39
40
|
}
|
|
40
|
-
// src/chat/identities/sql.ts
|
|
41
|
-
function dateFromMs$1(ms) {
|
|
42
|
-
return new Date(ms);
|
|
43
|
-
}
|
|
44
|
-
function tenantId$1(value) {
|
|
45
|
-
return value ?? "";
|
|
46
|
-
}
|
|
47
|
-
async function upsertUser(executor, args) {
|
|
48
|
-
const id = (await executor.db().insert(juniorUsers).values({
|
|
49
|
-
id: randomUUID(),
|
|
50
|
-
primaryEmail: args.email,
|
|
51
|
-
primaryEmailNormalized: args.emailNormalized,
|
|
52
|
-
displayName: args.displayName ?? null,
|
|
53
|
-
createdAt: dateFromMs$1(args.nowMs),
|
|
54
|
-
updatedAt: dateFromMs$1(args.nowMs)
|
|
55
|
-
}).onConflictDoUpdate({
|
|
56
|
-
target: juniorUsers.primaryEmailNormalized,
|
|
57
|
-
set: {
|
|
58
|
-
displayName: sql`coalesce(${juniorUsers.displayName}, excluded.display_name)`,
|
|
59
|
-
updatedAt: sql`excluded.updated_at`
|
|
60
|
-
}
|
|
61
|
-
}).returning({ id: juniorUsers.id }))[0]?.id;
|
|
62
|
-
if (!id) throw new Error("User identity upsert returned no row");
|
|
63
|
-
return id;
|
|
64
|
-
}
|
|
65
|
-
async function existingIdentity(executor, identity) {
|
|
66
|
-
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];
|
|
67
|
-
}
|
|
68
|
-
async function upsertIdentityRecord(executor, identity, linkedUserId, nowMs) {
|
|
69
|
-
const emailNormalized = normalizeIdentityEmail(identity.email);
|
|
70
|
-
const email = emailNormalized ? identity.email?.trim() || emailNormalized : void 0;
|
|
71
|
-
const existing = await existingIdentity(executor, identity);
|
|
72
|
-
const userEmailNormalized = existing?.emailVerified && existing.emailNormalized ? existing.emailNormalized : identity.emailVerified ? emailNormalized : void 0;
|
|
73
|
-
const userEmail = existing?.emailVerified && existing.email ? existing.email : email ?? userEmailNormalized;
|
|
74
|
-
const verifiedUserId = identity.kind === "user" && userEmailNormalized ? await upsertUser(executor, {
|
|
75
|
-
email: userEmail ?? userEmailNormalized,
|
|
76
|
-
emailNormalized: userEmailNormalized,
|
|
77
|
-
nowMs,
|
|
78
|
-
...existing?.displayName || identity.displayName ? { displayName: existing?.displayName ?? identity.displayName } : void 0
|
|
79
|
-
}) : void 0;
|
|
80
|
-
if (existing?.userId && verifiedUserId && existing.userId !== verifiedUserId) throw new Error("Identity verified email conflicts with linked user");
|
|
81
|
-
if (linkedUserId && verifiedUserId && linkedUserId !== verifiedUserId) throw new Error("Linked identity conflicts with verified email user");
|
|
82
|
-
if (existing?.userId && linkedUserId && existing.userId !== linkedUserId) throw new Error("Identity conflicts with linked user");
|
|
83
|
-
const userId = existing?.userId ?? linkedUserId ?? verifiedUserId;
|
|
84
|
-
const row = (await executor.db().insert(juniorIdentities).values({
|
|
85
|
-
id: randomUUID(),
|
|
86
|
-
userId: userId ?? null,
|
|
87
|
-
kind: identity.kind,
|
|
88
|
-
provider: identity.provider,
|
|
89
|
-
providerTenantId: tenantId$1(identity.providerTenantId),
|
|
90
|
-
providerSubjectId: identity.providerSubjectId,
|
|
91
|
-
displayName: identity.displayName ?? null,
|
|
92
|
-
handle: identity.handle ?? null,
|
|
93
|
-
email: email ?? null,
|
|
94
|
-
emailNormalized: emailNormalized ?? null,
|
|
95
|
-
emailVerified: Boolean(identity.emailVerified && emailNormalized),
|
|
96
|
-
avatarUrl: null,
|
|
97
|
-
metadata: identity.metadata ?? null,
|
|
98
|
-
createdAt: dateFromMs$1(nowMs),
|
|
99
|
-
updatedAt: dateFromMs$1(nowMs)
|
|
100
|
-
}).onConflictDoUpdate({
|
|
101
|
-
target: [
|
|
102
|
-
juniorIdentities.provider,
|
|
103
|
-
juniorIdentities.providerTenantId,
|
|
104
|
-
juniorIdentities.providerSubjectId
|
|
105
|
-
],
|
|
106
|
-
set: {
|
|
107
|
-
kind: sql`excluded.kind`,
|
|
108
|
-
userId: sql`coalesce(${juniorIdentities.userId}, excluded.user_id)`,
|
|
109
|
-
displayName: linkedUserId ? sql`coalesce(excluded.display_name, ${juniorIdentities.displayName})` : sql`coalesce(${juniorIdentities.displayName}, excluded.display_name)`,
|
|
110
|
-
handle: linkedUserId ? sql`coalesce(excluded.handle, ${juniorIdentities.handle})` : sql`coalesce(${juniorIdentities.handle}, excluded.handle)`,
|
|
111
|
-
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`,
|
|
112
|
-
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`,
|
|
113
|
-
emailVerified: sql`${juniorIdentities.emailVerified} OR excluded.email_verified`,
|
|
114
|
-
avatarUrl: sql`coalesce(${juniorIdentities.avatarUrl}, excluded.avatar_url)`,
|
|
115
|
-
metadata: sql`coalesce(${juniorIdentities.metadata}, excluded.metadata_json)`,
|
|
116
|
-
updatedAt: sql`excluded.updated_at`
|
|
117
|
-
},
|
|
118
|
-
...linkedUserId ? { setWhere: sql`${juniorIdentities.userId} IS NULL OR ${juniorIdentities.userId} = excluded.user_id` } : void 0
|
|
119
|
-
}).returning({
|
|
120
|
-
id: juniorIdentities.id,
|
|
121
|
-
userId: juniorIdentities.userId
|
|
122
|
-
}))[0];
|
|
123
|
-
if (!row) {
|
|
124
|
-
if (linkedUserId) throw new Error("Identity conflicts with linked user");
|
|
125
|
-
throw new Error("Identity upsert returned no row");
|
|
126
|
-
}
|
|
127
|
-
return {
|
|
128
|
-
id: row.id,
|
|
129
|
-
...row.userId ? { userId: row.userId } : void 0
|
|
130
|
-
};
|
|
131
|
-
}
|
|
132
|
-
/** Read the saved timezone for one user. */
|
|
133
|
-
async function readUserTimezone(db, userId) {
|
|
134
|
-
return (await db.select({ timezone: juniorUsers.timezone }).from(juniorUsers).where(eq(juniorUsers.id, userId)).limit(1))[0]?.timezone ?? void 0;
|
|
135
|
-
}
|
|
136
|
-
/** Save the IANA timezone for one user. */
|
|
137
|
-
async function saveUserTimezone(db, userId, timezone, nowMs) {
|
|
138
|
-
await db.update(juniorUsers).set({
|
|
139
|
-
timezone,
|
|
140
|
-
updatedAt: dateFromMs$1(nowMs)
|
|
141
|
-
}).where(and(eq(juniorUsers.id, userId), sql`${juniorUsers.timezone} is distinct from ${timezone}`));
|
|
142
|
-
}
|
|
143
|
-
/** Persist one provider identity observation and link verified emails to users. */
|
|
144
|
-
async function upsertIdentity(executor, identity, nowMs = Date.now()) {
|
|
145
|
-
return await upsertIdentityRecord(executor, identity, void 0, nowMs);
|
|
146
|
-
}
|
|
147
|
-
/**
|
|
148
|
-
* Persist one provider identity after a trusted account-linking flow.
|
|
149
|
-
* Callers must use provider-verified account data, such as GitHub's authenticated
|
|
150
|
-
* user response, never identity claims inferred from content such as Git commits.
|
|
151
|
-
*/
|
|
152
|
-
async function upsertLinkedIdentity(executor, userId, identity, nowMs = Date.now()) {
|
|
153
|
-
return await upsertIdentityRecord(executor, identity, userId, nowMs);
|
|
154
|
-
}
|
|
155
|
-
/**
|
|
156
|
-
* Find the one user whose primary email is in a provider-verified email list.
|
|
157
|
-
* Return undefined when no user or more than one user matches.
|
|
158
|
-
*/
|
|
159
|
-
async function findUserByVerifiedEmails(executor, emails) {
|
|
160
|
-
const normalized = new Set(emails.map((email) => normalizeIdentityEmail(email) ?? ""));
|
|
161
|
-
normalized.delete("");
|
|
162
|
-
if (normalized.size === 0) return void 0;
|
|
163
|
-
const rows = await executor.db().select({
|
|
164
|
-
email: juniorUsers.primaryEmail,
|
|
165
|
-
userId: juniorUsers.id
|
|
166
|
-
}).from(juniorUsers).where(inArray(juniorUsers.primaryEmailNormalized, Array.from(normalized))).limit(2);
|
|
167
|
-
return rows.length === 1 ? rows[0] : void 0;
|
|
168
|
-
}
|
|
169
|
-
/** Remove one exact provider identity owned by a workspace Slack user. */
|
|
170
|
-
async function deleteProviderIdentityForSlackUser(executor, slackTeamId, slackUserId, provider, providerSubjectId) {
|
|
171
|
-
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;
|
|
172
|
-
if (!userId) return;
|
|
173
|
-
await deleteProviderIdentityForUser(executor, userId, provider, providerSubjectId);
|
|
174
|
-
}
|
|
175
|
-
/** Remove one exact provider identity owned by a user. */
|
|
176
|
-
async function deleteProviderIdentityForUser(executor, userId, provider, providerSubjectId) {
|
|
177
|
-
await executor.db().delete(juniorIdentities).where(and(eq(juniorIdentities.userId, userId), eq(juniorIdentities.provider, provider), eq(juniorIdentities.providerTenantId, ""), eq(juniorIdentities.providerSubjectId, providerSubjectId)));
|
|
178
|
-
}
|
|
179
|
-
/**
|
|
180
|
-
* Save one Google sign-in. Link the Google identity to the user of its
|
|
181
|
-
* verified email, and set its sign-in time.
|
|
182
|
-
*/
|
|
183
|
-
async function recordGoogleSignIn(executor, account, nowMs = Date.now()) {
|
|
184
|
-
const stored = await upsertIdentity(executor, {
|
|
185
|
-
kind: "user",
|
|
186
|
-
provider: GOOGLE_IDENTITY_PROVIDER,
|
|
187
|
-
providerSubjectId: account.subject,
|
|
188
|
-
email: account.email,
|
|
189
|
-
emailVerified: true,
|
|
190
|
-
displayName: account.displayName
|
|
191
|
-
}, nowMs);
|
|
192
|
-
await executor.db().update(juniorIdentities).set({ signedInAt: dateFromMs$1(nowMs) }).where(eq(juniorIdentities.id, stored.id));
|
|
193
|
-
return stored;
|
|
194
|
-
}
|
|
195
41
|
// src/chat/conversations/sql/participants.ts
|
|
196
42
|
/** Resolve the root conversation id that owns personal-feed membership. */
|
|
197
43
|
async function resolveRootConversationId(executor, conversationId) {
|
|
@@ -339,6 +185,38 @@ function conversationMetricValues(args) {
|
|
|
339
185
|
cost_usd: agentTurnCostUsd(usage)
|
|
340
186
|
};
|
|
341
187
|
}
|
|
188
|
+
/**
|
|
189
|
+
* Read one Run's stored totals from its metric rows.
|
|
190
|
+
*
|
|
191
|
+
* Returns undefined when the Run has no rows. Cost keeps only its total.
|
|
192
|
+
*/
|
|
193
|
+
async function readConversationRunMetrics(executor, args) {
|
|
194
|
+
const rows = await executor.db().select({
|
|
195
|
+
metric: juniorConversationMetrics.metric,
|
|
196
|
+
value: juniorConversationMetrics.value
|
|
197
|
+
}).from(juniorConversationMetrics).where(and(eq(juniorConversationMetrics.conversationId, args.conversationId), eq(juniorConversationMetrics.runId, args.runId)));
|
|
198
|
+
const values = new Map(rows.map((row) => [row.metric, row.value]));
|
|
199
|
+
const durationMs = values.get("duration_ms");
|
|
200
|
+
if (durationMs === void 0) return void 0;
|
|
201
|
+
const components = {
|
|
202
|
+
inputTokens: values.get("input_tokens"),
|
|
203
|
+
outputTokens: values.get("output_tokens"),
|
|
204
|
+
cachedInputTokens: values.get("cached_input_tokens"),
|
|
205
|
+
cacheCreationTokens: values.get("cache_creation_tokens")
|
|
206
|
+
};
|
|
207
|
+
const hasComponents = Object.values(components).some((value) => value !== void 0);
|
|
208
|
+
const costUsd = values.get("cost_usd");
|
|
209
|
+
const usage = Object.fromEntries(Object.entries({
|
|
210
|
+
...components,
|
|
211
|
+
reasoningTokens: values.get("reasoning_tokens"),
|
|
212
|
+
totalTokens: hasComponents ? void 0 : values.get("total_tokens"),
|
|
213
|
+
cost: costUsd === void 0 ? void 0 : { total: costUsd }
|
|
214
|
+
}).filter(([, value]) => value !== void 0));
|
|
215
|
+
return {
|
|
216
|
+
durationMs,
|
|
217
|
+
...Object.keys(usage).length > 0 ? { usage } : void 0
|
|
218
|
+
};
|
|
219
|
+
}
|
|
342
220
|
/** Replace one Run's metric rows and refresh its Conversation totals. */
|
|
343
221
|
async function replaceConversationMetrics(executor, args) {
|
|
344
222
|
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);
|
|
@@ -448,44 +326,271 @@ async function bindProviderConversation(executor, args) {
|
|
|
448
326
|
createdAt: /* @__PURE__ */ new Date()
|
|
449
327
|
}).onConflictDoNothing().returning({ conversationId: juniorConversationBindings.conversationId }))[0]?.conversationId ?? await getConversationIdByProviderConversation(executor, args)) !== args.conversationId) throw new Error("Provider conversation is already bound to another conversation");
|
|
450
328
|
}
|
|
329
|
+
// src/chat/identities/sql.ts
|
|
330
|
+
function dateFromMs$1(ms) {
|
|
331
|
+
return new Date(ms);
|
|
332
|
+
}
|
|
333
|
+
function tenantId$1(value) {
|
|
334
|
+
return value ?? "";
|
|
335
|
+
}
|
|
336
|
+
async function upsertUser(executor, args) {
|
|
337
|
+
const id = (await executor.db().insert(juniorUsers).values({
|
|
338
|
+
id: randomUUID(),
|
|
339
|
+
primaryEmail: args.email,
|
|
340
|
+
primaryEmailNormalized: args.emailNormalized,
|
|
341
|
+
displayName: args.displayName ?? null,
|
|
342
|
+
createdAt: dateFromMs$1(args.nowMs),
|
|
343
|
+
updatedAt: dateFromMs$1(args.nowMs)
|
|
344
|
+
}).onConflictDoUpdate({
|
|
345
|
+
target: juniorUsers.primaryEmailNormalized,
|
|
346
|
+
set: {
|
|
347
|
+
displayName: sql`coalesce(${juniorUsers.displayName}, excluded.display_name)`,
|
|
348
|
+
updatedAt: sql`excluded.updated_at`
|
|
349
|
+
}
|
|
350
|
+
}).returning({ id: juniorUsers.id }))[0]?.id;
|
|
351
|
+
if (!id) throw new Error("User identity upsert returned no row");
|
|
352
|
+
return id;
|
|
353
|
+
}
|
|
354
|
+
async function existingIdentity(executor, identity) {
|
|
355
|
+
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];
|
|
356
|
+
}
|
|
357
|
+
async function upsertIdentityRecord(executor, identity, linkedUserId, nowMs) {
|
|
358
|
+
const emailNormalized = normalizeIdentityEmail(identity.email);
|
|
359
|
+
const email = emailNormalized ? identity.email?.trim() || emailNormalized : void 0;
|
|
360
|
+
const existing = await existingIdentity(executor, identity);
|
|
361
|
+
const userEmailNormalized = existing?.emailVerified && existing.emailNormalized ? existing.emailNormalized : identity.emailVerified ? emailNormalized : void 0;
|
|
362
|
+
const userEmail = existing?.emailVerified && existing.email ? existing.email : email ?? userEmailNormalized;
|
|
363
|
+
const verifiedUserId = identity.kind === "user" && userEmailNormalized ? await upsertUser(executor, {
|
|
364
|
+
email: userEmail ?? userEmailNormalized,
|
|
365
|
+
emailNormalized: userEmailNormalized,
|
|
366
|
+
nowMs,
|
|
367
|
+
...existing?.displayName || identity.displayName ? { displayName: existing?.displayName ?? identity.displayName } : void 0
|
|
368
|
+
}) : void 0;
|
|
369
|
+
if (existing?.userId && verifiedUserId && existing.userId !== verifiedUserId) throw new Error("Identity verified email conflicts with linked user");
|
|
370
|
+
if (linkedUserId && verifiedUserId && linkedUserId !== verifiedUserId) throw new Error("Linked identity conflicts with verified email user");
|
|
371
|
+
if (existing?.userId && linkedUserId && existing.userId !== linkedUserId) throw new Error("Identity conflicts with linked user");
|
|
372
|
+
const userId = existing?.userId ?? linkedUserId ?? verifiedUserId;
|
|
373
|
+
const row = (await executor.db().insert(juniorIdentities).values({
|
|
374
|
+
id: randomUUID(),
|
|
375
|
+
userId: userId ?? null,
|
|
376
|
+
kind: identity.kind,
|
|
377
|
+
provider: identity.provider,
|
|
378
|
+
providerTenantId: tenantId$1(identity.providerTenantId),
|
|
379
|
+
providerSubjectId: identity.providerSubjectId,
|
|
380
|
+
displayName: identity.displayName ?? null,
|
|
381
|
+
handle: identity.handle ?? null,
|
|
382
|
+
email: email ?? null,
|
|
383
|
+
emailNormalized: emailNormalized ?? null,
|
|
384
|
+
emailVerified: Boolean(identity.emailVerified && emailNormalized),
|
|
385
|
+
avatarUrl: identity.avatarUrl ?? null,
|
|
386
|
+
metadata: identity.metadata ?? null,
|
|
387
|
+
createdAt: dateFromMs$1(nowMs),
|
|
388
|
+
updatedAt: dateFromMs$1(nowMs)
|
|
389
|
+
}).onConflictDoUpdate({
|
|
390
|
+
target: [
|
|
391
|
+
juniorIdentities.provider,
|
|
392
|
+
juniorIdentities.providerTenantId,
|
|
393
|
+
juniorIdentities.providerSubjectId
|
|
394
|
+
],
|
|
395
|
+
set: {
|
|
396
|
+
kind: sql`excluded.kind`,
|
|
397
|
+
userId: sql`coalesce(${juniorIdentities.userId}, excluded.user_id)`,
|
|
398
|
+
displayName: linkedUserId ? sql`coalesce(excluded.display_name, ${juniorIdentities.displayName})` : sql`coalesce(${juniorIdentities.displayName}, excluded.display_name)`,
|
|
399
|
+
handle: linkedUserId ? sql`coalesce(excluded.handle, ${juniorIdentities.handle})` : sql`coalesce(${juniorIdentities.handle}, excluded.handle)`,
|
|
400
|
+
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`,
|
|
401
|
+
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`,
|
|
402
|
+
emailVerified: sql`${juniorIdentities.emailVerified} OR excluded.email_verified`,
|
|
403
|
+
avatarUrl: identity.avatarUrl === void 0 ? sql`${juniorIdentities.avatarUrl}` : sql`excluded.avatar_url`,
|
|
404
|
+
metadata: sql`coalesce(${juniorIdentities.metadata}, excluded.metadata_json)`,
|
|
405
|
+
updatedAt: sql`excluded.updated_at`
|
|
406
|
+
},
|
|
407
|
+
...linkedUserId ? { setWhere: sql`${juniorIdentities.userId} IS NULL OR ${juniorIdentities.userId} = excluded.user_id` } : void 0
|
|
408
|
+
}).returning({
|
|
409
|
+
id: juniorIdentities.id,
|
|
410
|
+
userId: juniorIdentities.userId
|
|
411
|
+
}))[0];
|
|
412
|
+
if (!row) {
|
|
413
|
+
if (linkedUserId) throw new Error("Identity conflicts with linked user");
|
|
414
|
+
throw new Error("Identity upsert returned no row");
|
|
415
|
+
}
|
|
416
|
+
return {
|
|
417
|
+
id: row.id,
|
|
418
|
+
...row.userId ? { userId: row.userId } : void 0
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
/** Read the saved timezone for one user. */
|
|
422
|
+
async function readUserTimezone(db, userId) {
|
|
423
|
+
return (await db.select({ timezone: juniorUsers.timezone }).from(juniorUsers).where(eq(juniorUsers.id, userId)).limit(1))[0]?.timezone ?? void 0;
|
|
424
|
+
}
|
|
425
|
+
/** Save the IANA timezone for one user. */
|
|
426
|
+
async function saveUserTimezone(db, userId, timezone, nowMs) {
|
|
427
|
+
await db.update(juniorUsers).set({
|
|
428
|
+
timezone,
|
|
429
|
+
updatedAt: dateFromMs$1(nowMs)
|
|
430
|
+
}).where(and(eq(juniorUsers.id, userId), sql`${juniorUsers.timezone} is distinct from ${timezone}`));
|
|
431
|
+
}
|
|
432
|
+
/** Persist one provider identity observation and link verified emails to users. */
|
|
433
|
+
async function upsertIdentity(executor, identity, nowMs = Date.now()) {
|
|
434
|
+
return await upsertIdentityRecord(executor, identity, void 0, nowMs);
|
|
435
|
+
}
|
|
436
|
+
/**
|
|
437
|
+
* Persist one provider identity after a trusted account-linking flow.
|
|
438
|
+
* Callers must use provider-verified account data, such as GitHub's authenticated
|
|
439
|
+
* user response, never identity claims inferred from content such as Git commits.
|
|
440
|
+
*/
|
|
441
|
+
async function upsertLinkedIdentity(executor, userId, identity, nowMs = Date.now()) {
|
|
442
|
+
return await upsertIdentityRecord(executor, identity, userId, nowMs);
|
|
443
|
+
}
|
|
444
|
+
/**
|
|
445
|
+
* Find the one user whose primary email is in a provider-verified email list.
|
|
446
|
+
* Return undefined when no user or more than one user matches.
|
|
447
|
+
*/
|
|
448
|
+
async function findUserByVerifiedEmails(executor, emails) {
|
|
449
|
+
const normalized = new Set(emails.map((email) => normalizeIdentityEmail(email) ?? ""));
|
|
450
|
+
normalized.delete("");
|
|
451
|
+
if (normalized.size === 0) return void 0;
|
|
452
|
+
const rows = await executor.db().select({
|
|
453
|
+
email: juniorUsers.primaryEmail,
|
|
454
|
+
userId: juniorUsers.id
|
|
455
|
+
}).from(juniorUsers).where(inArray(juniorUsers.primaryEmailNormalized, Array.from(normalized))).limit(2);
|
|
456
|
+
return rows.length === 1 ? rows[0] : void 0;
|
|
457
|
+
}
|
|
458
|
+
/** Remove one exact provider identity owned by a workspace Slack user. */
|
|
459
|
+
async function deleteProviderIdentityForSlackUser(executor, slackTeamId, slackUserId, provider, providerSubjectId) {
|
|
460
|
+
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;
|
|
461
|
+
if (!userId) return;
|
|
462
|
+
await deleteProviderIdentityForUser(executor, userId, provider, providerSubjectId);
|
|
463
|
+
}
|
|
464
|
+
/** Remove one exact provider identity owned by a user. */
|
|
465
|
+
async function deleteProviderIdentityForUser(executor, userId, provider, providerSubjectId) {
|
|
466
|
+
await executor.db().delete(juniorIdentities).where(and(eq(juniorIdentities.userId, userId), eq(juniorIdentities.provider, provider), eq(juniorIdentities.providerTenantId, ""), eq(juniorIdentities.providerSubjectId, providerSubjectId)));
|
|
467
|
+
}
|
|
468
|
+
/**
|
|
469
|
+
* Save one Google sign-in. Link the Google identity to the user of its
|
|
470
|
+
* verified email, and set its sign-in time.
|
|
471
|
+
*/
|
|
472
|
+
async function recordGoogleSignIn(executor, account, nowMs = Date.now()) {
|
|
473
|
+
const stored = await upsertIdentity(executor, {
|
|
474
|
+
kind: "user",
|
|
475
|
+
provider: GOOGLE_IDENTITY_PROVIDER,
|
|
476
|
+
providerSubjectId: account.subject,
|
|
477
|
+
email: account.email,
|
|
478
|
+
emailVerified: true,
|
|
479
|
+
displayName: account.displayName,
|
|
480
|
+
avatarUrl: account.avatarUrl ?? null
|
|
481
|
+
}, nowMs);
|
|
482
|
+
await executor.db().update(juniorIdentities).set({ signedInAt: dateFromMs$1(nowMs) }).where(eq(juniorIdentities.id, stored.id));
|
|
483
|
+
return stored;
|
|
484
|
+
}
|
|
485
|
+
// src/chat/identities/actor.ts
|
|
486
|
+
/**
|
|
487
|
+
* Resolve runtime Actors to the Identity rows that Junior stores for them.
|
|
488
|
+
*
|
|
489
|
+
* The Conversation store, OAuth, the viewer, and message events use this
|
|
490
|
+
* module, so they agree on one Identity per person. The key for each Actor
|
|
491
|
+
* stays private here, so it can change without changing callers.
|
|
492
|
+
*/
|
|
493
|
+
/**
|
|
494
|
+
* The fixed id of the one system Identity: kind `system`, provider `junior`,
|
|
495
|
+
* an empty tenant, and subject `system`. A migration creates the row, so
|
|
496
|
+
* callers use this id without a database read.
|
|
497
|
+
*/
|
|
498
|
+
const SYSTEM_IDENTITY_ID = "system";
|
|
499
|
+
/**
|
|
500
|
+
* Return whether an Actor uses the system Identity.
|
|
501
|
+
*
|
|
502
|
+
* `SystemActor.name` is provenance, not part of the Identity. A LocalActor
|
|
503
|
+
* uses the system Identity because the local runner has no per-person user id.
|
|
504
|
+
*/
|
|
505
|
+
function usesSystemIdentity(actor) {
|
|
506
|
+
return actor.platform === "local" || actor.platform === "system";
|
|
507
|
+
}
|
|
508
|
+
/** Return the Identity key for one Slack or web Actor. */
|
|
509
|
+
function identityKey(actor) {
|
|
510
|
+
switch (actor.platform) {
|
|
511
|
+
case "slack": return {
|
|
512
|
+
kind: "user",
|
|
513
|
+
provider: "slack",
|
|
514
|
+
providerTenantId: actor.teamId,
|
|
515
|
+
providerSubjectId: actor.userId,
|
|
516
|
+
...actor.fullName ? { displayName: actor.fullName } : void 0,
|
|
517
|
+
...actor.userName ? { handle: actor.userName } : void 0,
|
|
518
|
+
...actor.email ? {
|
|
519
|
+
email: actor.email,
|
|
520
|
+
emailVerified: true
|
|
521
|
+
} : void 0,
|
|
522
|
+
metadata: { platform: "slack" }
|
|
523
|
+
};
|
|
524
|
+
case "web": {
|
|
525
|
+
const email = normalizeIdentityEmail(actor.email);
|
|
526
|
+
if (!email) return;
|
|
527
|
+
return {
|
|
528
|
+
kind: "user",
|
|
529
|
+
provider: "junior",
|
|
530
|
+
providerSubjectId: email,
|
|
531
|
+
email,
|
|
532
|
+
emailVerified: true,
|
|
533
|
+
...actor.fullName ? { displayName: actor.fullName } : void 0,
|
|
534
|
+
metadata: { platform: "web" }
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
/**
|
|
540
|
+
* Return the Identity id for one Actor. Save a Slack or web Identity first.
|
|
541
|
+
* Return undefined only for a web Actor without a verified email.
|
|
542
|
+
*/
|
|
543
|
+
async function resolveActorIdentityId(executor, actor, nowMs = Date.now()) {
|
|
544
|
+
if (usesSystemIdentity(actor)) return SYSTEM_IDENTITY_ID;
|
|
545
|
+
const key = identityKey(actor);
|
|
546
|
+
return key ? (await upsertIdentity(executor, key, nowMs)).id : void 0;
|
|
547
|
+
}
|
|
548
|
+
/** Save the profile photo on the Identity of one Slack Actor. Null clears it. */
|
|
549
|
+
async function saveActorAvatar(executor, actor, avatarUrl, nowMs = Date.now()) {
|
|
550
|
+
const key = identityKey(actor);
|
|
551
|
+
if (key) await upsertIdentity(executor, {
|
|
552
|
+
...key,
|
|
553
|
+
avatarUrl
|
|
554
|
+
}, nowMs);
|
|
555
|
+
}
|
|
556
|
+
/** Find the stored Identity id for one Actor without writing. */
|
|
557
|
+
async function findActorIdentityId(db, actor) {
|
|
558
|
+
if (usesSystemIdentity(actor)) return SYSTEM_IDENTITY_ID;
|
|
559
|
+
const key = identityKey(actor);
|
|
560
|
+
if (!key) return;
|
|
561
|
+
return (await db.select({ id: juniorIdentities.id }).from(juniorIdentities).where(and(eq(juniorIdentities.provider, key.provider), eq(juniorIdentities.providerTenantId, key.providerTenantId ?? ""), eq(juniorIdentities.providerSubjectId, key.providerSubjectId))).limit(1))[0]?.id;
|
|
562
|
+
}
|
|
451
563
|
// src/chat/conversations/sql/actor-identity.ts
|
|
452
|
-
/**
|
|
453
|
-
|
|
564
|
+
/**
|
|
565
|
+
* Convert an Actor in the stored Conversation shape to a runtime Actor.
|
|
566
|
+
*
|
|
567
|
+
* TODO(dcramer): Remove after Conversations store the Identity id instead of
|
|
568
|
+
* StoredSlackActor.
|
|
569
|
+
*/
|
|
570
|
+
function actorFromStoredActor(actor) {
|
|
454
571
|
if (actor?.slackUserId) return {
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
...actor.slackUserName ? {
|
|
461
|
-
...actor.email ? {
|
|
462
|
-
email: actor.email,
|
|
463
|
-
emailVerified: true
|
|
464
|
-
} : void 0,
|
|
465
|
-
metadata: { platform: "slack" }
|
|
572
|
+
...actor.email ? { email: actor.email } : void 0,
|
|
573
|
+
...actor.fullName ? { fullName: actor.fullName } : void 0,
|
|
574
|
+
platform: "slack",
|
|
575
|
+
teamId: actor.teamId ?? "",
|
|
576
|
+
userId: actor.slackUserId,
|
|
577
|
+
...actor.slackUserName ? { userName: actor.slackUserName } : void 0
|
|
466
578
|
};
|
|
467
|
-
|
|
468
|
-
if (!email) return;
|
|
579
|
+
if (!actor?.email) return;
|
|
469
580
|
return {
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
email
|
|
474
|
-
emailVerified: true,
|
|
475
|
-
...actor?.fullName ? { displayName: actor.fullName } : void 0,
|
|
476
|
-
metadata: { platform: "web" }
|
|
581
|
+
email: actor.email,
|
|
582
|
+
...actor.fullName ? { fullName: actor.fullName } : void 0,
|
|
583
|
+
platform: "web",
|
|
584
|
+
userId: actor.email
|
|
477
585
|
};
|
|
478
586
|
}
|
|
479
587
|
/**
|
|
480
|
-
* Build the
|
|
481
|
-
*
|
|
482
|
-
*
|
|
588
|
+
* Build the legacy system Identity for a Conversation that has no Actor.
|
|
589
|
+
*
|
|
590
|
+
* TODO(dcramer): Remove after every ingress supplies its Actor. These rows use
|
|
591
|
+
* one subject per Source (`scheduler`, `local-cli`, `event`). An Actor
|
|
592
|
+
* uses the one system Identity instead, so the backfill must merge them.
|
|
483
593
|
*/
|
|
484
|
-
function identityFromRuntimeActor(actor) {
|
|
485
|
-
if (actor.platform === "slack") return identityFromActor(toStoredSlackActor(actor));
|
|
486
|
-
if (actor.platform === "web") return identityFromActor(actor.email ? { email: actor.email } : void 0);
|
|
487
|
-
}
|
|
488
|
-
/** Build a system identity when a conversation has no user actor. */
|
|
489
594
|
function systemIdentityFromSource(source) {
|
|
490
595
|
if (source === "scheduler") return {
|
|
491
596
|
kind: "system",
|
|
@@ -506,9 +611,15 @@ function systemIdentityFromSource(source) {
|
|
|
506
611
|
displayName: "Event"
|
|
507
612
|
};
|
|
508
613
|
}
|
|
509
|
-
/**
|
|
510
|
-
|
|
511
|
-
|
|
614
|
+
/**
|
|
615
|
+
* Save the Identity of a Conversation's actor and return its id. Use the
|
|
616
|
+
* legacy source system Identity when the Conversation has no actor.
|
|
617
|
+
*/
|
|
618
|
+
async function resolveConversationActorIdentityId(executor, conversation) {
|
|
619
|
+
const actor = actorFromStoredActor(conversation.actor);
|
|
620
|
+
if (actor) return await resolveActorIdentityId(executor, actor, conversation.updatedAtMs);
|
|
621
|
+
const fallback = systemIdentityFromSource(conversation.source);
|
|
622
|
+
return fallback ? (await upsertIdentity(executor, fallback, conversation.updatedAtMs)).id : void 0;
|
|
512
623
|
}
|
|
513
624
|
/**
|
|
514
625
|
* Reconstruct a stored actor with the linked user name and identity-scoped
|
|
@@ -780,6 +891,9 @@ var SqlStore = class {
|
|
|
780
891
|
if (!row) return;
|
|
781
892
|
return conversationFromRow(row);
|
|
782
893
|
}
|
|
894
|
+
async getRunMetrics(args) {
|
|
895
|
+
return await readConversationRunMetrics(this.executor, args);
|
|
896
|
+
}
|
|
783
897
|
async recordActivity(args) {
|
|
784
898
|
const nowMs = args.nowMs ?? now();
|
|
785
899
|
const activityAtMs = args.activityAtMs ?? nowMs;
|
|
@@ -824,7 +938,12 @@ var SqlStore = class {
|
|
|
824
938
|
const existing = existingRow ? conversationFromRow(existingRow) : void 0;
|
|
825
939
|
if (!existing && !args.destination) return;
|
|
826
940
|
const incomingExecutionAt = args.execution.updatedAtMs ?? args.updatedAtMs;
|
|
827
|
-
const
|
|
941
|
+
const existingExecutionAt = existing?.execution.updatedAtMs ?? existing?.updatedAtMs ?? 0;
|
|
942
|
+
const latestMetricRunId = existing?.executionMetrics?.runId;
|
|
943
|
+
const incomingIsFresh = !(args.metrics !== null && args.execution.runId !== void 0 && latestMetricRunId !== void 0 && latestMetricRunId !== args.execution.runId && (args.execution.status === "idle" || args.execution.status === "failed") && await readConversationRunMetrics(this.executor, {
|
|
944
|
+
conversationId: args.conversationId,
|
|
945
|
+
runId: args.execution.runId
|
|
946
|
+
}) !== void 0) && incomingExecutionAt >= existingExecutionAt;
|
|
828
947
|
const execution = incomingIsFresh ? args.execution : existing?.execution ?? args.execution;
|
|
829
948
|
const actor = mergeActor(existing?.actor, args.actor);
|
|
830
949
|
const source = existing?.source ?? args.source;
|
|
@@ -924,8 +1043,7 @@ var SqlStore = class {
|
|
|
924
1043
|
location: conversation.location,
|
|
925
1044
|
sessionSource: conversation.sessionSource
|
|
926
1045
|
});
|
|
927
|
-
const
|
|
928
|
-
const actorIdentity = actorIdentityObservation ? await upsertIdentity(this.executor, actorIdentityObservation, conversation.updatedAtMs) : void 0;
|
|
1046
|
+
const actorIdentityId = await resolveConversationActorIdentityId(this.executor, conversation);
|
|
929
1047
|
const rootConversationId = conversation.parentConversationId ? sql`(
|
|
930
1048
|
select parent.root_conversation_id
|
|
931
1049
|
from junior_conversations parent
|
|
@@ -939,7 +1057,7 @@ var SqlStore = class {
|
|
|
939
1057
|
location: location ?? null,
|
|
940
1058
|
destinationId: destinationId ?? null,
|
|
941
1059
|
destination: null,
|
|
942
|
-
actorIdentityId:
|
|
1060
|
+
actorIdentityId: actorIdentityId ?? null,
|
|
943
1061
|
creatorIdentityId: null,
|
|
944
1062
|
credentialSubjectIdentityId: null,
|
|
945
1063
|
actor: null,
|
|
@@ -962,7 +1080,7 @@ var SqlStore = class {
|
|
|
962
1080
|
sessionSource: sql`coalesce(${juniorConversations.sessionSource}, excluded.source_json)`,
|
|
963
1081
|
location: sql`coalesce(excluded.location_json, ${juniorConversations.location})`,
|
|
964
1082
|
destinationId: sql`coalesce(excluded.destination_id, ${juniorConversations.destinationId})`,
|
|
965
|
-
actorIdentityId: sql`coalesce(
|
|
1083
|
+
actorIdentityId: sql`coalesce(${juniorConversations.actorIdentityId}, excluded.actor_identity_id)`,
|
|
966
1084
|
creatorIdentityId: sql`coalesce(excluded.creator_identity_id, ${juniorConversations.creatorIdentityId})`,
|
|
967
1085
|
credentialSubjectIdentityId: sql`coalesce(excluded.credential_subject_identity_id, ${juniorConversations.credentialSubjectIdentityId})`,
|
|
968
1086
|
channelName: sql`coalesce(excluded.channel_name, ${juniorConversations.channelName})`,
|
|
@@ -1061,7 +1179,6 @@ async function ensureConversationRow(executor, conversationId, atMs, options = {
|
|
|
1061
1179
|
});
|
|
1062
1180
|
}
|
|
1063
1181
|
// src/chat/conversations/sql/event-actor.ts
|
|
1064
|
-
const SLACK_USER_ID_PATTERN = /^[UW][A-Z0-9]{5,}$/;
|
|
1065
1182
|
function isRecord(value) {
|
|
1066
1183
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
1067
1184
|
}
|
|
@@ -1118,48 +1235,41 @@ async function findJuniorIdentityIdByDashboardAuthorId(executor, dashboardAuthor
|
|
|
1118
1235
|
}
|
|
1119
1236
|
/**
|
|
1120
1237
|
* Resolve the durable actor identity for one event payload.
|
|
1121
|
-
*
|
|
1122
|
-
*
|
|
1238
|
+
* Ingress sets `authorIdentityId` on new user Messages. Legacy writes without
|
|
1239
|
+
* it still guess the author from its data, and a new Message logs the guess.
|
|
1123
1240
|
*/
|
|
1124
1241
|
async function resolveEventActorIdentityId(executor, args) {
|
|
1125
1242
|
const data = args.data;
|
|
1126
1243
|
if (data.type !== "message" && data.type !== "message_updated") return;
|
|
1127
1244
|
if (typeof data.authorIdentityId === "string" && data.authorIdentityId) return data.authorIdentityId;
|
|
1128
1245
|
if (data.role !== "user") return;
|
|
1246
|
+
if (data.type === "message") logWarn("conversation.message.author_identity.missing", { "app.message.id": data.messageId });
|
|
1129
1247
|
const author = readAuthor(data.meta);
|
|
1130
1248
|
if (author.isBot === true) return;
|
|
1131
1249
|
const userId = parseActorUserId(author.userId);
|
|
1132
1250
|
if (!userId) return;
|
|
1133
|
-
if (
|
|
1251
|
+
if (isSlackUserId(userId)) {
|
|
1134
1252
|
const tenantId = await loadConversationSlackTenantId(executor, args.conversationId);
|
|
1135
|
-
|
|
1136
|
-
kind: "user",
|
|
1137
|
-
provider: "slack",
|
|
1138
|
-
providerSubjectId: userId,
|
|
1139
|
-
...tenantId ? { providerTenantId: tenantId } : void 0,
|
|
1140
|
-
...author.fullName ? { displayName: author.fullName } : void 0,
|
|
1141
|
-
...author.userName ? { handle: author.userName } : void 0,
|
|
1142
|
-
...author.email ? {
|
|
1143
|
-
email: author.email,
|
|
1144
|
-
emailVerified: true
|
|
1145
|
-
} : void 0
|
|
1146
|
-
};
|
|
1147
|
-
if (tenantId) return (await upsertIdentity(executor, observation, args.nowMs)).id;
|
|
1148
|
-
return await findIdentityIdByProviderSubject(executor, {
|
|
1253
|
+
if (!tenantId) return await findIdentityIdByProviderSubject(executor, {
|
|
1149
1254
|
provider: "slack",
|
|
1150
1255
|
providerSubjectId: userId
|
|
1151
|
-
})
|
|
1256
|
+
});
|
|
1257
|
+
return await resolveActorIdentityId(executor, {
|
|
1258
|
+
platform: "slack",
|
|
1259
|
+
teamId: tenantId,
|
|
1260
|
+
userId,
|
|
1261
|
+
...author.email ? { email: author.email } : void 0,
|
|
1262
|
+
...author.fullName ? { fullName: author.fullName } : void 0,
|
|
1263
|
+
...author.userName ? { userName: author.userName } : void 0
|
|
1264
|
+
}, args.nowMs);
|
|
1152
1265
|
}
|
|
1153
|
-
const email = typeof author.email === "string" && author.email.includes("@") ? author.email
|
|
1154
|
-
if (email) return
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
providerSubjectId: email,
|
|
1266
|
+
const email = typeof author.email === "string" && author.email.includes("@") ? author.email : userId.includes("@") ? userId : void 0;
|
|
1267
|
+
if (email) return await resolveActorIdentityId(executor, {
|
|
1268
|
+
platform: "web",
|
|
1269
|
+
userId,
|
|
1158
1270
|
email,
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
metadata: { platform: "web" }
|
|
1162
|
-
}, args.nowMs)).id;
|
|
1271
|
+
...author.fullName ? { fullName: author.fullName } : void 0
|
|
1272
|
+
}, args.nowMs);
|
|
1163
1273
|
if (userId.startsWith("dashboard:")) return findJuniorIdentityIdByDashboardAuthorId(executor, userId);
|
|
1164
1274
|
return findJuniorIdentityIdByEmail(executor, userId);
|
|
1165
1275
|
}
|
|
@@ -1556,6 +1666,163 @@ async function closeDb() {
|
|
|
1556
1666
|
current = void 0;
|
|
1557
1667
|
await previous?.db.close();
|
|
1558
1668
|
}
|
|
1669
|
+
// src/chat/state/cache.ts
|
|
1670
|
+
const MAX_CACHE_KEY_LENGTH = 512;
|
|
1671
|
+
const LOAD_WAIT_MS = 1e4;
|
|
1672
|
+
/**
|
|
1673
|
+
* Create a cache in the state adapter under `junior:cache:{namespace}:`.
|
|
1674
|
+
* Entries are wrapped, so `null` and `false` values are cache hits.
|
|
1675
|
+
*/
|
|
1676
|
+
function createCache(state, namespace) {
|
|
1677
|
+
return { async getOrLoad(key, load, options) {
|
|
1678
|
+
if (!key.trim()) throw new Error("Cache key is required");
|
|
1679
|
+
if (key.length > MAX_CACHE_KEY_LENGTH) throw new Error("Cache key exceeds the maximum length");
|
|
1680
|
+
const cacheKey = `junior:cache:${namespace}:${key}`;
|
|
1681
|
+
const cached = async () => options?.refresh ? void 0 : await state.get(cacheKey) ?? void 0;
|
|
1682
|
+
await state.connect();
|
|
1683
|
+
const hit = await cached();
|
|
1684
|
+
if (hit) return hit.value;
|
|
1685
|
+
const attempt = await withLock(state, `${cacheKey}:load`, async () => {
|
|
1686
|
+
const loadedMeanwhile = await cached();
|
|
1687
|
+
if (loadedMeanwhile) return loadedMeanwhile.value;
|
|
1688
|
+
const entry = await load();
|
|
1689
|
+
if (entry.ttlMs !== void 0) {
|
|
1690
|
+
if (!(entry.ttlMs > 0)) throw new Error("Cache ttlMs must be positive");
|
|
1691
|
+
await state.set(cacheKey, { value: entry.value }, entry.ttlMs);
|
|
1692
|
+
}
|
|
1693
|
+
return entry.value;
|
|
1694
|
+
}, {
|
|
1695
|
+
keepAlive: true,
|
|
1696
|
+
waitMs: LOAD_WAIT_MS
|
|
1697
|
+
});
|
|
1698
|
+
if (attempt.acquired) return attempt.value;
|
|
1699
|
+
return (await load()).value;
|
|
1700
|
+
} };
|
|
1701
|
+
}
|
|
1702
|
+
// src/chat/identities/avatar.ts
|
|
1703
|
+
/**
|
|
1704
|
+
* Profile photos of people, saved on their native Slack and Google
|
|
1705
|
+
* identities. Junior serves each photo from its own API, so browsers never
|
|
1706
|
+
* load a photo from Slack or Google.
|
|
1707
|
+
*/
|
|
1708
|
+
const AVATAR_HOSTS = {
|
|
1709
|
+
slack: ["avatars.slack-edge.com"],
|
|
1710
|
+
[GOOGLE_IDENTITY_PROVIDER]: ["googleusercontent.com"]
|
|
1711
|
+
};
|
|
1712
|
+
const AVATAR_PROVIDERS = Object.keys(AVATAR_HOSTS);
|
|
1713
|
+
const AVATAR_CACHE_TTL_MS = 864e5;
|
|
1714
|
+
const AVATAR_FAILURE_TTL_MS = 3e5;
|
|
1715
|
+
const AVATAR_FETCH_TIMEOUT_MS = 5e3;
|
|
1716
|
+
const MAX_AVATAR_BYTES = 1048576;
|
|
1717
|
+
/**
|
|
1718
|
+
* Return the URL when it is HTTPS on a photo host of the provider or a
|
|
1719
|
+
* subdomain. Junior fetches only these URLs.
|
|
1720
|
+
*/
|
|
1721
|
+
function parseAvatarUrl(value, provider) {
|
|
1722
|
+
const hosts = AVATAR_HOSTS[provider];
|
|
1723
|
+
if (!hosts || !value || !URL.canParse(value)) return void 0;
|
|
1724
|
+
const url = new URL(value);
|
|
1725
|
+
if (url.protocol !== "https:" || url.username || url.password) return;
|
|
1726
|
+
const host = url.hostname.toLowerCase();
|
|
1727
|
+
return hosts.some((allowed) => host === allowed || host.endsWith(`.${allowed}`)) ? url.href : void 0;
|
|
1728
|
+
}
|
|
1729
|
+
function avatarPath(identityId) {
|
|
1730
|
+
return `/api/people/avatars/${encodeURIComponent(identityId)}`;
|
|
1731
|
+
}
|
|
1732
|
+
/**
|
|
1733
|
+
* Read the photo path of each user: the Slack photo, then the Google photo.
|
|
1734
|
+
* Users without a photo are not in the map.
|
|
1735
|
+
*/
|
|
1736
|
+
async function readUserAvatarUrls(db, userIds) {
|
|
1737
|
+
if (userIds.length === 0) return /* @__PURE__ */ new Map();
|
|
1738
|
+
const rows = await db.select({
|
|
1739
|
+
identityId: juniorIdentities.id,
|
|
1740
|
+
provider: juniorIdentities.provider,
|
|
1741
|
+
updatedAt: juniorIdentities.updatedAt,
|
|
1742
|
+
userId: juniorIdentities.userId
|
|
1743
|
+
}).from(juniorIdentities).where(and(inArray(juniorIdentities.userId, [...new Set(userIds)]), eq(juniorIdentities.kind, "user"), inArray(juniorIdentities.provider, AVATAR_PROVIDERS), isNotNull(juniorIdentities.avatarUrl)));
|
|
1744
|
+
const rank = (row) => AVATAR_PROVIDERS.indexOf(row.provider);
|
|
1745
|
+
rows.sort((left, right) => rank(left) - rank(right) || right.updatedAt.getTime() - left.updatedAt.getTime());
|
|
1746
|
+
const avatars = /* @__PURE__ */ new Map();
|
|
1747
|
+
for (const row of rows) if (row.userId && !avatars.has(row.userId)) avatars.set(row.userId, avatarPath(row.identityId));
|
|
1748
|
+
return avatars;
|
|
1749
|
+
}
|
|
1750
|
+
/**
|
|
1751
|
+
* Return the photo path for one Identity row. A linked Identity uses the
|
|
1752
|
+
* photo of its user. An Identity without a user uses its own photo.
|
|
1753
|
+
*/
|
|
1754
|
+
function identityAvatarUrl(row, userAvatarUrls) {
|
|
1755
|
+
if (row.userId) return userAvatarUrls.get(row.userId);
|
|
1756
|
+
return row.avatarUrl ? avatarPath(row.identityId) : void 0;
|
|
1757
|
+
}
|
|
1758
|
+
async function readBoundedBody(response, maxBytes) {
|
|
1759
|
+
if (!response.body) return /* @__PURE__ */ new Uint8Array();
|
|
1760
|
+
const reader = response.body.getReader();
|
|
1761
|
+
const chunks = [];
|
|
1762
|
+
let total = 0;
|
|
1763
|
+
while (true) {
|
|
1764
|
+
const { done, value } = await reader.read();
|
|
1765
|
+
if (done) return Buffer.concat(chunks);
|
|
1766
|
+
total += value.byteLength;
|
|
1767
|
+
if (total > maxBytes) {
|
|
1768
|
+
await reader.cancel();
|
|
1769
|
+
return;
|
|
1770
|
+
}
|
|
1771
|
+
chunks.push(value);
|
|
1772
|
+
}
|
|
1773
|
+
}
|
|
1774
|
+
function avatarFetchFailed(host, attributes) {
|
|
1775
|
+
logWarn("avatar.fetch.failed", {
|
|
1776
|
+
"server.address": host,
|
|
1777
|
+
...attributes
|
|
1778
|
+
});
|
|
1779
|
+
return null;
|
|
1780
|
+
}
|
|
1781
|
+
async function fetchAvatarImage(url) {
|
|
1782
|
+
const host = new URL(url).hostname;
|
|
1783
|
+
let data;
|
|
1784
|
+
try {
|
|
1785
|
+
const response = await fetch(url, {
|
|
1786
|
+
redirect: "error",
|
|
1787
|
+
signal: AbortSignal.timeout(AVATAR_FETCH_TIMEOUT_MS)
|
|
1788
|
+
});
|
|
1789
|
+
if (!response.ok) {
|
|
1790
|
+
await response.body?.cancel();
|
|
1791
|
+
return avatarFetchFailed(host, { "http.response.status_code": response.status });
|
|
1792
|
+
}
|
|
1793
|
+
data = await readBoundedBody(response, MAX_AVATAR_BYTES);
|
|
1794
|
+
} catch (error) {
|
|
1795
|
+
return avatarFetchFailed(host, { "exception.message": error instanceof Error ? error.message : String(error) });
|
|
1796
|
+
}
|
|
1797
|
+
if (!data) return avatarFetchFailed(host, { "app.reason": "too_large" });
|
|
1798
|
+
const contentType = detectImageMediaType(data);
|
|
1799
|
+
if (!contentType) return avatarFetchFailed(host, { "app.reason": "not_an_image" });
|
|
1800
|
+
return {
|
|
1801
|
+
contentType,
|
|
1802
|
+
data: Buffer.from(data).toString("base64")
|
|
1803
|
+
};
|
|
1804
|
+
}
|
|
1805
|
+
/**
|
|
1806
|
+
* Read the saved photo of one Identity. Junior fetches it from the provider
|
|
1807
|
+
* once and keeps it in the state cache for a day. Return undefined when the
|
|
1808
|
+
* Identity has no photo or the provider does not return one.
|
|
1809
|
+
*/
|
|
1810
|
+
async function readAvatarImage(db, state, identityId) {
|
|
1811
|
+
const [row] = await db.select({
|
|
1812
|
+
avatarUrl: juniorIdentities.avatarUrl,
|
|
1813
|
+
provider: juniorIdentities.provider
|
|
1814
|
+
}).from(juniorIdentities).where(and(eq(juniorIdentities.id, identityId), eq(juniorIdentities.kind, "user"))).limit(1);
|
|
1815
|
+
const url = row ? parseAvatarUrl(row.avatarUrl, row.provider) : void 0;
|
|
1816
|
+
if (!url) return void 0;
|
|
1817
|
+
const key = `avatar:${createHash("sha256").update(url).digest("hex")}`;
|
|
1818
|
+
return await createCache(state, "core").getOrLoad(key, async () => {
|
|
1819
|
+
const loaded = await fetchAvatarImage(url);
|
|
1820
|
+
return {
|
|
1821
|
+
value: loaded,
|
|
1822
|
+
ttlMs: loaded ? AVATAR_CACHE_TTL_MS : AVATAR_FAILURE_TTL_MS
|
|
1823
|
+
};
|
|
1824
|
+
}) ?? void 0;
|
|
1825
|
+
}
|
|
1559
1826
|
// src/chat/plugins/viewer.ts
|
|
1560
1827
|
function identityFromRow(row) {
|
|
1561
1828
|
return identitySchema.parse({
|
|
@@ -1615,6 +1882,10 @@ async function updateViewerDisplayNameFromSql(db, userId, displayName) {
|
|
|
1615
1882
|
}).where(eq(juniorUsers.id, userId)).returning())[0];
|
|
1616
1883
|
return userRow ? await readUserById(db, userRow) : void 0;
|
|
1617
1884
|
}
|
|
1885
|
+
/** Read the photo path of one canonical user: Slack, then Google. */
|
|
1886
|
+
async function readViewerAvatarUrl(userId) {
|
|
1887
|
+
return (await readUserAvatarUrls(getDb(), [userId])).get(userId);
|
|
1888
|
+
}
|
|
1618
1889
|
/** Update one canonical user's display name. */
|
|
1619
1890
|
async function updateViewerDisplayName(userId, displayName) {
|
|
1620
1891
|
return await updateViewerDisplayNameFromSql(getDb(), userId, displayName);
|
|
@@ -1641,17 +1912,8 @@ async function listAdminUsersFromSql(db) {
|
|
|
1641
1912
|
}
|
|
1642
1913
|
/** Resolve the stored identity and linked user for one runtime actor. */
|
|
1643
1914
|
async function readActorIdentityFromSql(db, actor) {
|
|
1644
|
-
const
|
|
1645
|
-
|
|
1646
|
-
const row = (await db.select({
|
|
1647
|
-
identity: juniorIdentities,
|
|
1648
|
-
user: juniorUsers
|
|
1649
|
-
}).from(juniorIdentities).leftJoin(juniorUsers, eq(juniorUsers.id, juniorIdentities.userId)).where(and(eq(juniorIdentities.kind, "user"), eq(juniorIdentities.provider, key.provider), eq(juniorIdentities.providerTenantId, key.providerTenantId ?? ""), eq(juniorIdentities.providerSubjectId, key.providerSubjectId))).limit(1))[0];
|
|
1650
|
-
if (!row) return void 0;
|
|
1651
|
-
return {
|
|
1652
|
-
identity: identityFromRow(row.identity),
|
|
1653
|
-
...row.user ? { user: await readUserById(db, row.user) } : void 0
|
|
1654
|
-
};
|
|
1915
|
+
const identityId = await findActorIdentityId(db, actor);
|
|
1916
|
+
return identityId ? await readIdentityByIdFromSql(db, identityId) : void 0;
|
|
1655
1917
|
}
|
|
1656
1918
|
/** Resolve the stored identity and linked user for one runtime actor. */
|
|
1657
1919
|
async function readActorIdentity(actor) {
|
|
@@ -1673,4 +1935,4 @@ async function readIdentityByIdFromSql(db, identityId) {
|
|
|
1673
1935
|
async function readIdentityById(identityId) {
|
|
1674
1936
|
return await readIdentityByIdFromSql(getDb(), identityId);
|
|
1675
1937
|
}
|
|
1676
|
-
export {
|
|
1938
|
+
export { startLocationThread as A, upsertLinkedIdentity as B, getSqlExecutor as C, createSqlStore as D, loadConversationSlackTenantIds as E, deleteProviderIdentityForUser as F, agentTurnTotalTokens as G, parseSessionSource as H, findUserByVerifiedEmails as I, resolveRootConversationId as J, agentTurnUsageSchema as K, readUserTimezone as L, resolveActorIdentityId as M, saveActorAvatar as N, withConversationMutationLock as O, deleteProviderIdentityForSlackUser as P, sameDestination as Q, recordGoogleSignIn as R, getDb as S, withConversationEventLock as T, addAgentTurnUsage as U, bindProviderConversation as V, agentTurnCostUsd as W, parseDestination as X, createSlackDestination as Y, requireSlackDestination as Z, closeDb as _, readActorIdentityFromSql as a, getConversationMessageSearchStore as b, readViewerAvatarUrl as c, updateViewerDisplayName as d, identityAvatarUrl as f, createCache as g, readUserAvatarUrls as h, readActorIdentity as i, SYSTEM_IDENTITY_ID as j, locationFromRow as k, resolveViewerUser as l, readAvatarImage as m, findUserByEmailFromSql as n, readIdentityById as o, parseAvatarUrl as p, hasAgentTurnUsage as q, listAdminUsersFromSql as r, readIdentityByIdFromSql as s, findUserByEmail as t, setUserAdminFromSql as u, db_exports as v, createSqlConversationEventStore as w, getConversationStore as x, getConversationEventStore as y, saveUserTimezone as z };
|