@sentry/junior 0.205.0 → 0.207.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/{agent-D4nu8MQV.js → agent-CmQlX68D.js} +603 -394
- package/dist/{agent-hooks-DXHpTJ4m.js → agent-hooks-BBX9p6aG.js} +1171 -100
- package/dist/{agent-runner-C04colea.js → agent-runner-CQD4HKK2.js} +9 -9
- package/dist/api/schema.d.ts +116 -39
- package/dist/api/schema.js +3 -3
- package/dist/api.d.ts +1 -1
- package/dist/api.js +182 -649
- package/dist/{app-Cb776lL8.d.ts → app-DfNrn_gS.d.ts} +9 -2
- package/dist/app.d.ts +1 -1
- package/dist/app.js +205 -233
- package/dist/{auth-pause-state-C2P3guS1.js → auth-pause-state-uv8on6cT.js} +7 -7
- package/dist/briefs-BXEmjD9D.js +398 -0
- package/dist/{catalog-runtime-CXw3asJD.js → catalog-runtime-1BZEOR8J.js} +1 -1
- package/dist/{checkpoint-CrViJ8Ps.js → checkpoint-B-JO5F3c.js} +9 -8
- package/dist/cli/chat.js +14 -12
- package/dist/cli/check.js +3 -3
- package/dist/cli/init.js +1 -0
- package/dist/cli/main.js +5 -1
- package/dist/cli/plugins.js +3 -3
- package/dist/cli/run.d.ts +2 -1
- package/dist/cli/run.js +2 -1
- package/dist/cli/snapshot-create.js +3 -3
- package/dist/cli/upgrade.d.ts +1 -1
- package/dist/cli/upgrade.js +3 -3
- package/dist/{config-BOtJQ7Pc.js → client-Bp297GA0.js} +6 -369
- package/dist/config-G2gv97R1.js +386 -0
- package/dist/{context-B45_WqVX.js → context-Bplcgypl.js} +13 -13
- package/dist/{schema-Rn8-XEaN.js → conversation-AA4R1VUn.js} +580 -558
- package/dist/{conversation-privacy-BZRA6Pes.js → conversation-privacy-BUfel19D.js} +1 -1
- package/dist/{credential-sync-Bep34IB_.js → credential-sync-D04NWqMR.js} +21 -20
- package/dist/{db-BeyXKJOI.d.ts → db-D74z7BZH.d.ts} +1 -1
- package/dist/{execution-stats-B6lgWxCK.js → execution-stats-Ds4bSvam.js} +77 -77
- package/dist/{executor-BlUhFL7_.js → executor-BZu1Ta06.js} +41 -13
- package/dist/instrumentation.js +2 -2
- package/dist/{job-queue-DLMqIK74.js → job-queue-rnAPfUhP.js} +1 -1
- package/dist/{logging-BMJzdDlP.js → logging-Dx0S5k2z.js} +1 -1
- package/dist/nitro.d.ts +1 -1
- package/dist/nitro.js +3 -3
- package/dist/{oauth-callback-server-WwG5_Zgm.js → oauth-callback-server-xGpwURIj.js} +2 -2
- package/dist/{pending-auth-CUbXq06G.js → pending-auth-DIZ4iZjS.js} +2 -2
- package/dist/{profile-T0tXE4zu.js → profile-d-fKypxo.js} +4 -3
- package/dist/prompt-x-0ZtTyb.js +552 -0
- package/dist/{registry-BA0__8ai.js → registry-CWKdLCTG.js} +2 -2
- package/dist/{resolve-bvD1ORdv.js → resolve-BhRB1s2A.js} +2 -2
- package/dist/{runner-BXYxfIUL.js → runner-DNml3owD.js} +10 -10
- package/dist/{sandbox-egress-signals-ngEPjSEW.js → sandbox-egress-signals-DUIl3-gW.js} +1 -1
- package/dist/schema-CwTeb-BF.js +559 -0
- package/dist/{skills-DJOlYdBs.js → skills-D9WesdJJ.js} +2 -2
- package/dist/{spawn-h0R1bTEo.js → spawn-Eh_3-s0M.js} +2 -2
- package/dist/{stats-3YZn-Swi.js → stats-CFlUGqVA.js} +292 -268
- package/dist/{store-C5qxF2ac.d.ts → store-Det8MQ9g.d.ts} +1 -1
- package/dist/{task-queue-CoXwd2eq.js → task-queue-D8I1_5wP.js} +3 -3
- package/dist/{task-runner-B2_mJZj0.js → task-runner-DO79gdho.js} +18 -18
- package/dist/{thread-state-CqZ1m-es.js → thread-state-DD6BA4xz.js} +5 -5
- package/dist/{turn-execution-fI6PZ0iI.js → turn-execution-BSIHxmQJ.js} +3 -3
- package/dist/{turn-lifecycle-CcnwrOcp.js → turn-lifecycle-DSBK4Ars.js} +1 -1
- package/dist/{usage-B5SfMb3H.js → usage-WRXkNOHp.js} +4 -125
- package/dist/{validation-DfWcvhuS.js → validation-BbHZ4r4S.js} +1 -1
- package/dist/{version-oD-oPrbO.js → version-C71UcRWw.js} +1 -1
- package/dist/version.js +1 -1
- package/dist/web-authorization-C73gJV7l.js +38 -0
- package/dist/{web-input-BOuq3oKK.js → web-input-Ce1FcNIM.js} +8 -8
- package/dist/{work-B91LQKHr.js → work-BEzDU_2x.js} +9 -9
- package/dist/{worker-BcRRTe6k.js → worker-MK2ubOv-.js} +5 -4
- package/migrations/0040_conversation_briefs.sql +17 -0
- package/migrations/0041_automation_outcomes_backfill.sql +27 -0
- package/migrations/meta/0040_snapshot.json +3516 -0
- package/migrations/meta/0041_snapshot.json +3516 -0
- package/migrations/meta/_journal.json +14 -0
- package/package.json +4 -4
- package/dist/history-CqTuSAdI.js +0 -441
- package/dist/usage-schema-CKaDkH_j.js +0 -20
- package/dist/web-authorization-Crbzlmzo.js +0 -76
|
@@ -1,17 +1,21 @@
|
|
|
1
1
|
import { t as __exportAll } from "./rolldown-runtime-B4FMCO8f.js";
|
|
2
|
-
import {
|
|
3
|
-
import { C as withSpan, P as normalizeIdentityEmail, _ as setSpanAttributes, d as logInfo, f as logWarn, u as logException, v as setSpanStatus } from "./logging-
|
|
2
|
+
import { a as embedTexts, r as completeObject } from "./client-Bp297GA0.js";
|
|
3
|
+
import { C as withSpan, P as normalizeIdentityEmail, _ as setSpanAttributes, d as logInfo, f as logWarn, u as logException, v as setSpanStatus } from "./logging-Dx0S5k2z.js";
|
|
4
|
+
import { a as getClient$1 } from "./sentry-CD3SdJl-.js";
|
|
5
|
+
import { a as getChatConfig, n as botConfig, s as getSlackBotToken } from "./config-G2gv97R1.js";
|
|
4
6
|
import { n as defaultModelId } from "./model-profile-CZmVoGdX.js";
|
|
5
|
-
import { _ as parseSlackMessageTs, c as isActorUserId, h as parseSlackTeamId, m as parseSlackChannelReferenceId, u as parseActorUserId } from "./context-
|
|
7
|
+
import { _ as parseSlackMessageTs, c as isActorUserId, h as parseSlackTeamId, m as parseSlackChannelReferenceId, u as parseActorUserId } from "./context-Bplcgypl.js";
|
|
6
8
|
import { n as juniorDestinations } from "./destinations-BeBiJeEM.js";
|
|
7
|
-
import { C as
|
|
8
|
-
import {
|
|
9
|
-
import { A as sleep$1, P as SANDBOX_WORKSPACE_ROOT, l as isSandboxMissingError, n as hash, v as getVercelSandboxCredentials, w as getStateAdapter, x as runNonInteractiveCommand } from "./profile-
|
|
9
|
+
import { C as conversationReadColumns, E as juniorUsers, T as juniorIdentities, _ as juniorConversationEvents, b as juniorCodeChanges, g as juniorConversationBriefs, h as juniorConversationBindings, i as juniorWorkspaces, m as juniorConversationParticipants, n as juniorSnapshots, r as juniorWorkspaceRepos, t as createJuniorSqlExecutor, w as juniorConversations, x as juniorCodeRepositories, y as juniorConversationAnnotations } from "./executor-BZu1Ta06.js";
|
|
10
|
+
import { B as decodeStoredConversationEvent, C as conversationReportEventSchema, H as newConversationEventSchema, L as conversationEventDataSchema, M as conversationBriefSchema, N as parseBriefInput, V as historyReplacementSchema } from "./conversation-AA4R1VUn.js";
|
|
11
|
+
import { A as sleep$1, P as SANDBOX_WORKSPACE_ROOT, l as isSandboxMissingError, n as hash, v as getVercelSandboxCredentials, w as getStateAdapter, x as runNonInteractiveCommand } from "./profile-d-fKypxo.js";
|
|
12
|
+
import { n as briefEntriesFromReportEvents, s as generateBrief, t as BRIEF_PROMPT } from "./prompt-x-0ZtTyb.js";
|
|
10
13
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
11
14
|
import { z } from "zod";
|
|
12
15
|
import { createHash, createHmac, randomUUID, timingSafeEqual } from "node:crypto";
|
|
13
|
-
import { and, asc, desc, eq, gt, gte, inArray, isNull, lt, lte, ne, or, sql } from "drizzle-orm";
|
|
14
|
-
import { codeChangeInputSchema, conversationAnnotationInputSchema, destinationSchema,
|
|
16
|
+
import { and, arrayContains, asc, desc, eq, getTableColumns, gt, gte, inArray, isNotNull, isNull, lt, lte, max, ne, or, sql } from "drizzle-orm";
|
|
17
|
+
import { codeChangeInputSchema, conversationAnnotationInputSchema, conversationEventPresentationSchema, defineConversationEvent, destinationSchema, eventInputSchema, eventMatchSchema, eventMatches, eventTypeSchema, identitySchema, locationSchema, missingToolAnnotationKeys, normalizeEventIdentifier, pluginEventsSchema, promptContextSchema, promptMessageSchema, sourceSchema, stableEventMatchKey, userSchema } from "@sentry/junior-plugin-api";
|
|
18
|
+
import { alias } from "drizzle-orm/pg-core";
|
|
15
19
|
import { WebClient } from "@slack/web-api";
|
|
16
20
|
import { Sandbox } from "@vercel/sandbox";
|
|
17
21
|
//#region src/chat/destination.ts
|
|
@@ -203,7 +207,7 @@ function normalizeSessionSource(value) {
|
|
|
203
207
|
visibility: value.visibility,
|
|
204
208
|
conversationId: value.conversationId
|
|
205
209
|
};
|
|
206
|
-
if (value.kind === "
|
|
210
|
+
if (value.kind === "event" || value.kind === "scheduled_automation" || value.kind === "event_automation" || value.kind === "plugin_dispatch" || value.kind === "agent_invocation") return;
|
|
207
211
|
const threadTs = value.threadTs?.trim();
|
|
208
212
|
return {
|
|
209
213
|
kind: "slack",
|
|
@@ -274,11 +278,11 @@ function systemIdentityFromSource(source) {
|
|
|
274
278
|
providerSubjectId: "local-cli",
|
|
275
279
|
displayName: "Local CLI"
|
|
276
280
|
};
|
|
277
|
-
if (source === "
|
|
281
|
+
if (source === "event") return {
|
|
278
282
|
kind: "system",
|
|
279
283
|
provider: "junior",
|
|
280
|
-
providerSubjectId: "
|
|
281
|
-
displayName: "
|
|
284
|
+
providerSubjectId: "event",
|
|
285
|
+
displayName: "Event"
|
|
282
286
|
};
|
|
283
287
|
}
|
|
284
288
|
/** Prefer the user actor identity, else a source-derived system identity. */
|
|
@@ -399,7 +403,7 @@ function tenantId(value) {
|
|
|
399
403
|
return value ?? "";
|
|
400
404
|
}
|
|
401
405
|
function sourceFromValue(value) {
|
|
402
|
-
if (value === "api" || value === "internal" || value === "local" || value === "plugin" || value === "
|
|
406
|
+
if (value === "api" || value === "internal" || value === "local" || value === "plugin" || value === "event" || value === "scheduler" || value === "slack" || value === "web") return value;
|
|
403
407
|
}
|
|
404
408
|
function originTypeFromSource(source) {
|
|
405
409
|
return source;
|
|
@@ -1001,7 +1005,7 @@ const HUMAN_INSTRUCTION_PLATFORMS = /* @__PURE__ */ new Set([
|
|
|
1001
1005
|
/**
|
|
1002
1006
|
* Whether one event should restore an archived conversation to the feed.
|
|
1003
1007
|
*
|
|
1004
|
-
* Archive hides finished noise until a human comes back.
|
|
1008
|
+
* Archive hides finished noise until a human comes back. Events,
|
|
1005
1009
|
* turn lifecycle, compaction, and other system writes may still refresh
|
|
1006
1010
|
* activity clocks, but they must not unarchive on their own.
|
|
1007
1011
|
*/
|
|
@@ -1358,6 +1362,126 @@ async function closeDb() {
|
|
|
1358
1362
|
await previous?.db.close();
|
|
1359
1363
|
}
|
|
1360
1364
|
//#endregion
|
|
1365
|
+
//#region src/chat/conversations/structured-events.ts
|
|
1366
|
+
/** Reserved namespace for Junior-owned conversation metadata events. */
|
|
1367
|
+
const JUNIOR_NATIVE_EVENT_NAMESPACE = "junior";
|
|
1368
|
+
const authenticationEventContentSchema = z.object({
|
|
1369
|
+
accountLabel: z.string().trim().min(1).max(120).optional(),
|
|
1370
|
+
actorId: z.string().min(1),
|
|
1371
|
+
authorizationId: z.string().min(1).optional(),
|
|
1372
|
+
provider: z.string().regex(/^[a-z][a-z0-9-]*$/),
|
|
1373
|
+
providerLabel: z.string().trim().min(1).max(80).optional()
|
|
1374
|
+
}).strict();
|
|
1375
|
+
const agentsInstructionsUpdatedContentSchema = z.object({
|
|
1376
|
+
action: z.enum([
|
|
1377
|
+
"loaded",
|
|
1378
|
+
"replaced",
|
|
1379
|
+
"cleared"
|
|
1380
|
+
]),
|
|
1381
|
+
directory: z.string().trim().min(1).max(500).optional(),
|
|
1382
|
+
fingerprint: z.string().trim().min(1).max(128),
|
|
1383
|
+
sources: z.array(z.object({
|
|
1384
|
+
content: z.string().min(1).max(32768),
|
|
1385
|
+
path: z.string().trim().min(1).max(500)
|
|
1386
|
+
}).strict()).max(32),
|
|
1387
|
+
textBytes: z.number().int().nonnegative().optional()
|
|
1388
|
+
}).strict();
|
|
1389
|
+
function providerTitle(content) {
|
|
1390
|
+
return content.providerLabel?.trim() || content.provider;
|
|
1391
|
+
}
|
|
1392
|
+
function agentsInstructionsTitle(action) {
|
|
1393
|
+
if (action === "loaded") return "Loaded AGENTS.md";
|
|
1394
|
+
if (action === "replaced") return "Updated AGENTS.md";
|
|
1395
|
+
return "Cleared AGENTS.md";
|
|
1396
|
+
}
|
|
1397
|
+
function sourceFilename(sourcePath) {
|
|
1398
|
+
return sourcePath.split("/").at(-1) || sourcePath;
|
|
1399
|
+
}
|
|
1400
|
+
function formatBytes(bytes) {
|
|
1401
|
+
if (bytes < 1024) return `${bytes} bytes`;
|
|
1402
|
+
const kilobytes = bytes / 1024;
|
|
1403
|
+
return `${Number.isInteger(kilobytes) ? kilobytes : kilobytes.toFixed(1)} KB`;
|
|
1404
|
+
}
|
|
1405
|
+
function agentsInstructionsPreview(content) {
|
|
1406
|
+
const source = content.sources[0];
|
|
1407
|
+
if (!source) return void 0;
|
|
1408
|
+
const filename = sourceFilename(source.path);
|
|
1409
|
+
const label = content.sources.length === 1 ? filename : `${content.sources.length} AGENTS.md files`;
|
|
1410
|
+
return typeof content.textBytes === "number" ? `${label} · ${formatBytes(content.textBytes)}` : label;
|
|
1411
|
+
}
|
|
1412
|
+
/** Junior-owned account-link transcript event. */
|
|
1413
|
+
const authenticationLinkedEvent = defineConversationEvent({
|
|
1414
|
+
name: "authentication_linked",
|
|
1415
|
+
version: 1,
|
|
1416
|
+
schema: authenticationEventContentSchema,
|
|
1417
|
+
renderEvent(event) {
|
|
1418
|
+
const provider = providerTitle(event);
|
|
1419
|
+
return {
|
|
1420
|
+
icon: "link",
|
|
1421
|
+
title: `${provider} connected`,
|
|
1422
|
+
...event.accountLabel ? { preview: `Connected as \`${event.accountLabel}\`` } : void 0,
|
|
1423
|
+
details: [{
|
|
1424
|
+
title: `${provider} connected`,
|
|
1425
|
+
...event.accountLabel ? { description: `Connected as \`${event.accountLabel}\`` } : void 0,
|
|
1426
|
+
metadata: [event.provider]
|
|
1427
|
+
}]
|
|
1428
|
+
};
|
|
1429
|
+
}
|
|
1430
|
+
});
|
|
1431
|
+
/** Junior-owned account-unlink transcript event. */
|
|
1432
|
+
const authenticationUnlinkedEvent = defineConversationEvent({
|
|
1433
|
+
name: "authentication_unlinked",
|
|
1434
|
+
version: 1,
|
|
1435
|
+
schema: authenticationEventContentSchema,
|
|
1436
|
+
renderEvent(event) {
|
|
1437
|
+
const provider = providerTitle(event);
|
|
1438
|
+
return {
|
|
1439
|
+
icon: "key",
|
|
1440
|
+
title: `${provider} disconnected`,
|
|
1441
|
+
details: [{
|
|
1442
|
+
title: `${provider} disconnected`,
|
|
1443
|
+
metadata: [event.provider]
|
|
1444
|
+
}]
|
|
1445
|
+
};
|
|
1446
|
+
}
|
|
1447
|
+
});
|
|
1448
|
+
/** Junior-owned AGENTS.md bootstrap transition for the dashboard timeline. */
|
|
1449
|
+
const agentsInstructionsUpdatedEvent = defineConversationEvent({
|
|
1450
|
+
name: "agents_instructions_updated",
|
|
1451
|
+
version: 1,
|
|
1452
|
+
schema: agentsInstructionsUpdatedContentSchema,
|
|
1453
|
+
renderEvent(event) {
|
|
1454
|
+
const title = agentsInstructionsTitle(event.action);
|
|
1455
|
+
const preview = agentsInstructionsPreview(event);
|
|
1456
|
+
return {
|
|
1457
|
+
icon: "brain",
|
|
1458
|
+
title,
|
|
1459
|
+
...preview ? { preview } : void 0,
|
|
1460
|
+
...event.sources.length > 0 ? { details: event.sources.map((source) => ({
|
|
1461
|
+
title: sourceFilename(source.path),
|
|
1462
|
+
content: source.content
|
|
1463
|
+
})) } : void 0
|
|
1464
|
+
};
|
|
1465
|
+
}
|
|
1466
|
+
});
|
|
1467
|
+
const NATIVE_EVENT_DEFINITIONS = [
|
|
1468
|
+
authenticationLinkedEvent,
|
|
1469
|
+
authenticationUnlinkedEvent,
|
|
1470
|
+
agentsInstructionsUpdatedEvent
|
|
1471
|
+
];
|
|
1472
|
+
/** Resolve a registered Junior-native conversation event definition. */
|
|
1473
|
+
function getJuniorNativeEventDefinition(args) {
|
|
1474
|
+
return NATIVE_EVENT_DEFINITIONS.find((definition) => definition.eventName === args.name && definition.version === args.version);
|
|
1475
|
+
}
|
|
1476
|
+
/** Render one Junior-native event through its active definition. */
|
|
1477
|
+
function renderJuniorNativeConversationEvent(args) {
|
|
1478
|
+
if (args.namespace !== "junior") return void 0;
|
|
1479
|
+
const definition = getJuniorNativeEventDefinition(args);
|
|
1480
|
+
if (!definition) return void 0;
|
|
1481
|
+
const presentation = definition.renderEvent(definition.parse(args.content));
|
|
1482
|
+
return presentation === void 0 ? void 0 : conversationEventPresentationSchema.parse(presentation);
|
|
1483
|
+
}
|
|
1484
|
+
//#endregion
|
|
1361
1485
|
//#region src/chat/plugins/annotations.ts
|
|
1362
1486
|
function annotationFromRow(row) {
|
|
1363
1487
|
const parsed = conversationAnnotationInputSchema.safeParse(row.annotation);
|
|
@@ -1422,6 +1546,926 @@ async function listConversationAnnotationsById(db, conversationIds) {
|
|
|
1422
1546
|
return byConversation;
|
|
1423
1547
|
}
|
|
1424
1548
|
//#endregion
|
|
1549
|
+
//#region src/chat/conversations/sql/privacy.ts
|
|
1550
|
+
const privacyRoot = alias(juniorConversations, "privacy_root");
|
|
1551
|
+
/** Accept only a structurally valid persisted root as privacy authority. */
|
|
1552
|
+
async function readRootCandidate(executor, conversationId) {
|
|
1553
|
+
const row = (await executor.db().select({
|
|
1554
|
+
requestedConversationId: juniorConversations.conversationId,
|
|
1555
|
+
requestedParentConversationId: juniorConversations.parentConversationId,
|
|
1556
|
+
requestedRootConversationId: juniorConversations.rootConversationId,
|
|
1557
|
+
rootConversationId: privacyRoot.conversationId,
|
|
1558
|
+
rootDestinationId: privacyRoot.destinationId,
|
|
1559
|
+
rootParentConversationId: privacyRoot.parentConversationId,
|
|
1560
|
+
rootRootConversationId: privacyRoot.rootConversationId
|
|
1561
|
+
}).from(juniorConversations).leftJoin(privacyRoot, eq(privacyRoot.conversationId, juniorConversations.rootConversationId)).where(eq(juniorConversations.conversationId, conversationId)))[0];
|
|
1562
|
+
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 ? {
|
|
1563
|
+
destinationId: row.rootDestinationId,
|
|
1564
|
+
rootConversationId: row.rootConversationId
|
|
1565
|
+
} : void 0;
|
|
1566
|
+
}
|
|
1567
|
+
async function readCandidateVisibility(executor, candidate) {
|
|
1568
|
+
const destinations = await executor.db().select({ visibility: juniorDestinations.visibility }).from(juniorDestinations).where(eq(juniorDestinations.id, candidate.destinationId)).for("share");
|
|
1569
|
+
return {
|
|
1570
|
+
rootConversationId: candidate.rootConversationId,
|
|
1571
|
+
visibility: destinations[0]?.visibility ?? null
|
|
1572
|
+
};
|
|
1573
|
+
}
|
|
1574
|
+
/**
|
|
1575
|
+
* Resolve a conversation's privacy authority from its persisted root.
|
|
1576
|
+
*
|
|
1577
|
+
* Missing or structurally invalid root metadata fails closed. The root
|
|
1578
|
+
* destination is locked so callers that keep the transaction open receive a
|
|
1579
|
+
* stable visibility decision.
|
|
1580
|
+
*/
|
|
1581
|
+
async function resolveRootVisibility(executor, conversationId) {
|
|
1582
|
+
const candidate = await readRootCandidate(executor, conversationId);
|
|
1583
|
+
if (!candidate) return {
|
|
1584
|
+
rootConversationId: conversationId,
|
|
1585
|
+
visibility: null
|
|
1586
|
+
};
|
|
1587
|
+
return readCandidateVisibility(executor, candidate);
|
|
1588
|
+
}
|
|
1589
|
+
//#endregion
|
|
1590
|
+
//#region src/chat/briefs/events.ts
|
|
1591
|
+
/** Durable cost and size record for one stored Brief version. */
|
|
1592
|
+
const briefUpdatedEvent = defineConversationEvent({
|
|
1593
|
+
name: "brief_updated",
|
|
1594
|
+
version: 1,
|
|
1595
|
+
schema: z.object({
|
|
1596
|
+
version: z.number().int().positive(),
|
|
1597
|
+
modelId: z.string().min(1),
|
|
1598
|
+
costUsd: z.number().finite().nonnegative().optional(),
|
|
1599
|
+
decisions: z.number().int().nonnegative(),
|
|
1600
|
+
openDecisions: z.number().int().nonnegative(),
|
|
1601
|
+
links: z.number().int().nonnegative()
|
|
1602
|
+
}).strict(),
|
|
1603
|
+
renderEvent(event) {
|
|
1604
|
+
return {
|
|
1605
|
+
icon: "activity",
|
|
1606
|
+
title: `Brief updated (v${event.version})`
|
|
1607
|
+
};
|
|
1608
|
+
}
|
|
1609
|
+
});
|
|
1610
|
+
//#endregion
|
|
1611
|
+
//#region src/chat/sentry-links.ts
|
|
1612
|
+
function getSentryOrgSlug() {
|
|
1613
|
+
return process.env.SENTRY_ORG_SLUG?.trim() || void 0;
|
|
1614
|
+
}
|
|
1615
|
+
function isSentrySaasDsnHost(host) {
|
|
1616
|
+
return host === "sentry.io" || host.endsWith(".sentry.io");
|
|
1617
|
+
}
|
|
1618
|
+
function buildSentryWebBaseUrl(dsn) {
|
|
1619
|
+
if (isSentrySaasDsnHost(dsn.host)) return "https://sentry.io";
|
|
1620
|
+
const port = dsn.port ? `:${dsn.port}` : "";
|
|
1621
|
+
const path = dsn.path ? `/${dsn.path}` : "";
|
|
1622
|
+
return `${dsn.protocol}://${dsn.host}${port}${path}`;
|
|
1623
|
+
}
|
|
1624
|
+
function buildSentryOrgPath(args) {
|
|
1625
|
+
const dsn = getClient$1()?.getDsn();
|
|
1626
|
+
if (!dsn?.host || !dsn.projectId) return;
|
|
1627
|
+
const orgSlug = getSentryOrgSlug();
|
|
1628
|
+
if (!orgSlug) return;
|
|
1629
|
+
const params = new URLSearchParams();
|
|
1630
|
+
params.set("project", dsn.projectId);
|
|
1631
|
+
if (args.query) for (const [key, value] of Object.entries(args.query)) params.set(key, value);
|
|
1632
|
+
const path = `${args.path}?${params.toString()}`;
|
|
1633
|
+
if (isSentrySaasDsnHost(dsn.host)) return `https://${orgSlug}.sentry.io/${path}`;
|
|
1634
|
+
return `${buildSentryWebBaseUrl(dsn)}/organizations/${orgSlug}/${path}`;
|
|
1635
|
+
}
|
|
1636
|
+
/** Build a Sentry conversation URL only when the runtime has enough Sentry config. */
|
|
1637
|
+
function buildSentryConversationUrl(conversationId) {
|
|
1638
|
+
return buildSentryOrgPath({ path: `explore/conversations/${encodeURIComponent(conversationId)}/` });
|
|
1639
|
+
}
|
|
1640
|
+
/** Build a Sentry event URL only when the runtime has enough Sentry config. */
|
|
1641
|
+
function buildSentryEventUrl(eventId) {
|
|
1642
|
+
return buildSentryOrgPath({
|
|
1643
|
+
path: "issues/",
|
|
1644
|
+
query: { query: eventId }
|
|
1645
|
+
});
|
|
1646
|
+
}
|
|
1647
|
+
//#endregion
|
|
1648
|
+
//#region src/api/conversations/events.ts
|
|
1649
|
+
/** Canonical event types that can contribute to the reporting projection. */
|
|
1650
|
+
const conversationReportSourceEventTypes = [
|
|
1651
|
+
"message",
|
|
1652
|
+
"message_handled",
|
|
1653
|
+
"assistant_message",
|
|
1654
|
+
"tool_result",
|
|
1655
|
+
"tool_execution_started",
|
|
1656
|
+
"guardian_action_reviewed",
|
|
1657
|
+
"turn_started",
|
|
1658
|
+
"turn_context",
|
|
1659
|
+
"structured_event",
|
|
1660
|
+
"attachments_delivered",
|
|
1661
|
+
"turn_routed",
|
|
1662
|
+
"turn_completed",
|
|
1663
|
+
"turn_failed",
|
|
1664
|
+
"compaction",
|
|
1665
|
+
"handoff",
|
|
1666
|
+
"subagent_started",
|
|
1667
|
+
"subagent_ended"
|
|
1668
|
+
];
|
|
1669
|
+
const reportingAssistantMessageSchema = z.object({
|
|
1670
|
+
type: z.literal("assistant_message"),
|
|
1671
|
+
content: z.array(z.unknown())
|
|
1672
|
+
}).passthrough();
|
|
1673
|
+
const reportingToolCallPartSchema = z.object({
|
|
1674
|
+
type: z.literal("toolCall"),
|
|
1675
|
+
id: z.string().min(1),
|
|
1676
|
+
name: z.string().min(1),
|
|
1677
|
+
arguments: z.unknown()
|
|
1678
|
+
}).passthrough();
|
|
1679
|
+
const reportingToolResultMessageSchema = z.object({
|
|
1680
|
+
type: z.literal("tool_result"),
|
|
1681
|
+
toolCallId: z.string().min(1),
|
|
1682
|
+
toolName: z.string().min(1).optional(),
|
|
1683
|
+
name: z.string().min(1).optional(),
|
|
1684
|
+
content: z.array(z.unknown()),
|
|
1685
|
+
details: z.unknown().optional(),
|
|
1686
|
+
isError: z.boolean().optional()
|
|
1687
|
+
}).passthrough();
|
|
1688
|
+
const reportingToolResultDetailsSchema = z.object({
|
|
1689
|
+
ok: z.boolean().optional(),
|
|
1690
|
+
status: z.string().optional()
|
|
1691
|
+
}).passthrough();
|
|
1692
|
+
const reportingTextPartSchema = z.object({
|
|
1693
|
+
type: z.literal("text"),
|
|
1694
|
+
text: z.string()
|
|
1695
|
+
}).passthrough();
|
|
1696
|
+
const reportingReasoningPartSchema = z.object({
|
|
1697
|
+
type: z.literal("thinking"),
|
|
1698
|
+
thinking: z.string(),
|
|
1699
|
+
redacted: z.literal(true).optional()
|
|
1700
|
+
}).passthrough();
|
|
1701
|
+
const reportingMediaPartSchema = z.object({
|
|
1702
|
+
type: z.enum(["image", "audio"]),
|
|
1703
|
+
mimeType: z.string().min(1).optional()
|
|
1704
|
+
}).passthrough();
|
|
1705
|
+
const reportingMessageAuthorSchema = z.object({
|
|
1706
|
+
fullName: z.string().min(1).optional(),
|
|
1707
|
+
userId: z.string().min(1).optional(),
|
|
1708
|
+
userName: z.string().min(1).optional()
|
|
1709
|
+
}).passthrough();
|
|
1710
|
+
/** Retain model-visible text and media descriptors, dropping all opaque fields. */
|
|
1711
|
+
function sanitizeModelContentPart(value) {
|
|
1712
|
+
const text = reportingTextPartSchema.safeParse(value);
|
|
1713
|
+
if (text.success) return {
|
|
1714
|
+
type: "text",
|
|
1715
|
+
text: text.data.text
|
|
1716
|
+
};
|
|
1717
|
+
const media = reportingMediaPartSchema.safeParse(value);
|
|
1718
|
+
if (!media.success) return void 0;
|
|
1719
|
+
return {
|
|
1720
|
+
type: media.data.type,
|
|
1721
|
+
...media.data.mimeType ? { mimeType: media.data.mimeType } : void 0
|
|
1722
|
+
};
|
|
1723
|
+
}
|
|
1724
|
+
/** Convert allowlisted Pi content parts into the reporting result value. */
|
|
1725
|
+
function modelVisibleToolOutput(content) {
|
|
1726
|
+
const sanitized = content.map(sanitizeModelContentPart).filter((part) => part !== void 0);
|
|
1727
|
+
if (sanitized.length === 0) return void 0;
|
|
1728
|
+
if (sanitized.length !== 1) return sanitized;
|
|
1729
|
+
const only = sanitized[0];
|
|
1730
|
+
return only.type === "text" ? only.text : only;
|
|
1731
|
+
}
|
|
1732
|
+
function reportMessageActorIdentity(data) {
|
|
1733
|
+
if (data.role !== "user") return void 0;
|
|
1734
|
+
const author = reportingMessageAuthorSchema.safeParse(data.meta?.author);
|
|
1735
|
+
if (!author.success) return void 0;
|
|
1736
|
+
const actorIdentity = {
|
|
1737
|
+
...author.data.fullName ? { fullName: author.data.fullName } : void 0,
|
|
1738
|
+
...author.data.userId ? { slackUserId: author.data.userId } : void 0,
|
|
1739
|
+
...author.data.userName ? { slackUserName: author.data.userName } : void 0
|
|
1740
|
+
};
|
|
1741
|
+
return Object.keys(actorIdentity).length > 0 ? actorIdentity : void 0;
|
|
1742
|
+
}
|
|
1743
|
+
/** Project native assistant tool calls through the existing reporting shape. */
|
|
1744
|
+
function reportToolCalls(args) {
|
|
1745
|
+
const message = reportingAssistantMessageSchema.safeParse(args.message);
|
|
1746
|
+
if (!message.success) return void 0;
|
|
1747
|
+
const calls = message.data.content.flatMap((part) => {
|
|
1748
|
+
const call = reportingToolCallPartSchema.safeParse(part);
|
|
1749
|
+
if (!call.success) return [];
|
|
1750
|
+
return [{
|
|
1751
|
+
toolCallId: call.data.id,
|
|
1752
|
+
name: call.data.name,
|
|
1753
|
+
status: "running",
|
|
1754
|
+
startedAt: new Date(args.createdAtMs).toISOString(),
|
|
1755
|
+
startedSeq: args.seq,
|
|
1756
|
+
...args.canExposePayload && call.data.arguments !== void 0 ? { input: call.data.arguments } : void 0
|
|
1757
|
+
}];
|
|
1758
|
+
});
|
|
1759
|
+
return calls.length > 0 ? {
|
|
1760
|
+
type: "tool_calls",
|
|
1761
|
+
calls
|
|
1762
|
+
} : void 0;
|
|
1763
|
+
}
|
|
1764
|
+
/** Project assistant reasoning with tool ids retained only for display order. */
|
|
1765
|
+
function reportAssistantMessage(args) {
|
|
1766
|
+
const message = reportingAssistantMessageSchema.safeParse(args.message);
|
|
1767
|
+
if (!message.success) return void 0;
|
|
1768
|
+
if (!message.data.content.some((part) => {
|
|
1769
|
+
const reasoning = reportingReasoningPartSchema.safeParse(part);
|
|
1770
|
+
return reasoning.success && reasoning.data.redacted !== true && reasoning.data.thinking.trim().length > 0;
|
|
1771
|
+
})) return reportToolCalls(args);
|
|
1772
|
+
const reasoningParts = [];
|
|
1773
|
+
const assistantParts = [];
|
|
1774
|
+
for (const part of message.data.content) {
|
|
1775
|
+
const reasoning = reportingReasoningPartSchema.safeParse(part);
|
|
1776
|
+
if (reasoning.success) {
|
|
1777
|
+
if (reasoning.data.redacted !== true && reasoning.data.thinking.trim().length > 0) {
|
|
1778
|
+
const projected = {
|
|
1779
|
+
type: "reasoning",
|
|
1780
|
+
...args.canExposePayload ? { text: reasoning.data.thinking } : { redacted: true }
|
|
1781
|
+
};
|
|
1782
|
+
reasoningParts.push(projected);
|
|
1783
|
+
assistantParts.push(projected);
|
|
1784
|
+
}
|
|
1785
|
+
continue;
|
|
1786
|
+
}
|
|
1787
|
+
const call = reportingToolCallPartSchema.safeParse(part);
|
|
1788
|
+
if (call.success) assistantParts.push({
|
|
1789
|
+
type: "tool_call",
|
|
1790
|
+
toolCallId: call.data.id
|
|
1791
|
+
});
|
|
1792
|
+
}
|
|
1793
|
+
const toolCalls = reportToolCalls(args);
|
|
1794
|
+
if (toolCalls?.type === "tool_calls") return {
|
|
1795
|
+
...toolCalls,
|
|
1796
|
+
assistant: { parts: assistantParts }
|
|
1797
|
+
};
|
|
1798
|
+
return reasoningParts.length > 0 ? {
|
|
1799
|
+
type: "assistant_message",
|
|
1800
|
+
parts: reasoningParts
|
|
1801
|
+
} : void 0;
|
|
1802
|
+
}
|
|
1803
|
+
/** Project a native tool result without exposing host-only result details. */
|
|
1804
|
+
function reportToolResult(args) {
|
|
1805
|
+
const message = reportingToolResultMessageSchema.safeParse(args.message);
|
|
1806
|
+
if (!message.success) return void 0;
|
|
1807
|
+
const details = reportingToolResultDetailsSchema.safeParse(message.data.details);
|
|
1808
|
+
const outcome = message.data.isError === true || details.success && (details.data.ok === false || details.data.status === "error") ? "error" : "completed";
|
|
1809
|
+
const output = modelVisibleToolOutput(message.data.content);
|
|
1810
|
+
const name = message.data.toolName ?? message.data.name ?? args.start?.name;
|
|
1811
|
+
if (!name) return void 0;
|
|
1812
|
+
return {
|
|
1813
|
+
type: "tool_calls",
|
|
1814
|
+
calls: [{
|
|
1815
|
+
toolCallId: message.data.toolCallId,
|
|
1816
|
+
name,
|
|
1817
|
+
status: outcome,
|
|
1818
|
+
...args.start ? {
|
|
1819
|
+
startedAt: new Date(args.start.createdAtMs).toISOString(),
|
|
1820
|
+
startedSeq: args.start.seq
|
|
1821
|
+
} : void 0,
|
|
1822
|
+
...args.canExposePayload && output !== void 0 ? { output } : void 0
|
|
1823
|
+
}]
|
|
1824
|
+
};
|
|
1825
|
+
}
|
|
1826
|
+
/** Return tool result ids that may need start metadata from outside a page. */
|
|
1827
|
+
function conversationReportToolResultIds(events) {
|
|
1828
|
+
return [...new Set(events.flatMap((event) => {
|
|
1829
|
+
const result = reportingToolResultMessageSchema.safeParse(event.data);
|
|
1830
|
+
return result.success ? [result.data.toolCallId] : [];
|
|
1831
|
+
}))];
|
|
1832
|
+
}
|
|
1833
|
+
function reportEventData(args) {
|
|
1834
|
+
const { data } = args;
|
|
1835
|
+
if (data.type === "structured_event") {
|
|
1836
|
+
if (!args.canExposePayload) return void 0;
|
|
1837
|
+
const presentation = data.namespace === "junior" ? renderJuniorNativeConversationEvent(data) : renderPluginConversationEvent(data);
|
|
1838
|
+
if (!presentation) return void 0;
|
|
1839
|
+
return {
|
|
1840
|
+
type: "structured_event",
|
|
1841
|
+
namespace: data.namespace,
|
|
1842
|
+
name: data.name,
|
|
1843
|
+
version: data.version,
|
|
1844
|
+
...data.turnId ? { turnId: data.turnId } : void 0,
|
|
1845
|
+
presentation
|
|
1846
|
+
};
|
|
1847
|
+
}
|
|
1848
|
+
switch (data.type) {
|
|
1849
|
+
case "message": {
|
|
1850
|
+
const actorIdentity = args.canExposePayload ? reportMessageActorIdentity(data) : void 0;
|
|
1851
|
+
return {
|
|
1852
|
+
type: "message",
|
|
1853
|
+
messageId: data.messageId,
|
|
1854
|
+
role: data.role,
|
|
1855
|
+
...data.meta?.source === "web" || data.meta?.source === "slack" ? { source: data.meta.source } : void 0,
|
|
1856
|
+
...actorIdentity ? { actorIdentity } : void 0,
|
|
1857
|
+
...typeof data.meta?.eventType === "string" ? { eventType: data.meta.eventType } : void 0,
|
|
1858
|
+
...typeof data.meta?.explicitMention === "boolean" ? { explicitMention: data.meta.explicitMention } : void 0,
|
|
1859
|
+
...args.canExposePayload && typeof data.meta?.trustedSummary === "string" ? { trustedSummary: data.meta.trustedSummary } : void 0,
|
|
1860
|
+
...args.canExposePayload ? { text: data.text } : { redacted: true }
|
|
1861
|
+
};
|
|
1862
|
+
}
|
|
1863
|
+
case "message_handled": return {
|
|
1864
|
+
type: "message_handled",
|
|
1865
|
+
messageId: data.messageId
|
|
1866
|
+
};
|
|
1867
|
+
case "turn_started": return {
|
|
1868
|
+
type: "turn_lifecycle",
|
|
1869
|
+
turnId: data.turnId,
|
|
1870
|
+
state: "started",
|
|
1871
|
+
...data.inputMessageIds.length > 0 ? { inputMessageIds: data.inputMessageIds } : void 0
|
|
1872
|
+
};
|
|
1873
|
+
case "turn_context":
|
|
1874
|
+
if (!args.canExposePayload) return;
|
|
1875
|
+
return {
|
|
1876
|
+
type: "turn_context",
|
|
1877
|
+
turnId: data.turnId,
|
|
1878
|
+
pluginName: data.pluginName,
|
|
1879
|
+
kind: data.kind,
|
|
1880
|
+
version: data.version,
|
|
1881
|
+
content: data.content
|
|
1882
|
+
};
|
|
1883
|
+
case "attachments_delivered":
|
|
1884
|
+
if (!args.canExposePayload) return;
|
|
1885
|
+
return {
|
|
1886
|
+
type: "attachments_delivered",
|
|
1887
|
+
attachments: data.attachments
|
|
1888
|
+
};
|
|
1889
|
+
case "turn_routed": return {
|
|
1890
|
+
type: "turn_routed",
|
|
1891
|
+
turnId: data.turnId,
|
|
1892
|
+
modelProfile: data.modelProfile,
|
|
1893
|
+
modelId: data.modelId,
|
|
1894
|
+
...data.costUsd !== void 0 ? { costUsd: data.costUsd } : void 0,
|
|
1895
|
+
reasoningLevel: data.reasoningLevel,
|
|
1896
|
+
...data.confidence !== void 0 ? { confidence: data.confidence } : void 0,
|
|
1897
|
+
source: data.source
|
|
1898
|
+
};
|
|
1899
|
+
case "guardian_action_reviewed": return {
|
|
1900
|
+
type: "guardian_action_reviewed",
|
|
1901
|
+
turnId: data.turnId,
|
|
1902
|
+
toolCallId: data.toolCallId,
|
|
1903
|
+
toolName: data.toolName,
|
|
1904
|
+
decision: data.decision,
|
|
1905
|
+
riskLevel: data.riskLevel,
|
|
1906
|
+
userAuthorization: data.userAuthorization
|
|
1907
|
+
};
|
|
1908
|
+
case "turn_completed": return {
|
|
1909
|
+
type: "turn_lifecycle",
|
|
1910
|
+
turnId: data.turnId,
|
|
1911
|
+
state: data.outcome === "success" ? "succeeded" : "no_reply"
|
|
1912
|
+
};
|
|
1913
|
+
case "turn_failed": {
|
|
1914
|
+
const eventId = data.eventId;
|
|
1915
|
+
const sentryEventUrl = eventId ? buildSentryEventUrl(eventId) : void 0;
|
|
1916
|
+
return {
|
|
1917
|
+
type: "turn_lifecycle",
|
|
1918
|
+
turnId: data.turnId,
|
|
1919
|
+
state: "failed",
|
|
1920
|
+
failureCode: data.failureCode,
|
|
1921
|
+
...data.failureReason ? { failureReason: data.failureReason } : void 0,
|
|
1922
|
+
...eventId ? { eventId } : void 0,
|
|
1923
|
+
...sentryEventUrl ? { sentryEventUrl } : void 0
|
|
1924
|
+
};
|
|
1925
|
+
}
|
|
1926
|
+
case "compaction": return {
|
|
1927
|
+
type: "compaction",
|
|
1928
|
+
modelProfile: data.modelProfile,
|
|
1929
|
+
modelId: data.modelId,
|
|
1930
|
+
...args.canExposePayload && data.summary ? { summary: data.summary } : void 0,
|
|
1931
|
+
...data.details ? { details: data.details } : void 0
|
|
1932
|
+
};
|
|
1933
|
+
default: return;
|
|
1934
|
+
}
|
|
1935
|
+
}
|
|
1936
|
+
/**
|
|
1937
|
+
* Project canonical events into the ordered reporting boundary.
|
|
1938
|
+
*
|
|
1939
|
+
* `canExposePayload` must come from the authorized conversation-detail policy;
|
|
1940
|
+
* this projection never derives visibility or authorization from event data.
|
|
1941
|
+
*/
|
|
1942
|
+
function projectConversationReportEventPage(args) {
|
|
1943
|
+
const subagentStarts = /* @__PURE__ */ new Map();
|
|
1944
|
+
for (const event of args.subagentStartEvents ?? []) if (event.data.type === "subagent_started") subagentStarts.set(event.data.subagentInvocationId, {
|
|
1945
|
+
createdAtMs: event.createdAtMs,
|
|
1946
|
+
data: event.data,
|
|
1947
|
+
seq: event.seq
|
|
1948
|
+
});
|
|
1949
|
+
const toolStarts = /* @__PURE__ */ new Map();
|
|
1950
|
+
for (const event of args.toolStartEvents ?? []) if (event.data.type === "tool_execution_started") {
|
|
1951
|
+
const current = toolStarts.get(event.data.toolCallId);
|
|
1952
|
+
if (!current || event.seq < current.seq) toolStarts.set(event.data.toolCallId, {
|
|
1953
|
+
createdAtMs: event.createdAtMs,
|
|
1954
|
+
name: event.data.toolName,
|
|
1955
|
+
seq: event.seq
|
|
1956
|
+
});
|
|
1957
|
+
}
|
|
1958
|
+
const projected = [];
|
|
1959
|
+
for (const event of args.events) {
|
|
1960
|
+
let data;
|
|
1961
|
+
if (event.data.type === "assistant_message" || event.data.type === "tool_result") {
|
|
1962
|
+
const reportArgs = {
|
|
1963
|
+
canExposePayload: args.canExposePayload,
|
|
1964
|
+
createdAtMs: event.createdAtMs,
|
|
1965
|
+
message: event.data,
|
|
1966
|
+
seq: event.seq
|
|
1967
|
+
};
|
|
1968
|
+
const result = reportingToolResultMessageSchema.safeParse(event.data);
|
|
1969
|
+
const start = result.success ? toolStarts.get(result.data.toolCallId) : void 0;
|
|
1970
|
+
data = reportAssistantMessage(reportArgs) ?? reportToolResult({
|
|
1971
|
+
canExposePayload: args.canExposePayload,
|
|
1972
|
+
message: event.data,
|
|
1973
|
+
...start && start.seq < event.seq ? { start } : void 0
|
|
1974
|
+
});
|
|
1975
|
+
} else if (event.data.type === "tool_execution_started") {
|
|
1976
|
+
toolStarts.set(event.data.toolCallId, {
|
|
1977
|
+
createdAtMs: event.createdAtMs,
|
|
1978
|
+
name: event.data.toolName,
|
|
1979
|
+
seq: event.seq
|
|
1980
|
+
});
|
|
1981
|
+
data = {
|
|
1982
|
+
type: "tool_calls",
|
|
1983
|
+
calls: [{
|
|
1984
|
+
toolCallId: event.data.toolCallId,
|
|
1985
|
+
name: event.data.toolName,
|
|
1986
|
+
status: "running",
|
|
1987
|
+
startedAt: new Date(event.createdAtMs).toISOString(),
|
|
1988
|
+
startedSeq: event.seq
|
|
1989
|
+
}]
|
|
1990
|
+
};
|
|
1991
|
+
} else if (event.data.type === "subagent_started") {
|
|
1992
|
+
subagentStarts.set(event.data.subagentInvocationId, {
|
|
1993
|
+
createdAtMs: event.createdAtMs,
|
|
1994
|
+
data: event.data,
|
|
1995
|
+
seq: event.seq
|
|
1996
|
+
});
|
|
1997
|
+
data = {
|
|
1998
|
+
type: "subagent",
|
|
1999
|
+
startedSeq: event.seq,
|
|
2000
|
+
startedAt: new Date(event.createdAtMs).toISOString(),
|
|
2001
|
+
childConversationId: event.data.childConversationId,
|
|
2002
|
+
subagentKind: event.data.subagentKind,
|
|
2003
|
+
status: "running",
|
|
2004
|
+
...event.data.parentToolCallId ? { parentToolCallId: event.data.parentToolCallId } : void 0
|
|
2005
|
+
};
|
|
2006
|
+
} else if (event.data.type === "subagent_ended") {
|
|
2007
|
+
const started = subagentStarts.get(event.data.subagentInvocationId);
|
|
2008
|
+
if (started && started.seq < event.seq) {
|
|
2009
|
+
data = {
|
|
2010
|
+
type: "subagent",
|
|
2011
|
+
startedSeq: started.seq,
|
|
2012
|
+
startedAt: new Date(started.createdAtMs).toISOString(),
|
|
2013
|
+
childConversationId: started.data.childConversationId,
|
|
2014
|
+
subagentKind: started.data.subagentKind,
|
|
2015
|
+
...started.data.parentToolCallId ? { parentToolCallId: started.data.parentToolCallId } : void 0,
|
|
2016
|
+
status: event.data.outcome === "success" ? "completed" : event.data.outcome
|
|
2017
|
+
};
|
|
2018
|
+
subagentStarts.delete(event.data.subagentInvocationId);
|
|
2019
|
+
}
|
|
2020
|
+
} else if (event.data.type === "handoff") data = {
|
|
2021
|
+
type: "handoff",
|
|
2022
|
+
modelProfile: event.data.modelProfile,
|
|
2023
|
+
modelId: event.data.modelId,
|
|
2024
|
+
...event.data.reasoningLevel ? { reasoningLevel: event.data.reasoningLevel } : void 0,
|
|
2025
|
+
...event.data.triggeringToolCallId ? { triggeringToolCallId: event.data.triggeringToolCallId } : void 0,
|
|
2026
|
+
...args.canExposePayload && event.data.summary ? { summary: event.data.summary } : void 0
|
|
2027
|
+
};
|
|
2028
|
+
else data = reportEventData({
|
|
2029
|
+
canExposePayload: args.canExposePayload,
|
|
2030
|
+
data: event.data
|
|
2031
|
+
});
|
|
2032
|
+
if (!data) continue;
|
|
2033
|
+
projected.push(conversationReportEventSchema.parse({
|
|
2034
|
+
seq: event.seq,
|
|
2035
|
+
createdAt: new Date(event.createdAtMs).toISOString(),
|
|
2036
|
+
data
|
|
2037
|
+
}));
|
|
2038
|
+
}
|
|
2039
|
+
return projected;
|
|
2040
|
+
}
|
|
2041
|
+
//#endregion
|
|
2042
|
+
//#region src/api/conversations/event-page.ts
|
|
2043
|
+
const conversationEventColumns = getTableColumns(juniorConversationEvents);
|
|
2044
|
+
const MIN_SCAN_SIZE = 64;
|
|
2045
|
+
/** Read bounded canonical rows while excluding model-only replacement history. */
|
|
2046
|
+
async function readConversationEventRows(executor, args) {
|
|
2047
|
+
return executor.db().select({
|
|
2048
|
+
...conversationEventColumns,
|
|
2049
|
+
payload: sql`case
|
|
2050
|
+
when ${juniorConversationEvents.type} in ('compaction', 'handoff')
|
|
2051
|
+
then jsonb_set(
|
|
2052
|
+
${juniorConversationEvents.payload},
|
|
2053
|
+
'{replacementHistory}',
|
|
2054
|
+
'[]'::jsonb
|
|
2055
|
+
)
|
|
2056
|
+
else ${juniorConversationEvents.payload}
|
|
2057
|
+
end`
|
|
2058
|
+
}).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);
|
|
2059
|
+
}
|
|
2060
|
+
function decodeConversationEventRow(row) {
|
|
2061
|
+
return decodeStoredConversationEvent({
|
|
2062
|
+
schemaVersion: row.schemaVersion,
|
|
2063
|
+
seq: row.seq,
|
|
2064
|
+
historyVersion: row.historyVersion,
|
|
2065
|
+
...row.idempotencyKey ? { idempotencyKey: row.idempotencyKey } : void 0,
|
|
2066
|
+
createdAtMs: row.createdAt.getTime(),
|
|
2067
|
+
type: row.type,
|
|
2068
|
+
payload: row.payload
|
|
2069
|
+
});
|
|
2070
|
+
}
|
|
2071
|
+
/** Project stored rows and resolve entity starts that precede the page. */
|
|
2072
|
+
async function projectConversationEventRows(executor, args) {
|
|
2073
|
+
const events = args.rows.map(decodeConversationEventRow).sort((left, right) => left.seq - right.seq);
|
|
2074
|
+
const endedInvocationIds = [...new Set(events.flatMap((event) => event.data.type === "subagent_ended" ? [event.data.subagentInvocationId] : []))];
|
|
2075
|
+
const subagentStartRows = endedInvocationIds.length === 0 ? [] : await readConversationEventRows(executor, {
|
|
2076
|
+
conversationId: args.conversationId,
|
|
2077
|
+
direction: "forward",
|
|
2078
|
+
limit: endedInvocationIds.length,
|
|
2079
|
+
subagentInvocationIds: endedInvocationIds,
|
|
2080
|
+
types: ["subagent_started"]
|
|
2081
|
+
});
|
|
2082
|
+
const toolResultIds = conversationReportToolResultIds(events);
|
|
2083
|
+
const toolStartRows = toolResultIds.length === 0 ? [] : await readConversationEventRows(executor, {
|
|
2084
|
+
conversationId: args.conversationId,
|
|
2085
|
+
direction: "forward",
|
|
2086
|
+
limit: toolResultIds.length,
|
|
2087
|
+
toolCallIds: toolResultIds,
|
|
2088
|
+
types: ["tool_execution_started"]
|
|
2089
|
+
});
|
|
2090
|
+
return projectConversationReportEventPage({
|
|
2091
|
+
canExposePayload: args.canExposePayload,
|
|
2092
|
+
events,
|
|
2093
|
+
subagentStartEvents: subagentStartRows.map(decodeConversationEventRow),
|
|
2094
|
+
toolStartEvents: toolStartRows.map(decodeConversationEventRow)
|
|
2095
|
+
});
|
|
2096
|
+
}
|
|
2097
|
+
/**
|
|
2098
|
+
* Read the latest reporting events before an exclusive boundary.
|
|
2099
|
+
*
|
|
2100
|
+
* Source rows are scanned in bounded chunks until the requested projected page
|
|
2101
|
+
* and one older event are known. This keeps ordinary pages bounded without
|
|
2102
|
+
* assuming every agent history event produces a reporting event.
|
|
2103
|
+
*/
|
|
2104
|
+
async function readConversationEventPage(executor, args) {
|
|
2105
|
+
const scanSize = Math.max(args.limit + 1, MIN_SCAN_SIZE);
|
|
2106
|
+
const rows = [];
|
|
2107
|
+
let beforeSeq = args.beforeSeq;
|
|
2108
|
+
let projected = [];
|
|
2109
|
+
while (projected.length <= args.limit) {
|
|
2110
|
+
const batch = await readConversationEventRows(executor, {
|
|
2111
|
+
beforeSeq,
|
|
2112
|
+
conversationId: args.conversationId,
|
|
2113
|
+
direction: "backward",
|
|
2114
|
+
limit: scanSize
|
|
2115
|
+
});
|
|
2116
|
+
if (batch.length === 0) break;
|
|
2117
|
+
rows.push(...batch);
|
|
2118
|
+
projected = await projectConversationEventRows(executor, {
|
|
2119
|
+
canExposePayload: args.canExposePayload,
|
|
2120
|
+
conversationId: args.conversationId,
|
|
2121
|
+
rows
|
|
2122
|
+
});
|
|
2123
|
+
if (projected.length > args.limit || batch.length < scanSize) break;
|
|
2124
|
+
beforeSeq = batch.at(-1).seq;
|
|
2125
|
+
}
|
|
2126
|
+
const events = projected.slice(-args.limit);
|
|
2127
|
+
return {
|
|
2128
|
+
events,
|
|
2129
|
+
...projected.length > events.length && events[0] ? { previousSeq: events[0].seq } : void 0
|
|
2130
|
+
};
|
|
2131
|
+
}
|
|
2132
|
+
//#endregion
|
|
2133
|
+
//#region src/chat/briefs/input.ts
|
|
2134
|
+
const EVENT_PAGE_SIZE = 500;
|
|
2135
|
+
async function readReportEventsThrough(executor, conversationId, throughSeq) {
|
|
2136
|
+
const pages = [];
|
|
2137
|
+
let beforeSeq = throughSeq + 1;
|
|
2138
|
+
do {
|
|
2139
|
+
const page = await readConversationEventPage(executor, {
|
|
2140
|
+
beforeSeq,
|
|
2141
|
+
canExposePayload: true,
|
|
2142
|
+
conversationId,
|
|
2143
|
+
limit: EVENT_PAGE_SIZE
|
|
2144
|
+
});
|
|
2145
|
+
pages.unshift(page.events);
|
|
2146
|
+
beforeSeq = page.previousSeq;
|
|
2147
|
+
} while (beforeSeq !== void 0);
|
|
2148
|
+
const bySequence = /* @__PURE__ */ new Map();
|
|
2149
|
+
for (const event of pages.flat()) bySequence.set(event.seq, event);
|
|
2150
|
+
return [...bySequence.values()].sort((left, right) => left.seq - right.seq);
|
|
2151
|
+
}
|
|
2152
|
+
/**
|
|
2153
|
+
* Build generator input from SQL through one durable event. The caller owns
|
|
2154
|
+
* the boundary, so a Turn that completes during the read cannot be covered by
|
|
2155
|
+
* a Brief that never saw it.
|
|
2156
|
+
*/
|
|
2157
|
+
async function briefInputFromSql(executor, conversationId, throughSeq) {
|
|
2158
|
+
const db = executor.db();
|
|
2159
|
+
const conversation = await createSqlStore(executor).get({ conversationId });
|
|
2160
|
+
if (!conversation) throw new Error(`Conversation ${conversationId} is unavailable`);
|
|
2161
|
+
const [events, annotations, codeChanges] = await Promise.all([
|
|
2162
|
+
readReportEventsThrough(executor, conversationId, throughSeq),
|
|
2163
|
+
listConversationAnnotations(db, conversationId),
|
|
2164
|
+
db.select({
|
|
2165
|
+
closedAt: juniorCodeChanges.closedAt,
|
|
2166
|
+
mergedAt: juniorCodeChanges.mergedAt,
|
|
2167
|
+
number: juniorCodeChanges.number,
|
|
2168
|
+
openedAt: juniorCodeChanges.openedAt,
|
|
2169
|
+
repository: juniorCodeRepositories.name,
|
|
2170
|
+
state: juniorCodeChanges.state,
|
|
2171
|
+
title: juniorCodeChanges.title,
|
|
2172
|
+
url: juniorCodeChanges.url
|
|
2173
|
+
}).from(juniorCodeChanges).innerJoin(juniorCodeRepositories, eq(juniorCodeRepositories.id, juniorCodeChanges.repositoryId)).where(and(arrayContains(juniorCodeChanges.conversationIds, [conversationId]), isNotNull(juniorCodeChanges.url)))
|
|
2174
|
+
]);
|
|
2175
|
+
const location = conversation.location;
|
|
2176
|
+
return parseBriefInput({
|
|
2177
|
+
conversationId,
|
|
2178
|
+
...conversation.title ? { title: conversation.title } : void 0,
|
|
2179
|
+
visibility: conversation.visibility ?? "private",
|
|
2180
|
+
...location ? { location: {
|
|
2181
|
+
provider: location.provider,
|
|
2182
|
+
...conversation.channelName ? { channelName: conversation.channelName } : void 0
|
|
2183
|
+
} } : void 0,
|
|
2184
|
+
entries: briefEntriesFromReportEvents(events),
|
|
2185
|
+
codeChanges: codeChanges.map((change) => ({
|
|
2186
|
+
repository: change.repository,
|
|
2187
|
+
number: change.number,
|
|
2188
|
+
...change.title ? { title: change.title } : void 0,
|
|
2189
|
+
url: change.url,
|
|
2190
|
+
state: change.state,
|
|
2191
|
+
openedAt: change.openedAt.toISOString(),
|
|
2192
|
+
...change.mergedAt ? { mergedAt: change.mergedAt.toISOString() } : void 0,
|
|
2193
|
+
...change.closedAt ? { closedAt: change.closedAt.toISOString() } : void 0
|
|
2194
|
+
})),
|
|
2195
|
+
resources: annotations.map((annotation) => ({
|
|
2196
|
+
label: annotation.label,
|
|
2197
|
+
url: annotation.url,
|
|
2198
|
+
...annotation.status ? { status: annotation.status } : void 0
|
|
2199
|
+
}))
|
|
2200
|
+
});
|
|
2201
|
+
}
|
|
2202
|
+
//#endregion
|
|
2203
|
+
//#region src/chat/briefs/operational-report.ts
|
|
2204
|
+
const DAY_MS$1 = 864e5;
|
|
2205
|
+
const WINDOWS = [
|
|
2206
|
+
7,
|
|
2207
|
+
30,
|
|
2208
|
+
90
|
|
2209
|
+
];
|
|
2210
|
+
function startOfUtcDay$1(value) {
|
|
2211
|
+
const date = new Date(value);
|
|
2212
|
+
date.setUTCHours(0, 0, 0, 0);
|
|
2213
|
+
return date;
|
|
2214
|
+
}
|
|
2215
|
+
function formatCount(value) {
|
|
2216
|
+
return new Intl.NumberFormat("en-US").format(value);
|
|
2217
|
+
}
|
|
2218
|
+
function formatUsd(value) {
|
|
2219
|
+
const maximumFractionDigits = value > 0 && value < .01 ? 4 : 2;
|
|
2220
|
+
return new Intl.NumberFormat("en-US", {
|
|
2221
|
+
currency: "USD",
|
|
2222
|
+
maximumFractionDigits,
|
|
2223
|
+
minimumFractionDigits: 2,
|
|
2224
|
+
style: "currency"
|
|
2225
|
+
}).format(value);
|
|
2226
|
+
}
|
|
2227
|
+
/**
|
|
2228
|
+
* Build Brief storage and model-cost diagnostics for the System page. Cost
|
|
2229
|
+
* comes from retained Conversation events, so spend on purged private
|
|
2230
|
+
* Conversations leaves the report with them; the labels say so.
|
|
2231
|
+
*/
|
|
2232
|
+
async function buildBriefsOperationalReport(args) {
|
|
2233
|
+
const newestDay = startOfUtcDay$1(args.nowMs);
|
|
2234
|
+
const windowStart = /* @__PURE__ */ new Date(newestDay.getTime() - 29 * DAY_MS$1);
|
|
2235
|
+
const windowEnd = new Date(newestDay.getTime() + DAY_MS$1);
|
|
2236
|
+
const [[counts], recentCounts] = await Promise.all([args.db.select({
|
|
2237
|
+
conversations: sql`count(distinct ${juniorConversationBriefs.conversationId})`.mapWith(Number),
|
|
2238
|
+
stored: sql`count(*)`.mapWith(Number)
|
|
2239
|
+
}).from(juniorConversationBriefs), args.db.select({ stored: sql`count(*)`.mapWith(Number) }).from(juniorConversationBriefs).where(and(gte(juniorConversationBriefs.createdAt, windowStart), lt(juniorConversationBriefs.createdAt, windowEnd)))]);
|
|
2240
|
+
const recentDays = args.briefDays.filter((day) => {
|
|
2241
|
+
const dayStart = Date.parse(`${day.date}T00:00:00.000Z`);
|
|
2242
|
+
return dayStart >= windowStart.getTime() && dayStart < windowEnd.getTime();
|
|
2243
|
+
});
|
|
2244
|
+
const recentCost = recentDays.reduce((total, day) => total + day.costUsd, 0);
|
|
2245
|
+
const recentBriefs = recentDays.reduce((total, day) => total + day.events, 0);
|
|
2246
|
+
return {
|
|
2247
|
+
generatedAt: new Date(args.nowMs).toISOString(),
|
|
2248
|
+
title: "Briefs",
|
|
2249
|
+
metrics: [
|
|
2250
|
+
{
|
|
2251
|
+
label: "briefs stored",
|
|
2252
|
+
tone: counts?.stored ? "good" : "neutral",
|
|
2253
|
+
value: formatCount(counts?.stored ?? 0)
|
|
2254
|
+
},
|
|
2255
|
+
{
|
|
2256
|
+
label: "conversations with a brief",
|
|
2257
|
+
value: formatCount(counts?.conversations ?? 0)
|
|
2258
|
+
},
|
|
2259
|
+
{
|
|
2260
|
+
label: "briefs · 30d",
|
|
2261
|
+
value: formatCount(recentCounts[0]?.stored ?? 0)
|
|
2262
|
+
},
|
|
2263
|
+
{
|
|
2264
|
+
label: "cost · 30d · retained",
|
|
2265
|
+
value: formatUsd(recentCost)
|
|
2266
|
+
},
|
|
2267
|
+
{
|
|
2268
|
+
label: "average cost per brief · 30d · retained",
|
|
2269
|
+
value: formatUsd(recentBriefs === 0 ? 0 : recentCost / recentBriefs)
|
|
2270
|
+
}
|
|
2271
|
+
],
|
|
2272
|
+
widgets: [{
|
|
2273
|
+
categories: args.briefDays.map((day) => ({
|
|
2274
|
+
id: day.date,
|
|
2275
|
+
label: day.date,
|
|
2276
|
+
values: {
|
|
2277
|
+
briefs: day.events,
|
|
2278
|
+
costUsd: day.costUsd
|
|
2279
|
+
}
|
|
2280
|
+
})),
|
|
2281
|
+
description: "Stored Brief versions and estimated model cost",
|
|
2282
|
+
id: "briefs-created",
|
|
2283
|
+
series: [{
|
|
2284
|
+
key: "briefs",
|
|
2285
|
+
label: "Briefs"
|
|
2286
|
+
}, {
|
|
2287
|
+
format: "usd",
|
|
2288
|
+
key: "costUsd",
|
|
2289
|
+
label: "Cost"
|
|
2290
|
+
}],
|
|
2291
|
+
timeRangeDays: [...WINDOWS],
|
|
2292
|
+
title: "Briefs created",
|
|
2293
|
+
type: "bar_chart"
|
|
2294
|
+
}]
|
|
2295
|
+
};
|
|
2296
|
+
}
|
|
2297
|
+
//#endregion
|
|
2298
|
+
//#region src/chat/briefs/store.ts
|
|
2299
|
+
function briefVersionFromRow(row) {
|
|
2300
|
+
return {
|
|
2301
|
+
conversationId: row.conversationId,
|
|
2302
|
+
version: row.version,
|
|
2303
|
+
turnId: row.turnId,
|
|
2304
|
+
throughSeq: row.throughSeq,
|
|
2305
|
+
content: conversationBriefSchema.parse(row.content),
|
|
2306
|
+
searchText: row.searchText,
|
|
2307
|
+
modelId: row.modelId,
|
|
2308
|
+
...row.costUsd !== null ? { costUsd: row.costUsd } : void 0,
|
|
2309
|
+
createdAt: row.createdAt
|
|
2310
|
+
};
|
|
2311
|
+
}
|
|
2312
|
+
/** Append the next Brief version, or return the row for an existing Turn. */
|
|
2313
|
+
async function appendConversationBrief(db, input) {
|
|
2314
|
+
return await db.transaction(async (tx) => {
|
|
2315
|
+
const conversation = (await tx.select({
|
|
2316
|
+
conversationId: juniorConversations.conversationId,
|
|
2317
|
+
transcriptPurgedAt: juniorConversations.transcriptPurgedAt
|
|
2318
|
+
}).from(juniorConversations).where(eq(juniorConversations.conversationId, input.conversationId)).for("update"))[0];
|
|
2319
|
+
if (!conversation) throw new Error(`Conversation ${input.conversationId} is unavailable`);
|
|
2320
|
+
if (conversation.transcriptPurgedAt) {
|
|
2321
|
+
if ((await resolveRootVisibility({ db: () => tx }, input.conversationId)).visibility !== "public") throw new Error(`Cannot append a Brief to purged non-public Conversation ${input.conversationId}`);
|
|
2322
|
+
}
|
|
2323
|
+
const existing = await tx.select().from(juniorConversationBriefs).where(and(eq(juniorConversationBriefs.conversationId, input.conversationId), eq(juniorConversationBriefs.turnId, input.turnId))).limit(1);
|
|
2324
|
+
if (existing[0]) return {
|
|
2325
|
+
inserted: false,
|
|
2326
|
+
value: briefVersionFromRow(existing[0])
|
|
2327
|
+
};
|
|
2328
|
+
const version = ((await tx.select({ version: max(juniorConversationBriefs.version) }).from(juniorConversationBriefs).where(eq(juniorConversationBriefs.conversationId, input.conversationId)))[0]?.version ?? 0) + 1;
|
|
2329
|
+
const row = (await tx.insert(juniorConversationBriefs).values({
|
|
2330
|
+
conversationId: input.conversationId,
|
|
2331
|
+
version,
|
|
2332
|
+
turnId: input.turnId,
|
|
2333
|
+
throughSeq: input.throughSeq,
|
|
2334
|
+
content: conversationBriefSchema.parse(input.content),
|
|
2335
|
+
searchText: input.searchText,
|
|
2336
|
+
modelId: input.modelId,
|
|
2337
|
+
costUsd: input.costUsd ?? null
|
|
2338
|
+
}).onConflictDoNothing({ target: [juniorConversationBriefs.conversationId, juniorConversationBriefs.turnId] }).returning())[0];
|
|
2339
|
+
if (!row) throw new Error("Brief insert conflicted after the Turn check");
|
|
2340
|
+
return {
|
|
2341
|
+
inserted: true,
|
|
2342
|
+
value: briefVersionFromRow(row)
|
|
2343
|
+
};
|
|
2344
|
+
});
|
|
2345
|
+
}
|
|
2346
|
+
/** Read the Brief version produced for one completed Turn. */
|
|
2347
|
+
async function readConversationBriefForTurn(db, conversationId, turnId) {
|
|
2348
|
+
const rows = await db.select().from(juniorConversationBriefs).where(and(eq(juniorConversationBriefs.conversationId, conversationId), eq(juniorConversationBriefs.turnId, turnId))).limit(1);
|
|
2349
|
+
return rows[0] ? briefVersionFromRow(rows[0]) : void 0;
|
|
2350
|
+
}
|
|
2351
|
+
/** Read the latest Brief version for one Conversation. */
|
|
2352
|
+
async function readLatestConversationBrief(db, conversationId) {
|
|
2353
|
+
const rows = await db.select().from(juniorConversationBriefs).where(eq(juniorConversationBriefs.conversationId, conversationId)).orderBy(desc(juniorConversationBriefs.version)).limit(1);
|
|
2354
|
+
return rows[0] ? briefVersionFromRow(rows[0]) : void 0;
|
|
2355
|
+
}
|
|
2356
|
+
//#endregion
|
|
2357
|
+
//#region src/chat/briefs/task.ts
|
|
2358
|
+
const BRIEF_LOCK_TTL_MS = 6e5;
|
|
2359
|
+
function hasUserInstruction(run) {
|
|
2360
|
+
return run.transcript.some((entry) => entry.type === "message" && entry.role === "user" && entry.provenance?.authority === "instruction");
|
|
2361
|
+
}
|
|
2362
|
+
function logSkip(context, conversationId, reason) {
|
|
2363
|
+
context.log.info("Brief update skipped", {
|
|
2364
|
+
conversationId,
|
|
2365
|
+
reason
|
|
2366
|
+
});
|
|
2367
|
+
}
|
|
2368
|
+
async function readTurnCompletedSeq(db, conversationId, turnId) {
|
|
2369
|
+
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;
|
|
2370
|
+
if (seq === void 0) throw new Error(`Completed Turn ${turnId} has no terminal event in Conversation ${conversationId}`);
|
|
2371
|
+
return seq;
|
|
2372
|
+
}
|
|
2373
|
+
async function emitBriefUpdated(context, stored) {
|
|
2374
|
+
await context.events.emit(briefUpdatedEvent({
|
|
2375
|
+
version: stored.version,
|
|
2376
|
+
modelId: stored.modelId,
|
|
2377
|
+
...stored.costUsd !== void 0 ? { costUsd: stored.costUsd } : void 0,
|
|
2378
|
+
decisions: stored.content.decisions.length,
|
|
2379
|
+
openDecisions: stored.content.openDecisions.length,
|
|
2380
|
+
links: stored.content.links.length
|
|
2381
|
+
}));
|
|
2382
|
+
}
|
|
2383
|
+
/** Generate and store the next Brief after one completed Turn. */
|
|
2384
|
+
async function updateConversationBrief(context) {
|
|
2385
|
+
const run = await context.run.load();
|
|
2386
|
+
if (run.source.kind !== "slack" && run.source.kind !== "web" && run.source.kind !== "local") {
|
|
2387
|
+
logSkip(context, run.conversationId, "unsupported_source");
|
|
2388
|
+
return;
|
|
2389
|
+
}
|
|
2390
|
+
const executor = getSqlExecutor();
|
|
2391
|
+
const conversation = await createSqlStore(executor).get({ conversationId: run.conversationId });
|
|
2392
|
+
if (conversation?.parentConversationId) {
|
|
2393
|
+
logSkip(context, run.conversationId, "child_conversation");
|
|
2394
|
+
return;
|
|
2395
|
+
}
|
|
2396
|
+
if (conversation?.transcriptPurgedAtMs !== void 0) {
|
|
2397
|
+
if ((await resolveRootVisibility(executor, run.conversationId)).visibility !== "public") {
|
|
2398
|
+
logSkip(context, run.conversationId, "purged_private");
|
|
2399
|
+
return;
|
|
2400
|
+
}
|
|
2401
|
+
}
|
|
2402
|
+
if (!hasUserInstruction(run)) {
|
|
2403
|
+
logSkip(context, run.conversationId, "no_user_instruction");
|
|
2404
|
+
return;
|
|
2405
|
+
}
|
|
2406
|
+
await context.state.withLock(`brief:${run.conversationId}`, BRIEF_LOCK_TTL_MS, async () => {
|
|
2407
|
+
const db = context.db;
|
|
2408
|
+
const existing = await readConversationBriefForTurn(db, run.conversationId, run.runId);
|
|
2409
|
+
if (existing) {
|
|
2410
|
+
await emitBriefUpdated(context, existing);
|
|
2411
|
+
logSkip(context, run.conversationId, "turn_already_stored");
|
|
2412
|
+
return;
|
|
2413
|
+
}
|
|
2414
|
+
const terminalSeq = await readTurnCompletedSeq(db, run.conversationId, run.runId);
|
|
2415
|
+
const previous = await readLatestConversationBrief(db, run.conversationId);
|
|
2416
|
+
if (previous && previous.throughSeq >= terminalSeq) {
|
|
2417
|
+
logSkip(context, run.conversationId, "turn_already_covered");
|
|
2418
|
+
return;
|
|
2419
|
+
}
|
|
2420
|
+
const input = await briefInputFromSql(executor, run.conversationId, terminalSeq);
|
|
2421
|
+
const modelId = defaultModelId(botConfig);
|
|
2422
|
+
const generation = await generateBrief({
|
|
2423
|
+
completeObject: (request) => completeObject({
|
|
2424
|
+
...request,
|
|
2425
|
+
modelId,
|
|
2426
|
+
promptName: "junior.brief_update"
|
|
2427
|
+
}),
|
|
2428
|
+
input,
|
|
2429
|
+
previous: previous?.content,
|
|
2430
|
+
prompt: BRIEF_PROMPT,
|
|
2431
|
+
throughIndex: terminalSeq
|
|
2432
|
+
});
|
|
2433
|
+
const stored = await appendConversationBrief(db, {
|
|
2434
|
+
conversationId: run.conversationId,
|
|
2435
|
+
turnId: run.runId,
|
|
2436
|
+
throughSeq: terminalSeq,
|
|
2437
|
+
content: generation.brief,
|
|
2438
|
+
searchText: generation.searchText,
|
|
2439
|
+
modelId,
|
|
2440
|
+
...generation.costUsd !== void 0 ? { costUsd: generation.costUsd } : void 0
|
|
2441
|
+
});
|
|
2442
|
+
await emitBriefUpdated(context, stored.value);
|
|
2443
|
+
if (!stored.inserted) logSkip(context, run.conversationId, "turn_already_stored");
|
|
2444
|
+
});
|
|
2445
|
+
}
|
|
2446
|
+
/** Core task registration kept outside the installed plugin catalog. */
|
|
2447
|
+
const briefsTaskRegistration = {
|
|
2448
|
+
manifest: {
|
|
2449
|
+
name: "briefs",
|
|
2450
|
+
displayName: "Briefs",
|
|
2451
|
+
description: "Durable Conversation Brief generation"
|
|
2452
|
+
},
|
|
2453
|
+
conversationEvents: [briefUpdatedEvent],
|
|
2454
|
+
hooks: { async operationalReport(context) {
|
|
2455
|
+
return await buildBriefsOperationalReport({
|
|
2456
|
+
briefDays: await context.eventStats.costsByDay({
|
|
2457
|
+
days: 90,
|
|
2458
|
+
eventName: "brief_updated"
|
|
2459
|
+
}),
|
|
2460
|
+
db: context.db,
|
|
2461
|
+
nowMs: context.nowMs
|
|
2462
|
+
});
|
|
2463
|
+
} },
|
|
2464
|
+
tasks: { updateBrief: { async run(context) {
|
|
2465
|
+
await updateConversationBrief(context);
|
|
2466
|
+
} } }
|
|
2467
|
+
};
|
|
2468
|
+
//#endregion
|
|
1425
2469
|
//#region src/chat/plugins/conversation-events.ts
|
|
1426
2470
|
function registeredDefinition(plugin, value) {
|
|
1427
2471
|
return plugin.conversationEvents?.find((definition) => definition === value.definition);
|
|
@@ -1455,7 +2499,7 @@ function createPluginConversationEvents(args) {
|
|
|
1455
2499
|
}
|
|
1456
2500
|
/** Render a stored plugin event through its currently registered definition. */
|
|
1457
2501
|
function renderPluginConversationEvent(args) {
|
|
1458
|
-
const definition = getPlugins().find((candidate) => candidate.manifest.name === args.namespace)?.conversationEvents?.find((candidate) => candidate.eventName === args.name && candidate.version === args.version);
|
|
2502
|
+
const definition = [briefsTaskRegistration, ...getPlugins()].find((candidate) => candidate.manifest.name === args.namespace)?.conversationEvents?.find((candidate) => candidate.eventName === args.name && candidate.version === args.version);
|
|
1459
2503
|
if (!definition) return void 0;
|
|
1460
2504
|
return definition.renderEvent(definition.parse(args.content));
|
|
1461
2505
|
}
|
|
@@ -1666,7 +2710,7 @@ function createPluginState(plugin, adapter) {
|
|
|
1666
2710
|
};
|
|
1667
2711
|
}
|
|
1668
2712
|
//#endregion
|
|
1669
|
-
//#region src/chat/
|
|
2713
|
+
//#region src/chat/dashboard-link.ts
|
|
1670
2714
|
let dashboardConversationLinkOptions;
|
|
1671
2715
|
function withHttps(host) {
|
|
1672
2716
|
return /^https?:\/\//.test(host) ? host : `https://${host}`;
|
|
@@ -1689,7 +2733,7 @@ function resolveDashboardBaseURL(config) {
|
|
|
1689
2733
|
if (vercelUrl) return stripTrailingSlashes(withHttps(vercelUrl));
|
|
1690
2734
|
return "http://localhost:3000";
|
|
1691
2735
|
}
|
|
1692
|
-
/** Configure core dashboard links
|
|
2736
|
+
/** Configure core dashboard links. */
|
|
1693
2737
|
function setDashboardConversationLinkOptions(options) {
|
|
1694
2738
|
const previous = dashboardConversationLinkOptions;
|
|
1695
2739
|
dashboardConversationLinkOptions = options?.disabled ? void 0 : options;
|
|
@@ -1707,15 +2751,15 @@ function getDashboardConversationLink(conversationId) {
|
|
|
1707
2751
|
}
|
|
1708
2752
|
/** Build the dashboard task detail URL when the core dashboard is enabled. */
|
|
1709
2753
|
function getDashboardTaskLink(taskId) {
|
|
1710
|
-
return resolveDashboardPath(`/
|
|
2754
|
+
return resolveDashboardPath(`/automations/${encodeURIComponent(taskId)}`);
|
|
1711
2755
|
}
|
|
1712
2756
|
//#endregion
|
|
1713
2757
|
//#region src/chat/state/ttl.ts
|
|
1714
2758
|
/** Retain expiring conversation/runtime state long enough for resume and dashboard history. */
|
|
1715
2759
|
const JUNIOR_THREAD_STATE_TTL_MS = 6048e5;
|
|
1716
2760
|
//#endregion
|
|
1717
|
-
//#region src/chat/
|
|
1718
|
-
const
|
|
2761
|
+
//#region src/chat/events/store.ts
|
|
2762
|
+
const EVENT_PREFIX = "junior:resource_event_subscription:v5";
|
|
1719
2763
|
const INDEX_LOCK_TTL_MS = 1e4;
|
|
1720
2764
|
const SUBSCRIPTION_LOCK_TTL_MS = 1e4;
|
|
1721
2765
|
const SUBSCRIPTION_LOCK_WAIT_MS = 1e4;
|
|
@@ -1730,12 +2774,12 @@ const subscriptionIdIndexSchema = z.array(z.string().min(1));
|
|
|
1730
2774
|
const subscriptionSchema = z.object({
|
|
1731
2775
|
conversationId: z.string().min(1),
|
|
1732
2776
|
createdAtMs: z.number().finite(),
|
|
1733
|
-
events: z.array(
|
|
2777
|
+
events: z.array(eventTypeSchema).min(1),
|
|
1734
2778
|
expiresAtMs: z.number().finite(),
|
|
1735
2779
|
id: z.string().min(1),
|
|
1736
2780
|
intent: z.string().min(1),
|
|
1737
2781
|
label: z.string().min(1),
|
|
1738
|
-
match:
|
|
2782
|
+
match: eventMatchSchema.optional(),
|
|
1739
2783
|
namespace: z.string().min(1),
|
|
1740
2784
|
identifier: z.string().min(1),
|
|
1741
2785
|
resourceType: z.string().min(1),
|
|
@@ -1746,16 +2790,16 @@ function digest(value) {
|
|
|
1746
2790
|
return createHash("sha256").update(value).digest("hex").slice(0, 32);
|
|
1747
2791
|
}
|
|
1748
2792
|
function subscriptionKey(id) {
|
|
1749
|
-
return `${
|
|
2793
|
+
return `${EVENT_PREFIX}:record:${id}`;
|
|
1750
2794
|
}
|
|
1751
2795
|
function subscriptionLockKey(id) {
|
|
1752
|
-
return `${
|
|
2796
|
+
return `${EVENT_PREFIX}:lock:${id}`;
|
|
1753
2797
|
}
|
|
1754
2798
|
function resourceIndexKey(namespace, identifier) {
|
|
1755
|
-
return `${
|
|
2799
|
+
return `${EVENT_PREFIX}:resource:${digest(`${namespace}\0${identifier}`)}`;
|
|
1756
2800
|
}
|
|
1757
2801
|
function conversationIndexKey(conversationId) {
|
|
1758
|
-
return `${
|
|
2802
|
+
return `${EVENT_PREFIX}:conversation:${digest(conversationId)}`;
|
|
1759
2803
|
}
|
|
1760
2804
|
function indexLockKey(key) {
|
|
1761
2805
|
return `${key}:lock`;
|
|
@@ -1769,7 +2813,7 @@ async function acquireSubscriptionLock(state, subscriptionId, waitDeadlineMs = D
|
|
|
1769
2813
|
while (true) {
|
|
1770
2814
|
const lock = await state.acquireLock(subscriptionLockKey(subscriptionId), SUBSCRIPTION_LOCK_TTL_MS);
|
|
1771
2815
|
if (lock) return lock;
|
|
1772
|
-
if (Date.now() >= waitDeadlineMs) throw new Error(`Could not acquire
|
|
2816
|
+
if (Date.now() >= waitDeadlineMs) throw new Error(`Could not acquire watch lock for ${subscriptionId}`);
|
|
1773
2817
|
await sleep(SUBSCRIPTION_LOCK_RETRY_MS);
|
|
1774
2818
|
}
|
|
1775
2819
|
}
|
|
@@ -1783,7 +2827,7 @@ async function withSubscriptionLock(state, subscriptionId, callback, waitDeadlin
|
|
|
1783
2827
|
if (heartbeat) return;
|
|
1784
2828
|
heartbeat = state.extendLock(lock, SUBSCRIPTION_LOCK_TTL_MS).then((extended) => {
|
|
1785
2829
|
if (extended) heartbeatError = void 0;
|
|
1786
|
-
else lockLostError = /* @__PURE__ */ new Error(`
|
|
2830
|
+
else lockLostError = /* @__PURE__ */ new Error(`Watch lock was lost for ${subscriptionId}`);
|
|
1787
2831
|
}).catch((error) => {
|
|
1788
2832
|
heartbeatError = error instanceof Error ? error : /* @__PURE__ */ new Error(`Failed to extend subscription lock: ${String(error)}`);
|
|
1789
2833
|
}).finally(() => {
|
|
@@ -1813,12 +2857,12 @@ async function readSubscriptionIdIndex(state, key) {
|
|
|
1813
2857
|
}
|
|
1814
2858
|
function buildSubscriptionId(input) {
|
|
1815
2859
|
const eventKey = [...new Set(input.events)].sort().join("\0");
|
|
1816
|
-
const matchKey =
|
|
2860
|
+
const matchKey = stableEventMatchKey(input.match);
|
|
1817
2861
|
return `resub_${digest(matchKey ? `${input.namespace}\0${input.identifier}\0${input.conversationId}\0${eventKey}\0${matchKey}` : `${input.namespace}\0${input.identifier}\0${input.conversationId}\0${eventKey}`)}`;
|
|
1818
2862
|
}
|
|
1819
2863
|
async function withIndexLock(state, key, callback) {
|
|
1820
2864
|
const lock = await state.acquireLock(indexLockKey(key), INDEX_LOCK_TTL_MS);
|
|
1821
|
-
if (!lock) throw new Error(`Could not acquire
|
|
2865
|
+
if (!lock) throw new Error(`Could not acquire event index lock for ${key}`);
|
|
1822
2866
|
try {
|
|
1823
2867
|
return await callback();
|
|
1824
2868
|
} finally {
|
|
@@ -1850,16 +2894,16 @@ function activeAt(subscription, nowMs) {
|
|
|
1850
2894
|
return subscription.status === "active" && subscription.expiresAtMs > nowMs;
|
|
1851
2895
|
}
|
|
1852
2896
|
function matchesEvent(subscription, input) {
|
|
1853
|
-
return subscription.namespace === input.namespace && subscription.identifier === input.identifier && subscription.events.includes(input.eventType) && activeAt(subscription, input.nowMs) &&
|
|
2897
|
+
return subscription.namespace === input.namespace && subscription.identifier === input.identifier && subscription.events.includes(input.eventType) && activeAt(subscription, input.nowMs) && eventMatches(subscription.match, input.data);
|
|
1854
2898
|
}
|
|
1855
2899
|
/** Create or replace the active subscription for one conversation/resource/event set. */
|
|
1856
|
-
async function
|
|
2900
|
+
async function createWatch(input, options = {}) {
|
|
1857
2901
|
const state = options.state ?? getStateAdapter();
|
|
1858
2902
|
await state.connect();
|
|
1859
2903
|
const nowMs = options.nowMs ?? Date.now();
|
|
1860
2904
|
const events = [...new Set(input.events.map((event) => event.trim()))].filter(Boolean);
|
|
1861
|
-
if (events.length === 0) throw new Error("
|
|
1862
|
-
if (input.expiresAtMs <= nowMs) throw new Error("
|
|
2905
|
+
if (events.length === 0) throw new Error("Watch requires at least one event");
|
|
2906
|
+
if (input.expiresAtMs <= nowMs) throw new Error("Watch expiry must be in the future");
|
|
1863
2907
|
const match = input.match && Object.keys(input.match).length > 0 ? input.match : void 0;
|
|
1864
2908
|
const id = buildSubscriptionId({
|
|
1865
2909
|
conversationId: input.conversationId,
|
|
@@ -1890,7 +2934,7 @@ async function createResourceEventSubscription(input, options = {}) {
|
|
|
1890
2934
|
return parsed;
|
|
1891
2935
|
}
|
|
1892
2936
|
/** List active subscriptions bound to one conversation. */
|
|
1893
|
-
async function
|
|
2937
|
+
async function listWatches(input) {
|
|
1894
2938
|
const state = input.state ?? getStateAdapter();
|
|
1895
2939
|
await state.connect();
|
|
1896
2940
|
const nowMs = input.nowMs ?? Date.now();
|
|
@@ -1898,7 +2942,7 @@ async function listResourceEventSubscriptions(input) {
|
|
|
1898
2942
|
return (await Promise.all(ids.map(async (id) => parseSubscription(await state.get(subscriptionKey(id)))))).filter((record) => record !== void 0 && record.conversationId === input.conversationId && activeAt(record, nowMs)).sort((left, right) => left.createdAtMs - right.createdAtMs);
|
|
1899
2943
|
}
|
|
1900
2944
|
/** Cancel a current-conversation subscription and remove it from match indexes. */
|
|
1901
|
-
async function
|
|
2945
|
+
async function cancelWatch(input) {
|
|
1902
2946
|
const state = input.state ?? getStateAdapter();
|
|
1903
2947
|
await state.connect();
|
|
1904
2948
|
return await withSubscriptionLock(state, input.id, async () => {
|
|
@@ -1916,10 +2960,10 @@ async function cancelResourceEventSubscription(input) {
|
|
|
1916
2960
|
return next;
|
|
1917
2961
|
});
|
|
1918
2962
|
}
|
|
1919
|
-
/** Cancel every active
|
|
2963
|
+
/** Cancel every active watch bound to one conversation. */
|
|
1920
2964
|
async function cancelSubscriptions(input) {
|
|
1921
|
-
const subscriptions = await
|
|
1922
|
-
for (const subscription of subscriptions) await
|
|
2965
|
+
const subscriptions = await listWatches(input);
|
|
2966
|
+
for (const subscription of subscriptions) await cancelWatch({
|
|
1923
2967
|
conversationId: input.conversationId,
|
|
1924
2968
|
id: subscription.id,
|
|
1925
2969
|
nowMs: input.nowMs,
|
|
@@ -1930,7 +2974,7 @@ async function cancelSubscriptions(input) {
|
|
|
1930
2974
|
* Collect match keys from active watches for these identifiers and event types.
|
|
1931
2975
|
* Does not evaluate match values — only reports keys that filters use.
|
|
1932
2976
|
*/
|
|
1933
|
-
async function
|
|
2977
|
+
async function collectEventMatchKeys(input) {
|
|
1934
2978
|
const state = input.state ?? getStateAdapter();
|
|
1935
2979
|
await state.connect();
|
|
1936
2980
|
const nowMs = input.nowMs ?? Date.now();
|
|
@@ -1952,8 +2996,8 @@ async function collectResourceEventMatchKeys(input) {
|
|
|
1952
2996
|
}
|
|
1953
2997
|
return [...keys].sort();
|
|
1954
2998
|
}
|
|
1955
|
-
/** Find active subscriptions interested in a normalized
|
|
1956
|
-
async function
|
|
2999
|
+
/** Find active subscriptions interested in a normalized event. */
|
|
3000
|
+
async function findMatchingWatches(input) {
|
|
1957
3001
|
const state = input.state ?? getStateAdapter();
|
|
1958
3002
|
await state.connect();
|
|
1959
3003
|
const nowMs = input.nowMs ?? Date.now();
|
|
@@ -1967,7 +3011,7 @@ async function findMatchingResourceEventSubscriptions(input) {
|
|
|
1967
3011
|
}));
|
|
1968
3012
|
}
|
|
1969
3013
|
/** Recheck and deliver a matched subscription while holding its status lock. */
|
|
1970
|
-
async function
|
|
3014
|
+
async function deliverWatch(input) {
|
|
1971
3015
|
const state = input.state ?? getStateAdapter();
|
|
1972
3016
|
await state.connect();
|
|
1973
3017
|
return await withSubscriptionLock(state, input.subscription.id, async () => {
|
|
@@ -1997,15 +3041,15 @@ async function deliverResourceEventSubscription(input) {
|
|
|
1997
3041
|
}, input.waitDeadlineMs);
|
|
1998
3042
|
}
|
|
1999
3043
|
//#endregion
|
|
2000
|
-
//#region src/chat/
|
|
2001
|
-
/** Default lifetime for temporary
|
|
3044
|
+
//#region src/chat/events/tool-support.ts
|
|
3045
|
+
/** Default lifetime for temporary watches. */
|
|
2002
3046
|
const RESOURCE_SUBSCRIPTION_DEFAULT_TTL_MS = 12096e5;
|
|
2003
|
-
/** Hard upper bound for temporary
|
|
3047
|
+
/** Hard upper bound for temporary watches. */
|
|
2004
3048
|
const RESOURCE_SUBSCRIPTION_MAX_TTL_MS = 2592e6;
|
|
2005
3049
|
const STOP_WATCHING_TOOL_NAME = "stopWatchingResources";
|
|
2006
3050
|
const RESOURCE_WATCH_TOOL_SOURCE = {
|
|
2007
3051
|
id: "resource-watches",
|
|
2008
|
-
description: "Inspect or stop
|
|
3052
|
+
description: "Inspect or stop watches for the current conversation."
|
|
2009
3053
|
};
|
|
2010
3054
|
//#endregion
|
|
2011
3055
|
//#region src/chat/slack/tool-support/context.ts
|
|
@@ -2534,24 +3578,24 @@ function verifySlackDirectCredentialSubject(input) {
|
|
|
2534
3578
|
])), binding.signature);
|
|
2535
3579
|
}
|
|
2536
3580
|
/** Bind a delegated user subject to one scheduler task dispatch. */
|
|
2537
|
-
function
|
|
3581
|
+
function bindScheduledAutomationCredentialSubject(input) {
|
|
2538
3582
|
const secret = getCredentialSubjectSecret();
|
|
2539
3583
|
const plugin = input.plugin.trim();
|
|
2540
3584
|
const userId = parseActorUserId(input.subject.userId);
|
|
2541
|
-
if (!secret || plugin !== "scheduler" || !userId || input.subject.allowedWhen !== "scheduled-
|
|
3585
|
+
if (!secret || plugin !== "scheduler" || !userId || input.subject.allowedWhen !== "scheduled-automation") return;
|
|
2542
3586
|
const taskId = input.subject.taskId;
|
|
2543
3587
|
if (!taskId || taskId !== taskId.trim()) return;
|
|
2544
3588
|
return {
|
|
2545
3589
|
type: "user",
|
|
2546
3590
|
userId,
|
|
2547
|
-
allowedWhen: "scheduled-
|
|
3591
|
+
allowedWhen: "scheduled-automation",
|
|
2548
3592
|
taskId,
|
|
2549
3593
|
binding: {
|
|
2550
|
-
type: "scheduled-
|
|
3594
|
+
type: "scheduled-automation",
|
|
2551
3595
|
plugin,
|
|
2552
3596
|
taskId,
|
|
2553
3597
|
signature: signPayload(secret, buildPayload([
|
|
2554
|
-
"scheduled-
|
|
3598
|
+
"scheduled-automation",
|
|
2555
3599
|
plugin,
|
|
2556
3600
|
taskId,
|
|
2557
3601
|
userId
|
|
@@ -2560,37 +3604,37 @@ function bindScheduledTaskCredentialSubject(input) {
|
|
|
2560
3604
|
};
|
|
2561
3605
|
}
|
|
2562
3606
|
/** Verify that a delegated subject was signed for one scheduler task. */
|
|
2563
|
-
function
|
|
3607
|
+
function verifyScheduledAutomationCredentialSubject(input) {
|
|
2564
3608
|
const secret = getCredentialSubjectSecret();
|
|
2565
3609
|
const { subject } = input;
|
|
2566
3610
|
const binding = subject.binding;
|
|
2567
|
-
if (!secret || input.plugin !== "scheduler" || subject.type !== "user" || !isActorUserId(subject.userId) || subject.allowedWhen !== "scheduled-
|
|
3611
|
+
if (!secret || input.plugin !== "scheduler" || subject.type !== "user" || !isActorUserId(subject.userId) || subject.allowedWhen !== "scheduled-automation" || !subject.taskId || binding.type !== "scheduled-automation" || binding.plugin !== input.plugin || binding.taskId !== subject.taskId) return false;
|
|
2568
3612
|
return timingSafeMatch(signPayload(secret, buildPayload([
|
|
2569
|
-
"scheduled-
|
|
3613
|
+
"scheduled-automation",
|
|
2570
3614
|
binding.plugin,
|
|
2571
3615
|
binding.taskId,
|
|
2572
3616
|
subject.userId
|
|
2573
3617
|
])), binding.signature);
|
|
2574
3618
|
}
|
|
2575
|
-
/** Bind a delegated user subject to one event
|
|
2576
|
-
function
|
|
3619
|
+
/** Bind a delegated user subject to one event automation dispatch. */
|
|
3620
|
+
function bindEventAutomationCredentialSubject(input) {
|
|
2577
3621
|
const secret = getCredentialSubjectSecret();
|
|
2578
3622
|
const plugin = input.plugin.trim();
|
|
2579
3623
|
const userId = parseActorUserId(input.subject.userId);
|
|
2580
|
-
if (!secret || plugin !== "junior" || !userId || input.subject.allowedWhen !== "event-
|
|
3624
|
+
if (!secret || plugin !== "junior" || !userId || input.subject.allowedWhen !== "event-automation") return;
|
|
2581
3625
|
const taskId = input.subject.taskId;
|
|
2582
3626
|
if (!taskId || taskId !== taskId.trim()) return;
|
|
2583
3627
|
return {
|
|
2584
3628
|
type: "user",
|
|
2585
3629
|
userId,
|
|
2586
|
-
allowedWhen: "event-
|
|
3630
|
+
allowedWhen: "event-automation",
|
|
2587
3631
|
taskId,
|
|
2588
3632
|
binding: {
|
|
2589
|
-
type: "event-
|
|
3633
|
+
type: "event-automation",
|
|
2590
3634
|
plugin,
|
|
2591
3635
|
taskId,
|
|
2592
3636
|
signature: signPayload(secret, buildPayload([
|
|
2593
|
-
"event-
|
|
3637
|
+
"event-automation",
|
|
2594
3638
|
plugin,
|
|
2595
3639
|
taskId,
|
|
2596
3640
|
userId
|
|
@@ -2598,14 +3642,14 @@ function bindEventTaskCredentialSubject(input) {
|
|
|
2598
3642
|
}
|
|
2599
3643
|
};
|
|
2600
3644
|
}
|
|
2601
|
-
/** Verify that a delegated subject was signed for one event
|
|
2602
|
-
function
|
|
3645
|
+
/** Verify that a delegated subject was signed for one event automation. */
|
|
3646
|
+
function verifyEventAutomationCredentialSubject(input) {
|
|
2603
3647
|
const secret = getCredentialSubjectSecret();
|
|
2604
3648
|
const { subject } = input;
|
|
2605
3649
|
const binding = subject.binding;
|
|
2606
|
-
if (!secret || input.plugin !== "junior" || subject.type !== "user" || !isActorUserId(subject.userId) || subject.allowedWhen !== "event-
|
|
3650
|
+
if (!secret || input.plugin !== "junior" || subject.type !== "user" || !isActorUserId(subject.userId) || subject.allowedWhen !== "event-automation" || !subject.taskId || binding.type !== "event-automation" || binding.plugin !== input.plugin || binding.taskId !== subject.taskId) return false;
|
|
2607
3651
|
return timingSafeMatch(signPayload(secret, buildPayload([
|
|
2608
|
-
"event-
|
|
3652
|
+
"event-automation",
|
|
2609
3653
|
binding.plugin,
|
|
2610
3654
|
binding.taskId,
|
|
2611
3655
|
subject.userId
|
|
@@ -3093,6 +4137,28 @@ function createCodeChangePublisher(provider) {
|
|
|
3093
4137
|
};
|
|
3094
4138
|
}
|
|
3095
4139
|
//#endregion
|
|
4140
|
+
//#region src/chat/briefs/registration.ts
|
|
4141
|
+
var registration_exports = /* @__PURE__ */ __exportAll({
|
|
4142
|
+
coreTaskRegistrations: () => coreTaskRegistrations,
|
|
4143
|
+
isBriefsEnabled: () => isBriefsEnabled,
|
|
4144
|
+
setBriefsConfig: () => setBriefsConfig
|
|
4145
|
+
});
|
|
4146
|
+
let configuredBriefs = {};
|
|
4147
|
+
/** Replace app-level Brief settings and return the previous setting. */
|
|
4148
|
+
function setBriefsConfig(config) {
|
|
4149
|
+
const previous = { ...configuredBriefs };
|
|
4150
|
+
configuredBriefs = config ? { ...config } : {};
|
|
4151
|
+
return previous;
|
|
4152
|
+
}
|
|
4153
|
+
/** Return whether automatic Brief generation is enabled. */
|
|
4154
|
+
function isBriefsEnabled() {
|
|
4155
|
+
return configuredBriefs.enabled === true;
|
|
4156
|
+
}
|
|
4157
|
+
/** Return enabled core task registrations. */
|
|
4158
|
+
function coreTaskRegistrations() {
|
|
4159
|
+
return isBriefsEnabled() ? [briefsTaskRegistration] : [];
|
|
4160
|
+
}
|
|
4161
|
+
//#endregion
|
|
3096
4162
|
//#region src/chat/plugins/agent-hooks.ts
|
|
3097
4163
|
var agent_hooks_exports = /* @__PURE__ */ __exportAll({
|
|
3098
4164
|
PluginHookDeniedError: () => PluginHookDeniedError,
|
|
@@ -3190,9 +4256,9 @@ function pluginInvocationContext(context) {
|
|
|
3190
4256
|
destination: context.destination,
|
|
3191
4257
|
source: context.source
|
|
3192
4258
|
};
|
|
3193
|
-
case "
|
|
3194
|
-
case "
|
|
3195
|
-
case "
|
|
4259
|
+
case "event":
|
|
4260
|
+
case "scheduled_automation":
|
|
4261
|
+
case "event_automation":
|
|
3196
4262
|
case "plugin_dispatch":
|
|
3197
4263
|
case "agent_invocation": return {
|
|
3198
4264
|
...common,
|
|
@@ -3296,7 +4362,7 @@ function validatePlugins(plugins) {
|
|
|
3296
4362
|
const name = plugin.manifest.name;
|
|
3297
4363
|
if (!PLUGIN_NAME_RE.test(name)) throw new Error(`Plugin name "${name}" must be a lowercase plugin identifier`);
|
|
3298
4364
|
if (seen.has(name)) throw new Error(`Duplicate plugin name "${name}"`);
|
|
3299
|
-
if (plugin.
|
|
4365
|
+
if (plugin.events !== void 0 && !pluginEventsSchema.safeParse(plugin.events).success) throw new Error(`Plugin "${name}" events is invalid`);
|
|
3300
4366
|
for (const [taskName, task] of Object.entries(plugin.tasks ?? {})) {
|
|
3301
4367
|
if (!PLUGIN_TOOL_NAME_RE.test(taskName)) throw new Error(`Plugin task "${taskName}" from plugin "${name}" must be a camelCase identifier`);
|
|
3302
4368
|
if (typeof task.run !== "function") throw new Error(`Plugin task "${taskName}" from plugin "${name}" must define a run function`);
|
|
@@ -3451,22 +4517,22 @@ function getPluginTools(context, sandbox = createSandboxCapability(context.works
|
|
|
3451
4517
|
plugin: pluginName
|
|
3452
4518
|
});
|
|
3453
4519
|
const mcp = pluginMcpContext(plugin, context);
|
|
3454
|
-
const canSubscribe = Boolean(plugin.
|
|
3455
|
-
const
|
|
4520
|
+
const canSubscribe = Boolean(plugin.events) && plugin.events?.isEnabled?.() !== false;
|
|
4521
|
+
const events = {
|
|
3456
4522
|
canSubscribe,
|
|
3457
4523
|
async subscribe(input) {
|
|
3458
|
-
if (!canSubscribe) throw new Error("
|
|
3459
|
-
const registration = plugin.
|
|
4524
|
+
if (!canSubscribe) throw new Error("Watches are not available in this conversation.");
|
|
4525
|
+
const registration = plugin.events;
|
|
3460
4526
|
const resourceType = registration?.resourceTypes.find((candidate) => candidate.type === input.resource.type);
|
|
3461
|
-
if (input.resource.namespace !== pluginName || !resourceType || input.events.length === 0 || input.events.some((eventType) => !resourceType.supportedEvents.includes(eventType))) throw new Error("
|
|
3462
|
-
const subscription = await
|
|
4527
|
+
if (input.resource.namespace !== pluginName || !resourceType || input.events.length === 0 || input.events.some((eventType) => !resourceType.supportedEvents.includes(eventType))) throw new Error("Watch contains an event or resource that the plugin does not support.");
|
|
4528
|
+
const subscription = await createWatch({
|
|
3463
4529
|
conversationId: context.conversationId,
|
|
3464
4530
|
events: input.events,
|
|
3465
4531
|
expiresAtMs: Date.now() + RESOURCE_SUBSCRIPTION_DEFAULT_TTL_MS,
|
|
3466
4532
|
intent: input.intent,
|
|
3467
4533
|
label: input.resource.label,
|
|
3468
4534
|
namespace: pluginName,
|
|
3469
|
-
identifier:
|
|
4535
|
+
identifier: normalizeEventIdentifier(registration, input.resource.identifier),
|
|
3470
4536
|
resourceType: input.resource.type
|
|
3471
4537
|
});
|
|
3472
4538
|
return {
|
|
@@ -3487,7 +4553,7 @@ function getPluginTools(context, sandbox = createSandboxCapability(context.works
|
|
|
3487
4553
|
egress: context.egress,
|
|
3488
4554
|
...mcp ? { mcp } : void 0,
|
|
3489
4555
|
model: createPluginModel(pluginName, plugin.model),
|
|
3490
|
-
|
|
4556
|
+
events,
|
|
3491
4557
|
sandbox,
|
|
3492
4558
|
state: createPluginState(pluginName),
|
|
3493
4559
|
users: { resolveActor },
|
|
@@ -3523,9 +4589,9 @@ function getPluginTools(context, sandbox = createSandboxCapability(context.works
|
|
|
3523
4589
|
source: context.source
|
|
3524
4590
|
};
|
|
3525
4591
|
break;
|
|
3526
|
-
case "
|
|
3527
|
-
case "
|
|
3528
|
-
case "
|
|
4592
|
+
case "event":
|
|
4593
|
+
case "scheduled_automation":
|
|
4594
|
+
case "event_automation":
|
|
3529
4595
|
case "plugin_dispatch":
|
|
3530
4596
|
case "agent_invocation": pluginContext = {
|
|
3531
4597
|
...common,
|
|
@@ -3571,10 +4637,10 @@ function routeMethods(route, pluginName) {
|
|
|
3571
4637
|
if (methods.includes("ALL") && methods.length > 1) throw new Error(`Plugin route "${route.path}" from plugin "${pluginName}" must not combine ALL with explicit methods`);
|
|
3572
4638
|
return methods;
|
|
3573
4639
|
}
|
|
3574
|
-
function
|
|
3575
|
-
const registration = plugin.
|
|
3576
|
-
if (!registration || registration.isEnabled?.() === false) throw new Error(`Plugin "${plugin.manifest.name}" cannot publish
|
|
3577
|
-
if (!registration.resourceTypes.some((resourceType) => resourceType.supportedEvents.includes(eventType))) throw new Error(`Plugin "${plugin.manifest.name}" did not register
|
|
4640
|
+
function requirePublishedEvent(plugin, eventType) {
|
|
4641
|
+
const registration = plugin.events;
|
|
4642
|
+
if (!registration || registration.isEnabled?.() === false) throw new Error(`Plugin "${plugin.manifest.name}" cannot publish events without an active registration`);
|
|
4643
|
+
if (!registration.resourceTypes.some((resourceType) => resourceType.supportedEvents.includes(eventType))) throw new Error(`Plugin "${plugin.manifest.name}" did not register event "${eventType}"`);
|
|
3578
4644
|
}
|
|
3579
4645
|
/** Collect route handlers exposed by plugins for app-level mounting. */
|
|
3580
4646
|
function getPluginRoutes(options) {
|
|
@@ -3593,33 +4659,33 @@ function getPluginRoutes(options) {
|
|
|
3593
4659
|
plugin: pluginName
|
|
3594
4660
|
}) },
|
|
3595
4661
|
codeChanges: createCodeChangePublisher(pluginName),
|
|
3596
|
-
|
|
4662
|
+
events: {
|
|
3597
4663
|
async hasMatch(event) {
|
|
3598
|
-
if (!options.
|
|
3599
|
-
const parsed =
|
|
3600
|
-
|
|
3601
|
-
return await options.
|
|
4664
|
+
if (!options.events.hasMatch) return false;
|
|
4665
|
+
const parsed = eventInputSchema.parse(event);
|
|
4666
|
+
requirePublishedEvent(plugin, parsed.eventType);
|
|
4667
|
+
return await options.events.hasMatch({
|
|
3602
4668
|
...parsed,
|
|
3603
|
-
identifier:
|
|
4669
|
+
identifier: normalizeEventIdentifier(plugin.events, parsed.identifier),
|
|
3604
4670
|
namespace: pluginName
|
|
3605
4671
|
});
|
|
3606
4672
|
},
|
|
3607
4673
|
async neededMatchKeys(input) {
|
|
3608
|
-
if (!options.
|
|
3609
|
-
const identifiers = [...new Set(input.identifiers.map((identifier) =>
|
|
4674
|
+
if (!options.events.neededMatchKeys) return [];
|
|
4675
|
+
const identifiers = [...new Set(input.identifiers.map((identifier) => normalizeEventIdentifier(plugin.events, identifier)).filter(Boolean))];
|
|
3610
4676
|
if (identifiers.length === 0 || input.eventTypes.length === 0) return [];
|
|
3611
|
-
return await options.
|
|
4677
|
+
return await options.events.neededMatchKeys({
|
|
3612
4678
|
eventTypes: input.eventTypes,
|
|
3613
4679
|
identifiers,
|
|
3614
4680
|
namespace: pluginName
|
|
3615
4681
|
});
|
|
3616
4682
|
},
|
|
3617
4683
|
async publish(event) {
|
|
3618
|
-
const parsed =
|
|
3619
|
-
|
|
3620
|
-
await options.
|
|
4684
|
+
const parsed = eventInputSchema.parse(event);
|
|
4685
|
+
requirePublishedEvent(plugin, parsed.eventType);
|
|
4686
|
+
await options.events.publish({
|
|
3621
4687
|
...parsed,
|
|
3622
|
-
identifier:
|
|
4688
|
+
identifier: normalizeEventIdentifier(plugin.events, parsed.identifier),
|
|
3623
4689
|
namespace: pluginName
|
|
3624
4690
|
});
|
|
3625
4691
|
}
|
|
@@ -3817,7 +4883,7 @@ function failedOperationalReport(args) {
|
|
|
3817
4883
|
/** Collect read-only operational summaries exposed by plugins. */
|
|
3818
4884
|
async function getPluginOperationalReports(nowMs) {
|
|
3819
4885
|
const reports = [];
|
|
3820
|
-
for (const plugin of getPlugins()) {
|
|
4886
|
+
for (const plugin of [...coreTaskRegistrations(), ...getPlugins()]) {
|
|
3821
4887
|
const pluginName = plugin.manifest.name;
|
|
3822
4888
|
const hook = plugin.hooks?.operationalReport;
|
|
3823
4889
|
if (!hook) continue;
|
|
@@ -3960,6 +5026,7 @@ function createPluginHookRunner(input = {}) {
|
|
|
3960
5026
|
paths.add(key);
|
|
3961
5027
|
}
|
|
3962
5028
|
const sandboxCapability = createSandboxCapability(sandbox, signal);
|
|
5029
|
+
const finalizers = [];
|
|
3963
5030
|
for (const plugin of loaded) {
|
|
3964
5031
|
const hook = plugin.hooks?.workspacePrepare;
|
|
3965
5032
|
if (!hook) continue;
|
|
@@ -3968,12 +5035,16 @@ function createPluginHookRunner(input = {}) {
|
|
|
3968
5035
|
repo: repo.repo
|
|
3969
5036
|
}));
|
|
3970
5037
|
if (selected.length === 0) continue;
|
|
3971
|
-
await hook({
|
|
5038
|
+
const finalize = await hook({
|
|
3972
5039
|
...basePluginContext(plugin),
|
|
3973
5040
|
repos: selected,
|
|
3974
5041
|
sandbox: sandboxCapability
|
|
3975
5042
|
});
|
|
5043
|
+
if (finalize) finalizers.push(finalize);
|
|
3976
5044
|
}
|
|
5045
|
+
return async () => {
|
|
5046
|
+
for (const finalize of finalizers) await finalize();
|
|
5047
|
+
};
|
|
3977
5048
|
},
|
|
3978
5049
|
async prepareSandbox(sandbox) {
|
|
3979
5050
|
const sandboxCapability = createSandboxCapability(sandbox);
|
|
@@ -4039,4 +5110,4 @@ function createPluginHookRunner(input = {}) {
|
|
|
4039
5110
|
};
|
|
4040
5111
|
}
|
|
4041
5112
|
//#endregion
|
|
4042
|
-
export {
|
|
5113
|
+
export { resolveViewerUser as $, tryWorkspaceRepoCheckoutPath as A, authenticationLinkedEvent as At, downloadPrivateSlackFile as B, sanitizePostgresJson as Bt, invalidateReadySnapshot as C, readConversationEventPage as Ct, SANDBOX_RUNTIME as D, listConversationAnnotationsById as Dt, setWorkspaceSnapshotBuild as E, listConversationAnnotations as Et, bindSlackDirectCredentialSubject as F, getConversationStore as Ft, isDmChannel as G, resolveRootConversationId as Gt, getHeaderString as H, locationFromRow as Ht, verifyEventAutomationCredentialSubject as I, getDb as It, withSlackRetries as J, upsertLinkedIdentity as Jt, normalizeSlackConversationId as K, deleteProviderIdentityForSlackUser as Kt, verifyScheduledAutomationCredentialSubject as L, getSqlExecutor as Lt, resolveChannelCapabilities as M, db_exports as Mt, bindEventAutomationCredentialSubject as N, getConversationEventStore as Nt, deleteWorkspaceSnapshotBuilders as O, JUNIOR_NATIVE_EVENT_NAMESPACE as Ot, bindScheduledAutomationCredentialSubject as P, getConversationMessageSearchStore as Pt, readActorIdentity as Q, sameDestination as Qt, verifySlackDirectCredentialSubject as R, createSqlConversationEventStore as Rt, WorkspaceValidationError as S, readLatestConversationBrief as St, setWorkspaceSnapshot as T, resolveRootVisibility as Tt, getSlackClient as U, bindProviderConversation as Ut, getFilePermalink as V, withConversationMutationLock as Vt, isConversationScopedChannel as W, parseSessionSource as Wt, runWithWorkspaceTeamId as X, parseDestination as Xt, getWorkspaceTeamId as Y, createSlackDestination as Yt, findUserByEmail as Z, requireSlackDestination as Zt, deleteWorkspace as _, createPluginState as _t, getPluginRoutes as a, STOP_WATCHING_TOOL_NAME as at, listWorkspaces as b, createPluginLogger as bt, getPluginTools as c, collectEventMatchKeys as ct, setPlugins as d, findMatchingWatches as dt, updateViewerDisplayName as et, validatePlugins as f, listWatches as ft, createWorkspace as g, setDashboardConversationLinkOptions as gt, setBriefsConfig as h, getDashboardTaskLink as ht, getPluginApiRoutes as i, RESOURCE_WATCH_TOOL_SOURCE as it, workspaceRepoCheckoutPath as j, authenticationUnlinkedEvent as jt, getWorkspaceSnapshotBuilder as k, agentsInstructionsUpdatedEvent as kt, getPluginUserPromptContributions as l, createWatch as lt, registration_exports as m, getDashboardConversationLink as mt, applyPluginFormatMarkdown as n, RESOURCE_SUBSCRIPTION_DEFAULT_TTL_MS as nt, getPluginSlackConversationLink as o, cancelSubscriptions as ot, coreTaskRegistrations as p, JUNIOR_THREAD_STATE_TTL_MS as pt, runWithSlackInstallationToken as q, upsertIdentity as qt, createPluginHookRunner as r, RESOURCE_SUBSCRIPTION_MAX_TTL_MS as rt, getPluginSystemPromptContributions as s, cancelWatch as st, agent_hooks_exports as t, getSlackToolContext as tt, getPlugins as u, deliverWatch as ut, getWorkspace as v, createPluginEmbedder as vt, loadSnapshotsForProfile as w, buildSentryConversationUrl as wt, updateWorkspace as x, createPluginConversationEvents as xt, getWorkspaceByName as y, createPluginModel as yt, SlackActionError as z, withConversationEventLock as zt };
|