@vellumai/assistant 0.11.8-staging.1 → 0.11.8
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/docs/guardian-request-flow.md +3 -3
- package/openapi.yaml +0 -24
- package/package.json +1 -1
- package/src/__tests__/access-request-seed-content-blocks.test.ts +2 -2
- package/src/__tests__/anthropic-provider.test.ts +2 -0
- package/src/__tests__/channel-availability-routes.test.ts +15 -0
- package/src/__tests__/channel-readiness-routes.test.ts +96 -3
- package/src/__tests__/channel-readiness-service.test.ts +19 -4
- package/src/__tests__/config-loader-backfill.test.ts +2 -2
- package/src/__tests__/credential-security-invariants.test.ts +1 -1
- package/src/__tests__/email-invite-adapter.test.ts +53 -12
- package/src/__tests__/external-binding-chat-name.test.ts +60 -0
- package/src/__tests__/guardian-verify-setup-skill-regression.test.ts +28 -0
- package/src/__tests__/managed-profile-guard.test.ts +1 -1
- package/src/__tests__/model-intents.test.ts +2 -2
- package/src/__tests__/pricing.test.ts +13 -0
- package/src/__tests__/tool-approval-seed-content-blocks.test.ts +16 -3
- package/src/api/responses/home.ts +0 -6
- package/src/config/__tests__/default-profile-catalog.test.ts +1 -1
- package/src/config/call-site-defaults.ts +1 -1
- package/src/config/default-profile-catalog.ts +12 -12
- package/src/config/default-profile-names.ts +2 -2
- package/src/email/byo-email-credential.test.ts +39 -0
- package/src/email/byo-email-credential.ts +76 -0
- package/src/email/registered-inbox.test.ts +272 -0
- package/src/email/registered-inbox.ts +169 -0
- package/src/messaging/providers/slack/approval-source.test.ts +70 -0
- package/src/messaging/providers/slack/approval-source.ts +18 -5
- package/src/notifications/__tests__/guardian-feed-projection.test.ts +13 -14
- package/src/notifications/approval-card-data.ts +13 -5
- package/src/notifications/emit-signal.ts +5 -8
- package/src/notifications/guardian-feed-projection.ts +9 -74
- package/src/notifications/guardian-question-mode.ts +33 -1
- package/src/notifications/home-feed-side-effect.ts +0 -4
- package/src/persistence/external-conversation-store.ts +5 -1
- package/src/providers/anthropic/client.ts +1 -1
- package/src/providers/model-catalog.ts +54 -0
- package/src/providers/model-intents.ts +3 -3
- package/src/runtime/approval-source-link.ts +6 -0
- package/src/runtime/channel-invite-transports/email.ts +10 -9
- package/src/runtime/channel-readiness-service.ts +83 -23
- package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +1 -1
- package/src/runtime/routes/channel-availability-routes.ts +18 -31
- package/src/runtime/routes/credential-routes.ts +3 -0
- package/src/runtime/routes/email-routes.ts +35 -47
- package/src/runtime/verification-outbound-actions.ts +8 -12
- package/src/tools/credentials/store.ts +3 -0
- package/src/util/pricing.ts +2 -1
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { describe, expect, mock, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
// Spread the actual modules so transitive importers of names these
|
|
4
|
+
// factories do not stub keep resolving (a partial factory breaks at
|
|
5
|
+
// import time when the graph gains a new named import).
|
|
6
|
+
const actualDeliveryCrud =
|
|
7
|
+
await import("../../../persistence/delivery-crud.js");
|
|
8
|
+
const actualBindingStore =
|
|
9
|
+
await import("../../../persistence/external-conversation-store.js");
|
|
10
|
+
|
|
11
|
+
let bindingRow: {
|
|
12
|
+
sourceChannel: string;
|
|
13
|
+
externalChatId: string;
|
|
14
|
+
externalChatName: string | null;
|
|
15
|
+
conversationId: string;
|
|
16
|
+
} | null = null;
|
|
17
|
+
|
|
18
|
+
mock.module("../../../persistence/delivery-crud.js", () => ({
|
|
19
|
+
...actualDeliveryCrud,
|
|
20
|
+
getLatestInboundEventReference: () => null,
|
|
21
|
+
}));
|
|
22
|
+
|
|
23
|
+
mock.module("../../../persistence/external-conversation-store.js", () => ({
|
|
24
|
+
...actualBindingStore,
|
|
25
|
+
getBindingByConversation: () => bindingRow,
|
|
26
|
+
}));
|
|
27
|
+
|
|
28
|
+
const { resolveSlackApprovalSource } = await import("./approval-source.js");
|
|
29
|
+
|
|
30
|
+
const HINT = {
|
|
31
|
+
requesterChatId: "C0123CHANNEL",
|
|
32
|
+
sourceMessageId: "1700000000.000100",
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
describe("resolveSlackApprovalSource chat names", () => {
|
|
36
|
+
test("the binding's chat name rides the reference", () => {
|
|
37
|
+
bindingRow = {
|
|
38
|
+
sourceChannel: "slack",
|
|
39
|
+
externalChatId: "C0123CHANNEL",
|
|
40
|
+
externalChatName: "user-feedback",
|
|
41
|
+
conversationId: "conv-1",
|
|
42
|
+
};
|
|
43
|
+
const reference = resolveSlackApprovalSource("conv-1", HINT);
|
|
44
|
+
expect(reference?.sourceChatId).toBe("C0123CHANNEL");
|
|
45
|
+
expect(reference?.sourceChatName).toBe("user-feedback");
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("a binding for a different chat never lends its name", () => {
|
|
49
|
+
bindingRow = {
|
|
50
|
+
sourceChannel: "slack",
|
|
51
|
+
externalChatId: "C0OTHER",
|
|
52
|
+
externalChatName: "somewhere-else",
|
|
53
|
+
conversationId: "conv-1",
|
|
54
|
+
};
|
|
55
|
+
const reference = resolveSlackApprovalSource("conv-1", HINT);
|
|
56
|
+
expect(reference?.sourceChatId).toBe("C0123CHANNEL");
|
|
57
|
+
expect(reference?.sourceChatName).toBeUndefined();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("an unnamed binding leaves the reference nameless", () => {
|
|
61
|
+
bindingRow = {
|
|
62
|
+
sourceChannel: "slack",
|
|
63
|
+
externalChatId: "C0123CHANNEL",
|
|
64
|
+
externalChatName: null,
|
|
65
|
+
conversationId: "conv-1",
|
|
66
|
+
};
|
|
67
|
+
const reference = resolveSlackApprovalSource("conv-1", HINT);
|
|
68
|
+
expect(reference?.sourceChatName).toBeUndefined();
|
|
69
|
+
});
|
|
70
|
+
});
|
|
@@ -26,16 +26,19 @@ import { isSlackTs } from "./message-metadata.js";
|
|
|
26
26
|
|
|
27
27
|
function toReference(
|
|
28
28
|
chatId: string,
|
|
29
|
+
chatName: string | null,
|
|
29
30
|
messageTs: string | undefined,
|
|
30
31
|
threadTs: string | undefined,
|
|
31
32
|
): ApprovalSourceReference {
|
|
33
|
+
const named = chatName ? { sourceChatName: chatName } : {};
|
|
32
34
|
// Without a message ts, the thread root is still a valid anchor.
|
|
33
35
|
const anchorTs = messageTs ?? threadTs;
|
|
34
36
|
if (!anchorTs) {
|
|
35
|
-
return { sourceChatId: chatId };
|
|
37
|
+
return { sourceChatId: chatId, ...named };
|
|
36
38
|
}
|
|
37
39
|
return {
|
|
38
40
|
sourceChatId: chatId,
|
|
41
|
+
...named,
|
|
39
42
|
sourceLink: {
|
|
40
43
|
webUrl: buildSlackPermalink({
|
|
41
44
|
channelId: chatId,
|
|
@@ -50,20 +53,30 @@ export function resolveSlackApprovalSource(
|
|
|
50
53
|
conversationId: string,
|
|
51
54
|
hint: ApprovalSourceHint | undefined,
|
|
52
55
|
): ApprovalSourceReference | null {
|
|
56
|
+
const rawBinding = getBindingByConversation(conversationId);
|
|
57
|
+
const binding = rawBinding?.sourceChannel === "slack" ? rawBinding : null;
|
|
58
|
+
|
|
59
|
+
// The chat's display name lives on the binding row, which ingress
|
|
60
|
+
// upserts from every inbound message. Matched by chat id so a binding
|
|
61
|
+
// that moved to another chat never lends its name.
|
|
62
|
+
const chatNameFor = (chatId: string): string | null =>
|
|
63
|
+
binding?.externalChatId === chatId
|
|
64
|
+
? binding.externalChatName?.trim() || null
|
|
65
|
+
: null;
|
|
66
|
+
|
|
53
67
|
// Exact provenance from the turn's trust context. An absent thread id here
|
|
54
68
|
// is authoritative (the message arrived at the chat root), so no binding
|
|
55
|
-
// lookup is needed.
|
|
69
|
+
// lookup is needed for the reference itself.
|
|
56
70
|
if (hint?.requesterChatId && isSlackTs(hint.sourceMessageId)) {
|
|
57
71
|
return toReference(
|
|
58
72
|
hint.requesterChatId,
|
|
73
|
+
chatNameFor(hint.requesterChatId),
|
|
59
74
|
hint.sourceMessageId,
|
|
60
75
|
isSlackTs(hint.sourceThreadId) ? hint.sourceThreadId : undefined,
|
|
61
76
|
);
|
|
62
77
|
}
|
|
63
78
|
|
|
64
79
|
const inbound = getLatestInboundEventReference(conversationId, "slack");
|
|
65
|
-
const rawBinding = getBindingByConversation(conversationId);
|
|
66
|
-
const binding = rawBinding?.sourceChannel === "slack" ? rawBinding : null;
|
|
67
80
|
|
|
68
81
|
const chatId =
|
|
69
82
|
hint?.requesterChatId ?? inbound?.externalChatId ?? binding?.externalChatId;
|
|
@@ -83,5 +96,5 @@ export function resolveSlackApprovalSource(
|
|
|
83
96
|
? binding.externalThreadId
|
|
84
97
|
: undefined;
|
|
85
98
|
|
|
86
|
-
return toReference(chatId, messageTs, threadTs);
|
|
99
|
+
return toReference(chatId, chatNameFor(chatId), messageTs, threadTs);
|
|
87
100
|
}
|
|
@@ -48,7 +48,7 @@ const {
|
|
|
48
48
|
} = await import("../guardian-feed-projection.js");
|
|
49
49
|
const { appendFeedItem, bulkSetFeedItemStatus, getHomeFeedPath, readHomeFeed } =
|
|
50
50
|
await import("../../home/feed-writer.js");
|
|
51
|
-
const { isPendingGuardianFeedItem } =
|
|
51
|
+
const { GUARDIAN_TERMINAL_REASON_SUPERSEDED, isPendingGuardianFeedItem } =
|
|
52
52
|
await import("../../api/responses/home.js");
|
|
53
53
|
type FeedItem = import("../../api/responses/home.js").FeedItem;
|
|
54
54
|
|
|
@@ -115,7 +115,7 @@ describe("buildPendingGuardianProjection", () => {
|
|
|
115
115
|
requesterLabel: "Alice",
|
|
116
116
|
toolName: "linear_graphql",
|
|
117
117
|
sourceChannel: "slack",
|
|
118
|
-
sourceContextLabel: "
|
|
118
|
+
sourceContextLabel: "#C0123456789",
|
|
119
119
|
});
|
|
120
120
|
});
|
|
121
121
|
|
|
@@ -130,14 +130,12 @@ describe("buildPendingGuardianProjection", () => {
|
|
|
130
130
|
expect(projection?.status).toBe("pending");
|
|
131
131
|
});
|
|
132
132
|
|
|
133
|
-
test("a
|
|
134
|
-
const projection = buildPendingGuardianProjection(
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
status: "sent",
|
|
138
|
-
messageId: "1725100000.000100",
|
|
133
|
+
test("a captured channel name becomes the source context label", () => {
|
|
134
|
+
const projection = buildPendingGuardianProjection({
|
|
135
|
+
...toolApprovalPayload,
|
|
136
|
+
sourceChatName: "user-feedback",
|
|
139
137
|
});
|
|
140
|
-
expect(projection?.
|
|
138
|
+
expect(projection?.sourceContextLabel).toBe("#user-feedback");
|
|
141
139
|
});
|
|
142
140
|
|
|
143
141
|
test("a payload without a requestId projects nothing", () => {
|
|
@@ -154,7 +152,6 @@ describe("buildPendingGuardianProjection", () => {
|
|
|
154
152
|
sourceChannel: "telegram",
|
|
155
153
|
senderIdentifier: "Alice",
|
|
156
154
|
},
|
|
157
|
-
undefined,
|
|
158
155
|
"access_request",
|
|
159
156
|
);
|
|
160
157
|
expect(projection).toMatchObject({
|
|
@@ -250,19 +247,21 @@ describe("writeGuardianFeedReceipt", () => {
|
|
|
250
247
|
expect(item && isPendingGuardianFeedItem(item)).toBe(false);
|
|
251
248
|
});
|
|
252
249
|
|
|
253
|
-
test("a superseded auto-deny carries its reason
|
|
250
|
+
test("a superseded auto-deny carries its reason", async () => {
|
|
254
251
|
await appendFeedItem(pendingGuardianItem("req-4"));
|
|
255
252
|
await writeGuardianFeedReceipt({
|
|
256
253
|
requestId: "req-4",
|
|
257
254
|
status: "denied",
|
|
258
255
|
decidedAction: "reject",
|
|
259
|
-
terminalReason:
|
|
256
|
+
terminalReason: GUARDIAN_TERMINAL_REASON_SUPERSEDED,
|
|
260
257
|
});
|
|
261
258
|
const item = readHomeFeed().items.find(
|
|
262
259
|
(i) => i.id === guardianFeedItemId("req-4"),
|
|
263
260
|
);
|
|
264
|
-
expect(item?.guardianRequest?.terminalReason).toBe(
|
|
265
|
-
|
|
261
|
+
expect(item?.guardianRequest?.terminalReason).toBe(
|
|
262
|
+
GUARDIAN_TERMINAL_REASON_SUPERSEDED,
|
|
263
|
+
);
|
|
264
|
+
expect(item?.guardianRequest?.status).toBe("denied");
|
|
266
265
|
});
|
|
267
266
|
|
|
268
267
|
test("a request with no item resolves true (nothing to retry)", async () => {
|
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
buildGuardianRequestCodeInstruction,
|
|
32
32
|
buildQuestionDeliveryText,
|
|
33
33
|
buildToolApprovalSourceView,
|
|
34
|
+
describeSlackChatLabel,
|
|
34
35
|
type GuardianQuestionPayload,
|
|
35
36
|
type LenientToolApprovalPayload,
|
|
36
37
|
LenientToolApprovalPayloadSchema,
|
|
@@ -151,12 +152,17 @@ function sourceMetadataRow(
|
|
|
151
152
|
channel: string | undefined,
|
|
152
153
|
slackChatId: string | undefined,
|
|
153
154
|
isSlackDm: boolean,
|
|
155
|
+
chatName?: string,
|
|
154
156
|
): { label: string; value: string } | undefined {
|
|
155
|
-
if (channel === "slack"
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
157
|
+
if (channel === "slack") {
|
|
158
|
+
const chat = describeSlackChatLabel({
|
|
159
|
+
chatId: slackChatId,
|
|
160
|
+
chatName,
|
|
161
|
+
isSlackDm,
|
|
162
|
+
});
|
|
163
|
+
if (chat) {
|
|
164
|
+
return { label: "Source", value: `Slack · ${chat}` };
|
|
165
|
+
}
|
|
160
166
|
}
|
|
161
167
|
if (channel) {
|
|
162
168
|
return { label: "Source", value: channel };
|
|
@@ -298,6 +304,7 @@ function extractToolApprovalCard(
|
|
|
298
304
|
nonEmpty(p.sourceChannel),
|
|
299
305
|
sourceView?.chatId,
|
|
300
306
|
sourceView?.isSlackDm ?? false,
|
|
307
|
+
sourceView?.chatName,
|
|
301
308
|
);
|
|
302
309
|
if (sourceRow) {
|
|
303
310
|
metadata.push(sourceRow);
|
|
@@ -364,6 +371,7 @@ function extractQuestionCard(
|
|
|
364
371
|
nonEmpty(p.sourceChannel),
|
|
365
372
|
sourceView?.chatId,
|
|
366
373
|
sourceView?.isSlackDm ?? false,
|
|
374
|
+
sourceView?.chatName,
|
|
367
375
|
);
|
|
368
376
|
if (sourceRow) {
|
|
369
377
|
metadata.push(sourceRow);
|
|
@@ -547,14 +547,11 @@ export async function emitNotificationSignal<TEventName extends string>(
|
|
|
547
547
|
const vellumDelivery = dispatchResult.deliveryResults.find(
|
|
548
548
|
(r) => r.channel === "vellum",
|
|
549
549
|
);
|
|
550
|
-
await writeHomeFeedItemForSignal(
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
).catch((err) => {
|
|
556
|
-
log.warn({ err, signalId }, "writeHomeFeedItemForSignal threw");
|
|
557
|
-
});
|
|
550
|
+
await writeHomeFeedItemForSignal(signal, decision, vellumDelivery).catch(
|
|
551
|
+
(err) => {
|
|
552
|
+
log.warn({ err, signalId }, "writeHomeFeedItemForSignal threw");
|
|
553
|
+
},
|
|
554
|
+
);
|
|
558
555
|
|
|
559
556
|
log.info(
|
|
560
557
|
{
|
|
@@ -28,27 +28,22 @@ import {
|
|
|
28
28
|
type GuardianRequestWire,
|
|
29
29
|
listGuardianRequests,
|
|
30
30
|
} from "../channels/gateway-guardian-requests.js";
|
|
31
|
-
import { getConfig } from "../config/loader.js";
|
|
32
31
|
import {
|
|
33
32
|
appendFeedItem,
|
|
34
33
|
patchFeedItemContent,
|
|
35
34
|
readHomeFeed,
|
|
36
35
|
} from "../home/feed-writer.js";
|
|
37
|
-
import { buildSlackMessageDeepLinks } from "../messaging/providers/slack/deep-link.js";
|
|
38
|
-
import {
|
|
39
|
-
DEFAULT_USER_REFERENCE,
|
|
40
|
-
resolveGuardianName,
|
|
41
|
-
} from "../prompts/user-reference.js";
|
|
42
36
|
import { getLogger } from "../util/logger.js";
|
|
43
37
|
import {
|
|
44
38
|
buildToolApprovalSourceView,
|
|
39
|
+
describeSlackChatLabel,
|
|
45
40
|
type GuardianQuestionRequestKind,
|
|
46
41
|
LenientToolApprovalPayloadSchema,
|
|
47
42
|
resolveGuardianInstructionModeFromFields,
|
|
48
43
|
resolveGuardianQuestionInstructionMode,
|
|
44
|
+
type ToolApprovalSourceView,
|
|
49
45
|
} from "./guardian-question-mode.js";
|
|
50
46
|
import { readPayloadString } from "./notification-utils.js";
|
|
51
|
-
import type { NotificationDeliveryResult } from "./types.js";
|
|
52
47
|
|
|
53
48
|
const log = getLogger("guardian-feed-projection");
|
|
54
49
|
|
|
@@ -90,29 +85,13 @@ export function requestIdFromGuardianFeedItemId(itemId: string): string | null {
|
|
|
90
85
|
: null;
|
|
91
86
|
}
|
|
92
87
|
|
|
93
|
-
/**
|
|
94
|
-
* Guardian display name for receipt copy, or undefined when only the
|
|
95
|
-
* prompt-voice fallback ("my human") is available. A receipt with no
|
|
96
|
-
* decider label renders as the bare status word.
|
|
97
|
-
*/
|
|
98
|
-
function guardianReceiptName(): string | undefined {
|
|
99
|
-
const name = resolveGuardianName();
|
|
100
|
-
return name === DEFAULT_USER_REFERENCE ? undefined : name;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
88
|
/**
|
|
104
89
|
* Build the pending `guardianRequest` projection for a `guardian.question`
|
|
105
90
|
* signal's feed item. Returns null when the payload does not carry the
|
|
106
91
|
* request id the projection is keyed by.
|
|
107
|
-
*
|
|
108
|
-
* `slackDelivery` is the signal's Slack delivery outcome, when one
|
|
109
|
-
* shipped: its destination chat + message ts locate the guardian-DM
|
|
110
|
-
* approval card, and the derived deep links are what the client's
|
|
111
|
-
* "Open in Slack" affordance opens.
|
|
112
92
|
*/
|
|
113
93
|
export function buildPendingGuardianProjection(
|
|
114
94
|
contextPayload: unknown,
|
|
115
|
-
slackDelivery?: NotificationDeliveryResult,
|
|
116
95
|
fallbackKind?: GuardianQuestionRequestKind,
|
|
117
96
|
): FeedItemGuardianRequest | null {
|
|
118
97
|
// Access-request payloads predate the kind registry and carry no
|
|
@@ -140,7 +119,6 @@ export function buildPendingGuardianProjection(
|
|
|
140
119
|
);
|
|
141
120
|
|
|
142
121
|
const sourceView = buildToolApprovalSourceView(payload);
|
|
143
|
-
const slackLinks = buildSlackCardLinks(slackDelivery);
|
|
144
122
|
// Access-request payloads name their requester differently.
|
|
145
123
|
const requesterLabel =
|
|
146
124
|
payload.requesterIdentifier?.trim() ||
|
|
@@ -159,8 +137,6 @@ export function buildPendingGuardianProjection(
|
|
|
159
137
|
? { sourceContextLabel: describeApprovalSourceContext(sourceView) }
|
|
160
138
|
: {}),
|
|
161
139
|
...(sourceView?.permalink ? { sourceUrl: sourceView.permalink } : {}),
|
|
162
|
-
...(slackLinks?.webUrl ? { slackCardUrl: slackLinks.webUrl } : {}),
|
|
163
|
-
...(slackLinks?.appUrl ? { slackCardAppUrl: slackLinks.appUrl } : {}),
|
|
164
140
|
};
|
|
165
141
|
}
|
|
166
142
|
|
|
@@ -177,43 +153,18 @@ function intentFromInstructionMode(
|
|
|
177
153
|
}
|
|
178
154
|
|
|
179
155
|
/**
|
|
180
|
-
* Display label for the originating chat
|
|
181
|
-
*
|
|
182
|
-
*
|
|
183
|
-
*
|
|
156
|
+
* Display label for the originating chat: the bare chat label from the
|
|
157
|
+
* shared derivation (the bell's context line joins it with the tool, so
|
|
158
|
+
* the channel word would be noise), or the channel id for channels the
|
|
159
|
+
* view carries no chat facts for.
|
|
184
160
|
*/
|
|
185
|
-
function describeApprovalSourceContext(view: {
|
|
186
|
-
channel
|
|
187
|
-
|
|
188
|
-
isSlackDm: boolean;
|
|
189
|
-
}): string {
|
|
190
|
-
if (view.channel === "slack" && view.chatId) {
|
|
191
|
-
return view.isSlackDm ? "Slack direct message" : `Slack #${view.chatId}`;
|
|
161
|
+
function describeApprovalSourceContext(view: ToolApprovalSourceView): string {
|
|
162
|
+
if (view.channel === "slack") {
|
|
163
|
+
return describeSlackChatLabel(view) || view.channel;
|
|
192
164
|
}
|
|
193
165
|
return view.channel;
|
|
194
166
|
}
|
|
195
167
|
|
|
196
|
-
/** Deep links to the Slack guardian-DM approval card, when one shipped. */
|
|
197
|
-
function buildSlackCardLinks(
|
|
198
|
-
slackDelivery?: NotificationDeliveryResult,
|
|
199
|
-
): { appUrl?: string; webUrl?: string } | undefined {
|
|
200
|
-
if (
|
|
201
|
-
!slackDelivery ||
|
|
202
|
-
slackDelivery.status !== "sent" ||
|
|
203
|
-
!slackDelivery.destination ||
|
|
204
|
-
!slackDelivery.messageId
|
|
205
|
-
) {
|
|
206
|
-
return undefined;
|
|
207
|
-
}
|
|
208
|
-
const slackConfig = getConfig().slack;
|
|
209
|
-
return buildSlackMessageDeepLinks({
|
|
210
|
-
teamId: slackConfig?.teamId,
|
|
211
|
-
teamUrl: slackConfig?.teamUrl,
|
|
212
|
-
channelId: slackDelivery.destination,
|
|
213
|
-
messageTs: slackDelivery.messageId,
|
|
214
|
-
});
|
|
215
|
-
}
|
|
216
|
-
|
|
217
168
|
export interface GuardianFeedReceiptParams {
|
|
218
169
|
requestId: string;
|
|
219
170
|
status: GuardianRequestStatus;
|
|
@@ -267,13 +218,6 @@ export async function writeGuardianFeedReceipt(
|
|
|
267
218
|
...(params.decidedAction
|
|
268
219
|
? { decidedAction: params.decidedAction }
|
|
269
220
|
: {}),
|
|
270
|
-
// A decider label only when a person decided: a non-decision
|
|
271
|
-
// terminal (superseded, expired) must not read as the guardian's
|
|
272
|
-
// own rejection.
|
|
273
|
-
...((params.status === "approved" || params.status === "denied") &&
|
|
274
|
-
!params.terminalReason
|
|
275
|
-
? decidedByLabelPatch()
|
|
276
|
-
: {}),
|
|
277
221
|
decidedAt: new Date(params.decidedAtMs ?? Date.now()).toISOString(),
|
|
278
222
|
...(params.terminalReason
|
|
279
223
|
? { terminalReason: params.terminalReason }
|
|
@@ -297,15 +241,6 @@ export async function writeGuardianFeedReceipt(
|
|
|
297
241
|
}
|
|
298
242
|
}
|
|
299
243
|
|
|
300
|
-
/** `{ decidedByLabel }` when a real display name exists, `{}` otherwise. */
|
|
301
|
-
function decidedByLabelPatch(): Pick<
|
|
302
|
-
Partial<FeedItemGuardianRequest>,
|
|
303
|
-
"decidedByLabel"
|
|
304
|
-
> {
|
|
305
|
-
const name = guardianReceiptName();
|
|
306
|
-
return name === undefined ? {} : { decidedByLabel: name };
|
|
307
|
-
}
|
|
308
|
-
|
|
309
244
|
/**
|
|
310
245
|
* Rewrite a request's feed item into its receipt when the canonical row
|
|
311
246
|
* is already terminal. Converges the write-vs-resolve race from the
|
|
@@ -99,6 +99,8 @@ export const PendingQuestionPayloadSchema =
|
|
|
99
99
|
const sourceReferenceFields = {
|
|
100
100
|
/** Channel-native chat/conversation id the request originated from. */
|
|
101
101
|
sourceChatId: z.string().optional(),
|
|
102
|
+
/** Display name of the originating chat, when ingress captured one. */
|
|
103
|
+
sourceChatName: z.string().optional(),
|
|
102
104
|
/** Deep link to the originating message/thread, when derivable. */
|
|
103
105
|
sourceLink: externalSourceLinkSchema.optional(),
|
|
104
106
|
};
|
|
@@ -162,6 +164,7 @@ export const LenientToolApprovalPayloadSchema = z.object({
|
|
|
162
164
|
riskLevel: z.string().nullable().optional(),
|
|
163
165
|
commandPreview: z.string().nullable().optional(),
|
|
164
166
|
sourceChatId: z.string().nullable().optional(),
|
|
167
|
+
sourceChatName: z.string().nullable().optional(),
|
|
165
168
|
sourceLink: sourceReferenceFields.sourceLink.nullable(),
|
|
166
169
|
});
|
|
167
170
|
|
|
@@ -190,6 +193,8 @@ export const ToolApprovalSourceViewSchema = z.object({
|
|
|
190
193
|
channel: z.string(),
|
|
191
194
|
/** Channel-native chat id, when the payload carries one. */
|
|
192
195
|
chatId: z.string().optional(),
|
|
196
|
+
/** Display name of the originating chat, when ingress captured one. */
|
|
197
|
+
chatName: z.string().optional(),
|
|
193
198
|
/** Whether the source chat is a Slack direct message (`false` for other channels). */
|
|
194
199
|
isSlackDm: z.boolean(),
|
|
195
200
|
/** Link to the originating message/thread, when derivable. */
|
|
@@ -205,10 +210,36 @@ export type ToolApprovalSourceView = z.infer<
|
|
|
205
210
|
* facts. Returns `undefined` when the payload names no chat and carries no
|
|
206
211
|
* link — renderers then fall back to the plain channel label.
|
|
207
212
|
*/
|
|
213
|
+
/**
|
|
214
|
+
* Bare display label for the source chat, derived once for every
|
|
215
|
+
* renderer: `Direct message` for a DM, `#name` for a named channel,
|
|
216
|
+
* `#<id>` when only the id is known, empty when the view names no chat.
|
|
217
|
+
* Slack-scoped like the facts it reads; surfaces add their own framing
|
|
218
|
+
* (the card prefixes the channel word, the bell uses it bare).
|
|
219
|
+
*/
|
|
220
|
+
export function describeSlackChatLabel(
|
|
221
|
+
view: Pick<ToolApprovalSourceView, "chatId" | "chatName" | "isSlackDm">,
|
|
222
|
+
): string {
|
|
223
|
+
if (view.isSlackDm) {
|
|
224
|
+
return "Direct message";
|
|
225
|
+
}
|
|
226
|
+
if (view.chatName) {
|
|
227
|
+
return `#${view.chatName}`;
|
|
228
|
+
}
|
|
229
|
+
if (view.chatId) {
|
|
230
|
+
return `#${view.chatId}`;
|
|
231
|
+
}
|
|
232
|
+
return "";
|
|
233
|
+
}
|
|
234
|
+
|
|
208
235
|
export function buildToolApprovalSourceView(
|
|
209
236
|
p: Pick<
|
|
210
237
|
LenientToolApprovalPayload,
|
|
211
|
-
|
|
238
|
+
| "sourceChannel"
|
|
239
|
+
| "sourceChatId"
|
|
240
|
+
| "sourceChatName"
|
|
241
|
+
| "sourceLink"
|
|
242
|
+
| "requesterChatId"
|
|
212
243
|
>,
|
|
213
244
|
): ToolApprovalSourceView | undefined {
|
|
214
245
|
const channel = nonEmpty(p.sourceChannel);
|
|
@@ -225,6 +256,7 @@ export function buildToolApprovalSourceView(
|
|
|
225
256
|
return {
|
|
226
257
|
channel,
|
|
227
258
|
chatId,
|
|
259
|
+
chatName: nonEmpty(p.sourceChatName),
|
|
228
260
|
isSlackDm:
|
|
229
261
|
channel === "slack" && chatId != null && isSlackDmConversation(chatId),
|
|
230
262
|
permalink,
|
|
@@ -85,7 +85,6 @@ export async function writeHomeFeedItemForSignal(
|
|
|
85
85
|
signal: NotificationSignal,
|
|
86
86
|
decision: NotificationDecision,
|
|
87
87
|
vellumDelivery?: NotificationDeliveryResult,
|
|
88
|
-
deliveryResults?: NotificationDeliveryResult[],
|
|
89
88
|
): Promise<FeedItem | null> {
|
|
90
89
|
const { mirror, sourceConversationId, sourceScheduleJobId } =
|
|
91
90
|
resolveHomeFeedMirror(signal, vellumDelivery?.conversationId);
|
|
@@ -178,9 +177,6 @@ export async function writeHomeFeedItemForSignal(
|
|
|
178
177
|
)
|
|
179
178
|
? buildPendingGuardianProjection(
|
|
180
179
|
signal.contextPayload,
|
|
181
|
-
deliveryResults?.find(
|
|
182
|
-
(r) => r.channel === "slack" && r.status === "sent",
|
|
183
|
-
),
|
|
184
180
|
signal.sourceEventName === "ingress.access_request"
|
|
185
181
|
? "access_request"
|
|
186
182
|
: undefined,
|
|
@@ -113,9 +113,13 @@ export function upsertBinding(input: UpsertBindingInput): void {
|
|
|
113
113
|
set: {
|
|
114
114
|
sourceChannel: input.sourceChannel,
|
|
115
115
|
externalChatId: input.externalChatId,
|
|
116
|
+
// An absent chat name preserves the stored one only while the
|
|
117
|
+
// binding still addresses the same chat; a move to a chat that
|
|
118
|
+
// reported no name clears it, so the old chat's name can never
|
|
119
|
+
// label the new one.
|
|
116
120
|
externalChatName:
|
|
117
121
|
externalChatName ??
|
|
118
|
-
sql
|
|
122
|
+
sql`CASE WHEN ${externalConversationBindings.sourceChannel} = ${input.sourceChannel} AND ${externalConversationBindings.externalChatId} = ${input.externalChatId} THEN ${externalConversationBindings.externalChatName} ELSE NULL END`,
|
|
119
123
|
externalThreadId,
|
|
120
124
|
// An omitted sender field keeps what is stored; an explicit value,
|
|
121
125
|
// including null, replaces it. A caller that knows only the chat
|
|
@@ -1056,7 +1056,7 @@ export class AnthropicProvider implements Provider {
|
|
|
1056
1056
|
/claude-opus-4-[78]\b/.test(effectiveModel) ||
|
|
1057
1057
|
/claude-opus-5\b/.test(effectiveModel) ||
|
|
1058
1058
|
/claude-sonnet-5\b/.test(effectiveModel) ||
|
|
1059
|
-
|
|
1059
|
+
/claude-fable-/.test(effectiveModel);
|
|
1060
1060
|
const mergedOutputConfig = {
|
|
1061
1061
|
...(output_config ?? {}),
|
|
1062
1062
|
...(effort && effort !== "none" && supportsEffort
|
|
@@ -193,6 +193,24 @@ const RAW_PROVIDER_CATALOG: ProviderCatalogEntry[] = [
|
|
|
193
193
|
linkLabel: "Open Anthropic Console",
|
|
194
194
|
},
|
|
195
195
|
models: [
|
|
196
|
+
{
|
|
197
|
+
id: "claude-fable-5-1",
|
|
198
|
+
displayName: "Claude Fable 5.1",
|
|
199
|
+
contextWindowTokens: 1000000,
|
|
200
|
+
maxOutputTokens: 128000,
|
|
201
|
+
longContextPricingThresholdTokens: 200000,
|
|
202
|
+
supportsThinking: true,
|
|
203
|
+
adaptiveThinkingOnly: true,
|
|
204
|
+
supportsCaching: true,
|
|
205
|
+
supportsVision: true,
|
|
206
|
+
supportsToolUse: true,
|
|
207
|
+
pricing: {
|
|
208
|
+
inputPer1mTokens: 10,
|
|
209
|
+
outputPer1mTokens: 50,
|
|
210
|
+
cacheWritePer1mTokens: 12.5,
|
|
211
|
+
cacheReadPer1mTokens: 0.25,
|
|
212
|
+
},
|
|
213
|
+
},
|
|
196
214
|
{
|
|
197
215
|
id: "claude-fable-5",
|
|
198
216
|
displayName: "Claude Fable 5",
|
|
@@ -1086,6 +1104,24 @@ const RAW_PROVIDER_CATALOG: ProviderCatalogEntry[] = [
|
|
|
1086
1104
|
// OpenRouter proxies anthropic/* through Anthropic's Messages API, so
|
|
1087
1105
|
// prompt caching and cache TTL metadata pass through unchanged and
|
|
1088
1106
|
// billing matches Anthropic's direct rates.
|
|
1107
|
+
{
|
|
1108
|
+
id: "anthropic/claude-fable-5.1",
|
|
1109
|
+
displayName: "Claude Fable 5.1",
|
|
1110
|
+
contextWindowTokens: 1000000,
|
|
1111
|
+
maxOutputTokens: 128000,
|
|
1112
|
+
longContextPricingThresholdTokens: 200000,
|
|
1113
|
+
supportsThinking: true,
|
|
1114
|
+
adaptiveThinkingOnly: true,
|
|
1115
|
+
supportsCaching: true,
|
|
1116
|
+
supportsVision: true,
|
|
1117
|
+
supportsToolUse: true,
|
|
1118
|
+
pricing: {
|
|
1119
|
+
inputPer1mTokens: 10,
|
|
1120
|
+
outputPer1mTokens: 50,
|
|
1121
|
+
cacheWritePer1mTokens: 12.5,
|
|
1122
|
+
cacheReadPer1mTokens: 0.25,
|
|
1123
|
+
},
|
|
1124
|
+
},
|
|
1089
1125
|
{
|
|
1090
1126
|
id: "anthropic/claude-fable-5",
|
|
1091
1127
|
displayName: "Claude Fable 5",
|
|
@@ -1934,6 +1970,24 @@ const RAW_PROVIDER_CATALOG: ProviderCatalogEntry[] = [
|
|
|
1934
1970
|
// The gateway proxies anthropic/* through Anthropic's Messages API, so
|
|
1935
1971
|
// prompt caching and cache TTL metadata pass through unchanged and
|
|
1936
1972
|
// billing matches Anthropic's direct rates.
|
|
1973
|
+
{
|
|
1974
|
+
id: "anthropic/claude-fable-5.1",
|
|
1975
|
+
displayName: "Claude Fable 5.1",
|
|
1976
|
+
contextWindowTokens: 1000000,
|
|
1977
|
+
maxOutputTokens: 128000,
|
|
1978
|
+
longContextPricingThresholdTokens: 200000,
|
|
1979
|
+
supportsThinking: true,
|
|
1980
|
+
adaptiveThinkingOnly: true,
|
|
1981
|
+
supportsCaching: true,
|
|
1982
|
+
supportsVision: true,
|
|
1983
|
+
supportsToolUse: true,
|
|
1984
|
+
pricing: {
|
|
1985
|
+
inputPer1mTokens: 10,
|
|
1986
|
+
outputPer1mTokens: 50,
|
|
1987
|
+
cacheWritePer1mTokens: 12.5,
|
|
1988
|
+
cacheReadPer1mTokens: 0.25,
|
|
1989
|
+
},
|
|
1990
|
+
},
|
|
1937
1991
|
{
|
|
1938
1992
|
id: "anthropic/claude-fable-5",
|
|
1939
1993
|
displayName: "Claude Fable 5",
|
|
@@ -20,7 +20,7 @@ const PROVIDER_MODEL_INTENTS: Record<string, Record<ModelIntent, string>> = {
|
|
|
20
20
|
balanced: "claude-sonnet-4-6",
|
|
21
21
|
"cost-optimized": "claude-haiku-4-5-20251001",
|
|
22
22
|
"latency-optimized": "claude-haiku-4-5-20251001",
|
|
23
|
-
"quality-optimized": "claude-fable-5",
|
|
23
|
+
"quality-optimized": "claude-fable-5-1",
|
|
24
24
|
"vision-optimized": "claude-opus-4-6",
|
|
25
25
|
},
|
|
26
26
|
openai: {
|
|
@@ -55,14 +55,14 @@ const PROVIDER_MODEL_INTENTS: Record<string, Record<ModelIntent, string>> = {
|
|
|
55
55
|
balanced: "anthropic/claude-sonnet-4.6",
|
|
56
56
|
"cost-optimized": "deepseek/deepseek-v4-flash",
|
|
57
57
|
"latency-optimized": "anthropic/claude-haiku-4.5",
|
|
58
|
-
"quality-optimized": "anthropic/claude-fable-5",
|
|
58
|
+
"quality-optimized": "anthropic/claude-fable-5.1",
|
|
59
59
|
"vision-optimized": "anthropic/claude-opus-4.6",
|
|
60
60
|
},
|
|
61
61
|
"vercel-ai-gateway": {
|
|
62
62
|
balanced: "anthropic/claude-sonnet-4.6",
|
|
63
63
|
"cost-optimized": "deepseek/deepseek-v4-flash",
|
|
64
64
|
"latency-optimized": "anthropic/claude-haiku-4.5",
|
|
65
|
-
"quality-optimized": "anthropic/claude-fable-5",
|
|
65
|
+
"quality-optimized": "anthropic/claude-fable-5.1",
|
|
66
66
|
"vision-optimized": "anthropic/claude-opus-4.6",
|
|
67
67
|
},
|
|
68
68
|
};
|
|
@@ -45,6 +45,12 @@ const log = getLogger("approval-source-link");
|
|
|
45
45
|
export interface ApprovalSourceReference {
|
|
46
46
|
/** Channel-native chat/conversation id the request originated from. */
|
|
47
47
|
sourceChatId: string;
|
|
48
|
+
/**
|
|
49
|
+
* Display name of the originating chat, when ingress captured one (a
|
|
50
|
+
* Slack channel's name, without the `#`). Renderers prefer it over the
|
|
51
|
+
* raw chat id and fall back when absent (DMs, pre-capture rows).
|
|
52
|
+
*/
|
|
53
|
+
sourceChatName?: string;
|
|
48
54
|
/** Deep link to the originating message/thread, when derivable. */
|
|
49
55
|
sourceLink?: ExternalSourceLink;
|
|
50
56
|
}
|