@vellumai/assistant 0.4.3 → 0.4.4
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/.env.example +3 -0
- package/ARCHITECTURE.md +40 -3
- package/README.md +43 -35
- package/package.json +1 -1
- package/scripts/ipc/generate-swift.ts +1 -0
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +58 -120
- package/src/__tests__/actor-token-service.test.ts +1099 -0
- package/src/__tests__/agent-loop.test.ts +51 -0
- package/src/__tests__/approval-routes-http.test.ts +2 -0
- package/src/__tests__/assistant-events-sse-hardening.test.ts +7 -5
- package/src/__tests__/assistant-id-boundary-guard.test.ts +125 -0
- package/src/__tests__/call-controller.test.ts +49 -0
- package/src/__tests__/call-pointer-message-composer.test.ts +171 -0
- package/src/__tests__/call-pointer-messages.test.ts +93 -3
- package/src/__tests__/call-pointer-no-hardcoded-copy.guard.test.ts +42 -0
- package/src/__tests__/callback-handoff-copy.test.ts +186 -0
- package/src/__tests__/channel-approval-routes.test.ts +133 -12
- package/src/__tests__/channel-guardian.test.ts +0 -87
- package/src/__tests__/channel-readiness-service.test.ts +10 -16
- package/src/__tests__/checker.test.ts +33 -12
- package/src/__tests__/config-schema.test.ts +4 -0
- package/src/__tests__/confirmation-request-guardian-bridge.test.ts +410 -0
- package/src/__tests__/conversation-routes-guardian-reply.test.ts +256 -0
- package/src/__tests__/conversation-routes.test.ts +12 -3
- package/src/__tests__/credential-security-invariants.test.ts +1 -1
- package/src/__tests__/daemon-server-session-init.test.ts +4 -0
- package/src/__tests__/guardian-actions-endpoint.test.ts +19 -14
- package/src/__tests__/guardian-dispatch.test.ts +8 -0
- package/src/__tests__/guardian-outbound-http.test.ts +4 -4
- package/src/__tests__/guardian-question-mode.test.ts +200 -0
- package/src/__tests__/guardian-routing-invariants.test.ts +178 -0
- package/src/__tests__/guardian-routing-state.test.ts +525 -0
- package/src/__tests__/handle-user-message-secret-resume.test.ts +2 -0
- package/src/__tests__/handlers-telegram-config.test.ts +0 -83
- package/src/__tests__/handlers-user-message-approval-consumption.test.ts +55 -0
- package/src/__tests__/headless-browser-navigate.test.ts +2 -0
- package/src/__tests__/ipc-snapshot.test.ts +18 -51
- package/src/__tests__/non-member-access-request.test.ts +131 -8
- package/src/__tests__/notification-decision-fallback.test.ts +129 -4
- package/src/__tests__/notification-decision-strategy.test.ts +62 -2
- package/src/__tests__/notification-guardian-path.test.ts +3 -0
- package/src/__tests__/recording-intent-handler.test.ts +1 -0
- package/src/__tests__/relay-server.test.ts +841 -39
- package/src/__tests__/send-endpoint-busy.test.ts +5 -0
- package/src/__tests__/session-agent-loop.test.ts +1 -0
- package/src/__tests__/session-confirmation-signals.test.ts +523 -0
- package/src/__tests__/session-init.benchmark.test.ts +0 -1
- package/src/__tests__/session-surfaces-task-progress.test.ts +1 -1
- package/src/__tests__/session-tool-setup-app-refresh.test.ts +81 -2
- package/src/__tests__/session-tool-setup-memory-scope.test.ts +1 -1
- package/src/__tests__/session-tool-setup-side-effect-flag.test.ts +1 -1
- package/src/__tests__/tool-executor.test.ts +21 -2
- package/src/__tests__/tool-grant-request-escalation.test.ts +333 -27
- package/src/__tests__/trusted-contact-approval-notifier.test.ts +678 -0
- package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +1064 -0
- package/src/__tests__/twilio-config.test.ts +2 -13
- package/src/agent/loop.ts +1 -1
- package/src/approvals/guardian-decision-primitive.ts +10 -2
- package/src/approvals/guardian-request-resolvers.ts +128 -9
- package/src/calls/call-constants.ts +21 -0
- package/src/calls/call-controller.ts +9 -2
- package/src/calls/call-domain.ts +28 -7
- package/src/calls/call-pointer-message-composer.ts +154 -0
- package/src/calls/call-pointer-messages.ts +106 -27
- package/src/calls/guardian-dispatch.ts +4 -2
- package/src/calls/relay-server.ts +424 -12
- package/src/calls/twilio-config.ts +4 -11
- package/src/calls/twilio-routes.ts +1 -1
- package/src/calls/types.ts +3 -1
- package/src/cli.ts +5 -4
- package/src/config/bundled-skills/agentmail/SKILL.md +4 -0
- package/src/config/bundled-skills/app-builder/SKILL.md +146 -10
- package/src/config/bundled-skills/app-builder/TOOLS.json +1 -1
- package/src/config/bundled-skills/email-setup/SKILL.md +1 -1
- package/src/config/bundled-skills/google-oauth-setup/SKILL.md +105 -81
- package/src/config/bundled-skills/messaging/SKILL.md +61 -12
- package/src/config/bundled-skills/messaging/TOOLS.json +58 -0
- package/src/config/bundled-skills/messaging/tools/gmail-sender-digest.ts +6 -1
- package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +35 -0
- package/src/config/bundled-skills/messaging/tools/messaging-sender-digest.ts +52 -0
- package/src/config/bundled-skills/phone-calls/SKILL.md +30 -39
- package/src/config/bundled-skills/twitter/SKILL.md +3 -3
- package/src/config/bundled-skills/vercel-token-setup/SKILL.md +1 -0
- package/src/config/calls-schema.ts +24 -0
- package/src/config/env.ts +22 -0
- package/src/config/feature-flag-registry.json +8 -0
- package/src/config/schema.ts +2 -2
- package/src/config/skills.ts +11 -0
- package/src/config/system-prompt.ts +11 -1
- package/src/config/templates/SOUL.md +2 -0
- package/src/config/vellum-skills/sms-setup/SKILL.md +71 -82
- package/src/config/vellum-skills/trusted-contacts/SKILL.md +10 -9
- package/src/config/vellum-skills/twilio-setup/SKILL.md +88 -73
- package/src/daemon/call-pointer-generators.ts +59 -0
- package/src/daemon/computer-use-session.ts +2 -5
- package/src/daemon/handlers/apps.ts +76 -20
- package/src/daemon/handlers/config-channels.ts +5 -55
- package/src/daemon/handlers/config-inbox.ts +9 -3
- package/src/daemon/handlers/config-ingress.ts +28 -3
- package/src/daemon/handlers/config-telegram.ts +12 -0
- package/src/daemon/handlers/config.ts +2 -6
- package/src/daemon/handlers/pairing.ts +2 -0
- package/src/daemon/handlers/sessions.ts +48 -3
- package/src/daemon/handlers/shared.ts +17 -2
- package/src/daemon/ipc-contract/integrations.ts +1 -99
- package/src/daemon/ipc-contract/messages.ts +47 -1
- package/src/daemon/ipc-contract/notifications.ts +11 -0
- package/src/daemon/ipc-contract-inventory.json +2 -4
- package/src/daemon/lifecycle.ts +17 -0
- package/src/daemon/server.ts +14 -1
- package/src/daemon/session-agent-loop-handlers.ts +20 -0
- package/src/daemon/session-agent-loop.ts +22 -11
- package/src/daemon/session-lifecycle.ts +1 -1
- package/src/daemon/session-process.ts +11 -1
- package/src/daemon/session-runtime-assembly.ts +3 -0
- package/src/daemon/session-surfaces.ts +3 -2
- package/src/daemon/session.ts +88 -1
- package/src/daemon/tool-side-effects.ts +22 -0
- package/src/home-base/prebuilt/brain-graph.html +1483 -0
- package/src/home-base/prebuilt/index.html +40 -0
- package/src/inbound/platform-callback-registration.ts +157 -0
- package/src/memory/canonical-guardian-store.ts +1 -1
- package/src/memory/db-init.ts +4 -0
- package/src/memory/migrations/038-actor-token-records.ts +39 -0
- package/src/memory/migrations/index.ts +1 -0
- package/src/memory/schema.ts +16 -0
- package/src/messaging/provider-types.ts +24 -0
- package/src/messaging/provider.ts +7 -0
- package/src/messaging/providers/gmail/adapter.ts +127 -0
- package/src/messaging/providers/sms/adapter.ts +40 -37
- package/src/notifications/adapters/macos.ts +45 -2
- package/src/notifications/broadcaster.ts +16 -0
- package/src/notifications/copy-composer.ts +39 -1
- package/src/notifications/decision-engine.ts +22 -9
- package/src/notifications/destination-resolver.ts +16 -2
- package/src/notifications/emit-signal.ts +16 -8
- package/src/notifications/guardian-question-mode.ts +419 -0
- package/src/notifications/signal.ts +14 -3
- package/src/permissions/checker.ts +13 -1
- package/src/permissions/prompter.ts +14 -0
- package/src/providers/anthropic/client.ts +20 -0
- package/src/providers/provider-send-message.ts +15 -3
- package/src/runtime/access-request-helper.ts +71 -1
- package/src/runtime/actor-token-service.ts +234 -0
- package/src/runtime/actor-token-store.ts +236 -0
- package/src/runtime/channel-approvals.ts +5 -3
- package/src/runtime/channel-readiness-service.ts +23 -64
- package/src/runtime/channel-readiness-types.ts +3 -4
- package/src/runtime/channel-retry-sweep.ts +4 -1
- package/src/runtime/confirmation-request-guardian-bridge.ts +197 -0
- package/src/runtime/guardian-action-followup-executor.ts +1 -1
- package/src/runtime/guardian-context-resolver.ts +82 -0
- package/src/runtime/guardian-outbound-actions.ts +0 -3
- package/src/runtime/guardian-reply-router.ts +67 -30
- package/src/runtime/guardian-vellum-migration.ts +57 -0
- package/src/runtime/http-server.ts +65 -12
- package/src/runtime/http-types.ts +13 -0
- package/src/runtime/invite-redemption-service.ts +8 -0
- package/src/runtime/local-actor-identity.ts +76 -0
- package/src/runtime/middleware/actor-token.ts +271 -0
- package/src/runtime/routes/approval-routes.ts +82 -7
- package/src/runtime/routes/brain-graph-routes.ts +222 -0
- package/src/runtime/routes/channel-readiness-routes.ts +71 -0
- package/src/runtime/routes/conversation-routes.ts +140 -52
- package/src/runtime/routes/events-routes.ts +20 -5
- package/src/runtime/routes/guardian-action-routes.ts +45 -3
- package/src/runtime/routes/guardian-approval-interception.ts +29 -0
- package/src/runtime/routes/guardian-bootstrap-routes.ts +145 -0
- package/src/runtime/routes/inbound-message-handler.ts +143 -2
- package/src/runtime/routes/integration-routes.ts +7 -15
- package/src/runtime/routes/pairing-routes.ts +163 -0
- package/src/runtime/routes/twilio-routes.ts +934 -0
- package/src/runtime/tool-grant-request-helper.ts +3 -1
- package/src/security/oauth2.ts +27 -2
- package/src/security/token-manager.ts +46 -10
- package/src/tools/browser/browser-execution.ts +4 -3
- package/src/tools/browser/browser-handoff.ts +10 -18
- package/src/tools/browser/browser-manager.ts +80 -25
- package/src/tools/browser/browser-screencast.ts +35 -119
- package/src/tools/permission-checker.ts +15 -4
- package/src/tools/tool-approval-handler.ts +242 -18
- package/src/__tests__/handlers-twilio-config.test.ts +0 -1928
- package/src/daemon/handlers/config-twilio.ts +0 -1082
|
@@ -86,25 +86,14 @@ describe('twilio-config', () => {
|
|
|
86
86
|
expect(config.phoneNumber).toBe('+15558888888');
|
|
87
87
|
});
|
|
88
88
|
|
|
89
|
-
test('
|
|
89
|
+
test('returns global phone number when assistantPhoneNumbers mapping exists', () => {
|
|
90
90
|
mockLoadConfigResult = {
|
|
91
91
|
sms: {
|
|
92
92
|
phoneNumber: '+15551234567',
|
|
93
93
|
assistantPhoneNumbers: { 'ast-1': '+15557777777' },
|
|
94
94
|
},
|
|
95
95
|
};
|
|
96
|
-
const config = getTwilioConfig(
|
|
97
|
-
expect(config.phoneNumber).toBe('+15557777777');
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
test('falls back to global phone number when assistant has no dedicated number', () => {
|
|
101
|
-
mockLoadConfigResult = {
|
|
102
|
-
sms: {
|
|
103
|
-
phoneNumber: '+15551234567',
|
|
104
|
-
assistantPhoneNumbers: { 'ast-1': '+15557777777' },
|
|
105
|
-
},
|
|
106
|
-
};
|
|
107
|
-
const config = getTwilioConfig('ast-unknown');
|
|
96
|
+
const config = getTwilioConfig();
|
|
108
97
|
expect(config.phoneNumber).toBe('+15551234567');
|
|
109
98
|
});
|
|
110
99
|
});
|
package/src/agent/loop.ts
CHANGED
|
@@ -53,6 +53,7 @@ import {
|
|
|
53
53
|
type ActorContext,
|
|
54
54
|
type ChannelDeliveryContext,
|
|
55
55
|
getResolver,
|
|
56
|
+
type ResolverEmissionContext,
|
|
56
57
|
} from './guardian-request-resolvers.js';
|
|
57
58
|
|
|
58
59
|
const log = getLogger('guardian-decision-primitive');
|
|
@@ -295,10 +296,12 @@ export interface ApplyCanonicalGuardianDecisionParams {
|
|
|
295
296
|
userText?: string;
|
|
296
297
|
/** Optional channel delivery context — present when the decision arrived via a channel message. */
|
|
297
298
|
channelDeliveryContext?: ChannelDeliveryContext;
|
|
299
|
+
/** Optional emission context threaded to handleConfirmationResponse for correct source attribution. */
|
|
300
|
+
emissionContext?: ResolverEmissionContext;
|
|
298
301
|
}
|
|
299
302
|
|
|
300
303
|
export type CanonicalDecisionResult =
|
|
301
|
-
| { applied: true; requestId: string; grantMinted: boolean; resolverFailed?: boolean; resolverFailureReason?: string }
|
|
304
|
+
| { applied: true; requestId: string; grantMinted: boolean; resolverFailed?: boolean; resolverFailureReason?: string; resolverReplyText?: string }
|
|
302
305
|
| { applied: false; reason: 'not_found' | 'already_resolved' | 'identity_mismatch' | 'invalid_action' | 'expired'; detail?: string };
|
|
303
306
|
|
|
304
307
|
/**
|
|
@@ -319,7 +322,7 @@ export type CanonicalDecisionResult =
|
|
|
319
322
|
export async function applyCanonicalGuardianDecision(
|
|
320
323
|
params: ApplyCanonicalGuardianDecisionParams,
|
|
321
324
|
): Promise<CanonicalDecisionResult> {
|
|
322
|
-
const { requestId, action, actorContext, userText, channelDeliveryContext } = params;
|
|
325
|
+
const { requestId, action, actorContext, userText, channelDeliveryContext, emissionContext } = params;
|
|
323
326
|
|
|
324
327
|
// 1. Look up the canonical request
|
|
325
328
|
const request = getCanonicalGuardianRequest(requestId);
|
|
@@ -427,6 +430,7 @@ export async function applyCanonicalGuardianDecision(
|
|
|
427
430
|
// 5. Dispatch to kind-specific resolver
|
|
428
431
|
let resolverFailed = false;
|
|
429
432
|
let resolverFailureReason: string | undefined;
|
|
433
|
+
let resolverReplyText: string | undefined;
|
|
430
434
|
const resolver = getResolver(request.kind);
|
|
431
435
|
if (resolver) {
|
|
432
436
|
const resolverResult = await resolver.resolve({
|
|
@@ -434,6 +438,7 @@ export async function applyCanonicalGuardianDecision(
|
|
|
434
438
|
decision: { action: effectiveAction, userText },
|
|
435
439
|
actor: actorContext,
|
|
436
440
|
channelDeliveryContext,
|
|
441
|
+
emissionContext,
|
|
437
442
|
});
|
|
438
443
|
|
|
439
444
|
if (!resolverResult.ok) {
|
|
@@ -452,6 +457,8 @@ export async function applyCanonicalGuardianDecision(
|
|
|
452
457
|
// still being informed that the resolver had an issue.
|
|
453
458
|
resolverFailed = true;
|
|
454
459
|
resolverFailureReason = resolverResult.reason;
|
|
460
|
+
} else {
|
|
461
|
+
resolverReplyText = resolverResult.guardianReplyText;
|
|
455
462
|
}
|
|
456
463
|
} else {
|
|
457
464
|
log.info(
|
|
@@ -494,5 +501,6 @@ export async function applyCanonicalGuardianDecision(
|
|
|
494
501
|
requestId,
|
|
495
502
|
grantMinted,
|
|
496
503
|
...(resolverFailed ? { resolverFailed, resolverFailureReason } : {}),
|
|
504
|
+
...(resolverReplyText ? { resolverReplyText } : {}),
|
|
497
505
|
};
|
|
498
506
|
}
|
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import { answerCall } from '../calls/call-domain.js';
|
|
15
|
-
import
|
|
15
|
+
import { getGatewayInternalBaseUrl } from '../config/env.js';
|
|
16
|
+
import { type CanonicalGuardianRequest,getCanonicalGuardianRequest } from '../memory/canonical-guardian-store.js';
|
|
16
17
|
import { upsertMember } from '../memory/ingress-member-store.js';
|
|
17
18
|
import { emitNotificationSignal } from '../notifications/emit-signal.js';
|
|
18
19
|
import { addRule } from '../permissions/trust-store.js';
|
|
@@ -22,7 +23,9 @@ import { createOutboundSession } from '../runtime/channel-guardian-service.js';
|
|
|
22
23
|
import { deliverChannelReply } from '../runtime/gateway-client.js';
|
|
23
24
|
import * as pendingInteractions from '../runtime/pending-interactions.js';
|
|
24
25
|
import { getTool } from '../tools/registry.js';
|
|
26
|
+
import { TC_GRANT_WAIT_MAX_MS } from '../tools/tool-approval-handler.js';
|
|
25
27
|
import { getLogger } from '../util/logger.js';
|
|
28
|
+
import { readHttpToken } from '../util/platform.js';
|
|
26
29
|
|
|
27
30
|
const log = getLogger('guardian-request-resolvers');
|
|
28
31
|
|
|
@@ -64,6 +67,13 @@ export interface ChannelDeliveryContext {
|
|
|
64
67
|
bearerToken?: string;
|
|
65
68
|
}
|
|
66
69
|
|
|
70
|
+
/** Emission context threaded from callers to handleConfirmationResponse. */
|
|
71
|
+
export interface ResolverEmissionContext {
|
|
72
|
+
source?: 'button' | 'inline_nl' | 'auto_deny' | 'timeout' | 'system';
|
|
73
|
+
causedByRequestId?: string;
|
|
74
|
+
decisionText?: string;
|
|
75
|
+
}
|
|
76
|
+
|
|
67
77
|
/** Context passed to each resolver after CAS resolution succeeds. */
|
|
68
78
|
export interface ResolverContext {
|
|
69
79
|
/** The canonical request record (already resolved to its terminal status). */
|
|
@@ -74,13 +84,27 @@ export interface ResolverContext {
|
|
|
74
84
|
actor: ActorContext;
|
|
75
85
|
/** Optional channel delivery context — present when the decision arrived via a channel message. */
|
|
76
86
|
channelDeliveryContext?: ChannelDeliveryContext;
|
|
87
|
+
/** Optional emission context threaded to handleConfirmationResponse for correct source attribution. */
|
|
88
|
+
emissionContext?: ResolverEmissionContext;
|
|
77
89
|
}
|
|
78
90
|
|
|
79
91
|
/** Discriminated result from a resolver. */
|
|
80
92
|
export type ResolverResult =
|
|
81
|
-
| { ok: true; applied: true; grantMinted?: boolean }
|
|
93
|
+
| { ok: true; applied: true; grantMinted?: boolean; guardianReplyText?: string }
|
|
82
94
|
| { ok: false; reason: string };
|
|
83
95
|
|
|
96
|
+
function resolveDeliverCallbackUrlForChannel(channel: string): string | null {
|
|
97
|
+
switch (channel) {
|
|
98
|
+
case 'telegram':
|
|
99
|
+
case 'sms':
|
|
100
|
+
case 'whatsapp':
|
|
101
|
+
case 'slack':
|
|
102
|
+
return `${getGatewayInternalBaseUrl()}/deliver/${channel}`;
|
|
103
|
+
default:
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
84
108
|
/** Interface that kind-specific resolvers implement. */
|
|
85
109
|
export interface GuardianRequestResolver {
|
|
86
110
|
/** The request kind this resolver handles (matches canonical_guardian_requests.kind). */
|
|
@@ -139,11 +163,13 @@ const pendingInteractionResolver: GuardianRequestResolver = {
|
|
|
139
163
|
decision.action === 'approve_always' &&
|
|
140
164
|
details &&
|
|
141
165
|
details.persistentDecisionsAllowed !== false &&
|
|
142
|
-
details.allowlistOptions?.length
|
|
143
|
-
details.scopeOptions?.length
|
|
166
|
+
details.allowlistOptions?.length
|
|
144
167
|
) {
|
|
145
168
|
const pattern = details.allowlistOptions[0].pattern;
|
|
146
|
-
|
|
169
|
+
// Non-scoped tools (web_fetch, network_request, etc.) have empty
|
|
170
|
+
// scopeOptions — default to 'everywhere' so approve_always still
|
|
171
|
+
// persists a trust rule instead of silently degrading to one-time.
|
|
172
|
+
const scope = details.scopeOptions?.length ? details.scopeOptions[0].scope : 'everywhere';
|
|
147
173
|
const tool = getTool(details.toolName);
|
|
148
174
|
const executionTarget = tool?.origin === 'skill' ? details.executionTarget : undefined;
|
|
149
175
|
addRule(details.toolName, pattern, scope, 'allow', 100, { executionTarget });
|
|
@@ -166,7 +192,7 @@ const pendingInteractionResolver: GuardianRequestResolver = {
|
|
|
166
192
|
|
|
167
193
|
// Map action to the permission system's UserDecision type and notify session.
|
|
168
194
|
const userDecision = decision.action === 'reject' ? 'deny' as const : 'allow' as const;
|
|
169
|
-
resolved.session.handleConfirmationResponse(request.id, userDecision);
|
|
195
|
+
resolved.session.handleConfirmationResponse(request.id, userDecision, undefined, undefined, undefined, ctx.emissionContext);
|
|
170
196
|
|
|
171
197
|
log.info(
|
|
172
198
|
{
|
|
@@ -283,8 +309,11 @@ const accessRequestResolver: GuardianRequestResolver = {
|
|
|
283
309
|
const channel = request.sourceChannel ?? 'unknown';
|
|
284
310
|
const requesterExternalUserId = request.requesterExternalUserId ?? '';
|
|
285
311
|
const requesterChatId = request.requesterChatId ?? request.requesterExternalUserId ?? '';
|
|
312
|
+
const requesterLabel = requesterExternalUserId || requesterChatId || 'the requester';
|
|
286
313
|
const decidedByExternalUserId = ctx.actor.externalUserId ?? '';
|
|
287
314
|
const assistantId = DAEMON_INTERNAL_ASSISTANT_ID;
|
|
315
|
+
const desktopDeliverUrl = resolveDeliverCallbackUrlForChannel(channel);
|
|
316
|
+
const desktopBearerToken = readHttpToken() ?? undefined;
|
|
288
317
|
|
|
289
318
|
if (decision.action === 'reject') {
|
|
290
319
|
log.info(
|
|
@@ -341,9 +370,23 @@ const accessRequestResolver: GuardianRequestResolver = {
|
|
|
341
370
|
contextPayload: deniedPayload,
|
|
342
371
|
dedupeKey: `trusted-contact:denied:${request.id}`,
|
|
343
372
|
});
|
|
373
|
+
} else if (desktopDeliverUrl && requesterChatId) {
|
|
374
|
+
try {
|
|
375
|
+
await deliverChannelReply(desktopDeliverUrl, {
|
|
376
|
+
chatId: requesterChatId,
|
|
377
|
+
text: 'Your access request has been denied by the guardian.',
|
|
378
|
+
assistantId,
|
|
379
|
+
}, desktopBearerToken);
|
|
380
|
+
} catch (err) {
|
|
381
|
+
log.error({ err, requesterChatId }, 'Failed to notify requester of access request denial (desktop decision path)');
|
|
382
|
+
}
|
|
344
383
|
}
|
|
345
384
|
|
|
346
|
-
return {
|
|
385
|
+
return {
|
|
386
|
+
ok: true,
|
|
387
|
+
applied: true,
|
|
388
|
+
...(ctx.actor.isTrusted ? { guardianReplyText: `Access denied for ${requesterLabel}.` } : {}),
|
|
389
|
+
};
|
|
347
390
|
}
|
|
348
391
|
|
|
349
392
|
// Voice approvals: directly activate the trusted contact without minting
|
|
@@ -404,6 +447,7 @@ const accessRequestResolver: GuardianRequestResolver = {
|
|
|
404
447
|
|
|
405
448
|
// Deliver the verification code to the guardian and notify the requester
|
|
406
449
|
// when channel delivery context is available (channel message path).
|
|
450
|
+
let requesterNotified = false;
|
|
407
451
|
if (channelDeliveryContext) {
|
|
408
452
|
let codeDelivered = true;
|
|
409
453
|
|
|
@@ -434,6 +478,7 @@ const accessRequestResolver: GuardianRequestResolver = {
|
|
|
434
478
|
+ 'Please enter the 6-digit verification code you receive from the guardian.',
|
|
435
479
|
assistantId,
|
|
436
480
|
}, channelDeliveryContext.bearerToken);
|
|
481
|
+
requesterNotified = true;
|
|
437
482
|
} catch (err) {
|
|
438
483
|
log.error({ err, requesterChatId }, 'Failed to notify requester of access request approval');
|
|
439
484
|
}
|
|
@@ -473,9 +518,29 @@ const accessRequestResolver: GuardianRequestResolver = {
|
|
|
473
518
|
dedupeKey: `trusted-contact:verification-sent:${session.sessionId}`,
|
|
474
519
|
});
|
|
475
520
|
}
|
|
521
|
+
} else if (desktopDeliverUrl && requesterChatId) {
|
|
522
|
+
try {
|
|
523
|
+
await deliverChannelReply(desktopDeliverUrl, {
|
|
524
|
+
chatId: requesterChatId,
|
|
525
|
+
text: 'Your access request has been approved! '
|
|
526
|
+
+ 'Please enter the 6-digit verification code you receive from the guardian.',
|
|
527
|
+
assistantId,
|
|
528
|
+
}, desktopBearerToken);
|
|
529
|
+
requesterNotified = true;
|
|
530
|
+
} catch (err) {
|
|
531
|
+
log.error({ err, requesterChatId }, 'Failed to notify requester of access request approval (desktop decision path)');
|
|
532
|
+
}
|
|
476
533
|
}
|
|
477
534
|
|
|
478
|
-
|
|
535
|
+
const verificationReplyText = requesterNotified
|
|
536
|
+
? `Access approved for ${requesterLabel}. Give them this verification code: ${session.secret}. The code expires in 10 minutes.`
|
|
537
|
+
: `Access approved for ${requesterLabel}. Give them this verification code: ${session.secret}. The code expires in 10 minutes. I could not notify them automatically, so please tell them to send the code manually.`;
|
|
538
|
+
|
|
539
|
+
return {
|
|
540
|
+
ok: true,
|
|
541
|
+
applied: true,
|
|
542
|
+
...(ctx.actor.isTrusted ? { guardianReplyText: verificationReplyText } : {}),
|
|
543
|
+
};
|
|
479
544
|
},
|
|
480
545
|
};
|
|
481
546
|
|
|
@@ -534,7 +599,61 @@ const toolGrantRequestResolver: GuardianRequestResolver = {
|
|
|
534
599
|
'Tool grant request resolver: approved (grant minting deferred to canonical primitive)',
|
|
535
600
|
);
|
|
536
601
|
|
|
537
|
-
|
|
602
|
+
// Re-read the canonical request to check whether an inline grant waiter
|
|
603
|
+
// has already claimed this request. When followupState is
|
|
604
|
+
// 'inline_wait_active', the requester's original tool call is blocking
|
|
605
|
+
// on the grant and will resume automatically — sending a "please retry"
|
|
606
|
+
// notification would be stale and confusing (and could cause duplicate
|
|
607
|
+
// attempts or one-time-grant denials).
|
|
608
|
+
//
|
|
609
|
+
// Staleness guard: the inline_wait_active marker is persisted in DB and
|
|
610
|
+
// can outlive the actual waiter if the daemon crashes or restarts during
|
|
611
|
+
// the wait. To avoid permanently suppressing the retry notification, we
|
|
612
|
+
// treat the marker as stale if the encoded start timestamp is older than
|
|
613
|
+
// the maximum wait budget plus a 30s buffer.
|
|
614
|
+
const INLINE_WAIT_STALENESS_BUFFER_MS = 30_000;
|
|
615
|
+
const freshRequest = getCanonicalGuardianRequest(request.id);
|
|
616
|
+
const followupState = freshRequest?.followupState ?? '';
|
|
617
|
+
let inlineWaitActive = followupState.startsWith('inline_wait_active');
|
|
618
|
+
if (inlineWaitActive && freshRequest) {
|
|
619
|
+
// The followupState encodes the wall-clock epoch when the inline wait
|
|
620
|
+
// started (e.g. 'inline_wait_active:1700000000000'). We use this
|
|
621
|
+
// instead of updatedAt because resolveCanonicalGuardianRequest sets
|
|
622
|
+
// updatedAt = now during CAS resolution, making updatedAt always fresh
|
|
623
|
+
// by the time this resolver runs.
|
|
624
|
+
const colonIdx = followupState.indexOf(':');
|
|
625
|
+
const waitStartMs = colonIdx !== -1 ? Number(followupState.slice(colonIdx + 1)) : NaN;
|
|
626
|
+
const markerAgeMs = Number.isFinite(waitStartMs)
|
|
627
|
+
? Date.now() - waitStartMs
|
|
628
|
+
: Infinity; // Treat unparseable timestamps as stale for safety.
|
|
629
|
+
const stalenessThresholdMs = TC_GRANT_WAIT_MAX_MS + INLINE_WAIT_STALENESS_BUFFER_MS;
|
|
630
|
+
if (markerAgeMs > stalenessThresholdMs) {
|
|
631
|
+
log.warn(
|
|
632
|
+
{
|
|
633
|
+
event: 'resolver_tool_grant_request_stale_inline_wait',
|
|
634
|
+
requestId: request.id,
|
|
635
|
+
toolName: request.toolName,
|
|
636
|
+
markerAgeMs,
|
|
637
|
+
stalenessThresholdMs,
|
|
638
|
+
waitStartMs,
|
|
639
|
+
},
|
|
640
|
+
'inline_wait_active marker is stale (daemon likely crashed during wait) — sending retry notification',
|
|
641
|
+
);
|
|
642
|
+
inlineWaitActive = false;
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
if (inlineWaitActive) {
|
|
647
|
+
log.info(
|
|
648
|
+
{
|
|
649
|
+
event: 'resolver_tool_grant_request_skip_retry_notification',
|
|
650
|
+
requestId: request.id,
|
|
651
|
+
toolName: request.toolName,
|
|
652
|
+
followupState: freshRequest?.followupState,
|
|
653
|
+
},
|
|
654
|
+
'Skipping requester retry notification — inline grant wait is active and will resume the original invocation',
|
|
655
|
+
);
|
|
656
|
+
} else if (channelDeliveryContext && requesterChatId) {
|
|
538
657
|
try {
|
|
539
658
|
await deliverChannelReply(channelDeliveryContext.replyCallbackUrl, {
|
|
540
659
|
chatId: requesterChatId,
|
|
@@ -49,6 +49,27 @@ export function getAccessRequestPollIntervalMs(): number {
|
|
|
49
49
|
return getConfig().calls.accessRequestPollIntervalMs;
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
export function getGuardianWaitUpdateInitialIntervalMs(): number {
|
|
53
|
+
return getConfig().calls.guardianWaitUpdateInitialIntervalMs;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function getGuardianWaitUpdateInitialWindowMs(): number {
|
|
57
|
+
return getConfig().calls.guardianWaitUpdateInitialWindowMs;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function getGuardianWaitUpdateSteadyMinIntervalMs(): number {
|
|
61
|
+
return getConfig().calls.guardianWaitUpdateSteadyMinIntervalMs;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function getGuardianWaitUpdateSteadyMaxIntervalMs(): number {
|
|
65
|
+
return getConfig().calls.guardianWaitUpdateSteadyMaxIntervalMs;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function getSilenceTimeoutMs(): number {
|
|
69
|
+
return 30 * 1000; // 30 seconds
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** @deprecated Use getSilenceTimeoutMs() for mockability in tests. */
|
|
52
73
|
export const SILENCE_TIMEOUT_MS = 30 * 1000; // 30 seconds
|
|
53
74
|
|
|
54
75
|
// Legacy named exports for backward compatibility (use functions above for config-backed values)
|
|
@@ -22,7 +22,7 @@ import { DAEMON_INTERNAL_ASSISTANT_ID } from '../runtime/assistant-scope.js';
|
|
|
22
22
|
import { computeToolApprovalDigest } from '../security/tool-approval-digest.js';
|
|
23
23
|
import { getLogger } from '../util/logger.js';
|
|
24
24
|
import { readHttpToken } from '../util/platform.js';
|
|
25
|
-
import { getMaxCallDurationMs,
|
|
25
|
+
import { getMaxCallDurationMs, getSilenceTimeoutMs, getUserConsultationTimeoutMs } from './call-constants.js';
|
|
26
26
|
import { persistCallCompletionMessage } from './call-conversation-messages.js';
|
|
27
27
|
import { addPointerMessage, formatDuration } from './call-pointer-messages.js';
|
|
28
28
|
import { fireCallCompletionNotifier, fireCallQuestionNotifier, fireCallTranscriptNotifier,registerCallController, unregisterCallController } from './call-state.js';
|
|
@@ -1049,8 +1049,15 @@ export class CallController {
|
|
|
1049
1049
|
private resetSilenceTimer(): void {
|
|
1050
1050
|
if (this.silenceTimer) clearTimeout(this.silenceTimer);
|
|
1051
1051
|
this.silenceTimer = setTimeout(() => {
|
|
1052
|
+
// During guardian wait states, the relay heartbeat timer handles
|
|
1053
|
+
// periodic updates — suppress the generic "Are you still there?"
|
|
1054
|
+
// which is confusing when the caller is waiting on a decision.
|
|
1055
|
+
if (this.relay.getConnectionState() === 'awaiting_guardian_decision') {
|
|
1056
|
+
log.debug({ callSessionId: this.callSessionId }, 'Silence timeout suppressed during guardian wait');
|
|
1057
|
+
return;
|
|
1058
|
+
}
|
|
1052
1059
|
log.info({ callSessionId: this.callSessionId }, 'Silence timeout triggered');
|
|
1053
1060
|
this.relay.sendTextToken('Are you still there?', true);
|
|
1054
|
-
},
|
|
1061
|
+
}, getSilenceTimeoutMs());
|
|
1055
1062
|
}
|
|
1056
1063
|
}
|
package/src/calls/call-domain.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { getTwilioUserPhoneNumber } from '../config/env.js';
|
|
|
9
9
|
import { loadConfig } from '../config/loader.js';
|
|
10
10
|
import { VALID_CALLER_IDENTITY_MODES } from '../config/schema.js';
|
|
11
11
|
import type { AssistantConfig } from '../config/types.js';
|
|
12
|
+
import { resolveCallbackUrl } from '../inbound/platform-callback-registration.js';
|
|
12
13
|
import { getTwilioStatusCallbackUrl,getTwilioVoiceWebhookUrl } from '../inbound/public-ingress-urls.js';
|
|
13
14
|
import { getOrCreateConversation } from '../memory/conversation-key-store.js';
|
|
14
15
|
import { queueGenerateConversationTitle } from '../memory/conversation-title-service.js';
|
|
@@ -101,8 +102,7 @@ export type CallerIdentityResult =
|
|
|
101
102
|
* - Otherwise, always use `assistant_number` (implicit default).
|
|
102
103
|
*
|
|
103
104
|
* For `assistant_number`: uses the Twilio phone number from
|
|
104
|
-
* `getTwilioConfig(
|
|
105
|
-
* No eligibility check is performed — this is a fast path.
|
|
105
|
+
* `getTwilioConfig()`. No eligibility check is performed — this is a fast path.
|
|
106
106
|
* For `user_number`: uses `config.calls.callerIdentity.userNumber` or the
|
|
107
107
|
* secure key `credential:twilio:user_phone_number`, then validates that the
|
|
108
108
|
* number is usable as an outbound caller ID via the Twilio API.
|
|
@@ -134,7 +134,7 @@ export async function resolveCallerIdentity(
|
|
|
134
134
|
|
|
135
135
|
if (mode === 'assistant_number') {
|
|
136
136
|
const twilioConfig = getTwilioConfig(assistantId);
|
|
137
|
-
log.info({ mode, source, fromNumber: twilioConfig.phoneNumber
|
|
137
|
+
log.info({ mode, source, fromNumber: twilioConfig.phoneNumber }, 'Resolved caller identity');
|
|
138
138
|
return { ok: true, mode, fromNumber: twilioConfig.phoneNumber, source };
|
|
139
139
|
}
|
|
140
140
|
|
|
@@ -358,11 +358,23 @@ export async function startCall(input: StartCallInput): Promise<StartCallResult
|
|
|
358
358
|
|
|
359
359
|
log.info({ callSessionId: session.id, voiceConversationId, initiatedFrom: conversationId, to: phoneNumber, from: fromNumber, task }, 'Initiating outbound call');
|
|
360
360
|
|
|
361
|
+
const webhookUrl = await resolveCallbackUrl(
|
|
362
|
+
() => getTwilioVoiceWebhookUrl(ingressConfig, session.id),
|
|
363
|
+
'webhooks/twilio/voice',
|
|
364
|
+
'twilio_voice',
|
|
365
|
+
{ callSessionId: session.id },
|
|
366
|
+
);
|
|
367
|
+
const statusCallbackUrl = await resolveCallbackUrl(
|
|
368
|
+
() => getTwilioStatusCallbackUrl(ingressConfig),
|
|
369
|
+
'webhooks/twilio/status',
|
|
370
|
+
'twilio_status',
|
|
371
|
+
);
|
|
372
|
+
|
|
361
373
|
const { callSid } = await provider.initiateCall({
|
|
362
374
|
from: fromNumber,
|
|
363
375
|
to: phoneNumber,
|
|
364
|
-
webhookUrl
|
|
365
|
-
statusCallbackUrl
|
|
376
|
+
webhookUrl,
|
|
377
|
+
statusCallbackUrl,
|
|
366
378
|
});
|
|
367
379
|
|
|
368
380
|
updateCallSession(session.id, { providerCallSid: callSid });
|
|
@@ -687,8 +699,17 @@ export async function startGuardianVerificationCall(
|
|
|
687
699
|
});
|
|
688
700
|
sessionId = session.id;
|
|
689
701
|
|
|
690
|
-
const webhookUrl =
|
|
691
|
-
|
|
702
|
+
const webhookUrl = await resolveCallbackUrl(
|
|
703
|
+
() => getTwilioVoiceWebhookUrl(config, session.id),
|
|
704
|
+
'webhooks/twilio/voice',
|
|
705
|
+
'twilio_voice',
|
|
706
|
+
{ callSessionId: session.id },
|
|
707
|
+
);
|
|
708
|
+
const statusCallbackUrl = await resolveCallbackUrl(
|
|
709
|
+
() => getTwilioStatusCallbackUrl(config),
|
|
710
|
+
'webhooks/twilio/status',
|
|
711
|
+
'twilio_status',
|
|
712
|
+
);
|
|
692
713
|
|
|
693
714
|
const { callSid } = await provider.initiateCall({
|
|
694
715
|
from: identityResult.fromNumber,
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Layered call pointer message composition system.
|
|
3
|
+
*
|
|
4
|
+
* Generates pointer/status copy through a priority chain:
|
|
5
|
+
* 1. Generator-produced text (when provided by daemon and audience is trusted)
|
|
6
|
+
* 2. Deterministic fallback templates (preserving existing semantics)
|
|
7
|
+
*
|
|
8
|
+
* Follows the same pattern as approval-message-composer.ts and
|
|
9
|
+
* guardian-action-message-composer.ts.
|
|
10
|
+
*/
|
|
11
|
+
import type { PointerCopyGenerator } from '../runtime/http-types.js';
|
|
12
|
+
import { getLogger } from '../util/logger.js';
|
|
13
|
+
|
|
14
|
+
const log = getLogger('call-pointer-message-composer');
|
|
15
|
+
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
// Types
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
|
|
20
|
+
export type CallPointerMessageScenario =
|
|
21
|
+
| 'started'
|
|
22
|
+
| 'completed'
|
|
23
|
+
| 'failed'
|
|
24
|
+
| 'guardian_verification_succeeded'
|
|
25
|
+
| 'guardian_verification_failed';
|
|
26
|
+
|
|
27
|
+
export interface CallPointerMessageContext {
|
|
28
|
+
scenario: CallPointerMessageScenario;
|
|
29
|
+
phoneNumber: string;
|
|
30
|
+
duration?: string;
|
|
31
|
+
reason?: string;
|
|
32
|
+
verificationCode?: string;
|
|
33
|
+
channel?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface ComposeCallPointerMessageOptions {
|
|
37
|
+
fallbackText?: string;
|
|
38
|
+
requiredFacts?: string[];
|
|
39
|
+
maxTokens?: number;
|
|
40
|
+
timeoutMs?: number;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
// Constants (exported for the daemon-injected generator implementation)
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
|
|
47
|
+
export const POINTER_COPY_TIMEOUT_MS = 3_000;
|
|
48
|
+
export const POINTER_COPY_MAX_TOKENS = 120;
|
|
49
|
+
export const POINTER_COPY_SYSTEM_PROMPT =
|
|
50
|
+
'You are an assistant writing a brief status update about a phone call. '
|
|
51
|
+
+ 'Keep it concise (1-2 sentences), natural, and informative. '
|
|
52
|
+
+ 'Preserve all factual details exactly (phone numbers, durations, failure reasons, verification status). '
|
|
53
|
+
+ 'Do not mention internal systems or technical details. '
|
|
54
|
+
+ 'Return plain text only.';
|
|
55
|
+
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
// Public API
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Compose pointer copy using the daemon-injected generator when available,
|
|
62
|
+
* with deterministic fallback for reliability.
|
|
63
|
+
*
|
|
64
|
+
* The generator parameter is the daemon-provided function that knows about
|
|
65
|
+
* providers. When absent (or in test env), only the deterministic fallback
|
|
66
|
+
* is used.
|
|
67
|
+
*/
|
|
68
|
+
export async function composeCallPointerMessageGenerative(
|
|
69
|
+
context: CallPointerMessageContext,
|
|
70
|
+
options: ComposeCallPointerMessageOptions = {},
|
|
71
|
+
generator?: PointerCopyGenerator,
|
|
72
|
+
): Promise<string> {
|
|
73
|
+
const fallbackText = options.fallbackText?.trim() || getPointerFallbackMessage(context);
|
|
74
|
+
|
|
75
|
+
if (process.env.NODE_ENV === 'test') {
|
|
76
|
+
return fallbackText;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (generator) {
|
|
80
|
+
try {
|
|
81
|
+
const generated = await generator(context, options);
|
|
82
|
+
if (generated) return generated;
|
|
83
|
+
} catch (err) {
|
|
84
|
+
log.warn({ err, scenario: context.scenario }, 'Failed to generate pointer copy, using fallback');
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return fallbackText;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** @internal Exported for use by the daemon-injected generator implementation. */
|
|
92
|
+
export function buildPointerGenerationPrompt(
|
|
93
|
+
context: CallPointerMessageContext,
|
|
94
|
+
fallbackText: string,
|
|
95
|
+
requiredFacts: string[] | undefined,
|
|
96
|
+
): string {
|
|
97
|
+
const factClause = requiredFacts && requiredFacts.length > 0
|
|
98
|
+
? `Required facts to include: ${requiredFacts.join(', ')}.\n`
|
|
99
|
+
: '';
|
|
100
|
+
return [
|
|
101
|
+
'Rewrite the following call status message as a natural, conversational update.',
|
|
102
|
+
'Keep the same concrete facts (phone number, duration, failure reason, verification status).',
|
|
103
|
+
factClause,
|
|
104
|
+
`Context JSON: ${JSON.stringify(context)}`,
|
|
105
|
+
`Fallback message: ${fallbackText}`,
|
|
106
|
+
].filter(Boolean).join('\n\n');
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** @internal Exported for use by the daemon-injected generator implementation. */
|
|
110
|
+
export function includesRequiredFacts(text: string, requiredFacts: string[] | undefined): boolean {
|
|
111
|
+
if (!requiredFacts || requiredFacts.length === 0) return true;
|
|
112
|
+
return requiredFacts.every((fact) => text.includes(fact));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ---------------------------------------------------------------------------
|
|
116
|
+
// Deterministic fallback templates
|
|
117
|
+
// ---------------------------------------------------------------------------
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Return a scenario-specific deterministic fallback message.
|
|
121
|
+
*
|
|
122
|
+
* These preserve the exact semantics of the original hard-coded pointer
|
|
123
|
+
* templates from call-pointer-messages.ts.
|
|
124
|
+
*/
|
|
125
|
+
export function getPointerFallbackMessage(context: CallPointerMessageContext): string {
|
|
126
|
+
switch (context.scenario) {
|
|
127
|
+
case 'started':
|
|
128
|
+
return context.verificationCode
|
|
129
|
+
? `\u{1F4DE} Call to ${context.phoneNumber} started. Verification code: ${context.verificationCode}`
|
|
130
|
+
: `\u{1F4DE} Call to ${context.phoneNumber} started.`;
|
|
131
|
+
case 'completed':
|
|
132
|
+
return context.duration
|
|
133
|
+
? `\u{1F4DE} Call to ${context.phoneNumber} completed (${context.duration}).`
|
|
134
|
+
: `\u{1F4DE} Call to ${context.phoneNumber} completed.`;
|
|
135
|
+
case 'failed':
|
|
136
|
+
return context.reason
|
|
137
|
+
? `\u{1F4DE} Call to ${context.phoneNumber} failed: ${context.reason}.`
|
|
138
|
+
: `\u{1F4DE} Call to ${context.phoneNumber} failed.`;
|
|
139
|
+
case 'guardian_verification_succeeded': {
|
|
140
|
+
const ch = context.channel ?? 'voice';
|
|
141
|
+
return `\u{2705} Guardian verification (${ch}) for ${context.phoneNumber} succeeded.`;
|
|
142
|
+
}
|
|
143
|
+
case 'guardian_verification_failed': {
|
|
144
|
+
const ch = context.channel ?? 'voice';
|
|
145
|
+
return context.reason
|
|
146
|
+
? `\u{274C} Guardian verification (${ch}) for ${context.phoneNumber} failed: ${context.reason}.`
|
|
147
|
+
: `\u{274C} Guardian verification (${ch}) for ${context.phoneNumber} failed.`;
|
|
148
|
+
}
|
|
149
|
+
default: {
|
|
150
|
+
const _exhaustive: never = context.scenario;
|
|
151
|
+
return `Call status update. ${String(_exhaustive)}`;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|