@vellumai/assistant 0.3.28 → 0.4.1
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 +33 -3
- package/bun.lock +4 -1
- package/docs/trusted-contact-access.md +9 -2
- package/package.json +6 -3
- package/scripts/ipc/generate-swift.ts +3 -3
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +80 -0
- package/src/__tests__/agent-loop-thinking.test.ts +1 -1
- package/src/__tests__/approval-routes-http.test.ts +13 -5
- package/src/__tests__/asset-materialize-tool.test.ts +2 -0
- package/src/__tests__/asset-search-tool.test.ts +2 -0
- package/src/__tests__/assistant-events-sse-hardening.test.ts +4 -2
- package/src/__tests__/attachments-store.test.ts +2 -0
- package/src/__tests__/browser-skill-endstate.test.ts +3 -3
- package/src/__tests__/call-controller.test.ts +30 -29
- package/src/__tests__/call-routes-http.test.ts +34 -32
- package/src/__tests__/call-start-guardian-guard.test.ts +2 -0
- package/src/__tests__/channel-invite-transport.test.ts +6 -6
- package/src/__tests__/channel-reply-delivery.test.ts +19 -0
- package/src/__tests__/channel-retry-sweep.test.ts +130 -0
- package/src/__tests__/clarification-resolver.test.ts +2 -0
- package/src/__tests__/claude-code-skill-regression.test.ts +2 -0
- package/src/__tests__/claude-code-tool-profiles.test.ts +2 -0
- package/src/__tests__/commit-message-enrichment-service.test.ts +9 -1
- package/src/__tests__/computer-use-session-lifecycle.test.ts +2 -0
- package/src/__tests__/computer-use-session-working-dir.test.ts +1 -0
- package/src/__tests__/computer-use-skill-lifecycle-cleanup.test.ts +2 -0
- package/src/__tests__/config-schema.test.ts +5 -5
- package/src/__tests__/config-watcher.test.ts +3 -1
- package/src/__tests__/connection-policy.test.ts +14 -5
- package/src/__tests__/contacts-tools.test.ts +3 -1
- package/src/__tests__/contradiction-checker.test.ts +2 -0
- package/src/__tests__/conversation-pairing.test.ts +10 -0
- package/src/__tests__/conversation-routes.test.ts +1 -1
- package/src/__tests__/credential-security-invariants.test.ts +16 -6
- package/src/__tests__/credential-vault-unit.test.ts +2 -2
- package/src/__tests__/credential-vault.test.ts +5 -4
- package/src/__tests__/daemon-lifecycle.test.ts +9 -0
- package/src/__tests__/daemon-server-session-init.test.ts +27 -0
- package/src/__tests__/elevenlabs-config.test.ts +2 -0
- package/src/__tests__/encrypted-store.test.ts +10 -5
- package/src/__tests__/followup-tools.test.ts +3 -1
- package/src/__tests__/gateway-only-enforcement.test.ts +21 -21
- package/src/__tests__/gmail-integration.test.ts +0 -1
- package/src/__tests__/guardian-control-plane-policy.test.ts +25 -21
- package/src/__tests__/guardian-dispatch.test.ts +2 -0
- package/src/__tests__/guardian-grant-minting.test.ts +68 -1
- package/src/__tests__/guardian-outbound-http.test.ts +12 -9
- package/src/__tests__/guardian-routing-invariants.test.ts +138 -0
- package/src/__tests__/handle-user-message-secret-resume.test.ts +1 -0
- package/src/__tests__/handlers-slack-config.test.ts +3 -1
- package/src/__tests__/handlers-telegram-config.test.ts +3 -1
- package/src/__tests__/handlers-twilio-config.test.ts +3 -1
- package/src/__tests__/handlers-twitter-config.test.ts +3 -1
- package/src/__tests__/handlers-user-message-approval-consumption.test.ts +318 -0
- package/src/__tests__/heartbeat-service.test.ts +20 -0
- package/src/__tests__/inbound-invite-redemption.test.ts +33 -0
- package/src/__tests__/ingress-reconcile.test.ts +3 -1
- package/src/__tests__/ingress-routes-http.test.ts +231 -4
- package/src/__tests__/intent-routing.test.ts +2 -0
- package/src/__tests__/ipc-snapshot.test.ts +13 -0
- package/src/__tests__/media-generate-image.test.ts +21 -0
- package/src/__tests__/media-reuse-story.e2e.test.ts +2 -0
- package/src/__tests__/memory-regressions.test.ts +20 -20
- package/src/__tests__/non-member-access-request.test.ts +183 -9
- package/src/__tests__/notification-decision-fallback.test.ts +2 -0
- package/src/__tests__/notification-decision-strategy.test.ts +61 -0
- package/src/__tests__/notification-guardian-path.test.ts +2 -0
- package/src/__tests__/oauth-connect-handler.test.ts +3 -1
- package/src/__tests__/oauth2-gateway-transport.test.ts +2 -0
- package/src/__tests__/onboarding-starter-tasks.test.ts +4 -4
- package/src/__tests__/pairing-routes.test.ts +171 -0
- package/src/__tests__/playbook-execution.test.ts +3 -1
- package/src/__tests__/playbook-tools.test.ts +3 -1
- package/src/__tests__/provider-error-scenarios.test.ts +59 -8
- package/src/__tests__/proxy-approval-callback.test.ts +2 -0
- package/src/__tests__/recording-handler.test.ts +11 -0
- package/src/__tests__/recording-intent-handler.test.ts +15 -0
- package/src/__tests__/recording-state-machine.test.ts +13 -2
- package/src/__tests__/registry.test.ts +7 -3
- package/src/__tests__/relay-server.test.ts +148 -28
- package/src/__tests__/runtime-attachment-metadata.test.ts +4 -2
- package/src/__tests__/runtime-events-sse-parity.test.ts +21 -0
- package/src/__tests__/runtime-events-sse.test.ts +4 -2
- package/src/__tests__/sandbox-diagnostics.test.ts +2 -0
- package/src/__tests__/schedule-tools.test.ts +3 -1
- package/src/__tests__/send-endpoint-busy.test.ts +288 -0
- package/src/__tests__/session-abort-tool-results.test.ts +23 -0
- package/src/__tests__/session-agent-loop.test.ts +16 -0
- package/src/__tests__/session-conflict-gate.test.ts +21 -0
- package/src/__tests__/session-load-history-repair.test.ts +27 -17
- package/src/__tests__/session-pre-run-repair.test.ts +23 -0
- package/src/__tests__/session-profile-injection.test.ts +21 -0
- package/src/__tests__/session-provider-retry-repair.test.ts +20 -0
- package/src/__tests__/session-queue.test.ts +23 -0
- package/src/__tests__/session-runtime-assembly.test.ts +50 -12
- package/src/__tests__/session-skill-tools.test.ts +27 -5
- package/src/__tests__/session-slash-known.test.ts +23 -0
- package/src/__tests__/session-slash-queue.test.ts +23 -0
- package/src/__tests__/session-slash-unknown.test.ts +23 -0
- package/src/__tests__/session-workspace-cache-state.test.ts +7 -0
- package/src/__tests__/session-workspace-injection.test.ts +21 -0
- package/src/__tests__/session-workspace-tool-tracking.test.ts +21 -0
- package/src/__tests__/shell-credential-ref.test.ts +2 -0
- package/src/__tests__/skill-feature-flags-integration.test.ts +6 -6
- package/src/__tests__/skill-load-feature-flag.test.ts +5 -4
- package/src/__tests__/skill-projection-feature-flag.test.ts +22 -0
- package/src/__tests__/skills.test.ts +8 -4
- package/src/__tests__/slack-channel-config.test.ts +3 -1
- package/src/__tests__/subagent-tools.test.ts +19 -0
- package/src/__tests__/swarm-recursion.test.ts +2 -0
- package/src/__tests__/swarm-session-integration.test.ts +2 -0
- package/src/__tests__/swarm-tool.test.ts +2 -0
- package/src/__tests__/system-prompt.test.ts +3 -1
- package/src/__tests__/task-compiler.test.ts +3 -1
- package/src/__tests__/task-management-tools.test.ts +3 -1
- package/src/__tests__/task-tools.test.ts +3 -1
- package/src/__tests__/terminal-sandbox.test.ts +13 -12
- package/src/__tests__/terminal-tools.test.ts +2 -0
- package/src/__tests__/tool-approval-handler.test.ts +15 -15
- package/src/__tests__/tool-execution-abort-cleanup.test.ts +2 -0
- package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +2 -0
- package/src/__tests__/tool-grant-request-escalation.test.ts +7 -7
- package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +48 -0
- package/src/__tests__/trusted-contact-multichannel.test.ts +22 -19
- package/src/__tests__/trusted-contact-verification.test.ts +91 -0
- package/src/__tests__/twilio-routes-elevenlabs.test.ts +2 -0
- package/src/__tests__/twitter-auth-handler.test.ts +3 -1
- package/src/__tests__/twitter-cli-routing.test.ts +3 -1
- package/src/__tests__/view-image-tool.test.ts +3 -1
- package/src/__tests__/voice-invite-redemption.test.ts +329 -0
- package/src/__tests__/voice-scoped-grant-consumer.test.ts +7 -5
- package/src/__tests__/voice-session-bridge.test.ts +10 -10
- package/src/__tests__/work-item-output.test.ts +3 -1
- package/src/__tests__/workspace-lifecycle.test.ts +13 -2
- package/src/calls/call-controller.ts +26 -23
- package/src/calls/guardian-action-sweep.ts +10 -2
- package/src/calls/relay-server.ts +216 -27
- package/src/calls/types.ts +1 -1
- package/src/calls/voice-session-bridge.ts +3 -3
- package/src/cli.ts +12 -0
- package/src/config/agent-schema.ts +14 -3
- package/src/config/calls-schema.ts +6 -6
- package/src/config/core-schema.ts +3 -3
- package/src/config/feature-flag-registry.json +8 -0
- package/src/config/mcp-schema.ts +1 -1
- package/src/config/memory-schema.ts +27 -19
- package/src/config/schema.ts +21 -21
- package/src/config/skills-schema.ts +7 -7
- package/src/config/vellum-skills/trusted-contacts/SKILL.md +139 -16
- package/src/daemon/handlers/config-inbox.ts +4 -4
- package/src/daemon/handlers/sessions.ts +148 -4
- package/src/daemon/ipc-contract/messages.ts +16 -0
- package/src/daemon/ipc-contract-inventory.json +1 -0
- package/src/daemon/lifecycle.ts +19 -0
- package/src/daemon/pairing-store.ts +86 -3
- package/src/daemon/response-tier.ts +6 -5
- package/src/daemon/session-agent-loop.ts +5 -5
- package/src/daemon/session-lifecycle.ts +25 -17
- package/src/daemon/session-memory.ts +2 -2
- package/src/daemon/session-process.ts +1 -20
- package/src/daemon/session-runtime-assembly.ts +28 -22
- package/src/daemon/session-tool-setup.ts +2 -2
- package/src/daemon/session.ts +3 -3
- package/src/memory/canonical-guardian-store.ts +63 -1
- package/src/memory/channel-guardian-store.ts +1 -0
- package/src/memory/conversation-crud.ts +7 -7
- package/src/memory/db-init.ts +4 -0
- package/src/memory/embedding-local.ts +257 -39
- package/src/memory/embedding-runtime-manager.ts +471 -0
- package/src/memory/guardian-bindings.ts +25 -1
- package/src/memory/indexer.ts +3 -3
- package/src/memory/ingress-invite-store.ts +45 -0
- package/src/memory/job-handlers/backfill.ts +16 -9
- package/src/memory/migrations/037-voice-invite-columns.ts +16 -0
- package/src/memory/migrations/index.ts +1 -0
- package/src/memory/qdrant-client.ts +31 -22
- package/src/memory/schema.ts +4 -0
- package/src/notifications/copy-composer.ts +15 -0
- package/src/runtime/access-request-helper.ts +43 -7
- package/src/runtime/actor-trust-resolver.ts +46 -50
- package/src/runtime/channel-invite-transports/voice.ts +58 -0
- package/src/runtime/channel-retry-sweep.ts +18 -6
- package/src/runtime/guardian-context-resolver.ts +38 -96
- package/src/runtime/guardian-reply-router.ts +31 -1
- package/src/runtime/ingress-service.ts +80 -3
- package/src/runtime/invite-redemption-service.ts +141 -2
- package/src/runtime/routes/channel-route-shared.ts +1 -1
- package/src/runtime/routes/channel-routes.ts +1 -1
- package/src/runtime/routes/conversation-routes.ts +166 -2
- package/src/runtime/routes/guardian-approval-interception.ts +17 -6
- package/src/runtime/routes/inbound-message-handler.ts +41 -10
- package/src/runtime/routes/ingress-routes.ts +52 -4
- package/src/runtime/routes/pairing-routes.ts +3 -0
- package/src/tools/guardian-control-plane-policy.ts +2 -2
- package/src/tools/reminder/reminder-store.ts +10 -14
- package/src/tools/tool-approval-handler.ts +11 -11
- package/src/tools/types.ts +2 -2
- package/src/util/logger.ts +20 -8
- package/src/util/platform.ts +10 -0
- package/src/util/voice-code.ts +29 -0
- package/src/daemon/guardian-invite-intent.ts +0 -124
package/ARCHITECTURE.md
CHANGED
|
@@ -392,26 +392,56 @@ A complementary access-granting flow where the guardian proactively creates a sh
|
|
|
392
392
|
|
|
393
393
|
**Inbound intercept points:** Invite token extraction runs early in the inbound handler, before ACL denial, so valid invites short-circuit the membership check. Two intercept branches handle: (a) non-members — the invite creates their first member record; (b) inactive members (revoked/pending) — the invite reactivates them.
|
|
394
394
|
|
|
395
|
-
**
|
|
395
|
+
**Channel adapter status:**
|
|
396
396
|
|
|
397
397
|
| Channel | Status | Prerequisites |
|
|
398
398
|
|---------|--------|--------------|
|
|
399
399
|
| Telegram | Shipped | Bot username resolved from credential metadata or `TELEGRAM_BOT_USERNAME` env |
|
|
400
|
+
| Voice | Shipped | Identity-bound voice code redemption via DTMF/speech in the relay state machine. Gated behind `feature_flags.voice-invite-redemption.enabled` (default OFF). |
|
|
400
401
|
| SMS | Deferred | Needs a deep-link strategy compatible with SMS (short URL or web redemption page) |
|
|
401
402
|
| Slack | Deferred | Needs DM-safe ingress — Socket Mode handles channel messages but DM-initiated invite flows need routing |
|
|
402
|
-
|
|
403
|
+
|
|
404
|
+
### Voice Invite Flow (invite_redemption_pending)
|
|
405
|
+
|
|
406
|
+
Voice invites use a short numeric code (4-10 digits, default 6) instead of a URL token. The guardian creates an invite bound to the invitee's E.164 phone number; the invitee redeems it by entering the code during an inbound voice call.
|
|
407
|
+
|
|
408
|
+
**Creation flow:**
|
|
409
|
+
1. Guardian creates a voice invite via `POST /v1/ingress/invites` with `sourceChannel: "voice"` and `expectedExternalUserId` (E.164 phone).
|
|
410
|
+
2. `ingress-service.ts` generates a cryptographically random numeric code (`generateVoiceCode`), hashes it with SHA-256 (`hashVoiceCode`), and stores only the hash.
|
|
411
|
+
3. The one-time plaintext `voiceCode` is returned in the creation response. The raw token is NOT returned for voice invites — redemption uses the identity-bound code flow exclusively.
|
|
412
|
+
4. Guardian communicates the code to the invitee out-of-band.
|
|
413
|
+
|
|
414
|
+
**Call-time redemption subflow (`invite_redemption_pending`):**
|
|
415
|
+
1. Unknown caller dials in. `relay-server.ts` resolves trust via `resolveActorTrust`. Caller is `unknown`, no pending guardian challenge.
|
|
416
|
+
2. If `feature_flags.voice-invite-redemption.enabled` is ON, the relay checks `findActiveVoiceInvites` for invites bound to the caller's phone number.
|
|
417
|
+
3. If active, non-expired invites exist, the relay enters the `invite_redemption_pending` state (reuses the `verification_pending` connection state) and prompts the caller to enter their invite code via DTMF or speech.
|
|
418
|
+
4. `redeemVoiceInviteCode` validates: identity match, code hash match, expiry, use count. On success, an active member record is upserted and the call transitions to the normal call flow.
|
|
419
|
+
5. On failure, the caller gets up to 3 attempts. After max attempts, the call is terminated.
|
|
420
|
+
|
|
421
|
+
**Security invariants:**
|
|
422
|
+
- The plaintext voice code is returned exactly once at creation time and never stored.
|
|
423
|
+
- Voice invites are identity-bound: `expectedExternalUserId` must match the caller's E.164 number. An attacker with the code but the wrong phone number cannot redeem.
|
|
424
|
+
- Failure responses are intentionally generic (`invalid_or_expired`) to prevent oracle attacks.
|
|
425
|
+
- Blocked members cannot bypass the guardian's explicit block via invite redemption.
|
|
426
|
+
|
|
427
|
+
**Feature flag:** `feature_flags.voice-invite-redemption.enabled` (default OFF). When disabled, unknown callers with active voice invites are denied normally — the invite check is skipped entirely.
|
|
403
428
|
|
|
404
429
|
**Key source files:**
|
|
405
430
|
|
|
406
431
|
| File | Purpose |
|
|
407
432
|
|------|---------|
|
|
408
|
-
| `src/runtime/invite-redemption-service.ts` | Core redemption engine — token validation, member creation, discriminated-union outcomes |
|
|
433
|
+
| `src/runtime/invite-redemption-service.ts` | Core redemption engine — token validation, voice code redemption, member creation, discriminated-union outcomes |
|
|
409
434
|
| `src/runtime/invite-redemption-templates.ts` | Deterministic reply templates for each redemption outcome |
|
|
410
435
|
| `src/runtime/channel-invite-transport.ts` | Transport adapter registry with `buildShareableInvite` / `extractInboundToken` interface |
|
|
411
436
|
| `src/runtime/channel-invite-transports/telegram.ts` | Telegram adapter — `t.me/<bot>?start=iv_<token>` deep links, `/start iv_<token>` extraction |
|
|
437
|
+
| `src/runtime/channel-invite-transports/voice.ts` | Voice transport adapter — code-based redemption metadata |
|
|
412
438
|
| `src/daemon/guardian-invite-intent.ts` | Intent detection — routes create/list/revoke requests into the trusted-contacts skill |
|
|
413
439
|
| `src/runtime/ingress-service.ts` | Shared business logic for invite/member operations (used by both HTTP routes and IPC) |
|
|
440
|
+
| `src/runtime/routes/ingress-routes.ts` | HTTP API handlers for member/invite management including voice invite creation and redemption |
|
|
414
441
|
| `src/runtime/routes/inbound-message-handler.ts` | Invite token intercept in the inbound flow (non-member and inactive-member branches) |
|
|
442
|
+
| `src/calls/relay-server.ts` | Voice relay state machine — `invite_redemption_pending` subflow, feature flag gate |
|
|
443
|
+
| `src/util/voice-code.ts` | Cryptographic voice code generation and SHA-256 hashing |
|
|
444
|
+
| `src/memory/ingress-invite-store.ts` | Invite persistence including `findActiveVoiceInvites` for identity-bound lookup |
|
|
415
445
|
|
|
416
446
|
### Update Bulletin System
|
|
417
447
|
|
package/bun.lock
CHANGED
|
@@ -8,7 +8,6 @@
|
|
|
8
8
|
"@anthropic-ai/claude-agent-sdk": "^0.2.42",
|
|
9
9
|
"@anthropic-ai/sdk": "^0.39.0",
|
|
10
10
|
"@google/genai": "^1.40.0",
|
|
11
|
-
"@huggingface/transformers": "^3.8.1",
|
|
12
11
|
"@modelcontextprotocol/sdk": "^1.15.1",
|
|
13
12
|
"@qdrant/js-client-rest": "^1.16.2",
|
|
14
13
|
"@sentry/node": "^10.38.0",
|
|
@@ -35,6 +34,7 @@
|
|
|
35
34
|
"zod": "^4.3.6",
|
|
36
35
|
},
|
|
37
36
|
"devDependencies": {
|
|
37
|
+
"@huggingface/transformers": "^3.8.1",
|
|
38
38
|
"@pydantic/logfire-node": "^0.13.0",
|
|
39
39
|
"@types/archiver": "^7.0.0",
|
|
40
40
|
"@types/bun": "^1.2.4",
|
|
@@ -46,6 +46,7 @@
|
|
|
46
46
|
"eslint-plugin-simple-import-sort": "^12.1.1",
|
|
47
47
|
"fast-check": "^4.5.3",
|
|
48
48
|
"knip": "^5.83.1",
|
|
49
|
+
"prettier": "^3.8.1",
|
|
49
50
|
"quicktype-core": "^23.2.6",
|
|
50
51
|
"typescript": "^5.7.3",
|
|
51
52
|
"typescript-eslint": "^8.54.0",
|
|
@@ -1136,6 +1137,8 @@
|
|
|
1136
1137
|
|
|
1137
1138
|
"prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="],
|
|
1138
1139
|
|
|
1140
|
+
"prettier": ["prettier@3.8.1", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg=="],
|
|
1141
|
+
|
|
1139
1142
|
"process": ["process@0.11.10", "", {}, "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A=="],
|
|
1140
1143
|
|
|
1141
1144
|
"process-nextick-args": ["process-nextick-args@2.0.1", "", {}, "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="],
|
|
@@ -13,8 +13,15 @@ Design doc defining how unknown users gain access to a Vellum assistant via chan
|
|
|
13
13
|
## User Journey
|
|
14
14
|
|
|
15
15
|
1. **Unknown user messages the assistant** on Telegram (or SMS, or any channel).
|
|
16
|
-
2. **Assistant rejects the message** via the ingress ACL in `inbound-message-handler.ts`. The user has no `assistant_ingress_members` record, so the handler replies: *"
|
|
17
|
-
3. **Notification pipeline alerts the guardian.** The rejection triggers `emitNotificationSignal()` with `sourceEventName: 'ingress.access_request'
|
|
16
|
+
2. **Assistant rejects the message** via the ingress ACL in `inbound-message-handler.ts`. The user has no `assistant_ingress_members` record, so the handler replies: *"Hmm looks like you don't have access to talk to me. I'll let them know you tried talking to me and get back to you."* and returns `{ denied: true, reason: 'not_a_member' }`.
|
|
17
|
+
3. **Notification pipeline alerts the guardian.** The rejection triggers `notifyGuardianOfAccessRequest()` which creates a canonical access request and calls `emitNotificationSignal()` with `sourceEventName: 'ingress.access_request'`. The notification routes through the decision engine to all connected channels (vellum macOS app, Telegram, etc.). The guardian sees who is requesting access, including a request code for approve/reject and an `open invite flow` option to start the Trusted Contacts invite flow.
|
|
18
|
+
|
|
19
|
+
**Guardian binding resolution for access requests** uses a fallback strategy:
|
|
20
|
+
1. Source-channel active binding first (e.g., Telegram binding for a Telegram access request).
|
|
21
|
+
2. Any active binding for the assistant on another channel (deterministic: most recently verified first, then alphabetical by channel).
|
|
22
|
+
3. No guardian identity — the notification pipeline delivers via trusted/vellum channels even when no channel binding exists.
|
|
23
|
+
|
|
24
|
+
This ensures unknown inbound access attempts always trigger guardian notification, even when the requester's source channel has no guardian binding.
|
|
18
25
|
4. **Guardian approves the request.** The guardian responds to the notification (via Telegram inline button, macOS app, or IPC). On approval, the assistant creates a verification session via `createOutboundSession()` and generates a 6-digit verification code.
|
|
19
26
|
5. **Guardian receives the verification code.** The assistant delivers the code to the guardian's verified channel (Telegram chat, SMS, etc.).
|
|
20
27
|
6. **Guardian gives the code to the requester out-of-band** (in person, text message, phone call, etc.). This out-of-band transfer is the trust anchor: it proves the requester has a real-world relationship with the guardian.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vellumai/assistant",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"vellum": "./src/index.ts"
|
|
@@ -14,6 +14,8 @@
|
|
|
14
14
|
"ipc:inventory:update": "bun run scripts/ipc/check-contract-inventory.ts --update",
|
|
15
15
|
"generate:ipc": "bun run scripts/ipc/generate-swift.ts",
|
|
16
16
|
"check:ipc-generated": "bun run scripts/ipc/generate-swift.ts --check",
|
|
17
|
+
"format": "prettier --write .",
|
|
18
|
+
"format:check": "prettier --check .",
|
|
17
19
|
"ipc:check-swift-drift": "bun run scripts/ipc/check-swift-decoder-drift.ts",
|
|
18
20
|
"lint": "eslint",
|
|
19
21
|
"typecheck": "bunx tsc --noEmit",
|
|
@@ -28,7 +30,6 @@
|
|
|
28
30
|
"@anthropic-ai/sdk": "^0.39.0",
|
|
29
31
|
"@google/genai": "^1.40.0",
|
|
30
32
|
"@modelcontextprotocol/sdk": "^1.15.1",
|
|
31
|
-
"@huggingface/transformers": "^3.8.1",
|
|
32
33
|
"@qdrant/js-client-rest": "^1.16.2",
|
|
33
34
|
"@sentry/node": "^10.38.0",
|
|
34
35
|
"agentmail": "^0.1.0",
|
|
@@ -65,9 +66,11 @@
|
|
|
65
66
|
"eslint-plugin-simple-import-sort": "^12.1.1",
|
|
66
67
|
"fast-check": "^4.5.3",
|
|
67
68
|
"knip": "^5.83.1",
|
|
69
|
+
"prettier": "^3.8.1",
|
|
68
70
|
"quicktype-core": "^23.2.6",
|
|
69
71
|
"typescript": "^5.7.3",
|
|
70
72
|
"typescript-eslint": "^8.54.0",
|
|
71
|
-
"typescript-json-schema": "^0.67.1"
|
|
73
|
+
"typescript-json-schema": "^0.67.1",
|
|
74
|
+
"@huggingface/transformers": "^3.8.1"
|
|
72
75
|
}
|
|
73
76
|
}
|
|
@@ -94,9 +94,9 @@ function generateSchemas(): Record<string, SchemaDef> {
|
|
|
94
94
|
|
|
95
95
|
const program = TJS.getProgramFromFiles(contractFiles, {
|
|
96
96
|
strict: true,
|
|
97
|
-
target:
|
|
98
|
-
module:
|
|
99
|
-
moduleResolution:
|
|
97
|
+
target: 'es2022',
|
|
98
|
+
module: 'commonjs',
|
|
99
|
+
moduleResolution: 'node',
|
|
100
100
|
skipLibCheck: true,
|
|
101
101
|
});
|
|
102
102
|
|
|
@@ -1302,6 +1302,15 @@ exports[`IPC message snapshots ServerMessage types message_complete serializes t
|
|
|
1302
1302
|
}
|
|
1303
1303
|
`;
|
|
1304
1304
|
|
|
1305
|
+
exports[`IPC message snapshots ServerMessage types message_request_complete serializes to expected JSON 1`] = `
|
|
1306
|
+
{
|
|
1307
|
+
"requestId": "req-inline-001",
|
|
1308
|
+
"runStillActive": true,
|
|
1309
|
+
"sessionId": "sess-001",
|
|
1310
|
+
"type": "message_request_complete",
|
|
1311
|
+
}
|
|
1312
|
+
`;
|
|
1313
|
+
|
|
1305
1314
|
exports[`IPC message snapshots ServerMessage types session_info serializes to expected JSON 1`] = `
|
|
1306
1315
|
{
|
|
1307
1316
|
"correlationId": "corr-001",
|
|
@@ -3112,3 +3121,74 @@ exports[`IPC message snapshots ServerMessage types generate_avatar_response seri
|
|
|
3112
3121
|
"type": "generate_avatar_response",
|
|
3113
3122
|
}
|
|
3114
3123
|
`;
|
|
3124
|
+
|
|
3125
|
+
exports[`IPC message snapshots ClientMessage types guardian_actions_pending_request serializes to expected JSON 1`] = `
|
|
3126
|
+
{
|
|
3127
|
+
"conversationId": "conv-guardian-001",
|
|
3128
|
+
"type": "guardian_actions_pending_request",
|
|
3129
|
+
}
|
|
3130
|
+
`;
|
|
3131
|
+
|
|
3132
|
+
exports[`IPC message snapshots ClientMessage types guardian_action_decision serializes to expected JSON 1`] = `
|
|
3133
|
+
{
|
|
3134
|
+
"action": "approve_once",
|
|
3135
|
+
"conversationId": "conv-guardian-001",
|
|
3136
|
+
"requestId": "req-guardian-001",
|
|
3137
|
+
"type": "guardian_action_decision",
|
|
3138
|
+
}
|
|
3139
|
+
`;
|
|
3140
|
+
|
|
3141
|
+
exports[`IPC message snapshots ClientMessage types reorder_threads serializes to expected JSON 1`] = `
|
|
3142
|
+
{
|
|
3143
|
+
"type": "reorder_threads",
|
|
3144
|
+
"updates": [
|
|
3145
|
+
{
|
|
3146
|
+
"displayOrder": 0,
|
|
3147
|
+
"isPinned": false,
|
|
3148
|
+
"sessionId": "sess-001",
|
|
3149
|
+
},
|
|
3150
|
+
{
|
|
3151
|
+
"displayOrder": 1,
|
|
3152
|
+
"isPinned": true,
|
|
3153
|
+
"sessionId": "sess-002",
|
|
3154
|
+
},
|
|
3155
|
+
],
|
|
3156
|
+
}
|
|
3157
|
+
`;
|
|
3158
|
+
|
|
3159
|
+
exports[`IPC message snapshots ServerMessage types guardian_actions_pending_response serializes to expected JSON 1`] = `
|
|
3160
|
+
{
|
|
3161
|
+
"conversationId": "conv-guardian-001",
|
|
3162
|
+
"prompts": [
|
|
3163
|
+
{
|
|
3164
|
+
"actions": [
|
|
3165
|
+
{
|
|
3166
|
+
"action": "approve_once",
|
|
3167
|
+
"label": "Approve once",
|
|
3168
|
+
},
|
|
3169
|
+
{
|
|
3170
|
+
"action": "reject",
|
|
3171
|
+
"label": "Reject",
|
|
3172
|
+
},
|
|
3173
|
+
],
|
|
3174
|
+
"callSessionId": null,
|
|
3175
|
+
"conversationId": "conv-guardian-001",
|
|
3176
|
+
"expiresAt": 1700100000000,
|
|
3177
|
+
"questionText": "Approve tool: bash",
|
|
3178
|
+
"requestCode": "REQ-GU",
|
|
3179
|
+
"requestId": "req-guardian-001",
|
|
3180
|
+
"state": "pending",
|
|
3181
|
+
"toolName": "bash",
|
|
3182
|
+
},
|
|
3183
|
+
],
|
|
3184
|
+
"type": "guardian_actions_pending_response",
|
|
3185
|
+
}
|
|
3186
|
+
`;
|
|
3187
|
+
|
|
3188
|
+
exports[`IPC message snapshots ServerMessage types guardian_action_decision_response serializes to expected JSON 1`] = `
|
|
3189
|
+
{
|
|
3190
|
+
"applied": true,
|
|
3191
|
+
"requestId": "req-guardian-001",
|
|
3192
|
+
"type": "guardian_action_decision_response",
|
|
3193
|
+
}
|
|
3194
|
+
`;
|
|
@@ -44,7 +44,7 @@ describe('AgentLoop thinking budget clamping', () => {
|
|
|
44
44
|
const config = lastConfig()!;
|
|
45
45
|
const thinking = config.thinking as { type: string; budget_tokens: number };
|
|
46
46
|
expect(thinking.type).toBe('enabled');
|
|
47
|
-
expect(thinking.budget_tokens).toBe(
|
|
47
|
+
expect(thinking.budget_tokens).toBe(3072); // clamped to floor(maxTokens * 0.75)
|
|
48
48
|
});
|
|
49
49
|
|
|
50
50
|
test('does not clamp when budget_tokens is within max_tokens', async () => {
|
|
@@ -36,6 +36,8 @@ mock.module('../util/logger.js', () => ({
|
|
|
36
36
|
|
|
37
37
|
mock.module('../config/loader.js', () => ({
|
|
38
38
|
getConfig: () => ({
|
|
39
|
+
ui: {},
|
|
40
|
+
|
|
39
41
|
model: 'test',
|
|
40
42
|
provider: 'test',
|
|
41
43
|
apiKeys: {},
|
|
@@ -79,8 +81,11 @@ function makeIdleSession(opts?: {
|
|
|
79
81
|
setAssistantId: () => {},
|
|
80
82
|
setGuardianContext: () => {},
|
|
81
83
|
setCommandIntent: () => {},
|
|
84
|
+
setTurnChannelContext: () => {},
|
|
85
|
+
setTurnInterfaceContext: () => {},
|
|
82
86
|
updateClient: () => {},
|
|
83
87
|
enqueueMessage: () => ({ queued: false, requestId: 'noop' }),
|
|
88
|
+
hasAnyPendingConfirmation: () => false,
|
|
84
89
|
runAgentLoop: async (_content: string, _messageId: string, onEvent: (msg: ServerMessage) => void) => {
|
|
85
90
|
onEvent({ type: 'assistant_text_delta', text: 'Hello!' });
|
|
86
91
|
onEvent({ type: 'message_complete', sessionId: 'test-session' });
|
|
@@ -118,8 +123,11 @@ function makeConfirmationEmittingSession(opts?: {
|
|
|
118
123
|
setAssistantId: () => {},
|
|
119
124
|
setGuardianContext: () => {},
|
|
120
125
|
setCommandIntent: () => {},
|
|
126
|
+
setTurnChannelContext: () => {},
|
|
127
|
+
setTurnInterfaceContext: () => {},
|
|
121
128
|
updateClient: () => {},
|
|
122
129
|
enqueueMessage: () => ({ queued: false, requestId: 'noop' }),
|
|
130
|
+
hasAnyPendingConfirmation: () => false,
|
|
123
131
|
runAgentLoop: async (_content: string, _messageId: string, onEvent: (msg: ServerMessage) => void) => {
|
|
124
132
|
// Emit confirmation_request — this triggers the hub publisher to register
|
|
125
133
|
// the pending interaction
|
|
@@ -549,8 +557,8 @@ describe('standalone approval endpoints — HTTP layer', () => {
|
|
|
549
557
|
});
|
|
550
558
|
|
|
551
559
|
expect(res.status).toBe(403);
|
|
552
|
-
const body = await res.json() as { error: string };
|
|
553
|
-
expect(body.error).toContain('pattern');
|
|
560
|
+
const body = await res.json() as { error: { message: string; code?: string } };
|
|
561
|
+
expect(body.error.message).toContain('pattern');
|
|
554
562
|
|
|
555
563
|
await stopServer();
|
|
556
564
|
});
|
|
@@ -579,8 +587,8 @@ describe('standalone approval endpoints — HTTP layer', () => {
|
|
|
579
587
|
});
|
|
580
588
|
|
|
581
589
|
expect(res.status).toBe(403);
|
|
582
|
-
const body = await res.json() as { error: string };
|
|
583
|
-
expect(body.error).toContain('scope');
|
|
590
|
+
const body = await res.json() as { error: { message: string; code?: string } };
|
|
591
|
+
expect(body.error.message).toContain('scope');
|
|
584
592
|
|
|
585
593
|
await stopServer();
|
|
586
594
|
});
|
|
@@ -637,7 +645,7 @@ describe('standalone approval endpoints — HTTP layer', () => {
|
|
|
637
645
|
const res = await fetch(url('messages'), {
|
|
638
646
|
method: 'POST',
|
|
639
647
|
headers: { 'Content-Type': 'application/json', ...AUTH_HEADERS },
|
|
640
|
-
body: JSON.stringify({ conversationKey: 'conv-auto', content: 'Run ls', sourceChannel: 'vellum' }),
|
|
648
|
+
body: JSON.stringify({ conversationKey: 'conv-auto', content: 'Run ls', sourceChannel: 'vellum', interface: 'macos' }),
|
|
641
649
|
});
|
|
642
650
|
expect(res.status).toBe(202);
|
|
643
651
|
|
|
@@ -37,6 +37,8 @@ mock.module('../util/logger.js', () => ({
|
|
|
37
37
|
|
|
38
38
|
mock.module('../config/loader.js', () => ({
|
|
39
39
|
getConfig: () => ({
|
|
40
|
+
ui: {},
|
|
41
|
+
|
|
40
42
|
model: 'test',
|
|
41
43
|
provider: 'test',
|
|
42
44
|
apiKeys: {},
|
|
@@ -181,8 +183,8 @@ describe('SSE route — capacity limit', () => {
|
|
|
181
183
|
|
|
182
184
|
const response = handleSubscribeAssistantEvents(req, new URL(req.url), { hub });
|
|
183
185
|
expect(response.status).toBe(503);
|
|
184
|
-
const body = await response.json() as { error: string };
|
|
185
|
-
expect(body.error).toMatch(/Too many concurrent connections/);
|
|
186
|
+
const body = await response.json() as { error: { message: string; code?: string } };
|
|
187
|
+
expect(body.error.message).toMatch(/Too many concurrent connections/);
|
|
186
188
|
});
|
|
187
189
|
|
|
188
190
|
test('returns 200 when hub has remaining capacity', () => {
|
|
@@ -64,10 +64,10 @@ describe('browser skill migration end-state', () => {
|
|
|
64
64
|
|
|
65
65
|
test('startup tool definition count is reduced (no browser tools)', () => {
|
|
66
66
|
const definitions = getAllToolDefinitions();
|
|
67
|
-
// Startup has ~
|
|
67
|
+
// Startup has ~15 eager + ~11 explicit definitions (no browser tools).
|
|
68
68
|
// Allow wider drift for unrelated tool additions while still failing if
|
|
69
|
-
// browser tools are reintroduced at startup (+
|
|
70
|
-
expect(definitions.length).toBeGreaterThanOrEqual(
|
|
69
|
+
// browser tools are reintroduced at startup (+14 definitions).
|
|
70
|
+
expect(definitions.length).toBeGreaterThanOrEqual(10);
|
|
71
71
|
expect(definitions.length).toBeLessThanOrEqual(50);
|
|
72
72
|
|
|
73
73
|
const defNames = definitions.map((d) => d.name);
|
|
@@ -32,6 +32,8 @@ mock.module('../util/logger.js', () => ({
|
|
|
32
32
|
|
|
33
33
|
mock.module('../config/loader.js', () => ({
|
|
34
34
|
getConfig: () => ({
|
|
35
|
+
ui: {},
|
|
36
|
+
|
|
35
37
|
provider: 'anthropic',
|
|
36
38
|
providerOrder: ['anthropic'],
|
|
37
39
|
apiKeys: { anthropic: 'test-key' },
|
|
@@ -47,6 +49,7 @@ mock.module('../config/loader.js', () => ({
|
|
|
47
49
|
model: undefined,
|
|
48
50
|
},
|
|
49
51
|
memory: { enabled: false },
|
|
52
|
+
notifications: { decisionModelIntent: 'latency-optimized' },
|
|
50
53
|
}),
|
|
51
54
|
}));
|
|
52
55
|
|
|
@@ -127,11 +130,11 @@ import {
|
|
|
127
130
|
updateCallSession,
|
|
128
131
|
} from '../calls/call-store.js';
|
|
129
132
|
import type { RelayConnection } from '../calls/relay-server.js';
|
|
130
|
-
import { getDb, initializeDb, resetDb } from '../memory/db.js';
|
|
131
133
|
import {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
} from '../memory/guardian-
|
|
134
|
+
getCanonicalGuardianRequest,
|
|
135
|
+
getPendingCanonicalRequestByCallSessionId,
|
|
136
|
+
} from '../memory/canonical-guardian-store.js';
|
|
137
|
+
import { getDb, initializeDb, resetDb } from '../memory/db.js';
|
|
135
138
|
import { conversations } from '../memory/schema.js';
|
|
136
139
|
|
|
137
140
|
initializeDb();
|
|
@@ -192,6 +195,8 @@ function ensureConversation(id: string): void {
|
|
|
192
195
|
|
|
193
196
|
function resetTables() {
|
|
194
197
|
const db = getDb();
|
|
198
|
+
db.run('DELETE FROM canonical_guardian_deliveries');
|
|
199
|
+
db.run('DELETE FROM canonical_guardian_requests');
|
|
195
200
|
db.run('DELETE FROM guardian_action_deliveries');
|
|
196
201
|
db.run('DELETE FROM guardian_action_requests');
|
|
197
202
|
db.run('DELETE FROM call_pending_questions');
|
|
@@ -631,7 +636,7 @@ describe('call-controller', () => {
|
|
|
631
636
|
test('handleCallerUtterance: passes guardian context to startVoiceTurn', async () => {
|
|
632
637
|
const guardianCtx = {
|
|
633
638
|
sourceChannel: 'voice' as const,
|
|
634
|
-
|
|
639
|
+
trustClass: 'trusted_contact' as const,
|
|
635
640
|
guardianExternalUserId: '+15550009999',
|
|
636
641
|
guardianChatId: '+15550009999',
|
|
637
642
|
requesterExternalUserId: '+15550002222',
|
|
@@ -683,13 +688,13 @@ describe('call-controller', () => {
|
|
|
683
688
|
test('setGuardianContext: subsequent turns use updated guardian context', async () => {
|
|
684
689
|
const initialCtx = {
|
|
685
690
|
sourceChannel: 'voice' as const,
|
|
686
|
-
|
|
691
|
+
trustClass: 'unknown' as const,
|
|
687
692
|
denialReason: 'no_binding' as const,
|
|
688
693
|
};
|
|
689
694
|
|
|
690
695
|
const upgradedCtx = {
|
|
691
696
|
sourceChannel: 'voice' as const,
|
|
692
|
-
|
|
697
|
+
trustClass: 'guardian' as const,
|
|
693
698
|
guardianExternalUserId: '+15550003333',
|
|
694
699
|
guardianChatId: '+15550003333',
|
|
695
700
|
};
|
|
@@ -1163,7 +1168,7 @@ describe('call-controller', () => {
|
|
|
1163
1168
|
await new Promise((r) => setTimeout(r, 10));
|
|
1164
1169
|
|
|
1165
1170
|
// Verify a guardian action request was created
|
|
1166
|
-
const pendingRequest =
|
|
1171
|
+
const pendingRequest = getPendingCanonicalRequestByCallSessionId(session.id);
|
|
1167
1172
|
expect(pendingRequest).not.toBeNull();
|
|
1168
1173
|
expect(pendingRequest!.status).toBe('pending');
|
|
1169
1174
|
|
|
@@ -1175,11 +1180,10 @@ describe('call-controller', () => {
|
|
|
1175
1180
|
// Wait for the consultation timeout
|
|
1176
1181
|
await new Promise((r) => setTimeout(r, 200));
|
|
1177
1182
|
|
|
1178
|
-
// The guardian
|
|
1179
|
-
const timedOutRequest =
|
|
1183
|
+
// The canonical guardian request should now be expired
|
|
1184
|
+
const timedOutRequest = getCanonicalGuardianRequest(pendingRequest!.id);
|
|
1180
1185
|
expect(timedOutRequest).not.toBeNull();
|
|
1181
1186
|
expect(timedOutRequest!.status).toBe('expired');
|
|
1182
|
-
expect(timedOutRequest!.expiredReason).toBe('call_timeout');
|
|
1183
1187
|
|
|
1184
1188
|
// Event should be recorded
|
|
1185
1189
|
const events = getCallEvents(session.id);
|
|
@@ -1277,7 +1281,7 @@ describe('call-controller', () => {
|
|
|
1277
1281
|
expect(question!.questionText).toBe('Allow send_email to bob@example.com?');
|
|
1278
1282
|
|
|
1279
1283
|
// Verify the guardian action request has tool metadata
|
|
1280
|
-
const pendingRequest =
|
|
1284
|
+
const pendingRequest = getPendingCanonicalRequestByCallSessionId(session.id);
|
|
1281
1285
|
expect(pendingRequest).not.toBeNull();
|
|
1282
1286
|
expect(pendingRequest!.toolName).toBe('send_email');
|
|
1283
1287
|
expect(pendingRequest!.inputDigest).not.toBeNull();
|
|
@@ -1305,7 +1309,7 @@ describe('call-controller', () => {
|
|
|
1305
1309
|
await controller.handleCallerUtterance('Send it');
|
|
1306
1310
|
await new Promise((r) => setTimeout(r, 50));
|
|
1307
1311
|
|
|
1308
|
-
const request1 =
|
|
1312
|
+
const request1 = getPendingCanonicalRequestByCallSessionId(session.id);
|
|
1309
1313
|
expect(request1).not.toBeNull();
|
|
1310
1314
|
|
|
1311
1315
|
// Compute expected digest independently using the same utility
|
|
@@ -1326,7 +1330,7 @@ describe('call-controller', () => {
|
|
|
1326
1330
|
await new Promise((r) => setTimeout(r, 50));
|
|
1327
1331
|
|
|
1328
1332
|
// Verify the guardian action request has NO tool metadata
|
|
1329
|
-
const pendingRequest =
|
|
1333
|
+
const pendingRequest = getPendingCanonicalRequestByCallSessionId(session.id);
|
|
1330
1334
|
expect(pendingRequest).not.toBeNull();
|
|
1331
1335
|
expect(pendingRequest!.toolName).toBeNull();
|
|
1332
1336
|
expect(pendingRequest!.inputDigest).toBeNull();
|
|
@@ -1384,7 +1388,7 @@ describe('call-controller', () => {
|
|
|
1384
1388
|
expect(question!.questionText).toBe('Allow send_message?');
|
|
1385
1389
|
|
|
1386
1390
|
// Verify tool metadata was parsed correctly
|
|
1387
|
-
const pendingRequest =
|
|
1391
|
+
const pendingRequest = getPendingCanonicalRequestByCallSessionId(session.id);
|
|
1388
1392
|
expect(pendingRequest).not.toBeNull();
|
|
1389
1393
|
expect(pendingRequest!.toolName).toBe('send_message');
|
|
1390
1394
|
expect(pendingRequest!.inputDigest).not.toBeNull();
|
|
@@ -1411,7 +1415,7 @@ describe('call-controller', () => {
|
|
|
1411
1415
|
await controller.handleCallerUtterance('Do something');
|
|
1412
1416
|
await new Promise((r) => setTimeout(r, 50));
|
|
1413
1417
|
|
|
1414
|
-
const pendingRequest =
|
|
1418
|
+
const pendingRequest = getPendingCanonicalRequestByCallSessionId(session.id);
|
|
1415
1419
|
expect(pendingRequest).not.toBeNull();
|
|
1416
1420
|
expect(pendingRequest!.questionText).toBe('Fallback question?');
|
|
1417
1421
|
// Tool metadata should be null since the approval marker was malformed
|
|
@@ -1547,7 +1551,7 @@ describe('call-controller', () => {
|
|
|
1547
1551
|
|
|
1548
1552
|
const firstQuestionId = controller.getPendingConsultationQuestionId();
|
|
1549
1553
|
expect(firstQuestionId).not.toBeNull();
|
|
1550
|
-
const firstRequest =
|
|
1554
|
+
const firstRequest = getPendingCanonicalRequestByCallSessionId(session.id);
|
|
1551
1555
|
expect(firstRequest).not.toBeNull();
|
|
1552
1556
|
|
|
1553
1557
|
// Repeated ASK_GUARDIAN with same informational question (no tool metadata)
|
|
@@ -1559,7 +1563,7 @@ describe('call-controller', () => {
|
|
|
1559
1563
|
|
|
1560
1564
|
// Should coalesce: same consultation ID, same request
|
|
1561
1565
|
expect(controller.getPendingConsultationQuestionId()).toBe(firstQuestionId);
|
|
1562
|
-
const currentRequest =
|
|
1566
|
+
const currentRequest = getPendingCanonicalRequestByCallSessionId(session.id);
|
|
1563
1567
|
expect(currentRequest).not.toBeNull();
|
|
1564
1568
|
expect(currentRequest!.id).toBe(firstRequest!.id);
|
|
1565
1569
|
expect(currentRequest!.status).toBe('pending');
|
|
@@ -1589,7 +1593,7 @@ describe('call-controller', () => {
|
|
|
1589
1593
|
|
|
1590
1594
|
const firstQuestionId = controller.getPendingConsultationQuestionId();
|
|
1591
1595
|
expect(firstQuestionId).not.toBeNull();
|
|
1592
|
-
const firstRequest =
|
|
1596
|
+
const firstRequest = getPendingCanonicalRequestByCallSessionId(session.id);
|
|
1593
1597
|
expect(firstRequest).not.toBeNull();
|
|
1594
1598
|
|
|
1595
1599
|
// Repeated ASK_GUARDIAN_APPROVAL with same tool/input
|
|
@@ -1601,7 +1605,7 @@ describe('call-controller', () => {
|
|
|
1601
1605
|
|
|
1602
1606
|
// Should coalesce: same consultation, same request
|
|
1603
1607
|
expect(controller.getPendingConsultationQuestionId()).toBe(firstQuestionId);
|
|
1604
|
-
const currentRequest =
|
|
1608
|
+
const currentRequest = getPendingCanonicalRequestByCallSessionId(session.id);
|
|
1605
1609
|
expect(currentRequest!.id).toBe(firstRequest!.id);
|
|
1606
1610
|
expect(currentRequest!.status).toBe('pending');
|
|
1607
1611
|
|
|
@@ -1623,7 +1627,7 @@ describe('call-controller', () => {
|
|
|
1623
1627
|
await controller.handleCallerUtterance('Send email');
|
|
1624
1628
|
await new Promise((r) => setTimeout(r, 50));
|
|
1625
1629
|
|
|
1626
|
-
const firstRequest =
|
|
1630
|
+
const firstRequest = getPendingCanonicalRequestByCallSessionId(session.id);
|
|
1627
1631
|
expect(firstRequest).not.toBeNull();
|
|
1628
1632
|
expect(firstRequest!.toolName).toBe('send_email');
|
|
1629
1633
|
|
|
@@ -1640,18 +1644,15 @@ describe('call-controller', () => {
|
|
|
1640
1644
|
await new Promise((r) => setTimeout(r, 100));
|
|
1641
1645
|
|
|
1642
1646
|
// New consultation should be active
|
|
1643
|
-
const secondRequest =
|
|
1647
|
+
const secondRequest = getPendingCanonicalRequestByCallSessionId(session.id);
|
|
1644
1648
|
expect(secondRequest).not.toBeNull();
|
|
1645
1649
|
expect(secondRequest!.id).not.toBe(firstRequest!.id);
|
|
1646
1650
|
expect(secondRequest!.toolName).toBe('calendar_create');
|
|
1647
1651
|
|
|
1648
|
-
// Old request should be expired
|
|
1649
|
-
const expiredRequest =
|
|
1652
|
+
// Old request should be expired (superseded by the new one)
|
|
1653
|
+
const expiredRequest = getCanonicalGuardianRequest(firstRequest!.id);
|
|
1650
1654
|
expect(expiredRequest).not.toBeNull();
|
|
1651
1655
|
expect(expiredRequest!.status).toBe('expired');
|
|
1652
|
-
expect(expiredRequest!.expiredReason).toBe('superseded');
|
|
1653
|
-
expect(expiredRequest!.supersededByRequestId).toBe(secondRequest!.id);
|
|
1654
|
-
expect(expiredRequest!.supersededAt).not.toBeNull();
|
|
1655
1656
|
|
|
1656
1657
|
controller.destroy();
|
|
1657
1658
|
});
|
|
@@ -1671,7 +1672,7 @@ describe('call-controller', () => {
|
|
|
1671
1672
|
await controller.handleCallerUtterance('Send email to Bob');
|
|
1672
1673
|
await new Promise((r) => setTimeout(r, 50));
|
|
1673
1674
|
|
|
1674
|
-
const firstRequest =
|
|
1675
|
+
const firstRequest = getPendingCanonicalRequestByCallSessionId(session.id);
|
|
1675
1676
|
expect(firstRequest).not.toBeNull();
|
|
1676
1677
|
expect(firstRequest!.toolName).toBe('send_email');
|
|
1677
1678
|
|
|
@@ -1685,7 +1686,7 @@ describe('call-controller', () => {
|
|
|
1685
1686
|
await new Promise((r) => setTimeout(r, 50));
|
|
1686
1687
|
|
|
1687
1688
|
// Should coalesce: the inherited tool metadata matches the existing consultation
|
|
1688
|
-
const currentRequest =
|
|
1689
|
+
const currentRequest = getPendingCanonicalRequestByCallSessionId(session.id);
|
|
1689
1690
|
expect(currentRequest!.id).toBe(firstRequest!.id);
|
|
1690
1691
|
expect(currentRequest!.status).toBe('pending');
|
|
1691
1692
|
|