@vellumai/assistant 0.4.45 → 0.4.48
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/ARCHITECTURE.md +6 -6
- package/docs/architecture/memory.md +1 -1
- package/docs/architecture/scheduling.md +2 -3
- package/docs/architecture/security.md +5 -5
- package/docs/trusted-contact-access.md +5 -6
- package/package.json +4 -1
- package/src/__tests__/avatar-e2e.test.ts +18 -219
- package/src/__tests__/avatar-generator.test.ts +5 -57
- package/src/__tests__/browser-fill-credential.test.ts +5 -2
- package/src/__tests__/bundled-skill-retrieval-guard.test.ts +2 -1
- package/src/__tests__/channel-readiness-routes.test.ts +20 -19
- package/src/__tests__/cli.test.ts +23 -0
- package/src/__tests__/credential-broker-browser-fill.test.ts +23 -22
- package/src/__tests__/credential-broker-server-use.test.ts +22 -21
- package/src/__tests__/credential-broker.test.ts +2 -1
- package/src/__tests__/credential-metadata-store.test.ts +240 -18
- package/src/__tests__/credential-resolve.test.ts +5 -4
- package/src/__tests__/credential-security-e2e.test.ts +8 -8
- package/src/__tests__/credential-security-invariants.test.ts +104 -7
- package/src/__tests__/credential-vault-unit.test.ts +22 -20
- package/src/__tests__/credential-vault.test.ts +284 -12
- package/src/__tests__/credentials-cli.test.ts +11 -6
- package/src/__tests__/gateway-only-enforcement.test.ts +4 -2
- package/src/__tests__/gemini-image-service.test.ts +75 -45
- package/src/__tests__/gemini-provider.test.ts +9 -6
- package/src/__tests__/guardian-action-conversation-turn.test.ts +1 -33
- package/src/__tests__/guardian-action-copy-generator.test.ts +0 -20
- package/src/__tests__/guardian-action-followup-executor.test.ts +1 -28
- package/src/__tests__/guardian-action-followup-store.test.ts +1 -1
- package/src/__tests__/guardian-grant-minting.test.ts +35 -0
- package/src/__tests__/integration-status.test.ts +53 -21
- package/src/__tests__/managed-proxy-context.test.ts +5 -3
- package/src/__tests__/media-generate-image.test.ts +63 -2
- package/src/__tests__/media-reuse-story.e2e.test.ts +7 -3
- package/src/__tests__/messaging-send-tool.test.ts +4 -6
- package/src/__tests__/provider-fail-open-selection.test.ts +3 -1
- package/src/__tests__/provider-managed-proxy-integration.test.ts +70 -6
- package/src/__tests__/schedule-store.test.ts +1 -1
- package/src/__tests__/schema-transforms.test.ts +226 -0
- package/src/__tests__/script-proxy-injection-runtime.test.ts +23 -13
- package/src/__tests__/script-proxy-policy-runtime.test.ts +1 -1
- package/src/__tests__/script-proxy-session-manager.test.ts +1 -1
- package/src/__tests__/secret-onetime-send.test.ts +5 -3
- package/src/__tests__/session-messaging-secret-redirect.test.ts +5 -4
- package/src/__tests__/skills-uninstall.test.ts +2 -2
- package/src/__tests__/skills.test.ts +0 -9
- package/src/__tests__/slack-channel-config.test.ts +9 -8
- package/src/__tests__/slack-share-routes.test.ts +11 -6
- package/src/__tests__/telegram-bot-username-resolution.test.ts +3 -0
- package/src/__tests__/twilio-config.test.ts +2 -1
- package/src/__tests__/twilio-provider.test.ts +4 -2
- package/src/__tests__/twilio-routes.test.ts +5 -4
- package/src/__tests__/verification-control-plane-policy.test.ts +1 -1
- package/src/approvals/AGENTS.md +1 -1
- package/src/calls/call-domain.ts +7 -4
- package/src/calls/twilio-config.ts +2 -1
- package/src/calls/twilio-provider.ts +2 -1
- package/src/calls/twilio-rest.ts +2 -2
- package/src/cli/commands/browser-relay.ts +40 -15
- package/src/cli/commands/credentials.ts +9 -8
- package/src/cli/commands/oauth.ts +1 -1
- package/src/cli.ts +3 -2
- package/src/config/bundled-skills/claude-code/TOOLS.json +0 -4
- package/src/config/bundled-skills/contacts/tools/google-contacts.ts +29 -32
- package/src/config/bundled-skills/gmail/SKILL.md +4 -4
- package/src/config/bundled-skills/gmail/tools/gmail-archive.ts +54 -61
- package/src/config/bundled-skills/gmail/tools/gmail-attachments.ts +25 -28
- package/src/config/bundled-skills/gmail/tools/gmail-draft.ts +14 -17
- package/src/config/bundled-skills/gmail/tools/gmail-filters.ts +39 -44
- package/src/config/bundled-skills/gmail/tools/gmail-follow-up.ts +61 -58
- package/src/config/bundled-skills/gmail/tools/gmail-forward.ts +50 -49
- package/src/config/bundled-skills/gmail/tools/gmail-label.ts +11 -13
- package/src/config/bundled-skills/gmail/tools/gmail-outreach-scan.ts +148 -146
- package/src/config/bundled-skills/gmail/tools/gmail-send-draft.ts +4 -7
- package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +175 -173
- package/src/config/bundled-skills/gmail/tools/gmail-trash.ts +4 -7
- package/src/config/bundled-skills/gmail/tools/gmail-unsubscribe.ts +71 -76
- package/src/config/bundled-skills/gmail/tools/gmail-vacation.ts +32 -38
- package/src/config/bundled-skills/google-calendar/SKILL.md +2 -2
- package/src/config/bundled-skills/google-calendar/calendar-client.ts +70 -29
- package/src/config/bundled-skills/google-calendar/tools/calendar-check-availability.ts +9 -10
- package/src/config/bundled-skills/google-calendar/tools/calendar-create-event.ts +5 -6
- package/src/config/bundled-skills/google-calendar/tools/calendar-get-event.ts +4 -5
- package/src/config/bundled-skills/google-calendar/tools/calendar-list-events.ts +14 -15
- package/src/config/bundled-skills/google-calendar/tools/calendar-rsvp.ts +37 -37
- package/src/config/bundled-skills/google-calendar/tools/shared.ts +4 -9
- package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +24 -3
- package/src/config/bundled-skills/messaging/SKILL.md +6 -6
- package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +62 -63
- package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +15 -16
- package/src/config/bundled-skills/messaging/tools/messaging-auth-test.ts +4 -5
- package/src/config/bundled-skills/messaging/tools/messaging-list-conversations.ts +6 -7
- package/src/config/bundled-skills/messaging/tools/messaging-mark-read.ts +4 -5
- package/src/config/bundled-skills/messaging/tools/messaging-read.ts +14 -15
- package/src/config/bundled-skills/messaging/tools/messaging-search.ts +4 -5
- package/src/config/bundled-skills/messaging/tools/messaging-send.ts +128 -128
- package/src/config/bundled-skills/messaging/tools/messaging-sender-digest.ts +33 -34
- package/src/config/bundled-skills/messaging/tools/shared.ts +11 -11
- package/src/config/bundled-skills/notifications/SKILL.md +1 -1
- package/src/config/bundled-skills/phone-calls/SKILL.md +5 -5
- package/src/config/bundled-skills/schedule/SKILL.md +1 -1
- package/src/config/bundled-skills/skill-management/SKILL.md +1 -1
- package/src/config/bundled-skills/slack/tools/shared.ts +4 -10
- package/src/config/bundled-skills/slack/tools/slack-add-reaction.ts +4 -5
- package/src/config/bundled-skills/slack/tools/slack-channel-details.ts +15 -16
- package/src/config/bundled-skills/slack/tools/slack-delete-message.ts +4 -5
- package/src/config/bundled-skills/slack/tools/slack-edit-message.ts +4 -5
- package/src/config/bundled-skills/slack/tools/slack-leave-channel.ts +4 -5
- package/src/config/bundled-skills/slack/tools/slack-scan-digest.ts +95 -92
- package/src/config/loader.ts +6 -0
- package/src/daemon/computer-use-session.ts +7 -1
- package/src/daemon/guardian-action-generators.ts +4 -5
- package/src/daemon/handlers/config-slack-channel.ts +37 -20
- package/src/daemon/handlers/config-telegram.ts +33 -20
- package/src/daemon/lifecycle.ts +9 -1
- package/src/daemon/message-types/integrations.ts +1 -0
- package/src/daemon/ride-shotgun-handler.ts +3 -1
- package/src/daemon/session-messaging.ts +3 -1
- package/src/daemon/session-tool-setup.ts +18 -2
- package/src/daemon/session.ts +1 -1
- package/src/email/providers/index.ts +2 -1
- package/src/instrument.ts +15 -1
- package/src/media/app-icon-generator.ts +30 -4
- package/src/media/avatar-router.ts +28 -62
- package/src/media/gemini-image-service.ts +28 -2
- package/src/memory/canonical-guardian-store.ts +1 -1
- package/src/memory/guardian-action-store.ts +1 -1
- package/src/memory/schema/guardian.ts +1 -1
- package/src/messaging/provider.ts +16 -10
- package/src/messaging/providers/gmail/adapter.ts +40 -23
- package/src/messaging/providers/gmail/client.ts +203 -122
- package/src/messaging/providers/gmail/people-client.ts +26 -18
- package/src/messaging/providers/slack/adapter.ts +29 -19
- package/src/messaging/providers/slack/client.ts +265 -78
- package/src/messaging/providers/telegram-bot/adapter.ts +5 -4
- package/src/messaging/providers/whatsapp/adapter.ts +6 -3
- package/src/messaging/registry.ts +2 -1
- package/src/oauth/byo-connection.test.ts +436 -0
- package/src/oauth/byo-connection.ts +112 -0
- package/src/oauth/connect-orchestrator.ts +27 -0
- package/src/oauth/connection-resolver.ts +34 -0
- package/src/oauth/connection.ts +38 -0
- package/src/oauth/platform-connection.test.ts +163 -0
- package/src/oauth/platform-connection.ts +110 -0
- package/src/oauth/provider-base-urls.ts +21 -0
- package/src/oauth/provider-profiles.ts +1 -1
- package/src/oauth/token-persistence.ts +20 -20
- package/src/permissions/checker.ts +6 -1
- package/src/prompts/system-prompt.ts +52 -15
- package/src/prompts/templates/BOOTSTRAP.md +1 -1
- package/src/providers/gemini/client.ts +15 -6
- package/src/providers/managed-proxy/constants.ts +2 -2
- package/src/providers/managed-proxy/context.ts +5 -1
- package/src/providers/ratelimit.ts +17 -0
- package/src/providers/registry.ts +2 -2
- package/src/runtime/AGENTS.md +18 -1
- package/src/runtime/auth/route-policy.ts +1 -0
- package/src/runtime/channel-invite-transports/telegram.ts +2 -1
- package/src/runtime/channel-readiness-service.ts +168 -195
- package/src/runtime/channel-readiness-types.ts +4 -0
- package/src/runtime/guardian-action-conversation-turn.ts +1 -3
- package/src/runtime/guardian-action-followup-executor.ts +1 -2
- package/src/runtime/guardian-action-message-composer.ts +3 -23
- package/src/runtime/http-server.ts +9 -4
- package/src/runtime/http-types.ts +0 -1
- package/src/runtime/middleware/rate-limiter.ts +74 -20
- package/src/runtime/middleware/twilio-validation.ts +1 -3
- package/src/runtime/routes/channel-readiness-routes.ts +2 -0
- package/src/runtime/routes/diagnostics-routes.ts +11 -9
- package/src/runtime/routes/guardian-approval-interception.ts +20 -5
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +71 -25
- package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +12 -5
- package/src/runtime/routes/integrations/slack/share.ts +3 -2
- package/src/runtime/routes/integrations/twilio.ts +6 -5
- package/src/runtime/routes/secret-routes.ts +3 -2
- package/src/runtime/routes/settings-routes.ts +75 -17
- package/src/runtime/telegram-streaming-delivery.test.ts +132 -0
- package/src/runtime/telegram-streaming-delivery.ts +11 -1
- package/src/schedule/integration-status.ts +5 -4
- package/src/security/credential-key.ts +170 -0
- package/src/security/token-manager.ts +36 -7
- package/src/tools/apps/definitions.ts +0 -5
- package/src/tools/assets/materialize.ts +0 -5
- package/src/tools/assets/search.ts +0 -5
- package/src/tools/browser/headless-browser.ts +1 -67
- package/src/tools/claude-code/claude-code.ts +0 -5
- package/src/tools/computer-use/request-computer-control.ts +0 -5
- package/src/tools/credentials/broker.ts +6 -4
- package/src/tools/credentials/metadata-store.ts +72 -20
- package/src/tools/credentials/resolve.ts +2 -1
- package/src/tools/credentials/vault.ts +77 -16
- package/src/tools/filesystem/edit.ts +1 -6
- package/src/tools/filesystem/read.ts +0 -5
- package/src/tools/filesystem/write.ts +1 -6
- package/src/tools/host-filesystem/edit.ts +1 -6
- package/src/tools/host-filesystem/read.ts +1 -6
- package/src/tools/host-filesystem/write.ts +1 -6
- package/src/tools/mcp/mcp-tool-factory.ts +18 -1
- package/src/tools/memory/definitions.ts +0 -5
- package/src/tools/network/web-fetch.ts +0 -5
- package/src/tools/network/web-search.ts +0 -5
- package/src/tools/schema-transforms.ts +99 -0
- package/src/tools/skills/load.ts +0 -5
- package/src/tools/swarm/delegate.ts +0 -5
- package/src/tools/system/avatar-generator.ts +3 -44
- package/src/tools/ui-surface/definitions.ts +0 -15
- package/src/tools/watch/screen-watch.ts +0 -5
- package/src/version.ts +10 -0
- package/src/watcher/providers/github.ts +51 -52
- package/src/watcher/providers/gmail.ts +88 -80
- package/src/watcher/providers/google-calendar.ts +93 -86
- package/src/watcher/providers/linear.ts +87 -93
- package/src/__tests__/avatar-router.test.ts +0 -149
- package/src/__tests__/managed-avatar-client.test.ts +0 -337
- package/src/config/bundled-skills/doordash/SKILL.md +0 -170
- package/src/config/bundled-skills/doordash/__tests__/doordash-client.test.ts +0 -205
- package/src/config/bundled-skills/doordash/__tests__/doordash-session.test.ts +0 -74
- package/src/config/bundled-skills/doordash/doordash-cli.ts +0 -1081
- package/src/config/bundled-skills/doordash/doordash-entry.ts +0 -22
- package/src/config/bundled-skills/doordash/lib/cart-queries.ts +0 -787
- package/src/config/bundled-skills/doordash/lib/client.ts +0 -1069
- package/src/config/bundled-skills/doordash/lib/order-queries.ts +0 -85
- package/src/config/bundled-skills/doordash/lib/queries.ts +0 -28
- package/src/config/bundled-skills/doordash/lib/query-extractor.ts +0 -94
- package/src/config/bundled-skills/doordash/lib/search-queries.ts +0 -203
- package/src/config/bundled-skills/doordash/lib/session.ts +0 -96
- package/src/config/bundled-skills/doordash/lib/shared/errors.ts +0 -61
- package/src/config/bundled-skills/doordash/lib/shared/network-recorder.ts +0 -380
- package/src/config/bundled-skills/doordash/lib/shared/platform.ts +0 -55
- package/src/config/bundled-skills/doordash/lib/shared/recording-store.ts +0 -43
- package/src/config/bundled-skills/doordash/lib/shared/recording-types.ts +0 -49
- package/src/config/bundled-skills/doordash/lib/shared/truncate.ts +0 -6
- package/src/config/bundled-skills/doordash/lib/store-queries.ts +0 -246
- package/src/config/bundled-skills/doordash/lib/types.ts +0 -367
- package/src/media/avatar-types.ts +0 -53
- package/src/media/managed-avatar-client.ts +0 -225
|
@@ -74,6 +74,15 @@ export class RateLimitProvider implements Provider {
|
|
|
74
74
|
|
|
75
75
|
if (this.requestTimestamps.length >= limit) {
|
|
76
76
|
const waitSec = Math.ceil((oldestInWindow + 60_000 - now) / 1000);
|
|
77
|
+
log.warn(
|
|
78
|
+
{
|
|
79
|
+
provider: this.name,
|
|
80
|
+
limit,
|
|
81
|
+
currentCount: this.requestTimestamps.length,
|
|
82
|
+
retryAfterSec: waitSec,
|
|
83
|
+
},
|
|
84
|
+
`Provider rate limit exceeded: ${limit} requests/minute for ${this.name}`,
|
|
85
|
+
);
|
|
77
86
|
throw new RateLimitError(
|
|
78
87
|
`Rate limit exceeded: ${limit} requests/minute. Try again in ${waitSec}s.`,
|
|
79
88
|
);
|
|
@@ -85,6 +94,14 @@ export class RateLimitProvider implements Provider {
|
|
|
85
94
|
if (limit <= 0) return;
|
|
86
95
|
|
|
87
96
|
if (this.sessionTokens >= limit) {
|
|
97
|
+
log.warn(
|
|
98
|
+
{
|
|
99
|
+
provider: this.name,
|
|
100
|
+
sessionTokens: this.sessionTokens,
|
|
101
|
+
limit,
|
|
102
|
+
},
|
|
103
|
+
`Session token budget exhausted for ${this.name}: ${this.sessionTokens.toLocaleString()}/${limit.toLocaleString()}`,
|
|
104
|
+
);
|
|
88
105
|
throw new RateLimitError(
|
|
89
106
|
`Session token budget exhausted: ${this.sessionTokens.toLocaleString()}/${limit.toLocaleString()} tokens used. Start a new session to continue.`,
|
|
90
107
|
);
|
|
@@ -289,8 +289,8 @@ export function initializeProviders(config: ProvidersConfig): void {
|
|
|
289
289
|
);
|
|
290
290
|
routingSources.set("gemini", "user-key");
|
|
291
291
|
} else {
|
|
292
|
-
// No user Gemini key —
|
|
293
|
-
const managedBaseUrl = buildManagedBaseUrl("
|
|
292
|
+
// No user Gemini key — route through Vertex managed proxy
|
|
293
|
+
const managedBaseUrl = buildManagedBaseUrl("vertex");
|
|
294
294
|
if (managedBaseUrl) {
|
|
295
295
|
const ctx = resolveManagedProxyContext();
|
|
296
296
|
const model = resolveModel(config, "gemini");
|
package/src/runtime/AGENTS.md
CHANGED
|
@@ -43,7 +43,7 @@ Host file allows the assistant to perform file operations (read, write, edit) on
|
|
|
43
43
|
- `POST /v1/host-file-result` — `{ requestId, content, isError }`
|
|
44
44
|
- **Tracking**: Uses the same `pending-interactions` tracker as approvals and host bash, with `kind: "host_file"`. The endpoint validates the interaction kind before resolving.
|
|
45
45
|
|
|
46
|
-
### Channel approvals (Telegram,
|
|
46
|
+
### Channel approvals (Telegram, Slack)
|
|
47
47
|
|
|
48
48
|
Channel approval flows use `requestId` (not `runId`) as the primary identifier:
|
|
49
49
|
|
|
@@ -51,6 +51,23 @@ Channel approval flows use `requestId` (not `runId`) as the primary identifier:
|
|
|
51
51
|
- Guardian approval records in `channelGuardianApprovalRequests` link via `requestId`.
|
|
52
52
|
- The conversational approval engine classifies user intent and resolves via `session.handleConfirmationResponse(requestId, decision)`.
|
|
53
53
|
|
|
54
|
+
## Rate Limiting & Diagnostics
|
|
55
|
+
|
|
56
|
+
All `/v1/*` endpoints share a per-client-IP sliding-window rate limiter (`middleware/rate-limiter.ts`):
|
|
57
|
+
|
|
58
|
+
- **Authenticated**: 300 requests/minute
|
|
59
|
+
- **Unauthenticated**: 20 requests/minute
|
|
60
|
+
|
|
61
|
+
When the limit is exceeded, the limiter returns 429 and logs a structured warning (module: `rate-limiter`) with the denied endpoint and a breakdown of which endpoints consumed the budget in the current window. This makes it easy to identify whether the cause is rapid thread switching, polling, or unexpected request volume.
|
|
62
|
+
|
|
63
|
+
Logs are written to `~/.vellum/workspace/data/logs/vellum.log` by default. If `logFile.dir` is configured, logs rotate daily as `assistant-YYYY-MM-DD.log` in that directory. To watch rate limit events in real time:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
tail -f ~/.vellum/workspace/data/logs/vellum.log | grep rate-limit
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
The provider-level rate limiter (`providers/ratelimit.ts`) also logs warnings (module: `rate-limit`) when request rate or token budget limits are enforced.
|
|
70
|
+
|
|
54
71
|
## HTTP-Only Transport
|
|
55
72
|
|
|
56
73
|
HTTP is the sole transport for client-daemon communication. The runtime HTTP server (`assistant/src/runtime/http-server.ts`) is the canonical API surface. Clients connect via HTTP for request/response operations and SSE (`GET /v1/events`) for streaming server-to-client events.
|
|
@@ -403,6 +403,7 @@ const ACTOR_ENDPOINTS: Array<{ endpoint: string; scopes: Scope[] }> = [
|
|
|
403
403
|
{ endpoint: "schedules/cancel", scopes: ["settings.write"] },
|
|
404
404
|
|
|
405
405
|
// Diagnostics
|
|
406
|
+
{ endpoint: "export", scopes: ["settings.read"] },
|
|
406
407
|
{ endpoint: "diagnostics/export", scopes: ["settings.read"] },
|
|
407
408
|
{ endpoint: "diagnostics/env-vars", scopes: ["settings.read"] },
|
|
408
409
|
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
saveRawConfig,
|
|
17
17
|
setNestedValue,
|
|
18
18
|
} from "../../config/loader.js";
|
|
19
|
+
import { credentialKey } from "../../security/credential-key.js";
|
|
19
20
|
import { getSecureKey } from "../../security/secure-keys.js";
|
|
20
21
|
import { getTelegramBotUsername } from "../../telegram/bot-username.js";
|
|
21
22
|
import { getLogger } from "../../util/logger.js";
|
|
@@ -40,7 +41,7 @@ export async function ensureTelegramBotUsernameResolved(): Promise<void> {
|
|
|
40
41
|
return; // Username already cached in config
|
|
41
42
|
}
|
|
42
43
|
|
|
43
|
-
const token = getSecureKey("
|
|
44
|
+
const token = getSecureKey(credentialKey("telegram", "bot_token"));
|
|
44
45
|
if (!token) return;
|
|
45
46
|
|
|
46
47
|
try {
|
|
@@ -3,6 +3,7 @@ import { hasTwilioCredentials } from "../calls/twilio-rest.js";
|
|
|
3
3
|
import { getChannelInvitePolicy } from "../channels/config.js";
|
|
4
4
|
import { loadRawConfig } from "../config/loader.js";
|
|
5
5
|
import { getEmailService } from "../email/service.js";
|
|
6
|
+
import { credentialKey } from "../security/credential-key.js";
|
|
6
7
|
import { getSecureKey } from "../security/secure-keys.js";
|
|
7
8
|
import { resolveWhatsAppDisplayNumber } from "./channel-invite-transports/whatsapp.js";
|
|
8
9
|
import type {
|
|
@@ -11,6 +12,7 @@ import type {
|
|
|
11
12
|
ChannelProbeContext,
|
|
12
13
|
ChannelReadinessSnapshot,
|
|
13
14
|
ReadinessCheckResult,
|
|
15
|
+
SetupStatus,
|
|
14
16
|
} from "./channel-readiness-types.js";
|
|
15
17
|
import { probeLocalGatewayHealth } from "./local-gateway-health.js";
|
|
16
18
|
|
|
@@ -31,55 +33,83 @@ function hasIngressConfigured(): boolean {
|
|
|
31
33
|
}
|
|
32
34
|
}
|
|
33
35
|
|
|
36
|
+
// ── Shared check helpers ────────────────────────────────────────────────────
|
|
37
|
+
|
|
38
|
+
/** Build a check result from a boolean condition. */
|
|
39
|
+
function check(
|
|
40
|
+
name: string,
|
|
41
|
+
passed: boolean,
|
|
42
|
+
passMessage: string,
|
|
43
|
+
failMessage: string,
|
|
44
|
+
): ReadinessCheckResult {
|
|
45
|
+
return { name, passed, message: passed ? passMessage : failMessage };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Check that a secure credential key exists. */
|
|
49
|
+
function checkCredential(
|
|
50
|
+
name: string,
|
|
51
|
+
service: string,
|
|
52
|
+
field: string,
|
|
53
|
+
label: string,
|
|
54
|
+
): ReadinessCheckResult {
|
|
55
|
+
const exists = !!getSecureKey(credentialKey(service, field));
|
|
56
|
+
return check(
|
|
57
|
+
name,
|
|
58
|
+
exists,
|
|
59
|
+
`${label} is configured`,
|
|
60
|
+
`${label} is not configured`,
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Check that public ingress is configured and enabled. */
|
|
65
|
+
function checkIngress(): ReadinessCheckResult {
|
|
66
|
+
const has = hasIngressConfigured();
|
|
67
|
+
return check(
|
|
68
|
+
"ingress",
|
|
69
|
+
has,
|
|
70
|
+
"Public ingress URL is configured",
|
|
71
|
+
"Public ingress URL is not configured or disabled",
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
34
75
|
// ── Voice Probe ─────────────────────────────────────────────────────────────
|
|
35
76
|
|
|
36
77
|
const voiceProbe: ChannelProbe = {
|
|
37
78
|
channel: "phone",
|
|
38
79
|
async runLocalChecks(): Promise<ReadinessCheckResult[]> {
|
|
39
|
-
const results: ReadinessCheckResult[] = [];
|
|
40
|
-
|
|
41
80
|
const hasCreds = hasTwilioCredentials();
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
results.push({
|
|
72
|
-
name: "gateway_health",
|
|
73
|
-
passed: gatewayHealth.healthy,
|
|
74
|
-
message: gatewayHealth.healthy
|
|
75
|
-
? `Local gateway is serving requests at ${gatewayHealth.target}`
|
|
76
|
-
: `Local gateway is not serving requests at ${gatewayHealth.target}${
|
|
77
|
-
gatewayHealth.error ? `: ${gatewayHealth.error}` : ""
|
|
78
|
-
}`,
|
|
79
|
-
});
|
|
81
|
+
const hasPhone = !!resolveTwilioPhoneNumber();
|
|
82
|
+
const ingress = checkIngress();
|
|
83
|
+
|
|
84
|
+
const checks: ReadinessCheckResult[] = [
|
|
85
|
+
check(
|
|
86
|
+
"twilio_credentials",
|
|
87
|
+
hasCreds,
|
|
88
|
+
"Twilio credentials are configured",
|
|
89
|
+
"Twilio Account SID and Auth Token are not configured",
|
|
90
|
+
),
|
|
91
|
+
check(
|
|
92
|
+
"phone_number",
|
|
93
|
+
hasPhone,
|
|
94
|
+
"Phone number is assigned for voice calls",
|
|
95
|
+
"No phone number assigned for voice calls",
|
|
96
|
+
),
|
|
97
|
+
ingress,
|
|
98
|
+
];
|
|
99
|
+
|
|
100
|
+
if (ingress.passed) {
|
|
101
|
+
const gw = await probeLocalGatewayHealth();
|
|
102
|
+
checks.push(
|
|
103
|
+
check(
|
|
104
|
+
"gateway_health",
|
|
105
|
+
gw.healthy,
|
|
106
|
+
`Local gateway is serving requests at ${gw.target}`,
|
|
107
|
+
`Local gateway is not serving requests at ${gw.target}${gw.error ? `: ${gw.error}` : ""}`,
|
|
108
|
+
),
|
|
109
|
+
);
|
|
80
110
|
}
|
|
81
111
|
|
|
82
|
-
return
|
|
112
|
+
return checks;
|
|
83
113
|
},
|
|
84
114
|
};
|
|
85
115
|
|
|
@@ -87,41 +117,16 @@ const voiceProbe: ChannelProbe = {
|
|
|
87
117
|
|
|
88
118
|
const telegramProbe: ChannelProbe = {
|
|
89
119
|
channel: "telegram",
|
|
90
|
-
runLocalChecks()
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
const hasWebhookSecret = !!getSecureKey(
|
|
103
|
-
"credential:telegram:webhook_secret",
|
|
104
|
-
);
|
|
105
|
-
results.push({
|
|
106
|
-
name: "webhook_secret",
|
|
107
|
-
passed: hasWebhookSecret,
|
|
108
|
-
message: hasWebhookSecret
|
|
109
|
-
? "Telegram webhook secret is configured"
|
|
110
|
-
: "Telegram webhook secret is not configured",
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
const hasIngress = hasIngressConfigured();
|
|
114
|
-
results.push({
|
|
115
|
-
name: "ingress",
|
|
116
|
-
passed: hasIngress,
|
|
117
|
-
message: hasIngress
|
|
118
|
-
? "Public ingress URL is configured"
|
|
119
|
-
: "Public ingress URL is not configured or disabled",
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
return results;
|
|
123
|
-
},
|
|
124
|
-
// Telegram has no remote checks currently
|
|
120
|
+
runLocalChecks: () => [
|
|
121
|
+
checkCredential("bot_token", "telegram", "bot_token", "Telegram bot token"),
|
|
122
|
+
checkCredential(
|
|
123
|
+
"webhook_secret",
|
|
124
|
+
"telegram",
|
|
125
|
+
"webhook_secret",
|
|
126
|
+
"Telegram webhook secret",
|
|
127
|
+
),
|
|
128
|
+
checkIngress(),
|
|
129
|
+
],
|
|
125
130
|
};
|
|
126
131
|
|
|
127
132
|
// ── Email Probe ─────────────────────────────────────────────────────────────
|
|
@@ -129,43 +134,33 @@ const telegramProbe: ChannelProbe = {
|
|
|
129
134
|
const emailProbe: ChannelProbe = {
|
|
130
135
|
channel: "email",
|
|
131
136
|
runLocalChecks(): ReadinessCheckResult[] {
|
|
132
|
-
const results: ReadinessCheckResult[] = [];
|
|
133
|
-
|
|
134
137
|
const hasApiKey = !!(
|
|
135
|
-
getSecureKey("agentmail") ||
|
|
138
|
+
getSecureKey("agentmail") ||
|
|
139
|
+
getSecureKey(credentialKey("agentmail", "api_key"))
|
|
136
140
|
);
|
|
137
|
-
results.push({
|
|
138
|
-
name: "agentmail_api_key",
|
|
139
|
-
passed: hasApiKey,
|
|
140
|
-
message: hasApiKey
|
|
141
|
-
? "AgentMail API key is configured"
|
|
142
|
-
: "AgentMail API key is not configured",
|
|
143
|
-
});
|
|
144
|
-
|
|
145
141
|
const invitePolicy = getChannelInvitePolicy("email");
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
return results;
|
|
142
|
+
return [
|
|
143
|
+
check(
|
|
144
|
+
"agentmail_api_key",
|
|
145
|
+
hasApiKey,
|
|
146
|
+
"AgentMail API key is configured",
|
|
147
|
+
"AgentMail API key is not configured",
|
|
148
|
+
),
|
|
149
|
+
check(
|
|
150
|
+
"invite_policy",
|
|
151
|
+
invitePolicy.codeRedemptionEnabled,
|
|
152
|
+
"Email invite code redemption is enabled",
|
|
153
|
+
"Email invite code redemption is disabled",
|
|
154
|
+
),
|
|
155
|
+
checkIngress(),
|
|
156
|
+
];
|
|
164
157
|
},
|
|
158
|
+
// runRemoteChecks UNCHANGED — keep the existing inbox_configured check as-is
|
|
165
159
|
async runRemoteChecks(): Promise<ReadinessCheckResult[]> {
|
|
166
160
|
// Only worth checking if the API key is present
|
|
167
161
|
const hasApiKey = !!(
|
|
168
|
-
getSecureKey("agentmail") ||
|
|
162
|
+
getSecureKey("agentmail") ||
|
|
163
|
+
getSecureKey(credentialKey("agentmail", "api_key"))
|
|
169
164
|
);
|
|
170
165
|
if (!hasApiKey) return [];
|
|
171
166
|
|
|
@@ -199,77 +194,47 @@ const emailProbe: ChannelProbe = {
|
|
|
199
194
|
const whatsappProbe: ChannelProbe = {
|
|
200
195
|
channel: "whatsapp",
|
|
201
196
|
runLocalChecks(): ReadinessCheckResult[] {
|
|
202
|
-
const results: ReadinessCheckResult[] = [];
|
|
203
|
-
|
|
204
|
-
const hasPhoneNumberId = !!getSecureKey(
|
|
205
|
-
"credential:whatsapp:phone_number_id",
|
|
206
|
-
);
|
|
207
|
-
results.push({
|
|
208
|
-
name: "whatsapp_phone_number_id",
|
|
209
|
-
passed: hasPhoneNumberId,
|
|
210
|
-
message: hasPhoneNumberId
|
|
211
|
-
? "WhatsApp phone number ID is configured"
|
|
212
|
-
: "WhatsApp phone number ID is not configured",
|
|
213
|
-
});
|
|
214
|
-
|
|
215
|
-
const hasAccessToken = !!getSecureKey("credential:whatsapp:access_token");
|
|
216
|
-
results.push({
|
|
217
|
-
name: "whatsapp_access_token",
|
|
218
|
-
passed: hasAccessToken,
|
|
219
|
-
message: hasAccessToken
|
|
220
|
-
? "WhatsApp access token is configured"
|
|
221
|
-
: "WhatsApp access token is not configured",
|
|
222
|
-
});
|
|
223
|
-
|
|
224
|
-
const hasAppSecret = !!getSecureKey("credential:whatsapp:app_secret");
|
|
225
|
-
results.push({
|
|
226
|
-
name: "whatsapp_app_secret",
|
|
227
|
-
passed: hasAppSecret,
|
|
228
|
-
message: hasAppSecret
|
|
229
|
-
? "WhatsApp app secret is configured"
|
|
230
|
-
: "WhatsApp app secret is not configured",
|
|
231
|
-
});
|
|
232
|
-
|
|
233
|
-
const hasWebhookVerifyToken = !!getSecureKey(
|
|
234
|
-
"credential:whatsapp:webhook_verify_token",
|
|
235
|
-
);
|
|
236
|
-
results.push({
|
|
237
|
-
name: "whatsapp_webhook_verify_token",
|
|
238
|
-
passed: hasWebhookVerifyToken,
|
|
239
|
-
message: hasWebhookVerifyToken
|
|
240
|
-
? "WhatsApp webhook verify token is configured"
|
|
241
|
-
: "WhatsApp webhook verify token is not configured",
|
|
242
|
-
});
|
|
243
|
-
|
|
244
197
|
const displayNumber = resolveWhatsAppDisplayNumber();
|
|
245
|
-
const hasDisplayNumber = !!displayNumber;
|
|
246
|
-
results.push({
|
|
247
|
-
name: "whatsapp_display_phone_number",
|
|
248
|
-
passed: hasDisplayNumber,
|
|
249
|
-
message: hasDisplayNumber
|
|
250
|
-
? `WhatsApp display phone number is configured (${displayNumber})`
|
|
251
|
-
: "WhatsApp display phone number is not configured — set whatsapp.phoneNumber in workspace config",
|
|
252
|
-
});
|
|
253
|
-
|
|
254
198
|
const invitePolicy = getChannelInvitePolicy("whatsapp");
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
199
|
+
return [
|
|
200
|
+
checkCredential(
|
|
201
|
+
"whatsapp_phone_number_id",
|
|
202
|
+
"whatsapp",
|
|
203
|
+
"phone_number_id",
|
|
204
|
+
"WhatsApp phone number ID",
|
|
205
|
+
),
|
|
206
|
+
checkCredential(
|
|
207
|
+
"whatsapp_access_token",
|
|
208
|
+
"whatsapp",
|
|
209
|
+
"access_token",
|
|
210
|
+
"WhatsApp access token",
|
|
211
|
+
),
|
|
212
|
+
checkCredential(
|
|
213
|
+
"whatsapp_app_secret",
|
|
214
|
+
"whatsapp",
|
|
215
|
+
"app_secret",
|
|
216
|
+
"WhatsApp app secret",
|
|
217
|
+
),
|
|
218
|
+
checkCredential(
|
|
219
|
+
"whatsapp_webhook_verify_token",
|
|
220
|
+
"whatsapp",
|
|
221
|
+
"webhook_verify_token",
|
|
222
|
+
"WhatsApp webhook verify token",
|
|
223
|
+
),
|
|
224
|
+
check(
|
|
225
|
+
"whatsapp_display_phone_number",
|
|
226
|
+
!!displayNumber,
|
|
227
|
+
`WhatsApp display phone number is configured (${displayNumber})`,
|
|
228
|
+
"WhatsApp display phone number is not configured — set whatsapp.phoneNumber in workspace config",
|
|
229
|
+
),
|
|
230
|
+
check(
|
|
231
|
+
"invite_policy",
|
|
232
|
+
invitePolicy.codeRedemptionEnabled,
|
|
233
|
+
"WhatsApp invite code redemption is enabled",
|
|
234
|
+
"WhatsApp invite code redemption is disabled",
|
|
235
|
+
),
|
|
236
|
+
checkIngress(),
|
|
237
|
+
];
|
|
273
238
|
},
|
|
274
239
|
};
|
|
275
240
|
|
|
@@ -277,26 +242,20 @@ const whatsappProbe: ChannelProbe = {
|
|
|
277
242
|
|
|
278
243
|
const slackProbe: ChannelProbe = {
|
|
279
244
|
channel: "slack",
|
|
280
|
-
runLocalChecks()
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
message: hasAppToken
|
|
295
|
-
? "Slack app token is configured"
|
|
296
|
-
: "Slack app token is not configured",
|
|
297
|
-
},
|
|
298
|
-
];
|
|
299
|
-
},
|
|
245
|
+
runLocalChecks: () => [
|
|
246
|
+
checkCredential(
|
|
247
|
+
"bot_token",
|
|
248
|
+
"slack_channel",
|
|
249
|
+
"bot_token",
|
|
250
|
+
"Slack bot token",
|
|
251
|
+
),
|
|
252
|
+
checkCredential(
|
|
253
|
+
"app_token",
|
|
254
|
+
"slack_channel",
|
|
255
|
+
"app_token",
|
|
256
|
+
"Slack app token",
|
|
257
|
+
),
|
|
258
|
+
],
|
|
300
259
|
};
|
|
301
260
|
|
|
302
261
|
// ── Service ─────────────────────────────────────────────────────────────────
|
|
@@ -369,6 +328,18 @@ export class ChannelReadinessService {
|
|
|
369
328
|
: true;
|
|
370
329
|
const ready = allLocalPassed && allRemotePassed;
|
|
371
330
|
|
|
331
|
+
// setupStatus: considers all checks (credentials + infrastructure)
|
|
332
|
+
const consideredChecks = [
|
|
333
|
+
...localChecks,
|
|
334
|
+
...(remoteChecks && remoteChecksAffectReadiness ? remoteChecks : []),
|
|
335
|
+
];
|
|
336
|
+
const anyCheckPassed = consideredChecks.some((c) => c.passed);
|
|
337
|
+
const setupStatus: SetupStatus = !anyCheckPassed
|
|
338
|
+
? "not_configured"
|
|
339
|
+
: ready
|
|
340
|
+
? "ready"
|
|
341
|
+
: "incomplete";
|
|
342
|
+
|
|
372
343
|
const reasons: Array<{ code: string; text: string }> = [];
|
|
373
344
|
for (const check of localChecks) {
|
|
374
345
|
if (!check.passed) {
|
|
@@ -386,6 +357,7 @@ export class ChannelReadinessService {
|
|
|
386
357
|
const snapshot: ChannelReadinessSnapshot = {
|
|
387
358
|
channel: ch,
|
|
388
359
|
ready,
|
|
360
|
+
setupStatus,
|
|
389
361
|
checkedAt:
|
|
390
362
|
remoteChecks && cached && !remoteChecksFreshlyFetched
|
|
391
363
|
? cached.checkedAt
|
|
@@ -422,6 +394,7 @@ export class ChannelReadinessService {
|
|
|
422
394
|
return {
|
|
423
395
|
channel,
|
|
424
396
|
ready: false,
|
|
397
|
+
setupStatus: "not_configured",
|
|
425
398
|
checkedAt: Date.now(),
|
|
426
399
|
stale: false,
|
|
427
400
|
reasons: [
|
|
@@ -4,6 +4,9 @@ import type { ChannelId } from "../channels/types.js";
|
|
|
4
4
|
|
|
5
5
|
export type { ChannelId };
|
|
6
6
|
|
|
7
|
+
/** Setup progress for a channel: not_configured → incomplete → ready. */
|
|
8
|
+
export type SetupStatus = "not_configured" | "incomplete" | "ready";
|
|
9
|
+
|
|
7
10
|
/** Result of a single readiness check (local or remote). */
|
|
8
11
|
export interface ReadinessCheckResult {
|
|
9
12
|
name: string;
|
|
@@ -15,6 +18,7 @@ export interface ReadinessCheckResult {
|
|
|
15
18
|
export interface ChannelReadinessSnapshot {
|
|
16
19
|
channel: ChannelId;
|
|
17
20
|
ready: boolean;
|
|
21
|
+
setupStatus: SetupStatus;
|
|
18
22
|
checkedAt: number;
|
|
19
23
|
stale: boolean;
|
|
20
24
|
reasons: Array<{ code: string; text: string }>;
|
|
@@ -2,12 +2,11 @@
|
|
|
2
2
|
* Guardian follow-up conversation engine.
|
|
3
3
|
*
|
|
4
4
|
* When a guardian replies to a post-timeout follow-up prompt (e.g. "would you
|
|
5
|
-
* like to call them back
|
|
5
|
+
* like to call them back?"), this engine classifies the
|
|
6
6
|
* guardian's intent into a structured disposition and produces a natural reply.
|
|
7
7
|
*
|
|
8
8
|
* Dispositions:
|
|
9
9
|
* - call_back: Guardian wants to call the original caller back
|
|
10
|
-
* - message_back: Guardian wants to send a text/message to the caller
|
|
11
10
|
* - decline: Guardian declines to follow up ("never mind", "no thanks")
|
|
12
11
|
* - keep_pending: Intent is ambiguous — ask for clarification
|
|
13
12
|
*
|
|
@@ -37,7 +36,6 @@ const FALLBACK_RETRY_TEXT = getGuardianActionFallbackMessage({
|
|
|
37
36
|
|
|
38
37
|
const VALID_DISPOSITIONS: ReadonlySet<string> = new Set([
|
|
39
38
|
"call_back",
|
|
40
|
-
"message_back",
|
|
41
39
|
"decline",
|
|
42
40
|
"keep_pending",
|
|
43
41
|
]);
|
|
@@ -173,7 +173,7 @@ async function executeCallBack(
|
|
|
173
173
|
|
|
174
174
|
/**
|
|
175
175
|
* Execute a follow-up action after the conversation engine has classified
|
|
176
|
-
* the guardian's intent as call_back
|
|
176
|
+
* the guardian's intent as call_back and the follow-up
|
|
177
177
|
* state has been transitioned to `dispatching`.
|
|
178
178
|
*
|
|
179
179
|
* On success: finalizes the follow-up to `completed` and returns
|
|
@@ -250,7 +250,6 @@ export async function executeFollowupAction(
|
|
|
250
250
|
actionResult = await executeCallBack(request, counterparty);
|
|
251
251
|
} else {
|
|
252
252
|
// decline is already handled in M5 — should not reach the executor.
|
|
253
|
-
// message_back (SMS) is no longer supported.
|
|
254
253
|
finalizeFollowup(requestId, "failed");
|
|
255
254
|
const errorText = await composeGuardianActionMessageGenerative(
|
|
256
255
|
{
|
|
@@ -36,8 +36,6 @@ export type GuardianActionMessageScenario =
|
|
|
36
36
|
| "guardian_superseded_remap"
|
|
37
37
|
| "guardian_unknown_code"
|
|
38
38
|
| "guardian_auto_matched"
|
|
39
|
-
| "outbound_message_copy"
|
|
40
|
-
| "followup_message_sent"
|
|
41
39
|
| "followup_call_started"
|
|
42
40
|
| "followup_action_failed"
|
|
43
41
|
| "guardian_answer_delivery_failed";
|
|
@@ -183,8 +181,8 @@ export function getGuardianActionFallbackMessage(
|
|
|
183
181
|
|
|
184
182
|
case "guardian_late_answer_followup":
|
|
185
183
|
return context.callerIdentifier
|
|
186
|
-
? `${context.callerIdentifier} called earlier with a question, but I wasn't able to connect them. Would you like to call them back
|
|
187
|
-
: "Someone called earlier with a question, but I wasn't able to connect them. Would you like to call them back
|
|
184
|
+
? `${context.callerIdentifier} called earlier with a question, but I wasn't able to connect them. Would you like to call them back?`
|
|
185
|
+
: "Someone called earlier with a question, but I wasn't able to connect them. Would you like to call them back?";
|
|
188
186
|
|
|
189
187
|
case "guardian_followup_dispatching":
|
|
190
188
|
return context.followupAction
|
|
@@ -205,7 +203,7 @@ export function getGuardianActionFallbackMessage(
|
|
|
205
203
|
return "No problem. Let me know if you change your mind or need anything else.";
|
|
206
204
|
|
|
207
205
|
case "guardian_followup_clarification":
|
|
208
|
-
return "Sorry, I didn't quite catch that. Would you like to call them back
|
|
206
|
+
return "Sorry, I didn't quite catch that. Would you like to call them back or skip it for now?";
|
|
209
207
|
|
|
210
208
|
case "guardian_pending_disambiguation":
|
|
211
209
|
return listedCodes
|
|
@@ -247,24 +245,6 @@ export function getGuardianActionFallbackMessage(
|
|
|
247
245
|
? `Got it! Your answer has been applied to the current active request: "${context.questionText}"`
|
|
248
246
|
: "Got it! Your answer has been applied to the current active request on the call.";
|
|
249
247
|
|
|
250
|
-
case "outbound_message_copy":
|
|
251
|
-
// This message is sent TO the original caller relaying the guardian's answer.
|
|
252
|
-
// When lateAnswerText is available, include it — that's the whole point of message_back.
|
|
253
|
-
if (context.lateAnswerText && context.questionText) {
|
|
254
|
-
return `Hi! You asked "${context.questionText}" earlier. Here's the answer: ${context.lateAnswerText}`;
|
|
255
|
-
}
|
|
256
|
-
if (context.lateAnswerText) {
|
|
257
|
-
return `Hi! Regarding your earlier question — here's the answer: ${context.lateAnswerText}`;
|
|
258
|
-
}
|
|
259
|
-
return context.questionText
|
|
260
|
-
? `Hi! You asked "${context.questionText}" earlier. We'll get back to you with an answer soon.`
|
|
261
|
-
: "Hi! Thanks for calling earlier. We'll get back to you soon.";
|
|
262
|
-
|
|
263
|
-
case "followup_message_sent":
|
|
264
|
-
return context.counterpartyPhone
|
|
265
|
-
? `Done! I've sent a text message to ${context.counterpartyPhone} with your answer.`
|
|
266
|
-
: "Done! I've sent them a text message with your answer.";
|
|
267
|
-
|
|
268
248
|
case "followup_call_started":
|
|
269
249
|
return context.counterpartyPhone
|
|
270
250
|
? `Got it! I'm calling ${context.counterpartyPhone} back now to relay your answer.`
|