@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.
Files changed (149) hide show
  1. package/package.json +1 -1
  2. package/src/__tests__/access-request-decision.test.ts +2 -3
  3. package/src/__tests__/actor-token-service.test.ts +4 -11
  4. package/src/__tests__/approval-primitive.test.ts +0 -45
  5. package/src/__tests__/assistant-id-boundary-guard.test.ts +169 -0
  6. package/src/__tests__/callback-handoff-copy.test.ts +0 -1
  7. package/src/__tests__/channel-approval-routes.test.ts +5 -45
  8. package/src/__tests__/channel-guardian.test.ts +122 -345
  9. package/src/__tests__/confirmation-request-guardian-bridge.test.ts +4 -3
  10. package/src/__tests__/contacts-tools.test.ts +4 -5
  11. package/src/__tests__/conversation-attention-store.test.ts +2 -65
  12. package/src/__tests__/conversation-attention-telegram.test.ts +0 -2
  13. package/src/__tests__/conversation-pairing.test.ts +0 -1
  14. package/src/__tests__/deterministic-verification-control-plane.test.ts +0 -2
  15. package/src/__tests__/guardian-action-conversation-turn.test.ts +1 -7
  16. package/src/__tests__/guardian-action-grant-mint-consume.test.ts +0 -74
  17. package/src/__tests__/guardian-action-late-reply.test.ts +1 -8
  18. package/src/__tests__/guardian-grant-minting.test.ts +0 -1
  19. package/src/__tests__/guardian-routing-state.test.ts +0 -3
  20. package/src/__tests__/inbound-invite-redemption.test.ts +0 -3
  21. package/src/__tests__/non-member-access-request.test.ts +0 -7
  22. package/src/__tests__/notification-broadcaster.test.ts +1 -2
  23. package/src/__tests__/notification-decision-fallback.test.ts +0 -2
  24. package/src/__tests__/notification-decision-strategy.test.ts +0 -1
  25. package/src/__tests__/relay-server.test.ts +11 -83
  26. package/src/__tests__/scoped-approval-grants.test.ts +9 -40
  27. package/src/__tests__/scoped-grant-security-matrix.test.ts +0 -36
  28. package/src/__tests__/send-endpoint-busy.test.ts +0 -1
  29. package/src/__tests__/send-notification-tool.test.ts +0 -1
  30. package/src/__tests__/slack-inbound-verification.test.ts +2 -4
  31. package/src/__tests__/thread-seed-composer.test.ts +0 -1
  32. package/src/__tests__/tool-approval-handler.test.ts +0 -1
  33. package/src/__tests__/tool-grant-request-escalation.test.ts +0 -4
  34. package/src/__tests__/trusted-contact-approval-notifier.test.ts +1 -5
  35. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +1 -17
  36. package/src/__tests__/trusted-contact-multichannel.test.ts +0 -13
  37. package/src/__tests__/trusted-contact-verification.test.ts +3 -15
  38. package/src/__tests__/twilio-routes.test.ts +2 -2
  39. package/src/__tests__/voice-invite-redemption.test.ts +0 -1
  40. package/src/__tests__/voice-scoped-grant-consumer.test.ts +0 -37
  41. package/src/approvals/approval-primitive.ts +0 -15
  42. package/src/approvals/guardian-decision-primitive.ts +0 -3
  43. package/src/approvals/guardian-request-resolvers.ts +0 -5
  44. package/src/calls/call-domain.ts +0 -3
  45. package/src/calls/call-store.ts +0 -3
  46. package/src/calls/guardian-action-sweep.ts +2 -1
  47. package/src/calls/guardian-dispatch.ts +1 -2
  48. package/src/calls/relay-access-wait.ts +0 -4
  49. package/src/calls/relay-server.ts +3 -11
  50. package/src/calls/relay-setup-router.ts +1 -2
  51. package/src/calls/relay-verification.ts +0 -1
  52. package/src/calls/twilio-routes.ts +0 -3
  53. package/src/calls/types.ts +0 -1
  54. package/src/calls/voice-session-bridge.ts +0 -1
  55. package/src/config/bundled-skills/google-oauth-setup/SKILL.md +100 -171
  56. package/src/config/bundled-skills/notifications/tools/send-notification.ts +0 -1
  57. package/src/contacts/contact-store.ts +13 -88
  58. package/src/contacts/contacts-write.ts +3 -11
  59. package/src/contacts/types.ts +0 -1
  60. package/src/daemon/handlers/config-channels.ts +16 -42
  61. package/src/daemon/handlers/config-inbox.ts +6 -6
  62. package/src/daemon/handlers/contacts.ts +3 -11
  63. package/src/daemon/handlers/index.ts +0 -2
  64. package/src/daemon/session-process.ts +0 -4
  65. package/src/memory/conversation-attention-store.ts +4 -19
  66. package/src/memory/conversation-crud.ts +0 -2
  67. package/src/memory/db-init.ts +4 -0
  68. package/src/memory/guardian-action-store.ts +0 -12
  69. package/src/memory/guardian-approvals.ts +35 -80
  70. package/src/memory/guardian-rate-limits.ts +1 -14
  71. package/src/memory/guardian-verification.ts +6 -34
  72. package/src/memory/invite-store.ts +5 -14
  73. package/src/memory/migrations/026-guardian-verification-sessions.ts +28 -9
  74. package/src/memory/migrations/027a-guardian-bootstrap-token.ts +16 -3
  75. package/src/memory/migrations/038-actor-token-records.ts +8 -1
  76. package/src/memory/migrations/039-actor-refresh-token-records.ts +11 -2
  77. package/src/memory/migrations/110-channel-guardian.ts +27 -6
  78. package/src/memory/migrations/112-assistant-inbox.ts +39 -15
  79. package/src/memory/migrations/114-notifications.ts +37 -15
  80. package/src/memory/migrations/117-conversation-attention.ts +33 -9
  81. package/src/memory/migrations/134-contacts-notes-column.ts +64 -45
  82. package/src/memory/migrations/136-drop-assistant-id-columns.ts +263 -0
  83. package/src/memory/migrations/index.ts +1 -0
  84. package/src/memory/migrations/registry.ts +14 -1
  85. package/src/memory/migrations/schema-introspection.ts +18 -0
  86. package/src/memory/schema/calls.ts +0 -7
  87. package/src/memory/schema/contacts.ts +0 -8
  88. package/src/memory/schema/guardian.ts +0 -5
  89. package/src/memory/schema/infrastructure.ts +0 -2
  90. package/src/memory/schema/notifications.ts +3 -17
  91. package/src/memory/scoped-approval-grants.ts +2 -24
  92. package/src/notifications/adapters/sms.ts +2 -1
  93. package/src/notifications/broadcaster.ts +1 -6
  94. package/src/notifications/decision-engine.ts +3 -4
  95. package/src/notifications/deliveries-store.ts +0 -4
  96. package/src/notifications/destination-resolver.ts +4 -6
  97. package/src/notifications/deterministic-checks.ts +1 -6
  98. package/src/notifications/emit-signal.ts +4 -11
  99. package/src/notifications/events-store.ts +7 -17
  100. package/src/notifications/preference-summary.ts +2 -2
  101. package/src/notifications/preferences-store.ts +2 -9
  102. package/src/notifications/signal.ts +0 -1
  103. package/src/notifications/thread-candidates.ts +1 -11
  104. package/src/notifications/types.ts +0 -3
  105. package/src/runtime/access-request-helper.ts +3 -10
  106. package/src/runtime/actor-refresh-token-store.ts +0 -6
  107. package/src/runtime/actor-token-store.ts +3 -16
  108. package/src/runtime/actor-trust-resolver.ts +1 -4
  109. package/src/runtime/auth/__tests__/credential-service.test.ts +0 -9
  110. package/src/runtime/auth/credential-service.ts +1 -15
  111. package/src/runtime/auth/require-bound-guardian.ts +1 -4
  112. package/src/runtime/channel-guardian-service.ts +15 -46
  113. package/src/runtime/channel-invite-transport.ts +8 -0
  114. package/src/runtime/channel-invite-transports/email.ts +4 -0
  115. package/src/runtime/channel-invite-transports/slack.ts +6 -0
  116. package/src/runtime/channel-invite-transports/sms.ts +4 -0
  117. package/src/runtime/channel-invite-transports/telegram.ts +6 -0
  118. package/src/runtime/confirmation-request-guardian-bridge.ts +0 -1
  119. package/src/runtime/guardian-action-followup-executor.ts +3 -2
  120. package/src/runtime/guardian-action-grant-minter.ts +0 -1
  121. package/src/runtime/guardian-outbound-actions.ts +2 -12
  122. package/src/runtime/guardian-vellum-migration.ts +2 -3
  123. package/src/runtime/http-server.ts +3 -10
  124. package/src/runtime/http-types.ts +13 -1
  125. package/src/runtime/invite-redemption-service.ts +1 -14
  126. package/src/runtime/local-actor-identity.ts +2 -5
  127. package/src/runtime/routes/access-request-decision.ts +0 -1
  128. package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +0 -9
  129. package/src/runtime/routes/channel-readiness-routes.ts +29 -18
  130. package/src/runtime/routes/contact-routes.ts +15 -40
  131. package/src/runtime/routes/conversation-attention-routes.ts +0 -2
  132. package/src/runtime/routes/global-search-routes.ts +0 -2
  133. package/src/runtime/routes/guardian-bootstrap-routes.ts +6 -7
  134. package/src/runtime/routes/guardian-expiry-sweep.ts +3 -2
  135. package/src/runtime/routes/inbound-message-handler.ts +0 -3
  136. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +7 -43
  137. package/src/runtime/routes/inbound-stages/background-dispatch.ts +1 -4
  138. package/src/runtime/routes/inbound-stages/bootstrap-intercept.ts +1 -6
  139. package/src/runtime/routes/inbound-stages/escalation-intercept.ts +0 -1
  140. package/src/runtime/routes/inbound-stages/secret-ingress-check.ts +0 -1
  141. package/src/runtime/routes/inbound-stages/verification-intercept.ts +3 -7
  142. package/src/runtime/routes/pairing-routes.ts +4 -4
  143. package/src/runtime/routes/surface-content-routes.ts +104 -0
  144. package/src/runtime/tool-grant-request-helper.ts +0 -1
  145. package/src/tools/browser/browser-manager.ts +22 -21
  146. package/src/tools/browser/runtime-check.ts +111 -6
  147. package/src/tools/calls/call-start.ts +1 -3
  148. package/src/tools/followups/followup_create.ts +1 -2
  149. package/src/tools/tool-approval-handler.ts +0 -2
@@ -0,0 +1,263 @@
1
+ import { getLogger } from "../../util/logger.js";
2
+ import { type DrizzleDb, getSqliteFrom } from "../db-connection.js";
3
+ import { withCrashRecovery } from "./validate-migration-state.js";
4
+
5
+ const log = getLogger("migration-136");
6
+
7
+ /**
8
+ * Drop `assistant_id` columns from all 16 daemon tables that carried the
9
+ * per-assistant scoping column. After wave-1 PRs normalised every value to
10
+ * 'self' (the implicit single-tenant identity), the column is dead weight.
11
+ *
12
+ * Steps:
13
+ * 1. Safety assertion: verify all rows are 'self' or NULL.
14
+ * 2. Drop composite indexes that include `assistant_id`.
15
+ * 3. `ALTER TABLE ... DROP COLUMN assistant_id` for each table.
16
+ * 4. Recreate indexes without the `assistant_id` column.
17
+ */
18
+ export function migrateDropAssistantIdColumns(database: DrizzleDb): void {
19
+ withCrashRecovery(database, "migration_drop_assistant_id_columns_v1", () => {
20
+ const raw = getSqliteFrom(database);
21
+
22
+ // The 16 tables that carry assistant_id.
23
+ const tables = [
24
+ "contacts",
25
+ "assistant_ingress_invites",
26
+ "assistant_inbox_thread_state",
27
+ "call_sessions",
28
+ "channel_guardian_verification_challenges",
29
+ "channel_guardian_approval_requests",
30
+ "channel_guardian_rate_limits",
31
+ "guardian_action_requests",
32
+ "scoped_approval_grants",
33
+ "notification_events",
34
+ "notification_preferences",
35
+ "notification_deliveries",
36
+ "conversation_attention_events",
37
+ "conversation_assistant_attention_state",
38
+ "actor_token_records",
39
+ "actor_refresh_token_records",
40
+ ];
41
+
42
+ // --- Safety assertion ---
43
+ // Verify all existing assistant_id values are 'self' or NULL before dropping.
44
+ for (const table of tables) {
45
+ const cols = new Set(
46
+ (
47
+ raw.query(`PRAGMA table_info(${table})`).all() as Array<{
48
+ name: string;
49
+ }>
50
+ ).map((c) => c.name),
51
+ );
52
+
53
+ if (!cols.has("assistant_id")) {
54
+ log.info(
55
+ { table },
56
+ "Table does not have assistant_id column — skipping",
57
+ );
58
+ continue;
59
+ }
60
+
61
+ const unexpected = raw
62
+ .query(
63
+ `SELECT DISTINCT assistant_id FROM ${table} WHERE assistant_id IS NOT NULL AND assistant_id != 'self'`,
64
+ )
65
+ .all() as Array<{ assistant_id: string }>;
66
+
67
+ if (unexpected.length > 0) {
68
+ log.warn(
69
+ { table, values: unexpected.map((r) => r.assistant_id) },
70
+ "Unexpected assistant_id values found — skipping table",
71
+ );
72
+ continue;
73
+ }
74
+ }
75
+
76
+ // --- Drop ALL indexes that include assistant_id ---
77
+ // Every index below references the assistant_id column. SQLite will error
78
+ // on ALTER TABLE ... DROP COLUMN if any index still references the column.
79
+
80
+ // channel_guardian_verification_challenges indexes (migrations 110, 026, 027a)
81
+ raw.exec(
82
+ /*sql*/ `DROP INDEX IF EXISTS idx_channel_guardian_challenges_lookup`,
83
+ );
84
+ raw.exec(/*sql*/ `DROP INDEX IF EXISTS idx_guardian_sessions_active`);
85
+ raw.exec(/*sql*/ `DROP INDEX IF EXISTS idx_guardian_sessions_identity`);
86
+ raw.exec(/*sql*/ `DROP INDEX IF EXISTS idx_guardian_sessions_destination`);
87
+ raw.exec(/*sql*/ `DROP INDEX IF EXISTS idx_guardian_sessions_bootstrap`);
88
+
89
+ // channel_guardian_rate_limits indexes (migration 110)
90
+ raw.exec(
91
+ /*sql*/ `DROP INDEX IF EXISTS idx_channel_guardian_rate_limits_actor`,
92
+ );
93
+
94
+ // assistant_ingress_invites indexes (migration 112)
95
+ raw.exec(/*sql*/ `DROP INDEX IF EXISTS idx_ingress_invites_channel_status`);
96
+ raw.exec(
97
+ /*sql*/ `DROP INDEX IF EXISTS idx_ingress_invites_channel_created`,
98
+ );
99
+
100
+ // assistant_inbox_thread_state indexes (migration 112)
101
+ raw.exec(/*sql*/ `DROP INDEX IF EXISTS idx_inbox_thread_state_channel`);
102
+ raw.exec(/*sql*/ `DROP INDEX IF EXISTS idx_inbox_thread_state_last_msg`);
103
+ raw.exec(/*sql*/ `DROP INDEX IF EXISTS idx_inbox_thread_state_escalation`);
104
+
105
+ // notification_preferences indexes (migration 114)
106
+ raw.exec(
107
+ /*sql*/ `DROP INDEX IF EXISTS idx_notification_preferences_assistant_id`,
108
+ );
109
+ raw.exec(
110
+ /*sql*/ `DROP INDEX IF EXISTS idx_notification_preferences_assistant_priority`,
111
+ );
112
+
113
+ // notification_events indexes (migration 114)
114
+ raw.exec(
115
+ /*sql*/ `DROP INDEX IF EXISTS idx_notification_events_assistant_event_created`,
116
+ );
117
+ raw.exec(/*sql*/ `DROP INDEX IF EXISTS idx_notification_events_dedupe`);
118
+
119
+ // notification_deliveries indexes (migration 114)
120
+ raw.exec(
121
+ /*sql*/ `DROP INDEX IF EXISTS idx_notification_deliveries_assistant_status`,
122
+ );
123
+
124
+ // conversation_attention_events indexes (migration 117)
125
+ raw.exec(
126
+ /*sql*/ `DROP INDEX IF EXISTS idx_conv_attn_events_assistant_observed`,
127
+ );
128
+
129
+ // conversation_assistant_attention_state indexes (migration 117)
130
+ raw.exec(
131
+ /*sql*/ `DROP INDEX IF EXISTS idx_conv_attn_state_assistant_latest_msg`,
132
+ );
133
+ raw.exec(
134
+ /*sql*/ `DROP INDEX IF EXISTS idx_conv_attn_state_assistant_last_seen`,
135
+ );
136
+
137
+ // actor_token_records indexes (migration 038)
138
+ raw.exec(/*sql*/ `DROP INDEX IF EXISTS idx_actor_tokens_active_device`);
139
+
140
+ // actor_refresh_token_records indexes (migration 039)
141
+ raw.exec(/*sql*/ `DROP INDEX IF EXISTS idx_refresh_tokens_active_device`);
142
+
143
+ // --- Drop assistant_id column from each table ---
144
+ for (const table of tables) {
145
+ const cols = new Set(
146
+ (
147
+ raw.query(`PRAGMA table_info(${table})`).all() as Array<{
148
+ name: string;
149
+ }>
150
+ ).map((c) => c.name),
151
+ );
152
+
153
+ if (!cols.has("assistant_id")) continue;
154
+
155
+ // Re-verify safety before each drop
156
+ const unexpected = raw
157
+ .query(
158
+ `SELECT DISTINCT assistant_id FROM ${table} WHERE assistant_id IS NOT NULL AND assistant_id != 'self'`,
159
+ )
160
+ .all() as Array<{ assistant_id: string }>;
161
+
162
+ if (unexpected.length > 0) {
163
+ log.warn(
164
+ { table, values: unexpected.map((r) => r.assistant_id) },
165
+ "Unexpected assistant_id values — skipping column drop",
166
+ );
167
+ continue;
168
+ }
169
+
170
+ raw.exec(/*sql*/ `ALTER TABLE ${table} DROP COLUMN assistant_id`);
171
+ log.info({ table }, "Dropped assistant_id column");
172
+ }
173
+
174
+ // --- Recreate indexes without assistant_id ---
175
+ // Each index below is the equivalent of the dropped index but with the
176
+ // assistant_id column removed. Index names are updated to avoid
177
+ // collisions with the old names.
178
+
179
+ // channel_guardian_verification_challenges
180
+ raw.exec(
181
+ /*sql*/ `CREATE INDEX IF NOT EXISTS idx_channel_guardian_challenges_lookup ON channel_guardian_verification_challenges(channel, challenge_hash, status)`,
182
+ );
183
+ raw.exec(
184
+ /*sql*/ `CREATE INDEX IF NOT EXISTS idx_guardian_sessions_active ON channel_guardian_verification_challenges(channel, status)`,
185
+ );
186
+ raw.exec(
187
+ /*sql*/ `CREATE INDEX IF NOT EXISTS idx_guardian_sessions_identity ON channel_guardian_verification_challenges(channel, expected_external_user_id, expected_chat_id, status)`,
188
+ );
189
+ raw.exec(
190
+ /*sql*/ `CREATE INDEX IF NOT EXISTS idx_guardian_sessions_destination ON channel_guardian_verification_challenges(channel, destination_address)`,
191
+ );
192
+ raw.exec(
193
+ /*sql*/ `CREATE INDEX IF NOT EXISTS idx_guardian_sessions_bootstrap ON channel_guardian_verification_challenges(channel, bootstrap_token_hash, status)`,
194
+ );
195
+
196
+ // channel_guardian_rate_limits
197
+ raw.exec(
198
+ /*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)`,
199
+ );
200
+
201
+ // assistant_ingress_invites
202
+ raw.exec(
203
+ /*sql*/ `CREATE INDEX IF NOT EXISTS idx_ingress_invites_channel_status ON assistant_ingress_invites(source_channel, status, expires_at)`,
204
+ );
205
+ raw.exec(
206
+ /*sql*/ `CREATE INDEX IF NOT EXISTS idx_ingress_invites_channel_created ON assistant_ingress_invites(source_channel, created_at)`,
207
+ );
208
+
209
+ // assistant_inbox_thread_state
210
+ raw.exec(
211
+ /*sql*/ `CREATE UNIQUE INDEX IF NOT EXISTS idx_inbox_thread_state_channel ON assistant_inbox_thread_state(source_channel, external_chat_id)`,
212
+ );
213
+ raw.exec(
214
+ /*sql*/ `CREATE INDEX IF NOT EXISTS idx_inbox_thread_state_last_msg ON assistant_inbox_thread_state(last_message_at)`,
215
+ );
216
+ raw.exec(
217
+ /*sql*/ `CREATE INDEX IF NOT EXISTS idx_inbox_thread_state_escalation ON assistant_inbox_thread_state(has_pending_escalation, last_message_at)`,
218
+ );
219
+
220
+ // notification_preferences
221
+ raw.exec(
222
+ /*sql*/ `CREATE INDEX IF NOT EXISTS idx_notification_preferences_priority ON notification_preferences(priority DESC)`,
223
+ );
224
+
225
+ // notification_events
226
+ raw.exec(
227
+ /*sql*/ `CREATE INDEX IF NOT EXISTS idx_notification_events_event_created ON notification_events(source_event_name, created_at)`,
228
+ );
229
+ raw.exec(
230
+ /*sql*/ `CREATE UNIQUE INDEX IF NOT EXISTS idx_notification_events_dedupe ON notification_events(dedupe_key) WHERE dedupe_key IS NOT NULL`,
231
+ );
232
+
233
+ // notification_deliveries
234
+ raw.exec(
235
+ /*sql*/ `CREATE INDEX IF NOT EXISTS idx_notification_deliveries_status ON notification_deliveries(status)`,
236
+ );
237
+
238
+ // conversation_attention_events
239
+ raw.exec(
240
+ /*sql*/ `CREATE INDEX IF NOT EXISTS idx_conv_attn_events_observed ON conversation_attention_events(observed_at)`,
241
+ );
242
+
243
+ // conversation_assistant_attention_state
244
+ raw.exec(
245
+ /*sql*/ `CREATE INDEX IF NOT EXISTS idx_conv_attn_state_latest_msg ON conversation_assistant_attention_state(latest_assistant_message_at)`,
246
+ );
247
+ raw.exec(
248
+ /*sql*/ `CREATE INDEX IF NOT EXISTS idx_conv_attn_state_last_seen ON conversation_assistant_attention_state(last_seen_assistant_message_at)`,
249
+ );
250
+
251
+ // actor_token_records
252
+ raw.exec(
253
+ /*sql*/ `CREATE UNIQUE INDEX IF NOT EXISTS idx_actor_tokens_active_device ON actor_token_records(guardian_principal_id, hashed_device_id) WHERE status = 'active'`,
254
+ );
255
+
256
+ // actor_refresh_token_records
257
+ raw.exec(
258
+ /*sql*/ `CREATE UNIQUE INDEX IF NOT EXISTS idx_refresh_tokens_active_device ON actor_refresh_token_records(guardian_principal_id, hashed_device_id) WHERE status = 'active'`,
259
+ );
260
+
261
+ log.info("Completed dropping assistant_id columns from all tables");
262
+ });
263
+ }
@@ -78,6 +78,7 @@ export { migrateContactsAssistantId } from "./132-contacts-assistant-id.js";
78
78
  export { migrateAssistantContactMetadata } from "./133-assistant-contact-metadata.js";
79
79
  export { migrateContactsNotesColumn } from "./134-contacts-notes-column.js";
80
80
  export { migrateBackfillContactInteractionStats } from "./135-backfill-contact-interaction-stats.js";
81
+ export { migrateDropAssistantIdColumns } from "./136-drop-assistant-id-columns.js";
81
82
  export {
82
83
  MIGRATION_REGISTRY,
83
84
  type MigrationRegistryEntry,
@@ -123,11 +123,24 @@ export const MIGRATION_REGISTRY: MigrationRegistryEntry[] = [
123
123
  "Enforce NOT NULL on channel_guardian_bindings.guardian_principal_id after backfill",
124
124
  },
125
125
  {
126
- key: "backfill_contact_interaction_stats",
126
+ key: "migration_contacts_notes_column_v1",
127
127
  version: 17,
128
+ description:
129
+ "Consolidate relationship/importance/response_expectation/preferred_tone into a single notes TEXT column, then drop the legacy columns",
130
+ },
131
+ {
132
+ key: "backfill_contact_interaction_stats",
133
+ version: 18,
128
134
  description:
129
135
  "Backfill contacts.last_interaction from the max lastSeenAt across each contact's channels",
130
136
  },
137
+ {
138
+ key: "migration_drop_assistant_id_columns_v1",
139
+ version: 19,
140
+ dependsOn: ["migration_normalize_assistant_id_to_self_v1"],
141
+ description:
142
+ "Drop assistant_id columns from all 16 daemon tables after normalization to single-tenant identity",
143
+ },
131
144
  ];
132
145
 
133
146
  export interface MigrationValidationResult {
@@ -0,0 +1,18 @@
1
+ import { type DrizzleDb, getSqliteFrom } from "../db-connection.js";
2
+
3
+ /**
4
+ * Startup still replays the historical table/index bootstrap helpers on every
5
+ * process launch, so migrations need a cheap way to branch on the live schema.
6
+ */
7
+ export function tableHasColumn(
8
+ database: DrizzleDb,
9
+ tableName: string,
10
+ columnName: string,
11
+ ): boolean {
12
+ const raw = getSqliteFrom(database);
13
+ const columns = raw.query(`PRAGMA table_info(${tableName})`).all() as Array<{
14
+ name: string;
15
+ }>;
16
+
17
+ return columns.some((column) => column.name === columnName);
18
+ }
@@ -6,7 +6,6 @@ import {
6
6
  text,
7
7
  } from "drizzle-orm/sqlite-core";
8
8
 
9
- import { DAEMON_INTERNAL_ASSISTANT_ID } from "../../runtime/assistant-scope.js";
10
9
  import { conversations } from "./conversations.js";
11
10
 
12
11
  export const callSessions = sqliteTable(
@@ -26,7 +25,6 @@ export const callSessions = sqliteTable(
26
25
  guardianVerificationSessionId: text("guardian_verification_session_id"),
27
26
  callerIdentityMode: text("caller_identity_mode"),
28
27
  callerIdentitySource: text("caller_identity_source"),
29
- assistantId: text("assistant_id"),
30
28
  initiatedFromConversationId: text("initiated_from_conversation_id"),
31
29
  startedAt: integer("started_at"),
32
30
  endedAt: integer("ended_at"),
@@ -91,7 +89,6 @@ export const channelGuardianVerificationChallenges = sqliteTable(
91
89
  "channel_guardian_verification_challenges",
92
90
  {
93
91
  id: text("id").primaryKey(),
94
- assistantId: text("assistant_id").notNull(),
95
92
  channel: text("channel").notNull(),
96
93
  challengeHash: text("challenge_hash").notNull(),
97
94
  expiresAt: integer("expires_at").notNull(),
@@ -128,9 +125,6 @@ export const channelGuardianApprovalRequests = sqliteTable(
128
125
  runId: text("run_id").notNull(),
129
126
  requestId: text("request_id"),
130
127
  conversationId: text("conversation_id").notNull(),
131
- assistantId: text("assistant_id")
132
- .notNull()
133
- .default(DAEMON_INTERNAL_ASSISTANT_ID),
134
128
  channel: text("channel").notNull(),
135
129
  requesterExternalUserId: text("requester_external_user_id").notNull(),
136
130
  requesterChatId: text("requester_chat_id").notNull(),
@@ -151,7 +145,6 @@ export const channelGuardianRateLimits = sqliteTable(
151
145
  "channel_guardian_rate_limits",
152
146
  {
153
147
  id: text("id").primaryKey(),
154
- assistantId: text("assistant_id").notNull(),
155
148
  channel: text("channel").notNull(),
156
149
  actorExternalUserId: text("actor_external_user_id").notNull(),
157
150
  actorChatId: text("actor_chat_id").notNull(),
@@ -1,6 +1,5 @@
1
1
  import { index, integer, sqliteTable, text } from "drizzle-orm/sqlite-core";
2
2
 
3
- import { DAEMON_INTERNAL_ASSISTANT_ID } from "../../runtime/assistant-scope.js";
4
3
  import { conversations } from "./conversations.js";
5
4
 
6
5
  export const contacts = sqliteTable("contacts", {
@@ -13,7 +12,6 @@ export const contacts = sqliteTable("contacts", {
13
12
  updatedAt: integer("updated_at").notNull(),
14
13
  role: text("role").notNull().default("contact"), // 'guardian' | 'contact'
15
14
  principalId: text("principal_id"), // internal auth principal (nullable)
16
- assistantId: text("assistant_id"), // which assistant this guardian is for (nullable, daemon default is DAEMON_INTERNAL_ASSISTANT_ID)
17
15
  contactType: text("contact_type").notNull().default("human"), // 'human' | 'assistant'
18
16
  });
19
17
 
@@ -69,9 +67,6 @@ export const assistantIngressInvites = sqliteTable(
69
67
  "assistant_ingress_invites",
70
68
  {
71
69
  id: text("id").primaryKey(),
72
- assistantId: text("assistant_id")
73
- .notNull()
74
- .default(DAEMON_INTERNAL_ASSISTANT_ID),
75
70
  sourceChannel: text("source_channel").notNull(),
76
71
  tokenHash: text("token_hash").notNull(),
77
72
  createdBySessionId: text("created_by_session_id"),
@@ -103,9 +98,6 @@ export const assistantInboxThreadState = sqliteTable(
103
98
  conversationId: text("conversation_id")
104
99
  .primaryKey()
105
100
  .references(() => conversations.id, { onDelete: "cascade" }),
106
- assistantId: text("assistant_id")
107
- .notNull()
108
- .default(DAEMON_INTERNAL_ASSISTANT_ID),
109
101
  sourceChannel: text("source_channel").notNull(),
110
102
  externalChatId: text("external_chat_id").notNull(),
111
103
  externalUserId: text("external_user_id"),
@@ -1,15 +1,11 @@
1
1
  import { index, integer, sqliteTable, text } from "drizzle-orm/sqlite-core";
2
2
 
3
- import { DAEMON_INTERNAL_ASSISTANT_ID } from "../../runtime/assistant-scope.js";
4
3
  import { callPendingQuestions, callSessions } from "./calls.js";
5
4
 
6
5
  export const guardianActionRequests = sqliteTable(
7
6
  "guardian_action_requests",
8
7
  {
9
8
  id: text("id").primaryKey(),
10
- assistantId: text("assistant_id")
11
- .notNull()
12
- .default(DAEMON_INTERNAL_ASSISTANT_ID),
13
9
  kind: text("kind").notNull(), // 'ask_guardian'
14
10
  sourceChannel: text("source_channel").notNull(), // 'voice'
15
11
  sourceConversationId: text("source_conversation_id").notNull(),
@@ -145,7 +141,6 @@ export const scopedApprovalGrants = sqliteTable(
145
141
  "scoped_approval_grants",
146
142
  {
147
143
  id: text("id").primaryKey(),
148
- assistantId: text("assistant_id").notNull(),
149
144
  scopeMode: text("scope_mode").notNull(), // 'request_id' | 'tool_signature'
150
145
  requestId: text("request_id"),
151
146
  toolName: text("tool_name"),
@@ -173,7 +173,6 @@ export const llmUsageEvents = sqliteTable(
173
173
  export const actorTokenRecords = sqliteTable("actor_token_records", {
174
174
  id: text("id").primaryKey(),
175
175
  tokenHash: text("token_hash").notNull(),
176
- assistantId: text("assistant_id").notNull(),
177
176
  guardianPrincipalId: text("guardian_principal_id").notNull(),
178
177
  hashedDeviceId: text("hashed_device_id").notNull(),
179
178
  platform: text("platform").notNull(),
@@ -190,7 +189,6 @@ export const actorRefreshTokenRecords = sqliteTable(
190
189
  id: text("id").primaryKey(),
191
190
  tokenHash: text("token_hash").notNull(),
192
191
  familyId: text("family_id").notNull(),
193
- assistantId: text("assistant_id").notNull(),
194
192
  guardianPrincipalId: text("guardian_principal_id").notNull(),
195
193
  hashedDeviceId: text("hashed_device_id").notNull(),
196
194
  platform: text("platform").notNull(),
@@ -11,7 +11,6 @@ import { conversations } from "./conversations.js";
11
11
 
12
12
  export const notificationEvents = sqliteTable("notification_events", {
13
13
  id: text("id").primaryKey(),
14
- assistantId: text("assistant_id").notNull(),
15
14
  sourceEventName: text("source_event_name").notNull(),
16
15
  sourceChannel: text("source_channel").notNull(),
17
16
  sourceSessionId: text("source_session_id").notNull(),
@@ -39,7 +38,6 @@ export const notificationDecisions = sqliteTable("notification_decisions", {
39
38
 
40
39
  export const notificationPreferences = sqliteTable("notification_preferences", {
41
40
  id: text("id").primaryKey(),
42
- assistantId: text("assistant_id").notNull(),
43
41
  preferenceText: text("preference_text").notNull(),
44
42
  appliesWhenJson: text("applies_when_json").notNull().default("{}"),
45
43
  priority: integer("priority").notNull().default(0),
@@ -95,7 +93,6 @@ export const notificationDeliveries = sqliteTable(
95
93
  notificationDecisionId: text("notification_decision_id")
96
94
  .notNull()
97
95
  .references(() => notificationDecisions.id, { onDelete: "cascade" }),
98
- assistantId: text("assistant_id").notNull(),
99
96
  channel: text("channel").notNull(),
100
97
  destination: text("destination").notNull(),
101
98
  status: text("status").notNull().default("pending"),
@@ -132,7 +129,6 @@ export const conversationAttentionEvents = sqliteTable(
132
129
  conversationId: text("conversation_id")
133
130
  .notNull()
134
131
  .references(() => conversations.id, { onDelete: "cascade" }),
135
- assistantId: text("assistant_id").notNull(),
136
132
  sourceChannel: text("source_channel").notNull(),
137
133
  signalType: text("signal_type").notNull(),
138
134
  confidence: text("confidence").notNull(),
@@ -147,10 +143,7 @@ export const conversationAttentionEvents = sqliteTable(
147
143
  table.conversationId,
148
144
  table.observedAt,
149
145
  ),
150
- index("idx_conv_attn_events_assistant_observed").on(
151
- table.assistantId,
152
- table.observedAt,
153
- ),
146
+ index("idx_conv_attn_events_observed").on(table.observedAt),
154
147
  index("idx_conv_attn_events_channel_observed").on(
155
148
  table.sourceChannel,
156
149
  table.observedAt,
@@ -164,7 +157,6 @@ export const conversationAssistantAttentionState = sqliteTable(
164
157
  conversationId: text("conversation_id")
165
158
  .primaryKey()
166
159
  .references(() => conversations.id, { onDelete: "cascade" }),
167
- assistantId: text("assistant_id").notNull(),
168
160
  latestAssistantMessageId: text("latest_assistant_message_id"),
169
161
  latestAssistantMessageAt: integer("latest_assistant_message_at"),
170
162
  lastSeenAssistantMessageId: text("last_seen_assistant_message_id"),
@@ -179,13 +171,7 @@ export const conversationAssistantAttentionState = sqliteTable(
179
171
  updatedAt: integer("updated_at").notNull(),
180
172
  },
181
173
  (table) => [
182
- index("idx_conv_attn_state_assistant_latest_msg").on(
183
- table.assistantId,
184
- table.latestAssistantMessageAt,
185
- ),
186
- index("idx_conv_attn_state_assistant_last_seen").on(
187
- table.assistantId,
188
- table.lastSeenAssistantMessageAt,
189
- ),
174
+ index("idx_conv_attn_state_latest_msg").on(table.latestAssistantMessageAt),
175
+ index("idx_conv_attn_state_last_seen").on(table.lastSeenAssistantMessageAt),
190
176
  ],
191
177
  );
@@ -29,7 +29,6 @@ export type GrantStatus = "active" | "consumed" | "expired" | "revoked";
29
29
 
30
30
  export interface ScopedApprovalGrant {
31
31
  id: string;
32
- assistantId: string;
33
32
  scopeMode: ScopeMode;
34
33
  requestId: string | null;
35
34
  toolName: string | null;
@@ -65,7 +64,6 @@ function rowToGrant(
65
64
  ): ScopedApprovalGrant {
66
65
  return {
67
66
  id: row.id,
68
- assistantId: row.assistantId,
69
67
  scopeMode: row.scopeMode as ScopeMode,
70
68
  requestId: row.requestId,
71
69
  toolName: row.toolName,
@@ -91,7 +89,6 @@ function rowToGrant(
91
89
  // ---------------------------------------------------------------------------
92
90
 
93
91
  export interface CreateScopedApprovalGrantParams {
94
- assistantId: string;
95
92
  scopeMode: ScopeMode;
96
93
  requestId?: string | null;
97
94
  toolName?: string | null;
@@ -115,7 +112,6 @@ function createScopedApprovalGrant(
115
112
 
116
113
  const row = {
117
114
  id,
118
- assistantId: params.assistantId,
119
115
  scopeMode: params.scopeMode,
120
116
  requestId: params.requestId ?? null,
121
117
  toolName: params.toolName ?? null,
@@ -143,7 +139,6 @@ function createScopedApprovalGrant(
143
139
  grantId: id,
144
140
  scopeMode: params.scopeMode,
145
141
  toolName: params.toolName ?? null,
146
- assistantId: params.assistantId,
147
142
  requestChannel: params.requestChannel,
148
143
  decisionChannel: params.decisionChannel,
149
144
  executionChannel: params.executionChannel ?? null,
@@ -168,13 +163,12 @@ export interface ConsumeByRequestIdResult {
168
163
  * Atomically consume a grant by request ID.
169
164
  *
170
165
  * Only succeeds when exactly one active, non-expired grant matches the
171
- * given `requestId` and `assistantId`. Uses compare-and-swap on the
172
- * `status` column so concurrent consumers race safely — at most one wins.
166
+ * given `requestId`. Uses compare-and-swap on the `status` column so
167
+ * concurrent consumers race safely — at most one wins.
173
168
  */
174
169
  function consumeScopedApprovalGrantByRequestId(
175
170
  requestId: string,
176
171
  consumingRequestId: string,
177
- assistantId: string,
178
172
  now?: string,
179
173
  ): ConsumeByRequestIdResult {
180
174
  const db = getDb();
@@ -189,7 +183,6 @@ function consumeScopedApprovalGrantByRequestId(
189
183
  .where(
190
184
  and(
191
185
  eq(scopedApprovalGrants.requestId, requestId),
192
- eq(scopedApprovalGrants.assistantId, assistantId),
193
186
  eq(scopedApprovalGrants.scopeMode, "request_id"),
194
187
  eq(scopedApprovalGrants.status, "active"),
195
188
  sql`${scopedApprovalGrants.expiresAt} > ${currentTime}`,
@@ -204,7 +197,6 @@ function consumeScopedApprovalGrantByRequestId(
204
197
  event: "scoped_grant_consume_miss",
205
198
  requestId,
206
199
  consumingRequestId,
207
- assistantId,
208
200
  scopeMode: "request_id",
209
201
  attempt,
210
202
  },
@@ -247,7 +239,6 @@ function consumeScopedApprovalGrantByRequestId(
247
239
  grantId: grant?.id,
248
240
  requestId,
249
241
  consumingRequestId,
250
- assistantId,
251
242
  scopeMode: "request_id",
252
243
  },
253
244
  "Scoped approval grant consumed by request ID",
@@ -262,7 +253,6 @@ function consumeScopedApprovalGrantByRequestId(
262
253
  event: "scoped_grant_consume_miss",
263
254
  requestId,
264
255
  consumingRequestId,
265
- assistantId,
266
256
  scopeMode: "request_id",
267
257
  reason: "cas_exhausted",
268
258
  },
@@ -280,7 +270,6 @@ export interface ConsumeByToolSignatureParams {
280
270
  inputDigest: string;
281
271
  consumingRequestId: string;
282
272
  /** Optional context constraints — only matched when the grant has a non-null value */
283
- assistantId?: string;
284
273
  executionChannel?: string;
285
274
  conversationId?: string;
286
275
  callSessionId?: string;
@@ -319,12 +308,6 @@ function consumeScopedApprovalGrantByToolSignature(
319
308
  sql`${scopedApprovalGrants.expiresAt} > ${currentTime}`,
320
309
  ];
321
310
 
322
- // assistantId is always set on grants — scope consumption to the current
323
- // assistant so grants minted for one assistant cannot be consumed by another.
324
- if (params.assistantId !== undefined) {
325
- conditions.push(eq(scopedApprovalGrants.assistantId, params.assistantId));
326
- }
327
-
328
311
  // Context constraints: grant field must be NULL (any) or match exactly
329
312
  if (params.executionChannel !== undefined) {
330
313
  conditions.push(
@@ -488,7 +471,6 @@ export function expireScopedApprovalGrants(now?: string): number {
488
471
  // ---------------------------------------------------------------------------
489
472
 
490
473
  export interface RevokeContextParams {
491
- assistantId?: string;
492
474
  conversationId?: string;
493
475
  callSessionId?: string;
494
476
  requestChannel?: string;
@@ -510,9 +492,6 @@ export function revokeScopedApprovalGrantsForContext(
510
492
 
511
493
  const conditions = [eq(scopedApprovalGrants.status, "active")];
512
494
 
513
- if (params.assistantId !== undefined) {
514
- conditions.push(eq(scopedApprovalGrants.assistantId, params.assistantId));
515
- }
516
495
  if (params.conversationId !== undefined) {
517
496
  conditions.push(
518
497
  eq(scopedApprovalGrants.conversationId, params.conversationId),
@@ -550,7 +529,6 @@ export function revokeScopedApprovalGrantsForContext(
550
529
  {
551
530
  event: "scoped_grant_revoked",
552
531
  count,
553
- assistantId: params.assistantId,
554
532
  conversationId: params.conversationId,
555
533
  callSessionId: params.callSessionId,
556
534
  requestChannel: params.requestChannel,
@@ -13,6 +13,7 @@
13
13
  */
14
14
 
15
15
  import { getGatewayInternalBaseUrl } from "../../config/env.js";
16
+ import { DAEMON_INTERNAL_ASSISTANT_ID } from "../../runtime/assistant-scope.js";
16
17
  import { mintDaemonDeliveryToken } from "../../runtime/auth/token-service.js";
17
18
  import { deliverChannelReply } from "../../runtime/gateway-client.js";
18
19
  import { getLogger } from "../../util/logger.js";
@@ -70,7 +71,7 @@ export class SmsAdapter implements ChannelAdapter {
70
71
  {
71
72
  chatId: phoneNumber,
72
73
  text: messageText,
73
- assistantId: payload.assistantId,
74
+ assistantId: DAEMON_INTERNAL_ASSISTANT_ID,
74
75
  },
75
76
  mintDaemonDeliveryToken(),
76
77
  );
@@ -77,10 +77,7 @@ export class NotificationBroadcaster {
77
77
  decision: NotificationDecision,
78
78
  options?: BroadcastDecisionOptions,
79
79
  ): Promise<NotificationDeliveryResult[]> {
80
- const destinations = resolveDestinations(
81
- signal.assistantId,
82
- decision.selectedChannels,
83
- );
80
+ const destinations = resolveDestinations(decision.selectedChannels);
84
81
 
85
82
  // Ensure vellum is processed first so the notification_thread_created IPC
86
83
  // push fires immediately, before slower channel sends (e.g. Telegram 30s
@@ -271,7 +268,6 @@ export class NotificationBroadcaster {
271
268
  const payload: ChannelDeliveryPayload = {
272
269
  deliveryId,
273
270
  sourceEventName: signal.sourceEventName,
274
- assistantId: signal.assistantId,
275
271
  copy,
276
272
  deepLinkTarget,
277
273
  };
@@ -291,7 +287,6 @@ export class NotificationBroadcaster {
291
287
  createDelivery({
292
288
  id: deliveryId,
293
289
  notificationDecisionId: persistedDecisionId,
294
- assistantId: signal.assistantId,
295
290
  channel,
296
291
  destination: destinationLabel,
297
292
  status: "pending",