From 3b374bac32fd73a670bd89215fd25f3a8bece4c0 Mon Sep 17 00:00:00 2001 From: Noa Flaherty Date: Mon, 23 Feb 2026 20:26:22 -0500 Subject: [PATCH] feat: truthful SMS send semantics and skill-driven SMS doctor Co-Authored-By: Claude --- assistant/README.md | 4 +- .../__snapshots__/ipc-snapshot.test.ts.snap | 2601 +++++++++++++++++ assistant/src/__tests__/ipc-snapshot.test.ts | 12 + .../__tests__/sms-messaging-provider.test.ts | 21 +- assistant/src/calls/twilio-rest.ts | 32 + .../messaging/tools/messaging-send.ts | 3 + assistant/src/daemon/handlers/config.ts | 274 ++ assistant/src/daemon/ipc-contract.ts | 22 +- .../src/messaging/providers/sms/adapter.ts | 8 +- .../src/messaging/providers/sms/client.ts | 33 +- gateway/src/http/routes/sms-deliver.test.ts | 36 +- gateway/src/http/routes/sms-deliver.ts | 51 +- 12 files changed, 3082 insertions(+), 15 deletions(-) create mode 100644 assistant/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap diff --git a/assistant/README.md b/assistant/README.md index ced46406a82..9cee8454c50 100644 --- a/assistant/README.md +++ b/assistant/README.md @@ -213,8 +213,10 @@ The daemon handles `twilio_config` messages with the following actions: | `sms_update_tollfree_verification` | Updates an existing toll-free verification by SID. Requires `verificationSid`. | | `sms_delete_tollfree_verification` | Deletes a toll-free verification by SID. Includes warning about queue priority reset. | | `release_number` | Releases (deletes) a phone number from the Twilio account. Clears the number from config and secure storage. Includes warning about toll-free verification context loss. | +| `sms_send_test` | Sends a test SMS to the specified `phoneNumber` with the given `text`, polls Twilio for delivery status (up to 3 retries at 2-second intervals), and returns the result in `testResult`. Stores the last result in memory for use by `sms_doctor`. | +| `sms_doctor` | Runs a comprehensive SMS health diagnostic. Checks channel readiness, compliance/toll-free verification status, and the last `sms_send_test` result. Returns structured diagnostics in `diagnostics` with an overall `status` ("healthy", "degraded", or "unhealthy") and actionable `items`. | -Response type: `twilio_config_response` with `success`, `hasCredentials`, optional `phoneNumber`, optional `numbers` array, optional `error`, optional `warning` (for non-fatal webhook sync failures), and optional `compliance` object (for compliance status actions, containing `numberType`, `verificationSid`, `verificationStatus`, `rejectionReason`, `rejectionReasons`, `errorCode`, `editAllowed`, `editExpiration`). +Response type: `twilio_config_response` with `success`, `hasCredentials`, optional `phoneNumber`, optional `numbers` array, optional `error`, optional `warning` (for non-fatal webhook sync failures), optional `compliance` object (for compliance status actions, containing `numberType`, `verificationSid`, `verificationStatus`, `rejectionReason`, `rejectionReasons`, `errorCode`, `editAllowed`, `editExpiration`), optional `testResult` (for `sms_send_test`), and optional `diagnostics` (for `sms_doctor`). ### Ingress Webhook Reconciliation diff --git a/assistant/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap b/assistant/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap new file mode 100644 index 00000000000..03e1ce78d47 --- /dev/null +++ b/assistant/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap @@ -0,0 +1,2601 @@ +// Bun Snapshot v1, https://bun.sh/docs/test/snapshots + +exports[`IPC message snapshots ClientMessage types auth serializes to expected JSON 1`] = ` +{ + "token": "abc123def456", + "type": "auth", +} +`; + +exports[`IPC message snapshots ClientMessage types user_message serializes to expected JSON 1`] = ` +{ + "content": "Hello, assistant!", + "sessionId": "sess-001", + "type": "user_message", +} +`; + +exports[`IPC message snapshots ClientMessage types confirmation_response serializes to expected JSON 1`] = ` +{ + "decision": "allow", + "requestId": "req-001", + "selectedPattern": "bash:npm *", + "selectedScope": "/projects/my-app", + "type": "confirmation_response", +} +`; + +exports[`IPC message snapshots ClientMessage types session_list serializes to expected JSON 1`] = ` +{ + "type": "session_list", +} +`; + +exports[`IPC message snapshots ClientMessage types session_create serializes to expected JSON 1`] = ` +{ + "correlationId": "corr-001", + "threadType": "standard", + "title": "New session", + "transport": { + "channelId": "desktop", + "hints": [ + "dashboard-capable", + ], + "uxBrief": "Prefer dashboard-first onboarding.", + }, + "type": "session_create", +} +`; + +exports[`IPC message snapshots ClientMessage types session_switch serializes to expected JSON 1`] = ` +{ + "sessionId": "sess-002", + "type": "session_switch", +} +`; + +exports[`IPC message snapshots ClientMessage types ping serializes to expected JSON 1`] = ` +{ + "type": "ping", +} +`; + +exports[`IPC message snapshots ClientMessage types cancel serializes to expected JSON 1`] = ` +{ + "type": "cancel", +} +`; + +exports[`IPC message snapshots ClientMessage types delete_queued_message serializes to expected JSON 1`] = ` +{ + "requestId": "req-001", + "sessionId": "sess-001", + "type": "delete_queued_message", +} +`; + +exports[`IPC message snapshots ClientMessage types model_get serializes to expected JSON 1`] = ` +{ + "type": "model_get", +} +`; + +exports[`IPC message snapshots ClientMessage types model_set serializes to expected JSON 1`] = ` +{ + "model": "claude-opus-4-6", + "type": "model_set", +} +`; + +exports[`IPC message snapshots ClientMessage types image_gen_model_set serializes to expected JSON 1`] = ` +{ + "model": "gemini-2.5-flash-image", + "type": "image_gen_model_set", +} +`; + +exports[`IPC message snapshots ClientMessage types history_request serializes to expected JSON 1`] = ` +{ + "sessionId": "sess-001", + "type": "history_request", +} +`; + +exports[`IPC message snapshots ClientMessage types undo serializes to expected JSON 1`] = ` +{ + "sessionId": "sess-001", + "type": "undo", +} +`; + +exports[`IPC message snapshots ClientMessage types regenerate serializes to expected JSON 1`] = ` +{ + "sessionId": "sess-001", + "type": "regenerate", +} +`; + +exports[`IPC message snapshots ClientMessage types usage_request serializes to expected JSON 1`] = ` +{ + "sessionId": "sess-001", + "type": "usage_request", +} +`; + +exports[`IPC message snapshots ClientMessage types sandbox_set serializes to expected JSON 1`] = ` +{ + "enabled": true, + "type": "sandbox_set", +} +`; + +exports[`IPC message snapshots ClientMessage types cu_session_create serializes to expected JSON 1`] = ` +{ + "screenHeight": 1080, + "screenWidth": 1920, + "sessionId": "cu-sess-001", + "task": "Open Safari and search for weather", + "type": "cu_session_create", +} +`; + +exports[`IPC message snapshots ClientMessage types cu_session_abort serializes to expected JSON 1`] = ` +{ + "sessionId": "cu-sess-001", + "type": "cu_session_abort", +} +`; + +exports[`IPC message snapshots ClientMessage types cu_observation serializes to expected JSON 1`] = ` +{ + "axDiff": "+ new element", + "axTree": "...", + "captureDisplayId": 69734112, + "coordinateOrigin": "top_left", + "executionResult": "click completed", + "screenHeightPt": 1080, + "screenWidthPt": 1920, + "screenshot": "base64-screenshot-data", + "screenshotHeightPx": 720, + "screenshotWidthPx": 1280, + "secondaryWindows": "Finder, Terminal", + "sessionId": "cu-sess-001", + "type": "cu_observation", +} +`; + +exports[`IPC message snapshots ClientMessage types ride_shotgun_start serializes to expected JSON 1`] = ` +{ + "durationSeconds": 300, + "intervalSeconds": 10, + "type": "ride_shotgun_start", +} +`; + +exports[`IPC message snapshots ClientMessage types ride_shotgun_stop serializes to expected JSON 1`] = ` +{ + "type": "ride_shotgun_stop", + "watchId": "watch-001", +} +`; + +exports[`IPC message snapshots ClientMessage types watch_observation serializes to expected JSON 1`] = ` +{ + "appName": "Xcode", + "bundleIdentifier": "com.apple.dt.Xcode", + "captureIndex": 0, + "ocrText": "Screen text captured during watch", + "sessionId": "sess-001", + "timestamp": 1700000000, + "totalExpected": 10, + "type": "watch_observation", + "watchId": "watch-001", + "windowTitle": "Project.swift", +} +`; + +exports[`IPC message snapshots ClientMessage types task_submit serializes to expected JSON 1`] = ` +{ + "screenHeight": 1080, + "screenWidth": 1920, + "task": "Open Safari and search for weather", + "type": "task_submit", +} +`; + +exports[`IPC message snapshots ClientMessage types ui_surface_action serializes to expected JSON 1`] = ` +{ + "actionId": "btn-ok", + "data": { + "selectedItem": "item-1", + }, + "sessionId": "sess-001", + "surfaceId": "surface-001", + "type": "ui_surface_action", +} +`; + +exports[`IPC message snapshots ClientMessage types app_data_request serializes to expected JSON 1`] = ` +{ + "appId": "app-001", + "callId": "call-001", + "method": "query", + "surfaceId": "surface-001", + "type": "app_data_request", +} +`; + +exports[`IPC message snapshots ClientMessage types skills_list serializes to expected JSON 1`] = ` +{ + "type": "skills_list", +} +`; + +exports[`IPC message snapshots ClientMessage types skill_detail serializes to expected JSON 1`] = ` +{ + "skillId": "my-skill", + "type": "skill_detail", +} +`; + +exports[`IPC message snapshots ClientMessage types skills_enable serializes to expected JSON 1`] = ` +{ + "name": "my-skill", + "type": "skills_enable", +} +`; + +exports[`IPC message snapshots ClientMessage types skills_disable serializes to expected JSON 1`] = ` +{ + "name": "my-skill", + "type": "skills_disable", +} +`; + +exports[`IPC message snapshots ClientMessage types skills_configure serializes to expected JSON 1`] = ` +{ + "apiKey": "sk-test", + "config": { + "verbose": true, + }, + "env": { + "API_KEY": "test-key", + }, + "name": "my-skill", + "type": "skills_configure", +} +`; + +exports[`IPC message snapshots ClientMessage types skills_install serializes to expected JSON 1`] = ` +{ + "slug": "clawhub/my-skill", + "type": "skills_install", + "version": "1.0.0", +} +`; + +exports[`IPC message snapshots ClientMessage types skills_uninstall serializes to expected JSON 1`] = ` +{ + "name": "my-skill", + "type": "skills_uninstall", +} +`; + +exports[`IPC message snapshots ClientMessage types skills_update serializes to expected JSON 1`] = ` +{ + "name": "my-skill", + "type": "skills_update", +} +`; + +exports[`IPC message snapshots ClientMessage types skills_check_updates serializes to expected JSON 1`] = ` +{ + "type": "skills_check_updates", +} +`; + +exports[`IPC message snapshots ClientMessage types skills_search serializes to expected JSON 1`] = ` +{ + "query": "weather", + "type": "skills_search", +} +`; + +exports[`IPC message snapshots ClientMessage types skills_inspect serializes to expected JSON 1`] = ` +{ + "slug": "clawhub/my-skill", + "type": "skills_inspect", +} +`; + +exports[`IPC message snapshots ClientMessage types suggestion_request serializes to expected JSON 1`] = ` +{ + "requestId": "req-suggest-001", + "sessionId": "sess-001", + "type": "suggestion_request", +} +`; + +exports[`IPC message snapshots ClientMessage types add_trust_rule serializes to expected JSON 1`] = ` +{ + "allowHighRisk": true, + "decision": "allow", + "executionTarget": "host", + "pattern": "git *", + "scope": "/projects/my-app", + "toolName": "bash", + "type": "add_trust_rule", +} +`; + +exports[`IPC message snapshots ClientMessage types trust_rules_list serializes to expected JSON 1`] = ` +{ + "type": "trust_rules_list", +} +`; + +exports[`IPC message snapshots ClientMessage types remove_trust_rule serializes to expected JSON 1`] = ` +{ + "id": "rule-001", + "type": "remove_trust_rule", +} +`; + +exports[`IPC message snapshots ClientMessage types update_trust_rule serializes to expected JSON 1`] = ` +{ + "decision": "allow", + "id": "rule-001", + "pattern": "git push *", + "priority": 50, + "scope": "/projects/my-app", + "tool": "bash", + "type": "update_trust_rule", +} +`; + +exports[`IPC message snapshots ClientMessage types schedules_list serializes to expected JSON 1`] = ` +{ + "type": "schedules_list", +} +`; + +exports[`IPC message snapshots ClientMessage types schedule_toggle serializes to expected JSON 1`] = ` +{ + "enabled": false, + "id": "sched-001", + "type": "schedule_toggle", +} +`; + +exports[`IPC message snapshots ClientMessage types schedule_remove serializes to expected JSON 1`] = ` +{ + "id": "sched-001", + "type": "schedule_remove", +} +`; + +exports[`IPC message snapshots ClientMessage types reminders_list serializes to expected JSON 1`] = ` +{ + "type": "reminders_list", +} +`; + +exports[`IPC message snapshots ClientMessage types reminder_cancel serializes to expected JSON 1`] = ` +{ + "id": "rem-001", + "type": "reminder_cancel", +} +`; + +exports[`IPC message snapshots ClientMessage types bundle_app serializes to expected JSON 1`] = ` +{ + "appId": "app-001", + "type": "bundle_app", +} +`; + +exports[`IPC message snapshots ClientMessage types app_open_request serializes to expected JSON 1`] = ` +{ + "appId": "app-001", + "type": "app_open_request", +} +`; + +exports[`IPC message snapshots ClientMessage types apps_list serializes to expected JSON 1`] = ` +{ + "type": "apps_list", +} +`; + +exports[`IPC message snapshots ClientMessage types home_base_get serializes to expected JSON 1`] = ` +{ + "ensureLinked": true, + "type": "home_base_get", +} +`; + +exports[`IPC message snapshots ClientMessage types shared_apps_list serializes to expected JSON 1`] = ` +{ + "type": "shared_apps_list", +} +`; + +exports[`IPC message snapshots ClientMessage types shared_app_delete serializes to expected JSON 1`] = ` +{ + "type": "shared_app_delete", + "uuid": "abc-123-def", +} +`; + +exports[`IPC message snapshots ClientMessage types fork_shared_app serializes to expected JSON 1`] = ` +{ + "type": "fork_shared_app", + "uuid": "abc-123-def", +} +`; + +exports[`IPC message snapshots ClientMessage types open_bundle serializes to expected JSON 1`] = ` +{ + "filePath": "/tmp/My_App.vellumapp", + "type": "open_bundle", +} +`; + +exports[`IPC message snapshots ClientMessage types sign_bundle_payload_response serializes to expected JSON 1`] = ` +{ + "keyId": "abc123", + "publicKey": "dGVzdA==", + "requestId": "req-sign-001", + "signature": "dGVzdC1zaWduYXR1cmU=", + "type": "sign_bundle_payload_response", +} +`; + +exports[`IPC message snapshots ClientMessage types get_signing_identity_response serializes to expected JSON 1`] = ` +{ + "keyId": "abc123", + "publicKey": "dGVzdA==", + "requestId": "req-identity-001", + "type": "get_signing_identity_response", +} +`; + +exports[`IPC message snapshots ClientMessage types secret_response serializes to expected JSON 1`] = ` +{ + "delivery": "store", + "requestId": "req-secret-001", + "type": "secret_response", + "value": "ghp_test_token_value", +} +`; + +exports[`IPC message snapshots ClientMessage types sessions_clear serializes to expected JSON 1`] = ` +{ + "type": "sessions_clear", +} +`; + +exports[`IPC message snapshots ClientMessage types ipc_blob_probe serializes to expected JSON 1`] = ` +{ + "nonceSha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "probeId": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", + "type": "ipc_blob_probe", +} +`; + +exports[`IPC message snapshots ClientMessage types gallery_list serializes to expected JSON 1`] = ` +{ + "type": "gallery_list", +} +`; + +exports[`IPC message snapshots ClientMessage types gallery_install serializes to expected JSON 1`] = ` +{ + "galleryAppId": "gallery-focus-timer", + "type": "gallery_install", +} +`; + +exports[`IPC message snapshots ClientMessage types app_update_preview serializes to expected JSON 1`] = ` +{ + "appId": "app-001", + "preview": "base64-png-data", + "type": "app_update_preview", +} +`; + +exports[`IPC message snapshots ClientMessage types app_preview_request serializes to expected JSON 1`] = ` +{ + "appId": "app-001", + "type": "app_preview_request", +} +`; + +exports[`IPC message snapshots ClientMessage types app_history_request serializes to expected JSON 1`] = ` +{ + "appId": "app-001", + "limit": 25, + "type": "app_history_request", +} +`; + +exports[`IPC message snapshots ClientMessage types app_diff_request serializes to expected JSON 1`] = ` +{ + "appId": "app-001", + "fromCommit": "abc123def456", + "toCommit": "789abc123def", + "type": "app_diff_request", +} +`; + +exports[`IPC message snapshots ClientMessage types app_file_at_version_request serializes to expected JSON 1`] = ` +{ + "appId": "app-001", + "commitHash": "abc123def456", + "path": "index.html", + "type": "app_file_at_version_request", +} +`; + +exports[`IPC message snapshots ClientMessage types app_restore_request serializes to expected JSON 1`] = ` +{ + "appId": "app-001", + "commitHash": "abc123def456", + "type": "app_restore_request", +} +`; + +exports[`IPC message snapshots ClientMessage types share_app_cloud serializes to expected JSON 1`] = ` +{ + "appId": "app-001", + "type": "share_app_cloud", +} +`; + +exports[`IPC message snapshots ClientMessage types share_to_slack serializes to expected JSON 1`] = ` +{ + "appId": "app-001", + "type": "share_to_slack", +} +`; + +exports[`IPC message snapshots ClientMessage types slack_webhook_config serializes to expected JSON 1`] = ` +{ + "action": "get", + "type": "slack_webhook_config", +} +`; + +exports[`IPC message snapshots ClientMessage types ingress_config serializes to expected JSON 1`] = ` +{ + "action": "get", + "type": "ingress_config", +} +`; + +exports[`IPC message snapshots ClientMessage types vercel_api_config serializes to expected JSON 1`] = ` +{ + "action": "get", + "type": "vercel_api_config", +} +`; + +exports[`IPC message snapshots ClientMessage types twitter_integration_config serializes to expected JSON 1`] = ` +{ + "action": "get", + "type": "twitter_integration_config", +} +`; + +exports[`IPC message snapshots ClientMessage types telegram_config serializes to expected JSON 1`] = ` +{ + "action": "get", + "type": "telegram_config", +} +`; + +exports[`IPC message snapshots ClientMessage types twilio_config serializes to expected JSON 1`] = ` +{ + "action": "get", + "type": "twilio_config", +} +`; + +exports[`IPC message snapshots ClientMessage types channel_readiness serializes to expected JSON 1`] = ` +{ + "action": "get", + "channel": "sms", + "includeRemote": true, + "type": "channel_readiness", +} +`; + +exports[`IPC message snapshots ClientMessage types guardian_verification serializes to expected JSON 1`] = ` +{ + "action": "create_challenge", + "channel": "telegram", + "sessionId": "sess-001", + "type": "guardian_verification", +} +`; + +exports[`IPC message snapshots ClientMessage types twitter_auth_start serializes to expected JSON 1`] = ` +{ + "type": "twitter_auth_start", +} +`; + +exports[`IPC message snapshots ClientMessage types twitter_auth_status serializes to expected JSON 1`] = ` +{ + "type": "twitter_auth_status", +} +`; + +exports[`IPC message snapshots ClientMessage types link_open_request serializes to expected JSON 1`] = ` +{ + "type": "link_open_request", + "url": "https://example.com", +} +`; + +exports[`IPC message snapshots ClientMessage types ui_surface_undo serializes to expected JSON 1`] = ` +{ + "sessionId": "sess-001", + "surfaceId": "surface-001", + "type": "ui_surface_undo", +} +`; + +exports[`IPC message snapshots ClientMessage types publish_page serializes to expected JSON 1`] = ` +{ + "html": "Hello", + "type": "publish_page", +} +`; + +exports[`IPC message snapshots ClientMessage types unpublish_page serializes to expected JSON 1`] = ` +{ + "deploymentId": "dpl-001", + "type": "unpublish_page", +} +`; + +exports[`IPC message snapshots ClientMessage types diagnostics_export_request serializes to expected JSON 1`] = ` +{ + "anchorMessageId": "msg-042", + "conversationId": "conv-001", + "type": "diagnostics_export_request", +} +`; + +exports[`IPC message snapshots ClientMessage types accept_starter_bundle serializes to expected JSON 1`] = ` +{ + "type": "accept_starter_bundle", +} +`; + +exports[`IPC message snapshots ClientMessage types env_vars_request serializes to expected JSON 1`] = ` +{ + "type": "env_vars_request", +} +`; + +exports[`IPC message snapshots ClientMessage types integration_list serializes to expected JSON 1`] = ` +{ + "type": "integration_list", +} +`; + +exports[`IPC message snapshots ClientMessage types integration_connect serializes to expected JSON 1`] = ` +{ + "integrationId": "gmail", + "type": "integration_connect", +} +`; + +exports[`IPC message snapshots ClientMessage types integration_disconnect serializes to expected JSON 1`] = ` +{ + "integrationId": "gmail", + "type": "integration_disconnect", +} +`; + +exports[`IPC message snapshots ClientMessage types browser_cdp_response serializes to expected JSON 1`] = ` +{ + "sessionId": "test-session", + "success": true, + "type": "browser_cdp_response", +} +`; + +exports[`IPC message snapshots ClientMessage types browser_user_click serializes to expected JSON 1`] = ` +{ + "sessionId": "test-session", + "surfaceId": "test-surface", + "type": "browser_user_click", + "x": 100, + "y": 200, +} +`; + +exports[`IPC message snapshots ClientMessage types browser_user_scroll serializes to expected JSON 1`] = ` +{ + "deltaX": 0, + "deltaY": -100, + "sessionId": "test-session", + "surfaceId": "test-surface", + "type": "browser_user_scroll", + "x": 100, + "y": 200, +} +`; + +exports[`IPC message snapshots ClientMessage types browser_user_keypress serializes to expected JSON 1`] = ` +{ + "key": "Enter", + "sessionId": "test-session", + "surfaceId": "test-surface", + "type": "browser_user_keypress", +} +`; + +exports[`IPC message snapshots ClientMessage types browser_interactive_mode serializes to expected JSON 1`] = ` +{ + "enabled": true, + "sessionId": "test-session", + "surfaceId": "test-surface", + "type": "browser_interactive_mode", +} +`; + +exports[`IPC message snapshots ClientMessage types work_items_list serializes to expected JSON 1`] = ` +{ + "status": "queued", + "type": "work_items_list", +} +`; + +exports[`IPC message snapshots ClientMessage types work_item_get serializes to expected JSON 1`] = ` +{ + "id": "wi-001", + "type": "work_item_get", +} +`; + +exports[`IPC message snapshots ClientMessage types work_item_update serializes to expected JSON 1`] = ` +{ + "id": "wi-001", + "status": "running", + "title": "Updated title", + "type": "work_item_update", +} +`; + +exports[`IPC message snapshots ClientMessage types work_item_complete serializes to expected JSON 1`] = ` +{ + "id": "wi-001", + "type": "work_item_complete", +} +`; + +exports[`IPC message snapshots ClientMessage types work_item_delete serializes to expected JSON 1`] = ` +{ + "id": "wi-001", + "type": "work_item_delete", +} +`; + +exports[`IPC message snapshots ClientMessage types work_item_run_task serializes to expected JSON 1`] = ` +{ + "id": "wi-001", + "type": "work_item_run_task", +} +`; + +exports[`IPC message snapshots ClientMessage types work_item_output serializes to expected JSON 1`] = ` +{ + "id": "wi-001", + "type": "work_item_output", +} +`; + +exports[`IPC message snapshots ClientMessage types work_item_preflight serializes to expected JSON 1`] = ` +{ + "id": "wi-001", + "type": "work_item_preflight", +} +`; + +exports[`IPC message snapshots ClientMessage types work_item_approve_permissions serializes to expected JSON 1`] = ` +{ + "approvedTools": [ + "bash", + "file_write", + ], + "id": "wi-001", + "type": "work_item_approve_permissions", +} +`; + +exports[`IPC message snapshots ClientMessage types work_item_cancel serializes to expected JSON 1`] = ` +{ + "id": "wi-001", + "type": "work_item_cancel", +} +`; + +exports[`IPC message snapshots ClientMessage types document_save serializes to expected JSON 1`] = ` +{ + "content": "# Hello", + "conversationId": "conv-001", + "surfaceId": "doc-001", + "title": "My Document", + "type": "document_save", + "wordCount": 1, +} +`; + +exports[`IPC message snapshots ClientMessage types document_load serializes to expected JSON 1`] = ` +{ + "surfaceId": "doc-001", + "type": "document_load", +} +`; + +exports[`IPC message snapshots ClientMessage types document_list serializes to expected JSON 1`] = ` +{ + "conversationId": "conv-001", + "type": "document_list", +} +`; + +exports[`IPC message snapshots ClientMessage types subagent_abort serializes to expected JSON 1`] = ` +{ + "subagentId": "sub-001", + "type": "subagent_abort", +} +`; + +exports[`IPC message snapshots ClientMessage types subagent_status serializes to expected JSON 1`] = ` +{ + "subagentId": "sub-001", + "type": "subagent_status", +} +`; + +exports[`IPC message snapshots ClientMessage types subagent_message serializes to expected JSON 1`] = ` +{ + "content": "Hello subagent", + "subagentId": "sub-001", + "type": "subagent_message", +} +`; + +exports[`IPC message snapshots ClientMessage types subagent_detail_request serializes to expected JSON 1`] = ` +{ + "conversationId": "conv-001", + "subagentId": "sub-001", + "type": "subagent_detail_request", +} +`; + +exports[`IPC message snapshots ClientMessage types workspace_files_list serializes to expected JSON 1`] = ` +{ + "type": "workspace_files_list", +} +`; + +exports[`IPC message snapshots ClientMessage types workspace_file_read serializes to expected JSON 1`] = ` +{ + "path": "IDENTITY.md", + "type": "workspace_file_read", +} +`; + +exports[`IPC message snapshots ClientMessage types identity_get serializes to expected JSON 1`] = ` +{ + "type": "identity_get", +} +`; + +exports[`IPC message snapshots ClientMessage types tool_permission_simulate serializes to expected JSON 1`] = ` +{ + "forcePromptSideEffects": false, + "input": { + "command": "rm -rf /tmp/test", + }, + "isInteractive": true, + "toolName": "bash", + "type": "tool_permission_simulate", + "workingDir": "/projects/my-app", +} +`; + +exports[`IPC message snapshots ClientMessage types tool_names_list serializes to expected JSON 1`] = ` +{ + "type": "tool_names_list", +} +`; + +exports[`IPC message snapshots ClientMessage types dictation_request serializes to expected JSON 1`] = ` +{ + "context": { + "appName": "Example App", + "bundleIdentifier": "com.example.app", + "cursorInTextField": true, + "selectedText": "some selected text", + "windowTitle": "Main Window", + }, + "transcription": "Hello world", + "type": "dictation_request", +} +`; + +exports[`IPC message snapshots ServerMessage types auth_result serializes to expected JSON 1`] = ` +{ + "success": true, + "type": "auth_result", +} +`; + +exports[`IPC message snapshots ServerMessage types user_message_echo serializes to expected JSON 1`] = ` +{ + "sessionId": "sess-001", + "text": "Check the weather for me", + "type": "user_message_echo", +} +`; + +exports[`IPC message snapshots ServerMessage types assistant_text_delta serializes to expected JSON 1`] = ` +{ + "sessionId": "sess-001", + "text": "Here is some output", + "type": "assistant_text_delta", +} +`; + +exports[`IPC message snapshots ServerMessage types assistant_thinking_delta serializes to expected JSON 1`] = ` +{ + "thinking": "Let me consider this...", + "type": "assistant_thinking_delta", +} +`; + +exports[`IPC message snapshots ServerMessage types tool_use_start serializes to expected JSON 1`] = ` +{ + "input": { + "command": "ls -la", + }, + "toolName": "bash", + "type": "tool_use_start", +} +`; + +exports[`IPC message snapshots ServerMessage types tool_output_chunk serializes to expected JSON 1`] = ` +{ + "chunk": +"file1.ts +file2.ts +" +, + "type": "tool_output_chunk", +} +`; + +exports[`IPC message snapshots ServerMessage types tool_input_delta serializes to expected JSON 1`] = ` +{ + "content": "{"html": "
Hello
"}", + "sessionId": "sess-001", + "toolName": "app_create", + "type": "tool_input_delta", +} +`; + +exports[`IPC message snapshots ServerMessage types tool_result serializes to expected JSON 1`] = ` +{ + "diff": { + "filePath": "/tmp/test.ts", + "isNewFile": false, + "newContent": "const x = 2;", + "oldContent": "const x = 1;", + }, + "isError": false, + "result": "Command completed successfully", + "status": "success", + "toolName": "bash", + "type": "tool_result", +} +`; + +exports[`IPC message snapshots ServerMessage types secret_request serializes to expected JSON 1`] = ` +{ + "allowOneTimeSend": false, + "allowedDomains": [ + "github.com", + ], + "allowedTools": [ + "browser_fill_credential", + ], + "description": "Needed to push changes", + "field": "token", + "label": "GitHub Personal Access Token", + "placeholder": "ghp_xxxxxxxxxxxx", + "purpose": "Push code changes to GitHub", + "requestId": "req-secret-001", + "service": "github", + "sessionId": "sess-001", + "type": "secret_request", +} +`; + +exports[`IPC message snapshots ServerMessage types confirmation_request serializes to expected JSON 1`] = ` +{ + "allowlistOptions": [ + { + "description": "Allow rm commands", + "label": "Allow rm commands", + "pattern": "bash:rm *", + }, + ], + "diff": { + "filePath": "/tmp/test.ts", + "isNewFile": false, + "newContent": "new", + "oldContent": "old", + }, + "executionTarget": "sandbox", + "input": { + "command": "rm -rf /tmp/test", + }, + "requestId": "req-002", + "riskLevel": "high", + "sandboxed": false, + "scopeOptions": [ + { + "label": "In /tmp", + "scope": "/tmp", + }, + ], + "sessionId": "sess-001", + "toolName": "bash", + "type": "confirmation_request", +} +`; + +exports[`IPC message snapshots ServerMessage types message_complete serializes to expected JSON 1`] = ` +{ + "attachments": [ + { + "data": "iVBORw0K", + "filename": "chart.png", + "mimeType": "image/png", + }, + ], + "sessionId": "sess-001", + "type": "message_complete", +} +`; + +exports[`IPC message snapshots ServerMessage types session_info serializes to expected JSON 1`] = ` +{ + "correlationId": "corr-001", + "sessionId": "sess-001", + "threadType": "standard", + "title": "My session", + "type": "session_info", +} +`; + +exports[`IPC message snapshots ServerMessage types session_list_response serializes to expected JSON 1`] = ` +{ + "sessions": [ + { + "id": "sess-001", + "threadType": "standard", + "title": "First session", + "updatedAt": 1700000000, + }, + { + "id": "sess-002", + "threadType": "standard", + "title": "Second session", + "updatedAt": 1700001000, + }, + ], + "type": "session_list_response", +} +`; + +exports[`IPC message snapshots ServerMessage types sessions_clear_response serializes to expected JSON 1`] = ` +{ + "cleared": 3, + "type": "sessions_clear_response", +} +`; + +exports[`IPC message snapshots ServerMessage types error serializes to expected JSON 1`] = ` +{ + "message": "Something went wrong", + "type": "error", +} +`; + +exports[`IPC message snapshots ServerMessage types pong serializes to expected JSON 1`] = ` +{ + "type": "pong", +} +`; + +exports[`IPC message snapshots ServerMessage types daemon_status serializes to expected JSON 1`] = ` +{ + "httpPort": 7821, + "type": "daemon_status", +} +`; + +exports[`IPC message snapshots ServerMessage types generation_cancelled serializes to expected JSON 1`] = ` +{ + "type": "generation_cancelled", +} +`; + +exports[`IPC message snapshots ServerMessage types generation_handoff serializes to expected JSON 1`] = ` +{ + "attachments": [ + { + "data": "JVBER", + "filename": "report.pdf", + "mimeType": "application/pdf", + }, + ], + "queuedCount": 2, + "requestId": "req-handoff-001", + "sessionId": "sess-001", + "type": "generation_handoff", +} +`; + +exports[`IPC message snapshots ServerMessage types model_info serializes to expected JSON 1`] = ` +{ + "model": "claude-opus-4-6", + "provider": "anthropic", + "type": "model_info", +} +`; + +exports[`IPC message snapshots ServerMessage types history_response serializes to expected JSON 1`] = ` +{ + "messages": [ + { + "role": "user", + "text": "Hello", + "timestamp": 1700000000, + }, + { + "attachments": [ + { + "data": "iVBORw0K", + "filename": "result.png", + "mimeType": "image/png", + }, + ], + "role": "assistant", + "text": "Hi there!", + "timestamp": 1700000001, + }, + ], + "sessionId": "sess-history-001", + "type": "history_response", +} +`; + +exports[`IPC message snapshots ServerMessage types undo_complete serializes to expected JSON 1`] = ` +{ + "removedCount": 2, + "sessionId": "session-abc", + "type": "undo_complete", +} +`; + +exports[`IPC message snapshots ServerMessage types usage_update serializes to expected JSON 1`] = ` +{ + "estimatedCost": 0.025, + "inputTokens": 150, + "model": "claude-opus-4-6", + "outputTokens": 50, + "totalInputTokens": 1500, + "totalOutputTokens": 500, + "type": "usage_update", +} +`; + +exports[`IPC message snapshots ServerMessage types usage_response serializes to expected JSON 1`] = ` +{ + "estimatedCost": 0.025, + "model": "claude-opus-4-6", + "totalInputTokens": 1500, + "totalOutputTokens": 500, + "type": "usage_response", +} +`; + +exports[`IPC message snapshots ServerMessage types context_compacted serializes to expected JSON 1`] = ` +{ + "compactedMessages": 56, + "estimatedInputTokens": 108000, + "maxInputTokens": 180000, + "previousEstimatedInputTokens": 220000, + "summaryCalls": 3, + "summaryInputTokens": 4200, + "summaryModel": "claude-opus-4-6", + "summaryOutputTokens": 900, + "thresholdTokens": 144000, + "type": "context_compacted", +} +`; + +exports[`IPC message snapshots ServerMessage types secret_detected serializes to expected JSON 1`] = ` +{ + "action": "redact", + "matches": [ + { + "redactedValue": "sk-****abcd", + "type": "api_key", + }, + ], + "toolName": "bash", + "type": "secret_detected", +} +`; + +exports[`IPC message snapshots ServerMessage types memory_recalled serializes to expected JSON 1`] = ` +{ + "earlyTerminated": false, + "entityHits": 3, + "injectedTokens": 480, + "latencyMs": 55, + "lexicalHits": 12, + "mergedCount": 18, + "model": "text-embedding-3-small", + "provider": "openai", + "recencyHits": 6, + "relationExpandedItemCount": 4, + "relationNeighborEntityCount": 3, + "relationSeedEntityCount": 2, + "relationTraversedEdgeCount": 5, + "rerankApplied": false, + "selectedCount": 10, + "semanticHits": 8, + "topCandidates": [ + { + "finalScore": 0.85, + "key": "segment:seg-1", + "kind": "fact", + "lexical": 0.9, + "recency": 0.3, + "semantic": 0.7, + "type": "segment", + }, + { + "finalScore": 0.72, + "key": "item:item-1", + "kind": "preference", + "lexical": 0.6, + "recency": 0.1, + "semantic": 0.8, + "type": "item", + }, + ], + "type": "memory_recalled", +} +`; + +exports[`IPC message snapshots ServerMessage types memory_status serializes to expected JSON 1`] = ` +{ + "cleanupResolvedJobsCompleted24h": 12, + "cleanupResolvedJobsPending": 1, + "cleanupSupersededJobsCompleted24h": 8, + "cleanupSupersededJobsPending": 0, + "conflictsPending": 2, + "conflictsResolved": 7, + "degraded": false, + "enabled": true, + "model": "text-embedding-3-small", + "oldestPendingConflictAgeMs": 90000, + "provider": "openai", + "type": "memory_status", +} +`; + +exports[`IPC message snapshots ServerMessage types cu_action serializes to expected JSON 1`] = ` +{ + "input": { + "x": 100, + "y": 200, + }, + "reasoning": "Clicking the search button", + "sessionId": "cu-sess-001", + "stepNumber": 1, + "toolName": "click", + "type": "cu_action", +} +`; + +exports[`IPC message snapshots ServerMessage types cu_complete serializes to expected JSON 1`] = ` +{ + "sessionId": "cu-sess-001", + "stepCount": 5, + "summary": "Successfully opened Safari and searched for weather", + "type": "cu_complete", +} +`; + +exports[`IPC message snapshots ServerMessage types cu_error serializes to expected JSON 1`] = ` +{ + "message": "Session timed out after 30 steps", + "sessionId": "cu-sess-001", + "type": "cu_error", +} +`; + +exports[`IPC message snapshots ServerMessage types task_routed serializes to expected JSON 1`] = ` +{ + "interactionType": "computer_use", + "sessionId": "sess-routed-001", + "type": "task_routed", +} +`; + +exports[`IPC message snapshots ServerMessage types ride_shotgun_progress serializes to expected JSON 1`] = ` +{ + "message": "Observing user activity...", + "type": "ride_shotgun_progress", + "watchId": "watch-shotgun-001", +} +`; + +exports[`IPC message snapshots ServerMessage types ride_shotgun_result serializes to expected JSON 1`] = ` +{ + "observationCount": 5, + "sessionId": "sess-shotgun-001", + "summary": "User was debugging a test failure", + "type": "ride_shotgun_result", + "watchId": "watch-shotgun-001", +} +`; + +exports[`IPC message snapshots ServerMessage types ui_surface_show serializes to expected JSON 1`] = ` +{ + "actions": [ + { + "id": "dismiss", + "label": "OK", + "style": "primary", + }, + ], + "data": { + "body": "All tests passed.", + "title": "Build Complete", + }, + "sessionId": "sess-001", + "surfaceId": "surface-001", + "surfaceType": "card", + "title": "Status Update", + "type": "ui_surface_show", +} +`; + +exports[`IPC message snapshots ServerMessage types ui_surface_update serializes to expected JSON 1`] = ` +{ + "data": { + "body": "Updated body text.", + }, + "sessionId": "sess-001", + "surfaceId": "surface-001", + "type": "ui_surface_update", +} +`; + +exports[`IPC message snapshots ServerMessage types ui_surface_dismiss serializes to expected JSON 1`] = ` +{ + "sessionId": "sess-001", + "surfaceId": "surface-001", + "type": "ui_surface_dismiss", +} +`; + +exports[`IPC message snapshots ServerMessage types ui_surface_complete serializes to expected JSON 1`] = ` +{ + "sessionId": "sess-001", + "summary": "Confirmed", + "surfaceId": "surface-001", + "type": "ui_surface_complete", +} +`; + +exports[`IPC message snapshots ServerMessage types app_data_response serializes to expected JSON 1`] = ` +{ + "callId": "call-001", + "result": [ + { + "appId": "app-001", + "createdAt": 1700000000, + "data": { + "name": "Test", + }, + "id": "rec-001", + "updatedAt": 1700000000, + }, + ], + "success": true, + "surfaceId": "surface-001", + "type": "app_data_response", +} +`; + +exports[`IPC message snapshots ServerMessage types skills_list_response serializes to expected JSON 1`] = ` +{ + "skills": [ + { + "degraded": false, + "description": "A test skill", + "emoji": "🔧", + "id": "my-skill", + "name": "My Skill", + "source": "bundled", + "state": "enabled", + "updateAvailable": false, + "userInvocable": true, + }, + ], + "type": "skills_list_response", +} +`; + +exports[`IPC message snapshots ServerMessage types skills_state_changed serializes to expected JSON 1`] = ` +{ + "name": "my-skill", + "state": "enabled", + "type": "skills_state_changed", +} +`; + +exports[`IPC message snapshots ServerMessage types skills_operation_response serializes to expected JSON 1`] = ` +{ + "operation": "enable", + "success": true, + "type": "skills_operation_response", +} +`; + +exports[`IPC message snapshots ServerMessage types skill_detail_response serializes to expected JSON 1`] = ` +{ + "body": +"# Skill content + +Do the thing." +, + "skillId": "my-skill", + "type": "skill_detail_response", +} +`; + +exports[`IPC message snapshots ServerMessage types skills_inspect_response serializes to expected JSON 1`] = ` +{ + "data": { + "createdAt": 1700000000, + "files": [ + { + "path": "SKILL.md", + "size": 1024, + }, + ], + "latestVersion": { + "changelog": "Bug fixes", + "version": "1.2.0", + }, + "owner": { + "displayName": "ClaWHub", + "handle": "clawhub", + }, + "skill": { + "displayName": "My Skill", + "slug": "clawhub/my-skill", + "summary": "A test skill", + }, + "skillMdContent": +"# My Skill + +Does things." +, + "stats": { + "downloads": 5000, + "installs": 1000, + "stars": 42, + "versions": 3, + }, + "updatedAt": 1700001000, + }, + "slug": "clawhub/my-skill", + "type": "skills_inspect_response", +} +`; + +exports[`IPC message snapshots ServerMessage types suggestion_response serializes to expected JSON 1`] = ` +{ + "requestId": "req-suggest-001", + "source": "llm", + "suggestion": "Tell me more about that", + "type": "suggestion_response", +} +`; + +exports[`IPC message snapshots ServerMessage types message_queued serializes to expected JSON 1`] = ` +{ + "position": 1, + "requestId": "req-queue-001", + "sessionId": "sess-001", + "type": "message_queued", +} +`; + +exports[`IPC message snapshots ServerMessage types message_dequeued serializes to expected JSON 1`] = ` +{ + "requestId": "req-queue-001", + "sessionId": "sess-001", + "type": "message_dequeued", +} +`; + +exports[`IPC message snapshots ServerMessage types message_queued_deleted serializes to expected JSON 1`] = ` +{ + "requestId": "req-queue-001", + "sessionId": "sess-001", + "type": "message_queued_deleted", +} +`; + +exports[`IPC message snapshots ServerMessage types reminder_fired serializes to expected JSON 1`] = ` +{ + "label": "Call Sidd", + "message": "Remember to call Sidd about the project", + "reminderId": "rem-001", + "type": "reminder_fired", +} +`; + +exports[`IPC message snapshots ServerMessage types schedule_complete serializes to expected JSON 1`] = ` +{ + "name": "Daily backup", + "scheduleId": "sched-001", + "type": "schedule_complete", +} +`; + +exports[`IPC message snapshots ServerMessage types watcher_notification serializes to expected JSON 1`] = ` +{ + "body": "Disabled after 5 consecutive errors.", + "title": "Watcher disabled: My Gmail", + "type": "watcher_notification", +} +`; + +exports[`IPC message snapshots ServerMessage types watcher_escalation serializes to expected JSON 1`] = ` +{ + "body": "Meeting rescheduled to 3pm today.", + "title": "Urgent email from Alice", + "type": "watcher_escalation", +} +`; + +exports[`IPC message snapshots ServerMessage types agent_heartbeat_alert serializes to expected JSON 1`] = ` +{ + "body": "No activity detected in the last 60 minutes.", + "title": "Agent heartbeat stalled", + "type": "agent_heartbeat_alert", +} +`; + +exports[`IPC message snapshots ServerMessage types watch_started serializes to expected JSON 1`] = ` +{ + "durationSeconds": 300, + "intervalSeconds": 5, + "sessionId": "sess-001", + "type": "watch_started", + "watchId": "watch-001", +} +`; + +exports[`IPC message snapshots ServerMessage types watch_complete_request serializes to expected JSON 1`] = ` +{ + "sessionId": "sess-001", + "type": "watch_complete_request", + "watchId": "watch-001", +} +`; + +exports[`IPC message snapshots ServerMessage types trust_rules_list_response serializes to expected JSON 1`] = ` +{ + "rules": [ + { + "createdAt": 1700000000, + "decision": "allow", + "id": "rule-001", + "pattern": "git *", + "priority": 100, + "scope": "/projects/my-app", + "tool": "bash", + }, + ], + "type": "trust_rules_list_response", +} +`; + +exports[`IPC message snapshots ServerMessage types schedules_list_response serializes to expected JSON 1`] = ` +{ + "schedules": [ + { + "cronExpression": "0 9 * * 1-5", + "description": "Every weekday at 9:00 AM", + "enabled": true, + "expression": "0 9 * * 1-5", + "id": "sched-001", + "lastRunAt": 1700000000000, + "lastStatus": "ok", + "message": "Remind me about the standup", + "name": "Daily standup reminder", + "nextRunAt": 1700100000000, + "syntax": "cron", + "timezone": "America/Los_Angeles", + }, + ], + "type": "schedules_list_response", +} +`; + +exports[`IPC message snapshots ServerMessage types reminders_list_response serializes to expected JSON 1`] = ` +{ + "reminders": [ + { + "createdAt": 1700000000000, + "fireAt": 1700100000000, + "firedAt": null, + "id": "rem-001", + "label": "Call Sidd", + "message": "Remember to call Sidd about the project", + "mode": "notify", + "status": "pending", + }, + ], + "type": "reminders_list_response", +} +`; + +exports[`IPC message snapshots ServerMessage types bundle_app_response serializes to expected JSON 1`] = ` +{ + "bundlePath": "/tmp/My_App-abc12345.vellumapp", + "manifest": { + "capabilities": [], + "content_id": "a1b2c3d4e5f6a7b8", + "created_at": "2026-01-01T00:00:00.000Z", + "created_by": "vellum-assistant/0.1.6", + "description": "A test app", + "entry": "index.html", + "format_version": 1, + "name": "My App", + "version": "1.0.0", + }, + "type": "bundle_app_response", +} +`; + +exports[`IPC message snapshots ServerMessage types apps_list_response serializes to expected JSON 1`] = ` +{ + "apps": [ + { + "contentId": "a1b2c3d4e5f6a7b8", + "createdAt": 1700000000, + "description": "A test app", + "icon": "📱", + "id": "app-001", + "name": "My App", + "preview": "iVBORw0KGgoAAAANSUhEUg==", + "version": "1.0.0", + }, + ], + "type": "apps_list_response", +} +`; + +exports[`IPC message snapshots ServerMessage types home_base_get_response serializes to expected JSON 1`] = ` +{ + "homeBase": { + "appId": "home-base-001", + "onboardingTasks": [ + "Make it mine", + "Enable voice mode", + "Enable computer control", + "Try ambient mode", + ], + "preview": { + "description": "Prebuilt onboarding + starter task canvas", + "icon": "🏠", + "metrics": [ + { + "label": "Starter tasks", + "value": "3", + }, + { + "label": "Onboarding tasks", + "value": "4", + }, + ], + "subtitle": "Dashboard", + "title": "Home Base", + }, + "source": "prebuilt_seed", + "starterTasks": [ + "Change the look and feel", + "Research something for me about X", + "Turn it into a webpage or interactive UI", + ], + }, + "type": "home_base_get_response", +} +`; + +exports[`IPC message snapshots ServerMessage types shared_apps_list_response serializes to expected JSON 1`] = ` +{ + "apps": [ + { + "bundleSizeBytes": 4096, + "contentId": "abcdef0123456789", + "description": "A shared app", + "entry": "index.html", + "icon": "📱", + "installedAt": "2026-01-15T00:00:00Z", + "name": "Shared App", + "preview": "iVBORw0KGgoAAAANSUhEUg==", + "signerDisplayName": "Test User", + "trustTier": "signed", + "updateAvailable": true, + "uuid": "abc-123-def", + "version": "1.2.0", + }, + ], + "type": "shared_apps_list_response", +} +`; + +exports[`IPC message snapshots ServerMessage types shared_app_delete_response serializes to expected JSON 1`] = ` +{ + "success": true, + "type": "shared_app_delete_response", +} +`; + +exports[`IPC message snapshots ServerMessage types fork_shared_app_response serializes to expected JSON 1`] = ` +{ + "appId": "new-app-id", + "name": "My App (Fork)", + "success": true, + "type": "fork_shared_app_response", +} +`; + +exports[`IPC message snapshots ServerMessage types open_bundle_response serializes to expected JSON 1`] = ` +{ + "bundleSizeBytes": 4096, + "manifest": { + "capabilities": [], + "created_at": "2026-01-01T00:00:00.000Z", + "created_by": "vellum-assistant/0.1.6", + "description": "A test app", + "entry": "index.html", + "format_version": 1, + "name": "My App", + }, + "scanResult": { + "blocked": [], + "passed": true, + "warnings": [ + "Use of fetch() detected", + ], + }, + "signatureResult": { + "signerAccount": "test@example.com", + "signerDisplayName": "Test Signer", + "signerKeyId": "key-001", + "trustTier": "signed", + }, + "type": "open_bundle_response", +} +`; + +exports[`IPC message snapshots ServerMessage types sign_bundle_payload serializes to expected JSON 1`] = ` +{ + "payload": "{"content_hashes":{},"manifest":{}}", + "requestId": "req-sign-001", + "type": "sign_bundle_payload", +} +`; + +exports[`IPC message snapshots ServerMessage types get_signing_identity serializes to expected JSON 1`] = ` +{ + "requestId": "req-identity-001", + "type": "get_signing_identity", +} +`; + +exports[`IPC message snapshots ServerMessage types session_error serializes to expected JSON 1`] = ` +{ + "code": "PROVIDER_NETWORK", + "debugDetails": "ETIMEDOUT after 30000ms", + "retryable": true, + "sessionId": "sess-001", + "type": "session_error", + "userMessage": "Unable to reach the AI provider. Please try again.", +} +`; + +exports[`IPC message snapshots ServerMessage types trace_event serializes to expected JSON 1`] = ` +{ + "attributes": { + "command": "ls -la", + "riskLevel": "low", + "sandboxed": true, + "toolName": "bash", + }, + "eventId": "evt-001", + "kind": "tool_started", + "requestId": "req-001", + "sequence": 1, + "sessionId": "sess-001", + "status": "info", + "summary": "Running bash: ls -la", + "timestampMs": 1700000000000, + "type": "trace_event", +} +`; + +exports[`IPC message snapshots ServerMessage types ipc_blob_probe_result serializes to expected JSON 1`] = ` +{ + "observedNonceSha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "ok": true, + "probeId": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", + "type": "ipc_blob_probe_result", +} +`; + +exports[`IPC message snapshots ServerMessage types gallery_list_response serializes to expected JSON 1`] = ` +{ + "gallery": { + "apps": [], + "categories": [ + { + "icon": "📋", + "id": "productivity", + "name": "Productivity", + }, + ], + "updatedAt": "2026-02-15T00:00:00Z", + "version": 1, + }, + "type": "gallery_list_response", +} +`; + +exports[`IPC message snapshots ServerMessage types gallery_install_response serializes to expected JSON 1`] = ` +{ + "appId": "app-new-001", + "name": "Focus Timer", + "success": true, + "type": "gallery_install_response", +} +`; + +exports[`IPC message snapshots ServerMessage types share_app_cloud_response serializes to expected JSON 1`] = ` +{ + "shareToken": "abc123def456", + "shareUrl": "http://localhost:7821/v1/apps/shared/abc123def456", + "success": true, + "type": "share_app_cloud_response", +} +`; + +exports[`IPC message snapshots ServerMessage types share_to_slack_response serializes to expected JSON 1`] = ` +{ + "success": true, + "type": "share_to_slack_response", +} +`; + +exports[`IPC message snapshots ServerMessage types slack_webhook_config_response serializes to expected JSON 1`] = ` +{ + "success": true, + "type": "slack_webhook_config_response", + "webhookUrl": "https://hooks.slack.com/services/T00/B00/xxx", +} +`; + +exports[`IPC message snapshots ServerMessage types ingress_config_response serializes to expected JSON 1`] = ` +{ + "enabled": true, + "localGatewayTarget": "http://127.0.0.1:7830", + "publicBaseUrl": "https://example.com", + "success": true, + "type": "ingress_config_response", +} +`; + +exports[`IPC message snapshots ServerMessage types vercel_api_config_response serializes to expected JSON 1`] = ` +{ + "hasToken": true, + "success": true, + "type": "vercel_api_config_response", +} +`; + +exports[`IPC message snapshots ServerMessage types twitter_integration_config_response serializes to expected JSON 1`] = ` +{ + "connected": false, + "localClientConfigured": true, + "managedAvailable": false, + "mode": "local_byo", + "success": true, + "type": "twitter_integration_config_response", +} +`; + +exports[`IPC message snapshots ServerMessage types telegram_config_response serializes to expected JSON 1`] = ` +{ + "botUsername": "my_test_bot", + "connected": true, + "hasBotToken": true, + "hasWebhookSecret": true, + "success": true, + "type": "telegram_config_response", +} +`; + +exports[`IPC message snapshots ServerMessage types twilio_config_response serializes to expected JSON 1`] = ` +{ + "compliance": { + "numberType": "toll_free", + "verificationSid": "TF_VER_001", + "verificationStatus": "TWILIO_APPROVED", + }, + "diagnostics": { + "actionItems": [], + "compliance": { + "detail": "Toll-free verification: TWILIO_APPROVED", + "status": "TWILIO_APPROVED", + }, + "overallStatus": "healthy", + "readiness": { + "issues": [], + "ready": true, + }, + }, + "hasCredentials": true, + "phoneNumber": "+15551234567", + "success": true, + "testResult": { + "finalStatus": "delivered", + "initialStatus": "queued", + "messageSid": "SM-test-001", + "to": "+15559876543", + }, + "type": "twilio_config_response", +} +`; + +exports[`IPC message snapshots ServerMessage types channel_readiness_response serializes to expected JSON 1`] = ` +{ + "snapshots": [ + { + "channel": "sms", + "checkedAt": 1700000000000, + "localChecks": [ + { + "message": "Twilio credentials are not configured", + "name": "twilio_credentials", + "passed": false, + }, + { + "message": "Phone number is assigned", + "name": "phone_number", + "passed": true, + }, + { + "message": "Public ingress URL is configured", + "name": "ingress", + "passed": true, + }, + ], + "ready": false, + "reasons": [ + { + "code": "twilio_credentials", + "text": "Twilio credentials are not configured", + }, + ], + "stale": false, + }, + ], + "success": true, + "type": "channel_readiness_response", +} +`; + +exports[`IPC message snapshots ServerMessage types guardian_verification_response serializes to expected JSON 1`] = ` +{ + "instruction": "Send this code to the Telegram bot", + "secret": "verify-secret-123", + "success": true, + "type": "guardian_verification_response", +} +`; + +exports[`IPC message snapshots ServerMessage types twitter_auth_result serializes to expected JSON 1`] = ` +{ + "accountInfo": "@vellum_test", + "success": true, + "type": "twitter_auth_result", +} +`; + +exports[`IPC message snapshots ServerMessage types twitter_auth_status_response serializes to expected JSON 1`] = ` +{ + "accountInfo": "@vellum_test", + "connected": true, + "mode": "local_byo", + "type": "twitter_auth_status_response", +} +`; + +exports[`IPC message snapshots ServerMessage types open_url serializes to expected JSON 1`] = ` +{ + "title": "Example", + "type": "open_url", + "url": "https://example.com", +} +`; + +exports[`IPC message snapshots ServerMessage types app_update_preview_response serializes to expected JSON 1`] = ` +{ + "appId": "app-001", + "success": true, + "type": "app_update_preview_response", +} +`; + +exports[`IPC message snapshots ServerMessage types app_preview_response serializes to expected JSON 1`] = ` +{ + "appId": "app-001", + "preview": "base64-png-data", + "type": "app_preview_response", +} +`; + +exports[`IPC message snapshots ServerMessage types app_history_response serializes to expected JSON 1`] = ` +{ + "appId": "app-001", + "type": "app_history_response", + "versions": [ + { + "commitHash": "abc123def456", + "message": "Initial app commit", + "timestamp": 1700000000, + }, + { + "commitHash": "789abc123def", + "message": "Update landing page", + "timestamp": 1700001000, + }, + ], +} +`; + +exports[`IPC message snapshots ServerMessage types app_diff_response serializes to expected JSON 1`] = ` +{ + "appId": "app-001", + "diff": "diff --git a/index.html b/index.html", + "type": "app_diff_response", +} +`; + +exports[`IPC message snapshots ServerMessage types app_file_at_version_response serializes to expected JSON 1`] = ` +{ + "appId": "app-001", + "content": "Hello", + "path": "index.html", + "type": "app_file_at_version_response", +} +`; + +exports[`IPC message snapshots ServerMessage types app_restore_response serializes to expected JSON 1`] = ` +{ + "success": true, + "type": "app_restore_response", +} +`; + +exports[`IPC message snapshots ServerMessage types ui_surface_undo_result serializes to expected JSON 1`] = ` +{ + "remainingUndos": 3, + "sessionId": "sess-001", + "success": true, + "surfaceId": "surface-001", + "type": "ui_surface_undo_result", +} +`; + +exports[`IPC message snapshots ServerMessage types publish_page_response serializes to expected JSON 1`] = ` +{ + "deploymentId": "dpl-001", + "publicUrl": "https://example.vercel.app", + "success": true, + "type": "publish_page_response", +} +`; + +exports[`IPC message snapshots ServerMessage types unpublish_page_response serializes to expected JSON 1`] = ` +{ + "success": true, + "type": "unpublish_page_response", +} +`; + +exports[`IPC message snapshots ServerMessage types app_files_changed serializes to expected JSON 1`] = ` +{ + "appId": "app-001", + "type": "app_files_changed", +} +`; + +exports[`IPC message snapshots ServerMessage types browser_frame serializes to expected JSON 1`] = ` +{ + "frame": "base64-jpeg-data", + "metadata": { + "offsetTop": 0, + "pageScaleFactor": 1, + "scrollOffsetX": 0, + "scrollOffsetY": 0, + "timestamp": 1700000000, + }, + "sessionId": "sess-001", + "surfaceId": "surface-001", + "type": "browser_frame", +} +`; + +exports[`IPC message snapshots ServerMessage types diagnostics_export_response serializes to expected JSON 1`] = ` +{ + "filePath": "/tmp/diagnostics-conv-001.zip", + "success": true, + "type": "diagnostics_export_response", +} +`; + +exports[`IPC message snapshots ServerMessage types accept_starter_bundle_response serializes to expected JSON 1`] = ` +{ + "accepted": true, + "alreadyAccepted": false, + "rulesAdded": 5, + "type": "accept_starter_bundle_response", +} +`; + +exports[`IPC message snapshots ServerMessage types env_vars_response serializes to expected JSON 1`] = ` +{ + "type": "env_vars_response", + "vars": { + "HOME": "/Users/test", + "PATH": "/usr/bin", + }, +} +`; + +exports[`IPC message snapshots ServerMessage types integration_list_response serializes to expected JSON 1`] = ` +{ + "integrations": [ + { + "connected": false, + "id": "gmail", + }, + ], + "type": "integration_list_response", +} +`; + +exports[`IPC message snapshots ServerMessage types integration_connect_result serializes to expected JSON 1`] = ` +{ + "integrationId": "gmail", + "success": true, + "type": "integration_connect_result", +} +`; + +exports[`IPC message snapshots ServerMessage types browser_cdp_request serializes to expected JSON 1`] = ` +{ + "sessionId": "test-session", + "type": "browser_cdp_request", +} +`; + +exports[`IPC message snapshots ServerMessage types browser_interactive_mode_changed serializes to expected JSON 1`] = ` +{ + "enabled": true, + "sessionId": "test-session", + "surfaceId": "test-surface", + "type": "browser_interactive_mode_changed", +} +`; + +exports[`IPC message snapshots ServerMessage types browser_handoff_request serializes to expected JSON 1`] = ` +{ + "message": "Login required", + "reason": "auth", + "sessionId": "test-session", + "surfaceId": "test-surface", + "type": "browser_handoff_request", +} +`; + +exports[`IPC message snapshots ServerMessage types document_editor_show serializes to expected JSON 1`] = ` +{ + "initialContent": "# Hello World", + "sessionId": "sess-001", + "surfaceId": "doc-001", + "title": "My Document", + "type": "document_editor_show", +} +`; + +exports[`IPC message snapshots ServerMessage types document_editor_update serializes to expected JSON 1`] = ` +{ + "markdown": "# Updated Content", + "mode": "replace", + "sessionId": "sess-001", + "surfaceId": "doc-001", + "type": "document_editor_update", +} +`; + +exports[`IPC message snapshots ServerMessage types document_save_response serializes to expected JSON 1`] = ` +{ + "success": true, + "surfaceId": "doc-001", + "type": "document_save_response", +} +`; + +exports[`IPC message snapshots ServerMessage types document_load_response serializes to expected JSON 1`] = ` +{ + "content": "# Hello", + "conversationId": "conv-001", + "createdAt": 1700000000, + "success": true, + "surfaceId": "doc-001", + "title": "My Document", + "type": "document_load_response", + "updatedAt": 1700001000, + "wordCount": 1, +} +`; + +exports[`IPC message snapshots ServerMessage types document_list_response serializes to expected JSON 1`] = ` +{ + "documents": [ + { + "conversationId": "conv-001", + "createdAt": 1700000000, + "surfaceId": "doc-001", + "title": "My Document", + "updatedAt": 1700001000, + "wordCount": 100, + }, + ], + "type": "document_list_response", +} +`; + +exports[`IPC message snapshots ServerMessage types work_items_list_response serializes to expected JSON 1`] = ` +{ + "items": [ + { + "createdAt": 1700000000, + "id": "wi-001", + "lastRunConversationId": null, + "lastRunId": null, + "lastRunStatus": null, + "notes": null, + "priorityTier": 1, + "sortIndex": null, + "sourceId": null, + "sourceType": null, + "status": "queued", + "taskId": "task-001", + "title": "Process report", + "updatedAt": 1700000000, + }, + ], + "type": "work_items_list_response", +} +`; + +exports[`IPC message snapshots ServerMessage types work_item_get_response serializes to expected JSON 1`] = ` +{ + "item": { + "createdAt": 1700000000, + "id": "wi-001", + "lastRunConversationId": null, + "lastRunId": null, + "lastRunStatus": null, + "notes": null, + "priorityTier": 1, + "sortIndex": null, + "sourceId": null, + "sourceType": null, + "status": "queued", + "taskId": "task-001", + "title": "Process report", + "updatedAt": 1700000000, + }, + "type": "work_item_get_response", +} +`; + +exports[`IPC message snapshots ServerMessage types work_item_update_response serializes to expected JSON 1`] = ` +{ + "item": { + "createdAt": 1700000000, + "id": "wi-001", + "lastRunConversationId": null, + "lastRunId": null, + "lastRunStatus": null, + "notes": null, + "priorityTier": 1, + "sortIndex": null, + "sourceId": null, + "sourceType": null, + "status": "running", + "taskId": "task-001", + "title": "Updated title", + "updatedAt": 1700001000, + }, + "type": "work_item_update_response", +} +`; + +exports[`IPC message snapshots ServerMessage types work_item_delete_response serializes to expected JSON 1`] = ` +{ + "id": "wi-001", + "success": true, + "type": "work_item_delete_response", +} +`; + +exports[`IPC message snapshots ServerMessage types work_item_run_task_response serializes to expected JSON 1`] = ` +{ + "id": "wi-001", + "lastRunId": "run-001", + "success": true, + "type": "work_item_run_task_response", +} +`; + +exports[`IPC message snapshots ServerMessage types work_item_output_response serializes to expected JSON 1`] = ` +{ + "id": "wi-001", + "output": { + "completedAt": 1700002000, + "conversationId": "conv-001", + "highlights": [ + "- Key finding 1", + "- Key finding 2", + ], + "runId": "run-001", + "status": "completed", + "summary": "Report processed successfully.", + "title": "Process report", + }, + "success": true, + "type": "work_item_output_response", +} +`; + +exports[`IPC message snapshots ServerMessage types work_item_preflight_response serializes to expected JSON 1`] = ` +{ + "id": "wi-001", + "permissions": [ + { + "currentDecision": "prompt", + "description": "Run shell commands", + "riskLevel": "medium", + "tool": "bash", + }, + ], + "success": true, + "type": "work_item_preflight_response", +} +`; + +exports[`IPC message snapshots ServerMessage types work_item_approve_permissions_response serializes to expected JSON 1`] = ` +{ + "id": "wi-001", + "success": true, + "type": "work_item_approve_permissions_response", +} +`; + +exports[`IPC message snapshots ServerMessage types work_item_cancel_response serializes to expected JSON 1`] = ` +{ + "id": "wi-001", + "success": true, + "type": "work_item_cancel_response", +} +`; + +exports[`IPC message snapshots ServerMessage types work_item_status_changed serializes to expected JSON 1`] = ` +{ + "item": { + "id": "wi-001", + "lastRunConversationId": "conv-001", + "lastRunId": "run-001", + "lastRunStatus": "completed", + "status": "awaiting_review", + "taskId": "task-001", + "title": "Process report", + "updatedAt": 1700001000, + }, + "type": "work_item_status_changed", +} +`; + +exports[`IPC message snapshots ServerMessage types tasks_changed serializes to expected JSON 1`] = ` +{ + "type": "tasks_changed", +} +`; + +exports[`IPC message snapshots ServerMessage types open_tasks_window serializes to expected JSON 1`] = ` +{ + "type": "open_tasks_window", +} +`; + +exports[`IPC message snapshots ServerMessage types task_run_thread_created serializes to expected JSON 1`] = ` +{ + "conversationId": "conv-task-run-001", + "title": "Process report", + "type": "task_run_thread_created", + "workItemId": "wi-001", +} +`; + +exports[`IPC message snapshots ServerMessage types subagent_spawned serializes to expected JSON 1`] = ` +{ + "label": "Research Agent", + "objective": "Find relevant documentation", + "parentSessionId": "sess-001", + "subagentId": "sub-001", + "type": "subagent_spawned", +} +`; + +exports[`IPC message snapshots ServerMessage types subagent_status_changed serializes to expected JSON 1`] = ` +{ + "status": "completed", + "subagentId": "sub-001", + "type": "subagent_status_changed", +} +`; + +exports[`IPC message snapshots ServerMessage types subagent_event serializes to expected JSON 1`] = ` +{ + "event": { + "sessionId": "sub-sess-001", + "text": "Searching for docs...", + "type": "assistant_text_delta", + }, + "subagentId": "sub-001", + "type": "subagent_event", +} +`; + +exports[`IPC message snapshots ServerMessage types subagent_detail_response serializes to expected JSON 1`] = ` +{ + "events": [ + { + "content": "Reading file...", + "isError": false, + "toolName": "read_file", + "type": "tool_use", + }, + ], + "objective": "Search for documentation", + "subagentId": "sub-001", + "type": "subagent_detail_response", +} +`; + +exports[`IPC message snapshots ServerMessage types workspace_files_list_response serializes to expected JSON 1`] = ` +{ + "files": [ + { + "exists": true, + "name": "IDENTITY.md", + "path": "IDENTITY.md", + }, + ], + "type": "workspace_files_list_response", +} +`; + +exports[`IPC message snapshots ServerMessage types workspace_file_read_response serializes to expected JSON 1`] = ` +{ + "content": "# My Identity", + "path": "IDENTITY.md", + "type": "workspace_file_read_response", +} +`; + +exports[`IPC message snapshots ServerMessage types identity_get_response serializes to expected JSON 1`] = ` +{ + "emoji": "✨", + "found": true, + "home": "~/workspace", + "name": "Vex", + "personality": "Friendly", + "role": "AI assistant", + "type": "identity_get_response", +} +`; + +exports[`IPC message snapshots ServerMessage types tool_permission_simulate_response serializes to expected JSON 1`] = ` +{ + "decision": "prompt", + "executionTarget": "host", + "promptPayload": { + "allowlistOptions": [ + { + "description": "Allow rm commands", + "label": "Allow rm commands", + "pattern": "bash:rm *", + }, + ], + "persistentDecisionsAllowed": true, + "scopeOptions": [ + { + "label": "In /projects/my-app", + "scope": "/projects/my-app", + }, + ], + }, + "reason": "No matching trust rule; tool requires approval", + "riskLevel": "high", + "success": true, + "type": "tool_permission_simulate_response", +} +`; + +exports[`IPC message snapshots ServerMessage types tool_names_list_response serializes to expected JSON 1`] = ` +{ + "names": [ + "bash", + "file_read", + "file_write", + ], + "type": "tool_names_list_response", +} +`; + +exports[`IPC message snapshots ServerMessage types dictation_response serializes to expected JSON 1`] = ` +{ + "mode": "dictation", + "text": "Hello world", + "type": "dictation_response", +} +`; diff --git a/assistant/src/__tests__/ipc-snapshot.test.ts b/assistant/src/__tests__/ipc-snapshot.test.ts index fc746d184e9..76718f80427 100644 --- a/assistant/src/__tests__/ipc-snapshot.test.ts +++ b/assistant/src/__tests__/ipc-snapshot.test.ts @@ -1254,6 +1254,18 @@ const serverMessages: Record = { verificationSid: 'TF_VER_001', verificationStatus: 'TWILIO_APPROVED', }, + testResult: { + messageSid: 'SM-test-001', + to: '+15559876543', + initialStatus: 'queued', + finalStatus: 'delivered', + }, + diagnostics: { + readiness: { ready: true, issues: [] }, + compliance: { status: 'TWILIO_APPROVED', detail: 'Toll-free verification: TWILIO_APPROVED' }, + overallStatus: 'healthy', + actionItems: [], + }, }, channel_readiness_response: { type: 'channel_readiness_response', diff --git a/assistant/src/__tests__/sms-messaging-provider.test.ts b/assistant/src/__tests__/sms-messaging-provider.test.ts index ba501c3703b..691a4d2908f 100644 --- a/assistant/src/__tests__/sms-messaging-provider.test.ts +++ b/assistant/src/__tests__/sms-messaging-provider.test.ts @@ -1,6 +1,6 @@ import { beforeEach, describe, expect, mock, test } from 'bun:test'; -const sendSmsMock = mock(async (..._args: unknown[]) => {}); +const sendSmsMock = mock(async (..._args: unknown[]) => ({ messageSid: 'SM-mock-sid', status: 'queued' })); const getOrCreateConversationMock = mock((_key: string) => ({ conversationId: 'conv-1' })); const upsertOutboundBindingMock = mock((_input: Record) => {}); @@ -91,6 +91,25 @@ describe('smsMessagingProvider', () => { expect(upsertOutboundBindingMock).not.toHaveBeenCalled(); }); + test('sendMessage uses messageSid from gateway response as result ID', async () => { + sendSmsMock.mockImplementation(async () => ({ messageSid: 'SM-test-12345', status: 'queued' })); + const result = await smsMessagingProvider.sendMessage('', '+15550009999', 'sid test', { + assistantId: 'self', + }); + expect(result.id).toBe('SM-test-12345'); + }); + + test('sendMessage falls back to timestamp-based ID when messageSid is absent', async () => { + sendSmsMock.mockImplementation(async () => ({})); + const before = Date.now(); + const result = await smsMessagingProvider.sendMessage('', '+15550009999', 'no sid', { + assistantId: 'self', + }); + expect(result.id).toMatch(/^sms-\d+$/); + const ts = parseInt(result.id.replace('sms-', ''), 10); + expect(ts).toBeGreaterThanOrEqual(before); + }); + test('sendMessage uses canonical self key and writes outbound binding for self scope', async () => { await smsMessagingProvider.sendMessage('', '+15550003333', 'hello', { assistantId: 'self', diff --git a/assistant/src/calls/twilio-rest.ts b/assistant/src/calls/twilio-rest.ts index 994ba61965b..47b75fa52a3 100644 --- a/assistant/src/calls/twilio-rest.ts +++ b/assistant/src/calls/twilio-rest.ts @@ -155,6 +155,38 @@ export async function provisionPhoneNumber( }; } +/** Fetch the current status of a Twilio message by SID. */ +export async function fetchMessageStatus( + accountSid: string, + authToken: string, + messageSid: string, +): Promise<{ status: string; errorCode?: string; errorMessage?: string }> { + const res = await fetch( + `${twilioBaseUrl(accountSid)}/Messages/${encodeURIComponent(messageSid)}.json`, + { + method: 'GET', + headers: { Authorization: twilioAuthHeader(accountSid, authToken) }, + }, + ); + + if (!res.ok) { + const text = await res.text(); + throw new Error(`Twilio API error ${res.status}: ${text}`); + } + + const data = (await res.json()) as { + status?: string; + error_code?: number | null; + error_message?: string | null; + }; + + return { + status: data.status ?? 'unknown', + errorCode: data.error_code != null ? String(data.error_code) : undefined, + errorMessage: data.error_message ?? undefined, + }; +} + export interface WebhookUrls { voiceUrl: string; statusCallbackUrl: string; diff --git a/assistant/src/config/bundled-skills/messaging/tools/messaging-send.ts b/assistant/src/config/bundled-skills/messaging/tools/messaging-send.ts index a8ec182a57a..692858da2c8 100644 --- a/assistant/src/config/bundled-skills/messaging/tools/messaging-send.ts +++ b/assistant/src/config/bundled-skills/messaging/tools/messaging-send.ts @@ -24,6 +24,9 @@ export async function run(input: Record, context: ToolContext): assistantId: context.assistantId, }); + if (provider.id === 'sms') { + return ok(`SMS accepted by Twilio (ID: ${result.id}). Note: "accepted" means Twilio received it for delivery — it has not yet been confirmed as delivered to the handset.`); + } return ok(`Message sent (ID: ${result.id}).`); }); } catch (e) { diff --git a/assistant/src/daemon/handlers/config.ts b/assistant/src/daemon/handlers/config.ts index 58e43d584db..10f9d9b48aa 100644 --- a/assistant/src/daemon/handlers/config.ts +++ b/assistant/src/daemon/handlers/config.ts @@ -49,6 +49,7 @@ import { deleteTollFreeVerification, getPhoneNumberSid, releasePhoneNumber, + fetchMessageStatus, type TollFreeVerificationSubmitParams, } from '../../calls/twilio-rest.js'; import { @@ -1188,6 +1189,32 @@ export async function handleTelegramConfig( } } +/** In-memory store for the last SMS send test result. Shared between sms_send_test and sms_doctor. */ +let _lastTestResult: { + messageSid: string; + to: string; + initialStatus: string; + finalStatus: string; + errorCode?: string; + errorMessage?: string; + timestamp: number; +} | undefined; + +/** Map a Twilio error code to a human-readable remediation suggestion. */ +function mapTwilioErrorRemediation(errorCode: string | undefined): string | undefined { + if (!errorCode) return undefined; + const map: Record = { + '30003': 'Unreachable destination. The handset may be off or out of service.', + '30004': 'Message blocked by carrier or recipient.', + '30005': 'Unknown destination phone number. Verify the number is valid.', + '30006': 'Landline or unreachable carrier. SMS cannot be delivered to this number.', + '30007': 'Message flagged as spam by carrier. Adjust content or register for A2P.', + '30008': 'Unknown error from the carrier network.', + '21610': 'Recipient has opted out (STOP). Cannot send until they opt back in.', + }; + return map[errorCode]; +} + export async function handleTwilioConfig( msg: TwilioConfigRequest, socket: net.Socket, @@ -1843,6 +1870,253 @@ export async function handleTwilioConfig( hasCredentials: true, warning: 'Phone number released from Twilio. Any associated toll-free verification context is lost.', }); + } else if (msg.action === 'sms_send_test') { + // ── SMS send test ──────────────────────────────────────────────── + if (!hasTwilioCredentials()) { + ctx.send(socket, { + type: 'twilio_config_response', + success: false, + hasCredentials: false, + error: 'Twilio credentials not configured. Set credentials first.', + }); + return; + } + + const to = msg.phoneNumber; + if (!to) { + ctx.send(socket, { + type: 'twilio_config_response', + success: false, + hasCredentials: true, + error: 'phoneNumber is required for sms_send_test action.', + }); + return; + } + + const raw = loadRawConfig(); + const smsSection = (raw?.sms ?? {}) as Record; + const from = (smsSection.phoneNumber as string | undefined) + || getSecureKey('credential:twilio:phone_number') + || ''; + if (!from) { + ctx.send(socket, { + type: 'twilio_config_response', + success: false, + hasCredentials: true, + error: 'No phone number assigned. Run the twilio-setup skill to assign a number.', + }); + return; + } + + const accountSid = getSecureKey('credential:twilio:account_sid')!; + const authToken = getSecureKey('credential:twilio:auth_token')!; + const text = msg.text || 'Test SMS from your Vellum assistant'; + + // Send via gateway's /deliver/sms endpoint + const bearerToken = readHttpToken(); + const gatewayPort = Number(process.env.GATEWAY_PORT) || 7830; + const gatewayUrl = process.env.GATEWAY_INTERNAL_BASE_URL?.replace(/\/+$/, '') || `http://127.0.0.1:${gatewayPort}`; + + const sendResp = await fetch(`${gatewayUrl}/deliver/sms`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + ...(bearerToken ? { Authorization: `Bearer ${bearerToken}` } : {}), + }, + body: JSON.stringify({ to, text }), + signal: AbortSignal.timeout(30_000), + }); + + if (!sendResp.ok) { + const errBody = await sendResp.text().catch(() => ''); + ctx.send(socket, { + type: 'twilio_config_response', + success: false, + hasCredentials: true, + error: `SMS send failed (${sendResp.status}): ${errBody}`, + }); + return; + } + + const sendData = await sendResp.json().catch(() => ({})) as { + messageSid?: string; + status?: string; + }; + const messageSid = sendData.messageSid || ''; + const initialStatus = sendData.status || 'unknown'; + + // Poll Twilio for final status (up to 3 times, 2s apart) + let finalStatus = initialStatus; + let errorCode: string | undefined; + let errorMessage: string | undefined; + + if (messageSid) { + for (let i = 0; i < 3; i++) { + await new Promise((r) => setTimeout(r, 2000)); + try { + const pollResult = await fetchMessageStatus(accountSid, authToken, messageSid); + finalStatus = pollResult.status; + errorCode = pollResult.errorCode; + errorMessage = pollResult.errorMessage; + // Stop polling if we've reached a terminal status + if (['delivered', 'undelivered', 'failed'].includes(finalStatus)) break; + } catch { + // Polling failure is non-fatal; we'll use the last known status + break; + } + } + } + + const testResult = { + messageSid, + to, + initialStatus, + finalStatus, + ...(errorCode ? { errorCode } : {}), + ...(errorMessage ? { errorMessage } : {}), + }; + + // Store for sms_doctor + _lastTestResult = { ...testResult, timestamp: Date.now() }; + + ctx.send(socket, { + type: 'twilio_config_response', + success: true, + hasCredentials: true, + testResult, + }); + + } else if (msg.action === 'sms_doctor') { + // ── SMS doctor diagnostic ──────────────────────────────────────── + const hasCredentials = hasTwilioCredentials(); + + // 1. Channel readiness check + let readinessReady = false; + const readinessIssues: string[] = []; + try { + const readinessService = getReadinessService(); + const snapshot = await readinessService.getReadiness('sms', { includeRemote: false }); + readinessReady = snapshot.ready; + for (const r of snapshot.reasons) { + readinessIssues.push(r.text); + } + } catch (err) { + readinessIssues.push(`Readiness check failed: ${err instanceof Error ? err.message : String(err)}`); + } + + // 2. Compliance status + let complianceStatus = 'unknown'; + let complianceDetail: string | undefined; + let complianceRemediation: string | undefined; + if (hasCredentials) { + try { + const raw = loadRawConfig(); + const smsSection = (raw?.sms ?? {}) as Record; + const phoneNumber = (smsSection.phoneNumber as string | undefined) || getSecureKey('credential:twilio:phone_number') || ''; + if (phoneNumber) { + const accountSid = getSecureKey('credential:twilio:account_sid')!; + const authToken = getSecureKey('credential:twilio:auth_token')!; + // Determine number type and verification status + const isTollFree = phoneNumber.startsWith('+1') && ['800','888','877','866','855','844','833'].some( + (p) => phoneNumber.startsWith(`+1${p}`), + ); + if (isTollFree) { + try { + const verification = await getTollFreeVerificationStatus(accountSid, authToken, phoneNumber); + if (verification) { + const status = verification.status; + complianceStatus = status; + complianceDetail = `Toll-free verification: ${status}`; + if (status === 'TWILIO_APPROVED') { + complianceRemediation = undefined; + } else if (status === 'PENDING_REVIEW' || status === 'IN_REVIEW') { + complianceRemediation = 'Toll-free verification is pending. Messaging may have limited throughput until approved.'; + } else if (status === 'TWILIO_REJECTED') { + complianceRemediation = 'Toll-free verification was rejected. Check rejection reasons and resubmit.'; + } else { + complianceRemediation = 'Submit a toll-free verification to enable full messaging throughput.'; + } + } else { + complianceStatus = 'unverified'; + complianceDetail = 'Toll-free number without verification'; + complianceRemediation = 'Submit a toll-free verification request to avoid filtering.'; + } + } catch { + complianceStatus = 'check_failed'; + complianceDetail = 'Could not retrieve toll-free verification status'; + } + } else { + complianceStatus = 'local_10dlc'; + complianceDetail = 'Local/10DLC number — carrier registration handled externally'; + } + } else { + complianceStatus = 'no_number'; + complianceDetail = 'No phone number assigned'; + complianceRemediation = 'Assign a phone number via the twilio-setup skill.'; + } + } catch { + complianceStatus = 'check_failed'; + complianceDetail = 'Could not determine compliance status'; + } + } else { + complianceStatus = 'no_credentials'; + complianceDetail = 'Twilio credentials are not configured'; + complianceRemediation = 'Set Twilio credentials via the twilio-setup skill.'; + } + + // 3. Last send test result + let lastSend: { status: string; errorCode?: string; remediation?: string } | undefined; + if (_lastTestResult) { + lastSend = { + status: _lastTestResult.finalStatus, + ...((_lastTestResult.errorCode) ? { errorCode: _lastTestResult.errorCode } : {}), + ...((_lastTestResult.errorCode) ? { remediation: mapTwilioErrorRemediation(_lastTestResult.errorCode) } : {}), + }; + } + + // 4. Determine overall status + const actionItems: string[] = []; + let overallStatus: 'healthy' | 'degraded' | 'broken' = 'healthy'; + + if (!hasCredentials) { + overallStatus = 'broken'; + actionItems.push('Configure Twilio credentials.'); + } + if (!readinessReady) { + overallStatus = 'broken'; + for (const issue of readinessIssues) actionItems.push(issue); + } + if (complianceStatus === 'unverified' || complianceStatus === 'PENDING_REVIEW' || complianceStatus === 'IN_REVIEW') { + if (overallStatus === 'healthy') overallStatus = 'degraded'; + if (complianceRemediation) actionItems.push(complianceRemediation); + } + if (complianceStatus === 'TWILIO_REJECTED' || complianceStatus === 'no_number') { + overallStatus = 'broken'; + if (complianceRemediation) actionItems.push(complianceRemediation); + } + if (_lastTestResult && ['failed', 'undelivered'].includes(_lastTestResult.finalStatus)) { + if (overallStatus === 'healthy') overallStatus = 'degraded'; + const remediation = mapTwilioErrorRemediation(_lastTestResult.errorCode); + actionItems.push(remediation || `Last test SMS ${_lastTestResult.finalStatus}. Check Twilio logs for details.`); + } + + ctx.send(socket, { + type: 'twilio_config_response', + success: true, + hasCredentials, + diagnostics: { + readiness: { ready: readinessReady, issues: readinessIssues }, + compliance: { + status: complianceStatus, + ...(complianceDetail ? { detail: complianceDetail } : {}), + ...(complianceRemediation ? { remediation: complianceRemediation } : {}), + }, + ...(lastSend ? { lastSend } : {}), + overallStatus, + actionItems, + }, + }); + } else { ctx.send(socket, { type: 'twilio_config_response', diff --git a/assistant/src/daemon/ipc-contract.ts b/assistant/src/daemon/ipc-contract.ts index 04631d13778..1c34386d855 100644 --- a/assistant/src/daemon/ipc-contract.ts +++ b/assistant/src/daemon/ipc-contract.ts @@ -565,10 +565,10 @@ export interface TwilioConfigRequest { type: 'twilio_config'; action: 'get' | 'set_credentials' | 'clear_credentials' | 'provision_number' | 'assign_number' | 'list_numbers' | 'sms_compliance_status' | 'sms_submit_tollfree_verification' | 'sms_update_tollfree_verification' - | 'sms_delete_tollfree_verification' | 'release_number'; + | 'sms_delete_tollfree_verification' | 'release_number' | 'sms_send_test' | 'sms_doctor'; accountSid?: string; // Only for action: 'set_credentials' authToken?: string; // Only for action: 'set_credentials' - phoneNumber?: string; // Only for action: 'assign_number' + phoneNumber?: string; // Only for action: 'assign_number' or 'sms_send_test' areaCode?: string; // Only for action: 'provision_number' country?: string; // Only for action: 'provision_number' (ISO 3166-1 alpha-2, default 'US') assistantId?: string; // Scope number assignment/lookup to a specific assistant @@ -587,6 +587,7 @@ export interface TwilioConfigRequest { businessType?: string; customerProfileSid?: string; }; + text?: string; // Only for action: 'sms_send_test' (default: "Test SMS from your Vellum assistant") } export interface TwilioConfigResponse { @@ -608,6 +609,23 @@ export interface TwilioConfigResponse { editAllowed?: boolean; editExpiration?: string; }; + /** Present when action is 'sms_send_test'. */ + testResult?: { + messageSid: string; + to: string; + initialStatus: string; + finalStatus: string; + errorCode?: string; + errorMessage?: string; + }; + /** Present when action is 'sms_doctor'. */ + diagnostics?: { + readiness: { ready: boolean; issues: string[] }; + compliance: { status: string; detail?: string; remediation?: string }; + lastSend?: { status: string; errorCode?: string; remediation?: string }; + overallStatus: 'healthy' | 'degraded' | 'broken'; + actionItems: string[]; + }; } export interface ChannelReadinessRequest { diff --git a/assistant/src/messaging/providers/sms/adapter.ts b/assistant/src/messaging/providers/sms/adapter.ts index 65dc8dcea5a..f45755c9f3b 100644 --- a/assistant/src/messaging/providers/sms/adapter.ts +++ b/assistant/src/messaging/providers/sms/adapter.ts @@ -151,7 +151,7 @@ export const smsMessagingProvider: MessagingProvider = { const bearerToken = getBearerToken(); const assistantId = options?.assistantId; - await sms.sendMessage(gatewayUrl, bearerToken, conversationId, text, assistantId); + const sendResult = await sms.sendMessage(gatewayUrl, bearerToken, conversationId, text, assistantId); // Upsert external conversation binding so the conversation key mapping // exists for the next inbound SMS from this number. @@ -175,8 +175,12 @@ export const smsMessagingProvider: MessagingProvider = { // Best-effort — don't fail the send if binding upsert fails } + // Use the Twilio message SID as the send result ID when available, + // falling back to a timestamp-based ID for older gateway versions. + const id = sendResult.messageSid || `sms-${Date.now()}`; + return { - id: `sms-${Date.now()}`, + id, timestamp: Date.now(), conversationId, }; diff --git a/assistant/src/messaging/providers/sms/client.ts b/assistant/src/messaging/providers/sms/client.ts index c424a5ba4fc..122614a22e2 100644 --- a/assistant/src/messaging/providers/sms/client.ts +++ b/assistant/src/messaging/providers/sms/client.ts @@ -26,8 +26,20 @@ interface DeliverPayload { assistantId?: string; } +/** Result returned by sendMessage with Twilio acceptance details. */ +export interface SmsSendResult { + messageSid?: string; + status?: string; + errorCode?: string | null; + errorMessage?: string | null; +} + /** * Send an SMS message via the gateway's /deliver/sms endpoint. + * + * Returns Twilio acceptance details propagated from the gateway. + * "Accepted" means Twilio received it for delivery -- it has NOT yet + * been confirmed as delivered to the handset. */ export async function sendMessage( gatewayUrl: string, @@ -35,7 +47,7 @@ export async function sendMessage( to: string, text: string, assistantId?: string, -): Promise { +): Promise { const payload: DeliverPayload = { to, text }; if (assistantId) { payload.assistantId = assistantId; @@ -59,4 +71,23 @@ export async function sendMessage( `Gateway /deliver/sms failed (${resp.status}): ${body}`, ); } + + try { + const data = (await resp.json()) as { + ok?: boolean; + messageSid?: string; + status?: string; + errorCode?: string | null; + errorMessage?: string | null; + }; + return { + messageSid: data.messageSid, + status: data.status, + errorCode: data.errorCode, + errorMessage: data.errorMessage, + }; + } catch { + // Older gateway versions may not return JSON with Twilio details + return {}; + } } diff --git a/gateway/src/http/routes/sms-deliver.test.ts b/gateway/src/http/routes/sms-deliver.test.ts index eeb24c1afa9..d18078ab828 100644 --- a/gateway/src/http/routes/sms-deliver.test.ts +++ b/gateway/src/http/routes/sms-deliver.test.ts @@ -64,9 +64,9 @@ afterEach(() => { globalThis.fetch = originalFetch; }); -function mockTwilioApi() { +function mockTwilioApi(overrides?: Record) { globalThis.fetch = mock(async () => { - return new Response(JSON.stringify({ sid: "SM-sent" }), { + return new Response(JSON.stringify({ sid: "SM-sent", status: "queued", error_code: null, error_message: null, ...overrides }), { status: 201, headers: { "content-type": "application/json" }, }); @@ -371,6 +371,38 @@ describe("/deliver/sms", () => { ); }); + it("returns enriched Twilio acceptance details in response", async () => { + mockTwilioApi({ sid: "SM-enrich-test", status: "queued", error_code: null, error_message: null }); + const handler = createSmsDeliverHandler( + makeConfig({ runtimeProxyBearerToken: undefined, smsDeliverAuthBypass: true }), + ); + const req = makeRequest({ to: "+15559876543", text: "enriched" }); + const res = await handler(req); + expect(res.status).toBe(200); + const body = await res.json(); + expect(body.ok).toBe(true); + expect(body.messageSid).toBe("SM-enrich-test"); + expect(body.status).toBe("queued"); + expect(body.errorCode).toBeNull(); + expect(body.errorMessage).toBeNull(); + }); + + it("returns Twilio error details in response when error_code is present", async () => { + mockTwilioApi({ sid: "SM-err-test", status: "failed", error_code: 30003, error_message: "Unreachable" }); + const handler = createSmsDeliverHandler( + makeConfig({ runtimeProxyBearerToken: undefined, smsDeliverAuthBypass: true }), + ); + const req = makeRequest({ to: "+15559876543", text: "fail test" }); + const res = await handler(req); + expect(res.status).toBe(200); + const body = await res.json(); + expect(body.ok).toBe(true); + expect(body.messageSid).toBe("SM-err-test"); + expect(body.status).toBe("failed"); + expect(body.errorCode).toBe("30003"); + expect(body.errorMessage).toBe("Unreachable"); + }); + it("returns 503 when no From number is available", async () => { const handler = createSmsDeliverHandler( makeConfig({ diff --git a/gateway/src/http/routes/sms-deliver.ts b/gateway/src/http/routes/sms-deliver.ts index 0fa0423f3f3..087a4f5ef61 100644 --- a/gateway/src/http/routes/sms-deliver.ts +++ b/gateway/src/http/routes/sms-deliver.ts @@ -4,8 +4,19 @@ import { getLogger } from "../../logger.js"; const log = getLogger("sms-deliver"); +/** Parsed subset of the Twilio Messages API response. */ +export interface TwilioSmsResult { + sid: string; + status: string; + errorCode: string | null; + errorMessage: string | null; +} + /** * Send an SMS message via the Twilio Messages API. + * + * Returns the Twilio acceptance details so callers can distinguish + * "accepted for delivery" from "confirmed delivered to handset". */ async function sendTwilioSms( accountSid: string, @@ -13,7 +24,7 @@ async function sendTwilioSms( from: string, to: string, body: string, -): Promise { +): Promise { const url = `https://api.twilio.com/2010-04-01/Accounts/${accountSid}/Messages.json`; const params = new URLSearchParams({ From: from, To: to, Body: body }); const authHeader = @@ -29,9 +40,30 @@ async function sendTwilioSms( }); if (!response.ok) { - const text = await response.text(); - throw new Error(`Twilio Messages API error ${response.status}: ${text}`); + // Try to parse structured error details from the Twilio error body + let errorText: string; + try { + const errBody = await response.json() as Record; + errorText = `Twilio Messages API error ${response.status}: code=${errBody.code ?? "unknown"} message=${errBody.message ?? "unknown"}`; + } catch { + errorText = `Twilio Messages API error ${response.status}: ${await response.text().catch(() => "")}`; + } + throw new Error(errorText); } + + const data = (await response.json()) as { + sid?: string; + status?: string; + error_code?: number | null; + error_message?: string | null; + }; + + return { + sid: data.sid ?? "", + status: data.status ?? "unknown", + errorCode: data.error_code != null ? String(data.error_code) : null, + errorMessage: data.error_message ?? null, + }; } function resolveFromNumber( @@ -130,8 +162,9 @@ export function createSmsDeliverHandler(config: GatewayConfig) { ); } + let result: TwilioSmsResult; try { - await sendTwilioSms( + result = await sendTwilioSms( config.twilioAccountSid, config.twilioAuthToken, from, @@ -143,7 +176,13 @@ export function createSmsDeliverHandler(config: GatewayConfig) { return Response.json({ error: "SMS delivery failed" }, { status: 502 }); } - tlog.info({ to, textLength: effectiveText.length }, "SMS delivered"); - return Response.json({ ok: true }); + tlog.info({ to, textLength: effectiveText.length, messageSid: result.sid, status: result.status }, "SMS accepted by Twilio"); + return Response.json({ + ok: true, + messageSid: result.sid, + status: result.status, + errorCode: result.errorCode ?? null, + errorMessage: result.errorMessage ?? null, + }); }; }