@vellumai/assistant 0.3.4 → 0.3.5

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 (122) hide show
  1. package/Dockerfile +2 -0
  2. package/README.md +37 -2
  3. package/package.json +1 -1
  4. package/scripts/ipc/generate-swift.ts +13 -0
  5. package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +100 -0
  6. package/src/__tests__/approval-hardcoded-copy-guard.test.ts +41 -0
  7. package/src/__tests__/approval-message-composer.test.ts +253 -0
  8. package/src/__tests__/call-domain.test.ts +12 -2
  9. package/src/__tests__/call-orchestrator.test.ts +70 -1
  10. package/src/__tests__/call-routes-http.test.ts +27 -2
  11. package/src/__tests__/channel-approval-routes.test.ts +21 -17
  12. package/src/__tests__/channel-approvals.test.ts +48 -1
  13. package/src/__tests__/channel-guardian.test.ts +74 -22
  14. package/src/__tests__/channel-readiness-service.test.ts +257 -0
  15. package/src/__tests__/config-schema.test.ts +2 -1
  16. package/src/__tests__/credential-security-invariants.test.ts +1 -0
  17. package/src/__tests__/daemon-lifecycle.test.ts +13 -12
  18. package/src/__tests__/dictation-mode-detection.test.ts +63 -0
  19. package/src/__tests__/entity-search.test.ts +615 -0
  20. package/src/__tests__/handlers-twilio-config.test.ts +407 -0
  21. package/src/__tests__/ipc-snapshot.test.ts +63 -0
  22. package/src/__tests__/messaging-send-tool.test.ts +65 -0
  23. package/src/__tests__/run-orchestrator-assistant-events.test.ts +4 -0
  24. package/src/__tests__/run-orchestrator.test.ts +22 -0
  25. package/src/__tests__/session-runtime-assembly.test.ts +85 -1
  26. package/src/__tests__/sms-messaging-provider.test.ts +125 -0
  27. package/src/__tests__/twilio-routes.test.ts +39 -3
  28. package/src/__tests__/twitter-cli-error-shaping.test.ts +2 -2
  29. package/src/__tests__/web-search.test.ts +1 -1
  30. package/src/__tests__/work-item-output.test.ts +110 -0
  31. package/src/calls/call-domain.ts +8 -5
  32. package/src/calls/call-orchestrator.ts +22 -11
  33. package/src/calls/twilio-config.ts +17 -11
  34. package/src/calls/twilio-rest.ts +276 -0
  35. package/src/calls/twilio-routes.ts +39 -1
  36. package/src/config/bundled-skills/knowledge-graph/SKILL.md +15 -0
  37. package/src/config/bundled-skills/knowledge-graph/TOOLS.json +56 -0
  38. package/src/config/bundled-skills/knowledge-graph/tools/graph-query.ts +185 -0
  39. package/src/config/bundled-skills/media-processing/SKILL.md +199 -0
  40. package/src/config/bundled-skills/media-processing/TOOLS.json +320 -0
  41. package/src/config/bundled-skills/media-processing/services/capability-registry.ts +137 -0
  42. package/src/config/bundled-skills/media-processing/services/event-detection-service.ts +280 -0
  43. package/src/config/bundled-skills/media-processing/services/feedback-aggregation.ts +144 -0
  44. package/src/config/bundled-skills/media-processing/services/feedback-store.ts +136 -0
  45. package/src/config/bundled-skills/media-processing/services/processing-pipeline.ts +261 -0
  46. package/src/config/bundled-skills/media-processing/services/retrieval-service.ts +95 -0
  47. package/src/config/bundled-skills/media-processing/services/timeline-service.ts +267 -0
  48. package/src/config/bundled-skills/media-processing/tools/analyze-keyframes.ts +301 -0
  49. package/src/config/bundled-skills/media-processing/tools/detect-events.ts +110 -0
  50. package/src/config/bundled-skills/media-processing/tools/extract-keyframes.ts +190 -0
  51. package/src/config/bundled-skills/media-processing/tools/generate-clip.ts +195 -0
  52. package/src/config/bundled-skills/media-processing/tools/ingest-media.ts +197 -0
  53. package/src/config/bundled-skills/media-processing/tools/media-diagnostics.ts +166 -0
  54. package/src/config/bundled-skills/media-processing/tools/media-status.ts +75 -0
  55. package/src/config/bundled-skills/media-processing/tools/query-media-events.ts +300 -0
  56. package/src/config/bundled-skills/media-processing/tools/recalibrate.ts +235 -0
  57. package/src/config/bundled-skills/media-processing/tools/select-tracking-profile.ts +142 -0
  58. package/src/config/bundled-skills/media-processing/tools/submit-feedback.ts +150 -0
  59. package/src/config/bundled-skills/messaging/SKILL.md +21 -6
  60. package/src/config/bundled-skills/messaging/tools/messaging-send.ts +5 -1
  61. package/src/config/bundled-skills/phone-calls/SKILL.md +2 -2
  62. package/src/config/bundled-skills/twitter/SKILL.md +19 -3
  63. package/src/config/defaults.ts +2 -1
  64. package/src/config/schema.ts +9 -3
  65. package/src/config/system-prompt.ts +24 -0
  66. package/src/config/templates/IDENTITY.md +2 -2
  67. package/src/config/vellum-skills/catalog.json +6 -0
  68. package/src/config/vellum-skills/google-oauth-setup/SKILL.md +3 -3
  69. package/src/config/vellum-skills/slack-oauth-setup/SKILL.md +3 -3
  70. package/src/config/vellum-skills/sms-setup/SKILL.md +118 -0
  71. package/src/config/vellum-skills/twilio-setup/SKILL.md +40 -8
  72. package/src/daemon/handlers/config.ts +783 -9
  73. package/src/daemon/handlers/dictation.ts +182 -0
  74. package/src/daemon/handlers/identity.ts +14 -23
  75. package/src/daemon/handlers/index.ts +2 -0
  76. package/src/daemon/handlers/sessions.ts +2 -0
  77. package/src/daemon/handlers/shared.ts +3 -0
  78. package/src/daemon/handlers/work-items.ts +15 -7
  79. package/src/daemon/ipc-contract-inventory.json +10 -0
  80. package/src/daemon/ipc-contract.ts +108 -4
  81. package/src/daemon/lifecycle.ts +2 -0
  82. package/src/daemon/ride-shotgun-handler.ts +1 -1
  83. package/src/daemon/server.ts +6 -2
  84. package/src/daemon/session-agent-loop.ts +5 -1
  85. package/src/daemon/session-runtime-assembly.ts +55 -0
  86. package/src/daemon/session-tool-setup.ts +2 -0
  87. package/src/daemon/session.ts +11 -1
  88. package/src/inbound/public-ingress-urls.ts +3 -3
  89. package/src/memory/channel-guardian-store.ts +2 -1
  90. package/src/memory/db-init.ts +144 -0
  91. package/src/memory/job-handlers/media-processing.ts +100 -0
  92. package/src/memory/jobs-store.ts +2 -1
  93. package/src/memory/jobs-worker.ts +4 -0
  94. package/src/memory/media-store.ts +759 -0
  95. package/src/memory/retriever.ts +6 -1
  96. package/src/memory/schema.ts +98 -0
  97. package/src/memory/search/entity.ts +208 -25
  98. package/src/memory/search/ranking.ts +6 -1
  99. package/src/memory/search/types.ts +24 -0
  100. package/src/messaging/provider-types.ts +2 -0
  101. package/src/messaging/providers/sms/adapter.ts +204 -0
  102. package/src/messaging/providers/sms/client.ts +93 -0
  103. package/src/messaging/providers/sms/types.ts +7 -0
  104. package/src/permissions/checker.ts +16 -2
  105. package/src/runtime/approval-message-composer.ts +143 -0
  106. package/src/runtime/channel-approvals.ts +12 -4
  107. package/src/runtime/channel-guardian-service.ts +44 -18
  108. package/src/runtime/channel-readiness-service.ts +292 -0
  109. package/src/runtime/channel-readiness-types.ts +29 -0
  110. package/src/runtime/http-server.ts +53 -27
  111. package/src/runtime/http-types.ts +3 -0
  112. package/src/runtime/routes/call-routes.ts +2 -1
  113. package/src/runtime/routes/channel-routes.ts +67 -21
  114. package/src/runtime/run-orchestrator.ts +35 -2
  115. package/src/tools/assets/materialize.ts +2 -2
  116. package/src/tools/calls/call-start.ts +1 -0
  117. package/src/tools/credentials/vault.ts +1 -1
  118. package/src/tools/execution-target.ts +11 -1
  119. package/src/tools/network/web-search.ts +1 -1
  120. package/src/tools/types.ts +2 -0
  121. package/src/twitter/router.ts +1 -1
  122. package/src/util/platform.ts +35 -0
@@ -34,7 +34,7 @@ import {
34
34
  stripInjectedContext,
35
35
  } from './session-runtime-assembly.js';
36
36
  import { buildTemporalContext } from './date-context.js';
37
- import type { ActiveSurfaceContext, ChannelCapabilities } from './session-runtime-assembly.js';
37
+ import type { ActiveSurfaceContext, ChannelCapabilities, GuardianRuntimeContext } from './session-runtime-assembly.js';
38
38
  import {
39
39
  cleanAssistantContent,
40
40
  drainDirectiveDisplayBuffer,
@@ -95,6 +95,7 @@ export interface AgentLoopSessionContext {
95
95
  workspaceTopLevelContext: string | null;
96
96
  workspaceTopLevelDirty: boolean;
97
97
  channelCapabilities?: ChannelCapabilities;
98
+ guardianContext?: GuardianRuntimeContext;
98
99
 
99
100
  readonly coreToolNames: Set<string>;
100
101
  allowedToolNames?: Set<string>;
@@ -296,6 +297,7 @@ export async function runAgentLoopImpl(
296
297
  activeSurface,
297
298
  workspaceTopLevelContext: ctx.workspaceTopLevelContext,
298
299
  channelCapabilities: ctx.channelCapabilities ?? null,
300
+ guardianContext: ctx.guardianContext ?? null,
299
301
  temporalContext,
300
302
  });
301
303
 
@@ -617,6 +619,7 @@ export async function runAgentLoopImpl(
617
619
  activeSurface,
618
620
  workspaceTopLevelContext: ctx.workspaceTopLevelContext,
619
621
  channelCapabilities: ctx.channelCapabilities ?? null,
622
+ guardianContext: ctx.guardianContext ?? null,
620
623
  temporalContext,
621
624
  });
622
625
  preRepairMessages = runMessages;
@@ -649,6 +652,7 @@ export async function runAgentLoopImpl(
649
652
  activeSurface,
650
653
  workspaceTopLevelContext: ctx.workspaceTopLevelContext,
651
654
  channelCapabilities: ctx.channelCapabilities ?? null,
655
+ guardianContext: ctx.guardianContext ?? null,
652
656
  temporalContext,
653
657
  });
654
658
  preRepairMessages = runMessages;
@@ -25,6 +25,18 @@ export interface ChannelCapabilities {
25
25
  supportsVoiceInput: boolean;
26
26
  }
27
27
 
28
+ /** Guardian identity/trust context for external chat channels. */
29
+ export interface GuardianRuntimeContext {
30
+ sourceChannel: string;
31
+ actorRole: 'guardian' | 'non-guardian' | 'unverified_channel';
32
+ guardianChatId?: string;
33
+ guardianExternalUserId?: string;
34
+ requesterIdentifier?: string;
35
+ requesterExternalUserId?: string;
36
+ requesterChatId?: string;
37
+ denialReason?: 'no_binding' | 'no_identity';
38
+ }
39
+
28
40
  /** Derive channel capabilities from a raw source channel identifier. */
29
41
  export function resolveChannelCapabilities(sourceChannel?: string | null): ChannelCapabilities {
30
42
  const channel = sourceChannel ?? 'dashboard';
@@ -264,6 +276,32 @@ export function injectChannelCapabilityContext(message: Message, caps: ChannelCa
264
276
  };
265
277
  }
266
278
 
279
+ /**
280
+ * Prepend guardian trust/identity facts to the last user message so the
281
+ * model can reason about guardian status from deterministic runtime facts.
282
+ */
283
+ export function injectGuardianContext(message: Message, ctx: GuardianRuntimeContext): Message {
284
+ const lines: string[] = ['<guardian_context>'];
285
+ lines.push(`source_channel: ${ctx.sourceChannel}`);
286
+ lines.push(`actor_role: ${ctx.actorRole}`);
287
+ lines.push(`guardian_external_user_id: ${ctx.guardianExternalUserId ?? 'unknown'}`);
288
+ lines.push(`guardian_chat_id: ${ctx.guardianChatId ?? 'unknown'}`);
289
+ lines.push(`requester_identifier: ${ctx.requesterIdentifier ?? 'unknown'}`);
290
+ lines.push(`requester_external_user_id: ${ctx.requesterExternalUserId ?? 'unknown'}`);
291
+ lines.push(`requester_chat_id: ${ctx.requesterChatId ?? 'unknown'}`);
292
+ lines.push(`denial_reason: ${ctx.denialReason ?? 'none'}`);
293
+ lines.push('</guardian_context>');
294
+
295
+ const block = lines.join('\n');
296
+ return {
297
+ ...message,
298
+ content: [
299
+ { type: 'text', text: block },
300
+ ...message.content,
301
+ ],
302
+ };
303
+ }
304
+
267
305
  // ---------------------------------------------------------------------------
268
306
  // Prefix-based stripping primitive
269
307
  // ---------------------------------------------------------------------------
@@ -298,6 +336,11 @@ export function stripChannelCapabilityContext(messages: Message[]): Message[] {
298
336
  return stripUserTextBlocksByPrefix(messages, ['<channel_capabilities>']);
299
337
  }
300
338
 
339
+ /** Strip `<guardian_context>` blocks injected by `injectGuardianContext`. */
340
+ export function stripGuardianContext(messages: Message[]): Message[] {
341
+ return stripUserTextBlocksByPrefix(messages, ['<guardian_context>']);
342
+ }
343
+
301
344
  /**
302
345
  * Prepend workspace top-level directory context to a user message.
303
346
  */
@@ -358,6 +401,7 @@ export function stripActiveSurfaceContext(messages: Message[]): Message[] {
358
401
  /** Prefixes stripped by the pipeline (order doesn't matter — single pass). */
359
402
  const RUNTIME_INJECTION_PREFIXES = [
360
403
  '<channel_capabilities>',
404
+ '<guardian_context>',
361
405
  '<workspace_top_level>',
362
406
  TEMPORAL_INJECTED_PREFIX,
363
407
  '<active_workspace>',
@@ -398,6 +442,7 @@ export function applyRuntimeInjections(
398
442
  activeSurface?: ActiveSurfaceContext | null;
399
443
  workspaceTopLevelContext?: string | null;
400
444
  channelCapabilities?: ChannelCapabilities | null;
445
+ guardianContext?: GuardianRuntimeContext | null;
401
446
  temporalContext?: string | null;
402
447
  },
403
448
  ): Message[] {
@@ -433,6 +478,16 @@ export function applyRuntimeInjections(
433
478
  }
434
479
  }
435
480
 
481
+ if (options.guardianContext) {
482
+ const userTail = result[result.length - 1];
483
+ if (userTail && userTail.role === 'user') {
484
+ result = [
485
+ ...result.slice(0, -1),
486
+ injectGuardianContext(userTail, options.guardianContext),
487
+ ];
488
+ }
489
+ }
490
+
436
491
  // Temporal context is injected before workspace top-level so it
437
492
  // appears after workspace context in the final message content
438
493
  // (both are prepended, so later injections appear first).
@@ -41,6 +41,7 @@ import { projectSkillTools, type SkillProjectionCache } from './session-skill-to
41
41
  */
42
42
  export interface ToolSetupContext extends SurfaceSessionContext {
43
43
  readonly conversationId: string;
44
+ assistantId?: string;
44
45
  currentRequestId?: string;
45
46
  workingDir: string;
46
47
  sandboxOverride?: boolean;
@@ -186,6 +187,7 @@ export function createToolExecutor(
186
187
  workingDir: ctx.workingDir,
187
188
  sessionId: ctx.conversationId,
188
189
  conversationId: ctx.conversationId,
190
+ assistantId: ctx.assistantId,
189
191
  requestId: ctx.currentRequestId,
190
192
  taskRunId: ctx.taskRunId,
191
193
  onOutput,
@@ -38,7 +38,7 @@ import { getHookManager } from '../hooks/manager.js';
38
38
  import { ConflictGate } from './session-conflict-gate.js';
39
39
  import { MessageQueue } from './session-queue-manager.js';
40
40
  import type { QueueDrainReason } from './session-queue-manager.js';
41
- import type { ChannelCapabilities } from './session-runtime-assembly.js';
41
+ import type { ChannelCapabilities, GuardianRuntimeContext } from './session-runtime-assembly.js';
42
42
  import type { AssistantAttachmentDraft } from './assistant-attachments.js';
43
43
  import {
44
44
  handleSurfaceAction as handleSurfaceActionImpl,
@@ -127,6 +127,8 @@ export class Session {
127
127
  /** @internal */ currentActiveSurfaceId?: string;
128
128
  /** @internal */ currentPage?: string;
129
129
  /** @internal */ channelCapabilities?: ChannelCapabilities;
130
+ /** @internal */ guardianContext?: GuardianRuntimeContext;
131
+ /** @internal */ assistantId?: string;
130
132
  /** @internal */ pendingSurfaceActions = new Map<string, { surfaceType: SurfaceType }>();
131
133
  /** @internal */ lastSurfaceAction = new Map<string, { actionId: string; data?: Record<string, unknown> }>();
132
134
  /** @internal */ surfaceState = new Map<string, { surfaceType: SurfaceType; data: SurfaceData }>();
@@ -327,6 +329,14 @@ export class Session {
327
329
  this.channelCapabilities = caps ?? undefined;
328
330
  }
329
331
 
332
+ setGuardianContext(ctx: GuardianRuntimeContext | null): void {
333
+ this.guardianContext = ctx ?? undefined;
334
+ }
335
+
336
+ setAssistantId(assistantId: string | null): void {
337
+ this.assistantId = assistantId ?? undefined;
338
+ }
339
+
330
340
  persistUserMessage(
331
341
  content: string,
332
342
  attachments: UserMessageAttachment[],
@@ -5,8 +5,8 @@
5
5
  *
6
6
  * The canonical public base URL is resolved through a two-level chain:
7
7
  *
8
- * 1. **User Settings** (`config.ingress.publicBaseUrl`) — set via the
9
- * Settings UI or `config set ingress.publicBaseUrl`. This is the
8
+ * 1. **User Settings** (`config.ingress.publicBaseUrl`) — set via
9
+ * the in-chat config flow, the Settings UI, or `config set ingress.publicBaseUrl`. This is the
10
10
  * primary source of truth. When the assistant spawns or restarts
11
11
  * the gateway, this value is forwarded as the `INGRESS_PUBLIC_BASE_URL`
12
12
  * environment variable so both processes agree on the same URL.
@@ -51,7 +51,7 @@ function normalizeUrl(url: string): string {
51
51
  export function getPublicBaseUrl(config: IngressConfig): string {
52
52
  if (config.ingress?.enabled === false) {
53
53
  throw new Error(
54
- 'Public ingress is disabled. Enable it in Settings to use public-facing webhooks.',
54
+ 'Public ingress is disabled. Ask the assistant to enable it, or update it from the Settings page.',
55
55
  );
56
56
  }
57
57
 
@@ -142,6 +142,7 @@ export function createBinding(params: {
142
142
  guardianExternalUserId: string;
143
143
  guardianDeliveryChatId: string;
144
144
  verifiedVia?: string;
145
+ metadataJson?: string | null;
145
146
  }): GuardianBinding {
146
147
  const db = getDb();
147
148
  const now = Date.now();
@@ -156,7 +157,7 @@ export function createBinding(params: {
156
157
  status: 'active' as const,
157
158
  verifiedAt: now,
158
159
  verifiedVia: params.verifiedVia ?? 'challenge',
159
- metadataJson: null,
160
+ metadataJson: params.metadataJson ?? null,
160
161
  createdAt: now,
161
162
  updatedAt: now,
162
163
  };
@@ -1015,5 +1015,149 @@ export function initializeDb(): void {
1015
1015
 
1016
1016
  database.run(/*sql*/ `CREATE UNIQUE INDEX IF NOT EXISTS idx_channel_guardian_rate_limits_actor ON channel_guardian_rate_limits(assistant_id, channel, actor_external_user_id, actor_chat_id)`);
1017
1017
 
1018
+ // ── Media Assets ───────────────────────────────────────────────────
1019
+
1020
+ database.run(/*sql*/ `
1021
+ CREATE TABLE IF NOT EXISTS media_assets (
1022
+ id TEXT PRIMARY KEY,
1023
+ title TEXT NOT NULL,
1024
+ file_path TEXT NOT NULL,
1025
+ mime_type TEXT NOT NULL,
1026
+ duration_seconds REAL,
1027
+ file_hash TEXT NOT NULL,
1028
+ status TEXT NOT NULL DEFAULT 'registered',
1029
+ media_type TEXT NOT NULL,
1030
+ metadata TEXT,
1031
+ created_at INTEGER NOT NULL,
1032
+ updated_at INTEGER NOT NULL
1033
+ )
1034
+ `);
1035
+
1036
+ // Drop the old non-unique index so it can be recreated as UNIQUE (migration for existing databases)
1037
+ database.run(/*sql*/ `DROP INDEX IF EXISTS idx_media_assets_file_hash`);
1038
+ database.run(/*sql*/ `CREATE UNIQUE INDEX IF NOT EXISTS idx_media_assets_file_hash ON media_assets(file_hash)`);
1039
+ database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_media_assets_status ON media_assets(status)`);
1040
+
1041
+ database.run(/*sql*/ `
1042
+ CREATE TABLE IF NOT EXISTS processing_stages (
1043
+ id TEXT PRIMARY KEY,
1044
+ asset_id TEXT NOT NULL REFERENCES media_assets(id) ON DELETE CASCADE,
1045
+ stage TEXT NOT NULL,
1046
+ status TEXT NOT NULL DEFAULT 'pending',
1047
+ progress INTEGER NOT NULL DEFAULT 0,
1048
+ last_error TEXT,
1049
+ started_at INTEGER,
1050
+ completed_at INTEGER
1051
+ )
1052
+ `);
1053
+
1054
+ database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_processing_stages_asset_id ON processing_stages(asset_id)`);
1055
+
1056
+ // ── Media Keyframes ─────────────────────────────────────────────────
1057
+
1058
+ database.run(/*sql*/ `
1059
+ CREATE TABLE IF NOT EXISTS media_keyframes (
1060
+ id TEXT PRIMARY KEY,
1061
+ asset_id TEXT NOT NULL REFERENCES media_assets(id) ON DELETE CASCADE,
1062
+ timestamp REAL NOT NULL,
1063
+ file_path TEXT NOT NULL,
1064
+ metadata TEXT,
1065
+ created_at INTEGER NOT NULL
1066
+ )
1067
+ `);
1068
+
1069
+ database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_media_keyframes_asset_id ON media_keyframes(asset_id)`);
1070
+ database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_media_keyframes_asset_timestamp ON media_keyframes(asset_id, timestamp)`);
1071
+
1072
+ // ── Media Vision Outputs ────────────────────────────────────────────
1073
+
1074
+ database.run(/*sql*/ `
1075
+ CREATE TABLE IF NOT EXISTS media_vision_outputs (
1076
+ id TEXT PRIMARY KEY,
1077
+ asset_id TEXT NOT NULL REFERENCES media_assets(id) ON DELETE CASCADE,
1078
+ keyframe_id TEXT NOT NULL REFERENCES media_keyframes(id) ON DELETE CASCADE,
1079
+ analysis_type TEXT NOT NULL,
1080
+ output TEXT NOT NULL,
1081
+ confidence REAL,
1082
+ created_at INTEGER NOT NULL
1083
+ )
1084
+ `);
1085
+
1086
+ database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_media_vision_outputs_asset_id ON media_vision_outputs(asset_id)`);
1087
+ database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_media_vision_outputs_keyframe_id ON media_vision_outputs(keyframe_id)`);
1088
+ database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_media_vision_outputs_asset_type ON media_vision_outputs(asset_id, analysis_type)`);
1089
+
1090
+ // ── Media Timelines ─────────────────────────────────────────────────
1091
+
1092
+ database.run(/*sql*/ `
1093
+ CREATE TABLE IF NOT EXISTS media_timelines (
1094
+ id TEXT PRIMARY KEY,
1095
+ asset_id TEXT NOT NULL REFERENCES media_assets(id) ON DELETE CASCADE,
1096
+ start_time REAL NOT NULL,
1097
+ end_time REAL NOT NULL,
1098
+ segment_type TEXT NOT NULL,
1099
+ attributes TEXT,
1100
+ confidence REAL,
1101
+ created_at INTEGER NOT NULL
1102
+ )
1103
+ `);
1104
+
1105
+ database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_media_timelines_asset_id ON media_timelines(asset_id)`);
1106
+ database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_media_timelines_asset_time ON media_timelines(asset_id, start_time)`);
1107
+
1108
+ // ── Media Events ──────────────────────────────────────────────────
1109
+
1110
+ database.run(/*sql*/ `
1111
+ CREATE TABLE IF NOT EXISTS media_events (
1112
+ id TEXT PRIMARY KEY,
1113
+ asset_id TEXT NOT NULL REFERENCES media_assets(id) ON DELETE CASCADE,
1114
+ event_type TEXT NOT NULL,
1115
+ start_time REAL NOT NULL,
1116
+ end_time REAL NOT NULL,
1117
+ confidence REAL NOT NULL,
1118
+ reasons TEXT NOT NULL,
1119
+ metadata TEXT,
1120
+ created_at INTEGER NOT NULL
1121
+ )
1122
+ `);
1123
+
1124
+ database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_media_events_asset_id ON media_events(asset_id)`);
1125
+ database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_media_events_asset_type ON media_events(asset_id, event_type)`);
1126
+ database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_media_events_confidence ON media_events(confidence DESC)`);
1127
+
1128
+ // ── Media Tracking Profiles ─────────────────────────────────────────
1129
+
1130
+ database.run(/*sql*/ `
1131
+ CREATE TABLE IF NOT EXISTS media_tracking_profiles (
1132
+ id TEXT PRIMARY KEY,
1133
+ asset_id TEXT NOT NULL REFERENCES media_assets(id) ON DELETE CASCADE,
1134
+ capabilities TEXT NOT NULL,
1135
+ created_at INTEGER NOT NULL
1136
+ )
1137
+ `);
1138
+
1139
+ database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_media_tracking_profiles_asset_id ON media_tracking_profiles(asset_id)`);
1140
+
1141
+ // ── Media Event Feedback ──────────────────────────────────────────
1142
+
1143
+ database.run(/*sql*/ `
1144
+ CREATE TABLE IF NOT EXISTS media_event_feedback (
1145
+ id TEXT PRIMARY KEY,
1146
+ asset_id TEXT NOT NULL REFERENCES media_assets(id) ON DELETE CASCADE,
1147
+ event_id TEXT NOT NULL REFERENCES media_events(id) ON DELETE CASCADE,
1148
+ feedback_type TEXT NOT NULL,
1149
+ original_start_time REAL,
1150
+ original_end_time REAL,
1151
+ corrected_start_time REAL,
1152
+ corrected_end_time REAL,
1153
+ notes TEXT,
1154
+ created_at INTEGER NOT NULL
1155
+ )
1156
+ `);
1157
+
1158
+ database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_media_event_feedback_asset_id ON media_event_feedback(asset_id)`);
1159
+ database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_media_event_feedback_event_id ON media_event_feedback(event_id)`);
1160
+ database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_media_event_feedback_type ON media_event_feedback(asset_id, feedback_type)`);
1161
+
1018
1162
  migrateMemoryFtsBackfill(database);
1019
1163
  }
@@ -0,0 +1,100 @@
1
+ import { getLogger } from '../../util/logger.js';
2
+ import { asString } from '../job-utils.js';
3
+ import { getMediaAssetById, updateMediaAssetStatus } from '../media-store.js';
4
+ import type { MemoryJob } from '../jobs-store.js';
5
+ import {
6
+ runPipeline,
7
+ type PipelineStageName,
8
+ type StageHandler,
9
+ } from '../../config/bundled-skills/media-processing/services/processing-pipeline.js';
10
+ import { extractKeyframesForAsset } from '../../config/bundled-skills/media-processing/tools/extract-keyframes.js';
11
+ import { analyzeKeyframesForAsset } from '../../config/bundled-skills/media-processing/tools/analyze-keyframes.js';
12
+ import { generateTimeline } from '../../config/bundled-skills/media-processing/services/timeline-service.js';
13
+ import {
14
+ detectEvents,
15
+ type DetectionConfig,
16
+ } from '../../config/bundled-skills/media-processing/services/event-detection-service.js';
17
+
18
+ const log = getLogger('media-processing-job');
19
+
20
+ const defaultDetectionConfig: DetectionConfig = {
21
+ eventType: 'scene_change',
22
+ rules: [
23
+ {
24
+ ruleType: 'segment_transition',
25
+ params: { field: 'segmentType' },
26
+ weight: 1.0,
27
+ },
28
+ ],
29
+ };
30
+
31
+ export async function mediaProcessingJob(job: MemoryJob): Promise<void> {
32
+ const mediaAssetId = asString(job.payload.mediaAssetId);
33
+ if (!mediaAssetId) {
34
+ log.warn({ jobId: job.id }, 'Missing mediaAssetId in job payload');
35
+ return;
36
+ }
37
+
38
+ const asset = getMediaAssetById(mediaAssetId);
39
+ if (!asset) {
40
+ log.warn({ jobId: job.id, mediaAssetId }, 'Media asset not found');
41
+ return;
42
+ }
43
+
44
+ if (asset.mediaType !== 'video') {
45
+ log.info(
46
+ { assetId: mediaAssetId, mediaType: asset.mediaType },
47
+ 'Skipping media processing pipeline — only video assets are supported',
48
+ );
49
+ updateMediaAssetStatus(mediaAssetId, 'indexed');
50
+ return;
51
+ }
52
+
53
+ // Build detection config, allowing optional eventType override from payload
54
+ const eventType = asString(job.payload.eventType);
55
+ const detectionConfig: DetectionConfig = eventType
56
+ ? { ...defaultDetectionConfig, eventType }
57
+ : defaultDetectionConfig;
58
+
59
+ const handlers: Record<PipelineStageName, StageHandler> = {
60
+ keyframe_extraction: {
61
+ execute: (assetId, onProgress) =>
62
+ extractKeyframesForAsset(assetId, 1, onProgress),
63
+ },
64
+ vision_analysis: {
65
+ execute: (assetId, onProgress) =>
66
+ analyzeKeyframesForAsset(assetId, undefined, undefined, onProgress),
67
+ },
68
+ timeline_generation: {
69
+ execute: async (assetId, onProgress) => {
70
+ generateTimeline(assetId, { onProgress });
71
+ },
72
+ },
73
+ event_detection: {
74
+ execute: async (assetId, onProgress) => {
75
+ detectEvents(assetId, detectionConfig, { onProgress });
76
+ },
77
+ },
78
+ };
79
+
80
+ const result = await runPipeline(mediaAssetId, handlers, {
81
+ onProgress: (msg) => log.info({ mediaAssetId }, msg),
82
+ });
83
+
84
+ log.info(
85
+ {
86
+ mediaAssetId,
87
+ completedStages: result.completedStages,
88
+ failedStage: result.failedStage,
89
+ cancelled: result.cancelled,
90
+ },
91
+ 'Media processing pipeline finished',
92
+ );
93
+
94
+ if (result.failedStage) {
95
+ throw new Error(`Media processing failed at stage ${result.failedStage}: ${result.failureReason}`);
96
+ }
97
+ if (result.cancelled) {
98
+ throw new Error(`Media processing cancelled for asset ${mediaAssetId}`);
99
+ }
100
+ }
@@ -20,7 +20,8 @@ export type MemoryJobType =
20
20
  | 'build_conversation_summary'
21
21
  | 'backfill'
22
22
  | 'rebuild_index'
23
- | 'delete_qdrant_vectors';
23
+ | 'delete_qdrant_vectors'
24
+ | 'media_processing';
24
25
 
25
26
  const EMBED_JOB_TYPES: MemoryJobType[] = ['embed_segment', 'embed_item', 'embed_summary'];
26
27
 
@@ -29,6 +29,7 @@ import { checkContradictionsJob, cleanupStaleSupersededItemsJob } from './job-ha
29
29
  import { buildConversationSummaryJob, buildGlobalSummaryJob } from './job-handlers/summarization.js';
30
30
  import { backfillJob, backfillEntityRelationsJob } from './job-handlers/backfill.js';
31
31
  import { rebuildIndexJob, deleteQdrantVectorsJob } from './job-handlers/index-maintenance.js';
32
+ import { mediaProcessingJob } from './job-handlers/media-processing.js';
32
33
 
33
34
  // Re-export public utilities consumed by tests and other modules
34
35
  export { currentWeekWindow } from './job-utils.js';
@@ -229,6 +230,9 @@ async function processJob(job: MemoryJob, config: AssistantConfig): Promise<void
229
230
  case 'delete_qdrant_vectors':
230
231
  await deleteQdrantVectorsJob(job);
231
232
  return;
233
+ case 'media_processing':
234
+ await mediaProcessingJob(job);
235
+ return;
232
236
  default:
233
237
  throw new Error(`Unknown memory job type: ${(job as { type: string }).type}`);
234
238
  }