@vellumai/assistant 0.4.33 → 0.4.35
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/package.json +1 -1
- package/src/__tests__/access-request-decision.test.ts +2 -3
- package/src/__tests__/actor-token-service.test.ts +4 -11
- package/src/__tests__/approval-primitive.test.ts +0 -45
- package/src/__tests__/assistant-id-boundary-guard.test.ts +169 -0
- package/src/__tests__/callback-handoff-copy.test.ts +0 -1
- package/src/__tests__/channel-approval-routes.test.ts +5 -45
- package/src/__tests__/channel-guardian.test.ts +122 -345
- package/src/__tests__/confirmation-request-guardian-bridge.test.ts +4 -3
- package/src/__tests__/contacts-tools.test.ts +4 -5
- package/src/__tests__/conversation-attention-store.test.ts +2 -65
- package/src/__tests__/conversation-attention-telegram.test.ts +0 -2
- package/src/__tests__/conversation-pairing.test.ts +0 -1
- package/src/__tests__/deterministic-verification-control-plane.test.ts +0 -2
- package/src/__tests__/guardian-action-conversation-turn.test.ts +1 -7
- package/src/__tests__/guardian-action-grant-mint-consume.test.ts +0 -74
- package/src/__tests__/guardian-action-late-reply.test.ts +1 -8
- package/src/__tests__/guardian-grant-minting.test.ts +0 -1
- package/src/__tests__/guardian-routing-state.test.ts +0 -3
- package/src/__tests__/inbound-invite-redemption.test.ts +0 -3
- package/src/__tests__/non-member-access-request.test.ts +0 -7
- package/src/__tests__/notification-broadcaster.test.ts +1 -2
- package/src/__tests__/notification-decision-fallback.test.ts +0 -2
- package/src/__tests__/notification-decision-strategy.test.ts +0 -1
- package/src/__tests__/relay-server.test.ts +11 -83
- package/src/__tests__/scoped-approval-grants.test.ts +9 -40
- package/src/__tests__/scoped-grant-security-matrix.test.ts +0 -36
- package/src/__tests__/send-endpoint-busy.test.ts +0 -1
- package/src/__tests__/send-notification-tool.test.ts +0 -1
- package/src/__tests__/slack-inbound-verification.test.ts +2 -4
- package/src/__tests__/thread-seed-composer.test.ts +0 -1
- package/src/__tests__/tool-approval-handler.test.ts +0 -1
- package/src/__tests__/tool-grant-request-escalation.test.ts +0 -4
- package/src/__tests__/trusted-contact-approval-notifier.test.ts +1 -5
- package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +1 -17
- package/src/__tests__/trusted-contact-multichannel.test.ts +0 -13
- package/src/__tests__/trusted-contact-verification.test.ts +3 -15
- package/src/__tests__/twilio-routes.test.ts +2 -2
- package/src/__tests__/voice-invite-redemption.test.ts +0 -1
- package/src/__tests__/voice-scoped-grant-consumer.test.ts +0 -37
- package/src/approvals/approval-primitive.ts +0 -15
- package/src/approvals/guardian-decision-primitive.ts +0 -3
- package/src/approvals/guardian-request-resolvers.ts +0 -5
- package/src/calls/call-domain.ts +0 -3
- package/src/calls/call-store.ts +0 -3
- package/src/calls/guardian-action-sweep.ts +2 -1
- package/src/calls/guardian-dispatch.ts +1 -2
- package/src/calls/relay-access-wait.ts +0 -4
- package/src/calls/relay-server.ts +3 -11
- package/src/calls/relay-setup-router.ts +1 -2
- package/src/calls/relay-verification.ts +0 -1
- package/src/calls/twilio-routes.ts +0 -3
- package/src/calls/types.ts +0 -1
- package/src/calls/voice-session-bridge.ts +0 -1
- package/src/config/bundled-skills/google-oauth-setup/SKILL.md +100 -171
- package/src/config/bundled-skills/notifications/tools/send-notification.ts +0 -1
- package/src/contacts/contact-store.ts +13 -88
- package/src/contacts/contacts-write.ts +3 -11
- package/src/contacts/types.ts +0 -1
- package/src/daemon/handlers/config-channels.ts +16 -42
- package/src/daemon/handlers/config-inbox.ts +6 -6
- package/src/daemon/handlers/contacts.ts +3 -11
- package/src/daemon/handlers/index.ts +0 -2
- package/src/daemon/session-process.ts +0 -4
- package/src/memory/conversation-attention-store.ts +4 -19
- package/src/memory/conversation-crud.ts +0 -2
- package/src/memory/db-init.ts +4 -0
- package/src/memory/guardian-action-store.ts +0 -12
- package/src/memory/guardian-approvals.ts +35 -80
- package/src/memory/guardian-rate-limits.ts +1 -14
- package/src/memory/guardian-verification.ts +6 -34
- package/src/memory/invite-store.ts +5 -14
- package/src/memory/migrations/026-guardian-verification-sessions.ts +28 -9
- package/src/memory/migrations/027a-guardian-bootstrap-token.ts +16 -3
- package/src/memory/migrations/038-actor-token-records.ts +8 -1
- package/src/memory/migrations/039-actor-refresh-token-records.ts +11 -2
- package/src/memory/migrations/110-channel-guardian.ts +27 -6
- package/src/memory/migrations/112-assistant-inbox.ts +39 -15
- package/src/memory/migrations/114-notifications.ts +37 -15
- package/src/memory/migrations/117-conversation-attention.ts +33 -9
- package/src/memory/migrations/134-contacts-notes-column.ts +64 -45
- package/src/memory/migrations/136-drop-assistant-id-columns.ts +263 -0
- package/src/memory/migrations/index.ts +1 -0
- package/src/memory/migrations/registry.ts +14 -1
- package/src/memory/migrations/schema-introspection.ts +18 -0
- package/src/memory/schema/calls.ts +0 -7
- package/src/memory/schema/contacts.ts +0 -8
- package/src/memory/schema/guardian.ts +0 -5
- package/src/memory/schema/infrastructure.ts +0 -2
- package/src/memory/schema/notifications.ts +3 -17
- package/src/memory/scoped-approval-grants.ts +2 -24
- package/src/notifications/adapters/sms.ts +2 -1
- package/src/notifications/broadcaster.ts +1 -6
- package/src/notifications/decision-engine.ts +3 -4
- package/src/notifications/deliveries-store.ts +0 -4
- package/src/notifications/destination-resolver.ts +4 -6
- package/src/notifications/deterministic-checks.ts +1 -6
- package/src/notifications/emit-signal.ts +4 -11
- package/src/notifications/events-store.ts +7 -17
- package/src/notifications/preference-summary.ts +2 -2
- package/src/notifications/preferences-store.ts +2 -9
- package/src/notifications/signal.ts +0 -1
- package/src/notifications/thread-candidates.ts +1 -11
- package/src/notifications/types.ts +0 -3
- package/src/runtime/access-request-helper.ts +3 -10
- package/src/runtime/actor-refresh-token-store.ts +0 -6
- package/src/runtime/actor-token-store.ts +3 -16
- package/src/runtime/actor-trust-resolver.ts +1 -4
- package/src/runtime/auth/__tests__/credential-service.test.ts +0 -9
- package/src/runtime/auth/credential-service.ts +1 -15
- package/src/runtime/auth/require-bound-guardian.ts +1 -4
- package/src/runtime/channel-guardian-service.ts +15 -46
- package/src/runtime/channel-invite-transport.ts +8 -0
- package/src/runtime/channel-invite-transports/email.ts +4 -0
- package/src/runtime/channel-invite-transports/slack.ts +6 -0
- package/src/runtime/channel-invite-transports/sms.ts +4 -0
- package/src/runtime/channel-invite-transports/telegram.ts +6 -0
- package/src/runtime/confirmation-request-guardian-bridge.ts +0 -1
- package/src/runtime/guardian-action-followup-executor.ts +3 -2
- package/src/runtime/guardian-action-grant-minter.ts +0 -1
- package/src/runtime/guardian-outbound-actions.ts +2 -12
- package/src/runtime/guardian-vellum-migration.ts +2 -3
- package/src/runtime/http-server.ts +3 -10
- package/src/runtime/http-types.ts +13 -1
- package/src/runtime/invite-redemption-service.ts +1 -14
- package/src/runtime/local-actor-identity.ts +2 -5
- package/src/runtime/routes/access-request-decision.ts +0 -1
- package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +0 -9
- package/src/runtime/routes/channel-readiness-routes.ts +29 -18
- package/src/runtime/routes/contact-routes.ts +15 -40
- package/src/runtime/routes/conversation-attention-routes.ts +0 -2
- package/src/runtime/routes/global-search-routes.ts +0 -2
- package/src/runtime/routes/guardian-bootstrap-routes.ts +6 -7
- package/src/runtime/routes/guardian-expiry-sweep.ts +3 -2
- package/src/runtime/routes/inbound-message-handler.ts +0 -3
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +7 -43
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +1 -4
- package/src/runtime/routes/inbound-stages/bootstrap-intercept.ts +1 -6
- package/src/runtime/routes/inbound-stages/escalation-intercept.ts +0 -1
- package/src/runtime/routes/inbound-stages/secret-ingress-check.ts +0 -1
- package/src/runtime/routes/inbound-stages/verification-intercept.ts +3 -7
- package/src/runtime/routes/pairing-routes.ts +4 -4
- package/src/runtime/routes/surface-content-routes.ts +104 -0
- package/src/runtime/tool-grant-request-helper.ts +0 -1
- package/src/tools/browser/browser-manager.ts +22 -21
- package/src/tools/browser/runtime-check.ts +111 -6
- package/src/tools/calls/call-start.ts +1 -3
- package/src/tools/followups/followup_create.ts +1 -2
- package/src/tools/tool-approval-handler.ts +0 -2
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { DrizzleDb } from "../db-connection.js";
|
|
2
|
+
import { tableHasColumn } from "./schema-introspection.js";
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Create the actor_refresh_token_records table for hash-only refresh token persistence.
|
|
@@ -34,10 +35,18 @@ export function createActorRefreshTokenRecordsTable(database: DrizzleDb): void {
|
|
|
34
35
|
// Unique active refresh token per device binding.
|
|
35
36
|
// DROP first so that databases that already created the older non-unique
|
|
36
37
|
// index with the same name get upgraded to UNIQUE.
|
|
37
|
-
database
|
|
38
|
-
|
|
38
|
+
if (tableHasColumn(database, "actor_refresh_token_records", "assistant_id")) {
|
|
39
|
+
database.run(
|
|
40
|
+
/*sql*/ `DROP INDEX IF EXISTS idx_refresh_tokens_active_device`,
|
|
41
|
+
);
|
|
42
|
+
database.run(/*sql*/ `CREATE UNIQUE INDEX IF NOT EXISTS idx_refresh_tokens_active_device
|
|
39
43
|
ON actor_refresh_token_records(assistant_id, guardian_principal_id, hashed_device_id)
|
|
40
44
|
WHERE status = 'active'`);
|
|
45
|
+
} else {
|
|
46
|
+
database.run(/*sql*/ `CREATE UNIQUE INDEX IF NOT EXISTS idx_refresh_tokens_active_device
|
|
47
|
+
ON actor_refresh_token_records(guardian_principal_id, hashed_device_id)
|
|
48
|
+
WHERE status = 'active'`);
|
|
49
|
+
}
|
|
41
50
|
|
|
42
51
|
// Family lookup for replay detection (revoke entire family)
|
|
43
52
|
database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_refresh_tokens_family
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { DrizzleDb } from "../db-connection.js";
|
|
2
|
+
import { tableHasColumn } from "./schema-introspection.js";
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Channel guardian tables: bindings, verification challenges, approval requests,
|
|
@@ -43,9 +44,21 @@ export function createChannelGuardianTables(database: DrizzleDb): void {
|
|
|
43
44
|
)
|
|
44
45
|
`);
|
|
45
46
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
47
|
+
if (
|
|
48
|
+
tableHasColumn(
|
|
49
|
+
database,
|
|
50
|
+
"channel_guardian_verification_challenges",
|
|
51
|
+
"assistant_id",
|
|
52
|
+
)
|
|
53
|
+
) {
|
|
54
|
+
database.run(
|
|
55
|
+
/*sql*/ `CREATE INDEX IF NOT EXISTS idx_channel_guardian_challenges_lookup ON channel_guardian_verification_challenges(assistant_id, channel, challenge_hash, status)`,
|
|
56
|
+
);
|
|
57
|
+
} else {
|
|
58
|
+
database.run(
|
|
59
|
+
/*sql*/ `CREATE INDEX IF NOT EXISTS idx_channel_guardian_challenges_lookup ON channel_guardian_verification_challenges(channel, challenge_hash, status)`,
|
|
60
|
+
);
|
|
61
|
+
}
|
|
49
62
|
|
|
50
63
|
database.run(/*sql*/ `
|
|
51
64
|
CREATE TABLE IF NOT EXISTS channel_guardian_approval_requests (
|
|
@@ -139,7 +152,15 @@ export function createChannelGuardianTables(database: DrizzleDb): void {
|
|
|
139
152
|
/* already exists */
|
|
140
153
|
}
|
|
141
154
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
)
|
|
155
|
+
if (
|
|
156
|
+
tableHasColumn(database, "channel_guardian_rate_limits", "assistant_id")
|
|
157
|
+
) {
|
|
158
|
+
database.run(
|
|
159
|
+
/*sql*/ `CREATE UNIQUE INDEX IF NOT EXISTS idx_channel_guardian_rate_limits_actor ON channel_guardian_rate_limits(assistant_id, channel, actor_external_user_id, actor_chat_id)`,
|
|
160
|
+
);
|
|
161
|
+
} else {
|
|
162
|
+
database.run(
|
|
163
|
+
/*sql*/ `CREATE UNIQUE INDEX IF NOT EXISTS idx_channel_guardian_rate_limits_actor ON channel_guardian_rate_limits(channel, actor_external_user_id, actor_chat_id)`,
|
|
164
|
+
);
|
|
165
|
+
}
|
|
145
166
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { DrizzleDb } from "../db-connection.js";
|
|
2
2
|
import { migrateBackfillInboxThreadStateFromBindings } from "./014-backfill-inbox-thread-state.js";
|
|
3
|
+
import { tableHasColumn } from "./schema-introspection.js";
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Assistant inbox tables: ingress invites, ingress members, inbox thread state.
|
|
@@ -28,12 +29,21 @@ export function createAssistantInboxTables(database: DrizzleDb): void {
|
|
|
28
29
|
database.run(
|
|
29
30
|
/*sql*/ `CREATE UNIQUE INDEX IF NOT EXISTS idx_ingress_invites_token_hash ON assistant_ingress_invites(token_hash)`,
|
|
30
31
|
);
|
|
31
|
-
database
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
32
|
+
if (tableHasColumn(database, "assistant_ingress_invites", "assistant_id")) {
|
|
33
|
+
database.run(
|
|
34
|
+
/*sql*/ `CREATE INDEX IF NOT EXISTS idx_ingress_invites_channel_status ON assistant_ingress_invites(assistant_id, source_channel, status, expires_at)`,
|
|
35
|
+
);
|
|
36
|
+
database.run(
|
|
37
|
+
/*sql*/ `CREATE INDEX IF NOT EXISTS idx_ingress_invites_channel_created ON assistant_ingress_invites(assistant_id, source_channel, created_at)`,
|
|
38
|
+
);
|
|
39
|
+
} else {
|
|
40
|
+
database.run(
|
|
41
|
+
/*sql*/ `CREATE INDEX IF NOT EXISTS idx_ingress_invites_channel_status ON assistant_ingress_invites(source_channel, status, expires_at)`,
|
|
42
|
+
);
|
|
43
|
+
database.run(
|
|
44
|
+
/*sql*/ `CREATE INDEX IF NOT EXISTS idx_ingress_invites_channel_created ON assistant_ingress_invites(source_channel, created_at)`,
|
|
45
|
+
);
|
|
46
|
+
}
|
|
37
47
|
|
|
38
48
|
database.run(/*sql*/ `
|
|
39
49
|
CREATE TABLE IF NOT EXISTS assistant_ingress_members (
|
|
@@ -90,15 +100,29 @@ export function createAssistantInboxTables(database: DrizzleDb): void {
|
|
|
90
100
|
)
|
|
91
101
|
`);
|
|
92
102
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
)
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
103
|
+
if (
|
|
104
|
+
tableHasColumn(database, "assistant_inbox_thread_state", "assistant_id")
|
|
105
|
+
) {
|
|
106
|
+
database.run(
|
|
107
|
+
/*sql*/ `CREATE UNIQUE INDEX IF NOT EXISTS idx_inbox_thread_state_channel ON assistant_inbox_thread_state(assistant_id, source_channel, external_chat_id)`,
|
|
108
|
+
);
|
|
109
|
+
database.run(
|
|
110
|
+
/*sql*/ `CREATE INDEX IF NOT EXISTS idx_inbox_thread_state_last_msg ON assistant_inbox_thread_state(assistant_id, last_message_at)`,
|
|
111
|
+
);
|
|
112
|
+
database.run(
|
|
113
|
+
/*sql*/ `CREATE INDEX IF NOT EXISTS idx_inbox_thread_state_escalation ON assistant_inbox_thread_state(assistant_id, has_pending_escalation, last_message_at)`,
|
|
114
|
+
);
|
|
115
|
+
} else {
|
|
116
|
+
database.run(
|
|
117
|
+
/*sql*/ `CREATE UNIQUE INDEX IF NOT EXISTS idx_inbox_thread_state_channel ON assistant_inbox_thread_state(source_channel, external_chat_id)`,
|
|
118
|
+
);
|
|
119
|
+
database.run(
|
|
120
|
+
/*sql*/ `CREATE INDEX IF NOT EXISTS idx_inbox_thread_state_last_msg ON assistant_inbox_thread_state(last_message_at)`,
|
|
121
|
+
);
|
|
122
|
+
database.run(
|
|
123
|
+
/*sql*/ `CREATE INDEX IF NOT EXISTS idx_inbox_thread_state_escalation ON assistant_inbox_thread_state(has_pending_escalation, last_message_at)`,
|
|
124
|
+
);
|
|
125
|
+
}
|
|
102
126
|
|
|
103
127
|
migrateBackfillInboxThreadStateFromBindings(database);
|
|
104
128
|
}
|
|
@@ -2,6 +2,7 @@ import type { DrizzleDb } from "../db-connection.js";
|
|
|
2
2
|
import { migrateNotificationTablesSchema } from "./019-notification-tables-schema-migration.js";
|
|
3
3
|
import { migrateNotificationDeliveryPairingColumns } from "./027-notification-delivery-pairing-columns.js";
|
|
4
4
|
import { migrateNotificationDeliveryClientAck } from "./028-notification-delivery-client-ack.js";
|
|
5
|
+
import { tableHasColumn } from "./schema-introspection.js";
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Notification system tables: preferences, events, decisions, and deliveries.
|
|
@@ -24,12 +25,18 @@ export function createNotificationTables(database: DrizzleDb): void {
|
|
|
24
25
|
)
|
|
25
26
|
`);
|
|
26
27
|
|
|
27
|
-
database
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
28
|
+
if (tableHasColumn(database, "notification_preferences", "assistant_id")) {
|
|
29
|
+
database.run(
|
|
30
|
+
/*sql*/ `CREATE INDEX IF NOT EXISTS idx_notification_preferences_assistant_id ON notification_preferences(assistant_id)`,
|
|
31
|
+
);
|
|
32
|
+
database.run(
|
|
33
|
+
/*sql*/ `CREATE INDEX IF NOT EXISTS idx_notification_preferences_assistant_priority ON notification_preferences(assistant_id, priority DESC)`,
|
|
34
|
+
);
|
|
35
|
+
} else {
|
|
36
|
+
database.run(
|
|
37
|
+
/*sql*/ `CREATE INDEX IF NOT EXISTS idx_notification_preferences_priority ON notification_preferences(priority DESC)`,
|
|
38
|
+
);
|
|
39
|
+
}
|
|
33
40
|
|
|
34
41
|
database.run(/*sql*/ `
|
|
35
42
|
CREATE TABLE IF NOT EXISTS notification_events (
|
|
@@ -46,12 +53,21 @@ export function createNotificationTables(database: DrizzleDb): void {
|
|
|
46
53
|
)
|
|
47
54
|
`);
|
|
48
55
|
|
|
49
|
-
database
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
56
|
+
if (tableHasColumn(database, "notification_events", "assistant_id")) {
|
|
57
|
+
database.run(
|
|
58
|
+
/*sql*/ `CREATE INDEX IF NOT EXISTS idx_notification_events_assistant_event_created ON notification_events(assistant_id, source_event_name, created_at)`,
|
|
59
|
+
);
|
|
60
|
+
database.run(
|
|
61
|
+
/*sql*/ `CREATE UNIQUE INDEX IF NOT EXISTS idx_notification_events_dedupe ON notification_events(assistant_id, dedupe_key) WHERE dedupe_key IS NOT NULL`,
|
|
62
|
+
);
|
|
63
|
+
} else {
|
|
64
|
+
database.run(
|
|
65
|
+
/*sql*/ `CREATE INDEX IF NOT EXISTS idx_notification_events_event_created ON notification_events(source_event_name, created_at)`,
|
|
66
|
+
);
|
|
67
|
+
database.run(
|
|
68
|
+
/*sql*/ `CREATE UNIQUE INDEX IF NOT EXISTS idx_notification_events_dedupe ON notification_events(dedupe_key) WHERE dedupe_key IS NOT NULL`,
|
|
69
|
+
);
|
|
70
|
+
}
|
|
55
71
|
|
|
56
72
|
database.run(/*sql*/ `
|
|
57
73
|
CREATE TABLE IF NOT EXISTS notification_decisions (
|
|
@@ -97,9 +113,15 @@ export function createNotificationTables(database: DrizzleDb): void {
|
|
|
97
113
|
database.run(
|
|
98
114
|
/*sql*/ `CREATE INDEX IF NOT EXISTS idx_notification_deliveries_decision_id ON notification_deliveries(notification_decision_id)`,
|
|
99
115
|
);
|
|
100
|
-
database
|
|
101
|
-
|
|
102
|
-
|
|
116
|
+
if (tableHasColumn(database, "notification_deliveries", "assistant_id")) {
|
|
117
|
+
database.run(
|
|
118
|
+
/*sql*/ `CREATE INDEX IF NOT EXISTS idx_notification_deliveries_assistant_status ON notification_deliveries(assistant_id, status)`,
|
|
119
|
+
);
|
|
120
|
+
} else {
|
|
121
|
+
database.run(
|
|
122
|
+
/*sql*/ `CREATE INDEX IF NOT EXISTS idx_notification_deliveries_status ON notification_deliveries(status)`,
|
|
123
|
+
);
|
|
124
|
+
}
|
|
103
125
|
|
|
104
126
|
// Add conversation pairing audit columns (idempotent ALTER TABLE)
|
|
105
127
|
migrateNotificationDeliveryPairingColumns(database);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { DrizzleDb } from "../db-connection.js";
|
|
2
|
+
import { tableHasColumn } from "./schema-introspection.js";
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Conversation attention tables: append-only evidence log and single-row
|
|
@@ -24,9 +25,17 @@ export function createConversationAttentionTables(database: DrizzleDb): void {
|
|
|
24
25
|
database.run(
|
|
25
26
|
/*sql*/ `CREATE INDEX IF NOT EXISTS idx_conv_attn_events_conv_observed ON conversation_attention_events(conversation_id, observed_at DESC)`,
|
|
26
27
|
);
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
)
|
|
28
|
+
if (
|
|
29
|
+
tableHasColumn(database, "conversation_attention_events", "assistant_id")
|
|
30
|
+
) {
|
|
31
|
+
database.run(
|
|
32
|
+
/*sql*/ `CREATE INDEX IF NOT EXISTS idx_conv_attn_events_assistant_observed ON conversation_attention_events(assistant_id, observed_at DESC)`,
|
|
33
|
+
);
|
|
34
|
+
} else {
|
|
35
|
+
database.run(
|
|
36
|
+
/*sql*/ `CREATE INDEX IF NOT EXISTS idx_conv_attn_events_observed ON conversation_attention_events(observed_at)`,
|
|
37
|
+
);
|
|
38
|
+
}
|
|
30
39
|
database.run(
|
|
31
40
|
/*sql*/ `CREATE INDEX IF NOT EXISTS idx_conv_attn_events_channel_observed ON conversation_attention_events(source_channel, observed_at DESC)`,
|
|
32
41
|
);
|
|
@@ -50,10 +59,25 @@ export function createConversationAttentionTables(database: DrizzleDb): void {
|
|
|
50
59
|
)
|
|
51
60
|
`);
|
|
52
61
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
62
|
+
if (
|
|
63
|
+
tableHasColumn(
|
|
64
|
+
database,
|
|
65
|
+
"conversation_assistant_attention_state",
|
|
66
|
+
"assistant_id",
|
|
67
|
+
)
|
|
68
|
+
) {
|
|
69
|
+
database.run(
|
|
70
|
+
/*sql*/ `CREATE INDEX IF NOT EXISTS idx_conv_attn_state_assistant_latest_msg ON conversation_assistant_attention_state(assistant_id, latest_assistant_message_at DESC)`,
|
|
71
|
+
);
|
|
72
|
+
database.run(
|
|
73
|
+
/*sql*/ `CREATE INDEX IF NOT EXISTS idx_conv_attn_state_assistant_last_seen ON conversation_assistant_attention_state(assistant_id, last_seen_assistant_message_at DESC)`,
|
|
74
|
+
);
|
|
75
|
+
} else {
|
|
76
|
+
database.run(
|
|
77
|
+
/*sql*/ `CREATE INDEX IF NOT EXISTS idx_conv_attn_state_latest_msg ON conversation_assistant_attention_state(latest_assistant_message_at)`,
|
|
78
|
+
);
|
|
79
|
+
database.run(
|
|
80
|
+
/*sql*/ `CREATE INDEX IF NOT EXISTS idx_conv_attn_state_last_seen ON conversation_assistant_attention_state(last_seen_assistant_message_at)`,
|
|
81
|
+
);
|
|
82
|
+
}
|
|
59
83
|
}
|
|
@@ -1,68 +1,87 @@
|
|
|
1
1
|
import { getLogger } from "../../util/logger.js";
|
|
2
2
|
import { type DrizzleDb, getSqliteFrom } from "../db-connection.js";
|
|
3
|
+
import { withCrashRecovery } from "./validate-migration-state.js";
|
|
3
4
|
|
|
4
5
|
const log = getLogger("migration-134");
|
|
5
6
|
|
|
6
7
|
export function migrateContactsNotesColumn(database: DrizzleDb): void {
|
|
7
|
-
|
|
8
|
+
withCrashRecovery(database, "migration_contacts_notes_column_v1", () => {
|
|
9
|
+
const raw = getSqliteFrom(database);
|
|
8
10
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
try {
|
|
12
|
+
raw.exec(/*sql*/ `ALTER TABLE contacts ADD COLUMN notes TEXT`);
|
|
13
|
+
} catch {
|
|
14
|
+
/* already exists */
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Check which legacy columns still exist — handles partial completion
|
|
18
|
+
// if a previous run crashed after dropping some columns but not all.
|
|
19
|
+
const cols = new Set(
|
|
20
|
+
(
|
|
21
|
+
raw.query(`PRAGMA table_info(contacts)`).all() as Array<{
|
|
22
|
+
name: string;
|
|
23
|
+
}>
|
|
24
|
+
).map((c) => c.name),
|
|
25
|
+
);
|
|
14
26
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
27
|
+
const legacyCols = [
|
|
28
|
+
"relationship",
|
|
29
|
+
"importance",
|
|
30
|
+
"response_expectation",
|
|
31
|
+
"preferred_tone",
|
|
32
|
+
] as const;
|
|
33
|
+
const remaining = legacyCols.filter((c) => cols.has(c));
|
|
21
34
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
35
|
+
// Backfill notes from legacy columns if any are still present and notes
|
|
36
|
+
// haven't been populated yet (only run once, before any columns are dropped).
|
|
37
|
+
if (remaining.length === legacyCols.length) {
|
|
38
|
+
const rows = raw
|
|
39
|
+
.query(
|
|
40
|
+
`SELECT id, relationship, importance, response_expectation, preferred_tone
|
|
26
41
|
FROM contacts
|
|
27
42
|
WHERE relationship IS NOT NULL
|
|
28
43
|
OR importance != 0.5
|
|
29
44
|
OR response_expectation IS NOT NULL
|
|
30
45
|
OR preferred_tone IS NOT NULL`,
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
46
|
+
)
|
|
47
|
+
.all() as Array<{
|
|
48
|
+
id: string;
|
|
49
|
+
relationship: string | null;
|
|
50
|
+
importance: number;
|
|
51
|
+
response_expectation: string | null;
|
|
52
|
+
preferred_tone: string | null;
|
|
53
|
+
}>;
|
|
39
54
|
|
|
40
|
-
|
|
55
|
+
const update = raw.prepare(`UPDATE contacts SET notes = ? WHERE id = ?`);
|
|
41
56
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
57
|
+
for (const row of rows) {
|
|
58
|
+
const parts: string[] = [];
|
|
59
|
+
if (row.relationship) parts.push(`Relationship: ${row.relationship}`);
|
|
60
|
+
if (row.importance !== 0.5) parts.push(`Importance: ${row.importance}`);
|
|
61
|
+
if (row.response_expectation)
|
|
62
|
+
parts.push(`Response expectation: ${row.response_expectation}`);
|
|
63
|
+
if (row.preferred_tone)
|
|
64
|
+
parts.push(`Preferred tone: ${row.preferred_tone}`);
|
|
65
|
+
if (parts.length > 0) {
|
|
66
|
+
update.run(parts.join("\n"), row.id);
|
|
67
|
+
}
|
|
52
68
|
}
|
|
53
|
-
}
|
|
54
69
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
70
|
+
const migrated = rows.length;
|
|
71
|
+
if (migrated > 0) {
|
|
72
|
+
log.info({ migrated }, "Migrated contact metadata to notes field");
|
|
73
|
+
}
|
|
58
74
|
}
|
|
59
75
|
|
|
60
|
-
// Drop indexes that reference columns we're about to remove
|
|
76
|
+
// Drop indexes that reference columns we're about to remove.
|
|
77
|
+
// Must happen before the column drops — SQLite rejects dropping a column
|
|
78
|
+
// that is still referenced by an index.
|
|
61
79
|
raw.exec(/*sql*/ `DROP INDEX IF EXISTS idx_contacts_importance`);
|
|
62
80
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
81
|
+
// Drop each legacy column individually so partial completion is safe:
|
|
82
|
+
// on crash recovery the loop picks up only the columns that remain.
|
|
83
|
+
for (const col of remaining) {
|
|
84
|
+
raw.exec(/*sql*/ `ALTER TABLE contacts DROP COLUMN ${col}`);
|
|
85
|
+
}
|
|
86
|
+
});
|
|
68
87
|
}
|