@vellumai/assistant 0.3.27 → 0.4.0
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 +81 -4
- package/Dockerfile +2 -2
- package/bun.lock +4 -1
- package/docs/trusted-contact-access.md +9 -2
- package/package.json +6 -3
- package/scripts/ipc/generate-swift.ts +9 -5
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +80 -0
- package/src/__tests__/agent-loop-thinking.test.ts +1 -1
- package/src/__tests__/agent-loop.test.ts +119 -0
- 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__/bundled-asset.test.ts +107 -0
- 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__/canonical-guardian-store.test.ts +636 -0
- package/src/__tests__/channel-approval-routes.test.ts +174 -1
- 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__/emit-signal-routing-intent.test.ts +43 -1
- 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-actions-endpoint.test.ts +205 -345
- package/src/__tests__/guardian-control-plane-policy.test.ts +19 -19
- package/src/__tests__/guardian-decision-primitive-canonical.test.ts +599 -0
- package/src/__tests__/guardian-dispatch.test.ts +21 -19
- 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 +1092 -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__/mcp-cli.test.ts +77 -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 +212 -36
- package/src/__tests__/notification-decision-fallback.test.ts +63 -3
- package/src/__tests__/notification-decision-strategy.test.ts +78 -0
- package/src/__tests__/notification-guardian-path.test.ts +15 -15
- 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__/onboarding-template-contract.test.ts +116 -21
- 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__/secret-scanner-executor.test.ts +59 -0
- package/src/__tests__/secret-scanner.test.ts +8 -0
- package/src/__tests__/send-endpoint-busy.test.ts +4 -0
- package/src/__tests__/sensitive-output-placeholders.test.ts +208 -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 +126 -59
- 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 +497 -0
- 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/agent/loop.ts +46 -3
- package/src/approvals/guardian-decision-primitive.ts +285 -0
- package/src/approvals/guardian-request-resolvers.ts +539 -0
- package/src/calls/call-controller.ts +26 -23
- package/src/calls/guardian-action-sweep.ts +10 -2
- package/src/calls/guardian-dispatch.ts +46 -40
- package/src/calls/relay-server.ts +358 -24
- 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/system-prompt.ts +2 -1
- package/src/config/templates/BOOTSTRAP.md +47 -31
- package/src/config/templates/USER.md +5 -0
- package/src/config/update-bulletin-template-path.ts +4 -1
- package/src/config/vellum-skills/trusted-contacts/SKILL.md +149 -21
- package/src/daemon/handlers/config-inbox.ts +4 -4
- package/src/daemon/handlers/guardian-actions.ts +45 -66
- package/src/daemon/handlers/sessions.ts +148 -4
- package/src/daemon/ipc-contract/guardian-actions.ts +7 -0
- package/src/daemon/ipc-contract/messages.ts +16 -0
- package/src/daemon/ipc-contract-inventory.json +1 -0
- package/src/daemon/lifecycle.ts +22 -16
- package/src/daemon/pairing-store.ts +86 -3
- package/src/daemon/server.ts +18 -0
- package/src/daemon/session-agent-loop-handlers.ts +5 -4
- package/src/daemon/session-agent-loop.ts +33 -6
- package/src/daemon/session-lifecycle.ts +25 -17
- package/src/daemon/session-memory.ts +2 -2
- package/src/daemon/session-process.ts +68 -326
- package/src/daemon/session-runtime-assembly.ts +119 -25
- package/src/daemon/session-tool-setup.ts +3 -2
- package/src/daemon/session.ts +4 -3
- package/src/home-base/prebuilt/seed.ts +2 -1
- package/src/hooks/templates.ts +2 -1
- package/src/memory/canonical-guardian-store.ts +586 -0
- package/src/memory/channel-guardian-store.ts +2 -0
- package/src/memory/conversation-crud.ts +7 -7
- package/src/memory/db-init.ts +20 -0
- package/src/memory/embedding-local.ts +257 -39
- package/src/memory/embedding-runtime-manager.ts +471 -0
- package/src/memory/guardian-action-store.ts +7 -60
- package/src/memory/guardian-approvals.ts +9 -4
- 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/036-normalize-phone-identities.ts +289 -0
- package/src/memory/migrations/037-voice-invite-columns.ts +16 -0
- package/src/memory/migrations/118-reminder-routing-intent.ts +3 -3
- package/src/memory/migrations/121-canonical-guardian-requests.ts +59 -0
- package/src/memory/migrations/122-canonical-guardian-requester-chat-id.ts +15 -0
- package/src/memory/migrations/123-canonical-guardian-deliveries-destination-index.ts +15 -0
- package/src/memory/migrations/index.ts +5 -0
- package/src/memory/migrations/registry.ts +5 -0
- package/src/memory/qdrant-client.ts +31 -22
- package/src/memory/schema-migration.ts +1 -0
- package/src/memory/schema.ts +56 -0
- package/src/notifications/copy-composer.ts +31 -4
- package/src/notifications/decision-engine.ts +57 -0
- package/src/permissions/defaults.ts +2 -0
- package/src/runtime/access-request-helper.ts +173 -0
- package/src/runtime/actor-trust-resolver.ts +221 -0
- package/src/runtime/channel-guardian-service.ts +12 -4
- 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 -71
- package/src/runtime/guardian-decision-types.ts +6 -0
- package/src/runtime/guardian-reply-router.ts +717 -0
- package/src/runtime/http-server.ts +8 -0
- package/src/runtime/ingress-service.ts +80 -3
- package/src/runtime/invite-redemption-service.ts +141 -2
- package/src/runtime/routes/canonical-guardian-expiry-sweep.ts +116 -0
- 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 +20 -2
- package/src/runtime/routes/guardian-action-routes.ts +100 -109
- package/src/runtime/routes/guardian-approval-interception.ts +17 -6
- package/src/runtime/routes/inbound-message-handler.ts +205 -529
- package/src/runtime/routes/ingress-routes.ts +52 -4
- package/src/runtime/routes/pairing-routes.ts +3 -0
- package/src/runtime/tool-grant-request-helper.ts +195 -0
- package/src/tools/executor.ts +13 -1
- package/src/tools/guardian-control-plane-policy.ts +2 -2
- package/src/tools/sensitive-output-placeholders.ts +203 -0
- package/src/tools/tool-approval-handler.ts +53 -10
- package/src/tools/types.ts +13 -2
- package/src/util/bundled-asset.ts +31 -0
- package/src/util/canonicalize-identity.ts +52 -0
- 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
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { mkdirSync, rmSync } from 'node:fs';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
|
|
6
|
+
|
|
7
|
+
import { resolveBundledDir } from '../util/bundled-asset.js';
|
|
8
|
+
|
|
9
|
+
let tempDir: string;
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
tempDir = join(tmpdir(), `bundled-asset-test-${crypto.randomUUID()}`);
|
|
13
|
+
mkdirSync(tempDir, { recursive: true });
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
afterEach(() => {
|
|
17
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
describe('resolveBundledDir', () => {
|
|
21
|
+
test('source mode: returns join(callerDir, relativePath) when callerDir is a normal path', () => {
|
|
22
|
+
const result = resolveBundledDir('/some/source/path', 'templates', 'templates');
|
|
23
|
+
expect(result).toBe(join('/some/source/path', 'templates'));
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test('source mode: does not check existsSync for the source path', () => {
|
|
27
|
+
// Even if the resolved path does not exist, it returns it as-is
|
|
28
|
+
const result = resolveBundledDir('/nonexistent/path', 'templates', 'templates');
|
|
29
|
+
expect(result).toBe(join('/nonexistent/path', 'templates'));
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe('compiled mode (/$bunfs/ prefix)', () => {
|
|
33
|
+
// In compiled mode, process.execPath determines fallback locations.
|
|
34
|
+
// We simulate by creating real directories at the expected fallback paths.
|
|
35
|
+
|
|
36
|
+
let savedExecPath: string;
|
|
37
|
+
|
|
38
|
+
beforeEach(() => {
|
|
39
|
+
savedExecPath = process.execPath;
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
afterEach(() => {
|
|
43
|
+
process.execPath = savedExecPath;
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('prefers Contents/Resources/<bundleName> when it exists', () => {
|
|
47
|
+
// Simulate macOS .app bundle: binary at Contents/MacOS/vellum-daemon
|
|
48
|
+
const macosDir = join(tempDir, 'Contents', 'MacOS');
|
|
49
|
+
const resourcesDir = join(tempDir, 'Contents', 'Resources');
|
|
50
|
+
mkdirSync(macosDir, { recursive: true });
|
|
51
|
+
mkdirSync(join(resourcesDir, 'templates'), { recursive: true });
|
|
52
|
+
|
|
53
|
+
process.execPath = join(macosDir, 'vellum-daemon');
|
|
54
|
+
|
|
55
|
+
const result = resolveBundledDir('/$bunfs/root/src/config', 'templates', 'templates');
|
|
56
|
+
expect(result).toBe(join(resourcesDir, 'templates'));
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test('falls back to <execDir>/<bundleName> when Resources does not exist', () => {
|
|
60
|
+
// Simulate standalone binary deployment (no .app bundle)
|
|
61
|
+
const binDir = join(tempDir, 'bin');
|
|
62
|
+
mkdirSync(join(binDir, 'templates'), { recursive: true });
|
|
63
|
+
|
|
64
|
+
process.execPath = join(binDir, 'vellum-daemon');
|
|
65
|
+
|
|
66
|
+
const result = resolveBundledDir('/$bunfs/root/src/config', 'templates', 'templates');
|
|
67
|
+
expect(result).toBe(join(binDir, 'templates'));
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test('falls back to source path when neither Resources nor execDir have the asset', () => {
|
|
71
|
+
const binDir = join(tempDir, 'bin');
|
|
72
|
+
mkdirSync(binDir, { recursive: true });
|
|
73
|
+
// Don't create any asset directories
|
|
74
|
+
|
|
75
|
+
process.execPath = join(binDir, 'vellum-daemon');
|
|
76
|
+
|
|
77
|
+
const result = resolveBundledDir('/$bunfs/root/src/config', 'templates', 'templates');
|
|
78
|
+
expect(result).toBe(join('/$bunfs/root/src/config', 'templates'));
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test('Resources path takes priority over execDir path when both exist', () => {
|
|
82
|
+
const macosDir = join(tempDir, 'Contents', 'MacOS');
|
|
83
|
+
const resourcesDir = join(tempDir, 'Contents', 'Resources');
|
|
84
|
+
mkdirSync(macosDir, { recursive: true });
|
|
85
|
+
mkdirSync(join(resourcesDir, 'hook-templates'), { recursive: true });
|
|
86
|
+
// Also create at execDir level
|
|
87
|
+
mkdirSync(join(macosDir, 'hook-templates'), { recursive: true });
|
|
88
|
+
|
|
89
|
+
process.execPath = join(macosDir, 'vellum-daemon');
|
|
90
|
+
|
|
91
|
+
const result = resolveBundledDir('/$bunfs/root/src/hooks', '../../hook-templates', 'hook-templates');
|
|
92
|
+
expect(result).toBe(join(resourcesDir, 'hook-templates'));
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test('works with different bundleName values', () => {
|
|
96
|
+
const macosDir = join(tempDir, 'Contents', 'MacOS');
|
|
97
|
+
const resourcesDir = join(tempDir, 'Contents', 'Resources');
|
|
98
|
+
mkdirSync(macosDir, { recursive: true });
|
|
99
|
+
mkdirSync(join(resourcesDir, 'prebuilt'), { recursive: true });
|
|
100
|
+
|
|
101
|
+
process.execPath = join(macosDir, 'vellum-daemon');
|
|
102
|
+
|
|
103
|
+
const result = resolveBundledDir('/$bunfs/root/src/home-base/prebuilt', '.', 'prebuilt');
|
|
104
|
+
expect(result).toBe(join(resourcesDir, 'prebuilt'));
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
});
|
|
@@ -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
|
|
|
@@ -47,6 +47,8 @@ const mockCallsConfig = {
|
|
|
47
47
|
|
|
48
48
|
mock.module('../config/loader.js', () => ({
|
|
49
49
|
getConfig: () => ({
|
|
50
|
+
ui: {},
|
|
51
|
+
|
|
50
52
|
model: 'test',
|
|
51
53
|
provider: 'test',
|
|
52
54
|
apiKeys: {},
|
|
@@ -227,8 +229,8 @@ describe('runtime call routes — HTTP layer', () => {
|
|
|
227
229
|
});
|
|
228
230
|
|
|
229
231
|
expect(res.status).toBe(400);
|
|
230
|
-
const body = await res.json() as { error: string };
|
|
231
|
-
expect(body.error).toContain('conversationId');
|
|
232
|
+
const body = await res.json() as { error: { message: string; code?: string } };
|
|
233
|
+
expect(body.error.message).toContain('conversationId');
|
|
232
234
|
|
|
233
235
|
await stopServer();
|
|
234
236
|
});
|
|
@@ -269,8 +271,8 @@ describe('runtime call routes — HTTP layer', () => {
|
|
|
269
271
|
});
|
|
270
272
|
|
|
271
273
|
expect(res.status).toBe(400);
|
|
272
|
-
const body = await res.json() as { error: string };
|
|
273
|
-
expect(body.error).toContain('E.164');
|
|
274
|
+
const body = await res.json() as { error: { message: string; code?: string } };
|
|
275
|
+
expect(body.error.message).toContain('E.164');
|
|
274
276
|
|
|
275
277
|
await stopServer();
|
|
276
278
|
});
|
|
@@ -285,8 +287,8 @@ describe('runtime call routes — HTTP layer', () => {
|
|
|
285
287
|
});
|
|
286
288
|
|
|
287
289
|
expect(res.status).toBe(400);
|
|
288
|
-
const body = await res.json() as { error: string };
|
|
289
|
-
expect(body.error).toContain('Invalid JSON');
|
|
290
|
+
const body = await res.json() as { error: { message: string; code?: string } };
|
|
291
|
+
expect(body.error.message).toContain('Invalid JSON');
|
|
290
292
|
|
|
291
293
|
await stopServer();
|
|
292
294
|
});
|
|
@@ -309,8 +311,8 @@ describe('runtime call routes — HTTP layer', () => {
|
|
|
309
311
|
// user_number mode requires a configured user phone number;
|
|
310
312
|
// since we haven't set one, this should return a 400 explaining why
|
|
311
313
|
expect(res.status).toBe(400);
|
|
312
|
-
const body = await res.json() as { error: string };
|
|
313
|
-
expect(body.error).toContain('user_number');
|
|
314
|
+
const body = await res.json() as { error: { message: string; code?: string } };
|
|
315
|
+
expect(body.error.message).toContain('user_number');
|
|
314
316
|
|
|
315
317
|
await stopServer();
|
|
316
318
|
});
|
|
@@ -364,11 +366,11 @@ describe('runtime call routes — HTTP layer', () => {
|
|
|
364
366
|
});
|
|
365
367
|
|
|
366
368
|
expect(res.status).toBe(400);
|
|
367
|
-
const body = await res.json() as { error: string };
|
|
368
|
-
expect(body.error).toContain('Invalid callerIdentityMode');
|
|
369
|
-
expect(body.error).toContain('bogus');
|
|
370
|
-
expect(body.error).toContain('assistant_number');
|
|
371
|
-
expect(body.error).toContain('user_number');
|
|
369
|
+
const body = await res.json() as { error: { message: string; code?: string } };
|
|
370
|
+
expect(body.error.message).toContain('Invalid callerIdentityMode');
|
|
371
|
+
expect(body.error.message).toContain('bogus');
|
|
372
|
+
expect(body.error.message).toContain('assistant_number');
|
|
373
|
+
expect(body.error.message).toContain('user_number');
|
|
372
374
|
|
|
373
375
|
await stopServer();
|
|
374
376
|
});
|
|
@@ -510,8 +512,8 @@ describe('runtime call routes — HTTP layer', () => {
|
|
|
510
512
|
});
|
|
511
513
|
|
|
512
514
|
expect(res.status).toBe(400);
|
|
513
|
-
const body = await res.json() as { error: string };
|
|
514
|
-
expect(body.error).toContain('Invalid JSON');
|
|
515
|
+
const body = await res.json() as { error: { message: string; code?: string } };
|
|
516
|
+
expect(body.error.message).toContain('Invalid JSON');
|
|
515
517
|
|
|
516
518
|
await stopServer();
|
|
517
519
|
});
|
|
@@ -533,9 +535,9 @@ describe('runtime call routes — HTTP layer', () => {
|
|
|
533
535
|
body: JSON.stringify({ answer: 'Yes, please' }),
|
|
534
536
|
});
|
|
535
537
|
|
|
536
|
-
expect(res.status).toBe(
|
|
537
|
-
const body = await res.json() as { error: string };
|
|
538
|
-
expect(body.error).toContain('
|
|
538
|
+
expect(res.status).toBe(409);
|
|
539
|
+
const body = await res.json() as { error: { message: string; code?: string } };
|
|
540
|
+
expect(body.error.message).toContain('No active controller');
|
|
539
541
|
|
|
540
542
|
await stopServer();
|
|
541
543
|
});
|
|
@@ -583,8 +585,8 @@ describe('runtime call routes — HTTP layer', () => {
|
|
|
583
585
|
});
|
|
584
586
|
|
|
585
587
|
expect(res.status).toBe(409);
|
|
586
|
-
const body = await res.json() as { error: string };
|
|
587
|
-
expect(body.error).toContain('
|
|
588
|
+
const body = await res.json() as { error: { message: string; code?: string } };
|
|
589
|
+
expect(body.error.message).toContain('No active controller');
|
|
588
590
|
|
|
589
591
|
await stopServer();
|
|
590
592
|
});
|
|
@@ -609,8 +611,8 @@ describe('runtime call routes — HTTP layer', () => {
|
|
|
609
611
|
});
|
|
610
612
|
|
|
611
613
|
expect(res.status).toBe(400);
|
|
612
|
-
const body = await res.json() as { error: string };
|
|
613
|
-
expect(body.error).toContain('Invalid JSON');
|
|
614
|
+
const body = await res.json() as { error: { message: string; code?: string } };
|
|
615
|
+
expect(body.error.message).toContain('Invalid JSON');
|
|
614
616
|
|
|
615
617
|
await stopServer();
|
|
616
618
|
});
|
|
@@ -633,8 +635,8 @@ describe('runtime call routes — HTTP layer', () => {
|
|
|
633
635
|
});
|
|
634
636
|
|
|
635
637
|
expect(res.status).toBe(400);
|
|
636
|
-
const body = await res.json() as { error: string };
|
|
637
|
-
expect(body.error).toContain('instructionText');
|
|
638
|
+
const body = await res.json() as { error: { message: string; code?: string } };
|
|
639
|
+
expect(body.error.message).toContain('instructionText');
|
|
638
640
|
|
|
639
641
|
await stopServer();
|
|
640
642
|
});
|
|
@@ -657,8 +659,8 @@ describe('runtime call routes — HTTP layer', () => {
|
|
|
657
659
|
});
|
|
658
660
|
|
|
659
661
|
expect(res.status).toBe(400);
|
|
660
|
-
const body = await res.json() as { error: string };
|
|
661
|
-
expect(body.error).toContain('instructionText');
|
|
662
|
+
const body = await res.json() as { error: { message: string; code?: string } };
|
|
663
|
+
expect(body.error.message).toContain('instructionText');
|
|
662
664
|
|
|
663
665
|
await stopServer();
|
|
664
666
|
});
|
|
@@ -673,8 +675,8 @@ describe('runtime call routes — HTTP layer', () => {
|
|
|
673
675
|
});
|
|
674
676
|
|
|
675
677
|
expect(res.status).toBe(404);
|
|
676
|
-
const body = await res.json() as { error: string };
|
|
677
|
-
expect(body.error).toContain('No call session found');
|
|
678
|
+
const body = await res.json() as { error: { message: string; code?: string } };
|
|
679
|
+
expect(body.error.message).toContain('No call session found');
|
|
678
680
|
|
|
679
681
|
await stopServer();
|
|
680
682
|
});
|
|
@@ -699,8 +701,8 @@ describe('runtime call routes — HTTP layer', () => {
|
|
|
699
701
|
});
|
|
700
702
|
|
|
701
703
|
expect(res.status).toBe(409);
|
|
702
|
-
const body = await res.json() as { error: string };
|
|
703
|
-
expect(body.error).toContain('not active');
|
|
704
|
+
const body = await res.json() as { error: { message: string; code?: string } };
|
|
705
|
+
expect(body.error.message).toContain('not active');
|
|
704
706
|
|
|
705
707
|
await stopServer();
|
|
706
708
|
});
|
|
@@ -723,8 +725,8 @@ describe('runtime call routes — HTTP layer', () => {
|
|
|
723
725
|
});
|
|
724
726
|
|
|
725
727
|
expect(res.status).toBe(409);
|
|
726
|
-
const body = await res.json() as { error: string };
|
|
727
|
-
expect(body.error).toContain('
|
|
728
|
+
const body = await res.json() as { error: { message: string; code?: string } };
|
|
729
|
+
expect(body.error.message).toContain('No active controller');
|
|
728
730
|
|
|
729
731
|
await stopServer();
|
|
730
732
|
});
|