@vellumai/assistant 0.4.49 → 0.4.50
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 +24 -33
- package/README.md +3 -3
- package/docs/architecture/memory.md +180 -119
- package/package.json +2 -2
- package/src/__tests__/agent-loop.test.ts +3 -1
- package/src/__tests__/anthropic-provider.test.ts +114 -23
- package/src/__tests__/approval-cascade.test.ts +1 -15
- package/src/__tests__/approval-routes-http.test.ts +2 -0
- package/src/__tests__/assistant-feature-flag-guard.test.ts +0 -23
- package/src/__tests__/canonical-guardian-store.test.ts +95 -0
- package/src/__tests__/checker.test.ts +13 -0
- package/src/__tests__/config-schema.test.ts +1 -68
- package/src/__tests__/context-memory-e2e.test.ts +11 -100
- package/src/__tests__/conversation-routes-guardian-reply.test.ts +8 -0
- package/src/__tests__/conversation-routes-slash-commands.test.ts +1 -0
- package/src/__tests__/credential-security-e2e.test.ts +1 -0
- package/src/__tests__/credential-vault-unit.test.ts +4 -0
- package/src/__tests__/credential-vault.test.ts +13 -1
- package/src/__tests__/cu-unified-flow.test.ts +532 -0
- package/src/__tests__/date-context.test.ts +93 -77
- package/src/__tests__/deterministic-verification-control-plane.test.ts +64 -0
- package/src/__tests__/guardian-routing-invariants.test.ts +93 -0
- package/src/__tests__/history-repair.test.ts +245 -0
- package/src/__tests__/host-cu-proxy.test.ts +165 -3
- package/src/__tests__/http-user-message-parity.test.ts +1 -0
- package/src/__tests__/invite-redemption-service.test.ts +65 -1
- package/src/__tests__/keychain-broker-client.test.ts +4 -4
- package/src/__tests__/memory-context-benchmark.benchmark.test.ts +56 -18
- package/src/__tests__/memory-lifecycle-e2e.test.ts +244 -387
- package/src/__tests__/memory-recall-quality.test.ts +244 -407
- package/src/__tests__/memory-regressions.experimental.test.ts +126 -101
- package/src/__tests__/memory-regressions.test.ts +477 -2841
- package/src/__tests__/memory-retrieval.benchmark.test.ts +33 -150
- package/src/__tests__/memory-upsert-concurrency.test.ts +5 -244
- package/src/__tests__/mime-builder.test.ts +28 -0
- package/src/__tests__/native-web-search.test.ts +1 -0
- package/src/__tests__/oauth-cli.test.ts +572 -5
- package/src/__tests__/oauth-store.test.ts +120 -6
- package/src/__tests__/qdrant-collection-migration.test.ts +53 -8
- package/src/__tests__/registry.test.ts +0 -1
- package/src/__tests__/relay-server.test.ts +46 -1
- package/src/__tests__/schedule-tools.test.ts +32 -0
- package/src/__tests__/script-proxy-certs.test.ts +1 -1
- package/src/__tests__/secret-onetime-send.test.ts +1 -0
- package/src/__tests__/secure-keys.test.ts +7 -2
- package/src/__tests__/send-endpoint-busy.test.ts +3 -0
- package/src/__tests__/session-abort-tool-results.test.ts +1 -14
- package/src/__tests__/session-agent-loop-overflow.test.ts +1583 -0
- package/src/__tests__/session-agent-loop.test.ts +19 -15
- package/src/__tests__/session-confirmation-signals.test.ts +1 -15
- package/src/__tests__/session-error.test.ts +124 -2
- package/src/__tests__/session-history-web-search.test.ts +918 -0
- package/src/__tests__/session-pre-run-repair.test.ts +1 -14
- package/src/__tests__/session-provider-retry-repair.test.ts +25 -28
- package/src/__tests__/session-queue.test.ts +37 -27
- package/src/__tests__/session-runtime-assembly.test.ts +54 -0
- package/src/__tests__/session-slash-known.test.ts +1 -15
- package/src/__tests__/session-slash-queue.test.ts +1 -15
- package/src/__tests__/session-slash-unknown.test.ts +1 -15
- package/src/__tests__/session-workspace-cache-state.test.ts +3 -33
- package/src/__tests__/session-workspace-injection.test.ts +3 -37
- package/src/__tests__/session-workspace-tool-tracking.test.ts +3 -37
- package/src/__tests__/skills-install-extract.test.ts +93 -0
- package/src/__tests__/skillssh-registry.test.ts +451 -0
- package/src/__tests__/trust-store.test.ts +15 -0
- package/src/__tests__/voice-invite-redemption.test.ts +32 -1
- package/src/agent/ax-tree-compaction.test.ts +51 -0
- package/src/agent/loop.ts +39 -12
- package/src/approvals/AGENTS.md +1 -1
- package/src/approvals/guardian-request-resolvers.ts +14 -2
- package/src/bundler/compiler-tools.ts +66 -2
- package/src/calls/call-domain.ts +132 -0
- package/src/calls/call-store.ts +6 -0
- package/src/calls/relay-server.ts +43 -5
- package/src/calls/relay-setup-router.ts +17 -1
- package/src/calls/twilio-config.ts +1 -1
- package/src/calls/types.ts +3 -1
- package/src/cli/commands/doctor.ts +4 -3
- package/src/cli/commands/mcp.ts +46 -59
- package/src/cli/commands/memory.ts +16 -165
- package/src/cli/commands/oauth/apps.ts +31 -2
- package/src/cli/commands/oauth/connections.ts +431 -97
- package/src/cli/commands/oauth/providers.ts +15 -1
- package/src/cli/commands/sessions.ts +5 -2
- package/src/cli/commands/skills.ts +173 -1
- package/src/cli/http-client.ts +0 -20
- package/src/cli/main-screen.tsx +2 -2
- package/src/cli/program.ts +5 -6
- package/src/cli.ts +4 -10
- package/src/config/bundled-skills/computer-use/TOOLS.json +1 -1
- package/src/config/bundled-skills/computer-use/tools/computer-use-observe.ts +12 -0
- package/src/config/bundled-tool-registry.ts +2 -5
- package/src/config/schema.ts +1 -12
- package/src/config/schemas/memory-lifecycle.ts +0 -9
- package/src/config/schemas/memory-processing.ts +0 -180
- package/src/config/schemas/memory-retrieval.ts +32 -104
- package/src/config/schemas/memory.ts +0 -10
- package/src/config/types.ts +0 -4
- package/src/context/window-manager.ts +4 -1
- package/src/daemon/config-watcher.ts +61 -3
- package/src/daemon/daemon-control.ts +1 -1
- package/src/daemon/date-context.ts +114 -31
- package/src/daemon/handlers/sessions.ts +18 -13
- package/src/daemon/handlers/skills.ts +20 -1
- package/src/daemon/history-repair.ts +72 -8
- package/src/daemon/host-cu-proxy.ts +55 -26
- package/src/daemon/lifecycle.ts +31 -3
- package/src/daemon/mcp-reload-service.ts +2 -2
- package/src/daemon/message-types/computer-use.ts +1 -12
- package/src/daemon/message-types/memory.ts +4 -16
- package/src/daemon/message-types/messages.ts +1 -0
- package/src/daemon/message-types/sessions.ts +4 -0
- package/src/daemon/server.ts +12 -1
- package/src/daemon/session-agent-loop-handlers.ts +38 -0
- package/src/daemon/session-agent-loop.ts +334 -48
- package/src/daemon/session-error.ts +89 -6
- package/src/daemon/session-history.ts +17 -7
- package/src/daemon/session-media-retry.ts +6 -2
- package/src/daemon/session-memory.ts +69 -149
- package/src/daemon/session-process.ts +10 -1
- package/src/daemon/session-runtime-assembly.ts +49 -19
- package/src/daemon/session-surfaces.ts +4 -1
- package/src/daemon/session-tool-setup.ts +7 -1
- package/src/daemon/session.ts +12 -2
- package/src/instrument.ts +61 -1
- package/src/memory/admin.ts +2 -191
- package/src/memory/canonical-guardian-store.ts +38 -2
- package/src/memory/conversation-crud.ts +0 -33
- package/src/memory/conversation-queries.ts +22 -3
- package/src/memory/db-init.ts +28 -0
- package/src/memory/embedding-backend.ts +84 -8
- package/src/memory/embedding-types.ts +9 -1
- package/src/memory/indexer.ts +7 -46
- package/src/memory/items-extractor.ts +274 -76
- package/src/memory/job-handlers/backfill.ts +2 -127
- package/src/memory/job-handlers/cleanup.ts +2 -16
- package/src/memory/job-handlers/extraction.ts +2 -138
- package/src/memory/job-handlers/index-maintenance.ts +1 -6
- package/src/memory/job-handlers/summarization.ts +3 -148
- package/src/memory/job-utils.ts +21 -59
- package/src/memory/jobs-store.ts +1 -159
- package/src/memory/jobs-worker.ts +9 -52
- package/src/memory/migrations/104-core-indexes.ts +3 -3
- package/src/memory/migrations/149-oauth-tables.ts +2 -0
- package/src/memory/migrations/150-oauth-apps-client-secret-path.ts +98 -0
- package/src/memory/migrations/151-oauth-providers-ping-url.ts +11 -0
- package/src/memory/migrations/152-memory-item-supersession.ts +44 -0
- package/src/memory/migrations/153-drop-entity-tables.ts +15 -0
- package/src/memory/migrations/154-drop-fts.ts +20 -0
- package/src/memory/migrations/155-drop-conflicts.ts +7 -0
- package/src/memory/migrations/156-call-session-invite-metadata.ts +24 -0
- package/src/memory/migrations/index.ts +7 -0
- package/src/memory/qdrant-client.ts +148 -51
- package/src/memory/raw-query.ts +1 -1
- package/src/memory/retriever.test.ts +294 -273
- package/src/memory/retriever.ts +421 -645
- package/src/memory/schema/calls.ts +2 -0
- package/src/memory/schema/memory-core.ts +3 -48
- package/src/memory/schema/oauth.ts +2 -0
- package/src/memory/search/formatting.ts +263 -176
- package/src/memory/search/lexical.ts +1 -254
- package/src/memory/search/ranking.ts +0 -455
- package/src/memory/search/semantic.ts +100 -14
- package/src/memory/search/staleness.ts +47 -0
- package/src/memory/search/tier-classifier.ts +21 -0
- package/src/memory/search/types.ts +15 -77
- package/src/memory/task-memory-cleanup.ts +4 -6
- package/src/messaging/providers/gmail/mime-builder.ts +17 -7
- package/src/oauth/byo-connection.test.ts +8 -1
- package/src/oauth/oauth-store.ts +113 -27
- package/src/oauth/seed-providers.ts +6 -0
- package/src/oauth/token-persistence.ts +11 -3
- package/src/permissions/defaults.ts +1 -0
- package/src/permissions/trust-store.ts +23 -1
- package/src/playbooks/playbook-compiler.ts +1 -1
- package/src/prompts/system-prompt.ts +18 -2
- package/src/providers/anthropic/client.ts +56 -126
- package/src/providers/types.ts +7 -1
- package/src/runtime/AGENTS.md +9 -0
- package/src/runtime/auth/route-policy.ts +6 -3
- package/src/runtime/guardian-reply-router.ts +24 -22
- package/src/runtime/http-server.ts +2 -2
- package/src/runtime/invite-redemption-service.ts +19 -1
- package/src/runtime/invite-service.ts +25 -0
- package/src/runtime/pending-interactions.ts +2 -2
- package/src/runtime/routes/brain-graph-routes.ts +10 -90
- package/src/runtime/routes/conversation-routes.ts +9 -1
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +21 -12
- package/src/runtime/routes/memory-item-routes.test.ts +754 -0
- package/src/runtime/routes/memory-item-routes.ts +503 -0
- package/src/runtime/routes/session-management-routes.ts +3 -3
- package/src/runtime/routes/settings-routes.ts +2 -2
- package/src/runtime/routes/trust-rules-routes.ts +14 -0
- package/src/runtime/routes/workspace-routes.ts +2 -1
- package/src/security/keychain-broker-client.ts +17 -4
- package/src/security/secure-keys.ts +25 -3
- package/src/security/token-manager.ts +36 -36
- package/src/skills/catalog-install.ts +74 -18
- package/src/skills/skillssh-registry.ts +503 -0
- package/src/tools/assets/search.ts +5 -1
- package/src/tools/computer-use/definitions.ts +0 -10
- package/src/tools/computer-use/registry.ts +1 -1
- package/src/tools/credentials/vault.ts +1 -3
- package/src/tools/memory/definitions.ts +4 -13
- package/src/tools/memory/handlers.test.ts +83 -103
- package/src/tools/memory/handlers.ts +50 -85
- package/src/tools/schedule/create.ts +8 -1
- package/src/tools/schedule/update.ts +8 -1
- package/src/tools/skills/load.ts +25 -2
- package/src/__tests__/clarification-resolver.test.ts +0 -193
- package/src/__tests__/conflict-intent-tokenization.test.ts +0 -160
- package/src/__tests__/conflict-policy.test.ts +0 -269
- package/src/__tests__/conflict-store.test.ts +0 -372
- package/src/__tests__/contradiction-checker.test.ts +0 -361
- package/src/__tests__/entity-extractor.test.ts +0 -211
- package/src/__tests__/entity-search.test.ts +0 -1117
- package/src/__tests__/profile-compiler.test.ts +0 -392
- package/src/__tests__/session-conflict-gate.test.ts +0 -1228
- package/src/__tests__/session-profile-injection.test.ts +0 -557
- package/src/config/bundled-skills/knowledge-graph/SKILL.md +0 -25
- package/src/config/bundled-skills/knowledge-graph/TOOLS.json +0 -66
- package/src/config/bundled-skills/knowledge-graph/tools/graph-query.ts +0 -211
- package/src/daemon/session-conflict-gate.ts +0 -167
- package/src/daemon/session-dynamic-profile.ts +0 -77
- package/src/memory/clarification-resolver.ts +0 -417
- package/src/memory/conflict-intent.ts +0 -205
- package/src/memory/conflict-policy.ts +0 -127
- package/src/memory/conflict-store.ts +0 -410
- package/src/memory/contradiction-checker.ts +0 -508
- package/src/memory/entity-extractor.ts +0 -535
- package/src/memory/format-recall.ts +0 -47
- package/src/memory/fts-reconciler.ts +0 -165
- package/src/memory/job-handlers/conflict.ts +0 -200
- package/src/memory/profile-compiler.ts +0 -195
- package/src/memory/recall-cache.ts +0 -117
- package/src/memory/search/entity.ts +0 -535
- package/src/memory/search/query-expansion.test.ts +0 -70
- package/src/memory/search/query-expansion.ts +0 -118
- package/src/runtime/routes/mcp-routes.ts +0 -20
package/src/agent/loop.ts
CHANGED
|
@@ -35,6 +35,7 @@ export interface CheckpointInfo {
|
|
|
35
35
|
turnIndex: number;
|
|
36
36
|
toolCount: number;
|
|
37
37
|
hasToolUse: boolean;
|
|
38
|
+
history: Message[]; // current history snapshot for token estimation
|
|
38
39
|
}
|
|
39
40
|
|
|
40
41
|
export type CheckpointDecision = "continue" | "yield";
|
|
@@ -71,7 +72,13 @@ export type AgentEvent =
|
|
|
71
72
|
toolUseId: string;
|
|
72
73
|
accumulatedJson: string;
|
|
73
74
|
}
|
|
74
|
-
| {
|
|
75
|
+
| {
|
|
76
|
+
type: "server_tool_start";
|
|
77
|
+
name: string;
|
|
78
|
+
toolUseId: string;
|
|
79
|
+
input: Record<string, unknown>;
|
|
80
|
+
}
|
|
81
|
+
| { type: "server_tool_complete"; toolUseId: string }
|
|
75
82
|
| { type: "error"; error: Error }
|
|
76
83
|
| {
|
|
77
84
|
type: "usage";
|
|
@@ -305,6 +312,12 @@ export class AgentLoop {
|
|
|
305
312
|
type: "server_tool_start",
|
|
306
313
|
name: event.name,
|
|
307
314
|
toolUseId: event.toolUseId,
|
|
315
|
+
input: event.input,
|
|
316
|
+
});
|
|
317
|
+
} else if (event.type === "server_tool_complete") {
|
|
318
|
+
onEvent({
|
|
319
|
+
type: "server_tool_complete",
|
|
320
|
+
toolUseId: event.toolUseId,
|
|
308
321
|
});
|
|
309
322
|
}
|
|
310
323
|
},
|
|
@@ -561,6 +574,7 @@ export class AgentLoop {
|
|
|
561
574
|
turnIndex: toolUseTurns - 1, // 0-based (toolUseTurns was already incremented)
|
|
562
575
|
toolCount: toolUseBlocks.length,
|
|
563
576
|
hasToolUse: true,
|
|
577
|
+
history,
|
|
564
578
|
});
|
|
565
579
|
if (decision === "yield") {
|
|
566
580
|
break;
|
|
@@ -622,40 +636,53 @@ export function escapeAxTreeContent(content: string): string {
|
|
|
622
636
|
* `MAX_AX_TREES_IN_HISTORY` `<ax-tree>` blocks have been replaced with a
|
|
623
637
|
* short placeholder. This keeps the conversation context small so that
|
|
624
638
|
* TTFT does not grow linearly with step count in computer-use sessions.
|
|
639
|
+
*
|
|
640
|
+
* Counting is per-block, not per-message — a single user message can
|
|
641
|
+
* contain multiple tool_result blocks each with their own AX tree snapshot.
|
|
625
642
|
*/
|
|
626
643
|
export function compactAxTreeHistory(messages: Message[]): Message[] {
|
|
627
|
-
// Collect
|
|
628
|
-
const
|
|
644
|
+
// Collect (messageIndex, blockIndex) for every tool_result block with <ax-tree>
|
|
645
|
+
const axBlocks: Array<{ msgIdx: number; blockIdx: number }> = [];
|
|
629
646
|
for (let i = 0; i < messages.length; i++) {
|
|
630
647
|
const msg = messages[i];
|
|
631
648
|
if (msg.role !== "user") continue;
|
|
632
|
-
for (
|
|
649
|
+
for (let j = 0; j < msg.content.length; j++) {
|
|
650
|
+
const block = msg.content[j];
|
|
633
651
|
if (
|
|
634
652
|
block.type === "tool_result" &&
|
|
635
653
|
typeof block.content === "string" &&
|
|
636
654
|
block.content.includes("<ax-tree>")
|
|
637
655
|
) {
|
|
638
|
-
|
|
639
|
-
break;
|
|
656
|
+
axBlocks.push({ msgIdx: i, blockIdx: j });
|
|
640
657
|
}
|
|
641
658
|
}
|
|
642
659
|
}
|
|
643
660
|
|
|
644
|
-
if (
|
|
661
|
+
if (axBlocks.length <= MAX_AX_TREES_IN_HISTORY) {
|
|
645
662
|
return messages;
|
|
646
663
|
}
|
|
647
664
|
|
|
648
|
-
|
|
665
|
+
// Build a set of "msgIdx:blockIdx" keys for blocks that should be stripped
|
|
666
|
+
const toStrip = new Set(
|
|
667
|
+
axBlocks
|
|
668
|
+
.slice(0, -MAX_AX_TREES_IN_HISTORY)
|
|
669
|
+
.map((b) => `${b.msgIdx}:${b.blockIdx}`),
|
|
670
|
+
);
|
|
649
671
|
|
|
650
672
|
return messages.map((msg, idx) => {
|
|
651
|
-
|
|
673
|
+
// Quick check: does this message have any blocks to strip?
|
|
674
|
+
const hasStripTarget = msg.content.some((_, j) =>
|
|
675
|
+
toStrip.has(`${idx}:${j}`),
|
|
676
|
+
);
|
|
677
|
+
if (!hasStripTarget) return msg;
|
|
678
|
+
|
|
652
679
|
return {
|
|
653
680
|
...msg,
|
|
654
|
-
content: msg.content.map((block) => {
|
|
681
|
+
content: msg.content.map((block, j) => {
|
|
655
682
|
if (
|
|
683
|
+
toStrip.has(`${idx}:${j}`) &&
|
|
656
684
|
block.type === "tool_result" &&
|
|
657
|
-
typeof block.content === "string"
|
|
658
|
-
block.content.includes("<ax-tree>")
|
|
685
|
+
typeof block.content === "string"
|
|
659
686
|
) {
|
|
660
687
|
return {
|
|
661
688
|
...block,
|
package/src/approvals/AGENTS.md
CHANGED
|
@@ -16,7 +16,7 @@ Conversational guardian verification control-plane invocation is guardian-only.
|
|
|
16
16
|
|
|
17
17
|
## Memory Provenance Invariant
|
|
18
18
|
|
|
19
|
-
All memory
|
|
19
|
+
All memory retrieval decisions must consider actor-role provenance. Untrusted actors (non-guardian, unverified_channel) must not receive memory recall results. This invariant is enforced in `indexer.ts` (write gate) and `session-memory.ts` (read gate).
|
|
20
20
|
|
|
21
21
|
## Guardian Privilege Isolation Invariant
|
|
22
22
|
|
|
@@ -424,11 +424,17 @@ const accessRequestResolver: GuardianRequestResolver = {
|
|
|
424
424
|
dedupeKey: `trusted-contact:denied:${request.id}`,
|
|
425
425
|
});
|
|
426
426
|
} else if (desktopDeliverUrl && requesterChatId) {
|
|
427
|
+
// For Slack, route to DM via requesterExternalUserId (user ID) instead
|
|
428
|
+
// of requesterChatId (channel ID) to avoid posting in public channels.
|
|
429
|
+
const targetChatId =
|
|
430
|
+
channel === "slack" && requesterExternalUserId
|
|
431
|
+
? requesterExternalUserId
|
|
432
|
+
: requesterChatId;
|
|
427
433
|
try {
|
|
428
434
|
await deliverChannelReply(
|
|
429
435
|
desktopDeliverUrl,
|
|
430
436
|
{
|
|
431
|
-
chatId:
|
|
437
|
+
chatId: targetChatId,
|
|
432
438
|
text: "Your access request has been denied by the guardian.",
|
|
433
439
|
assistantId,
|
|
434
440
|
},
|
|
@@ -601,11 +607,17 @@ const accessRequestResolver: GuardianRequestResolver = {
|
|
|
601
607
|
});
|
|
602
608
|
}
|
|
603
609
|
} else if (desktopDeliverUrl && requesterChatId) {
|
|
610
|
+
// For Slack, route to DM via requesterExternalUserId (user ID) instead
|
|
611
|
+
// of requesterChatId (channel ID) to avoid posting in public channels.
|
|
612
|
+
const targetChatId =
|
|
613
|
+
channel === "slack" && requesterExternalUserId
|
|
614
|
+
? requesterExternalUserId
|
|
615
|
+
: requesterChatId;
|
|
604
616
|
try {
|
|
605
617
|
await deliverChannelReply(
|
|
606
618
|
desktopDeliverUrl,
|
|
607
619
|
{
|
|
608
|
-
chatId:
|
|
620
|
+
chatId: targetChatId,
|
|
609
621
|
text:
|
|
610
622
|
"Your access request has been approved! " +
|
|
611
623
|
"Please enter the 6-digit verification code you receive from the guardian.",
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* same pattern as EmbeddingRuntimeManager.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
+
import { createHash } from "node:crypto";
|
|
9
10
|
import {
|
|
10
11
|
chmodSync,
|
|
11
12
|
existsSync,
|
|
@@ -54,11 +55,69 @@ function npmTarballUrl(pkg: string, version: string): string {
|
|
|
54
55
|
return `https://registry.npmjs.org/${encoded}/-/${basename}-${version}.tgz`;
|
|
55
56
|
}
|
|
56
57
|
|
|
58
|
+
async function fetchNpmIntegrity(
|
|
59
|
+
pkg: string,
|
|
60
|
+
version: string,
|
|
61
|
+
): Promise<string> {
|
|
62
|
+
const encoded = pkg.replace("/", "%2f");
|
|
63
|
+
const metadataUrl = `https://registry.npmjs.org/${encoded}/${version}`;
|
|
64
|
+
const response = await fetch(metadataUrl);
|
|
65
|
+
if (!response.ok) {
|
|
66
|
+
throw new Error(
|
|
67
|
+
`Failed to fetch npm metadata for ${pkg}@${version}: ${response.status} ${response.statusText}`,
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const data = (await response.json()) as {
|
|
72
|
+
dist?: { integrity?: string; shasum?: string };
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
if (
|
|
76
|
+
typeof data.dist?.integrity === "string" &&
|
|
77
|
+
data.dist.integrity.length > 0
|
|
78
|
+
) {
|
|
79
|
+
return data.dist.integrity;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (typeof data.dist?.shasum === "string" && data.dist.shasum.length > 0) {
|
|
83
|
+
return `sha1-${Buffer.from(data.dist.shasum, "hex").toString("base64")}`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
throw new Error(`Missing npm integrity metadata for ${pkg}@${version}`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function verifyIntegrity(
|
|
90
|
+
tarball: Uint8Array,
|
|
91
|
+
integrity: string,
|
|
92
|
+
pkg: string,
|
|
93
|
+
version: string,
|
|
94
|
+
): void {
|
|
95
|
+
const [algorithm, expectedDigest] = integrity.split("-", 2);
|
|
96
|
+
if (!algorithm || !expectedDigest) {
|
|
97
|
+
throw new Error(`Invalid integrity metadata for ${pkg}@${version}`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (algorithm !== "sha512" && algorithm !== "sha1") {
|
|
101
|
+
throw new Error(
|
|
102
|
+
`Unsupported integrity algorithm ${algorithm} for ${pkg}@${version}`,
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const actualDigest = createHash(algorithm).update(tarball).digest("base64");
|
|
107
|
+
if (actualDigest !== expectedDigest) {
|
|
108
|
+
throw new Error(`Integrity verification failed for ${pkg}@${version}`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
57
112
|
async function downloadAndExtract(
|
|
113
|
+
pkg: string,
|
|
114
|
+
version: string,
|
|
58
115
|
url: string,
|
|
59
116
|
targetDir: string,
|
|
60
117
|
): Promise<void> {
|
|
61
|
-
log.info({ url, targetDir }, "Downloading npm package");
|
|
118
|
+
log.info({ pkg, version, url, targetDir }, "Downloading npm package");
|
|
119
|
+
|
|
120
|
+
const integrity = await fetchNpmIntegrity(pkg, version);
|
|
62
121
|
|
|
63
122
|
const response = await fetch(url);
|
|
64
123
|
if (!response.ok) {
|
|
@@ -67,7 +126,8 @@ async function downloadAndExtract(
|
|
|
67
126
|
);
|
|
68
127
|
}
|
|
69
128
|
|
|
70
|
-
const tarball = await response.arrayBuffer();
|
|
129
|
+
const tarball = new Uint8Array(await response.arrayBuffer());
|
|
130
|
+
verifyIntegrity(tarball, integrity, pkg, version);
|
|
71
131
|
mkdirSync(targetDir, { recursive: true });
|
|
72
132
|
|
|
73
133
|
const tmpTar = join(targetDir, `download-${Date.now()}.tgz`);
|
|
@@ -191,10 +251,14 @@ async function install(baseDir: string): Promise<void> {
|
|
|
191
251
|
// Download esbuild binary + preact in parallel
|
|
192
252
|
await Promise.all([
|
|
193
253
|
downloadAndExtract(
|
|
254
|
+
`@esbuild/${esbuildPlatform}`,
|
|
255
|
+
ESBUILD_VERSION,
|
|
194
256
|
npmTarballUrl(`@esbuild/${esbuildPlatform}`, ESBUILD_VERSION),
|
|
195
257
|
join(tmpDir, "esbuild-pkg"),
|
|
196
258
|
),
|
|
197
259
|
downloadAndExtract(
|
|
260
|
+
"preact",
|
|
261
|
+
PREACT_VERSION,
|
|
198
262
|
npmTarballUrl("preact", PREACT_VERSION),
|
|
199
263
|
join(tmpDir, "node_modules", "preact"),
|
|
200
264
|
),
|
package/src/calls/call-domain.ts
CHANGED
|
@@ -1017,3 +1017,135 @@ export async function startVerificationCall(
|
|
|
1017
1017
|
};
|
|
1018
1018
|
}
|
|
1019
1019
|
}
|
|
1020
|
+
|
|
1021
|
+
// ── Invite call ───────────────────────────────────────────────────────
|
|
1022
|
+
|
|
1023
|
+
export type StartInviteCallInput = {
|
|
1024
|
+
phoneNumber: string;
|
|
1025
|
+
friendName: string;
|
|
1026
|
+
guardianName: string;
|
|
1027
|
+
assistantId?: string;
|
|
1028
|
+
};
|
|
1029
|
+
|
|
1030
|
+
export type StartInviteCallResult = { ok: true; callSid: string } | CallError;
|
|
1031
|
+
|
|
1032
|
+
/**
|
|
1033
|
+
* Initiate an outbound call to deliver a voice invite to a contact.
|
|
1034
|
+
*
|
|
1035
|
+
* Creates a minimal call session with a voice channel binding and
|
|
1036
|
+
* passes invite-specific custom parameters so the relay server can
|
|
1037
|
+
* detect this is an invite redemption call.
|
|
1038
|
+
*/
|
|
1039
|
+
export async function startInviteCall(
|
|
1040
|
+
input: StartInviteCallInput,
|
|
1041
|
+
): Promise<StartInviteCallResult> {
|
|
1042
|
+
const { phoneNumber, friendName, guardianName } = input;
|
|
1043
|
+
|
|
1044
|
+
if (!phoneNumber || !E164_REGEX.test(phoneNumber)) {
|
|
1045
|
+
return {
|
|
1046
|
+
ok: false,
|
|
1047
|
+
error: "phone_number must be in E.164 format",
|
|
1048
|
+
status: 400,
|
|
1049
|
+
};
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
let sessionId: string | null = null;
|
|
1053
|
+
|
|
1054
|
+
try {
|
|
1055
|
+
const config = loadConfig();
|
|
1056
|
+
const provider = new TwilioConversationRelayProvider();
|
|
1057
|
+
|
|
1058
|
+
// Resolve the assistant's Twilio number as the caller ID
|
|
1059
|
+
const identityResult = await resolveCallerIdentity(config);
|
|
1060
|
+
if (!identityResult.ok) {
|
|
1061
|
+
return { ok: false, error: identityResult.error, status: 400 };
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
const preflightResult = await preflightVoiceIngress();
|
|
1065
|
+
if (!preflightResult.ok) {
|
|
1066
|
+
return preflightResult;
|
|
1067
|
+
}
|
|
1068
|
+
const ingressConfig = preflightResult.ingressConfig;
|
|
1069
|
+
|
|
1070
|
+
// Create a minimal conversation so the call session has a valid FK,
|
|
1071
|
+
// and bind it to the voice channel so it never appears as an unbound
|
|
1072
|
+
// desktop thread.
|
|
1073
|
+
const timestamp = Date.now();
|
|
1074
|
+
const convKey = `invite-call:${phoneNumber}:${timestamp}`;
|
|
1075
|
+
const { conversationId } = getOrCreateConversation(convKey);
|
|
1076
|
+
|
|
1077
|
+
upsertBinding({
|
|
1078
|
+
conversationId,
|
|
1079
|
+
sourceChannel: "phone",
|
|
1080
|
+
externalChatId: `invite-call:${phoneNumber}:${timestamp}`,
|
|
1081
|
+
});
|
|
1082
|
+
|
|
1083
|
+
const session = createCallSession({
|
|
1084
|
+
conversationId,
|
|
1085
|
+
provider: "twilio",
|
|
1086
|
+
fromNumber: identityResult.fromNumber,
|
|
1087
|
+
toNumber: phoneNumber,
|
|
1088
|
+
callMode: "invite",
|
|
1089
|
+
inviteFriendName: friendName,
|
|
1090
|
+
inviteGuardianName: guardianName,
|
|
1091
|
+
initiatedFromConversationId: conversationId,
|
|
1092
|
+
});
|
|
1093
|
+
sessionId = session.id;
|
|
1094
|
+
|
|
1095
|
+
const webhookUrl = await resolveCallbackUrl(
|
|
1096
|
+
() => getTwilioVoiceWebhookUrl(ingressConfig, session.id),
|
|
1097
|
+
"webhooks/twilio/voice",
|
|
1098
|
+
"twilio_voice",
|
|
1099
|
+
{ callSessionId: session.id },
|
|
1100
|
+
);
|
|
1101
|
+
const statusCallbackUrl = await resolveCallbackUrl(
|
|
1102
|
+
() => getTwilioStatusCallbackUrl(ingressConfig),
|
|
1103
|
+
"webhooks/twilio/status",
|
|
1104
|
+
"twilio_status",
|
|
1105
|
+
);
|
|
1106
|
+
|
|
1107
|
+
upsertActiveCallLease({ callSessionId: session.id });
|
|
1108
|
+
|
|
1109
|
+
const { callSid } = await provider.initiateCall({
|
|
1110
|
+
from: identityResult.fromNumber,
|
|
1111
|
+
to: phoneNumber,
|
|
1112
|
+
webhookUrl,
|
|
1113
|
+
statusCallbackUrl,
|
|
1114
|
+
});
|
|
1115
|
+
|
|
1116
|
+
updateCallSession(session.id, { providerCallSid: callSid });
|
|
1117
|
+
|
|
1118
|
+
log.info(
|
|
1119
|
+
{
|
|
1120
|
+
callSessionId: session.id,
|
|
1121
|
+
callSid,
|
|
1122
|
+
to: phoneNumber,
|
|
1123
|
+
friendName,
|
|
1124
|
+
guardianName,
|
|
1125
|
+
},
|
|
1126
|
+
"Invite call initiated",
|
|
1127
|
+
);
|
|
1128
|
+
|
|
1129
|
+
return { ok: true, callSid };
|
|
1130
|
+
} catch (err) {
|
|
1131
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1132
|
+
log.error(
|
|
1133
|
+
{ err, phoneNumber, friendName, guardianName },
|
|
1134
|
+
"Failed to initiate invite call",
|
|
1135
|
+
);
|
|
1136
|
+
|
|
1137
|
+
if (sessionId) {
|
|
1138
|
+
updateCallSession(sessionId, {
|
|
1139
|
+
status: "failed",
|
|
1140
|
+
endedAt: Date.now(),
|
|
1141
|
+
lastError: msg,
|
|
1142
|
+
});
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
return {
|
|
1146
|
+
ok: false,
|
|
1147
|
+
error: `Error initiating invite call: ${msg}`,
|
|
1148
|
+
status: 500,
|
|
1149
|
+
};
|
|
1150
|
+
}
|
|
1151
|
+
}
|
package/src/calls/call-store.ts
CHANGED
|
@@ -37,6 +37,8 @@ const parseCallSession = createRowMapper<
|
|
|
37
37
|
status: { from: "status", transform: cast<CallSession["status"]>() },
|
|
38
38
|
callMode: { from: "callMode", transform: cast<CallSession["callMode"]>() },
|
|
39
39
|
verificationSessionId: "verificationSessionId",
|
|
40
|
+
inviteFriendName: "inviteFriendName",
|
|
41
|
+
inviteGuardianName: "inviteGuardianName",
|
|
40
42
|
callerIdentityMode: "callerIdentityMode",
|
|
41
43
|
callerIdentitySource: "callerIdentitySource",
|
|
42
44
|
initiatedFromConversationId: "initiatedFromConversationId",
|
|
@@ -81,6 +83,8 @@ export function createCallSession(opts: {
|
|
|
81
83
|
task?: string;
|
|
82
84
|
callMode?: string;
|
|
83
85
|
verificationSessionId?: string;
|
|
86
|
+
inviteFriendName?: string;
|
|
87
|
+
inviteGuardianName?: string;
|
|
84
88
|
callerIdentityMode?: string;
|
|
85
89
|
callerIdentitySource?: string;
|
|
86
90
|
initiatedFromConversationId?: string;
|
|
@@ -98,6 +102,8 @@ export function createCallSession(opts: {
|
|
|
98
102
|
status: "initiated" as const,
|
|
99
103
|
callMode: (opts.callMode ?? null) as CallSession["callMode"],
|
|
100
104
|
verificationSessionId: opts.verificationSessionId ?? null,
|
|
105
|
+
inviteFriendName: opts.inviteFriendName ?? null,
|
|
106
|
+
inviteGuardianName: opts.inviteGuardianName ?? null,
|
|
101
107
|
callerIdentityMode: opts.callerIdentityMode ?? null,
|
|
102
108
|
callerIdentitySource: opts.callerIdentitySource ?? null,
|
|
103
109
|
initiatedFromConversationId: opts.initiatedFromConversationId ?? null,
|
|
@@ -575,6 +575,7 @@ export class RelayConnection {
|
|
|
575
575
|
outcome.fromNumber,
|
|
576
576
|
outcome.friendName,
|
|
577
577
|
outcome.guardianName,
|
|
578
|
+
!resolved.isInbound,
|
|
578
579
|
);
|
|
579
580
|
return;
|
|
580
581
|
case "name_capture":
|
|
@@ -772,6 +773,12 @@ export class RelayConnection {
|
|
|
772
773
|
fromNumber: string;
|
|
773
774
|
callerName?: string;
|
|
774
775
|
skipMemberActivation?: boolean;
|
|
776
|
+
activationReason?:
|
|
777
|
+
| "invite_redeemed"
|
|
778
|
+
| "access_approved"
|
|
779
|
+
| "trusted_contact_verified";
|
|
780
|
+
friendName?: string;
|
|
781
|
+
guardianName?: string;
|
|
775
782
|
}): void {
|
|
776
783
|
const { assistantId, fromNumber, callerName } = params;
|
|
777
784
|
|
|
@@ -808,7 +815,25 @@ export class RelayConnection {
|
|
|
808
815
|
updateCallSession(this.callSessionId, { status: "in_progress" });
|
|
809
816
|
|
|
810
817
|
const guardianLabel = this.resolveGuardianLabel();
|
|
811
|
-
|
|
818
|
+
let handoffText: string;
|
|
819
|
+
|
|
820
|
+
if (params.activationReason === "invite_redeemed") {
|
|
821
|
+
const name = params.friendName;
|
|
822
|
+
const assistantName = this.resolveAssistantLabel();
|
|
823
|
+
const gLabel = params.guardianName || guardianLabel;
|
|
824
|
+
if (name) {
|
|
825
|
+
handoffText = assistantName
|
|
826
|
+
? `Great, I've verified that you are ${name}. It's nice to meet you! I'm ${assistantName}, ${gLabel}'s assistant. How can I help?`
|
|
827
|
+
: `Great, I've verified that you are ${name}. It's nice to meet you! How can I help?`;
|
|
828
|
+
} else {
|
|
829
|
+
handoffText = assistantName
|
|
830
|
+
? `Great, I've verified your identity. It's nice to meet you! I'm ${assistantName}, ${gLabel}'s assistant. How can I help?`
|
|
831
|
+
: `Great, I've verified your identity. It's nice to meet you! How can I help?`;
|
|
832
|
+
}
|
|
833
|
+
} else {
|
|
834
|
+
handoffText = `Great! ${guardianLabel} said I can speak with you. How can I help?`;
|
|
835
|
+
}
|
|
836
|
+
|
|
812
837
|
this.sendTextToken(handoffText, true);
|
|
813
838
|
|
|
814
839
|
recordCallEvent(this.callSessionId, "assistant_spoke", {
|
|
@@ -1000,6 +1025,7 @@ export class RelayConnection {
|
|
|
1000
1025
|
this.continueCallAfterTrustedContactActivation({
|
|
1001
1026
|
assistantId,
|
|
1002
1027
|
fromNumber,
|
|
1028
|
+
activationReason: "trusted_contact_verified",
|
|
1003
1029
|
});
|
|
1004
1030
|
} else {
|
|
1005
1031
|
// Inbound guardian verification: binding already handled above,
|
|
@@ -1096,6 +1122,7 @@ export class RelayConnection {
|
|
|
1096
1122
|
fromNumber: string,
|
|
1097
1123
|
friendName: string | null,
|
|
1098
1124
|
guardianName: string | null,
|
|
1125
|
+
isOutbound: boolean,
|
|
1099
1126
|
): void {
|
|
1100
1127
|
this.inviteRedemptionActive = true;
|
|
1101
1128
|
this.inviteRedemptionAssistantId = assistantId;
|
|
@@ -1116,10 +1143,17 @@ export class RelayConnection {
|
|
|
1116
1143
|
|
|
1117
1144
|
const displayFriend = friendName ?? "there";
|
|
1118
1145
|
const displayGuardian = guardianName ?? "your contact";
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1146
|
+
|
|
1147
|
+
let promptText: string;
|
|
1148
|
+
if (isOutbound) {
|
|
1149
|
+
const assistantName = this.resolveAssistantLabel();
|
|
1150
|
+
promptText = assistantName
|
|
1151
|
+
? `Hi ${displayFriend}, this is ${assistantName}, ${displayGuardian}'s assistant. To get started, please enter the 6-digit code that ${displayGuardian} shared with you.`
|
|
1152
|
+
: `Hi ${displayFriend}, this is ${displayGuardian}'s assistant. To get started, please enter the 6-digit code that ${displayGuardian} shared with you.`;
|
|
1153
|
+
} else {
|
|
1154
|
+
promptText = `Welcome ${displayFriend}. Please enter the 6-digit code that ${displayGuardian} provided you to verify your identity.`;
|
|
1155
|
+
}
|
|
1156
|
+
this.sendTextToken(promptText, true);
|
|
1123
1157
|
|
|
1124
1158
|
log.info(
|
|
1125
1159
|
{ callSessionId: this.callSessionId, assistantId },
|
|
@@ -1358,6 +1392,7 @@ export class RelayConnection {
|
|
|
1358
1392
|
assistantId,
|
|
1359
1393
|
fromNumber,
|
|
1360
1394
|
callerName: callerName ?? undefined,
|
|
1395
|
+
activationReason: "access_approved",
|
|
1361
1396
|
});
|
|
1362
1397
|
|
|
1363
1398
|
recordCallEvent(
|
|
@@ -1541,6 +1576,9 @@ export class RelayConnection {
|
|
|
1541
1576
|
fromNumber: this.inviteRedemptionFromNumber,
|
|
1542
1577
|
callerName: this.inviteRedemptionFriendName ?? undefined,
|
|
1543
1578
|
skipMemberActivation: true,
|
|
1579
|
+
activationReason: "invite_redeemed",
|
|
1580
|
+
friendName: this.inviteRedemptionFriendName ?? undefined,
|
|
1581
|
+
guardianName: this.inviteRedemptionGuardianName ?? undefined,
|
|
1544
1582
|
});
|
|
1545
1583
|
} else {
|
|
1546
1584
|
this.inviteRedemptionActive = false;
|
|
@@ -99,8 +99,24 @@ export function routeSetup(ctx: SetupContext): {
|
|
|
99
99
|
actorTrust,
|
|
100
100
|
};
|
|
101
101
|
|
|
102
|
-
// ── Outbound
|
|
102
|
+
// ── Outbound flow selection based on persisted call mode ──────────
|
|
103
103
|
const persistedMode = ctx.session?.callMode;
|
|
104
|
+
|
|
105
|
+
// ── Outbound invite redemption (persisted mode) ─────────────────
|
|
106
|
+
if (persistedMode === "invite") {
|
|
107
|
+
return {
|
|
108
|
+
outcome: {
|
|
109
|
+
action: "invite_redemption" as const,
|
|
110
|
+
assistantId,
|
|
111
|
+
fromNumber: ctx.to,
|
|
112
|
+
friendName: ctx.session?.inviteFriendName ?? null,
|
|
113
|
+
guardianName: ctx.session?.inviteGuardianName ?? null,
|
|
114
|
+
},
|
|
115
|
+
resolved,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ── Outbound guardian verification (persisted mode) ──────────────
|
|
104
120
|
const persistedVsId = ctx.session?.verificationSessionId;
|
|
105
121
|
const customParamVsId = ctx.customParameters?.verificationSessionId;
|
|
106
122
|
const verificationSessionId = persistedVsId ?? customParamVsId;
|
|
@@ -20,7 +20,7 @@ export interface TwilioConfig {
|
|
|
20
20
|
|
|
21
21
|
/**
|
|
22
22
|
* Resolve the Twilio phone number using a unified fallback chain so that
|
|
23
|
-
* all callers (calls,
|
|
23
|
+
* all callers (calls, readiness checks, invite transports)
|
|
24
24
|
* agree on the same number.
|
|
25
25
|
*
|
|
26
26
|
* Resolution order:
|
package/src/calls/types.ts
CHANGED
|
@@ -58,7 +58,7 @@ export type PendingQuestionStatus =
|
|
|
58
58
|
* uses this as the primary signal for deterministic flow selection,
|
|
59
59
|
* with Twilio setup custom parameters as a secondary/observability signal.
|
|
60
60
|
*/
|
|
61
|
-
export type CallMode = "normal" | "verification";
|
|
61
|
+
export type CallMode = "normal" | "verification" | "invite";
|
|
62
62
|
|
|
63
63
|
export interface CallSession {
|
|
64
64
|
id: string;
|
|
@@ -71,6 +71,8 @@ export interface CallSession {
|
|
|
71
71
|
status: CallStatus;
|
|
72
72
|
callMode: CallMode | null;
|
|
73
73
|
verificationSessionId: string | null;
|
|
74
|
+
inviteFriendName: string | null;
|
|
75
|
+
inviteGuardianName: string | null;
|
|
74
76
|
callerIdentityMode: string | null;
|
|
75
77
|
callerIdentitySource: string | null;
|
|
76
78
|
initiatedFromConversationId?: string | null;
|
|
@@ -3,8 +3,10 @@ import { existsSync, readFileSync, statSync } from "node:fs";
|
|
|
3
3
|
|
|
4
4
|
import type { Command } from "commander";
|
|
5
5
|
|
|
6
|
+
import { getRuntimeHttpPort } from "../../config/env.js";
|
|
6
7
|
import { loadRawConfig } from "../../config/loader.js";
|
|
7
8
|
import { shouldAutoStartDaemon } from "../../daemon/connection-policy.js";
|
|
9
|
+
import { isHttpHealthy } from "../../daemon/daemon-control.js";
|
|
8
10
|
import {
|
|
9
11
|
getDbPath,
|
|
10
12
|
getLogPath,
|
|
@@ -13,7 +15,6 @@ import {
|
|
|
13
15
|
getWorkspaceHooksDir,
|
|
14
16
|
getWorkspaceSkillsDir,
|
|
15
17
|
} from "../../util/platform.js";
|
|
16
|
-
import { getHttpBaseUrl, httpHealthCheck } from "../http-client.js";
|
|
17
18
|
import { log } from "../logger.js";
|
|
18
19
|
|
|
19
20
|
export function registerDoctorCommand(program: Command): void {
|
|
@@ -57,7 +58,7 @@ Examples:
|
|
|
57
58
|
log.info("Vellum Doctor\n");
|
|
58
59
|
|
|
59
60
|
// 0. Connection policy info
|
|
60
|
-
const httpUrl =
|
|
61
|
+
const httpUrl = `http://127.0.0.1:${getRuntimeHttpPort()}`;
|
|
61
62
|
const autostart = shouldAutoStartDaemon();
|
|
62
63
|
log.info(` HTTP: ${httpUrl}`);
|
|
63
64
|
log.info(` Autostart: ${autostart ? "enabled" : "disabled"}\n`);
|
|
@@ -103,7 +104,7 @@ Examples:
|
|
|
103
104
|
|
|
104
105
|
// 3. Daemon reachable (HTTP health check)
|
|
105
106
|
try {
|
|
106
|
-
const healthy = await
|
|
107
|
+
const healthy = await isHttpHealthy();
|
|
107
108
|
if (healthy) {
|
|
108
109
|
pass("Assistant reachable");
|
|
109
110
|
} else {
|