@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
|
@@ -12,10 +12,12 @@ import type { ServerWebSocket } from 'bun';
|
|
|
12
12
|
|
|
13
13
|
import { getConfig } from '../config/loader.js';
|
|
14
14
|
import { getCanonicalGuardianRequest } from '../memory/canonical-guardian-store.js';
|
|
15
|
+
import { listActiveBindingsByAssistant } from '../memory/channel-guardian-store.js';
|
|
15
16
|
import * as conversationStore from '../memory/conversation-store.js';
|
|
16
17
|
import { findActiveVoiceInvites } from '../memory/ingress-invite-store.js';
|
|
17
|
-
import { upsertMember } from '../memory/ingress-member-store.js';
|
|
18
|
+
import { findMember, upsertMember } from '../memory/ingress-member-store.js';
|
|
18
19
|
import { revokeScopedApprovalGrantsForContext } from '../memory/scoped-approval-grants.js';
|
|
20
|
+
import { emitNotificationSignal } from '../notifications/emit-signal.js';
|
|
19
21
|
import { notifyGuardianOfAccessRequest } from '../runtime/access-request-helper.js';
|
|
20
22
|
import {
|
|
21
23
|
resolveActorTrust,
|
|
@@ -23,6 +25,7 @@ import {
|
|
|
23
25
|
} from '../runtime/actor-trust-resolver.js';
|
|
24
26
|
import { DAEMON_INTERNAL_ASSISTANT_ID } from '../runtime/assistant-scope.js';
|
|
25
27
|
import {
|
|
28
|
+
getGuardianBinding,
|
|
26
29
|
getPendingChallenge,
|
|
27
30
|
validateAndConsumeChallenge,
|
|
28
31
|
} from '../runtime/channel-guardian-service.js';
|
|
@@ -33,7 +36,15 @@ import {
|
|
|
33
36
|
import { redeemVoiceInviteCode } from '../runtime/ingress-service.js';
|
|
34
37
|
import { parseJsonSafe } from '../util/json.js';
|
|
35
38
|
import { getLogger } from '../util/logger.js';
|
|
36
|
-
import {
|
|
39
|
+
import {
|
|
40
|
+
getAccessRequestPollIntervalMs,
|
|
41
|
+
getGuardianWaitUpdateInitialIntervalMs,
|
|
42
|
+
getGuardianWaitUpdateInitialWindowMs,
|
|
43
|
+
getGuardianWaitUpdateSteadyMaxIntervalMs,
|
|
44
|
+
getGuardianWaitUpdateSteadyMinIntervalMs,
|
|
45
|
+
getTtsPlaybackDelayMs,
|
|
46
|
+
getUserConsultationTimeoutMs,
|
|
47
|
+
} from './call-constants.js';
|
|
37
48
|
import { CallController } from './call-controller.js';
|
|
38
49
|
import { persistCallCompletionMessage } from './call-conversation-messages.js';
|
|
39
50
|
import { addPointerMessage, formatDuration } from './call-pointer-messages.js';
|
|
@@ -197,6 +208,20 @@ export class RelayConnection {
|
|
|
197
208
|
// Name capture timeout (unknown inbound callers)
|
|
198
209
|
private nameCaptureTimeoutTimer: ReturnType<typeof setTimeout> | null = null;
|
|
199
210
|
|
|
211
|
+
// Guardian wait heartbeat state
|
|
212
|
+
private accessRequestHeartbeatTimer: ReturnType<typeof setTimeout> | null = null;
|
|
213
|
+
private accessRequestWaitStartedAt: number = 0;
|
|
214
|
+
private heartbeatSequence = 0;
|
|
215
|
+
|
|
216
|
+
// In-wait prompt handling state
|
|
217
|
+
private lastInWaitReplyAt = 0;
|
|
218
|
+
private static readonly IN_WAIT_REPLY_COOLDOWN_MS = 3000;
|
|
219
|
+
|
|
220
|
+
// Callback offer state (in-memory per-call)
|
|
221
|
+
private callbackOfferMade = false;
|
|
222
|
+
private callbackOptIn = false;
|
|
223
|
+
private callbackHandoffNotified = false;
|
|
224
|
+
|
|
200
225
|
constructor(ws: ServerWebSocket<RelayWebSocketData>, callSessionId: string) {
|
|
201
226
|
this.ws = ws;
|
|
202
227
|
this.callSessionId = callSessionId;
|
|
@@ -328,6 +353,10 @@ export class RelayConnection {
|
|
|
328
353
|
clearTimeout(this.accessRequestTimeoutTimer);
|
|
329
354
|
this.accessRequestTimeoutTimer = null;
|
|
330
355
|
}
|
|
356
|
+
if (this.accessRequestHeartbeatTimer) {
|
|
357
|
+
clearTimeout(this.accessRequestHeartbeatTimer);
|
|
358
|
+
this.accessRequestHeartbeatTimer = null;
|
|
359
|
+
}
|
|
331
360
|
if (this.nameCaptureTimeoutTimer) {
|
|
332
361
|
clearTimeout(this.nameCaptureTimeoutTimer);
|
|
333
362
|
this.nameCaptureTimeoutTimer = null;
|
|
@@ -344,6 +373,12 @@ export class RelayConnection {
|
|
|
344
373
|
* we still finalize the call lifecycle from the relay close signal.
|
|
345
374
|
*/
|
|
346
375
|
handleTransportClosed(code?: number, reason?: string): void {
|
|
376
|
+
// If the call was still in guardian-wait with callback opt-in, emit the
|
|
377
|
+
// handoff notification before cleaning up wait state.
|
|
378
|
+
if (this.accessRequestWaitActive && this.callbackOptIn) {
|
|
379
|
+
this.emitAccessRequestCallbackHandoff('transport_closed');
|
|
380
|
+
}
|
|
381
|
+
|
|
347
382
|
// Clean up access request wait state on disconnect to stop polling
|
|
348
383
|
this.clearAccessRequestWait();
|
|
349
384
|
if (this.nameCaptureTimeoutTimer) {
|
|
@@ -1166,13 +1201,19 @@ export class RelayConnection {
|
|
|
1166
1201
|
const timeoutMs = getUserConsultationTimeoutMs();
|
|
1167
1202
|
const pollIntervalMs = getAccessRequestPollIntervalMs();
|
|
1168
1203
|
|
|
1204
|
+
const guardianLabel = this.resolveGuardianLabel();
|
|
1169
1205
|
this.sendTextToken(
|
|
1170
|
-
|
|
1206
|
+
`Thank you. I've let ${guardianLabel} know. Please hold while I check if I have permission to speak with you.`,
|
|
1171
1207
|
true,
|
|
1172
1208
|
);
|
|
1173
1209
|
|
|
1174
1210
|
updateCallSession(this.callSessionId, { status: 'waiting_on_user' });
|
|
1175
1211
|
|
|
1212
|
+
// Start the heartbeat timer for periodic progress updates
|
|
1213
|
+
this.accessRequestWaitStartedAt = Date.now();
|
|
1214
|
+
this.heartbeatSequence = 0;
|
|
1215
|
+
this.scheduleNextHeartbeat();
|
|
1216
|
+
|
|
1176
1217
|
// Poll the canonical request status
|
|
1177
1218
|
this.accessRequestPollTimer = setInterval(() => {
|
|
1178
1219
|
if (!this.accessRequestWaitActive || !this.accessRequestId) {
|
|
@@ -1224,6 +1265,10 @@ export class RelayConnection {
|
|
|
1224
1265
|
clearTimeout(this.accessRequestTimeoutTimer);
|
|
1225
1266
|
this.accessRequestTimeoutTimer = null;
|
|
1226
1267
|
}
|
|
1268
|
+
if (this.accessRequestHeartbeatTimer) {
|
|
1269
|
+
clearTimeout(this.accessRequestHeartbeatTimer);
|
|
1270
|
+
this.accessRequestHeartbeatTimer = null;
|
|
1271
|
+
}
|
|
1227
1272
|
}
|
|
1228
1273
|
|
|
1229
1274
|
/**
|
|
@@ -1282,10 +1327,10 @@ export class RelayConnection {
|
|
|
1282
1327
|
|
|
1283
1328
|
// Use handleUserInstruction to deliver the approval-aware greeting
|
|
1284
1329
|
// through the normal session pipeline.
|
|
1285
|
-
const
|
|
1330
|
+
const guardianLabel = this.resolveGuardianLabel();
|
|
1286
1331
|
if (this.controller) {
|
|
1287
1332
|
this.controller.handleUserInstruction(
|
|
1288
|
-
`Great, ${
|
|
1333
|
+
`Great, ${guardianLabel} approved! Now how can I help you?`,
|
|
1289
1334
|
).catch((err) => {
|
|
1290
1335
|
log.error({ err, callSessionId: this.callSessionId }, 'Failed to deliver approval greeting');
|
|
1291
1336
|
});
|
|
@@ -1298,13 +1343,15 @@ export class RelayConnection {
|
|
|
1298
1343
|
private handleAccessRequestDenied(): void {
|
|
1299
1344
|
this.clearAccessRequestWait();
|
|
1300
1345
|
|
|
1346
|
+
const guardianLabel = this.resolveGuardianLabel();
|
|
1347
|
+
|
|
1301
1348
|
recordCallEvent(this.callSessionId, 'inbound_acl_access_denied', {
|
|
1302
1349
|
from: this.accessRequestFromNumber,
|
|
1303
1350
|
requestId: this.accessRequestId,
|
|
1304
1351
|
});
|
|
1305
1352
|
|
|
1306
1353
|
this.sendTextToken(
|
|
1307
|
-
|
|
1354
|
+
`Sorry, ${guardianLabel} says I'm not allowed to speak with you. Goodbye.`,
|
|
1308
1355
|
true,
|
|
1309
1356
|
);
|
|
1310
1357
|
|
|
@@ -1330,15 +1377,24 @@ export class RelayConnection {
|
|
|
1330
1377
|
* Handle an access request timeout: deliver deterministic copy and hang up.
|
|
1331
1378
|
*/
|
|
1332
1379
|
private handleAccessRequestTimeout(): void {
|
|
1380
|
+
// Emit callback handoff notification before clearing wait state
|
|
1381
|
+
this.emitAccessRequestCallbackHandoff('timeout');
|
|
1382
|
+
|
|
1333
1383
|
this.clearAccessRequestWait();
|
|
1334
1384
|
|
|
1385
|
+
const guardianLabel = this.resolveGuardianLabel();
|
|
1386
|
+
|
|
1335
1387
|
recordCallEvent(this.callSessionId, 'inbound_acl_access_timeout', {
|
|
1336
1388
|
from: this.accessRequestFromNumber,
|
|
1337
1389
|
requestId: this.accessRequestId,
|
|
1390
|
+
callbackOptIn: this.callbackOptIn,
|
|
1338
1391
|
});
|
|
1339
1392
|
|
|
1393
|
+
const callbackNote = this.callbackOptIn
|
|
1394
|
+
? ` I've noted that you'd like a callback — I'll pass that along to ${guardianLabel}.`
|
|
1395
|
+
: '';
|
|
1340
1396
|
this.sendTextToken(
|
|
1341
|
-
|
|
1397
|
+
`Sorry, I can't get ahold of ${guardianLabel} right now. I'll let them know you called.${callbackNote}`,
|
|
1342
1398
|
true,
|
|
1343
1399
|
);
|
|
1344
1400
|
|
|
@@ -1360,6 +1416,100 @@ export class RelayConnection {
|
|
|
1360
1416
|
}, getTtsPlaybackDelayMs());
|
|
1361
1417
|
}
|
|
1362
1418
|
|
|
1419
|
+
/**
|
|
1420
|
+
* Emit a callback handoff notification to the guardian when the caller
|
|
1421
|
+
* opted into a callback during guardian wait but the wait ended without
|
|
1422
|
+
* resolution (timeout or transport close).
|
|
1423
|
+
*
|
|
1424
|
+
* Idempotent: uses callbackHandoffNotified guard + deterministic dedupeKey
|
|
1425
|
+
* to ensure at most one notification per call/request.
|
|
1426
|
+
*/
|
|
1427
|
+
private emitAccessRequestCallbackHandoff(reason: 'timeout' | 'transport_closed'): void {
|
|
1428
|
+
if (!this.callbackOptIn) return;
|
|
1429
|
+
if (!this.accessRequestId) return;
|
|
1430
|
+
if (this.callbackHandoffNotified) return;
|
|
1431
|
+
|
|
1432
|
+
this.callbackHandoffNotified = true;
|
|
1433
|
+
|
|
1434
|
+
const assistantId = this.accessRequestAssistantId ?? DAEMON_INTERNAL_ASSISTANT_ID;
|
|
1435
|
+
const fromNumber = this.accessRequestFromNumber ?? null;
|
|
1436
|
+
|
|
1437
|
+
// Resolve canonical request for requestCode and conversationId
|
|
1438
|
+
const canonicalRequest = this.accessRequestId
|
|
1439
|
+
? getCanonicalGuardianRequest(this.accessRequestId)
|
|
1440
|
+
: null;
|
|
1441
|
+
|
|
1442
|
+
// Resolve trusted-contact member reference when possible
|
|
1443
|
+
let requesterMemberId: string | null = null;
|
|
1444
|
+
if (fromNumber) {
|
|
1445
|
+
try {
|
|
1446
|
+
const member = findMember({
|
|
1447
|
+
assistantId,
|
|
1448
|
+
sourceChannel: 'voice',
|
|
1449
|
+
externalUserId: fromNumber,
|
|
1450
|
+
externalChatId: fromNumber,
|
|
1451
|
+
});
|
|
1452
|
+
if (member && member.status === 'active' && member.policy === 'allow') {
|
|
1453
|
+
requesterMemberId = member.id;
|
|
1454
|
+
}
|
|
1455
|
+
} catch (err) {
|
|
1456
|
+
log.warn({ err, callSessionId: this.callSessionId }, 'Failed to resolve member for callback handoff');
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
const dedupeKey = `access-request-callback-handoff:${this.accessRequestId}`;
|
|
1461
|
+
const sourceSessionId = canonicalRequest?.conversationId
|
|
1462
|
+
?? `access-req-callback-${this.accessRequestId}`;
|
|
1463
|
+
|
|
1464
|
+
void emitNotificationSignal({
|
|
1465
|
+
sourceEventName: 'ingress.access_request.callback_handoff',
|
|
1466
|
+
sourceChannel: 'voice',
|
|
1467
|
+
sourceSessionId,
|
|
1468
|
+
assistantId,
|
|
1469
|
+
attentionHints: {
|
|
1470
|
+
requiresAction: false,
|
|
1471
|
+
urgency: 'medium',
|
|
1472
|
+
isAsyncBackground: true,
|
|
1473
|
+
visibleInSourceNow: false,
|
|
1474
|
+
},
|
|
1475
|
+
contextPayload: {
|
|
1476
|
+
requestId: this.accessRequestId,
|
|
1477
|
+
requestCode: canonicalRequest?.requestCode ?? null,
|
|
1478
|
+
callSessionId: this.callSessionId,
|
|
1479
|
+
sourceChannel: 'voice',
|
|
1480
|
+
reason,
|
|
1481
|
+
callbackOptIn: true,
|
|
1482
|
+
callerPhoneNumber: fromNumber,
|
|
1483
|
+
callerName: this.accessRequestCallerName ?? null,
|
|
1484
|
+
requesterExternalUserId: fromNumber,
|
|
1485
|
+
requesterChatId: fromNumber,
|
|
1486
|
+
requesterMemberId,
|
|
1487
|
+
requesterMemberSourceChannel: requesterMemberId ? 'voice' : null,
|
|
1488
|
+
},
|
|
1489
|
+
dedupeKey,
|
|
1490
|
+
}).then(() => {
|
|
1491
|
+
recordCallEvent(this.callSessionId, 'callback_handoff_notified', {
|
|
1492
|
+
requestId: this.accessRequestId,
|
|
1493
|
+
reason,
|
|
1494
|
+
requesterMemberId,
|
|
1495
|
+
});
|
|
1496
|
+
log.info(
|
|
1497
|
+
{ callSessionId: this.callSessionId, requestId: this.accessRequestId, reason },
|
|
1498
|
+
'Callback handoff notification emitted',
|
|
1499
|
+
);
|
|
1500
|
+
}).catch((err) => {
|
|
1501
|
+
recordCallEvent(this.callSessionId, 'callback_handoff_failed', {
|
|
1502
|
+
requestId: this.accessRequestId,
|
|
1503
|
+
reason,
|
|
1504
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1505
|
+
});
|
|
1506
|
+
log.error(
|
|
1507
|
+
{ err, callSessionId: this.callSessionId, requestId: this.accessRequestId },
|
|
1508
|
+
'Failed to emit callback handoff notification',
|
|
1509
|
+
);
|
|
1510
|
+
});
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1363
1513
|
/**
|
|
1364
1514
|
* Handle a name capture timeout: the caller never provided their name
|
|
1365
1515
|
* within the allotted window. Deliver deterministic copy and hang up.
|
|
@@ -1482,6 +1632,270 @@ export class RelayConnection {
|
|
|
1482
1632
|
}
|
|
1483
1633
|
}
|
|
1484
1634
|
|
|
1635
|
+
// ── Guardian wait UX layer ─────────────────────────────────────
|
|
1636
|
+
|
|
1637
|
+
/**
|
|
1638
|
+
* Resolve a human-readable guardian label for voice wait copy.
|
|
1639
|
+
* Prefers displayName from the guardian binding metadata, falls back
|
|
1640
|
+
* to @username, then "my guardian".
|
|
1641
|
+
*/
|
|
1642
|
+
private resolveGuardianLabel(): string {
|
|
1643
|
+
const assistantId = this.accessRequestAssistantId ?? DAEMON_INTERNAL_ASSISTANT_ID;
|
|
1644
|
+
|
|
1645
|
+
// Try the voice-channel binding first, then fall back to any active
|
|
1646
|
+
// binding for the assistant (mirrors the cross-channel fallback pattern
|
|
1647
|
+
// in access-request-helper.ts).
|
|
1648
|
+
let metadataJson: string | null = null;
|
|
1649
|
+
const voiceBinding = getGuardianBinding(assistantId, 'voice');
|
|
1650
|
+
if (voiceBinding?.metadataJson) {
|
|
1651
|
+
metadataJson = voiceBinding.metadataJson;
|
|
1652
|
+
} else {
|
|
1653
|
+
const allBindings = listActiveBindingsByAssistant(assistantId);
|
|
1654
|
+
if (allBindings.length > 0 && allBindings[0].metadataJson) {
|
|
1655
|
+
metadataJson = allBindings[0].metadataJson;
|
|
1656
|
+
}
|
|
1657
|
+
}
|
|
1658
|
+
|
|
1659
|
+
if (metadataJson) {
|
|
1660
|
+
try {
|
|
1661
|
+
const parsed = JSON.parse(metadataJson) as Record<string, unknown>;
|
|
1662
|
+
if (typeof parsed.displayName === 'string' && parsed.displayName.trim().length > 0) {
|
|
1663
|
+
return parsed.displayName.trim();
|
|
1664
|
+
}
|
|
1665
|
+
if (typeof parsed.username === 'string' && parsed.username.trim().length > 0) {
|
|
1666
|
+
return `@${parsed.username.trim()}`;
|
|
1667
|
+
}
|
|
1668
|
+
} catch {
|
|
1669
|
+
// ignore malformed metadata
|
|
1670
|
+
}
|
|
1671
|
+
}
|
|
1672
|
+
return 'my guardian';
|
|
1673
|
+
}
|
|
1674
|
+
|
|
1675
|
+
/**
|
|
1676
|
+
* Generate a non-repetitive heartbeat message for the caller based
|
|
1677
|
+
* on the current sequence counter and guardian label.
|
|
1678
|
+
*/
|
|
1679
|
+
private getHeartbeatMessage(): string {
|
|
1680
|
+
const guardianLabel = this.resolveGuardianLabel();
|
|
1681
|
+
const seq = this.heartbeatSequence++;
|
|
1682
|
+
const messages = [
|
|
1683
|
+
`Still waiting to hear back from ${guardianLabel}. Thank you for your patience.`,
|
|
1684
|
+
`I'm still trying to reach ${guardianLabel}. One moment please.`,
|
|
1685
|
+
`Hang tight, still waiting on ${guardianLabel}.`,
|
|
1686
|
+
`Still checking with ${guardianLabel}. I appreciate you waiting.`,
|
|
1687
|
+
`I haven't heard back from ${guardianLabel} yet. Thanks for holding.`,
|
|
1688
|
+
];
|
|
1689
|
+
return messages[seq % messages.length];
|
|
1690
|
+
}
|
|
1691
|
+
|
|
1692
|
+
/**
|
|
1693
|
+
* Schedule the next heartbeat update. Uses the initial fixed interval
|
|
1694
|
+
* during the initial window, then jitters between steady min/max.
|
|
1695
|
+
*/
|
|
1696
|
+
private scheduleNextHeartbeat(): void {
|
|
1697
|
+
if (!this.accessRequestWaitActive) return;
|
|
1698
|
+
|
|
1699
|
+
const elapsed = Date.now() - this.accessRequestWaitStartedAt;
|
|
1700
|
+
const initialWindow = getGuardianWaitUpdateInitialWindowMs();
|
|
1701
|
+
const intervalMs = elapsed < initialWindow
|
|
1702
|
+
? getGuardianWaitUpdateInitialIntervalMs()
|
|
1703
|
+
: getGuardianWaitUpdateSteadyMinIntervalMs() +
|
|
1704
|
+
Math.floor(Math.random() * Math.max(0, getGuardianWaitUpdateSteadyMaxIntervalMs() - getGuardianWaitUpdateSteadyMinIntervalMs()));
|
|
1705
|
+
|
|
1706
|
+
this.accessRequestHeartbeatTimer = setTimeout(() => {
|
|
1707
|
+
if (!this.accessRequestWaitActive) return;
|
|
1708
|
+
|
|
1709
|
+
const message = this.getHeartbeatMessage();
|
|
1710
|
+
this.sendTextToken(message, true);
|
|
1711
|
+
|
|
1712
|
+
recordCallEvent(this.callSessionId, 'voice_guardian_wait_heartbeat_sent', {
|
|
1713
|
+
sequence: this.heartbeatSequence - 1,
|
|
1714
|
+
message,
|
|
1715
|
+
});
|
|
1716
|
+
|
|
1717
|
+
log.debug(
|
|
1718
|
+
{ callSessionId: this.callSessionId, sequence: this.heartbeatSequence - 1 },
|
|
1719
|
+
'Guardian wait heartbeat sent',
|
|
1720
|
+
);
|
|
1721
|
+
|
|
1722
|
+
// Schedule the next heartbeat
|
|
1723
|
+
this.scheduleNextHeartbeat();
|
|
1724
|
+
}, intervalMs);
|
|
1725
|
+
}
|
|
1726
|
+
|
|
1727
|
+
/**
|
|
1728
|
+
* Classify a caller utterance during guardian wait into one of:
|
|
1729
|
+
* - 'empty': whitespace or noise
|
|
1730
|
+
* - 'patience_check': asking for status or checking in
|
|
1731
|
+
* - 'impatient': expressing frustration or wanting to end
|
|
1732
|
+
* - 'callback_opt_in': explicitly agreeing to a callback
|
|
1733
|
+
* - 'callback_decline': explicitly declining a callback
|
|
1734
|
+
* - 'neutral': anything else
|
|
1735
|
+
*/
|
|
1736
|
+
private classifyWaitUtterance(text: string): 'empty' | 'patience_check' | 'impatient' | 'callback_opt_in' | 'callback_decline' | 'neutral' {
|
|
1737
|
+
const lower = text.toLowerCase().trim();
|
|
1738
|
+
if (lower.length === 0) return 'empty';
|
|
1739
|
+
|
|
1740
|
+
// Callback opt-in patterns (check before impatience to catch "yes call me back")
|
|
1741
|
+
if (this.callbackOfferMade) {
|
|
1742
|
+
if (/\b(yes|yeah|yep|sure|okay|ok|please)\b.*\b(call\s*(me\s*)?back|callback)\b/.test(lower)
|
|
1743
|
+
|| /\b(call\s*(me\s*)?back|callback)\b.*\b(yes|yeah|please|sure)\b/.test(lower)
|
|
1744
|
+
|| /^(yes|yeah|yep|sure|okay|ok|please)\s*[.,!]?\s*$/.test(lower)
|
|
1745
|
+
|| /\bcall\s*(me\s*)?back\b/.test(lower)
|
|
1746
|
+
|| /\bplease\s+do\b/.test(lower)) {
|
|
1747
|
+
return 'callback_opt_in';
|
|
1748
|
+
}
|
|
1749
|
+
if (/\b(no|nah|nope)\b/.test(lower)
|
|
1750
|
+
|| /\bi('?ll| will)\s+hold\b/.test(lower)
|
|
1751
|
+
|| /\bi('?ll| will)\s+wait\b/.test(lower)) {
|
|
1752
|
+
return 'callback_decline';
|
|
1753
|
+
}
|
|
1754
|
+
}
|
|
1755
|
+
|
|
1756
|
+
// Impatience patterns
|
|
1757
|
+
if (/\bhurry\s*(up)?\b/.test(lower)
|
|
1758
|
+
|| /\btaking\s+(too\s+|so\s+)?long\b/.test(lower)
|
|
1759
|
+
|| /\bforget\s+it\b/.test(lower)
|
|
1760
|
+
|| /\bnever\s*mind\b/.test(lower)
|
|
1761
|
+
|| /\bdon'?t\s+have\s+time\b/.test(lower)
|
|
1762
|
+
|| /\bhow\s+much\s+longer\b/.test(lower)
|
|
1763
|
+
|| /\bi('?m| am)\s+(getting\s+)?impatient\b/.test(lower)
|
|
1764
|
+
|| /\bthis\s+is\s+(ridiculous|absurd|crazy)\b/.test(lower)
|
|
1765
|
+
|| /\bcome\s+on\b/.test(lower)
|
|
1766
|
+
|| /\bi\s+(gotta|have\s+to|need\s+to)\s+go\b/.test(lower)) {
|
|
1767
|
+
return 'impatient';
|
|
1768
|
+
}
|
|
1769
|
+
|
|
1770
|
+
// Patience check / status inquiry patterns
|
|
1771
|
+
if (/\bhello\??\s*$/.test(lower)
|
|
1772
|
+
|| /\bstill\s+there\b/.test(lower)
|
|
1773
|
+
|| /\bany\s+(update|news)\b/.test(lower)
|
|
1774
|
+
|| /\bwhat('?s| is)\s+(happening|going\s+on)\b/.test(lower)
|
|
1775
|
+
|| /\bare\s+you\s+still\b/.test(lower)
|
|
1776
|
+
|| /\bhow\s+(long|much\s+longer)\b/.test(lower)
|
|
1777
|
+
|| /\banyone\s+there\b/.test(lower)) {
|
|
1778
|
+
return 'patience_check';
|
|
1779
|
+
}
|
|
1780
|
+
|
|
1781
|
+
return 'neutral';
|
|
1782
|
+
}
|
|
1783
|
+
|
|
1784
|
+
/**
|
|
1785
|
+
* Handle a caller utterance during the guardian decision wait state.
|
|
1786
|
+
* Provides reassurance, impatience detection, and callback offer.
|
|
1787
|
+
*/
|
|
1788
|
+
private handleWaitStatePrompt(text: string): void {
|
|
1789
|
+
const now = Date.now();
|
|
1790
|
+
const classification = this.classifyWaitUtterance(text);
|
|
1791
|
+
|
|
1792
|
+
recordCallEvent(this.callSessionId, 'voice_guardian_wait_prompt_classified', {
|
|
1793
|
+
classification,
|
|
1794
|
+
transcript: text,
|
|
1795
|
+
});
|
|
1796
|
+
|
|
1797
|
+
if (classification === 'empty') return;
|
|
1798
|
+
|
|
1799
|
+
const guardianLabel = this.resolveGuardianLabel();
|
|
1800
|
+
|
|
1801
|
+
// Callback decisions must always be processed regardless of cooldown —
|
|
1802
|
+
// the caller is answering a direct question and dropping their response
|
|
1803
|
+
// would silently discard their decision.
|
|
1804
|
+
switch (classification) {
|
|
1805
|
+
case 'callback_opt_in': {
|
|
1806
|
+
this.callbackOptIn = true;
|
|
1807
|
+
this.lastInWaitReplyAt = now;
|
|
1808
|
+
recordCallEvent(this.callSessionId, 'voice_guardian_wait_callback_opt_in_set', {});
|
|
1809
|
+
if (this.accessRequestHeartbeatTimer) {
|
|
1810
|
+
clearTimeout(this.accessRequestHeartbeatTimer);
|
|
1811
|
+
this.accessRequestHeartbeatTimer = null;
|
|
1812
|
+
}
|
|
1813
|
+
this.sendTextToken(
|
|
1814
|
+
`Noted, I'll make sure ${guardianLabel} knows you'd like a callback. For now, I'll keep trying to reach them.`,
|
|
1815
|
+
true,
|
|
1816
|
+
);
|
|
1817
|
+
this.scheduleNextHeartbeat();
|
|
1818
|
+
return;
|
|
1819
|
+
}
|
|
1820
|
+
case 'callback_decline': {
|
|
1821
|
+
this.callbackOptIn = false;
|
|
1822
|
+
this.lastInWaitReplyAt = now;
|
|
1823
|
+
recordCallEvent(this.callSessionId, 'voice_guardian_wait_callback_opt_in_declined', {});
|
|
1824
|
+
if (this.accessRequestHeartbeatTimer) {
|
|
1825
|
+
clearTimeout(this.accessRequestHeartbeatTimer);
|
|
1826
|
+
this.accessRequestHeartbeatTimer = null;
|
|
1827
|
+
}
|
|
1828
|
+
this.sendTextToken(
|
|
1829
|
+
`No problem, I'll keep holding. Still waiting on ${guardianLabel}.`,
|
|
1830
|
+
true,
|
|
1831
|
+
);
|
|
1832
|
+
this.scheduleNextHeartbeat();
|
|
1833
|
+
return;
|
|
1834
|
+
}
|
|
1835
|
+
default:
|
|
1836
|
+
break;
|
|
1837
|
+
}
|
|
1838
|
+
|
|
1839
|
+
// Enforce cooldown on non-callback utterances to prevent spam
|
|
1840
|
+
if (now - this.lastInWaitReplyAt < RelayConnection.IN_WAIT_REPLY_COOLDOWN_MS) {
|
|
1841
|
+
log.debug({ callSessionId: this.callSessionId }, 'In-wait reply suppressed by cooldown');
|
|
1842
|
+
return;
|
|
1843
|
+
}
|
|
1844
|
+
this.lastInWaitReplyAt = now;
|
|
1845
|
+
|
|
1846
|
+
switch (classification) {
|
|
1847
|
+
case 'impatient': {
|
|
1848
|
+
if (this.accessRequestHeartbeatTimer) {
|
|
1849
|
+
clearTimeout(this.accessRequestHeartbeatTimer);
|
|
1850
|
+
this.accessRequestHeartbeatTimer = null;
|
|
1851
|
+
}
|
|
1852
|
+
if (!this.callbackOfferMade) {
|
|
1853
|
+
this.callbackOfferMade = true;
|
|
1854
|
+
recordCallEvent(this.callSessionId, 'voice_guardian_wait_callback_offer_sent', {});
|
|
1855
|
+
this.sendTextToken(
|
|
1856
|
+
`I understand this is taking a while. I can have ${guardianLabel} call you back once I hear from them. Would you like that, or would you prefer to keep holding?`,
|
|
1857
|
+
true,
|
|
1858
|
+
);
|
|
1859
|
+
} else {
|
|
1860
|
+
// Already offered callback — just reassure
|
|
1861
|
+
this.sendTextToken(
|
|
1862
|
+
`I hear you, I'm sorry for the wait. Still trying to reach ${guardianLabel}.`,
|
|
1863
|
+
true,
|
|
1864
|
+
);
|
|
1865
|
+
}
|
|
1866
|
+
this.scheduleNextHeartbeat();
|
|
1867
|
+
break;
|
|
1868
|
+
}
|
|
1869
|
+
case 'patience_check': {
|
|
1870
|
+
// Immediate reassurance — reset the heartbeat timer so we
|
|
1871
|
+
// don't double up with a scheduled heartbeat
|
|
1872
|
+
if (this.accessRequestHeartbeatTimer) {
|
|
1873
|
+
clearTimeout(this.accessRequestHeartbeatTimer);
|
|
1874
|
+
this.accessRequestHeartbeatTimer = null;
|
|
1875
|
+
}
|
|
1876
|
+
this.sendTextToken(
|
|
1877
|
+
`Yes, I'm still here. Still waiting to hear back from ${guardianLabel}.`,
|
|
1878
|
+
true,
|
|
1879
|
+
);
|
|
1880
|
+
this.scheduleNextHeartbeat();
|
|
1881
|
+
break;
|
|
1882
|
+
}
|
|
1883
|
+
case 'neutral':
|
|
1884
|
+
default: {
|
|
1885
|
+
if (this.accessRequestHeartbeatTimer) {
|
|
1886
|
+
clearTimeout(this.accessRequestHeartbeatTimer);
|
|
1887
|
+
this.accessRequestHeartbeatTimer = null;
|
|
1888
|
+
}
|
|
1889
|
+
this.sendTextToken(
|
|
1890
|
+
`Thanks for that. I'm still waiting on ${guardianLabel}. I'll let you know as soon as I hear back.`,
|
|
1891
|
+
true,
|
|
1892
|
+
);
|
|
1893
|
+
this.scheduleNextHeartbeat();
|
|
1894
|
+
break;
|
|
1895
|
+
}
|
|
1896
|
+
}
|
|
1897
|
+
}
|
|
1898
|
+
|
|
1485
1899
|
private async handlePrompt(msg: RelayPromptMessage): Promise<void> {
|
|
1486
1900
|
if (this.connectionState === 'disconnecting') {
|
|
1487
1901
|
return;
|
|
@@ -1509,12 +1923,10 @@ export class RelayConnection {
|
|
|
1509
1923
|
return;
|
|
1510
1924
|
}
|
|
1511
1925
|
|
|
1512
|
-
// During guardian decision wait,
|
|
1926
|
+
// During guardian decision wait, classify caller speech for
|
|
1927
|
+
// reassurance, impatience detection, and callback offer.
|
|
1513
1928
|
if (this.connectionState === 'awaiting_guardian_decision') {
|
|
1514
|
-
|
|
1515
|
-
{ callSessionId: this.callSessionId },
|
|
1516
|
-
'Ignoring voice prompt during guardian decision wait',
|
|
1517
|
-
);
|
|
1929
|
+
this.handleWaitStatePrompt(msg.voicePrompt);
|
|
1518
1930
|
return;
|
|
1519
1931
|
}
|
|
1520
1932
|
|
|
@@ -15,18 +15,11 @@ export interface TwilioConfig {
|
|
|
15
15
|
wssBaseUrl: string;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
function resolveTwilioPhoneNumber(
|
|
18
|
+
function resolveTwilioPhoneNumber(config: ReturnType<typeof loadConfig>, assistantId?: string): string {
|
|
19
19
|
if (assistantId) {
|
|
20
|
-
const
|
|
21
|
-
if (
|
|
22
|
-
return assistantPhone;
|
|
23
|
-
}
|
|
20
|
+
const scoped = (config.sms?.assistantPhoneNumbers as Record<string, string> | undefined)?.[assistantId];
|
|
21
|
+
if (scoped) return scoped;
|
|
24
22
|
}
|
|
25
|
-
|
|
26
|
-
// Global fallback order:
|
|
27
|
-
// 1. TWILIO_PHONE_NUMBER env var (explicit override)
|
|
28
|
-
// 2. config file sms.phoneNumber (primary storage)
|
|
29
|
-
// 3. credential:twilio:phone_number secure key (backward-compat fallback)
|
|
30
23
|
return getTwilioPhoneNumberEnv() || config.sms?.phoneNumber || getSecureKey('credential:twilio:phone_number') || '';
|
|
31
24
|
}
|
|
32
25
|
|
|
@@ -34,7 +27,7 @@ export function getTwilioConfig(assistantId?: string): TwilioConfig {
|
|
|
34
27
|
const accountSid = getSecureKey('credential:twilio:account_sid');
|
|
35
28
|
const authToken = getSecureKey('credential:twilio:auth_token');
|
|
36
29
|
const config = loadConfig();
|
|
37
|
-
const phoneNumber = resolveTwilioPhoneNumber(
|
|
30
|
+
const phoneNumber = resolveTwilioPhoneNumber(config, assistantId);
|
|
38
31
|
const webhookBaseUrl = getPublicBaseUrl(config);
|
|
39
32
|
|
|
40
33
|
// Always use the centralized relay URL derived from the public ingress base URL.
|
package/src/calls/types.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
export type CallStatus = 'initiated' | 'ringing' | 'in_progress' | 'waiting_on_user' | 'completed' | 'failed' | 'cancelled';
|
|
2
|
-
export type CallEventType = 'call_started' | 'call_connected' | 'caller_spoke' | 'assistant_spoke' | 'user_question_asked' | 'user_answered' | 'user_instruction_relayed' | 'call_ended' | 'call_failed' | 'callee_verification_started' | 'callee_verification_succeeded' | 'callee_verification_failed' | 'guardian_voice_verification_started' | 'guardian_voice_verification_succeeded' | 'guardian_voice_verification_failed' | 'outbound_guardian_voice_verification_started' | 'outbound_guardian_voice_verification_succeeded' | 'outbound_guardian_voice_verification_failed' | 'guardian_consultation_timed_out' | 'guardian_unavailable_skipped' | 'guardian_consult_deferred' | 'guardian_consult_coalesced' | 'inbound_acl_denied' | 'inbound_acl_name_capture_started' | 'inbound_acl_name_captured' | 'inbound_acl_name_capture_timeout' | 'inbound_acl_access_approved' | 'inbound_acl_access_denied' | 'inbound_acl_access_timeout' | 'invite_redemption_started' | 'invite_redemption_succeeded' | 'invite_redemption_failed'
|
|
2
|
+
export type CallEventType = 'call_started' | 'call_connected' | 'caller_spoke' | 'assistant_spoke' | 'user_question_asked' | 'user_answered' | 'user_instruction_relayed' | 'call_ended' | 'call_failed' | 'callee_verification_started' | 'callee_verification_succeeded' | 'callee_verification_failed' | 'guardian_voice_verification_started' | 'guardian_voice_verification_succeeded' | 'guardian_voice_verification_failed' | 'outbound_guardian_voice_verification_started' | 'outbound_guardian_voice_verification_succeeded' | 'outbound_guardian_voice_verification_failed' | 'guardian_consultation_timed_out' | 'guardian_unavailable_skipped' | 'guardian_consult_deferred' | 'guardian_consult_coalesced' | 'inbound_acl_denied' | 'inbound_acl_name_capture_started' | 'inbound_acl_name_captured' | 'inbound_acl_name_capture_timeout' | 'inbound_acl_access_approved' | 'inbound_acl_access_denied' | 'inbound_acl_access_timeout' | 'invite_redemption_started' | 'invite_redemption_succeeded' | 'invite_redemption_failed' | 'voice_guardian_wait_heartbeat_sent' | 'voice_guardian_wait_prompt_classified' | 'voice_guardian_wait_callback_offer_sent' | 'voice_guardian_wait_callback_opt_in_set' | 'voice_guardian_wait_callback_opt_in_declined'
|
|
3
|
+
| 'callback_handoff_notified'
|
|
4
|
+
| 'callback_handoff_failed';
|
|
3
5
|
export type PendingQuestionStatus = 'pending' | 'answered' | 'expired' | 'cancelled';
|
|
4
6
|
|
|
5
7
|
/**
|
package/src/cli.ts
CHANGED
|
@@ -233,7 +233,7 @@ export async function startCli(): Promise<void> {
|
|
|
233
233
|
process.stdout.write(`\u2502\n`);
|
|
234
234
|
process.stdout.write(`\u2502 [a] Allow once\n`);
|
|
235
235
|
process.stdout.write(`\u2502 [d] Deny once\n`);
|
|
236
|
-
if (req.allowlistOptions.length > 0) {
|
|
236
|
+
if (req.allowlistOptions.length > 0 && req.scopeOptions.length > 0) {
|
|
237
237
|
process.stdout.write(`\u2502 [A] Allowlist...\n`);
|
|
238
238
|
process.stdout.write(`\u2502 [H] Allowlist (high-risk)...\n`);
|
|
239
239
|
process.stdout.write(`\u2502 [D] Denylist...\n`);
|
|
@@ -246,21 +246,22 @@ export async function startCli(): Promise<void> {
|
|
|
246
246
|
const choice = trimmed.toLowerCase();
|
|
247
247
|
|
|
248
248
|
// Uppercase 'A' → allowlist pattern selection (check before lowercase 'a')
|
|
249
|
-
|
|
249
|
+
// Only process when scope options exist, matching the display guard above
|
|
250
|
+
if ((trimmed === 'A' || choice === 'allowlist') && req.allowlistOptions.length > 0 && req.scopeOptions.length > 0) {
|
|
250
251
|
// pendingConfirmation stays true through sub-prompts
|
|
251
252
|
renderPatternSelection(req, 'always_allow');
|
|
252
253
|
return;
|
|
253
254
|
}
|
|
254
255
|
|
|
255
256
|
// Uppercase 'H' → high-risk allowlist pattern selection
|
|
256
|
-
if (trimmed === 'H') {
|
|
257
|
+
if (trimmed === 'H' && req.allowlistOptions.length > 0 && req.scopeOptions.length > 0) {
|
|
257
258
|
// pendingConfirmation stays true through sub-prompts
|
|
258
259
|
renderPatternSelection(req, 'always_allow_high_risk');
|
|
259
260
|
return;
|
|
260
261
|
}
|
|
261
262
|
|
|
262
263
|
// Uppercase 'D' → denylist pattern selection (check before lowercase 'd')
|
|
263
|
-
if (trimmed === 'D' || choice === 'denylist') {
|
|
264
|
+
if ((trimmed === 'D' || choice === 'denylist') && req.allowlistOptions.length > 0 && req.scopeOptions.length > 0) {
|
|
264
265
|
// pendingConfirmation stays true through sub-prompts
|
|
265
266
|
renderPatternSelection(req, 'always_deny');
|
|
266
267
|
return;
|
|
@@ -14,6 +14,10 @@ Example: `host_bash("vellum email status --json")`
|
|
|
14
14
|
|
|
15
15
|
Never use browser/computer-use unless user explicitly approves fallback.
|
|
16
16
|
|
|
17
|
+
## When to Use This Skill
|
|
18
|
+
|
|
19
|
+
This skill manages the **assistant's own** AgentMail address (`@agentmail.to`) — not the user's personal email. Only use this skill when the user explicitly asks the assistant to send email **from the assistant's own address**, manage the assistant's inbox, or perform operations on the assistant's AgentMail account. Generic email requests ("send an email", "check my email", "draft a reply") are about the **user's Gmail** and should be handled by the Messaging skill instead.
|
|
20
|
+
|
|
17
21
|
## Rules
|
|
18
22
|
|
|
19
23
|
- Always run `vellum email` commands via `host_bash` and parse JSON output.
|