@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.
Files changed (186) hide show
  1. package/docs/architecture/memory.md +1 -1
  2. package/package.json +1 -1
  3. package/src/__tests__/access-request-decision.test.ts +85 -4
  4. package/src/__tests__/actor-token-service.test.ts +4 -12
  5. package/src/__tests__/approval-primitive.test.ts +0 -45
  6. package/src/__tests__/approval-routes-http.test.ts +0 -1
  7. package/src/__tests__/assistant-id-boundary-guard.test.ts +150 -0
  8. package/src/__tests__/call-controller.test.ts +0 -1
  9. package/src/__tests__/call-routes-http.test.ts +0 -1
  10. package/src/__tests__/callback-handoff-copy.test.ts +0 -1
  11. package/src/__tests__/channel-approval-routes.test.ts +5 -45
  12. package/src/__tests__/channel-guardian.test.ts +122 -346
  13. package/src/__tests__/channel-invite-transport.test.ts +52 -40
  14. package/src/__tests__/commit-message-enrichment-service.test.ts +4 -38
  15. package/src/__tests__/computer-use-session-working-dir.test.ts +0 -1
  16. package/src/__tests__/confirmation-request-guardian-bridge.test.ts +4 -3
  17. package/src/__tests__/contacts-tools.test.ts +4 -5
  18. package/src/__tests__/conversation-attention-store.test.ts +2 -65
  19. package/src/__tests__/conversation-attention-telegram.test.ts +0 -2
  20. package/src/__tests__/conversation-pairing.test.ts +0 -1
  21. package/src/__tests__/credential-security-invariants.test.ts +1 -0
  22. package/src/__tests__/deterministic-verification-control-plane.test.ts +0 -3
  23. package/src/__tests__/guardian-action-conversation-turn.test.ts +1 -7
  24. package/src/__tests__/guardian-action-followup-executor.test.ts +0 -1
  25. package/src/__tests__/guardian-action-grant-mint-consume.test.ts +0 -74
  26. package/src/__tests__/guardian-action-late-reply.test.ts +1 -8
  27. package/src/__tests__/guardian-dispatch.test.ts +0 -1
  28. package/src/__tests__/guardian-grant-minting.test.ts +0 -1
  29. package/src/__tests__/guardian-outbound-http.test.ts +0 -1
  30. package/src/__tests__/guardian-routing-state.test.ts +0 -3
  31. package/src/__tests__/handlers-telegram-config.test.ts +0 -1
  32. package/src/__tests__/inbound-invite-redemption.test.ts +1 -7
  33. package/src/__tests__/ingress-reconcile.test.ts +3 -36
  34. package/src/__tests__/migration-cross-version-compatibility.test.ts +0 -1
  35. package/src/__tests__/migration-export-http.test.ts +0 -1
  36. package/src/__tests__/migration-import-commit-http.test.ts +0 -1
  37. package/src/__tests__/migration-import-preflight-http.test.ts +0 -1
  38. package/src/__tests__/migration-validate-http.test.ts +0 -1
  39. package/src/__tests__/non-member-access-request.test.ts +0 -8
  40. package/src/__tests__/notification-broadcaster.test.ts +1 -2
  41. package/src/__tests__/notification-decision-fallback.test.ts +0 -2
  42. package/src/__tests__/notification-decision-strategy.test.ts +0 -1
  43. package/src/__tests__/notification-guardian-path.test.ts +0 -1
  44. package/src/__tests__/notification-telegram-adapter.test.ts +0 -4
  45. package/src/__tests__/relay-server.test.ts +151 -80
  46. package/src/__tests__/sandbox-host-parity.test.ts +5 -2
  47. package/src/__tests__/scoped-approval-grants.test.ts +9 -40
  48. package/src/__tests__/scoped-grant-security-matrix.test.ts +0 -36
  49. package/src/__tests__/send-endpoint-busy.test.ts +0 -1
  50. package/src/__tests__/send-notification-tool.test.ts +0 -1
  51. package/src/__tests__/session-init.benchmark.test.ts +0 -2
  52. package/src/__tests__/slack-channel-config.test.ts +0 -1
  53. package/src/__tests__/slack-inbound-verification.test.ts +2 -5
  54. package/src/__tests__/sms-messaging-provider.test.ts +0 -4
  55. package/src/__tests__/terminal-tools.test.ts +5 -2
  56. package/src/__tests__/thread-seed-composer.test.ts +0 -1
  57. package/src/__tests__/tool-approval-handler.test.ts +0 -1
  58. package/src/__tests__/tool-grant-request-escalation.test.ts +0 -4
  59. package/src/__tests__/trusted-contact-approval-notifier.test.ts +65 -77
  60. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +0 -1
  61. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +1 -18
  62. package/src/__tests__/trusted-contact-multichannel.test.ts +0 -14
  63. package/src/__tests__/trusted-contact-verification.test.ts +3 -16
  64. package/src/__tests__/twilio-routes.test.ts +2 -3
  65. package/src/__tests__/update-bulletin.test.ts +0 -2
  66. package/src/__tests__/user-reference.test.ts +47 -1
  67. package/src/__tests__/voice-invite-redemption.test.ts +0 -1
  68. package/src/__tests__/voice-scoped-grant-consumer.test.ts +0 -38
  69. package/src/__tests__/workspace-git-service.test.ts +2 -2
  70. package/src/approvals/approval-primitive.ts +0 -15
  71. package/src/approvals/guardian-decision-primitive.ts +0 -3
  72. package/src/approvals/guardian-request-resolvers.ts +0 -5
  73. package/src/calls/call-domain.ts +0 -3
  74. package/src/calls/call-store.ts +0 -3
  75. package/src/calls/guardian-action-sweep.ts +2 -1
  76. package/src/calls/guardian-dispatch.ts +1 -2
  77. package/src/calls/relay-access-wait.ts +0 -4
  78. package/src/calls/relay-server.ts +8 -66
  79. package/src/calls/relay-setup-router.ts +1 -2
  80. package/src/calls/relay-verification.ts +0 -1
  81. package/src/calls/twilio-routes.ts +0 -3
  82. package/src/calls/types.ts +0 -1
  83. package/src/calls/voice-session-bridge.ts +0 -1
  84. package/src/channels/config.ts +41 -2
  85. package/src/config/bundled-skills/notifications/tools/send-notification.ts +0 -1
  86. package/src/config/bundled-skills/slack/SKILL.md +2 -0
  87. package/src/config/bundled-skills/slack-digest-setup/SKILL.md +164 -0
  88. package/src/config/env.ts +0 -4
  89. package/src/config/feature-flag-registry.json +4 -4
  90. package/src/config/user-reference.ts +47 -9
  91. package/src/contacts/contact-store.ts +13 -88
  92. package/src/contacts/contacts-write.ts +3 -11
  93. package/src/contacts/types.ts +0 -1
  94. package/src/daemon/handlers/config-channels.ts +19 -44
  95. package/src/daemon/handlers/config-inbox.ts +6 -6
  96. package/src/daemon/handlers/contacts.ts +8 -12
  97. package/src/daemon/handlers/index.ts +0 -2
  98. package/src/daemon/lifecycle.ts +18 -26
  99. package/src/daemon/session-process.ts +0 -4
  100. package/src/memory/channel-delivery-store.ts +1 -0
  101. package/src/memory/conversation-attention-store.ts +4 -19
  102. package/src/memory/conversation-crud.ts +0 -2
  103. package/src/memory/db-init.ts +8 -0
  104. package/src/memory/delivery-crud.ts +13 -0
  105. package/src/memory/guardian-action-store.ts +0 -12
  106. package/src/memory/guardian-approvals.ts +35 -80
  107. package/src/memory/guardian-rate-limits.ts +1 -14
  108. package/src/memory/guardian-verification.ts +6 -34
  109. package/src/memory/invite-store.ts +76 -15
  110. package/src/memory/migrations/040-invite-code-hash-column.ts +16 -0
  111. package/src/memory/migrations/134-contacts-notes-column.ts +64 -45
  112. package/src/memory/migrations/136-drop-assistant-id-columns.ts +263 -0
  113. package/src/memory/migrations/index.ts +2 -0
  114. package/src/memory/migrations/registry.ts +14 -1
  115. package/src/memory/schema/calls.ts +0 -7
  116. package/src/memory/schema/contacts.ts +2 -8
  117. package/src/memory/schema/guardian.ts +0 -5
  118. package/src/memory/schema/infrastructure.ts +0 -2
  119. package/src/memory/schema/notifications.ts +3 -17
  120. package/src/memory/scoped-approval-grants.ts +2 -24
  121. package/src/notifications/adapters/sms.ts +2 -1
  122. package/src/notifications/broadcaster.ts +1 -6
  123. package/src/notifications/decision-engine.ts +3 -4
  124. package/src/notifications/deliveries-store.ts +0 -4
  125. package/src/notifications/destination-resolver.ts +4 -6
  126. package/src/notifications/deterministic-checks.ts +1 -6
  127. package/src/notifications/emit-signal.ts +4 -11
  128. package/src/notifications/events-store.ts +7 -17
  129. package/src/notifications/preference-summary.ts +2 -2
  130. package/src/notifications/preferences-store.ts +2 -9
  131. package/src/notifications/signal.ts +0 -1
  132. package/src/notifications/thread-candidates.ts +1 -11
  133. package/src/notifications/types.ts +0 -3
  134. package/src/runtime/access-request-helper.ts +3 -10
  135. package/src/runtime/actor-refresh-token-store.ts +0 -6
  136. package/src/runtime/actor-token-store.ts +3 -16
  137. package/src/runtime/actor-trust-resolver.ts +1 -4
  138. package/src/runtime/auth/__tests__/credential-service.test.ts +0 -9
  139. package/src/runtime/auth/__tests__/guard-tests.test.ts +1 -3
  140. package/src/runtime/auth/credential-service.ts +1 -15
  141. package/src/runtime/auth/require-bound-guardian.ts +1 -4
  142. package/src/runtime/auth/token-service.ts +50 -0
  143. package/src/runtime/channel-guardian-service.ts +16 -49
  144. package/src/runtime/channel-invite-transport.ts +129 -34
  145. package/src/runtime/channel-invite-transports/email.ts +54 -0
  146. package/src/runtime/channel-invite-transports/slack.ts +87 -0
  147. package/src/runtime/channel-invite-transports/sms.ts +74 -0
  148. package/src/runtime/channel-invite-transports/telegram.ts +35 -11
  149. package/src/runtime/channel-invite-transports/voice.ts +12 -12
  150. package/src/runtime/confirmation-request-guardian-bridge.ts +0 -1
  151. package/src/runtime/guardian-action-followup-executor.ts +3 -2
  152. package/src/runtime/guardian-action-grant-minter.ts +0 -1
  153. package/src/runtime/guardian-outbound-actions.ts +2 -12
  154. package/src/runtime/guardian-vellum-migration.ts +2 -3
  155. package/src/runtime/http-server.ts +0 -1
  156. package/src/runtime/invite-redemption-service.ts +191 -11
  157. package/src/runtime/invite-redemption-templates.ts +6 -6
  158. package/src/runtime/invite-service.ts +81 -11
  159. package/src/runtime/local-actor-identity.ts +2 -5
  160. package/src/runtime/routes/access-request-decision.ts +52 -7
  161. package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +6 -9
  162. package/src/runtime/routes/channel-readiness-routes.ts +29 -18
  163. package/src/runtime/routes/contact-routes.ts +48 -46
  164. package/src/runtime/routes/conversation-attention-routes.ts +0 -2
  165. package/src/runtime/routes/global-search-routes.ts +0 -2
  166. package/src/runtime/routes/guardian-bootstrap-routes.ts +6 -12
  167. package/src/runtime/routes/guardian-expiry-sweep.ts +3 -2
  168. package/src/runtime/routes/inbound-message-handler.ts +1 -6
  169. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +296 -47
  170. package/src/runtime/routes/inbound-stages/background-dispatch.ts +6 -42
  171. package/src/runtime/routes/inbound-stages/bootstrap-intercept.ts +1 -6
  172. package/src/runtime/routes/inbound-stages/edit-intercept.ts +10 -0
  173. package/src/runtime/routes/inbound-stages/escalation-intercept.ts +0 -1
  174. package/src/runtime/routes/inbound-stages/secret-ingress-check.ts +0 -1
  175. package/src/runtime/routes/inbound-stages/verification-intercept.ts +3 -7
  176. package/src/runtime/routes/invite-routes.ts +1 -0
  177. package/src/runtime/routes/pairing-routes.ts +4 -4
  178. package/src/runtime/tool-grant-request-helper.ts +0 -1
  179. package/src/tools/browser/browser-manager.ts +22 -12
  180. package/src/tools/browser/runtime-check.ts +110 -3
  181. package/src/tools/calls/call-start.ts +1 -3
  182. package/src/tools/followups/followup_create.ts +1 -2
  183. package/src/tools/shared/shell-output.ts +7 -2
  184. package/src/tools/tool-approval-handler.ts +0 -2
  185. package/src/util/platform.ts +0 -4
  186. 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 { resolveUserReference } from "../config/user-reference.js";
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(assistantId, "voice");
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
- * Prefers displayName from the guardian binding metadata, falls back
1601
- * to @username, then the user's preferred name from USER.md.
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
- const assistantId =
1605
- this.accessRequestAssistantId ?? DAEMON_INTERNAL_ASSISTANT_ID;
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 resolveUserReference();
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(assistantId, "voice");
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) {
@@ -126,7 +126,6 @@ export function attemptGuardianCodeVerification(
126
126
  } = params;
127
127
 
128
128
  const result = validateAndConsumeChallenge(
129
- guardianChallengeAssistantId,
130
129
  "voice",
131
130
  enteredCode,
132
131
  guardianVerificationFromNumber,
@@ -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 {
@@ -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,
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Canonical per-channel notification policy registry.
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 notification policy for a channel. */
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
+ }
@@ -131,7 +131,6 @@ export async function run(
131
131
  sourceEventName,
132
132
  sourceChannel: "assistant_tool",
133
133
  sourceSessionId,
134
- assistantId: context.assistantId,
135
134
  attentionHints: {
136
135
  requiresAction: parseBool(input.requires_action, true),
137
136
  urgency,
@@ -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-tab",
86
- "scope": "macos",
87
- "key": "contacts_tab",
88
- "label": "Contacts Tab",
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
- * Resolve the name/reference the assistant uses when referring to
8
- * the human it represents in external communications.
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
- export function resolveUserReference(): string {
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 === "declined_by_user") return null;
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
+ }