@sentry/junior 0.239.0 → 0.241.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/{admin-Cc-iA6z9.js → admin-CJkjl-aN.js} +1 -1
- package/dist/{agent-hooks-DzfA5m3R.js → agent-hooks-C5zYOaJk.js} +1972 -1866
- package/dist/{agent-runner--1wC26S6.js → agent-runner-CMZ6lFQe.js} +377 -1227
- package/dist/api/schema.d.ts +9 -0
- package/dist/api/schema.js +2 -2
- package/dist/api.d.ts +1 -1
- package/dist/api.js +22 -21
- package/dist/{app-FB2hmHpZ.d.ts → app-MEd7wbF0.d.ts} +1 -1
- package/dist/app.d.ts +1 -1
- package/dist/app.js +105 -31
- package/dist/{automation-CrQGQ2MT.js → automation-gznOMaQ2.js} +13 -255
- package/dist/{briefs-DGc2Thua.js → briefs-CsQbFjRi.js} +2 -2
- package/dist/cadence-D00SCzT1.js +256 -0
- package/dist/{catalog-runtime-DpB_hG_I.js → catalog-runtime-Tz0O2MZe.js} +1 -1
- package/dist/cli/chat.js +12 -12
- package/dist/cli/check.js +3 -3
- package/dist/cli/main.js +2 -2
- package/dist/cli/plugins.js +4 -4
- 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-DNf-sJmM.js → client-CRZ-i1tH.js} +2 -2
- package/dist/{config-B8QN-04U.js → config-CCI82OTD.js} +2 -2
- package/dist/{conversation-privacy-CrYizZM3.js → conversation-privacy-D5Byl5eC.js} +1 -1
- package/dist/{credential-sync-BG_Yvja8.js → credential-sync-DUpSxUum.js} +14 -14
- package/dist/{db-CWZP77Ex.d.ts → db-Ca9Nt5ci.d.ts} +1 -1
- package/dist/{executor-BQV1Zc1z.js → executor-CewKi7EG.js} +4 -2
- package/dist/{images-C_0iwPsW.js → images-DY6Pxq4w.js} +2 -2
- package/dist/instrumentation.js +2 -2
- package/dist/{job-queue-D82gksYM.js → job-queue-C6o2aJQ6.js} +1 -1
- package/dist/{logging-Crg0e1jb.js → logging-LQnLlPf8.js} +1 -1
- package/dist/nitro.d.ts +1 -1
- package/dist/nitro.js +3 -3
- package/dist/{oauth-authorization-Bdu39G-H.js → oauth-authorization-Clzkm-Ts.js} +1 -1
- package/dist/{oauth-callback-server-eJfI6skP.js → oauth-callback-server-DcYAJSEz.js} +2 -2
- package/dist/{pending-auth-DauOaies.js → pending-auth-CT3ojhKe.js} +2 -2
- package/dist/{profile-XzHSfIah.js → profile-mWuVZ8ht.js} +4 -4
- package/dist/{registry-BJME30oR.js → registry-DPDH6s5G.js} +1 -1
- package/dist/{resolve-ZKXfsZuZ.js → resolve-BAN8BSGy.js} +2 -2
- package/dist/{runner-DkHL8vRA.js → runner-CqXZJfzk.js} +9 -9
- package/dist/{sandbox-egress-signals-BEQsK7ym.js → sandbox-egress-signals-DFUUokws.js} +1 -1
- package/dist/{schema-CxWAN2zq.js → schema-Cabm4jcj.js} +1 -1
- package/dist/{skills-CMl248J3.js → skills-BtHhSATy.js} +2 -2
- package/dist/{spawn-DrvZYrTQ.js → spawn-BszkFJKJ.js} +2 -2
- package/dist/{store-BxxbxBlm.d.ts → store-DoewL5mG.d.ts} +0 -7
- package/dist/{task-queue-BMzTARMo.js → task-queue-1LP6benz.js} +3 -3
- package/dist/{task-runner-CXTpWSmw.js → task-runner-kLN3HC-_.js} +54 -12
- package/dist/{checkpoint-CN7yMfSo.js → tasks-CO52vLkI.js} +1064 -18
- package/dist/{thread-state-BM1sLptE.js → thread-state-CaFRG5Mh.js} +6 -6
- package/dist/{turn-failure-response-ISrbka5I.js → turn-failure-response-Dmovuiq1.js} +9 -8
- package/dist/{turn-lifecycle-f7TPxQsc.js → turn-lifecycle-DGqZgU6J.js} +1 -1
- package/dist/{turn-session-routing-BcAD7onh.js → turn-session-routing-B_v8Wbn4.js} +1 -1
- package/dist/{validation-Bo6BHmig.js → validation-B4L2Rmiw.js} +1 -1
- package/dist/{version-Cv__n5ue.js → version-DiR36OaN.js} +1 -1
- package/dist/version.js +1 -1
- package/dist/{viewer-EftpWXcF.js → viewer-D6stAU7h.js} +9 -11
- package/dist/{web-authorization-BLDXKk2u.js → web-authorization-DLMBx2nL.js} +6 -6
- package/dist/{web-input-DSa6geKr.js → web-input---xuox1t.js} +136 -147
- package/dist/{work-WOk5DovU.js → work-G-HdoOSF.js} +7 -7
- package/dist/{worker-B7rtMFzw.js → worker-CDUDfL9P.js} +5 -5
- package/migrations/0049_event_automation_status_reason.sql +1 -0
- package/migrations/meta/0049_snapshot.json +3581 -0
- package/migrations/meta/_journal.json +8 -1
- package/package.json +5 -5
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
import { t as __exportAll } from "./rolldown-runtime-B4FMCO8f.js";
|
|
2
|
-
import { a as embedTexts, r as completeObject } from "./client-
|
|
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-
|
|
2
|
+
import { a as embedTexts, r as completeObject } from "./client-CRZ-i1tH.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-LQnLlPf8.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-D5Byl5eC.js";
|
|
6
|
+
import { i as getChatConfig, n as botConfig, o as getSlackBotToken } from "./config-CCI82OTD.js";
|
|
7
7
|
import { r as defaultModelId } from "./model-profile-BwU8mqwD.js";
|
|
8
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,
|
|
9
|
+
import { O as readUserTimezone, T as hasAgentTurnUsage, c as getConversationEventStore, d as getDb, f as getSqlExecutor, h as createSqlStore, i as resolveViewerUser, k as saveUserTimezone, r as readActorIdentity } from "./viewer-D6stAU7h.js";
|
|
10
10
|
import { n as juniorDestinations } from "./destinations-BeBiJeEM.js";
|
|
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-
|
|
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-CewKi7EG.js";
|
|
12
12
|
import { o as decodeStoredConversationEvent } from "./history-BLTUgFYO.js";
|
|
13
|
-
import { p as credentialSubjectSchema } from "./registry-
|
|
13
|
+
import { p as credentialSubjectSchema } from "./registry-DPDH6s5G.js";
|
|
14
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-
|
|
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-mWuVZ8ht.js";
|
|
16
|
+
import { i as isValidTimeZone } from "./cadence-D00SCzT1.js";
|
|
16
17
|
import { n as briefEntriesFromReportEvents, s as generateBrief, t as BRIEF_PROMPT } from "./prompt-CMU-B4FA.js";
|
|
17
18
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
18
19
|
import { z } from "zod";
|
|
@@ -142,2005 +143,2095 @@ function renderJuniorNativeConversationEvent(args) {
|
|
|
142
143
|
return presentation === void 0 ? void 0 : conversationEventPresentationSchema.parse(presentation);
|
|
143
144
|
}
|
|
144
145
|
//#endregion
|
|
145
|
-
//#region src/chat/
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
return
|
|
150
|
-
|
|
151
|
-
plugin: row.plugin,
|
|
152
|
-
createdAt: row.createdAt.toISOString(),
|
|
153
|
-
updatedAt: row.updatedAt.toISOString()
|
|
154
|
-
};
|
|
155
|
-
}
|
|
156
|
-
function createPluginAnnotations(args) {
|
|
157
|
-
return {
|
|
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
|
-
}
|
|
188
|
-
};
|
|
146
|
+
//#region src/chat/slack/workspace-context.ts
|
|
147
|
+
const workspaceTeamIdStorage = new AsyncLocalStorage();
|
|
148
|
+
/** Run a callback with the Slack workspace team ID for the inbound webhook. */
|
|
149
|
+
function runWithWorkspaceTeamId(teamId, fn) {
|
|
150
|
+
if (!teamId) return fn();
|
|
151
|
+
return workspaceTeamIdStorage.run(teamId, fn);
|
|
189
152
|
}
|
|
190
|
-
/**
|
|
191
|
-
|
|
192
|
-
return
|
|
153
|
+
/** Return the Slack workspace team ID for the current inbound webhook. */
|
|
154
|
+
function getWorkspaceTeamId() {
|
|
155
|
+
return workspaceTeamIdStorage.getStore();
|
|
193
156
|
}
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
157
|
+
//#endregion
|
|
158
|
+
//#region src/chat/slack/client.ts
|
|
159
|
+
var SlackActionError = class extends Error {
|
|
160
|
+
code;
|
|
161
|
+
apiError;
|
|
162
|
+
needed;
|
|
163
|
+
provided;
|
|
164
|
+
statusCode;
|
|
165
|
+
requestId;
|
|
166
|
+
errorData;
|
|
167
|
+
retryAfterSeconds;
|
|
168
|
+
detail;
|
|
169
|
+
detailLine;
|
|
170
|
+
detailRule;
|
|
171
|
+
constructor(message, code, options = {}) {
|
|
172
|
+
super(message);
|
|
173
|
+
this.name = "SlackActionError";
|
|
174
|
+
this.code = code;
|
|
175
|
+
this.apiError = options.apiError;
|
|
176
|
+
this.needed = options.needed;
|
|
177
|
+
this.provided = options.provided;
|
|
178
|
+
this.statusCode = options.statusCode;
|
|
179
|
+
this.requestId = options.requestId;
|
|
180
|
+
this.errorData = options.errorData;
|
|
181
|
+
this.retryAfterSeconds = options.retryAfterSeconds;
|
|
182
|
+
this.detail = options.detail;
|
|
183
|
+
this.detailLine = options.detailLine;
|
|
184
|
+
this.detailRule = options.detailRule;
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
function serializeSlackErrorData(data) {
|
|
188
|
+
if (!data || typeof data !== "object") return;
|
|
189
|
+
const filtered = Object.fromEntries(Object.entries(data).filter(([key]) => key !== "error"));
|
|
190
|
+
if (Object.keys(filtered).length === 0) return;
|
|
191
|
+
try {
|
|
192
|
+
const serialized = JSON.stringify(filtered);
|
|
193
|
+
return serialized.length <= 600 ? serialized : `${serialized.slice(0, 597)}...`;
|
|
194
|
+
} catch {
|
|
195
|
+
return;
|
|
205
196
|
}
|
|
206
|
-
return byConversation;
|
|
207
197
|
}
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
const
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
for (const annotation of annotations) {
|
|
220
|
-
await store.upsert(annotation);
|
|
221
|
-
await store.remove("resource_link", annotation.key);
|
|
198
|
+
/** Extract a header value by case-insensitive name from a raw headers object. */
|
|
199
|
+
function getHeaderString(headers, name) {
|
|
200
|
+
if (!headers || typeof headers !== "object") return;
|
|
201
|
+
const key = name.toLowerCase();
|
|
202
|
+
const entries = headers;
|
|
203
|
+
for (const [entryKey, value] of Object.entries(entries)) {
|
|
204
|
+
if (entryKey.toLowerCase() !== key) continue;
|
|
205
|
+
if (typeof value === "string") return value;
|
|
206
|
+
if (Array.isArray(value)) {
|
|
207
|
+
const first = value.find((entry) => typeof entry === "string");
|
|
208
|
+
return typeof first === "string" ? first : void 0;
|
|
222
209
|
}
|
|
223
|
-
}
|
|
224
|
-
return annotations.map((annotation) => ({
|
|
225
|
-
...annotation,
|
|
226
|
-
plugin
|
|
227
|
-
}));
|
|
210
|
+
}
|
|
228
211
|
}
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
const
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
...result,
|
|
242
|
-
details: annotated
|
|
243
|
-
};
|
|
244
|
-
return annotated;
|
|
212
|
+
function parseSlackCanvasDetail(detail) {
|
|
213
|
+
if (typeof detail !== "string") return {};
|
|
214
|
+
const trimmed = detail.trim();
|
|
215
|
+
if (!trimmed) return {};
|
|
216
|
+
const parsed = { detail: trimmed };
|
|
217
|
+
const lineMatch = trimmed.match(/line\s+(\d+):/i);
|
|
218
|
+
if (lineMatch) {
|
|
219
|
+
const line = Number.parseInt(lineMatch[1] ?? "", 10);
|
|
220
|
+
if (Number.isFinite(line)) parsed.detailLine = line;
|
|
221
|
+
}
|
|
222
|
+
if (/unsupported heading depth/i.test(trimmed)) parsed.detailRule = "unsupported_heading_depth";
|
|
223
|
+
return parsed;
|
|
245
224
|
}
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
rootConversationId: row.rootConversationId
|
|
263
|
-
} : void 0;
|
|
225
|
+
/**
|
|
226
|
+
* Ambient destination-installation token for Slack outbound calls, bound by
|
|
227
|
+
* installation-scoped entry points so writes carry the destination
|
|
228
|
+
* workspace's credentials instead of the process-global env token.
|
|
229
|
+
*/
|
|
230
|
+
const installationTokenStorage = new AsyncLocalStorage();
|
|
231
|
+
const clientsByToken = /* @__PURE__ */ new Map();
|
|
232
|
+
/**
|
|
233
|
+
* Bind a workspace installation token for all Slack Web API calls inside
|
|
234
|
+
* `fn`, so multi-workspace outbound writes use the destination workspace's
|
|
235
|
+
* credentials rather than the env fallback token.
|
|
236
|
+
*/
|
|
237
|
+
function runWithSlackInstallationToken(token, fn) {
|
|
238
|
+
const trimmed = token.trim();
|
|
239
|
+
if (!trimmed) throw new SlackActionError("Slack installation token binding requires a non-empty token", "missing_token");
|
|
240
|
+
return installationTokenStorage.run({ token: trimmed }, fn);
|
|
264
241
|
}
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
242
|
+
/**
|
|
243
|
+
* Token precedence: ambient installation token, then env token, then a hard
|
|
244
|
+
* failure for team-scoped calls — a call scoped to one workspace must never
|
|
245
|
+
* silently fall through to another workspace's credentials.
|
|
246
|
+
*/
|
|
247
|
+
function resolveSlackToken() {
|
|
248
|
+
const ambientToken = installationTokenStorage.getStore()?.token;
|
|
249
|
+
if (ambientToken) return ambientToken;
|
|
250
|
+
const envToken = getSlackBotToken();
|
|
251
|
+
if (envToken) return envToken;
|
|
252
|
+
const teamId = getWorkspaceTeamId();
|
|
253
|
+
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");
|
|
254
|
+
throw new SlackActionError("SLACK_BOT_TOKEN (or SLACK_BOT_USER_TOKEN) is required for Slack Web API actions in this service", "missing_token");
|
|
255
|
+
}
|
|
256
|
+
/** Normalize Junior Slack references to native Slack conversation IDs. */
|
|
257
|
+
function normalizeSlackConversationId(channelId) {
|
|
258
|
+
return parseSlackChannelReferenceId(channelId);
|
|
271
259
|
}
|
|
272
260
|
/**
|
|
273
|
-
*
|
|
274
|
-
*
|
|
275
|
-
*
|
|
276
|
-
*
|
|
277
|
-
*
|
|
261
|
+
* Return the per-token cached WebClient. `withSlackRetries` owns retry
|
|
262
|
+
* classification for this boundary: the WebClient's built-in policy would
|
|
263
|
+
* re-post timed-out writes (a duplicate-message hazard) and sleep for the
|
|
264
|
+
* full unbounded Retry-After inside the request, so both internal behaviors
|
|
265
|
+
* are disabled here.
|
|
278
266
|
*/
|
|
279
|
-
|
|
280
|
-
const
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
267
|
+
function getClient() {
|
|
268
|
+
const token = resolveSlackToken();
|
|
269
|
+
let cached = clientsByToken.get(token);
|
|
270
|
+
if (!cached) {
|
|
271
|
+
cached = new WebClient(token, {
|
|
272
|
+
retryConfig: { retries: 0 },
|
|
273
|
+
rejectRateLimitedCalls: true
|
|
274
|
+
});
|
|
275
|
+
clientsByToken.set(token, cached);
|
|
276
|
+
}
|
|
277
|
+
return cached;
|
|
286
278
|
}
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
const
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
279
|
+
function mapSlackError(error) {
|
|
280
|
+
if (error instanceof SlackActionError) return error;
|
|
281
|
+
const candidate = error;
|
|
282
|
+
const apiError = candidate.data?.error;
|
|
283
|
+
const message = candidate.message ?? "Slack action failed";
|
|
284
|
+
const baseOptions = {
|
|
285
|
+
apiError,
|
|
286
|
+
statusCode: candidate.statusCode,
|
|
287
|
+
requestId: getHeaderString(candidate.headers, "x-slack-req-id"),
|
|
288
|
+
errorData: serializeSlackErrorData(candidate.data),
|
|
289
|
+
...parseSlackCanvasDetail(candidate.data?.detail)
|
|
290
|
+
};
|
|
291
|
+
if (apiError === "missing_scope") return new SlackActionError(message, "missing_scope", {
|
|
292
|
+
...baseOptions,
|
|
293
|
+
needed: candidate.data?.needed,
|
|
294
|
+
provided: candidate.data?.provided
|
|
295
|
+
});
|
|
296
|
+
if (apiError === "not_in_channel") return new SlackActionError(message, "not_in_channel", baseOptions);
|
|
297
|
+
if (apiError === "restricted_action_read_only_channel") return new SlackActionError(message, "read_only_channel", baseOptions);
|
|
298
|
+
if (apiError === "already_reacted") return new SlackActionError(message, "already_reacted", baseOptions);
|
|
299
|
+
if (apiError === "no_reaction") return new SlackActionError(message, "no_reaction", baseOptions);
|
|
300
|
+
if (apiError === "invalid_arguments") return new SlackActionError(message, "invalid_arguments", baseOptions);
|
|
301
|
+
if (apiError === "invalid_cursor") return new SlackActionError(message, "invalid_arguments", baseOptions);
|
|
302
|
+
if (apiError === "invalid_name") return new SlackActionError(message, "invalid_arguments", baseOptions);
|
|
303
|
+
if (apiError === "not_found" || apiError === "channel_not_found" || apiError === "message_not_found") return new SlackActionError(message, "not_found", baseOptions);
|
|
304
|
+
if (apiError === "feature_not_enabled" || apiError === "not_allowed_token_type") return new SlackActionError(message, "feature_unavailable", baseOptions);
|
|
305
|
+
if (apiError === "canvas_creation_failed") return new SlackActionError(message, "canvas_creation_failed", baseOptions);
|
|
306
|
+
if (apiError === "canvas_editing_failed") return new SlackActionError(message, "canvas_editing_failed", baseOptions);
|
|
307
|
+
if (candidate.code === "slack_webapi_rate_limited_error" || candidate.statusCode === 429) return new SlackActionError(message, "rate_limited", {
|
|
308
|
+
...baseOptions,
|
|
309
|
+
retryAfterSeconds: candidate.retryAfter
|
|
310
|
+
});
|
|
311
|
+
return new SlackActionError(message, "internal_error", baseOptions);
|
|
313
312
|
}
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
313
|
+
const MAX_RETRY_DELAY_MS = 1e4;
|
|
314
|
+
const MAX_TOTAL_RETRY_DELAY_MS = 2e4;
|
|
315
|
+
const CONNECTION_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
316
|
+
"ECONNREFUSED",
|
|
317
|
+
"ECONNRESET",
|
|
318
|
+
"ENOTFOUND",
|
|
319
|
+
"EAI_AGAIN"
|
|
320
|
+
]);
|
|
321
|
+
const TIMEOUT_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
322
|
+
"ETIMEDOUT",
|
|
323
|
+
"ECONNABORTED",
|
|
324
|
+
"ESOCKETTIMEDOUT"
|
|
325
|
+
]);
|
|
326
|
+
function findNetworkErrorCode(error, depth = 0) {
|
|
327
|
+
if (!error || typeof error !== "object" || depth > 4) return;
|
|
328
|
+
const candidate = error;
|
|
329
|
+
if (typeof candidate.code === "string" && (CONNECTION_ERROR_CODES.has(candidate.code) || TIMEOUT_ERROR_CODES.has(candidate.code))) return candidate.code;
|
|
330
|
+
return findNetworkErrorCode(candidate.original, depth + 1) ?? findNetworkErrorCode(candidate.cause, depth + 1);
|
|
318
331
|
}
|
|
319
|
-
|
|
320
|
-
|
|
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)));
|
|
332
|
+
function hasSocketHangUpMessage(error) {
|
|
333
|
+
return error instanceof Error && error.message.toLowerCase().includes("socket hang up");
|
|
329
334
|
}
|
|
330
|
-
/**
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
+
/**
|
|
336
|
+
* Classify a failed Slack call for retry: rate limits, Slack 5xx, and
|
|
337
|
+
* connection-phase failures are always retryable; timeouts are retried only
|
|
338
|
+
* for idempotent operations because Slack may already have accepted the
|
|
339
|
+
* request.
|
|
340
|
+
*/
|
|
341
|
+
function classifySlackRetry(raw, mapped) {
|
|
342
|
+
if (mapped.code === "rate_limited") return "rate_limited";
|
|
343
|
+
if (mapped.statusCode !== void 0 && mapped.statusCode >= 500) return "server_error";
|
|
344
|
+
const networkCode = findNetworkErrorCode(raw);
|
|
345
|
+
if (networkCode && CONNECTION_ERROR_CODES.has(networkCode)) return "connection";
|
|
346
|
+
if (networkCode && TIMEOUT_ERROR_CODES.has(networkCode)) return "timeout";
|
|
347
|
+
if (hasSocketHangUpMessage(raw)) return "connection";
|
|
348
|
+
return "none";
|
|
335
349
|
}
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
const
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
const
|
|
352
|
-
|
|
353
|
-
return
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
350
|
+
/**
|
|
351
|
+
* Run a Slack Web API call with bounded retries so transient platform
|
|
352
|
+
* failures do not surface as turn failures, while non-idempotent posts are
|
|
353
|
+
* never re-sent when Slack may already have accepted them.
|
|
354
|
+
*
|
|
355
|
+
* Retry classes: rate limits (bounded Retry-After), connection-phase network
|
|
356
|
+
* failures, and Slack 5xx responses always retry; request timeouts retry only
|
|
357
|
+
* when the caller marks the operation `idempotent`.
|
|
358
|
+
*/
|
|
359
|
+
async function withSlackRetries(task, maxAttempts = 3, context = {}) {
|
|
360
|
+
let attempt = 0;
|
|
361
|
+
let totalDelayMs = 0;
|
|
362
|
+
const action = context.action ?? "unknown";
|
|
363
|
+
while (attempt < maxAttempts) {
|
|
364
|
+
attempt += 1;
|
|
365
|
+
const attemptNumber = attempt;
|
|
366
|
+
try {
|
|
367
|
+
return await withSpan(`POST slack.com/api/${action}`, "http.client", {}, async () => {
|
|
368
|
+
try {
|
|
369
|
+
return await task();
|
|
370
|
+
} catch (error) {
|
|
371
|
+
const mapped = mapSlackError(error);
|
|
372
|
+
const errorAttrs = { "error.type": mapped.code };
|
|
373
|
+
if (mapped.apiError) errorAttrs["app.slack.api_error_code"] = mapped.apiError;
|
|
374
|
+
if (mapped.code === "rate_limited") {
|
|
375
|
+
errorAttrs["http.response.status_code"] = 429;
|
|
376
|
+
if (mapped.retryAfterSeconds) errorAttrs["app.slack.retry_after_ms"] = mapped.retryAfterSeconds * 1e3;
|
|
377
|
+
} else if (mapped.statusCode != null) errorAttrs["http.response.status_code"] = mapped.statusCode;
|
|
378
|
+
setSpanAttributes(errorAttrs);
|
|
379
|
+
setSpanStatus("error");
|
|
380
|
+
throw error;
|
|
381
|
+
}
|
|
382
|
+
}, {
|
|
383
|
+
"http.request.method": "POST",
|
|
384
|
+
"server.address": "slack.com",
|
|
385
|
+
"url.scheme": "https",
|
|
386
|
+
"url.path": `/api/${action}`,
|
|
387
|
+
"app.slack.method": action,
|
|
388
|
+
"app.retry.max_attempts": maxAttempts,
|
|
389
|
+
...attemptNumber > 1 ? { "http.request.resend_count": attemptNumber - 1 } : void 0,
|
|
390
|
+
...context.attributes ?? {},
|
|
391
|
+
...context.spanAttributes ?? {}
|
|
392
|
+
});
|
|
393
|
+
} catch (error) {
|
|
394
|
+
const mapped = mapSlackError(error);
|
|
395
|
+
const retryClass = classifySlackRetry(error, mapped);
|
|
396
|
+
const isRetryable = retryClass === "rate_limited" || retryClass === "connection" || retryClass === "server_error" || retryClass === "timeout" && context.idempotent === true;
|
|
397
|
+
const remainingDelayBudgetMs = MAX_TOTAL_RETRY_DELAY_MS - totalDelayMs;
|
|
398
|
+
const baseLogAttributes = {
|
|
399
|
+
"app.slack.action": action,
|
|
400
|
+
"app.slack.error_code": mapped.code,
|
|
401
|
+
...mapped.apiError ? { "app.slack.api_error": mapped.apiError } : void 0,
|
|
402
|
+
...mapped.detail ? { "app.slack.detail": mapped.detail } : void 0,
|
|
403
|
+
...mapped.detailLine !== void 0 ? { "app.slack.detail_line": mapped.detailLine } : void 0,
|
|
404
|
+
...mapped.detailRule ? { "app.slack.detail_rule": mapped.detailRule } : void 0,
|
|
405
|
+
...mapped.requestId ? { "app.slack.request_id": mapped.requestId } : void 0,
|
|
406
|
+
...mapped.statusCode !== void 0 ? { "http.response.status_code": mapped.statusCode } : void 0,
|
|
407
|
+
...context.attributes ?? {}
|
|
408
|
+
};
|
|
409
|
+
if (!isRetryable || attempt >= maxAttempts || remainingDelayBudgetMs <= 0) {
|
|
410
|
+
logWarn("slack.action.failed", {
|
|
411
|
+
...baseLogAttributes,
|
|
412
|
+
...mapped.errorData ? { "app.slack.error_data": mapped.errorData } : void 0
|
|
413
|
+
});
|
|
414
|
+
throw mapped;
|
|
415
|
+
}
|
|
416
|
+
logWarn("slack.action.retrying", {
|
|
417
|
+
...baseLogAttributes,
|
|
418
|
+
"app.slack.retry_attempt": attempt,
|
|
419
|
+
"app.slack.retry_class": retryClass
|
|
420
|
+
});
|
|
421
|
+
const retryAfterMs = retryClass === "rate_limited" && mapped.retryAfterSeconds && mapped.retryAfterSeconds > 0 ? mapped.retryAfterSeconds * 1e3 : void 0;
|
|
422
|
+
const backoffMs = Math.min(2e3, 250 * 2 ** (attempt - 1));
|
|
423
|
+
const delayMs = Math.min(retryAfterMs ?? backoffMs, MAX_RETRY_DELAY_MS, remainingDelayBudgetMs);
|
|
424
|
+
totalDelayMs += delayMs;
|
|
425
|
+
await sleep$1(delayMs);
|
|
426
|
+
}
|
|
359
427
|
}
|
|
428
|
+
throw new SlackActionError("Slack action exhausted retries", "internal_error");
|
|
360
429
|
}
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
430
|
+
/**
|
|
431
|
+
* Slack Web API client for the current destination workspace: the ambient
|
|
432
|
+
* installation token when one is bound, otherwise the env bot token for
|
|
433
|
+
* single-workspace deployments. Fails instead of falling back when the call
|
|
434
|
+
* is workspace-scoped and no installation token can be resolved, so a write
|
|
435
|
+
* never goes out with another workspace's credentials.
|
|
436
|
+
*/
|
|
437
|
+
function getSlackClient(options) {
|
|
438
|
+
if (options) return new WebClient(resolveSlackToken(), {
|
|
439
|
+
retryConfig: { retries: 0 },
|
|
440
|
+
rejectRateLimitedCalls: true,
|
|
441
|
+
timeout: options.timeoutMs
|
|
442
|
+
});
|
|
443
|
+
return getClient();
|
|
369
444
|
}
|
|
370
|
-
|
|
371
|
-
|
|
445
|
+
/**
|
|
446
|
+
* Slack channel ID prefixes:
|
|
447
|
+
* - C: channel — modern private channels also use C, so the prefix never
|
|
448
|
+
* proves a channel is public
|
|
449
|
+
* - G: legacy private channel / group DM
|
|
450
|
+
* - D: direct message (1:1)
|
|
451
|
+
*/
|
|
452
|
+
function isDmChannel(channelId) {
|
|
453
|
+
const normalized = normalizeSlackConversationId(channelId);
|
|
454
|
+
return Boolean(normalized && normalized.startsWith("D"));
|
|
372
455
|
}
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
456
|
+
/**
|
|
457
|
+
* Conversation-scoped Slack contexts backed by a concrete conversation ID.
|
|
458
|
+
* Includes channels/groups/DMs (C/G/D).
|
|
459
|
+
*/
|
|
460
|
+
function isConversationScopedChannel(channelId) {
|
|
461
|
+
const normalized = normalizeSlackConversationId(channelId);
|
|
462
|
+
if (!normalized) return false;
|
|
463
|
+
return normalized.startsWith("C") || normalized.startsWith("G") || normalized.startsWith("D");
|
|
378
464
|
}
|
|
379
|
-
function
|
|
380
|
-
const
|
|
381
|
-
if (!
|
|
382
|
-
|
|
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}`;
|
|
465
|
+
function isConversationChannel(channelId) {
|
|
466
|
+
const normalized = normalizeSlackConversationId(channelId);
|
|
467
|
+
if (!normalized) return false;
|
|
468
|
+
return normalized.startsWith("C") || normalized.startsWith("G");
|
|
390
469
|
}
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
return
|
|
470
|
+
async function getFilePermalink(fileId) {
|
|
471
|
+
const client = getClient();
|
|
472
|
+
return (await withSlackRetries(() => client.files.info({ file: fileId }), 3, {
|
|
473
|
+
action: "files.info",
|
|
474
|
+
idempotent: true,
|
|
475
|
+
spanAttributes: { "app.slack.file_id": fileId }
|
|
476
|
+
})).file?.permalink;
|
|
394
477
|
}
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
return
|
|
398
|
-
|
|
399
|
-
|
|
478
|
+
async function downloadPrivateSlackFile(url) {
|
|
479
|
+
const token = resolveSlackToken();
|
|
480
|
+
return withSpan("GET files.slack.com", "http.client", {}, async () => {
|
|
481
|
+
const response = await fetch(url, { headers: { Authorization: `Bearer ${token}` } });
|
|
482
|
+
setSpanAttributes({ "http.response.status_code": response.status });
|
|
483
|
+
if (!response.ok) {
|
|
484
|
+
setSpanAttributes({ "error.type": String(response.status) });
|
|
485
|
+
setSpanStatus("error");
|
|
486
|
+
throw new Error(`Slack file download failed: ${response.status}`);
|
|
487
|
+
}
|
|
488
|
+
return Buffer.from(await response.arrayBuffer());
|
|
489
|
+
}, {
|
|
490
|
+
"http.request.method": "GET",
|
|
491
|
+
"server.address": "files.slack.com",
|
|
492
|
+
"url.scheme": "https",
|
|
493
|
+
"app.slack.method": "files.download"
|
|
400
494
|
});
|
|
401
495
|
}
|
|
402
496
|
//#endregion
|
|
403
|
-
//#region src/
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
497
|
+
//#region src/chat/slack/user.ts
|
|
498
|
+
const slackUserInfoSchema = z.object({
|
|
499
|
+
ok: z.literal(true),
|
|
500
|
+
user: z.object({
|
|
501
|
+
name: z.string().nullish(),
|
|
502
|
+
tz: z.string().nullish(),
|
|
503
|
+
profile: z.object({
|
|
504
|
+
email: z.string().nullish(),
|
|
505
|
+
real_name: z.string().nullish()
|
|
506
|
+
}).optional()
|
|
507
|
+
})
|
|
508
|
+
});
|
|
509
|
+
const USER_CACHE_TTL_MS = 3e5;
|
|
510
|
+
const userCache = /* @__PURE__ */ new Map();
|
|
511
|
+
function userCacheKey(teamId, userId) {
|
|
512
|
+
return `${teamId}:${userId}`;
|
|
513
|
+
}
|
|
514
|
+
function readFromCache(teamId, userId) {
|
|
515
|
+
const hit = userCache.get(userCacheKey(teamId, userId));
|
|
516
|
+
if (!hit) return null;
|
|
517
|
+
if (hit.expiresAt < Date.now()) {
|
|
518
|
+
userCache.delete(userCacheKey(teamId, userId));
|
|
519
|
+
return null;
|
|
520
|
+
}
|
|
521
|
+
return hit.value;
|
|
522
|
+
}
|
|
523
|
+
function writeToCache(teamId, userId, value) {
|
|
524
|
+
userCache.set(userCacheKey(teamId, userId), {
|
|
525
|
+
value,
|
|
526
|
+
expiresAt: Date.now() + USER_CACHE_TTL_MS
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
/** Fetch Slack user profile info with in-memory TTL cache to avoid repeated API calls. */
|
|
530
|
+
async function lookupSlackUser(teamId, userId) {
|
|
531
|
+
if (!teamId || !userId) return null;
|
|
532
|
+
const cached = readFromCache(teamId, userId);
|
|
533
|
+
if (cached) return cached;
|
|
534
|
+
const token = getSlackBotToken();
|
|
535
|
+
if (!token) return null;
|
|
536
|
+
try {
|
|
537
|
+
const response = await fetch(`https://slack.com/api/users.info?user=${encodeURIComponent(userId)}`, { headers: { authorization: `Bearer ${token}` } });
|
|
538
|
+
if (!response.ok) {
|
|
539
|
+
logWarn("slack.user.lookup.failed", {
|
|
540
|
+
"enduser.id": userId,
|
|
541
|
+
"app.slack.team_id": teamId,
|
|
542
|
+
"http.response.status_code": response.status
|
|
543
|
+
});
|
|
544
|
+
return null;
|
|
421
545
|
}
|
|
546
|
+
const parsed = slackUserInfoSchema.safeParse(await response.json());
|
|
547
|
+
if (!parsed.success) return null;
|
|
548
|
+
const payload = parsed.data;
|
|
549
|
+
const result = {
|
|
550
|
+
userName: payload.user.name?.trim() || void 0,
|
|
551
|
+
fullName: payload.user.profile?.real_name?.trim() || void 0,
|
|
552
|
+
email: payload.user.profile?.email?.trim() || void 0,
|
|
553
|
+
timezone: payload.user.tz?.trim() || void 0
|
|
554
|
+
};
|
|
555
|
+
writeToCache(teamId, userId, result);
|
|
556
|
+
return result;
|
|
557
|
+
} catch (error) {
|
|
558
|
+
logWarn("slack.user.lookup.failed", {
|
|
559
|
+
"enduser.id": userId,
|
|
560
|
+
"app.slack.team_id": teamId,
|
|
561
|
+
"exception.message": error instanceof Error ? error.message : String(error)
|
|
562
|
+
});
|
|
563
|
+
return null;
|
|
422
564
|
}
|
|
423
|
-
return current;
|
|
424
565
|
}
|
|
425
|
-
|
|
426
|
-
|
|
566
|
+
//#endregion
|
|
567
|
+
//#region src/chat/identities/timezone.ts
|
|
568
|
+
const DEFAULT_TIMEZONE = "America/Los_Angeles";
|
|
569
|
+
/** The install timezone, used when Junior does not know a person's timezone. */
|
|
570
|
+
function defaultTimezone() {
|
|
571
|
+
return process.env.JUNIOR_TIMEZONE?.trim() || DEFAULT_TIMEZONE;
|
|
427
572
|
}
|
|
428
|
-
/**
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
const
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
};
|
|
573
|
+
/**
|
|
574
|
+
* Pick a person's timezone: their Slack profile, then the saved user
|
|
575
|
+
* timezone, then the install default. A valid Slack timezone is saved on the
|
|
576
|
+
* linked user, so later lookups without Slack still find it.
|
|
577
|
+
*/
|
|
578
|
+
async function resolveUserTimezone(args) {
|
|
579
|
+
const timezone = (args.slack ? await lookupSlackUser(args.slack.teamId, args.slack.userId) : void 0)?.timezone;
|
|
580
|
+
if (timezone && isValidTimeZone(timezone)) {
|
|
581
|
+
if (args.userId) await saveUserTimezone(args.db, args.userId, timezone, args.nowMs);
|
|
582
|
+
return timezone;
|
|
583
|
+
}
|
|
584
|
+
return (args.userId ? await readUserTimezone(args.db, args.userId) : void 0) ?? defaultTimezone();
|
|
441
585
|
}
|
|
442
586
|
//#endregion
|
|
443
|
-
//#region src/
|
|
444
|
-
|
|
445
|
-
const
|
|
446
|
-
|
|
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;
|
|
587
|
+
//#region src/chat/plugins/annotations.ts
|
|
588
|
+
function annotationFromRow(row) {
|
|
589
|
+
const parsed = conversationAnnotationInputSchema.safeParse(row.annotation);
|
|
590
|
+
if (!parsed.success) return void 0;
|
|
514
591
|
return {
|
|
515
|
-
|
|
516
|
-
|
|
592
|
+
...parsed.data,
|
|
593
|
+
plugin: row.plugin,
|
|
594
|
+
createdAt: row.createdAt.toISOString(),
|
|
595
|
+
updatedAt: row.updatedAt.toISOString()
|
|
517
596
|
};
|
|
518
597
|
}
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
598
|
+
function createPluginAnnotations(args) {
|
|
599
|
+
return {
|
|
600
|
+
async upsert(input) {
|
|
601
|
+
const annotation = conversationAnnotationInputSchema.parse(input);
|
|
602
|
+
const now = /* @__PURE__ */ new Date();
|
|
603
|
+
await args.db.insert(juniorConversationAnnotations).values({
|
|
604
|
+
annotation,
|
|
605
|
+
conversationId: args.conversationId,
|
|
606
|
+
createdAt: now,
|
|
607
|
+
key: annotation.key,
|
|
608
|
+
kind: annotation.kind,
|
|
609
|
+
plugin: args.plugin,
|
|
610
|
+
updatedAt: now
|
|
611
|
+
}).onConflictDoUpdate({
|
|
612
|
+
target: [
|
|
613
|
+
juniorConversationAnnotations.conversationId,
|
|
614
|
+
juniorConversationAnnotations.plugin,
|
|
615
|
+
juniorConversationAnnotations.kind,
|
|
616
|
+
juniorConversationAnnotations.key
|
|
617
|
+
],
|
|
618
|
+
set: {
|
|
619
|
+
annotation,
|
|
620
|
+
updatedAt: now
|
|
621
|
+
}
|
|
622
|
+
});
|
|
623
|
+
},
|
|
624
|
+
async remove(kind, key) {
|
|
625
|
+
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)));
|
|
626
|
+
},
|
|
627
|
+
async list() {
|
|
628
|
+
return (await listConversationAnnotations(args.db, args.conversationId)).filter((a) => a.plugin === args.plugin);
|
|
629
|
+
}
|
|
535
630
|
};
|
|
536
|
-
return Object.keys(actorIdentity).length > 0 ? actorIdentity : void 0;
|
|
537
631
|
}
|
|
538
|
-
/**
|
|
539
|
-
function
|
|
540
|
-
|
|
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
|
-
}];
|
|
553
|
-
});
|
|
554
|
-
return calls.length > 0 ? {
|
|
555
|
-
type: "tool_calls",
|
|
556
|
-
calls
|
|
557
|
-
} : void 0;
|
|
632
|
+
/** Load stored annotations for one conversation, newest update first. */
|
|
633
|
+
async function listConversationAnnotations(db, conversationId) {
|
|
634
|
+
return (await listConversationAnnotationsById(db, [conversationId])).get(conversationId) ?? [];
|
|
558
635
|
}
|
|
559
|
-
/**
|
|
560
|
-
function
|
|
561
|
-
const
|
|
562
|
-
if (
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
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;
|
|
584
|
-
}
|
|
585
|
-
const call = reportingToolCallPartSchema.safeParse(part);
|
|
586
|
-
if (call.success) assistantParts.push({
|
|
587
|
-
type: "tool_call",
|
|
588
|
-
toolCallId: call.data.id
|
|
589
|
-
});
|
|
636
|
+
/** Load stored annotations for a bounded set of conversations. */
|
|
637
|
+
async function listConversationAnnotationsById(db, conversationIds) {
|
|
638
|
+
const byConversation = /* @__PURE__ */ new Map();
|
|
639
|
+
if (conversationIds.length === 0) return byConversation;
|
|
640
|
+
const rows = await db.select().from(juniorConversationAnnotations).where(inArray(juniorConversationAnnotations.conversationId, [...conversationIds])).orderBy(desc(juniorConversationAnnotations.updatedAt));
|
|
641
|
+
for (const row of rows) {
|
|
642
|
+
const annotation = annotationFromRow(row);
|
|
643
|
+
if (!annotation) continue;
|
|
644
|
+
const current = byConversation.get(row.conversationId);
|
|
645
|
+
if (current) current.push(annotation);
|
|
646
|
+
else byConversation.set(row.conversationId, [annotation]);
|
|
590
647
|
}
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
648
|
+
return byConversation;
|
|
649
|
+
}
|
|
650
|
+
//#endregion
|
|
651
|
+
//#region src/chat/conversations/annotation-results.ts
|
|
652
|
+
/** Save object annotations and retain returned facts in the tool result. */
|
|
653
|
+
async function saveObjectAnnotations(conversationId, plugin, value) {
|
|
654
|
+
const annotations = objectAnnotationSchema.array().parse(value);
|
|
655
|
+
await getDb().transaction(async (db) => {
|
|
656
|
+
const store = createPluginAnnotations({
|
|
657
|
+
conversationId,
|
|
658
|
+
plugin,
|
|
659
|
+
db
|
|
660
|
+
});
|
|
661
|
+
for (const annotation of annotations) {
|
|
662
|
+
await store.upsert(annotation);
|
|
663
|
+
await store.remove("resource_link", annotation.key);
|
|
664
|
+
}
|
|
665
|
+
});
|
|
666
|
+
return annotations.map((annotation) => ({
|
|
667
|
+
...annotation,
|
|
668
|
+
plugin
|
|
669
|
+
}));
|
|
670
|
+
}
|
|
671
|
+
/** Save annotations from a successful plugin tool result and attach their cards. */
|
|
672
|
+
async function annotateToolResult(conversationId, plugin, result) {
|
|
673
|
+
if (!isRecord$1(result) || result.isError === true) return result;
|
|
674
|
+
const details = isRecord$1(result.details) ? result.details : result;
|
|
675
|
+
if (details.timed_out === true || details.isError === true || !Array.isArray(details.objectAnnotations)) return result;
|
|
676
|
+
const cards = await saveObjectAnnotations(conversationId, plugin, details.objectAnnotations);
|
|
677
|
+
const { objectAnnotations: _annotations, ...rest } = details;
|
|
678
|
+
const annotated = {
|
|
679
|
+
...rest,
|
|
680
|
+
objectCards: cards
|
|
595
681
|
};
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
682
|
+
if (isRecord$1(result.details)) return {
|
|
683
|
+
...result,
|
|
684
|
+
details: annotated
|
|
685
|
+
};
|
|
686
|
+
return annotated;
|
|
687
|
+
}
|
|
688
|
+
//#endregion
|
|
689
|
+
//#region src/chat/conversations/sql/privacy.ts
|
|
690
|
+
const privacyRoot = alias(juniorConversations, "privacy_root");
|
|
691
|
+
/** Accept only a structurally valid persisted root as privacy authority. */
|
|
692
|
+
async function readRootCandidate(executor, conversationId) {
|
|
693
|
+
const row = (await executor.db().select({
|
|
694
|
+
requestedConversationId: juniorConversations.conversationId,
|
|
695
|
+
requestedParentConversationId: juniorConversations.parentConversationId,
|
|
696
|
+
requestedRootConversationId: juniorConversations.rootConversationId,
|
|
697
|
+
rootConversationId: privacyRoot.conversationId,
|
|
698
|
+
rootDestinationId: privacyRoot.destinationId,
|
|
699
|
+
rootParentConversationId: privacyRoot.parentConversationId,
|
|
700
|
+
rootRootConversationId: privacyRoot.rootConversationId
|
|
701
|
+
}).from(juniorConversations).leftJoin(privacyRoot, eq(privacyRoot.conversationId, juniorConversations.rootConversationId)).where(eq(juniorConversations.conversationId, conversationId)))[0];
|
|
702
|
+
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 ? {
|
|
703
|
+
destinationId: row.rootDestinationId,
|
|
704
|
+
rootConversationId: row.rootConversationId
|
|
599
705
|
} : void 0;
|
|
600
706
|
}
|
|
601
|
-
|
|
602
|
-
|
|
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;
|
|
707
|
+
async function readCandidateVisibility(executor, candidate) {
|
|
708
|
+
const destinations = await executor.db().select({ visibility: juniorDestinations.visibility }).from(juniorDestinations).where(eq(juniorDestinations.id, candidate.destinationId)).for("share");
|
|
610
709
|
return {
|
|
611
|
-
|
|
612
|
-
|
|
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
|
-
}]
|
|
710
|
+
rootConversationId: candidate.rootConversationId,
|
|
711
|
+
visibility: destinations[0]?.visibility ?? null
|
|
622
712
|
};
|
|
623
713
|
}
|
|
624
|
-
/**
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
714
|
+
/**
|
|
715
|
+
* Resolve a conversation's privacy authority from its persisted root.
|
|
716
|
+
*
|
|
717
|
+
* Missing or structurally invalid root metadata fails closed. The root
|
|
718
|
+
* destination is locked so callers that keep the transaction open receive a
|
|
719
|
+
* stable visibility decision.
|
|
720
|
+
*/
|
|
721
|
+
async function resolveRootVisibility(executor, conversationId) {
|
|
722
|
+
const candidate = await readRootCandidate(executor, conversationId);
|
|
723
|
+
if (!candidate) return {
|
|
724
|
+
rootConversationId: conversationId,
|
|
725
|
+
visibility: null
|
|
726
|
+
};
|
|
727
|
+
return readCandidateVisibility(executor, candidate);
|
|
630
728
|
}
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
729
|
+
//#endregion
|
|
730
|
+
//#region src/chat/briefs/events.ts
|
|
731
|
+
/** Durable cost and size record for one stored Brief version. */
|
|
732
|
+
const briefUpdatedEvent = defineConversationEvent({
|
|
733
|
+
name: "brief_updated",
|
|
734
|
+
version: 1,
|
|
735
|
+
schema: z.object({
|
|
736
|
+
version: z.number().int().positive(),
|
|
737
|
+
modelId: z.string().min(1),
|
|
738
|
+
costUsd: z.number().finite().nonnegative().optional(),
|
|
739
|
+
decisions: z.number().int().nonnegative(),
|
|
740
|
+
openDecisions: z.number().int().nonnegative(),
|
|
741
|
+
links: z.number().int().nonnegative()
|
|
742
|
+
}).strict(),
|
|
743
|
+
renderEvent(event) {
|
|
637
744
|
return {
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
name: data.name,
|
|
641
|
-
version: data.version,
|
|
642
|
-
...data.turnId ? { turnId: data.turnId } : void 0,
|
|
643
|
-
presentation
|
|
745
|
+
icon: "activity",
|
|
746
|
+
title: `Brief updated (v${event.version})`
|
|
644
747
|
};
|
|
645
748
|
}
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
749
|
+
});
|
|
750
|
+
//#endregion
|
|
751
|
+
//#region src/chat/slack/attachments.ts
|
|
752
|
+
/** Save the vision summary for a stored Slack attachment. */
|
|
753
|
+
async function recordSlackAttachment(args) {
|
|
754
|
+
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)));
|
|
755
|
+
}
|
|
756
|
+
/** Read a saved vision summary for one live attachment. */
|
|
757
|
+
async function readAttachmentVisionSummary(args) {
|
|
758
|
+
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)));
|
|
759
|
+
return row?.visionSummary;
|
|
760
|
+
}
|
|
761
|
+
/** Match live conversation attachments to Slack file ids. */
|
|
762
|
+
async function matchSlackAttachments(args) {
|
|
763
|
+
if (args.providerIds.length === 0) return [];
|
|
764
|
+
return await args.db.db().select({
|
|
765
|
+
id: juniorAttachments.id,
|
|
766
|
+
providerId: juniorAttachments.providerId,
|
|
767
|
+
bytes: juniorAttachments.bytes,
|
|
768
|
+
contentType: juniorAttachments.contentType,
|
|
769
|
+
filename: juniorAttachments.filename
|
|
770
|
+
}).from(juniorAttachments).where(and(eq(juniorAttachments.conversationId, args.conversationId), eq(juniorAttachments.provider, "slack"), inArray(juniorAttachments.providerId, args.providerIds), isNull(juniorAttachments.deleteRequestedAt)));
|
|
771
|
+
}
|
|
772
|
+
/** Read the Slack file identity retained by the Chat SDK. */
|
|
773
|
+
function getSlackFileId(attachment) {
|
|
774
|
+
const metadataId = attachment.fetchMetadata?.fileId;
|
|
775
|
+
if (metadataId) return metadataId;
|
|
776
|
+
return (attachment.url ?? attachment.fetchMetadata?.url ?? "").match(/(?:^|[-/])(F[A-Z0-9]+)(?:[-/]|$)/i)?.[1];
|
|
777
|
+
}
|
|
778
|
+
//#endregion
|
|
779
|
+
//#region src/api/conversations/message-attachments.ts
|
|
780
|
+
/** Resolve saved Slack file references without exposing private storage details. */
|
|
781
|
+
async function hydrateMessageAttachments(db, conversationId, events) {
|
|
782
|
+
const messages = events.flatMap((event) => event.data.type === "message" ? [event.data] : []);
|
|
783
|
+
const ids = [...new Set(messages.flatMap((message) => slackFileIds(message.meta)))];
|
|
784
|
+
if (!ids.length) return;
|
|
785
|
+
const attachments = await matchSlackAttachments({
|
|
786
|
+
db,
|
|
787
|
+
conversationId,
|
|
788
|
+
providerIds: ids
|
|
789
|
+
});
|
|
790
|
+
const byFileId = new Map(attachments.map(({ providerId, ...attachment }) => [providerId, attachment]));
|
|
791
|
+
for (const message of messages) {
|
|
792
|
+
const stored = readMessageAttachments(message.meta?.attachments);
|
|
793
|
+
const matched = slackFileIds(message.meta).flatMap((id) => {
|
|
794
|
+
const attachment = byFileId.get(id);
|
|
795
|
+
return attachment ? [attachment] : [];
|
|
796
|
+
});
|
|
797
|
+
if (matched.length) message.meta = {
|
|
798
|
+
...message.meta,
|
|
799
|
+
attachments: [...stored, ...matched]
|
|
675
800
|
};
|
|
676
|
-
case "turn_context":
|
|
677
|
-
if (!args.canExposePayload) return;
|
|
678
|
-
return {
|
|
679
|
-
type: "turn_context",
|
|
680
|
-
turnId: data.turnId,
|
|
681
|
-
pluginName: data.pluginName,
|
|
682
|
-
kind: data.kind,
|
|
683
|
-
version: data.version,
|
|
684
|
-
content: data.content
|
|
685
|
-
};
|
|
686
|
-
case "attachments_delivered":
|
|
687
|
-
if (!args.canExposePayload) return;
|
|
688
|
-
return {
|
|
689
|
-
type: "attachments_delivered",
|
|
690
|
-
attachments: data.attachments
|
|
691
|
-
};
|
|
692
|
-
case "turn_routed": return {
|
|
693
|
-
type: "turn_routed",
|
|
694
|
-
turnId: data.turnId,
|
|
695
|
-
modelProfile: data.modelProfile,
|
|
696
|
-
modelId: data.modelId,
|
|
697
|
-
...data.costUsd !== void 0 ? { costUsd: data.costUsd } : void 0,
|
|
698
|
-
reasoningLevel: data.reasoningLevel,
|
|
699
|
-
...data.confidence !== void 0 ? { confidence: data.confidence } : void 0,
|
|
700
|
-
source: data.source
|
|
701
|
-
};
|
|
702
|
-
case "guardian_action_reviewed": return {
|
|
703
|
-
type: "guardian_action_reviewed",
|
|
704
|
-
turnId: data.turnId,
|
|
705
|
-
toolCallId: data.toolCallId,
|
|
706
|
-
toolName: data.toolName,
|
|
707
|
-
decision: data.decision,
|
|
708
|
-
riskLevel: data.riskLevel,
|
|
709
|
-
userAuthorization: data.userAuthorization
|
|
710
|
-
};
|
|
711
|
-
case "turn_completed": return {
|
|
712
|
-
type: "turn_lifecycle",
|
|
713
|
-
turnId: data.turnId,
|
|
714
|
-
state: data.outcome === "success" ? "succeeded" : "no_reply"
|
|
715
|
-
};
|
|
716
|
-
case "turn_failed": {
|
|
717
|
-
const eventId = data.eventId;
|
|
718
|
-
const sentryEventUrl = eventId ? buildSentryEventUrl(eventId) : void 0;
|
|
719
|
-
return {
|
|
720
|
-
type: "turn_lifecycle",
|
|
721
|
-
turnId: data.turnId,
|
|
722
|
-
state: "failed",
|
|
723
|
-
failureCode: data.failureCode,
|
|
724
|
-
...data.failureReason ? { failureReason: data.failureReason } : void 0,
|
|
725
|
-
...eventId ? { eventId } : void 0,
|
|
726
|
-
...sentryEventUrl ? { sentryEventUrl } : void 0
|
|
727
|
-
};
|
|
728
|
-
}
|
|
729
|
-
case "compaction": return {
|
|
730
|
-
type: "compaction",
|
|
731
|
-
modelProfile: data.modelProfile,
|
|
732
|
-
modelId: data.modelId,
|
|
733
|
-
...args.canExposePayload && data.summary ? { summary: data.summary } : void 0,
|
|
734
|
-
...data.details ? { details: data.details } : void 0
|
|
735
|
-
};
|
|
736
|
-
default: return;
|
|
737
801
|
}
|
|
738
802
|
}
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
* `canExposePayload` must come from the authorized conversation-detail policy;
|
|
743
|
-
* this projection never derives visibility or authorization from event data.
|
|
744
|
-
*/
|
|
745
|
-
function projectConversationReportEventPage(args) {
|
|
746
|
-
const subagentStarts = /* @__PURE__ */ new Map();
|
|
747
|
-
for (const event of args.subagentStartEvents ?? []) if (event.data.type === "subagent_started") subagentStarts.set(event.data.subagentInvocationId, {
|
|
748
|
-
createdAtMs: event.createdAtMs,
|
|
749
|
-
data: event.data,
|
|
750
|
-
seq: event.seq
|
|
751
|
-
});
|
|
752
|
-
const toolStarts = /* @__PURE__ */ new Map();
|
|
753
|
-
for (const event of args.toolStartEvents ?? []) if (event.data.type === "tool_execution_started") {
|
|
754
|
-
const current = toolStarts.get(event.data.toolCallId);
|
|
755
|
-
if (!current || event.seq < current.seq) toolStarts.set(event.data.toolCallId, {
|
|
756
|
-
createdAtMs: event.createdAtMs,
|
|
757
|
-
name: event.data.toolName,
|
|
758
|
-
seq: event.seq
|
|
759
|
-
});
|
|
760
|
-
}
|
|
761
|
-
const projected = [];
|
|
762
|
-
let model;
|
|
763
|
-
for (const event of args.modelContextEvents ?? []) {
|
|
764
|
-
model = conversationEventModel(event.data, model);
|
|
765
|
-
if (event.data.type === "turn_completed" || event.data.type === "turn_failed") model = void 0;
|
|
766
|
-
}
|
|
767
|
-
for (const event of args.events) {
|
|
768
|
-
model = conversationEventModel(event.data, model);
|
|
769
|
-
const eventModel = model;
|
|
770
|
-
const modelCall = conversationEventModelCall(event.data);
|
|
771
|
-
if (event.data.type === "turn_completed" || event.data.type === "turn_failed") model = void 0;
|
|
772
|
-
let data;
|
|
773
|
-
if (event.data.type === "assistant_message" || event.data.type === "tool_result") {
|
|
774
|
-
const reportArgs = {
|
|
775
|
-
canExposePayload: args.canExposePayload,
|
|
776
|
-
createdAtMs: event.createdAtMs,
|
|
777
|
-
message: event.data,
|
|
778
|
-
seq: event.seq
|
|
779
|
-
};
|
|
780
|
-
const result = reportingToolResultMessageSchema.safeParse(event.data);
|
|
781
|
-
const start = result.success ? toolStarts.get(result.data.toolCallId) : void 0;
|
|
782
|
-
data = reportAssistantMessage(reportArgs) ?? reportToolResult({
|
|
783
|
-
canExposePayload: args.canExposePayload,
|
|
784
|
-
message: event.data,
|
|
785
|
-
...start && start.seq < event.seq ? { start } : void 0
|
|
786
|
-
});
|
|
787
|
-
} else if (event.data.type === "tool_execution_started") {
|
|
788
|
-
toolStarts.set(event.data.toolCallId, {
|
|
789
|
-
createdAtMs: event.createdAtMs,
|
|
790
|
-
name: event.data.toolName,
|
|
791
|
-
seq: event.seq
|
|
792
|
-
});
|
|
793
|
-
data = {
|
|
794
|
-
type: "tool_calls",
|
|
795
|
-
calls: [{
|
|
796
|
-
toolCallId: event.data.toolCallId,
|
|
797
|
-
name: event.data.toolName,
|
|
798
|
-
status: "running",
|
|
799
|
-
startedAt: new Date(event.createdAtMs).toISOString(),
|
|
800
|
-
startedSeq: event.seq
|
|
801
|
-
}]
|
|
802
|
-
};
|
|
803
|
-
} else if (event.data.type === "subagent_started") {
|
|
804
|
-
subagentStarts.set(event.data.subagentInvocationId, {
|
|
805
|
-
createdAtMs: event.createdAtMs,
|
|
806
|
-
data: event.data,
|
|
807
|
-
seq: event.seq
|
|
808
|
-
});
|
|
809
|
-
data = {
|
|
810
|
-
type: "subagent",
|
|
811
|
-
startedSeq: event.seq,
|
|
812
|
-
startedAt: new Date(event.createdAtMs).toISOString(),
|
|
813
|
-
childConversationId: event.data.childConversationId,
|
|
814
|
-
subagentKind: event.data.subagentKind,
|
|
815
|
-
status: "running",
|
|
816
|
-
...event.data.parentToolCallId ? { parentToolCallId: event.data.parentToolCallId } : void 0
|
|
817
|
-
};
|
|
818
|
-
} else if (event.data.type === "subagent_ended") {
|
|
819
|
-
const started = subagentStarts.get(event.data.subagentInvocationId);
|
|
820
|
-
if (started && started.seq < event.seq) {
|
|
821
|
-
data = {
|
|
822
|
-
type: "subagent",
|
|
823
|
-
startedSeq: started.seq,
|
|
824
|
-
startedAt: new Date(started.createdAtMs).toISOString(),
|
|
825
|
-
childConversationId: started.data.childConversationId,
|
|
826
|
-
subagentKind: started.data.subagentKind,
|
|
827
|
-
...started.data.parentToolCallId ? { parentToolCallId: started.data.parentToolCallId } : void 0,
|
|
828
|
-
status: event.data.outcome === "success" ? "completed" : event.data.outcome
|
|
829
|
-
};
|
|
830
|
-
subagentStarts.delete(event.data.subagentInvocationId);
|
|
831
|
-
}
|
|
832
|
-
} else if (event.data.type === "handoff") data = {
|
|
833
|
-
type: "handoff",
|
|
834
|
-
modelProfile: event.data.modelProfile,
|
|
835
|
-
modelId: event.data.modelId,
|
|
836
|
-
...event.data.reasoningLevel ? { reasoningLevel: event.data.reasoningLevel } : void 0,
|
|
837
|
-
...event.data.triggeringToolCallId ? { triggeringToolCallId: event.data.triggeringToolCallId } : void 0,
|
|
838
|
-
...args.canExposePayload && event.data.summary ? { summary: event.data.summary } : void 0
|
|
839
|
-
};
|
|
840
|
-
else data = reportEventData({
|
|
841
|
-
canExposePayload: args.canExposePayload,
|
|
842
|
-
data: event.data
|
|
843
|
-
});
|
|
844
|
-
if (!data) continue;
|
|
845
|
-
projected.push(conversationReportEventSchema.parse({
|
|
846
|
-
seq: event.seq,
|
|
847
|
-
createdAt: new Date(event.createdAtMs).toISOString(),
|
|
848
|
-
data,
|
|
849
|
-
...eventModel ? { model: eventModel } : void 0,
|
|
850
|
-
...modelCall ? { modelCall } : void 0
|
|
851
|
-
}));
|
|
852
|
-
}
|
|
853
|
-
return projected;
|
|
803
|
+
function slackFileIds(meta) {
|
|
804
|
+
const ids = meta?.slackFileIds ?? meta?.imageFileIds;
|
|
805
|
+
return Array.isArray(ids) ? ids.filter((id) => typeof id === "string") : [];
|
|
854
806
|
}
|
|
855
807
|
//#endregion
|
|
856
|
-
//#region src/
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
/** Read bounded canonical rows while excluding model-only replacement history. */
|
|
860
|
-
async function readConversationEventRows(executor, args) {
|
|
861
|
-
return executor.db().select({
|
|
862
|
-
...conversationEventColumns,
|
|
863
|
-
payload: sql`case
|
|
864
|
-
when ${juniorConversationEvents.type} in ('compaction', 'handoff')
|
|
865
|
-
then jsonb_set(
|
|
866
|
-
${juniorConversationEvents.payload},
|
|
867
|
-
'{replacementHistory}',
|
|
868
|
-
'[]'::jsonb
|
|
869
|
-
)
|
|
870
|
-
else ${juniorConversationEvents.payload}
|
|
871
|
-
end`
|
|
872
|
-
}).from(juniorConversationEvents).where(and(eq(juniorConversationEvents.conversationId, args.conversationId), args.beforeSeq === void 0 ? void 0 : lt(juniorConversationEvents.seq, args.beforeSeq), inArray(juniorConversationEvents.type, args.types ?? [...conversationReportSourceEventTypes]), args.subagentInvocationIds === void 0 ? void 0 : inArray(sql`${juniorConversationEvents.payload}->>'subagentInvocationId'`, args.subagentInvocationIds), args.toolCallIds === void 0 ? void 0 : inArray(sql`${juniorConversationEvents.payload}->>'toolCallId'`, args.toolCallIds))).orderBy(args.direction === "forward" ? asc(juniorConversationEvents.seq) : desc(juniorConversationEvents.seq)).limit(args.limit);
|
|
808
|
+
//#region src/chat/sentry-links.ts
|
|
809
|
+
function getSentryOrgSlug() {
|
|
810
|
+
return process.env.SENTRY_ORG_SLUG?.trim() || void 0;
|
|
873
811
|
}
|
|
874
|
-
function
|
|
875
|
-
return
|
|
876
|
-
schemaVersion: row.schemaVersion,
|
|
877
|
-
seq: row.seq,
|
|
878
|
-
historyVersion: row.historyVersion,
|
|
879
|
-
...row.idempotencyKey ? { idempotencyKey: row.idempotencyKey } : void 0,
|
|
880
|
-
createdAtMs: row.createdAt.getTime(),
|
|
881
|
-
type: row.type,
|
|
882
|
-
payload: row.payload
|
|
883
|
-
});
|
|
812
|
+
function isSentrySaasDsnHost(host) {
|
|
813
|
+
return host === "sentry.io" || host.endsWith(".sentry.io");
|
|
884
814
|
}
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
const
|
|
888
|
-
const
|
|
889
|
-
|
|
890
|
-
beforeSeq: events[0].seq,
|
|
891
|
-
direction: "backward",
|
|
892
|
-
limit: 1,
|
|
893
|
-
types: [
|
|
894
|
-
"turn_started",
|
|
895
|
-
"turn_routed",
|
|
896
|
-
"handoff",
|
|
897
|
-
"turn_completed",
|
|
898
|
-
"turn_failed"
|
|
899
|
-
]
|
|
900
|
-
}), readConversationEventRows(executor, {
|
|
901
|
-
conversationId: args.conversationId,
|
|
902
|
-
beforeSeq: events[0].seq,
|
|
903
|
-
direction: "backward",
|
|
904
|
-
limit: 1,
|
|
905
|
-
types: ["assistant_message"]
|
|
906
|
-
})])).flat().sort((left, right) => left.seq - right.seq) : [];
|
|
907
|
-
const endedInvocationIds = [...new Set(events.flatMap((event) => event.data.type === "subagent_ended" ? [event.data.subagentInvocationId] : []))];
|
|
908
|
-
const subagentStartRows = endedInvocationIds.length === 0 ? [] : await readConversationEventRows(executor, {
|
|
909
|
-
conversationId: args.conversationId,
|
|
910
|
-
direction: "forward",
|
|
911
|
-
limit: endedInvocationIds.length,
|
|
912
|
-
subagentInvocationIds: endedInvocationIds,
|
|
913
|
-
types: ["subagent_started"]
|
|
914
|
-
});
|
|
915
|
-
const toolResultIds = conversationReportToolResultIds(events);
|
|
916
|
-
const toolStartRows = toolResultIds.length === 0 ? [] : await readConversationEventRows(executor, {
|
|
917
|
-
conversationId: args.conversationId,
|
|
918
|
-
direction: "forward",
|
|
919
|
-
limit: toolResultIds.length,
|
|
920
|
-
toolCallIds: toolResultIds,
|
|
921
|
-
types: ["tool_execution_started"]
|
|
922
|
-
});
|
|
923
|
-
if (args.canExposePayload) await hydrateMessageAttachments(executor, args.conversationId, events);
|
|
924
|
-
return projectConversationReportEventPage({
|
|
925
|
-
canExposePayload: args.canExposePayload,
|
|
926
|
-
events,
|
|
927
|
-
subagentStartEvents: subagentStartRows.map(decodeConversationEventRow),
|
|
928
|
-
toolStartEvents: toolStartRows.map(decodeConversationEventRow),
|
|
929
|
-
modelContextEvents: modelContextRows.map(decodeConversationEventRow)
|
|
930
|
-
});
|
|
815
|
+
function buildSentryWebBaseUrl(dsn) {
|
|
816
|
+
if (isSentrySaasDsnHost(dsn.host)) return "https://sentry.io";
|
|
817
|
+
const port = dsn.port ? `:${dsn.port}` : "";
|
|
818
|
+
const path = dsn.path ? `/${dsn.path}` : "";
|
|
819
|
+
return `${dsn.protocol}://${dsn.host}${port}${path}`;
|
|
931
820
|
}
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
const
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
let projected = [];
|
|
944
|
-
while (projected.length <= args.limit) {
|
|
945
|
-
const batch = await readConversationEventRows(executor, {
|
|
946
|
-
beforeSeq,
|
|
947
|
-
conversationId: args.conversationId,
|
|
948
|
-
direction: "backward",
|
|
949
|
-
limit: scanSize
|
|
950
|
-
});
|
|
951
|
-
if (batch.length === 0) break;
|
|
952
|
-
rows.push(...batch);
|
|
953
|
-
projected = await projectConversationEventRows(executor, {
|
|
954
|
-
canExposePayload: args.canExposePayload,
|
|
955
|
-
conversationId: args.conversationId,
|
|
956
|
-
rows
|
|
957
|
-
});
|
|
958
|
-
if (projected.length > args.limit || batch.length < scanSize) break;
|
|
959
|
-
beforeSeq = batch.at(-1).seq;
|
|
960
|
-
}
|
|
961
|
-
const events = projected.slice(-args.limit);
|
|
962
|
-
return {
|
|
963
|
-
events,
|
|
964
|
-
...projected.length > events.length && events[0] ? { previousSeq: events[0].seq } : void 0
|
|
965
|
-
};
|
|
821
|
+
function buildSentryOrgPath(args) {
|
|
822
|
+
const dsn = getClient$1()?.getDsn();
|
|
823
|
+
if (!dsn?.host || !dsn.projectId) return;
|
|
824
|
+
const orgSlug = getSentryOrgSlug();
|
|
825
|
+
if (!orgSlug) return;
|
|
826
|
+
const params = new URLSearchParams();
|
|
827
|
+
params.set("project", dsn.projectId);
|
|
828
|
+
if (args.query) for (const [key, value] of Object.entries(args.query)) params.set(key, value);
|
|
829
|
+
const path = `${args.path}?${params.toString()}`;
|
|
830
|
+
if (isSentrySaasDsnHost(dsn.host)) return `https://${orgSlug}.sentry.io/${path}`;
|
|
831
|
+
return `${buildSentryWebBaseUrl(dsn)}/organizations/${orgSlug}/${path}`;
|
|
966
832
|
}
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
async function readReportEventsThrough(executor, conversationId, throughSeq) {
|
|
971
|
-
const pages = [];
|
|
972
|
-
let beforeSeq = throughSeq + 1;
|
|
973
|
-
do {
|
|
974
|
-
const page = await readConversationEventPage(executor, {
|
|
975
|
-
beforeSeq,
|
|
976
|
-
canExposePayload: true,
|
|
977
|
-
conversationId,
|
|
978
|
-
limit: EVENT_PAGE_SIZE
|
|
979
|
-
});
|
|
980
|
-
pages.unshift(page.events);
|
|
981
|
-
beforeSeq = page.previousSeq;
|
|
982
|
-
} while (beforeSeq !== void 0);
|
|
983
|
-
const bySequence = /* @__PURE__ */ new Map();
|
|
984
|
-
for (const event of pages.flat()) bySequence.set(event.seq, event);
|
|
985
|
-
return [...bySequence.values()].sort((left, right) => left.seq - right.seq);
|
|
833
|
+
/** Build a Sentry conversation URL only when the runtime has enough Sentry config. */
|
|
834
|
+
function buildSentryConversationUrl(conversationId) {
|
|
835
|
+
return buildSentryOrgPath({ path: `explore/conversations/${encodeURIComponent(conversationId)}/` });
|
|
986
836
|
}
|
|
987
|
-
/**
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
async function briefInputFromSql(executor, conversationId, throughSeq) {
|
|
993
|
-
const db = executor.db();
|
|
994
|
-
const conversation = await createSqlStore(executor).get({ conversationId });
|
|
995
|
-
if (!conversation) throw new Error(`Conversation ${conversationId} is unavailable`);
|
|
996
|
-
const [events, annotations, codeChanges] = await Promise.all([
|
|
997
|
-
readReportEventsThrough(executor, conversationId, throughSeq),
|
|
998
|
-
listConversationAnnotations(db, conversationId),
|
|
999
|
-
db.select({
|
|
1000
|
-
closedAt: juniorCodeChanges.closedAt,
|
|
1001
|
-
mergedAt: juniorCodeChanges.mergedAt,
|
|
1002
|
-
number: juniorCodeChanges.number,
|
|
1003
|
-
openedAt: juniorCodeChanges.openedAt,
|
|
1004
|
-
repository: juniorCodeRepositories.name,
|
|
1005
|
-
state: juniorCodeChanges.state,
|
|
1006
|
-
title: juniorCodeChanges.title,
|
|
1007
|
-
url: juniorCodeChanges.url
|
|
1008
|
-
}).from(juniorCodeChanges).innerJoin(juniorCodeRepositories, eq(juniorCodeRepositories.id, juniorCodeChanges.repositoryId)).where(and(arrayContains(juniorCodeChanges.conversationIds, [conversationId]), isNotNull(juniorCodeChanges.url)))
|
|
1009
|
-
]);
|
|
1010
|
-
const location = conversation.location;
|
|
1011
|
-
return parseBriefInput({
|
|
1012
|
-
conversationId,
|
|
1013
|
-
...conversation.title ? { title: conversation.title } : void 0,
|
|
1014
|
-
visibility: conversation.visibility ?? "private",
|
|
1015
|
-
...location ? { location: {
|
|
1016
|
-
provider: location.provider,
|
|
1017
|
-
...conversation.channelName ? { channelName: conversation.channelName } : void 0
|
|
1018
|
-
} } : void 0,
|
|
1019
|
-
entries: briefEntriesFromReportEvents(events),
|
|
1020
|
-
codeChanges: codeChanges.map((change) => ({
|
|
1021
|
-
repository: change.repository,
|
|
1022
|
-
number: change.number,
|
|
1023
|
-
...change.title ? { title: change.title } : void 0,
|
|
1024
|
-
url: change.url,
|
|
1025
|
-
state: change.state,
|
|
1026
|
-
openedAt: change.openedAt.toISOString(),
|
|
1027
|
-
...change.mergedAt ? { mergedAt: change.mergedAt.toISOString() } : void 0,
|
|
1028
|
-
...change.closedAt ? { closedAt: change.closedAt.toISOString() } : void 0
|
|
1029
|
-
})),
|
|
1030
|
-
resources: annotations.map((annotation) => ({
|
|
1031
|
-
label: annotation.label,
|
|
1032
|
-
url: annotation.url,
|
|
1033
|
-
...annotation.status ? { status: annotation.status } : void 0
|
|
1034
|
-
}))
|
|
837
|
+
/** Build a Sentry event URL only when the runtime has enough Sentry config. */
|
|
838
|
+
function buildSentryEventUrl(eventId) {
|
|
839
|
+
return buildSentryOrgPath({
|
|
840
|
+
path: "issues/",
|
|
841
|
+
query: { query: eventId }
|
|
1035
842
|
});
|
|
1036
843
|
}
|
|
1037
844
|
//#endregion
|
|
1038
|
-
//#region src/
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
845
|
+
//#region src/api/conversations/event-model.ts
|
|
846
|
+
/** Apply recorded model choices without carrying a previous turn's settings forward. */
|
|
847
|
+
function conversationEventModel(data, current) {
|
|
848
|
+
if (data.type === "turn_started") return void 0;
|
|
849
|
+
if (data.type === "turn_routed" || data.type === "handoff") return {
|
|
850
|
+
modelId: data.modelId,
|
|
851
|
+
modelProfile: data.modelProfile,
|
|
852
|
+
...data.reasoningLevel ? { reasoningLevel: data.reasoningLevel } : void 0
|
|
853
|
+
};
|
|
854
|
+
if (data.type === "assistant_message") {
|
|
855
|
+
const model = text$1(data.model);
|
|
856
|
+
const provider = text$1(data.provider);
|
|
857
|
+
if (model) {
|
|
858
|
+
const modelId = model.includes("/") || !provider ? model : `${provider}/${model}`;
|
|
859
|
+
return {
|
|
860
|
+
...current,
|
|
861
|
+
modelId
|
|
862
|
+
};
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
return current;
|
|
1052
866
|
}
|
|
1053
|
-
function
|
|
1054
|
-
|
|
1055
|
-
return new Intl.NumberFormat("en-US", {
|
|
1056
|
-
currency: "USD",
|
|
1057
|
-
maximumFractionDigits,
|
|
1058
|
-
minimumFractionDigits: 2,
|
|
1059
|
-
style: "currency"
|
|
1060
|
-
}).format(value);
|
|
867
|
+
function text$1(value) {
|
|
868
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
1061
869
|
}
|
|
1062
|
-
/**
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
const
|
|
1069
|
-
const windowStart = /* @__PURE__ */ new Date(newestDay.getTime() - 29 * DAY_MS$1);
|
|
1070
|
-
const windowEnd = new Date(newestDay.getTime() + DAY_MS$1);
|
|
1071
|
-
const [[counts], recentCounts] = await Promise.all([args.db.select({
|
|
1072
|
-
conversations: sql`count(distinct ${juniorConversationBriefs.conversationId})`.mapWith(Number),
|
|
1073
|
-
stored: sql`count(*)`.mapWith(Number)
|
|
1074
|
-
}).from(juniorConversationBriefs), args.db.select({ stored: sql`count(*)`.mapWith(Number) }).from(juniorConversationBriefs).where(and(gte(juniorConversationBriefs.createdAt, windowStart), lt(juniorConversationBriefs.createdAt, windowEnd)))]);
|
|
1075
|
-
const recentDays = args.briefDays.filter((day) => {
|
|
1076
|
-
const dayStart = Date.parse(`${day.date}T00:00:00.000Z`);
|
|
1077
|
-
return dayStart >= windowStart.getTime() && dayStart < windowEnd.getTime();
|
|
1078
|
-
});
|
|
1079
|
-
const recentCost = recentDays.reduce((total, day) => total + day.costUsd, 0);
|
|
1080
|
-
const recentBriefs = recentDays.reduce((total, day) => total + day.events, 0);
|
|
870
|
+
/** Allowlist model-call diagnostics; never expose opaque provider metadata. */
|
|
871
|
+
function conversationEventModelCall(data) {
|
|
872
|
+
if (data.type !== "assistant_message") return void 0;
|
|
873
|
+
const usage = extractGenAiUsageSummary(data);
|
|
874
|
+
const provider = text$1(data.provider);
|
|
875
|
+
const api = text$1(data.api);
|
|
876
|
+
const stopReason = text$1(data.stopReason);
|
|
1081
877
|
return {
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
label: "briefs stored",
|
|
1087
|
-
tone: counts?.stored ? "good" : "neutral",
|
|
1088
|
-
value: formatCount(counts?.stored ?? 0)
|
|
1089
|
-
},
|
|
1090
|
-
{
|
|
1091
|
-
label: "conversations with a brief",
|
|
1092
|
-
value: formatCount(counts?.conversations ?? 0)
|
|
1093
|
-
},
|
|
1094
|
-
{
|
|
1095
|
-
label: "briefs · 30d",
|
|
1096
|
-
value: formatCount(recentCounts[0]?.stored ?? 0)
|
|
1097
|
-
},
|
|
1098
|
-
{
|
|
1099
|
-
label: "cost · 30d · retained",
|
|
1100
|
-
value: formatUsd(recentCost)
|
|
1101
|
-
},
|
|
1102
|
-
{
|
|
1103
|
-
label: "average cost per brief · 30d · retained",
|
|
1104
|
-
value: formatUsd(recentBriefs === 0 ? 0 : recentCost / recentBriefs)
|
|
1105
|
-
}
|
|
1106
|
-
],
|
|
1107
|
-
widgets: [{
|
|
1108
|
-
categories: args.briefDays.map((day) => ({
|
|
1109
|
-
id: day.date,
|
|
1110
|
-
label: day.date,
|
|
1111
|
-
values: {
|
|
1112
|
-
briefs: day.events,
|
|
1113
|
-
costUsd: day.costUsd
|
|
1114
|
-
}
|
|
1115
|
-
})),
|
|
1116
|
-
description: "Stored Brief versions and estimated model cost",
|
|
1117
|
-
id: "briefs-created",
|
|
1118
|
-
series: [{
|
|
1119
|
-
key: "briefs",
|
|
1120
|
-
label: "Briefs"
|
|
1121
|
-
}, {
|
|
1122
|
-
format: "usd",
|
|
1123
|
-
key: "costUsd",
|
|
1124
|
-
label: "Cost"
|
|
1125
|
-
}],
|
|
1126
|
-
timeRangeDays: [...WINDOWS],
|
|
1127
|
-
title: "Briefs created",
|
|
1128
|
-
type: "bar_chart"
|
|
1129
|
-
}]
|
|
878
|
+
...provider ? { provider } : void 0,
|
|
879
|
+
...api ? { api } : void 0,
|
|
880
|
+
...stopReason ? { stopReason } : void 0,
|
|
881
|
+
...hasAgentTurnUsage(usage) ? { usage } : void 0
|
|
1130
882
|
};
|
|
1131
883
|
}
|
|
1132
884
|
//#endregion
|
|
1133
|
-
//#region src/
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
}
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
885
|
+
//#region src/api/conversations/events.ts
|
|
886
|
+
/** Canonical event types that can contribute to the reporting projection. */
|
|
887
|
+
const conversationReportSourceEventTypes = [
|
|
888
|
+
"message",
|
|
889
|
+
"message_handled",
|
|
890
|
+
"assistant_message",
|
|
891
|
+
"tool_result",
|
|
892
|
+
"tool_execution_started",
|
|
893
|
+
"guardian_action_reviewed",
|
|
894
|
+
"turn_started",
|
|
895
|
+
"turn_context",
|
|
896
|
+
"structured_event",
|
|
897
|
+
"attachments_delivered",
|
|
898
|
+
"turn_routed",
|
|
899
|
+
"turn_completed",
|
|
900
|
+
"turn_failed",
|
|
901
|
+
"compaction",
|
|
902
|
+
"handoff",
|
|
903
|
+
"subagent_started",
|
|
904
|
+
"subagent_ended"
|
|
905
|
+
];
|
|
906
|
+
const reportingAssistantMessageSchema = z.object({
|
|
907
|
+
type: z.literal("assistant_message"),
|
|
908
|
+
content: z.array(z.unknown())
|
|
909
|
+
}).passthrough();
|
|
910
|
+
const reportingToolCallPartSchema = z.object({
|
|
911
|
+
type: z.literal("toolCall"),
|
|
912
|
+
id: z.string().min(1),
|
|
913
|
+
name: z.string().min(1),
|
|
914
|
+
arguments: z.unknown()
|
|
915
|
+
}).passthrough();
|
|
916
|
+
const reportingToolResultMessageSchema = z.object({
|
|
917
|
+
type: z.literal("tool_result"),
|
|
918
|
+
toolCallId: z.string().min(1),
|
|
919
|
+
toolName: z.string().min(1).optional(),
|
|
920
|
+
name: z.string().min(1).optional(),
|
|
921
|
+
content: z.array(z.unknown()),
|
|
922
|
+
details: z.unknown().optional(),
|
|
923
|
+
isError: z.boolean().optional()
|
|
924
|
+
}).passthrough();
|
|
925
|
+
const reportingToolResultDetailsSchema = z.object({
|
|
926
|
+
ok: z.boolean().optional(),
|
|
927
|
+
status: z.string().optional()
|
|
928
|
+
}).passthrough();
|
|
929
|
+
const reportingTextPartSchema = z.object({
|
|
930
|
+
type: z.literal("text"),
|
|
931
|
+
text: z.string()
|
|
932
|
+
}).passthrough();
|
|
933
|
+
const reportingReasoningPartSchema = z.object({
|
|
934
|
+
type: z.literal("thinking"),
|
|
935
|
+
thinking: z.string(),
|
|
936
|
+
redacted: z.literal(true).optional()
|
|
937
|
+
}).passthrough();
|
|
938
|
+
const reportingMediaPartSchema = z.object({
|
|
939
|
+
type: z.enum(["image", "audio"]),
|
|
940
|
+
mimeType: z.string().min(1).optional()
|
|
941
|
+
}).passthrough();
|
|
942
|
+
const reportingMessageAuthorSchema = z.object({
|
|
943
|
+
fullName: z.string().min(1).optional(),
|
|
944
|
+
userId: z.string().min(1).optional(),
|
|
945
|
+
userName: z.string().min(1).optional()
|
|
946
|
+
}).passthrough();
|
|
947
|
+
/** Retain model-visible text and media descriptors, dropping all opaque fields. */
|
|
948
|
+
function sanitizeModelContentPart(value) {
|
|
949
|
+
const text = reportingTextPartSchema.safeParse(value);
|
|
950
|
+
if (text.success) return {
|
|
951
|
+
type: "text",
|
|
952
|
+
text: text.data.text
|
|
953
|
+
};
|
|
954
|
+
const media = reportingMediaPartSchema.safeParse(value);
|
|
955
|
+
if (!media.success) return void 0;
|
|
956
|
+
return {
|
|
957
|
+
type: media.data.type,
|
|
958
|
+
...media.data.mimeType ? { mimeType: media.data.mimeType } : void 0
|
|
959
|
+
};
|
|
1185
960
|
}
|
|
1186
|
-
/**
|
|
1187
|
-
|
|
1188
|
-
const
|
|
1189
|
-
|
|
961
|
+
/** Convert allowlisted Pi content parts into the reporting result value. */
|
|
962
|
+
function modelVisibleToolOutput(content) {
|
|
963
|
+
const sanitized = content.map(sanitizeModelContentPart).filter((part) => part !== void 0);
|
|
964
|
+
if (sanitized.length === 0) return void 0;
|
|
965
|
+
if (sanitized.length !== 1) return sanitized;
|
|
966
|
+
const only = sanitized[0];
|
|
967
|
+
return only.type === "text" ? only.text : only;
|
|
1190
968
|
}
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
const
|
|
1194
|
-
|
|
1195
|
-
|
|
969
|
+
function reportMessageActorIdentity(data) {
|
|
970
|
+
if (data.role !== "user") return void 0;
|
|
971
|
+
const author = reportingMessageAuthorSchema.safeParse(data.meta?.author);
|
|
972
|
+
if (!author.success) return void 0;
|
|
973
|
+
const actorIdentity = {
|
|
974
|
+
...author.data.fullName ? { fullName: author.data.fullName } : void 0,
|
|
975
|
+
...author.data.userId ? { slackUserId: author.data.userId } : void 0,
|
|
976
|
+
...author.data.userName ? { slackUserName: author.data.userName } : void 0
|
|
977
|
+
};
|
|
978
|
+
return Object.keys(actorIdentity).length > 0 ? actorIdentity : void 0;
|
|
1196
979
|
}
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
980
|
+
/** Project native assistant tool calls through the existing reporting shape. */
|
|
981
|
+
function reportToolCalls(args) {
|
|
982
|
+
const message = reportingAssistantMessageSchema.safeParse(args.message);
|
|
983
|
+
if (!message.success) return void 0;
|
|
984
|
+
const calls = message.data.content.flatMap((part) => {
|
|
985
|
+
const call = reportingToolCallPartSchema.safeParse(part);
|
|
986
|
+
if (!call.success) return [];
|
|
987
|
+
return [{
|
|
988
|
+
toolCallId: call.data.id,
|
|
989
|
+
name: call.data.name,
|
|
990
|
+
status: "running",
|
|
991
|
+
startedAt: new Date(args.createdAtMs).toISOString(),
|
|
992
|
+
startedSeq: args.seq,
|
|
993
|
+
...args.canExposePayload && call.data.arguments !== void 0 ? { input: call.data.arguments } : void 0
|
|
994
|
+
}];
|
|
1201
995
|
});
|
|
996
|
+
return calls.length > 0 ? {
|
|
997
|
+
type: "tool_calls",
|
|
998
|
+
calls
|
|
999
|
+
} : void 0;
|
|
1202
1000
|
}
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
return
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
logSkip(context, run.conversationId, "child_conversation");
|
|
1229
|
-
return;
|
|
1230
|
-
}
|
|
1231
|
-
if (conversation?.transcriptPurgedAtMs !== void 0) {
|
|
1232
|
-
if ((await resolveRootVisibility(executor, run.conversationId)).visibility !== "public") {
|
|
1233
|
-
logSkip(context, run.conversationId, "purged_private");
|
|
1234
|
-
return;
|
|
1235
|
-
}
|
|
1236
|
-
}
|
|
1237
|
-
if (!hasUserInstruction(run)) {
|
|
1238
|
-
logSkip(context, run.conversationId, "no_user_instruction");
|
|
1239
|
-
return;
|
|
1240
|
-
}
|
|
1241
|
-
await context.state.withLock(`brief:${run.conversationId}`, BRIEF_LOCK_TTL_MS, async () => {
|
|
1242
|
-
const db = context.db;
|
|
1243
|
-
const existing = await readConversationBriefForTurn(db, run.conversationId, run.runId);
|
|
1244
|
-
if (existing) {
|
|
1245
|
-
await emitBriefUpdated(context, existing);
|
|
1246
|
-
logSkip(context, run.conversationId, "turn_already_stored");
|
|
1247
|
-
return;
|
|
1248
|
-
}
|
|
1249
|
-
const terminalSeq = await readTurnCompletedSeq(db, run.conversationId, run.runId);
|
|
1250
|
-
const previous = await readLatestConversationBrief(db, run.conversationId);
|
|
1251
|
-
if (previous && previous.throughSeq >= terminalSeq) {
|
|
1252
|
-
logSkip(context, run.conversationId, "turn_already_covered");
|
|
1253
|
-
return;
|
|
1001
|
+
/** Project assistant reasoning with tool ids retained only for display order. */
|
|
1002
|
+
function reportAssistantMessage(args) {
|
|
1003
|
+
const message = reportingAssistantMessageSchema.safeParse(args.message);
|
|
1004
|
+
if (!message.success) return void 0;
|
|
1005
|
+
if (!message.data.content.some((part) => {
|
|
1006
|
+
const reasoning = reportingReasoningPartSchema.safeParse(part);
|
|
1007
|
+
return reasoning.success && reasoning.data.redacted !== true && reasoning.data.thinking.trim().length > 0;
|
|
1008
|
+
})) return reportToolCalls(args) ?? {
|
|
1009
|
+
type: "assistant_message",
|
|
1010
|
+
parts: []
|
|
1011
|
+
};
|
|
1012
|
+
const reasoningParts = [];
|
|
1013
|
+
const assistantParts = [];
|
|
1014
|
+
for (const part of message.data.content) {
|
|
1015
|
+
const reasoning = reportingReasoningPartSchema.safeParse(part);
|
|
1016
|
+
if (reasoning.success) {
|
|
1017
|
+
if (reasoning.data.redacted !== true && reasoning.data.thinking.trim().length > 0) {
|
|
1018
|
+
const projected = {
|
|
1019
|
+
type: "reasoning",
|
|
1020
|
+
...args.canExposePayload ? { text: reasoning.data.thinking } : { redacted: true }
|
|
1021
|
+
};
|
|
1022
|
+
reasoningParts.push(projected);
|
|
1023
|
+
assistantParts.push(projected);
|
|
1024
|
+
}
|
|
1025
|
+
continue;
|
|
1254
1026
|
}
|
|
1255
|
-
const
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
...request,
|
|
1260
|
-
modelId,
|
|
1261
|
-
promptName: "junior.brief_update"
|
|
1262
|
-
}),
|
|
1263
|
-
input,
|
|
1264
|
-
previous: previous?.content,
|
|
1265
|
-
prompt: BRIEF_PROMPT,
|
|
1266
|
-
throughIndex: terminalSeq
|
|
1027
|
+
const call = reportingToolCallPartSchema.safeParse(part);
|
|
1028
|
+
if (call.success) assistantParts.push({
|
|
1029
|
+
type: "tool_call",
|
|
1030
|
+
toolCallId: call.data.id
|
|
1267
1031
|
});
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
if (!stored.inserted) logSkip(context, run.conversationId, "turn_already_stored");
|
|
1279
|
-
});
|
|
1280
|
-
}
|
|
1281
|
-
/** Core task registration kept outside the installed plugin catalog. */
|
|
1282
|
-
const briefsTaskRegistration = {
|
|
1283
|
-
manifest: {
|
|
1284
|
-
name: "briefs",
|
|
1285
|
-
displayName: "Briefs",
|
|
1286
|
-
description: "Durable Conversation Brief generation"
|
|
1287
|
-
},
|
|
1288
|
-
conversationEvents: [briefUpdatedEvent],
|
|
1289
|
-
hooks: { async operationalReport(context) {
|
|
1290
|
-
return await buildBriefsOperationalReport({
|
|
1291
|
-
briefDays: await context.eventStats.costsByDay({
|
|
1292
|
-
days: 90,
|
|
1293
|
-
eventName: "brief_updated"
|
|
1294
|
-
}),
|
|
1295
|
-
db: context.db,
|
|
1296
|
-
nowMs: context.nowMs
|
|
1297
|
-
});
|
|
1298
|
-
} },
|
|
1299
|
-
tasks: { updateBrief: { async run(context) {
|
|
1300
|
-
await updateConversationBrief(context);
|
|
1301
|
-
} } }
|
|
1302
|
-
};
|
|
1303
|
-
//#endregion
|
|
1304
|
-
//#region src/chat/plugins/conversation-events.ts
|
|
1305
|
-
function registeredDefinition(plugin, value) {
|
|
1306
|
-
return plugin.conversationEvents?.find((definition) => definition === value.definition);
|
|
1307
|
-
}
|
|
1308
|
-
/** Keep operation idempotency stable when a registered event schema advances. */
|
|
1309
|
-
function eventIdentityVersion(plugin, definition) {
|
|
1310
|
-
let version = definition.version;
|
|
1311
|
-
for (const candidate of plugin.conversationEvents ?? []) if (candidate.eventName === definition.eventName) version = Math.min(version, candidate.version);
|
|
1312
|
-
return version;
|
|
1032
|
+
}
|
|
1033
|
+
const toolCalls = reportToolCalls(args);
|
|
1034
|
+
if (toolCalls?.type === "tool_calls") return {
|
|
1035
|
+
...toolCalls,
|
|
1036
|
+
assistant: { parts: assistantParts }
|
|
1037
|
+
};
|
|
1038
|
+
return reasoningParts.length > 0 ? {
|
|
1039
|
+
type: "assistant_message",
|
|
1040
|
+
parts: reasoningParts
|
|
1041
|
+
} : void 0;
|
|
1313
1042
|
}
|
|
1314
|
-
/**
|
|
1315
|
-
function
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1043
|
+
/** Project a native tool result without exposing host-only result details. */
|
|
1044
|
+
function reportToolResult(args) {
|
|
1045
|
+
const message = reportingToolResultMessageSchema.safeParse(args.message);
|
|
1046
|
+
if (!message.success) return void 0;
|
|
1047
|
+
const details = reportingToolResultDetailsSchema.safeParse(message.data.details);
|
|
1048
|
+
const outcome = message.data.isError === true || details.success && (details.data.ok === false || details.data.status === "error") ? "error" : "completed";
|
|
1049
|
+
const output = modelVisibleToolOutput(message.data.content);
|
|
1050
|
+
const name = message.data.toolName ?? message.data.name ?? args.start?.name;
|
|
1051
|
+
if (!name) return void 0;
|
|
1052
|
+
return {
|
|
1053
|
+
type: "tool_calls",
|
|
1054
|
+
calls: [{
|
|
1055
|
+
toolCallId: message.data.toolCallId,
|
|
1056
|
+
name,
|
|
1057
|
+
status: outcome,
|
|
1058
|
+
...args.start ? {
|
|
1059
|
+
startedAt: new Date(args.start.createdAtMs).toISOString(),
|
|
1060
|
+
startedSeq: args.start.seq
|
|
1061
|
+
} : void 0,
|
|
1062
|
+
...args.canExposePayload && output !== void 0 ? { output } : void 0
|
|
1063
|
+
}]
|
|
1064
|
+
};
|
|
1334
1065
|
}
|
|
1335
|
-
/**
|
|
1336
|
-
function
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1066
|
+
/** Return tool result ids that may need start metadata from outside a page. */
|
|
1067
|
+
function conversationReportToolResultIds(events) {
|
|
1068
|
+
return [...new Set(events.flatMap((event) => {
|
|
1069
|
+
const result = reportingToolResultMessageSchema.safeParse(event.data);
|
|
1070
|
+
return result.success ? [result.data.toolCallId] : [];
|
|
1071
|
+
}))];
|
|
1340
1072
|
}
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1073
|
+
function reportEventData(args) {
|
|
1074
|
+
const { data } = args;
|
|
1075
|
+
if (data.type === "structured_event") {
|
|
1076
|
+
if (!args.canExposePayload) return void 0;
|
|
1077
|
+
const presentation = data.namespace === "junior" ? renderJuniorNativeConversationEvent(data) : renderPluginConversationEvent(data);
|
|
1078
|
+
if (!presentation) return void 0;
|
|
1079
|
+
return {
|
|
1080
|
+
type: "structured_event",
|
|
1081
|
+
namespace: data.namespace,
|
|
1082
|
+
name: data.name,
|
|
1083
|
+
version: data.version,
|
|
1084
|
+
...data.turnId ? { turnId: data.turnId } : void 0,
|
|
1085
|
+
presentation
|
|
1086
|
+
};
|
|
1087
|
+
}
|
|
1088
|
+
switch (data.type) {
|
|
1089
|
+
case "message": {
|
|
1090
|
+
const actorIdentity = args.canExposePayload ? reportMessageActorIdentity(data) : void 0;
|
|
1091
|
+
return {
|
|
1092
|
+
type: "message",
|
|
1093
|
+
messageId: data.messageId,
|
|
1094
|
+
role: data.role,
|
|
1095
|
+
...data.meta?.source === "web" || data.meta?.source === "slack" ? { source: data.meta.source } : void 0,
|
|
1096
|
+
...actorIdentity ? { actorIdentity } : void 0,
|
|
1097
|
+
eventObjectType: objectTypeSchema.safeParse(data.meta?.eventObjectType).data,
|
|
1098
|
+
...typeof data.meta?.eventType === "string" ? { eventType: data.meta.eventType } : void 0,
|
|
1099
|
+
...typeof data.meta?.explicitMention === "boolean" ? { explicitMention: data.meta.explicitMention } : void 0,
|
|
1100
|
+
...args.canExposePayload && typeof data.meta?.trustedSummary === "string" ? { trustedSummary: data.meta.trustedSummary } : void 0,
|
|
1101
|
+
...args.canExposePayload && (data.meta?.cards || data.meta?.objectCards) ? { cards: readMessageCardRefs(data.meta) } : void 0,
|
|
1102
|
+
...args.canExposePayload ? {
|
|
1103
|
+
text: data.text,
|
|
1104
|
+
...data.meta?.attachments ? { attachments: readMessageAttachments(data.meta.attachments) } : void 0
|
|
1105
|
+
} : { redacted: true }
|
|
1106
|
+
};
|
|
1107
|
+
}
|
|
1108
|
+
case "message_handled": return {
|
|
1109
|
+
type: "message_handled",
|
|
1110
|
+
messageId: data.messageId
|
|
1111
|
+
};
|
|
1112
|
+
case "turn_started": return {
|
|
1113
|
+
type: "turn_lifecycle",
|
|
1114
|
+
turnId: data.turnId,
|
|
1115
|
+
state: "started",
|
|
1116
|
+
...data.inputMessageIds.length > 0 ? { inputMessageIds: data.inputMessageIds } : void 0
|
|
1117
|
+
};
|
|
1118
|
+
case "turn_context":
|
|
1119
|
+
if (!args.canExposePayload) return;
|
|
1120
|
+
return {
|
|
1121
|
+
type: "turn_context",
|
|
1122
|
+
turnId: data.turnId,
|
|
1123
|
+
pluginName: data.pluginName,
|
|
1124
|
+
kind: data.kind,
|
|
1125
|
+
version: data.version,
|
|
1126
|
+
content: data.content
|
|
1127
|
+
};
|
|
1128
|
+
case "attachments_delivered":
|
|
1129
|
+
if (!args.canExposePayload) return;
|
|
1130
|
+
return {
|
|
1131
|
+
type: "attachments_delivered",
|
|
1132
|
+
attachments: data.attachments
|
|
1133
|
+
};
|
|
1134
|
+
case "turn_routed": return {
|
|
1135
|
+
type: "turn_routed",
|
|
1136
|
+
turnId: data.turnId,
|
|
1137
|
+
modelProfile: data.modelProfile,
|
|
1138
|
+
modelId: data.modelId,
|
|
1139
|
+
...data.costUsd !== void 0 ? { costUsd: data.costUsd } : void 0,
|
|
1140
|
+
reasoningLevel: data.reasoningLevel,
|
|
1141
|
+
...data.confidence !== void 0 ? { confidence: data.confidence } : void 0,
|
|
1142
|
+
source: data.source
|
|
1143
|
+
};
|
|
1144
|
+
case "guardian_action_reviewed": return {
|
|
1145
|
+
type: "guardian_action_reviewed",
|
|
1146
|
+
turnId: data.turnId,
|
|
1147
|
+
toolCallId: data.toolCallId,
|
|
1148
|
+
toolName: data.toolName,
|
|
1149
|
+
decision: data.decision,
|
|
1150
|
+
riskLevel: data.riskLevel,
|
|
1151
|
+
userAuthorization: data.userAuthorization
|
|
1152
|
+
};
|
|
1153
|
+
case "turn_completed": return {
|
|
1154
|
+
type: "turn_lifecycle",
|
|
1155
|
+
turnId: data.turnId,
|
|
1156
|
+
state: data.outcome === "success" ? "succeeded" : "no_reply"
|
|
1157
|
+
};
|
|
1158
|
+
case "turn_failed": {
|
|
1159
|
+
const eventId = data.eventId;
|
|
1160
|
+
const sentryEventUrl = eventId ? buildSentryEventUrl(eventId) : void 0;
|
|
1161
|
+
return {
|
|
1162
|
+
type: "turn_lifecycle",
|
|
1163
|
+
turnId: data.turnId,
|
|
1164
|
+
state: "failed",
|
|
1165
|
+
failureCode: data.failureCode,
|
|
1166
|
+
...data.failureReason ? { failureReason: data.failureReason } : void 0,
|
|
1167
|
+
...eventId ? { eventId } : void 0,
|
|
1168
|
+
...sentryEventUrl ? { sentryEventUrl } : void 0
|
|
1169
|
+
};
|
|
1170
|
+
}
|
|
1171
|
+
case "compaction": return {
|
|
1172
|
+
type: "compaction",
|
|
1173
|
+
modelProfile: data.modelProfile,
|
|
1174
|
+
modelId: data.modelId,
|
|
1175
|
+
...args.canExposePayload && data.summary ? { summary: data.summary } : void 0,
|
|
1176
|
+
...data.details ? { details: data.details } : void 0
|
|
1177
|
+
};
|
|
1178
|
+
default: return;
|
|
1179
|
+
}
|
|
1347
1180
|
}
|
|
1348
1181
|
/**
|
|
1349
|
-
*
|
|
1350
|
-
*
|
|
1182
|
+
* Project canonical events into the ordered reporting boundary.
|
|
1183
|
+
*
|
|
1184
|
+
* `canExposePayload` must come from the authorized conversation-detail policy;
|
|
1185
|
+
* this projection never derives visibility or authorization from event data.
|
|
1351
1186
|
*/
|
|
1352
|
-
function
|
|
1353
|
-
const
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
function conversationArchivedForEmail(email, archivedAfter) {
|
|
1368
|
-
const archive = getDb().select({ rootConversationId: juniorConversationParticipants.rootConversationId }).from(juniorConversationParticipants).innerJoin(juniorUsers, eq(juniorUsers.id, juniorConversationParticipants.userId)).where(and(eq(juniorConversationParticipants.rootConversationId, juniorConversations.conversationId), eq(juniorUsers.primaryEmailNormalized, email), isNotNull(juniorConversationParticipants.archivedAt), archivedAfter ? gte(juniorConversationParticipants.archivedAt, archivedAfter) : void 0));
|
|
1369
|
-
return exists(archive);
|
|
1370
|
-
}
|
|
1371
|
-
/** True when one primary-email user's personal feed has not archived this root. */
|
|
1372
|
-
function conversationNotArchivedForEmail(email) {
|
|
1373
|
-
const archive = getDb().select({ rootConversationId: juniorConversationParticipants.rootConversationId }).from(juniorConversationParticipants).innerJoin(juniorUsers, eq(juniorUsers.id, juniorConversationParticipants.userId)).where(and(eq(juniorConversationParticipants.rootConversationId, juniorConversations.conversationId), eq(juniorUsers.primaryEmailNormalized, email), isNotNull(juniorConversationParticipants.archivedAt)));
|
|
1374
|
-
return notExists(archive);
|
|
1375
|
-
}
|
|
1376
|
-
/** Root-actor ownership or a materialized participant row for the same person. */
|
|
1377
|
-
function viewerConversationMembership(args) {
|
|
1378
|
-
if (args.actorMatch && args.participantMatch) return or(args.actorMatch, args.participantMatch);
|
|
1379
|
-
return args.actorMatch ?? args.participantMatch;
|
|
1380
|
-
}
|
|
1381
|
-
/** Read personal archive timestamps for a bounded set of conversation roots. */
|
|
1382
|
-
async function conversationArchiveTimes(db, conversationIds, subject) {
|
|
1383
|
-
if (!subject || conversationIds.length === 0) return /* @__PURE__ */ new Map();
|
|
1384
|
-
const query = db.select({
|
|
1385
|
-
archivedAt: juniorConversationParticipants.archivedAt,
|
|
1386
|
-
rootConversationId: juniorConversationParticipants.rootConversationId
|
|
1387
|
-
}).from(juniorConversationParticipants);
|
|
1388
|
-
const rows = "userId" in subject ? await query.where(and(eq(juniorConversationParticipants.userId, subject.userId), inArray(juniorConversationParticipants.rootConversationId, [...conversationIds]), isNotNull(juniorConversationParticipants.archivedAt))) : await query.innerJoin(juniorUsers, eq(juniorUsers.id, juniorConversationParticipants.userId)).where(and(eq(juniorUsers.primaryEmailNormalized, subject.email), inArray(juniorConversationParticipants.rootConversationId, [...conversationIds]), isNotNull(juniorConversationParticipants.archivedAt)));
|
|
1389
|
-
return new Map(rows.flatMap((row) => row.archivedAt ? [[row.rootConversationId, row.archivedAt.getTime()]] : []));
|
|
1390
|
-
}
|
|
1391
|
-
/** Find which of the supplied roots include the linked viewer as a participant. */
|
|
1392
|
-
async function conversationIdsWithParticipantUser(db, conversationIds, userId) {
|
|
1393
|
-
if (conversationIds.length === 0) return /* @__PURE__ */ new Set();
|
|
1394
|
-
const rows = await db.select({ rootConversationId: juniorConversationParticipants.rootConversationId }).from(juniorConversationParticipants).where(and(eq(juniorConversationParticipants.userId, userId), inArray(juniorConversationParticipants.rootConversationId, [...conversationIds])));
|
|
1395
|
-
return new Set(rows.map((row) => row.rootConversationId));
|
|
1396
|
-
}
|
|
1397
|
-
//#endregion
|
|
1398
|
-
//#region src/api/conversations/access.ts
|
|
1399
|
-
const rootConversation = alias(juniorConversations, "access_root_conversation");
|
|
1400
|
-
const rootDestination = alias(juniorDestinations, "access_root_destination");
|
|
1401
|
-
const rootIdentity = alias(juniorIdentities, "access_root_identity");
|
|
1402
|
-
/** Resolve viewer access for a bounded set of persisted conversations. */
|
|
1403
|
-
async function readConversationAccessFromSql(db, conversationIds, viewer) {
|
|
1404
|
-
if (conversationIds.length === 0) return /* @__PURE__ */ new Map();
|
|
1405
|
-
const normalizedViewerEmail = viewer?.email.trim().toLowerCase() || void 0;
|
|
1406
|
-
const rows = await db.select({
|
|
1407
|
-
conversationId: juniorConversations.conversationId,
|
|
1408
|
-
parentConversationId: juniorConversations.parentConversationId,
|
|
1409
|
-
storedRootConversationId: juniorConversations.rootConversationId,
|
|
1410
|
-
rootConversationId: rootConversation.conversationId,
|
|
1411
|
-
rootParentConversationId: rootConversation.parentConversationId,
|
|
1412
|
-
rootRootConversationId: rootConversation.rootConversationId,
|
|
1413
|
-
visibility: rootDestination.visibility,
|
|
1414
|
-
rootEmailNormalized: rootIdentity.emailNormalized,
|
|
1415
|
-
rootEmailVerified: rootIdentity.emailVerified,
|
|
1416
|
-
rootUserId: rootIdentity.userId
|
|
1417
|
-
}).from(juniorConversations).leftJoin(rootConversation, eq(rootConversation.conversationId, juniorConversations.rootConversationId)).leftJoin(rootDestination, eq(rootDestination.id, rootConversation.destinationId)).leftJoin(rootIdentity, eq(rootIdentity.id, rootConversation.actorIdentityId)).where(inArray(juniorConversations.conversationId, [...conversationIds]));
|
|
1418
|
-
const membershipConversationIds = /* @__PURE__ */ new Set();
|
|
1419
|
-
for (const row of rows) {
|
|
1420
|
-
membershipConversationIds.add(row.conversationId);
|
|
1421
|
-
if (row.storedRootConversationId) membershipConversationIds.add(row.storedRootConversationId);
|
|
1187
|
+
function projectConversationReportEventPage(args) {
|
|
1188
|
+
const subagentStarts = /* @__PURE__ */ new Map();
|
|
1189
|
+
for (const event of args.subagentStartEvents ?? []) if (event.data.type === "subagent_started") subagentStarts.set(event.data.subagentInvocationId, {
|
|
1190
|
+
createdAtMs: event.createdAtMs,
|
|
1191
|
+
data: event.data,
|
|
1192
|
+
seq: event.seq
|
|
1193
|
+
});
|
|
1194
|
+
const toolStarts = /* @__PURE__ */ new Map();
|
|
1195
|
+
for (const event of args.toolStartEvents ?? []) if (event.data.type === "tool_execution_started") {
|
|
1196
|
+
const current = toolStarts.get(event.data.toolCallId);
|
|
1197
|
+
if (!current || event.seq < current.seq) toolStarts.set(event.data.toolCallId, {
|
|
1198
|
+
createdAtMs: event.createdAtMs,
|
|
1199
|
+
name: event.data.toolName,
|
|
1200
|
+
seq: event.seq
|
|
1201
|
+
});
|
|
1422
1202
|
}
|
|
1423
|
-
const
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
if (
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1203
|
+
const projected = [];
|
|
1204
|
+
let model;
|
|
1205
|
+
for (const event of args.modelContextEvents ?? []) {
|
|
1206
|
+
model = conversationEventModel(event.data, model);
|
|
1207
|
+
if (event.data.type === "turn_completed" || event.data.type === "turn_failed") model = void 0;
|
|
1208
|
+
}
|
|
1209
|
+
for (const event of args.events) {
|
|
1210
|
+
model = conversationEventModel(event.data, model);
|
|
1211
|
+
const eventModel = model;
|
|
1212
|
+
const modelCall = conversationEventModelCall(event.data);
|
|
1213
|
+
if (event.data.type === "turn_completed" || event.data.type === "turn_failed") model = void 0;
|
|
1214
|
+
let data;
|
|
1215
|
+
if (event.data.type === "assistant_message" || event.data.type === "tool_result") {
|
|
1216
|
+
const reportArgs = {
|
|
1217
|
+
canExposePayload: args.canExposePayload,
|
|
1218
|
+
createdAtMs: event.createdAtMs,
|
|
1219
|
+
message: event.data,
|
|
1220
|
+
seq: event.seq
|
|
1221
|
+
};
|
|
1222
|
+
const result = reportingToolResultMessageSchema.safeParse(event.data);
|
|
1223
|
+
const start = result.success ? toolStarts.get(result.data.toolCallId) : void 0;
|
|
1224
|
+
data = reportAssistantMessage(reportArgs) ?? reportToolResult({
|
|
1225
|
+
canExposePayload: args.canExposePayload,
|
|
1226
|
+
message: event.data,
|
|
1227
|
+
...start && start.seq < event.seq ? { start } : void 0
|
|
1228
|
+
});
|
|
1229
|
+
} else if (event.data.type === "tool_execution_started") {
|
|
1230
|
+
toolStarts.set(event.data.toolCallId, {
|
|
1231
|
+
createdAtMs: event.createdAtMs,
|
|
1232
|
+
name: event.data.toolName,
|
|
1233
|
+
seq: event.seq
|
|
1234
|
+
});
|
|
1235
|
+
data = {
|
|
1236
|
+
type: "tool_calls",
|
|
1237
|
+
calls: [{
|
|
1238
|
+
toolCallId: event.data.toolCallId,
|
|
1239
|
+
name: event.data.toolName,
|
|
1240
|
+
status: "running",
|
|
1241
|
+
startedAt: new Date(event.createdAtMs).toISOString(),
|
|
1242
|
+
startedSeq: event.seq
|
|
1243
|
+
}]
|
|
1244
|
+
};
|
|
1245
|
+
} else if (event.data.type === "subagent_started") {
|
|
1246
|
+
subagentStarts.set(event.data.subagentInvocationId, {
|
|
1247
|
+
createdAtMs: event.createdAtMs,
|
|
1248
|
+
data: event.data,
|
|
1249
|
+
seq: event.seq
|
|
1250
|
+
});
|
|
1251
|
+
data = {
|
|
1252
|
+
type: "subagent",
|
|
1253
|
+
startedSeq: event.seq,
|
|
1254
|
+
startedAt: new Date(event.createdAtMs).toISOString(),
|
|
1255
|
+
childConversationId: event.data.childConversationId,
|
|
1256
|
+
subagentKind: event.data.subagentKind,
|
|
1257
|
+
status: "running",
|
|
1258
|
+
...event.data.parentToolCallId ? { parentToolCallId: event.data.parentToolCallId } : void 0
|
|
1259
|
+
};
|
|
1260
|
+
} else if (event.data.type === "subagent_ended") {
|
|
1261
|
+
const started = subagentStarts.get(event.data.subagentInvocationId);
|
|
1262
|
+
if (started && started.seq < event.seq) {
|
|
1263
|
+
data = {
|
|
1264
|
+
type: "subagent",
|
|
1265
|
+
startedSeq: started.seq,
|
|
1266
|
+
startedAt: new Date(started.createdAtMs).toISOString(),
|
|
1267
|
+
childConversationId: started.data.childConversationId,
|
|
1268
|
+
subagentKind: started.data.subagentKind,
|
|
1269
|
+
...started.data.parentToolCallId ? { parentToolCallId: started.data.parentToolCallId } : void 0,
|
|
1270
|
+
status: event.data.outcome === "success" ? "completed" : event.data.outcome
|
|
1271
|
+
};
|
|
1272
|
+
subagentStarts.delete(event.data.subagentInvocationId);
|
|
1273
|
+
}
|
|
1274
|
+
} else if (event.data.type === "handoff") data = {
|
|
1275
|
+
type: "handoff",
|
|
1276
|
+
modelProfile: event.data.modelProfile,
|
|
1277
|
+
modelId: event.data.modelId,
|
|
1278
|
+
...event.data.reasoningLevel ? { reasoningLevel: event.data.reasoningLevel } : void 0,
|
|
1279
|
+
...event.data.triggeringToolCallId ? { triggeringToolCallId: event.data.triggeringToolCallId } : void 0,
|
|
1280
|
+
...args.canExposePayload && event.data.summary ? { summary: event.data.summary } : void 0
|
|
1281
|
+
};
|
|
1282
|
+
else data = reportEventData({
|
|
1283
|
+
canExposePayload: args.canExposePayload,
|
|
1284
|
+
data: event.data
|
|
1285
|
+
});
|
|
1286
|
+
if (!data) continue;
|
|
1287
|
+
projected.push(conversationReportEventSchema.parse({
|
|
1288
|
+
seq: event.seq,
|
|
1289
|
+
createdAt: new Date(event.createdAtMs).toISOString(),
|
|
1290
|
+
data,
|
|
1291
|
+
...eventModel ? { model: eventModel } : void 0,
|
|
1292
|
+
...modelCall ? { modelCall } : void 0
|
|
1458
1293
|
}));
|
|
1459
|
-
}
|
|
1294
|
+
}
|
|
1295
|
+
return projected;
|
|
1460
1296
|
}
|
|
1461
1297
|
//#endregion
|
|
1462
|
-
//#region src/
|
|
1463
|
-
const
|
|
1464
|
-
const
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1298
|
+
//#region src/api/conversations/event-page.ts
|
|
1299
|
+
const conversationEventColumns = getTableColumns(juniorConversationEvents);
|
|
1300
|
+
const MIN_SCAN_SIZE = 64;
|
|
1301
|
+
/** Read bounded canonical rows while excluding model-only replacement history. */
|
|
1302
|
+
async function readConversationEventRows(executor, args) {
|
|
1303
|
+
return executor.db().select({
|
|
1304
|
+
...conversationEventColumns,
|
|
1305
|
+
payload: sql`case
|
|
1306
|
+
when ${juniorConversationEvents.type} in ('compaction', 'handoff')
|
|
1307
|
+
then jsonb_set(
|
|
1308
|
+
${juniorConversationEvents.payload},
|
|
1309
|
+
'{replacementHistory}',
|
|
1310
|
+
'[]'::jsonb
|
|
1311
|
+
)
|
|
1312
|
+
else ${juniorConversationEvents.payload}
|
|
1313
|
+
end`
|
|
1314
|
+
}).from(juniorConversationEvents).where(and(eq(juniorConversationEvents.conversationId, args.conversationId), args.beforeSeq === void 0 ? void 0 : lt(juniorConversationEvents.seq, args.beforeSeq), inArray(juniorConversationEvents.type, args.types ?? [...conversationReportSourceEventTypes]), args.subagentInvocationIds === void 0 ? void 0 : inArray(sql`${juniorConversationEvents.payload}->>'subagentInvocationId'`, args.subagentInvocationIds), args.toolCallIds === void 0 ? void 0 : inArray(sql`${juniorConversationEvents.payload}->>'toolCallId'`, args.toolCallIds))).orderBy(args.direction === "forward" ? asc(juniorConversationEvents.seq) : desc(juniorConversationEvents.seq)).limit(args.limit);
|
|
1473
1315
|
}
|
|
1474
|
-
function
|
|
1475
|
-
|
|
1476
|
-
|
|
1316
|
+
function decodeConversationEventRow(row) {
|
|
1317
|
+
return decodeStoredConversationEvent({
|
|
1318
|
+
schemaVersion: row.schemaVersion,
|
|
1319
|
+
seq: row.seq,
|
|
1320
|
+
historyVersion: row.historyVersion,
|
|
1321
|
+
...row.idempotencyKey ? { idempotencyKey: row.idempotencyKey } : void 0,
|
|
1322
|
+
createdAtMs: row.createdAt.getTime(),
|
|
1323
|
+
type: row.type,
|
|
1324
|
+
payload: row.payload
|
|
1325
|
+
});
|
|
1477
1326
|
}
|
|
1478
|
-
/**
|
|
1479
|
-
function
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
ELSE 0
|
|
1525
|
-
END`;
|
|
1526
|
-
const rows = await getDb().select({
|
|
1527
|
-
costUsd: sql`SUM(${cost})::double precision`.mapWith(Number),
|
|
1528
|
-
date,
|
|
1529
|
-
events: sql`COUNT(*)::integer`.mapWith(Number)
|
|
1530
|
-
}).from(juniorConversationEvents).where(and(eq(juniorConversationEvents.type, "structured_event"), sql`${juniorConversationEvents.payload}->>'namespace' = ${plugin.manifest.name}`, sql`${juniorConversationEvents.payload}->>'name' = ${eventName}`, gte(juniorConversationEvents.createdAt, start), lt(juniorConversationEvents.createdAt, endExclusive))).groupBy(date);
|
|
1531
|
-
const byDate = new Map(z.array(costRowSchema).parse(rows).map((row) => [row.date, row]));
|
|
1532
|
-
return Array.from({ length: input.days }, (_, index) => {
|
|
1533
|
-
const date = new Date(start.getTime() + index * DAY_MS).toISOString().slice(0, 10);
|
|
1534
|
-
return byDate.get(date) ?? {
|
|
1535
|
-
costUsd: 0,
|
|
1536
|
-
date,
|
|
1537
|
-
events: 0
|
|
1538
|
-
};
|
|
1539
|
-
});
|
|
1540
|
-
}
|
|
1541
|
-
};
|
|
1327
|
+
/** Project stored rows and resolve entity starts that precede the page. */
|
|
1328
|
+
async function projectConversationEventRows(executor, args) {
|
|
1329
|
+
const events = args.rows.map(decodeConversationEventRow).sort((left, right) => left.seq - right.seq);
|
|
1330
|
+
const modelContextRows = events[0] ? (await Promise.all([readConversationEventRows(executor, {
|
|
1331
|
+
conversationId: args.conversationId,
|
|
1332
|
+
beforeSeq: events[0].seq,
|
|
1333
|
+
direction: "backward",
|
|
1334
|
+
limit: 1,
|
|
1335
|
+
types: [
|
|
1336
|
+
"turn_started",
|
|
1337
|
+
"turn_routed",
|
|
1338
|
+
"handoff",
|
|
1339
|
+
"turn_completed",
|
|
1340
|
+
"turn_failed"
|
|
1341
|
+
]
|
|
1342
|
+
}), readConversationEventRows(executor, {
|
|
1343
|
+
conversationId: args.conversationId,
|
|
1344
|
+
beforeSeq: events[0].seq,
|
|
1345
|
+
direction: "backward",
|
|
1346
|
+
limit: 1,
|
|
1347
|
+
types: ["assistant_message"]
|
|
1348
|
+
})])).flat().sort((left, right) => left.seq - right.seq) : [];
|
|
1349
|
+
const endedInvocationIds = [...new Set(events.flatMap((event) => event.data.type === "subagent_ended" ? [event.data.subagentInvocationId] : []))];
|
|
1350
|
+
const subagentStartRows = endedInvocationIds.length === 0 ? [] : await readConversationEventRows(executor, {
|
|
1351
|
+
conversationId: args.conversationId,
|
|
1352
|
+
direction: "forward",
|
|
1353
|
+
limit: endedInvocationIds.length,
|
|
1354
|
+
subagentInvocationIds: endedInvocationIds,
|
|
1355
|
+
types: ["subagent_started"]
|
|
1356
|
+
});
|
|
1357
|
+
const toolResultIds = conversationReportToolResultIds(events);
|
|
1358
|
+
const toolStartRows = toolResultIds.length === 0 ? [] : await readConversationEventRows(executor, {
|
|
1359
|
+
conversationId: args.conversationId,
|
|
1360
|
+
direction: "forward",
|
|
1361
|
+
limit: toolResultIds.length,
|
|
1362
|
+
toolCallIds: toolResultIds,
|
|
1363
|
+
types: ["tool_execution_started"]
|
|
1364
|
+
});
|
|
1365
|
+
if (args.canExposePayload) await hydrateMessageAttachments(executor, args.conversationId, events);
|
|
1366
|
+
return projectConversationReportEventPage({
|
|
1367
|
+
canExposePayload: args.canExposePayload,
|
|
1368
|
+
events,
|
|
1369
|
+
subagentStartEvents: subagentStartRows.map(decodeConversationEventRow),
|
|
1370
|
+
toolStartEvents: toolStartRows.map(decodeConversationEventRow),
|
|
1371
|
+
modelContextEvents: modelContextRows.map(decodeConversationEventRow)
|
|
1372
|
+
});
|
|
1542
1373
|
}
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1374
|
+
/**
|
|
1375
|
+
* Read the latest reporting events before an exclusive boundary.
|
|
1376
|
+
*
|
|
1377
|
+
* Source rows are scanned in bounded chunks until the requested projected page
|
|
1378
|
+
* and one older event are known. This keeps ordinary pages bounded without
|
|
1379
|
+
* assuming every agent history event produces a reporting event.
|
|
1380
|
+
*/
|
|
1381
|
+
async function readConversationEventPage(executor, args) {
|
|
1382
|
+
const scanSize = Math.max(args.limit + 1, MIN_SCAN_SIZE);
|
|
1383
|
+
const rows = [];
|
|
1384
|
+
let beforeSeq = args.beforeSeq;
|
|
1385
|
+
let projected = [];
|
|
1386
|
+
while (projected.length <= args.limit) {
|
|
1387
|
+
const batch = await readConversationEventRows(executor, {
|
|
1388
|
+
beforeSeq,
|
|
1389
|
+
conversationId: args.conversationId,
|
|
1390
|
+
direction: "backward",
|
|
1391
|
+
limit: scanSize
|
|
1392
|
+
});
|
|
1393
|
+
if (batch.length === 0) break;
|
|
1394
|
+
rows.push(...batch);
|
|
1395
|
+
projected = await projectConversationEventRows(executor, {
|
|
1396
|
+
canExposePayload: args.canExposePayload,
|
|
1397
|
+
conversationId: args.conversationId,
|
|
1398
|
+
rows
|
|
1399
|
+
});
|
|
1400
|
+
if (projected.length > args.limit || batch.length < scanSize) break;
|
|
1401
|
+
beforeSeq = batch.at(-1).seq;
|
|
1402
|
+
}
|
|
1403
|
+
const events = projected.slice(-args.limit);
|
|
1547
1404
|
return {
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
"app.log.message": message,
|
|
1551
|
-
"app.plugin.name": plugin,
|
|
1552
|
-
...metadata
|
|
1553
|
-
});
|
|
1554
|
-
},
|
|
1555
|
-
warn(message, metadata) {
|
|
1556
|
-
logWarn("plugin.log.warn", {
|
|
1557
|
-
"app.log.message": message,
|
|
1558
|
-
"app.plugin.name": plugin,
|
|
1559
|
-
...metadata
|
|
1560
|
-
});
|
|
1561
|
-
},
|
|
1562
|
-
error(message, metadata) {
|
|
1563
|
-
logException(new Error(message), "plugin.log.error", {
|
|
1564
|
-
"app.plugin.name": plugin,
|
|
1565
|
-
...metadata
|
|
1566
|
-
});
|
|
1567
|
-
}
|
|
1405
|
+
events,
|
|
1406
|
+
...projected.length > events.length && events[0] ? { previousSeq: events[0].seq } : void 0
|
|
1568
1407
|
};
|
|
1569
1408
|
}
|
|
1570
1409
|
//#endregion
|
|
1571
|
-
//#region src/chat/
|
|
1572
|
-
|
|
1573
|
-
function
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
signal: runtime.signal,
|
|
1583
|
-
promptName: `${pluginName}.structured_completion`,
|
|
1584
|
-
metadata: {
|
|
1585
|
-
pluginName,
|
|
1586
|
-
pluginModelRole: "structured"
|
|
1587
|
-
}
|
|
1410
|
+
//#region src/chat/briefs/input.ts
|
|
1411
|
+
const EVENT_PAGE_SIZE = 500;
|
|
1412
|
+
async function readReportEventsThrough(executor, conversationId, throughSeq) {
|
|
1413
|
+
const pages = [];
|
|
1414
|
+
let beforeSeq = throughSeq + 1;
|
|
1415
|
+
do {
|
|
1416
|
+
const page = await readConversationEventPage(executor, {
|
|
1417
|
+
beforeSeq,
|
|
1418
|
+
canExposePayload: true,
|
|
1419
|
+
conversationId,
|
|
1420
|
+
limit: EVENT_PAGE_SIZE
|
|
1588
1421
|
});
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1422
|
+
pages.unshift(page.events);
|
|
1423
|
+
beforeSeq = page.previousSeq;
|
|
1424
|
+
} while (beforeSeq !== void 0);
|
|
1425
|
+
const bySequence = /* @__PURE__ */ new Map();
|
|
1426
|
+
for (const event of pages.flat()) bySequence.set(event.seq, event);
|
|
1427
|
+
return [...bySequence.values()].sort((left, right) => left.seq - right.seq);
|
|
1594
1428
|
}
|
|
1595
|
-
/**
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1429
|
+
/**
|
|
1430
|
+
* Build generator input from SQL through one durable event. The caller owns
|
|
1431
|
+
* the boundary, so a Turn that completes during the read cannot be covered by
|
|
1432
|
+
* a Brief that never saw it.
|
|
1433
|
+
*/
|
|
1434
|
+
async function briefInputFromSql(executor, conversationId, throughSeq) {
|
|
1435
|
+
const db = executor.db();
|
|
1436
|
+
const conversation = await createSqlStore(executor).get({ conversationId });
|
|
1437
|
+
if (!conversation) throw new Error(`Conversation ${conversationId} is unavailable`);
|
|
1438
|
+
const [events, annotations, codeChanges] = await Promise.all([
|
|
1439
|
+
readReportEventsThrough(executor, conversationId, throughSeq),
|
|
1440
|
+
listConversationAnnotations(db, conversationId),
|
|
1441
|
+
db.select({
|
|
1442
|
+
closedAt: juniorCodeChanges.closedAt,
|
|
1443
|
+
mergedAt: juniorCodeChanges.mergedAt,
|
|
1444
|
+
number: juniorCodeChanges.number,
|
|
1445
|
+
openedAt: juniorCodeChanges.openedAt,
|
|
1446
|
+
repository: juniorCodeRepositories.name,
|
|
1447
|
+
state: juniorCodeChanges.state,
|
|
1448
|
+
title: juniorCodeChanges.title,
|
|
1449
|
+
url: juniorCodeChanges.url
|
|
1450
|
+
}).from(juniorCodeChanges).innerJoin(juniorCodeRepositories, eq(juniorCodeRepositories.id, juniorCodeChanges.repositoryId)).where(and(arrayContains(juniorCodeChanges.conversationIds, [conversationId]), isNotNull(juniorCodeChanges.url)))
|
|
1451
|
+
]);
|
|
1452
|
+
const location = conversation.location;
|
|
1453
|
+
return parseBriefInput({
|
|
1454
|
+
conversationId,
|
|
1455
|
+
...conversation.title ? { title: conversation.title } : void 0,
|
|
1456
|
+
visibility: conversation.visibility ?? "private",
|
|
1457
|
+
...location ? { location: {
|
|
1458
|
+
provider: location.provider,
|
|
1459
|
+
...conversation.channelName ? { channelName: conversation.channelName } : void 0
|
|
1460
|
+
} } : void 0,
|
|
1461
|
+
entries: briefEntriesFromReportEvents(events),
|
|
1462
|
+
codeChanges: codeChanges.map((change) => ({
|
|
1463
|
+
repository: change.repository,
|
|
1464
|
+
number: change.number,
|
|
1465
|
+
...change.title ? { title: change.title } : void 0,
|
|
1466
|
+
url: change.url,
|
|
1467
|
+
state: change.state,
|
|
1468
|
+
openedAt: change.openedAt.toISOString(),
|
|
1469
|
+
...change.mergedAt ? { mergedAt: change.mergedAt.toISOString() } : void 0,
|
|
1470
|
+
...change.closedAt ? { closedAt: change.closedAt.toISOString() } : void 0
|
|
1471
|
+
})),
|
|
1472
|
+
resources: annotations.map((annotation) => ({
|
|
1473
|
+
label: annotation.label,
|
|
1474
|
+
url: annotation.url,
|
|
1475
|
+
...annotation.status ? { status: annotation.status } : void 0
|
|
1476
|
+
}))
|
|
1477
|
+
});
|
|
1608
1478
|
}
|
|
1609
1479
|
//#endregion
|
|
1610
|
-
//#region src/chat/
|
|
1611
|
-
const
|
|
1612
|
-
|
|
1613
|
-
|
|
1480
|
+
//#region src/chat/briefs/operational-report.ts
|
|
1481
|
+
const DAY_MS$1 = 864e5;
|
|
1482
|
+
const WINDOWS = [
|
|
1483
|
+
7,
|
|
1484
|
+
30,
|
|
1485
|
+
90
|
|
1486
|
+
];
|
|
1487
|
+
function startOfUtcDay$1(value) {
|
|
1488
|
+
const date = new Date(value);
|
|
1489
|
+
date.setUTCHours(0, 0, 0, 0);
|
|
1490
|
+
return date;
|
|
1614
1491
|
}
|
|
1615
|
-
function
|
|
1616
|
-
|
|
1617
|
-
if (key === pluginPrefix || key.startsWith(`${pluginPrefix}:`)) return key;
|
|
1618
|
-
return `junior:plugin_state:${hashKeyPart(plugin)}:${hashKeyPart(key)}`;
|
|
1492
|
+
function formatCount(value) {
|
|
1493
|
+
return new Intl.NumberFormat("en-US").format(value);
|
|
1619
1494
|
}
|
|
1620
|
-
function
|
|
1621
|
-
|
|
1622
|
-
|
|
1495
|
+
function formatUsd(value) {
|
|
1496
|
+
const maximumFractionDigits = value > 0 && value < .01 ? 4 : 2;
|
|
1497
|
+
return new Intl.NumberFormat("en-US", {
|
|
1498
|
+
currency: "USD",
|
|
1499
|
+
maximumFractionDigits,
|
|
1500
|
+
minimumFractionDigits: 2,
|
|
1501
|
+
style: "currency"
|
|
1502
|
+
}).format(value);
|
|
1623
1503
|
}
|
|
1624
|
-
/**
|
|
1625
|
-
|
|
1626
|
-
|
|
1504
|
+
/**
|
|
1505
|
+
* Build Brief storage and model-cost diagnostics for the System page. Cost
|
|
1506
|
+
* comes from retained Conversation events, so spend on purged private
|
|
1507
|
+
* Conversations leaves the report with them; the labels say so.
|
|
1508
|
+
*/
|
|
1509
|
+
async function buildBriefsOperationalReport(args) {
|
|
1510
|
+
const newestDay = startOfUtcDay$1(args.nowMs);
|
|
1511
|
+
const windowStart = /* @__PURE__ */ new Date(newestDay.getTime() - 29 * DAY_MS$1);
|
|
1512
|
+
const windowEnd = new Date(newestDay.getTime() + DAY_MS$1);
|
|
1513
|
+
const [[counts], recentCounts] = await Promise.all([args.db.select({
|
|
1514
|
+
conversations: sql`count(distinct ${juniorConversationBriefs.conversationId})`.mapWith(Number),
|
|
1515
|
+
stored: sql`count(*)`.mapWith(Number)
|
|
1516
|
+
}).from(juniorConversationBriefs), args.db.select({ stored: sql`count(*)`.mapWith(Number) }).from(juniorConversationBriefs).where(and(gte(juniorConversationBriefs.createdAt, windowStart), lt(juniorConversationBriefs.createdAt, windowEnd)))]);
|
|
1517
|
+
const recentDays = args.briefDays.filter((day) => {
|
|
1518
|
+
const dayStart = Date.parse(`${day.date}T00:00:00.000Z`);
|
|
1519
|
+
return dayStart >= windowStart.getTime() && dayStart < windowEnd.getTime();
|
|
1520
|
+
});
|
|
1521
|
+
const recentCost = recentDays.reduce((total, day) => total + day.costUsd, 0);
|
|
1522
|
+
const recentBriefs = recentDays.reduce((total, day) => total + day.events, 0);
|
|
1627
1523
|
return {
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
},
|
|
1652
|
-
async withLock(key, ttlMs, callback) {
|
|
1653
|
-
validatePluginStateKey(key);
|
|
1654
|
-
const state = getAdapter();
|
|
1655
|
-
await state.connect();
|
|
1656
|
-
const lockKey = pluginStateKey(plugin, key);
|
|
1657
|
-
const lock = await state.acquireLock(lockKey, ttlMs);
|
|
1658
|
-
if (!lock) throw new Error(`Could not acquire plugin state lock for ${key}`);
|
|
1659
|
-
try {
|
|
1660
|
-
return await callback();
|
|
1661
|
-
} finally {
|
|
1662
|
-
await state.releaseLock(lock);
|
|
1524
|
+
generatedAt: new Date(args.nowMs).toISOString(),
|
|
1525
|
+
title: "Briefs",
|
|
1526
|
+
metrics: [
|
|
1527
|
+
{
|
|
1528
|
+
label: "briefs stored",
|
|
1529
|
+
tone: counts?.stored ? "good" : "neutral",
|
|
1530
|
+
value: formatCount(counts?.stored ?? 0)
|
|
1531
|
+
},
|
|
1532
|
+
{
|
|
1533
|
+
label: "conversations with a brief",
|
|
1534
|
+
value: formatCount(counts?.conversations ?? 0)
|
|
1535
|
+
},
|
|
1536
|
+
{
|
|
1537
|
+
label: "briefs · 30d",
|
|
1538
|
+
value: formatCount(recentCounts[0]?.stored ?? 0)
|
|
1539
|
+
},
|
|
1540
|
+
{
|
|
1541
|
+
label: "cost · 30d · retained",
|
|
1542
|
+
value: formatUsd(recentCost)
|
|
1543
|
+
},
|
|
1544
|
+
{
|
|
1545
|
+
label: "average cost per brief · 30d · retained",
|
|
1546
|
+
value: formatUsd(recentBriefs === 0 ? 0 : recentCost / recentBriefs)
|
|
1663
1547
|
}
|
|
1664
|
-
|
|
1548
|
+
],
|
|
1549
|
+
widgets: [{
|
|
1550
|
+
categories: args.briefDays.map((day) => ({
|
|
1551
|
+
id: day.date,
|
|
1552
|
+
label: day.date,
|
|
1553
|
+
values: {
|
|
1554
|
+
briefs: day.events,
|
|
1555
|
+
costUsd: day.costUsd
|
|
1556
|
+
}
|
|
1557
|
+
})),
|
|
1558
|
+
description: "Stored Brief versions and estimated model cost",
|
|
1559
|
+
id: "briefs-created",
|
|
1560
|
+
series: [{
|
|
1561
|
+
key: "briefs",
|
|
1562
|
+
label: "Briefs"
|
|
1563
|
+
}, {
|
|
1564
|
+
format: "usd",
|
|
1565
|
+
key: "costUsd",
|
|
1566
|
+
label: "Cost"
|
|
1567
|
+
}],
|
|
1568
|
+
timeRangeDays: [...WINDOWS],
|
|
1569
|
+
title: "Briefs created",
|
|
1570
|
+
type: "bar_chart"
|
|
1571
|
+
}]
|
|
1665
1572
|
};
|
|
1666
1573
|
}
|
|
1667
1574
|
//#endregion
|
|
1668
|
-
//#region src/chat/
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1575
|
+
//#region src/chat/briefs/store.ts
|
|
1576
|
+
function briefVersionFromRow(row) {
|
|
1577
|
+
return {
|
|
1578
|
+
conversationId: row.conversationId,
|
|
1579
|
+
version: row.version,
|
|
1580
|
+
turnId: row.turnId,
|
|
1581
|
+
throughSeq: row.throughSeq,
|
|
1582
|
+
content: conversationBriefSchema.parse(row.content),
|
|
1583
|
+
searchText: row.searchText,
|
|
1584
|
+
modelId: row.modelId,
|
|
1585
|
+
...row.costUsd !== null ? { costUsd: row.costUsd } : void 0,
|
|
1586
|
+
createdAt: row.createdAt
|
|
1587
|
+
};
|
|
1677
1588
|
}
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
return
|
|
1589
|
+
/** Append the next Brief version, or return the row for an existing Turn. */
|
|
1590
|
+
async function appendConversationBrief(db, input) {
|
|
1591
|
+
return await db.transaction(async (tx) => {
|
|
1592
|
+
const conversation = (await tx.select({
|
|
1593
|
+
conversationId: juniorConversations.conversationId,
|
|
1594
|
+
transcriptPurgedAt: juniorConversations.transcriptPurgedAt
|
|
1595
|
+
}).from(juniorConversations).where(eq(juniorConversations.conversationId, input.conversationId)).for("update"))[0];
|
|
1596
|
+
if (!conversation) throw new Error(`Conversation ${input.conversationId} is unavailable`);
|
|
1597
|
+
if (conversation.transcriptPurgedAt) {
|
|
1598
|
+
if ((await resolveRootVisibility({ db: () => tx }, input.conversationId)).visibility !== "public") throw new Error(`Cannot append a Brief to purged non-public Conversation ${input.conversationId}`);
|
|
1599
|
+
}
|
|
1600
|
+
const existing = await tx.select().from(juniorConversationBriefs).where(and(eq(juniorConversationBriefs.conversationId, input.conversationId), eq(juniorConversationBriefs.turnId, input.turnId))).limit(1);
|
|
1601
|
+
if (existing[0]) return {
|
|
1602
|
+
inserted: false,
|
|
1603
|
+
value: briefVersionFromRow(existing[0])
|
|
1604
|
+
};
|
|
1605
|
+
const version = ((await tx.select({ version: max(juniorConversationBriefs.version) }).from(juniorConversationBriefs).where(eq(juniorConversationBriefs.conversationId, input.conversationId)))[0]?.version ?? 0) + 1;
|
|
1606
|
+
const row = (await tx.insert(juniorConversationBriefs).values({
|
|
1607
|
+
conversationId: input.conversationId,
|
|
1608
|
+
version,
|
|
1609
|
+
turnId: input.turnId,
|
|
1610
|
+
throughSeq: input.throughSeq,
|
|
1611
|
+
content: conversationBriefSchema.parse(input.content),
|
|
1612
|
+
searchText: input.searchText,
|
|
1613
|
+
modelId: input.modelId,
|
|
1614
|
+
costUsd: input.costUsd ?? null
|
|
1615
|
+
}).onConflictDoNothing({ target: [juniorConversationBriefs.conversationId, juniorConversationBriefs.turnId] }).returning())[0];
|
|
1616
|
+
if (!row) throw new Error("Brief insert conflicted after the Turn check");
|
|
1617
|
+
return {
|
|
1618
|
+
inserted: true,
|
|
1619
|
+
value: briefVersionFromRow(row)
|
|
1620
|
+
};
|
|
1621
|
+
});
|
|
1681
1622
|
}
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
if (vercelProd) return stripTrailingSlashes(withHttps(vercelProd));
|
|
1687
|
-
const vercelUrl = process.env.VERCEL_URL?.trim();
|
|
1688
|
-
if (vercelUrl) return stripTrailingSlashes(withHttps(vercelUrl));
|
|
1689
|
-
return "http://localhost:3000";
|
|
1623
|
+
/** Read the Brief version produced for one completed Turn. */
|
|
1624
|
+
async function readConversationBriefForTurn(db, conversationId, turnId) {
|
|
1625
|
+
const rows = await db.select().from(juniorConversationBriefs).where(and(eq(juniorConversationBriefs.conversationId, conversationId), eq(juniorConversationBriefs.turnId, turnId))).limit(1);
|
|
1626
|
+
return rows[0] ? briefVersionFromRow(rows[0]) : void 0;
|
|
1690
1627
|
}
|
|
1691
|
-
/**
|
|
1692
|
-
function
|
|
1693
|
-
const
|
|
1694
|
-
|
|
1695
|
-
return previous;
|
|
1628
|
+
/** Read the latest Brief version for one Conversation. */
|
|
1629
|
+
async function readLatestConversationBrief(db, conversationId) {
|
|
1630
|
+
const rows = await db.select().from(juniorConversationBriefs).where(eq(juniorConversationBriefs.conversationId, conversationId)).orderBy(desc(juniorConversationBriefs.version)).limit(1);
|
|
1631
|
+
return rows[0] ? briefVersionFromRow(rows[0]) : void 0;
|
|
1696
1632
|
}
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
return
|
|
1633
|
+
//#endregion
|
|
1634
|
+
//#region src/chat/briefs/task.ts
|
|
1635
|
+
const BRIEF_LOCK_TTL_MS = 6e5;
|
|
1636
|
+
function hasUserInstruction(run) {
|
|
1637
|
+
return run.transcript.some((entry) => entry.type === "message" && entry.role === "user" && entry.provenance?.authority === "instruction");
|
|
1702
1638
|
}
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1639
|
+
function logSkip(context, conversationId, reason) {
|
|
1640
|
+
context.log.info("Brief update skipped", {
|
|
1641
|
+
conversationId,
|
|
1642
|
+
reason
|
|
1643
|
+
});
|
|
1706
1644
|
}
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1645
|
+
async function readTurnCompletedSeq(db, conversationId, turnId) {
|
|
1646
|
+
const seq = (await db.select({ seq: juniorConversationEvents.seq }).from(juniorConversationEvents).where(and(eq(juniorConversationEvents.conversationId, conversationId), eq(juniorConversationEvents.type, "turn_completed"), sql`${juniorConversationEvents.payload}->>'turnId' = ${turnId}`)).orderBy(desc(juniorConversationEvents.seq)).limit(1))[0]?.seq;
|
|
1647
|
+
if (seq === void 0) throw new Error(`Completed Turn ${turnId} has no terminal event in Conversation ${conversationId}`);
|
|
1648
|
+
return seq;
|
|
1710
1649
|
}
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1650
|
+
async function emitBriefUpdated(context, stored) {
|
|
1651
|
+
await context.events.emit(briefUpdatedEvent({
|
|
1652
|
+
version: stored.version,
|
|
1653
|
+
modelId: stored.modelId,
|
|
1654
|
+
...stored.costUsd !== void 0 ? { costUsd: stored.costUsd } : void 0,
|
|
1655
|
+
decisions: stored.content.decisions.length,
|
|
1656
|
+
openDecisions: stored.content.openDecisions.length,
|
|
1657
|
+
links: stored.content.links.length
|
|
1658
|
+
}));
|
|
1717
1659
|
}
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
const DUE_KEY = "junior:watches:due";
|
|
1725
|
-
const CLAIM_MS = 12e4;
|
|
1726
|
-
const BATCH_SIZE = 25;
|
|
1727
|
-
const entriesSchema = z.array(z.object({
|
|
1728
|
-
id: z.string(),
|
|
1729
|
-
atMs: z.number()
|
|
1730
|
-
}));
|
|
1731
|
-
/** Keep timer index operations atomic in Redis and the local memory adapter. */
|
|
1732
|
-
async function updateTimerIndex(state, operation) {
|
|
1733
|
-
const redis = await getDefaultRedisStateAdapterFor(state);
|
|
1734
|
-
if (redis) {
|
|
1735
|
-
const prefix = getChatConfig().state.keyPrefix;
|
|
1736
|
-
const key = prefix ? `${prefix}:${DUE_KEY}` : DUE_KEY;
|
|
1737
|
-
const client = redis.getClient();
|
|
1738
|
-
if (operation.kind === "add") {
|
|
1739
|
-
await client.sendCommand([
|
|
1740
|
-
"ZADD",
|
|
1741
|
-
key,
|
|
1742
|
-
String(operation.atMs),
|
|
1743
|
-
operation.id
|
|
1744
|
-
]);
|
|
1745
|
-
return [];
|
|
1746
|
-
}
|
|
1747
|
-
if (operation.kind === "remove") {
|
|
1748
|
-
await client.sendCommand([
|
|
1749
|
-
"ZREM",
|
|
1750
|
-
key,
|
|
1751
|
-
operation.id
|
|
1752
|
-
]);
|
|
1753
|
-
return [];
|
|
1754
|
-
}
|
|
1755
|
-
return await client.sendCommand([
|
|
1756
|
-
"EVAL",
|
|
1757
|
-
`local ids = redis.call('ZRANGEBYSCORE', KEYS[1], '-inf', ARGV[1], 'LIMIT', 0, ARGV[3])
|
|
1758
|
-
for _, id in ipairs(ids) do
|
|
1759
|
-
redis.call('ZADD', KEYS[1], ARGV[2], id)
|
|
1760
|
-
end
|
|
1761
|
-
return ids`,
|
|
1762
|
-
"1",
|
|
1763
|
-
key,
|
|
1764
|
-
String(operation.nowMs),
|
|
1765
|
-
String(operation.nowMs + CLAIM_MS),
|
|
1766
|
-
String(BATCH_SIZE)
|
|
1767
|
-
]);
|
|
1660
|
+
/** Generate and store the next Brief after one completed Turn. */
|
|
1661
|
+
async function updateConversationBrief(context) {
|
|
1662
|
+
const run = await context.run.load();
|
|
1663
|
+
if (run.source.kind !== "slack" && run.source.kind !== "web" && run.source.kind !== "local") {
|
|
1664
|
+
logSkip(context, run.conversationId, "unsupported_source");
|
|
1665
|
+
return;
|
|
1768
1666
|
}
|
|
1769
|
-
const
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
atMs: operation.nowMs + CLAIM_MS
|
|
1780
|
-
} : entry);
|
|
1781
|
-
} else {
|
|
1782
|
-
entries = entries.filter((entry) => entry.id !== operation.id);
|
|
1783
|
-
if (operation.kind === "add") entries.push({
|
|
1784
|
-
id: operation.id,
|
|
1785
|
-
atMs: operation.atMs
|
|
1786
|
-
});
|
|
1667
|
+
const executor = getSqlExecutor();
|
|
1668
|
+
const conversation = await createSqlStore(executor).get({ conversationId: run.conversationId });
|
|
1669
|
+
if (conversation?.parentConversationId) {
|
|
1670
|
+
logSkip(context, run.conversationId, "child_conversation");
|
|
1671
|
+
return;
|
|
1672
|
+
}
|
|
1673
|
+
if (conversation?.transcriptPurgedAtMs !== void 0) {
|
|
1674
|
+
if ((await resolveRootVisibility(executor, run.conversationId)).visibility !== "public") {
|
|
1675
|
+
logSkip(context, run.conversationId, "purged_private");
|
|
1676
|
+
return;
|
|
1787
1677
|
}
|
|
1788
|
-
await state.set(DUE_KEY, entries);
|
|
1789
|
-
return ids;
|
|
1790
|
-
} finally {
|
|
1791
|
-
await state.releaseLock(lock);
|
|
1792
1678
|
}
|
|
1679
|
+
if (!hasUserInstruction(run)) {
|
|
1680
|
+
logSkip(context, run.conversationId, "no_user_instruction");
|
|
1681
|
+
return;
|
|
1682
|
+
}
|
|
1683
|
+
await context.state.withLock(`brief:${run.conversationId}`, BRIEF_LOCK_TTL_MS, async () => {
|
|
1684
|
+
const db = context.db;
|
|
1685
|
+
const existing = await readConversationBriefForTurn(db, run.conversationId, run.runId);
|
|
1686
|
+
if (existing) {
|
|
1687
|
+
await emitBriefUpdated(context, existing);
|
|
1688
|
+
logSkip(context, run.conversationId, "turn_already_stored");
|
|
1689
|
+
return;
|
|
1690
|
+
}
|
|
1691
|
+
const terminalSeq = await readTurnCompletedSeq(db, run.conversationId, run.runId);
|
|
1692
|
+
const previous = await readLatestConversationBrief(db, run.conversationId);
|
|
1693
|
+
if (previous && previous.throughSeq >= terminalSeq) {
|
|
1694
|
+
logSkip(context, run.conversationId, "turn_already_covered");
|
|
1695
|
+
return;
|
|
1696
|
+
}
|
|
1697
|
+
const input = await briefInputFromSql(executor, run.conversationId, terminalSeq);
|
|
1698
|
+
const modelId = defaultModelId(botConfig);
|
|
1699
|
+
const generation = await generateBrief({
|
|
1700
|
+
completeObject: (request) => completeObject({
|
|
1701
|
+
...request,
|
|
1702
|
+
modelId,
|
|
1703
|
+
promptName: "junior.brief_update"
|
|
1704
|
+
}),
|
|
1705
|
+
input,
|
|
1706
|
+
previous: previous?.content,
|
|
1707
|
+
prompt: BRIEF_PROMPT,
|
|
1708
|
+
throughIndex: terminalSeq
|
|
1709
|
+
});
|
|
1710
|
+
const stored = await appendConversationBrief(db, {
|
|
1711
|
+
conversationId: run.conversationId,
|
|
1712
|
+
turnId: run.runId,
|
|
1713
|
+
throughSeq: terminalSeq,
|
|
1714
|
+
content: generation.brief,
|
|
1715
|
+
searchText: generation.searchText,
|
|
1716
|
+
modelId,
|
|
1717
|
+
...generation.costUsd !== void 0 ? { costUsd: generation.costUsd } : void 0
|
|
1718
|
+
});
|
|
1719
|
+
await emitBriefUpdated(context, stored.value);
|
|
1720
|
+
if (!stored.inserted) logSkip(context, run.conversationId, "turn_already_stored");
|
|
1721
|
+
});
|
|
1793
1722
|
}
|
|
1723
|
+
/** Core task registration kept outside the installed plugin catalog. */
|
|
1724
|
+
const briefsTaskRegistration = {
|
|
1725
|
+
manifest: {
|
|
1726
|
+
name: "briefs",
|
|
1727
|
+
displayName: "Briefs",
|
|
1728
|
+
description: "Durable Conversation Brief generation"
|
|
1729
|
+
},
|
|
1730
|
+
conversationEvents: [briefUpdatedEvent],
|
|
1731
|
+
hooks: { async operationalReport(context) {
|
|
1732
|
+
return await buildBriefsOperationalReport({
|
|
1733
|
+
briefDays: await context.eventStats.costsByDay({
|
|
1734
|
+
days: 90,
|
|
1735
|
+
eventName: "brief_updated"
|
|
1736
|
+
}),
|
|
1737
|
+
db: context.db,
|
|
1738
|
+
nowMs: context.nowMs
|
|
1739
|
+
});
|
|
1740
|
+
} },
|
|
1741
|
+
tasks: { updateBrief: { async run(context) {
|
|
1742
|
+
await updateConversationBrief(context);
|
|
1743
|
+
} } }
|
|
1744
|
+
};
|
|
1794
1745
|
//#endregion
|
|
1795
|
-
//#region src/chat/
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
function runWithWorkspaceTeamId(teamId, fn) {
|
|
1799
|
-
if (!teamId) return fn();
|
|
1800
|
-
return workspaceTeamIdStorage.run(teamId, fn);
|
|
1746
|
+
//#region src/chat/plugins/conversation-events.ts
|
|
1747
|
+
function registeredDefinition(plugin, value) {
|
|
1748
|
+
return plugin.conversationEvents?.find((definition) => definition === value.definition);
|
|
1801
1749
|
}
|
|
1802
|
-
/**
|
|
1803
|
-
function
|
|
1804
|
-
|
|
1750
|
+
/** Keep operation idempotency stable when a registered event schema advances. */
|
|
1751
|
+
function eventIdentityVersion(plugin, definition) {
|
|
1752
|
+
let version = definition.version;
|
|
1753
|
+
for (const candidate of plugin.conversationEvents ?? []) if (candidate.eventName === definition.eventName) version = Math.min(version, candidate.version);
|
|
1754
|
+
return version;
|
|
1805
1755
|
}
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
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
|
-
}
|
|
1756
|
+
/** Create a conversation-bound writer for one plugin operation. */
|
|
1757
|
+
function createPluginConversationEvents(args) {
|
|
1758
|
+
return { async emit(value) {
|
|
1759
|
+
const definition = registeredDefinition(args.plugin, value);
|
|
1760
|
+
if (!definition) throw new Error(`Plugin "${args.plugin.manifest.name}" cannot emit an unregistered conversation event.`);
|
|
1761
|
+
const content = definition.parse(value.data);
|
|
1762
|
+
const identityVersion = eventIdentityVersion(args.plugin, definition);
|
|
1763
|
+
await getConversationEventStore().append(args.conversationId, [{
|
|
1764
|
+
createdAtMs: Date.now(),
|
|
1765
|
+
idempotencyKey: `plugin:${args.plugin.manifest.name}:operation:${args.operationId}:event:${definition.eventName}@${identityVersion}`,
|
|
1766
|
+
data: {
|
|
1767
|
+
type: "structured_event",
|
|
1768
|
+
namespace: args.plugin.manifest.name,
|
|
1769
|
+
name: definition.eventName,
|
|
1770
|
+
version: definition.version,
|
|
1771
|
+
turnId: args.turnId,
|
|
1772
|
+
content
|
|
1773
|
+
}
|
|
1774
|
+
}], { activity: "preserve" });
|
|
1775
|
+
} };
|
|
1846
1776
|
}
|
|
1847
|
-
/**
|
|
1848
|
-
function
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
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
|
-
}
|
|
1859
|
-
}
|
|
1777
|
+
/** Render a stored plugin event through its currently registered definition. */
|
|
1778
|
+
function renderPluginConversationEvent(args) {
|
|
1779
|
+
const definition = [briefsTaskRegistration, ...getPlugins()].find((candidate) => candidate.manifest.name === args.namespace)?.conversationEvents?.find((candidate) => candidate.eventName === args.name && candidate.version === args.version);
|
|
1780
|
+
if (!definition) return void 0;
|
|
1781
|
+
return definition.renderEvent(definition.parse(args.content));
|
|
1860
1782
|
}
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
const
|
|
1866
|
-
|
|
1867
|
-
if (lineMatch) {
|
|
1868
|
-
const line = Number.parseInt(lineMatch[1] ?? "", 10);
|
|
1869
|
-
if (Number.isFinite(line)) parsed.detailLine = line;
|
|
1870
|
-
}
|
|
1871
|
-
if (/unsupported heading depth/i.test(trimmed)) parsed.detailRule = "unsupported_heading_depth";
|
|
1872
|
-
return parsed;
|
|
1783
|
+
//#endregion
|
|
1784
|
+
//#region src/api/conversations/membership.ts
|
|
1785
|
+
/** True when the viewer is a materialized participant on this root conversation. */
|
|
1786
|
+
function conversationHasParticipantUser(userId) {
|
|
1787
|
+
const participant = getDb().select({ rootConversationId: juniorConversationParticipants.rootConversationId }).from(juniorConversationParticipants).where(and(eq(juniorConversationParticipants.rootConversationId, juniorConversations.conversationId), eq(juniorConversationParticipants.userId, userId)));
|
|
1788
|
+
return exists(participant);
|
|
1873
1789
|
}
|
|
1874
1790
|
/**
|
|
1875
|
-
*
|
|
1876
|
-
*
|
|
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.
|
|
1791
|
+
* True when a verified primary-email user participates on this root
|
|
1792
|
+
* conversation. Used only for the actorEmail feed fallback.
|
|
1885
1793
|
*/
|
|
1886
|
-
function
|
|
1887
|
-
const
|
|
1888
|
-
|
|
1889
|
-
return installationTokenStorage.run({ token: trimmed }, fn);
|
|
1794
|
+
function conversationHasParticipantEmail(email) {
|
|
1795
|
+
const participant = getDb().select({ rootConversationId: juniorConversationParticipants.rootConversationId }).from(juniorConversationParticipants).innerJoin(juniorUsers, eq(juniorUsers.id, juniorConversationParticipants.userId)).where(and(eq(juniorConversationParticipants.rootConversationId, juniorConversations.conversationId), eq(juniorUsers.primaryEmailNormalized, email)));
|
|
1796
|
+
return exists(participant);
|
|
1890
1797
|
}
|
|
1891
|
-
/**
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
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");
|
|
1798
|
+
/** True when one user's personal feed has archived this root after the cutoff. */
|
|
1799
|
+
function conversationArchivedForUser(userId, archivedAfter) {
|
|
1800
|
+
const archive = getDb().select({ rootConversationId: juniorConversationParticipants.rootConversationId }).from(juniorConversationParticipants).where(and(eq(juniorConversationParticipants.rootConversationId, juniorConversations.conversationId), eq(juniorConversationParticipants.userId, userId), isNotNull(juniorConversationParticipants.archivedAt), archivedAfter ? gte(juniorConversationParticipants.archivedAt, archivedAfter) : void 0));
|
|
1801
|
+
return exists(archive);
|
|
1904
1802
|
}
|
|
1905
|
-
/**
|
|
1906
|
-
function
|
|
1907
|
-
|
|
1803
|
+
/** True when one user's personal feed has not archived this root. */
|
|
1804
|
+
function conversationNotArchivedForUser(userId) {
|
|
1805
|
+
const archive = getDb().select({ rootConversationId: juniorConversationParticipants.rootConversationId }).from(juniorConversationParticipants).where(and(eq(juniorConversationParticipants.rootConversationId, juniorConversations.conversationId), eq(juniorConversationParticipants.userId, userId), isNotNull(juniorConversationParticipants.archivedAt)));
|
|
1806
|
+
return notExists(archive);
|
|
1908
1807
|
}
|
|
1909
|
-
/**
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1808
|
+
/** True when one primary-email user's feed archived this root after the cutoff. */
|
|
1809
|
+
function conversationArchivedForEmail(email, archivedAfter) {
|
|
1810
|
+
const archive = getDb().select({ rootConversationId: juniorConversationParticipants.rootConversationId }).from(juniorConversationParticipants).innerJoin(juniorUsers, eq(juniorUsers.id, juniorConversationParticipants.userId)).where(and(eq(juniorConversationParticipants.rootConversationId, juniorConversations.conversationId), eq(juniorUsers.primaryEmailNormalized, email), isNotNull(juniorConversationParticipants.archivedAt), archivedAfter ? gte(juniorConversationParticipants.archivedAt, archivedAfter) : void 0));
|
|
1811
|
+
return exists(archive);
|
|
1812
|
+
}
|
|
1813
|
+
/** True when one primary-email user's personal feed has not archived this root. */
|
|
1814
|
+
function conversationNotArchivedForEmail(email) {
|
|
1815
|
+
const archive = getDb().select({ rootConversationId: juniorConversationParticipants.rootConversationId }).from(juniorConversationParticipants).innerJoin(juniorUsers, eq(juniorUsers.id, juniorConversationParticipants.userId)).where(and(eq(juniorConversationParticipants.rootConversationId, juniorConversations.conversationId), eq(juniorUsers.primaryEmailNormalized, email), isNotNull(juniorConversationParticipants.archivedAt)));
|
|
1816
|
+
return notExists(archive);
|
|
1817
|
+
}
|
|
1818
|
+
/** Root-actor ownership or a materialized participant row for the same person. */
|
|
1819
|
+
function viewerConversationMembership(args) {
|
|
1820
|
+
if (args.actorMatch && args.participantMatch) return or(args.actorMatch, args.participantMatch);
|
|
1821
|
+
return args.actorMatch ?? args.participantMatch;
|
|
1822
|
+
}
|
|
1823
|
+
/** Read personal archive timestamps for a bounded set of conversation roots. */
|
|
1824
|
+
async function conversationArchiveTimes(db, conversationIds, subject) {
|
|
1825
|
+
if (!subject || conversationIds.length === 0) return /* @__PURE__ */ new Map();
|
|
1826
|
+
const query = db.select({
|
|
1827
|
+
archivedAt: juniorConversationParticipants.archivedAt,
|
|
1828
|
+
rootConversationId: juniorConversationParticipants.rootConversationId
|
|
1829
|
+
}).from(juniorConversationParticipants);
|
|
1830
|
+
const rows = "userId" in subject ? await query.where(and(eq(juniorConversationParticipants.userId, subject.userId), inArray(juniorConversationParticipants.rootConversationId, [...conversationIds]), isNotNull(juniorConversationParticipants.archivedAt))) : await query.innerJoin(juniorUsers, eq(juniorUsers.id, juniorConversationParticipants.userId)).where(and(eq(juniorUsers.primaryEmailNormalized, subject.email), inArray(juniorConversationParticipants.rootConversationId, [...conversationIds]), isNotNull(juniorConversationParticipants.archivedAt)));
|
|
1831
|
+
return new Map(rows.flatMap((row) => row.archivedAt ? [[row.rootConversationId, row.archivedAt.getTime()]] : []));
|
|
1832
|
+
}
|
|
1833
|
+
/** Find which of the supplied roots include the linked viewer as a participant. */
|
|
1834
|
+
async function conversationIdsWithParticipantUser(db, conversationIds, userId) {
|
|
1835
|
+
if (conversationIds.length === 0) return /* @__PURE__ */ new Set();
|
|
1836
|
+
const rows = await db.select({ rootConversationId: juniorConversationParticipants.rootConversationId }).from(juniorConversationParticipants).where(and(eq(juniorConversationParticipants.userId, userId), inArray(juniorConversationParticipants.rootConversationId, [...conversationIds])));
|
|
1837
|
+
return new Set(rows.map((row) => row.rootConversationId));
|
|
1838
|
+
}
|
|
1839
|
+
//#endregion
|
|
1840
|
+
//#region src/api/conversations/access.ts
|
|
1841
|
+
const rootConversation = alias(juniorConversations, "access_root_conversation");
|
|
1842
|
+
const rootDestination = alias(juniorDestinations, "access_root_destination");
|
|
1843
|
+
const rootIdentity = alias(juniorIdentities, "access_root_identity");
|
|
1844
|
+
/** Resolve viewer access for a bounded set of persisted conversations. */
|
|
1845
|
+
async function readConversationAccessFromSql(db, conversationIds, viewer) {
|
|
1846
|
+
if (conversationIds.length === 0) return /* @__PURE__ */ new Map();
|
|
1847
|
+
const normalizedViewerEmail = viewer?.email.trim().toLowerCase() || void 0;
|
|
1848
|
+
const rows = await db.select({
|
|
1849
|
+
conversationId: juniorConversations.conversationId,
|
|
1850
|
+
parentConversationId: juniorConversations.parentConversationId,
|
|
1851
|
+
storedRootConversationId: juniorConversations.rootConversationId,
|
|
1852
|
+
rootConversationId: rootConversation.conversationId,
|
|
1853
|
+
rootParentConversationId: rootConversation.parentConversationId,
|
|
1854
|
+
rootRootConversationId: rootConversation.rootConversationId,
|
|
1855
|
+
visibility: rootDestination.visibility,
|
|
1856
|
+
rootEmailNormalized: rootIdentity.emailNormalized,
|
|
1857
|
+
rootEmailVerified: rootIdentity.emailVerified,
|
|
1858
|
+
rootUserId: rootIdentity.userId
|
|
1859
|
+
}).from(juniorConversations).leftJoin(rootConversation, eq(rootConversation.conversationId, juniorConversations.rootConversationId)).leftJoin(rootDestination, eq(rootDestination.id, rootConversation.destinationId)).leftJoin(rootIdentity, eq(rootIdentity.id, rootConversation.actorIdentityId)).where(inArray(juniorConversations.conversationId, [...conversationIds]));
|
|
1860
|
+
const membershipConversationIds = /* @__PURE__ */ new Set();
|
|
1861
|
+
for (const row of rows) {
|
|
1862
|
+
membershipConversationIds.add(row.conversationId);
|
|
1863
|
+
if (row.storedRootConversationId) membershipConversationIds.add(row.storedRootConversationId);
|
|
1925
1864
|
}
|
|
1926
|
-
|
|
1865
|
+
const participantRootIds = viewer !== void 0 ? await conversationIdsWithParticipantUser(db, [...membershipConversationIds], viewer.id) : /* @__PURE__ */ new Set();
|
|
1866
|
+
return new Map(rows.map((row) => {
|
|
1867
|
+
const validRootConversationId = row.rootConversationId !== null && row.rootConversationId === row.storedRootConversationId && row.rootRootConversationId === row.rootConversationId && row.rootParentConversationId === null && (row.parentConversationId !== null || row.storedRootConversationId === row.conversationId) ? row.rootConversationId ?? void 0 : void 0;
|
|
1868
|
+
const visibility = validRootConversationId === void 0 ? null : row.visibility;
|
|
1869
|
+
const isRootActor = validRootConversationId !== void 0 && viewer !== void 0 && (row.rootUserId === viewer.id || normalizedViewerEmail !== void 0 && row.rootEmailVerified === true && row.rootEmailNormalized === normalizedViewerEmail);
|
|
1870
|
+
const isMaterializedParticipant = participantRootIds.has(row.conversationId) || validRootConversationId !== void 0 && participantRootIds.has(validRootConversationId);
|
|
1871
|
+
const isParticipant = isRootActor || isMaterializedParticipant;
|
|
1872
|
+
const canViewPrivateContent = isParticipant || validRootConversationId !== void 0 && canExposeConversationPayload({ visibility: visibility ?? void 0 });
|
|
1873
|
+
return [row.conversationId, {
|
|
1874
|
+
canViewPrivateContent,
|
|
1875
|
+
isParticipant,
|
|
1876
|
+
visibility
|
|
1877
|
+
}];
|
|
1878
|
+
}));
|
|
1927
1879
|
}
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
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
|
|
1944
|
-
});
|
|
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
|
|
1959
|
-
});
|
|
1960
|
-
return new SlackActionError(message, "internal_error", baseOptions);
|
|
1880
|
+
//#endregion
|
|
1881
|
+
//#region src/chat/plugins/conversation-event-reader.ts
|
|
1882
|
+
function registeredEventName$1(plugin, eventName) {
|
|
1883
|
+
if (!plugin.conversationEvents?.some((definition) => definition.eventName === eventName)) throw new TypeError(`Plugin "${plugin.manifest.name}" did not register event "${eventName}"`);
|
|
1884
|
+
return eventName;
|
|
1961
1885
|
}
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
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);
|
|
1886
|
+
/** Create Conversation event reads bound to one plugin namespace. */
|
|
1887
|
+
function createPluginConversationEventReader(plugin) {
|
|
1888
|
+
return { async list(input) {
|
|
1889
|
+
const eventName = registeredEventName$1(plugin, input.eventName);
|
|
1890
|
+
const db = getDb();
|
|
1891
|
+
if (!(await readConversationAccessFromSql(db, [input.conversationId], input.viewer)).get(input.conversationId)?.canViewPrivateContent) return void 0;
|
|
1892
|
+
return (await db.select({
|
|
1893
|
+
content: sql`${juniorConversationEvents.payload}->'content'`,
|
|
1894
|
+
createdAt: juniorConversationEvents.createdAt,
|
|
1895
|
+
version: sql`(${juniorConversationEvents.payload}->>'version')::integer`
|
|
1896
|
+
}).from(juniorConversationEvents).where(and(eq(juniorConversationEvents.conversationId, input.conversationId), eq(juniorConversationEvents.type, "structured_event"), sql`${juniorConversationEvents.payload}->>'namespace' = ${plugin.manifest.name}`, sql`${juniorConversationEvents.payload}->>'name' = ${eventName}`)).orderBy(desc(juniorConversationEvents.seq))).map((row) => ({
|
|
1897
|
+
content: row.content,
|
|
1898
|
+
createdAt: row.createdAt.toISOString(),
|
|
1899
|
+
version: row.version
|
|
1900
|
+
}));
|
|
1901
|
+
} };
|
|
1980
1902
|
}
|
|
1981
|
-
|
|
1982
|
-
|
|
1903
|
+
//#endregion
|
|
1904
|
+
//#region src/chat/plugins/conversation-event-stats.ts
|
|
1905
|
+
const DAY_MS = 864e5;
|
|
1906
|
+
const costRowSchema = z.object({
|
|
1907
|
+
costUsd: z.number().finite().nonnegative(),
|
|
1908
|
+
date: z.string().regex(/^\d{4}-\d{2}-\d{2}(T\d{2})?$/),
|
|
1909
|
+
events: z.number().int().nonnegative()
|
|
1910
|
+
}).strict();
|
|
1911
|
+
function startOfUtcDay(value) {
|
|
1912
|
+
const date = new Date(value);
|
|
1913
|
+
date.setUTCHours(0, 0, 0, 0);
|
|
1914
|
+
return date;
|
|
1983
1915
|
}
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
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";
|
|
1916
|
+
function registeredEventName(plugin, eventName) {
|
|
1917
|
+
if (!plugin.conversationEvents?.some((definition) => definition.eventName === eventName)) throw new TypeError(`Plugin "${plugin.manifest.name}" did not register event "${eventName}"`);
|
|
1918
|
+
return eventName;
|
|
1998
1919
|
}
|
|
1999
|
-
/**
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
*/
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
}
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
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 ?? {}
|
|
1920
|
+
/** Create aggregate event-cost reads bound to one plugin's registered namespace. */
|
|
1921
|
+
function createPluginConversationEventStats(plugin, now = Date.now) {
|
|
1922
|
+
return {
|
|
1923
|
+
async costsByHour(input) {
|
|
1924
|
+
const eventName = registeredEventName(plugin, input.eventName);
|
|
1925
|
+
const hourCount = input.hours ?? 24;
|
|
1926
|
+
const end = new Date(now());
|
|
1927
|
+
end.setUTCMinutes(0, 0, 0);
|
|
1928
|
+
const start = /* @__PURE__ */ new Date(end.getTime() - (hourCount - 1) * 60 * 60 * 1e3);
|
|
1929
|
+
const endExclusive = new Date(end.getTime() + 36e5);
|
|
1930
|
+
const date = sql`TO_CHAR(
|
|
1931
|
+
${juniorConversationEvents.createdAt} AT TIME ZONE 'UTC',
|
|
1932
|
+
'YYYY-MM-DD"T"HH24'
|
|
1933
|
+
)`;
|
|
1934
|
+
const cost = sql`CASE
|
|
1935
|
+
WHEN jsonb_typeof(${juniorConversationEvents.payload}->'content'->'costUsd') = 'number'
|
|
1936
|
+
THEN (${juniorConversationEvents.payload}->'content'->>'costUsd')::double precision
|
|
1937
|
+
ELSE 0
|
|
1938
|
+
END`;
|
|
1939
|
+
const rows = await getDb().select({
|
|
1940
|
+
costUsd: sql`SUM(${cost})::double precision`.mapWith(Number),
|
|
1941
|
+
date,
|
|
1942
|
+
events: sql`COUNT(*)::integer`.mapWith(Number)
|
|
1943
|
+
}).from(juniorConversationEvents).where(and(eq(juniorConversationEvents.type, "structured_event"), sql`${juniorConversationEvents.payload}->>'namespace' = ${plugin.manifest.name}`, sql`${juniorConversationEvents.payload}->>'name' = ${eventName}`, gte(juniorConversationEvents.createdAt, start), lt(juniorConversationEvents.createdAt, endExclusive))).groupBy(date);
|
|
1944
|
+
const byHour = new Map(z.array(costRowSchema).parse(rows).map((row) => [row.date, row]));
|
|
1945
|
+
return Array.from({ length: hourCount }, (_, index) => {
|
|
1946
|
+
const key = new Date(start.getTime() + index * 60 * 60 * 1e3).toISOString().slice(0, 13);
|
|
1947
|
+
return byHour.get(key) ?? {
|
|
1948
|
+
costUsd: 0,
|
|
1949
|
+
date: key,
|
|
1950
|
+
events: 0
|
|
1951
|
+
};
|
|
2041
1952
|
});
|
|
2042
|
-
}
|
|
2043
|
-
|
|
2044
|
-
const
|
|
2045
|
-
const
|
|
2046
|
-
const
|
|
2047
|
-
const
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
1953
|
+
},
|
|
1954
|
+
async costsByDay(input) {
|
|
1955
|
+
const eventName = registeredEventName(plugin, input.eventName);
|
|
1956
|
+
const end = startOfUtcDay(now());
|
|
1957
|
+
const start = /* @__PURE__ */ new Date(end.getTime() - (input.days - 1) * DAY_MS);
|
|
1958
|
+
const endExclusive = new Date(end.getTime() + DAY_MS);
|
|
1959
|
+
const date = sql`TO_CHAR(
|
|
1960
|
+
${juniorConversationEvents.createdAt} AT TIME ZONE 'UTC',
|
|
1961
|
+
'YYYY-MM-DD'
|
|
1962
|
+
)`;
|
|
1963
|
+
const cost = sql`CASE
|
|
1964
|
+
WHEN jsonb_typeof(${juniorConversationEvents.payload}->'content'->'costUsd') = 'number'
|
|
1965
|
+
THEN (${juniorConversationEvents.payload}->'content'->>'costUsd')::double precision
|
|
1966
|
+
ELSE 0
|
|
1967
|
+
END`;
|
|
1968
|
+
const rows = await getDb().select({
|
|
1969
|
+
costUsd: sql`SUM(${cost})::double precision`.mapWith(Number),
|
|
1970
|
+
date,
|
|
1971
|
+
events: sql`COUNT(*)::integer`.mapWith(Number)
|
|
1972
|
+
}).from(juniorConversationEvents).where(and(eq(juniorConversationEvents.type, "structured_event"), sql`${juniorConversationEvents.payload}->>'namespace' = ${plugin.manifest.name}`, sql`${juniorConversationEvents.payload}->>'name' = ${eventName}`, gte(juniorConversationEvents.createdAt, start), lt(juniorConversationEvents.createdAt, endExclusive))).groupBy(date);
|
|
1973
|
+
const byDate = new Map(z.array(costRowSchema).parse(rows).map((row) => [row.date, row]));
|
|
1974
|
+
return Array.from({ length: input.days }, (_, index) => {
|
|
1975
|
+
const date = new Date(start.getTime() + index * DAY_MS).toISOString().slice(0, 10);
|
|
1976
|
+
return byDate.get(date) ?? {
|
|
1977
|
+
costUsd: 0,
|
|
1978
|
+
date,
|
|
1979
|
+
events: 0
|
|
1980
|
+
};
|
|
1981
|
+
});
|
|
1982
|
+
}
|
|
1983
|
+
};
|
|
1984
|
+
}
|
|
1985
|
+
//#endregion
|
|
1986
|
+
//#region src/chat/plugins/logging.ts
|
|
1987
|
+
/** Create the host logger exposed to plugin hooks. */
|
|
1988
|
+
function createPluginLogger(plugin) {
|
|
1989
|
+
return {
|
|
1990
|
+
info(message, metadata) {
|
|
1991
|
+
logInfo("plugin.log.info", {
|
|
1992
|
+
"app.log.message": message,
|
|
1993
|
+
"app.plugin.name": plugin,
|
|
1994
|
+
...metadata
|
|
1995
|
+
});
|
|
1996
|
+
},
|
|
1997
|
+
warn(message, metadata) {
|
|
1998
|
+
logWarn("plugin.log.warn", {
|
|
1999
|
+
"app.log.message": message,
|
|
2000
|
+
"app.plugin.name": plugin,
|
|
2001
|
+
...metadata
|
|
2002
|
+
});
|
|
2003
|
+
},
|
|
2004
|
+
error(message, metadata) {
|
|
2005
|
+
logException(new Error(message), "plugin.log.error", {
|
|
2006
|
+
"app.plugin.name": plugin,
|
|
2007
|
+
...metadata
|
|
2008
|
+
});
|
|
2009
|
+
}
|
|
2010
|
+
};
|
|
2011
|
+
}
|
|
2012
|
+
//#endregion
|
|
2013
|
+
//#region src/chat/plugins/model.ts
|
|
2014
|
+
/** Create the host-owned structured model capability exposed to plugins. */
|
|
2015
|
+
function createPluginModel(pluginName, options = {}, runtime = {}) {
|
|
2016
|
+
return { async completeObject(input) {
|
|
2017
|
+
const modelId = options.structuredModelId ?? (options.structuredModel === "default" ? defaultModelId(botConfig) : botConfig.fastModelId);
|
|
2018
|
+
const result = await completeObject({
|
|
2019
|
+
modelId,
|
|
2020
|
+
schema: input.schema,
|
|
2021
|
+
prompt: input.prompt,
|
|
2022
|
+
...input.system !== void 0 ? { system: input.system } : void 0,
|
|
2023
|
+
...input.maxTokens !== void 0 ? { maxTokens: input.maxTokens } : void 0,
|
|
2024
|
+
signal: runtime.signal,
|
|
2025
|
+
promptName: `${pluginName}.structured_completion`,
|
|
2026
|
+
metadata: {
|
|
2027
|
+
pluginName,
|
|
2028
|
+
pluginModelRole: "structured"
|
|
2029
|
+
}
|
|
2030
|
+
});
|
|
2031
|
+
return {
|
|
2032
|
+
object: result.object,
|
|
2033
|
+
...result.costUsd !== void 0 ? { costUsd: result.costUsd } : void 0
|
|
2034
|
+
};
|
|
2035
|
+
} };
|
|
2036
|
+
}
|
|
2037
|
+
/** Create the host-owned embedding capability exposed to prompt hooks. */
|
|
2038
|
+
function createPluginEmbedder(pluginName, runtime = {}) {
|
|
2039
|
+
return { async embedTexts(input) {
|
|
2040
|
+
return await embedTexts({
|
|
2041
|
+
modelId: botConfig.embeddingModelId,
|
|
2042
|
+
texts: input.texts,
|
|
2043
|
+
signal: runtime.signal,
|
|
2044
|
+
metadata: {
|
|
2045
|
+
pluginName,
|
|
2046
|
+
pluginModelRole: "embedding"
|
|
2047
|
+
}
|
|
2048
|
+
});
|
|
2049
|
+
} };
|
|
2050
|
+
}
|
|
2051
|
+
//#endregion
|
|
2052
|
+
//#region src/chat/plugins/state.ts
|
|
2053
|
+
const MAX_PLUGIN_STATE_KEY_LENGTH = 512;
|
|
2054
|
+
function hashKeyPart(value) {
|
|
2055
|
+
return createHash("sha256").update(value).digest("hex").slice(0, 32);
|
|
2056
|
+
}
|
|
2057
|
+
function pluginStateKey(plugin, key) {
|
|
2058
|
+
const pluginPrefix = `junior:${plugin}`;
|
|
2059
|
+
if (key === pluginPrefix || key.startsWith(`${pluginPrefix}:`)) return key;
|
|
2060
|
+
return `junior:plugin_state:${hashKeyPart(plugin)}:${hashKeyPart(key)}`;
|
|
2061
|
+
}
|
|
2062
|
+
function validatePluginStateKey(key) {
|
|
2063
|
+
if (!key.trim()) throw new Error("Plugin state key is required");
|
|
2064
|
+
if (key.length > MAX_PLUGIN_STATE_KEY_LENGTH) throw new Error("Plugin state key exceeds the maximum length");
|
|
2065
|
+
}
|
|
2066
|
+
/** Create a durable state namespace scoped to one plugin. */
|
|
2067
|
+
function createPluginState(plugin, adapter) {
|
|
2068
|
+
const getAdapter = () => adapter ?? getStateAdapter();
|
|
2069
|
+
return {
|
|
2070
|
+
async delete(key) {
|
|
2071
|
+
validatePluginStateKey(key);
|
|
2072
|
+
const state = getAdapter();
|
|
2073
|
+
await state.connect();
|
|
2074
|
+
await state.delete(pluginStateKey(plugin, key));
|
|
2075
|
+
},
|
|
2076
|
+
async get(key) {
|
|
2077
|
+
validatePluginStateKey(key);
|
|
2078
|
+
const state = getAdapter();
|
|
2079
|
+
await state.connect();
|
|
2080
|
+
return await state.get(pluginStateKey(plugin, key)) ?? void 0;
|
|
2081
|
+
},
|
|
2082
|
+
async set(key, value, ttlMs) {
|
|
2083
|
+
validatePluginStateKey(key);
|
|
2084
|
+
const state = getAdapter();
|
|
2085
|
+
await state.connect();
|
|
2086
|
+
await state.set(pluginStateKey(plugin, key), value, ttlMs);
|
|
2087
|
+
},
|
|
2088
|
+
async setIfNotExists(key, value, ttlMs) {
|
|
2089
|
+
validatePluginStateKey(key);
|
|
2090
|
+
const state = getAdapter();
|
|
2091
|
+
await state.connect();
|
|
2092
|
+
return await state.setIfNotExists(pluginStateKey(plugin, key), value, ttlMs);
|
|
2093
|
+
},
|
|
2094
|
+
async withLock(key, ttlMs, callback) {
|
|
2095
|
+
validatePluginStateKey(key);
|
|
2096
|
+
const state = getAdapter();
|
|
2097
|
+
await state.connect();
|
|
2098
|
+
const lockKey = pluginStateKey(plugin, key);
|
|
2099
|
+
const lock = await state.acquireLock(lockKey, ttlMs);
|
|
2100
|
+
if (!lock) throw new Error(`Could not acquire plugin state lock for ${key}`);
|
|
2101
|
+
try {
|
|
2102
|
+
return await callback();
|
|
2103
|
+
} finally {
|
|
2104
|
+
await state.releaseLock(lock);
|
|
2064
2105
|
}
|
|
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
2106
|
}
|
|
2076
|
-
}
|
|
2077
|
-
throw new SlackActionError("Slack action exhausted retries", "internal_error");
|
|
2107
|
+
};
|
|
2078
2108
|
}
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
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
|
|
2091
|
-
});
|
|
2092
|
-
return getClient();
|
|
2109
|
+
//#endregion
|
|
2110
|
+
//#region src/chat/dashboard-link.ts
|
|
2111
|
+
let dashboardConversationLinkOptions;
|
|
2112
|
+
function withHttps(host) {
|
|
2113
|
+
return /^https?:\/\//.test(host) ? host : `https://${host}`;
|
|
2093
2114
|
}
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
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"));
|
|
2115
|
+
function stripTrailingSlashes(value) {
|
|
2116
|
+
let end = value.length;
|
|
2117
|
+
while (end > 1 && value.charCodeAt(end - 1) === 47) end -= 1;
|
|
2118
|
+
return end === value.length ? value : value.slice(0, end);
|
|
2104
2119
|
}
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
*/
|
|
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");
|
|
2120
|
+
function normalizeDashboardPath(path, fallback) {
|
|
2121
|
+
const value = path?.trim() || fallback;
|
|
2122
|
+
return stripTrailingSlashes(value.startsWith("/") ? value : `/${value}`);
|
|
2113
2123
|
}
|
|
2114
|
-
function
|
|
2115
|
-
const
|
|
2116
|
-
if (
|
|
2117
|
-
|
|
2124
|
+
function resolveDashboardBaseURL(config) {
|
|
2125
|
+
const explicit = config.baseURL ?? process.env.JUNIOR_BASE_URL;
|
|
2126
|
+
if (explicit?.trim()) return stripTrailingSlashes(withHttps(explicit.trim()));
|
|
2127
|
+
const vercelProd = process.env.VERCEL_PROJECT_PRODUCTION_URL?.trim();
|
|
2128
|
+
if (vercelProd) return stripTrailingSlashes(withHttps(vercelProd));
|
|
2129
|
+
const vercelUrl = process.env.VERCEL_URL?.trim();
|
|
2130
|
+
if (vercelUrl) return stripTrailingSlashes(withHttps(vercelUrl));
|
|
2131
|
+
return "http://localhost:3000";
|
|
2118
2132
|
}
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
spanAttributes: { "app.slack.file_id": fileId }
|
|
2125
|
-
})).file?.permalink;
|
|
2133
|
+
/** Configure core dashboard links. */
|
|
2134
|
+
function setDashboardConversationLinkOptions(options) {
|
|
2135
|
+
const previous = dashboardConversationLinkOptions;
|
|
2136
|
+
dashboardConversationLinkOptions = options?.disabled ? void 0 : options;
|
|
2137
|
+
return previous;
|
|
2126
2138
|
}
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2139
|
+
function resolveDashboardPath(segmentPath) {
|
|
2140
|
+
if (!dashboardConversationLinkOptions) return;
|
|
2141
|
+
const baseURL = resolveDashboardBaseURL(dashboardConversationLinkOptions);
|
|
2142
|
+
const basePath = normalizeDashboardPath(dashboardConversationLinkOptions.basePath, "/");
|
|
2143
|
+
return `${baseURL}${basePath === "/" ? segmentPath : `${basePath}${segmentPath}`}`;
|
|
2144
|
+
}
|
|
2145
|
+
/** Build the dashboard conversation URL when the core dashboard is enabled. */
|
|
2146
|
+
function getDashboardConversationLink(conversationId) {
|
|
2147
|
+
return resolveDashboardPath(`/conversations/${encodeURIComponent(conversationId)}`);
|
|
2148
|
+
}
|
|
2149
|
+
/** Build the dashboard task detail URL when the core dashboard is enabled. */
|
|
2150
|
+
function getDashboardTaskLink(taskId) {
|
|
2151
|
+
return resolveDashboardPath(`/automations/${encodeURIComponent(taskId)}`);
|
|
2152
|
+
}
|
|
2153
|
+
/** Public icon asset URL. Headless and local installs keep text-only identity. */
|
|
2154
|
+
function getDashboardObjectIconLink(icon) {
|
|
2155
|
+
if (!dashboardConversationLinkOptions) return void 0;
|
|
2156
|
+
const url = new URL(objectIconPath(icon), resolveDashboardBaseURL(dashboardConversationLinkOptions));
|
|
2157
|
+
if (url.protocol !== "https:" || url.hostname === "localhost") return void 0;
|
|
2158
|
+
return url.href;
|
|
2159
|
+
}
|
|
2160
|
+
//#endregion
|
|
2161
|
+
//#region src/chat/state/ttl.ts
|
|
2162
|
+
/** Retain expiring conversation/runtime state long enough for resume and dashboard history. */
|
|
2163
|
+
const JUNIOR_THREAD_STATE_TTL_MS = 6048e5;
|
|
2164
|
+
//#endregion
|
|
2165
|
+
//#region src/chat/events/timer-index.ts
|
|
2166
|
+
const DUE_KEY = "junior:watches:due";
|
|
2167
|
+
const CLAIM_MS = 12e4;
|
|
2168
|
+
const BATCH_SIZE = 25;
|
|
2169
|
+
const entriesSchema = z.array(z.object({
|
|
2170
|
+
id: z.string(),
|
|
2171
|
+
atMs: z.number()
|
|
2172
|
+
}));
|
|
2173
|
+
/** Keep timer index operations atomic in Redis and the local memory adapter. */
|
|
2174
|
+
async function updateTimerIndex(state, operation) {
|
|
2175
|
+
const redis = await getDefaultRedisStateAdapterFor(state);
|
|
2176
|
+
if (redis) {
|
|
2177
|
+
const prefix = getChatConfig().state.keyPrefix;
|
|
2178
|
+
const key = prefix ? `${prefix}:${DUE_KEY}` : DUE_KEY;
|
|
2179
|
+
const client = redis.getClient();
|
|
2180
|
+
if (operation.kind === "add") {
|
|
2181
|
+
await client.sendCommand([
|
|
2182
|
+
"ZADD",
|
|
2183
|
+
key,
|
|
2184
|
+
String(operation.atMs),
|
|
2185
|
+
operation.id
|
|
2186
|
+
]);
|
|
2187
|
+
return [];
|
|
2136
2188
|
}
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2189
|
+
if (operation.kind === "remove") {
|
|
2190
|
+
await client.sendCommand([
|
|
2191
|
+
"ZREM",
|
|
2192
|
+
key,
|
|
2193
|
+
operation.id
|
|
2194
|
+
]);
|
|
2195
|
+
return [];
|
|
2196
|
+
}
|
|
2197
|
+
return await client.sendCommand([
|
|
2198
|
+
"EVAL",
|
|
2199
|
+
`local ids = redis.call('ZRANGEBYSCORE', KEYS[1], '-inf', ARGV[1], 'LIMIT', 0, ARGV[3])
|
|
2200
|
+
for _, id in ipairs(ids) do
|
|
2201
|
+
redis.call('ZADD', KEYS[1], ARGV[2], id)
|
|
2202
|
+
end
|
|
2203
|
+
return ids`,
|
|
2204
|
+
"1",
|
|
2205
|
+
key,
|
|
2206
|
+
String(operation.nowMs),
|
|
2207
|
+
String(operation.nowMs + CLAIM_MS),
|
|
2208
|
+
String(BATCH_SIZE)
|
|
2209
|
+
]);
|
|
2210
|
+
}
|
|
2211
|
+
const lock = await state.acquireLock(`${DUE_KEY}:lock`, 1e4);
|
|
2212
|
+
if (!lock) throw new Error("Could not acquire timer index lock");
|
|
2213
|
+
try {
|
|
2214
|
+
let entries = entriesSchema.parse(await state.get(DUE_KEY) ?? []);
|
|
2215
|
+
let ids = [];
|
|
2216
|
+
if (operation.kind === "claim") {
|
|
2217
|
+
ids = entries.filter((entry) => entry.atMs <= operation.nowMs).sort((a, b) => a.atMs - b.atMs).slice(0, BATCH_SIZE).map((entry) => entry.id);
|
|
2218
|
+
const claimed = new Set(ids);
|
|
2219
|
+
entries = entries.map((entry) => claimed.has(entry.id) ? {
|
|
2220
|
+
...entry,
|
|
2221
|
+
atMs: operation.nowMs + CLAIM_MS
|
|
2222
|
+
} : entry);
|
|
2223
|
+
} else {
|
|
2224
|
+
entries = entries.filter((entry) => entry.id !== operation.id);
|
|
2225
|
+
if (operation.kind === "add") entries.push({
|
|
2226
|
+
id: operation.id,
|
|
2227
|
+
atMs: operation.atMs
|
|
2228
|
+
});
|
|
2229
|
+
}
|
|
2230
|
+
await state.set(DUE_KEY, entries);
|
|
2231
|
+
return ids;
|
|
2232
|
+
} finally {
|
|
2233
|
+
await state.releaseLock(lock);
|
|
2234
|
+
}
|
|
2144
2235
|
}
|
|
2145
2236
|
//#endregion
|
|
2146
2237
|
//#region src/chat/credentials/subject.ts
|
|
@@ -3639,7 +3730,22 @@ function getPluginTools(context, sandbox = createSandboxCapability(context.works
|
|
|
3639
3730
|
events,
|
|
3640
3731
|
sandbox,
|
|
3641
3732
|
state: createPluginState(pluginName),
|
|
3642
|
-
users: {
|
|
3733
|
+
users: {
|
|
3734
|
+
resolveActor,
|
|
3735
|
+
async resolveTimezone() {
|
|
3736
|
+
const actor = context.actor;
|
|
3737
|
+
const resolved = await resolveActor();
|
|
3738
|
+
return await resolveUserTimezone({
|
|
3739
|
+
db: getDb(),
|
|
3740
|
+
nowMs: Date.now(),
|
|
3741
|
+
...actor?.platform === "slack" ? { slack: {
|
|
3742
|
+
teamId: actor.teamId,
|
|
3743
|
+
userId: actor.userId
|
|
3744
|
+
} } : void 0,
|
|
3745
|
+
...resolved?.user ? { userId: resolved.user.id } : void 0
|
|
3746
|
+
});
|
|
3747
|
+
}
|
|
3748
|
+
},
|
|
3643
3749
|
workspaces: { async findByRepository(input) {
|
|
3644
3750
|
return await listWorkspaceNamesByRepository(getDb(), input);
|
|
3645
3751
|
} }
|
|
@@ -4204,4 +4310,4 @@ function createPluginHookRunner(input = {}) {
|
|
|
4204
4310
|
};
|
|
4205
4311
|
}
|
|
4206
4312
|
//#endregion
|
|
4207
|
-
export { verifySlackDirectCredentialSubject as $, getWorkspaceSnapshotBuilder as A,
|
|
4313
|
+
export { verifySlackDirectCredentialSubject as $, getWorkspaceSnapshotBuilder as A, listConversationAnnotations as At, cancelWatch as B, isConversationScopedChannel as Bt, WorkspaceValidationError as C, getSlackFileId as Ct, setWorkspaceSnapshotBuild as D, resolveRootVisibility as Dt, setWorkspaceSnapshot as E, recordSlackAttachment as Et, RESOURCE_SUBSCRIPTION_DEFAULT_TTL_MS as F, SlackActionError as Ft, findMatchingWatches as G, getWorkspaceTeamId as Gt, createTimerWatch as H, normalizeSlackConversationId as Ht, RESOURCE_SUBSCRIPTION_MAX_TTL_MS as I, downloadPrivateSlackFile as It, bindEventAutomationCredentialSubject as J, agentsInstructionsUpdatedEvent as Jt, listWatches as K, runWithWorkspaceTeamId as Kt, RESOURCE_WATCH_TOOL_SOURCE as L, getFilePermalink as Lt, workspaceRepoCheckoutPath as M, defaultTimezone as Mt, resolveChannelCapabilities as N, resolveUserTimezone as Nt, SANDBOX_RUNTIME as O, saveObjectAnnotations as Ot, getSlackToolContext as P, lookupSlackUser as Pt, verifyScheduledAutomationCredentialSubject as Q, STOP_WATCHING_TOOL_NAME as R, getHeaderString as Rt, updateWorkspace as S, buildSentryConversationUrl as St, loadSnapshotsForProfile as T, readAttachmentVisionSummary as Tt, createWatch as U, runWithSlackInstallationToken as Ut, collectEventMatchKeys as V, isDmChannel as Vt, deliverWatch as W, withSlackRetries as Wt, bindSlackDirectCredentialSubject as X, authenticationUnlinkedEvent as Xt, bindScheduledAutomationCredentialSubject as Y, authenticationLinkedEvent as Yt, verifyEventAutomationCredentialSubject as Z, deleteWorkspace as _, conversationNotArchivedForUser as _t, getPluginRoutes as a, setDashboardConversationLinkOptions as at, listWorkspaces as b, readLatestConversationBrief as bt, getPluginTools as c, createPluginModel as ct, setPlugins as d, conversationArchiveTimes as dt, verifyTimerWatchCredentialSubject as et, validatePlugins as f, conversationArchivedForEmail as ft, createWorkspace as g, conversationNotArchivedForEmail as gt, setBriefsConfig as h, conversationHasParticipantUser as ht, getPluginApiRoutes as i, getDashboardTaskLink as it, tryWorkspaceRepoCheckoutPath as j, listConversationAnnotationsById as jt, deleteWorkspaceSnapshotBuilders as k, createPluginAnnotations as kt, getPluginUserPromptContributions as l, createPluginLogger as lt, registration_exports as m, conversationHasParticipantEmail as mt, applyPluginFormatMarkdown as n, getDashboardConversationLink as nt, getPluginSlackConversationLink as o, createPluginState as ot, coreTaskRegistrations as p, conversationArchivedForUser as pt, takeDueTimerWatches as q, JUNIOR_NATIVE_EVENT_NAMESPACE as qt, createPluginHookRunner as r, getDashboardObjectIconLink as rt, getPluginSystemPromptContributions as s, createPluginEmbedder as st, agent_hooks_exports as t, JUNIOR_THREAD_STATE_TTL_MS as tt, getPlugins as u, readConversationAccessFromSql as ut, getWorkspace as v, viewerConversationMembership as vt, invalidateReadySnapshot as w, matchSlackAttachments as wt, listWorkspacesByRepository as x, readConversationEventPage as xt, getWorkspaceByName as y, createPluginConversationEvents as yt, cancelSubscriptions as z, getSlackClient as zt };
|