@vellumai/assistant 0.4.31 → 0.4.32

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 (121) hide show
  1. package/ARCHITECTURE.md +1 -1
  2. package/package.json +1 -1
  3. package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +0 -7
  4. package/src/__tests__/anthropic-provider.test.ts +86 -1
  5. package/src/__tests__/assistant-feature-flags-integration.test.ts +2 -2
  6. package/src/__tests__/checker.test.ts +37 -98
  7. package/src/__tests__/commit-message-enrichment-service.test.ts +15 -0
  8. package/src/__tests__/config-schema.test.ts +6 -5
  9. package/src/__tests__/credential-security-invariants.test.ts +1 -0
  10. package/src/__tests__/daemon-server-session-init.test.ts +1 -19
  11. package/src/__tests__/followup-tools.test.ts +0 -30
  12. package/src/__tests__/gemini-provider.test.ts +79 -1
  13. package/src/__tests__/ipc-snapshot.test.ts +0 -4
  14. package/src/__tests__/managed-proxy-context.test.ts +163 -0
  15. package/src/__tests__/memory-lifecycle-e2e.test.ts +2 -2
  16. package/src/__tests__/memory-regressions.test.ts +6 -6
  17. package/src/__tests__/openai-provider.test.ts +82 -0
  18. package/src/__tests__/provider-fail-open-selection.test.ts +134 -1
  19. package/src/__tests__/provider-managed-proxy-integration.test.ts +269 -0
  20. package/src/__tests__/recurrence-types.test.ts +0 -15
  21. package/src/__tests__/schedule-tools.test.ts +28 -44
  22. package/src/__tests__/skill-feature-flags.test.ts +2 -2
  23. package/src/__tests__/task-management-tools.test.ts +111 -0
  24. package/src/__tests__/twilio-config.test.ts +0 -3
  25. package/src/amazon/session.ts +30 -91
  26. package/src/calls/call-controller.ts +423 -571
  27. package/src/calls/finalize-call.ts +20 -0
  28. package/src/calls/relay-access-wait.ts +340 -0
  29. package/src/calls/relay-server.ts +267 -902
  30. package/src/calls/relay-setup-router.ts +307 -0
  31. package/src/calls/relay-verification.ts +280 -0
  32. package/src/calls/twilio-config.ts +1 -8
  33. package/src/calls/voice-control-protocol.ts +184 -0
  34. package/src/calls/voice-session-bridge.ts +1 -8
  35. package/src/config/agent-schema.ts +1 -1
  36. package/src/config/bundled-skills/followups/TOOLS.json +0 -4
  37. package/src/config/bundled-skills/schedule/SKILL.md +1 -1
  38. package/src/config/bundled-skills/schedule/TOOLS.json +2 -10
  39. package/src/config/core-schema.ts +1 -1
  40. package/src/config/env.ts +0 -10
  41. package/src/config/feature-flag-registry.json +1 -1
  42. package/src/config/loader.ts +19 -0
  43. package/src/config/schema.ts +2 -2
  44. package/src/daemon/handlers/session-history.ts +398 -0
  45. package/src/daemon/handlers/session-user-message.ts +982 -0
  46. package/src/daemon/handlers/sessions.ts +9 -1338
  47. package/src/daemon/ipc-contract/sessions.ts +0 -6
  48. package/src/daemon/ipc-contract-inventory.json +0 -1
  49. package/src/daemon/lifecycle.ts +0 -29
  50. package/src/home-base/app-link-store.ts +0 -7
  51. package/src/memory/conversation-attention-store.ts +1 -1
  52. package/src/memory/conversation-store.ts +0 -51
  53. package/src/memory/db-init.ts +5 -1
  54. package/src/memory/job-handlers/conflict.ts +24 -0
  55. package/src/memory/migrations/105-contacts-and-triage.ts +4 -7
  56. package/src/memory/migrations/134-contacts-notes-column.ts +50 -33
  57. package/src/memory/migrations/registry.ts +6 -0
  58. package/src/memory/recall-cache.ts +0 -5
  59. package/src/memory/schema/calls.ts +274 -0
  60. package/src/memory/schema/contacts.ts +125 -0
  61. package/src/memory/schema/conversations.ts +129 -0
  62. package/src/memory/schema/guardian.ts +172 -0
  63. package/src/memory/schema/index.ts +8 -0
  64. package/src/memory/schema/infrastructure.ts +205 -0
  65. package/src/memory/schema/memory-core.ts +196 -0
  66. package/src/memory/schema/notifications.ts +191 -0
  67. package/src/memory/schema/tasks.ts +78 -0
  68. package/src/memory/schema.ts +1 -1385
  69. package/src/memory/slack-thread-store.ts +0 -69
  70. package/src/notifications/decisions-store.ts +2 -105
  71. package/src/notifications/deliveries-store.ts +0 -11
  72. package/src/notifications/preferences-store.ts +1 -58
  73. package/src/permissions/checker.ts +6 -17
  74. package/src/providers/anthropic/client.ts +6 -2
  75. package/src/providers/gemini/client.ts +13 -2
  76. package/src/providers/managed-proxy/constants.ts +55 -0
  77. package/src/providers/managed-proxy/context.ts +77 -0
  78. package/src/providers/registry.ts +112 -0
  79. package/src/runtime/auth/__tests__/guard-tests.test.ts +51 -23
  80. package/src/runtime/http-server.ts +83 -722
  81. package/src/runtime/http-types.ts +0 -16
  82. package/src/runtime/middleware/auth.ts +0 -12
  83. package/src/runtime/routes/app-routes.ts +33 -0
  84. package/src/runtime/routes/approval-routes.ts +32 -0
  85. package/src/runtime/routes/attachment-routes.ts +32 -0
  86. package/src/runtime/routes/brain-graph-routes.ts +27 -0
  87. package/src/runtime/routes/call-routes.ts +41 -0
  88. package/src/runtime/routes/channel-readiness-routes.ts +20 -0
  89. package/src/runtime/routes/channel-routes.ts +70 -0
  90. package/src/runtime/routes/contact-routes.ts +63 -0
  91. package/src/runtime/routes/conversation-attention-routes.ts +15 -0
  92. package/src/runtime/routes/conversation-routes.ts +190 -193
  93. package/src/runtime/routes/debug-routes.ts +15 -0
  94. package/src/runtime/routes/events-routes.ts +16 -0
  95. package/src/runtime/routes/global-search-routes.ts +15 -0
  96. package/src/runtime/routes/guardian-action-routes.ts +22 -0
  97. package/src/runtime/routes/guardian-bootstrap-routes.ts +20 -0
  98. package/src/runtime/routes/guardian-refresh-routes.ts +20 -0
  99. package/src/runtime/routes/identity-routes.ts +20 -0
  100. package/src/runtime/routes/inbound-message-handler.ts +8 -0
  101. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +6 -6
  102. package/src/runtime/routes/integration-routes.ts +83 -0
  103. package/src/runtime/routes/invite-routes.ts +31 -0
  104. package/src/runtime/routes/migration-routes.ts +30 -0
  105. package/src/runtime/routes/pairing-routes.ts +18 -0
  106. package/src/runtime/routes/secret-routes.ts +20 -0
  107. package/src/runtime/routes/surface-action-routes.ts +26 -0
  108. package/src/runtime/routes/trust-rules-routes.ts +31 -0
  109. package/src/runtime/routes/twilio-routes.ts +79 -0
  110. package/src/schedule/recurrence-types.ts +1 -11
  111. package/src/tools/followups/followup_create.ts +9 -3
  112. package/src/tools/mcp/mcp-tool-factory.ts +0 -17
  113. package/src/tools/memory/definitions.ts +0 -6
  114. package/src/tools/network/script-proxy/session-manager.ts +38 -3
  115. package/src/tools/schedule/create.ts +1 -3
  116. package/src/tools/schedule/update.ts +9 -6
  117. package/src/twitter/session.ts +29 -77
  118. package/src/util/cookie-session.ts +114 -0
  119. package/src/__tests__/conversation-routes.test.ts +0 -99
  120. package/src/__tests__/task-tools.test.ts +0 -685
  121. package/src/contacts/startup-migration.ts +0 -21
@@ -141,11 +141,6 @@ export interface UsageRequest {
141
141
  sessionId: string;
142
142
  }
143
143
 
144
- export interface SandboxSetRequest {
145
- type: "sandbox_set";
146
- enabled: boolean;
147
- }
148
-
149
144
  export interface SessionsClearRequest {
150
145
  type: "sessions_clear";
151
146
  }
@@ -421,7 +416,6 @@ export type _SessionsClientMessages =
421
416
  | UndoRequest
422
417
  | RegenerateRequest
423
418
  | UsageRequest
424
- | SandboxSetRequest
425
419
  | SessionListRequest
426
420
  | SessionCreateRequest
427
421
  | SessionSwitchRequest
@@ -123,7 +123,6 @@
123
123
  "reorder_threads",
124
124
  "ride_shotgun_start",
125
125
  "ride_shotgun_stop",
126
- "sandbox_set",
127
126
  "schedule_remove",
128
127
  "schedule_run_now",
129
128
  "schedule_toggle",
@@ -20,7 +20,6 @@ import {
20
20
  import { loadConfig } from "../config/loader.js";
21
21
  import { ensurePromptFiles } from "../config/system-prompt.js";
22
22
  import { syncUpdateBulletinOnStartup } from "../config/update-bulletin.js";
23
- import { migrateContactsFromLegacyTables } from "../contacts/startup-migration.js";
24
23
  import { HeartbeatService } from "../heartbeat/heartbeat-service.js";
25
24
  import { getHookManager } from "../hooks/manager.js";
26
25
  import { installTemplates } from "../hooks/templates.js";
@@ -204,18 +203,6 @@ export async function runDaemon(): Promise<void> {
204
203
  );
205
204
  }
206
205
 
207
- // Catch-up migration: populate contacts table from legacy guardian
208
- // bindings and contact rows. Ensures upgrades from pre-contacts
209
- // versions have a populated contacts table on first boot.
210
- try {
211
- migrateContactsFromLegacyTables("self");
212
- } catch (err) {
213
- log.warn(
214
- { err },
215
- "Contacts startup migration failed — continuing startup",
216
- );
217
- }
218
-
219
206
  try {
220
207
  syncUpdateBulletinOnStartup();
221
208
  } catch (err) {
@@ -437,22 +424,6 @@ export async function runDaemon(): Promise<void> {
437
424
  sourceChannel,
438
425
  sourceInterface,
439
426
  ),
440
- persistAndProcessMessage: (
441
- conversationId,
442
- content,
443
- attachmentIds,
444
- options,
445
- sourceChannel,
446
- sourceInterface,
447
- ) =>
448
- server.persistAndProcessMessage(
449
- conversationId,
450
- content,
451
- attachmentIds,
452
- options,
453
- sourceChannel,
454
- sourceInterface,
455
- ),
456
427
  interfacesDir: getInterfacesDir(),
457
428
  approvalCopyGenerator: createApprovalCopyGenerator(),
458
429
  approvalConversationGenerator: createApprovalConversationGenerator(),
@@ -76,10 +76,3 @@ export function setHomeBaseAppLink(
76
76
  updatedAt: now,
77
77
  };
78
78
  }
79
-
80
- export function clearHomeBaseAppLink(): void {
81
- const db = getDb();
82
- db.delete(homeBaseAppLinks)
83
- .where(eq(homeBaseAppLinks.id, HOME_BASE_LINK_ID))
84
- .run();
85
- }
@@ -439,7 +439,7 @@ export function listConversationAttention(
439
439
  })
440
440
  .from(conversationAssistantAttentionState);
441
441
 
442
- // Join with conversations table when filtering by source or sourceChannel
442
+ // Only join conversations table when filtering by source or sourceChannel
443
443
  if (source || sourceChannel) {
444
444
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
445
445
  query = (query as any).innerJoin(
@@ -54,27 +54,6 @@ export { ensureDisplayOrderMigration as ensureColumns } from "./conversation-dis
54
54
  // CRUD functions for display_order and is_pinned
55
55
  // ---------------------------------------------------------------------------
56
56
 
57
- export function getDisplayOrder(conversationId: string): number | null {
58
- ensureDisplayOrderMigration();
59
- const row = rawGet<{ display_order: number | null }>(
60
- "SELECT display_order FROM conversations WHERE id = ?",
61
- conversationId,
62
- );
63
- return row?.display_order ?? null;
64
- }
65
-
66
- export function setDisplayOrder(
67
- conversationId: string,
68
- order: number | null,
69
- ): void {
70
- ensureDisplayOrderMigration();
71
- rawRun(
72
- "UPDATE conversations SET display_order = ? WHERE id = ?",
73
- order,
74
- conversationId,
75
- );
76
- }
77
-
78
57
  export function batchSetDisplayOrders(
79
58
  updates: Array<{
80
59
  id: string;
@@ -100,36 +79,6 @@ export function batchSetDisplayOrders(
100
79
  }
101
80
  }
102
81
 
103
- export function setConversationPinned(
104
- conversationId: string,
105
- isPinned: boolean,
106
- ): void {
107
- ensureDisplayOrderMigration();
108
- rawRun(
109
- "UPDATE conversations SET is_pinned = ? WHERE id = ?",
110
- isPinned ? 1 : 0,
111
- conversationId,
112
- );
113
- }
114
-
115
- export function getConversationDisplayMeta(conversationId: string): {
116
- displayOrder: number | null;
117
- isPinned: boolean;
118
- } {
119
- ensureDisplayOrderMigration();
120
- const row = rawGet<{
121
- display_order: number | null;
122
- is_pinned: number | null;
123
- }>(
124
- "SELECT display_order, is_pinned FROM conversations WHERE id = ?",
125
- conversationId,
126
- );
127
- return {
128
- displayOrder: row?.display_order ?? null,
129
- isPinned: (row?.is_pinned ?? 0) === 1,
130
- };
131
- }
132
-
133
82
  export function getDisplayMetaForConversations(
134
83
  conversationIds: string[],
135
84
  ): Map<string, { displayOrder: number | null; isPinned: boolean }> {
@@ -42,6 +42,7 @@ import {
42
42
  migrateContactChannelsAccessFields,
43
43
  migrateContactChannelsTypeChatIdIndex,
44
44
  migrateContactsAssistantId,
45
+ migrateContactsNotesColumn,
45
46
  migrateContactsRolePrincipal,
46
47
  migrateConversationsThreadTypeIndex,
47
48
  migrateDropLegacyMemberGuardianTables,
@@ -278,7 +279,10 @@ export function initializeDb(): void {
278
279
  // 37. Add contact_type to contacts and assistant_contact_metadata table
279
280
  migrateAssistantContactMetadata(database);
280
281
 
281
- // 38. Backfill contact interaction stats from channel lastSeenAt
282
+ // 38. Consolidate contact metadata columns into single notes field
283
+ migrateContactsNotesColumn(database);
284
+
285
+ // 39. Backfill contact interaction stats from channel lastSeenAt
282
286
  migrateBackfillContactInteractionStats(database);
283
287
 
284
288
  validateMigrationState(database);
@@ -4,12 +4,14 @@ import type { AssistantConfig } from "../../config/types.js";
4
4
  import { getLogger } from "../../util/logger.js";
5
5
  import { resolveConflictClarification } from "../clarification-resolver.js";
6
6
  import {
7
+ areStatementsCoherent,
7
8
  computeConflictRelevance,
8
9
  looksLikeClarificationReply,
9
10
  shouldAttemptConflictResolution,
10
11
  } from "../conflict-intent.js";
11
12
  import {
12
13
  isConflictKindPairEligible,
14
+ isConflictUserEvidenced,
13
15
  isStatementConflictEligible,
14
16
  } from "../conflict-policy.js";
15
17
  import {
@@ -82,6 +84,28 @@ export async function resolvePendingConflictsForMessageJob(
82
84
  status: "dismissed",
83
85
  resolutionNote: "Dismissed by conflict policy (transient/non-durable).",
84
86
  });
87
+ } else if (
88
+ !isConflictUserEvidenced(
89
+ conflict.existingVerificationState,
90
+ conflict.candidateVerificationState,
91
+ )
92
+ ) {
93
+ resolveConflict(conflict.id, {
94
+ status: "dismissed",
95
+ resolutionNote:
96
+ "Dismissed by conflict policy (no user-evidenced provenance).",
97
+ });
98
+ } else if (
99
+ !areStatementsCoherent(
100
+ conflict.existingStatement,
101
+ conflict.candidateStatement,
102
+ )
103
+ ) {
104
+ resolveConflict(conflict.id, {
105
+ status: "dismissed",
106
+ resolutionNote:
107
+ "Dismissed by conflict policy (incoherent — zero statement overlap).",
108
+ });
85
109
  }
86
110
  }
87
111
 
@@ -4,14 +4,14 @@ import type { DrizzleDb } from "../db-connection.js";
4
4
  * Contacts, contact channels, and triage results tables with indexes.
5
5
  */
6
6
  export function createContactsAndTriageTables(database: DrizzleDb): void {
7
+ // Columns removed: relationship, importance, response_expectation, preferred_tone
8
+ // — dropped by migration 134 (contacts-notes-column). Omitting them here keeps
9
+ // the CREATE TABLE idempotent when initializeDb() runs a second time (e.g. the
10
+ // "daemon restart" tests) after migration 134 has already dropped them.
7
11
  database.run(/*sql*/ `
8
12
  CREATE TABLE IF NOT EXISTS contacts (
9
13
  id TEXT PRIMARY KEY,
10
14
  display_name TEXT NOT NULL,
11
- relationship TEXT,
12
- importance REAL NOT NULL DEFAULT 0.5,
13
- response_expectation TEXT,
14
- preferred_tone TEXT,
15
15
  last_interaction INTEGER,
16
16
  interaction_count INTEGER NOT NULL DEFAULT 0,
17
17
  created_at INTEGER NOT NULL,
@@ -33,9 +33,6 @@ export function createContactsAndTriageTables(database: DrizzleDb): void {
33
33
  database.run(
34
34
  /*sql*/ `CREATE INDEX IF NOT EXISTS idx_contacts_display_name ON contacts(display_name)`,
35
35
  );
36
- database.run(
37
- /*sql*/ `CREATE INDEX IF NOT EXISTS idx_contacts_importance ON contacts(importance DESC)`,
38
- );
39
36
  database.run(
40
37
  /*sql*/ `CREATE INDEX IF NOT EXISTS idx_contacts_last_interaction ON contacts(last_interaction DESC)`,
41
38
  );
@@ -6,46 +6,63 @@ const log = getLogger("migration-134");
6
6
  export function migrateContactsNotesColumn(database: DrizzleDb): void {
7
7
  const raw = getSqliteFrom(database);
8
8
 
9
- raw.exec(/*sql*/ `ALTER TABLE contacts ADD COLUMN notes TEXT`);
9
+ try {
10
+ raw.exec(/*sql*/ `ALTER TABLE contacts ADD COLUMN notes TEXT`);
11
+ } catch {
12
+ /* already exists */
13
+ }
14
+
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
+ );
10
21
 
11
- const rows = raw
12
- .query(
13
- `SELECT id, relationship, importance, response_expectation, preferred_tone
22
+ if (cols.has("relationship")) {
23
+ const rows = raw
24
+ .query(
25
+ `SELECT id, relationship, importance, response_expectation, preferred_tone
14
26
  FROM contacts
15
27
  WHERE relationship IS NOT NULL
16
28
  OR importance != 0.5
17
29
  OR response_expectation IS NOT NULL
18
30
  OR preferred_tone IS NOT NULL`,
19
- )
20
- .all() as Array<{
21
- id: string;
22
- relationship: string | null;
23
- importance: number;
24
- response_expectation: string | null;
25
- preferred_tone: string | null;
26
- }>;
27
-
28
- const update = raw.prepare(`UPDATE contacts SET notes = ? WHERE id = ?`);
29
-
30
- for (const row of rows) {
31
- const parts: string[] = [];
32
- if (row.relationship) parts.push(`Relationship: ${row.relationship}`);
33
- if (row.importance !== 0.5) parts.push(`Importance: ${row.importance}`);
34
- if (row.response_expectation)
35
- parts.push(`Response expectation: ${row.response_expectation}`);
36
- if (row.preferred_tone) parts.push(`Preferred tone: ${row.preferred_tone}`);
37
- if (parts.length > 0) {
38
- update.run(parts.join("\n"), row.id);
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
+ }>;
39
+
40
+ const update = raw.prepare(`UPDATE contacts SET notes = ? WHERE id = ?`);
41
+
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);
52
+ }
39
53
  }
40
- }
41
54
 
42
- const migrated = rows.length;
43
- if (migrated > 0) {
44
- log.info({ migrated }, "Migrated contact metadata to notes field");
45
- }
55
+ const migrated = rows.length;
56
+ if (migrated > 0) {
57
+ log.info({ migrated }, "Migrated contact metadata to notes field");
58
+ }
46
59
 
47
- raw.exec(/*sql*/ `ALTER TABLE contacts DROP COLUMN relationship`);
48
- raw.exec(/*sql*/ `ALTER TABLE contacts DROP COLUMN importance`);
49
- raw.exec(/*sql*/ `ALTER TABLE contacts DROP COLUMN response_expectation`);
50
- raw.exec(/*sql*/ `ALTER TABLE contacts DROP COLUMN preferred_tone`);
60
+ // Drop indexes that reference columns we're about to remove
61
+ raw.exec(/*sql*/ `DROP INDEX IF EXISTS idx_contacts_importance`);
62
+
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
+ }
51
68
  }
@@ -122,6 +122,12 @@ export const MIGRATION_REGISTRY: MigrationRegistryEntry[] = [
122
122
  description:
123
123
  "Enforce NOT NULL on channel_guardian_bindings.guardian_principal_id after backfill",
124
124
  },
125
+ {
126
+ key: "backfill_contact_interaction_stats",
127
+ version: 17,
128
+ description:
129
+ "Backfill contacts.last_interaction from the max lastSeenAt across each contact's channels",
130
+ },
125
131
  ];
126
132
 
127
133
  export interface MigrationValidationResult {
@@ -115,8 +115,3 @@ export function setCachedRecall(
115
115
 
116
116
  _cache.set(key, { version: _version, createdAt: Date.now(), result });
117
117
  }
118
-
119
- /** Clear the entire cache (useful for testing). */
120
- export function clearRecallCache(): void {
121
- _cache.clear();
122
- }
@@ -0,0 +1,274 @@
1
+ import {
2
+ index,
3
+ integer,
4
+ real,
5
+ sqliteTable,
6
+ text,
7
+ } from "drizzle-orm/sqlite-core";
8
+
9
+ import { DAEMON_INTERNAL_ASSISTANT_ID } from "../../runtime/assistant-scope.js";
10
+ import { conversations } from "./conversations.js";
11
+
12
+ export const callSessions = sqliteTable(
13
+ "call_sessions",
14
+ {
15
+ id: text("id").primaryKey(),
16
+ conversationId: text("conversation_id")
17
+ .notNull()
18
+ .references(() => conversations.id, { onDelete: "cascade" }),
19
+ provider: text("provider").notNull(),
20
+ providerCallSid: text("provider_call_sid"),
21
+ fromNumber: text("from_number").notNull(),
22
+ toNumber: text("to_number").notNull(),
23
+ task: text("task"),
24
+ status: text("status").notNull().default("initiated"),
25
+ callMode: text("call_mode"),
26
+ guardianVerificationSessionId: text("guardian_verification_session_id"),
27
+ callerIdentityMode: text("caller_identity_mode"),
28
+ callerIdentitySource: text("caller_identity_source"),
29
+ assistantId: text("assistant_id"),
30
+ initiatedFromConversationId: text("initiated_from_conversation_id"),
31
+ startedAt: integer("started_at"),
32
+ endedAt: integer("ended_at"),
33
+ lastError: text("last_error"),
34
+ createdAt: integer("created_at").notNull(),
35
+ updatedAt: integer("updated_at").notNull(),
36
+ },
37
+ (table) => [index("idx_call_sessions_status").on(table.status)],
38
+ );
39
+
40
+ export const callEvents = sqliteTable("call_events", {
41
+ id: text("id").primaryKey(),
42
+ callSessionId: text("call_session_id")
43
+ .notNull()
44
+ .references(() => callSessions.id, { onDelete: "cascade" }),
45
+ eventType: text("event_type").notNull(),
46
+ payloadJson: text("payload_json").notNull().default("{}"),
47
+ createdAt: integer("created_at").notNull(),
48
+ });
49
+
50
+ export const callPendingQuestions = sqliteTable("call_pending_questions", {
51
+ id: text("id").primaryKey(),
52
+ callSessionId: text("call_session_id")
53
+ .notNull()
54
+ .references(() => callSessions.id, { onDelete: "cascade" }),
55
+ questionText: text("question_text").notNull(),
56
+ status: text("status").notNull().default("pending"),
57
+ askedAt: integer("asked_at").notNull(),
58
+ answeredAt: integer("answered_at"),
59
+ answerText: text("answer_text"),
60
+ });
61
+
62
+ export const processedCallbacks = sqliteTable("processed_callbacks", {
63
+ id: text("id").primaryKey(),
64
+ dedupeKey: text("dedupe_key").notNull().unique(),
65
+ callSessionId: text("call_session_id")
66
+ .notNull()
67
+ .references(() => callSessions.id, { onDelete: "cascade" }),
68
+ claimId: text("claim_id"),
69
+ createdAt: integer("created_at").notNull(),
70
+ });
71
+
72
+ export const externalConversationBindings = sqliteTable(
73
+ "external_conversation_bindings",
74
+ {
75
+ conversationId: text("conversation_id")
76
+ .primaryKey()
77
+ .references(() => conversations.id, { onDelete: "cascade" }),
78
+ sourceChannel: text("source_channel").notNull(),
79
+ externalChatId: text("external_chat_id").notNull(),
80
+ externalUserId: text("external_user_id"),
81
+ displayName: text("display_name"),
82
+ username: text("username"),
83
+ createdAt: integer("created_at").notNull(),
84
+ updatedAt: integer("updated_at").notNull(),
85
+ lastInboundAt: integer("last_inbound_at"),
86
+ lastOutboundAt: integer("last_outbound_at"),
87
+ },
88
+ );
89
+
90
+ export const channelGuardianVerificationChallenges = sqliteTable(
91
+ "channel_guardian_verification_challenges",
92
+ {
93
+ id: text("id").primaryKey(),
94
+ assistantId: text("assistant_id").notNull(),
95
+ channel: text("channel").notNull(),
96
+ challengeHash: text("challenge_hash").notNull(),
97
+ expiresAt: integer("expires_at").notNull(),
98
+ status: text("status").notNull().default("pending"),
99
+ createdBySessionId: text("created_by_session_id"),
100
+ consumedByExternalUserId: text("consumed_by_external_user_id"),
101
+ consumedByChatId: text("consumed_by_chat_id"),
102
+ // Outbound session: expected-identity binding
103
+ expectedExternalUserId: text("expected_external_user_id"),
104
+ expectedChatId: text("expected_chat_id"),
105
+ expectedPhoneE164: text("expected_phone_e164"),
106
+ identityBindingStatus: text("identity_binding_status").default("bound"),
107
+ // Outbound session: delivery tracking
108
+ destinationAddress: text("destination_address"),
109
+ lastSentAt: integer("last_sent_at"),
110
+ sendCount: integer("send_count").default(0),
111
+ nextResendAt: integer("next_resend_at"),
112
+ // Session configuration
113
+ codeDigits: integer("code_digits").default(6),
114
+ maxAttempts: integer("max_attempts").default(3),
115
+ // Distinguishes guardian verification from trusted contact verification
116
+ verificationPurpose: text("verification_purpose").default("guardian"),
117
+ // Telegram bootstrap deep-link token hash
118
+ bootstrapTokenHash: text("bootstrap_token_hash"),
119
+ createdAt: integer("created_at").notNull(),
120
+ updatedAt: integer("updated_at").notNull(),
121
+ },
122
+ );
123
+
124
+ export const channelGuardianApprovalRequests = sqliteTable(
125
+ "channel_guardian_approval_requests",
126
+ {
127
+ id: text("id").primaryKey(),
128
+ runId: text("run_id").notNull(),
129
+ requestId: text("request_id"),
130
+ conversationId: text("conversation_id").notNull(),
131
+ assistantId: text("assistant_id")
132
+ .notNull()
133
+ .default(DAEMON_INTERNAL_ASSISTANT_ID),
134
+ channel: text("channel").notNull(),
135
+ requesterExternalUserId: text("requester_external_user_id").notNull(),
136
+ requesterChatId: text("requester_chat_id").notNull(),
137
+ guardianExternalUserId: text("guardian_external_user_id").notNull(),
138
+ guardianChatId: text("guardian_chat_id").notNull(),
139
+ toolName: text("tool_name").notNull(),
140
+ riskLevel: text("risk_level"),
141
+ reason: text("reason"),
142
+ status: text("status").notNull().default("pending"),
143
+ decidedByExternalUserId: text("decided_by_external_user_id"),
144
+ expiresAt: integer("expires_at").notNull(),
145
+ createdAt: integer("created_at").notNull(),
146
+ updatedAt: integer("updated_at").notNull(),
147
+ },
148
+ );
149
+
150
+ export const channelGuardianRateLimits = sqliteTable(
151
+ "channel_guardian_rate_limits",
152
+ {
153
+ id: text("id").primaryKey(),
154
+ assistantId: text("assistant_id").notNull(),
155
+ channel: text("channel").notNull(),
156
+ actorExternalUserId: text("actor_external_user_id").notNull(),
157
+ actorChatId: text("actor_chat_id").notNull(),
158
+ // Legacy columns kept with defaults for backward compatibility with upgraded databases
159
+ // that still have the old NOT NULL columns without DEFAULT. Not read by app logic.
160
+ invalidAttempts: integer("invalid_attempts").notNull().default(0),
161
+ windowStartedAt: integer("window_started_at").notNull().default(0),
162
+ attemptTimestampsJson: text("attempt_timestamps_json")
163
+ .notNull()
164
+ .default("[]"),
165
+ lockedUntil: integer("locked_until"),
166
+ createdAt: integer("created_at").notNull(),
167
+ updatedAt: integer("updated_at").notNull(),
168
+ },
169
+ );
170
+
171
+ export const mediaAssets = sqliteTable("media_assets", {
172
+ id: text("id").primaryKey(),
173
+ title: text("title").notNull(),
174
+ filePath: text("file_path").notNull(),
175
+ mimeType: text("mime_type").notNull(),
176
+ durationSeconds: real("duration_seconds"),
177
+ fileHash: text("file_hash").notNull(),
178
+ status: text("status").notNull().default("registered"), // registered | processing | indexed | failed
179
+ mediaType: text("media_type").notNull(), // video | audio | image
180
+ metadata: text("metadata"), // JSON
181
+ createdAt: integer("created_at").notNull(),
182
+ updatedAt: integer("updated_at").notNull(),
183
+ });
184
+
185
+ export const processingStages = sqliteTable("processing_stages", {
186
+ id: text("id").primaryKey(),
187
+ assetId: text("asset_id")
188
+ .notNull()
189
+ .references(() => mediaAssets.id, { onDelete: "cascade" }),
190
+ stage: text("stage").notNull(),
191
+ status: text("status").notNull().default("pending"), // pending | running | completed | failed
192
+ progress: integer("progress").notNull().default(0), // 0-100
193
+ lastError: text("last_error"),
194
+ startedAt: integer("started_at"),
195
+ completedAt: integer("completed_at"),
196
+ });
197
+
198
+ export const mediaKeyframes = sqliteTable("media_keyframes", {
199
+ id: text("id").primaryKey(),
200
+ assetId: text("asset_id")
201
+ .notNull()
202
+ .references(() => mediaAssets.id, { onDelete: "cascade" }),
203
+ timestamp: real("timestamp").notNull(),
204
+ filePath: text("file_path").notNull(),
205
+ metadata: text("metadata"), // JSON
206
+ createdAt: integer("created_at").notNull(),
207
+ });
208
+
209
+ export const mediaVisionOutputs = sqliteTable("media_vision_outputs", {
210
+ id: text("id").primaryKey(),
211
+ assetId: text("asset_id")
212
+ .notNull()
213
+ .references(() => mediaAssets.id, { onDelete: "cascade" }),
214
+ keyframeId: text("keyframe_id")
215
+ .notNull()
216
+ .references(() => mediaKeyframes.id, { onDelete: "cascade" }),
217
+ analysisType: text("analysis_type").notNull(),
218
+ output: text("output").notNull(), // JSON
219
+ confidence: real("confidence"),
220
+ createdAt: integer("created_at").notNull(),
221
+ });
222
+
223
+ export const mediaTimelines = sqliteTable("media_timelines", {
224
+ id: text("id").primaryKey(),
225
+ assetId: text("asset_id")
226
+ .notNull()
227
+ .references(() => mediaAssets.id, { onDelete: "cascade" }),
228
+ startTime: real("start_time").notNull(),
229
+ endTime: real("end_time").notNull(),
230
+ segmentType: text("segment_type").notNull(),
231
+ attributes: text("attributes"), // JSON
232
+ confidence: real("confidence"),
233
+ createdAt: integer("created_at").notNull(),
234
+ });
235
+
236
+ export const mediaEvents = sqliteTable("media_events", {
237
+ id: text("id").primaryKey(),
238
+ assetId: text("asset_id")
239
+ .notNull()
240
+ .references(() => mediaAssets.id, { onDelete: "cascade" }),
241
+ eventType: text("event_type").notNull(),
242
+ startTime: real("start_time").notNull(),
243
+ endTime: real("end_time").notNull(),
244
+ confidence: real("confidence").notNull(),
245
+ reasons: text("reasons").notNull(), // JSON array
246
+ metadata: text("metadata"), // JSON
247
+ createdAt: integer("created_at").notNull(),
248
+ });
249
+
250
+ export const mediaTrackingProfiles = sqliteTable("media_tracking_profiles", {
251
+ id: text("id").primaryKey(),
252
+ assetId: text("asset_id")
253
+ .notNull()
254
+ .references(() => mediaAssets.id, { onDelete: "cascade" }),
255
+ capabilities: text("capabilities").notNull(), // JSON: { [capName]: { enabled, tier } }
256
+ createdAt: integer("created_at").notNull(),
257
+ });
258
+
259
+ export const mediaEventFeedback = sqliteTable("media_event_feedback", {
260
+ id: text("id").primaryKey(),
261
+ assetId: text("asset_id")
262
+ .notNull()
263
+ .references(() => mediaAssets.id, { onDelete: "cascade" }),
264
+ eventId: text("event_id")
265
+ .notNull()
266
+ .references(() => mediaEvents.id, { onDelete: "cascade" }),
267
+ feedbackType: text("feedback_type").notNull(), // correct | incorrect | boundary_edit | missed
268
+ originalStartTime: real("original_start_time"),
269
+ originalEndTime: real("original_end_time"),
270
+ correctedStartTime: real("corrected_start_time"),
271
+ correctedEndTime: real("corrected_end_time"),
272
+ notes: text("notes"),
273
+ createdAt: integer("created_at").notNull(),
274
+ });