@sentry/junior 0.236.0 → 0.238.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{context-0Et9iVSB.js → actor-BTtLaeWy.js} +1 -96
- package/dist/admin-ZGlZ8lY6.js +33 -0
- package/dist/{agent-hooks-Da_H0jHv.js → agent-hooks-CxdSqU2u.js} +1426 -2965
- package/dist/{agent-runner-uP7QRc4c.js → agent-runner-ZS1LHtSR.js} +275 -143
- package/dist/api/schema.d.ts +4 -0
- package/dist/api/schema.js +4 -3
- package/dist/api.d.ts +1 -1
- package/dist/api.js +32 -25
- package/dist/{app-MEd7wbF0.d.ts → app-FB2hmHpZ.d.ts} +1 -1
- package/dist/app.d.ts +1 -1
- package/dist/app.js +27 -23
- package/dist/{automation-B9Kc1gcf.js → automation-CrQGQ2MT.js} +3 -2
- package/dist/{briefs-DrjBPQFy.js → briefs-B6IhUAgK.js} +4 -4
- package/dist/{catalog-runtime-C7339Cfm.js → catalog-runtime-BzNg07bb.js} +1 -1
- package/dist/{checkpoint-DaD7_Gsb.js → checkpoint-DWGyJg4c.js} +19 -10
- package/dist/cli/chat.js +13 -13
- package/dist/cli/check.js +3 -3
- package/dist/cli/main.js +6 -2
- package/dist/cli/plugins.js +5 -3
- package/dist/cli/run.d.ts +2 -1
- package/dist/cli/run.js +2 -1
- package/dist/cli/snapshot-create.js +3 -3
- package/dist/cli/upgrade.d.ts +1 -1
- package/dist/cli/upgrade.js +3 -3
- package/dist/{client-Ch9Hg9NU.js → client-CKs0rj3A.js} +2 -2
- package/dist/{config-JR9wke1W.js → config-DRw358bB.js} +2 -2
- package/dist/{conversation-9Fxa1Zbq.js → conversation-DwD-vquD.js} +3 -517
- package/dist/{conversation-privacy-au02dyZK.js → conversation-privacy-DAYxHp74.js} +1 -1
- package/dist/{credential-sync-BWTA3LnL.js → credential-sync-DmRbuyE4.js} +15 -14
- package/dist/{db-Ca9Nt5ci.d.ts → db-CWZP77Ex.d.ts} +1 -1
- package/dist/{executor-BW9XWgUQ.js → executor-DgPk8Lsx.js} +5 -2
- package/dist/experimental-Qn9rKLgx.js +50 -0
- package/dist/history-BLTUgFYO.js +518 -0
- package/dist/{images-BdG7zcNf.js → images-puyEuXoY.js} +3 -3
- package/dist/instrumentation.js +2 -2
- package/dist/{job-queue-BqXd0ZPV.js → job-queue-BL9stv_E.js} +1 -1
- package/dist/{logging-B4sKwmCG.js → logging-BNi_jyHb.js} +1 -1
- package/dist/nitro.d.ts +1 -1
- package/dist/nitro.js +3 -3
- package/dist/{oauth-authorization-BjRN99dc.js → oauth-authorization-Zv1oLB1J.js} +1 -1
- package/dist/{oauth-callback-server-B_1p98Mb.js → oauth-callback-server-BHtVbKvO.js} +2 -2
- package/dist/pending-auth-DulDjfwL.js +433 -0
- package/dist/{profile-BamYnmhO.js → profile-C2TScny6.js} +5 -7
- package/dist/{prompt-Ctwy6hSI.js → prompt-CMU-B4FA.js} +1 -1
- package/dist/{registry-CXNv9fDX.js → registry-D1RsbgDb.js} +111 -3
- package/dist/{resolve-C9ETkt4T.js → resolve-CuY3dhxy.js} +2 -2
- package/dist/{runner-DnBGVRnW.js → runner-BUkReLln.js} +10 -10
- package/dist/{sandbox-egress-signals-DwJ7Boi_.js → sandbox-egress-signals-etjG6qQX.js} +1 -1
- package/dist/{schema-CJiqDc5r.js → schema-CxWAN2zq.js} +2 -2
- package/dist/{skills-Dd8O9Aav.js → skills-BGhThSGR.js} +2 -2
- package/dist/{spawn-DFdRspSD.js → spawn-PtyOnoXt.js} +2 -2
- package/dist/{store-DoewL5mG.d.ts → store-BxxbxBlm.d.ts} +7 -0
- package/dist/{task-queue-DGQEOk1A.js → task-queue-Bi2uNATu.js} +3 -3
- package/dist/{task-runner-C5XCIDSF.js → task-runner-CDoZCo8g.js} +20 -10
- package/dist/{thread-state-D3E29wZk.js → thread-state-B-t88gH0.js} +6 -5
- package/dist/{turn-failure-response-TDoj0aZz.js → turn-failure-response-di4Jx90o.js} +10 -9
- package/dist/{turn-lifecycle-DOnU45p7.js → turn-lifecycle-ByhUHYjD.js} +1 -1
- package/dist/{turn-session-routing-DbpZZoin.js → turn-session-routing-QjNvsevm.js} +1 -1
- package/dist/{validation-B9lMbisx.js → validation-D3li76Ti.js} +2 -3
- package/dist/{version-CWkIGcLU.js → version-Chc6bVG5.js} +1 -1
- package/dist/version.js +1 -1
- package/dist/viewer-DsyM9kb9.js +1636 -0
- package/dist/{web-authorization-PPaoj2ns.js → web-authorization-Enb5lVYt.js} +6 -6
- package/dist/{web-input-CfNxXtHm.js → web-input-efQlcCis.js} +64 -502
- package/dist/{work-DNaoFrEe.js → work-CeiA_ZxD.js} +9 -10
- package/dist/{worker-xeJLxYwm.js → worker-BJb2hsUB.js} +5 -5
- package/migrations/0047_user_timezone.sql +1 -0
- package/migrations/0048_user_admin.sql +1 -0
- package/migrations/meta/0047_snapshot.json +3713 -0
- package/migrations/meta/0048_snapshot.json +3720 -0
- package/migrations/meta/_journal.json +14 -0
- package/package.json +4 -4
- package/dist/pending-auth-WqF4Gh8Z.js +0 -59
|
@@ -1,2134 +1,632 @@
|
|
|
1
1
|
import { t as __exportAll } from "./rolldown-runtime-B4FMCO8f.js";
|
|
2
|
-
import { a as embedTexts, r as completeObject } from "./client-
|
|
3
|
-
import {
|
|
2
|
+
import { a as embedTexts, r as completeObject } from "./client-CKs0rj3A.js";
|
|
3
|
+
import { I as isRecord$1, a as extractGenAiUsageSummary, d as logException, f as logInfo, p as logWarn, v as setSpanAttributes, w as withSpan, y as setSpanStatus } from "./logging-BNi_jyHb.js";
|
|
4
4
|
import { a as getClient$1 } from "./sentry-CgWZM_kV.js";
|
|
5
|
-
import { t as canExposeConversationPayload } from "./conversation-privacy-
|
|
6
|
-
import { i as getChatConfig, n as botConfig, o as getSlackBotToken } from "./config-
|
|
5
|
+
import { t as canExposeConversationPayload } from "./conversation-privacy-DAYxHp74.js";
|
|
6
|
+
import { i as getChatConfig, n as botConfig, o as getSlackBotToken } from "./config-DRw358bB.js";
|
|
7
7
|
import { r as defaultModelId } from "./model-profile-BwU8mqwD.js";
|
|
8
|
-
import {
|
|
8
|
+
import { d as parseSlackTeamId, g as parseSlackMessageTs, i as isActorUserId, o as parseActorUserId, u as parseSlackChannelReferenceId } from "./actor-BTtLaeWy.js";
|
|
9
|
+
import { c as getConversationEventStore, d as getDb, f as getSqlExecutor, h as createSqlStore, i as resolveViewerUser, r as readActorIdentity, w as hasAgentTurnUsage } from "./viewer-DsyM9kb9.js";
|
|
9
10
|
import { n as juniorDestinations } from "./destinations-BeBiJeEM.js";
|
|
10
|
-
import { C as juniorCodeChanges, D as juniorConversations,
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
11
|
+
import { C as juniorCodeChanges, D as juniorConversations, O as juniorIdentities, S as juniorConversationAnnotations, T as juniorAttachments, _ as juniorConversationBriefs, b as juniorConversationEvents, h as juniorConversationParticipants, i as juniorWorkspaces, k as juniorUsers, n as juniorSnapshots, r as juniorWorkspaceRepos, w as juniorCodeRepositories } from "./executor-DgPk8Lsx.js";
|
|
12
|
+
import { o as decodeStoredConversationEvent } from "./history-BLTUgFYO.js";
|
|
13
|
+
import { p as credentialSubjectSchema } from "./registry-D1RsbgDb.js";
|
|
14
|
+
import { B as parseBriefInput, C as conversationReportEventSchema, N as readMessageAttachments, W as readMessageCardRefs, z as conversationBriefSchema } from "./conversation-DwD-vquD.js";
|
|
15
|
+
import { C as getDefaultRedisStateAdapterFor, N as SANDBOX_WORKSPACE_ROOT, k as sleep$1, l as isSandboxMissingError, n as hash, v as getVercelSandboxCredentials, w as getStateAdapter, x as runNonInteractiveCommand } from "./profile-C2TScny6.js";
|
|
16
|
+
import { n as briefEntriesFromReportEvents, s as generateBrief, t as BRIEF_PROMPT } from "./prompt-CMU-B4FA.js";
|
|
14
17
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
15
18
|
import { z } from "zod";
|
|
16
19
|
import { createHash, createHmac, randomUUID, timingSafeEqual } from "node:crypto";
|
|
17
|
-
import { and, arrayContains, asc, desc, eq, exists, getTableColumns,
|
|
18
|
-
import { codeChangeInputSchema, conversationAnnotationInputSchema, conversationEventPresentationSchema, defineConversationEvent,
|
|
20
|
+
import { and, arrayContains, asc, desc, eq, exists, getTableColumns, gte, inArray, isNotNull, isNull, lt, lte, max, ne, notExists, or, sql } from "drizzle-orm";
|
|
21
|
+
import { codeChangeInputSchema, conversationAnnotationInputSchema, conversationEventPresentationSchema, defineConversationEvent, eventInputSchema, eventMatchSchema, eventMatches, eventTypeSchema, missingToolAnnotationKeys, normalizeEventIdentifier, objectAnnotationSchema, objectIconPath, objectTypeSchema, pluginEventsSchema, promptContextSchema, promptMessageSchema, stableEventMatchKey } from "@sentry/junior-plugin-api";
|
|
19
22
|
import { alias } from "drizzle-orm/pg-core";
|
|
20
23
|
import { WebClient } from "@slack/web-api";
|
|
21
24
|
import { Sandbox } from "@vercel/sandbox";
|
|
22
|
-
//#region src/chat/
|
|
23
|
-
/**
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
if (left.platform === "slack" && right.platform === "slack") return left.teamId === right.teamId && left.channelId === right.channelId;
|
|
49
|
-
return false;
|
|
50
|
-
}
|
|
51
|
-
//#endregion
|
|
52
|
-
//#region src/chat/identities/sql.ts
|
|
53
|
-
function dateFromMs$1(ms) {
|
|
54
|
-
return new Date(ms);
|
|
55
|
-
}
|
|
56
|
-
function tenantId$1(value) {
|
|
57
|
-
return value ?? "";
|
|
58
|
-
}
|
|
59
|
-
async function upsertUser(executor, args) {
|
|
60
|
-
const id = (await executor.db().insert(juniorUsers).values({
|
|
61
|
-
id: randomUUID(),
|
|
62
|
-
primaryEmail: args.email,
|
|
63
|
-
primaryEmailNormalized: args.emailNormalized,
|
|
64
|
-
displayName: args.displayName ?? null,
|
|
65
|
-
createdAt: dateFromMs$1(args.nowMs),
|
|
66
|
-
updatedAt: dateFromMs$1(args.nowMs)
|
|
67
|
-
}).onConflictDoUpdate({
|
|
68
|
-
target: juniorUsers.primaryEmailNormalized,
|
|
69
|
-
set: {
|
|
70
|
-
displayName: sql`coalesce(${juniorUsers.displayName}, excluded.display_name)`,
|
|
71
|
-
updatedAt: sql`excluded.updated_at`
|
|
72
|
-
}
|
|
73
|
-
}).returning({ id: juniorUsers.id }))[0]?.id;
|
|
74
|
-
if (!id) throw new Error("User identity upsert returned no row");
|
|
75
|
-
return id;
|
|
76
|
-
}
|
|
77
|
-
async function existingIdentity(executor, identity) {
|
|
78
|
-
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];
|
|
79
|
-
}
|
|
80
|
-
async function upsertIdentityRecord(executor, identity, linkedUserId, nowMs) {
|
|
81
|
-
const emailNormalized = normalizeIdentityEmail(identity.email);
|
|
82
|
-
const email = emailNormalized ? identity.email?.trim() || emailNormalized : void 0;
|
|
83
|
-
const existing = await existingIdentity(executor, identity);
|
|
84
|
-
const userEmailNormalized = existing?.emailVerified && existing.emailNormalized ? existing.emailNormalized : identity.emailVerified ? emailNormalized : void 0;
|
|
85
|
-
const userEmail = existing?.emailVerified && existing.email ? existing.email : email ?? userEmailNormalized;
|
|
86
|
-
const verifiedUserId = identity.kind === "user" && userEmailNormalized ? await upsertUser(executor, {
|
|
87
|
-
email: userEmail ?? userEmailNormalized,
|
|
88
|
-
emailNormalized: userEmailNormalized,
|
|
89
|
-
nowMs,
|
|
90
|
-
...existing?.displayName || identity.displayName ? { displayName: existing?.displayName ?? identity.displayName } : void 0
|
|
91
|
-
}) : void 0;
|
|
92
|
-
if (existing?.userId && verifiedUserId && existing.userId !== verifiedUserId) throw new Error("Identity verified email conflicts with linked user");
|
|
93
|
-
if (linkedUserId && verifiedUserId && linkedUserId !== verifiedUserId) throw new Error("Linked identity conflicts with verified email user");
|
|
94
|
-
if (existing?.userId && linkedUserId && existing.userId !== linkedUserId) throw new Error("Identity conflicts with linked user");
|
|
95
|
-
const userId = existing?.userId ?? linkedUserId ?? verifiedUserId;
|
|
96
|
-
const row = (await executor.db().insert(juniorIdentities).values({
|
|
97
|
-
id: randomUUID(),
|
|
98
|
-
userId: userId ?? null,
|
|
99
|
-
kind: identity.kind,
|
|
100
|
-
provider: identity.provider,
|
|
101
|
-
providerTenantId: tenantId$1(identity.providerTenantId),
|
|
102
|
-
providerSubjectId: identity.providerSubjectId,
|
|
103
|
-
displayName: identity.displayName ?? null,
|
|
104
|
-
handle: identity.handle ?? null,
|
|
105
|
-
email: email ?? null,
|
|
106
|
-
emailNormalized: emailNormalized ?? null,
|
|
107
|
-
emailVerified: Boolean(identity.emailVerified && emailNormalized),
|
|
108
|
-
avatarUrl: null,
|
|
109
|
-
metadata: identity.metadata ?? null,
|
|
110
|
-
createdAt: dateFromMs$1(nowMs),
|
|
111
|
-
updatedAt: dateFromMs$1(nowMs)
|
|
112
|
-
}).onConflictDoUpdate({
|
|
113
|
-
target: [
|
|
114
|
-
juniorIdentities.provider,
|
|
115
|
-
juniorIdentities.providerTenantId,
|
|
116
|
-
juniorIdentities.providerSubjectId
|
|
117
|
-
],
|
|
118
|
-
set: {
|
|
119
|
-
kind: sql`excluded.kind`,
|
|
120
|
-
userId: sql`coalesce(${juniorIdentities.userId}, excluded.user_id)`,
|
|
121
|
-
displayName: linkedUserId ? sql`coalesce(excluded.display_name, ${juniorIdentities.displayName})` : sql`coalesce(${juniorIdentities.displayName}, excluded.display_name)`,
|
|
122
|
-
handle: linkedUserId ? sql`coalesce(excluded.handle, ${juniorIdentities.handle})` : sql`coalesce(${juniorIdentities.handle}, excluded.handle)`,
|
|
123
|
-
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`,
|
|
124
|
-
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`,
|
|
125
|
-
emailVerified: sql`${juniorIdentities.emailVerified} OR excluded.email_verified`,
|
|
126
|
-
avatarUrl: sql`coalesce(${juniorIdentities.avatarUrl}, excluded.avatar_url)`,
|
|
127
|
-
metadata: sql`coalesce(${juniorIdentities.metadata}, excluded.metadata_json)`,
|
|
128
|
-
updatedAt: sql`excluded.updated_at`
|
|
129
|
-
},
|
|
130
|
-
...linkedUserId ? { setWhere: sql`${juniorIdentities.userId} IS NULL OR ${juniorIdentities.userId} = excluded.user_id` } : void 0
|
|
131
|
-
}).returning({
|
|
132
|
-
id: juniorIdentities.id,
|
|
133
|
-
userId: juniorIdentities.userId
|
|
134
|
-
}))[0];
|
|
135
|
-
if (!row) {
|
|
136
|
-
if (linkedUserId) throw new Error("Identity conflicts with linked user");
|
|
137
|
-
throw new Error("Identity upsert returned no row");
|
|
138
|
-
}
|
|
139
|
-
return {
|
|
140
|
-
id: row.id,
|
|
141
|
-
...row.userId ? { userId: row.userId } : void 0
|
|
142
|
-
};
|
|
143
|
-
}
|
|
144
|
-
/** Persist one provider identity observation and link verified emails to users. */
|
|
145
|
-
async function upsertIdentity(executor, identity, nowMs = Date.now()) {
|
|
146
|
-
return await upsertIdentityRecord(executor, identity, void 0, nowMs);
|
|
147
|
-
}
|
|
148
|
-
/**
|
|
149
|
-
* Persist one provider identity after a trusted account-linking flow.
|
|
150
|
-
* Callers must use provider-verified account data, such as GitHub's authenticated
|
|
151
|
-
* user response, never identity claims inferred from content such as Git commits.
|
|
152
|
-
*/
|
|
153
|
-
async function upsertLinkedIdentity(executor, userId, identity, nowMs = Date.now()) {
|
|
154
|
-
return await upsertIdentityRecord(executor, identity, userId, nowMs);
|
|
155
|
-
}
|
|
156
|
-
/** Remove one exact provider identity owned by a workspace Slack user. */
|
|
157
|
-
async function deleteProviderIdentityForSlackUser(executor, slackTeamId, slackUserId, provider, providerSubjectId) {
|
|
158
|
-
const userId = (await executor.db().select({ userId: juniorIdentities.userId }).from(juniorIdentities).where(and(eq(juniorIdentities.kind, "user"), eq(juniorIdentities.provider, "slack"), eq(juniorIdentities.providerTenantId, slackTeamId), eq(juniorIdentities.providerSubjectId, slackUserId))).limit(1))[0]?.userId;
|
|
159
|
-
if (!userId) return;
|
|
160
|
-
await executor.db().delete(juniorIdentities).where(and(eq(juniorIdentities.userId, userId), eq(juniorIdentities.provider, provider), eq(juniorIdentities.providerTenantId, ""), eq(juniorIdentities.providerSubjectId, providerSubjectId)));
|
|
161
|
-
}
|
|
162
|
-
//#endregion
|
|
163
|
-
//#region src/chat/conversations/sql/participants.ts
|
|
164
|
-
/** Resolve the root conversation id that owns personal-feed membership. */
|
|
165
|
-
async function resolveRootConversationId(executor, conversationId) {
|
|
166
|
-
const [row] = await executor.db().select({ rootConversationId: juniorConversations.rootConversationId }).from(juniorConversations).where(eq(juniorConversations.conversationId, conversationId)).limit(1);
|
|
167
|
-
return row?.rootConversationId ?? conversationId;
|
|
168
|
-
}
|
|
169
|
-
/**
|
|
170
|
-
* Upsert one linked user as a participant on a root conversation.
|
|
171
|
-
* No-ops when the identity is missing, unlinked, or not a user.
|
|
172
|
-
*/
|
|
173
|
-
async function recordConversationParticipant(executor, args) {
|
|
174
|
-
const [identity] = await executor.db().select({
|
|
175
|
-
kind: juniorIdentities.kind,
|
|
176
|
-
userId: juniorIdentities.userId
|
|
177
|
-
}).from(juniorIdentities).where(eq(juniorIdentities.id, args.actorIdentityId)).limit(1);
|
|
178
|
-
if (!identity?.userId || identity.kind !== "user") return;
|
|
179
|
-
const rootConversationId = await resolveRootConversationId(executor, args.conversationId);
|
|
180
|
-
const at = new Date(args.atMs);
|
|
181
|
-
await executor.db().insert(juniorConversationParticipants).values({
|
|
182
|
-
userId: identity.userId,
|
|
183
|
-
rootConversationId,
|
|
184
|
-
lastMessageAt: at
|
|
185
|
-
}).onConflictDoUpdate({
|
|
186
|
-
target: [juniorConversationParticipants.userId, juniorConversationParticipants.rootConversationId],
|
|
187
|
-
set: {
|
|
188
|
-
lastMessageAt: sql`greatest(${juniorConversationParticipants.lastMessageAt}, excluded.last_message_at)`,
|
|
189
|
-
...args.restoreArchive ? { archivedAt: null } : void 0
|
|
190
|
-
}
|
|
191
|
-
});
|
|
25
|
+
//#region src/chat/conversations/structured-events.ts
|
|
26
|
+
/** Reserved namespace for Junior-owned conversation metadata events. */
|
|
27
|
+
const JUNIOR_NATIVE_EVENT_NAMESPACE = "junior";
|
|
28
|
+
const authenticationEventContentSchema = z.object({
|
|
29
|
+
accountLabel: z.string().trim().min(1).max(120).optional(),
|
|
30
|
+
actorId: z.string().min(1),
|
|
31
|
+
authorizationId: z.string().min(1).optional(),
|
|
32
|
+
provider: z.string().regex(/^[a-z][a-z0-9-]*$/),
|
|
33
|
+
providerLabel: z.string().trim().min(1).max(80).optional()
|
|
34
|
+
}).strict();
|
|
35
|
+
const agentsInstructionsUpdatedContentSchema = z.object({
|
|
36
|
+
action: z.enum([
|
|
37
|
+
"loaded",
|
|
38
|
+
"replaced",
|
|
39
|
+
"cleared"
|
|
40
|
+
]),
|
|
41
|
+
directory: z.string().trim().min(1).max(500).optional(),
|
|
42
|
+
fingerprint: z.string().trim().min(1).max(128),
|
|
43
|
+
sources: z.array(z.object({
|
|
44
|
+
content: z.string().min(1).max(32768),
|
|
45
|
+
path: z.string().trim().min(1).max(500)
|
|
46
|
+
}).strict()).max(32),
|
|
47
|
+
textBytes: z.number().int().nonnegative().optional()
|
|
48
|
+
}).strict();
|
|
49
|
+
function providerTitle(content) {
|
|
50
|
+
return content.providerLabel?.trim() || content.provider;
|
|
192
51
|
}
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
"inputTokens",
|
|
198
|
-
"outputTokens",
|
|
199
|
-
"cachedInputTokens",
|
|
200
|
-
"cacheCreationTokens"
|
|
201
|
-
];
|
|
202
|
-
const COST_COMPONENT_FIELDS = [
|
|
203
|
-
"input",
|
|
204
|
-
"output",
|
|
205
|
-
"cacheRead",
|
|
206
|
-
"cacheWrite"
|
|
207
|
-
];
|
|
208
|
-
/** Return whether any token counter is present on a usage record. */
|
|
209
|
-
function hasAgentTurnUsage(usage) {
|
|
210
|
-
return Boolean(usage && (Object.entries(usage).some(([field, value]) => field !== "cost" && typeof value === "number" && Number.isFinite(value)) || Object.values(usage.cost ?? {}).some((value) => typeof value === "number" && Number.isFinite(value))));
|
|
52
|
+
function agentsInstructionsTitle(action) {
|
|
53
|
+
if (action === "loaded") return "Loaded AGENTS.md";
|
|
54
|
+
if (action === "replaced") return "Updated AGENTS.md";
|
|
55
|
+
return "Cleared AGENTS.md";
|
|
211
56
|
}
|
|
212
|
-
function
|
|
213
|
-
return
|
|
57
|
+
function sourceFilename(sourcePath) {
|
|
58
|
+
return sourcePath.split("/").at(-1) || sourcePath;
|
|
214
59
|
}
|
|
215
|
-
function
|
|
216
|
-
|
|
60
|
+
function formatBytes(bytes) {
|
|
61
|
+
if (bytes < 1024) return `${bytes} bytes`;
|
|
62
|
+
const kilobytes = bytes / 1024;
|
|
63
|
+
return `${Number.isInteger(kilobytes) ? kilobytes : kilobytes.toFixed(1)} KB`;
|
|
217
64
|
}
|
|
218
|
-
function
|
|
219
|
-
|
|
65
|
+
function agentsInstructionsPreview(content) {
|
|
66
|
+
const source = content.sources[0];
|
|
67
|
+
if (!source) return void 0;
|
|
68
|
+
const filename = sourceFilename(source.path);
|
|
69
|
+
const label = content.sources.length === 1 ? filename : `${content.sources.length} AGENTS.md files`;
|
|
70
|
+
return typeof content.textBytes === "number" ? `${label} · ${formatBytes(content.textBytes)}` : label;
|
|
220
71
|
}
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
72
|
+
/** Junior-owned account-link transcript event. */
|
|
73
|
+
const authenticationLinkedEvent = defineConversationEvent({
|
|
74
|
+
name: "authentication_linked",
|
|
75
|
+
version: 1,
|
|
76
|
+
schema: authenticationEventContentSchema,
|
|
77
|
+
renderEvent(event) {
|
|
78
|
+
const provider = providerTitle(event);
|
|
79
|
+
return {
|
|
80
|
+
icon: "link",
|
|
81
|
+
title: `${provider} connected`,
|
|
82
|
+
...event.accountLabel ? { preview: `Connected as \`${event.accountLabel}\`` } : void 0,
|
|
83
|
+
details: [{
|
|
84
|
+
title: `${provider} connected`,
|
|
85
|
+
...event.accountLabel ? { description: `Connected as \`${event.accountLabel}\`` } : void 0,
|
|
86
|
+
metadata: [event.provider]
|
|
87
|
+
}]
|
|
88
|
+
};
|
|
227
89
|
}
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
90
|
+
});
|
|
91
|
+
/** Junior-owned account-unlink transcript event. */
|
|
92
|
+
const authenticationUnlinkedEvent = defineConversationEvent({
|
|
93
|
+
name: "authentication_unlinked",
|
|
94
|
+
version: 1,
|
|
95
|
+
schema: authenticationEventContentSchema,
|
|
96
|
+
renderEvent(event) {
|
|
97
|
+
const provider = providerTitle(event);
|
|
98
|
+
return {
|
|
99
|
+
icon: "key",
|
|
100
|
+
title: `${provider} disconnected`,
|
|
101
|
+
details: [{
|
|
102
|
+
title: `${provider} disconnected`,
|
|
103
|
+
metadata: [event.provider]
|
|
104
|
+
}]
|
|
105
|
+
};
|
|
242
106
|
}
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
const reasoning = getFiniteCount(usage.reasoningTokens);
|
|
262
|
-
if (reasoning !== void 0) reasoningTokens = (reasoningTokens ?? 0) + reasoning;
|
|
263
|
-
if (usage.cost) for (const field of [...COST_COMPONENT_FIELDS, "total"]) {
|
|
264
|
-
const value = getFiniteCost(usage.cost[field]);
|
|
265
|
-
if (value === void 0) continue;
|
|
266
|
-
cost[field] = addCost(cost[field], value);
|
|
267
|
-
}
|
|
268
|
-
const usageComponentTotal = getComponentTotal(usage);
|
|
269
|
-
if (usageComponentTotal !== void 0) {
|
|
270
|
-
componentTotal = (componentTotal ?? 0) + usageComponentTotal;
|
|
271
|
-
for (const field of COMPONENT_USAGE_FIELDS) {
|
|
272
|
-
const value = getFiniteCount(usage[field]);
|
|
273
|
-
if (value === void 0) continue;
|
|
274
|
-
components[field] = (components[field] ?? 0) + value;
|
|
275
|
-
}
|
|
276
|
-
continue;
|
|
277
|
-
}
|
|
278
|
-
const totalTokens = getFiniteCount(usage.totalTokens);
|
|
279
|
-
if (totalTokens !== void 0) totalOnlyTokens = (totalOnlyTokens ?? 0) + totalTokens;
|
|
107
|
+
});
|
|
108
|
+
/** Junior-owned AGENTS.md bootstrap transition for the dashboard timeline. */
|
|
109
|
+
const agentsInstructionsUpdatedEvent = defineConversationEvent({
|
|
110
|
+
name: "agents_instructions_updated",
|
|
111
|
+
version: 1,
|
|
112
|
+
schema: agentsInstructionsUpdatedContentSchema,
|
|
113
|
+
renderEvent(event) {
|
|
114
|
+
const title = agentsInstructionsTitle(event.action);
|
|
115
|
+
const preview = agentsInstructionsPreview(event);
|
|
116
|
+
return {
|
|
117
|
+
icon: "brain",
|
|
118
|
+
title,
|
|
119
|
+
...preview ? { preview } : void 0,
|
|
120
|
+
...event.sources.length > 0 ? { details: event.sources.map((source) => ({
|
|
121
|
+
title: sourceFilename(source.path),
|
|
122
|
+
content: source.content
|
|
123
|
+
})) } : void 0
|
|
124
|
+
};
|
|
280
125
|
}
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
};
|
|
291
|
-
return hasAgentTurnUsage(result) ? result : void 0;
|
|
292
|
-
}
|
|
293
|
-
//#endregion
|
|
294
|
-
//#region src/chat/conversations/metrics.ts
|
|
295
|
-
function finite(value) {
|
|
296
|
-
return value !== void 0 && Number.isFinite(value) && value >= 0 ? value : void 0;
|
|
297
|
-
}
|
|
298
|
-
/** Convert one Run summary to metric rows. */
|
|
299
|
-
function conversationMetricValues(args) {
|
|
300
|
-
const usage = args.usage;
|
|
301
|
-
return {
|
|
302
|
-
duration_ms: Math.max(0, args.durationMs),
|
|
303
|
-
total_tokens: agentTurnTotalTokens(usage),
|
|
304
|
-
input_tokens: finite(usage?.inputTokens),
|
|
305
|
-
output_tokens: finite(usage?.outputTokens),
|
|
306
|
-
cached_input_tokens: finite(usage?.cachedInputTokens),
|
|
307
|
-
cache_creation_tokens: finite(usage?.cacheCreationTokens),
|
|
308
|
-
reasoning_tokens: finite(usage?.reasoningTokens),
|
|
309
|
-
cost_usd: agentTurnCostUsd(usage)
|
|
310
|
-
};
|
|
311
|
-
}
|
|
312
|
-
/** Replace one Run's metric rows and refresh its Conversation totals. */
|
|
313
|
-
async function replaceConversationMetrics(executor, args) {
|
|
314
|
-
const [current] = await executor.db().select({ updatedAt: juniorConversationMetrics.updatedAt }).from(juniorConversationMetrics).where(and(eq(juniorConversationMetrics.conversationId, args.conversationId), eq(juniorConversationMetrics.runId, args.runId))).limit(1);
|
|
315
|
-
if (current && current.updatedAt.getTime() > args.updatedAtMs) return;
|
|
316
|
-
await executor.db().delete(juniorConversationMetrics).where(and(eq(juniorConversationMetrics.conversationId, args.conversationId), eq(juniorConversationMetrics.runId, args.runId)));
|
|
317
|
-
const metricValues = conversationMetricValues({
|
|
318
|
-
durationMs: args.durationMs,
|
|
319
|
-
usage: args.usage
|
|
320
|
-
});
|
|
321
|
-
const values = CONVERSATION_METRICS.flatMap((metric) => {
|
|
322
|
-
const value = metricValues[metric];
|
|
323
|
-
return value === void 0 ? [] : [{
|
|
324
|
-
conversationId: args.conversationId,
|
|
325
|
-
runId: args.runId,
|
|
326
|
-
metric,
|
|
327
|
-
value,
|
|
328
|
-
occurredAt: new Date(args.occurredAtMs),
|
|
329
|
-
updatedAt: new Date(args.updatedAtMs)
|
|
330
|
-
}];
|
|
331
|
-
});
|
|
332
|
-
if (values.length > 0) await executor.db().insert(juniorConversationMetrics).values(values);
|
|
333
|
-
const metric = (name) => sql`(
|
|
334
|
-
select sum(${juniorConversationMetrics.value})
|
|
335
|
-
from ${juniorConversationMetrics}
|
|
336
|
-
where ${juniorConversationMetrics.conversationId} = ${args.conversationId}
|
|
337
|
-
and ${juniorConversationMetrics.metric} = ${name}
|
|
338
|
-
)`;
|
|
339
|
-
const totalTokens = metric("total_tokens");
|
|
340
|
-
const componentTokens = sql`
|
|
341
|
-
coalesce(${metric("input_tokens")}, 0)
|
|
342
|
-
+ coalesce(${metric("output_tokens")}, 0)
|
|
343
|
-
+ coalesce(${metric("cached_input_tokens")}, 0)
|
|
344
|
-
+ coalesce(${metric("cache_creation_tokens")}, 0)
|
|
345
|
-
`;
|
|
346
|
-
const hasComponents = sql`
|
|
347
|
-
${metric("input_tokens")} is not null
|
|
348
|
-
or ${metric("output_tokens")} is not null
|
|
349
|
-
or ${metric("cached_input_tokens")} is not null
|
|
350
|
-
or ${metric("cache_creation_tokens")} is not null
|
|
351
|
-
`;
|
|
352
|
-
const cost = sql`case when ${metric("cost_usd")} is null then null
|
|
353
|
-
else jsonb_build_object('total', ${metric("cost_usd")}) end`;
|
|
354
|
-
await executor.db().update(juniorConversations).set({
|
|
355
|
-
durationMs: sql`coalesce(${metric("duration_ms")}, 0)`,
|
|
356
|
-
usage: sql`case
|
|
357
|
-
when ${totalTokens} is not null
|
|
358
|
-
and (${hasComponents})
|
|
359
|
-
and ${totalTokens} <> ${componentTokens}
|
|
360
|
-
then jsonb_strip_nulls(jsonb_build_object(
|
|
361
|
-
'totalTokens', ${totalTokens},
|
|
362
|
-
'reasoningTokens', ${metric("reasoning_tokens")},
|
|
363
|
-
'cost', ${cost}
|
|
364
|
-
))
|
|
365
|
-
else jsonb_strip_nulls(jsonb_build_object(
|
|
366
|
-
'totalTokens', ${totalTokens},
|
|
367
|
-
'inputTokens', ${metric("input_tokens")},
|
|
368
|
-
'outputTokens', ${metric("output_tokens")},
|
|
369
|
-
'cachedInputTokens', ${metric("cached_input_tokens")},
|
|
370
|
-
'cacheCreationTokens', ${metric("cache_creation_tokens")},
|
|
371
|
-
'reasoningTokens', ${metric("reasoning_tokens")},
|
|
372
|
-
'cost', ${cost}
|
|
373
|
-
))
|
|
374
|
-
end`
|
|
375
|
-
}).where(eq(juniorConversations.conversationId, args.conversationId));
|
|
376
|
-
}
|
|
377
|
-
//#endregion
|
|
378
|
-
//#region src/chat/source.ts
|
|
379
|
-
/**
|
|
380
|
-
* Normalize a Turn Source into legacy provider fields stored on a Conversation.
|
|
381
|
-
* System Sources do not contain a Conversation Location.
|
|
382
|
-
*/
|
|
383
|
-
function normalizeSessionSource(value) {
|
|
384
|
-
if (!value) return;
|
|
385
|
-
if (value.kind === "local") return {
|
|
386
|
-
kind: "local",
|
|
387
|
-
visibility: value.visibility,
|
|
388
|
-
conversationId: value.conversationId
|
|
389
|
-
};
|
|
390
|
-
if (value.kind === "web") return {
|
|
391
|
-
kind: "web",
|
|
392
|
-
visibility: value.visibility,
|
|
393
|
-
conversationId: value.conversationId
|
|
394
|
-
};
|
|
395
|
-
if (value.kind === "event" || value.kind === "scheduled_automation" || value.kind === "event_automation" || value.kind === "plugin_dispatch" || value.kind === "agent_invocation") return;
|
|
396
|
-
const threadTs = value.threadTs?.trim();
|
|
397
|
-
return {
|
|
398
|
-
kind: "slack",
|
|
399
|
-
visibility: value.visibility,
|
|
400
|
-
teamId: value.teamId,
|
|
401
|
-
channelId: value.channelId,
|
|
402
|
-
...threadTs ? { threadTs } : void 0
|
|
403
|
-
};
|
|
404
|
-
}
|
|
405
|
-
/** Parse a serialized Source into legacy provider fields stored on a Conversation. */
|
|
406
|
-
function parseSessionSource(value) {
|
|
407
|
-
const parsed = sourceSchema.safeParse(value);
|
|
408
|
-
return parsed.success ? normalizeSessionSource(parsed.data) : void 0;
|
|
126
|
+
});
|
|
127
|
+
const NATIVE_EVENT_DEFINITIONS = [
|
|
128
|
+
authenticationLinkedEvent,
|
|
129
|
+
authenticationUnlinkedEvent,
|
|
130
|
+
agentsInstructionsUpdatedEvent
|
|
131
|
+
];
|
|
132
|
+
/** Resolve a registered Junior-native conversation event definition. */
|
|
133
|
+
function getJuniorNativeEventDefinition(args) {
|
|
134
|
+
return NATIVE_EVENT_DEFINITIONS.find((definition) => definition.eventName === args.name && definition.version === args.version);
|
|
409
135
|
}
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
async function bindProviderConversation(executor, args) {
|
|
418
|
-
if (((await executor.db().insert(juniorConversationBindings).values({
|
|
419
|
-
...args,
|
|
420
|
-
createdAt: /* @__PURE__ */ new Date()
|
|
421
|
-
}).onConflictDoNothing().returning({ conversationId: juniorConversationBindings.conversationId }))[0]?.conversationId ?? await getConversationIdByProviderConversation(executor, args)) !== args.conversationId) throw new Error("Provider conversation is already bound to another conversation");
|
|
136
|
+
/** Render one Junior-native event through its active definition. */
|
|
137
|
+
function renderJuniorNativeConversationEvent(args) {
|
|
138
|
+
if (args.namespace !== "junior") return void 0;
|
|
139
|
+
const definition = getJuniorNativeEventDefinition(args);
|
|
140
|
+
if (!definition) return void 0;
|
|
141
|
+
const presentation = definition.renderEvent(definition.parse(args.content));
|
|
142
|
+
return presentation === void 0 ? void 0 : conversationEventPresentationSchema.parse(presentation);
|
|
422
143
|
}
|
|
423
144
|
//#endregion
|
|
424
|
-
//#region src/chat/
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
if (
|
|
428
|
-
kind: "user",
|
|
429
|
-
provider: "slack",
|
|
430
|
-
providerTenantId: actor.teamId,
|
|
431
|
-
providerSubjectId: actor.slackUserId,
|
|
432
|
-
...actor.fullName ? { displayName: actor.fullName } : void 0,
|
|
433
|
-
...actor.slackUserName ? { handle: actor.slackUserName } : void 0,
|
|
434
|
-
...actor.email ? {
|
|
435
|
-
email: actor.email,
|
|
436
|
-
emailVerified: true
|
|
437
|
-
} : void 0,
|
|
438
|
-
metadata: { platform: "slack" }
|
|
439
|
-
};
|
|
440
|
-
const email = actor?.email?.trim().toLowerCase();
|
|
441
|
-
if (!email) return;
|
|
145
|
+
//#region src/chat/plugins/annotations.ts
|
|
146
|
+
function annotationFromRow(row) {
|
|
147
|
+
const parsed = conversationAnnotationInputSchema.safeParse(row.annotation);
|
|
148
|
+
if (!parsed.success) return void 0;
|
|
442
149
|
return {
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
emailVerified: true,
|
|
448
|
-
...actor?.fullName ? { displayName: actor.fullName } : void 0,
|
|
449
|
-
metadata: { platform: "web" }
|
|
450
|
-
};
|
|
451
|
-
}
|
|
452
|
-
/** Build a system identity when a conversation has no user actor. */
|
|
453
|
-
function systemIdentityFromSource(source) {
|
|
454
|
-
if (source === "scheduler") return {
|
|
455
|
-
kind: "system",
|
|
456
|
-
provider: "junior",
|
|
457
|
-
providerSubjectId: "scheduler",
|
|
458
|
-
displayName: "Junior Scheduler"
|
|
459
|
-
};
|
|
460
|
-
if (source === "local") return {
|
|
461
|
-
kind: "system",
|
|
462
|
-
provider: "junior",
|
|
463
|
-
providerSubjectId: "local-cli",
|
|
464
|
-
displayName: "Local CLI"
|
|
465
|
-
};
|
|
466
|
-
if (source === "event") return {
|
|
467
|
-
kind: "system",
|
|
468
|
-
provider: "junior",
|
|
469
|
-
providerSubjectId: "event",
|
|
470
|
-
displayName: "Event"
|
|
471
|
-
};
|
|
472
|
-
}
|
|
473
|
-
/** Prefer the user actor identity, else a source-derived system identity. */
|
|
474
|
-
function actorIdentityForConversation(conversation) {
|
|
475
|
-
return identityFromActor(conversation.actor) ?? systemIdentityFromSource(conversation.source);
|
|
476
|
-
}
|
|
477
|
-
/**
|
|
478
|
-
* Reconstruct a stored actor with the linked user name and identity-scoped
|
|
479
|
-
* provider fields.
|
|
480
|
-
*/
|
|
481
|
-
function actorFromIdentityRow(identity, userDisplayName) {
|
|
482
|
-
if (!identity) return;
|
|
483
|
-
const fullName = userDisplayName?.trim() ? userDisplayName : identity.displayName;
|
|
484
|
-
const email = identity.emailNormalized ?? identity.email ?? void 0;
|
|
485
|
-
if (identity.provider === "slack") return {
|
|
486
|
-
...email ? { email } : void 0,
|
|
487
|
-
...fullName ? { fullName } : void 0,
|
|
488
|
-
platform: "slack",
|
|
489
|
-
slackUserId: identity.providerSubjectId,
|
|
490
|
-
...identity.handle ? { slackUserName: identity.handle } : void 0,
|
|
491
|
-
...identity.providerTenantId ? { teamId: identity.providerTenantId } : void 0
|
|
492
|
-
};
|
|
493
|
-
if (identity.provider === "junior" && email) return {
|
|
494
|
-
email,
|
|
495
|
-
...fullName ? { fullName } : void 0
|
|
150
|
+
...parsed.data,
|
|
151
|
+
plugin: row.plugin,
|
|
152
|
+
createdAt: row.createdAt.toISOString(),
|
|
153
|
+
updatedAt: row.updatedAt.toISOString()
|
|
496
154
|
};
|
|
497
155
|
}
|
|
498
|
-
|
|
499
|
-
function mergeActor(current, next) {
|
|
500
|
-
if (!current) return next;
|
|
501
|
-
if (!next) return current;
|
|
502
|
-
if (current.slackUserId && next.slackUserId && current.slackUserId !== next.slackUserId) return current;
|
|
156
|
+
function createPluginAnnotations(args) {
|
|
503
157
|
return {
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
158
|
+
async upsert(input) {
|
|
159
|
+
const annotation = conversationAnnotationInputSchema.parse(input);
|
|
160
|
+
const now = /* @__PURE__ */ new Date();
|
|
161
|
+
await args.db.insert(juniorConversationAnnotations).values({
|
|
162
|
+
annotation,
|
|
163
|
+
conversationId: args.conversationId,
|
|
164
|
+
createdAt: now,
|
|
165
|
+
key: annotation.key,
|
|
166
|
+
kind: annotation.kind,
|
|
167
|
+
plugin: args.plugin,
|
|
168
|
+
updatedAt: now
|
|
169
|
+
}).onConflictDoUpdate({
|
|
170
|
+
target: [
|
|
171
|
+
juniorConversationAnnotations.conversationId,
|
|
172
|
+
juniorConversationAnnotations.plugin,
|
|
173
|
+
juniorConversationAnnotations.kind,
|
|
174
|
+
juniorConversationAnnotations.key
|
|
175
|
+
],
|
|
176
|
+
set: {
|
|
177
|
+
annotation,
|
|
178
|
+
updatedAt: now
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
},
|
|
182
|
+
async remove(kind, key) {
|
|
183
|
+
await args.db.delete(juniorConversationAnnotations).where(and(eq(juniorConversationAnnotations.conversationId, args.conversationId), eq(juniorConversationAnnotations.plugin, args.plugin), eq(juniorConversationAnnotations.kind, kind), eq(juniorConversationAnnotations.key, key)));
|
|
184
|
+
},
|
|
185
|
+
async list() {
|
|
186
|
+
return (await listConversationAnnotations(args.db, args.conversationId)).filter((a) => a.plugin === args.plugin);
|
|
187
|
+
}
|
|
511
188
|
};
|
|
512
189
|
}
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
function locationForWrite(args) {
|
|
517
|
-
const location = args.location ? locationSchema.parse(args.location) : void 0;
|
|
518
|
-
const destination = args.destination?.platform === "slack" ? args.destination : void 0;
|
|
519
|
-
const source = args.sessionSource?.kind === "slack" ? args.sessionSource : void 0;
|
|
520
|
-
if (location) {
|
|
521
|
-
if (destination && (location.id !== args.destinationId || location.teamId !== destination.teamId || location.channelId !== destination.channelId) || source && (location.teamId !== source.teamId || location.channelId !== source.channelId)) throw new Error("Conversation Location changed");
|
|
522
|
-
const threadTs = location.threadTs ?? destination?.threadTs ?? source?.threadTs;
|
|
523
|
-
return threadTs === location.threadTs ? location : {
|
|
524
|
-
...location,
|
|
525
|
-
threadTs
|
|
526
|
-
};
|
|
527
|
-
}
|
|
528
|
-
if (destination && source && (destination.teamId !== source.teamId || destination.channelId !== source.channelId)) throw new Error("Conversation Location does not match its session Source");
|
|
529
|
-
if (!destination || !args.destinationId) return;
|
|
530
|
-
const threadTs = destination.threadTs ?? source?.threadTs;
|
|
531
|
-
return locationSchema.parse({
|
|
532
|
-
id: args.destinationId,
|
|
533
|
-
provider: "slack",
|
|
534
|
-
teamId: destination.teamId,
|
|
535
|
-
channelId: destination.channelId,
|
|
536
|
-
...threadTs ? { threadTs } : void 0
|
|
537
|
-
});
|
|
190
|
+
/** Load stored annotations for one conversation, newest update first. */
|
|
191
|
+
async function listConversationAnnotations(db, conversationId) {
|
|
192
|
+
return (await listConversationAnnotationsById(db, [conversationId])).get(conversationId) ?? [];
|
|
538
193
|
}
|
|
539
|
-
/**
|
|
540
|
-
function
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
194
|
+
/** Load stored annotations for a bounded set of conversations. */
|
|
195
|
+
async function listConversationAnnotationsById(db, conversationIds) {
|
|
196
|
+
const byConversation = /* @__PURE__ */ new Map();
|
|
197
|
+
if (conversationIds.length === 0) return byConversation;
|
|
198
|
+
const rows = await db.select().from(juniorConversationAnnotations).where(inArray(juniorConversationAnnotations.conversationId, [...conversationIds])).orderBy(desc(juniorConversationAnnotations.updatedAt));
|
|
199
|
+
for (const row of rows) {
|
|
200
|
+
const annotation = annotationFromRow(row);
|
|
201
|
+
if (!annotation) continue;
|
|
202
|
+
const current = byConversation.get(row.conversationId);
|
|
203
|
+
if (current) current.push(annotation);
|
|
204
|
+
else byConversation.set(row.conversationId, [annotation]);
|
|
548
205
|
}
|
|
549
|
-
|
|
550
|
-
if (row.provider !== "slack") throw new Error("Conversation Location provider is not supported");
|
|
551
|
-
const slackSource = sessionSource?.kind === "slack" && sessionSource.teamId === row.providerTenantId && sessionSource.channelId === row.providerDestinationId ? sessionSource : void 0;
|
|
552
|
-
return locationSchema.parse({
|
|
553
|
-
id: row.id,
|
|
554
|
-
provider: "slack",
|
|
555
|
-
teamId: row.providerTenantId,
|
|
556
|
-
channelId: row.providerDestinationId,
|
|
557
|
-
...slackSource?.threadTs ? { threadTs: slackSource.threadTs } : void 0
|
|
558
|
-
});
|
|
559
|
-
}
|
|
560
|
-
/** Read confirmed visibility from one linked Location row. */
|
|
561
|
-
function visibilityFromLocationRow(row) {
|
|
562
|
-
if (!row) return;
|
|
563
|
-
if (row.visibility === "public" || row.visibility === "private") return row.visibility;
|
|
206
|
+
return byConversation;
|
|
564
207
|
}
|
|
565
208
|
//#endregion
|
|
566
|
-
//#region src/chat/conversations/
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
return ms;
|
|
586
|
-
}
|
|
587
|
-
function tenantId(value) {
|
|
588
|
-
return value ?? "";
|
|
589
|
-
}
|
|
590
|
-
function sourceFromValue(value) {
|
|
591
|
-
if (value === "api" || value === "internal" || value === "local" || value === "plugin" || value === "event" || value === "scheduler" || value === "slack" || value === "web") return value;
|
|
592
|
-
}
|
|
593
|
-
function originTypeFromSource(source) {
|
|
594
|
-
return source;
|
|
595
|
-
}
|
|
596
|
-
function localWorkspaceFromConversationId(conversationId) {
|
|
597
|
-
return /^local:([^:]+):/.exec(conversationId)?.[1];
|
|
598
|
-
}
|
|
599
|
-
function destinationUpsertFromDestination(args) {
|
|
600
|
-
const { destination } = args;
|
|
601
|
-
if (!destination) return;
|
|
602
|
-
if (destination.platform === "slack") {
|
|
603
|
-
const channelId = destination.channelId;
|
|
604
|
-
return {
|
|
605
|
-
kind: channelId.startsWith("D") ? "dm" : channelId.startsWith("G") ? "group" : "channel",
|
|
606
|
-
provider: "slack",
|
|
607
|
-
providerTenantId: destination.teamId,
|
|
608
|
-
providerDestinationId: channelId,
|
|
609
|
-
refreshVisibility: args.visibility !== void 0,
|
|
610
|
-
visibility: args.visibility ?? "unknown",
|
|
611
|
-
...args.channelName ? { displayName: args.channelName } : void 0,
|
|
612
|
-
metadata: { platform: "slack" }
|
|
613
|
-
};
|
|
614
|
-
}
|
|
615
|
-
return {
|
|
616
|
-
kind: "local_conversation",
|
|
617
|
-
provider: "local",
|
|
618
|
-
providerTenantId: localWorkspaceFromConversationId(destination.conversationId) ?? localWorkspaceFromConversationId(args.conversationId ?? ""),
|
|
619
|
-
providerDestinationId: destination.conversationId,
|
|
620
|
-
refreshVisibility: args.visibility !== void 0,
|
|
621
|
-
visibility: args.visibility ?? "direct",
|
|
622
|
-
metadata: { platform: "local" }
|
|
623
|
-
};
|
|
209
|
+
//#region src/chat/conversations/annotation-results.ts
|
|
210
|
+
/** Save object annotations and retain returned facts in the tool result. */
|
|
211
|
+
async function saveObjectAnnotations(conversationId, plugin, value) {
|
|
212
|
+
const annotations = objectAnnotationSchema.array().parse(value);
|
|
213
|
+
await getDb().transaction(async (db) => {
|
|
214
|
+
const store = createPluginAnnotations({
|
|
215
|
+
conversationId,
|
|
216
|
+
plugin,
|
|
217
|
+
db
|
|
218
|
+
});
|
|
219
|
+
for (const annotation of annotations) {
|
|
220
|
+
await store.upsert(annotation);
|
|
221
|
+
await store.remove("resource_link", annotation.key);
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
return annotations.map((annotation) => ({
|
|
225
|
+
...annotation,
|
|
226
|
+
plugin
|
|
227
|
+
}));
|
|
624
228
|
}
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
if (
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
platform: "slack",
|
|
636
|
-
teamId: destination.providerTenantId,
|
|
637
|
-
channelId: destination.providerDestinationId
|
|
638
|
-
} : destination?.provider === "local" ? {
|
|
639
|
-
platform: "local",
|
|
640
|
-
conversationId: destination.providerDestinationId
|
|
641
|
-
} : void 0);
|
|
642
|
-
}
|
|
643
|
-
/** Decode one SQL row and reject invalid durable conversation records. */
|
|
644
|
-
function conversationFromRow(readRow) {
|
|
645
|
-
const row = readRow.conversation;
|
|
646
|
-
const visibility = visibilityFromLocationRow(readRow.destination);
|
|
647
|
-
if (row.schemaVersion !== 1) throw new Error("Conversation record schema version is invalid");
|
|
648
|
-
if (row.destination !== null && readRow.destination === null) throw new Error("Conversation legacy destination is not migrated");
|
|
649
|
-
if (row.actor !== null && readRow.actorIdentity === null) throw new Error("Conversation legacy actor is not migrated");
|
|
650
|
-
const destination = destinationFromRow(readRow.destination);
|
|
651
|
-
const actor = actorFromIdentityRow(readRow.actorIdentity, readRow.actorUserDisplayName);
|
|
652
|
-
if (readRow.destination !== null && !destination) throw new Error("Conversation record destination is invalid");
|
|
653
|
-
const source = row.source === void 0 || row.source === null ? void 0 : sourceFromValue(row.source);
|
|
654
|
-
if (row.source !== void 0 && row.source !== null && !source) throw new Error("Conversation record source is invalid");
|
|
655
|
-
const sessionSource = row.sessionSource === void 0 || row.sessionSource === null ? void 0 : parseSessionSource(row.sessionSource);
|
|
656
|
-
if (row.sessionSource !== void 0 && row.sessionSource !== null && !sessionSource) throw new Error("Conversation record session source is invalid");
|
|
657
|
-
const execution = {
|
|
658
|
-
status: executionStatusFromValue(row.executionStatus),
|
|
659
|
-
lastCheckpointAtMs: msFromDate(row.lastCheckpointAt),
|
|
660
|
-
lastEnqueuedAtMs: msFromDate(row.lastEnqueuedAt),
|
|
661
|
-
...row.runId ? { runId: row.runId } : void 0,
|
|
662
|
-
updatedAtMs: msFromDate(row.executionUpdatedAt) ?? requiredMsFromDate(row.updatedAt)
|
|
229
|
+
/** Save annotations from a successful plugin tool result and attach their cards. */
|
|
230
|
+
async function annotateToolResult(conversationId, plugin, result) {
|
|
231
|
+
if (!isRecord$1(result) || result.isError === true) return result;
|
|
232
|
+
const details = isRecord$1(result.details) ? result.details : result;
|
|
233
|
+
if (details.timed_out === true || details.isError === true || !Array.isArray(details.objectAnnotations)) return result;
|
|
234
|
+
const cards = await saveObjectAnnotations(conversationId, plugin, details.objectAnnotations);
|
|
235
|
+
const { objectAnnotations: _annotations, ...rest } = details;
|
|
236
|
+
const annotated = {
|
|
237
|
+
...rest,
|
|
238
|
+
objectCards: cards
|
|
663
239
|
};
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
conversationId: row.conversationId,
|
|
668
|
-
createdAtMs: requiredMsFromDate(row.createdAt),
|
|
669
|
-
lastActivityAtMs: requiredMsFromDate(row.lastActivityAt),
|
|
670
|
-
updatedAtMs: requiredMsFromDate(row.updatedAt),
|
|
671
|
-
execution,
|
|
672
|
-
executionMetrics: {
|
|
673
|
-
durationMs: row.executionDurationMs,
|
|
674
|
-
...row.metricRunId ? { runId: row.metricRunId } : void 0,
|
|
675
|
-
...row.executionUsage ? { usage: row.executionUsage } : void 0
|
|
676
|
-
},
|
|
677
|
-
...row.parentConversationId ? { parentConversationId: row.parentConversationId } : void 0,
|
|
678
|
-
...destination ? { destination } : void 0,
|
|
679
|
-
...location ? { location } : void 0,
|
|
680
|
-
...actor ? { actor } : void 0,
|
|
681
|
-
...row.channelName ? { channelName: row.channelName } : void 0,
|
|
682
|
-
...source ? { source } : void 0,
|
|
683
|
-
...sessionSource ? { sessionSource } : void 0,
|
|
684
|
-
...row.title ? { title: row.title } : void 0,
|
|
685
|
-
...msFromDate(row.transcriptPurgedAt) !== void 0 ? { transcriptPurgedAtMs: msFromDate(row.transcriptPurgedAt) } : void 0,
|
|
686
|
-
...visibility ? { visibility } : void 0
|
|
240
|
+
if (isRecord$1(result.details)) return {
|
|
241
|
+
...result,
|
|
242
|
+
details: annotated
|
|
687
243
|
};
|
|
244
|
+
return annotated;
|
|
245
|
+
}
|
|
246
|
+
//#endregion
|
|
247
|
+
//#region src/chat/conversations/sql/privacy.ts
|
|
248
|
+
const privacyRoot = alias(juniorConversations, "privacy_root");
|
|
249
|
+
/** Accept only a structurally valid persisted root as privacy authority. */
|
|
250
|
+
async function readRootCandidate(executor, conversationId) {
|
|
251
|
+
const row = (await executor.db().select({
|
|
252
|
+
requestedConversationId: juniorConversations.conversationId,
|
|
253
|
+
requestedParentConversationId: juniorConversations.parentConversationId,
|
|
254
|
+
requestedRootConversationId: juniorConversations.rootConversationId,
|
|
255
|
+
rootConversationId: privacyRoot.conversationId,
|
|
256
|
+
rootDestinationId: privacyRoot.destinationId,
|
|
257
|
+
rootParentConversationId: privacyRoot.parentConversationId,
|
|
258
|
+
rootRootConversationId: privacyRoot.rootConversationId
|
|
259
|
+
}).from(juniorConversations).leftJoin(privacyRoot, eq(privacyRoot.conversationId, juniorConversations.rootConversationId)).where(eq(juniorConversations.conversationId, conversationId)))[0];
|
|
260
|
+
return row?.requestedRootConversationId !== null && (row?.requestedParentConversationId !== null || row?.requestedRootConversationId === row?.requestedConversationId) && row?.rootConversationId === row.requestedRootConversationId && row.rootRootConversationId === row.rootConversationId && row.rootParentConversationId === null && row.rootDestinationId !== null ? {
|
|
261
|
+
destinationId: row.rootDestinationId,
|
|
262
|
+
rootConversationId: row.rootConversationId
|
|
263
|
+
} : void 0;
|
|
688
264
|
}
|
|
689
|
-
function
|
|
265
|
+
async function readCandidateVisibility(executor, candidate) {
|
|
266
|
+
const destinations = await executor.db().select({ visibility: juniorDestinations.visibility }).from(juniorDestinations).where(eq(juniorDestinations.id, candidate.destinationId)).for("share");
|
|
690
267
|
return {
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
createdAtMs: args.nowMs,
|
|
694
|
-
lastActivityAtMs: args.nowMs,
|
|
695
|
-
updatedAtMs: args.nowMs,
|
|
696
|
-
...args.destination ? { destination: args.destination } : void 0,
|
|
697
|
-
...args.source ? { source: args.source } : void 0,
|
|
698
|
-
...args.sessionSource ? { sessionSource: args.sessionSource } : void 0,
|
|
699
|
-
execution: {
|
|
700
|
-
status: "idle",
|
|
701
|
-
updatedAtMs: args.nowMs
|
|
702
|
-
}
|
|
268
|
+
rootConversationId: candidate.rootConversationId,
|
|
269
|
+
visibility: destinations[0]?.visibility ?? null
|
|
703
270
|
};
|
|
704
271
|
}
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
272
|
+
/**
|
|
273
|
+
* Resolve a conversation's privacy authority from its persisted root.
|
|
274
|
+
*
|
|
275
|
+
* Missing or structurally invalid root metadata fails closed. The root
|
|
276
|
+
* destination is locked so callers that keep the transaction open receive a
|
|
277
|
+
* stable visibility decision.
|
|
278
|
+
*/
|
|
279
|
+
async function resolveRootVisibility(executor, conversationId) {
|
|
280
|
+
const candidate = await readRootCandidate(executor, conversationId);
|
|
281
|
+
if (!candidate) return {
|
|
282
|
+
rootConversationId: conversationId,
|
|
283
|
+
visibility: null
|
|
284
|
+
};
|
|
285
|
+
return readCandidateVisibility(executor, candidate);
|
|
708
286
|
}
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
const existing = await this.get({ conversationId: args.childConversationId });
|
|
725
|
-
if (existing) {
|
|
726
|
-
if (existing.parentConversationId !== args.parentConversationId) throw new Error(`Conversation parent changed for ${args.childConversationId}`);
|
|
727
|
-
return;
|
|
728
|
-
}
|
|
729
|
-
const parent = await this.get({ conversationId: args.parentConversationId });
|
|
730
|
-
if (!parent) throw new Error(`Conversation parent is missing for ${args.childConversationId}`);
|
|
731
|
-
if (parent.parentConversationId) throw new Error("Recursive agent delegation is not enabled");
|
|
732
|
-
const child = {
|
|
733
|
-
...emptyConversation({
|
|
734
|
-
conversationId: args.childConversationId,
|
|
735
|
-
nowMs,
|
|
736
|
-
source: args.source
|
|
737
|
-
}),
|
|
738
|
-
parentConversationId: args.parentConversationId
|
|
739
|
-
};
|
|
740
|
-
await this.upsertConversation({ conversation: child });
|
|
741
|
-
});
|
|
742
|
-
});
|
|
743
|
-
}
|
|
744
|
-
async get(args) {
|
|
745
|
-
const row = await this.readConversationRow(args.conversationId);
|
|
746
|
-
if (!row) return;
|
|
747
|
-
return conversationFromRow(row);
|
|
748
|
-
}
|
|
749
|
-
async recordActivity(args) {
|
|
750
|
-
const nowMs = args.nowMs ?? now();
|
|
751
|
-
const activityAtMs = args.activityAtMs ?? nowMs;
|
|
752
|
-
const sessionSource = normalizeSessionSource(args.sessionSource);
|
|
753
|
-
await this.withConversationMutation(args.conversationId, async () => {
|
|
754
|
-
const existing = await this.get({ conversationId: args.conversationId });
|
|
755
|
-
if (existing && args.destination) assertSameConversationDestination({
|
|
756
|
-
conversationId: args.conversationId,
|
|
757
|
-
current: existing.destination,
|
|
758
|
-
next: args.destination
|
|
759
|
-
});
|
|
760
|
-
const current = existing ?? emptyConversation({
|
|
761
|
-
conversationId: args.conversationId,
|
|
762
|
-
destination: args.destination,
|
|
763
|
-
nowMs,
|
|
764
|
-
source: args.source,
|
|
765
|
-
...sessionSource ? { sessionSource } : void 0
|
|
766
|
-
});
|
|
767
|
-
const { sessionSource: _persistedSessionSource, visibility: _persistedVisibility, ...currentWithoutPersistedSignals } = current;
|
|
768
|
-
await this.upsertConversation({ conversation: {
|
|
769
|
-
...currentWithoutPersistedSignals,
|
|
770
|
-
destination: current.destination ?? args.destination,
|
|
771
|
-
source: current.source ?? args.source,
|
|
772
|
-
...sessionSource ? { sessionSource } : void 0,
|
|
773
|
-
channelName: current.channelName ?? args.channelName,
|
|
774
|
-
actor: mergeActor(current.actor, args.actor),
|
|
775
|
-
title: current.title ?? args.title,
|
|
776
|
-
lastActivityAtMs: Math.max(current.lastActivityAtMs, activityAtMs),
|
|
777
|
-
updatedAtMs: nowMs,
|
|
778
|
-
execution: {
|
|
779
|
-
...current.execution,
|
|
780
|
-
updatedAtMs: current.execution.updatedAtMs ?? nowMs
|
|
781
|
-
},
|
|
782
|
-
...args.visibility ? { visibility: args.visibility } : void 0
|
|
783
|
-
} });
|
|
784
|
-
});
|
|
785
|
-
}
|
|
786
|
-
async recordExecution(args) {
|
|
787
|
-
if (args.metrics && !args.execution.runId) throw new Error("Conversation Run metrics require a Run id");
|
|
788
|
-
await this.withConversationMutation(args.conversationId, async () => {
|
|
789
|
-
const existingRow = await this.readConversationRow(args.conversationId);
|
|
790
|
-
const existing = existingRow ? conversationFromRow(existingRow) : void 0;
|
|
791
|
-
if (!existing && !args.destination) return;
|
|
792
|
-
const incomingExecutionAt = args.execution.updatedAtMs ?? args.updatedAtMs;
|
|
793
|
-
const incomingIsFresh = incomingExecutionAt >= (existing?.execution.updatedAtMs ?? existing?.updatedAtMs ?? 0);
|
|
794
|
-
const execution = incomingIsFresh ? args.execution : existing?.execution ?? args.execution;
|
|
795
|
-
await this.upsertConversation({ conversation: {
|
|
796
|
-
schemaVersion: 1,
|
|
797
|
-
conversationId: args.conversationId,
|
|
798
|
-
createdAtMs: args.createdAtMs,
|
|
799
|
-
lastActivityAtMs: args.lastActivityAtMs,
|
|
800
|
-
updatedAtMs: args.updatedAtMs,
|
|
801
|
-
...existing?.parentConversationId ? { parentConversationId: existing.parentConversationId } : void 0,
|
|
802
|
-
...existing?.location ? { location: existing.location } : void 0,
|
|
803
|
-
...args.channelName ? { channelName: args.channelName } : void 0,
|
|
804
|
-
...args.destination ? { destination: args.destination } : void 0,
|
|
805
|
-
...args.actor ? { actor: args.actor } : void 0,
|
|
806
|
-
...args.source ? { source: args.source } : void 0,
|
|
807
|
-
...args.title ? { title: args.title } : void 0,
|
|
808
|
-
...args.visibility ? { visibility: args.visibility } : void 0,
|
|
809
|
-
execution
|
|
810
|
-
} });
|
|
811
|
-
if (args.metrics && args.execution.runId) {
|
|
812
|
-
await replaceConversationMetrics(this.executor, {
|
|
813
|
-
conversationId: args.conversationId,
|
|
814
|
-
runId: args.execution.runId,
|
|
815
|
-
occurredAtMs: args.createdAtMs,
|
|
816
|
-
updatedAtMs: incomingExecutionAt,
|
|
817
|
-
...args.metrics
|
|
818
|
-
});
|
|
819
|
-
if (incomingIsFresh) await this.executor.db().update(juniorConversations).set({
|
|
820
|
-
metricRunId: args.execution.runId,
|
|
821
|
-
executionDurationMs: args.metrics.durationMs,
|
|
822
|
-
executionUsage: args.metrics.usage ?? null
|
|
823
|
-
}).where(eq(juniorConversations.conversationId, args.conversationId));
|
|
824
|
-
}
|
|
825
|
-
});
|
|
826
|
-
}
|
|
827
|
-
async listByActivity(args = {}) {
|
|
828
|
-
const rows = await this.executor.db().select({
|
|
829
|
-
conversation: conversationReadColumns(),
|
|
830
|
-
destination: juniorDestinations,
|
|
831
|
-
actorIdentity: juniorIdentities,
|
|
832
|
-
actorUserDisplayName: juniorUsers.displayName
|
|
833
|
-
}).from(juniorConversations).leftJoin(juniorDestinations, eq(juniorDestinations.id, juniorConversations.destinationId)).leftJoin(juniorIdentities, eq(juniorIdentities.id, juniorConversations.actorIdentityId)).leftJoin(juniorUsers, eq(juniorUsers.id, juniorIdentities.userId)).where(isNull(juniorConversations.parentConversationId)).orderBy(desc(juniorConversations.lastActivityAt), asc(juniorConversations.conversationId)).limit(Math.max(0, args.limit ?? 1e4)).offset(Math.max(0, args.offset ?? 0));
|
|
834
|
-
const conversations = [];
|
|
835
|
-
for (const row of rows) conversations.push(conversationFromRow(row));
|
|
836
|
-
return conversations;
|
|
837
|
-
}
|
|
838
|
-
async getDestinationVisibility(args) {
|
|
839
|
-
const row = (await this.executor.db().select({ visibility: juniorDestinations.visibility }).from(juniorDestinations).where(and(eq(juniorDestinations.provider, args.provider), eq(juniorDestinations.providerTenantId, tenantId(args.providerTenantId)), eq(juniorDestinations.providerDestinationId, args.providerDestinationId))))[0];
|
|
840
|
-
if (!row) return;
|
|
841
|
-
if (row.visibility === "public" || row.visibility === "private") return row.visibility;
|
|
842
|
-
}
|
|
843
|
-
async findSlackDestinationByName(args) {
|
|
844
|
-
const normalized = args.channelName.trim().replace(/^#/, "").toLowerCase();
|
|
845
|
-
if (!normalized) return;
|
|
846
|
-
const rows = await this.executor.db().select({
|
|
847
|
-
channelId: juniorDestinations.providerDestinationId,
|
|
848
|
-
channelName: juniorDestinations.displayName
|
|
849
|
-
}).from(juniorDestinations).where(and(eq(juniorDestinations.provider, "slack"), eq(juniorDestinations.providerTenantId, tenantId(args.teamId)), eq(juniorDestinations.kind, "channel"), sql`lower(ltrim(${juniorDestinations.displayName}, '#')) = ${normalized}`)).limit(2);
|
|
850
|
-
if (rows.length !== 1) return;
|
|
851
|
-
const row = rows[0];
|
|
852
|
-
if (!row?.channelId) return;
|
|
287
|
+
//#endregion
|
|
288
|
+
//#region src/chat/briefs/events.ts
|
|
289
|
+
/** Durable cost and size record for one stored Brief version. */
|
|
290
|
+
const briefUpdatedEvent = defineConversationEvent({
|
|
291
|
+
name: "brief_updated",
|
|
292
|
+
version: 1,
|
|
293
|
+
schema: z.object({
|
|
294
|
+
version: z.number().int().positive(),
|
|
295
|
+
modelId: z.string().min(1),
|
|
296
|
+
costUsd: z.number().finite().nonnegative().optional(),
|
|
297
|
+
decisions: z.number().int().nonnegative(),
|
|
298
|
+
openDecisions: z.number().int().nonnegative(),
|
|
299
|
+
links: z.number().int().nonnegative()
|
|
300
|
+
}).strict(),
|
|
301
|
+
renderEvent(event) {
|
|
853
302
|
return {
|
|
854
|
-
|
|
855
|
-
|
|
303
|
+
icon: "activity",
|
|
304
|
+
title: `Brief updated (v${event.version})`
|
|
856
305
|
};
|
|
857
306
|
}
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
destination: conversation.destination,
|
|
887
|
-
destinationId,
|
|
888
|
-
location: conversation.location,
|
|
889
|
-
sessionSource: conversation.sessionSource
|
|
890
|
-
});
|
|
891
|
-
const actorIdentityObservation = actorIdentityForConversation(conversation);
|
|
892
|
-
const actorIdentity = actorIdentityObservation ? await upsertIdentity(this.executor, actorIdentityObservation, conversation.updatedAtMs) : void 0;
|
|
893
|
-
const rootConversationId = conversation.parentConversationId ? sql`(
|
|
894
|
-
select parent.root_conversation_id
|
|
895
|
-
from junior_conversations parent
|
|
896
|
-
where parent.conversation_id = ${conversation.parentConversationId}
|
|
897
|
-
)` : conversation.conversationId;
|
|
898
|
-
const row = (await this.executor.db().insert(juniorConversations).values({
|
|
899
|
-
conversationId: conversation.conversationId,
|
|
900
|
-
schemaVersion: 1,
|
|
901
|
-
source: conversation.source ?? null,
|
|
902
|
-
sessionSource: conversation.sessionSource ?? null,
|
|
903
|
-
originType: originTypeFromSource(conversation.source) ?? null,
|
|
904
|
-
originId: null,
|
|
905
|
-
originRunId: null,
|
|
906
|
-
location: location ?? null,
|
|
907
|
-
destinationId: destinationId ?? null,
|
|
908
|
-
destination: null,
|
|
909
|
-
actorIdentityId: actorIdentity?.id ?? null,
|
|
910
|
-
creatorIdentityId: null,
|
|
911
|
-
credentialSubjectIdentityId: null,
|
|
912
|
-
actor: null,
|
|
913
|
-
channelName: conversation.channelName ?? null,
|
|
914
|
-
title: conversation.title ?? null,
|
|
915
|
-
createdAt: dateFromMs(conversation.createdAtMs),
|
|
916
|
-
lastActivityAt: dateFromMs(conversation.lastActivityAtMs),
|
|
917
|
-
updatedAt: dateFromMs(conversation.updatedAtMs),
|
|
918
|
-
executionUpdatedAt: conversation.execution.updatedAtMs === void 0 ? null : dateFromMs(conversation.execution.updatedAtMs),
|
|
919
|
-
executionStatus: executionStatusToSql(conversation.execution.status),
|
|
920
|
-
runId: conversation.execution.runId ?? null,
|
|
921
|
-
lastCheckpointAt: conversation.execution.lastCheckpointAtMs === void 0 ? null : dateFromMs(conversation.execution.lastCheckpointAtMs),
|
|
922
|
-
lastEnqueuedAt: conversation.execution.lastEnqueuedAtMs === void 0 ? null : dateFromMs(conversation.execution.lastEnqueuedAtMs),
|
|
923
|
-
parentConversationId: conversation.parentConversationId ?? null,
|
|
924
|
-
rootConversationId
|
|
925
|
-
}).onConflictDoUpdate({
|
|
926
|
-
target: juniorConversations.conversationId,
|
|
927
|
-
set: {
|
|
928
|
-
source: sql`coalesce(${juniorConversations.source}, excluded.source)`,
|
|
929
|
-
sessionSource: sql`coalesce(${juniorConversations.sessionSource}, excluded.source_json)`,
|
|
930
|
-
originType: sql`coalesce(excluded.origin_type, ${juniorConversations.originType})`,
|
|
931
|
-
originId: sql`coalesce(excluded.origin_id, ${juniorConversations.originId})`,
|
|
932
|
-
originRunId: sql`coalesce(excluded.origin_run_id, ${juniorConversations.originRunId})`,
|
|
933
|
-
location: sql`coalesce(excluded.location_json, ${juniorConversations.location})`,
|
|
934
|
-
destinationId: sql`coalesce(excluded.destination_id, ${juniorConversations.destinationId})`,
|
|
935
|
-
actorIdentityId: sql`coalesce(excluded.actor_identity_id, ${juniorConversations.actorIdentityId})`,
|
|
936
|
-
creatorIdentityId: sql`coalesce(excluded.creator_identity_id, ${juniorConversations.creatorIdentityId})`,
|
|
937
|
-
credentialSubjectIdentityId: sql`coalesce(excluded.credential_subject_identity_id, ${juniorConversations.credentialSubjectIdentityId})`,
|
|
938
|
-
channelName: sql`coalesce(excluded.channel_name, ${juniorConversations.channelName})`,
|
|
939
|
-
title: sql`coalesce(excluded.title, ${juniorConversations.title})`,
|
|
940
|
-
createdAt: sql`least(${juniorConversations.createdAt}, excluded.created_at)`,
|
|
941
|
-
lastActivityAt: sql`greatest(${juniorConversations.lastActivityAt}, excluded.last_activity_at)`,
|
|
942
|
-
updatedAt: sql`greatest(${juniorConversations.updatedAt}, excluded.updated_at)`,
|
|
943
|
-
executionUpdatedAt: sql`case when ${incomingExecutionIsFresh} then excluded.execution_updated_at else ${juniorConversations.executionUpdatedAt} end`,
|
|
944
|
-
executionStatus: sql`case when ${incomingExecutionIsFresh} then excluded.execution_status else ${juniorConversations.executionStatus} end`,
|
|
945
|
-
runId: sql`case when ${incomingExecutionIsFresh} then excluded.run_id else ${juniorConversations.runId} end`,
|
|
946
|
-
lastCheckpointAt: sql`case when ${incomingExecutionIsFresh} then coalesce(excluded.last_checkpoint_at, ${juniorConversations.lastCheckpointAt}) else ${juniorConversations.lastCheckpointAt} end`,
|
|
947
|
-
lastEnqueuedAt: sql`case when ${incomingExecutionIsFresh} then coalesce(excluded.last_enqueued_at, ${juniorConversations.lastEnqueuedAt}) else ${juniorConversations.lastEnqueuedAt} end`,
|
|
948
|
-
rootConversationId: sql`coalesce(${juniorConversations.rootConversationId}, excluded.root_conversation_id)`
|
|
949
|
-
}
|
|
950
|
-
}).returning({
|
|
951
|
-
actorIdentityId: juniorConversations.actorIdentityId,
|
|
952
|
-
parentConversationId: juniorConversations.parentConversationId,
|
|
953
|
-
rootConversationId: juniorConversations.rootConversationId
|
|
954
|
-
}))[0];
|
|
955
|
-
if (!row?.rootConversationId) throw new Error("Conversation parent is missing its persisted root");
|
|
956
|
-
if (row.actorIdentityId && row.parentConversationId === null) await recordConversationParticipant(this.executor, {
|
|
957
|
-
actorIdentityId: row.actorIdentityId,
|
|
958
|
-
conversationId: conversation.conversationId,
|
|
959
|
-
atMs: conversation.lastActivityAtMs
|
|
960
|
-
});
|
|
961
|
-
}
|
|
962
|
-
async upsertDestination(destination, nowMs) {
|
|
963
|
-
if (!destination) return;
|
|
964
|
-
const visibilityUpdate = destination.refreshVisibility ? sql`excluded.visibility` : juniorDestinations.visibility;
|
|
965
|
-
return (await this.executor.db().insert(juniorDestinations).values({
|
|
966
|
-
id: randomUUID(),
|
|
967
|
-
provider: destination.provider,
|
|
968
|
-
providerTenantId: tenantId(destination.providerTenantId),
|
|
969
|
-
providerDestinationId: destination.providerDestinationId,
|
|
970
|
-
kind: destination.kind,
|
|
971
|
-
parentDestinationId: null,
|
|
972
|
-
displayName: destination.displayName ?? null,
|
|
973
|
-
visibility: destination.visibility,
|
|
974
|
-
metadata: destination.metadata ?? null,
|
|
975
|
-
createdAt: dateFromMs(nowMs),
|
|
976
|
-
updatedAt: dateFromMs(nowMs)
|
|
977
|
-
}).onConflictDoUpdate({
|
|
978
|
-
target: [
|
|
979
|
-
juniorDestinations.provider,
|
|
980
|
-
juniorDestinations.providerTenantId,
|
|
981
|
-
juniorDestinations.providerDestinationId
|
|
982
|
-
],
|
|
983
|
-
set: {
|
|
984
|
-
kind: sql`excluded.kind`,
|
|
985
|
-
displayName: sql`coalesce(excluded.display_name, ${juniorDestinations.displayName})`,
|
|
986
|
-
visibility: visibilityUpdate,
|
|
987
|
-
metadata: sql`coalesce(excluded.metadata_json, ${juniorDestinations.metadata})`,
|
|
988
|
-
updatedAt: sql`excluded.updated_at`
|
|
989
|
-
}
|
|
990
|
-
}).returning({ id: juniorDestinations.id }))[0]?.id;
|
|
991
|
-
}
|
|
992
|
-
};
|
|
993
|
-
/** Create a SQL-backed conversation store. */
|
|
994
|
-
function createSqlStore(executor) {
|
|
995
|
-
return new SqlStore(executor);
|
|
307
|
+
});
|
|
308
|
+
//#endregion
|
|
309
|
+
//#region src/chat/slack/attachments.ts
|
|
310
|
+
/** Save the vision summary for a stored Slack attachment. */
|
|
311
|
+
async function recordSlackAttachment(args) {
|
|
312
|
+
await args.db.db().update(juniorAttachments).set({ visionSummary: args.visionSummary }).where(and(eq(juniorAttachments.id, args.attachmentId), eq(juniorAttachments.conversationId, args.conversationId), isNull(juniorAttachments.deleteRequestedAt)));
|
|
313
|
+
}
|
|
314
|
+
/** Read a saved vision summary for one live attachment. */
|
|
315
|
+
async function readAttachmentVisionSummary(args) {
|
|
316
|
+
const [row] = await args.db.db().select({ visionSummary: juniorAttachments.visionSummary }).from(juniorAttachments).where(and(eq(juniorAttachments.id, args.attachmentId), eq(juniorAttachments.conversationId, args.conversationId), isNull(juniorAttachments.deleteRequestedAt)));
|
|
317
|
+
return row?.visionSummary;
|
|
318
|
+
}
|
|
319
|
+
/** Match live conversation attachments to Slack file ids. */
|
|
320
|
+
async function matchSlackAttachments(args) {
|
|
321
|
+
if (args.providerIds.length === 0) return [];
|
|
322
|
+
return await args.db.db().select({
|
|
323
|
+
id: juniorAttachments.id,
|
|
324
|
+
providerId: juniorAttachments.providerId,
|
|
325
|
+
bytes: juniorAttachments.bytes,
|
|
326
|
+
contentType: juniorAttachments.contentType,
|
|
327
|
+
filename: juniorAttachments.filename
|
|
328
|
+
}).from(juniorAttachments).where(and(eq(juniorAttachments.conversationId, args.conversationId), eq(juniorAttachments.provider, "slack"), inArray(juniorAttachments.providerId, args.providerIds), isNull(juniorAttachments.deleteRequestedAt)));
|
|
329
|
+
}
|
|
330
|
+
/** Read the Slack file identity retained by the Chat SDK. */
|
|
331
|
+
function getSlackFileId(attachment) {
|
|
332
|
+
const metadataId = attachment.fetchMetadata?.fileId;
|
|
333
|
+
if (metadataId) return metadataId;
|
|
334
|
+
return (attachment.url ?? attachment.fetchMetadata?.url ?? "").match(/(?:^|[-/])(F[A-Z0-9]+)(?:[-/]|$)/i)?.[1];
|
|
996
335
|
}
|
|
997
336
|
//#endregion
|
|
998
|
-
//#region src/
|
|
999
|
-
/**
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
* append refreshes the retention window). Background observations may opt out
|
|
1007
|
-
* and leave an existing row unchanged. The timestamp is content-intrinsic (the
|
|
1008
|
-
* first item's `createdAtMs`), and `greatest(...)` guarantees a backfilled or
|
|
1009
|
-
* imported historical write — which carries an old timestamp — can never
|
|
1010
|
-
* regress `last_activity_at`.
|
|
1011
|
-
*/
|
|
1012
|
-
async function ensureConversationRow(executor, conversationId, atMs, options = {}) {
|
|
1013
|
-
const at = new Date(atMs);
|
|
1014
|
-
const insert = executor.db().insert(juniorConversations).values({
|
|
337
|
+
//#region src/api/conversations/message-attachments.ts
|
|
338
|
+
/** Resolve saved Slack file references without exposing private storage details. */
|
|
339
|
+
async function hydrateMessageAttachments(db, conversationId, events) {
|
|
340
|
+
const messages = events.flatMap((event) => event.data.type === "message" ? [event.data] : []);
|
|
341
|
+
const ids = [...new Set(messages.flatMap((message) => slackFileIds(message.meta)))];
|
|
342
|
+
if (!ids.length) return;
|
|
343
|
+
const attachments = await matchSlackAttachments({
|
|
344
|
+
db,
|
|
1015
345
|
conversationId,
|
|
1016
|
-
|
|
1017
|
-
lastActivityAt: at,
|
|
1018
|
-
updatedAt: at,
|
|
1019
|
-
executionStatus: "idle"
|
|
346
|
+
providerIds: ids
|
|
1020
347
|
});
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
348
|
+
const byFileId = new Map(attachments.map(({ providerId, ...attachment }) => [providerId, attachment]));
|
|
349
|
+
for (const message of messages) {
|
|
350
|
+
const stored = readMessageAttachments(message.meta?.attachments);
|
|
351
|
+
const matched = slackFileIds(message.meta).flatMap((id) => {
|
|
352
|
+
const attachment = byFileId.get(id);
|
|
353
|
+
return attachment ? [attachment] : [];
|
|
354
|
+
});
|
|
355
|
+
if (matched.length) message.meta = {
|
|
356
|
+
...message.meta,
|
|
357
|
+
attachments: [...stored, ...matched]
|
|
358
|
+
};
|
|
1024
359
|
}
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
updatedAt: sql`greatest(${juniorConversations.updatedAt}, excluded.updated_at)`,
|
|
1030
|
-
transcriptPurgedAt: null
|
|
1031
|
-
}
|
|
1032
|
-
});
|
|
360
|
+
}
|
|
361
|
+
function slackFileIds(meta) {
|
|
362
|
+
const ids = meta?.slackFileIds ?? meta?.imageFileIds;
|
|
363
|
+
return Array.isArray(ids) ? ids.filter((id) => typeof id === "string") : [];
|
|
1033
364
|
}
|
|
1034
365
|
//#endregion
|
|
1035
|
-
//#region src/chat/
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
1039
|
-
}
|
|
1040
|
-
function readAuthor(meta) {
|
|
1041
|
-
if (!isRecord$1(meta) || !isRecord$1(meta.author)) return {};
|
|
1042
|
-
const author = meta.author;
|
|
1043
|
-
return {
|
|
1044
|
-
...typeof author.email === "string" ? { email: author.email } : void 0,
|
|
1045
|
-
...typeof author.fullName === "string" ? { fullName: author.fullName } : void 0,
|
|
1046
|
-
...typeof author.isBot === "boolean" ? { isBot: author.isBot } : void 0,
|
|
1047
|
-
...typeof author.userId === "string" ? { userId: author.userId } : void 0,
|
|
1048
|
-
...typeof author.userName === "string" ? { userName: author.userName } : void 0
|
|
1049
|
-
};
|
|
366
|
+
//#region src/chat/sentry-links.ts
|
|
367
|
+
function getSentryOrgSlug() {
|
|
368
|
+
return process.env.SENTRY_ORG_SLUG?.trim() || void 0;
|
|
1050
369
|
}
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
providerTenantId: juniorDestinations.providerTenantId,
|
|
1054
|
-
sessionSource: juniorConversations.sessionSource
|
|
1055
|
-
}).from(juniorConversations).leftJoin(juniorDestinations, eq(juniorDestinations.id, juniorConversations.destinationId)).where(eq(juniorConversations.conversationId, conversationId)).limit(1);
|
|
1056
|
-
const destinationTenant = row?.providerTenantId?.trim();
|
|
1057
|
-
if (destinationTenant) return destinationTenant;
|
|
1058
|
-
const sessionSource = parseSessionSource(row?.sessionSource);
|
|
1059
|
-
if (sessionSource?.kind === "slack") return sessionSource.teamId;
|
|
370
|
+
function isSentrySaasDsnHost(host) {
|
|
371
|
+
return host === "sentry.io" || host.endsWith(".sentry.io");
|
|
1060
372
|
}
|
|
1061
|
-
|
|
1062
|
-
|
|
373
|
+
function buildSentryWebBaseUrl(dsn) {
|
|
374
|
+
if (isSentrySaasDsnHost(dsn.host)) return "https://sentry.io";
|
|
375
|
+
const port = dsn.port ? `:${dsn.port}` : "";
|
|
376
|
+
const path = dsn.path ? `/${dsn.path}` : "";
|
|
377
|
+
return `${dsn.protocol}://${dsn.host}${port}${path}`;
|
|
1063
378
|
}
|
|
1064
|
-
|
|
1065
|
-
const
|
|
1066
|
-
if (!
|
|
1067
|
-
|
|
379
|
+
function buildSentryOrgPath(args) {
|
|
380
|
+
const dsn = getClient$1()?.getDsn();
|
|
381
|
+
if (!dsn?.host || !dsn.projectId) return;
|
|
382
|
+
const orgSlug = getSentryOrgSlug();
|
|
383
|
+
if (!orgSlug) return;
|
|
384
|
+
const params = new URLSearchParams();
|
|
385
|
+
params.set("project", dsn.projectId);
|
|
386
|
+
if (args.query) for (const [key, value] of Object.entries(args.query)) params.set(key, value);
|
|
387
|
+
const path = `${args.path}?${params.toString()}`;
|
|
388
|
+
if (isSentrySaasDsnHost(dsn.host)) return `https://${orgSlug}.sentry.io/${path}`;
|
|
389
|
+
return `${buildSentryWebBaseUrl(dsn)}/organizations/${orgSlug}/${path}`;
|
|
1068
390
|
}
|
|
1069
|
-
/**
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
* durable identity subject remains the verified email.
|
|
1073
|
-
*/
|
|
1074
|
-
async function findJuniorIdentityIdByDashboardAuthorId(executor, dashboardAuthorId) {
|
|
1075
|
-
if (!dashboardAuthorId.startsWith("dashboard:")) return;
|
|
1076
|
-
return (await executor.db().select({ id: juniorIdentities.id }).from(juniorIdentities).where(and(eq(juniorIdentities.provider, "junior"), eq(juniorIdentities.kind, "user"), eq(juniorIdentities.emailVerified, true), sql`${juniorIdentities.emailNormalized} is not null`, sql`concat(
|
|
1077
|
-
'dashboard:',
|
|
1078
|
-
left(encode(digest(${juniorIdentities.emailNormalized}, 'sha256'), 'hex'), 24)
|
|
1079
|
-
) = ${dashboardAuthorId}`)).limit(1))[0]?.id;
|
|
391
|
+
/** Build a Sentry conversation URL only when the runtime has enough Sentry config. */
|
|
392
|
+
function buildSentryConversationUrl(conversationId) {
|
|
393
|
+
return buildSentryOrgPath({ path: `explore/conversations/${encodeURIComponent(conversationId)}/` });
|
|
1080
394
|
}
|
|
1081
|
-
/**
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
const data = args.data;
|
|
1088
|
-
if (data.type !== "message" && data.type !== "message_updated") return;
|
|
1089
|
-
if (typeof data.authorIdentityId === "string" && data.authorIdentityId) return data.authorIdentityId;
|
|
1090
|
-
if (data.role !== "user") return;
|
|
1091
|
-
const author = readAuthor(data.meta);
|
|
1092
|
-
if (author.isBot === true) return;
|
|
1093
|
-
const userId = parseActorUserId(author.userId);
|
|
1094
|
-
if (!userId) return;
|
|
1095
|
-
if (SLACK_USER_ID_PATTERN.test(userId)) {
|
|
1096
|
-
const tenantId = await loadConversationSlackTenantId(executor, args.conversationId);
|
|
1097
|
-
const observation = {
|
|
1098
|
-
kind: "user",
|
|
1099
|
-
provider: "slack",
|
|
1100
|
-
providerSubjectId: userId,
|
|
1101
|
-
...tenantId ? { providerTenantId: tenantId } : void 0,
|
|
1102
|
-
...author.fullName ? { displayName: author.fullName } : void 0,
|
|
1103
|
-
...author.userName ? { handle: author.userName } : void 0,
|
|
1104
|
-
...author.email ? {
|
|
1105
|
-
email: author.email,
|
|
1106
|
-
emailVerified: true
|
|
1107
|
-
} : void 0
|
|
1108
|
-
};
|
|
1109
|
-
if (tenantId) return (await upsertIdentity(executor, observation, args.nowMs)).id;
|
|
1110
|
-
return await findIdentityIdByProviderSubject(executor, {
|
|
1111
|
-
provider: "slack",
|
|
1112
|
-
providerSubjectId: userId
|
|
1113
|
-
}) ?? void 0;
|
|
1114
|
-
}
|
|
1115
|
-
const email = typeof author.email === "string" && author.email.includes("@") ? author.email.trim().toLowerCase() : userId.includes("@") ? userId.trim().toLowerCase() : void 0;
|
|
1116
|
-
if (email) return (await upsertIdentity(executor, {
|
|
1117
|
-
kind: "user",
|
|
1118
|
-
provider: "junior",
|
|
1119
|
-
providerSubjectId: email,
|
|
1120
|
-
email,
|
|
1121
|
-
emailVerified: true,
|
|
1122
|
-
...author.fullName ? { displayName: author.fullName } : void 0,
|
|
1123
|
-
metadata: { platform: "web" }
|
|
1124
|
-
}, args.nowMs)).id;
|
|
1125
|
-
if (userId.startsWith("dashboard:")) return findJuniorIdentityIdByDashboardAuthorId(executor, userId);
|
|
1126
|
-
return findJuniorIdentityIdByEmail(executor, userId);
|
|
1127
|
-
}
|
|
1128
|
-
/** Drop payload-only authorIdentityId after lifting it onto the event column. */
|
|
1129
|
-
function stripPayloadAuthorIdentityId(data) {
|
|
1130
|
-
if (data.type !== "message" && data.type !== "message_updated" || data.authorIdentityId === void 0) return data;
|
|
1131
|
-
const { authorIdentityId: _authorIdentityId, ...rest } = data;
|
|
1132
|
-
return rest;
|
|
395
|
+
/** Build a Sentry event URL only when the runtime has enough Sentry config. */
|
|
396
|
+
function buildSentryEventUrl(eventId) {
|
|
397
|
+
return buildSentryOrgPath({
|
|
398
|
+
path: "issues/",
|
|
399
|
+
query: { query: eventId }
|
|
400
|
+
});
|
|
1133
401
|
}
|
|
1134
402
|
//#endregion
|
|
1135
|
-
//#region src/
|
|
1136
|
-
/**
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
403
|
+
//#region src/api/conversations/event-model.ts
|
|
404
|
+
/** Apply recorded model choices without carrying a previous turn's settings forward. */
|
|
405
|
+
function conversationEventModel(data, current) {
|
|
406
|
+
if (data.type === "turn_started") return void 0;
|
|
407
|
+
if (data.type === "turn_routed" || data.type === "handoff") return {
|
|
408
|
+
modelId: data.modelId,
|
|
409
|
+
modelProfile: data.modelProfile,
|
|
410
|
+
...data.reasoningLevel ? { reasoningLevel: data.reasoningLevel } : void 0
|
|
411
|
+
};
|
|
412
|
+
if (data.type === "assistant_message") {
|
|
413
|
+
const model = text$1(data.model);
|
|
414
|
+
const provider = text$1(data.provider);
|
|
415
|
+
if (model) {
|
|
416
|
+
const modelId = model.includes("/") || !provider ? model : `${provider}/${model}`;
|
|
417
|
+
return {
|
|
418
|
+
...current,
|
|
419
|
+
modelId
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
return current;
|
|
1147
424
|
}
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
/** Serialize event sequence allocation with other conversation writes. */
|
|
1151
|
-
async function withConversationEventLock(executor, conversationId, callback) {
|
|
1152
|
-
return executor.withLock(`junior_conversation:${conversationId}`, callback);
|
|
425
|
+
function text$1(value) {
|
|
426
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
1153
427
|
}
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
const HUMAN_INSTRUCTION_PLATFORMS = /* @__PURE__ */ new Set([
|
|
1162
|
-
"slack",
|
|
1163
|
-
"local",
|
|
1164
|
-
"web"
|
|
1165
|
-
]);
|
|
1166
|
-
/**
|
|
1167
|
-
* Whether one event should restore an archived conversation to the feed.
|
|
1168
|
-
*
|
|
1169
|
-
* Archive hides finished noise until a human comes back. Events,
|
|
1170
|
-
* turn lifecycle, compaction, and other system writes may still refresh
|
|
1171
|
-
* activity clocks, but they must not unarchive on their own.
|
|
1172
|
-
*/
|
|
1173
|
-
function eventUnarchivesConversation(data) {
|
|
1174
|
-
if (data.type === "user_message") {
|
|
1175
|
-
const provenance = data.provenance;
|
|
1176
|
-
if (provenance.authority !== "instruction") return false;
|
|
1177
|
-
const platform = provenance.actor?.platform;
|
|
1178
|
-
return platform === void 0 || HUMAN_INSTRUCTION_PLATFORMS.has(platform);
|
|
1179
|
-
}
|
|
1180
|
-
if (data.type !== "message") return false;
|
|
1181
|
-
if (data.role !== "user") return false;
|
|
1182
|
-
const meta = data.meta;
|
|
1183
|
-
if (!meta) return true;
|
|
1184
|
-
if (typeof meta.eventType === "string" && meta.eventType.length > 0) return false;
|
|
1185
|
-
const author = meta.author;
|
|
1186
|
-
if (author && typeof author === "object" && !Array.isArray(author) && author.isBot === true) return false;
|
|
1187
|
-
return true;
|
|
1188
|
-
}
|
|
1189
|
-
/** Split validated event data into column-lifted and JSON payload fields. */
|
|
1190
|
-
function insertFromEvent(conversationId, seq, historyVersion, event, actorIdentityId) {
|
|
1191
|
-
const stripped = stripPayloadAuthorIdentityId(event.data);
|
|
1192
|
-
const data = conversationEventDataSchema.parse(stripped);
|
|
1193
|
-
const encoded = encodeHistoryPayload(data);
|
|
428
|
+
/** Allowlist model-call diagnostics; never expose opaque provider metadata. */
|
|
429
|
+
function conversationEventModelCall(data) {
|
|
430
|
+
if (data.type !== "assistant_message") return void 0;
|
|
431
|
+
const usage = extractGenAiUsageSummary(data);
|
|
432
|
+
const provider = text$1(data.provider);
|
|
433
|
+
const api = text$1(data.api);
|
|
434
|
+
const stopReason = text$1(data.stopReason);
|
|
1194
435
|
return {
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
idempotencyKey: event.idempotencyKey ?? null,
|
|
1200
|
-
type: data.type,
|
|
1201
|
-
payload: sanitizePostgresJson(encoded.payload),
|
|
1202
|
-
actorIdentityId: actorIdentityId ?? null,
|
|
1203
|
-
createdAt: new Date(event.createdAtMs)
|
|
436
|
+
...provider ? { provider } : void 0,
|
|
437
|
+
...api ? { api } : void 0,
|
|
438
|
+
...stopReason ? { stopReason } : void 0,
|
|
439
|
+
...hasAgentTurnUsage(usage) ? { usage } : void 0
|
|
1204
440
|
};
|
|
1205
441
|
}
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
442
|
+
//#endregion
|
|
443
|
+
//#region src/api/conversations/events.ts
|
|
444
|
+
/** Canonical event types that can contribute to the reporting projection. */
|
|
445
|
+
const conversationReportSourceEventTypes = [
|
|
446
|
+
"message",
|
|
447
|
+
"message_handled",
|
|
448
|
+
"assistant_message",
|
|
449
|
+
"tool_result",
|
|
450
|
+
"tool_execution_started",
|
|
451
|
+
"guardian_action_reviewed",
|
|
452
|
+
"turn_started",
|
|
453
|
+
"turn_context",
|
|
454
|
+
"structured_event",
|
|
455
|
+
"attachments_delivered",
|
|
456
|
+
"turn_routed",
|
|
457
|
+
"turn_completed",
|
|
458
|
+
"turn_failed",
|
|
459
|
+
"compaction",
|
|
460
|
+
"handoff",
|
|
461
|
+
"subagent_started",
|
|
462
|
+
"subagent_ended"
|
|
463
|
+
];
|
|
464
|
+
const reportingAssistantMessageSchema = z.object({
|
|
465
|
+
type: z.literal("assistant_message"),
|
|
466
|
+
content: z.array(z.unknown())
|
|
467
|
+
}).passthrough();
|
|
468
|
+
const reportingToolCallPartSchema = z.object({
|
|
469
|
+
type: z.literal("toolCall"),
|
|
470
|
+
id: z.string().min(1),
|
|
471
|
+
name: z.string().min(1),
|
|
472
|
+
arguments: z.unknown()
|
|
473
|
+
}).passthrough();
|
|
474
|
+
const reportingToolResultMessageSchema = z.object({
|
|
475
|
+
type: z.literal("tool_result"),
|
|
476
|
+
toolCallId: z.string().min(1),
|
|
477
|
+
toolName: z.string().min(1).optional(),
|
|
478
|
+
name: z.string().min(1).optional(),
|
|
479
|
+
content: z.array(z.unknown()),
|
|
480
|
+
details: z.unknown().optional(),
|
|
481
|
+
isError: z.boolean().optional()
|
|
482
|
+
}).passthrough();
|
|
483
|
+
const reportingToolResultDetailsSchema = z.object({
|
|
484
|
+
ok: z.boolean().optional(),
|
|
485
|
+
status: z.string().optional()
|
|
486
|
+
}).passthrough();
|
|
487
|
+
const reportingTextPartSchema = z.object({
|
|
488
|
+
type: z.literal("text"),
|
|
489
|
+
text: z.string()
|
|
490
|
+
}).passthrough();
|
|
491
|
+
const reportingReasoningPartSchema = z.object({
|
|
492
|
+
type: z.literal("thinking"),
|
|
493
|
+
thinking: z.string(),
|
|
494
|
+
redacted: z.literal(true).optional()
|
|
495
|
+
}).passthrough();
|
|
496
|
+
const reportingMediaPartSchema = z.object({
|
|
497
|
+
type: z.enum(["image", "audio"]),
|
|
498
|
+
mimeType: z.string().min(1).optional()
|
|
499
|
+
}).passthrough();
|
|
500
|
+
const reportingMessageAuthorSchema = z.object({
|
|
501
|
+
fullName: z.string().min(1).optional(),
|
|
502
|
+
userId: z.string().min(1).optional(),
|
|
503
|
+
userName: z.string().min(1).optional()
|
|
504
|
+
}).passthrough();
|
|
505
|
+
/** Retain model-visible text and media descriptors, dropping all opaque fields. */
|
|
506
|
+
function sanitizeModelContentPart(value) {
|
|
507
|
+
const text = reportingTextPartSchema.safeParse(value);
|
|
508
|
+
if (text.success) return {
|
|
509
|
+
type: "text",
|
|
510
|
+
text: text.data.text
|
|
511
|
+
};
|
|
512
|
+
const media = reportingMediaPartSchema.safeParse(value);
|
|
513
|
+
if (!media.success) return void 0;
|
|
1226
514
|
return {
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
data: event.data
|
|
515
|
+
type: media.data.type,
|
|
516
|
+
...media.data.mimeType ? { mimeType: media.data.mimeType } : void 0
|
|
1230
517
|
};
|
|
1231
518
|
}
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
if (parsed.length === 0) return [];
|
|
1240
|
-
return withConversationEventLock(this.executor, conversationId, async () => {
|
|
1241
|
-
const existingKeys = parsed.map((event) => event.idempotencyKey).filter((key) => key !== void 0);
|
|
1242
|
-
const persistedKeys = existingKeys.length === 0 ? /* @__PURE__ */ new Set() : new Set((await this.executor.db().select({ key: juniorConversationEvents.idempotencyKey }).from(juniorConversationEvents).where(and(eq(juniorConversationEvents.conversationId, conversationId), inArray(juniorConversationEvents.idempotencyKey, existingKeys)))).flatMap((row) => row.key ? [row.key] : []));
|
|
1243
|
-
const acceptedKeys = new Set(persistedKeys);
|
|
1244
|
-
const pending = parsed.filter((event) => {
|
|
1245
|
-
if (event.idempotencyKey === void 0) return true;
|
|
1246
|
-
if (acceptedKeys.has(event.idempotencyKey)) return false;
|
|
1247
|
-
acceptedKeys.add(event.idempotencyKey);
|
|
1248
|
-
return true;
|
|
1249
|
-
});
|
|
1250
|
-
if (pending.length === 0) return [];
|
|
1251
|
-
const newestCreatedAtMs = Math.max(...pending.map((event) => event.createdAtMs));
|
|
1252
|
-
await ensureConversationRow(this.executor, conversationId, newestCreatedAtMs, options);
|
|
1253
|
-
const cursor = await this.readCursor(conversationId);
|
|
1254
|
-
const historyVersion = cursor.maxHistoryVersion ?? 0;
|
|
1255
|
-
let seq = cursor.nextSeq;
|
|
1256
|
-
const rows = [];
|
|
1257
|
-
for (const event of pending) {
|
|
1258
|
-
const actorIdentityId = await resolveEventActorIdentityId(this.executor, {
|
|
1259
|
-
conversationId,
|
|
1260
|
-
data: event.data,
|
|
1261
|
-
nowMs: event.createdAtMs
|
|
1262
|
-
});
|
|
1263
|
-
rows.push(insertFromEvent(conversationId, seq++, historyVersion, event, actorIdentityId));
|
|
1264
|
-
if (actorIdentityId) await recordConversationParticipant(this.executor, {
|
|
1265
|
-
actorIdentityId,
|
|
1266
|
-
conversationId,
|
|
1267
|
-
atMs: event.createdAtMs,
|
|
1268
|
-
restoreArchive: eventUnarchivesConversation(event.data)
|
|
1269
|
-
});
|
|
1270
|
-
}
|
|
1271
|
-
await this.executor.db().insert(juniorConversationEvents).values(rows);
|
|
1272
|
-
return rows.map((row) => ({
|
|
1273
|
-
seq: row.seq,
|
|
1274
|
-
historyVersion: row.historyVersion
|
|
1275
|
-
}));
|
|
1276
|
-
});
|
|
1277
|
-
}
|
|
1278
|
-
async replaceHistory(conversationId, replacement) {
|
|
1279
|
-
const parsed = historyReplacementSchema.parse(replacement);
|
|
1280
|
-
await withConversationEventLock(this.executor, conversationId, async () => {
|
|
1281
|
-
await ensureConversationRow(this.executor, conversationId, Date.now());
|
|
1282
|
-
const cursor = await this.readCursor(conversationId);
|
|
1283
|
-
const historyVersion = (cursor.maxHistoryVersion ?? 0) + 1;
|
|
1284
|
-
await this.executor.db().insert(juniorConversationEvents).values(insertFromEvent(conversationId, cursor.nextSeq, historyVersion, parsed));
|
|
1285
|
-
});
|
|
1286
|
-
}
|
|
1287
|
-
async loadCurrentHistoryVersion(conversationId) {
|
|
1288
|
-
return (await this.readCursor(conversationId)).maxHistoryVersion ?? 0;
|
|
1289
|
-
}
|
|
1290
|
-
async loadCurrentHistory(conversationId) {
|
|
1291
|
-
const cursor = await this.readCursor(conversationId);
|
|
1292
|
-
if (cursor.maxHistoryVersion === null) return [];
|
|
1293
|
-
return (await this.executor.db().select().from(juniorConversationEvents).where(and(eq(juniorConversationEvents.conversationId, conversationId), eq(juniorConversationEvents.historyVersion, cursor.maxHistoryVersion))).orderBy(asc(juniorConversationEvents.seq))).map(eventFromRow);
|
|
1294
|
-
}
|
|
1295
|
-
async loadByIdempotencyKey(conversationId, idempotencyKey) {
|
|
1296
|
-
const [row] = await this.executor.db().select().from(juniorConversationEvents).where(and(eq(juniorConversationEvents.conversationId, conversationId), eq(juniorConversationEvents.idempotencyKey, idempotencyKey))).limit(1);
|
|
1297
|
-
return row ? eventFromRow(row) : void 0;
|
|
1298
|
-
}
|
|
1299
|
-
async loadHistoryContaining(conversationId, seq, throughSeq) {
|
|
1300
|
-
const [boundary] = await this.executor.db().select({ historyVersion: juniorConversationEvents.historyVersion }).from(juniorConversationEvents).where(and(eq(juniorConversationEvents.conversationId, conversationId), eq(juniorConversationEvents.seq, seq))).limit(1);
|
|
1301
|
-
if (!boundary) return void 0;
|
|
1302
|
-
return (await this.executor.db().select().from(juniorConversationEvents).where(and(eq(juniorConversationEvents.conversationId, conversationId), eq(juniorConversationEvents.historyVersion, boundary.historyVersion), throughSeq === void 0 ? void 0 : lte(juniorConversationEvents.seq, throughSeq))).orderBy(asc(juniorConversationEvents.seq))).map(eventFromRow);
|
|
1303
|
-
}
|
|
1304
|
-
async loadLatestStructuredEvent(conversationId, namespace, name) {
|
|
1305
|
-
const [row] = await this.executor.db().select().from(juniorConversationEvents).where(and(eq(juniorConversationEvents.conversationId, conversationId), eq(juniorConversationEvents.type, "structured_event"), sql`${juniorConversationEvents.payload}->>'namespace' = ${namespace}`, sql`${juniorConversationEvents.payload}->>'name' = ${name}`)).orderBy(desc(juniorConversationEvents.seq)).limit(1);
|
|
1306
|
-
return row ? eventFromRow(row) : void 0;
|
|
1307
|
-
}
|
|
1308
|
-
async loadLatestInstruction(conversationId) {
|
|
1309
|
-
const [row] = await this.executor.db().select().from(juniorConversationEvents).where(and(eq(juniorConversationEvents.conversationId, conversationId), eq(juniorConversationEvents.type, "user_message"), sql`${juniorConversationEvents.payload}->'provenance'->>'authority' = 'instruction'`)).orderBy(desc(juniorConversationEvents.seq)).limit(1);
|
|
1310
|
-
return row ? eventFromRow(row) : void 0;
|
|
1311
|
-
}
|
|
1312
|
-
async loadMessageHistory(conversationId) {
|
|
1313
|
-
const [compactionRow] = await this.executor.db().select().from(juniorConversationEvents).where(and(eq(juniorConversationEvents.conversationId, conversationId), eq(juniorConversationEvents.type, "messages_summarized"))).orderBy(desc(juniorConversationEvents.seq)).limit(1);
|
|
1314
|
-
const compaction = compactionRow ? messagesSummarizedEventFromRow(compactionRow) : void 0;
|
|
1315
|
-
const historyFromSeq = compaction?.data.historyFromSeq ?? 0;
|
|
1316
|
-
return {
|
|
1317
|
-
events: (await this.executor.db().select().from(juniorConversationEvents).where(and(eq(juniorConversationEvents.conversationId, conversationId), inArray(juniorConversationEvents.type, messageHistoryEventTypes), gte(juniorConversationEvents.seq, historyFromSeq))).orderBy(asc(juniorConversationEvents.seq))).map(eventFromRow),
|
|
1318
|
-
compaction,
|
|
1319
|
-
historyFromSeq
|
|
1320
|
-
};
|
|
1321
|
-
}
|
|
1322
|
-
async loadHistory(conversationId) {
|
|
1323
|
-
return (await this.executor.db().select().from(juniorConversationEvents).where(eq(juniorConversationEvents.conversationId, conversationId)).orderBy(asc(juniorConversationEvents.seq))).map(eventFromRow);
|
|
1324
|
-
}
|
|
1325
|
-
async query(conversationId, query) {
|
|
1326
|
-
if (query.limit < 1) throw new Error("Conversation event query limit must be at least 1");
|
|
1327
|
-
if (query.afterSeq !== void 0 && query.beforeSeq !== void 0 && query.afterSeq >= query.beforeSeq) return {
|
|
1328
|
-
events: [],
|
|
1329
|
-
hasOlder: false,
|
|
1330
|
-
hasNewer: false
|
|
1331
|
-
};
|
|
1332
|
-
const typeFilter = query.types && query.types.length > 0 ? inArray(juniorConversationEvents.type, [...query.types]) : void 0;
|
|
1333
|
-
const bounds = and(eq(juniorConversationEvents.conversationId, conversationId), query.afterSeq === void 0 ? void 0 : gt(juniorConversationEvents.seq, query.afterSeq), query.beforeSeq === void 0 ? void 0 : lt(juniorConversationEvents.seq, query.beforeSeq), typeFilter);
|
|
1334
|
-
const newestFirst = query.afterSeq === void 0;
|
|
1335
|
-
const rows = await this.executor.db().select().from(juniorConversationEvents).where(bounds).orderBy(newestFirst ? desc(juniorConversationEvents.seq) : asc(juniorConversationEvents.seq)).limit(query.limit + 1);
|
|
1336
|
-
const overflow = rows.length > query.limit;
|
|
1337
|
-
const pageRows = overflow ? rows.slice(0, query.limit) : rows;
|
|
1338
|
-
const events = (newestFirst ? [...pageRows].reverse() : pageRows).map(eventFromRow);
|
|
1339
|
-
if (events.length === 0) return {
|
|
1340
|
-
events,
|
|
1341
|
-
hasOlder: false,
|
|
1342
|
-
hasNewer: false
|
|
1343
|
-
};
|
|
1344
|
-
const firstSeq = events[0].seq;
|
|
1345
|
-
const lastSeq = events[events.length - 1].seq;
|
|
1346
|
-
return {
|
|
1347
|
-
events,
|
|
1348
|
-
hasOlder: newestFirst && overflow ? true : await this.hasMatchingEvent({
|
|
1349
|
-
conversationId,
|
|
1350
|
-
beforeSeq: firstSeq,
|
|
1351
|
-
typeFilter
|
|
1352
|
-
}),
|
|
1353
|
-
hasNewer: !newestFirst && overflow ? true : await this.hasMatchingEvent({
|
|
1354
|
-
conversationId,
|
|
1355
|
-
afterSeq: lastSeq,
|
|
1356
|
-
typeFilter
|
|
1357
|
-
})
|
|
1358
|
-
};
|
|
1359
|
-
}
|
|
1360
|
-
/** Return whether any matching event exists inside the exclusive seq window. */
|
|
1361
|
-
async hasMatchingEvent(args) {
|
|
1362
|
-
if (args.afterSeq !== void 0 && args.beforeSeq !== void 0 && args.afterSeq >= args.beforeSeq) return false;
|
|
1363
|
-
const [row] = await this.executor.db().select({ seq: juniorConversationEvents.seq }).from(juniorConversationEvents).where(and(eq(juniorConversationEvents.conversationId, args.conversationId), args.afterSeq === void 0 ? void 0 : gt(juniorConversationEvents.seq, args.afterSeq), args.beforeSeq === void 0 ? void 0 : lt(juniorConversationEvents.seq, args.beforeSeq), args.typeFilter)).limit(1);
|
|
1364
|
-
return row !== void 0;
|
|
1365
|
-
}
|
|
1366
|
-
/** Read the next sequence and active model-history version. */
|
|
1367
|
-
async readCursor(conversationId) {
|
|
1368
|
-
const rows = await this.executor.db().select({
|
|
1369
|
-
maxSeq: sql`max(${juniorConversationEvents.seq})`,
|
|
1370
|
-
maxHistoryVersion: sql`max(${juniorConversationEvents.historyVersion})`
|
|
1371
|
-
}).from(juniorConversationEvents).where(eq(juniorConversationEvents.conversationId, conversationId));
|
|
1372
|
-
const maxSeq = rows[0]?.maxSeq;
|
|
1373
|
-
const maxHistoryVersion = rows[0]?.maxHistoryVersion;
|
|
1374
|
-
return {
|
|
1375
|
-
maxHistoryVersion: maxHistoryVersion === null || maxHistoryVersion === void 0 ? null : Number(maxHistoryVersion),
|
|
1376
|
-
nextSeq: maxSeq === null || maxSeq === void 0 ? 0 : Number(maxSeq) + 1
|
|
1377
|
-
};
|
|
1378
|
-
}
|
|
1379
|
-
};
|
|
1380
|
-
/** Create a SQL-backed canonical conversation event store. */
|
|
1381
|
-
function createSqlConversationEventStore(executor) {
|
|
1382
|
-
return new SqlConversationEventStore(executor);
|
|
519
|
+
/** Convert allowlisted Pi content parts into the reporting result value. */
|
|
520
|
+
function modelVisibleToolOutput(content) {
|
|
521
|
+
const sanitized = content.map(sanitizeModelContentPart).filter((part) => part !== void 0);
|
|
522
|
+
if (sanitized.length === 0) return void 0;
|
|
523
|
+
if (sanitized.length !== 1) return sanitized;
|
|
524
|
+
const only = sanitized[0];
|
|
525
|
+
return only.type === "text" ? only.text : only;
|
|
1383
526
|
}
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
const
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
const query = args.filters.query?.trim() || void 0;
|
|
1395
|
-
const text = sql`${juniorConversationEvents.payload}->>'text'`;
|
|
1396
|
-
const role = sql`${juniorConversationEvents.payload}->>'role'`;
|
|
1397
|
-
const messageId = sql`${juniorConversationEvents.payload}->>'messageId'`;
|
|
1398
|
-
const tsquery = query ? sql`websearch_to_tsquery('english', ${query})` : void 0;
|
|
1399
|
-
const rank = tsquery ? sql`ts_rank_cd(to_tsvector('english', ${text}), ${tsquery})` : sql`1`;
|
|
1400
|
-
const excerpt = tsquery ? sql`ts_headline('english', ${text}, ${tsquery}, 'MaxFragments=2, MinWords=8, MaxWords=40, FragmentDelimiter=" … ", StartSel=**, StopSel=**')` : sql`left(${text}, ${EXCERPT_WITHOUT_QUERY_CHARS})`;
|
|
1401
|
-
const conditions = [
|
|
1402
|
-
eq(juniorConversations.source, "slack"),
|
|
1403
|
-
isNull(juniorConversations.parentConversationId),
|
|
1404
|
-
isNull(juniorConversations.transcriptPurgedAt),
|
|
1405
|
-
ne(juniorConversations.conversationId, args.currentConversationId),
|
|
1406
|
-
eq(juniorDestinations.provider, args.scope.provider),
|
|
1407
|
-
eq(juniorDestinations.providerTenantId, args.scope.providerTenantId),
|
|
1408
|
-
eq(juniorDestinations.visibility, "public"),
|
|
1409
|
-
eq(juniorConversationEvents.type, "message"),
|
|
1410
|
-
sql`${role} in ('user', 'assistant')`
|
|
1411
|
-
];
|
|
1412
|
-
if (tsquery) conditions.push(sql`to_tsvector('english', ${text}) @@ ${tsquery}`);
|
|
1413
|
-
if (args.filters.channelId) conditions.push(eq(juniorDestinations.providerDestinationId, args.filters.channelId));
|
|
1414
|
-
if (args.filters.afterMs !== void 0 && Number.isFinite(args.filters.afterMs)) conditions.push(gte(juniorConversations.lastActivityAt, new Date(args.filters.afterMs)));
|
|
1415
|
-
if (args.filters.beforeMs !== void 0 && Number.isFinite(args.filters.beforeMs)) conditions.push(lt(juniorConversations.lastActivityAt, new Date(args.filters.beforeMs)));
|
|
1416
|
-
if (args.filters.annotation) {
|
|
1417
|
-
const annotation = args.filters.annotation.toLowerCase();
|
|
1418
|
-
const nestedAnnotationPrefix = `${annotation}#`;
|
|
1419
|
-
conditions.push(sql`exists (
|
|
1420
|
-
select 1
|
|
1421
|
-
from ${juniorConversationAnnotations}
|
|
1422
|
-
where ${and(eq(juniorConversationAnnotations.conversationId, juniorConversations.conversationId), sql`(
|
|
1423
|
-
lower(${juniorConversationAnnotations.key}) = ${annotation}
|
|
1424
|
-
or starts_with(
|
|
1425
|
-
lower(${juniorConversationAnnotations.key}),
|
|
1426
|
-
${nestedAnnotationPrefix}
|
|
1427
|
-
)
|
|
1428
|
-
)`)}
|
|
1429
|
-
)`);
|
|
1430
|
-
}
|
|
1431
|
-
const bestPerConversation = db.selectDistinctOn([juniorConversations.conversationId], {
|
|
1432
|
-
channelName: juniorDestinations.displayName,
|
|
1433
|
-
conversationId: juniorConversations.conversationId,
|
|
1434
|
-
excerpt: excerpt.as("excerpt"),
|
|
1435
|
-
lastActivityAt: juniorConversations.lastActivityAt,
|
|
1436
|
-
messageCreatedAt: juniorConversationEvents.createdAt,
|
|
1437
|
-
messageId: messageId.as("message_id"),
|
|
1438
|
-
providerDestinationId: juniorDestinations.providerDestinationId,
|
|
1439
|
-
rank: rank.as("rank"),
|
|
1440
|
-
role: role.as("role")
|
|
1441
|
-
}).from(juniorConversationEvents).innerJoin(juniorConversations, eq(juniorConversations.conversationId, juniorConversationEvents.conversationId)).innerJoin(juniorDestinations, eq(juniorDestinations.id, juniorConversations.destinationId)).where(and(...conditions)).orderBy(juniorConversations.conversationId, desc(rank), desc(juniorConversationEvents.createdAt)).as("best_conversation_matches");
|
|
1442
|
-
return (await db.select().from(bestPerConversation).orderBy(desc(bestPerConversation.rank), desc(bestPerConversation.lastActivityAt), desc(bestPerConversation.messageCreatedAt)).limit(args.limit)).map((row) => ({
|
|
1443
|
-
conversationId: row.conversationId,
|
|
1444
|
-
excerpt: row.excerpt,
|
|
1445
|
-
messageCreatedAtMs: row.messageCreatedAt.getTime(),
|
|
1446
|
-
messageId: row.messageId,
|
|
1447
|
-
providerDestinationId: row.providerDestinationId,
|
|
1448
|
-
role: row.role,
|
|
1449
|
-
...row.channelName ? { channelName: row.channelName } : void 0
|
|
1450
|
-
}));
|
|
1451
|
-
}
|
|
1452
|
-
};
|
|
1453
|
-
/** Create a SQL-backed public workspace message search store. */
|
|
1454
|
-
function createSqlConversationMessageSearchStore(executor) {
|
|
1455
|
-
return new SqlConversationMessageSearchStore(executor);
|
|
527
|
+
function reportMessageActorIdentity(data) {
|
|
528
|
+
if (data.role !== "user") return void 0;
|
|
529
|
+
const author = reportingMessageAuthorSchema.safeParse(data.meta?.author);
|
|
530
|
+
if (!author.success) return void 0;
|
|
531
|
+
const actorIdentity = {
|
|
532
|
+
...author.data.fullName ? { fullName: author.data.fullName } : void 0,
|
|
533
|
+
...author.data.userId ? { slackUserId: author.data.userId } : void 0,
|
|
534
|
+
...author.data.userName ? { slackUserName: author.data.userName } : void 0
|
|
535
|
+
};
|
|
536
|
+
return Object.keys(actorIdentity).length > 0 ? actorIdentity : void 0;
|
|
1456
537
|
}
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
statementTimeoutMs: args.statementTimeoutMs
|
|
538
|
+
/** Project native assistant tool calls through the existing reporting shape. */
|
|
539
|
+
function reportToolCalls(args) {
|
|
540
|
+
const message = reportingAssistantMessageSchema.safeParse(args.message);
|
|
541
|
+
if (!message.success) return void 0;
|
|
542
|
+
const calls = message.data.content.flatMap((part) => {
|
|
543
|
+
const call = reportingToolCallPartSchema.safeParse(part);
|
|
544
|
+
if (!call.success) return [];
|
|
545
|
+
return [{
|
|
546
|
+
toolCallId: call.data.id,
|
|
547
|
+
name: call.data.name,
|
|
548
|
+
status: "running",
|
|
549
|
+
startedAt: new Date(args.createdAtMs).toISOString(),
|
|
550
|
+
startedSeq: args.seq,
|
|
551
|
+
...args.canExposePayload && call.data.arguments !== void 0 ? { input: call.data.arguments } : void 0
|
|
552
|
+
}];
|
|
1473
553
|
});
|
|
554
|
+
return calls.length > 0 ? {
|
|
555
|
+
type: "tool_calls",
|
|
556
|
+
calls
|
|
557
|
+
} : void 0;
|
|
1474
558
|
}
|
|
1475
|
-
/**
|
|
1476
|
-
function
|
|
1477
|
-
const
|
|
1478
|
-
if (
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
559
|
+
/** Project assistant reasoning with tool ids retained only for display order. */
|
|
560
|
+
function reportAssistantMessage(args) {
|
|
561
|
+
const message = reportingAssistantMessageSchema.safeParse(args.message);
|
|
562
|
+
if (!message.success) return void 0;
|
|
563
|
+
if (!message.data.content.some((part) => {
|
|
564
|
+
const reasoning = reportingReasoningPartSchema.safeParse(part);
|
|
565
|
+
return reasoning.success && reasoning.data.redacted !== true && reasoning.data.thinking.trim().length > 0;
|
|
566
|
+
})) return reportToolCalls(args) ?? {
|
|
567
|
+
type: "assistant_message",
|
|
568
|
+
parts: []
|
|
569
|
+
};
|
|
570
|
+
const reasoningParts = [];
|
|
571
|
+
const assistantParts = [];
|
|
572
|
+
for (const part of message.data.content) {
|
|
573
|
+
const reasoning = reportingReasoningPartSchema.safeParse(part);
|
|
574
|
+
if (reasoning.success) {
|
|
575
|
+
if (reasoning.data.redacted !== true && reasoning.data.thinking.trim().length > 0) {
|
|
576
|
+
const projected = {
|
|
577
|
+
type: "reasoning",
|
|
578
|
+
...args.canExposePayload ? { text: reasoning.data.thinking } : { redacted: true }
|
|
579
|
+
};
|
|
580
|
+
reasoningParts.push(projected);
|
|
581
|
+
assistantParts.push(projected);
|
|
582
|
+
}
|
|
583
|
+
continue;
|
|
1483
584
|
}
|
|
1484
|
-
const
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
585
|
+
const call = reportingToolCallPartSchema.safeParse(part);
|
|
586
|
+
if (call.success) assistantParts.push({
|
|
587
|
+
type: "tool_call",
|
|
588
|
+
toolCallId: call.data.id
|
|
1488
589
|
});
|
|
1489
|
-
current = {
|
|
1490
|
-
databaseUrl: sql.databaseUrl,
|
|
1491
|
-
driver: sql.driver,
|
|
1492
|
-
statementTimeoutMs: sql.statementTimeoutMs,
|
|
1493
|
-
db,
|
|
1494
|
-
store: createSqlStore(db),
|
|
1495
|
-
eventStore: createSqlConversationEventStore(db),
|
|
1496
|
-
searchStore: createSqlConversationMessageSearchStore(db)
|
|
1497
|
-
};
|
|
1498
590
|
}
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
return current.store;
|
|
1509
|
-
}
|
|
1510
|
-
/** Return the canonical SQL-backed conversation event store. */
|
|
1511
|
-
function getConversationEventStore() {
|
|
1512
|
-
getSqlExecutor();
|
|
1513
|
-
return current.eventStore;
|
|
591
|
+
const toolCalls = reportToolCalls(args);
|
|
592
|
+
if (toolCalls?.type === "tool_calls") return {
|
|
593
|
+
...toolCalls,
|
|
594
|
+
assistant: { parts: assistantParts }
|
|
595
|
+
};
|
|
596
|
+
return reasoningParts.length > 0 ? {
|
|
597
|
+
type: "assistant_message",
|
|
598
|
+
parts: reasoningParts
|
|
599
|
+
} : void 0;
|
|
1514
600
|
}
|
|
1515
|
-
/**
|
|
1516
|
-
function
|
|
1517
|
-
|
|
1518
|
-
return
|
|
601
|
+
/** Project a native tool result without exposing host-only result details. */
|
|
602
|
+
function reportToolResult(args) {
|
|
603
|
+
const message = reportingToolResultMessageSchema.safeParse(args.message);
|
|
604
|
+
if (!message.success) return void 0;
|
|
605
|
+
const details = reportingToolResultDetailsSchema.safeParse(message.data.details);
|
|
606
|
+
const outcome = message.data.isError === true || details.success && (details.data.ok === false || details.data.status === "error") ? "error" : "completed";
|
|
607
|
+
const output = modelVisibleToolOutput(message.data.content);
|
|
608
|
+
const name = message.data.toolName ?? message.data.name ?? args.start?.name;
|
|
609
|
+
if (!name) return void 0;
|
|
610
|
+
return {
|
|
611
|
+
type: "tool_calls",
|
|
612
|
+
calls: [{
|
|
613
|
+
toolCallId: message.data.toolCallId,
|
|
614
|
+
name,
|
|
615
|
+
status: outcome,
|
|
616
|
+
...args.start ? {
|
|
617
|
+
startedAt: new Date(args.start.createdAtMs).toISOString(),
|
|
618
|
+
startedSeq: args.start.seq
|
|
619
|
+
} : void 0,
|
|
620
|
+
...args.canExposePayload && output !== void 0 ? { output } : void 0
|
|
621
|
+
}]
|
|
622
|
+
};
|
|
1519
623
|
}
|
|
1520
|
-
/**
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
}
|
|
1526
|
-
//#endregion
|
|
1527
|
-
//#region src/chat/conversations/structured-events.ts
|
|
1528
|
-
/** Reserved namespace for Junior-owned conversation metadata events. */
|
|
1529
|
-
const JUNIOR_NATIVE_EVENT_NAMESPACE = "junior";
|
|
1530
|
-
const authenticationEventContentSchema = z.object({
|
|
1531
|
-
accountLabel: z.string().trim().min(1).max(120).optional(),
|
|
1532
|
-
actorId: z.string().min(1),
|
|
1533
|
-
authorizationId: z.string().min(1).optional(),
|
|
1534
|
-
provider: z.string().regex(/^[a-z][a-z0-9-]*$/),
|
|
1535
|
-
providerLabel: z.string().trim().min(1).max(80).optional()
|
|
1536
|
-
}).strict();
|
|
1537
|
-
const agentsInstructionsUpdatedContentSchema = z.object({
|
|
1538
|
-
action: z.enum([
|
|
1539
|
-
"loaded",
|
|
1540
|
-
"replaced",
|
|
1541
|
-
"cleared"
|
|
1542
|
-
]),
|
|
1543
|
-
directory: z.string().trim().min(1).max(500).optional(),
|
|
1544
|
-
fingerprint: z.string().trim().min(1).max(128),
|
|
1545
|
-
sources: z.array(z.object({
|
|
1546
|
-
content: z.string().min(1).max(32768),
|
|
1547
|
-
path: z.string().trim().min(1).max(500)
|
|
1548
|
-
}).strict()).max(32),
|
|
1549
|
-
textBytes: z.number().int().nonnegative().optional()
|
|
1550
|
-
}).strict();
|
|
1551
|
-
function providerTitle(content) {
|
|
1552
|
-
return content.providerLabel?.trim() || content.provider;
|
|
1553
|
-
}
|
|
1554
|
-
function agentsInstructionsTitle(action) {
|
|
1555
|
-
if (action === "loaded") return "Loaded AGENTS.md";
|
|
1556
|
-
if (action === "replaced") return "Updated AGENTS.md";
|
|
1557
|
-
return "Cleared AGENTS.md";
|
|
1558
|
-
}
|
|
1559
|
-
function sourceFilename(sourcePath) {
|
|
1560
|
-
return sourcePath.split("/").at(-1) || sourcePath;
|
|
1561
|
-
}
|
|
1562
|
-
function formatBytes(bytes) {
|
|
1563
|
-
if (bytes < 1024) return `${bytes} bytes`;
|
|
1564
|
-
const kilobytes = bytes / 1024;
|
|
1565
|
-
return `${Number.isInteger(kilobytes) ? kilobytes : kilobytes.toFixed(1)} KB`;
|
|
1566
|
-
}
|
|
1567
|
-
function agentsInstructionsPreview(content) {
|
|
1568
|
-
const source = content.sources[0];
|
|
1569
|
-
if (!source) return void 0;
|
|
1570
|
-
const filename = sourceFilename(source.path);
|
|
1571
|
-
const label = content.sources.length === 1 ? filename : `${content.sources.length} AGENTS.md files`;
|
|
1572
|
-
return typeof content.textBytes === "number" ? `${label} · ${formatBytes(content.textBytes)}` : label;
|
|
1573
|
-
}
|
|
1574
|
-
/** Junior-owned account-link transcript event. */
|
|
1575
|
-
const authenticationLinkedEvent = defineConversationEvent({
|
|
1576
|
-
name: "authentication_linked",
|
|
1577
|
-
version: 1,
|
|
1578
|
-
schema: authenticationEventContentSchema,
|
|
1579
|
-
renderEvent(event) {
|
|
1580
|
-
const provider = providerTitle(event);
|
|
1581
|
-
return {
|
|
1582
|
-
icon: "link",
|
|
1583
|
-
title: `${provider} connected`,
|
|
1584
|
-
...event.accountLabel ? { preview: `Connected as \`${event.accountLabel}\`` } : void 0,
|
|
1585
|
-
details: [{
|
|
1586
|
-
title: `${provider} connected`,
|
|
1587
|
-
...event.accountLabel ? { description: `Connected as \`${event.accountLabel}\`` } : void 0,
|
|
1588
|
-
metadata: [event.provider]
|
|
1589
|
-
}]
|
|
1590
|
-
};
|
|
1591
|
-
}
|
|
1592
|
-
});
|
|
1593
|
-
/** Junior-owned account-unlink transcript event. */
|
|
1594
|
-
const authenticationUnlinkedEvent = defineConversationEvent({
|
|
1595
|
-
name: "authentication_unlinked",
|
|
1596
|
-
version: 1,
|
|
1597
|
-
schema: authenticationEventContentSchema,
|
|
1598
|
-
renderEvent(event) {
|
|
1599
|
-
const provider = providerTitle(event);
|
|
1600
|
-
return {
|
|
1601
|
-
icon: "key",
|
|
1602
|
-
title: `${provider} disconnected`,
|
|
1603
|
-
details: [{
|
|
1604
|
-
title: `${provider} disconnected`,
|
|
1605
|
-
metadata: [event.provider]
|
|
1606
|
-
}]
|
|
1607
|
-
};
|
|
1608
|
-
}
|
|
1609
|
-
});
|
|
1610
|
-
/** Junior-owned AGENTS.md bootstrap transition for the dashboard timeline. */
|
|
1611
|
-
const agentsInstructionsUpdatedEvent = defineConversationEvent({
|
|
1612
|
-
name: "agents_instructions_updated",
|
|
1613
|
-
version: 1,
|
|
1614
|
-
schema: agentsInstructionsUpdatedContentSchema,
|
|
1615
|
-
renderEvent(event) {
|
|
1616
|
-
const title = agentsInstructionsTitle(event.action);
|
|
1617
|
-
const preview = agentsInstructionsPreview(event);
|
|
1618
|
-
return {
|
|
1619
|
-
icon: "brain",
|
|
1620
|
-
title,
|
|
1621
|
-
...preview ? { preview } : void 0,
|
|
1622
|
-
...event.sources.length > 0 ? { details: event.sources.map((source) => ({
|
|
1623
|
-
title: sourceFilename(source.path),
|
|
1624
|
-
content: source.content
|
|
1625
|
-
})) } : void 0
|
|
1626
|
-
};
|
|
1627
|
-
}
|
|
1628
|
-
});
|
|
1629
|
-
const NATIVE_EVENT_DEFINITIONS = [
|
|
1630
|
-
authenticationLinkedEvent,
|
|
1631
|
-
authenticationUnlinkedEvent,
|
|
1632
|
-
agentsInstructionsUpdatedEvent
|
|
1633
|
-
];
|
|
1634
|
-
/** Resolve a registered Junior-native conversation event definition. */
|
|
1635
|
-
function getJuniorNativeEventDefinition(args) {
|
|
1636
|
-
return NATIVE_EVENT_DEFINITIONS.find((definition) => definition.eventName === args.name && definition.version === args.version);
|
|
1637
|
-
}
|
|
1638
|
-
/** Render one Junior-native event through its active definition. */
|
|
1639
|
-
function renderJuniorNativeConversationEvent(args) {
|
|
1640
|
-
if (args.namespace !== "junior") return void 0;
|
|
1641
|
-
const definition = getJuniorNativeEventDefinition(args);
|
|
1642
|
-
if (!definition) return void 0;
|
|
1643
|
-
const presentation = definition.renderEvent(definition.parse(args.content));
|
|
1644
|
-
return presentation === void 0 ? void 0 : conversationEventPresentationSchema.parse(presentation);
|
|
1645
|
-
}
|
|
1646
|
-
//#endregion
|
|
1647
|
-
//#region src/chat/plugins/annotations.ts
|
|
1648
|
-
function annotationFromRow(row) {
|
|
1649
|
-
const parsed = conversationAnnotationInputSchema.safeParse(row.annotation);
|
|
1650
|
-
if (!parsed.success) return void 0;
|
|
1651
|
-
return {
|
|
1652
|
-
...parsed.data,
|
|
1653
|
-
plugin: row.plugin,
|
|
1654
|
-
createdAt: row.createdAt.toISOString(),
|
|
1655
|
-
updatedAt: row.updatedAt.toISOString()
|
|
1656
|
-
};
|
|
1657
|
-
}
|
|
1658
|
-
function createPluginAnnotations(args) {
|
|
1659
|
-
return {
|
|
1660
|
-
async upsert(input) {
|
|
1661
|
-
const annotation = conversationAnnotationInputSchema.parse(input);
|
|
1662
|
-
const now = /* @__PURE__ */ new Date();
|
|
1663
|
-
await args.db.insert(juniorConversationAnnotations).values({
|
|
1664
|
-
annotation,
|
|
1665
|
-
conversationId: args.conversationId,
|
|
1666
|
-
createdAt: now,
|
|
1667
|
-
key: annotation.key,
|
|
1668
|
-
kind: annotation.kind,
|
|
1669
|
-
plugin: args.plugin,
|
|
1670
|
-
updatedAt: now
|
|
1671
|
-
}).onConflictDoUpdate({
|
|
1672
|
-
target: [
|
|
1673
|
-
juniorConversationAnnotations.conversationId,
|
|
1674
|
-
juniorConversationAnnotations.plugin,
|
|
1675
|
-
juniorConversationAnnotations.kind,
|
|
1676
|
-
juniorConversationAnnotations.key
|
|
1677
|
-
],
|
|
1678
|
-
set: {
|
|
1679
|
-
annotation,
|
|
1680
|
-
updatedAt: now
|
|
1681
|
-
}
|
|
1682
|
-
});
|
|
1683
|
-
},
|
|
1684
|
-
async remove(kind, key) {
|
|
1685
|
-
await args.db.delete(juniorConversationAnnotations).where(and(eq(juniorConversationAnnotations.conversationId, args.conversationId), eq(juniorConversationAnnotations.plugin, args.plugin), eq(juniorConversationAnnotations.kind, kind), eq(juniorConversationAnnotations.key, key)));
|
|
1686
|
-
},
|
|
1687
|
-
async list() {
|
|
1688
|
-
return (await listConversationAnnotations(args.db, args.conversationId)).filter((a) => a.plugin === args.plugin);
|
|
1689
|
-
}
|
|
1690
|
-
};
|
|
1691
|
-
}
|
|
1692
|
-
/** Load stored annotations for one conversation, newest update first. */
|
|
1693
|
-
async function listConversationAnnotations(db, conversationId) {
|
|
1694
|
-
return (await listConversationAnnotationsById(db, [conversationId])).get(conversationId) ?? [];
|
|
1695
|
-
}
|
|
1696
|
-
/** Load stored annotations for a bounded set of conversations. */
|
|
1697
|
-
async function listConversationAnnotationsById(db, conversationIds) {
|
|
1698
|
-
const byConversation = /* @__PURE__ */ new Map();
|
|
1699
|
-
if (conversationIds.length === 0) return byConversation;
|
|
1700
|
-
const rows = await db.select().from(juniorConversationAnnotations).where(inArray(juniorConversationAnnotations.conversationId, [...conversationIds])).orderBy(desc(juniorConversationAnnotations.updatedAt));
|
|
1701
|
-
for (const row of rows) {
|
|
1702
|
-
const annotation = annotationFromRow(row);
|
|
1703
|
-
if (!annotation) continue;
|
|
1704
|
-
const current = byConversation.get(row.conversationId);
|
|
1705
|
-
if (current) current.push(annotation);
|
|
1706
|
-
else byConversation.set(row.conversationId, [annotation]);
|
|
1707
|
-
}
|
|
1708
|
-
return byConversation;
|
|
1709
|
-
}
|
|
1710
|
-
//#endregion
|
|
1711
|
-
//#region src/chat/conversations/annotation-results.ts
|
|
1712
|
-
/** Save object annotations and retain returned facts in the tool result. */
|
|
1713
|
-
async function saveObjectAnnotations(conversationId, plugin, value) {
|
|
1714
|
-
const annotations = objectAnnotationSchema.array().parse(value);
|
|
1715
|
-
await getDb().transaction(async (db) => {
|
|
1716
|
-
const store = createPluginAnnotations({
|
|
1717
|
-
conversationId,
|
|
1718
|
-
plugin,
|
|
1719
|
-
db
|
|
1720
|
-
});
|
|
1721
|
-
for (const annotation of annotations) {
|
|
1722
|
-
await store.upsert(annotation);
|
|
1723
|
-
await store.remove("resource_link", annotation.key);
|
|
1724
|
-
}
|
|
1725
|
-
});
|
|
1726
|
-
return annotations.map((annotation) => ({
|
|
1727
|
-
...annotation,
|
|
1728
|
-
plugin
|
|
1729
|
-
}));
|
|
1730
|
-
}
|
|
1731
|
-
/** Save annotations from a successful plugin tool result and attach their cards. */
|
|
1732
|
-
async function annotateToolResult(conversationId, plugin, result) {
|
|
1733
|
-
if (!isRecord$2(result) || result.isError === true) return result;
|
|
1734
|
-
const details = isRecord$2(result.details) ? result.details : result;
|
|
1735
|
-
if (details.timed_out === true || details.isError === true || !Array.isArray(details.objectAnnotations)) return result;
|
|
1736
|
-
const cards = await saveObjectAnnotations(conversationId, plugin, details.objectAnnotations);
|
|
1737
|
-
const { objectAnnotations: _annotations, ...rest } = details;
|
|
1738
|
-
const annotated = {
|
|
1739
|
-
...rest,
|
|
1740
|
-
objectCards: cards
|
|
1741
|
-
};
|
|
1742
|
-
if (isRecord$2(result.details)) return {
|
|
1743
|
-
...result,
|
|
1744
|
-
details: annotated
|
|
1745
|
-
};
|
|
1746
|
-
return annotated;
|
|
1747
|
-
}
|
|
1748
|
-
//#endregion
|
|
1749
|
-
//#region src/chat/conversations/sql/privacy.ts
|
|
1750
|
-
const privacyRoot = alias(juniorConversations, "privacy_root");
|
|
1751
|
-
/** Accept only a structurally valid persisted root as privacy authority. */
|
|
1752
|
-
async function readRootCandidate(executor, conversationId) {
|
|
1753
|
-
const row = (await executor.db().select({
|
|
1754
|
-
requestedConversationId: juniorConversations.conversationId,
|
|
1755
|
-
requestedParentConversationId: juniorConversations.parentConversationId,
|
|
1756
|
-
requestedRootConversationId: juniorConversations.rootConversationId,
|
|
1757
|
-
rootConversationId: privacyRoot.conversationId,
|
|
1758
|
-
rootDestinationId: privacyRoot.destinationId,
|
|
1759
|
-
rootParentConversationId: privacyRoot.parentConversationId,
|
|
1760
|
-
rootRootConversationId: privacyRoot.rootConversationId
|
|
1761
|
-
}).from(juniorConversations).leftJoin(privacyRoot, eq(privacyRoot.conversationId, juniorConversations.rootConversationId)).where(eq(juniorConversations.conversationId, conversationId)))[0];
|
|
1762
|
-
return row?.requestedRootConversationId !== null && (row?.requestedParentConversationId !== null || row?.requestedRootConversationId === row?.requestedConversationId) && row?.rootConversationId === row.requestedRootConversationId && row.rootRootConversationId === row.rootConversationId && row.rootParentConversationId === null && row.rootDestinationId !== null ? {
|
|
1763
|
-
destinationId: row.rootDestinationId,
|
|
1764
|
-
rootConversationId: row.rootConversationId
|
|
1765
|
-
} : void 0;
|
|
1766
|
-
}
|
|
1767
|
-
async function readCandidateVisibility(executor, candidate) {
|
|
1768
|
-
const destinations = await executor.db().select({ visibility: juniorDestinations.visibility }).from(juniorDestinations).where(eq(juniorDestinations.id, candidate.destinationId)).for("share");
|
|
1769
|
-
return {
|
|
1770
|
-
rootConversationId: candidate.rootConversationId,
|
|
1771
|
-
visibility: destinations[0]?.visibility ?? null
|
|
1772
|
-
};
|
|
1773
|
-
}
|
|
1774
|
-
/**
|
|
1775
|
-
* Resolve a conversation's privacy authority from its persisted root.
|
|
1776
|
-
*
|
|
1777
|
-
* Missing or structurally invalid root metadata fails closed. The root
|
|
1778
|
-
* destination is locked so callers that keep the transaction open receive a
|
|
1779
|
-
* stable visibility decision.
|
|
1780
|
-
*/
|
|
1781
|
-
async function resolveRootVisibility(executor, conversationId) {
|
|
1782
|
-
const candidate = await readRootCandidate(executor, conversationId);
|
|
1783
|
-
if (!candidate) return {
|
|
1784
|
-
rootConversationId: conversationId,
|
|
1785
|
-
visibility: null
|
|
1786
|
-
};
|
|
1787
|
-
return readCandidateVisibility(executor, candidate);
|
|
1788
|
-
}
|
|
1789
|
-
//#endregion
|
|
1790
|
-
//#region src/chat/briefs/events.ts
|
|
1791
|
-
/** Durable cost and size record for one stored Brief version. */
|
|
1792
|
-
const briefUpdatedEvent = defineConversationEvent({
|
|
1793
|
-
name: "brief_updated",
|
|
1794
|
-
version: 1,
|
|
1795
|
-
schema: z.object({
|
|
1796
|
-
version: z.number().int().positive(),
|
|
1797
|
-
modelId: z.string().min(1),
|
|
1798
|
-
costUsd: z.number().finite().nonnegative().optional(),
|
|
1799
|
-
decisions: z.number().int().nonnegative(),
|
|
1800
|
-
openDecisions: z.number().int().nonnegative(),
|
|
1801
|
-
links: z.number().int().nonnegative()
|
|
1802
|
-
}).strict(),
|
|
1803
|
-
renderEvent(event) {
|
|
1804
|
-
return {
|
|
1805
|
-
icon: "activity",
|
|
1806
|
-
title: `Brief updated (v${event.version})`
|
|
1807
|
-
};
|
|
1808
|
-
}
|
|
1809
|
-
});
|
|
1810
|
-
//#endregion
|
|
1811
|
-
//#region src/chat/slack/attachments.ts
|
|
1812
|
-
/** Save the vision summary for a stored Slack attachment. */
|
|
1813
|
-
async function recordSlackAttachment(args) {
|
|
1814
|
-
await args.db.db().update(juniorAttachments).set({ visionSummary: args.visionSummary }).where(and(eq(juniorAttachments.id, args.attachmentId), eq(juniorAttachments.conversationId, args.conversationId), isNull(juniorAttachments.deleteRequestedAt)));
|
|
1815
|
-
}
|
|
1816
|
-
/** Read a saved vision summary for one live attachment. */
|
|
1817
|
-
async function readAttachmentVisionSummary(args) {
|
|
1818
|
-
const [row] = await args.db.db().select({ visionSummary: juniorAttachments.visionSummary }).from(juniorAttachments).where(and(eq(juniorAttachments.id, args.attachmentId), eq(juniorAttachments.conversationId, args.conversationId), isNull(juniorAttachments.deleteRequestedAt)));
|
|
1819
|
-
return row?.visionSummary;
|
|
1820
|
-
}
|
|
1821
|
-
/** Match live conversation attachments to Slack file ids. */
|
|
1822
|
-
async function matchSlackAttachments(args) {
|
|
1823
|
-
if (args.providerIds.length === 0) return [];
|
|
1824
|
-
return await args.db.db().select({
|
|
1825
|
-
id: juniorAttachments.id,
|
|
1826
|
-
providerId: juniorAttachments.providerId,
|
|
1827
|
-
bytes: juniorAttachments.bytes,
|
|
1828
|
-
contentType: juniorAttachments.contentType,
|
|
1829
|
-
filename: juniorAttachments.filename
|
|
1830
|
-
}).from(juniorAttachments).where(and(eq(juniorAttachments.conversationId, args.conversationId), eq(juniorAttachments.provider, "slack"), inArray(juniorAttachments.providerId, args.providerIds), isNull(juniorAttachments.deleteRequestedAt)));
|
|
1831
|
-
}
|
|
1832
|
-
/** Read the Slack file identity retained by the Chat SDK. */
|
|
1833
|
-
function getSlackFileId(attachment) {
|
|
1834
|
-
const metadataId = attachment.fetchMetadata?.fileId;
|
|
1835
|
-
if (metadataId) return metadataId;
|
|
1836
|
-
return (attachment.url ?? attachment.fetchMetadata?.url ?? "").match(/(?:^|[-/])(F[A-Z0-9]+)(?:[-/]|$)/i)?.[1];
|
|
1837
|
-
}
|
|
1838
|
-
//#endregion
|
|
1839
|
-
//#region src/api/conversations/message-attachments.ts
|
|
1840
|
-
/** Resolve saved Slack file references without exposing private storage details. */
|
|
1841
|
-
async function hydrateMessageAttachments(db, conversationId, events) {
|
|
1842
|
-
const messages = events.flatMap((event) => event.data.type === "message" ? [event.data] : []);
|
|
1843
|
-
const ids = [...new Set(messages.flatMap((message) => slackFileIds(message.meta)))];
|
|
1844
|
-
if (!ids.length) return;
|
|
1845
|
-
const attachments = await matchSlackAttachments({
|
|
1846
|
-
db,
|
|
1847
|
-
conversationId,
|
|
1848
|
-
providerIds: ids
|
|
1849
|
-
});
|
|
1850
|
-
const byFileId = new Map(attachments.map(({ providerId, ...attachment }) => [providerId, attachment]));
|
|
1851
|
-
for (const message of messages) {
|
|
1852
|
-
const stored = readMessageAttachments(message.meta?.attachments);
|
|
1853
|
-
const matched = slackFileIds(message.meta).flatMap((id) => {
|
|
1854
|
-
const attachment = byFileId.get(id);
|
|
1855
|
-
return attachment ? [attachment] : [];
|
|
1856
|
-
});
|
|
1857
|
-
if (matched.length) message.meta = {
|
|
1858
|
-
...message.meta,
|
|
1859
|
-
attachments: [...stored, ...matched]
|
|
1860
|
-
};
|
|
1861
|
-
}
|
|
1862
|
-
}
|
|
1863
|
-
function slackFileIds(meta) {
|
|
1864
|
-
const ids = meta?.slackFileIds ?? meta?.imageFileIds;
|
|
1865
|
-
return Array.isArray(ids) ? ids.filter((id) => typeof id === "string") : [];
|
|
1866
|
-
}
|
|
1867
|
-
//#endregion
|
|
1868
|
-
//#region src/chat/sentry-links.ts
|
|
1869
|
-
function getSentryOrgSlug() {
|
|
1870
|
-
return process.env.SENTRY_ORG_SLUG?.trim() || void 0;
|
|
1871
|
-
}
|
|
1872
|
-
function isSentrySaasDsnHost(host) {
|
|
1873
|
-
return host === "sentry.io" || host.endsWith(".sentry.io");
|
|
1874
|
-
}
|
|
1875
|
-
function buildSentryWebBaseUrl(dsn) {
|
|
1876
|
-
if (isSentrySaasDsnHost(dsn.host)) return "https://sentry.io";
|
|
1877
|
-
const port = dsn.port ? `:${dsn.port}` : "";
|
|
1878
|
-
const path = dsn.path ? `/${dsn.path}` : "";
|
|
1879
|
-
return `${dsn.protocol}://${dsn.host}${port}${path}`;
|
|
1880
|
-
}
|
|
1881
|
-
function buildSentryOrgPath(args) {
|
|
1882
|
-
const dsn = getClient$1()?.getDsn();
|
|
1883
|
-
if (!dsn?.host || !dsn.projectId) return;
|
|
1884
|
-
const orgSlug = getSentryOrgSlug();
|
|
1885
|
-
if (!orgSlug) return;
|
|
1886
|
-
const params = new URLSearchParams();
|
|
1887
|
-
params.set("project", dsn.projectId);
|
|
1888
|
-
if (args.query) for (const [key, value] of Object.entries(args.query)) params.set(key, value);
|
|
1889
|
-
const path = `${args.path}?${params.toString()}`;
|
|
1890
|
-
if (isSentrySaasDsnHost(dsn.host)) return `https://${orgSlug}.sentry.io/${path}`;
|
|
1891
|
-
return `${buildSentryWebBaseUrl(dsn)}/organizations/${orgSlug}/${path}`;
|
|
1892
|
-
}
|
|
1893
|
-
/** Build a Sentry conversation URL only when the runtime has enough Sentry config. */
|
|
1894
|
-
function buildSentryConversationUrl(conversationId) {
|
|
1895
|
-
return buildSentryOrgPath({ path: `explore/conversations/${encodeURIComponent(conversationId)}/` });
|
|
1896
|
-
}
|
|
1897
|
-
/** Build a Sentry event URL only when the runtime has enough Sentry config. */
|
|
1898
|
-
function buildSentryEventUrl(eventId) {
|
|
1899
|
-
return buildSentryOrgPath({
|
|
1900
|
-
path: "issues/",
|
|
1901
|
-
query: { query: eventId }
|
|
1902
|
-
});
|
|
1903
|
-
}
|
|
1904
|
-
//#endregion
|
|
1905
|
-
//#region src/api/conversations/event-model.ts
|
|
1906
|
-
/** Apply recorded model choices without carrying a previous turn's settings forward. */
|
|
1907
|
-
function conversationEventModel(data, current) {
|
|
1908
|
-
if (data.type === "turn_started") return void 0;
|
|
1909
|
-
if (data.type === "turn_routed" || data.type === "handoff") return {
|
|
1910
|
-
modelId: data.modelId,
|
|
1911
|
-
modelProfile: data.modelProfile,
|
|
1912
|
-
...data.reasoningLevel ? { reasoningLevel: data.reasoningLevel } : void 0
|
|
1913
|
-
};
|
|
1914
|
-
if (data.type === "assistant_message") {
|
|
1915
|
-
const model = text$1(data.model);
|
|
1916
|
-
const provider = text$1(data.provider);
|
|
1917
|
-
if (model) {
|
|
1918
|
-
const modelId = model.includes("/") || !provider ? model : `${provider}/${model}`;
|
|
1919
|
-
return {
|
|
1920
|
-
...current,
|
|
1921
|
-
modelId
|
|
1922
|
-
};
|
|
1923
|
-
}
|
|
1924
|
-
}
|
|
1925
|
-
return current;
|
|
1926
|
-
}
|
|
1927
|
-
function text$1(value) {
|
|
1928
|
-
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
1929
|
-
}
|
|
1930
|
-
/** Allowlist model-call diagnostics; never expose opaque provider metadata. */
|
|
1931
|
-
function conversationEventModelCall(data) {
|
|
1932
|
-
if (data.type !== "assistant_message") return void 0;
|
|
1933
|
-
const usage = extractGenAiUsageSummary(data);
|
|
1934
|
-
const provider = text$1(data.provider);
|
|
1935
|
-
const api = text$1(data.api);
|
|
1936
|
-
const stopReason = text$1(data.stopReason);
|
|
1937
|
-
return {
|
|
1938
|
-
...provider ? { provider } : void 0,
|
|
1939
|
-
...api ? { api } : void 0,
|
|
1940
|
-
...stopReason ? { stopReason } : void 0,
|
|
1941
|
-
...hasAgentTurnUsage(usage) ? { usage } : void 0
|
|
1942
|
-
};
|
|
1943
|
-
}
|
|
1944
|
-
//#endregion
|
|
1945
|
-
//#region src/api/conversations/events.ts
|
|
1946
|
-
/** Canonical event types that can contribute to the reporting projection. */
|
|
1947
|
-
const conversationReportSourceEventTypes = [
|
|
1948
|
-
"message",
|
|
1949
|
-
"message_handled",
|
|
1950
|
-
"assistant_message",
|
|
1951
|
-
"tool_result",
|
|
1952
|
-
"tool_execution_started",
|
|
1953
|
-
"guardian_action_reviewed",
|
|
1954
|
-
"turn_started",
|
|
1955
|
-
"turn_context",
|
|
1956
|
-
"structured_event",
|
|
1957
|
-
"attachments_delivered",
|
|
1958
|
-
"turn_routed",
|
|
1959
|
-
"turn_completed",
|
|
1960
|
-
"turn_failed",
|
|
1961
|
-
"compaction",
|
|
1962
|
-
"handoff",
|
|
1963
|
-
"subagent_started",
|
|
1964
|
-
"subagent_ended"
|
|
1965
|
-
];
|
|
1966
|
-
const reportingAssistantMessageSchema = z.object({
|
|
1967
|
-
type: z.literal("assistant_message"),
|
|
1968
|
-
content: z.array(z.unknown())
|
|
1969
|
-
}).passthrough();
|
|
1970
|
-
const reportingToolCallPartSchema = z.object({
|
|
1971
|
-
type: z.literal("toolCall"),
|
|
1972
|
-
id: z.string().min(1),
|
|
1973
|
-
name: z.string().min(1),
|
|
1974
|
-
arguments: z.unknown()
|
|
1975
|
-
}).passthrough();
|
|
1976
|
-
const reportingToolResultMessageSchema = z.object({
|
|
1977
|
-
type: z.literal("tool_result"),
|
|
1978
|
-
toolCallId: z.string().min(1),
|
|
1979
|
-
toolName: z.string().min(1).optional(),
|
|
1980
|
-
name: z.string().min(1).optional(),
|
|
1981
|
-
content: z.array(z.unknown()),
|
|
1982
|
-
details: z.unknown().optional(),
|
|
1983
|
-
isError: z.boolean().optional()
|
|
1984
|
-
}).passthrough();
|
|
1985
|
-
const reportingToolResultDetailsSchema = z.object({
|
|
1986
|
-
ok: z.boolean().optional(),
|
|
1987
|
-
status: z.string().optional()
|
|
1988
|
-
}).passthrough();
|
|
1989
|
-
const reportingTextPartSchema = z.object({
|
|
1990
|
-
type: z.literal("text"),
|
|
1991
|
-
text: z.string()
|
|
1992
|
-
}).passthrough();
|
|
1993
|
-
const reportingReasoningPartSchema = z.object({
|
|
1994
|
-
type: z.literal("thinking"),
|
|
1995
|
-
thinking: z.string(),
|
|
1996
|
-
redacted: z.literal(true).optional()
|
|
1997
|
-
}).passthrough();
|
|
1998
|
-
const reportingMediaPartSchema = z.object({
|
|
1999
|
-
type: z.enum(["image", "audio"]),
|
|
2000
|
-
mimeType: z.string().min(1).optional()
|
|
2001
|
-
}).passthrough();
|
|
2002
|
-
const reportingMessageAuthorSchema = z.object({
|
|
2003
|
-
fullName: z.string().min(1).optional(),
|
|
2004
|
-
userId: z.string().min(1).optional(),
|
|
2005
|
-
userName: z.string().min(1).optional()
|
|
2006
|
-
}).passthrough();
|
|
2007
|
-
/** Retain model-visible text and media descriptors, dropping all opaque fields. */
|
|
2008
|
-
function sanitizeModelContentPart(value) {
|
|
2009
|
-
const text = reportingTextPartSchema.safeParse(value);
|
|
2010
|
-
if (text.success) return {
|
|
2011
|
-
type: "text",
|
|
2012
|
-
text: text.data.text
|
|
2013
|
-
};
|
|
2014
|
-
const media = reportingMediaPartSchema.safeParse(value);
|
|
2015
|
-
if (!media.success) return void 0;
|
|
2016
|
-
return {
|
|
2017
|
-
type: media.data.type,
|
|
2018
|
-
...media.data.mimeType ? { mimeType: media.data.mimeType } : void 0
|
|
2019
|
-
};
|
|
2020
|
-
}
|
|
2021
|
-
/** Convert allowlisted Pi content parts into the reporting result value. */
|
|
2022
|
-
function modelVisibleToolOutput(content) {
|
|
2023
|
-
const sanitized = content.map(sanitizeModelContentPart).filter((part) => part !== void 0);
|
|
2024
|
-
if (sanitized.length === 0) return void 0;
|
|
2025
|
-
if (sanitized.length !== 1) return sanitized;
|
|
2026
|
-
const only = sanitized[0];
|
|
2027
|
-
return only.type === "text" ? only.text : only;
|
|
2028
|
-
}
|
|
2029
|
-
function reportMessageActorIdentity(data) {
|
|
2030
|
-
if (data.role !== "user") return void 0;
|
|
2031
|
-
const author = reportingMessageAuthorSchema.safeParse(data.meta?.author);
|
|
2032
|
-
if (!author.success) return void 0;
|
|
2033
|
-
const actorIdentity = {
|
|
2034
|
-
...author.data.fullName ? { fullName: author.data.fullName } : void 0,
|
|
2035
|
-
...author.data.userId ? { slackUserId: author.data.userId } : void 0,
|
|
2036
|
-
...author.data.userName ? { slackUserName: author.data.userName } : void 0
|
|
2037
|
-
};
|
|
2038
|
-
return Object.keys(actorIdentity).length > 0 ? actorIdentity : void 0;
|
|
2039
|
-
}
|
|
2040
|
-
/** Project native assistant tool calls through the existing reporting shape. */
|
|
2041
|
-
function reportToolCalls(args) {
|
|
2042
|
-
const message = reportingAssistantMessageSchema.safeParse(args.message);
|
|
2043
|
-
if (!message.success) return void 0;
|
|
2044
|
-
const calls = message.data.content.flatMap((part) => {
|
|
2045
|
-
const call = reportingToolCallPartSchema.safeParse(part);
|
|
2046
|
-
if (!call.success) return [];
|
|
2047
|
-
return [{
|
|
2048
|
-
toolCallId: call.data.id,
|
|
2049
|
-
name: call.data.name,
|
|
2050
|
-
status: "running",
|
|
2051
|
-
startedAt: new Date(args.createdAtMs).toISOString(),
|
|
2052
|
-
startedSeq: args.seq,
|
|
2053
|
-
...args.canExposePayload && call.data.arguments !== void 0 ? { input: call.data.arguments } : void 0
|
|
2054
|
-
}];
|
|
2055
|
-
});
|
|
2056
|
-
return calls.length > 0 ? {
|
|
2057
|
-
type: "tool_calls",
|
|
2058
|
-
calls
|
|
2059
|
-
} : void 0;
|
|
2060
|
-
}
|
|
2061
|
-
/** Project assistant reasoning with tool ids retained only for display order. */
|
|
2062
|
-
function reportAssistantMessage(args) {
|
|
2063
|
-
const message = reportingAssistantMessageSchema.safeParse(args.message);
|
|
2064
|
-
if (!message.success) return void 0;
|
|
2065
|
-
if (!message.data.content.some((part) => {
|
|
2066
|
-
const reasoning = reportingReasoningPartSchema.safeParse(part);
|
|
2067
|
-
return reasoning.success && reasoning.data.redacted !== true && reasoning.data.thinking.trim().length > 0;
|
|
2068
|
-
})) return reportToolCalls(args) ?? {
|
|
2069
|
-
type: "assistant_message",
|
|
2070
|
-
parts: []
|
|
2071
|
-
};
|
|
2072
|
-
const reasoningParts = [];
|
|
2073
|
-
const assistantParts = [];
|
|
2074
|
-
for (const part of message.data.content) {
|
|
2075
|
-
const reasoning = reportingReasoningPartSchema.safeParse(part);
|
|
2076
|
-
if (reasoning.success) {
|
|
2077
|
-
if (reasoning.data.redacted !== true && reasoning.data.thinking.trim().length > 0) {
|
|
2078
|
-
const projected = {
|
|
2079
|
-
type: "reasoning",
|
|
2080
|
-
...args.canExposePayload ? { text: reasoning.data.thinking } : { redacted: true }
|
|
2081
|
-
};
|
|
2082
|
-
reasoningParts.push(projected);
|
|
2083
|
-
assistantParts.push(projected);
|
|
2084
|
-
}
|
|
2085
|
-
continue;
|
|
2086
|
-
}
|
|
2087
|
-
const call = reportingToolCallPartSchema.safeParse(part);
|
|
2088
|
-
if (call.success) assistantParts.push({
|
|
2089
|
-
type: "tool_call",
|
|
2090
|
-
toolCallId: call.data.id
|
|
2091
|
-
});
|
|
2092
|
-
}
|
|
2093
|
-
const toolCalls = reportToolCalls(args);
|
|
2094
|
-
if (toolCalls?.type === "tool_calls") return {
|
|
2095
|
-
...toolCalls,
|
|
2096
|
-
assistant: { parts: assistantParts }
|
|
2097
|
-
};
|
|
2098
|
-
return reasoningParts.length > 0 ? {
|
|
2099
|
-
type: "assistant_message",
|
|
2100
|
-
parts: reasoningParts
|
|
2101
|
-
} : void 0;
|
|
2102
|
-
}
|
|
2103
|
-
/** Project a native tool result without exposing host-only result details. */
|
|
2104
|
-
function reportToolResult(args) {
|
|
2105
|
-
const message = reportingToolResultMessageSchema.safeParse(args.message);
|
|
2106
|
-
if (!message.success) return void 0;
|
|
2107
|
-
const details = reportingToolResultDetailsSchema.safeParse(message.data.details);
|
|
2108
|
-
const outcome = message.data.isError === true || details.success && (details.data.ok === false || details.data.status === "error") ? "error" : "completed";
|
|
2109
|
-
const output = modelVisibleToolOutput(message.data.content);
|
|
2110
|
-
const name = message.data.toolName ?? message.data.name ?? args.start?.name;
|
|
2111
|
-
if (!name) return void 0;
|
|
2112
|
-
return {
|
|
2113
|
-
type: "tool_calls",
|
|
2114
|
-
calls: [{
|
|
2115
|
-
toolCallId: message.data.toolCallId,
|
|
2116
|
-
name,
|
|
2117
|
-
status: outcome,
|
|
2118
|
-
...args.start ? {
|
|
2119
|
-
startedAt: new Date(args.start.createdAtMs).toISOString(),
|
|
2120
|
-
startedSeq: args.start.seq
|
|
2121
|
-
} : void 0,
|
|
2122
|
-
...args.canExposePayload && output !== void 0 ? { output } : void 0
|
|
2123
|
-
}]
|
|
2124
|
-
};
|
|
2125
|
-
}
|
|
2126
|
-
/** Return tool result ids that may need start metadata from outside a page. */
|
|
2127
|
-
function conversationReportToolResultIds(events) {
|
|
2128
|
-
return [...new Set(events.flatMap((event) => {
|
|
2129
|
-
const result = reportingToolResultMessageSchema.safeParse(event.data);
|
|
2130
|
-
return result.success ? [result.data.toolCallId] : [];
|
|
2131
|
-
}))];
|
|
624
|
+
/** Return tool result ids that may need start metadata from outside a page. */
|
|
625
|
+
function conversationReportToolResultIds(events) {
|
|
626
|
+
return [...new Set(events.flatMap((event) => {
|
|
627
|
+
const result = reportingToolResultMessageSchema.safeParse(event.data);
|
|
628
|
+
return result.success ? [result.data.toolCallId] : [];
|
|
629
|
+
}))];
|
|
2132
630
|
}
|
|
2133
631
|
function reportEventData(args) {
|
|
2134
632
|
const { data } = args;
|
|
@@ -3294,982 +1792,945 @@ async function updateTimerIndex(state, operation) {
|
|
|
3294
1792
|
}
|
|
3295
1793
|
}
|
|
3296
1794
|
//#endregion
|
|
3297
|
-
//#region src/chat/
|
|
3298
|
-
const
|
|
3299
|
-
|
|
3300
|
-
|
|
3301
|
-
|
|
3302
|
-
|
|
3303
|
-
const SUBSCRIPTION_LOCK_HEARTBEAT_MS = 3e3;
|
|
3304
|
-
const subscriptionStatusSchema = z.enum([
|
|
3305
|
-
"active",
|
|
3306
|
-
"cancelled",
|
|
3307
|
-
"completed"
|
|
3308
|
-
]);
|
|
3309
|
-
const subscriptionIdIndexSchema = z.array(z.string().min(1));
|
|
3310
|
-
const subscriptionSchema = z.object({
|
|
3311
|
-
conversationId: z.string().min(1),
|
|
3312
|
-
createdAtMs: z.number().finite(),
|
|
3313
|
-
events: z.array(eventTypeSchema).min(1),
|
|
3314
|
-
expiresAtMs: z.number().finite(),
|
|
3315
|
-
firesAtMs: z.number().finite().optional(),
|
|
3316
|
-
id: z.string().min(1),
|
|
3317
|
-
intent: z.string().min(1),
|
|
3318
|
-
label: z.string().min(1),
|
|
3319
|
-
match: eventMatchSchema.optional(),
|
|
3320
|
-
namespace: z.string().min(1),
|
|
3321
|
-
identifier: z.string().min(1),
|
|
3322
|
-
resourceType: z.string().min(1),
|
|
3323
|
-
status: subscriptionStatusSchema,
|
|
3324
|
-
updatedAtMs: z.number().finite()
|
|
3325
|
-
}).strict();
|
|
3326
|
-
function digest(value) {
|
|
3327
|
-
return createHash("sha256").update(value).digest("hex").slice(0, 32);
|
|
3328
|
-
}
|
|
3329
|
-
function subscriptionKey(id) {
|
|
3330
|
-
return `${EVENT_PREFIX}:record:${id}`;
|
|
3331
|
-
}
|
|
3332
|
-
function subscriptionLockKey(id) {
|
|
3333
|
-
return `${EVENT_PREFIX}:lock:${id}`;
|
|
3334
|
-
}
|
|
3335
|
-
function resourceIndexKey(namespace, identifier) {
|
|
3336
|
-
return `${EVENT_PREFIX}:resource:${digest(`${namespace}\0${identifier}`)}`;
|
|
3337
|
-
}
|
|
3338
|
-
function conversationIndexKey(conversationId) {
|
|
3339
|
-
return `${EVENT_PREFIX}:conversation:${digest(conversationId)}`;
|
|
1795
|
+
//#region src/chat/slack/workspace-context.ts
|
|
1796
|
+
const workspaceTeamIdStorage = new AsyncLocalStorage();
|
|
1797
|
+
/** Run a callback with the Slack workspace team ID for the inbound webhook. */
|
|
1798
|
+
function runWithWorkspaceTeamId(teamId, fn) {
|
|
1799
|
+
if (!teamId) return fn();
|
|
1800
|
+
return workspaceTeamIdStorage.run(teamId, fn);
|
|
3340
1801
|
}
|
|
3341
|
-
|
|
3342
|
-
|
|
1802
|
+
/** Return the Slack workspace team ID for the current inbound webhook. */
|
|
1803
|
+
function getWorkspaceTeamId() {
|
|
1804
|
+
return workspaceTeamIdStorage.getStore();
|
|
3343
1805
|
}
|
|
3344
|
-
|
|
3345
|
-
|
|
3346
|
-
|
|
3347
|
-
|
|
1806
|
+
//#endregion
|
|
1807
|
+
//#region src/chat/slack/client.ts
|
|
1808
|
+
var SlackActionError = class extends Error {
|
|
1809
|
+
code;
|
|
1810
|
+
apiError;
|
|
1811
|
+
needed;
|
|
1812
|
+
provided;
|
|
1813
|
+
statusCode;
|
|
1814
|
+
requestId;
|
|
1815
|
+
errorData;
|
|
1816
|
+
retryAfterSeconds;
|
|
1817
|
+
detail;
|
|
1818
|
+
detailLine;
|
|
1819
|
+
detailRule;
|
|
1820
|
+
constructor(message, code, options = {}) {
|
|
1821
|
+
super(message);
|
|
1822
|
+
this.name = "SlackActionError";
|
|
1823
|
+
this.code = code;
|
|
1824
|
+
this.apiError = options.apiError;
|
|
1825
|
+
this.needed = options.needed;
|
|
1826
|
+
this.provided = options.provided;
|
|
1827
|
+
this.statusCode = options.statusCode;
|
|
1828
|
+
this.requestId = options.requestId;
|
|
1829
|
+
this.errorData = options.errorData;
|
|
1830
|
+
this.retryAfterSeconds = options.retryAfterSeconds;
|
|
1831
|
+
this.detail = options.detail;
|
|
1832
|
+
this.detailLine = options.detailLine;
|
|
1833
|
+
this.detailRule = options.detailRule;
|
|
1834
|
+
}
|
|
1835
|
+
};
|
|
1836
|
+
function serializeSlackErrorData(data) {
|
|
1837
|
+
if (!data || typeof data !== "object") return;
|
|
1838
|
+
const filtered = Object.fromEntries(Object.entries(data).filter(([key]) => key !== "error"));
|
|
1839
|
+
if (Object.keys(filtered).length === 0) return;
|
|
1840
|
+
try {
|
|
1841
|
+
const serialized = JSON.stringify(filtered);
|
|
1842
|
+
return serialized.length <= 600 ? serialized : `${serialized.slice(0, 597)}...`;
|
|
1843
|
+
} catch {
|
|
1844
|
+
return;
|
|
1845
|
+
}
|
|
3348
1846
|
}
|
|
3349
|
-
|
|
3350
|
-
|
|
3351
|
-
|
|
3352
|
-
|
|
3353
|
-
|
|
3354
|
-
|
|
1847
|
+
/** Extract a header value by case-insensitive name from a raw headers object. */
|
|
1848
|
+
function getHeaderString(headers, name) {
|
|
1849
|
+
if (!headers || typeof headers !== "object") return;
|
|
1850
|
+
const key = name.toLowerCase();
|
|
1851
|
+
const entries = headers;
|
|
1852
|
+
for (const [entryKey, value] of Object.entries(entries)) {
|
|
1853
|
+
if (entryKey.toLowerCase() !== key) continue;
|
|
1854
|
+
if (typeof value === "string") return value;
|
|
1855
|
+
if (Array.isArray(value)) {
|
|
1856
|
+
const first = value.find((entry) => typeof entry === "string");
|
|
1857
|
+
return typeof first === "string" ? first : void 0;
|
|
1858
|
+
}
|
|
3355
1859
|
}
|
|
3356
1860
|
}
|
|
3357
|
-
|
|
3358
|
-
|
|
3359
|
-
const
|
|
3360
|
-
|
|
3361
|
-
|
|
3362
|
-
|
|
3363
|
-
|
|
3364
|
-
|
|
3365
|
-
|
|
3366
|
-
if (extended) heartbeatError = void 0;
|
|
3367
|
-
else lockLostError = /* @__PURE__ */ new Error(`Watch lock was lost for ${subscriptionId}`);
|
|
3368
|
-
}).catch((error) => {
|
|
3369
|
-
heartbeatError = error instanceof Error ? error : /* @__PURE__ */ new Error(`Failed to extend subscription lock: ${String(error)}`);
|
|
3370
|
-
}).finally(() => {
|
|
3371
|
-
heartbeat = void 0;
|
|
3372
|
-
});
|
|
3373
|
-
}, SUBSCRIPTION_LOCK_HEARTBEAT_MS);
|
|
3374
|
-
timer.unref?.();
|
|
3375
|
-
let result;
|
|
3376
|
-
try {
|
|
3377
|
-
result = await callback();
|
|
3378
|
-
} finally {
|
|
3379
|
-
clearInterval(timer);
|
|
3380
|
-
await heartbeat;
|
|
3381
|
-
await state.releaseLock(lock);
|
|
1861
|
+
function parseSlackCanvasDetail(detail) {
|
|
1862
|
+
if (typeof detail !== "string") return {};
|
|
1863
|
+
const trimmed = detail.trim();
|
|
1864
|
+
if (!trimmed) return {};
|
|
1865
|
+
const parsed = { detail: trimmed };
|
|
1866
|
+
const lineMatch = trimmed.match(/line\s+(\d+):/i);
|
|
1867
|
+
if (lineMatch) {
|
|
1868
|
+
const line = Number.parseInt(lineMatch[1] ?? "", 10);
|
|
1869
|
+
if (Number.isFinite(line)) parsed.detailLine = line;
|
|
3382
1870
|
}
|
|
3383
|
-
|
|
3384
|
-
|
|
3385
|
-
return result;
|
|
1871
|
+
if (/unsupported heading depth/i.test(trimmed)) parsed.detailRule = "unsupported_heading_depth";
|
|
1872
|
+
return parsed;
|
|
3386
1873
|
}
|
|
3387
|
-
|
|
3388
|
-
|
|
1874
|
+
/**
|
|
1875
|
+
* Ambient destination-installation token for Slack outbound calls, bound by
|
|
1876
|
+
* installation-scoped entry points so writes carry the destination
|
|
1877
|
+
* workspace's credentials instead of the process-global env token.
|
|
1878
|
+
*/
|
|
1879
|
+
const installationTokenStorage = new AsyncLocalStorage();
|
|
1880
|
+
const clientsByToken = /* @__PURE__ */ new Map();
|
|
1881
|
+
/**
|
|
1882
|
+
* Bind a workspace installation token for all Slack Web API calls inside
|
|
1883
|
+
* `fn`, so multi-workspace outbound writes use the destination workspace's
|
|
1884
|
+
* credentials rather than the env fallback token.
|
|
1885
|
+
*/
|
|
1886
|
+
function runWithSlackInstallationToken(token, fn) {
|
|
1887
|
+
const trimmed = token.trim();
|
|
1888
|
+
if (!trimmed) throw new SlackActionError("Slack installation token binding requires a non-empty token", "missing_token");
|
|
1889
|
+
return installationTokenStorage.run({ token: trimmed }, fn);
|
|
3389
1890
|
}
|
|
3390
|
-
|
|
3391
|
-
|
|
3392
|
-
|
|
3393
|
-
|
|
1891
|
+
/**
|
|
1892
|
+
* Token precedence: ambient installation token, then env token, then a hard
|
|
1893
|
+
* failure for team-scoped calls — a call scoped to one workspace must never
|
|
1894
|
+
* silently fall through to another workspace's credentials.
|
|
1895
|
+
*/
|
|
1896
|
+
function resolveSlackToken() {
|
|
1897
|
+
const ambientToken = installationTokenStorage.getStore()?.token;
|
|
1898
|
+
if (ambientToken) return ambientToken;
|
|
1899
|
+
const envToken = getSlackBotToken();
|
|
1900
|
+
if (envToken) return envToken;
|
|
1901
|
+
const teamId = getWorkspaceTeamId();
|
|
1902
|
+
if (teamId) throw new SlackActionError(`Slack call is scoped to workspace ${teamId} but no installation token is bound and no default bot token is configured`, "missing_token");
|
|
1903
|
+
throw new SlackActionError("SLACK_BOT_TOKEN (or SLACK_BOT_USER_TOKEN) is required for Slack Web API actions in this service", "missing_token");
|
|
3394
1904
|
}
|
|
3395
|
-
|
|
3396
|
-
|
|
3397
|
-
|
|
3398
|
-
return `resub_${digest(matchKey ? `${input.namespace}\0${input.identifier}\0${input.conversationId}\0${eventKey}\0${matchKey}` : `${input.namespace}\0${input.identifier}\0${input.conversationId}\0${eventKey}`)}`;
|
|
1905
|
+
/** Normalize Junior Slack references to native Slack conversation IDs. */
|
|
1906
|
+
function normalizeSlackConversationId(channelId) {
|
|
1907
|
+
return parseSlackChannelReferenceId(channelId);
|
|
3399
1908
|
}
|
|
3400
|
-
|
|
3401
|
-
|
|
3402
|
-
|
|
3403
|
-
|
|
3404
|
-
|
|
3405
|
-
|
|
3406
|
-
|
|
1909
|
+
/**
|
|
1910
|
+
* Return the per-token cached WebClient. `withSlackRetries` owns retry
|
|
1911
|
+
* classification for this boundary: the WebClient's built-in policy would
|
|
1912
|
+
* re-post timed-out writes (a duplicate-message hazard) and sleep for the
|
|
1913
|
+
* full unbounded Retry-After inside the request, so both internal behaviors
|
|
1914
|
+
* are disabled here.
|
|
1915
|
+
*/
|
|
1916
|
+
function getClient() {
|
|
1917
|
+
const token = resolveSlackToken();
|
|
1918
|
+
let cached = clientsByToken.get(token);
|
|
1919
|
+
if (!cached) {
|
|
1920
|
+
cached = new WebClient(token, {
|
|
1921
|
+
retryConfig: { retries: 0 },
|
|
1922
|
+
rejectRateLimitedCalls: true
|
|
1923
|
+
});
|
|
1924
|
+
clientsByToken.set(token, cached);
|
|
3407
1925
|
}
|
|
1926
|
+
return cached;
|
|
3408
1927
|
}
|
|
3409
|
-
|
|
3410
|
-
|
|
3411
|
-
|
|
3412
|
-
|
|
3413
|
-
|
|
1928
|
+
function mapSlackError(error) {
|
|
1929
|
+
if (error instanceof SlackActionError) return error;
|
|
1930
|
+
const candidate = error;
|
|
1931
|
+
const apiError = candidate.data?.error;
|
|
1932
|
+
const message = candidate.message ?? "Slack action failed";
|
|
1933
|
+
const baseOptions = {
|
|
1934
|
+
apiError,
|
|
1935
|
+
statusCode: candidate.statusCode,
|
|
1936
|
+
requestId: getHeaderString(candidate.headers, "x-slack-req-id"),
|
|
1937
|
+
errorData: serializeSlackErrorData(candidate.data),
|
|
1938
|
+
...parseSlackCanvasDetail(candidate.data?.detail)
|
|
1939
|
+
};
|
|
1940
|
+
if (apiError === "missing_scope") return new SlackActionError(message, "missing_scope", {
|
|
1941
|
+
...baseOptions,
|
|
1942
|
+
needed: candidate.data?.needed,
|
|
1943
|
+
provided: candidate.data?.provided
|
|
3414
1944
|
});
|
|
3415
|
-
|
|
3416
|
-
|
|
3417
|
-
|
|
3418
|
-
|
|
3419
|
-
|
|
1945
|
+
if (apiError === "not_in_channel") return new SlackActionError(message, "not_in_channel", baseOptions);
|
|
1946
|
+
if (apiError === "restricted_action_read_only_channel") return new SlackActionError(message, "read_only_channel", baseOptions);
|
|
1947
|
+
if (apiError === "already_reacted") return new SlackActionError(message, "already_reacted", baseOptions);
|
|
1948
|
+
if (apiError === "no_reaction") return new SlackActionError(message, "no_reaction", baseOptions);
|
|
1949
|
+
if (apiError === "invalid_arguments") return new SlackActionError(message, "invalid_arguments", baseOptions);
|
|
1950
|
+
if (apiError === "invalid_cursor") return new SlackActionError(message, "invalid_arguments", baseOptions);
|
|
1951
|
+
if (apiError === "invalid_name") return new SlackActionError(message, "invalid_arguments", baseOptions);
|
|
1952
|
+
if (apiError === "not_found" || apiError === "channel_not_found" || apiError === "message_not_found") return new SlackActionError(message, "not_found", baseOptions);
|
|
1953
|
+
if (apiError === "feature_not_enabled" || apiError === "not_allowed_token_type") return new SlackActionError(message, "feature_unavailable", baseOptions);
|
|
1954
|
+
if (apiError === "canvas_creation_failed") return new SlackActionError(message, "canvas_creation_failed", baseOptions);
|
|
1955
|
+
if (apiError === "canvas_editing_failed") return new SlackActionError(message, "canvas_editing_failed", baseOptions);
|
|
1956
|
+
if (candidate.code === "slack_webapi_rate_limited_error" || candidate.statusCode === 429) return new SlackActionError(message, "rate_limited", {
|
|
1957
|
+
...baseOptions,
|
|
1958
|
+
retryAfterSeconds: candidate.retryAfter
|
|
3420
1959
|
});
|
|
1960
|
+
return new SlackActionError(message, "internal_error", baseOptions);
|
|
3421
1961
|
}
|
|
3422
|
-
|
|
3423
|
-
|
|
3424
|
-
|
|
3425
|
-
|
|
3426
|
-
|
|
3427
|
-
|
|
3428
|
-
|
|
3429
|
-
|
|
3430
|
-
|
|
3431
|
-
|
|
3432
|
-
|
|
3433
|
-
|
|
3434
|
-
|
|
3435
|
-
|
|
3436
|
-
|
|
3437
|
-
|
|
3438
|
-
|
|
3439
|
-
|
|
3440
|
-
const nowMs = options.nowMs ?? Date.now();
|
|
3441
|
-
const events = [...new Set(input.events.map((event) => event.trim()))].filter(Boolean);
|
|
3442
|
-
if (events.length === 0) throw new Error("Watch requires at least one event");
|
|
3443
|
-
if (input.expiresAtMs <= nowMs) throw new Error("Watch expiry must be in the future");
|
|
3444
|
-
const match = input.match && Object.keys(input.match).length > 0 ? input.match : void 0;
|
|
3445
|
-
const id = buildSubscriptionId({
|
|
3446
|
-
conversationId: input.conversationId,
|
|
3447
|
-
events,
|
|
3448
|
-
match,
|
|
3449
|
-
namespace: input.namespace,
|
|
3450
|
-
identifier: input.identifier
|
|
3451
|
-
});
|
|
3452
|
-
const record = {
|
|
3453
|
-
conversationId: input.conversationId,
|
|
3454
|
-
createdAtMs: nowMs,
|
|
3455
|
-
events,
|
|
3456
|
-
expiresAtMs: input.expiresAtMs,
|
|
3457
|
-
id,
|
|
3458
|
-
intent: input.intent,
|
|
3459
|
-
label: input.label,
|
|
3460
|
-
...match ? { match } : void 0,
|
|
3461
|
-
namespace: input.namespace,
|
|
3462
|
-
identifier: input.identifier,
|
|
3463
|
-
resourceType: input.resourceType,
|
|
3464
|
-
status: "active",
|
|
3465
|
-
updatedAtMs: nowMs
|
|
3466
|
-
};
|
|
3467
|
-
const parsed = subscriptionSchema.parse(record);
|
|
3468
|
-
await state.set(subscriptionKey(id), parsed, ttlUntil(parsed.expiresAtMs, nowMs));
|
|
3469
|
-
await addToIndex(state, resourceIndexKey(input.namespace, input.identifier), id, nowMs);
|
|
3470
|
-
await addToIndex(state, conversationIndexKey(input.conversationId), id, nowMs);
|
|
3471
|
-
return parsed;
|
|
1962
|
+
const MAX_RETRY_DELAY_MS = 1e4;
|
|
1963
|
+
const MAX_TOTAL_RETRY_DELAY_MS = 2e4;
|
|
1964
|
+
const CONNECTION_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
1965
|
+
"ECONNREFUSED",
|
|
1966
|
+
"ECONNRESET",
|
|
1967
|
+
"ENOTFOUND",
|
|
1968
|
+
"EAI_AGAIN"
|
|
1969
|
+
]);
|
|
1970
|
+
const TIMEOUT_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
1971
|
+
"ETIMEDOUT",
|
|
1972
|
+
"ECONNABORTED",
|
|
1973
|
+
"ESOCKETTIMEDOUT"
|
|
1974
|
+
]);
|
|
1975
|
+
function findNetworkErrorCode(error, depth = 0) {
|
|
1976
|
+
if (!error || typeof error !== "object" || depth > 4) return;
|
|
1977
|
+
const candidate = error;
|
|
1978
|
+
if (typeof candidate.code === "string" && (CONNECTION_ERROR_CODES.has(candidate.code) || TIMEOUT_ERROR_CODES.has(candidate.code))) return candidate.code;
|
|
1979
|
+
return findNetworkErrorCode(candidate.original, depth + 1) ?? findNetworkErrorCode(candidate.cause, depth + 1);
|
|
3472
1980
|
}
|
|
3473
|
-
|
|
3474
|
-
|
|
3475
|
-
const state = getStateAdapter();
|
|
3476
|
-
await state.connect();
|
|
3477
|
-
const identifier = digest(`${input.conversationId}\0${input.toolCallId}`);
|
|
3478
|
-
const selector = {
|
|
3479
|
-
conversationId: input.conversationId,
|
|
3480
|
-
namespace: "junior",
|
|
3481
|
-
identifier,
|
|
3482
|
-
events: ["timer.fired"]
|
|
3483
|
-
};
|
|
3484
|
-
const id = buildSubscriptionId(selector);
|
|
3485
|
-
return await withSubscriptionLock(state, id, async () => {
|
|
3486
|
-
const existing = parseSubscription(await state.get(subscriptionKey(id)));
|
|
3487
|
-
if (existing) return existing;
|
|
3488
|
-
const nowMs = Date.now();
|
|
3489
|
-
const firesAtMs = nowMs + input.afterMs;
|
|
3490
|
-
const expiresAtMs = firesAtMs + 864e5;
|
|
3491
|
-
const record = subscriptionSchema.parse({
|
|
3492
|
-
...selector,
|
|
3493
|
-
id,
|
|
3494
|
-
firesAtMs,
|
|
3495
|
-
expiresAtMs,
|
|
3496
|
-
resourceType: "timer",
|
|
3497
|
-
label: "Timer",
|
|
3498
|
-
intent: input.intent,
|
|
3499
|
-
status: "active",
|
|
3500
|
-
createdAtMs: nowMs,
|
|
3501
|
-
updatedAtMs: nowMs
|
|
3502
|
-
});
|
|
3503
|
-
await withIndexLock(state, conversationIndexKey(input.conversationId), async () => {
|
|
3504
|
-
const key = conversationIndexKey(input.conversationId);
|
|
3505
|
-
const ids = [.../* @__PURE__ */ new Set([...await readSubscriptionIdIndex(state, key), id])];
|
|
3506
|
-
await state.set(key, ids, Math.max(ttlUntil(expiresAtMs, nowMs), await indexTtlMs(state, ids, nowMs)));
|
|
3507
|
-
await state.set(resourceIndexKey("junior", identifier), [id], ttlUntil(expiresAtMs, nowMs));
|
|
3508
|
-
await updateTimerIndex(state, {
|
|
3509
|
-
kind: "add",
|
|
3510
|
-
id,
|
|
3511
|
-
atMs: firesAtMs
|
|
3512
|
-
});
|
|
3513
|
-
await state.set(subscriptionKey(id), record, ttlUntil(expiresAtMs, nowMs));
|
|
3514
|
-
});
|
|
3515
|
-
return record;
|
|
3516
|
-
});
|
|
1981
|
+
function hasSocketHangUpMessage(error) {
|
|
1982
|
+
return error instanceof Error && error.message.toLowerCase().includes("socket hang up");
|
|
3517
1983
|
}
|
|
3518
|
-
/**
|
|
3519
|
-
|
|
3520
|
-
|
|
3521
|
-
|
|
3522
|
-
|
|
3523
|
-
|
|
3524
|
-
|
|
3525
|
-
|
|
3526
|
-
|
|
3527
|
-
|
|
3528
|
-
|
|
3529
|
-
|
|
3530
|
-
|
|
3531
|
-
|
|
3532
|
-
id
|
|
3533
|
-
});
|
|
3534
|
-
});
|
|
3535
|
-
return watches;
|
|
1984
|
+
/**
|
|
1985
|
+
* Classify a failed Slack call for retry: rate limits, Slack 5xx, and
|
|
1986
|
+
* connection-phase failures are always retryable; timeouts are retried only
|
|
1987
|
+
* for idempotent operations because Slack may already have accepted the
|
|
1988
|
+
* request.
|
|
1989
|
+
*/
|
|
1990
|
+
function classifySlackRetry(raw, mapped) {
|
|
1991
|
+
if (mapped.code === "rate_limited") return "rate_limited";
|
|
1992
|
+
if (mapped.statusCode !== void 0 && mapped.statusCode >= 500) return "server_error";
|
|
1993
|
+
const networkCode = findNetworkErrorCode(raw);
|
|
1994
|
+
if (networkCode && CONNECTION_ERROR_CODES.has(networkCode)) return "connection";
|
|
1995
|
+
if (networkCode && TIMEOUT_ERROR_CODES.has(networkCode)) return "timeout";
|
|
1996
|
+
if (hasSocketHangUpMessage(raw)) return "connection";
|
|
1997
|
+
return "none";
|
|
3536
1998
|
}
|
|
3537
|
-
/**
|
|
3538
|
-
|
|
3539
|
-
|
|
3540
|
-
|
|
3541
|
-
|
|
3542
|
-
|
|
3543
|
-
|
|
1999
|
+
/**
|
|
2000
|
+
* Run a Slack Web API call with bounded retries so transient platform
|
|
2001
|
+
* failures do not surface as turn failures, while non-idempotent posts are
|
|
2002
|
+
* never re-sent when Slack may already have accepted them.
|
|
2003
|
+
*
|
|
2004
|
+
* Retry classes: rate limits (bounded Retry-After), connection-phase network
|
|
2005
|
+
* failures, and Slack 5xx responses always retry; request timeouts retry only
|
|
2006
|
+
* when the caller marks the operation `idempotent`.
|
|
2007
|
+
*/
|
|
2008
|
+
async function withSlackRetries(task, maxAttempts = 3, context = {}) {
|
|
2009
|
+
let attempt = 0;
|
|
2010
|
+
let totalDelayMs = 0;
|
|
2011
|
+
const action = context.action ?? "unknown";
|
|
2012
|
+
while (attempt < maxAttempts) {
|
|
2013
|
+
attempt += 1;
|
|
2014
|
+
const attemptNumber = attempt;
|
|
2015
|
+
try {
|
|
2016
|
+
return await withSpan(`POST slack.com/api/${action}`, "http.client", {}, async () => {
|
|
2017
|
+
try {
|
|
2018
|
+
return await task();
|
|
2019
|
+
} catch (error) {
|
|
2020
|
+
const mapped = mapSlackError(error);
|
|
2021
|
+
const errorAttrs = { "error.type": mapped.code };
|
|
2022
|
+
if (mapped.apiError) errorAttrs["app.slack.api_error_code"] = mapped.apiError;
|
|
2023
|
+
if (mapped.code === "rate_limited") {
|
|
2024
|
+
errorAttrs["http.response.status_code"] = 429;
|
|
2025
|
+
if (mapped.retryAfterSeconds) errorAttrs["app.slack.retry_after_ms"] = mapped.retryAfterSeconds * 1e3;
|
|
2026
|
+
} else if (mapped.statusCode != null) errorAttrs["http.response.status_code"] = mapped.statusCode;
|
|
2027
|
+
setSpanAttributes(errorAttrs);
|
|
2028
|
+
setSpanStatus("error");
|
|
2029
|
+
throw error;
|
|
2030
|
+
}
|
|
2031
|
+
}, {
|
|
2032
|
+
"http.request.method": "POST",
|
|
2033
|
+
"server.address": "slack.com",
|
|
2034
|
+
"url.scheme": "https",
|
|
2035
|
+
"url.path": `/api/${action}`,
|
|
2036
|
+
"app.slack.method": action,
|
|
2037
|
+
"app.retry.max_attempts": maxAttempts,
|
|
2038
|
+
...attemptNumber > 1 ? { "http.request.resend_count": attemptNumber - 1 } : void 0,
|
|
2039
|
+
...context.attributes ?? {},
|
|
2040
|
+
...context.spanAttributes ?? {}
|
|
2041
|
+
});
|
|
2042
|
+
} catch (error) {
|
|
2043
|
+
const mapped = mapSlackError(error);
|
|
2044
|
+
const retryClass = classifySlackRetry(error, mapped);
|
|
2045
|
+
const isRetryable = retryClass === "rate_limited" || retryClass === "connection" || retryClass === "server_error" || retryClass === "timeout" && context.idempotent === true;
|
|
2046
|
+
const remainingDelayBudgetMs = MAX_TOTAL_RETRY_DELAY_MS - totalDelayMs;
|
|
2047
|
+
const baseLogAttributes = {
|
|
2048
|
+
"app.slack.action": action,
|
|
2049
|
+
"app.slack.error_code": mapped.code,
|
|
2050
|
+
...mapped.apiError ? { "app.slack.api_error": mapped.apiError } : void 0,
|
|
2051
|
+
...mapped.detail ? { "app.slack.detail": mapped.detail } : void 0,
|
|
2052
|
+
...mapped.detailLine !== void 0 ? { "app.slack.detail_line": mapped.detailLine } : void 0,
|
|
2053
|
+
...mapped.detailRule ? { "app.slack.detail_rule": mapped.detailRule } : void 0,
|
|
2054
|
+
...mapped.requestId ? { "app.slack.request_id": mapped.requestId } : void 0,
|
|
2055
|
+
...mapped.statusCode !== void 0 ? { "http.response.status_code": mapped.statusCode } : void 0,
|
|
2056
|
+
...context.attributes ?? {}
|
|
2057
|
+
};
|
|
2058
|
+
if (!isRetryable || attempt >= maxAttempts || remainingDelayBudgetMs <= 0) {
|
|
2059
|
+
logWarn("slack.action.failed", {
|
|
2060
|
+
...baseLogAttributes,
|
|
2061
|
+
...mapped.errorData ? { "app.slack.error_data": mapped.errorData } : void 0
|
|
2062
|
+
});
|
|
2063
|
+
throw mapped;
|
|
2064
|
+
}
|
|
2065
|
+
logWarn("slack.action.retrying", {
|
|
2066
|
+
...baseLogAttributes,
|
|
2067
|
+
"app.slack.retry_attempt": attempt,
|
|
2068
|
+
"app.slack.retry_class": retryClass
|
|
2069
|
+
});
|
|
2070
|
+
const retryAfterMs = retryClass === "rate_limited" && mapped.retryAfterSeconds && mapped.retryAfterSeconds > 0 ? mapped.retryAfterSeconds * 1e3 : void 0;
|
|
2071
|
+
const backoffMs = Math.min(2e3, 250 * 2 ** (attempt - 1));
|
|
2072
|
+
const delayMs = Math.min(retryAfterMs ?? backoffMs, MAX_RETRY_DELAY_MS, remainingDelayBudgetMs);
|
|
2073
|
+
totalDelayMs += delayMs;
|
|
2074
|
+
await sleep$1(delayMs);
|
|
2075
|
+
}
|
|
2076
|
+
}
|
|
2077
|
+
throw new SlackActionError("Slack action exhausted retries", "internal_error");
|
|
3544
2078
|
}
|
|
3545
|
-
/**
|
|
3546
|
-
|
|
3547
|
-
|
|
3548
|
-
|
|
3549
|
-
|
|
3550
|
-
|
|
3551
|
-
|
|
3552
|
-
|
|
3553
|
-
|
|
3554
|
-
|
|
3555
|
-
|
|
3556
|
-
|
|
3557
|
-
updatedAtMs: nowMs
|
|
3558
|
-
};
|
|
3559
|
-
await state.set(subscriptionKey(input.id), next, JUNIOR_THREAD_STATE_TTL_MS);
|
|
3560
|
-
await removeFromIndex(state, resourceIndexKey(current.namespace, current.identifier), input.id, nowMs);
|
|
3561
|
-
await removeFromIndex(state, conversationIndexKey(current.conversationId), input.id, nowMs);
|
|
3562
|
-
if (current.firesAtMs !== void 0) await updateTimerIndex(state, {
|
|
3563
|
-
kind: "remove",
|
|
3564
|
-
id: input.id
|
|
3565
|
-
});
|
|
3566
|
-
return next;
|
|
2079
|
+
/**
|
|
2080
|
+
* Slack Web API client for the current destination workspace: the ambient
|
|
2081
|
+
* installation token when one is bound, otherwise the env bot token for
|
|
2082
|
+
* single-workspace deployments. Fails instead of falling back when the call
|
|
2083
|
+
* is workspace-scoped and no installation token can be resolved, so a write
|
|
2084
|
+
* never goes out with another workspace's credentials.
|
|
2085
|
+
*/
|
|
2086
|
+
function getSlackClient(options) {
|
|
2087
|
+
if (options) return new WebClient(resolveSlackToken(), {
|
|
2088
|
+
retryConfig: { retries: 0 },
|
|
2089
|
+
rejectRateLimitedCalls: true,
|
|
2090
|
+
timeout: options.timeoutMs
|
|
3567
2091
|
});
|
|
2092
|
+
return getClient();
|
|
3568
2093
|
}
|
|
3569
|
-
/**
|
|
3570
|
-
|
|
3571
|
-
|
|
3572
|
-
|
|
3573
|
-
|
|
3574
|
-
|
|
3575
|
-
|
|
3576
|
-
|
|
3577
|
-
|
|
2094
|
+
/**
|
|
2095
|
+
* Slack channel ID prefixes:
|
|
2096
|
+
* - C: channel — modern private channels also use C, so the prefix never
|
|
2097
|
+
* proves a channel is public
|
|
2098
|
+
* - G: legacy private channel / group DM
|
|
2099
|
+
* - D: direct message (1:1)
|
|
2100
|
+
*/
|
|
2101
|
+
function isDmChannel(channelId) {
|
|
2102
|
+
const normalized = normalizeSlackConversationId(channelId);
|
|
2103
|
+
return Boolean(normalized && normalized.startsWith("D"));
|
|
3578
2104
|
}
|
|
3579
2105
|
/**
|
|
3580
|
-
*
|
|
3581
|
-
*
|
|
2106
|
+
* Conversation-scoped Slack contexts backed by a concrete conversation ID.
|
|
2107
|
+
* Includes channels/groups/DMs (C/G/D).
|
|
3582
2108
|
*/
|
|
3583
|
-
|
|
3584
|
-
const
|
|
3585
|
-
|
|
3586
|
-
|
|
3587
|
-
const eventTypes = new Set(input.eventTypes.map((eventType) => eventType.trim()).filter(Boolean));
|
|
3588
|
-
if (eventTypes.size === 0) return [];
|
|
3589
|
-
const keys = /* @__PURE__ */ new Set();
|
|
3590
|
-
const identifiers = new Set(input.identifiers.map((value) => value.trim()).filter(Boolean));
|
|
3591
|
-
for (const identifier of identifiers) {
|
|
3592
|
-
const ids = await readSubscriptionIdIndex(state, resourceIndexKey(input.namespace, identifier));
|
|
3593
|
-
const records = await Promise.all(ids.map(async (id) => parseSubscription(await state.get(subscriptionKey(id)))));
|
|
3594
|
-
for (const record of records) {
|
|
3595
|
-
if (!record) continue;
|
|
3596
|
-
if (record.namespace !== input.namespace) continue;
|
|
3597
|
-
if (record.identifier !== identifier) continue;
|
|
3598
|
-
if (!activeAt(record, nowMs)) continue;
|
|
3599
|
-
if (!record.events.some((eventType) => eventTypes.has(eventType))) continue;
|
|
3600
|
-
for (const key of Object.keys(record.match ?? {})) keys.add(key);
|
|
3601
|
-
}
|
|
3602
|
-
}
|
|
3603
|
-
return [...keys].sort();
|
|
2109
|
+
function isConversationScopedChannel(channelId) {
|
|
2110
|
+
const normalized = normalizeSlackConversationId(channelId);
|
|
2111
|
+
if (!normalized) return false;
|
|
2112
|
+
return normalized.startsWith("C") || normalized.startsWith("G") || normalized.startsWith("D");
|
|
3604
2113
|
}
|
|
3605
|
-
|
|
3606
|
-
|
|
3607
|
-
|
|
3608
|
-
|
|
3609
|
-
const nowMs = input.nowMs ?? Date.now();
|
|
3610
|
-
const ids = await readSubscriptionIdIndex(state, resourceIndexKey(input.namespace, input.identifier));
|
|
3611
|
-
return (await Promise.all(ids.map(async (id) => parseSubscription(await state.get(subscriptionKey(id)))))).filter((record) => record !== void 0 && matchesEvent(record, {
|
|
3612
|
-
data: input.data,
|
|
3613
|
-
eventType: input.eventType,
|
|
3614
|
-
nowMs,
|
|
3615
|
-
namespace: input.namespace,
|
|
3616
|
-
identifier: input.identifier
|
|
3617
|
-
}));
|
|
2114
|
+
function isConversationChannel(channelId) {
|
|
2115
|
+
const normalized = normalizeSlackConversationId(channelId);
|
|
2116
|
+
if (!normalized) return false;
|
|
2117
|
+
return normalized.startsWith("C") || normalized.startsWith("G");
|
|
3618
2118
|
}
|
|
3619
|
-
|
|
3620
|
-
|
|
3621
|
-
|
|
3622
|
-
|
|
3623
|
-
|
|
3624
|
-
|
|
3625
|
-
|
|
3626
|
-
|
|
3627
|
-
|
|
3628
|
-
|
|
3629
|
-
|
|
3630
|
-
|
|
3631
|
-
|
|
3632
|
-
|
|
3633
|
-
|
|
3634
|
-
|
|
3635
|
-
|
|
3636
|
-
if (!latest || latest.status !== current.status || latest.updatedAtMs !== current.updatedAtMs) return delivered;
|
|
3637
|
-
const next = {
|
|
3638
|
-
...current,
|
|
3639
|
-
status: "completed",
|
|
3640
|
-
updatedAtMs: nowMs
|
|
3641
|
-
};
|
|
3642
|
-
await state.set(subscriptionKey(current.id), next, JUNIOR_THREAD_STATE_TTL_MS);
|
|
3643
|
-
await removeFromIndex(state, resourceIndexKey(current.namespace, current.identifier), current.id, nowMs);
|
|
3644
|
-
await removeFromIndex(state, conversationIndexKey(current.conversationId), current.id, nowMs);
|
|
2119
|
+
async function getFilePermalink(fileId) {
|
|
2120
|
+
const client = getClient();
|
|
2121
|
+
return (await withSlackRetries(() => client.files.info({ file: fileId }), 3, {
|
|
2122
|
+
action: "files.info",
|
|
2123
|
+
idempotent: true,
|
|
2124
|
+
spanAttributes: { "app.slack.file_id": fileId }
|
|
2125
|
+
})).file?.permalink;
|
|
2126
|
+
}
|
|
2127
|
+
async function downloadPrivateSlackFile(url) {
|
|
2128
|
+
const token = resolveSlackToken();
|
|
2129
|
+
return withSpan("GET files.slack.com", "http.client", {}, async () => {
|
|
2130
|
+
const response = await fetch(url, { headers: { Authorization: `Bearer ${token}` } });
|
|
2131
|
+
setSpanAttributes({ "http.response.status_code": response.status });
|
|
2132
|
+
if (!response.ok) {
|
|
2133
|
+
setSpanAttributes({ "error.type": String(response.status) });
|
|
2134
|
+
setSpanStatus("error");
|
|
2135
|
+
throw new Error(`Slack file download failed: ${response.status}`);
|
|
3645
2136
|
}
|
|
3646
|
-
|
|
3647
|
-
|
|
3648
|
-
|
|
3649
|
-
|
|
3650
|
-
|
|
3651
|
-
|
|
2137
|
+
return Buffer.from(await response.arrayBuffer());
|
|
2138
|
+
}, {
|
|
2139
|
+
"http.request.method": "GET",
|
|
2140
|
+
"server.address": "files.slack.com",
|
|
2141
|
+
"url.scheme": "https",
|
|
2142
|
+
"app.slack.method": "files.download"
|
|
2143
|
+
});
|
|
3652
2144
|
}
|
|
3653
2145
|
//#endregion
|
|
3654
|
-
//#region src/chat/
|
|
3655
|
-
|
|
3656
|
-
const
|
|
3657
|
-
|
|
3658
|
-
|
|
3659
|
-
|
|
3660
|
-
|
|
3661
|
-
|
|
3662
|
-
|
|
3663
|
-
}
|
|
3664
|
-
|
|
3665
|
-
|
|
3666
|
-
|
|
3667
|
-
|
|
3668
|
-
|
|
3669
|
-
const
|
|
3670
|
-
|
|
3671
|
-
|
|
3672
|
-
|
|
3673
|
-
|
|
3674
|
-
|
|
3675
|
-
|
|
3676
|
-
const
|
|
3677
|
-
const
|
|
3678
|
-
const
|
|
3679
|
-
if (!
|
|
2146
|
+
//#region src/chat/credentials/subject.ts
|
|
2147
|
+
const CREDENTIAL_SUBJECT_HMAC_CONTEXT = "junior.credential_subject.v1";
|
|
2148
|
+
const CREDENTIAL_SUBJECT_SIGNATURE_VERSION = "v1";
|
|
2149
|
+
function getCredentialSubjectSecret() {
|
|
2150
|
+
return process.env.JUNIOR_SECRET?.trim() || void 0;
|
|
2151
|
+
}
|
|
2152
|
+
/** Encode ordered signing fields without allowing delimiter ambiguity. */
|
|
2153
|
+
function buildPayload(parts) {
|
|
2154
|
+
return [CREDENTIAL_SUBJECT_HMAC_CONTEXT, ...parts].join("\0");
|
|
2155
|
+
}
|
|
2156
|
+
function signPayload(secret, payload) {
|
|
2157
|
+
const digest = createHmac("sha256", secret).update(payload).digest("hex");
|
|
2158
|
+
return `${CREDENTIAL_SUBJECT_SIGNATURE_VERSION}=${digest}`;
|
|
2159
|
+
}
|
|
2160
|
+
function timingSafeMatch(expected, actual) {
|
|
2161
|
+
const expectedBuffer = Buffer.from(expected);
|
|
2162
|
+
const actualBuffer = Buffer.from(actual);
|
|
2163
|
+
if (expectedBuffer.length !== actualBuffer.length) return false;
|
|
2164
|
+
return timingSafeEqual(expectedBuffer, actualBuffer);
|
|
2165
|
+
}
|
|
2166
|
+
/** Create a delegated user credential subject for a verified Slack DM turn. */
|
|
2167
|
+
function createSlackDirectCredentialSubject(input) {
|
|
2168
|
+
const channelId = normalizeSlackConversationId(input.channelId);
|
|
2169
|
+
const teamId = input.teamId?.trim();
|
|
2170
|
+
const userId = parseActorUserId(input.userId);
|
|
2171
|
+
if (!channelId || !teamId || !userId || !isDmChannel(channelId)) return;
|
|
3680
2172
|
return {
|
|
3681
|
-
|
|
3682
|
-
|
|
3683
|
-
|
|
3684
|
-
messageChannelId: source ? parseSlackChannelReferenceId(source.channelId) : void 0,
|
|
3685
|
-
messageTs: parseSlackMessageTs(source?.messageTs),
|
|
3686
|
-
teamId,
|
|
3687
|
-
threadTs: parseSlackMessageTs(location.threadTs)
|
|
2173
|
+
type: "user",
|
|
2174
|
+
userId,
|
|
2175
|
+
allowedWhen: "private-direct-conversation"
|
|
3688
2176
|
};
|
|
3689
2177
|
}
|
|
3690
|
-
|
|
3691
|
-
|
|
3692
|
-
|
|
3693
|
-
|
|
3694
|
-
|
|
3695
|
-
|
|
3696
|
-
|
|
3697
|
-
|
|
3698
|
-
|
|
3699
|
-
|
|
3700
|
-
|
|
2178
|
+
/** Bind a delegated user subject to the Slack DM destination being dispatched. */
|
|
2179
|
+
function bindSlackDirectCredentialSubject(input) {
|
|
2180
|
+
const channelId = normalizeSlackConversationId(input.channelId);
|
|
2181
|
+
const teamId = input.teamId.trim();
|
|
2182
|
+
const secret = getCredentialSubjectSecret();
|
|
2183
|
+
const { subject } = input;
|
|
2184
|
+
const userId = parseActorUserId(subject.userId);
|
|
2185
|
+
if (!channelId || !teamId || !secret || !isDmChannel(channelId) || subject.type !== "user" || !userId || subject.allowedWhen !== "private-direct-conversation") return;
|
|
2186
|
+
return {
|
|
2187
|
+
type: "user",
|
|
2188
|
+
userId,
|
|
2189
|
+
allowedWhen: subject.allowedWhen,
|
|
2190
|
+
binding: {
|
|
2191
|
+
type: "slack-direct-conversation",
|
|
2192
|
+
teamId,
|
|
2193
|
+
channelId,
|
|
2194
|
+
signature: signPayload(secret, buildPayload([
|
|
2195
|
+
subject.allowedWhen,
|
|
2196
|
+
teamId,
|
|
2197
|
+
channelId,
|
|
2198
|
+
userId
|
|
2199
|
+
]))
|
|
2200
|
+
}
|
|
2201
|
+
};
|
|
3701
2202
|
}
|
|
3702
|
-
|
|
3703
|
-
|
|
3704
|
-
|
|
3705
|
-
|
|
3706
|
-
|
|
3707
|
-
|
|
3708
|
-
|
|
3709
|
-
|
|
2203
|
+
/** Verify that a delegated subject was signed for the dispatch destination. */
|
|
2204
|
+
function verifySlackDirectCredentialSubject(input) {
|
|
2205
|
+
const channelId = normalizeSlackConversationId(input.channelId);
|
|
2206
|
+
const secret = getCredentialSubjectSecret();
|
|
2207
|
+
if (!channelId || !secret) return false;
|
|
2208
|
+
const { subject } = input;
|
|
2209
|
+
const binding = subject.binding;
|
|
2210
|
+
if (subject.type !== "user" || !isActorUserId(subject.userId) || subject.allowedWhen !== "private-direct-conversation" || !binding || binding.type !== "slack-direct-conversation" || typeof binding.signature !== "string" || !binding.signature || binding.teamId !== input.teamId || binding.channelId !== channelId) return false;
|
|
2211
|
+
return timingSafeMatch(signPayload(secret, buildPayload([
|
|
2212
|
+
subject.allowedWhen,
|
|
2213
|
+
binding.teamId,
|
|
2214
|
+
binding.channelId,
|
|
2215
|
+
subject.userId
|
|
2216
|
+
])), binding.signature);
|
|
3710
2217
|
}
|
|
3711
|
-
/**
|
|
3712
|
-
|
|
3713
|
-
const
|
|
3714
|
-
|
|
3715
|
-
const
|
|
3716
|
-
|
|
3717
|
-
|
|
3718
|
-
|
|
3719
|
-
primaryEmailNormalized: normalizedEmail,
|
|
3720
|
-
displayName: null,
|
|
3721
|
-
createdAt: now,
|
|
3722
|
-
updatedAt: now
|
|
3723
|
-
}).onConflictDoNothing({ target: juniorUsers.primaryEmailNormalized }).returning())[0] ?? (await db.select().from(juniorUsers).where(eq(juniorUsers.primaryEmailNormalized, normalizedEmail)).limit(1))[0];
|
|
3724
|
-
return userRow ? await readUserById(db, userRow) : void 0;
|
|
3725
|
-
}
|
|
3726
|
-
/** Resolve or create the canonical user for one authenticated viewer. */
|
|
3727
|
-
async function resolveViewerUser(email) {
|
|
3728
|
-
return await resolveViewerUserFromSql(getDb(), email);
|
|
3729
|
-
}
|
|
3730
|
-
/** Read one existing canonical user by verified email without creating a row. */
|
|
3731
|
-
async function findUserByEmailFromSql(db, email) {
|
|
3732
|
-
const normalizedEmail = normalizeIdentityEmail(email);
|
|
3733
|
-
if (!normalizedEmail) return void 0;
|
|
3734
|
-
const userRow = (await db.select().from(juniorUsers).where(eq(juniorUsers.primaryEmailNormalized, normalizedEmail)).limit(1))[0];
|
|
3735
|
-
return userRow ? await readUserById(db, userRow) : void 0;
|
|
3736
|
-
}
|
|
3737
|
-
/** Read one existing canonical user by verified email without creating a row. */
|
|
3738
|
-
async function findUserByEmail(email) {
|
|
3739
|
-
return await findUserByEmailFromSql(getDb(), email);
|
|
3740
|
-
}
|
|
3741
|
-
/** Update one canonical user's display name in SQL. */
|
|
3742
|
-
async function updateViewerDisplayNameFromSql(db, userId, displayName) {
|
|
3743
|
-
const userRow = (await db.update(juniorUsers).set({
|
|
3744
|
-
displayName,
|
|
3745
|
-
updatedAt: /* @__PURE__ */ new Date()
|
|
3746
|
-
}).where(eq(juniorUsers.id, userId)).returning())[0];
|
|
3747
|
-
return userRow ? await readUserById(db, userRow) : void 0;
|
|
3748
|
-
}
|
|
3749
|
-
/** Update one canonical user's display name. */
|
|
3750
|
-
async function updateViewerDisplayName(userId, displayName) {
|
|
3751
|
-
return await updateViewerDisplayNameFromSql(getDb(), userId, displayName);
|
|
3752
|
-
}
|
|
3753
|
-
/** Resolve the stored identity and linked user for one runtime actor. */
|
|
3754
|
-
async function readActorIdentityFromSql(db, actor) {
|
|
3755
|
-
if (actor.platform === "system") return void 0;
|
|
3756
|
-
const providerTenantId = actor.platform === "slack" ? actor.teamId : "";
|
|
3757
|
-
const row = (await db.select({
|
|
3758
|
-
identity: juniorIdentities,
|
|
3759
|
-
user: juniorUsers
|
|
3760
|
-
}).from(juniorIdentities).leftJoin(juniorUsers, eq(juniorUsers.id, juniorIdentities.userId)).where(and(eq(juniorIdentities.kind, "user"), eq(juniorIdentities.provider, actor.platform), eq(juniorIdentities.providerTenantId, providerTenantId), eq(juniorIdentities.providerSubjectId, actor.userId))).limit(1))[0];
|
|
3761
|
-
if (!row) return void 0;
|
|
2218
|
+
/** Bind a delegated user subject to one scheduler task dispatch. */
|
|
2219
|
+
function bindScheduledAutomationCredentialSubject(input) {
|
|
2220
|
+
const secret = getCredentialSubjectSecret();
|
|
2221
|
+
const plugin = input.plugin.trim();
|
|
2222
|
+
const userId = parseActorUserId(input.subject.userId);
|
|
2223
|
+
if (!secret || plugin !== "scheduler" || !userId || input.subject.allowedWhen !== "scheduled-automation") return;
|
|
2224
|
+
const taskId = input.subject.taskId;
|
|
2225
|
+
if (!taskId || taskId !== taskId.trim()) return;
|
|
3762
2226
|
return {
|
|
3763
|
-
|
|
3764
|
-
|
|
2227
|
+
type: "user",
|
|
2228
|
+
userId,
|
|
2229
|
+
allowedWhen: "scheduled-automation",
|
|
2230
|
+
taskId,
|
|
2231
|
+
binding: {
|
|
2232
|
+
type: "scheduled-automation",
|
|
2233
|
+
plugin,
|
|
2234
|
+
taskId,
|
|
2235
|
+
signature: signPayload(secret, buildPayload([
|
|
2236
|
+
"scheduled-automation",
|
|
2237
|
+
plugin,
|
|
2238
|
+
taskId,
|
|
2239
|
+
userId
|
|
2240
|
+
]))
|
|
2241
|
+
}
|
|
3765
2242
|
};
|
|
3766
2243
|
}
|
|
3767
|
-
/**
|
|
3768
|
-
|
|
3769
|
-
|
|
3770
|
-
}
|
|
3771
|
-
|
|
3772
|
-
|
|
3773
|
-
|
|
3774
|
-
|
|
3775
|
-
|
|
3776
|
-
|
|
3777
|
-
|
|
2244
|
+
/** Verify that a delegated subject was signed for one scheduler task. */
|
|
2245
|
+
function verifyScheduledAutomationCredentialSubject(input) {
|
|
2246
|
+
const secret = getCredentialSubjectSecret();
|
|
2247
|
+
const { subject } = input;
|
|
2248
|
+
const binding = subject.binding;
|
|
2249
|
+
if (!secret || input.plugin !== "scheduler" || subject.type !== "user" || !isActorUserId(subject.userId) || subject.allowedWhen !== "scheduled-automation" || !subject.taskId || binding.type !== "scheduled-automation" || binding.plugin !== input.plugin || binding.taskId !== subject.taskId) return false;
|
|
2250
|
+
return timingSafeMatch(signPayload(secret, buildPayload([
|
|
2251
|
+
"scheduled-automation",
|
|
2252
|
+
binding.plugin,
|
|
2253
|
+
binding.taskId,
|
|
2254
|
+
subject.userId
|
|
2255
|
+
])), binding.signature);
|
|
3778
2256
|
}
|
|
3779
|
-
/**
|
|
3780
|
-
function
|
|
3781
|
-
|
|
2257
|
+
/** Bind a delegated user subject to one event automation dispatch. */
|
|
2258
|
+
function bindEventAutomationCredentialSubject(input) {
|
|
2259
|
+
const secret = getCredentialSubjectSecret();
|
|
2260
|
+
const plugin = input.plugin.trim();
|
|
2261
|
+
const userId = parseActorUserId(input.subject.userId);
|
|
2262
|
+
if (!secret || plugin !== "junior" || !userId || input.subject.allowedWhen !== "event-automation") return;
|
|
2263
|
+
const taskId = input.subject.taskId;
|
|
2264
|
+
if (!taskId || taskId !== taskId.trim()) return;
|
|
2265
|
+
return {
|
|
2266
|
+
type: "user",
|
|
2267
|
+
userId,
|
|
2268
|
+
allowedWhen: "event-automation",
|
|
2269
|
+
taskId,
|
|
2270
|
+
binding: {
|
|
2271
|
+
type: "event-automation",
|
|
2272
|
+
plugin,
|
|
2273
|
+
taskId,
|
|
2274
|
+
signature: signPayload(secret, buildPayload([
|
|
2275
|
+
"event-automation",
|
|
2276
|
+
plugin,
|
|
2277
|
+
taskId,
|
|
2278
|
+
userId
|
|
2279
|
+
]))
|
|
2280
|
+
}
|
|
2281
|
+
};
|
|
3782
2282
|
}
|
|
3783
|
-
|
|
3784
|
-
|
|
3785
|
-
|
|
3786
|
-
|
|
3787
|
-
|
|
3788
|
-
|
|
3789
|
-
|
|
3790
|
-
|
|
3791
|
-
|
|
3792
|
-
|
|
3793
|
-
|
|
3794
|
-
|
|
3795
|
-
detailLine;
|
|
3796
|
-
detailRule;
|
|
3797
|
-
constructor(message, code, options = {}) {
|
|
3798
|
-
super(message);
|
|
3799
|
-
this.name = "SlackActionError";
|
|
3800
|
-
this.code = code;
|
|
3801
|
-
this.apiError = options.apiError;
|
|
3802
|
-
this.needed = options.needed;
|
|
3803
|
-
this.provided = options.provided;
|
|
3804
|
-
this.statusCode = options.statusCode;
|
|
3805
|
-
this.requestId = options.requestId;
|
|
3806
|
-
this.errorData = options.errorData;
|
|
3807
|
-
this.retryAfterSeconds = options.retryAfterSeconds;
|
|
3808
|
-
this.detail = options.detail;
|
|
3809
|
-
this.detailLine = options.detailLine;
|
|
3810
|
-
this.detailRule = options.detailRule;
|
|
3811
|
-
}
|
|
3812
|
-
};
|
|
3813
|
-
function serializeSlackErrorData(data) {
|
|
3814
|
-
if (!data || typeof data !== "object") return;
|
|
3815
|
-
const filtered = Object.fromEntries(Object.entries(data).filter(([key]) => key !== "error"));
|
|
3816
|
-
if (Object.keys(filtered).length === 0) return;
|
|
3817
|
-
try {
|
|
3818
|
-
const serialized = JSON.stringify(filtered);
|
|
3819
|
-
return serialized.length <= 600 ? serialized : `${serialized.slice(0, 597)}...`;
|
|
3820
|
-
} catch {
|
|
3821
|
-
return;
|
|
3822
|
-
}
|
|
2283
|
+
/** Verify that a delegated subject was signed for one event automation. */
|
|
2284
|
+
function verifyEventAutomationCredentialSubject(input) {
|
|
2285
|
+
const secret = getCredentialSubjectSecret();
|
|
2286
|
+
const { subject } = input;
|
|
2287
|
+
const binding = subject.binding;
|
|
2288
|
+
if (!secret || input.plugin !== "junior" || subject.type !== "user" || !isActorUserId(subject.userId) || subject.allowedWhen !== "event-automation" || !subject.taskId || binding.type !== "event-automation" || binding.plugin !== input.plugin || binding.taskId !== subject.taskId) return false;
|
|
2289
|
+
return timingSafeMatch(signPayload(secret, buildPayload([
|
|
2290
|
+
"event-automation",
|
|
2291
|
+
binding.plugin,
|
|
2292
|
+
binding.taskId,
|
|
2293
|
+
subject.userId
|
|
2294
|
+
])), binding.signature);
|
|
3823
2295
|
}
|
|
3824
|
-
/**
|
|
3825
|
-
function
|
|
3826
|
-
|
|
3827
|
-
const
|
|
3828
|
-
const
|
|
3829
|
-
|
|
3830
|
-
|
|
3831
|
-
|
|
3832
|
-
|
|
3833
|
-
|
|
3834
|
-
|
|
2296
|
+
/** Bind the user who set a timer Watch to that one Watch. */
|
|
2297
|
+
function bindTimerWatchCredentialSubject(input) {
|
|
2298
|
+
const secret = getCredentialSubjectSecret();
|
|
2299
|
+
const userId = parseActorUserId(input.userId);
|
|
2300
|
+
const { conversationId, watchId } = input;
|
|
2301
|
+
if (!secret || !userId || !conversationId || conversationId !== conversationId.trim() || !watchId || watchId !== watchId.trim()) return;
|
|
2302
|
+
return {
|
|
2303
|
+
type: "user",
|
|
2304
|
+
userId,
|
|
2305
|
+
allowedWhen: "timer-watch",
|
|
2306
|
+
binding: {
|
|
2307
|
+
type: "timer-watch",
|
|
2308
|
+
conversationId,
|
|
2309
|
+
watchId,
|
|
2310
|
+
signature: signPayload(secret, buildPayload([
|
|
2311
|
+
"timer-watch",
|
|
2312
|
+
conversationId,
|
|
2313
|
+
watchId,
|
|
2314
|
+
userId
|
|
2315
|
+
]))
|
|
3835
2316
|
}
|
|
3836
|
-
}
|
|
2317
|
+
};
|
|
3837
2318
|
}
|
|
3838
|
-
|
|
3839
|
-
|
|
3840
|
-
const
|
|
3841
|
-
|
|
3842
|
-
const
|
|
3843
|
-
|
|
3844
|
-
|
|
3845
|
-
|
|
3846
|
-
|
|
3847
|
-
|
|
3848
|
-
|
|
3849
|
-
|
|
2319
|
+
/** Verify that a delegated subject was signed for one timer Watch. */
|
|
2320
|
+
function verifyTimerWatchCredentialSubject(input) {
|
|
2321
|
+
const secret = getCredentialSubjectSecret();
|
|
2322
|
+
const { subject } = input;
|
|
2323
|
+
const binding = subject.binding;
|
|
2324
|
+
if (!secret || subject.type !== "user" || !isActorUserId(subject.userId) || subject.allowedWhen !== "timer-watch" || binding.type !== "timer-watch" || binding.conversationId !== input.conversationId || binding.watchId !== input.watchId) return false;
|
|
2325
|
+
return timingSafeMatch(signPayload(secret, buildPayload([
|
|
2326
|
+
"timer-watch",
|
|
2327
|
+
binding.conversationId,
|
|
2328
|
+
binding.watchId,
|
|
2329
|
+
subject.userId
|
|
2330
|
+
])), binding.signature);
|
|
3850
2331
|
}
|
|
3851
|
-
|
|
3852
|
-
|
|
3853
|
-
|
|
3854
|
-
|
|
3855
|
-
|
|
3856
|
-
const
|
|
3857
|
-
const
|
|
3858
|
-
|
|
3859
|
-
|
|
3860
|
-
|
|
3861
|
-
|
|
3862
|
-
|
|
3863
|
-
|
|
3864
|
-
|
|
3865
|
-
|
|
3866
|
-
|
|
2332
|
+
//#endregion
|
|
2333
|
+
//#region src/chat/events/store.ts
|
|
2334
|
+
const EVENT_PREFIX = "junior:resource_event_subscription:v5";
|
|
2335
|
+
const INDEX_LOCK_TTL_MS = 1e4;
|
|
2336
|
+
const SUBSCRIPTION_LOCK_TTL_MS = 1e4;
|
|
2337
|
+
const SUBSCRIPTION_LOCK_WAIT_MS = 1e4;
|
|
2338
|
+
const SUBSCRIPTION_LOCK_RETRY_MS = 25;
|
|
2339
|
+
const SUBSCRIPTION_LOCK_HEARTBEAT_MS = 3e3;
|
|
2340
|
+
const subscriptionStatusSchema = z.enum([
|
|
2341
|
+
"active",
|
|
2342
|
+
"cancelled",
|
|
2343
|
+
"completed"
|
|
2344
|
+
]);
|
|
2345
|
+
const subscriptionIdIndexSchema = z.array(z.string().min(1));
|
|
2346
|
+
const subscriptionSchema = z.object({
|
|
2347
|
+
conversationId: z.string().min(1),
|
|
2348
|
+
createdAtMs: z.number().finite(),
|
|
2349
|
+
/** Timer creator whose grants the timer Turn may use. */
|
|
2350
|
+
credentialSubject: credentialSubjectSchema.optional(),
|
|
2351
|
+
events: z.array(eventTypeSchema).min(1),
|
|
2352
|
+
expiresAtMs: z.number().finite(),
|
|
2353
|
+
firesAtMs: z.number().finite().optional(),
|
|
2354
|
+
id: z.string().min(1),
|
|
2355
|
+
intent: z.string().min(1),
|
|
2356
|
+
label: z.string().min(1),
|
|
2357
|
+
match: eventMatchSchema.optional(),
|
|
2358
|
+
namespace: z.string().min(1),
|
|
2359
|
+
identifier: z.string().min(1),
|
|
2360
|
+
resourceType: z.string().min(1),
|
|
2361
|
+
status: subscriptionStatusSchema,
|
|
2362
|
+
updatedAtMs: z.number().finite()
|
|
2363
|
+
}).strict();
|
|
2364
|
+
function digest(value) {
|
|
2365
|
+
return createHash("sha256").update(value).digest("hex").slice(0, 32);
|
|
3867
2366
|
}
|
|
3868
|
-
|
|
3869
|
-
|
|
3870
|
-
* failure for team-scoped calls — a call scoped to one workspace must never
|
|
3871
|
-
* silently fall through to another workspace's credentials.
|
|
3872
|
-
*/
|
|
3873
|
-
function resolveSlackToken() {
|
|
3874
|
-
const ambientToken = installationTokenStorage.getStore()?.token;
|
|
3875
|
-
if (ambientToken) return ambientToken;
|
|
3876
|
-
const envToken = getSlackBotToken();
|
|
3877
|
-
if (envToken) return envToken;
|
|
3878
|
-
const teamId = getWorkspaceTeamId();
|
|
3879
|
-
if (teamId) throw new SlackActionError(`Slack call is scoped to workspace ${teamId} but no installation token is bound and no default bot token is configured`, "missing_token");
|
|
3880
|
-
throw new SlackActionError("SLACK_BOT_TOKEN (or SLACK_BOT_USER_TOKEN) is required for Slack Web API actions in this service", "missing_token");
|
|
2367
|
+
function subscriptionKey(id) {
|
|
2368
|
+
return `${EVENT_PREFIX}:record:${id}`;
|
|
3881
2369
|
}
|
|
3882
|
-
|
|
3883
|
-
|
|
3884
|
-
return parseSlackChannelReferenceId(channelId);
|
|
2370
|
+
function subscriptionLockKey(id) {
|
|
2371
|
+
return `${EVENT_PREFIX}:lock:${id}`;
|
|
3885
2372
|
}
|
|
3886
|
-
|
|
3887
|
-
|
|
3888
|
-
* classification for this boundary: the WebClient's built-in policy would
|
|
3889
|
-
* re-post timed-out writes (a duplicate-message hazard) and sleep for the
|
|
3890
|
-
* full unbounded Retry-After inside the request, so both internal behaviors
|
|
3891
|
-
* are disabled here.
|
|
3892
|
-
*/
|
|
3893
|
-
function getClient() {
|
|
3894
|
-
const token = resolveSlackToken();
|
|
3895
|
-
let cached = clientsByToken.get(token);
|
|
3896
|
-
if (!cached) {
|
|
3897
|
-
cached = new WebClient(token, {
|
|
3898
|
-
retryConfig: { retries: 0 },
|
|
3899
|
-
rejectRateLimitedCalls: true
|
|
3900
|
-
});
|
|
3901
|
-
clientsByToken.set(token, cached);
|
|
3902
|
-
}
|
|
3903
|
-
return cached;
|
|
2373
|
+
function resourceIndexKey(namespace, identifier) {
|
|
2374
|
+
return `${EVENT_PREFIX}:resource:${digest(`${namespace}\0${identifier}`)}`;
|
|
3904
2375
|
}
|
|
3905
|
-
function
|
|
3906
|
-
|
|
3907
|
-
const candidate = error;
|
|
3908
|
-
const apiError = candidate.data?.error;
|
|
3909
|
-
const message = candidate.message ?? "Slack action failed";
|
|
3910
|
-
const baseOptions = {
|
|
3911
|
-
apiError,
|
|
3912
|
-
statusCode: candidate.statusCode,
|
|
3913
|
-
requestId: getHeaderString(candidate.headers, "x-slack-req-id"),
|
|
3914
|
-
errorData: serializeSlackErrorData(candidate.data),
|
|
3915
|
-
...parseSlackCanvasDetail(candidate.data?.detail)
|
|
3916
|
-
};
|
|
3917
|
-
if (apiError === "missing_scope") return new SlackActionError(message, "missing_scope", {
|
|
3918
|
-
...baseOptions,
|
|
3919
|
-
needed: candidate.data?.needed,
|
|
3920
|
-
provided: candidate.data?.provided
|
|
3921
|
-
});
|
|
3922
|
-
if (apiError === "not_in_channel") return new SlackActionError(message, "not_in_channel", baseOptions);
|
|
3923
|
-
if (apiError === "restricted_action_read_only_channel") return new SlackActionError(message, "read_only_channel", baseOptions);
|
|
3924
|
-
if (apiError === "already_reacted") return new SlackActionError(message, "already_reacted", baseOptions);
|
|
3925
|
-
if (apiError === "no_reaction") return new SlackActionError(message, "no_reaction", baseOptions);
|
|
3926
|
-
if (apiError === "invalid_arguments") return new SlackActionError(message, "invalid_arguments", baseOptions);
|
|
3927
|
-
if (apiError === "invalid_cursor") return new SlackActionError(message, "invalid_arguments", baseOptions);
|
|
3928
|
-
if (apiError === "invalid_name") return new SlackActionError(message, "invalid_arguments", baseOptions);
|
|
3929
|
-
if (apiError === "not_found" || apiError === "channel_not_found" || apiError === "message_not_found") return new SlackActionError(message, "not_found", baseOptions);
|
|
3930
|
-
if (apiError === "feature_not_enabled" || apiError === "not_allowed_token_type") return new SlackActionError(message, "feature_unavailable", baseOptions);
|
|
3931
|
-
if (apiError === "canvas_creation_failed") return new SlackActionError(message, "canvas_creation_failed", baseOptions);
|
|
3932
|
-
if (apiError === "canvas_editing_failed") return new SlackActionError(message, "canvas_editing_failed", baseOptions);
|
|
3933
|
-
if (candidate.code === "slack_webapi_rate_limited_error" || candidate.statusCode === 429) return new SlackActionError(message, "rate_limited", {
|
|
3934
|
-
...baseOptions,
|
|
3935
|
-
retryAfterSeconds: candidate.retryAfter
|
|
3936
|
-
});
|
|
3937
|
-
return new SlackActionError(message, "internal_error", baseOptions);
|
|
2376
|
+
function conversationIndexKey(conversationId) {
|
|
2377
|
+
return `${EVENT_PREFIX}:conversation:${digest(conversationId)}`;
|
|
3938
2378
|
}
|
|
3939
|
-
|
|
3940
|
-
|
|
3941
|
-
const CONNECTION_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
3942
|
-
"ECONNREFUSED",
|
|
3943
|
-
"ECONNRESET",
|
|
3944
|
-
"ENOTFOUND",
|
|
3945
|
-
"EAI_AGAIN"
|
|
3946
|
-
]);
|
|
3947
|
-
const TIMEOUT_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
3948
|
-
"ETIMEDOUT",
|
|
3949
|
-
"ECONNABORTED",
|
|
3950
|
-
"ESOCKETTIMEDOUT"
|
|
3951
|
-
]);
|
|
3952
|
-
function findNetworkErrorCode(error, depth = 0) {
|
|
3953
|
-
if (!error || typeof error !== "object" || depth > 4) return;
|
|
3954
|
-
const candidate = error;
|
|
3955
|
-
if (typeof candidate.code === "string" && (CONNECTION_ERROR_CODES.has(candidate.code) || TIMEOUT_ERROR_CODES.has(candidate.code))) return candidate.code;
|
|
3956
|
-
return findNetworkErrorCode(candidate.original, depth + 1) ?? findNetworkErrorCode(candidate.cause, depth + 1);
|
|
2379
|
+
function indexLockKey(key) {
|
|
2380
|
+
return `${key}:lock`;
|
|
3957
2381
|
}
|
|
3958
|
-
function
|
|
3959
|
-
|
|
2382
|
+
async function sleep(ms) {
|
|
2383
|
+
await new Promise((resolve) => {
|
|
2384
|
+
setTimeout(resolve, ms).unref?.();
|
|
2385
|
+
});
|
|
3960
2386
|
}
|
|
3961
|
-
|
|
3962
|
-
|
|
3963
|
-
|
|
3964
|
-
|
|
3965
|
-
|
|
3966
|
-
|
|
3967
|
-
|
|
3968
|
-
if (mapped.code === "rate_limited") return "rate_limited";
|
|
3969
|
-
if (mapped.statusCode !== void 0 && mapped.statusCode >= 500) return "server_error";
|
|
3970
|
-
const networkCode = findNetworkErrorCode(raw);
|
|
3971
|
-
if (networkCode && CONNECTION_ERROR_CODES.has(networkCode)) return "connection";
|
|
3972
|
-
if (networkCode && TIMEOUT_ERROR_CODES.has(networkCode)) return "timeout";
|
|
3973
|
-
if (hasSocketHangUpMessage(raw)) return "connection";
|
|
3974
|
-
return "none";
|
|
2387
|
+
async function acquireSubscriptionLock(state, subscriptionId, waitDeadlineMs = Date.now() + SUBSCRIPTION_LOCK_WAIT_MS) {
|
|
2388
|
+
while (true) {
|
|
2389
|
+
const lock = await state.acquireLock(subscriptionLockKey(subscriptionId), SUBSCRIPTION_LOCK_TTL_MS);
|
|
2390
|
+
if (lock) return lock;
|
|
2391
|
+
if (Date.now() >= waitDeadlineMs) throw new Error(`Could not acquire watch lock for ${subscriptionId}`);
|
|
2392
|
+
await sleep(SUBSCRIPTION_LOCK_RETRY_MS);
|
|
2393
|
+
}
|
|
3975
2394
|
}
|
|
3976
|
-
/**
|
|
3977
|
-
|
|
3978
|
-
|
|
3979
|
-
|
|
3980
|
-
|
|
3981
|
-
|
|
3982
|
-
|
|
3983
|
-
|
|
3984
|
-
|
|
3985
|
-
|
|
3986
|
-
|
|
3987
|
-
|
|
3988
|
-
|
|
3989
|
-
|
|
3990
|
-
|
|
3991
|
-
|
|
3992
|
-
|
|
3993
|
-
|
|
3994
|
-
|
|
3995
|
-
|
|
3996
|
-
|
|
3997
|
-
|
|
3998
|
-
|
|
3999
|
-
|
|
4000
|
-
|
|
4001
|
-
errorAttrs["http.response.status_code"] = 429;
|
|
4002
|
-
if (mapped.retryAfterSeconds) errorAttrs["app.slack.retry_after_ms"] = mapped.retryAfterSeconds * 1e3;
|
|
4003
|
-
} else if (mapped.statusCode != null) errorAttrs["http.response.status_code"] = mapped.statusCode;
|
|
4004
|
-
setSpanAttributes(errorAttrs);
|
|
4005
|
-
setSpanStatus("error");
|
|
4006
|
-
throw error;
|
|
4007
|
-
}
|
|
4008
|
-
}, {
|
|
4009
|
-
"http.request.method": "POST",
|
|
4010
|
-
"server.address": "slack.com",
|
|
4011
|
-
"url.scheme": "https",
|
|
4012
|
-
"url.path": `/api/${action}`,
|
|
4013
|
-
"app.slack.method": action,
|
|
4014
|
-
"app.retry.max_attempts": maxAttempts,
|
|
4015
|
-
...attemptNumber > 1 ? { "http.request.resend_count": attemptNumber - 1 } : void 0,
|
|
4016
|
-
...context.attributes ?? {},
|
|
4017
|
-
...context.spanAttributes ?? {}
|
|
4018
|
-
});
|
|
4019
|
-
} catch (error) {
|
|
4020
|
-
const mapped = mapSlackError(error);
|
|
4021
|
-
const retryClass = classifySlackRetry(error, mapped);
|
|
4022
|
-
const isRetryable = retryClass === "rate_limited" || retryClass === "connection" || retryClass === "server_error" || retryClass === "timeout" && context.idempotent === true;
|
|
4023
|
-
const remainingDelayBudgetMs = MAX_TOTAL_RETRY_DELAY_MS - totalDelayMs;
|
|
4024
|
-
const baseLogAttributes = {
|
|
4025
|
-
"app.slack.action": action,
|
|
4026
|
-
"app.slack.error_code": mapped.code,
|
|
4027
|
-
...mapped.apiError ? { "app.slack.api_error": mapped.apiError } : void 0,
|
|
4028
|
-
...mapped.detail ? { "app.slack.detail": mapped.detail } : void 0,
|
|
4029
|
-
...mapped.detailLine !== void 0 ? { "app.slack.detail_line": mapped.detailLine } : void 0,
|
|
4030
|
-
...mapped.detailRule ? { "app.slack.detail_rule": mapped.detailRule } : void 0,
|
|
4031
|
-
...mapped.requestId ? { "app.slack.request_id": mapped.requestId } : void 0,
|
|
4032
|
-
...mapped.statusCode !== void 0 ? { "http.response.status_code": mapped.statusCode } : void 0,
|
|
4033
|
-
...context.attributes ?? {}
|
|
4034
|
-
};
|
|
4035
|
-
if (!isRetryable || attempt >= maxAttempts || remainingDelayBudgetMs <= 0) {
|
|
4036
|
-
logWarn("slack.action.failed", {
|
|
4037
|
-
...baseLogAttributes,
|
|
4038
|
-
...mapped.errorData ? { "app.slack.error_data": mapped.errorData } : void 0
|
|
4039
|
-
});
|
|
4040
|
-
throw mapped;
|
|
4041
|
-
}
|
|
4042
|
-
logWarn("slack.action.retrying", {
|
|
4043
|
-
...baseLogAttributes,
|
|
4044
|
-
"app.slack.retry_attempt": attempt,
|
|
4045
|
-
"app.slack.retry_class": retryClass
|
|
4046
|
-
});
|
|
4047
|
-
const retryAfterMs = retryClass === "rate_limited" && mapped.retryAfterSeconds && mapped.retryAfterSeconds > 0 ? mapped.retryAfterSeconds * 1e3 : void 0;
|
|
4048
|
-
const backoffMs = Math.min(2e3, 250 * 2 ** (attempt - 1));
|
|
4049
|
-
const delayMs = Math.min(retryAfterMs ?? backoffMs, MAX_RETRY_DELAY_MS, remainingDelayBudgetMs);
|
|
4050
|
-
totalDelayMs += delayMs;
|
|
4051
|
-
await sleep$1(delayMs);
|
|
4052
|
-
}
|
|
2395
|
+
/** Run one subscription lifecycle transition while keeping its lease alive. */
|
|
2396
|
+
async function withSubscriptionLock(state, subscriptionId, callback, waitDeadlineMs) {
|
|
2397
|
+
const lock = await acquireSubscriptionLock(state, subscriptionId, waitDeadlineMs);
|
|
2398
|
+
let lockLostError;
|
|
2399
|
+
let heartbeatError;
|
|
2400
|
+
let heartbeat;
|
|
2401
|
+
const timer = setInterval(() => {
|
|
2402
|
+
if (heartbeat) return;
|
|
2403
|
+
heartbeat = state.extendLock(lock, SUBSCRIPTION_LOCK_TTL_MS).then((extended) => {
|
|
2404
|
+
if (extended) heartbeatError = void 0;
|
|
2405
|
+
else lockLostError = /* @__PURE__ */ new Error(`Watch lock was lost for ${subscriptionId}`);
|
|
2406
|
+
}).catch((error) => {
|
|
2407
|
+
heartbeatError = error instanceof Error ? error : /* @__PURE__ */ new Error(`Failed to extend subscription lock: ${String(error)}`);
|
|
2408
|
+
}).finally(() => {
|
|
2409
|
+
heartbeat = void 0;
|
|
2410
|
+
});
|
|
2411
|
+
}, SUBSCRIPTION_LOCK_HEARTBEAT_MS);
|
|
2412
|
+
timer.unref?.();
|
|
2413
|
+
let result;
|
|
2414
|
+
try {
|
|
2415
|
+
result = await callback();
|
|
2416
|
+
} finally {
|
|
2417
|
+
clearInterval(timer);
|
|
2418
|
+
await heartbeat;
|
|
2419
|
+
await state.releaseLock(lock);
|
|
4053
2420
|
}
|
|
4054
|
-
|
|
2421
|
+
const lockError = lockLostError ?? heartbeatError;
|
|
2422
|
+
if (lockError) throw lockError;
|
|
2423
|
+
return result;
|
|
4055
2424
|
}
|
|
4056
|
-
|
|
4057
|
-
|
|
4058
|
-
|
|
4059
|
-
|
|
4060
|
-
|
|
4061
|
-
|
|
4062
|
-
|
|
4063
|
-
|
|
4064
|
-
|
|
4065
|
-
|
|
4066
|
-
|
|
4067
|
-
|
|
2425
|
+
function ttlUntil(expiresAtMs, nowMs) {
|
|
2426
|
+
return Math.max(1, expiresAtMs - nowMs);
|
|
2427
|
+
}
|
|
2428
|
+
async function readSubscriptionIdIndex(state, key) {
|
|
2429
|
+
const value = await state.get(key);
|
|
2430
|
+
if (value === void 0 || value === null) return [];
|
|
2431
|
+
return subscriptionIdIndexSchema.parse(value);
|
|
2432
|
+
}
|
|
2433
|
+
function buildSubscriptionId(input) {
|
|
2434
|
+
const eventKey = [...new Set(input.events)].sort().join("\0");
|
|
2435
|
+
const matchKey = stableEventMatchKey(input.match);
|
|
2436
|
+
return `resub_${digest(matchKey ? `${input.namespace}\0${input.identifier}\0${input.conversationId}\0${eventKey}\0${matchKey}` : `${input.namespace}\0${input.identifier}\0${input.conversationId}\0${eventKey}`)}`;
|
|
2437
|
+
}
|
|
2438
|
+
async function withIndexLock(state, key, callback) {
|
|
2439
|
+
const lock = await state.acquireLock(indexLockKey(key), INDEX_LOCK_TTL_MS);
|
|
2440
|
+
if (!lock) throw new Error(`Could not acquire event index lock for ${key}`);
|
|
2441
|
+
try {
|
|
2442
|
+
return await callback();
|
|
2443
|
+
} finally {
|
|
2444
|
+
await state.releaseLock(lock);
|
|
2445
|
+
}
|
|
2446
|
+
}
|
|
2447
|
+
async function addToIndex(state, key, subscriptionId, nowMs) {
|
|
2448
|
+
await withIndexLock(state, key, async () => {
|
|
2449
|
+
const ids = [...new Set(await readSubscriptionIdIndex(state, key))];
|
|
2450
|
+
const next = ids.includes(subscriptionId) ? ids : [...ids, subscriptionId];
|
|
2451
|
+
await state.set(key, next, await indexTtlMs(state, next, nowMs));
|
|
4068
2452
|
});
|
|
4069
|
-
return getClient();
|
|
4070
2453
|
}
|
|
4071
|
-
|
|
4072
|
-
|
|
4073
|
-
|
|
4074
|
-
|
|
4075
|
-
|
|
4076
|
-
* - D: direct message (1:1)
|
|
4077
|
-
*/
|
|
4078
|
-
function isDmChannel(channelId) {
|
|
4079
|
-
const normalized = normalizeSlackConversationId(channelId);
|
|
4080
|
-
return Boolean(normalized && normalized.startsWith("D"));
|
|
2454
|
+
async function removeFromIndex(state, key, subscriptionId, nowMs) {
|
|
2455
|
+
await withIndexLock(state, key, async () => {
|
|
2456
|
+
const next = (await readSubscriptionIdIndex(state, key)).filter((id) => id !== subscriptionId);
|
|
2457
|
+
await state.set(key, next, await indexTtlMs(state, next, nowMs));
|
|
2458
|
+
});
|
|
4081
2459
|
}
|
|
4082
|
-
|
|
4083
|
-
|
|
4084
|
-
|
|
4085
|
-
*/
|
|
4086
|
-
function isConversationScopedChannel(channelId) {
|
|
4087
|
-
const normalized = normalizeSlackConversationId(channelId);
|
|
4088
|
-
if (!normalized) return false;
|
|
4089
|
-
return normalized.startsWith("C") || normalized.startsWith("G") || normalized.startsWith("D");
|
|
2460
|
+
async function indexTtlMs(state, subscriptionIds, nowMs) {
|
|
2461
|
+
const records = await Promise.all(subscriptionIds.map(async (id) => parseSubscription(await state.get(subscriptionKey(id)))));
|
|
2462
|
+
return ttlUntil(Math.max(nowMs, ...records.filter((record) => record !== void 0 && activeAt(record, nowMs)).map((record) => record.expiresAtMs)), nowMs);
|
|
4090
2463
|
}
|
|
4091
|
-
function
|
|
4092
|
-
|
|
4093
|
-
|
|
4094
|
-
return normalized.startsWith("C") || normalized.startsWith("G");
|
|
2464
|
+
function parseSubscription(value) {
|
|
2465
|
+
if (value === void 0 || value === null) return;
|
|
2466
|
+
return subscriptionSchema.parse(value);
|
|
4095
2467
|
}
|
|
4096
|
-
|
|
4097
|
-
|
|
4098
|
-
return (await withSlackRetries(() => client.files.info({ file: fileId }), 3, {
|
|
4099
|
-
action: "files.info",
|
|
4100
|
-
idempotent: true,
|
|
4101
|
-
spanAttributes: { "app.slack.file_id": fileId }
|
|
4102
|
-
})).file?.permalink;
|
|
2468
|
+
function activeAt(subscription, nowMs) {
|
|
2469
|
+
return subscription.status === "active" && subscription.expiresAtMs > nowMs;
|
|
4103
2470
|
}
|
|
4104
|
-
|
|
4105
|
-
|
|
4106
|
-
|
|
4107
|
-
|
|
4108
|
-
|
|
4109
|
-
|
|
4110
|
-
|
|
4111
|
-
|
|
4112
|
-
|
|
4113
|
-
|
|
4114
|
-
|
|
4115
|
-
|
|
4116
|
-
|
|
4117
|
-
|
|
4118
|
-
|
|
4119
|
-
|
|
2471
|
+
function matchesEvent(subscription, input) {
|
|
2472
|
+
return subscription.namespace === input.namespace && subscription.identifier === input.identifier && subscription.events.includes(input.eventType) && activeAt(subscription, input.nowMs) && eventMatches(subscription.match, input.data);
|
|
2473
|
+
}
|
|
2474
|
+
/** Create or replace the active subscription for one conversation/resource/event set. */
|
|
2475
|
+
async function createWatch(input, options = {}) {
|
|
2476
|
+
const state = options.state ?? getStateAdapter();
|
|
2477
|
+
await state.connect();
|
|
2478
|
+
const nowMs = options.nowMs ?? Date.now();
|
|
2479
|
+
const events = [...new Set(input.events.map((event) => event.trim()))].filter(Boolean);
|
|
2480
|
+
if (events.length === 0) throw new Error("Watch requires at least one event");
|
|
2481
|
+
if (input.expiresAtMs <= nowMs) throw new Error("Watch expiry must be in the future");
|
|
2482
|
+
const match = input.match && Object.keys(input.match).length > 0 ? input.match : void 0;
|
|
2483
|
+
const id = buildSubscriptionId({
|
|
2484
|
+
conversationId: input.conversationId,
|
|
2485
|
+
events,
|
|
2486
|
+
match,
|
|
2487
|
+
namespace: input.namespace,
|
|
2488
|
+
identifier: input.identifier
|
|
2489
|
+
});
|
|
2490
|
+
const record = {
|
|
2491
|
+
conversationId: input.conversationId,
|
|
2492
|
+
createdAtMs: nowMs,
|
|
2493
|
+
events,
|
|
2494
|
+
expiresAtMs: input.expiresAtMs,
|
|
2495
|
+
id,
|
|
2496
|
+
intent: input.intent,
|
|
2497
|
+
label: input.label,
|
|
2498
|
+
...match ? { match } : void 0,
|
|
2499
|
+
namespace: input.namespace,
|
|
2500
|
+
identifier: input.identifier,
|
|
2501
|
+
resourceType: input.resourceType,
|
|
2502
|
+
status: "active",
|
|
2503
|
+
updatedAtMs: nowMs
|
|
2504
|
+
};
|
|
2505
|
+
const parsed = subscriptionSchema.parse(record);
|
|
2506
|
+
await state.set(subscriptionKey(id), parsed, ttlUntil(parsed.expiresAtMs, nowMs));
|
|
2507
|
+
await addToIndex(state, resourceIndexKey(input.namespace, input.identifier), id, nowMs);
|
|
2508
|
+
await addToIndex(state, conversationIndexKey(input.conversationId), id, nowMs);
|
|
2509
|
+
return parsed;
|
|
2510
|
+
}
|
|
2511
|
+
/** Create a retry-stable timer without keeping a process open. */
|
|
2512
|
+
async function createTimerWatch(input) {
|
|
2513
|
+
const state = getStateAdapter();
|
|
2514
|
+
await state.connect();
|
|
2515
|
+
const identifier = digest(`${input.conversationId}\0${input.toolCallId}`);
|
|
2516
|
+
const selector = {
|
|
2517
|
+
conversationId: input.conversationId,
|
|
2518
|
+
namespace: "junior",
|
|
2519
|
+
identifier,
|
|
2520
|
+
events: ["timer.fired"]
|
|
2521
|
+
};
|
|
2522
|
+
const id = buildSubscriptionId(selector);
|
|
2523
|
+
return await withSubscriptionLock(state, id, async () => {
|
|
2524
|
+
const existing = parseSubscription(await state.get(subscriptionKey(id)));
|
|
2525
|
+
if (existing) return existing;
|
|
2526
|
+
const nowMs = Date.now();
|
|
2527
|
+
const firesAtMs = nowMs + input.afterMs;
|
|
2528
|
+
const expiresAtMs = firesAtMs + 864e5;
|
|
2529
|
+
const credentialSubject = input.creatorUserId ? bindTimerWatchCredentialSubject({
|
|
2530
|
+
conversationId: input.conversationId,
|
|
2531
|
+
userId: input.creatorUserId,
|
|
2532
|
+
watchId: id
|
|
2533
|
+
}) : void 0;
|
|
2534
|
+
const record = subscriptionSchema.parse({
|
|
2535
|
+
...selector,
|
|
2536
|
+
...credentialSubject ? { credentialSubject } : void 0,
|
|
2537
|
+
id,
|
|
2538
|
+
firesAtMs,
|
|
2539
|
+
expiresAtMs,
|
|
2540
|
+
resourceType: "timer",
|
|
2541
|
+
label: "Timer",
|
|
2542
|
+
intent: input.intent,
|
|
2543
|
+
status: "active",
|
|
2544
|
+
createdAtMs: nowMs,
|
|
2545
|
+
updatedAtMs: nowMs
|
|
2546
|
+
});
|
|
2547
|
+
await withIndexLock(state, conversationIndexKey(input.conversationId), async () => {
|
|
2548
|
+
const key = conversationIndexKey(input.conversationId);
|
|
2549
|
+
const ids = [.../* @__PURE__ */ new Set([...await readSubscriptionIdIndex(state, key), id])];
|
|
2550
|
+
await state.set(key, ids, Math.max(ttlUntil(expiresAtMs, nowMs), await indexTtlMs(state, ids, nowMs)));
|
|
2551
|
+
await state.set(resourceIndexKey("junior", identifier), [id], ttlUntil(expiresAtMs, nowMs));
|
|
2552
|
+
await updateTimerIndex(state, {
|
|
2553
|
+
kind: "add",
|
|
2554
|
+
id,
|
|
2555
|
+
atMs: firesAtMs
|
|
2556
|
+
});
|
|
2557
|
+
await state.set(subscriptionKey(id), record, ttlUntil(expiresAtMs, nowMs));
|
|
2558
|
+
});
|
|
2559
|
+
return record;
|
|
2560
|
+
});
|
|
2561
|
+
}
|
|
2562
|
+
/** Claim live timer Watches and discard stale due-index entries. */
|
|
2563
|
+
async function takeDueTimerWatches(nowMs) {
|
|
2564
|
+
const state = getStateAdapter();
|
|
2565
|
+
await state.connect();
|
|
2566
|
+
const watches = [];
|
|
2567
|
+
for (const id of await updateTimerIndex(state, {
|
|
2568
|
+
kind: "claim",
|
|
2569
|
+
nowMs
|
|
2570
|
+
})) await withSubscriptionLock(state, id, async () => {
|
|
2571
|
+
const watch = parseSubscription(await state.get(subscriptionKey(id)));
|
|
2572
|
+
if (watch && activeAt(watch, nowMs) && watch.firesAtMs !== void 0) {
|
|
2573
|
+
if (watch.firesAtMs <= nowMs) watches.push(watch);
|
|
2574
|
+
} else await updateTimerIndex(state, {
|
|
2575
|
+
kind: "remove",
|
|
2576
|
+
id
|
|
2577
|
+
});
|
|
2578
|
+
});
|
|
2579
|
+
return watches;
|
|
2580
|
+
}
|
|
2581
|
+
/** List active subscriptions bound to one conversation. */
|
|
2582
|
+
async function listWatches(input) {
|
|
2583
|
+
const state = input.state ?? getStateAdapter();
|
|
2584
|
+
await state.connect();
|
|
2585
|
+
const nowMs = input.nowMs ?? Date.now();
|
|
2586
|
+
const ids = await readSubscriptionIdIndex(state, conversationIndexKey(input.conversationId));
|
|
2587
|
+
return (await Promise.all(ids.map(async (id) => parseSubscription(await state.get(subscriptionKey(id)))))).filter((record) => record !== void 0 && record.conversationId === input.conversationId && activeAt(record, nowMs)).sort((left, right) => left.createdAtMs - right.createdAtMs);
|
|
2588
|
+
}
|
|
2589
|
+
/** Cancel a current-conversation subscription and remove it from match indexes. */
|
|
2590
|
+
async function cancelWatch(input) {
|
|
2591
|
+
const state = input.state ?? getStateAdapter();
|
|
2592
|
+
await state.connect();
|
|
2593
|
+
return await withSubscriptionLock(state, input.id, async () => {
|
|
2594
|
+
const current = parseSubscription(await state.get(subscriptionKey(input.id)));
|
|
2595
|
+
if (!current || current.conversationId !== input.conversationId) return;
|
|
2596
|
+
const nowMs = input.nowMs ?? Date.now();
|
|
2597
|
+
if (current.status === "completed") return current;
|
|
2598
|
+
const next = {
|
|
2599
|
+
...current,
|
|
2600
|
+
status: "cancelled",
|
|
2601
|
+
updatedAtMs: nowMs
|
|
2602
|
+
};
|
|
2603
|
+
await state.set(subscriptionKey(input.id), next, JUNIOR_THREAD_STATE_TTL_MS);
|
|
2604
|
+
await removeFromIndex(state, resourceIndexKey(current.namespace, current.identifier), input.id, nowMs);
|
|
2605
|
+
await removeFromIndex(state, conversationIndexKey(current.conversationId), input.id, nowMs);
|
|
2606
|
+
if (current.firesAtMs !== void 0) await updateTimerIndex(state, {
|
|
2607
|
+
kind: "remove",
|
|
2608
|
+
id: input.id
|
|
2609
|
+
});
|
|
2610
|
+
return next;
|
|
4120
2611
|
});
|
|
4121
2612
|
}
|
|
4122
|
-
|
|
4123
|
-
|
|
4124
|
-
const
|
|
4125
|
-
const
|
|
4126
|
-
|
|
4127
|
-
|
|
4128
|
-
|
|
4129
|
-
|
|
4130
|
-
|
|
4131
|
-
return [CREDENTIAL_SUBJECT_HMAC_CONTEXT, ...parts].join("\0");
|
|
4132
|
-
}
|
|
4133
|
-
function signPayload(secret, payload) {
|
|
4134
|
-
const digest = createHmac("sha256", secret).update(payload).digest("hex");
|
|
4135
|
-
return `${CREDENTIAL_SUBJECT_SIGNATURE_VERSION}=${digest}`;
|
|
4136
|
-
}
|
|
4137
|
-
function timingSafeMatch(expected, actual) {
|
|
4138
|
-
const expectedBuffer = Buffer.from(expected);
|
|
4139
|
-
const actualBuffer = Buffer.from(actual);
|
|
4140
|
-
if (expectedBuffer.length !== actualBuffer.length) return false;
|
|
4141
|
-
return timingSafeEqual(expectedBuffer, actualBuffer);
|
|
4142
|
-
}
|
|
4143
|
-
/** Create a delegated user credential subject for a verified Slack DM turn. */
|
|
4144
|
-
function createSlackDirectCredentialSubject(input) {
|
|
4145
|
-
const channelId = normalizeSlackConversationId(input.channelId);
|
|
4146
|
-
const teamId = input.teamId?.trim();
|
|
4147
|
-
const userId = parseActorUserId(input.userId);
|
|
4148
|
-
if (!channelId || !teamId || !userId || !isDmChannel(channelId)) return;
|
|
4149
|
-
return {
|
|
4150
|
-
type: "user",
|
|
4151
|
-
userId,
|
|
4152
|
-
allowedWhen: "private-direct-conversation"
|
|
4153
|
-
};
|
|
2613
|
+
/** Cancel every active watch bound to one conversation. */
|
|
2614
|
+
async function cancelSubscriptions(input) {
|
|
2615
|
+
const subscriptions = await listWatches(input);
|
|
2616
|
+
for (const subscription of subscriptions) await cancelWatch({
|
|
2617
|
+
conversationId: input.conversationId,
|
|
2618
|
+
id: subscription.id,
|
|
2619
|
+
nowMs: input.nowMs,
|
|
2620
|
+
state: input.state
|
|
2621
|
+
});
|
|
4154
2622
|
}
|
|
4155
|
-
/**
|
|
4156
|
-
|
|
4157
|
-
|
|
4158
|
-
|
|
4159
|
-
|
|
4160
|
-
const
|
|
4161
|
-
|
|
4162
|
-
|
|
4163
|
-
|
|
4164
|
-
|
|
4165
|
-
|
|
4166
|
-
|
|
4167
|
-
|
|
4168
|
-
|
|
4169
|
-
|
|
4170
|
-
|
|
4171
|
-
|
|
4172
|
-
|
|
4173
|
-
|
|
4174
|
-
|
|
4175
|
-
|
|
4176
|
-
|
|
2623
|
+
/**
|
|
2624
|
+
* Collect match keys from active watches for these identifiers and event types.
|
|
2625
|
+
* Does not evaluate match values — only reports keys that filters use.
|
|
2626
|
+
*/
|
|
2627
|
+
async function collectEventMatchKeys(input) {
|
|
2628
|
+
const state = input.state ?? getStateAdapter();
|
|
2629
|
+
await state.connect();
|
|
2630
|
+
const nowMs = input.nowMs ?? Date.now();
|
|
2631
|
+
const eventTypes = new Set(input.eventTypes.map((eventType) => eventType.trim()).filter(Boolean));
|
|
2632
|
+
if (eventTypes.size === 0) return [];
|
|
2633
|
+
const keys = /* @__PURE__ */ new Set();
|
|
2634
|
+
const identifiers = new Set(input.identifiers.map((value) => value.trim()).filter(Boolean));
|
|
2635
|
+
for (const identifier of identifiers) {
|
|
2636
|
+
const ids = await readSubscriptionIdIndex(state, resourceIndexKey(input.namespace, identifier));
|
|
2637
|
+
const records = await Promise.all(ids.map(async (id) => parseSubscription(await state.get(subscriptionKey(id)))));
|
|
2638
|
+
for (const record of records) {
|
|
2639
|
+
if (!record) continue;
|
|
2640
|
+
if (record.namespace !== input.namespace) continue;
|
|
2641
|
+
if (record.identifier !== identifier) continue;
|
|
2642
|
+
if (!activeAt(record, nowMs)) continue;
|
|
2643
|
+
if (!record.events.some((eventType) => eventTypes.has(eventType))) continue;
|
|
2644
|
+
for (const key of Object.keys(record.match ?? {})) keys.add(key);
|
|
4177
2645
|
}
|
|
4178
|
-
}
|
|
2646
|
+
}
|
|
2647
|
+
return [...keys].sort();
|
|
4179
2648
|
}
|
|
4180
|
-
/**
|
|
4181
|
-
function
|
|
4182
|
-
const
|
|
4183
|
-
|
|
4184
|
-
|
|
4185
|
-
const
|
|
4186
|
-
|
|
4187
|
-
|
|
4188
|
-
|
|
4189
|
-
|
|
4190
|
-
|
|
4191
|
-
|
|
4192
|
-
|
|
4193
|
-
])), binding.signature);
|
|
2649
|
+
/** Find active subscriptions interested in a normalized event. */
|
|
2650
|
+
async function findMatchingWatches(input) {
|
|
2651
|
+
const state = input.state ?? getStateAdapter();
|
|
2652
|
+
await state.connect();
|
|
2653
|
+
const nowMs = input.nowMs ?? Date.now();
|
|
2654
|
+
const ids = await readSubscriptionIdIndex(state, resourceIndexKey(input.namespace, input.identifier));
|
|
2655
|
+
return (await Promise.all(ids.map(async (id) => parseSubscription(await state.get(subscriptionKey(id)))))).filter((record) => record !== void 0 && matchesEvent(record, {
|
|
2656
|
+
data: input.data,
|
|
2657
|
+
eventType: input.eventType,
|
|
2658
|
+
nowMs,
|
|
2659
|
+
namespace: input.namespace,
|
|
2660
|
+
identifier: input.identifier
|
|
2661
|
+
}));
|
|
4194
2662
|
}
|
|
4195
|
-
/**
|
|
4196
|
-
function
|
|
4197
|
-
const
|
|
4198
|
-
|
|
4199
|
-
|
|
4200
|
-
|
|
4201
|
-
|
|
4202
|
-
|
|
4203
|
-
|
|
4204
|
-
|
|
4205
|
-
|
|
4206
|
-
|
|
4207
|
-
|
|
4208
|
-
|
|
4209
|
-
|
|
4210
|
-
|
|
4211
|
-
|
|
4212
|
-
|
|
4213
|
-
|
|
4214
|
-
|
|
4215
|
-
|
|
4216
|
-
|
|
4217
|
-
|
|
2663
|
+
/** Recheck and deliver a matched subscription while holding its status lock. */
|
|
2664
|
+
async function deliverWatch(input) {
|
|
2665
|
+
const state = input.state ?? getStateAdapter();
|
|
2666
|
+
await state.connect();
|
|
2667
|
+
return await withSubscriptionLock(state, input.subscription.id, async () => {
|
|
2668
|
+
const nowMs = input.nowMs ?? Date.now();
|
|
2669
|
+
const current = parseSubscription(await state.get(subscriptionKey(input.subscription.id)));
|
|
2670
|
+
if (!current || !matchesEvent(current, {
|
|
2671
|
+
data: input.data,
|
|
2672
|
+
eventType: input.eventType,
|
|
2673
|
+
nowMs,
|
|
2674
|
+
namespace: input.namespace,
|
|
2675
|
+
identifier: input.identifier
|
|
2676
|
+
})) return false;
|
|
2677
|
+
const delivered = await input.deliver(current);
|
|
2678
|
+
if (input.terminal) {
|
|
2679
|
+
const latest = parseSubscription(await state.get(subscriptionKey(current.id)));
|
|
2680
|
+
if (!latest || latest.status !== current.status || latest.updatedAtMs !== current.updatedAtMs) return delivered;
|
|
2681
|
+
const next = {
|
|
2682
|
+
...current,
|
|
2683
|
+
status: "completed",
|
|
2684
|
+
updatedAtMs: nowMs
|
|
2685
|
+
};
|
|
2686
|
+
await state.set(subscriptionKey(current.id), next, JUNIOR_THREAD_STATE_TTL_MS);
|
|
2687
|
+
await removeFromIndex(state, resourceIndexKey(current.namespace, current.identifier), current.id, nowMs);
|
|
2688
|
+
await removeFromIndex(state, conversationIndexKey(current.conversationId), current.id, nowMs);
|
|
4218
2689
|
}
|
|
4219
|
-
|
|
4220
|
-
|
|
4221
|
-
|
|
4222
|
-
|
|
4223
|
-
|
|
4224
|
-
|
|
4225
|
-
const binding = subject.binding;
|
|
4226
|
-
if (!secret || input.plugin !== "scheduler" || subject.type !== "user" || !isActorUserId(subject.userId) || subject.allowedWhen !== "scheduled-automation" || !subject.taskId || binding.type !== "scheduled-automation" || binding.plugin !== input.plugin || binding.taskId !== subject.taskId) return false;
|
|
4227
|
-
return timingSafeMatch(signPayload(secret, buildPayload([
|
|
4228
|
-
"scheduled-automation",
|
|
4229
|
-
binding.plugin,
|
|
4230
|
-
binding.taskId,
|
|
4231
|
-
subject.userId
|
|
4232
|
-
])), binding.signature);
|
|
2690
|
+
if (input.terminal && current.firesAtMs !== void 0) await updateTimerIndex(state, {
|
|
2691
|
+
kind: "remove",
|
|
2692
|
+
id: current.id
|
|
2693
|
+
});
|
|
2694
|
+
return delivered;
|
|
2695
|
+
}, input.waitDeadlineMs);
|
|
4233
2696
|
}
|
|
4234
|
-
|
|
4235
|
-
|
|
4236
|
-
|
|
4237
|
-
|
|
4238
|
-
|
|
4239
|
-
|
|
4240
|
-
|
|
4241
|
-
|
|
2697
|
+
//#endregion
|
|
2698
|
+
//#region src/chat/events/tool-support.ts
|
|
2699
|
+
/** Default lifetime for temporary watches. */
|
|
2700
|
+
const RESOURCE_SUBSCRIPTION_DEFAULT_TTL_MS = 12096e5;
|
|
2701
|
+
/** Hard upper bound for temporary watches. */
|
|
2702
|
+
const RESOURCE_SUBSCRIPTION_MAX_TTL_MS = 2592e6;
|
|
2703
|
+
const STOP_WATCHING_TOOL_NAME = "stopWatchingResources";
|
|
2704
|
+
const RESOURCE_WATCH_TOOL_SOURCE = {
|
|
2705
|
+
id: "resource-watches",
|
|
2706
|
+
description: "Inspect or stop watches for the current conversation."
|
|
2707
|
+
};
|
|
2708
|
+
//#endregion
|
|
2709
|
+
//#region src/chat/slack/tool-support/context.ts
|
|
2710
|
+
/** Resolve Slack tool context from the Conversation Location. */
|
|
2711
|
+
function getSlackToolContext(context) {
|
|
2712
|
+
const source = context.source.kind === "slack" ? context.source : void 0;
|
|
2713
|
+
const location = context.location?.provider === "slack" ? context.location : source ? {
|
|
2714
|
+
provider: "slack",
|
|
2715
|
+
teamId: source.teamId,
|
|
2716
|
+
channelId: source.channelId,
|
|
2717
|
+
threadTs: source.threadTs
|
|
2718
|
+
} : void 0;
|
|
2719
|
+
if (!location) return void 0;
|
|
2720
|
+
const destinationChannelId = parseSlackChannelReferenceId(context.destination.platform === "slack" ? context.destination.channelId : location.channelId);
|
|
2721
|
+
const locationChannelId = parseSlackChannelReferenceId(location.channelId);
|
|
2722
|
+
const teamId = parseSlackTeamId(location.teamId);
|
|
2723
|
+
if (!destinationChannelId || !locationChannelId || !teamId) return;
|
|
4242
2724
|
return {
|
|
4243
|
-
|
|
4244
|
-
|
|
4245
|
-
|
|
4246
|
-
|
|
4247
|
-
|
|
4248
|
-
|
|
4249
|
-
|
|
4250
|
-
taskId,
|
|
4251
|
-
signature: signPayload(secret, buildPayload([
|
|
4252
|
-
"event-automation",
|
|
4253
|
-
plugin,
|
|
4254
|
-
taskId,
|
|
4255
|
-
userId
|
|
4256
|
-
]))
|
|
4257
|
-
}
|
|
2725
|
+
actor: context.actor?.platform === "slack" ? context.actor : void 0,
|
|
2726
|
+
destinationChannelId,
|
|
2727
|
+
locationChannelId,
|
|
2728
|
+
messageChannelId: source ? parseSlackChannelReferenceId(source.channelId) : void 0,
|
|
2729
|
+
messageTs: parseSlackMessageTs(source?.messageTs),
|
|
2730
|
+
teamId,
|
|
2731
|
+
threadTs: parseSlackMessageTs(location.threadTs)
|
|
4258
2732
|
};
|
|
4259
2733
|
}
|
|
4260
|
-
/** Verify that a delegated subject was signed for one event automation. */
|
|
4261
|
-
function verifyEventAutomationCredentialSubject(input) {
|
|
4262
|
-
const secret = getCredentialSubjectSecret();
|
|
4263
|
-
const { subject } = input;
|
|
4264
|
-
const binding = subject.binding;
|
|
4265
|
-
if (!secret || input.plugin !== "junior" || subject.type !== "user" || !isActorUserId(subject.userId) || subject.allowedWhen !== "event-automation" || !subject.taskId || binding.type !== "event-automation" || binding.plugin !== input.plugin || binding.taskId !== subject.taskId) return false;
|
|
4266
|
-
return timingSafeMatch(signPayload(secret, buildPayload([
|
|
4267
|
-
"event-automation",
|
|
4268
|
-
binding.plugin,
|
|
4269
|
-
binding.taskId,
|
|
4270
|
-
subject.userId
|
|
4271
|
-
])), binding.signature);
|
|
4272
|
-
}
|
|
4273
2734
|
//#endregion
|
|
4274
2735
|
//#region src/chat/slack/tool-support/channel-capabilities.ts
|
|
4275
2736
|
/** Resolve channel capabilities from a Slack channel ID. */
|
|
@@ -5743,4 +4204,4 @@ function createPluginHookRunner(input = {}) {
|
|
|
5743
4204
|
};
|
|
5744
4205
|
}
|
|
5745
4206
|
//#endregion
|
|
5746
|
-
export {
|
|
4207
|
+
export { verifySlackDirectCredentialSubject as $, getWorkspaceSnapshotBuilder as A, viewerConversationMembership as At, cancelWatch as B, saveObjectAnnotations as Bt, WorkspaceValidationError as C, conversationArchiveTimes as Ct, setWorkspaceSnapshotBuild as D, conversationHasParticipantUser as Dt, setWorkspaceSnapshot as E, conversationHasParticipantEmail as Et, RESOURCE_SUBSCRIPTION_DEFAULT_TTL_MS as F, getSlackFileId as Ft, findMatchingWatches as G, agentsInstructionsUpdatedEvent as Gt, createTimerWatch as H, listConversationAnnotations as Ht, RESOURCE_SUBSCRIPTION_MAX_TTL_MS as I, matchSlackAttachments as It, bindEventAutomationCredentialSubject as J, listWatches as K, authenticationLinkedEvent as Kt, RESOURCE_WATCH_TOOL_SOURCE as L, readAttachmentVisionSummary as Lt, workspaceRepoCheckoutPath as M, readLatestConversationBrief as Mt, resolveChannelCapabilities as N, readConversationEventPage as Nt, SANDBOX_RUNTIME as O, conversationNotArchivedForEmail as Ot, getSlackToolContext as P, buildSentryConversationUrl as Pt, verifyScheduledAutomationCredentialSubject as Q, STOP_WATCHING_TOOL_NAME as R, recordSlackAttachment as Rt, updateWorkspace as S, readConversationAccessFromSql as St, loadSnapshotsForProfile as T, conversationArchivedForUser as Tt, createWatch as U, listConversationAnnotationsById as Ut, collectEventMatchKeys as V, createPluginAnnotations as Vt, deliverWatch as W, JUNIOR_NATIVE_EVENT_NAMESPACE as Wt, bindSlackDirectCredentialSubject as X, bindScheduledAutomationCredentialSubject as Y, verifyEventAutomationCredentialSubject as Z, deleteWorkspace as _, setDashboardConversationLinkOptions as _t, getPluginRoutes as a, getSlackClient as at, listWorkspaces as b, createPluginModel as bt, getPluginTools as c, normalizeSlackConversationId as ct, setPlugins as d, getWorkspaceTeamId as dt, verifyTimerWatchCredentialSubject as et, validatePlugins as f, runWithWorkspaceTeamId as ft, createWorkspace as g, getDashboardTaskLink as gt, setBriefsConfig as h, getDashboardObjectIconLink as ht, getPluginApiRoutes as i, getHeaderString as it, tryWorkspaceRepoCheckoutPath as j, createPluginConversationEvents as jt, deleteWorkspaceSnapshotBuilders as k, conversationNotArchivedForUser as kt, getPluginUserPromptContributions as l, runWithSlackInstallationToken as lt, registration_exports as m, getDashboardConversationLink as mt, applyPluginFormatMarkdown as n, downloadPrivateSlackFile as nt, getPluginSlackConversationLink as o, isConversationScopedChannel as ot, coreTaskRegistrations as p, JUNIOR_THREAD_STATE_TTL_MS as pt, takeDueTimerWatches as q, authenticationUnlinkedEvent as qt, createPluginHookRunner as r, getFilePermalink as rt, getPluginSystemPromptContributions as s, isDmChannel as st, agent_hooks_exports as t, SlackActionError as tt, getPlugins as u, withSlackRetries as ut, getWorkspace as v, createPluginState as vt, invalidateReadySnapshot as w, conversationArchivedForEmail as wt, listWorkspacesByRepository as x, createPluginLogger as xt, getWorkspaceByName as y, createPluginEmbedder as yt, cancelSubscriptions as z, resolveRootVisibility as zt };
|