vellum 0.2.1 → 0.2.2
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/README.md +15 -2
- package/bun.lock +5 -2
- package/package.json +4 -2
- package/scripts/capture-x-graphql.ts +562 -0
- package/scripts/ipc/check-swift-decoder-drift.ts +2 -1
- package/scripts/test.sh +5 -0
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +133 -34
- package/src/__tests__/account-registry.test.ts +2 -1
- package/src/__tests__/agent-heartbeat-service.test.ts +250 -0
- package/src/__tests__/asset-materialize-tool.test.ts +16 -15
- package/src/__tests__/asset-search-tool.test.ts +23 -22
- package/src/__tests__/attachments-store.test.ts +56 -127
- package/src/__tests__/browser-skill-baseline-tool-payload.test.ts +5 -4
- package/src/__tests__/browser-skill-endstate.test.ts +4 -3
- package/src/__tests__/call-bridge.test.ts +385 -0
- package/src/__tests__/call-constants.test.ts +40 -0
- package/src/__tests__/call-orchestrator.test.ts +130 -4
- package/src/__tests__/call-recovery.test.ts +518 -0
- package/src/__tests__/call-routes-http.test.ts +459 -0
- package/src/__tests__/call-state-machine.test.ts +143 -0
- package/src/__tests__/call-store.test.ts +216 -1
- package/src/__tests__/cli-discover.test.ts +1 -1
- package/src/__tests__/commit-message-enrichment-service.test.ts +148 -7
- package/src/__tests__/compaction.benchmark.test.ts +176 -0
- package/src/__tests__/computer-use-tools.test.ts +250 -0
- package/src/__tests__/config-schema.test.ts +299 -3
- package/src/__tests__/conflict-store.test.ts +2 -1
- package/src/__tests__/contacts-tools.test.ts +331 -0
- package/src/__tests__/conversation-store.test.ts +30 -32
- package/src/__tests__/credential-security-invariants.test.ts +4 -0
- package/src/__tests__/date-context.test.ts +373 -0
- package/src/__tests__/db-schedule-syntax-migration.test.ts +129 -0
- package/src/__tests__/fixtures/media-reuse-fixtures.ts +3 -3
- package/src/__tests__/followup-tools.test.ts +303 -0
- package/src/__tests__/handlers-twitter-config.test.ts +718 -0
- package/src/__tests__/intent-routing.test.ts +64 -57
- package/src/__tests__/ipc-roundtrip.benchmark.test.ts +237 -0
- package/src/__tests__/ipc-snapshot.test.ts +62 -28
- package/src/__tests__/llm-usage-store.test.ts +3 -8
- package/src/__tests__/media-generate-image.test.ts +1 -1
- package/src/__tests__/media-reuse-story.e2e.test.ts +7 -7
- package/src/__tests__/memory-retrieval.benchmark.test.ts +430 -0
- package/src/__tests__/parallel-tool.benchmark.test.ts +294 -0
- package/src/__tests__/playbook-tools.test.ts +342 -0
- package/src/__tests__/profile-compiler.test.ts +2 -1
- package/src/__tests__/provider-streaming.benchmark.test.ts +773 -0
- package/src/__tests__/recurrence-engine-rruleset.test.ts +78 -0
- package/src/__tests__/recurrence-engine.test.ts +69 -0
- package/src/__tests__/recurrence-types.test.ts +71 -0
- package/src/__tests__/registry.test.ts +5 -3
- package/src/__tests__/relay-server.test.ts +633 -0
- package/src/__tests__/reminder-store.test.ts +6 -3
- package/src/__tests__/reminder.test.ts +43 -77
- package/src/__tests__/run-orchestrator-assistant-events.test.ts +8 -4
- package/src/__tests__/run-orchestrator.test.ts +4 -4
- package/src/__tests__/runtime-attachment-metadata.test.ts +7 -6
- package/src/__tests__/runtime-runs-http.test.ts +4 -4
- package/src/__tests__/runtime-runs.test.ts +4 -4
- package/src/__tests__/schedule-store.test.ts +482 -0
- package/src/__tests__/schedule-tools.test.ts +700 -0
- package/src/__tests__/scheduler-recurrence.test.ts +329 -0
- package/src/__tests__/server-history-render.test.ts +14 -13
- package/src/__tests__/session-error.test.ts +28 -0
- package/src/__tests__/session-init.benchmark.test.ts +462 -0
- package/src/__tests__/session-queue.test.ts +71 -48
- package/src/__tests__/session-runtime-assembly.test.ts +161 -0
- package/src/__tests__/session-surfaces-task-progress.test.ts +104 -0
- package/src/__tests__/signup-e2e.test.ts +2 -1
- package/src/__tests__/skill-projection.benchmark.test.ts +328 -0
- package/src/__tests__/skill-script-runner.test.ts +159 -0
- package/src/__tests__/speaker-identification.test.ts +52 -0
- package/src/__tests__/subagent-manager-notify.test.ts +42 -10
- package/src/__tests__/subagent-tools.test.ts +141 -41
- package/src/__tests__/task-compiler.test.ts +2 -1
- package/src/__tests__/task-runner.test.ts +2 -1
- package/src/__tests__/task-scheduler.test.ts +2 -1
- package/src/__tests__/task-tools.test.ts +49 -56
- package/src/__tests__/tool-audit-listener.test.ts +1 -0
- package/src/__tests__/tool-domain-event-publisher.test.ts +2 -0
- package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +500 -0
- package/src/__tests__/tool-executor.test.ts +13 -17
- package/src/__tests__/turn-commit.test.ts +218 -3
- package/src/__tests__/twilio-provider.test.ts +143 -0
- package/src/__tests__/twilio-routes.test.ts +789 -0
- package/src/__tests__/twitter-auth-handler.test.ts +581 -0
- package/src/__tests__/view-image-tool.test.ts +217 -0
- package/src/__tests__/workspace-git-service.test.ts +186 -0
- package/src/__tests__/workspace-heartbeat-service.test.ts +13 -3
- package/src/agent-heartbeat/agent-heartbeat-service.ts +155 -0
- package/src/bundler/app-bundler.ts +12 -8
- package/src/calls/call-bridge.ts +95 -0
- package/src/calls/call-constants.ts +43 -5
- package/src/calls/call-domain.ts +276 -0
- package/src/calls/call-orchestrator.ts +43 -17
- package/src/calls/call-recovery.ts +207 -0
- package/src/calls/call-state-machine.ts +68 -0
- package/src/calls/call-store.ts +192 -5
- package/src/calls/relay-server.ts +41 -4
- package/src/calls/speaker-identification.ts +213 -0
- package/src/calls/twilio-provider.ts +10 -6
- package/src/calls/twilio-routes.ts +90 -76
- package/src/calls/types.ts +1 -1
- package/src/cli/config-commands.ts +334 -0
- package/src/cli/core-commands.ts +776 -0
- package/src/cli/doordash.ts +251 -1
- package/src/cli/ipc-client.ts +82 -0
- package/src/cli/map.ts +246 -0
- package/src/cli/twitter.ts +575 -0
- package/src/cli.ts +7 -5
- package/src/commands/__tests__/cc-command-registry.test.ts +319 -0
- package/src/commands/cc-command-registry.ts +209 -0
- package/src/config/bundled-skills/contacts/SKILL.md +39 -0
- package/src/config/bundled-skills/contacts/TOOLS.json +122 -0
- package/src/config/bundled-skills/contacts/tools/contact-merge.ts +9 -0
- package/src/config/bundled-skills/contacts/tools/contact-search.ts +9 -0
- package/src/config/bundled-skills/contacts/tools/contact-upsert.ts +9 -0
- package/src/config/bundled-skills/document/SKILL.md +18 -0
- package/src/config/bundled-skills/document/TOOLS.json +53 -0
- package/src/config/bundled-skills/document/tools/document-create.ts +9 -0
- package/src/config/bundled-skills/document/tools/document-update.ts +9 -0
- package/src/config/bundled-skills/doordash/SKILL.md +82 -23
- package/src/config/bundled-skills/followups/SKILL.md +32 -0
- package/src/config/bundled-skills/followups/TOOLS.json +100 -0
- package/src/config/bundled-skills/followups/tools/followup-create.ts +9 -0
- package/src/config/bundled-skills/followups/tools/followup-list.ts +9 -0
- package/src/config/bundled-skills/followups/tools/followup-resolve.ts +9 -0
- package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +1 -23
- package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +2 -1
- package/src/config/bundled-skills/playbooks/SKILL.md +31 -0
- package/src/config/bundled-skills/playbooks/TOOLS.json +126 -0
- package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +9 -0
- package/src/config/bundled-skills/playbooks/tools/playbook-delete.ts +9 -0
- package/src/config/bundled-skills/playbooks/tools/playbook-list.ts +9 -0
- package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +9 -0
- package/src/config/bundled-skills/reminder/SKILL.md +20 -0
- package/src/config/bundled-skills/reminder/TOOLS.json +67 -0
- package/src/config/bundled-skills/reminder/tools/reminder-cancel.ts +9 -0
- package/src/config/bundled-skills/reminder/tools/reminder-create.ts +9 -0
- package/src/config/bundled-skills/reminder/tools/reminder-list.ts +9 -0
- package/src/config/bundled-skills/schedule/SKILL.md +74 -0
- package/src/config/bundled-skills/schedule/TOOLS.json +135 -0
- package/src/config/bundled-skills/schedule/tools/schedule-create.ts +9 -0
- package/src/config/bundled-skills/schedule/tools/schedule-delete.ts +9 -0
- package/src/config/bundled-skills/schedule/tools/schedule-list.ts +9 -0
- package/src/config/bundled-skills/schedule/tools/schedule-update.ts +9 -0
- package/src/config/bundled-skills/subagent/SKILL.md +25 -0
- package/src/config/bundled-skills/subagent/TOOLS.json +107 -0
- package/src/config/bundled-skills/subagent/tools/subagent-abort.ts +9 -0
- package/src/config/bundled-skills/subagent/tools/subagent-message.ts +9 -0
- package/src/config/bundled-skills/subagent/tools/subagent-read.ts +9 -0
- package/src/config/bundled-skills/subagent/tools/subagent-spawn.ts +9 -0
- package/src/config/bundled-skills/subagent/tools/subagent-status.ts +9 -0
- package/src/config/bundled-skills/tasks/SKILL.md +28 -0
- package/src/config/bundled-skills/tasks/TOOLS.json +256 -0
- package/src/config/bundled-skills/tasks/tools/task-delete.ts +9 -0
- package/src/config/bundled-skills/tasks/tools/task-list-add.ts +9 -0
- package/src/config/bundled-skills/tasks/tools/task-list-remove.ts +9 -0
- package/src/config/bundled-skills/tasks/tools/task-list-show.ts +9 -0
- package/src/config/bundled-skills/tasks/tools/task-list-update.ts +9 -0
- package/src/config/bundled-skills/tasks/tools/task-list.ts +9 -0
- package/src/config/bundled-skills/tasks/tools/task-run.ts +9 -0
- package/src/config/bundled-skills/tasks/tools/task-save.ts +9 -0
- package/src/config/bundled-skills/twitter/SKILL.md +134 -0
- package/src/config/bundled-skills/watcher/SKILL.md +27 -0
- package/src/config/bundled-skills/watcher/TOOLS.json +147 -0
- package/src/config/bundled-skills/watcher/tools/watcher-create.ts +9 -0
- package/src/config/bundled-skills/watcher/tools/watcher-delete.ts +9 -0
- package/src/config/bundled-skills/watcher/tools/watcher-digest.ts +9 -0
- package/src/config/bundled-skills/watcher/tools/watcher-list.ts +9 -0
- package/src/config/bundled-skills/watcher/tools/watcher-update.ts +9 -0
- package/src/config/defaults.ts +33 -0
- package/src/config/loader.ts +4 -1
- package/src/config/schema.ts +161 -1
- package/src/config/system-prompt.ts +61 -16
- package/src/config/templates/IDENTITY.md +7 -0
- package/src/config/types.ts +4 -0
- package/src/contacts/contact-store.ts +4 -4
- package/src/daemon/assistant-attachments.ts +10 -0
- package/src/daemon/classifier.ts +3 -1
- package/src/daemon/computer-use-session.ts +3 -1
- package/src/daemon/date-context.ts +136 -0
- package/src/daemon/handlers/apps.ts +16 -1
- package/src/daemon/handlers/browser.ts +54 -0
- package/src/daemon/handlers/computer-use.ts +7 -1
- package/src/daemon/handlers/config.ts +163 -5
- package/src/daemon/handlers/diagnostics.ts +5 -1
- package/src/daemon/handlers/documents.ts +18 -29
- package/src/daemon/handlers/home-base.ts +5 -1
- package/src/daemon/handlers/index.ts +40 -277
- package/src/daemon/handlers/misc.ts +9 -1
- package/src/daemon/handlers/publish.ts +6 -1
- package/src/daemon/handlers/sessions.ts +65 -12
- package/src/daemon/handlers/shared.ts +36 -1
- package/src/daemon/handlers/signing.ts +37 -0
- package/src/daemon/handlers/skills.ts +20 -6
- package/src/daemon/handlers/subagents.ts +8 -3
- package/src/daemon/handlers/twitter-auth.ts +169 -0
- package/src/daemon/handlers/work-items.ts +384 -68
- package/src/daemon/ipc-contract-inventory.json +28 -4
- package/src/daemon/ipc-contract.ts +133 -37
- package/src/daemon/ipc-protocol.ts +7 -2
- package/src/daemon/lifecycle.ts +21 -0
- package/src/daemon/main.ts +10 -4
- package/src/daemon/ride-shotgun-handler.ts +74 -10
- package/src/daemon/server.ts +143 -26
- package/src/daemon/session-agent-loop.ts +887 -0
- package/src/daemon/session-attachments.ts +28 -5
- package/src/daemon/session-error.ts +24 -3
- package/src/daemon/session-lifecycle.ts +147 -0
- package/src/daemon/session-media-retry.ts +147 -0
- package/src/daemon/session-messaging.ts +145 -0
- package/src/daemon/session-notifiers.ts +164 -0
- package/src/daemon/session-process.ts +2 -2
- package/src/daemon/session-queue-manager.ts +1 -0
- package/src/daemon/session-runtime-assembly.ts +52 -0
- package/src/daemon/session-skill-tools.ts +124 -5
- package/src/daemon/session-slash.ts +3 -0
- package/src/daemon/session-surfaces.ts +77 -2
- package/src/daemon/session-tool-setup.ts +216 -2
- package/src/daemon/session-usage.ts +0 -2
- package/src/daemon/session.ts +114 -1404
- package/src/daemon/video-thumbnail.ts +60 -0
- package/src/doordash/client.ts +121 -27
- package/src/doordash/queries.ts +1 -2
- package/src/export/formatter.ts +3 -1
- package/src/followups/followup-store.ts +4 -2
- package/src/followups/types.ts +6 -0
- package/src/hooks/templates.ts +1 -1
- package/src/index.ts +32 -1153
- package/src/memory/attachments-store.ts +28 -83
- package/src/memory/channel-delivery-store.ts +7 -21
- package/src/memory/clarification-resolver.ts +6 -5
- package/src/memory/contradiction-checker.ts +3 -2
- package/src/memory/conversation-key-store.ts +10 -29
- package/src/memory/conversation-store.ts +2 -1
- package/src/memory/db.ts +96 -2
- package/src/memory/entity-extractor.ts +6 -3
- package/src/memory/items-extractor.ts +5 -4
- package/src/memory/jobs-store.ts +3 -2
- package/src/memory/llm-usage-store.ts +1 -2
- package/src/memory/runs-store.ts +1 -2
- package/src/memory/schema.ts +23 -2
- package/src/messaging/style-analyzer.ts +3 -2
- package/src/messaging/thread-summarizer.ts +8 -12
- package/src/messaging/triage-engine.ts +4 -2
- package/src/providers/openrouter/client.ts +20 -0
- package/src/providers/registry.ts +8 -0
- package/src/runtime/http-server.ts +108 -20
- package/src/runtime/routes/attachment-routes.ts +2 -3
- package/src/runtime/routes/call-routes.ts +140 -0
- package/src/runtime/routes/channel-routes.ts +5 -10
- package/src/runtime/routes/conversation-routes.ts +5 -5
- package/src/runtime/routes/run-routes.ts +2 -2
- package/src/runtime/run-orchestrator.ts +9 -3
- package/src/schedule/recurrence-engine.ts +138 -0
- package/src/schedule/recurrence-types.ts +67 -0
- package/src/schedule/schedule-store.ts +102 -57
- package/src/schedule/scheduler.ts +9 -6
- package/src/security/oauth2.ts +29 -4
- package/src/security/secret-allowlist.ts +46 -0
- package/src/skills/clawhub.ts +1 -1
- package/src/subagent/manager.ts +40 -8
- package/src/swarm/backend-claude-code.ts +64 -9
- package/src/swarm/worker-prompts.ts +2 -1
- package/src/tasks/SPEC.md +34 -28
- package/src/tasks/ephemeral-permissions.ts +16 -7
- package/src/tasks/task-compiler.ts +5 -4
- package/src/tasks/task-runner.ts +10 -5
- package/src/tasks/task-scheduler.ts +1 -1
- package/src/tasks/tool-sanitizer.ts +36 -0
- package/src/tools/assets/search.ts +4 -4
- package/src/tools/browser/api-map.ts +220 -0
- package/src/tools/browser/auto-navigate.ts +270 -0
- package/src/tools/browser/browser-execution.ts +2 -1
- package/src/tools/browser/browser-manager.ts +2 -2
- package/src/tools/browser/network-recorder.ts +5 -4
- package/src/tools/browser/x-auto-navigate.ts +207 -0
- package/src/tools/calls/call-end.ts +17 -67
- package/src/tools/calls/call-start.ts +24 -85
- package/src/tools/calls/call-status.ts +35 -51
- package/src/tools/claude-code/claude-code.ts +77 -11
- package/src/tools/contacts/contact-merge.ts +46 -78
- package/src/tools/contacts/contact-search.ts +35 -79
- package/src/tools/contacts/contact-upsert.ts +35 -108
- package/src/tools/credentials/vault.ts +20 -4
- package/src/tools/document/document-tool.ts +71 -144
- package/src/tools/executor.ts +129 -10
- package/src/tools/followups/followup_create.ts +46 -88
- package/src/tools/followups/followup_list.ts +34 -74
- package/src/tools/followups/followup_resolve.ts +31 -66
- package/src/tools/host-terminal/cli-discover.ts +2 -1
- package/src/tools/host-terminal/host-shell.ts +10 -0
- package/src/tools/memory/handlers.ts +5 -4
- package/src/tools/network/__tests__/web-search.test.ts +427 -0
- package/src/tools/network/script-proxy/__tests__/logging.test.ts +248 -0
- package/src/tools/network/script-proxy/__tests__/policy.test.ts +234 -0
- package/src/tools/network/script-proxy/__tests__/router.test.ts +76 -0
- package/src/tools/network/web-fetch.ts +18 -6
- package/src/tools/playbooks/index.ts +4 -5
- package/src/tools/playbooks/playbook-create.ts +3 -47
- package/src/tools/playbooks/playbook-delete.ts +1 -25
- package/src/tools/playbooks/playbook-list.ts +1 -28
- package/src/tools/playbooks/playbook-update.ts +3 -51
- package/src/tools/reminder/reminder.ts +5 -78
- package/src/tools/schedule/create.ts +69 -74
- package/src/tools/schedule/delete.ts +21 -47
- package/src/tools/schedule/list.ts +55 -74
- package/src/tools/schedule/update.ts +77 -84
- package/src/tools/subagent/abort.ts +29 -58
- package/src/tools/subagent/message.ts +30 -63
- package/src/tools/subagent/read.ts +53 -84
- package/src/tools/subagent/spawn.ts +43 -82
- package/src/tools/subagent/status.ts +42 -71
- package/src/tools/swarm/delegate.ts +2 -1
- package/src/tools/tasks/index.ts +8 -8
- package/src/tools/tasks/task-delete.ts +60 -88
- package/src/tools/tasks/task-list.ts +31 -52
- package/src/tools/tasks/task-run.ts +72 -108
- package/src/tools/tasks/task-save.ts +33 -65
- package/src/tools/tasks/work-item-enqueue.ts +183 -215
- package/src/tools/tasks/work-item-list.ts +33 -63
- package/src/tools/tasks/work-item-remove.ts +45 -97
- package/src/tools/tasks/work-item-update.ts +91 -163
- package/src/tools/terminal/backends/native.ts +3 -1
- package/src/tools/tool-manifest.ts +0 -62
- package/src/tools/types.ts +6 -0
- package/src/tools/ui-surface/definitions.ts +3 -1
- package/src/tools/watch/screen-watch.ts +3 -1
- package/src/tools/watcher/create.ts +52 -98
- package/src/tools/watcher/delete.ts +20 -46
- package/src/tools/watcher/digest.ts +36 -70
- package/src/tools/watcher/list.ts +49 -79
- package/src/tools/watcher/update.ts +45 -91
- package/src/twitter/client.ts +690 -0
- package/src/twitter/session.ts +91 -0
- package/src/usage/types.ts +0 -1
- package/src/util/truncate.ts +6 -0
- package/src/watcher/providers/slack.ts +2 -1
- package/src/watcher/watcher-store.ts +3 -2
- package/src/work-items/work-item-store.ts +27 -2
- package/src/workspace/commit-message-enrichment-service.ts +31 -7
- package/src/workspace/git-service.ts +87 -22
- package/src/workspace/provider-commit-message-generator.ts +242 -0
- package/src/workspace/turn-commit.ts +62 -3
- package/src/tools/contacts/index.ts +0 -4
- package/src/tools/document/index.ts +0 -5
- package/src/tools/followups/index.ts +0 -3
- package/src/tools/subagent/index.ts +0 -5
- /package/src/__tests__/{memory-context-benchmark.test.ts → memory-context-benchmark.benchmark.test.ts} +0 -0
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
export interface PromptSpeakerContext {
|
|
2
|
+
speakerId: string;
|
|
3
|
+
speakerLabel: string;
|
|
4
|
+
speakerConfidence: number | null;
|
|
5
|
+
source: 'provider' | 'inferred';
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface PromptSpeakerMetadata {
|
|
9
|
+
speakerId?: string;
|
|
10
|
+
speakerLabel?: string;
|
|
11
|
+
speakerName?: string;
|
|
12
|
+
speakerConfidence?: number;
|
|
13
|
+
participantId?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface SpeakerProfile {
|
|
17
|
+
speakerId: string;
|
|
18
|
+
speakerLabel: string;
|
|
19
|
+
speakerConfidence: number | null;
|
|
20
|
+
source: 'provider' | 'inferred';
|
|
21
|
+
utteranceCount: number;
|
|
22
|
+
firstSeenAt: number;
|
|
23
|
+
lastSeenAt: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function toCleanString(value: unknown): string | null {
|
|
27
|
+
if (typeof value !== 'string') return null;
|
|
28
|
+
const trimmed = value.trim();
|
|
29
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function toNumber(value: unknown): number | null {
|
|
33
|
+
if (typeof value === 'number' && Number.isFinite(value)) return value;
|
|
34
|
+
if (typeof value === 'string') {
|
|
35
|
+
const parsed = Number(value);
|
|
36
|
+
if (Number.isFinite(parsed)) return parsed;
|
|
37
|
+
}
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function getObject(value: unknown): Record<string, unknown> | null {
|
|
42
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return null;
|
|
43
|
+
return value as Record<string, unknown>;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function normalizeSpeakerLabel(metadata: PromptSpeakerMetadata, fallbackIndex: number): string {
|
|
47
|
+
const preferredLabel = toCleanString(metadata.speakerName) ?? toCleanString(metadata.speakerLabel);
|
|
48
|
+
if (preferredLabel) return preferredLabel;
|
|
49
|
+
return `Speaker ${fallbackIndex}`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function extractPromptSpeakerMetadata(message: Record<string, unknown>): PromptSpeakerMetadata {
|
|
53
|
+
const providerMetadata = getObject(message.providerMetadata);
|
|
54
|
+
const metadata = getObject(message.metadata);
|
|
55
|
+
const participant = getObject(message.participant);
|
|
56
|
+
const speaker = getObject(message.speaker);
|
|
57
|
+
|
|
58
|
+
const pick = (...values: unknown[]): string | undefined => {
|
|
59
|
+
for (const value of values) {
|
|
60
|
+
const cleaned = toCleanString(value);
|
|
61
|
+
if (cleaned) return cleaned;
|
|
62
|
+
}
|
|
63
|
+
return undefined;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const pickNumber = (...values: unknown[]): number | undefined => {
|
|
67
|
+
for (const value of values) {
|
|
68
|
+
const parsed = toNumber(value);
|
|
69
|
+
if (parsed !== null) return parsed;
|
|
70
|
+
}
|
|
71
|
+
return undefined;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
speakerId: pick(
|
|
76
|
+
message.speakerId,
|
|
77
|
+
message.speaker_id,
|
|
78
|
+
speaker?.id,
|
|
79
|
+
speaker?.speakerId,
|
|
80
|
+
metadata?.speakerId,
|
|
81
|
+
providerMetadata?.speakerId,
|
|
82
|
+
metadata?.speaker_id,
|
|
83
|
+
providerMetadata?.speaker_id,
|
|
84
|
+
),
|
|
85
|
+
speakerLabel: pick(
|
|
86
|
+
message.speakerLabel,
|
|
87
|
+
message.speaker_label,
|
|
88
|
+
message.speaker,
|
|
89
|
+
speaker?.label,
|
|
90
|
+
speaker?.name,
|
|
91
|
+
metadata?.speakerLabel,
|
|
92
|
+
providerMetadata?.speakerLabel,
|
|
93
|
+
metadata?.speaker_label,
|
|
94
|
+
providerMetadata?.speaker_label,
|
|
95
|
+
),
|
|
96
|
+
speakerName: pick(
|
|
97
|
+
message.speakerName,
|
|
98
|
+
message.speaker_name,
|
|
99
|
+
participant?.name,
|
|
100
|
+
metadata?.speakerName,
|
|
101
|
+
providerMetadata?.speakerName,
|
|
102
|
+
metadata?.speaker_name,
|
|
103
|
+
providerMetadata?.speaker_name,
|
|
104
|
+
),
|
|
105
|
+
speakerConfidence: pickNumber(
|
|
106
|
+
message.speakerConfidence,
|
|
107
|
+
message.speaker_confidence,
|
|
108
|
+
message.confidence,
|
|
109
|
+
speaker?.confidence,
|
|
110
|
+
metadata?.speakerConfidence,
|
|
111
|
+
providerMetadata?.speakerConfidence,
|
|
112
|
+
metadata?.speaker_confidence,
|
|
113
|
+
providerMetadata?.speaker_confidence,
|
|
114
|
+
),
|
|
115
|
+
participantId: pick(
|
|
116
|
+
message.participantId,
|
|
117
|
+
message.participant_id,
|
|
118
|
+
participant?.id,
|
|
119
|
+
metadata?.participantId,
|
|
120
|
+
providerMetadata?.participantId,
|
|
121
|
+
metadata?.participant_id,
|
|
122
|
+
providerMetadata?.participant_id,
|
|
123
|
+
),
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export class SpeakerIdentityTracker {
|
|
128
|
+
private profiles = new Map<string, SpeakerProfile>();
|
|
129
|
+
private nextInferredIndex = 1;
|
|
130
|
+
|
|
131
|
+
identifySpeaker(metadata: PromptSpeakerMetadata): PromptSpeakerContext {
|
|
132
|
+
const providerSpeakerId =
|
|
133
|
+
toCleanString(metadata.speakerId)
|
|
134
|
+
?? toCleanString(metadata.participantId)
|
|
135
|
+
?? null;
|
|
136
|
+
|
|
137
|
+
if (providerSpeakerId) {
|
|
138
|
+
const existing = this.profiles.get(providerSpeakerId);
|
|
139
|
+
if (existing) {
|
|
140
|
+
existing.lastSeenAt = Date.now();
|
|
141
|
+
existing.utteranceCount += 1;
|
|
142
|
+
if (metadata.speakerConfidence !== undefined) {
|
|
143
|
+
existing.speakerConfidence = metadata.speakerConfidence;
|
|
144
|
+
}
|
|
145
|
+
return {
|
|
146
|
+
speakerId: existing.speakerId,
|
|
147
|
+
speakerLabel: existing.speakerLabel,
|
|
148
|
+
speakerConfidence: existing.speakerConfidence,
|
|
149
|
+
source: existing.source,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const profile: SpeakerProfile = {
|
|
154
|
+
speakerId: providerSpeakerId,
|
|
155
|
+
speakerLabel: normalizeSpeakerLabel(metadata, this.nextInferredIndex),
|
|
156
|
+
speakerConfidence: metadata.speakerConfidence ?? null,
|
|
157
|
+
source: 'provider',
|
|
158
|
+
utteranceCount: 1,
|
|
159
|
+
firstSeenAt: Date.now(),
|
|
160
|
+
lastSeenAt: Date.now(),
|
|
161
|
+
};
|
|
162
|
+
this.profiles.set(providerSpeakerId, profile);
|
|
163
|
+
this.nextInferredIndex += 1;
|
|
164
|
+
return {
|
|
165
|
+
speakerId: profile.speakerId,
|
|
166
|
+
speakerLabel: profile.speakerLabel,
|
|
167
|
+
speakerConfidence: profile.speakerConfidence,
|
|
168
|
+
source: profile.source,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const inferredSpeakerId = 'primary-speaker';
|
|
173
|
+
const existingPrimary = this.profiles.get(inferredSpeakerId);
|
|
174
|
+
if (existingPrimary) {
|
|
175
|
+
existingPrimary.lastSeenAt = Date.now();
|
|
176
|
+
existingPrimary.utteranceCount += 1;
|
|
177
|
+
return {
|
|
178
|
+
speakerId: existingPrimary.speakerId,
|
|
179
|
+
speakerLabel: existingPrimary.speakerLabel,
|
|
180
|
+
speakerConfidence: existingPrimary.speakerConfidence,
|
|
181
|
+
source: existingPrimary.source,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const inferredProfile: SpeakerProfile = {
|
|
186
|
+
speakerId: inferredSpeakerId,
|
|
187
|
+
speakerLabel: normalizeSpeakerLabel(metadata, this.nextInferredIndex),
|
|
188
|
+
speakerConfidence: metadata.speakerConfidence ?? null,
|
|
189
|
+
source: 'inferred',
|
|
190
|
+
utteranceCount: 1,
|
|
191
|
+
firstSeenAt: Date.now(),
|
|
192
|
+
lastSeenAt: Date.now(),
|
|
193
|
+
};
|
|
194
|
+
this.profiles.set(inferredSpeakerId, inferredProfile);
|
|
195
|
+
this.nextInferredIndex += 1;
|
|
196
|
+
|
|
197
|
+
return {
|
|
198
|
+
speakerId: inferredProfile.speakerId,
|
|
199
|
+
speakerLabel: inferredProfile.speakerLabel,
|
|
200
|
+
speakerConfidence: inferredProfile.speakerConfidence,
|
|
201
|
+
source: inferredProfile.source,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
listProfiles(): PromptSpeakerContext[] {
|
|
206
|
+
return [...this.profiles.values()].map((profile) => ({
|
|
207
|
+
speakerId: profile.speakerId,
|
|
208
|
+
speakerLabel: profile.speakerLabel,
|
|
209
|
+
speakerConfidence: profile.speakerConfidence,
|
|
210
|
+
source: profile.source,
|
|
211
|
+
}));
|
|
212
|
+
}
|
|
213
|
+
}
|
|
@@ -128,6 +128,15 @@ export class TwilioConversationRelayProvider implements VoiceProvider {
|
|
|
128
128
|
|
|
129
129
|
// ── Webhook signature verification ──────────────────────────────────
|
|
130
130
|
|
|
131
|
+
/**
|
|
132
|
+
* Returns the Twilio auth token from the secure key store, or null if
|
|
133
|
+
* not configured. Exposed as a static method so callers (e.g. the
|
|
134
|
+
* HTTP server webhook middleware) can check availability independently.
|
|
135
|
+
*/
|
|
136
|
+
static getAuthToken(): string | null {
|
|
137
|
+
return getSecureKey('twilio_auth_token') ?? null;
|
|
138
|
+
}
|
|
139
|
+
|
|
131
140
|
/**
|
|
132
141
|
* Validates an X-Twilio-Signature header using HMAC-SHA1.
|
|
133
142
|
*
|
|
@@ -143,13 +152,8 @@ export class TwilioConversationRelayProvider implements VoiceProvider {
|
|
|
143
152
|
url: string,
|
|
144
153
|
params: Record<string, string>,
|
|
145
154
|
signature: string,
|
|
155
|
+
authToken: string,
|
|
146
156
|
): boolean {
|
|
147
|
-
const authToken = getSecureKey('twilio_auth_token');
|
|
148
|
-
if (!authToken) {
|
|
149
|
-
log.error('Cannot verify Twilio webhook signature: auth token not configured');
|
|
150
|
-
return false;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
157
|
const sortedKeys = Object.keys(params).sort();
|
|
154
158
|
let data = url;
|
|
155
159
|
for (const key of sortedKeys) {
|
|
@@ -13,11 +13,15 @@ import {
|
|
|
13
13
|
updateCallSession,
|
|
14
14
|
recordCallEvent,
|
|
15
15
|
expirePendingQuestions,
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
buildCallbackDedupeKey,
|
|
17
|
+
claimCallback,
|
|
18
|
+
releaseCallbackClaim,
|
|
19
|
+
finalizeCallbackClaim,
|
|
18
20
|
} from './call-store.js';
|
|
19
21
|
import type { CallStatus } from './types.js';
|
|
20
|
-
import {
|
|
22
|
+
import { logDeadLetterEvent } from './call-recovery.js';
|
|
23
|
+
import { isTerminalState } from './call-state-machine.js';
|
|
24
|
+
import { getTwilioConfig } from './twilio-config.js';
|
|
21
25
|
|
|
22
26
|
const log = getLogger('twilio-routes');
|
|
23
27
|
|
|
@@ -32,12 +36,12 @@ function escapeXml(str: string): string {
|
|
|
32
36
|
.replace(/'/g, ''');
|
|
33
37
|
}
|
|
34
38
|
|
|
35
|
-
function generateTwiML(callSessionId: string,
|
|
39
|
+
function generateTwiML(callSessionId: string, relayUrl: string, welcomeGreeting: string): string {
|
|
36
40
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
37
41
|
<Response>
|
|
38
42
|
<Connect>
|
|
39
43
|
<ConversationRelay
|
|
40
|
-
url="${escapeXml(
|
|
44
|
+
url="${escapeXml(relayUrl)}?callSessionId=${escapeXml(callSessionId)}"
|
|
41
45
|
welcomeGreeting="${escapeXml(welcomeGreeting)}"
|
|
42
46
|
voice="Google.en-US-Journey-O"
|
|
43
47
|
language="en-US"
|
|
@@ -50,6 +54,19 @@ function generateTwiML(callSessionId: string, wssBaseUrl: string, welcomeGreetin
|
|
|
50
54
|
</Response>`;
|
|
51
55
|
}
|
|
52
56
|
|
|
57
|
+
/**
|
|
58
|
+
* Resolve the WebSocket relay URL from Twilio config.
|
|
59
|
+
*
|
|
60
|
+
* Treats wssBaseUrl as present only when it is non-empty after trimming.
|
|
61
|
+
* Falls back to webhookBaseUrl, normalizing the scheme from http(s) to ws(s)
|
|
62
|
+
* and stripping any trailing slash.
|
|
63
|
+
*/
|
|
64
|
+
export function resolveRelayUrl(wssBaseUrl: string, webhookBaseUrl: string): string {
|
|
65
|
+
const base = wssBaseUrl.trim() || webhookBaseUrl;
|
|
66
|
+
const normalized = base.replace(/\/$/, '').replace(/^http(s?)/, 'ws$1');
|
|
67
|
+
return `${normalized}/v1/calls/relay`;
|
|
68
|
+
}
|
|
69
|
+
|
|
53
70
|
/**
|
|
54
71
|
* Map Twilio call status strings to our internal CallStatus.
|
|
55
72
|
*/
|
|
@@ -93,6 +110,11 @@ export async function handleVoiceWebhook(req: Request): Promise<Response> {
|
|
|
93
110
|
return new Response('Call session not found', { status: 404 });
|
|
94
111
|
}
|
|
95
112
|
|
|
113
|
+
if (isTerminalState(session.status)) {
|
|
114
|
+
log.warn({ callSessionId, status: session.status }, 'Voice webhook: call session is in terminal state');
|
|
115
|
+
return new Response('Call session is no longer active', { status: 410 });
|
|
116
|
+
}
|
|
117
|
+
|
|
96
118
|
// Parse the Twilio POST body to capture CallSid immediately, so status
|
|
97
119
|
// callbacks (keyed by CallSid) can locate this session even if the
|
|
98
120
|
// WebSocket relay hasn't been set up yet.
|
|
@@ -103,10 +125,11 @@ export async function handleVoiceWebhook(req: Request): Promise<Response> {
|
|
|
103
125
|
log.info({ callSessionId, callSid }, 'Stored CallSid from voice webhook');
|
|
104
126
|
}
|
|
105
127
|
|
|
106
|
-
const
|
|
128
|
+
const config = getTwilioConfig();
|
|
129
|
+
const relayUrl = resolveRelayUrl(config.wssBaseUrl, config.webhookBaseUrl);
|
|
107
130
|
const welcomeGreeting = process.env.CALL_WELCOME_GREETING ?? 'Hello, how can I help you today?';
|
|
108
131
|
|
|
109
|
-
const twiml = generateTwiML(callSessionId,
|
|
132
|
+
const twiml = generateTwiML(callSessionId, relayUrl, welcomeGreeting);
|
|
110
133
|
|
|
111
134
|
log.info({ callSessionId }, 'Returning ConversationRelay TwiML');
|
|
112
135
|
|
|
@@ -126,7 +149,8 @@ export async function handleStatusCallback(req: Request): Promise<Response> {
|
|
|
126
149
|
const callStatus = formBody.get('CallStatus');
|
|
127
150
|
|
|
128
151
|
if (!callSid || !callStatus) {
|
|
129
|
-
|
|
152
|
+
const rawPayload = Object.fromEntries(formBody.entries());
|
|
153
|
+
logDeadLetterEvent('Status callback missing CallSid or CallStatus', rawPayload, log);
|
|
130
154
|
return new Response(null, { status: 200 });
|
|
131
155
|
}
|
|
132
156
|
|
|
@@ -140,39 +164,70 @@ export async function handleStatusCallback(req: Request): Promise<Response> {
|
|
|
140
164
|
|
|
141
165
|
const mappedStatus = mapTwilioStatus(callStatus);
|
|
142
166
|
if (!mappedStatus) {
|
|
143
|
-
|
|
167
|
+
const rawPayload = Object.fromEntries(formBody.entries());
|
|
168
|
+
logDeadLetterEvent(`Unknown Twilio status: ${callStatus}`, rawPayload, log);
|
|
144
169
|
return new Response(null, { status: 200 });
|
|
145
170
|
}
|
|
146
171
|
|
|
147
|
-
//
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
|
|
172
|
+
// ── Atomic idempotency claim ────────────────────────────────────
|
|
173
|
+
const timestamp = formBody.get('Timestamp');
|
|
174
|
+
const sequenceNumber = formBody.get('SequenceNumber');
|
|
175
|
+
const dedupeKey = buildCallbackDedupeKey(callSid, callStatus, timestamp, sequenceNumber);
|
|
151
176
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
const isTerminal = mappedStatus === 'completed' || mappedStatus === 'failed';
|
|
157
|
-
if (isTerminal) {
|
|
158
|
-
updates.endedAt = Date.now();
|
|
177
|
+
const claimId = claimCallback(dedupeKey, session.id);
|
|
178
|
+
if (!claimId) {
|
|
179
|
+
log.info({ callSid, callStatus, dedupeKey }, 'Duplicate status callback — skipping');
|
|
180
|
+
return new Response(null, { status: 200 });
|
|
159
181
|
}
|
|
160
182
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
183
|
+
try {
|
|
184
|
+
// Build updates
|
|
185
|
+
const updates: Parameters<typeof updateCallSession>[1] = {
|
|
186
|
+
status: mappedStatus,
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
if (mappedStatus === 'in_progress' && !session.startedAt) {
|
|
190
|
+
updates.startedAt = Date.now();
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const isTerminal = mappedStatus === 'completed' || mappedStatus === 'failed';
|
|
194
|
+
if (isTerminal) {
|
|
195
|
+
updates.endedAt = Date.now();
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
updateCallSession(session.id, updates);
|
|
199
|
+
|
|
200
|
+
// Record event
|
|
201
|
+
const eventType = isTerminal
|
|
202
|
+
? (mappedStatus === 'completed' ? 'call_ended' : 'call_failed')
|
|
203
|
+
: (mappedStatus === 'in_progress' ? 'call_connected' : 'call_started');
|
|
204
|
+
|
|
205
|
+
recordCallEvent(session.id, eventType, {
|
|
206
|
+
twilioStatus: callStatus,
|
|
207
|
+
callSid,
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
// Expire pending questions on terminal status
|
|
211
|
+
if (isTerminal) {
|
|
212
|
+
expirePendingQuestions(session.id);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Mark the claim as permanently processed so it never expires.
|
|
216
|
+
// If finalization returns false, another handler reclaimed this key
|
|
217
|
+
// after our claim expired — our business writes already landed but
|
|
218
|
+
// the dedupe row now belongs to the other handler, risking duplicate
|
|
219
|
+
// processing on later retries.
|
|
220
|
+
const finalized = finalizeCallbackClaim(dedupeKey, claimId);
|
|
221
|
+
if (!finalized) {
|
|
222
|
+
log.warn(
|
|
223
|
+
{ dedupeKey, claimId, callSid, callStatus },
|
|
224
|
+
'Lost claim during finalization — business writes committed but dedupe ownership was taken by another handler',
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
} catch (err) {
|
|
228
|
+
// Release claim so Twilio retries can reprocess
|
|
229
|
+
releaseCallbackClaim(dedupeKey, claimId);
|
|
230
|
+
throw err;
|
|
176
231
|
}
|
|
177
232
|
|
|
178
233
|
return new Response(null, { status: 200 });
|
|
@@ -193,44 +248,3 @@ export async function handleConnectAction(_req: Request): Promise<Response> {
|
|
|
193
248
|
);
|
|
194
249
|
}
|
|
195
250
|
|
|
196
|
-
/**
|
|
197
|
-
* Answer a pending question for an active call.
|
|
198
|
-
* POST /v1/calls/:callSessionId/answer
|
|
199
|
-
* Body: { answer: string }
|
|
200
|
-
*/
|
|
201
|
-
export async function handleCallAnswer(req: Request, callSessionId: string): Promise<Response> {
|
|
202
|
-
const body = await req.json() as { answer?: string };
|
|
203
|
-
if (!body.answer) {
|
|
204
|
-
return Response.json({ error: 'Missing answer' }, { status: 400 });
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
const question = getPendingQuestion(callSessionId);
|
|
208
|
-
if (!question) {
|
|
209
|
-
return Response.json({ error: 'No pending question found' }, { status: 404 });
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
// Verify the orchestrator exists before attempting to route the answer.
|
|
213
|
-
const orchestrator = getCallOrchestrator(callSessionId);
|
|
214
|
-
if (!orchestrator) {
|
|
215
|
-
log.warn({ callSessionId }, 'handleCallAnswer: no active orchestrator for call session');
|
|
216
|
-
return Response.json({ error: 'No active orchestrator for this call' }, { status: 409 });
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
// Route answer to the orchestrator FIRST — it atomically checks whether it is
|
|
220
|
-
// in the `waiting_on_user` state and transitions to `processing`. Only persist
|
|
221
|
-
// the answer to the DB if the orchestrator actually accepted it, preventing a
|
|
222
|
-
// race where the consultation timer expires between our check and the persist.
|
|
223
|
-
const accepted = await orchestrator.handleUserAnswer(body.answer);
|
|
224
|
-
if (!accepted) {
|
|
225
|
-
log.warn(
|
|
226
|
-
{ callSessionId },
|
|
227
|
-
'handleCallAnswer: orchestrator rejected the answer (not in waiting_on_user state)',
|
|
228
|
-
);
|
|
229
|
-
return Response.json({ error: 'Orchestrator is not waiting for an answer' }, { status: 409 });
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
// Mark question as answered — only after the orchestrator has accepted
|
|
233
|
-
answerPendingQuestion(question.id, body.answer);
|
|
234
|
-
|
|
235
|
-
return Response.json({ ok: true, questionId: question.id });
|
|
236
|
-
}
|
package/src/calls/types.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type CallStatus = 'initiated' | 'ringing' | 'in_progress' | 'waiting_on_user' | 'completed' | 'failed';
|
|
1
|
+
export type CallStatus = 'initiated' | 'ringing' | 'in_progress' | 'waiting_on_user' | 'completed' | 'failed' | 'cancelled';
|
|
2
2
|
export type CallEventType = 'call_started' | 'call_connected' | 'caller_spoke' | 'assistant_spoke' | 'user_question_asked' | 'user_answered' | 'call_ended' | 'call_failed';
|
|
3
3
|
export type PendingQuestionStatus = 'pending' | 'answered' | 'expired' | 'cancelled';
|
|
4
4
|
|