@vellumai/assistant 0.5.2 → 0.5.4

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 (144) hide show
  1. package/ARCHITECTURE.md +109 -0
  2. package/docs/architecture/memory.md +105 -0
  3. package/docs/skills.md +100 -0
  4. package/package.json +1 -1
  5. package/src/__tests__/archive-recall.test.ts +560 -0
  6. package/src/__tests__/conversation-agent-loop-overflow.test.ts +7 -0
  7. package/src/__tests__/conversation-agent-loop.test.ts +7 -0
  8. package/src/__tests__/conversation-clear-safety.test.ts +259 -0
  9. package/src/__tests__/conversation-memory-dirty-tail.test.ts +150 -0
  10. package/src/__tests__/conversation-provider-retry-repair.test.ts +7 -0
  11. package/src/__tests__/conversation-switch-memory-reduction.test.ts +474 -0
  12. package/src/__tests__/conversation-wipe.test.ts +226 -0
  13. package/src/__tests__/db-memory-archive-migration.test.ts +372 -0
  14. package/src/__tests__/db-memory-brief-state-migration.test.ts +213 -0
  15. package/src/__tests__/db-memory-reducer-checkpoints.test.ts +273 -0
  16. package/src/__tests__/db-schedule-syntax-migration.test.ts +3 -0
  17. package/src/__tests__/inline-command-runner.test.ts +311 -0
  18. package/src/__tests__/inline-skill-authoring-guard.test.ts +220 -0
  19. package/src/__tests__/inline-skill-load-permissions.test.ts +435 -0
  20. package/src/__tests__/list-messages-attachments.test.ts +96 -0
  21. package/src/__tests__/memory-brief-open-loops.test.ts +530 -0
  22. package/src/__tests__/memory-brief-time.test.ts +285 -0
  23. package/src/__tests__/memory-brief-wrapper.test.ts +311 -0
  24. package/src/__tests__/memory-chunk-archive.test.ts +400 -0
  25. package/src/__tests__/memory-chunk-dual-write.test.ts +453 -0
  26. package/src/__tests__/memory-episode-archive.test.ts +370 -0
  27. package/src/__tests__/memory-episode-dual-write.test.ts +626 -0
  28. package/src/__tests__/memory-observation-archive.test.ts +375 -0
  29. package/src/__tests__/memory-observation-dual-write.test.ts +318 -0
  30. package/src/__tests__/memory-recall-quality.test.ts +2 -2
  31. package/src/__tests__/memory-reducer-job.test.ts +538 -0
  32. package/src/__tests__/memory-reducer-scheduling.test.ts +473 -0
  33. package/src/__tests__/memory-reducer-store.test.ts +728 -0
  34. package/src/__tests__/memory-reducer-types.test.ts +707 -0
  35. package/src/__tests__/memory-reducer.test.ts +704 -0
  36. package/src/__tests__/memory-regressions.test.ts +30 -8
  37. package/src/__tests__/memory-simplified-config.test.ts +281 -0
  38. package/src/__tests__/parse-identity-fields.test.ts +129 -0
  39. package/src/__tests__/simplified-memory-e2e.test.ts +666 -0
  40. package/src/__tests__/simplified-memory-runtime.test.ts +616 -0
  41. package/src/__tests__/skill-load-inline-command.test.ts +598 -0
  42. package/src/__tests__/skill-load-inline-includes.test.ts +644 -0
  43. package/src/__tests__/skills-inline-command-expansions.test.ts +301 -0
  44. package/src/__tests__/skills-transitive-hash.test.ts +333 -0
  45. package/src/__tests__/vellum-self-knowledge-inline-command.test.ts +320 -0
  46. package/src/__tests__/workspace-migration-backfill-installation-id.test.ts +4 -4
  47. package/src/cli/commands/conversations.ts +18 -0
  48. package/src/config/bundled-skills/app-builder/SKILL.md +8 -8
  49. package/src/config/bundled-skills/schedule/TOOLS.json +8 -0
  50. package/src/config/bundled-skills/skill-management/SKILL.md +1 -1
  51. package/src/config/bundled-skills/skill-management/TOOLS.json +2 -2
  52. package/src/config/feature-flag-registry.json +16 -0
  53. package/src/config/raw-config-utils.ts +28 -0
  54. package/src/config/schema.ts +12 -0
  55. package/src/config/schemas/memory-simplified.ts +101 -0
  56. package/src/config/schemas/memory.ts +4 -0
  57. package/src/config/skills.ts +50 -4
  58. package/src/daemon/conversation-agent-loop-handlers.ts +8 -3
  59. package/src/daemon/conversation-agent-loop.ts +71 -1
  60. package/src/daemon/conversation-lifecycle.ts +11 -1
  61. package/src/daemon/conversation-memory.ts +117 -0
  62. package/src/daemon/conversation-runtime-assembly.ts +3 -1
  63. package/src/daemon/conversation-surfaces.ts +31 -8
  64. package/src/daemon/conversation.ts +40 -23
  65. package/src/daemon/handlers/config-embeddings.ts +10 -2
  66. package/src/daemon/handlers/config-model.ts +0 -9
  67. package/src/daemon/handlers/conversations.ts +11 -0
  68. package/src/daemon/handlers/identity.ts +12 -1
  69. package/src/daemon/lifecycle.ts +52 -1
  70. package/src/daemon/message-types/conversations.ts +0 -1
  71. package/src/daemon/server.ts +1 -1
  72. package/src/followups/followup-store.ts +47 -1
  73. package/src/memory/archive-recall.ts +516 -0
  74. package/src/memory/archive-store.ts +400 -0
  75. package/src/memory/brief-formatting.ts +33 -0
  76. package/src/memory/brief-open-loops.ts +266 -0
  77. package/src/memory/brief-time.ts +162 -0
  78. package/src/memory/brief.ts +75 -0
  79. package/src/memory/conversation-crud.ts +455 -101
  80. package/src/memory/conversation-key-store.ts +33 -4
  81. package/src/memory/db-init.ts +16 -0
  82. package/src/memory/indexer.ts +106 -15
  83. package/src/memory/job-handlers/backfill-simplified-memory.ts +462 -0
  84. package/src/memory/job-handlers/conversation-starters.ts +9 -3
  85. package/src/memory/job-handlers/embedding.test.ts +1 -0
  86. package/src/memory/job-handlers/embedding.ts +83 -0
  87. package/src/memory/job-handlers/reduce-conversation-memory.ts +229 -0
  88. package/src/memory/job-utils.ts +1 -1
  89. package/src/memory/jobs-store.ts +8 -0
  90. package/src/memory/jobs-worker.ts +20 -0
  91. package/src/memory/migrations/036-normalize-phone-identities.ts +49 -14
  92. package/src/memory/migrations/135-backfill-contact-interaction-stats.ts +9 -1
  93. package/src/memory/migrations/141-rename-verification-table.ts +8 -0
  94. package/src/memory/migrations/142-rename-verification-session-id-column.ts +7 -2
  95. package/src/memory/migrations/174-rename-thread-starters-table.ts +8 -0
  96. package/src/memory/migrations/185-memory-brief-state.ts +52 -0
  97. package/src/memory/migrations/186-memory-archive.ts +109 -0
  98. package/src/memory/migrations/187-memory-reducer-checkpoints.ts +19 -0
  99. package/src/memory/migrations/188-schedule-quiet-flag.ts +13 -0
  100. package/src/memory/migrations/index.ts +4 -0
  101. package/src/memory/qdrant-client.ts +23 -4
  102. package/src/memory/reducer-scheduler.ts +242 -0
  103. package/src/memory/reducer-store.ts +271 -0
  104. package/src/memory/reducer-types.ts +106 -0
  105. package/src/memory/reducer.ts +467 -0
  106. package/src/memory/schema/conversations.ts +3 -0
  107. package/src/memory/schema/index.ts +2 -0
  108. package/src/memory/schema/infrastructure.ts +1 -0
  109. package/src/memory/schema/memory-archive.ts +121 -0
  110. package/src/memory/schema/memory-brief.ts +55 -0
  111. package/src/memory/search/semantic.ts +17 -4
  112. package/src/oauth/oauth-store.ts +3 -1
  113. package/src/permissions/checker.ts +89 -6
  114. package/src/permissions/defaults.ts +14 -0
  115. package/src/runtime/auth/route-policy.ts +10 -1
  116. package/src/runtime/routes/conversation-management-routes.ts +94 -2
  117. package/src/runtime/routes/conversation-query-routes.ts +7 -0
  118. package/src/runtime/routes/conversation-routes.ts +52 -5
  119. package/src/runtime/routes/guardian-bootstrap-routes.ts +19 -7
  120. package/src/runtime/routes/identity-routes.ts +2 -35
  121. package/src/runtime/routes/llm-context-normalization.ts +14 -1
  122. package/src/runtime/routes/memory-item-routes.ts +90 -5
  123. package/src/runtime/routes/secret-routes.ts +3 -0
  124. package/src/runtime/routes/surface-action-routes.ts +68 -1
  125. package/src/schedule/schedule-store.ts +28 -0
  126. package/src/schedule/scheduler.ts +6 -2
  127. package/src/skills/inline-command-expansions.ts +204 -0
  128. package/src/skills/inline-command-render.ts +127 -0
  129. package/src/skills/inline-command-runner.ts +242 -0
  130. package/src/skills/transitive-version-hash.ts +88 -0
  131. package/src/tasks/task-store.ts +43 -1
  132. package/src/telemetry/usage-telemetry-reporter.ts +1 -1
  133. package/src/tools/filesystem/edit.ts +6 -1
  134. package/src/tools/filesystem/read.ts +6 -1
  135. package/src/tools/filesystem/write.ts +6 -1
  136. package/src/tools/memory/handlers.ts +129 -1
  137. package/src/tools/permission-checker.ts +8 -1
  138. package/src/tools/schedule/create.ts +3 -0
  139. package/src/tools/schedule/list.ts +5 -1
  140. package/src/tools/schedule/update.ts +6 -0
  141. package/src/tools/skills/load.ts +140 -6
  142. package/src/util/platform.ts +18 -0
  143. package/src/workspace/migrations/{002-backfill-installation-id.ts → 011-backfill-installation-id.ts} +1 -1
  144. package/src/workspace/migrations/registry.ts +1 -1
@@ -134,6 +134,7 @@ export function getOrCreateConversation(
134
134
  opts?: { conversationType?: "standard" | "private" },
135
135
  ): {
136
136
  conversationId: string;
137
+ conversationType: string;
137
138
  created: boolean;
138
139
  } {
139
140
  const db = getDb();
@@ -147,7 +148,16 @@ export function getOrCreateConversation(
147
148
  .get();
148
149
 
149
150
  if (existing) {
150
- return { conversationId: existing.conversationId, created: false as const };
151
+ const conv = tx
152
+ .select({ conversationType: conversations.conversationType })
153
+ .from(conversations)
154
+ .where(eq(conversations.id, existing.conversationId))
155
+ .get();
156
+ return {
157
+ conversationId: existing.conversationId,
158
+ conversationType: conv?.conversationType ?? "standard",
159
+ created: false as const,
160
+ };
151
161
  }
152
162
 
153
163
  // Check if the conversationKey itself is an existing conversation ID.
@@ -168,7 +178,16 @@ export function getOrCreateConversation(
168
178
  createdAt: Date.now(),
169
179
  })
170
180
  .run();
171
- return { conversationId: existingConversation.id, created: false as const };
181
+ const conv = tx
182
+ .select({ conversationType: conversations.conversationType })
183
+ .from(conversations)
184
+ .where(eq(conversations.id, existingConversation.id))
185
+ .get();
186
+ return {
187
+ conversationId: existingConversation.id,
188
+ conversationType: conv?.conversationType ?? "standard",
189
+ created: false as const,
190
+ };
172
191
  }
173
192
 
174
193
  const now = Date.now();
@@ -205,8 +224,14 @@ export function getOrCreateConversation(
205
224
 
206
225
  return {
207
226
  conversationId,
227
+ conversationType,
208
228
  created: true as const,
209
- conversation: { id: conversationId, title, createdAt: now, conversationType },
229
+ conversation: {
230
+ id: conversationId,
231
+ title,
232
+ createdAt: now,
233
+ conversationType,
234
+ },
210
235
  };
211
236
  });
212
237
 
@@ -214,5 +239,9 @@ export function getOrCreateConversation(
214
239
  initConversationDir({ ...result.conversation, originChannel: null });
215
240
  }
216
241
 
217
- return { conversationId: result.conversationId, created: result.created };
242
+ return {
243
+ conversationId: result.conversationId,
244
+ conversationType: result.conversationType,
245
+ created: result.created,
246
+ };
218
247
  }
@@ -82,7 +82,10 @@ import {
82
82
  migrateInviteContactId,
83
83
  migrateLlmRequestLogMessageId,
84
84
  migrateLlmRequestLogProvider,
85
+ migrateMemoryArchiveTables,
86
+ migrateMemoryBriefState,
85
87
  migrateMemoryItemSupersession,
88
+ migrateMemoryReducerCheckpoints,
86
89
  migrateMessagesFtsBackfill,
87
90
  migrateNormalizePhoneIdentities,
88
91
  migrateNotificationDeliveryThreadDecision,
@@ -108,6 +111,7 @@ import {
108
111
  migrateRenameVerificationTable,
109
112
  migrateRenameVoiceToPhone,
110
113
  migrateScheduleOneShotRouting,
114
+ migrateScheduleQuietFlag,
111
115
  migrateSchemaIndexesAndColumns,
112
116
  migrateUsageDashboardIndexes,
113
117
  migrateVoiceInviteColumns,
@@ -484,6 +488,18 @@ export function initializeDb(): void {
484
488
  // 84. Add nullable conversation fork lineage columns and parent lookup index
485
489
  migrateConversationForkLineage(database);
486
490
 
491
+ // 85. Memory brief state tables (time_contexts, open_loops) for simplified memory system
492
+ migrateMemoryBriefState(database);
493
+
494
+ // 86. Memory archive tables (observations, chunks, episodes) for simplified memory v1
495
+ migrateMemoryArchiveTables(database);
496
+
497
+ // 87. Add memory reducer checkpoint columns to conversations
498
+ migrateMemoryReducerCheckpoints(database);
499
+
500
+ // 88. Add quiet flag to schedule jobs
501
+ migrateScheduleQuietFlag(database);
502
+
487
503
  validateMigrationState(database);
488
504
 
489
505
  if (process.env.BUN_TEST === "1") {
@@ -5,6 +5,7 @@ import { getConfig } from "../config/loader.js";
5
5
  import type { MemoryConfig } from "../config/types.js";
6
6
  import type { TrustClass } from "../runtime/actor-trust-resolver.js";
7
7
  import { getLogger } from "../util/logger.js";
8
+ import { computeChunkContentHash } from "./archive-store.js";
8
9
  import { getDb } from "./db.js";
9
10
  import { selectedBackendSupportsMultimodal } from "./embedding-backend.js";
10
11
  import { enqueueMemoryJob } from "./jobs-store.js";
@@ -12,7 +13,7 @@ import {
12
13
  extractMediaBlockMeta,
13
14
  extractTextFromStoredMessageContent,
14
15
  } from "./message-content.js";
15
- import { memorySegments } from "./schema.js";
16
+ import { memoryChunks, memoryObservations, memorySegments } from "./schema.js";
16
17
  import { segmentText } from "./segmenter.js";
17
18
 
18
19
  const log = getLogger("memory-indexer");
@@ -53,7 +54,12 @@ export async function indexMessageNow(
53
54
  input.provenanceTrustClass === undefined;
54
55
 
55
56
  const text = extractTextFromStoredMessageContent(input.content);
56
- if (text.length === 0) {
57
+ const hasText = text.length > 0;
58
+ const candidateMediaMeta = extractMediaBlockMeta(input.content).filter(
59
+ (b) => b.type === "image",
60
+ );
61
+ const hasMedia = candidateMediaMeta.length > 0;
62
+ if (!hasText && !hasMedia) {
57
63
  enqueueMemoryJob("build_conversation_summary", {
58
64
  conversationId: input.conversationId,
59
65
  });
@@ -62,11 +68,13 @@ export async function indexMessageNow(
62
68
 
63
69
  const db = getDb();
64
70
  const now = Date.now();
65
- const segments = segmentText(
66
- text,
67
- config.segmentation.targetTokens,
68
- config.segmentation.overlapTokens,
69
- );
71
+ const segments = hasText
72
+ ? segmentText(
73
+ text,
74
+ config.segmentation.targetTokens,
75
+ config.segmentation.overlapTokens,
76
+ )
77
+ : [];
70
78
  const shouldExtract =
71
79
  input.role === "user" ||
72
80
  (input.role === "assistant" && config.extraction.extractFromAssistant);
@@ -76,9 +84,6 @@ export async function indexMessageNow(
76
84
  // overhead for messages on non-multimodal backends.
77
85
  // selectedBackendSupportsMultimodal requires async key resolution, so we
78
86
  // skip it entirely for text-only messages.
79
- const candidateMediaMeta = extractMediaBlockMeta(input.content).filter(
80
- (b) => b.type === "image",
81
- );
82
87
  const mediaBlocks =
83
88
  candidateMediaMeta.length > 0 &&
84
89
  (await selectedBackendSupportsMultimodal(getConfig()))
@@ -88,7 +93,10 @@ export async function indexMessageNow(
88
93
  // Wrap all segment inserts and job enqueues in a single transaction so they
89
94
  // either all succeed or all roll back, preventing partial/orphaned state.
90
95
  let skippedEmbedJobs = 0;
96
+ let skippedChunkEmbedJobs = 0;
97
+ const scopeId = input.scopeId ?? "default";
91
98
  db.transaction((tx) => {
99
+ // ── Legacy segment path (kept intact for parallel validation) ───
92
100
  for (const segment of segments) {
93
101
  const segmentId = buildSegmentId(input.messageId, segment.segmentIndex);
94
102
  const hash = createHash("sha256").update(segment.text).digest("hex");
@@ -109,7 +117,7 @@ export async function indexMessageNow(
109
117
  segmentIndex: segment.segmentIndex,
110
118
  text: segment.text,
111
119
  tokenEstimate: segment.tokenEstimate,
112
- scopeId: input.scopeId ?? "default",
120
+ scopeId,
113
121
  contentHash: hash,
114
122
  createdAt: input.createdAt,
115
123
  updatedAt: now,
@@ -119,7 +127,7 @@ export async function indexMessageNow(
119
127
  set: {
120
128
  text: segment.text,
121
129
  tokenEstimate: segment.tokenEstimate,
122
- scopeId: input.scopeId ?? "default",
130
+ scopeId,
123
131
  contentHash: hash,
124
132
  updatedAt: now,
125
133
  },
@@ -133,6 +141,65 @@ export async function indexMessageNow(
133
141
  }
134
142
  }
135
143
 
144
+ // ── Archive chunk dual-write (mirrors segment boundaries) ──────
145
+ // Create a single observation per message, then create one chunk per
146
+ // segment using the same segmentation boundaries. Chunks are
147
+ // deduplicated by (scopeId, contentHash) via onConflictDoNothing so
148
+ // unchanged content does not enqueue duplicate embed_chunk jobs.
149
+ const observationId = buildObservationId(input.messageId);
150
+ tx.insert(memoryObservations)
151
+ .values({
152
+ id: observationId,
153
+ scopeId,
154
+ conversationId: input.conversationId,
155
+ messageId: input.messageId,
156
+ role: input.role,
157
+ content: hasText ? text : input.content,
158
+ modality: hasMedia ? "multimodal" : "text",
159
+ source: null,
160
+ createdAt: input.createdAt,
161
+ })
162
+ .onConflictDoNothing({ target: memoryObservations.id })
163
+ .run();
164
+
165
+ for (const segment of segments) {
166
+ const chunkId = buildChunkId(input.messageId, segment.segmentIndex);
167
+ const chunkHash = computeChunkContentHash(scopeId, segment.text);
168
+
169
+ // Check if this chunk already exists with the same content hash
170
+ const existingChunk = tx
171
+ .select({ contentHash: memoryChunks.contentHash })
172
+ .from(memoryChunks)
173
+ .where(eq(memoryChunks.id, chunkId))
174
+ .get();
175
+
176
+ tx.insert(memoryChunks)
177
+ .values({
178
+ id: chunkId,
179
+ scopeId,
180
+ observationId,
181
+ content: segment.text,
182
+ tokenEstimate: segment.tokenEstimate,
183
+ contentHash: chunkHash,
184
+ createdAt: input.createdAt,
185
+ })
186
+ .onConflictDoUpdate({
187
+ target: memoryChunks.id,
188
+ set: {
189
+ content: segment.text,
190
+ tokenEstimate: segment.tokenEstimate,
191
+ contentHash: chunkHash,
192
+ },
193
+ })
194
+ .run();
195
+
196
+ if (existingChunk?.contentHash === chunkHash) {
197
+ skippedChunkEmbedJobs++;
198
+ } else {
199
+ enqueueMemoryJob("embed_chunk", { chunkId, scopeId }, Date.now(), tx);
200
+ }
201
+ }
202
+
136
203
  // Enqueue embed_attachment jobs for image content blocks when the
137
204
  // embedding provider supports multimodal (Gemini only).
138
205
  for (const block of mediaBlocks) {
@@ -147,7 +214,7 @@ export async function indexMessageNow(
147
214
  if (shouldExtract && isTrustedActor && !input.automated) {
148
215
  enqueueMemoryJob(
149
216
  "extract_items",
150
- { messageId: input.messageId, scopeId: input.scopeId ?? "default" },
217
+ { messageId: input.messageId, scopeId },
151
218
  Date.now(),
152
219
  tx,
153
220
  );
@@ -166,6 +233,12 @@ export async function indexMessageNow(
166
233
  );
167
234
  }
168
235
 
236
+ if (skippedChunkEmbedJobs > 0) {
237
+ log.debug(
238
+ `Skipped ${skippedChunkEmbedJobs}/${segments.length} embed_chunk jobs (content unchanged)`,
239
+ );
240
+ }
241
+
169
242
  if (!isTrustedActor && shouldExtract) {
170
243
  log.info(
171
244
  `Skipping extraction jobs for untrusted actor (trustClass=${input.provenanceTrustClass})`,
@@ -177,9 +250,11 @@ export async function indexMessageNow(
177
250
  }
178
251
 
179
252
  const extractionGated = !isTrustedActor || !!input.automated;
253
+ const segmentEmbedJobs = segments.length - skippedEmbedJobs;
254
+ const chunkEmbedJobs = segments.length - skippedChunkEmbedJobs;
180
255
  const enqueuedJobs =
181
- segments.length -
182
- skippedEmbedJobs +
256
+ segmentEmbedJobs +
257
+ chunkEmbedJobs +
183
258
  mediaBlocks.length +
184
259
  (shouldExtract && !extractionGated ? 2 : 1);
185
260
  return {
@@ -213,3 +288,19 @@ export function getRecentSegmentsForConversation(
213
288
  function buildSegmentId(messageId: string, segmentIndex: number): string {
214
289
  return `${messageId}:${segmentIndex}`;
215
290
  }
291
+
292
+ /**
293
+ * Deterministic observation ID derived from the messageId so repeated
294
+ * indexer runs for the same message converge on the same observation row.
295
+ */
296
+ function buildObservationId(messageId: string): string {
297
+ return `obs:${messageId}`;
298
+ }
299
+
300
+ /**
301
+ * Deterministic chunk ID derived from the messageId and segment index so
302
+ * the dual-write path mirrors the legacy segment identity scheme exactly.
303
+ */
304
+ function buildChunkId(messageId: string, segmentIndex: number): string {
305
+ return `chunk:${messageId}:${segmentIndex}`;
306
+ }