@sentry/junior 0.267.0 → 0.268.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.
Files changed (80) hide show
  1. package/dist/{actor-DZSokpwj.js → actor-CP9K2Idu.js} +2 -1
  2. package/dist/{adapter-BybMEezj.js → adapter-04AcD2SO.js} +4 -6
  3. package/dist/{admin-DOs1_EiW.js → admin-DIU1QyG3.js} +1 -1
  4. package/dist/{agent-invocations-CxKldAeB.js → agent-invocations-C1RNOCs8.js} +2 -0
  5. package/dist/{agent-runner-DgNATN4D.js → agent-runner-CAYfFmU7.js} +4409 -3020
  6. package/dist/api/schema.d.ts +2 -5629
  7. package/dist/api/schema.js +4 -4
  8. package/dist/api.d.ts +1 -1
  9. package/dist/api.js +154 -32
  10. package/dist/{app-DAB3fzyp.d.ts → app-z6Uuyq3j.d.ts} +3 -2
  11. package/dist/app.d.ts +1 -1
  12. package/dist/app.js +332 -98
  13. package/dist/{briefs-CWKjka7v.js → briefs-DQbV2jNr.js} +2 -2
  14. package/dist/{catalog-runtime-cMlAI5LY.js → catalog-runtime-BD9GSpc6.js} +1 -1
  15. package/dist/cli/chat.js +15 -15
  16. package/dist/cli/check.js +3 -3
  17. package/dist/cli/main.js +3 -3
  18. package/dist/cli/plugins.js +4 -4
  19. package/dist/cli/snapshot-create.js +4 -4
  20. package/dist/cli/upgrade.d.ts +1 -1
  21. package/dist/cli/upgrade.js +3 -3
  22. package/dist/{client-BjgbMawj.js → client-C1yDYjoS.js} +2 -2
  23. package/dist/{config-CdB6yJj5.js → config-DrTeF80T.js} +2 -2
  24. package/dist/{connections-RE_65jzK.js → connections-D46f8vvi.js} +10 -11
  25. package/dist/{conversation-privacy-BzgJC5k0.js → conversation-privacy-CxlOCiUo.js} +1 -1
  26. package/dist/{credential-sync-DhEGEpgq.js → credential-sync-sm9fegLY.js} +29 -34
  27. package/dist/{credentialed-CIGwoLU2.js → credentialed-lukuD-cc.js} +39 -13
  28. package/dist/db-BU4LQD8s.d.ts +6 -0
  29. package/dist/{executor-VYrx_K4w.js → executor-4vhvsMZJ.js} +78 -4
  30. package/dist/instrumentation.js +2 -2
  31. package/dist/{assistant-reply-CT7VcXQe.js → interruption-marker-CkncDT8d.js} +8 -2
  32. package/dist/{job-queue-C0BZSa7r.js → job-queue-In9QKz-I.js} +2 -2
  33. package/dist/{list-1XQP-o6K.js → list-BkgnXEEB.js} +298 -807
  34. package/dist/{logging-DuZ1A4ab.js → logging-DLSpkSSr.js} +1 -1
  35. package/dist/{message-NKSbbnA6.js → message-BObyg59s.js} +4 -4
  36. package/dist/nitro.d.ts +1 -1
  37. package/dist/nitro.js +3 -3
  38. package/dist/{oauth-callback-server-qAHLPtml.js → oauth-callback-server-B-WbSdoH.js} +2 -2
  39. package/dist/{registry-FQutO926.js → registry-BgecKva0.js} +2 -2
  40. package/dist/{web-message-id-CJSUiksE.js → reporting-window-DVCD_10C.js} +1 -10
  41. package/dist/{resolve-Bk4yeuHy.js → resolve-Cpa4Bu1O.js} +3 -3
  42. package/dist/{result-B9e9ATOU.js → result-D3TSmZZ8.js} +8 -8
  43. package/dist/{runner-C3S2ObH-.js → runner-DS5OKhEp.js} +10 -10
  44. package/dist/{sandbox-egress-signals-DqpGzzMB.js → sandbox-egress-signals-CnDjY5rx.js} +1 -1
  45. package/dist/{schema-X7Fj4fv1.js → schema-0050HFnB.js} +80 -4
  46. package/dist/schema-ZngqcOWM.d.ts +5813 -0
  47. package/dist/{service-account-Do30HUNv.js → service-account-CKV2WTY7.js} +3 -3
  48. package/dist/{service-account-BQasl-qq.js → service-account-DGjnDw9y.js} +3 -3
  49. package/dist/{session-stop-DkOjt46_.js → session-stop-Dam1EI7B.js} +494 -18
  50. package/dist/{skills-CQHrwfOa.js → skills-CfAnfImD.js} +2 -2
  51. package/dist/{spawn-BHuqN14f.js → spawn-cmq9p4Ly.js} +2 -2
  52. package/dist/{store-6EetVP-m.js → store-C5PN02QO.js} +4 -4
  53. package/dist/{store-B8CABbmR.d.ts → store-xMuuXRE0.d.ts} +13 -7
  54. package/dist/{store-DvNZIsCr.js → store-zPEoJPBB.js} +3 -3
  55. package/dist/{task-queue-B0DLgd2P.js → task-queue-_mf28dSu.js} +3 -3
  56. package/dist/{task-runner-B-PD_JOj.js → task-runner-DMI2prDO.js} +11 -12
  57. package/dist/{tasks-0ilmuCux.js → tasks-SB9z-VTU.js} +124 -21
  58. package/dist/{thread-state-DRmdJ2QN.js → thread-state-BbbPh0Sn.js} +8 -18
  59. package/dist/{turn-failure-response-G11cs2gE.js → turn-failure-response-BKKmnMB5.js} +10 -15
  60. package/dist/{turn-lifecycle-OzLIwvi1.js → turn-lifecycle-oP0kVeGz.js} +1 -1
  61. package/dist/{validation-CxEmt1id.js → validation--JsAGLwc.js} +1 -1
  62. package/dist/{version-Bapedn4A.js → version-jHwJp4hC.js} +1 -1
  63. package/dist/version.js +1 -1
  64. package/dist/{viewer-DrN3oC2X.js → viewer-CpXTGlep.js} +323 -235
  65. package/dist/{user-input-request-BDFBdF8r.js → web-message-id-DlHNANtd.js} +17 -1
  66. package/dist/{work-DUQoNcqP.js → work-3N-GHY9m.js} +10 -10
  67. package/dist/{worker-D1l4Ugk0.js → worker-CCdbcItd.js} +6 -6
  68. package/migrations/0065_user_customization.sql +1 -0
  69. package/migrations/0066_review_items.sql +53 -0
  70. package/migrations/0067_review_item_dedupe_tenant.sql +2 -0
  71. package/migrations/0068_review_item_slack_cards.sql +10 -0
  72. package/migrations/meta/0065_snapshot.json +3511 -0
  73. package/migrations/meta/0066_snapshot.json +4099 -0
  74. package/migrations/meta/0067_snapshot.json +3921 -0
  75. package/migrations/meta/0068_snapshot.json +4176 -0
  76. package/migrations/meta/_journal.json +29 -1
  77. package/package.json +5 -5
  78. package/dist/db-Bg-zC3r5.d.ts +0 -5
  79. package/dist/images-CecjXAAz.js +0 -64
  80. package/dist/turn-session-routing-Dyut9l7Y.js +0 -76
@@ -1,9 +1,9 @@
1
1
  import { t as __exportAll } from "./rolldown-runtime-nwUcYXjP.js";
2
- import { i as getChatConfig } from "./config-CdB6yJj5.js";
3
- import { L as GOOGLE_IDENTITY_PROVIDER, z as normalizeIdentityEmail } from "./logging-DuZ1A4ab.js";
4
- import { c as toStoredSlackActor, d as parseSlackTeamId, o as parseActorUserId, u as parseSlackChannelReferenceId } from "./actor-DZSokpwj.js";
5
- import { c as juniorIdentities, d as juniorDestinations, l as juniorUsers, o as conversationReadColumns, s as juniorConversations } from "./agent-invocations-CxKldAeB.js";
6
- import { _ as juniorConversationAnnotations, d as juniorConversationBindings, h as juniorConversationEvents, m as juniorConversationMetrics, p as CONVERSATION_METRICS, t as createJuniorSqlExecutor, u as juniorConversationParticipants } from "./executor-VYrx_K4w.js";
2
+ import { i as getChatConfig } from "./config-DrTeF80T.js";
3
+ import { L as GOOGLE_IDENTITY_PROVIDER, z as normalizeIdentityEmail } from "./logging-DLSpkSSr.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-4vhvsMZJ.js";
7
7
  import { c as newConversationEventSchema, h as usageSchema, l as encodeHistoryPayload, o as decodeStoredConversationEvent, r as conversationEventDataSchema, s as historyReplacementSchema } from "./history-C84mlAwO.js";
8
8
  import { randomUUID } from "node:crypto";
9
9
  import { and, asc, desc, eq, gt, gte, inArray, isNull, lt, lte, ne, or, sql } from "drizzle-orm";
@@ -37,161 +37,6 @@ function sameDestination(left, right) {
37
37
  if (left.platform === "slack" && right.platform === "slack") return left.teamId === right.teamId && left.channelId === right.channelId;
38
38
  return false;
39
39
  }
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
40
  // src/chat/conversations/sql/participants.ts
196
41
  /** Resolve the root conversation id that owns personal-feed membership. */
197
42
  async function resolveRootConversationId(executor, conversationId) {
@@ -339,6 +184,38 @@ function conversationMetricValues(args) {
339
184
  cost_usd: agentTurnCostUsd(usage)
340
185
  };
341
186
  }
187
+ /**
188
+ * Read one Run's stored totals from its metric rows.
189
+ *
190
+ * Returns undefined when the Run has no rows. Cost keeps only its total.
191
+ */
192
+ async function readConversationRunMetrics(executor, args) {
193
+ const rows = await executor.db().select({
194
+ metric: juniorConversationMetrics.metric,
195
+ value: juniorConversationMetrics.value
196
+ }).from(juniorConversationMetrics).where(and(eq(juniorConversationMetrics.conversationId, args.conversationId), eq(juniorConversationMetrics.runId, args.runId)));
197
+ const values = new Map(rows.map((row) => [row.metric, row.value]));
198
+ const durationMs = values.get("duration_ms");
199
+ if (durationMs === void 0) return void 0;
200
+ const components = {
201
+ inputTokens: values.get("input_tokens"),
202
+ outputTokens: values.get("output_tokens"),
203
+ cachedInputTokens: values.get("cached_input_tokens"),
204
+ cacheCreationTokens: values.get("cache_creation_tokens")
205
+ };
206
+ const hasComponents = Object.values(components).some((value) => value !== void 0);
207
+ const costUsd = values.get("cost_usd");
208
+ const usage = Object.fromEntries(Object.entries({
209
+ ...components,
210
+ reasoningTokens: values.get("reasoning_tokens"),
211
+ totalTokens: hasComponents ? void 0 : values.get("total_tokens"),
212
+ cost: costUsd === void 0 ? void 0 : { total: costUsd }
213
+ }).filter(([, value]) => value !== void 0));
214
+ return {
215
+ durationMs,
216
+ ...Object.keys(usage).length > 0 ? { usage } : void 0
217
+ };
218
+ }
342
219
  /** Replace one Run's metric rows and refresh its Conversation totals. */
343
220
  async function replaceConversationMetrics(executor, args) {
344
221
  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 +325,260 @@ async function bindProviderConversation(executor, args) {
448
325
  createdAt: /* @__PURE__ */ new Date()
449
326
  }).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
327
  }
328
+ // src/chat/identities/sql.ts
329
+ function dateFromMs$1(ms) {
330
+ return new Date(ms);
331
+ }
332
+ function tenantId$1(value) {
333
+ return value ?? "";
334
+ }
335
+ async function upsertUser(executor, args) {
336
+ const id = (await executor.db().insert(juniorUsers).values({
337
+ id: randomUUID(),
338
+ primaryEmail: args.email,
339
+ primaryEmailNormalized: args.emailNormalized,
340
+ displayName: args.displayName ?? null,
341
+ createdAt: dateFromMs$1(args.nowMs),
342
+ updatedAt: dateFromMs$1(args.nowMs)
343
+ }).onConflictDoUpdate({
344
+ target: juniorUsers.primaryEmailNormalized,
345
+ set: {
346
+ displayName: sql`coalesce(${juniorUsers.displayName}, excluded.display_name)`,
347
+ updatedAt: sql`excluded.updated_at`
348
+ }
349
+ }).returning({ id: juniorUsers.id }))[0]?.id;
350
+ if (!id) throw new Error("User identity upsert returned no row");
351
+ return id;
352
+ }
353
+ async function existingIdentity(executor, identity) {
354
+ 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];
355
+ }
356
+ async function upsertIdentityRecord(executor, identity, linkedUserId, nowMs) {
357
+ const emailNormalized = normalizeIdentityEmail(identity.email);
358
+ const email = emailNormalized ? identity.email?.trim() || emailNormalized : void 0;
359
+ const existing = await existingIdentity(executor, identity);
360
+ const userEmailNormalized = existing?.emailVerified && existing.emailNormalized ? existing.emailNormalized : identity.emailVerified ? emailNormalized : void 0;
361
+ const userEmail = existing?.emailVerified && existing.email ? existing.email : email ?? userEmailNormalized;
362
+ const verifiedUserId = identity.kind === "user" && userEmailNormalized ? await upsertUser(executor, {
363
+ email: userEmail ?? userEmailNormalized,
364
+ emailNormalized: userEmailNormalized,
365
+ nowMs,
366
+ ...existing?.displayName || identity.displayName ? { displayName: existing?.displayName ?? identity.displayName } : void 0
367
+ }) : void 0;
368
+ if (existing?.userId && verifiedUserId && existing.userId !== verifiedUserId) throw new Error("Identity verified email conflicts with linked user");
369
+ if (linkedUserId && verifiedUserId && linkedUserId !== verifiedUserId) throw new Error("Linked identity conflicts with verified email user");
370
+ if (existing?.userId && linkedUserId && existing.userId !== linkedUserId) throw new Error("Identity conflicts with linked user");
371
+ const userId = existing?.userId ?? linkedUserId ?? verifiedUserId;
372
+ const row = (await executor.db().insert(juniorIdentities).values({
373
+ id: randomUUID(),
374
+ userId: userId ?? null,
375
+ kind: identity.kind,
376
+ provider: identity.provider,
377
+ providerTenantId: tenantId$1(identity.providerTenantId),
378
+ providerSubjectId: identity.providerSubjectId,
379
+ displayName: identity.displayName ?? null,
380
+ handle: identity.handle ?? null,
381
+ email: email ?? null,
382
+ emailNormalized: emailNormalized ?? null,
383
+ emailVerified: Boolean(identity.emailVerified && emailNormalized),
384
+ avatarUrl: null,
385
+ metadata: identity.metadata ?? null,
386
+ createdAt: dateFromMs$1(nowMs),
387
+ updatedAt: dateFromMs$1(nowMs)
388
+ }).onConflictDoUpdate({
389
+ target: [
390
+ juniorIdentities.provider,
391
+ juniorIdentities.providerTenantId,
392
+ juniorIdentities.providerSubjectId
393
+ ],
394
+ set: {
395
+ kind: sql`excluded.kind`,
396
+ userId: sql`coalesce(${juniorIdentities.userId}, excluded.user_id)`,
397
+ displayName: linkedUserId ? sql`coalesce(excluded.display_name, ${juniorIdentities.displayName})` : sql`coalesce(${juniorIdentities.displayName}, excluded.display_name)`,
398
+ handle: linkedUserId ? sql`coalesce(excluded.handle, ${juniorIdentities.handle})` : sql`coalesce(${juniorIdentities.handle}, excluded.handle)`,
399
+ 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`,
400
+ 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`,
401
+ emailVerified: sql`${juniorIdentities.emailVerified} OR excluded.email_verified`,
402
+ avatarUrl: sql`coalesce(${juniorIdentities.avatarUrl}, excluded.avatar_url)`,
403
+ metadata: sql`coalesce(${juniorIdentities.metadata}, excluded.metadata_json)`,
404
+ updatedAt: sql`excluded.updated_at`
405
+ },
406
+ ...linkedUserId ? { setWhere: sql`${juniorIdentities.userId} IS NULL OR ${juniorIdentities.userId} = excluded.user_id` } : void 0
407
+ }).returning({
408
+ id: juniorIdentities.id,
409
+ userId: juniorIdentities.userId
410
+ }))[0];
411
+ if (!row) {
412
+ if (linkedUserId) throw new Error("Identity conflicts with linked user");
413
+ throw new Error("Identity upsert returned no row");
414
+ }
415
+ return {
416
+ id: row.id,
417
+ ...row.userId ? { userId: row.userId } : void 0
418
+ };
419
+ }
420
+ /** Read the saved timezone for one user. */
421
+ async function readUserTimezone(db, userId) {
422
+ return (await db.select({ timezone: juniorUsers.timezone }).from(juniorUsers).where(eq(juniorUsers.id, userId)).limit(1))[0]?.timezone ?? void 0;
423
+ }
424
+ /** Save the IANA timezone for one user. */
425
+ async function saveUserTimezone(db, userId, timezone, nowMs) {
426
+ await db.update(juniorUsers).set({
427
+ timezone,
428
+ updatedAt: dateFromMs$1(nowMs)
429
+ }).where(and(eq(juniorUsers.id, userId), sql`${juniorUsers.timezone} is distinct from ${timezone}`));
430
+ }
431
+ /** Persist one provider identity observation and link verified emails to users. */
432
+ async function upsertIdentity(executor, identity, nowMs = Date.now()) {
433
+ return await upsertIdentityRecord(executor, identity, void 0, nowMs);
434
+ }
435
+ /**
436
+ * Persist one provider identity after a trusted account-linking flow.
437
+ * Callers must use provider-verified account data, such as GitHub's authenticated
438
+ * user response, never identity claims inferred from content such as Git commits.
439
+ */
440
+ async function upsertLinkedIdentity(executor, userId, identity, nowMs = Date.now()) {
441
+ return await upsertIdentityRecord(executor, identity, userId, nowMs);
442
+ }
443
+ /**
444
+ * Find the one user whose primary email is in a provider-verified email list.
445
+ * Return undefined when no user or more than one user matches.
446
+ */
447
+ async function findUserByVerifiedEmails(executor, emails) {
448
+ const normalized = new Set(emails.map((email) => normalizeIdentityEmail(email) ?? ""));
449
+ normalized.delete("");
450
+ if (normalized.size === 0) return void 0;
451
+ const rows = await executor.db().select({
452
+ email: juniorUsers.primaryEmail,
453
+ userId: juniorUsers.id
454
+ }).from(juniorUsers).where(inArray(juniorUsers.primaryEmailNormalized, Array.from(normalized))).limit(2);
455
+ return rows.length === 1 ? rows[0] : void 0;
456
+ }
457
+ /** Remove one exact provider identity owned by a workspace Slack user. */
458
+ async function deleteProviderIdentityForSlackUser(executor, slackTeamId, slackUserId, provider, providerSubjectId) {
459
+ 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;
460
+ if (!userId) return;
461
+ await deleteProviderIdentityForUser(executor, userId, provider, providerSubjectId);
462
+ }
463
+ /** Remove one exact provider identity owned by a user. */
464
+ async function deleteProviderIdentityForUser(executor, userId, provider, providerSubjectId) {
465
+ await executor.db().delete(juniorIdentities).where(and(eq(juniorIdentities.userId, userId), eq(juniorIdentities.provider, provider), eq(juniorIdentities.providerTenantId, ""), eq(juniorIdentities.providerSubjectId, providerSubjectId)));
466
+ }
467
+ /**
468
+ * Save one Google sign-in. Link the Google identity to the user of its
469
+ * verified email, and set its sign-in time.
470
+ */
471
+ async function recordGoogleSignIn(executor, account, nowMs = Date.now()) {
472
+ const stored = await upsertIdentity(executor, {
473
+ kind: "user",
474
+ provider: GOOGLE_IDENTITY_PROVIDER,
475
+ providerSubjectId: account.subject,
476
+ email: account.email,
477
+ emailVerified: true,
478
+ displayName: account.displayName
479
+ }, nowMs);
480
+ await executor.db().update(juniorIdentities).set({ signedInAt: dateFromMs$1(nowMs) }).where(eq(juniorIdentities.id, stored.id));
481
+ return stored;
482
+ }
483
+ // src/chat/identities/actor.ts
484
+ /**
485
+ * Resolve runtime Actors to the Identity rows that Junior stores for them.
486
+ *
487
+ * The Conversation store, OAuth, the viewer, and message events use this
488
+ * module, so they agree on one Identity per person. The key for each Actor
489
+ * stays private here, so it can change without changing callers.
490
+ */
491
+ /** The one Identity for work that Junior does on its own. */
492
+ const SYSTEM_IDENTITY = {
493
+ kind: "system",
494
+ provider: "junior",
495
+ providerSubjectId: "system",
496
+ displayName: "System"
497
+ };
498
+ /**
499
+ * Return the Identity key for one Actor.
500
+ *
501
+ * Every SystemActor uses the one system Identity. `SystemActor.name` is
502
+ * provenance, not part of the key. A LocalActor uses the system Identity too,
503
+ * because the local runner has no per-person user id.
504
+ */
505
+ function identityKey(actor) {
506
+ switch (actor.platform) {
507
+ case "slack": return {
508
+ kind: "user",
509
+ provider: "slack",
510
+ providerTenantId: actor.teamId,
511
+ providerSubjectId: actor.userId,
512
+ ...actor.fullName ? { displayName: actor.fullName } : void 0,
513
+ ...actor.userName ? { handle: actor.userName } : void 0,
514
+ ...actor.email ? {
515
+ email: actor.email,
516
+ emailVerified: true
517
+ } : void 0,
518
+ metadata: { platform: "slack" }
519
+ };
520
+ case "web": {
521
+ const email = normalizeIdentityEmail(actor.email);
522
+ if (!email) return;
523
+ return {
524
+ kind: "user",
525
+ provider: "junior",
526
+ providerSubjectId: email,
527
+ email,
528
+ emailVerified: true,
529
+ ...actor.fullName ? { displayName: actor.fullName } : void 0,
530
+ metadata: { platform: "web" }
531
+ };
532
+ }
533
+ case "local":
534
+ case "system": return SYSTEM_IDENTITY;
535
+ }
536
+ }
537
+ /**
538
+ * Save the Identity for one Actor and return its id.
539
+ * Return undefined only for a web Actor without a verified email.
540
+ */
541
+ async function resolveActorIdentityId(executor, actor, nowMs = Date.now()) {
542
+ const key = identityKey(actor);
543
+ return key ? (await upsertIdentity(executor, key, nowMs)).id : void 0;
544
+ }
545
+ /** Find the stored Identity id for one Actor without writing. */
546
+ async function findActorIdentityId(db, actor) {
547
+ const key = identityKey(actor);
548
+ if (!key) return;
549
+ 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;
550
+ }
451
551
  // src/chat/conversations/sql/actor-identity.ts
452
- /** Build a durable identity observation from a stored conversation actor. */
453
- function identityFromActor(actor) {
552
+ /**
553
+ * Convert an Actor in the stored Conversation shape to a runtime Actor.
554
+ *
555
+ * TODO(dcramer): Remove after Conversations store the Identity id instead of
556
+ * StoredSlackActor.
557
+ */
558
+ function actorFromStoredActor(actor) {
454
559
  if (actor?.slackUserId) return {
455
- kind: "user",
456
- provider: "slack",
457
- providerTenantId: actor.teamId,
458
- providerSubjectId: actor.slackUserId,
459
- ...actor.fullName ? { displayName: actor.fullName } : void 0,
460
- ...actor.slackUserName ? { handle: actor.slackUserName } : void 0,
461
- ...actor.email ? {
462
- email: actor.email,
463
- emailVerified: true
464
- } : void 0,
465
- metadata: { platform: "slack" }
560
+ ...actor.email ? { email: actor.email } : void 0,
561
+ ...actor.fullName ? { fullName: actor.fullName } : void 0,
562
+ platform: "slack",
563
+ teamId: actor.teamId ?? "",
564
+ userId: actor.slackUserId,
565
+ ...actor.slackUserName ? { userName: actor.slackUserName } : void 0
466
566
  };
467
- const email = actor?.email?.trim().toLowerCase();
468
- if (!email) return;
567
+ if (!actor?.email) return;
469
568
  return {
470
- kind: "user",
471
- provider: "junior",
472
- providerSubjectId: email,
473
- email,
474
- emailVerified: true,
475
- ...actor?.fullName ? { displayName: actor.fullName } : void 0,
476
- metadata: { platform: "web" }
569
+ email: actor.email,
570
+ ...actor.fullName ? { fullName: actor.fullName } : void 0,
571
+ platform: "web",
572
+ userId: actor.email
477
573
  };
478
574
  }
479
575
  /**
480
- * Build the identity observation for one runtime actor with the same key the
481
- * Conversation write path stores. Local and system actors have no user
482
- * identity.
576
+ * Build the legacy system Identity for a Conversation that has no Actor.
577
+ *
578
+ * TODO(dcramer): Remove after every ingress supplies its Actor. These rows use
579
+ * one subject per Source (`scheduler`, `local-cli`, `event`). An Actor
580
+ * uses the one system Identity instead, so the backfill must merge them.
483
581
  */
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
582
  function systemIdentityFromSource(source) {
490
583
  if (source === "scheduler") return {
491
584
  kind: "system",
@@ -506,9 +599,15 @@ function systemIdentityFromSource(source) {
506
599
  displayName: "Event"
507
600
  };
508
601
  }
509
- /** Prefer the user actor identity, else a source-derived system identity. */
510
- function actorIdentityForConversation(conversation) {
511
- return identityFromActor(conversation.actor) ?? systemIdentityFromSource(conversation.source);
602
+ /**
603
+ * Save the Identity of a Conversation's actor and return its id. Use the
604
+ * legacy source system Identity when the Conversation has no actor.
605
+ */
606
+ async function resolveConversationActorIdentityId(executor, conversation) {
607
+ const actor = actorFromStoredActor(conversation.actor);
608
+ if (actor) return await resolveActorIdentityId(executor, actor, conversation.updatedAtMs);
609
+ const fallback = systemIdentityFromSource(conversation.source);
610
+ return fallback ? (await upsertIdentity(executor, fallback, conversation.updatedAtMs)).id : void 0;
512
611
  }
513
612
  /**
514
613
  * Reconstruct a stored actor with the linked user name and identity-scoped
@@ -780,6 +879,9 @@ var SqlStore = class {
780
879
  if (!row) return;
781
880
  return conversationFromRow(row);
782
881
  }
882
+ async getRunMetrics(args) {
883
+ return await readConversationRunMetrics(this.executor, args);
884
+ }
783
885
  async recordActivity(args) {
784
886
  const nowMs = args.nowMs ?? now();
785
887
  const activityAtMs = args.activityAtMs ?? nowMs;
@@ -824,7 +926,12 @@ var SqlStore = class {
824
926
  const existing = existingRow ? conversationFromRow(existingRow) : void 0;
825
927
  if (!existing && !args.destination) return;
826
928
  const incomingExecutionAt = args.execution.updatedAtMs ?? args.updatedAtMs;
827
- const incomingIsFresh = incomingExecutionAt >= (existing?.execution.updatedAtMs ?? existing?.updatedAtMs ?? 0);
929
+ const existingExecutionAt = existing?.execution.updatedAtMs ?? existing?.updatedAtMs ?? 0;
930
+ const latestMetricRunId = existing?.executionMetrics?.runId;
931
+ 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, {
932
+ conversationId: args.conversationId,
933
+ runId: args.execution.runId
934
+ }) !== void 0) && incomingExecutionAt >= existingExecutionAt;
828
935
  const execution = incomingIsFresh ? args.execution : existing?.execution ?? args.execution;
829
936
  const actor = mergeActor(existing?.actor, args.actor);
830
937
  const source = existing?.source ?? args.source;
@@ -924,8 +1031,7 @@ var SqlStore = class {
924
1031
  location: conversation.location,
925
1032
  sessionSource: conversation.sessionSource
926
1033
  });
927
- const actorIdentityObservation = actorIdentityForConversation(conversation);
928
- const actorIdentity = actorIdentityObservation ? await upsertIdentity(this.executor, actorIdentityObservation, conversation.updatedAtMs) : void 0;
1034
+ const actorIdentityId = await resolveConversationActorIdentityId(this.executor, conversation);
929
1035
  const rootConversationId = conversation.parentConversationId ? sql`(
930
1036
  select parent.root_conversation_id
931
1037
  from junior_conversations parent
@@ -939,7 +1045,7 @@ var SqlStore = class {
939
1045
  location: location ?? null,
940
1046
  destinationId: destinationId ?? null,
941
1047
  destination: null,
942
- actorIdentityId: actorIdentity?.id ?? null,
1048
+ actorIdentityId: actorIdentityId ?? null,
943
1049
  creatorIdentityId: null,
944
1050
  credentialSubjectIdentityId: null,
945
1051
  actor: null,
@@ -1061,7 +1167,6 @@ async function ensureConversationRow(executor, conversationId, atMs, options = {
1061
1167
  });
1062
1168
  }
1063
1169
  // src/chat/conversations/sql/event-actor.ts
1064
- const SLACK_USER_ID_PATTERN = /^[UW][A-Z0-9]{5,}$/;
1065
1170
  function isRecord(value) {
1066
1171
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
1067
1172
  }
@@ -1130,36 +1235,28 @@ async function resolveEventActorIdentityId(executor, args) {
1130
1235
  if (author.isBot === true) return;
1131
1236
  const userId = parseActorUserId(author.userId);
1132
1237
  if (!userId) return;
1133
- if (SLACK_USER_ID_PATTERN.test(userId)) {
1238
+ if (isSlackUserId(userId)) {
1134
1239
  const tenantId = await loadConversationSlackTenantId(executor, args.conversationId);
1135
- const observation = {
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, {
1240
+ if (!tenantId) return await findIdentityIdByProviderSubject(executor, {
1149
1241
  provider: "slack",
1150
1242
  providerSubjectId: userId
1151
- }) ?? void 0;
1243
+ });
1244
+ return await resolveActorIdentityId(executor, {
1245
+ platform: "slack",
1246
+ teamId: tenantId,
1247
+ userId,
1248
+ ...author.email ? { email: author.email } : void 0,
1249
+ ...author.fullName ? { fullName: author.fullName } : void 0,
1250
+ ...author.userName ? { userName: author.userName } : void 0
1251
+ }, args.nowMs);
1152
1252
  }
1153
- const email = typeof author.email === "string" && author.email.includes("@") ? author.email.trim().toLowerCase() : userId.includes("@") ? userId.trim().toLowerCase() : void 0;
1154
- if (email) return (await upsertIdentity(executor, {
1155
- kind: "user",
1156
- provider: "junior",
1157
- providerSubjectId: email,
1253
+ const email = typeof author.email === "string" && author.email.includes("@") ? author.email : userId.includes("@") ? userId : void 0;
1254
+ if (email) return await resolveActorIdentityId(executor, {
1255
+ platform: "web",
1256
+ userId,
1158
1257
  email,
1159
- emailVerified: true,
1160
- ...author.fullName ? { displayName: author.fullName } : void 0,
1161
- metadata: { platform: "web" }
1162
- }, args.nowMs)).id;
1258
+ ...author.fullName ? { fullName: author.fullName } : void 0
1259
+ }, args.nowMs);
1163
1260
  if (userId.startsWith("dashboard:")) return findJuniorIdentityIdByDashboardAuthorId(executor, userId);
1164
1261
  return findJuniorIdentityIdByEmail(executor, userId);
1165
1262
  }
@@ -1641,17 +1738,8 @@ async function listAdminUsersFromSql(db) {
1641
1738
  }
1642
1739
  /** Resolve the stored identity and linked user for one runtime actor. */
1643
1740
  async function readActorIdentityFromSql(db, actor) {
1644
- const key = identityFromRuntimeActor(actor);
1645
- if (!key) return void 0;
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
- };
1741
+ const identityId = await findActorIdentityId(db, actor);
1742
+ return identityId ? await readIdentityByIdFromSql(db, identityId) : void 0;
1655
1743
  }
1656
1744
  /** Resolve the stored identity and linked user for one runtime actor. */
1657
1745
  async function readActorIdentity(actor) {
@@ -1673,4 +1761,4 @@ async function readIdentityByIdFromSql(db, identityId) {
1673
1761
  async function readIdentityById(identityId) {
1674
1762
  return await readIdentityByIdFromSql(getDb(), identityId);
1675
1763
  }
1676
- export { resolveRootConversationId as A, parseDestination as B, bindProviderConversation as C, agentTurnTotalTokens as D, agentTurnCostUsd as E, recordGoogleSignIn as F, sameDestination as H, saveUserTimezone as I, upsertIdentity as L, deleteProviderIdentityForUser as M, findUserByVerifiedEmails as N, agentTurnUsageSchema as O, readUserTimezone as P, upsertLinkedIdentity as R, identityFromActor as S, addAgentTurnUsage as T, requireSlackDestination as V, loadConversationSlackTenantIds as _, resolveViewerUser as a, locationFromRow as b, closeDb as c, getConversationMessageSearchStore as d, getConversationStore as f, withConversationEventLock as g, createSqlConversationEventStore as h, readIdentityById as i, deleteProviderIdentityForSlackUser as j, hasAgentTurnUsage as k, db_exports as l, getSqlExecutor as m, listAdminUsersFromSql as n, setUserAdminFromSql as o, getDb as p, readActorIdentity as r, updateViewerDisplayName as s, findUserByEmail as t, getConversationEventStore as u, createSqlStore as v, parseSessionSource as w, startLocationThread as x, withConversationMutationLock as y, createSlackDestination as z };
1764
+ export { upsertLinkedIdentity as A, parseDestination as B, resolveActorIdentityId as C, readUserTimezone as D, findUserByVerifiedEmails as E, agentTurnTotalTokens as F, sameDestination as H, agentTurnUsageSchema as I, hasAgentTurnUsage as L, parseSessionSource as M, addAgentTurnUsage as N, recordGoogleSignIn as O, agentTurnCostUsd as P, resolveRootConversationId as R, startLocationThread as S, deleteProviderIdentityForUser as T, requireSlackDestination as V, withConversationEventLock as _, readIdentityById as a, withConversationMutationLock as b, updateViewerDisplayName as c, getConversationEventStore as d, getConversationMessageSearchStore as f, createSqlConversationEventStore as g, getSqlExecutor as h, readActorIdentity as i, bindProviderConversation as j, saveUserTimezone as k, closeDb as l, getDb as m, findUserByEmailFromSql as n, resolveViewerUser as o, getConversationStore as p, listAdminUsersFromSql as r, setUserAdminFromSql as s, findUserByEmail as t, db_exports as u, loadConversationSlackTenantIds as v, deleteProviderIdentityForSlackUser as w, locationFromRow as x, createSqlStore as y, createSlackDestination as z };