@vellumai/assistant 0.4.33 → 0.4.34

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 (137) 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 +150 -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/notifications/tools/send-notification.ts +0 -1
  56. package/src/contacts/contact-store.ts +13 -88
  57. package/src/contacts/contacts-write.ts +3 -11
  58. package/src/contacts/types.ts +0 -1
  59. package/src/daemon/handlers/config-channels.ts +16 -42
  60. package/src/daemon/handlers/config-inbox.ts +6 -6
  61. package/src/daemon/handlers/contacts.ts +3 -11
  62. package/src/daemon/handlers/index.ts +0 -2
  63. package/src/daemon/session-process.ts +0 -4
  64. package/src/memory/conversation-attention-store.ts +4 -19
  65. package/src/memory/conversation-crud.ts +0 -2
  66. package/src/memory/db-init.ts +4 -0
  67. package/src/memory/guardian-action-store.ts +0 -12
  68. package/src/memory/guardian-approvals.ts +35 -80
  69. package/src/memory/guardian-rate-limits.ts +1 -14
  70. package/src/memory/guardian-verification.ts +6 -34
  71. package/src/memory/invite-store.ts +5 -14
  72. package/src/memory/migrations/134-contacts-notes-column.ts +64 -45
  73. package/src/memory/migrations/136-drop-assistant-id-columns.ts +263 -0
  74. package/src/memory/migrations/index.ts +1 -0
  75. package/src/memory/migrations/registry.ts +14 -1
  76. package/src/memory/schema/calls.ts +0 -7
  77. package/src/memory/schema/contacts.ts +0 -8
  78. package/src/memory/schema/guardian.ts +0 -5
  79. package/src/memory/schema/infrastructure.ts +0 -2
  80. package/src/memory/schema/notifications.ts +3 -17
  81. package/src/memory/scoped-approval-grants.ts +2 -24
  82. package/src/notifications/adapters/sms.ts +2 -1
  83. package/src/notifications/broadcaster.ts +1 -6
  84. package/src/notifications/decision-engine.ts +3 -4
  85. package/src/notifications/deliveries-store.ts +0 -4
  86. package/src/notifications/destination-resolver.ts +4 -6
  87. package/src/notifications/deterministic-checks.ts +1 -6
  88. package/src/notifications/emit-signal.ts +4 -11
  89. package/src/notifications/events-store.ts +7 -17
  90. package/src/notifications/preference-summary.ts +2 -2
  91. package/src/notifications/preferences-store.ts +2 -9
  92. package/src/notifications/signal.ts +0 -1
  93. package/src/notifications/thread-candidates.ts +1 -11
  94. package/src/notifications/types.ts +0 -3
  95. package/src/runtime/access-request-helper.ts +3 -10
  96. package/src/runtime/actor-refresh-token-store.ts +0 -6
  97. package/src/runtime/actor-token-store.ts +3 -16
  98. package/src/runtime/actor-trust-resolver.ts +1 -4
  99. package/src/runtime/auth/__tests__/credential-service.test.ts +0 -9
  100. package/src/runtime/auth/credential-service.ts +1 -15
  101. package/src/runtime/auth/require-bound-guardian.ts +1 -4
  102. package/src/runtime/channel-guardian-service.ts +15 -46
  103. package/src/runtime/channel-invite-transport.ts +8 -0
  104. package/src/runtime/channel-invite-transports/email.ts +4 -0
  105. package/src/runtime/channel-invite-transports/slack.ts +6 -0
  106. package/src/runtime/channel-invite-transports/sms.ts +4 -0
  107. package/src/runtime/channel-invite-transports/telegram.ts +6 -0
  108. package/src/runtime/confirmation-request-guardian-bridge.ts +0 -1
  109. package/src/runtime/guardian-action-followup-executor.ts +3 -2
  110. package/src/runtime/guardian-action-grant-minter.ts +0 -1
  111. package/src/runtime/guardian-outbound-actions.ts +2 -12
  112. package/src/runtime/guardian-vellum-migration.ts +2 -3
  113. package/src/runtime/http-server.ts +0 -1
  114. package/src/runtime/invite-redemption-service.ts +1 -14
  115. package/src/runtime/local-actor-identity.ts +2 -5
  116. package/src/runtime/routes/access-request-decision.ts +0 -1
  117. package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +0 -9
  118. package/src/runtime/routes/channel-readiness-routes.ts +29 -18
  119. package/src/runtime/routes/contact-routes.ts +15 -40
  120. package/src/runtime/routes/conversation-attention-routes.ts +0 -2
  121. package/src/runtime/routes/global-search-routes.ts +0 -2
  122. package/src/runtime/routes/guardian-bootstrap-routes.ts +5 -6
  123. package/src/runtime/routes/guardian-expiry-sweep.ts +3 -2
  124. package/src/runtime/routes/inbound-message-handler.ts +0 -3
  125. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +7 -43
  126. package/src/runtime/routes/inbound-stages/background-dispatch.ts +1 -4
  127. package/src/runtime/routes/inbound-stages/bootstrap-intercept.ts +1 -6
  128. package/src/runtime/routes/inbound-stages/escalation-intercept.ts +0 -1
  129. package/src/runtime/routes/inbound-stages/secret-ingress-check.ts +0 -1
  130. package/src/runtime/routes/inbound-stages/verification-intercept.ts +3 -7
  131. package/src/runtime/routes/pairing-routes.ts +4 -4
  132. package/src/runtime/tool-grant-request-helper.ts +0 -1
  133. package/src/tools/browser/browser-manager.ts +22 -21
  134. package/src/tools/browser/runtime-check.ts +111 -6
  135. package/src/tools/calls/call-start.ts +1 -3
  136. package/src/tools/followups/followup_create.ts +1 -2
  137. package/src/tools/tool-approval-handler.ts +0 -2
@@ -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
- const raw = getSqliteFrom(database);
8
+ withCrashRecovery(database, "migration_contacts_notes_column_v1", () => {
9
+ const raw = getSqliteFrom(database);
8
10
 
9
- try {
10
- raw.exec(/*sql*/ `ALTER TABLE contacts ADD COLUMN notes TEXT`);
11
- } catch {
12
- /* already exists */
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
- // Check if legacy columns still exist before attempting backfill + drop
16
- const cols = new Set(
17
- (
18
- raw.query(`PRAGMA table_info(contacts)`).all() as Array<{ name: string }>
19
- ).map((c) => c.name),
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
- if (cols.has("relationship")) {
23
- const rows = raw
24
- .query(
25
- `SELECT id, relationship, importance, response_expectation, preferred_tone
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
- .all() as Array<{
33
- id: string;
34
- relationship: string | null;
35
- importance: number;
36
- response_expectation: string | null;
37
- preferred_tone: string | null;
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
- const update = raw.prepare(`UPDATE contacts SET notes = ? WHERE id = ?`);
55
+ const update = raw.prepare(`UPDATE contacts SET notes = ? WHERE id = ?`);
41
56
 
42
- for (const row of rows) {
43
- const parts: string[] = [];
44
- if (row.relationship) parts.push(`Relationship: ${row.relationship}`);
45
- if (row.importance !== 0.5) parts.push(`Importance: ${row.importance}`);
46
- if (row.response_expectation)
47
- parts.push(`Response expectation: ${row.response_expectation}`);
48
- if (row.preferred_tone)
49
- parts.push(`Preferred tone: ${row.preferred_tone}`);
50
- if (parts.length > 0) {
51
- update.run(parts.join("\n"), row.id);
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
- const migrated = rows.length;
56
- if (migrated > 0) {
57
- log.info({ migrated }, "Migrated contact metadata to notes field");
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
- raw.exec(/*sql*/ `ALTER TABLE contacts DROP COLUMN relationship`);
64
- raw.exec(/*sql*/ `ALTER TABLE contacts DROP COLUMN importance`);
65
- raw.exec(/*sql*/ `ALTER TABLE contacts DROP COLUMN response_expectation`);
66
- raw.exec(/*sql*/ `ALTER TABLE contacts DROP COLUMN preferred_tone`);
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
  }
@@ -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 {
@@ -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
  );