@vellumai/assistant 0.4.2 → 0.4.3

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 (84) hide show
  1. package/ARCHITECTURE.md +84 -7
  2. package/docs/trusted-contact-access.md +20 -0
  3. package/package.json +1 -1
  4. package/src/__tests__/access-request-decision.test.ts +0 -1
  5. package/src/__tests__/assistant-id-boundary-guard.test.ts +290 -0
  6. package/src/__tests__/call-routes-http.test.ts +0 -25
  7. package/src/__tests__/channel-guardian.test.ts +6 -5
  8. package/src/__tests__/config-schema.test.ts +2 -0
  9. package/src/__tests__/deterministic-verification-control-plane.test.ts +0 -1
  10. package/src/__tests__/guardian-actions-endpoint.test.ts +21 -0
  11. package/src/__tests__/guardian-outbound-http.test.ts +0 -1
  12. package/src/__tests__/inbound-invite-redemption.test.ts +0 -1
  13. package/src/__tests__/ingress-routes-http.test.ts +55 -0
  14. package/src/__tests__/non-member-access-request.test.ts +28 -1
  15. package/src/__tests__/notification-decision-strategy.test.ts +44 -0
  16. package/src/__tests__/relay-server.test.ts +644 -4
  17. package/src/__tests__/session-init.benchmark.test.ts +0 -1
  18. package/src/__tests__/session-runtime-assembly.test.ts +4 -1
  19. package/src/__tests__/session-surfaces-task-progress.test.ts +43 -0
  20. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +11 -1
  21. package/src/__tests__/trusted-contact-multichannel.test.ts +0 -1
  22. package/src/__tests__/trusted-contact-verification.test.ts +0 -1
  23. package/src/__tests__/twilio-routes.test.ts +4 -3
  24. package/src/__tests__/update-bulletin.test.ts +0 -1
  25. package/src/approvals/guardian-decision-primitive.ts +2 -1
  26. package/src/approvals/guardian-request-resolvers.ts +42 -3
  27. package/src/calls/call-constants.ts +8 -0
  28. package/src/calls/call-controller.ts +2 -1
  29. package/src/calls/call-domain.ts +5 -4
  30. package/src/calls/relay-server.ts +513 -116
  31. package/src/calls/twilio-routes.ts +3 -5
  32. package/src/calls/types.ts +1 -1
  33. package/src/calls/voice-session-bridge.ts +4 -3
  34. package/src/cli/core-commands.ts +7 -4
  35. package/src/config/bundled-skills/app-builder/SKILL.md +164 -1
  36. package/src/config/bundled-skills/vercel-token-setup/SKILL.md +214 -0
  37. package/src/config/calls-schema.ts +12 -0
  38. package/src/config/feature-flag-registry.json +0 -8
  39. package/src/config/vellum-skills/trusted-contacts/SKILL.md +8 -2
  40. package/src/daemon/handlers/config-channels.ts +5 -7
  41. package/src/daemon/handlers/config-inbox.ts +2 -0
  42. package/src/daemon/handlers/index.ts +2 -1
  43. package/src/daemon/handlers/publish.ts +11 -46
  44. package/src/daemon/handlers/sessions.ts +11 -2
  45. package/src/daemon/ipc-contract/apps.ts +1 -0
  46. package/src/daemon/ipc-contract/inbox.ts +4 -0
  47. package/src/daemon/ipc-contract/integrations.ts +3 -1
  48. package/src/daemon/server.ts +2 -1
  49. package/src/daemon/session-agent-loop.ts +2 -1
  50. package/src/daemon/session-runtime-assembly.ts +3 -1
  51. package/src/daemon/session-surfaces.ts +29 -1
  52. package/src/memory/conversation-crud.ts +2 -1
  53. package/src/memory/conversation-title-service.ts +16 -2
  54. package/src/memory/db-init.ts +4 -0
  55. package/src/memory/delivery-crud.ts +2 -1
  56. package/src/memory/guardian-action-store.ts +2 -1
  57. package/src/memory/guardian-approvals.ts +3 -2
  58. package/src/memory/ingress-invite-store.ts +12 -2
  59. package/src/memory/ingress-member-store.ts +4 -3
  60. package/src/memory/migrations/124-voice-invite-display-metadata.ts +14 -0
  61. package/src/memory/migrations/index.ts +1 -0
  62. package/src/memory/schema.ts +10 -5
  63. package/src/notifications/copy-composer.ts +11 -1
  64. package/src/notifications/emit-signal.ts +2 -1
  65. package/src/runtime/access-request-helper.ts +11 -3
  66. package/src/runtime/actor-trust-resolver.ts +2 -2
  67. package/src/runtime/assistant-scope.ts +10 -0
  68. package/src/runtime/guardian-outbound-actions.ts +5 -4
  69. package/src/runtime/http-server.ts +11 -20
  70. package/src/runtime/ingress-service.ts +14 -0
  71. package/src/runtime/invite-redemption-service.ts +2 -1
  72. package/src/runtime/middleware/twilio-validation.ts +2 -4
  73. package/src/runtime/routes/call-routes.ts +2 -1
  74. package/src/runtime/routes/channel-route-shared.ts +3 -3
  75. package/src/runtime/routes/conversation-attention-routes.ts +2 -1
  76. package/src/runtime/routes/conversation-routes.ts +2 -1
  77. package/src/runtime/routes/events-routes.ts +2 -3
  78. package/src/runtime/routes/inbound-conversation.ts +4 -3
  79. package/src/runtime/routes/inbound-message-handler.ts +4 -3
  80. package/src/runtime/routes/ingress-routes.ts +2 -0
  81. package/src/tools/calls/call-start.ts +2 -1
  82. package/src/tools/terminal/parser.ts +12 -0
  83. package/src/tools/tool-approval-handler.ts +2 -1
  84. package/src/workspace/git-service.ts +19 -0
@@ -105,14 +105,22 @@ export function notifyGuardianOfAccessRequest(
105
105
  }
106
106
  }
107
107
 
108
+ // The conversationId is assistant-scoped so the dedupe query below only
109
+ // matches requests for the same assistant. Without this, a pending request
110
+ // from assistant A could be returned for assistant B, allowing the caller
111
+ // to piggyback on A's guardian approval.
112
+ const conversationId = `access-req-${canonicalAssistantId}-${sourceChannel}-${senderExternalUserId}`;
113
+
108
114
  // Deduplicate: skip creation if there is already a pending canonical request
109
- // for the same requester on this channel. Still return notified: true with
110
- // the existing request ID so callers know the guardian was already notified.
115
+ // for the same requester on this channel *and* assistant. Still return
116
+ // notified: true with the existing request ID so callers know the guardian
117
+ // was already notified.
111
118
  const existingCanonical = listCanonicalGuardianRequests({
112
119
  status: 'pending',
113
120
  requesterExternalUserId: senderExternalUserId,
114
121
  sourceChannel,
115
122
  kind: 'access_request',
123
+ conversationId,
116
124
  });
117
125
  if (existingCanonical.length > 0) {
118
126
  log.debug(
@@ -130,7 +138,7 @@ export function notifyGuardianOfAccessRequest(
130
138
  kind: 'access_request',
131
139
  sourceType: 'channel',
132
140
  sourceChannel,
133
- conversationId: `access-req-${sourceChannel}-${senderExternalUserId}`,
141
+ conversationId,
134
142
  requesterExternalUserId: senderExternalUserId,
135
143
  requesterChatId: externalChatId,
136
144
  guardianExternalUserId: guardianExternalUserId ?? undefined,
@@ -17,7 +17,7 @@ import type { GuardianRuntimeContext } from '../daemon/session-runtime-assembly.
17
17
  import type { IngressMember } from '../memory/ingress-member-store.js';
18
18
  import { findMember } from '../memory/ingress-member-store.js';
19
19
  import { canonicalizeInboundIdentity } from '../util/canonicalize-identity.js';
20
- import { normalizeAssistantId } from '../util/platform.js';
20
+ import { DAEMON_INTERNAL_ASSISTANT_ID } from './assistant-scope.js';
21
21
  import { getGuardianBinding } from './channel-guardian-service.js';
22
22
 
23
23
  // ---------------------------------------------------------------------------
@@ -76,7 +76,7 @@ export interface ResolveActorTrustInput {
76
76
  * 5. Classify: guardian > trusted_contact (active member) > unknown.
77
77
  */
78
78
  export function resolveActorTrust(input: ResolveActorTrustInput): ActorTrustContext {
79
- const assistantId = normalizeAssistantId(input.assistantId);
79
+ const assistantId = DAEMON_INTERNAL_ASSISTANT_ID;
80
80
 
81
81
  const rawUserId = typeof input.senderExternalUserId === 'string' && input.senderExternalUserId.trim().length > 0
82
82
  ? input.senderExternalUserId.trim()
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Canonical internal scope ID for all daemon-side assistant-scoped storage.
3
+ *
4
+ * The daemon uses a single fixed identity (`'self'`) for its own assistant
5
+ * scope. Public/external assistant IDs are an edge concern owned by the
6
+ * gateway and platform layers (hatch, invite links, etc.). Daemon code
7
+ * should never derive scoping decisions from externally-provided assistant
8
+ * IDs — use this constant instead.
9
+ */
10
+ export const DAEMON_INTERNAL_ASSISTANT_ID = 'self' as const;
@@ -16,7 +16,8 @@ import { sendMessage as sendSms } from '../messaging/providers/sms/client.js';
16
16
  import { getCredentialMetadata } from '../tools/credentials/metadata-store.js';
17
17
  import { getLogger } from '../util/logger.js';
18
18
  import { normalizePhoneNumber } from '../util/phone.js';
19
- import { normalizeAssistantId, readHttpToken } from '../util/platform.js';
19
+ import { readHttpToken } from '../util/platform.js';
20
+ import { DAEMON_INTERNAL_ASSISTANT_ID } from './assistant-scope.js';
20
21
  import {
21
22
  countRecentSendsToDestination,
22
23
  createOutboundSession,
@@ -243,7 +244,7 @@ function initiateGuardianVoiceCall(
243
244
  // ---------------------------------------------------------------------------
244
245
 
245
246
  export function startOutbound(params: StartOutboundParams): OutboundActionResult {
246
- const assistantId = normalizeAssistantId(params.assistantId ?? 'self');
247
+ const assistantId = DAEMON_INTERNAL_ASSISTANT_ID;
247
248
  const channel = params.channel;
248
249
  const originConversationId = params.originConversationId;
249
250
 
@@ -541,7 +542,7 @@ function startOutboundVoice(
541
542
  // ---------------------------------------------------------------------------
542
543
 
543
544
  export function resendOutbound(params: ResendOutboundParams): OutboundActionResult {
544
- const assistantId = normalizeAssistantId(params.assistantId ?? 'self');
545
+ const assistantId = DAEMON_INTERNAL_ASSISTANT_ID;
545
546
  const channel = params.channel;
546
547
  const originConversationId = params.originConversationId;
547
548
 
@@ -707,7 +708,7 @@ export function resendOutbound(params: ResendOutboundParams): OutboundActionResu
707
708
  // ---------------------------------------------------------------------------
708
709
 
709
710
  export function cancelOutbound(params: CancelOutboundParams): OutboundActionResult {
710
- const assistantId = normalizeAssistantId(params.assistantId ?? 'self');
711
+ const assistantId = DAEMON_INTERNAL_ASSISTANT_ID;
711
712
  const channel = params.channel;
712
713
 
713
714
  const session = findActiveSession(assistantId, channel);
@@ -40,6 +40,7 @@ import { consumeCallback, consumeCallbackError } from '../security/oauth-callbac
40
40
  import { getLogger } from '../util/logger.js';
41
41
  import { buildAssistantEvent } from './assistant-event.js';
42
42
  import { assistantEventHub } from './assistant-event-hub.js';
43
+ import { DAEMON_INTERNAL_ASSISTANT_ID } from './assistant-scope.js';
43
44
  import { sweepFailedEvents } from './channel-retry-sweep.js';
44
45
  import { httpError } from './http-errors.js';
45
46
  // Middleware
@@ -97,7 +98,6 @@ import {
97
98
  startCanonicalGuardianExpirySweep,
98
99
  stopCanonicalGuardianExpirySweep,
99
100
  } from './routes/canonical-guardian-expiry-sweep.js';
100
- import { canonicalChannelAssistantId } from './routes/channel-route-shared.js';
101
101
  import {
102
102
  handleChannelDeliveryAck,
103
103
  handleChannelInbound,
@@ -270,7 +270,7 @@ export class RuntimeHttpServer {
270
270
  ipcBroadcast(msg);
271
271
  // Also publish to the event hub so HTTP/SSE clients (e.g. macOS
272
272
  // app with localHttpEnabled) receive pairing approval requests.
273
- void assistantEventHub.publish(buildAssistantEvent('self', msg));
273
+ void assistantEventHub.publish(buildAssistantEvent(DAEMON_INTERNAL_ASSISTANT_ID, msg));
274
274
  }
275
275
  : undefined,
276
276
  };
@@ -521,22 +521,13 @@ export class RuntimeHttpServer {
521
521
  }
522
522
  }
523
523
 
524
- // New assistant-less runtime routes: /v1/<endpoint>
525
- const newRouteMatch = path.match(/^\/v1\/(?!assistants\/)(.+)$/);
526
- if (newRouteMatch) {
527
- return this.dispatchEndpoint(newRouteMatch[1], req, url);
524
+ // Runtime routes: /v1/<endpoint>
525
+ const routeMatch = path.match(/^\/v1\/(.+)$/);
526
+ if (routeMatch) {
527
+ return this.dispatchEndpoint(routeMatch[1], req, url);
528
528
  }
529
529
 
530
- // Legacy: /v1/assistants/:assistantId/<endpoint>
531
- const match = path.match(/^\/v1\/assistants\/([^/]+)\/(.+)$/);
532
- if (!match) {
533
- return httpError('NOT_FOUND', 'Not found', 404);
534
- }
535
-
536
- const assistantId = canonicalChannelAssistantId(match[1]);
537
- const endpoint = match[2];
538
- log.warn({ endpoint, assistantId }, '[deprecated] /v1/assistants/:assistantId/... route used; migrate to /v1/...');
539
- return this.dispatchEndpoint(endpoint, req, url, assistantId);
530
+ return httpError('NOT_FOUND', 'Not found', 404);
540
531
  }
541
532
 
542
533
  private handleBrowserRelayUpgrade(req: Request, server: ReturnType<typeof Bun.serve>): Response {
@@ -616,8 +607,8 @@ export class RuntimeHttpServer {
616
607
  endpoint: string,
617
608
  req: Request,
618
609
  url: URL,
619
- assistantId: string = 'self',
620
610
  ): Promise<Response> {
611
+ const assistantId = DAEMON_INTERNAL_ASSISTANT_ID;
621
612
  return withErrorHandling(endpoint, async () => {
622
613
  if (endpoint === 'health' && req.method === 'GET') return handleHealth();
623
614
  if (endpoint === 'debug' && req.method === 'GET') return handleDebug();
@@ -690,7 +681,7 @@ export class RuntimeHttpServer {
690
681
  try {
691
682
  recordConversationSeenSignal({
692
683
  conversationId,
693
- assistantId: 'self',
684
+ assistantId: DAEMON_INTERNAL_ASSISTANT_ID,
694
685
  sourceChannel: (body.sourceChannel as string) ?? 'vellum',
695
686
  signalType: (body.signalType as string ?? 'macos_conversation_opened') as SignalType,
696
687
  confidence: (body.confidence as string ?? 'explicit') as Confidence,
@@ -813,11 +804,11 @@ export class RuntimeHttpServer {
813
804
 
814
805
  // Internal Twilio forwarding endpoints (gateway -> runtime)
815
806
  if (endpoint === 'internal/twilio/voice-webhook' && req.method === 'POST') {
816
- const json = await req.json() as { params: Record<string, string>; originalUrl?: string; assistantId?: string };
807
+ const json = await req.json() as { params: Record<string, string>; originalUrl?: string };
817
808
  const formBody = new URLSearchParams(json.params).toString();
818
809
  const reconstructedUrl = json.originalUrl ?? req.url;
819
810
  const fakeReq = new Request(reconstructedUrl, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: formBody });
820
- return await handleVoiceWebhook(fakeReq, json.assistantId);
811
+ return await handleVoiceWebhook(fakeReq);
821
812
  }
822
813
 
823
814
  if (endpoint === 'internal/twilio/status' && req.method === 'POST') {
@@ -57,6 +57,8 @@ export interface InviteResponseData {
57
57
  expectedExternalUserId?: string;
58
58
  voiceCode?: string;
59
59
  voiceCodeDigits?: number;
60
+ friendName?: string;
61
+ guardianName?: string;
60
62
  createdAt: number;
61
63
  }
62
64
 
@@ -110,6 +112,8 @@ function inviteToResponse(inv: IngressInvite, opts?: { rawToken?: string; voiceC
110
112
  ...(inv.expectedExternalUserId ? { expectedExternalUserId: inv.expectedExternalUserId } : {}),
111
113
  ...(opts?.voiceCode ? { voiceCode: opts.voiceCode } : {}),
112
114
  ...(inv.voiceCodeDigits != null ? { voiceCodeDigits: inv.voiceCodeDigits } : {}),
115
+ ...(inv.friendName ? { friendName: inv.friendName } : {}),
116
+ ...(inv.guardianName ? { guardianName: inv.guardianName } : {}),
113
117
  createdAt: inv.createdAt,
114
118
  };
115
119
  }
@@ -149,6 +153,8 @@ export function createIngressInvite(params: {
149
153
  // Voice invite parameters
150
154
  expectedExternalUserId?: string;
151
155
  voiceCodeDigits?: number;
156
+ friendName?: string;
157
+ guardianName?: string;
152
158
  }): IngressResult<InviteResponseData> {
153
159
  if (!params.sourceChannel) {
154
160
  return { ok: false, error: 'sourceChannel is required for create' };
@@ -168,6 +174,12 @@ export function createIngressInvite(params: {
168
174
  if (!isValidE164(params.expectedExternalUserId)) {
169
175
  return { ok: false, error: 'expectedExternalUserId must be in E.164 format (e.g., +15551234567)' };
170
176
  }
177
+ if (typeof params.friendName !== 'string' || !params.friendName.trim()) {
178
+ return { ok: false, error: 'friendName is required for voice invites' };
179
+ }
180
+ if (typeof params.guardianName !== 'string' || !params.guardianName.trim()) {
181
+ return { ok: false, error: 'guardianName is required for voice invites' };
182
+ }
171
183
  voiceCode = generateVoiceCode(6);
172
184
  voiceCodeHash = hashVoiceCode(voiceCode);
173
185
  }
@@ -181,6 +193,8 @@ export function createIngressInvite(params: {
181
193
  expectedExternalUserId: params.expectedExternalUserId,
182
194
  voiceCodeHash,
183
195
  voiceCodeDigits: 6,
196
+ friendName: params.friendName,
197
+ guardianName: params.guardianName,
184
198
  } : {}),
185
199
  });
186
200
  // Voice invites must not expose the token — callers must redeem via the
@@ -13,6 +13,7 @@ import { findActiveVoiceInvites,findByTokenHash, hashToken, markInviteExpired, r
13
13
  import { findMember, upsertMember } from '../memory/ingress-member-store.js';
14
14
  import { canonicalizeInboundIdentity } from '../util/canonicalize-identity.js';
15
15
  import { hashVoiceCode } from '../util/voice-code.js';
16
+ import { DAEMON_INTERNAL_ASSISTANT_ID } from './assistant-scope.js';
16
17
 
17
18
  // ---------------------------------------------------------------------------
18
19
  // Outcome type
@@ -223,7 +224,7 @@ export function redeemVoiceInviteCode(params: {
223
224
  sourceChannel: 'voice';
224
225
  code: string;
225
226
  }): VoiceRedemptionOutcome {
226
- const { assistantId = 'self', callerExternalUserId, code } = params;
227
+ const { assistantId = DAEMON_INTERNAL_ASSISTANT_ID, callerExternalUserId, code } = params;
227
228
 
228
229
  if (!callerExternalUserId) {
229
230
  return { ok: false, reason: 'invalid_or_expired' };
@@ -12,12 +12,10 @@ import { httpError } from '../http-errors.js';
12
12
  const log = getLogger('runtime-http');
13
13
 
14
14
  /**
15
- * Regex to extract the Twilio webhook subpath from both top-level and
16
- * assistant-scoped route shapes:
15
+ * Regex to extract the Twilio webhook subpath:
17
16
  * /v1/calls/twilio/<subpath>
18
- * /v1/assistants/<id>/calls/twilio/<subpath>
19
17
  */
20
- export const TWILIO_WEBHOOK_RE = /^\/v1\/(?:assistants\/[^/]+\/)?calls\/twilio\/(.+)$/;
18
+ export const TWILIO_WEBHOOK_RE = /^\/v1\/calls\/twilio\/(.+)$/;
21
19
 
22
20
  /**
23
21
  * Gateway-compatible Twilio webhook paths:
@@ -11,6 +11,7 @@
11
11
  import { answerCall, cancelCall, getCallStatus, relayInstruction,startCall } from '../../calls/call-domain.js';
12
12
  import { getConfig } from '../../config/loader.js';
13
13
  import { VALID_CALLER_IDENTITY_MODES } from '../../config/schema.js';
14
+ import { DAEMON_INTERNAL_ASSISTANT_ID } from '../assistant-scope.js';
14
15
  import { httpError, httpErrorCodeFromStatus } from '../http-errors.js';
15
16
 
16
17
  // ── Idempotency cache ─────────────────────────────────────────────────────────
@@ -41,7 +42,7 @@ function pruneIdempotencyCache(): void {
41
42
  * Optional `idempotencyKey`: if supplied, duplicate requests with the same key
42
43
  * within 5 minutes return the cached 201 response without starting a second call.
43
44
  */
44
- export async function handleStartCall(req: Request, assistantId: string = 'self'): Promise<Response> {
45
+ export async function handleStartCall(req: Request, assistantId: string = DAEMON_INTERNAL_ASSISTANT_ID): Promise<Response> {
45
46
  if (!getConfig().calls.enabled) {
46
47
  return httpError('FORBIDDEN', 'Calls feature is disabled via configuration. Set calls.enabled to true to use this feature.', 403);
47
48
  }
@@ -4,7 +4,7 @@
4
4
  import { timingSafeEqual } from 'node:crypto';
5
5
 
6
6
  import type { ChannelId } from '../../channels/types.js';
7
- import { normalizeAssistantId } from '../../util/platform.js';
7
+ import { DAEMON_INTERNAL_ASSISTANT_ID } from '../assistant-scope.js';
8
8
  import type {
9
9
  ApprovalAction,
10
10
  ApprovalDecisionResult,
@@ -15,8 +15,8 @@ export type { ActorTrustClass, DenialReason, GuardianContext } from '../guardian
15
15
  export { toGuardianRuntimeContext } from '../guardian-context-resolver.js';
16
16
 
17
17
  /** Canonicalize assistantId for channel ingress paths. */
18
- export function canonicalChannelAssistantId(assistantId: string): string {
19
- return normalizeAssistantId(assistantId);
18
+ export function canonicalChannelAssistantId(_assistantId: string): string {
19
+ return DAEMON_INTERNAL_ASSISTANT_ID;
20
20
  }
21
21
 
22
22
  // ---------------------------------------------------------------------------
@@ -10,6 +10,7 @@ import {
10
10
  } from '../../memory/conversation-attention-store.js';
11
11
  import * as conversationStore from '../../memory/conversation-store.js';
12
12
  import { truncate } from '../../util/truncate.js';
13
+ import { DAEMON_INTERNAL_ASSISTANT_ID } from '../assistant-scope.js';
13
14
  import { httpError } from '../http-errors.js';
14
15
 
15
16
  export function handleListConversationAttention(url: URL): Response {
@@ -27,7 +28,7 @@ export function handleListConversationAttention(url: URL): Response {
27
28
  }
28
29
 
29
30
  const attentionStates = listConversationAttention({
30
- assistantId: 'self',
31
+ assistantId: DAEMON_INTERNAL_ASSISTANT_ID,
31
32
  state: stateParam as AttentionFilterState,
32
33
  sourceChannel: channel,
33
34
  source: sourceParam !== 'all' ? sourceParam : undefined,
@@ -24,6 +24,7 @@ import { getConfiguredProvider } from '../../providers/provider-send-message.js'
24
24
  import type { Provider } from '../../providers/types.js';
25
25
  import { getLogger } from '../../util/logger.js';
26
26
  import { buildAssistantEvent } from '../assistant-event.js';
27
+ import { DAEMON_INTERNAL_ASSISTANT_ID } from '../assistant-scope.js';
27
28
  import { routeGuardianReply } from '../guardian-reply-router.js';
28
29
  import { httpError } from '../http-errors.js';
29
30
  import type {
@@ -363,7 +364,7 @@ function makeHubPublisher(
363
364
  ? (msg as { sessionId: string }).sessionId
364
365
  : undefined;
365
366
  const resolvedSessionId = msgSessionId ?? conversationId;
366
- const event = buildAssistantEvent('self', msg, resolvedSessionId);
367
+ const event = buildAssistantEvent(DAEMON_INTERNAL_ASSISTANT_ID, msg, resolvedSessionId);
367
368
  hubChain = (async () => {
368
369
  await hubChain;
369
370
  try {
@@ -11,6 +11,7 @@ import { getOrCreateConversation } from '../../memory/conversation-key-store.js'
11
11
  import { formatSseFrame, formatSseHeartbeat } from '../assistant-event.js';
12
12
  import type { AssistantEventSubscription } from '../assistant-event-hub.js';
13
13
  import { AssistantEventHub,assistantEventHub } from '../assistant-event-hub.js';
14
+ import { DAEMON_INTERNAL_ASSISTANT_ID } from '../assistant-scope.js';
14
15
  import { httpError } from '../http-errors.js';
15
16
 
16
17
  /** Keep-alive comment sent to idle clients every 30 s by default. */
@@ -50,8 +51,6 @@ export function handleSubscribeAssistantEvents(
50
51
  // closures are in place before events can arrive. `controllerRef` is set
51
52
  // synchronously inside ReadableStream's start(), so it is non-null by the
52
53
  // time any event or eviction fires.
53
- // 'self' is the assistantId used by buildAssistantEvent('self', ...) for
54
- // all HTTP and voice session events.
55
54
  let controllerRef: ReadableStreamDefaultController<Uint8Array> | null = null;
56
55
  let heartbeatTimer: ReturnType<typeof setInterval> | null = null;
57
56
  let sub!: AssistantEventSubscription;
@@ -63,7 +62,7 @@ export function handleSubscribeAssistantEvents(
63
62
 
64
63
  try {
65
64
  sub = hub.subscribe(
66
- { assistantId: 'self', sessionId: mapping.conversationId },
65
+ { assistantId: DAEMON_INTERNAL_ASSISTANT_ID, sessionId: mapping.conversationId },
67
66
  (event) => {
68
67
  const controller = controllerRef;
69
68
  if (!controller) return;
@@ -3,9 +3,10 @@
3
3
  */
4
4
  import { deleteConversationKey } from '../../memory/conversation-key-store.js';
5
5
  import * as externalConversationStore from '../../memory/external-conversation-store.js';
6
+ import { DAEMON_INTERNAL_ASSISTANT_ID } from '../assistant-scope.js';
6
7
  import { httpError } from '../http-errors.js';
7
8
 
8
- export async function handleDeleteConversation(req: Request, assistantId: string = 'self'): Promise<Response> {
9
+ export async function handleDeleteConversation(req: Request, assistantId: string = DAEMON_INTERNAL_ASSISTANT_ID): Promise<Response> {
9
10
  const body = await req.json() as {
10
11
  sourceChannel?: string;
11
12
  externalChatId?: string;
@@ -26,14 +27,14 @@ export async function handleDeleteConversation(req: Request, assistantId: string
26
27
  const legacyKey = `${sourceChannel}:${externalChatId}`;
27
28
  const scopedKey = `asst:${assistantId}:${sourceChannel}:${externalChatId}`;
28
29
  deleteConversationKey(scopedKey);
29
- if (assistantId === 'self') {
30
+ if (assistantId === DAEMON_INTERNAL_ASSISTANT_ID) {
30
31
  deleteConversationKey(legacyKey);
31
32
  }
32
33
  // external_conversation_bindings is currently assistant-agnostic
33
34
  // (unique by sourceChannel + externalChatId). Restrict mutations to the
34
35
  // canonical self-assistant route so multi-assistant legacy routes do not
35
36
  // clobber each other's bindings.
36
- if (assistantId === 'self') {
37
+ if (assistantId === DAEMON_INTERNAL_ASSISTANT_ID) {
37
38
  externalConversationStore.deleteBindingByChannelChat(sourceChannel, externalChatId);
38
39
  }
39
40
 
@@ -28,6 +28,7 @@ import { IngressBlockedError } from '../../util/errors.js';
28
28
  import { getLogger } from '../../util/logger.js';
29
29
  import { readHttpToken } from '../../util/platform.js';
30
30
  import { notifyGuardianOfAccessRequest } from '../access-request-helper.js';
31
+ import { DAEMON_INTERNAL_ASSISTANT_ID } from '../assistant-scope.js';
31
32
  import {
32
33
  buildApprovalUIMetadata,
33
34
  getApprovalInfoByConversation,
@@ -97,7 +98,7 @@ export async function handleChannelInbound(
97
98
  req: Request,
98
99
  processMessage?: MessageProcessor,
99
100
  bearerToken?: string,
100
- assistantId: string = 'self',
101
+ assistantId: string = DAEMON_INTERNAL_ASSISTANT_ID,
101
102
  gatewayOriginSecret?: string,
102
103
  approvalCopyGenerator?: ApprovalCopyGenerator,
103
104
  approvalConversationGenerator?: ApprovalConversationGenerator,
@@ -580,7 +581,7 @@ export async function handleChannelInbound(
580
581
  // external_conversation_bindings is assistant-agnostic. Restrict writes to
581
582
  // self so assistant-scoped legacy routes do not overwrite each other's
582
583
  // channel binding metadata for the same chat.
583
- if (canonicalAssistantId === 'self') {
584
+ if (canonicalAssistantId === DAEMON_INTERNAL_ASSISTANT_ID) {
584
585
  externalConversationStore.upsertBinding({
585
586
  conversationId: result.conversationId,
586
587
  sourceChannel,
@@ -1447,7 +1448,7 @@ function startPendingApprovalPromptWatcher(params: {
1447
1448
  replyCallbackUrl,
1448
1449
  chatId: externalChatId,
1449
1450
  sourceChannel,
1450
- assistantId: assistantId ?? 'self',
1451
+ assistantId: assistantId ?? DAEMON_INTERNAL_ASSISTANT_ID,
1451
1452
  bearerToken,
1452
1453
  prompt,
1453
1454
  uiMetadata: buildApprovalUIMetadata(prompt, info),
@@ -147,6 +147,8 @@ export async function handleCreateInvite(req: Request): Promise<Response> {
147
147
  expiresInMs: body.expiresInMs as number | undefined,
148
148
  expectedExternalUserId: body.expectedExternalUserId as string | undefined,
149
149
  voiceCodeDigits: body.voiceCodeDigits as number | undefined,
150
+ friendName: body.friendName as string | undefined,
151
+ guardianName: body.guardianName as string | undefined,
150
152
  });
151
153
 
152
154
  if (!result.ok) {
@@ -1,5 +1,6 @@
1
1
  import { startCall } from '../../calls/call-domain.js';
2
2
  import { getConfig } from '../../config/loader.js';
3
+ import { DAEMON_INTERNAL_ASSISTANT_ID } from '../../runtime/assistant-scope.js';
3
4
  import { findActiveSession } from '../../runtime/channel-guardian-service.js';
4
5
  import { normalizePhoneNumber } from '../../util/phone.js';
5
6
  import type { ToolContext, ToolExecutionResult } from '../types.js';
@@ -16,7 +17,7 @@ export async function executeCallStart(
16
17
  ? normalizePhoneNumber(input.phone_number)
17
18
  : null;
18
19
  if (requestedPhone) {
19
- const assistantId = context.assistantId ?? 'self';
20
+ const assistantId = context.assistantId ?? DAEMON_INTERNAL_ASSISTANT_ID;
20
21
  const activeVoiceVerification = findActiveSession(assistantId, 'voice');
21
22
  const verificationDestination = activeVoiceVerification?.destinationAddress ?? activeVoiceVerification?.expectedPhoneE164;
22
23
  if (verificationDestination === requestedPhone) {
@@ -132,6 +132,18 @@ function findWasmPath(pkg: string, file: string): string {
132
132
  return execDirPath;
133
133
  }
134
134
 
135
+ // Use module resolution to find the package. This handles hoisted
136
+ // dependencies (e.g. global bun installs where web-tree-sitter is at the
137
+ // top-level node_modules rather than nested under @vellumai/assistant).
138
+ try {
139
+ const resolved = require.resolve(`${pkg}/package.json`);
140
+ const pkgDir = dirname(resolved);
141
+ const resolvedPath = join(pkgDir, file);
142
+ if (existsSync(resolvedPath)) return resolvedPath;
143
+ } catch (err) {
144
+ log.warn({ err, pkg, file }, 'require.resolve failed for WASM package, falling back to manual resolution');
145
+ }
146
+
135
147
  const sourcePath = join(dir, '..', '..', '..', 'node_modules', pkg, file);
136
148
 
137
149
  if (existsSync(sourcePath)) return sourcePath;
@@ -1,4 +1,5 @@
1
1
  import { consumeGrantForInvocation } from '../approvals/approval-primitive.js';
2
+ import { DAEMON_INTERNAL_ASSISTANT_ID } from '../runtime/assistant-scope.js';
2
3
  import { createOrReuseToolGrantRequest } from '../runtime/tool-grant-request-helper.js';
3
4
  import { computeToolApprovalDigest } from '../security/tool-approval-digest.js';
4
5
  import { getTaskRunRules } from '../tasks/ephemeral-permissions.js';
@@ -128,7 +129,7 @@ export class ToolApprovalHandler {
128
129
  toolName: name,
129
130
  inputDigest,
130
131
  consumingRequestId: context.requestId ?? `preexec-${context.sessionId}-${Date.now()}`,
131
- assistantId: context.assistantId ?? 'self',
132
+ assistantId: context.assistantId ?? DAEMON_INTERNAL_ASSISTANT_ID,
132
133
  executionChannel: context.executionChannel,
133
134
  conversationId: context.conversationId,
134
135
  callSessionId: context.callSessionId,
@@ -16,6 +16,11 @@ const log = getLogger('workspace-git');
16
16
  * Strips all GIT_* env vars (e.g. GIT_DIR, GIT_WORK_TREE) that CI runners
17
17
  * or parent processes may set, then adds GIT_CEILING_DIRECTORIES to prevent
18
18
  * walking up to a parent repo.
19
+ *
20
+ * On macOS, augments PATH with common binary directories so the real git
21
+ * binary is found even when the daemon is launched from a .app bundle with
22
+ * a minimal PATH. Without this, the macOS /usr/bin/git shim triggers an
23
+ * "Install Command Line Developer Tools" popup on every git invocation.
19
24
  */
20
25
  function cleanGitEnv(workspaceDir: string): Record<string, string> {
21
26
  const env: Record<string, string> = {};
@@ -25,6 +30,20 @@ function cleanGitEnv(workspaceDir: string): Record<string, string> {
25
30
  }
26
31
  }
27
32
  env.GIT_CEILING_DIRECTORIES = workspaceDir;
33
+
34
+ const home = process.env.HOME ?? '';
35
+ const extraDirs = [
36
+ '/opt/homebrew/bin',
37
+ '/usr/local/bin',
38
+ `${home}/.local/bin`,
39
+ ];
40
+ const currentPath = env.PATH ?? '';
41
+ const pathDirs = currentPath.split(':');
42
+ const missing = extraDirs.filter(d => !pathDirs.includes(d));
43
+ if (missing.length > 0) {
44
+ env.PATH = [...missing, currentPath].filter(Boolean).join(':');
45
+ }
46
+
28
47
  return env;
29
48
  }
30
49