@vellumai/assistant 0.5.6 → 0.5.7
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/.env.example +16 -2
- package/ARCHITECTURE.md +6 -75
- package/Dockerfile +1 -1
- package/README.md +0 -2
- package/bun.lock +0 -414
- package/docs/architecture/keychain-broker.md +45 -240
- package/docs/architecture/security.md +0 -17
- package/docs/credential-execution-service.md +2 -2
- package/node_modules/@vellumai/ces-contracts/package.json +1 -0
- package/node_modules/@vellumai/ces-contracts/src/rpc.ts +119 -0
- package/node_modules/@vellumai/credential-storage/package.json +1 -0
- package/node_modules/@vellumai/egress-proxy/package.json +1 -0
- package/package.json +2 -3
- package/src/__tests__/actor-token-service.test.ts +0 -114
- package/src/__tests__/assistant-feature-flags-integration.test.ts +30 -29
- package/src/__tests__/browser-skill-endstate.test.ts +6 -5
- package/src/__tests__/btw-routes.test.ts +0 -39
- package/src/__tests__/call-domain.test.ts +0 -128
- package/src/__tests__/ces-rpc-credential-backend.test.ts +199 -0
- package/src/__tests__/channel-approval-routes.test.ts +0 -5
- package/src/__tests__/channel-readiness-service.test.ts +1 -60
- package/src/__tests__/checker.test.ts +4 -2
- package/src/__tests__/cli-command-risk-guard.test.ts +112 -0
- package/src/__tests__/config-schema-cmd.test.ts +0 -1
- package/src/__tests__/config-schema.test.ts +1 -1
- package/src/__tests__/conversation-attention-telegram.test.ts +0 -5
- package/src/__tests__/conversation-init.benchmark.test.ts +0 -2
- package/src/__tests__/conversation-skill-tools.test.ts +0 -54
- package/src/__tests__/conversation-title-service.test.ts +87 -0
- package/src/__tests__/credential-execution-feature-gates.test.ts +28 -14
- package/src/__tests__/credential-execution-managed-contract.test.ts +33 -18
- package/src/__tests__/credential-security-e2e.test.ts +0 -66
- package/src/__tests__/credential-security-invariants.test.ts +4 -45
- package/src/__tests__/credentials-cli.test.ts +78 -0
- package/src/__tests__/db-migration-rollback.test.ts +2015 -1
- package/src/__tests__/docker-signing-key-bootstrap.test.ts +34 -143
- package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +6 -4
- package/src/__tests__/guardian-routing-state.test.ts +0 -5
- package/src/__tests__/host-shell-tool.test.ts +6 -7
- package/src/__tests__/http-user-message-parity.test.ts +3 -103
- package/src/__tests__/inbound-invite-redemption.test.ts +0 -4
- package/src/__tests__/inline-skill-load-permissions.test.ts +6 -8
- package/src/__tests__/intent-routing.test.ts +0 -13
- package/src/__tests__/jobs-store-qdrant-breaker.test.ts +178 -0
- package/src/__tests__/keychain-broker-client.test.ts +161 -22
- package/src/__tests__/memory-jobs-worker-backoff.test.ts +150 -0
- package/src/__tests__/migration-export-http.test.ts +2 -2
- package/src/__tests__/migration-import-commit-http.test.ts +2 -2
- package/src/__tests__/migration-import-preflight-http.test.ts +2 -2
- package/src/__tests__/migration-validate-http.test.ts +2 -2
- package/src/__tests__/non-member-access-request.test.ts +0 -5
- package/src/__tests__/notification-decision-fallback.test.ts +4 -0
- package/src/__tests__/notification-decision-identity.test.ts +4 -0
- package/src/__tests__/permission-types.test.ts +1 -0
- package/src/__tests__/provider-managed-proxy-integration.test.ts +5 -6
- package/src/__tests__/qdrant-manager.test.ts +28 -2
- package/src/__tests__/registry.test.ts +0 -6
- package/src/__tests__/runtime-attachment-metadata.test.ts +0 -4
- package/src/__tests__/secret-routes-managed-proxy.test.ts +0 -4
- package/src/__tests__/secure-keys.test.ts +83 -263
- package/src/__tests__/shell-identity.test.ts +96 -6
- package/src/__tests__/skill-feature-flags-integration.test.ts +22 -14
- package/src/__tests__/skill-feature-flags.test.ts +46 -45
- package/src/__tests__/skill-load-feature-flag.test.ts +7 -10
- package/src/__tests__/skill-load-inline-command.test.ts +8 -12
- package/src/__tests__/skill-load-inline-includes.test.ts +6 -10
- package/src/__tests__/skill-load-tool.test.ts +0 -2
- package/src/__tests__/skill-projection-feature-flag.test.ts +33 -29
- package/src/__tests__/skills.test.ts +0 -2
- package/src/__tests__/slack-inbound-verification.test.ts +0 -4
- package/src/__tests__/suggestion-routes.test.ts +1 -32
- package/src/__tests__/system-prompt.test.ts +0 -1
- package/src/__tests__/tool-executor-shell-integration.test.ts +5 -3
- package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +0 -5
- package/src/__tests__/trusted-contact-multichannel.test.ts +0 -4
- package/src/__tests__/update-bulletin.test.ts +0 -2
- package/src/__tests__/vellum-self-knowledge-inline-command.test.ts +6 -9
- package/src/__tests__/voice-scoped-grant-consumer.test.ts +0 -6
- package/src/__tests__/workspace-migration-015-migrate-credentials-to-keychain.test.ts +252 -0
- package/src/__tests__/workspace-migration-016-migrate-credentials-from-keychain.test.ts +218 -0
- package/src/__tests__/workspace-migration-down-functions.test.ts +1009 -0
- package/src/__tests__/workspace-migrations-runner.test.ts +114 -0
- package/src/calls/audio-store.test.ts +97 -0
- package/src/calls/audio-store.ts +205 -0
- package/src/calls/call-controller.ts +85 -7
- package/src/calls/call-domain.ts +3 -0
- package/src/calls/call-store.ts +10 -3
- package/src/calls/fish-audio-client.ts +117 -0
- package/src/calls/relay-server.ts +27 -0
- package/src/calls/twilio-routes.ts +2 -1
- package/src/calls/types.ts +1 -0
- package/src/calls/voice-ingress-preflight.ts +0 -42
- package/src/calls/voice-quality.ts +26 -5
- package/src/calls/voice-session-bridge.ts +6 -12
- package/src/cli/commands/config.ts +1 -4
- package/src/cli/commands/credentials.ts +34 -4
- package/src/cli/commands/oauth/index.ts +7 -0
- package/src/cli/commands/oauth/platform.ts +179 -0
- package/src/cli/commands/platform.ts +3 -3
- package/src/config/assistant-feature-flags.ts +186 -5
- package/src/config/bundled-skills/messaging/SKILL.md +5 -5
- package/src/config/bundled-skills/phone-calls/TOOLS.json +4 -0
- package/src/config/bundled-skills/settings/TOOLS.json +2 -2
- package/src/config/bundled-skills/settings/tools/voice-config-update.ts +42 -0
- package/src/config/bundled-tool-registry.ts +1 -11
- package/src/config/env-registry.ts +1 -1
- package/src/config/env.ts +8 -14
- package/src/config/feature-flag-registry.json +48 -8
- package/src/config/loader.ts +98 -31
- package/src/config/schema.ts +4 -13
- package/src/config/schemas/calls.ts +13 -0
- package/src/config/schemas/fish-audio.ts +39 -0
- package/src/config/schemas/security.ts +0 -4
- package/src/config/types.ts +0 -1
- package/src/contacts/contact-store.ts +39 -0
- package/src/contacts/types.ts +2 -0
- package/src/credential-execution/approval-bridge.ts +1 -0
- package/src/credential-execution/executable-discovery.ts +28 -4
- package/src/credential-execution/feature-gates.ts +16 -0
- package/src/credential-execution/process-manager.ts +38 -0
- package/src/daemon/assistant-attachments.ts +9 -0
- package/src/daemon/config-watcher.ts +5 -0
- package/src/daemon/conversation-tool-setup.ts +0 -105
- package/src/daemon/conversation.ts +10 -1
- package/src/daemon/handlers/config-vercel.ts +92 -0
- package/src/daemon/handlers/skills.ts +2 -15
- package/src/daemon/install-symlink.ts +195 -0
- package/src/daemon/lifecycle.ts +227 -51
- package/src/daemon/message-types/conversations.ts +3 -4
- package/src/daemon/message-types/diagnostics.ts +3 -22
- package/src/daemon/message-types/messages.ts +0 -2
- package/src/daemon/message-types/upgrades.ts +8 -0
- package/src/daemon/server.ts +30 -92
- package/src/events/domain-events.ts +2 -1
- package/src/inbound/platform-callback-registration.ts +3 -3
- package/src/instrument.ts +8 -5
- package/src/memory/conversation-title-service.ts +50 -1
- package/src/memory/db-init.ts +12 -0
- package/src/memory/items-extractor.ts +15 -1
- package/src/memory/job-handlers/conversation-starters.ts +4 -1
- package/src/memory/jobs-store.ts +30 -5
- package/src/memory/jobs-worker.ts +31 -7
- package/src/memory/migrations/001-job-deferrals.ts +19 -0
- package/src/memory/migrations/004-entity-relation-dedup.ts +10 -0
- package/src/memory/migrations/005-fingerprint-scope-unique.ts +76 -0
- package/src/memory/migrations/006-scope-salted-fingerprints.ts +50 -0
- package/src/memory/migrations/007-assistant-id-to-self.ts +10 -0
- package/src/memory/migrations/008-remove-assistant-id-columns.ts +34 -0
- package/src/memory/migrations/009-llm-usage-events-drop-assistant-id.ts +26 -0
- package/src/memory/migrations/014-backfill-inbox-thread-state.ts +10 -0
- package/src/memory/migrations/015-drop-active-search-index.ts +17 -0
- package/src/memory/migrations/019-notification-tables-schema-migration.ts +12 -0
- package/src/memory/migrations/020-rename-macos-ios-channel-to-vellum.ts +121 -0
- package/src/memory/migrations/024-embedding-vector-blob.ts +74 -0
- package/src/memory/migrations/026a-embeddings-nullable-vector-json.ts +82 -0
- package/src/memory/migrations/036-normalize-phone-identities.ts +11 -0
- package/src/memory/migrations/116-messages-fts.ts +106 -1
- package/src/memory/migrations/126-backfill-guardian-principal-id.ts +52 -0
- package/src/memory/migrations/127-guardian-principal-id-not-null.ts +77 -0
- package/src/memory/migrations/134-contacts-notes-column.ts +13 -0
- package/src/memory/migrations/135-backfill-contact-interaction-stats.ts +20 -0
- package/src/memory/migrations/136-drop-assistant-id-columns.ts +52 -0
- package/src/memory/migrations/140-backfill-usage-cache-accounting.ts +13 -0
- package/src/memory/migrations/141-rename-verification-table.ts +54 -0
- package/src/memory/migrations/142-rename-verification-session-id-column.ts +25 -0
- package/src/memory/migrations/143-rename-guardian-verification-values.ts +35 -0
- package/src/memory/migrations/144-rename-voice-to-phone.ts +136 -0
- package/src/memory/migrations/145-drop-accounts-table.ts +32 -0
- package/src/memory/migrations/147-migrate-reminders-to-schedules.ts +14 -1
- package/src/memory/migrations/148-drop-reminders-table.ts +35 -1
- package/src/memory/migrations/150-oauth-apps-client-secret-path.ts +69 -1
- package/src/memory/migrations/162-guardian-timestamps-epoch-ms.ts +290 -0
- package/src/memory/migrations/169-rename-gmail-provider-key-to-google.ts +51 -1
- package/src/memory/migrations/174-rename-thread-starters-table.ts +47 -1
- package/src/memory/migrations/176-drop-capability-card-state.ts +13 -0
- package/src/memory/migrations/180-backfill-inline-attachments-to-disk.ts +16 -0
- package/src/memory/migrations/181-rename-thread-starters-checkpoints.ts +28 -1
- package/src/memory/migrations/190-call-session-skip-disclosure.ts +15 -0
- package/src/memory/migrations/191-backfill-audio-attachment-mime-types.ts +64 -0
- package/src/memory/migrations/192-contacts-user-file-column.ts +15 -0
- package/src/memory/migrations/index.ts +4 -0
- package/src/memory/migrations/registry.ts +90 -0
- package/src/memory/migrations/validate-migration-state.ts +137 -11
- package/src/memory/qdrant-circuit-breaker.ts +9 -0
- package/src/memory/qdrant-manager.ts +64 -7
- package/src/memory/schema/calls.ts +1 -0
- package/src/memory/schema/contacts.ts +1 -0
- package/src/notifications/decision-engine.ts +4 -1
- package/src/oauth/connection-resolver.ts +6 -4
- package/src/permissions/checker.ts +0 -38
- package/src/permissions/shell-identity.ts +76 -22
- package/src/permissions/types.ts +4 -2
- package/src/platform/client.ts +35 -7
- package/src/prompts/persona-resolver.ts +138 -0
- package/src/prompts/system-prompt.ts +36 -4
- package/src/prompts/templates/users/default.md +1 -0
- package/src/providers/registry.ts +27 -40
- package/src/runtime/auth/__tests__/credential-service.test.ts +0 -1
- package/src/runtime/auth/__tests__/external-assistant-id.test.ts +13 -68
- package/src/runtime/auth/external-assistant-id.ts +13 -59
- package/src/runtime/auth/route-policy.ts +15 -1
- package/src/runtime/auth/token-service.ts +43 -138
- package/src/runtime/channel-readiness-service.ts +1 -16
- package/src/runtime/http-server.ts +27 -2
- package/src/runtime/middleware/error-handler.ts +1 -9
- package/src/runtime/routes/audio-routes.ts +40 -0
- package/src/runtime/routes/btw-routes.ts +0 -17
- package/src/runtime/routes/conversation-query-routes.ts +63 -1
- package/src/runtime/routes/conversation-routes.ts +4 -44
- package/src/runtime/routes/diagnostics-routes.ts +1 -477
- package/src/runtime/routes/identity-routes.ts +18 -29
- package/src/runtime/routes/inbound-stages/secret-ingress-check.ts +4 -33
- package/src/runtime/routes/inbound-stages/transcribe-audio.test.ts +1 -1
- package/src/runtime/routes/integrations/vercel.ts +89 -0
- package/src/runtime/routes/log-export-routes.ts +5 -0
- package/src/runtime/routes/memory-item-routes.ts +24 -6
- package/src/runtime/routes/migration-rollback-routes.ts +209 -0
- package/src/runtime/routes/migration-routes.ts +17 -1
- package/src/runtime/routes/notification-routes.ts +58 -0
- package/src/runtime/routes/schedule-routes.ts +65 -0
- package/src/runtime/routes/settings-routes.ts +41 -1
- package/src/runtime/routes/tts-routes.ts +86 -0
- package/src/runtime/routes/upgrade-broadcast-routes.ts +26 -2
- package/src/runtime/routes/workspace-commit-routes.ts +62 -0
- package/src/runtime/routes/workspace-routes.test.ts +22 -1
- package/src/runtime/routes/workspace-routes.ts +1 -1
- package/src/runtime/routes/workspace-utils.ts +86 -2
- package/src/security/ces-credential-client.ts +59 -22
- package/src/security/ces-rpc-credential-backend.ts +85 -0
- package/src/security/credential-backend.ts +12 -88
- package/src/security/keychain-broker-client.ts +10 -2
- package/src/security/secure-keys.ts +94 -113
- package/src/skills/catalog-install.ts +13 -7
- package/src/telemetry/usage-telemetry-reporter.ts +4 -2
- package/src/tools/calls/call-start.ts +1 -0
- package/src/tools/executor.ts +0 -4
- package/src/tools/network/script-proxy/session-manager.ts +19 -4
- package/src/tools/network/web-fetch.ts +3 -1
- package/src/tools/skills/execute.ts +1 -1
- package/src/tools/types.ts +0 -8
- package/src/util/errors.ts +0 -12
- package/src/util/platform.ts +3 -50
- package/src/workspace/git-service.ts +5 -2
- package/src/workspace/migrations/001-avatar-rename.ts +15 -0
- package/src/workspace/migrations/003-seed-device-id.ts +17 -1
- package/src/workspace/migrations/004-extract-collect-usage-data.ts +33 -0
- package/src/workspace/migrations/005-add-send-diagnostics.ts +3 -0
- package/src/workspace/migrations/006-services-config.ts +49 -0
- package/src/workspace/migrations/007-web-search-provider-rename.ts +27 -0
- package/src/workspace/migrations/008-voice-timeout-and-max-steps.ts +3 -0
- package/src/workspace/migrations/009-backfill-conversation-disk-view.ts +4 -0
- package/src/workspace/migrations/010-app-dir-rename.ts +78 -0
- package/src/workspace/migrations/011-backfill-installation-id.ts +11 -0
- package/src/workspace/migrations/012-rename-conversation-disk-view-dirs.ts +44 -0
- package/src/workspace/migrations/013-repair-conversation-disk-view.ts +5 -0
- package/src/workspace/migrations/015-migrate-credentials-to-keychain.ts +153 -0
- package/src/workspace/migrations/016-extract-feature-flags-to-protected.ts +156 -0
- package/src/workspace/migrations/016-migrate-credentials-from-keychain.ts +150 -0
- package/src/workspace/migrations/017-seed-persona-dirs.ts +95 -0
- package/src/workspace/migrations/migrate-to-workspace-volume.ts +23 -1
- package/src/workspace/migrations/registry.ts +8 -0
- package/src/workspace/migrations/runner.ts +106 -2
- package/src/workspace/migrations/types.ts +4 -0
- package/src/__tests__/claude-code-skill-regression.test.ts +0 -206
- package/src/__tests__/claude-code-tool-profiles.test.ts +0 -99
- package/src/__tests__/diagnostics-export.test.ts +0 -288
- package/src/__tests__/local-gateway-health.test.ts +0 -209
- package/src/__tests__/secret-ingress-handler.test.ts +0 -120
- package/src/__tests__/swarm-conversation-integration.test.ts +0 -358
- package/src/__tests__/swarm-dag-pathological.test.ts +0 -547
- package/src/__tests__/swarm-orchestrator.test.ts +0 -463
- package/src/__tests__/swarm-plan-validator.test.ts +0 -384
- package/src/__tests__/swarm-recursion.test.ts +0 -197
- package/src/__tests__/swarm-router-planner.test.ts +0 -234
- package/src/__tests__/swarm-tool.test.ts +0 -185
- package/src/__tests__/swarm-worker-backend.test.ts +0 -144
- package/src/__tests__/swarm-worker-runner.test.ts +0 -288
- package/src/commands/__tests__/cc-command-registry.test.ts +0 -396
- package/src/commands/cc-command-registry.ts +0 -248
- package/src/config/bundled-skills/claude-code/SKILL.md +0 -53
- package/src/config/bundled-skills/claude-code/TOOLS.json +0 -47
- package/src/config/bundled-skills/claude-code/tools/claude-code.ts +0 -12
- package/src/config/bundled-skills/orchestration/SKILL.md +0 -33
- package/src/config/bundled-skills/orchestration/TOOLS.json +0 -35
- package/src/config/bundled-skills/orchestration/tools/swarm-delegate.ts +0 -12
- package/src/config/schemas/swarm.ts +0 -82
- package/src/logfire.ts +0 -135
- package/src/runtime/local-gateway-health.ts +0 -275
- package/src/security/secret-ingress.ts +0 -68
- package/src/swarm/backend-claude-code.ts +0 -225
- package/src/swarm/checkpoint.ts +0 -137
- package/src/swarm/graph-utils.ts +0 -53
- package/src/swarm/index.ts +0 -55
- package/src/swarm/limits.ts +0 -66
- package/src/swarm/orchestrator.ts +0 -424
- package/src/swarm/plan-validator.ts +0 -117
- package/src/swarm/router-planner.ts +0 -162
- package/src/swarm/router-prompts.ts +0 -39
- package/src/swarm/synthesizer.ts +0 -81
- package/src/swarm/types.ts +0 -72
- package/src/swarm/worker-backend.ts +0 -131
- package/src/swarm/worker-prompts.ts +0 -80
- package/src/swarm/worker-runner.ts +0 -170
- package/src/tools/claude-code/claude-code.ts +0 -610
- package/src/tools/swarm/delegate.ts +0 -205
|
@@ -31,3 +31,20 @@ export function migrateDropActiveSearchIndex(database: DrizzleDb): void {
|
|
|
31
31
|
throw e;
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Recreate the old idx_memory_items_active_search index with its original
|
|
37
|
+
* covering columns (before the migration added status and invalid_at as
|
|
38
|
+
* indexed columns).
|
|
39
|
+
*/
|
|
40
|
+
export function downDropActiveSearchIndex(database: DrizzleDb): void {
|
|
41
|
+
const raw = getSqliteFrom(database);
|
|
42
|
+
|
|
43
|
+
// Drop the current index if it exists, then recreate with the old column set.
|
|
44
|
+
raw.exec(/*sql*/ `DROP INDEX IF EXISTS idx_memory_items_active_search`);
|
|
45
|
+
raw.exec(/*sql*/ `
|
|
46
|
+
CREATE INDEX IF NOT EXISTS idx_memory_items_active_search
|
|
47
|
+
ON memory_items(last_seen_at DESC, subject, statement, id, kind, confidence, importance, first_seen_at, scope_id)
|
|
48
|
+
WHERE status = 'active' AND invalid_at IS NULL
|
|
49
|
+
`);
|
|
50
|
+
}
|
|
@@ -82,3 +82,15 @@ export function migrateNotificationTablesSchema(database: DrizzleDb): void {
|
|
|
82
82
|
}
|
|
83
83
|
}
|
|
84
84
|
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* No-op down: the old enum-based notification tables cannot be recreated
|
|
88
|
+
* without the original schema definitions (column names, types, constraints,
|
|
89
|
+
* and enum values). The forward migration dropped these tables entirely.
|
|
90
|
+
* Any data that was in them is permanently lost. The new signal-contract
|
|
91
|
+
* schema tables are structurally incompatible with the old enum-based ones.
|
|
92
|
+
*/
|
|
93
|
+
export function downNotificationTablesSchema(_database: DrizzleDb): void {
|
|
94
|
+
// Intentionally empty — old enum-based tables cannot be recreated without
|
|
95
|
+
// the original schema, and any data they contained is permanently lost.
|
|
96
|
+
}
|
|
@@ -130,3 +130,124 @@ export function migrateRenameChannelToVellum(database: DrizzleDb): void {
|
|
|
130
130
|
throw e;
|
|
131
131
|
}
|
|
132
132
|
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Reverse the channel rename by changing "vellum" back to "macos" in all tables.
|
|
136
|
+
*
|
|
137
|
+
* NOTE: The forward migration renamed both "macos" and "ios" to "vellum", so we
|
|
138
|
+
* cannot distinguish which rows were originally "ios". This down migration
|
|
139
|
+
* conservatively maps all "vellum" values back to "macos" since that was the
|
|
140
|
+
* primary desktop channel identifier.
|
|
141
|
+
*/
|
|
142
|
+
export function downRenameChannelToVellum(database: DrizzleDb): void {
|
|
143
|
+
const raw = getSqliteFrom(database);
|
|
144
|
+
|
|
145
|
+
try {
|
|
146
|
+
raw.exec("BEGIN");
|
|
147
|
+
|
|
148
|
+
// guardian_action_deliveries.destination_channel
|
|
149
|
+
const gadExists = raw
|
|
150
|
+
.query(
|
|
151
|
+
`SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'guardian_action_deliveries'`,
|
|
152
|
+
)
|
|
153
|
+
.get();
|
|
154
|
+
if (gadExists) {
|
|
155
|
+
raw
|
|
156
|
+
.query(
|
|
157
|
+
`UPDATE guardian_action_deliveries SET destination_channel = 'macos' WHERE destination_channel = 'vellum'`,
|
|
158
|
+
)
|
|
159
|
+
.run();
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// messages.user_message_channel / assistant_message_channel
|
|
163
|
+
const msgsExists = raw
|
|
164
|
+
.query(
|
|
165
|
+
`SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'messages'`,
|
|
166
|
+
)
|
|
167
|
+
.get();
|
|
168
|
+
if (msgsExists) {
|
|
169
|
+
const hasUserMsgChannel = raw
|
|
170
|
+
.query(
|
|
171
|
+
`SELECT 1 FROM pragma_table_info('messages') WHERE name = 'user_message_channel'`,
|
|
172
|
+
)
|
|
173
|
+
.get();
|
|
174
|
+
if (hasUserMsgChannel) {
|
|
175
|
+
raw
|
|
176
|
+
.query(
|
|
177
|
+
`UPDATE messages SET user_message_channel = 'macos' WHERE user_message_channel = 'vellum'`,
|
|
178
|
+
)
|
|
179
|
+
.run();
|
|
180
|
+
}
|
|
181
|
+
const hasAssistantMsgChannel = raw
|
|
182
|
+
.query(
|
|
183
|
+
`SELECT 1 FROM pragma_table_info('messages') WHERE name = 'assistant_message_channel'`,
|
|
184
|
+
)
|
|
185
|
+
.get();
|
|
186
|
+
if (hasAssistantMsgChannel) {
|
|
187
|
+
raw
|
|
188
|
+
.query(
|
|
189
|
+
`UPDATE messages SET assistant_message_channel = 'macos' WHERE assistant_message_channel = 'vellum'`,
|
|
190
|
+
)
|
|
191
|
+
.run();
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// external_conversation_bindings.source_channel
|
|
196
|
+
const ecbExists = raw
|
|
197
|
+
.query(
|
|
198
|
+
`SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'external_conversation_bindings'`,
|
|
199
|
+
)
|
|
200
|
+
.get();
|
|
201
|
+
if (ecbExists) {
|
|
202
|
+
raw
|
|
203
|
+
.query(
|
|
204
|
+
`UPDATE external_conversation_bindings SET source_channel = 'macos' WHERE source_channel = 'vellum'`,
|
|
205
|
+
)
|
|
206
|
+
.run();
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// assistant_inbox_thread_state.source_channel
|
|
210
|
+
const aitsExists = raw
|
|
211
|
+
.query(
|
|
212
|
+
`SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'assistant_inbox_thread_state'`,
|
|
213
|
+
)
|
|
214
|
+
.get();
|
|
215
|
+
if (aitsExists) {
|
|
216
|
+
raw
|
|
217
|
+
.query(
|
|
218
|
+
`UPDATE assistant_inbox_thread_state SET source_channel = 'macos' WHERE source_channel = 'vellum'`,
|
|
219
|
+
)
|
|
220
|
+
.run();
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// conversations.origin_channel
|
|
224
|
+
const convExists = raw
|
|
225
|
+
.query(
|
|
226
|
+
`SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'conversations'`,
|
|
227
|
+
)
|
|
228
|
+
.get();
|
|
229
|
+
if (convExists) {
|
|
230
|
+
const hasOriginChannel = raw
|
|
231
|
+
.query(
|
|
232
|
+
`SELECT 1 FROM pragma_table_info('conversations') WHERE name = 'origin_channel'`,
|
|
233
|
+
)
|
|
234
|
+
.get();
|
|
235
|
+
if (hasOriginChannel) {
|
|
236
|
+
raw
|
|
237
|
+
.query(
|
|
238
|
+
`UPDATE conversations SET origin_channel = 'macos' WHERE origin_channel = 'vellum'`,
|
|
239
|
+
)
|
|
240
|
+
.run();
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
raw.exec("COMMIT");
|
|
245
|
+
} catch (e) {
|
|
246
|
+
try {
|
|
247
|
+
raw.exec("ROLLBACK");
|
|
248
|
+
} catch {
|
|
249
|
+
/* no active transaction */
|
|
250
|
+
}
|
|
251
|
+
throw e;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
@@ -71,3 +71,77 @@ export function migrateEmbeddingVectorBlob(database: DrizzleDb): void {
|
|
|
71
71
|
.run(checkpointKey, Date.now());
|
|
72
72
|
}
|
|
73
73
|
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Drop the vector_blob column from memory_embeddings.
|
|
77
|
+
*
|
|
78
|
+
* NOTE: Binary embedding data stored in vector_blob is lost on rollback.
|
|
79
|
+
* Rows that still have vector_json will continue to work; rows that only
|
|
80
|
+
* had vector_blob will lose their embedding vectors.
|
|
81
|
+
*
|
|
82
|
+
* SQLite does not support DROP COLUMN on all versions, so we rebuild the table.
|
|
83
|
+
*/
|
|
84
|
+
export function downEmbeddingVectorBlob(database: DrizzleDb): void {
|
|
85
|
+
const raw = getSqliteFrom(database);
|
|
86
|
+
|
|
87
|
+
// Check if vector_blob column exists
|
|
88
|
+
const hasColumn = raw
|
|
89
|
+
.query(
|
|
90
|
+
`SELECT 1 FROM pragma_table_info('memory_embeddings') WHERE name = 'vector_blob'`,
|
|
91
|
+
)
|
|
92
|
+
.get();
|
|
93
|
+
if (!hasColumn) return;
|
|
94
|
+
|
|
95
|
+
raw.exec("PRAGMA foreign_keys = OFF");
|
|
96
|
+
try {
|
|
97
|
+
raw.exec("BEGIN");
|
|
98
|
+
|
|
99
|
+
// Get the current columns minus vector_blob
|
|
100
|
+
const columns = raw
|
|
101
|
+
.query(`SELECT name FROM pragma_table_info('memory_embeddings')`)
|
|
102
|
+
.all() as Array<{ name: string }>;
|
|
103
|
+
const keepColumns = columns
|
|
104
|
+
.map((c) => c.name)
|
|
105
|
+
.filter((n) => n !== "vector_blob");
|
|
106
|
+
|
|
107
|
+
// Get the current DDL to understand the table structure
|
|
108
|
+
const ddl = raw
|
|
109
|
+
.query(
|
|
110
|
+
`SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'memory_embeddings'`,
|
|
111
|
+
)
|
|
112
|
+
.get() as { sql: string } | null;
|
|
113
|
+
if (!ddl) {
|
|
114
|
+
raw.exec("ROLLBACK");
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Remove the vector_blob column definition from the DDL
|
|
119
|
+
const newDdl = ddl.sql
|
|
120
|
+
.replace(/,\s*vector_blob\s+BLOB/i, "")
|
|
121
|
+
.replace("memory_embeddings", "memory_embeddings_new");
|
|
122
|
+
|
|
123
|
+
raw.exec(newDdl);
|
|
124
|
+
|
|
125
|
+
const colList = keepColumns.join(", ");
|
|
126
|
+
raw.exec(/*sql*/ `
|
|
127
|
+
INSERT INTO memory_embeddings_new (${colList})
|
|
128
|
+
SELECT ${colList} FROM memory_embeddings
|
|
129
|
+
`);
|
|
130
|
+
|
|
131
|
+
raw.exec(/*sql*/ `DROP TABLE memory_embeddings`);
|
|
132
|
+
raw.exec(
|
|
133
|
+
/*sql*/ `ALTER TABLE memory_embeddings_new RENAME TO memory_embeddings`,
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
raw.exec("COMMIT");
|
|
137
|
+
} catch (e) {
|
|
138
|
+
try {
|
|
139
|
+
raw.exec("ROLLBACK");
|
|
140
|
+
} catch {
|
|
141
|
+
/* no active transaction */
|
|
142
|
+
}
|
|
143
|
+
throw e;
|
|
144
|
+
} finally {
|
|
145
|
+
raw.exec("PRAGMA foreign_keys = ON");
|
|
146
|
+
}
|
|
147
|
+
}
|
|
@@ -105,6 +105,88 @@ export function migrateEmbeddingsNullableVectorJson(database: DrizzleDb): void {
|
|
|
105
105
|
}
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
+
/**
|
|
109
|
+
* Reverse v13: rebuild memory_embeddings with NOT NULL on vector_json.
|
|
110
|
+
*
|
|
111
|
+
* WARNING: Any rows with NULL vector_json will be lost — they cannot satisfy
|
|
112
|
+
* the NOT NULL constraint. This is acceptable because the forward migration
|
|
113
|
+
* only relaxed the constraint; rows written after the forward migration may
|
|
114
|
+
* have NULL vector_json (relying on vector_blob instead).
|
|
115
|
+
*/
|
|
116
|
+
export function downEmbeddingsNullableVectorJson(database: DrizzleDb): void {
|
|
117
|
+
const raw = getSqliteFrom(database);
|
|
118
|
+
|
|
119
|
+
const tableExists = raw
|
|
120
|
+
.query(
|
|
121
|
+
`SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'memory_embeddings'`,
|
|
122
|
+
)
|
|
123
|
+
.get();
|
|
124
|
+
if (!tableExists) return;
|
|
125
|
+
|
|
126
|
+
// Check if vector_json already has NOT NULL — already rolled back
|
|
127
|
+
const ddl = raw
|
|
128
|
+
.query(
|
|
129
|
+
`SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'memory_embeddings'`,
|
|
130
|
+
)
|
|
131
|
+
.get() as { sql: string } | null;
|
|
132
|
+
if (ddl && isColumnNotNull(ddl.sql, "vector_json")) return;
|
|
133
|
+
|
|
134
|
+
raw.exec("PRAGMA foreign_keys = OFF");
|
|
135
|
+
try {
|
|
136
|
+
raw.exec("BEGIN");
|
|
137
|
+
|
|
138
|
+
raw.exec(/*sql*/ `
|
|
139
|
+
CREATE TABLE memory_embeddings_new (
|
|
140
|
+
id TEXT PRIMARY KEY,
|
|
141
|
+
target_type TEXT NOT NULL,
|
|
142
|
+
target_id TEXT NOT NULL,
|
|
143
|
+
provider TEXT NOT NULL,
|
|
144
|
+
model TEXT NOT NULL,
|
|
145
|
+
dimensions INTEGER NOT NULL,
|
|
146
|
+
vector_json TEXT NOT NULL,
|
|
147
|
+
vector_blob BLOB,
|
|
148
|
+
content_hash TEXT,
|
|
149
|
+
created_at INTEGER NOT NULL,
|
|
150
|
+
updated_at INTEGER NOT NULL,
|
|
151
|
+
UNIQUE (target_type, target_id, provider, model)
|
|
152
|
+
)
|
|
153
|
+
`);
|
|
154
|
+
// Only copy rows where vector_json is NOT NULL — rows with NULL cannot
|
|
155
|
+
// satisfy the restored constraint and are lost.
|
|
156
|
+
raw.exec(/*sql*/ `
|
|
157
|
+
INSERT OR IGNORE INTO memory_embeddings_new (
|
|
158
|
+
id, target_type, target_id, provider, model, dimensions,
|
|
159
|
+
vector_json, vector_blob, content_hash, created_at, updated_at
|
|
160
|
+
)
|
|
161
|
+
SELECT
|
|
162
|
+
id, target_type, target_id, provider, model, dimensions,
|
|
163
|
+
vector_json, vector_blob, content_hash, created_at, updated_at
|
|
164
|
+
FROM memory_embeddings
|
|
165
|
+
WHERE vector_json IS NOT NULL
|
|
166
|
+
ORDER BY updated_at DESC
|
|
167
|
+
`);
|
|
168
|
+
raw.exec(/*sql*/ `DROP TABLE memory_embeddings`);
|
|
169
|
+
raw.exec(
|
|
170
|
+
/*sql*/ `ALTER TABLE memory_embeddings_new RENAME TO memory_embeddings`,
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
raw.exec(
|
|
174
|
+
/*sql*/ `CREATE INDEX IF NOT EXISTS idx_memory_embeddings_content_hash ON memory_embeddings(content_hash, provider, model)`,
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
raw.exec("COMMIT");
|
|
178
|
+
} catch (e) {
|
|
179
|
+
try {
|
|
180
|
+
raw.exec("ROLLBACK");
|
|
181
|
+
} catch {
|
|
182
|
+
/* no active transaction */
|
|
183
|
+
}
|
|
184
|
+
throw e;
|
|
185
|
+
} finally {
|
|
186
|
+
raw.exec("PRAGMA foreign_keys = ON");
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
108
190
|
/** Check whether a column is declared NOT NULL in a CREATE TABLE DDL string. */
|
|
109
191
|
function isColumnNotNull(ddl: string, column: string): boolean {
|
|
110
192
|
const pattern = new RegExp(`${column}\\s+\\w+.*?NOT\\s+NULL`, "i");
|
|
@@ -336,3 +336,14 @@ export function migrateNormalizePhoneIdentities(database: DrizzleDb): void {
|
|
|
336
336
|
throw e;
|
|
337
337
|
}
|
|
338
338
|
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Reverse v14: no-op — original non-E.164 phone formats are not recoverable.
|
|
342
|
+
*
|
|
343
|
+
* The forward migration normalised phone numbers to E.164. The original
|
|
344
|
+
* formatting (parentheses, dashes, spaces, country-code variants) was
|
|
345
|
+
* discarded during normalisation and cannot be reconstructed.
|
|
346
|
+
*/
|
|
347
|
+
export function downNormalizePhoneIdentities(_database: DrizzleDb): void {
|
|
348
|
+
// Lossy — original phone formats are not recoverable.
|
|
349
|
+
}
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { getLogger } from "../../util/logger.js";
|
|
2
|
+
import { type DrizzleDb, getSqliteFrom } from "../db-connection.js";
|
|
3
|
+
|
|
4
|
+
const logger = getLogger("messages-fts");
|
|
2
5
|
|
|
3
6
|
/**
|
|
4
7
|
* FTS5 virtual table for full-text search over messages.content.
|
|
@@ -21,7 +24,81 @@ import type { DrizzleDb } from "../db-connection.js";
|
|
|
21
24
|
* ALL writes to the messages table to fail until the FTS table is rebuilt.
|
|
22
25
|
* If this happens, `messages_fts` should be dropped and recreated, then
|
|
23
26
|
* backfilled via `migrateMessagesFtsBackfill`.
|
|
27
|
+
*
|
|
28
|
+
* ## Auto-recovery from corruption
|
|
29
|
+
*
|
|
30
|
+
* After creating (or finding an existing) messages_fts table, we probe it
|
|
31
|
+
* with a lightweight MATCH query that exercises the FTS inverted index
|
|
32
|
+
* in O(1). If the probe throws SQLITE_CORRUPT_VTAB or SQLITE_CORRUPT,
|
|
33
|
+
* we force-remove all shadow tables and the vtable entry (falling back
|
|
34
|
+
* to `PRAGMA writable_schema` if DROP TABLE itself fails on the corrupt
|
|
35
|
+
* vtable) and recreate it from scratch. The subsequent
|
|
36
|
+
* `migrateMessagesFtsBackfill` call in db-init.ts will repopulate the
|
|
37
|
+
* index from the messages table — no message data is lost.
|
|
38
|
+
*/
|
|
39
|
+
function isSqliteCorruptionError(err: unknown): boolean {
|
|
40
|
+
const code =
|
|
41
|
+
err != null && typeof err === "object" && "code" in err
|
|
42
|
+
? (err as { code: string }).code
|
|
43
|
+
: undefined;
|
|
44
|
+
return code === "SQLITE_CORRUPT_VTAB" || code === "SQLITE_CORRUPT";
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Force-remove all FTS5 shadow tables, triggers, and the vtable entry.
|
|
49
|
+
*
|
|
50
|
+
* We drop each artifact individually so that a corrupt shadow table
|
|
51
|
+
* doesn't block cleanup of the others. If `DROP TABLE messages_fts`
|
|
52
|
+
* itself fails (FTS5's xDestroy hits a corrupt shadow table), we fall
|
|
53
|
+
* back to `PRAGMA writable_schema` to delete the vtable entry directly
|
|
54
|
+
* from `sqlite_schema`. Without this fallback, `CREATE VIRTUAL TABLE
|
|
55
|
+
* IF NOT EXISTS` would be a no-op and the crash loop would persist.
|
|
24
56
|
*/
|
|
57
|
+
function dropFtsShadowTables(raw: ReturnType<typeof getSqliteFrom>): void {
|
|
58
|
+
const drops = [
|
|
59
|
+
`DROP TRIGGER IF EXISTS messages_fts_ai`,
|
|
60
|
+
`DROP TRIGGER IF EXISTS messages_fts_ad`,
|
|
61
|
+
`DROP TRIGGER IF EXISTS messages_fts_au`,
|
|
62
|
+
`DROP TABLE IF EXISTS messages_fts_config`,
|
|
63
|
+
`DROP TABLE IF EXISTS messages_fts_docsize`,
|
|
64
|
+
`DROP TABLE IF EXISTS messages_fts_content`,
|
|
65
|
+
`DROP TABLE IF EXISTS messages_fts_idx`,
|
|
66
|
+
`DROP TABLE IF EXISTS messages_fts_data`,
|
|
67
|
+
];
|
|
68
|
+
for (const sql of drops) {
|
|
69
|
+
try {
|
|
70
|
+
raw.exec(sql);
|
|
71
|
+
} catch {
|
|
72
|
+
// Shadow table may itself be corrupt — ignore and continue
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Try the normal DROP TABLE path first (lets FTS5 clean up properly).
|
|
77
|
+
try {
|
|
78
|
+
raw.exec(`DROP TABLE IF EXISTS messages_fts`);
|
|
79
|
+
} catch {
|
|
80
|
+
// FTS5's xDestroy failed — force-remove the vtable entry from
|
|
81
|
+
// sqlite_schema so CREATE VIRTUAL TABLE isn't a no-op.
|
|
82
|
+
logger.warn(
|
|
83
|
+
"[messages-fts] DROP TABLE messages_fts failed — removing vtable entry via writable_schema",
|
|
84
|
+
);
|
|
85
|
+
raw.exec(`PRAGMA writable_schema = ON`);
|
|
86
|
+
try {
|
|
87
|
+
raw.exec(
|
|
88
|
+
`DELETE FROM sqlite_schema WHERE type = 'table' AND name = 'messages_fts'`,
|
|
89
|
+
);
|
|
90
|
+
} catch (schemaErr) {
|
|
91
|
+
logger.error(
|
|
92
|
+
{ err: schemaErr },
|
|
93
|
+
"[messages-fts] Failed to remove vtable entry from sqlite_schema",
|
|
94
|
+
);
|
|
95
|
+
throw schemaErr;
|
|
96
|
+
} finally {
|
|
97
|
+
raw.exec(`PRAGMA writable_schema = OFF`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
25
102
|
export function createMessagesFts(database: DrizzleDb): void {
|
|
26
103
|
database.run(/*sql*/ `
|
|
27
104
|
CREATE VIRTUAL TABLE IF NOT EXISTS messages_fts USING fts5(
|
|
@@ -30,6 +107,34 @@ export function createMessagesFts(database: DrizzleDb): void {
|
|
|
30
107
|
)
|
|
31
108
|
`);
|
|
32
109
|
|
|
110
|
+
// Probe the FTS inverted index for corruption. A MATCH query exercises
|
|
111
|
+
// the index structures (not just the content store), so it catches
|
|
112
|
+
// corruption in shadow tables like _idx and _data. On empty tables
|
|
113
|
+
// this returns null gracefully. O(1) with LIMIT 1.
|
|
114
|
+
const raw = getSqliteFrom(database);
|
|
115
|
+
try {
|
|
116
|
+
raw
|
|
117
|
+
.query(`SELECT * FROM messages_fts WHERE messages_fts MATCH 'a' LIMIT 1`)
|
|
118
|
+
.get();
|
|
119
|
+
} catch (err: unknown) {
|
|
120
|
+
if (!isSqliteCorruptionError(err)) {
|
|
121
|
+
throw err;
|
|
122
|
+
}
|
|
123
|
+
logger.warn(
|
|
124
|
+
{ err },
|
|
125
|
+
"[messages-fts] Detected corrupt messages_fts virtual table — dropping and recreating",
|
|
126
|
+
);
|
|
127
|
+
// DROP TABLE on a corrupt vtable can itself throw, so drop the
|
|
128
|
+
// FTS5 shadow tables directly to guarantee cleanup.
|
|
129
|
+
dropFtsShadowTables(raw);
|
|
130
|
+
database.run(/*sql*/ `
|
|
131
|
+
CREATE VIRTUAL TABLE messages_fts USING fts5(
|
|
132
|
+
message_id UNINDEXED,
|
|
133
|
+
content
|
|
134
|
+
)
|
|
135
|
+
`);
|
|
136
|
+
}
|
|
137
|
+
|
|
33
138
|
database.run(/*sql*/ `
|
|
34
139
|
CREATE TRIGGER IF NOT EXISTS messages_fts_ai
|
|
35
140
|
AFTER INSERT ON messages
|
|
@@ -1,6 +1,58 @@
|
|
|
1
1
|
import { type DrizzleDb, getSqliteFrom } from "../db-connection.js";
|
|
2
2
|
import { withCrashRecovery } from "./validate-migration-state.js";
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* Reverse v15: set guardian_principal_id back to NULL on all rows in
|
|
6
|
+
* channel_guardian_bindings and canonical_guardian_requests.
|
|
7
|
+
*
|
|
8
|
+
* Also un-expires requests that the forward migration expired (sets them
|
|
9
|
+
* back to 'pending'). This is a best-effort reversal — the original status
|
|
10
|
+
* of expired requests cannot be perfectly reconstructed if they were already
|
|
11
|
+
* expired before the forward migration ran, but the forward migration only
|
|
12
|
+
* expired requests that had NULL guardian_principal_id and status = 'pending'.
|
|
13
|
+
*/
|
|
14
|
+
export function downBackfillGuardianPrincipalId(database: DrizzleDb): void {
|
|
15
|
+
const raw = getSqliteFrom(database);
|
|
16
|
+
|
|
17
|
+
// Null out guardian_principal_id on channel_guardian_bindings
|
|
18
|
+
const bindingsExists = raw
|
|
19
|
+
.query(
|
|
20
|
+
`SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'channel_guardian_bindings'`,
|
|
21
|
+
)
|
|
22
|
+
.get();
|
|
23
|
+
if (bindingsExists) {
|
|
24
|
+
const colExists = raw
|
|
25
|
+
.query(
|
|
26
|
+
`SELECT 1 FROM pragma_table_info('channel_guardian_bindings') WHERE name = 'guardian_principal_id'`,
|
|
27
|
+
)
|
|
28
|
+
.get();
|
|
29
|
+
if (colExists) {
|
|
30
|
+
raw.exec(
|
|
31
|
+
/*sql*/ `UPDATE channel_guardian_bindings SET guardian_principal_id = NULL`,
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Null out guardian_principal_id on canonical_guardian_requests
|
|
37
|
+
const requestsExists = raw
|
|
38
|
+
.query(
|
|
39
|
+
`SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'canonical_guardian_requests'`,
|
|
40
|
+
)
|
|
41
|
+
.get();
|
|
42
|
+
if (requestsExists) {
|
|
43
|
+
const colExists = raw
|
|
44
|
+
.query(
|
|
45
|
+
`SELECT 1 FROM pragma_table_info('canonical_guardian_requests') WHERE name = 'guardian_principal_id'`,
|
|
46
|
+
)
|
|
47
|
+
.get();
|
|
48
|
+
if (colExists) {
|
|
49
|
+
raw.exec(
|
|
50
|
+
/*sql*/ `UPDATE canonical_guardian_requests SET guardian_principal_id = NULL`,
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
4
56
|
/**
|
|
5
57
|
* Backfill guardianPrincipalId for existing channel_guardian_bindings and
|
|
6
58
|
* canonical_guardian_requests rows.
|
|
@@ -1,6 +1,83 @@
|
|
|
1
1
|
import { type DrizzleDb, getSqliteFrom } from "../db-connection.js";
|
|
2
2
|
import { withCrashRecovery } from "./validate-migration-state.js";
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* Reverse v16: rebuild channel_guardian_bindings to make guardian_principal_id
|
|
6
|
+
* nullable again (removing the NOT NULL constraint added by the forward migration).
|
|
7
|
+
*/
|
|
8
|
+
export function downGuardianPrincipalIdNotNull(database: DrizzleDb): void {
|
|
9
|
+
const raw = getSqliteFrom(database);
|
|
10
|
+
|
|
11
|
+
const tableExists = raw
|
|
12
|
+
.query(
|
|
13
|
+
`SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'channel_guardian_bindings'`,
|
|
14
|
+
)
|
|
15
|
+
.get();
|
|
16
|
+
if (!tableExists) return;
|
|
17
|
+
|
|
18
|
+
// Check if guardian_principal_id has NOT NULL — if not, already rolled back
|
|
19
|
+
const colInfo = raw
|
|
20
|
+
.query(
|
|
21
|
+
`SELECT "notnull" FROM pragma_table_info('channel_guardian_bindings') WHERE name = 'guardian_principal_id'`,
|
|
22
|
+
)
|
|
23
|
+
.get() as { notnull: number } | null;
|
|
24
|
+
if (!colInfo || colInfo.notnull === 0) return;
|
|
25
|
+
|
|
26
|
+
raw.exec("PRAGMA foreign_keys = OFF");
|
|
27
|
+
try {
|
|
28
|
+
raw.exec("BEGIN");
|
|
29
|
+
|
|
30
|
+
raw.exec(/*sql*/ `
|
|
31
|
+
CREATE TABLE channel_guardian_bindings_new (
|
|
32
|
+
id TEXT PRIMARY KEY,
|
|
33
|
+
assistant_id TEXT NOT NULL,
|
|
34
|
+
channel TEXT NOT NULL,
|
|
35
|
+
guardian_external_user_id TEXT NOT NULL,
|
|
36
|
+
guardian_delivery_chat_id TEXT NOT NULL,
|
|
37
|
+
guardian_principal_id TEXT,
|
|
38
|
+
status TEXT NOT NULL DEFAULT 'active',
|
|
39
|
+
verified_at INTEGER NOT NULL,
|
|
40
|
+
verified_via TEXT NOT NULL DEFAULT 'challenge',
|
|
41
|
+
metadata_json TEXT,
|
|
42
|
+
created_at INTEGER NOT NULL,
|
|
43
|
+
updated_at INTEGER NOT NULL
|
|
44
|
+
)
|
|
45
|
+
`);
|
|
46
|
+
|
|
47
|
+
raw.exec(/*sql*/ `
|
|
48
|
+
INSERT INTO channel_guardian_bindings_new
|
|
49
|
+
SELECT id, assistant_id, channel, guardian_external_user_id,
|
|
50
|
+
guardian_delivery_chat_id, guardian_principal_id,
|
|
51
|
+
status, verified_at, verified_via, metadata_json,
|
|
52
|
+
created_at, updated_at
|
|
53
|
+
FROM channel_guardian_bindings
|
|
54
|
+
`);
|
|
55
|
+
|
|
56
|
+
raw.exec(/*sql*/ `DROP TABLE channel_guardian_bindings`);
|
|
57
|
+
raw.exec(
|
|
58
|
+
/*sql*/ `ALTER TABLE channel_guardian_bindings_new RENAME TO channel_guardian_bindings`,
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
// Recreate the unique index for active bindings
|
|
62
|
+
raw.exec(/*sql*/ `
|
|
63
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_channel_guardian_bindings_active
|
|
64
|
+
ON channel_guardian_bindings(assistant_id, channel)
|
|
65
|
+
WHERE status = 'active'
|
|
66
|
+
`);
|
|
67
|
+
|
|
68
|
+
raw.exec("COMMIT");
|
|
69
|
+
} catch (e) {
|
|
70
|
+
try {
|
|
71
|
+
raw.exec("ROLLBACK");
|
|
72
|
+
} catch {
|
|
73
|
+
/* no active transaction */
|
|
74
|
+
}
|
|
75
|
+
throw e;
|
|
76
|
+
} finally {
|
|
77
|
+
raw.exec("PRAGMA foreign_keys = ON");
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
4
81
|
/**
|
|
5
82
|
* Enforce NOT NULL on channel_guardian_bindings.guardian_principal_id.
|
|
6
83
|
*
|
|
@@ -2,6 +2,19 @@ import { getLogger } from "../../util/logger.js";
|
|
|
2
2
|
import { type DrizzleDb, getSqliteFrom } from "../db-connection.js";
|
|
3
3
|
import { withCrashRecovery } from "./validate-migration-state.js";
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Reverse v17: no-op — the original separate columns (relationship, importance,
|
|
7
|
+
* response_expectation, preferred_tone) cannot be reliably restored from the
|
|
8
|
+
* consolidated notes TEXT column.
|
|
9
|
+
*
|
|
10
|
+
* The forward migration concatenated multiple typed fields into a single
|
|
11
|
+
* free-text notes field and then dropped the original columns. Parsing the
|
|
12
|
+
* notes back into structured fields would be lossy and error-prone.
|
|
13
|
+
*/
|
|
14
|
+
export function downContactsNotesColumn(_database: DrizzleDb): void {
|
|
15
|
+
// Lossy — original structured columns cannot be restored from notes text.
|
|
16
|
+
}
|
|
17
|
+
|
|
5
18
|
const log = getLogger("migration-134");
|
|
6
19
|
|
|
7
20
|
export function migrateContactsNotesColumn(database: DrizzleDb): void {
|
|
@@ -1,6 +1,26 @@
|
|
|
1
1
|
import { type DrizzleDb, getSqliteFrom } from "../db-connection.js";
|
|
2
2
|
import { withCrashRecovery } from "./validate-migration-state.js";
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* Reverse v18: set contacts.last_interaction back to NULL.
|
|
6
|
+
*
|
|
7
|
+
* The forward migration backfilled last_interaction from channel data.
|
|
8
|
+
* Rolling back simply clears the column — the data can be re-derived by
|
|
9
|
+
* re-running the forward migration.
|
|
10
|
+
*/
|
|
11
|
+
export function downBackfillContactInteractionStats(database: DrizzleDb): void {
|
|
12
|
+
const raw = getSqliteFrom(database);
|
|
13
|
+
|
|
14
|
+
const colExists = raw
|
|
15
|
+
.query(
|
|
16
|
+
`SELECT 1 FROM pragma_table_info('contacts') WHERE name = 'last_interaction'`,
|
|
17
|
+
)
|
|
18
|
+
.get();
|
|
19
|
+
if (!colExists) return;
|
|
20
|
+
|
|
21
|
+
raw.exec(/*sql*/ `UPDATE contacts SET last_interaction = NULL`);
|
|
22
|
+
}
|
|
23
|
+
|
|
4
24
|
/**
|
|
5
25
|
* Backfill contacts.last_interaction from the max lastSeenAt across each
|
|
6
26
|
* contact's channels. interactionCount cannot be reliably derived from
|