@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
|
@@ -33,6 +33,10 @@ export interface IngressInvite {
|
|
|
33
33
|
redeemedByExternalUserId: string | null;
|
|
34
34
|
redeemedByExternalChatId: string | null;
|
|
35
35
|
redeemedAt: number | null;
|
|
36
|
+
// Voice invite fields (null for non-voice invites)
|
|
37
|
+
expectedExternalUserId: string | null;
|
|
38
|
+
voiceCodeHash: string | null;
|
|
39
|
+
voiceCodeDigits: number | null;
|
|
36
40
|
createdAt: number;
|
|
37
41
|
updatedAt: number;
|
|
38
42
|
}
|
|
@@ -90,6 +94,9 @@ function rowToInvite(row: typeof assistantIngressInvites.$inferSelect): IngressI
|
|
|
90
94
|
redeemedByExternalUserId: row.redeemedByExternalUserId,
|
|
91
95
|
redeemedByExternalChatId: row.redeemedByExternalChatId,
|
|
92
96
|
redeemedAt: row.redeemedAt,
|
|
97
|
+
expectedExternalUserId: row.expectedExternalUserId,
|
|
98
|
+
voiceCodeHash: row.voiceCodeHash,
|
|
99
|
+
voiceCodeDigits: row.voiceCodeDigits,
|
|
93
100
|
createdAt: row.createdAt,
|
|
94
101
|
updatedAt: row.updatedAt,
|
|
95
102
|
};
|
|
@@ -127,6 +134,10 @@ export function createInvite(params: {
|
|
|
127
134
|
note?: string;
|
|
128
135
|
maxUses?: number;
|
|
129
136
|
expiresInMs?: number;
|
|
137
|
+
// Voice invite metadata (all optional — omitted for non-voice invites)
|
|
138
|
+
expectedExternalUserId?: string;
|
|
139
|
+
voiceCodeHash?: string;
|
|
140
|
+
voiceCodeDigits?: number;
|
|
130
141
|
}): { invite: IngressInvite; rawToken: string } {
|
|
131
142
|
const db = getDb();
|
|
132
143
|
const now = Date.now();
|
|
@@ -148,6 +159,9 @@ export function createInvite(params: {
|
|
|
148
159
|
redeemedByExternalUserId: null,
|
|
149
160
|
redeemedByExternalChatId: null,
|
|
150
161
|
redeemedAt: null,
|
|
162
|
+
expectedExternalUserId: params.expectedExternalUserId ?? null,
|
|
163
|
+
voiceCodeHash: params.voiceCodeHash ?? null,
|
|
164
|
+
voiceCodeDigits: params.voiceCodeDigits ?? null,
|
|
151
165
|
createdAt: now,
|
|
152
166
|
updatedAt: now,
|
|
153
167
|
};
|
|
@@ -432,3 +446,34 @@ export function findByTokenHash(tokenHash: string): IngressInvite | null {
|
|
|
432
446
|
|
|
433
447
|
return row ? rowToInvite(row) : null;
|
|
434
448
|
}
|
|
449
|
+
|
|
450
|
+
// ---------------------------------------------------------------------------
|
|
451
|
+
// findActiveVoiceInvites
|
|
452
|
+
// ---------------------------------------------------------------------------
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* Find all active voice invites bound to a specific caller identity.
|
|
456
|
+
* Used by the voice invite redemption flow to locate candidate invites
|
|
457
|
+
* before code hash matching.
|
|
458
|
+
*/
|
|
459
|
+
export function findActiveVoiceInvites(params: {
|
|
460
|
+
assistantId: string;
|
|
461
|
+
expectedExternalUserId: string;
|
|
462
|
+
}): IngressInvite[] {
|
|
463
|
+
const db = getDb();
|
|
464
|
+
|
|
465
|
+
const rows = db
|
|
466
|
+
.select()
|
|
467
|
+
.from(assistantIngressInvites)
|
|
468
|
+
.where(
|
|
469
|
+
and(
|
|
470
|
+
eq(assistantIngressInvites.assistantId, params.assistantId),
|
|
471
|
+
eq(assistantIngressInvites.sourceChannel, 'voice'),
|
|
472
|
+
eq(assistantIngressInvites.status, 'active'),
|
|
473
|
+
eq(assistantIngressInvites.expectedExternalUserId, params.expectedExternalUserId),
|
|
474
|
+
),
|
|
475
|
+
)
|
|
476
|
+
.all();
|
|
477
|
+
|
|
478
|
+
return rows.map(rowToInvite);
|
|
479
|
+
}
|
|
@@ -24,21 +24,28 @@ const BACKFILL_CHECKPOINT_ID_KEY = 'memory:backfill:last_message_id';
|
|
|
24
24
|
const RELATION_BACKFILL_CHECKPOINT_KEY = 'memory:relation_backfill:last_created_at';
|
|
25
25
|
const RELATION_BACKFILL_CHECKPOINT_ID_KEY = 'memory:relation_backfill:last_message_id';
|
|
26
26
|
|
|
27
|
-
type
|
|
27
|
+
type ProvenanceTrustClass = 'guardian' | 'trusted_contact' | 'unknown';
|
|
28
28
|
|
|
29
|
-
function
|
|
29
|
+
function parseProvenanceTrustClass(rawMetadata: string | null): ProvenanceTrustClass | undefined {
|
|
30
30
|
if (!rawMetadata) return undefined;
|
|
31
31
|
try {
|
|
32
32
|
const parsedJson: unknown = JSON.parse(rawMetadata);
|
|
33
33
|
const parsed = messageMetadataSchema.safeParse(parsedJson);
|
|
34
|
-
|
|
34
|
+
if (!parsed.success) return undefined;
|
|
35
|
+
if (parsed.data.provenanceTrustClass) return parsed.data.provenanceTrustClass;
|
|
36
|
+
// Legacy fallback for rows written before provenanceTrustClass existed.
|
|
37
|
+
const legacyRole = (parsedJson as { provenanceActorRole?: unknown }).provenanceActorRole;
|
|
38
|
+
if (legacyRole === 'guardian') return 'guardian';
|
|
39
|
+
if (legacyRole === 'non-guardian') return 'trusted_contact';
|
|
40
|
+
if (legacyRole === 'unverified_channel') return 'unknown';
|
|
41
|
+
return undefined;
|
|
35
42
|
} catch {
|
|
36
43
|
return undefined;
|
|
37
44
|
}
|
|
38
45
|
}
|
|
39
46
|
|
|
40
|
-
function
|
|
41
|
-
return
|
|
47
|
+
function isTrustedTrustClass(trustClass: ProvenanceTrustClass | undefined): boolean {
|
|
48
|
+
return trustClass === 'guardian' || trustClass === undefined;
|
|
42
49
|
}
|
|
43
50
|
|
|
44
51
|
export function backfillJob(job: MemoryJob, config: AssistantConfig): void {
|
|
@@ -68,7 +75,7 @@ export function backfillJob(job: MemoryJob, config: AssistantConfig): void {
|
|
|
68
75
|
scopeId = getConversationMemoryScopeId(message.conversationId);
|
|
69
76
|
scopeCache.set(message.conversationId, scopeId);
|
|
70
77
|
}
|
|
71
|
-
const
|
|
78
|
+
const provenanceTrustClass = parseProvenanceTrustClass(message.metadata ?? null);
|
|
72
79
|
indexMessageNow({
|
|
73
80
|
messageId: message.id,
|
|
74
81
|
conversationId: message.conversationId,
|
|
@@ -76,7 +83,7 @@ export function backfillJob(job: MemoryJob, config: AssistantConfig): void {
|
|
|
76
83
|
content: message.content,
|
|
77
84
|
createdAt: message.createdAt,
|
|
78
85
|
scopeId,
|
|
79
|
-
|
|
86
|
+
provenanceTrustClass,
|
|
80
87
|
}, config.memory);
|
|
81
88
|
}
|
|
82
89
|
const lastMessage = batch[batch.length - 1];
|
|
@@ -139,8 +146,8 @@ export function backfillEntityRelationsJob(job: MemoryJob, config: AssistantConf
|
|
|
139
146
|
let queuedExtractEntityJobs = 0;
|
|
140
147
|
let skippedUntrusted = 0;
|
|
141
148
|
for (const message of batch) {
|
|
142
|
-
const
|
|
143
|
-
if (!
|
|
149
|
+
const provenanceTrustClass = parseProvenanceTrustClass(message.metadata ?? null);
|
|
150
|
+
if (!isTrustedTrustClass(provenanceTrustClass)) {
|
|
144
151
|
skippedUntrusted += 1;
|
|
145
152
|
continue;
|
|
146
153
|
}
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
import { normalizePhoneNumber } from '../../util/phone.js';
|
|
2
|
+
import { type DrizzleDb, getSqliteFrom } from '../db-connection.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* One-shot migration: normalize phone-like identity fields to E.164 format.
|
|
6
|
+
*
|
|
7
|
+
* Historical records may contain phone numbers in inconsistent formats
|
|
8
|
+
* (e.g., "(555) 123-4567", "1-555-123-4567", "+1 555 123 4567").
|
|
9
|
+
* This migration normalizes them to E.164 ("+15551234567") using the same
|
|
10
|
+
* normalizePhoneNumber utility used at runtime.
|
|
11
|
+
*
|
|
12
|
+
* Strategy:
|
|
13
|
+
* - Tables with a `channel` column: only process rows where the channel
|
|
14
|
+
* is phone-like (sms, voice, whatsapp).
|
|
15
|
+
* - The `expected_phone_e164` column is always a phone number regardless
|
|
16
|
+
* of channel, so it is normalized unconditionally.
|
|
17
|
+
*
|
|
18
|
+
* Collision handling: source queries are ordered by `updated_at DESC`
|
|
19
|
+
* (falling back to `rowid DESC` when the column is absent) so the
|
|
20
|
+
* most-recently-updated row is processed first and receives the UPDATE.
|
|
21
|
+
* When a subsequent (older) duplicate normalizes to the same value
|
|
22
|
+
* within the same unique-key scope, it is deleted — preserving the
|
|
23
|
+
* most recent state deterministically.
|
|
24
|
+
*
|
|
25
|
+
* Idempotent: already-normalized values pass through normalizePhoneNumber
|
|
26
|
+
* unchanged, and the checkpoint key prevents re-execution.
|
|
27
|
+
*/
|
|
28
|
+
export function migrateNormalizePhoneIdentities(database: DrizzleDb): void {
|
|
29
|
+
const raw = getSqliteFrom(database);
|
|
30
|
+
const checkpointKey = 'migration_normalize_phone_identities_v1';
|
|
31
|
+
const checkpoint = raw.query(
|
|
32
|
+
`SELECT 1 FROM memory_checkpoints WHERE key = ?`,
|
|
33
|
+
).get(checkpointKey);
|
|
34
|
+
if (checkpoint) return;
|
|
35
|
+
|
|
36
|
+
const PHONE_CHANNELS = ['sms', 'voice', 'whatsapp'];
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Unique key scope definition for collision detection.
|
|
40
|
+
* `peerColumns` are the other columns in the composite unique index
|
|
41
|
+
* (besides the column being normalized). When the normalized value
|
|
42
|
+
* matches an existing row with the same peer-column values, the
|
|
43
|
+
* current row is a duplicate and should be deleted.
|
|
44
|
+
* `whereClause` is an optional SQL fragment for partial unique indexes
|
|
45
|
+
* (e.g., `WHERE external_user_id IS NOT NULL`).
|
|
46
|
+
*/
|
|
47
|
+
type UniqueKeyScope = {
|
|
48
|
+
peerColumns: string[];
|
|
49
|
+
whereClause?: string;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
// Helper: normalize a column's phone-like values in a table filtered by channel.
|
|
53
|
+
// When uniqueKeyScope is provided, checks for collisions before updating.
|
|
54
|
+
// Rows are ordered by updated_at DESC (or rowid DESC as fallback) so the
|
|
55
|
+
// most-recently-updated row is processed first and survives collisions.
|
|
56
|
+
function normalizeColumnByChannel(
|
|
57
|
+
table: string,
|
|
58
|
+
column: string,
|
|
59
|
+
channelColumn: string,
|
|
60
|
+
uniqueKeyScope?: UniqueKeyScope,
|
|
61
|
+
): void {
|
|
62
|
+
const tableExists = raw.query(
|
|
63
|
+
`SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ?`,
|
|
64
|
+
).get(table);
|
|
65
|
+
if (!tableExists) return;
|
|
66
|
+
|
|
67
|
+
const colExists = raw.query(
|
|
68
|
+
`SELECT 1 FROM pragma_table_info(?) WHERE name = ?`,
|
|
69
|
+
).get(table, column);
|
|
70
|
+
if (!colExists) return;
|
|
71
|
+
|
|
72
|
+
const chanColExists = raw.query(
|
|
73
|
+
`SELECT 1 FROM pragma_table_info(?) WHERE name = ?`,
|
|
74
|
+
).get(table, channelColumn);
|
|
75
|
+
if (!chanColExists) return;
|
|
76
|
+
|
|
77
|
+
const hasUpdatedAt = !!raw.query(
|
|
78
|
+
`SELECT 1 FROM pragma_table_info(?) WHERE name = 'updated_at'`,
|
|
79
|
+
).get(table);
|
|
80
|
+
const orderBy = hasUpdatedAt ? 'updated_at DESC, rowid DESC' : 'rowid DESC';
|
|
81
|
+
|
|
82
|
+
const selectColumns = [`id`, column];
|
|
83
|
+
if (uniqueKeyScope) {
|
|
84
|
+
for (const peer of uniqueKeyScope.peerColumns) {
|
|
85
|
+
if (!selectColumns.includes(peer)) selectColumns.push(peer);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const rows = raw.query(
|
|
90
|
+
`SELECT ${selectColumns.join(', ')} FROM ${table} WHERE ${channelColumn} IN (${PHONE_CHANNELS.map(() => '?').join(',')}) AND ${column} IS NOT NULL ORDER BY ${orderBy}`,
|
|
91
|
+
).all(...PHONE_CHANNELS) as Array<{ id: string; [key: string]: string }>;
|
|
92
|
+
|
|
93
|
+
if (rows.length === 0) return;
|
|
94
|
+
|
|
95
|
+
const update = raw.prepare(
|
|
96
|
+
`UPDATE ${table} SET ${column} = ? WHERE id = ?`,
|
|
97
|
+
);
|
|
98
|
+
const deleteRow = raw.prepare(
|
|
99
|
+
`DELETE FROM ${table} WHERE id = ?`,
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
for (const row of rows) {
|
|
103
|
+
const original = row[column];
|
|
104
|
+
if (!original) continue;
|
|
105
|
+
const normalized = normalizePhoneNumber(original);
|
|
106
|
+
if (normalized && normalized !== original) {
|
|
107
|
+
if (uniqueKeyScope) {
|
|
108
|
+
// Check if another row already has the normalized value within the same unique-key scope
|
|
109
|
+
const peerConditions = uniqueKeyScope.peerColumns
|
|
110
|
+
.map((col) => `${col} = ?`)
|
|
111
|
+
.join(' AND ');
|
|
112
|
+
const peerValues = uniqueKeyScope.peerColumns.map((col) => row[col]);
|
|
113
|
+
const whereExtra = uniqueKeyScope.whereClause ? ` AND (${uniqueKeyScope.whereClause})` : '';
|
|
114
|
+
const existing = raw.query(
|
|
115
|
+
`SELECT 1 FROM ${table} WHERE ${column} = ? AND ${peerConditions} AND id != ?${whereExtra}`,
|
|
116
|
+
).get(normalized, ...peerValues, row.id);
|
|
117
|
+
if (existing) {
|
|
118
|
+
// A canonical row already exists — delete this duplicate
|
|
119
|
+
deleteRow.run(row.id);
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
update.run(normalized, row.id);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Helper: normalize a column unconditionally (no channel filter).
|
|
129
|
+
// Used for columns that are always phone numbers (e.g., expected_phone_e164).
|
|
130
|
+
// When uniqueKeyScope is provided, checks for collisions before updating.
|
|
131
|
+
// Rows are ordered by updated_at DESC (or rowid DESC as fallback) so the
|
|
132
|
+
// most-recently-updated row is processed first and survives collisions.
|
|
133
|
+
function normalizeColumnUnconditionally(
|
|
134
|
+
table: string,
|
|
135
|
+
column: string,
|
|
136
|
+
uniqueKeyScope?: UniqueKeyScope,
|
|
137
|
+
): void {
|
|
138
|
+
const tableExists = raw.query(
|
|
139
|
+
`SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ?`,
|
|
140
|
+
).get(table);
|
|
141
|
+
if (!tableExists) return;
|
|
142
|
+
|
|
143
|
+
const colExists = raw.query(
|
|
144
|
+
`SELECT 1 FROM pragma_table_info(?) WHERE name = ?`,
|
|
145
|
+
).get(table, column);
|
|
146
|
+
if (!colExists) return;
|
|
147
|
+
|
|
148
|
+
const hasUpdatedAt = !!raw.query(
|
|
149
|
+
`SELECT 1 FROM pragma_table_info(?) WHERE name = 'updated_at'`,
|
|
150
|
+
).get(table);
|
|
151
|
+
const orderBy = hasUpdatedAt ? 'updated_at DESC, rowid DESC' : 'rowid DESC';
|
|
152
|
+
|
|
153
|
+
const selectColumns = [`id`, column];
|
|
154
|
+
if (uniqueKeyScope) {
|
|
155
|
+
for (const peer of uniqueKeyScope.peerColumns) {
|
|
156
|
+
if (!selectColumns.includes(peer)) selectColumns.push(peer);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const rows = raw.query(
|
|
161
|
+
`SELECT ${selectColumns.join(', ')} FROM ${table} WHERE ${column} IS NOT NULL ORDER BY ${orderBy}`,
|
|
162
|
+
).all() as Array<{ id: string; [key: string]: string }>;
|
|
163
|
+
|
|
164
|
+
if (rows.length === 0) return;
|
|
165
|
+
|
|
166
|
+
const update = raw.prepare(
|
|
167
|
+
`UPDATE ${table} SET ${column} = ? WHERE id = ?`,
|
|
168
|
+
);
|
|
169
|
+
const deleteRow = raw.prepare(
|
|
170
|
+
`DELETE FROM ${table} WHERE id = ?`,
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
for (const row of rows) {
|
|
174
|
+
const original = row[column];
|
|
175
|
+
if (!original) continue;
|
|
176
|
+
const normalized = normalizePhoneNumber(original);
|
|
177
|
+
if (normalized && normalized !== original) {
|
|
178
|
+
if (uniqueKeyScope) {
|
|
179
|
+
const peerConditions = uniqueKeyScope.peerColumns
|
|
180
|
+
.map((col) => `${col} = ?`)
|
|
181
|
+
.join(' AND ');
|
|
182
|
+
const peerValues = uniqueKeyScope.peerColumns.map((col) => row[col]);
|
|
183
|
+
const whereExtra = uniqueKeyScope.whereClause ? ` AND (${uniqueKeyScope.whereClause})` : '';
|
|
184
|
+
const existing = raw.query(
|
|
185
|
+
`SELECT 1 FROM ${table} WHERE ${column} = ? AND ${peerConditions} AND id != ?${whereExtra}`,
|
|
186
|
+
).get(normalized, ...peerValues, row.id);
|
|
187
|
+
if (existing) {
|
|
188
|
+
deleteRow.run(row.id);
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
update.run(normalized, row.id);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
try {
|
|
198
|
+
raw.exec('BEGIN');
|
|
199
|
+
|
|
200
|
+
// ── channel_guardian_bindings ──────────────────────────────────
|
|
201
|
+
// Has `channel` column — only normalize phone-like channels.
|
|
202
|
+
// Unique index idx_channel_guardian_bindings_active is on (assistant_id, channel)
|
|
203
|
+
// and does NOT include guardian_external_user_id, so no collision risk.
|
|
204
|
+
normalizeColumnByChannel(
|
|
205
|
+
'channel_guardian_bindings',
|
|
206
|
+
'guardian_external_user_id',
|
|
207
|
+
'channel',
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
// ── assistant_ingress_members ─────────────────────────────────
|
|
211
|
+
// Has `source_channel` column — only normalize phone-like channels.
|
|
212
|
+
// Unique index idx_ingress_members_user is on (assistant_id, source_channel, external_user_id)
|
|
213
|
+
// WHERE external_user_id IS NOT NULL — collision possible when two format variants normalize
|
|
214
|
+
// to the same E.164 within the same (assistant_id, source_channel) scope.
|
|
215
|
+
normalizeColumnByChannel(
|
|
216
|
+
'assistant_ingress_members',
|
|
217
|
+
'external_user_id',
|
|
218
|
+
'source_channel',
|
|
219
|
+
{
|
|
220
|
+
peerColumns: ['assistant_id', 'source_channel'],
|
|
221
|
+
whereClause: 'external_user_id IS NOT NULL',
|
|
222
|
+
},
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
// ── channel_guardian_verification_challenges ──────────────────
|
|
226
|
+
// Has `channel` column — normalize identity columns for phone-like channels.
|
|
227
|
+
// Index idx_channel_guardian_challenges_lookup is non-unique, no collision risk.
|
|
228
|
+
normalizeColumnByChannel(
|
|
229
|
+
'channel_guardian_verification_challenges',
|
|
230
|
+
'expected_external_user_id',
|
|
231
|
+
'channel',
|
|
232
|
+
);
|
|
233
|
+
normalizeColumnByChannel(
|
|
234
|
+
'channel_guardian_verification_challenges',
|
|
235
|
+
'consumed_by_external_user_id',
|
|
236
|
+
'channel',
|
|
237
|
+
);
|
|
238
|
+
// expected_phone_e164 is always a phone number regardless of channel.
|
|
239
|
+
// No unique index includes this column, no collision risk.
|
|
240
|
+
normalizeColumnUnconditionally(
|
|
241
|
+
'channel_guardian_verification_challenges',
|
|
242
|
+
'expected_phone_e164',
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
// ── canonical_guardian_requests ───────────────────────────────
|
|
246
|
+
// Has `source_channel` column — only normalize phone-like channels.
|
|
247
|
+
// All indexes on this table are non-unique, no collision risk.
|
|
248
|
+
normalizeColumnByChannel(
|
|
249
|
+
'canonical_guardian_requests',
|
|
250
|
+
'requester_external_user_id',
|
|
251
|
+
'source_channel',
|
|
252
|
+
);
|
|
253
|
+
normalizeColumnByChannel(
|
|
254
|
+
'canonical_guardian_requests',
|
|
255
|
+
'guardian_external_user_id',
|
|
256
|
+
'source_channel',
|
|
257
|
+
);
|
|
258
|
+
normalizeColumnByChannel(
|
|
259
|
+
'canonical_guardian_requests',
|
|
260
|
+
'decided_by_external_user_id',
|
|
261
|
+
'source_channel',
|
|
262
|
+
);
|
|
263
|
+
|
|
264
|
+
// ── channel_guardian_rate_limits ──────────────────────────────
|
|
265
|
+
// Has `channel` column — only normalize phone-like channels.
|
|
266
|
+
// Unique index idx_channel_guardian_rate_limits_actor is on
|
|
267
|
+
// (assistant_id, channel, actor_external_user_id, actor_chat_id) —
|
|
268
|
+
// collision possible when two format variants normalize to the same E.164
|
|
269
|
+
// within the same (assistant_id, channel, actor_chat_id) scope.
|
|
270
|
+
normalizeColumnByChannel(
|
|
271
|
+
'channel_guardian_rate_limits',
|
|
272
|
+
'actor_external_user_id',
|
|
273
|
+
'channel',
|
|
274
|
+
{
|
|
275
|
+
peerColumns: ['assistant_id', 'channel', 'actor_chat_id'],
|
|
276
|
+
},
|
|
277
|
+
);
|
|
278
|
+
|
|
279
|
+
// Write checkpoint
|
|
280
|
+
raw.query(
|
|
281
|
+
`INSERT OR IGNORE INTO memory_checkpoints (key, value, updated_at) VALUES (?, '1', ?)`,
|
|
282
|
+
).run(checkpointKey, Date.now());
|
|
283
|
+
|
|
284
|
+
raw.exec('COMMIT');
|
|
285
|
+
} catch (e) {
|
|
286
|
+
try { raw.exec('ROLLBACK'); } catch { /* no active transaction */ }
|
|
287
|
+
throw e;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { DrizzleDb } from '../db-connection.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Add voice invite columns to assistant_ingress_invites for guardian-initiated
|
|
5
|
+
* voice invite codes. All columns are nullable to keep existing invite rows
|
|
6
|
+
* compatible.
|
|
7
|
+
*
|
|
8
|
+
* - expected_external_user_id: E.164 phone number for identity binding
|
|
9
|
+
* - voice_code_hash: SHA-256 hash of the short numeric code
|
|
10
|
+
* - voice_code_digits: configurable digit count (nullable — NULL for non-voice invites)
|
|
11
|
+
*/
|
|
12
|
+
export function migrateVoiceInviteColumns(database: DrizzleDb): void {
|
|
13
|
+
try { database.run(/*sql*/ `ALTER TABLE assistant_ingress_invites ADD COLUMN expected_external_user_id TEXT`); } catch { /* already exists */ }
|
|
14
|
+
try { database.run(/*sql*/ `ALTER TABLE assistant_ingress_invites ADD COLUMN voice_code_hash TEXT`); } catch { /* already exists */ }
|
|
15
|
+
try { database.run(/*sql*/ `ALTER TABLE assistant_ingress_invites ADD COLUMN voice_code_digits INTEGER`); } catch { /* already exists */ }
|
|
16
|
+
}
|
|
@@ -4,9 +4,9 @@ import type { DrizzleDb } from '../db-connection.js';
|
|
|
4
4
|
* Add routing_intent and routing_hints_json columns to reminders table.
|
|
5
5
|
*
|
|
6
6
|
* routing_intent controls how the reminder is delivered at trigger time:
|
|
7
|
-
* - single_channel
|
|
7
|
+
* - single_channel: deliver to a single best channel
|
|
8
8
|
* - multi_channel: deliver to a subset of channels
|
|
9
|
-
* - all_channels: deliver to every available channel
|
|
9
|
+
* - all_channels (default): deliver to every available channel
|
|
10
10
|
*
|
|
11
11
|
* routing_hints_json stores an opaque JSON object with hints for the
|
|
12
12
|
* routing engine (e.g. preferred channels, exclusions).
|
|
@@ -14,7 +14,7 @@ import type { DrizzleDb } from '../db-connection.js';
|
|
|
14
14
|
export function migrateReminderRoutingIntent(database: DrizzleDb): void {
|
|
15
15
|
try {
|
|
16
16
|
database.run(
|
|
17
|
-
/*sql*/ `ALTER TABLE reminders ADD COLUMN routing_intent TEXT NOT NULL DEFAULT '
|
|
17
|
+
/*sql*/ `ALTER TABLE reminders ADD COLUMN routing_intent TEXT NOT NULL DEFAULT 'all_channels'`,
|
|
18
18
|
);
|
|
19
19
|
} catch { /* already exists */ }
|
|
20
20
|
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { DrizzleDb } from '../db-connection.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Create canonical_guardian_requests and canonical_guardian_deliveries tables.
|
|
5
|
+
*
|
|
6
|
+
* These tables unify the split voice (guardian_action_requests / guardian_action_deliveries)
|
|
7
|
+
* and channel (channel_guardian_approval_requests) persistence models into a single
|
|
8
|
+
* canonical domain. Uses CREATE TABLE IF NOT EXISTS for idempotency.
|
|
9
|
+
*/
|
|
10
|
+
export function createCanonicalGuardianTables(database: DrizzleDb): void {
|
|
11
|
+
database.run(/*sql*/ `
|
|
12
|
+
CREATE TABLE IF NOT EXISTS canonical_guardian_requests (
|
|
13
|
+
id TEXT PRIMARY KEY,
|
|
14
|
+
kind TEXT NOT NULL,
|
|
15
|
+
source_type TEXT NOT NULL,
|
|
16
|
+
source_channel TEXT,
|
|
17
|
+
conversation_id TEXT,
|
|
18
|
+
requester_external_user_id TEXT,
|
|
19
|
+
guardian_external_user_id TEXT,
|
|
20
|
+
call_session_id TEXT,
|
|
21
|
+
pending_question_id TEXT,
|
|
22
|
+
question_text TEXT,
|
|
23
|
+
request_code TEXT,
|
|
24
|
+
tool_name TEXT,
|
|
25
|
+
input_digest TEXT,
|
|
26
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
27
|
+
answer_text TEXT,
|
|
28
|
+
decided_by_external_user_id TEXT,
|
|
29
|
+
followup_state TEXT,
|
|
30
|
+
expires_at TEXT,
|
|
31
|
+
created_at TEXT NOT NULL,
|
|
32
|
+
updated_at TEXT NOT NULL
|
|
33
|
+
)
|
|
34
|
+
`);
|
|
35
|
+
|
|
36
|
+
database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_canonical_guardian_requests_status ON canonical_guardian_requests(status)`);
|
|
37
|
+
database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_canonical_guardian_requests_guardian ON canonical_guardian_requests(guardian_external_user_id, status)`);
|
|
38
|
+
database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_canonical_guardian_requests_conversation ON canonical_guardian_requests(conversation_id, status)`);
|
|
39
|
+
database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_canonical_guardian_requests_source ON canonical_guardian_requests(source_type, status)`);
|
|
40
|
+
database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_canonical_guardian_requests_kind ON canonical_guardian_requests(kind, status)`);
|
|
41
|
+
database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_canonical_guardian_requests_request_code ON canonical_guardian_requests(request_code)`);
|
|
42
|
+
|
|
43
|
+
database.run(/*sql*/ `
|
|
44
|
+
CREATE TABLE IF NOT EXISTS canonical_guardian_deliveries (
|
|
45
|
+
id TEXT PRIMARY KEY,
|
|
46
|
+
request_id TEXT NOT NULL REFERENCES canonical_guardian_requests(id) ON DELETE CASCADE,
|
|
47
|
+
destination_channel TEXT NOT NULL,
|
|
48
|
+
destination_conversation_id TEXT,
|
|
49
|
+
destination_chat_id TEXT,
|
|
50
|
+
destination_message_id TEXT,
|
|
51
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
52
|
+
created_at TEXT NOT NULL,
|
|
53
|
+
updated_at TEXT NOT NULL
|
|
54
|
+
)
|
|
55
|
+
`);
|
|
56
|
+
|
|
57
|
+
database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_canonical_guardian_deliveries_request_id ON canonical_guardian_deliveries(request_id)`);
|
|
58
|
+
database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_canonical_guardian_deliveries_status ON canonical_guardian_deliveries(status)`);
|
|
59
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { DrizzleDb } from '../db-connection.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Add requester_chat_id column to canonical_guardian_requests.
|
|
5
|
+
*
|
|
6
|
+
* On channels like Slack, the external chat ID (channel/DM ID) differs from
|
|
7
|
+
* the sender's external user ID. Without this column the access_request
|
|
8
|
+
* resolver would deliver approval/denial messages to the wrong destination.
|
|
9
|
+
*
|
|
10
|
+
* Uses ALTER TABLE ADD COLUMN with try/catch for idempotency — no registry
|
|
11
|
+
* entry needed.
|
|
12
|
+
*/
|
|
13
|
+
export function migrateCanonicalGuardianRequesterChatId(database: DrizzleDb): void {
|
|
14
|
+
try { database.run(/*sql*/ `ALTER TABLE canonical_guardian_requests ADD COLUMN requester_chat_id TEXT`); } catch { /* already exists */ }
|
|
15
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { DrizzleDb } from '../db-connection.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Add composite index on canonical_guardian_deliveries(destination_channel, destination_chat_id).
|
|
5
|
+
*
|
|
6
|
+
* The listPendingCanonicalGuardianRequestsByDestinationChat helper queries
|
|
7
|
+
* deliveries by (destination_channel, destination_chat_id) to bridge inbound
|
|
8
|
+
* guardian replies back to canonical requests. Without an index these
|
|
9
|
+
* degrade to full table scans as delivery history grows.
|
|
10
|
+
*/
|
|
11
|
+
export function migrateCanonicalGuardianDeliveriesDestinationIndex(database: DrizzleDb): void {
|
|
12
|
+
database.run(
|
|
13
|
+
/*sql*/ `CREATE INDEX IF NOT EXISTS idx_canonical_guardian_deliveries_destination ON canonical_guardian_deliveries(destination_channel, destination_chat_id)`,
|
|
14
|
+
);
|
|
15
|
+
}
|
|
@@ -37,6 +37,8 @@ export { migrateNotificationDeliveryThreadDecision } from './032-notification-de
|
|
|
37
37
|
export { createScopedApprovalGrantsTable } from './033-scoped-approval-grants.js';
|
|
38
38
|
export { migrateGuardianActionToolMetadata } from './034-guardian-action-tool-metadata.js';
|
|
39
39
|
export { migrateGuardianActionSupersession } from './035-guardian-action-supersession.js';
|
|
40
|
+
export { migrateNormalizePhoneIdentities } from './036-normalize-phone-identities.js';
|
|
41
|
+
export { migrateVoiceInviteColumns } from './037-voice-invite-columns.js';
|
|
40
42
|
export { createCoreTables } from './100-core-tables.js';
|
|
41
43
|
export { createWatchersAndLogsTables } from './101-watchers-and-logs.js';
|
|
42
44
|
export { addCoreColumns } from './102-alter-table-columns.js';
|
|
@@ -58,6 +60,9 @@ export { createConversationAttentionTables } from './117-conversation-attention.
|
|
|
58
60
|
export { migrateReminderRoutingIntent } from './118-reminder-routing-intent.js';
|
|
59
61
|
export { migrateSchemaIndexesAndColumns } from './119-schema-indexes-and-columns.js';
|
|
60
62
|
export { migrateFkCascadeRebuilds } from './120-fk-cascade-rebuilds.js';
|
|
63
|
+
export { createCanonicalGuardianTables } from './121-canonical-guardian-requests.js';
|
|
64
|
+
export { migrateCanonicalGuardianRequesterChatId } from './122-canonical-guardian-requester-chat-id.js';
|
|
65
|
+
export { migrateCanonicalGuardianDeliveriesDestinationIndex } from './123-canonical-guardian-deliveries-destination-index.js';
|
|
61
66
|
export {
|
|
62
67
|
MIGRATION_REGISTRY,
|
|
63
68
|
type MigrationRegistryEntry,
|
|
@@ -90,6 +90,11 @@ export const MIGRATION_REGISTRY: MigrationRegistryEntry[] = [
|
|
|
90
90
|
dependsOn: ['migration_embedding_vector_blob_v1'],
|
|
91
91
|
description: 'Rebuild memory_embeddings to make vector_json nullable (pre-100 DBs had NOT NULL)',
|
|
92
92
|
},
|
|
93
|
+
{
|
|
94
|
+
key: 'migration_normalize_phone_identities_v1',
|
|
95
|
+
version: 14,
|
|
96
|
+
description: 'Normalize phone-like identity fields to E.164 format across guardian bindings, verification challenges, canonical requests, ingress members, and rate limits',
|
|
97
|
+
},
|
|
93
98
|
];
|
|
94
99
|
|
|
95
100
|
export interface MigrationValidationResult {
|
|
@@ -80,28 +80,37 @@ export class VellumQdrantClient {
|
|
|
80
80
|
|
|
81
81
|
log.info({ collection: this.collection, vectorSize: this.vectorSize }, 'Creating Qdrant collection');
|
|
82
82
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
83
|
+
try {
|
|
84
|
+
await this.client.createCollection(this.collection, {
|
|
85
|
+
vectors: {
|
|
86
|
+
size: this.vectorSize,
|
|
87
|
+
distance: 'Cosine',
|
|
88
|
+
on_disk: this.onDisk,
|
|
89
|
+
},
|
|
90
|
+
hnsw_config: {
|
|
91
|
+
on_disk: this.onDisk,
|
|
92
|
+
m: 16,
|
|
93
|
+
ef_construct: 100,
|
|
94
|
+
},
|
|
95
|
+
quantization_config: this.quantization === 'scalar'
|
|
96
|
+
? {
|
|
97
|
+
scalar: {
|
|
98
|
+
type: 'int8',
|
|
99
|
+
quantile: 0.99,
|
|
100
|
+
always_ram: true,
|
|
101
|
+
},
|
|
102
|
+
}
|
|
103
|
+
: undefined,
|
|
104
|
+
on_disk_payload: this.onDisk,
|
|
105
|
+
});
|
|
106
|
+
} catch (err) {
|
|
107
|
+
// 409 = collection was created by a concurrent caller — that's fine
|
|
108
|
+
if (err instanceof Error && 'status' in err && (err as { status: number }).status === 409) {
|
|
109
|
+
this.collectionReady = true;
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
throw err;
|
|
113
|
+
}
|
|
105
114
|
|
|
106
115
|
// Create payload indexes for efficient filtering
|
|
107
116
|
await Promise.all([
|
|
@@ -19,6 +19,7 @@ export {
|
|
|
19
19
|
migrateMemoryItemsScopeSaltedFingerprints,
|
|
20
20
|
migrateMemorySegmentsIndexes,
|
|
21
21
|
migrateMessagesFtsBackfill,
|
|
22
|
+
migrateNormalizePhoneIdentities,
|
|
22
23
|
migrateNotificationDeliveryPairingColumns,
|
|
23
24
|
migrateNotificationDeliveryThreadDecision,
|
|
24
25
|
migrateNotificationTablesSchema,
|