@vellumai/assistant 0.4.32 → 0.4.34
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/architecture/memory.md +1 -1
- package/package.json +1 -1
- package/src/__tests__/access-request-decision.test.ts +85 -4
- package/src/__tests__/actor-token-service.test.ts +4 -12
- package/src/__tests__/approval-primitive.test.ts +0 -45
- package/src/__tests__/approval-routes-http.test.ts +0 -1
- package/src/__tests__/assistant-id-boundary-guard.test.ts +150 -0
- package/src/__tests__/call-controller.test.ts +0 -1
- package/src/__tests__/call-routes-http.test.ts +0 -1
- package/src/__tests__/callback-handoff-copy.test.ts +0 -1
- package/src/__tests__/channel-approval-routes.test.ts +5 -45
- package/src/__tests__/channel-guardian.test.ts +122 -346
- package/src/__tests__/channel-invite-transport.test.ts +52 -40
- package/src/__tests__/commit-message-enrichment-service.test.ts +4 -38
- package/src/__tests__/computer-use-session-working-dir.test.ts +0 -1
- package/src/__tests__/confirmation-request-guardian-bridge.test.ts +4 -3
- package/src/__tests__/contacts-tools.test.ts +4 -5
- package/src/__tests__/conversation-attention-store.test.ts +2 -65
- package/src/__tests__/conversation-attention-telegram.test.ts +0 -2
- package/src/__tests__/conversation-pairing.test.ts +0 -1
- package/src/__tests__/credential-security-invariants.test.ts +1 -0
- package/src/__tests__/deterministic-verification-control-plane.test.ts +0 -3
- package/src/__tests__/guardian-action-conversation-turn.test.ts +1 -7
- package/src/__tests__/guardian-action-followup-executor.test.ts +0 -1
- package/src/__tests__/guardian-action-grant-mint-consume.test.ts +0 -74
- package/src/__tests__/guardian-action-late-reply.test.ts +1 -8
- package/src/__tests__/guardian-dispatch.test.ts +0 -1
- package/src/__tests__/guardian-grant-minting.test.ts +0 -1
- package/src/__tests__/guardian-outbound-http.test.ts +0 -1
- package/src/__tests__/guardian-routing-state.test.ts +0 -3
- package/src/__tests__/handlers-telegram-config.test.ts +0 -1
- package/src/__tests__/inbound-invite-redemption.test.ts +1 -7
- package/src/__tests__/ingress-reconcile.test.ts +3 -36
- package/src/__tests__/migration-cross-version-compatibility.test.ts +0 -1
- package/src/__tests__/migration-export-http.test.ts +0 -1
- package/src/__tests__/migration-import-commit-http.test.ts +0 -1
- package/src/__tests__/migration-import-preflight-http.test.ts +0 -1
- package/src/__tests__/migration-validate-http.test.ts +0 -1
- package/src/__tests__/non-member-access-request.test.ts +0 -8
- package/src/__tests__/notification-broadcaster.test.ts +1 -2
- package/src/__tests__/notification-decision-fallback.test.ts +0 -2
- package/src/__tests__/notification-decision-strategy.test.ts +0 -1
- package/src/__tests__/notification-guardian-path.test.ts +0 -1
- package/src/__tests__/notification-telegram-adapter.test.ts +0 -4
- package/src/__tests__/relay-server.test.ts +151 -80
- package/src/__tests__/sandbox-host-parity.test.ts +5 -2
- package/src/__tests__/scoped-approval-grants.test.ts +9 -40
- package/src/__tests__/scoped-grant-security-matrix.test.ts +0 -36
- package/src/__tests__/send-endpoint-busy.test.ts +0 -1
- package/src/__tests__/send-notification-tool.test.ts +0 -1
- package/src/__tests__/session-init.benchmark.test.ts +0 -2
- package/src/__tests__/slack-channel-config.test.ts +0 -1
- package/src/__tests__/slack-inbound-verification.test.ts +2 -5
- package/src/__tests__/sms-messaging-provider.test.ts +0 -4
- package/src/__tests__/terminal-tools.test.ts +5 -2
- package/src/__tests__/thread-seed-composer.test.ts +0 -1
- package/src/__tests__/tool-approval-handler.test.ts +0 -1
- package/src/__tests__/tool-grant-request-escalation.test.ts +0 -4
- package/src/__tests__/trusted-contact-approval-notifier.test.ts +65 -77
- package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +0 -1
- package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +1 -18
- package/src/__tests__/trusted-contact-multichannel.test.ts +0 -14
- package/src/__tests__/trusted-contact-verification.test.ts +3 -16
- package/src/__tests__/twilio-routes.test.ts +2 -3
- package/src/__tests__/update-bulletin.test.ts +0 -2
- package/src/__tests__/user-reference.test.ts +47 -1
- package/src/__tests__/voice-invite-redemption.test.ts +0 -1
- package/src/__tests__/voice-scoped-grant-consumer.test.ts +0 -38
- package/src/__tests__/workspace-git-service.test.ts +2 -2
- package/src/approvals/approval-primitive.ts +0 -15
- package/src/approvals/guardian-decision-primitive.ts +0 -3
- package/src/approvals/guardian-request-resolvers.ts +0 -5
- package/src/calls/call-domain.ts +0 -3
- package/src/calls/call-store.ts +0 -3
- package/src/calls/guardian-action-sweep.ts +2 -1
- package/src/calls/guardian-dispatch.ts +1 -2
- package/src/calls/relay-access-wait.ts +0 -4
- package/src/calls/relay-server.ts +8 -66
- package/src/calls/relay-setup-router.ts +1 -2
- package/src/calls/relay-verification.ts +0 -1
- package/src/calls/twilio-routes.ts +0 -3
- package/src/calls/types.ts +0 -1
- package/src/calls/voice-session-bridge.ts +0 -1
- package/src/channels/config.ts +41 -2
- package/src/config/bundled-skills/notifications/tools/send-notification.ts +0 -1
- package/src/config/bundled-skills/slack/SKILL.md +2 -0
- package/src/config/bundled-skills/slack-digest-setup/SKILL.md +164 -0
- package/src/config/env.ts +0 -4
- package/src/config/feature-flag-registry.json +4 -4
- package/src/config/user-reference.ts +47 -9
- package/src/contacts/contact-store.ts +13 -88
- package/src/contacts/contacts-write.ts +3 -11
- package/src/contacts/types.ts +0 -1
- package/src/daemon/handlers/config-channels.ts +19 -44
- package/src/daemon/handlers/config-inbox.ts +6 -6
- package/src/daemon/handlers/contacts.ts +8 -12
- package/src/daemon/handlers/index.ts +0 -2
- package/src/daemon/lifecycle.ts +18 -26
- package/src/daemon/session-process.ts +0 -4
- package/src/memory/channel-delivery-store.ts +1 -0
- package/src/memory/conversation-attention-store.ts +4 -19
- package/src/memory/conversation-crud.ts +0 -2
- package/src/memory/db-init.ts +8 -0
- package/src/memory/delivery-crud.ts +13 -0
- package/src/memory/guardian-action-store.ts +0 -12
- package/src/memory/guardian-approvals.ts +35 -80
- package/src/memory/guardian-rate-limits.ts +1 -14
- package/src/memory/guardian-verification.ts +6 -34
- package/src/memory/invite-store.ts +76 -15
- package/src/memory/migrations/040-invite-code-hash-column.ts +16 -0
- package/src/memory/migrations/134-contacts-notes-column.ts +64 -45
- package/src/memory/migrations/136-drop-assistant-id-columns.ts +263 -0
- package/src/memory/migrations/index.ts +2 -0
- package/src/memory/migrations/registry.ts +14 -1
- package/src/memory/schema/calls.ts +0 -7
- package/src/memory/schema/contacts.ts +2 -8
- package/src/memory/schema/guardian.ts +0 -5
- package/src/memory/schema/infrastructure.ts +0 -2
- package/src/memory/schema/notifications.ts +3 -17
- package/src/memory/scoped-approval-grants.ts +2 -24
- package/src/notifications/adapters/sms.ts +2 -1
- package/src/notifications/broadcaster.ts +1 -6
- package/src/notifications/decision-engine.ts +3 -4
- package/src/notifications/deliveries-store.ts +0 -4
- package/src/notifications/destination-resolver.ts +4 -6
- package/src/notifications/deterministic-checks.ts +1 -6
- package/src/notifications/emit-signal.ts +4 -11
- package/src/notifications/events-store.ts +7 -17
- package/src/notifications/preference-summary.ts +2 -2
- package/src/notifications/preferences-store.ts +2 -9
- package/src/notifications/signal.ts +0 -1
- package/src/notifications/thread-candidates.ts +1 -11
- package/src/notifications/types.ts +0 -3
- package/src/runtime/access-request-helper.ts +3 -10
- package/src/runtime/actor-refresh-token-store.ts +0 -6
- package/src/runtime/actor-token-store.ts +3 -16
- package/src/runtime/actor-trust-resolver.ts +1 -4
- package/src/runtime/auth/__tests__/credential-service.test.ts +0 -9
- package/src/runtime/auth/__tests__/guard-tests.test.ts +1 -3
- package/src/runtime/auth/credential-service.ts +1 -15
- package/src/runtime/auth/require-bound-guardian.ts +1 -4
- package/src/runtime/auth/token-service.ts +50 -0
- package/src/runtime/channel-guardian-service.ts +16 -49
- package/src/runtime/channel-invite-transport.ts +129 -34
- package/src/runtime/channel-invite-transports/email.ts +54 -0
- package/src/runtime/channel-invite-transports/slack.ts +87 -0
- package/src/runtime/channel-invite-transports/sms.ts +74 -0
- package/src/runtime/channel-invite-transports/telegram.ts +35 -11
- package/src/runtime/channel-invite-transports/voice.ts +12 -12
- package/src/runtime/confirmation-request-guardian-bridge.ts +0 -1
- package/src/runtime/guardian-action-followup-executor.ts +3 -2
- package/src/runtime/guardian-action-grant-minter.ts +0 -1
- package/src/runtime/guardian-outbound-actions.ts +2 -12
- package/src/runtime/guardian-vellum-migration.ts +2 -3
- package/src/runtime/http-server.ts +0 -1
- package/src/runtime/invite-redemption-service.ts +191 -11
- package/src/runtime/invite-redemption-templates.ts +6 -6
- package/src/runtime/invite-service.ts +81 -11
- package/src/runtime/local-actor-identity.ts +2 -5
- package/src/runtime/routes/access-request-decision.ts +52 -7
- package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +6 -9
- package/src/runtime/routes/channel-readiness-routes.ts +29 -18
- package/src/runtime/routes/contact-routes.ts +48 -46
- package/src/runtime/routes/conversation-attention-routes.ts +0 -2
- package/src/runtime/routes/global-search-routes.ts +0 -2
- package/src/runtime/routes/guardian-bootstrap-routes.ts +6 -12
- package/src/runtime/routes/guardian-expiry-sweep.ts +3 -2
- package/src/runtime/routes/inbound-message-handler.ts +1 -6
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +296 -47
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +6 -42
- package/src/runtime/routes/inbound-stages/bootstrap-intercept.ts +1 -6
- package/src/runtime/routes/inbound-stages/edit-intercept.ts +10 -0
- package/src/runtime/routes/inbound-stages/escalation-intercept.ts +0 -1
- package/src/runtime/routes/inbound-stages/secret-ingress-check.ts +0 -1
- package/src/runtime/routes/inbound-stages/verification-intercept.ts +3 -7
- package/src/runtime/routes/invite-routes.ts +1 -0
- package/src/runtime/routes/pairing-routes.ts +4 -4
- package/src/runtime/tool-grant-request-helper.ts +0 -1
- package/src/tools/browser/browser-manager.ts +22 -12
- package/src/tools/browser/runtime-check.ts +110 -3
- package/src/tools/calls/call-start.ts +1 -3
- package/src/tools/followups/followup_create.ts +1 -2
- package/src/tools/shared/shell-output.ts +7 -2
- package/src/tools/tool-approval-handler.ts +0 -2
- package/src/util/platform.ts +0 -4
- package/src/workspace/git-service.ts +10 -4
|
@@ -10,7 +10,7 @@ import { randomInt } from "node:crypto";
|
|
|
10
10
|
|
|
11
11
|
import type { ServerWebSocket } from "bun";
|
|
12
12
|
|
|
13
|
-
import {
|
|
13
|
+
import { resolveGuardianName } from "../config/user-reference.js";
|
|
14
14
|
import {
|
|
15
15
|
findGuardianForChannel,
|
|
16
16
|
listGuardianChannels,
|
|
@@ -30,8 +30,6 @@ import {
|
|
|
30
30
|
resolveActorTrust,
|
|
31
31
|
toTrustContext,
|
|
32
32
|
} from "../runtime/actor-trust-resolver.js";
|
|
33
|
-
import { DAEMON_INTERNAL_ASSISTANT_ID } from "../runtime/assistant-scope.js";
|
|
34
|
-
import { getGuardianBinding } from "../runtime/channel-guardian-service.js";
|
|
35
33
|
import {
|
|
36
34
|
composeVerificationVoice,
|
|
37
35
|
GUARDIAN_VERIFY_TEMPLATE_KEYS,
|
|
@@ -788,7 +786,6 @@ export class RelayConnection {
|
|
|
788
786
|
if (!params.skipMemberActivation) {
|
|
789
787
|
try {
|
|
790
788
|
upsertMember({
|
|
791
|
-
assistantId,
|
|
792
789
|
sourceChannel: "voice",
|
|
793
790
|
externalUserId: fromNumber,
|
|
794
791
|
externalChatId: fromNumber,
|
|
@@ -973,9 +970,8 @@ export class RelayConnection {
|
|
|
973
970
|
"Guardian binding conflict: another user already holds the voice binding",
|
|
974
971
|
);
|
|
975
972
|
} else {
|
|
976
|
-
revokeGuardianBinding(
|
|
973
|
+
revokeGuardianBinding("voice");
|
|
977
974
|
createGuardianBinding({
|
|
978
|
-
assistantId,
|
|
979
975
|
channel: "voice",
|
|
980
976
|
guardianExternalUserId: fromNumber,
|
|
981
977
|
guardianDeliveryChatId: fromNumber,
|
|
@@ -1597,70 +1593,16 @@ export class RelayConnection {
|
|
|
1597
1593
|
|
|
1598
1594
|
/**
|
|
1599
1595
|
* Resolve a human-readable guardian label for voice wait copy.
|
|
1600
|
-
*
|
|
1601
|
-
*
|
|
1596
|
+
* Delegates to the shared resolveGuardianName() which checks USER.md
|
|
1597
|
+
* first, then falls back to Contact.displayName, then DEFAULT_USER_REFERENCE.
|
|
1602
1598
|
*/
|
|
1603
1599
|
private resolveGuardianLabel(): string {
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
// Try the voice-channel binding first, then fall back to any active
|
|
1608
|
-
// binding for the assistant (mirrors the cross-channel fallback pattern
|
|
1609
|
-
// in access-request-helper.ts).
|
|
1610
|
-
let metadataJson: string | null = null;
|
|
1611
|
-
// Contacts-first: prefer the voice-bound guardian, then fall back to
|
|
1612
|
-
// any guardian channel (mirrors the voice-first pattern in the legacy path).
|
|
1613
|
-
const voiceGuardian = findGuardianForChannel("voice", assistantId);
|
|
1614
|
-
const guardianChannels = voiceGuardian
|
|
1615
|
-
? null
|
|
1616
|
-
: listGuardianChannels(assistantId);
|
|
1600
|
+
// Look up the guardian contact for a displayName fallback
|
|
1601
|
+
const voiceGuardian = findGuardianForChannel("voice");
|
|
1602
|
+
const guardianChannels = voiceGuardian ? null : listGuardianChannels();
|
|
1617
1603
|
const guardianContact = voiceGuardian?.contact ?? guardianChannels?.contact;
|
|
1618
|
-
if (guardianContact) {
|
|
1619
|
-
const meta: Record<string, string> = {};
|
|
1620
|
-
if (guardianContact.displayName) {
|
|
1621
|
-
meta.displayName = guardianContact.displayName;
|
|
1622
|
-
}
|
|
1623
|
-
// Preserve the username fallback: use the voice channel's externalUserId
|
|
1624
|
-
// so downstream parsing can fall back to @username when displayName is a
|
|
1625
|
-
// raw external ID (e.g., phone number from contact-sync).
|
|
1626
|
-
const voiceChannel =
|
|
1627
|
-
voiceGuardian?.channel ??
|
|
1628
|
-
guardianChannels?.channels.find((ch) => ch.type === "voice");
|
|
1629
|
-
if (voiceChannel?.externalUserId) {
|
|
1630
|
-
meta.username = voiceChannel.externalUserId;
|
|
1631
|
-
}
|
|
1632
|
-
if (Object.keys(meta).length > 0) {
|
|
1633
|
-
metadataJson = JSON.stringify(meta);
|
|
1634
|
-
}
|
|
1635
|
-
}
|
|
1636
|
-
if (!metadataJson) {
|
|
1637
|
-
const voiceBinding = getGuardianBinding(assistantId, "voice");
|
|
1638
|
-
if (voiceBinding?.metadataJson) {
|
|
1639
|
-
metadataJson = voiceBinding.metadataJson;
|
|
1640
|
-
}
|
|
1641
|
-
}
|
|
1642
|
-
|
|
1643
|
-
if (metadataJson) {
|
|
1644
|
-
try {
|
|
1645
|
-
const parsed = JSON.parse(metadataJson) as Record<string, unknown>;
|
|
1646
|
-
if (
|
|
1647
|
-
typeof parsed.displayName === "string" &&
|
|
1648
|
-
parsed.displayName.trim().length > 0
|
|
1649
|
-
) {
|
|
1650
|
-
return parsed.displayName.trim();
|
|
1651
|
-
}
|
|
1652
|
-
if (
|
|
1653
|
-
typeof parsed.username === "string" &&
|
|
1654
|
-
parsed.username.trim().length > 0
|
|
1655
|
-
) {
|
|
1656
|
-
return `@${parsed.username.trim()}`;
|
|
1657
|
-
}
|
|
1658
|
-
} catch {
|
|
1659
|
-
// ignore malformed metadata
|
|
1660
|
-
}
|
|
1661
|
-
}
|
|
1662
1604
|
|
|
1663
|
-
return
|
|
1605
|
+
return resolveGuardianName(guardianContact?.displayName);
|
|
1664
1606
|
}
|
|
1665
1607
|
|
|
1666
1608
|
/**
|
|
@@ -164,7 +164,7 @@ export function routeSetup(ctx: SetupContext): {
|
|
|
164
164
|
}
|
|
165
165
|
|
|
166
166
|
// ── Inbound call ACL evaluation ─────────────────────────────────
|
|
167
|
-
const pendingChallenge = getPendingChallenge(
|
|
167
|
+
const pendingChallenge = getPendingChallenge("voice");
|
|
168
168
|
|
|
169
169
|
if (actorTrust.trustClass === "unknown" && !pendingChallenge) {
|
|
170
170
|
// Check for blocked caller
|
|
@@ -191,7 +191,6 @@ export function routeSetup(ctx: SetupContext): {
|
|
|
191
191
|
let voiceInvites: ReturnType<typeof findActiveVoiceInvites> = [];
|
|
192
192
|
try {
|
|
193
193
|
voiceInvites = findActiveVoiceInvites({
|
|
194
|
-
assistantId,
|
|
195
194
|
expectedExternalUserId: ctx.from,
|
|
196
195
|
});
|
|
197
196
|
} catch (err) {
|
|
@@ -197,7 +197,6 @@ export async function handleVoiceWebhook(req: Request): Promise<Response> {
|
|
|
197
197
|
|
|
198
198
|
return buildVoiceWebhookTwiml(
|
|
199
199
|
session.id,
|
|
200
|
-
session.assistantId ?? undefined,
|
|
201
200
|
session.task,
|
|
202
201
|
session.guardianVerificationSessionId,
|
|
203
202
|
);
|
|
@@ -226,7 +225,6 @@ export async function handleVoiceWebhook(req: Request): Promise<Response> {
|
|
|
226
225
|
|
|
227
226
|
return buildVoiceWebhookTwiml(
|
|
228
227
|
callSessionId,
|
|
229
|
-
session.assistantId ?? undefined,
|
|
230
228
|
session.task,
|
|
231
229
|
session.guardianVerificationSessionId,
|
|
232
230
|
);
|
|
@@ -244,7 +242,6 @@ export async function handleVoiceWebhook(req: Request): Promise<Response> {
|
|
|
244
242
|
*/
|
|
245
243
|
function buildVoiceWebhookTwiml(
|
|
246
244
|
callSessionId: string,
|
|
247
|
-
assistantId: string | undefined,
|
|
248
245
|
task: string | null,
|
|
249
246
|
guardianVerificationSessionId?: string | null,
|
|
250
247
|
): Response {
|
package/src/calls/types.ts
CHANGED
|
@@ -73,7 +73,6 @@ export interface CallSession {
|
|
|
73
73
|
guardianVerificationSessionId: string | null;
|
|
74
74
|
callerIdentityMode: string | null;
|
|
75
75
|
callerIdentitySource: string | null;
|
|
76
|
-
assistantId: string | null;
|
|
77
76
|
initiatedFromConversationId?: string | null;
|
|
78
77
|
startedAt: number | null;
|
|
79
78
|
endedAt: number | null;
|
|
@@ -392,7 +392,6 @@ export async function startVoiceTurn(
|
|
|
392
392
|
toolName: msg.toolName,
|
|
393
393
|
inputDigest,
|
|
394
394
|
consumingRequestId: msg.requestId,
|
|
395
|
-
assistantId: opts.assistantId ?? DAEMON_INTERNAL_ASSISTANT_ID,
|
|
396
395
|
executionChannel: "voice",
|
|
397
396
|
conversationId: opts.conversationId,
|
|
398
397
|
callSessionId: opts.callSessionId,
|
package/src/channels/config.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Canonical per-channel
|
|
2
|
+
* Canonical per-channel policy registry.
|
|
3
3
|
*
|
|
4
4
|
* Every ChannelId must have an entry here. The `satisfies` constraint
|
|
5
5
|
* ensures that adding a new ChannelId to channels/types.ts will fail
|
|
@@ -13,11 +13,17 @@ export type ConversationStrategy =
|
|
|
13
13
|
| "continue_existing_conversation"
|
|
14
14
|
| "not_deliverable";
|
|
15
15
|
|
|
16
|
+
export interface ChannelInvitePolicy {
|
|
17
|
+
/** Whether inbound invite code redemption is supported on this channel. */
|
|
18
|
+
codeRedemptionEnabled: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
16
21
|
export interface ChannelNotificationPolicy {
|
|
17
22
|
notification: {
|
|
18
23
|
deliveryEnabled: boolean;
|
|
19
24
|
conversationStrategy: ConversationStrategy;
|
|
20
25
|
};
|
|
26
|
+
invite: ChannelInvitePolicy;
|
|
21
27
|
}
|
|
22
28
|
|
|
23
29
|
const CHANNEL_POLICIES = {
|
|
@@ -26,48 +32,69 @@ const CHANNEL_POLICIES = {
|
|
|
26
32
|
deliveryEnabled: true,
|
|
27
33
|
conversationStrategy: "start_new_conversation",
|
|
28
34
|
},
|
|
35
|
+
invite: {
|
|
36
|
+
codeRedemptionEnabled: false,
|
|
37
|
+
},
|
|
29
38
|
},
|
|
30
39
|
telegram: {
|
|
31
40
|
notification: {
|
|
32
41
|
deliveryEnabled: true,
|
|
33
42
|
conversationStrategy: "continue_existing_conversation",
|
|
34
43
|
},
|
|
44
|
+
invite: {
|
|
45
|
+
codeRedemptionEnabled: true,
|
|
46
|
+
},
|
|
35
47
|
},
|
|
36
48
|
sms: {
|
|
37
49
|
notification: {
|
|
38
50
|
deliveryEnabled: true,
|
|
39
51
|
conversationStrategy: "continue_existing_conversation",
|
|
40
52
|
},
|
|
53
|
+
invite: {
|
|
54
|
+
codeRedemptionEnabled: true,
|
|
55
|
+
},
|
|
41
56
|
},
|
|
42
57
|
whatsapp: {
|
|
43
58
|
notification: {
|
|
44
59
|
deliveryEnabled: false,
|
|
45
60
|
conversationStrategy: "continue_existing_conversation",
|
|
46
61
|
},
|
|
62
|
+
invite: {
|
|
63
|
+
codeRedemptionEnabled: false,
|
|
64
|
+
},
|
|
47
65
|
},
|
|
48
66
|
slack: {
|
|
49
67
|
notification: {
|
|
50
68
|
deliveryEnabled: true,
|
|
51
69
|
conversationStrategy: "continue_existing_conversation",
|
|
52
70
|
},
|
|
71
|
+
invite: {
|
|
72
|
+
codeRedemptionEnabled: true,
|
|
73
|
+
},
|
|
53
74
|
},
|
|
54
75
|
email: {
|
|
55
76
|
notification: {
|
|
56
77
|
deliveryEnabled: false,
|
|
57
78
|
conversationStrategy: "continue_existing_conversation",
|
|
58
79
|
},
|
|
80
|
+
invite: {
|
|
81
|
+
codeRedemptionEnabled: true,
|
|
82
|
+
},
|
|
59
83
|
},
|
|
60
84
|
voice: {
|
|
61
85
|
notification: {
|
|
62
86
|
deliveryEnabled: false,
|
|
63
87
|
conversationStrategy: "not_deliverable",
|
|
64
88
|
},
|
|
89
|
+
invite: {
|
|
90
|
+
codeRedemptionEnabled: false,
|
|
91
|
+
},
|
|
65
92
|
},
|
|
66
93
|
} as const satisfies Record<ChannelId, ChannelNotificationPolicy>;
|
|
67
94
|
|
|
68
95
|
export type ChannelPolicies = typeof CHANNEL_POLICIES;
|
|
69
96
|
|
|
70
|
-
/** Returns the full
|
|
97
|
+
/** Returns the full policy for a channel. */
|
|
71
98
|
export function getChannelPolicy(
|
|
72
99
|
channelId: ChannelId,
|
|
73
100
|
): ChannelNotificationPolicy {
|
|
@@ -97,3 +124,15 @@ export function getConversationStrategy(
|
|
|
97
124
|
): ConversationStrategy {
|
|
98
125
|
return CHANNEL_POLICIES[channelId].notification.conversationStrategy;
|
|
99
126
|
}
|
|
127
|
+
|
|
128
|
+
/** Returns the invite policy for the given channel. */
|
|
129
|
+
export function getChannelInvitePolicy(
|
|
130
|
+
channelId: ChannelId,
|
|
131
|
+
): ChannelInvitePolicy {
|
|
132
|
+
return CHANNEL_POLICIES[channelId].invite;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Whether invite code redemption is enabled for the given channel. */
|
|
136
|
+
export function isInviteCodeRedemptionEnabled(channelId: ChannelId): boolean {
|
|
137
|
+
return CHANNEL_POLICIES[channelId].invite.codeRedemptionEnabled;
|
|
138
|
+
}
|
|
@@ -69,6 +69,8 @@ When you need to **send** content to Slack proactively (e.g. a scheduled digest,
|
|
|
69
69
|
- `send_notification` is appropriate for short alerts and status updates where you want the router to pick the best channel. `messaging_send` is appropriate when you have specific content to deliver to a specific Slack destination.
|
|
70
70
|
- For scheduled tasks (cron/RRULE), always end with a `messaging_send` call so the results actually reach the user. Without it, the output only lives in the conversation log.
|
|
71
71
|
|
|
72
|
+
For setting up recurring digests, load the `slack-digest-setup` skill which covers the full configuration, scheduling, and delivery protocol.
|
|
73
|
+
|
|
72
74
|
## Watcher Integration
|
|
73
75
|
|
|
74
76
|
For real-time monitoring (not just on-demand scanning), the user can set up a Slack watcher using the watcher skill with the same channel IDs. Mention this if the user wants ongoing monitoring.
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: "Slack Digest Setup"
|
|
3
|
+
description: "Set up recurring Slack channel digests with scanning schedules, channel configuration, and delivery — codifies best practices for high-quality automated summaries"
|
|
4
|
+
user-invocable: true
|
|
5
|
+
includes: ["slack", "schedule"]
|
|
6
|
+
metadata: { "vellum": { "emoji": "📊" } }
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
You are helping your user set up a recurring Slack digest: automated channel scanning on a schedule that delivers prose-style summaries of what's happening across their workspace. This skill walks through configuration, scheduling, and — critically — the execution protocol that ensures every digest is actually useful.
|
|
10
|
+
|
|
11
|
+
## Prerequisites
|
|
12
|
+
|
|
13
|
+
Before starting, verify:
|
|
14
|
+
|
|
15
|
+
1. **Slack is connected.** The `slack_scan_digest` tool must be available. If not, load the `slack-app-setup` skill first.
|
|
16
|
+
2. **Bot has channel access.** The Slack bot must be invited to each channel it needs to scan. Users can do this with `/invite @BotName` in each channel.
|
|
17
|
+
|
|
18
|
+
## Step 1: Configure Channels
|
|
19
|
+
|
|
20
|
+
Help the user decide which channels to scan. Ask what channels matter most to them, then use `slack_configure_channels` with `action: "set"` to save their preferences.
|
|
21
|
+
|
|
22
|
+
Tips for channel selection:
|
|
23
|
+
|
|
24
|
+
- Start with 5-10 high-signal channels (team channels, engineering, announcements)
|
|
25
|
+
- Skip noisy bot/CI channels unless the user specifically wants them
|
|
26
|
+
- Private channels work too, as long as the bot is invited
|
|
27
|
+
- The user can update this list anytime with `slack_configure_channels`
|
|
28
|
+
|
|
29
|
+
If the user already has preferred channels configured, show the current list and ask if they want to adjust.
|
|
30
|
+
|
|
31
|
+
## Step 2: Set Up the Schedule
|
|
32
|
+
|
|
33
|
+
Create a recurring schedule using `schedule_create`. The recommended default is **hourly, 7am-7pm in the user's timezone**, with the overnight gap.
|
|
34
|
+
|
|
35
|
+
**Cron expression:** `0 7-19 * * *` (fires at the top of each hour, 7am through 7pm)
|
|
36
|
+
|
|
37
|
+
**Determine the delivery target.** Ask the user where digests should be delivered — typically their Slack DM or a dedicated channel like `#alex-agent-messages`. Use `messaging_read` or the Slack API to resolve the `conversation_id` for the target. This ID gets baked into the schedule message so scheduled sessions know where to post.
|
|
38
|
+
|
|
39
|
+
**The schedule message is critical.** Scheduled sessions have no memory of this setup conversation. The message must be completely self-contained with every instruction needed to execute properly. Use the Scan Execution Protocol below as the template for the schedule message content.
|
|
40
|
+
|
|
41
|
+
### Scan Window Logic
|
|
42
|
+
|
|
43
|
+
- The **first scan of the day** (e.g. 7am) covers everything since the last scan the previous evening. If the last scan was at 7pm, that's 12 hours of overnight activity.
|
|
44
|
+
- All **subsequent scans** cover since the previous hourly scan (roughly 1 hour).
|
|
45
|
+
- The scan reads `data/last_slack_scan.json` for the timestamp of the last scan and calculates `hours_back` accordingly.
|
|
46
|
+
|
|
47
|
+
## Step 3: Create Tracking Files
|
|
48
|
+
|
|
49
|
+
Set up two files for scan state and history:
|
|
50
|
+
|
|
51
|
+
**`data/last_slack_scan.json`** — Stores the timestamp of the last successful scan:
|
|
52
|
+
|
|
53
|
+
```json
|
|
54
|
+
{ "timestamp": "2026-01-01T12:00:00Z", "hours_back": 1, "channels_scanned": 0 }
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
**`data/slack_scan_log.md`** — Running log of scan activity:
|
|
58
|
+
|
|
59
|
+
```markdown
|
|
60
|
+
# Slack Scan Log
|
|
61
|
+
|
|
62
|
+
## YYYY-MM-DD HH:MM ET | Window: Xh | N channels scanned
|
|
63
|
+
|
|
64
|
+
Summary of what was found, or "All clear."
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Create both files during setup with initial values. The scheduled scan sessions update them after each run.
|
|
68
|
+
|
|
69
|
+
## Step 4: Scan Execution Protocol
|
|
70
|
+
|
|
71
|
+
**This is the most important section.** Every scheduled scan MUST follow this exact protocol. Embed these instructions directly in the schedule message so the session cannot deviate.
|
|
72
|
+
|
|
73
|
+
### The Protocol
|
|
74
|
+
|
|
75
|
+
1. **Load the Slack skill.** Call `skill_load` with `skill: "slack"` to ensure scan tools are available.
|
|
76
|
+
|
|
77
|
+
2. **Call `slack_scan_digest` with `include_threads: true`.** Check `data/last_slack_scan.json` for the last scan timestamp and calculate the appropriate `hours_back`. Actually call the tool. Do not skip this step. Do not assume nothing happened.
|
|
78
|
+
|
|
79
|
+
3. **Read the actual results.** Look at what the scan returned before deciding what to report. If the tool returned messages, proceed to step 4a. If it returned zero messages, proceed to step 4b.
|
|
80
|
+
|
|
81
|
+
4a. **If there are messages: Write a full prose digest.** Break down by channel. For each channel with activity, include:
|
|
82
|
+
|
|
83
|
+
- Channel name
|
|
84
|
+
- Who's talking (real names, not user IDs)
|
|
85
|
+
- What's being discussed (specific topics, not vague summaries)
|
|
86
|
+
- Reply counts on notable threads
|
|
87
|
+
- Decisions made, questions asked, action items
|
|
88
|
+
- Anything that looks like it needs the user's attention or a reply
|
|
89
|
+
|
|
90
|
+
Write in prose style, conversational. Not bullet lists of channel names. Highlight what matters, skip noise, but be specific about what's quiet too.
|
|
91
|
+
|
|
92
|
+
4b. **If genuinely zero messages: Name the channels you scanned.** The user needs to know coverage was complete. Example: "Scanned team-atlas, team-illuminati, ask-eng, team-jarvis, and 3 others. Nothing new in the last hour."
|
|
93
|
+
|
|
94
|
+
5. **Deliver via `messaging_send`.** Call `messaging_send` with `platform: "slack"` and the target `conversation_id` (determined during setup — typically the user's preferred DM or a dedicated digest channel). The `message` field MUST contain the full prose digest you wrote in step 4. Do NOT use `send_notification` for digests — the notification router's decision engine rewrites content into short alerts, stripping the actual digest.
|
|
95
|
+
|
|
96
|
+
6. **Update tracking files.** Write the current timestamp to `data/last_slack_scan.json` and append a log entry to `data/slack_scan_log.md`.
|
|
97
|
+
|
|
98
|
+
### Template Schedule Message
|
|
99
|
+
|
|
100
|
+
Use this as the schedule message when creating the schedule. Adjust the delivery target and channel exclusions per the user's preferences:
|
|
101
|
+
|
|
102
|
+
```
|
|
103
|
+
Run the Slack digest scan. Follow every instruction exactly:
|
|
104
|
+
|
|
105
|
+
1. Load the Slack skill.
|
|
106
|
+
2. Call slack_scan_digest with include_threads: true to scan preferred channels. Check data/last_slack_scan.json for the time window.
|
|
107
|
+
3. Read the actual results. Do NOT skip this or assume nothing happened.
|
|
108
|
+
4. Build the digest:
|
|
109
|
+
- If there are messages: write a prose-style digest broken down by channel with channel names, who's talking (real names), specific topics, reply counts, decisions, questions, and anything needing attention.
|
|
110
|
+
- If zero messages: list which channels were scanned so coverage is clear.
|
|
111
|
+
5. Send using messaging_send with platform "slack" and conversation_id "<target_channel_id>". The MESSAGE field must contain the full digest from step 4. Never send a generic status like "scan completed." Do NOT use send_notification — it rewrites content into short alerts.
|
|
112
|
+
6. Update data/last_slack_scan.json and append to data/slack_scan_log.md.
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Step 5: HEARTBEAT.md Integration (Optional)
|
|
116
|
+
|
|
117
|
+
If the user has the heartbeat feature enabled (`heartbeat.enabled: true` in config.json), add the scan protocol to `HEARTBEAT.md` as a checklist item instead of (or in addition to) using `schedule_create`. The heartbeat runs on its own interval and reads HEARTBEAT.md for instructions.
|
|
118
|
+
|
|
119
|
+
Either approach works. The key is that the execution instructions are explicit and self-contained regardless of where they live.
|
|
120
|
+
|
|
121
|
+
## Quality Standard
|
|
122
|
+
|
|
123
|
+
Every digest, whether it covers 1 hour or 12 hours, must meet the same quality bar:
|
|
124
|
+
|
|
125
|
+
- **Prose style, conversational.** Not bullet lists of channel names. Write like you're briefing a busy person.
|
|
126
|
+
- **Specific.** Name the people, name the topics, give reply counts. "Team-atlas had some discussion" is useless. "Marina and Emmie discussed the CI pipeline fix in team-atlas (7 replies, still in progress)" is useful.
|
|
127
|
+
- **Prioritized.** Lead with what matters. Decisions, blockers, and things needing the user's attention come first. Background chatter comes last or gets skipped.
|
|
128
|
+
- **Honest about quiet periods.** If nothing happened, say so, but name what you scanned.
|
|
129
|
+
|
|
130
|
+
The morning overnight digest is not a different format. It just covers more time and will naturally be longer.
|
|
131
|
+
|
|
132
|
+
## Privacy Rules
|
|
133
|
+
|
|
134
|
+
- Content from `isPrivate: true` channels MUST NEVER appear in digests delivered to other channels or external destinations.
|
|
135
|
+
- If the user designates specific channels as sensitive (e.g. a war-room), exclude them from digests entirely. Document these exclusions in the schedule message so scheduled sessions respect them.
|
|
136
|
+
- When in doubt about whether content can be shared, err on the side of omission.
|
|
137
|
+
|
|
138
|
+
## Common Pitfalls
|
|
139
|
+
|
|
140
|
+
These are hard-won lessons from debugging digest quality issues. They exist here so other assistants don't repeat the same mistakes.
|
|
141
|
+
|
|
142
|
+
### Pitfall: Skipping the scan call
|
|
143
|
+
|
|
144
|
+
The `slack_scan_digest` tool is what fetches messages. Without calling it, you have nothing to summarize. Never report "all clear" or "0 channels active" without having actually called the scan tool and confirmed zero messages came back. The scan does the fetching. You do the summarizing.
|
|
145
|
+
|
|
146
|
+
### Pitfall: Using `send_notification` for digests
|
|
147
|
+
|
|
148
|
+
The notification router's decision engine rewrites `send_notification` content into short alerts (title ≤ 8 words, body ≤ 2 sentences). If you put a full prose digest in `send_notification`, it will be truncated or rewritten. Always use `messaging_send` with the target `conversation_id` for digest delivery. The `messaging_send` message field is delivered verbatim.
|
|
149
|
+
|
|
150
|
+
### Pitfall: Not naming channels in quiet reports
|
|
151
|
+
|
|
152
|
+
Even when nothing happened, the user needs to know the scan actually ran and what it covered. "All clear" by itself could mean you scanned 12 channels and found nothing, or it could mean you didn't scan at all. List the channels.
|
|
153
|
+
|
|
154
|
+
### Pitfall: Losing context between sessions
|
|
155
|
+
|
|
156
|
+
Scheduled sessions start fresh with no memory of the setup conversation. They don't know what format to use, where to deliver, or what channels to skip unless you tell them. The schedule message must contain every instruction needed to execute properly. If it's not in the message, it won't happen.
|
|
157
|
+
|
|
158
|
+
### Pitfall: Different quality for hourly vs overnight
|
|
159
|
+
|
|
160
|
+
There is no "hourly format" vs "overnight format." Every digest follows the same structure. The overnight one is just longer because it covers more time. Don't phone in the hourly scans with one-line summaries while giving the overnight scan full channel-by-channel treatment.
|
|
161
|
+
|
|
162
|
+
## Verification
|
|
163
|
+
|
|
164
|
+
After setup is complete, suggest the user test the digest by temporarily setting the schedule to run every 2 minutes (`*/2 * * * *`). Watch for the first notification, verify it contains actual content (not a generic status), then switch back to hourly.
|
package/src/config/env.ts
CHANGED
|
@@ -94,10 +94,6 @@ export function getRuntimeHttpHost(): string {
|
|
|
94
94
|
return str("RUNTIME_HTTP_HOST") || "127.0.0.1";
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
-
export function getRuntimeProxyBearerToken(): string | undefined {
|
|
98
|
-
return str("RUNTIME_PROXY_BEARER_TOKEN");
|
|
99
|
-
}
|
|
100
|
-
|
|
101
97
|
export function getRuntimeGatewayOriginSecret(): string | undefined {
|
|
102
98
|
return str("RUNTIME_GATEWAY_ORIGIN_SECRET");
|
|
103
99
|
}
|
|
@@ -82,10 +82,10 @@
|
|
|
82
82
|
"defaultEnabled": false
|
|
83
83
|
},
|
|
84
84
|
{
|
|
85
|
-
"id": "contacts
|
|
86
|
-
"scope": "
|
|
87
|
-
"key": "
|
|
88
|
-
"label": "Contacts
|
|
85
|
+
"id": "contacts",
|
|
86
|
+
"scope": "assistant",
|
|
87
|
+
"key": "feature_flags.contacts.enabled",
|
|
88
|
+
"label": "Contacts",
|
|
89
89
|
"description": "Show the Contacts tab in Settings for viewing and managing contacts",
|
|
90
90
|
"defaultEnabled": false
|
|
91
91
|
},
|
|
@@ -1,17 +1,15 @@
|
|
|
1
1
|
import { readTextFileSync } from "../util/fs.js";
|
|
2
2
|
import { getWorkspacePromptPath } from "../util/platform.js";
|
|
3
3
|
|
|
4
|
-
const DEFAULT_USER_REFERENCE = "my human";
|
|
4
|
+
export const DEFAULT_USER_REFERENCE = "my human";
|
|
5
|
+
export const DECLINED_BY_USER_SENTINEL = "declined_by_user";
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
|
-
*
|
|
8
|
-
* the
|
|
9
|
-
*
|
|
10
|
-
* Reads the "Preferred name/reference:" field from the Onboarding
|
|
11
|
-
* Snapshot section of USER.md. Falls back to "my human" when the
|
|
12
|
-
* file is missing, unreadable, or the field is empty.
|
|
8
|
+
* Read the raw "Preferred name/reference:" value from USER.md.
|
|
9
|
+
* Returns the trimmed value when present, or `null` when the file
|
|
10
|
+
* is missing, unreadable, or the field is empty.
|
|
13
11
|
*/
|
|
14
|
-
|
|
12
|
+
function readPreferredNameFromUserMd(): string | null {
|
|
15
13
|
const content = readTextFileSync(getWorkspacePromptPath("USER.md"));
|
|
16
14
|
if (content != null) {
|
|
17
15
|
const match = content.match(/Preferred name\/reference:[ \t]*(.*)/);
|
|
@@ -19,7 +17,22 @@ export function resolveUserReference(): string {
|
|
|
19
17
|
return match[1].trim();
|
|
20
18
|
}
|
|
21
19
|
}
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
22
|
|
|
23
|
+
/**
|
|
24
|
+
* Resolve the name/reference the assistant uses when referring to
|
|
25
|
+
* the human it represents in external communications.
|
|
26
|
+
*
|
|
27
|
+
* Reads the "Preferred name/reference:" field from the Onboarding
|
|
28
|
+
* Snapshot section of USER.md. Falls back to "my human" when the
|
|
29
|
+
* file is missing, unreadable, or the field is empty.
|
|
30
|
+
*/
|
|
31
|
+
export function resolveUserReference(): string {
|
|
32
|
+
const preferredName = readPreferredNameFromUserMd();
|
|
33
|
+
if (preferredName != null && preferredName !== DECLINED_BY_USER_SENTINEL) {
|
|
34
|
+
return preferredName;
|
|
35
|
+
}
|
|
23
36
|
return DEFAULT_USER_REFERENCE;
|
|
24
37
|
}
|
|
25
38
|
|
|
@@ -62,7 +75,32 @@ export function resolveUserPronouns(): string | null {
|
|
|
62
75
|
}
|
|
63
76
|
|
|
64
77
|
function cleanPronounValue(raw: string): string | null {
|
|
65
|
-
if (raw ===
|
|
78
|
+
if (raw === DECLINED_BY_USER_SENTINEL) return null;
|
|
66
79
|
// Strip "inferred: " prefix for clean output
|
|
67
80
|
return raw.replace(/^inferred:\s*/i, "");
|
|
68
81
|
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Resolve the guardian's display name.
|
|
85
|
+
*
|
|
86
|
+
* Priority:
|
|
87
|
+
* 1. USER.md "Preferred name/reference:" — the user-editable, actively
|
|
88
|
+
* maintained source of truth.
|
|
89
|
+
* 2. guardianDisplayName (fallback for when USER.md is missing or empty,
|
|
90
|
+
* e.g. pre-onboarding). Callers pass in Contact.displayName.
|
|
91
|
+
* 3. DEFAULT_USER_REFERENCE ("my human").
|
|
92
|
+
*/
|
|
93
|
+
export function resolveGuardianName(
|
|
94
|
+
guardianDisplayName?: string | null,
|
|
95
|
+
): string {
|
|
96
|
+
const preferredName = readPreferredNameFromUserMd();
|
|
97
|
+
if (preferredName != null && preferredName !== DECLINED_BY_USER_SENTINEL) {
|
|
98
|
+
return preferredName;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (guardianDisplayName && guardianDisplayName.trim().length > 0) {
|
|
102
|
+
return guardianDisplayName.trim();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return DEFAULT_USER_REFERENCE;
|
|
106
|
+
}
|