@vellumai/assistant 0.5.2 → 0.5.3
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.
- package/ARCHITECTURE.md +109 -0
- package/docs/skills.md +100 -0
- package/package.json +1 -1
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +7 -0
- package/src/__tests__/conversation-agent-loop.test.ts +7 -0
- package/src/__tests__/conversation-memory-dirty-tail.test.ts +150 -0
- package/src/__tests__/conversation-provider-retry-repair.test.ts +7 -0
- package/src/__tests__/conversation-wipe.test.ts +226 -0
- package/src/__tests__/db-memory-archive-migration.test.ts +372 -0
- package/src/__tests__/db-memory-brief-state-migration.test.ts +213 -0
- package/src/__tests__/db-memory-reducer-checkpoints.test.ts +273 -0
- package/src/__tests__/inline-command-runner.test.ts +311 -0
- package/src/__tests__/inline-skill-authoring-guard.test.ts +220 -0
- package/src/__tests__/inline-skill-load-permissions.test.ts +435 -0
- package/src/__tests__/list-messages-attachments.test.ts +96 -0
- package/src/__tests__/memory-brief-open-loops.test.ts +530 -0
- package/src/__tests__/memory-brief-time.test.ts +285 -0
- package/src/__tests__/memory-brief-wrapper.test.ts +311 -0
- package/src/__tests__/memory-chunk-archive.test.ts +400 -0
- package/src/__tests__/memory-chunk-dual-write.test.ts +453 -0
- package/src/__tests__/memory-episode-archive.test.ts +370 -0
- package/src/__tests__/memory-episode-dual-write.test.ts +626 -0
- package/src/__tests__/memory-observation-archive.test.ts +375 -0
- package/src/__tests__/memory-observation-dual-write.test.ts +318 -0
- package/src/__tests__/memory-recall-quality.test.ts +2 -2
- package/src/__tests__/memory-reducer-store.test.ts +728 -0
- package/src/__tests__/memory-reducer-types.test.ts +699 -0
- package/src/__tests__/memory-reducer.test.ts +698 -0
- package/src/__tests__/memory-regressions.test.ts +6 -4
- package/src/__tests__/memory-simplified-config.test.ts +281 -0
- package/src/__tests__/parse-identity-fields.test.ts +129 -0
- package/src/__tests__/skill-load-inline-command.test.ts +598 -0
- package/src/__tests__/skill-load-inline-includes.test.ts +644 -0
- package/src/__tests__/skills-inline-command-expansions.test.ts +301 -0
- package/src/__tests__/skills-transitive-hash.test.ts +333 -0
- package/src/__tests__/vellum-self-knowledge-inline-command.test.ts +320 -0
- package/src/__tests__/workspace-migration-backfill-installation-id.test.ts +4 -4
- package/src/config/bundled-skills/app-builder/SKILL.md +8 -8
- package/src/config/bundled-skills/skill-management/SKILL.md +1 -1
- package/src/config/bundled-skills/skill-management/TOOLS.json +2 -2
- package/src/config/feature-flag-registry.json +16 -0
- package/src/config/loader.ts +1 -0
- package/src/config/raw-config-utils.ts +28 -0
- package/src/config/schema.ts +12 -0
- package/src/config/schemas/memory-simplified.ts +101 -0
- package/src/config/schemas/memory.ts +4 -0
- package/src/config/skills.ts +50 -4
- package/src/daemon/conversation-agent-loop-handlers.ts +8 -3
- package/src/daemon/conversation-agent-loop.ts +71 -1
- package/src/daemon/conversation-lifecycle.ts +11 -1
- package/src/daemon/conversation-runtime-assembly.ts +2 -1
- package/src/daemon/conversation-surfaces.ts +31 -8
- package/src/daemon/conversation.ts +40 -23
- package/src/daemon/handlers/config-embeddings.ts +10 -2
- package/src/daemon/handlers/config-model.ts +0 -9
- package/src/daemon/handlers/identity.ts +12 -1
- package/src/daemon/lifecycle.ts +9 -1
- package/src/daemon/message-types/conversations.ts +0 -1
- package/src/daemon/server.ts +1 -1
- package/src/followups/followup-store.ts +47 -1
- package/src/memory/archive-store.ts +400 -0
- package/src/memory/brief-formatting.ts +33 -0
- package/src/memory/brief-open-loops.ts +266 -0
- package/src/memory/brief-time.ts +161 -0
- package/src/memory/brief.ts +75 -0
- package/src/memory/conversation-crud.ts +245 -101
- package/src/memory/db-init.ts +12 -0
- package/src/memory/indexer.ts +106 -15
- package/src/memory/job-handlers/embedding.test.ts +1 -0
- package/src/memory/job-handlers/embedding.ts +83 -0
- package/src/memory/job-utils.ts +1 -1
- package/src/memory/jobs-store.ts +6 -0
- package/src/memory/jobs-worker.ts +12 -0
- package/src/memory/migrations/185-memory-brief-state.ts +52 -0
- package/src/memory/migrations/186-memory-archive.ts +109 -0
- package/src/memory/migrations/187-memory-reducer-checkpoints.ts +19 -0
- package/src/memory/migrations/index.ts +3 -0
- package/src/memory/qdrant-client.ts +23 -4
- package/src/memory/reducer-store.ts +271 -0
- package/src/memory/reducer-types.ts +99 -0
- package/src/memory/reducer.ts +453 -0
- package/src/memory/schema/conversations.ts +3 -0
- package/src/memory/schema/index.ts +2 -0
- package/src/memory/schema/memory-archive.ts +121 -0
- package/src/memory/schema/memory-brief.ts +55 -0
- package/src/memory/search/semantic.ts +17 -4
- package/src/oauth/oauth-store.ts +3 -1
- package/src/permissions/checker.ts +89 -6
- package/src/permissions/defaults.ts +14 -0
- package/src/runtime/routes/conversation-management-routes.ts +6 -0
- package/src/runtime/routes/conversation-query-routes.ts +7 -0
- package/src/runtime/routes/conversation-routes.ts +52 -5
- package/src/runtime/routes/identity-routes.ts +2 -35
- package/src/runtime/routes/llm-context-normalization.ts +14 -1
- package/src/runtime/routes/memory-item-routes.ts +90 -5
- package/src/runtime/routes/secret-routes.ts +2 -0
- package/src/runtime/routes/surface-action-routes.ts +68 -1
- package/src/schedule/schedule-store.ts +21 -0
- package/src/skills/inline-command-expansions.ts +204 -0
- package/src/skills/inline-command-render.ts +127 -0
- package/src/skills/inline-command-runner.ts +242 -0
- package/src/skills/transitive-version-hash.ts +88 -0
- package/src/tasks/task-store.ts +43 -1
- package/src/tools/permission-checker.ts +8 -1
- package/src/tools/skills/load.ts +140 -6
- package/src/util/platform.ts +18 -0
- package/src/workspace/migrations/{002-backfill-installation-id.ts → 011-backfill-installation-id.ts} +1 -1
- package/src/workspace/migrations/registry.ts +1 -1
package/src/memory/indexer.ts
CHANGED
|
@@ -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
|
-
|
|
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 =
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
182
|
-
|
|
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
|
+
}
|
|
@@ -11,7 +11,10 @@ import type { MemoryJob } from "../jobs-store.js";
|
|
|
11
11
|
import { extractMediaBlocks } from "../message-content.js";
|
|
12
12
|
import {
|
|
13
13
|
mediaAssets,
|
|
14
|
+
memoryChunks,
|
|
15
|
+
memoryEpisodes,
|
|
14
16
|
memoryItems,
|
|
17
|
+
memoryObservations,
|
|
15
18
|
memorySegments,
|
|
16
19
|
memorySummaries,
|
|
17
20
|
messages,
|
|
@@ -34,6 +37,7 @@ export async function embedSegmentJob(
|
|
|
34
37
|
conversation_id: segment.conversationId,
|
|
35
38
|
message_id: segment.messageId,
|
|
36
39
|
created_at: segment.createdAt,
|
|
40
|
+
memory_scope_id: segment.scopeId,
|
|
37
41
|
});
|
|
38
42
|
}
|
|
39
43
|
|
|
@@ -58,6 +62,7 @@ export async function embedItemJob(
|
|
|
58
62
|
confidence: item.confidence,
|
|
59
63
|
created_at: item.firstSeenAt,
|
|
60
64
|
last_seen_at: item.lastSeenAt,
|
|
65
|
+
memory_scope_id: item.scopeId,
|
|
61
66
|
});
|
|
62
67
|
}
|
|
63
68
|
|
|
@@ -83,10 +88,31 @@ export async function embedSummaryJob(
|
|
|
83
88
|
kind: summary.scope,
|
|
84
89
|
created_at: summary.startAt,
|
|
85
90
|
last_seen_at: summary.endAt,
|
|
91
|
+
memory_scope_id: summary.scopeId,
|
|
86
92
|
},
|
|
87
93
|
);
|
|
88
94
|
}
|
|
89
95
|
|
|
96
|
+
export async function embedChunkJob(
|
|
97
|
+
job: MemoryJob,
|
|
98
|
+
config: AssistantConfig,
|
|
99
|
+
): Promise<void> {
|
|
100
|
+
const chunkId = asString(job.payload.chunkId);
|
|
101
|
+
if (!chunkId) return;
|
|
102
|
+
const db = getDb();
|
|
103
|
+
const chunk = db
|
|
104
|
+
.select()
|
|
105
|
+
.from(memoryChunks)
|
|
106
|
+
.where(eq(memoryChunks.id, chunkId))
|
|
107
|
+
.get();
|
|
108
|
+
if (!chunk) return;
|
|
109
|
+
await embedAndUpsert(config, "chunk", chunk.id, chunk.content, {
|
|
110
|
+
observation_id: chunk.observationId,
|
|
111
|
+
created_at: chunk.createdAt,
|
|
112
|
+
memory_scope_id: chunk.scopeId,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
90
116
|
export async function embedMediaJob(
|
|
91
117
|
job: MemoryJob,
|
|
92
118
|
config: AssistantConfig,
|
|
@@ -116,6 +142,41 @@ export async function embedMediaJob(
|
|
|
116
142
|
created_at: asset.createdAt,
|
|
117
143
|
kind: asset.mediaType,
|
|
118
144
|
subject: asset.title,
|
|
145
|
+
memory_scope_id: "default",
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export async function embedObservationJob(
|
|
150
|
+
job: MemoryJob,
|
|
151
|
+
config: AssistantConfig,
|
|
152
|
+
): Promise<void> {
|
|
153
|
+
const observationId = asString(job.payload.observationId);
|
|
154
|
+
const chunkId = asString(job.payload.chunkId);
|
|
155
|
+
if (!observationId || !chunkId) return;
|
|
156
|
+
|
|
157
|
+
const db = getDb();
|
|
158
|
+
const observation = db
|
|
159
|
+
.select()
|
|
160
|
+
.from(memoryObservations)
|
|
161
|
+
.where(eq(memoryObservations.id, observationId))
|
|
162
|
+
.get();
|
|
163
|
+
if (!observation) return;
|
|
164
|
+
|
|
165
|
+
const chunk = db
|
|
166
|
+
.select()
|
|
167
|
+
.from(memoryChunks)
|
|
168
|
+
.where(eq(memoryChunks.id, chunkId))
|
|
169
|
+
.get();
|
|
170
|
+
if (!chunk) return;
|
|
171
|
+
|
|
172
|
+
await embedAndUpsert(config, "observation", chunk.id, chunk.content, {
|
|
173
|
+
observation_id: observationId,
|
|
174
|
+
conversation_id: observation.conversationId,
|
|
175
|
+
role: observation.role,
|
|
176
|
+
modality: observation.modality,
|
|
177
|
+
source: observation.source,
|
|
178
|
+
created_at: observation.createdAt,
|
|
179
|
+
memory_scope_id: observation.scopeId,
|
|
119
180
|
});
|
|
120
181
|
}
|
|
121
182
|
|
|
@@ -155,3 +216,25 @@ export async function embedAttachmentJob(
|
|
|
155
216
|
memory_scope_id: memoryScopeId,
|
|
156
217
|
});
|
|
157
218
|
}
|
|
219
|
+
|
|
220
|
+
export async function embedEpisodeJob(
|
|
221
|
+
job: MemoryJob,
|
|
222
|
+
config: AssistantConfig,
|
|
223
|
+
): Promise<void> {
|
|
224
|
+
const episodeId = asString(job.payload.episodeId);
|
|
225
|
+
if (!episodeId) return;
|
|
226
|
+
const db = getDb();
|
|
227
|
+
const episode = db
|
|
228
|
+
.select()
|
|
229
|
+
.from(memoryEpisodes)
|
|
230
|
+
.where(eq(memoryEpisodes.id, episodeId))
|
|
231
|
+
.get();
|
|
232
|
+
if (!episode) return;
|
|
233
|
+
const text = `[episode] ${episode.title}: ${episode.summary}`;
|
|
234
|
+
await embedAndUpsert(config, "episode", episode.id, text, {
|
|
235
|
+
conversation_id: episode.conversationId,
|
|
236
|
+
created_at: episode.startAt,
|
|
237
|
+
last_seen_at: episode.endAt,
|
|
238
|
+
memory_scope_id: episode.scopeId,
|
|
239
|
+
});
|
|
240
|
+
}
|
package/src/memory/job-utils.ts
CHANGED
|
@@ -142,7 +142,7 @@ export function truncate(text: string, max: number): string {
|
|
|
142
142
|
|
|
143
143
|
export async function embedAndUpsert(
|
|
144
144
|
config: AssistantConfig,
|
|
145
|
-
targetType: "segment" | "item" | "summary" | "media",
|
|
145
|
+
targetType: "segment" | "item" | "summary" | "observation" | "chunk" | "episode" | "media",
|
|
146
146
|
targetId: string,
|
|
147
147
|
input: EmbeddingInput,
|
|
148
148
|
extraPayload?: Record<string, unknown>,
|
package/src/memory/jobs-store.ts
CHANGED
|
@@ -12,6 +12,9 @@ export type MemoryJobType =
|
|
|
12
12
|
| "embed_segment"
|
|
13
13
|
| "embed_item"
|
|
14
14
|
| "embed_summary"
|
|
15
|
+
| "embed_chunk"
|
|
16
|
+
| "embed_episode"
|
|
17
|
+
| "embed_observation"
|
|
15
18
|
| "extract_items"
|
|
16
19
|
| "extract_entities"
|
|
17
20
|
| "cleanup_stale_superseded_items"
|
|
@@ -34,6 +37,9 @@ const EMBED_JOB_TYPES: MemoryJobType[] = [
|
|
|
34
37
|
"embed_segment",
|
|
35
38
|
"embed_item",
|
|
36
39
|
"embed_summary",
|
|
40
|
+
"embed_chunk",
|
|
41
|
+
"embed_episode",
|
|
42
|
+
"embed_observation",
|
|
37
43
|
"embed_media",
|
|
38
44
|
"embed_attachment",
|
|
39
45
|
];
|
|
@@ -11,8 +11,11 @@ import { generateConversationStartersJob } from "./job-handlers/conversation-sta
|
|
|
11
11
|
// ── Per-job-type handlers ──────────────────────────────────────────
|
|
12
12
|
import {
|
|
13
13
|
embedAttachmentJob,
|
|
14
|
+
embedChunkJob,
|
|
15
|
+
embedEpisodeJob,
|
|
14
16
|
embedItemJob,
|
|
15
17
|
embedMediaJob,
|
|
18
|
+
embedObservationJob,
|
|
16
19
|
embedSegmentJob,
|
|
17
20
|
embedSummaryJob,
|
|
18
21
|
} from "./job-handlers/embedding.js";
|
|
@@ -267,6 +270,15 @@ async function processJob(
|
|
|
267
270
|
case "embed_summary":
|
|
268
271
|
await embedSummaryJob(job, config);
|
|
269
272
|
return;
|
|
273
|
+
case "embed_chunk":
|
|
274
|
+
await embedChunkJob(job, config);
|
|
275
|
+
return;
|
|
276
|
+
case "embed_episode":
|
|
277
|
+
await embedEpisodeJob(job, config);
|
|
278
|
+
return;
|
|
279
|
+
case "embed_observation":
|
|
280
|
+
await embedObservationJob(job, config);
|
|
281
|
+
return;
|
|
270
282
|
case "extract_items":
|
|
271
283
|
await extractItemsJob(job);
|
|
272
284
|
return;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { DrizzleDb } from "../db-connection.js";
|
|
2
|
+
import { getSqliteFrom } from "../db-connection.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Create the memory brief state tables: time_contexts and open_loops.
|
|
6
|
+
*
|
|
7
|
+
* Both tables use CREATE TABLE IF NOT EXISTS and CREATE INDEX IF NOT EXISTS,
|
|
8
|
+
* making this migration inherently idempotent — safe to re-run on every startup
|
|
9
|
+
* without a checkpoint guard.
|
|
10
|
+
*/
|
|
11
|
+
export function migrateMemoryBriefState(database: DrizzleDb): void {
|
|
12
|
+
const raw = getSqliteFrom(database);
|
|
13
|
+
|
|
14
|
+
// -- time_contexts: bounded temporal windows for the brief --
|
|
15
|
+
raw.exec(/*sql*/ `
|
|
16
|
+
CREATE TABLE IF NOT EXISTS time_contexts (
|
|
17
|
+
id TEXT PRIMARY KEY,
|
|
18
|
+
scope_id TEXT NOT NULL,
|
|
19
|
+
summary TEXT NOT NULL,
|
|
20
|
+
source TEXT NOT NULL,
|
|
21
|
+
active_from INTEGER NOT NULL,
|
|
22
|
+
active_until INTEGER NOT NULL,
|
|
23
|
+
created_at INTEGER NOT NULL,
|
|
24
|
+
updated_at INTEGER NOT NULL
|
|
25
|
+
)
|
|
26
|
+
`);
|
|
27
|
+
|
|
28
|
+
raw.exec(/*sql*/ `
|
|
29
|
+
CREATE INDEX IF NOT EXISTS idx_time_contexts_scope_active_until
|
|
30
|
+
ON time_contexts (scope_id, active_until)
|
|
31
|
+
`);
|
|
32
|
+
|
|
33
|
+
// -- open_loops: unresolved items the brief should surface --
|
|
34
|
+
raw.exec(/*sql*/ `
|
|
35
|
+
CREATE TABLE IF NOT EXISTS open_loops (
|
|
36
|
+
id TEXT PRIMARY KEY,
|
|
37
|
+
scope_id TEXT NOT NULL,
|
|
38
|
+
summary TEXT NOT NULL,
|
|
39
|
+
status TEXT NOT NULL DEFAULT 'open',
|
|
40
|
+
source TEXT NOT NULL,
|
|
41
|
+
due_at INTEGER,
|
|
42
|
+
surfaced_at INTEGER,
|
|
43
|
+
created_at INTEGER NOT NULL,
|
|
44
|
+
updated_at INTEGER NOT NULL
|
|
45
|
+
)
|
|
46
|
+
`);
|
|
47
|
+
|
|
48
|
+
raw.exec(/*sql*/ `
|
|
49
|
+
CREATE INDEX IF NOT EXISTS idx_open_loops_scope_status_due
|
|
50
|
+
ON open_loops (scope_id, status, due_at)
|
|
51
|
+
`);
|
|
52
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import type { DrizzleDb } from "../db-connection.js";
|
|
2
|
+
import { getSqliteFrom } from "../db-connection.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Create the memory archive tables (memory_observations, memory_chunks,
|
|
6
|
+
* memory_episodes) with prefetch indexes on scopeId, conversationId, and
|
|
7
|
+
* createdAt.
|
|
8
|
+
*
|
|
9
|
+
* All statements use IF NOT EXISTS / IF NOT EXISTS guards so the migration
|
|
10
|
+
* is safe to re-run on every startup.
|
|
11
|
+
*/
|
|
12
|
+
export function migrateMemoryArchiveTables(database: DrizzleDb): void {
|
|
13
|
+
const raw = getSqliteFrom(database);
|
|
14
|
+
|
|
15
|
+
// -- memory_observations --------------------------------------------------
|
|
16
|
+
raw.exec(/*sql*/ `
|
|
17
|
+
CREATE TABLE IF NOT EXISTS memory_observations (
|
|
18
|
+
id TEXT PRIMARY KEY,
|
|
19
|
+
scope_id TEXT NOT NULL DEFAULT 'default',
|
|
20
|
+
conversation_id TEXT NOT NULL REFERENCES conversations(id) ON DELETE CASCADE,
|
|
21
|
+
message_id TEXT REFERENCES messages(id) ON DELETE SET NULL,
|
|
22
|
+
role TEXT NOT NULL,
|
|
23
|
+
content TEXT NOT NULL,
|
|
24
|
+
modality TEXT NOT NULL DEFAULT 'text',
|
|
25
|
+
source TEXT,
|
|
26
|
+
created_at INTEGER NOT NULL
|
|
27
|
+
)
|
|
28
|
+
`);
|
|
29
|
+
|
|
30
|
+
raw.exec(/*sql*/ `
|
|
31
|
+
CREATE INDEX IF NOT EXISTS idx_memory_observations_scope_id
|
|
32
|
+
ON memory_observations (scope_id)
|
|
33
|
+
`);
|
|
34
|
+
|
|
35
|
+
raw.exec(/*sql*/ `
|
|
36
|
+
CREATE INDEX IF NOT EXISTS idx_memory_observations_conversation_id
|
|
37
|
+
ON memory_observations (conversation_id)
|
|
38
|
+
`);
|
|
39
|
+
|
|
40
|
+
raw.exec(/*sql*/ `
|
|
41
|
+
CREATE INDEX IF NOT EXISTS idx_memory_observations_created_at
|
|
42
|
+
ON memory_observations (created_at)
|
|
43
|
+
`);
|
|
44
|
+
|
|
45
|
+
// -- memory_chunks --------------------------------------------------------
|
|
46
|
+
raw.exec(/*sql*/ `
|
|
47
|
+
CREATE TABLE IF NOT EXISTS memory_chunks (
|
|
48
|
+
id TEXT PRIMARY KEY,
|
|
49
|
+
scope_id TEXT NOT NULL DEFAULT 'default',
|
|
50
|
+
observation_id TEXT NOT NULL REFERENCES memory_observations(id) ON DELETE CASCADE,
|
|
51
|
+
content TEXT NOT NULL,
|
|
52
|
+
token_estimate INTEGER NOT NULL,
|
|
53
|
+
content_hash TEXT NOT NULL,
|
|
54
|
+
created_at INTEGER NOT NULL
|
|
55
|
+
)
|
|
56
|
+
`);
|
|
57
|
+
|
|
58
|
+
raw.exec(/*sql*/ `
|
|
59
|
+
CREATE INDEX IF NOT EXISTS idx_memory_chunks_scope_id
|
|
60
|
+
ON memory_chunks (scope_id)
|
|
61
|
+
`);
|
|
62
|
+
|
|
63
|
+
raw.exec(/*sql*/ `
|
|
64
|
+
CREATE INDEX IF NOT EXISTS idx_memory_chunks_observation_id
|
|
65
|
+
ON memory_chunks (observation_id)
|
|
66
|
+
`);
|
|
67
|
+
|
|
68
|
+
raw.exec(/*sql*/ `
|
|
69
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_memory_chunks_content_hash
|
|
70
|
+
ON memory_chunks (scope_id, content_hash)
|
|
71
|
+
`);
|
|
72
|
+
|
|
73
|
+
raw.exec(/*sql*/ `
|
|
74
|
+
CREATE INDEX IF NOT EXISTS idx_memory_chunks_created_at
|
|
75
|
+
ON memory_chunks (created_at)
|
|
76
|
+
`);
|
|
77
|
+
|
|
78
|
+
// -- memory_episodes ------------------------------------------------------
|
|
79
|
+
raw.exec(/*sql*/ `
|
|
80
|
+
CREATE TABLE IF NOT EXISTS memory_episodes (
|
|
81
|
+
id TEXT PRIMARY KEY,
|
|
82
|
+
scope_id TEXT NOT NULL DEFAULT 'default',
|
|
83
|
+
conversation_id TEXT NOT NULL REFERENCES conversations(id) ON DELETE CASCADE,
|
|
84
|
+
title TEXT NOT NULL,
|
|
85
|
+
summary TEXT NOT NULL,
|
|
86
|
+
token_estimate INTEGER NOT NULL,
|
|
87
|
+
source TEXT,
|
|
88
|
+
start_at INTEGER NOT NULL,
|
|
89
|
+
end_at INTEGER NOT NULL,
|
|
90
|
+
created_at INTEGER NOT NULL,
|
|
91
|
+
updated_at INTEGER NOT NULL
|
|
92
|
+
)
|
|
93
|
+
`);
|
|
94
|
+
|
|
95
|
+
raw.exec(/*sql*/ `
|
|
96
|
+
CREATE INDEX IF NOT EXISTS idx_memory_episodes_scope_id
|
|
97
|
+
ON memory_episodes (scope_id)
|
|
98
|
+
`);
|
|
99
|
+
|
|
100
|
+
raw.exec(/*sql*/ `
|
|
101
|
+
CREATE INDEX IF NOT EXISTS idx_memory_episodes_conversation_id
|
|
102
|
+
ON memory_episodes (conversation_id)
|
|
103
|
+
`);
|
|
104
|
+
|
|
105
|
+
raw.exec(/*sql*/ `
|
|
106
|
+
CREATE INDEX IF NOT EXISTS idx_memory_episodes_created_at
|
|
107
|
+
ON memory_episodes (created_at)
|
|
108
|
+
`);
|
|
109
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { DrizzleDb } from "../db-connection.js";
|
|
2
|
+
import { getSqliteFrom } from "../db-connection.js";
|
|
3
|
+
|
|
4
|
+
export function migrateMemoryReducerCheckpoints(database: DrizzleDb): void {
|
|
5
|
+
const raw = getSqliteFrom(database);
|
|
6
|
+
const columns = [
|
|
7
|
+
"memory_reduced_through_message_id TEXT",
|
|
8
|
+
"memory_dirty_tail_since_message_id TEXT",
|
|
9
|
+
"memory_last_reduced_at INTEGER",
|
|
10
|
+
];
|
|
11
|
+
|
|
12
|
+
for (const column of columns) {
|
|
13
|
+
try {
|
|
14
|
+
raw.exec(`ALTER TABLE conversations ADD COLUMN ${column}`);
|
|
15
|
+
} catch {
|
|
16
|
+
// Column already exists — nothing to do.
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -126,6 +126,9 @@ export { migrateRenameThreadStartersCheckpoints } from "./181-rename-thread-star
|
|
|
126
126
|
export { migrateOAuthProvidersDisplayMetadata } from "./182-oauth-providers-display-metadata.js";
|
|
127
127
|
export { migrateConversationForkLineage } from "./183-add-conversation-fork-lineage.js";
|
|
128
128
|
export { migrateLlmRequestLogProvider } from "./184-llm-request-log-provider.js";
|
|
129
|
+
export { migrateMemoryBriefState } from "./185-memory-brief-state.js";
|
|
130
|
+
export { migrateMemoryArchiveTables } from "./186-memory-archive.js";
|
|
131
|
+
export { migrateMemoryReducerCheckpoints } from "./187-memory-reducer-checkpoints.js";
|
|
129
132
|
export {
|
|
130
133
|
MIGRATION_REGISTRY,
|
|
131
134
|
type MigrationRegistryEntry,
|
|
@@ -20,7 +20,7 @@ export interface QdrantClientConfig {
|
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
export interface QdrantPointPayload {
|
|
23
|
-
target_type: "segment" | "item" | "summary" | "media";
|
|
23
|
+
target_type: "segment" | "item" | "summary" | "observation" | "chunk" | "episode" | "media";
|
|
24
24
|
target_id: string;
|
|
25
25
|
text: string;
|
|
26
26
|
kind?: string;
|
|
@@ -230,7 +230,7 @@ export class VellumQdrantClient {
|
|
|
230
230
|
}
|
|
231
231
|
|
|
232
232
|
async upsert(
|
|
233
|
-
targetType: "segment" | "item" | "summary" | "media",
|
|
233
|
+
targetType: "segment" | "item" | "summary" | "observation" | "chunk" | "episode" | "media",
|
|
234
234
|
targetId: string,
|
|
235
235
|
vector: number[],
|
|
236
236
|
payload: Omit<QdrantPointPayload, "target_type" | "target_id">,
|
|
@@ -324,8 +324,11 @@ export class VellumQdrantClient {
|
|
|
324
324
|
async searchWithFilter(
|
|
325
325
|
vector: number[],
|
|
326
326
|
limit: number,
|
|
327
|
-
targetTypes: Array<
|
|
327
|
+
targetTypes: Array<
|
|
328
|
+
"segment" | "item" | "summary" | "media" | "chunk" | "episode"
|
|
329
|
+
>,
|
|
328
330
|
excludeMessageIds?: string[],
|
|
331
|
+
scopeIds?: string[],
|
|
329
332
|
): Promise<QdrantSearchResult[]> {
|
|
330
333
|
const mustConditions: Array<Record<string, unknown>> = [
|
|
331
334
|
{
|
|
@@ -346,12 +349,24 @@ export class VellumQdrantClient {
|
|
|
346
349
|
},
|
|
347
350
|
{
|
|
348
351
|
key: "target_type",
|
|
349
|
-
match: { any: ["segment", "summary", "media"] },
|
|
352
|
+
match: { any: ["segment", "summary", "media", "chunk"] },
|
|
350
353
|
},
|
|
351
354
|
],
|
|
352
355
|
});
|
|
353
356
|
}
|
|
354
357
|
|
|
358
|
+
// Scope filtering: accept points whose memory_scope_id matches one of the
|
|
359
|
+
// allowed scopes, OR points that lack the field entirely (legacy data).
|
|
360
|
+
// Post-query DB filtering remains as defense-in-depth for legacy points.
|
|
361
|
+
if (scopeIds && scopeIds.length > 0) {
|
|
362
|
+
mustConditions.push({
|
|
363
|
+
should: [
|
|
364
|
+
{ key: "memory_scope_id", match: { any: scopeIds } },
|
|
365
|
+
{ is_empty: { key: "memory_scope_id" } },
|
|
366
|
+
],
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
|
|
355
370
|
const mustNotConditions: Array<Record<string, unknown>> = [
|
|
356
371
|
{ key: "_meta", match: { value: true } },
|
|
357
372
|
];
|
|
@@ -561,6 +576,10 @@ export class VellumQdrantClient {
|
|
|
561
576
|
field_name: "modality",
|
|
562
577
|
field_schema: "keyword",
|
|
563
578
|
}),
|
|
579
|
+
this.client.createPayloadIndex(this.collection, {
|
|
580
|
+
field_name: "memory_scope_id",
|
|
581
|
+
field_schema: "keyword",
|
|
582
|
+
}),
|
|
564
583
|
]);
|
|
565
584
|
}
|
|
566
585
|
|